@delorenj/pjangler 1.2.19 → 1.2.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,15 +1472,15 @@ 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";
1297
1484
  import YAML2 from "yaml";
1298
1485
 
1299
1486
  // src/project/RegistryStore.ts
@@ -1528,18 +1715,18 @@ var PROJECT_SOURCE_SKILL_ROOTS_ENV = "PJ_SOURCE_SKILL_ROOTS";
1528
1715
  var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
1529
1716
  var DEFAULT_SOURCE_SKILL_ROOTS = [
1530
1717
  "/home/delorenj/code/skillex/all-skills",
1531
- join9(homedir3(), ".agents", "skills"),
1532
- join9(homedir3(), ".codex", "skills")
1718
+ join11(homedir3(), ".agents", "skills"),
1719
+ join11(homedir3(), ".codex", "skills")
1533
1720
  ];
1534
1721
  function projectRegistryPath(env2 = process.env) {
1535
- 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"));
1536
1723
  }
1537
1724
  function emptyProjectRegistry() {
1538
1725
  return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
1539
1726
  }
1540
1727
  function loadProjectRegistry(path = projectRegistryPath()) {
1541
- if (!existsSync7(path)) return emptyProjectRegistry();
1542
- const raw = YAML2.parse(readFileSync3(path, "utf8"));
1728
+ if (!existsSync8(path)) return emptyProjectRegistry();
1729
+ const raw = YAML2.parse(readFileSync4(path, "utf8"));
1543
1730
  if (raw == null) return emptyProjectRegistry();
1544
1731
  if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
1545
1732
  const registry = raw;
@@ -1554,7 +1741,7 @@ function saveProjectRegistry(registry, path = projectRegistryPath()) {
1554
1741
  validateProjectRegistry(registry);
1555
1742
  mkdirSync4(dirname4(path), { recursive: true });
1556
1743
  const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
1557
- writeFileSync4(temp, YAML2.stringify(registry, { lineWidth: 0 }), "utf8");
1744
+ writeFileSync5(temp, YAML2.stringify(registry, { lineWidth: 0 }), "utf8");
1558
1745
  renameSync(temp, path);
1559
1746
  }
1560
1747
  function validateProjectRegistry(registry) {
@@ -1636,7 +1823,7 @@ function resolveAgentHooksLayer(input, env2 = process.env) {
1636
1823
  const override = env2.PJ_AGENT_HOOKS_LAYER;
1637
1824
  if (override === "0" || override === "false") return false;
1638
1825
  if (override === "1" || override === "true") return true;
1639
- return !existsSync7(join9(homedir3(), ".agents", "hooks"));
1826
+ return !existsSync8(join11(homedir3(), ".agents", "hooks"));
1640
1827
  }
1641
1828
  function jsonStable(value) {
1642
1829
  return JSON.stringify(value);
@@ -1667,12 +1854,12 @@ function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
1667
1854
  if (!sourceSkill) return void 0;
1668
1855
  const expanded = expandHome(sourceSkill);
1669
1856
  const direct = resolve(expanded);
1670
- if (existsSync7(direct)) return direct;
1857
+ if (existsSync8(direct)) return direct;
1671
1858
  const name = basename2(sourceSkill);
1672
1859
  const roots = sourceSkillRoots(env2);
1673
1860
  for (const root of roots) {
1674
- const candidate = join9(root, name);
1675
- if (existsSync7(candidate)) return candidate;
1861
+ const candidate = join11(root, name);
1862
+ if (existsSync8(candidate)) return candidate;
1676
1863
  }
1677
1864
  const searched = roots.length ? ` Searched roots: ${roots.join(", ")}.` : "";
1678
1865
  const hint = `${searched} Add project-specific roots with ${PROJECT_SOURCE_SKILL_ROOTS_ENV}.`;
@@ -1753,7 +1940,7 @@ function planProjectInit(input) {
1753
1940
  }));
1754
1941
  }
