@delorenj/pjangler 1.2.21 → 1.2.25
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 +1852 -368
- package/dist/mcp-server.js +1782 -296
- package/package.json +14 -3
- package/templates/commonproject/copier.yml +3 -0
- package/templates/commonproject/template/.mise/scripts/provision-bmad-skills.py +480 -0
- package/templates/commonproject/template/.mise/scripts/sync-skills.py +449 -0
- package/templates/commonproject/template/mise.toml.jinja +9 -2
- package/templates/commonproject/tests/test_provision_bmad_skills.py +203 -0
- package/templates/commonproject/tests/test_sync_skills_topology.py +120 -0
- package/templates/hermes-agent/README.md +70 -29
- package/templates/hermes-agent/copier.yml +9 -6
- package/templates/hermes-agent/docs/architecture.md +75 -51
- package/templates/hermes-agent/docs/fleet-control-plane/architecture.md +11 -6
- package/templates/hermes-agent/docs/fleet-control-plane/epics-and-stories.md +2 -2
- package/templates/hermes-agent/docs/fleet-control-plane/prd.md +2 -3
- package/templates/hermes-agent/docs/operations.md +179 -81
- package/templates/hermes-agent/docs/runbooks/runtime-checkpoint-repair.md +31 -171
- package/templates/hermes-agent/docs/sentinel/README.md +10 -10
- package/templates/hermes-agent/docs/sentinel/architecture.md +6 -8
- package/templates/hermes-agent/docs/sentinel/development.md +12 -15
- package/templates/hermes-agent/docs/sentinel.md +1 -1
- package/templates/hermes-agent/install-local.sh +41 -4
- package/templates/hermes-agent/runtime-scaffold/README.md +4 -2
- package/templates/hermes-agent/scripts/degit-runtime.sh +192 -0
- package/templates/hermes-agent/scripts/fleet-prune-debris.sh +128 -0
- package/templates/hermes-agent/scripts/fleet-sync.sh +164 -7
- package/templates/hermes-agent/scripts/hermes-runtime-templatize.py +321 -0
- package/templates/hermes-agent/scripts/migrate-profile-scoped-chat-secrets.py +136 -0
- package/templates/hermes-agent/scripts/momo-unify-agent.py +224 -0
- package/templates/hermes-agent/scripts/repair-runtime-checkpoint.sh +7 -119
- package/templates/hermes-agent/template/.gitignore.jinja +5 -2
- package/templates/hermes-agent/template/.runtime-scaffold/README.md +4 -2
- package/templates/hermes-agent/template/.scripts/00-banner.sh +1 -1
- package/templates/hermes-agent/template/.scripts/01-config.sh +3 -0
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +12 -1
- package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +55 -129
- package/templates/hermes-agent/template/.scripts/30-telegram.sh +276 -22
- package/templates/hermes-agent/template/.scripts/31-slack.sh +362 -0
- package/templates/hermes-agent/template/.scripts/60-bloodbank.sh +8 -66
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +44 -30
- package/templates/hermes-agent/template/.scripts/80-registry.sh +93 -13
- package/templates/hermes-agent/template/.scripts/99-summary.sh +1 -4
- package/templates/hermes-agent/template/.scripts/_lib.sh +66 -1
- package/templates/hermes-agent/template/.scripts/checkpoint.sh +3 -2
- package/templates/hermes-agent/template/.scripts/config.example.toml +10 -10
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +1 -1
- package/templates/hermes-agent/template/SOUL.md.jinja +2 -2
- package/templates/hermes-agent/template/hermes.jinja +6 -3
- package/templates/hermes-agent/template/role.yaml.jinja +24 -13
- package/templates/hermes-agent/tests/test_bloodbank_consumer_contract.py +429 -130
- package/templates/hermes-agent/tests/test_fleet_upgrade_contract.py +176 -0
- package/templates/hermes-agent/tests/test_launcher_chat_secret_isolation.py +85 -0
- package/templates/hermes-agent/tests/test_local_runtime_provisioning.py +130 -0
- package/templates/hermes-agent/tests/test_operator_docs_local_runtime_contract.py +132 -0
- package/templates/hermes-agent/tests/test_profile_env_detach_contract.py +14 -0
- package/templates/hermes-agent/tests/test_profile_scoped_chat_secrets.py +89 -0
- package/templates/hermes-agent/tests/test_repository_privacy_contract.py +32 -0
- package/templates/hermes-agent/tests/test_slack_provisioning.py +344 -0
- package/templates/hermes-agent/tests/test_telegram_provisioning.py +475 -0
- package/templates/hermes-agent/.codegraph/daemon.pid +0 -6
- package/templates/hermes-agent/.omo/run-continuation/ses_0e5b28303ffeXxL53hZjKggXDW.json +0 -10
- package/templates/hermes-agent/runtime-scaffold/bloodbank-consumer.py +0 -181
- package/templates/hermes-agent/template/.runtime-scaffold/bloodbank-consumer.py +0 -181
package/dist/mcp-server.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/mcp-server.ts
|
|
4
4
|
import { existsSync as existsSync11, statSync as statSync2 } from "node:fs";
|
|
5
|
-
import { basename as
|
|
5
|
+
import { basename as basename5, dirname as dirname8, join as join16, resolve as resolve4 } from "node:path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
7
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -521,8 +521,8 @@ ${envBlock}`);
|
|
|
521
521
|
const enterRe = /(enter\s*=\s*\[[\s\S]*?)(\n[ \t]*\])/;
|
|
522
522
|
if (enterRe.test(content)) {
|
|
523
523
|
content = content.replace(enterRe, (_m, head, close) => {
|
|
524
|
-
const
|
|
525
|
-
return `${head}${
|
|
524
|
+
const sep2 = /[,[]\s*$/.test(head) ? "" : ",";
|
|
525
|
+
return `${head}${sep2}
|
|
526
526
|
${enterAdds}${close}`;
|
|
527
527
|
});
|
|
528
528
|
} else {
|
|
@@ -792,19 +792,151 @@ var NodeRecipe = class extends Recipe {
|
|
|
792
792
|
// src/commands/hermes/EnsureTemplateConfig.ts
|
|
793
793
|
import { homedir, platform } from "node:os";
|
|
794
794
|
import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
795
|
-
import { join as
|
|
795
|
+
import { join as join6, dirname as dirname2 } from "node:path";
|
|
796
|
+
|
|
797
|
+
// src/parity/bmadPack.ts
|
|
798
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync as readFileSync2, readdirSync } from "node:fs";
|
|
799
|
+
import { createHash } from "node:crypto";
|
|
800
|
+
import { basename, join as join5, relative, resolve, sep } from "node:path";
|
|
801
|
+
var BMAD_PACK_VERSION = "6.10.1-next.31";
|
|
802
|
+
var BMAD_PACK_CHECKSUMS_SHA256 = "a8bc005612ac60e3ec775fff5a11eafe38be6acdae96efa3d770b48322cb3224";
|
|
803
|
+
var BMAD_PACK_SKILL_COUNT = 76;
|
|
804
|
+
var BMAD_PACK_PAYLOAD_FILES = 1072;
|
|
805
|
+
function sha256(content) {
|
|
806
|
+
return createHash("sha256").update(content).digest("hex");
|
|
807
|
+
}
|
|
808
|
+
function readRegularFile(path) {
|
|
809
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
810
|
+
try {
|
|
811
|
+
if (!fstatSync(fd).isFile()) throw new Error(`BMAD pack entry is not a regular file: ${path}`);
|
|
812
|
+
return readFileSync2(fd);
|
|
813
|
+
} finally {
|
|
814
|
+
closeSync(fd);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
function safeRelativePath(value) {
|
|
818
|
+
if (!value || value.includes("\\") || value.startsWith("/") || value.split("/").some((part) => part === "" || part === "." || part === "..")) {
|
|
819
|
+
throw new Error(`Unsafe BMAD checksum path: ${JSON.stringify(value)}`);
|
|
820
|
+
}
|
|
821
|
+
return value;
|
|
822
|
+
}
|
|
823
|
+
function parsePackToml(content) {
|
|
824
|
+
const required = [
|
|
825
|
+
/^name = "bmad"$/m,
|
|
826
|
+
new RegExp(`^version = "${BMAD_PACK_VERSION.replaceAll(".", "\\.")}"$`, "m"),
|
|
827
|
+
/^upstream = "bmad-method"$/m,
|
|
828
|
+
new RegExp(`^upstream_version = "${BMAD_PACK_VERSION.replaceAll(".", "\\.")}"$`, "m"),
|
|
829
|
+
/^rendered_from = "\.agent\/skills"$/m,
|
|
830
|
+
/^immutable = true$/m,
|
|
831
|
+
/^project_projection = "symlink"$/m
|
|
832
|
+
];
|
|
833
|
+
if (required.some((pattern) => !pattern.test(content))) {
|
|
834
|
+
throw new Error(`BMAD pack.toml does not declare the trusted ${BMAD_PACK_VERSION} contract`);
|
|
835
|
+
}
|
|
836
|
+
const payloadMatch = content.match(/^payload_files = (\d+)$/m);
|
|
837
|
+
const skillsMatch = content.match(/^skills = \[\n([\s\S]*?)^\]$/m);
|
|
838
|
+
if (!payloadMatch || !skillsMatch) throw new Error("BMAD pack.toml is missing payload inventory metadata");
|
|
839
|
+
const skillNames = [...skillsMatch[1].matchAll(/^\s+"([^"]+)",$/gm)].map((match) => match[1]);
|
|
840
|
+
const payloadFiles = Number.parseInt(payloadMatch[1], 10);
|
|
841
|
+
if (skillNames.length !== BMAD_PACK_SKILL_COUNT || new Set(skillNames).size !== skillNames.length) {
|
|
842
|
+
throw new Error(`BMAD pack.toml must declare exactly ${BMAD_PACK_SKILL_COUNT} unique skills`);
|
|
843
|
+
}
|
|
844
|
+
if (payloadFiles !== BMAD_PACK_PAYLOAD_FILES) {
|
|
845
|
+
throw new Error(`BMAD pack.toml must declare exactly ${BMAD_PACK_PAYLOAD_FILES} payload files`);
|
|
846
|
+
}
|
|
847
|
+
for (const name of skillNames) {
|
|
848
|
+
if (!name.startsWith("bmad-") || basename(name) !== name) throw new Error(`Unsafe BMAD skill identity: ${name}`);
|
|
849
|
+
}
|
|
850
|
+
return { skillNames, payloadFiles };
|
|
851
|
+
}
|
|
852
|
+
function walkRegularTree(root) {
|
|
853
|
+
const files = /* @__PURE__ */ new Map();
|
|
854
|
+
const directories = /* @__PURE__ */ new Set();
|
|
855
|
+
const visit = (directory) => {
|
|
856
|
+
for (const name of readdirSync(directory).sort()) {
|
|
857
|
+
const path = join5(directory, name);
|
|
858
|
+
const stat = lstatSync(path);
|
|
859
|
+
if (stat.isSymbolicLink()) throw new Error(`BMAD pack may not contain symlinks: ${path}`);
|
|
860
|
+
if (stat.isDirectory()) {
|
|
861
|
+
directories.add(relative(root, path).split(sep).join("/"));
|
|
862
|
+
visit(path);
|
|
863
|
+
} else if (stat.isFile()) files.set(relative(root, path).split(sep).join("/"), readRegularFile(path));
|
|
864
|
+
else throw new Error(`BMAD pack may contain only regular files/directories: ${path}`);
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
visit(root);
|
|
868
|
+
return { files, directories };
|
|
869
|
+
}
|
|
870
|
+
function validateTrustedBmadPack(packRoot) {
|
|
871
|
+
const root = resolve(packRoot);
|
|
872
|
+
const rootStat = lstatSync(root);
|
|
873
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
874
|
+
throw new Error(`BMAD pack root must be a real directory: ${root}`);
|
|
875
|
+
}
|
|
876
|
+
const checksumsContent = readRegularFile(join5(root, "SHA256SUMS"));
|
|
877
|
+
if (sha256(checksumsContent) !== BMAD_PACK_CHECKSUMS_SHA256) {
|
|
878
|
+
throw new Error(`BMAD pack checksum manifest is not the trusted ${BMAD_PACK_VERSION} manifest`);
|
|
879
|
+
}
|
|
880
|
+
const expected = /* @__PURE__ */ new Map();
|
|
881
|
+
for (const line of checksumsContent.toString("utf8").split("\n")) {
|
|
882
|
+
if (!line) continue;
|
|
883
|
+
const match = line.match(/^([0-9a-f]{64}) (.+)$/);
|
|
884
|
+
if (!match) throw new Error(`Invalid BMAD SHA256SUMS entry: ${line}`);
|
|
885
|
+
const path = safeRelativePath(match[2]);
|
|
886
|
+
if (expected.has(path)) throw new Error(`Duplicate BMAD SHA256SUMS entry: ${path}`);
|
|
887
|
+
expected.set(path, match[1]);
|
|
888
|
+
}
|
|
889
|
+
const walked = walkRegularTree(root);
|
|
890
|
+
const actual = walked.files;
|
|
891
|
+
actual.delete("SHA256SUMS");
|
|
892
|
+
const missing = [...expected.keys()].filter((path) => !actual.has(path));
|
|
893
|
+
const extra = [...actual.keys()].filter((path) => !expected.has(path));
|
|
894
|
+
if (missing.length || extra.length) {
|
|
895
|
+
throw new Error(`BMAD checksum coverage mismatch; missing=${JSON.stringify(missing)} extra=${JSON.stringify(extra)}`);
|
|
896
|
+
}
|
|
897
|
+
for (const [path, digest] of expected) {
|
|
898
|
+
const content = actual.get(path);
|
|
899
|
+
if (!content || sha256(content) !== digest) throw new Error(`BMAD pack digest mismatch: ${path}`);
|
|
900
|
+
}
|
|
901
|
+
for (const directory of walked.directories) {
|
|
902
|
+
if (![...actual.keys()].some((path) => path.startsWith(`${directory}/`))) {
|
|
903
|
+
throw new Error(`BMAD pack contains an unauthenticated empty directory: ${directory}`);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
const packToml = actual.get("pack.toml");
|
|
907
|
+
if (!packToml) throw new Error("BMAD pack.toml is missing");
|
|
908
|
+
const metadata = parsePackToml(packToml.toString("utf8"));
|
|
909
|
+
const skillSet = new Set(metadata.skillNames);
|
|
910
|
+
const topLevelDirectories = readdirSync(root).filter((name) => lstatSync(join5(root, name)).isDirectory()).sort();
|
|
911
|
+
if (topLevelDirectories.length !== skillSet.size || topLevelDirectories.some((name) => !skillSet.has(name))) {
|
|
912
|
+
throw new Error("BMAD pack directory inventory differs from authenticated pack.toml skills");
|
|
913
|
+
}
|
|
914
|
+
for (const name of metadata.skillNames) {
|
|
915
|
+
const skillMd = join5(root, name, "SKILL.md");
|
|
916
|
+
if (!lstatSync(skillMd).isFile() || lstatSync(skillMd).isSymbolicLink()) {
|
|
917
|
+
throw new Error(`BMAD skill is missing a regular SKILL.md: ${name}`);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
const payloadCount = [...actual.keys()].filter((path) => skillSet.has(path.split("/", 1)[0])).length;
|
|
921
|
+
if (payloadCount !== metadata.payloadFiles) {
|
|
922
|
+
throw new Error(`BMAD payload inventory mismatch: ${payloadCount} != ${metadata.payloadFiles}`);
|
|
923
|
+
}
|
|
924
|
+
return { root, skillNames: metadata.skillNames, payloadFiles: metadata.payloadFiles };
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// src/commands/hermes/EnsureTemplateConfig.ts
|
|
796
928
|
function resolveTemplateConfigPath() {
|
|
797
929
|
const fromEnv = process.env.HERMES_TEMPLATE_CONFIG;
|
|
798
930
|
if (fromEnv && fromEnv.trim()) return fromEnv.trim();
|
|
799
931
|
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
800
|
-
const base = xdg && xdg.length ? xdg :
|
|
801
|
-
return
|
|
932
|
+
const base = xdg && xdg.length ? xdg : join6(homedir(), ".config");
|
|
933
|
+
return join6(base, "hermes-agent-template", "config.toml");
|
|
802
934
|
}
|
|
803
935
|
function detectHermesBin(home) {
|
|
804
936
|
const candidates = [
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
937
|
+
join6(home, "code", "hermes-agent", "venv", "bin", "hermes"),
|
|
938
|
+
join6(home, "code", "hermes-agent", ".venv", "bin", "hermes"),
|
|
939
|
+
join6(home, ".local", "bin", "hermes")
|
|
808
940
|
];
|
|
809
941
|
for (const c of candidates) {
|
|
810
942
|
if (existsSync3(c)) return c;
|
|
@@ -814,11 +946,11 @@ function detectHermesBin(home) {
|
|
|
814
946
|
function renderHostConfig() {
|
|
815
947
|
const home = homedir();
|
|
816
948
|
const hermesBin = detectHermesBin(home);
|
|
817
|
-
const hermesRepo =
|
|
818
|
-
const scaffoldDir =
|
|
819
|
-
const skillsDir =
|
|
820
|
-
const pmExternalSkillGlobalDir =
|
|
821
|
-
const pmExternalSkillBmadDir =
|
|
949
|
+
const hermesRepo = join6(home, "code", "hermes-agent");
|
|
950
|
+
const scaffoldDir = join6(home, "code", "hermes-agent-template", "runtime-scaffold");
|
|
951
|
+
const skillsDir = join6(home, ".agents", "skills");
|
|
952
|
+
const pmExternalSkillGlobalDir = join6(home, "code", "skillex", "skill-sets", "global", ".system");
|
|
953
|
+
const pmExternalSkillBmadDir = join6(home, "code", "skillex", "packs", "bmad", BMAD_PACK_VERSION);
|
|
822
954
|
return `# hermes-agent-template \u2014 host configuration
|
|
823
955
|
# Bootstrapped by \`pjangler config bootstrap\` for $HOME=${home} (platform=${platform()}).
|
|
824
956
|
#
|
|
@@ -890,8 +1022,8 @@ var EnsureTemplateConfig = class extends Command {
|
|
|
890
1022
|
};
|
|
891
1023
|
|
|
892
1024
|
// src/commands/hermes/PromptForAgentConfig.ts
|
|
893
|
-
import { basename, join as
|
|
894
|
-
import { readFileSync as
|
|
1025
|
+
import { basename as basename2, join as join7 } from "node:path";
|
|
1026
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
895
1027
|
import * as p from "@clack/prompts";
|
|
896
1028
|
|
|
897
1029
|
// src/commands/hermes/types.ts
|
|
@@ -906,7 +1038,7 @@ function deriveProfileName(repo, role) {
|
|
|
906
1038
|
// src/commands/hermes/PromptForAgentConfig.ts
|
|
907
1039
|
function detectTicketProvider(targetDir) {
|
|
908
1040
|
try {
|
|
909
|
-
const t = JSON.parse(
|
|
1041
|
+
const t = JSON.parse(readFileSync3(join7(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
|
|
910
1042
|
return t === "plane" || t === "trello" ? t : void 0;
|
|
911
1043
|
} catch {
|
|
912
1044
|
return void 0;
|
|
@@ -915,7 +1047,7 @@ function detectTicketProvider(targetDir) {
|
|
|
915
1047
|
var PromptForAgentConfig = class extends Command {
|
|
916
1048
|
async invoke() {
|
|
917
1049
|
const ctx = this.context;
|
|
918
|
-
const defaultRepo =
|
|
1050
|
+
const defaultRepo = basename2(ctx.targetDir).toLowerCase();
|
|
919
1051
|
ctx.targetRepo = (ctx.targetRepo ?? defaultRepo).toLowerCase();
|
|
920
1052
|
ctx.role ??= "pm";
|
|
921
1053
|
ctx.agentPurpose ??= `${ctx.role} agent for ${ctx.targetRepo}`;
|
|
@@ -964,7 +1096,7 @@ var PromptForAgentConfig = class extends Command {
|
|
|
964
1096
|
// src/commands/hermes/RunCopierTemplate.ts
|
|
965
1097
|
import { spawnSync } from "node:child_process";
|
|
966
1098
|
import { homedir as homedir2 } from "node:os";
|
|
967
|
-
import { join as
|
|
1099
|
+
import { join as join8, dirname as dirname3 } from "node:path";
|
|
968
1100
|
import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "node:fs";
|
|
969
1101
|
import { fileURLToPath } from "node:url";
|
|
970
1102
|
import * as p2 from "@clack/prompts";
|
|
@@ -976,8 +1108,8 @@ function resolveVendoredTemplate(name) {
|
|
|
976
1108
|
return void 0;
|
|
977
1109
|
}
|
|
978
1110
|
for (let i = 0; i < 8; i++) {
|
|
979
|
-
const candidate =
|
|
980
|
-
if (existsSync4(
|
|
1111
|
+
const candidate = join8(dir, "templates", name);
|
|
1112
|
+
if (existsSync4(join8(candidate, "copier.yml"))) return candidate;
|
|
981
1113
|
const parent = dirname3(dir);
|
|
982
1114
|
if (parent === dir) break;
|
|
983
1115
|
dir = parent;
|
|
@@ -996,7 +1128,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
996
1128
|
message: "PromptForAgentConfig must run before RunCopierTemplate (targetRepo/role unset)"
|
|
997
1129
|
};
|
|
998
1130
|
}
|
|
999
|
-
const roleDir =
|
|
1131
|
+
const roleDir = join8(ctx.targetDir, "agents", "hermes", role);
|
|
1000
1132
|
ctx.roleDir = roleDir;
|
|
1001
1133
|
ctx.runtimeRepo = `delorenj/agent-hm-${targetRepo}-${role}`;
|
|
1002
1134
|
const which = spawnSync("which", ["copier"], { encoding: "utf8" });
|
|
@@ -1006,7 +1138,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
1006
1138
|
message: "\u2717 copier not found on PATH. Install with: `uv tool install copier` or `pip install copier`"
|
|
1007
1139
|
};
|
|
1008
1140
|
}
|
|
1009
|
-
if (existsSync4(
|
|
1141
|
+
if (existsSync4(join8(roleDir, "role.yaml")) && !ctx.force) {
|
|
1010
1142
|
if (ctx.yes) {
|
|
1011
1143
|
ctx.force = true;
|
|
1012
1144
|
} else {
|
|
@@ -1027,15 +1159,16 @@ var RunCopierTemplate = class extends Command {
|
|
|
1027
1159
|
...process.env,
|
|
1028
1160
|
SKIP_TELEGRAM: "1",
|
|
1029
1161
|
SKIP_EMAIL: "1",
|
|
1030
|
-
//
|
|
1162
|
+
// Bloodbank is a fleet-shared Hermes gateway. Never provision the legacy
|
|
1163
|
+
// per-profile file consumer, even when an older template still exposes it.
|
|
1031
1164
|
SKIP_RUNTIME_REPO: ctx.skipRuntimeRepo ? "1" : "0",
|
|
1032
1165
|
SKIP_PLANE: ctx.skipPlane ? "1" : "0",
|
|
1033
|
-
SKIP_BLOODBANK:
|
|
1166
|
+
SKIP_BLOODBANK: "1",
|
|
1034
1167
|
SKIP_SYSTEMD: ctx.skipSystemd ? "1" : "0"
|
|
1035
1168
|
};
|
|
1036
|
-
const LOCAL_TEMPLATE =
|
|
1169
|
+
const LOCAL_TEMPLATE = join8(homedir2(), "code", "hermes-agent-template");
|
|
1037
1170
|
const vendored = resolveVendoredTemplate("hermes-agent");
|
|
1038
|
-
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync4(
|
|
1171
|
+
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync4(join8(LOCAL_TEMPLATE, "copier.yml")) ? LOCAL_TEMPLATE : HERMES_AGENT_TEMPLATE);
|
|
1039
1172
|
const args = [
|
|
1040
1173
|
"copy",
|
|
1041
1174
|
templateSrc,
|
|
@@ -1066,7 +1199,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
1066
1199
|
message: this.formatMessage(`Would run: copier ${args.join(" ")}`)
|
|
1067
1200
|
};
|
|
1068
1201
|
}
|
|
1069
|
-
mkdirSync3(
|
|
1202
|
+
mkdirSync3(join8(ctx.targetDir, "agents", "hermes"), { recursive: true });
|
|
1070
1203
|
const spinner4 = p2.spinner();
|
|
1071
1204
|
spinner4.start(`Running copier copy (target: agents/hermes/${role})`);
|
|
1072
1205
|
const result = spawnSync("copier", args, {
|
|
@@ -1090,20 +1223,26 @@ var RunCopierTemplate = class extends Command {
|
|
|
1090
1223
|
};
|
|
1091
1224
|
|
|
1092
1225
|
// src/commands/hermes/UntrackHermesRuntimes.ts
|
|
1093
|
-
import { existsSync as existsSync5, readFileSync as
|
|
1094
|
-
import { join as
|
|
1226
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4, readdirSync as readdirSync2 } from "fs";
|
|
1227
|
+
import { join as join9 } from "path";
|
|
1095
1228
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
1229
|
+
function sectionHasPath(section, targetPath) {
|
|
1230
|
+
return section.split(/\r?\n/).some((line) => /^\s*path\s*=/.test(line) && line.replace(/^\s*path\s*=\s*/, "").trim() === targetPath);
|
|
1231
|
+
}
|
|
1232
|
+
function removeSubmodulePath(content, targetPath) {
|
|
1233
|
+
return content.replace(/^\[submodule "[^"\n]+"\][\s\S]*?(?=^\[submodule "|(?![\s\S]))/gm, (section) => sectionHasPath(section, targetPath) ? "" : section).replace(/\n{3,}/g, "\n\n").trim();
|
|
1234
|
+
}
|
|
1096
1235
|
var UntrackHermesRuntimes = class extends Command {
|
|
1097
1236
|
async invoke() {
|
|
1098
1237
|
const targetDir = this.context.targetDir;
|
|
1099
|
-
const rolesDir =
|
|
1238
|
+
const rolesDir = join9(targetDir, "agents", "hermes");
|
|
1100
1239
|
if (!existsSync5(rolesDir)) {
|
|
1101
1240
|
return {
|
|
1102
1241
|
success: true,
|
|
1103
1242
|
message: "No Hermes agents found (no agents/hermes directory)."
|
|
1104
1243
|
};
|
|
1105
1244
|
}
|
|
1106
|
-
const roles =
|
|
1245
|
+
const roles = readdirSync2(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
1107
1246
|
if (roles.length === 0) {
|
|
1108
1247
|
return {
|
|
1109
1248
|
success: true,
|
|
@@ -1113,47 +1252,79 @@ var UntrackHermesRuntimes = class extends Command {
|
|
|
1113
1252
|
let modifiedAny = false;
|
|
1114
1253
|
const details = [];
|
|
1115
1254
|
for (const role of roles) {
|
|
1116
|
-
const roleDir =
|
|
1117
|
-
const runtimePath =
|
|
1118
|
-
const gitignorePath =
|
|
1255
|
+
const roleDir = join9("agents", "hermes", role);
|
|
1256
|
+
const runtimePath = join9(roleDir, "runtime");
|
|
1257
|
+
const gitignorePath = join9(roleDir, ".gitignore");
|
|
1258
|
+
const gitmodulesPath = join9(targetDir, ".gitmodules");
|
|
1119
1259
|
let isTracked = false;
|
|
1120
|
-
const lsResult = spawnSync2("git", ["ls-files", "--stage", runtimePath], {
|
|
1260
|
+
const lsResult = spawnSync2("git", ["ls-files", "--stage", "--", runtimePath], {
|
|
1121
1261
|
cwd: targetDir,
|
|
1122
1262
|
encoding: "utf8"
|
|
1123
1263
|
});
|
|
1264
|
+
if (lsResult.status !== 0) {
|
|
1265
|
+
return {
|
|
1266
|
+
success: false,
|
|
1267
|
+
message: `\u2717 Failed to inspect runtime index at ${runtimePath}: ${lsResult.stderr.trim() || `exit ${lsResult.status}`}`
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1124
1270
|
if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
|
|
1125
1271
|
isTracked = true;
|
|
1126
1272
|
}
|
|
1273
|
+
let hasStaleMapping = false;
|
|
1274
|
+
let gitmodulesContent = "";
|
|
1275
|
+
if (existsSync5(gitmodulesPath)) {
|
|
1276
|
+
gitmodulesContent = readFileSync4(gitmodulesPath, "utf8");
|
|
1277
|
+
const sections = gitmodulesContent.match(/^\[submodule "[^"\n]+"\][\s\S]*?(?=^\[submodule "|(?![\s\S]))/gm) ?? [];
|
|
1278
|
+
hasStaleMapping = sections.some((section) => sectionHasPath(section, runtimePath));
|
|
1279
|
+
}
|
|
1127
1280
|
let isIgnored = false;
|
|
1128
|
-
const fullGitignorePath =
|
|
1281
|
+
const fullGitignorePath = join9(targetDir, gitignorePath);
|
|
1129
1282
|
if (existsSync5(fullGitignorePath)) {
|
|
1130
|
-
const content =
|
|
1283
|
+
const content = readFileSync4(fullGitignorePath, "utf8");
|
|
1131
1284
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
1132
1285
|
isIgnored = lines.includes("runtime/") || lines.includes("runtime");
|
|
1133
1286
|
}
|
|
1134
|
-
if (isTracked || !isIgnored) {
|
|
1287
|
+
if (isTracked || hasStaleMapping || !isIgnored) {
|
|
1135
1288
|
modifiedAny = true;
|
|
1136
1289
|
if (isTracked) {
|
|
1137
1290
|
details.push(`untrack agents/hermes/${role}/runtime`);
|
|
1138
1291
|
if (!this.context.dryRun) {
|
|
1139
|
-
const rmResult = spawnSync2("git", ["rm", "--cached", "-r", runtimePath], {
|
|
1292
|
+
const rmResult = spawnSync2("git", ["rm", "--cached", "-r", "-f", "--", runtimePath], {
|
|
1140
1293
|
cwd: targetDir,
|
|
1141
1294
|
encoding: "utf8"
|
|
1142
1295
|
});
|
|
1143
1296
|
if (rmResult.status !== 0) {
|
|
1144
1297
|
return {
|
|
1145
1298
|
success: false,
|
|
1146
|
-
message:
|
|
1299
|
+
message: `\u2717 Failed to untrack ${runtimePath}: ${rmResult.stderr.trim() || `exit ${rmResult.status}`}`
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
const verifyResult = spawnSync2("git", ["ls-files", "--stage", "--", runtimePath], {
|
|
1303
|
+
cwd: targetDir,
|
|
1304
|
+
encoding: "utf8"
|
|
1305
|
+
});
|
|
1306
|
+
if (verifyResult.status !== 0 || verifyResult.stdout.trim()) {
|
|
1307
|
+
return {
|
|
1308
|
+
success: false,
|
|
1309
|
+
message: verifyResult.status !== 0 ? `\u2717 Failed to verify untracked runtime ${runtimePath}: ${verifyResult.stderr.trim() || `exit ${verifyResult.status}`}` : `\u2717 Runtime remains tracked after index-only removal: ${runtimePath}`
|
|
1147
1310
|
};
|
|
1148
1311
|
}
|
|
1149
1312
|
}
|
|
1150
1313
|
}
|
|
1314
|
+
if (hasStaleMapping) {
|
|
1315
|
+
details.push(`remove stale .gitmodules mapping for ${runtimePath}`);
|
|
1316
|
+
if (!this.context.dryRun) {
|
|
1317
|
+
const next = removeSubmodulePath(gitmodulesContent, runtimePath);
|
|
1318
|
+
writeFileSync4(gitmodulesPath, next ? `${next}
|
|
1319
|
+
` : "", "utf8");
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1151
1322
|
if (!isIgnored) {
|
|
1152
1323
|
details.push(`ignore runtime/ in agents/hermes/${role}/.gitignore`);
|
|
1153
1324
|
if (!this.context.dryRun) {
|
|
1154
1325
|
let content = "";
|
|
1155
1326
|
if (existsSync5(fullGitignorePath)) {
|
|
1156
|
-
content =
|
|
1327
|
+
content = readFileSync4(fullGitignorePath, "utf8");
|
|
1157
1328
|
}
|
|
1158
1329
|
if (content && !content.endsWith("\n")) {
|
|
1159
1330
|
content += "\n";
|
|
@@ -1181,7 +1352,7 @@ ${details.map((d) => ` - ${d}`).join("\n")}`
|
|
|
1181
1352
|
|
|
1182
1353
|
// src/commands/hermes/WireTelegram.ts
|
|
1183
1354
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
1184
|
-
import { join as
|
|
1355
|
+
import { join as join10 } from "node:path";
|
|
1185
1356
|
import { existsSync as existsSync6, unlinkSync } from "node:fs";
|
|
1186
1357
|
import * as p3 from "@clack/prompts";
|
|
1187
1358
|
var WireTelegram = class extends Command {
|
|
@@ -1270,14 +1441,14 @@ var WireTelegram = class extends Command {
|
|
|
1270
1441
|
if (p3.isCancel(allowedAnswer)) {
|
|
1271
1442
|
return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
|
|
1272
1443
|
}
|
|
1273
|
-
const script =
|
|
1444
|
+
const script = join10(roleDir, ".scripts", "30-telegram.sh");
|
|
1274
1445
|
if (!existsSync6(script)) {
|
|
1275
1446
|
return {
|
|
1276
1447
|
success: false,
|
|
1277
1448
|
message: `\u2717 ${script} not found. Did copier finish? Re-run with --skip-runtime-repo=0 if you skipped it.`
|
|
1278
1449
|
};
|
|
1279
1450
|
}
|
|
1280
|
-
const marker =
|
|
1451
|
+
const marker = join10(roleDir, ".scripts", ".done-30-telegram");
|
|
1281
1452
|
if (existsSync6(marker)) unlinkSync(marker);
|
|
1282
1453
|
const spinner4 = p3.spinner();
|
|
1283
1454
|
spinner4.start("Verifying token + wiring profile");
|
|
@@ -1305,7 +1476,7 @@ function cap(s) {
|
|
|
1305
1476
|
|
|
1306
1477
|
// src/commands/hermes/WireEmail.ts
|
|
1307
1478
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
1308
|
-
import { join as
|
|
1479
|
+
import { join as join11 } from "node:path";
|
|
1309
1480
|
import { existsSync as existsSync7, unlinkSync as unlinkSync2 } from "node:fs";
|
|
1310
1481
|
import * as p4 from "@clack/prompts";
|
|
1311
1482
|
var WireEmail = class extends Command {
|
|
@@ -1321,7 +1492,7 @@ var WireEmail = class extends Command {
|
|
|
1321
1492
|
if (!targetRepo || !role || !roleDir) {
|
|
1322
1493
|
return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
|
|
1323
1494
|
}
|
|
1324
|
-
const script =
|
|
1495
|
+
const script = join11(roleDir, ".scripts", "50-email.sh");
|
|
1325
1496
|
if (!existsSync7(script)) {
|
|
1326
1497
|
return { success: false, message: `\u2717 ${script} not found` };
|
|
1327
1498
|
}
|
|
@@ -1384,7 +1555,7 @@ var WireEmail = class extends Command {
|
|
|
1384
1555
|
}
|
|
1385
1556
|
}
|
|
1386
1557
|
}
|
|
1387
|
-
const marker =
|
|
1558
|
+
const marker = join11(roleDir, ".scripts", ".done-50-email");
|
|
1388
1559
|
if (existsSync7(marker)) unlinkSync2(marker);
|
|
1389
1560
|
const spinner4 = p4.spinner();
|
|
1390
1561
|
spinner4.start("Creating Cloudflare Email Routing rule");
|
|
@@ -1413,7 +1584,6 @@ var PrintHermesSummary = class extends Command {
|
|
|
1413
1584
|
const botHandle = `${targetRepo?.toLowerCase().replace(/-/g, "_")}_${role?.toLowerCase()}_bot`;
|
|
1414
1585
|
const email = `${targetRepo}-${role}@delo.sh`;
|
|
1415
1586
|
const gw = `hermes-${agentId}-gateway.service`;
|
|
1416
|
-
const csm = `hermes-${agentId}-consumer.service`;
|
|
1417
1587
|
const hb = `hermes-${agentId}-heartbeat.timer`;
|
|
1418
1588
|
const lines = [];
|
|
1419
1589
|
lines.push(`agent_id ${agentId}`);
|
|
@@ -1423,13 +1593,13 @@ var PrintHermesSummary = class extends Command {
|
|
|
1423
1593
|
if (!skipEmail) lines.push(`email ${email}`);
|
|
1424
1594
|
lines.push("");
|
|
1425
1595
|
lines.push("Start daemons:");
|
|
1426
|
-
lines.push(` systemctl --user start ${csm}`);
|
|
1427
1596
|
lines.push(` systemctl --user start ${hb}`);
|
|
1428
1597
|
if (!skipTelegram) {
|
|
1429
1598
|
lines.push(` systemctl --user start ${gw}`);
|
|
1430
1599
|
} else {
|
|
1431
1600
|
lines.push(` # gateway needs Telegram wired first (re-run with --skip-telegram=0)`);
|
|
1432
1601
|
}
|
|
1602
|
+
lines.push(" # Bloodbank commands arrive through the fleet-shared Hermes gateway");
|
|
1433
1603
|
lines.push("");
|
|
1434
1604
|
lines.push("Talk locally:");
|
|
1435
1605
|
lines.push(` ${ctx.roleDir}/hermes chat "status"`);
|
|
@@ -1472,15 +1642,15 @@ var HermesAgentRecipe = class extends Recipe {
|
|
|
1472
1642
|
|
|
1473
1643
|
// src/commands/AgentHooksCommands.ts
|
|
1474
1644
|
import { homedir as homedir4 } from "node:os";
|
|
1475
|
-
import { join as
|
|
1476
|
-
import { existsSync as existsSync9, cpSync, mkdirSync as mkdirSync5, readFileSync as
|
|
1645
|
+
import { join as join13, dirname as dirname5 } from "node:path";
|
|
1646
|
+
import { existsSync as existsSync9, cpSync, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1477
1647
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1478
1648
|
|
|
1479
1649
|
// src/project/index.ts
|
|
1480
1650
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
1481
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as
|
|
1651
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync5, renameSync, statSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1482
1652
|
import { homedir as homedir3 } from "node:os";
|
|
1483
|
-
import { basename as
|
|
1653
|
+
import { basename as basename3, delimiter, dirname as dirname4, join as join12, resolve as resolve2 } from "node:path";
|
|
1484
1654
|
import YAML2 from "yaml";
|
|
1485
1655
|
|
|
1486
1656
|
// src/project/RegistryStore.ts
|
|
@@ -1715,18 +1885,18 @@ var PROJECT_SOURCE_SKILL_ROOTS_ENV = "PJ_SOURCE_SKILL_ROOTS";
|
|
|
1715
1885
|
var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
|
|
1716
1886
|
var DEFAULT_SOURCE_SKILL_ROOTS = [
|
|
1717
1887
|
"/home/delorenj/code/skillex/all-skills",
|
|
1718
|
-
|
|
1719
|
-
|
|
1888
|
+
join12(homedir3(), ".agents", "skills"),
|
|
1889
|
+
join12(homedir3(), ".codex", "skills")
|
|
1720
1890
|
];
|
|
1721
1891
|
function projectRegistryPath(env2 = process.env) {
|
|
1722
|
-
return expandHome(env2[PROJECT_REGISTRY_ENV] ||
|
|
1892
|
+
return expandHome(env2[PROJECT_REGISTRY_ENV] || join12(homedir3(), ".config", "pjangler", "projects.yaml"));
|
|
1723
1893
|
}
|
|
1724
1894
|
function emptyProjectRegistry() {
|
|
1725
1895
|
return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
|
|
1726
1896
|
}
|
|
1727
1897
|
function loadProjectRegistry(path = projectRegistryPath()) {
|
|
1728
1898
|
if (!existsSync8(path)) return emptyProjectRegistry();
|
|
1729
|
-
const raw = YAML2.parse(
|
|
1899
|
+
const raw = YAML2.parse(readFileSync5(path, "utf8"));
|
|
1730
1900
|
if (raw == null) return emptyProjectRegistry();
|
|
1731
1901
|
if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
|
|
1732
1902
|
const registry = raw;
|
|
@@ -1756,7 +1926,7 @@ function validateProjectRegistry(registry) {
|
|
|
1756
1926
|
validateProjectRecord(project, slug);
|
|
1757
1927
|
if (slugs.has(project.slug)) throw new Error(`Duplicate project slug: ${project.slug}`);
|
|
1758
1928
|
slugs.add(project.slug);
|
|
1759
|
-
const repoKey =
|
|
1929
|
+
const repoKey = resolve2(project.repo_path);
|
|
1760
1930
|
const existingRepoSlug = repoPaths.get(repoKey);
|
|
1761
1931
|
if (existingRepoSlug && existingRepoSlug !== slug) {
|
|
1762
1932
|
throw new Error(`Duplicate project repo_path: ${project.repo_path} used by ${existingRepoSlug} and ${slug}`);
|
|
@@ -1823,7 +1993,7 @@ function resolveAgentHooksLayer(input, env2 = process.env) {
|
|
|
1823
1993
|
const override = env2.PJ_AGENT_HOOKS_LAYER;
|
|
1824
1994
|
if (override === "0" || override === "false") return false;
|
|
1825
1995
|
if (override === "1" || override === "true") return true;
|
|
1826
|
-
return !existsSync8(
|
|
1996
|
+
return !existsSync8(join12(homedir3(), ".agents", "hooks"));
|
|
1827
1997
|
}
|
|
1828
1998
|
function jsonStable(value) {
|
|
1829
1999
|
return JSON.stringify(value);
|
|
@@ -1836,14 +2006,14 @@ function projectRecordEquivalent(a, b) {
|
|
|
1836
2006
|
}
|
|
1837
2007
|
function defaultProjectTargetDir(name, cwd = process.cwd()) {
|
|
1838
2008
|
const compactName = name.replace(/[^A-Za-z0-9._-]/g, "") || slugifyProjectName(name);
|
|
1839
|
-
return
|
|
2009
|
+
return resolve2(dirname4(resolve2(cwd)), compactName);
|
|
1840
2010
|
}
|
|
1841
2011
|
function sourceSkillRoots(env2 = process.env) {
|
|
1842
2012
|
const configuredRoots = (env2[PROJECT_SOURCE_SKILL_ROOTS_ENV] || "").split(delimiter).map((root) => root.trim()).filter(Boolean);
|
|
1843
2013
|
const seen = /* @__PURE__ */ new Set();
|
|
1844
2014
|
const roots = [];
|
|
1845
2015
|
for (const root of [...DEFAULT_SOURCE_SKILL_ROOTS, ...configuredRoots]) {
|
|
1846
|
-
const normalized =
|
|
2016
|
+
const normalized = resolve2(expandHome(root));
|
|
1847
2017
|
if (seen.has(normalized)) continue;
|
|
1848
2018
|
seen.add(normalized);
|
|
1849
2019
|
roots.push(normalized);
|
|
@@ -1853,12 +2023,12 @@ function sourceSkillRoots(env2 = process.env) {
|
|
|
1853
2023
|
function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
|
|
1854
2024
|
if (!sourceSkill) return void 0;
|
|
1855
2025
|
const expanded = expandHome(sourceSkill);
|
|
1856
|
-
const direct =
|
|
2026
|
+
const direct = resolve2(expanded);
|
|
1857
2027
|
if (existsSync8(direct)) return direct;
|
|
1858
|
-
const name =
|
|
2028
|
+
const name = basename3(sourceSkill);
|
|
1859
2029
|
const roots = sourceSkillRoots(env2);
|
|
1860
2030
|
for (const root of roots) {
|
|
1861
|
-
const candidate =
|
|
2031
|
+
const candidate = join12(root, name);
|
|
1862
2032
|
if (existsSync8(candidate)) return candidate;
|
|
1863
2033
|
}
|
|
1864
2034
|
const searched = roots.length ? ` Searched roots: ${roots.join(", ")}.` : "";
|
|
@@ -1867,11 +2037,11 @@ function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
|
|
|
1867
2037
|
}
|
|
1868
2038
|
function planProjectInit(input) {
|
|
1869
2039
|
if (!input.name.trim()) throw new Error("Project name is required");
|
|
1870
|
-
const registryPath2 =
|
|
2040
|
+
const registryPath2 = resolve2(projectRegistryPath({ ...process.env, [PROJECT_REGISTRY_ENV]: input.registryPath || process.env[PROJECT_REGISTRY_ENV] }));
|
|
1871
2041
|
const registry = loadProjectRegistry(registryPath2);
|
|
1872
2042
|
const now = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1873
2043
|
const slug = input.projectSlug ?? slugifyProjectName(input.name);
|
|
1874
|
-
const targetDir =
|
|
2044
|
+
const targetDir = resolve2(input.targetDir ?? defaultProjectTargetDir(input.name, input.cwd));
|
|
1875
2045
|
const identifier = (input.projectIdentifier ?? deriveProjectIdentifier(input.name)).toUpperCase();
|
|
1876
2046
|
const existing = registry.projects[slug];
|
|
1877
2047
|
const sourceSkillPath = resolveSourceSkillPath(input.sourceSkill);
|
|
@@ -1914,7 +2084,7 @@ function planProjectInit(input) {
|
|
|
1914
2084
|
updated_at: projectRecordEquivalent(existing, candidateProject) ? existing.updated_at : now
|
|
1915
2085
|
};
|
|
1916
2086
|
validateNoDuplicateProject(registry, project, overwrite);
|
|
1917
|
-
const pjanglerRoot =
|
|
2087
|
+
const pjanglerRoot = resolve2(input.pjanglerRoot ?? resolvePjanglerRoot());
|
|
1918
2088
|
const manifest = projectManifestFromRegistryProject(project);
|
|
1919
2089
|
const apply = input.apply ?? false;
|
|
1920
2090
|
const live = input.live ?? false;
|
|
@@ -1940,7 +2110,7 @@ function planProjectInit(input) {
|
|
|
1940
2110
|
}));
|
|
1941
2111
|
}
|
|
1942
2112
|
actions.push(
|
|
1943
|
-
{ kind: "project.write-manifest", path:
|
|
2113
|
+
{ kind: "project.write-manifest", path: join12(targetDir, ".project.json"), manifest },
|
|
1944
2114
|
{
|
|
1945
2115
|
kind: "ticket-provider.create-or-link",
|
|
1946
2116
|
enabled: live,
|
|
@@ -2001,7 +2171,7 @@ async function executeProjectInitPlan(plan) {
|
|
|
2001
2171
|
mkdirSync4(dirname4(action.path), { recursive: true });
|
|
2002
2172
|
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
2003
2173
|
`;
|
|
2004
|
-
const current = existsSync8(action.path) ?
|
|
2174
|
+
const current = existsSync8(action.path) ? readFileSync5(action.path, "utf8") : void 0;
|
|
2005
2175
|
if (current !== next) {
|
|
2006
2176
|
writeFileSync5(action.path, next, "utf8");
|
|
2007
2177
|
changedFiles.push(action.path);
|
|
@@ -2066,7 +2236,7 @@ function getProject(registry, slug) {
|
|
|
2066
2236
|
return project;
|
|
2067
2237
|
}
|
|
2068
2238
|
function buildCommonProjectCopierAction(input) {
|
|
2069
|
-
const templateDir =
|
|
2239
|
+
const templateDir = join12(input.pjanglerRoot, "templates", "commonproject");
|
|
2070
2240
|
const data = {
|
|
2071
2241
|
project_name: input.projectName,
|
|
2072
2242
|
project_description: input.projectDescription ?? "",
|
|
@@ -2095,19 +2265,19 @@ function buildCommonProjectCopierAction(input) {
|
|
|
2095
2265
|
function resolvePjanglerRoot() {
|
|
2096
2266
|
let dir = dirname4(new URL(import.meta.url).pathname);
|
|
2097
2267
|
while (dir !== dirname4(dir)) {
|
|
2098
|
-
if (existsSync8(
|
|
2268
|
+
if (existsSync8(join12(dir, "package.json")) && existsSync8(join12(dir, "templates", "commonproject", "copier.yml"))) return dir;
|
|
2099
2269
|
dir = dirname4(dir);
|
|
2100
2270
|
}
|
|
2101
|
-
return
|
|
2271
|
+
return resolve2(process.cwd());
|
|
2102
2272
|
}
|
|
2103
2273
|
function validateNoDuplicateProject(registry, project, overwrite) {
|
|
2104
2274
|
const existingSameSlug = registry.projects[project.slug];
|
|
2105
|
-
if (existingSameSlug && !overwrite &&
|
|
2275
|
+
if (existingSameSlug && !overwrite && resolve2(existingSameSlug.repo_path) !== resolve2(project.repo_path)) {
|
|
2106
2276
|
throw new Error(`Project slug already exists in registry: ${project.slug}`);
|
|
2107
2277
|
}
|
|
2108
2278
|
for (const [slug, existing] of Object.entries(registry.projects)) {
|
|
2109
2279
|
if (slug === project.slug) continue;
|
|
2110
|
-
if (
|
|
2280
|
+
if (resolve2(existing.repo_path) === resolve2(project.repo_path)) {
|
|
2111
2281
|
throw new Error(`Project repo_path already registered by ${slug}: ${project.repo_path}`);
|
|
2112
2282
|
}
|
|
2113
2283
|
if (existing.ticket_provider.identifier && existing.ticket_provider.identifier.toUpperCase() === project.ticket_provider.identifier?.toUpperCase()) {
|
|
@@ -2127,7 +2297,7 @@ function validateProjectRecord(project, key) {
|
|
|
2127
2297
|
}
|
|
2128
2298
|
function expandHome(path) {
|
|
2129
2299
|
if (path === "~") return homedir3();
|
|
2130
|
-
if (path.startsWith("~/")) return
|
|
2300
|
+
if (path.startsWith("~/")) return join12(homedir3(), path.slice(2));
|
|
2131
2301
|
return path;
|
|
2132
2302
|
}
|
|
2133
2303
|
function isRecord(value) {
|
|
@@ -2144,16 +2314,16 @@ function resolveTemplateRoot() {
|
|
|
2144
2314
|
try {
|
|
2145
2315
|
let dir = dirname5(fileURLToPath2(import.meta.url));
|
|
2146
2316
|
for (let i = 0; i < 8; i++) {
|
|
2147
|
-
candidates.push(
|
|
2317
|
+
candidates.push(join13(dir, "templates", "commonproject", "template"));
|
|
2148
2318
|
const parent = dirname5(dir);
|
|
2149
2319
|
if (parent === dir) break;
|
|
2150
2320
|
dir = parent;
|
|
2151
2321
|
}
|
|
2152
2322
|
} catch {
|
|
2153
2323
|
}
|
|
2154
|
-
candidates.push(
|
|
2324
|
+
candidates.push(join13(homedir4(), "code", "pjangler", "templates", "commonproject", "template"));
|
|
2155
2325
|
for (const c of candidates) {
|
|
2156
|
-
if (existsSync9(
|
|
2326
|
+
if (existsSync9(join13(c, ".agents", "hooks", "hooks.master.json"))) return c;
|
|
2157
2327
|
}
|
|
2158
2328
|
throw new Error(
|
|
2159
2329
|
"Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
|
|
@@ -2179,8 +2349,8 @@ var CopyAgentHooksTree = class extends Command {
|
|
|
2179
2349
|
const created = [];
|
|
2180
2350
|
const skipped = [];
|
|
2181
2351
|
for (const { rel, dir } of items) {
|
|
2182
|
-
const src =
|
|
2183
|
-
const dest =
|
|
2352
|
+
const src = join13(templateRoot, rel);
|
|
2353
|
+
const dest = join13(this.context.targetDir, rel);
|
|
2184
2354
|
if (!existsSync9(src)) continue;
|
|
2185
2355
|
if (existsSync9(dest) && !this.context.force) {
|
|
2186
2356
|
skipped.push(rel);
|
|
@@ -2208,14 +2378,14 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
|
2208
2378
|
if (!resolveAgentHooksLayer()) {
|
|
2209
2379
|
return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
|
|
2210
2380
|
}
|
|
2211
|
-
const misePath =
|
|
2381
|
+
const misePath = join13(this.context.targetDir, "mise.toml");
|
|
2212
2382
|
if (!existsSync9(misePath)) {
|
|
2213
2383
|
return {
|
|
2214
2384
|
success: false,
|
|
2215
2385
|
message: "\u26A0\uFE0F No mise.toml found \u2014 run `pjangler init mise` first, then re-run."
|
|
2216
2386
|
};
|
|
2217
2387
|
}
|
|
2218
|
-
let content =
|
|
2388
|
+
let content = readFileSync6(misePath, "utf8");
|
|
2219
2389
|
if (content.includes(_WireMiseAgentHooks.MARKER)) {
|
|
2220
2390
|
return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
|
|
2221
2391
|
}
|
|
@@ -2230,15 +2400,15 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
|
2230
2400
|
const enterRe = /(enter\s*=\s*\[[\s\S]*?)(\n[ \t]*\])/;
|
|
2231
2401
|
if (enterRe.test(content)) {
|
|
2232
2402
|
content = content.replace(enterRe, (_m, head, close) => {
|
|
2233
|
-
const
|
|
2234
|
-
return `${head}${
|
|
2403
|
+
const sep2 = /[,[]\s*$/.test(head) ? "" : ",";
|
|
2404
|
+
return `${head}${sep2}
|
|
2235
2405
|
${enterAdds}${close}`;
|
|
2236
2406
|
});
|
|
2237
2407
|
const leaveRe = /(leave\s*=\s*\[[\s\S]*?)(\n[ \t]*\])/;
|
|
2238
2408
|
if (leaveRe.test(content)) {
|
|
2239
2409
|
content = content.replace(leaveRe, (_m, head, close) => {
|
|
2240
|
-
const
|
|
2241
|
-
return `${head}${
|
|
2410
|
+
const sep2 = /[,[]\s*$/.test(head) ? "" : ",";
|
|
2411
|
+
return `${head}${sep2}
|
|
2242
2412
|
"${cr}/.agents/hooks/sync.py --uninstall --quiet",${close}`;
|
|
2243
2413
|
});
|
|
2244
2414
|
} else {
|
|
@@ -2463,15 +2633,15 @@ function createRecipe(name, context) {
|
|
|
2463
2633
|
}
|
|
2464
2634
|
|
|
2465
2635
|
// src/utils/version.ts
|
|
2466
|
-
import { readFileSync as
|
|
2467
|
-
import { dirname as dirname6, join as
|
|
2636
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
2637
|
+
import { dirname as dirname6, join as join14 } from "node:path";
|
|
2468
2638
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
2469
2639
|
var PJANGLER_VERSION = (() => {
|
|
2470
2640
|
try {
|
|
2471
2641
|
let dir = dirname6(fileURLToPath3(import.meta.url));
|
|
2472
2642
|
for (let i = 0; i < 4; i++) {
|
|
2473
2643
|
try {
|
|
2474
|
-
const raw =
|
|
2644
|
+
const raw = readFileSync7(join14(dir, "package.json"), "utf8");
|
|
2475
2645
|
return JSON.parse(raw).version ?? "0.0.0";
|
|
2476
2646
|
} catch {
|
|
2477
2647
|
const parent = dirname6(dir);
|
|
@@ -2485,24 +2655,41 @@ var PJANGLER_VERSION = (() => {
|
|
|
2485
2655
|
})();
|
|
2486
2656
|
|
|
2487
2657
|
// src/parity/index.ts
|
|
2488
|
-
import { existsSync as existsSync10, lstatSync, mkdirSync as mkdirSync6, readFileSync as
|
|
2489
|
-
import { basename as
|
|
2490
|
-
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
2658
|
+
import { existsSync as existsSync10, lstatSync as lstatSync2, mkdirSync as mkdirSync6, mkdtempSync, readFileSync as readFileSync8, readlinkSync, readdirSync as readdirSync3, realpathSync, renameSync as renameSync2, rmdirSync, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync7, chmodSync as chmodSync4, copyFileSync, rmSync } from "node:fs";
|
|
2659
|
+
import { basename as basename4, dirname as dirname7, join as join15, relative as relative2, resolve as resolve3 } from "node:path";
|
|
2660
|
+
import { fileURLToPath as fileURLToPath4, pathToFileURL } from "node:url";
|
|
2491
2661
|
import { homedir as homedir5 } from "node:os";
|
|
2492
2662
|
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
2493
2663
|
import YAML3 from "yaml";
|
|
2494
2664
|
var LINK_AGENTFILES_SCRIPT = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'";
|
|
2495
2665
|
var OP_INJECT_SCRIPT = "op inject -i .env.op > .env";
|
|
2496
|
-
var
|
|
2666
|
+
var PROVISION_BMAD_SKILLS_SCRIPT = "python3 '{{config_root}}/.mise/scripts/provision-bmad-skills.py'";
|
|
2667
|
+
var SYNC_SKILLS_SCRIPT = "python3 '{{config_root}}/.mise/scripts/sync-skills.py' --scope project";
|
|
2497
2668
|
var CODEGRAPH_SCRIPT = "[ -f '{{config_root}}/.mise/scripts/codegraph.sh' ] && '{{config_root}}/.mise/scripts/codegraph.sh' || true";
|
|
2498
2669
|
var SKILLS_REGISTRY_URL = "https://github.com/delorenj/skillex.git";
|
|
2670
|
+
var PROJECT_CLI_SKILL_DIRS = [
|
|
2671
|
+
".gemini/skills",
|
|
2672
|
+
".codex/skills",
|
|
2673
|
+
".kimi/skills",
|
|
2674
|
+
".augment/skills",
|
|
2675
|
+
".config/opencode/skills",
|
|
2676
|
+
".hermes/skills",
|
|
2677
|
+
".claude/skills",
|
|
2678
|
+
".openclaw/skills"
|
|
2679
|
+
];
|
|
2680
|
+
var CANONICAL_CLAUDE_SKILLS_ALIAS = "../.agents/skills";
|
|
2499
2681
|
var HOOKS_COMMENT_HEADER = `# This block will handle the linking of
|
|
2500
2682
|
# agent files to the main AGENTS.md file.
|
|
2501
2683
|
#
|
|
2502
2684
|
# TODO: Ensure this works for all levels of nesting.
|
|
2503
2685
|
# i.e. All linked agent files MUST be siblings at
|
|
2504
2686
|
# any given level of nesting.`;
|
|
2505
|
-
var LINK_AGENTFILES_HOOK_ENTRIES = [
|
|
2687
|
+
var LINK_AGENTFILES_HOOK_ENTRIES = [
|
|
2688
|
+
LINK_AGENTFILES_SCRIPT,
|
|
2689
|
+
OP_INJECT_SCRIPT,
|
|
2690
|
+
PROVISION_BMAD_SKILLS_SCRIPT,
|
|
2691
|
+
SYNC_SKILLS_SCRIPT
|
|
2692
|
+
];
|
|
2506
2693
|
var LINK_AGENTFILES_WATCH_TASK_BLOCK = `[[watch_files]]
|
|
2507
2694
|
patterns = ["AGENTS.md"]
|
|
2508
2695
|
task = "link-agentfiles"
|
|
@@ -2517,7 +2704,12 @@ run = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'"
|
|
|
2517
2704
|
|
|
2518
2705
|
[tasks.skills-sync]
|
|
2519
2706
|
description = "Sync skills from manifest to local CLI dirs"
|
|
2520
|
-
|
|
2707
|
+
depends = ["skills-provision-bmad"]
|
|
2708
|
+
run = "python3 '{{config_root}}/.mise/scripts/sync-skills.py' --scope project"
|
|
2709
|
+
|
|
2710
|
+
[tasks.skills-provision-bmad]
|
|
2711
|
+
description = "Provision pinned BMAD skills from the Skillex pack"
|
|
2712
|
+
run = "python3 '{{config_root}}/.mise/scripts/provision-bmad-skills.py'"`;
|
|
2521
2713
|
var VERSIONING_BLOCK = `# >>> mise-versioning >>> (managed block \u2014 do not edit by hand; re-run init to update)
|
|
2522
2714
|
[tasks."version"]
|
|
2523
2715
|
description = "Print the current version (vX.Y.Z)"
|
|
@@ -2547,7 +2739,7 @@ run = "'{{config_root}}/.mise/scripts/versioning.sh' sync"
|
|
|
2547
2739
|
function resolvePjanglerRoot2() {
|
|
2548
2740
|
let dir = dirname7(fileURLToPath4(import.meta.url));
|
|
2549
2741
|
while (dir !== dirname7(dir)) {
|
|
2550
|
-
if (existsSync10(
|
|
2742
|
+
if (existsSync10(join15(dir, "package.json")) && existsSync10(join15(dir, "templates", "commonproject", "copier.yml"))) {
|
|
2551
2743
|
return dir;
|
|
2552
2744
|
}
|
|
2553
2745
|
dir = dirname7(dir);
|
|
@@ -2558,7 +2750,7 @@ function normalizeNewlines(value) {
|
|
|
2558
2750
|
return value.replace(/\r\n/g, "\n");
|
|
2559
2751
|
}
|
|
2560
2752
|
function readText(path) {
|
|
2561
|
-
return normalizeNewlines(
|
|
2753
|
+
return normalizeNewlines(readFileSync8(path, "utf8"));
|
|
2562
2754
|
}
|
|
2563
2755
|
function safeReadText(path) {
|
|
2564
2756
|
return existsSync10(path) ? readText(path) : null;
|
|
@@ -2594,7 +2786,7 @@ function readSymlinkTarget(path) {
|
|
|
2594
2786
|
}
|
|
2595
2787
|
function ensureSymlink(path, target, dryRun) {
|
|
2596
2788
|
if (existsSync10(path)) {
|
|
2597
|
-
const stat =
|
|
2789
|
+
const stat = lstatSync2(path);
|
|
2598
2790
|
if (stat.isSymbolicLink()) {
|
|
2599
2791
|
const current = readSymlinkTarget(path);
|
|
2600
2792
|
if (current === target) return { changed: false };
|
|
@@ -2604,18 +2796,18 @@ function ensureSymlink(path, target, dryRun) {
|
|
|
2604
2796
|
}
|
|
2605
2797
|
return { changed: true };
|
|
2606
2798
|
}
|
|
2607
|
-
return { changed: false, blocked: `${
|
|
2799
|
+
return { changed: false, blocked: `${relative2(process.cwd(), path) || path} exists and is not a symlink` };
|
|
2608
2800
|
}
|
|
2609
2801
|
if (!dryRun) symlinkSync(target, path);
|
|
2610
2802
|
return { changed: true };
|
|
2611
2803
|
}
|
|
2612
2804
|
function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
2613
|
-
const agentsPath =
|
|
2805
|
+
const agentsPath = join15(repoRoot, "AGENTS.md");
|
|
2614
2806
|
if (existsSync10(agentsPath)) return { changedFiles: [], details: [] };
|
|
2615
2807
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2616
|
-
const source =
|
|
2808
|
+
const source = join15(repoRoot, file);
|
|
2617
2809
|
if (!existsSync10(source)) continue;
|
|
2618
|
-
const stat =
|
|
2810
|
+
const stat = lstatSync2(source);
|
|
2619
2811
|
if (stat.isSymbolicLink()) continue;
|
|
2620
2812
|
if (stat.isFile()) {
|
|
2621
2813
|
if (!dryRun) renameSync2(source, agentsPath);
|
|
@@ -2623,9 +2815,9 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
|
2623
2815
|
}
|
|
2624
2816
|
return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
|
|
2625
2817
|
}
|
|
2626
|
-
const readmePath =
|
|
2818
|
+
const readmePath = join15(repoRoot, "README.md");
|
|
2627
2819
|
if (existsSync10(readmePath)) {
|
|
2628
|
-
const stat =
|
|
2820
|
+
const stat = lstatSync2(readmePath);
|
|
2629
2821
|
if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
|
|
2630
2822
|
if (!dryRun) copyFileSync(readmePath, agentsPath);
|
|
2631
2823
|
return { changedFiles: [agentsPath], details: ["Copied README.md to AGENTS.md before wiring agent-file symlinks"] };
|
|
@@ -2663,11 +2855,11 @@ function yamlGet(text2, keyPath) {
|
|
|
2663
2855
|
return "";
|
|
2664
2856
|
}
|
|
2665
2857
|
function discoverRoles(repoRoot) {
|
|
2666
|
-
const rolesDir =
|
|
2858
|
+
const rolesDir = join15(repoRoot, "agents", "hermes");
|
|
2667
2859
|
if (!existsSync10(rolesDir)) return [];
|
|
2668
|
-
return
|
|
2669
|
-
const roleDir =
|
|
2670
|
-
const roleYamlPath =
|
|
2860
|
+
return readdirSync3(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
|
|
2861
|
+
const roleDir = join15(rolesDir, entry.name);
|
|
2862
|
+
const roleYamlPath = join15(roleDir, "role.yaml");
|
|
2671
2863
|
if (!existsSync10(roleYamlPath)) return null;
|
|
2672
2864
|
const text2 = readText(roleYamlPath);
|
|
2673
2865
|
const runtimeRepoRaw = yamlGet(text2, "runtime.github_repo");
|
|
@@ -2696,7 +2888,7 @@ function discoverRoles(repoRoot) {
|
|
|
2696
2888
|
}).filter((value) => Boolean(value));
|
|
2697
2889
|
}
|
|
2698
2890
|
function registryPath(homeDir) {
|
|
2699
|
-
return
|
|
2891
|
+
return join15(homeDir, ".hermes", "agents-registry.yaml");
|
|
2700
2892
|
}
|
|
2701
2893
|
function systemctlUser(args) {
|
|
2702
2894
|
const result = spawnSync6("systemctl", ["--user", ...args], { encoding: "utf8" });
|
|
@@ -2707,7 +2899,7 @@ function systemctlUser(args) {
|
|
|
2707
2899
|
};
|
|
2708
2900
|
}
|
|
2709
2901
|
function templateScript(ctx, name) {
|
|
2710
|
-
const source =
|
|
2902
|
+
const source = join15(ctx.pjanglerRoot, ".mise", "scripts", name);
|
|
2711
2903
|
return existsSync10(source) ? readText(source) : void 0;
|
|
2712
2904
|
}
|
|
2713
2905
|
function templateVersioningScript(ctx) {
|
|
@@ -2720,8 +2912,8 @@ function resolveAgentHooksLayer2(ctx) {
|
|
|
2720
2912
|
const override = process.env.PJ_AGENT_HOOKS_LAYER;
|
|
2721
2913
|
if (override === "0" || override === "false") return false;
|
|
2722
2914
|
if (override === "1" || override === "true") return true;
|
|
2723
|
-
if (existsSync10(
|
|
2724
|
-
return !existsSync10(
|
|
2915
|
+
if (existsSync10(join15(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
|
|
2916
|
+
return !existsSync10(join15(ctx.homeDir, ".agents", "hooks"));
|
|
2725
2917
|
}
|
|
2726
2918
|
function evaluateMiseConditionals(template, agentHooksLayer) {
|
|
2727
2919
|
const out = [];
|
|
@@ -2747,13 +2939,13 @@ function evaluateMiseConditionals(template, agentHooksLayer) {
|
|
|
2747
2939
|
}
|
|
2748
2940
|
function renderGeneratedProjectMiseToml(ctx, template) {
|
|
2749
2941
|
const project = readProjectJson(ctx);
|
|
2750
|
-
const projectName = String(project?.project_name ??
|
|
2942
|
+
const projectName = String(project?.project_name ?? basename4(ctx.repoRoot) ?? "project");
|
|
2751
2943
|
return evaluateMiseConditionals(template, resolveAgentHooksLayer2(ctx)).replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
|
|
2752
2944
|
}
|
|
2753
2945
|
function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
2754
|
-
const targetPath =
|
|
2946
|
+
const targetPath = join15(ctx.repoRoot, "mise.toml");
|
|
2755
2947
|
if (existsSync10(targetPath)) return false;
|
|
2756
|
-
const sourcePath =
|
|
2948
|
+
const sourcePath = join15(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
|
|
2757
2949
|
if (!existsSync10(sourcePath)) return false;
|
|
2758
2950
|
changedFiles.push(targetPath);
|
|
2759
2951
|
if (!ctx.dryRun) {
|
|
@@ -2762,24 +2954,426 @@ function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
|
2762
2954
|
return true;
|
|
2763
2955
|
}
|
|
2764
2956
|
function templateCommonProjectText(ctx, rel) {
|
|
2765
|
-
const path =
|
|
2957
|
+
const path = join15(ctx.pjanglerRoot, "templates", "commonproject", "template", rel);
|
|
2766
2958
|
return existsSync10(path) ? readText(path) : void 0;
|
|
2767
2959
|
}
|
|
2768
|
-
function
|
|
2960
|
+
function validateSkillName(name) {
|
|
2961
|
+
if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\\") || basename4(name) !== name) {
|
|
2962
|
+
throw new Error(`Unsafe skill name: ${JSON.stringify(name)}`);
|
|
2963
|
+
}
|
|
2964
|
+
return name;
|
|
2965
|
+
}
|
|
2966
|
+
function lstatIfPresent(path) {
|
|
2967
|
+
try {
|
|
2968
|
+
return lstatSync2(path);
|
|
2969
|
+
} catch (error) {
|
|
2970
|
+
if (error.code === "ENOENT") return void 0;
|
|
2971
|
+
throw error;
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
function isContainedBy(root, target) {
|
|
2975
|
+
const rel = relative2(root, target);
|
|
2976
|
+
return rel === "" || rel !== ".." && !rel.startsWith("../") && !rel.startsWith("..\\");
|
|
2977
|
+
}
|
|
2978
|
+
function prepareSafeProjectSkillsDirs(ctx) {
|
|
2979
|
+
const projectRoot = realpathSync(ctx.repoRoot);
|
|
2980
|
+
const agentsDir = join15(projectRoot, ".agents");
|
|
2981
|
+
const skillsDir = join15(agentsDir, "skills");
|
|
2982
|
+
for (const path of [agentsDir, skillsDir]) {
|
|
2983
|
+
if (!isContainedBy(projectRoot, path)) throw new Error(`Project skills path escapes repository: ${path}`);
|
|
2984
|
+
const stat = lstatIfPresent(path);
|
|
2985
|
+
if (stat?.isSymbolicLink()) throw new Error(`Refusing symlinked project skills directory: ${path}`);
|
|
2986
|
+
if (stat && !stat.isDirectory()) throw new Error(`Project skills path is not a directory: ${path}`);
|
|
2987
|
+
}
|
|
2988
|
+
if (!ctx.dryRun) {
|
|
2989
|
+
if (!existsSync10(agentsDir)) mkdirSync6(agentsDir, { recursive: false });
|
|
2990
|
+
if (!existsSync10(skillsDir)) mkdirSync6(skillsDir, { recursive: false });
|
|
2991
|
+
for (const path of [agentsDir, skillsDir]) {
|
|
2992
|
+
if (lstatSync2(path).isSymbolicLink() || !lstatSync2(path).isDirectory()) {
|
|
2993
|
+
throw new Error(`Unsafe project skills directory after creation: ${path}`);
|
|
2994
|
+
}
|
|
2995
|
+
if (!isContainedBy(projectRoot, realpathSync(path))) {
|
|
2996
|
+
throw new Error(`Resolved project skills directory escapes repository: ${path}`);
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
return { agentsDir, skillsDir };
|
|
3001
|
+
}
|
|
3002
|
+
function projectSkillTopologyIssues(repoRoot) {
|
|
3003
|
+
const issues = [];
|
|
3004
|
+
let projectRoot;
|
|
3005
|
+
try {
|
|
3006
|
+
projectRoot = realpathSync(repoRoot);
|
|
3007
|
+
} catch (error) {
|
|
3008
|
+
return [`Project root is not a readable real directory: ${error instanceof Error ? error.message : String(error)}`];
|
|
3009
|
+
}
|
|
3010
|
+
const managedSkills = join15(projectRoot, ".agents", "skills");
|
|
3011
|
+
for (const rel of PROJECT_CLI_SKILL_DIRS) {
|
|
3012
|
+
const cliDir = join15(projectRoot, rel);
|
|
3013
|
+
const parent = dirname7(cliDir);
|
|
3014
|
+
const parentStat = lstatIfPresent(parent);
|
|
3015
|
+
if (!parentStat) continue;
|
|
3016
|
+
if (parentStat.isSymbolicLink() || !parentStat.isDirectory()) {
|
|
3017
|
+
issues.push(`${rel} has an unsafe symlinked/non-directory parent`);
|
|
3018
|
+
continue;
|
|
3019
|
+
}
|
|
3020
|
+
if (!isContainedBy(projectRoot, realOrSelf(parent))) {
|
|
3021
|
+
issues.push(`${rel} parent resolves outside the project`);
|
|
3022
|
+
continue;
|
|
3023
|
+
}
|
|
3024
|
+
const stat = lstatIfPresent(cliDir);
|
|
3025
|
+
if (!stat) continue;
|
|
3026
|
+
if (stat.isSymbolicLink()) {
|
|
3027
|
+
let rawTarget = "";
|
|
3028
|
+
try {
|
|
3029
|
+
rawTarget = readlinkSync(cliDir);
|
|
3030
|
+
} catch {
|
|
3031
|
+
issues.push(`${rel} is an unreadable skills directory symlink`);
|
|
3032
|
+
continue;
|
|
3033
|
+
}
|
|
3034
|
+
if (rel !== ".claude/skills" || rawTarget !== CANONICAL_CLAUDE_SKILLS_ALIAS) {
|
|
3035
|
+
issues.push(`${rel} is an unsupported skills directory symlink`);
|
|
3036
|
+
continue;
|
|
3037
|
+
}
|
|
3038
|
+
const managedStat = lstatIfPresent(managedSkills);
|
|
3039
|
+
if (!managedStat || managedStat.isSymbolicLink() || !managedStat.isDirectory()) {
|
|
3040
|
+
issues.push(`${rel} canonical alias target .agents/skills is missing or unsafe`);
|
|
3041
|
+
continue;
|
|
3042
|
+
}
|
|
3043
|
+
try {
|
|
3044
|
+
if (realpathSync(cliDir) !== realpathSync(managedSkills)) {
|
|
3045
|
+
issues.push(`${rel} canonical alias resolves outside .agents/skills`);
|
|
3046
|
+
}
|
|
3047
|
+
} catch {
|
|
3048
|
+
issues.push(`${rel} canonical alias is broken`);
|
|
3049
|
+
}
|
|
3050
|
+
continue;
|
|
3051
|
+
}
|
|
3052
|
+
if (!stat.isDirectory()) {
|
|
3053
|
+
issues.push(`${rel} is not a directory`);
|
|
3054
|
+
continue;
|
|
3055
|
+
}
|
|
3056
|
+
if (!isContainedBy(projectRoot, realOrSelf(cliDir))) {
|
|
3057
|
+
issues.push(`${rel} resolves outside the project`);
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
return issues;
|
|
3061
|
+
}
|
|
3062
|
+
function bmadPackRoot(ctx) {
|
|
3063
|
+
return resolve3(
|
|
3064
|
+
process.env.PJ_BMAD_PACK_ROOT?.trim() || join15(ctx.homeDir, "code", "skillex", "packs", "bmad", BMAD_PACK_VERSION)
|
|
3065
|
+
);
|
|
3066
|
+
}
|
|
3067
|
+
function canonicalBmadSkillEntries(ctx) {
|
|
3068
|
+
const root = bmadPackRoot(ctx);
|
|
3069
|
+
const trusted = validateTrustedBmadPack(root);
|
|
3070
|
+
return trusted.skillNames.map((name) => ({ name: validateSkillName(name), source: pathToFileURL(join15(root, name)).href }));
|
|
3071
|
+
}
|
|
3072
|
+
function skillManifestEntryName(entry) {
|
|
3073
|
+
if (typeof entry === "string") return entry;
|
|
3074
|
+
if (!entry || typeof entry !== "object") return void 0;
|
|
3075
|
+
const name = entry.name;
|
|
3076
|
+
return typeof name === "string" ? name : void 0;
|
|
3077
|
+
}
|
|
3078
|
+
function isPackManagedManifestEntry(entry, expectedNames, packRoot) {
|
|
3079
|
+
const name = skillManifestEntryName(entry);
|
|
3080
|
+
if (!name) return false;
|
|
3081
|
+
if (expectedNames.has(name)) return true;
|
|
3082
|
+
if (!entry || typeof entry !== "object") return false;
|
|
3083
|
+
const source = entry.source;
|
|
3084
|
+
if (typeof source !== "string" || !source.startsWith("file:")) return false;
|
|
3085
|
+
try {
|
|
3086
|
+
const sourcePath = resolve3(fileURLToPath4(source));
|
|
3087
|
+
return basename4(sourcePath) === name && isContainedBy(packRoot, sourcePath);
|
|
3088
|
+
} catch {
|
|
3089
|
+
return false;
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
function canonicalSkillsManifest(ctx, current, packSkills = canonicalBmadSkillEntries(ctx)) {
|
|
3093
|
+
const existing = Array.isArray(current?.skills) ? current.skills : [];
|
|
3094
|
+
const expectedNames = new Set(packSkills.map((entry) => entry.name));
|
|
3095
|
+
const packRoot = bmadPackRoot(ctx);
|
|
2769
3096
|
return `${JSON.stringify(
|
|
2770
3097
|
{
|
|
3098
|
+
...current ?? {},
|
|
2771
3099
|
$schema: "https://raw.githubusercontent.com/skillex/schemas/main/skills.schema.json",
|
|
2772
3100
|
inherit_global: true,
|
|
2773
3101
|
registry: SKILLS_REGISTRY_URL,
|
|
2774
|
-
skills: [
|
|
3102
|
+
skills: [
|
|
3103
|
+
...existing.filter((entry) => !isPackManagedManifestEntry(entry, expectedNames, packRoot)),
|
|
3104
|
+
...packSkills
|
|
3105
|
+
]
|
|
2775
3106
|
},
|
|
2776
3107
|
null,
|
|
2777
3108
|
2
|
|
2778
3109
|
)}
|
|
2779
3110
|
`;
|
|
2780
3111
|
}
|
|
3112
|
+
function removeProjectEntry(path) {
|
|
3113
|
+
const stat = lstatIfPresent(path);
|
|
3114
|
+
if (!stat) return;
|
|
3115
|
+
if (stat.isDirectory() && !stat.isSymbolicLink()) {
|
|
3116
|
+
rmSync(path, { recursive: true, force: true });
|
|
3117
|
+
return;
|
|
3118
|
+
}
|
|
3119
|
+
try {
|
|
3120
|
+
unlinkSync3(path);
|
|
3121
|
+
} catch (error) {
|
|
3122
|
+
if (error.code !== "ENOENT") throw error;
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
function normalizeExecutableTemplate(ctx, target, expected, changedFiles) {
|
|
3126
|
+
const stat = lstatIfPresent(target);
|
|
3127
|
+
if (stat && (!stat.isFile() || stat.isSymbolicLink())) {
|
|
3128
|
+
throw new Error(`Refusing non-regular managed executable target: ${target}`);
|
|
3129
|
+
}
|
|
3130
|
+
const contentChanged = !stat || safeReadText(target) !== expected;
|
|
3131
|
+
const modeChanged = !stat || (Number(stat.mode) & 73) === 0;
|
|
3132
|
+
if (!contentChanged && !modeChanged) return;
|
|
3133
|
+
if (!changedFiles.includes(target)) changedFiles.push(target);
|
|
3134
|
+
if (ctx.dryRun) return;
|
|
3135
|
+
if (contentChanged) {
|
|
3136
|
+
writeText(target, expected);
|
|
3137
|
+
}
|
|
3138
|
+
const beforeChmod = lstatIfPresent(target);
|
|
3139
|
+
if (!beforeChmod?.isFile() || beforeChmod.isSymbolicLink()) {
|
|
3140
|
+
throw new Error(`Refusing changed managed executable target: ${target}`);
|
|
3141
|
+
}
|
|
3142
|
+
chmodSync4(target, 493);
|
|
3143
|
+
}
|
|
3144
|
+
function atomicWriteBuffer(path, content, mode, temporary) {
|
|
3145
|
+
writeFileSync7(temporary, content, { flag: "wx" });
|
|
3146
|
+
chmodSync4(temporary, mode);
|
|
3147
|
+
renameSync2(temporary, path);
|
|
3148
|
+
}
|
|
3149
|
+
function provisionBmadSkills(ctx, preservedManifest, hooks = {}) {
|
|
3150
|
+
const packRoot = bmadPackRoot(ctx);
|
|
3151
|
+
let packSkills;
|
|
3152
|
+
try {
|
|
3153
|
+
packSkills = canonicalBmadSkillEntries(ctx);
|
|
3154
|
+
} catch (error) {
|
|
3155
|
+
return {
|
|
3156
|
+
ok: false,
|
|
3157
|
+
changedFiles: [],
|
|
3158
|
+
error: `BMAD Skillex pack ${BMAD_PACK_VERSION} is not trusted at ${packRoot}: ${error instanceof Error ? error.message : String(error)}`
|
|
3159
|
+
};
|
|
3160
|
+
}
|
|
3161
|
+
hooks.afterPreflight?.();
|
|
3162
|
+
const projectRoot = realpathSync(ctx.repoRoot);
|
|
3163
|
+
const agentsPath = join15(projectRoot, ".agents");
|
|
3164
|
+
const skillsPath = join15(agentsPath, "skills");
|
|
3165
|
+
const agentsExisted = Boolean(lstatIfPresent(agentsPath));
|
|
3166
|
+
const skillsExisted = Boolean(lstatIfPresent(skillsPath));
|
|
3167
|
+
let preflightDirs;
|
|
3168
|
+
try {
|
|
3169
|
+
preflightDirs = prepareSafeProjectSkillsDirs({ ...ctx, dryRun: true });
|
|
3170
|
+
} catch (error) {
|
|
3171
|
+
return { ok: false, changedFiles: [], error: error instanceof Error ? error.message : String(error) };
|
|
3172
|
+
}
|
|
3173
|
+
const manifestPath = join15(preflightDirs.agentsDir, "skills.json");
|
|
3174
|
+
const manifestStat = lstatIfPresent(manifestPath);
|
|
3175
|
+
if (manifestStat?.isSymbolicLink() || manifestStat && !manifestStat.isFile()) {
|
|
3176
|
+
return { ok: false, changedFiles: [], error: `Refusing unsafe skills manifest: ${manifestPath}` };
|
|
3177
|
+
}
|
|
3178
|
+
const manifestBytes = manifestStat ? readFileSync8(manifestPath) : null;
|
|
3179
|
+
const manifestMode = manifestStat ? Number(manifestStat.mode) & 511 : 420;
|
|
3180
|
+
let currentManifest = {};
|
|
3181
|
+
if (manifestBytes !== null) {
|
|
3182
|
+
try {
|
|
3183
|
+
const parsed = JSON.parse(manifestBytes.toString("utf8"));
|
|
3184
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("must contain a JSON object");
|
|
3185
|
+
currentManifest = parsed;
|
|
3186
|
+
if (currentManifest.skills !== void 0 && !Array.isArray(currentManifest.skills)) throw new Error("skills must be an array");
|
|
3187
|
+
} catch (error) {
|
|
3188
|
+
return { ok: false, changedFiles: [], error: `Invalid existing skills manifest ${manifestPath}: ${error instanceof Error ? error.message : String(error)}` };
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
let safeDirs;
|
|
3192
|
+
try {
|
|
3193
|
+
safeDirs = prepareSafeProjectSkillsDirs(ctx);
|
|
3194
|
+
} catch (error) {
|
|
3195
|
+
return { ok: false, changedFiles: [], error: error instanceof Error ? error.message : String(error) };
|
|
3196
|
+
}
|
|
3197
|
+
const nextManifest = canonicalSkillsManifest(ctx, preservedManifest ?? currentManifest, packSkills);
|
|
3198
|
+
const skillsDir = safeDirs.skillsDir;
|
|
3199
|
+
const resolvedSkillsDir = ctx.dryRun && !existsSync10(skillsDir) ? skillsDir : realpathSync(skillsDir);
|
|
3200
|
+
const expected = new Map(packSkills.map((entry) => [entry.name, fileURLToPath4(entry.source)]));
|
|
3201
|
+
const expectedNames = new Set(expected.keys());
|
|
3202
|
+
const ownershipManifest = preservedManifest ?? currentManifest;
|
|
3203
|
+
const managedManifestNames = new Set(
|
|
3204
|
+
(Array.isArray(ownershipManifest.skills) ? ownershipManifest.skills : []).filter((entry) => isPackManagedManifestEntry(entry, expectedNames, packRoot)).map(skillManifestEntryName).filter((name) => Boolean(name))
|
|
3205
|
+
);
|
|
3206
|
+
const affected = /* @__PURE__ */ new Set();
|
|
3207
|
+
const staleManagedNames = /* @__PURE__ */ new Set();
|
|
3208
|
+
const originalCorrectLinks = /* @__PURE__ */ new Map();
|
|
3209
|
+
if (existsSync10(skillsDir)) {
|
|
3210
|
+
for (const name of readdirSync3(skillsDir)) {
|
|
3211
|
+
validateSkillName(name);
|
|
3212
|
+
if (dirname7(join15(resolvedSkillsDir, name)) !== resolvedSkillsDir) {
|
|
3213
|
+
return { ok: false, changedFiles: [], error: `BMAD skill path escapes project skills directory: ${name}` };
|
|
3214
|
+
}
|
|
3215
|
+
const entryPath = join15(skillsDir, name);
|
|
3216
|
+
let linkTargetsPack = false;
|
|
3217
|
+
try {
|
|
3218
|
+
linkTargetsPack = lstatSync2(entryPath).isSymbolicLink() && isContainedBy(packRoot, resolve3(dirname7(entryPath), readlinkSync(entryPath)));
|
|
3219
|
+
} catch {
|
|
3220
|
+
linkTargetsPack = false;
|
|
3221
|
+
}
|
|
3222
|
+
if (!expected.has(name) && !managedManifestNames.has(name) && !linkTargetsPack) continue;
|
|
3223
|
+
const target = expected.get(name);
|
|
3224
|
+
let correct = false;
|
|
3225
|
+
try {
|
|
3226
|
+
correct = Boolean(target) && lstatSync2(entryPath).isSymbolicLink() && resolve3(dirname7(entryPath), readlinkSync(entryPath)) === target;
|
|
3227
|
+
} catch {
|
|
3228
|
+
correct = false;
|
|
3229
|
+
}
|
|
3230
|
+
if (correct) originalCorrectLinks.set(name, readlinkSync(join15(skillsDir, name)));
|
|
3231
|
+
else {
|
|
3232
|
+
affected.add(name);
|
|
3233
|
+
if (!target) staleManagedNames.add(name);
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
for (const [name, target] of expected) {
|
|
3238
|
+
const link = join15(resolvedSkillsDir, validateSkillName(name));
|
|
3239
|
+
if (dirname7(link) !== resolvedSkillsDir) {
|
|
3240
|
+
return { ok: false, changedFiles: [], error: `BMAD skill path escapes project skills directory: ${name}` };
|
|
3241
|
+
}
|
|
3242
|
+
let correct = false;
|
|
3243
|
+
try {
|
|
3244
|
+
correct = lstatSync2(link).isSymbolicLink() && resolve3(dirname7(link), readlinkSync(link)) === target;
|
|
3245
|
+
} catch {
|
|
3246
|
+
correct = false;
|
|
3247
|
+
}
|
|
3248
|
+
if (!correct) affected.add(name);
|
|
3249
|
+
}
|
|
3250
|
+
const manifestChanged = manifestBytes?.toString("utf8") !== nextManifest;
|
|
3251
|
+
const changedFiles = [
|
|
3252
|
+
...manifestChanged ? [manifestPath] : [],
|
|
3253
|
+
...affected.size ? [skillsDir] : []
|
|
3254
|
+
];
|
|
3255
|
+
if (ctx.dryRun || changedFiles.length === 0) {
|
|
3256
|
+
try {
|
|
3257
|
+
const postflight = validateTrustedBmadPack(packRoot);
|
|
3258
|
+
if (JSON.stringify(postflight.skillNames) !== JSON.stringify(packSkills.map((entry) => entry.name))) {
|
|
3259
|
+
throw new Error("BMAD pack inventory changed after preflight");
|
|
3260
|
+
}
|
|
3261
|
+
return { ok: true, changedFiles };
|
|
3262
|
+
} catch (error) {
|
|
3263
|
+
return { ok: false, changedFiles: [], error: error instanceof Error ? error.message : String(error) };
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
const transaction = mkdtempSync(join15(safeDirs.agentsDir, ".bmad-transaction-"));
|
|
3267
|
+
const backup = join15(transaction, "entries");
|
|
3268
|
+
mkdirSync6(backup);
|
|
3269
|
+
const moved = [];
|
|
3270
|
+
const rollback = () => {
|
|
3271
|
+
const errors = [];
|
|
3272
|
+
try {
|
|
3273
|
+
for (const name of affected) {
|
|
3274
|
+
removeProjectEntry(join15(skillsDir, validateSkillName(name)));
|
|
3275
|
+
}
|
|
3276
|
+
for (const name of originalCorrectLinks.keys()) {
|
|
3277
|
+
removeProjectEntry(join15(skillsDir, validateSkillName(name)));
|
|
3278
|
+
}
|
|
3279
|
+
} catch (error) {
|
|
3280
|
+
errors.push(`remove applied projection: ${String(error)}`);
|
|
3281
|
+
}
|
|
3282
|
+
for (const name of [...moved].reverse()) {
|
|
3283
|
+
try {
|
|
3284
|
+
renameSync2(join15(backup, name), join15(skillsDir, name));
|
|
3285
|
+
} catch (error) {
|
|
3286
|
+
errors.push(`restore ${name}: ${String(error)}`);
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
for (const [name, rawTarget] of originalCorrectLinks) {
|
|
3290
|
+
try {
|
|
3291
|
+
symlinkSync(rawTarget, join15(skillsDir, name), "dir");
|
|
3292
|
+
} catch (error) {
|
|
3293
|
+
errors.push(`restore ${name}: ${String(error)}`);
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
try {
|
|
3297
|
+
if (manifestBytes === null) removeProjectEntry(manifestPath);
|
|
3298
|
+
else atomicWriteBuffer(manifestPath, manifestBytes, manifestMode, join15(transaction, "manifest.restore"));
|
|
3299
|
+
} catch (error) {
|
|
3300
|
+
errors.push(`restore manifest: ${String(error)}`);
|
|
3301
|
+
}
|
|
3302
|
+
rmSync(transaction, { recursive: true, force: true });
|
|
3303
|
+
try {
|
|
3304
|
+
if (!skillsExisted && existsSync10(skillsDir) && readdirSync3(skillsDir).length === 0) rmdirSync(skillsDir);
|
|
3305
|
+
if (!agentsExisted && existsSync10(safeDirs.agentsDir) && readdirSync3(safeDirs.agentsDir).length === 0) rmdirSync(safeDirs.agentsDir);
|
|
3306
|
+
} catch (error) {
|
|
3307
|
+
errors.push(`remove created directories: ${String(error)}`);
|
|
3308
|
+
}
|
|
3309
|
+
if (errors.length) throw new Error(`BMAD rollback was incomplete: ${errors.join("; ")}`);
|
|
3310
|
+
};
|
|
3311
|
+
try {
|
|
3312
|
+
for (const name of affected) {
|
|
3313
|
+
const entry = join15(skillsDir, name);
|
|
3314
|
+
if (lstatIfPresent(entry)) {
|
|
3315
|
+
renameSync2(entry, join15(backup, name));
|
|
3316
|
+
moved.push(name);
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
let index = 0;
|
|
3320
|
+
for (const [name, target] of expected) {
|
|
3321
|
+
index += 1;
|
|
3322
|
+
const link = join15(skillsDir, name);
|
|
3323
|
+
let correct = false;
|
|
3324
|
+
try {
|
|
3325
|
+
correct = lstatSync2(link).isSymbolicLink() && resolve3(skillsDir, readlinkSync(link)) === target;
|
|
3326
|
+
} catch {
|
|
3327
|
+
correct = false;
|
|
3328
|
+
}
|
|
3329
|
+
if (correct) continue;
|
|
3330
|
+
if (hooks.createLink) hooks.createLink(target, link, index);
|
|
3331
|
+
else symlinkSync(target, link, "dir");
|
|
3332
|
+
}
|
|
3333
|
+
if (manifestChanged) {
|
|
3334
|
+
atomicWriteBuffer(manifestPath, Buffer.from(nextManifest), manifestMode, join15(transaction, "manifest.next"));
|
|
3335
|
+
}
|
|
3336
|
+
const postflight = validateTrustedBmadPack(packRoot);
|
|
3337
|
+
if (JSON.stringify(postflight.skillNames) !== JSON.stringify(packSkills.map((entry) => entry.name))) {
|
|
3338
|
+
throw new Error("BMAD pack inventory changed after preflight");
|
|
3339
|
+
}
|
|
3340
|
+
hooks.afterApply?.(manifestPath, skillsDir);
|
|
3341
|
+
for (const name of staleManagedNames) {
|
|
3342
|
+
if (lstatIfPresent(join15(skillsDir, name))) {
|
|
3343
|
+
throw new Error(`Applied BMAD projection retained stale managed entry: ${name}`);
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
for (const [name, target] of expected) {
|
|
3347
|
+
const link = join15(skillsDir, name);
|
|
3348
|
+
let correct = false;
|
|
3349
|
+
try {
|
|
3350
|
+
correct = lstatSync2(link).isSymbolicLink() && resolve3(skillsDir, readlinkSync(link)) === target;
|
|
3351
|
+
} catch {
|
|
3352
|
+
correct = false;
|
|
3353
|
+
}
|
|
3354
|
+
if (!correct) throw new Error(`Applied BMAD projection link differs from plan: ${name}`);
|
|
3355
|
+
}
|
|
3356
|
+
const finalManifestStat = lstatIfPresent(manifestPath);
|
|
3357
|
+
if (!finalManifestStat || finalManifestStat.isSymbolicLink() || !finalManifestStat.isFile() || (Number(finalManifestStat.mode) & 511) !== manifestMode || readFileSync8(manifestPath).toString("utf8") !== nextManifest) {
|
|
3358
|
+
throw new Error("Applied BMAD skills manifest differs from planned bytes or mode");
|
|
3359
|
+
}
|
|
3360
|
+
const finalManifest = JSON.parse(readFileSync8(manifestPath, "utf8"));
|
|
3361
|
+
if (finalManifest.$schema !== "https://raw.githubusercontent.com/skillex/schemas/main/skills.schema.json" || finalManifest.inherit_global !== true || finalManifest.registry !== SKILLS_REGISTRY_URL || !Array.isArray(finalManifest.skills)) {
|
|
3362
|
+
throw new Error("Applied BMAD skills manifest schema differs from plan");
|
|
3363
|
+
}
|
|
3364
|
+
} catch (error) {
|
|
3365
|
+
try {
|
|
3366
|
+
rollback();
|
|
3367
|
+
} catch (rollbackError) {
|
|
3368
|
+
return { ok: false, changedFiles: [], error: `BMAD provisioning failed (${String(error)}); ${String(rollbackError)}` };
|
|
3369
|
+
}
|
|
3370
|
+
return { ok: false, changedFiles: [], error: error instanceof Error ? error.message : String(error) };
|
|
3371
|
+
}
|
|
3372
|
+
rmSync(transaction, { recursive: true });
|
|
3373
|
+
return { ok: true, changedFiles };
|
|
3374
|
+
}
|
|
2781
3375
|
function templateVersionFilesConf(ctx, repoRoot) {
|
|
2782
|
-
const packageJson =
|
|
3376
|
+
const packageJson = join15(repoRoot, "package.json");
|
|
2783
3377
|
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";
|
|
2784
3378
|
}
|
|
2785
3379
|
function replaceOrAppendManagedBlock(text2, startMarker, block, beforePattern) {
|
|
@@ -2804,7 +3398,7 @@ var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/herm
|
|
|
2804
3398
|
function requiredMisePathEntries(ctx) {
|
|
2805
3399
|
const required = [...BASE_MISE_PATH_ENTRIES];
|
|
2806
3400
|
for (const candidate of CONDITIONAL_HERMES_PATHS) {
|
|
2807
|
-
if (existsSync10(
|
|
3401
|
+
if (existsSync10(join15(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
|
|
2808
3402
|
}
|
|
2809
3403
|
return required;
|
|
2810
3404
|
}
|
|
@@ -2906,6 +3500,9 @@ function isManagedHookEntry(value) {
|
|
|
2906
3500
|
const trimmed = value.trim();
|
|
2907
3501
|
if (trimmed === OP_INJECT_SCRIPT) return true;
|
|
2908
3502
|
if (trimmed === SYNC_SKILLS_SCRIPT) return true;
|
|
3503
|
+
if (trimmed === PROVISION_BMAD_SKILLS_SCRIPT) return true;
|
|
3504
|
+
if (/sync-skills(?:\.py)?["']?\s+--scope project/.test(trimmed)) return true;
|
|
3505
|
+
if (/provision-bmad-skills\.py/.test(trimmed)) return true;
|
|
2909
3506
|
if (/link-project-skills-to-clis\.sh'?\s*$/.test(trimmed)) return true;
|
|
2910
3507
|
if (/unlink-project-skills-from-clis\.sh'?\s*$/.test(trimmed)) return true;
|
|
2911
3508
|
return /link-agentfiles\.sh'?\s*$/.test(trimmed);
|
|
@@ -3011,6 +3608,7 @@ function upsertLinkAgentfilesBlock(text2, ctx) {
|
|
|
3011
3608
|
const withPath = upsertMisePath(text2, requiredMisePathEntries(ctx));
|
|
3012
3609
|
let cleaned = removeTomlSection(withPath, /^\[tasks\.link-agentfiles\]$/, /link-agentfiles/, { includePrecedingComments: false });
|
|
3013
3610
|
cleaned = removeTomlSection(cleaned, /^\[tasks\.skills-sync\]$/, void 0, { includePrecedingComments: false });
|
|
3611
|
+
cleaned = removeTomlSection(cleaned, /^\[tasks\.skills-provision-bmad\]$/, void 0, { includePrecedingComments: false });
|
|
3014
3612
|
cleaned = removeTomlSection(cleaned, /^\[tasks\.link-project-skills-to-clis\]$/, void 0, { includePrecedingComments: false });
|
|
3015
3613
|
cleaned = removeTomlSection(cleaned, /^\[tasks\.unlink-project-skills-from-clis\]$/, void 0, { includePrecedingComments: false });
|
|
3016
3614
|
cleaned = removeTomlSection(cleaned, /^\[tasks\.skills-relink\]$/, void 0, { includePrecedingComments: false });
|
|
@@ -3020,7 +3618,7 @@ function upsertLinkAgentfilesBlock(text2, ctx) {
|
|
|
3020
3618
|
return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
|
|
3021
3619
|
}
|
|
3022
3620
|
function readProjectJson(ctx) {
|
|
3023
|
-
return tryParseJson(safeReadText(
|
|
3621
|
+
return tryParseJson(safeReadText(join15(ctx.repoRoot, ".project.json")));
|
|
3024
3622
|
}
|
|
3025
3623
|
function boolSetting(value, fallback) {
|
|
3026
3624
|
if (typeof value === "boolean") return value;
|
|
@@ -3058,7 +3656,7 @@ function canonicalProjectJson(ctx) {
|
|
|
3058
3656
|
role.agentId || `${slug}-${role.role}`,
|
|
3059
3657
|
{
|
|
3060
3658
|
role: role.role,
|
|
3061
|
-
role_dir:
|
|
3659
|
+
role_dir: relative2(ctx.repoRoot, role.roleDir)
|
|
3062
3660
|
}
|
|
3063
3661
|
])
|
|
3064
3662
|
);
|
|
@@ -3095,8 +3693,8 @@ function canonicalProjectJson(ctx) {
|
|
|
3095
3693
|
};
|
|
3096
3694
|
}
|
|
3097
3695
|
function projectJsonFinding(ctx) {
|
|
3098
|
-
const projectPath =
|
|
3099
|
-
const planeJsonPath =
|
|
3696
|
+
const projectPath = join15(ctx.repoRoot, ".project.json");
|
|
3697
|
+
const planeJsonPath = join15(ctx.repoRoot, ".plane.json");
|
|
3100
3698
|
const details = [];
|
|
3101
3699
|
const data = readProjectJson(ctx);
|
|
3102
3700
|
const roles = discoverRoles(ctx.repoRoot);
|
|
@@ -3118,8 +3716,8 @@ function projectJsonFinding(ctx) {
|
|
|
3118
3716
|
continue;
|
|
3119
3717
|
}
|
|
3120
3718
|
if (agent.role !== role.role) details.push(`agents.${role.agentId}.role should be ${role.role}`);
|
|
3121
|
-
if (agent.role_dir !==
|
|
3122
|
-
details.push(`agents.${role.agentId}.role_dir should be ${
|
|
3719
|
+
if (agent.role_dir !== relative2(ctx.repoRoot, role.roleDir)) {
|
|
3720
|
+
details.push(`agents.${role.agentId}.role_dir should be ${relative2(ctx.repoRoot, role.roleDir)}`);
|
|
3123
3721
|
}
|
|
3124
3722
|
}
|
|
3125
3723
|
const ticketProvider = data.ticket_provider ?? {};
|
|
@@ -3148,8 +3746,7 @@ function projectJsonFinding(ctx) {
|
|
|
3148
3746
|
function renderSoul(role) {
|
|
3149
3747
|
const telegram = role.botHandle ? `@${role.botHandle}` : "(unwired)";
|
|
3150
3748
|
const tone = role.role === "pm" ? `Direct and brief. Decision-forward. No throat-clearing, no apologies, no "I'll help you with that" preambles.` : "Direct and brief.";
|
|
3151
|
-
const roleSpecific = role.role === "pm" ? `You are the project manager. You triage incoming work, create or refine tickets, and delegate implementation. You do not ship product code. A systemd heartbeat
|
|
3152
|
-
const runtimeOwner = role.runtimeOwner || "delorenj";
|
|
3749
|
+
const roleSpecific = role.role === "pm" ? `You are the project manager. You triage incoming work, create or refine tickets, and delegate implementation. You do not ship product code. A systemd heartbeat checks runtime health; when this repo opts into reconciliation (\`automation.reconcile.enabled\` in repo-root \`.project.json\`), the same heartbeat also runs your continuous board-reconciliation pass out-of-band (\`.scripts/sentinel.prompt.md\`, \`--source cron\`), kept separate from your interactive session memory.` : `You operate as the ${role.role} agent for this repo.`;
|
|
3153
3750
|
return `# ${role.displayName || role.agentId}
|
|
3154
3751
|
|
|
3155
3752
|
You are **${role.displayName || role.agentId}** \u2014 a Hermes agent provisioned to work inside the
|
|
@@ -3168,7 +3765,7 @@ You are **${role.displayName || role.agentId}** \u2014 a Hermes agent provisione
|
|
|
3168
3765
|
|
|
3169
3766
|
## Scope
|
|
3170
3767
|
|
|
3171
|
-
You operate only within the working directory of \`${role.repo}\`. Your HERMES_HOME is the
|
|
3768
|
+
You operate only within the working directory of \`${role.repo}\`. Your HERMES_HOME is the ignored local directory at \`./runtime/\`, which \`~/.hermes/profiles/${role.profileName || role.agentId}\` projects into (so \`--profile\` invocations resolve here too). Secrets, SOUL, memories, skills, sessions, gateway state, and runtime files stay local to that runtime and are never project gitlinks.
|
|
3172
3769
|
|
|
3173
3770
|
## Tone
|
|
3174
3771
|
|
|
@@ -3180,12 +3777,12 @@ ${roleSpecific}
|
|
|
3180
3777
|
|
|
3181
3778
|
## Memory hygiene
|
|
3182
3779
|
|
|
3183
|
-
Your memory is
|
|
3780
|
+
Your memory is stored locally at \`./runtime/memories/\`. Use durable memory deliberately and keep \`memories/MEMORY.md\` current.
|
|
3184
3781
|
`;
|
|
3185
3782
|
}
|
|
3186
3783
|
function renderHermesWrapper(role) {
|
|
3187
3784
|
return `#!/usr/bin/env bash
|
|
3188
|
-
# Launcher for ${role.agentId}. Resolves HERMES_HOME to the runtime
|
|
3785
|
+
# Launcher for ${role.agentId}. Resolves HERMES_HOME to the local runtime.
|
|
3189
3786
|
|
|
3190
3787
|
set -euo pipefail
|
|
3191
3788
|
|
|
@@ -3198,30 +3795,44 @@ if [[ -f "$FLEET_ENV" ]]; then
|
|
|
3198
3795
|
source "$FLEET_ENV"
|
|
3199
3796
|
fi
|
|
3200
3797
|
|
|
3201
|
-
HERMES_BIN="{HERMES_BIN:-{HERMES_FLEET_BIN
|
|
3202
|
-
HERMES_OAUTH_FILE="{HERMES_OAUTH_FILE:-{HERMES_FLEET_OAUTH_FILE:-$HOME/.hermes/auth.json}}"
|
|
3798
|
+
HERMES_BIN="{HERMES_BIN:-{HERMES_FLEET_BIN:-$HOME/.hermes/hermes-agent/.venv/bin/hermes}}"
|
|
3203
3799
|
CODEX_HOME="{CODEX_HOME:-{HERMES_FLEET_CODEX_HOME:-$HOME/.codex}}"
|
|
3204
3800
|
|
|
3205
3801
|
FLEET_HOME="{HERMES_FLEET_HOME:-$HOME/.hermes}"
|
|
3206
3802
|
PROFILE_NAME="{HERMES_PROFILE_NAME:-${role.profileName || role.agentId}}"
|
|
3207
|
-
|
|
3803
|
+
|
|
3804
|
+
# Singleton-runtime contract: HERMES_HOME MUST be the named profile dir, never
|
|
3805
|
+
# the raw runtime path. Hermes treats any HERMES_HOME that is neither under
|
|
3806
|
+
# ~/.hermes nor a child of a "profiles" dir as its own standalone root, which
|
|
3807
|
+
# makes get_active_profile_name() report "default" and _global_auth_file_path()
|
|
3808
|
+
# return None -- silently disabling shared fleet auth and giving every agent a
|
|
3809
|
+
# divergent config.yaml. The profile dir is a REAL dir whose shared entries
|
|
3810
|
+
# (config.yaml, .env, skills) symlink to the fleet root and whose person-owned
|
|
3811
|
+
# entries (memories, sessions, state.db, workspace) symlink into $RUNTIME_HOME.
|
|
3812
|
+
HERMES_HOME="$FLEET_HOME/profiles/$PROFILE_NAME"
|
|
3208
3813
|
|
|
3209
3814
|
if [[ ! -d "$RUNTIME_HOME" ]]; then
|
|
3210
|
-
echo "hermes: runtime
|
|
3211
|
-
echo " fix:
|
|
3815
|
+
echo "hermes: local runtime not provisioned at $RUNTIME_HOME" >&2
|
|
3816
|
+
echo " fix: run the role's .scripts/20-runtime-repo.sh" >&2
|
|
3817
|
+
exit 1
|
|
3818
|
+
fi
|
|
3819
|
+
|
|
3820
|
+
if [[ ! -d "$HERMES_HOME" ]]; then
|
|
3821
|
+
echo "hermes: profile not provisioned at $HERMES_HOME" >&2
|
|
3822
|
+
echo " fix: pj migrate hermes.runtime-singleton" >&2
|
|
3212
3823
|
exit 1
|
|
3213
3824
|
fi
|
|
3214
3825
|
|
|
3215
|
-
exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV"
|
|
3826
|
+
exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" CODEX_HOME="$CODEX_HOME" "$HERMES_BIN" "$@"
|
|
3216
3827
|
`.replace(/\u0010/g, "$");
|
|
3217
3828
|
}
|
|
3218
3829
|
function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
|
|
3219
3830
|
if (!existsSync10(sourceDir)) return;
|
|
3220
3831
|
mkdirSync6(targetDir, { recursive: true });
|
|
3221
|
-
for (const entry of
|
|
3222
|
-
const sourcePath =
|
|
3832
|
+
for (const entry of readdirSync3(sourceDir, { withFileTypes: true })) {
|
|
3833
|
+
const sourcePath = join15(sourceDir, entry.name);
|
|
3223
3834
|
if (skip?.(sourcePath)) continue;
|
|
3224
|
-
const targetPath =
|
|
3835
|
+
const targetPath = join15(targetDir, entry.name);
|
|
3225
3836
|
if (entry.isDirectory()) {
|
|
3226
3837
|
copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
|
|
3227
3838
|
continue;
|
|
@@ -3234,21 +3845,78 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
|
|
|
3234
3845
|
}
|
|
3235
3846
|
}
|
|
3236
3847
|
}
|
|
3237
|
-
function
|
|
3238
|
-
const
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3848
|
+
function runtimeSubmodulePath(repoRoot, role) {
|
|
3849
|
+
const rolePath = relative2(repoRoot, role.roleDir).replace(/\\/g, "/");
|
|
3850
|
+
if (!/^agents\/hermes\/[^/]+$/.test(rolePath)) return null;
|
|
3851
|
+
return `${rolePath}/runtime`;
|
|
3852
|
+
}
|
|
3853
|
+
function submoduleSectionHasPath(section, targetPath) {
|
|
3854
|
+
return section.split(/\r?\n/).some((line) => /^\s*path\s*=/.test(line) && line.replace(/^\s*path\s*=\s*/, "").trim() === targetPath);
|
|
3855
|
+
}
|
|
3856
|
+
function hasRuntimeSubmoduleMapping(repoRoot, role) {
|
|
3857
|
+
const gitmodulesPath = join15(repoRoot, ".gitmodules");
|
|
3858
|
+
const current = safeReadText(gitmodulesPath) ?? "";
|
|
3859
|
+
const sections = current.match(/^\[submodule "[^"\n]+"\][\s\S]*?(?=^\[submodule "|(?![\s\S]))/gm) ?? [];
|
|
3860
|
+
const targetPath = runtimeSubmodulePath(repoRoot, role);
|
|
3861
|
+
return Boolean(targetPath && sections.some((section) => submoduleSectionHasPath(section, targetPath)));
|
|
3862
|
+
}
|
|
3863
|
+
function removeRuntimeSubmoduleMapping(repoRoot, role, changedFiles, dryRun) {
|
|
3864
|
+
const gitmodulesPath = join15(repoRoot, ".gitmodules");
|
|
3245
3865
|
const current = safeReadText(gitmodulesPath) ?? "";
|
|
3246
|
-
|
|
3247
|
-
|
|
3866
|
+
if (!hasRuntimeSubmoduleMapping(repoRoot, role)) return [];
|
|
3867
|
+
const targetPath = runtimeSubmodulePath(repoRoot, role);
|
|
3868
|
+
if (!targetPath) return [];
|
|
3869
|
+
const next = current.replace(/^\[submodule "[^"\n]+"\][\s\S]*?(?=^\[submodule "|(?![\s\S]))/gm, (section) => submoduleSectionHasPath(section, targetPath) ? "" : section).replace(/\n{3,}/g, "\n\n").trim();
|
|
3248
3870
|
changedFiles.push(gitmodulesPath);
|
|
3249
|
-
if (!dryRun) writeText(gitmodulesPath,
|
|
3871
|
+
if (!dryRun) writeText(gitmodulesPath, next ? `${next}
|
|
3872
|
+
` : "");
|
|
3250
3873
|
return [gitmodulesPath];
|
|
3251
3874
|
}
|
|
3875
|
+
function retireRuntimeSubmodule(repoRoot, role, changedFiles, dryRun) {
|
|
3876
|
+
const runtimePath = runtimeSubmodulePath(repoRoot, role);
|
|
3877
|
+
if (!runtimePath) {
|
|
3878
|
+
return { ok: false, details: [], error: `refusing unsafe runtime path for ${role.roleDir}` };
|
|
3879
|
+
}
|
|
3880
|
+
const probe = spawnSync6("git", ["ls-files", "--stage", "--", runtimePath], {
|
|
3881
|
+
cwd: repoRoot,
|
|
3882
|
+
encoding: "utf8"
|
|
3883
|
+
});
|
|
3884
|
+
if (probe.status !== 0) {
|
|
3885
|
+
return { ok: false, details: [], error: `failed to inspect runtime index at ${runtimePath}: ${probe.stderr.trim() || `exit ${probe.status}`}` };
|
|
3886
|
+
}
|
|
3887
|
+
const details = [];
|
|
3888
|
+
if (probe.stdout.trim()) {
|
|
3889
|
+
details.push(`untrack ${runtimePath}`);
|
|
3890
|
+
if (dryRun) {
|
|
3891
|
+
changedFiles.push(runtimePath);
|
|
3892
|
+
} else {
|
|
3893
|
+
const removal = spawnSync6("git", ["rm", "--cached", "-r", "-f", "--", runtimePath], {
|
|
3894
|
+
cwd: repoRoot,
|
|
3895
|
+
encoding: "utf8"
|
|
3896
|
+
});
|
|
3897
|
+
if (removal.status !== 0) {
|
|
3898
|
+
return { ok: false, details, error: `failed to untrack ${runtimePath}: ${removal.stderr.trim() || `exit ${removal.status}`}` };
|
|
3899
|
+
}
|
|
3900
|
+
const verification = spawnSync6("git", ["ls-files", "--stage", "--", runtimePath], {
|
|
3901
|
+
cwd: repoRoot,
|
|
3902
|
+
encoding: "utf8"
|
|
3903
|
+
});
|
|
3904
|
+
if (verification.status !== 0 || verification.stdout.trim()) {
|
|
3905
|
+
return {
|
|
3906
|
+
ok: false,
|
|
3907
|
+
details,
|
|
3908
|
+
error: verification.status !== 0 ? `failed to verify untracked runtime ${runtimePath}: ${verification.stderr.trim() || `exit ${verification.status}`}` : `runtime remains tracked after index-only removal: ${runtimePath}`
|
|
3909
|
+
};
|
|
3910
|
+
}
|
|
3911
|
+
changedFiles.push(runtimePath);
|
|
3912
|
+
}
|
|
3913
|
+
}
|
|
3914
|
+
if (hasRuntimeSubmoduleMapping(repoRoot, role)) {
|
|
3915
|
+
details.push(`remove stale .gitmodules mapping for ${runtimePath}`);
|
|
3916
|
+
removeRuntimeSubmoduleMapping(repoRoot, role, changedFiles, dryRun);
|
|
3917
|
+
}
|
|
3918
|
+
return { ok: true, details };
|
|
3919
|
+
}
|
|
3252
3920
|
function upsertRegistryEntry(role, homeDir, changedFiles, dryRun) {
|
|
3253
3921
|
const path = registryPath(homeDir);
|
|
3254
3922
|
const current = safeReadText(path) ?? "# Hermes agent fleet registry.\n# One entry per provisioned agent. Managed by hermes-agent-template/.scripts/80-registry.sh.\nschema_version: 1\nagents: {}\n";
|
|
@@ -3267,9 +3935,11 @@ function upsertRegistryEntry(role, homeDir, changedFiles, dryRun) {
|
|
|
3267
3935
|
project_id: ${ctxEscape(role.ticketProviderBoardId)}
|
|
3268
3936
|
identifier: ${ctxEscape(role.ticketProviderIdentifier)}
|
|
3269
3937
|
runtime_repo: ${ctxEscape(role.runtimeRepo)}
|
|
3938
|
+
bloodbank:
|
|
3939
|
+
gateway_scope: fleet
|
|
3940
|
+
target_agent_id: ${role.agentId}
|
|
3270
3941
|
systemd:
|
|
3271
3942
|
gateway_unit: hermes-${role.agentId}-gateway.service
|
|
3272
|
-
consumer_unit: hermes-${role.agentId}-consumer.service
|
|
3273
3943
|
heartbeat_timer: hermes-${role.agentId}-heartbeat.timer
|
|
3274
3944
|
`;
|
|
3275
3945
|
const next = current.includes("agents: {}") ? current.replace("agents: {}", `agents:
|
|
@@ -3333,6 +4003,7 @@ function checkUnit(unit) {
|
|
|
3333
4003
|
var BMAD_NPM_PACKAGE = "bmad-method";
|
|
3334
4004
|
var BMAD_TARGET_CHANNEL = "next";
|
|
3335
4005
|
var BMAD_DIST_TAGS_TTL_MS = 60 * 60 * 1e3;
|
|
4006
|
+
var DEFAULT_BMAD_MODULES = ["bmm", "bmb", "cis"];
|
|
3336
4007
|
var BMAD_INSTALL_TOOLS = [
|
|
3337
4008
|
"claude-code",
|
|
3338
4009
|
"codex",
|
|
@@ -3380,7 +4051,53 @@ var BMAD_INSTALL_TOOLS = [
|
|
|
3380
4051
|
"windsurf",
|
|
3381
4052
|
"zencoder"
|
|
3382
4053
|
];
|
|
3383
|
-
function
|
|
4054
|
+
function manifestBmadModules(repoRoot) {
|
|
4055
|
+
const manifestPath = join15(repoRoot, "_bmad", "_config", "manifest.yaml");
|
|
4056
|
+
const raw = safeReadText(manifestPath);
|
|
4057
|
+
if (raw === null) return { status: "absent" };
|
|
4058
|
+
try {
|
|
4059
|
+
const parsed = YAML3.parse(raw);
|
|
4060
|
+
if (!Array.isArray(parsed?.modules)) {
|
|
4061
|
+
return { status: "invalid", error: `${manifestPath} must define a modules array` };
|
|
4062
|
+
}
|
|
4063
|
+
const declared = [];
|
|
4064
|
+
for (const entry of parsed.modules) {
|
|
4065
|
+
const name = typeof entry === "string" ? entry : entry && typeof entry === "object" ? entry.name : void 0;
|
|
4066
|
+
if (typeof name !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(name)) {
|
|
4067
|
+
return { status: "invalid", error: `${manifestPath} contains an invalid module entry` };
|
|
4068
|
+
}
|
|
4069
|
+
if (name !== "core" && name !== "custom") declared.push(name);
|
|
4070
|
+
}
|
|
4071
|
+
return { status: "valid", modules: Array.from(new Set(declared)) };
|
|
4072
|
+
} catch (error) {
|
|
4073
|
+
return {
|
|
4074
|
+
status: "invalid",
|
|
4075
|
+
error: `Could not parse ${manifestPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
4076
|
+
};
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4079
|
+
function configuredBmadModules(repoRoot) {
|
|
4080
|
+
const raw = safeReadText(join15(repoRoot, "_bmad", "config.toml"));
|
|
4081
|
+
if (raw === null) return void 0;
|
|
4082
|
+
const modules = [...raw.matchAll(/^\[modules\.([A-Za-z0-9][A-Za-z0-9_-]*)\]\s*$/gm)].map((match) => match[1]);
|
|
4083
|
+
return Array.from(new Set(modules));
|
|
4084
|
+
}
|
|
4085
|
+
function selectedBmadModules(repoRoot) {
|
|
4086
|
+
const manifest = manifestBmadModules(repoRoot);
|
|
4087
|
+
if (manifest.status === "valid") return manifest.modules;
|
|
4088
|
+
if (manifest.status === "invalid") throw new Error(manifest.error);
|
|
4089
|
+
return configuredBmadModules(repoRoot) ?? [...DEFAULT_BMAD_MODULES];
|
|
4090
|
+
}
|
|
4091
|
+
function requiredBmadSentinels(repoRoot, modules = selectedBmadModules(repoRoot)) {
|
|
4092
|
+
return [
|
|
4093
|
+
join15("core", "config.yaml"),
|
|
4094
|
+
join15("config.toml"),
|
|
4095
|
+
join15("_config", "manifest.yaml"),
|
|
4096
|
+
...modules.map((module) => join15(module, "config.yaml"))
|
|
4097
|
+
];
|
|
4098
|
+
}
|
|
4099
|
+
function bmadInstallArgs(repoRoot, modules = selectedBmadModules(repoRoot)) {
|
|
4100
|
+
const installerModules = modules.length ? modules.join(",") : "core";
|
|
3384
4101
|
return [
|
|
3385
4102
|
"-y",
|
|
3386
4103
|
`${BMAD_NPM_PACKAGE}@${BMAD_TARGET_CHANNEL}`,
|
|
@@ -3389,20 +4106,20 @@ function bmadInstallArgs(repoRoot) {
|
|
|
3389
4106
|
"--directory",
|
|
3390
4107
|
repoRoot,
|
|
3391
4108
|
"--modules",
|
|
3392
|
-
|
|
4109
|
+
installerModules,
|
|
3393
4110
|
"--tools",
|
|
3394
4111
|
BMAD_INSTALL_TOOLS.join(",")
|
|
3395
4112
|
];
|
|
3396
4113
|
}
|
|
3397
|
-
function runBmadInstall(repoRoot) {
|
|
3398
|
-
const result = spawnSync6("npx", bmadInstallArgs(repoRoot), { encoding: "utf8" });
|
|
4114
|
+
function runBmadInstall(repoRoot, modules = selectedBmadModules(repoRoot)) {
|
|
4115
|
+
const result = spawnSync6("npx", bmadInstallArgs(repoRoot, modules), { encoding: "utf8" });
|
|
3399
4116
|
if (result.status !== 0) {
|
|
3400
4117
|
return { ok: false, error: result.stderr || result.error?.message || "Unknown error" };
|
|
3401
4118
|
}
|
|
3402
4119
|
return { ok: true };
|
|
3403
4120
|
}
|
|
3404
4121
|
function readInstalledBmadVersion(repoRoot) {
|
|
3405
|
-
const raw = safeReadText(
|
|
4122
|
+
const raw = safeReadText(join15(repoRoot, "_bmad", "_config", "manifest.yaml"));
|
|
3406
4123
|
if (!raw) return void 0;
|
|
3407
4124
|
try {
|
|
3408
4125
|
const parsed = YAML3.parse(raw);
|
|
@@ -3413,8 +4130,8 @@ function readInstalledBmadVersion(repoRoot) {
|
|
|
3413
4130
|
}
|
|
3414
4131
|
}
|
|
3415
4132
|
function bmadCachePath(homeDir) {
|
|
3416
|
-
const cacheRoot = process.env.XDG_CACHE_HOME?.trim() ||
|
|
3417
|
-
return
|
|
4133
|
+
const cacheRoot = process.env.XDG_CACHE_HOME?.trim() || join15(homeDir, ".cache");
|
|
4134
|
+
return join15(cacheRoot, "pjangler", "bmad-dist-tags.json");
|
|
3418
4135
|
}
|
|
3419
4136
|
function readBmadDistTagsCache(homeDir) {
|
|
3420
4137
|
const raw = safeReadText(bmadCachePath(homeDir));
|
|
@@ -3497,18 +4214,232 @@ function compareBmadVersions(a, b) {
|
|
|
3497
4214
|
}
|
|
3498
4215
|
return 0;
|
|
3499
4216
|
}
|
|
4217
|
+
var SHARED_PROFILE_ENTRIES = ["config.yaml", ".env", "skills"];
|
|
4218
|
+
var OWNED_PROFILE_ENTRIES = [
|
|
4219
|
+
"memories",
|
|
4220
|
+
"sessions",
|
|
4221
|
+
"workspace",
|
|
4222
|
+
"logs",
|
|
4223
|
+
"cron",
|
|
4224
|
+
"plans",
|
|
4225
|
+
"hooks",
|
|
4226
|
+
"pairing",
|
|
4227
|
+
"audio_cache",
|
|
4228
|
+
"image_cache"
|
|
4229
|
+
];
|
|
4230
|
+
var OWNED_PROFILE_FILES = ["SOUL.md", "state.db", "kanban.db"];
|
|
4231
|
+
function fleetHome(ctx) {
|
|
4232
|
+
return process.env.HERMES_FLEET_HOME || join15(ctx.homeDir, ".hermes");
|
|
4233
|
+
}
|
|
4234
|
+
function fleetBinPath(ctx) {
|
|
4235
|
+
const candidates = [
|
|
4236
|
+
process.env.HERMES_FLEET_BIN,
|
|
4237
|
+
join15(fleetHome(ctx), "hermes-agent", ".venv", "bin", "hermes"),
|
|
4238
|
+
join15(fleetHome(ctx), "hermes-agent", "venv", "bin", "hermes"),
|
|
4239
|
+
join15(ctx.homeDir, ".local", "bin", "hermes")
|
|
4240
|
+
].filter(Boolean);
|
|
4241
|
+
return candidates.find((candidate) => existsSync10(candidate)) ?? "";
|
|
4242
|
+
}
|
|
4243
|
+
function singletonPlan(ctx, role) {
|
|
4244
|
+
const fleetRoot = fleetHome(ctx);
|
|
4245
|
+
const profileName = role.profileName || role.agentId;
|
|
4246
|
+
const profileDir = join15(fleetRoot, "profiles", profileName);
|
|
4247
|
+
const runtimeDir = join15(role.roleDir, "runtime");
|
|
4248
|
+
const links = [];
|
|
4249
|
+
for (const entry of SHARED_PROFILE_ENTRIES) {
|
|
4250
|
+
links.push({ path: join15(profileDir, entry), target: join15(fleetRoot, entry), ensureTargetDir: entry === "skills" });
|
|
4251
|
+
}
|
|
4252
|
+
for (const entry of OWNED_PROFILE_ENTRIES) {
|
|
4253
|
+
links.push({ path: join15(profileDir, entry), target: join15(runtimeDir, entry), ensureTargetDir: true });
|
|
4254
|
+
}
|
|
4255
|
+
for (const entry of OWNED_PROFILE_FILES) {
|
|
4256
|
+
links.push({ path: join15(profileDir, entry), target: join15(runtimeDir, entry), ensureTargetDir: false });
|
|
4257
|
+
}
|
|
4258
|
+
const sharedSeeds = ["config.yaml", "auth.json", ".env"].map((entry) => ({
|
|
4259
|
+
rootPath: join15(fleetRoot, entry),
|
|
4260
|
+
runtimePath: join15(runtimeDir, entry)
|
|
4261
|
+
}));
|
|
4262
|
+
return { fleetRoot, profileDir, runtimeDir, links, sharedSeeds };
|
|
4263
|
+
}
|
|
4264
|
+
function isDanglingLink(path) {
|
|
4265
|
+
try {
|
|
4266
|
+
return lstatSync2(path).isSymbolicLink() && !existsSync10(path);
|
|
4267
|
+
} catch {
|
|
4268
|
+
return false;
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
4271
|
+
function linkState(path, target) {
|
|
4272
|
+
let stat;
|
|
4273
|
+
try {
|
|
4274
|
+
stat = lstatSync2(path);
|
|
4275
|
+
} catch {
|
|
4276
|
+
return "missing";
|
|
4277
|
+
}
|
|
4278
|
+
if (!stat.isSymbolicLink()) return "not-a-symlink";
|
|
4279
|
+
try {
|
|
4280
|
+
return readlinkSync(path) === target ? "ok" : "wrong-target";
|
|
4281
|
+
} catch {
|
|
4282
|
+
return "wrong-target";
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
function realOrSelf(path) {
|
|
4286
|
+
try {
|
|
4287
|
+
return realpathSync(path);
|
|
4288
|
+
} catch {
|
|
4289
|
+
return path;
|
|
4290
|
+
}
|
|
4291
|
+
}
|
|
4292
|
+
function profileUnits(role) {
|
|
4293
|
+
return [
|
|
4294
|
+
`hermes-${role.agentId}-gateway.service`,
|
|
4295
|
+
`hermes-${role.agentId}-heartbeat.service`,
|
|
4296
|
+
`hermes-${role.agentId}-heartbeat.timer`,
|
|
4297
|
+
`hermes-${role.agentId}-checkpoint.service`
|
|
4298
|
+
];
|
|
4299
|
+
}
|
|
4300
|
+
function readRegistry(registryPath2) {
|
|
4301
|
+
const raw = safeReadText(registryPath2);
|
|
4302
|
+
if (raw === null) return null;
|
|
4303
|
+
try {
|
|
4304
|
+
const doc = YAML3.parse(raw);
|
|
4305
|
+
return doc?.agents ?? {};
|
|
4306
|
+
} catch {
|
|
4307
|
+
return null;
|
|
4308
|
+
}
|
|
4309
|
+
}
|
|
4310
|
+
function declaredAgentIds(repoRoot) {
|
|
4311
|
+
return declaredAgentEntries(repoRoot).map(([agentId]) => agentId);
|
|
4312
|
+
}
|
|
4313
|
+
function declaredAgentEntries(repoRoot) {
|
|
4314
|
+
const raw = safeReadText(join15(repoRoot, ".project.json"));
|
|
4315
|
+
if (raw === null) return [];
|
|
4316
|
+
try {
|
|
4317
|
+
const doc = JSON.parse(raw);
|
|
4318
|
+
return Object.entries(doc.agents ?? {}).map(([agentId, entry]) => [agentId, entry ?? {}]);
|
|
4319
|
+
} catch {
|
|
4320
|
+
return [];
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
4323
|
+
function ownedRegistryEntries(registry, repoRoot) {
|
|
4324
|
+
const want = realOrSelf(repoRoot);
|
|
4325
|
+
const owned = [];
|
|
4326
|
+
for (const [agentId, raw] of Object.entries(registry)) {
|
|
4327
|
+
const entry = raw ?? {};
|
|
4328
|
+
const roleDir = String(entry.role_dir ?? "");
|
|
4329
|
+
if (!roleDir) continue;
|
|
4330
|
+
if (realOrSelf(dirname7(dirname7(dirname7(roleDir)))) !== want) continue;
|
|
4331
|
+
owned.push([agentId, entry]);
|
|
4332
|
+
}
|
|
4333
|
+
return owned;
|
|
4334
|
+
}
|
|
4335
|
+
function unprovisionedRoleAgents(registry, repoRoot, canonical) {
|
|
4336
|
+
const blockers = /* @__PURE__ */ new Map();
|
|
4337
|
+
const record = (agentId, roleDir, source) => {
|
|
4338
|
+
const current = blockers.get(agentId) ?? { roleDir, sources: /* @__PURE__ */ new Set() };
|
|
4339
|
+
if (!current.roleDir && roleDir) current.roleDir = roleDir;
|
|
4340
|
+
current.sources.add(source);
|
|
4341
|
+
blockers.set(agentId, current);
|
|
4342
|
+
};
|
|
4343
|
+
for (const [agentId, entry] of ownedRegistryEntries(registry, repoRoot)) {
|
|
4344
|
+
if (canonical.has(agentId)) continue;
|
|
4345
|
+
const roleDir = String(entry.role_dir ?? "");
|
|
4346
|
+
if (!roleDir || !existsSync10(join15(roleDir, "role.yaml"))) record(agentId, roleDir, "registry");
|
|
4347
|
+
}
|
|
4348
|
+
for (const [agentId, entry] of declaredAgentEntries(repoRoot)) {
|
|
4349
|
+
if (canonical.has(agentId)) continue;
|
|
4350
|
+
const configured = String(entry.role_dir ?? "");
|
|
4351
|
+
const roleDir = configured ? resolve3(repoRoot, configured) : "";
|
|
4352
|
+
if (!roleDir || !existsSync10(join15(roleDir, "role.yaml"))) record(agentId, roleDir, ".project.json");
|
|
4353
|
+
}
|
|
4354
|
+
return [...blockers.entries()].map(([agentId, value]) => ({
|
|
4355
|
+
agentId,
|
|
4356
|
+
roleDir: value.roleDir,
|
|
4357
|
+
sources: [...value.sources]
|
|
4358
|
+
}));
|
|
4359
|
+
}
|
|
4360
|
+
function dropDeclaredAgent(ctx, agentId, changedFiles, details) {
|
|
4361
|
+
const path = join15(ctx.repoRoot, ".project.json");
|
|
4362
|
+
const raw = safeReadText(path);
|
|
4363
|
+
if (raw === null) return;
|
|
4364
|
+
let doc;
|
|
4365
|
+
try {
|
|
4366
|
+
doc = JSON.parse(raw);
|
|
4367
|
+
} catch {
|
|
4368
|
+
return;
|
|
4369
|
+
}
|
|
4370
|
+
if (!doc.agents || !(agentId in doc.agents)) return;
|
|
4371
|
+
delete doc.agents[agentId];
|
|
4372
|
+
details.push(`drop agent "${agentId}" from .project.json`);
|
|
4373
|
+
changedFiles.push(path);
|
|
4374
|
+
if (!ctx.dryRun) writeText(path, `${JSON.stringify(doc, null, 2)}
|
|
4375
|
+
`);
|
|
4376
|
+
}
|
|
4377
|
+
function rewriteLauncher(text2) {
|
|
4378
|
+
let next = text2.replace(/^HERMES_HOME="\$RUNTIME_HOME"\s*$/m, 'HERMES_HOME="$FLEET_HOME/profiles/$PROFILE_NAME"');
|
|
4379
|
+
next = next.replace(/^HERMES_OAUTH_FILE=.*\n/m, "");
|
|
4380
|
+
next = next.replace(/\s*HERMES_OAUTH_FILE="\$HERMES_OAUTH_FILE"/g, "");
|
|
4381
|
+
next = next.replace(
|
|
4382
|
+
/^.*\/home\/delorenj\/code\/hermes-agent\/\.venv\/bin\/hermes.*$/m,
|
|
4383
|
+
(line) => line.replace("/home/delorenj/code/hermes-agent/.venv/bin/hermes", "$HOME/.hermes/hermes-agent/.venv/bin/hermes")
|
|
4384
|
+
);
|
|
4385
|
+
return next;
|
|
4386
|
+
}
|
|
4387
|
+
function isValidOpReference(value) {
|
|
4388
|
+
if (!value.startsWith("op://") || /[\[\]{}<>]/.test(value)) return false;
|
|
4389
|
+
const withoutScheme = value.slice("op://".length);
|
|
4390
|
+
const fragmentIndex = withoutScheme.indexOf("#");
|
|
4391
|
+
if (fragmentIndex >= 0) return false;
|
|
4392
|
+
const queryIndex = withoutScheme.indexOf("?");
|
|
4393
|
+
const pathPart = queryIndex >= 0 ? withoutScheme.slice(0, queryIndex) : withoutScheme;
|
|
4394
|
+
const queryPart = queryIndex >= 0 ? withoutScheme.slice(queryIndex + 1) : "";
|
|
4395
|
+
const parts = pathPart.split("/");
|
|
4396
|
+
if (parts.length < 3 || parts.length > 4 || parts.some((part) => !part)) return false;
|
|
4397
|
+
try {
|
|
4398
|
+
for (const part of parts) decodeURIComponent(part);
|
|
4399
|
+
} catch {
|
|
4400
|
+
return false;
|
|
4401
|
+
}
|
|
4402
|
+
if (queryIndex >= 0 && !/^attribute=[A-Za-z0-9._~-]+$/.test(queryPart)) return false;
|
|
4403
|
+
return true;
|
|
4404
|
+
}
|
|
4405
|
+
function malformedOpReferences(text2) {
|
|
4406
|
+
const occurrences = [];
|
|
4407
|
+
const lines = text2.split("\n");
|
|
4408
|
+
for (let index = 0; index < lines.length; index++) {
|
|
4409
|
+
const line = lines[index];
|
|
4410
|
+
for (const match of line.matchAll(/op:\/\/[^\s"'`]+/g)) {
|
|
4411
|
+
const value = match[0];
|
|
4412
|
+
if (!isValidOpReference(value)) {
|
|
4413
|
+
occurrences.push({ line: index + 1, value, commentOnly: line.trimStart().startsWith("#") });
|
|
4414
|
+
}
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4417
|
+
return occurrences;
|
|
4418
|
+
}
|
|
4419
|
+
function removeMalformedCommentOpReferences(text2) {
|
|
4420
|
+
let changed = false;
|
|
4421
|
+
const lines = text2.split("\n").map((line) => {
|
|
4422
|
+
if (!line.trimStart().startsWith("#")) return line;
|
|
4423
|
+
return line.replace(/op:\/\/[^\s"'`]+/g, (value) => {
|
|
4424
|
+
if (isValidOpReference(value)) return value;
|
|
4425
|
+
changed = true;
|
|
4426
|
+
return "<invalid 1Password reference removed by pjangler>";
|
|
4427
|
+
});
|
|
4428
|
+
});
|
|
4429
|
+
return { text: lines.join("\n"), changed };
|
|
4430
|
+
}
|
|
3500
4431
|
var RULES = [
|
|
3501
4432
|
{
|
|
3502
4433
|
id: "mise.config-root",
|
|
3503
4434
|
title: "mise config_root + AGENTS link hooks",
|
|
3504
4435
|
audit: (ctx) => {
|
|
3505
|
-
const misePath =
|
|
4436
|
+
const misePath = join15(ctx.repoRoot, "mise.toml");
|
|
3506
4437
|
if (!existsSync10(misePath)) {
|
|
3507
4438
|
return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
|
|
3508
4439
|
}
|
|
3509
4440
|
const text2 = readText(misePath);
|
|
3510
4441
|
const details = [];
|
|
3511
|
-
const linkAgentfilesPath =
|
|
4442
|
+
const linkAgentfilesPath = join15(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
3512
4443
|
if (!existsSync10(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
|
|
3513
4444
|
const pathValues = [...(text2.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
3514
4445
|
const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
|
|
@@ -3527,7 +4458,7 @@ var RULES = [
|
|
|
3527
4458
|
};
|
|
3528
4459
|
},
|
|
3529
4460
|
migrate: (ctx, finding) => {
|
|
3530
|
-
const path =
|
|
4461
|
+
const path = join15(ctx.repoRoot, "mise.toml");
|
|
3531
4462
|
const changedFiles = [];
|
|
3532
4463
|
const details = [];
|
|
3533
4464
|
if (!existsSync10(path)) {
|
|
@@ -3546,7 +4477,7 @@ var RULES = [
|
|
|
3546
4477
|
if (!ctx.dryRun) writeText(path, next);
|
|
3547
4478
|
text2 = next;
|
|
3548
4479
|
}
|
|
3549
|
-
const linkAgentfilesPath =
|
|
4480
|
+
const linkAgentfilesPath = join15(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
3550
4481
|
const expectedScript = templateLinkAgentfilesScript(ctx);
|
|
3551
4482
|
if (expectedScript === void 0) {
|
|
3552
4483
|
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: [] };
|
|
@@ -3573,9 +4504,9 @@ var RULES = [
|
|
|
3573
4504
|
title: "managed mise versioning block",
|
|
3574
4505
|
audit: (ctx) => {
|
|
3575
4506
|
const details = [];
|
|
3576
|
-
const misePath =
|
|
3577
|
-
const versioningPath =
|
|
3578
|
-
const manifestPath =
|
|
4507
|
+
const misePath = join15(ctx.repoRoot, "mise.toml");
|
|
4508
|
+
const versioningPath = join15(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
4509
|
+
const manifestPath = join15(ctx.repoRoot, ".mise", "version-files.conf");
|
|
3579
4510
|
const text2 = safeReadText(misePath);
|
|
3580
4511
|
if (!text2?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
|
|
3581
4512
|
if (!existsSync10(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
|
|
@@ -3592,7 +4523,7 @@ var RULES = [
|
|
|
3592
4523
|
migrate: (ctx, finding) => {
|
|
3593
4524
|
const changedFiles = [];
|
|
3594
4525
|
const details = [];
|
|
3595
|
-
const misePath =
|
|
4526
|
+
const misePath = join15(ctx.repoRoot, "mise.toml");
|
|
3596
4527
|
if (!existsSync10(misePath)) {
|
|
3597
4528
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
3598
4529
|
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: [] };
|
|
@@ -3617,7 +4548,7 @@ var RULES = [
|
|
|
3617
4548
|
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
3618
4549
|
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
3619
4550
|
}
|
|
3620
|
-
const versioningPath =
|
|
4551
|
+
const versioningPath = join15(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
3621
4552
|
const expectedScript = templateVersioningScript(ctx);
|
|
3622
4553
|
if (expectedScript === void 0) {
|
|
3623
4554
|
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: [] };
|
|
@@ -3629,7 +4560,7 @@ var RULES = [
|
|
|
3629
4560
|
chmodSync4(versioningPath, 493);
|
|
3630
4561
|
}
|
|
3631
4562
|
}
|
|
3632
|
-
const manifestPath =
|
|
4563
|
+
const manifestPath = join15(ctx.repoRoot, ".mise", "version-files.conf");
|
|
3633
4564
|
const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
|
|
3634
4565
|
if (safeReadText(manifestPath) !== expectedManifest) {
|
|
3635
4566
|
changedFiles.push(manifestPath);
|
|
@@ -3650,39 +4581,112 @@ var RULES = [
|
|
|
3650
4581
|
title: "Skillex project skills manifest",
|
|
3651
4582
|
audit: (ctx) => {
|
|
3652
4583
|
const details = [];
|
|
3653
|
-
const manifestPath =
|
|
3654
|
-
const legacyDir =
|
|
3655
|
-
const localExamplePath =
|
|
3656
|
-
const misePath =
|
|
4584
|
+
const manifestPath = join15(ctx.repoRoot, ".agents", "skills.json");
|
|
4585
|
+
const legacyDir = join15(ctx.repoRoot, ".agents", "skills");
|
|
4586
|
+
const localExamplePath = join15(ctx.repoRoot, ".agents", "local.example.json");
|
|
4587
|
+
const misePath = join15(ctx.repoRoot, "mise.toml");
|
|
3657
4588
|
let fixable = true;
|
|
3658
|
-
|
|
3659
|
-
|
|
4589
|
+
let expectedBmad = [];
|
|
4590
|
+
try {
|
|
4591
|
+
expectedBmad = canonicalBmadSkillEntries(ctx);
|
|
4592
|
+
} catch (error) {
|
|
4593
|
+
details.push(
|
|
4594
|
+
`BMAD Skillex pack ${BMAD_PACK_VERSION} is not trusted at ${bmadPackRoot(ctx)}: ${error instanceof Error ? error.message : String(error)}`
|
|
4595
|
+
);
|
|
4596
|
+
fixable = false;
|
|
4597
|
+
}
|
|
4598
|
+
const expectedByName = new Map(expectedBmad.map((entry) => [entry.name, fileURLToPath4(entry.source)]));
|
|
4599
|
+
const expectedNames = new Set(expectedByName.keys());
|
|
4600
|
+
const packRoot = bmadPackRoot(ctx);
|
|
4601
|
+
const manifest = tryParseJson(safeReadText(manifestPath));
|
|
4602
|
+
if (!manifest) {
|
|
3660
4603
|
details.push(".agents/skills.json missing or invalid JSON");
|
|
3661
4604
|
} else {
|
|
3662
4605
|
if (manifest.inherit_global !== true) details.push(".agents/skills.json should set inherit_global: true");
|
|
3663
4606
|
if (manifest.registry !== SKILLS_REGISTRY_URL) details.push(`.agents/skills.json should set registry to ${SKILLS_REGISTRY_URL}`);
|
|
3664
|
-
if (!Array.isArray(manifest.skills))
|
|
4607
|
+
if (!Array.isArray(manifest.skills)) {
|
|
4608
|
+
details.push(".agents/skills.json should define a skills array");
|
|
4609
|
+
} else {
|
|
4610
|
+
const actualBmad = new Map(
|
|
4611
|
+
manifest.skills.filter((entry) => Boolean(entry) && typeof entry === "object" && isPackManagedManifestEntry(entry, expectedNames, packRoot)).map((entry) => [String(entry.name), String(entry.source ?? "")])
|
|
4612
|
+
);
|
|
4613
|
+
const stale = expectedBmad.filter((entry) => actualBmad.get(entry.name) !== entry.source);
|
|
4614
|
+
if (stale.length > 0 || actualBmad.size !== expectedBmad.length) {
|
|
4615
|
+
details.push(`.agents/skills.json should record all ${expectedBmad.length} BMAD ${BMAD_PACK_VERSION} pack entries as file:// sources`);
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
3665
4618
|
}
|
|
4619
|
+
const invalidBmadLinkNames = /* @__PURE__ */ new Set();
|
|
3666
4620
|
if (existsSync10(legacyDir)) {
|
|
3667
|
-
const
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
4621
|
+
for (const name of readdirSync3(legacyDir)) {
|
|
4622
|
+
const expected = expectedByName.get(name);
|
|
4623
|
+
const path = join15(legacyDir, name);
|
|
4624
|
+
let linkTargetsPack = false;
|
|
4625
|
+
try {
|
|
4626
|
+
linkTargetsPack = lstatSync2(path).isSymbolicLink() && isContainedBy(packRoot, resolve3(dirname7(path), readlinkSync(path)));
|
|
4627
|
+
} catch {
|
|
4628
|
+
linkTargetsPack = false;
|
|
4629
|
+
}
|
|
4630
|
+
if (!expected && !linkTargetsPack) continue;
|
|
4631
|
+
try {
|
|
4632
|
+
if (!expected || !lstatSync2(path).isSymbolicLink() || resolve3(dirname7(path), readlinkSync(path)) !== expected) invalidBmadLinkNames.add(name);
|
|
4633
|
+
} catch {
|
|
4634
|
+
invalidBmadLinkNames.add(name);
|
|
4635
|
+
}
|
|
3673
4636
|
}
|
|
4637
|
+
for (const [name, expected] of expectedByName) {
|
|
4638
|
+
const path = join15(legacyDir, name);
|
|
4639
|
+
try {
|
|
4640
|
+
if (!lstatSync2(path).isSymbolicLink() || resolve3(dirname7(path), readlinkSync(path)) !== expected) invalidBmadLinkNames.add(name);
|
|
4641
|
+
} catch {
|
|
4642
|
+
invalidBmadLinkNames.add(name);
|
|
4643
|
+
}
|
|
4644
|
+
}
|
|
4645
|
+
} else {
|
|
4646
|
+
for (const name of expectedByName.keys()) invalidBmadLinkNames.add(name);
|
|
4647
|
+
}
|
|
4648
|
+
if (invalidBmadLinkNames.size > 0) {
|
|
4649
|
+
details.push(`${invalidBmadLinkNames.size} managed BMAD skill path(s) should be symlinks into the ${BMAD_PACK_VERSION} pack`);
|
|
3674
4650
|
}
|
|
3675
4651
|
for (const rel of [".mise/scripts/link-project-skills-to-clis.sh", ".mise/scripts/unlink-project-skills-from-clis.sh"]) {
|
|
3676
|
-
if (existsSync10(
|
|
4652
|
+
if (existsSync10(join15(ctx.repoRoot, rel))) details.push(`${rel} is a legacy symlink-era script and should be removed`);
|
|
3677
4653
|
}
|
|
3678
4654
|
const localExample = tryParseJson(safeReadText(localExamplePath));
|
|
3679
4655
|
if (localExample && Object.prototype.hasOwnProperty.call(localExample, "skills")) {
|
|
3680
4656
|
details.push(".agents/local.example.json still documents legacy skills overrides; drop the skills section");
|
|
3681
4657
|
}
|
|
3682
4658
|
const mise = safeReadText(misePath);
|
|
3683
|
-
if (!mise?.includes(SYNC_SKILLS_SCRIPT)) details.push("mise.toml should run sync-skills.py
|
|
4659
|
+
if (!mise?.includes(SYNC_SKILLS_SCRIPT)) details.push("mise.toml should run the shipped project-local sync-skills.py engine via config_root");
|
|
4660
|
+
if (!mise?.includes(PROVISION_BMAD_SKILLS_SCRIPT)) details.push("mise.toml should provision pinned BMAD pack links before syncing skills");
|
|
4661
|
+
if (mise?.includes(SYNC_SKILLS_SCRIPT) && mise.includes(PROVISION_BMAD_SKILLS_SCRIPT) && mise.indexOf(PROVISION_BMAD_SKILLS_SCRIPT) > mise.indexOf(SYNC_SKILLS_SCRIPT)) {
|
|
4662
|
+
details.push("mise.toml should run the BMAD provisioner before project skill sync");
|
|
4663
|
+
}
|
|
4664
|
+
if (mise?.includes('script = "sync-skills.py --scope project"') || mise?.includes('run = "sync-skills.py --scope project"')) {
|
|
4665
|
+
details.push("mise.toml still invokes the missing bare sync-skills.py executable");
|
|
4666
|
+
}
|
|
3684
4667
|
if (!mise?.includes('patterns = [".agents/skills.json"]')) details.push("mise.toml should watch .agents/skills.json");
|
|
3685
4668
|
if (!mise?.includes("[tasks.skills-sync]")) details.push("mise.toml should define a skills-sync task");
|
|
4669
|
+
if (!mise?.includes('depends = ["skills-provision-bmad"]')) details.push("skills-sync task should depend on skills-provision-bmad");
|
|
4670
|
+
for (const [rel, label] of [
|
|
4671
|
+
[".mise/scripts/provision-bmad-skills.py", "BMAD Skillex provisioning script"],
|
|
4672
|
+
[".mise/scripts/sync-skills.py", "Project-local skills sync engine"]
|
|
4673
|
+
]) {
|
|
4674
|
+
const target = join15(ctx.repoRoot, rel);
|
|
4675
|
+
const expected = templateCommonProjectText(ctx, rel);
|
|
4676
|
+
const stat = lstatIfPresent(target);
|
|
4677
|
+
if (!stat || !stat.isFile() || stat.isSymbolicLink()) {
|
|
4678
|
+
details.push(`${label} is missing or unsafe`);
|
|
4679
|
+
if (stat) fixable = false;
|
|
4680
|
+
} else {
|
|
4681
|
+
if (expected === void 0 || safeReadText(target) !== expected) details.push(`${label} differs from the shipped template`);
|
|
4682
|
+
if ((Number(stat.mode) & 73) === 0) details.push(`${label} is not executable`);
|
|
4683
|
+
}
|
|
4684
|
+
}
|
|
4685
|
+
const topologyIssues = projectSkillTopologyIssues(ctx.repoRoot);
|
|
4686
|
+
if (topologyIssues.length) {
|
|
4687
|
+
details.push(...topologyIssues.map((issue) => `CLI skill topology: ${issue}`));
|
|
4688
|
+
fixable = false;
|
|
4689
|
+
}
|
|
3686
4690
|
if (mise?.includes("link-project-skills-to-clis.sh") || mise?.includes("unlink-project-skills-from-clis.sh") || mise?.includes("[tasks.skills-relink]")) {
|
|
3687
4691
|
details.push("mise.toml still contains legacy skill-link wiring");
|
|
3688
4692
|
}
|
|
@@ -3698,33 +4702,66 @@ var RULES = [
|
|
|
3698
4702
|
migrate: (ctx, finding) => {
|
|
3699
4703
|
const changedFiles = [];
|
|
3700
4704
|
const details = [];
|
|
3701
|
-
const manifestPath =
|
|
3702
|
-
const
|
|
3703
|
-
const
|
|
3704
|
-
const
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
if (!ctx.dryRun) rmSync(legacyDir, { recursive: true, force: true });
|
|
4705
|
+
const manifestPath = join15(ctx.repoRoot, ".agents", "skills.json");
|
|
4706
|
+
const localExamplePath = join15(ctx.repoRoot, ".agents", "local.example.json");
|
|
4707
|
+
const misePath = join15(ctx.repoRoot, "mise.toml");
|
|
4708
|
+
const provisionScriptPath = join15(ctx.repoRoot, ".mise", "scripts", "provision-bmad-skills.py");
|
|
4709
|
+
const syncScriptPath = join15(ctx.repoRoot, ".mise", "scripts", "sync-skills.py");
|
|
4710
|
+
const expectedProvisionScript = templateCommonProjectText(ctx, ".mise/scripts/provision-bmad-skills.py");
|
|
4711
|
+
const expectedSyncScript = templateCommonProjectText(ctx, ".mise/scripts/sync-skills.py");
|
|
4712
|
+
const topologyIssues = projectSkillTopologyIssues(ctx.repoRoot);
|
|
4713
|
+
if (topologyIssues.length) {
|
|
4714
|
+
return {
|
|
4715
|
+
id: finding.id,
|
|
4716
|
+
title: finding.title,
|
|
4717
|
+
status: "blocked",
|
|
4718
|
+
summary: "Unsafe project CLI skill topology must be repaired manually",
|
|
4719
|
+
changedFiles,
|
|
4720
|
+
details: topologyIssues
|
|
4721
|
+
};
|
|
3719
4722
|
}
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
4723
|
+
if (!expectedProvisionScript || !expectedSyncScript) {
|
|
4724
|
+
return {
|
|
4725
|
+
id: finding.id,
|
|
4726
|
+
title: finding.title,
|
|
4727
|
+
status: "blocked",
|
|
4728
|
+
summary: "pjangler install is missing a shipped skills executable",
|
|
4729
|
+
changedFiles,
|
|
4730
|
+
details: [
|
|
4731
|
+
...!expectedProvisionScript ? ["Missing BMAD Skillex provisioning script template"] : [],
|
|
4732
|
+
...!expectedSyncScript ? ["Missing project-local skills sync engine template"] : []
|
|
4733
|
+
]
|
|
4734
|
+
};
|
|
3725
4735
|
}
|
|
4736
|
+
const unsafeScriptTargets = [provisionScriptPath, syncScriptPath].filter((path) => {
|
|
4737
|
+
const stat = lstatIfPresent(path);
|
|
4738
|
+
return Boolean(stat && (!stat.isFile() || stat.isSymbolicLink()));
|
|
4739
|
+
});
|
|
4740
|
+
if (unsafeScriptTargets.length) {
|
|
4741
|
+
return {
|
|
4742
|
+
id: finding.id,
|
|
4743
|
+
title: finding.title,
|
|
4744
|
+
status: "blocked",
|
|
4745
|
+
summary: "Refusing non-regular managed skills executable target",
|
|
4746
|
+
changedFiles,
|
|
4747
|
+
details: unsafeScriptTargets.map((path) => `${path} must be removed or repaired manually`)
|
|
4748
|
+
};
|
|
4749
|
+
}
|
|
4750
|
+
const provisioned = provisionBmadSkills(ctx);
|
|
4751
|
+
if (!provisioned.ok) {
|
|
4752
|
+
return {
|
|
4753
|
+
id: finding.id,
|
|
4754
|
+
title: finding.title,
|
|
4755
|
+
status: "blocked",
|
|
4756
|
+
summary: `BMAD Skillex pack ${BMAD_PACK_VERSION} is unavailable`,
|
|
4757
|
+
changedFiles,
|
|
4758
|
+
details: [provisioned.error ?? "Unknown BMAD pack error"]
|
|
4759
|
+
};
|
|
4760
|
+
}
|
|
4761
|
+
changedFiles.push(...provisioned.changedFiles);
|
|
4762
|
+
if (provisioned.changedFiles.includes(manifestPath)) details.push(`Recorded BMAD pack ${BMAD_PACK_VERSION} in .agents/skills.json`);
|
|
3726
4763
|
for (const rel of [".mise/scripts/link-project-skills-to-clis.sh", ".mise/scripts/unlink-project-skills-from-clis.sh"]) {
|
|
3727
|
-
const path =
|
|
4764
|
+
const path = join15(ctx.repoRoot, rel);
|
|
3728
4765
|
if (existsSync10(path)) {
|
|
3729
4766
|
changedFiles.push(path);
|
|
3730
4767
|
if (!ctx.dryRun) unlinkSync3(path);
|
|
@@ -3736,6 +4773,8 @@ var RULES = [
|
|
|
3736
4773
|
changedFiles.push(localExamplePath);
|
|
3737
4774
|
if (!ctx.dryRun) writeText(localExamplePath, templateLocalExample);
|
|
3738
4775
|
}
|
|
4776
|
+
normalizeExecutableTemplate(ctx, provisionScriptPath, expectedProvisionScript, changedFiles);
|
|
4777
|
+
normalizeExecutableTemplate(ctx, syncScriptPath, expectedSyncScript, changedFiles);
|
|
3739
4778
|
if (!existsSync10(misePath)) {
|
|
3740
4779
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
3741
4780
|
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 };
|
|
@@ -3761,9 +4800,9 @@ var RULES = [
|
|
|
3761
4800
|
id: "sot.agent-symlinks",
|
|
3762
4801
|
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
3763
4802
|
audit: (ctx) => {
|
|
3764
|
-
const agentsPath =
|
|
4803
|
+
const agentsPath = join15(ctx.repoRoot, "AGENTS.md");
|
|
3765
4804
|
if (!existsSync10(agentsPath)) {
|
|
3766
|
-
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync10(
|
|
4805
|
+
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync10(join15(ctx.repoRoot, file)));
|
|
3767
4806
|
if (fallbackSources.length === 0) {
|
|
3768
4807
|
return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
|
|
3769
4808
|
}
|
|
@@ -3778,7 +4817,7 @@ var RULES = [
|
|
|
3778
4817
|
}
|
|
3779
4818
|
const details = [];
|
|
3780
4819
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
3781
|
-
const full =
|
|
4820
|
+
const full = join15(ctx.repoRoot, file);
|
|
3782
4821
|
const target = readSymlinkTarget(full);
|
|
3783
4822
|
if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
|
|
3784
4823
|
}
|
|
@@ -3802,7 +4841,7 @@ var RULES = [
|
|
|
3802
4841
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
|
|
3803
4842
|
}
|
|
3804
4843
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
3805
|
-
const full =
|
|
4844
|
+
const full = join15(ctx.repoRoot, file);
|
|
3806
4845
|
const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
|
|
3807
4846
|
if (result.blocked) blockedDetails.push(result.blocked);
|
|
3808
4847
|
if (result.changed) changedFiles.push(full);
|
|
@@ -3824,7 +4863,7 @@ var RULES = [
|
|
|
3824
4863
|
migrate: (ctx, finding) => {
|
|
3825
4864
|
const changedFiles = [];
|
|
3826
4865
|
const details = [];
|
|
3827
|
-
const path =
|
|
4866
|
+
const path = join15(ctx.repoRoot, ".project.json");
|
|
3828
4867
|
const existing = readProjectJson(ctx) ?? {};
|
|
3829
4868
|
const canonical = canonicalProjectJson(ctx);
|
|
3830
4869
|
const merged = { ...existing, ...canonical };
|
|
@@ -3834,11 +4873,11 @@ var RULES = [
|
|
|
3834
4873
|
changedFiles.push(path);
|
|
3835
4874
|
if (!ctx.dryRun) writeText(path, expected);
|
|
3836
4875
|
}
|
|
3837
|
-
const planeJson =
|
|
4876
|
+
const planeJson = join15(ctx.repoRoot, ".plane.json");
|
|
3838
4877
|
if (existsSync10(planeJson)) {
|
|
3839
4878
|
const backup = `${planeJson}.migrated-backup`;
|
|
3840
4879
|
if (existsSync10(backup)) {
|
|
3841
|
-
details.push(`cannot back up .plane.json because ${
|
|
4880
|
+
details.push(`cannot back up .plane.json because ${relative2(ctx.repoRoot, backup)} already exists`);
|
|
3842
4881
|
} else {
|
|
3843
4882
|
changedFiles.push(backup);
|
|
3844
4883
|
if (!ctx.dryRun) renameSync2(planeJson, backup);
|
|
@@ -3859,17 +4898,21 @@ var RULES = [
|
|
|
3859
4898
|
title: ".env.op + gitignore secrets contract",
|
|
3860
4899
|
audit: (ctx) => {
|
|
3861
4900
|
const details = [];
|
|
3862
|
-
const envOp = safeReadText(
|
|
3863
|
-
const gitignore = safeReadText(
|
|
4901
|
+
const envOp = safeReadText(join15(ctx.repoRoot, ".env.op"));
|
|
4902
|
+
const gitignore = safeReadText(join15(ctx.repoRoot, ".gitignore"));
|
|
3864
4903
|
if (!envOp) {
|
|
3865
4904
|
details.push(".env.op missing");
|
|
3866
4905
|
} else {
|
|
3867
|
-
const
|
|
4906
|
+
const malformed = malformedOpReferences(envOp);
|
|
4907
|
+
if (malformed.length) {
|
|
4908
|
+
details.push(`.env.op has malformed op:// reference(s) on line(s): ${Array.from(new Set(malformed.map((entry) => entry.line))).join(", ")}`);
|
|
4909
|
+
}
|
|
4910
|
+
const invalidLines = envOp.split("\n").map((line, index) => ({ line: line.trim(), number: index + 1 })).filter(({ line }) => line && !line.startsWith("#") && line.includes("=")).filter(({ line }) => {
|
|
3868
4911
|
const value = line.slice(line.indexOf("=") + 1).trim();
|
|
3869
4912
|
const quotedLiteral = /^"[^"\r\n]*"$/.test(value) || /^'[^'\r\n]*'$/.test(value);
|
|
3870
4913
|
return !value.startsWith("op://") && !/^https?:\/\//.test(value) && !/^[A-Za-z0-9_.:-]+$/.test(value) && !quotedLiteral;
|
|
3871
4914
|
});
|
|
3872
|
-
if (invalidLines.length) details.push(`.env.op has non-reference values that do not look like safe literals: ${invalidLines.join(", ")}`);
|
|
4915
|
+
if (invalidLines.length) details.push(`.env.op has non-reference values that do not look like safe literals on line(s): ${invalidLines.map((entry) => entry.number).join(", ")}`);
|
|
3873
4916
|
}
|
|
3874
4917
|
if (!gitignore?.includes(".env\n") && !gitignore?.includes(".env\r\n")) details.push(".gitignore should ignore .env");
|
|
3875
4918
|
if (!gitignore?.includes(".env.*")) details.push(".gitignore should ignore .env.*");
|
|
@@ -3886,12 +4929,23 @@ var RULES = [
|
|
|
3886
4929
|
migrate: (ctx, finding) => {
|
|
3887
4930
|
const changedFiles = [];
|
|
3888
4931
|
const details = [];
|
|
3889
|
-
const envOpPath =
|
|
4932
|
+
const envOpPath = join15(ctx.repoRoot, ".env.op");
|
|
3890
4933
|
if (!existsSync10(envOpPath)) {
|
|
3891
4934
|
changedFiles.push(envOpPath);
|
|
3892
|
-
if (!ctx.dryRun) writeText(envOpPath, readText(
|
|
4935
|
+
if (!ctx.dryRun) writeText(envOpPath, readText(join15(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
|
|
4936
|
+
} else {
|
|
4937
|
+
const current = readText(envOpPath);
|
|
4938
|
+
const repaired = removeMalformedCommentOpReferences(current);
|
|
4939
|
+
if (repaired.changed) {
|
|
4940
|
+
changedFiles.push(envOpPath);
|
|
4941
|
+
if (!ctx.dryRun) writeText(envOpPath, repaired.text);
|
|
4942
|
+
}
|
|
4943
|
+
const remaining = malformedOpReferences(repaired.text).filter((entry) => !entry.commentOnly);
|
|
4944
|
+
if (remaining.length) {
|
|
4945
|
+
details.push(`Malformed active op:// reference(s) remain on line(s) ${Array.from(new Set(remaining.map((entry) => entry.line))).join(", ")}; repair them manually without replacing valid user references`);
|
|
4946
|
+
}
|
|
3893
4947
|
}
|
|
3894
|
-
const gitignorePath =
|
|
4948
|
+
const gitignorePath = join15(ctx.repoRoot, ".gitignore");
|
|
3895
4949
|
const gitignore = safeReadText(gitignorePath) ?? "";
|
|
3896
4950
|
const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
|
|
3897
4951
|
# NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
|
|
@@ -3918,7 +4972,7 @@ var RULES = [
|
|
|
3918
4972
|
title: ".copier-answers.yml provenance + drift report",
|
|
3919
4973
|
audit: (ctx) => {
|
|
3920
4974
|
const details = [];
|
|
3921
|
-
const path =
|
|
4975
|
+
const path = join15(ctx.repoRoot, ".copier-answers.yml");
|
|
3922
4976
|
const text2 = safeReadText(path);
|
|
3923
4977
|
const project = readProjectJson(ctx);
|
|
3924
4978
|
if (!text2) {
|
|
@@ -3949,12 +5003,12 @@ var RULES = [
|
|
|
3949
5003
|
const changedFiles = [];
|
|
3950
5004
|
const project = canonicalProjectJson(ctx);
|
|
3951
5005
|
const text2 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
|
|
3952
|
-
_src_path: ${
|
|
5006
|
+
_src_path: ${join15(ctx.pjanglerRoot, "templates", "commonproject")}
|
|
3953
5007
|
project_description: ${String(project.project_description)}
|
|
3954
5008
|
project_name: ${String(project.project_name)}
|
|
3955
5009
|
ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
3956
5010
|
`;
|
|
3957
|
-
const path =
|
|
5011
|
+
const path = join15(ctx.repoRoot, ".copier-answers.yml");
|
|
3958
5012
|
if (safeReadText(path) !== text2) {
|
|
3959
5013
|
changedFiles.push(path);
|
|
3960
5014
|
if (!ctx.dryRun) writeText(path, text2);
|
|
@@ -3973,14 +5027,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3973
5027
|
id: "bmad.scaffold",
|
|
3974
5028
|
title: "BMAD modules/docs scaffold",
|
|
3975
5029
|
audit: (ctx) => {
|
|
3976
|
-
const
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
5030
|
+
const manifestSelection = manifestBmadModules(ctx.repoRoot);
|
|
5031
|
+
if (manifestSelection.status === "invalid") {
|
|
5032
|
+
return {
|
|
5033
|
+
id: "bmad.scaffold",
|
|
5034
|
+
title: "BMAD modules/docs scaffold",
|
|
5035
|
+
status: "fail",
|
|
5036
|
+
summary: "BMAD module manifest is invalid; refusing fallback module selection",
|
|
5037
|
+
details: [manifestSelection.error],
|
|
5038
|
+
fixable: false
|
|
5039
|
+
};
|
|
5040
|
+
}
|
|
5041
|
+
const targetRoot = join15(ctx.repoRoot, "_bmad");
|
|
5042
|
+
const selectedModules = manifestSelection.status === "valid" ? manifestSelection.modules : configuredBmadModules(ctx.repoRoot) ?? [...DEFAULT_BMAD_MODULES];
|
|
5043
|
+
const sentinels = requiredBmadSentinels(ctx.repoRoot, selectedModules);
|
|
5044
|
+
const missing = sentinels.filter((file) => !existsSync10(join15(targetRoot, file)));
|
|
3984
5045
|
return {
|
|
3985
5046
|
id: "bmad.scaffold",
|
|
3986
5047
|
title: "BMAD modules/docs scaffold",
|
|
@@ -3992,9 +5053,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3992
5053
|
},
|
|
3993
5054
|
migrate: (ctx, finding) => {
|
|
3994
5055
|
const changedFiles = [];
|
|
5056
|
+
const manifestSelection = manifestBmadModules(ctx.repoRoot);
|
|
5057
|
+
if (manifestSelection.status === "invalid") {
|
|
5058
|
+
return {
|
|
5059
|
+
id: finding.id,
|
|
5060
|
+
title: finding.title,
|
|
5061
|
+
status: "blocked",
|
|
5062
|
+
summary: "BMAD module manifest is invalid; refusing fallback module selection",
|
|
5063
|
+
changedFiles,
|
|
5064
|
+
details: [manifestSelection.error]
|
|
5065
|
+
};
|
|
5066
|
+
}
|
|
5067
|
+
const selectedModules = manifestSelection.status === "valid" ? manifestSelection.modules : configuredBmadModules(ctx.repoRoot) ?? [...DEFAULT_BMAD_MODULES];
|
|
3995
5068
|
if (ctx.dryRun) {
|
|
3996
5069
|
for (const detail of finding.details) {
|
|
3997
|
-
changedFiles.push(
|
|
5070
|
+
changedFiles.push(join15(ctx.repoRoot, detail));
|
|
3998
5071
|
}
|
|
3999
5072
|
return {
|
|
4000
5073
|
id: finding.id,
|
|
@@ -4003,11 +5076,14 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4003
5076
|
summary: changedFiles.length ? "Would run non-interactive bmad-method install" : "No changes required",
|
|
4004
5077
|
changedFiles,
|
|
4005
5078
|
details: [
|
|
4006
|
-
`Would run: npx ${bmadInstallArgs(ctx.repoRoot).join(" ").replace(BMAD_INSTALL_TOOLS.join(","), "...")}`
|
|
5079
|
+
`Would run: npx ${bmadInstallArgs(ctx.repoRoot, selectedModules).join(" ").replace(BMAD_INSTALL_TOOLS.join(","), "...")}`
|
|
4007
5080
|
]
|
|
4008
5081
|
};
|
|
4009
5082
|
}
|
|
4010
|
-
const
|
|
5083
|
+
const preservedSkillsManifest = tryParseJson(
|
|
5084
|
+
safeReadText(join15(ctx.repoRoot, ".agents", "skills.json"))
|
|
5085
|
+
);
|
|
5086
|
+
const install = runBmadInstall(ctx.repoRoot, selectedModules);
|
|
4011
5087
|
if (!install.ok) {
|
|
4012
5088
|
return {
|
|
4013
5089
|
id: finding.id,
|
|
@@ -4018,16 +5094,28 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4018
5094
|
details: [install.error ?? "Unknown error"]
|
|
4019
5095
|
};
|
|
4020
5096
|
}
|
|
5097
|
+
const provisioned = provisionBmadSkills(ctx, preservedSkillsManifest);
|
|
5098
|
+
if (!provisioned.ok) {
|
|
5099
|
+
return {
|
|
5100
|
+
id: finding.id,
|
|
5101
|
+
title: finding.title,
|
|
5102
|
+
status: "blocked",
|
|
5103
|
+
summary: `BMAD installed but Skillex pack ${BMAD_PACK_VERSION} provisioning failed`,
|
|
5104
|
+
changedFiles: [],
|
|
5105
|
+
details: [provisioned.error ?? "Unknown BMAD pack error"]
|
|
5106
|
+
};
|
|
5107
|
+
}
|
|
4021
5108
|
for (const detail of finding.details) {
|
|
4022
|
-
if (existsSync10(
|
|
4023
|
-
changedFiles.push(
|
|
5109
|
+
if (existsSync10(join15(ctx.repoRoot, detail))) {
|
|
5110
|
+
changedFiles.push(join15(ctx.repoRoot, detail));
|
|
4024
5111
|
}
|
|
4025
5112
|
}
|
|
5113
|
+
changedFiles.push(...provisioned.changedFiles);
|
|
4026
5114
|
return {
|
|
4027
5115
|
id: finding.id,
|
|
4028
5116
|
title: finding.title,
|
|
4029
5117
|
status: changedFiles.length ? "applied" : "noop",
|
|
4030
|
-
summary: changedFiles.length ?
|
|
5118
|
+
summary: changedFiles.length ? `Installed BMAD scaffold with Skillex pack ${BMAD_PACK_VERSION} skills` : "No changes required",
|
|
4031
5119
|
changedFiles,
|
|
4032
5120
|
details: []
|
|
4033
5121
|
};
|
|
@@ -4043,7 +5131,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4043
5131
|
id: "bmad.version",
|
|
4044
5132
|
title: "BMAD version currency",
|
|
4045
5133
|
status: "skip",
|
|
4046
|
-
summary: existsSync10(
|
|
5134
|
+
summary: existsSync10(join15(ctx.repoRoot, "_bmad")) ? "BMAD installed but version manifest unreadable" : "No BMAD install present",
|
|
4047
5135
|
details: [],
|
|
4048
5136
|
fixable: false
|
|
4049
5137
|
};
|
|
@@ -4098,7 +5186,19 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4098
5186
|
}
|
|
4099
5187
|
const installed = readInstalledBmadVersion(ctx.repoRoot);
|
|
4100
5188
|
const available = resolveBmadDistTags(ctx.homeDir)?.distTags?.[BMAD_TARGET_CHANNEL];
|
|
4101
|
-
const manifestPath =
|
|
5189
|
+
const manifestPath = join15(ctx.repoRoot, "_bmad", "_config", "manifest.yaml");
|
|
5190
|
+
const manifestSelection = manifestBmadModules(ctx.repoRoot);
|
|
5191
|
+
if (manifestSelection.status === "invalid") {
|
|
5192
|
+
return {
|
|
5193
|
+
id: finding.id,
|
|
5194
|
+
title: finding.title,
|
|
5195
|
+
status: "blocked",
|
|
5196
|
+
summary: "BMAD module manifest is invalid; refusing fallback module selection",
|
|
5197
|
+
changedFiles: [],
|
|
5198
|
+
details: [manifestSelection.error]
|
|
5199
|
+
};
|
|
5200
|
+
}
|
|
5201
|
+
const selectedModules = manifestSelection.status === "valid" ? manifestSelection.modules : configuredBmadModules(ctx.repoRoot) ?? [...DEFAULT_BMAD_MODULES];
|
|
4102
5202
|
if (ctx.dryRun) {
|
|
4103
5203
|
return {
|
|
4104
5204
|
id: finding.id,
|
|
@@ -4107,11 +5207,14 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4107
5207
|
summary: `Would upgrade BMAD ${installed ?? "?"} -> ${available ?? BMAD_TARGET_CHANNEL}`,
|
|
4108
5208
|
changedFiles: [manifestPath],
|
|
4109
5209
|
details: [
|
|
4110
|
-
`Would run: npx ${bmadInstallArgs(ctx.repoRoot).join(" ").replace(BMAD_INSTALL_TOOLS.join(","), "...")}`
|
|
5210
|
+
`Would run: npx ${bmadInstallArgs(ctx.repoRoot, selectedModules).join(" ").replace(BMAD_INSTALL_TOOLS.join(","), "...")}`
|
|
4111
5211
|
]
|
|
4112
5212
|
};
|
|
4113
5213
|
}
|
|
4114
|
-
const
|
|
5214
|
+
const preservedSkillsManifest = tryParseJson(
|
|
5215
|
+
safeReadText(join15(ctx.repoRoot, ".agents", "skills.json"))
|
|
5216
|
+
);
|
|
5217
|
+
const install = runBmadInstall(ctx.repoRoot, selectedModules);
|
|
4115
5218
|
if (!install.ok) {
|
|
4116
5219
|
return {
|
|
4117
5220
|
id: finding.id,
|
|
@@ -4122,6 +5225,17 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4122
5225
|
details: [install.error ?? "Unknown error"]
|
|
4123
5226
|
};
|
|
4124
5227
|
}
|
|
5228
|
+
const provisioned = provisionBmadSkills(ctx, preservedSkillsManifest);
|
|
5229
|
+
if (!provisioned.ok) {
|
|
5230
|
+
return {
|
|
5231
|
+
id: finding.id,
|
|
5232
|
+
title: finding.title,
|
|
5233
|
+
status: "blocked",
|
|
5234
|
+
summary: `BMAD upgraded but Skillex pack ${BMAD_PACK_VERSION} provisioning failed`,
|
|
5235
|
+
changedFiles: [],
|
|
5236
|
+
details: [provisioned.error ?? "Unknown BMAD pack error"]
|
|
5237
|
+
};
|
|
5238
|
+
}
|
|
4125
5239
|
const nowInstalled = readInstalledBmadVersion(ctx.repoRoot);
|
|
4126
5240
|
const upgraded = Boolean(nowInstalled && installed && compareBmadVersions(nowInstalled, installed) > 0);
|
|
4127
5241
|
return {
|
|
@@ -4129,7 +5243,10 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4129
5243
|
title: finding.title,
|
|
4130
5244
|
status: upgraded ? "applied" : "noop",
|
|
4131
5245
|
summary: upgraded ? `Upgraded BMAD ${installed} -> ${nowInstalled}` : `BMAD reinstalled (${nowInstalled ?? "?"})`,
|
|
4132
|
-
changedFiles:
|
|
5246
|
+
changedFiles: Array.from(/* @__PURE__ */ new Set([
|
|
5247
|
+
...upgraded ? [manifestPath] : [],
|
|
5248
|
+
...provisioned.changedFiles
|
|
5249
|
+
])),
|
|
4133
5250
|
details: []
|
|
4134
5251
|
};
|
|
4135
5252
|
}
|
|
@@ -4144,12 +5261,13 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4144
5261
|
return { id: "hermes.pm-scaffold", title: "Hermes PM scaffold parity", status: "skip", summary: "No pm role present", details: [], fixable: false };
|
|
4145
5262
|
}
|
|
4146
5263
|
const details = [];
|
|
4147
|
-
for (const rel of ["role.yaml", "SOUL.md", "hermes", ".gitignore", ".scripts/70-systemd.sh", ".scripts/heartbeat.sh", ".scripts/checkpoint.sh", ".runtime-scaffold/README.md", "runtime/memories/MEMORY.md"
|
|
4148
|
-
if (!existsSync10(
|
|
5264
|
+
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"]) {
|
|
5265
|
+
if (!existsSync10(join15(role.roleDir, rel))) details.push(`missing ${relative2(ctx.repoRoot, join15(role.roleDir, rel))}`);
|
|
5266
|
+
}
|
|
5267
|
+
if (hasRuntimeSubmoduleMapping(ctx.repoRoot, role)) {
|
|
5268
|
+
details.push(".gitmodules contains retired pm runtime submodule mapping");
|
|
4149
5269
|
}
|
|
4150
|
-
|
|
4151
|
-
if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
|
|
4152
|
-
if (!profileMetaInheritsDefault(join14(role.roleDir, "runtime", "profile.yaml"))) {
|
|
5270
|
+
if (!profileMetaInheritsDefault(join15(role.roleDir, "runtime", "profile.yaml"))) {
|
|
4153
5271
|
details.push("runtime/profile.yaml missing inherited default config metadata");
|
|
4154
5272
|
}
|
|
4155
5273
|
const registry = safeReadText(registryPath(ctx.homeDir));
|
|
@@ -4170,21 +5288,32 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4170
5288
|
if (!role) {
|
|
4171
5289
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
|
|
4172
5290
|
}
|
|
4173
|
-
const
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
5291
|
+
const retirement = retireRuntimeSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
|
|
5292
|
+
details.push(...retirement.details);
|
|
5293
|
+
if (!retirement.ok) {
|
|
5294
|
+
return {
|
|
5295
|
+
id: finding.id,
|
|
5296
|
+
title: finding.title,
|
|
5297
|
+
status: "blocked",
|
|
5298
|
+
summary: "Failed to retire PM runtime submodule metadata safely",
|
|
5299
|
+
changedFiles,
|
|
5300
|
+
details: [retirement.error ?? "unknown runtime retirement failure"]
|
|
5301
|
+
};
|
|
5302
|
+
}
|
|
5303
|
+
const templateRoleDir = join15(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
|
|
5304
|
+
writeIfDifferent(join15(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
|
|
5305
|
+
writeIfDifferent(join15(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
|
|
5306
|
+
writeIfDifferent(join15(role.roleDir, ".gitignore"), readText(join15(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
|
|
5307
|
+
copyMissingRecursive(join15(templateRoleDir, ".runtime-scaffold"), join15(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
|
|
5308
|
+
copyMissingRecursive(join15(templateRoleDir, ".runtime-scaffold"), join15(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
|
|
5309
|
+
copyMissingRecursive(join15(templateRoleDir, ".scripts"), join15(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
|
|
5310
|
+
const promptSource = join15(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
|
|
5311
|
+
const promptTarget = join15(role.roleDir, ".scripts", "sentinel.prompt.md");
|
|
4182
5312
|
if (existsSync10(promptSource) && !existsSync10(promptTarget)) {
|
|
4183
5313
|
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);
|
|
4184
5314
|
writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
|
|
4185
5315
|
}
|
|
4186
|
-
|
|
4187
|
-
const profileMetaUpdated = upsertInheritedProfileMeta(join14(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
|
|
5316
|
+
const profileMetaUpdated = upsertInheritedProfileMeta(join15(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
|
|
4188
5317
|
if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
|
|
4189
5318
|
const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
|
|
4190
5319
|
if (registryUpdated) details.push(`updated ${registryUpdated}`);
|
|
@@ -4215,8 +5344,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4215
5344
|
}
|
|
4216
5345
|
const details = [];
|
|
4217
5346
|
for (const role of roles) {
|
|
4218
|
-
const roleRelDir =
|
|
4219
|
-
const runtimeRelPath =
|
|
5347
|
+
const roleRelDir = relative2(ctx.repoRoot, role.roleDir);
|
|
5348
|
+
const runtimeRelPath = join15(roleRelDir, "runtime");
|
|
4220
5349
|
const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
|
|
4221
5350
|
cwd: ctx.repoRoot,
|
|
4222
5351
|
encoding: "utf8"
|
|
@@ -4224,15 +5353,18 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4224
5353
|
if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
|
|
4225
5354
|
details.push(`submodule runtime is tracked in Git index at ${runtimeRelPath}`);
|
|
4226
5355
|
}
|
|
4227
|
-
|
|
5356
|
+
if (hasRuntimeSubmoduleMapping(ctx.repoRoot, role)) {
|
|
5357
|
+
details.push(`stale .gitmodules mapping exists for ${runtimeRelPath}`);
|
|
5358
|
+
}
|
|
5359
|
+
const gitignorePath = join15(role.roleDir, ".gitignore");
|
|
4228
5360
|
if (existsSync10(gitignorePath)) {
|
|
4229
5361
|
const content = safeReadText(gitignorePath) ?? "";
|
|
4230
5362
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
4231
5363
|
if (!lines.includes("runtime/") && !lines.includes("runtime")) {
|
|
4232
|
-
details.push(`.gitignore missing runtime/ ignore entry in ${
|
|
5364
|
+
details.push(`.gitignore missing runtime/ ignore entry in ${relative2(ctx.repoRoot, gitignorePath)}`);
|
|
4233
5365
|
}
|
|
4234
5366
|
} else {
|
|
4235
|
-
details.push(`.gitignore is missing in ${
|
|
5367
|
+
details.push(`.gitignore is missing in ${relative2(ctx.repoRoot, gitignorePath)}`);
|
|
4236
5368
|
}
|
|
4237
5369
|
}
|
|
4238
5370
|
return {
|
|
@@ -4249,23 +5381,19 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4249
5381
|
const changedFiles = [];
|
|
4250
5382
|
const details = [];
|
|
4251
5383
|
for (const role of roles) {
|
|
4252
|
-
const
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
cwd: ctx.repoRoot,
|
|
4264
|
-
encoding: "utf8"
|
|
4265
|
-
});
|
|
4266
|
-
}
|
|
5384
|
+
const retirement = retireRuntimeSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
|
|
5385
|
+
details.push(...retirement.details);
|
|
5386
|
+
if (!retirement.ok) {
|
|
5387
|
+
return {
|
|
5388
|
+
id: finding.id,
|
|
5389
|
+
title: finding.title,
|
|
5390
|
+
status: "blocked",
|
|
5391
|
+
summary: "Failed to retire Hermes runtime submodule metadata safely",
|
|
5392
|
+
changedFiles,
|
|
5393
|
+
details: [retirement.error ?? "unknown runtime retirement failure"]
|
|
5394
|
+
};
|
|
4267
5395
|
}
|
|
4268
|
-
const gitignorePath =
|
|
5396
|
+
const gitignorePath = join15(role.roleDir, ".gitignore");
|
|
4269
5397
|
let content = "";
|
|
4270
5398
|
let isIgnored = false;
|
|
4271
5399
|
if (existsSync10(gitignorePath)) {
|
|
@@ -4274,7 +5402,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4274
5402
|
isIgnored = lines.includes("runtime/") || lines.includes("runtime");
|
|
4275
5403
|
}
|
|
4276
5404
|
if (!isIgnored) {
|
|
4277
|
-
details.push(`ignore runtime/ in ${
|
|
5405
|
+
details.push(`ignore runtime/ in ${relative2(ctx.repoRoot, gitignorePath)}`);
|
|
4278
5406
|
changedFiles.push(gitignorePath);
|
|
4279
5407
|
if (!ctx.dryRun) {
|
|
4280
5408
|
if (content && !content.endsWith("\n")) {
|
|
@@ -4309,7 +5437,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4309
5437
|
}
|
|
4310
5438
|
const details = [];
|
|
4311
5439
|
for (const role of roles) {
|
|
4312
|
-
for (const unit of [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-
|
|
5440
|
+
for (const unit of [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-heartbeat.timer`]) {
|
|
4313
5441
|
const state = checkUnit(unit);
|
|
4314
5442
|
if (!state.enabled || !state.active) details.push(`${unit} should be enabled+active`);
|
|
4315
5443
|
}
|
|
@@ -4335,9 +5463,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4335
5463
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
|
|
4336
5464
|
}
|
|
4337
5465
|
for (const role of roles) {
|
|
4338
|
-
const sysDir =
|
|
4339
|
-
const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-
|
|
4340
|
-
const allUnitsPresent = units.every((unit) => existsSync10(
|
|
5466
|
+
const sysDir = join15(ctx.homeDir, ".config", "systemd", "user");
|
|
5467
|
+
const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-heartbeat.timer`];
|
|
5468
|
+
const allUnitsPresent = units.every((unit) => existsSync10(join15(sysDir, unit)));
|
|
4341
5469
|
if (allUnitsPresent) {
|
|
4342
5470
|
if (ctx.dryRun) {
|
|
4343
5471
|
details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
|
|
@@ -4349,7 +5477,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4349
5477
|
}
|
|
4350
5478
|
continue;
|
|
4351
5479
|
}
|
|
4352
|
-
for (const script of [
|
|
5480
|
+
for (const script of [join15(role.roleDir, ".scripts", "70-systemd.sh")]) {
|
|
4353
5481
|
if (!script || !existsSync10(script)) continue;
|
|
4354
5482
|
if (ctx.dryRun) {
|
|
4355
5483
|
details.push(`would run: bash ${script}`);
|
|
@@ -4368,6 +5496,349 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4368
5496
|
details
|
|
4369
5497
|
};
|
|
4370
5498
|
}
|
|
5499
|
+
},
|
|
5500
|
+
{
|
|
5501
|
+
id: "hermes.runtime-singleton",
|
|
5502
|
+
title: "Hermes singleton runtime (shared config/auth, per-agent memory)",
|
|
5503
|
+
audit: (ctx) => {
|
|
5504
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
5505
|
+
if (!roles.length) {
|
|
5506
|
+
return { id: "hermes.runtime-singleton", title: "Hermes singleton runtime (shared config/auth, per-agent memory)", status: "skip", summary: "No Hermes roles present", details: [], fixable: false };
|
|
5507
|
+
}
|
|
5508
|
+
const details = [];
|
|
5509
|
+
for (const role of roles) {
|
|
5510
|
+
const plan = singletonPlan(ctx, role);
|
|
5511
|
+
if (!existsSync10(plan.fleetRoot)) {
|
|
5512
|
+
details.push(`fleet root missing at ${plan.fleetRoot}`);
|
|
5513
|
+
continue;
|
|
5514
|
+
}
|
|
5515
|
+
if (!existsSync10(plan.profileDir)) {
|
|
5516
|
+
details.push(`profile dir missing: ${plan.profileDir}`);
|
|
5517
|
+
} else if (lstatSync2(plan.profileDir).isSymbolicLink()) {
|
|
5518
|
+
details.push(`profile dir is a symlink (must be a real dir): ${plan.profileDir}`);
|
|
5519
|
+
}
|
|
5520
|
+
for (const link of plan.links) {
|
|
5521
|
+
const state = linkState(link.path, link.target);
|
|
5522
|
+
if (state !== "ok") details.push(`${state}: ${link.path} -> ${link.target}`);
|
|
5523
|
+
}
|
|
5524
|
+
}
|
|
5525
|
+
return {
|
|
5526
|
+
id: "hermes.runtime-singleton",
|
|
5527
|
+
title: "Hermes singleton runtime (shared config/auth, per-agent memory)",
|
|
5528
|
+
status: details.length === 0 ? "pass" : "fail",
|
|
5529
|
+
summary: details.length === 0 ? "Singleton runtime contract satisfied" : `${details.length} singleton-runtime issue(s) detected`,
|
|
5530
|
+
details,
|
|
5531
|
+
fixable: true
|
|
5532
|
+
};
|
|
5533
|
+
},
|
|
5534
|
+
migrate: (ctx, finding) => {
|
|
5535
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
5536
|
+
const changedFiles = [];
|
|
5537
|
+
const details = [];
|
|
5538
|
+
for (const role of roles) {
|
|
5539
|
+
const plan = singletonPlan(ctx, role);
|
|
5540
|
+
if (!existsSync10(plan.fleetRoot)) {
|
|
5541
|
+
details.push(`blocked: fleet root missing at ${plan.fleetRoot}`);
|
|
5542
|
+
continue;
|
|
5543
|
+
}
|
|
5544
|
+
for (const shared of plan.sharedSeeds) {
|
|
5545
|
+
if (existsSync10(shared.rootPath)) continue;
|
|
5546
|
+
const donor = existsSync10(shared.runtimePath) ? shared.runtimePath : null;
|
|
5547
|
+
if (!donor) continue;
|
|
5548
|
+
details.push(`seed fleet ${basename4(shared.rootPath)} from ${donor}`);
|
|
5549
|
+
changedFiles.push(shared.rootPath);
|
|
5550
|
+
if (!ctx.dryRun) copyFileSync(donor, shared.rootPath);
|
|
5551
|
+
}
|
|
5552
|
+
if (existsSync10(plan.profileDir) && lstatSync2(plan.profileDir).isSymbolicLink()) {
|
|
5553
|
+
details.push(`convert profile symlink to real dir: ${plan.profileDir}`);
|
|
5554
|
+
changedFiles.push(plan.profileDir);
|
|
5555
|
+
if (!ctx.dryRun) unlinkSync3(plan.profileDir);
|
|
5556
|
+
}
|
|
5557
|
+
if (!existsSync10(plan.profileDir)) {
|
|
5558
|
+
details.push(`create profile dir: ${plan.profileDir}`);
|
|
5559
|
+
changedFiles.push(plan.profileDir);
|
|
5560
|
+
if (!ctx.dryRun) mkdirSync6(plan.profileDir, { recursive: true });
|
|
5561
|
+
}
|
|
5562
|
+
for (const link of plan.links) {
|
|
5563
|
+
const state = linkState(link.path, link.target);
|
|
5564
|
+
if (state === "ok") continue;
|
|
5565
|
+
if (link.ensureTargetDir && !existsSync10(link.target) && !ctx.dryRun) {
|
|
5566
|
+
mkdirSync6(link.target, { recursive: true });
|
|
5567
|
+
}
|
|
5568
|
+
details.push(`link ${link.path} -> ${link.target}`);
|
|
5569
|
+
changedFiles.push(link.path);
|
|
5570
|
+
if (ctx.dryRun) continue;
|
|
5571
|
+
if (existsSync10(link.path) || isDanglingLink(link.path)) {
|
|
5572
|
+
const lst = lstatSync2(link.path);
|
|
5573
|
+
if (lst.isSymbolicLink()) {
|
|
5574
|
+
unlinkSync3(link.path);
|
|
5575
|
+
} else {
|
|
5576
|
+
const parked = `${link.path}.pre-singleton`;
|
|
5577
|
+
renameSync2(link.path, parked);
|
|
5578
|
+
details.push(`parked pre-existing ${link.path} at ${parked}`);
|
|
5579
|
+
}
|
|
5580
|
+
}
|
|
5581
|
+
ensureParent(link.path);
|
|
5582
|
+
symlinkSync(link.target, link.path);
|
|
5583
|
+
}
|
|
5584
|
+
}
|
|
5585
|
+
return {
|
|
5586
|
+
id: finding.id,
|
|
5587
|
+
title: finding.title,
|
|
5588
|
+
status: details.some((d) => d.startsWith("blocked:")) ? "blocked" : changedFiles.length ? ctx.dryRun ? "skipped" : "applied" : "noop",
|
|
5589
|
+
summary: changedFiles.length ? ctx.dryRun ? "Planned singleton-runtime wiring" : "Singleton runtime wired" : "No changes required",
|
|
5590
|
+
changedFiles,
|
|
5591
|
+
details
|
|
5592
|
+
};
|
|
5593
|
+
}
|
|
5594
|
+
},
|
|
5595
|
+
{
|
|
5596
|
+
id: "hermes.profile-wiring",
|
|
5597
|
+
title: "Launcher + systemd HERMES_HOME points at the named profile",
|
|
5598
|
+
audit: (ctx) => {
|
|
5599
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
5600
|
+
if (!roles.length) {
|
|
5601
|
+
return { id: "hermes.profile-wiring", title: "Launcher + systemd HERMES_HOME points at the named profile", status: "skip", summary: "No Hermes roles present", details: [], fixable: false };
|
|
5602
|
+
}
|
|
5603
|
+
const details = [];
|
|
5604
|
+
for (const role of roles) {
|
|
5605
|
+
const plan = singletonPlan(ctx, role);
|
|
5606
|
+
const launcher = join15(role.roleDir, "hermes");
|
|
5607
|
+
const text2 = safeReadText(launcher);
|
|
5608
|
+
if (text2 === null) {
|
|
5609
|
+
details.push(`launcher missing: ${relative2(ctx.repoRoot, launcher)}`);
|
|
5610
|
+
} else {
|
|
5611
|
+
if (/^HERMES_HOME="\$RUNTIME_HOME"\s*$/m.test(text2)) {
|
|
5612
|
+
details.push(`launcher sets HERMES_HOME to the raw runtime path (disables shared auth + profile identity): ${relative2(ctx.repoRoot, launcher)}`);
|
|
5613
|
+
}
|
|
5614
|
+
if (/HERMES_OAUTH_FILE/.test(text2)) {
|
|
5615
|
+
details.push(`launcher exports HERMES_OAUTH_FILE, which Hermes does not implement (dead config): ${relative2(ctx.repoRoot, launcher)}`);
|
|
5616
|
+
}
|
|
5617
|
+
}
|
|
5618
|
+
for (const unit of profileUnits(role)) {
|
|
5619
|
+
const unitPath = join15(ctx.homeDir, ".config", "systemd", "user", unit);
|
|
5620
|
+
const unitText = safeReadText(unitPath);
|
|
5621
|
+
if (unitText === null) continue;
|
|
5622
|
+
const current = /^Environment=HERMES_HOME=(.*)$/m.exec(unitText)?.[1]?.trim();
|
|
5623
|
+
if (current && current !== plan.profileDir) {
|
|
5624
|
+
details.push(`${unit} HERMES_HOME=${current} (expected ${plan.profileDir})`);
|
|
5625
|
+
}
|
|
5626
|
+
if (/^Environment=HERMES_OAUTH_FILE=/m.test(unitText)) {
|
|
5627
|
+
details.push(`${unit} sets HERMES_OAUTH_FILE (dead config)`);
|
|
5628
|
+
}
|
|
5629
|
+
}
|
|
5630
|
+
}
|
|
5631
|
+
return {
|
|
5632
|
+
id: "hermes.profile-wiring",
|
|
5633
|
+
title: "Launcher + systemd HERMES_HOME points at the named profile",
|
|
5634
|
+
status: details.length === 0 ? "pass" : "fail",
|
|
5635
|
+
summary: details.length === 0 ? "HERMES_HOME wiring is in parity" : `${details.length} HERMES_HOME wiring issue(s) detected`,
|
|
5636
|
+
details,
|
|
5637
|
+
fixable: true
|
|
5638
|
+
};
|
|
5639
|
+
},
|
|
5640
|
+
migrate: (ctx, finding) => {
|
|
5641
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
5642
|
+
const changedFiles = [];
|
|
5643
|
+
const details = [];
|
|
5644
|
+
let unitsTouched = false;
|
|
5645
|
+
for (const role of roles) {
|
|
5646
|
+
const plan = singletonPlan(ctx, role);
|
|
5647
|
+
const launcher = join15(role.roleDir, "hermes");
|
|
5648
|
+
const text2 = safeReadText(launcher);
|
|
5649
|
+
if (text2 !== null) {
|
|
5650
|
+
const rewritten = rewriteLauncher(text2);
|
|
5651
|
+
if (rewritten !== text2) {
|
|
5652
|
+
details.push(`rewrite launcher HERMES_HOME -> profile path: ${relative2(ctx.repoRoot, launcher)}`);
|
|
5653
|
+
writeIfDifferent(launcher, rewritten, ctx.dryRun, changedFiles, 493);
|
|
5654
|
+
}
|
|
5655
|
+
}
|
|
5656
|
+
for (const unit of profileUnits(role)) {
|
|
5657
|
+
const unitPath = join15(ctx.homeDir, ".config", "systemd", "user", unit);
|
|
5658
|
+
const unitText = safeReadText(unitPath);
|
|
5659
|
+
if (unitText === null) continue;
|
|
5660
|
+
let next = unitText.replace(/^Environment=HERMES_HOME=.*$/m, `Environment=HERMES_HOME=${plan.profileDir}`);
|
|
5661
|
+
next = next.replace(/^Environment=HERMES_OAUTH_FILE=.*\n/m, "");
|
|
5662
|
+
if (next !== unitText) {
|
|
5663
|
+
details.push(`repoint ${unit} HERMES_HOME -> ${plan.profileDir}`);
|
|
5664
|
+
writeIfDifferent(unitPath, next, ctx.dryRun, changedFiles);
|
|
5665
|
+
unitsTouched = true;
|
|
5666
|
+
}
|
|
5667
|
+
}
|
|
5668
|
+
}
|
|
5669
|
+
if (unitsTouched && !ctx.dryRun) {
|
|
5670
|
+
systemctlUser(["daemon-reload"]);
|
|
5671
|
+
details.push("systemctl --user daemon-reload (restart units to pick up the new HERMES_HOME)");
|
|
5672
|
+
}
|
|
5673
|
+
return {
|
|
5674
|
+
id: finding.id,
|
|
5675
|
+
title: finding.title,
|
|
5676
|
+
status: changedFiles.length ? ctx.dryRun ? "skipped" : "applied" : "noop",
|
|
5677
|
+
summary: changedFiles.length ? ctx.dryRun ? "Planned HERMES_HOME rewiring" : "HERMES_HOME rewired to named profiles" : "No changes required",
|
|
5678
|
+
changedFiles,
|
|
5679
|
+
details
|
|
5680
|
+
};
|
|
5681
|
+
}
|
|
5682
|
+
},
|
|
5683
|
+
{
|
|
5684
|
+
id: "hermes.registry-parity",
|
|
5685
|
+
title: "Fleet registry matches .project.json (no duplicate or stale agents)",
|
|
5686
|
+
audit: (ctx) => {
|
|
5687
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
5688
|
+
const details = [];
|
|
5689
|
+
const registryPath2 = join15(ctx.homeDir, ".hermes", "agents-registry.yaml");
|
|
5690
|
+
const registry = readRegistry(registryPath2);
|
|
5691
|
+
if (!registry) {
|
|
5692
|
+
if (!roles.length && declaredAgentIds(ctx.repoRoot).length === 0) {
|
|
5693
|
+
return { id: "hermes.registry-parity", title: "Fleet registry matches .project.json (no duplicate or stale agents)", status: "skip", summary: "No Hermes roles or declared agents present", details: [], fixable: false };
|
|
5694
|
+
}
|
|
5695
|
+
return { id: "hermes.registry-parity", title: "Fleet registry matches .project.json (no duplicate or stale agents)", status: "warn", summary: `registry unreadable at ${registryPath2}`, details: [], fixable: false };
|
|
5696
|
+
}
|
|
5697
|
+
const canonical = new Set(roles.map((role) => role.agentId).filter(Boolean));
|
|
5698
|
+
const owned = ownedRegistryEntries(registry, ctx.repoRoot);
|
|
5699
|
+
const unprovisioned = unprovisionedRoleAgents(registry, ctx.repoRoot, canonical);
|
|
5700
|
+
if (unprovisioned.length) {
|
|
5701
|
+
return {
|
|
5702
|
+
id: "hermes.registry-parity",
|
|
5703
|
+
title: "Fleet registry matches .project.json (no duplicate or stale agents)",
|
|
5704
|
+
status: "fail",
|
|
5705
|
+
summary: `${unprovisioned.length} unprovisioned Hermes role blocker(s) detected`,
|
|
5706
|
+
details: unprovisioned.map(
|
|
5707
|
+
({ agentId, roleDir, sources }) => `agent "${agentId}" (${sources.join(" + ")}) has no role.yaml${roleDir ? ` at ${roleDir}` : ""}; provision or restore the role, do not delete its registry/declaration`
|
|
5708
|
+
),
|
|
5709
|
+
fixable: false
|
|
5710
|
+
};
|
|
5711
|
+
}
|
|
5712
|
+
if (canonical.size === 0) {
|
|
5713
|
+
return { id: "hermes.registry-parity", title: "Fleet registry matches .project.json (no duplicate or stale agents)", status: "skip", summary: "No Hermes roles, declarations, or registry entries present", details: [], fixable: false };
|
|
5714
|
+
}
|
|
5715
|
+
for (const [agentId, entry] of owned) {
|
|
5716
|
+
const roleDir = String(entry?.role_dir ?? "");
|
|
5717
|
+
if (!canonical.has(agentId)) {
|
|
5718
|
+
details.push(`stale/duplicate registry agent "${agentId}" for ${roleDir} (role.yaml declares ${[...canonical].join(", ")})`);
|
|
5719
|
+
}
|
|
5720
|
+
}
|
|
5721
|
+
for (const extra of declaredAgentIds(ctx.repoRoot).filter((id) => !canonical.has(id))) {
|
|
5722
|
+
details.push(`.project.json declares agent "${extra}" that no role.yaml claims`);
|
|
5723
|
+
}
|
|
5724
|
+
for (const role of roles) {
|
|
5725
|
+
const entry = registry[role.agentId];
|
|
5726
|
+
if (!entry) {
|
|
5727
|
+
details.push(`registry is missing an entry for ${role.agentId}`);
|
|
5728
|
+
continue;
|
|
5729
|
+
}
|
|
5730
|
+
const entryRoleDir = String(entry.role_dir ?? "");
|
|
5731
|
+
if (entryRoleDir && realOrSelf(entryRoleDir) !== realOrSelf(role.roleDir)) {
|
|
5732
|
+
details.push(`registry role_dir for ${role.agentId} is ${entryRoleDir} (expected ${role.roleDir})`);
|
|
5733
|
+
}
|
|
5734
|
+
const bin = String(entry.hermes?.bin ?? "");
|
|
5735
|
+
if (bin && !existsSync10(bin)) {
|
|
5736
|
+
details.push(`registry hermes.bin for ${role.agentId} does not exist: ${bin}`);
|
|
5737
|
+
}
|
|
5738
|
+
}
|
|
5739
|
+
return {
|
|
5740
|
+
id: "hermes.registry-parity",
|
|
5741
|
+
title: "Fleet registry matches .project.json (no duplicate or stale agents)",
|
|
5742
|
+
status: details.length === 0 ? "pass" : "fail",
|
|
5743
|
+
summary: details.length === 0 ? "Fleet registry is in parity" : `${details.length} registry parity issue(s) detected`,
|
|
5744
|
+
details,
|
|
5745
|
+
fixable: true
|
|
5746
|
+
};
|
|
5747
|
+
},
|
|
5748
|
+
migrate: (ctx, finding) => {
|
|
5749
|
+
const changedFiles = [];
|
|
5750
|
+
const details = [];
|
|
5751
|
+
const registryPath2 = join15(ctx.homeDir, ".hermes", "agents-registry.yaml");
|
|
5752
|
+
const raw = safeReadText(registryPath2);
|
|
5753
|
+
if (raw === null) {
|
|
5754
|
+
return { id: finding.id, title: finding.title, status: "blocked", summary: `registry unreadable at ${registryPath2}`, changedFiles, details };
|
|
5755
|
+
}
|
|
5756
|
+
let doc;
|
|
5757
|
+
try {
|
|
5758
|
+
doc = YAML3.parse(raw);
|
|
5759
|
+
} catch {
|
|
5760
|
+
return { id: finding.id, title: finding.title, status: "blocked", summary: "registry is not valid YAML", changedFiles, details };
|
|
5761
|
+
}
|
|
5762
|
+
const agents = doc?.agents ?? {};
|
|
5763
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
5764
|
+
const canonical = new Set(roles.map((role) => role.agentId).filter(Boolean));
|
|
5765
|
+
const unprovisioned = unprovisionedRoleAgents(agents, ctx.repoRoot, canonical);
|
|
5766
|
+
if (unprovisioned.length) {
|
|
5767
|
+
return {
|
|
5768
|
+
id: finding.id,
|
|
5769
|
+
title: finding.title,
|
|
5770
|
+
status: "blocked",
|
|
5771
|
+
summary: "Registry parity is blocked by an unprovisioned Hermes role",
|
|
5772
|
+
changedFiles,
|
|
5773
|
+
details: unprovisioned.map(
|
|
5774
|
+
({ agentId, roleDir, sources }) => `blocked: "${agentId}" (${sources.join(" + ")}) has no role.yaml${roleDir ? ` at ${roleDir}` : ""}; provision or restore the role without pruning registry/declaration state`
|
|
5775
|
+
)
|
|
5776
|
+
};
|
|
5777
|
+
}
|
|
5778
|
+
const fleetBin = fleetBinPath(ctx);
|
|
5779
|
+
let dirty = false;
|
|
5780
|
+
if (canonical.size === 0) {
|
|
5781
|
+
for (const [agentId] of ownedRegistryEntries(agents, ctx.repoRoot)) {
|
|
5782
|
+
details.push(`blocked: "${agentId}" has no role.yaml; provision the role instead of pruning the registry`);
|
|
5783
|
+
}
|
|
5784
|
+
for (const agentId of declaredAgentIds(ctx.repoRoot)) {
|
|
5785
|
+
if (!details.some((detail) => detail.includes(`"${agentId}"`))) {
|
|
5786
|
+
details.push(`blocked: "${agentId}" is declared but has no role.yaml; provision or restore the role`);
|
|
5787
|
+
}
|
|
5788
|
+
}
|
|
5789
|
+
if (details.length) {
|
|
5790
|
+
return {
|
|
5791
|
+
id: finding.id,
|
|
5792
|
+
title: finding.title,
|
|
5793
|
+
status: "blocked",
|
|
5794
|
+
summary: "Registry parity is blocked by an unprovisioned Hermes role",
|
|
5795
|
+
changedFiles,
|
|
5796
|
+
details
|
|
5797
|
+
};
|
|
5798
|
+
}
|
|
5799
|
+
}
|
|
5800
|
+
for (const [agentId, entry] of ownedRegistryEntries(agents, ctx.repoRoot)) {
|
|
5801
|
+
if (canonical.size > 0 && !canonical.has(agentId)) {
|
|
5802
|
+
details.push(`drop stale/duplicate registry agent "${agentId}"`);
|
|
5803
|
+
delete agents[agentId];
|
|
5804
|
+
dropDeclaredAgent(ctx, agentId, changedFiles, details);
|
|
5805
|
+
dirty = true;
|
|
5806
|
+
continue;
|
|
5807
|
+
}
|
|
5808
|
+
const hermes = entry.hermes ?? {};
|
|
5809
|
+
if (fleetBin && String(hermes.bin ?? "") !== fleetBin && !existsSync10(String(hermes.bin ?? ""))) {
|
|
5810
|
+
details.push(`repoint ${agentId} hermes.bin -> ${fleetBin}`);
|
|
5811
|
+
hermes.bin = fleetBin;
|
|
5812
|
+
entry.hermes = hermes;
|
|
5813
|
+
dirty = true;
|
|
5814
|
+
}
|
|
5815
|
+
if (hermes.oauth_file) {
|
|
5816
|
+
details.push(`drop dead hermes.oauth_file from ${agentId}`);
|
|
5817
|
+
delete hermes.oauth_file;
|
|
5818
|
+
dirty = true;
|
|
5819
|
+
}
|
|
5820
|
+
}
|
|
5821
|
+
if (canonical.size > 0) {
|
|
5822
|
+
for (const extra of declaredAgentIds(ctx.repoRoot).filter((id) => !canonical.has(id))) {
|
|
5823
|
+
dropDeclaredAgent(ctx, extra, changedFiles, details);
|
|
5824
|
+
}
|
|
5825
|
+
}
|
|
5826
|
+
if (dirty) {
|
|
5827
|
+
changedFiles.push(registryPath2);
|
|
5828
|
+
if (!ctx.dryRun) {
|
|
5829
|
+
doc.agents = agents;
|
|
5830
|
+
writeText(registryPath2, YAML3.stringify(doc));
|
|
5831
|
+
}
|
|
5832
|
+
}
|
|
5833
|
+
return {
|
|
5834
|
+
id: finding.id,
|
|
5835
|
+
title: finding.title,
|
|
5836
|
+
status: changedFiles.length ? ctx.dryRun ? "skipped" : "applied" : "noop",
|
|
5837
|
+
summary: changedFiles.length ? ctx.dryRun ? "Planned registry repair" : "Fleet registry repaired" : "No changes required",
|
|
5838
|
+
changedFiles,
|
|
5839
|
+
details
|
|
5840
|
+
};
|
|
5841
|
+
}
|
|
4371
5842
|
}
|
|
4372
5843
|
];
|
|
4373
5844
|
function writeIfDifferent(path, content, dryRun, changedFiles, mode) {
|
|
@@ -4386,7 +5857,7 @@ function getParityRuleIds() {
|
|
|
4386
5857
|
function runAudit(repoArg) {
|
|
4387
5858
|
const pjanglerRoot = resolvePjanglerRoot2();
|
|
4388
5859
|
const ctx = {
|
|
4389
|
-
repoRoot:
|
|
5860
|
+
repoRoot: resolve3(repoArg ?? process.cwd()),
|
|
4390
5861
|
dryRun: true,
|
|
4391
5862
|
pjanglerRoot,
|
|
4392
5863
|
homeDir: homedir5()
|
|
@@ -4402,7 +5873,7 @@ function runAudit(repoArg) {
|
|
|
4402
5873
|
function runMigrationForRules(ruleIds, repoArg, dryRun) {
|
|
4403
5874
|
const pjanglerRoot = resolvePjanglerRoot2();
|
|
4404
5875
|
const ctx = {
|
|
4405
|
-
repoRoot:
|
|
5876
|
+
repoRoot: resolve3(repoArg ?? process.cwd()),
|
|
4406
5877
|
dryRun,
|
|
4407
5878
|
pjanglerRoot,
|
|
4408
5879
|
homeDir: homedir5()
|
|
@@ -4436,7 +5907,22 @@ function runMigrationForRules(ruleIds, repoArg, dryRun) {
|
|
|
4436
5907
|
};
|
|
4437
5908
|
}
|
|
4438
5909
|
function runMigration(selector, repoArg, dryRun, all) {
|
|
4439
|
-
|
|
5910
|
+
if (all) {
|
|
5911
|
+
const audit = runAudit(repoArg);
|
|
5912
|
+
const ruleIds2 = audit.rules.filter((finding) => finding.fixable && (finding.status === "fail" || finding.status === "warn")).map((finding) => finding.id);
|
|
5913
|
+
if (ruleIds2.length === 0) {
|
|
5914
|
+
return {
|
|
5915
|
+
repo: audit.repo,
|
|
5916
|
+
dryRun,
|
|
5917
|
+
ok: true,
|
|
5918
|
+
selectedRules: [],
|
|
5919
|
+
results: [],
|
|
5920
|
+
changedFiles: []
|
|
5921
|
+
};
|
|
5922
|
+
}
|
|
5923
|
+
return runMigrationForRules(ruleIds2, repoArg, dryRun);
|
|
5924
|
+
}
|
|
5925
|
+
const ruleIds = selector ? [selector] : [];
|
|
4440
5926
|
return runMigrationForRules(ruleIds, repoArg, dryRun);
|
|
4441
5927
|
}
|
|
4442
5928
|
function prettyTimestamp(iso) {
|
|
@@ -4473,7 +5959,7 @@ var server = new McpServer({
|
|
|
4473
5959
|
});
|
|
4474
5960
|
var TICKET_PROVIDER_SCHEMA = z.enum(["plane", "trello"]);
|
|
4475
5961
|
function resolveTargetDir(targetDir) {
|
|
4476
|
-
const dir =
|
|
5962
|
+
const dir = resolve4(targetDir ?? process.cwd());
|
|
4477
5963
|
if (!existsSync11(dir)) {
|
|
4478
5964
|
throw new Error(`Target directory does not exist: ${dir}`);
|
|
4479
5965
|
}
|
|
@@ -4485,7 +5971,7 @@ function resolveTargetDir(targetDir) {
|
|
|
4485
5971
|
function resolvePjanglerRoot3() {
|
|
4486
5972
|
let dir = dirname8(fileURLToPath5(import.meta.url));
|
|
4487
5973
|
while (dir !== dirname8(dir)) {
|
|
4488
|
-
if (existsSync11(
|
|
5974
|
+
if (existsSync11(join16(dir, "package.json")) && existsSync11(join16(dir, "templates", "commonproject", "copier.yml"))) {
|
|
4489
5975
|
return dir;
|
|
4490
5976
|
}
|
|
4491
5977
|
dir = dirname8(dir);
|
|
@@ -4686,9 +6172,9 @@ server.registerTool(
|
|
|
4686
6172
|
try {
|
|
4687
6173
|
const pjanglerRoot = resolvePjanglerRoot3();
|
|
4688
6174
|
const projectSlug = input.projectSlug ?? slugify(input.projectName);
|
|
4689
|
-
const parentDir =
|
|
6175
|
+
const parentDir = resolve4(input.parentDir ?? process.cwd());
|
|
4690
6176
|
if (!existsSync11(parentDir) || !statSync2(parentDir).isDirectory()) throw new Error(`Parent directory does not exist: ${parentDir}`);
|
|
4691
|
-
const targetDir =
|
|
6177
|
+
const targetDir = resolve4(input.targetDir ?? join16(parentDir, projectSlug));
|
|
4692
6178
|
const overwrite = input.overwrite ?? input.force ?? false;
|
|
4693
6179
|
const dryRun = input.dryRun ?? true;
|
|
4694
6180
|
const local = input.local ?? true;
|
|
@@ -4931,7 +6417,7 @@ server.registerTool(
|
|
|
4931
6417
|
targetDir: resolvedTarget,
|
|
4932
6418
|
yes: true,
|
|
4933
6419
|
local,
|
|
4934
|
-
targetRepo: input.targetRepo ??
|
|
6420
|
+
targetRepo: input.targetRepo ?? basename5(resolvedTarget),
|
|
4935
6421
|
role: input.role,
|
|
4936
6422
|
agentPurpose: input.agentPurpose,
|
|
4937
6423
|
soulTone: input.soulTone,
|