@delorenj/pjangler 1.2.9 → 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/index.js +498 -204
- package/dist/mcp-server.js +488 -194
- package/package.json +1 -1
- package/templates/commonproject/copier.yml +4 -1
- package/templates/commonproject/template/.project.json.jinja +9 -12
- package/templates/commonproject/template/mise.toml.jinja +1 -1
- package/templates/hermes-agent/README.md +1 -1
- package/templates/hermes-agent/config.example.toml +66 -0
- package/templates/hermes-agent/docs/architecture.md +5 -0
- package/templates/hermes-agent/docs/bloodbank-gateway.md +57 -0
- package/templates/hermes-agent/docs/fleet-control-plane/README.md +1 -1
- package/templates/hermes-agent/docs/fleet-control-plane/n8n-service-hub.md +60 -0
- package/templates/hermes-agent/docs/operations.md +1 -1
- package/templates/hermes-agent/docs/sentinel/README.md +2 -2
- package/templates/hermes-agent/docs/sentinel/development.md +3 -3
- package/templates/hermes-agent/docs/sentinel/providers.md +6 -5
- package/templates/hermes-agent/install-local.sh +10 -4
- package/templates/hermes-agent/runtime-scaffold/bloodbank-consumer.py +5 -3
- package/templates/hermes-agent/scripts/fleet-sync.sh +64 -1
- package/templates/hermes-agent/template/.gitignore.jinja +2 -0
- package/templates/hermes-agent/template/.scripts/01-config.sh +1 -0
- package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +9 -0
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +22 -3
- package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +22 -0
- package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +18 -33
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +8 -1
- package/templates/hermes-agent/template/.scripts/_lib.sh +61 -3
- package/templates/hermes-agent/template/.scripts/config.example.toml +5 -0
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +66 -22
- package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +3 -3
- package/templates/hermes-agent/template/.scripts/providers/plane.sh +29 -9
- package/templates/hermes-agent/template/role.yaml.jinja +2 -27
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";
|
|
@@ -630,6 +630,8 @@ function renderHostConfig() {
|
|
|
630
630
|
const hermesRepo = join3(home, "code", "hermes-agent");
|
|
631
631
|
const scaffoldDir = join3(home, "code", "hermes-agent-template", "runtime-scaffold");
|
|
632
632
|
const skillsDir = join3(home, ".agents", "skills");
|
|
633
|
+
const pmExternalSkillGlobalDir = join3(home, "code", "skillex", "skill-sets", "global", ".system");
|
|
634
|
+
const pmExternalSkillBmadDir = join3(home, "code", "skillex", "packs", "bmad", "6.10.2");
|
|
633
635
|
return `# hermes-agent-template \u2014 host configuration
|
|
634
636
|
# Bootstrapped by \`pjangler config bootstrap\` for $HOME=${home} (platform=${platform()}).
|
|
635
637
|
#
|
|
@@ -645,9 +647,14 @@ home = "~/.hermes"
|
|
|
645
647
|
hermes_bin = "${hermesBin}"
|
|
646
648
|
hermes_repo = "${hermesRepo}"
|
|
647
649
|
runtime_scaffold_dir = "${scaffoldDir}"
|
|
650
|
+
# Shared fleet source-of-truth env file + fleet registry. ~ is expanded.
|
|
648
651
|
fleet_env = "~/.hermes/fleet.env"
|
|
649
652
|
registry_file = "~/.hermes/agents-registry.yaml"
|
|
650
653
|
canonical_skills_dir = "${skillsDir}"
|
|
654
|
+
pm_external_skill_dirs = [
|
|
655
|
+
"${pmExternalSkillGlobalDir}",
|
|
656
|
+
"${pmExternalSkillBmadDir}",
|
|
657
|
+
]
|
|
651
658
|
symlinked_runtime_skills = []
|
|
652
659
|
|
|
653
660
|
[github]
|
|
@@ -895,10 +902,100 @@ var RunCopierTemplate = class extends Command {
|
|
|
895
902
|
}
|
|
896
903
|
};
|
|
897
904
|
|
|
898
|
-
// 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";
|
|
899
908
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
900
|
-
|
|
901
|
-
|
|
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";
|
|
902
999
|
import * as p3 from "@clack/prompts";
|
|
903
1000
|
var WireTelegram = class extends Command {
|
|
904
1001
|
async invoke() {
|
|
@@ -920,7 +1017,7 @@ var WireTelegram = class extends Command {
|
|
|
920
1017
|
let token = process.env.TELEGRAM_BOT_TOKEN;
|
|
921
1018
|
let source = token ? "env" : null;
|
|
922
1019
|
if (!token) {
|
|
923
|
-
const tryOp =
|
|
1020
|
+
const tryOp = spawnSync3("op", ["read", vaultRef], { encoding: "utf8" });
|
|
924
1021
|
if (tryOp.status === 0) {
|
|
925
1022
|
token = tryOp.stdout.trim();
|
|
926
1023
|
source = "op";
|
|
@@ -959,7 +1056,7 @@ var WireTelegram = class extends Command {
|
|
|
959
1056
|
initialValue: true
|
|
960
1057
|
});
|
|
961
1058
|
if (!p3.isCancel(persist) && persist) {
|
|
962
|
-
const create =
|
|
1059
|
+
const create = spawnSync3(
|
|
963
1060
|
"op",
|
|
964
1061
|
[
|
|
965
1062
|
"item",
|
|
@@ -986,18 +1083,18 @@ var WireTelegram = class extends Command {
|
|
|
986
1083
|
if (p3.isCancel(allowedAnswer)) {
|
|
987
1084
|
return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
|
|
988
1085
|
}
|
|
989
|
-
const script =
|
|
990
|
-
if (!
|
|
1086
|
+
const script = join7(roleDir, ".scripts", "30-telegram.sh");
|
|
1087
|
+
if (!existsSync5(script)) {
|
|
991
1088
|
return {
|
|
992
1089
|
success: false,
|
|
993
1090
|
message: `\u2717 ${script} not found. Did copier finish? Re-run with --skip-runtime-repo=0 if you skipped it.`
|
|
994
1091
|
};
|
|
995
1092
|
}
|
|
996
|
-
const marker =
|
|
997
|
-
if (
|
|
1093
|
+
const marker = join7(roleDir, ".scripts", ".done-30-telegram");
|
|
1094
|
+
if (existsSync5(marker)) unlinkSync(marker);
|
|
998
1095
|
const spinner4 = p3.spinner();
|
|
999
1096
|
spinner4.start("Verifying token + wiring profile");
|
|
1000
|
-
const result =
|
|
1097
|
+
const result = spawnSync3("bash", [script], {
|
|
1001
1098
|
stdio: "inherit",
|
|
1002
1099
|
env: {
|
|
1003
1100
|
...process.env,
|
|
@@ -1020,9 +1117,9 @@ function cap(s) {
|
|
|
1020
1117
|
}
|
|
1021
1118
|
|
|
1022
1119
|
// src/commands/hermes/WireEmail.ts
|
|
1023
|
-
import { spawnSync as
|
|
1024
|
-
import { join as
|
|
1025
|
-
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";
|
|
1026
1123
|
import * as p4 from "@clack/prompts";
|
|
1027
1124
|
var WireEmail = class extends Command {
|
|
1028
1125
|
async invoke() {
|
|
@@ -1037,13 +1134,13 @@ var WireEmail = class extends Command {
|
|
|
1037
1134
|
if (!targetRepo || !role || !roleDir) {
|
|
1038
1135
|
return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
|
|
1039
1136
|
}
|
|
1040
|
-
const script =
|
|
1041
|
-
if (!
|
|
1137
|
+
const script = join8(roleDir, ".scripts", "50-email.sh");
|
|
1138
|
+
if (!existsSync6(script)) {
|
|
1042
1139
|
return { success: false, message: `\u2717 ${script} not found` };
|
|
1043
1140
|
}
|
|
1044
1141
|
let token = process.env.CF_EMAIL_ROUTING_TOKEN;
|
|
1045
1142
|
if (!token) {
|
|
1046
|
-
const tryOp =
|
|
1143
|
+
const tryOp = spawnSync4(
|
|
1047
1144
|
"op",
|
|
1048
1145
|
["read", "op://DeLoSecrets/Cloudflare-EmailRouting/token"],
|
|
1049
1146
|
{ encoding: "utf8" }
|
|
@@ -1083,7 +1180,7 @@ var WireEmail = class extends Command {
|
|
|
1083
1180
|
initialValue: true
|
|
1084
1181
|
});
|
|
1085
1182
|
if (!p4.isCancel(persist) && persist) {
|
|
1086
|
-
const create =
|
|
1183
|
+
const create = spawnSync4(
|
|
1087
1184
|
"op",
|
|
1088
1185
|
[
|
|
1089
1186
|
"item",
|
|
@@ -1100,11 +1197,11 @@ var WireEmail = class extends Command {
|
|
|
1100
1197
|
}
|
|
1101
1198
|
}
|
|
1102
1199
|
}
|
|
1103
|
-
const marker =
|
|
1104
|
-
if (
|
|
1200
|
+
const marker = join8(roleDir, ".scripts", ".done-50-email");
|
|
1201
|
+
if (existsSync6(marker)) unlinkSync2(marker);
|
|
1105
1202
|
const spinner4 = p4.spinner();
|
|
1106
1203
|
spinner4.start("Creating Cloudflare Email Routing rule");
|
|
1107
|
-
const result =
|
|
1204
|
+
const result = spawnSync4("bash", [script], {
|
|
1108
1205
|
stdio: "inherit",
|
|
1109
1206
|
env: { ...process.env, SKIP_EMAIL: "0", CF_EMAIL_ROUTING_TOKEN: token },
|
|
1110
1207
|
cwd: roleDir
|
|
@@ -1164,7 +1261,7 @@ var PrintHermesSummary = class extends Command {
|
|
|
1164
1261
|
var HermesAgentRecipe = class extends Recipe {
|
|
1165
1262
|
constructor(context) {
|
|
1166
1263
|
super(context);
|
|
1167
|
-
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);
|
|
1168
1265
|
}
|
|
1169
1266
|
// Override execute() to suppress the base class's per-command logging since
|
|
1170
1267
|
// our commands already render their own UI via @clack/prompts.
|
|
@@ -1188,33 +1285,33 @@ var HermesAgentRecipe = class extends Recipe {
|
|
|
1188
1285
|
|
|
1189
1286
|
// src/commands/AgentHooksCommands.ts
|
|
1190
1287
|
import { homedir as homedir4 } from "node:os";
|
|
1191
|
-
import { join as
|
|
1192
|
-
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";
|
|
1193
1290
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1194
1291
|
|
|
1195
1292
|
// src/project/index.ts
|
|
1196
|
-
import { spawnSync as
|
|
1197
|
-
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";
|
|
1198
1295
|
import { homedir as homedir3 } from "node:os";
|
|
1199
|
-
import { basename as basename2, dirname as dirname4, join as
|
|
1296
|
+
import { basename as basename2, delimiter, dirname as dirname4, join as join9, resolve } from "node:path";
|
|
1200
1297
|
import YAML from "yaml";
|
|
1201
1298
|
var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
|
|
1299
|
+
var PROJECT_SOURCE_SKILL_ROOTS_ENV = "PJ_SOURCE_SKILL_ROOTS";
|
|
1202
1300
|
var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
|
|
1203
|
-
var
|
|
1301
|
+
var DEFAULT_SOURCE_SKILL_ROOTS = [
|
|
1204
1302
|
"/home/delorenj/code/skillex/all-skills",
|
|
1205
|
-
"
|
|
1206
|
-
"
|
|
1207
|
-
join8(homedir3(), ".codex", "skills")
|
|
1303
|
+
join9(homedir3(), ".agents", "skills"),
|
|
1304
|
+
join9(homedir3(), ".codex", "skills")
|
|
1208
1305
|
];
|
|
1209
1306
|
function projectRegistryPath(env2 = process.env) {
|
|
1210
|
-
return expandHome(env2[PROJECT_REGISTRY_ENV] ||
|
|
1307
|
+
return expandHome(env2[PROJECT_REGISTRY_ENV] || join9(homedir3(), ".config", "pjangler", "projects.yaml"));
|
|
1211
1308
|
}
|
|
1212
1309
|
function emptyProjectRegistry() {
|
|
1213
1310
|
return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
|
|
1214
1311
|
}
|
|
1215
1312
|
function loadProjectRegistry(path = projectRegistryPath()) {
|
|
1216
|
-
if (!
|
|
1217
|
-
const raw = YAML.parse(
|
|
1313
|
+
if (!existsSync7(path)) return emptyProjectRegistry();
|
|
1314
|
+
const raw = YAML.parse(readFileSync3(path, "utf8"));
|
|
1218
1315
|
if (raw == null) return emptyProjectRegistry();
|
|
1219
1316
|
if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
|
|
1220
1317
|
const registry = raw;
|
|
@@ -1229,7 +1326,7 @@ function saveProjectRegistry(registry, path = projectRegistryPath()) {
|
|
|
1229
1326
|
validateProjectRegistry(registry);
|
|
1230
1327
|
mkdirSync4(dirname4(path), { recursive: true });
|
|
1231
1328
|
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
1232
|
-
|
|
1329
|
+
writeFileSync4(temp, YAML.stringify(registry, { lineWidth: 0 }), "utf8");
|
|
1233
1330
|
renameSync(temp, path);
|
|
1234
1331
|
}
|
|
1235
1332
|
function validateProjectRegistry(registry) {
|
|
@@ -1274,8 +1371,7 @@ function buildTicketProviderBlock(input) {
|
|
|
1274
1371
|
workspace: input.workspace ?? "",
|
|
1275
1372
|
identifier: input.identifier,
|
|
1276
1373
|
board_id: boardId,
|
|
1277
|
-
|
|
1278
|
-
state: "planned"
|
|
1374
|
+
state: boardId ? "linked" : "planned"
|
|
1279
1375
|
};
|
|
1280
1376
|
}
|
|
1281
1377
|
const workspace = input.workspace ?? "33god";
|
|
@@ -1284,8 +1380,16 @@ function buildTicketProviderBlock(input) {
|
|
|
1284
1380
|
workspace,
|
|
1285
1381
|
identifier: input.identifier,
|
|
1286
1382
|
board_id: boardId,
|
|
1287
|
-
|
|
1288
|
-
|
|
1383
|
+
state: boardId ? "linked" : "planned"
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
function defaultProjectAutomation() {
|
|
1387
|
+
return {
|
|
1388
|
+
reconcile: {
|
|
1389
|
+
enabled: false,
|
|
1390
|
+
grace_hours: 0,
|
|
1391
|
+
auto_review: true
|
|
1392
|
+
}
|
|
1289
1393
|
};
|
|
1290
1394
|
}
|
|
1291
1395
|
function slugifyProjectName(value) {
|
|
@@ -1304,7 +1408,7 @@ function resolveAgentHooksLayer(input, env2 = process.env) {
|
|
|
1304
1408
|
const override = env2.PJ_AGENT_HOOKS_LAYER;
|
|
1305
1409
|
if (override === "0" || override === "false") return false;
|
|
1306
1410
|
if (override === "1" || override === "true") return true;
|
|
1307
|
-
return !
|
|
1411
|
+
return !existsSync7(join9(homedir3(), ".agents", "hooks"));
|
|
1308
1412
|
}
|
|
1309
1413
|
function jsonStable(value) {
|
|
1310
1414
|
return JSON.stringify(value);
|
|
@@ -1319,18 +1423,31 @@ function defaultProjectTargetDir(name, cwd = process.cwd()) {
|
|
|
1319
1423
|
const compactName = name.replace(/[^A-Za-z0-9._-]/g, "") || slugifyProjectName(name);
|
|
1320
1424
|
return resolve(dirname4(resolve(cwd)), compactName);
|
|
1321
1425
|
}
|
|
1322
|
-
function
|
|
1426
|
+
function sourceSkillRoots(env2 = process.env) {
|
|
1427
|
+
const configuredRoots = (env2[PROJECT_SOURCE_SKILL_ROOTS_ENV] || "").split(delimiter).map((root) => root.trim()).filter(Boolean);
|
|
1428
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1429
|
+
const roots = [];
|
|
1430
|
+
for (const root of [...DEFAULT_SOURCE_SKILL_ROOTS, ...configuredRoots]) {
|
|
1431
|
+
const normalized = resolve(expandHome(root));
|
|
1432
|
+
if (seen.has(normalized)) continue;
|
|
1433
|
+
seen.add(normalized);
|
|
1434
|
+
roots.push(normalized);
|
|
1435
|
+
}
|
|
1436
|
+
return roots;
|
|
1437
|
+
}
|
|
1438
|
+
function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
|
|
1323
1439
|
if (!sourceSkill) return void 0;
|
|
1324
1440
|
const expanded = expandHome(sourceSkill);
|
|
1325
1441
|
const direct = resolve(expanded);
|
|
1326
|
-
if (
|
|
1442
|
+
if (existsSync7(direct)) return direct;
|
|
1327
1443
|
const name = basename2(sourceSkill);
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1444
|
+
const roots = sourceSkillRoots(env2);
|
|
1445
|
+
for (const root of roots) {
|
|
1446
|
+
const candidate = join9(root, name);
|
|
1447
|
+
if (existsSync7(candidate)) return candidate;
|
|
1331
1448
|
}
|
|
1332
|
-
const
|
|
1333
|
-
const hint =
|
|
1449
|
+
const searched = roots.length ? ` Searched roots: ${roots.join(", ")}.` : "";
|
|
1450
|
+
const hint = `${searched} Add project-specific roots with ${PROJECT_SOURCE_SKILL_ROOTS_ENV}.`;
|
|
1334
1451
|
throw new Error(`Source skill not found: ${sourceSkill}.${hint}`);
|
|
1335
1452
|
}
|
|
1336
1453
|
function planProjectInit(input) {
|
|
@@ -1370,10 +1487,10 @@ function planProjectInit(input) {
|
|
|
1370
1487
|
type: input.ticketProvider ?? "plane",
|
|
1371
1488
|
identifier,
|
|
1372
1489
|
boardId: input.boardId ?? input.planeProjectId,
|
|
1373
|
-
boardUrl: input.boardUrl,
|
|
1374
1490
|
workspace: input.boardWorkspace ?? input.planeWorkspace
|
|
1375
1491
|
}),
|
|
1376
1492
|
agents,
|
|
1493
|
+
automation: existing?.automation ?? defaultProjectAutomation(),
|
|
1377
1494
|
created_at: existing?.created_at ?? now,
|
|
1378
1495
|
updated_at: now
|
|
1379
1496
|
};
|
|
@@ -1401,7 +1518,6 @@ function planProjectInit(input) {
|
|
|
1401
1518
|
planeProjectId: project.ticket_provider.board_id ?? "",
|
|
1402
1519
|
ticketWorkspace: project.ticket_provider.workspace ?? "",
|
|
1403
1520
|
boardId: project.ticket_provider.board_id ?? "",
|
|
1404
|
-
boardUrl: project.ticket_provider.board_url ?? "",
|
|
1405
1521
|
projectIdentifier: identifier,
|
|
1406
1522
|
primaryLanguage: project.template.commonproject.primary_language,
|
|
1407
1523
|
agentHooksLayer: resolveAgentHooksLayer(input.agentHooksLayer),
|
|
@@ -1409,7 +1525,7 @@ function planProjectInit(input) {
|
|
|
1409
1525
|
}));
|
|
1410
1526
|
}
|
|
1411
1527
|
actions.push(
|
|
1412
|
-
{ kind: "project.write-manifest", path:
|
|
1528
|
+
{ kind: "project.write-manifest", path: join9(targetDir, ".project.json"), manifest },
|
|
1413
1529
|
{
|
|
1414
1530
|
kind: "ticket-provider.create-or-link",
|
|
1415
1531
|
enabled: live,
|
|
@@ -1450,7 +1566,7 @@ function executeProjectInitPlan(plan) {
|
|
|
1450
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"
|
|
1451
1567
|
);
|
|
1452
1568
|
mkdirSync4(dirname4(action.targetDir), { recursive: true });
|
|
1453
|
-
const result =
|
|
1569
|
+
const result = spawnSync5(action.command[0], action.command.slice(1), { encoding: "utf8", cwd: action.cwd });
|
|
1454
1570
|
if (result.stdout?.trim()) logs.push(result.stdout.trim());
|
|
1455
1571
|
if (result.stderr?.trim()) logs.push(result.stderr.trim());
|
|
1456
1572
|
if (result.error) {
|
|
@@ -1462,7 +1578,7 @@ function executeProjectInitPlan(plan) {
|
|
|
1462
1578
|
}
|
|
1463
1579
|
if (result.status !== 0) {
|
|
1464
1580
|
errors.push(`copier exited with status ${result.status ?? "unknown"}`);
|
|
1465
|
-
if (
|
|
1581
|
+
if (existsSync7(action.targetDir)) changedFiles.push(action.targetDir);
|
|
1466
1582
|
break;
|
|
1467
1583
|
}
|
|
1468
1584
|
changedFiles.push(action.targetDir);
|
|
@@ -1470,9 +1586,9 @@ function executeProjectInitPlan(plan) {
|
|
|
1470
1586
|
mkdirSync4(dirname4(action.path), { recursive: true });
|
|
1471
1587
|
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
1472
1588
|
`;
|
|
1473
|
-
const current =
|
|
1589
|
+
const current = existsSync7(action.path) ? readFileSync3(action.path, "utf8") : void 0;
|
|
1474
1590
|
if (current !== next) {
|
|
1475
|
-
|
|
1591
|
+
writeFileSync4(action.path, next, "utf8");
|
|
1476
1592
|
changedFiles.push(action.path);
|
|
1477
1593
|
}
|
|
1478
1594
|
} else if (action.kind === "registry.upsert") {
|
|
@@ -1513,10 +1629,10 @@ function projectManifestFromRegistryProject(project) {
|
|
|
1513
1629
|
workspace: project.ticket_provider.workspace ?? "",
|
|
1514
1630
|
identifier: project.ticket_provider.identifier ?? "",
|
|
1515
1631
|
board_id: project.ticket_provider.board_id ?? "",
|
|
1516
|
-
board_url: project.ticket_provider.board_url ?? "",
|
|
1517
1632
|
state: project.ticket_provider.state
|
|
1518
1633
|
},
|
|
1519
|
-
agents
|
|
1634
|
+
agents,
|
|
1635
|
+
automation: project.automation ?? defaultProjectAutomation()
|
|
1520
1636
|
};
|
|
1521
1637
|
}
|
|
1522
1638
|
function getProject(registry, slug) {
|
|
@@ -1525,7 +1641,7 @@ function getProject(registry, slug) {
|
|
|
1525
1641
|
return project;
|
|
1526
1642
|
}
|
|
1527
1643
|
function buildCommonProjectCopierAction(input) {
|
|
1528
|
-
const templateDir =
|
|
1644
|
+
const templateDir = join9(input.pjanglerRoot, "templates", "commonproject");
|
|
1529
1645
|
const data = {
|
|
1530
1646
|
project_name: input.projectName,
|
|
1531
1647
|
project_description: input.projectDescription ?? "",
|
|
@@ -1535,7 +1651,6 @@ function buildCommonProjectCopierAction(input) {
|
|
|
1535
1651
|
plane_project_id: input.planeProjectId ?? "",
|
|
1536
1652
|
ticket_workspace: input.ticketWorkspace ?? input.planeWorkspace,
|
|
1537
1653
|
board_id: input.boardId ?? input.planeProjectId ?? "",
|
|
1538
|
-
board_url: input.boardUrl ?? "",
|
|
1539
1654
|
project_identifier: input.projectIdentifier,
|
|
1540
1655
|
primary_language: input.primaryLanguage,
|
|
1541
1656
|
agent_hooks_layer: input.agentHooksLayer ?? true ? "true" : "false"
|
|
@@ -1555,7 +1670,7 @@ function buildCommonProjectCopierAction(input) {
|
|
|
1555
1670
|
function resolvePjanglerRoot() {
|
|
1556
1671
|
let dir = dirname4(new URL(import.meta.url).pathname);
|
|
1557
1672
|
while (dir !== dirname4(dir)) {
|
|
1558
|
-
if (
|
|
1673
|
+
if (existsSync7(join9(dir, "package.json")) && existsSync7(join9(dir, "templates", "commonproject", "copier.yml"))) return dir;
|
|
1559
1674
|
dir = dirname4(dir);
|
|
1560
1675
|
}
|
|
1561
1676
|
return resolve(process.cwd());
|
|
@@ -1587,7 +1702,7 @@ function validateProjectRecord(project, key) {
|
|
|
1587
1702
|
}
|
|
1588
1703
|
function expandHome(path) {
|
|
1589
1704
|
if (path === "~") return homedir3();
|
|
1590
|
-
if (path.startsWith("~/")) return
|
|
1705
|
+
if (path.startsWith("~/")) return join9(homedir3(), path.slice(2));
|
|
1591
1706
|
return path;
|
|
1592
1707
|
}
|
|
1593
1708
|
function isRecord(value) {
|
|
@@ -1604,16 +1719,16 @@ function resolveTemplateRoot() {
|
|
|
1604
1719
|
try {
|
|
1605
1720
|
let dir = dirname5(fileURLToPath2(import.meta.url));
|
|
1606
1721
|
for (let i = 0; i < 8; i++) {
|
|
1607
|
-
candidates.push(
|
|
1722
|
+
candidates.push(join10(dir, "templates", "commonproject", "template"));
|
|
1608
1723
|
const parent = dirname5(dir);
|
|
1609
1724
|
if (parent === dir) break;
|
|
1610
1725
|
dir = parent;
|
|
1611
1726
|
}
|
|
1612
1727
|
} catch {
|
|
1613
1728
|
}
|
|
1614
|
-
candidates.push(
|
|
1729
|
+
candidates.push(join10(homedir4(), "code", "pjangler", "templates", "commonproject", "template"));
|
|
1615
1730
|
for (const c of candidates) {
|
|
1616
|
-
if (
|
|
1731
|
+
if (existsSync8(join10(c, ".agents", "hooks", "hooks.master.json"))) return c;
|
|
1617
1732
|
}
|
|
1618
1733
|
throw new Error(
|
|
1619
1734
|
"Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
|
|
@@ -1640,10 +1755,10 @@ var CopyAgentHooksTree = class extends Command {
|
|
|
1640
1755
|
const created = [];
|
|
1641
1756
|
const skipped = [];
|
|
1642
1757
|
for (const { rel, dir } of items) {
|
|
1643
|
-
const src =
|
|
1644
|
-
const dest =
|
|
1645
|
-
if (!
|
|
1646
|
-
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) {
|
|
1647
1762
|
skipped.push(rel);
|
|
1648
1763
|
continue;
|
|
1649
1764
|
}
|
|
@@ -1669,14 +1784,14 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
|
1669
1784
|
if (!resolveAgentHooksLayer()) {
|
|
1670
1785
|
return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
|
|
1671
1786
|
}
|
|
1672
|
-
const misePath =
|
|
1673
|
-
if (!
|
|
1787
|
+
const misePath = join10(this.context.targetDir, "mise.toml");
|
|
1788
|
+
if (!existsSync8(misePath)) {
|
|
1674
1789
|
return {
|
|
1675
1790
|
success: false,
|
|
1676
1791
|
message: "\u26A0\uFE0F No mise.toml found \u2014 run `pjangler init mise` first, then re-run."
|
|
1677
1792
|
};
|
|
1678
1793
|
}
|
|
1679
|
-
let content =
|
|
1794
|
+
let content = readFileSync4(misePath, "utf8");
|
|
1680
1795
|
if (content.includes(_WireMiseAgentHooks.MARKER)) {
|
|
1681
1796
|
return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
|
|
1682
1797
|
}
|
|
@@ -1752,7 +1867,7 @@ ${leaveBlock}`);
|
|
|
1752
1867
|
""
|
|
1753
1868
|
].join("\n");
|
|
1754
1869
|
content = content.replace(/\n*$/, "\n") + appended;
|
|
1755
|
-
if (!this.context.dryRun)
|
|
1870
|
+
if (!this.context.dryRun) writeFileSync5(misePath, content);
|
|
1756
1871
|
if (wiredHooks) {
|
|
1757
1872
|
return { success: true, message: this.formatMessage("\u2705 Wired mise.toml ([hooks] enter/leave + tasks)") };
|
|
1758
1873
|
}
|
|
@@ -1906,15 +2021,15 @@ function createRecipe(name, context) {
|
|
|
1906
2021
|
}
|
|
1907
2022
|
|
|
1908
2023
|
// src/utils/version.ts
|
|
1909
|
-
import { readFileSync as
|
|
1910
|
-
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";
|
|
1911
2026
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1912
2027
|
var PJANGLER_VERSION = (() => {
|
|
1913
2028
|
try {
|
|
1914
2029
|
let dir = dirname6(fileURLToPath3(import.meta.url));
|
|
1915
2030
|
for (let i = 0; i < 4; i++) {
|
|
1916
2031
|
try {
|
|
1917
|
-
const raw =
|
|
2032
|
+
const raw = readFileSync5(join11(dir, "package.json"), "utf8");
|
|
1918
2033
|
return JSON.parse(raw).version ?? "0.0.0";
|
|
1919
2034
|
} catch {
|
|
1920
2035
|
const parent = dirname6(dir);
|
|
@@ -1928,11 +2043,11 @@ var PJANGLER_VERSION = (() => {
|
|
|
1928
2043
|
})();
|
|
1929
2044
|
|
|
1930
2045
|
// src/parity/index.ts
|
|
1931
|
-
import { existsSync as
|
|
1932
|
-
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";
|
|
1933
2048
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
1934
2049
|
import { homedir as homedir5 } from "node:os";
|
|
1935
|
-
import { spawnSync as
|
|
2050
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
1936
2051
|
var LINK_AGENTFILES_BLOCK = `# This block will handle the linking of
|
|
1937
2052
|
# agent files to the main AGENTS.md file.
|
|
1938
2053
|
#
|
|
@@ -2003,7 +2118,7 @@ run = "{{config_root}}/.mise/scripts/versioning.sh sync"
|
|
|
2003
2118
|
function resolvePjanglerRoot2() {
|
|
2004
2119
|
let dir = dirname7(fileURLToPath4(import.meta.url));
|
|
2005
2120
|
while (dir !== dirname7(dir)) {
|
|
2006
|
-
if (
|
|
2121
|
+
if (existsSync9(join12(dir, "package.json")) && existsSync9(join12(dir, "templates", "commonproject", "copier.yml"))) {
|
|
2007
2122
|
return dir;
|
|
2008
2123
|
}
|
|
2009
2124
|
dir = dirname7(dir);
|
|
@@ -2014,17 +2129,17 @@ function normalizeNewlines(value) {
|
|
|
2014
2129
|
return value.replace(/\r\n/g, "\n");
|
|
2015
2130
|
}
|
|
2016
2131
|
function readText(path) {
|
|
2017
|
-
return normalizeNewlines(
|
|
2132
|
+
return normalizeNewlines(readFileSync6(path, "utf8"));
|
|
2018
2133
|
}
|
|
2019
2134
|
function safeReadText(path) {
|
|
2020
|
-
return
|
|
2135
|
+
return existsSync9(path) ? readText(path) : null;
|
|
2021
2136
|
}
|
|
2022
2137
|
function ensureParent(path) {
|
|
2023
2138
|
mkdirSync6(dirname7(path), { recursive: true });
|
|
2024
2139
|
}
|
|
2025
2140
|
function writeText(path, content) {
|
|
2026
2141
|
ensureParent(path);
|
|
2027
|
-
|
|
2142
|
+
writeFileSync6(path, content);
|
|
2028
2143
|
}
|
|
2029
2144
|
function tryParseJson(text2) {
|
|
2030
2145
|
if (!text2) return null;
|
|
@@ -2041,7 +2156,7 @@ function titleCaseSlug(slug) {
|
|
|
2041
2156
|
return slug.split(/[-_]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
2042
2157
|
}
|
|
2043
2158
|
function readSymlinkTarget(path) {
|
|
2044
|
-
if (!
|
|
2159
|
+
if (!existsSync9(path)) return null;
|
|
2045
2160
|
try {
|
|
2046
2161
|
return readlinkSync(path);
|
|
2047
2162
|
} catch {
|
|
@@ -2049,7 +2164,7 @@ function readSymlinkTarget(path) {
|
|
|
2049
2164
|
}
|
|
2050
2165
|
}
|
|
2051
2166
|
function ensureSymlink(path, target, dryRun) {
|
|
2052
|
-
if (
|
|
2167
|
+
if (existsSync9(path)) {
|
|
2053
2168
|
const stat = lstatSync(path);
|
|
2054
2169
|
if (stat.isSymbolicLink()) {
|
|
2055
2170
|
const current = readSymlinkTarget(path);
|
|
@@ -2066,11 +2181,11 @@ function ensureSymlink(path, target, dryRun) {
|
|
|
2066
2181
|
return { changed: true };
|
|
2067
2182
|
}
|
|
2068
2183
|
function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
2069
|
-
const agentsPath =
|
|
2070
|
-
if (
|
|
2184
|
+
const agentsPath = join12(repoRoot, "AGENTS.md");
|
|
2185
|
+
if (existsSync9(agentsPath)) return { changedFiles: [], details: [] };
|
|
2071
2186
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2072
|
-
const source =
|
|
2073
|
-
if (!
|
|
2187
|
+
const source = join12(repoRoot, file);
|
|
2188
|
+
if (!existsSync9(source)) continue;
|
|
2074
2189
|
const stat = lstatSync(source);
|
|
2075
2190
|
if (stat.isSymbolicLink()) continue;
|
|
2076
2191
|
if (stat.isFile()) {
|
|
@@ -2079,8 +2194,8 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
|
2079
2194
|
}
|
|
2080
2195
|
return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
|
|
2081
2196
|
}
|
|
2082
|
-
const readmePath =
|
|
2083
|
-
if (
|
|
2197
|
+
const readmePath = join12(repoRoot, "README.md");
|
|
2198
|
+
if (existsSync9(readmePath)) {
|
|
2084
2199
|
const stat = lstatSync(readmePath);
|
|
2085
2200
|
if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
|
|
2086
2201
|
if (!dryRun) copyFileSync(readmePath, agentsPath);
|
|
@@ -2119,12 +2234,12 @@ function yamlGet(text2, keyPath) {
|
|
|
2119
2234
|
return "";
|
|
2120
2235
|
}
|
|
2121
2236
|
function discoverRoles(repoRoot) {
|
|
2122
|
-
const rolesDir =
|
|
2123
|
-
if (!
|
|
2124
|
-
return
|
|
2125
|
-
const roleDir =
|
|
2126
|
-
const roleYamlPath =
|
|
2127
|
-
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;
|
|
2128
2243
|
const text2 = readText(roleYamlPath);
|
|
2129
2244
|
const runtimeRepoRaw = yamlGet(text2, "runtime.github_repo");
|
|
2130
2245
|
return {
|
|
@@ -2142,16 +2257,20 @@ function discoverRoles(repoRoot) {
|
|
|
2142
2257
|
planeWorkspace: yamlGet(text2, "ticket_provider.workspace") || yamlGet(text2, "plane.workspace"),
|
|
2143
2258
|
ticketProviderName: yamlGet(text2, "ticket_provider.name"),
|
|
2144
2259
|
ticketProviderBoardId: yamlGet(text2, "ticket_provider.board_id"),
|
|
2145
|
-
|
|
2146
|
-
|
|
2260
|
+
ticketProviderIdentifier: yamlGet(text2, "plane.identifier"),
|
|
2261
|
+
legacyReconcileEnabled: yamlGet(text2, "reconcile.enabled"),
|
|
2262
|
+
legacyReconcileGraceHours: yamlGet(text2, "reconcile.grace_hours"),
|
|
2263
|
+
legacyReconcileAutoReview: yamlGet(text2, "reconcile.auto_review"),
|
|
2264
|
+
legacyScrumGraceHours: yamlGet(text2, "scrum_master.grace_hours"),
|
|
2265
|
+
legacyScrumAutoReview: yamlGet(text2, "scrum_master.auto_review")
|
|
2147
2266
|
};
|
|
2148
2267
|
}).filter((value) => Boolean(value));
|
|
2149
2268
|
}
|
|
2150
2269
|
function registryPath(homeDir) {
|
|
2151
|
-
return
|
|
2270
|
+
return join12(homeDir, ".hermes", "agents-registry.yaml");
|
|
2152
2271
|
}
|
|
2153
2272
|
function systemctlUser(args) {
|
|
2154
|
-
const result =
|
|
2273
|
+
const result = spawnSync6("systemctl", ["--user", ...args], { encoding: "utf8" });
|
|
2155
2274
|
return {
|
|
2156
2275
|
ok: result.status === 0,
|
|
2157
2276
|
stdout: result.stdout.trim(),
|
|
@@ -2159,8 +2278,8 @@ function systemctlUser(args) {
|
|
|
2159
2278
|
};
|
|
2160
2279
|
}
|
|
2161
2280
|
function templateScript(ctx, name) {
|
|
2162
|
-
const source =
|
|
2163
|
-
return
|
|
2281
|
+
const source = join12(ctx.pjanglerRoot, ".mise", "scripts", name);
|
|
2282
|
+
return existsSync9(source) ? readText(source) : void 0;
|
|
2164
2283
|
}
|
|
2165
2284
|
function templateVersioningScript(ctx) {
|
|
2166
2285
|
return templateScript(ctx, "versioning.sh");
|
|
@@ -2168,16 +2287,45 @@ function templateVersioningScript(ctx) {
|
|
|
2168
2287
|
function templateLinkAgentfilesScript(ctx) {
|
|
2169
2288
|
return templateScript(ctx, "link-agentfiles.sh");
|
|
2170
2289
|
}
|
|
2290
|
+
function resolveAgentHooksLayer2(ctx) {
|
|
2291
|
+
const override = process.env.PJ_AGENT_HOOKS_LAYER;
|
|
2292
|
+
if (override === "0" || override === "false") return false;
|
|
2293
|
+
if (override === "1" || override === "true") return true;
|
|
2294
|
+
if (existsSync9(join12(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
|
|
2295
|
+
return !existsSync9(join12(ctx.homeDir, ".agents", "hooks"));
|
|
2296
|
+
}
|
|
2297
|
+
function evaluateMiseConditionals(template, agentHooksLayer) {
|
|
2298
|
+
const out = [];
|
|
2299
|
+
let depth = 0;
|
|
2300
|
+
let skipDepth = 0;
|
|
2301
|
+
for (const line of template.split("\n")) {
|
|
2302
|
+
const stmt = line.trim();
|
|
2303
|
+
const ifMatch = /^\{%-?\s*if\s+(\w+)\s*-?%\}$/.exec(stmt);
|
|
2304
|
+
if (ifMatch) {
|
|
2305
|
+
depth += 1;
|
|
2306
|
+
const truthy = ifMatch[1] === "agent_hooks_layer" ? agentHooksLayer : false;
|
|
2307
|
+
if (skipDepth === 0 && !truthy) skipDepth = depth;
|
|
2308
|
+
continue;
|
|
2309
|
+
}
|
|
2310
|
+
if (/^\{%-?\s*endif\s*-?%\}$/.test(stmt)) {
|
|
2311
|
+
if (skipDepth === depth) skipDepth = 0;
|
|
2312
|
+
depth = Math.max(0, depth - 1);
|
|
2313
|
+
continue;
|
|
2314
|
+
}
|
|
2315
|
+
if (skipDepth === 0) out.push(line);
|
|
2316
|
+
}
|
|
2317
|
+
return out.join("\n");
|
|
2318
|
+
}
|
|
2171
2319
|
function renderGeneratedProjectMiseToml(ctx, template) {
|
|
2172
2320
|
const project = readProjectJson(ctx);
|
|
2173
2321
|
const projectName = String(project?.project_name ?? basename3(ctx.repoRoot) ?? "project");
|
|
2174
|
-
return template.replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
|
|
2322
|
+
return evaluateMiseConditionals(template, resolveAgentHooksLayer2(ctx)).replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
|
|
2175
2323
|
}
|
|
2176
2324
|
function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
2177
|
-
const targetPath =
|
|
2178
|
-
if (
|
|
2179
|
-
const sourcePath =
|
|
2180
|
-
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;
|
|
2181
2329
|
changedFiles.push(targetPath);
|
|
2182
2330
|
if (!ctx.dryRun) {
|
|
2183
2331
|
writeText(targetPath, renderGeneratedProjectMiseToml(ctx, readText(sourcePath)));
|
|
@@ -2185,8 +2333,8 @@ function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
|
2185
2333
|
return true;
|
|
2186
2334
|
}
|
|
2187
2335
|
function templateVersionFilesConf(ctx, repoRoot) {
|
|
2188
|
-
const packageJson =
|
|
2189
|
-
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";
|
|
2190
2338
|
}
|
|
2191
2339
|
function replaceOrAppendManagedBlock(text2, startMarker, block, beforePattern) {
|
|
2192
2340
|
if (startMarker.test(text2)) {
|
|
@@ -2210,7 +2358,7 @@ var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/herm
|
|
|
2210
2358
|
function requiredMisePathEntries(ctx) {
|
|
2211
2359
|
const required = [...BASE_MISE_PATH_ENTRIES];
|
|
2212
2360
|
for (const candidate of CONDITIONAL_HERMES_PATHS) {
|
|
2213
|
-
if (
|
|
2361
|
+
if (existsSync9(join12(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
|
|
2214
2362
|
}
|
|
2215
2363
|
return required;
|
|
2216
2364
|
}
|
|
@@ -2359,7 +2507,24 @@ function upsertLinkAgentfilesBlock(text2, ctx) {
|
|
|
2359
2507
|
return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
|
|
2360
2508
|
}
|
|
2361
2509
|
function readProjectJson(ctx) {
|
|
2362
|
-
return tryParseJson(safeReadText(
|
|
2510
|
+
return tryParseJson(safeReadText(join12(ctx.repoRoot, ".project.json")));
|
|
2511
|
+
}
|
|
2512
|
+
function boolSetting(value, fallback) {
|
|
2513
|
+
if (typeof value === "boolean") return value;
|
|
2514
|
+
if (typeof value === "string") {
|
|
2515
|
+
const normalized = value.trim().toLowerCase();
|
|
2516
|
+
if (["true", "1", "yes", "on"].includes(normalized)) return true;
|
|
2517
|
+
if (["false", "0", "no", "off"].includes(normalized)) return false;
|
|
2518
|
+
}
|
|
2519
|
+
return fallback;
|
|
2520
|
+
}
|
|
2521
|
+
function numberSetting(value, fallback) {
|
|
2522
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
2523
|
+
if (typeof value === "string" && value.trim()) {
|
|
2524
|
+
const parsed = Number(value);
|
|
2525
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
2526
|
+
}
|
|
2527
|
+
return fallback;
|
|
2363
2528
|
}
|
|
2364
2529
|
function canonicalProjectJson(ctx) {
|
|
2365
2530
|
const roles = discoverRoles(ctx.repoRoot);
|
|
@@ -2371,9 +2536,9 @@ function canonicalProjectJson(ctx) {
|
|
|
2371
2536
|
workspace: String((existing.ticket_provider?.workspace ?? firstRole?.planeWorkspace ?? "") || ""),
|
|
2372
2537
|
identifier: String((existing.ticket_provider?.identifier ?? firstRole?.ticketProviderIdentifier ?? "") || ""),
|
|
2373
2538
|
board_id: String((existing.ticket_provider?.board_id ?? firstRole?.ticketProviderBoardId ?? "") || ""),
|
|
2374
|
-
|
|
2375
|
-
state: String((existing.ticket_provider?.state ?? "planned") || "planned")
|
|
2539
|
+
state: String((existing.ticket_provider?.state ?? (firstRole?.ticketProviderBoardId ? "linked" : "planned")) || "planned")
|
|
2376
2540
|
};
|
|
2541
|
+
if (ticketProvider.board_id && ticketProvider.state === "planned") ticketProvider.state = "linked";
|
|
2377
2542
|
const existingAgents = existing.agents ?? {};
|
|
2378
2543
|
const discoveredAgents = Object.fromEntries(
|
|
2379
2544
|
roles.map((role) => [
|
|
@@ -2393,28 +2558,42 @@ function canonicalProjectJson(ctx) {
|
|
|
2393
2558
|
provisioning_state: existingAgent.provisioning_state
|
|
2394
2559
|
};
|
|
2395
2560
|
}
|
|
2561
|
+
const existingAutomation = existing.automation ?? {};
|
|
2562
|
+
const existingReconcile = existingAutomation.reconcile ?? {};
|
|
2563
|
+
const legacyEnabled = roles.find((role) => role.legacyReconcileEnabled)?.legacyReconcileEnabled;
|
|
2564
|
+
const legacyGrace = roles.find((role) => role.legacyReconcileGraceHours || role.legacyScrumGraceHours);
|
|
2565
|
+
const legacyAutoReview = roles.find((role) => role.legacyReconcileAutoReview || role.legacyScrumAutoReview);
|
|
2566
|
+
const automation = {
|
|
2567
|
+
...existingAutomation,
|
|
2568
|
+
reconcile: {
|
|
2569
|
+
enabled: boolSetting(existingReconcile.enabled, boolSetting(legacyEnabled, false)),
|
|
2570
|
+
grace_hours: numberSetting(existingReconcile.grace_hours, numberSetting(legacyGrace?.legacyReconcileGraceHours || legacyGrace?.legacyScrumGraceHours, 0)),
|
|
2571
|
+
auto_review: boolSetting(existingReconcile.auto_review, boolSetting(legacyAutoReview?.legacyReconcileAutoReview || legacyAutoReview?.legacyScrumAutoReview, true))
|
|
2572
|
+
}
|
|
2573
|
+
};
|
|
2396
2574
|
return {
|
|
2397
2575
|
project_name: String(existing.project_name ?? titleCaseSlug(slug)),
|
|
2398
2576
|
project_description: String(existing.project_description ?? ""),
|
|
2399
2577
|
project_slug: slug,
|
|
2400
2578
|
repo_path: ctx.repoRoot,
|
|
2401
2579
|
ticket_provider: ticketProvider,
|
|
2402
|
-
agents
|
|
2580
|
+
agents,
|
|
2581
|
+
automation
|
|
2403
2582
|
};
|
|
2404
2583
|
}
|
|
2405
2584
|
function projectJsonFinding(ctx) {
|
|
2406
|
-
const projectPath =
|
|
2407
|
-
const planeJsonPath =
|
|
2585
|
+
const projectPath = join12(ctx.repoRoot, ".project.json");
|
|
2586
|
+
const planeJsonPath = join12(ctx.repoRoot, ".plane.json");
|
|
2408
2587
|
const details = [];
|
|
2409
2588
|
const data = readProjectJson(ctx);
|
|
2410
2589
|
const roles = discoverRoles(ctx.repoRoot);
|
|
2411
|
-
if (!
|
|
2590
|
+
if (!existsSync9(projectPath)) {
|
|
2412
2591
|
return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json missing", details: [], fixable: true };
|
|
2413
2592
|
}
|
|
2414
2593
|
if (!data) {
|
|
2415
2594
|
return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json is not valid JSON", details: [], fixable: true };
|
|
2416
2595
|
}
|
|
2417
|
-
for (const key of ["project_name", "project_description", "project_slug", "repo_path", "ticket_provider", "agents"]) {
|
|
2596
|
+
for (const key of ["project_name", "project_description", "project_slug", "repo_path", "ticket_provider", "agents", "automation"]) {
|
|
2418
2597
|
if (!(key in data)) details.push(`missing key: ${key}`);
|
|
2419
2598
|
}
|
|
2420
2599
|
if (data.repo_path !== ctx.repoRoot) details.push(`repo_path should be ${ctx.repoRoot}`);
|
|
@@ -2431,10 +2610,19 @@ function projectJsonFinding(ctx) {
|
|
|
2431
2610
|
}
|
|
2432
2611
|
}
|
|
2433
2612
|
const ticketProvider = data.ticket_provider ?? {};
|
|
2434
|
-
for (const key of ["type", "workspace", "identifier", "board_id", "
|
|
2613
|
+
for (const key of ["type", "workspace", "identifier", "board_id", "state"]) {
|
|
2435
2614
|
if (!(key in ticketProvider)) details.push(`ticket_provider.${key} missing`);
|
|
2436
2615
|
}
|
|
2437
|
-
if (
|
|
2616
|
+
if ("board_url" in ticketProvider) details.push("ticket_provider.board_url should be removed; derive it from provider/workspace/board_id");
|
|
2617
|
+
if (!ticketProvider.board_id && roles.some((role) => role.ticketProviderBoardId)) {
|
|
2618
|
+
details.push("ticket_provider.board_id missing even though legacy role.yaml contains a board binding");
|
|
2619
|
+
}
|
|
2620
|
+
const automation = data.automation ?? {};
|
|
2621
|
+
const reconcile = automation.reconcile ?? {};
|
|
2622
|
+
for (const key of ["enabled", "grace_hours", "auto_review"]) {
|
|
2623
|
+
if (!(key in reconcile)) details.push(`automation.reconcile.${key} missing`);
|
|
2624
|
+
}
|
|
2625
|
+
if (existsSync9(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
|
|
2438
2626
|
return {
|
|
2439
2627
|
id: "sot.project-json",
|
|
2440
2628
|
title: "Canonical .project.json",
|
|
@@ -2447,7 +2635,7 @@ function projectJsonFinding(ctx) {
|
|
|
2447
2635
|
function renderSoul(role) {
|
|
2448
2636
|
const telegram = role.botHandle ? `@${role.botHandle}` : "(unwired)";
|
|
2449
2637
|
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.";
|
|
2450
|
-
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 checkpoints your runtime; when this repo opts into reconciliation (\`reconcile.enabled\` in
|
|
2638
|
+
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 checkpoints your runtime; 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.`;
|
|
2451
2639
|
const runtimeOwner = role.runtimeOwner || "delorenj";
|
|
2452
2640
|
return `# ${role.displayName || role.agentId}
|
|
2453
2641
|
|
|
@@ -2515,17 +2703,17 @@ exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" HERMES_OAUTH
|
|
|
2515
2703
|
`.replace(/\u0010/g, "$");
|
|
2516
2704
|
}
|
|
2517
2705
|
function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
|
|
2518
|
-
if (!
|
|
2706
|
+
if (!existsSync9(sourceDir)) return;
|
|
2519
2707
|
mkdirSync6(targetDir, { recursive: true });
|
|
2520
|
-
for (const entry of
|
|
2521
|
-
const sourcePath =
|
|
2708
|
+
for (const entry of readdirSync2(sourceDir, { withFileTypes: true })) {
|
|
2709
|
+
const sourcePath = join12(sourceDir, entry.name);
|
|
2522
2710
|
if (skip?.(sourcePath)) continue;
|
|
2523
|
-
const targetPath =
|
|
2711
|
+
const targetPath = join12(targetDir, entry.name);
|
|
2524
2712
|
if (entry.isDirectory()) {
|
|
2525
2713
|
copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
|
|
2526
2714
|
continue;
|
|
2527
2715
|
}
|
|
2528
|
-
if (
|
|
2716
|
+
if (existsSync9(targetPath)) continue;
|
|
2529
2717
|
changedFiles.push(targetPath);
|
|
2530
2718
|
if (!dryRun) {
|
|
2531
2719
|
ensureParent(targetPath);
|
|
@@ -2534,7 +2722,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
|
|
|
2534
2722
|
}
|
|
2535
2723
|
}
|
|
2536
2724
|
function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
|
|
2537
|
-
const gitmodulesPath =
|
|
2725
|
+
const gitmodulesPath = join12(repoRoot, ".gitmodules");
|
|
2538
2726
|
const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
|
|
2539
2727
|
const owner = role.runtimeOwner || "delorenj";
|
|
2540
2728
|
const block = `[submodule "agents/hermes/${role.role}/runtime"]
|
|
@@ -2634,14 +2822,14 @@ var RULES = [
|
|
|
2634
2822
|
id: "mise.config-root",
|
|
2635
2823
|
title: "mise config_root + AGENTS link hooks",
|
|
2636
2824
|
audit: (ctx) => {
|
|
2637
|
-
const misePath =
|
|
2638
|
-
if (!
|
|
2825
|
+
const misePath = join12(ctx.repoRoot, "mise.toml");
|
|
2826
|
+
if (!existsSync9(misePath)) {
|
|
2639
2827
|
return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
|
|
2640
2828
|
}
|
|
2641
2829
|
const text2 = readText(misePath);
|
|
2642
2830
|
const details = [];
|
|
2643
|
-
const linkAgentfilesPath =
|
|
2644
|
-
if (!
|
|
2831
|
+
const linkAgentfilesPath = join12(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
2832
|
+
if (!existsSync9(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
|
|
2645
2833
|
const pathValues = [...(text2.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
2646
2834
|
const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
|
|
2647
2835
|
if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
|
|
@@ -2659,10 +2847,10 @@ var RULES = [
|
|
|
2659
2847
|
};
|
|
2660
2848
|
},
|
|
2661
2849
|
migrate: (ctx, finding) => {
|
|
2662
|
-
const path =
|
|
2850
|
+
const path = join12(ctx.repoRoot, "mise.toml");
|
|
2663
2851
|
const changedFiles = [];
|
|
2664
2852
|
const details = [];
|
|
2665
|
-
if (!
|
|
2853
|
+
if (!existsSync9(path)) {
|
|
2666
2854
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
2667
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: [] };
|
|
2668
2856
|
}
|
|
@@ -2678,7 +2866,7 @@ var RULES = [
|
|
|
2678
2866
|
if (!ctx.dryRun) writeText(path, next);
|
|
2679
2867
|
text2 = next;
|
|
2680
2868
|
}
|
|
2681
|
-
const linkAgentfilesPath =
|
|
2869
|
+
const linkAgentfilesPath = join12(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
2682
2870
|
const expectedScript = templateLinkAgentfilesScript(ctx);
|
|
2683
2871
|
if (expectedScript === void 0) {
|
|
2684
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: [] };
|
|
@@ -2705,13 +2893,13 @@ var RULES = [
|
|
|
2705
2893
|
title: "managed mise versioning block",
|
|
2706
2894
|
audit: (ctx) => {
|
|
2707
2895
|
const details = [];
|
|
2708
|
-
const misePath =
|
|
2709
|
-
const versioningPath =
|
|
2710
|
-
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");
|
|
2711
2899
|
const text2 = safeReadText(misePath);
|
|
2712
2900
|
if (!text2?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
|
|
2713
|
-
if (!
|
|
2714
|
-
if (!
|
|
2901
|
+
if (!existsSync9(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
|
|
2902
|
+
if (!existsSync9(manifestPath)) details.push(".mise/version-files.conf missing");
|
|
2715
2903
|
return {
|
|
2716
2904
|
id: "mise.versioning",
|
|
2717
2905
|
title: "managed mise versioning block",
|
|
@@ -2724,8 +2912,8 @@ var RULES = [
|
|
|
2724
2912
|
migrate: (ctx, finding) => {
|
|
2725
2913
|
const changedFiles = [];
|
|
2726
2914
|
const details = [];
|
|
2727
|
-
const misePath =
|
|
2728
|
-
if (!
|
|
2915
|
+
const misePath = join12(ctx.repoRoot, "mise.toml");
|
|
2916
|
+
if (!existsSync9(misePath)) {
|
|
2729
2917
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
2730
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: [] };
|
|
2731
2919
|
}
|
|
@@ -2735,12 +2923,21 @@ var RULES = [
|
|
|
2735
2923
|
}
|
|
2736
2924
|
}
|
|
2737
2925
|
const currentMise = readText(misePath);
|
|
2738
|
-
|
|
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);
|
|
2739
2936
|
if (nextMise !== currentMise) {
|
|
2740
2937
|
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
2741
2938
|
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
2742
2939
|
}
|
|
2743
|
-
const versioningPath =
|
|
2940
|
+
const versioningPath = join12(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
2744
2941
|
const expectedScript = templateVersioningScript(ctx);
|
|
2745
2942
|
if (expectedScript === void 0) {
|
|
2746
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: [] };
|
|
@@ -2752,7 +2949,7 @@ var RULES = [
|
|
|
2752
2949
|
chmodSync2(versioningPath, 493);
|
|
2753
2950
|
}
|
|
2754
2951
|
}
|
|
2755
|
-
const manifestPath =
|
|
2952
|
+
const manifestPath = join12(ctx.repoRoot, ".mise", "version-files.conf");
|
|
2756
2953
|
const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
|
|
2757
2954
|
if (safeReadText(manifestPath) !== expectedManifest) {
|
|
2758
2955
|
changedFiles.push(manifestPath);
|
|
@@ -2772,9 +2969,9 @@ var RULES = [
|
|
|
2772
2969
|
id: "sot.agent-symlinks",
|
|
2773
2970
|
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
2774
2971
|
audit: (ctx) => {
|
|
2775
|
-
const agentsPath =
|
|
2776
|
-
if (!
|
|
2777
|
-
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)));
|
|
2778
2975
|
if (fallbackSources.length === 0) {
|
|
2779
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 };
|
|
2780
2977
|
}
|
|
@@ -2789,7 +2986,7 @@ var RULES = [
|
|
|
2789
2986
|
}
|
|
2790
2987
|
const details = [];
|
|
2791
2988
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2792
|
-
const full =
|
|
2989
|
+
const full = join12(ctx.repoRoot, file);
|
|
2793
2990
|
const target = readSymlinkTarget(full);
|
|
2794
2991
|
if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
|
|
2795
2992
|
}
|
|
@@ -2813,7 +3010,7 @@ var RULES = [
|
|
|
2813
3010
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
|
|
2814
3011
|
}
|
|
2815
3012
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2816
|
-
const full =
|
|
3013
|
+
const full = join12(ctx.repoRoot, file);
|
|
2817
3014
|
const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
|
|
2818
3015
|
if (result.blocked) blockedDetails.push(result.blocked);
|
|
2819
3016
|
if (result.changed) changedFiles.push(full);
|
|
@@ -2835,7 +3032,7 @@ var RULES = [
|
|
|
2835
3032
|
migrate: (ctx, finding) => {
|
|
2836
3033
|
const changedFiles = [];
|
|
2837
3034
|
const details = [];
|
|
2838
|
-
const path =
|
|
3035
|
+
const path = join12(ctx.repoRoot, ".project.json");
|
|
2839
3036
|
const existing = readProjectJson(ctx) ?? {};
|
|
2840
3037
|
const canonical = canonicalProjectJson(ctx);
|
|
2841
3038
|
const merged = { ...existing, ...canonical };
|
|
@@ -2845,10 +3042,10 @@ var RULES = [
|
|
|
2845
3042
|
changedFiles.push(path);
|
|
2846
3043
|
if (!ctx.dryRun) writeText(path, expected);
|
|
2847
3044
|
}
|
|
2848
|
-
const planeJson =
|
|
2849
|
-
if (
|
|
3045
|
+
const planeJson = join12(ctx.repoRoot, ".plane.json");
|
|
3046
|
+
if (existsSync9(planeJson)) {
|
|
2850
3047
|
const backup = `${planeJson}.migrated-backup`;
|
|
2851
|
-
if (
|
|
3048
|
+
if (existsSync9(backup)) {
|
|
2852
3049
|
details.push(`cannot back up .plane.json because ${relative(ctx.repoRoot, backup)} already exists`);
|
|
2853
3050
|
} else {
|
|
2854
3051
|
changedFiles.push(backup);
|
|
@@ -2870,8 +3067,8 @@ var RULES = [
|
|
|
2870
3067
|
title: ".env.op + gitignore secrets contract",
|
|
2871
3068
|
audit: (ctx) => {
|
|
2872
3069
|
const details = [];
|
|
2873
|
-
const envOp = safeReadText(
|
|
2874
|
-
const gitignore = safeReadText(
|
|
3070
|
+
const envOp = safeReadText(join12(ctx.repoRoot, ".env.op"));
|
|
3071
|
+
const gitignore = safeReadText(join12(ctx.repoRoot, ".gitignore"));
|
|
2875
3072
|
if (!envOp) {
|
|
2876
3073
|
details.push(".env.op missing");
|
|
2877
3074
|
} else {
|
|
@@ -2897,12 +3094,12 @@ var RULES = [
|
|
|
2897
3094
|
migrate: (ctx, finding) => {
|
|
2898
3095
|
const changedFiles = [];
|
|
2899
3096
|
const details = [];
|
|
2900
|
-
const envOpPath =
|
|
2901
|
-
if (!
|
|
3097
|
+
const envOpPath = join12(ctx.repoRoot, ".env.op");
|
|
3098
|
+
if (!existsSync9(envOpPath)) {
|
|
2902
3099
|
changedFiles.push(envOpPath);
|
|
2903
|
-
if (!ctx.dryRun) writeText(envOpPath, readText(
|
|
3100
|
+
if (!ctx.dryRun) writeText(envOpPath, readText(join12(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
|
|
2904
3101
|
}
|
|
2905
|
-
const gitignorePath =
|
|
3102
|
+
const gitignorePath = join12(ctx.repoRoot, ".gitignore");
|
|
2906
3103
|
const gitignore = safeReadText(gitignorePath) ?? "";
|
|
2907
3104
|
const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
|
|
2908
3105
|
# NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
|
|
@@ -2929,7 +3126,7 @@ var RULES = [
|
|
|
2929
3126
|
title: ".copier-answers.yml provenance + drift report",
|
|
2930
3127
|
audit: (ctx) => {
|
|
2931
3128
|
const details = [];
|
|
2932
|
-
const path =
|
|
3129
|
+
const path = join12(ctx.repoRoot, ".copier-answers.yml");
|
|
2933
3130
|
const text2 = safeReadText(path);
|
|
2934
3131
|
const project = readProjectJson(ctx);
|
|
2935
3132
|
if (!text2) {
|
|
@@ -2960,12 +3157,12 @@ var RULES = [
|
|
|
2960
3157
|
const changedFiles = [];
|
|
2961
3158
|
const project = canonicalProjectJson(ctx);
|
|
2962
3159
|
const text2 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
|
|
2963
|
-
_src_path: ${
|
|
3160
|
+
_src_path: ${join12(ctx.pjanglerRoot, "templates", "commonproject")}
|
|
2964
3161
|
project_description: ${String(project.project_description)}
|
|
2965
3162
|
project_name: ${String(project.project_name)}
|
|
2966
3163
|
ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
2967
3164
|
`;
|
|
2968
|
-
const path =
|
|
3165
|
+
const path = join12(ctx.repoRoot, ".copier-answers.yml");
|
|
2969
3166
|
if (safeReadText(path) !== text2) {
|
|
2970
3167
|
changedFiles.push(path);
|
|
2971
3168
|
if (!ctx.dryRun) writeText(path, text2);
|
|
@@ -2984,15 +3181,15 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2984
3181
|
id: "bmad.scaffold",
|
|
2985
3182
|
title: "BMAD modules/docs scaffold",
|
|
2986
3183
|
audit: (ctx) => {
|
|
2987
|
-
const sourceRoot =
|
|
2988
|
-
const targetRoot =
|
|
3184
|
+
const sourceRoot = join12(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
|
|
3185
|
+
const targetRoot = join12(ctx.repoRoot, "_bmad");
|
|
2989
3186
|
const sentinels = [
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
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")
|
|
2994
3191
|
];
|
|
2995
|
-
const missing = sentinels.filter((file) =>
|
|
3192
|
+
const missing = sentinels.filter((file) => existsSync9(join12(sourceRoot, file)) && !existsSync9(join12(targetRoot, file)));
|
|
2996
3193
|
return {
|
|
2997
3194
|
id: "bmad.scaffold",
|
|
2998
3195
|
title: "BMAD modules/docs scaffold",
|
|
@@ -3004,7 +3201,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3004
3201
|
},
|
|
3005
3202
|
migrate: (ctx, finding) => {
|
|
3006
3203
|
const changedFiles = [];
|
|
3007
|
-
copyMissingRecursive(
|
|
3204
|
+
copyMissingRecursive(join12(ctx.pjanglerRoot, "templates", "commonproject", "_bmad"), join12(ctx.repoRoot, "_bmad"), changedFiles, ctx.dryRun);
|
|
3008
3205
|
return {
|
|
3009
3206
|
id: finding.id,
|
|
3010
3207
|
title: finding.title,
|
|
@@ -3026,11 +3223,11 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3026
3223
|
}
|
|
3027
3224
|
const details = [];
|
|
3028
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"]) {
|
|
3029
|
-
if (!
|
|
3226
|
+
if (!existsSync9(join12(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join12(role.roleDir, rel))}`);
|
|
3030
3227
|
}
|
|
3031
|
-
const gitmodules = safeReadText(
|
|
3228
|
+
const gitmodules = safeReadText(join12(ctx.repoRoot, ".gitmodules")) ?? "";
|
|
3032
3229
|
if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
|
|
3033
|
-
if (!profileMetaInheritsDefault(
|
|
3230
|
+
if (!profileMetaInheritsDefault(join12(role.roleDir, "runtime", "profile.yaml"))) {
|
|
3034
3231
|
details.push("runtime/profile.yaml missing inherited default config metadata");
|
|
3035
3232
|
}
|
|
3036
3233
|
const registry = safeReadText(registryPath(ctx.homeDir));
|
|
@@ -3051,21 +3248,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3051
3248
|
if (!role) {
|
|
3052
3249
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
|
|
3053
3250
|
}
|
|
3054
|
-
const templateRoleDir =
|
|
3055
|
-
writeIfDifferent(
|
|
3056
|
-
writeIfDifferent(
|
|
3057
|
-
writeIfDifferent(
|
|
3058
|
-
copyMissingRecursive(
|
|
3059
|
-
copyMissingRecursive(
|
|
3060
|
-
copyMissingRecursive(
|
|
3061
|
-
const promptSource =
|
|
3062
|
-
const promptTarget =
|
|
3063
|
-
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)) {
|
|
3064
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);
|
|
3065
3262
|
writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
|
|
3066
3263
|
}
|
|
3067
3264
|
upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
|
|
3068
|
-
const profileMetaUpdated = upsertInheritedProfileMeta(
|
|
3265
|
+
const profileMetaUpdated = upsertInheritedProfileMeta(join12(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
|
|
3069
3266
|
if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
|
|
3070
3267
|
const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
|
|
3071
3268
|
if (registryUpdated) details.push(`updated ${registryUpdated}`);
|
|
@@ -3079,6 +3276,103 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3079
3276
|
};
|
|
3080
3277
|
}
|
|
3081
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
|
+
},
|
|
3082
3376
|
{
|
|
3083
3377
|
id: "systemd.sentinel",
|
|
3084
3378
|
title: "Hermes systemd/sentinel units enabled + active",
|
|
@@ -3119,9 +3413,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3119
3413
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
|
|
3120
3414
|
}
|
|
3121
3415
|
for (const role of roles) {
|
|
3122
|
-
const sysDir =
|
|
3416
|
+
const sysDir = join12(ctx.homeDir, ".config", "systemd", "user");
|
|
3123
3417
|
const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`];
|
|
3124
|
-
const allUnitsPresent = units.every((unit) =>
|
|
3418
|
+
const allUnitsPresent = units.every((unit) => existsSync9(join12(sysDir, unit)));
|
|
3125
3419
|
if (allUnitsPresent) {
|
|
3126
3420
|
if (ctx.dryRun) {
|
|
3127
3421
|
details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
|
|
@@ -3133,12 +3427,12 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3133
3427
|
}
|
|
3134
3428
|
continue;
|
|
3135
3429
|
}
|
|
3136
|
-
for (const script of [
|
|
3137
|
-
if (!script || !
|
|
3430
|
+
for (const script of [join12(role.roleDir, ".scripts", "70-systemd.sh")]) {
|
|
3431
|
+
if (!script || !existsSync9(script)) continue;
|
|
3138
3432
|
if (ctx.dryRun) {
|
|
3139
3433
|
details.push(`would run: bash ${script}`);
|
|
3140
3434
|
} else {
|
|
3141
|
-
const result =
|
|
3435
|
+
const result = spawnSync6("bash", [script], { cwd: role.roleDir, encoding: "utf8" });
|
|
3142
3436
|
if (result.status !== 0) details.push(`script failed: ${script}: ${result.stderr.trim() || result.stdout.trim()}`);
|
|
3143
3437
|
}
|
|
3144
3438
|
}
|
|
@@ -3258,7 +3552,7 @@ var server = new McpServer({
|
|
|
3258
3552
|
var TICKET_PROVIDER_SCHEMA = z.enum(["plane", "trello"]);
|
|
3259
3553
|
function resolveTargetDir(targetDir) {
|
|
3260
3554
|
const dir = resolve3(targetDir ?? process.cwd());
|
|
3261
|
-
if (!
|
|
3555
|
+
if (!existsSync10(dir)) {
|
|
3262
3556
|
throw new Error(`Target directory does not exist: ${dir}`);
|
|
3263
3557
|
}
|
|
3264
3558
|
if (!statSync2(dir).isDirectory()) {
|
|
@@ -3269,7 +3563,7 @@ function resolveTargetDir(targetDir) {
|
|
|
3269
3563
|
function resolvePjanglerRoot3() {
|
|
3270
3564
|
let dir = dirname8(fileURLToPath5(import.meta.url));
|
|
3271
3565
|
while (dir !== dirname8(dir)) {
|
|
3272
|
-
if (
|
|
3566
|
+
if (existsSync10(join13(dir, "package.json")) && existsSync10(join13(dir, "templates", "commonproject", "copier.yml"))) {
|
|
3273
3567
|
return dir;
|
|
3274
3568
|
}
|
|
3275
3569
|
dir = dirname8(dir);
|
|
@@ -3471,8 +3765,8 @@ server.registerTool(
|
|
|
3471
3765
|
const pjanglerRoot = resolvePjanglerRoot3();
|
|
3472
3766
|
const projectSlug = input.projectSlug ?? slugify(input.projectName);
|
|
3473
3767
|
const parentDir = resolve3(input.parentDir ?? process.cwd());
|
|
3474
|
-
if (!
|
|
3475
|
-
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));
|
|
3476
3770
|
const overwrite = input.overwrite ?? input.force ?? false;
|
|
3477
3771
|
const dryRun = input.dryRun ?? true;
|
|
3478
3772
|
const local = input.local ?? true;
|
|
@@ -3482,7 +3776,7 @@ server.registerTool(
|
|
|
3482
3776
|
if (!skipPlane && ticketProvider === "plane" && !boardId) {
|
|
3483
3777
|
throw new Error("boardId or planeProjectId is required when skipPlane=false for Plane; keep skipPlane=true for safe local bootstrap");
|
|
3484
3778
|
}
|
|
3485
|
-
if (!dryRun &&
|
|
3779
|
+
if (!dryRun && existsSync10(targetDir) && !overwrite) throw new Error(`Target already exists: ${targetDir} (set force/overwrite=true to re-render)`);
|
|
3486
3780
|
const plan = planProjectInit({
|
|
3487
3781
|
name: input.projectName,
|
|
3488
3782
|
description: input.projectDescription,
|