@delorenj/pjangler 1.2.4 → 1.2.8
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 +797 -737
- package/dist/mcp-server.js +753 -683
- package/package.json +1 -1
- package/templates/commonproject/AGENTS.md +3 -3
- package/templates/commonproject/README.md +12 -11
- package/templates/commonproject/copier.yml +58 -10
- package/templates/commonproject/mise.toml +3 -3
- package/templates/commonproject/template/.agents/hooks/sync.py +12 -1
- package/templates/commonproject/template/.agents/local.example.json +3 -1
- package/templates/commonproject/template/.project.json.jinja +13 -6
- package/templates/commonproject/template/mise.toml.jinja +8 -4
- package/templates/hermes-agent/README.md +8 -8
- package/templates/hermes-agent/copier.yml +3 -4
- package/templates/hermes-agent/docs/architecture.md +4 -4
- package/templates/hermes-agent/docs/operations.md +2 -2
- package/templates/hermes-agent/docs/sentinel/README.md +6 -8
- package/templates/hermes-agent/docs/sentinel/architecture.md +2 -1
- package/templates/hermes-agent/docs/sentinel/development.md +13 -12
- package/templates/hermes-agent/docs/sentinel/providers.md +12 -32
- package/templates/hermes-agent/install-local.sh +6 -13
- package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +14 -37
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +2 -0
- package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +6 -2
- package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md +2 -2
- package/templates/hermes-agent/template/.scripts/sentinel/docs/continuous-ticket-orchestration.md +1 -1
- package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +6 -1
- package/templates/hermes-agent/template/SOUL.md.jinja +9 -3
- package/templates/hermes-agent/template/role.yaml.jinja +3 -6
- package/templates/hermes-agent/template/.scripts/40-plane.sh +0 -51
- package/templates/hermes-agent/template/.scripts/providers/linear.sh +0 -176
package/dist/mcp-server.js
CHANGED
|
@@ -713,7 +713,7 @@ function deriveProfileName(repo, role) {
|
|
|
713
713
|
function detectTicketProvider(targetDir) {
|
|
714
714
|
try {
|
|
715
715
|
const t = JSON.parse(readFileSync(join4(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
|
|
716
|
-
return t === "plane" || t === "
|
|
716
|
+
return t === "plane" || t === "trello" ? t : void 0;
|
|
717
717
|
} catch {
|
|
718
718
|
return void 0;
|
|
719
719
|
}
|
|
@@ -1187,70 +1187,478 @@ var HermesAgentRecipe = class extends Recipe {
|
|
|
1187
1187
|
};
|
|
1188
1188
|
|
|
1189
1189
|
// src/commands/AgentHooksCommands.ts
|
|
1190
|
-
import { homedir as
|
|
1191
|
-
import { join as
|
|
1192
|
-
import { existsSync as
|
|
1190
|
+
import { homedir as homedir4 } from "node:os";
|
|
1191
|
+
import { join as join9, dirname as dirname5 } from "node:path";
|
|
1192
|
+
import { existsSync as existsSync7, cpSync, mkdirSync as mkdirSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1193
1193
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1194
|
+
|
|
1195
|
+
// src/project/index.ts
|
|
1196
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
1197
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync2, renameSync, statSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1198
|
+
import { homedir as homedir3 } from "node:os";
|
|
1199
|
+
import { basename as basename2, dirname as dirname4, join as join8, resolve } from "node:path";
|
|
1200
|
+
import YAML from "yaml";
|
|
1201
|
+
var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
|
|
1202
|
+
var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
|
|
1203
|
+
var KNOWN_SKILL_ROOTS = [
|
|
1204
|
+
"/home/delorenj/code/skillex/all-skills",
|
|
1205
|
+
"/home/delorenj/code/CoachingAgentFramework/.agents/skills",
|
|
1206
|
+
"/home/delorenj/code/pjangler/.agents/skills",
|
|
1207
|
+
join8(homedir3(), ".codex", "skills")
|
|
1208
|
+
];
|
|
1209
|
+
function projectRegistryPath(env2 = process.env) {
|
|
1210
|
+
return expandHome(env2[PROJECT_REGISTRY_ENV] || join8(homedir3(), ".config", "pjangler", "projects.yaml"));
|
|
1211
|
+
}
|
|
1212
|
+
function emptyProjectRegistry() {
|
|
1213
|
+
return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
|
|
1214
|
+
}
|
|
1215
|
+
function loadProjectRegistry(path = projectRegistryPath()) {
|
|
1216
|
+
if (!existsSync6(path)) return emptyProjectRegistry();
|
|
1217
|
+
const raw = YAML.parse(readFileSync2(path, "utf8"));
|
|
1218
|
+
if (raw == null) return emptyProjectRegistry();
|
|
1219
|
+
if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
|
|
1220
|
+
const registry = raw;
|
|
1221
|
+
const normalized = {
|
|
1222
|
+
schema_version: Number(registry.schema_version ?? PROJECT_REGISTRY_SCHEMA_VERSION),
|
|
1223
|
+
projects: isRecord(registry.projects) ? registry.projects : {}
|
|
1224
|
+
};
|
|
1225
|
+
validateProjectRegistry(normalized);
|
|
1226
|
+
return normalized;
|
|
1227
|
+
}
|
|
1228
|
+
function saveProjectRegistry(registry, path = projectRegistryPath()) {
|
|
1229
|
+
validateProjectRegistry(registry);
|
|
1230
|
+
mkdirSync4(dirname4(path), { recursive: true });
|
|
1231
|
+
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
1232
|
+
writeFileSync3(temp, YAML.stringify(registry, { lineWidth: 0 }), "utf8");
|
|
1233
|
+
renameSync(temp, path);
|
|
1234
|
+
}
|
|
1235
|
+
function validateProjectRegistry(registry) {
|
|
1236
|
+
if (registry.schema_version !== PROJECT_REGISTRY_SCHEMA_VERSION) {
|
|
1237
|
+
throw new Error(`Unsupported project registry schema_version: ${registry.schema_version}`);
|
|
1198
1238
|
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1239
|
+
if (!isRecord(registry.projects)) throw new Error("Project registry projects must be a mapping");
|
|
1240
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
1241
|
+
const repoPaths = /* @__PURE__ */ new Map();
|
|
1242
|
+
const identifiers = /* @__PURE__ */ new Map();
|
|
1243
|
+
for (const [slug, project] of Object.entries(registry.projects)) {
|
|
1244
|
+
validateProjectRecord(project, slug);
|
|
1245
|
+
if (slugs.has(project.slug)) throw new Error(`Duplicate project slug: ${project.slug}`);
|
|
1246
|
+
slugs.add(project.slug);
|
|
1247
|
+
const repoKey = resolve(project.repo_path);
|
|
1248
|
+
const existingRepoSlug = repoPaths.get(repoKey);
|
|
1249
|
+
if (existingRepoSlug && existingRepoSlug !== slug) {
|
|
1250
|
+
throw new Error(`Duplicate project repo_path: ${project.repo_path} used by ${existingRepoSlug} and ${slug}`);
|
|
1251
|
+
}
|
|
1252
|
+
repoPaths.set(repoKey, slug);
|
|
1253
|
+
const identifier = project.ticket_provider.identifier?.toUpperCase();
|
|
1254
|
+
if (identifier) {
|
|
1255
|
+
const existingIdentifierSlug = identifiers.get(identifier);
|
|
1256
|
+
if (existingIdentifierSlug && existingIdentifierSlug !== slug) {
|
|
1257
|
+
throw new Error(`Duplicate project identifier: ${identifier} used by ${existingIdentifierSlug} and ${slug}`);
|
|
1258
|
+
}
|
|
1259
|
+
identifiers.set(identifier, slug);
|
|
1206
1260
|
}
|
|
1207
|
-
} catch {
|
|
1208
1261
|
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1262
|
+
}
|
|
1263
|
+
function normalizeTicketProvider(value) {
|
|
1264
|
+
const type = (value || "plane").trim().toLowerCase();
|
|
1265
|
+
if (type === "plane" || type === "trello") return type;
|
|
1266
|
+
throw new Error(`Unsupported ticket provider: ${value}. Supported providers: plane, trello`);
|
|
1267
|
+
}
|
|
1268
|
+
function buildTicketProviderBlock(input) {
|
|
1269
|
+
const type = normalizeTicketProvider(input.type);
|
|
1270
|
+
const boardId = input.boardId ?? "";
|
|
1271
|
+
if (type === "trello") {
|
|
1272
|
+
return {
|
|
1273
|
+
type,
|
|
1274
|
+
workspace: input.workspace ?? "",
|
|
1275
|
+
identifier: input.identifier,
|
|
1276
|
+
board_id: boardId,
|
|
1277
|
+
board_url: input.boardUrl ?? (boardId ? `https://trello.com/b/${boardId}` : ""),
|
|
1278
|
+
state: "planned"
|
|
1279
|
+
};
|
|
1212
1280
|
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1281
|
+
const workspace = input.workspace ?? "33god";
|
|
1282
|
+
return {
|
|
1283
|
+
type,
|
|
1284
|
+
workspace,
|
|
1285
|
+
identifier: input.identifier,
|
|
1286
|
+
board_id: boardId,
|
|
1287
|
+
board_url: input.boardUrl ?? (boardId ? `https://plane.delo.sh/${workspace}/projects/${boardId}/issues/` : ""),
|
|
1288
|
+
state: "planned"
|
|
1289
|
+
};
|
|
1216
1290
|
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1291
|
+
function slugifyProjectName(value) {
|
|
1292
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
1293
|
+
}
|
|
1294
|
+
function deriveProjectIdentifier(value) {
|
|
1295
|
+
const compact = value.replace(/[^A-Za-z0-9]/g, "").toUpperCase();
|
|
1296
|
+
const identifier = compact.slice(0, 4) || "PROJ";
|
|
1297
|
+
return identifier.length >= 2 ? identifier : `${identifier}XX`.slice(0, 4);
|
|
1298
|
+
}
|
|
1299
|
+
function normalizeAgentRole(value) {
|
|
1300
|
+
return value?.trim() || "pm";
|
|
1301
|
+
}
|
|
1302
|
+
function resolveAgentHooksLayer(input, env2 = process.env) {
|
|
1303
|
+
if (typeof input === "boolean") return input;
|
|
1304
|
+
const override = env2.PJ_AGENT_HOOKS_LAYER;
|
|
1305
|
+
if (override === "0" || override === "false") return false;
|
|
1306
|
+
if (override === "1" || override === "true") return true;
|
|
1307
|
+
return !existsSync6(join8(homedir3(), ".agents", "hooks"));
|
|
1308
|
+
}
|
|
1309
|
+
function jsonStable(value) {
|
|
1310
|
+
return JSON.stringify(value);
|
|
1311
|
+
}
|
|
1312
|
+
function projectRecordEquivalent(a, b) {
|
|
1313
|
+
if (!a) return false;
|
|
1314
|
+
const { created_at: _aCreated, updated_at: _aUpdated, ...aComparable } = a;
|
|
1315
|
+
const { created_at: _bCreated, updated_at: _bUpdated, ...bComparable } = b;
|
|
1316
|
+
return jsonStable(aComparable) === jsonStable(bComparable);
|
|
1317
|
+
}
|
|
1318
|
+
function defaultProjectTargetDir(name, cwd = process.cwd()) {
|
|
1319
|
+
const compactName = name.replace(/[^A-Za-z0-9._-]/g, "") || slugifyProjectName(name);
|
|
1320
|
+
return resolve(dirname4(resolve(cwd)), compactName);
|
|
1321
|
+
}
|
|
1322
|
+
function resolveSourceSkillPath(sourceSkill) {
|
|
1323
|
+
if (!sourceSkill) return void 0;
|
|
1324
|
+
const expanded = expandHome(sourceSkill);
|
|
1325
|
+
const direct = resolve(expanded);
|
|
1326
|
+
if (existsSync6(direct)) return direct;
|
|
1327
|
+
const name = basename2(sourceSkill);
|
|
1328
|
+
for (const root of KNOWN_SKILL_ROOTS) {
|
|
1329
|
+
const candidate = join8(root, name);
|
|
1330
|
+
if (existsSync6(candidate)) return candidate;
|
|
1331
|
+
}
|
|
1332
|
+
const civilWarLetterifier = "/home/delorenj/code/skillex/all-skills/civilwar-letterifier";
|
|
1333
|
+
const hint = existsSync6(civilWarLetterifier) ? ` Did you mean ${civilWarLetterifier}?` : "";
|
|
1334
|
+
throw new Error(`Source skill not found: ${sourceSkill}.${hint}`);
|
|
1335
|
+
}
|
|
1336
|
+
function planProjectInit(input) {
|
|
1337
|
+
if (!input.name.trim()) throw new Error("Project name is required");
|
|
1338
|
+
const registryPath2 = resolve(projectRegistryPath({ ...process.env, [PROJECT_REGISTRY_ENV]: input.registryPath || process.env[PROJECT_REGISTRY_ENV] }));
|
|
1339
|
+
const registry = loadProjectRegistry(registryPath2);
|
|
1340
|
+
const now = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1341
|
+
const slug = input.projectSlug ?? slugifyProjectName(input.name);
|
|
1342
|
+
const targetDir = resolve(input.targetDir ?? defaultProjectTargetDir(input.name, input.cwd));
|
|
1343
|
+
const identifier = (input.projectIdentifier ?? deriveProjectIdentifier(input.name)).toUpperCase();
|
|
1344
|
+
const existing = registry.projects[slug];
|
|
1345
|
+
const sourceSkillPath = resolveSourceSkillPath(input.sourceSkill);
|
|
1346
|
+
const overwrite = input.overwrite ?? input.force ?? false;
|
|
1347
|
+
const agentRole = normalizeAgentRole(input.agentRole);
|
|
1348
|
+
const agents = input.provisionAgent ? {
|
|
1349
|
+
...existing?.agents ?? {},
|
|
1350
|
+
[agentRole]: {
|
|
1351
|
+
role: agentRole,
|
|
1352
|
+
provisioning_state: "planned"
|
|
1224
1353
|
}
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
if (existsSync6(dest) && !this.context.force) {
|
|
1239
|
-
skipped.push(rel);
|
|
1240
|
-
continue;
|
|
1354
|
+
} : existing?.agents ?? {};
|
|
1355
|
+
const scaffold = input.scaffold ?? true;
|
|
1356
|
+
const candidateProject = {
|
|
1357
|
+
name: input.name,
|
|
1358
|
+
slug,
|
|
1359
|
+
repo_path: targetDir,
|
|
1360
|
+
description: input.description ?? "",
|
|
1361
|
+
status: "planned",
|
|
1362
|
+
source_artifacts: sourceSkillPath ? [{ kind: "skill", path: sourceSkillPath, package_name: input.packageName ?? slug }] : [],
|
|
1363
|
+
template: {
|
|
1364
|
+
commonproject: {
|
|
1365
|
+
enabled: true,
|
|
1366
|
+
primary_language: input.primaryLanguage ?? "python"
|
|
1241
1367
|
}
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1368
|
+
},
|
|
1369
|
+
ticket_provider: buildTicketProviderBlock({
|
|
1370
|
+
type: input.ticketProvider ?? "plane",
|
|
1371
|
+
identifier,
|
|
1372
|
+
boardId: input.boardId ?? input.planeProjectId,
|
|
1373
|
+
boardUrl: input.boardUrl,
|
|
1374
|
+
workspace: input.boardWorkspace ?? input.planeWorkspace
|
|
1375
|
+
}),
|
|
1376
|
+
agents,
|
|
1377
|
+
created_at: existing?.created_at ?? now,
|
|
1378
|
+
updated_at: now
|
|
1379
|
+
};
|
|
1380
|
+
const project = {
|
|
1381
|
+
...candidateProject,
|
|
1382
|
+
updated_at: projectRecordEquivalent(existing, candidateProject) ? existing.updated_at : now
|
|
1383
|
+
};
|
|
1384
|
+
validateNoDuplicateProject(registry, project, overwrite);
|
|
1385
|
+
const pjanglerRoot = resolve(input.pjanglerRoot ?? resolvePjanglerRoot());
|
|
1386
|
+
const manifest = projectManifestFromRegistryProject(project);
|
|
1387
|
+
const apply = input.apply ?? false;
|
|
1388
|
+
const live = input.live ?? false;
|
|
1389
|
+
const actions = [
|
|
1390
|
+
{ kind: "registry.upsert", registryPath: registryPath2, slug, project }
|
|
1391
|
+
];
|
|
1392
|
+
if (scaffold) {
|
|
1393
|
+
actions.push(buildCommonProjectCopierAction({
|
|
1394
|
+
pjanglerRoot,
|
|
1395
|
+
targetDir,
|
|
1396
|
+
projectName: project.name,
|
|
1397
|
+
projectDescription: project.description,
|
|
1398
|
+
projectSlug: project.slug,
|
|
1399
|
+
ticketProvider: project.ticket_provider.type,
|
|
1400
|
+
planeWorkspace: project.ticket_provider.workspace ?? "33god",
|
|
1401
|
+
planeProjectId: project.ticket_provider.board_id ?? "",
|
|
1402
|
+
ticketWorkspace: project.ticket_provider.workspace ?? "",
|
|
1403
|
+
boardId: project.ticket_provider.board_id ?? "",
|
|
1404
|
+
boardUrl: project.ticket_provider.board_url ?? "",
|
|
1405
|
+
projectIdentifier: identifier,
|
|
1406
|
+
primaryLanguage: project.template.commonproject.primary_language,
|
|
1407
|
+
agentHooksLayer: resolveAgentHooksLayer(input.agentHooksLayer),
|
|
1408
|
+
overwrite
|
|
1409
|
+
}));
|
|
1410
|
+
}
|
|
1411
|
+
actions.push(
|
|
1412
|
+
{ kind: "project.write-manifest", path: join8(targetDir, ".project.json"), manifest },
|
|
1413
|
+
{
|
|
1414
|
+
kind: "ticket-provider.create-or-link",
|
|
1415
|
+
enabled: live,
|
|
1416
|
+
live,
|
|
1417
|
+
provider: project.ticket_provider.type,
|
|
1418
|
+
workspace: project.ticket_provider.workspace ?? "33god",
|
|
1419
|
+
identifier,
|
|
1420
|
+
state: live ? "planned" : "planned",
|
|
1421
|
+
reason: live ? void 0 : "network/cloud actions require --live"
|
|
1422
|
+
},
|
|
1423
|
+
{
|
|
1424
|
+
kind: "hermes.provision-agent",
|
|
1425
|
+
enabled: input.provisionAgent ?? false,
|
|
1426
|
+
local: !live,
|
|
1427
|
+
targetDir,
|
|
1428
|
+
targetRepo: slug,
|
|
1429
|
+
role: agentRole,
|
|
1430
|
+
context: {
|
|
1431
|
+
skipRuntimeRepo: !live,
|
|
1432
|
+
skipPlane: !live,
|
|
1433
|
+
skipBloodbank: !live,
|
|
1434
|
+
skipSystemd: !live || process.platform === "darwin"
|
|
1245
1435
|
}
|
|
1246
|
-
created.push(rel);
|
|
1247
1436
|
}
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1437
|
+
);
|
|
1438
|
+
return { ok: true, apply, dryRun: !apply, live, registryPath: registryPath2, project, manifest, actions };
|
|
1439
|
+
}
|
|
1440
|
+
function executeProjectInitPlan(plan) {
|
|
1441
|
+
const logs = [];
|
|
1442
|
+
const errors = [];
|
|
1443
|
+
const changedFiles = [];
|
|
1444
|
+
if (!plan.apply) return { ok: true, plan, logs, errors, changedFiles };
|
|
1445
|
+
const registry = loadProjectRegistry(plan.registryPath);
|
|
1446
|
+
let pendingRegistryAction;
|
|
1447
|
+
for (const action of plan.actions) {
|
|
1448
|
+
if (action.kind === "copier.copy.commonproject") {
|
|
1449
|
+
logs.push(
|
|
1450
|
+
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
|
+
);
|
|
1452
|
+
mkdirSync4(dirname4(action.targetDir), { recursive: true });
|
|
1453
|
+
const result = spawnSync4(action.command[0], action.command.slice(1), { encoding: "utf8", cwd: action.cwd });
|
|
1454
|
+
if (result.stdout?.trim()) logs.push(result.stdout.trim());
|
|
1455
|
+
if (result.stderr?.trim()) logs.push(result.stderr.trim());
|
|
1456
|
+
if (result.error) {
|
|
1457
|
+
const code = result.error.code;
|
|
1458
|
+
errors.push(
|
|
1459
|
+
code === "ENOENT" ? "copier not found on PATH. Install with: uv tool install copier or pip install copier" : `copier failed: ${result.error.message}`
|
|
1460
|
+
);
|
|
1461
|
+
break;
|
|
1462
|
+
}
|
|
1463
|
+
if (result.status !== 0) {
|
|
1464
|
+
errors.push(`copier exited with status ${result.status ?? "unknown"}`);
|
|
1465
|
+
if (existsSync6(action.targetDir)) changedFiles.push(action.targetDir);
|
|
1466
|
+
break;
|
|
1467
|
+
}
|
|
1468
|
+
changedFiles.push(action.targetDir);
|
|
1469
|
+
} else if (action.kind === "project.write-manifest") {
|
|
1470
|
+
mkdirSync4(dirname4(action.path), { recursive: true });
|
|
1471
|
+
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
1472
|
+
`;
|
|
1473
|
+
const current = existsSync6(action.path) ? readFileSync2(action.path, "utf8") : void 0;
|
|
1474
|
+
if (current !== next) {
|
|
1475
|
+
writeFileSync3(action.path, next, "utf8");
|
|
1476
|
+
changedFiles.push(action.path);
|
|
1477
|
+
}
|
|
1478
|
+
} else if (action.kind === "registry.upsert") {
|
|
1479
|
+
pendingRegistryAction = action;
|
|
1480
|
+
} else if (action.kind === "ticket-provider.create-or-link") {
|
|
1481
|
+
logs.push(action.enabled ? "ticket-provider.create-or-link requires a live provider integration" : "ticket-provider.create-or-link skipped (requires --live)");
|
|
1482
|
+
} else if (action.kind === "hermes.provision-agent") {
|
|
1483
|
+
logs.push(action.enabled ? "hermes.provision-agent planned for the caller to execute" : "hermes.provision-agent skipped");
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
if (pendingRegistryAction && errors.length === 0) {
|
|
1487
|
+
if (!projectRecordEquivalent(registry.projects[pendingRegistryAction.slug], pendingRegistryAction.project)) {
|
|
1488
|
+
registry.projects[pendingRegistryAction.slug] = pendingRegistryAction.project;
|
|
1489
|
+
saveProjectRegistry(registry, pendingRegistryAction.registryPath);
|
|
1490
|
+
changedFiles.push(pendingRegistryAction.registryPath);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
return { ok: errors.length === 0, plan, logs, errors, changedFiles };
|
|
1494
|
+
}
|
|
1495
|
+
function projectManifestFromRegistryProject(project) {
|
|
1496
|
+
const agents = Object.fromEntries(
|
|
1497
|
+
Object.entries(project.agents).map(([name, agent]) => [
|
|
1498
|
+
`${project.slug}-${name}`,
|
|
1499
|
+
{
|
|
1500
|
+
role: agent.role,
|
|
1501
|
+
role_dir: agent.role_dir,
|
|
1502
|
+
provisioning_state: agent.provisioning_state
|
|
1503
|
+
}
|
|
1504
|
+
])
|
|
1505
|
+
);
|
|
1506
|
+
return {
|
|
1507
|
+
project_name: project.name,
|
|
1508
|
+
project_description: project.description,
|
|
1509
|
+
project_slug: project.slug,
|
|
1510
|
+
repo_path: project.repo_path,
|
|
1511
|
+
ticket_provider: {
|
|
1512
|
+
type: project.ticket_provider.type,
|
|
1513
|
+
workspace: project.ticket_provider.workspace ?? "",
|
|
1514
|
+
identifier: project.ticket_provider.identifier ?? "",
|
|
1515
|
+
board_id: project.ticket_provider.board_id ?? "",
|
|
1516
|
+
board_url: project.ticket_provider.board_url ?? "",
|
|
1517
|
+
state: project.ticket_provider.state
|
|
1518
|
+
},
|
|
1519
|
+
agents
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
function getProject(registry, slug) {
|
|
1523
|
+
const project = registry.projects[slug];
|
|
1524
|
+
if (!project) throw new Error(`Project not found in registry: ${slug}`);
|
|
1525
|
+
return project;
|
|
1526
|
+
}
|
|
1527
|
+
function buildCommonProjectCopierAction(input) {
|
|
1528
|
+
const templateDir = join8(input.pjanglerRoot, "templates", "commonproject");
|
|
1529
|
+
const data = {
|
|
1530
|
+
project_name: input.projectName,
|
|
1531
|
+
project_description: input.projectDescription ?? "",
|
|
1532
|
+
project_slug: input.projectSlug,
|
|
1533
|
+
ticket_provider: input.ticketProvider,
|
|
1534
|
+
plane_workspace: input.planeWorkspace,
|
|
1535
|
+
plane_project_id: input.planeProjectId ?? "",
|
|
1536
|
+
ticket_workspace: input.ticketWorkspace ?? input.planeWorkspace,
|
|
1537
|
+
board_id: input.boardId ?? input.planeProjectId ?? "",
|
|
1538
|
+
board_url: input.boardUrl ?? "",
|
|
1539
|
+
project_identifier: input.projectIdentifier,
|
|
1540
|
+
primary_language: input.primaryLanguage,
|
|
1541
|
+
agent_hooks_layer: input.agentHooksLayer ?? true ? "true" : "false"
|
|
1542
|
+
};
|
|
1543
|
+
const command = ["copier", "copy", "--trust", templateDir, input.targetDir, "--defaults"];
|
|
1544
|
+
for (const [key, value] of Object.entries(data)) command.push("--data", `${key}=${value}`);
|
|
1545
|
+
if (input.overwrite) command.push("--overwrite");
|
|
1546
|
+
return {
|
|
1547
|
+
kind: "copier.copy.commonproject",
|
|
1548
|
+
cwd: input.pjanglerRoot,
|
|
1549
|
+
command,
|
|
1550
|
+
targetDir: input.targetDir,
|
|
1551
|
+
data,
|
|
1552
|
+
overwrite: input.overwrite
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
function resolvePjanglerRoot() {
|
|
1556
|
+
let dir = dirname4(new URL(import.meta.url).pathname);
|
|
1557
|
+
while (dir !== dirname4(dir)) {
|
|
1558
|
+
if (existsSync6(join8(dir, "package.json")) && existsSync6(join8(dir, "templates", "commonproject", "copier.yml"))) return dir;
|
|
1559
|
+
dir = dirname4(dir);
|
|
1560
|
+
}
|
|
1561
|
+
return resolve(process.cwd());
|
|
1562
|
+
}
|
|
1563
|
+
function validateNoDuplicateProject(registry, project, overwrite) {
|
|
1564
|
+
const existingSameSlug = registry.projects[project.slug];
|
|
1565
|
+
if (existingSameSlug && !overwrite && resolve(existingSameSlug.repo_path) !== resolve(project.repo_path)) {
|
|
1566
|
+
throw new Error(`Project slug already exists in registry: ${project.slug}`);
|
|
1567
|
+
}
|
|
1568
|
+
for (const [slug, existing] of Object.entries(registry.projects)) {
|
|
1569
|
+
if (slug === project.slug) continue;
|
|
1570
|
+
if (resolve(existing.repo_path) === resolve(project.repo_path)) {
|
|
1571
|
+
throw new Error(`Project repo_path already registered by ${slug}: ${project.repo_path}`);
|
|
1572
|
+
}
|
|
1573
|
+
if (existing.ticket_provider.identifier && existing.ticket_provider.identifier.toUpperCase() === project.ticket_provider.identifier?.toUpperCase()) {
|
|
1574
|
+
throw new Error(`Project identifier already registered by ${slug}: ${project.ticket_provider.identifier}`);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
function validateProjectRecord(project, key) {
|
|
1579
|
+
if (!isRecord(project)) throw new Error(`Project ${key} must be a mapping`);
|
|
1580
|
+
if (!project.name) throw new Error(`Project ${key} missing name`);
|
|
1581
|
+
if (!project.slug) throw new Error(`Project ${key} missing slug`);
|
|
1582
|
+
if (project.slug !== key) throw new Error(`Project key ${key} does not match slug ${project.slug}`);
|
|
1583
|
+
if (!project.repo_path) throw new Error(`Project ${key} missing repo_path`);
|
|
1584
|
+
if (!Array.isArray(project.source_artifacts)) throw new Error(`Project ${key} source_artifacts must be a list`);
|
|
1585
|
+
if (!isRecord(project.ticket_provider)) throw new Error(`Project ${key} ticket_provider must be a mapping`);
|
|
1586
|
+
if (!isRecord(project.agents)) throw new Error(`Project ${key} agents must be a mapping`);
|
|
1587
|
+
}
|
|
1588
|
+
function expandHome(path) {
|
|
1589
|
+
if (path === "~") return homedir3();
|
|
1590
|
+
if (path.startsWith("~/")) return join8(homedir3(), path.slice(2));
|
|
1591
|
+
return path;
|
|
1592
|
+
}
|
|
1593
|
+
function isRecord(value) {
|
|
1594
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// src/commands/AgentHooksCommands.ts
|
|
1598
|
+
var AGENT_HOOKS_SKIP_MESSAGE = "\u21B7 agent-hooks layer skipped: global ~/.agents/hooks detected (these hooks already run globally).\n Set PJ_AGENT_HOOKS_LAYER=1 to install the project-scoped layer anyway.";
|
|
1599
|
+
function resolveTemplateRoot() {
|
|
1600
|
+
const candidates = [];
|
|
1601
|
+
if (process.env.PJANGLER_COMMONPROJECT_TEMPLATE) {
|
|
1602
|
+
candidates.push(process.env.PJANGLER_COMMONPROJECT_TEMPLATE);
|
|
1603
|
+
}
|
|
1604
|
+
try {
|
|
1605
|
+
let dir = dirname5(fileURLToPath2(import.meta.url));
|
|
1606
|
+
for (let i = 0; i < 8; i++) {
|
|
1607
|
+
candidates.push(join9(dir, "templates", "commonproject", "template"));
|
|
1608
|
+
const parent = dirname5(dir);
|
|
1609
|
+
if (parent === dir) break;
|
|
1610
|
+
dir = parent;
|
|
1611
|
+
}
|
|
1612
|
+
} catch {
|
|
1613
|
+
}
|
|
1614
|
+
candidates.push(join9(homedir4(), "code", "pjangler", "templates", "commonproject", "template"));
|
|
1615
|
+
for (const c of candidates) {
|
|
1616
|
+
if (existsSync7(join9(c, ".agents", "hooks", "hooks.master.json"))) return c;
|
|
1617
|
+
}
|
|
1618
|
+
throw new Error(
|
|
1619
|
+
"Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
var CopyAgentHooksTree = class extends Command {
|
|
1623
|
+
async invoke() {
|
|
1624
|
+
if (!resolveAgentHooksLayer()) {
|
|
1625
|
+
return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
|
|
1626
|
+
}
|
|
1627
|
+
let templateRoot;
|
|
1628
|
+
try {
|
|
1629
|
+
templateRoot = resolveTemplateRoot();
|
|
1630
|
+
} catch (e) {
|
|
1631
|
+
return { success: false, message: `\u26A0\uFE0F ${e.message}` };
|
|
1632
|
+
}
|
|
1633
|
+
const items = [
|
|
1634
|
+
{ rel: ".agents/hooks", dir: true },
|
|
1635
|
+
{ rel: ".agents/local.example.json", dir: false },
|
|
1636
|
+
{ rel: ".mise/scripts/link-project-skills-to-clis.sh", dir: false },
|
|
1637
|
+
{ rel: ".mise/scripts/unlink-project-skills-from-clis.sh", dir: false },
|
|
1638
|
+
{ rel: ".mise/scripts/hindsight-setup.sh", dir: false }
|
|
1639
|
+
];
|
|
1640
|
+
const created = [];
|
|
1641
|
+
const skipped = [];
|
|
1642
|
+
for (const { rel, dir } of items) {
|
|
1643
|
+
const src = join9(templateRoot, rel);
|
|
1644
|
+
const dest = join9(this.context.targetDir, rel);
|
|
1645
|
+
if (!existsSync7(src)) continue;
|
|
1646
|
+
if (existsSync7(dest) && !this.context.force) {
|
|
1647
|
+
skipped.push(rel);
|
|
1648
|
+
continue;
|
|
1649
|
+
}
|
|
1650
|
+
if (!this.context.dryRun) {
|
|
1651
|
+
mkdirSync5(dirname5(dest), { recursive: true });
|
|
1652
|
+
cpSync(src, dest, { recursive: dir, force: true });
|
|
1653
|
+
}
|
|
1654
|
+
created.push(rel);
|
|
1655
|
+
}
|
|
1656
|
+
const verb = this.context.dryRun ? "Would copy" : "Copied";
|
|
1657
|
+
const tail = skipped.length ? ` (${skipped.length} already present \u2014 use --force to overwrite)` : "";
|
|
1658
|
+
return {
|
|
1659
|
+
success: created.length > 0,
|
|
1660
|
+
message: this.formatMessage(`\u2705 ${verb} ${created.length} agent-hooks path(s)${tail}`)
|
|
1661
|
+
};
|
|
1254
1662
|
}
|
|
1255
1663
|
};
|
|
1256
1664
|
var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
@@ -1258,14 +1666,17 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
|
1258
1666
|
static CR = "{{config_root}}";
|
|
1259
1667
|
// mise's own runtime var — emitted literally
|
|
1260
1668
|
async invoke() {
|
|
1261
|
-
|
|
1262
|
-
|
|
1669
|
+
if (!resolveAgentHooksLayer()) {
|
|
1670
|
+
return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
|
|
1671
|
+
}
|
|
1672
|
+
const misePath = join9(this.context.targetDir, "mise.toml");
|
|
1673
|
+
if (!existsSync7(misePath)) {
|
|
1263
1674
|
return {
|
|
1264
1675
|
success: false,
|
|
1265
1676
|
message: "\u26A0\uFE0F No mise.toml found \u2014 run `pjangler init mise` first, then re-run."
|
|
1266
1677
|
};
|
|
1267
1678
|
}
|
|
1268
|
-
let content =
|
|
1679
|
+
let content = readFileSync3(misePath, "utf8");
|
|
1269
1680
|
if (content.includes(_WireMiseAgentHooks.MARKER)) {
|
|
1270
1681
|
return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
|
|
1271
1682
|
}
|
|
@@ -1341,7 +1752,7 @@ ${leaveBlock}`);
|
|
|
1341
1752
|
""
|
|
1342
1753
|
].join("\n");
|
|
1343
1754
|
content = content.replace(/\n*$/, "\n") + appended;
|
|
1344
|
-
if (!this.context.dryRun)
|
|
1755
|
+
if (!this.context.dryRun) writeFileSync4(misePath, content);
|
|
1345
1756
|
if (wiredHooks) {
|
|
1346
1757
|
return { success: true, message: this.formatMessage("\u2705 Wired mise.toml ([hooks] enter/leave + tasks)") };
|
|
1347
1758
|
}
|
|
@@ -1495,18 +1906,18 @@ function createRecipe(name, context) {
|
|
|
1495
1906
|
}
|
|
1496
1907
|
|
|
1497
1908
|
// src/utils/version.ts
|
|
1498
|
-
import { readFileSync as
|
|
1499
|
-
import { dirname as
|
|
1909
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
1910
|
+
import { dirname as dirname6, join as join10 } from "node:path";
|
|
1500
1911
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1501
1912
|
var PJANGLER_VERSION = (() => {
|
|
1502
1913
|
try {
|
|
1503
|
-
let dir =
|
|
1914
|
+
let dir = dirname6(fileURLToPath3(import.meta.url));
|
|
1504
1915
|
for (let i = 0; i < 4; i++) {
|
|
1505
1916
|
try {
|
|
1506
|
-
const raw =
|
|
1917
|
+
const raw = readFileSync4(join10(dir, "package.json"), "utf8");
|
|
1507
1918
|
return JSON.parse(raw).version ?? "0.0.0";
|
|
1508
1919
|
} catch {
|
|
1509
|
-
const parent =
|
|
1920
|
+
const parent = dirname6(dir);
|
|
1510
1921
|
if (parent === dir) break;
|
|
1511
1922
|
dir = parent;
|
|
1512
1923
|
}
|
|
@@ -1517,11 +1928,11 @@ var PJANGLER_VERSION = (() => {
|
|
|
1517
1928
|
})();
|
|
1518
1929
|
|
|
1519
1930
|
// src/parity/index.ts
|
|
1520
|
-
import { existsSync as
|
|
1521
|
-
import { basename as
|
|
1931
|
+
import { existsSync as existsSync8, lstatSync, mkdirSync as mkdirSync6, readFileSync as readFileSync5, readlinkSync, readdirSync, renameSync as renameSync2, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5, chmodSync as chmodSync2, copyFileSync } from "node:fs";
|
|
1932
|
+
import { basename as basename3, dirname as dirname7, join as join11, relative, resolve as resolve2 } from "node:path";
|
|
1522
1933
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
1523
|
-
import { homedir as
|
|
1524
|
-
import { spawnSync as
|
|
1934
|
+
import { homedir as homedir5 } from "node:os";
|
|
1935
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
1525
1936
|
var LINK_AGENTFILES_BLOCK = `# This block will handle the linking of
|
|
1526
1937
|
# agent files to the main AGENTS.md file.
|
|
1527
1938
|
#
|
|
@@ -1589,13 +2000,13 @@ run = "{{config_root}}/.mise/scripts/versioning.sh check"
|
|
|
1589
2000
|
description = "Force every versioned file up to the highest version"
|
|
1590
2001
|
run = "{{config_root}}/.mise/scripts/versioning.sh sync"
|
|
1591
2002
|
# <<< mise-versioning <<<`;
|
|
1592
|
-
function
|
|
1593
|
-
let dir =
|
|
1594
|
-
while (dir !==
|
|
1595
|
-
if (
|
|
2003
|
+
function resolvePjanglerRoot2() {
|
|
2004
|
+
let dir = dirname7(fileURLToPath4(import.meta.url));
|
|
2005
|
+
while (dir !== dirname7(dir)) {
|
|
2006
|
+
if (existsSync8(join11(dir, "package.json")) && existsSync8(join11(dir, "templates", "commonproject", "copier.yml"))) {
|
|
1596
2007
|
return dir;
|
|
1597
2008
|
}
|
|
1598
|
-
dir =
|
|
2009
|
+
dir = dirname7(dir);
|
|
1599
2010
|
}
|
|
1600
2011
|
throw new Error("Unable to resolve pjangler root");
|
|
1601
2012
|
}
|
|
@@ -1603,17 +2014,17 @@ function normalizeNewlines(value) {
|
|
|
1603
2014
|
return value.replace(/\r\n/g, "\n");
|
|
1604
2015
|
}
|
|
1605
2016
|
function readText(path) {
|
|
1606
|
-
return normalizeNewlines(
|
|
2017
|
+
return normalizeNewlines(readFileSync5(path, "utf8"));
|
|
1607
2018
|
}
|
|
1608
2019
|
function safeReadText(path) {
|
|
1609
|
-
return
|
|
2020
|
+
return existsSync8(path) ? readText(path) : null;
|
|
1610
2021
|
}
|
|
1611
2022
|
function ensureParent(path) {
|
|
1612
|
-
|
|
2023
|
+
mkdirSync6(dirname7(path), { recursive: true });
|
|
1613
2024
|
}
|
|
1614
2025
|
function writeText(path, content) {
|
|
1615
2026
|
ensureParent(path);
|
|
1616
|
-
|
|
2027
|
+
writeFileSync5(path, content);
|
|
1617
2028
|
}
|
|
1618
2029
|
function tryParseJson(text2) {
|
|
1619
2030
|
if (!text2) return null;
|
|
@@ -1630,7 +2041,7 @@ function titleCaseSlug(slug) {
|
|
|
1630
2041
|
return slug.split(/[-_]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
1631
2042
|
}
|
|
1632
2043
|
function readSymlinkTarget(path) {
|
|
1633
|
-
if (!
|
|
2044
|
+
if (!existsSync8(path)) return null;
|
|
1634
2045
|
try {
|
|
1635
2046
|
return readlinkSync(path);
|
|
1636
2047
|
} catch {
|
|
@@ -1638,7 +2049,7 @@ function readSymlinkTarget(path) {
|
|
|
1638
2049
|
}
|
|
1639
2050
|
}
|
|
1640
2051
|
function ensureSymlink(path, target, dryRun) {
|
|
1641
|
-
if (
|
|
2052
|
+
if (existsSync8(path)) {
|
|
1642
2053
|
const stat = lstatSync(path);
|
|
1643
2054
|
if (stat.isSymbolicLink()) {
|
|
1644
2055
|
const current = readSymlinkTarget(path);
|
|
@@ -1655,21 +2066,21 @@ function ensureSymlink(path, target, dryRun) {
|
|
|
1655
2066
|
return { changed: true };
|
|
1656
2067
|
}
|
|
1657
2068
|
function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
1658
|
-
const agentsPath =
|
|
1659
|
-
if (
|
|
2069
|
+
const agentsPath = join11(repoRoot, "AGENTS.md");
|
|
2070
|
+
if (existsSync8(agentsPath)) return { changedFiles: [], details: [] };
|
|
1660
2071
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
1661
|
-
const source =
|
|
1662
|
-
if (!
|
|
2072
|
+
const source = join11(repoRoot, file);
|
|
2073
|
+
if (!existsSync8(source)) continue;
|
|
1663
2074
|
const stat = lstatSync(source);
|
|
1664
2075
|
if (stat.isSymbolicLink()) continue;
|
|
1665
2076
|
if (stat.isFile()) {
|
|
1666
|
-
if (!dryRun)
|
|
2077
|
+
if (!dryRun) renameSync2(source, agentsPath);
|
|
1667
2078
|
return { changedFiles: [agentsPath], details: [`Moved ${file} to AGENTS.md before wiring agent-file symlinks`] };
|
|
1668
2079
|
}
|
|
1669
2080
|
return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
|
|
1670
2081
|
}
|
|
1671
|
-
const readmePath =
|
|
1672
|
-
if (
|
|
2082
|
+
const readmePath = join11(repoRoot, "README.md");
|
|
2083
|
+
if (existsSync8(readmePath)) {
|
|
1673
2084
|
const stat = lstatSync(readmePath);
|
|
1674
2085
|
if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
|
|
1675
2086
|
if (!dryRun) copyFileSync(readmePath, agentsPath);
|
|
@@ -1708,12 +2119,12 @@ function yamlGet(text2, keyPath) {
|
|
|
1708
2119
|
return "";
|
|
1709
2120
|
}
|
|
1710
2121
|
function discoverRoles(repoRoot) {
|
|
1711
|
-
const rolesDir =
|
|
1712
|
-
if (!
|
|
2122
|
+
const rolesDir = join11(repoRoot, "agents", "hermes");
|
|
2123
|
+
if (!existsSync8(rolesDir)) return [];
|
|
1713
2124
|
return readdirSync(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
|
|
1714
|
-
const roleDir =
|
|
1715
|
-
const roleYamlPath =
|
|
1716
|
-
if (!
|
|
2125
|
+
const roleDir = join11(rolesDir, entry.name);
|
|
2126
|
+
const roleYamlPath = join11(roleDir, "role.yaml");
|
|
2127
|
+
if (!existsSync8(roleYamlPath)) return null;
|
|
1717
2128
|
const text2 = readText(roleYamlPath);
|
|
1718
2129
|
const runtimeRepoRaw = yamlGet(text2, "runtime.github_repo");
|
|
1719
2130
|
return {
|
|
@@ -1737,10 +2148,10 @@ function discoverRoles(repoRoot) {
|
|
|
1737
2148
|
}).filter((value) => Boolean(value));
|
|
1738
2149
|
}
|
|
1739
2150
|
function registryPath(homeDir) {
|
|
1740
|
-
return
|
|
2151
|
+
return join11(homeDir, ".hermes", "agents-registry.yaml");
|
|
1741
2152
|
}
|
|
1742
2153
|
function systemctlUser(args) {
|
|
1743
|
-
const result =
|
|
2154
|
+
const result = spawnSync5("systemctl", ["--user", ...args], { encoding: "utf8" });
|
|
1744
2155
|
return {
|
|
1745
2156
|
ok: result.status === 0,
|
|
1746
2157
|
stdout: result.stdout.trim(),
|
|
@@ -1748,8 +2159,8 @@ function systemctlUser(args) {
|
|
|
1748
2159
|
};
|
|
1749
2160
|
}
|
|
1750
2161
|
function templateScript(ctx, name) {
|
|
1751
|
-
const source =
|
|
1752
|
-
return
|
|
2162
|
+
const source = join11(ctx.pjanglerRoot, ".mise", "scripts", name);
|
|
2163
|
+
return existsSync8(source) ? readText(source) : void 0;
|
|
1753
2164
|
}
|
|
1754
2165
|
function templateVersioningScript(ctx) {
|
|
1755
2166
|
return templateScript(ctx, "versioning.sh");
|
|
@@ -1759,14 +2170,14 @@ function templateLinkAgentfilesScript(ctx) {
|
|
|
1759
2170
|
}
|
|
1760
2171
|
function renderGeneratedProjectMiseToml(ctx, template) {
|
|
1761
2172
|
const project = readProjectJson(ctx);
|
|
1762
|
-
const projectName = String(project?.project_name ??
|
|
2173
|
+
const projectName = String(project?.project_name ?? basename3(ctx.repoRoot) ?? "project");
|
|
1763
2174
|
return template.replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
|
|
1764
2175
|
}
|
|
1765
2176
|
function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
1766
|
-
const targetPath =
|
|
1767
|
-
if (
|
|
1768
|
-
const sourcePath =
|
|
1769
|
-
if (!
|
|
2177
|
+
const targetPath = join11(ctx.repoRoot, "mise.toml");
|
|
2178
|
+
if (existsSync8(targetPath)) return false;
|
|
2179
|
+
const sourcePath = join11(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
|
|
2180
|
+
if (!existsSync8(sourcePath)) return false;
|
|
1770
2181
|
changedFiles.push(targetPath);
|
|
1771
2182
|
if (!ctx.dryRun) {
|
|
1772
2183
|
writeText(targetPath, renderGeneratedProjectMiseToml(ctx, readText(sourcePath)));
|
|
@@ -1774,8 +2185,8 @@ function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
|
1774
2185
|
return true;
|
|
1775
2186
|
}
|
|
1776
2187
|
function templateVersionFilesConf(ctx, repoRoot) {
|
|
1777
|
-
const packageJson =
|
|
1778
|
-
return
|
|
2188
|
+
const packageJson = join11(repoRoot, "package.json");
|
|
2189
|
+
return existsSync8(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";
|
|
1779
2190
|
}
|
|
1780
2191
|
function replaceOrAppendManagedBlock(text2, startMarker, block, beforePattern) {
|
|
1781
2192
|
if (startMarker.test(text2)) {
|
|
@@ -1799,7 +2210,7 @@ var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/herm
|
|
|
1799
2210
|
function requiredMisePathEntries(ctx) {
|
|
1800
2211
|
const required = [...BASE_MISE_PATH_ENTRIES];
|
|
1801
2212
|
for (const candidate of CONDITIONAL_HERMES_PATHS) {
|
|
1802
|
-
if (
|
|
2213
|
+
if (existsSync8(join11(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
|
|
1803
2214
|
}
|
|
1804
2215
|
return required;
|
|
1805
2216
|
}
|
|
@@ -1948,12 +2359,12 @@ function upsertLinkAgentfilesBlock(text2, ctx) {
|
|
|
1948
2359
|
return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
|
|
1949
2360
|
}
|
|
1950
2361
|
function readProjectJson(ctx) {
|
|
1951
|
-
return tryParseJson(safeReadText(
|
|
2362
|
+
return tryParseJson(safeReadText(join11(ctx.repoRoot, ".project.json")));
|
|
1952
2363
|
}
|
|
1953
2364
|
function canonicalProjectJson(ctx) {
|
|
1954
2365
|
const roles = discoverRoles(ctx.repoRoot);
|
|
1955
2366
|
const existing = readProjectJson(ctx) ?? {};
|
|
1956
|
-
const slug = String(existing.project_slug ?? slugifyRepoName(
|
|
2367
|
+
const slug = String(existing.project_slug ?? slugifyRepoName(dirname7(ctx.repoRoot) === ctx.repoRoot ? ctx.repoRoot.split("/").pop() ?? "project" : ctx.repoRoot.split("/").pop() ?? "project"));
|
|
1957
2368
|
const firstRole = roles[0];
|
|
1958
2369
|
const ticketProvider = {
|
|
1959
2370
|
type: String((existing.ticket_provider?.type ?? firstRole?.ticketProviderName ?? "plane") || "plane"),
|
|
@@ -1992,12 +2403,12 @@ function canonicalProjectJson(ctx) {
|
|
|
1992
2403
|
};
|
|
1993
2404
|
}
|
|
1994
2405
|
function projectJsonFinding(ctx) {
|
|
1995
|
-
const projectPath =
|
|
1996
|
-
const planeJsonPath =
|
|
2406
|
+
const projectPath = join11(ctx.repoRoot, ".project.json");
|
|
2407
|
+
const planeJsonPath = join11(ctx.repoRoot, ".plane.json");
|
|
1997
2408
|
const details = [];
|
|
1998
2409
|
const data = readProjectJson(ctx);
|
|
1999
2410
|
const roles = discoverRoles(ctx.repoRoot);
|
|
2000
|
-
if (!
|
|
2411
|
+
if (!existsSync8(projectPath)) {
|
|
2001
2412
|
return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json missing", details: [], fixable: true };
|
|
2002
2413
|
}
|
|
2003
2414
|
if (!data) {
|
|
@@ -2023,7 +2434,7 @@ function projectJsonFinding(ctx) {
|
|
|
2023
2434
|
for (const key of ["type", "workspace", "identifier", "board_id", "board_url", "state"]) {
|
|
2024
2435
|
if (!(key in ticketProvider)) details.push(`ticket_provider.${key} missing`);
|
|
2025
2436
|
}
|
|
2026
|
-
if (
|
|
2437
|
+
if (existsSync8(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
|
|
2027
2438
|
return {
|
|
2028
2439
|
id: "sot.project-json",
|
|
2029
2440
|
title: "Canonical .project.json",
|
|
@@ -2104,17 +2515,17 @@ exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" HERMES_OAUTH
|
|
|
2104
2515
|
`.replace(/\u0010/g, "$");
|
|
2105
2516
|
}
|
|
2106
2517
|
function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
|
|
2107
|
-
if (!
|
|
2108
|
-
|
|
2518
|
+
if (!existsSync8(sourceDir)) return;
|
|
2519
|
+
mkdirSync6(targetDir, { recursive: true });
|
|
2109
2520
|
for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
|
|
2110
|
-
const sourcePath =
|
|
2521
|
+
const sourcePath = join11(sourceDir, entry.name);
|
|
2111
2522
|
if (skip?.(sourcePath)) continue;
|
|
2112
|
-
const targetPath =
|
|
2523
|
+
const targetPath = join11(targetDir, entry.name);
|
|
2113
2524
|
if (entry.isDirectory()) {
|
|
2114
2525
|
copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
|
|
2115
2526
|
continue;
|
|
2116
2527
|
}
|
|
2117
|
-
if (
|
|
2528
|
+
if (existsSync8(targetPath)) continue;
|
|
2118
2529
|
changedFiles.push(targetPath);
|
|
2119
2530
|
if (!dryRun) {
|
|
2120
2531
|
ensureParent(targetPath);
|
|
@@ -2123,7 +2534,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
|
|
|
2123
2534
|
}
|
|
2124
2535
|
}
|
|
2125
2536
|
function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
|
|
2126
|
-
const gitmodulesPath =
|
|
2537
|
+
const gitmodulesPath = join11(repoRoot, ".gitmodules");
|
|
2127
2538
|
const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
|
|
2128
2539
|
const owner = role.runtimeOwner || "delorenj";
|
|
2129
2540
|
const block = `[submodule "agents/hermes/${role.role}/runtime"]
|
|
@@ -2145,7 +2556,7 @@ function upsertRegistryEntry(role, homeDir, changedFiles, dryRun) {
|
|
|
2145
2556
|
repo: ${role.repo}
|
|
2146
2557
|
role: ${role.role}
|
|
2147
2558
|
display_name: ${JSON.stringify(role.displayName || role.agentId)}
|
|
2148
|
-
project_path: ${ctxEscape(role.roleDir ?
|
|
2559
|
+
project_path: ${ctxEscape(role.roleDir ? dirname7(dirname7(dirname7(role.roleDir))) : "")}
|
|
2149
2560
|
role_dir: ${ctxEscape(role.roleDir)}
|
|
2150
2561
|
profile_name: ${role.profileName || role.agentId}
|
|
2151
2562
|
telegram:
|
|
@@ -2223,14 +2634,14 @@ var RULES = [
|
|
|
2223
2634
|
id: "mise.config-root",
|
|
2224
2635
|
title: "mise config_root + AGENTS link hooks",
|
|
2225
2636
|
audit: (ctx) => {
|
|
2226
|
-
const misePath =
|
|
2227
|
-
if (!
|
|
2637
|
+
const misePath = join11(ctx.repoRoot, "mise.toml");
|
|
2638
|
+
if (!existsSync8(misePath)) {
|
|
2228
2639
|
return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
|
|
2229
2640
|
}
|
|
2230
2641
|
const text2 = readText(misePath);
|
|
2231
2642
|
const details = [];
|
|
2232
|
-
const linkAgentfilesPath =
|
|
2233
|
-
if (!
|
|
2643
|
+
const linkAgentfilesPath = join11(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
2644
|
+
if (!existsSync8(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
|
|
2234
2645
|
const pathValues = [...(text2.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
2235
2646
|
const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
|
|
2236
2647
|
if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
|
|
@@ -2248,10 +2659,10 @@ var RULES = [
|
|
|
2248
2659
|
};
|
|
2249
2660
|
},
|
|
2250
2661
|
migrate: (ctx, finding) => {
|
|
2251
|
-
const path =
|
|
2662
|
+
const path = join11(ctx.repoRoot, "mise.toml");
|
|
2252
2663
|
const changedFiles = [];
|
|
2253
2664
|
const details = [];
|
|
2254
|
-
if (!
|
|
2665
|
+
if (!existsSync8(path)) {
|
|
2255
2666
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
2256
2667
|
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: [] };
|
|
2257
2668
|
}
|
|
@@ -2267,7 +2678,7 @@ var RULES = [
|
|
|
2267
2678
|
if (!ctx.dryRun) writeText(path, next);
|
|
2268
2679
|
text2 = next;
|
|
2269
2680
|
}
|
|
2270
|
-
const linkAgentfilesPath =
|
|
2681
|
+
const linkAgentfilesPath = join11(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
2271
2682
|
const expectedScript = templateLinkAgentfilesScript(ctx);
|
|
2272
2683
|
if (expectedScript === void 0) {
|
|
2273
2684
|
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: [] };
|
|
@@ -2294,13 +2705,13 @@ var RULES = [
|
|
|
2294
2705
|
title: "managed mise versioning block",
|
|
2295
2706
|
audit: (ctx) => {
|
|
2296
2707
|
const details = [];
|
|
2297
|
-
const misePath =
|
|
2298
|
-
const versioningPath =
|
|
2299
|
-
const manifestPath =
|
|
2708
|
+
const misePath = join11(ctx.repoRoot, "mise.toml");
|
|
2709
|
+
const versioningPath = join11(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
2710
|
+
const manifestPath = join11(ctx.repoRoot, ".mise", "version-files.conf");
|
|
2300
2711
|
const text2 = safeReadText(misePath);
|
|
2301
2712
|
if (!text2?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
|
|
2302
|
-
if (!
|
|
2303
|
-
if (!
|
|
2713
|
+
if (!existsSync8(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
|
|
2714
|
+
if (!existsSync8(manifestPath)) details.push(".mise/version-files.conf missing");
|
|
2304
2715
|
return {
|
|
2305
2716
|
id: "mise.versioning",
|
|
2306
2717
|
title: "managed mise versioning block",
|
|
@@ -2313,8 +2724,8 @@ var RULES = [
|
|
|
2313
2724
|
migrate: (ctx, finding) => {
|
|
2314
2725
|
const changedFiles = [];
|
|
2315
2726
|
const details = [];
|
|
2316
|
-
const misePath =
|
|
2317
|
-
if (!
|
|
2727
|
+
const misePath = join11(ctx.repoRoot, "mise.toml");
|
|
2728
|
+
if (!existsSync8(misePath)) {
|
|
2318
2729
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
2319
2730
|
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: [] };
|
|
2320
2731
|
}
|
|
@@ -2329,7 +2740,7 @@ var RULES = [
|
|
|
2329
2740
|
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
2330
2741
|
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
2331
2742
|
}
|
|
2332
|
-
const versioningPath =
|
|
2743
|
+
const versioningPath = join11(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
2333
2744
|
const expectedScript = templateVersioningScript(ctx);
|
|
2334
2745
|
if (expectedScript === void 0) {
|
|
2335
2746
|
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: [] };
|
|
@@ -2341,7 +2752,7 @@ var RULES = [
|
|
|
2341
2752
|
chmodSync2(versioningPath, 493);
|
|
2342
2753
|
}
|
|
2343
2754
|
}
|
|
2344
|
-
const manifestPath =
|
|
2755
|
+
const manifestPath = join11(ctx.repoRoot, ".mise", "version-files.conf");
|
|
2345
2756
|
const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
|
|
2346
2757
|
if (safeReadText(manifestPath) !== expectedManifest) {
|
|
2347
2758
|
changedFiles.push(manifestPath);
|
|
@@ -2361,9 +2772,9 @@ var RULES = [
|
|
|
2361
2772
|
id: "sot.agent-symlinks",
|
|
2362
2773
|
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
2363
2774
|
audit: (ctx) => {
|
|
2364
|
-
const agentsPath =
|
|
2365
|
-
if (!
|
|
2366
|
-
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) =>
|
|
2775
|
+
const agentsPath = join11(ctx.repoRoot, "AGENTS.md");
|
|
2776
|
+
if (!existsSync8(agentsPath)) {
|
|
2777
|
+
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync8(join11(ctx.repoRoot, file)));
|
|
2367
2778
|
if (fallbackSources.length === 0) {
|
|
2368
2779
|
return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
|
|
2369
2780
|
}
|
|
@@ -2378,7 +2789,7 @@ var RULES = [
|
|
|
2378
2789
|
}
|
|
2379
2790
|
const details = [];
|
|
2380
2791
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2381
|
-
const full =
|
|
2792
|
+
const full = join11(ctx.repoRoot, file);
|
|
2382
2793
|
const target = readSymlinkTarget(full);
|
|
2383
2794
|
if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
|
|
2384
2795
|
}
|
|
@@ -2402,7 +2813,7 @@ var RULES = [
|
|
|
2402
2813
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
|
|
2403
2814
|
}
|
|
2404
2815
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2405
|
-
const full =
|
|
2816
|
+
const full = join11(ctx.repoRoot, file);
|
|
2406
2817
|
const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
|
|
2407
2818
|
if (result.blocked) blockedDetails.push(result.blocked);
|
|
2408
2819
|
if (result.changed) changedFiles.push(full);
|
|
@@ -2424,7 +2835,7 @@ var RULES = [
|
|
|
2424
2835
|
migrate: (ctx, finding) => {
|
|
2425
2836
|
const changedFiles = [];
|
|
2426
2837
|
const details = [];
|
|
2427
|
-
const path =
|
|
2838
|
+
const path = join11(ctx.repoRoot, ".project.json");
|
|
2428
2839
|
const existing = readProjectJson(ctx) ?? {};
|
|
2429
2840
|
const canonical = canonicalProjectJson(ctx);
|
|
2430
2841
|
const merged = { ...existing, ...canonical };
|
|
@@ -2434,14 +2845,14 @@ var RULES = [
|
|
|
2434
2845
|
changedFiles.push(path);
|
|
2435
2846
|
if (!ctx.dryRun) writeText(path, expected);
|
|
2436
2847
|
}
|
|
2437
|
-
const planeJson =
|
|
2438
|
-
if (
|
|
2848
|
+
const planeJson = join11(ctx.repoRoot, ".plane.json");
|
|
2849
|
+
if (existsSync8(planeJson)) {
|
|
2439
2850
|
const backup = `${planeJson}.migrated-backup`;
|
|
2440
|
-
if (
|
|
2851
|
+
if (existsSync8(backup)) {
|
|
2441
2852
|
details.push(`cannot back up .plane.json because ${relative(ctx.repoRoot, backup)} already exists`);
|
|
2442
2853
|
} else {
|
|
2443
2854
|
changedFiles.push(backup);
|
|
2444
|
-
if (!ctx.dryRun)
|
|
2855
|
+
if (!ctx.dryRun) renameSync2(planeJson, backup);
|
|
2445
2856
|
}
|
|
2446
2857
|
}
|
|
2447
2858
|
return {
|
|
@@ -2459,8 +2870,8 @@ var RULES = [
|
|
|
2459
2870
|
title: ".env.op + gitignore secrets contract",
|
|
2460
2871
|
audit: (ctx) => {
|
|
2461
2872
|
const details = [];
|
|
2462
|
-
const envOp = safeReadText(
|
|
2463
|
-
const gitignore = safeReadText(
|
|
2873
|
+
const envOp = safeReadText(join11(ctx.repoRoot, ".env.op"));
|
|
2874
|
+
const gitignore = safeReadText(join11(ctx.repoRoot, ".gitignore"));
|
|
2464
2875
|
if (!envOp) {
|
|
2465
2876
|
details.push(".env.op missing");
|
|
2466
2877
|
} else {
|
|
@@ -2486,12 +2897,12 @@ var RULES = [
|
|
|
2486
2897
|
migrate: (ctx, finding) => {
|
|
2487
2898
|
const changedFiles = [];
|
|
2488
2899
|
const details = [];
|
|
2489
|
-
const envOpPath =
|
|
2490
|
-
if (!
|
|
2900
|
+
const envOpPath = join11(ctx.repoRoot, ".env.op");
|
|
2901
|
+
if (!existsSync8(envOpPath)) {
|
|
2491
2902
|
changedFiles.push(envOpPath);
|
|
2492
|
-
if (!ctx.dryRun) writeText(envOpPath, readText(
|
|
2903
|
+
if (!ctx.dryRun) writeText(envOpPath, readText(join11(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
|
|
2493
2904
|
}
|
|
2494
|
-
const gitignorePath =
|
|
2905
|
+
const gitignorePath = join11(ctx.repoRoot, ".gitignore");
|
|
2495
2906
|
const gitignore = safeReadText(gitignorePath) ?? "";
|
|
2496
2907
|
const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
|
|
2497
2908
|
# NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
|
|
@@ -2518,7 +2929,7 @@ var RULES = [
|
|
|
2518
2929
|
title: ".copier-answers.yml provenance + drift report",
|
|
2519
2930
|
audit: (ctx) => {
|
|
2520
2931
|
const details = [];
|
|
2521
|
-
const path =
|
|
2932
|
+
const path = join11(ctx.repoRoot, ".copier-answers.yml");
|
|
2522
2933
|
const text2 = safeReadText(path);
|
|
2523
2934
|
const project = readProjectJson(ctx);
|
|
2524
2935
|
if (!text2) {
|
|
@@ -2549,12 +2960,12 @@ var RULES = [
|
|
|
2549
2960
|
const changedFiles = [];
|
|
2550
2961
|
const project = canonicalProjectJson(ctx);
|
|
2551
2962
|
const text2 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
|
|
2552
|
-
_src_path: ${
|
|
2963
|
+
_src_path: ${join11(ctx.pjanglerRoot, "templates", "commonproject")}
|
|
2553
2964
|
project_description: ${String(project.project_description)}
|
|
2554
2965
|
project_name: ${String(project.project_name)}
|
|
2555
2966
|
ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
2556
2967
|
`;
|
|
2557
|
-
const path =
|
|
2968
|
+
const path = join11(ctx.repoRoot, ".copier-answers.yml");
|
|
2558
2969
|
if (safeReadText(path) !== text2) {
|
|
2559
2970
|
changedFiles.push(path);
|
|
2560
2971
|
if (!ctx.dryRun) writeText(path, text2);
|
|
@@ -2573,15 +2984,15 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2573
2984
|
id: "bmad.scaffold",
|
|
2574
2985
|
title: "BMAD modules/docs scaffold",
|
|
2575
2986
|
audit: (ctx) => {
|
|
2576
|
-
const sourceRoot =
|
|
2577
|
-
const targetRoot =
|
|
2987
|
+
const sourceRoot = join11(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
|
|
2988
|
+
const targetRoot = join11(ctx.repoRoot, "_bmad");
|
|
2578
2989
|
const sentinels = [
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2990
|
+
join11("core", "config.yaml"),
|
|
2991
|
+
join11("custom", "config.yaml"),
|
|
2992
|
+
join11("custom", "workflows", "ticket-lifecycle", "workflow.yaml"),
|
|
2993
|
+
join11("bmm", "workflows", "workflow-status", "workflow.yaml")
|
|
2583
2994
|
];
|
|
2584
|
-
const missing = sentinels.filter((file) =>
|
|
2995
|
+
const missing = sentinels.filter((file) => existsSync8(join11(sourceRoot, file)) && !existsSync8(join11(targetRoot, file)));
|
|
2585
2996
|
return {
|
|
2586
2997
|
id: "bmad.scaffold",
|
|
2587
2998
|
title: "BMAD modules/docs scaffold",
|
|
@@ -2593,7 +3004,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2593
3004
|
},
|
|
2594
3005
|
migrate: (ctx, finding) => {
|
|
2595
3006
|
const changedFiles = [];
|
|
2596
|
-
copyMissingRecursive(
|
|
3007
|
+
copyMissingRecursive(join11(ctx.pjanglerRoot, "templates", "commonproject", "_bmad"), join11(ctx.repoRoot, "_bmad"), changedFiles, ctx.dryRun);
|
|
2597
3008
|
return {
|
|
2598
3009
|
id: finding.id,
|
|
2599
3010
|
title: finding.title,
|
|
@@ -2615,11 +3026,11 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2615
3026
|
}
|
|
2616
3027
|
const details = [];
|
|
2617
3028
|
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"]) {
|
|
2618
|
-
if (!
|
|
3029
|
+
if (!existsSync8(join11(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join11(role.roleDir, rel))}`);
|
|
2619
3030
|
}
|
|
2620
|
-
const gitmodules = safeReadText(
|
|
3031
|
+
const gitmodules = safeReadText(join11(ctx.repoRoot, ".gitmodules")) ?? "";
|
|
2621
3032
|
if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
|
|
2622
|
-
if (!profileMetaInheritsDefault(
|
|
3033
|
+
if (!profileMetaInheritsDefault(join11(role.roleDir, "runtime", "profile.yaml"))) {
|
|
2623
3034
|
details.push("runtime/profile.yaml missing inherited default config metadata");
|
|
2624
3035
|
}
|
|
2625
3036
|
const registry = safeReadText(registryPath(ctx.homeDir));
|
|
@@ -2640,21 +3051,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2640
3051
|
if (!role) {
|
|
2641
3052
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
|
|
2642
3053
|
}
|
|
2643
|
-
const templateRoleDir =
|
|
2644
|
-
writeIfDifferent(
|
|
2645
|
-
writeIfDifferent(
|
|
2646
|
-
writeIfDifferent(
|
|
2647
|
-
copyMissingRecursive(
|
|
2648
|
-
copyMissingRecursive(
|
|
2649
|
-
copyMissingRecursive(
|
|
2650
|
-
const promptSource =
|
|
2651
|
-
const promptTarget =
|
|
2652
|
-
if (
|
|
3054
|
+
const templateRoleDir = join11(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
|
|
3055
|
+
writeIfDifferent(join11(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
|
|
3056
|
+
writeIfDifferent(join11(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
|
|
3057
|
+
writeIfDifferent(join11(role.roleDir, ".gitignore"), readText(join11(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
|
|
3058
|
+
copyMissingRecursive(join11(templateRoleDir, ".runtime-scaffold"), join11(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
|
|
3059
|
+
copyMissingRecursive(join11(templateRoleDir, ".runtime-scaffold"), join11(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
|
|
3060
|
+
copyMissingRecursive(join11(templateRoleDir, ".scripts"), join11(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
|
|
3061
|
+
const promptSource = join11(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
|
|
3062
|
+
const promptTarget = join11(role.roleDir, ".scripts", "sentinel.prompt.md");
|
|
3063
|
+
if (existsSync8(promptSource) && !existsSync8(promptTarget)) {
|
|
2653
3064
|
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);
|
|
2654
3065
|
writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
|
|
2655
3066
|
}
|
|
2656
3067
|
upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
|
|
2657
|
-
const profileMetaUpdated = upsertInheritedProfileMeta(
|
|
3068
|
+
const profileMetaUpdated = upsertInheritedProfileMeta(join11(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
|
|
2658
3069
|
if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
|
|
2659
3070
|
const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
|
|
2660
3071
|
if (registryUpdated) details.push(`updated ${registryUpdated}`);
|
|
@@ -2672,527 +3083,171 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2672
3083
|
id: "systemd.sentinel",
|
|
2673
3084
|
title: "Hermes systemd/sentinel units enabled + active",
|
|
2674
3085
|
audit: (ctx) => {
|
|
2675
|
-
const roles = discoverRoles(ctx.repoRoot);
|
|
2676
|
-
if (!roles.length) {
|
|
2677
|
-
return { id: "systemd.sentinel", title: "Hermes systemd/sentinel units enabled + active", status: "skip", summary: "No Hermes roles present", details: [], fixable: false };
|
|
2678
|
-
}
|
|
2679
|
-
const probe = systemctlUser(["is-system-running"]);
|
|
2680
|
-
if (!probe.ok && !/running|degraded|starting|maintenance/.test(`${probe.stdout} ${probe.stderr}`)) {
|
|
2681
|
-
return { id: "systemd.sentinel", title: "Hermes systemd/sentinel units enabled + active", status: "warn", summary: "systemd --user unavailable; unit state not auditable here", details: [], fixable: false };
|
|
2682
|
-
}
|
|
2683
|
-
const details = [];
|
|
2684
|
-
for (const role of roles) {
|
|
2685
|
-
for (const unit of [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`]) {
|
|
2686
|
-
const state = checkUnit(unit);
|
|
2687
|
-
if (!state.enabled || !state.active) details.push(`${unit} should be enabled+active`);
|
|
2688
|
-
}
|
|
2689
|
-
}
|
|
2690
|
-
return {
|
|
2691
|
-
id: "systemd.sentinel",
|
|
2692
|
-
title: "Hermes systemd/sentinel units enabled + active",
|
|
2693
|
-
status: details.length === 0 ? "pass" : "fail",
|
|
2694
|
-
summary: details.length === 0 ? "Hermes user units are enabled and active" : `${details.length} systemd parity issue(s) detected`,
|
|
2695
|
-
details,
|
|
2696
|
-
fixable: true
|
|
2697
|
-
};
|
|
2698
|
-
},
|
|
2699
|
-
migrate: (ctx, finding) => {
|
|
2700
|
-
const roles = discoverRoles(ctx.repoRoot);
|
|
2701
|
-
const changedFiles = [];
|
|
2702
|
-
const details = [];
|
|
2703
|
-
if (!roles.length) {
|
|
2704
|
-
return { id: finding.id, title: finding.title, status: "blocked", summary: "No Hermes roles present", changedFiles, details };
|
|
2705
|
-
}
|
|
2706
|
-
const probe = systemctlUser(["is-system-running"]);
|
|
2707
|
-
if (!probe.ok && !/running|degraded|starting|maintenance/.test(`${probe.stdout} ${probe.stderr}`)) {
|
|
2708
|
-
return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
|
|
2709
|
-
}
|
|
2710
|
-
for (const role of roles) {
|
|
2711
|
-
const sysDir = join10(ctx.homeDir, ".config", "systemd", "user");
|
|
2712
|
-
const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`];
|
|
2713
|
-
const allUnitsPresent = units.every((unit) => existsSync7(join10(sysDir, unit)));
|
|
2714
|
-
if (allUnitsPresent) {
|
|
2715
|
-
if (ctx.dryRun) {
|
|
2716
|
-
details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
|
|
2717
|
-
} else {
|
|
2718
|
-
systemctlUser(["daemon-reload"]);
|
|
2719
|
-
for (const unit of units) {
|
|
2720
|
-
systemctlUser(["enable", "--now", unit]);
|
|
2721
|
-
}
|
|
2722
|
-
}
|
|
2723
|
-
continue;
|
|
2724
|
-
}
|
|
2725
|
-
for (const script of [join10(role.roleDir, ".scripts", "70-systemd.sh")]) {
|
|
2726
|
-
if (!script || !existsSync7(script)) continue;
|
|
2727
|
-
if (ctx.dryRun) {
|
|
2728
|
-
details.push(`would run: bash ${script}`);
|
|
2729
|
-
} else {
|
|
2730
|
-
const result = spawnSync4("bash", [script], { cwd: role.roleDir, encoding: "utf8" });
|
|
2731
|
-
if (result.status !== 0) details.push(`script failed: ${script}: ${result.stderr.trim() || result.stdout.trim()}`);
|
|
2732
|
-
}
|
|
2733
|
-
}
|
|
2734
|
-
}
|
|
2735
|
-
return {
|
|
2736
|
-
id: finding.id,
|
|
2737
|
-
title: finding.title,
|
|
2738
|
-
status: details.some((detail) => detail.includes("failed:")) ? "blocked" : details.length ? ctx.dryRun ? "skipped" : "applied" : "noop",
|
|
2739
|
-
summary: details.length ? ctx.dryRun ? "Planned systemd remediation commands" : "Attempted systemd remediation" : "No changes required",
|
|
2740
|
-
changedFiles,
|
|
2741
|
-
details
|
|
2742
|
-
};
|
|
2743
|
-
}
|
|
2744
|
-
}
|
|
2745
|
-
];
|
|
2746
|
-
function writeIfDifferent(path, content, dryRun, changedFiles, mode) {
|
|
2747
|
-
const normalized = content.endsWith("\n") ? content : `${content}
|
|
2748
|
-
`;
|
|
2749
|
-
if (safeReadText(path) === normalized) return;
|
|
2750
|
-
changedFiles.push(path);
|
|
2751
|
-
if (!dryRun) {
|
|
2752
|
-
writeText(path, normalized);
|
|
2753
|
-
if (mode) chmodSync2(path, mode);
|
|
2754
|
-
}
|
|
2755
|
-
}
|
|
2756
|
-
function getParityRuleIds() {
|
|
2757
|
-
return RULES.map((rule) => rule.id);
|
|
2758
|
-
}
|
|
2759
|
-
function runAudit(repoArg) {
|
|
2760
|
-
const pjanglerRoot = resolvePjanglerRoot();
|
|
2761
|
-
const ctx = {
|
|
2762
|
-
repoRoot: resolve(repoArg ?? process.cwd()),
|
|
2763
|
-
dryRun: true,
|
|
2764
|
-
pjanglerRoot,
|
|
2765
|
-
homeDir: homedir4()
|
|
2766
|
-
};
|
|
2767
|
-
const rules = RULES.map((rule) => rule.audit(ctx));
|
|
2768
|
-
return {
|
|
2769
|
-
repo: ctx.repoRoot,
|
|
2770
|
-
ok: rules.every((rule) => rule.status === "pass" || rule.status === "skip"),
|
|
2771
|
-
auditedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2772
|
-
rules
|
|
2773
|
-
};
|
|
2774
|
-
}
|
|
2775
|
-
function runMigrationForRules(ruleIds, repoArg, dryRun) {
|
|
2776
|
-
const pjanglerRoot = resolvePjanglerRoot();
|
|
2777
|
-
const ctx = {
|
|
2778
|
-
repoRoot: resolve(repoArg ?? process.cwd()),
|
|
2779
|
-
dryRun,
|
|
2780
|
-
pjanglerRoot,
|
|
2781
|
-
homeDir: homedir4()
|
|
2782
|
-
};
|
|
2783
|
-
const selected = RULES.filter((rule) => ruleIds.includes(rule.id));
|
|
2784
|
-
if (!selected.length) {
|
|
2785
|
-
throw new Error(`Unknown parity rules: ${ruleIds.join(", ")}`);
|
|
2786
|
-
}
|
|
2787
|
-
const results = selected.map((rule) => {
|
|
2788
|
-
try {
|
|
2789
|
-
return rule.migrate(ctx, rule.audit(ctx));
|
|
2790
|
-
} catch (err) {
|
|
2791
|
-
return {
|
|
2792
|
-
id: rule.id,
|
|
2793
|
-
title: rule.title,
|
|
2794
|
-
status: "blocked",
|
|
2795
|
-
summary: `migrate threw: ${err instanceof Error ? err.message : String(err)}`,
|
|
2796
|
-
changedFiles: [],
|
|
2797
|
-
details: []
|
|
2798
|
-
};
|
|
2799
|
-
}
|
|
2800
|
-
});
|
|
2801
|
-
const changedFiles = Array.from(new Set(results.flatMap((result) => result.changedFiles))).sort();
|
|
2802
|
-
return {
|
|
2803
|
-
repo: ctx.repoRoot,
|
|
2804
|
-
dryRun,
|
|
2805
|
-
ok: results.every((result) => result.status !== "blocked"),
|
|
2806
|
-
selectedRules: selected.map((rule) => rule.id),
|
|
2807
|
-
results,
|
|
2808
|
-
changedFiles
|
|
2809
|
-
};
|
|
2810
|
-
}
|
|
2811
|
-
function runMigration(selector, repoArg, dryRun, all) {
|
|
2812
|
-
const ruleIds = all ? RULES.map((rule) => rule.id) : selector ? [selector] : [];
|
|
2813
|
-
return runMigrationForRules(ruleIds, repoArg, dryRun);
|
|
2814
|
-
}
|
|
2815
|
-
function prettyTimestamp(iso) {
|
|
2816
|
-
const match = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})/.exec(iso);
|
|
2817
|
-
return match ? `${match[1]} ${match[2]} UTC` : iso;
|
|
2818
|
-
}
|
|
2819
|
-
function formatAuditReport(report) {
|
|
2820
|
-
const counts = {};
|
|
2821
|
-
for (const rule of report.rules) counts[rule.status] = (counts[rule.status] ?? 0) + 1;
|
|
2822
|
-
const idWidth = report.rules.reduce((width, rule) => Math.max(width, rule.id.length), 0);
|
|
2823
|
-
const tally = [];
|
|
2824
|
-
if (counts.pass) tally.push(green(`${counts.pass} passed`));
|
|
2825
|
-
if (counts.fail) tally.push(red(`${counts.fail} failed`));
|
|
2826
|
-
if (counts.warn) tally.push(yellow(`${counts.warn} warning${counts.warn === 1 ? "" : "s"}`));
|
|
2827
|
-
if (counts.skip) tally.push(gray(`${counts.skip} skipped`));
|
|
2828
|
-
const overall = report.ok ? `${green(glyph.pass)} ${bold("Parity audit passed")}` : `${red(glyph.fail)} ${bold("Parity audit failed")}`;
|
|
2829
|
-
const lines = [""];
|
|
2830
|
-
lines.push(` ${overall}${tally.length ? ` ${dim(glyph.dot)} ${joinDot(tally)}` : ""}`);
|
|
2831
|
-
lines.push(` ${dim(report.repo)} ${dim(glyph.dot)} ${dim(prettyTimestamp(report.auditedAt))}`);
|
|
2832
|
-
lines.push("");
|
|
2833
|
-
for (const rule of report.rules) {
|
|
2834
|
-
const style = statusStyle(rule.status);
|
|
2835
|
-
lines.push(` ${style.color(style.glyph)} ${style.color(rule.id.padEnd(idWidth))} ${rule.summary}`);
|
|
2836
|
-
for (const detail of rule.details) lines.push(` ${dim(glyph.arrow)} ${dim(detail)}`);
|
|
2837
|
-
}
|
|
2838
|
-
lines.push("");
|
|
2839
|
-
return lines.join("\n");
|
|
2840
|
-
}
|
|
2841
|
-
|
|
2842
|
-
// src/project/index.ts
|
|
2843
|
-
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
2844
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync5, renameSync as renameSync2, statSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2845
|
-
import { homedir as homedir5 } from "node:os";
|
|
2846
|
-
import { basename as basename3, dirname as dirname7, join as join11, resolve as resolve2 } from "node:path";
|
|
2847
|
-
import YAML from "yaml";
|
|
2848
|
-
var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
|
|
2849
|
-
var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
|
|
2850
|
-
var KNOWN_SKILL_ROOTS = [
|
|
2851
|
-
"/home/delorenj/code/skillex/all-skills",
|
|
2852
|
-
"/home/delorenj/code/CoachingAgentFramework/.agents/skills",
|
|
2853
|
-
"/home/delorenj/code/pjangler/.agents/skills",
|
|
2854
|
-
join11(homedir5(), ".codex", "skills")
|
|
2855
|
-
];
|
|
2856
|
-
function projectRegistryPath(env2 = process.env) {
|
|
2857
|
-
return expandHome(env2[PROJECT_REGISTRY_ENV] || join11(homedir5(), ".config", "pjangler", "projects.yaml"));
|
|
2858
|
-
}
|
|
2859
|
-
function emptyProjectRegistry() {
|
|
2860
|
-
return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
|
|
2861
|
-
}
|
|
2862
|
-
function loadProjectRegistry(path = projectRegistryPath()) {
|
|
2863
|
-
if (!existsSync8(path)) return emptyProjectRegistry();
|
|
2864
|
-
const raw = YAML.parse(readFileSync5(path, "utf8"));
|
|
2865
|
-
if (raw == null) return emptyProjectRegistry();
|
|
2866
|
-
if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
|
|
2867
|
-
const registry = raw;
|
|
2868
|
-
const normalized = {
|
|
2869
|
-
schema_version: Number(registry.schema_version ?? PROJECT_REGISTRY_SCHEMA_VERSION),
|
|
2870
|
-
projects: isRecord(registry.projects) ? registry.projects : {}
|
|
2871
|
-
};
|
|
2872
|
-
validateProjectRegistry(normalized);
|
|
2873
|
-
return normalized;
|
|
2874
|
-
}
|
|
2875
|
-
function saveProjectRegistry(registry, path = projectRegistryPath()) {
|
|
2876
|
-
validateProjectRegistry(registry);
|
|
2877
|
-
mkdirSync6(dirname7(path), { recursive: true });
|
|
2878
|
-
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
2879
|
-
writeFileSync5(temp, YAML.stringify(registry, { lineWidth: 0 }), "utf8");
|
|
2880
|
-
renameSync2(temp, path);
|
|
2881
|
-
}
|
|
2882
|
-
function validateProjectRegistry(registry) {
|
|
2883
|
-
if (registry.schema_version !== PROJECT_REGISTRY_SCHEMA_VERSION) {
|
|
2884
|
-
throw new Error(`Unsupported project registry schema_version: ${registry.schema_version}`);
|
|
2885
|
-
}
|
|
2886
|
-
if (!isRecord(registry.projects)) throw new Error("Project registry projects must be a mapping");
|
|
2887
|
-
const slugs = /* @__PURE__ */ new Set();
|
|
2888
|
-
const repoPaths = /* @__PURE__ */ new Map();
|
|
2889
|
-
const identifiers = /* @__PURE__ */ new Map();
|
|
2890
|
-
for (const [slug, project] of Object.entries(registry.projects)) {
|
|
2891
|
-
validateProjectRecord(project, slug);
|
|
2892
|
-
if (slugs.has(project.slug)) throw new Error(`Duplicate project slug: ${project.slug}`);
|
|
2893
|
-
slugs.add(project.slug);
|
|
2894
|
-
const repoKey = resolve2(project.repo_path);
|
|
2895
|
-
const existingRepoSlug = repoPaths.get(repoKey);
|
|
2896
|
-
if (existingRepoSlug && existingRepoSlug !== slug) {
|
|
2897
|
-
throw new Error(`Duplicate project repo_path: ${project.repo_path} used by ${existingRepoSlug} and ${slug}`);
|
|
2898
|
-
}
|
|
2899
|
-
repoPaths.set(repoKey, slug);
|
|
2900
|
-
const identifier = project.ticket_provider.identifier?.toUpperCase();
|
|
2901
|
-
if (identifier) {
|
|
2902
|
-
const existingIdentifierSlug = identifiers.get(identifier);
|
|
2903
|
-
if (existingIdentifierSlug && existingIdentifierSlug !== slug) {
|
|
2904
|
-
throw new Error(`Duplicate project identifier: ${identifier} used by ${existingIdentifierSlug} and ${slug}`);
|
|
2905
|
-
}
|
|
2906
|
-
identifiers.set(identifier, slug);
|
|
2907
|
-
}
|
|
2908
|
-
}
|
|
2909
|
-
}
|
|
2910
|
-
function slugifyProjectName(value) {
|
|
2911
|
-
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
2912
|
-
}
|
|
2913
|
-
function deriveProjectIdentifier(value) {
|
|
2914
|
-
const compact = value.replace(/[^A-Za-z0-9]/g, "").toUpperCase();
|
|
2915
|
-
const identifier = compact.slice(0, 4) || "PROJ";
|
|
2916
|
-
return identifier.length >= 2 ? identifier : `${identifier}XX`.slice(0, 4);
|
|
2917
|
-
}
|
|
2918
|
-
function normalizeAgentRole(value) {
|
|
2919
|
-
return value?.trim() || "pm";
|
|
2920
|
-
}
|
|
2921
|
-
function jsonStable(value) {
|
|
2922
|
-
return JSON.stringify(value);
|
|
2923
|
-
}
|
|
2924
|
-
function projectRecordEquivalent(a, b) {
|
|
2925
|
-
if (!a) return false;
|
|
2926
|
-
const { created_at: _aCreated, updated_at: _aUpdated, ...aComparable } = a;
|
|
2927
|
-
const { created_at: _bCreated, updated_at: _bUpdated, ...bComparable } = b;
|
|
2928
|
-
return jsonStable(aComparable) === jsonStable(bComparable);
|
|
2929
|
-
}
|
|
2930
|
-
function defaultProjectTargetDir(name, cwd = process.cwd()) {
|
|
2931
|
-
const compactName = name.replace(/[^A-Za-z0-9._-]/g, "") || slugifyProjectName(name);
|
|
2932
|
-
return resolve2(dirname7(resolve2(cwd)), compactName);
|
|
2933
|
-
}
|
|
2934
|
-
function resolveSourceSkillPath(sourceSkill) {
|
|
2935
|
-
if (!sourceSkill) return void 0;
|
|
2936
|
-
const expanded = expandHome(sourceSkill);
|
|
2937
|
-
const direct = resolve2(expanded);
|
|
2938
|
-
if (existsSync8(direct)) return direct;
|
|
2939
|
-
const name = basename3(sourceSkill);
|
|
2940
|
-
for (const root of KNOWN_SKILL_ROOTS) {
|
|
2941
|
-
const candidate = join11(root, name);
|
|
2942
|
-
if (existsSync8(candidate)) return candidate;
|
|
2943
|
-
}
|
|
2944
|
-
const civilWarLetterifier = "/home/delorenj/code/skillex/all-skills/civilwar-letterifier";
|
|
2945
|
-
const hint = existsSync8(civilWarLetterifier) ? ` Did you mean ${civilWarLetterifier}?` : "";
|
|
2946
|
-
throw new Error(`Source skill not found: ${sourceSkill}.${hint}`);
|
|
2947
|
-
}
|
|
2948
|
-
function planProjectInit(input) {
|
|
2949
|
-
if (!input.name.trim()) throw new Error("Project name is required");
|
|
2950
|
-
const registryPath2 = resolve2(projectRegistryPath({ ...process.env, [PROJECT_REGISTRY_ENV]: input.registryPath || process.env[PROJECT_REGISTRY_ENV] }));
|
|
2951
|
-
const registry = loadProjectRegistry(registryPath2);
|
|
2952
|
-
const now = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
2953
|
-
const slug = input.projectSlug ?? slugifyProjectName(input.name);
|
|
2954
|
-
const targetDir = resolve2(input.targetDir ?? defaultProjectTargetDir(input.name, input.cwd));
|
|
2955
|
-
const identifier = (input.projectIdentifier ?? deriveProjectIdentifier(input.name)).toUpperCase();
|
|
2956
|
-
const existing = registry.projects[slug];
|
|
2957
|
-
const sourceSkillPath = resolveSourceSkillPath(input.sourceSkill);
|
|
2958
|
-
const overwrite = input.overwrite ?? input.force ?? false;
|
|
2959
|
-
const agentRole = normalizeAgentRole(input.agentRole);
|
|
2960
|
-
const agents = input.provisionAgent ? {
|
|
2961
|
-
...existing?.agents ?? {},
|
|
2962
|
-
[agentRole]: {
|
|
2963
|
-
role: agentRole,
|
|
2964
|
-
provisioning_state: "planned"
|
|
2965
|
-
}
|
|
2966
|
-
} : existing?.agents ?? {};
|
|
2967
|
-
const scaffold = input.scaffold ?? true;
|
|
2968
|
-
const candidateProject = {
|
|
2969
|
-
name: input.name,
|
|
2970
|
-
slug,
|
|
2971
|
-
repo_path: targetDir,
|
|
2972
|
-
description: input.description ?? "",
|
|
2973
|
-
status: "planned",
|
|
2974
|
-
source_artifacts: sourceSkillPath ? [{ kind: "skill", path: sourceSkillPath, package_name: input.packageName ?? slug }] : [],
|
|
2975
|
-
template: {
|
|
2976
|
-
commonproject: {
|
|
2977
|
-
enabled: true,
|
|
2978
|
-
primary_language: input.primaryLanguage ?? "python"
|
|
2979
|
-
}
|
|
2980
|
-
},
|
|
2981
|
-
ticket_provider: {
|
|
2982
|
-
type: input.ticketProvider ?? "plane",
|
|
2983
|
-
workspace: input.planeWorkspace ?? "33god",
|
|
2984
|
-
identifier,
|
|
2985
|
-
board_id: input.planeProjectId ?? "",
|
|
2986
|
-
board_url: input.planeProjectId ? `https://plane.delo.sh/${input.planeWorkspace ?? "33god"}/projects/${input.planeProjectId}/issues/` : "",
|
|
2987
|
-
state: input.live ? "planned" : "planned"
|
|
2988
|
-
},
|
|
2989
|
-
agents,
|
|
2990
|
-
created_at: existing?.created_at ?? now,
|
|
2991
|
-
updated_at: now
|
|
2992
|
-
};
|
|
2993
|
-
const project = {
|
|
2994
|
-
...candidateProject,
|
|
2995
|
-
updated_at: projectRecordEquivalent(existing, candidateProject) ? existing.updated_at : now
|
|
2996
|
-
};
|
|
2997
|
-
validateNoDuplicateProject(registry, project, overwrite);
|
|
2998
|
-
const pjanglerRoot = resolve2(input.pjanglerRoot ?? resolvePjanglerRoot2());
|
|
2999
|
-
const manifest = projectManifestFromRegistryProject(project);
|
|
3000
|
-
const apply = input.apply ?? false;
|
|
3001
|
-
const live = input.live ?? false;
|
|
3002
|
-
const actions = [
|
|
3003
|
-
{ kind: "registry.upsert", registryPath: registryPath2, slug, project }
|
|
3004
|
-
];
|
|
3005
|
-
if (scaffold) {
|
|
3006
|
-
actions.push(buildCommonProjectCopierAction({
|
|
3007
|
-
pjanglerRoot,
|
|
3008
|
-
targetDir,
|
|
3009
|
-
projectName: project.name,
|
|
3010
|
-
projectDescription: project.description,
|
|
3011
|
-
projectSlug: project.slug,
|
|
3012
|
-
ticketProvider: project.ticket_provider.type,
|
|
3013
|
-
planeWorkspace: project.ticket_provider.workspace ?? "33god",
|
|
3014
|
-
planeProjectId: project.ticket_provider.board_id ?? "",
|
|
3015
|
-
projectIdentifier: identifier,
|
|
3016
|
-
primaryLanguage: project.template.commonproject.primary_language,
|
|
3017
|
-
overwrite
|
|
3018
|
-
}));
|
|
3019
|
-
}
|
|
3020
|
-
actions.push(
|
|
3021
|
-
{ kind: "project.write-manifest", path: join11(targetDir, ".project.json"), manifest },
|
|
3022
|
-
{
|
|
3023
|
-
kind: "plane.create-or-link",
|
|
3024
|
-
enabled: live,
|
|
3025
|
-
live,
|
|
3026
|
-
workspace: project.ticket_provider.workspace ?? "33god",
|
|
3027
|
-
identifier,
|
|
3028
|
-
state: live ? "planned" : "planned",
|
|
3029
|
-
reason: live ? void 0 : "network/cloud actions require --live"
|
|
3030
|
-
},
|
|
3031
|
-
{
|
|
3032
|
-
kind: "hermes.provision-agent",
|
|
3033
|
-
enabled: input.provisionAgent ?? false,
|
|
3034
|
-
local: !live,
|
|
3035
|
-
targetDir,
|
|
3036
|
-
targetRepo: slug,
|
|
3037
|
-
role: agentRole,
|
|
3038
|
-
context: {
|
|
3039
|
-
skipRuntimeRepo: !live,
|
|
3040
|
-
skipPlane: !live,
|
|
3041
|
-
skipBloodbank: !live,
|
|
3042
|
-
skipSystemd: !live || process.platform === "darwin"
|
|
3086
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
3087
|
+
if (!roles.length) {
|
|
3088
|
+
return { id: "systemd.sentinel", title: "Hermes systemd/sentinel units enabled + active", status: "skip", summary: "No Hermes roles present", details: [], fixable: false };
|
|
3043
3089
|
}
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
}
|
|
3048
|
-
function executeProjectInitPlan(plan) {
|
|
3049
|
-
const logs = [];
|
|
3050
|
-
const errors = [];
|
|
3051
|
-
const changedFiles = [];
|
|
3052
|
-
if (!plan.apply) return { ok: true, plan, logs, errors, changedFiles };
|
|
3053
|
-
const registry = loadProjectRegistry(plan.registryPath);
|
|
3054
|
-
let pendingRegistryAction;
|
|
3055
|
-
for (const action of plan.actions) {
|
|
3056
|
-
if (action.kind === "copier.copy.commonproject") {
|
|
3057
|
-
mkdirSync6(dirname7(action.targetDir), { recursive: true });
|
|
3058
|
-
const result = spawnSync5(action.command[0], action.command.slice(1), { encoding: "utf8", cwd: action.cwd });
|
|
3059
|
-
if (result.stdout?.trim()) logs.push(result.stdout.trim());
|
|
3060
|
-
if (result.stderr?.trim()) logs.push(result.stderr.trim());
|
|
3061
|
-
if (result.error) {
|
|
3062
|
-
const code = result.error.code;
|
|
3063
|
-
errors.push(
|
|
3064
|
-
code === "ENOENT" ? "copier not found on PATH. Install with: uv tool install copier or pip install copier" : `copier failed: ${result.error.message}`
|
|
3065
|
-
);
|
|
3066
|
-
break;
|
|
3090
|
+
const probe = systemctlUser(["is-system-running"]);
|
|
3091
|
+
if (!probe.ok && !/running|degraded|starting|maintenance/.test(`${probe.stdout} ${probe.stderr}`)) {
|
|
3092
|
+
return { id: "systemd.sentinel", title: "Hermes systemd/sentinel units enabled + active", status: "warn", summary: "systemd --user unavailable; unit state not auditable here", details: [], fixable: false };
|
|
3067
3093
|
}
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3094
|
+
const details = [];
|
|
3095
|
+
for (const role of roles) {
|
|
3096
|
+
for (const unit of [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`]) {
|
|
3097
|
+
const state = checkUnit(unit);
|
|
3098
|
+
if (!state.enabled || !state.active) details.push(`${unit} should be enabled+active`);
|
|
3099
|
+
}
|
|
3072
3100
|
}
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3101
|
+
return {
|
|
3102
|
+
id: "systemd.sentinel",
|
|
3103
|
+
title: "Hermes systemd/sentinel units enabled + active",
|
|
3104
|
+
status: details.length === 0 ? "pass" : "fail",
|
|
3105
|
+
summary: details.length === 0 ? "Hermes user units are enabled and active" : `${details.length} systemd parity issue(s) detected`,
|
|
3106
|
+
details,
|
|
3107
|
+
fixable: true
|
|
3108
|
+
};
|
|
3109
|
+
},
|
|
3110
|
+
migrate: (ctx, finding) => {
|
|
3111
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
3112
|
+
const changedFiles = [];
|
|
3113
|
+
const details = [];
|
|
3114
|
+
if (!roles.length) {
|
|
3115
|
+
return { id: finding.id, title: finding.title, status: "blocked", summary: "No Hermes roles present", changedFiles, details };
|
|
3082
3116
|
}
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3117
|
+
const probe = systemctlUser(["is-system-running"]);
|
|
3118
|
+
if (!probe.ok && !/running|degraded|starting|maintenance/.test(`${probe.stdout} ${probe.stderr}`)) {
|
|
3119
|
+
return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
|
|
3120
|
+
}
|
|
3121
|
+
for (const role of roles) {
|
|
3122
|
+
const sysDir = join11(ctx.homeDir, ".config", "systemd", "user");
|
|
3123
|
+
const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`];
|
|
3124
|
+
const allUnitsPresent = units.every((unit) => existsSync8(join11(sysDir, unit)));
|
|
3125
|
+
if (allUnitsPresent) {
|
|
3126
|
+
if (ctx.dryRun) {
|
|
3127
|
+
details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
|
|
3128
|
+
} else {
|
|
3129
|
+
systemctlUser(["daemon-reload"]);
|
|
3130
|
+
for (const unit of units) {
|
|
3131
|
+
systemctlUser(["enable", "--now", unit]);
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
continue;
|
|
3135
|
+
}
|
|
3136
|
+
for (const script of [join11(role.roleDir, ".scripts", "70-systemd.sh")]) {
|
|
3137
|
+
if (!script || !existsSync8(script)) continue;
|
|
3138
|
+
if (ctx.dryRun) {
|
|
3139
|
+
details.push(`would run: bash ${script}`);
|
|
3140
|
+
} else {
|
|
3141
|
+
const result = spawnSync5("bash", [script], { cwd: role.roleDir, encoding: "utf8" });
|
|
3142
|
+
if (result.status !== 0) details.push(`script failed: ${script}: ${result.stderr.trim() || result.stdout.trim()}`);
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
return {
|
|
3147
|
+
id: finding.id,
|
|
3148
|
+
title: finding.title,
|
|
3149
|
+
status: details.some((detail) => detail.includes("failed:")) ? "blocked" : details.length ? ctx.dryRun ? "skipped" : "applied" : "noop",
|
|
3150
|
+
summary: details.length ? ctx.dryRun ? "Planned systemd remediation commands" : "Attempted systemd remediation" : "No changes required",
|
|
3151
|
+
changedFiles,
|
|
3152
|
+
details
|
|
3153
|
+
};
|
|
3089
3154
|
}
|
|
3090
3155
|
}
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3156
|
+
];
|
|
3157
|
+
function writeIfDifferent(path, content, dryRun, changedFiles, mode) {
|
|
3158
|
+
const normalized = content.endsWith("\n") ? content : `${content}
|
|
3159
|
+
`;
|
|
3160
|
+
if (safeReadText(path) === normalized) return;
|
|
3161
|
+
changedFiles.push(path);
|
|
3162
|
+
if (!dryRun) {
|
|
3163
|
+
writeText(path, normalized);
|
|
3164
|
+
if (mode) chmodSync2(path, mode);
|
|
3097
3165
|
}
|
|
3098
|
-
return { ok: errors.length === 0, plan, logs, errors, changedFiles };
|
|
3099
|
-
}
|
|
3100
|
-
function projectManifestFromRegistryProject(project) {
|
|
3101
|
-
const agents = Object.fromEntries(
|
|
3102
|
-
Object.entries(project.agents).map(([name, agent]) => [
|
|
3103
|
-
`${project.slug}-${name}`,
|
|
3104
|
-
{
|
|
3105
|
-
role: agent.role,
|
|
3106
|
-
role_dir: agent.role_dir,
|
|
3107
|
-
provisioning_state: agent.provisioning_state
|
|
3108
|
-
}
|
|
3109
|
-
])
|
|
3110
|
-
);
|
|
3111
|
-
return {
|
|
3112
|
-
project_name: project.name,
|
|
3113
|
-
project_description: project.description,
|
|
3114
|
-
project_slug: project.slug,
|
|
3115
|
-
repo_path: project.repo_path,
|
|
3116
|
-
ticket_provider: {
|
|
3117
|
-
type: project.ticket_provider.type,
|
|
3118
|
-
workspace: project.ticket_provider.workspace ?? "",
|
|
3119
|
-
identifier: project.ticket_provider.identifier ?? "",
|
|
3120
|
-
board_id: project.ticket_provider.board_id ?? "",
|
|
3121
|
-
board_url: project.ticket_provider.board_url ?? "",
|
|
3122
|
-
state: project.ticket_provider.state
|
|
3123
|
-
},
|
|
3124
|
-
agents
|
|
3125
|
-
};
|
|
3126
3166
|
}
|
|
3127
|
-
function
|
|
3128
|
-
|
|
3129
|
-
if (!project) throw new Error(`Project not found in registry: ${slug}`);
|
|
3130
|
-
return project;
|
|
3167
|
+
function getParityRuleIds() {
|
|
3168
|
+
return RULES.map((rule) => rule.id);
|
|
3131
3169
|
}
|
|
3132
|
-
function
|
|
3133
|
-
const
|
|
3134
|
-
const
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
plane_workspace: input.planeWorkspace,
|
|
3140
|
-
plane_project_id: input.planeProjectId ?? "",
|
|
3141
|
-
project_identifier: input.projectIdentifier,
|
|
3142
|
-
primary_language: input.primaryLanguage
|
|
3170
|
+
function runAudit(repoArg) {
|
|
3171
|
+
const pjanglerRoot = resolvePjanglerRoot2();
|
|
3172
|
+
const ctx = {
|
|
3173
|
+
repoRoot: resolve2(repoArg ?? process.cwd()),
|
|
3174
|
+
dryRun: true,
|
|
3175
|
+
pjanglerRoot,
|
|
3176
|
+
homeDir: homedir5()
|
|
3143
3177
|
};
|
|
3144
|
-
const
|
|
3145
|
-
for (const [key, value] of Object.entries(data)) command.push("--data", `${key}=${value}`);
|
|
3146
|
-
if (input.overwrite) command.push("--overwrite");
|
|
3178
|
+
const rules = RULES.map((rule) => rule.audit(ctx));
|
|
3147
3179
|
return {
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
data,
|
|
3153
|
-
overwrite: input.overwrite
|
|
3180
|
+
repo: ctx.repoRoot,
|
|
3181
|
+
ok: rules.every((rule) => rule.status === "pass" || rule.status === "skip"),
|
|
3182
|
+
auditedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3183
|
+
rules
|
|
3154
3184
|
};
|
|
3155
3185
|
}
|
|
3156
|
-
function
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
}
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
throw new Error(`Project slug already exists in registry: ${project.slug}`);
|
|
3186
|
+
function runMigrationForRules(ruleIds, repoArg, dryRun) {
|
|
3187
|
+
const pjanglerRoot = resolvePjanglerRoot2();
|
|
3188
|
+
const ctx = {
|
|
3189
|
+
repoRoot: resolve2(repoArg ?? process.cwd()),
|
|
3190
|
+
dryRun,
|
|
3191
|
+
pjanglerRoot,
|
|
3192
|
+
homeDir: homedir5()
|
|
3193
|
+
};
|
|
3194
|
+
const selected = RULES.filter((rule) => ruleIds.includes(rule.id));
|
|
3195
|
+
if (!selected.length) {
|
|
3196
|
+
throw new Error(`Unknown parity rules: ${ruleIds.join(", ")}`);
|
|
3168
3197
|
}
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3198
|
+
const results = selected.map((rule) => {
|
|
3199
|
+
try {
|
|
3200
|
+
return rule.migrate(ctx, rule.audit(ctx));
|
|
3201
|
+
} catch (err) {
|
|
3202
|
+
return {
|
|
3203
|
+
id: rule.id,
|
|
3204
|
+
title: rule.title,
|
|
3205
|
+
status: "blocked",
|
|
3206
|
+
summary: `migrate threw: ${err instanceof Error ? err.message : String(err)}`,
|
|
3207
|
+
changedFiles: [],
|
|
3208
|
+
details: []
|
|
3209
|
+
};
|
|
3176
3210
|
}
|
|
3177
|
-
}
|
|
3211
|
+
});
|
|
3212
|
+
const changedFiles = Array.from(new Set(results.flatMap((result) => result.changedFiles))).sort();
|
|
3213
|
+
return {
|
|
3214
|
+
repo: ctx.repoRoot,
|
|
3215
|
+
dryRun,
|
|
3216
|
+
ok: results.every((result) => result.status !== "blocked"),
|
|
3217
|
+
selectedRules: selected.map((rule) => rule.id),
|
|
3218
|
+
results,
|
|
3219
|
+
changedFiles
|
|
3220
|
+
};
|
|
3178
3221
|
}
|
|
3179
|
-
function
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
if (!project.slug) throw new Error(`Project ${key} missing slug`);
|
|
3183
|
-
if (project.slug !== key) throw new Error(`Project key ${key} does not match slug ${project.slug}`);
|
|
3184
|
-
if (!project.repo_path) throw new Error(`Project ${key} missing repo_path`);
|
|
3185
|
-
if (!Array.isArray(project.source_artifacts)) throw new Error(`Project ${key} source_artifacts must be a list`);
|
|
3186
|
-
if (!isRecord(project.ticket_provider)) throw new Error(`Project ${key} ticket_provider must be a mapping`);
|
|
3187
|
-
if (!isRecord(project.agents)) throw new Error(`Project ${key} agents must be a mapping`);
|
|
3222
|
+
function runMigration(selector, repoArg, dryRun, all) {
|
|
3223
|
+
const ruleIds = all ? RULES.map((rule) => rule.id) : selector ? [selector] : [];
|
|
3224
|
+
return runMigrationForRules(ruleIds, repoArg, dryRun);
|
|
3188
3225
|
}
|
|
3189
|
-
function
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
return path;
|
|
3226
|
+
function prettyTimestamp(iso) {
|
|
3227
|
+
const match = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})/.exec(iso);
|
|
3228
|
+
return match ? `${match[1]} ${match[2]} UTC` : iso;
|
|
3193
3229
|
}
|
|
3194
|
-
function
|
|
3195
|
-
|
|
3230
|
+
function formatAuditReport(report) {
|
|
3231
|
+
const counts = {};
|
|
3232
|
+
for (const rule of report.rules) counts[rule.status] = (counts[rule.status] ?? 0) + 1;
|
|
3233
|
+
const idWidth = report.rules.reduce((width, rule) => Math.max(width, rule.id.length), 0);
|
|
3234
|
+
const tally = [];
|
|
3235
|
+
if (counts.pass) tally.push(green(`${counts.pass} passed`));
|
|
3236
|
+
if (counts.fail) tally.push(red(`${counts.fail} failed`));
|
|
3237
|
+
if (counts.warn) tally.push(yellow(`${counts.warn} warning${counts.warn === 1 ? "" : "s"}`));
|
|
3238
|
+
if (counts.skip) tally.push(gray(`${counts.skip} skipped`));
|
|
3239
|
+
const overall = report.ok ? `${green(glyph.pass)} ${bold("Parity audit passed")}` : `${red(glyph.fail)} ${bold("Parity audit failed")}`;
|
|
3240
|
+
const lines = [""];
|
|
3241
|
+
lines.push(` ${overall}${tally.length ? ` ${dim(glyph.dot)} ${joinDot(tally)}` : ""}`);
|
|
3242
|
+
lines.push(` ${dim(report.repo)} ${dim(glyph.dot)} ${dim(prettyTimestamp(report.auditedAt))}`);
|
|
3243
|
+
lines.push("");
|
|
3244
|
+
for (const rule of report.rules) {
|
|
3245
|
+
const style = statusStyle(rule.status);
|
|
3246
|
+
lines.push(` ${style.color(style.glyph)} ${style.color(rule.id.padEnd(idWidth))} ${rule.summary}`);
|
|
3247
|
+
for (const detail of rule.details) lines.push(` ${dim(glyph.arrow)} ${dim(detail)}`);
|
|
3248
|
+
}
|
|
3249
|
+
lines.push("");
|
|
3250
|
+
return lines.join("\n");
|
|
3196
3251
|
}
|
|
3197
3252
|
|
|
3198
3253
|
// src/mcp-server.ts
|
|
@@ -3200,7 +3255,7 @@ var server = new McpServer({
|
|
|
3200
3255
|
name: "pjangler-mcp",
|
|
3201
3256
|
version: PJANGLER_VERSION
|
|
3202
3257
|
});
|
|
3203
|
-
var TICKET_PROVIDER_SCHEMA = z.enum(["plane", "
|
|
3258
|
+
var TICKET_PROVIDER_SCHEMA = z.enum(["plane", "trello"]);
|
|
3204
3259
|
function resolveTargetDir(targetDir) {
|
|
3205
3260
|
const dir = resolve3(targetDir ?? process.cwd());
|
|
3206
3261
|
if (!existsSync9(dir)) {
|
|
@@ -3391,6 +3446,9 @@ server.registerTool(
|
|
|
3391
3446
|
projectDescription: z.string().optional(),
|
|
3392
3447
|
projectSlug: z.string().optional(),
|
|
3393
3448
|
ticketProvider: TICKET_PROVIDER_SCHEMA.optional(),
|
|
3449
|
+
boardId: z.string().optional(),
|
|
3450
|
+
boardUrl: z.string().optional(),
|
|
3451
|
+
workspace: z.string().optional(),
|
|
3394
3452
|
planeWorkspace: z.string().optional(),
|
|
3395
3453
|
planeProjectId: z.string().optional(),
|
|
3396
3454
|
projectIdentifier: z.string().optional(),
|
|
@@ -3419,9 +3477,10 @@ server.registerTool(
|
|
|
3419
3477
|
const dryRun = input.dryRun ?? true;
|
|
3420
3478
|
const local = input.local ?? true;
|
|
3421
3479
|
const skipPlane = input.skipPlane ?? true;
|
|
3422
|
-
const
|
|
3423
|
-
|
|
3424
|
-
|
|
3480
|
+
const ticketProvider = input.ticketProvider ?? "plane";
|
|
3481
|
+
const boardId = input.boardId ?? input.planeProjectId ?? "";
|
|
3482
|
+
if (!skipPlane && ticketProvider === "plane" && !boardId) {
|
|
3483
|
+
throw new Error("boardId or planeProjectId is required when skipPlane=false for Plane; keep skipPlane=true for safe local bootstrap");
|
|
3425
3484
|
}
|
|
3426
3485
|
if (!dryRun && existsSync9(targetDir) && !overwrite) throw new Error(`Target already exists: ${targetDir} (set force/overwrite=true to re-render)`);
|
|
3427
3486
|
const plan = planProjectInit({
|
|
@@ -3437,9 +3496,12 @@ server.registerTool(
|
|
|
3437
3496
|
live: input.live ?? false,
|
|
3438
3497
|
registryPath: input.registryPath,
|
|
3439
3498
|
projectIdentifier: input.projectIdentifier ?? projectSlug.slice(0, 4).toUpperCase(),
|
|
3440
|
-
ticketProvider
|
|
3499
|
+
ticketProvider,
|
|
3500
|
+
boardId,
|
|
3501
|
+
boardUrl: input.boardUrl,
|
|
3502
|
+
boardWorkspace: input.workspace ?? input.planeWorkspace,
|
|
3441
3503
|
planeWorkspace: input.planeWorkspace ?? "33god",
|
|
3442
|
-
planeProjectId,
|
|
3504
|
+
planeProjectId: input.planeProjectId,
|
|
3443
3505
|
pjanglerRoot,
|
|
3444
3506
|
overwrite
|
|
3445
3507
|
});
|
|
@@ -3491,6 +3553,10 @@ server.registerTool(
|
|
|
3491
3553
|
live: z.boolean().optional(),
|
|
3492
3554
|
slug: z.string().optional(),
|
|
3493
3555
|
identifier: z.string().optional(),
|
|
3556
|
+
ticketProvider: TICKET_PROVIDER_SCHEMA.optional(),
|
|
3557
|
+
boardId: z.string().optional(),
|
|
3558
|
+
boardUrl: z.string().optional(),
|
|
3559
|
+
workspace: z.string().optional(),
|
|
3494
3560
|
registryPath: z.string().optional(),
|
|
3495
3561
|
force: z.boolean().optional()
|
|
3496
3562
|
}
|
|
@@ -3509,6 +3575,10 @@ server.registerTool(
|
|
|
3509
3575
|
live: input.live ?? false,
|
|
3510
3576
|
projectSlug: input.slug,
|
|
3511
3577
|
projectIdentifier: input.identifier,
|
|
3578
|
+
ticketProvider: input.ticketProvider,
|
|
3579
|
+
boardId: input.boardId,
|
|
3580
|
+
boardUrl: input.boardUrl,
|
|
3581
|
+
boardWorkspace: input.workspace,
|
|
3512
3582
|
registryPath: input.registryPath,
|
|
3513
3583
|
force: input.force ?? false,
|
|
3514
3584
|
overwrite: input.force ?? false
|
|
@@ -3616,7 +3686,7 @@ server.registerTool(
|
|
|
3616
3686
|
"pjangler_deploy_hermes_agent",
|
|
3617
3687
|
{
|
|
3618
3688
|
title: "Deploy Hermes agent",
|
|
3619
|
-
description: "Provision a Hermes agent role for @33god-projects. For safe MCP use local=true defaults skip runtime repo,
|
|
3689
|
+
description: "Provision a Hermes agent role for @33god-projects. For safe MCP use local=true defaults skip runtime repo, ticket-board creation, Bloodbank, and systemd; opt out with local=false plus explicit skip flags.",
|
|
3620
3690
|
inputSchema: {
|
|
3621
3691
|
targetDir: z.string(),
|
|
3622
3692
|
targetRepo: z.string().optional(),
|