@delorenj/pjangler 1.2.3 → 1.2.6

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.
@@ -702,19 +702,6 @@ import * as p from "@clack/prompts";
702
702
 
703
703
  // src/commands/hermes/types.ts
704
704
  var HERMES_AGENT_TEMPLATE = "gh:delorenj/hermes-agent-template";
705
- var SOUL_TONES = ["direct", "playful", "formal", "terse"];
706
- var ROLE_CHOICES = [
707
- { value: "pm", label: "Project Manager (pm)", hint: "triage, planning, ticket authorship, board reconciliation" },
708
- { value: "dev", label: "Developer (dev)", hint: "implements tickets" },
709
- { value: "review", label: "Reviewer (review)", hint: "adversarial code review" },
710
- { value: "ops", label: "Ops (ops)", hint: "deploy / infra" },
711
- { value: "qa", label: "QA (qa)", hint: "test authorship + verification" }
712
- ];
713
- var TICKET_PROVIDERS = [
714
- { value: "plane", label: "Plane", hint: "self-hosted at plane.delo.sh (default)" },
715
- { value: "linear", label: "Linear", hint: "team board (created in Linear UI)" },
716
- { value: "trello", label: "Trello", hint: "board = project" }
717
- ];
718
705
  function deriveAgentId(repo, role) {
719
706
  return `${repo}-${role}`.toLowerCase();
720
707
  }
