@delorenj/pjangler 1.2.18 → 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.
Files changed (57) hide show
  1. package/dist/index.js +884 -295
  2. package/dist/mcp-server.js +892 -303
  3. package/package.json +8 -2
  4. package/templates/commonproject/AGENTS.md +3 -3
  5. package/templates/commonproject/README.md +11 -12
  6. package/templates/commonproject/copier.yml +11 -31
  7. package/templates/commonproject/template/.agents/hooks/README.md +13 -26
  8. package/templates/commonproject/template/.agents/hooks/lib/local-config.sh +4 -14
  9. package/templates/commonproject/template/.agents/hooks/sync.py +5 -6
  10. package/templates/commonproject/template/.agents/local.example.json +2 -8
  11. package/templates/commonproject/template/.agents/skills.json +6 -0
  12. package/templates/commonproject/template/.project.json.jinja +9 -13
  13. package/templates/commonproject/template/mise.toml.jinja +9 -16
  14. package/templates/hermes-agent/README.md +9 -9
  15. package/templates/hermes-agent/config.example.toml +1 -66
  16. package/templates/hermes-agent/copier.yml +4 -3
  17. package/templates/hermes-agent/docs/architecture.md +9 -12
  18. package/templates/hermes-agent/docs/fleet-control-plane/README.md +1 -1
  19. package/templates/hermes-agent/docs/operations.md +6 -5
  20. package/templates/hermes-agent/docs/sentinel/README.md +9 -7
  21. package/templates/hermes-agent/docs/sentinel/architecture.md +1 -2
  22. package/templates/hermes-agent/docs/sentinel/development.md +12 -13
  23. package/templates/hermes-agent/docs/sentinel/providers.md +34 -15
  24. package/templates/hermes-agent/install-local.sh +15 -14
  25. package/templates/hermes-agent/runtime-scaffold/README.md +1 -1
  26. package/templates/hermes-agent/runtime-scaffold/bloodbank-consumer.py +46 -14
  27. package/templates/hermes-agent/runtime-scaffold/memories/MEMORY.md +2 -2
  28. package/templates/hermes-agent/scripts/fleet-sync.sh +1 -64
  29. package/templates/hermes-agent/template/.gitignore.jinja +0 -2
  30. package/templates/hermes-agent/template/.runtime-scaffold/README.md +1 -1
  31. package/templates/hermes-agent/template/.runtime-scaffold/bloodbank-consumer.py +43 -9
  32. package/templates/hermes-agent/template/.runtime-scaffold/memories/MEMORY.md +2 -2
  33. package/templates/hermes-agent/template/.scripts/01-config.sh +0 -1
  34. package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +0 -9
  35. package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +3 -22
  36. package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +0 -22
  37. package/templates/hermes-agent/template/.scripts/40-plane.sh +51 -0
  38. package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +59 -21
  39. package/templates/hermes-agent/template/.scripts/60-bloodbank.sh +2 -1
  40. package/templates/hermes-agent/template/.scripts/70-systemd.sh +1 -10
  41. package/templates/hermes-agent/template/.scripts/_lib.sh +3 -61
  42. package/templates/hermes-agent/template/.scripts/config.example.toml +0 -5
  43. package/templates/hermes-agent/template/.scripts/heartbeat.sh +33 -66
  44. package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +5 -9
  45. package/templates/hermes-agent/template/.scripts/momo-wip-lock.py +137 -0
  46. package/templates/hermes-agent/template/.scripts/providers/linear.sh +176 -0
  47. package/templates/hermes-agent/template/.scripts/providers/plane.sh +9 -29
  48. package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md +2 -2
  49. package/templates/hermes-agent/template/.scripts/sentinel/docs/continuous-ticket-orchestration.md +1 -33
  50. package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +22 -27
  51. package/templates/hermes-agent/template/SOUL.md.jinja +49 -30
  52. package/templates/hermes-agent/template/role.yaml.jinja +35 -4
  53. package/templates/hermes-agent/tests/test_bloodbank_consumer_contract.py +138 -0
  54. package/templates/commonproject/template/.mise/scripts/link-project-skills-to-clis.sh +0 -110
  55. package/templates/commonproject/template/.mise/scripts/unlink-project-skills-from-clis.sh +0 -45
  56. package/templates/hermes-agent/docs/bloodbank-gateway.md +0 -57
  57. package/templates/hermes-agent/docs/fleet-control-plane/n8n-service-hub.md +0 -60
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,33 +1486,261 @@ 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";
1498
+ import YAML2 from "yaml";
1499
+
1500
+ // src/project/RegistryStore.ts
1501
+ import { Pool } from "pg";
1311
1502
  import YAML from "yaml";
