@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
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/mcp-server.ts
4
- import { existsSync as existsSync10, statSync as statSync2 } from "node:fs";
5
- import { basename as basename4, dirname as dirname8, join as join13, resolve as resolve3 } from "node:path";
4
+ import { existsSync as existsSync11, statSync as statSync2 } from "node:fs";
5
+ import { basename as basename4, dirname as dirname8, join as join15, resolve as resolve3 } from "node:path";
6
6
  import { fileURLToPath as fileURLToPath5 } from "node:url";
7
7
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
8
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -285,97 +285,284 @@ var AddMiseCodegraphScript = class extends Command {
285
285
  };
286
286
  }
287
287
  const content = `#!/usr/bin/env bash
288
- # Mise enter hook: ensure the CodeGraph CLI is available and initialize the
289
- # project index. If \`codegraph\` is not installed, this script installs it
290
- # non-interactively into the project-local .mise/bin directory and retries.
291
- #
292
- # This is intended to run from a mise enter hook so onboarding a new host is
293
- # fully automatic.
288
+ # Auto-generated by pjangler
289
+
290
+ REPO_ROOT="$(pwd)"
291
+ PROJECT_NAME="$(basename "$REPO_ROOT")"
292
+ CONTAINER_NAME="codegraph-mcp-$PROJECT_NAME"
293
+ CACHE_DIR="$REPO_ROOT/.codegraph"
294
+
295
+ # Deterministically generate a port based on the repository path
296
+ PORT=$(echo -n "$REPO_ROOT" | md5sum | awk '{print $1}' | tr -d 'a-f' | cut -c1-4)
297
+ # Ensure port is > 1024
298
+ PORT=$(( (PORT % 60000) + 1025 ))
299
+
300
+ if ! docker ps --format '{{.Names}}' | grep -q "^$CONTAINER_NAME$"; then
301
+ echo "\u{1F680} Starting CodeGraph MCP Server on port $PORT..."
302
+
303
+ # Ensure the container isn't lingering in a stopped state
304
+ docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
305
+
306
+ # Run the true Colby McHenry CodeGraph Docker image
307
+ # which we built locally as colbymchenry-codegraph-mcp:latest
308
+ docker run -d \\
309
+ --name "$CONTAINER_NAME" \\
310
+ --restart unless-stopped \\
311
+ -p "$PORT:8045" \\
312
+ -v "$REPO_ROOT:/repo" \\
313
+ colbymchenry-codegraph-mcp:latest >/dev/null
314
+
315
+ echo "\u2705 CodeGraph MCP running in background. SSE URL: http://localhost:$PORT/sse"
316
+ fi
317
+
318
+ # Run init inside the container to ensure the index is bootstrapped.
319
+ # We run this using the standard codegraph CLI inside the container
320
+ docker exec "$CONTAINER_NAME" codegraph init -i /repo >/dev/null 2>&1 || true
321
+
322
+ # Wire up the MCP server to local agents
323
+ WIRE_SCRIPT="$(dirname "$0")/codegraph-wire.sh"
324
+ if [ -x "$WIRE_SCRIPT" ]; then
325
+ "$WIRE_SCRIPT"
326
+ fi
327
+ `;
328
+ this.writeFile(filePath, content);
329
+ if (!this.context.dryRun) {
330
+ chmodSync(join2(this.context.targetDir, filePath), 493);
331
+ }
332
+ return {
333
+ success: true,
334
+ message: this.formatMessage(this.context.dryRun ? "Would create .mise/scripts/codegraph.sh" : "\u2705 Created .mise/scripts/codegraph.sh"),
335
+ filePath
336
+ };
337
+ }
338
+ };
339
+
340
+ // src/commands/AddMiseCodegraphWireScript.ts
341
+ import { chmodSync as chmodSync2 } from "fs";
342
+ import { join as join3 } from "path";
343
+ var AddMiseCodegraphWireScript = class extends Command {
344
+ async invoke() {
345
+ const filePath = ".mise/scripts/codegraph-wire.sh";
346
+ if (this.fileExists(filePath) && !this.context.force) {
347
+ return {
348
+ success: false,
349
+ message: this.formatMessage("\u26A0\uFE0F .mise/scripts/codegraph-wire.sh already exists"),
350
+ filePath
351
+ };
352
+ }
353
+ const content = `#!/usr/bin/env bash
354
+ # Mise enter hook: Auto-wire CodeGraph MCP Server to local agents.
355
+ # Detects Claude Code, Codex, Gemini, Kimi, and OpenCode and configures
356
+ # them to use the local SSE endpoint for the current project.
294
357
 
295
358
  set -euo pipefail
296
359
 
297
360
  REPO_ROOT="\${MISE_PROJECT_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}"
298
- PROJECT_BIN_DIR="$REPO_ROOT/.mise/bin"
299
- mkdir -p "$PROJECT_BIN_DIR"
300
361
 
301
- # Install the CodeGraph CLI into the project-local bin directory.
302
- install_codegraph() {
303
- echo "[mise] codegraph not found. Installing non-interactively..."
304
- export CODEGRAPH_BIN_DIR="$PROJECT_BIN_DIR"
305
- curl -fsSL https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh | sh
362
+ # Compute the same port as the container script
363
+ PORT_HASH=$(echo -n "$REPO_ROOT" | md5sum | awk '{print $1}')
364
+ PORT_DEC=$(printf "%d" "0x\${PORT_HASH:0:4}")
365
+ PORT=$(( 8045 + (PORT_DEC % 955) ))
306
366
 
307
- if [ -x "$PROJECT_BIN_DIR/codegraph" ]; then
308
- export PATH="$PROJECT_BIN_DIR:$PATH"
309
- else
310
- echo "[mise] codegraph install did not place a binary at $PROJECT_BIN_DIR/codegraph" >&2
311
- return 1
367
+ SSE_URL="http://localhost:$PORT/sse"
368
+
369
+ inject_sse() {
370
+ local target="$1"
371
+ local agent="$2"
372
+
373
+ if [ ! -f "$target" ]; then
374
+ mkdir -p "$(dirname "$target")"
375
+ echo '{"mcpServers": {}}' > "$target"
376
+ fi
377
+
378
+ # Ensure the file is valid JSON (fail gracefully if it's garbled)
379
+ if ! jq . "$target" >/dev/null 2>&1; then
380
+ echo "[mise] WARNING: $target is not valid JSON, skipping $agent wiring" >&2
381
+ return
312
382
  fi
383
+
384
+ # Inject or update the codegraph server
385
+ jq --arg url "$SSE_URL" '.mcpServers.codegraph = {"type": "sse", "url": $url}' "$target" > "$target.tmp" && mv "$target.tmp" "$target"
386
+ echo "[mise] Wired CodeGraph SSE for $agent -> $target"
313
387
  }
314
388
 
315
- # Ensure a codegraph binary is available on PATH.
316
- ensure_codegraph() {
317
- if command -v codegraph >/dev/null 2>&1; then
318
- return 0
319
- fi
389
+ # 1. Claude Code (Project-scoped)
390
+ inject_sse "$REPO_ROOT/.claude.json" "Claude Code"
320
391
 
321
- # Check the project-local bin dir first (previous install from this hook).
322
- if [ -x "$PROJECT_BIN_DIR/codegraph" ]; then
323
- export PATH="$PROJECT_BIN_DIR:$PATH"
324
- return 0
325
- fi
392
+ # 2. Codex (Global)
393
+ inject_sse "$HOME/.codex/mcp.json" "Codex"
326
394
 
327
- # Check typical user-level install locations before fetching anything.
328
- for d in "$HOME/.local/bin" "$HOME/.codegraph/current/bin"; do
329
- if [ -x "$d/codegraph" ]; then
330
- export PATH="$d:$PATH"
331
- return 0
332
- fi
333
- done
395
+ # 3. Gemini (Global)
396
+ inject_sse "$HOME/.gemini/config/mcp.json" "Gemini"
334
397
 
335
- install_codegraph
336
- }
398
+ # 4. Kimi (Global)
399
+ inject_sse "$HOME/.kimi-code/mcp.json" "Kimi"
337
400
 
338
- # Attempt to initialize the project graph. If the command is missing, install
339
- # it and retry once.
340
- init_project() {
341
- local err_file
342
- err_file="$(mktemp)"
343
- trap 'rm -f "$err_file"' RETURN
401
+ # 5. OpenCode (Global)
402
+ inject_sse "$HOME/.opencode/mcp.json" "OpenCode"
344
403
 
345
- if codegraph init -i "$REPO_ROOT" 2>"$err_file"; then
346
- return 0
347
- fi
404
+ # 6. Cursor (Global)
405
+ inject_sse "$HOME/.cursor/mcp.json" "Cursor"
348
406
 
349
- # If the failure looks like a missing binary, install and retry.
350
- if grep -qiE 'command not found|not installed|No such file|executable file not found' "$err_file" 2>/dev/null; then
351
- ensure_codegraph
352
- codegraph init -i "$REPO_ROOT"
353
- return 0
354
- fi
407
+ # 7. VSCode native MCP (Global)
408
+ inject_sse "$HOME/.vscode/mcp.json" "VSCode"
355
409
 
356
- cat "$err_file" >&2
357
- return 1
358
- }
410
+ # 8. Cline (VSCode extension)
411
+ inject_sse "$HOME/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" "Cline"
359
412
 
360
- init_project
413
+ # 9. Roo (VSCode extension)
414
+ inject_sse "$HOME/.config/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/mcp_settings.json" "Roo Code"
361
415
  `;
362
416
  this.writeFile(filePath, content);
363
417
  if (!this.context.dryRun) {
364
- chmodSync(join2(this.context.targetDir, filePath), 493);
418
+ chmodSync2(join3(this.context.targetDir, filePath), 493);
365
419
  }
366
420
  return {
367
421
  success: true,
368
- message: this.formatMessage(this.context.dryRun ? "Would create .mise/scripts/codegraph.sh" : "\u2705 Created .mise/scripts/codegraph.sh"),
422
+ message: this.formatMessage(this.context.dryRun ? "Would create .mise/scripts/codegraph-wire.sh" : "\u2705 Created .mise/scripts/codegraph-wire.sh"),
369
423
  filePath
370
424
  };
371
425
  }
372
426
  };
373
427
 