@@ -735,19 +722,18 @@ var PromptForAgentConfig = class extends Command {
735
722
  async invoke() {
736
723
  const ctx = this.context;
737
724
  const defaultRepo = basename(ctx.targetDir).toLowerCase();
738
- const defaultRole = "pm";
725
+ ctx.targetRepo = (ctx.targetRepo ?? defaultRepo).toLowerCase();
726
+ ctx.role ??= "pm";
727
+ ctx.agentPurpose ??= `${ctx.role} agent for ${ctx.targetRepo}`;
728
+ ctx.soulTone ??= "direct";
729
+ ctx.modelProvider ??= "";
730
+ ctx.modelName ??= "";
731
+ ctx.ticketProvider ??= detectTicketProvider(ctx.targetDir) ?? "plane";
732
+ ctx.skipEmail ??= true;
733
+ ctx.agentId = deriveAgentId(ctx.targetRepo, ctx.role);
734
+ ctx.profileName = deriveProfileName(ctx.targetRepo, ctx.role);
739
735
  if (ctx.yes) {
740
- ctx.targetRepo = (ctx.targetRepo ?? defaultRepo).toLowerCase();
741
- ctx.role ??= defaultRole;
742
- ctx.agentPurpose ??= `${ctx.role} agent for ${ctx.targetRepo}`;
743
- ctx.soulTone ??= "direct";
744
- ctx.modelProvider ??= "";
745
- ctx.modelName ??= "";
746
- ctx.ticketProvider ??= detectTicketProvider(ctx.targetDir) ?? "plane";
747
736
  ctx.skipTelegram ??= true;
748
- ctx.skipEmail ??= true;
749
- ctx.agentId = deriveAgentId(ctx.targetRepo, ctx.role);
750
- ctx.profileName = deriveProfileName(ctx.targetRepo, ctx.role);
751
737
  return {
752
738
  success: true,
753
739
  message: this.formatMessage(
@@ -755,96 +741,19 @@ var PromptForAgentConfig = class extends Command {
755
741
  )
756
742
  };
757
743
  }
758
- p.intro("\u2695 hermes-agent \xB7 add a new agent role to this repo");
759
- if (!ctx.targetRepo) {
760
- const answer = await p.text({
761
- message: "Target repo name",
762
- placeholder: defaultRepo,
763
- initialValue: defaultRepo,
764
- validate: (v) => v && v.trim() ? void 0 : "required"
765
- });
766
- if (p.isCancel(answer)) return this.cancelled();
767
- ctx.targetRepo = String(answer).trim().toLowerCase();
768
- }
769
- if (!ctx.role) {
770
- const answer = await p.select({
771
- message: "Role",
772
- options: ROLE_CHOICES.map((r) => ({ value: r.value, label: r.label, hint: r.hint })),
773
- initialValue: defaultRole
774
- });
775
- if (p.isCancel(answer)) return this.cancelled();
776
- ctx.role = String(answer).trim();
777
- }
778
- if (ctx.ticketProvider === void 0) {
779
- const detected = detectTicketProvider(ctx.targetDir);
780
- const answer = await p.select({
781
- message: "Ticket board provider",
782
- options: TICKET_PROVIDERS.map((t) => ({
783
- value: t.value,
784
- label: t.label,
785
- hint: t.value === detected ? `${t.hint} \u2014 current .project.json` : t.hint
786
- })),
787
- initialValue: detected ?? "plane"
788
- });
789
- if (p.isCancel(answer)) return this.cancelled();
790
- ctx.ticketProvider = answer;
791
- }
792
- if (!ctx.agentPurpose) {
793
- const answer = await p.text({
794
- message: "One-line purpose",
795
- placeholder: `${ctx.role} agent for ${ctx.targetRepo}`,
796
- initialValue: `${ctx.role} agent for ${ctx.targetRepo}`
797
- });
798
- if (p.isCancel(answer)) return this.cancelled();
799
- ctx.agentPurpose = String(answer).trim();
800
- }
801
- if (!ctx.soulTone) {
802
- const answer = await p.select({
803
- message: "Personality tone",
804
- options: SOUL_TONES.map((t) => ({
805
- value: t,
806
- label: t,
807
- hint: t === "direct" ? "decision-forward, no preamble (default)" : t === "terse" ? "minimum words, conclusion-first" : t === "playful" ? "warm, mildly funny" : "precise, structured"
808
- })),
809
- initialValue: "direct"
810
- });
811
- if (p.isCancel(answer)) return this.cancelled();
812
- ctx.soulTone = answer;
813
- }
814
- if (ctx.modelProvider === void 0) {
815
- const answer = await p.text({
816
- message: "Provider override (empty = inherit shared default profile)",
817
- placeholder: ""
818
- });
819
- if (p.isCancel(answer)) return this.cancelled();
820
- ctx.modelProvider = String(answer).trim();
821
- }
822
- if (ctx.modelName === void 0) {
823
- const answer = await p.text({
824
- message: "Model name override (empty = inherit shared default profile)",
825
- placeholder: ""
826
- });
827
- if (p.isCancel(answer)) return this.cancelled();
828
- ctx.modelName = String(answer).trim();
829
- }
744
+ p.intro("\u2695 hermes-agent \xB7 provision the PM agent for this repo");
745
+ p.log.info(
746
+ `agent ${ctx.agentId} \xB7 board ${ctx.ticketProvider} \xB7 tone ${ctx.soulTone}`
747
+ );
830
748
  if (ctx.skipTelegram === void 0) {
749
+ const botHandle = `${ctx.targetRepo.replace(/-/g, "_")}_${ctx.role}_bot`;
831
750
  const wire = await p.confirm({
832
- message: `Wire up the Telegram bot (@${ctx.targetRepo}_${ctx.role}_bot) now?`,
751
+ message: `Wire up the Telegram bot (@${botHandle}) now?`,
833
752
  initialValue: true
834
753
  });
835
754
  if (p.isCancel(wire)) return this.cancelled();
836
755
  ctx.skipTelegram = !wire;
837
756
  }
838
- if (ctx.skipEmail === void 0) {
839
- const wire = await p.confirm({
840
- message: `Provision the delo.sh email address (${ctx.targetRepo}-${ctx.role}@delo.sh) now?`,
841
- initialValue: true
842
- });
843
- if (p.isCancel(wire)) return this.cancelled();
844
- ctx.skipEmail = !wire;
845
- }
846
- ctx.agentId = deriveAgentId(ctx.targetRepo, ctx.role);
847
- ctx.profileName = deriveProfileName(ctx.targetRepo, ctx.role);
848
757
  return {
849
758
  success: true,
850
759
  message: this.formatMessage(
@@ -1119,7 +1028,7 @@ var WireEmail = class extends Command {
1119
1028
  async invoke() {
1120
1029
  const ctx = this.context;
1121
1030
  if (ctx.skipEmail) {
1122
- return { success: true, message: "\u2192 Email wire-up skipped" };
1031
+ return { success: true, message: "" };
1123
1032
  }
1124
1033
  if (ctx.dryRun) {
1125
1034
  return { success: true, message: this.formatMessage("Would create CF Email Routing rule") };
@@ -1227,7 +1136,7 @@ var PrintHermesSummary = class extends Command {
1227
1136
  lines.push(`role dir ${ctx.roleDir}`);
1228
1137
  lines.push(`runtime gh:${runtimeRepo}`);
1229
1138
  lines.push(`telegram @${botHandle}${skipTelegram ? " (NOT yet wired)" : ""}`);
1230
- lines.push(`email ${email}${skipEmail ? " (NOT yet wired)" : ""}`);
1139
+ if (!skipEmail) lines.push(`email ${email}`);
1231
1140
  lines.push("");
1232
1141
  lines.push("Start daemons:");
1233
1142
  lines.push(` systemctl --user start ${csm}`);
@@ -1240,11 +1149,10 @@ var PrintHermesSummary = class extends Command {
1240
1149
  lines.push("");
1241
1150
  lines.push("Talk locally:");
1242
1151
  lines.push(` ${ctx.roleDir}/hermes chat "status"`);
1243
- if (skipTelegram || skipEmail) {
1152
+ if (skipTelegram) {
1244
1153
  lines.push("");
1245
- lines.push("Deferred \u2014 re-run pjangler hermes-agent without --yes (or with explicit flags):");
1246
- if (skipTelegram) lines.push(" pjangler hermes-agent --skip-telegram=false # wire just telegram");
1247
- if (skipEmail) lines.push(" pjangler hermes-agent --skip-email=false # wire just email");
1154
+ lines.push("Wire Telegram later:");
1155
+ lines.push(" pjangler hermes-agent # re-run and answer yes when asked");
1248
1156
  }
1249
1157
  p5.note(lines.join("\n"), `Provisioned ${agentId}`);
1250
1158
  p5.outro("Done.");
@@ -1279,89 +1187,466 @@ var HermesAgentRecipe = class extends Recipe {
1279
1187
  };
1280
1188
 
1281
1189
  // src/commands/AgentHooksCommands.ts
1282
- import { homedir as homedir3 } from "node:os";
1283
- import { join as join8, dirname as dirname4 } from "node:path";
1284
- import { existsSync as existsSync6, cpSync, mkdirSync as mkdirSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
1190
+ import { homedir as homedir4 } from "node:os";
1191
+ import { join as join9, dirname as dirname5 } from "node:path";
1192
+ import { existsSync as existsSync7, cpSync, mkdirSync as mkdirSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
1285
1193
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1286
- function resolveTemplateRoot() {
1287
- const candidates = [];
1288
- if (process.env.PJANGLER_COMMONPROJECT_TEMPLATE) {
1289
- candidates.push(process.env.PJANGLER_COMMONPROJECT_TEMPLATE);
1194
+
1195
+ // src/project/index.ts
1196
+ import { spawnSync as spawnSync4 } from "node:child_process";
1197
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync2, renameSync, statSync, writeFileSync as writeFileSync3 } from "node:fs";
1198
+ import { homedir as homedir3 } from "node:os";
1199
+ import { basename as basename2, dirname as dirname4, join as join8, resolve } from "node:path";
1200
+ import YAML from "yaml";
1201
+ var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
1202
+ var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
1203
+ var KNOWN_SKILL_ROOTS = [
1204
+ "/home/delorenj/code/skillex/all-skills",
1205
+ "/home/delorenj/code/CoachingAgentFramework/.agents/skills",
1206
+ "/home/delorenj/code/pjangler/.agents/skills",
1207
+ join8(homedir3(), ".codex", "skills")
1208
+ ];
1209
+ function projectRegistryPath(env2 = process.env) {
1210
+ return expandHome(env2[PROJECT_REGISTRY_ENV] || join8(homedir3(), ".config", "pjangler", "projects.yaml"));
1211
+ }
1212
+ function emptyProjectRegistry() {
1213
+ return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
1214
+ }
1215
+ function loadProjectRegistry(path = projectRegistryPath()) {
1216
+ if (!existsSync6(path)) return emptyProjectRegistry();
1217
+ const raw = YAML.parse(readFileSync2(path, "utf8"));
1218
+ if (raw == null) return emptyProjectRegistry();
1219
+ if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
1220
+ const registry = raw;
1221
+ const normalized = {
1222
+ schema_version: Number(registry.schema_version ?? PROJECT_REGISTRY_SCHEMA_VERSION),
1223
+ projects: isRecord(registry.projects) ? registry.projects : {}
1224
+ };
1225
+ validateProjectRegistry(normalized);
1226
+ return normalized;
1227
+ }
1228
+ function saveProjectRegistry(registry, path = projectRegistryPath()) {
1229
+ validateProjectRegistry(registry);
1230
+ mkdirSync4(dirname4(path), { recursive: true });
1231
+ const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
1232
+ writeFileSync3(temp, YAML.stringify(registry, { lineWidth: 0 }), "utf8");
1233
+ renameSync(temp, path);
1234
+ }
1235
+ function validateProjectRegistry(registry) {
1236
+ if (registry.schema_version !== PROJECT_REGISTRY_SCHEMA_VERSION) {
1237
+ throw new Error(`Unsupported project registry schema_version: ${registry.schema_version}`);
1290
1238
  }
1291
- try {
1292
- let dir = dirname4(fileURLToPath2(import.meta.url));
1293
- for (let i = 0; i < 8; i++) {
1294
- candidates.push(join8(dir, "templates", "commonproject", "template"));
1295
- const parent = dirname4(dir);
1296
- if (parent === dir) break;
1297
- dir = parent;
1239
+ if (!isRecord(registry.projects)) throw new Error("Project registry projects must be a mapping");
1240
+ const slugs = /* @__PURE__ */ new Set();
1241
+ const repoPaths = /* @__PURE__ */ new Map();
1242
+ const identifiers = /* @__PURE__ */ new Map();
1243
+ for (const [slug, project] of Object.entries(registry.projects)) {
1244
+ validateProjectRecord(project, slug);
1245
+ if (slugs.has(project.slug)) throw new Error(`Duplicate project slug: ${project.slug}`);
1246
+ slugs.add(project.slug);
1247
+ const repoKey = resolve(project.repo_path);
1248
+ const existingRepoSlug = repoPaths.get(repoKey);
1249
+ if (existingRepoSlug && existingRepoSlug !== slug) {
1250
+ throw new Error(`Duplicate project repo_path: ${project.repo_path} used by ${existingRepoSlug} and ${slug}`);
1251
+ }
1252
+ repoPaths.set(repoKey, slug);
1253
+ const identifier = project.ticket_provider.identifier?.toUpperCase();
1254
+ if (identifier) {
1255
+ const existingIdentifierSlug = identifiers.get(identifier);
1256
+ if (existingIdentifierSlug && existingIdentifierSlug !== slug) {
1257
+ throw new Error(`Duplicate project identifier: ${identifier} used by ${existingIdentifierSlug} and ${slug}`);
1258
+ }
1259
+ identifiers.set(identifier, slug);
1298
1260
  }
1299
- } catch {
1300
1261
  }
1301
- candidates.push(join8(homedir3(), "code", "pjangler", "templates", "commonproject", "template"));
1302
- for (const c of candidates) {
1303
- if (existsSync6(join8(c, ".agents", "hooks", "hooks.master.json"))) return c;
1262
+ }
1263
+ function slugifyProjectName(value) {
1264
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
1265
+ }
1266
+ function deriveProjectIdentifier(value) {
1267
+ const compact = value.replace(/[^A-Za-z0-9]/g, "").toUpperCase();
1268
+ const identifier = compact.slice(0, 4) || "PROJ";
1269
+ return identifier.length >= 2 ? identifier : `${identifier}XX`.slice(0, 4);
1270
+ }
1271
+ function normalizeAgentRole(value) {
1272
+ return value?.trim() || "pm";
1273
+ }
1274
+ function resolveAgentHooksLayer(input, env2 = process.env) {
1275
+ if (typeof input === "boolean") return input;
1276
+ const override = env2.PJ_AGENT_HOOKS_LAYER;
1277
+ if (override === "0" || override === "false") return false;
1278
+ if (override === "1" || override === "true") return true;
1279
+ return !existsSync6(join8(homedir3(), ".agents", "hooks"));
1280
+ }
1281
+ function jsonStable(value) {
1282
+ return JSON.stringify(value);
1283
+ }
1284
+ function projectRecordEquivalent(a, b) {
1285
+ if (!a) return false;
1286
+ const { created_at: _aCreated, updated_at: _aUpdated, ...aComparable } = a;
1287
+ const { created_at: _bCreated, updated_at: _bUpdated, ...bComparable } = b;
1288
+ return jsonStable(aComparable) === jsonStable(bComparable);
1289
+ }
1290
+ function defaultProjectTargetDir(name, cwd = process.cwd()) {
1291
+ const compactName = name.replace(/[^A-Za-z0-9._-]/g, "") || slugifyProjectName(name);
1292
+ return resolve(dirname4(resolve(cwd)), compactName);
1293
+ }
1294
+ function resolveSourceSkillPath(sourceSkill) {
1295
+ if (!sourceSkill) return void 0;
1296
+ const expanded = expandHome(sourceSkill);
1297
+ const direct = resolve(expanded);
1298
+ if (existsSync6(direct)) return direct;
1299
+ const name = basename2(sourceSkill);
1300
+ for (const root of KNOWN_SKILL_ROOTS) {
1301
+ const candidate = join8(root, name);
1302
+ if (existsSync6(candidate)) return candidate;
1304
1303
  }
1305
- throw new Error(
1306
- "Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
1307
- );
1304
+ const civilWarLetterifier = "/home/delorenj/code/skillex/all-skills/civilwar-letterifier";
1305
+ const hint = existsSync6(civilWarLetterifier) ? ` Did you mean ${civilWarLetterifier}?` : "";
1306
+ throw new Error(`Source skill not found: ${sourceSkill}.${hint}`);
1308
1307
  }
1309
- var CopyAgentHooksTree = class extends Command {
1310
- async invoke() {
1311
- let templateRoot;
1312
- try {
1313
- templateRoot = resolveTemplateRoot();
1314
- } catch (e) {
1315
- return { success: false, message: `\u26A0\uFE0F ${e.message}` };
1308
+ function planProjectInit(input) {
1309
+ if (!input.name.trim()) throw new Error("Project name is required");
1310
+ const registryPath2 = resolve(projectRegistryPath({ ...process.env, [PROJECT_REGISTRY_ENV]: input.registryPath || process.env[PROJECT_REGISTRY_ENV] }));
1311
+ const registry = loadProjectRegistry(registryPath2);
1312
+ const now = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
1313
+ const slug = input.projectSlug ?? slugifyProjectName(input.name);
1314
+ const targetDir = resolve(input.targetDir ?? defaultProjectTargetDir(input.name, input.cwd));
1315
+ const identifier = (input.projectIdentifier ?? deriveProjectIdentifier(input.name)).toUpperCase();
1316
+ const existing = registry.projects[slug];
1317
+ const sourceSkillPath = resolveSourceSkillPath(input.sourceSkill);
1318
+ const overwrite = input.overwrite ?? input.force ?? false;
1319
+ const agentRole = normalizeAgentRole(input.agentRole);
1320
+ const agents = input.provisionAgent ? {
1321
+ ...existing?.agents ?? {},
1322
+ [agentRole]: {
1323
+ role: agentRole,
1324
+ provisioning_state: "planned"
1316
1325
  }
1317
- const items = [
1318
- { rel: ".agents/hooks", dir: true },
1319
- { rel: ".agents/local.example.json", dir: false },
1320
- { rel: ".mise/scripts/link-project-skills-to-clis.sh", dir: false },
1321
- { rel: ".mise/scripts/unlink-project-skills-from-clis.sh", dir: false },
1322
- { rel: ".mise/scripts/hindsight-setup.sh", dir: false }
1323
- ];
1324
- const created = [];
1325
- const skipped = [];
1326
- for (const { rel, dir } of items) {
1327
- const src = join8(templateRoot, rel);
1328
- const dest = join8(this.context.targetDir, rel);
1329
- if (!existsSync6(src)) continue;
1330
- if (existsSync6(dest) && !this.context.force) {
1331
- skipped.push(rel);
1332
- continue;
1333
- }
1334
- if (!this.context.dryRun) {
1335
- mkdirSync4(dirname4(dest), { recursive: true });
1336
- cpSync(src, dest, { recursive: dir, force: true });
1326
+ } : existing?.agents ?? {};
1327
+ const scaffold = input.scaffold ?? true;
1328
+ const candidateProject = {
1329
+ name: input.name,
1330
+ slug,
1331
+ repo_path: targetDir,
1332
+ description: input.description ?? "",
1333
+ status: "planned",
1334
+ source_artifacts: sourceSkillPath ? [{ kind: "skill", path: sourceSkillPath, package_name: input.packageName ?? slug }] : [],
1335
+ template: {
1336
+ commonproject: {
1337
+ enabled: true,
1338
+ primary_language: input.primaryLanguage ?? "python"
1337
1339
  }
1338
- created.push(rel);
1339
- }
1340
- const verb = this.context.dryRun ? "Would copy" : "Copied";
1341
- const tail = skipped.length ? ` (${skipped.length} already present \u2014 use --force to overwrite)` : "";
1342
- return {
1343
- success: created.length > 0,
1344
- message: this.formatMessage(`\u2705 ${verb} ${created.length} agent-hooks path(s)${tail}`)
1345
- };
1340
+ },
1341
+ ticket_provider: {
1342
+ type: input.ticketProvider ?? "plane",
1343
+ workspace: input.planeWorkspace ?? "33god",
1344
+ identifier,
1345
+ board_id: input.planeProjectId ?? "",
1346
+ board_url: input.planeProjectId ? `https://plane.delo.sh/${input.planeWorkspace ?? "33god"}/projects/${input.planeProjectId}/issues/` : "",
1347
+ state: input.live ? "planned" : "planned"
1348
+ },
1349
+ agents,
1350
+ created_at: existing?.created_at ?? now,
1351
+ updated_at: now
1352
+ };
1353
+ const project = {
1354
+ ...candidateProject,
1355
+ updated_at: projectRecordEquivalent(existing, candidateProject) ? existing.updated_at : now
1356
+ };
1357
+ validateNoDuplicateProject(registry, project, overwrite);
1358
+ const pjanglerRoot = resolve(input.pjanglerRoot ?? resolvePjanglerRoot());
1359
+ const manifest = projectManifestFromRegistryProject(project);
1360
+ const apply = input.apply ?? false;
1361
+ const live = input.live ?? false;
1362
+ const actions = [
1363
+ { kind: "registry.upsert", registryPath: registryPath2, slug, project }
1364
+ ];
1365
+ if (scaffold) {
1366
+ actions.push(buildCommonProjectCopierAction({
1367
+ pjanglerRoot,
1368
+ targetDir,
1369
+ projectName: project.name,
1370
+ projectDescription: project.description,
1371
+ projectSlug: project.slug,
1372
+ ticketProvider: project.ticket_provider.type,
1373
+ planeWorkspace: project.ticket_provider.workspace ?? "33god",
1374
+ planeProjectId: project.ticket_provider.board_id ?? "",
1375
+ projectIdentifier: identifier,
1376
+ primaryLanguage: project.template.commonproject.primary_language,
1377
+ agentHooksLayer: resolveAgentHooksLayer(input.agentHooksLayer),
1378
+ overwrite
1379
+ }));
1346
1380
  }
1347
- };
1348
- var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
1349
- static MARKER = "# pjangler:agent-hooks";
1350
- static CR = "{{config_root}}";
1351
- // mise's own runtime var — emitted literally
1352
- async invoke() {
1353
- const misePath = join8(this.context.targetDir, "mise.toml");
1354
- if (!existsSync6(misePath)) {
1355
- return {
1356
- success: false,
1357
- message: "\u26A0\uFE0F No mise.toml found \u2014 run `pjangler init mise` first, then re-run."
1358
- };
1359
- }
1360
- let content = readFileSync2(misePath, "utf8");
1361
- if (content.includes(_WireMiseAgentHooks.MARKER)) {
1362
- return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
1381
+ actions.push(
1382
+ { kind: "project.write-manifest", path: join8(targetDir, ".project.json"), manifest },
1383
+ {
1384
+ kind: "plane.create-or-link",
1385
+ enabled: live,
1386
+ live,
1387
+ workspace: project.ticket_provider.workspace ?? "33god",
1388
+ identifier,
1389
+ state: live ? "planned" : "planned",
1390
+ reason: live ? void 0 : "network/cloud actions require --live"
1391
+ },
1392
+ {
1393
+ kind: "hermes.provision-agent",
1394
+ enabled: input.provisionAgent ?? false,
1395
+ local: !live,
1396
+ targetDir,
1397
+ targetRepo: slug,
1398
+ role: agentRole,
1399
+ context: {
1400
+ skipRuntimeRepo: !live,
1401
+ skipPlane: !live,
1402
+ skipBloodbank: !live,
1403
+ skipSystemd: !live || process.platform === "darwin"
1404
+ }
1363
1405
  }
1364
- const cr = _WireMiseAgentHooks.CR;
1406
+ );
1407
+ return { ok: true, apply, dryRun: !apply, live, registryPath: registryPath2, project, manifest, actions };
1408
+ }
1409
+ function executeProjectInitPlan(plan) {
1410
+ const logs = [];
1411
+ const errors = [];
1412
+ const changedFiles = [];
1413
+ if (!plan.apply) return { ok: true, plan, logs, errors, changedFiles };
1414
+ const registry = loadProjectRegistry(plan.registryPath);
1415
+ let pendingRegistryAction;
1416
+ for (const action of plan.actions) {
1417
+ if (action.kind === "copier.copy.commonproject") {
1418
+ logs.push(
1419
+ 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"
1420
+ );
1421
+ mkdirSync4(dirname4(action.targetDir), { recursive: true });
1422
+ const result = spawnSync4(action.command[0], action.command.slice(1), { encoding: "utf8", cwd: action.cwd });
1423
+ if (result.stdout?.trim()) logs.push(result.stdout.trim());
1424
+ if (result.stderr?.trim()) logs.push(result.stderr.trim());
1425
+ if (result.error) {
1426
+ const code = result.error.code;
1427
+ errors.push(
1428
+ code === "ENOENT" ? "copier not found on PATH. Install with: uv tool install copier or pip install copier" : `copier failed: ${result.error.message}`
1429
+ );
1430
+ break;
1431
+ }
1432
+ if (result.status !== 0) {
1433
+ errors.push(`copier exited with status ${result.status ?? "unknown"}`);
1434
+ if (existsSync6(action.targetDir)) changedFiles.push(action.targetDir);
1435
+ break;
1436
+ }
1437
+ changedFiles.push(action.targetDir);
1438
+ } else if (action.kind === "project.write-manifest") {
1439
+ mkdirSync4(dirname4(action.path), { recursive: true });
1440
+ const next = `${JSON.stringify(action.manifest, null, 2)}
1441
+ `;
1442
+ const current = existsSync6(action.path) ? readFileSync2(action.path, "utf8") : void 0;
1443
+ if (current !== next) {
1444
+ writeFileSync3(action.path, next, "utf8");
1445
+ changedFiles.push(action.path);
1446
+ }
1447
+ } else if (action.kind === "registry.upsert") {
1448
+ pendingRegistryAction = action;
1449
+ } else if (action.kind === "plane.create-or-link") {
1450
+ logs.push(action.enabled ? "plane.create-or-link requires a live provider integration" : "plane.create-or-link skipped (requires --live)");
1451
+ } else if (action.kind === "hermes.provision-agent") {
1452
+ logs.push(action.enabled ? "hermes.provision-agent planned for the caller to execute" : "hermes.provision-agent skipped");
1453
+ }
1454
+ }
1455
+ if (pendingRegistryAction && errors.length === 0) {
1456
+ if (!projectRecordEquivalent(registry.projects[pendingRegistryAction.slug], pendingRegistryAction.project)) {
1457
+ registry.projects[pendingRegistryAction.slug] = pendingRegistryAction.project;
1458
+ saveProjectRegistry(registry, pendingRegistryAction.registryPath);
1459
+ changedFiles.push(pendingRegistryAction.registryPath);
1460
+ }
1461
+ }
1462
+ return { ok: errors.length === 0, plan, logs, errors, changedFiles };
1463
+ }
1464
+ function projectManifestFromRegistryProject(project) {
1465
+ const agents = Object.fromEntries(
1466
+ Object.entries(project.agents).map(([name, agent]) => [
1467
+ `${project.slug}-${name}`,
1468
+ {
1469
+ role: agent.role,
1470
+ role_dir: agent.role_dir,
1471
+ provisioning_state: agent.provisioning_state
1472
+ }
1473
+ ])
1474
+ );
1475
+ return {
1476
+ project_name: project.name,
1477
+ project_description: project.description,
1478
+ project_slug: project.slug,
1479
+ repo_path: project.repo_path,
1480
+ ticket_provider: {
1481
+ type: project.ticket_provider.type,
1482
+ workspace: project.ticket_provider.workspace ?? "",
1483
+ identifier: project.ticket_provider.identifier ?? "",
1484
+ board_id: project.ticket_provider.board_id ?? "",
1485
+ board_url: project.ticket_provider.board_url ?? "",
1486
+ state: project.ticket_provider.state
1487
+ },
1488
+ agents
1489
+ };
1490
+ }
1491
+ function getProject(registry, slug) {
1492
+ const project = registry.projects[slug];
1493
+ if (!project) throw new Error(`Project not found in registry: ${slug}`);
1494
+ return project;
1495
+ }
1496
+ function buildCommonProjectCopierAction(input) {
1497
+ const templateDir = join8(input.pjanglerRoot, "templates", "commonproject");
1498
+ const data = {
1499
+ project_name: input.projectName,
1500
+ project_description: input.projectDescription ?? "",
1501
+ project_slug: input.projectSlug,
1502
+ ticket_provider: input.ticketProvider,
1503
+ plane_workspace: input.planeWorkspace,
1504
+ plane_project_id: input.planeProjectId ?? "",
1505
+ project_identifier: input.projectIdentifier,
1506
+ primary_language: input.primaryLanguage,
1507
+ agent_hooks_layer: input.agentHooksLayer ?? true ? "true" : "false"
1508
+ };
1509
+ const command = ["copier", "copy", "--trust", templateDir, input.targetDir, "--defaults"];
1510
+ for (const [key, value] of Object.entries(data)) command.push("--data", `${key}=${value}`);
1511
+ if (input.overwrite) command.push("--overwrite");
1512
+ return {
1513
+ kind: "copier.copy.commonproject",
1514
+ cwd: input.pjanglerRoot,
1515
+ command,
1516
+ targetDir: input.targetDir,
1517
+ data,
1518
+ overwrite: input.overwrite
1519
+ };
1520
+ }
1521
+ function resolvePjanglerRoot() {
1522
+ let dir = dirname4(new URL(import.meta.url).pathname);
1523
+ while (dir !== dirname4(dir)) {
1524
+ if (existsSync6(join8(dir, "package.json")) && existsSync6(join8(dir, "templates", "commonproject", "copier.yml"))) return dir;
1525
+ dir = dirname4(dir);
1526
+ }
1527
+ return resolve(process.cwd());
1528
+ }
1529
+ function validateNoDuplicateProject(registry, project, overwrite) {
1530
+ const existingSameSlug = registry.projects[project.slug];
1531
+ if (existingSameSlug && !overwrite && resolve(existingSameSlug.repo_path) !== resolve(project.repo_path)) {
1532
+ throw new Error(`Project slug already exists in registry: ${project.slug}`);
1533
+ }
1534
+ for (const [slug, existing] of Object.entries(registry.projects)) {
1535
+ if (slug === project.slug) continue;
1536
+ if (resolve(existing.repo_path) === resolve(project.repo_path)) {
1537
+ throw new Error(`Project repo_path already registered by ${slug}: ${project.repo_path}`);
1538
+ }
1539
+ if (existing.ticket_provider.identifier && existing.ticket_provider.identifier.toUpperCase() === project.ticket_provider.identifier?.toUpperCase()) {
1540
+ throw new Error(`Project identifier already registered by ${slug}: ${project.ticket_provider.identifier}`);
1541
+ }
1542
+ }
1543
+ }
1544
+ function validateProjectRecord(project, key) {
1545
+ if (!isRecord(project)) throw new Error(`Project ${key} must be a mapping`);
1546
+ if (!project.name) throw new Error(`Project ${key} missing name`);
1547
+ if (!project.slug) throw new Error(`Project ${key} missing slug`);
1548
+ if (project.slug !== key) throw new Error(`Project key ${key} does not match slug ${project.slug}`);
1549
+ if (!project.repo_path) throw new Error(`Project ${key} missing repo_path`);
1550
+ if (!Array.isArray(project.source_artifacts)) throw new Error(`Project ${key} source_artifacts must be a list`);
1551
+ if (!isRecord(project.ticket_provider)) throw new Error(`Project ${key} ticket_provider must be a mapping`);
1552
+ if (!isRecord(project.agents)) throw new Error(`Project ${key} agents must be a mapping`);
1553
+ }
1554
+ function expandHome(path) {
1555
+ if (path === "~") return homedir3();
1556
+ if (path.startsWith("~/")) return join8(homedir3(), path.slice(2));
1557
+ return path;
1558
+ }
1559
+ function isRecord(value) {
1560
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1561
+ }
1562
+
1563
+ // src/commands/AgentHooksCommands.ts
1564
+ 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.";
1565
+ function resolveTemplateRoot() {
1566
+ const candidates = [];
1567
+ if (process.env.PJANGLER_COMMONPROJECT_TEMPLATE) {
1568
+ candidates.push(process.env.PJANGLER_COMMONPROJECT_TEMPLATE);
1569
+ }
1570
+ try {
1571
+ let dir = dirname5(fileURLToPath2(import.meta.url));
1572
+ for (let i = 0; i < 8; i++) {
1573
+ candidates.push(join9(dir, "templates", "commonproject", "template"));
1574
+ const parent = dirname5(dir);
1575
+ if (parent === dir) break;
1576
+ dir = parent;
1577
+ }
1578
+ } catch {
1579
+ }
1580
+ candidates.push(join9(homedir4(), "code", "pjangler", "templates", "commonproject", "template"));
1581
+ for (const c of candidates) {
1582
+ if (existsSync7(join9(c, ".agents", "hooks", "hooks.master.json"))) return c;
1583
+ }
1584
+ throw new Error(
1585
+ "Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
1586
+ );
1587
+ }
1588
+ var CopyAgentHooksTree = class extends Command {
1589
+ async invoke() {
1590
+ if (!resolveAgentHooksLayer()) {
1591
+ return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
1592
+ }
1593
+ let templateRoot;
1594
+ try {
1595
+ templateRoot = resolveTemplateRoot();
1596
+ } catch (e) {
1597
+ return { success: false, message: `\u26A0\uFE0F ${e.message}` };
1598
+ }
1599
+ const items = [
1600
+ { rel: ".agents/hooks", dir: true },
1601
+ { rel: ".agents/local.example.json", dir: false },
1602
+ { rel: ".mise/scripts/link-project-skills-to-clis.sh", dir: false },
1603
+ { rel: ".mise/scripts/unlink-project-skills-from-clis.sh", dir: false },
1604
+ { rel: ".mise/scripts/hindsight-setup.sh", dir: false }
1605
+ ];
1606
+ const created = [];
1607
+ const skipped = [];
1608
+ for (const { rel, dir } of items) {
1609
+ const src = join9(templateRoot, rel);
1610
+ const dest = join9(this.context.targetDir, rel);
1611
+ if (!existsSync7(src)) continue;
1612
+ if (existsSync7(dest) && !this.context.force) {
1613
+ skipped.push(rel);
1614
+ continue;
1615
+ }
1616
+ if (!this.context.dryRun) {
1617
+ mkdirSync5(dirname5(dest), { recursive: true });
1618
+ cpSync(src, dest, { recursive: dir, force: true });
1619
+ }
1620
+ created.push(rel);
1621
+ }
1622
+ const verb = this.context.dryRun ? "Would copy" : "Copied";
1623
+ const tail = skipped.length ? ` (${skipped.length} already present \u2014 use --force to overwrite)` : "";
1624
+ return {
1625
+ success: created.length > 0,
1626
+ message: this.formatMessage(`\u2705 ${verb} ${created.length} agent-hooks path(s)${tail}`)
1627
+ };
1628
+ }
1629
+ };
1630
+ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
1631
+ static MARKER = "# pjangler:agent-hooks";
1632
+ static CR = "{{config_root}}";
1633
+ // mise's own runtime var — emitted literally
1634
+ async invoke() {
1635
+ if (!resolveAgentHooksLayer()) {
1636
+ return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
1637
+ }
1638
+ const misePath = join9(this.context.targetDir, "mise.toml");
1639
+ if (!existsSync7(misePath)) {
1640
+ return {
1641
+ success: false,
1642
+ message: "\u26A0\uFE0F No mise.toml found \u2014 run `pjangler init mise` first, then re-run."
1643
+ };
1644
+ }
1645
+ let content = readFileSync3(misePath, "utf8");
1646
+ if (content.includes(_WireMiseAgentHooks.MARKER)) {
1647
+ return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
1648
+ }
1649
+ const cr = _WireMiseAgentHooks.CR;
1365
1650
  const enterAdds = [
1366
1651
  ` "${cr}/.mise/scripts/link-project-skills-to-clis.sh",`,
1367
1652
  ` "${cr}/.agents/hooks/sync.py --install --quiet",`
@@ -1433,7 +1718,7 @@ ${leaveBlock}`);
1433
1718
  ""
1434
1719
  ].join("\n");
1435
1720
  content = content.replace(/\n*$/, "\n") + appended;
1436
- if (!this.context.dryRun) writeFileSync3(misePath, content);
1721
+ if (!this.context.dryRun) writeFileSync4(misePath, content);
1437
1722
  if (wiredHooks) {
1438
1723
  return { success: true, message: this.formatMessage("\u2705 Wired mise.toml ([hooks] enter/leave + tasks)") };
1439
1724
  }
@@ -1587,18 +1872,18 @@ function createRecipe(name, context) {
1587
1872
  }
1588
1873
 
1589
1874
  // src/utils/version.ts
1590
- import { readFileSync as readFileSync3 } from "node:fs";
1591
- import { dirname as dirname5, join as join9 } from "node:path";
1875
+ import { readFileSync as readFileSync4 } from "node:fs";
1876
+ import { dirname as dirname6, join as join10 } from "node:path";
1592
1877
  import { fileURLToPath as fileURLToPath3 } from "node:url";
1593
1878
  var PJANGLER_VERSION = (() => {
1594
1879
  try {
1595
- let dir = dirname5(fileURLToPath3(import.meta.url));
1880
+ let dir = dirname6(fileURLToPath3(import.meta.url));
1596
1881
  for (let i = 0; i < 4; i++) {
1597
1882
  try {
1598
- const raw = readFileSync3(join9(dir, "package.json"), "utf8");
1883
+ const raw = readFileSync4(join10(dir, "package.json"), "utf8");
1599
1884
  return JSON.parse(raw).version ?? "0.0.0";
1600
1885
  } catch {
1601
- const parent = dirname5(dir);
1886
+ const parent = dirname6(dir);
1602
1887
  if (parent === dir) break;
1603
1888
  dir = parent;
1604
1889
  }
@@ -1609,11 +1894,11 @@ var PJANGLER_VERSION = (() => {
1609
1894
  })();
1610
1895
 
1611
1896
  // src/parity/index.ts
1612
- import { existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync5, readFileSync as readFileSync4, readlinkSync, readdirSync, renameSync, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4, chmodSync as chmodSync2, copyFileSync } from "node:fs";
1613
- import { basename as basename2, dirname as dirname6, join as join10, relative, resolve } from "node:path";
1897
+ import { existsSync as existsSync8, lstatSync, mkdirSync as mkdirSync6, readFileSync as readFileSync5, readlinkSync, readdirSync, renameSync as renameSync2, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5, chmodSync as chmodSync2, copyFileSync } from "node:fs";
1898
+ import { basename as basename3, dirname as dirname7, join as join11, relative, resolve as resolve2 } from "node:path";
1614
1899
  import { fileURLToPath as fileURLToPath4 } from "node:url";
1615
- import { homedir as homedir4 } from "node:os";
1616
- import { spawnSync as spawnSync4 } from "node:child_process";
1900
+ import { homedir as homedir5 } from "node:os";
1901
+ import { spawnSync as spawnSync5 } from "node:child_process";
1617
1902
  var LINK_AGENTFILES_BLOCK = `# This block will handle the linking of
1618
1903
  # agent files to the main AGENTS.md file.
1619
1904
  #
@@ -1681,13 +1966,13 @@ run = "{{config_root}}/.mise/scripts/versioning.sh check"
1681
1966
  description = "Force every versioned file up to the highest version"
1682
1967
  run = "{{config_root}}/.mise/scripts/versioning.sh sync"
1683
1968
  # <<< mise-versioning <<<`;
1684
- function resolvePjanglerRoot() {
1685
- let dir = dirname6(fileURLToPath4(import.meta.url));
1686
- while (dir !== dirname6(dir)) {
1687
- if (existsSync7(join10(dir, "package.json")) && existsSync7(join10(dir, "templates", "commonproject", "copier.yml"))) {
1969
+ function resolvePjanglerRoot2() {
1970
+ let dir = dirname7(fileURLToPath4(import.meta.url));
1971
+ while (dir !== dirname7(dir)) {
1972
+ if (existsSync8(join11(dir, "package.json")) && existsSync8(join11(dir, "templates", "commonproject", "copier.yml"))) {
1688
1973
  return dir;
1689
1974
  }
1690
- dir = dirname6(dir);
1975
+ dir = dirname7(dir);
1691
1976
  }
1692
1977
  throw new Error("Unable to resolve pjangler root");
1693
1978
  }
@@ -1695,22 +1980,22 @@ function normalizeNewlines(value) {
1695
1980
  return value.replace(/\r\n/g, "\n");
1696
1981
  }
1697
1982
  function readText(path) {
1698
- return normalizeNewlines(readFileSync4(path, "utf8"));
1983
+ return normalizeNewlines(readFileSync5(path, "utf8"));
1699
1984
  }
1700
1985
  function safeReadText(path) {
1701
- return existsSync7(path) ? readText(path) : null;
1986
+ return existsSync8(path) ? readText(path) : null;
1702
1987
  }
1703
1988
  function ensureParent(path) {
1704
- mkdirSync5(dirname6(path), { recursive: true });
1989
+ mkdirSync6(dirname7(path), { recursive: true });
1705
1990
  }
1706
1991
  function writeText(path, content) {
1707
1992
  ensureParent(path);
1708
- writeFileSync4(path, content);
1993
+ writeFileSync5(path, content);
1709
1994
  }
1710
- function tryParseJson(text3) {
1711
- if (!text3) return null;
1995
+ function tryParseJson(text2) {
1996
+ if (!text2) return null;
1712
1997
  try {
1713
- return JSON.parse(text3);
1998
+ return JSON.parse(text2);
1714
1999
  } catch {
1715
2000
  return null;
1716
2001
  }
@@ -1722,7 +2007,7 @@ function titleCaseSlug(slug) {
1722
2007
  return slug.split(/[-_]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
1723
2008
  }
1724
2009
  function readSymlinkTarget(path) {
1725
- if (!existsSync7(path)) return null;
2010
+ if (!existsSync8(path)) return null;
1726
2011
  try {
1727
2012
  return readlinkSync(path);
1728
2013
  } catch {
@@ -1730,7 +2015,7 @@ function readSymlinkTarget(path) {
1730
2015
  }
1731
2016
  }
1732
2017
  function ensureSymlink(path, target, dryRun) {
1733
- if (existsSync7(path)) {
2018
+ if (existsSync8(path)) {
1734
2019
  const stat = lstatSync(path);
1735
2020
  if (stat.isSymbolicLink()) {
1736
2021
  const current = readSymlinkTarget(path);
@@ -1747,21 +2032,21 @@ function ensureSymlink(path, target, dryRun) {
1747
2032
  return { changed: true };
1748
2033
  }
1749
2034
  function bootstrapAgentsFile(repoRoot, dryRun) {
1750
- const agentsPath = join10(repoRoot, "AGENTS.md");
1751
- if (existsSync7(agentsPath)) return { changedFiles: [], details: [] };
2035
+ const agentsPath = join11(repoRoot, "AGENTS.md");
2036
+ if (existsSync8(agentsPath)) return { changedFiles: [], details: [] };
1752
2037
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
1753
- const source = join10(repoRoot, file);
1754
- if (!existsSync7(source)) continue;
2038
+ const source = join11(repoRoot, file);
2039
+ if (!existsSync8(source)) continue;
1755
2040
  const stat = lstatSync(source);
1756
2041
  if (stat.isSymbolicLink()) continue;
1757
2042
  if (stat.isFile()) {
1758
- if (!dryRun) renameSync(source, agentsPath);
2043
+ if (!dryRun) renameSync2(source, agentsPath);
1759
2044
  return { changedFiles: [agentsPath], details: [`Moved ${file} to AGENTS.md before wiring agent-file symlinks`] };
1760
2045
  }
1761
2046
  return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
1762
2047
  }
1763
- const readmePath = join10(repoRoot, "README.md");
1764
- if (existsSync7(readmePath)) {
2048
+ const readmePath = join11(repoRoot, "README.md");
2049
+ if (existsSync8(readmePath)) {
1765
2050
  const stat = lstatSync(readmePath);
1766
2051
  if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
1767
2052
  if (!dryRun) copyFileSync(readmePath, agentsPath);
@@ -1769,9 +2054,9 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
1769
2054
  }
1770
2055
  return { changedFiles: [], details: [], blocked: "AGENTS.md missing and no CLAUDE.md, GEMINI.md, or README.md source exists" };
1771
2056
  }
1772
- function yamlGet(text3, keyPath) {
2057
+ function yamlGet(text2, keyPath) {
1773
2058
  const parts = keyPath.split(".");
1774
- const lines = text3.split("\n");
2059
+ const lines = text2.split("\n");
1775
2060
  let start = 0;
1776
2061
  let indent = 0;
1777
2062
  for (let idx = 0; idx < parts.length; idx += 1) {
@@ -1800,39 +2085,39 @@ function yamlGet(text3, keyPath) {
1800
2085
  return "";
1801
2086
  }
1802
2087
  function discoverRoles(repoRoot) {
1803
- const rolesDir = join10(repoRoot, "agents", "hermes");
1804
- if (!existsSync7(rolesDir)) return [];
2088
+ const rolesDir = join11(repoRoot, "agents", "hermes");
2089
+ if (!existsSync8(rolesDir)) return [];
1805
2090
  return readdirSync(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
1806
- const roleDir = join10(rolesDir, entry.name);
1807
- const roleYamlPath = join10(roleDir, "role.yaml");
1808
- if (!existsSync7(roleYamlPath)) return null;
1809
- const text3 = readText(roleYamlPath);
1810
- const runtimeRepoRaw = yamlGet(text3, "runtime.github_repo");
2091
+ const roleDir = join11(rolesDir, entry.name);
2092
+ const roleYamlPath = join11(roleDir, "role.yaml");
2093
+ if (!existsSync8(roleYamlPath)) return null;
2094
+ const text2 = readText(roleYamlPath);
2095
+ const runtimeRepoRaw = yamlGet(text2, "runtime.github_repo");
1811
2096
  return {
1812
- role: yamlGet(text3, "role") || entry.name,
2097
+ role: yamlGet(text2, "role") || entry.name,
1813
2098
  roleDir,
1814
2099
  roleYamlPath,
1815
- repo: yamlGet(text3, "repo"),
1816
- agentId: yamlGet(text3, "agent_id"),
1817
- profileName: yamlGet(text3, "profile") || yamlGet(text3, "agent_id"),
1818
- displayName: yamlGet(text3, "display_name"),
1819
- purpose: yamlGet(text3, "purpose"),
1820
- botHandle: yamlGet(text3, "telegram.bot_username"),
2100
+ repo: yamlGet(text2, "repo"),
2101
+ agentId: yamlGet(text2, "agent_id"),
2102
+ profileName: yamlGet(text2, "profile") || yamlGet(text2, "agent_id"),
2103
+ displayName: yamlGet(text2, "display_name"),
2104
+ purpose: yamlGet(text2, "purpose"),
2105
+ botHandle: yamlGet(text2, "telegram.bot_username"),
1821
2106
  runtimeRepo: runtimeRepoRaw.includes("/") ? runtimeRepoRaw.split("/").slice(-1)[0] ?? runtimeRepoRaw : runtimeRepoRaw,
1822
- runtimeOwner: yamlGet(text3, "runtime.github_owner"),
1823
- planeWorkspace: yamlGet(text3, "ticket_provider.workspace") || yamlGet(text3, "plane.workspace"),
1824
- ticketProviderName: yamlGet(text3, "ticket_provider.name"),
1825
- ticketProviderBoardId: yamlGet(text3, "ticket_provider.board_id"),
1826
- ticketProviderBoardUrl: yamlGet(text3, "ticket_provider.board_url"),
1827
- ticketProviderIdentifier: yamlGet(text3, "plane.identifier")
2107
+ runtimeOwner: yamlGet(text2, "runtime.github_owner"),
2108
+ planeWorkspace: yamlGet(text2, "ticket_provider.workspace") || yamlGet(text2, "plane.workspace"),
2109
+ ticketProviderName: yamlGet(text2, "ticket_provider.name"),
2110
+ ticketProviderBoardId: yamlGet(text2, "ticket_provider.board_id"),
2111
+ ticketProviderBoardUrl: yamlGet(text2, "ticket_provider.board_url"),
2112
+ ticketProviderIdentifier: yamlGet(text2, "plane.identifier")
1828
2113
  };
1829
2114
  }).filter((value) => Boolean(value));
1830
2115
  }
1831
2116
  function registryPath(homeDir) {
1832
- return join10(homeDir, ".hermes", "agents-registry.yaml");
2117
+ return join11(homeDir, ".hermes", "agents-registry.yaml");
1833
2118
  }
1834
2119
  function systemctlUser(args) {
1835
- const result = spawnSync4("systemctl", ["--user", ...args], { encoding: "utf8" });
2120
+ const result = spawnSync5("systemctl", ["--user", ...args], { encoding: "utf8" });
1836
2121
  return {
1837
2122
  ok: result.status === 0,
1838
2123
  stdout: result.stdout.trim(),
@@ -1840,8 +2125,8 @@ function systemctlUser(args) {
1840
2125
  };
1841
2126
  }
1842
2127
  function templateScript(ctx, name) {
1843
- const source = join10(ctx.pjanglerRoot, ".mise", "scripts", name);
1844
- return existsSync7(source) ? readText(source) : void 0;
2128
+ const source = join11(ctx.pjanglerRoot, ".mise", "scripts", name);
2129
+ return existsSync8(source) ? readText(source) : void 0;
1845
2130
  }
1846
2131
  function templateVersioningScript(ctx) {
1847
2132
  return templateScript(ctx, "versioning.sh");
@@ -1851,14 +2136,14 @@ function templateLinkAgentfilesScript(ctx) {
1851
2136
  }
1852
2137
  function renderGeneratedProjectMiseToml(ctx, template) {
1853
2138
  const project = readProjectJson(ctx);
1854
- const projectName = String(project?.project_name ?? basename2(ctx.repoRoot) ?? "project");
2139
+ const projectName = String(project?.project_name ?? basename3(ctx.repoRoot) ?? "project");
1855
2140
  return template.replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
1856
2141
  }
1857
2142
  function ensureMiseTomlFromTemplate(ctx, changedFiles) {
1858
- const targetPath = join10(ctx.repoRoot, "mise.toml");
1859
- if (existsSync7(targetPath)) return false;
1860
- const sourcePath = join10(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
1861
- if (!existsSync7(sourcePath)) return false;
2143
+ const targetPath = join11(ctx.repoRoot, "mise.toml");
2144
+ if (existsSync8(targetPath)) return false;
2145
+ const sourcePath = join11(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
2146
+ if (!existsSync8(sourcePath)) return false;
1862
2147
  changedFiles.push(targetPath);
1863
2148
  if (!ctx.dryRun) {
1864
2149
  writeText(targetPath, renderGeneratedProjectMiseToml(ctx, readText(sourcePath)));
@@ -1866,22 +2151,22 @@ function ensureMiseTomlFromTemplate(ctx, changedFiles) {
1866
2151
  return true;
1867
2152
  }
1868
2153
  function templateVersionFilesConf(ctx, repoRoot) {
1869
- const packageJson = join10(repoRoot, "package.json");
1870
- return existsSync7(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";
2154
+ const packageJson = join11(repoRoot, "package.json");
2155
+ 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";
1871
2156
  }
1872
- function replaceOrAppendManagedBlock(text3, startMarker, block, beforePattern) {
1873
- if (startMarker.test(text3)) {
1874
- return text3.replace(/# >>> mise-versioning >>>[\s\S]*?# <<< mise-versioning <<</, block);
2157
+ function replaceOrAppendManagedBlock(text2, startMarker, block, beforePattern) {
2158
+ if (startMarker.test(text2)) {
2159
+ return text2.replace(/# >>> mise-versioning >>>[\s\S]*?# <<< mise-versioning <<</, block);
1875
2160
  }
1876
2161
  if (beforePattern) {
1877
- const match = text3.match(beforePattern);
2162
+ const match = text2.match(beforePattern);
1878
2163
  if (match && typeof match.index === "number") {
1879
- return `${text3.slice(0, match.index).replace(/\s*$/, "\n\n")}${block}
2164
+ return `${text2.slice(0, match.index).replace(/\s*$/, "\n\n")}${block}
1880
2165
 
1881
- ${text3.slice(match.index)}`;
2166
+ ${text2.slice(match.index)}`;
1882
2167
  }
1883
2168
  }
1884
- return `${text3.replace(/\s*$/, "")}
2169
+ return `${text2.replace(/\s*$/, "")}
1885
2170
 
1886
2171
  ${block}
1887
2172
  `;
@@ -1891,22 +2176,22 @@ var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/herm
1891
2176
  function requiredMisePathEntries(ctx) {
1892
2177
  const required = [...BASE_MISE_PATH_ENTRIES];
1893
2178
  for (const candidate of CONDITIONAL_HERMES_PATHS) {
1894
- if (existsSync7(join10(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
2179
+ if (existsSync8(join11(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
1895
2180
  }
1896
2181
  return required;
1897
2182
  }
1898
- function upsertMisePath(text3, required = BASE_MISE_PATH_ENTRIES) {
2183
+ function upsertMisePath(text2, required = BASE_MISE_PATH_ENTRIES) {
1899
2184
  const render = (values) => `_.path = [${values.map((value) => JSON.stringify(value)).join(", ")}]`;
1900
- const envMatch = text3.match(/(^|\n)(\[env\][\s\S]*?)(?=\n\[[^\]]+\]|$)/);
2185
+ const envMatch = text2.match(/(^|\n)(\[env\][\s\S]*?)(?=\n\[[^\]]+\]|$)/);
1901
2186
  if (!envMatch || typeof envMatch.index !== "number") {
1902
2187
  return `[env]
1903
2188
  ${render(required)}
1904
2189
 
1905
- ${text3.replace(/^\s+/, "")}`;
2190
+ ${text2.replace(/^\s+/, "")}`;
1906
2191
  }
1907
- const prefix = text3.slice(0, envMatch.index + envMatch[1].length);
2192
+ const prefix = text2.slice(0, envMatch.index + envMatch[1].length);
1908
2193
  const section = envMatch[2];
1909
- const suffix = text3.slice(envMatch.index + envMatch[1].length + section.length);
2194
+ const suffix = text2.slice(envMatch.index + envMatch[1].length + section.length);
1910
2195
  const pathLine = section.match(/^_\.path\s*=\s*\[([^\]]*)\]\s*$/m);
1911
2196
  if (!pathLine) {
1912
2197
  return `${prefix}${section.replace(/\n?$/, "\n")}${render(required)}${suffix}`;
@@ -1917,11 +2202,11 @@ ${text3.replace(/^\s+/, "")}`;
1917
2202
  if (!merged.includes(value)) merged.push(value);
1918
2203
  }
1919
2204
  const nextLine = render(merged);
1920
- if (pathLine[0] === nextLine) return text3;
2205
+ if (pathLine[0] === nextLine) return text2;
1921
2206
  return `${prefix}${section.replace(pathLine[0], nextLine)}${suffix}`;
1922
2207
  }
1923
- function removeTomlSection(text3, headerPattern, marker, options) {
1924
- const lines = text3.split("\n");
2208
+ function removeTomlSection(text2, headerPattern, marker, options) {
2209
+ const lines = text2.split("\n");
1925
2210
  let start = -1;
1926
2211
  let end = -1;
1927
2212
  for (let i = 0; i < lines.length; i++) {
@@ -1946,7 +2231,7 @@ function removeTomlSection(text3, headerPattern, marker, options) {
1946
2231
  if (end === -1) end = lines.length;
1947
2232
  break;
1948
2233
  }
1949
- if (start === -1) return text3;
2234
+ if (start === -1) return text2;
1950
2235
  if (options?.includePrecedingComments) {
1951
2236
  while (start > 0 && lines[start - 1].trim().startsWith("#")) {
1952
2237
  start--;
@@ -1955,22 +2240,22 @@ function removeTomlSection(text3, headerPattern, marker, options) {
1955
2240
  const result = lines.slice(0, start).concat(lines.slice(end)).join("\n");
1956
2241
  return result.replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
1957
2242
  }
1958
- function insertTomlBlockBeforeVersioning(text3, block) {
1959
- const versioningIndex = text3.indexOf("# >>> mise-versioning >>>");
2243
+ function insertTomlBlockBeforeVersioning(text2, block) {
2244
+ const versioningIndex = text2.indexOf("# >>> mise-versioning >>>");
1960
2245
  if (versioningIndex >= 0) {
1961
- return `${text3.slice(0, versioningIndex).replace(/\s*$/, "\n\n")}${block}
2246
+ return `${text2.slice(0, versioningIndex).replace(/\s*$/, "\n\n")}${block}
1962
2247
 
1963
- ${text3.slice(versioningIndex)}`;
2248
+ ${text2.slice(versioningIndex)}`;
1964
2249
  }
1965
- return `${text3.replace(/\s*$/, "")}
2250
+ return `${text2.replace(/\s*$/, "")}
1966
2251
 
1967
2252
  ${block}
1968
2253
  `;
1969
2254
  }
1970
- function extractTomlStrings(text3) {
2255
+ function extractTomlStrings(text2) {
1971
2256
  const values = [];
1972
2257
  const stringPattern = /"((?:\\.|[^"\\])*)"|'([^']*)'/g;
1973
- for (const match of text3.matchAll(stringPattern)) {
2258
+ for (const match of text2.matchAll(stringPattern)) {
1974
2259
  if (match[1] !== void 0) {
1975
2260
  try {
1976
2261
  values.push(JSON.parse(`"${match[1]}"`));
@@ -1994,10 +2279,10 @@ function renderHookEntries(entries, indent = "") {
1994
2279
  `${indent}]`
1995
2280
  ];
1996
2281
  }
1997
- function upsertLinkAgentfilesHooks(text3) {
1998
- const lines = text3.split("\n");
2282
+ function upsertLinkAgentfilesHooks(text2) {
2283
+ const lines = text2.split("\n");
1999
2284
  const hooksStart = lines.findIndex((line) => /^\[hooks\]$/.test(line.trim()));
2000
- if (hooksStart === -1) return insertTomlBlockBeforeVersioning(text3, LINK_AGENTFILES_HOOKS_BLOCK);
2285
+ if (hooksStart === -1) return insertTomlBlockBeforeVersioning(text2, LINK_AGENTFILES_HOOKS_BLOCK);
2001
2286
  let hooksEnd = lines.length;
2002
2287
  for (let i = hooksStart + 1; i < lines.length; i++) {
2003
2288
  if (/^\[[^\]]+\]/.test(lines[i].trim())) {
@@ -2031,8 +2316,8 @@ function upsertLinkAgentfilesHooks(text3) {
2031
2316
  }
2032
2317
  return lines.slice(0, hooksStart + 1).concat(rendered, lines.slice(hooksStart + 1)).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
2033
2318
  }
2034
- function upsertLinkAgentfilesBlock(text3, ctx) {
2035
- const withPath = upsertMisePath(text3, requiredMisePathEntries(ctx));
2319
+ function upsertLinkAgentfilesBlock(text2, ctx) {
2320
+ const withPath = upsertMisePath(text2, requiredMisePathEntries(ctx));
2036
2321
  if (withPath.includes(LINK_AGENTFILES_BLOCK)) return withPath;
2037
2322
  let cleaned = removeTomlSection(withPath, /^\[tasks\.link-agentfiles\]$/, /link-agentfiles/, { includePrecedingComments: false });
2038
2323
  cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /AGENTS\.md/, { includePrecedingComments: false });
@@ -2040,12 +2325,12 @@ function upsertLinkAgentfilesBlock(text3, ctx) {
2040
2325
  return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
2041
2326
  }
2042
2327
  function readProjectJson(ctx) {
2043
- return tryParseJson(safeReadText(join10(ctx.repoRoot, ".project.json")));
2328
+ return tryParseJson(safeReadText(join11(ctx.repoRoot, ".project.json")));
2044
2329
  }
2045
2330
  function canonicalProjectJson(ctx) {
2046
2331
  const roles = discoverRoles(ctx.repoRoot);
2047
2332
  const existing = readProjectJson(ctx) ?? {};
2048
- const slug = String(existing.project_slug ?? slugifyRepoName(dirname6(ctx.repoRoot) === ctx.repoRoot ? ctx.repoRoot.split("/").pop() ?? "project" : ctx.repoRoot.split("/").pop() ?? "project"));
2333
+ const slug = String(existing.project_slug ?? slugifyRepoName(dirname7(ctx.repoRoot) === ctx.repoRoot ? ctx.repoRoot.split("/").pop() ?? "project" : ctx.repoRoot.split("/").pop() ?? "project"));
2049
2334
  const firstRole = roles[0];
2050
2335
  const ticketProvider = {
2051
2336
  type: String((existing.ticket_provider?.type ?? firstRole?.ticketProviderName ?? "plane") || "plane"),
@@ -2084,12 +2369,12 @@ function canonicalProjectJson(ctx) {
2084
2369
  };
2085
2370
  }
2086
2371
  function projectJsonFinding(ctx) {
2087
- const projectPath = join10(ctx.repoRoot, ".project.json");
2088
- const planeJsonPath = join10(ctx.repoRoot, ".plane.json");
2372
+ const projectPath = join11(ctx.repoRoot, ".project.json");
2373
+ const planeJsonPath = join11(ctx.repoRoot, ".plane.json");
2089
2374
  const details = [];
2090
2375
  const data = readProjectJson(ctx);
2091
2376
  const roles = discoverRoles(ctx.repoRoot);
2092
- if (!existsSync7(projectPath)) {
2377
+ if (!existsSync8(projectPath)) {
2093
2378
  return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json missing", details: [], fixable: true };
2094
2379
  }
2095
2380
  if (!data) {
@@ -2115,7 +2400,7 @@ function projectJsonFinding(ctx) {
2115
2400
  for (const key of ["type", "workspace", "identifier", "board_id", "board_url", "state"]) {
2116
2401
  if (!(key in ticketProvider)) details.push(`ticket_provider.${key} missing`);
2117
2402
  }
2118
- if (existsSync7(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
2403
+ if (existsSync8(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
2119
2404
  return {
2120
2405
  id: "sot.project-json",
2121
2406
  title: "Canonical .project.json",
@@ -2196,17 +2481,17 @@ exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" HERMES_OAUTH
2196
2481
  `.replace(/\u0010/g, "$");
2197
2482
  }
2198
2483
  function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
2199
- if (!existsSync7(sourceDir)) return;
2200
- mkdirSync5(targetDir, { recursive: true });
2484
+ if (!existsSync8(sourceDir)) return;
2485
+ mkdirSync6(targetDir, { recursive: true });
2201
2486
  for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
2202
- const sourcePath = join10(sourceDir, entry.name);
2487
+ const sourcePath = join11(sourceDir, entry.name);
2203
2488
  if (skip?.(sourcePath)) continue;
2204
- const targetPath = join10(targetDir, entry.name);
2489
+ const targetPath = join11(targetDir, entry.name);
2205
2490
  if (entry.isDirectory()) {
2206
2491
  copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
2207
2492
  continue;
2208
2493
  }
2209
- if (existsSync7(targetPath)) continue;
2494
+ if (existsSync8(targetPath)) continue;
2210
2495
  changedFiles.push(targetPath);
2211
2496
  if (!dryRun) {
2212
2497
  ensureParent(targetPath);
@@ -2215,7 +2500,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
2215
2500
  }
2216
2501
  }
2217
2502
  function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
2218
- const gitmodulesPath = join10(repoRoot, ".gitmodules");
2503
+ const gitmodulesPath = join11(repoRoot, ".gitmodules");
2219
2504
  const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
2220
2505
  const owner = role.runtimeOwner || "delorenj";
2221
2506
  const block = `[submodule "agents/hermes/${role.role}/runtime"]
@@ -2237,7 +2522,7 @@ function upsertRegistryEntry(role, homeDir, changedFiles, dryRun) {
2237
2522
  repo: ${role.repo}
2238
2523
  role: ${role.role}
2239
2524
  display_name: ${JSON.stringify(role.displayName || role.agentId)}
2240
- project_path: ${ctxEscape(role.roleDir ? dirname6(dirname6(dirname6(role.roleDir))) : "")}
2525
+ project_path: ${ctxEscape(role.roleDir ? dirname7(dirname7(dirname7(role.roleDir))) : "")}
2241
2526
  role_dir: ${ctxEscape(role.roleDir)}
2242
2527
  profile_name: ${role.profileName || role.agentId}
2243
2528
  telegram:
@@ -2259,9 +2544,9 @@ ${block}`) : `${current.replace(/\s*$/, "\n")}${block}`;
2259
2544
  return path;
2260
2545
  }
2261
2546
  function profileMetaInheritsDefault(path) {
2262
- const text3 = safeReadText(path);
2547
+ const text2 = safeReadText(path);
2263
2548
  return Boolean(
2264
- text3 && /^config:\s*$/m.test(text3) && /^\s+inherit_from:\s*default\s*$/m.test(text3) && /^\s+save_mode:\s*delta\s*$/m.test(text3)
2549
+ text2 && /^config:\s*$/m.test(text2) && /^\s+inherit_from:\s*default\s*$/m.test(text2) && /^\s+save_mode:\s*delta\s*$/m.test(text2)
2265
2550
  );
2266
2551
  }
2267
2552
  function upsertInheritedProfileMeta(path, changedFiles, dryRun) {
@@ -2315,21 +2600,21 @@ var RULES = [
2315
2600
  id: "mise.config-root",
2316
2601
  title: "mise config_root + AGENTS link hooks",
2317
2602
  audit: (ctx) => {
2318
- const misePath = join10(ctx.repoRoot, "mise.toml");
2319
- if (!existsSync7(misePath)) {
2603
+ const misePath = join11(ctx.repoRoot, "mise.toml");
2604
+ if (!existsSync8(misePath)) {
2320
2605
  return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
2321
2606
  }
2322
- const text3 = readText(misePath);
2607
+ const text2 = readText(misePath);
2323
2608
  const details = [];
2324
- const linkAgentfilesPath = join10(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
2325
- if (!existsSync7(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
2326
- const pathValues = [...(text3.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
2609
+ const linkAgentfilesPath = join11(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
2610
+ if (!existsSync8(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
2611
+ const pathValues = [...(text2.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
2327
2612
  const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
2328
2613
  if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
2329
- if (!text3.includes('"{{config_root}}/.mise/scripts/link-agentfiles.sh"')) details.push("link-agentfiles must use raw {{config_root}} guard");
2330
- if (!text3.includes("op inject -i .env.op > .env")) details.push("[hooks].enter must materialize .env from .env.op");
2331
- if (!text3.includes('patterns = ["AGENTS.md"]')) details.push("watch_files must monitor AGENTS.md");
2332
- if (!text3.includes('task = "link-agentfiles"')) details.push("watch_files must dispatch link-agentfiles task");
2614
+ if (!text2.includes('"{{config_root}}/.mise/scripts/link-agentfiles.sh"')) details.push("link-agentfiles must use raw {{config_root}} guard");
2615
+ if (!text2.includes("op inject -i .env.op > .env")) details.push("[hooks].enter must materialize .env from .env.op");
2616
+ if (!text2.includes('patterns = ["AGENTS.md"]')) details.push("watch_files must monitor AGENTS.md");
2617
+ if (!text2.includes('task = "link-agentfiles"')) details.push("watch_files must dispatch link-agentfiles task");
2333
2618
  return {
2334
2619
  id: "mise.config-root",
2335
2620
  title: "mise config_root + AGENTS link hooks",
@@ -2340,10 +2625,10 @@ var RULES = [
2340
2625
  };
2341
2626
  },
2342
2627
  migrate: (ctx, finding) => {
2343
- const path = join10(ctx.repoRoot, "mise.toml");
2628
+ const path = join11(ctx.repoRoot, "mise.toml");
2344
2629
  const changedFiles = [];
2345
2630
  const details = [];
2346
- if (!existsSync7(path)) {
2631
+ if (!existsSync8(path)) {
2347
2632
  if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
2348
2633
  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: [] };
2349
2634
  }
@@ -2352,14 +2637,14 @@ var RULES = [
2352
2637
  return { id: finding.id, title: finding.title, status: "applied", summary: "Would initialize mise.toml from generated-project template", changedFiles, details };
2353
2638
  }
2354
2639
  }
2355
- let text3 = readText(path);
2356
- const next = upsertLinkAgentfilesBlock(text3, ctx);
2357
- if (next !== text3) {
2640
+ let text2 = readText(path);
2641
+ const next = upsertLinkAgentfilesBlock(text2, ctx);
2642
+ if (next !== text2) {
2358
2643
  if (!changedFiles.includes(path)) changedFiles.push(path);
2359
2644
  if (!ctx.dryRun) writeText(path, next);
2360
- text3 = next;
2645
+ text2 = next;
2361
2646
  }
2362
- const linkAgentfilesPath = join10(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
2647
+ const linkAgentfilesPath = join11(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
2363
2648
  const expectedScript = templateLinkAgentfilesScript(ctx);
2364
2649
  if (expectedScript === void 0) {
2365
2650
  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: [] };
@@ -2386,13 +2671,13 @@ var RULES = [
2386
2671
  title: "managed mise versioning block",
2387
2672
  audit: (ctx) => {
2388
2673
  const details = [];
2389
- const misePath = join10(ctx.repoRoot, "mise.toml");
2390
- const versioningPath = join10(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
2391
- const manifestPath = join10(ctx.repoRoot, ".mise", "version-files.conf");
2392
- const text3 = safeReadText(misePath);
2393
- if (!text3?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
2394
- if (!existsSync7(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
2395
- if (!existsSync7(manifestPath)) details.push(".mise/version-files.conf missing");
2674
+ const misePath = join11(ctx.repoRoot, "mise.toml");
2675
+ const versioningPath = join11(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
2676
+ const manifestPath = join11(ctx.repoRoot, ".mise", "version-files.conf");
2677
+ const text2 = safeReadText(misePath);
2678
+ if (!text2?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
2679
+ if (!existsSync8(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
2680
+ if (!existsSync8(manifestPath)) details.push(".mise/version-files.conf missing");
2396
2681
  return {
2397
2682
  id: "mise.versioning",
2398
2683
  title: "managed mise versioning block",
@@ -2405,8 +2690,8 @@ var RULES = [
2405
2690
  migrate: (ctx, finding) => {
2406
2691
  const changedFiles = [];
2407
2692
  const details = [];
2408
- const misePath = join10(ctx.repoRoot, "mise.toml");
2409
- if (!existsSync7(misePath)) {
2693
+ const misePath = join11(ctx.repoRoot, "mise.toml");
2694
+ if (!existsSync8(misePath)) {
2410
2695
  if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
2411
2696
  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: [] };
2412
2697
  }
@@ -2421,7 +2706,7 @@ var RULES = [
2421
2706
  if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
2422
2707
  if (!ctx.dryRun) writeText(misePath, nextMise);
2423
2708
  }
2424
- const versioningPath = join10(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
2709
+ const versioningPath = join11(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
2425
2710
  const expectedScript = templateVersioningScript(ctx);
2426
2711
  if (expectedScript === void 0) {
2427
2712
  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: [] };
@@ -2433,7 +2718,7 @@ var RULES = [
2433
2718
  chmodSync2(versioningPath, 493);
2434
2719
  }
2435
2720
  }
2436
- const manifestPath = join10(ctx.repoRoot, ".mise", "version-files.conf");
2721
+ const manifestPath = join11(ctx.repoRoot, ".mise", "version-files.conf");
2437
2722
  const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
2438
2723
  if (safeReadText(manifestPath) !== expectedManifest) {
2439
2724
  changedFiles.push(manifestPath);
@@ -2453,9 +2738,9 @@ var RULES = [
2453
2738
  id: "sot.agent-symlinks",
2454
2739
  title: "AGENTS/CLAUDE/GEMINI symlink contract",
2455
2740
  audit: (ctx) => {
2456
- const agentsPath = join10(ctx.repoRoot, "AGENTS.md");
2457
- if (!existsSync7(agentsPath)) {
2458
- const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync7(join10(ctx.repoRoot, file)));
2741
+ const agentsPath = join11(ctx.repoRoot, "AGENTS.md");
2742
+ if (!existsSync8(agentsPath)) {
2743
+ const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync8(join11(ctx.repoRoot, file)));
2459
2744
  if (fallbackSources.length === 0) {
2460
2745
  return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
2461
2746
  }
@@ -2470,7 +2755,7 @@ var RULES = [
2470
2755
  }
2471
2756
  const details = [];
2472
2757
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
2473
- const full = join10(ctx.repoRoot, file);
2758
+ const full = join11(ctx.repoRoot, file);
2474
2759
  const target = readSymlinkTarget(full);
2475
2760
  if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
2476
2761
  }
@@ -2494,7 +2779,7 @@ var RULES = [
2494
2779
  return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
2495
2780
  }
2496
2781
  for (const file of ["CLAUDE.md", "GEMINI.md"]) {
2497
- const full = join10(ctx.repoRoot, file);
2782
+ const full = join11(ctx.repoRoot, file);
2498
2783
  const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
2499
2784
  if (result.blocked) blockedDetails.push(result.blocked);
2500
2785
  if (result.changed) changedFiles.push(full);
@@ -2516,7 +2801,7 @@ var RULES = [
2516
2801
  migrate: (ctx, finding) => {
2517
2802
  const changedFiles = [];
2518
2803
  const details = [];
2519
- const path = join10(ctx.repoRoot, ".project.json");
2804
+ const path = join11(ctx.repoRoot, ".project.json");
2520
2805
  const existing = readProjectJson(ctx) ?? {};
2521
2806
  const canonical = canonicalProjectJson(ctx);
2522
2807
  const merged = { ...existing, ...canonical };
@@ -2526,14 +2811,14 @@ var RULES = [
2526
2811
  changedFiles.push(path);
2527
2812
  if (!ctx.dryRun) writeText(path, expected);
2528
2813
  }
2529
- const planeJson = join10(ctx.repoRoot, ".plane.json");
2530
- if (existsSync7(planeJson)) {
2814
+ const planeJson = join11(ctx.repoRoot, ".plane.json");
2815
+ if (existsSync8(planeJson)) {
2531
2816
  const backup = `${planeJson}.migrated-backup`;
2532
- if (existsSync7(backup)) {
2817
+ if (existsSync8(backup)) {
2533
2818
  details.push(`cannot back up .plane.json because ${relative(ctx.repoRoot, backup)} already exists`);
2534
2819
  } else {
2535
2820
  changedFiles.push(backup);
2536
- if (!ctx.dryRun) renameSync(planeJson, backup);
2821
+ if (!ctx.dryRun) renameSync2(planeJson, backup);
2537
2822
  }
2538
2823
  }
2539
2824
  return {
@@ -2551,8 +2836,8 @@ var RULES = [
2551
2836
  title: ".env.op + gitignore secrets contract",
2552
2837
  audit: (ctx) => {
2553
2838
  const details = [];
2554
- const envOp = safeReadText(join10(ctx.repoRoot, ".env.op"));
2555
- const gitignore = safeReadText(join10(ctx.repoRoot, ".gitignore"));
2839
+ const envOp = safeReadText(join11(ctx.repoRoot, ".env.op"));
2840
+ const gitignore = safeReadText(join11(ctx.repoRoot, ".gitignore"));
2556
2841
  if (!envOp) {
2557
2842
  details.push(".env.op missing");
2558
2843
  } else {
@@ -2578,12 +2863,12 @@ var RULES = [
2578
2863
  migrate: (ctx, finding) => {
2579
2864
  const changedFiles = [];
2580
2865
  const details = [];
2581
- const envOpPath = join10(ctx.repoRoot, ".env.op");
2582
- if (!existsSync7(envOpPath)) {
2866
+ const envOpPath = join11(ctx.repoRoot, ".env.op");
2867
+ if (!existsSync8(envOpPath)) {
2583
2868
  changedFiles.push(envOpPath);
2584
- if (!ctx.dryRun) writeText(envOpPath, readText(join10(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
2869
+ if (!ctx.dryRun) writeText(envOpPath, readText(join11(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
2585
2870
  }
2586
- const gitignorePath = join10(ctx.repoRoot, ".gitignore");
2871
+ const gitignorePath = join11(ctx.repoRoot, ".gitignore");
2587
2872
  const gitignore = safeReadText(gitignorePath) ?? "";
2588
2873
  const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
2589
2874
  # NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
@@ -2610,20 +2895,20 @@ var RULES = [
2610
2895
  title: ".copier-answers.yml provenance + drift report",
2611
2896
  audit: (ctx) => {
2612
2897
  const details = [];
2613
- const path = join10(ctx.repoRoot, ".copier-answers.yml");
2614
- const text3 = safeReadText(path);
2898
+ const path = join11(ctx.repoRoot, ".copier-answers.yml");
2899
+ const text2 = safeReadText(path);
2615
2900
  const project = readProjectJson(ctx);
2616
- if (!text3) {
2901
+ if (!text2) {
2617
2902
  details.push(".copier-answers.yml missing");
2618
2903
  } else {
2619
- if (!text3.startsWith("# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY")) details.push("missing Copier overwrite warning header");
2620
- if (!text3.includes("_src_path:")) details.push("_src_path missing");
2904
+ if (!text2.startsWith("# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY")) details.push("missing Copier overwrite warning header");
2905
+ if (!text2.includes("_src_path:")) details.push("_src_path missing");
2621
2906
  if (project?.project_name) {
2622
- const nameMatch = text3.match(/project_name:\s*(.+)/);
2907
+ const nameMatch = text2.match(/project_name:\s*(.+)/);
2623
2908
  if (!nameMatch || nameMatch[1]?.trim() !== String(project.project_name)) details.push("project_name drift between .copier-answers.yml and .project.json");
2624
2909
  }
2625
2910
  if (project?.project_description) {
2626
- const descMatch = text3.match(/project_description:\s*([\s\S]*?)(?=\n\w|$)/);
2911
+ const descMatch = text2.match(/project_description:\s*([\s\S]*?)(?=\n\w|$)/);
2627
2912
  const yamlDesc = descMatch?.[1]?.replace(/\n\s+/g, " ").trim() ?? "";
2628
2913
  if (yamlDesc !== String(project.project_description)) details.push("project_description drift between .copier-answers.yml and .project.json");
2629
2914
  }
@@ -2640,16 +2925,16 @@ var RULES = [
2640
2925
  migrate: (ctx, finding) => {
2641
2926
  const changedFiles = [];
2642
2927
  const project = canonicalProjectJson(ctx);
2643
- const text3 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
2644
- _src_path: ${join10(ctx.pjanglerRoot, "templates", "commonproject")}
2928
+ const text2 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
2929
+ _src_path: ${join11(ctx.pjanglerRoot, "templates", "commonproject")}
2645
2930
  project_description: ${String(project.project_description)}
2646
2931
  project_name: ${String(project.project_name)}
2647
2932
  ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2648
2933
  `;
2649
- const path = join10(ctx.repoRoot, ".copier-answers.yml");
2650
- if (safeReadText(path) !== text3) {
2934
+ const path = join11(ctx.repoRoot, ".copier-answers.yml");
2935
+ if (safeReadText(path) !== text2) {
2651
2936
  changedFiles.push(path);
2652
- if (!ctx.dryRun) writeText(path, text3);
2937
+ if (!ctx.dryRun) writeText(path, text2);
2653
2938
  }
2654
2939
  return {
2655
2940
  id: finding.id,
@@ -2665,15 +2950,15 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2665
2950
  id: "bmad.scaffold",
2666
2951
  title: "BMAD modules/docs scaffold",
2667
2952
  audit: (ctx) => {
2668
- const sourceRoot = join10(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
2669
- const targetRoot = join10(ctx.repoRoot, "_bmad");
2953
+ const sourceRoot = join11(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
2954
+ const targetRoot = join11(ctx.repoRoot, "_bmad");
2670
2955
  const sentinels = [
2671
- join10("core", "config.yaml"),
2672
- join10("custom", "config.yaml"),
2673
- join10("custom", "workflows", "ticket-lifecycle", "workflow.yaml"),
2674
- join10("bmm", "workflows", "workflow-status", "workflow.yaml")
2956
+ join11("core", "config.yaml"),
2957
+ join11("custom", "config.yaml"),
2958
+ join11("custom", "workflows", "ticket-lifecycle", "workflow.yaml"),
2959
+ join11("bmm", "workflows", "workflow-status", "workflow.yaml")
2675
2960
  ];
2676
- const missing = sentinels.filter((file) => existsSync7(join10(sourceRoot, file)) && !existsSync7(join10(targetRoot, file)));
2961
+ const missing = sentinels.filter((file) => existsSync8(join11(sourceRoot, file)) && !existsSync8(join11(targetRoot, file)));
2677
2962
  return {
2678
2963
  id: "bmad.scaffold",
2679
2964
  title: "BMAD modules/docs scaffold",
@@ -2685,7 +2970,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2685
2970
  },
2686
2971
  migrate: (ctx, finding) => {
2687
2972
  const changedFiles = [];
2688
- copyMissingRecursive(join10(ctx.pjanglerRoot, "templates", "commonproject", "_bmad"), join10(ctx.repoRoot, "_bmad"), changedFiles, ctx.dryRun);
2973
+ copyMissingRecursive(join11(ctx.pjanglerRoot, "templates", "commonproject", "_bmad"), join11(ctx.repoRoot, "_bmad"), changedFiles, ctx.dryRun);
2689
2974
  return {
2690
2975
  id: finding.id,
2691
2976
  title: finding.title,
@@ -2707,11 +2992,11 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2707
2992
  }
2708
2993
  const details = [];
2709
2994
  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"]) {
2710
- if (!existsSync7(join10(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join10(role.roleDir, rel))}`);
2995
+ if (!existsSync8(join11(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join11(role.roleDir, rel))}`);
2711
2996
  }
2712
- const gitmodules = safeReadText(join10(ctx.repoRoot, ".gitmodules")) ?? "";
2997
+ const gitmodules = safeReadText(join11(ctx.repoRoot, ".gitmodules")) ?? "";
2713
2998
  if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
2714
- if (!profileMetaInheritsDefault(join10(role.roleDir, "runtime", "profile.yaml"))) {
2999
+ if (!profileMetaInheritsDefault(join11(role.roleDir, "runtime", "profile.yaml"))) {
2715
3000
  details.push("runtime/profile.yaml missing inherited default config metadata");
2716
3001
  }
2717
3002
  const registry = safeReadText(registryPath(ctx.homeDir));
@@ -2732,21 +3017,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2732
3017
  if (!role) {
2733
3018
  return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
2734
3019
  }
2735
- const templateRoleDir = join10(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
2736
- writeIfDifferent(join10(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
2737
- writeIfDifferent(join10(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
2738
- writeIfDifferent(join10(role.roleDir, ".gitignore"), readText(join10(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
2739
- copyMissingRecursive(join10(templateRoleDir, ".runtime-scaffold"), join10(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
2740
- copyMissingRecursive(join10(templateRoleDir, ".runtime-scaffold"), join10(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
2741
- copyMissingRecursive(join10(templateRoleDir, ".scripts"), join10(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
2742
- const promptSource = join10(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
2743
- const promptTarget = join10(role.roleDir, ".scripts", "sentinel.prompt.md");
2744
- if (existsSync7(promptSource) && !existsSync7(promptTarget)) {
3020
+ const templateRoleDir = join11(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
3021
+ writeIfDifferent(join11(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
3022
+ writeIfDifferent(join11(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
3023
+ writeIfDifferent(join11(role.roleDir, ".gitignore"), readText(join11(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
3024
+ copyMissingRecursive(join11(templateRoleDir, ".runtime-scaffold"), join11(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
3025
+ copyMissingRecursive(join11(templateRoleDir, ".runtime-scaffold"), join11(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
3026
+ copyMissingRecursive(join11(templateRoleDir, ".scripts"), join11(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
3027
+ const promptSource = join11(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
3028
+ const promptTarget = join11(role.roleDir, ".scripts", "sentinel.prompt.md");
3029
+ if (existsSync8(promptSource) && !existsSync8(promptTarget)) {
2745
3030
  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);
2746
3031
  writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
2747
3032
  }
2748
3033
  upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
2749
- const profileMetaUpdated = upsertInheritedProfileMeta(join10(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
3034
+ const profileMetaUpdated = upsertInheritedProfileMeta(join11(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
2750
3035
  if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
2751
3036
  const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
2752
3037
  if (registryUpdated) details.push(`updated ${registryUpdated}`);
@@ -2800,9 +3085,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2800
3085
  return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
2801
3086
  }
2802
3087
  for (const role of roles) {
2803
- const sysDir = join10(ctx.homeDir, ".config", "systemd", "user");
3088
+ const sysDir = join11(ctx.homeDir, ".config", "systemd", "user");
2804
3089
  const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`];
2805
- const allUnitsPresent = units.every((unit) => existsSync7(join10(sysDir, unit)));
3090
+ const allUnitsPresent = units.every((unit) => existsSync8(join11(sysDir, unit)));
2806
3091
  if (allUnitsPresent) {
2807
3092
  if (ctx.dryRun) {
2808
3093
  details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
@@ -2814,12 +3099,12 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
2814
3099
  }
2815
3100
  continue;
2816
3101
  }
2817
- for (const script of [join10(role.roleDir, ".scripts", "70-systemd.sh")]) {
2818
- if (!script || !existsSync7(script)) continue;
3102
+ for (const script of [join11(role.roleDir, ".scripts", "70-systemd.sh")]) {
3103
+ if (!script || !existsSync8(script)) continue;
2819
3104
  if (ctx.dryRun) {
2820
3105
  details.push(`would run: bash ${script}`);
2821
3106
  } else {
2822
- const result = spawnSync4("bash", [script], { cwd: role.roleDir, encoding: "utf8" });
3107
+ const result = spawnSync5("bash", [script], { cwd: role.roleDir, encoding: "utf8" });
2823
3108
  if (result.status !== 0) details.push(`script failed: ${script}: ${result.stderr.trim() || result.stdout.trim()}`);
2824
3109
  }
2825
3110
  }
@@ -2849,12 +3134,12 @@ function getParityRuleIds() {
2849
3134
  return RULES.map((rule) => rule.id);
2850
3135
  }
2851
3136
  function runAudit(repoArg) {
2852
- const pjanglerRoot = resolvePjanglerRoot();
3137
+ const pjanglerRoot = resolvePjanglerRoot2();
2853
3138
  const ctx = {
2854
- repoRoot: resolve(repoArg ?? process.cwd()),
3139
+ repoRoot: resolve2(repoArg ?? process.cwd()),
2855
3140
  dryRun: true,
2856
3141
  pjanglerRoot,
2857
- homeDir: homedir4()
3142
+ homeDir: homedir5()
2858
3143
  };
2859
3144
  const rules = RULES.map((rule) => rule.audit(ctx));
2860
3145
  return {
@@ -2865,12 +3150,12 @@ function runAudit(repoArg) {
2865
3150
  };
2866
3151
  }
2867
3152
  function runMigrationForRules(ruleIds, repoArg, dryRun) {
2868
- const pjanglerRoot = resolvePjanglerRoot();
3153
+ const pjanglerRoot = resolvePjanglerRoot2();
2869
3154
  const ctx = {
2870
- repoRoot: resolve(repoArg ?? process.cwd()),
3155
+ repoRoot: resolve2(repoArg ?? process.cwd()),
2871
3156
  dryRun,
2872
3157
  pjanglerRoot,
2873
- homeDir: homedir4()
3158
+ homeDir: homedir5()
2874
3159
  };
2875
3160
  const selected = RULES.filter((rule) => ruleIds.includes(rule.id));
2876
3161
  if (!selected.length) {
@@ -2931,362 +3216,6 @@ function formatAuditReport(report) {
2931
3216
  return lines.join("\n");
2932
3217
  }
2933
3218
 
2934
- // src/project/index.ts
2935
- import { spawnSync as spawnSync5 } from "node:child_process";
2936
- import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync5, renameSync as renameSync2, statSync, writeFileSync as writeFileSync5 } from "node:fs";
2937
- import { homedir as homedir5 } from "node:os";
2938
- import { basename as basename3, dirname as dirname7, join as join11, resolve as resolve2 } from "node:path";
2939
- import YAML from "yaml";
2940
- var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
2941
- var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
2942
- var KNOWN_SKILL_ROOTS = [
2943
- "/home/delorenj/code/skillex/all-skills",
2944
- "/home/delorenj/code/CoachingAgentFramework/.agents/skills",
2945
- "/home/delorenj/code/pjangler/.agents/skills",
2946
- join11(homedir5(), ".codex", "skills")
2947
- ];
2948
- function projectRegistryPath(env2 = process.env) {
2949
- return expandHome(env2[PROJECT_REGISTRY_ENV] || join11(homedir5(), ".config", "pjangler", "projects.yaml"));
2950
- }
2951
- function emptyProjectRegistry() {
2952
- return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
2953
- }
2954
- function loadProjectRegistry(path = projectRegistryPath()) {
2955
- if (!existsSync8(path)) return emptyProjectRegistry();
2956
- const raw = YAML.parse(readFileSync5(path, "utf8"));
2957
- if (raw == null) return emptyProjectRegistry();
2958
- if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
2959
- const registry = raw;
2960
- const normalized = {
2961
- schema_version: Number(registry.schema_version ?? PROJECT_REGISTRY_SCHEMA_VERSION),
2962
- projects: isRecord(registry.projects) ? registry.projects : {}
2963
- };
2964
- validateProjectRegistry(normalized);
2965
- return normalized;
2966
- }
2967
- function saveProjectRegistry(registry, path = projectRegistryPath()) {
2968
- validateProjectRegistry(registry);
2969
- mkdirSync6(dirname7(path), { recursive: true });
2970
- const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
2971
- writeFileSync5(temp, YAML.stringify(registry, { lineWidth: 0 }), "utf8");
2972
- renameSync2(temp, path);
2973
- }
2974
- function validateProjectRegistry(registry) {
2975
- if (registry.schema_version !== PROJECT_REGISTRY_SCHEMA_VERSION) {
2976
- throw new Error(`Unsupported project registry schema_version: ${registry.schema_version}`);
2977
- }
2978
- if (!isRecord(registry.projects)) throw new Error("Project registry projects must be a mapping");
2979
- const slugs = /* @__PURE__ */ new Set();
2980
- const repoPaths = /* @__PURE__ */ new Map();
2981
- const identifiers = /* @__PURE__ */ new Map();
2982
- for (const [slug, project] of Object.entries(registry.projects)) {
2983
- validateProjectRecord(project, slug);
2984
- if (slugs.has(project.slug)) throw new Error(`Duplicate project slug: ${project.slug}`);
2985
- slugs.add(project.slug);
2986
- const repoKey = resolve2(project.repo_path);
2987
- const existingRepoSlug = repoPaths.get(repoKey);
2988
- if (existingRepoSlug && existingRepoSlug !== slug) {
2989
- throw new Error(`Duplicate project repo_path: ${project.repo_path} used by ${existingRepoSlug} and ${slug}`);
2990
- }
2991
- repoPaths.set(repoKey, slug);
2992
- const identifier = project.ticket_provider.identifier?.toUpperCase();
2993
- if (identifier) {
2994
- const existingIdentifierSlug = identifiers.get(identifier);
2995
- if (existingIdentifierSlug && existingIdentifierSlug !== slug) {
2996
- throw new Error(`Duplicate project identifier: ${identifier} used by ${existingIdentifierSlug} and ${slug}`);
2997
- }
2998
- identifiers.set(identifier, slug);
2999
- }
3000
- }
3001
- }
3002
- function slugifyProjectName(value) {
3003
- return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
3004
- }
3005
- function deriveProjectIdentifier(value) {
3006
- const compact = value.replace(/[^A-Za-z0-9]/g, "").toUpperCase();
3007
- const identifier = compact.slice(0, 4) || "PROJ";
3008
- return identifier.length >= 2 ? identifier : `${identifier}XX`.slice(0, 4);
3009
- }
3010
- function normalizeAgentRole(value) {
3011
- return value?.trim() || "pm";
3012
- }
3013
- function jsonStable(value) {
3014
- return JSON.stringify(value);
3015
- }
3016
- function projectRecordEquivalent(a, b) {
3017
- if (!a) return false;
3018
- const { created_at: _aCreated, updated_at: _aUpdated, ...aComparable } = a;
3019
- const { created_at: _bCreated, updated_at: _bUpdated, ...bComparable } = b;
3020
- return jsonStable(aComparable) === jsonStable(bComparable);
3021
- }
3022
- function defaultProjectTargetDir(name, cwd = process.cwd()) {
3023
- const compactName = name.replace(/[^A-Za-z0-9._-]/g, "") || slugifyProjectName(name);
3024
- return resolve2(dirname7(resolve2(cwd)), compactName);
3025
- }
3026
- function resolveSourceSkillPath(sourceSkill) {
3027
- if (!sourceSkill) return void 0;
3028
- const expanded = expandHome(sourceSkill);
3029
- const direct = resolve2(expanded);
3030
- if (existsSync8(direct)) return direct;
3031
- const name = basename3(sourceSkill);
3032
- for (const root of KNOWN_SKILL_ROOTS) {
3033
- const candidate = join11(root, name);
3034
- if (existsSync8(candidate)) return candidate;
3035
- }
3036
- const civilWarLetterifier = "/home/delorenj/code/skillex/all-skills/civilwar-letterifier";
3037
- const hint = existsSync8(civilWarLetterifier) ? ` Did you mean ${civilWarLetterifier}?` : "";
3038
- throw new Error(`Source skill not found: ${sourceSkill}.${hint}`);
3039
- }
3040
- function planProjectInit(input) {
3041
- if (!input.name.trim()) throw new Error("Project name is required");
3042
- const registryPath2 = resolve2(projectRegistryPath({ ...process.env, [PROJECT_REGISTRY_ENV]: input.registryPath || process.env[PROJECT_REGISTRY_ENV] }));
3043
- const registry = loadProjectRegistry(registryPath2);
3044
- const now = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
3045
- const slug = input.projectSlug ?? slugifyProjectName(input.name);
3046
- const targetDir = resolve2(input.targetDir ?? defaultProjectTargetDir(input.name, input.cwd));
3047
- const identifier = (input.projectIdentifier ?? deriveProjectIdentifier(input.name)).toUpperCase();
3048
- const existing = registry.projects[slug];
3049
- const sourceSkillPath = resolveSourceSkillPath(input.sourceSkill);
3050
- const overwrite = input.overwrite ?? input.force ?? false;
3051
- const agentRole = normalizeAgentRole(input.agentRole);
3052
- const agents = input.provisionAgent ? {
3053
- ...existing?.agents ?? {},
3054
- [agentRole]: {
3055
- role: agentRole,
3056
- provisioning_state: "planned"
3057
- }
3058
- } : existing?.agents ?? {};
3059
- const scaffold = input.scaffold ?? true;
3060
- const candidateProject = {
3061
- name: input.name,
3062
- slug,
3063
- repo_path: targetDir,
3064
- description: input.description ?? "",
3065
- status: "planned",
3066
- source_artifacts: sourceSkillPath ? [{ kind: "skill", path: sourceSkillPath, package_name: input.packageName ?? slug }] : [],
3067
- template: {
3068
- commonproject: {
3069
- enabled: true,
3070
- primary_language: input.primaryLanguage ?? "python"
3071
- }
3072
- },
3073
- ticket_provider: {
3074
- type: input.ticketProvider ?? "plane",
3075
- workspace: input.planeWorkspace ?? "33god",
3076
- identifier,
3077
- board_id: input.planeProjectId ?? "",
3078
- board_url: input.planeProjectId ? `https://plane.delo.sh/${input.planeWorkspace ?? "33god"}/projects/${input.planeProjectId}/issues/` : "",
3079
- state: input.live ? "planned" : "planned"
3080
- },
3081
- agents,
3082
- created_at: existing?.created_at ?? now,
3083
- updated_at: now
3084
- };
3085
- const project = {
3086
- ...candidateProject,
3087
- updated_at: projectRecordEquivalent(existing, candidateProject) ? existing.updated_at : now
3088
- };
3089
- validateNoDuplicateProject(registry, project, overwrite);
3090
- const pjanglerRoot = resolve2(input.pjanglerRoot ?? resolvePjanglerRoot2());
3091
- const manifest = projectManifestFromRegistryProject(project);
3092
- const apply = input.apply ?? false;
3093
- const live = input.live ?? false;
3094
- const actions = [
3095
- { kind: "registry.upsert", registryPath: registryPath2, slug, project }
3096
- ];
3097
- if (scaffold) {
3098
- actions.push(buildCommonProjectCopierAction({
3099
- pjanglerRoot,
3100
- targetDir,
3101
- projectName: project.name,
3102
- projectDescription: project.description,
3103
- projectSlug: project.slug,
3104
- ticketProvider: project.ticket_provider.type,
3105
- planeWorkspace: project.ticket_provider.workspace ?? "33god",
3106
- planeProjectId: project.ticket_provider.board_id ?? "",
3107
- projectIdentifier: identifier,
3108
- primaryLanguage: project.template.commonproject.primary_language,
3109
- overwrite
3110
- }));
3111
- }
3112
- actions.push(
3113
- { kind: "project.write-manifest", path: join11(targetDir, ".project.json"), manifest },
3114
- {
3115
- kind: "plane.create-or-link",
3116
- enabled: live,
3117
- live,
3118
- workspace: project.ticket_provider.workspace ?? "33god",
3119
- identifier,
3120
- state: live ? "planned" : "planned",
3121
- reason: live ? void 0 : "network/cloud actions require --live"
3122
- },
3123
- {
3124
- kind: "hermes.provision-agent",
3125
- enabled: input.provisionAgent ?? false,
3126
- local: !live,
3127
- targetDir,
3128
- targetRepo: slug,
3129
- role: agentRole,
3130
- context: {
3131
- skipRuntimeRepo: !live,
3132
- skipPlane: !live,
3133
- skipBloodbank: !live,
3134
- skipSystemd: !live || process.platform === "darwin"
3135
- }
3136
- }
3137
- );
3138
- return { ok: true, apply, dryRun: !apply, live, registryPath: registryPath2, project, manifest, actions };
3139
- }
3140
- function executeProjectInitPlan(plan) {
3141
- const logs = [];
3142
- const errors = [];
3143
- const changedFiles = [];
3144
- if (!plan.apply) return { ok: true, plan, logs, errors, changedFiles };
3145
- const registry = loadProjectRegistry(plan.registryPath);
3146
- let pendingRegistryAction;
3147
- for (const action of plan.actions) {
3148
- if (action.kind === "copier.copy.commonproject") {
3149
- mkdirSync6(dirname7(action.targetDir), { recursive: true });
3150
- const result = spawnSync5(action.command[0], action.command.slice(1), { encoding: "utf8", cwd: action.cwd });
3151
- if (result.stdout?.trim()) logs.push(result.stdout.trim());
3152
- if (result.stderr?.trim()) logs.push(result.stderr.trim());
3153
- if (result.error) {
3154
- const code = result.error.code;
3155
- errors.push(
3156
- code === "ENOENT" ? "copier not found on PATH. Install with: uv tool install copier or pip install copier" : `copier failed: ${result.error.message}`
3157
- );
3158
- break;
3159
- }
3160
- if (result.status !== 0) {
3161
- errors.push(`copier exited with status ${result.status ?? "unknown"}`);
3162
- if (existsSync8(action.targetDir)) changedFiles.push(action.targetDir);
3163
- break;
3164
- }
3165
- changedFiles.push(action.targetDir);
3166
- } else if (action.kind === "project.write-manifest") {
3167
- mkdirSync6(dirname7(action.path), { recursive: true });
3168
- const next = `${JSON.stringify(action.manifest, null, 2)}
3169
- `;
3170
- const current = existsSync8(action.path) ? readFileSync5(action.path, "utf8") : void 0;
3171
- if (current !== next) {
3172
- writeFileSync5(action.path, next, "utf8");
3173
- changedFiles.push(action.path);
3174
- }
3175
- } else if (action.kind === "registry.upsert") {
3176
- pendingRegistryAction = action;
3177
- } else if (action.kind === "plane.create-or-link") {
3178
- logs.push(action.enabled ? "plane.create-or-link requires a live provider integration" : "plane.create-or-link skipped (requires --live)");
3179
- } else if (action.kind === "hermes.provision-agent") {
3180
- logs.push(action.enabled ? "hermes.provision-agent planned for the caller to execute" : "hermes.provision-agent skipped");
3181
- }
3182
- }
3183
- if (pendingRegistryAction && errors.length === 0) {
3184
- if (!projectRecordEquivalent(registry.projects[pendingRegistryAction.slug], pendingRegistryAction.project)) {
3185
- registry.projects[pendingRegistryAction.slug] = pendingRegistryAction.project;
3186
- saveProjectRegistry(registry, pendingRegistryAction.registryPath);
3187
- changedFiles.push(pendingRegistryAction.registryPath);
3188
- }
3189
- }
3190
- return { ok: errors.length === 0, plan, logs, errors, changedFiles };
3191
- }
3192
- function projectManifestFromRegistryProject(project) {
3193
- const agents = Object.fromEntries(
3194
- Object.entries(project.agents).map(([name, agent]) => [
3195
- `${project.slug}-${name}`,
3196
- {
3197
- role: agent.role,
3198
- role_dir: agent.role_dir,
3199
- provisioning_state: agent.provisioning_state
3200
- }
3201
- ])
3202
- );
3203
- return {
3204
- project_name: project.name,
3205
- project_description: project.description,
3206
- project_slug: project.slug,
3207
- repo_path: project.repo_path,
3208
- ticket_provider: {
3209
- type: project.ticket_provider.type,
3210
- workspace: project.ticket_provider.workspace ?? "",
3211
- identifier: project.ticket_provider.identifier ?? "",
3212
- board_id: project.ticket_provider.board_id ?? "",
3213
- board_url: project.ticket_provider.board_url ?? "",
3214
- state: project.ticket_provider.state
3215
- },
3216
- agents
3217
- };
3218
- }
3219
- function getProject(registry, slug) {
3220
- const project = registry.projects[slug];
3221
- if (!project) throw new Error(`Project not found in registry: ${slug}`);
3222
- return project;
3223
- }
3224
- function buildCommonProjectCopierAction(input) {
3225
- const templateDir = join11(input.pjanglerRoot, "templates", "commonproject");
3226
- const data = {
3227
- project_name: input.projectName,
3228
- project_description: input.projectDescription ?? "",
3229
- project_slug: input.projectSlug,
3230
- ticket_provider: input.ticketProvider,
3231
- plane_workspace: input.planeWorkspace,
3232
- plane_project_id: input.planeProjectId ?? "",
3233
- project_identifier: input.projectIdentifier,
3234
- primary_language: input.primaryLanguage
3235
- };
3236
- const command = ["copier", "copy", "--trust", templateDir, input.targetDir, "--defaults"];
3237
- for (const [key, value] of Object.entries(data)) command.push("--data", `${key}=${value}`);
3238
- if (input.overwrite) command.push("--overwrite");
3239
- return {
3240
- kind: "copier.copy.commonproject",
3241
- cwd: input.pjanglerRoot,
3242
- command,
3243
- targetDir: input.targetDir,
3244
- data,
3245
- overwrite: input.overwrite
3246
- };
3247
- }
3248
- function resolvePjanglerRoot2() {
3249
- let dir = dirname7(new URL(import.meta.url).pathname);
3250
- while (dir !== dirname7(dir)) {
3251
- if (existsSync8(join11(dir, "package.json")) && existsSync8(join11(dir, "templates", "commonproject", "copier.yml"))) return dir;
3252
- dir = dirname7(dir);
3253
- }
3254
- return resolve2(process.cwd());
3255
- }
3256
- function validateNoDuplicateProject(registry, project, overwrite) {
3257
- const existingSameSlug = registry.projects[project.slug];
3258
- if (existingSameSlug && !overwrite && resolve2(existingSameSlug.repo_path) !== resolve2(project.repo_path)) {
3259
- throw new Error(`Project slug already exists in registry: ${project.slug}`);
3260
- }
3261
- for (const [slug, existing] of Object.entries(registry.projects)) {
3262
- if (slug === project.slug) continue;
3263
- if (resolve2(existing.repo_path) === resolve2(project.repo_path)) {
3264
- throw new Error(`Project repo_path already registered by ${slug}: ${project.repo_path}`);
3265
- }
3266
- if (existing.ticket_provider.identifier && existing.ticket_provider.identifier.toUpperCase() === project.ticket_provider.identifier?.toUpperCase()) {
3267
- throw new Error(`Project identifier already registered by ${slug}: ${project.ticket_provider.identifier}`);
3268
- }
3269
- }
3270
- }
3271
- function validateProjectRecord(project, key) {
3272
- if (!isRecord(project)) throw new Error(`Project ${key} must be a mapping`);
3273
- if (!project.name) throw new Error(`Project ${key} missing name`);
3274
- if (!project.slug) throw new Error(`Project ${key} missing slug`);
3275
- if (project.slug !== key) throw new Error(`Project key ${key} does not match slug ${project.slug}`);
3276
- if (!project.repo_path) throw new Error(`Project ${key} missing repo_path`);
3277
- if (!Array.isArray(project.source_artifacts)) throw new Error(`Project ${key} source_artifacts must be a list`);
3278
- if (!isRecord(project.ticket_provider)) throw new Error(`Project ${key} ticket_provider must be a mapping`);
3279
- if (!isRecord(project.agents)) throw new Error(`Project ${key} agents must be a mapping`);
3280
- }
3281
- function expandHome(path) {
3282
- if (path === "~") return homedir5();
3283
- if (path.startsWith("~/")) return join11(homedir5(), path.slice(2));
3284
- return path;
3285
- }
3286
- function isRecord(value) {
3287
- return typeof value === "object" && value !== null && !Array.isArray(value);
3288
- }
3289
-
3290
3219
  // src/mcp-server.ts
3291
3220
  var server = new McpServer({
3292
3221
  name: "pjangler-mcp",