1503
+ function pgRegistryConfigFromEnv(env2 = process.env) {
1504
+ return {
1505
+ host: env2.PGHOST || "localhost",
1506
+ port: parseInt(env2.PGPORT || "5432", 10),
1507
+ user: env2.PGUSER || "delorenj",
1508
+ password: env2.PGPASSWORD || "",
1509
+ database: env2.PGDATABASE || "33god"
1510
+ };
1511
+ }
1512
+ var PgRegistryStore = class {
1513
+ pool;
1514
+ constructor(config) {
1515
+ this.pool = new Pool({
1516
+ host: config.host,
1517
+ port: config.port,
1518
+ user: config.user,
1519
+ password: config.password,
1520
+ database: config.database
1521
+ });
1522
+ }
1523
+ async load() {
1524
+ const client = await this.pool.connect();
1525
+ try {
1526
+ const { rows } = await client.query(
1527
+ `SELECT p.id, p.name, p.description, p.slug, p.status,
1528
+ p.source_artifacts, p.template, p.automation,
1529
+ p.created_at, p.updated_at,
1530
+ r.id AS repo_id, r.local_path
1531
+ FROM public.projects p
1532
+ LEFT JOIN public.repos r ON r.project_id = p.id
1533
+ WHERE p.slug IS NOT NULL`
1534
+ );
1535
+ const projects = {};
1536
+ for (const row of rows) {
1537
+ const slug = row.slug;
1538
+ const ticketProvider = await this.loadTicketProvider(client, row.id);
1539
+ const agents = await this.loadAgents(client, row.id, slug);
1540
+ projects[slug] = {
1541
+ name: row.name ?? "",
1542
+ slug,
1543
+ repo_path: row.local_path ?? "",
1544
+ description: row.description ?? "",
1545
+ status: row.status ?? "planned",
1546
+ source_artifacts: row.source_artifacts ?? [],
1547
+ template: row.template ?? {
1548
+ commonproject: { enabled: false, primary_language: "python" }
1549
+ },
1550
+ ticket_provider: ticketProvider,
1551
+ agents,
1552
+ automation: row.automation ?? void 0,
1553
+ created_at: row.created_at.toISOString(),
1554
+ updated_at: row.updated_at.toISOString()
1555
+ };
1556
+ }
1557
+ const registry = {
1558
+ schema_version: PROJECT_REGISTRY_SCHEMA_VERSION,
1559
+ projects
1560
+ };
1561
+ validateProjectRegistry(registry);
1562
+ return registry;
1563
+ } finally {
1564
+ client.release();
1565
+ }
1566
+ }
1567
+ async save(registry) {
1568
+ validateProjectRegistry(registry);
1569
+ const client = await this.pool.connect();
1570
+ try {
1571
+ await client.query("BEGIN");
1572
+ for (const [slug, record] of Object.entries(registry.projects)) {
1573
+ await this.upsertInTx(client, slug, record);
1574
+ }
1575
+ await client.query("COMMIT");
1576
+ } catch (err) {
1577
+ await client.query("ROLLBACK");
1578
+ throw err;
1579
+ } finally {
1580
+ client.release();
1581
+ }
1582
+ }
1583
+ async upsert(slug, record) {
1584
+ const client = await this.pool.connect();
1585
+ try {
1586
+ await client.query("BEGIN");
1587
+ await this.upsertInTx(client, slug, record);
1588
+ await client.query("COMMIT");
1589
+ } catch (err) {
1590
+ await client.query("ROLLBACK");
1591
+ throw err;
1592
+ } finally {
1593
+ client.release();
1594
+ }
1595
+ }
1596
+ async getBySlug(slug) {
1597
+ const registry = await this.load();
1598
+ return registry.projects[slug];
1599
+ }
1600
+ async getByRepoPath(repoPath) {
1601
+ const registry = await this.load();
1602
+ return Object.values(registry.projects).find(
1603
+ (p6) => p6.repo_path === repoPath
1604
+ );
1605
+ }
1606
+ async close() {
1607
+ await this.pool.end();
1608
+ }
1609
+ // --- private helpers ---
1610
+ async upsertInTx(client, slug, record) {
1611
+ if (!slug) throw new Error("PgRegistryStore.upsert: slug is required");
1612
+ const projectResult = await client.query(
1613
+ `INSERT INTO public.projects (name, description, slug, status, source_artifacts, template, automation)
1614
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
1615
+ ON CONFLICT (slug) WHERE slug IS NOT NULL
1616
+ DO UPDATE SET
1617
+ name = EXCLUDED.name,
1618
+ description = EXCLUDED.description,
1619
+ status = EXCLUDED.status,
1620
+ source_artifacts = EXCLUDED.source_artifacts,
1621
+ template = EXCLUDED.template,
1622
+ automation = EXCLUDED.automation
1623
+ RETURNING id`,
1624
+ [
1625
+ record.name,
1626
+ record.description,
1627
+ slug,
1628
+ record.status,
1629
+ JSON.stringify(record.source_artifacts),
1630
+ JSON.stringify(record.template),
1631
+ record.automation ? JSON.stringify(record.automation) : null
1632
+ ]
1633
+ );
1634
+ const projectId = projectResult.rows[0]?.id;
1635
+ if (!projectId) throw new Error(`Failed to upsert project: ${slug}`);
1636
+ await client.query(
1637
+ `INSERT INTO public.repos (project_id, local_path)
1638
+ VALUES ($1, $2)
1639
+ ON CONFLICT (local_path)
1640
+ DO UPDATE SET project_id = EXCLUDED.project_id
1641
+ RETURNING id`,
1642
+ [projectId, record.repo_path]
1643
+ );
1644
+ const repoResult = await client.query(
1645
+ `SELECT id FROM public.repos WHERE project_id = $1 AND local_path = $2`,
1646
+ [projectId, record.repo_path]
1647
+ );
1648
+ const repoId = repoResult.rows[0]?.id;
1649
+ if (!repoId) throw new Error(`Failed to find repo for project ${slug} at path ${record.repo_path}`);
1650
+ await this.upsertTicketProvider(client, projectId, repoId, record.ticket_provider);
1651
+ await client.query(
1652
+ `DELETE FROM public.project_agents WHERE project_id = $1`,
1653
+ [projectId]
1654
+ );
1655
+ for (const [agentKey, agent] of Object.entries(record.agents)) {
1656
+ await client.query(
1657
+ `INSERT INTO public.project_agents (repo_id, project_id, agent_key, role, role_dir, provisioning_state)
1658
+ VALUES ($1, $2, $3, $4, $5, $6)`,
1659
+ [repoId, projectId, agentKey, agent.role, agent.role_dir ?? null, agent.provisioning_state]
1660
+ );
1661
+ }
1662
+ }
1663
+ async loadTicketProvider(client, projectId) {
1664
+ const { rows } = await client.query(
1665
+ `SELECT provider_type, workspace, identifier, board_id, state
1666
+ FROM public.project_ticket_boards
1667
+ WHERE project_id = $1
1668
+ LIMIT 1`,
1669
+ [projectId]
1670
+ );
1671
+ if (!rows.length) {
1672
+ return { type: "plane", workspace: "33god", identifier: "", board_id: "", state: "planned" };
1673
+ }
1674
+ const row = rows[0];
1675
+ return {
1676
+ type: row.provider_type,
1677
+ workspace: row.workspace ?? void 0,
1678
+ identifier: row.identifier ?? void 0,
1679
+ board_id: row.board_id ?? void 0,
1680
+ state: row.state ?? void 0
1681
+ };
1682
+ }
1683
+ async loadAgents(client, projectId, slug) {
1684
+ const { rows } = await client.query(
1685
+ `SELECT agent_key, role, role_dir, provisioning_state
1686
+ FROM public.project_agents
1687
+ WHERE project_id = $1`,
1688
+ [projectId]
1689
+ );
1690
+ const agents = {};
1691
+ for (const row of rows) {
1692
+ agents[row.agent_key] = {
1693
+ role: row.role,
1694
+ provisioning_state: row.provisioning_state,
1695
+ role_dir: row.role_dir ?? void 0
1696
+ };
1697
+ }
1698
+ return agents;
1699
+ }
1700
+ async upsertTicketProvider(client, projectId, repoId, tp) {
1701
+ await client.query(
1702
+ `DELETE FROM public.project_ticket_boards WHERE project_id = $1`,
1703
+ [projectId]
1704
+ );
1705
+ await client.query(
1706
+ `INSERT INTO public.project_ticket_boards
1707
+ (repo_id, project_id, provider_type, workspace, identifier, board_id, state)
1708
+ VALUES ($1, $2, $3, $4, $5, $6, $7)`,
1709
+ [
1710
+ repoId,
1711
+ projectId,
1712
+ tp.type,
1713
+ tp.workspace ?? null,
1714
+ tp.identifier ?? null,
1715
+ tp.board_id ?? null,
1716
+ tp.state ?? null
1717
+ ]
1718
+ );
1719
+ }
1720
+ };
1721
+ var PJ_REGISTRY_PG_ENV = "PJ_REGISTRY_PG";
1722
+ function isPgRegistryEnabled(env2 = process.env) {
1723
+ return env2[PJ_REGISTRY_PG_ENV] === "1" || env2[PJ_REGISTRY_PG_ENV] === "true";
1724
+ }
1725
+
1726
+ // src/project/index.ts
1312
1727
  var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
1313
1728
  var PROJECT_SOURCE_SKILL_ROOTS_ENV = "PJ_SOURCE_SKILL_ROOTS";
1314
1729
  var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
1315
1730
  var DEFAULT_SOURCE_SKILL_ROOTS = [
1316
1731
  "/home/delorenj/code/skillex/all-skills",
1317
- join9(homedir3(), ".agents", "skills"),
1318
- join9(homedir3(), ".codex", "skills")
1732
+ join11(homedir3(), ".agents", "skills"),
1733
+ join11(homedir3(), ".codex", "skills")
1319
1734
  ];
1320
1735
  function projectRegistryPath(env2 = process.env) {
1321
- 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"));
1322
1737
  }
1323
1738
  function emptyProjectRegistry() {
1324
1739
  return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
1325
1740
  }
1326
1741
  function loadProjectRegistry(path = projectRegistryPath()) {
1327
- if (!existsSync7(path)) return emptyProjectRegistry();
1328
- const raw = YAML.parse(readFileSync3(path, "utf8"));
1742
+ if (!existsSync8(path)) return emptyProjectRegistry();
1743
+ const raw = YAML2.parse(readFileSync4(path, "utf8"));
1329
1744
  if (raw == null) return emptyProjectRegistry();
1330
1745
  if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
1331
1746
  const registry = raw;
@@ -1340,7 +1755,7 @@ function saveProjectRegistry(registry, path = projectRegistryPath()) {
1340
1755
  validateProjectRegistry(registry);
1341
1756
  mkdirSync4(dirname4(path), { recursive: true });
1342
1757
  const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
1343
- writeFileSync4(temp, YAML.stringify(registry, { lineWidth: 0 }), "utf8");
1758
+ writeFileSync5(temp, YAML2.stringify(registry, { lineWidth: 0 }), "utf8");
1344
1759
  renameSync(temp, path);
1345
1760
  }
1346
1761
  function validateProjectRegistry(registry) {
@@ -1422,7 +1837,7 @@ function resolveAgentHooksLayer(input, env2 = process.env) {
1422
1837
  const override = env2.PJ_AGENT_HOOKS_LAYER;
1423
1838
  if (override === "0" || override === "false") return false;
1424
1839
  if (override === "1" || override === "true") return true;
1425
- return !existsSync7(join9(homedir3(), ".agents", "hooks"));
1840
+ return !existsSync8(join11(homedir3(), ".agents", "hooks"));
1426
1841
  }
1427
1842
  function jsonStable(value) {
1428
1843
  return JSON.stringify(value);
@@ -1453,12 +1868,12 @@ function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
1453
1868
  if (!sourceSkill) return void 0;
1454
1869
  const expanded = expandHome(sourceSkill);
1455
1870
  const direct = resolve(expanded);
1456
- if (existsSync7(direct)) return direct;
1871
+ if (existsSync8(direct)) return direct;
1457
1872
  const name = basename2(sourceSkill);
1458
1873
  const roots = sourceSkillRoots(env2);
1459
1874
  for (const root of roots) {
1460
- const candidate = join9(root, name);
1461
- if (existsSync7(candidate)) return candidate;
1875
+ const candidate = join11(root, name);
1876
+ if (existsSync8(candidate)) return candidate;
1462
1877
  }
1463
1878
  const searched = roots.length ? ` Searched roots: ${roots.join(", ")}.` : "";
1464
1879
  const hint = `${searched} Add project-specific roots with ${PROJECT_SOURCE_SKILL_ROOTS_ENV}.`;
@@ -1539,7 +1954,7 @@ function planProjectInit(input) {
1539
1954
  }));
1540
1955
  }
