@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.
- package/dist/index.js +1950 -2018
- package/dist/mcp-server.js +668 -739
- package/package.json +1 -1
- package/templates/commonproject/copier.yml +34 -2
- package/templates/commonproject/template/.agents/hooks/sync.py +12 -1
- package/templates/commonproject/template/.agents/local.example.json +3 -1
- package/templates/commonproject/template/mise.toml.jinja +8 -4
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +1 -0
- package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +7 -0
- package/templates/hermes-agent/template/SOUL.md.jinja +7 -2
package/dist/mcp-server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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 (@${
|
|
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: "
|
|
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}
|
|
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
|
|
1152
|
+
if (skipTelegram) {
|
|
1244
1153
|
lines.push("");
|
|
1245
|
-
lines.push("
|
|
1246
|
-
|
|
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
|
|
1283
|
-
import { join as
|
|
1284
|
-
import { existsSync as
|
|
1190
|
+
import { homedir as homedir4 } from "node:os";
|
|
1191
|
+
import { join as join9, dirname as dirname5 } from "node:path";
|
|
1192
|
+
import { existsSync as existsSync7, cpSync, mkdirSync as mkdirSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1285
1193
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
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
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
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
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
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
|
-
|
|
1306
|
-
|
|
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
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
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
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
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
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
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
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
1591
|
-
import { dirname as
|
|
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 =
|
|
1880
|
+
let dir = dirname6(fileURLToPath3(import.meta.url));
|
|
1596
1881
|
for (let i = 0; i < 4; i++) {
|
|
1597
1882
|
try {
|
|
1598
|
-
const raw =
|
|
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 =
|
|
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
|
|
1613
|
-
import { basename as
|
|
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
|
|
1616
|
-
import { spawnSync as
|
|
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
|
|
1685
|
-
let dir =
|
|
1686
|
-
while (dir !==
|
|
1687
|
-
if (
|
|
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 =
|
|
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(
|
|
1983
|
+
return normalizeNewlines(readFileSync5(path, "utf8"));
|
|
1699
1984
|
}
|
|
1700
1985
|
function safeReadText(path) {
|
|
1701
|
-
return
|
|
1986
|
+
return existsSync8(path) ? readText(path) : null;
|
|
1702
1987
|
}
|
|
1703
1988
|
function ensureParent(path) {
|
|
1704
|
-
|
|
1989
|
+
mkdirSync6(dirname7(path), { recursive: true });
|
|
1705
1990
|
}
|
|
1706
1991
|
function writeText(path, content) {
|
|
1707
1992
|
ensureParent(path);
|
|
1708
|
-
|
|
1993
|
+
writeFileSync5(path, content);
|
|
1709
1994
|
}
|
|
1710
|
-
function tryParseJson(
|
|
1711
|
-
if (!
|
|
1995
|
+
function tryParseJson(text2) {
|
|
1996
|
+
if (!text2) return null;
|
|
1712
1997
|
try {
|
|
1713
|
-
return JSON.parse(
|
|
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 (!
|
|
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 (
|
|
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 =
|
|
1751
|
-
if (
|
|
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 =
|
|
1754
|
-
if (!
|
|
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)
|
|
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 =
|
|
1764
|
-
if (
|
|
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(
|
|
2057
|
+
function yamlGet(text2, keyPath) {
|
|
1773
2058
|
const parts = keyPath.split(".");
|
|
1774
|
-
const lines =
|
|
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 =
|
|
1804
|
-
if (!
|
|
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 =
|
|
1807
|
-
const roleYamlPath =
|
|
1808
|
-
if (!
|
|
1809
|
-
const
|
|
1810
|
-
const runtimeRepoRaw = yamlGet(
|
|
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(
|
|
2097
|
+
role: yamlGet(text2, "role") || entry.name,
|
|
1813
2098
|
roleDir,
|
|
1814
2099
|
roleYamlPath,
|
|
1815
|
-
repo: yamlGet(
|
|
1816
|
-
agentId: yamlGet(
|
|
1817
|
-
profileName: yamlGet(
|
|
1818
|
-
displayName: yamlGet(
|
|
1819
|
-
purpose: yamlGet(
|
|
1820
|
-
botHandle: yamlGet(
|
|
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(
|
|
1823
|
-
planeWorkspace: yamlGet(
|
|
1824
|
-
ticketProviderName: yamlGet(
|
|
1825
|
-
ticketProviderBoardId: yamlGet(
|
|
1826
|
-
ticketProviderBoardUrl: yamlGet(
|
|
1827
|
-
ticketProviderIdentifier: yamlGet(
|
|
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
|
|
2117
|
+
return join11(homeDir, ".hermes", "agents-registry.yaml");
|
|
1833
2118
|
}
|
|
1834
2119
|
function systemctlUser(args) {
|
|
1835
|
-
const result =
|
|
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 =
|
|
1844
|
-
return
|
|
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 ??
|
|
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 =
|
|
1859
|
-
if (
|
|
1860
|
-
const sourcePath =
|
|
1861
|
-
if (!
|
|
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 =
|
|
1870
|
-
return
|
|
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(
|
|
1873
|
-
if (startMarker.test(
|
|
1874
|
-
return
|
|
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 =
|
|
2162
|
+
const match = text2.match(beforePattern);
|
|
1878
2163
|
if (match && typeof match.index === "number") {
|
|
1879
|
-
return `${
|
|
2164
|
+
return `${text2.slice(0, match.index).replace(/\s*$/, "\n\n")}${block}
|
|
1880
2165
|
|
|
1881
|
-
${
|
|
2166
|
+
${text2.slice(match.index)}`;
|
|
1882
2167
|
}
|
|
1883
2168
|
}
|
|
1884
|
-
return `${
|
|
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 (
|
|
2179
|
+
if (existsSync8(join11(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
|
|
1895
2180
|
}
|
|
1896
2181
|
return required;
|
|
1897
2182
|
}
|
|
1898
|
-
function upsertMisePath(
|
|
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 =
|
|
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
|
-
${
|
|
2190
|
+
${text2.replace(/^\s+/, "")}`;
|
|
1906
2191
|
}
|
|
1907
|
-
const prefix =
|
|
2192
|
+
const prefix = text2.slice(0, envMatch.index + envMatch[1].length);
|
|
1908
2193
|
const section = envMatch[2];
|
|
1909
|
-
const suffix =
|
|
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
|
|
2205
|
+
if (pathLine[0] === nextLine) return text2;
|
|
1921
2206
|
return `${prefix}${section.replace(pathLine[0], nextLine)}${suffix}`;
|
|
1922
2207
|
}
|
|
1923
|
-
function removeTomlSection(
|
|
1924
|
-
const lines =
|
|
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
|
|
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(
|
|
1959
|
-
const versioningIndex =
|
|
2243
|
+
function insertTomlBlockBeforeVersioning(text2, block) {
|
|
2244
|
+
const versioningIndex = text2.indexOf("# >>> mise-versioning >>>");
|
|
1960
2245
|
if (versioningIndex >= 0) {
|
|
1961
|
-
return `${
|
|
2246
|
+
return `${text2.slice(0, versioningIndex).replace(/\s*$/, "\n\n")}${block}
|
|
1962
2247
|
|
|
1963
|
-
${
|
|
2248
|
+
${text2.slice(versioningIndex)}`;
|
|
1964
2249
|
}
|
|
1965
|
-
return `${
|
|
2250
|
+
return `${text2.replace(/\s*$/, "")}
|
|
1966
2251
|
|
|
1967
2252
|
${block}
|
|
1968
2253
|
`;
|
|
1969
2254
|
}
|
|
1970
|
-
function extractTomlStrings(
|
|
2255
|
+
function extractTomlStrings(text2) {
|
|
1971
2256
|
const values = [];
|
|
1972
2257
|
const stringPattern = /"((?:\\.|[^"\\])*)"|'([^']*)'/g;
|
|
1973
|
-
for (const match of
|
|
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(
|
|
1998
|
-
const lines =
|
|
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(
|
|
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(
|
|
2035
|
-
const withPath = upsertMisePath(
|
|
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(
|
|
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(
|
|
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 =
|
|
2088
|
-
const planeJsonPath =
|
|
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 (!
|
|
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 (
|
|
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 (!
|
|
2200
|
-
|
|
2484
|
+
if (!existsSync8(sourceDir)) return;
|
|
2485
|
+
mkdirSync6(targetDir, { recursive: true });
|
|
2201
2486
|
for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
|
|
2202
|
-
const sourcePath =
|
|
2487
|
+
const sourcePath = join11(sourceDir, entry.name);
|
|
2203
2488
|
if (skip?.(sourcePath)) continue;
|
|
2204
|
-
const targetPath =
|
|
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 (
|
|
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 =
|
|
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 ?
|
|
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
|
|
2547
|
+
const text2 = safeReadText(path);
|
|
2263
2548
|
return Boolean(
|
|
2264
|
-
|
|
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 =
|
|
2319
|
-
if (!
|
|
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
|
|
2607
|
+
const text2 = readText(misePath);
|
|
2323
2608
|
const details = [];
|
|
2324
|
-
const linkAgentfilesPath =
|
|
2325
|
-
if (!
|
|
2326
|
-
const pathValues = [...(
|
|
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 (!
|
|
2330
|
-
if (!
|
|
2331
|
-
if (!
|
|
2332
|
-
if (!
|
|
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 =
|
|
2628
|
+
const path = join11(ctx.repoRoot, "mise.toml");
|
|
2344
2629
|
const changedFiles = [];
|
|
2345
2630
|
const details = [];
|
|
2346
|
-
if (!
|
|
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
|
|
2356
|
-
const next = upsertLinkAgentfilesBlock(
|
|
2357
|
-
if (next !==
|
|
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
|
-
|
|
2645
|
+
text2 = next;
|
|
2361
2646
|
}
|
|
2362
|
-
const linkAgentfilesPath =
|
|
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 =
|
|
2390
|
-
const versioningPath =
|
|
2391
|
-
const manifestPath =
|
|
2392
|
-
const
|
|
2393
|
-
if (!
|
|
2394
|
-
if (!
|
|
2395
|
-
if (!
|
|
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 =
|
|
2409
|
-
if (!
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2457
|
-
if (!
|
|
2458
|
-
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2530
|
-
if (
|
|
2814
|
+
const planeJson = join11(ctx.repoRoot, ".plane.json");
|
|
2815
|
+
if (existsSync8(planeJson)) {
|
|
2531
2816
|
const backup = `${planeJson}.migrated-backup`;
|
|
2532
|
-
if (
|
|
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)
|
|
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(
|
|
2555
|
-
const gitignore = safeReadText(
|
|
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 =
|
|
2582
|
-
if (!
|
|
2866
|
+
const envOpPath = join11(ctx.repoRoot, ".env.op");
|
|
2867
|
+
if (!existsSync8(envOpPath)) {
|
|
2583
2868
|
changedFiles.push(envOpPath);
|
|
2584
|
-
if (!ctx.dryRun) writeText(envOpPath, readText(
|
|
2869
|
+
if (!ctx.dryRun) writeText(envOpPath, readText(join11(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
|
|
2585
2870
|
}
|
|
2586
|
-
const gitignorePath =
|
|
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 =
|
|
2614
|
-
const
|
|
2898
|
+
const path = join11(ctx.repoRoot, ".copier-answers.yml");
|
|
2899
|
+
const text2 = safeReadText(path);
|
|
2615
2900
|
const project = readProjectJson(ctx);
|
|
2616
|
-
if (!
|
|
2901
|
+
if (!text2) {
|
|
2617
2902
|
details.push(".copier-answers.yml missing");
|
|
2618
2903
|
} else {
|
|
2619
|
-
if (!
|
|
2620
|
-
if (!
|
|
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 =
|
|
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 =
|
|
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
|
|
2644
|
-
_src_path: ${
|
|
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 =
|
|
2650
|
-
if (safeReadText(path) !==
|
|
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,
|
|
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 =
|
|
2669
|
-
const targetRoot =
|
|
2953
|
+
const sourceRoot = join11(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
|
|
2954
|
+
const targetRoot = join11(ctx.repoRoot, "_bmad");
|
|
2670
2955
|
const sentinels = [
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
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) =>
|
|
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(
|
|
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 (!
|
|
2995
|
+
if (!existsSync8(join11(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join11(role.roleDir, rel))}`);
|
|
2711
2996
|
}
|
|
2712
|
-
const gitmodules = safeReadText(
|
|
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(
|
|
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 =
|
|
2736
|
-
writeIfDifferent(
|
|
2737
|
-
writeIfDifferent(
|
|
2738
|
-
writeIfDifferent(
|
|
2739
|
-
copyMissingRecursive(
|
|
2740
|
-
copyMissingRecursive(
|
|
2741
|
-
copyMissingRecursive(
|
|
2742
|
-
const promptSource =
|
|
2743
|
-
const promptTarget =
|
|
2744
|
-
if (
|
|
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(
|
|
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 =
|
|
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) =>
|
|
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 [
|
|
2818
|
-
if (!script || !
|
|
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 =
|
|
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 =
|
|
3137
|
+
const pjanglerRoot = resolvePjanglerRoot2();
|
|
2853
3138
|
const ctx = {
|
|
2854
|
-
repoRoot:
|
|
3139
|
+
repoRoot: resolve2(repoArg ?? process.cwd()),
|
|
2855
3140
|
dryRun: true,
|
|
2856
3141
|
pjanglerRoot,
|
|
2857
|
-
homeDir:
|
|
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 =
|
|
3153
|
+
const pjanglerRoot = resolvePjanglerRoot2();
|
|
2869
3154
|
const ctx = {
|
|
2870
|
-
repoRoot:
|
|
3155
|
+
repoRoot: resolve2(repoArg ?? process.cwd()),
|
|
2871
3156
|
dryRun,
|
|
2872
3157
|
pjanglerRoot,
|
|
2873
|
-
homeDir:
|
|
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",
|