@delorenj/pjangler 1.2.18 → 1.2.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +884 -295
- package/dist/mcp-server.js +892 -303
- package/package.json +8 -2
- package/templates/commonproject/AGENTS.md +3 -3
- package/templates/commonproject/README.md +11 -12
- package/templates/commonproject/copier.yml +11 -31
- package/templates/commonproject/template/.agents/hooks/README.md +13 -26
- package/templates/commonproject/template/.agents/hooks/lib/local-config.sh +4 -14
- package/templates/commonproject/template/.agents/hooks/sync.py +5 -6
- package/templates/commonproject/template/.agents/local.example.json +2 -8
- package/templates/commonproject/template/.agents/skills.json +6 -0
- package/templates/commonproject/template/.project.json.jinja +9 -13
- package/templates/commonproject/template/mise.toml.jinja +9 -16
- package/templates/hermes-agent/README.md +9 -9
- package/templates/hermes-agent/config.example.toml +1 -66
- package/templates/hermes-agent/copier.yml +4 -3
- package/templates/hermes-agent/docs/architecture.md +9 -12
- package/templates/hermes-agent/docs/fleet-control-plane/README.md +1 -1
- package/templates/hermes-agent/docs/operations.md +6 -5
- package/templates/hermes-agent/docs/sentinel/README.md +9 -7
- package/templates/hermes-agent/docs/sentinel/architecture.md +1 -2
- package/templates/hermes-agent/docs/sentinel/development.md +12 -13
- package/templates/hermes-agent/docs/sentinel/providers.md +34 -15
- package/templates/hermes-agent/install-local.sh +15 -14
- package/templates/hermes-agent/runtime-scaffold/README.md +1 -1
- package/templates/hermes-agent/runtime-scaffold/bloodbank-consumer.py +46 -14
- package/templates/hermes-agent/runtime-scaffold/memories/MEMORY.md +2 -2
- package/templates/hermes-agent/scripts/fleet-sync.sh +1 -64
- package/templates/hermes-agent/template/.gitignore.jinja +0 -2
- package/templates/hermes-agent/template/.runtime-scaffold/README.md +1 -1
- package/templates/hermes-agent/template/.runtime-scaffold/bloodbank-consumer.py +43 -9
- package/templates/hermes-agent/template/.runtime-scaffold/memories/MEMORY.md +2 -2
- package/templates/hermes-agent/template/.scripts/01-config.sh +0 -1
- package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +0 -9
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +3 -22
- package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +0 -22
- package/templates/hermes-agent/template/.scripts/40-plane.sh +51 -0
- package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +59 -21
- package/templates/hermes-agent/template/.scripts/60-bloodbank.sh +2 -1
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +1 -10
- package/templates/hermes-agent/template/.scripts/_lib.sh +3 -61
- package/templates/hermes-agent/template/.scripts/config.example.toml +0 -5
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +33 -66
- package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +5 -9
- package/templates/hermes-agent/template/.scripts/momo-wip-lock.py +137 -0
- package/templates/hermes-agent/template/.scripts/providers/linear.sh +176 -0
- package/templates/hermes-agent/template/.scripts/providers/plane.sh +9 -29
- package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md +2 -2
- package/templates/hermes-agent/template/.scripts/sentinel/docs/continuous-ticket-orchestration.md +1 -33
- package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +22 -27
- package/templates/hermes-agent/template/SOUL.md.jinja +49 -30
- package/templates/hermes-agent/template/role.yaml.jinja +35 -4
- package/templates/hermes-agent/tests/test_bloodbank_consumer_contract.py +138 -0
- package/templates/commonproject/template/.mise/scripts/link-project-skills-to-clis.sh +0 -110
- package/templates/commonproject/template/.mise/scripts/unlink-project-skills-from-clis.sh +0 -45
- package/templates/hermes-agent/docs/bloodbank-gateway.md +0 -57
- package/templates/hermes-agent/docs/fleet-control-plane/n8n-service-hub.md +0 -60
package/dist/index.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
5
|
-
import { existsSync as
|
|
6
|
-
import { basename as basename4, join as
|
|
5
|
+
import { existsSync as existsSync11, readFileSync as readFileSync8, statSync as statSync2 } from "node:fs";
|
|
6
|
+
import { basename as basename4, join as join15, resolve as resolve3 } from "node:path";
|
|
7
7
|
import { Command as Command3 } from "commander";
|
|
8
8
|
|
|
9
9
|
// src/commands/hermes/types.ts
|
|
@@ -410,97 +410,284 @@ var AddMiseCodegraphScript = class extends Command {
|
|
|
410
410
|
};
|
|
411
411
|
}
|
|
412
412
|
const content = `#!/usr/bin/env bash
|
|
413
|
-
#
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
413
|
+
# Auto-generated by pjangler
|
|
414
|
+
|
|
415
|
+
REPO_ROOT="$(pwd)"
|
|
416
|
+
PROJECT_NAME="$(basename "$REPO_ROOT")"
|
|
417
|
+
CONTAINER_NAME="codegraph-mcp-$PROJECT_NAME"
|
|
418
|
+
CACHE_DIR="$REPO_ROOT/.codegraph"
|
|
419
|
+
|
|
420
|
+
# Deterministically generate a port based on the repository path
|
|
421
|
+
PORT=$(echo -n "$REPO_ROOT" | md5sum | awk '{print $1}' | tr -d 'a-f' | cut -c1-4)
|
|
422
|
+
# Ensure port is > 1024
|
|
423
|
+
PORT=$(( (PORT % 60000) + 1025 ))
|
|
424
|
+
|
|
425
|
+
if ! docker ps --format '{{.Names}}' | grep -q "^$CONTAINER_NAME$"; then
|
|
426
|
+
echo "\u{1F680} Starting CodeGraph MCP Server on port $PORT..."
|
|
427
|
+
|
|
428
|
+
# Ensure the container isn't lingering in a stopped state
|
|
429
|
+
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
|
430
|
+
|
|
431
|
+
# Run the true Colby McHenry CodeGraph Docker image
|
|
432
|
+
# which we built locally as colbymchenry-codegraph-mcp:latest
|
|
433
|
+
docker run -d \\
|
|
434
|
+
--name "$CONTAINER_NAME" \\
|
|
435
|
+
--restart unless-stopped \\
|
|
436
|
+
-p "$PORT:8045" \\
|
|
437
|
+
-v "$REPO_ROOT:/repo" \\
|
|
438
|
+
colbymchenry-codegraph-mcp:latest >/dev/null
|
|
439
|
+
|
|
440
|
+
echo "\u2705 CodeGraph MCP running in background. SSE URL: http://localhost:$PORT/sse"
|
|
441
|
+
fi
|
|
442
|
+
|
|
443
|
+
# Run init inside the container to ensure the index is bootstrapped.
|
|
444
|
+
# We run this using the standard codegraph CLI inside the container
|
|
445
|
+
docker exec "$CONTAINER_NAME" codegraph init -i /repo >/dev/null 2>&1 || true
|
|
446
|
+
|
|
447
|
+
# Wire up the MCP server to local agents
|
|
448
|
+
WIRE_SCRIPT="$(dirname "$0")/codegraph-wire.sh"
|
|
449
|
+
if [ -x "$WIRE_SCRIPT" ]; then
|
|
450
|
+
"$WIRE_SCRIPT"
|
|
451
|
+
fi
|
|
452
|
+
`;
|
|
453
|
+
this.writeFile(filePath, content);
|
|
454
|
+
if (!this.context.dryRun) {
|
|
455
|
+
chmodSync(join3(this.context.targetDir, filePath), 493);
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
success: true,
|
|
459
|
+
message: this.formatMessage(this.context.dryRun ? "Would create .mise/scripts/codegraph.sh" : "\u2705 Created .mise/scripts/codegraph.sh"),
|
|
460
|
+
filePath
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
// src/commands/AddMiseCodegraphWireScript.ts
|
|
466
|
+
import { chmodSync as chmodSync2 } from "fs";
|
|
467
|
+
import { join as join4 } from "path";
|
|
468
|
+
var AddMiseCodegraphWireScript = class extends Command {
|
|
469
|
+
async invoke() {
|
|
470
|
+
const filePath = ".mise/scripts/codegraph-wire.sh";
|
|
471
|
+
if (this.fileExists(filePath) && !this.context.force) {
|
|
472
|
+
return {
|
|
473
|
+
success: false,
|
|
474
|
+
message: this.formatMessage("\u26A0\uFE0F .mise/scripts/codegraph-wire.sh already exists"),
|
|
475
|
+
filePath
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
const content = `#!/usr/bin/env bash
|
|
479
|
+
# Mise enter hook: Auto-wire CodeGraph MCP Server to local agents.
|
|
480
|
+
# Detects Claude Code, Codex, Gemini, Kimi, and OpenCode and configures
|
|
481
|
+
# them to use the local SSE endpoint for the current project.
|
|
419
482
|
|
|
420
483
|
set -euo pipefail
|
|
421
484
|
|
|
422
485
|
REPO_ROOT="\${MISE_PROJECT_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}"
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
486
|
+
|
|
487
|
+
# Compute the same port as the container script
|
|
488
|
+
PORT_HASH=$(echo -n "$REPO_ROOT" | md5sum | awk '{print $1}')
|
|
489
|
+
PORT_DEC=$(printf "%d" "0x\${PORT_HASH:0:4}")
|
|
490
|
+
PORT=$(( 8045 + (PORT_DEC % 955) ))
|
|
491
|
+
|
|
492
|
+
SSE_URL="http://localhost:$PORT/sse"
|
|
493
|
+
|
|
494
|
+
inject_sse() {
|
|
495
|
+
local target="$1"
|
|
496
|
+
local agent="$2"
|
|
497
|
+
|
|
498
|
+
if [ ! -f "$target" ]; then
|
|
499
|
+
mkdir -p "$(dirname "$target")"
|
|
500
|
+
echo '{"mcpServers": {}}' > "$target"
|
|
501
|
+
fi
|
|
502
|
+
|
|
503
|
+
# Ensure the file is valid JSON (fail gracefully if it's garbled)
|
|
504
|
+
if ! jq . "$target" >/dev/null 2>&1; then
|
|
505
|
+
echo "[mise] WARNING: $target is not valid JSON, skipping $agent wiring" >&2
|
|
506
|
+
return
|
|
437
507
|
fi
|
|
508
|
+
|
|
509
|
+
# Inject or update the codegraph server
|
|
510
|
+
jq --arg url "$SSE_URL" '.mcpServers.codegraph = {"type": "sse", "url": $url}' "$target" > "$target.tmp" && mv "$target.tmp" "$target"
|
|
511
|
+
echo "[mise] Wired CodeGraph SSE for $agent -> $target"
|
|
438
512
|
}
|
|
439
513
|
|
|
440
|
-
#
|
|
441
|
-
|
|
442
|
-
if command -v codegraph >/dev/null 2>&1; then
|
|
443
|
-
return 0
|
|
444
|
-
fi
|
|
514
|
+
# 1. Claude Code (Project-scoped)
|
|
515
|
+
inject_sse "$REPO_ROOT/.claude.json" "Claude Code"
|
|
445
516
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
export PATH="$PROJECT_BIN_DIR:$PATH"
|
|
449
|
-
return 0
|
|
450
|
-
fi
|
|
517
|
+
# 2. Codex (Global)
|
|
518
|
+
inject_sse "$HOME/.codex/mcp.json" "Codex"
|
|
451
519
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
if [ -x "$d/codegraph" ]; then
|
|
455
|
-
export PATH="$d:$PATH"
|
|
456
|
-
return 0
|
|
457
|
-
fi
|
|
458
|
-
done
|
|
520
|
+
# 3. Gemini (Global)
|
|
521
|
+
inject_sse "$HOME/.gemini/config/mcp.json" "Gemini"
|
|
459
522
|
|
|
460
|
-
|
|
461
|
-
|
|
523
|
+
# 4. Kimi (Global)
|
|
524
|
+
inject_sse "$HOME/.kimi-code/mcp.json" "Kimi"
|
|
462
525
|
|
|
463
|
-
#
|
|
464
|
-
|
|
465
|
-
init_project() {
|
|
466
|
-
local err_file
|
|
467
|
-
err_file="$(mktemp)"
|
|
468
|
-
trap 'rm -f "$err_file"' RETURN
|
|
526
|
+
# 5. OpenCode (Global)
|
|
527
|
+
inject_sse "$HOME/.opencode/mcp.json" "OpenCode"
|
|
469
528
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
fi
|
|
529
|
+
# 6. Cursor (Global)
|
|
530
|
+
inject_sse "$HOME/.cursor/mcp.json" "Cursor"
|
|
473
531
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
ensure_codegraph
|
|
477
|
-
codegraph init -i "$REPO_ROOT"
|
|
478
|
-
return 0
|
|
479
|
-
fi
|
|
532
|
+
# 7. VSCode native MCP (Global)
|
|
533
|
+
inject_sse "$HOME/.vscode/mcp.json" "VSCode"
|
|
480
534
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
}
|
|
535
|
+
# 8. Cline (VSCode extension)
|
|
536
|
+
inject_sse "$HOME/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" "Cline"
|
|
484
537
|
|
|
485
|
-
|
|
538
|
+
# 9. Roo (VSCode extension)
|
|
539
|
+
inject_sse "$HOME/.config/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/mcp_settings.json" "Roo Code"
|
|
486
540
|
`;
|
|
487
541
|
this.writeFile(filePath, content);
|
|
488
542
|
if (!this.context.dryRun) {
|
|
489
|
-
|
|
543
|
+
chmodSync2(join4(this.context.targetDir, filePath), 493);
|
|
490
544
|
}
|
|
491
545
|
return {
|
|
492
546
|
success: true,
|
|
493
|
-
message: this.formatMessage(this.context.dryRun ? "Would create .mise/scripts/codegraph.sh" : "\u2705 Created .mise/scripts/codegraph.sh"),
|
|
547
|
+
message: this.formatMessage(this.context.dryRun ? "Would create .mise/scripts/codegraph-wire.sh" : "\u2705 Created .mise/scripts/codegraph-wire.sh"),
|
|
494
548
|
filePath
|
|
495
549
|
};
|
|
496
550
|
}
|
|
497
551
|
};
|
|
498
552
|
|
|
553
|
+
// src/commands/WireMiseOpInject.ts
|
|
554
|
+
import { join as join5 } from "node:path";
|
|
555
|
+
import { existsSync as existsSync3, readFileSync, writeFileSync as writeFileSync3, chmodSync as chmodSync3 } from "node:fs";
|
|
556
|
+
var WireMiseOpInject = class _WireMiseOpInject extends Command {
|
|
557
|
+
static MARKER = "# pjangler:op-inject";
|
|
558
|
+
static CR = "{{config_root}}";
|
|
559
|
+
async invoke() {
|
|
560
|
+
const scriptPath = ".mise/scripts/inject-op-secrets.sh";
|
|
561
|
+
const fullScriptPath = join5(this.context.targetDir, scriptPath);
|
|
562
|
+
const scriptContent = `#!/usr/bin/env bash
|
|
563
|
+
# Resolve 1Password \`op://\` references in .env.op into .env.secrets, which mise
|
|
564
|
+
# loads via \`_.file\`.
|
|
565
|
+
#
|
|
566
|
+
# Runs from the mise enter hook: fail-open and non-interactive.
|
|
567
|
+
# No .env.op means no 1Password, so this exits before spending anything.
|
|
568
|
+
|
|
569
|
+
set -euo pipefail
|
|
570
|
+
|
|
571
|
+
REPO_ROOT="\${MISE_PROJECT_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}"
|
|
572
|
+
SRC="$REPO_ROOT/.env.op"
|
|
573
|
+
OUT="$REPO_ROOT/.env.secrets"
|
|
574
|
+
|
|
575
|
+
TTL_HOURS="\${OP_INJECT_TTL_HOURS:-12}"
|
|
576
|
+
|
|
577
|
+
[[ -f "$SRC" ]] || exit 0
|
|
578
|
+
command -v op >/dev/null 2>&1 || exit 0
|
|
579
|
+
|
|
580
|
+
if [[ -f "$OUT" && "$SRC" -ot "$OUT" ]]; then
|
|
581
|
+
if [[ "$TTL_HOURS" != "0" ]] && [[ -z "$(find "$OUT" -mmin "+$((TTL_HOURS * 60))" 2>/dev/null)" ]]; then
|
|
582
|
+
exit 0
|
|
583
|
+
fi
|
|
584
|
+
fi
|
|
585
|
+
|
|
586
|
+
op account list >/dev/null 2>&1 || exit 0
|
|
587
|
+
|
|
588
|
+
tmp="$(mktemp "\${OUT}.XXXXXX")"
|
|
589
|
+
trap 'rm -f "$tmp"' EXIT
|
|
590
|
+
chmod 600 "$tmp"
|
|
591
|
+
|
|
592
|
+
if op inject -i "$SRC" -o "$tmp" --force >/dev/null 2>&1; then
|
|
593
|
+
mv "$tmp" "$OUT"
|
|
594
|
+
trap - EXIT
|
|
595
|
+
else
|
|
596
|
+
echo "mise: op inject from .env.op failed \u2014 $(basename "$OUT") unchanged." >&2
|
|
597
|
+
echo " Check 'op signin', or that .env.op's op:// items are readable." >&2
|
|
598
|
+
echo " To disable: rm .env.op" >&2
|
|
599
|
+
fi
|
|
600
|
+
`;
|
|
601
|
+
this.writeFile(scriptPath, scriptContent);
|
|
602
|
+
if (!this.context.dryRun) {
|
|
603
|
+
chmodSync3(fullScriptPath, 493);
|
|
604
|
+
}
|
|
605
|
+
const misePath = join5(this.context.targetDir, "mise.toml");
|
|
606
|
+
if (!existsSync3(misePath)) {
|
|
607
|
+
return {
|
|
608
|
+
success: false,
|
|
609
|
+
message: "\u26A0\uFE0F No mise.toml found \u2014 run `pj init mise` first, then re-run."
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
let content = readFileSync(misePath, "utf8");
|
|
613
|
+
if (content.includes(_WireMiseOpInject.MARKER)) {
|
|
614
|
+
return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for op-inject") };
|
|
615
|
+
}
|
|
616
|
+
const cr = _WireMiseOpInject.CR;
|
|
617
|
+
const envRe = /\[env\]\s*\n([\s\S]*?(?=\n\[|$))/;
|
|
618
|
+
if (envRe.test(content)) {
|
|
619
|
+
content = content.replace(envRe, (m) => {
|
|
620
|
+
let block = m;
|
|
621
|
+
if (!block.includes("_.file")) {
|
|
622
|
+
block += `
|
|
623
|
+
_.file = ['.env', '.env.secrets']
|
|
624
|
+
`;
|
|
625
|
+
} else if (!block.includes(".env.secrets")) {
|
|
626
|
+
block = block.replace(/(_\.file\s*=\s*\[)([^\]]*?)(\])/, (m2, prefix, files, suffix) => {
|
|
627
|
+
const added = files.trim() ? `${files}, '.env.secrets'` : `'.env.secrets'`;
|
|
628
|
+
return `${prefix}${added}${suffix}`;
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
return block;
|
|
632
|
+
});
|
|
633
|
+
} else {
|
|
634
|
+
const envBlock = `[env]
|
|
635
|
+
_.file = ['.env', '.env.secrets']
|
|
636
|
+
|
|
637
|
+
`;
|
|
638
|
+
if (content.includes("[tools]")) {
|
|
639
|
+
content = content.replace(/(\[tools\][\s\S]*?(?=\n\[|$))/, `$1
|
|
640
|
+
${envBlock}`);
|
|
641
|
+
} else {
|
|
642
|
+
content = envBlock + content;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
const enterAdds = ` '${cr}/.mise/scripts/inject-op-secrets.sh',`;
|
|
646
|
+
const enterRe = /(enter\s*=\s*\[[\s\S]*?)(\n[ \t]*\])/;
|
|
647
|
+
if (enterRe.test(content)) {
|
|
648
|
+
content = content.replace(enterRe, (_m, head, close) => {
|
|
649
|
+
const sep = /[,[]\s*$/.test(head) ? "" : ",";
|
|
650
|
+
return `${head}${sep}
|
|
651
|
+
${enterAdds}${close}`;
|
|
652
|
+
});
|
|
653
|
+
} else {
|
|
654
|
+
if (content.includes("[hooks]")) {
|
|
655
|
+
content = content.replace(/\[hooks\]/, `[hooks]
|
|
656
|
+
enter = [
|
|
657
|
+
${enterAdds}
|
|
658
|
+
]`);
|
|
659
|
+
} else {
|
|
660
|
+
content += `
|
|
661
|
+
[hooks]
|
|
662
|
+
enter = [
|
|
663
|
+
${enterAdds}
|
|
664
|
+
]
|
|
665
|
+
`;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
const appended = [
|
|
669
|
+
"",
|
|
670
|
+
_WireMiseOpInject.MARKER,
|
|
671
|
+
"[tasks.secrets-inject]",
|
|
672
|
+
'description = "Re-resolve .env.op secrets from 1Password into .env.secrets"',
|
|
673
|
+
`run = "OP_INJECT_TTL_HOURS=0 .mise/scripts/inject-op-secrets.sh"`,
|
|
674
|
+
_WireMiseOpInject.MARKER + ":end",
|
|
675
|
+
""
|
|
676
|
+
].join("\n");
|
|
677
|
+
content = content.replace(/\n*$/, "\n") + appended;
|
|
678
|
+
if (!this.context.dryRun) writeFileSync3(misePath, content);
|
|
679
|
+
return {
|
|
680
|
+
success: true,
|
|
681
|
+
message: this.formatMessage("\u2705 Wired mise.toml for op-inject (_.file, [hooks] enter, tasks.secrets-inject)")
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
|
|
499
686
|
// src/recipes/MiseRecipe.ts
|
|
500
687
|
var MiseRecipe = class extends Recipe {
|
|
501
688
|
constructor(context) {
|
|
502
689
|
super(context);
|
|
503
|
-
this.addIngredient(AddMiseToml).addIngredient(AddDotenv).addIngredient(AddMiseTasksStructure).addIngredient(AddMiseBaseToml).addIngredient(AddMiseBaseScript).addIngredient(AddMiseCodegraphScript);
|
|
690
|
+
this.addIngredient(AddMiseToml).addIngredient(AddDotenv).addIngredient(AddMiseTasksStructure).addIngredient(AddMiseBaseToml).addIngredient(AddMiseBaseScript).addIngredient(AddMiseCodegraphScript).addIngredient(AddMiseCodegraphWireScript).addIngredient(WireMiseOpInject);
|
|
504
691
|
}
|
|
505
692
|
printNextSteps() {
|
|
506
693
|
console.log("\u{1F389} Mise subsystem initialized successfully!");
|
|
@@ -728,12 +915,12 @@ var NodeRecipe = class extends Recipe {
|
|
|
728
915
|
};
|
|
729
916
|
|
|
730
917
|
// src/commands/hermes/PromptForAgentConfig.ts
|
|
731
|
-
import { basename, join as
|
|
732
|
-
import { readFileSync } from "node:fs";
|
|
918
|
+
import { basename, join as join6 } from "node:path";
|
|
919
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
733
920
|
import * as p from "@clack/prompts";
|
|
734
921
|
function detectTicketProvider(targetDir) {
|
|
735
922
|
try {
|
|
736
|
-
const t = JSON.parse(
|
|
923
|
+
const t = JSON.parse(readFileSync2(join6(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
|
|
737
924
|
return t === "plane" || t === "trello" ? t : void 0;
|
|
738
925
|
} catch {
|
|
739
926
|
return void 0;
|
|
@@ -791,8 +978,8 @@ var PromptForAgentConfig = class extends Command {
|
|
|
791
978
|
// src/commands/hermes/RunCopierTemplate.ts
|
|
792
979
|
import { spawnSync } from "node:child_process";
|
|
793
980
|
import { homedir as homedir2 } from "node:os";
|
|
794
|
-
import { join as
|
|
795
|
-
import { existsSync as
|
|
981
|
+
import { join as join7, dirname as dirname3 } from "node:path";
|
|
982
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "node:fs";
|
|
796
983
|
import { fileURLToPath } from "node:url";
|
|
797
984
|
import * as p2 from "@clack/prompts";
|
|
798
985
|
function resolveVendoredTemplate(name) {
|
|
@@ -803,8 +990,8 @@ function resolveVendoredTemplate(name) {
|
|
|
803
990
|
return void 0;
|
|
804
991
|
}
|
|
805
992
|
for (let i = 0; i < 8; i++) {
|
|
806
|
-
const candidate =
|
|
807
|
-
if (
|
|
993
|
+
const candidate = join7(dir, "templates", name);
|
|
994
|
+
if (existsSync4(join7(candidate, "copier.yml"))) return candidate;
|
|
808
995
|
const parent = dirname3(dir);
|
|
809
996
|
if (parent === dir) break;
|
|
810
997
|
dir = parent;
|
|
@@ -823,7 +1010,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
823
1010
|
message: "PromptForAgentConfig must run before RunCopierTemplate (targetRepo/role unset)"
|
|
824
1011
|
};
|
|
825
1012
|
}
|
|
826
|
-
const roleDir =
|
|
1013
|
+
const roleDir = join7(ctx.targetDir, "agents", "hermes", role);
|
|
827
1014
|
ctx.roleDir = roleDir;
|
|
828
1015
|
ctx.runtimeRepo = `delorenj/agent-hm-${targetRepo}-${role}`;
|
|
829
1016
|
const which = spawnSync("which", ["copier"], { encoding: "utf8" });
|
|
@@ -833,7 +1020,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
833
1020
|
message: "\u2717 copier not found on PATH. Install with: `uv tool install copier` or `pip install copier`"
|
|
834
1021
|
};
|
|
835
1022
|
}
|
|
836
|
-
if (
|
|
1023
|
+
if (existsSync4(join7(roleDir, "role.yaml")) && !ctx.force) {
|
|
837
1024
|
if (ctx.yes) {
|
|
838
1025
|
ctx.force = true;
|
|
839
1026
|
} else {
|
|
@@ -860,9 +1047,9 @@ var RunCopierTemplate = class extends Command {
|
|
|
860
1047
|
SKIP_BLOODBANK: ctx.skipBloodbank ? "1" : "0",
|
|
861
1048
|
SKIP_SYSTEMD: ctx.skipSystemd ? "1" : "0"
|
|
862
1049
|
};
|
|
863
|
-
const LOCAL_TEMPLATE =
|
|
1050
|
+
const LOCAL_TEMPLATE = join7(homedir2(), "code", "hermes-agent-template");
|
|
864
1051
|
const vendored = resolveVendoredTemplate("hermes-agent");
|
|
865
|
-
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (
|
|
1052
|
+
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync4(join7(LOCAL_TEMPLATE, "copier.yml")) ? LOCAL_TEMPLATE : HERMES_AGENT_TEMPLATE);
|
|
866
1053
|
const args = [
|
|
867
1054
|
"copy",
|
|
868
1055
|
templateSrc,
|
|
@@ -893,7 +1080,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
893
1080
|
message: this.formatMessage(`Would run: copier ${args.join(" ")}`)
|
|
894
1081
|
};
|
|
895
1082
|
}
|
|
896
|
-
mkdirSync3(
|
|
1083
|
+
mkdirSync3(join7(ctx.targetDir, "agents", "hermes"), { recursive: true });
|
|
897
1084
|
const spinner4 = p2.spinner();
|
|
898
1085
|
spinner4.start(`Running copier copy (target: agents/hermes/${role})`);
|
|
899
1086
|
const result = spawnSync("copier", args, {
|
|
@@ -917,14 +1104,14 @@ var RunCopierTemplate = class extends Command {
|
|
|
917
1104
|
};
|
|
918
1105
|
|
|
919
1106
|
// src/commands/hermes/UntrackHermesRuntimes.ts
|
|
920
|
-
import { existsSync as
|
|
921
|
-
import { join as
|
|
1107
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync4, readdirSync } from "fs";
|
|
1108
|
+
import { join as join8 } from "path";
|
|
922
1109
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
923
1110
|
var UntrackHermesRuntimes = class extends Command {
|
|
924
1111
|
async invoke() {
|
|
925
1112
|
const targetDir = this.context.targetDir;
|
|
926
|
-
const rolesDir =
|
|
927
|
-
if (!
|
|
1113
|
+
const rolesDir = join8(targetDir, "agents", "hermes");
|
|
1114
|
+
if (!existsSync5(rolesDir)) {
|
|
928
1115
|
return {
|
|
929
1116
|
success: true,
|
|
930
1117
|
message: "No Hermes agents found (no agents/hermes directory)."
|
|
@@ -940,9 +1127,9 @@ var UntrackHermesRuntimes = class extends Command {
|
|
|
940
1127
|
let modifiedAny = false;
|
|
941
1128
|
const details = [];
|
|
942
1129
|
for (const role of roles) {
|
|
943
|
-
const roleDir =
|
|
944
|
-
const runtimePath =
|
|
945
|
-
const gitignorePath =
|
|
1130
|
+
const roleDir = join8("agents", "hermes", role);
|
|
1131
|
+
const runtimePath = join8(roleDir, "runtime");
|
|
1132
|
+
const gitignorePath = join8(roleDir, ".gitignore");
|
|
946
1133
|
let isTracked = false;
|
|
947
1134
|
const lsResult = spawnSync2("git", ["ls-files", "--stage", runtimePath], {
|
|
948
1135
|
cwd: targetDir,
|
|
@@ -952,9 +1139,9 @@ var UntrackHermesRuntimes = class extends Command {
|
|
|
952
1139
|
isTracked = true;
|
|
953
1140
|
}
|
|
954
1141
|
let isIgnored = false;
|
|
955
|
-
const fullGitignorePath =
|
|
956
|
-
if (
|
|
957
|
-
const content =
|
|
1142
|
+
const fullGitignorePath = join8(targetDir, gitignorePath);
|
|
1143
|
+
if (existsSync5(fullGitignorePath)) {
|
|
1144
|
+
const content = readFileSync3(fullGitignorePath, "utf8");
|
|
958
1145
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
959
1146
|
isIgnored = lines.includes("runtime/") || lines.includes("runtime");
|
|
960
1147
|
}
|
|
@@ -979,14 +1166,14 @@ var UntrackHermesRuntimes = class extends Command {
|
|
|
979
1166
|
details.push(`ignore runtime/ in agents/hermes/${role}/.gitignore`);
|
|
980
1167
|
if (!this.context.dryRun) {
|
|
981
1168
|
let content = "";
|
|
982
|
-
if (
|
|
983
|
-
content =
|
|
1169
|
+
if (existsSync5(fullGitignorePath)) {
|
|
1170
|
+
content = readFileSync3(fullGitignorePath, "utf8");
|
|
984
1171
|
}
|
|
985
1172
|
if (content && !content.endsWith("\n")) {
|
|
986
1173
|
content += "\n";
|
|
987
1174
|
}
|
|
988
1175
|
content += "runtime/\n";
|
|
989
|
-
|
|
1176
|
+
writeFileSync4(fullGitignorePath, content, "utf8");
|
|
990
1177
|
}
|
|
991
1178
|
}
|
|
992
1179
|
}
|
|
@@ -1008,8 +1195,8 @@ ${details.map((d) => ` - ${d}`).join("\n")}`
|
|
|
1008
1195
|
|
|
1009
1196
|
// src/commands/hermes/WireTelegram.ts
|
|
1010
1197
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
1011
|
-
import { join as
|
|
1012
|
-
import { existsSync as
|
|
1198
|
+
import { join as join9 } from "node:path";
|
|
1199
|
+
import { existsSync as existsSync6, unlinkSync } from "node:fs";
|
|
1013
1200
|
import * as p3 from "@clack/prompts";
|
|
1014
1201
|
var WireTelegram = class extends Command {
|
|
1015
1202
|
async invoke() {
|
|
@@ -1097,15 +1284,15 @@ var WireTelegram = class extends Command {
|
|
|
1097
1284
|
if (p3.isCancel(allowedAnswer)) {
|
|
1098
1285
|
return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
|
|
1099
1286
|
}
|
|
1100
|
-
const script =
|
|
1101
|
-
if (!
|
|
1287
|
+
const script = join9(roleDir, ".scripts", "30-telegram.sh");
|
|
1288
|
+
if (!existsSync6(script)) {
|
|
1102
1289
|
return {
|
|
1103
1290
|
success: false,
|
|
1104
1291
|
message: `\u2717 ${script} not found. Did copier finish? Re-run with --skip-runtime-repo=0 if you skipped it.`
|
|
1105
1292
|
};
|
|
1106
1293
|
}
|
|
1107
|
-
const marker =
|
|
1108
|
-
if (
|
|
1294
|
+
const marker = join9(roleDir, ".scripts", ".done-30-telegram");
|
|
1295
|
+
if (existsSync6(marker)) unlinkSync(marker);
|
|
1109
1296
|
const spinner4 = p3.spinner();
|
|
1110
1297
|
spinner4.start("Verifying token + wiring profile");
|
|
1111
1298
|
const result = spawnSync3("bash", [script], {
|
|
@@ -1132,8 +1319,8 @@ function cap(s) {
|
|
|
1132
1319
|
|
|
1133
1320
|
// src/commands/hermes/WireEmail.ts
|
|
1134
1321
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
1135
|
-
import { join as
|
|
1136
|
-
import { existsSync as
|
|
1322
|
+
import { join as join10 } from "node:path";
|
|
1323
|
+
import { existsSync as existsSync7, unlinkSync as unlinkSync2 } from "node:fs";
|
|
1137
1324
|
import * as p4 from "@clack/prompts";
|
|
1138
1325
|
var WireEmail = class extends Command {
|
|
1139
1326
|
async invoke() {
|
|
@@ -1148,8 +1335,8 @@ var WireEmail = class extends Command {
|
|
|
1148
1335
|
if (!targetRepo || !role || !roleDir) {
|
|
1149
1336
|
return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
|
|
1150
1337
|
}
|
|
1151
|
-
const script =
|
|
1152
|
-
if (!
|
|
1338
|
+
const script = join10(roleDir, ".scripts", "50-email.sh");
|
|
1339
|
+
if (!existsSync7(script)) {
|
|
1153
1340
|
return { success: false, message: `\u2717 ${script} not found` };
|
|
1154
1341
|
}
|
|
1155
1342
|
let token = process.env.CF_EMAIL_ROUTING_TOKEN;
|
|
@@ -1211,8 +1398,8 @@ var WireEmail = class extends Command {
|
|
|
1211
1398
|
}
|
|
1212
1399
|
}
|
|
1213
1400
|
}
|
|
1214
|
-
const marker =
|
|
1215
|
-
if (
|
|
1401
|
+
const marker = join10(roleDir, ".scripts", ".done-50-email");
|
|
1402
|
+
if (existsSync7(marker)) unlinkSync2(marker);
|
|
1216
1403
|
const spinner4 = p4.spinner();
|
|
1217
1404
|
spinner4.start("Creating Cloudflare Email Routing rule");
|
|
1218
1405
|
const result = spawnSync4("bash", [script], {
|
|
@@ -1299,33 +1486,261 @@ var HermesAgentRecipe = class extends Recipe {
|
|
|
1299
1486
|
|
|
1300
1487
|
// src/commands/AgentHooksCommands.ts
|
|
1301
1488
|
import { homedir as homedir4 } from "node:os";
|
|
1302
|
-
import { join as
|
|
1303
|
-
import { existsSync as
|
|
1489
|
+
import { join as join12, dirname as dirname5 } from "node:path";
|
|
1490
|
+
import { existsSync as existsSync9, cpSync, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1304
1491
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1305
1492
|
|
|
1306
1493
|
// src/project/index.ts
|
|
1307
1494
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
1308
|
-
import { existsSync as
|
|
1495
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync, statSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1309
1496
|
import { homedir as homedir3 } from "node:os";
|
|
1310
|
-
import { basename as basename2, delimiter, dirname as dirname4, join as
|
|
1497
|
+
import { basename as basename2, delimiter, dirname as dirname4, join as join11, resolve } from "node:path";
|
|
1498
|
+
import YAML2 from "yaml";
|
|
1499
|
+
|
|
1500
|
+
// src/project/RegistryStore.ts
|
|
1501
|
+
import { Pool } from "pg";
|
|
1311
1502
|
import YAML from "yaml";
|
|
1503
|
+
function pgRegistryConfigFromEnv(env2 = process.env) {
|
|
1504
|
+
return {
|
|
1505
|
+
host: env2.PGHOST || "localhost",
|
|
1506
|
+
port: parseInt(env2.PGPORT || "5432", 10),
|
|
1507
|
+
user: env2.PGUSER || "delorenj",
|
|
1508
|
+
password: env2.PGPASSWORD || "",
|
|
1509
|
+
database: env2.PGDATABASE || "33god"
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
var PgRegistryStore = class {
|
|
1513
|
+
pool;
|
|
1514
|
+
constructor(config) {
|
|
1515
|
+
this.pool = new Pool({
|
|
1516
|
+
host: config.host,
|
|
1517
|
+
port: config.port,
|
|
1518
|
+
user: config.user,
|
|
1519
|
+
password: config.password,
|
|
1520
|
+
database: config.database
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
async load() {
|
|
1524
|
+
const client = await this.pool.connect();
|
|
1525
|
+
try {
|
|
1526
|
+
const { rows } = await client.query(
|
|
1527
|
+
`SELECT p.id, p.name, p.description, p.slug, p.status,
|
|
1528
|
+
p.source_artifacts, p.template, p.automation,
|
|
1529
|
+
p.created_at, p.updated_at,
|
|
1530
|
+
r.id AS repo_id, r.local_path
|
|
1531
|
+
FROM public.projects p
|
|
1532
|
+
LEFT JOIN public.repos r ON r.project_id = p.id
|
|
1533
|
+
WHERE p.slug IS NOT NULL`
|
|
1534
|
+
);
|
|
1535
|
+
const projects = {};
|
|
1536
|
+
for (const row of rows) {
|
|
1537
|
+
const slug = row.slug;
|
|
1538
|
+
const ticketProvider = await this.loadTicketProvider(client, row.id);
|
|
1539
|
+
const agents = await this.loadAgents(client, row.id, slug);
|
|
1540
|
+
projects[slug] = {
|
|
1541
|
+
name: row.name ?? "",
|
|
1542
|
+
slug,
|
|
1543
|
+
repo_path: row.local_path ?? "",
|
|
1544
|
+
description: row.description ?? "",
|
|
1545
|
+
status: row.status ?? "planned",
|
|
1546
|
+
source_artifacts: row.source_artifacts ?? [],
|
|
1547
|
+
template: row.template ?? {
|
|
1548
|
+
commonproject: { enabled: false, primary_language: "python" }
|
|
1549
|
+
},
|
|
1550
|
+
ticket_provider: ticketProvider,
|
|
1551
|
+
agents,
|
|
1552
|
+
automation: row.automation ?? void 0,
|
|
1553
|
+
created_at: row.created_at.toISOString(),
|
|
1554
|
+
updated_at: row.updated_at.toISOString()
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
const registry = {
|
|
1558
|
+
schema_version: PROJECT_REGISTRY_SCHEMA_VERSION,
|
|
1559
|
+
projects
|
|
1560
|
+
};
|
|
1561
|
+
validateProjectRegistry(registry);
|
|
1562
|
+
return registry;
|
|
1563
|
+
} finally {
|
|
1564
|
+
client.release();
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
async save(registry) {
|
|
1568
|
+
validateProjectRegistry(registry);
|
|
1569
|
+
const client = await this.pool.connect();
|
|
1570
|
+
try {
|
|
1571
|
+
await client.query("BEGIN");
|
|
1572
|
+
for (const [slug, record] of Object.entries(registry.projects)) {
|
|
1573
|
+
await this.upsertInTx(client, slug, record);
|
|
1574
|
+
}
|
|
1575
|
+
await client.query("COMMIT");
|
|
1576
|
+
} catch (err) {
|
|
1577
|
+
await client.query("ROLLBACK");
|
|
1578
|
+
throw err;
|
|
1579
|
+
} finally {
|
|
1580
|
+
client.release();
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
async upsert(slug, record) {
|
|
1584
|
+
const client = await this.pool.connect();
|
|
1585
|
+
try {
|
|
1586
|
+
await client.query("BEGIN");
|
|
1587
|
+
await this.upsertInTx(client, slug, record);
|
|
1588
|
+
await client.query("COMMIT");
|
|
1589
|
+
} catch (err) {
|
|
1590
|
+
await client.query("ROLLBACK");
|
|
1591
|
+
throw err;
|
|
1592
|
+
} finally {
|
|
1593
|
+
client.release();
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
async getBySlug(slug) {
|
|
1597
|
+
const registry = await this.load();
|
|
1598
|
+
return registry.projects[slug];
|
|
1599
|
+
}
|
|
1600
|
+
async getByRepoPath(repoPath) {
|
|
1601
|
+
const registry = await this.load();
|
|
1602
|
+
return Object.values(registry.projects).find(
|
|
1603
|
+
(p6) => p6.repo_path === repoPath
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
async close() {
|
|
1607
|
+
await this.pool.end();
|
|
1608
|
+
}
|
|
1609
|
+
// --- private helpers ---
|
|
1610
|
+
async upsertInTx(client, slug, record) {
|
|
1611
|
+
if (!slug) throw new Error("PgRegistryStore.upsert: slug is required");
|
|
1612
|
+
const projectResult = await client.query(
|
|
1613
|
+
`INSERT INTO public.projects (name, description, slug, status, source_artifacts, template, automation)
|
|
1614
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
1615
|
+
ON CONFLICT (slug) WHERE slug IS NOT NULL
|
|
1616
|
+
DO UPDATE SET
|
|
1617
|
+
name = EXCLUDED.name,
|
|
1618
|
+
description = EXCLUDED.description,
|
|
1619
|
+
status = EXCLUDED.status,
|
|
1620
|
+
source_artifacts = EXCLUDED.source_artifacts,
|
|
1621
|
+
template = EXCLUDED.template,
|
|
1622
|
+
automation = EXCLUDED.automation
|
|
1623
|
+
RETURNING id`,
|
|
1624
|
+
[
|
|
1625
|
+
record.name,
|
|
1626
|
+
record.description,
|
|
1627
|
+
slug,
|
|
1628
|
+
record.status,
|
|
1629
|
+
JSON.stringify(record.source_artifacts),
|
|
1630
|
+
JSON.stringify(record.template),
|
|
1631
|
+
record.automation ? JSON.stringify(record.automation) : null
|
|
1632
|
+
]
|
|
1633
|
+
);
|
|
1634
|
+
const projectId = projectResult.rows[0]?.id;
|
|
1635
|
+
if (!projectId) throw new Error(`Failed to upsert project: ${slug}`);
|
|
1636
|
+
await client.query(
|
|
1637
|
+
`INSERT INTO public.repos (project_id, local_path)
|
|
1638
|
+
VALUES ($1, $2)
|
|
1639
|
+
ON CONFLICT (local_path)
|
|
1640
|
+
DO UPDATE SET project_id = EXCLUDED.project_id
|
|
1641
|
+
RETURNING id`,
|
|
1642
|
+
[projectId, record.repo_path]
|
|
1643
|
+
);
|
|
1644
|
+
const repoResult = await client.query(
|
|
1645
|
+
`SELECT id FROM public.repos WHERE project_id = $1 AND local_path = $2`,
|
|
1646
|
+
[projectId, record.repo_path]
|
|
1647
|
+
);
|
|
1648
|
+
const repoId = repoResult.rows[0]?.id;
|
|
1649
|
+
if (!repoId) throw new Error(`Failed to find repo for project ${slug} at path ${record.repo_path}`);
|
|
1650
|
+
await this.upsertTicketProvider(client, projectId, repoId, record.ticket_provider);
|
|
1651
|
+
await client.query(
|
|
1652
|
+
`DELETE FROM public.project_agents WHERE project_id = $1`,
|
|
1653
|
+
[projectId]
|
|
1654
|
+
);
|
|
1655
|
+
for (const [agentKey, agent] of Object.entries(record.agents)) {
|
|
1656
|
+
await client.query(
|
|
1657
|
+
`INSERT INTO public.project_agents (repo_id, project_id, agent_key, role, role_dir, provisioning_state)
|
|
1658
|
+
VALUES ($1, $2, $3, $4, $5, $6)`,
|
|
1659
|
+
[repoId, projectId, agentKey, agent.role, agent.role_dir ?? null, agent.provisioning_state]
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
async loadTicketProvider(client, projectId) {
|
|
1664
|
+
const { rows } = await client.query(
|
|
1665
|
+
`SELECT provider_type, workspace, identifier, board_id, state
|
|
1666
|
+
FROM public.project_ticket_boards
|
|
1667
|
+
WHERE project_id = $1
|
|
1668
|
+
LIMIT 1`,
|
|
1669
|
+
[projectId]
|
|
1670
|
+
);
|
|
1671
|
+
if (!rows.length) {
|
|
1672
|
+
return { type: "plane", workspace: "33god", identifier: "", board_id: "", state: "planned" };
|
|
1673
|
+
}
|
|
1674
|
+
const row = rows[0];
|
|
1675
|
+
return {
|
|
1676
|
+
type: row.provider_type,
|
|
1677
|
+
workspace: row.workspace ?? void 0,
|
|
1678
|
+
identifier: row.identifier ?? void 0,
|
|
1679
|
+
board_id: row.board_id ?? void 0,
|
|
1680
|
+
state: row.state ?? void 0
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
async loadAgents(client, projectId, slug) {
|
|
1684
|
+
const { rows } = await client.query(
|
|
1685
|
+
`SELECT agent_key, role, role_dir, provisioning_state
|
|
1686
|
+
FROM public.project_agents
|
|
1687
|
+
WHERE project_id = $1`,
|
|
1688
|
+
[projectId]
|
|
1689
|
+
);
|
|
1690
|
+
const agents = {};
|
|
1691
|
+
for (const row of rows) {
|
|
1692
|
+
agents[row.agent_key] = {
|
|
1693
|
+
role: row.role,
|
|
1694
|
+
provisioning_state: row.provisioning_state,
|
|
1695
|
+
role_dir: row.role_dir ?? void 0
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
return agents;
|
|
1699
|
+
}
|
|
1700
|
+
async upsertTicketProvider(client, projectId, repoId, tp) {
|
|
1701
|
+
await client.query(
|
|
1702
|
+
`DELETE FROM public.project_ticket_boards WHERE project_id = $1`,
|
|
1703
|
+
[projectId]
|
|
1704
|
+
);
|
|
1705
|
+
await client.query(
|
|
1706
|
+
`INSERT INTO public.project_ticket_boards
|
|
1707
|
+
(repo_id, project_id, provider_type, workspace, identifier, board_id, state)
|
|
1708
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
|
1709
|
+
[
|
|
1710
|
+
repoId,
|
|
1711
|
+
projectId,
|
|
1712
|
+
tp.type,
|
|
1713
|
+
tp.workspace ?? null,
|
|
1714
|
+
tp.identifier ?? null,
|
|
1715
|
+
tp.board_id ?? null,
|
|
1716
|
+
tp.state ?? null
|
|
1717
|
+
]
|
|
1718
|
+
);
|
|
1719
|
+
}
|
|
1720
|
+
};
|
|
1721
|
+
var PJ_REGISTRY_PG_ENV = "PJ_REGISTRY_PG";
|
|
1722
|
+
function isPgRegistryEnabled(env2 = process.env) {
|
|
1723
|
+
return env2[PJ_REGISTRY_PG_ENV] === "1" || env2[PJ_REGISTRY_PG_ENV] === "true";
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// src/project/index.ts
|
|
1312
1727
|
var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
|
|
1313
1728
|
var PROJECT_SOURCE_SKILL_ROOTS_ENV = "PJ_SOURCE_SKILL_ROOTS";
|
|
1314
1729
|
var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
|
|
1315
1730
|
var DEFAULT_SOURCE_SKILL_ROOTS = [
|
|
1316
1731
|
"/home/delorenj/code/skillex/all-skills",
|
|
1317
|
-
|
|
1318
|
-
|
|
1732
|
+
join11(homedir3(), ".agents", "skills"),
|
|
1733
|
+
join11(homedir3(), ".codex", "skills")
|
|
1319
1734
|
];
|
|
1320
1735
|
function projectRegistryPath(env2 = process.env) {
|
|
1321
|
-
return expandHome(env2[PROJECT_REGISTRY_ENV] ||
|
|
1736
|
+
return expandHome(env2[PROJECT_REGISTRY_ENV] || join11(homedir3(), ".config", "pjangler", "projects.yaml"));
|
|
1322
1737
|
}
|
|
1323
1738
|
function emptyProjectRegistry() {
|
|
1324
1739
|
return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
|
|
1325
1740
|
}
|
|
1326
1741
|
function loadProjectRegistry(path = projectRegistryPath()) {
|
|
1327
|
-
if (!
|
|
1328
|
-
const raw =
|
|
1742
|
+
if (!existsSync8(path)) return emptyProjectRegistry();
|
|
1743
|
+
const raw = YAML2.parse(readFileSync4(path, "utf8"));
|
|
1329
1744
|
if (raw == null) return emptyProjectRegistry();
|
|
1330
1745
|
if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
|
|
1331
1746
|
const registry = raw;
|
|
@@ -1340,7 +1755,7 @@ function saveProjectRegistry(registry, path = projectRegistryPath()) {
|
|
|
1340
1755
|
validateProjectRegistry(registry);
|
|
1341
1756
|
mkdirSync4(dirname4(path), { recursive: true });
|
|
1342
1757
|
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
1343
|
-
|
|
1758
|
+
writeFileSync5(temp, YAML2.stringify(registry, { lineWidth: 0 }), "utf8");
|
|
1344
1759
|
renameSync(temp, path);
|
|
1345
1760
|
}
|
|
1346
1761
|
function validateProjectRegistry(registry) {
|
|
@@ -1422,7 +1837,7 @@ function resolveAgentHooksLayer(input, env2 = process.env) {
|
|
|
1422
1837
|
const override = env2.PJ_AGENT_HOOKS_LAYER;
|
|
1423
1838
|
if (override === "0" || override === "false") return false;
|
|
1424
1839
|
if (override === "1" || override === "true") return true;
|
|
1425
|
-
return !
|
|
1840
|
+
return !existsSync8(join11(homedir3(), ".agents", "hooks"));
|
|
1426
1841
|
}
|
|
1427
1842
|
function jsonStable(value) {
|
|
1428
1843
|
return JSON.stringify(value);
|
|
@@ -1453,12 +1868,12 @@ function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
|
|
|
1453
1868
|
if (!sourceSkill) return void 0;
|
|
1454
1869
|
const expanded = expandHome(sourceSkill);
|
|
1455
1870
|
const direct = resolve(expanded);
|
|
1456
|
-
if (
|
|
1871
|
+
if (existsSync8(direct)) return direct;
|
|
1457
1872
|
const name = basename2(sourceSkill);
|
|
1458
1873
|
const roots = sourceSkillRoots(env2);
|
|
1459
1874
|
for (const root of roots) {
|
|
1460
|
-
const candidate =
|
|
1461
|
-
if (
|
|
1875
|
+
const candidate = join11(root, name);
|
|
1876
|
+
if (existsSync8(candidate)) return candidate;
|
|
1462
1877
|
}
|
|
1463
1878
|
const searched = roots.length ? ` Searched roots: ${roots.join(", ")}.` : "";
|
|
1464
1879
|
const hint = `${searched} Add project-specific roots with ${PROJECT_SOURCE_SKILL_ROOTS_ENV}.`;
|
|
@@ -1539,7 +1954,7 @@ function planProjectInit(input) {
|
|
|
1539
1954
|
}));
|
|
1540
1955
|
}
|
|
1541
1956
|
actions.push(
|
|
1542
|
-
{ kind: "project.write-manifest", path:
|
|
1957
|
+
{ kind: "project.write-manifest", path: join11(targetDir, ".project.json"), manifest },
|
|
1543
1958
|
{
|
|
1544
1959
|
kind: "ticket-provider.create-or-link",
|
|
1545
1960
|
enabled: live,
|
|
@@ -1567,7 +1982,7 @@ function planProjectInit(input) {
|
|
|
1567
1982
|
);
|
|
1568
1983
|
return { ok: true, apply, dryRun: !apply, live, registryPath: registryPath2, project, manifest, actions };
|
|
1569
1984
|
}
|
|
1570
|
-
function executeProjectInitPlan(plan) {
|
|
1985
|
+
async function executeProjectInitPlan(plan) {
|
|
1571
1986
|
const logs = [];
|
|
1572
1987
|
const errors = [];
|
|
1573
1988
|
const changedFiles = [];
|
|
@@ -1592,7 +2007,7 @@ function executeProjectInitPlan(plan) {
|
|
|
1592
2007
|
}
|
|
1593
2008
|
if (result.status !== 0) {
|
|
1594
2009
|
errors.push(`copier exited with status ${result.status ?? "unknown"}`);
|
|
1595
|
-
if (
|
|
2010
|
+
if (existsSync8(action.targetDir)) changedFiles.push(action.targetDir);
|
|
1596
2011
|
break;
|
|
1597
2012
|
}
|
|
1598
2013
|
changedFiles.push(action.targetDir);
|
|
@@ -1600,9 +2015,9 @@ function executeProjectInitPlan(plan) {
|
|
|
1600
2015
|
mkdirSync4(dirname4(action.path), { recursive: true });
|
|
1601
2016
|
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
1602
2017
|
`;
|
|
1603
|
-
const current =
|
|
2018
|
+
const current = existsSync8(action.path) ? readFileSync4(action.path, "utf8") : void 0;
|
|
1604
2019
|
if (current !== next) {
|
|
1605
|
-
|
|
2020
|
+
writeFileSync5(action.path, next, "utf8");
|
|
1606
2021
|
changedFiles.push(action.path);
|
|
1607
2022
|
}
|
|
1608
2023
|
} else if (action.kind === "registry.upsert") {
|
|
@@ -1618,6 +2033,16 @@ function executeProjectInitPlan(plan) {
|
|
|
1618
2033
|
registry.projects[pendingRegistryAction.slug] = pendingRegistryAction.project;
|
|
1619
2034
|
saveProjectRegistry(registry, pendingRegistryAction.registryPath);
|
|
1620
2035
|
changedFiles.push(pendingRegistryAction.registryPath);
|
|
2036
|
+
if (isPgRegistryEnabled()) {
|
|
2037
|
+
try {
|
|
2038
|
+
const pgStore = new PgRegistryStore(pgRegistryConfigFromEnv());
|
|
2039
|
+
await pgStore.upsert(pendingRegistryAction.slug, pendingRegistryAction.project);
|
|
2040
|
+
await pgStore.close();
|
|
2041
|
+
logs.push("registry: PG dual-write complete");
|
|
2042
|
+
} catch (pgErr) {
|
|
2043
|
+
logs.push(`registry: PG dual-write failed (yaml is authoritative): ${pgErr instanceof Error ? pgErr.message : pgErr}`);
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
1621
2046
|
}
|
|
1622
2047
|
}
|
|
1623
2048
|
return { ok: errors.length === 0, plan, logs, errors, changedFiles };
|
|
@@ -1695,16 +2120,16 @@ function doctorProjectRegistry(registryPath2 = projectRegistryPath(), slug) {
|
|
|
1695
2120
|
const registry = loadProjectRegistry(registryPath2);
|
|
1696
2121
|
const projects = slug ? [[slug, getProject(registry, slug)]] : Object.entries(registry.projects);
|
|
1697
2122
|
for (const [projectSlug, project] of projects) {
|
|
1698
|
-
if (!
|
|
2123
|
+
if (!existsSync8(project.repo_path)) {
|
|
1699
2124
|
issues.push({ level: "warn", slug: projectSlug, message: `repo_path does not exist: ${project.repo_path}` });
|
|
1700
2125
|
} else if (!statSync(project.repo_path).isDirectory()) {
|
|
1701
2126
|
issues.push({ level: "error", slug: projectSlug, message: `repo_path is not a directory: ${project.repo_path}` });
|
|
1702
2127
|
} else {
|
|
1703
|
-
const manifestPath =
|
|
1704
|
-
if (!
|
|
2128
|
+
const manifestPath = join11(project.repo_path, ".project.json");
|
|
2129
|
+
if (!existsSync8(manifestPath)) issues.push({ level: "warn", slug: projectSlug, message: ".project.json is missing" });
|
|
1705
2130
|
}
|
|
1706
2131
|
for (const artifact of project.source_artifacts) {
|
|
1707
|
-
if (artifact.path && !
|
|
2132
|
+
if (artifact.path && !existsSync8(artifact.path)) {
|
|
1708
2133
|
issues.push({ level: "warn", slug: projectSlug, message: `source artifact missing: ${artifact.path}` });
|
|
1709
2134
|
}
|
|
1710
2135
|
}
|
|
@@ -1717,7 +2142,7 @@ function doctorProjectRegistry(registryPath2 = projectRegistryPath(), slug) {
|
|
|
1717
2142
|
};
|
|
1718
2143
|
}
|
|
1719
2144
|
function buildCommonProjectCopierAction(input) {
|
|
1720
|
-
const templateDir =
|
|
2145
|
+
const templateDir = join11(input.pjanglerRoot, "templates", "commonproject");
|
|
1721
2146
|
const data = {
|
|
1722
2147
|
project_name: input.projectName,
|
|
1723
2148
|
project_description: input.projectDescription ?? "",
|
|
@@ -1746,7 +2171,7 @@ function buildCommonProjectCopierAction(input) {
|
|
|
1746
2171
|
function resolvePjanglerRoot() {
|
|
1747
2172
|
let dir = dirname4(new URL(import.meta.url).pathname);
|
|
1748
2173
|
while (dir !== dirname4(dir)) {
|
|
1749
|
-
if (
|
|
2174
|
+
if (existsSync8(join11(dir, "package.json")) && existsSync8(join11(dir, "templates", "commonproject", "copier.yml"))) return dir;
|
|
1750
2175
|
dir = dirname4(dir);
|
|
1751
2176
|
}
|
|
1752
2177
|
return resolve(process.cwd());
|
|
@@ -1778,7 +2203,7 @@ function validateProjectRecord(project, key) {
|
|
|
1778
2203
|
}
|
|
1779
2204
|
function expandHome(path) {
|
|
1780
2205
|
if (path === "~") return homedir3();
|
|
1781
|
-
if (path.startsWith("~/")) return
|
|
2206
|
+
if (path.startsWith("~/")) return join11(homedir3(), path.slice(2));
|
|
1782
2207
|
return path;
|
|
1783
2208
|
}
|
|
1784
2209
|
function isRecord(value) {
|
|
@@ -1795,16 +2220,16 @@ function resolveTemplateRoot() {
|
|
|
1795
2220
|
try {
|
|
1796
2221
|
let dir = dirname5(fileURLToPath2(import.meta.url));
|
|
1797
2222
|
for (let i = 0; i < 8; i++) {
|
|
1798
|
-
candidates.push(
|
|
2223
|
+
candidates.push(join12(dir, "templates", "commonproject", "template"));
|
|
1799
2224
|
const parent = dirname5(dir);
|
|
1800
2225
|
if (parent === dir) break;
|
|
1801
2226
|
dir = parent;
|
|
1802
2227
|
}
|
|
1803
2228
|
} catch {
|
|
1804
2229
|
}
|
|
1805
|
-
candidates.push(
|
|
2230
|
+
candidates.push(join12(homedir4(), "code", "pjangler", "templates", "commonproject", "template"));
|
|
1806
2231
|
for (const c of candidates) {
|
|
1807
|
-
if (
|
|
2232
|
+
if (existsSync9(join12(c, ".agents", "hooks", "hooks.master.json"))) return c;
|
|
1808
2233
|
}
|
|
1809
2234
|
throw new Error(
|
|
1810
2235
|
"Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
|
|
@@ -1823,18 +2248,17 @@ var CopyAgentHooksTree = class extends Command {
|
|
|
1823
2248
|
}
|
|
1824
2249
|
const items = [
|
|
1825
2250
|
{ rel: ".agents/hooks", dir: true },
|
|
2251
|
+
{ rel: ".agents/skills.json", dir: false },
|
|
1826
2252
|
{ rel: ".agents/local.example.json", dir: false },
|
|
1827
|
-
{ rel: ".mise/scripts/link-project-skills-to-clis.sh", dir: false },
|
|
1828
|
-
{ rel: ".mise/scripts/unlink-project-skills-from-clis.sh", dir: false },
|
|
1829
2253
|
{ rel: ".mise/scripts/hindsight-setup.sh", dir: false }
|
|
1830
2254
|
];
|
|
1831
2255
|
const created = [];
|
|
1832
2256
|
const skipped = [];
|
|
1833
2257
|
for (const { rel, dir } of items) {
|
|
1834
|
-
const src =
|
|
1835
|
-
const dest =
|
|
1836
|
-
if (!
|
|
1837
|
-
if (
|
|
2258
|
+
const src = join12(templateRoot, rel);
|
|
2259
|
+
const dest = join12(this.context.targetDir, rel);
|
|
2260
|
+
if (!existsSync9(src)) continue;
|
|
2261
|
+
if (existsSync9(dest) && !this.context.force) {
|
|
1838
2262
|
skipped.push(rel);
|
|
1839
2263
|
continue;
|
|
1840
2264
|
}
|
|
@@ -1860,25 +2284,21 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
|
1860
2284
|
if (!resolveAgentHooksLayer()) {
|
|
1861
2285
|
return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
|
|
1862
2286
|
}
|
|
1863
|
-
const misePath =
|
|
1864
|
-
if (!
|
|
2287
|
+
const misePath = join12(this.context.targetDir, "mise.toml");
|
|
2288
|
+
if (!existsSync9(misePath)) {
|
|
1865
2289
|
return {
|
|
1866
2290
|
success: false,
|
|
1867
2291
|
message: "\u26A0\uFE0F No mise.toml found \u2014 run `pjangler init mise` first, then re-run."
|
|
1868
2292
|
};
|
|
1869
2293
|
}
|
|
1870
|
-
let content =
|
|
2294
|
+
let content = readFileSync5(misePath, "utf8");
|
|
1871
2295
|
if (content.includes(_WireMiseAgentHooks.MARKER)) {
|
|
1872
2296
|
return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
|
|
1873
2297
|
}
|
|
1874
2298
|
const cr = _WireMiseAgentHooks.CR;
|
|
1875
|
-
const enterAdds = [
|
|
1876
|
-
` "${cr}/.mise/scripts/link-project-skills-to-clis.sh",`,
|
|
1877
|
-
` "${cr}/.agents/hooks/sync.py --install --quiet",`
|
|
1878
|
-
].join("\n");
|
|
2299
|
+
const enterAdds = [` "sync-skills.py --scope project",`, ` "${cr}/.agents/hooks/sync.py --install --quiet",`].join("\n");
|
|
1879
2300
|
const leaveBlock = [
|
|
1880
2301
|
"leave = [",
|
|
1881
|
-
` "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh",`,
|
|
1882
2302
|
` "${cr}/.agents/hooks/sync.py --uninstall --quiet",`,
|
|
1883
2303
|
"]"
|
|
1884
2304
|
].join("\n");
|
|
@@ -1895,7 +2315,6 @@ ${enterAdds}${close}`;
|
|
|
1895
2315
|
content = content.replace(leaveRe, (_m, head, close) => {
|
|
1896
2316
|
const sep = /[,[]\s*$/.test(head) ? "" : ",";
|
|
1897
2317
|
return `${head}${sep}
|
|
1898
|
-
"${cr}/.mise/scripts/unlink-project-skills-from-clis.sh",
|
|
1899
2318
|
"${cr}/.agents/hooks/sync.py --uninstall --quiet",${close}`;
|
|
1900
2319
|
});
|
|
1901
2320
|
} else {
|
|
@@ -1908,6 +2327,14 @@ ${leaveBlock}`);
|
|
|
1908
2327
|
"",
|
|
1909
2328
|
_WireMiseAgentHooks.MARKER + " (generated \u2014 see .agents/hooks/README.md)",
|
|
1910
2329
|
"[[watch_files]]",
|
|
2330
|
+
'patterns = [".agents/skills.json"]',
|
|
2331
|
+
'task = "skills-sync"',
|
|
2332
|
+
"",
|
|
2333
|
+
"[tasks.skills-sync]",
|
|
2334
|
+
'description = "Sync skills from manifest to local CLI dirs"',
|
|
2335
|
+
'run = "sync-skills.py --scope project"',
|
|
2336
|
+
"",
|
|
2337
|
+
"[[watch_files]]",
|
|
1911
2338
|
'patterns = [".agents/hooks/hooks.master.json"]',
|
|
1912
2339
|
'task = "hooks-sync"',
|
|
1913
2340
|
"",
|
|
@@ -1923,18 +2350,6 @@ ${leaveBlock}`);
|
|
|
1923
2350
|
'description = "Remove per-user agent-hook injections (codex/kimi/hermes)"',
|
|
1924
2351
|
`run = "${cr}/.agents/hooks/sync.py --uninstall"`,
|
|
1925
2352
|
"",
|
|
1926
|
-
"[tasks.link-project-skills-to-clis]",
|
|
1927
|
-
'description = "Fan .agents/skills out to each agent CLI (honors local.json)"',
|
|
1928
|
-
`run = "${cr}/.mise/scripts/link-project-skills-to-clis.sh"`,
|
|
1929
|
-
"",
|
|
1930
|
-
"[tasks.unlink-project-skills-from-clis]",
|
|
1931
|
-
'description = "Remove project skill symlinks from shared per-CLI dirs"',
|
|
1932
|
-
`run = "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh"`,
|
|
1933
|
-
"",
|
|
1934
|
-
"[tasks.skills-relink]",
|
|
1935
|
-
'description = "Re-fan the project skill set to all CLIs"',
|
|
1936
|
-
`run = "${cr}/.mise/scripts/link-project-skills-to-clis.sh"`,
|
|
1937
|
-
"",
|
|
1938
2353
|
"[tasks.hindsight-setup]",
|
|
1939
2354
|
`description = "Provision this dev's shared project Hindsight key from 1Password into .env"`,
|
|
1940
2355
|
`run = "${cr}/.mise/scripts/hindsight-setup.sh"`,
|
|
@@ -1943,7 +2358,7 @@ ${leaveBlock}`);
|
|
|
1943
2358
|
""
|
|
1944
2359
|
].join("\n");
|
|
1945
2360
|
content = content.replace(/\n*$/, "\n") + appended;
|
|
1946
|
-
if (!this.context.dryRun)
|
|
2361
|
+
if (!this.context.dryRun) writeFileSync6(misePath, content);
|
|
1947
2362
|
if (wiredHooks) {
|
|
1948
2363
|
return { success: true, message: this.formatMessage("\u2705 Wired mise.toml ([hooks] enter/leave + tasks)") };
|
|
1949
2364
|
}
|
|
@@ -1952,8 +2367,8 @@ ${leaveBlock}`);
|
|
|
1952
2367
|
message: this.formatMessage(
|
|
1953
2368
|
`\u2705 Added agent-hooks tasks to mise.toml.
|
|
1954
2369
|
\u26A0\uFE0F Could not find a [hooks].enter array to extend \u2014 add these to your [hooks] block manually:
|
|
1955
|
-
enter += "
|
|
1956
|
-
leave += "${cr}/.
|
|
2370
|
+
enter += "sync-skills.py --scope project", "${cr}/.agents/hooks/sync.py --install --quiet"
|
|
2371
|
+
leave += "${cr}/.agents/hooks/sync.py --uninstall --quiet"`
|
|
1957
2372
|
)
|
|
1958
2373
|
};
|
|
1959
2374
|
}
|
|
@@ -1968,10 +2383,25 @@ var AgentHooksRecipe = class extends Recipe {
|
|
|
1968
2383
|
printNextSteps() {
|
|
1969
2384
|
console.log("\u{1FA9D} Agent-hooks layer installed!");
|
|
1970
2385
|
console.log(" Next steps:");
|
|
1971
|
-
console.log(" 1. mise run
|
|
1972
|
-
console.log(" 2.
|
|
1973
|
-
console.log(" 3.
|
|
1974
|
-
console.log(
|
|
2386
|
+
console.log(" 1. mise run skills-sync # sync .agents/skills.json into local CLI dirs");
|
|
2387
|
+
console.log(" 2. mise run hooks-sync # generate .claude/settings.json + inject codex/kimi/hermes");
|
|
2388
|
+
console.log(" 3. git add .claude/settings.json .agents/hooks .agents/skills.json && commit (codex/kimi/hermes are per-dev)");
|
|
2389
|
+
console.log(" 4. mise run hindsight-setup # set HINDSIGHT_OP_KEY_REF to your 1Password item first");
|
|
2390
|
+
console.log(" 5. Optional per-dev hook opt-out: copy .agents/local.example.json -> .agents/local.json");
|
|
2391
|
+
}
|
|
2392
|
+
};
|
|
2393
|
+
|
|
2394
|
+
// src/recipes/MiseOpInjectRecipe.ts
|
|
2395
|
+
var MiseOpInjectRecipe = class extends Recipe {
|
|
2396
|
+
constructor(context) {
|
|
2397
|
+
super(context);
|
|
2398
|
+
this.addIngredient(WireMiseOpInject);
|
|
2399
|
+
}
|
|
2400
|
+
printNextSteps() {
|
|
2401
|
+
console.log("\u{1F389} Wired up .env.op 1Password resolution via mise!");
|
|
2402
|
+
console.log(" Next steps:");
|
|
2403
|
+
console.log(" 1. Create .env.op with your op:// secret references");
|
|
2404
|
+
console.log(" 2. Run `mise run secrets-inject` or simply cd out and back in to trigger the hook");
|
|
1975
2405
|
}
|
|
1976
2406
|
};
|
|
1977
2407
|
|
|
@@ -2014,6 +2444,12 @@ var RECIPE_REGISTRY = {
|
|
|
2014
2444
|
description: "Retrofit the project-scoped agent-hooks + skill fan-out layer (Claude/Codex/Kimi/Hermes hooks via mise enter/leave)",
|
|
2015
2445
|
class: AgentHooksRecipe,
|
|
2016
2446
|
commands: ["CopyAgentHooksTree", "WireMiseAgentHooks"]
|
|
2447
|
+
},
|
|
2448
|
+
"mise-op-inject": {
|
|
2449
|
+
name: "mise-op-inject",
|
|
2450
|
+
description: "Wire up op-inject script to mise.toml for 1Password secret resolution",
|
|
2451
|
+
class: MiseOpInjectRecipe,
|
|
2452
|
+
commands: ["WireMiseOpInject"]
|
|
2017
2453
|
}
|
|
2018
2454
|
};
|
|
2019
2455
|
var COMMAND_REGISTRY = {
|
|
@@ -2082,6 +2518,12 @@ var COMMAND_REGISTRY = {
|
|
|
2082
2518
|
description: "Create .env.example file",
|
|
2083
2519
|
group: "environment",
|
|
2084
2520
|
class: AddDotenv
|
|
2521
|
+
},
|
|
2522
|
+
WireMiseOpInject: {
|
|
2523
|
+
name: "WireMiseOpInject",
|
|
2524
|
+
description: "Wire up op-inject script to mise.toml for 1Password secret resolution",
|
|
2525
|
+
group: "mise",
|
|
2526
|
+
class: WireMiseOpInject
|
|
2085
2527
|
}
|
|
2086
2528
|
};
|
|
2087
2529
|
function getRecipeNames() {
|
|
@@ -2116,29 +2558,39 @@ function createRecipe(name, context) {
|
|
|
2116
2558
|
import { cancel as cancel2, multiselect, text as text2, isCancel as isCancel5 } from "@clack/prompts";
|
|
2117
2559
|
|
|
2118
2560
|
// src/parity/index.ts
|
|
2119
|
-
import { existsSync as
|
|
2120
|
-
import { basename as basename3, dirname as dirname6, join as
|
|
2561
|
+
import { existsSync as existsSync10, lstatSync, mkdirSync as mkdirSync6, readFileSync as readFileSync6, readlinkSync, readdirSync as readdirSync2, renameSync as renameSync2, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync7, chmodSync as chmodSync4, copyFileSync, rmSync } from "node:fs";
|
|
2562
|
+
import { basename as basename3, dirname as dirname6, join as join13, relative, resolve as resolve2 } from "node:path";
|
|
2121
2563
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
2122
2564
|
import { homedir as homedir5 } from "node:os";
|
|
2123
2565
|
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
2124
|
-
import
|
|
2566
|
+
import YAML3 from "yaml";
|
|
2125
2567
|
var LINK_AGENTFILES_SCRIPT = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'";
|
|
2126
2568
|
var OP_INJECT_SCRIPT = "op inject -i .env.op > .env";
|
|
2569
|
+
var SYNC_SKILLS_SCRIPT = "sync-skills.py --scope project";
|
|
2127
2570
|
var CODEGRAPH_SCRIPT = "[ -f '{{config_root}}/.mise/scripts/codegraph.sh' ] && '{{config_root}}/.mise/scripts/codegraph.sh' || true";
|
|
2571
|
+
var SKILLS_REGISTRY_URL = "https://github.com/delorenj/skillex.git";
|
|
2128
2572
|
var HOOKS_COMMENT_HEADER = `# This block will handle the linking of
|
|
2129
2573
|
# agent files to the main AGENTS.md file.
|
|
2130
2574
|
#
|
|
2131
2575
|
# TODO: Ensure this works for all levels of nesting.
|
|
2132
2576
|
# i.e. All linked agent files MUST be siblings at
|
|
2133
2577
|
# any given level of nesting.`;
|
|
2134
|
-
var LINK_AGENTFILES_HOOK_ENTRIES = [LINK_AGENTFILES_SCRIPT, OP_INJECT_SCRIPT];
|
|
2578
|
+
var LINK_AGENTFILES_HOOK_ENTRIES = [LINK_AGENTFILES_SCRIPT, OP_INJECT_SCRIPT, SYNC_SKILLS_SCRIPT];
|
|
2135
2579
|
var LINK_AGENTFILES_WATCH_TASK_BLOCK = `[[watch_files]]
|
|
2136
2580
|
patterns = ["AGENTS.md"]
|
|
2137
2581
|
task = "link-agentfiles"
|
|
2138
2582
|
|
|
2583
|
+
[[watch_files]]
|
|
2584
|
+
patterns = [".agents/skills.json"]
|
|
2585
|
+
task = "skills-sync"
|
|
2586
|
+
|
|
2139
2587
|
[tasks.link-agentfiles]
|
|
2140
2588
|
description = "Symlink all agent files to AGENTS.md"
|
|
2141
|
-
run = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'"
|
|
2589
|
+
run = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'"
|
|
2590
|
+
|
|
2591
|
+
[tasks.skills-sync]
|
|
2592
|
+
description = "Sync skills from manifest to local CLI dirs"
|
|
2593
|
+
run = "sync-skills.py --scope project"`;
|
|
2142
2594
|
var VERSIONING_BLOCK = `# >>> mise-versioning >>> (managed block \u2014 do not edit by hand; re-run init to update)
|
|
2143
2595
|
[tasks."version"]
|
|
2144
2596
|
description = "Print the current version (vX.Y.Z)"
|
|
@@ -2168,7 +2620,7 @@ run = "'{{config_root}}/.mise/scripts/versioning.sh' sync"
|
|
|
2168
2620
|
function resolvePjanglerRoot2() {
|
|
2169
2621
|
let dir = dirname6(fileURLToPath3(import.meta.url));
|
|
2170
2622
|
while (dir !== dirname6(dir)) {
|
|
2171
|
-
if (
|
|
2623
|
+
if (existsSync10(join13(dir, "package.json")) && existsSync10(join13(dir, "templates", "commonproject", "copier.yml"))) {
|
|
2172
2624
|
return dir;
|
|
2173
2625
|
}
|
|
2174
2626
|
dir = dirname6(dir);
|
|
@@ -2179,17 +2631,17 @@ function normalizeNewlines(value) {
|
|
|
2179
2631
|
return value.replace(/\r\n/g, "\n");
|
|
2180
2632
|
}
|
|
2181
2633
|
function readText(path) {
|
|
2182
|
-
return normalizeNewlines(
|
|
2634
|
+
return normalizeNewlines(readFileSync6(path, "utf8"));
|
|
2183
2635
|
}
|
|
2184
2636
|
function safeReadText(path) {
|
|
2185
|
-
return
|
|
2637
|
+
return existsSync10(path) ? readText(path) : null;
|
|
2186
2638
|
}
|
|
2187
2639
|
function ensureParent(path) {
|
|
2188
2640
|
mkdirSync6(dirname6(path), { recursive: true });
|
|
2189
2641
|
}
|
|
2190
2642
|
function writeText(path, content) {
|
|
2191
2643
|
ensureParent(path);
|
|
2192
|
-
|
|
2644
|
+
writeFileSync7(path, content);
|
|
2193
2645
|
}
|
|
2194
2646
|
function tryParseJson(text3) {
|
|
2195
2647
|
if (!text3) return null;
|
|
@@ -2206,7 +2658,7 @@ function titleCaseSlug(slug) {
|
|
|
2206
2658
|
return slug.split(/[-_]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
2207
2659
|
}
|
|
2208
2660
|
function readSymlinkTarget(path) {
|
|
2209
|
-
if (!
|
|
2661
|
+
if (!existsSync10(path)) return null;
|
|
2210
2662
|
try {
|
|
2211
2663
|
return readlinkSync(path);
|
|
2212
2664
|
} catch {
|
|
@@ -2214,7 +2666,7 @@ function readSymlinkTarget(path) {
|
|
|
2214
2666
|
}
|
|
2215
2667
|
}
|
|
2216
2668
|
function ensureSymlink(path, target, dryRun) {
|
|
2217
|
-
if (
|
|
2669
|
+
if (existsSync10(path)) {
|
|
2218
2670
|
const stat = lstatSync(path);
|
|
2219
2671
|
if (stat.isSymbolicLink()) {
|
|
2220
2672
|
const current = readSymlinkTarget(path);
|
|
@@ -2231,11 +2683,11 @@ function ensureSymlink(path, target, dryRun) {
|
|
|
2231
2683
|
return { changed: true };
|
|
2232
2684
|
}
|
|
2233
2685
|
function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
2234
|
-
const agentsPath =
|
|
2235
|
-
if (
|
|
2686
|
+
const agentsPath = join13(repoRoot, "AGENTS.md");
|
|
2687
|
+
if (existsSync10(agentsPath)) return { changedFiles: [], details: [] };
|
|
2236
2688
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2237
|
-
const source =
|
|
2238
|
-
if (!
|
|
2689
|
+
const source = join13(repoRoot, file);
|
|
2690
|
+
if (!existsSync10(source)) continue;
|
|
2239
2691
|
const stat = lstatSync(source);
|
|
2240
2692
|
if (stat.isSymbolicLink()) continue;
|
|
2241
2693
|
if (stat.isFile()) {
|
|
@@ -2244,8 +2696,8 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
|
2244
2696
|
}
|
|
2245
2697
|
return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
|
|
2246
2698
|
}
|
|
2247
|
-
const readmePath =
|
|
2248
|
-
if (
|
|
2699
|
+
const readmePath = join13(repoRoot, "README.md");
|
|
2700
|
+
if (existsSync10(readmePath)) {
|
|
2249
2701
|
const stat = lstatSync(readmePath);
|
|
2250
2702
|
if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
|
|
2251
2703
|
if (!dryRun) copyFileSync(readmePath, agentsPath);
|
|
@@ -2284,12 +2736,12 @@ function yamlGet(text3, keyPath) {
|
|
|
2284
2736
|
return "";
|
|
2285
2737
|
}
|
|
2286
2738
|
function discoverRoles(repoRoot) {
|
|
2287
|
-
const rolesDir =
|
|
2288
|
-
if (!
|
|
2739
|
+
const rolesDir = join13(repoRoot, "agents", "hermes");
|
|
2740
|
+
if (!existsSync10(rolesDir)) return [];
|
|
2289
2741
|
return readdirSync2(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
|
|
2290
|
-
const roleDir =
|
|
2291
|
-
const roleYamlPath =
|
|
2292
|
-
if (!
|
|
2742
|
+
const roleDir = join13(rolesDir, entry.name);
|
|
2743
|
+
const roleYamlPath = join13(roleDir, "role.yaml");
|
|
2744
|
+
if (!existsSync10(roleYamlPath)) return null;
|
|
2293
2745
|
const text3 = readText(roleYamlPath);
|
|
2294
2746
|
const runtimeRepoRaw = yamlGet(text3, "runtime.github_repo");
|
|
2295
2747
|
return {
|
|
@@ -2317,7 +2769,7 @@ function discoverRoles(repoRoot) {
|
|
|
2317
2769
|
}).filter((value) => Boolean(value));
|
|
2318
2770
|
}
|
|
2319
2771
|
function registryPath(homeDir) {
|
|
2320
|
-
return
|
|
2772
|
+
return join13(homeDir, ".hermes", "agents-registry.yaml");
|
|
2321
2773
|
}
|
|
2322
2774
|
function systemctlUser(args) {
|
|
2323
2775
|
const result = spawnSync6("systemctl", ["--user", ...args], { encoding: "utf8" });
|
|
@@ -2328,8 +2780,8 @@ function systemctlUser(args) {
|
|
|
2328
2780
|
};
|
|
2329
2781
|
}
|
|
2330
2782
|
function templateScript(ctx, name) {
|
|
2331
|
-
const source =
|
|
2332
|
-
return
|
|
2783
|
+
const source = join13(ctx.pjanglerRoot, ".mise", "scripts", name);
|
|
2784
|
+
return existsSync10(source) ? readText(source) : void 0;
|
|
2333
2785
|
}
|
|
2334
2786
|
function templateVersioningScript(ctx) {
|
|
2335
2787
|
return templateScript(ctx, "versioning.sh");
|
|
@@ -2341,8 +2793,8 @@ function resolveAgentHooksLayer2(ctx) {
|
|
|
2341
2793
|
const override = process.env.PJ_AGENT_HOOKS_LAYER;
|
|
2342
2794
|
if (override === "0" || override === "false") return false;
|
|
2343
2795
|
if (override === "1" || override === "true") return true;
|
|
2344
|
-
if (
|
|
2345
|
-
return !
|
|
2796
|
+
if (existsSync10(join13(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
|
|
2797
|
+
return !existsSync10(join13(ctx.homeDir, ".agents", "hooks"));
|
|
2346
2798
|
}
|
|
2347
2799
|
function evaluateMiseConditionals(template, agentHooksLayer) {
|
|
2348
2800
|
const out = [];
|
|
@@ -2372,19 +2824,36 @@ function renderGeneratedProjectMiseToml(ctx, template) {
|
|
|
2372
2824
|
return evaluateMiseConditionals(template, resolveAgentHooksLayer2(ctx)).replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
|
|
2373
2825
|
}
|
|
2374
2826
|
function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
2375
|
-
const targetPath =
|
|
2376
|
-
if (
|
|
2377
|
-
const sourcePath =
|
|
2378
|
-
if (!
|
|
2827
|
+
const targetPath = join13(ctx.repoRoot, "mise.toml");
|
|
2828
|
+
if (existsSync10(targetPath)) return false;
|
|
2829
|
+
const sourcePath = join13(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
|
|
2830
|
+
if (!existsSync10(sourcePath)) return false;
|
|
2379
2831
|
changedFiles.push(targetPath);
|
|
2380
2832
|
if (!ctx.dryRun) {
|
|
2381
2833
|
writeText(targetPath, renderGeneratedProjectMiseToml(ctx, readText(sourcePath)));
|
|
2382
2834
|
}
|
|
2383
2835
|
return true;
|
|
2384
2836
|
}
|
|
2837
|
+
function templateCommonProjectText(ctx, rel) {
|
|
2838
|
+
const path = join13(ctx.pjanglerRoot, "templates", "commonproject", "template", rel);
|
|
2839
|
+
return existsSync10(path) ? readText(path) : void 0;
|
|
2840
|
+
}
|
|
2841
|
+
function canonicalSkillsManifest() {
|
|
2842
|
+
return `${JSON.stringify(
|
|
2843
|
+
{
|
|
2844
|
+
$schema: "https://raw.githubusercontent.com/skillex/schemas/main/skills.schema.json",
|
|
2845
|
+
inherit_global: true,
|
|
2846
|
+
registry: SKILLS_REGISTRY_URL,
|
|
2847
|
+
skills: []
|
|
2848
|
+
},
|
|
2849
|
+
null,
|
|
2850
|
+
2
|
|
2851
|
+
)}
|
|
2852
|
+
`;
|
|
2853
|
+
}
|
|
2385
2854
|
function templateVersionFilesConf(ctx, repoRoot) {
|
|
2386
|
-
const packageJson =
|
|
2387
|
-
return
|
|
2855
|
+
const packageJson = join13(repoRoot, "package.json");
|
|
2856
|
+
return existsSync10(packageJson) ? "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\njson package.json\ngittag .\n" : "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\ngittag .\n";
|
|
2388
2857
|
}
|
|
2389
2858
|
function replaceOrAppendManagedBlock(text3, startMarker, block, beforePattern) {
|
|
2390
2859
|
if (startMarker.test(text3)) {
|
|
@@ -2408,7 +2877,7 @@ var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/herm
|
|
|
2408
2877
|
function requiredMisePathEntries(ctx) {
|
|
2409
2878
|
const required = [...BASE_MISE_PATH_ENTRIES];
|
|
2410
2879
|
for (const candidate of CONDITIONAL_HERMES_PATHS) {
|
|
2411
|
-
if (
|
|
2880
|
+
if (existsSync10(join13(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
|
|
2412
2881
|
}
|
|
2413
2882
|
return required;
|
|
2414
2883
|
}
|
|
@@ -2509,6 +2978,9 @@ function stripTomlStringsAndComments(line) {
|
|
|
2509
2978
|
function isManagedHookEntry(value) {
|
|
2510
2979
|
const trimmed = value.trim();
|
|
2511
2980
|
if (trimmed === OP_INJECT_SCRIPT) return true;
|
|
2981
|
+
if (trimmed === SYNC_SKILLS_SCRIPT) return true;
|
|
2982
|
+
if (/link-project-skills-to-clis\.sh'?\s*$/.test(trimmed)) return true;
|
|
2983
|
+
if (/unlink-project-skills-from-clis\.sh'?\s*$/.test(trimmed)) return true;
|
|
2512
2984
|
return /link-agentfiles\.sh'?\s*$/.test(trimmed);
|
|
2513
2985
|
}
|
|
2514
2986
|
function normalizeHookScript(script) {
|
|
@@ -2611,12 +3083,17 @@ function upsertLinkAgentfilesHooks(text3) {
|
|
|
2611
3083
|
function upsertLinkAgentfilesBlock(text3, ctx) {
|
|
2612
3084
|
const withPath = upsertMisePath(text3, requiredMisePathEntries(ctx));
|
|
2613
3085
|
let cleaned = removeTomlSection(withPath, /^\[tasks\.link-agentfiles\]$/, /link-agentfiles/, { includePrecedingComments: false });
|
|
3086
|
+
cleaned = removeTomlSection(cleaned, /^\[tasks\.skills-sync\]$/, void 0, { includePrecedingComments: false });
|
|
3087
|
+
cleaned = removeTomlSection(cleaned, /^\[tasks\.link-project-skills-to-clis\]$/, void 0, { includePrecedingComments: false });
|
|
3088
|
+
cleaned = removeTomlSection(cleaned, /^\[tasks\.unlink-project-skills-from-clis\]$/, void 0, { includePrecedingComments: false });
|
|
3089
|
+
cleaned = removeTomlSection(cleaned, /^\[tasks\.skills-relink\]$/, void 0, { includePrecedingComments: false });
|
|
2614
3090
|
cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /AGENTS\.md/, { includePrecedingComments: false });
|
|
3091
|
+
cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /\.agents\/skills\.json/, { includePrecedingComments: false });
|
|
2615
3092
|
cleaned = upsertLinkAgentfilesHooks(cleaned);
|
|
2616
3093
|
return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
|
|
2617
3094
|
}
|
|
2618
3095
|
function readProjectJson(ctx) {
|
|
2619
|
-
return tryParseJson(safeReadText(
|
|
3096
|
+
return tryParseJson(safeReadText(join13(ctx.repoRoot, ".project.json")));
|
|
2620
3097
|
}
|
|
2621
3098
|
function boolSetting(value, fallback) {
|
|
2622
3099
|
if (typeof value === "boolean") return value;
|
|
@@ -2691,12 +3168,12 @@ function canonicalProjectJson(ctx) {
|
|
|
2691
3168
|
};
|
|
2692
3169
|
}
|
|
2693
3170
|
function projectJsonFinding(ctx) {
|
|
2694
|
-
const projectPath =
|
|
2695
|
-
const planeJsonPath =
|
|
3171
|
+
const projectPath = join13(ctx.repoRoot, ".project.json");
|
|
3172
|
+
const planeJsonPath = join13(ctx.repoRoot, ".plane.json");
|
|
2696
3173
|
const details = [];
|
|
2697
3174
|
const data = readProjectJson(ctx);
|
|
2698
3175
|
const roles = discoverRoles(ctx.repoRoot);
|
|
2699
|
-
if (!
|
|
3176
|
+
if (!existsSync10(projectPath)) {
|
|
2700
3177
|
return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json missing", details: [], fixable: true };
|
|
2701
3178
|
}
|
|
2702
3179
|
if (!data) {
|
|
@@ -2731,7 +3208,7 @@ function projectJsonFinding(ctx) {
|
|
|
2731
3208
|
for (const key of ["enabled", "grace_hours", "auto_review"]) {
|
|
2732
3209
|
if (!(key in reconcile)) details.push(`automation.reconcile.${key} missing`);
|
|
2733
3210
|
}
|
|
2734
|
-
if (
|
|
3211
|
+
if (existsSync10(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
|
|
2735
3212
|
return {
|
|
2736
3213
|
id: "sot.project-json",
|
|
2737
3214
|
title: "Canonical .project.json",
|
|
@@ -2812,17 +3289,17 @@ exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" HERMES_OAUTH
|
|
|
2812
3289
|
`.replace(/\u0010/g, "$");
|
|
2813
3290
|
}
|
|
2814
3291
|
function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
|
|
2815
|
-
if (!
|
|
3292
|
+
if (!existsSync10(sourceDir)) return;
|
|
2816
3293
|
mkdirSync6(targetDir, { recursive: true });
|
|
2817
3294
|
for (const entry of readdirSync2(sourceDir, { withFileTypes: true })) {
|
|
2818
|
-
const sourcePath =
|
|
3295
|
+
const sourcePath = join13(sourceDir, entry.name);
|
|
2819
3296
|
if (skip?.(sourcePath)) continue;
|
|
2820
|
-
const targetPath =
|
|
3297
|
+
const targetPath = join13(targetDir, entry.name);
|
|
2821
3298
|
if (entry.isDirectory()) {
|
|
2822
3299
|
copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
|
|
2823
3300
|
continue;
|
|
2824
3301
|
}
|
|
2825
|
-
if (
|
|
3302
|
+
if (existsSync10(targetPath)) continue;
|
|
2826
3303
|
changedFiles.push(targetPath);
|
|
2827
3304
|
if (!dryRun) {
|
|
2828
3305
|
ensureParent(targetPath);
|
|
@@ -2831,7 +3308,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
|
|
|
2831
3308
|
}
|
|
2832
3309
|
}
|
|
2833
3310
|
function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
|
|
2834
|
-
const gitmodulesPath =
|
|
3311
|
+
const gitmodulesPath = join13(repoRoot, ".gitmodules");
|
|
2835
3312
|
const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
|
|
2836
3313
|
const owner = role.runtimeOwner || "delorenj";
|
|
2837
3314
|
const block = `[submodule "agents/hermes/${role.role}/runtime"]
|
|
@@ -2998,10 +3475,10 @@ function runBmadInstall(repoRoot) {
|
|
|
2998
3475
|
return { ok: true };
|
|
2999
3476
|
}
|
|
3000
3477
|
function readInstalledBmadVersion(repoRoot) {
|
|
3001
|
-
const raw = safeReadText(
|
|
3478
|
+
const raw = safeReadText(join13(repoRoot, "_bmad", "_config", "manifest.yaml"));
|
|
3002
3479
|
if (!raw) return void 0;
|
|
3003
3480
|
try {
|
|
3004
|
-
const parsed =
|
|
3481
|
+
const parsed = YAML3.parse(raw);
|
|
3005
3482
|
const version = parsed?.installation?.version;
|
|
3006
3483
|
return typeof version === "string" && version.trim() ? version.trim() : void 0;
|
|
3007
3484
|
} catch {
|
|
@@ -3009,8 +3486,8 @@ function readInstalledBmadVersion(repoRoot) {
|
|
|
3009
3486
|
}
|
|
3010
3487
|
}
|
|
3011
3488
|
function bmadCachePath(homeDir) {
|
|
3012
|
-
const cacheRoot = process.env.XDG_CACHE_HOME?.trim() ||
|
|
3013
|
-
return
|
|
3489
|
+
const cacheRoot = process.env.XDG_CACHE_HOME?.trim() || join13(homeDir, ".cache");
|
|
3490
|
+
return join13(cacheRoot, "pjangler", "bmad-dist-tags.json");
|
|
3014
3491
|
}
|
|
3015
3492
|
function readBmadDistTagsCache(homeDir) {
|
|
3016
3493
|
const raw = safeReadText(bmadCachePath(homeDir));
|
|
@@ -3053,7 +3530,7 @@ function resolveBmadDistTags(homeDir) {
|
|
|
3053
3530
|
try {
|
|
3054
3531
|
const path = bmadCachePath(homeDir);
|
|
3055
3532
|
mkdirSync6(dirname6(path), { recursive: true });
|
|
3056
|
-
|
|
3533
|
+
writeFileSync7(path, JSON.stringify({ fetchedAt: Date.now(), distTags: fetched }, null, 2));
|
|
3057
3534
|
} catch {
|
|
3058
3535
|
}
|
|
3059
3536
|
return { distTags: fetched, stale: false };
|
|
@@ -3098,14 +3575,14 @@ var RULES = [
|
|
|
3098
3575
|
id: "mise.config-root",
|
|
3099
3576
|
title: "mise config_root + AGENTS link hooks",
|
|
3100
3577
|
audit: (ctx) => {
|
|
3101
|
-
const misePath =
|
|
3102
|
-
if (!
|
|
3578
|
+
const misePath = join13(ctx.repoRoot, "mise.toml");
|
|
3579
|
+
if (!existsSync10(misePath)) {
|
|
3103
3580
|
return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
|
|
3104
3581
|
}
|
|
3105
3582
|
const text3 = readText(misePath);
|
|
3106
3583
|
const details = [];
|
|
3107
|
-
const linkAgentfilesPath =
|
|
3108
|
-
if (!
|
|
3584
|
+
const linkAgentfilesPath = join13(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
3585
|
+
if (!existsSync10(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
|
|
3109
3586
|
const pathValues = [...(text3.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
3110
3587
|
const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
|
|
3111
3588
|
if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
|
|
@@ -3123,10 +3600,10 @@ var RULES = [
|
|
|
3123
3600
|
};
|
|
3124
3601
|
},
|
|
3125
3602
|
migrate: (ctx, finding) => {
|
|
3126
|
-
const path =
|
|
3603
|
+
const path = join13(ctx.repoRoot, "mise.toml");
|
|
3127
3604
|
const changedFiles = [];
|
|
3128
3605
|
const details = [];
|
|
3129
|
-
if (!
|
|
3606
|
+
if (!existsSync10(path)) {
|
|
3130
3607
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
3131
3608
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing and no generated-project mise template available to initialize from", changedFiles, details: [] };
|
|
3132
3609
|
}
|
|
@@ -3142,7 +3619,7 @@ var RULES = [
|
|
|
3142
3619
|
if (!ctx.dryRun) writeText(path, next);
|
|
3143
3620
|
text3 = next;
|
|
3144
3621
|
}
|
|
3145
|
-
const linkAgentfilesPath =
|
|
3622
|
+
const linkAgentfilesPath = join13(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
3146
3623
|
const expectedScript = templateLinkAgentfilesScript(ctx);
|
|
3147
3624
|
if (expectedScript === void 0) {
|
|
3148
3625
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "pjangler install is missing .mise/scripts/link-agentfiles.sh \u2014 update @delorenj/pjangler (broken package)", changedFiles, details: [] };
|
|
@@ -3151,7 +3628,7 @@ var RULES = [
|
|
|
3151
3628
|
changedFiles.push(linkAgentfilesPath);
|
|
3152
3629
|
if (!ctx.dryRun) {
|
|
3153
3630
|
writeText(linkAgentfilesPath, expectedScript);
|
|
3154
|
-
|
|
3631
|
+
chmodSync4(linkAgentfilesPath, 493);
|
|
3155
3632
|
}
|
|
3156
3633
|
}
|
|
3157
3634
|
return {
|
|
@@ -3169,13 +3646,13 @@ var RULES = [
|
|
|
3169
3646
|
title: "managed mise versioning block",
|
|
3170
3647
|
audit: (ctx) => {
|
|
3171
3648
|
const details = [];
|
|
3172
|
-
const misePath =
|
|
3173
|
-
const versioningPath =
|
|
3174
|
-
const manifestPath =
|
|
3649
|
+
const misePath = join13(ctx.repoRoot, "mise.toml");
|
|
3650
|
+
const versioningPath = join13(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
3651
|
+
const manifestPath = join13(ctx.repoRoot, ".mise", "version-files.conf");
|
|
3175
3652
|
const text3 = safeReadText(misePath);
|
|
3176
3653
|
if (!text3?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
|
|
3177
|
-
if (!
|
|
3178
|
-
if (!
|
|
3654
|
+
if (!existsSync10(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
|
|
3655
|
+
if (!existsSync10(manifestPath)) details.push(".mise/version-files.conf missing");
|
|
3179
3656
|
return {
|
|
3180
3657
|
id: "mise.versioning",
|
|
3181
3658
|
title: "managed mise versioning block",
|
|
@@ -3188,8 +3665,8 @@ var RULES = [
|
|
|
3188
3665
|
migrate: (ctx, finding) => {
|
|
3189
3666
|
const changedFiles = [];
|
|
3190
3667
|
const details = [];
|
|
3191
|
-
const misePath =
|
|
3192
|
-
if (!
|
|
3668
|
+
const misePath = join13(ctx.repoRoot, "mise.toml");
|
|
3669
|
+
if (!existsSync10(misePath)) {
|
|
3193
3670
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
3194
3671
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing and no generated-project mise template available to initialize from", changedFiles, details: [] };
|
|
3195
3672
|
}
|
|
@@ -3213,7 +3690,7 @@ var RULES = [
|
|
|
3213
3690
|
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
3214
3691
|
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
3215
3692
|
}
|
|
3216
|
-
const versioningPath =
|
|
3693
|
+
const versioningPath = join13(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
3217
3694
|
const expectedScript = templateVersioningScript(ctx);
|
|
3218
3695
|
if (expectedScript === void 0) {
|
|
3219
3696
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "pjangler install is missing .mise/scripts/versioning.sh \u2014 update @delorenj/pjangler (broken package)", changedFiles, details: [] };
|
|
@@ -3222,10 +3699,10 @@ var RULES = [
|
|
|
3222
3699
|
changedFiles.push(versioningPath);
|
|
3223
3700
|
if (!ctx.dryRun) {
|
|
3224
3701
|
writeText(versioningPath, expectedScript);
|
|
3225
|
-
|
|
3702
|
+
chmodSync4(versioningPath, 493);
|
|
3226
3703
|
}
|
|
3227
3704
|
}
|
|
3228
|
-
const manifestPath =
|
|
3705
|
+
const manifestPath = join13(ctx.repoRoot, ".mise", "version-files.conf");
|
|
3229
3706
|
const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
|
|
3230
3707
|
if (safeReadText(manifestPath) !== expectedManifest) {
|
|
3231
3708
|
changedFiles.push(manifestPath);
|
|
@@ -3241,13 +3718,125 @@ var RULES = [
|
|
|
3241
3718
|
};
|
|
3242
3719
|
}
|
|
3243
3720
|
},
|
|
3721
|
+
{
|
|
3722
|
+
id: "skills.project-manifest",
|
|
3723
|
+
title: "Skillex project skills manifest",
|
|
3724
|
+
audit: (ctx) => {
|
|
3725
|
+
const details = [];
|
|
3726
|
+
const manifestPath = join13(ctx.repoRoot, ".agents", "skills.json");
|
|
3727
|
+
const legacyDir = join13(ctx.repoRoot, ".agents", "skills");
|
|
3728
|
+
const localExamplePath = join13(ctx.repoRoot, ".agents", "local.example.json");
|
|
3729
|
+
const misePath = join13(ctx.repoRoot, "mise.toml");
|
|
3730
|
+
let fixable = true;
|
|
3731
|
+
const manifest = tryParseJson(safeReadText(manifestPath));
|
|
3732
|
+
if (!manifest) {
|
|
3733
|
+
details.push(".agents/skills.json missing or invalid JSON");
|
|
3734
|
+
} else {
|
|
3735
|
+
if (manifest.inherit_global !== true) details.push(".agents/skills.json should set inherit_global: true");
|
|
3736
|
+
if (manifest.registry !== SKILLS_REGISTRY_URL) details.push(`.agents/skills.json should set registry to ${SKILLS_REGISTRY_URL}`);
|
|
3737
|
+
if (!Array.isArray(manifest.skills)) details.push(".agents/skills.json should define a skills array");
|
|
3738
|
+
}
|
|
3739
|
+
if (existsSync10(legacyDir)) {
|
|
3740
|
+
const entries = readdirSync2(legacyDir);
|
|
3741
|
+
if (entries.length > 0) {
|
|
3742
|
+
details.push(".agents/skills/ still contains legacy committed skills; map them into .agents/skills.json before migrating");
|
|
3743
|
+
fixable = false;
|
|
3744
|
+
} else {
|
|
3745
|
+
details.push(".agents/skills/ legacy directory should be removed");
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
for (const rel of [".mise/scripts/link-project-skills-to-clis.sh", ".mise/scripts/unlink-project-skills-from-clis.sh"]) {
|
|
3749
|
+
if (existsSync10(join13(ctx.repoRoot, rel))) details.push(`${rel} is a legacy symlink-era script and should be removed`);
|
|
3750
|
+
}
|
|
3751
|
+
const localExample = tryParseJson(safeReadText(localExamplePath));
|
|
3752
|
+
if (localExample && Object.prototype.hasOwnProperty.call(localExample, "skills")) {
|
|
3753
|
+
details.push(".agents/local.example.json still documents legacy skills overrides; drop the skills section");
|
|
3754
|
+
}
|
|
3755
|
+
const mise = safeReadText(misePath);
|
|
3756
|
+
if (!mise?.includes(SYNC_SKILLS_SCRIPT)) details.push("mise.toml should run sync-skills.py --scope project on enter");
|
|
3757
|
+
if (!mise?.includes('patterns = [".agents/skills.json"]')) details.push("mise.toml should watch .agents/skills.json");
|
|
3758
|
+
if (!mise?.includes("[tasks.skills-sync]")) details.push("mise.toml should define a skills-sync task");
|
|
3759
|
+
if (mise?.includes("link-project-skills-to-clis.sh") || mise?.includes("unlink-project-skills-from-clis.sh") || mise?.includes("[tasks.skills-relink]")) {
|
|
3760
|
+
details.push("mise.toml still contains legacy skill-link wiring");
|
|
3761
|
+
}
|
|
3762
|
+
return {
|
|
3763
|
+
id: "skills.project-manifest",
|
|
3764
|
+
title: "Skillex project skills manifest",
|
|
3765
|
+
status: details.length === 0 ? "pass" : "fail",
|
|
3766
|
+
summary: details.length === 0 ? "Skillex skills manifest parity verified" : `${details.length} Skillex migration issue(s) detected`,
|
|
3767
|
+
details,
|
|
3768
|
+
fixable
|
|
3769
|
+
};
|
|
3770
|
+
},
|
|
3771
|
+
migrate: (ctx, finding) => {
|
|
3772
|
+
const changedFiles = [];
|
|
3773
|
+
const details = [];
|
|
3774
|
+
const manifestPath = join13(ctx.repoRoot, ".agents", "skills.json");
|
|
3775
|
+
const legacyDir = join13(ctx.repoRoot, ".agents", "skills");
|
|
3776
|
+
const localExamplePath = join13(ctx.repoRoot, ".agents", "local.example.json");
|
|
3777
|
+
const misePath = join13(ctx.repoRoot, "mise.toml");
|
|
3778
|
+
if (existsSync10(legacyDir)) {
|
|
3779
|
+
const entries = readdirSync2(legacyDir);
|
|
3780
|
+
if (entries.length > 0) {
|
|
3781
|
+
return {
|
|
3782
|
+
id: finding.id,
|
|
3783
|
+
title: finding.title,
|
|
3784
|
+
status: "blocked",
|
|
3785
|
+
summary: "Legacy .agents/skills/ still contains committed skills; migrate them into .agents/skills.json manually first",
|
|
3786
|
+
changedFiles,
|
|
3787
|
+
details: entries.map((entry) => `.agents/skills/${entry}`)
|
|
3788
|
+
};
|
|
3789
|
+
}
|
|
3790
|
+
changedFiles.push(legacyDir);
|
|
3791
|
+
if (!ctx.dryRun) rmSync(legacyDir, { recursive: true, force: true });
|
|
3792
|
+
}
|
|
3793
|
+
const expectedManifest = canonicalSkillsManifest();
|
|
3794
|
+
if (safeReadText(manifestPath) !== expectedManifest) {
|
|
3795
|
+
changedFiles.push(manifestPath);
|
|
3796
|
+
if (!ctx.dryRun) writeText(manifestPath, expectedManifest);
|
|
3797
|
+
details.push("Wrote canonical .agents/skills.json manifest");
|
|
3798
|
+
}
|
|
3799
|
+
for (const rel of [".mise/scripts/link-project-skills-to-clis.sh", ".mise/scripts/unlink-project-skills-from-clis.sh"]) {
|
|
3800
|
+
const path = join13(ctx.repoRoot, rel);
|
|
3801
|
+
if (existsSync10(path)) {
|
|
3802
|
+
changedFiles.push(path);
|
|
3803
|
+
if (!ctx.dryRun) unlinkSync3(path);
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
const templateLocalExample = templateCommonProjectText(ctx, ".agents/local.example.json");
|
|
3807
|
+
const currentLocalExample = safeReadText(localExamplePath);
|
|
3808
|
+
if (templateLocalExample && currentLocalExample && currentLocalExample !== templateLocalExample) {
|
|
3809
|
+
changedFiles.push(localExamplePath);
|
|
3810
|
+
if (!ctx.dryRun) writeText(localExamplePath, templateLocalExample);
|
|
3811
|
+
}
|
|
3812
|
+
if (!existsSync10(misePath)) {
|
|
3813
|
+
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
3814
|
+
return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing and no generated-project mise template available to initialize from", changedFiles, details };
|
|
3815
|
+
}
|
|
3816
|
+
}
|
|
3817
|
+
const currentMise = readText(misePath);
|
|
3818
|
+
const nextMise = upsertLinkAgentfilesBlock(currentMise, ctx);
|
|
3819
|
+
if (nextMise !== currentMise) {
|
|
3820
|
+
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
3821
|
+
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
3822
|
+
}
|
|
3823
|
+
return {
|
|
3824
|
+
id: finding.id,
|
|
3825
|
+
title: finding.title,
|
|
3826
|
+
status: changedFiles.length ? "applied" : "noop",
|
|
3827
|
+
summary: changedFiles.length ? "Skillex skills manifest contract normalized" : "No changes required",
|
|
3828
|
+
changedFiles,
|
|
3829
|
+
details
|
|
3830
|
+
};
|
|
3831
|
+
}
|
|
3832
|
+
},
|
|
3244
3833
|
{
|
|
3245
3834
|
id: "sot.agent-symlinks",
|
|
3246
3835
|
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
3247
3836
|
audit: (ctx) => {
|
|
3248
|
-
const agentsPath =
|
|
3249
|
-
if (!
|
|
3250
|
-
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) =>
|
|
3837
|
+
const agentsPath = join13(ctx.repoRoot, "AGENTS.md");
|
|
3838
|
+
if (!existsSync10(agentsPath)) {
|
|
3839
|
+
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync10(join13(ctx.repoRoot, file)));
|
|
3251
3840
|
if (fallbackSources.length === 0) {
|
|
3252
3841
|
return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
|
|
3253
3842
|
}
|
|
@@ -3262,7 +3851,7 @@ var RULES = [
|
|
|
3262
3851
|
}
|
|
3263
3852
|
const details = [];
|
|
3264
3853
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
3265
|
-
const full =
|
|
3854
|
+
const full = join13(ctx.repoRoot, file);
|
|
3266
3855
|
const target = readSymlinkTarget(full);
|
|
3267
3856
|
if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
|
|
3268
3857
|
}
|
|
@@ -3286,7 +3875,7 @@ var RULES = [
|
|
|
3286
3875
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
|
|
3287
3876
|
}
|
|
3288
3877
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
3289
|
-
const full =
|
|
3878
|
+
const full = join13(ctx.repoRoot, file);
|
|
3290
3879
|
const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
|
|
3291
3880
|
if (result.blocked) blockedDetails.push(result.blocked);
|
|
3292
3881
|
if (result.changed) changedFiles.push(full);
|
|
@@ -3308,7 +3897,7 @@ var RULES = [
|
|
|
3308
3897
|
migrate: (ctx, finding) => {
|
|
3309
3898
|
const changedFiles = [];
|
|
3310
3899
|
const details = [];
|
|
3311
|
-
const path =
|
|
3900
|
+
const path = join13(ctx.repoRoot, ".project.json");
|
|
3312
3901
|
const existing = readProjectJson(ctx) ?? {};
|
|
3313
3902
|
const canonical = canonicalProjectJson(ctx);
|
|
3314
3903
|
const merged = { ...existing, ...canonical };
|
|
@@ -3318,10 +3907,10 @@ var RULES = [
|
|
|
3318
3907
|
changedFiles.push(path);
|
|
3319
3908
|
if (!ctx.dryRun) writeText(path, expected);
|
|
3320
3909
|
}
|
|
3321
|
-
const planeJson =
|
|
3322
|
-
if (
|
|
3910
|
+
const planeJson = join13(ctx.repoRoot, ".plane.json");
|
|
3911
|
+
if (existsSync10(planeJson)) {
|
|
3323
3912
|
const backup = `${planeJson}.migrated-backup`;
|
|
3324
|
-
if (
|
|
3913
|
+
if (existsSync10(backup)) {
|
|
3325
3914
|
details.push(`cannot back up .plane.json because ${relative(ctx.repoRoot, backup)} already exists`);
|
|
3326
3915
|
} else {
|
|
3327
3916
|
changedFiles.push(backup);
|
|
@@ -3343,8 +3932,8 @@ var RULES = [
|
|
|
3343
3932
|
title: ".env.op + gitignore secrets contract",
|
|
3344
3933
|
audit: (ctx) => {
|
|
3345
3934
|
const details = [];
|
|
3346
|
-
const envOp = safeReadText(
|
|
3347
|
-
const gitignore = safeReadText(
|
|
3935
|
+
const envOp = safeReadText(join13(ctx.repoRoot, ".env.op"));
|
|
3936
|
+
const gitignore = safeReadText(join13(ctx.repoRoot, ".gitignore"));
|
|
3348
3937
|
if (!envOp) {
|
|
3349
3938
|
details.push(".env.op missing");
|
|
3350
3939
|
} else {
|
|
@@ -3370,12 +3959,12 @@ var RULES = [
|
|
|
3370
3959
|
migrate: (ctx, finding) => {
|
|
3371
3960
|
const changedFiles = [];
|
|
3372
3961
|
const details = [];
|
|
3373
|
-
const envOpPath =
|
|
3374
|
-
if (!
|
|
3962
|
+
const envOpPath = join13(ctx.repoRoot, ".env.op");
|
|
3963
|
+
if (!existsSync10(envOpPath)) {
|
|
3375
3964
|
changedFiles.push(envOpPath);
|
|
3376
|
-
if (!ctx.dryRun) writeText(envOpPath, readText(
|
|
3965
|
+
if (!ctx.dryRun) writeText(envOpPath, readText(join13(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
|
|
3377
3966
|
}
|
|
3378
|
-
const gitignorePath =
|
|
3967
|
+
const gitignorePath = join13(ctx.repoRoot, ".gitignore");
|
|
3379
3968
|
const gitignore = safeReadText(gitignorePath) ?? "";
|
|
3380
3969
|
const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
|
|
3381
3970
|
# NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
|
|
@@ -3402,7 +3991,7 @@ var RULES = [
|
|
|
3402
3991
|
title: ".copier-answers.yml provenance + drift report",
|
|
3403
3992
|
audit: (ctx) => {
|
|
3404
3993
|
const details = [];
|
|
3405
|
-
const path =
|
|
3994
|
+
const path = join13(ctx.repoRoot, ".copier-answers.yml");
|
|
3406
3995
|
const text3 = safeReadText(path);
|
|
3407
3996
|
const project = readProjectJson(ctx);
|
|
3408
3997
|
if (!text3) {
|
|
@@ -3433,12 +4022,12 @@ var RULES = [
|
|
|
3433
4022
|
const changedFiles = [];
|
|
3434
4023
|
const project = canonicalProjectJson(ctx);
|
|
3435
4024
|
const text3 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
|
|
3436
|
-
_src_path: ${
|
|
4025
|
+
_src_path: ${join13(ctx.pjanglerRoot, "templates", "commonproject")}
|
|
3437
4026
|
project_description: ${String(project.project_description)}
|
|
3438
4027
|
project_name: ${String(project.project_name)}
|
|
3439
4028
|
ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
3440
4029
|
`;
|
|
3441
|
-
const path =
|
|
4030
|
+
const path = join13(ctx.repoRoot, ".copier-answers.yml");
|
|
3442
4031
|
if (safeReadText(path) !== text3) {
|
|
3443
4032
|
changedFiles.push(path);
|
|
3444
4033
|
if (!ctx.dryRun) writeText(path, text3);
|
|
@@ -3457,14 +4046,14 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3457
4046
|
id: "bmad.scaffold",
|
|
3458
4047
|
title: "BMAD modules/docs scaffold",
|
|
3459
4048
|
audit: (ctx) => {
|
|
3460
|
-
const targetRoot =
|
|
4049
|
+
const targetRoot = join13(ctx.repoRoot, "_bmad");
|
|
3461
4050
|
const sentinels = [
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
4051
|
+
join13("core", "config.yaml"),
|
|
4052
|
+
join13("config.toml"),
|
|
4053
|
+
join13("_config", "manifest.yaml"),
|
|
4054
|
+
join13("bmm", "config.yaml")
|
|
3466
4055
|
];
|
|
3467
|
-
const missing = sentinels.filter((file) => !
|
|
4056
|
+
const missing = sentinels.filter((file) => !existsSync10(join13(targetRoot, file)));
|
|
3468
4057
|
return {
|
|
3469
4058
|
id: "bmad.scaffold",
|
|
3470
4059
|
title: "BMAD modules/docs scaffold",
|
|
@@ -3478,7 +4067,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3478
4067
|
const changedFiles = [];
|
|
3479
4068
|
if (ctx.dryRun) {
|
|
3480
4069
|
for (const detail of finding.details) {
|
|
3481
|
-
changedFiles.push(
|
|
4070
|
+
changedFiles.push(join13(ctx.repoRoot, detail));
|
|
3482
4071
|
}
|
|
3483
4072
|
return {
|
|
3484
4073
|
id: finding.id,
|
|
@@ -3503,8 +4092,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3503
4092
|
};
|
|
3504
4093
|
}
|
|
3505
4094
|
for (const detail of finding.details) {
|
|
3506
|
-
if (
|
|
3507
|
-
changedFiles.push(
|
|
4095
|
+
if (existsSync10(join13(ctx.repoRoot, detail))) {
|
|
4096
|
+
changedFiles.push(join13(ctx.repoRoot, detail));
|
|
3508
4097
|
}
|
|
3509
4098
|
}
|
|
3510
4099
|
return {
|
|
@@ -3527,7 +4116,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3527
4116
|
id: "bmad.version",
|
|
3528
4117
|
title: "BMAD version currency",
|
|
3529
4118
|
status: "skip",
|
|
3530
|
-
summary:
|
|
4119
|
+
summary: existsSync10(join13(ctx.repoRoot, "_bmad")) ? "BMAD installed but version manifest unreadable" : "No BMAD install present",
|
|
3531
4120
|
details: [],
|
|
3532
4121
|
fixable: false
|
|
3533
4122
|
};
|
|
@@ -3582,7 +4171,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3582
4171
|
}
|
|
3583
4172
|
const installed = readInstalledBmadVersion(ctx.repoRoot);
|
|
3584
4173
|
const available = resolveBmadDistTags(ctx.homeDir)?.distTags?.[BMAD_TARGET_CHANNEL];
|
|
3585
|
-
const manifestPath =
|
|
4174
|
+
const manifestPath = join13(ctx.repoRoot, "_bmad", "_config", "manifest.yaml");
|
|
3586
4175
|
if (ctx.dryRun) {
|
|
3587
4176
|
return {
|
|
3588
4177
|
id: finding.id,
|
|
@@ -3629,11 +4218,11 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3629
4218
|
}
|
|
3630
4219
|
const details = [];
|
|
3631
4220
|
for (const rel of ["role.yaml", "SOUL.md", "hermes", ".gitignore", ".scripts/70-systemd.sh", ".scripts/heartbeat.sh", ".scripts/checkpoint.sh", ".runtime-scaffold/README.md", "runtime/memories/MEMORY.md", "runtime/bloodbank-consumer.py"]) {
|
|
3632
|
-
if (!
|
|
4221
|
+
if (!existsSync10(join13(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join13(role.roleDir, rel))}`);
|
|
3633
4222
|
}
|
|
3634
|
-
const gitmodules = safeReadText(
|
|
4223
|
+
const gitmodules = safeReadText(join13(ctx.repoRoot, ".gitmodules")) ?? "";
|
|
3635
4224
|
if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
|
|
3636
|
-
if (!profileMetaInheritsDefault(
|
|
4225
|
+
if (!profileMetaInheritsDefault(join13(role.roleDir, "runtime", "profile.yaml"))) {
|
|
3637
4226
|
details.push("runtime/profile.yaml missing inherited default config metadata");
|
|
3638
4227
|
}
|
|
3639
4228
|
const registry = safeReadText(registryPath(ctx.homeDir));
|
|
@@ -3654,21 +4243,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3654
4243
|
if (!role) {
|
|
3655
4244
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
|
|
3656
4245
|
}
|
|
3657
|
-
const templateRoleDir =
|
|
3658
|
-
writeIfDifferent(
|
|
3659
|
-
writeIfDifferent(
|
|
3660
|
-
writeIfDifferent(
|
|
3661
|
-
copyMissingRecursive(
|
|
3662
|
-
copyMissingRecursive(
|
|
3663
|
-
copyMissingRecursive(
|
|
3664
|
-
const promptSource =
|
|
3665
|
-
const promptTarget =
|
|
3666
|
-
if (
|
|
4246
|
+
const templateRoleDir = join13(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
|
|
4247
|
+
writeIfDifferent(join13(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
|
|
4248
|
+
writeIfDifferent(join13(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
|
|
4249
|
+
writeIfDifferent(join13(role.roleDir, ".gitignore"), readText(join13(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
|
|
4250
|
+
copyMissingRecursive(join13(templateRoleDir, ".runtime-scaffold"), join13(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
|
|
4251
|
+
copyMissingRecursive(join13(templateRoleDir, ".runtime-scaffold"), join13(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
|
|
4252
|
+
copyMissingRecursive(join13(templateRoleDir, ".scripts"), join13(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
|
|
4253
|
+
const promptSource = join13(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
|
|
4254
|
+
const promptTarget = join13(role.roleDir, ".scripts", "sentinel.prompt.md");
|
|
4255
|
+
if (existsSync10(promptSource) && !existsSync10(promptTarget)) {
|
|
3667
4256
|
const prompt = readText(promptSource).replace(/\{\{ agent_id \}\}/g, role.agentId).replace(/\{\{ role \}\}/g, role.role).replace(/\{\{ target_repo \}\}/g, role.repo).replace(/\{\{ display_name \}\}/g, role.displayName || role.agentId);
|
|
3668
4257
|
writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
|
|
3669
4258
|
}
|
|
3670
4259
|
upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
|
|
3671
|
-
const profileMetaUpdated = upsertInheritedProfileMeta(
|
|
4260
|
+
const profileMetaUpdated = upsertInheritedProfileMeta(join13(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
|
|
3672
4261
|
if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
|
|
3673
4262
|
const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
|
|
3674
4263
|
if (registryUpdated) details.push(`updated ${registryUpdated}`);
|
|
@@ -3700,7 +4289,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3700
4289
|
const details = [];
|
|
3701
4290
|
for (const role of roles) {
|
|
3702
4291
|
const roleRelDir = relative(ctx.repoRoot, role.roleDir);
|
|
3703
|
-
const runtimeRelPath =
|
|
4292
|
+
const runtimeRelPath = join13(roleRelDir, "runtime");
|
|
3704
4293
|
const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
|
|
3705
4294
|
cwd: ctx.repoRoot,
|
|
3706
4295
|
encoding: "utf8"
|
|
@@ -3708,8 +4297,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3708
4297
|
if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
|
|
3709
4298
|
details.push(`submodule runtime is tracked in Git index at ${runtimeRelPath}`);
|
|
3710
4299
|
}
|
|
3711
|
-
const gitignorePath =
|
|
3712
|
-
if (
|
|
4300
|
+
const gitignorePath = join13(role.roleDir, ".gitignore");
|
|
4301
|
+
if (existsSync10(gitignorePath)) {
|
|
3713
4302
|
const content = safeReadText(gitignorePath) ?? "";
|
|
3714
4303
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
3715
4304
|
if (!lines.includes("runtime/") && !lines.includes("runtime")) {
|
|
@@ -3734,7 +4323,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3734
4323
|
const details = [];
|
|
3735
4324
|
for (const role of roles) {
|
|
3736
4325
|
const roleRelDir = relative(ctx.repoRoot, role.roleDir);
|
|
3737
|
-
const runtimeRelPath =
|
|
4326
|
+
const runtimeRelPath = join13(roleRelDir, "runtime");
|
|
3738
4327
|
const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
|
|
3739
4328
|
cwd: ctx.repoRoot,
|
|
3740
4329
|
encoding: "utf8"
|
|
@@ -3749,10 +4338,10 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3749
4338
|
});
|
|
3750
4339
|
}
|
|
3751
4340
|
}
|
|
3752
|
-
const gitignorePath =
|
|
4341
|
+
const gitignorePath = join13(role.roleDir, ".gitignore");
|
|
3753
4342
|
let content = "";
|
|
3754
4343
|
let isIgnored = false;
|
|
3755
|
-
if (
|
|
4344
|
+
if (existsSync10(gitignorePath)) {
|
|
3756
4345
|
content = safeReadText(gitignorePath) ?? "";
|
|
3757
4346
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
3758
4347
|
isIgnored = lines.includes("runtime/") || lines.includes("runtime");
|
|
@@ -3819,9 +4408,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3819
4408
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
|
|
3820
4409
|
}
|
|
3821
4410
|
for (const role of roles) {
|
|
3822
|
-
const sysDir =
|
|
4411
|
+
const sysDir = join13(ctx.homeDir, ".config", "systemd", "user");
|
|
3823
4412
|
const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`];
|
|
3824
|
-
const allUnitsPresent = units.every((unit) =>
|
|
4413
|
+
const allUnitsPresent = units.every((unit) => existsSync10(join13(sysDir, unit)));
|
|
3825
4414
|
if (allUnitsPresent) {
|
|
3826
4415
|
if (ctx.dryRun) {
|
|
3827
4416
|
details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
|
|
@@ -3833,8 +4422,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3833
4422
|
}
|
|
3834
4423
|
continue;
|
|
3835
4424
|
}
|
|
3836
|
-
for (const script of [
|
|
3837
|
-
if (!script || !
|
|
4425
|
+
for (const script of [join13(role.roleDir, ".scripts", "70-systemd.sh")]) {
|
|
4426
|
+
if (!script || !existsSync10(script)) continue;
|
|
3838
4427
|
if (ctx.dryRun) {
|
|
3839
4428
|
details.push(`would run: bash ${script}`);
|
|
3840
4429
|
} else {
|
|
@@ -3861,7 +4450,7 @@ function writeIfDifferent(path, content, dryRun, changedFiles, mode) {
|
|
|
3861
4450
|
changedFiles.push(path);
|
|
3862
4451
|
if (!dryRun) {
|
|
3863
4452
|
writeText(path, normalized);
|
|
3864
|
-
if (mode)
|
|
4453
|
+
if (mode) chmodSync4(path, mode);
|
|
3865
4454
|
}
|
|
3866
4455
|
}
|
|
3867
4456
|
function getParityRuleIds() {
|
|
@@ -3973,15 +4562,15 @@ function formatMigrationReport(report) {
|
|
|
3973
4562
|
}
|
|
3974
4563
|
|
|
3975
4564
|
// src/utils/version.ts
|
|
3976
|
-
import { readFileSync as
|
|
3977
|
-
import { dirname as dirname7, join as
|
|
4565
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
4566
|
+
import { dirname as dirname7, join as join14 } from "node:path";
|
|
3978
4567
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
3979
4568
|
var PJANGLER_VERSION = (() => {
|
|
3980
4569
|
try {
|
|
3981
4570
|
let dir = dirname7(fileURLToPath4(import.meta.url));
|
|
3982
4571
|
for (let i = 0; i < 4; i++) {
|
|
3983
4572
|
try {
|
|
3984
|
-
const raw =
|
|
4573
|
+
const raw = readFileSync7(join14(dir, "package.json"), "utf8");
|
|
3985
4574
|
return JSON.parse(raw).version ?? "0.0.0";
|
|
3986
4575
|
} catch {
|
|
3987
4576
|
const parent = dirname7(dir);
|
|
@@ -4024,9 +4613,9 @@ async function promptForRuleIds(rules) {
|
|
|
4024
4613
|
return selected;
|
|
4025
4614
|
}
|
|
4026
4615
|
function readJson(path) {
|
|
4027
|
-
if (!
|
|
4616
|
+
if (!existsSync11(path)) return void 0;
|
|
4028
4617
|
try {
|
|
4029
|
-
const parsed = JSON.parse(
|
|
4618
|
+
const parsed = JSON.parse(readFileSync8(path, "utf8"));
|
|
4030
4619
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
4031
4620
|
} catch {
|
|
4032
4621
|
return void 0;
|
|
@@ -4043,8 +4632,8 @@ function packageNameToProjectName(value) {
|
|
|
4043
4632
|
return name.replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()).trim();
|
|
4044
4633
|
}
|
|
4045
4634
|
function deriveProjectDefaults(targetDir) {
|
|
4046
|
-
const manifest = readJson(
|
|
4047
|
-
const pkg = readJson(
|
|
4635
|
+
const manifest = readJson(join15(targetDir, ".project.json"));
|
|
4636
|
+
const pkg = readJson(join15(targetDir, "package.json"));
|
|
4048
4637
|
const name = String(manifest?.project_name ?? "").trim() || packageNameToProjectName(typeof pkg?.name === "string" ? pkg.name : void 0) || packageNameToProjectName(basename4(targetDir)) || "Project";
|
|
4049
4638
|
const ticketProvider = manifest?.ticket_provider && typeof manifest.ticket_provider === "object" ? manifest.ticket_provider : {};
|
|
4050
4639
|
return {
|
|
@@ -4100,7 +4689,7 @@ function actionNeedsRun(plan, kind, syncMode) {
|
|
|
4100
4689
|
if (!action || action.kind !== "project.write-manifest") return false;
|
|
4101
4690
|
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
4102
4691
|
`;
|
|
4103
|
-
return !
|
|
4692
|
+
return !existsSync11(action.path) || readFileSync8(action.path, "utf8") !== next;
|
|
4104
4693
|
}
|
|
4105
4694
|
if (kind === "copier.copy.commonproject") return true;
|
|
4106
4695
|
if (kind === "ticket-provider.create-or-link") return plan.actions.some((action) => action.kind === kind && action.enabled);
|
|
@@ -4155,7 +4744,7 @@ async function resolveProjectInitTarget(name, options) {
|
|
|
4155
4744
|
if (!targetDir && interactive) {
|
|
4156
4745
|
const defaultName = name ?? basename4(cwd);
|
|
4157
4746
|
const promptedName = name ?? await promptTextValue("Project name", packageNameToProjectName(defaultName));
|
|
4158
|
-
const defaultDir =
|
|
4747
|
+
const defaultDir = join15(cwd, promptedName.replace(/[^A-Za-z0-9._-]/g, "") || promptedName.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
|
|
4159
4748
|
targetDir = await promptTextValue("Project directory", defaultDir);
|
|
4160
4749
|
name = promptedName;
|
|
4161
4750
|
}
|
|
@@ -4163,7 +4752,7 @@ async function resolveProjectInitTarget(name, options) {
|
|
|
4163
4752
|
if (!name) throw new Error("Project name or --target-dir is required when project init is not run inside a git repo");
|
|
4164
4753
|
targetDir = resolve3(process.cwd(), name.replace(/[^A-Za-z0-9._-]/g, "") || name.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
|
|
4165
4754
|
}
|
|
4166
|
-
const targetExists =
|
|
4755
|
+
const targetExists = existsSync11(targetDir);
|
|
4167
4756
|
if (targetExists && !statSync2(targetDir).isDirectory()) throw new Error(`Target path is not a directory: ${targetDir}`);
|
|
4168
4757
|
const targetGitRoot = targetExists ? findGitRoot(targetDir) : void 0;
|
|
4169
4758
|
const syncMode = Boolean(targetGitRoot && resolve3(targetGitRoot) === resolve3(targetDir));
|
|
@@ -4298,7 +4887,7 @@ async function runProjectInit(name, options) {
|
|
|
4298
4887
|
}
|
|
4299
4888
|
return;
|
|
4300
4889
|
}
|
|
4301
|
-
const initResult = selectedPlan.actions.length ? executeProjectInitPlan(selectedPlan) : { ok: true, plan: selectedPlan, logs: [], errors: [], changedFiles: [] };
|
|
4890
|
+
const initResult = selectedPlan.actions.length ? await executeProjectInitPlan(selectedPlan) : { ok: true, plan: selectedPlan, logs: [], errors: [], changedFiles: [] };
|
|
4302
4891
|
const migrationReport = selection.selectedParityRules.length ? runMigrationForRules(selection.selectedParityRules, target.targetDir, false) : void 0;
|
|
4303
4892
|
const migrationErrors = migrationReport?.results.filter((result2) => result2.status === "blocked").map((result2) => `${result2.id}: ${result2.summary}`) ?? [];
|
|
4304
4893
|
const changedFiles = Array.from(/* @__PURE__ */ new Set([
|