@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/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { spawnSync as
|
|
5
|
-
import { existsSync as
|
|
6
|
-
import { basename as basename4, join as
|
|
4
|
+
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
5
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7, statSync as statSync2 } from "node:fs";
|
|
6
|
+
import { basename as basename4, join as join13, resolve as resolve3 } from "node:path";
|
|
7
7
|
import { Command as Command3 } from "commander";
|
|
8
8
|
|
|
9
9
|
// src/commands/hermes/types.ts
|
|
@@ -82,6 +82,8 @@ function renderHostConfig() {
|
|
|
82
82
|
const hermesRepo = join2(home, "code", "hermes-agent");
|
|
83
83
|
const scaffoldDir = join2(home, "code", "hermes-agent-template", "runtime-scaffold");
|
|
84
84
|
const skillsDir = join2(home, ".agents", "skills");
|
|
85
|
+
const pmExternalSkillGlobalDir = join2(home, "code", "skillex", "skill-sets", "global", ".system");
|
|
86
|
+
const pmExternalSkillBmadDir = join2(home, "code", "skillex", "packs", "bmad", "6.10.2");
|
|
85
87
|
return `# hermes-agent-template \u2014 host configuration
|
|
86
88
|
# Bootstrapped by \`pjangler config bootstrap\` for $HOME=${home} (platform=${platform()}).
|
|
87
89
|
#
|
|
@@ -97,9 +99,14 @@ home = "~/.hermes"
|
|
|
97
99
|
hermes_bin = "${hermesBin}"
|
|
98
100
|
hermes_repo = "${hermesRepo}"
|
|
99
101
|
runtime_scaffold_dir = "${scaffoldDir}"
|
|
102
|
+
# Shared fleet source-of-truth env file + fleet registry. ~ is expanded.
|
|
100
103
|
fleet_env = "~/.hermes/fleet.env"
|
|
101
104
|
registry_file = "~/.hermes/agents-registry.yaml"
|
|
102
105
|
canonical_skills_dir = "${skillsDir}"
|
|
106
|
+
pm_external_skill_dirs = [
|
|
107
|
+
"${pmExternalSkillGlobalDir}",
|
|
108
|
+
"${pmExternalSkillBmadDir}",
|
|
109
|
+
]
|
|
103
110
|
symlinked_runtime_skills = []
|
|
104
111
|
|
|
105
112
|
[github]
|
|
@@ -909,10 +916,100 @@ var RunCopierTemplate = class extends Command {
|
|
|
909
916
|
}
|
|
910
917
|
};
|
|
911
918
|
|
|
912
|
-
// src/commands/hermes/
|
|
919
|
+
// src/commands/hermes/UntrackHermesRuntimes.ts
|
|
920
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync3, readdirSync } from "fs";
|
|
921
|
+
import { join as join6 } from "path";
|
|
913
922
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
914
|
-
|
|
915
|
-
|
|
923
|
+
var UntrackHermesRuntimes = class extends Command {
|
|
924
|
+
async invoke() {
|
|
925
|
+
const targetDir = this.context.targetDir;
|
|
926
|
+
const rolesDir = join6(targetDir, "agents", "hermes");
|
|
927
|
+
if (!existsSync4(rolesDir)) {
|
|
928
|
+
return {
|
|
929
|
+
success: true,
|
|
930
|
+
message: "No Hermes agents found (no agents/hermes directory)."
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
const roles = readdirSync(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
934
|
+
if (roles.length === 0) {
|
|
935
|
+
return {
|
|
936
|
+
success: true,
|
|
937
|
+
message: "No Hermes agents found."
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
let modifiedAny = false;
|
|
941
|
+
const details = [];
|
|
942
|
+
for (const role of roles) {
|
|
943
|
+
const roleDir = join6("agents", "hermes", role);
|
|
944
|
+
const runtimePath = join6(roleDir, "runtime");
|
|
945
|
+
const gitignorePath = join6(roleDir, ".gitignore");
|
|
946
|
+
let isTracked = false;
|
|
947
|
+
const lsResult = spawnSync2("git", ["ls-files", "--stage", runtimePath], {
|
|
948
|
+
cwd: targetDir,
|
|
949
|
+
encoding: "utf8"
|
|
950
|
+
});
|
|
951
|
+
if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
|
|
952
|
+
isTracked = true;
|
|
953
|
+
}
|
|
954
|
+
let isIgnored = false;
|
|
955
|
+
const fullGitignorePath = join6(targetDir, gitignorePath);
|
|
956
|
+
if (existsSync4(fullGitignorePath)) {
|
|
957
|
+
const content = readFileSync2(fullGitignorePath, "utf8");
|
|
958
|
+
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
959
|
+
isIgnored = lines.includes("runtime/") || lines.includes("runtime");
|
|
960
|
+
}
|
|
961
|
+
if (isTracked || !isIgnored) {
|
|
962
|
+
modifiedAny = true;
|
|
963
|
+
if (isTracked) {
|
|
964
|
+
details.push(`untrack agents/hermes/${role}/runtime`);
|
|
965
|
+
if (!this.context.dryRun) {
|
|
966
|
+
const rmResult = spawnSync2("git", ["rm", "--cached", "-r", runtimePath], {
|
|
967
|
+
cwd: targetDir,
|
|
968
|
+
encoding: "utf8"
|
|
969
|
+
});
|
|
970
|
+
if (rmResult.status !== 0) {
|
|
971
|
+
return {
|
|
972
|
+
success: false,
|
|
973
|
+
message: `Failed to untrack agents/hermes/${role}/runtime: ${rmResult.stderr}`
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
if (!isIgnored) {
|
|
979
|
+
details.push(`ignore runtime/ in agents/hermes/${role}/.gitignore`);
|
|
980
|
+
if (!this.context.dryRun) {
|
|
981
|
+
let content = "";
|
|
982
|
+
if (existsSync4(fullGitignorePath)) {
|
|
983
|
+
content = readFileSync2(fullGitignorePath, "utf8");
|
|
984
|
+
}
|
|
985
|
+
if (content && !content.endsWith("\n")) {
|
|
986
|
+
content += "\n";
|
|
987
|
+
}
|
|
988
|
+
content += "runtime/\n";
|
|
989
|
+
writeFileSync3(fullGitignorePath, content, "utf8");
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
if (!modifiedAny) {
|
|
995
|
+
return {
|
|
996
|
+
success: true,
|
|
997
|
+
message: "\u2705 All Hermes agent runtimes are already untracked and gitignored."
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
const actionText = this.context.dryRun ? "Would make" : "Made";
|
|
1001
|
+
return {
|
|
1002
|
+
success: true,
|
|
1003
|
+
message: `${actionText} Hermes agent runtimes untracked and gitignored:
|
|
1004
|
+
${details.map((d) => ` - ${d}`).join("\n")}`
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
// src/commands/hermes/WireTelegram.ts
|
|
1010
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
1011
|
+
import { join as join7 } from "node:path";
|
|
1012
|
+
import { existsSync as existsSync5, unlinkSync } from "node:fs";
|
|
916
1013
|
import * as p3 from "@clack/prompts";
|
|
917
1014
|
var WireTelegram = class extends Command {
|
|
918
1015
|
async invoke() {
|
|
@@ -934,7 +1031,7 @@ var WireTelegram = class extends Command {
|
|
|
934
1031
|
let token = process.env.TELEGRAM_BOT_TOKEN;
|
|
935
1032
|
let source = token ? "env" : null;
|
|
936
1033
|
if (!token) {
|
|
937
|
-
const tryOp =
|
|
1034
|
+
const tryOp = spawnSync3("op", ["read", vaultRef], { encoding: "utf8" });
|
|
938
1035
|
if (tryOp.status === 0) {
|
|
939
1036
|
token = tryOp.stdout.trim();
|
|
940
1037
|
source = "op";
|
|
@@ -973,7 +1070,7 @@ var WireTelegram = class extends Command {
|
|
|
973
1070
|
initialValue: true
|
|
974
1071
|
});
|
|
975
1072
|
if (!p3.isCancel(persist) && persist) {
|
|
976
|
-
const create =
|
|
1073
|
+
const create = spawnSync3(
|
|
977
1074
|
"op",
|
|
978
1075
|
[
|
|
979
1076
|
"item",
|
|
@@ -1000,18 +1097,18 @@ var WireTelegram = class extends Command {
|
|
|
1000
1097
|
if (p3.isCancel(allowedAnswer)) {
|
|
1001
1098
|
return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
|
|
1002
1099
|
}
|
|
1003
|
-
const script =
|
|
1004
|
-
if (!
|
|
1100
|
+
const script = join7(roleDir, ".scripts", "30-telegram.sh");
|
|
1101
|
+
if (!existsSync5(script)) {
|
|
1005
1102
|
return {
|
|
1006
1103
|
success: false,
|
|
1007
1104
|
message: `\u2717 ${script} not found. Did copier finish? Re-run with --skip-runtime-repo=0 if you skipped it.`
|
|
1008
1105
|
};
|
|
1009
1106
|
}
|
|
1010
|
-
const marker =
|
|
1011
|
-
if (
|
|
1107
|
+
const marker = join7(roleDir, ".scripts", ".done-30-telegram");
|
|
1108
|
+
if (existsSync5(marker)) unlinkSync(marker);
|
|
1012
1109
|
const spinner4 = p3.spinner();
|
|
1013
1110
|
spinner4.start("Verifying token + wiring profile");
|
|
1014
|
-
const result =
|
|
1111
|
+
const result = spawnSync3("bash", [script], {
|
|
1015
1112
|
stdio: "inherit",
|
|
1016
1113
|
env: {
|
|
1017
1114
|
...process.env,
|
|
@@ -1034,9 +1131,9 @@ function cap(s) {
|
|
|
1034
1131
|
}
|
|
1035
1132
|
|
|
1036
1133
|
// src/commands/hermes/WireEmail.ts
|
|
1037
|
-
import { spawnSync as
|
|
1038
|
-
import { join as
|
|
1039
|
-
import { existsSync as
|
|
1134
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
1135
|
+
import { join as join8 } from "node:path";
|
|
1136
|
+
import { existsSync as existsSync6, unlinkSync as unlinkSync2 } from "node:fs";
|
|
1040
1137
|
import * as p4 from "@clack/prompts";
|
|
1041
1138
|
var WireEmail = class extends Command {
|
|
1042
1139
|
async invoke() {
|
|
@@ -1051,13 +1148,13 @@ var WireEmail = class extends Command {
|
|
|
1051
1148
|
if (!targetRepo || !role || !roleDir) {
|
|
1052
1149
|
return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
|
|
1053
1150
|
}
|
|
1054
|
-
const script =
|
|
1055
|
-
if (!
|
|
1151
|
+
const script = join8(roleDir, ".scripts", "50-email.sh");
|
|
1152
|
+
if (!existsSync6(script)) {
|
|
1056
1153
|
return { success: false, message: `\u2717 ${script} not found` };
|
|
1057
1154
|
}
|
|
1058
1155
|
let token = process.env.CF_EMAIL_ROUTING_TOKEN;
|
|
1059
1156
|
if (!token) {
|
|
1060
|
-
const tryOp =
|
|
1157
|
+
const tryOp = spawnSync4(
|
|
1061
1158
|
"op",
|
|
1062
1159
|
["read", "op://DeLoSecrets/Cloudflare-EmailRouting/token"],
|
|
1063
1160
|
{ encoding: "utf8" }
|
|
@@ -1097,7 +1194,7 @@ var WireEmail = class extends Command {
|
|
|
1097
1194
|
initialValue: true
|
|
1098
1195
|
});
|
|
1099
1196
|
if (!p4.isCancel(persist) && persist) {
|
|
1100
|
-
const create =
|
|
1197
|
+
const create = spawnSync4(
|
|
1101
1198
|
"op",
|
|
1102
1199
|
[
|
|
1103
1200
|
"item",
|
|
@@ -1114,11 +1211,11 @@ var WireEmail = class extends Command {
|
|
|
1114
1211
|
}
|
|
1115
1212
|
}
|
|
1116
1213
|
}
|
|
1117
|
-
const marker =
|
|
1118
|
-
if (
|
|
1214
|
+
const marker = join8(roleDir, ".scripts", ".done-50-email");
|
|
1215
|
+
if (existsSync6(marker)) unlinkSync2(marker);
|
|
1119
1216
|
const spinner4 = p4.spinner();
|
|
1120
1217
|
spinner4.start("Creating Cloudflare Email Routing rule");
|
|
1121
|
-
const result =
|
|
1218
|
+
const result = spawnSync4("bash", [script], {
|
|
1122
1219
|
stdio: "inherit",
|
|
1123
1220
|
env: { ...process.env, SKIP_EMAIL: "0", CF_EMAIL_ROUTING_TOKEN: token },
|
|
1124
1221
|
cwd: roleDir
|
|
@@ -1178,7 +1275,7 @@ var PrintHermesSummary = class extends Command {
|
|
|
1178
1275
|
var HermesAgentRecipe = class extends Recipe {
|
|
1179
1276
|
constructor(context) {
|
|
1180
1277
|
super(context);
|
|
1181
|
-
this.addIngredient(EnsureTemplateConfig).addIngredient(PromptForAgentConfig).addIngredient(RunCopierTemplate).addIngredient(WireTelegram).addIngredient(WireEmail).addIngredient(PrintHermesSummary);
|
|
1278
|
+
this.addIngredient(EnsureTemplateConfig).addIngredient(PromptForAgentConfig).addIngredient(RunCopierTemplate).addIngredient(UntrackHermesRuntimes).addIngredient(WireTelegram).addIngredient(WireEmail).addIngredient(PrintHermesSummary);
|
|
1182
1279
|
}
|
|
1183
1280
|
// Override execute() to suppress the base class's per-command logging since
|
|
1184
1281
|
// our commands already render their own UI via @clack/prompts.
|
|
@@ -1202,33 +1299,33 @@ var HermesAgentRecipe = class extends Recipe {
|
|
|
1202
1299
|
|
|
1203
1300
|
// src/commands/AgentHooksCommands.ts
|
|
1204
1301
|
import { homedir as homedir4 } from "node:os";
|
|
1205
|
-
import { join as
|
|
1206
|
-
import { existsSync as
|
|
1302
|
+
import { join as join10, dirname as dirname5 } from "node:path";
|
|
1303
|
+
import { existsSync as existsSync8, cpSync, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1207
1304
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1208
1305
|
|
|
1209
1306
|
// src/project/index.ts
|
|
1210
|
-
import { spawnSync as
|
|
1211
|
-
import { existsSync as
|
|
1307
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
1308
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync3, renameSync, statSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1212
1309
|
import { homedir as homedir3 } from "node:os";
|
|
1213
|
-
import { basename as basename2, dirname as dirname4, join as
|
|
1310
|
+
import { basename as basename2, delimiter, dirname as dirname4, join as join9, resolve } from "node:path";
|
|
1214
1311
|
import YAML from "yaml";
|
|
1215
1312
|
var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
|
|
1313
|
+
var PROJECT_SOURCE_SKILL_ROOTS_ENV = "PJ_SOURCE_SKILL_ROOTS";
|
|
1216
1314
|
var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
|
|
1217
|
-
var
|
|
1315
|
+
var DEFAULT_SOURCE_SKILL_ROOTS = [
|
|
1218
1316
|
"/home/delorenj/code/skillex/all-skills",
|
|
1219
|
-
"
|
|
1220
|
-
"
|
|
1221
|
-
join8(homedir3(), ".codex", "skills")
|
|
1317
|
+
join9(homedir3(), ".agents", "skills"),
|
|
1318
|
+
join9(homedir3(), ".codex", "skills")
|
|
1222
1319
|
];
|
|
1223
1320
|
function projectRegistryPath(env2 = process.env) {
|
|
1224
|
-
return expandHome(env2[PROJECT_REGISTRY_ENV] ||
|
|
1321
|
+
return expandHome(env2[PROJECT_REGISTRY_ENV] || join9(homedir3(), ".config", "pjangler", "projects.yaml"));
|
|
1225
1322
|
}
|
|
1226
1323
|
function emptyProjectRegistry() {
|
|
1227
1324
|
return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
|
|
1228
1325
|
}
|
|
1229
1326
|
function loadProjectRegistry(path = projectRegistryPath()) {
|
|
1230
|
-
if (!
|
|
1231
|
-
const raw = YAML.parse(
|
|
1327
|
+
if (!existsSync7(path)) return emptyProjectRegistry();
|
|
1328
|
+
const raw = YAML.parse(readFileSync3(path, "utf8"));
|
|
1232
1329
|
if (raw == null) return emptyProjectRegistry();
|
|
1233
1330
|
if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
|
|
1234
1331
|
const registry = raw;
|
|
@@ -1243,7 +1340,7 @@ function saveProjectRegistry(registry, path = projectRegistryPath()) {
|
|
|
1243
1340
|
validateProjectRegistry(registry);
|
|
1244
1341
|
mkdirSync4(dirname4(path), { recursive: true });
|
|
1245
1342
|
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
1246
|
-
|
|
1343
|
+
writeFileSync4(temp, YAML.stringify(registry, { lineWidth: 0 }), "utf8");
|
|
1247
1344
|
renameSync(temp, path);
|
|
1248
1345
|
}
|
|
1249
1346
|
function validateProjectRegistry(registry) {
|
|
@@ -1288,8 +1385,7 @@ function buildTicketProviderBlock(input) {
|
|
|
1288
1385
|
workspace: input.workspace ?? "",
|
|
1289
1386
|
identifier: input.identifier,
|
|
1290
1387
|
board_id: boardId,
|
|
1291
|
-
|
|
1292
|
-
state: "planned"
|
|
1388
|
+
state: boardId ? "linked" : "planned"
|
|
1293
1389
|
};
|
|
1294
1390
|
}
|
|
1295
1391
|
const workspace = input.workspace ?? "33god";
|
|
@@ -1298,8 +1394,16 @@ function buildTicketProviderBlock(input) {
|
|
|
1298
1394
|
workspace,
|
|
1299
1395
|
identifier: input.identifier,
|
|
1300
1396
|
board_id: boardId,
|
|
1301
|
-
|
|
1302
|
-
|
|
1397
|
+
state: boardId ? "linked" : "planned"
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
function defaultProjectAutomation() {
|
|
1401
|
+
return {
|
|
1402
|
+
reconcile: {
|
|
1403
|
+
enabled: false,
|
|
1404
|
+
grace_hours: 0,
|
|
1405
|
+
auto_review: true
|
|
1406
|
+
}
|
|
1303
1407
|
};
|
|
1304
1408
|
}
|
|
1305
1409
|
function slugifyProjectName(value) {
|
|
@@ -1318,7 +1422,7 @@ function resolveAgentHooksLayer(input, env2 = process.env) {
|
|
|
1318
1422
|
const override = env2.PJ_AGENT_HOOKS_LAYER;
|
|
1319
1423
|
if (override === "0" || override === "false") return false;
|
|
1320
1424
|
if (override === "1" || override === "true") return true;
|
|
1321
|
-
return !
|
|
1425
|
+
return !existsSync7(join9(homedir3(), ".agents", "hooks"));
|
|
1322
1426
|
}
|
|
1323
1427
|
function jsonStable(value) {
|
|
1324
1428
|
return JSON.stringify(value);
|
|
@@ -1333,18 +1437,31 @@ function defaultProjectTargetDir(name, cwd = process.cwd()) {
|
|
|
1333
1437
|
const compactName = name.replace(/[^A-Za-z0-9._-]/g, "") || slugifyProjectName(name);
|
|
1334
1438
|
return resolve(dirname4(resolve(cwd)), compactName);
|
|
1335
1439
|
}
|
|
1336
|
-
function
|
|
1440
|
+
function sourceSkillRoots(env2 = process.env) {
|
|
1441
|
+
const configuredRoots = (env2[PROJECT_SOURCE_SKILL_ROOTS_ENV] || "").split(delimiter).map((root) => root.trim()).filter(Boolean);
|
|
1442
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1443
|
+
const roots = [];
|
|
1444
|
+
for (const root of [...DEFAULT_SOURCE_SKILL_ROOTS, ...configuredRoots]) {
|
|
1445
|
+
const normalized = resolve(expandHome(root));
|
|
1446
|
+
if (seen.has(normalized)) continue;
|
|
1447
|
+
seen.add(normalized);
|
|
1448
|
+
roots.push(normalized);
|
|
1449
|
+
}
|
|
1450
|
+
return roots;
|
|
1451
|
+
}
|
|
1452
|
+
function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
|
|
1337
1453
|
if (!sourceSkill) return void 0;
|
|
1338
1454
|
const expanded = expandHome(sourceSkill);
|
|
1339
1455
|
const direct = resolve(expanded);
|
|
1340
|
-
if (
|
|
1456
|
+
if (existsSync7(direct)) return direct;
|
|
1341
1457
|
const name = basename2(sourceSkill);
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1458
|
+
const roots = sourceSkillRoots(env2);
|
|
1459
|
+
for (const root of roots) {
|
|
1460
|
+
const candidate = join9(root, name);
|
|
1461
|
+
if (existsSync7(candidate)) return candidate;
|
|
1345
1462
|
}
|
|
1346
|
-
const
|
|
1347
|
-
const hint =
|
|
1463
|
+
const searched = roots.length ? ` Searched roots: ${roots.join(", ")}.` : "";
|
|
1464
|
+
const hint = `${searched} Add project-specific roots with ${PROJECT_SOURCE_SKILL_ROOTS_ENV}.`;
|
|
1348
1465
|
throw new Error(`Source skill not found: ${sourceSkill}.${hint}`);
|
|
1349
1466
|
}
|
|
1350
1467
|
function planProjectInit(input) {
|
|
@@ -1384,10 +1501,10 @@ function planProjectInit(input) {
|
|
|
1384
1501
|
type: input.ticketProvider ?? "plane",
|
|
1385
1502
|
identifier,
|
|
1386
1503
|
boardId: input.boardId ?? input.planeProjectId,
|
|
1387
|
-
boardUrl: input.boardUrl,
|
|
1388
1504
|
workspace: input.boardWorkspace ?? input.planeWorkspace
|
|
1389
1505
|
}),
|
|
1390
1506
|
agents,
|
|
1507
|
+
automation: existing?.automation ?? defaultProjectAutomation(),
|
|
1391
1508
|
created_at: existing?.created_at ?? now,
|
|
1392
1509
|
updated_at: now
|
|
1393
1510
|
};
|
|
@@ -1415,7 +1532,6 @@ function planProjectInit(input) {
|
|
|
1415
1532
|
planeProjectId: project.ticket_provider.board_id ?? "",
|
|
1416
1533
|
ticketWorkspace: project.ticket_provider.workspace ?? "",
|
|
1417
1534
|
boardId: project.ticket_provider.board_id ?? "",
|
|
1418
|
-
boardUrl: project.ticket_provider.board_url ?? "",
|
|
1419
1535
|
projectIdentifier: identifier,
|
|
1420
1536
|
primaryLanguage: project.template.commonproject.primary_language,
|
|
1421
1537
|
agentHooksLayer: resolveAgentHooksLayer(input.agentHooksLayer),
|
|
@@ -1423,7 +1539,7 @@ function planProjectInit(input) {
|
|
|
1423
1539
|
}));
|
|
1424
1540
|
}
|
|
1425
1541
|
actions.push(
|
|
1426
|
-
{ kind: "project.write-manifest", path:
|
|
1542
|
+
{ kind: "project.write-manifest", path: join9(targetDir, ".project.json"), manifest },
|
|
1427
1543
|
{
|
|
1428
1544
|
kind: "ticket-provider.create-or-link",
|
|
1429
1545
|
enabled: live,
|
|
@@ -1464,7 +1580,7 @@ function executeProjectInitPlan(plan) {
|
|
|
1464
1580
|
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"
|
|
1465
1581
|
);
|
|
1466
1582
|
mkdirSync4(dirname4(action.targetDir), { recursive: true });
|
|
1467
|
-
const result =
|
|
1583
|
+
const result = spawnSync5(action.command[0], action.command.slice(1), { encoding: "utf8", cwd: action.cwd });
|
|
1468
1584
|
if (result.stdout?.trim()) logs.push(result.stdout.trim());
|
|
1469
1585
|
if (result.stderr?.trim()) logs.push(result.stderr.trim());
|
|
1470
1586
|
if (result.error) {
|
|
@@ -1476,7 +1592,7 @@ function executeProjectInitPlan(plan) {
|
|
|
1476
1592
|
}
|
|
1477
1593
|
if (result.status !== 0) {
|
|
1478
1594
|
errors.push(`copier exited with status ${result.status ?? "unknown"}`);
|
|
1479
|
-
if (
|
|
1595
|
+
if (existsSync7(action.targetDir)) changedFiles.push(action.targetDir);
|
|
1480
1596
|
break;
|
|
1481
1597
|
}
|
|
1482
1598
|
changedFiles.push(action.targetDir);
|
|
@@ -1484,9 +1600,9 @@ function executeProjectInitPlan(plan) {
|
|
|
1484
1600
|
mkdirSync4(dirname4(action.path), { recursive: true });
|
|
1485
1601
|
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
1486
1602
|
`;
|
|
1487
|
-
const current =
|
|
1603
|
+
const current = existsSync7(action.path) ? readFileSync3(action.path, "utf8") : void 0;
|
|
1488
1604
|
if (current !== next) {
|
|
1489
|
-
|
|
1605
|
+
writeFileSync4(action.path, next, "utf8");
|
|
1490
1606
|
changedFiles.push(action.path);
|
|
1491
1607
|
}
|
|
1492
1608
|
} else if (action.kind === "registry.upsert") {
|
|
@@ -1527,10 +1643,10 @@ function projectManifestFromRegistryProject(project) {
|
|
|
1527
1643
|
workspace: project.ticket_provider.workspace ?? "",
|
|
1528
1644
|
identifier: project.ticket_provider.identifier ?? "",
|
|
1529
1645
|
board_id: project.ticket_provider.board_id ?? "",
|
|
1530
|
-
board_url: project.ticket_provider.board_url ?? "",
|
|
1531
1646
|
state: project.ticket_provider.state
|
|
1532
1647
|
},
|
|
1533
|
-
agents
|
|
1648
|
+
agents,
|
|
1649
|
+
automation: project.automation ?? defaultProjectAutomation()
|
|
1534
1650
|
};
|
|
1535
1651
|
}
|
|
1536
1652
|
function formatProjectInitPlan(plan) {
|
|
@@ -1579,16 +1695,16 @@ function doctorProjectRegistry(registryPath2 = projectRegistryPath(), slug) {
|
|
|
1579
1695
|
const registry = loadProjectRegistry(registryPath2);
|
|
1580
1696
|
const projects = slug ? [[slug, getProject(registry, slug)]] : Object.entries(registry.projects);
|
|
1581
1697
|
for (const [projectSlug, project] of projects) {
|
|
1582
|
-
if (!
|
|
1698
|
+
if (!existsSync7(project.repo_path)) {
|
|
1583
1699
|
issues.push({ level: "warn", slug: projectSlug, message: `repo_path does not exist: ${project.repo_path}` });
|
|
1584
1700
|
} else if (!statSync(project.repo_path).isDirectory()) {
|
|
1585
1701
|
issues.push({ level: "error", slug: projectSlug, message: `repo_path is not a directory: ${project.repo_path}` });
|
|
1586
1702
|
} else {
|
|
1587
|
-
const manifestPath =
|
|
1588
|
-
if (!
|
|
1703
|
+
const manifestPath = join9(project.repo_path, ".project.json");
|
|
1704
|
+
if (!existsSync7(manifestPath)) issues.push({ level: "warn", slug: projectSlug, message: ".project.json is missing" });
|
|
1589
1705
|
}
|
|
1590
1706
|
for (const artifact of project.source_artifacts) {
|
|
1591
|
-
if (artifact.path && !
|
|
1707
|
+
if (artifact.path && !existsSync7(artifact.path)) {
|
|
1592
1708
|
issues.push({ level: "warn", slug: projectSlug, message: `source artifact missing: ${artifact.path}` });
|
|
1593
1709
|
}
|
|
1594
1710
|
}
|
|
@@ -1601,7 +1717,7 @@ function doctorProjectRegistry(registryPath2 = projectRegistryPath(), slug) {
|
|
|
1601
1717
|
};
|
|
1602
1718
|
}
|
|
1603
1719
|
function buildCommonProjectCopierAction(input) {
|
|
1604
|
-
const templateDir =
|
|
1720
|
+
const templateDir = join9(input.pjanglerRoot, "templates", "commonproject");
|
|
1605
1721
|
const data = {
|
|
1606
1722
|
project_name: input.projectName,
|
|
1607
1723
|
project_description: input.projectDescription ?? "",
|
|
@@ -1611,7 +1727,6 @@ function buildCommonProjectCopierAction(input) {
|
|
|
1611
1727
|
plane_project_id: input.planeProjectId ?? "",
|
|
1612
1728
|
ticket_workspace: input.ticketWorkspace ?? input.planeWorkspace,
|
|
1613
1729
|
board_id: input.boardId ?? input.planeProjectId ?? "",
|
|
1614
|
-
board_url: input.boardUrl ?? "",
|
|
1615
1730
|
project_identifier: input.projectIdentifier,
|
|
1616
1731
|
primary_language: input.primaryLanguage,
|
|
1617
1732
|
agent_hooks_layer: input.agentHooksLayer ?? true ? "true" : "false"
|
|
@@ -1631,7 +1746,7 @@ function buildCommonProjectCopierAction(input) {
|
|
|
1631
1746
|
function resolvePjanglerRoot() {
|
|
1632
1747
|
let dir = dirname4(new URL(import.meta.url).pathname);
|
|
1633
1748
|
while (dir !== dirname4(dir)) {
|
|
1634
|
-
if (
|
|
1749
|
+
if (existsSync7(join9(dir, "package.json")) && existsSync7(join9(dir, "templates", "commonproject", "copier.yml"))) return dir;
|
|
1635
1750
|
dir = dirname4(dir);
|
|
1636
1751
|
}
|
|
1637
1752
|
return resolve(process.cwd());
|
|
@@ -1663,7 +1778,7 @@ function validateProjectRecord(project, key) {
|
|
|
1663
1778
|
}
|
|
1664
1779
|
function expandHome(path) {
|
|
1665
1780
|
if (path === "~") return homedir3();
|
|
1666
|
-
if (path.startsWith("~/")) return
|
|
1781
|
+
if (path.startsWith("~/")) return join9(homedir3(), path.slice(2));
|
|
1667
1782
|
return path;
|
|
1668
1783
|
}
|
|
1669
1784
|
function isRecord(value) {
|
|
@@ -1680,16 +1795,16 @@ function resolveTemplateRoot() {
|
|
|
1680
1795
|
try {
|
|
1681
1796
|
let dir = dirname5(fileURLToPath2(import.meta.url));
|
|
1682
1797
|
for (let i = 0; i < 8; i++) {
|
|
1683
|
-
candidates.push(
|
|
1798
|
+
candidates.push(join10(dir, "templates", "commonproject", "template"));
|
|
1684
1799
|
const parent = dirname5(dir);
|
|
1685
1800
|
if (parent === dir) break;
|
|
1686
1801
|
dir = parent;
|
|
1687
1802
|
}
|
|
1688
1803
|
} catch {
|
|
1689
1804
|
}
|
|
1690
|
-
candidates.push(
|
|
1805
|
+
candidates.push(join10(homedir4(), "code", "pjangler", "templates", "commonproject", "template"));
|
|
1691
1806
|
for (const c of candidates) {
|
|
1692
|
-
if (
|
|
1807
|
+
if (existsSync8(join10(c, ".agents", "hooks", "hooks.master.json"))) return c;
|
|
1693
1808
|
}
|
|
1694
1809
|
throw new Error(
|
|
1695
1810
|
"Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
|
|
@@ -1716,10 +1831,10 @@ var CopyAgentHooksTree = class extends Command {
|
|
|
1716
1831
|
const created = [];
|
|
1717
1832
|
const skipped = [];
|
|
1718
1833
|
for (const { rel, dir } of items) {
|
|
1719
|
-
const src =
|
|
1720
|
-
const dest =
|
|
1721
|
-
if (!
|
|
1722
|
-
if (
|
|
1834
|
+
const src = join10(templateRoot, rel);
|
|
1835
|
+
const dest = join10(this.context.targetDir, rel);
|
|
1836
|
+
if (!existsSync8(src)) continue;
|
|
1837
|
+
if (existsSync8(dest) && !this.context.force) {
|
|
1723
1838
|
skipped.push(rel);
|
|
1724
1839
|
continue;
|
|
1725
1840
|
}
|
|
@@ -1745,14 +1860,14 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
|
1745
1860
|
if (!resolveAgentHooksLayer()) {
|
|
1746
1861
|
return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
|
|
1747
1862
|
}
|
|
1748
|
-
const misePath =
|
|
1749
|
-
if (!
|
|
1863
|
+
const misePath = join10(this.context.targetDir, "mise.toml");
|
|
1864
|
+
if (!existsSync8(misePath)) {
|
|
1750
1865
|
return {
|
|
1751
1866
|
success: false,
|
|
1752
1867
|
message: "\u26A0\uFE0F No mise.toml found \u2014 run `pjangler init mise` first, then re-run."
|
|
1753
1868
|
};
|
|
1754
1869
|
}
|
|
1755
|
-
let content =
|
|
1870
|
+
let content = readFileSync4(misePath, "utf8");
|
|
1756
1871
|
if (content.includes(_WireMiseAgentHooks.MARKER)) {
|
|
1757
1872
|
return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
|
|
1758
1873
|
}
|
|
@@ -1828,7 +1943,7 @@ ${leaveBlock}`);
|
|
|
1828
1943
|
""
|
|
1829
1944
|
].join("\n");
|
|
1830
1945
|
content = content.replace(/\n*$/, "\n") + appended;
|
|
1831
|
-
if (!this.context.dryRun)
|
|
1946
|
+
if (!this.context.dryRun) writeFileSync5(misePath, content);
|
|
1832
1947
|
if (wiredHooks) {
|
|
1833
1948
|
return { success: true, message: this.formatMessage("\u2705 Wired mise.toml ([hooks] enter/leave + tasks)") };
|
|
1834
1949
|
}
|
|
@@ -2001,11 +2116,11 @@ function createRecipe(name, context) {
|
|
|
2001
2116
|
import { cancel as cancel2, multiselect, text as text2, isCancel as isCancel5 } from "@clack/prompts";
|
|
2002
2117
|
|
|
2003
2118
|
// src/parity/index.ts
|
|
2004
|
-
import { existsSync as
|
|
2005
|
-
import { basename as basename3, dirname as dirname6, join as
|
|
2119
|
+
import { existsSync as existsSync9, lstatSync, mkdirSync as mkdirSync6, readFileSync as readFileSync5, readlinkSync, readdirSync as readdirSync2, renameSync as renameSync2, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync6, chmodSync as chmodSync2, copyFileSync } from "node:fs";
|
|
2120
|
+
import { basename as basename3, dirname as dirname6, join as join11, relative, resolve as resolve2 } from "node:path";
|
|
2006
2121
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
2007
2122
|
import { homedir as homedir5 } from "node:os";
|
|
2008
|
-
import { spawnSync as
|
|
2123
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
2009
2124
|
var LINK_AGENTFILES_BLOCK = `# This block will handle the linking of
|
|
2010
2125
|
# agent files to the main AGENTS.md file.
|
|
2011
2126
|
#
|
|
@@ -2076,7 +2191,7 @@ run = "{{config_root}}/.mise/scripts/versioning.sh sync"
|
|
|
2076
2191
|
function resolvePjanglerRoot2() {
|
|
2077
2192
|
let dir = dirname6(fileURLToPath3(import.meta.url));
|
|
2078
2193
|
while (dir !== dirname6(dir)) {
|
|
2079
|
-
if (
|
|
2194
|
+
if (existsSync9(join11(dir, "package.json")) && existsSync9(join11(dir, "templates", "commonproject", "copier.yml"))) {
|
|
2080
2195
|
return dir;
|
|
2081
2196
|
}
|
|
2082
2197
|
dir = dirname6(dir);
|
|
@@ -2087,17 +2202,17 @@ function normalizeNewlines(value) {
|
|
|
2087
2202
|
return value.replace(/\r\n/g, "\n");
|
|
2088
2203
|
}
|
|
2089
2204
|
function readText(path) {
|
|
2090
|
-
return normalizeNewlines(
|
|
2205
|
+
return normalizeNewlines(readFileSync5(path, "utf8"));
|
|
2091
2206
|
}
|
|
2092
2207
|
function safeReadText(path) {
|
|
2093
|
-
return
|
|
2208
|
+
return existsSync9(path) ? readText(path) : null;
|
|
2094
2209
|
}
|
|
2095
2210
|
function ensureParent(path) {
|
|
2096
2211
|
mkdirSync6(dirname6(path), { recursive: true });
|
|
2097
2212
|
}
|
|
2098
2213
|
function writeText(path, content) {
|
|
2099
2214
|
ensureParent(path);
|
|
2100
|
-
|
|
2215
|
+
writeFileSync6(path, content);
|
|
2101
2216
|
}
|
|
2102
2217
|
function tryParseJson(text3) {
|
|
2103
2218
|
if (!text3) return null;
|
|
@@ -2114,7 +2229,7 @@ function titleCaseSlug(slug) {
|
|
|
2114
2229
|
return slug.split(/[-_]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
2115
2230
|
}
|
|
2116
2231
|
function readSymlinkTarget(path) {
|
|
2117
|
-
if (!
|
|
2232
|
+
if (!existsSync9(path)) return null;
|
|
2118
2233
|
try {
|
|
2119
2234
|
return readlinkSync(path);
|
|
2120
2235
|
} catch {
|
|
@@ -2122,7 +2237,7 @@ function readSymlinkTarget(path) {
|
|
|
2122
2237
|
}
|
|
2123
2238
|
}
|
|
2124
2239
|
function ensureSymlink(path, target, dryRun) {
|
|
2125
|
-
if (
|
|
2240
|
+
if (existsSync9(path)) {
|
|
2126
2241
|
const stat = lstatSync(path);
|
|
2127
2242
|
if (stat.isSymbolicLink()) {
|
|
2128
2243
|
const current = readSymlinkTarget(path);
|
|
@@ -2139,11 +2254,11 @@ function ensureSymlink(path, target, dryRun) {
|
|
|
2139
2254
|
return { changed: true };
|
|
2140
2255
|
}
|
|
2141
2256
|
function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
2142
|
-
const agentsPath =
|
|
2143
|
-
if (
|
|
2257
|
+
const agentsPath = join11(repoRoot, "AGENTS.md");
|
|
2258
|
+
if (existsSync9(agentsPath)) return { changedFiles: [], details: [] };
|
|
2144
2259
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2145
|
-
const source =
|
|
2146
|
-
if (!
|
|
2260
|
+
const source = join11(repoRoot, file);
|
|
2261
|
+
if (!existsSync9(source)) continue;
|
|
2147
2262
|
const stat = lstatSync(source);
|
|
2148
2263
|
if (stat.isSymbolicLink()) continue;
|
|
2149
2264
|
if (stat.isFile()) {
|
|
@@ -2152,8 +2267,8 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
|
2152
2267
|
}
|
|
2153
2268
|
return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
|
|
2154
2269
|
}
|
|
2155
|
-
const readmePath =
|
|
2156
|
-
if (
|
|
2270
|
+
const readmePath = join11(repoRoot, "README.md");
|
|
2271
|
+
if (existsSync9(readmePath)) {
|
|
2157
2272
|
const stat = lstatSync(readmePath);
|
|
2158
2273
|
if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
|
|
2159
2274
|
if (!dryRun) copyFileSync(readmePath, agentsPath);
|
|
@@ -2192,12 +2307,12 @@ function yamlGet(text3, keyPath) {
|
|
|
2192
2307
|
return "";
|
|
2193
2308
|
}
|
|
2194
2309
|
function discoverRoles(repoRoot) {
|
|
2195
|
-
const rolesDir =
|
|
2196
|
-
if (!
|
|
2197
|
-
return
|
|
2198
|
-
const roleDir =
|
|
2199
|
-
const roleYamlPath =
|
|
2200
|
-
if (!
|
|
2310
|
+
const rolesDir = join11(repoRoot, "agents", "hermes");
|
|
2311
|
+
if (!existsSync9(rolesDir)) return [];
|
|
2312
|
+
return readdirSync2(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
|
|
2313
|
+
const roleDir = join11(rolesDir, entry.name);
|
|
2314
|
+
const roleYamlPath = join11(roleDir, "role.yaml");
|
|
2315
|
+
if (!existsSync9(roleYamlPath)) return null;
|
|
2201
2316
|
const text3 = readText(roleYamlPath);
|
|
2202
2317
|
const runtimeRepoRaw = yamlGet(text3, "runtime.github_repo");
|
|
2203
2318
|
return {
|
|
@@ -2215,16 +2330,20 @@ function discoverRoles(repoRoot) {
|
|
|
2215
2330
|
planeWorkspace: yamlGet(text3, "ticket_provider.workspace") || yamlGet(text3, "plane.workspace"),
|
|
2216
2331
|
ticketProviderName: yamlGet(text3, "ticket_provider.name"),
|
|
2217
2332
|
ticketProviderBoardId: yamlGet(text3, "ticket_provider.board_id"),
|
|
2218
|
-
|
|
2219
|
-
|
|
2333
|
+
ticketProviderIdentifier: yamlGet(text3, "plane.identifier"),
|
|
2334
|
+
legacyReconcileEnabled: yamlGet(text3, "reconcile.enabled"),
|
|
2335
|
+
legacyReconcileGraceHours: yamlGet(text3, "reconcile.grace_hours"),
|
|
2336
|
+
legacyReconcileAutoReview: yamlGet(text3, "reconcile.auto_review"),
|
|
2337
|
+
legacyScrumGraceHours: yamlGet(text3, "scrum_master.grace_hours"),
|
|
2338
|
+
legacyScrumAutoReview: yamlGet(text3, "scrum_master.auto_review")
|
|
2220
2339
|
};
|
|
2221
2340
|
}).filter((value) => Boolean(value));
|
|
2222
2341
|
}
|
|
2223
2342
|
function registryPath(homeDir) {
|
|
2224
|
-
return
|
|
2343
|
+
return join11(homeDir, ".hermes", "agents-registry.yaml");
|
|
2225
2344
|
}
|
|
2226
2345
|
function systemctlUser(args) {
|
|
2227
|
-
const result =
|
|
2346
|
+
const result = spawnSync6("systemctl", ["--user", ...args], { encoding: "utf8" });
|
|
2228
2347
|
return {
|
|
2229
2348
|
ok: result.status === 0,
|
|
2230
2349
|
stdout: result.stdout.trim(),
|
|
@@ -2232,8 +2351,8 @@ function systemctlUser(args) {
|
|
|
2232
2351
|
};
|
|
2233
2352
|
}
|
|
2234
2353
|
function templateScript(ctx, name) {
|
|
2235
|
-
const source =
|
|
2236
|
-
return
|
|
2354
|
+
const source = join11(ctx.pjanglerRoot, ".mise", "scripts", name);
|
|
2355
|
+
return existsSync9(source) ? readText(source) : void 0;
|
|
2237
2356
|
}
|
|
2238
2357
|
function templateVersioningScript(ctx) {
|
|
2239
2358
|
return templateScript(ctx, "versioning.sh");
|
|
@@ -2241,16 +2360,45 @@ function templateVersioningScript(ctx) {
|
|
|
2241
2360
|
function templateLinkAgentfilesScript(ctx) {
|
|
2242
2361
|
return templateScript(ctx, "link-agentfiles.sh");
|
|
2243
2362
|
}
|
|
2363
|
+
function resolveAgentHooksLayer2(ctx) {
|
|
2364
|
+
const override = process.env.PJ_AGENT_HOOKS_LAYER;
|
|
2365
|
+
if (override === "0" || override === "false") return false;
|
|
2366
|
+
if (override === "1" || override === "true") return true;
|
|
2367
|
+
if (existsSync9(join11(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
|
|
2368
|
+
return !existsSync9(join11(ctx.homeDir, ".agents", "hooks"));
|
|
2369
|
+
}
|
|
2370
|
+
function evaluateMiseConditionals(template, agentHooksLayer) {
|
|
2371
|
+
const out = [];
|
|
2372
|
+
let depth = 0;
|
|
2373
|
+
let skipDepth = 0;
|
|
2374
|
+
for (const line of template.split("\n")) {
|
|
2375
|
+
const stmt = line.trim();
|
|
2376
|
+
const ifMatch = /^\{%-?\s*if\s+(\w+)\s*-?%\}$/.exec(stmt);
|
|
2377
|
+
if (ifMatch) {
|
|
2378
|
+
depth += 1;
|
|
2379
|
+
const truthy = ifMatch[1] === "agent_hooks_layer" ? agentHooksLayer : false;
|
|
2380
|
+
if (skipDepth === 0 && !truthy) skipDepth = depth;
|
|
2381
|
+
continue;
|
|
2382
|
+
}
|
|
2383
|
+
if (/^\{%-?\s*endif\s*-?%\}$/.test(stmt)) {
|
|
2384
|
+
if (skipDepth === depth) skipDepth = 0;
|
|
2385
|
+
depth = Math.max(0, depth - 1);
|
|
2386
|
+
continue;
|
|
2387
|
+
}
|
|
2388
|
+
if (skipDepth === 0) out.push(line);
|
|
2389
|
+
}
|
|
2390
|
+
return out.join("\n");
|
|
2391
|
+
}
|
|
2244
2392
|
function renderGeneratedProjectMiseToml(ctx, template) {
|
|
2245
2393
|
const project = readProjectJson(ctx);
|
|
2246
2394
|
const projectName = String(project?.project_name ?? basename3(ctx.repoRoot) ?? "project");
|
|
2247
|
-
return template.replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
|
|
2395
|
+
return evaluateMiseConditionals(template, resolveAgentHooksLayer2(ctx)).replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
|
|
2248
2396
|
}
|
|
2249
2397
|
function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
2250
|
-
const targetPath =
|
|
2251
|
-
if (
|
|
2252
|
-
const sourcePath =
|
|
2253
|
-
if (!
|
|
2398
|
+
const targetPath = join11(ctx.repoRoot, "mise.toml");
|
|
2399
|
+
if (existsSync9(targetPath)) return false;
|
|
2400
|
+
const sourcePath = join11(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
|
|
2401
|
+
if (!existsSync9(sourcePath)) return false;
|
|
2254
2402
|
changedFiles.push(targetPath);
|
|
2255
2403
|
if (!ctx.dryRun) {
|
|
2256
2404
|
writeText(targetPath, renderGeneratedProjectMiseToml(ctx, readText(sourcePath)));
|
|
@@ -2258,8 +2406,8 @@ function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
|
2258
2406
|
return true;
|
|
2259
2407
|
}
|
|
2260
2408
|
function templateVersionFilesConf(ctx, repoRoot) {
|
|
2261
|
-
const packageJson =
|
|
2262
|
-
return
|
|
2409
|
+
const packageJson = join11(repoRoot, "package.json");
|
|
2410
|
+
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";
|
|
2263
2411
|
}
|
|
2264
2412
|
function replaceOrAppendManagedBlock(text3, startMarker, block, beforePattern) {
|
|
2265
2413
|
if (startMarker.test(text3)) {
|
|
@@ -2283,7 +2431,7 @@ var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/herm
|
|
|
2283
2431
|
function requiredMisePathEntries(ctx) {
|
|
2284
2432
|
const required = [...BASE_MISE_PATH_ENTRIES];
|
|
2285
2433
|
for (const candidate of CONDITIONAL_HERMES_PATHS) {
|
|
2286
|
-
if (
|
|
2434
|
+
if (existsSync9(join11(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
|
|
2287
2435
|
}
|
|
2288
2436
|
return required;
|
|
2289
2437
|
}
|
|
@@ -2432,7 +2580,24 @@ function upsertLinkAgentfilesBlock(text3, ctx) {
|
|
|
2432
2580
|
return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
|
|
2433
2581
|
}
|
|
2434
2582
|
function readProjectJson(ctx) {
|
|
2435
|
-
return tryParseJson(safeReadText(
|
|
2583
|
+
return tryParseJson(safeReadText(join11(ctx.repoRoot, ".project.json")));
|
|
2584
|
+
}
|
|
2585
|
+
function boolSetting(value, fallback) {
|
|
2586
|
+
if (typeof value === "boolean") return value;
|
|
2587
|
+
if (typeof value === "string") {
|
|
2588
|
+
const normalized = value.trim().toLowerCase();
|
|
2589
|
+
if (["true", "1", "yes", "on"].includes(normalized)) return true;
|
|
2590
|
+
if (["false", "0", "no", "off"].includes(normalized)) return false;
|
|
2591
|
+
}
|
|
2592
|
+
return fallback;
|
|
2593
|
+
}
|
|
2594
|
+
function numberSetting(value, fallback) {
|
|
2595
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
2596
|
+
if (typeof value === "string" && value.trim()) {
|
|
2597
|
+
const parsed = Number(value);
|
|
2598
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
2599
|
+
}
|
|
2600
|
+
return fallback;
|
|
2436
2601
|
}
|
|
2437
2602
|
function canonicalProjectJson(ctx) {
|
|
2438
2603
|
const roles = discoverRoles(ctx.repoRoot);
|
|
@@ -2444,9 +2609,9 @@ function canonicalProjectJson(ctx) {
|
|
|
2444
2609
|
workspace: String((existing.ticket_provider?.workspace ?? firstRole?.planeWorkspace ?? "") || ""),
|
|
2445
2610
|
identifier: String((existing.ticket_provider?.identifier ?? firstRole?.ticketProviderIdentifier ?? "") || ""),
|
|
2446
2611
|
board_id: String((existing.ticket_provider?.board_id ?? firstRole?.ticketProviderBoardId ?? "") || ""),
|
|
2447
|
-
|
|
2448
|
-
state: String((existing.ticket_provider?.state ?? "planned") || "planned")
|
|
2612
|
+
state: String((existing.ticket_provider?.state ?? (firstRole?.ticketProviderBoardId ? "linked" : "planned")) || "planned")
|
|
2449
2613
|
};
|
|
2614
|
+
if (ticketProvider.board_id && ticketProvider.state === "planned") ticketProvider.state = "linked";
|
|
2450
2615
|
const existingAgents = existing.agents ?? {};
|
|
2451
2616
|
const discoveredAgents = Object.fromEntries(
|
|
2452
2617
|
roles.map((role) => [
|
|
@@ -2466,28 +2631,42 @@ function canonicalProjectJson(ctx) {
|
|
|
2466
2631
|
provisioning_state: existingAgent.provisioning_state
|
|
2467
2632
|
};
|
|
2468
2633
|
}
|
|
2634
|
+
const existingAutomation = existing.automation ?? {};
|
|
2635
|
+
const existingReconcile = existingAutomation.reconcile ?? {};
|
|
2636
|
+
const legacyEnabled = roles.find((role) => role.legacyReconcileEnabled)?.legacyReconcileEnabled;
|
|
2637
|
+
const legacyGrace = roles.find((role) => role.legacyReconcileGraceHours || role.legacyScrumGraceHours);
|
|
2638
|
+
const legacyAutoReview = roles.find((role) => role.legacyReconcileAutoReview || role.legacyScrumAutoReview);
|
|
2639
|
+
const automation = {
|
|
2640
|
+
...existingAutomation,
|
|
2641
|
+
reconcile: {
|
|
2642
|
+
enabled: boolSetting(existingReconcile.enabled, boolSetting(legacyEnabled, false)),
|
|
2643
|
+
grace_hours: numberSetting(existingReconcile.grace_hours, numberSetting(legacyGrace?.legacyReconcileGraceHours || legacyGrace?.legacyScrumGraceHours, 0)),
|
|
2644
|
+
auto_review: boolSetting(existingReconcile.auto_review, boolSetting(legacyAutoReview?.legacyReconcileAutoReview || legacyAutoReview?.legacyScrumAutoReview, true))
|
|
2645
|
+
}
|
|
2646
|
+
};
|
|
2469
2647
|
return {
|
|
2470
2648
|
project_name: String(existing.project_name ?? titleCaseSlug(slug)),
|
|
2471
2649
|
project_description: String(existing.project_description ?? ""),
|
|
2472
2650
|
project_slug: slug,
|
|
2473
2651
|
repo_path: ctx.repoRoot,
|
|
2474
2652
|
ticket_provider: ticketProvider,
|
|
2475
|
-
agents
|
|
2653
|
+
agents,
|
|
2654
|
+
automation
|
|
2476
2655
|
};
|
|
2477
2656
|
}
|
|
2478
2657
|
function projectJsonFinding(ctx) {
|
|
2479
|
-
const projectPath =
|
|
2480
|
-
const planeJsonPath =
|
|
2658
|
+
const projectPath = join11(ctx.repoRoot, ".project.json");
|
|
2659
|
+
const planeJsonPath = join11(ctx.repoRoot, ".plane.json");
|
|
2481
2660
|
const details = [];
|
|
2482
2661
|
const data = readProjectJson(ctx);
|
|
2483
2662
|
const roles = discoverRoles(ctx.repoRoot);
|
|
2484
|
-
if (!
|
|
2663
|
+
if (!existsSync9(projectPath)) {
|
|
2485
2664
|
return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json missing", details: [], fixable: true };
|
|
2486
2665
|
}
|
|
2487
2666
|
if (!data) {
|
|
2488
2667
|
return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json is not valid JSON", details: [], fixable: true };
|
|
2489
2668
|
}
|
|
2490
|
-
for (const key of ["project_name", "project_description", "project_slug", "repo_path", "ticket_provider", "agents"]) {
|
|
2669
|
+
for (const key of ["project_name", "project_description", "project_slug", "repo_path", "ticket_provider", "agents", "automation"]) {
|
|
2491
2670
|
if (!(key in data)) details.push(`missing key: ${key}`);
|
|
2492
2671
|
}
|
|
2493
2672
|
if (data.repo_path !== ctx.repoRoot) details.push(`repo_path should be ${ctx.repoRoot}`);
|
|
@@ -2504,10 +2683,19 @@ function projectJsonFinding(ctx) {
|
|
|
2504
2683
|
}
|
|
2505
2684
|
}
|
|
2506
2685
|
const ticketProvider = data.ticket_provider ?? {};
|
|
2507
|
-
for (const key of ["type", "workspace", "identifier", "board_id", "
|
|
2686
|
+
for (const key of ["type", "workspace", "identifier", "board_id", "state"]) {
|
|
2508
2687
|
if (!(key in ticketProvider)) details.push(`ticket_provider.${key} missing`);
|
|
2509
2688
|
}
|
|
2510
|
-
if (
|
|
2689
|
+
if ("board_url" in ticketProvider) details.push("ticket_provider.board_url should be removed; derive it from provider/workspace/board_id");
|
|
2690
|
+
if (!ticketProvider.board_id && roles.some((role) => role.ticketProviderBoardId)) {
|
|
2691
|
+
details.push("ticket_provider.board_id missing even though legacy role.yaml contains a board binding");
|
|
2692
|
+
}
|
|
2693
|
+
const automation = data.automation ?? {};
|
|
2694
|
+
const reconcile = automation.reconcile ?? {};
|
|
2695
|
+
for (const key of ["enabled", "grace_hours", "auto_review"]) {
|
|
2696
|
+
if (!(key in reconcile)) details.push(`automation.reconcile.${key} missing`);
|
|
2697
|
+
}
|
|
2698
|
+
if (existsSync9(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
|
|
2511
2699
|
return {
|
|
2512
2700
|
id: "sot.project-json",
|
|
2513
2701
|
title: "Canonical .project.json",
|
|
@@ -2520,7 +2708,7 @@ function projectJsonFinding(ctx) {
|
|
|
2520
2708
|
function renderSoul(role) {
|
|
2521
2709
|
const telegram = role.botHandle ? `@${role.botHandle}` : "(unwired)";
|
|
2522
2710
|
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.";
|
|
2523
|
-
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
|
|
2711
|
+
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.`;
|
|
2524
2712
|
const runtimeOwner = role.runtimeOwner || "delorenj";
|
|
2525
2713
|
return `# ${role.displayName || role.agentId}
|
|
2526
2714
|
|
|
@@ -2588,17 +2776,17 @@ exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" HERMES_OAUTH
|
|
|
2588
2776
|
`.replace(/\u0010/g, "$");
|
|
2589
2777
|
}
|
|
2590
2778
|
function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
|
|
2591
|
-
if (!
|
|
2779
|
+
if (!existsSync9(sourceDir)) return;
|
|
2592
2780
|
mkdirSync6(targetDir, { recursive: true });
|
|
2593
|
-
for (const entry of
|
|
2594
|
-
const sourcePath =
|
|
2781
|
+
for (const entry of readdirSync2(sourceDir, { withFileTypes: true })) {
|
|
2782
|
+
const sourcePath = join11(sourceDir, entry.name);
|
|
2595
2783
|
if (skip?.(sourcePath)) continue;
|
|
2596
|
-
const targetPath =
|
|
2784
|
+
const targetPath = join11(targetDir, entry.name);
|
|
2597
2785
|
if (entry.isDirectory()) {
|
|
2598
2786
|
copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
|
|
2599
2787
|
continue;
|
|
2600
2788
|
}
|
|
2601
|
-
if (
|
|
2789
|
+
if (existsSync9(targetPath)) continue;
|
|
2602
2790
|
changedFiles.push(targetPath);
|
|
2603
2791
|
if (!dryRun) {
|
|
2604
2792
|
ensureParent(targetPath);
|
|
@@ -2607,7 +2795,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
|
|
|
2607
2795
|
}
|
|
2608
2796
|
}
|
|
2609
2797
|
function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
|
|
2610
|
-
const gitmodulesPath =
|
|
2798
|
+
const gitmodulesPath = join11(repoRoot, ".gitmodules");
|
|
2611
2799
|
const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
|
|
2612
2800
|
const owner = role.runtimeOwner || "delorenj";
|
|
2613
2801
|
const block = `[submodule "agents/hermes/${role.role}/runtime"]
|
|
@@ -2707,14 +2895,14 @@ var RULES = [
|
|
|
2707
2895
|
id: "mise.config-root",
|
|
2708
2896
|
title: "mise config_root + AGENTS link hooks",
|
|
2709
2897
|
audit: (ctx) => {
|
|
2710
|
-
const misePath =
|
|
2711
|
-
if (!
|
|
2898
|
+
const misePath = join11(ctx.repoRoot, "mise.toml");
|
|
2899
|
+
if (!existsSync9(misePath)) {
|
|
2712
2900
|
return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
|
|
2713
2901
|
}
|
|
2714
2902
|
const text3 = readText(misePath);
|
|
2715
2903
|
const details = [];
|
|
2716
|
-
const linkAgentfilesPath =
|
|
2717
|
-
if (!
|
|
2904
|
+
const linkAgentfilesPath = join11(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
2905
|
+
if (!existsSync9(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
|
|
2718
2906
|
const pathValues = [...(text3.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
2719
2907
|
const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
|
|
2720
2908
|
if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
|
|
@@ -2732,10 +2920,10 @@ var RULES = [
|
|
|
2732
2920
|
};
|
|
2733
2921
|
},
|
|
2734
2922
|
migrate: (ctx, finding) => {
|
|
2735
|
-
const path =
|
|
2923
|
+
const path = join11(ctx.repoRoot, "mise.toml");
|
|
2736
2924
|
const changedFiles = [];
|
|
2737
2925
|
const details = [];
|
|
2738
|
-
if (!
|
|
2926
|
+
if (!existsSync9(path)) {
|
|
2739
2927
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
2740
2928
|
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: [] };
|
|
2741
2929
|
}
|
|
@@ -2751,7 +2939,7 @@ var RULES = [
|
|
|
2751
2939
|
if (!ctx.dryRun) writeText(path, next);
|
|
2752
2940
|
text3 = next;
|
|
2753
2941
|
}
|
|
2754
|
-
const linkAgentfilesPath =
|
|
2942
|
+
const linkAgentfilesPath = join11(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
2755
2943
|
const expectedScript = templateLinkAgentfilesScript(ctx);
|
|
2756
2944
|
if (expectedScript === void 0) {
|
|
2757
2945
|
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: [] };
|
|
@@ -2778,13 +2966,13 @@ var RULES = [
|
|
|
2778
2966
|
title: "managed mise versioning block",
|
|
2779
2967
|
audit: (ctx) => {
|
|
2780
2968
|
const details = [];
|
|
2781
|
-
const misePath =
|
|
2782
|
-
const versioningPath =
|
|
2783
|
-
const manifestPath =
|
|
2969
|
+
const misePath = join11(ctx.repoRoot, "mise.toml");
|
|
2970
|
+
const versioningPath = join11(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
2971
|
+
const manifestPath = join11(ctx.repoRoot, ".mise", "version-files.conf");
|
|
2784
2972
|
const text3 = safeReadText(misePath);
|
|
2785
2973
|
if (!text3?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
|
|
2786
|
-
if (!
|
|
2787
|
-
if (!
|
|
2974
|
+
if (!existsSync9(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
|
|
2975
|
+
if (!existsSync9(manifestPath)) details.push(".mise/version-files.conf missing");
|
|
2788
2976
|
return {
|
|
2789
2977
|
id: "mise.versioning",
|
|
2790
2978
|
title: "managed mise versioning block",
|
|
@@ -2797,8 +2985,8 @@ var RULES = [
|
|
|
2797
2985
|
migrate: (ctx, finding) => {
|
|
2798
2986
|
const changedFiles = [];
|
|
2799
2987
|
const details = [];
|
|
2800
|
-
const misePath =
|
|
2801
|
-
if (!
|
|
2988
|
+
const misePath = join11(ctx.repoRoot, "mise.toml");
|
|
2989
|
+
if (!existsSync9(misePath)) {
|
|
2802
2990
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
2803
2991
|
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: [] };
|
|
2804
2992
|
}
|
|
@@ -2808,12 +2996,21 @@ var RULES = [
|
|
|
2808
2996
|
}
|
|
2809
2997
|
}
|
|
2810
2998
|
const currentMise = readText(misePath);
|
|
2811
|
-
|
|
2999
|
+
let cleanedMise = currentMise;
|
|
3000
|
+
if (!currentMise.includes("# >>> mise-versioning >>>")) {
|
|
3001
|
+
const taskNames = ["version", "version:bump", "version:bump-patch", "version:bump-minor", "version:bump-major", "version:check", "version:sync"];
|
|
3002
|
+
for (const taskName of taskNames) {
|
|
3003
|
+
const escaped = taskName.replace(/:/g, "\\:");
|
|
3004
|
+
const headerPattern = new RegExp(`^\\[tasks\\.(?:"${escaped}"|'${escaped}'|${escaped})\\]$`);
|
|
3005
|
+
cleanedMise = removeTomlSection(cleanedMise, headerPattern);
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
const nextMise = replaceOrAppendManagedBlock(cleanedMise, /# >>> mise-versioning >>>/, VERSIONING_BLOCK, /^\[tasks\.build\]/m);
|
|
2812
3009
|
if (nextMise !== currentMise) {
|
|
2813
3010
|
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
2814
3011
|
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
2815
3012
|
}
|
|
2816
|
-
const versioningPath =
|
|
3013
|
+
const versioningPath = join11(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
2817
3014
|
const expectedScript = templateVersioningScript(ctx);
|
|
2818
3015
|
if (expectedScript === void 0) {
|
|
2819
3016
|
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: [] };
|
|
@@ -2825,7 +3022,7 @@ var RULES = [
|
|
|
2825
3022
|
chmodSync2(versioningPath, 493);
|
|
2826
3023
|
}
|
|
2827
3024
|
}
|
|
2828
|
-
const manifestPath =
|
|
3025
|
+
const manifestPath = join11(ctx.repoRoot, ".mise", "version-files.conf");
|
|
2829
3026
|
const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
|
|
2830
3027
|
if (safeReadText(manifestPath) !== expectedManifest) {
|
|
2831
3028
|
changedFiles.push(manifestPath);
|
|
@@ -2845,9 +3042,9 @@ var RULES = [
|
|
|
2845
3042
|
id: "sot.agent-symlinks",
|
|
2846
3043
|
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
2847
3044
|
audit: (ctx) => {
|
|
2848
|
-
const agentsPath =
|
|
2849
|
-
if (!
|
|
2850
|
-
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) =>
|
|
3045
|
+
const agentsPath = join11(ctx.repoRoot, "AGENTS.md");
|
|
3046
|
+
if (!existsSync9(agentsPath)) {
|
|
3047
|
+
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync9(join11(ctx.repoRoot, file)));
|
|
2851
3048
|
if (fallbackSources.length === 0) {
|
|
2852
3049
|
return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
|
|
2853
3050
|
}
|
|
@@ -2862,7 +3059,7 @@ var RULES = [
|
|
|
2862
3059
|
}
|
|
2863
3060
|
const details = [];
|
|
2864
3061
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2865
|
-
const full =
|
|
3062
|
+
const full = join11(ctx.repoRoot, file);
|
|
2866
3063
|
const target = readSymlinkTarget(full);
|
|
2867
3064
|
if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
|
|
2868
3065
|
}
|
|
@@ -2886,7 +3083,7 @@ var RULES = [
|
|
|
2886
3083
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
|
|
2887
3084
|
}
|
|
2888
3085
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2889
|
-
const full =
|
|
3086
|
+
const full = join11(ctx.repoRoot, file);
|
|
2890
3087
|
const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
|
|
2891
3088
|
if (result.blocked) blockedDetails.push(result.blocked);
|
|
2892
3089
|
if (result.changed) changedFiles.push(full);
|
|
@@ -2908,7 +3105,7 @@ var RULES = [
|
|
|
2908
3105
|
migrate: (ctx, finding) => {
|
|
2909
3106
|
const changedFiles = [];
|
|
2910
3107
|
const details = [];
|
|
2911
|
-
const path =
|
|
3108
|
+
const path = join11(ctx.repoRoot, ".project.json");
|
|
2912
3109
|
const existing = readProjectJson(ctx) ?? {};
|
|
2913
3110
|
const canonical = canonicalProjectJson(ctx);
|
|
2914
3111
|
const merged = { ...existing, ...canonical };
|
|
@@ -2918,10 +3115,10 @@ var RULES = [
|
|
|
2918
3115
|
changedFiles.push(path);
|
|
2919
3116
|
if (!ctx.dryRun) writeText(path, expected);
|
|
2920
3117
|
}
|
|
2921
|
-
const planeJson =
|
|
2922
|
-
if (
|
|
3118
|
+
const planeJson = join11(ctx.repoRoot, ".plane.json");
|
|
3119
|
+
if (existsSync9(planeJson)) {
|
|
2923
3120
|
const backup = `${planeJson}.migrated-backup`;
|
|
2924
|
-
if (
|
|
3121
|
+
if (existsSync9(backup)) {
|
|
2925
3122
|
details.push(`cannot back up .plane.json because ${relative(ctx.repoRoot, backup)} already exists`);
|
|
2926
3123
|
} else {
|
|
2927
3124
|
changedFiles.push(backup);
|
|
@@ -2943,8 +3140,8 @@ var RULES = [
|
|
|
2943
3140
|
title: ".env.op + gitignore secrets contract",
|
|
2944
3141
|
audit: (ctx) => {
|
|
2945
3142
|
const details = [];
|
|
2946
|
-
const envOp = safeReadText(
|
|
2947
|
-
const gitignore = safeReadText(
|
|
3143
|
+
const envOp = safeReadText(join11(ctx.repoRoot, ".env.op"));
|
|
3144
|
+
const gitignore = safeReadText(join11(ctx.repoRoot, ".gitignore"));
|
|
2948
3145
|
if (!envOp) {
|
|
2949
3146
|
details.push(".env.op missing");
|
|
2950
3147
|
} else {
|
|
@@ -2970,12 +3167,12 @@ var RULES = [
|
|
|
2970
3167
|
migrate: (ctx, finding) => {
|
|
2971
3168
|
const changedFiles = [];
|
|
2972
3169
|
const details = [];
|
|
2973
|
-
const envOpPath =
|
|
2974
|
-
if (!
|
|
3170
|
+
const envOpPath = join11(ctx.repoRoot, ".env.op");
|
|
3171
|
+
if (!existsSync9(envOpPath)) {
|
|
2975
3172
|
changedFiles.push(envOpPath);
|
|
2976
|
-
if (!ctx.dryRun) writeText(envOpPath, readText(
|
|
3173
|
+
if (!ctx.dryRun) writeText(envOpPath, readText(join11(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
|
|
2977
3174
|
}
|
|
2978
|
-
const gitignorePath =
|
|
3175
|
+
const gitignorePath = join11(ctx.repoRoot, ".gitignore");
|
|
2979
3176
|
const gitignore = safeReadText(gitignorePath) ?? "";
|
|
2980
3177
|
const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
|
|
2981
3178
|
# NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
|
|
@@ -3002,7 +3199,7 @@ var RULES = [
|
|
|
3002
3199
|
title: ".copier-answers.yml provenance + drift report",
|
|
3003
3200
|
audit: (ctx) => {
|
|
3004
3201
|
const details = [];
|
|
3005
|
-
const path =
|
|
3202
|
+
const path = join11(ctx.repoRoot, ".copier-answers.yml");
|
|
3006
3203
|
const text3 = safeReadText(path);
|
|
3007
3204
|
const project = readProjectJson(ctx);
|
|
3008
3205
|
if (!text3) {
|
|
@@ -3033,12 +3230,12 @@ var RULES = [
|
|
|
3033
3230
|
const changedFiles = [];
|
|
3034
3231
|
const project = canonicalProjectJson(ctx);
|
|
3035
3232
|
const text3 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
|
|
3036
|
-
_src_path: ${
|
|
3233
|
+
_src_path: ${join11(ctx.pjanglerRoot, "templates", "commonproject")}
|
|
3037
3234
|
project_description: ${String(project.project_description)}
|
|
3038
3235
|
project_name: ${String(project.project_name)}
|
|
3039
3236
|
ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
3040
3237
|
`;
|
|
3041
|
-
const path =
|
|
3238
|
+
const path = join11(ctx.repoRoot, ".copier-answers.yml");
|
|
3042
3239
|
if (safeReadText(path) !== text3) {
|
|
3043
3240
|
changedFiles.push(path);
|
|
3044
3241
|
if (!ctx.dryRun) writeText(path, text3);
|
|
@@ -3057,15 +3254,15 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3057
3254
|
id: "bmad.scaffold",
|
|
3058
3255
|
title: "BMAD modules/docs scaffold",
|
|
3059
3256
|
audit: (ctx) => {
|
|
3060
|
-
const sourceRoot =
|
|
3061
|
-
const targetRoot =
|
|
3257
|
+
const sourceRoot = join11(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
|
|
3258
|
+
const targetRoot = join11(ctx.repoRoot, "_bmad");
|
|
3062
3259
|
const sentinels = [
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3260
|
+
join11("core", "config.yaml"),
|
|
3261
|
+
join11("custom", "config.yaml"),
|
|
3262
|
+
join11("custom", "workflows", "ticket-lifecycle", "workflow.yaml"),
|
|
3263
|
+
join11("bmm", "workflows", "workflow-status", "workflow.yaml")
|
|
3067
3264
|
];
|
|
3068
|
-
const missing = sentinels.filter((file) =>
|
|
3265
|
+
const missing = sentinels.filter((file) => existsSync9(join11(sourceRoot, file)) && !existsSync9(join11(targetRoot, file)));
|
|
3069
3266
|
return {
|
|
3070
3267
|
id: "bmad.scaffold",
|
|
3071
3268
|
title: "BMAD modules/docs scaffold",
|
|
@@ -3077,7 +3274,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3077
3274
|
},
|
|
3078
3275
|
migrate: (ctx, finding) => {
|
|
3079
3276
|
const changedFiles = [];
|
|
3080
|
-
copyMissingRecursive(
|
|
3277
|
+
copyMissingRecursive(join11(ctx.pjanglerRoot, "templates", "commonproject", "_bmad"), join11(ctx.repoRoot, "_bmad"), changedFiles, ctx.dryRun);
|
|
3081
3278
|
return {
|
|
3082
3279
|
id: finding.id,
|
|
3083
3280
|
title: finding.title,
|
|
@@ -3099,11 +3296,11 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3099
3296
|
}
|
|
3100
3297
|
const details = [];
|
|
3101
3298
|
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"]) {
|
|
3102
|
-
if (!
|
|
3299
|
+
if (!existsSync9(join11(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join11(role.roleDir, rel))}`);
|
|
3103
3300
|
}
|
|
3104
|
-
const gitmodules = safeReadText(
|
|
3301
|
+
const gitmodules = safeReadText(join11(ctx.repoRoot, ".gitmodules")) ?? "";
|
|
3105
3302
|
if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
|
|
3106
|
-
if (!profileMetaInheritsDefault(
|
|
3303
|
+
if (!profileMetaInheritsDefault(join11(role.roleDir, "runtime", "profile.yaml"))) {
|
|
3107
3304
|
details.push("runtime/profile.yaml missing inherited default config metadata");
|
|
3108
3305
|
}
|
|
3109
3306
|
const registry = safeReadText(registryPath(ctx.homeDir));
|
|
@@ -3124,21 +3321,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3124
3321
|
if (!role) {
|
|
3125
3322
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
|
|
3126
3323
|
}
|
|
3127
|
-
const templateRoleDir =
|
|
3128
|
-
writeIfDifferent(
|
|
3129
|
-
writeIfDifferent(
|
|
3130
|
-
writeIfDifferent(
|
|
3131
|
-
copyMissingRecursive(
|
|
3132
|
-
copyMissingRecursive(
|
|
3133
|
-
copyMissingRecursive(
|
|
3134
|
-
const promptSource =
|
|
3135
|
-
const promptTarget =
|
|
3136
|
-
if (
|
|
3324
|
+
const templateRoleDir = join11(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
|
|
3325
|
+
writeIfDifferent(join11(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
|
|
3326
|
+
writeIfDifferent(join11(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
|
|
3327
|
+
writeIfDifferent(join11(role.roleDir, ".gitignore"), readText(join11(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
|
|
3328
|
+
copyMissingRecursive(join11(templateRoleDir, ".runtime-scaffold"), join11(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
|
|
3329
|
+
copyMissingRecursive(join11(templateRoleDir, ".runtime-scaffold"), join11(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
|
|
3330
|
+
copyMissingRecursive(join11(templateRoleDir, ".scripts"), join11(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
|
|
3331
|
+
const promptSource = join11(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
|
|
3332
|
+
const promptTarget = join11(role.roleDir, ".scripts", "sentinel.prompt.md");
|
|
3333
|
+
if (existsSync9(promptSource) && !existsSync9(promptTarget)) {
|
|
3137
3334
|
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);
|
|
3138
3335
|
writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
|
|
3139
3336
|
}
|
|
3140
3337
|
upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
|
|
3141
|
-
const profileMetaUpdated = upsertInheritedProfileMeta(
|
|
3338
|
+
const profileMetaUpdated = upsertInheritedProfileMeta(join11(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
|
|
3142
3339
|
if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
|
|
3143
3340
|
const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
|
|
3144
3341
|
if (registryUpdated) details.push(`updated ${registryUpdated}`);
|
|
@@ -3152,6 +3349,103 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3152
3349
|
};
|
|
3153
3350
|
}
|
|
3154
3351
|
},
|
|
3352
|
+
{
|
|
3353
|
+
id: "hermes.untracked-runtimes",
|
|
3354
|
+
title: "Hermes agent runtimes untracked + gitignored",
|
|
3355
|
+
audit: (ctx) => {
|
|
3356
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
3357
|
+
if (roles.length === 0) {
|
|
3358
|
+
return {
|
|
3359
|
+
id: "hermes.untracked-runtimes",
|
|
3360
|
+
title: "Hermes agent runtimes untracked + gitignored",
|
|
3361
|
+
status: "skip",
|
|
3362
|
+
summary: "No Hermes roles present",
|
|
3363
|
+
details: [],
|
|
3364
|
+
fixable: false
|
|
3365
|
+
};
|
|
3366
|
+
}
|
|
3367
|
+
const details = [];
|
|
3368
|
+
for (const role of roles) {
|
|
3369
|
+
const roleRelDir = relative(ctx.repoRoot, role.roleDir);
|
|
3370
|
+
const runtimeRelPath = join11(roleRelDir, "runtime");
|
|
3371
|
+
const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
|
|
3372
|
+
cwd: ctx.repoRoot,
|
|
3373
|
+
encoding: "utf8"
|
|
3374
|
+
});
|
|
3375
|
+
if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
|
|
3376
|
+
details.push(`submodule runtime is tracked in Git index at ${runtimeRelPath}`);
|
|
3377
|
+
}
|
|
3378
|
+
const gitignorePath = join11(role.roleDir, ".gitignore");
|
|
3379
|
+
if (existsSync9(gitignorePath)) {
|
|
3380
|
+
const content = safeReadText(gitignorePath) ?? "";
|
|
3381
|
+
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
3382
|
+
if (!lines.includes("runtime/") && !lines.includes("runtime")) {
|
|
3383
|
+
details.push(`.gitignore missing runtime/ ignore entry in ${relative(ctx.repoRoot, gitignorePath)}`);
|
|
3384
|
+
}
|
|
3385
|
+
} else {
|
|
3386
|
+
details.push(`.gitignore is missing in ${relative(ctx.repoRoot, gitignorePath)}`);
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
return {
|
|
3390
|
+
id: "hermes.untracked-runtimes",
|
|
3391
|
+
title: "Hermes agent runtimes untracked + gitignored",
|
|
3392
|
+
status: details.length === 0 ? "pass" : "fail",
|
|
3393
|
+
summary: details.length === 0 ? "All Hermes agent runtimes are untracked and gitignored" : `${details.length} issue(s) with untracked/ignored runtimes detected`,
|
|
3394
|
+
details,
|
|
3395
|
+
fixable: true
|
|
3396
|
+
};
|
|
3397
|
+
},
|
|
3398
|
+
migrate: (ctx, finding) => {
|
|
3399
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
3400
|
+
const changedFiles = [];
|
|
3401
|
+
const details = [];
|
|
3402
|
+
for (const role of roles) {
|
|
3403
|
+
const roleRelDir = relative(ctx.repoRoot, role.roleDir);
|
|
3404
|
+
const runtimeRelPath = join11(roleRelDir, "runtime");
|
|
3405
|
+
const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
|
|
3406
|
+
cwd: ctx.repoRoot,
|
|
3407
|
+
encoding: "utf8"
|
|
3408
|
+
});
|
|
3409
|
+
if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
|
|
3410
|
+
details.push(`untrack ${runtimeRelPath}`);
|
|
3411
|
+
changedFiles.push(runtimeRelPath);
|
|
3412
|
+
if (!ctx.dryRun) {
|
|
3413
|
+
spawnSync6("git", ["rm", "--cached", "-r", runtimeRelPath], {
|
|
3414
|
+
cwd: ctx.repoRoot,
|
|
3415
|
+
encoding: "utf8"
|
|
3416
|
+
});
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
const gitignorePath = join11(role.roleDir, ".gitignore");
|
|
3420
|
+
let content = "";
|
|
3421
|
+
let isIgnored = false;
|
|
3422
|
+
if (existsSync9(gitignorePath)) {
|
|
3423
|
+
content = safeReadText(gitignorePath) ?? "";
|
|
3424
|
+
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
3425
|
+
isIgnored = lines.includes("runtime/") || lines.includes("runtime");
|
|
3426
|
+
}
|
|
3427
|
+
if (!isIgnored) {
|
|
3428
|
+
details.push(`ignore runtime/ in ${relative(ctx.repoRoot, gitignorePath)}`);
|
|
3429
|
+
changedFiles.push(gitignorePath);
|
|
3430
|
+
if (!ctx.dryRun) {
|
|
3431
|
+
if (content && !content.endsWith("\n")) {
|
|
3432
|
+
content += "\n";
|
|
3433
|
+
}
|
|
3434
|
+
content += "runtime/\n";
|
|
3435
|
+
writeText(gitignorePath, content);
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
return {
|
|
3440
|
+
id: finding.id,
|
|
3441
|
+
title: finding.title,
|
|
3442
|
+
status: changedFiles.length ? "applied" : "noop",
|
|
3443
|
+
summary: changedFiles.length ? "Hermes agent runtimes made untracked and ignored" : "No changes required",
|
|
3444
|
+
changedFiles,
|
|
3445
|
+
details
|
|
3446
|
+
};
|
|
3447
|
+
}
|
|
3448
|
+
},
|
|
3155
3449
|
{
|
|
3156
3450
|
id: "systemd.sentinel",
|
|
3157
3451
|
title: "Hermes systemd/sentinel units enabled + active",
|
|
@@ -3192,9 +3486,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3192
3486
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
|
|
3193
3487
|
}
|
|
3194
3488
|
for (const role of roles) {
|
|
3195
|
-
const sysDir =
|
|
3489
|
+
const sysDir = join11(ctx.homeDir, ".config", "systemd", "user");
|
|
3196
3490
|
const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`];
|
|
3197
|
-
const allUnitsPresent = units.every((unit) =>
|
|
3491
|
+
const allUnitsPresent = units.every((unit) => existsSync9(join11(sysDir, unit)));
|
|
3198
3492
|
if (allUnitsPresent) {
|
|
3199
3493
|
if (ctx.dryRun) {
|
|
3200
3494
|
details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
|
|
@@ -3206,12 +3500,12 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3206
3500
|
}
|
|
3207
3501
|
continue;
|
|
3208
3502
|
}
|
|
3209
|
-
for (const script of [
|
|
3210
|
-
if (!script || !
|
|
3503
|
+
for (const script of [join11(role.roleDir, ".scripts", "70-systemd.sh")]) {
|
|
3504
|
+
if (!script || !existsSync9(script)) continue;
|
|
3211
3505
|
if (ctx.dryRun) {
|
|
3212
3506
|
details.push(`would run: bash ${script}`);
|
|
3213
3507
|
} else {
|
|
3214
|
-
const result =
|
|
3508
|
+
const result = spawnSync6("bash", [script], { cwd: role.roleDir, encoding: "utf8" });
|
|
3215
3509
|
if (result.status !== 0) details.push(`script failed: ${script}: ${result.stderr.trim() || result.stdout.trim()}`);
|
|
3216
3510
|
}
|
|
3217
3511
|
}
|
|
@@ -3346,15 +3640,15 @@ function formatMigrationReport(report) {
|
|
|
3346
3640
|
}
|
|
3347
3641
|
|
|
3348
3642
|
// src/utils/version.ts
|
|
3349
|
-
import { readFileSync as
|
|
3350
|
-
import { dirname as dirname7, join as
|
|
3643
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
3644
|
+
import { dirname as dirname7, join as join12 } from "node:path";
|
|
3351
3645
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
3352
3646
|
var PJANGLER_VERSION = (() => {
|
|
3353
3647
|
try {
|
|
3354
3648
|
let dir = dirname7(fileURLToPath4(import.meta.url));
|
|
3355
3649
|
for (let i = 0; i < 4; i++) {
|
|
3356
3650
|
try {
|
|
3357
|
-
const raw =
|
|
3651
|
+
const raw = readFileSync6(join12(dir, "package.json"), "utf8");
|
|
3358
3652
|
return JSON.parse(raw).version ?? "0.0.0";
|
|
3359
3653
|
} catch {
|
|
3360
3654
|
const parent = dirname7(dir);
|
|
@@ -3397,16 +3691,16 @@ async function promptForRuleIds(rules) {
|
|
|
3397
3691
|
return selected;
|
|
3398
3692
|
}
|
|
3399
3693
|
function readJson(path) {
|
|
3400
|
-
if (!
|
|
3694
|
+
if (!existsSync10(path)) return void 0;
|
|
3401
3695
|
try {
|
|
3402
|
-
const parsed = JSON.parse(
|
|
3696
|
+
const parsed = JSON.parse(readFileSync7(path, "utf8"));
|
|
3403
3697
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
3404
3698
|
} catch {
|
|
3405
3699
|
return void 0;
|
|
3406
3700
|
}
|
|
3407
3701
|
}
|
|
3408
3702
|
function findGitRoot(cwd) {
|
|
3409
|
-
const result =
|
|
3703
|
+
const result = spawnSync7("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8" });
|
|
3410
3704
|
if (result.status !== 0) return void 0;
|
|
3411
3705
|
return resolve3(result.stdout.trim());
|
|
3412
3706
|
}
|
|
@@ -3416,8 +3710,8 @@ function packageNameToProjectName(value) {
|
|
|
3416
3710
|
return name.replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()).trim();
|
|
3417
3711
|
}
|
|
3418
3712
|
function deriveProjectDefaults(targetDir) {
|
|
3419
|
-
const manifest = readJson(
|
|
3420
|
-
const pkg = readJson(
|
|
3713
|
+
const manifest = readJson(join13(targetDir, ".project.json"));
|
|
3714
|
+
const pkg = readJson(join13(targetDir, "package.json"));
|
|
3421
3715
|
const name = String(manifest?.project_name ?? "").trim() || packageNameToProjectName(typeof pkg?.name === "string" ? pkg.name : void 0) || packageNameToProjectName(basename4(targetDir)) || "Project";
|
|
3422
3716
|
const ticketProvider = manifest?.ticket_provider && typeof manifest.ticket_provider === "object" ? manifest.ticket_provider : {};
|
|
3423
3717
|
return {
|
|
@@ -3473,7 +3767,7 @@ function actionNeedsRun(plan, kind, syncMode) {
|
|
|
3473
3767
|
if (!action || action.kind !== "project.write-manifest") return false;
|
|
3474
3768
|
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
3475
3769
|
`;
|
|
3476
|
-
return !
|
|
3770
|
+
return !existsSync10(action.path) || readFileSync7(action.path, "utf8") !== next;
|
|
3477
3771
|
}
|
|
3478
3772
|
if (kind === "copier.copy.commonproject") return true;
|
|
3479
3773
|
if (kind === "ticket-provider.create-or-link") return plan.actions.some((action) => action.kind === kind && action.enabled);
|
|
@@ -3528,7 +3822,7 @@ async function resolveProjectInitTarget(name, options) {
|
|
|
3528
3822
|
if (!targetDir && interactive) {
|
|
3529
3823
|
const defaultName = name ?? basename4(cwd);
|
|
3530
3824
|
const promptedName = name ?? await promptTextValue("Project name", packageNameToProjectName(defaultName));
|
|
3531
|
-
const defaultDir =
|
|
3825
|
+
const defaultDir = join13(cwd, promptedName.replace(/[^A-Za-z0-9._-]/g, "") || promptedName.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
|
|
3532
3826
|
targetDir = await promptTextValue("Project directory", defaultDir);
|
|
3533
3827
|
name = promptedName;
|
|
3534
3828
|
}
|
|
@@ -3536,7 +3830,7 @@ async function resolveProjectInitTarget(name, options) {
|
|
|
3536
3830
|
if (!name) throw new Error("Project name or --target-dir is required when project init is not run inside a git repo");
|
|
3537
3831
|
targetDir = resolve3(process.cwd(), name.replace(/[^A-Za-z0-9._-]/g, "") || name.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
|
|
3538
3832
|
}
|
|
3539
|
-
const targetExists =
|
|
3833
|
+
const targetExists = existsSync10(targetDir);
|
|
3540
3834
|
if (targetExists && !statSync2(targetDir).isDirectory()) throw new Error(`Target path is not a directory: ${targetDir}`);
|
|
3541
3835
|
const targetGitRoot = targetExists ? findGitRoot(targetDir) : void 0;
|
|
3542
3836
|
const syncMode = Boolean(targetGitRoot && resolve3(targetGitRoot) === resolve3(targetDir));
|
|
@@ -3574,7 +3868,7 @@ async function runRecipeSubsystem(name, options) {
|
|
|
3574
3868
|
}
|
|
3575
3869
|
var program = new Command3();
|
|
3576
3870
|
program.name("pjangler").description("Project subsystem bootstrapper CLI").version(PJANGLER_VERSION);
|
|
3577
|
-
program.command("init").argument("[name]", "Project name to bootstrap (omit inside an existing git repo)").description("Bootstrap a project: registry entry + CommonProject scaffold + .project.json").option("--description <text>", "Project description").option("--target-dir <path>", "Target repo path").option("--source-skill <path>", "Source skill/template provenance path").option("--primary-language <language>", "Primary language for CommonProject rendering", "python").option("--provision-agent", "Plan local Hermes PM agent provisioning").option("--agent-role <role>", "Hermes agent role to plan when --provision-agent is set", "pm").option("--apply", "Write the registry and render the repo scaffold").option("--dry-run", "Preview changes without writing files (default)").option("--live", "Allow live/network/cloud provisioning actions").option("--slug <slug>", "Project registry slug override").option("--identifier <identifier>", "Ticket identifier override").option("--ticket-provider <type>", "Ticket provider: plane | trello", "plane").option("--board-id <id>", "Board id (Plane project UUID or Trello board id)").option("--board-url <url>", "
|
|
3871
|
+
program.command("init").argument("[name]", "Project name to bootstrap (omit inside an existing git repo)").description("Bootstrap a project: registry entry + CommonProject scaffold + .project.json").option("--description <text>", "Project description").option("--target-dir <path>", "Target repo path").option("--source-skill <path>", "Source skill/template provenance path").option("--primary-language <language>", "Primary language for CommonProject rendering", "python").option("--provision-agent", "Plan local Hermes PM agent provisioning").option("--agent-role <role>", "Hermes agent role to plan when --provision-agent is set", "pm").option("--apply", "Write the registry and render the repo scaffold").option("--dry-run", "Preview changes without writing files (default)").option("--live", "Allow live/network/cloud provisioning actions").option("--slug <slug>", "Project registry slug override").option("--identifier <identifier>", "Ticket identifier override").option("--ticket-provider <type>", "Ticket provider: plane | trello", "plane").option("--board-id <id>", "Board id (Plane project UUID or Trello board id)").option("--board-url <url>", "Deprecated no-op; board URLs are derived from provider + workspace + board-id").option("--workspace <name>", "Ticket workspace/org (Plane workspace; blank for Trello)").option("--registry <path>", `Registry path override (default: ${projectRegistryPath()})`).option("-f, --force", "Allow replacing an existing registry entry and re-rendering files").option("-y, --yes", "Apply every proposed operation without prompting").option("--no-tui", "Disable interactive prompts").option("--json", "Output machine-parseable JSON").action(async (name, options) => {
|
|
3578
3872
|
if (name && getRecipeNames().includes(name)) {
|
|
3579
3873
|
if (!options.json) {
|
|
3580
3874
|
console.error(`${yellow(glyph.warn)} ${dim(`"pjangler init ${name}" is deprecated \u2014 use "pjangler add ${name}". Forwarding\u2026`)}`);
|
|
@@ -3603,7 +3897,7 @@ program.command("list").description("List available subsystems").action(() => {
|
|
|
3603
3897
|
console.log("");
|
|
3604
3898
|
});
|
|
3605
3899
|
var projectCmd = program.command("project").description("Manage the pjangler project registry");
|
|
3606
|
-
projectCmd.command("init").argument("[name]", "Project display name").description("Plan or apply a registry-backed CommonProject initialization or legacy repo sync").option("--description <text>", "Project description").option("--target-dir <path>", "Target repo path").option("--source-skill <path>", "Source skill/template provenance path").option("--primary-language <language>", "Primary language for CommonProject rendering", "python").option("--provision-agent", "Plan local Hermes PM agent provisioning").option("--agent-role <role>", "Hermes agent role to plan when --provision-agent is set", "pm").option("--apply", "Write the registry and render the repo scaffold").option("--dry-run", "Preview changes without writing files (default)").option("--live", "Allow live/network/cloud provisioning actions").option("--slug <slug>", "Project registry slug override").option("--identifier <identifier>", "Ticket identifier override").option("--ticket-provider <type>", "Ticket provider: plane | trello", "plane").option("--board-id <id>", "Board id (Plane project UUID or Trello board id)").option("--board-url <url>", "
|
|
3900
|
+
projectCmd.command("init").argument("[name]", "Project display name").description("Plan or apply a registry-backed CommonProject initialization or legacy repo sync").option("--description <text>", "Project description").option("--target-dir <path>", "Target repo path").option("--source-skill <path>", "Source skill/template provenance path").option("--primary-language <language>", "Primary language for CommonProject rendering", "python").option("--provision-agent", "Plan local Hermes PM agent provisioning").option("--agent-role <role>", "Hermes agent role to plan when --provision-agent is set", "pm").option("--apply", "Write the registry and render the repo scaffold").option("--dry-run", "Preview changes without writing files (default)").option("--live", "Allow live/network/cloud provisioning actions").option("--slug <slug>", "Project registry slug override").option("--identifier <identifier>", "Ticket identifier override").option("--ticket-provider <type>", "Ticket provider: plane | trello", "plane").option("--board-id <id>", "Board id (Plane project UUID or Trello board id)").option("--board-url <url>", "Deprecated no-op; board URLs are derived from provider + workspace + board-id").option("--workspace <name>", "Ticket workspace/org (Plane workspace; blank for Trello)").option("--registry <path>", `Registry path override (default: ${projectRegistryPath()})`).option("-f, --force", "Allow replacing an existing registry entry and re-rendering files").option("-y, --yes", "Apply every proposed operation without prompting").option("--no-tui", "Disable interactive prompts").option("--json", "Output machine-parseable JSON").action((name, options) => {
|
|
3607
3901
|
if (!options.json) console.error(`${yellow(glyph.warn)} ${dim('"pjangler project init" is deprecated \u2014 use "pjangler init".')}`);
|
|
3608
3902
|
return runProjectInit(name, options);
|
|
3609
3903
|
});
|