@delorenj/pjangler 1.2.12 → 1.2.13
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/mcp-server.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/mcp-server.ts
|
|
4
|
-
import { existsSync as
|
|
5
|
-
import { basename as basename4, dirname as dirname8, join as
|
|
4
|
+
import { existsSync as existsSync10, statSync as statSync2 } from "node:fs";
|
|
5
|
+
import { basename as basename4, dirname as dirname8, join as join13, resolve as resolve3 } from "node:path";
|
|
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";
|
|
@@ -902,10 +902,100 @@ var RunCopierTemplate = class extends Command {
|
|
|
902
902
|
}
|
|
903
903
|
};
|
|
904
904
|
|
|
905
|
-
// src/commands/hermes/
|
|
905
|
+
// src/commands/hermes/UntrackHermesRuntimes.ts
|
|
906
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync3, readdirSync } from "fs";
|
|
907
|
+
import { join as join6 } from "path";
|
|
906
908
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
907
|
-
|
|
908
|
-
|
|
909
|
+
var UntrackHermesRuntimes = class extends Command {
|
|
910
|
+
async invoke() {
|
|
911
|
+
const targetDir = this.context.targetDir;
|
|
912
|
+
const rolesDir = join6(targetDir, "agents", "hermes");
|
|
913
|
+
if (!existsSync4(rolesDir)) {
|
|
914
|
+
return {
|
|
915
|
+
success: true,
|
|
916
|
+
message: "No Hermes agents found (no agents/hermes directory)."
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
const roles = readdirSync(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
920
|
+
if (roles.length === 0) {
|
|
921
|
+
return {
|
|
922
|
+
success: true,
|
|
923
|
+
message: "No Hermes agents found."
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
let modifiedAny = false;
|
|
927
|
+
const details = [];
|
|
928
|
+
for (const role of roles) {
|
|
929
|
+
const roleDir = join6("agents", "hermes", role);
|
|
930
|
+
const runtimePath = join6(roleDir, "runtime");
|
|
931
|
+
const gitignorePath = join6(roleDir, ".gitignore");
|
|
932
|
+
let isTracked = false;
|
|
933
|
+
const lsResult = spawnSync2("git", ["ls-files", "--stage", runtimePath], {
|
|
934
|
+
cwd: targetDir,
|
|
935
|
+
encoding: "utf8"
|
|
936
|
+
});
|
|
937
|
+
if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
|
|
938
|
+
isTracked = true;
|
|
939
|
+
}
|
|
940
|
+
let isIgnored = false;
|
|
941
|
+
const fullGitignorePath = join6(targetDir, gitignorePath);
|
|
942
|
+
if (existsSync4(fullGitignorePath)) {
|
|
943
|
+
const content = readFileSync2(fullGitignorePath, "utf8");
|
|
944
|
+
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
945
|
+
isIgnored = lines.includes("runtime/") || lines.includes("runtime");
|
|
946
|
+
}
|
|
947
|
+
if (isTracked || !isIgnored) {
|
|
948
|
+
modifiedAny = true;
|
|
949
|
+
if (isTracked) {
|
|
950
|
+
details.push(`untrack agents/hermes/${role}/runtime`);
|
|
951
|
+
if (!this.context.dryRun) {
|
|
952
|
+
const rmResult = spawnSync2("git", ["rm", "--cached", "-r", runtimePath], {
|
|
953
|
+
cwd: targetDir,
|
|
954
|
+
encoding: "utf8"
|
|
955
|
+
});
|
|
956
|
+
if (rmResult.status !== 0) {
|
|
957
|
+
return {
|
|
958
|
+
success: false,
|
|
959
|
+
message: `Failed to untrack agents/hermes/${role}/runtime: ${rmResult.stderr}`
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
if (!isIgnored) {
|
|
965
|
+
details.push(`ignore runtime/ in agents/hermes/${role}/.gitignore`);
|
|
966
|
+
if (!this.context.dryRun) {
|
|
967
|
+
let content = "";
|
|
968
|
+
if (existsSync4(fullGitignorePath)) {
|
|
969
|
+
content = readFileSync2(fullGitignorePath, "utf8");
|
|
970
|
+
}
|
|
971
|
+
if (content && !content.endsWith("\n")) {
|
|
972
|
+
content += "\n";
|
|
973
|
+
}
|
|
974
|
+
content += "runtime/\n";
|
|
975
|
+
writeFileSync3(fullGitignorePath, content, "utf8");
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
if (!modifiedAny) {
|
|
981
|
+
return {
|
|
982
|
+
success: true,
|
|
983
|
+
message: "\u2705 All Hermes agent runtimes are already untracked and gitignored."
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
const actionText = this.context.dryRun ? "Would make" : "Made";
|
|
987
|
+
return {
|
|
988
|
+
success: true,
|
|
989
|
+
message: `${actionText} Hermes agent runtimes untracked and gitignored:
|
|
990
|
+
${details.map((d) => ` - ${d}`).join("\n")}`
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
// src/commands/hermes/WireTelegram.ts
|
|
996
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
997
|
+
import { join as join7 } from "node:path";
|
|
998
|
+
import { existsSync as existsSync5, unlinkSync } from "node:fs";
|
|
909
999
|
import * as p3 from "@clack/prompts";
|
|
910
1000
|
var WireTelegram = class extends Command {
|
|
911
1001
|
async invoke() {
|
|
@@ -927,7 +1017,7 @@ var WireTelegram = class extends Command {
|
|
|
927
1017
|
let token = process.env.TELEGRAM_BOT_TOKEN;
|
|
928
1018
|
let source = token ? "env" : null;
|
|
929
1019
|
if (!token) {
|
|
930
|
-
const tryOp =
|
|
1020
|
+
const tryOp = spawnSync3("op", ["read", vaultRef], { encoding: "utf8" });
|
|
931
1021
|
if (tryOp.status === 0) {
|
|
932
1022
|
token = tryOp.stdout.trim();
|
|
933
1023
|
source = "op";
|
|
@@ -966,7 +1056,7 @@ var WireTelegram = class extends Command {
|
|
|
966
1056
|
initialValue: true
|
|
967
1057
|
});
|
|
968
1058
|
if (!p3.isCancel(persist) && persist) {
|
|
969
|
-
const create =
|
|
1059
|
+
const create = spawnSync3(
|
|
970
1060
|
"op",
|
|
971
1061
|
[
|
|
972
1062
|
"item",
|
|
@@ -993,18 +1083,18 @@ var WireTelegram = class extends Command {
|
|
|
993
1083
|
if (p3.isCancel(allowedAnswer)) {
|
|
994
1084
|
return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
|
|
995
1085
|
}
|
|
996
|
-
const script =
|
|
997
|
-
if (!
|
|
1086
|
+
const script = join7(roleDir, ".scripts", "30-telegram.sh");
|
|
1087
|
+
if (!existsSync5(script)) {
|
|
998
1088
|
return {
|
|
999
1089
|
success: false,
|
|
1000
1090
|
message: `\u2717 ${script} not found. Did copier finish? Re-run with --skip-runtime-repo=0 if you skipped it.`
|
|
1001
1091
|
};
|
|
1002
1092
|
}
|
|
1003
|
-
const marker =
|
|
1004
|
-
if (
|
|
1093
|
+
const marker = join7(roleDir, ".scripts", ".done-30-telegram");
|
|
1094
|
+
if (existsSync5(marker)) unlinkSync(marker);
|
|
1005
1095
|
const spinner4 = p3.spinner();
|
|
1006
1096
|
spinner4.start("Verifying token + wiring profile");
|
|
1007
|
-
const result =
|
|
1097
|
+
const result = spawnSync3("bash", [script], {
|
|
1008
1098
|
stdio: "inherit",
|
|
1009
1099
|
env: {
|
|
1010
1100
|
...process.env,
|
|
@@ -1027,9 +1117,9 @@ function cap(s) {
|
|
|
1027
1117
|
}
|
|
1028
1118
|
|
|
1029
1119
|
// src/commands/hermes/WireEmail.ts
|
|
1030
|
-
import { spawnSync as
|
|
1031
|
-
import { join as
|
|
1032
|
-
import { existsSync as
|
|
1120
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
1121
|
+
import { join as join8 } from "node:path";
|
|
1122
|
+
import { existsSync as existsSync6, unlinkSync as unlinkSync2 } from "node:fs";
|
|
1033
1123
|
import * as p4 from "@clack/prompts";
|
|
1034
1124
|
var WireEmail = class extends Command {
|
|
1035
1125
|
async invoke() {
|
|
@@ -1044,13 +1134,13 @@ var WireEmail = class extends Command {
|
|
|
1044
1134
|
if (!targetRepo || !role || !roleDir) {
|
|
1045
1135
|
return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
|
|
1046
1136
|
}
|
|
1047
|
-
const script =
|
|
1048
|
-
if (!
|
|
1137
|
+
const script = join8(roleDir, ".scripts", "50-email.sh");
|
|
1138
|
+
if (!existsSync6(script)) {
|
|
1049
1139
|
return { success: false, message: `\u2717 ${script} not found` };
|
|
1050
1140
|
}
|
|
1051
1141
|
let token = process.env.CF_EMAIL_ROUTING_TOKEN;
|
|
1052
1142
|
if (!token) {
|
|
1053
|
-
const tryOp =
|
|
1143
|
+
const tryOp = spawnSync4(
|
|
1054
1144
|
"op",
|
|
1055
1145
|
["read", "op://DeLoSecrets/Cloudflare-EmailRouting/token"],
|
|
1056
1146
|
{ encoding: "utf8" }
|
|
@@ -1090,7 +1180,7 @@ var WireEmail = class extends Command {
|
|
|
1090
1180
|
initialValue: true
|
|
1091
1181
|
});
|
|
1092
1182
|
if (!p4.isCancel(persist) && persist) {
|
|
1093
|
-
const create =
|
|
1183
|
+
const create = spawnSync4(
|
|
1094
1184
|
"op",
|
|
1095
1185
|
[
|
|
1096
1186
|
"item",
|
|
@@ -1107,11 +1197,11 @@ var WireEmail = class extends Command {
|
|
|
1107
1197
|
}
|
|
1108
1198
|
}
|
|
1109
1199
|
}
|
|
1110
|
-
const marker =
|
|
1111
|
-
if (
|
|
1200
|
+
const marker = join8(roleDir, ".scripts", ".done-50-email");
|
|
1201
|
+
if (existsSync6(marker)) unlinkSync2(marker);
|
|
1112
1202
|
const spinner4 = p4.spinner();
|
|
1113
1203
|
spinner4.start("Creating Cloudflare Email Routing rule");
|
|
1114
|
-
const result =
|
|
1204
|
+
const result = spawnSync4("bash", [script], {
|
|
1115
1205
|
stdio: "inherit",
|
|
1116
1206
|
env: { ...process.env, SKIP_EMAIL: "0", CF_EMAIL_ROUTING_TOKEN: token },
|
|
1117
1207
|
cwd: roleDir
|
|
@@ -1171,7 +1261,7 @@ var PrintHermesSummary = class extends Command {
|
|
|
1171
1261
|
var HermesAgentRecipe = class extends Recipe {
|
|
1172
1262
|
constructor(context) {
|
|
1173
1263
|
super(context);
|
|
1174
|
-
this.addIngredient(EnsureTemplateConfig).addIngredient(PromptForAgentConfig).addIngredient(RunCopierTemplate).addIngredient(WireTelegram).addIngredient(WireEmail).addIngredient(PrintHermesSummary);
|
|
1264
|
+
this.addIngredient(EnsureTemplateConfig).addIngredient(PromptForAgentConfig).addIngredient(RunCopierTemplate).addIngredient(UntrackHermesRuntimes).addIngredient(WireTelegram).addIngredient(WireEmail).addIngredient(PrintHermesSummary);
|
|
1175
1265
|
}
|
|
1176
1266
|
// Override execute() to suppress the base class's per-command logging since
|
|
1177
1267
|
// our commands already render their own UI via @clack/prompts.
|
|
@@ -1195,33 +1285,33 @@ var HermesAgentRecipe = class extends Recipe {
|
|
|
1195
1285
|
|
|
1196
1286
|
// src/commands/AgentHooksCommands.ts
|
|
1197
1287
|
import { homedir as homedir4 } from "node:os";
|
|
1198
|
-
import { join as
|
|
1199
|
-
import { existsSync as
|
|
1288
|
+
import { join as join10, dirname as dirname5 } from "node:path";
|
|
1289
|
+
import { existsSync as existsSync8, cpSync, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1200
1290
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1201
1291
|
|
|
1202
1292
|
// src/project/index.ts
|
|
1203
|
-
import { spawnSync as
|
|
1204
|
-
import { existsSync as
|
|
1293
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
1294
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync3, renameSync, statSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1205
1295
|
import { homedir as homedir3 } from "node:os";
|
|
1206
|
-
import { basename as basename2, delimiter, dirname as dirname4, join as
|
|
1296
|
+
import { basename as basename2, delimiter, dirname as dirname4, join as join9, resolve } from "node:path";
|
|
1207
1297
|
import YAML from "yaml";
|
|
1208
1298
|
var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
|
|
1209
1299
|
var PROJECT_SOURCE_SKILL_ROOTS_ENV = "PJ_SOURCE_SKILL_ROOTS";
|
|
1210
1300
|
var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
|
|
1211
1301
|
var DEFAULT_SOURCE_SKILL_ROOTS = [
|
|
1212
1302
|
"/home/delorenj/code/skillex/all-skills",
|
|
1213
|
-
|
|
1214
|
-
|
|
1303
|
+
join9(homedir3(), ".agents", "skills"),
|
|
1304
|
+
join9(homedir3(), ".codex", "skills")
|
|
1215
1305
|
];
|
|
1216
1306
|
function projectRegistryPath(env2 = process.env) {
|
|
1217
|
-
return expandHome(env2[PROJECT_REGISTRY_ENV] ||
|
|
1307
|
+
return expandHome(env2[PROJECT_REGISTRY_ENV] || join9(homedir3(), ".config", "pjangler", "projects.yaml"));
|
|
1218
1308
|
}
|
|
1219
1309
|
function emptyProjectRegistry() {
|
|
1220
1310
|
return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
|
|
1221
1311
|
}
|
|
1222
1312
|
function loadProjectRegistry(path = projectRegistryPath()) {
|
|
1223
|
-
if (!
|
|
1224
|
-
const raw = YAML.parse(
|
|
1313
|
+
if (!existsSync7(path)) return emptyProjectRegistry();
|
|
1314
|
+
const raw = YAML.parse(readFileSync3(path, "utf8"));
|
|
1225
1315
|
if (raw == null) return emptyProjectRegistry();
|
|
1226
1316
|
if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
|
|
1227
1317
|
const registry = raw;
|
|
@@ -1236,7 +1326,7 @@ function saveProjectRegistry(registry, path = projectRegistryPath()) {
|
|
|
1236
1326
|
validateProjectRegistry(registry);
|
|
1237
1327
|
mkdirSync4(dirname4(path), { recursive: true });
|
|
1238
1328
|
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
1239
|
-
|
|
1329
|
+
writeFileSync4(temp, YAML.stringify(registry, { lineWidth: 0 }), "utf8");
|
|
1240
1330
|
renameSync(temp, path);
|
|
1241
1331
|
}
|
|
1242
1332
|
function validateProjectRegistry(registry) {
|
|
@@ -1318,7 +1408,7 @@ function resolveAgentHooksLayer(input, env2 = process.env) {
|
|
|
1318
1408
|
const override = env2.PJ_AGENT_HOOKS_LAYER;
|
|
1319
1409
|
if (override === "0" || override === "false") return false;
|
|
1320
1410
|
if (override === "1" || override === "true") return true;
|
|
1321
|
-
return !
|
|
1411
|
+
return !existsSync7(join9(homedir3(), ".agents", "hooks"));
|
|
1322
1412
|
}
|
|
1323
1413
|
function jsonStable(value) {
|
|
1324
1414
|
return JSON.stringify(value);
|
|
@@ -1349,12 +1439,12 @@ function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
|
|
|
1349
1439
|
if (!sourceSkill) return void 0;
|
|
1350
1440
|
const expanded = expandHome(sourceSkill);
|
|
1351
1441
|
const direct = resolve(expanded);
|
|
1352
|
-
if (
|
|
1442
|
+
if (existsSync7(direct)) return direct;
|
|
1353
1443
|
const name = basename2(sourceSkill);
|
|
1354
1444
|
const roots = sourceSkillRoots(env2);
|
|
1355
1445
|
for (const root of roots) {
|
|
1356
|
-
const candidate =
|
|
1357
|
-
if (
|
|
1446
|
+
const candidate = join9(root, name);
|
|
1447
|
+
if (existsSync7(candidate)) return candidate;
|
|
1358
1448
|
}
|
|
1359
1449
|
const searched = roots.length ? ` Searched roots: ${roots.join(", ")}.` : "";
|
|
1360
1450
|
const hint = `${searched} Add project-specific roots with ${PROJECT_SOURCE_SKILL_ROOTS_ENV}.`;
|
|
@@ -1435,7 +1525,7 @@ function planProjectInit(input) {
|
|
|
1435
1525
|
}));
|
|
1436
1526
|
}
|
|
1437
1527
|
actions.push(
|
|
1438
|
-
{ kind: "project.write-manifest", path:
|
|
1528
|
+
{ kind: "project.write-manifest", path: join9(targetDir, ".project.json"), manifest },
|
|
1439
1529
|
{
|
|
1440
1530
|
kind: "ticket-provider.create-or-link",
|
|
1441
1531
|
enabled: live,
|
|
@@ -1476,7 +1566,7 @@ function executeProjectInitPlan(plan) {
|
|
|
1476
1566
|
action.data.agent_hooks_layer === "false" ? "commonproject: agent-hooks layer skipped (global ~/.agents/hooks detected \u2014 no per-user CLI injection)" : "commonproject: agent-hooks layer included"
|
|
1477
1567
|
);
|
|
1478
1568
|
mkdirSync4(dirname4(action.targetDir), { recursive: true });
|
|
1479
|
-
const result =
|
|
1569
|
+
const result = spawnSync5(action.command[0], action.command.slice(1), { encoding: "utf8", cwd: action.cwd });
|
|
1480
1570
|
if (result.stdout?.trim()) logs.push(result.stdout.trim());
|
|
1481
1571
|
if (result.stderr?.trim()) logs.push(result.stderr.trim());
|
|
1482
1572
|
if (result.error) {
|
|
@@ -1488,7 +1578,7 @@ function executeProjectInitPlan(plan) {
|
|
|
1488
1578
|
}
|
|
1489
1579
|
if (result.status !== 0) {
|
|
1490
1580
|
errors.push(`copier exited with status ${result.status ?? "unknown"}`);
|
|
1491
|
-
if (
|
|
1581
|
+
if (existsSync7(action.targetDir)) changedFiles.push(action.targetDir);
|
|
1492
1582
|
break;
|
|
1493
1583
|
}
|
|
1494
1584
|
changedFiles.push(action.targetDir);
|
|
@@ -1496,9 +1586,9 @@ function executeProjectInitPlan(plan) {
|
|
|
1496
1586
|
mkdirSync4(dirname4(action.path), { recursive: true });
|
|
1497
1587
|
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
1498
1588
|
`;
|
|
1499
|
-
const current =
|
|
1589
|
+
const current = existsSync7(action.path) ? readFileSync3(action.path, "utf8") : void 0;
|
|
1500
1590
|
if (current !== next) {
|
|
1501
|
-
|
|
1591
|
+
writeFileSync4(action.path, next, "utf8");
|
|
1502
1592
|
changedFiles.push(action.path);
|
|
1503
1593
|
}
|
|
1504
1594
|
} else if (action.kind === "registry.upsert") {
|
|
@@ -1551,7 +1641,7 @@ function getProject(registry, slug) {
|
|
|
1551
1641
|
return project;
|
|
1552
1642
|
}
|
|
1553
1643
|
function buildCommonProjectCopierAction(input) {
|
|
1554
|
-
const templateDir =
|
|
1644
|
+
const templateDir = join9(input.pjanglerRoot, "templates", "commonproject");
|
|
1555
1645
|
const data = {
|
|
1556
1646
|
project_name: input.projectName,
|
|
1557
1647
|
project_description: input.projectDescription ?? "",
|
|
@@ -1580,7 +1670,7 @@ function buildCommonProjectCopierAction(input) {
|
|
|
1580
1670
|
function resolvePjanglerRoot() {
|
|
1581
1671
|
let dir = dirname4(new URL(import.meta.url).pathname);
|
|
1582
1672
|
while (dir !== dirname4(dir)) {
|
|
1583
|
-
if (
|
|
1673
|
+
if (existsSync7(join9(dir, "package.json")) && existsSync7(join9(dir, "templates", "commonproject", "copier.yml"))) return dir;
|
|
1584
1674
|
dir = dirname4(dir);
|
|
1585
1675
|
}
|
|
1586
1676
|
return resolve(process.cwd());
|
|
@@ -1612,7 +1702,7 @@ function validateProjectRecord(project, key) {
|
|
|
1612
1702
|
}
|
|
1613
1703
|
function expandHome(path) {
|
|
1614
1704
|
if (path === "~") return homedir3();
|
|
1615
|
-
if (path.startsWith("~/")) return
|
|
1705
|
+
if (path.startsWith("~/")) return join9(homedir3(), path.slice(2));
|
|
1616
1706
|
return path;
|
|
1617
1707
|
}
|
|
1618
1708
|
function isRecord(value) {
|
|
@@ -1629,16 +1719,16 @@ function resolveTemplateRoot() {
|
|
|
1629
1719
|
try {
|
|
1630
1720
|
let dir = dirname5(fileURLToPath2(import.meta.url));
|
|
1631
1721
|
for (let i = 0; i < 8; i++) {
|
|
1632
|
-
candidates.push(
|
|
1722
|
+
candidates.push(join10(dir, "templates", "commonproject", "template"));
|
|
1633
1723
|
const parent = dirname5(dir);
|
|
1634
1724
|
if (parent === dir) break;
|
|
1635
1725
|
dir = parent;
|
|
1636
1726
|
}
|
|
1637
1727
|
} catch {
|
|
1638
1728
|
}
|
|
1639
|
-
candidates.push(
|
|
1729
|
+
candidates.push(join10(homedir4(), "code", "pjangler", "templates", "commonproject", "template"));
|
|
1640
1730
|
for (const c of candidates) {
|
|
1641
|
-
if (
|
|
1731
|
+
if (existsSync8(join10(c, ".agents", "hooks", "hooks.master.json"))) return c;
|
|
1642
1732
|
}
|
|
1643
1733
|
throw new Error(
|
|
1644
1734
|
"Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
|
|
@@ -1665,10 +1755,10 @@ var CopyAgentHooksTree = class extends Command {
|
|
|
1665
1755
|
const created = [];
|
|
1666
1756
|
const skipped = [];
|
|
1667
1757
|
for (const { rel, dir } of items) {
|
|
1668
|
-
const src =
|
|
1669
|
-
const dest =
|
|
1670
|
-
if (!
|
|
1671
|
-
if (
|
|
1758
|
+
const src = join10(templateRoot, rel);
|
|
1759
|
+
const dest = join10(this.context.targetDir, rel);
|
|
1760
|
+
if (!existsSync8(src)) continue;
|
|
1761
|
+
if (existsSync8(dest) && !this.context.force) {
|
|
1672
1762
|
skipped.push(rel);
|
|
1673
1763
|
continue;
|
|
1674
1764
|
}
|
|
@@ -1694,14 +1784,14 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
|
1694
1784
|
if (!resolveAgentHooksLayer()) {
|
|
1695
1785
|
return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
|
|
1696
1786
|
}
|
|
1697
|
-
const misePath =
|
|
1698
|
-
if (!
|
|
1787
|
+
const misePath = join10(this.context.targetDir, "mise.toml");
|
|
1788
|
+
if (!existsSync8(misePath)) {
|
|
1699
1789
|
return {
|
|
1700
1790
|
success: false,
|
|
1701
1791
|
message: "\u26A0\uFE0F No mise.toml found \u2014 run `pjangler init mise` first, then re-run."
|
|
1702
1792
|
};
|
|
1703
1793
|
}
|
|
1704
|
-
let content =
|
|
1794
|
+
let content = readFileSync4(misePath, "utf8");
|
|
1705
1795
|
if (content.includes(_WireMiseAgentHooks.MARKER)) {
|
|
1706
1796
|
return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
|
|
1707
1797
|
}
|
|
@@ -1777,7 +1867,7 @@ ${leaveBlock}`);
|
|
|
1777
1867
|
""
|
|
1778
1868
|
].join("\n");
|
|
1779
1869
|
content = content.replace(/\n*$/, "\n") + appended;
|
|
1780
|
-
if (!this.context.dryRun)
|
|
1870
|
+
if (!this.context.dryRun) writeFileSync5(misePath, content);
|
|
1781
1871
|
if (wiredHooks) {
|
|
1782
1872
|
return { success: true, message: this.formatMessage("\u2705 Wired mise.toml ([hooks] enter/leave + tasks)") };
|
|
1783
1873
|
}
|
|
@@ -1931,15 +2021,15 @@ function createRecipe(name, context) {
|
|
|
1931
2021
|
}
|
|
1932
2022
|
|
|
1933
2023
|
// src/utils/version.ts
|
|
1934
|
-
import { readFileSync as
|
|
1935
|
-
import { dirname as dirname6, join as
|
|
2024
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
2025
|
+
import { dirname as dirname6, join as join11 } from "node:path";
|
|
1936
2026
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1937
2027
|
var PJANGLER_VERSION = (() => {
|
|
1938
2028
|
try {
|
|
1939
2029
|
let dir = dirname6(fileURLToPath3(import.meta.url));
|
|
1940
2030
|
for (let i = 0; i < 4; i++) {
|
|
1941
2031
|
try {
|
|
1942
|
-
const raw =
|
|
2032
|
+
const raw = readFileSync5(join11(dir, "package.json"), "utf8");
|
|
1943
2033
|
return JSON.parse(raw).version ?? "0.0.0";
|
|
1944
2034
|
} catch {
|
|
1945
2035
|
const parent = dirname6(dir);
|
|
@@ -1953,11 +2043,11 @@ var PJANGLER_VERSION = (() => {
|
|
|
1953
2043
|
})();
|
|
1954
2044
|
|
|
1955
2045
|
// src/parity/index.ts
|
|
1956
|
-
import { existsSync as
|
|
1957
|
-
import { basename as basename3, dirname as dirname7, join as
|
|
2046
|
+
import { existsSync as existsSync9, lstatSync, mkdirSync as mkdirSync6, readFileSync as readFileSync6, readlinkSync, readdirSync as readdirSync2, renameSync as renameSync2, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync6, chmodSync as chmodSync2, copyFileSync } from "node:fs";
|
|
2047
|
+
import { basename as basename3, dirname as dirname7, join as join12, relative, resolve as resolve2 } from "node:path";
|
|
1958
2048
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
1959
2049
|
import { homedir as homedir5 } from "node:os";
|
|
1960
|
-
import { spawnSync as
|
|
2050
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
1961
2051
|
var LINK_AGENTFILES_BLOCK = `# This block will handle the linking of
|
|
1962
2052
|
# agent files to the main AGENTS.md file.
|
|
1963
2053
|
#
|
|
@@ -2028,7 +2118,7 @@ run = "{{config_root}}/.mise/scripts/versioning.sh sync"
|
|
|
2028
2118
|
function resolvePjanglerRoot2() {
|
|
2029
2119
|
let dir = dirname7(fileURLToPath4(import.meta.url));
|
|
2030
2120
|
while (dir !== dirname7(dir)) {
|
|
2031
|
-
if (
|
|
2121
|
+
if (existsSync9(join12(dir, "package.json")) && existsSync9(join12(dir, "templates", "commonproject", "copier.yml"))) {
|
|
2032
2122
|
return dir;
|
|
2033
2123
|
}
|
|
2034
2124
|
dir = dirname7(dir);
|
|
@@ -2039,17 +2129,17 @@ function normalizeNewlines(value) {
|
|
|
2039
2129
|
return value.replace(/\r\n/g, "\n");
|
|
2040
2130
|
}
|
|
2041
2131
|
function readText(path) {
|
|
2042
|
-
return normalizeNewlines(
|
|
2132
|
+
return normalizeNewlines(readFileSync6(path, "utf8"));
|
|
2043
2133
|
}
|
|
2044
2134
|
function safeReadText(path) {
|
|
2045
|
-
return
|
|
2135
|
+
return existsSync9(path) ? readText(path) : null;
|
|
2046
2136
|
}
|
|
2047
2137
|
function ensureParent(path) {
|
|
2048
2138
|
mkdirSync6(dirname7(path), { recursive: true });
|
|
2049
2139
|
}
|
|
2050
2140
|
function writeText(path, content) {
|
|
2051
2141
|
ensureParent(path);
|
|
2052
|
-
|
|
2142
|
+
writeFileSync6(path, content);
|
|
2053
2143
|
}
|
|
2054
2144
|
function tryParseJson(text2) {
|
|
2055
2145
|
if (!text2) return null;
|
|
@@ -2066,7 +2156,7 @@ function titleCaseSlug(slug) {
|
|
|
2066
2156
|
return slug.split(/[-_]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
2067
2157
|
}
|
|
2068
2158
|
function readSymlinkTarget(path) {
|
|
2069
|
-
if (!
|
|
2159
|
+
if (!existsSync9(path)) return null;
|
|
2070
2160
|
try {
|
|
2071
2161
|
return readlinkSync(path);
|
|
2072
2162
|
} catch {
|
|
@@ -2074,7 +2164,7 @@ function readSymlinkTarget(path) {
|
|
|
2074
2164
|
}
|
|
2075
2165
|
}
|
|
2076
2166
|
function ensureSymlink(path, target, dryRun) {
|
|
2077
|
-
if (
|
|
2167
|
+
if (existsSync9(path)) {
|
|
2078
2168
|
const stat = lstatSync(path);
|
|
2079
2169
|
if (stat.isSymbolicLink()) {
|
|
2080
2170
|
const current = readSymlinkTarget(path);
|
|
@@ -2091,11 +2181,11 @@ function ensureSymlink(path, target, dryRun) {
|
|
|
2091
2181
|
return { changed: true };
|
|
2092
2182
|
}
|
|
2093
2183
|
function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
2094
|
-
const agentsPath =
|
|
2095
|
-
if (
|
|
2184
|
+
const agentsPath = join12(repoRoot, "AGENTS.md");
|
|
2185
|
+
if (existsSync9(agentsPath)) return { changedFiles: [], details: [] };
|
|
2096
2186
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2097
|
-
const source =
|
|
2098
|
-
if (!
|
|
2187
|
+
const source = join12(repoRoot, file);
|
|
2188
|
+
if (!existsSync9(source)) continue;
|
|
2099
2189
|
const stat = lstatSync(source);
|
|
2100
2190
|
if (stat.isSymbolicLink()) continue;
|
|
2101
2191
|
if (stat.isFile()) {
|
|
@@ -2104,8 +2194,8 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
|
2104
2194
|
}
|
|
2105
2195
|
return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
|
|
2106
2196
|
}
|
|
2107
|
-
const readmePath =
|
|
2108
|
-
if (
|
|
2197
|
+
const readmePath = join12(repoRoot, "README.md");
|
|
2198
|
+
if (existsSync9(readmePath)) {
|
|
2109
2199
|
const stat = lstatSync(readmePath);
|
|
2110
2200
|
if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
|
|
2111
2201
|
if (!dryRun) copyFileSync(readmePath, agentsPath);
|
|
@@ -2144,12 +2234,12 @@ function yamlGet(text2, keyPath) {
|
|
|
2144
2234
|
return "";
|
|
2145
2235
|
}
|
|
2146
2236
|
function discoverRoles(repoRoot) {
|
|
2147
|
-
const rolesDir =
|
|
2148
|
-
if (!
|
|
2149
|
-
return
|
|
2150
|
-
const roleDir =
|
|
2151
|
-
const roleYamlPath =
|
|
2152
|
-
if (!
|
|
2237
|
+
const rolesDir = join12(repoRoot, "agents", "hermes");
|
|
2238
|
+
if (!existsSync9(rolesDir)) return [];
|
|
2239
|
+
return readdirSync2(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
|
|
2240
|
+
const roleDir = join12(rolesDir, entry.name);
|
|
2241
|
+
const roleYamlPath = join12(roleDir, "role.yaml");
|
|
2242
|
+
if (!existsSync9(roleYamlPath)) return null;
|
|
2153
2243
|
const text2 = readText(roleYamlPath);
|
|
2154
2244
|
const runtimeRepoRaw = yamlGet(text2, "runtime.github_repo");
|
|
2155
2245
|
return {
|
|
@@ -2177,10 +2267,10 @@ function discoverRoles(repoRoot) {
|
|
|
2177
2267
|
}).filter((value) => Boolean(value));
|
|
2178
2268
|
}
|
|
2179
2269
|
function registryPath(homeDir) {
|
|
2180
|
-
return
|
|
2270
|
+
return join12(homeDir, ".hermes", "agents-registry.yaml");
|
|
2181
2271
|
}
|
|
2182
2272
|
function systemctlUser(args) {
|
|
2183
|
-
const result =
|
|
2273
|
+
const result = spawnSync6("systemctl", ["--user", ...args], { encoding: "utf8" });
|
|
2184
2274
|
return {
|
|
2185
2275
|
ok: result.status === 0,
|
|
2186
2276
|
stdout: result.stdout.trim(),
|
|
@@ -2188,8 +2278,8 @@ function systemctlUser(args) {
|
|
|
2188
2278
|
};
|
|
2189
2279
|
}
|
|
2190
2280
|
function templateScript(ctx, name) {
|
|
2191
|
-
const source =
|
|
2192
|
-
return
|
|
2281
|
+
const source = join12(ctx.pjanglerRoot, ".mise", "scripts", name);
|
|
2282
|
+
return existsSync9(source) ? readText(source) : void 0;
|
|
2193
2283
|
}
|
|
2194
2284
|
function templateVersioningScript(ctx) {
|
|
2195
2285
|
return templateScript(ctx, "versioning.sh");
|
|
@@ -2201,8 +2291,8 @@ function resolveAgentHooksLayer2(ctx) {
|
|
|
2201
2291
|
const override = process.env.PJ_AGENT_HOOKS_LAYER;
|
|
2202
2292
|
if (override === "0" || override === "false") return false;
|
|
2203
2293
|
if (override === "1" || override === "true") return true;
|
|
2204
|
-
if (
|
|
2205
|
-
return !
|
|
2294
|
+
if (existsSync9(join12(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
|
|
2295
|
+
return !existsSync9(join12(ctx.homeDir, ".agents", "hooks"));
|
|
2206
2296
|
}
|
|
2207
2297
|
function evaluateMiseConditionals(template, agentHooksLayer) {
|
|
2208
2298
|
const out = [];
|
|
@@ -2232,10 +2322,10 @@ function renderGeneratedProjectMiseToml(ctx, template) {
|
|
|
2232
2322
|
return evaluateMiseConditionals(template, resolveAgentHooksLayer2(ctx)).replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
|
|
2233
2323
|
}
|
|
2234
2324
|
function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
2235
|
-
const targetPath =
|
|
2236
|
-
if (
|
|
2237
|
-
const sourcePath =
|
|
2238
|
-
if (!
|
|
2325
|
+
const targetPath = join12(ctx.repoRoot, "mise.toml");
|
|
2326
|
+
if (existsSync9(targetPath)) return false;
|
|
2327
|
+
const sourcePath = join12(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
|
|
2328
|
+
if (!existsSync9(sourcePath)) return false;
|
|
2239
2329
|
changedFiles.push(targetPath);
|
|
2240
2330
|
if (!ctx.dryRun) {
|
|
2241
2331
|
writeText(targetPath, renderGeneratedProjectMiseToml(ctx, readText(sourcePath)));
|
|
@@ -2243,8 +2333,8 @@ function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
|
2243
2333
|
return true;
|
|
2244
2334
|
}
|
|
2245
2335
|
function templateVersionFilesConf(ctx, repoRoot) {
|
|
2246
|
-
const packageJson =
|
|
2247
|
-
return
|
|
2336
|
+
const packageJson = join12(repoRoot, "package.json");
|
|
2337
|
+
return existsSync9(packageJson) ? "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\njson package.json\ngittag .\n" : "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\ngittag .\n";
|
|
2248
2338
|
}
|
|
2249
2339
|
function replaceOrAppendManagedBlock(text2, startMarker, block, beforePattern) {
|
|
2250
2340
|
if (startMarker.test(text2)) {
|
|
@@ -2268,7 +2358,7 @@ var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/herm
|
|
|
2268
2358
|
function requiredMisePathEntries(ctx) {
|
|
2269
2359
|
const required = [...BASE_MISE_PATH_ENTRIES];
|
|
2270
2360
|
for (const candidate of CONDITIONAL_HERMES_PATHS) {
|
|
2271
|
-
if (
|
|
2361
|
+
if (existsSync9(join12(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
|
|
2272
2362
|
}
|
|
2273
2363
|
return required;
|
|
2274
2364
|
}
|
|
@@ -2417,7 +2507,7 @@ function upsertLinkAgentfilesBlock(text2, ctx) {
|
|
|
2417
2507
|
return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
|
|
2418
2508
|
}
|
|
2419
2509
|
function readProjectJson(ctx) {
|
|
2420
|
-
return tryParseJson(safeReadText(
|
|
2510
|
+
return tryParseJson(safeReadText(join12(ctx.repoRoot, ".project.json")));
|
|
2421
2511
|
}
|
|
2422
2512
|
function boolSetting(value, fallback) {
|
|
2423
2513
|
if (typeof value === "boolean") return value;
|
|
@@ -2492,12 +2582,12 @@ function canonicalProjectJson(ctx) {
|
|
|
2492
2582
|
};
|
|
2493
2583
|
}
|
|
2494
2584
|
function projectJsonFinding(ctx) {
|
|
2495
|
-
const projectPath =
|
|
2496
|
-
const planeJsonPath =
|
|
2585
|
+
const projectPath = join12(ctx.repoRoot, ".project.json");
|
|
2586
|
+
const planeJsonPath = join12(ctx.repoRoot, ".plane.json");
|
|
2497
2587
|
const details = [];
|
|
2498
2588
|
const data = readProjectJson(ctx);
|
|
2499
2589
|
const roles = discoverRoles(ctx.repoRoot);
|
|
2500
|
-
if (!
|
|
2590
|
+
if (!existsSync9(projectPath)) {
|
|
2501
2591
|
return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json missing", details: [], fixable: true };
|
|
2502
2592
|
}
|
|
2503
2593
|
if (!data) {
|
|
@@ -2532,7 +2622,7 @@ function projectJsonFinding(ctx) {
|
|
|
2532
2622
|
for (const key of ["enabled", "grace_hours", "auto_review"]) {
|
|
2533
2623
|
if (!(key in reconcile)) details.push(`automation.reconcile.${key} missing`);
|
|
2534
2624
|
}
|
|
2535
|
-
if (
|
|
2625
|
+
if (existsSync9(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
|
|
2536
2626
|
return {
|
|
2537
2627
|
id: "sot.project-json",
|
|
2538
2628
|
title: "Canonical .project.json",
|
|
@@ -2613,17 +2703,17 @@ exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" HERMES_OAUTH
|
|
|
2613
2703
|
`.replace(/\u0010/g, "$");
|
|
2614
2704
|
}
|
|
2615
2705
|
function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
|
|
2616
|
-
if (!
|
|
2706
|
+
if (!existsSync9(sourceDir)) return;
|
|
2617
2707
|
mkdirSync6(targetDir, { recursive: true });
|
|
2618
|
-
for (const entry of
|
|
2619
|
-
const sourcePath =
|
|
2708
|
+
for (const entry of readdirSync2(sourceDir, { withFileTypes: true })) {
|
|
2709
|
+
const sourcePath = join12(sourceDir, entry.name);
|
|
2620
2710
|
if (skip?.(sourcePath)) continue;
|
|
2621
|
-
const targetPath =
|
|
2711
|
+
const targetPath = join12(targetDir, entry.name);
|
|
2622
2712
|
if (entry.isDirectory()) {
|
|
2623
2713
|
copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
|
|
2624
2714
|
continue;
|
|
2625
2715
|
}
|
|
2626
|
-
if (
|
|
2716
|
+
if (existsSync9(targetPath)) continue;
|
|
2627
2717
|
changedFiles.push(targetPath);
|
|
2628
2718
|
if (!dryRun) {
|
|
2629
2719
|
ensureParent(targetPath);
|
|
@@ -2632,7 +2722,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
|
|
|
2632
2722
|
}
|
|
2633
2723
|
}
|
|
2634
2724
|
function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
|
|
2635
|
-
const gitmodulesPath =
|
|
2725
|
+
const gitmodulesPath = join12(repoRoot, ".gitmodules");
|
|
2636
2726
|
const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
|
|
2637
2727
|
const owner = role.runtimeOwner || "delorenj";
|
|
2638
2728
|
const block = `[submodule "agents/hermes/${role.role}/runtime"]
|
|
@@ -2732,14 +2822,14 @@ var RULES = [
|
|
|
2732
2822
|
id: "mise.config-root",
|
|
2733
2823
|
title: "mise config_root + AGENTS link hooks",
|
|
2734
2824
|
audit: (ctx) => {
|
|
2735
|
-
const misePath =
|
|
2736
|
-
if (!
|
|
2825
|
+
const misePath = join12(ctx.repoRoot, "mise.toml");
|
|
2826
|
+
if (!existsSync9(misePath)) {
|
|
2737
2827
|
return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
|
|
2738
2828
|
}
|
|
2739
2829
|
const text2 = readText(misePath);
|
|
2740
2830
|
const details = [];
|
|
2741
|
-
const linkAgentfilesPath =
|
|
2742
|
-
if (!
|
|
2831
|
+
const linkAgentfilesPath = join12(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
2832
|
+
if (!existsSync9(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
|
|
2743
2833
|
const pathValues = [...(text2.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
2744
2834
|
const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
|
|
2745
2835
|
if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
|
|
@@ -2757,10 +2847,10 @@ var RULES = [
|
|
|
2757
2847
|
};
|
|
2758
2848
|
},
|
|
2759
2849
|
migrate: (ctx, finding) => {
|
|
2760
|
-
const path =
|
|
2850
|
+
const path = join12(ctx.repoRoot, "mise.toml");
|
|
2761
2851
|
const changedFiles = [];
|
|
2762
2852
|
const details = [];
|
|
2763
|
-
if (!
|
|
2853
|
+
if (!existsSync9(path)) {
|
|
2764
2854
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
2765
2855
|
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: [] };
|
|
2766
2856
|
}
|
|
@@ -2776,7 +2866,7 @@ var RULES = [
|
|
|
2776
2866
|
if (!ctx.dryRun) writeText(path, next);
|
|
2777
2867
|
text2 = next;
|
|
2778
2868
|
}
|
|
2779
|
-
const linkAgentfilesPath =
|
|
2869
|
+
const linkAgentfilesPath = join12(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
2780
2870
|
const expectedScript = templateLinkAgentfilesScript(ctx);
|
|
2781
2871
|
if (expectedScript === void 0) {
|
|
2782
2872
|
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: [] };
|
|
@@ -2803,13 +2893,13 @@ var RULES = [
|
|
|
2803
2893
|
title: "managed mise versioning block",
|
|
2804
2894
|
audit: (ctx) => {
|
|
2805
2895
|
const details = [];
|
|
2806
|
-
const misePath =
|
|
2807
|
-
const versioningPath =
|
|
2808
|
-
const manifestPath =
|
|
2896
|
+
const misePath = join12(ctx.repoRoot, "mise.toml");
|
|
2897
|
+
const versioningPath = join12(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
2898
|
+
const manifestPath = join12(ctx.repoRoot, ".mise", "version-files.conf");
|
|
2809
2899
|
const text2 = safeReadText(misePath);
|
|
2810
2900
|
if (!text2?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
|
|
2811
|
-
if (!
|
|
2812
|
-
if (!
|
|
2901
|
+
if (!existsSync9(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
|
|
2902
|
+
if (!existsSync9(manifestPath)) details.push(".mise/version-files.conf missing");
|
|
2813
2903
|
return {
|
|
2814
2904
|
id: "mise.versioning",
|
|
2815
2905
|
title: "managed mise versioning block",
|
|
@@ -2822,8 +2912,8 @@ var RULES = [
|
|
|
2822
2912
|
migrate: (ctx, finding) => {
|
|
2823
2913
|
const changedFiles = [];
|
|
2824
2914
|
const details = [];
|
|
2825
|
-
const misePath =
|
|
2826
|
-
if (!
|
|
2915
|
+
const misePath = join12(ctx.repoRoot, "mise.toml");
|
|
2916
|
+
if (!existsSync9(misePath)) {
|
|
2827
2917
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
2828
2918
|
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: [] };
|
|
2829
2919
|
}
|
|
@@ -2833,12 +2923,21 @@ var RULES = [
|
|
|
2833
2923
|
}
|
|
2834
2924
|
}
|
|
2835
2925
|
const currentMise = readText(misePath);
|
|
2836
|
-
|
|
2926
|
+
let cleanedMise = currentMise;
|
|
2927
|
+
if (!currentMise.includes("# >>> mise-versioning >>>")) {
|
|
2928
|
+
const taskNames = ["version", "version:bump", "version:bump-patch", "version:bump-minor", "version:bump-major", "version:check", "version:sync"];
|
|
2929
|
+
for (const taskName of taskNames) {
|
|
2930
|
+
const escaped = taskName.replace(/:/g, "\\:");
|
|
2931
|
+
const headerPattern = new RegExp(`^\\[tasks\\.(?:"${escaped}"|'${escaped}'|${escaped})\\]$`);
|
|
2932
|
+
cleanedMise = removeTomlSection(cleanedMise, headerPattern);
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
const nextMise = replaceOrAppendManagedBlock(cleanedMise, /# >>> mise-versioning >>>/, VERSIONING_BLOCK, /^\[tasks\.build\]/m);
|
|
2837
2936
|
if (nextMise !== currentMise) {
|
|
2838
2937
|
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
2839
2938
|
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
2840
2939
|
}
|
|
2841
|
-
const versioningPath =
|
|
2940
|
+
const versioningPath = join12(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
2842
2941
|
const expectedScript = templateVersioningScript(ctx);
|
|
2843
2942
|
if (expectedScript === void 0) {
|
|
2844
2943
|
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: [] };
|
|
@@ -2850,7 +2949,7 @@ var RULES = [
|
|
|
2850
2949
|
chmodSync2(versioningPath, 493);
|
|
2851
2950
|
}
|
|
2852
2951
|
}
|
|
2853
|
-
const manifestPath =
|
|
2952
|
+
const manifestPath = join12(ctx.repoRoot, ".mise", "version-files.conf");
|
|
2854
2953
|
const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
|
|
2855
2954
|
if (safeReadText(manifestPath) !== expectedManifest) {
|
|
2856
2955
|
changedFiles.push(manifestPath);
|
|
@@ -2870,9 +2969,9 @@ var RULES = [
|
|
|
2870
2969
|
id: "sot.agent-symlinks",
|
|
2871
2970
|
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
2872
2971
|
audit: (ctx) => {
|
|
2873
|
-
const agentsPath =
|
|
2874
|
-
if (!
|
|
2875
|
-
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) =>
|
|
2972
|
+
const agentsPath = join12(ctx.repoRoot, "AGENTS.md");
|
|
2973
|
+
if (!existsSync9(agentsPath)) {
|
|
2974
|
+
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync9(join12(ctx.repoRoot, file)));
|
|
2876
2975
|
if (fallbackSources.length === 0) {
|
|
2877
2976
|
return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
|
|
2878
2977
|
}
|
|
@@ -2887,7 +2986,7 @@ var RULES = [
|
|
|
2887
2986
|
}
|
|
2888
2987
|
const details = [];
|
|
2889
2988
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2890
|
-
const full =
|
|
2989
|
+
const full = join12(ctx.repoRoot, file);
|
|
2891
2990
|
const target = readSymlinkTarget(full);
|
|
2892
2991
|
if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
|
|
2893
2992
|
}
|
|
@@ -2911,7 +3010,7 @@ var RULES = [
|
|
|
2911
3010
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
|
|
2912
3011
|
}
|
|
2913
3012
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2914
|
-
const full =
|
|
3013
|
+
const full = join12(ctx.repoRoot, file);
|
|
2915
3014
|
const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
|
|
2916
3015
|
if (result.blocked) blockedDetails.push(result.blocked);
|
|
2917
3016
|
if (result.changed) changedFiles.push(full);
|
|
@@ -2933,7 +3032,7 @@ var RULES = [
|
|
|
2933
3032
|
migrate: (ctx, finding) => {
|
|
2934
3033
|
const changedFiles = [];
|
|
2935
3034
|
const details = [];
|
|
2936
|
-
const path =
|
|
3035
|
+
const path = join12(ctx.repoRoot, ".project.json");
|
|
2937
3036
|
const existing = readProjectJson(ctx) ?? {};
|
|
2938
3037
|
const canonical = canonicalProjectJson(ctx);
|
|
2939
3038
|
const merged = { ...existing, ...canonical };
|
|
@@ -2943,10 +3042,10 @@ var RULES = [
|
|
|
2943
3042
|
changedFiles.push(path);
|
|
2944
3043
|
if (!ctx.dryRun) writeText(path, expected);
|
|
2945
3044
|
}
|
|
2946
|
-
const planeJson =
|
|
2947
|
-
if (
|
|
3045
|
+
const planeJson = join12(ctx.repoRoot, ".plane.json");
|
|
3046
|
+
if (existsSync9(planeJson)) {
|
|
2948
3047
|
const backup = `${planeJson}.migrated-backup`;
|
|
2949
|
-
if (
|
|
3048
|
+
if (existsSync9(backup)) {
|
|
2950
3049
|
details.push(`cannot back up .plane.json because ${relative(ctx.repoRoot, backup)} already exists`);
|
|
2951
3050
|
} else {
|
|
2952
3051
|
changedFiles.push(backup);
|
|
@@ -2968,8 +3067,8 @@ var RULES = [
|
|
|
2968
3067
|
title: ".env.op + gitignore secrets contract",
|
|
2969
3068
|
audit: (ctx) => {
|
|
2970
3069
|
const details = [];
|
|
2971
|
-
const envOp = safeReadText(
|
|
2972
|
-
const gitignore = safeReadText(
|
|
3070
|
+
const envOp = safeReadText(join12(ctx.repoRoot, ".env.op"));
|
|
3071
|
+
const gitignore = safeReadText(join12(ctx.repoRoot, ".gitignore"));
|
|
2973
3072
|
if (!envOp) {
|
|
2974
3073
|
details.push(".env.op missing");
|
|
2975
3074
|
} else {
|
|
@@ -2995,12 +3094,12 @@ var RULES = [
|
|
|
2995
3094
|
migrate: (ctx, finding) => {
|
|
2996
3095
|
const changedFiles = [];
|
|
2997
3096
|
const details = [];
|
|
2998
|
-
const envOpPath =
|
|
2999
|
-
if (!
|
|
3097
|
+
const envOpPath = join12(ctx.repoRoot, ".env.op");
|
|
3098
|
+
if (!existsSync9(envOpPath)) {
|
|
3000
3099
|
changedFiles.push(envOpPath);
|
|
3001
|
-
if (!ctx.dryRun) writeText(envOpPath, readText(
|
|
3100
|
+
if (!ctx.dryRun) writeText(envOpPath, readText(join12(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
|
|
3002
3101
|
}
|
|
3003
|
-
const gitignorePath =
|
|
3102
|
+
const gitignorePath = join12(ctx.repoRoot, ".gitignore");
|
|
3004
3103
|
const gitignore = safeReadText(gitignorePath) ?? "";
|
|
3005
3104
|
const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
|
|
3006
3105
|
# NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
|
|
@@ -3027,7 +3126,7 @@ var RULES = [
|
|
|
3027
3126
|
title: ".copier-answers.yml provenance + drift report",
|
|
3028
3127
|
audit: (ctx) => {
|
|
3029
3128
|
const details = [];
|
|
3030
|
-
const path =
|
|
3129
|
+
const path = join12(ctx.repoRoot, ".copier-answers.yml");
|
|
3031
3130
|
const text2 = safeReadText(path);
|
|
3032
3131
|
const project = readProjectJson(ctx);
|
|
3033
3132
|
if (!text2) {
|
|
@@ -3058,12 +3157,12 @@ var RULES = [
|
|
|
3058
3157
|
const changedFiles = [];
|
|
3059
3158
|
const project = canonicalProjectJson(ctx);
|
|
3060
3159
|
const text2 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
|
|
3061
|
-
_src_path: ${
|
|
3160
|
+
_src_path: ${join12(ctx.pjanglerRoot, "templates", "commonproject")}
|
|
3062
3161
|
project_description: ${String(project.project_description)}
|
|
3063
3162
|
project_name: ${String(project.project_name)}
|
|
3064
3163
|
ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
3065
3164
|
`;
|
|
3066
|
-
const path =
|
|
3165
|
+
const path = join12(ctx.repoRoot, ".copier-answers.yml");
|
|
3067
3166
|
if (safeReadText(path) !== text2) {
|
|
3068
3167
|
changedFiles.push(path);
|
|
3069
3168
|
if (!ctx.dryRun) writeText(path, text2);
|
|
@@ -3082,15 +3181,15 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3082
3181
|
id: "bmad.scaffold",
|
|
3083
3182
|
title: "BMAD modules/docs scaffold",
|
|
3084
3183
|
audit: (ctx) => {
|
|
3085
|
-
const sourceRoot =
|
|
3086
|
-
const targetRoot =
|
|
3184
|
+
const sourceRoot = join12(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
|
|
3185
|
+
const targetRoot = join12(ctx.repoRoot, "_bmad");
|
|
3087
3186
|
const sentinels = [
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3187
|
+
join12("core", "config.yaml"),
|
|
3188
|
+
join12("custom", "config.yaml"),
|
|
3189
|
+
join12("custom", "workflows", "ticket-lifecycle", "workflow.yaml"),
|
|
3190
|
+
join12("bmm", "workflows", "workflow-status", "workflow.yaml")
|
|
3092
3191
|
];
|
|
3093
|
-
const missing = sentinels.filter((file) =>
|
|
3192
|
+
const missing = sentinels.filter((file) => existsSync9(join12(sourceRoot, file)) && !existsSync9(join12(targetRoot, file)));
|
|
3094
3193
|
return {
|
|
3095
3194
|
id: "bmad.scaffold",
|
|
3096
3195
|
title: "BMAD modules/docs scaffold",
|
|
@@ -3102,7 +3201,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3102
3201
|
},
|
|
3103
3202
|
migrate: (ctx, finding) => {
|
|
3104
3203
|
const changedFiles = [];
|
|
3105
|
-
copyMissingRecursive(
|
|
3204
|
+
copyMissingRecursive(join12(ctx.pjanglerRoot, "templates", "commonproject", "_bmad"), join12(ctx.repoRoot, "_bmad"), changedFiles, ctx.dryRun);
|
|
3106
3205
|
return {
|
|
3107
3206
|
id: finding.id,
|
|
3108
3207
|
title: finding.title,
|
|
@@ -3124,11 +3223,11 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3124
3223
|
}
|
|
3125
3224
|
const details = [];
|
|
3126
3225
|
for (const rel of ["role.yaml", "SOUL.md", "hermes", ".gitignore", ".scripts/70-systemd.sh", ".scripts/heartbeat.sh", ".scripts/checkpoint.sh", ".runtime-scaffold/README.md", "runtime/memories/MEMORY.md", "runtime/bloodbank-consumer.py"]) {
|
|
3127
|
-
if (!
|
|
3226
|
+
if (!existsSync9(join12(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join12(role.roleDir, rel))}`);
|
|
3128
3227
|
}
|
|
3129
|
-
const gitmodules = safeReadText(
|
|
3228
|
+
const gitmodules = safeReadText(join12(ctx.repoRoot, ".gitmodules")) ?? "";
|
|
3130
3229
|
if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
|
|
3131
|
-
if (!profileMetaInheritsDefault(
|
|
3230
|
+
if (!profileMetaInheritsDefault(join12(role.roleDir, "runtime", "profile.yaml"))) {
|
|
3132
3231
|
details.push("runtime/profile.yaml missing inherited default config metadata");
|
|
3133
3232
|
}
|
|
3134
3233
|
const registry = safeReadText(registryPath(ctx.homeDir));
|
|
@@ -3149,21 +3248,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3149
3248
|
if (!role) {
|
|
3150
3249
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
|
|
3151
3250
|
}
|
|
3152
|
-
const templateRoleDir =
|
|
3153
|
-
writeIfDifferent(
|
|
3154
|
-
writeIfDifferent(
|
|
3155
|
-
writeIfDifferent(
|
|
3156
|
-
copyMissingRecursive(
|
|
3157
|
-
copyMissingRecursive(
|
|
3158
|
-
copyMissingRecursive(
|
|
3159
|
-
const promptSource =
|
|
3160
|
-
const promptTarget =
|
|
3161
|
-
if (
|
|
3251
|
+
const templateRoleDir = join12(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
|
|
3252
|
+
writeIfDifferent(join12(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
|
|
3253
|
+
writeIfDifferent(join12(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
|
|
3254
|
+
writeIfDifferent(join12(role.roleDir, ".gitignore"), readText(join12(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
|
|
3255
|
+
copyMissingRecursive(join12(templateRoleDir, ".runtime-scaffold"), join12(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
|
|
3256
|
+
copyMissingRecursive(join12(templateRoleDir, ".runtime-scaffold"), join12(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
|
|
3257
|
+
copyMissingRecursive(join12(templateRoleDir, ".scripts"), join12(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
|
|
3258
|
+
const promptSource = join12(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
|
|
3259
|
+
const promptTarget = join12(role.roleDir, ".scripts", "sentinel.prompt.md");
|
|
3260
|
+
if (existsSync9(promptSource) && !existsSync9(promptTarget)) {
|
|
3162
3261
|
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);
|
|
3163
3262
|
writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
|
|
3164
3263
|
}
|
|
3165
3264
|
upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
|
|
3166
|
-
const profileMetaUpdated = upsertInheritedProfileMeta(
|
|
3265
|
+
const profileMetaUpdated = upsertInheritedProfileMeta(join12(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
|
|
3167
3266
|
if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
|
|
3168
3267
|
const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
|
|
3169
3268
|
if (registryUpdated) details.push(`updated ${registryUpdated}`);
|
|
@@ -3177,6 +3276,103 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3177
3276
|
};
|
|
3178
3277
|
}
|
|
3179
3278
|
},
|
|
3279
|
+
{
|
|
3280
|
+
id: "hermes.untracked-runtimes",
|
|
3281
|
+
title: "Hermes agent runtimes untracked + gitignored",
|
|
3282
|
+
audit: (ctx) => {
|
|
3283
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
3284
|
+
if (roles.length === 0) {
|
|
3285
|
+
return {
|
|
3286
|
+
id: "hermes.untracked-runtimes",
|
|
3287
|
+
title: "Hermes agent runtimes untracked + gitignored",
|
|
3288
|
+
status: "skip",
|
|
3289
|
+
summary: "No Hermes roles present",
|
|
3290
|
+
details: [],
|
|
3291
|
+
fixable: false
|
|
3292
|
+
};
|
|
3293
|
+
}
|
|
3294
|
+
const details = [];
|
|
3295
|
+
for (const role of roles) {
|
|
3296
|
+
const roleRelDir = relative(ctx.repoRoot, role.roleDir);
|
|
3297
|
+
const runtimeRelPath = join12(roleRelDir, "runtime");
|
|
3298
|
+
const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
|
|
3299
|
+
cwd: ctx.repoRoot,
|
|
3300
|
+
encoding: "utf8"
|
|
3301
|
+
});
|
|
3302
|
+
if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
|
|
3303
|
+
details.push(`submodule runtime is tracked in Git index at ${runtimeRelPath}`);
|
|
3304
|
+
}
|
|
3305
|
+
const gitignorePath = join12(role.roleDir, ".gitignore");
|
|
3306
|
+
if (existsSync9(gitignorePath)) {
|
|
3307
|
+
const content = safeReadText(gitignorePath) ?? "";
|
|
3308
|
+
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
3309
|
+
if (!lines.includes("runtime/") && !lines.includes("runtime")) {
|
|
3310
|
+
details.push(`.gitignore missing runtime/ ignore entry in ${relative(ctx.repoRoot, gitignorePath)}`);
|
|
3311
|
+
}
|
|
3312
|
+
} else {
|
|
3313
|
+
details.push(`.gitignore is missing in ${relative(ctx.repoRoot, gitignorePath)}`);
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
return {
|
|
3317
|
+
id: "hermes.untracked-runtimes",
|
|
3318
|
+
title: "Hermes agent runtimes untracked + gitignored",
|
|
3319
|
+
status: details.length === 0 ? "pass" : "fail",
|
|
3320
|
+
summary: details.length === 0 ? "All Hermes agent runtimes are untracked and gitignored" : `${details.length} issue(s) with untracked/ignored runtimes detected`,
|
|
3321
|
+
details,
|
|
3322
|
+
fixable: true
|
|
3323
|
+
};
|
|
3324
|
+
},
|
|
3325
|
+
migrate: (ctx, finding) => {
|
|
3326
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
3327
|
+
const changedFiles = [];
|
|
3328
|
+
const details = [];
|
|
3329
|
+
for (const role of roles) {
|
|
3330
|
+
const roleRelDir = relative(ctx.repoRoot, role.roleDir);
|
|
3331
|
+
const runtimeRelPath = join12(roleRelDir, "runtime");
|
|
3332
|
+
const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
|
|
3333
|
+
cwd: ctx.repoRoot,
|
|
3334
|
+
encoding: "utf8"
|
|
3335
|
+
});
|
|
3336
|
+
if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
|
|
3337
|
+
details.push(`untrack ${runtimeRelPath}`);
|
|
3338
|
+
changedFiles.push(runtimeRelPath);
|
|
3339
|
+
if (!ctx.dryRun) {
|
|
3340
|
+
spawnSync6("git", ["rm", "--cached", "-r", runtimeRelPath], {
|
|
3341
|
+
cwd: ctx.repoRoot,
|
|
3342
|
+
encoding: "utf8"
|
|
3343
|
+
});
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
const gitignorePath = join12(role.roleDir, ".gitignore");
|
|
3347
|
+
let content = "";
|
|
3348
|
+
let isIgnored = false;
|
|
3349
|
+
if (existsSync9(gitignorePath)) {
|
|
3350
|
+
content = safeReadText(gitignorePath) ?? "";
|
|
3351
|
+
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
3352
|
+
isIgnored = lines.includes("runtime/") || lines.includes("runtime");
|
|
3353
|
+
}
|
|
3354
|
+
if (!isIgnored) {
|
|
3355
|
+
details.push(`ignore runtime/ in ${relative(ctx.repoRoot, gitignorePath)}`);
|
|
3356
|
+
changedFiles.push(gitignorePath);
|
|
3357
|
+
if (!ctx.dryRun) {
|
|
3358
|
+
if (content && !content.endsWith("\n")) {
|
|
3359
|
+
content += "\n";
|
|
3360
|
+
}
|
|
3361
|
+
content += "runtime/\n";
|
|
3362
|
+
writeText(gitignorePath, content);
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
return {
|
|
3367
|
+
id: finding.id,
|
|
3368
|
+
title: finding.title,
|
|
3369
|
+
status: changedFiles.length ? "applied" : "noop",
|
|
3370
|
+
summary: changedFiles.length ? "Hermes agent runtimes made untracked and ignored" : "No changes required",
|
|
3371
|
+
changedFiles,
|
|
3372
|
+
details
|
|
3373
|
+
};
|
|
3374
|
+
}
|
|
3375
|
+
},
|
|
3180
3376
|
{
|
|
3181
3377
|
id: "systemd.sentinel",
|
|
3182
3378
|
title: "Hermes systemd/sentinel units enabled + active",
|
|
@@ -3217,9 +3413,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3217
3413
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
|
|
3218
3414
|
}
|
|
3219
3415
|
for (const role of roles) {
|
|
3220
|
-
const sysDir =
|
|
3416
|
+
const sysDir = join12(ctx.homeDir, ".config", "systemd", "user");
|
|
3221
3417
|
const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`];
|
|
3222
|
-
const allUnitsPresent = units.every((unit) =>
|
|
3418
|
+
const allUnitsPresent = units.every((unit) => existsSync9(join12(sysDir, unit)));
|
|
3223
3419
|
if (allUnitsPresent) {
|
|
3224
3420
|
if (ctx.dryRun) {
|
|
3225
3421
|
details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
|
|
@@ -3231,12 +3427,12 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3231
3427
|
}
|
|
3232
3428
|
continue;
|
|
3233
3429
|
}
|
|
3234
|
-
for (const script of [
|
|
3235
|
-
if (!script || !
|
|
3430
|
+
for (const script of [join12(role.roleDir, ".scripts", "70-systemd.sh")]) {
|
|
3431
|
+
if (!script || !existsSync9(script)) continue;
|
|
3236
3432
|
if (ctx.dryRun) {
|
|
3237
3433
|
details.push(`would run: bash ${script}`);
|
|
3238
3434
|
} else {
|
|
3239
|
-
const result =
|
|
3435
|
+
const result = spawnSync6("bash", [script], { cwd: role.roleDir, encoding: "utf8" });
|
|
3240
3436
|
if (result.status !== 0) details.push(`script failed: ${script}: ${result.stderr.trim() || result.stdout.trim()}`);
|
|
3241
3437
|
}
|
|
3242
3438
|
}
|
|
@@ -3356,7 +3552,7 @@ var server = new McpServer({
|
|
|
3356
3552
|
var TICKET_PROVIDER_SCHEMA = z.enum(["plane", "trello"]);
|
|
3357
3553
|
function resolveTargetDir(targetDir) {
|
|
3358
3554
|
const dir = resolve3(targetDir ?? process.cwd());
|
|
3359
|
-
if (!
|
|
3555
|
+
if (!existsSync10(dir)) {
|
|
3360
3556
|
throw new Error(`Target directory does not exist: ${dir}`);
|
|
3361
3557
|
}
|
|
3362
3558
|
if (!statSync2(dir).isDirectory()) {
|
|
@@ -3367,7 +3563,7 @@ function resolveTargetDir(targetDir) {
|
|
|
3367
3563
|
function resolvePjanglerRoot3() {
|
|
3368
3564
|
let dir = dirname8(fileURLToPath5(import.meta.url));
|
|
3369
3565
|
while (dir !== dirname8(dir)) {
|
|
3370
|
-
if (
|
|
3566
|
+
if (existsSync10(join13(dir, "package.json")) && existsSync10(join13(dir, "templates", "commonproject", "copier.yml"))) {
|
|
3371
3567
|
return dir;
|
|
3372
3568
|
}
|
|
3373
3569
|
dir = dirname8(dir);
|
|
@@ -3569,8 +3765,8 @@ server.registerTool(
|
|
|
3569
3765
|
const pjanglerRoot = resolvePjanglerRoot3();
|
|
3570
3766
|
const projectSlug = input.projectSlug ?? slugify(input.projectName);
|
|
3571
3767
|
const parentDir = resolve3(input.parentDir ?? process.cwd());
|
|
3572
|
-
if (!
|
|
3573
|
-
const targetDir = resolve3(input.targetDir ??
|
|
3768
|
+
if (!existsSync10(parentDir) || !statSync2(parentDir).isDirectory()) throw new Error(`Parent directory does not exist: ${parentDir}`);
|
|
3769
|
+
const targetDir = resolve3(input.targetDir ?? join13(parentDir, projectSlug));
|
|
3574
3770
|
const overwrite = input.overwrite ?? input.force ?? false;
|
|
3575
3771
|
const dryRun = input.dryRun ?? true;
|
|
3576
3772
|
const local = input.local ?? true;
|
|
@@ -3580,7 +3776,7 @@ server.registerTool(
|
|
|
3580
3776
|
if (!skipPlane && ticketProvider === "plane" && !boardId) {
|
|
3581
3777
|
throw new Error("boardId or planeProjectId is required when skipPlane=false for Plane; keep skipPlane=true for safe local bootstrap");
|
|
3582
3778
|
}
|
|
3583
|
-
if (!dryRun &&
|
|
3779
|
+
if (!dryRun && existsSync10(targetDir) && !overwrite) throw new Error(`Target already exists: ${targetDir} (set force/overwrite=true to re-render)`);
|
|
3584
3780
|
const plan = planProjectInit({
|
|
3585
3781
|
name: input.projectName,
|
|
3586
3782
|
description: input.projectDescription,
|