@delorenj/pjangler 1.2.17 → 1.2.19
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 +244 -6
- package/dist/mcp-server.js +245 -7
- package/package.json +8 -2
- package/templates/commonproject/AGENTS.md +3 -3
- package/templates/commonproject/README.md +11 -12
- package/templates/commonproject/copier.yml +8 -27
- package/templates/commonproject/template/.project.json.jinja +9 -13
- package/templates/hermes-agent/README.md +9 -9
- package/templates/hermes-agent/config.example.toml +1 -66
- package/templates/hermes-agent/copier.yml +4 -3
- package/templates/hermes-agent/docs/architecture.md +9 -12
- package/templates/hermes-agent/docs/fleet-control-plane/README.md +1 -1
- package/templates/hermes-agent/docs/operations.md +6 -5
- package/templates/hermes-agent/docs/sentinel/README.md +9 -7
- package/templates/hermes-agent/docs/sentinel/architecture.md +1 -2
- package/templates/hermes-agent/docs/sentinel/development.md +12 -13
- package/templates/hermes-agent/docs/sentinel/providers.md +34 -15
- package/templates/hermes-agent/install-local.sh +15 -14
- package/templates/hermes-agent/runtime-scaffold/README.md +1 -1
- package/templates/hermes-agent/runtime-scaffold/bloodbank-consumer.py +46 -14
- package/templates/hermes-agent/runtime-scaffold/memories/MEMORY.md +2 -2
- package/templates/hermes-agent/scripts/fleet-sync.sh +1 -64
- package/templates/hermes-agent/template/.gitignore.jinja +0 -2
- package/templates/hermes-agent/template/.runtime-scaffold/README.md +1 -1
- package/templates/hermes-agent/template/.runtime-scaffold/bloodbank-consumer.py +43 -9
- package/templates/hermes-agent/template/.runtime-scaffold/memories/MEMORY.md +2 -2
- package/templates/hermes-agent/template/.scripts/01-config.sh +0 -1
- package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +0 -9
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +3 -22
- package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +0 -22
- package/templates/hermes-agent/template/.scripts/40-plane.sh +51 -0
- package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +59 -21
- package/templates/hermes-agent/template/.scripts/60-bloodbank.sh +2 -1
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +1 -10
- package/templates/hermes-agent/template/.scripts/_lib.sh +3 -61
- package/templates/hermes-agent/template/.scripts/config.example.toml +0 -5
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +22 -66
- package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +5 -9
- package/templates/hermes-agent/template/.scripts/providers/linear.sh +176 -0
- package/templates/hermes-agent/template/.scripts/providers/plane.sh +9 -29
- package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md +2 -2
- package/templates/hermes-agent/template/.scripts/sentinel/docs/continuous-ticket-orchestration.md +1 -1
- package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +1 -6
- package/templates/hermes-agent/template/SOUL.md.jinja +19 -21
- package/templates/hermes-agent/template/role.yaml.jinja +35 -4
- package/templates/hermes-agent/tests/test_bloodbank_consumer_contract.py +138 -0
- package/templates/hermes-agent/docs/bloodbank-gateway.md +0 -57
- package/templates/hermes-agent/docs/fleet-control-plane/n8n-service-hub.md +0 -60
package/dist/index.js
CHANGED
|
@@ -1308,7 +1308,235 @@ import { spawnSync as spawnSync5 } from "node:child_process";
|
|
|
1308
1308
|
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync3, renameSync, statSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1309
1309
|
import { homedir as homedir3 } from "node:os";
|
|
1310
1310
|
import { basename as basename2, delimiter, dirname as dirname4, join as join9, resolve } from "node:path";
|
|
1311
|
+
import YAML2 from "yaml";
|
|
1312
|
+
|
|
1313
|
+
// src/project/RegistryStore.ts
|
|
1314
|
+
import { Pool } from "pg";
|
|
1311
1315
|
import YAML from "yaml";
|
|
1316
|
+
function pgRegistryConfigFromEnv(env2 = process.env) {
|
|
1317
|
+
return {
|
|
1318
|
+
host: env2.PGHOST || "localhost",
|
|
1319
|
+
port: parseInt(env2.PGPORT || "5432", 10),
|
|
1320
|
+
user: env2.PGUSER || "delorenj",
|
|
1321
|
+
password: env2.PGPASSWORD || "",
|
|
1322
|
+
database: env2.PGDATABASE || "33god"
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
var PgRegistryStore = class {
|
|
1326
|
+
pool;
|
|
1327
|
+
constructor(config) {
|
|
1328
|
+
this.pool = new Pool({
|
|
1329
|
+
host: config.host,
|
|
1330
|
+
port: config.port,
|
|
1331
|
+
user: config.user,
|
|
1332
|
+
password: config.password,
|
|
1333
|
+
database: config.database
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
async load() {
|
|
1337
|
+
const client = await this.pool.connect();
|
|
1338
|
+
try {
|
|
1339
|
+
const { rows } = await client.query(
|
|
1340
|
+
`SELECT p.id, p.name, p.description, p.slug, p.status,
|
|
1341
|
+
p.source_artifacts, p.template, p.automation,
|
|
1342
|
+
p.created_at, p.updated_at,
|
|
1343
|
+
r.id AS repo_id, r.local_path
|
|
1344
|
+
FROM public.projects p
|
|
1345
|
+
LEFT JOIN public.repos r ON r.project_id = p.id
|
|
1346
|
+
WHERE p.slug IS NOT NULL`
|
|
1347
|
+
);
|
|
1348
|
+
const projects = {};
|
|
1349
|
+
for (const row of rows) {
|
|
1350
|
+
const slug = row.slug;
|
|
1351
|
+
const ticketProvider = await this.loadTicketProvider(client, row.id);
|
|
1352
|
+
const agents = await this.loadAgents(client, row.id, slug);
|
|
1353
|
+
projects[slug] = {
|
|
1354
|
+
name: row.name ?? "",
|
|
1355
|
+
slug,
|
|
1356
|
+
repo_path: row.local_path ?? "",
|
|
1357
|
+
description: row.description ?? "",
|
|
1358
|
+
status: row.status ?? "planned",
|
|
1359
|
+
source_artifacts: row.source_artifacts ?? [],
|
|
1360
|
+
template: row.template ?? {
|
|
1361
|
+
commonproject: { enabled: false, primary_language: "python" }
|
|
1362
|
+
},
|
|
1363
|
+
ticket_provider: ticketProvider,
|
|
1364
|
+
agents,
|
|
1365
|
+
automation: row.automation ?? void 0,
|
|
1366
|
+
created_at: row.created_at.toISOString(),
|
|
1367
|
+
updated_at: row.updated_at.toISOString()
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
const registry = {
|
|
1371
|
+
schema_version: PROJECT_REGISTRY_SCHEMA_VERSION,
|
|
1372
|
+
projects
|
|
1373
|
+
};
|
|
1374
|
+
validateProjectRegistry(registry);
|
|
1375
|
+
return registry;
|
|
1376
|
+
} finally {
|
|
1377
|
+
client.release();
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
async save(registry) {
|
|
1381
|
+
validateProjectRegistry(registry);
|
|
1382
|
+
const client = await this.pool.connect();
|
|
1383
|
+
try {
|
|
1384
|
+
await client.query("BEGIN");
|
|
1385
|
+
for (const [slug, record] of Object.entries(registry.projects)) {
|
|
1386
|
+
await this.upsertInTx(client, slug, record);
|
|
1387
|
+
}
|
|
1388
|
+
await client.query("COMMIT");
|
|
1389
|
+
} catch (err) {
|
|
1390
|
+
await client.query("ROLLBACK");
|
|
1391
|
+
throw err;
|
|
1392
|
+
} finally {
|
|
1393
|
+
client.release();
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
async upsert(slug, record) {
|
|
1397
|
+
const client = await this.pool.connect();
|
|
1398
|
+
try {
|
|
1399
|
+
await client.query("BEGIN");
|
|
1400
|
+
await this.upsertInTx(client, slug, record);
|
|
1401
|
+
await client.query("COMMIT");
|
|
1402
|
+
} catch (err) {
|
|
1403
|
+
await client.query("ROLLBACK");
|
|
1404
|
+
throw err;
|
|
1405
|
+
} finally {
|
|
1406
|
+
client.release();
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
async getBySlug(slug) {
|
|
1410
|
+
const registry = await this.load();
|
|
1411
|
+
return registry.projects[slug];
|
|
1412
|
+
}
|
|
1413
|
+
async getByRepoPath(repoPath) {
|
|
1414
|
+
const registry = await this.load();
|
|
1415
|
+
return Object.values(registry.projects).find(
|
|
1416
|
+
(p6) => p6.repo_path === repoPath
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
async close() {
|
|
1420
|
+
await this.pool.end();
|
|
1421
|
+
}
|
|
1422
|
+
// --- private helpers ---
|
|
1423
|
+
async upsertInTx(client, slug, record) {
|
|
1424
|
+
if (!slug) throw new Error("PgRegistryStore.upsert: slug is required");
|
|
1425
|
+
const projectResult = await client.query(
|
|
1426
|
+
`INSERT INTO public.projects (name, description, slug, status, source_artifacts, template, automation)
|
|
1427
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
1428
|
+
ON CONFLICT (slug) WHERE slug IS NOT NULL
|
|
1429
|
+
DO UPDATE SET
|
|
1430
|
+
name = EXCLUDED.name,
|
|
1431
|
+
description = EXCLUDED.description,
|
|
1432
|
+
status = EXCLUDED.status,
|
|
1433
|
+
source_artifacts = EXCLUDED.source_artifacts,
|
|
1434
|
+
template = EXCLUDED.template,
|
|
1435
|
+
automation = EXCLUDED.automation
|
|
1436
|
+
RETURNING id`,
|
|
1437
|
+
[
|
|
1438
|
+
record.name,
|
|
1439
|
+
record.description,
|
|
1440
|
+
slug,
|
|
1441
|
+
record.status,
|
|
1442
|
+
JSON.stringify(record.source_artifacts),
|
|
1443
|
+
JSON.stringify(record.template),
|
|
1444
|
+
record.automation ? JSON.stringify(record.automation) : null
|
|
1445
|
+
]
|
|
1446
|
+
);
|
|
1447
|
+
const projectId = projectResult.rows[0]?.id;
|
|
1448
|
+
if (!projectId) throw new Error(`Failed to upsert project: ${slug}`);
|
|
1449
|
+
await client.query(
|
|
1450
|
+
`INSERT INTO public.repos (project_id, local_path)
|
|
1451
|
+
VALUES ($1, $2)
|
|
1452
|
+
ON CONFLICT (local_path)
|
|
1453
|
+
DO UPDATE SET project_id = EXCLUDED.project_id
|
|
1454
|
+
RETURNING id`,
|
|
1455
|
+
[projectId, record.repo_path]
|
|
1456
|
+
);
|
|
1457
|
+
const repoResult = await client.query(
|
|
1458
|
+
`SELECT id FROM public.repos WHERE project_id = $1 AND local_path = $2`,
|
|
1459
|
+
[projectId, record.repo_path]
|
|
1460
|
+
);
|
|
1461
|
+
const repoId = repoResult.rows[0]?.id;
|
|
1462
|
+
if (!repoId) throw new Error(`Failed to find repo for project ${slug} at path ${record.repo_path}`);
|
|
1463
|
+
await this.upsertTicketProvider(client, projectId, repoId, record.ticket_provider);
|
|
1464
|
+
await client.query(
|
|
1465
|
+
`DELETE FROM public.project_agents WHERE project_id = $1`,
|
|
1466
|
+
[projectId]
|
|
1467
|
+
);
|
|
1468
|
+
for (const [agentKey, agent] of Object.entries(record.agents)) {
|
|
1469
|
+
await client.query(
|
|
1470
|
+
`INSERT INTO public.project_agents (repo_id, project_id, agent_key, role, role_dir, provisioning_state)
|
|
1471
|
+
VALUES ($1, $2, $3, $4, $5, $6)`,
|
|
1472
|
+
[repoId, projectId, agentKey, agent.role, agent.role_dir ?? null, agent.provisioning_state]
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
async loadTicketProvider(client, projectId) {
|
|
1477
|
+
const { rows } = await client.query(
|
|
1478
|
+
`SELECT provider_type, workspace, identifier, board_id, state
|
|
1479
|
+
FROM public.project_ticket_boards
|
|
1480
|
+
WHERE project_id = $1
|
|
1481
|
+
LIMIT 1`,
|
|
1482
|
+
[projectId]
|
|
1483
|
+
);
|
|
1484
|
+
if (!rows.length) {
|
|
1485
|
+
return { type: "plane", workspace: "33god", identifier: "", board_id: "", state: "planned" };
|
|
1486
|
+
}
|
|
1487
|
+
const row = rows[0];
|
|
1488
|
+
return {
|
|
1489
|
+
type: row.provider_type,
|
|
1490
|
+
workspace: row.workspace ?? void 0,
|
|
1491
|
+
identifier: row.identifier ?? void 0,
|
|
1492
|
+
board_id: row.board_id ?? void 0,
|
|
1493
|
+
state: row.state ?? void 0
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
async loadAgents(client, projectId, slug) {
|
|
1497
|
+
const { rows } = await client.query(
|
|
1498
|
+
`SELECT agent_key, role, role_dir, provisioning_state
|
|
1499
|
+
FROM public.project_agents
|
|
1500
|
+
WHERE project_id = $1`,
|
|
1501
|
+
[projectId]
|
|
1502
|
+
);
|
|
1503
|
+
const agents = {};
|
|
1504
|
+
for (const row of rows) {
|
|
1505
|
+
agents[row.agent_key] = {
|
|
1506
|
+
role: row.role,
|
|
1507
|
+
provisioning_state: row.provisioning_state,
|
|
1508
|
+
role_dir: row.role_dir ?? void 0
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
return agents;
|
|
1512
|
+
}
|
|
1513
|
+
async upsertTicketProvider(client, projectId, repoId, tp) {
|
|
1514
|
+
await client.query(
|
|
1515
|
+
`DELETE FROM public.project_ticket_boards WHERE project_id = $1`,
|
|
1516
|
+
[projectId]
|
|
1517
|
+
);
|
|
1518
|
+
await client.query(
|
|
1519
|
+
`INSERT INTO public.project_ticket_boards
|
|
1520
|
+
(repo_id, project_id, provider_type, workspace, identifier, board_id, state)
|
|
1521
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
|
1522
|
+
[
|
|
1523
|
+
repoId,
|
|
1524
|
+
projectId,
|
|
1525
|
+
tp.type,
|
|
1526
|
+
tp.workspace ?? null,
|
|
1527
|
+
tp.identifier ?? null,
|
|
1528
|
+
tp.board_id ?? null,
|
|
1529
|
+
tp.state ?? null
|
|
1530
|
+
]
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1533
|
+
};
|
|
1534
|
+
var PJ_REGISTRY_PG_ENV = "PJ_REGISTRY_PG";
|
|
1535
|
+
function isPgRegistryEnabled(env2 = process.env) {
|
|
1536
|
+
return env2[PJ_REGISTRY_PG_ENV] === "1" || env2[PJ_REGISTRY_PG_ENV] === "true";
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
// src/project/index.ts
|
|
1312
1540
|
var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
|
|
1313
1541
|
var PROJECT_SOURCE_SKILL_ROOTS_ENV = "PJ_SOURCE_SKILL_ROOTS";
|
|
1314
1542
|
var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
|
|
@@ -1325,7 +1553,7 @@ function emptyProjectRegistry() {
|
|
|
1325
1553
|
}
|
|
1326
1554
|
function loadProjectRegistry(path = projectRegistryPath()) {
|
|
1327
1555
|
if (!existsSync7(path)) return emptyProjectRegistry();
|
|
1328
|
-
const raw =
|
|
1556
|
+
const raw = YAML2.parse(readFileSync3(path, "utf8"));
|
|
1329
1557
|
if (raw == null) return emptyProjectRegistry();
|
|
1330
1558
|
if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
|
|
1331
1559
|
const registry = raw;
|
|
@@ -1340,7 +1568,7 @@ function saveProjectRegistry(registry, path = projectRegistryPath()) {
|
|
|
1340
1568
|
validateProjectRegistry(registry);
|
|
1341
1569
|
mkdirSync4(dirname4(path), { recursive: true });
|
|
1342
1570
|
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
1343
|
-
writeFileSync4(temp,
|
|
1571
|
+
writeFileSync4(temp, YAML2.stringify(registry, { lineWidth: 0 }), "utf8");
|
|
1344
1572
|
renameSync(temp, path);
|
|
1345
1573
|
}
|
|
1346
1574
|
function validateProjectRegistry(registry) {
|
|
@@ -1567,7 +1795,7 @@ function planProjectInit(input) {
|
|
|
1567
1795
|
);
|
|
1568
1796
|
return { ok: true, apply, dryRun: !apply, live, registryPath: registryPath2, project, manifest, actions };
|
|
1569
1797
|
}
|
|
1570
|
-
function executeProjectInitPlan(plan) {
|
|
1798
|
+
async function executeProjectInitPlan(plan) {
|
|
1571
1799
|
const logs = [];
|
|
1572
1800
|
const errors = [];
|
|
1573
1801
|
const changedFiles = [];
|
|
@@ -1618,6 +1846,16 @@ function executeProjectInitPlan(plan) {
|
|
|
1618
1846
|
registry.projects[pendingRegistryAction.slug] = pendingRegistryAction.project;
|
|
1619
1847
|
saveProjectRegistry(registry, pendingRegistryAction.registryPath);
|
|
1620
1848
|
changedFiles.push(pendingRegistryAction.registryPath);
|
|
1849
|
+
if (isPgRegistryEnabled()) {
|
|
1850
|
+
try {
|
|
1851
|
+
const pgStore = new PgRegistryStore(pgRegistryConfigFromEnv());
|
|
1852
|
+
await pgStore.upsert(pendingRegistryAction.slug, pendingRegistryAction.project);
|
|
1853
|
+
await pgStore.close();
|
|
1854
|
+
logs.push("registry: PG dual-write complete");
|
|
1855
|
+
} catch (pgErr) {
|
|
1856
|
+
logs.push(`registry: PG dual-write failed (yaml is authoritative): ${pgErr instanceof Error ? pgErr.message : pgErr}`);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1621
1859
|
}
|
|
1622
1860
|
}
|
|
1623
1861
|
return { ok: errors.length === 0, plan, logs, errors, changedFiles };
|
|
@@ -2121,7 +2359,7 @@ import { basename as basename3, dirname as dirname6, join as join11, relative, r
|
|
|
2121
2359
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
2122
2360
|
import { homedir as homedir5 } from "node:os";
|
|
2123
2361
|
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
2124
|
-
import
|
|
2362
|
+
import YAML3 from "yaml";
|
|
2125
2363
|
var LINK_AGENTFILES_SCRIPT = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'";
|
|
2126
2364
|
var OP_INJECT_SCRIPT = "op inject -i .env.op > .env";
|
|
2127
2365
|
var CODEGRAPH_SCRIPT = "[ -f '{{config_root}}/.mise/scripts/codegraph.sh' ] && '{{config_root}}/.mise/scripts/codegraph.sh' || true";
|
|
@@ -3001,7 +3239,7 @@ function readInstalledBmadVersion(repoRoot) {
|
|
|
3001
3239
|
const raw = safeReadText(join11(repoRoot, "_bmad", "_config", "manifest.yaml"));
|
|
3002
3240
|
if (!raw) return void 0;
|
|
3003
3241
|
try {
|
|
3004
|
-
const parsed =
|
|
3242
|
+
const parsed = YAML3.parse(raw);
|
|
3005
3243
|
const version = parsed?.installation?.version;
|
|
3006
3244
|
return typeof version === "string" && version.trim() ? version.trim() : void 0;
|
|
3007
3245
|
} catch {
|
|
@@ -4298,7 +4536,7 @@ async function runProjectInit(name, options) {
|
|
|
4298
4536
|
}
|
|
4299
4537
|
return;
|
|
4300
4538
|
}
|
|
4301
|
-
const initResult = selectedPlan.actions.length ? executeProjectInitPlan(selectedPlan) : { ok: true, plan: selectedPlan, logs: [], errors: [], changedFiles: [] };
|
|
4539
|
+
const initResult = selectedPlan.actions.length ? await executeProjectInitPlan(selectedPlan) : { ok: true, plan: selectedPlan, logs: [], errors: [], changedFiles: [] };
|
|
4302
4540
|
const migrationReport = selection.selectedParityRules.length ? runMigrationForRules(selection.selectedParityRules, target.targetDir, false) : void 0;
|
|
4303
4541
|
const migrationErrors = migrationReport?.results.filter((result2) => result2.status === "blocked").map((result2) => `${result2.id}: ${result2.summary}`) ?? [];
|
|
4304
4542
|
const changedFiles = Array.from(/* @__PURE__ */ new Set([
|
package/dist/mcp-server.js
CHANGED
|
@@ -1294,7 +1294,235 @@ import { spawnSync as spawnSync5 } from "node:child_process";
|
|
|
1294
1294
|
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync3, renameSync, statSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1295
1295
|
import { homedir as homedir3 } from "node:os";
|
|
1296
1296
|
import { basename as basename2, delimiter, dirname as dirname4, join as join9, resolve } from "node:path";
|
|
1297
|
+
import YAML2 from "yaml";
|
|
1298
|
+
|
|
1299
|
+
// src/project/RegistryStore.ts
|
|
1300
|
+
import { Pool } from "pg";
|
|
1297
1301
|
import YAML from "yaml";
|
|
1302
|
+
function pgRegistryConfigFromEnv(env2 = process.env) {
|
|
1303
|
+
return {
|
|
1304
|
+
host: env2.PGHOST || "localhost",
|
|
1305
|
+
port: parseInt(env2.PGPORT || "5432", 10),
|
|
1306
|
+
user: env2.PGUSER || "delorenj",
|
|
1307
|
+
password: env2.PGPASSWORD || "",
|
|
1308
|
+
database: env2.PGDATABASE || "33god"
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
var PgRegistryStore = class {
|
|
1312
|
+
pool;
|
|
1313
|
+
constructor(config) {
|
|
1314
|
+
this.pool = new Pool({
|
|
1315
|
+
host: config.host,
|
|
1316
|
+
port: config.port,
|
|
1317
|
+
user: config.user,
|
|
1318
|
+
password: config.password,
|
|
1319
|
+
database: config.database
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
async load() {
|
|
1323
|
+
const client = await this.pool.connect();
|
|
1324
|
+
try {
|
|
1325
|
+
const { rows } = await client.query(
|
|
1326
|
+
`SELECT p.id, p.name, p.description, p.slug, p.status,
|
|
1327
|
+
p.source_artifacts, p.template, p.automation,
|
|
1328
|
+
p.created_at, p.updated_at,
|
|
1329
|
+
r.id AS repo_id, r.local_path
|
|
1330
|
+
FROM public.projects p
|
|
1331
|
+
LEFT JOIN public.repos r ON r.project_id = p.id
|
|
1332
|
+
WHERE p.slug IS NOT NULL`
|
|
1333
|
+
);
|
|
1334
|
+
const projects = {};
|
|
1335
|
+
for (const row of rows) {
|
|
1336
|
+
const slug = row.slug;
|
|
1337
|
+
const ticketProvider = await this.loadTicketProvider(client, row.id);
|
|
1338
|
+
const agents = await this.loadAgents(client, row.id, slug);
|
|
1339
|
+
projects[slug] = {
|
|
1340
|
+
name: row.name ?? "",
|
|
1341
|
+
slug,
|
|
1342
|
+
repo_path: row.local_path ?? "",
|
|
1343
|
+
description: row.description ?? "",
|
|
1344
|
+
status: row.status ?? "planned",
|
|
1345
|
+
source_artifacts: row.source_artifacts ?? [],
|
|
1346
|
+
template: row.template ?? {
|
|
1347
|
+
commonproject: { enabled: false, primary_language: "python" }
|
|
1348
|
+
},
|
|
1349
|
+
ticket_provider: ticketProvider,
|
|
1350
|
+
agents,
|
|
1351
|
+
automation: row.automation ?? void 0,
|
|
1352
|
+
created_at: row.created_at.toISOString(),
|
|
1353
|
+
updated_at: row.updated_at.toISOString()
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
const registry = {
|
|
1357
|
+
schema_version: PROJECT_REGISTRY_SCHEMA_VERSION,
|
|
1358
|
+
projects
|
|
1359
|
+
};
|
|
1360
|
+
validateProjectRegistry(registry);
|
|
1361
|
+
return registry;
|
|
1362
|
+
} finally {
|
|
1363
|
+
client.release();
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
async save(registry) {
|
|
1367
|
+
validateProjectRegistry(registry);
|
|
1368
|
+
const client = await this.pool.connect();
|
|
1369
|
+
try {
|
|
1370
|
+
await client.query("BEGIN");
|
|
1371
|
+
for (const [slug, record] of Object.entries(registry.projects)) {
|
|
1372
|
+
await this.upsertInTx(client, slug, record);
|
|
1373
|
+
}
|
|
1374
|
+
await client.query("COMMIT");
|
|
1375
|
+
} catch (err) {
|
|
1376
|
+
await client.query("ROLLBACK");
|
|
1377
|
+
throw err;
|
|
1378
|
+
} finally {
|
|
1379
|
+
client.release();
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
async upsert(slug, record) {
|
|
1383
|
+
const client = await this.pool.connect();
|
|
1384
|
+
try {
|
|
1385
|
+
await client.query("BEGIN");
|
|
1386
|
+
await this.upsertInTx(client, slug, record);
|
|
1387
|
+
await client.query("COMMIT");
|
|
1388
|
+
} catch (err) {
|
|
1389
|
+
await client.query("ROLLBACK");
|
|
1390
|
+
throw err;
|
|
1391
|
+
} finally {
|
|
1392
|
+
client.release();
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
async getBySlug(slug) {
|
|
1396
|
+
const registry = await this.load();
|
|
1397
|
+
return registry.projects[slug];
|
|
1398
|
+
}
|
|
1399
|
+
async getByRepoPath(repoPath) {
|
|
1400
|
+
const registry = await this.load();
|
|
1401
|
+
return Object.values(registry.projects).find(
|
|
1402
|
+
(p6) => p6.repo_path === repoPath
|
|
1403
|
+
);
|
|
1404
|
+
}
|
|
1405
|
+
async close() {
|
|
1406
|
+
await this.pool.end();
|
|
1407
|
+
}
|
|
1408
|
+
// --- private helpers ---
|
|
1409
|
+
async upsertInTx(client, slug, record) {
|
|
1410
|
+
if (!slug) throw new Error("PgRegistryStore.upsert: slug is required");
|
|
1411
|
+
const projectResult = await client.query(
|
|
1412
|
+
`INSERT INTO public.projects (name, description, slug, status, source_artifacts, template, automation)
|
|
1413
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
1414
|
+
ON CONFLICT (slug) WHERE slug IS NOT NULL
|
|
1415
|
+
DO UPDATE SET
|
|
1416
|
+
name = EXCLUDED.name,
|
|
1417
|
+
description = EXCLUDED.description,
|
|
1418
|
+
status = EXCLUDED.status,
|
|
1419
|
+
source_artifacts = EXCLUDED.source_artifacts,
|
|
1420
|
+
template = EXCLUDED.template,
|
|
1421
|
+
automation = EXCLUDED.automation
|
|
1422
|
+
RETURNING id`,
|
|
1423
|
+
[
|
|
1424
|
+
record.name,
|
|
1425
|
+
record.description,
|
|
1426
|
+
slug,
|
|
1427
|
+
record.status,
|
|
1428
|
+
JSON.stringify(record.source_artifacts),
|
|
1429
|
+
JSON.stringify(record.template),
|
|
1430
|
+
record.automation ? JSON.stringify(record.automation) : null
|
|
1431
|
+
]
|
|
1432
|
+
);
|
|
1433
|
+
const projectId = projectResult.rows[0]?.id;
|
|
1434
|
+
if (!projectId) throw new Error(`Failed to upsert project: ${slug}`);
|
|
1435
|
+
await client.query(
|
|
1436
|
+
`INSERT INTO public.repos (project_id, local_path)
|
|
1437
|
+
VALUES ($1, $2)
|
|
1438
|
+
ON CONFLICT (local_path)
|
|
1439
|
+
DO UPDATE SET project_id = EXCLUDED.project_id
|
|
1440
|
+
RETURNING id`,
|
|
1441
|
+
[projectId, record.repo_path]
|
|
1442
|
+
);
|
|
1443
|
+
const repoResult = await client.query(
|
|
1444
|
+
`SELECT id FROM public.repos WHERE project_id = $1 AND local_path = $2`,
|
|
1445
|
+
[projectId, record.repo_path]
|
|
1446
|
+
);
|
|
1447
|
+
const repoId = repoResult.rows[0]?.id;
|
|
1448
|
+
if (!repoId) throw new Error(`Failed to find repo for project ${slug} at path ${record.repo_path}`);
|
|
1449
|
+
await this.upsertTicketProvider(client, projectId, repoId, record.ticket_provider);
|
|
1450
|
+
await client.query(
|
|
1451
|
+
`DELETE FROM public.project_agents WHERE project_id = $1`,
|
|
1452
|
+
[projectId]
|
|
1453
|
+
);
|
|
1454
|
+
for (const [agentKey, agent] of Object.entries(record.agents)) {
|
|
1455
|
+
await client.query(
|
|
1456
|
+
`INSERT INTO public.project_agents (repo_id, project_id, agent_key, role, role_dir, provisioning_state)
|
|
1457
|
+
VALUES ($1, $2, $3, $4, $5, $6)`,
|
|
1458
|
+
[repoId, projectId, agentKey, agent.role, agent.role_dir ?? null, agent.provisioning_state]
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
async loadTicketProvider(client, projectId) {
|
|
1463
|
+
const { rows } = await client.query(
|
|
1464
|
+
`SELECT provider_type, workspace, identifier, board_id, state
|
|
1465
|
+
FROM public.project_ticket_boards
|
|
1466
|
+
WHERE project_id = $1
|
|
1467
|
+
LIMIT 1`,
|
|
1468
|
+
[projectId]
|
|
1469
|
+
);
|
|
1470
|
+
if (!rows.length) {
|
|
1471
|
+
return { type: "plane", workspace: "33god", identifier: "", board_id: "", state: "planned" };
|
|
1472
|
+
}
|
|
1473
|
+
const row = rows[0];
|
|
1474
|
+
return {
|
|
1475
|
+
type: row.provider_type,
|
|
1476
|
+
workspace: row.workspace ?? void 0,
|
|
1477
|
+
identifier: row.identifier ?? void 0,
|
|
1478
|
+
board_id: row.board_id ?? void 0,
|
|
1479
|
+
state: row.state ?? void 0
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
async loadAgents(client, projectId, slug) {
|
|
1483
|
+
const { rows } = await client.query(
|
|
1484
|
+
`SELECT agent_key, role, role_dir, provisioning_state
|
|
1485
|
+
FROM public.project_agents
|
|
1486
|
+
WHERE project_id = $1`,
|
|
1487
|
+
[projectId]
|
|
1488
|
+
);
|
|
1489
|
+
const agents = {};
|
|
1490
|
+
for (const row of rows) {
|
|
1491
|
+
agents[row.agent_key] = {
|
|
1492
|
+
role: row.role,
|
|
1493
|
+
provisioning_state: row.provisioning_state,
|
|
1494
|
+
role_dir: row.role_dir ?? void 0
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
return agents;
|
|
1498
|
+
}
|
|
1499
|
+
async upsertTicketProvider(client, projectId, repoId, tp) {
|
|
1500
|
+
await client.query(
|
|
1501
|
+
`DELETE FROM public.project_ticket_boards WHERE project_id = $1`,
|
|
1502
|
+
[projectId]
|
|
1503
|
+
);
|
|
1504
|
+
await client.query(
|
|
1505
|
+
`INSERT INTO public.project_ticket_boards
|
|
1506
|
+
(repo_id, project_id, provider_type, workspace, identifier, board_id, state)
|
|
1507
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
|
1508
|
+
[
|
|
1509
|
+
repoId,
|
|
1510
|
+
projectId,
|
|
1511
|
+
tp.type,
|
|
1512
|
+
tp.workspace ?? null,
|
|
1513
|
+
tp.identifier ?? null,
|
|
1514
|
+
tp.board_id ?? null,
|
|
1515
|
+
tp.state ?? null
|
|
1516
|
+
]
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
};
|
|
1520
|
+
var PJ_REGISTRY_PG_ENV = "PJ_REGISTRY_PG";
|
|
1521
|
+
function isPgRegistryEnabled(env2 = process.env) {
|
|
1522
|
+
return env2[PJ_REGISTRY_PG_ENV] === "1" || env2[PJ_REGISTRY_PG_ENV] === "true";
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// src/project/index.ts
|
|
1298
1526
|
var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
|
|
1299
1527
|
var PROJECT_SOURCE_SKILL_ROOTS_ENV = "PJ_SOURCE_SKILL_ROOTS";
|
|
1300
1528
|
var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
|
|
@@ -1311,7 +1539,7 @@ function emptyProjectRegistry() {
|
|
|
1311
1539
|
}
|
|
1312
1540
|
function loadProjectRegistry(path = projectRegistryPath()) {
|
|
1313
1541
|
if (!existsSync7(path)) return emptyProjectRegistry();
|
|
1314
|
-
const raw =
|
|
1542
|
+
const raw = YAML2.parse(readFileSync3(path, "utf8"));
|
|
1315
1543
|
if (raw == null) return emptyProjectRegistry();
|
|
1316
1544
|
if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
|
|
1317
1545
|
const registry = raw;
|
|
@@ -1326,7 +1554,7 @@ function saveProjectRegistry(registry, path = projectRegistryPath()) {
|
|
|
1326
1554
|
validateProjectRegistry(registry);
|
|
1327
1555
|
mkdirSync4(dirname4(path), { recursive: true });
|
|
1328
1556
|
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
1329
|
-
writeFileSync4(temp,
|
|
1557
|
+
writeFileSync4(temp, YAML2.stringify(registry, { lineWidth: 0 }), "utf8");
|
|
1330
1558
|
renameSync(temp, path);
|
|
1331
1559
|
}
|
|
1332
1560
|
function validateProjectRegistry(registry) {
|
|
@@ -1553,7 +1781,7 @@ function planProjectInit(input) {
|
|
|
1553
1781
|
);
|
|
1554
1782
|
return { ok: true, apply, dryRun: !apply, live, registryPath: registryPath2, project, manifest, actions };
|
|
1555
1783
|
}
|
|
1556
|
-
function executeProjectInitPlan(plan) {
|
|
1784
|
+
async function executeProjectInitPlan(plan) {
|
|
1557
1785
|
const logs = [];
|
|
1558
1786
|
const errors = [];
|
|
1559
1787
|
const changedFiles = [];
|
|
@@ -1604,6 +1832,16 @@ function executeProjectInitPlan(plan) {
|
|
|
1604
1832
|
registry.projects[pendingRegistryAction.slug] = pendingRegistryAction.project;
|
|
1605
1833
|
saveProjectRegistry(registry, pendingRegistryAction.registryPath);
|
|
1606
1834
|
changedFiles.push(pendingRegistryAction.registryPath);
|
|
1835
|
+
if (isPgRegistryEnabled()) {
|
|
1836
|
+
try {
|
|
1837
|
+
const pgStore = new PgRegistryStore(pgRegistryConfigFromEnv());
|
|
1838
|
+
await pgStore.upsert(pendingRegistryAction.slug, pendingRegistryAction.project);
|
|
1839
|
+
await pgStore.close();
|
|
1840
|
+
logs.push("registry: PG dual-write complete");
|
|
1841
|
+
} catch (pgErr) {
|
|
1842
|
+
logs.push(`registry: PG dual-write failed (yaml is authoritative): ${pgErr instanceof Error ? pgErr.message : pgErr}`);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1607
1845
|
}
|
|
1608
1846
|
}
|
|
1609
1847
|
return { ok: errors.length === 0, plan, logs, errors, changedFiles };
|
|
@@ -2048,7 +2286,7 @@ import { basename as basename3, dirname as dirname7, join as join12, relative, r
|
|
|
2048
2286
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
2049
2287
|
import { homedir as homedir5 } from "node:os";
|
|
2050
2288
|
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
2051
|
-
import
|
|
2289
|
+
import YAML3 from "yaml";
|
|
2052
2290
|
var LINK_AGENTFILES_SCRIPT = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'";
|
|
2053
2291
|
var OP_INJECT_SCRIPT = "op inject -i .env.op > .env";
|
|
2054
2292
|
var CODEGRAPH_SCRIPT = "[ -f '{{config_root}}/.mise/scripts/codegraph.sh' ] && '{{config_root}}/.mise/scripts/codegraph.sh' || true";
|
|
@@ -2928,7 +3166,7 @@ function readInstalledBmadVersion(repoRoot) {
|
|
|
2928
3166
|
const raw = safeReadText(join12(repoRoot, "_bmad", "_config", "manifest.yaml"));
|
|
2929
3167
|
if (!raw) return void 0;
|
|
2930
3168
|
try {
|
|
2931
|
-
const parsed =
|
|
3169
|
+
const parsed = YAML3.parse(raw);
|
|
2932
3170
|
const version = parsed?.installation?.version;
|
|
2933
3171
|
return typeof version === "string" && version.trim() ? version.trim() : void 0;
|
|
2934
3172
|
} catch {
|
|
@@ -4135,7 +4373,7 @@ server.registerTool(
|
|
|
4135
4373
|
if (dryRun) {
|
|
4136
4374
|
return asText({ ...plan, guidance: parityGuidance() });
|
|
4137
4375
|
}
|
|
4138
|
-
const result = executeProjectInitPlan(plan);
|
|
4376
|
+
const result = await executeProjectInitPlan(plan);
|
|
4139
4377
|
if (!result.ok) return asText({ ...result, guidance: parityGuidance() });
|
|
4140
4378
|
let agentResult;
|
|
4141
4379
|
if (input.provisionAgent) {
|
|
@@ -4211,7 +4449,7 @@ server.registerTool(
|
|
|
4211
4449
|
overwrite: input.force ?? false
|
|
4212
4450
|
});
|
|
4213
4451
|
if (!input.apply) return asText(plan);
|
|
4214
|
-
return asText(executeProjectInitPlan(plan));
|
|
4452
|
+
return asText(await executeProjectInitPlan(plan));
|
|
4215
4453
|
} catch (err) {
|
|
4216
4454
|
return { isError: true, content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }] };
|
|
4217
4455
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@delorenj/pjangler",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.19",
|
|
4
4
|
"description": "Project subsystem bootstrapper CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -23,7 +23,10 @@
|
|
|
23
23
|
"start": "node dist/index.js",
|
|
24
24
|
"mcp": "node dist/mcp-server.js",
|
|
25
25
|
"typecheck": "tsc --noEmit",
|
|
26
|
-
"test": "node tests/parity-migrate-regressions.mjs && node tests/mcp-catalog-regressions.mjs && node tests/mcp-server-regressions.mjs && node tests/project-registry-regressions.mjs",
|
|
26
|
+
"test": "node tests/parity-migrate-regressions.mjs && node tests/mcp-catalog-regressions.mjs && node tests/mcp-server-regressions.mjs && node tests/project-registry-regressions.mjs && node tests/pg-registry-regressions.mjs",
|
|
27
|
+
"migrate:up": "node-pg-migrate --migrations-dir migrations up",
|
|
28
|
+
"migrate:down": "node-pg-migrate --migrations-dir migrations down",
|
|
29
|
+
"migrate:create": "node-pg-migrate --migrations-dir migrations create",
|
|
27
30
|
"prepublishOnly": "npm run build"
|
|
28
31
|
},
|
|
29
32
|
"keywords": [
|
|
@@ -39,12 +42,15 @@
|
|
|
39
42
|
"@clack/prompts": "^1.4.0",
|
|
40
43
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
41
44
|
"commander": "^14.0.2",
|
|
45
|
+
"pg": "^8.16.0",
|
|
42
46
|
"yaml": "^2.9.0",
|
|
43
47
|
"zod": "^4.4.3"
|
|
44
48
|
},
|
|
45
49
|
"devDependencies": {
|
|
46
50
|
"@types/node": "^25.9.1",
|
|
51
|
+
"@types/pg": "^8.15.0",
|
|
47
52
|
"esbuild": "^0.25.0",
|
|
53
|
+
"node-pg-migrate": "^8.0.0",
|
|
48
54
|
"typescript": "^5"
|
|
49
55
|
}
|
|
50
56
|
}
|
|
@@ -20,7 +20,7 @@ CommonProject/
|
|
|
20
20
|
│ ├── .agentvibes/ # AgentVibes config
|
|
21
21
|
│ ├── .mise/tasks/ # File-based mise tasks
|
|
22
22
|
│ ├── .scripts/ # Post-generation utilities
|
|
23
|
-
│ │ └── setup-plane.py # Creates
|
|
23
|
+
│ │ └── setup-plane.py # Creates Plane project + ticket_provider block in .project.json
|
|
24
24
|
│ ├── AGENTS.md.jinja # Generated project's agent SSOT
|
|
25
25
|
│ ├── CLAUDE.md # Symlink → AGENTS.md
|
|
26
26
|
│ ├── GEMINI.md # Symlink → AGENTS.md
|
|
@@ -46,7 +46,7 @@ Root-level files describe the template itself. Files in `template/` are what Cop
|
|
|
46
46
|
Only two questions asked: `project_name` and `project_description`. Everything else is derived or automated:
|
|
47
47
|
- `project_slug` derived from project_name
|
|
48
48
|
- `user_name` / `user_skill_level` hardcoded for BMAD config
|
|
49
|
-
-
|
|
49
|
+
- Plane project created via API in post-generation task
|
|
50
50
|
- .gitignore copied from ~/.config/git/ignore
|
|
51
51
|
- git init + initial commit run automatically
|
|
52
52
|
|
|
@@ -55,7 +55,7 @@ Only two questions asked: `project_name` and `project_description`. Everything e
|
|
|
55
55
|
After rendering, Copier automatically:
|
|
56
56
|
1. Copies .gitignore from ~/.config/git/ignore
|
|
57
57
|
2. Makes scripts executable
|
|
58
|
-
3. Runs setup-plane.py (creates
|
|
58
|
+
3. Runs setup-plane.py (creates Plane project, writes the ticket_provider block in .project.json)
|
|
59
59
|
4. Runs git init + git add -A + git commit
|
|
60
60
|
|
|
61
61
|
### BMAD System
|