@delorenj/pjangler 1.2.18 → 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.
Files changed (48) hide show
  1. package/dist/index.js +244 -6
  2. package/dist/mcp-server.js +245 -7
  3. package/package.json +8 -2
  4. package/templates/commonproject/AGENTS.md +3 -3
  5. package/templates/commonproject/README.md +11 -12
  6. package/templates/commonproject/copier.yml +8 -27
  7. package/templates/commonproject/template/.project.json.jinja +9 -13
  8. package/templates/hermes-agent/README.md +9 -9
  9. package/templates/hermes-agent/config.example.toml +1 -66
  10. package/templates/hermes-agent/copier.yml +4 -3
  11. package/templates/hermes-agent/docs/architecture.md +9 -12
  12. package/templates/hermes-agent/docs/fleet-control-plane/README.md +1 -1
  13. package/templates/hermes-agent/docs/operations.md +6 -5
  14. package/templates/hermes-agent/docs/sentinel/README.md +9 -7
  15. package/templates/hermes-agent/docs/sentinel/architecture.md +1 -2
  16. package/templates/hermes-agent/docs/sentinel/development.md +12 -13
  17. package/templates/hermes-agent/docs/sentinel/providers.md +34 -15
  18. package/templates/hermes-agent/install-local.sh +15 -14
  19. package/templates/hermes-agent/runtime-scaffold/README.md +1 -1
  20. package/templates/hermes-agent/runtime-scaffold/bloodbank-consumer.py +46 -14
  21. package/templates/hermes-agent/runtime-scaffold/memories/MEMORY.md +2 -2
  22. package/templates/hermes-agent/scripts/fleet-sync.sh +1 -64
  23. package/templates/hermes-agent/template/.gitignore.jinja +0 -2
  24. package/templates/hermes-agent/template/.runtime-scaffold/README.md +1 -1
  25. package/templates/hermes-agent/template/.runtime-scaffold/bloodbank-consumer.py +43 -9
  26. package/templates/hermes-agent/template/.runtime-scaffold/memories/MEMORY.md +2 -2
  27. package/templates/hermes-agent/template/.scripts/01-config.sh +0 -1
  28. package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +0 -9
  29. package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +3 -22
  30. package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +0 -22
  31. package/templates/hermes-agent/template/.scripts/40-plane.sh +51 -0
  32. package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +59 -21
  33. package/templates/hermes-agent/template/.scripts/60-bloodbank.sh +2 -1
  34. package/templates/hermes-agent/template/.scripts/70-systemd.sh +1 -10
  35. package/templates/hermes-agent/template/.scripts/_lib.sh +3 -61
  36. package/templates/hermes-agent/template/.scripts/config.example.toml +0 -5
  37. package/templates/hermes-agent/template/.scripts/heartbeat.sh +22 -66
  38. package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +5 -9
  39. package/templates/hermes-agent/template/.scripts/providers/linear.sh +176 -0
  40. package/templates/hermes-agent/template/.scripts/providers/plane.sh +9 -29
  41. package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md +2 -2
  42. package/templates/hermes-agent/template/.scripts/sentinel/docs/continuous-ticket-orchestration.md +1 -33
  43. package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +1 -27
  44. package/templates/hermes-agent/template/SOUL.md.jinja +19 -21
  45. package/templates/hermes-agent/template/role.yaml.jinja +35 -4
  46. package/templates/hermes-agent/tests/test_bloodbank_consumer_contract.py +138 -0
  47. package/templates/hermes-agent/docs/bloodbank-gateway.md +0 -57
  48. 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 = YAML.parse(readFileSync3(path, "utf8"));
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, YAML.stringify(registry, { lineWidth: 0 }), "utf8");
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 YAML2 from "yaml";
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 = YAML2.parse(raw);
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([
@@ -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 = YAML.parse(readFileSync3(path, "utf8"));
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, YAML.stringify(registry, { lineWidth: 0 }), "utf8");
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 YAML2 from "yaml";
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 = YAML2.parse(raw);
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.18",
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/links ticket provider board + ticket_provider block in .project.json
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
- - Ticket board created or linked via post-generation task
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/links the ticket board, writes the ticket_provider block in .project.json)
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