@delorenj/pjangler 1.2.9 → 1.2.12
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 +126 -28
- package/dist/mcp-server.js +124 -26
- package/package.json +1 -1
- package/templates/commonproject/copier.yml +4 -1
- package/templates/commonproject/template/.project.json.jinja +9 -12
- package/templates/hermes-agent/README.md +1 -1
- package/templates/hermes-agent/config.example.toml +66 -0
- package/templates/hermes-agent/docs/architecture.md +5 -0
- package/templates/hermes-agent/docs/bloodbank-gateway.md +57 -0
- package/templates/hermes-agent/docs/fleet-control-plane/README.md +1 -1
- package/templates/hermes-agent/docs/fleet-control-plane/n8n-service-hub.md +60 -0
- package/templates/hermes-agent/docs/operations.md +1 -1
- package/templates/hermes-agent/docs/sentinel/README.md +2 -2
- package/templates/hermes-agent/docs/sentinel/development.md +3 -3
- package/templates/hermes-agent/docs/sentinel/providers.md +6 -5
- package/templates/hermes-agent/install-local.sh +10 -4
- package/templates/hermes-agent/runtime-scaffold/bloodbank-consumer.py +5 -3
- package/templates/hermes-agent/scripts/fleet-sync.sh +64 -1
- package/templates/hermes-agent/template/.scripts/01-config.sh +1 -0
- package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +9 -0
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +22 -3
- package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +22 -0
- package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +18 -33
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +8 -1
- package/templates/hermes-agent/template/.scripts/_lib.sh +61 -3
- package/templates/hermes-agent/template/.scripts/config.example.toml +5 -0
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +66 -22
- package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +3 -3
- package/templates/hermes-agent/template/.scripts/providers/plane.sh +29 -9
- package/templates/hermes-agent/template/role.yaml.jinja +2 -27
package/dist/index.js
CHANGED
|
@@ -82,6 +82,8 @@ function renderHostConfig() {
|
|
|
82
82
|
const hermesRepo = join2(home, "code", "hermes-agent");
|
|
83
83
|
const scaffoldDir = join2(home, "code", "hermes-agent-template", "runtime-scaffold");
|
|
84
84
|
const skillsDir = join2(home, ".agents", "skills");
|
|
85
|
+
const pmExternalSkillGlobalDir = join2(home, "code", "skillex", "skill-sets", "global", ".system");
|
|
86
|
+
const pmExternalSkillBmadDir = join2(home, "code", "skillex", "packs", "bmad", "6.10.2");
|
|
85
87
|
return `# hermes-agent-template \u2014 host configuration
|
|
86
88
|
# Bootstrapped by \`pjangler config bootstrap\` for $HOME=${home} (platform=${platform()}).
|
|
87
89
|
#
|
|
@@ -97,9 +99,14 @@ home = "~/.hermes"
|
|
|
97
99
|
hermes_bin = "${hermesBin}"
|
|
98
100
|
hermes_repo = "${hermesRepo}"
|
|
99
101
|
runtime_scaffold_dir = "${scaffoldDir}"
|
|
102
|
+
# Shared fleet source-of-truth env file + fleet registry. ~ is expanded.
|
|
100
103
|
fleet_env = "~/.hermes/fleet.env"
|
|
101
104
|
registry_file = "~/.hermes/agents-registry.yaml"
|
|
102
105
|
canonical_skills_dir = "${skillsDir}"
|
|
106
|
+
pm_external_skill_dirs = [
|
|
107
|
+
"${pmExternalSkillGlobalDir}",
|
|
108
|
+
"${pmExternalSkillBmadDir}",
|
|
109
|
+
]
|
|
103
110
|
symlinked_runtime_skills = []
|
|
104
111
|
|
|
105
112
|
[github]
|
|
@@ -1210,14 +1217,14 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
|
1210
1217
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
1211
1218
|
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync2, renameSync, statSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1212
1219
|
import { homedir as homedir3 } from "node:os";
|
|
1213
|
-
import { basename as basename2, dirname as dirname4, join as join8, resolve } from "node:path";
|
|
1220
|
+
import { basename as basename2, delimiter, dirname as dirname4, join as join8, resolve } from "node:path";
|
|
1214
1221
|
import YAML from "yaml";
|
|
1215
1222
|
var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
|
|
1223
|
+
var PROJECT_SOURCE_SKILL_ROOTS_ENV = "PJ_SOURCE_SKILL_ROOTS";
|
|
1216
1224
|
var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
|
|
1217
|
-
var
|
|
1225
|
+
var DEFAULT_SOURCE_SKILL_ROOTS = [
|
|
1218
1226
|
"/home/delorenj/code/skillex/all-skills",
|
|
1219
|
-
"
|
|
1220
|
-
"/home/delorenj/code/pjangler/.agents/skills",
|
|
1227
|
+
join8(homedir3(), ".agents", "skills"),
|
|
1221
1228
|
join8(homedir3(), ".codex", "skills")
|
|
1222
1229
|
];
|
|
1223
1230
|
function projectRegistryPath(env2 = process.env) {
|
|
@@ -1288,8 +1295,7 @@ function buildTicketProviderBlock(input) {
|
|
|
1288
1295
|
workspace: input.workspace ?? "",
|
|
1289
1296
|
identifier: input.identifier,
|
|
1290
1297
|
board_id: boardId,
|
|
1291
|
-
|
|
1292
|
-
state: "planned"
|
|
1298
|
+
state: boardId ? "linked" : "planned"
|
|
1293
1299
|
};
|
|
1294
1300
|
}
|
|
1295
1301
|
const workspace = input.workspace ?? "33god";
|
|
@@ -1298,8 +1304,16 @@ function buildTicketProviderBlock(input) {
|
|
|
1298
1304
|
workspace,
|
|
1299
1305
|
identifier: input.identifier,
|
|
1300
1306
|
board_id: boardId,
|
|
1301
|
-
|
|
1302
|
-
|
|
1307
|
+
state: boardId ? "linked" : "planned"
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
function defaultProjectAutomation() {
|
|
1311
|
+
return {
|
|
1312
|
+
reconcile: {
|
|
1313
|
+
enabled: false,
|
|
1314
|
+
grace_hours: 0,
|
|
1315
|
+
auto_review: true
|
|
1316
|
+
}
|
|
1303
1317
|
};
|
|
1304
1318
|
}
|
|
1305
1319
|
function slugifyProjectName(value) {
|
|
@@ -1333,18 +1347,31 @@ function defaultProjectTargetDir(name, cwd = process.cwd()) {
|
|
|
1333
1347
|
const compactName = name.replace(/[^A-Za-z0-9._-]/g, "") || slugifyProjectName(name);
|
|
1334
1348
|
return resolve(dirname4(resolve(cwd)), compactName);
|
|
1335
1349
|
}
|
|
1336
|
-
function
|
|
1350
|
+
function sourceSkillRoots(env2 = process.env) {
|
|
1351
|
+
const configuredRoots = (env2[PROJECT_SOURCE_SKILL_ROOTS_ENV] || "").split(delimiter).map((root) => root.trim()).filter(Boolean);
|
|
1352
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1353
|
+
const roots = [];
|
|
1354
|
+
for (const root of [...DEFAULT_SOURCE_SKILL_ROOTS, ...configuredRoots]) {
|
|
1355
|
+
const normalized = resolve(expandHome(root));
|
|
1356
|
+
if (seen.has(normalized)) continue;
|
|
1357
|
+
seen.add(normalized);
|
|
1358
|
+
roots.push(normalized);
|
|
1359
|
+
}
|
|
1360
|
+
return roots;
|
|
1361
|
+
}
|
|
1362
|
+
function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
|
|
1337
1363
|
if (!sourceSkill) return void 0;
|
|
1338
1364
|
const expanded = expandHome(sourceSkill);
|
|
1339
1365
|
const direct = resolve(expanded);
|
|
1340
1366
|
if (existsSync6(direct)) return direct;
|
|
1341
1367
|
const name = basename2(sourceSkill);
|
|
1342
|
-
|
|
1368
|
+
const roots = sourceSkillRoots(env2);
|
|
1369
|
+
for (const root of roots) {
|
|
1343
1370
|
const candidate = join8(root, name);
|
|
1344
1371
|
if (existsSync6(candidate)) return candidate;
|
|
1345
1372
|
}
|
|
1346
|
-
const
|
|
1347
|
-
const hint =
|
|
1373
|
+
const searched = roots.length ? ` Searched roots: ${roots.join(", ")}.` : "";
|
|
1374
|
+
const hint = `${searched} Add project-specific roots with ${PROJECT_SOURCE_SKILL_ROOTS_ENV}.`;
|
|
1348
1375
|
throw new Error(`Source skill not found: ${sourceSkill}.${hint}`);
|
|
1349
1376
|
}
|
|
1350
1377
|
function planProjectInit(input) {
|
|
@@ -1384,10 +1411,10 @@ function planProjectInit(input) {
|
|
|
1384
1411
|
type: input.ticketProvider ?? "plane",
|
|
1385
1412
|
identifier,
|
|
1386
1413
|
boardId: input.boardId ?? input.planeProjectId,
|
|
1387
|
-
boardUrl: input.boardUrl,
|
|
1388
1414
|
workspace: input.boardWorkspace ?? input.planeWorkspace
|
|
1389
1415
|
}),
|
|
1390
1416
|
agents,
|
|
1417
|
+
automation: existing?.automation ?? defaultProjectAutomation(),
|
|
1391
1418
|
created_at: existing?.created_at ?? now,
|
|
1392
1419
|
updated_at: now
|
|
1393
1420
|
};
|
|
@@ -1415,7 +1442,6 @@ function planProjectInit(input) {
|
|
|
1415
1442
|
planeProjectId: project.ticket_provider.board_id ?? "",
|
|
1416
1443
|
ticketWorkspace: project.ticket_provider.workspace ?? "",
|
|
1417
1444
|
boardId: project.ticket_provider.board_id ?? "",
|
|
1418
|
-
boardUrl: project.ticket_provider.board_url ?? "",
|
|
1419
1445
|
projectIdentifier: identifier,
|
|
1420
1446
|
primaryLanguage: project.template.commonproject.primary_language,
|
|
1421
1447
|
agentHooksLayer: resolveAgentHooksLayer(input.agentHooksLayer),
|
|
@@ -1527,10 +1553,10 @@ function projectManifestFromRegistryProject(project) {
|
|
|
1527
1553
|
workspace: project.ticket_provider.workspace ?? "",
|
|
1528
1554
|
identifier: project.ticket_provider.identifier ?? "",
|
|
1529
1555
|
board_id: project.ticket_provider.board_id ?? "",
|
|
1530
|
-
board_url: project.ticket_provider.board_url ?? "",
|
|
1531
1556
|
state: project.ticket_provider.state
|
|
1532
1557
|
},
|
|
1533
|
-
agents
|
|
1558
|
+
agents,
|
|
1559
|
+
automation: project.automation ?? defaultProjectAutomation()
|
|
1534
1560
|
};
|
|
1535
1561
|
}
|
|
1536
1562
|
function formatProjectInitPlan(plan) {
|
|
@@ -1611,7 +1637,6 @@ function buildCommonProjectCopierAction(input) {
|
|
|
1611
1637
|
plane_project_id: input.planeProjectId ?? "",
|
|
1612
1638
|
ticket_workspace: input.ticketWorkspace ?? input.planeWorkspace,
|
|
1613
1639
|
board_id: input.boardId ?? input.planeProjectId ?? "",
|
|
1614
|
-
board_url: input.boardUrl ?? "",
|
|
1615
1640
|
project_identifier: input.projectIdentifier,
|
|
1616
1641
|
primary_language: input.primaryLanguage,
|
|
1617
1642
|
agent_hooks_layer: input.agentHooksLayer ?? true ? "true" : "false"
|
|
@@ -2215,8 +2240,12 @@ function discoverRoles(repoRoot) {
|
|
|
2215
2240
|
planeWorkspace: yamlGet(text3, "ticket_provider.workspace") || yamlGet(text3, "plane.workspace"),
|
|
2216
2241
|
ticketProviderName: yamlGet(text3, "ticket_provider.name"),
|
|
2217
2242
|
ticketProviderBoardId: yamlGet(text3, "ticket_provider.board_id"),
|
|
2218
|
-
|
|
2219
|
-
|
|
2243
|
+
ticketProviderIdentifier: yamlGet(text3, "plane.identifier"),
|
|
2244
|
+
legacyReconcileEnabled: yamlGet(text3, "reconcile.enabled"),
|
|
2245
|
+
legacyReconcileGraceHours: yamlGet(text3, "reconcile.grace_hours"),
|
|
2246
|
+
legacyReconcileAutoReview: yamlGet(text3, "reconcile.auto_review"),
|
|
2247
|
+
legacyScrumGraceHours: yamlGet(text3, "scrum_master.grace_hours"),
|
|
2248
|
+
legacyScrumAutoReview: yamlGet(text3, "scrum_master.auto_review")
|
|
2220
2249
|
};
|
|
2221
2250
|
}).filter((value) => Boolean(value));
|
|
2222
2251
|
}
|
|
@@ -2241,10 +2270,39 @@ function templateVersioningScript(ctx) {
|
|
|
2241
2270
|
function templateLinkAgentfilesScript(ctx) {
|
|
2242
2271
|
return templateScript(ctx, "link-agentfiles.sh");
|
|
2243
2272
|
}
|
|
2273
|
+
function resolveAgentHooksLayer2(ctx) {
|
|
2274
|
+
const override = process.env.PJ_AGENT_HOOKS_LAYER;
|
|
2275
|
+
if (override === "0" || override === "false") return false;
|
|
2276
|
+
if (override === "1" || override === "true") return true;
|
|
2277
|
+
if (existsSync8(join10(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
|
|
2278
|
+
return !existsSync8(join10(ctx.homeDir, ".agents", "hooks"));
|
|
2279
|
+
}
|
|
2280
|
+
function evaluateMiseConditionals(template, agentHooksLayer) {
|
|
2281
|
+
const out = [];
|
|
2282
|
+
let depth = 0;
|
|
2283
|
+
let skipDepth = 0;
|
|
2284
|
+
for (const line of template.split("\n")) {
|
|
2285
|
+
const stmt = line.trim();
|
|
2286
|
+
const ifMatch = /^\{%-?\s*if\s+(\w+)\s*-?%\}$/.exec(stmt);
|
|
2287
|
+
if (ifMatch) {
|
|
2288
|
+
depth += 1;
|
|
2289
|
+
const truthy = ifMatch[1] === "agent_hooks_layer" ? agentHooksLayer : false;
|
|
2290
|
+
if (skipDepth === 0 && !truthy) skipDepth = depth;
|
|
2291
|
+
continue;
|
|
2292
|
+
}
|
|
2293
|
+
if (/^\{%-?\s*endif\s*-?%\}$/.test(stmt)) {
|
|
2294
|
+
if (skipDepth === depth) skipDepth = 0;
|
|
2295
|
+
depth = Math.max(0, depth - 1);
|
|
2296
|
+
continue;
|
|
2297
|
+
}
|
|
2298
|
+
if (skipDepth === 0) out.push(line);
|
|
2299
|
+
}
|
|
2300
|
+
return out.join("\n");
|
|
2301
|
+
}
|
|
2244
2302
|
function renderGeneratedProjectMiseToml(ctx, template) {
|
|
2245
2303
|
const project = readProjectJson(ctx);
|
|
2246
2304
|
const projectName = String(project?.project_name ?? basename3(ctx.repoRoot) ?? "project");
|
|
2247
|
-
return template.replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
|
|
2305
|
+
return evaluateMiseConditionals(template, resolveAgentHooksLayer2(ctx)).replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
|
|
2248
2306
|
}
|
|
2249
2307
|
function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
2250
2308
|
const targetPath = join10(ctx.repoRoot, "mise.toml");
|
|
@@ -2434,6 +2492,23 @@ function upsertLinkAgentfilesBlock(text3, ctx) {
|
|
|
2434
2492
|
function readProjectJson(ctx) {
|
|
2435
2493
|
return tryParseJson(safeReadText(join10(ctx.repoRoot, ".project.json")));
|
|
2436
2494
|
}
|
|
2495
|
+
function boolSetting(value, fallback) {
|
|
2496
|
+
if (typeof value === "boolean") return value;
|
|
2497
|
+
if (typeof value === "string") {
|
|
2498
|
+
const normalized = value.trim().toLowerCase();
|
|
2499
|
+
if (["true", "1", "yes", "on"].includes(normalized)) return true;
|
|
2500
|
+
if (["false", "0", "no", "off"].includes(normalized)) return false;
|
|
2501
|
+
}
|
|
2502
|
+
return fallback;
|
|
2503
|
+
}
|
|
2504
|
+
function numberSetting(value, fallback) {
|
|
2505
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
2506
|
+
if (typeof value === "string" && value.trim()) {
|
|
2507
|
+
const parsed = Number(value);
|
|
2508
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
2509
|
+
}
|
|
2510
|
+
return fallback;
|
|
2511
|
+
}
|
|
2437
2512
|
function canonicalProjectJson(ctx) {
|
|
2438
2513
|
const roles = discoverRoles(ctx.repoRoot);
|
|
2439
2514
|
const existing = readProjectJson(ctx) ?? {};
|
|
@@ -2444,9 +2519,9 @@ function canonicalProjectJson(ctx) {
|
|
|
2444
2519
|
workspace: String((existing.ticket_provider?.workspace ?? firstRole?.planeWorkspace ?? "") || ""),
|
|
2445
2520
|
identifier: String((existing.ticket_provider?.identifier ?? firstRole?.ticketProviderIdentifier ?? "") || ""),
|
|
2446
2521
|
board_id: String((existing.ticket_provider?.board_id ?? firstRole?.ticketProviderBoardId ?? "") || ""),
|
|
2447
|
-
|
|
2448
|
-
state: String((existing.ticket_provider?.state ?? "planned") || "planned")
|
|
2522
|
+
state: String((existing.ticket_provider?.state ?? (firstRole?.ticketProviderBoardId ? "linked" : "planned")) || "planned")
|
|
2449
2523
|
};
|
|
2524
|
+
if (ticketProvider.board_id && ticketProvider.state === "planned") ticketProvider.state = "linked";
|
|
2450
2525
|
const existingAgents = existing.agents ?? {};
|
|
2451
2526
|
const discoveredAgents = Object.fromEntries(
|
|
2452
2527
|
roles.map((role) => [
|
|
@@ -2466,13 +2541,27 @@ function canonicalProjectJson(ctx) {
|
|
|
2466
2541
|
provisioning_state: existingAgent.provisioning_state
|
|
2467
2542
|
};
|
|
2468
2543
|
}
|
|
2544
|
+
const existingAutomation = existing.automation ?? {};
|
|
2545
|
+
const existingReconcile = existingAutomation.reconcile ?? {};
|
|
2546
|
+
const legacyEnabled = roles.find((role) => role.legacyReconcileEnabled)?.legacyReconcileEnabled;
|
|
2547
|
+
const legacyGrace = roles.find((role) => role.legacyReconcileGraceHours || role.legacyScrumGraceHours);
|
|
2548
|
+
const legacyAutoReview = roles.find((role) => role.legacyReconcileAutoReview || role.legacyScrumAutoReview);
|
|
2549
|
+
const automation = {
|
|
2550
|
+
...existingAutomation,
|
|
2551
|
+
reconcile: {
|
|
2552
|
+
enabled: boolSetting(existingReconcile.enabled, boolSetting(legacyEnabled, false)),
|
|
2553
|
+
grace_hours: numberSetting(existingReconcile.grace_hours, numberSetting(legacyGrace?.legacyReconcileGraceHours || legacyGrace?.legacyScrumGraceHours, 0)),
|
|
2554
|
+
auto_review: boolSetting(existingReconcile.auto_review, boolSetting(legacyAutoReview?.legacyReconcileAutoReview || legacyAutoReview?.legacyScrumAutoReview, true))
|
|
2555
|
+
}
|
|
2556
|
+
};
|
|
2469
2557
|
return {
|
|
2470
2558
|
project_name: String(existing.project_name ?? titleCaseSlug(slug)),
|
|
2471
2559
|
project_description: String(existing.project_description ?? ""),
|
|
2472
2560
|
project_slug: slug,
|
|
2473
2561
|
repo_path: ctx.repoRoot,
|
|
2474
2562
|
ticket_provider: ticketProvider,
|
|
2475
|
-
agents
|
|
2563
|
+
agents,
|
|
2564
|
+
automation
|
|
2476
2565
|
};
|
|
2477
2566
|
}
|
|
2478
2567
|
function projectJsonFinding(ctx) {
|
|
@@ -2487,7 +2576,7 @@ function projectJsonFinding(ctx) {
|
|
|
2487
2576
|
if (!data) {
|
|
2488
2577
|
return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json is not valid JSON", details: [], fixable: true };
|
|
2489
2578
|
}
|
|
2490
|
-
for (const key of ["project_name", "project_description", "project_slug", "repo_path", "ticket_provider", "agents"]) {
|
|
2579
|
+
for (const key of ["project_name", "project_description", "project_slug", "repo_path", "ticket_provider", "agents", "automation"]) {
|
|
2491
2580
|
if (!(key in data)) details.push(`missing key: ${key}`);
|
|
2492
2581
|
}
|
|
2493
2582
|
if (data.repo_path !== ctx.repoRoot) details.push(`repo_path should be ${ctx.repoRoot}`);
|
|
@@ -2504,9 +2593,18 @@ function projectJsonFinding(ctx) {
|
|
|
2504
2593
|
}
|
|
2505
2594
|
}
|
|
2506
2595
|
const ticketProvider = data.ticket_provider ?? {};
|
|
2507
|
-
for (const key of ["type", "workspace", "identifier", "board_id", "
|
|
2596
|
+
for (const key of ["type", "workspace", "identifier", "board_id", "state"]) {
|
|
2508
2597
|
if (!(key in ticketProvider)) details.push(`ticket_provider.${key} missing`);
|
|
2509
2598
|
}
|
|
2599
|
+
if ("board_url" in ticketProvider) details.push("ticket_provider.board_url should be removed; derive it from provider/workspace/board_id");
|
|
2600
|
+
if (!ticketProvider.board_id && roles.some((role) => role.ticketProviderBoardId)) {
|
|
2601
|
+
details.push("ticket_provider.board_id missing even though legacy role.yaml contains a board binding");
|
|
2602
|
+
}
|
|
2603
|
+
const automation = data.automation ?? {};
|
|
2604
|
+
const reconcile = automation.reconcile ?? {};
|
|
2605
|
+
for (const key of ["enabled", "grace_hours", "auto_review"]) {
|
|
2606
|
+
if (!(key in reconcile)) details.push(`automation.reconcile.${key} missing`);
|
|
2607
|
+
}
|
|
2510
2608
|
if (existsSync8(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
|
|
2511
2609
|
return {
|
|
2512
2610
|
id: "sot.project-json",
|
|
@@ -2520,7 +2618,7 @@ function projectJsonFinding(ctx) {
|
|
|
2520
2618
|
function renderSoul(role) {
|
|
2521
2619
|
const telegram = role.botHandle ? `@${role.botHandle}` : "(unwired)";
|
|
2522
2620
|
const tone = role.role === "pm" ? `Direct and brief. Decision-forward. No throat-clearing, no apologies, no "I'll help you with that" preambles.` : "Direct and brief.";
|
|
2523
|
-
const roleSpecific = role.role === "pm" ? `You are the project manager. You triage incoming work, create or refine tickets, and delegate implementation. You do not ship product code. A systemd heartbeat checkpoints your runtime; when this repo opts into reconciliation (\`reconcile.enabled\` in
|
|
2621
|
+
const roleSpecific = role.role === "pm" ? `You are the project manager. You triage incoming work, create or refine tickets, and delegate implementation. You do not ship product code. A systemd heartbeat checkpoints your runtime; when this repo opts into reconciliation (\`automation.reconcile.enabled\` in repo-root \`.project.json\`), the same heartbeat also runs your continuous board-reconciliation pass out-of-band (\`.scripts/sentinel.prompt.md\`, \`--source cron\`), kept separate from your interactive session memory.` : `You operate as the ${role.role} agent for this repo.`;
|
|
2524
2622
|
const runtimeOwner = role.runtimeOwner || "delorenj";
|
|
2525
2623
|
return `# ${role.displayName || role.agentId}
|
|
2526
2624
|
|
|
@@ -3574,7 +3672,7 @@ async function runRecipeSubsystem(name, options) {
|
|
|
3574
3672
|
}
|
|
3575
3673
|
var program = new Command3();
|
|
3576
3674
|
program.name("pjangler").description("Project subsystem bootstrapper CLI").version(PJANGLER_VERSION);
|
|
3577
|
-
program.command("init").argument("[name]", "Project name to bootstrap (omit inside an existing git repo)").description("Bootstrap a project: registry entry + CommonProject scaffold + .project.json").option("--description <text>", "Project description").option("--target-dir <path>", "Target repo path").option("--source-skill <path>", "Source skill/template provenance path").option("--primary-language <language>", "Primary language for CommonProject rendering", "python").option("--provision-agent", "Plan local Hermes PM agent provisioning").option("--agent-role <role>", "Hermes agent role to plan when --provision-agent is set", "pm").option("--apply", "Write the registry and render the repo scaffold").option("--dry-run", "Preview changes without writing files (default)").option("--live", "Allow live/network/cloud provisioning actions").option("--slug <slug>", "Project registry slug override").option("--identifier <identifier>", "Ticket identifier override").option("--ticket-provider <type>", "Ticket provider: plane | trello", "plane").option("--board-id <id>", "Board id (Plane project UUID or Trello board id)").option("--board-url <url>", "
|
|
3675
|
+
program.command("init").argument("[name]", "Project name to bootstrap (omit inside an existing git repo)").description("Bootstrap a project: registry entry + CommonProject scaffold + .project.json").option("--description <text>", "Project description").option("--target-dir <path>", "Target repo path").option("--source-skill <path>", "Source skill/template provenance path").option("--primary-language <language>", "Primary language for CommonProject rendering", "python").option("--provision-agent", "Plan local Hermes PM agent provisioning").option("--agent-role <role>", "Hermes agent role to plan when --provision-agent is set", "pm").option("--apply", "Write the registry and render the repo scaffold").option("--dry-run", "Preview changes without writing files (default)").option("--live", "Allow live/network/cloud provisioning actions").option("--slug <slug>", "Project registry slug override").option("--identifier <identifier>", "Ticket identifier override").option("--ticket-provider <type>", "Ticket provider: plane | trello", "plane").option("--board-id <id>", "Board id (Plane project UUID or Trello board id)").option("--board-url <url>", "Deprecated no-op; board URLs are derived from provider + workspace + board-id").option("--workspace <name>", "Ticket workspace/org (Plane workspace; blank for Trello)").option("--registry <path>", `Registry path override (default: ${projectRegistryPath()})`).option("-f, --force", "Allow replacing an existing registry entry and re-rendering files").option("-y, --yes", "Apply every proposed operation without prompting").option("--no-tui", "Disable interactive prompts").option("--json", "Output machine-parseable JSON").action(async (name, options) => {
|
|
3578
3676
|
if (name && getRecipeNames().includes(name)) {
|
|
3579
3677
|
if (!options.json) {
|
|
3580
3678
|
console.error(`${yellow(glyph.warn)} ${dim(`"pjangler init ${name}" is deprecated \u2014 use "pjangler add ${name}". Forwarding\u2026`)}`);
|
|
@@ -3603,7 +3701,7 @@ program.command("list").description("List available subsystems").action(() => {
|
|
|
3603
3701
|
console.log("");
|
|
3604
3702
|
});
|
|
3605
3703
|
var projectCmd = program.command("project").description("Manage the pjangler project registry");
|
|
3606
|
-
projectCmd.command("init").argument("[name]", "Project display name").description("Plan or apply a registry-backed CommonProject initialization or legacy repo sync").option("--description <text>", "Project description").option("--target-dir <path>", "Target repo path").option("--source-skill <path>", "Source skill/template provenance path").option("--primary-language <language>", "Primary language for CommonProject rendering", "python").option("--provision-agent", "Plan local Hermes PM agent provisioning").option("--agent-role <role>", "Hermes agent role to plan when --provision-agent is set", "pm").option("--apply", "Write the registry and render the repo scaffold").option("--dry-run", "Preview changes without writing files (default)").option("--live", "Allow live/network/cloud provisioning actions").option("--slug <slug>", "Project registry slug override").option("--identifier <identifier>", "Ticket identifier override").option("--ticket-provider <type>", "Ticket provider: plane | trello", "plane").option("--board-id <id>", "Board id (Plane project UUID or Trello board id)").option("--board-url <url>", "
|
|
3704
|
+
projectCmd.command("init").argument("[name]", "Project display name").description("Plan or apply a registry-backed CommonProject initialization or legacy repo sync").option("--description <text>", "Project description").option("--target-dir <path>", "Target repo path").option("--source-skill <path>", "Source skill/template provenance path").option("--primary-language <language>", "Primary language for CommonProject rendering", "python").option("--provision-agent", "Plan local Hermes PM agent provisioning").option("--agent-role <role>", "Hermes agent role to plan when --provision-agent is set", "pm").option("--apply", "Write the registry and render the repo scaffold").option("--dry-run", "Preview changes without writing files (default)").option("--live", "Allow live/network/cloud provisioning actions").option("--slug <slug>", "Project registry slug override").option("--identifier <identifier>", "Ticket identifier override").option("--ticket-provider <type>", "Ticket provider: plane | trello", "plane").option("--board-id <id>", "Board id (Plane project UUID or Trello board id)").option("--board-url <url>", "Deprecated no-op; board URLs are derived from provider + workspace + board-id").option("--workspace <name>", "Ticket workspace/org (Plane workspace; blank for Trello)").option("--registry <path>", `Registry path override (default: ${projectRegistryPath()})`).option("-f, --force", "Allow replacing an existing registry entry and re-rendering files").option("-y, --yes", "Apply every proposed operation without prompting").option("--no-tui", "Disable interactive prompts").option("--json", "Output machine-parseable JSON").action((name, options) => {
|
|
3607
3705
|
if (!options.json) console.error(`${yellow(glyph.warn)} ${dim('"pjangler project init" is deprecated \u2014 use "pjangler init".')}`);
|
|
3608
3706
|
return runProjectInit(name, options);
|
|
3609
3707
|
});
|
package/dist/mcp-server.js
CHANGED
|
@@ -630,6 +630,8 @@ function renderHostConfig() {
|
|
|
630
630
|
const hermesRepo = join3(home, "code", "hermes-agent");
|
|
631
631
|
const scaffoldDir = join3(home, "code", "hermes-agent-template", "runtime-scaffold");
|
|
632
632
|
const skillsDir = join3(home, ".agents", "skills");
|
|
633
|
+
const pmExternalSkillGlobalDir = join3(home, "code", "skillex", "skill-sets", "global", ".system");
|
|
634
|
+
const pmExternalSkillBmadDir = join3(home, "code", "skillex", "packs", "bmad", "6.10.2");
|
|
633
635
|
return `# hermes-agent-template \u2014 host configuration
|
|
634
636
|
# Bootstrapped by \`pjangler config bootstrap\` for $HOME=${home} (platform=${platform()}).
|
|
635
637
|
#
|
|
@@ -645,9 +647,14 @@ home = "~/.hermes"
|
|
|
645
647
|
hermes_bin = "${hermesBin}"
|
|
646
648
|
hermes_repo = "${hermesRepo}"
|
|
647
649
|
runtime_scaffold_dir = "${scaffoldDir}"
|
|
650
|
+
# Shared fleet source-of-truth env file + fleet registry. ~ is expanded.
|
|
648
651
|
fleet_env = "~/.hermes/fleet.env"
|
|
649
652
|
registry_file = "~/.hermes/agents-registry.yaml"
|
|
650
653
|
canonical_skills_dir = "${skillsDir}"
|
|
654
|
+
pm_external_skill_dirs = [
|
|
655
|
+
"${pmExternalSkillGlobalDir}",
|
|
656
|
+
"${pmExternalSkillBmadDir}",
|
|
657
|
+
]
|
|
651
658
|
symlinked_runtime_skills = []
|
|
652
659
|
|
|
653
660
|
[github]
|
|
@@ -1196,14 +1203,14 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
|
1196
1203
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
1197
1204
|
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync2, renameSync, statSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1198
1205
|
import { homedir as homedir3 } from "node:os";
|
|
1199
|
-
import { basename as basename2, dirname as dirname4, join as join8, resolve } from "node:path";
|
|
1206
|
+
import { basename as basename2, delimiter, dirname as dirname4, join as join8, resolve } from "node:path";
|
|
1200
1207
|
import YAML from "yaml";
|
|
1201
1208
|
var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
|
|
1209
|
+
var PROJECT_SOURCE_SKILL_ROOTS_ENV = "PJ_SOURCE_SKILL_ROOTS";
|
|
1202
1210
|
var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
|
|
1203
|
-
var
|
|
1211
|
+
var DEFAULT_SOURCE_SKILL_ROOTS = [
|
|
1204
1212
|
"/home/delorenj/code/skillex/all-skills",
|
|
1205
|
-
"
|
|
1206
|
-
"/home/delorenj/code/pjangler/.agents/skills",
|
|
1213
|
+
join8(homedir3(), ".agents", "skills"),
|
|
1207
1214
|
join8(homedir3(), ".codex", "skills")
|
|
1208
1215
|
];
|
|
1209
1216
|
function projectRegistryPath(env2 = process.env) {
|
|
@@ -1274,8 +1281,7 @@ function buildTicketProviderBlock(input) {
|
|
|
1274
1281
|
workspace: input.workspace ?? "",
|
|
1275
1282
|
identifier: input.identifier,
|
|
1276
1283
|
board_id: boardId,
|
|
1277
|
-
|
|
1278
|
-
state: "planned"
|
|
1284
|
+
state: boardId ? "linked" : "planned"
|
|
1279
1285
|
};
|
|
1280
1286
|
}
|
|
1281
1287
|
const workspace = input.workspace ?? "33god";
|
|
@@ -1284,8 +1290,16 @@ function buildTicketProviderBlock(input) {
|
|
|
1284
1290
|
workspace,
|
|
1285
1291
|
identifier: input.identifier,
|
|
1286
1292
|
board_id: boardId,
|
|
1287
|
-
|
|
1288
|
-
|
|
1293
|
+
state: boardId ? "linked" : "planned"
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
function defaultProjectAutomation() {
|
|
1297
|
+
return {
|
|
1298
|
+
reconcile: {
|
|
1299
|
+
enabled: false,
|
|
1300
|
+
grace_hours: 0,
|
|
1301
|
+
auto_review: true
|
|
1302
|
+
}
|
|
1289
1303
|
};
|
|
1290
1304
|
}
|
|
1291
1305
|
function slugifyProjectName(value) {
|
|
@@ -1319,18 +1333,31 @@ function defaultProjectTargetDir(name, cwd = process.cwd()) {
|
|
|
1319
1333
|
const compactName = name.replace(/[^A-Za-z0-9._-]/g, "") || slugifyProjectName(name);
|
|
1320
1334
|
return resolve(dirname4(resolve(cwd)), compactName);
|
|
1321
1335
|
}
|
|
1322
|
-
function
|
|
1336
|
+
function sourceSkillRoots(env2 = process.env) {
|
|
1337
|
+
const configuredRoots = (env2[PROJECT_SOURCE_SKILL_ROOTS_ENV] || "").split(delimiter).map((root) => root.trim()).filter(Boolean);
|
|
1338
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1339
|
+
const roots = [];
|
|
1340
|
+
for (const root of [...DEFAULT_SOURCE_SKILL_ROOTS, ...configuredRoots]) {
|
|
1341
|
+
const normalized = resolve(expandHome(root));
|
|
1342
|
+
if (seen.has(normalized)) continue;
|
|
1343
|
+
seen.add(normalized);
|
|
1344
|
+
roots.push(normalized);
|
|
1345
|
+
}
|
|
1346
|
+
return roots;
|
|
1347
|
+
}
|
|
1348
|
+
function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
|
|
1323
1349
|
if (!sourceSkill) return void 0;
|
|
1324
1350
|
const expanded = expandHome(sourceSkill);
|
|
1325
1351
|
const direct = resolve(expanded);
|
|
1326
1352
|
if (existsSync6(direct)) return direct;
|
|
1327
1353
|
const name = basename2(sourceSkill);
|
|
1328
|
-
|
|
1354
|
+
const roots = sourceSkillRoots(env2);
|
|
1355
|
+
for (const root of roots) {
|
|
1329
1356
|
const candidate = join8(root, name);
|
|
1330
1357
|
if (existsSync6(candidate)) return candidate;
|
|
1331
1358
|
}
|
|
1332
|
-
const
|
|
1333
|
-
const hint =
|
|
1359
|
+
const searched = roots.length ? ` Searched roots: ${roots.join(", ")}.` : "";
|
|
1360
|
+
const hint = `${searched} Add project-specific roots with ${PROJECT_SOURCE_SKILL_ROOTS_ENV}.`;
|
|
1334
1361
|
throw new Error(`Source skill not found: ${sourceSkill}.${hint}`);
|
|
1335
1362
|
}
|
|
1336
1363
|
function planProjectInit(input) {
|
|
@@ -1370,10 +1397,10 @@ function planProjectInit(input) {
|
|
|
1370
1397
|
type: input.ticketProvider ?? "plane",
|
|
1371
1398
|
identifier,
|
|
1372
1399
|
boardId: input.boardId ?? input.planeProjectId,
|
|
1373
|
-
boardUrl: input.boardUrl,
|
|
1374
1400
|
workspace: input.boardWorkspace ?? input.planeWorkspace
|
|
1375
1401
|
}),
|
|
1376
1402
|
agents,
|
|
1403
|
+
automation: existing?.automation ?? defaultProjectAutomation(),
|
|
1377
1404
|
created_at: existing?.created_at ?? now,
|
|
1378
1405
|
updated_at: now
|
|
1379
1406
|
};
|
|
@@ -1401,7 +1428,6 @@ function planProjectInit(input) {
|
|
|
1401
1428
|
planeProjectId: project.ticket_provider.board_id ?? "",
|
|
1402
1429
|
ticketWorkspace: project.ticket_provider.workspace ?? "",
|
|
1403
1430
|
boardId: project.ticket_provider.board_id ?? "",
|
|
1404
|
-
boardUrl: project.ticket_provider.board_url ?? "",
|
|
1405
1431
|
projectIdentifier: identifier,
|
|
1406
1432
|
primaryLanguage: project.template.commonproject.primary_language,
|
|
1407
1433
|
agentHooksLayer: resolveAgentHooksLayer(input.agentHooksLayer),
|
|
@@ -1513,10 +1539,10 @@ function projectManifestFromRegistryProject(project) {
|
|
|
1513
1539
|
workspace: project.ticket_provider.workspace ?? "",
|
|
1514
1540
|
identifier: project.ticket_provider.identifier ?? "",
|
|
1515
1541
|
board_id: project.ticket_provider.board_id ?? "",
|
|
1516
|
-
board_url: project.ticket_provider.board_url ?? "",
|
|
1517
1542
|
state: project.ticket_provider.state
|
|
1518
1543
|
},
|
|
1519
|
-
agents
|
|
1544
|
+
agents,
|
|
1545
|
+
automation: project.automation ?? defaultProjectAutomation()
|
|
1520
1546
|
};
|
|
1521
1547
|
}
|
|
1522
1548
|
function getProject(registry, slug) {
|
|
@@ -1535,7 +1561,6 @@ function buildCommonProjectCopierAction(input) {
|
|
|
1535
1561
|
plane_project_id: input.planeProjectId ?? "",
|
|
1536
1562
|
ticket_workspace: input.ticketWorkspace ?? input.planeWorkspace,
|
|
1537
1563
|
board_id: input.boardId ?? input.planeProjectId ?? "",
|
|
1538
|
-
board_url: input.boardUrl ?? "",
|
|
1539
1564
|
project_identifier: input.projectIdentifier,
|
|
1540
1565
|
primary_language: input.primaryLanguage,
|
|
1541
1566
|
agent_hooks_layer: input.agentHooksLayer ?? true ? "true" : "false"
|
|
@@ -2142,8 +2167,12 @@ function discoverRoles(repoRoot) {
|
|
|
2142
2167
|
planeWorkspace: yamlGet(text2, "ticket_provider.workspace") || yamlGet(text2, "plane.workspace"),
|
|
2143
2168
|
ticketProviderName: yamlGet(text2, "ticket_provider.name"),
|
|
2144
2169
|
ticketProviderBoardId: yamlGet(text2, "ticket_provider.board_id"),
|
|
2145
|
-
|
|
2146
|
-
|
|
2170
|
+
ticketProviderIdentifier: yamlGet(text2, "plane.identifier"),
|
|
2171
|
+
legacyReconcileEnabled: yamlGet(text2, "reconcile.enabled"),
|
|
2172
|
+
legacyReconcileGraceHours: yamlGet(text2, "reconcile.grace_hours"),
|
|
2173
|
+
legacyReconcileAutoReview: yamlGet(text2, "reconcile.auto_review"),
|
|
2174
|
+
legacyScrumGraceHours: yamlGet(text2, "scrum_master.grace_hours"),
|
|
2175
|
+
legacyScrumAutoReview: yamlGet(text2, "scrum_master.auto_review")
|
|
2147
2176
|
};
|
|
2148
2177
|
}).filter((value) => Boolean(value));
|
|
2149
2178
|
}
|
|
@@ -2168,10 +2197,39 @@ function templateVersioningScript(ctx) {
|
|
|
2168
2197
|
function templateLinkAgentfilesScript(ctx) {
|
|
2169
2198
|
return templateScript(ctx, "link-agentfiles.sh");
|
|
2170
2199
|
}
|
|
2200
|
+
function resolveAgentHooksLayer2(ctx) {
|
|
2201
|
+
const override = process.env.PJ_AGENT_HOOKS_LAYER;
|
|
2202
|
+
if (override === "0" || override === "false") return false;
|
|
2203
|
+
if (override === "1" || override === "true") return true;
|
|
2204
|
+
if (existsSync8(join11(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
|
|
2205
|
+
return !existsSync8(join11(ctx.homeDir, ".agents", "hooks"));
|
|
2206
|
+
}
|
|
2207
|
+
function evaluateMiseConditionals(template, agentHooksLayer) {
|
|
2208
|
+
const out = [];
|
|
2209
|
+
let depth = 0;
|
|
2210
|
+
let skipDepth = 0;
|
|
2211
|
+
for (const line of template.split("\n")) {
|
|
2212
|
+
const stmt = line.trim();
|
|
2213
|
+
const ifMatch = /^\{%-?\s*if\s+(\w+)\s*-?%\}$/.exec(stmt);
|
|
2214
|
+
if (ifMatch) {
|
|
2215
|
+
depth += 1;
|
|
2216
|
+
const truthy = ifMatch[1] === "agent_hooks_layer" ? agentHooksLayer : false;
|
|
2217
|
+
if (skipDepth === 0 && !truthy) skipDepth = depth;
|
|
2218
|
+
continue;
|
|
2219
|
+
}
|
|
2220
|
+
if (/^\{%-?\s*endif\s*-?%\}$/.test(stmt)) {
|
|
2221
|
+
if (skipDepth === depth) skipDepth = 0;
|
|
2222
|
+
depth = Math.max(0, depth - 1);
|
|
2223
|
+
continue;
|
|
2224
|
+
}
|
|
2225
|
+
if (skipDepth === 0) out.push(line);
|
|
2226
|
+
}
|
|
2227
|
+
return out.join("\n");
|
|
2228
|
+
}
|
|
2171
2229
|
function renderGeneratedProjectMiseToml(ctx, template) {
|
|
2172
2230
|
const project = readProjectJson(ctx);
|
|
2173
2231
|
const projectName = String(project?.project_name ?? basename3(ctx.repoRoot) ?? "project");
|
|
2174
|
-
return template.replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
|
|
2232
|
+
return evaluateMiseConditionals(template, resolveAgentHooksLayer2(ctx)).replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
|
|
2175
2233
|
}
|
|
2176
2234
|
function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
2177
2235
|
const targetPath = join11(ctx.repoRoot, "mise.toml");
|
|
@@ -2361,6 +2419,23 @@ function upsertLinkAgentfilesBlock(text2, ctx) {
|
|
|
2361
2419
|
function readProjectJson(ctx) {
|
|
2362
2420
|
return tryParseJson(safeReadText(join11(ctx.repoRoot, ".project.json")));
|
|
2363
2421
|
}
|
|
2422
|
+
function boolSetting(value, fallback) {
|
|
2423
|
+
if (typeof value === "boolean") return value;
|
|
2424
|
+
if (typeof value === "string") {
|
|
2425
|
+
const normalized = value.trim().toLowerCase();
|
|
2426
|
+
if (["true", "1", "yes", "on"].includes(normalized)) return true;
|
|
2427
|
+
if (["false", "0", "no", "off"].includes(normalized)) return false;
|
|
2428
|
+
}
|
|
2429
|
+
return fallback;
|
|
2430
|
+
}
|
|
2431
|
+
function numberSetting(value, fallback) {
|
|
2432
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
2433
|
+
if (typeof value === "string" && value.trim()) {
|
|
2434
|
+
const parsed = Number(value);
|
|
2435
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
2436
|
+
}
|
|
2437
|
+
return fallback;
|
|
2438
|
+
}
|
|
2364
2439
|
function canonicalProjectJson(ctx) {
|
|
2365
2440
|
const roles = discoverRoles(ctx.repoRoot);
|
|
2366
2441
|
const existing = readProjectJson(ctx) ?? {};
|
|
@@ -2371,9 +2446,9 @@ function canonicalProjectJson(ctx) {
|
|
|
2371
2446
|
workspace: String((existing.ticket_provider?.workspace ?? firstRole?.planeWorkspace ?? "") || ""),
|
|
2372
2447
|
identifier: String((existing.ticket_provider?.identifier ?? firstRole?.ticketProviderIdentifier ?? "") || ""),
|
|
2373
2448
|
board_id: String((existing.ticket_provider?.board_id ?? firstRole?.ticketProviderBoardId ?? "") || ""),
|
|
2374
|
-
|
|
2375
|
-
state: String((existing.ticket_provider?.state ?? "planned") || "planned")
|
|
2449
|
+
state: String((existing.ticket_provider?.state ?? (firstRole?.ticketProviderBoardId ? "linked" : "planned")) || "planned")
|
|
2376
2450
|
};
|
|
2451
|
+
if (ticketProvider.board_id && ticketProvider.state === "planned") ticketProvider.state = "linked";
|
|
2377
2452
|
const existingAgents = existing.agents ?? {};
|
|
2378
2453
|
const discoveredAgents = Object.fromEntries(
|
|
2379
2454
|
roles.map((role) => [
|
|
@@ -2393,13 +2468,27 @@ function canonicalProjectJson(ctx) {
|
|
|
2393
2468
|
provisioning_state: existingAgent.provisioning_state
|
|
2394
2469
|
};
|
|
2395
2470
|
}
|
|
2471
|
+
const existingAutomation = existing.automation ?? {};
|
|
2472
|
+
const existingReconcile = existingAutomation.reconcile ?? {};
|
|
2473
|
+
const legacyEnabled = roles.find((role) => role.legacyReconcileEnabled)?.legacyReconcileEnabled;
|
|
2474
|
+
const legacyGrace = roles.find((role) => role.legacyReconcileGraceHours || role.legacyScrumGraceHours);
|
|
2475
|
+
const legacyAutoReview = roles.find((role) => role.legacyReconcileAutoReview || role.legacyScrumAutoReview);
|
|
2476
|
+
const automation = {
|
|
2477
|
+
...existingAutomation,
|
|
2478
|
+
reconcile: {
|
|
2479
|
+
enabled: boolSetting(existingReconcile.enabled, boolSetting(legacyEnabled, false)),
|
|
2480
|
+
grace_hours: numberSetting(existingReconcile.grace_hours, numberSetting(legacyGrace?.legacyReconcileGraceHours || legacyGrace?.legacyScrumGraceHours, 0)),
|
|
2481
|
+
auto_review: boolSetting(existingReconcile.auto_review, boolSetting(legacyAutoReview?.legacyReconcileAutoReview || legacyAutoReview?.legacyScrumAutoReview, true))
|
|
2482
|
+
}
|
|
2483
|
+
};
|
|
2396
2484
|
return {
|
|
2397
2485
|
project_name: String(existing.project_name ?? titleCaseSlug(slug)),
|
|
2398
2486
|
project_description: String(existing.project_description ?? ""),
|
|
2399
2487
|
project_slug: slug,
|
|
2400
2488
|
repo_path: ctx.repoRoot,
|
|
2401
2489
|
ticket_provider: ticketProvider,
|
|
2402
|
-
agents
|
|
2490
|
+
agents,
|
|
2491
|
+
automation
|
|
2403
2492
|
};
|
|
2404
2493
|
}
|
|
2405
2494
|
function projectJsonFinding(ctx) {
|
|
@@ -2414,7 +2503,7 @@ function projectJsonFinding(ctx) {
|
|
|
2414
2503
|
if (!data) {
|
|
2415
2504
|
return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json is not valid JSON", details: [], fixable: true };
|
|
2416
2505
|
}
|
|
2417
|
-
for (const key of ["project_name", "project_description", "project_slug", "repo_path", "ticket_provider", "agents"]) {
|
|
2506
|
+
for (const key of ["project_name", "project_description", "project_slug", "repo_path", "ticket_provider", "agents", "automation"]) {
|
|
2418
2507
|
if (!(key in data)) details.push(`missing key: ${key}`);
|
|
2419
2508
|
}
|
|
2420
2509
|
if (data.repo_path !== ctx.repoRoot) details.push(`repo_path should be ${ctx.repoRoot}`);
|
|
@@ -2431,9 +2520,18 @@ function projectJsonFinding(ctx) {
|
|
|
2431
2520
|
}
|
|
2432
2521
|
}
|
|
2433
2522
|
const ticketProvider = data.ticket_provider ?? {};
|
|
2434
|
-
for (const key of ["type", "workspace", "identifier", "board_id", "
|
|
2523
|
+
for (const key of ["type", "workspace", "identifier", "board_id", "state"]) {
|
|
2435
2524
|
if (!(key in ticketProvider)) details.push(`ticket_provider.${key} missing`);
|
|
2436
2525
|
}
|
|
2526
|
+
if ("board_url" in ticketProvider) details.push("ticket_provider.board_url should be removed; derive it from provider/workspace/board_id");
|
|
2527
|
+
if (!ticketProvider.board_id && roles.some((role) => role.ticketProviderBoardId)) {
|
|
2528
|
+
details.push("ticket_provider.board_id missing even though legacy role.yaml contains a board binding");
|
|
2529
|
+
}
|
|
2530
|
+
const automation = data.automation ?? {};
|
|
2531
|
+
const reconcile = automation.reconcile ?? {};
|
|
2532
|
+
for (const key of ["enabled", "grace_hours", "auto_review"]) {
|
|
2533
|
+
if (!(key in reconcile)) details.push(`automation.reconcile.${key} missing`);
|
|
2534
|
+
}
|
|
2437
2535
|
if (existsSync8(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
|
|
2438
2536
|
return {
|
|
2439
2537
|
id: "sot.project-json",
|
|
@@ -2447,7 +2545,7 @@ function projectJsonFinding(ctx) {
|
|
|
2447
2545
|
function renderSoul(role) {
|
|
2448
2546
|
const telegram = role.botHandle ? `@${role.botHandle}` : "(unwired)";
|
|
2449
2547
|
const tone = role.role === "pm" ? `Direct and brief. Decision-forward. No throat-clearing, no apologies, no "I'll help you with that" preambles.` : "Direct and brief.";
|
|
2450
|
-
const roleSpecific = role.role === "pm" ? `You are the project manager. You triage incoming work, create or refine tickets, and delegate implementation. You do not ship product code. A systemd heartbeat checkpoints your runtime; when this repo opts into reconciliation (\`reconcile.enabled\` in
|
|
2548
|
+
const roleSpecific = role.role === "pm" ? `You are the project manager. You triage incoming work, create or refine tickets, and delegate implementation. You do not ship product code. A systemd heartbeat checkpoints your runtime; when this repo opts into reconciliation (\`automation.reconcile.enabled\` in repo-root \`.project.json\`), the same heartbeat also runs your continuous board-reconciliation pass out-of-band (\`.scripts/sentinel.prompt.md\`, \`--source cron\`), kept separate from your interactive session memory.` : `You operate as the ${role.role} agent for this repo.`;
|
|
2451
2549
|
const runtimeOwner = role.runtimeOwner || "delorenj";
|
|
2452
2550
|
return `# ${role.displayName || role.agentId}
|
|
2453
2551
|
|