@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/index.js
CHANGED
|
@@ -727,7 +727,7 @@ import * as p from "@clack/prompts";
|
|
|
727
727
|
function detectTicketProvider(targetDir) {
|
|
728
728
|
try {
|
|
729
729
|
const t = JSON.parse(readFileSync(join4(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
|
|
730
|
-
return t === "plane" || t === "
|
|
730
|
+
return t === "plane" || t === "trello" ? t : void 0;
|
|
731
731
|
} catch {
|
|
732
732
|
return void 0;
|
|
733
733
|
}
|
|
@@ -1201,70 +1201,540 @@ var HermesAgentRecipe = class extends Recipe {
|
|
|
1201
1201
|
};
|
|
1202
1202
|
|
|
1203
1203
|
// src/commands/AgentHooksCommands.ts
|
|
1204
|
-
import { homedir as
|
|
1205
|
-
import { join as
|
|
1206
|
-
import { existsSync as
|
|
1204
|
+
import { homedir as homedir4 } from "node:os";
|
|
1205
|
+
import { join as join9, dirname as dirname5 } from "node:path";
|
|
1206
|
+
import { existsSync as existsSync7, cpSync, mkdirSync as mkdirSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1207
1207
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1208
|
+
|
|
1209
|
+
// src/project/index.ts
|
|
1210
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
1211
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync2, renameSync, statSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1212
|
+
import { homedir as homedir3 } from "node:os";
|
|
1213
|
+
import { basename as basename2, dirname as dirname4, join as join8, resolve } from "node:path";
|
|
1214
|
+
import YAML from "yaml";
|
|
1215
|
+
var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
|
|
1216
|
+
var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
|
|
1217
|
+
var KNOWN_SKILL_ROOTS = [
|
|
1218
|
+
"/home/delorenj/code/skillex/all-skills",
|
|
1219
|
+
"/home/delorenj/code/CoachingAgentFramework/.agents/skills",
|
|
1220
|
+
"/home/delorenj/code/pjangler/.agents/skills",
|
|
1221
|
+
join8(homedir3(), ".codex", "skills")
|
|
1222
|
+
];
|
|
1223
|
+
function projectRegistryPath(env2 = process.env) {
|
|
1224
|
+
return expandHome(env2[PROJECT_REGISTRY_ENV] || join8(homedir3(), ".config", "pjangler", "projects.yaml"));
|
|
1225
|
+
}
|
|
1226
|
+
function emptyProjectRegistry() {
|
|
1227
|
+
return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
|
|
1228
|
+
}
|
|
1229
|
+
function loadProjectRegistry(path = projectRegistryPath()) {
|
|
1230
|
+
if (!existsSync6(path)) return emptyProjectRegistry();
|
|
1231
|
+
const raw = YAML.parse(readFileSync2(path, "utf8"));
|
|
1232
|
+
if (raw == null) return emptyProjectRegistry();
|
|
1233
|
+
if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
|
|
1234
|
+
const registry = raw;
|
|
1235
|
+
const normalized = {
|
|
1236
|
+
schema_version: Number(registry.schema_version ?? PROJECT_REGISTRY_SCHEMA_VERSION),
|
|
1237
|
+
projects: isRecord(registry.projects) ? registry.projects : {}
|
|
1238
|
+
};
|
|
1239
|
+
validateProjectRegistry(normalized);
|
|
1240
|
+
return normalized;
|
|
1241
|
+
}
|
|
1242
|
+
function saveProjectRegistry(registry, path = projectRegistryPath()) {
|
|
1243
|
+
validateProjectRegistry(registry);
|
|
1244
|
+
mkdirSync4(dirname4(path), { recursive: true });
|
|
1245
|
+
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
1246
|
+
writeFileSync3(temp, YAML.stringify(registry, { lineWidth: 0 }), "utf8");
|
|
1247
|
+
renameSync(temp, path);
|
|
1248
|
+
}
|
|
1249
|
+
function validateProjectRegistry(registry) {
|
|
1250
|
+
if (registry.schema_version !== PROJECT_REGISTRY_SCHEMA_VERSION) {
|
|
1251
|
+
throw new Error(`Unsupported project registry schema_version: ${registry.schema_version}`);
|
|
1212
1252
|
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1253
|
+
if (!isRecord(registry.projects)) throw new Error("Project registry projects must be a mapping");
|
|
1254
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
1255
|
+
const repoPaths = /* @__PURE__ */ new Map();
|
|
1256
|
+
const identifiers = /* @__PURE__ */ new Map();
|
|
1257
|
+
for (const [slug, project] of Object.entries(registry.projects)) {
|
|
1258
|
+
validateProjectRecord(project, slug);
|
|
1259
|
+
if (slugs.has(project.slug)) throw new Error(`Duplicate project slug: ${project.slug}`);
|
|
1260
|
+
slugs.add(project.slug);
|
|
1261
|
+
const repoKey = resolve(project.repo_path);
|
|
1262
|
+
const existingRepoSlug = repoPaths.get(repoKey);
|
|
1263
|
+
if (existingRepoSlug && existingRepoSlug !== slug) {
|
|
1264
|
+
throw new Error(`Duplicate project repo_path: ${project.repo_path} used by ${existingRepoSlug} and ${slug}`);
|
|
1265
|
+
}
|
|
1266
|
+
repoPaths.set(repoKey, slug);
|
|
1267
|
+
const identifier = project.ticket_provider.identifier?.toUpperCase();
|
|
1268
|
+
if (identifier) {
|
|
1269
|
+
const existingIdentifierSlug = identifiers.get(identifier);
|
|
1270
|
+
if (existingIdentifierSlug && existingIdentifierSlug !== slug) {
|
|
1271
|
+
throw new Error(`Duplicate project identifier: ${identifier} used by ${existingIdentifierSlug} and ${slug}`);
|
|
1272
|
+
}
|
|
1273
|
+
identifiers.set(identifier, slug);
|
|
1220
1274
|
}
|
|
1221
|
-
} catch {
|
|
1222
1275
|
}
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1276
|
+
}
|
|
1277
|
+
function normalizeTicketProvider(value) {
|
|
1278
|
+
const type = (value || "plane").trim().toLowerCase();
|
|
1279
|
+
if (type === "plane" || type === "trello") return type;
|
|
1280
|
+
throw new Error(`Unsupported ticket provider: ${value}. Supported providers: plane, trello`);
|
|
1281
|
+
}
|
|
1282
|
+
function buildTicketProviderBlock(input) {
|
|
1283
|
+
const type = normalizeTicketProvider(input.type);
|
|
1284
|
+
const boardId = input.boardId ?? "";
|
|
1285
|
+
if (type === "trello") {
|
|
1286
|
+
return {
|
|
1287
|
+
type,
|
|
1288
|
+
workspace: input.workspace ?? "",
|
|
1289
|
+
identifier: input.identifier,
|
|
1290
|
+
board_id: boardId,
|
|
1291
|
+
board_url: input.boardUrl ?? (boardId ? `https://trello.com/b/${boardId}` : ""),
|
|
1292
|
+
state: "planned"
|
|
1293
|
+
};
|
|
1226
1294
|
}
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1295
|
+
const workspace = input.workspace ?? "33god";
|
|
1296
|
+
return {
|
|
1297
|
+
type,
|
|
1298
|
+
workspace,
|
|
1299
|
+
identifier: input.identifier,
|
|
1300
|
+
board_id: boardId,
|
|
1301
|
+
board_url: input.boardUrl ?? (boardId ? `https://plane.delo.sh/${workspace}/projects/${boardId}/issues/` : ""),
|
|
1302
|
+
state: "planned"
|
|
1303
|
+
};
|
|
1230
1304
|
}
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1305
|
+
function slugifyProjectName(value) {
|
|
1306
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
1307
|
+
}
|
|
1308
|
+
function deriveProjectIdentifier(value) {
|
|
1309
|
+
const compact = value.replace(/[^A-Za-z0-9]/g, "").toUpperCase();
|
|
1310
|
+
const identifier = compact.slice(0, 4) || "PROJ";
|
|
1311
|
+
return identifier.length >= 2 ? identifier : `${identifier}XX`.slice(0, 4);
|
|
1312
|
+
}
|
|
1313
|
+
function normalizeAgentRole(value) {
|
|
1314
|
+
return value?.trim() || "pm";
|
|
1315
|
+
}
|
|
1316
|
+
function resolveAgentHooksLayer(input, env2 = process.env) {
|
|
1317
|
+
if (typeof input === "boolean") return input;
|
|
1318
|
+
const override = env2.PJ_AGENT_HOOKS_LAYER;
|
|
1319
|
+
if (override === "0" || override === "false") return false;
|
|
1320
|
+
if (override === "1" || override === "true") return true;
|
|
1321
|
+
return !existsSync6(join8(homedir3(), ".agents", "hooks"));
|
|
1322
|
+
}
|
|
1323
|
+
function jsonStable(value) {
|
|
1324
|
+
return JSON.stringify(value);
|
|
1325
|
+
}
|
|
1326
|
+
function projectRecordEquivalent(a, b) {
|
|
1327
|
+
if (!a) return false;
|
|
1328
|
+
const { created_at: _aCreated, updated_at: _aUpdated, ...aComparable } = a;
|
|
1329
|
+
const { created_at: _bCreated, updated_at: _bUpdated, ...bComparable } = b;
|
|
1330
|
+
return jsonStable(aComparable) === jsonStable(bComparable);
|
|
1331
|
+
}
|
|
1332
|
+
function defaultProjectTargetDir(name, cwd = process.cwd()) {
|
|
1333
|
+
const compactName = name.replace(/[^A-Za-z0-9._-]/g, "") || slugifyProjectName(name);
|
|
1334
|
+
return resolve(dirname4(resolve(cwd)), compactName);
|
|
1335
|
+
}
|
|
1336
|
+
function resolveSourceSkillPath(sourceSkill) {
|
|
1337
|
+
if (!sourceSkill) return void 0;
|
|
1338
|
+
const expanded = expandHome(sourceSkill);
|
|
1339
|
+
const direct = resolve(expanded);
|
|
1340
|
+
if (existsSync6(direct)) return direct;
|
|
1341
|
+
const name = basename2(sourceSkill);
|
|
1342
|
+
for (const root of KNOWN_SKILL_ROOTS) {
|
|
1343
|
+
const candidate = join8(root, name);
|
|
1344
|
+
if (existsSync6(candidate)) return candidate;
|
|
1345
|
+
}
|
|
1346
|
+
const civilWarLetterifier = "/home/delorenj/code/skillex/all-skills/civilwar-letterifier";
|
|
1347
|
+
const hint = existsSync6(civilWarLetterifier) ? ` Did you mean ${civilWarLetterifier}?` : "";
|
|
1348
|
+
throw new Error(`Source skill not found: ${sourceSkill}.${hint}`);
|
|
1349
|
+
}
|
|
1350
|
+
function planProjectInit(input) {
|
|
1351
|
+
if (!input.name.trim()) throw new Error("Project name is required");
|
|
1352
|
+
const registryPath2 = resolve(projectRegistryPath({ ...process.env, [PROJECT_REGISTRY_ENV]: input.registryPath || process.env[PROJECT_REGISTRY_ENV] }));
|
|
1353
|
+
const registry = loadProjectRegistry(registryPath2);
|
|
1354
|
+
const now = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1355
|
+
const slug = input.projectSlug ?? slugifyProjectName(input.name);
|
|
1356
|
+
const targetDir = resolve(input.targetDir ?? defaultProjectTargetDir(input.name, input.cwd));
|
|
1357
|
+
const identifier = (input.projectIdentifier ?? deriveProjectIdentifier(input.name)).toUpperCase();
|
|
1358
|
+
const existing = registry.projects[slug];
|
|
1359
|
+
const sourceSkillPath = resolveSourceSkillPath(input.sourceSkill);
|
|
1360
|
+
const overwrite = input.overwrite ?? input.force ?? false;
|
|
1361
|
+
const agentRole = normalizeAgentRole(input.agentRole);
|
|
1362
|
+
const agents = input.provisionAgent ? {
|
|
1363
|
+
...existing?.agents ?? {},
|
|
1364
|
+
[agentRole]: {
|
|
1365
|
+
role: agentRole,
|
|
1366
|
+
provisioning_state: "planned"
|
|
1238
1367
|
}
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
if (existsSync6(dest) && !this.context.force) {
|
|
1253
|
-
skipped.push(rel);
|
|
1254
|
-
continue;
|
|
1368
|
+
} : existing?.agents ?? {};
|
|
1369
|
+
const scaffold = input.scaffold ?? true;
|
|
1370
|
+
const candidateProject = {
|
|
1371
|
+
name: input.name,
|
|
1372
|
+
slug,
|
|
1373
|
+
repo_path: targetDir,
|
|
1374
|
+
description: input.description ?? "",
|
|
1375
|
+
status: "planned",
|
|
1376
|
+
source_artifacts: sourceSkillPath ? [{ kind: "skill", path: sourceSkillPath, package_name: input.packageName ?? slug }] : [],
|
|
1377
|
+
template: {
|
|
1378
|
+
commonproject: {
|
|
1379
|
+
enabled: true,
|
|
1380
|
+
primary_language: input.primaryLanguage ?? "python"
|
|
1255
1381
|
}
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1382
|
+
},
|
|
1383
|
+
ticket_provider: buildTicketProviderBlock({
|
|
1384
|
+
type: input.ticketProvider ?? "plane",
|
|
1385
|
+
identifier,
|
|
1386
|
+
boardId: input.boardId ?? input.planeProjectId,
|
|
1387
|
+
boardUrl: input.boardUrl,
|
|
1388
|
+
workspace: input.boardWorkspace ?? input.planeWorkspace
|
|
1389
|
+
}),
|
|
1390
|
+
agents,
|
|
1391
|
+
created_at: existing?.created_at ?? now,
|
|
1392
|
+
updated_at: now
|
|
1393
|
+
};
|
|
1394
|
+
const project = {
|
|
1395
|
+
...candidateProject,
|
|
1396
|
+
updated_at: projectRecordEquivalent(existing, candidateProject) ? existing.updated_at : now
|
|
1397
|
+
};
|
|
1398
|
+
validateNoDuplicateProject(registry, project, overwrite);
|
|
1399
|
+
const pjanglerRoot = resolve(input.pjanglerRoot ?? resolvePjanglerRoot());
|
|
1400
|
+
const manifest = projectManifestFromRegistryProject(project);
|
|
1401
|
+
const apply = input.apply ?? false;
|
|
1402
|
+
const live = input.live ?? false;
|
|
1403
|
+
const actions = [
|
|
1404
|
+
{ kind: "registry.upsert", registryPath: registryPath2, slug, project }
|
|
1405
|
+
];
|
|
1406
|
+
if (scaffold) {
|
|
1407
|
+
actions.push(buildCommonProjectCopierAction({
|
|
1408
|
+
pjanglerRoot,
|
|
1409
|
+
targetDir,
|
|
1410
|
+
projectName: project.name,
|
|
1411
|
+
projectDescription: project.description,
|
|
1412
|
+
projectSlug: project.slug,
|
|
1413
|
+
ticketProvider: project.ticket_provider.type,
|
|
1414
|
+
planeWorkspace: project.ticket_provider.workspace ?? "33god",
|
|
1415
|
+
planeProjectId: project.ticket_provider.board_id ?? "",
|
|
1416
|
+
ticketWorkspace: project.ticket_provider.workspace ?? "",
|
|
1417
|
+
boardId: project.ticket_provider.board_id ?? "",
|
|
1418
|
+
boardUrl: project.ticket_provider.board_url ?? "",
|
|
1419
|
+
projectIdentifier: identifier,
|
|
1420
|
+
primaryLanguage: project.template.commonproject.primary_language,
|
|
1421
|
+
agentHooksLayer: resolveAgentHooksLayer(input.agentHooksLayer),
|
|
1422
|
+
overwrite
|
|
1423
|
+
}));
|
|
1424
|
+
}
|
|
1425
|
+
actions.push(
|
|
1426
|
+
{ kind: "project.write-manifest", path: join8(targetDir, ".project.json"), manifest },
|
|
1427
|
+
{
|
|
1428
|
+
kind: "ticket-provider.create-or-link",
|
|
1429
|
+
enabled: live,
|
|
1430
|
+
live,
|
|
1431
|
+
provider: project.ticket_provider.type,
|
|
1432
|
+
workspace: project.ticket_provider.workspace ?? "33god",
|
|
1433
|
+
identifier,
|
|
1434
|
+
state: live ? "planned" : "planned",
|
|
1435
|
+
reason: live ? void 0 : "network/cloud actions require --live"
|
|
1436
|
+
},
|
|
1437
|
+
{
|
|
1438
|
+
kind: "hermes.provision-agent",
|
|
1439
|
+
enabled: input.provisionAgent ?? false,
|
|
1440
|
+
local: !live,
|
|
1441
|
+
targetDir,
|
|
1442
|
+
targetRepo: slug,
|
|
1443
|
+
role: agentRole,
|
|
1444
|
+
context: {
|
|
1445
|
+
skipRuntimeRepo: !live,
|
|
1446
|
+
skipPlane: !live,
|
|
1447
|
+
skipBloodbank: !live,
|
|
1448
|
+
skipSystemd: !live || process.platform === "darwin"
|
|
1259
1449
|
}
|
|
1260
|
-
created.push(rel);
|
|
1261
1450
|
}
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1451
|
+
);
|
|
1452
|
+
return { ok: true, apply, dryRun: !apply, live, registryPath: registryPath2, project, manifest, actions };
|
|
1453
|
+
}
|
|
1454
|
+
function executeProjectInitPlan(plan) {
|
|
1455
|
+
const logs = [];
|
|
1456
|
+
const errors = [];
|
|
1457
|
+
const changedFiles = [];
|
|
1458
|
+
if (!plan.apply) return { ok: true, plan, logs, errors, changedFiles };
|
|
1459
|
+
const registry = loadProjectRegistry(plan.registryPath);
|
|
1460
|
+
let pendingRegistryAction;
|
|
1461
|
+
for (const action of plan.actions) {
|
|
1462
|
+
if (action.kind === "copier.copy.commonproject") {
|
|
1463
|
+
logs.push(
|
|
1464
|
+
action.data.agent_hooks_layer === "false" ? "commonproject: agent-hooks layer skipped (global ~/.agents/hooks detected \u2014 no per-user CLI injection)" : "commonproject: agent-hooks layer included"
|
|
1465
|
+
);
|
|
1466
|
+
mkdirSync4(dirname4(action.targetDir), { recursive: true });
|
|
1467
|
+
const result = spawnSync4(action.command[0], action.command.slice(1), { encoding: "utf8", cwd: action.cwd });
|
|
1468
|
+
if (result.stdout?.trim()) logs.push(result.stdout.trim());
|
|
1469
|
+
if (result.stderr?.trim()) logs.push(result.stderr.trim());
|
|
1470
|
+
if (result.error) {
|
|
1471
|
+
const code = result.error.code;
|
|
1472
|
+
errors.push(
|
|
1473
|
+
code === "ENOENT" ? "copier not found on PATH. Install with: uv tool install copier or pip install copier" : `copier failed: ${result.error.message}`
|
|
1474
|
+
);
|
|
1475
|
+
break;
|
|
1476
|
+
}
|
|
1477
|
+
if (result.status !== 0) {
|
|
1478
|
+
errors.push(`copier exited with status ${result.status ?? "unknown"}`);
|
|
1479
|
+
if (existsSync6(action.targetDir)) changedFiles.push(action.targetDir);
|
|
1480
|
+
break;
|
|
1481
|
+
}
|
|
1482
|
+
changedFiles.push(action.targetDir);
|
|
1483
|
+
} else if (action.kind === "project.write-manifest") {
|
|
1484
|
+
mkdirSync4(dirname4(action.path), { recursive: true });
|
|
1485
|
+
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
1486
|
+
`;
|
|
1487
|
+
const current = existsSync6(action.path) ? readFileSync2(action.path, "utf8") : void 0;
|
|
1488
|
+
if (current !== next) {
|
|
1489
|
+
writeFileSync3(action.path, next, "utf8");
|
|
1490
|
+
changedFiles.push(action.path);
|
|
1491
|
+
}
|
|
1492
|
+
} else if (action.kind === "registry.upsert") {
|
|
1493
|
+
pendingRegistryAction = action;
|
|
1494
|
+
} else if (action.kind === "ticket-provider.create-or-link") {
|
|
1495
|
+
logs.push(action.enabled ? "ticket-provider.create-or-link requires a live provider integration" : "ticket-provider.create-or-link skipped (requires --live)");
|
|
1496
|
+
} else if (action.kind === "hermes.provision-agent") {
|
|
1497
|
+
logs.push(action.enabled ? "hermes.provision-agent planned for the caller to execute" : "hermes.provision-agent skipped");
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
if (pendingRegistryAction && errors.length === 0) {
|
|
1501
|
+
if (!projectRecordEquivalent(registry.projects[pendingRegistryAction.slug], pendingRegistryAction.project)) {
|
|
1502
|
+
registry.projects[pendingRegistryAction.slug] = pendingRegistryAction.project;
|
|
1503
|
+
saveProjectRegistry(registry, pendingRegistryAction.registryPath);
|
|
1504
|
+
changedFiles.push(pendingRegistryAction.registryPath);
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
return { ok: errors.length === 0, plan, logs, errors, changedFiles };
|
|
1508
|
+
}
|
|
1509
|
+
function projectManifestFromRegistryProject(project) {
|
|
1510
|
+
const agents = Object.fromEntries(
|
|
1511
|
+
Object.entries(project.agents).map(([name, agent]) => [
|
|
1512
|
+
`${project.slug}-${name}`,
|
|
1513
|
+
{
|
|
1514
|
+
role: agent.role,
|
|
1515
|
+
role_dir: agent.role_dir,
|
|
1516
|
+
provisioning_state: agent.provisioning_state
|
|
1517
|
+
}
|
|
1518
|
+
])
|
|
1519
|
+
);
|
|
1520
|
+
return {
|
|
1521
|
+
project_name: project.name,
|
|
1522
|
+
project_description: project.description,
|
|
1523
|
+
project_slug: project.slug,
|
|
1524
|
+
repo_path: project.repo_path,
|
|
1525
|
+
ticket_provider: {
|
|
1526
|
+
type: project.ticket_provider.type,
|
|
1527
|
+
workspace: project.ticket_provider.workspace ?? "",
|
|
1528
|
+
identifier: project.ticket_provider.identifier ?? "",
|
|
1529
|
+
board_id: project.ticket_provider.board_id ?? "",
|
|
1530
|
+
board_url: project.ticket_provider.board_url ?? "",
|
|
1531
|
+
state: project.ticket_provider.state
|
|
1532
|
+
},
|
|
1533
|
+
agents
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
function formatProjectInitPlan(plan) {
|
|
1537
|
+
const lines = [""];
|
|
1538
|
+
const title = `${bold(plan.project.name)} ${dim(`(${plan.project.slug})`)}`;
|
|
1539
|
+
lines.push(` ${cyan(bold(glyph.chevron))} ${title}${plan.dryRun ? ` ${dim(glyph.dot)} ${yellow("dry run")}` : ""}`);
|
|
1540
|
+
lines.push(` ${dim("registry".padEnd(8))} ${dim(plan.registryPath)}`);
|
|
1541
|
+
lines.push(` ${dim("target".padEnd(8))} ${dim(plan.project.repo_path)}`);
|
|
1542
|
+
lines.push("");
|
|
1543
|
+
lines.push(` ${bold("Actions")} ${dim(`(${plan.actions.length})`)}`);
|
|
1544
|
+
if (!plan.actions.length) lines.push(` ${dim("(nothing to do)")}`);
|
|
1545
|
+
for (const action of plan.actions) {
|
|
1546
|
+
lines.push(` ${cyan(glyph.bullet)} ${action.kind}`);
|
|
1547
|
+
if (action.kind === "copier.copy.commonproject") lines.push(` ${dim(`target: ${action.targetDir}`)}`);
|
|
1548
|
+
if (action.kind === "project.write-manifest") lines.push(` ${dim(`path: ${action.path}`)}`);
|
|
1549
|
+
if (action.kind === "ticket-provider.create-or-link" && action.reason) lines.push(` ${dim(`note: ${action.reason}`)}`);
|
|
1550
|
+
}
|
|
1551
|
+
lines.push("");
|
|
1552
|
+
return lines.join("\n");
|
|
1553
|
+
}
|
|
1554
|
+
function formatProjectList(registry) {
|
|
1555
|
+
const projects = Object.values(registry.projects).sort((a, b) => a.slug.localeCompare(b.slug));
|
|
1556
|
+
if (!projects.length) return `
|
|
1557
|
+
${dim("No projects registered.")}
|
|
1558
|
+
`;
|
|
1559
|
+
const slugWidth = projects.reduce((width, project) => Math.max(width, project.slug.length), 0);
|
|
1560
|
+
const idWidth = projects.reduce((width, project) => Math.max(width, String(project.ticket_provider.identifier ?? "").length), 0);
|
|
1561
|
+
const statusWidth = projects.reduce((width, project) => Math.max(width, project.status.length), 0);
|
|
1562
|
+
const lines = ["", ` ${bold("Projects")} ${dim(`(${projects.length})`)}`, ""];
|
|
1563
|
+
for (const project of projects) {
|
|
1564
|
+
const slug = bold(project.slug.padEnd(slugWidth));
|
|
1565
|
+
const identifier = cyan(String(project.ticket_provider.identifier ?? "").padEnd(idWidth));
|
|
1566
|
+
const status = projectStatusColor(project.status)(project.status.padEnd(statusWidth));
|
|
1567
|
+
lines.push(` ${slug} ${identifier} ${status} ${dim(project.repo_path)}`);
|
|
1568
|
+
}
|
|
1569
|
+
lines.push("");
|
|
1570
|
+
return lines.join("\n");
|
|
1571
|
+
}
|
|
1572
|
+
function getProject(registry, slug) {
|
|
1573
|
+
const project = registry.projects[slug];
|
|
1574
|
+
if (!project) throw new Error(`Project not found in registry: ${slug}`);
|
|
1575
|
+
return project;
|
|
1576
|
+
}
|
|
1577
|
+
function doctorProjectRegistry(registryPath2 = projectRegistryPath(), slug) {
|
|
1578
|
+
const issues = [];
|
|
1579
|
+
const registry = loadProjectRegistry(registryPath2);
|
|
1580
|
+
const projects = slug ? [[slug, getProject(registry, slug)]] : Object.entries(registry.projects);
|
|
1581
|
+
for (const [projectSlug, project] of projects) {
|
|
1582
|
+
if (!existsSync6(project.repo_path)) {
|
|
1583
|
+
issues.push({ level: "warn", slug: projectSlug, message: `repo_path does not exist: ${project.repo_path}` });
|
|
1584
|
+
} else if (!statSync(project.repo_path).isDirectory()) {
|
|
1585
|
+
issues.push({ level: "error", slug: projectSlug, message: `repo_path is not a directory: ${project.repo_path}` });
|
|
1586
|
+
} else {
|
|
1587
|
+
const manifestPath = join8(project.repo_path, ".project.json");
|
|
1588
|
+
if (!existsSync6(manifestPath)) issues.push({ level: "warn", slug: projectSlug, message: ".project.json is missing" });
|
|
1589
|
+
}
|
|
1590
|
+
for (const artifact of project.source_artifacts) {
|
|
1591
|
+
if (artifact.path && !existsSync6(artifact.path)) {
|
|
1592
|
+
issues.push({ level: "warn", slug: projectSlug, message: `source artifact missing: ${artifact.path}` });
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
return {
|
|
1597
|
+
ok: !issues.some((issue) => issue.level === "error"),
|
|
1598
|
+
registryPath: registryPath2,
|
|
1599
|
+
checkedProjects: projects.map(([projectSlug]) => projectSlug),
|
|
1600
|
+
issues
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
function buildCommonProjectCopierAction(input) {
|
|
1604
|
+
const templateDir = join8(input.pjanglerRoot, "templates", "commonproject");
|
|
1605
|
+
const data = {
|
|
1606
|
+
project_name: input.projectName,
|
|
1607
|
+
project_description: input.projectDescription ?? "",
|
|
1608
|
+
project_slug: input.projectSlug,
|
|
1609
|
+
ticket_provider: input.ticketProvider,
|
|
1610
|
+
plane_workspace: input.planeWorkspace,
|
|
1611
|
+
plane_project_id: input.planeProjectId ?? "",
|
|
1612
|
+
ticket_workspace: input.ticketWorkspace ?? input.planeWorkspace,
|
|
1613
|
+
board_id: input.boardId ?? input.planeProjectId ?? "",
|
|
1614
|
+
board_url: input.boardUrl ?? "",
|
|
1615
|
+
project_identifier: input.projectIdentifier,
|
|
1616
|
+
primary_language: input.primaryLanguage,
|
|
1617
|
+
agent_hooks_layer: input.agentHooksLayer ?? true ? "true" : "false"
|
|
1618
|
+
};
|
|
1619
|
+
const command = ["copier", "copy", "--trust", templateDir, input.targetDir, "--defaults"];
|
|
1620
|
+
for (const [key, value] of Object.entries(data)) command.push("--data", `${key}=${value}`);
|
|
1621
|
+
if (input.overwrite) command.push("--overwrite");
|
|
1622
|
+
return {
|
|
1623
|
+
kind: "copier.copy.commonproject",
|
|
1624
|
+
cwd: input.pjanglerRoot,
|
|
1625
|
+
command,
|
|
1626
|
+
targetDir: input.targetDir,
|
|
1627
|
+
data,
|
|
1628
|
+
overwrite: input.overwrite
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
function resolvePjanglerRoot() {
|
|
1632
|
+
let dir = dirname4(new URL(import.meta.url).pathname);
|
|
1633
|
+
while (dir !== dirname4(dir)) {
|
|
1634
|
+
if (existsSync6(join8(dir, "package.json")) && existsSync6(join8(dir, "templates", "commonproject", "copier.yml"))) return dir;
|
|
1635
|
+
dir = dirname4(dir);
|
|
1636
|
+
}
|
|
1637
|
+
return resolve(process.cwd());
|
|
1638
|
+
}
|
|
1639
|
+
function validateNoDuplicateProject(registry, project, overwrite) {
|
|
1640
|
+
const existingSameSlug = registry.projects[project.slug];
|
|
1641
|
+
if (existingSameSlug && !overwrite && resolve(existingSameSlug.repo_path) !== resolve(project.repo_path)) {
|
|
1642
|
+
throw new Error(`Project slug already exists in registry: ${project.slug}`);
|
|
1643
|
+
}
|
|
1644
|
+
for (const [slug, existing] of Object.entries(registry.projects)) {
|
|
1645
|
+
if (slug === project.slug) continue;
|
|
1646
|
+
if (resolve(existing.repo_path) === resolve(project.repo_path)) {
|
|
1647
|
+
throw new Error(`Project repo_path already registered by ${slug}: ${project.repo_path}`);
|
|
1648
|
+
}
|
|
1649
|
+
if (existing.ticket_provider.identifier && existing.ticket_provider.identifier.toUpperCase() === project.ticket_provider.identifier?.toUpperCase()) {
|
|
1650
|
+
throw new Error(`Project identifier already registered by ${slug}: ${project.ticket_provider.identifier}`);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
function validateProjectRecord(project, key) {
|
|
1655
|
+
if (!isRecord(project)) throw new Error(`Project ${key} must be a mapping`);
|
|
1656
|
+
if (!project.name) throw new Error(`Project ${key} missing name`);
|
|
1657
|
+
if (!project.slug) throw new Error(`Project ${key} missing slug`);
|
|
1658
|
+
if (project.slug !== key) throw new Error(`Project key ${key} does not match slug ${project.slug}`);
|
|
1659
|
+
if (!project.repo_path) throw new Error(`Project ${key} missing repo_path`);
|
|
1660
|
+
if (!Array.isArray(project.source_artifacts)) throw new Error(`Project ${key} source_artifacts must be a list`);
|
|
1661
|
+
if (!isRecord(project.ticket_provider)) throw new Error(`Project ${key} ticket_provider must be a mapping`);
|
|
1662
|
+
if (!isRecord(project.agents)) throw new Error(`Project ${key} agents must be a mapping`);
|
|
1663
|
+
}
|
|
1664
|
+
function expandHome(path) {
|
|
1665
|
+
if (path === "~") return homedir3();
|
|
1666
|
+
if (path.startsWith("~/")) return join8(homedir3(), path.slice(2));
|
|
1667
|
+
return path;
|
|
1668
|
+
}
|
|
1669
|
+
function isRecord(value) {
|
|
1670
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// src/commands/AgentHooksCommands.ts
|
|
1674
|
+
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.";
|
|
1675
|
+
function resolveTemplateRoot() {
|
|
1676
|
+
const candidates = [];
|
|
1677
|
+
if (process.env.PJANGLER_COMMONPROJECT_TEMPLATE) {
|
|
1678
|
+
candidates.push(process.env.PJANGLER_COMMONPROJECT_TEMPLATE);
|
|
1679
|
+
}
|
|
1680
|
+
try {
|
|
1681
|
+
let dir = dirname5(fileURLToPath2(import.meta.url));
|
|
1682
|
+
for (let i = 0; i < 8; i++) {
|
|
1683
|
+
candidates.push(join9(dir, "templates", "commonproject", "template"));
|
|
1684
|
+
const parent = dirname5(dir);
|
|
1685
|
+
if (parent === dir) break;
|
|
1686
|
+
dir = parent;
|
|
1687
|
+
}
|
|
1688
|
+
} catch {
|
|
1689
|
+
}
|
|
1690
|
+
candidates.push(join9(homedir4(), "code", "pjangler", "templates", "commonproject", "template"));
|
|
1691
|
+
for (const c of candidates) {
|
|
1692
|
+
if (existsSync7(join9(c, ".agents", "hooks", "hooks.master.json"))) return c;
|
|
1693
|
+
}
|
|
1694
|
+
throw new Error(
|
|
1695
|
+
"Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
1698
|
+
var CopyAgentHooksTree = class extends Command {
|
|
1699
|
+
async invoke() {
|
|
1700
|
+
if (!resolveAgentHooksLayer()) {
|
|
1701
|
+
return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
|
|
1702
|
+
}
|
|
1703
|
+
let templateRoot;
|
|
1704
|
+
try {
|
|
1705
|
+
templateRoot = resolveTemplateRoot();
|
|
1706
|
+
} catch (e) {
|
|
1707
|
+
return { success: false, message: `\u26A0\uFE0F ${e.message}` };
|
|
1708
|
+
}
|
|
1709
|
+
const items = [
|
|
1710
|
+
{ rel: ".agents/hooks", dir: true },
|
|
1711
|
+
{ rel: ".agents/local.example.json", dir: false },
|
|
1712
|
+
{ rel: ".mise/scripts/link-project-skills-to-clis.sh", dir: false },
|
|
1713
|
+
{ rel: ".mise/scripts/unlink-project-skills-from-clis.sh", dir: false },
|
|
1714
|
+
{ rel: ".mise/scripts/hindsight-setup.sh", dir: false }
|
|
1715
|
+
];
|
|
1716
|
+
const created = [];
|
|
1717
|
+
const skipped = [];
|
|
1718
|
+
for (const { rel, dir } of items) {
|
|
1719
|
+
const src = join9(templateRoot, rel);
|
|
1720
|
+
const dest = join9(this.context.targetDir, rel);
|
|
1721
|
+
if (!existsSync7(src)) continue;
|
|
1722
|
+
if (existsSync7(dest) && !this.context.force) {
|
|
1723
|
+
skipped.push(rel);
|
|
1724
|
+
continue;
|
|
1725
|
+
}
|
|
1726
|
+
if (!this.context.dryRun) {
|
|
1727
|
+
mkdirSync5(dirname5(dest), { recursive: true });
|
|
1728
|
+
cpSync(src, dest, { recursive: dir, force: true });
|
|
1729
|
+
}
|
|
1730
|
+
created.push(rel);
|
|
1731
|
+
}
|
|
1732
|
+
const verb = this.context.dryRun ? "Would copy" : "Copied";
|
|
1733
|
+
const tail = skipped.length ? ` (${skipped.length} already present \u2014 use --force to overwrite)` : "";
|
|
1734
|
+
return {
|
|
1735
|
+
success: created.length > 0,
|
|
1736
|
+
message: this.formatMessage(`\u2705 ${verb} ${created.length} agent-hooks path(s)${tail}`)
|
|
1737
|
+
};
|
|
1268
1738
|
}
|
|
1269
1739
|
};
|
|
1270
1740
|
var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
@@ -1272,14 +1742,17 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
|
1272
1742
|
static CR = "{{config_root}}";
|
|
1273
1743
|
// mise's own runtime var — emitted literally
|
|
1274
1744
|
async invoke() {
|
|
1275
|
-
|
|
1276
|
-
|
|
1745
|
+
if (!resolveAgentHooksLayer()) {
|
|
1746
|
+
return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
|
|
1747
|
+
}
|
|
1748
|
+
const misePath = join9(this.context.targetDir, "mise.toml");
|
|
1749
|
+
if (!existsSync7(misePath)) {
|
|
1277
1750
|
return {
|
|
1278
1751
|
success: false,
|
|
1279
1752
|
message: "\u26A0\uFE0F No mise.toml found \u2014 run `pjangler init mise` first, then re-run."
|
|
1280
1753
|
};
|
|
1281
1754
|
}
|
|
1282
|
-
let content =
|
|
1755
|
+
let content = readFileSync3(misePath, "utf8");
|
|
1283
1756
|
if (content.includes(_WireMiseAgentHooks.MARKER)) {
|
|
1284
1757
|
return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
|
|
1285
1758
|
}
|
|
@@ -1355,7 +1828,7 @@ ${leaveBlock}`);
|
|
|
1355
1828
|
""
|
|
1356
1829
|
].join("\n");
|
|
1357
1830
|
content = content.replace(/\n*$/, "\n") + appended;
|
|
1358
|
-
if (!this.context.dryRun)
|
|
1831
|
+
if (!this.context.dryRun) writeFileSync4(misePath, content);
|
|
1359
1832
|
if (wiredHooks) {
|
|
1360
1833
|
return { success: true, message: this.formatMessage("\u2705 Wired mise.toml ([hooks] enter/leave + tasks)") };
|
|
1361
1834
|
}
|
|
@@ -1528,11 +2001,11 @@ function createRecipe(name, context) {
|
|
|
1528
2001
|
import { cancel as cancel2, multiselect, text as text2, isCancel as isCancel5 } from "@clack/prompts";
|
|
1529
2002
|
|
|
1530
2003
|
// src/parity/index.ts
|
|
1531
|
-
import { existsSync as
|
|
1532
|
-
import { basename as
|
|
2004
|
+
import { existsSync as existsSync8, lstatSync, mkdirSync as mkdirSync6, readFileSync as readFileSync4, readlinkSync, readdirSync, renameSync as renameSync2, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5, chmodSync as chmodSync2, copyFileSync } from "node:fs";
|
|
2005
|
+
import { basename as basename3, dirname as dirname6, join as join10, relative, resolve as resolve2 } from "node:path";
|
|
1533
2006
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1534
|
-
import { homedir as
|
|
1535
|
-
import { spawnSync as
|
|
2007
|
+
import { homedir as homedir5 } from "node:os";
|
|
2008
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
1536
2009
|
var LINK_AGENTFILES_BLOCK = `# This block will handle the linking of
|
|
1537
2010
|
# agent files to the main AGENTS.md file.
|
|
1538
2011
|
#
|
|
@@ -1600,13 +2073,13 @@ run = "{{config_root}}/.mise/scripts/versioning.sh check"
|
|
|
1600
2073
|
description = "Force every versioned file up to the highest version"
|
|
1601
2074
|
run = "{{config_root}}/.mise/scripts/versioning.sh sync"
|
|
1602
2075
|
# <<< mise-versioning <<<`;
|
|
1603
|
-
function
|
|
1604
|
-
let dir =
|
|
1605
|
-
while (dir !==
|
|
1606
|
-
if (
|
|
2076
|
+
function resolvePjanglerRoot2() {
|
|
2077
|
+
let dir = dirname6(fileURLToPath3(import.meta.url));
|
|
2078
|
+
while (dir !== dirname6(dir)) {
|
|
2079
|
+
if (existsSync8(join10(dir, "package.json")) && existsSync8(join10(dir, "templates", "commonproject", "copier.yml"))) {
|
|
1607
2080
|
return dir;
|
|
1608
2081
|
}
|
|
1609
|
-
dir =
|
|
2082
|
+
dir = dirname6(dir);
|
|
1610
2083
|
}
|
|
1611
2084
|
throw new Error("Unable to resolve pjangler root");
|
|
1612
2085
|
}
|
|
@@ -1614,17 +2087,17 @@ function normalizeNewlines(value) {
|
|
|
1614
2087
|
return value.replace(/\r\n/g, "\n");
|
|
1615
2088
|
}
|
|
1616
2089
|
function readText(path) {
|
|
1617
|
-
return normalizeNewlines(
|
|
2090
|
+
return normalizeNewlines(readFileSync4(path, "utf8"));
|
|
1618
2091
|
}
|
|
1619
2092
|
function safeReadText(path) {
|
|
1620
|
-
return
|
|
2093
|
+
return existsSync8(path) ? readText(path) : null;
|
|
1621
2094
|
}
|
|
1622
2095
|
function ensureParent(path) {
|
|
1623
|
-
|
|
2096
|
+
mkdirSync6(dirname6(path), { recursive: true });
|
|
1624
2097
|
}
|
|
1625
2098
|
function writeText(path, content) {
|
|
1626
2099
|
ensureParent(path);
|
|
1627
|
-
|
|
2100
|
+
writeFileSync5(path, content);
|
|
1628
2101
|
}
|
|
1629
2102
|
function tryParseJson(text3) {
|
|
1630
2103
|
if (!text3) return null;
|
|
@@ -1641,7 +2114,7 @@ function titleCaseSlug(slug) {
|
|
|
1641
2114
|
return slug.split(/[-_]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
1642
2115
|
}
|
|
1643
2116
|
function readSymlinkTarget(path) {
|
|
1644
|
-
if (!
|
|
2117
|
+
if (!existsSync8(path)) return null;
|
|
1645
2118
|
try {
|
|
1646
2119
|
return readlinkSync(path);
|
|
1647
2120
|
} catch {
|
|
@@ -1649,7 +2122,7 @@ function readSymlinkTarget(path) {
|
|
|
1649
2122
|
}
|
|
1650
2123
|
}
|
|
1651
2124
|
function ensureSymlink(path, target, dryRun) {
|
|
1652
|
-
if (
|
|
2125
|
+
if (existsSync8(path)) {
|
|
1653
2126
|
const stat = lstatSync(path);
|
|
1654
2127
|
if (stat.isSymbolicLink()) {
|
|
1655
2128
|
const current = readSymlinkTarget(path);
|
|
@@ -1666,21 +2139,21 @@ function ensureSymlink(path, target, dryRun) {
|
|
|
1666
2139
|
return { changed: true };
|
|
1667
2140
|
}
|
|
1668
2141
|
function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
1669
|
-
const agentsPath =
|
|
1670
|
-
if (
|
|
2142
|
+
const agentsPath = join10(repoRoot, "AGENTS.md");
|
|
2143
|
+
if (existsSync8(agentsPath)) return { changedFiles: [], details: [] };
|
|
1671
2144
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
1672
|
-
const source =
|
|
1673
|
-
if (!
|
|
2145
|
+
const source = join10(repoRoot, file);
|
|
2146
|
+
if (!existsSync8(source)) continue;
|
|
1674
2147
|
const stat = lstatSync(source);
|
|
1675
2148
|
if (stat.isSymbolicLink()) continue;
|
|
1676
2149
|
if (stat.isFile()) {
|
|
1677
|
-
if (!dryRun)
|
|
2150
|
+
if (!dryRun) renameSync2(source, agentsPath);
|
|
1678
2151
|
return { changedFiles: [agentsPath], details: [`Moved ${file} to AGENTS.md before wiring agent-file symlinks`] };
|
|
1679
2152
|
}
|
|
1680
2153
|
return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
|
|
1681
2154
|
}
|
|
1682
|
-
const readmePath =
|
|
1683
|
-
if (
|
|
2155
|
+
const readmePath = join10(repoRoot, "README.md");
|
|
2156
|
+
if (existsSync8(readmePath)) {
|
|
1684
2157
|
const stat = lstatSync(readmePath);
|
|
1685
2158
|
if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
|
|
1686
2159
|
if (!dryRun) copyFileSync(readmePath, agentsPath);
|
|
@@ -1719,12 +2192,12 @@ function yamlGet(text3, keyPath) {
|
|
|
1719
2192
|
return "";
|
|
1720
2193
|
}
|
|
1721
2194
|
function discoverRoles(repoRoot) {
|
|
1722
|
-
const rolesDir =
|
|
1723
|
-
if (!
|
|
2195
|
+
const rolesDir = join10(repoRoot, "agents", "hermes");
|
|
2196
|
+
if (!existsSync8(rolesDir)) return [];
|
|
1724
2197
|
return readdirSync(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
|
|
1725
|
-
const roleDir =
|
|
1726
|
-
const roleYamlPath =
|
|
1727
|
-
if (!
|
|
2198
|
+
const roleDir = join10(rolesDir, entry.name);
|
|
2199
|
+
const roleYamlPath = join10(roleDir, "role.yaml");
|
|
2200
|
+
if (!existsSync8(roleYamlPath)) return null;
|
|
1728
2201
|
const text3 = readText(roleYamlPath);
|
|
1729
2202
|
const runtimeRepoRaw = yamlGet(text3, "runtime.github_repo");
|
|
1730
2203
|
return {
|
|
@@ -1748,10 +2221,10 @@ function discoverRoles(repoRoot) {
|
|
|
1748
2221
|
}).filter((value) => Boolean(value));
|
|
1749
2222
|
}
|
|
1750
2223
|
function registryPath(homeDir) {
|
|
1751
|
-
return
|
|
2224
|
+
return join10(homeDir, ".hermes", "agents-registry.yaml");
|
|
1752
2225
|
}
|
|
1753
2226
|
function systemctlUser(args) {
|
|
1754
|
-
const result =
|
|
2227
|
+
const result = spawnSync5("systemctl", ["--user", ...args], { encoding: "utf8" });
|
|
1755
2228
|
return {
|
|
1756
2229
|
ok: result.status === 0,
|
|
1757
2230
|
stdout: result.stdout.trim(),
|
|
@@ -1759,8 +2232,8 @@ function systemctlUser(args) {
|
|
|
1759
2232
|
};
|
|
1760
2233
|
}
|
|
1761
2234
|
function templateScript(ctx, name) {
|
|
1762
|
-
const source =
|
|
1763
|
-
return
|
|
2235
|
+
const source = join10(ctx.pjanglerRoot, ".mise", "scripts", name);
|
|
2236
|
+
return existsSync8(source) ? readText(source) : void 0;
|
|
1764
2237
|
}
|
|
1765
2238
|
function templateVersioningScript(ctx) {
|
|
1766
2239
|
return templateScript(ctx, "versioning.sh");
|
|
@@ -1770,14 +2243,14 @@ function templateLinkAgentfilesScript(ctx) {
|
|
|
1770
2243
|
}
|
|
1771
2244
|
function renderGeneratedProjectMiseToml(ctx, template) {
|
|
1772
2245
|
const project = readProjectJson(ctx);
|
|
1773
|
-
const projectName = String(project?.project_name ??
|
|
2246
|
+
const projectName = String(project?.project_name ?? basename3(ctx.repoRoot) ?? "project");
|
|
1774
2247
|
return template.replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
|
|
1775
2248
|
}
|
|
1776
2249
|
function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
1777
|
-
const targetPath =
|
|
1778
|
-
if (
|
|
1779
|
-
const sourcePath =
|
|
1780
|
-
if (!
|
|
2250
|
+
const targetPath = join10(ctx.repoRoot, "mise.toml");
|
|
2251
|
+
if (existsSync8(targetPath)) return false;
|
|
2252
|
+
const sourcePath = join10(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
|
|
2253
|
+
if (!existsSync8(sourcePath)) return false;
|
|
1781
2254
|
changedFiles.push(targetPath);
|
|
1782
2255
|
if (!ctx.dryRun) {
|
|
1783
2256
|
writeText(targetPath, renderGeneratedProjectMiseToml(ctx, readText(sourcePath)));
|
|
@@ -1785,8 +2258,8 @@ function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
|
1785
2258
|
return true;
|
|
1786
2259
|
}
|
|
1787
2260
|
function templateVersionFilesConf(ctx, repoRoot) {
|
|
1788
|
-
const packageJson =
|
|
1789
|
-
return
|
|
2261
|
+
const packageJson = join10(repoRoot, "package.json");
|
|
2262
|
+
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";
|
|
1790
2263
|
}
|
|
1791
2264
|
function replaceOrAppendManagedBlock(text3, startMarker, block, beforePattern) {
|
|
1792
2265
|
if (startMarker.test(text3)) {
|
|
@@ -1810,7 +2283,7 @@ var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/herm
|
|
|
1810
2283
|
function requiredMisePathEntries(ctx) {
|
|
1811
2284
|
const required = [...BASE_MISE_PATH_ENTRIES];
|
|
1812
2285
|
for (const candidate of CONDITIONAL_HERMES_PATHS) {
|
|
1813
|
-
if (
|
|
2286
|
+
if (existsSync8(join10(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
|
|
1814
2287
|
}
|
|
1815
2288
|
return required;
|
|
1816
2289
|
}
|
|
@@ -1959,12 +2432,12 @@ function upsertLinkAgentfilesBlock(text3, ctx) {
|
|
|
1959
2432
|
return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
|
|
1960
2433
|
}
|
|
1961
2434
|
function readProjectJson(ctx) {
|
|
1962
|
-
return tryParseJson(safeReadText(
|
|
2435
|
+
return tryParseJson(safeReadText(join10(ctx.repoRoot, ".project.json")));
|
|
1963
2436
|
}
|
|
1964
2437
|
function canonicalProjectJson(ctx) {
|
|
1965
2438
|
const roles = discoverRoles(ctx.repoRoot);
|
|
1966
2439
|
const existing = readProjectJson(ctx) ?? {};
|
|
1967
|
-
const slug = String(existing.project_slug ?? slugifyRepoName(
|
|
2440
|
+
const slug = String(existing.project_slug ?? slugifyRepoName(dirname6(ctx.repoRoot) === ctx.repoRoot ? ctx.repoRoot.split("/").pop() ?? "project" : ctx.repoRoot.split("/").pop() ?? "project"));
|
|
1968
2441
|
const firstRole = roles[0];
|
|
1969
2442
|
const ticketProvider = {
|
|
1970
2443
|
type: String((existing.ticket_provider?.type ?? firstRole?.ticketProviderName ?? "plane") || "plane"),
|
|
@@ -2003,12 +2476,12 @@ function canonicalProjectJson(ctx) {
|
|
|
2003
2476
|
};
|
|
2004
2477
|
}
|
|
2005
2478
|
function projectJsonFinding(ctx) {
|
|
2006
|
-
const projectPath =
|
|
2007
|
-
const planeJsonPath =
|
|
2479
|
+
const projectPath = join10(ctx.repoRoot, ".project.json");
|
|
2480
|
+
const planeJsonPath = join10(ctx.repoRoot, ".plane.json");
|
|
2008
2481
|
const details = [];
|
|
2009
2482
|
const data = readProjectJson(ctx);
|
|
2010
2483
|
const roles = discoverRoles(ctx.repoRoot);
|
|
2011
|
-
if (!
|
|
2484
|
+
if (!existsSync8(projectPath)) {
|
|
2012
2485
|
return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json missing", details: [], fixable: true };
|
|
2013
2486
|
}
|
|
2014
2487
|
if (!data) {
|
|
@@ -2034,7 +2507,7 @@ function projectJsonFinding(ctx) {
|
|
|
2034
2507
|
for (const key of ["type", "workspace", "identifier", "board_id", "board_url", "state"]) {
|
|
2035
2508
|
if (!(key in ticketProvider)) details.push(`ticket_provider.${key} missing`);
|
|
2036
2509
|
}
|
|
2037
|
-
if (
|
|
2510
|
+
if (existsSync8(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
|
|
2038
2511
|
return {
|
|
2039
2512
|
id: "sot.project-json",
|
|
2040
2513
|
title: "Canonical .project.json",
|
|
@@ -2115,17 +2588,17 @@ exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" HERMES_OAUTH
|
|
|
2115
2588
|
`.replace(/\u0010/g, "$");
|
|
2116
2589
|
}
|
|
2117
2590
|
function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
|
|
2118
|
-
if (!
|
|
2119
|
-
|
|
2591
|
+
if (!existsSync8(sourceDir)) return;
|
|
2592
|
+
mkdirSync6(targetDir, { recursive: true });
|
|
2120
2593
|
for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
|
|
2121
|
-
const sourcePath =
|
|
2594
|
+
const sourcePath = join10(sourceDir, entry.name);
|
|
2122
2595
|
if (skip?.(sourcePath)) continue;
|
|
2123
|
-
const targetPath =
|
|
2596
|
+
const targetPath = join10(targetDir, entry.name);
|
|
2124
2597
|
if (entry.isDirectory()) {
|
|
2125
2598
|
copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
|
|
2126
2599
|
continue;
|
|
2127
2600
|
}
|
|
2128
|
-
if (
|
|
2601
|
+
if (existsSync8(targetPath)) continue;
|
|
2129
2602
|
changedFiles.push(targetPath);
|
|
2130
2603
|
if (!dryRun) {
|
|
2131
2604
|
ensureParent(targetPath);
|
|
@@ -2134,7 +2607,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
|
|
|
2134
2607
|
}
|
|
2135
2608
|
}
|
|
2136
2609
|
function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
|
|
2137
|
-
const gitmodulesPath =
|
|
2610
|
+
const gitmodulesPath = join10(repoRoot, ".gitmodules");
|
|
2138
2611
|
const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
|
|
2139
2612
|
const owner = role.runtimeOwner || "delorenj";
|
|
2140
2613
|
const block = `[submodule "agents/hermes/${role.role}/runtime"]
|
|
@@ -2156,7 +2629,7 @@ function upsertRegistryEntry(role, homeDir, changedFiles, dryRun) {
|
|
|
2156
2629
|
repo: ${role.repo}
|
|
2157
2630
|
role: ${role.role}
|
|
2158
2631
|
display_name: ${JSON.stringify(role.displayName || role.agentId)}
|
|
2159
|
-
project_path: ${ctxEscape(role.roleDir ?
|
|
2632
|
+
project_path: ${ctxEscape(role.roleDir ? dirname6(dirname6(dirname6(role.roleDir))) : "")}
|
|
2160
2633
|
role_dir: ${ctxEscape(role.roleDir)}
|
|
2161
2634
|
profile_name: ${role.profileName || role.agentId}
|
|
2162
2635
|
telegram:
|
|
@@ -2234,14 +2707,14 @@ var RULES = [
|
|
|
2234
2707
|
id: "mise.config-root",
|
|
2235
2708
|
title: "mise config_root + AGENTS link hooks",
|
|
2236
2709
|
audit: (ctx) => {
|
|
2237
|
-
const misePath =
|
|
2238
|
-
if (!
|
|
2710
|
+
const misePath = join10(ctx.repoRoot, "mise.toml");
|
|
2711
|
+
if (!existsSync8(misePath)) {
|
|
2239
2712
|
return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
|
|
2240
2713
|
}
|
|
2241
2714
|
const text3 = readText(misePath);
|
|
2242
2715
|
const details = [];
|
|
2243
|
-
const linkAgentfilesPath =
|
|
2244
|
-
if (!
|
|
2716
|
+
const linkAgentfilesPath = join10(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
2717
|
+
if (!existsSync8(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
|
|
2245
2718
|
const pathValues = [...(text3.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
2246
2719
|
const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
|
|
2247
2720
|
if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
|
|
@@ -2259,10 +2732,10 @@ var RULES = [
|
|
|
2259
2732
|
};
|
|
2260
2733
|
},
|
|
2261
2734
|
migrate: (ctx, finding) => {
|
|
2262
|
-
const path =
|
|
2735
|
+
const path = join10(ctx.repoRoot, "mise.toml");
|
|
2263
2736
|
const changedFiles = [];
|
|
2264
2737
|
const details = [];
|
|
2265
|
-
if (!
|
|
2738
|
+
if (!existsSync8(path)) {
|
|
2266
2739
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
2267
2740
|
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: [] };
|
|
2268
2741
|
}
|
|
@@ -2278,7 +2751,7 @@ var RULES = [
|
|
|
2278
2751
|
if (!ctx.dryRun) writeText(path, next);
|
|
2279
2752
|
text3 = next;
|
|
2280
2753
|
}
|
|
2281
|
-
const linkAgentfilesPath =
|
|
2754
|
+
const linkAgentfilesPath = join10(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
2282
2755
|
const expectedScript = templateLinkAgentfilesScript(ctx);
|
|
2283
2756
|
if (expectedScript === void 0) {
|
|
2284
2757
|
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: [] };
|
|
@@ -2305,13 +2778,13 @@ var RULES = [
|
|
|
2305
2778
|
title: "managed mise versioning block",
|
|
2306
2779
|
audit: (ctx) => {
|
|
2307
2780
|
const details = [];
|
|
2308
|
-
const misePath =
|
|
2309
|
-
const versioningPath =
|
|
2310
|
-
const manifestPath =
|
|
2781
|
+
const misePath = join10(ctx.repoRoot, "mise.toml");
|
|
2782
|
+
const versioningPath = join10(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
2783
|
+
const manifestPath = join10(ctx.repoRoot, ".mise", "version-files.conf");
|
|
2311
2784
|
const text3 = safeReadText(misePath);
|
|
2312
2785
|
if (!text3?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
|
|
2313
|
-
if (!
|
|
2314
|
-
if (!
|
|
2786
|
+
if (!existsSync8(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
|
|
2787
|
+
if (!existsSync8(manifestPath)) details.push(".mise/version-files.conf missing");
|
|
2315
2788
|
return {
|
|
2316
2789
|
id: "mise.versioning",
|
|
2317
2790
|
title: "managed mise versioning block",
|
|
@@ -2324,8 +2797,8 @@ var RULES = [
|
|
|
2324
2797
|
migrate: (ctx, finding) => {
|
|
2325
2798
|
const changedFiles = [];
|
|
2326
2799
|
const details = [];
|
|
2327
|
-
const misePath =
|
|
2328
|
-
if (!
|
|
2800
|
+
const misePath = join10(ctx.repoRoot, "mise.toml");
|
|
2801
|
+
if (!existsSync8(misePath)) {
|
|
2329
2802
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
2330
2803
|
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: [] };
|
|
2331
2804
|
}
|
|
@@ -2340,7 +2813,7 @@ var RULES = [
|
|
|
2340
2813
|
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
2341
2814
|
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
2342
2815
|
}
|
|
2343
|
-
const versioningPath =
|
|
2816
|
+
const versioningPath = join10(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
2344
2817
|
const expectedScript = templateVersioningScript(ctx);
|
|
2345
2818
|
if (expectedScript === void 0) {
|
|
2346
2819
|
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: [] };
|
|
@@ -2352,7 +2825,7 @@ var RULES = [
|
|
|
2352
2825
|
chmodSync2(versioningPath, 493);
|
|
2353
2826
|
}
|
|
2354
2827
|
}
|
|
2355
|
-
const manifestPath =
|
|
2828
|
+
const manifestPath = join10(ctx.repoRoot, ".mise", "version-files.conf");
|
|
2356
2829
|
const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
|
|
2357
2830
|
if (safeReadText(manifestPath) !== expectedManifest) {
|
|
2358
2831
|
changedFiles.push(manifestPath);
|
|
@@ -2372,9 +2845,9 @@ var RULES = [
|
|
|
2372
2845
|
id: "sot.agent-symlinks",
|
|
2373
2846
|
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
2374
2847
|
audit: (ctx) => {
|
|
2375
|
-
const agentsPath =
|
|
2376
|
-
if (!
|
|
2377
|
-
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) =>
|
|
2848
|
+
const agentsPath = join10(ctx.repoRoot, "AGENTS.md");
|
|
2849
|
+
if (!existsSync8(agentsPath)) {
|
|
2850
|
+
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync8(join10(ctx.repoRoot, file)));
|
|
2378
2851
|
if (fallbackSources.length === 0) {
|
|
2379
2852
|
return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
|
|
2380
2853
|
}
|
|
@@ -2389,7 +2862,7 @@ var RULES = [
|
|
|
2389
2862
|
}
|
|
2390
2863
|
const details = [];
|
|
2391
2864
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2392
|
-
const full =
|
|
2865
|
+
const full = join10(ctx.repoRoot, file);
|
|
2393
2866
|
const target = readSymlinkTarget(full);
|
|
2394
2867
|
if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
|
|
2395
2868
|
}
|
|
@@ -2413,7 +2886,7 @@ var RULES = [
|
|
|
2413
2886
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
|
|
2414
2887
|
}
|
|
2415
2888
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2416
|
-
const full =
|
|
2889
|
+
const full = join10(ctx.repoRoot, file);
|
|
2417
2890
|
const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
|
|
2418
2891
|
if (result.blocked) blockedDetails.push(result.blocked);
|
|
2419
2892
|
if (result.changed) changedFiles.push(full);
|
|
@@ -2435,7 +2908,7 @@ var RULES = [
|
|
|
2435
2908
|
migrate: (ctx, finding) => {
|
|
2436
2909
|
const changedFiles = [];
|
|
2437
2910
|
const details = [];
|
|
2438
|
-
const path =
|
|
2911
|
+
const path = join10(ctx.repoRoot, ".project.json");
|
|
2439
2912
|
const existing = readProjectJson(ctx) ?? {};
|
|
2440
2913
|
const canonical = canonicalProjectJson(ctx);
|
|
2441
2914
|
const merged = { ...existing, ...canonical };
|
|
@@ -2445,14 +2918,14 @@ var RULES = [
|
|
|
2445
2918
|
changedFiles.push(path);
|
|
2446
2919
|
if (!ctx.dryRun) writeText(path, expected);
|
|
2447
2920
|
}
|
|
2448
|
-
const planeJson =
|
|
2449
|
-
if (
|
|
2921
|
+
const planeJson = join10(ctx.repoRoot, ".plane.json");
|
|
2922
|
+
if (existsSync8(planeJson)) {
|
|
2450
2923
|
const backup = `${planeJson}.migrated-backup`;
|
|
2451
|
-
if (
|
|
2924
|
+
if (existsSync8(backup)) {
|
|
2452
2925
|
details.push(`cannot back up .plane.json because ${relative(ctx.repoRoot, backup)} already exists`);
|
|
2453
2926
|
} else {
|
|
2454
2927
|
changedFiles.push(backup);
|
|
2455
|
-
if (!ctx.dryRun)
|
|
2928
|
+
if (!ctx.dryRun) renameSync2(planeJson, backup);
|
|
2456
2929
|
}
|
|
2457
2930
|
}
|
|
2458
2931
|
return {
|
|
@@ -2470,8 +2943,8 @@ var RULES = [
|
|
|
2470
2943
|
title: ".env.op + gitignore secrets contract",
|
|
2471
2944
|
audit: (ctx) => {
|
|
2472
2945
|
const details = [];
|
|
2473
|
-
const envOp = safeReadText(
|
|
2474
|
-
const gitignore = safeReadText(
|
|
2946
|
+
const envOp = safeReadText(join10(ctx.repoRoot, ".env.op"));
|
|
2947
|
+
const gitignore = safeReadText(join10(ctx.repoRoot, ".gitignore"));
|
|
2475
2948
|
if (!envOp) {
|
|
2476
2949
|
details.push(".env.op missing");
|
|
2477
2950
|
} else {
|
|
@@ -2497,12 +2970,12 @@ var RULES = [
|
|
|
2497
2970
|
migrate: (ctx, finding) => {
|
|
2498
2971
|
const changedFiles = [];
|
|
2499
2972
|
const details = [];
|
|
2500
|
-
const envOpPath =
|
|
2501
|
-
if (!
|
|
2973
|
+
const envOpPath = join10(ctx.repoRoot, ".env.op");
|
|
2974
|
+
if (!existsSync8(envOpPath)) {
|
|
2502
2975
|
changedFiles.push(envOpPath);
|
|
2503
|
-
if (!ctx.dryRun) writeText(envOpPath, readText(
|
|
2976
|
+
if (!ctx.dryRun) writeText(envOpPath, readText(join10(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
|
|
2504
2977
|
}
|
|
2505
|
-
const gitignorePath =
|
|
2978
|
+
const gitignorePath = join10(ctx.repoRoot, ".gitignore");
|
|
2506
2979
|
const gitignore = safeReadText(gitignorePath) ?? "";
|
|
2507
2980
|
const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
|
|
2508
2981
|
# NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
|
|
@@ -2529,7 +3002,7 @@ var RULES = [
|
|
|
2529
3002
|
title: ".copier-answers.yml provenance + drift report",
|
|
2530
3003
|
audit: (ctx) => {
|
|
2531
3004
|
const details = [];
|
|
2532
|
-
const path =
|
|
3005
|
+
const path = join10(ctx.repoRoot, ".copier-answers.yml");
|
|
2533
3006
|
const text3 = safeReadText(path);
|
|
2534
3007
|
const project = readProjectJson(ctx);
|
|
2535
3008
|
if (!text3) {
|
|
@@ -2560,12 +3033,12 @@ var RULES = [
|
|
|
2560
3033
|
const changedFiles = [];
|
|
2561
3034
|
const project = canonicalProjectJson(ctx);
|
|
2562
3035
|
const text3 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
|
|
2563
|
-
_src_path: ${
|
|
3036
|
+
_src_path: ${join10(ctx.pjanglerRoot, "templates", "commonproject")}
|
|
2564
3037
|
project_description: ${String(project.project_description)}
|
|
2565
3038
|
project_name: ${String(project.project_name)}
|
|
2566
3039
|
ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
2567
3040
|
`;
|
|
2568
|
-
const path =
|
|
3041
|
+
const path = join10(ctx.repoRoot, ".copier-answers.yml");
|
|
2569
3042
|
if (safeReadText(path) !== text3) {
|
|
2570
3043
|
changedFiles.push(path);
|
|
2571
3044
|
if (!ctx.dryRun) writeText(path, text3);
|
|
@@ -2584,15 +3057,15 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2584
3057
|
id: "bmad.scaffold",
|
|
2585
3058
|
title: "BMAD modules/docs scaffold",
|
|
2586
3059
|
audit: (ctx) => {
|
|
2587
|
-
const sourceRoot =
|
|
2588
|
-
const targetRoot =
|
|
3060
|
+
const sourceRoot = join10(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
|
|
3061
|
+
const targetRoot = join10(ctx.repoRoot, "_bmad");
|
|
2589
3062
|
const sentinels = [
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
3063
|
+
join10("core", "config.yaml"),
|
|
3064
|
+
join10("custom", "config.yaml"),
|
|
3065
|
+
join10("custom", "workflows", "ticket-lifecycle", "workflow.yaml"),
|
|
3066
|
+
join10("bmm", "workflows", "workflow-status", "workflow.yaml")
|
|
2594
3067
|
];
|
|
2595
|
-
const missing = sentinels.filter((file) =>
|
|
3068
|
+
const missing = sentinels.filter((file) => existsSync8(join10(sourceRoot, file)) && !existsSync8(join10(targetRoot, file)));
|
|
2596
3069
|
return {
|
|
2597
3070
|
id: "bmad.scaffold",
|
|
2598
3071
|
title: "BMAD modules/docs scaffold",
|
|
@@ -2604,7 +3077,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2604
3077
|
},
|
|
2605
3078
|
migrate: (ctx, finding) => {
|
|
2606
3079
|
const changedFiles = [];
|
|
2607
|
-
copyMissingRecursive(
|
|
3080
|
+
copyMissingRecursive(join10(ctx.pjanglerRoot, "templates", "commonproject", "_bmad"), join10(ctx.repoRoot, "_bmad"), changedFiles, ctx.dryRun);
|
|
2608
3081
|
return {
|
|
2609
3082
|
id: finding.id,
|
|
2610
3083
|
title: finding.title,
|
|
@@ -2626,11 +3099,11 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2626
3099
|
}
|
|
2627
3100
|
const details = [];
|
|
2628
3101
|
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"]) {
|
|
2629
|
-
if (!
|
|
3102
|
+
if (!existsSync8(join10(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join10(role.roleDir, rel))}`);
|
|
2630
3103
|
}
|
|
2631
|
-
const gitmodules = safeReadText(
|
|
3104
|
+
const gitmodules = safeReadText(join10(ctx.repoRoot, ".gitmodules")) ?? "";
|
|
2632
3105
|
if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
|
|
2633
|
-
if (!profileMetaInheritsDefault(
|
|
3106
|
+
if (!profileMetaInheritsDefault(join10(role.roleDir, "runtime", "profile.yaml"))) {
|
|
2634
3107
|
details.push("runtime/profile.yaml missing inherited default config metadata");
|
|
2635
3108
|
}
|
|
2636
3109
|
const registry = safeReadText(registryPath(ctx.homeDir));
|
|
@@ -2651,21 +3124,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2651
3124
|
if (!role) {
|
|
2652
3125
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
|
|
2653
3126
|
}
|
|
2654
|
-
const templateRoleDir =
|
|
2655
|
-
writeIfDifferent(
|
|
2656
|
-
writeIfDifferent(
|
|
2657
|
-
writeIfDifferent(
|
|
2658
|
-
copyMissingRecursive(
|
|
2659
|
-
copyMissingRecursive(
|
|
2660
|
-
copyMissingRecursive(
|
|
2661
|
-
const promptSource =
|
|
2662
|
-
const promptTarget =
|
|
2663
|
-
if (
|
|
3127
|
+
const templateRoleDir = join10(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
|
|
3128
|
+
writeIfDifferent(join10(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
|
|
3129
|
+
writeIfDifferent(join10(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
|
|
3130
|
+
writeIfDifferent(join10(role.roleDir, ".gitignore"), readText(join10(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
|
|
3131
|
+
copyMissingRecursive(join10(templateRoleDir, ".runtime-scaffold"), join10(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
|
|
3132
|
+
copyMissingRecursive(join10(templateRoleDir, ".runtime-scaffold"), join10(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
|
|
3133
|
+
copyMissingRecursive(join10(templateRoleDir, ".scripts"), join10(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
|
|
3134
|
+
const promptSource = join10(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
|
|
3135
|
+
const promptTarget = join10(role.roleDir, ".scripts", "sentinel.prompt.md");
|
|
3136
|
+
if (existsSync8(promptSource) && !existsSync8(promptTarget)) {
|
|
2664
3137
|
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);
|
|
2665
3138
|
writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
|
|
2666
3139
|
}
|
|
2667
3140
|
upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
|
|
2668
|
-
const profileMetaUpdated = upsertInheritedProfileMeta(
|
|
3141
|
+
const profileMetaUpdated = upsertInheritedProfileMeta(join10(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
|
|
2669
3142
|
if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
|
|
2670
3143
|
const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
|
|
2671
3144
|
if (registryUpdated) details.push(`updated ${registryUpdated}`);
|
|
@@ -2719,9 +3192,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2719
3192
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
|
|
2720
3193
|
}
|
|
2721
3194
|
for (const role of roles) {
|
|
2722
|
-
const sysDir =
|
|
3195
|
+
const sysDir = join10(ctx.homeDir, ".config", "systemd", "user");
|
|
2723
3196
|
const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`];
|
|
2724
|
-
const allUnitsPresent = units.every((unit) =>
|
|
3197
|
+
const allUnitsPresent = units.every((unit) => existsSync8(join10(sysDir, unit)));
|
|
2725
3198
|
if (allUnitsPresent) {
|
|
2726
3199
|
if (ctx.dryRun) {
|
|
2727
3200
|
details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
|
|
@@ -2733,561 +3206,143 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2733
3206
|
}
|
|
2734
3207
|
continue;
|
|
2735
3208
|
}
|
|
2736
|
-
for (const script of [
|
|
2737
|
-
if (!script || !
|
|
3209
|
+
for (const script of [join10(role.roleDir, ".scripts", "70-systemd.sh")]) {
|
|
3210
|
+
if (!script || !existsSync8(script)) continue;
|
|
2738
3211
|
if (ctx.dryRun) {
|
|
2739
|
-
details.push(`would run: bash ${script}`);
|
|
2740
|
-
} else {
|
|
2741
|
-
const result =
|
|
2742
|
-
if (result.status !== 0) details.push(`script failed: ${script}: ${result.stderr.trim() || result.stdout.trim()}`);
|
|
2743
|
-
}
|
|
2744
|
-
}
|
|
2745
|
-
}
|
|
2746
|
-
return {
|
|
2747
|
-
id: finding.id,
|
|
2748
|
-
title: finding.title,
|
|
2749
|
-
status: details.some((detail) => detail.includes("failed:")) ? "blocked" : details.length ? ctx.dryRun ? "skipped" : "applied" : "noop",
|
|
2750
|
-
summary: details.length ? ctx.dryRun ? "Planned systemd remediation commands" : "Attempted systemd remediation" : "No changes required",
|
|
2751
|
-
changedFiles,
|
|
2752
|
-
details
|
|
2753
|
-
};
|
|
2754
|
-
}
|
|
2755
|
-
}
|
|
2756
|
-
];
|
|
2757
|
-
function writeIfDifferent(path, content, dryRun, changedFiles, mode) {
|
|
2758
|
-
const normalized = content.endsWith("\n") ? content : `${content}
|
|
2759
|
-
`;
|
|
2760
|
-
if (safeReadText(path) === normalized) return;
|
|
2761
|
-
changedFiles.push(path);
|
|
2762
|
-
if (!dryRun) {
|
|
2763
|
-
writeText(path, normalized);
|
|
2764
|
-
if (mode) chmodSync2(path, mode);
|
|
2765
|
-
}
|
|
2766
|
-
}
|
|
2767
|
-
function getParityRuleIds() {
|
|
2768
|
-
return RULES.map((rule) => rule.id);
|
|
2769
|
-
}
|
|
2770
|
-
function runAudit(repoArg) {
|
|
2771
|
-
const pjanglerRoot = resolvePjanglerRoot();
|
|
2772
|
-
const ctx = {
|
|
2773
|
-
repoRoot: resolve(repoArg ?? process.cwd()),
|
|
2774
|
-
dryRun: true,
|
|
2775
|
-
pjanglerRoot,
|
|
2776
|
-
homeDir: homedir4()
|
|
2777
|
-
};
|
|
2778
|
-
const rules = RULES.map((rule) => rule.audit(ctx));
|
|
2779
|
-
return {
|
|
2780
|
-
repo: ctx.repoRoot,
|
|
2781
|
-
ok: rules.every((rule) => rule.status === "pass" || rule.status === "skip"),
|
|
2782
|
-
auditedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2783
|
-
rules
|
|
2784
|
-
};
|
|
2785
|
-
}
|
|
2786
|
-
function runMigrationForRules(ruleIds, repoArg, dryRun) {
|
|
2787
|
-
const pjanglerRoot = resolvePjanglerRoot();
|
|
2788
|
-
const ctx = {
|
|
2789
|
-
repoRoot: resolve(repoArg ?? process.cwd()),
|
|
2790
|
-
dryRun,
|
|
2791
|
-
pjanglerRoot,
|
|
2792
|
-
homeDir: homedir4()
|
|
2793
|
-
};
|
|
2794
|
-
const selected = RULES.filter((rule) => ruleIds.includes(rule.id));
|
|
2795
|
-
if (!selected.length) {
|
|
2796
|
-
throw new Error(`Unknown parity rules: ${ruleIds.join(", ")}`);
|
|
2797
|
-
}
|
|
2798
|
-
const results = selected.map((rule) => {
|
|
2799
|
-
try {
|
|
2800
|
-
return rule.migrate(ctx, rule.audit(ctx));
|
|
2801
|
-
} catch (err) {
|
|
2802
|
-
return {
|
|
2803
|
-
id: rule.id,
|
|
2804
|
-
title: rule.title,
|
|
2805
|
-
status: "blocked",
|
|
2806
|
-
summary: `migrate threw: ${err instanceof Error ? err.message : String(err)}`,
|
|
2807
|
-
changedFiles: [],
|
|
2808
|
-
details: []
|
|
2809
|
-
};
|
|
2810
|
-
}
|
|
2811
|
-
});
|
|
2812
|
-
const changedFiles = Array.from(new Set(results.flatMap((result) => result.changedFiles))).sort();
|
|
2813
|
-
return {
|
|
2814
|
-
repo: ctx.repoRoot,
|
|
2815
|
-
dryRun,
|
|
2816
|
-
ok: results.every((result) => result.status !== "blocked"),
|
|
2817
|
-
selectedRules: selected.map((rule) => rule.id),
|
|
2818
|
-
results,
|
|
2819
|
-
changedFiles
|
|
2820
|
-
};
|
|
2821
|
-
}
|
|
2822
|
-
function runMigration(selector, repoArg, dryRun, all) {
|
|
2823
|
-
const ruleIds = all ? RULES.map((rule) => rule.id) : selector ? [selector] : [];
|
|
2824
|
-
return runMigrationForRules(ruleIds, repoArg, dryRun);
|
|
2825
|
-
}
|
|
2826
|
-
function prettyTimestamp(iso) {
|
|
2827
|
-
const match = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})/.exec(iso);
|
|
2828
|
-
return match ? `${match[1]} ${match[2]} UTC` : iso;
|
|
2829
|
-
}
|
|
2830
|
-
function formatAuditReport(report) {
|
|
2831
|
-
const counts = {};
|
|
2832
|
-
for (const rule of report.rules) counts[rule.status] = (counts[rule.status] ?? 0) + 1;
|
|
2833
|
-
const idWidth = report.rules.reduce((width, rule) => Math.max(width, rule.id.length), 0);
|
|
2834
|
-
const tally = [];
|
|
2835
|
-
if (counts.pass) tally.push(green(`${counts.pass} passed`));
|
|
2836
|
-
if (counts.fail) tally.push(red(`${counts.fail} failed`));
|
|
2837
|
-
if (counts.warn) tally.push(yellow(`${counts.warn} warning${counts.warn === 1 ? "" : "s"}`));
|
|
2838
|
-
if (counts.skip) tally.push(gray(`${counts.skip} skipped`));
|
|
2839
|
-
const overall = report.ok ? `${green(glyph.pass)} ${bold("Parity audit passed")}` : `${red(glyph.fail)} ${bold("Parity audit failed")}`;
|
|
2840
|
-
const lines = [""];
|
|
2841
|
-
lines.push(` ${overall}${tally.length ? ` ${dim(glyph.dot)} ${joinDot(tally)}` : ""}`);
|
|
2842
|
-
lines.push(` ${dim(report.repo)} ${dim(glyph.dot)} ${dim(prettyTimestamp(report.auditedAt))}`);
|
|
2843
|
-
lines.push("");
|
|
2844
|
-
for (const rule of report.rules) {
|
|
2845
|
-
const style = statusStyle(rule.status);
|
|
2846
|
-
lines.push(` ${style.color(style.glyph)} ${style.color(rule.id.padEnd(idWidth))} ${rule.summary}`);
|
|
2847
|
-
for (const detail of rule.details) lines.push(` ${dim(glyph.arrow)} ${dim(detail)}`);
|
|
2848
|
-
}
|
|
2849
|
-
lines.push("");
|
|
2850
|
-
return lines.join("\n");
|
|
2851
|
-
}
|
|
2852
|
-
function formatMigrationReport(report) {
|
|
2853
|
-
const idWidth = report.results.reduce((width, result) => Math.max(width, result.id.length), 0);
|
|
2854
|
-
const overall = report.ok ? `${green(glyph.pass)} ${bold(report.dryRun ? "Migration preview complete" : "Migration complete")}` : `${red(glyph.fail)} ${bold("Migration finished with blockers")}`;
|
|
2855
|
-
const lines = [""];
|
|
2856
|
-
lines.push(` ${overall}${report.dryRun ? ` ${dim(glyph.dot)} ${yellow("dry run")}` : ""}`);
|
|
2857
|
-
lines.push(` ${dim(report.repo)}`);
|
|
2858
|
-
if (report.selectedRules.length) lines.push(` ${dim(`rules: ${report.selectedRules.join(", ")}`)}`);
|
|
2859
|
-
lines.push("");
|
|
2860
|
-
for (const result of report.results) {
|
|
2861
|
-
const style = statusStyle(result.status);
|
|
2862
|
-
lines.push(` ${style.color(style.glyph)} ${style.color(result.id.padEnd(idWidth))} ${result.summary} ${dim(`[${style.label}]`)}`);
|
|
2863
|
-
for (const detail of result.details) lines.push(` ${dim(glyph.arrow)} ${dim(detail)}`);
|
|
2864
|
-
for (const file of result.changedFiles) lines.push(` ${green(glyph.add)} ${file}`);
|
|
2865
|
-
}
|
|
2866
|
-
if (report.changedFiles.length) {
|
|
2867
|
-
lines.push("");
|
|
2868
|
-
lines.push(` ${bold(`Changed files (${report.changedFiles.length})`)}`);
|
|
2869
|
-
for (const file of report.changedFiles) lines.push(` ${green(glyph.add)} ${file}`);
|
|
2870
|
-
}
|
|
2871
|
-
lines.push("");
|
|
2872
|
-
return lines.join("\n");
|
|
2873
|
-
}
|
|
2874
|
-
|
|
2875
|
-
// src/project/index.ts
|
|
2876
|
-
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
2877
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync4, renameSync as renameSync2, statSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2878
|
-
import { homedir as homedir5 } from "node:os";
|
|
2879
|
-
import { basename as basename3, dirname as dirname6, join as join10, resolve as resolve2 } from "node:path";
|
|
2880
|
-
import YAML from "yaml";
|
|
2881
|
-
var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
|
|
2882
|
-
var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
|
|
2883
|
-
var KNOWN_SKILL_ROOTS = [
|
|
2884
|
-
"/home/delorenj/code/skillex/all-skills",
|
|
2885
|
-
"/home/delorenj/code/CoachingAgentFramework/.agents/skills",
|
|
2886
|
-
"/home/delorenj/code/pjangler/.agents/skills",
|
|
2887
|
-
join10(homedir5(), ".codex", "skills")
|
|
2888
|
-
];
|
|
2889
|
-
function projectRegistryPath(env2 = process.env) {
|
|
2890
|
-
return expandHome(env2[PROJECT_REGISTRY_ENV] || join10(homedir5(), ".config", "pjangler", "projects.yaml"));
|
|
2891
|
-
}
|
|
2892
|
-
function emptyProjectRegistry() {
|
|
2893
|
-
return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
|
|
2894
|
-
}
|
|
2895
|
-
function loadProjectRegistry(path = projectRegistryPath()) {
|
|
2896
|
-
if (!existsSync8(path)) return emptyProjectRegistry();
|
|
2897
|
-
const raw = YAML.parse(readFileSync4(path, "utf8"));
|
|
2898
|
-
if (raw == null) return emptyProjectRegistry();
|
|
2899
|
-
if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
|
|
2900
|
-
const registry = raw;
|
|
2901
|
-
const normalized = {
|
|
2902
|
-
schema_version: Number(registry.schema_version ?? PROJECT_REGISTRY_SCHEMA_VERSION),
|
|
2903
|
-
projects: isRecord(registry.projects) ? registry.projects : {}
|
|
2904
|
-
};
|
|
2905
|
-
validateProjectRegistry(normalized);
|
|
2906
|
-
return normalized;
|
|
2907
|
-
}
|
|
2908
|
-
function saveProjectRegistry(registry, path = projectRegistryPath()) {
|
|
2909
|
-
validateProjectRegistry(registry);
|
|
2910
|
-
mkdirSync6(dirname6(path), { recursive: true });
|
|
2911
|
-
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
2912
|
-
writeFileSync5(temp, YAML.stringify(registry, { lineWidth: 0 }), "utf8");
|
|
2913
|
-
renameSync2(temp, path);
|
|
2914
|
-
}
|
|
2915
|
-
function validateProjectRegistry(registry) {
|
|
2916
|
-
if (registry.schema_version !== PROJECT_REGISTRY_SCHEMA_VERSION) {
|
|
2917
|
-
throw new Error(`Unsupported project registry schema_version: ${registry.schema_version}`);
|
|
2918
|
-
}
|
|
2919
|
-
if (!isRecord(registry.projects)) throw new Error("Project registry projects must be a mapping");
|
|
2920
|
-
const slugs = /* @__PURE__ */ new Set();
|
|
2921
|
-
const repoPaths = /* @__PURE__ */ new Map();
|
|
2922
|
-
const identifiers = /* @__PURE__ */ new Map();
|
|
2923
|
-
for (const [slug, project] of Object.entries(registry.projects)) {
|
|
2924
|
-
validateProjectRecord(project, slug);
|
|
2925
|
-
if (slugs.has(project.slug)) throw new Error(`Duplicate project slug: ${project.slug}`);
|
|
2926
|
-
slugs.add(project.slug);
|
|
2927
|
-
const repoKey = resolve2(project.repo_path);
|
|
2928
|
-
const existingRepoSlug = repoPaths.get(repoKey);
|
|
2929
|
-
if (existingRepoSlug && existingRepoSlug !== slug) {
|
|
2930
|
-
throw new Error(`Duplicate project repo_path: ${project.repo_path} used by ${existingRepoSlug} and ${slug}`);
|
|
2931
|
-
}
|
|
2932
|
-
repoPaths.set(repoKey, slug);
|
|
2933
|
-
const identifier = project.ticket_provider.identifier?.toUpperCase();
|
|
2934
|
-
if (identifier) {
|
|
2935
|
-
const existingIdentifierSlug = identifiers.get(identifier);
|
|
2936
|
-
if (existingIdentifierSlug && existingIdentifierSlug !== slug) {
|
|
2937
|
-
throw new Error(`Duplicate project identifier: ${identifier} used by ${existingIdentifierSlug} and ${slug}`);
|
|
2938
|
-
}
|
|
2939
|
-
identifiers.set(identifier, slug);
|
|
2940
|
-
}
|
|
2941
|
-
}
|
|
2942
|
-
}
|
|
2943
|
-
function slugifyProjectName(value) {
|
|
2944
|
-
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
2945
|
-
}
|
|
2946
|
-
function deriveProjectIdentifier(value) {
|
|
2947
|
-
const compact = value.replace(/[^A-Za-z0-9]/g, "").toUpperCase();
|
|
2948
|
-
const identifier = compact.slice(0, 4) || "PROJ";
|
|
2949
|
-
return identifier.length >= 2 ? identifier : `${identifier}XX`.slice(0, 4);
|
|
2950
|
-
}
|
|
2951
|
-
function normalizeAgentRole(value) {
|
|
2952
|
-
return value?.trim() || "pm";
|
|
2953
|
-
}
|
|
2954
|
-
function jsonStable(value) {
|
|
2955
|
-
return JSON.stringify(value);
|
|
2956
|
-
}
|
|
2957
|
-
function projectRecordEquivalent(a, b) {
|
|
2958
|
-
if (!a) return false;
|
|
2959
|
-
const { created_at: _aCreated, updated_at: _aUpdated, ...aComparable } = a;
|
|
2960
|
-
const { created_at: _bCreated, updated_at: _bUpdated, ...bComparable } = b;
|
|
2961
|
-
return jsonStable(aComparable) === jsonStable(bComparable);
|
|
2962
|
-
}
|
|
2963
|
-
function defaultProjectTargetDir(name, cwd = process.cwd()) {
|
|
2964
|
-
const compactName = name.replace(/[^A-Za-z0-9._-]/g, "") || slugifyProjectName(name);
|
|
2965
|
-
return resolve2(dirname6(resolve2(cwd)), compactName);
|
|
2966
|
-
}
|
|
2967
|
-
function resolveSourceSkillPath(sourceSkill) {
|
|
2968
|
-
if (!sourceSkill) return void 0;
|
|
2969
|
-
const expanded = expandHome(sourceSkill);
|
|
2970
|
-
const direct = resolve2(expanded);
|
|
2971
|
-
if (existsSync8(direct)) return direct;
|
|
2972
|
-
const name = basename3(sourceSkill);
|
|
2973
|
-
for (const root of KNOWN_SKILL_ROOTS) {
|
|
2974
|
-
const candidate = join10(root, name);
|
|
2975
|
-
if (existsSync8(candidate)) return candidate;
|
|
2976
|
-
}
|
|
2977
|
-
const civilWarLetterifier = "/home/delorenj/code/skillex/all-skills/civilwar-letterifier";
|
|
2978
|
-
const hint = existsSync8(civilWarLetterifier) ? ` Did you mean ${civilWarLetterifier}?` : "";
|
|
2979
|
-
throw new Error(`Source skill not found: ${sourceSkill}.${hint}`);
|
|
2980
|
-
}
|
|
2981
|
-
function planProjectInit(input) {
|
|
2982
|
-
if (!input.name.trim()) throw new Error("Project name is required");
|
|
2983
|
-
const registryPath2 = resolve2(projectRegistryPath({ ...process.env, [PROJECT_REGISTRY_ENV]: input.registryPath || process.env[PROJECT_REGISTRY_ENV] }));
|
|
2984
|
-
const registry = loadProjectRegistry(registryPath2);
|
|
2985
|
-
const now = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
2986
|
-
const slug = input.projectSlug ?? slugifyProjectName(input.name);
|
|
2987
|
-
const targetDir = resolve2(input.targetDir ?? defaultProjectTargetDir(input.name, input.cwd));
|
|
2988
|
-
const identifier = (input.projectIdentifier ?? deriveProjectIdentifier(input.name)).toUpperCase();
|
|
2989
|
-
const existing = registry.projects[slug];
|
|
2990
|
-
const sourceSkillPath = resolveSourceSkillPath(input.sourceSkill);
|
|
2991
|
-
const overwrite = input.overwrite ?? input.force ?? false;
|
|
2992
|
-
const agentRole = normalizeAgentRole(input.agentRole);
|
|
2993
|
-
const agents = input.provisionAgent ? {
|
|
2994
|
-
...existing?.agents ?? {},
|
|
2995
|
-
[agentRole]: {
|
|
2996
|
-
role: agentRole,
|
|
2997
|
-
provisioning_state: "planned"
|
|
2998
|
-
}
|
|
2999
|
-
} : existing?.agents ?? {};
|
|
3000
|
-
const scaffold = input.scaffold ?? true;
|
|
3001
|
-
const candidateProject = {
|
|
3002
|
-
name: input.name,
|
|
3003
|
-
slug,
|
|
3004
|
-
repo_path: targetDir,
|
|
3005
|
-
description: input.description ?? "",
|
|
3006
|
-
status: "planned",
|
|
3007
|
-
source_artifacts: sourceSkillPath ? [{ kind: "skill", path: sourceSkillPath, package_name: input.packageName ?? slug }] : [],
|
|
3008
|
-
template: {
|
|
3009
|
-
commonproject: {
|
|
3010
|
-
enabled: true,
|
|
3011
|
-
primary_language: input.primaryLanguage ?? "python"
|
|
3012
|
-
}
|
|
3013
|
-
},
|
|
3014
|
-
ticket_provider: {
|
|
3015
|
-
type: input.ticketProvider ?? "plane",
|
|
3016
|
-
workspace: input.planeWorkspace ?? "33god",
|
|
3017
|
-
identifier,
|
|
3018
|
-
board_id: input.planeProjectId ?? "",
|
|
3019
|
-
board_url: input.planeProjectId ? `https://plane.delo.sh/${input.planeWorkspace ?? "33god"}/projects/${input.planeProjectId}/issues/` : "",
|
|
3020
|
-
state: input.live ? "planned" : "planned"
|
|
3021
|
-
},
|
|
3022
|
-
agents,
|
|
3023
|
-
created_at: existing?.created_at ?? now,
|
|
3024
|
-
updated_at: now
|
|
3025
|
-
};
|
|
3026
|
-
const project = {
|
|
3027
|
-
...candidateProject,
|
|
3028
|
-
updated_at: projectRecordEquivalent(existing, candidateProject) ? existing.updated_at : now
|
|
3029
|
-
};
|
|
3030
|
-
validateNoDuplicateProject(registry, project, overwrite);
|
|
3031
|
-
const pjanglerRoot = resolve2(input.pjanglerRoot ?? resolvePjanglerRoot2());
|
|
3032
|
-
const manifest = projectManifestFromRegistryProject(project);
|
|
3033
|
-
const apply = input.apply ?? false;
|
|
3034
|
-
const live = input.live ?? false;
|
|
3035
|
-
const actions = [
|
|
3036
|
-
{ kind: "registry.upsert", registryPath: registryPath2, slug, project }
|
|
3037
|
-
];
|
|
3038
|
-
if (scaffold) {
|
|
3039
|
-
actions.push(buildCommonProjectCopierAction({
|
|
3040
|
-
pjanglerRoot,
|
|
3041
|
-
targetDir,
|
|
3042
|
-
projectName: project.name,
|
|
3043
|
-
projectDescription: project.description,
|
|
3044
|
-
projectSlug: project.slug,
|
|
3045
|
-
ticketProvider: project.ticket_provider.type,
|
|
3046
|
-
planeWorkspace: project.ticket_provider.workspace ?? "33god",
|
|
3047
|
-
planeProjectId: project.ticket_provider.board_id ?? "",
|
|
3048
|
-
projectIdentifier: identifier,
|
|
3049
|
-
primaryLanguage: project.template.commonproject.primary_language,
|
|
3050
|
-
overwrite
|
|
3051
|
-
}));
|
|
3052
|
-
}
|
|
3053
|
-
actions.push(
|
|
3054
|
-
{ kind: "project.write-manifest", path: join10(targetDir, ".project.json"), manifest },
|
|
3055
|
-
{
|
|
3056
|
-
kind: "plane.create-or-link",
|
|
3057
|
-
enabled: live,
|
|
3058
|
-
live,
|
|
3059
|
-
workspace: project.ticket_provider.workspace ?? "33god",
|
|
3060
|
-
identifier,
|
|
3061
|
-
state: live ? "planned" : "planned",
|
|
3062
|
-
reason: live ? void 0 : "network/cloud actions require --live"
|
|
3063
|
-
},
|
|
3064
|
-
{
|
|
3065
|
-
kind: "hermes.provision-agent",
|
|
3066
|
-
enabled: input.provisionAgent ?? false,
|
|
3067
|
-
local: !live,
|
|
3068
|
-
targetDir,
|
|
3069
|
-
targetRepo: slug,
|
|
3070
|
-
role: agentRole,
|
|
3071
|
-
context: {
|
|
3072
|
-
skipRuntimeRepo: !live,
|
|
3073
|
-
skipPlane: !live,
|
|
3074
|
-
skipBloodbank: !live,
|
|
3075
|
-
skipSystemd: !live || process.platform === "darwin"
|
|
3076
|
-
}
|
|
3077
|
-
}
|
|
3078
|
-
);
|
|
3079
|
-
return { ok: true, apply, dryRun: !apply, live, registryPath: registryPath2, project, manifest, actions };
|
|
3080
|
-
}
|
|
3081
|
-
function executeProjectInitPlan(plan) {
|
|
3082
|
-
const logs = [];
|
|
3083
|
-
const errors = [];
|
|
3084
|
-
const changedFiles = [];
|
|
3085
|
-
if (!plan.apply) return { ok: true, plan, logs, errors, changedFiles };
|
|
3086
|
-
const registry = loadProjectRegistry(plan.registryPath);
|
|
3087
|
-
let pendingRegistryAction;
|
|
3088
|
-
for (const action of plan.actions) {
|
|
3089
|
-
if (action.kind === "copier.copy.commonproject") {
|
|
3090
|
-
mkdirSync6(dirname6(action.targetDir), { recursive: true });
|
|
3091
|
-
const result = spawnSync5(action.command[0], action.command.slice(1), { encoding: "utf8", cwd: action.cwd });
|
|
3092
|
-
if (result.stdout?.trim()) logs.push(result.stdout.trim());
|
|
3093
|
-
if (result.stderr?.trim()) logs.push(result.stderr.trim());
|
|
3094
|
-
if (result.error) {
|
|
3095
|
-
const code = result.error.code;
|
|
3096
|
-
errors.push(
|
|
3097
|
-
code === "ENOENT" ? "copier not found on PATH. Install with: uv tool install copier or pip install copier" : `copier failed: ${result.error.message}`
|
|
3098
|
-
);
|
|
3099
|
-
break;
|
|
3100
|
-
}
|
|
3101
|
-
if (result.status !== 0) {
|
|
3102
|
-
errors.push(`copier exited with status ${result.status ?? "unknown"}`);
|
|
3103
|
-
if (existsSync8(action.targetDir)) changedFiles.push(action.targetDir);
|
|
3104
|
-
break;
|
|
3105
|
-
}
|
|
3106
|
-
changedFiles.push(action.targetDir);
|
|
3107
|
-
} else if (action.kind === "project.write-manifest") {
|
|
3108
|
-
mkdirSync6(dirname6(action.path), { recursive: true });
|
|
3109
|
-
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
3110
|
-
`;
|
|
3111
|
-
const current = existsSync8(action.path) ? readFileSync4(action.path, "utf8") : void 0;
|
|
3112
|
-
if (current !== next) {
|
|
3113
|
-
writeFileSync5(action.path, next, "utf8");
|
|
3114
|
-
changedFiles.push(action.path);
|
|
3115
|
-
}
|
|
3116
|
-
} else if (action.kind === "registry.upsert") {
|
|
3117
|
-
pendingRegistryAction = action;
|
|
3118
|
-
} else if (action.kind === "plane.create-or-link") {
|
|
3119
|
-
logs.push(action.enabled ? "plane.create-or-link requires a live provider integration" : "plane.create-or-link skipped (requires --live)");
|
|
3120
|
-
} else if (action.kind === "hermes.provision-agent") {
|
|
3121
|
-
logs.push(action.enabled ? "hermes.provision-agent planned for the caller to execute" : "hermes.provision-agent skipped");
|
|
3122
|
-
}
|
|
3123
|
-
}
|
|
3124
|
-
if (pendingRegistryAction && errors.length === 0) {
|
|
3125
|
-
if (!projectRecordEquivalent(registry.projects[pendingRegistryAction.slug], pendingRegistryAction.project)) {
|
|
3126
|
-
registry.projects[pendingRegistryAction.slug] = pendingRegistryAction.project;
|
|
3127
|
-
saveProjectRegistry(registry, pendingRegistryAction.registryPath);
|
|
3128
|
-
changedFiles.push(pendingRegistryAction.registryPath);
|
|
3129
|
-
}
|
|
3130
|
-
}
|
|
3131
|
-
return { ok: errors.length === 0, plan, logs, errors, changedFiles };
|
|
3132
|
-
}
|
|
3133
|
-
function projectManifestFromRegistryProject(project) {
|
|
3134
|
-
const agents = Object.fromEntries(
|
|
3135
|
-
Object.entries(project.agents).map(([name, agent]) => [
|
|
3136
|
-
`${project.slug}-${name}`,
|
|
3137
|
-
{
|
|
3138
|
-
role: agent.role,
|
|
3139
|
-
role_dir: agent.role_dir,
|
|
3140
|
-
provisioning_state: agent.provisioning_state
|
|
3212
|
+
details.push(`would run: bash ${script}`);
|
|
3213
|
+
} else {
|
|
3214
|
+
const result = spawnSync5("bash", [script], { cwd: role.roleDir, encoding: "utf8" });
|
|
3215
|
+
if (result.status !== 0) details.push(`script failed: ${script}: ${result.stderr.trim() || result.stdout.trim()}`);
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3141
3218
|
}
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
workspace: project.ticket_provider.workspace ?? "",
|
|
3152
|
-
identifier: project.ticket_provider.identifier ?? "",
|
|
3153
|
-
board_id: project.ticket_provider.board_id ?? "",
|
|
3154
|
-
board_url: project.ticket_provider.board_url ?? "",
|
|
3155
|
-
state: project.ticket_provider.state
|
|
3156
|
-
},
|
|
3157
|
-
agents
|
|
3158
|
-
};
|
|
3159
|
-
}
|
|
3160
|
-
function formatProjectInitPlan(plan) {
|
|
3161
|
-
const lines = [""];
|
|
3162
|
-
const title = `${bold(plan.project.name)} ${dim(`(${plan.project.slug})`)}`;
|
|
3163
|
-
lines.push(` ${cyan(bold(glyph.chevron))} ${title}${plan.dryRun ? ` ${dim(glyph.dot)} ${yellow("dry run")}` : ""}`);
|
|
3164
|
-
lines.push(` ${dim("registry".padEnd(8))} ${dim(plan.registryPath)}`);
|
|
3165
|
-
lines.push(` ${dim("target".padEnd(8))} ${dim(plan.project.repo_path)}`);
|
|
3166
|
-
lines.push("");
|
|
3167
|
-
lines.push(` ${bold("Actions")} ${dim(`(${plan.actions.length})`)}`);
|
|
3168
|
-
if (!plan.actions.length) lines.push(` ${dim("(nothing to do)")}`);
|
|
3169
|
-
for (const action of plan.actions) {
|
|
3170
|
-
lines.push(` ${cyan(glyph.bullet)} ${action.kind}`);
|
|
3171
|
-
if (action.kind === "copier.copy.commonproject") lines.push(` ${dim(`target: ${action.targetDir}`)}`);
|
|
3172
|
-
if (action.kind === "project.write-manifest") lines.push(` ${dim(`path: ${action.path}`)}`);
|
|
3173
|
-
if (action.kind === "plane.create-or-link" && action.reason) lines.push(` ${dim(`note: ${action.reason}`)}`);
|
|
3219
|
+
return {
|
|
3220
|
+
id: finding.id,
|
|
3221
|
+
title: finding.title,
|
|
3222
|
+
status: details.some((detail) => detail.includes("failed:")) ? "blocked" : details.length ? ctx.dryRun ? "skipped" : "applied" : "noop",
|
|
3223
|
+
summary: details.length ? ctx.dryRun ? "Planned systemd remediation commands" : "Attempted systemd remediation" : "No changes required",
|
|
3224
|
+
changedFiles,
|
|
3225
|
+
details
|
|
3226
|
+
};
|
|
3227
|
+
}
|
|
3174
3228
|
}
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
}
|
|
3178
|
-
function formatProjectList(registry) {
|
|
3179
|
-
const projects = Object.values(registry.projects).sort((a, b) => a.slug.localeCompare(b.slug));
|
|
3180
|
-
if (!projects.length) return `
|
|
3181
|
-
${dim("No projects registered.")}
|
|
3229
|
+
];
|
|
3230
|
+
function writeIfDifferent(path, content, dryRun, changedFiles, mode) {
|
|
3231
|
+
const normalized = content.endsWith("\n") ? content : `${content}
|
|
3182
3232
|
`;
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
const slug = bold(project.slug.padEnd(slugWidth));
|
|
3189
|
-
const identifier = cyan(String(project.ticket_provider.identifier ?? "").padEnd(idWidth));
|
|
3190
|
-
const status = projectStatusColor(project.status)(project.status.padEnd(statusWidth));
|
|
3191
|
-
lines.push(` ${slug} ${identifier} ${status} ${dim(project.repo_path)}`);
|
|
3233
|
+
if (safeReadText(path) === normalized) return;
|
|
3234
|
+
changedFiles.push(path);
|
|
3235
|
+
if (!dryRun) {
|
|
3236
|
+
writeText(path, normalized);
|
|
3237
|
+
if (mode) chmodSync2(path, mode);
|
|
3192
3238
|
}
|
|
3193
|
-
lines.push("");
|
|
3194
|
-
return lines.join("\n");
|
|
3195
3239
|
}
|
|
3196
|
-
function
|
|
3197
|
-
|
|
3198
|
-
if (!project) throw new Error(`Project not found in registry: ${slug}`);
|
|
3199
|
-
return project;
|
|
3240
|
+
function getParityRuleIds() {
|
|
3241
|
+
return RULES.map((rule) => rule.id);
|
|
3200
3242
|
}
|
|
3201
|
-
function
|
|
3202
|
-
const
|
|
3203
|
-
const
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
} else {
|
|
3211
|
-
const manifestPath = join10(project.repo_path, ".project.json");
|
|
3212
|
-
if (!existsSync8(manifestPath)) issues.push({ level: "warn", slug: projectSlug, message: ".project.json is missing" });
|
|
3213
|
-
}
|
|
3214
|
-
for (const artifact of project.source_artifacts) {
|
|
3215
|
-
if (artifact.path && !existsSync8(artifact.path)) {
|
|
3216
|
-
issues.push({ level: "warn", slug: projectSlug, message: `source artifact missing: ${artifact.path}` });
|
|
3217
|
-
}
|
|
3218
|
-
}
|
|
3219
|
-
}
|
|
3243
|
+
function runAudit(repoArg) {
|
|
3244
|
+
const pjanglerRoot = resolvePjanglerRoot2();
|
|
3245
|
+
const ctx = {
|
|
3246
|
+
repoRoot: resolve2(repoArg ?? process.cwd()),
|
|
3247
|
+
dryRun: true,
|
|
3248
|
+
pjanglerRoot,
|
|
3249
|
+
homeDir: homedir5()
|
|
3250
|
+
};
|
|
3251
|
+
const rules = RULES.map((rule) => rule.audit(ctx));
|
|
3220
3252
|
return {
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3253
|
+
repo: ctx.repoRoot,
|
|
3254
|
+
ok: rules.every((rule) => rule.status === "pass" || rule.status === "skip"),
|
|
3255
|
+
auditedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3256
|
+
rules
|
|
3225
3257
|
};
|
|
3226
3258
|
}
|
|
3227
|
-
function
|
|
3228
|
-
const
|
|
3229
|
-
const
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
plane_workspace: input.planeWorkspace,
|
|
3235
|
-
plane_project_id: input.planeProjectId ?? "",
|
|
3236
|
-
project_identifier: input.projectIdentifier,
|
|
3237
|
-
primary_language: input.primaryLanguage
|
|
3259
|
+
function runMigrationForRules(ruleIds, repoArg, dryRun) {
|
|
3260
|
+
const pjanglerRoot = resolvePjanglerRoot2();
|
|
3261
|
+
const ctx = {
|
|
3262
|
+
repoRoot: resolve2(repoArg ?? process.cwd()),
|
|
3263
|
+
dryRun,
|
|
3264
|
+
pjanglerRoot,
|
|
3265
|
+
homeDir: homedir5()
|
|
3238
3266
|
};
|
|
3239
|
-
const
|
|
3240
|
-
|
|
3241
|
-
|
|
3267
|
+
const selected = RULES.filter((rule) => ruleIds.includes(rule.id));
|
|
3268
|
+
if (!selected.length) {
|
|
3269
|
+
throw new Error(`Unknown parity rules: ${ruleIds.join(", ")}`);
|
|
3270
|
+
}
|
|
3271
|
+
const results = selected.map((rule) => {
|
|
3272
|
+
try {
|
|
3273
|
+
return rule.migrate(ctx, rule.audit(ctx));
|
|
3274
|
+
} catch (err) {
|
|
3275
|
+
return {
|
|
3276
|
+
id: rule.id,
|
|
3277
|
+
title: rule.title,
|
|
3278
|
+
status: "blocked",
|
|
3279
|
+
summary: `migrate threw: ${err instanceof Error ? err.message : String(err)}`,
|
|
3280
|
+
changedFiles: [],
|
|
3281
|
+
details: []
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
});
|
|
3285
|
+
const changedFiles = Array.from(new Set(results.flatMap((result) => result.changedFiles))).sort();
|
|
3242
3286
|
return {
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3287
|
+
repo: ctx.repoRoot,
|
|
3288
|
+
dryRun,
|
|
3289
|
+
ok: results.every((result) => result.status !== "blocked"),
|
|
3290
|
+
selectedRules: selected.map((rule) => rule.id),
|
|
3291
|
+
results,
|
|
3292
|
+
changedFiles
|
|
3249
3293
|
};
|
|
3250
3294
|
}
|
|
3251
|
-
function
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3295
|
+
function runMigration(selector, repoArg, dryRun, all) {
|
|
3296
|
+
const ruleIds = all ? RULES.map((rule) => rule.id) : selector ? [selector] : [];
|
|
3297
|
+
return runMigrationForRules(ruleIds, repoArg, dryRun);
|
|
3298
|
+
}
|
|
3299
|
+
function prettyTimestamp(iso) {
|
|
3300
|
+
const match = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})/.exec(iso);
|
|
3301
|
+
return match ? `${match[1]} ${match[2]} UTC` : iso;
|
|
3302
|
+
}
|
|
3303
|
+
function formatAuditReport(report) {
|
|
3304
|
+
const counts = {};
|
|
3305
|
+
for (const rule of report.rules) counts[rule.status] = (counts[rule.status] ?? 0) + 1;
|
|
3306
|
+
const idWidth = report.rules.reduce((width, rule) => Math.max(width, rule.id.length), 0);
|
|
3307
|
+
const tally = [];
|
|
3308
|
+
if (counts.pass) tally.push(green(`${counts.pass} passed`));
|
|
3309
|
+
if (counts.fail) tally.push(red(`${counts.fail} failed`));
|
|
3310
|
+
if (counts.warn) tally.push(yellow(`${counts.warn} warning${counts.warn === 1 ? "" : "s"}`));
|
|
3311
|
+
if (counts.skip) tally.push(gray(`${counts.skip} skipped`));
|
|
3312
|
+
const overall = report.ok ? `${green(glyph.pass)} ${bold("Parity audit passed")}` : `${red(glyph.fail)} ${bold("Parity audit failed")}`;
|
|
3313
|
+
const lines = [""];
|
|
3314
|
+
lines.push(` ${overall}${tally.length ? ` ${dim(glyph.dot)} ${joinDot(tally)}` : ""}`);
|
|
3315
|
+
lines.push(` ${dim(report.repo)} ${dim(glyph.dot)} ${dim(prettyTimestamp(report.auditedAt))}`);
|
|
3316
|
+
lines.push("");
|
|
3317
|
+
for (const rule of report.rules) {
|
|
3318
|
+
const style = statusStyle(rule.status);
|
|
3319
|
+
lines.push(` ${style.color(style.glyph)} ${style.color(rule.id.padEnd(idWidth))} ${rule.summary}`);
|
|
3320
|
+
for (const detail of rule.details) lines.push(` ${dim(glyph.arrow)} ${dim(detail)}`);
|
|
3256
3321
|
}
|
|
3257
|
-
|
|
3322
|
+
lines.push("");
|
|
3323
|
+
return lines.join("\n");
|
|
3258
3324
|
}
|
|
3259
|
-
function
|
|
3260
|
-
const
|
|
3261
|
-
|
|
3262
|
-
|
|
3325
|
+
function formatMigrationReport(report) {
|
|
3326
|
+
const idWidth = report.results.reduce((width, result) => Math.max(width, result.id.length), 0);
|
|
3327
|
+
const overall = report.ok ? `${green(glyph.pass)} ${bold(report.dryRun ? "Migration preview complete" : "Migration complete")}` : `${red(glyph.fail)} ${bold("Migration finished with blockers")}`;
|
|
3328
|
+
const lines = [""];
|
|
3329
|
+
lines.push(` ${overall}${report.dryRun ? ` ${dim(glyph.dot)} ${yellow("dry run")}` : ""}`);
|
|
3330
|
+
lines.push(` ${dim(report.repo)}`);
|
|
3331
|
+
if (report.selectedRules.length) lines.push(` ${dim(`rules: ${report.selectedRules.join(", ")}`)}`);
|
|
3332
|
+
lines.push("");
|
|
3333
|
+
for (const result of report.results) {
|
|
3334
|
+
const style = statusStyle(result.status);
|
|
3335
|
+
lines.push(` ${style.color(style.glyph)} ${style.color(result.id.padEnd(idWidth))} ${result.summary} ${dim(`[${style.label}]`)}`);
|
|
3336
|
+
for (const detail of result.details) lines.push(` ${dim(glyph.arrow)} ${dim(detail)}`);
|
|
3337
|
+
for (const file of result.changedFiles) lines.push(` ${green(glyph.add)} ${file}`);
|
|
3263
3338
|
}
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
}
|
|
3269
|
-
if (existing.ticket_provider.identifier && existing.ticket_provider.identifier.toUpperCase() === project.ticket_provider.identifier?.toUpperCase()) {
|
|
3270
|
-
throw new Error(`Project identifier already registered by ${slug}: ${project.ticket_provider.identifier}`);
|
|
3271
|
-
}
|
|
3339
|
+
if (report.changedFiles.length) {
|
|
3340
|
+
lines.push("");
|
|
3341
|
+
lines.push(` ${bold(`Changed files (${report.changedFiles.length})`)}`);
|
|
3342
|
+
for (const file of report.changedFiles) lines.push(` ${green(glyph.add)} ${file}`);
|
|
3272
3343
|
}
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
if (!isRecord(project)) throw new Error(`Project ${key} must be a mapping`);
|
|
3276
|
-
if (!project.name) throw new Error(`Project ${key} missing name`);
|
|
3277
|
-
if (!project.slug) throw new Error(`Project ${key} missing slug`);
|
|
3278
|
-
if (project.slug !== key) throw new Error(`Project key ${key} does not match slug ${project.slug}`);
|
|
3279
|
-
if (!project.repo_path) throw new Error(`Project ${key} missing repo_path`);
|
|
3280
|
-
if (!Array.isArray(project.source_artifacts)) throw new Error(`Project ${key} source_artifacts must be a list`);
|
|
3281
|
-
if (!isRecord(project.ticket_provider)) throw new Error(`Project ${key} ticket_provider must be a mapping`);
|
|
3282
|
-
if (!isRecord(project.agents)) throw new Error(`Project ${key} agents must be a mapping`);
|
|
3283
|
-
}
|
|
3284
|
-
function expandHome(path) {
|
|
3285
|
-
if (path === "~") return homedir5();
|
|
3286
|
-
if (path.startsWith("~/")) return join10(homedir5(), path.slice(2));
|
|
3287
|
-
return path;
|
|
3288
|
-
}
|
|
3289
|
-
function isRecord(value) {
|
|
3290
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3344
|
+
lines.push("");
|
|
3345
|
+
return lines.join("\n");
|
|
3291
3346
|
}
|
|
3292
3347
|
|
|
3293
3348
|
// src/utils/version.ts
|
|
@@ -3395,7 +3450,7 @@ function projectInitActionLabel(kind) {
|
|
|
3395
3450
|
return "Render CommonProject scaffold";
|
|
3396
3451
|
case "project.write-manifest":
|
|
3397
3452
|
return "Write repo-local .project.json projection";
|
|
3398
|
-
case "
|
|
3453
|
+
case "ticket-provider.create-or-link":
|
|
3399
3454
|
return "Create/link ticket provider project";
|
|
3400
3455
|
case "hermes.provision-agent":
|
|
3401
3456
|
return "Provision Hermes agent";
|
|
@@ -3421,7 +3476,7 @@ function actionNeedsRun(plan, kind, syncMode) {
|
|
|
3421
3476
|
return !existsSync9(action.path) || readFileSync6(action.path, "utf8") !== next;
|
|
3422
3477
|
}
|
|
3423
3478
|
if (kind === "copier.copy.commonproject") return true;
|
|
3424
|
-
if (kind === "
|
|
3479
|
+
if (kind === "ticket-provider.create-or-link") return plan.actions.some((action) => action.kind === kind && action.enabled);
|
|
3425
3480
|
if (kind === "hermes.provision-agent") return plan.actions.some((action) => action.kind === kind && action.enabled);
|
|
3426
3481
|
return true;
|
|
3427
3482
|
}
|
|
@@ -3498,26 +3553,39 @@ async function resolveProjectInitTarget(name, options) {
|
|
|
3498
3553
|
identifier: options.identifier ?? defaults.identifier
|
|
3499
3554
|
};
|
|
3500
3555
|
}
|
|
3501
|
-
|
|
3502
|
-
program.name("pjangler").description("Project subsystem bootstrapper CLI").version(PJANGLER_VERSION);
|
|
3503
|
-
program.command("init").argument("<subsystem>", "Subsystem to initialize").description("Initialize a project subsystem").option("--dry-run", "Preview changes without writing files").option("-f, --force", "Overwrite existing files").action(async (subsystem, options) => {
|
|
3556
|
+
async function runRecipeSubsystem(name, options) {
|
|
3504
3557
|
const context = {
|
|
3505
3558
|
targetDir: process.cwd(),
|
|
3506
3559
|
force: options.force || false,
|
|
3507
3560
|
dryRun: options.dryRun || false
|
|
3508
3561
|
};
|
|
3509
3562
|
try {
|
|
3510
|
-
const recipe = createRecipe(
|
|
3563
|
+
const recipe = createRecipe(name, context);
|
|
3511
3564
|
if (!recipe) {
|
|
3512
|
-
console.error(`${xmark} Unknown subsystem: ${bold(
|
|
3565
|
+
console.error(`${xmark} Unknown subsystem: ${bold(name)}`);
|
|
3513
3566
|
console.error(` ${dim("Available:")} ${getRecipeNames().map((available) => cyan(available)).join(dim(", "))}`);
|
|
3514
3567
|
process.exit(1);
|
|
3515
3568
|
}
|
|
3516
3569
|
await recipe.execute();
|
|
3517
3570
|
} catch (error) {
|
|
3518
|
-
console.error(`${xmark} Error
|
|
3571
|
+
console.error(`${xmark} Error scaffolding ${bold(name)}:`, error);
|
|
3519
3572
|
process.exit(1);
|
|
3520
3573
|
}
|
|
3574
|
+
}
|
|
3575
|
+
var program = new Command3();
|
|
3576
|
+
program.name("pjangler").description("Project subsystem bootstrapper CLI").version(PJANGLER_VERSION);
|
|
3577
|
+
program.command("init").argument("[name]", "Project name to bootstrap (omit inside an existing git repo)").description("Bootstrap a project: registry entry + CommonProject scaffold + .project.json").option("--description <text>", "Project description").option("--target-dir <path>", "Target repo path").option("--source-skill <path>", "Source skill/template provenance path").option("--primary-language <language>", "Primary language for CommonProject rendering", "python").option("--provision-agent", "Plan local Hermes PM agent provisioning").option("--agent-role <role>", "Hermes agent role to plan when --provision-agent is set", "pm").option("--apply", "Write the registry and render the repo scaffold").option("--dry-run", "Preview changes without writing files (default)").option("--live", "Allow live/network/cloud provisioning actions").option("--slug <slug>", "Project registry slug override").option("--identifier <identifier>", "Ticket identifier override").option("--ticket-provider <type>", "Ticket provider: plane | trello", "plane").option("--board-id <id>", "Board id (Plane project UUID or Trello board id)").option("--board-url <url>", "Board URL override (derived from provider + board-id if omitted)").option("--workspace <name>", "Ticket workspace/org (Plane workspace; blank for Trello)").option("--registry <path>", `Registry path override (default: ${projectRegistryPath()})`).option("-f, --force", "Allow replacing an existing registry entry and re-rendering files").option("-y, --yes", "Apply every proposed operation without prompting").option("--no-tui", "Disable interactive prompts").option("--json", "Output machine-parseable JSON").action(async (name, options) => {
|
|
3578
|
+
if (name && getRecipeNames().includes(name)) {
|
|
3579
|
+
if (!options.json) {
|
|
3580
|
+
console.error(`${yellow(glyph.warn)} ${dim(`"pjangler init ${name}" is deprecated \u2014 use "pjangler add ${name}". Forwarding\u2026`)}`);
|
|
3581
|
+
}
|
|
3582
|
+
await runRecipeSubsystem(name, { force: options.force, dryRun: options.dryRun });
|
|
3583
|
+
return;
|
|
3584
|
+
}
|
|
3585
|
+
await runProjectInit(name, options);
|
|
3586
|
+
});
|
|
3587
|
+
program.command("add").argument("<subsystem>", "Subsystem to scaffold (mise, docker, node, agent-hooks, \u2026)").description("Scaffold a subsystem/component into the current repo").option("--dry-run", "Preview changes without writing files").option("-f, --force", "Overwrite existing files").action(async (subsystem, options) => {
|
|
3588
|
+
await runRecipeSubsystem(subsystem, options);
|
|
3521
3589
|
});
|
|
3522
3590
|
program.command("list").description("List available subsystems").action(() => {
|
|
3523
3591
|
const width = Object.keys(RECIPE_REGISTRY).reduce((max, name) => Math.max(max, name.length), 0);
|
|
@@ -3529,13 +3597,17 @@ program.command("list").description("List available subsystems").action(() => {
|
|
|
3529
3597
|
}
|
|
3530
3598
|
console.log("");
|
|
3531
3599
|
console.log(` ${dim("Examples")}`);
|
|
3532
|
-
for (const example of ["pj
|
|
3600
|
+
for (const example of ["pj add mise", "pj add docker", "pj add node"]) {
|
|
3533
3601
|
console.log(` ${dim(glyph.pointer)} ${dim(example)}`);
|
|
3534
3602
|
}
|
|
3535
3603
|
console.log("");
|
|
3536
3604
|
});
|
|
3537
3605
|
var projectCmd = program.command("project").description("Manage the pjangler project registry");
|
|
3538
|
-
projectCmd.command("init").argument("[name]", "Project display name").description("Plan or apply a registry-backed CommonProject initialization or legacy repo sync").option("--description <text>", "Project description").option("--target-dir <path>", "Target repo path").option("--source-skill <path>", "Source skill/template provenance path").option("--primary-language <language>", "Primary language for CommonProject rendering", "python").option("--provision-agent", "Plan local Hermes PM agent provisioning").option("--agent-role <role>", "Hermes agent role to plan when --provision-agent is set", "pm").option("--apply", "Write the registry and render the repo scaffold").option("--dry-run", "Preview changes without writing files (default)").option("--live", "Allow live/network/cloud provisioning actions").option("--slug <slug>", "Project registry slug override").option("--identifier <identifier>", "Ticket identifier override").option("--registry <path>", `Registry path override (default: ${projectRegistryPath()})`).option("-f, --force", "Allow replacing an existing registry entry and re-rendering files").option("-y, --yes", "Apply every proposed operation without prompting").option("--no-tui", "Disable interactive prompts").option("--json", "Output machine-parseable JSON").action(
|
|
3606
|
+
projectCmd.command("init").argument("[name]", "Project display name").description("Plan or apply a registry-backed CommonProject initialization or legacy repo sync").option("--description <text>", "Project description").option("--target-dir <path>", "Target repo path").option("--source-skill <path>", "Source skill/template provenance path").option("--primary-language <language>", "Primary language for CommonProject rendering", "python").option("--provision-agent", "Plan local Hermes PM agent provisioning").option("--agent-role <role>", "Hermes agent role to plan when --provision-agent is set", "pm").option("--apply", "Write the registry and render the repo scaffold").option("--dry-run", "Preview changes without writing files (default)").option("--live", "Allow live/network/cloud provisioning actions").option("--slug <slug>", "Project registry slug override").option("--identifier <identifier>", "Ticket identifier override").option("--ticket-provider <type>", "Ticket provider: plane | trello", "plane").option("--board-id <id>", "Board id (Plane project UUID or Trello board id)").option("--board-url <url>", "Board URL override (derived from provider + board-id if omitted)").option("--workspace <name>", "Ticket workspace/org (Plane workspace; blank for Trello)").option("--registry <path>", `Registry path override (default: ${projectRegistryPath()})`).option("-f, --force", "Allow replacing an existing registry entry and re-rendering files").option("-y, --yes", "Apply every proposed operation without prompting").option("--no-tui", "Disable interactive prompts").option("--json", "Output machine-parseable JSON").action((name, options) => {
|
|
3607
|
+
if (!options.json) console.error(`${yellow(glyph.warn)} ${dim('"pjangler project init" is deprecated \u2014 use "pjangler init".')}`);
|
|
3608
|
+
return runProjectInit(name, options);
|
|
3609
|
+
});
|
|
3610
|
+
async function runProjectInit(name, options) {
|
|
3539
3611
|
try {
|
|
3540
3612
|
const target = await resolveProjectInitTarget(name, options);
|
|
3541
3613
|
const interactive = isInteractiveProjectInit(options);
|
|
@@ -3552,6 +3624,10 @@ projectCmd.command("init").argument("[name]", "Project display name").descriptio
|
|
|
3552
3624
|
live: options.live ?? false,
|
|
3553
3625
|
projectSlug: target.slug,
|
|
3554
3626
|
projectIdentifier: target.identifier,
|
|
3627
|
+
ticketProvider: options.ticketProvider,
|
|
3628
|
+
boardId: options.boardId,
|
|
3629
|
+
boardUrl: options.boardUrl,
|
|
3630
|
+
boardWorkspace: options.workspace,
|
|
3555
3631
|
registryPath: options.registry,
|
|
3556
3632
|
force: options.force ?? false,
|
|
3557
3633
|
overwrite: options.force ?? false,
|
|
@@ -3635,7 +3711,7 @@ projectCmd.command("init").argument("[name]", "Project display name").descriptio
|
|
|
3635
3711
|
}
|
|
3636
3712
|
process.exit(1);
|
|
3637
3713
|
}
|
|
3638
|
-
}
|
|
3714
|
+
}
|
|
3639
3715
|
projectCmd.command("list").description("List projects in the pjangler registry").option("--registry <path>", `Registry path override (default: ${projectRegistryPath()})`).option("--json", "Output machine-parseable JSON").action((options) => {
|
|
3640
3716
|
try {
|
|
3641
3717
|
const registry = loadProjectRegistry(options.registry ?? projectRegistryPath());
|
|
@@ -3720,27 +3796,11 @@ recipeCmd.command("describe").argument("<name>", "Recipe name").description("Sho
|
|
|
3720
3796
|
console.log("");
|
|
3721
3797
|
console.log(` ${dim("Usage")}`);
|
|
3722
3798
|
console.log(` ${dim(glyph.pointer)} ${dim(`pj recipe run ${name}`)}`);
|
|
3723
|
-
console.log(` ${dim(glyph.pointer)} ${dim(`pj
|
|
3799
|
+
console.log(` ${dim(glyph.pointer)} ${dim(`pj add ${name}`)}`);
|
|
3724
3800
|
console.log("");
|
|
3725
3801
|
});
|
|
3726
3802
|
recipeCmd.command("run").argument("<name>", "Recipe name").description("Execute a specific recipe").option("--dry-run", "Preview changes without writing files").option("-f, --force", "Overwrite existing files").action(async (name, options) => {
|
|
3727
|
-
|
|
3728
|
-
targetDir: process.cwd(),
|
|
3729
|
-
force: options.force || false,
|
|
3730
|
-
dryRun: options.dryRun || false
|
|
3731
|
-
};
|
|
3732
|
-
try {
|
|
3733
|
-
const recipe = createRecipe(name, context);
|
|
3734
|
-
if (!recipe) {
|
|
3735
|
-
console.error(`${xmark} Recipe not found: ${bold(name)}`);
|
|
3736
|
-
console.error(` ${dim("Available:")} ${getRecipeNames().map((available) => cyan(available)).join(dim(", "))}`);
|
|
3737
|
-
process.exit(1);
|
|
3738
|
-
}
|
|
3739
|
-
await recipe.execute();
|
|
3740
|
-
} catch (error) {
|
|
3741
|
-
console.error(`${xmark} Error running recipe ${bold(name)}:`, error);
|
|
3742
|
-
process.exit(1);
|
|
3743
|
-
}
|
|
3803
|
+
await runRecipeSubsystem(name, options);
|
|
3744
3804
|
});
|
|
3745
3805
|
var commandCmd = program.command("command").alias("cmd").description("Manage pjangler commands");
|
|
3746
3806
|
commandCmd.command("list").description("List all available commands").option("-g, --group", "Group commands by category").action((options) => {
|
|
@@ -3864,7 +3924,7 @@ program.command("migrate").argument("[rule-id]", "Rule ID to migrate (omit to op
|
|
|
3864
3924
|
process.exit(1);
|
|
3865
3925
|
}
|
|
3866
3926
|
});
|
|
3867
|
-
program.command("hermes-agent").alias("hermes").description("Provision the PM agent for the current repo (defaults everything; only asks about Telegram)").option("-y, --yes", "Non-interactive: accept all defaults (also skips the Telegram prompt)").option("--target-repo <name>", "Target repo name (default: basename of cwd)").option("--role <role>", "Agent role override (default: pm \u2014 the only role in the fleet)").option("--purpose <text>", 'One-line agent purpose (default: "pm agent for <repo>")').option(`--tone <tone>`, `Personality tone (default: direct; ${SOUL_TONES.join(" | ")})`).option("--model-provider <name>", 'Inference provider override ("" = inherit shared default profile)').option("--model-name <name>", 'Model name override ("" = inherit shared default profile)').option("--skip-telegram", "Skip the Telegram wire-up (no BotFather prompt)").option("--email", "Also provision the delo.sh email address (off by default; never prompted)").option("--skip-runtime-repo", "Skip creating the per-agent runtime GH repo").option("--skip-plane", "Skip creating the
|
|
3927
|
+
program.command("hermes-agent").alias("hermes").description("Provision the PM agent for the current repo (defaults everything; only asks about Telegram)").option("-y, --yes", "Non-interactive: accept all defaults (also skips the Telegram prompt)").option("--target-repo <name>", "Target repo name (default: basename of cwd)").option("--role <role>", "Agent role override (default: pm \u2014 the only role in the fleet)").option("--purpose <text>", 'One-line agent purpose (default: "pm agent for <repo>")').option(`--tone <tone>`, `Personality tone (default: direct; ${SOUL_TONES.join(" | ")})`).option("--model-provider <name>", 'Inference provider override ("" = inherit shared default profile)').option("--model-name <name>", 'Model name override ("" = inherit shared default profile)').option("--skip-telegram", "Skip the Telegram wire-up (no BotFather prompt)").option("--email", "Also provision the delo.sh email address (off by default; never prompted)").option("--skip-runtime-repo", "Skip creating the per-agent runtime GH repo").option("--skip-plane", "Skip creating or linking the ticket board").option("--skip-bloodbank", "Skip installing the Bloodbank NATS consumer").option("--skip-systemd", "Skip installing systemd --user units").option("--local", "Local-only: skip runtime repo, ticket-board creation, Bloodbank, and systemd (safe for laptops/macOS/non-technical operators)").option("--force-config", "Regenerate ~/.config/hermes-agent-template/config.toml even if it exists").option("--dry-run", "Preview what would run; don't execute copier").option("-f, --force", "Re-render even if agents/hermes/<role>/role.yaml already exists").action(async (options) => {
|
|
3868
3928
|
const isDarwin = process.platform === "darwin";
|
|
3869
3929
|
const local = options.local ?? false;
|
|
3870
3930
|
const context = {
|