1541
1956
  actions.push(
1542
- { kind: "project.write-manifest", path: join9(targetDir, ".project.json"), manifest },
1957
+ { kind: "project.write-manifest", path: join11(targetDir, ".project.json"), manifest },
1543
1958
  {
1544
1959
  kind: "ticket-provider.create-or-link",
1545
1960
  enabled: live,
@@ -1567,7 +1982,7 @@ function planProjectInit(input) {
1567
1982
  );
1568
1983
  return { ok: true, apply, dryRun: !apply, live, registryPath: registryPath2, project, manifest, actions };
1569
1984
  }
1570
- function executeProjectInitPlan(plan) {
1985
+ async function executeProjectInitPlan(plan) {
1571
1986
  const logs = [];
1572
1987
  const errors = [];
1573
1988
  const changedFiles = [];
@@ -1592,7 +2007,7 @@ function executeProjectInitPlan(plan) {
1592
2007
  }
1593
2008
  if (result.status !== 0) {
1594
2009
  errors.push(`copier exited with status ${result.status ?? "unknown"}`);
1595
- if (existsSync7(action.targetDir)) changedFiles.push(action.targetDir);
2010
+ if (existsSync8(action.targetDir)) changedFiles.push(action.targetDir);
1596
2011
  break;
1597
2012
  }
1598
2013
  changedFiles.push(action.targetDir);
@@ -1600,9 +2015,9 @@ function executeProjectInitPlan(plan) {
1600
2015
  mkdirSync4(dirname4(action.path), { recursive: true });
1601
2016
  const next = `${JSON.stringify(action.manifest, null, 2)}
1602
2017
  `;
1603
- const current = existsSync7(action.path) ? readFileSync3(action.path, "utf8") : void 0;
2018
+ const current = existsSync8(action.path) ? readFileSync4(action.path, "utf8") : void 0;
1604
2019
  if (current !== next) {
1605
- writeFileSync4(action.path, next, "utf8");
2020
+ writeFileSync5(action.path, next, "utf8");
1606
2021
  changedFiles.push(action.path);
1607
2022
  }
1608
2023
  } else if (action.kind === "registry.upsert") {
@@ -1618,6 +2033,16 @@ function executeProjectInitPlan(plan) {
1618
2033
  registry.projects[pendingRegistryAction.slug] = pendingRegistryAction.project;
1619
2034
  saveProjectRegistry(registry, pendingRegistryAction.registryPath);
1620
2035
  changedFiles.push(pendingRegistryAction.registryPath);
2036
+ if (isPgRegistryEnabled()) {
2037
+ try {
2038
+ const pgStore = new PgRegistryStore(pgRegistryConfigFromEnv());
2039
+ await pgStore.upsert(pendingRegistryAction.slug, pendingRegistryAction.project);
2040
+ await pgStore.close();
2041
+ logs.push("registry: PG dual-write complete");
2042
+ } catch (pgErr) {
2043
+ logs.push(`registry: PG dual-write failed (yaml is authoritative): ${pgErr instanceof Error ? pgErr.message : pgErr}`);
2044
+ }
2045
+ }
1621
2046
  }
1622
2047
  }
1623
2048
  return { ok: errors.length === 0, plan, logs, errors, changedFiles };
@@ -1695,16 +2120,16 @@ function doctorProjectRegistry(registryPath2 = projectRegistryPath(), slug) {
1695
2120
  const registry = loadProjectRegistry(registryPath2);
1696
2121
  const projects = slug ? [[slug, getProject(registry, slug)]] : Object.entries(registry.projects);
1697
2122
  for (const [projectSlug, project] of projects) {
1698
- if (!existsSync7(project.repo_path)) {
2123
+ if (!existsSync8(project.repo_path)) {
1699
2124
  issues.push({ level: "warn", slug: projectSlug, message: `repo_path does not exist: ${project.repo_path}` });
1700
2125
  } else if (!statSync(project.repo_path).isDirectory()) {
1701
2126
  issues.push({ level: "error", slug: projectSlug, message: `repo_path is not a directory: ${project.repo_path}` });
1702
2127
  } else {
1703
- const manifestPath = join9(project.repo_path, ".project.json");
1704
- 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" });
1705
2130
  }
1706
2131
  for (const artifact of project.source_artifacts) {
1707
- if (artifact.path && !existsSync7(artifact.path)) {
2132
+ if (artifact.path && !existsSync8(artifact.path)) {
1708
2133
  issues.push({ level: "warn", slug: projectSlug, message: `source artifact missing: ${artifact.path}` });
1709
2134
  }
1710
2135
  }
@@ -1717,7 +2142,7 @@ function doctorProjectRegistry(registryPath2 = projectRegistryPath(), slug) {
1717
2142
  };
1718
2143
  }
1719
2144
  function buildCommonProjectCopierAction(input) {
1720
- const templateDir = join9(input.pjanglerRoot, "templates", "commonproject");
2145
+ const templateDir = join11(input.pjanglerRoot, "templates", "commonproject");
1721
2146
  const data = {
1722
2147
  project_name: input.projectName,
1723
2148
  project_description: input.projectDescription ?? "",
@@ -1746,7 +2171,7 @@ function buildCommonProjectCopierAction(input) {
1746
2171
  function resolvePjanglerRoot() {
1747
2172
  let dir = dirname4(new URL(import.meta.url).pathname);
1748
2173
  while (dir !== dirname4(dir)) {
1749
- 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;
1750
2175
  dir = dirname4(dir);
1751
2176
  }
1752
2177
  return resolve(process.cwd());
@@ -1778,7 +2203,7 @@ function validateProjectRecord(project, key) {
1778
2203
  }
1779
2204
  function expandHome(path) {
1780
2205
  if (path === "~") return homedir3();
1781
- if (path.startsWith("~/")) return join9(homedir3(), path.slice(2));
2206
+ if (path.startsWith("~/")) return join11(homedir3(), path.slice(2));
1782
2207
  return path;
1783
2208
  }
1784
2209
  function isRecord(value) {
@@ -1795,16 +2220,16 @@ function resolveTemplateRoot() {
1795
2220
  try {
1796
2221
  let dir = dirname5(fileURLToPath2(import.meta.url));
1797
2222
  for (let i = 0; i < 8; i++) {
1798
- candidates.push(join10(dir, "templates", "commonproject", "template"));
2223
+ candidates.push(join12(dir, "templates", "commonproject", "template"));
1799
2224
  const parent = dirname5(dir);
1800
2225
  if (parent === dir) break;
1801
2226
  dir = parent;
1802
2227
  }
1803
2228
  } catch {
1804
2229
  }
1805
- candidates.push(join10(homedir4(), "code", "pjangler", "templates", "commonproject", "template"));
2230
+ candidates.push(join12(homedir4(), "code", "pjangler", "templates", "commonproject", "template"));
1806
2231
  for (const c of candidates) {
1807
- if (existsSync8(join10(c, ".agents", "hooks", "hooks.master.json"))) return c;
2232
+ if (existsSync9(join12(c, ".agents", "hooks", "hooks.master.json"))) return c;
1808
2233
  }
1809
2234
  throw new Error(
1810
2235
  "Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
@@ -1823,18 +2248,17 @@ var CopyAgentHooksTree = class extends Command {
1823
2248
  }
1824
2249
  const items = [
1825
2250
  { rel: ".agents/hooks", dir: true },
2251
+ { rel: ".agents/skills.json", dir: false },
1826
2252
  { rel: ".agents/local.example.json", dir: false },
1827
- { rel: ".mise/scripts/link-project-skills-to-clis.sh", dir: false },
1828
- { rel: ".mise/scripts/unlink-project-skills-from-clis.sh", dir: false },
1829
2253
  { rel: ".mise/scripts/hindsight-setup.sh", dir: false }
1830
2254
  ];
1831
2255
  const created = [];
1832
2256
  const skipped = [];
1833
2257
  for (const { rel, dir } of items) {
1834
- const src = join10(templateRoot, rel);
1835
- const dest = join10(this.context.targetDir, rel);
1836
- if (!existsSync8(src)) continue;
1837
- 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) {
1838
2262
  skipped.push(rel);
1839
2263
  continue;
1840
2264
  }
@@ -1860,25 +2284,21 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
1860
2284
  if (!resolveAgentHooksLayer()) {
1861
2285
  return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
1862
2286
  }
1863
- const misePath = join10(this.context.targetDir, "mise.toml");
1864
- if (!existsSync8(misePath)) {
2287
+ const misePath = join12(this.context.targetDir, "mise.toml");
2288
+ if (!existsSync9(misePath)) {
1865
2289
  return {
1866
2290
  success: false,
1867
2291
  message: "\u26A0\uFE0F No mise.toml found \u2014 run `pjangler init mise` first, then re-run."
1868
2292
  };
1869
2293
  }
1870
- let content = readFileSync4(misePath, "utf8");
2294
+ let content = readFileSync5(misePath, "utf8");
1871
2295
  if (content.includes(_WireMiseAgentHooks.MARKER)) {
1872
2296
  return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
1873
2297
  }
1874
2298
  const cr = _WireMiseAgentHooks.CR;
1875
- const enterAdds = [
1876
- ` "${cr}/.mise/scripts/link-project-skills-to-clis.sh",`,
1877
- ` "${cr}/.agents/hooks/sync.py --install --quiet",`
1878
- ].join("\n");
2299
+ const enterAdds = [` "sync-skills.py --scope project",`, ` "${cr}/.agents/hooks/sync.py --install --quiet",`].join("\n");
1879
2300
  const leaveBlock = [
1880
2301
  "leave = [",
1881
- ` "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh",`,
1882
2302
  ` "${cr}/.agents/hooks/sync.py --uninstall --quiet",`,
1883
2303
  "]"
1884
2304
  ].join("\n");
@@ -1895,7 +2315,6 @@ ${enterAdds}${close}`;
1895
2315
  content = content.replace(leaveRe, (_m, head, close) => {
1896
2316
  const sep = /[,[]\s*$/.test(head) ? "" : ",";
1897
2317
  return `${head}${sep}
1898
- "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh",
1899
2318
  "${cr}/.agents/hooks/sync.py --uninstall --quiet",${close}`;
1900
2319
  });
1901
2320
  } else {
@@ -1908,6 +2327,14 @@ ${leaveBlock}`);
1908
2327
  "",
1909
2328
  _WireMiseAgentHooks.MARKER + " (generated \u2014 see .agents/hooks/README.md)",
1910
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]]",
1911
2338
  'patterns = [".agents/hooks/hooks.master.json"]',
1912
2339
  'task = "hooks-sync"',
1913
2340
  "",
@@ -1923,18 +2350,6 @@ ${leaveBlock}`);
1923
2350
  'description = "Remove per-user agent-hook injections (codex/kimi/hermes)"',
1924
2351
  `run = "${cr}/.agents/hooks/sync.py --uninstall"`,
1925
2352
  "",
1926
- "[tasks.link-project-skills-to-clis]",
1927
- 'description = "Fan .agents/skills out to each agent CLI (honors local.json)"',
1928
- `run = "${cr}/.mise/scripts/link-project-skills-to-clis.sh"`,
1929
- "",
1930
- "[tasks.unlink-project-skills-from-clis]",
1931
- 'description = "Remove project skill symlinks from shared per-CLI dirs"',
1932
- `run = "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh"`,
1933
- "",
1934
- "[tasks.skills-relink]",
1935
- 'description = "Re-fan the project skill set to all CLIs"',
1936
- `run = "${cr}/.mise/scripts/link-project-skills-to-clis.sh"`,
1937
- "",
1938
2353
  "[tasks.hindsight-setup]",
1939
2354
  `description = "Provision this dev's shared project Hindsight key from 1Password into .env"`,
1940
2355
  `run = "${cr}/.mise/scripts/hindsight-setup.sh"`,
@@ -1943,7 +2358,7 @@ ${leaveBlock}`);
1943
2358
  ""
1944
2359
  ].join("\n");
1945
2360
  content = content.replace(/\n*$/, "\n") + appended;
1946
- if (!this.context.dryRun) writeFileSync5(misePath, content);
2361
+ if (!this.context.dryRun) writeFileSync6(misePath, content);
1947
2362
  if (wiredHooks) {
1948
2363
  return { success: true, message: this.formatMessage("\u2705 Wired mise.toml ([hooks] enter/leave + tasks)") };
1949
2364
  }
@@ -1952,8 +2367,8 @@ ${leaveBlock}`);
1952
2367
  message: this.formatMessage(
1953
2368
  `\u2705 Added agent-hooks tasks to mise.toml.
1954
2369
  \u26A0\uFE0F Could not find a [hooks].enter array to extend \u2014 add these to your [hooks] block manually:
1955
- enter += "${cr}/.mise/scripts/link-project-skills-to-clis.sh", "${cr}/.agents/hooks/sync.py --install --quiet"
1956
- 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"`
1957
2372
  )
1958
2373
  };
1959
2374
  }
@@ -1968,10 +2383,25 @@ var AgentHooksRecipe = class extends Recipe {
1968
2383
  printNextSteps() {
1969
2384
  console.log("\u{1FA9D} Agent-hooks layer installed!");
1970
2385
  console.log(" Next steps:");
1971
- console.log(" 1. mise run hooks-sync # generate .claude/settings.json + inject codex/kimi/hermes");
1972
- console.log(" 2. git add .claude/settings.json .agents/hooks && commit (codex/kimi/hermes are per-dev)");
1973
- console.log(" 3. mise run hindsight-setup # set HINDSIGHT_OP_KEY_REF to your 1Password item first");
1974
- 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");
1975
2405
  }
1976
2406
  };
1977
2407
 
@@ -2014,6 +2444,12 @@ var RECIPE_REGISTRY = {
2014
2444
  description: "Retrofit the project-scoped agent-hooks + skill fan-out layer (Claude/Codex/Kimi/Hermes hooks via mise enter/leave)",
2015
2445
  class: AgentHooksRecipe,
2016
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"]
2017
2453
  }
2018
2454
  };
2019
2455
  var COMMAND_REGISTRY = {
@@ -2082,6 +2518,12 @@ var COMMAND_REGISTRY = {
2082
2518
  description: "Create .env.example file",
2083
2519
  group: "environment",
2084
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
2085
2527
  }
2086
2528
  };
2087
2529
  function getRecipeNames() {
@@ -2116,29 +2558,39 @@ function createRecipe(name, context) {
2116
2558
  import { cancel as cancel2, multiselect, text as text2, isCancel as isCancel5 } from "@clack/prompts";
2117
2559
 
2118
2560
  // src/parity/index.ts
2119
- 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";
2120
- 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";
2121
2563
  import { fileURLToPath as fileURLToPath3 } from "node:url";
2122
2564
  import { homedir as homedir5 } from "node:os";
2123
2565
  import { spawnSync as spawnSync6 } from "node:child_process";
2124
- import YAML2 from "yaml";
2566
+ import YAML3 from "yaml";
2125
2567
  var LINK_AGENTFILES_SCRIPT = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'";
2126
2568
  var OP_INJECT_SCRIPT = "op inject -i .env.op > .env";
2569
+ var SYNC_SKILLS_SCRIPT = "sync-skills.py --scope project";
2127
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";
2128
2572
  var HOOKS_COMMENT_HEADER = `# This block will handle the linking of
2129
2573
  # agent files to the main AGENTS.md file.
2130
2574
  #
2131
2575
  # TODO: Ensure this works for all levels of nesting.
2132
2576
  # i.e. All linked agent files MUST be siblings at
2133
2577
  # any given level of nesting.`;
2134
- 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];
2135
2579
  var LINK_AGENTFILES_WATCH_TASK_BLOCK = `[[watch_files]]
2136
2580
  patterns = ["AGENTS.md"]
2137
2581
  task = "link-agentfiles"
2138
2582
 
2583
+ [[watch_files]]
2584
+ patterns = [".agents/skills.json"]
2585
+ task = "skills-sync"
2586
+
2139
2587
  [tasks.link-agentfiles]
2140
2588
  description = "Symlink all agent files to AGENTS.md"
2141
- 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"`;
2142
2594
  var VERSIONING_BLOCK = `# >>> mise-versioning >>> (managed block \u2014 do not edit by hand; re-run init to update)
2143
2595
  [tasks."version"]
2144
2596
  description = "Print the current version (vX.Y.Z)"
@@ -2168,7 +2620,7 @@ run = "'{{config_root}}/.mise/scripts/versioning.sh' sync"
2168
2620
  function resolvePjanglerRoot2() {
2169
2621
  let dir = dirname6(fileURLToPath3(import.meta.url));
2170
2622
  while (dir !== dirname6(dir)) {
2171
- 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"))) {
2172
2624
  return dir;
2173
2625
  }
2174
2626
  dir = dirname6(dir);
@@ -2179,17 +2631,17 @@ function normalizeNewlines(value) {
2179
2631
  return value.replace(/\r\n/g, "\n");
2180
2632
  }
2181
2633
  function readText(path) {
2182
- return normalizeNewlines(readFileSync5(path, "utf8"));
2634
+ return normalizeNewlines(readFileSync6(path, "utf8"));
2183
2635
  }
2184
2636
  function safeReadText(path) {
2185
- return existsSync9(path) ? readText(path) : null;
2637
+ return existsSync10(path) ? readText(path) : null;
2186
2638
  }
2187
2639
  function ensureParent(path) {
2188
2640
  mkdirSync6(dirname6(path), { recursive: true });
2189
2641
  }
2190
2642
  function writeText(path, content) {
2191
2643
  ensureParent(path);
2192
- writeFileSync6(path, content);
2644
+ writeFileSync7(path, content);
2193
2645
  }
2194
2646
  function tryParseJson(text3) {
2195
2647
  if (!text3) return null;
@@ -2206,7 +2658,7 @@ function titleCaseSlug(slug) {
2206
2658
  return slug.split(/[-_]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
2207
2659
  }
2208
2660
  function readSymlinkTarget(path) {
2209
- if (!existsSync9(path)) return null;
2661
+ if (!existsSync10(path)) return null;
2210
2662
  try {
2211
2663
  return readlinkSync(path);
2212
2664
  } catch {
@@ -2214,7 +2666,7 @@ function readSymlinkTarget(path) {
2214
2666
  }
2215
2667
  }
2216
2668
  function ensureSymlink(path, target, dryRun) {
2217
- if (existsSync9(path)) {
2669
+ if (existsSync10(path)) {
2218
2670
  const stat = lstatSync(path);
2219
2671
  if (stat.isSymbolicLink()) {
2220
2672
  const current = readSymlinkTarget(path);
@@ -2231,11 +2683,11 @@ function ensureSymlink(path, target, dryRun) {
2231
2683
  return { changed: true };
2232
2684
  }
2233
2685
  function bootstrapAgentsFile(repoRoot, dryRun) {
2234
- const agentsPath = join11(repoRoot, "AGENTS.md");
2235
- if (existsSync9(agentsPath)) return { changedFiles: [], details: [] };
2686
+ const agentsPath = join13(repoRoot, "AGENTS.md");
2687
+ if (existsSync10(agentsPath)) return { changedFiles: [], details: [] };
2236
2688
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
2237
- const source = join11(repoRoot, file);
2238
- if (!existsSync9(source)) continue;
2689
+ const source = join13(repoRoot, file);
2690
+ if (!existsSync10(source)) continue;
2239
2691
  const stat = lstatSync(source);
2240
2692
  if (stat.isSymbolicLink()) continue;
2241
2693
  if (stat.isFile()) {
@@ -2244,8 +2696,8 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
2244
2696
  }
2245
2697
  return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
2246
2698
  }
2247
- const readmePath = join11(repoRoot, "README.md");
2248
- if (existsSync9(readmePath)) {
2699
+ const readmePath = join13(repoRoot, "README.md");
2700
+ if (existsSync10(readmePath)) {
2249
2701
  const stat = lstatSync(readmePath);
2250
2702
  if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
2251
2703
  if (!dryRun) copyFileSync(readmePath, agentsPath);
@@ -2284,12 +2736,12 @@ function yamlGet(text3, keyPath) {
2284
2736
  return "";
2285
2737
  }
2286
2738
  function discoverRoles(repoRoot) {
2287
- const rolesDir = join11(repoRoot, "agents", "hermes");
2288
- if (!existsSync9(rolesDir)) return [];
2739
+ const rolesDir = join13(repoRoot, "agents", "hermes");
2740
+ if (!existsSync10(rolesDir)) return [];
2289
2741
  return readdirSync2(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
2290
- const roleDir = join11(rolesDir, entry.name);
2291
- const roleYamlPath = join11(roleDir, "role.yaml");
2292
- 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;
2293
2745
  const text3 = readText(roleYamlPath);
2294
2746
  const runtimeRepoRaw = yamlGet(text3, "runtime.github_repo");
2295
2747
  return {
@@ -2317,7 +2769,7 @@ function discoverRoles(repoRoot) {
2317
2769
  }).filter((value) => Boolean(value));
2318
2770
  }
2319
2771
  function registryPath(homeDir) {
2320
- return join11(homeDir, ".hermes", "agents-registry.yaml");
2772
+ return join13(homeDir, ".hermes", "agents-registry.yaml");
2321
2773
  }
2322
2774
  function systemctlUser(args) {
2323
2775
  const result = spawnSync6("systemctl", ["--user", ...args], { encoding: "utf8" });
@@ -2328,8 +2780,8 @@ function systemctlUser(args) {
2328
2780
  };
2329
2781
  }
2330
2782
  function templateScript(ctx, name) {
2331
- const source = join11(ctx.pjanglerRoot, ".mise", "scripts", name);
2332
- return existsSync9(source) ? readText(source) : void 0;
2783
+ const source = join13(ctx.pjanglerRoot, ".mise", "scripts", name);
2784
+ return existsSync10(source) ? readText(source) : void 0;
2333
2785
  }
2334
2786
  function templateVersioningScript(ctx) {
2335
2787
  return templateScript(ctx, "versioning.sh");
@@ -2341,8 +2793,8 @@ function resolveAgentHooksLayer2(ctx) {
2341
2793
  const override = process.env.PJ_AGENT_HOOKS_LAYER;
2342
2794
  if (override === "0" || override === "false") return false;
2343
2795
  if (override === "1" || override === "true") return true;
2344
- if (existsSync9(join11(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
2345
- 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"));
2346
2798
  }
2347
2799
  function evaluateMiseConditionals(template, agentHooksLayer) {
2348
2800
  const out = [];
@@ -2372,19 +2824,36 @@ function renderGeneratedProjectMiseToml(ctx, template) {
2372
2824
  return evaluateMiseConditionals(template, resolveAgentHooksLayer2(ctx)).replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
2373
2825
  }
2374
2826
  function ensureMiseTomlFromTemplate(ctx, changedFiles) {
2375
- const targetPath = join11(ctx.repoRoot, "mise.toml");
2376
- if (existsSync9(targetPath)) return false;
2377
- const sourcePath = join11(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
2378
- 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;
2379
2831
  changedFiles.push(targetPath);
2380
2832
  if (!ctx.dryRun) {
2381
2833
  writeText(targetPath, renderGeneratedProjectMiseToml(ctx, readText(sourcePath)));
2382
2834
  }
2383
2835
  return true;
2384
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
+ }
2385
2854
  function templateVersionFilesConf(ctx, repoRoot) {
2386
- const packageJson = join11(repoRoot, "package.json");
2387
- 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";
2388
2857
  }
2389
2858
  function replaceOrAppendManagedBlock(text3, startMarker, block, beforePattern) {
2390
2859
  if (startMarker.test(text3)) {
@@ -2408,7 +2877,7 @@ var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/herm
2408
2877
  function requiredMisePathEntries(ctx) {
2409
2878
  const required = [...BASE_MISE_PATH_ENTRIES];
2410
2879
  for (const candidate of CONDITIONAL_HERMES_PATHS) {
2411
- 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);
2412
2881
  }
2413
2882
  return required;
2414
2883
  }
@@ -2509,6 +2978,9 @@ function stripTomlStringsAndComments(line) {
2509
2978
  function isManagedHookEntry(value) {
2510
2979
  const trimmed = value.trim();
2511
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;
2512
2984
  return /link-agentfiles\.sh'?\s*$/.test(trimmed);
2513
2985
  }
2514
2986
  function normalizeHookScript(script) {
@@ -2611,12 +3083,17 @@ function upsertLinkAgentfilesHooks(text3) {
2611
3083
  function upsertLinkAgentfilesBlock(text3, ctx) {
2612
3084
  const withPath = upsertMisePath(text3, requiredMisePathEntries(ctx));
2613
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 });
2614
3090
  cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /AGENTS\.md/, { includePrecedingComments: false });
3091
+ cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /\.agents\/skills\.json/, { includePrecedingComments: false });
2615
3092
  cleaned = upsertLinkAgentfilesHooks(cleaned);
2616
3093
  return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
2617
3094
  }
2618
3095
  function readProjectJson(ctx) {
2619
- return tryParseJson(safeReadText(join11(ctx.repoRoot, ".project.json")));
3096
+ return tryParseJson(safeReadText(join13(ctx.repoRoot, ".project.json")));
2620
3097
  }
2621
3098
  function boolSetting(value, fallback) {
2622
3099
  if (typeof value === "boolean") return value;
@@ -2691,12 +3168,12 @@ function canonicalProjectJson(ctx) {
2691
3168
  };
2692
3169
  }
2693
3170
  function projectJsonFinding(ctx) {
2694
- const projectPath = join11(ctx.repoRoot, ".project.json");
2695
- const planeJsonPath = join11(ctx.repoRoot, ".plane.json");
3171
+ const projectPath = join13(ctx.repoRoot, ".project.json");
3172
+ const planeJsonPath = join13(ctx.repoRoot, ".plane.json");
2696
3173
  const details = [];
2697
3174
  const data = readProjectJson(ctx);
2698
3175
  const roles = discoverRoles(ctx.repoRoot);
2699
- if (!existsSync9(projectPath)) {
3176
+ if (!existsSync10(projectPath)) {
2700
3177
  return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json missing", details: [], fixable: true };
2701
3178
  }
2702
3179
  if (!data) {
@@ -2731,7 +3208,7 @@ function projectJsonFinding(ctx) {
2731
3208
  for (const key of ["enabled", "grace_hours", "auto_review"]) {
2732
3209
  if (!(key in reconcile)) details.push(`automation.reconcile.${key} missing`);
2733
3210
  }
2734
- 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");
2735
3212
  return {
2736
3213
  id: "sot.project-json",
2737
3214
  title: "Canonical .project.json",
@@ -2812,17 +3289,17 @@ exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" HERMES_OAUTH
2812
3289
  `.replace(/\u0010/g, "$");
2813
3290
  }
2814
3291
  function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
2815
- if (!existsSync9(sourceDir)) return;
3292
+ if (!existsSync10(sourceDir)) return;
2816
3293
  mkdirSync6(targetDir, { recursive: true });
2817
3294
  for (const entry of readdirSync2(sourceDir, { withFileTypes: true })) {
2818
- const sourcePath = join11(sourceDir, entry.name);
3295
+ const sourcePath = join13(sourceDir, entry.name);
2819
3296
  if (skip?.(sourcePath)) continue;
2820
- const targetPath = join11(targetDir, entry.name);
3297
+ const targetPath = join13(targetDir, entry.name);
2821
3298
  if (entry.isDirectory()) {
2822
3299
  copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
2823
3300
  continue;
2824
3301
  }
2825
- if (existsSync9(targetPath)) continue;
3302
+ if (existsSync10(targetPath)) continue;
2826
3303
  changedFiles.push(targetPath);
2827
3304
  if (!dryRun) {
2828
3305
  ensureParent(targetPath);
@@ -2831,7 +3308,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
2831
3308
  }
2832
3309
  }
2833
3310
  function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
2834
- const gitmodulesPath = join11(repoRoot, ".gitmodules");
3311
+ const gitmodulesPath = join13(repoRoot, ".gitmodules");
2835
3312
  const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
2836
3313
  const owner = role.runtimeOwner || "delorenj";
2837
3314
  const block = `[submodule "agents/hermes/${role.role}/runtime"]
@@ -2998,10 +3475,10 @@ function runBmadInstall(repoRoot) {
2998
3475
  return { ok: true };
2999
3476
  }
3000
3477
  function readInstalledBmadVersion(repoRoot) {
3001
- const raw = safeReadText(join11(repoRoot, "_bmad", "_config", "manifest.yaml"));
3478
+ const raw = safeReadText(join13(repoRoot, "_bmad", "_config", "manifest.yaml"));
3002
3479
  if (!raw) return void 0;
3003
3480
  try {
3004
- const parsed = YAML2.parse(raw);
3481
+ const parsed = YAML3.parse(raw);
3005
3482
  const version = parsed?.installation?.version;
3006
3483
  return typeof version === "string" && version.trim() ? version.trim() : void 0;
3007
3484
  } catch {
@@ -3009,8 +3486,8 @@ function readInstalledBmadVersion(repoRoot) {
3009
3486
  }
3010
3487
  }
3011
3488
  function bmadCachePath(homeDir) {
3012
- const cacheRoot = process.env.XDG_CACHE_HOME?.trim() || join11(homeDir, ".cache");
3013
- 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");
3014
3491
  }
3015
3492
  function readBmadDistTagsCache(homeDir) {
3016
3493
  const raw = safeReadText(bmadCachePath(homeDir));
@@ -3053,7 +3530,7 @@ function resolveBmadDistTags(homeDir) {
3053
3530
  try {
3054
3531
  const path = bmadCachePath(homeDir);
3055
3532
  mkdirSync6(dirname6(path), { recursive: true });
3056
- writeFileSync6(path, JSON.stringify({ fetchedAt: Date.now(), distTags: fetched }, null, 2));
3533
+ writeFileSync7(path, JSON.stringify({ fetchedAt: Date.now(), distTags: fetched }, null, 2));
3057
3534
  } catch {
3058
3535
  }
3059
3536
  return { distTags: fetched, stale: false };
@@ -3098,14 +3575,14 @@ var RULES = [
3098
3575
  id: "mise.config-root",
3099
3576
  title: "mise config_root + AGENTS link hooks",
3100
3577
  audit: (ctx) => {
3101
- const misePath = join11(ctx.repoRoot, "mise.toml");
3102
- if (!existsSync9(misePath)) {
3578
+ const misePath = join13(ctx.repoRoot, "mise.toml");
3579
+ if (!existsSync10(misePath)) {
3103
3580
  return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
3104
3581
  }
3105
3582
  const text3 = readText(misePath);
3106
3583
  const details = [];
3107
- const linkAgentfilesPath = join11(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
3108
- 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");
3109
3586
  const pathValues = [...(text3.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
3110
3587
  const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
3111
3588
  if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
@@ -3123,10 +3600,10 @@ var RULES = [
3123
3600
  };
3124
3601
  },
3125
3602
  migrate: (ctx, finding) => {
3126
- const path = join11(ctx.repoRoot, "mise.toml");
3603
+ const path = join13(ctx.repoRoot, "mise.toml");
3127
3604
  const changedFiles = [];
3128
3605
  const details = [];
3129
- if (!existsSync9(path)) {
3606
+ if (!existsSync10(path)) {
3130
3607
  if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
3131
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: [] };
3132
3609
  }
@@ -3142,7 +3619,7 @@ var RULES = [
3142
3619
  if (!ctx.dryRun) writeText(path, next);
3143
3620
  text3 = next;
3144
3621
  }
3145
- const linkAgentfilesPath = join11(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
3622
+ const linkAgentfilesPath = join13(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
3146
3623
  const expectedScript = templateLinkAgentfilesScript(ctx);
3147
3624
  if (expectedScript === void 0) {
3148
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: [] };
@@ -3151,7 +3628,7 @@ var RULES = [
3151
3628
  changedFiles.push(linkAgentfilesPath);
3152
3629
  if (!ctx.dryRun) {
3153
3630
  writeText(linkAgentfilesPath, expectedScript);
3154
- chmodSync2(linkAgentfilesPath, 493);
3631
+ chmodSync4(linkAgentfilesPath, 493);
3155
3632
  }
3156
3633
  }
3157
3634
  return {
@@ -3169,13 +3646,13 @@ var RULES = [
3169
3646
  title: "managed mise versioning block",
3170
3647
  audit: (ctx) => {
3171
3648
  const details = [];
3172
- const misePath = join11(ctx.repoRoot, "mise.toml");
3173
- const versioningPath = join11(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
3174
- 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");
3175
3652
  const text3 = safeReadText(misePath);
3176
3653
  if (!text3?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
3177
- if (!existsSync9(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
3178
- 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");
3179
3656
  return {
3180
3657
  id: "mise.versioning",
3181
3658
  title: "managed mise versioning block",
@@ -3188,8 +3665,8 @@ var RULES = [
3188
3665
  migrate: (ctx, finding) => {
3189
3666
  const changedFiles = [];
3190
3667
  const details = [];
3191
- const misePath = join11(ctx.repoRoot, "mise.toml");
3192
- if (!existsSync9(misePath)) {
3668
+ const misePath = join13(ctx.repoRoot, "mise.toml");
3669
+ if (!existsSync10(misePath)) {
3193
3670
  if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
3194
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: [] };
3195
3672
  }
@@ -3213,7 +3690,7 @@ var RULES = [
3213
3690
  if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
3214
3691
  if (!ctx.dryRun) writeText(misePath, nextMise);
3215
3692
  }
3216
- const versioningPath = join11(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
3693
+ const versioningPath = join13(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
3217
3694
  const expectedScript = templateVersioningScript(ctx);
3218
3695
  if (expectedScript === void 0) {
3219
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: [] };
@@ -3222,10 +3699,10 @@ var RULES = [
3222
3699
  changedFiles.push(versioningPath);
3223
3700
  if (!ctx.dryRun) {
3224
3701
  writeText(versioningPath, expectedScript);
3225
- chmodSync2(versioningPath, 493);
3702
+ chmodSync4(versioningPath, 493);
3226
3703
  }
3227
3704
  }
3228
- const manifestPath = join11(ctx.repoRoot, ".mise", "version-files.conf");
3705
+ const manifestPath = join13(ctx.repoRoot, ".mise", "version-files.conf");
3229
3706
  const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
3230
3707
  if (safeReadText(manifestPath) !== expectedManifest) {
3231
3708
  changedFiles.push(manifestPath);
@@ -3241,13 +3718,125 @@ var RULES = [
3241
3718
  };
3242
3719
  }
3243
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
+ },
3244
3833
  {
3245
3834
  id: "sot.agent-symlinks",
3246
3835
  title: "AGENTS/CLAUDE/GEMINI symlink contract",
3247
3836
  audit: (ctx) => {
3248
- const agentsPath = join11(ctx.repoRoot, "AGENTS.md");
3249
- if (!existsSync9(agentsPath)) {
3250
- 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)));
3251
3840
  if (fallbackSources.length === 0) {
3252
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 };
3253
3842
  }
@@ -3262,7 +3851,7 @@ var RULES = [
3262
3851
  }
3263
3852
  const details = [];
3264
3853
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
3265
- const full = join11(ctx.repoRoot, file);
3854
+ const full = join13(ctx.repoRoot, file);
3266
3855
  const target = readSymlinkTarget(full);
3267
3856
  if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
3268
3857
  }
@@ -3286,7 +3875,7 @@ var RULES = [
3286
3875
  return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
3287
3876
  }
3288
3877
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
3289
- const full = join11(ctx.repoRoot, file);
3878
+ const full = join13(ctx.repoRoot, file);
3290
3879
  const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
3291
3880
  if (result.blocked) blockedDetails.push(result.blocked);
3292
3881
  if (result.changed) changedFiles.push(full);
@@ -3308,7 +3897,7 @@ var RULES = [
3308
3897
  migrate: (ctx, finding) => {
3309
3898
  const changedFiles = [];
3310
3899
  const details = [];
3311
- const path = join11(ctx.repoRoot, ".project.json");
3900
+ const path = join13(ctx.repoRoot, ".project.json");
3312
3901
  const existing = readProjectJson(ctx) ?? {};
3313
3902
  const canonical = canonicalProjectJson(ctx);
3314
3903
  const merged = { ...existing, ...canonical };
@@ -3318,10 +3907,10 @@ var RULES = [
3318
3907
  changedFiles.push(path);
3319
3908
  if (!ctx.dryRun) writeText(path, expected);
3320
3909
  }
3321
- const planeJson = join11(ctx.repoRoot, ".plane.json");
3322
- if (existsSync9(planeJson)) {
3910
+ const planeJson = join13(ctx.repoRoot, ".plane.json");
3911
+ if (existsSync10(planeJson)) {
3323
3912
  const backup = `${planeJson}.migrated-backup`;
3324
- if (existsSync9(backup)) {
3913
+ if (existsSync10(backup)) {
3325
3914
  details.push(`cannot back up .plane.json because ${relative(ctx.repoRoot, backup)} already exists`);
3326
3915
  } else {
3327
3916
  changedFiles.push(backup);
@@ -3343,8 +3932,8 @@ var RULES = [
3343
3932
  title: ".env.op + gitignore secrets contract",
3344
3933
  audit: (ctx) => {
3345
3934
  const details = [];
3346
- const envOp = safeReadText(join11(ctx.repoRoot, ".env.op"));
3347
- const gitignore = safeReadText(join11(ctx.repoRoot, ".gitignore"));
3935
+ const envOp = safeReadText(join13(ctx.repoRoot, ".env.op"));
3936
+ const gitignore = safeReadText(join13(ctx.repoRoot, ".gitignore"));
3348
3937
  if (!envOp) {
3349
3938
  details.push(".env.op missing");
3350
3939
  } else {
@@ -3370,12 +3959,12 @@ var RULES = [
3370
3959
  migrate: (ctx, finding) => {
3371
3960
  const changedFiles = [];
3372
3961
  const details = [];
3373
- const envOpPath = join11(ctx.repoRoot, ".env.op");
3374
- if (!existsSync9(envOpPath)) {
3962
+ const envOpPath = join13(ctx.repoRoot, ".env.op");
3963
+ if (!existsSync10(envOpPath)) {
3375
3964
  changedFiles.push(envOpPath);
3376
- 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")));
3377
3966
  }
3378
- const gitignorePath = join11(ctx.repoRoot, ".gitignore");
3967
+ const gitignorePath = join13(ctx.repoRoot, ".gitignore");
3379
3968
  const gitignore = safeReadText(gitignorePath) ?? "";
3380
3969
  const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
3381
3970
  # NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
@@ -3402,7 +3991,7 @@ var RULES = [
3402
3991
  title: ".copier-answers.yml provenance + drift report",
3403
3992
  audit: (ctx) => {
3404
3993
  const details = [];
3405
- const path = join11(ctx.repoRoot, ".copier-answers.yml");
3994
+ const path = join13(ctx.repoRoot, ".copier-answers.yml");
3406
3995
  const text3 = safeReadText(path);
3407
3996
  const project = readProjectJson(ctx);
3408
3997
  if (!text3) {
@@ -3433,12 +4022,12 @@ var RULES = [
3433
4022
  const changedFiles = [];
3434
4023
  const project = canonicalProjectJson(ctx);
3435
4024
  const text3 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
3436
- _src_path: ${join11(ctx.pjanglerRoot, "templates", "commonproject")}
4025
+ _src_path: ${join13(ctx.pjanglerRoot, "templates", "commonproject")}
3437
4026
  project_description: ${String(project.project_description)}
3438
4027
  project_name: ${String(project.project_name)}
3439
4028
  ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3440
4029
  `;
3441
- const path = join11(ctx.repoRoot, ".copier-answers.yml");
4030
+ const path = join13(ctx.repoRoot, ".copier-answers.yml");
3442
4031
  if (safeReadText(path) !== text3) {
3443
4032
  changedFiles.push(path);
3444
4033
  if (!ctx.dryRun) writeText(path, text3);
@@ -3457,14 +4046,14 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3457
4046
  id: "bmad.scaffold",
3458
4047
  title: "BMAD modules/docs scaffold",
3459
4048
  audit: (ctx) => {
3460
- const targetRoot = join11(ctx.repoRoot, "_bmad");
4049
+ const targetRoot = join13(ctx.repoRoot, "_bmad");
3461
4050
  const sentinels = [
3462
- join11("core", "config.yaml"),
3463
- join11("config.toml"),
3464
- join11("_config", "manifest.yaml"),
3465
- join11("bmm", "config.yaml")
4051
+ join13("core", "config.yaml"),
4052
+ join13("config.toml"),
4053
+ join13("_config", "manifest.yaml"),
4054
+ join13("bmm", "config.yaml")
3466
4055
  ];
3467
- const missing = sentinels.filter((file) => !existsSync9(join11(targetRoot, file)));
4056
+ const missing = sentinels.filter((file) => !existsSync10(join13(targetRoot, file)));
3468
4057
  return {
3469
4058
  id: "bmad.scaffold",
3470
4059
  title: "BMAD modules/docs scaffold",
@@ -3478,7 +4067,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3478
4067
  const changedFiles = [];
3479
4068
  if (ctx.dryRun) {
3480
4069
  for (const detail of finding.details) {
3481
- changedFiles.push(join11(ctx.repoRoot, detail));
4070
+ changedFiles.push(join13(ctx.repoRoot, detail));
3482
4071
  }
3483
4072
  return {
3484
4073
  id: finding.id,
@@ -3503,8 +4092,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3503
4092
  };
3504
4093
  }
3505
4094
  for (const detail of finding.details) {
3506
- if (existsSync9(join11(ctx.repoRoot, detail))) {
3507
- changedFiles.push(join11(ctx.repoRoot, detail));
4095
+ if (existsSync10(join13(ctx.repoRoot, detail))) {
4096
+ changedFiles.push(join13(ctx.repoRoot, detail));
3508
4097
  }
3509
4098
  }
3510
4099
  return {
@@ -3527,7 +4116,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3527
4116
  id: "bmad.version",
3528
4117
  title: "BMAD version currency",
3529
4118
  status: "skip",
3530
- 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",
3531
4120
  details: [],
3532
4121
  fixable: false
3533
4122
  };
@@ -3582,7 +4171,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3582
4171
  }
3583
4172
  const installed = readInstalledBmadVersion(ctx.repoRoot);
3584
4173
  const available = resolveBmadDistTags(ctx.homeDir)?.distTags?.[BMAD_TARGET_CHANNEL];
3585
- const manifestPath = join11(ctx.repoRoot, "_bmad", "_config", "manifest.yaml");
4174
+ const manifestPath = join13(ctx.repoRoot, "_bmad", "_config", "manifest.yaml");
3586
4175
  if (ctx.dryRun) {
3587
4176
  return {
3588
4177
  id: finding.id,
@@ -3629,11 +4218,11 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3629
4218
  }
3630
4219
  const details = [];
3631
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"]) {
3632
- 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))}`);
3633
4222
  }
3634
- const gitmodules = safeReadText(join11(ctx.repoRoot, ".gitmodules")) ?? "";
4223
+ const gitmodules = safeReadText(join13(ctx.repoRoot, ".gitmodules")) ?? "";
3635
4224
  if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
3636
- if (!profileMetaInheritsDefault(join11(role.roleDir, "runtime", "profile.yaml"))) {
4225
+ if (!profileMetaInheritsDefault(join13(role.roleDir, "runtime", "profile.yaml"))) {
3637
4226
  details.push("runtime/profile.yaml missing inherited default config metadata");
3638
4227
  }
3639
4228
  const registry = safeReadText(registryPath(ctx.homeDir));
@@ -3654,21 +4243,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3654
4243
  if (!role) {
3655
4244
  return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
3656
4245
  }
3657
- const templateRoleDir = join11(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
3658
- writeIfDifferent(join11(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
3659
- writeIfDifferent(join11(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
3660
- writeIfDifferent(join11(role.roleDir, ".gitignore"), readText(join11(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
3661
- copyMissingRecursive(join11(templateRoleDir, ".runtime-scaffold"), join11(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
3662
- copyMissingRecursive(join11(templateRoleDir, ".runtime-scaffold"), join11(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
3663
- copyMissingRecursive(join11(templateRoleDir, ".scripts"), join11(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
3664
- const promptSource = join11(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
3665
- const promptTarget = join11(role.roleDir, ".scripts", "sentinel.prompt.md");
3666
- 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)) {
3667
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);
3668
4257
  writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
3669
4258
  }
3670
4259
  upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
3671
- 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);
3672
4261
  if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
3673
4262
  const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
3674
4263
  if (registryUpdated) details.push(`updated ${registryUpdated}`);
@@ -3700,7 +4289,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3700
4289
  const details = [];
3701
4290
  for (const role of roles) {
3702
4291
  const roleRelDir = relative(ctx.repoRoot, role.roleDir);
3703
- const runtimeRelPath = join11(roleRelDir, "runtime");
4292
+ const runtimeRelPath = join13(roleRelDir, "runtime");
3704
4293
  const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
3705
4294
  cwd: ctx.repoRoot,
3706
4295
  encoding: "utf8"
@@ -3708,8 +4297,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3708
4297
  if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
3709
4298
  details.push(`submodule runtime is tracked in Git index at ${runtimeRelPath}`);
3710
4299
  }
3711
- const gitignorePath = join11(role.roleDir, ".gitignore");
3712
- if (existsSync9(gitignorePath)) {
4300
+ const gitignorePath = join13(role.roleDir, ".gitignore");
4301
+ if (existsSync10(gitignorePath)) {
3713
4302
  const content = safeReadText(gitignorePath) ?? "";
3714
4303
  const lines = content.split(/\r?\n/).map((line) => line.trim());
3715
4304
  if (!lines.includes("runtime/") && !lines.includes("runtime")) {
@@ -3734,7 +4323,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3734
4323
  const details = [];
3735
4324
  for (const role of roles) {
3736
4325
  const roleRelDir = relative(ctx.repoRoot, role.roleDir);
3737
- const runtimeRelPath = join11(roleRelDir, "runtime");
4326
+ const runtimeRelPath = join13(roleRelDir, "runtime");
3738
4327
  const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
3739
4328
  cwd: ctx.repoRoot,
3740
4329
  encoding: "utf8"
@@ -3749,10 +4338,10 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3749
4338
  });
3750
4339
  }
3751
4340
  }
3752
- const gitignorePath = join11(role.roleDir, ".gitignore");
4341
+ const gitignorePath = join13(role.roleDir, ".gitignore");
3753
4342
  let content = "";
3754
4343
  let isIgnored = false;
3755
- if (existsSync9(gitignorePath)) {
4344
+ if (existsSync10(gitignorePath)) {
3756
4345
  content = safeReadText(gitignorePath) ?? "";
3757
4346
  const lines = content.split(/\r?\n/).map((line) => line.trim());
3758
4347
  isIgnored = lines.includes("runtime/") || lines.includes("runtime");
@@ -3819,9 +4408,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3819
4408
  return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
3820
4409
  }
3821
4410
  for (const role of roles) {
3822
- const sysDir = join11(ctx.homeDir, ".config", "systemd", "user");
4411
+ const sysDir = join13(ctx.homeDir, ".config", "systemd", "user");
3823
4412
  const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`];
3824
- const allUnitsPresent = units.every((unit) => existsSync9(join11(sysDir, unit)));
4413
+ const allUnitsPresent = units.every((unit) => existsSync10(join13(sysDir, unit)));
3825
4414
  if (allUnitsPresent) {
3826
4415
  if (ctx.dryRun) {
3827
4416
  details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
@@ -3833,8 +4422,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3833
4422
  }
3834
4423
  continue;
3835
4424
  }
3836
- for (const script of [join11(role.roleDir, ".scripts", "70-systemd.sh")]) {
3837
- if (!script || !existsSync9(script)) continue;
4425
+ for (const script of [join13(role.roleDir, ".scripts", "70-systemd.sh")]) {
4426
+ if (!script || !existsSync10(script)) continue;
3838
4427
  if (ctx.dryRun) {
3839
4428
  details.push(`would run: bash ${script}`);
3840
4429
  } else {
@@ -3861,7 +4450,7 @@ function writeIfDifferent(path, content, dryRun, changedFiles, mode) {
3861
4450
  changedFiles.push(path);
3862
4451
  if (!dryRun) {
3863
4452
  writeText(path, normalized);
3864
- if (mode) chmodSync2(path, mode);
4453
+ if (mode) chmodSync4(path, mode);
3865
4454
  }
3866
4455
  }
3867
4456
  function getParityRuleIds() {
@@ -3973,15 +4562,15 @@ function formatMigrationReport(report) {
3973
4562
  }
3974
4563
 
3975
4564
  // src/utils/version.ts
3976
- import { readFileSync as readFileSync6 } from "node:fs";
3977
- 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";
3978
4567
  import { fileURLToPath as fileURLToPath4 } from "node:url";
3979
4568
  var PJANGLER_VERSION = (() => {
3980
4569
  try {
3981
4570
  let dir = dirname7(fileURLToPath4(import.meta.url));
3982
4571
  for (let i = 0; i < 4; i++) {
3983
4572
  try {
3984
- const raw = readFileSync6(join12(dir, "package.json"), "utf8");
4573
+ const raw = readFileSync7(join14(dir, "package.json"), "utf8");
3985
4574
  return JSON.parse(raw).version ?? "0.0.0";
3986
4575
  } catch {
3987
4576
  const parent = dirname7(dir);
@@ -4024,9 +4613,9 @@ async function promptForRuleIds(rules) {
4024
4613
  return selected;
4025
4614
  }
4026
4615
  function readJson(path) {
4027
- if (!existsSync10(path)) return void 0;
4616
+ if (!existsSync11(path)) return void 0;
4028
4617
  try {
4029
- const parsed = JSON.parse(readFileSync7(path, "utf8"));
4618
+ const parsed = JSON.parse(readFileSync8(path, "utf8"));
4030
4619
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
4031
4620
  } catch {
4032
4621
  return void 0;
@@ -4043,8 +4632,8 @@ function packageNameToProjectName(value) {
4043
4632
  return name.replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()).trim();
4044
4633
  }
4045
4634
  function deriveProjectDefaults(targetDir) {
4046
- const manifest = readJson(join13(targetDir, ".project.json"));
4047
- const pkg = readJson(join13(targetDir, "package.json"));
4635
+ const manifest = readJson(join15(targetDir, ".project.json"));
4636
+ const pkg = readJson(join15(targetDir, "package.json"));
4048
4637
  const name = String(manifest?.project_name ?? "").trim() || packageNameToProjectName(typeof pkg?.name === "string" ? pkg.name : void 0) || packageNameToProjectName(basename4(targetDir)) || "Project";
4049
4638
  const ticketProvider = manifest?.ticket_provider && typeof manifest.ticket_provider === "object" ? manifest.ticket_provider : {};
4050
4639
  return {
@@ -4100,7 +4689,7 @@ function actionNeedsRun(plan, kind, syncMode) {
4100
4689
  if (!action || action.kind !== "project.write-manifest") return false;
4101
4690
  const next = `${JSON.stringify(action.manifest, null, 2)}
4102
4691
  `;
4103
- return !existsSync10(action.path) || readFileSync7(action.path, "utf8") !== next;
4692
+ return !existsSync11(action.path) || readFileSync8(action.path, "utf8") !== next;
4104
4693
  }
4105
4694
  if (kind === "copier.copy.commonproject") return true;
4106
4695
  if (kind === "ticket-provider.create-or-link") return plan.actions.some((action) => action.kind === kind && action.enabled);
@@ -4155,7 +4744,7 @@ async function resolveProjectInitTarget(name, options) {
4155
4744
  if (!targetDir && interactive) {
4156
4745
  const defaultName = name ?? basename4(cwd);
4157
4746
  const promptedName = name ?? await promptTextValue("Project name", packageNameToProjectName(defaultName));
4158
- 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, "-"));
4159
4748
  targetDir = await promptTextValue("Project directory", defaultDir);
4160
4749
  name = promptedName;
4161
4750
  }
@@ -4163,7 +4752,7 @@ async function resolveProjectInitTarget(name, options) {
4163
4752
  if (!name) throw new Error("Project name or --target-dir is required when project init is not run inside a git repo");
4164
4753
  targetDir = resolve3(process.cwd(), name.replace(/[^A-Za-z0-9._-]/g, "") || name.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
4165
4754
  }
4166
- const targetExists = existsSync10(targetDir);
4755
+ const targetExists = existsSync11(targetDir);
4167
4756
  if (targetExists && !statSync2(targetDir).isDirectory()) throw new Error(`Target path is not a directory: ${targetDir}`);
4168
4757
  const targetGitRoot = targetExists ? findGitRoot(targetDir) : void 0;
4169
4758
  const syncMode = Boolean(targetGitRoot && resolve3(targetGitRoot) === resolve3(targetDir));
@@ -4298,7 +4887,7 @@ async function runProjectInit(name, options) {
4298
4887
  }
4299
4888
  return;
4300
4889
  }
4301
- const initResult = selectedPlan.actions.length ? executeProjectInitPlan(selectedPlan) : { ok: true, plan: selectedPlan, logs: [], errors: [], changedFiles: [] };
4890
+ const initResult = selectedPlan.actions.length ? await executeProjectInitPlan(selectedPlan) : { ok: true, plan: selectedPlan, logs: [], errors: [], changedFiles: [] };
4302
4891
  const migrationReport = selection.selectedParityRules.length ? runMigrationForRules(selection.selectedParityRules, target.targetDir, false) : void 0;
4303
4892
  const migrationErrors = migrationReport?.results.filter((result2) => result2.status === "blocked").map((result2) => `${result2.id}: ${result2.summary}`) ?? [];
4304
4893
  const changedFiles = Array.from(/* @__PURE__ */ new Set([