428
+ // src/commands/WireMiseOpInject.ts
429
+ import { join as join4 } from "node:path";
430
+ import { existsSync as existsSync2, readFileSync, writeFileSync as writeFileSync2, chmodSync as chmodSync3 } from "node:fs";
431
+ var WireMiseOpInject = class _WireMiseOpInject extends Command {
432
+ static MARKER = "# pjangler:op-inject";
433
+ static CR = "{{config_root}}";
434
+ async invoke() {
435
+ const scriptPath = ".mise/scripts/inject-op-secrets.sh";
436
+ const fullScriptPath = join4(this.context.targetDir, scriptPath);
437
+ const scriptContent = `#!/usr/bin/env bash
438
+ # Resolve 1Password \`op://\` references in .env.op into .env.secrets, which mise
439
+ # loads via \`_.file\`.
440
+ #
441
+ # Runs from the mise enter hook: fail-open and non-interactive.
442
+ # No .env.op means no 1Password, so this exits before spending anything.
443
+
444
+ set -euo pipefail
445
+
446
+ REPO_ROOT="\${MISE_PROJECT_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}"
447
+ SRC="$REPO_ROOT/.env.op"
448
+ OUT="$REPO_ROOT/.env.secrets"
449
+
450
+ TTL_HOURS="\${OP_INJECT_TTL_HOURS:-12}"
451
+
452
+ [[ -f "$SRC" ]] || exit 0
453
+ command -v op >/dev/null 2>&1 || exit 0
454
+
455
+ if [[ -f "$OUT" && "$SRC" -ot "$OUT" ]]; then
456
+ if [[ "$TTL_HOURS" != "0" ]] && [[ -z "$(find "$OUT" -mmin "+$((TTL_HOURS * 60))" 2>/dev/null)" ]]; then
457
+ exit 0
458
+ fi
459
+ fi
460
+
461
+ op account list >/dev/null 2>&1 || exit 0
462
+
463
+ tmp="$(mktemp "\${OUT}.XXXXXX")"
464
+ trap 'rm -f "$tmp"' EXIT
465
+ chmod 600 "$tmp"
466
+
467
+ if op inject -i "$SRC" -o "$tmp" --force >/dev/null 2>&1; then
468
+ mv "$tmp" "$OUT"
469
+ trap - EXIT
470
+ else
471
+ echo "mise: op inject from .env.op failed \u2014 $(basename "$OUT") unchanged." >&2
472
+ echo " Check 'op signin', or that .env.op's op:// items are readable." >&2
473
+ echo " To disable: rm .env.op" >&2
474
+ fi
475
+ `;
476
+ this.writeFile(scriptPath, scriptContent);
477
+ if (!this.context.dryRun) {
478
+ chmodSync3(fullScriptPath, 493);
479
+ }
480
+ const misePath = join4(this.context.targetDir, "mise.toml");
481
+ if (!existsSync2(misePath)) {
482
+ return {
483
+ success: false,
484
+ message: "\u26A0\uFE0F No mise.toml found \u2014 run `pj init mise` first, then re-run."
485
+ };
486
+ }
487
+ let content = readFileSync(misePath, "utf8");
488
+ if (content.includes(_WireMiseOpInject.MARKER)) {
489
+ return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for op-inject") };
490
+ }
491
+ const cr = _WireMiseOpInject.CR;
492
+ const envRe = /\[env\]\s*\n([\s\S]*?(?=\n\[|$))/;
493
+ if (envRe.test(content)) {
494
+ content = content.replace(envRe, (m) => {
495
+ let block = m;
496
+ if (!block.includes("_.file")) {
497
+ block += `
498
+ _.file = ['.env', '.env.secrets']
499
+ `;
500
+ } else if (!block.includes(".env.secrets")) {
501
+ block = block.replace(/(_\.file\s*=\s*\[)([^\]]*?)(\])/, (m2, prefix, files, suffix) => {
502
+ const added = files.trim() ? `${files}, '.env.secrets'` : `'.env.secrets'`;
503
+ return `${prefix}${added}${suffix}`;
504
+ });
505
+ }
506
+ return block;
507
+ });
508
+ } else {
509
+ const envBlock = `[env]
510
+ _.file = ['.env', '.env.secrets']
511
+
512
+ `;
513
+ if (content.includes("[tools]")) {
514
+ content = content.replace(/(\[tools\][\s\S]*?(?=\n\[|$))/, `$1
515
+ ${envBlock}`);
516
+ } else {
517
+ content = envBlock + content;
518
+ }
519
+ }
520
+ const enterAdds = ` '${cr}/.mise/scripts/inject-op-secrets.sh',`;
521
+ const enterRe = /(enter\s*=\s*\[[\s\S]*?)(\n[ \t]*\])/;
522
+ if (enterRe.test(content)) {
523
+ content = content.replace(enterRe, (_m, head, close) => {
524
+ const sep = /[,[]\s*$/.test(head) ? "" : ",";
525
+ return `${head}${sep}
526
+ ${enterAdds}${close}`;
527
+ });
528
+ } else {
529
+ if (content.includes("[hooks]")) {
530
+ content = content.replace(/\[hooks\]/, `[hooks]
531
+ enter = [
532
+ ${enterAdds}
533
+ ]`);
534
+ } else {
535
+ content += `
536
+ [hooks]
537
+ enter = [
538
+ ${enterAdds}
539
+ ]
540
+ `;
541
+ }
542
+ }
543
+ const appended = [
544
+ "",
545
+ _WireMiseOpInject.MARKER,
546
+ "[tasks.secrets-inject]",
547
+ 'description = "Re-resolve .env.op secrets from 1Password into .env.secrets"',
548
+ `run = "OP_INJECT_TTL_HOURS=0 .mise/scripts/inject-op-secrets.sh"`,
549
+ _WireMiseOpInject.MARKER + ":end",
550
+ ""
551
+ ].join("\n");
552
+ content = content.replace(/\n*$/, "\n") + appended;
553
+ if (!this.context.dryRun) writeFileSync2(misePath, content);
554
+ return {
555
+ success: true,
556
+ message: this.formatMessage("\u2705 Wired mise.toml for op-inject (_.file, [hooks] enter, tasks.secrets-inject)")
557
+ };
558
+ }
559
+ };
560
+
374
561
  // src/recipes/MiseRecipe.ts
375
562
  var MiseRecipe = class extends Recipe {
376
563
  constructor(context) {
377
564
  super(context);
378
- this.addIngredient(AddMiseToml).addIngredient(AddDotenv).addIngredient(AddMiseTasksStructure).addIngredient(AddMiseBaseToml).addIngredient(AddMiseBaseScript).addIngredient(AddMiseCodegraphScript);
565
+ this.addIngredient(AddMiseToml).addIngredient(AddDotenv).addIngredient(AddMiseTasksStructure).addIngredient(AddMiseBaseToml).addIngredient(AddMiseBaseScript).addIngredient(AddMiseCodegraphScript).addIngredient(AddMiseCodegraphWireScript).addIngredient(WireMiseOpInject);
379
566
  }
380
567
  printNextSteps() {
381
568
  console.log("\u{1F389} Mise subsystem initialized successfully!");
@@ -604,34 +791,34 @@ var NodeRecipe = class extends Recipe {
604
791
 
605
792
  // src/commands/hermes/EnsureTemplateConfig.ts
606
793
  import { homedir, platform } from "node:os";
607
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
608
- import { join as join3, dirname as dirname2 } from "node:path";
794
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "node:fs";
795
+ import { join as join5, dirname as dirname2 } from "node:path";
609
796
  function resolveTemplateConfigPath() {
610
797
  const fromEnv = process.env.HERMES_TEMPLATE_CONFIG;
611
798
  if (fromEnv && fromEnv.trim()) return fromEnv.trim();
612
799
  const xdg = process.env.XDG_CONFIG_HOME?.trim();
613
- const base = xdg && xdg.length ? xdg : join3(homedir(), ".config");
614
- return join3(base, "hermes-agent-template", "config.toml");
800
+ const base = xdg && xdg.length ? xdg : join5(homedir(), ".config");
801
+ return join5(base, "hermes-agent-template", "config.toml");
615
802
  }
616
803
  function detectHermesBin(home) {
617
804
  const candidates = [
618
- join3(home, "code", "hermes-agent", "venv", "bin", "hermes"),
619
- join3(home, "code", "hermes-agent", ".venv", "bin", "hermes"),
620
- join3(home, ".local", "bin", "hermes")
805
+ join5(home, "code", "hermes-agent", "venv", "bin", "hermes"),
806
+ join5(home, "code", "hermes-agent", ".venv", "bin", "hermes"),
807
+ join5(home, ".local", "bin", "hermes")
621
808
  ];
622
809
  for (const c of candidates) {
623
- if (existsSync2(c)) return c;
810
+ if (existsSync3(c)) return c;
624
811
  }
625
812
  return candidates[0];
626
813
  }
627
814
  function renderHostConfig() {
628
815
  const home = homedir();
629
816
  const hermesBin = detectHermesBin(home);
630
- const hermesRepo = join3(home, "code", "hermes-agent");
631
- const scaffoldDir = join3(home, "code", "hermes-agent-template", "runtime-scaffold");
632
- const skillsDir = join3(home, ".agents", "skills");
633
- const pmExternalSkillGlobalDir = join3(home, "code", "skillex", "skill-sets", "global", ".system");
634
- const pmExternalSkillBmadDir = join3(home, "code", "skillex", "packs", "bmad", "6.10.2");
817
+ const hermesRepo = join5(home, "code", "hermes-agent");
818
+ const scaffoldDir = join5(home, "code", "hermes-agent-template", "runtime-scaffold");
819
+ const skillsDir = join5(home, ".agents", "skills");
820
+ const pmExternalSkillGlobalDir = join5(home, "code", "skillex", "skill-sets", "global", ".system");
821
+ const pmExternalSkillBmadDir = join5(home, "code", "skillex", "packs", "bmad", "6.10.2");
635
822
  return `# hermes-agent-template \u2014 host configuration
636
823
  # Bootstrapped by \`pjangler config bootstrap\` for $HOME=${home} (platform=${platform()}).
637
824
  #
@@ -680,7 +867,7 @@ var EnsureTemplateConfig = class extends Command {
680
867
  const ctx = this.context;
681
868
  const force = ctx.forceConfig === true || process.env.PJANGLER_FORCE_CONFIG === "1";
682
869
  const path = resolveTemplateConfigPath();
683
- const exists = existsSync2(path);
870
+ const exists = existsSync3(path);
684
871
  if (exists && !force) {
685
872
  console.log(`\u2713 Config present: ${path}`);
686
873
  return { success: true, message: "" };
@@ -691,7 +878,7 @@ var EnsureTemplateConfig = class extends Command {
691
878
  }
692
879
  try {
693
880
  mkdirSync2(dirname2(path), { recursive: true });
694
- writeFileSync2(path, renderHostConfig());
881
+ writeFileSync3(path, renderHostConfig());
695
882
  } catch (err) {
696
883
  const msg = err instanceof Error ? err.message : String(err);
697
884
  return { success: false, message: `\u2717 Failed to write ${path}: ${msg}` };
@@ -703,8 +890,8 @@ var EnsureTemplateConfig = class extends Command {
703
890
  };
704
891
 
705
892
  // src/commands/hermes/PromptForAgentConfig.ts
706
- import { basename, join as join4 } from "node:path";
707
- import { readFileSync } from "node:fs";
893
+ import { basename, join as join6 } from "node:path";
894
+ import { readFileSync as readFileSync2 } from "node:fs";
708
895
  import * as p from "@clack/prompts";
709
896
 
710
897
  // src/commands/hermes/types.ts
@@ -719,7 +906,7 @@ function deriveProfileName(repo, role) {
719
906
  // src/commands/hermes/PromptForAgentConfig.ts
720
907
  function detectTicketProvider(targetDir) {
721
908
  try {
722
- const t = JSON.parse(readFileSync(join4(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
909
+ const t = JSON.parse(readFileSync2(join6(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
723
910
  return t === "plane" || t === "trello" ? t : void 0;
724
911
  } catch {
725
912
  return void 0;
@@ -777,8 +964,8 @@ var PromptForAgentConfig = class extends Command {
777
964
  // src/commands/hermes/RunCopierTemplate.ts
778
965
  import { spawnSync } from "node:child_process";
779
966
  import { homedir as homedir2 } from "node:os";
780
- import { join as join5, dirname as dirname3 } from "node:path";
781
- import { existsSync as existsSync3, mkdirSync as mkdirSync3 } from "node:fs";
967
+ import { join as join7, dirname as dirname3 } from "node:path";
968
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "node:fs";
782
969
  import { fileURLToPath } from "node:url";
783
970
  import * as p2 from "@clack/prompts";
784
971
  function resolveVendoredTemplate(name) {
@@ -789,8 +976,8 @@ function resolveVendoredTemplate(name) {
789
976
  return void 0;
790
977
  }
791
978
  for (let i = 0; i < 8; i++) {
792
- const candidate = join5(dir, "templates", name);
793
- if (existsSync3(join5(candidate, "copier.yml"))) return candidate;
979
+ const candidate = join7(dir, "templates", name);
980
+ if (existsSync4(join7(candidate, "copier.yml"))) return candidate;
794
981
  const parent = dirname3(dir);
795
982
  if (parent === dir) break;
796
983
  dir = parent;
@@ -809,7 +996,7 @@ var RunCopierTemplate = class extends Command {
809
996
  message: "PromptForAgentConfig must run before RunCopierTemplate (targetRepo/role unset)"
810
997
  };
811
998
  }
812
- const roleDir = join5(ctx.targetDir, "agents", "hermes", role);
999
+ const roleDir = join7(ctx.targetDir, "agents", "hermes", role);
813
1000
  ctx.roleDir = roleDir;
814
1001
  ctx.runtimeRepo = `delorenj/agent-hm-${targetRepo}-${role}`;
815
1002
  const which = spawnSync("which", ["copier"], { encoding: "utf8" });
@@ -819,7 +1006,7 @@ var RunCopierTemplate = class extends Command {
819
1006
  message: "\u2717 copier not found on PATH. Install with: `uv tool install copier` or `pip install copier`"
820
1007
  };
821
1008
  }
822
- if (existsSync3(join5(roleDir, "role.yaml")) && !ctx.force) {
1009
+ if (existsSync4(join7(roleDir, "role.yaml")) && !ctx.force) {
823
1010
  if (ctx.yes) {
824
1011
  ctx.force = true;
825
1012
  } else {
@@ -846,9 +1033,9 @@ var RunCopierTemplate = class extends Command {
846
1033
  SKIP_BLOODBANK: ctx.skipBloodbank ? "1" : "0",
847
1034
  SKIP_SYSTEMD: ctx.skipSystemd ? "1" : "0"
848
1035
  };
849
- const LOCAL_TEMPLATE = join5(homedir2(), "code", "hermes-agent-template");
1036
+ const LOCAL_TEMPLATE = join7(homedir2(), "code", "hermes-agent-template");
850
1037
  const vendored = resolveVendoredTemplate("hermes-agent");
851
- const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync3(join5(LOCAL_TEMPLATE, "copier.yml")) ? LOCAL_TEMPLATE : HERMES_AGENT_TEMPLATE);
1038
+ const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync4(join7(LOCAL_TEMPLATE, "copier.yml")) ? LOCAL_TEMPLATE : HERMES_AGENT_TEMPLATE);
852
1039
  const args = [
853
1040
  "copy",
854
1041
  templateSrc,
@@ -879,7 +1066,7 @@ var RunCopierTemplate = class extends Command {
879
1066
  message: this.formatMessage(`Would run: copier ${args.join(" ")}`)
880
1067
  };
881
1068
  }
882
- mkdirSync3(join5(ctx.targetDir, "agents", "hermes"), { recursive: true });
1069
+ mkdirSync3(join7(ctx.targetDir, "agents", "hermes"), { recursive: true });
883
1070
  const spinner4 = p2.spinner();
884
1071
  spinner4.start(`Running copier copy (target: agents/hermes/${role})`);
885
1072
  const result = spawnSync("copier", args, {
@@ -903,14 +1090,14 @@ var RunCopierTemplate = class extends Command {
903
1090
  };
904
1091
 
905
1092
  // src/commands/hermes/UntrackHermesRuntimes.ts
906
- import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync3, readdirSync } from "fs";
907
- import { join as join6 } from "path";
1093
+ import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync4, readdirSync } from "fs";
1094
+ import { join as join8 } from "path";
908
1095
  import { spawnSync as spawnSync2 } from "node:child_process";
909
1096
  var UntrackHermesRuntimes = class extends Command {
910
1097
  async invoke() {
911
1098
  const targetDir = this.context.targetDir;
912
- const rolesDir = join6(targetDir, "agents", "hermes");
913
- if (!existsSync4(rolesDir)) {
1099
+ const rolesDir = join8(targetDir, "agents", "hermes");
1100
+ if (!existsSync5(rolesDir)) {
914
1101
  return {
915
1102
  success: true,
916
1103
  message: "No Hermes agents found (no agents/hermes directory)."
@@ -926,9 +1113,9 @@ var UntrackHermesRuntimes = class extends Command {
926
1113
  let modifiedAny = false;
927
1114
  const details = [];
928
1115
  for (const role of roles) {
929
- const roleDir = join6("agents", "hermes", role);
930
- const runtimePath = join6(roleDir, "runtime");
931
- const gitignorePath = join6(roleDir, ".gitignore");
1116
+ const roleDir = join8("agents", "hermes", role);
1117
+ const runtimePath = join8(roleDir, "runtime");
1118
+ const gitignorePath = join8(roleDir, ".gitignore");
932
1119
  let isTracked = false;
933
1120
  const lsResult = spawnSync2("git", ["ls-files", "--stage", runtimePath], {
934
1121
  cwd: targetDir,
@@ -938,9 +1125,9 @@ var UntrackHermesRuntimes = class extends Command {
938
1125
  isTracked = true;
939
1126
  }
940
1127
  let isIgnored = false;
941
- const fullGitignorePath = join6(targetDir, gitignorePath);
942
- if (existsSync4(fullGitignorePath)) {
943
- const content = readFileSync2(fullGitignorePath, "utf8");
1128
+ const fullGitignorePath = join8(targetDir, gitignorePath);
1129
+ if (existsSync5(fullGitignorePath)) {
1130
+ const content = readFileSync3(fullGitignorePath, "utf8");
944
1131
  const lines = content.split(/\r?\n/).map((line) => line.trim());
945
1132
  isIgnored = lines.includes("runtime/") || lines.includes("runtime");
946
1133
  }
@@ -965,14 +1152,14 @@ var UntrackHermesRuntimes = class extends Command {
965
1152
  details.push(`ignore runtime/ in agents/hermes/${role}/.gitignore`);
966
1153
  if (!this.context.dryRun) {
967
1154
  let content = "";
968
- if (existsSync4(fullGitignorePath)) {
969
- content = readFileSync2(fullGitignorePath, "utf8");
1155
+ if (existsSync5(fullGitignorePath)) {
1156
+ content = readFileSync3(fullGitignorePath, "utf8");
970
1157
  }
971
1158
  if (content && !content.endsWith("\n")) {
972
1159
  content += "\n";
973
1160
  }
974
1161
  content += "runtime/\n";
975
- writeFileSync3(fullGitignorePath, content, "utf8");
1162
+ writeFileSync4(fullGitignorePath, content, "utf8");
976
1163
  }
977
1164
  }
978
1165
  }
@@ -994,8 +1181,8 @@ ${details.map((d) => ` - ${d}`).join("\n")}`
994
1181
 
995
1182
  // src/commands/hermes/WireTelegram.ts
996
1183
  import { spawnSync as spawnSync3 } from "node:child_process";
997
- import { join as join7 } from "node:path";
998
- import { existsSync as existsSync5, unlinkSync } from "node:fs";
1184
+ import { join as join9 } from "node:path";
1185
+ import { existsSync as existsSync6, unlinkSync } from "node:fs";
999
1186
  import * as p3 from "@clack/prompts";
1000
1187
  var WireTelegram = class extends Command {
1001
1188
  async invoke() {
@@ -1083,15 +1270,15 @@ var WireTelegram = class extends Command {
1083
1270
  if (p3.isCancel(allowedAnswer)) {
1084
1271
  return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
1085
1272
  }
1086
- const script = join7(roleDir, ".scripts", "30-telegram.sh");
1087
- if (!existsSync5(script)) {
1273
+ const script = join9(roleDir, ".scripts", "30-telegram.sh");
1274
+ if (!existsSync6(script)) {
1088
1275
  return {
1089
1276
  success: false,
1090
1277
  message: `\u2717 ${script} not found. Did copier finish? Re-run with --skip-runtime-repo=0 if you skipped it.`
1091
1278
  };
1092
1279
  }
1093
- const marker = join7(roleDir, ".scripts", ".done-30-telegram");
1094
- if (existsSync5(marker)) unlinkSync(marker);
1280
+ const marker = join9(roleDir, ".scripts", ".done-30-telegram");
1281
+ if (existsSync6(marker)) unlinkSync(marker);
1095
1282
  const spinner4 = p3.spinner();
1096
1283
  spinner4.start("Verifying token + wiring profile");
1097
1284
  const result = spawnSync3("bash", [script], {
@@ -1118,8 +1305,8 @@ function cap(s) {
1118
1305
 
1119
1306
  // src/commands/hermes/WireEmail.ts
1120
1307
  import { spawnSync as spawnSync4 } from "node:child_process";
1121
- import { join as join8 } from "node:path";
1122
- import { existsSync as existsSync6, unlinkSync as unlinkSync2 } from "node:fs";
1308
+ import { join as join10 } from "node:path";
1309
+ import { existsSync as existsSync7, unlinkSync as unlinkSync2 } from "node:fs";
1123
1310
  import * as p4 from "@clack/prompts";
1124
1311
  var WireEmail = class extends Command {
1125
1312
  async invoke() {
@@ -1134,8 +1321,8 @@ var WireEmail = class extends Command {
1134
1321
  if (!targetRepo || !role || !roleDir) {
1135
1322
  return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
1136
1323
  }
1137
- const script = join8(roleDir, ".scripts", "50-email.sh");
1138
- if (!existsSync6(script)) {
1324
+ const script = join10(roleDir, ".scripts", "50-email.sh");
1325
+ if (!existsSync7(script)) {
1139
1326
  return { success: false, message: `\u2717 ${script} not found` };
1140
1327
  }
1141
1328
  let token = process.env.CF_EMAIL_ROUTING_TOKEN;
@@ -1197,8 +1384,8 @@ var WireEmail = class extends Command {
1197
1384
  }
1198
1385
  }
1199
1386
  }
1200
- const marker = join8(roleDir, ".scripts", ".done-50-email");
1201
- if (existsSync6(marker)) unlinkSync2(marker);
1387
+ const marker = join10(roleDir, ".scripts", ".done-50-email");
1388
+ if (existsSync7(marker)) unlinkSync2(marker);
1202
1389
  const spinner4 = p4.spinner();
1203
1390
  spinner4.start("Creating Cloudflare Email Routing rule");
1204
1391
  const result = spawnSync4("bash", [script], {
@@ -1285,33 +1472,261 @@ var HermesAgentRecipe = class extends Recipe {
1285
1472
 
1286
1473
  // src/commands/AgentHooksCommands.ts
1287
1474
  import { homedir as homedir4 } from "node:os";
1288
- import { join as join10, dirname as dirname5 } from "node:path";
1289
- import { existsSync as existsSync8, cpSync, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
1475
+ import { join as join12, dirname as dirname5 } from "node:path";
1476
+ import { existsSync as existsSync9, cpSync, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "node:fs";
1290
1477
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1291
1478
 
1292
1479
  // src/project/index.ts
1293
1480
  import { spawnSync as spawnSync5 } from "node:child_process";
1294
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync3, renameSync, statSync, writeFileSync as writeFileSync4 } from "node:fs";
1481
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync, statSync, writeFileSync as writeFileSync5 } from "node:fs";
1295
1482
  import { homedir as homedir3 } from "node:os";
1296
- import { basename as basename2, delimiter, dirname as dirname4, join as join9, resolve } from "node:path";
1483
+ import { basename as basename2, delimiter, dirname as dirname4, join as join11, resolve } from "node:path";
1484
+ import YAML2 from "yaml";
1485
+
1486
+ // src/project/RegistryStore.ts
1487
+ import { Pool } from "pg";
1297
1488
  import YAML from "yaml";
1489
+ function pgRegistryConfigFromEnv(env2 = process.env) {
1490
+ return {
1491
+ host: env2.PGHOST || "localhost",
1492
+ port: parseInt(env2.PGPORT || "5432", 10),
1493
+ user: env2.PGUSER || "delorenj",
1494
+ password: env2.PGPASSWORD || "",
1495
+ database: env2.PGDATABASE || "33god"
1496
+ };
1497
+ }
1498
+ var PgRegistryStore = class {
1499
+ pool;
1500
+ constructor(config) {
1501
+ this.pool = new Pool({
1502
+ host: config.host,
1503
+ port: config.port,
1504
+ user: config.user,
1505
+ password: config.password,
1506
+ database: config.database
1507
+ });
1508
+ }
1509
+ async load() {
1510
+ const client = await this.pool.connect();
1511
+ try {
1512
+ const { rows } = await client.query(
1513
+ `SELECT p.id, p.name, p.description, p.slug, p.status,
1514
+ p.source_artifacts, p.template, p.automation,
1515
+ p.created_at, p.updated_at,
1516
+ r.id AS repo_id, r.local_path
1517
+ FROM public.projects p
1518
+ LEFT JOIN public.repos r ON r.project_id = p.id
1519
+ WHERE p.slug IS NOT NULL`
1520
+ );
1521
+ const projects = {};
1522
+ for (const row of rows) {
1523
+ const slug = row.slug;
1524
+ const ticketProvider = await this.loadTicketProvider(client, row.id);
1525
+ const agents = await this.loadAgents(client, row.id, slug);
1526
+ projects[slug] = {
1527
+ name: row.name ?? "",
1528
+ slug,
1529
+ repo_path: row.local_path ?? "",
1530
+ description: row.description ?? "",
1531
+ status: row.status ?? "planned",
1532
+ source_artifacts: row.source_artifacts ?? [],
1533
+ template: row.template ?? {
1534
+ commonproject: { enabled: false, primary_language: "python" }
1535
+ },
1536
+ ticket_provider: ticketProvider,
1537
+ agents,
1538
+ automation: row.automation ?? void 0,
1539
+ created_at: row.created_at.toISOString(),
1540
+ updated_at: row.updated_at.toISOString()
1541
+ };
1542
+ }
1543
+ const registry = {
1544
+ schema_version: PROJECT_REGISTRY_SCHEMA_VERSION,
1545
+ projects
1546
+ };
1547
+ validateProjectRegistry(registry);
1548
+ return registry;
1549
+ } finally {
1550
+ client.release();
1551
+ }
1552
+ }
1553
+ async save(registry) {
1554
+ validateProjectRegistry(registry);
1555
+ const client = await this.pool.connect();
1556
+ try {
1557
+ await client.query("BEGIN");
1558
+ for (const [slug, record] of Object.entries(registry.projects)) {
1559
+ await this.upsertInTx(client, slug, record);
1560
+ }
1561
+ await client.query("COMMIT");
1562
+ } catch (err) {
1563
+ await client.query("ROLLBACK");
1564
+ throw err;
1565
+ } finally {
1566
+ client.release();
1567
+ }
1568
+ }
1569
+ async upsert(slug, record) {
1570
+ const client = await this.pool.connect();
1571
+ try {
1572
+ await client.query("BEGIN");
1573
+ await this.upsertInTx(client, slug, record);
1574
+ await client.query("COMMIT");
1575
+ } catch (err) {
1576
+ await client.query("ROLLBACK");
1577
+ throw err;
1578
+ } finally {
1579
+ client.release();
1580
+ }
1581
+ }
1582
+ async getBySlug(slug) {
1583
+ const registry = await this.load();
1584
+ return registry.projects[slug];
1585
+ }
1586
+ async getByRepoPath(repoPath) {
1587
+ const registry = await this.load();
1588
+ return Object.values(registry.projects).find(
1589
+ (p6) => p6.repo_path === repoPath
1590
+ );
1591
+ }
1592
+ async close() {
1593
+ await this.pool.end();
1594
+ }
1595
+ // --- private helpers ---
1596
+ async upsertInTx(client, slug, record) {
1597
+ if (!slug) throw new Error("PgRegistryStore.upsert: slug is required");
1598
+ const projectResult = await client.query(
1599
+ `INSERT INTO public.projects (name, description, slug, status, source_artifacts, template, automation)
1600
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
1601
+ ON CONFLICT (slug) WHERE slug IS NOT NULL
1602
+ DO UPDATE SET
1603
+ name = EXCLUDED.name,
1604
+ description = EXCLUDED.description,
1605
+ status = EXCLUDED.status,
1606
+ source_artifacts = EXCLUDED.source_artifacts,
1607
+ template = EXCLUDED.template,
1608
+ automation = EXCLUDED.automation
1609
+ RETURNING id`,
1610
+ [
1611
+ record.name,
1612
+ record.description,
1613
+ slug,
1614
+ record.status,
1615
+ JSON.stringify(record.source_artifacts),
1616
+ JSON.stringify(record.template),
1617
+ record.automation ? JSON.stringify(record.automation) : null
1618
+ ]
1619
+ );
1620
+ const projectId = projectResult.rows[0]?.id;
1621
+ if (!projectId) throw new Error(`Failed to upsert project: ${slug}`);
1622
+ await client.query(
1623
+ `INSERT INTO public.repos (project_id, local_path)
1624
+ VALUES ($1, $2)
1625
+ ON CONFLICT (local_path)
1626
+ DO UPDATE SET project_id = EXCLUDED.project_id
1627
+ RETURNING id`,
1628
+ [projectId, record.repo_path]
1629
+ );
1630
+ const repoResult = await client.query(
1631
+ `SELECT id FROM public.repos WHERE project_id = $1 AND local_path = $2`,
1632
+ [projectId, record.repo_path]
1633
+ );
1634
+ const repoId = repoResult.rows[0]?.id;
1635
+ if (!repoId) throw new Error(`Failed to find repo for project ${slug} at path ${record.repo_path}`);
1636
+ await this.upsertTicketProvider(client, projectId, repoId, record.ticket_provider);
1637
+ await client.query(
1638
+ `DELETE FROM public.project_agents WHERE project_id = $1`,
1639
+ [projectId]
1640
+ );
1641
+ for (const [agentKey, agent] of Object.entries(record.agents)) {
1642
+ await client.query(
1643
+ `INSERT INTO public.project_agents (repo_id, project_id, agent_key, role, role_dir, provisioning_state)
1644
+ VALUES ($1, $2, $3, $4, $5, $6)`,
1645
+ [repoId, projectId, agentKey, agent.role, agent.role_dir ?? null, agent.provisioning_state]
1646
+ );
1647
+ }
1648
+ }
1649
+ async loadTicketProvider(client, projectId) {
1650
+ const { rows } = await client.query(
1651
+ `SELECT provider_type, workspace, identifier, board_id, state
1652
+ FROM public.project_ticket_boards
1653
+ WHERE project_id = $1
1654
+ LIMIT 1`,
1655
+ [projectId]
1656
+ );
1657
+ if (!rows.length) {
1658
+ return { type: "plane", workspace: "33god", identifier: "", board_id: "", state: "planned" };
1659
+ }
1660
+ const row = rows[0];
1661
+ return {
1662
+ type: row.provider_type,
1663
+ workspace: row.workspace ?? void 0,
1664
+ identifier: row.identifier ?? void 0,
1665
+ board_id: row.board_id ?? void 0,
1666
+ state: row.state ?? void 0
1667
+ };
1668
+ }
1669
+ async loadAgents(client, projectId, slug) {
1670
+ const { rows } = await client.query(
1671
+ `SELECT agent_key, role, role_dir, provisioning_state
1672
+ FROM public.project_agents
1673
+ WHERE project_id = $1`,
1674
+ [projectId]
1675
+ );
1676
+ const agents = {};
1677
+ for (const row of rows) {
1678
+ agents[row.agent_key] = {
1679
+ role: row.role,
1680
+ provisioning_state: row.provisioning_state,
1681
+ role_dir: row.role_dir ?? void 0
1682
+ };
1683
+ }
1684
+ return agents;
1685
+ }
1686
+ async upsertTicketProvider(client, projectId, repoId, tp) {
1687
+ await client.query(
1688
+ `DELETE FROM public.project_ticket_boards WHERE project_id = $1`,
1689
+ [projectId]
1690
+ );
1691
+ await client.query(
1692
+ `INSERT INTO public.project_ticket_boards
1693
+ (repo_id, project_id, provider_type, workspace, identifier, board_id, state)
1694
+ VALUES ($1, $2, $3, $4, $5, $6, $7)`,
1695
+ [
1696
+ repoId,
1697
+ projectId,
1698
+ tp.type,
1699
+ tp.workspace ?? null,
1700
+ tp.identifier ?? null,
1701
+ tp.board_id ?? null,
1702
+ tp.state ?? null
1703
+ ]
1704
+ );
1705
+ }
1706
+ };
1707
+ var PJ_REGISTRY_PG_ENV = "PJ_REGISTRY_PG";
1708
+ function isPgRegistryEnabled(env2 = process.env) {
1709
+ return env2[PJ_REGISTRY_PG_ENV] === "1" || env2[PJ_REGISTRY_PG_ENV] === "true";
1710
+ }
1711
+
1712
+ // src/project/index.ts
1298
1713
  var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
1299
1714
  var PROJECT_SOURCE_SKILL_ROOTS_ENV = "PJ_SOURCE_SKILL_ROOTS";
1300
1715
  var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
1301
1716
  var DEFAULT_SOURCE_SKILL_ROOTS = [
1302
1717
  "/home/delorenj/code/skillex/all-skills",
1303
- join9(homedir3(), ".agents", "skills"),
1304
- join9(homedir3(), ".codex", "skills")
1718
+ join11(homedir3(), ".agents", "skills"),
1719
+ join11(homedir3(), ".codex", "skills")
1305
1720
  ];
1306
1721
  function projectRegistryPath(env2 = process.env) {
1307
- return expandHome(env2[PROJECT_REGISTRY_ENV] || join9(homedir3(), ".config", "pjangler", "projects.yaml"));
1722
+ return expandHome(env2[PROJECT_REGISTRY_ENV] || join11(homedir3(), ".config", "pjangler", "projects.yaml"));
1308
1723
  }
1309
1724
  function emptyProjectRegistry() {
1310
1725
  return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
1311
1726
  }
1312
1727
  function loadProjectRegistry(path = projectRegistryPath()) {
1313
- if (!existsSync7(path)) return emptyProjectRegistry();
1314
- const raw = YAML.parse(readFileSync3(path, "utf8"));
1728
+ if (!existsSync8(path)) return emptyProjectRegistry();
1729
+ const raw = YAML2.parse(readFileSync4(path, "utf8"));
1315
1730
  if (raw == null) return emptyProjectRegistry();
1316
1731
  if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
1317
1732
  const registry = raw;
@@ -1326,7 +1741,7 @@ function saveProjectRegistry(registry, path = projectRegistryPath()) {
1326
1741
  validateProjectRegistry(registry);
1327
1742
  mkdirSync4(dirname4(path), { recursive: true });
1328
1743
  const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
1329
- writeFileSync4(temp, YAML.stringify(registry, { lineWidth: 0 }), "utf8");
1744
+ writeFileSync5(temp, YAML2.stringify(registry, { lineWidth: 0 }), "utf8");
1330
1745
  renameSync(temp, path);
1331
1746
  }
1332
1747
  function validateProjectRegistry(registry) {
@@ -1408,7 +1823,7 @@ function resolveAgentHooksLayer(input, env2 = process.env) {
1408
1823
  const override = env2.PJ_AGENT_HOOKS_LAYER;
1409
1824
  if (override === "0" || override === "false") return false;
1410
1825
  if (override === "1" || override === "true") return true;
1411
- return !existsSync7(join9(homedir3(), ".agents", "hooks"));
1826
+ return !existsSync8(join11(homedir3(), ".agents", "hooks"));
1412
1827
  }
1413
1828
  function jsonStable(value) {
1414
1829
  return JSON.stringify(value);
@@ -1439,12 +1854,12 @@ function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
1439
1854
  if (!sourceSkill) return void 0;
1440
1855
  const expanded = expandHome(sourceSkill);
1441
1856
  const direct = resolve(expanded);
1442
- if (existsSync7(direct)) return direct;
1857
+ if (existsSync8(direct)) return direct;
1443
1858
  const name = basename2(sourceSkill);
1444
1859
  const roots = sourceSkillRoots(env2);
1445
1860
  for (const root of roots) {
1446
- const candidate = join9(root, name);
1447
- if (existsSync7(candidate)) return candidate;
1861
+ const candidate = join11(root, name);
1862
+ if (existsSync8(candidate)) return candidate;
1448
1863
  }
1449
1864
  const searched = roots.length ? ` Searched roots: ${roots.join(", ")}.` : "";
1450
1865
  const hint = `${searched} Add project-specific roots with ${PROJECT_SOURCE_SKILL_ROOTS_ENV}.`;
@@ -1525,7 +1940,7 @@ function planProjectInit(input) {
1525
1940
  }));
1526
1941
  }
1527
1942
  actions.push(
1528
- { kind: "project.write-manifest", path: join9(targetDir, ".project.json"), manifest },
1943
+ { kind: "project.write-manifest", path: join11(targetDir, ".project.json"), manifest },
1529
1944
  {
1530
1945
  kind: "ticket-provider.create-or-link",
1531
1946
  enabled: live,
@@ -1553,7 +1968,7 @@ function planProjectInit(input) {
1553
1968
  );
1554
1969
  return { ok: true, apply, dryRun: !apply, live, registryPath: registryPath2, project, manifest, actions };
1555
1970
  }
1556
- function executeProjectInitPlan(plan) {
1971
+ async function executeProjectInitPlan(plan) {
1557
1972
  const logs = [];
1558
1973
  const errors = [];
1559
1974
  const changedFiles = [];
@@ -1578,7 +1993,7 @@ function executeProjectInitPlan(plan) {
1578
1993
  }
1579
1994
  if (result.status !== 0) {
1580
1995
  errors.push(`copier exited with status ${result.status ?? "unknown"}`);
1581
- if (existsSync7(action.targetDir)) changedFiles.push(action.targetDir);
1996
+ if (existsSync8(action.targetDir)) changedFiles.push(action.targetDir);
1582
1997
  break;
1583
1998
  }
1584
1999
  changedFiles.push(action.targetDir);
@@ -1586,9 +2001,9 @@ function executeProjectInitPlan(plan) {
1586
2001
  mkdirSync4(dirname4(action.path), { recursive: true });
1587
2002
  const next = `${JSON.stringify(action.manifest, null, 2)}
1588
2003
  `;
1589
- const current = existsSync7(action.path) ? readFileSync3(action.path, "utf8") : void 0;
2004
+ const current = existsSync8(action.path) ? readFileSync4(action.path, "utf8") : void 0;
1590
2005
  if (current !== next) {
1591
- writeFileSync4(action.path, next, "utf8");
2006
+ writeFileSync5(action.path, next, "utf8");
1592
2007
  changedFiles.push(action.path);
1593
2008
  }
1594
2009
  } else if (action.kind === "registry.upsert") {
@@ -1604,6 +2019,16 @@ function executeProjectInitPlan(plan) {
1604
2019
  registry.projects[pendingRegistryAction.slug] = pendingRegistryAction.project;
1605
2020
  saveProjectRegistry(registry, pendingRegistryAction.registryPath);
1606
2021
  changedFiles.push(pendingRegistryAction.registryPath);
2022
+ if (isPgRegistryEnabled()) {
2023
+ try {
2024
+ const pgStore = new PgRegistryStore(pgRegistryConfigFromEnv());
2025
+ await pgStore.upsert(pendingRegistryAction.slug, pendingRegistryAction.project);
2026
+ await pgStore.close();
2027
+ logs.push("registry: PG dual-write complete");
2028
+ } catch (pgErr) {
2029
+ logs.push(`registry: PG dual-write failed (yaml is authoritative): ${pgErr instanceof Error ? pgErr.message : pgErr}`);
2030
+ }
2031
+ }
1607
2032
  }
1608
2033
  }
1609
2034
  return { ok: errors.length === 0, plan, logs, errors, changedFiles };
@@ -1641,7 +2066,7 @@ function getProject(registry, slug) {
1641
2066
  return project;
1642
2067
  }
1643
2068
  function buildCommonProjectCopierAction(input) {
1644
- const templateDir = join9(input.pjanglerRoot, "templates", "commonproject");
2069
+ const templateDir = join11(input.pjanglerRoot, "templates", "commonproject");
1645
2070
  const data = {
1646
2071
  project_name: input.projectName,
1647
2072
  project_description: input.projectDescription ?? "",
@@ -1670,7 +2095,7 @@ function buildCommonProjectCopierAction(input) {
1670
2095
  function resolvePjanglerRoot() {
1671
2096
  let dir = dirname4(new URL(import.meta.url).pathname);
1672
2097
  while (dir !== dirname4(dir)) {
1673
- if (existsSync7(join9(dir, "package.json")) && existsSync7(join9(dir, "templates", "commonproject", "copier.yml"))) return dir;
2098
+ if (existsSync8(join11(dir, "package.json")) && existsSync8(join11(dir, "templates", "commonproject", "copier.yml"))) return dir;
1674
2099
  dir = dirname4(dir);
1675
2100
  }
1676
2101
  return resolve(process.cwd());
@@ -1702,7 +2127,7 @@ function validateProjectRecord(project, key) {
1702
2127
  }
1703
2128
  function expandHome(path) {
1704
2129
  if (path === "~") return homedir3();
1705
- if (path.startsWith("~/")) return join9(homedir3(), path.slice(2));
2130
+ if (path.startsWith("~/")) return join11(homedir3(), path.slice(2));
1706
2131
  return path;
1707
2132
  }
1708
2133
  function isRecord(value) {
@@ -1719,16 +2144,16 @@ function resolveTemplateRoot() {
1719
2144
  try {
1720
2145
  let dir = dirname5(fileURLToPath2(import.meta.url));
1721
2146
  for (let i = 0; i < 8; i++) {
1722
- candidates.push(join10(dir, "templates", "commonproject", "template"));
2147
+ candidates.push(join12(dir, "templates", "commonproject", "template"));
1723
2148
  const parent = dirname5(dir);
1724
2149
  if (parent === dir) break;
1725
2150
  dir = parent;
1726
2151
  }
1727
2152
  } catch {
1728
2153
  }
1729
- candidates.push(join10(homedir4(), "code", "pjangler", "templates", "commonproject", "template"));
2154
+ candidates.push(join12(homedir4(), "code", "pjangler", "templates", "commonproject", "template"));
1730
2155
  for (const c of candidates) {
1731
- if (existsSync8(join10(c, ".agents", "hooks", "hooks.master.json"))) return c;
2156
+ if (existsSync9(join12(c, ".agents", "hooks", "hooks.master.json"))) return c;
1732
2157
  }
1733
2158
  throw new Error(
1734
2159
  "Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
@@ -1747,18 +2172,17 @@ var CopyAgentHooksTree = class extends Command {
1747
2172
  }
1748
2173
  const items = [
1749
2174
  { rel: ".agents/hooks", dir: true },
2175
+ { rel: ".agents/skills.json", dir: false },
1750
2176
  { rel: ".agents/local.example.json", dir: false },
1751
- { rel: ".mise/scripts/link-project-skills-to-clis.sh", dir: false },
1752
- { rel: ".mise/scripts/unlink-project-skills-from-clis.sh", dir: false },
1753
2177
  { rel: ".mise/scripts/hindsight-setup.sh", dir: false }
1754
2178
  ];
1755
2179
  const created = [];
1756
2180
  const skipped = [];
1757
2181
  for (const { rel, dir } of items) {
1758
- const src = join10(templateRoot, rel);
1759
- const dest = join10(this.context.targetDir, rel);
1760
- if (!existsSync8(src)) continue;
1761
- if (existsSync8(dest) && !this.context.force) {
2182
+ const src = join12(templateRoot, rel);
2183
+ const dest = join12(this.context.targetDir, rel);
2184
+ if (!existsSync9(src)) continue;
2185
+ if (existsSync9(dest) && !this.context.force) {
1762
2186
  skipped.push(rel);
1763
2187
  continue;
1764
2188
  }
@@ -1784,25 +2208,21 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
1784
2208
  if (!resolveAgentHooksLayer()) {
1785
2209
  return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
1786
2210
  }
1787
- const misePath = join10(this.context.targetDir, "mise.toml");
1788
- if (!existsSync8(misePath)) {
2211
+ const misePath = join12(this.context.targetDir, "mise.toml");
2212
+ if (!existsSync9(misePath)) {
1789
2213
  return {
1790
2214
  success: false,
1791
2215
  message: "\u26A0\uFE0F No mise.toml found \u2014 run `pjangler init mise` first, then re-run."
1792
2216
  };
1793
2217
  }
1794
- let content = readFileSync4(misePath, "utf8");
2218
+ let content = readFileSync5(misePath, "utf8");
1795
2219
  if (content.includes(_WireMiseAgentHooks.MARKER)) {
1796
2220
  return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
1797
2221
  }
1798
2222
  const cr = _WireMiseAgentHooks.CR;
1799
- const enterAdds = [
1800
- ` "${cr}/.mise/scripts/link-project-skills-to-clis.sh",`,
1801
- ` "${cr}/.agents/hooks/sync.py --install --quiet",`
1802
- ].join("\n");
2223
+ const enterAdds = [` "sync-skills.py --scope project",`, ` "${cr}/.agents/hooks/sync.py --install --quiet",`].join("\n");
1803
2224
  const leaveBlock = [
1804
2225
  "leave = [",
1805
- ` "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh",`,
1806
2226
  ` "${cr}/.agents/hooks/sync.py --uninstall --quiet",`,
1807
2227
  "]"
1808
2228
  ].join("\n");
@@ -1819,7 +2239,6 @@ ${enterAdds}${close}`;
1819
2239
  content = content.replace(leaveRe, (_m, head, close) => {
1820
2240
  const sep = /[,[]\s*$/.test(head) ? "" : ",";
1821
2241
  return `${head}${sep}
1822
- "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh",
1823
2242
  "${cr}/.agents/hooks/sync.py --uninstall --quiet",${close}`;
1824
2243
  });
1825
2244
  } else {
@@ -1832,6 +2251,14 @@ ${leaveBlock}`);
1832
2251
  "",
1833
2252
  _WireMiseAgentHooks.MARKER + " (generated \u2014 see .agents/hooks/README.md)",
1834
2253
  "[[watch_files]]",
2254
+ 'patterns = [".agents/skills.json"]',
2255
+ 'task = "skills-sync"',
2256
+ "",
2257
+ "[tasks.skills-sync]",
2258
+ 'description = "Sync skills from manifest to local CLI dirs"',
2259
+ 'run = "sync-skills.py --scope project"',
2260
+ "",
2261
+ "[[watch_files]]",
1835
2262
  'patterns = [".agents/hooks/hooks.master.json"]',
1836
2263
  'task = "hooks-sync"',
1837
2264
  "",
@@ -1847,18 +2274,6 @@ ${leaveBlock}`);
1847
2274
  'description = "Remove per-user agent-hook injections (codex/kimi/hermes)"',
1848
2275
  `run = "${cr}/.agents/hooks/sync.py --uninstall"`,
1849
2276
  "",
1850
- "[tasks.link-project-skills-to-clis]",
1851
- 'description = "Fan .agents/skills out to each agent CLI (honors local.json)"',
1852
- `run = "${cr}/.mise/scripts/link-project-skills-to-clis.sh"`,
1853
- "",
1854
- "[tasks.unlink-project-skills-from-clis]",
1855
- 'description = "Remove project skill symlinks from shared per-CLI dirs"',
1856
- `run = "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh"`,
1857
- "",
1858
- "[tasks.skills-relink]",
1859
- 'description = "Re-fan the project skill set to all CLIs"',
1860
- `run = "${cr}/.mise/scripts/link-project-skills-to-clis.sh"`,
1861
- "",
1862
2277
  "[tasks.hindsight-setup]",
1863
2278
  `description = "Provision this dev's shared project Hindsight key from 1Password into .env"`,
1864
2279
  `run = "${cr}/.mise/scripts/hindsight-setup.sh"`,
@@ -1867,7 +2282,7 @@ ${leaveBlock}`);
1867
2282
  ""
1868
2283
  ].join("\n");
1869
2284
  content = content.replace(/\n*$/, "\n") + appended;
1870
- if (!this.context.dryRun) writeFileSync5(misePath, content);
2285
+ if (!this.context.dryRun) writeFileSync6(misePath, content);
1871
2286
  if (wiredHooks) {
1872
2287
  return { success: true, message: this.formatMessage("\u2705 Wired mise.toml ([hooks] enter/leave + tasks)") };
1873
2288
  }
@@ -1876,8 +2291,8 @@ ${leaveBlock}`);
1876
2291
  message: this.formatMessage(
1877
2292
  `\u2705 Added agent-hooks tasks to mise.toml.
1878
2293
  \u26A0\uFE0F Could not find a [hooks].enter array to extend \u2014 add these to your [hooks] block manually:
1879
- enter += "${cr}/.mise/scripts/link-project-skills-to-clis.sh", "${cr}/.agents/hooks/sync.py --install --quiet"
1880
- leave += "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh", "${cr}/.agents/hooks/sync.py --uninstall --quiet"`
2294
+ enter += "sync-skills.py --scope project", "${cr}/.agents/hooks/sync.py --install --quiet"
2295
+ leave += "${cr}/.agents/hooks/sync.py --uninstall --quiet"`
1881
2296
  )
1882
2297
  };
1883
2298
  }
@@ -1892,10 +2307,25 @@ var AgentHooksRecipe = class extends Recipe {
1892
2307
  printNextSteps() {
1893
2308
  console.log("\u{1FA9D} Agent-hooks layer installed!");
1894
2309
  console.log(" Next steps:");
1895
- console.log(" 1. mise run hooks-sync # generate .claude/settings.json + inject codex/kimi/hermes");
1896
- console.log(" 2. git add .claude/settings.json .agents/hooks && commit (codex/kimi/hermes are per-dev)");
1897
- console.log(" 3. mise run hindsight-setup # set HINDSIGHT_OP_KEY_REF to your 1Password item first");
1898
- console.log(` 4. If you run a global agent system: echo '{"skills":{"defer_to_global":true}}' > .agents/local.json`);
2310
+ console.log(" 1. mise run skills-sync # sync .agents/skills.json into local CLI dirs");
2311
+ console.log(" 2. mise run hooks-sync # generate .claude/settings.json + inject codex/kimi/hermes");
2312
+ console.log(" 3. git add .claude/settings.json .agents/hooks .agents/skills.json && commit (codex/kimi/hermes are per-dev)");
2313
+ console.log(" 4. mise run hindsight-setup # set HINDSIGHT_OP_KEY_REF to your 1Password item first");
2314
+ console.log(" 5. Optional per-dev hook opt-out: copy .agents/local.example.json -> .agents/local.json");
2315
+ }
2316
+ };
2317
+
2318
+ // src/recipes/MiseOpInjectRecipe.ts
2319
+ var MiseOpInjectRecipe = class extends Recipe {
2320
+ constructor(context) {
2321
+ super(context);
2322
+ this.addIngredient(WireMiseOpInject);
2323
+ }
2324
+ printNextSteps() {
2325
+ console.log("\u{1F389} Wired up .env.op 1Password resolution via mise!");
2326
+ console.log(" Next steps:");
2327
+ console.log(" 1. Create .env.op with your op:// secret references");
2328
+ console.log(" 2. Run `mise run secrets-inject` or simply cd out and back in to trigger the hook");
1899
2329
  }
1900
2330
  };
1901
2331
 
@@ -1938,6 +2368,12 @@ var RECIPE_REGISTRY = {
1938
2368
  description: "Retrofit the project-scoped agent-hooks + skill fan-out layer (Claude/Codex/Kimi/Hermes hooks via mise enter/leave)",
1939
2369
  class: AgentHooksRecipe,
1940
2370
  commands: ["CopyAgentHooksTree", "WireMiseAgentHooks"]
2371
+ },
2372
+ "mise-op-inject": {
2373
+ name: "mise-op-inject",
2374
+ description: "Wire up op-inject script to mise.toml for 1Password secret resolution",
2375
+ class: MiseOpInjectRecipe,
2376
+ commands: ["WireMiseOpInject"]
1941
2377
  }
1942
2378
  };
1943
2379
  var COMMAND_REGISTRY = {
@@ -2006,6 +2442,12 @@ var COMMAND_REGISTRY = {
2006
2442
  description: "Create .env.example file",
2007
2443
  group: "environment",
2008
2444
  class: AddDotenv
2445
+ },
2446
+ WireMiseOpInject: {
2447
+ name: "WireMiseOpInject",
2448
+ description: "Wire up op-inject script to mise.toml for 1Password secret resolution",
2449
+ group: "mise",
2450
+ class: WireMiseOpInject
2009
2451
  }
2010
2452
  };
2011
2453
  function getRecipeNames() {
@@ -2021,15 +2463,15 @@ function createRecipe(name, context) {
2021
2463
  }
2022
2464
 
2023
2465
  // src/utils/version.ts
2024
- import { readFileSync as readFileSync5 } from "node:fs";
2025
- import { dirname as dirname6, join as join11 } from "node:path";
2466
+ import { readFileSync as readFileSync6 } from "node:fs";
2467
+ import { dirname as dirname6, join as join13 } from "node:path";
2026
2468
  import { fileURLToPath as fileURLToPath3 } from "node:url";
2027
2469
  var PJANGLER_VERSION = (() => {
2028
2470
  try {
2029
2471
  let dir = dirname6(fileURLToPath3(import.meta.url));
2030
2472
  for (let i = 0; i < 4; i++) {
2031
2473
  try {
2032
- const raw = readFileSync5(join11(dir, "package.json"), "utf8");
2474
+ const raw = readFileSync6(join13(dir, "package.json"), "utf8");
2033
2475
  return JSON.parse(raw).version ?? "0.0.0";
2034
2476
  } catch {
2035
2477
  const parent = dirname6(dir);
@@ -2043,29 +2485,39 @@ var PJANGLER_VERSION = (() => {
2043
2485
  })();
2044
2486
 
2045
2487
  // src/parity/index.ts
2046
- import { existsSync as existsSync9, lstatSync, mkdirSync as mkdirSync6, readFileSync as readFileSync6, readlinkSync, readdirSync as readdirSync2, renameSync as renameSync2, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync6, chmodSync as chmodSync2, copyFileSync } from "node:fs";
2047
- import { basename as basename3, dirname as dirname7, join as join12, relative, resolve as resolve2 } from "node:path";
2488
+ import { existsSync as existsSync10, lstatSync, mkdirSync as mkdirSync6, readFileSync as readFileSync7, readlinkSync, readdirSync as readdirSync2, renameSync as renameSync2, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync7, chmodSync as chmodSync4, copyFileSync, rmSync } from "node:fs";
2489
+ import { basename as basename3, dirname as dirname7, join as join14, relative, resolve as resolve2 } from "node:path";
2048
2490
  import { fileURLToPath as fileURLToPath4 } from "node:url";
2049
2491
  import { homedir as homedir5 } from "node:os";
2050
2492
  import { spawnSync as spawnSync6 } from "node:child_process";
2051
- import YAML2 from "yaml";
2493
+ import YAML3 from "yaml";
2052
2494
  var LINK_AGENTFILES_SCRIPT = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'";
2053
2495
  var OP_INJECT_SCRIPT = "op inject -i .env.op > .env";
2496
+ var SYNC_SKILLS_SCRIPT = "sync-skills.py --scope project";
2054
2497
  var CODEGRAPH_SCRIPT = "[ -f '{{config_root}}/.mise/scripts/codegraph.sh' ] && '{{config_root}}/.mise/scripts/codegraph.sh' || true";
2498
+ var SKILLS_REGISTRY_URL = "https://github.com/delorenj/skillex.git";
2055
2499
  var HOOKS_COMMENT_HEADER = `# This block will handle the linking of
2056
2500
  # agent files to the main AGENTS.md file.
2057
2501
  #
2058
2502
  # TODO: Ensure this works for all levels of nesting.
2059
2503
  # i.e. All linked agent files MUST be siblings at
2060
2504
  # any given level of nesting.`;
2061
- var LINK_AGENTFILES_HOOK_ENTRIES = [LINK_AGENTFILES_SCRIPT, OP_INJECT_SCRIPT];
2505
+ var LINK_AGENTFILES_HOOK_ENTRIES = [LINK_AGENTFILES_SCRIPT, OP_INJECT_SCRIPT, SYNC_SKILLS_SCRIPT];
2062
2506
  var LINK_AGENTFILES_WATCH_TASK_BLOCK = `[[watch_files]]
2063
2507
  patterns = ["AGENTS.md"]
2064
2508
  task = "link-agentfiles"
2065
2509
 
2510
+ [[watch_files]]
2511
+ patterns = [".agents/skills.json"]
2512
+ task = "skills-sync"
2513
+
2066
2514
  [tasks.link-agentfiles]
2067
2515
  description = "Symlink all agent files to AGENTS.md"
2068
- run = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'"`;
2516
+ run = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'"
2517
+
2518
+ [tasks.skills-sync]
2519
+ description = "Sync skills from manifest to local CLI dirs"
2520
+ run = "sync-skills.py --scope project"`;
2069
2521
  var VERSIONING_BLOCK = `# >>> mise-versioning >>> (managed block \u2014 do not edit by hand; re-run init to update)
2070
2522
  [tasks."version"]
2071
2523
  description = "Print the current version (vX.Y.Z)"
@@ -2095,7 +2547,7 @@ run = "'{{config_root}}/.mise/scripts/versioning.sh' sync"
2095
2547
  function resolvePjanglerRoot2() {
2096
2548
  let dir = dirname7(fileURLToPath4(import.meta.url));
2097
2549
  while (dir !== dirname7(dir)) {
2098
- if (existsSync9(join12(dir, "package.json")) && existsSync9(join12(dir, "templates", "commonproject", "copier.yml"))) {
2550
+ if (existsSync10(join14(dir, "package.json")) && existsSync10(join14(dir, "templates", "commonproject", "copier.yml"))) {
2099
2551
  return dir;
2100
2552
  }
2101
2553
  dir = dirname7(dir);
@@ -2106,17 +2558,17 @@ function normalizeNewlines(value) {
2106
2558
  return value.replace(/\r\n/g, "\n");
2107
2559
  }
2108
2560
  function readText(path) {
2109
- return normalizeNewlines(readFileSync6(path, "utf8"));
2561
+ return normalizeNewlines(readFileSync7(path, "utf8"));
2110
2562
  }
2111
2563
  function safeReadText(path) {
2112
- return existsSync9(path) ? readText(path) : null;
2564
+ return existsSync10(path) ? readText(path) : null;
2113
2565
  }
2114
2566
  function ensureParent(path) {
2115
2567
  mkdirSync6(dirname7(path), { recursive: true });
2116
2568
  }
2117
2569
  function writeText(path, content) {
2118
2570
  ensureParent(path);
2119
- writeFileSync6(path, content);
2571
+ writeFileSync7(path, content);
2120
2572
  }
2121
2573
  function tryParseJson(text2) {
2122
2574
  if (!text2) return null;
@@ -2133,7 +2585,7 @@ function titleCaseSlug(slug) {
2133
2585
  return slug.split(/[-_]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
2134
2586
  }
2135
2587
  function readSymlinkTarget(path) {
2136
- if (!existsSync9(path)) return null;
2588
+ if (!existsSync10(path)) return null;
2137
2589
  try {
2138
2590
  return readlinkSync(path);
2139
2591
  } catch {
@@ -2141,7 +2593,7 @@ function readSymlinkTarget(path) {
2141
2593
  }
2142
2594
  }
2143
2595
  function ensureSymlink(path, target, dryRun) {
2144
- if (existsSync9(path)) {
2596
+ if (existsSync10(path)) {
2145
2597
  const stat = lstatSync(path);
2146
2598
  if (stat.isSymbolicLink()) {
2147
2599
  const current = readSymlinkTarget(path);
@@ -2158,11 +2610,11 @@ function ensureSymlink(path, target, dryRun) {
2158
2610
  return { changed: true };
2159
2611
  }
2160
2612
  function bootstrapAgentsFile(repoRoot, dryRun) {
2161
- const agentsPath = join12(repoRoot, "AGENTS.md");
2162
- if (existsSync9(agentsPath)) return { changedFiles: [], details: [] };
2613
+ const agentsPath = join14(repoRoot, "AGENTS.md");
2614
+ if (existsSync10(agentsPath)) return { changedFiles: [], details: [] };
2163
2615
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
2164
- const source = join12(repoRoot, file);
2165
- if (!existsSync9(source)) continue;
2616
+ const source = join14(repoRoot, file);
2617
+ if (!existsSync10(source)) continue;
2166
2618
  const stat = lstatSync(source);
2167
2619
  if (stat.isSymbolicLink()) continue;
2168
2620
  if (stat.isFile()) {
@@ -2171,8 +2623,8 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
2171
2623
  }
2172
2624
  return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
2173
2625
  }
2174
- const readmePath = join12(repoRoot, "README.md");
2175
- if (existsSync9(readmePath)) {
2626
+ const readmePath = join14(repoRoot, "README.md");
2627
+ if (existsSync10(readmePath)) {
2176
2628
  const stat = lstatSync(readmePath);
2177
2629
  if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
2178
2630
  if (!dryRun) copyFileSync(readmePath, agentsPath);
@@ -2211,12 +2663,12 @@ function yamlGet(text2, keyPath) {
2211
2663
  return "";
2212
2664
  }
2213
2665
  function discoverRoles(repoRoot) {
2214
- const rolesDir = join12(repoRoot, "agents", "hermes");
2215
- if (!existsSync9(rolesDir)) return [];
2666
+ const rolesDir = join14(repoRoot, "agents", "hermes");
2667
+ if (!existsSync10(rolesDir)) return [];
2216
2668
  return readdirSync2(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
2217
- const roleDir = join12(rolesDir, entry.name);
2218
- const roleYamlPath = join12(roleDir, "role.yaml");
2219
- if (!existsSync9(roleYamlPath)) return null;
2669
+ const roleDir = join14(rolesDir, entry.name);
2670
+ const roleYamlPath = join14(roleDir, "role.yaml");
2671
+ if (!existsSync10(roleYamlPath)) return null;
2220
2672
  const text2 = readText(roleYamlPath);
2221
2673
  const runtimeRepoRaw = yamlGet(text2, "runtime.github_repo");
2222
2674
  return {
@@ -2244,7 +2696,7 @@ function discoverRoles(repoRoot) {
2244
2696
  }).filter((value) => Boolean(value));
2245
2697
  }
2246
2698
  function registryPath(homeDir) {
2247
- return join12(homeDir, ".hermes", "agents-registry.yaml");
2699
+ return join14(homeDir, ".hermes", "agents-registry.yaml");
2248
2700
  }
2249
2701
  function systemctlUser(args) {
2250
2702
  const result = spawnSync6("systemctl", ["--user", ...args], { encoding: "utf8" });
@@ -2255,8 +2707,8 @@ function systemctlUser(args) {
2255
2707
  };
2256
2708
  }
2257
2709
  function templateScript(ctx, name) {
2258
- const source = join12(ctx.pjanglerRoot, ".mise", "scripts", name);
2259
- return existsSync9(source) ? readText(source) : void 0;
2710
+ const source = join14(ctx.pjanglerRoot, ".mise", "scripts", name);
2711
+ return existsSync10(source) ? readText(source) : void 0;
2260
2712
  }
2261
2713
  function templateVersioningScript(ctx) {
2262
2714
  return templateScript(ctx, "versioning.sh");
@@ -2268,8 +2720,8 @@ function resolveAgentHooksLayer2(ctx) {
2268
2720
  const override = process.env.PJ_AGENT_HOOKS_LAYER;
2269
2721
  if (override === "0" || override === "false") return false;
2270
2722
  if (override === "1" || override === "true") return true;
2271
- if (existsSync9(join12(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
2272
- return !existsSync9(join12(ctx.homeDir, ".agents", "hooks"));
2723
+ if (existsSync10(join14(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
2724
+ return !existsSync10(join14(ctx.homeDir, ".agents", "hooks"));
2273
2725
  }
2274
2726
  function evaluateMiseConditionals(template, agentHooksLayer) {
2275
2727
  const out = [];
@@ -2299,19 +2751,36 @@ function renderGeneratedProjectMiseToml(ctx, template) {
2299
2751
  return evaluateMiseConditionals(template, resolveAgentHooksLayer2(ctx)).replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
2300
2752
  }
2301
2753
  function ensureMiseTomlFromTemplate(ctx, changedFiles) {
2302
- const targetPath = join12(ctx.repoRoot, "mise.toml");
2303
- if (existsSync9(targetPath)) return false;
2304
- const sourcePath = join12(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
2305
- if (!existsSync9(sourcePath)) return false;
2754
+ const targetPath = join14(ctx.repoRoot, "mise.toml");
2755
+ if (existsSync10(targetPath)) return false;
2756
+ const sourcePath = join14(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
2757
+ if (!existsSync10(sourcePath)) return false;
2306
2758
  changedFiles.push(targetPath);
2307
2759
  if (!ctx.dryRun) {
2308
2760
  writeText(targetPath, renderGeneratedProjectMiseToml(ctx, readText(sourcePath)));
2309
2761
  }
2310
2762
  return true;
2311
2763
  }
2764
+ function templateCommonProjectText(ctx, rel) {
2765
+ const path = join14(ctx.pjanglerRoot, "templates", "commonproject", "template", rel);
2766
+ return existsSync10(path) ? readText(path) : void 0;
2767
+ }
2768
+ function canonicalSkillsManifest() {
2769
+ return `${JSON.stringify(
2770
+ {
2771
+ $schema: "https://raw.githubusercontent.com/skillex/schemas/main/skills.schema.json",
2772
+ inherit_global: true,
2773
+ registry: SKILLS_REGISTRY_URL,
2774
+ skills: []
2775
+ },
2776
+ null,
2777
+ 2
2778
+ )}
2779
+ `;
2780
+ }
2312
2781
  function templateVersionFilesConf(ctx, repoRoot) {
2313
- const packageJson = join12(repoRoot, "package.json");
2314
- 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";
2782
+ const packageJson = join14(repoRoot, "package.json");
2783
+ 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";
2315
2784
  }
2316
2785
  function replaceOrAppendManagedBlock(text2, startMarker, block, beforePattern) {
2317
2786
  if (startMarker.test(text2)) {
@@ -2335,7 +2804,7 @@ var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/herm
2335
2804
  function requiredMisePathEntries(ctx) {
2336
2805
  const required = [...BASE_MISE_PATH_ENTRIES];
2337
2806
  for (const candidate of CONDITIONAL_HERMES_PATHS) {
2338
- if (existsSync9(join12(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
2807
+ if (existsSync10(join14(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
2339
2808
  }
2340
2809
  return required;
2341
2810
  }
@@ -2436,6 +2905,9 @@ function stripTomlStringsAndComments(line) {
2436
2905
  function isManagedHookEntry(value) {
2437
2906
  const trimmed = value.trim();
2438
2907
  if (trimmed === OP_INJECT_SCRIPT) return true;
2908
+ if (trimmed === SYNC_SKILLS_SCRIPT) return true;
2909
+ if (/link-project-skills-to-clis\.sh'?\s*$/.test(trimmed)) return true;
2910
+ if (/unlink-project-skills-from-clis\.sh'?\s*$/.test(trimmed)) return true;
2439
2911
  return /link-agentfiles\.sh'?\s*$/.test(trimmed);
2440
2912
  }
2441
2913
  function normalizeHookScript(script) {
@@ -2538,12 +3010,17 @@ function upsertLinkAgentfilesHooks(text2) {
2538
3010
  function upsertLinkAgentfilesBlock(text2, ctx) {
2539
3011
  const withPath = upsertMisePath(text2, requiredMisePathEntries(ctx));
2540
3012
  let cleaned = removeTomlSection(withPath, /^\[tasks\.link-agentfiles\]$/, /link-agentfiles/, { includePrecedingComments: false });
3013
+ cleaned = removeTomlSection(cleaned, /^\[tasks\.skills-sync\]$/, void 0, { includePrecedingComments: false });
3014
+ cleaned = removeTomlSection(cleaned, /^\[tasks\.link-project-skills-to-clis\]$/, void 0, { includePrecedingComments: false });
3015
+ cleaned = removeTomlSection(cleaned, /^\[tasks\.unlink-project-skills-from-clis\]$/, void 0, { includePrecedingComments: false });
3016
+ cleaned = removeTomlSection(cleaned, /^\[tasks\.skills-relink\]$/, void 0, { includePrecedingComments: false });
2541
3017
  cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /AGENTS\.md/, { includePrecedingComments: false });
3018
+ cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /\.agents\/skills\.json/, { includePrecedingComments: false });
2542
3019
  cleaned = upsertLinkAgentfilesHooks(cleaned);
2543
3020
  return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
2544
3021
  }
2545
3022
  function readProjectJson(ctx) {
2546
- return tryParseJson(safeReadText(join12(ctx.repoRoot, ".project.json")));
3023
+ return tryParseJson(safeReadText(join14(ctx.repoRoot, ".project.json")));
2547
3024
  }
2548
3025
  function boolSetting(value, fallback) {
2549
3026
  if (typeof value === "boolean") return value;
@@ -2618,12 +3095,12 @@ function canonicalProjectJson(ctx) {
2618
3095
  };
2619
3096
  }
2620
3097
  function projectJsonFinding(ctx) {
2621
- const projectPath = join12(ctx.repoRoot, ".project.json");
2622
- const planeJsonPath = join12(ctx.repoRoot, ".plane.json");
3098
+ const projectPath = join14(ctx.repoRoot, ".project.json");
3099
+ const planeJsonPath = join14(ctx.repoRoot, ".plane.json");
2623
3100
  const details = [];
2624
3101
  const data = readProjectJson(ctx);
2625
3102
  const roles = discoverRoles(ctx.repoRoot);
2626
- if (!existsSync9(projectPath)) {
3103
+ if (!existsSync10(projectPath)) {
2627
3104
  return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json missing", details: [], fixable: true };
2628
3105
  }
2629
3106
  if (!data) {
@@ -2658,7 +3135,7 @@ function projectJsonFinding(ctx) {
2658
3135
  for (const key of ["enabled", "grace_hours", "auto_review"]) {
2659
3136
  if (!(key in reconcile)) details.push(`automation.reconcile.${key} missing`);
2660
3137
  }
2661
- if (existsSync9(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
3138
+ if (existsSync10(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
2662
3139
  return {
2663
3140
  id: "sot.project-json",
2664
3141
  title: "Canonical .project.json",
@@ -2739,17 +3216,17 @@ exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" HERMES_OAUTH
2739
3216
  `.replace(/\u0010/g, "$");
2740
3217
  }
2741
3218
  function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
2742
- if (!existsSync9(sourceDir)) return;
3219
+ if (!existsSync10(sourceDir)) return;
2743
3220
  mkdirSync6(targetDir, { recursive: true });
2744
3221
  for (const entry of readdirSync2(sourceDir, { withFileTypes: true })) {
2745
- const sourcePath = join12(sourceDir, entry.name);
3222
+ const sourcePath = join14(sourceDir, entry.name);
2746
3223
  if (skip?.(sourcePath)) continue;
2747
- const targetPath = join12(targetDir, entry.name);
3224
+ const targetPath = join14(targetDir, entry.name);
2748
3225
  if (entry.isDirectory()) {
2749
3226
  copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
2750
3227
  continue;
2751
3228
  }
2752
- if (existsSync9(targetPath)) continue;
3229
+ if (existsSync10(targetPath)) continue;
2753
3230
  changedFiles.push(targetPath);
2754
3231
  if (!dryRun) {
2755
3232
  ensureParent(targetPath);
@@ -2758,7 +3235,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
2758
3235
  }
2759
3236
  }
2760
3237
  function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
2761
- const gitmodulesPath = join12(repoRoot, ".gitmodules");
3238
+ const gitmodulesPath = join14(repoRoot, ".gitmodules");
2762
3239
  const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
2763
3240
  const owner = role.runtimeOwner || "delorenj";
2764
3241
  const block = `[submodule "agents/hermes/${role.role}/runtime"]
@@ -2925,10 +3402,10 @@ function runBmadInstall(repoRoot) {
2925
3402
  return { ok: true };
2926
3403
  }
2927
3404
  function readInstalledBmadVersion(repoRoot) {
2928
- const raw = safeReadText(join12(repoRoot, "_bmad", "_config", "manifest.yaml"));
3405
+ const raw = safeReadText(join14(repoRoot, "_bmad", "_config", "manifest.yaml"));
2929
3406
  if (!raw) return void 0;
2930
3407
  try {
2931
- const parsed = YAML2.parse(raw);
3408
+ const parsed = YAML3.parse(raw);
2932
3409
  const version = parsed?.installation?.version;
2933
3410
  return typeof version === "string" && version.trim() ? version.trim() : void 0;
2934
3411
  } catch {
@@ -2936,8 +3413,8 @@ function readInstalledBmadVersion(repoRoot) {
2936
3413
  }
2937
3414
  }
2938
3415
  function bmadCachePath(homeDir) {
2939
- const cacheRoot = process.env.XDG_CACHE_HOME?.trim() || join12(homeDir, ".cache");
2940
- return join12(cacheRoot, "pjangler", "bmad-dist-tags.json");
3416
+ const cacheRoot = process.env.XDG_CACHE_HOME?.trim() || join14(homeDir, ".cache");
3417
+ return join14(cacheRoot, "pjangler", "bmad-dist-tags.json");
2941
3418
  }
2942
3419
  function readBmadDistTagsCache(homeDir) {
2943
3420
  const raw = safeReadText(bmadCachePath(homeDir));
@@ -2980,7 +3457,7 @@ function resolveBmadDistTags(homeDir) {
2980
3457
  try {
2981
3458
  const path = bmadCachePath(homeDir);
2982
3459
  mkdirSync6(dirname7(path), { recursive: true });
2983
- writeFileSync6(path, JSON.stringify({ fetchedAt: Date.now(), distTags: fetched }, null, 2));
3460
+ writeFileSync7(path, JSON.stringify({ fetchedAt: Date.now(), distTags: fetched }, null, 2));
2984
3461
  } catch {
2985
3462
  }
2986
3463
  return { distTags: fetched, stale: false };
@@ -3025,14 +3502,14 @@ var RULES = [
3025
3502
  id: "mise.config-root",
3026
3503
  title: "mise config_root + AGENTS link hooks",
3027
3504
  audit: (ctx) => {
3028
- const misePath = join12(ctx.repoRoot, "mise.toml");
3029
- if (!existsSync9(misePath)) {
3505
+ const misePath = join14(ctx.repoRoot, "mise.toml");
3506
+ if (!existsSync10(misePath)) {
3030
3507
  return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
3031
3508
  }
3032
3509
  const text2 = readText(misePath);
3033
3510
  const details = [];
3034
- const linkAgentfilesPath = join12(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
3035
- if (!existsSync9(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
3511
+ const linkAgentfilesPath = join14(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
3512
+ if (!existsSync10(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
3036
3513
  const pathValues = [...(text2.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
3037
3514
  const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
3038
3515
  if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
@@ -3050,10 +3527,10 @@ var RULES = [
3050
3527
  };
3051
3528
  },
3052
3529
  migrate: (ctx, finding) => {
3053
- const path = join12(ctx.repoRoot, "mise.toml");
3530
+ const path = join14(ctx.repoRoot, "mise.toml");
3054
3531
  const changedFiles = [];
3055
3532
  const details = [];
3056
- if (!existsSync9(path)) {
3533
+ if (!existsSync10(path)) {
3057
3534
  if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
3058
3535
  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: [] };
3059
3536
  }
@@ -3069,7 +3546,7 @@ var RULES = [
3069
3546
  if (!ctx.dryRun) writeText(path, next);
3070
3547
  text2 = next;
3071
3548
  }
3072
- const linkAgentfilesPath = join12(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
3549
+ const linkAgentfilesPath = join14(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
3073
3550
  const expectedScript = templateLinkAgentfilesScript(ctx);
3074
3551
  if (expectedScript === void 0) {
3075
3552
  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: [] };
@@ -3078,7 +3555,7 @@ var RULES = [
3078
3555
  changedFiles.push(linkAgentfilesPath);
3079
3556
  if (!ctx.dryRun) {
3080
3557
  writeText(linkAgentfilesPath, expectedScript);
3081
- chmodSync2(linkAgentfilesPath, 493);
3558
+ chmodSync4(linkAgentfilesPath, 493);
3082
3559
  }
3083
3560
  }
3084
3561
  return {
@@ -3096,13 +3573,13 @@ var RULES = [
3096
3573
  title: "managed mise versioning block",
3097
3574
  audit: (ctx) => {
3098
3575
  const details = [];
3099
- const misePath = join12(ctx.repoRoot, "mise.toml");
3100
- const versioningPath = join12(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
3101
- const manifestPath = join12(ctx.repoRoot, ".mise", "version-files.conf");
3576
+ const misePath = join14(ctx.repoRoot, "mise.toml");
3577
+ const versioningPath = join14(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
3578
+ const manifestPath = join14(ctx.repoRoot, ".mise", "version-files.conf");
3102
3579
  const text2 = safeReadText(misePath);
3103
3580
  if (!text2?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
3104
- if (!existsSync9(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
3105
- if (!existsSync9(manifestPath)) details.push(".mise/version-files.conf missing");
3581
+ if (!existsSync10(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
3582
+ if (!existsSync10(manifestPath)) details.push(".mise/version-files.conf missing");
3106
3583
  return {
3107
3584
  id: "mise.versioning",
3108
3585
  title: "managed mise versioning block",
@@ -3115,8 +3592,8 @@ var RULES = [
3115
3592
  migrate: (ctx, finding) => {
3116
3593
  const changedFiles = [];
3117
3594
  const details = [];
3118
- const misePath = join12(ctx.repoRoot, "mise.toml");
3119
- if (!existsSync9(misePath)) {
3595
+ const misePath = join14(ctx.repoRoot, "mise.toml");
3596
+ if (!existsSync10(misePath)) {
3120
3597
  if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
3121
3598
  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: [] };
3122
3599
  }
@@ -3140,7 +3617,7 @@ var RULES = [
3140
3617
  if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
3141
3618
  if (!ctx.dryRun) writeText(misePath, nextMise);
3142
3619
  }
3143
- const versioningPath = join12(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
3620
+ const versioningPath = join14(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
3144
3621
  const expectedScript = templateVersioningScript(ctx);
3145
3622
  if (expectedScript === void 0) {
3146
3623
  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: [] };
@@ -3149,10 +3626,10 @@ var RULES = [
3149
3626
  changedFiles.push(versioningPath);
3150
3627
  if (!ctx.dryRun) {
3151
3628
  writeText(versioningPath, expectedScript);
3152
- chmodSync2(versioningPath, 493);
3629
+ chmodSync4(versioningPath, 493);
3153
3630
  }
3154
3631
  }
3155
- const manifestPath = join12(ctx.repoRoot, ".mise", "version-files.conf");
3632
+ const manifestPath = join14(ctx.repoRoot, ".mise", "version-files.conf");
3156
3633
  const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
3157
3634
  if (safeReadText(manifestPath) !== expectedManifest) {
3158
3635
  changedFiles.push(manifestPath);
@@ -3168,13 +3645,125 @@ var RULES = [
3168
3645
  };
3169
3646
  }
3170
3647
  },
3648
+ {
3649
+ id: "skills.project-manifest",
3650
+ title: "Skillex project skills manifest",
3651
+ audit: (ctx) => {
3652
+ const details = [];
3653
+ const manifestPath = join14(ctx.repoRoot, ".agents", "skills.json");
3654
+ const legacyDir = join14(ctx.repoRoot, ".agents", "skills");
3655
+ const localExamplePath = join14(ctx.repoRoot, ".agents", "local.example.json");
3656
+ const misePath = join14(ctx.repoRoot, "mise.toml");
3657
+ let fixable = true;
3658
+ const manifest = tryParseJson(safeReadText(manifestPath));
3659
+ if (!manifest) {
3660
+ details.push(".agents/skills.json missing or invalid JSON");
3661
+ } else {
3662
+ if (manifest.inherit_global !== true) details.push(".agents/skills.json should set inherit_global: true");
3663
+ if (manifest.registry !== SKILLS_REGISTRY_URL) details.push(`.agents/skills.json should set registry to ${SKILLS_REGISTRY_URL}`);
3664
+ if (!Array.isArray(manifest.skills)) details.push(".agents/skills.json should define a skills array");
3665
+ }
3666
+ if (existsSync10(legacyDir)) {
3667
+ const entries = readdirSync2(legacyDir);
3668
+ if (entries.length > 0) {
3669
+ details.push(".agents/skills/ still contains legacy committed skills; map them into .agents/skills.json before migrating");
3670
+ fixable = false;
3671
+ } else {
3672
+ details.push(".agents/skills/ legacy directory should be removed");
3673
+ }
3674
+ }
3675
+ for (const rel of [".mise/scripts/link-project-skills-to-clis.sh", ".mise/scripts/unlink-project-skills-from-clis.sh"]) {
3676
+ if (existsSync10(join14(ctx.repoRoot, rel))) details.push(`${rel} is a legacy symlink-era script and should be removed`);
3677
+ }
3678
+ const localExample = tryParseJson(safeReadText(localExamplePath));
3679
+ if (localExample && Object.prototype.hasOwnProperty.call(localExample, "skills")) {
3680
+ details.push(".agents/local.example.json still documents legacy skills overrides; drop the skills section");
3681
+ }
3682
+ const mise = safeReadText(misePath);
3683
+ if (!mise?.includes(SYNC_SKILLS_SCRIPT)) details.push("mise.toml should run sync-skills.py --scope project on enter");
3684
+ if (!mise?.includes('patterns = [".agents/skills.json"]')) details.push("mise.toml should watch .agents/skills.json");
3685
+ if (!mise?.includes("[tasks.skills-sync]")) details.push("mise.toml should define a skills-sync task");
3686
+ if (mise?.includes("link-project-skills-to-clis.sh") || mise?.includes("unlink-project-skills-from-clis.sh") || mise?.includes("[tasks.skills-relink]")) {
3687
+ details.push("mise.toml still contains legacy skill-link wiring");
3688
+ }
3689
+ return {
3690
+ id: "skills.project-manifest",
3691
+ title: "Skillex project skills manifest",
3692
+ status: details.length === 0 ? "pass" : "fail",
3693
+ summary: details.length === 0 ? "Skillex skills manifest parity verified" : `${details.length} Skillex migration issue(s) detected`,
3694
+ details,
3695
+ fixable
3696
+ };
3697
+ },
3698
+ migrate: (ctx, finding) => {
3699
+ const changedFiles = [];
3700
+ const details = [];
3701
+ const manifestPath = join14(ctx.repoRoot, ".agents", "skills.json");
3702
+ const legacyDir = join14(ctx.repoRoot, ".agents", "skills");
3703
+ const localExamplePath = join14(ctx.repoRoot, ".agents", "local.example.json");
3704
+ const misePath = join14(ctx.repoRoot, "mise.toml");
3705
+ if (existsSync10(legacyDir)) {
3706
+ const entries = readdirSync2(legacyDir);
3707
+ if (entries.length > 0) {
3708
+ return {
3709
+ id: finding.id,
3710
+ title: finding.title,
3711
+ status: "blocked",
3712
+ summary: "Legacy .agents/skills/ still contains committed skills; migrate them into .agents/skills.json manually first",
3713
+ changedFiles,
3714
+ details: entries.map((entry) => `.agents/skills/${entry}`)
3715
+ };
3716
+ }
3717
+ changedFiles.push(legacyDir);
3718
+ if (!ctx.dryRun) rmSync(legacyDir, { recursive: true, force: true });
3719
+ }
3720
+ const expectedManifest = canonicalSkillsManifest();
3721
+ if (safeReadText(manifestPath) !== expectedManifest) {
3722
+ changedFiles.push(manifestPath);
3723
+ if (!ctx.dryRun) writeText(manifestPath, expectedManifest);
3724
+ details.push("Wrote canonical .agents/skills.json manifest");
3725
+ }
3726
+ for (const rel of [".mise/scripts/link-project-skills-to-clis.sh", ".mise/scripts/unlink-project-skills-from-clis.sh"]) {
3727
+ const path = join14(ctx.repoRoot, rel);
3728
+ if (existsSync10(path)) {
3729
+ changedFiles.push(path);
3730
+ if (!ctx.dryRun) unlinkSync3(path);
3731
+ }
3732
+ }
3733
+ const templateLocalExample = templateCommonProjectText(ctx, ".agents/local.example.json");
3734
+ const currentLocalExample = safeReadText(localExamplePath);
3735
+ if (templateLocalExample && currentLocalExample && currentLocalExample !== templateLocalExample) {
3736
+ changedFiles.push(localExamplePath);
3737
+ if (!ctx.dryRun) writeText(localExamplePath, templateLocalExample);
3738
+ }
3739
+ if (!existsSync10(misePath)) {
3740
+ if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
3741
+ 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 };
3742
+ }
3743
+ }
3744
+ const currentMise = readText(misePath);
3745
+ const nextMise = upsertLinkAgentfilesBlock(currentMise, ctx);
3746
+ if (nextMise !== currentMise) {
3747
+ if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
3748
+ if (!ctx.dryRun) writeText(misePath, nextMise);
3749
+ }
3750
+ return {
3751
+ id: finding.id,
3752
+ title: finding.title,
3753
+ status: changedFiles.length ? "applied" : "noop",
3754
+ summary: changedFiles.length ? "Skillex skills manifest contract normalized" : "No changes required",
3755
+ changedFiles,
3756
+ details
3757
+ };
3758
+ }
3759
+ },
3171
3760
  {
3172
3761
  id: "sot.agent-symlinks",
3173
3762
  title: "AGENTS/CLAUDE/GEMINI symlink contract",
3174
3763
  audit: (ctx) => {
3175
- const agentsPath = join12(ctx.repoRoot, "AGENTS.md");
3176
- if (!existsSync9(agentsPath)) {
3177
- const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync9(join12(ctx.repoRoot, file)));
3764
+ const agentsPath = join14(ctx.repoRoot, "AGENTS.md");
3765
+ if (!existsSync10(agentsPath)) {
3766
+ const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync10(join14(ctx.repoRoot, file)));
3178
3767
  if (fallbackSources.length === 0) {
3179
3768
  return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
3180
3769
  }
@@ -3189,7 +3778,7 @@ var RULES = [
3189
3778
  }
3190
3779
  const details = [];
3191
3780
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
3192
- const full = join12(ctx.repoRoot, file);
3781
+ const full = join14(ctx.repoRoot, file);
3193
3782
  const target = readSymlinkTarget(full);
3194
3783
  if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
3195
3784
  }
@@ -3213,7 +3802,7 @@ var RULES = [
3213
3802
  return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
3214
3803
  }
3215
3804
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
3216
- const full = join12(ctx.repoRoot, file);
3805
+ const full = join14(ctx.repoRoot, file);
3217
3806
  const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
3218
3807
  if (result.blocked) blockedDetails.push(result.blocked);
3219
3808
  if (result.changed) changedFiles.push(full);
@@ -3235,7 +3824,7 @@ var RULES = [
3235
3824
  migrate: (ctx, finding) => {
3236
3825
  const changedFiles = [];
3237
3826
  const details = [];
3238
- const path = join12(ctx.repoRoot, ".project.json");
3827
+ const path = join14(ctx.repoRoot, ".project.json");
3239
3828
  const existing = readProjectJson(ctx) ?? {};
3240
3829
  const canonical = canonicalProjectJson(ctx);
3241
3830
  const merged = { ...existing, ...canonical };
@@ -3245,10 +3834,10 @@ var RULES = [
3245
3834
  changedFiles.push(path);
3246
3835
  if (!ctx.dryRun) writeText(path, expected);
3247
3836
  }
3248
- const planeJson = join12(ctx.repoRoot, ".plane.json");
3249
- if (existsSync9(planeJson)) {
3837
+ const planeJson = join14(ctx.repoRoot, ".plane.json");
3838
+ if (existsSync10(planeJson)) {
3250
3839
  const backup = `${planeJson}.migrated-backup`;
3251
- if (existsSync9(backup)) {
3840
+ if (existsSync10(backup)) {
3252
3841
  details.push(`cannot back up .plane.json because ${relative(ctx.repoRoot, backup)} already exists`);
3253
3842
  } else {
3254
3843
  changedFiles.push(backup);
@@ -3270,8 +3859,8 @@ var RULES = [
3270
3859
  title: ".env.op + gitignore secrets contract",
3271
3860
  audit: (ctx) => {
3272
3861
  const details = [];
3273
- const envOp = safeReadText(join12(ctx.repoRoot, ".env.op"));
3274
- const gitignore = safeReadText(join12(ctx.repoRoot, ".gitignore"));
3862
+ const envOp = safeReadText(join14(ctx.repoRoot, ".env.op"));
3863
+ const gitignore = safeReadText(join14(ctx.repoRoot, ".gitignore"));
3275
3864
  if (!envOp) {
3276
3865
  details.push(".env.op missing");
3277
3866
  } else {
@@ -3297,12 +3886,12 @@ var RULES = [
3297
3886
  migrate: (ctx, finding) => {
3298
3887
  const changedFiles = [];
3299
3888
  const details = [];
3300
- const envOpPath = join12(ctx.repoRoot, ".env.op");
3301
- if (!existsSync9(envOpPath)) {
3889
+ const envOpPath = join14(ctx.repoRoot, ".env.op");
3890
+ if (!existsSync10(envOpPath)) {
3302
3891
  changedFiles.push(envOpPath);
3303
- if (!ctx.dryRun) writeText(envOpPath, readText(join12(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
3892
+ if (!ctx.dryRun) writeText(envOpPath, readText(join14(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
3304
3893
  }
3305
- const gitignorePath = join12(ctx.repoRoot, ".gitignore");
3894
+ const gitignorePath = join14(ctx.repoRoot, ".gitignore");
3306
3895
  const gitignore = safeReadText(gitignorePath) ?? "";
3307
3896
  const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
3308
3897
  # NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
@@ -3329,7 +3918,7 @@ var RULES = [
3329
3918
  title: ".copier-answers.yml provenance + drift report",
3330
3919
  audit: (ctx) => {
3331
3920
  const details = [];
3332
- const path = join12(ctx.repoRoot, ".copier-answers.yml");
3921
+ const path = join14(ctx.repoRoot, ".copier-answers.yml");
3333
3922
  const text2 = safeReadText(path);
3334
3923
  const project = readProjectJson(ctx);
3335
3924
  if (!text2) {
@@ -3360,12 +3949,12 @@ var RULES = [
3360
3949
  const changedFiles = [];
3361
3950
  const project = canonicalProjectJson(ctx);
3362
3951
  const text2 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
3363
- _src_path: ${join12(ctx.pjanglerRoot, "templates", "commonproject")}
3952
+ _src_path: ${join14(ctx.pjanglerRoot, "templates", "commonproject")}
3364
3953
  project_description: ${String(project.project_description)}
3365
3954
  project_name: ${String(project.project_name)}
3366
3955
  ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3367
3956
  `;
3368
- const path = join12(ctx.repoRoot, ".copier-answers.yml");
3957
+ const path = join14(ctx.repoRoot, ".copier-answers.yml");
3369
3958
  if (safeReadText(path) !== text2) {
3370
3959
  changedFiles.push(path);
3371
3960
  if (!ctx.dryRun) writeText(path, text2);
@@ -3384,14 +3973,14 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3384
3973
  id: "bmad.scaffold",
3385
3974
  title: "BMAD modules/docs scaffold",
3386
3975
  audit: (ctx) => {
3387
- const targetRoot = join12(ctx.repoRoot, "_bmad");
3976
+ const targetRoot = join14(ctx.repoRoot, "_bmad");
3388
3977
  const sentinels = [
3389
- join12("core", "config.yaml"),
3390
- join12("config.toml"),
3391
- join12("_config", "manifest.yaml"),
3392
- join12("bmm", "config.yaml")
3978
+ join14("core", "config.yaml"),
3979
+ join14("config.toml"),
3980
+ join14("_config", "manifest.yaml"),
3981
+ join14("bmm", "config.yaml")
3393
3982
  ];
3394
- const missing = sentinels.filter((file) => !existsSync9(join12(targetRoot, file)));
3983
+ const missing = sentinels.filter((file) => !existsSync10(join14(targetRoot, file)));
3395
3984
  return {
3396
3985
  id: "bmad.scaffold",
3397
3986
  title: "BMAD modules/docs scaffold",
@@ -3405,7 +3994,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3405
3994
  const changedFiles = [];
3406
3995
  if (ctx.dryRun) {
3407
3996
  for (const detail of finding.details) {
3408
- changedFiles.push(join12(ctx.repoRoot, detail));
3997
+ changedFiles.push(join14(ctx.repoRoot, detail));
3409
3998
  }
3410
3999
  return {
3411
4000
  id: finding.id,
@@ -3430,8 +4019,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3430
4019
  };
3431
4020
  }
3432
4021
  for (const detail of finding.details) {
3433
- if (existsSync9(join12(ctx.repoRoot, detail))) {
3434
- changedFiles.push(join12(ctx.repoRoot, detail));
4022
+ if (existsSync10(join14(ctx.repoRoot, detail))) {
4023
+ changedFiles.push(join14(ctx.repoRoot, detail));
3435
4024
  }
3436
4025
  }
3437
4026
  return {
@@ -3454,7 +4043,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3454
4043
  id: "bmad.version",
3455
4044
  title: "BMAD version currency",
3456
4045
  status: "skip",
3457
- summary: existsSync9(join12(ctx.repoRoot, "_bmad")) ? "BMAD installed but version manifest unreadable" : "No BMAD install present",
4046
+ summary: existsSync10(join14(ctx.repoRoot, "_bmad")) ? "BMAD installed but version manifest unreadable" : "No BMAD install present",
3458
4047
  details: [],
3459
4048
  fixable: false
3460
4049
  };
@@ -3509,7 +4098,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3509
4098
  }
3510
4099
  const installed = readInstalledBmadVersion(ctx.repoRoot);
3511
4100
  const available = resolveBmadDistTags(ctx.homeDir)?.distTags?.[BMAD_TARGET_CHANNEL];
3512
- const manifestPath = join12(ctx.repoRoot, "_bmad", "_config", "manifest.yaml");
4101
+ const manifestPath = join14(ctx.repoRoot, "_bmad", "_config", "manifest.yaml");
3513
4102
  if (ctx.dryRun) {
3514
4103
  return {
3515
4104
  id: finding.id,
@@ -3556,11 +4145,11 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3556
4145
  }
3557
4146
  const details = [];
3558
4147
  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"]) {
3559
- if (!existsSync9(join12(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join12(role.roleDir, rel))}`);
4148
+ if (!existsSync10(join14(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join14(role.roleDir, rel))}`);
3560
4149
  }
3561
- const gitmodules = safeReadText(join12(ctx.repoRoot, ".gitmodules")) ?? "";
4150
+ const gitmodules = safeReadText(join14(ctx.repoRoot, ".gitmodules")) ?? "";
3562
4151
  if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
3563
- if (!profileMetaInheritsDefault(join12(role.roleDir, "runtime", "profile.yaml"))) {
4152
+ if (!profileMetaInheritsDefault(join14(role.roleDir, "runtime", "profile.yaml"))) {
3564
4153
  details.push("runtime/profile.yaml missing inherited default config metadata");
3565
4154
  }
3566
4155
  const registry = safeReadText(registryPath(ctx.homeDir));
@@ -3581,21 +4170,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3581
4170
  if (!role) {
3582
4171
  return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
3583
4172
  }
3584
- const templateRoleDir = join12(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
3585
- writeIfDifferent(join12(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
3586
- writeIfDifferent(join12(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
3587
- writeIfDifferent(join12(role.roleDir, ".gitignore"), readText(join12(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
3588
- copyMissingRecursive(join12(templateRoleDir, ".runtime-scaffold"), join12(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
3589
- copyMissingRecursive(join12(templateRoleDir, ".runtime-scaffold"), join12(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
3590
- copyMissingRecursive(join12(templateRoleDir, ".scripts"), join12(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
3591
- const promptSource = join12(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
3592
- const promptTarget = join12(role.roleDir, ".scripts", "sentinel.prompt.md");
3593
- if (existsSync9(promptSource) && !existsSync9(promptTarget)) {
4173
+ const templateRoleDir = join14(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
4174
+ writeIfDifferent(join14(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
4175
+ writeIfDifferent(join14(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
4176
+ writeIfDifferent(join14(role.roleDir, ".gitignore"), readText(join14(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
4177
+ copyMissingRecursive(join14(templateRoleDir, ".runtime-scaffold"), join14(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
4178
+ copyMissingRecursive(join14(templateRoleDir, ".runtime-scaffold"), join14(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
4179
+ copyMissingRecursive(join14(templateRoleDir, ".scripts"), join14(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
4180
+ const promptSource = join14(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
4181
+ const promptTarget = join14(role.roleDir, ".scripts", "sentinel.prompt.md");
4182
+ if (existsSync10(promptSource) && !existsSync10(promptTarget)) {
3594
4183
  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);
3595
4184
  writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
3596
4185
  }
3597
4186
  upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
3598
- const profileMetaUpdated = upsertInheritedProfileMeta(join12(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
4187
+ const profileMetaUpdated = upsertInheritedProfileMeta(join14(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
3599
4188
  if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
3600
4189
  const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
3601
4190
  if (registryUpdated) details.push(`updated ${registryUpdated}`);
@@ -3627,7 +4216,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3627
4216
  const details = [];
3628
4217
  for (const role of roles) {
3629
4218
  const roleRelDir = relative(ctx.repoRoot, role.roleDir);
3630
- const runtimeRelPath = join12(roleRelDir, "runtime");
4219
+ const runtimeRelPath = join14(roleRelDir, "runtime");
3631
4220
  const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
3632
4221
  cwd: ctx.repoRoot,
3633
4222
  encoding: "utf8"
@@ -3635,8 +4224,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3635
4224
  if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
3636
4225
  details.push(`submodule runtime is tracked in Git index at ${runtimeRelPath}`);
3637
4226
  }
3638
- const gitignorePath = join12(role.roleDir, ".gitignore");
3639
- if (existsSync9(gitignorePath)) {
4227
+ const gitignorePath = join14(role.roleDir, ".gitignore");
4228
+ if (existsSync10(gitignorePath)) {
3640
4229
  const content = safeReadText(gitignorePath) ?? "";
3641
4230
  const lines = content.split(/\r?\n/).map((line) => line.trim());
3642
4231
  if (!lines.includes("runtime/") && !lines.includes("runtime")) {
@@ -3661,7 +4250,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3661
4250
  const details = [];
3662
4251
  for (const role of roles) {
3663
4252
  const roleRelDir = relative(ctx.repoRoot, role.roleDir);
3664
- const runtimeRelPath = join12(roleRelDir, "runtime");
4253
+ const runtimeRelPath = join14(roleRelDir, "runtime");
3665
4254
  const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
3666
4255
  cwd: ctx.repoRoot,
3667
4256
  encoding: "utf8"
@@ -3676,10 +4265,10 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3676
4265
  });
3677
4266
  }
3678
4267
  }
3679
- const gitignorePath = join12(role.roleDir, ".gitignore");
4268
+ const gitignorePath = join14(role.roleDir, ".gitignore");
3680
4269
  let content = "";
3681
4270
  let isIgnored = false;
3682
- if (existsSync9(gitignorePath)) {
4271
+ if (existsSync10(gitignorePath)) {
3683
4272
  content = safeReadText(gitignorePath) ?? "";
3684
4273
  const lines = content.split(/\r?\n/).map((line) => line.trim());
3685
4274
  isIgnored = lines.includes("runtime/") || lines.includes("runtime");
@@ -3746,9 +4335,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3746
4335
  return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
3747
4336
  }
3748
4337
  for (const role of roles) {
3749
- const sysDir = join12(ctx.homeDir, ".config", "systemd", "user");
4338
+ const sysDir = join14(ctx.homeDir, ".config", "systemd", "user");
3750
4339
  const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`];
3751
- const allUnitsPresent = units.every((unit) => existsSync9(join12(sysDir, unit)));
4340
+ const allUnitsPresent = units.every((unit) => existsSync10(join14(sysDir, unit)));
3752
4341
  if (allUnitsPresent) {
3753
4342
  if (ctx.dryRun) {
3754
4343
  details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
@@ -3760,8 +4349,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3760
4349
  }
3761
4350
  continue;
3762
4351
  }
3763
- for (const script of [join12(role.roleDir, ".scripts", "70-systemd.sh")]) {
3764
- if (!script || !existsSync9(script)) continue;
4352
+ for (const script of [join14(role.roleDir, ".scripts", "70-systemd.sh")]) {
4353
+ if (!script || !existsSync10(script)) continue;
3765
4354
  if (ctx.dryRun) {
3766
4355
  details.push(`would run: bash ${script}`);
3767
4356
  } else {
@@ -3788,7 +4377,7 @@ function writeIfDifferent(path, content, dryRun, changedFiles, mode) {
3788
4377
  changedFiles.push(path);
3789
4378
  if (!dryRun) {
3790
4379
  writeText(path, normalized);
3791
- if (mode) chmodSync2(path, mode);
4380
+ if (mode) chmodSync4(path, mode);
3792
4381
  }
3793
4382
  }
3794
4383
  function getParityRuleIds() {
@@ -3885,7 +4474,7 @@ var server = new McpServer({
3885
4474
  var TICKET_PROVIDER_SCHEMA = z.enum(["plane", "trello"]);
3886
4475
  function resolveTargetDir(targetDir) {
3887
4476
  const dir = resolve3(targetDir ?? process.cwd());
3888
- if (!existsSync10(dir)) {
4477
+ if (!existsSync11(dir)) {
3889
4478
  throw new Error(`Target directory does not exist: ${dir}`);
3890
4479
  }
3891
4480
  if (!statSync2(dir).isDirectory()) {
@@ -3896,7 +4485,7 @@ function resolveTargetDir(targetDir) {
3896
4485
  function resolvePjanglerRoot3() {
3897
4486
  let dir = dirname8(fileURLToPath5(import.meta.url));
3898
4487
  while (dir !== dirname8(dir)) {
3899
- if (existsSync10(join13(dir, "package.json")) && existsSync10(join13(dir, "templates", "commonproject", "copier.yml"))) {
4488
+ if (existsSync11(join15(dir, "package.json")) && existsSync11(join15(dir, "templates", "commonproject", "copier.yml"))) {
3900
4489
  return dir;
3901
4490
  }
3902
4491
  dir = dirname8(dir);
@@ -4098,8 +4687,8 @@ server.registerTool(
4098
4687
  const pjanglerRoot = resolvePjanglerRoot3();
4099
4688
  const projectSlug = input.projectSlug ?? slugify(input.projectName);
4100
4689
  const parentDir = resolve3(input.parentDir ?? process.cwd());
4101
- if (!existsSync10(parentDir) || !statSync2(parentDir).isDirectory()) throw new Error(`Parent directory does not exist: ${parentDir}`);
4102
- const targetDir = resolve3(input.targetDir ?? join13(parentDir, projectSlug));
4690
+ if (!existsSync11(parentDir) || !statSync2(parentDir).isDirectory()) throw new Error(`Parent directory does not exist: ${parentDir}`);
4691
+ const targetDir = resolve3(input.targetDir ?? join15(parentDir, projectSlug));
4103
4692
  const overwrite = input.overwrite ?? input.force ?? false;
4104
4693
  const dryRun = input.dryRun ?? true;
4105
4694
  const local = input.local ?? true;
@@ -4109,7 +4698,7 @@ server.registerTool(
4109
4698
  if (!skipPlane && ticketProvider === "plane" && !boardId) {
4110
4699
  throw new Error("boardId or planeProjectId is required when skipPlane=false for Plane; keep skipPlane=true for safe local bootstrap");
4111
4700
  }
4112
- if (!dryRun && existsSync10(targetDir) && !overwrite) throw new Error(`Target already exists: ${targetDir} (set force/overwrite=true to re-render)`);
4701
+ if (!dryRun && existsSync11(targetDir) && !overwrite) throw new Error(`Target already exists: ${targetDir} (set force/overwrite=true to re-render)`);
4113
4702
  const plan = planProjectInit({
4114
4703
  name: input.projectName,
4115
4704
  description: input.projectDescription,
@@ -4135,7 +4724,7 @@ server.registerTool(
4135
4724
  if (dryRun) {
4136
4725
  return asText({ ...plan, guidance: parityGuidance() });
4137
4726
  }
4138
- const result = executeProjectInitPlan(plan);
4727
+ const result = await executeProjectInitPlan(plan);
4139
4728
  if (!result.ok) return asText({ ...result, guidance: parityGuidance() });
4140
4729
  let agentResult;
4141
4730
  if (input.provisionAgent) {
@@ -4211,7 +4800,7 @@ server.registerTool(
4211
4800
  overwrite: input.force ?? false
4212
4801
  });
4213
4802
  if (!input.apply) return asText(plan);
4214
- return asText(executeProjectInitPlan(plan));
4803
+ return asText(await executeProjectInitPlan(plan));
4215
4804
  } catch (err) {
4216
4805
  return { isError: true, content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }] };
4217
4806
  }