1755
1942
  actions.push(
1756
- { kind: "project.write-manifest", path: join9(targetDir, ".project.json"), manifest },
1943
+ { kind: "project.write-manifest", path: join11(targetDir, ".project.json"), manifest },
1757
1944
  {
1758
1945
  kind: "ticket-provider.create-or-link",
1759
1946
  enabled: live,
@@ -1806,7 +1993,7 @@ async function executeProjectInitPlan(plan) {
1806
1993
  }
1807
1994
  if (result.status !== 0) {
1808
1995
  errors.push(`copier exited with status ${result.status ?? "unknown"}`);
1809
- if (existsSync7(action.targetDir)) changedFiles.push(action.targetDir);
1996
+ if (existsSync8(action.targetDir)) changedFiles.push(action.targetDir);
1810
1997
  break;
1811
1998
  }
1812
1999
  changedFiles.push(action.targetDir);
@@ -1814,9 +2001,9 @@ async function executeProjectInitPlan(plan) {
1814
2001
  mkdirSync4(dirname4(action.path), { recursive: true });
1815
2002
  const next = `${JSON.stringify(action.manifest, null, 2)}
1816
2003
  `;
1817
- const current = existsSync7(action.path) ? readFileSync3(action.path, "utf8") : void 0;
2004
+ const current = existsSync8(action.path) ? readFileSync4(action.path, "utf8") : void 0;
1818
2005
  if (current !== next) {
1819
- writeFileSync4(action.path, next, "utf8");
2006
+ writeFileSync5(action.path, next, "utf8");
1820
2007
  changedFiles.push(action.path);
1821
2008
  }
1822
2009
  } else if (action.kind === "registry.upsert") {
@@ -1879,7 +2066,7 @@ function getProject(registry, slug) {
1879
2066
  return project;
1880
2067
  }
1881
2068
  function buildCommonProjectCopierAction(input) {
1882
- const templateDir = join9(input.pjanglerRoot, "templates", "commonproject");
2069
+ const templateDir = join11(input.pjanglerRoot, "templates", "commonproject");
1883
2070
  const data = {
1884
2071
  project_name: input.projectName,
1885
2072
  project_description: input.projectDescription ?? "",
@@ -1908,7 +2095,7 @@ function buildCommonProjectCopierAction(input) {
1908
2095
  function resolvePjanglerRoot() {
1909
2096
  let dir = dirname4(new URL(import.meta.url).pathname);
1910
2097
  while (dir !== dirname4(dir)) {
1911
- 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;
1912
2099
  dir = dirname4(dir);
1913
2100
  }
1914
2101
  return resolve(process.cwd());
@@ -1940,7 +2127,7 @@ function validateProjectRecord(project, key) {
1940
2127
  }
1941
2128
  function expandHome(path) {
1942
2129
  if (path === "~") return homedir3();
1943
- if (path.startsWith("~/")) return join9(homedir3(), path.slice(2));
2130
+ if (path.startsWith("~/")) return join11(homedir3(), path.slice(2));
1944
2131
  return path;
1945
2132
  }
1946
2133
  function isRecord(value) {
@@ -1957,16 +2144,16 @@ function resolveTemplateRoot() {
1957
2144
  try {
1958
2145
  let dir = dirname5(fileURLToPath2(import.meta.url));
1959
2146
  for (let i = 0; i < 8; i++) {
1960
- candidates.push(join10(dir, "templates", "commonproject", "template"));
2147
+ candidates.push(join12(dir, "templates", "commonproject", "template"));
1961
2148
  const parent = dirname5(dir);
1962
2149
  if (parent === dir) break;
1963
2150
  dir = parent;
1964
2151
  }
1965
2152
  } catch {
1966
2153
  }
1967
- candidates.push(join10(homedir4(), "code", "pjangler", "templates", "commonproject", "template"));
2154
+ candidates.push(join12(homedir4(), "code", "pjangler", "templates", "commonproject", "template"));
1968
2155
  for (const c of candidates) {
1969
- if (existsSync8(join10(c, ".agents", "hooks", "hooks.master.json"))) return c;
2156
+ if (existsSync9(join12(c, ".agents", "hooks", "hooks.master.json"))) return c;
1970
2157
  }
1971
2158
  throw new Error(
1972
2159
  "Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
@@ -1985,18 +2172,17 @@ var CopyAgentHooksTree = class extends Command {
1985
2172
  }
1986
2173
  const items = [
1987
2174
  { rel: ".agents/hooks", dir: true },
2175
+ { rel: ".agents/skills.json", dir: false },
1988
2176
  { rel: ".agents/local.example.json", dir: false },
1989
- { rel: ".mise/scripts/link-project-skills-to-clis.sh", dir: false },
1990
- { rel: ".mise/scripts/unlink-project-skills-from-clis.sh", dir: false },
1991
2177
  { rel: ".mise/scripts/hindsight-setup.sh", dir: false }
1992
2178
  ];
1993
2179
  const created = [];
1994
2180
  const skipped = [];
1995
2181
  for (const { rel, dir } of items) {
1996
- const src = join10(templateRoot, rel);
1997
- const dest = join10(this.context.targetDir, rel);
1998
- if (!existsSync8(src)) continue;
1999
- 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) {
2000
2186
  skipped.push(rel);
2001
2187
  continue;
2002
2188
  }
@@ -2022,25 +2208,21 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
2022
2208
  if (!resolveAgentHooksLayer()) {
2023
2209
  return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
2024
2210
  }
2025
- const misePath = join10(this.context.targetDir, "mise.toml");
2026
- if (!existsSync8(misePath)) {
2211
+ const misePath = join12(this.context.targetDir, "mise.toml");
2212
+ if (!existsSync9(misePath)) {
2027
2213
  return {
2028
2214
  success: false,
2029
2215
  message: "\u26A0\uFE0F No mise.toml found \u2014 run `pjangler init mise` first, then re-run."
2030
2216
  };
2031
2217
  }
2032
- let content = readFileSync4(misePath, "utf8");
2218
+ let content = readFileSync5(misePath, "utf8");
2033
2219
  if (content.includes(_WireMiseAgentHooks.MARKER)) {
2034
2220
  return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
2035
2221
  }
2036
2222
  const cr = _WireMiseAgentHooks.CR;
2037
- const enterAdds = [
2038
- ` "${cr}/.mise/scripts/link-project-skills-to-clis.sh",`,
2039
- ` "${cr}/.agents/hooks/sync.py --install --quiet",`
2040
- ].join("\n");
2223
+ const enterAdds = [` "sync-skills.py --scope project",`, ` "${cr}/.agents/hooks/sync.py --install --quiet",`].join("\n");
2041
2224
  const leaveBlock = [
2042
2225
  "leave = [",
2043
- ` "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh",`,
2044
2226
  ` "${cr}/.agents/hooks/sync.py --uninstall --quiet",`,
2045
2227
  "]"
2046
2228
  ].join("\n");
@@ -2057,7 +2239,6 @@ ${enterAdds}${close}`;
2057
2239
  content = content.replace(leaveRe, (_m, head, close) => {
2058
2240
  const sep = /[,[]\s*$/.test(head) ? "" : ",";
2059
2241
  return `${head}${sep}
2060
- "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh",
2061
2242
  "${cr}/.agents/hooks/sync.py --uninstall --quiet",${close}`;
2062
2243
  });
2063
2244
  } else {
@@ -2070,6 +2251,14 @@ ${leaveBlock}`);
2070
2251
  "",
2071
2252
  _WireMiseAgentHooks.MARKER + " (generated \u2014 see .agents/hooks/README.md)",
2072
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]]",
2073
2262
  'patterns = [".agents/hooks/hooks.master.json"]',
2074
2263
  'task = "hooks-sync"',
2075
2264
  "",
@@ -2085,18 +2274,6 @@ ${leaveBlock}`);
2085
2274
  'description = "Remove per-user agent-hook injections (codex/kimi/hermes)"',
2086
2275
  `run = "${cr}/.agents/hooks/sync.py --uninstall"`,
2087
2276
  "",
2088
- "[tasks.link-project-skills-to-clis]",
2089
- 'description = "Fan .agents/skills out to each agent CLI (honors local.json)"',
2090
- `run = "${cr}/.mise/scripts/link-project-skills-to-clis.sh"`,
2091
- "",
2092
- "[tasks.unlink-project-skills-from-clis]",
2093
- 'description = "Remove project skill symlinks from shared per-CLI dirs"',
2094
- `run = "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh"`,
2095
- "",
2096
- "[tasks.skills-relink]",
2097
- 'description = "Re-fan the project skill set to all CLIs"',
2098
- `run = "${cr}/.mise/scripts/link-project-skills-to-clis.sh"`,
2099
- "",
2100
2277
  "[tasks.hindsight-setup]",
2101
2278
  `description = "Provision this dev's shared project Hindsight key from 1Password into .env"`,
2102
2279
  `run = "${cr}/.mise/scripts/hindsight-setup.sh"`,
@@ -2105,7 +2282,7 @@ ${leaveBlock}`);
2105
2282
  ""
2106
2283
  ].join("\n");
2107
2284
  content = content.replace(/\n*$/, "\n") + appended;
2108
- if (!this.context.dryRun) writeFileSync5(misePath, content);
2285
+ if (!this.context.dryRun) writeFileSync6(misePath, content);
2109
2286
  if (wiredHooks) {
2110
2287
  return { success: true, message: this.formatMessage("\u2705 Wired mise.toml ([hooks] enter/leave + tasks)") };
2111
2288
  }
@@ -2114,8 +2291,8 @@ ${leaveBlock}`);
2114
2291
  message: this.formatMessage(
2115
2292
  `\u2705 Added agent-hooks tasks to mise.toml.
2116
2293
  \u26A0\uFE0F Could not find a [hooks].enter array to extend \u2014 add these to your [hooks] block manually:
2117
- enter += "${cr}/.mise/scripts/link-project-skills-to-clis.sh", "${cr}/.agents/hooks/sync.py --install --quiet"
2118
- 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"`
2119
2296
  )
2120
2297
  };
2121
2298
  }
@@ -2130,10 +2307,25 @@ var AgentHooksRecipe = class extends Recipe {
2130
2307
  printNextSteps() {
2131
2308
  console.log("\u{1FA9D} Agent-hooks layer installed!");
2132
2309
  console.log(" Next steps:");
2133
- console.log(" 1. mise run hooks-sync # generate .claude/settings.json + inject codex/kimi/hermes");
2134
- console.log(" 2. git add .claude/settings.json .agents/hooks && commit (codex/kimi/hermes are per-dev)");
2135
- console.log(" 3. mise run hindsight-setup # set HINDSIGHT_OP_KEY_REF to your 1Password item first");
2136
- 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");
2137
2329
  }
2138
2330
  };
2139
2331
 
@@ -2176,6 +2368,12 @@ var RECIPE_REGISTRY = {
2176
2368
  description: "Retrofit the project-scoped agent-hooks + skill fan-out layer (Claude/Codex/Kimi/Hermes hooks via mise enter/leave)",
2177
2369
  class: AgentHooksRecipe,
2178
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"]
2179
2377
  }
2180
2378
  };
2181
2379
  var COMMAND_REGISTRY = {
@@ -2244,6 +2442,12 @@ var COMMAND_REGISTRY = {
2244
2442
  description: "Create .env.example file",
2245
2443
  group: "environment",
2246
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
2247
2451
  }
2248
2452
  };
2249
2453
  function getRecipeNames() {
@@ -2259,15 +2463,15 @@ function createRecipe(name, context) {
2259
2463
  }
2260
2464
 
2261
2465
  // src/utils/version.ts
2262
- import { readFileSync as readFileSync5 } from "node:fs";
2263
- 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";
2264
2468
  import { fileURLToPath as fileURLToPath3 } from "node:url";
2265
2469
  var PJANGLER_VERSION = (() => {
2266
2470
  try {
2267
2471
  let dir = dirname6(fileURLToPath3(import.meta.url));
2268
2472
  for (let i = 0; i < 4; i++) {
2269
2473
  try {
2270
- const raw = readFileSync5(join11(dir, "package.json"), "utf8");
2474
+ const raw = readFileSync6(join13(dir, "package.json"), "utf8");
2271
2475
  return JSON.parse(raw).version ?? "0.0.0";
2272
2476
  } catch {
2273
2477
  const parent = dirname6(dir);
@@ -2281,29 +2485,39 @@ var PJANGLER_VERSION = (() => {
2281
2485
  })();
2282
2486
 
2283
2487
  // src/parity/index.ts
2284
- 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";
2285
- 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";
2286
2490
  import { fileURLToPath as fileURLToPath4 } from "node:url";
2287
2491
  import { homedir as homedir5 } from "node:os";
2288
2492
  import { spawnSync as spawnSync6 } from "node:child_process";
2289
2493
  import YAML3 from "yaml";
2290
2494
  var LINK_AGENTFILES_SCRIPT = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'";
2291
2495
  var OP_INJECT_SCRIPT = "op inject -i .env.op > .env";
2496
+ var SYNC_SKILLS_SCRIPT = "sync-skills.py --scope project";
2292
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";
2293
2499
  var HOOKS_COMMENT_HEADER = `# This block will handle the linking of
2294
2500
  # agent files to the main AGENTS.md file.
2295
2501
  #
2296
2502
  # TODO: Ensure this works for all levels of nesting.
2297
2503
  # i.e. All linked agent files MUST be siblings at
2298
2504
  # any given level of nesting.`;
2299
- 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];
2300
2506
  var LINK_AGENTFILES_WATCH_TASK_BLOCK = `[[watch_files]]
2301
2507
  patterns = ["AGENTS.md"]
2302
2508
  task = "link-agentfiles"
2303
2509
 
2510
+ [[watch_files]]
2511
+ patterns = [".agents/skills.json"]
2512
+ task = "skills-sync"
2513
+
2304
2514
  [tasks.link-agentfiles]
2305
2515
  description = "Symlink all agent files to AGENTS.md"
2306
- 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"`;
2307
2521
  var VERSIONING_BLOCK = `# >>> mise-versioning >>> (managed block \u2014 do not edit by hand; re-run init to update)
2308
2522
  [tasks."version"]
2309
2523
  description = "Print the current version (vX.Y.Z)"
@@ -2333,7 +2547,7 @@ run = "'{{config_root}}/.mise/scripts/versioning.sh' sync"
2333
2547
  function resolvePjanglerRoot2() {
2334
2548
  let dir = dirname7(fileURLToPath4(import.meta.url));
2335
2549
  while (dir !== dirname7(dir)) {
2336
- 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"))) {
2337
2551
  return dir;
2338
2552
  }
2339
2553
  dir = dirname7(dir);
@@ -2344,17 +2558,17 @@ function normalizeNewlines(value) {
2344
2558
  return value.replace(/\r\n/g, "\n");
2345
2559
  }
2346
2560
  function readText(path) {
2347
- return normalizeNewlines(readFileSync6(path, "utf8"));
2561
+ return normalizeNewlines(readFileSync7(path, "utf8"));
2348
2562
  }
2349
2563
  function safeReadText(path) {
2350
- return existsSync9(path) ? readText(path) : null;
2564
+ return existsSync10(path) ? readText(path) : null;
2351
2565
  }
2352
2566
  function ensureParent(path) {
2353
2567
  mkdirSync6(dirname7(path), { recursive: true });
2354
2568
  }
2355
2569
  function writeText(path, content) {
2356
2570
  ensureParent(path);
2357
- writeFileSync6(path, content);
2571
+ writeFileSync7(path, content);
2358
2572
  }
2359
2573
  function tryParseJson(text2) {
2360
2574
  if (!text2) return null;
@@ -2371,7 +2585,7 @@ function titleCaseSlug(slug) {
2371
2585
  return slug.split(/[-_]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
2372
2586
  }
2373
2587
  function readSymlinkTarget(path) {
2374
- if (!existsSync9(path)) return null;
2588
+ if (!existsSync10(path)) return null;
2375
2589
  try {
2376
2590
  return readlinkSync(path);
2377
2591
  } catch {
@@ -2379,7 +2593,7 @@ function readSymlinkTarget(path) {
2379
2593
  }
2380
2594
  }
2381
2595
  function ensureSymlink(path, target, dryRun) {
2382
- if (existsSync9(path)) {
2596
+ if (existsSync10(path)) {
2383
2597
  const stat = lstatSync(path);
2384
2598
  if (stat.isSymbolicLink()) {
2385
2599
  const current = readSymlinkTarget(path);
@@ -2396,11 +2610,11 @@ function ensureSymlink(path, target, dryRun) {
2396
2610
  return { changed: true };
2397
2611
  }
2398
2612
  function bootstrapAgentsFile(repoRoot, dryRun) {
2399
- const agentsPath = join12(repoRoot, "AGENTS.md");
2400
- if (existsSync9(agentsPath)) return { changedFiles: [], details: [] };
2613
+ const agentsPath = join14(repoRoot, "AGENTS.md");
2614
+ if (existsSync10(agentsPath)) return { changedFiles: [], details: [] };
2401
2615
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
2402
- const source = join12(repoRoot, file);
2403
- if (!existsSync9(source)) continue;
2616
+ const source = join14(repoRoot, file);
2617
+ if (!existsSync10(source)) continue;
2404
2618
  const stat = lstatSync(source);
2405
2619
  if (stat.isSymbolicLink()) continue;
2406
2620
  if (stat.isFile()) {
@@ -2409,8 +2623,8 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
2409
2623
  }
2410
2624
  return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
2411
2625
  }
2412
- const readmePath = join12(repoRoot, "README.md");
2413
- if (existsSync9(readmePath)) {
2626
+ const readmePath = join14(repoRoot, "README.md");
2627
+ if (existsSync10(readmePath)) {
2414
2628
  const stat = lstatSync(readmePath);
2415
2629
  if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
2416
2630
  if (!dryRun) copyFileSync(readmePath, agentsPath);
@@ -2449,12 +2663,12 @@ function yamlGet(text2, keyPath) {
2449
2663
  return "";
2450
2664
  }
2451
2665
  function discoverRoles(repoRoot) {
2452
- const rolesDir = join12(repoRoot, "agents", "hermes");
2453
- if (!existsSync9(rolesDir)) return [];
2666
+ const rolesDir = join14(repoRoot, "agents", "hermes");
2667
+ if (!existsSync10(rolesDir)) return [];
2454
2668
  return readdirSync2(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
2455
- const roleDir = join12(rolesDir, entry.name);
2456
- const roleYamlPath = join12(roleDir, "role.yaml");
2457
- 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;
2458
2672
  const text2 = readText(roleYamlPath);
2459
2673
  const runtimeRepoRaw = yamlGet(text2, "runtime.github_repo");
2460
2674
  return {
@@ -2482,7 +2696,7 @@ function discoverRoles(repoRoot) {
2482
2696
  }).filter((value) => Boolean(value));
2483
2697
  }
2484
2698
  function registryPath(homeDir) {
2485
- return join12(homeDir, ".hermes", "agents-registry.yaml");
2699
+ return join14(homeDir, ".hermes", "agents-registry.yaml");
2486
2700
  }
2487
2701
  function systemctlUser(args) {
2488
2702
  const result = spawnSync6("systemctl", ["--user", ...args], { encoding: "utf8" });
@@ -2493,8 +2707,8 @@ function systemctlUser(args) {
2493
2707
  };
2494
2708
  }
2495
2709
  function templateScript(ctx, name) {
2496
- const source = join12(ctx.pjanglerRoot, ".mise", "scripts", name);
2497
- return existsSync9(source) ? readText(source) : void 0;
2710
+ const source = join14(ctx.pjanglerRoot, ".mise", "scripts", name);
2711
+ return existsSync10(source) ? readText(source) : void 0;
2498
2712
  }
2499
2713
  function templateVersioningScript(ctx) {
2500
2714
  return templateScript(ctx, "versioning.sh");
@@ -2506,8 +2720,8 @@ function resolveAgentHooksLayer2(ctx) {
2506
2720
  const override = process.env.PJ_AGENT_HOOKS_LAYER;
2507
2721
  if (override === "0" || override === "false") return false;
2508
2722
  if (override === "1" || override === "true") return true;
2509
- if (existsSync9(join12(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
2510
- 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"));
2511
2725
  }
2512
2726
  function evaluateMiseConditionals(template, agentHooksLayer) {
2513
2727
  const out = [];
@@ -2537,19 +2751,36 @@ function renderGeneratedProjectMiseToml(ctx, template) {
2537
2751
  return evaluateMiseConditionals(template, resolveAgentHooksLayer2(ctx)).replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
2538
2752
  }
2539
2753
  function ensureMiseTomlFromTemplate(ctx, changedFiles) {
2540
- const targetPath = join12(ctx.repoRoot, "mise.toml");
2541
- if (existsSync9(targetPath)) return false;
2542
- const sourcePath = join12(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
2543
- 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;
2544
2758
  changedFiles.push(targetPath);
2545
2759
  if (!ctx.dryRun) {
2546
2760
  writeText(targetPath, renderGeneratedProjectMiseToml(ctx, readText(sourcePath)));
2547
2761
  }
2548
2762
  return true;
2549
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
+ }
2550
2781
  function templateVersionFilesConf(ctx, repoRoot) {
2551
- const packageJson = join12(repoRoot, "package.json");
2552
- 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";
2553
2784
  }
2554
2785
  function replaceOrAppendManagedBlock(text2, startMarker, block, beforePattern) {
2555
2786
  if (startMarker.test(text2)) {
@@ -2573,7 +2804,7 @@ var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/herm
2573
2804
  function requiredMisePathEntries(ctx) {
2574
2805
  const required = [...BASE_MISE_PATH_ENTRIES];
2575
2806
  for (const candidate of CONDITIONAL_HERMES_PATHS) {
2576
- 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);
2577
2808
  }
2578
2809
  return required;
2579
2810
  }
@@ -2674,6 +2905,9 @@ function stripTomlStringsAndComments(line) {
2674
2905
  function isManagedHookEntry(value) {
2675
2906
  const trimmed = value.trim();
2676
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;
2677
2911
  return /link-agentfiles\.sh'?\s*$/.test(trimmed);
2678
2912
  }
2679
2913
  function normalizeHookScript(script) {
@@ -2776,12 +3010,17 @@ function upsertLinkAgentfilesHooks(text2) {
2776
3010
  function upsertLinkAgentfilesBlock(text2, ctx) {
2777
3011
  const withPath = upsertMisePath(text2, requiredMisePathEntries(ctx));
2778
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 });
2779
3017
  cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /AGENTS\.md/, { includePrecedingComments: false });
3018
+ cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /\.agents\/skills\.json/, { includePrecedingComments: false });
2780
3019
  cleaned = upsertLinkAgentfilesHooks(cleaned);
2781
3020
  return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
2782
3021
  }
2783
3022
  function readProjectJson(ctx) {
2784
- return tryParseJson(safeReadText(join12(ctx.repoRoot, ".project.json")));
3023
+ return tryParseJson(safeReadText(join14(ctx.repoRoot, ".project.json")));
2785
3024
  }
2786
3025
  function boolSetting(value, fallback) {
2787
3026
  if (typeof value === "boolean") return value;
@@ -2856,12 +3095,12 @@ function canonicalProjectJson(ctx) {
2856
3095
  };
2857
3096
  }
2858
3097
  function projectJsonFinding(ctx) {
2859
- const projectPath = join12(ctx.repoRoot, ".project.json");
2860
- const planeJsonPath = join12(ctx.repoRoot, ".plane.json");
3098
+ const projectPath = join14(ctx.repoRoot, ".project.json");
3099
+ const planeJsonPath = join14(ctx.repoRoot, ".plane.json");
2861
3100
  const details = [];
2862
3101
  const data = readProjectJson(ctx);
2863
3102
  const roles = discoverRoles(ctx.repoRoot);
2864
- if (!existsSync9(projectPath)) {
3103
+ if (!existsSync10(projectPath)) {
2865
3104
  return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json missing", details: [], fixable: true };
2866
3105
  }
2867
3106
  if (!data) {
@@ -2896,7 +3135,7 @@ function projectJsonFinding(ctx) {
2896
3135
  for (const key of ["enabled", "grace_hours", "auto_review"]) {
2897
3136
  if (!(key in reconcile)) details.push(`automation.reconcile.${key} missing`);
2898
3137
  }
2899
- 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");
2900
3139
  return {
2901
3140
  id: "sot.project-json",
2902
3141
  title: "Canonical .project.json",
@@ -2977,17 +3216,17 @@ exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" HERMES_OAUTH
2977
3216
  `.replace(/\u0010/g, "$");
2978
3217
  }
2979
3218
  function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
2980
- if (!existsSync9(sourceDir)) return;
3219
+ if (!existsSync10(sourceDir)) return;
2981
3220
  mkdirSync6(targetDir, { recursive: true });
2982
3221
  for (const entry of readdirSync2(sourceDir, { withFileTypes: true })) {
2983
- const sourcePath = join12(sourceDir, entry.name);
3222
+ const sourcePath = join14(sourceDir, entry.name);
2984
3223
  if (skip?.(sourcePath)) continue;
2985
- const targetPath = join12(targetDir, entry.name);
3224
+ const targetPath = join14(targetDir, entry.name);
2986
3225
  if (entry.isDirectory()) {
2987
3226
  copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
2988
3227
  continue;
2989
3228
  }
2990
- if (existsSync9(targetPath)) continue;
3229
+ if (existsSync10(targetPath)) continue;
2991
3230
  changedFiles.push(targetPath);
2992
3231
  if (!dryRun) {
2993
3232
  ensureParent(targetPath);
@@ -2996,7 +3235,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
2996
3235
  }
2997
3236
  }
2998
3237
  function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
2999
- const gitmodulesPath = join12(repoRoot, ".gitmodules");
3238
+ const gitmodulesPath = join14(repoRoot, ".gitmodules");
3000
3239
  const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
3001
3240
  const owner = role.runtimeOwner || "delorenj";
3002
3241
  const block = `[submodule "agents/hermes/${role.role}/runtime"]
@@ -3163,7 +3402,7 @@ function runBmadInstall(repoRoot) {
3163
3402
  return { ok: true };
3164
3403
  }
3165
3404
  function readInstalledBmadVersion(repoRoot) {
3166
- const raw = safeReadText(join12(repoRoot, "_bmad", "_config", "manifest.yaml"));
3405
+ const raw = safeReadText(join14(repoRoot, "_bmad", "_config", "manifest.yaml"));
3167
3406
  if (!raw) return void 0;
3168
3407
  try {
3169
3408
  const parsed = YAML3.parse(raw);
@@ -3174,8 +3413,8 @@ function readInstalledBmadVersion(repoRoot) {
3174
3413
  }
3175
3414
  }
3176
3415
  function bmadCachePath(homeDir) {
3177
- const cacheRoot = process.env.XDG_CACHE_HOME?.trim() || join12(homeDir, ".cache");
3178
- 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");
3179
3418
  }
3180
3419
  function readBmadDistTagsCache(homeDir) {
3181
3420
  const raw = safeReadText(bmadCachePath(homeDir));
@@ -3218,7 +3457,7 @@ function resolveBmadDistTags(homeDir) {
3218
3457
  try {
3219
3458
  const path = bmadCachePath(homeDir);
3220
3459
  mkdirSync6(dirname7(path), { recursive: true });
3221
- writeFileSync6(path, JSON.stringify({ fetchedAt: Date.now(), distTags: fetched }, null, 2));
3460
+ writeFileSync7(path, JSON.stringify({ fetchedAt: Date.now(), distTags: fetched }, null, 2));
3222
3461
  } catch {
3223
3462
  }
3224
3463
  return { distTags: fetched, stale: false };
@@ -3263,14 +3502,14 @@ var RULES = [
3263
3502
  id: "mise.config-root",
3264
3503
  title: "mise config_root + AGENTS link hooks",
3265
3504
  audit: (ctx) => {
3266
- const misePath = join12(ctx.repoRoot, "mise.toml");
3267
- if (!existsSync9(misePath)) {
3505
+ const misePath = join14(ctx.repoRoot, "mise.toml");
3506
+ if (!existsSync10(misePath)) {
3268
3507
  return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
3269
3508
  }
3270
3509
  const text2 = readText(misePath);
3271
3510
  const details = [];
3272
- const linkAgentfilesPath = join12(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
3273
- 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");
3274
3513
  const pathValues = [...(text2.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
3275
3514
  const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
3276
3515
  if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
@@ -3288,10 +3527,10 @@ var RULES = [
3288
3527
  };
3289
3528
  },
3290
3529
  migrate: (ctx, finding) => {
3291
- const path = join12(ctx.repoRoot, "mise.toml");
3530
+ const path = join14(ctx.repoRoot, "mise.toml");
3292
3531
  const changedFiles = [];
3293
3532
  const details = [];
3294
- if (!existsSync9(path)) {
3533
+ if (!existsSync10(path)) {
3295
3534
  if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
3296
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: [] };
3297
3536
  }
@@ -3307,7 +3546,7 @@ var RULES = [
3307
3546
  if (!ctx.dryRun) writeText(path, next);
3308
3547
  text2 = next;
3309
3548
  }
3310
- const linkAgentfilesPath = join12(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
3549
+ const linkAgentfilesPath = join14(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
3311
3550
  const expectedScript = templateLinkAgentfilesScript(ctx);
3312
3551
  if (expectedScript === void 0) {
3313
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: [] };
@@ -3316,7 +3555,7 @@ var RULES = [
3316
3555
  changedFiles.push(linkAgentfilesPath);
3317
3556
  if (!ctx.dryRun) {
3318
3557
  writeText(linkAgentfilesPath, expectedScript);
3319
- chmodSync2(linkAgentfilesPath, 493);
3558
+ chmodSync4(linkAgentfilesPath, 493);
3320
3559
  }
3321
3560
  }
3322
3561
  return {
@@ -3334,13 +3573,13 @@ var RULES = [
3334
3573
  title: "managed mise versioning block",
3335
3574
  audit: (ctx) => {
3336
3575
  const details = [];
3337
- const misePath = join12(ctx.repoRoot, "mise.toml");
3338
- const versioningPath = join12(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
3339
- 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");
3340
3579
  const text2 = safeReadText(misePath);
3341
3580
  if (!text2?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
3342
- if (!existsSync9(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
3343
- 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");
3344
3583
  return {
3345
3584
  id: "mise.versioning",
3346
3585
  title: "managed mise versioning block",
@@ -3353,8 +3592,8 @@ var RULES = [
3353
3592
  migrate: (ctx, finding) => {
3354
3593
  const changedFiles = [];
3355
3594
  const details = [];
3356
- const misePath = join12(ctx.repoRoot, "mise.toml");
3357
- if (!existsSync9(misePath)) {
3595
+ const misePath = join14(ctx.repoRoot, "mise.toml");
3596
+ if (!existsSync10(misePath)) {
3358
3597
  if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
3359
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: [] };
3360
3599
  }
@@ -3378,7 +3617,7 @@ var RULES = [
3378
3617
  if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
3379
3618
  if (!ctx.dryRun) writeText(misePath, nextMise);
3380
3619
  }
3381
- const versioningPath = join12(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
3620
+ const versioningPath = join14(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
3382
3621
  const expectedScript = templateVersioningScript(ctx);
3383
3622
  if (expectedScript === void 0) {
3384
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: [] };
@@ -3387,10 +3626,10 @@ var RULES = [
3387
3626
  changedFiles.push(versioningPath);
3388
3627
  if (!ctx.dryRun) {
3389
3628
  writeText(versioningPath, expectedScript);
3390
- chmodSync2(versioningPath, 493);
3629
+ chmodSync4(versioningPath, 493);
3391
3630
  }
3392
3631
  }
3393
- const manifestPath = join12(ctx.repoRoot, ".mise", "version-files.conf");
3632
+ const manifestPath = join14(ctx.repoRoot, ".mise", "version-files.conf");
3394
3633
  const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
3395
3634
  if (safeReadText(manifestPath) !== expectedManifest) {
3396
3635
  changedFiles.push(manifestPath);
@@ -3406,13 +3645,125 @@ var RULES = [
3406
3645
  };
3407
3646
  }
3408
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
+ },
3409
3760
  {
3410
3761
  id: "sot.agent-symlinks",
3411
3762
  title: "AGENTS/CLAUDE/GEMINI symlink contract",
3412
3763
  audit: (ctx) => {
3413
- const agentsPath = join12(ctx.repoRoot, "AGENTS.md");
3414
- if (!existsSync9(agentsPath)) {
3415
- 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)));
3416
3767
  if (fallbackSources.length === 0) {
3417
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 };
3418
3769
  }
@@ -3427,7 +3778,7 @@ var RULES = [
3427
3778
  }
3428
3779
  const details = [];
3429
3780
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
3430
- const full = join12(ctx.repoRoot, file);
3781
+ const full = join14(ctx.repoRoot, file);
3431
3782
  const target = readSymlinkTarget(full);
3432
3783
  if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
3433
3784
  }
@@ -3451,7 +3802,7 @@ var RULES = [
3451
3802
  return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
3452
3803
  }
3453
3804
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
3454
- const full = join12(ctx.repoRoot, file);
3805
+ const full = join14(ctx.repoRoot, file);
3455
3806
  const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
3456
3807
  if (result.blocked) blockedDetails.push(result.blocked);
3457
3808
  if (result.changed) changedFiles.push(full);
@@ -3473,7 +3824,7 @@ var RULES = [
3473
3824
  migrate: (ctx, finding) => {
3474
3825
  const changedFiles = [];
3475
3826
  const details = [];
3476
- const path = join12(ctx.repoRoot, ".project.json");
3827
+ const path = join14(ctx.repoRoot, ".project.json");
3477
3828
  const existing = readProjectJson(ctx) ?? {};
3478
3829
  const canonical = canonicalProjectJson(ctx);
3479
3830
  const merged = { ...existing, ...canonical };
@@ -3483,10 +3834,10 @@ var RULES = [
3483
3834
  changedFiles.push(path);
3484
3835
  if (!ctx.dryRun) writeText(path, expected);
3485
3836
  }
3486
- const planeJson = join12(ctx.repoRoot, ".plane.json");
3487
- if (existsSync9(planeJson)) {
3837
+ const planeJson = join14(ctx.repoRoot, ".plane.json");
3838
+ if (existsSync10(planeJson)) {
3488
3839
  const backup = `${planeJson}.migrated-backup`;
3489
- if (existsSync9(backup)) {
3840
+ if (existsSync10(backup)) {
3490
3841
  details.push(`cannot back up .plane.json because ${relative(ctx.repoRoot, backup)} already exists`);
3491
3842
  } else {
3492
3843
  changedFiles.push(backup);
@@ -3508,8 +3859,8 @@ var RULES = [
3508
3859
  title: ".env.op + gitignore secrets contract",
3509
3860
  audit: (ctx) => {
3510
3861
  const details = [];
3511
- const envOp = safeReadText(join12(ctx.repoRoot, ".env.op"));
3512
- const gitignore = safeReadText(join12(ctx.repoRoot, ".gitignore"));
3862
+ const envOp = safeReadText(join14(ctx.repoRoot, ".env.op"));
3863
+ const gitignore = safeReadText(join14(ctx.repoRoot, ".gitignore"));
3513
3864
  if (!envOp) {
3514
3865
  details.push(".env.op missing");
3515
3866
  } else {
@@ -3535,12 +3886,12 @@ var RULES = [
3535
3886
  migrate: (ctx, finding) => {
3536
3887
  const changedFiles = [];
3537
3888
  const details = [];
3538
- const envOpPath = join12(ctx.repoRoot, ".env.op");
3539
- if (!existsSync9(envOpPath)) {
3889
+ const envOpPath = join14(ctx.repoRoot, ".env.op");
3890
+ if (!existsSync10(envOpPath)) {
3540
3891
  changedFiles.push(envOpPath);
3541
- 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")));
3542
3893
  }
3543
- const gitignorePath = join12(ctx.repoRoot, ".gitignore");
3894
+ const gitignorePath = join14(ctx.repoRoot, ".gitignore");
3544
3895
  const gitignore = safeReadText(gitignorePath) ?? "";
3545
3896
  const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
3546
3897
  # NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
@@ -3567,7 +3918,7 @@ var RULES = [
3567
3918
  title: ".copier-answers.yml provenance + drift report",
3568
3919
  audit: (ctx) => {
3569
3920
  const details = [];
3570
- const path = join12(ctx.repoRoot, ".copier-answers.yml");
3921
+ const path = join14(ctx.repoRoot, ".copier-answers.yml");
3571
3922
  const text2 = safeReadText(path);
3572
3923
  const project = readProjectJson(ctx);
3573
3924
  if (!text2) {
@@ -3598,12 +3949,12 @@ var RULES = [
3598
3949
  const changedFiles = [];
3599
3950
  const project = canonicalProjectJson(ctx);
3600
3951
  const text2 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
3601
- _src_path: ${join12(ctx.pjanglerRoot, "templates", "commonproject")}
3952
+ _src_path: ${join14(ctx.pjanglerRoot, "templates", "commonproject")}
3602
3953
  project_description: ${String(project.project_description)}
3603
3954
  project_name: ${String(project.project_name)}
3604
3955
  ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3605
3956
  `;
3606
- const path = join12(ctx.repoRoot, ".copier-answers.yml");
3957
+ const path = join14(ctx.repoRoot, ".copier-answers.yml");
3607
3958
  if (safeReadText(path) !== text2) {
3608
3959
  changedFiles.push(path);
3609
3960
  if (!ctx.dryRun) writeText(path, text2);
@@ -3622,14 +3973,14 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3622
3973
  id: "bmad.scaffold",
3623
3974
  title: "BMAD modules/docs scaffold",
3624
3975
  audit: (ctx) => {
3625
- const targetRoot = join12(ctx.repoRoot, "_bmad");
3976
+ const targetRoot = join14(ctx.repoRoot, "_bmad");
3626
3977
  const sentinels = [
3627
- join12("core", "config.yaml"),
3628
- join12("config.toml"),
3629
- join12("_config", "manifest.yaml"),
3630
- join12("bmm", "config.yaml")
3978
+ join14("core", "config.yaml"),
3979
+ join14("config.toml"),
3980
+ join14("_config", "manifest.yaml"),
3981
+ join14("bmm", "config.yaml")
3631
3982
  ];
3632
- const missing = sentinels.filter((file) => !existsSync9(join12(targetRoot, file)));
3983
+ const missing = sentinels.filter((file) => !existsSync10(join14(targetRoot, file)));
3633
3984
  return {
3634
3985
  id: "bmad.scaffold",
3635
3986
  title: "BMAD modules/docs scaffold",
@@ -3643,7 +3994,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3643
3994
  const changedFiles = [];
3644
3995
  if (ctx.dryRun) {
3645
3996
  for (const detail of finding.details) {
3646
- changedFiles.push(join12(ctx.repoRoot, detail));
3997
+ changedFiles.push(join14(ctx.repoRoot, detail));
3647
3998
  }
3648
3999
  return {
3649
4000
  id: finding.id,
@@ -3668,8 +4019,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3668
4019
  };
3669
4020
  }
3670
4021
  for (const detail of finding.details) {
3671
- if (existsSync9(join12(ctx.repoRoot, detail))) {
3672
- changedFiles.push(join12(ctx.repoRoot, detail));
4022
+ if (existsSync10(join14(ctx.repoRoot, detail))) {
4023
+ changedFiles.push(join14(ctx.repoRoot, detail));
3673
4024
  }
3674
4025
  }
3675
4026
  return {
@@ -3692,7 +4043,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3692
4043
  id: "bmad.version",
3693
4044
  title: "BMAD version currency",
3694
4045
  status: "skip",
3695
- 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",
3696
4047
  details: [],
3697
4048
  fixable: false
3698
4049
  };
@@ -3747,7 +4098,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3747
4098
  }
3748
4099
  const installed = readInstalledBmadVersion(ctx.repoRoot);
3749
4100
  const available = resolveBmadDistTags(ctx.homeDir)?.distTags?.[BMAD_TARGET_CHANNEL];
3750
- const manifestPath = join12(ctx.repoRoot, "_bmad", "_config", "manifest.yaml");
4101
+ const manifestPath = join14(ctx.repoRoot, "_bmad", "_config", "manifest.yaml");
3751
4102
  if (ctx.dryRun) {
3752
4103
  return {
3753
4104
  id: finding.id,
@@ -3794,11 +4145,11 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3794
4145
  }
3795
4146
  const details = [];
3796
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"]) {
3797
- 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))}`);
3798
4149
  }
3799
- const gitmodules = safeReadText(join12(ctx.repoRoot, ".gitmodules")) ?? "";
4150
+ const gitmodules = safeReadText(join14(ctx.repoRoot, ".gitmodules")) ?? "";
3800
4151
  if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
3801
- if (!profileMetaInheritsDefault(join12(role.roleDir, "runtime", "profile.yaml"))) {
4152
+ if (!profileMetaInheritsDefault(join14(role.roleDir, "runtime", "profile.yaml"))) {
3802
4153
  details.push("runtime/profile.yaml missing inherited default config metadata");
3803
4154
  }
3804
4155
  const registry = safeReadText(registryPath(ctx.homeDir));
@@ -3819,21 +4170,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3819
4170
  if (!role) {
3820
4171
  return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
3821
4172
  }
3822
- const templateRoleDir = join12(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
3823
- writeIfDifferent(join12(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
3824
- writeIfDifferent(join12(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
3825
- writeIfDifferent(join12(role.roleDir, ".gitignore"), readText(join12(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
3826
- copyMissingRecursive(join12(templateRoleDir, ".runtime-scaffold"), join12(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
3827
- copyMissingRecursive(join12(templateRoleDir, ".runtime-scaffold"), join12(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
3828
- copyMissingRecursive(join12(templateRoleDir, ".scripts"), join12(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
3829
- const promptSource = join12(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
3830
- const promptTarget = join12(role.roleDir, ".scripts", "sentinel.prompt.md");
3831
- 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)) {
3832
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);
3833
4184
  writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
3834
4185
  }
3835
4186
  upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
3836
- 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);
3837
4188
  if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
3838
4189
  const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
3839
4190
  if (registryUpdated) details.push(`updated ${registryUpdated}`);
@@ -3865,7 +4216,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3865
4216
  const details = [];
3866
4217
  for (const role of roles) {
3867
4218
  const roleRelDir = relative(ctx.repoRoot, role.roleDir);
3868
- const runtimeRelPath = join12(roleRelDir, "runtime");
4219
+ const runtimeRelPath = join14(roleRelDir, "runtime");
3869
4220
  const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
3870
4221
  cwd: ctx.repoRoot,
3871
4222
  encoding: "utf8"
@@ -3873,8 +4224,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3873
4224
  if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
3874
4225
  details.push(`submodule runtime is tracked in Git index at ${runtimeRelPath}`);
3875
4226
  }
3876
- const gitignorePath = join12(role.roleDir, ".gitignore");
3877
- if (existsSync9(gitignorePath)) {
4227
+ const gitignorePath = join14(role.roleDir, ".gitignore");
4228
+ if (existsSync10(gitignorePath)) {
3878
4229
  const content = safeReadText(gitignorePath) ?? "";
3879
4230
  const lines = content.split(/\r?\n/).map((line) => line.trim());
3880
4231
  if (!lines.includes("runtime/") && !lines.includes("runtime")) {
@@ -3899,7 +4250,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3899
4250
  const details = [];
3900
4251
  for (const role of roles) {
3901
4252
  const roleRelDir = relative(ctx.repoRoot, role.roleDir);
3902
- const runtimeRelPath = join12(roleRelDir, "runtime");
4253
+ const runtimeRelPath = join14(roleRelDir, "runtime");
3903
4254
  const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
3904
4255
  cwd: ctx.repoRoot,
3905
4256
  encoding: "utf8"
@@ -3914,10 +4265,10 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3914
4265
  });
3915
4266
  }
3916
4267
  }
3917
- const gitignorePath = join12(role.roleDir, ".gitignore");
4268
+ const gitignorePath = join14(role.roleDir, ".gitignore");
3918
4269
  let content = "";
3919
4270
  let isIgnored = false;
3920
- if (existsSync9(gitignorePath)) {
4271
+ if (existsSync10(gitignorePath)) {
3921
4272
  content = safeReadText(gitignorePath) ?? "";
3922
4273
  const lines = content.split(/\r?\n/).map((line) => line.trim());
3923
4274
  isIgnored = lines.includes("runtime/") || lines.includes("runtime");
@@ -3984,9 +4335,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3984
4335
  return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
3985
4336
  }
3986
4337
  for (const role of roles) {
3987
- const sysDir = join12(ctx.homeDir, ".config", "systemd", "user");
4338
+ const sysDir = join14(ctx.homeDir, ".config", "systemd", "user");
3988
4339
  const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`];
3989
- const allUnitsPresent = units.every((unit) => existsSync9(join12(sysDir, unit)));
4340
+ const allUnitsPresent = units.every((unit) => existsSync10(join14(sysDir, unit)));
3990
4341
  if (allUnitsPresent) {
3991
4342
  if (ctx.dryRun) {
3992
4343
  details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
@@ -3998,8 +4349,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
3998
4349
  }
3999
4350
  continue;
4000
4351
  }
4001
- for (const script of [join12(role.roleDir, ".scripts", "70-systemd.sh")]) {
4002
- if (!script || !existsSync9(script)) continue;
4352
+ for (const script of [join14(role.roleDir, ".scripts", "70-systemd.sh")]) {
4353
+ if (!script || !existsSync10(script)) continue;
4003
4354
  if (ctx.dryRun) {
4004
4355
  details.push(`would run: bash ${script}`);
4005
4356
  } else {
@@ -4026,7 +4377,7 @@ function writeIfDifferent(path, content, dryRun, changedFiles, mode) {
4026
4377
  changedFiles.push(path);
4027
4378
  if (!dryRun) {
4028
4379
  writeText(path, normalized);
4029
- if (mode) chmodSync2(path, mode);
4380
+ if (mode) chmodSync4(path, mode);
4030
4381
  }
4031
4382
  }
4032
4383
  function getParityRuleIds() {
@@ -4123,7 +4474,7 @@ var server = new McpServer({
4123
4474
  var TICKET_PROVIDER_SCHEMA = z.enum(["plane", "trello"]);
4124
4475
  function resolveTargetDir(targetDir) {
4125
4476
  const dir = resolve3(targetDir ?? process.cwd());
4126
- if (!existsSync10(dir)) {
4477
+ if (!existsSync11(dir)) {
4127
4478
  throw new Error(`Target directory does not exist: ${dir}`);
4128
4479
  }
4129
4480
  if (!statSync2(dir).isDirectory()) {
@@ -4134,7 +4485,7 @@ function resolveTargetDir(targetDir) {
4134
4485
  function resolvePjanglerRoot3() {
4135
4486
  let dir = dirname8(fileURLToPath5(import.meta.url));
4136
4487
  while (dir !== dirname8(dir)) {
4137
- 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"))) {
4138
4489
  return dir;
4139
4490
  }
4140
4491
  dir = dirname8(dir);
@@ -4336,8 +4687,8 @@ server.registerTool(
4336
4687
  const pjanglerRoot = resolvePjanglerRoot3();
4337
4688
  const projectSlug = input.projectSlug ?? slugify(input.projectName);
4338
4689
  const parentDir = resolve3(input.parentDir ?? process.cwd());
4339
- if (!existsSync10(parentDir) || !statSync2(parentDir).isDirectory()) throw new Error(`Parent directory does not exist: ${parentDir}`);
4340
- 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));
4341
4692
  const overwrite = input.overwrite ?? input.force ?? false;
4342
4693
  const dryRun = input.dryRun ?? true;
4343
4694
  const local = input.local ?? true;
@@ -4347,7 +4698,7 @@ server.registerTool(
4347
4698
  if (!skipPlane && ticketProvider === "plane" && !boardId) {
4348
4699
  throw new Error("boardId or planeProjectId is required when skipPlane=false for Plane; keep skipPlane=true for safe local bootstrap");
4349
4700
  }
4350
- 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)`);
4351
4702
  const plan = planProjectInit({
4352
4703
  name: input.projectName,
4353
4704
  description: input.projectDescription,