@unbrained/pm-web 2026.7.11 → 2026.7.17-1

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 (76) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +26 -0
  3. package/dist/app.d.ts +16 -1
  4. package/dist/app.js +53 -3
  5. package/dist/app.js.map +1 -1
  6. package/dist/auth.d.ts +2 -1
  7. package/dist/auth.js +10 -0
  8. package/dist/auth.js.map +1 -1
  9. package/dist/db.js +17 -1
  10. package/dist/db.js.map +1 -1
  11. package/dist/ical.js.map +1 -1
  12. package/dist/index.d.ts +1 -22
  13. package/dist/index.js +2 -3
  14. package/dist/index.js.map +1 -1
  15. package/dist/oidc.d.ts +89 -0
  16. package/dist/oidc.js +305 -0
  17. package/dist/oidc.js.map +1 -0
  18. package/dist/routes/auth.js +3 -15
  19. package/dist/routes/auth.js.map +1 -1
  20. package/dist/routes/github.js +3 -3
  21. package/dist/routes/github.js.map +1 -1
  22. package/dist/routes/oidc.d.ts +4 -0
  23. package/dist/routes/oidc.js +147 -0
  24. package/dist/routes/oidc.js.map +1 -0
  25. package/dist/routes/pm.js +172 -108
  26. package/dist/routes/pm.js.map +1 -1
  27. package/dist/routes/projects.js +9 -2
  28. package/dist/routes/projects.js.map +1 -1
  29. package/dist/server.js +7 -2
  30. package/dist/server.js.map +1 -1
  31. package/dist/services/pm-runner.d.ts +27 -3
  32. package/dist/services/pm-runner.js +188 -54
  33. package/dist/services/pm-runner.js.map +1 -1
  34. package/dist/services/realtime-bus.d.ts +1 -0
  35. package/dist/services/realtime-bus.js +148 -0
  36. package/dist/services/realtime-bus.js.map +1 -0
  37. package/dist/services/sse.d.ts +3 -1
  38. package/dist/services/sse.js +83 -48
  39. package/dist/services/sse.js.map +1 -1
  40. package/manifest.json +1 -1
  41. package/package.json +11 -11
  42. package/public/cookie-consent.js +54 -39
  43. package/public/cookie-settings.html +7 -7
  44. package/public/index.html +32 -26
  45. package/public/legal-notice.html +10 -12
  46. package/public/privacy-policy.html +14 -24
  47. package/public/src/app.js +18 -4
  48. package/public/src/app.js.map +1 -1
  49. package/public/src/app.ts +19 -4
  50. package/public/src/components/modals.js +5 -3
  51. package/public/src/components/modals.js.map +1 -1
  52. package/public/src/components/modals.ts +6 -3
  53. package/public/src/components/toast.js.map +1 -1
  54. package/public/src/cookie-consent.ts +68 -0
  55. package/public/src/i18n/de.json +117 -0
  56. package/public/src/i18n/en.json +117 -0
  57. package/public/src/i18n/es.json +117 -0
  58. package/public/src/i18n/zh.json +117 -0
  59. package/public/src/i18n.js +287 -0
  60. package/public/src/i18n.js.map +1 -0
  61. package/public/src/i18n.ts +306 -0
  62. package/public/src/sw.ts +446 -0
  63. package/public/src/views/auth.js +29 -6
  64. package/public/src/views/auth.js.map +1 -1
  65. package/public/src/views/auth.ts +27 -6
  66. package/public/src/views/graph.js.map +1 -1
  67. package/public/src/views/settings.js +73 -48
  68. package/public/src/views/settings.js.map +1 -1
  69. package/public/src/views/settings.ts +77 -48
  70. package/public/styles.css +2 -0
  71. package/public/sw.js +323 -256
  72. package/public/terms.html +7 -9
  73. package/public/tsconfig.json +1 -1
  74. package/public/tsconfig.scripts.json +18 -0
  75. package/public/tsconfig.sw.json +18 -0
  76. package/sql/schema.sql +16 -0
package/dist/routes/pm.js CHANGED
@@ -1,13 +1,14 @@
1
1
  import { Router } from "express";
2
2
  import { requireAuth } from "../middleware/auth.js";
3
- import { ensureGraphExtension, runPm } from "../services/pm-runner.js";
3
+ import { ensureGraphExtension, runPm, runGetItemAt, PmCliError, EXIT_CODE } from "../services/pm-runner.js";
4
4
  import { boardColumns, filterItemsByQuery } from "../board.js";
5
5
  import { buildIcsCalendar } from "../ical.js";
6
6
  import { verifyProjectAccess } from "./projects.js";
7
- import { addSSEClient, broadcastProjectEvent, setupSSEHeaders, broadcastPresence, updateClientView, getProjectPresence } from "../services/sse.js";
7
+ import { addSSEClient, broadcastProjectEvent, setupSSEHeaders, updateClientView, getProjectPresence } from "../services/sse.js";
8
8
  import { v4 as uuidv4 } from "uuid";
9
9
  import neo4j from "neo4j-driver";
10
10
  import { routeParam } from "./route-params.js";
11
+ import { pool } from "../db.js";
11
12
  // Singleton Neo4j driver — reused across sync calls to avoid per-call connection overhead.
12
13
  let _neo4jDriver = null;
13
14
  let _neo4jDriverKey = "";
@@ -29,7 +30,7 @@ const router = Router({ mergeParams: true });
29
30
  router.use(requireAuth);
30
31
  const pendingGraphSyncs = new Map();
31
32
  router.use(async (req, res, next) => {
32
- if (["GET", "HEAD", "OPTIONS"].includes(req.method)) {
33
+ if (["GET", "HEAD", "OPTIONS"].includes(req.method) || (req.method === "PATCH" && req.path.startsWith("/presence/"))) {
33
34
  next();
34
35
  return;
35
36
  }
@@ -232,8 +233,8 @@ async function syncGraphToNeo4j(graph, projectKey) {
232
233
  return { syncedNodes: graph.nodes.length, syncedRelationships: graph.relationships.length };
233
234
  }
234
235
  async function syncProjectGraph(project) {
235
- const extensionGraph = pmGraphExtensionGraphForProject(project);
236
- const graph = extensionGraph.graph ?? fallbackGraphForProject(project.ownerUserId, project.slug);
236
+ const extensionGraph = await pmGraphExtensionGraphForProject(project);
237
+ const graph = extensionGraph.graph ?? await fallbackGraphForProject(project.ownerUserId, project.slug);
237
238
  return syncGraphToNeo4j(graph, graphProjectKey(project));
238
239
  }
239
240
  function scheduleGraphSync(projectId, project, reason) {
@@ -281,8 +282,8 @@ function broadcastDependencyEvent(projectId, kind, data) {
281
282
  function itemsFromListAll(parsed) {
282
283
  return ((parsed?.items) ?? []);
283
284
  }
284
- function fallbackGraphForProject(ownerUserId, slug) {
285
- const itemsResult = runPm({
285
+ async function fallbackGraphForProject(ownerUserId, slug) {
286
+ const itemsResult = await runPm({
286
287
  args: ["list-all"],
287
288
  userId: ownerUserId,
288
289
  slug,
@@ -295,12 +296,12 @@ function fallbackGraphForProject(ownerUserId, slug) {
295
296
  // Avoid N+1 subprocess calls by using only the embedded data.
296
297
  return graphFromItems(items, new Map());
297
298
  }
298
- function pmGraphExtensionGraphForProject(project) {
299
- const provision = ensureGraphExtension(project.ownerUserId, project.slug);
299
+ async function pmGraphExtensionGraphForProject(project) {
300
+ const provision = await ensureGraphExtension(project.ownerUserId, project.slug);
300
301
  if (!provision.ok) {
301
302
  return { error: provision.error };
302
303
  }
303
- const extensionResult = runPm({
304
+ const extensionResult = await runPm({
304
305
  args: ["pm-graph", "export", "--json"],
305
306
  userId: project.ownerUserId,
306
307
  slug: project.slug,
@@ -341,7 +342,7 @@ router.get("/schema", async (req, res) => {
341
342
  res.status(404).json({ error: "Project not found" });
342
343
  return;
343
344
  }
344
- const result = runPm({ args: ["contracts", "--json"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
345
+ const result = await runPm({ args: ["contracts", "--json"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
345
346
  const contracts = result.ok && result.parsed ? result.parsed : null;
346
347
  const rt = contracts?.["runtime_schema"];
347
348
  res.json({
@@ -375,7 +376,7 @@ router.get("/list", async (req, res) => {
375
376
  args.push("--release", release);
376
377
  if (assignee)
377
378
  args.push("--assignee", assignee);
378
- const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
379
+ const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
379
380
  res.json(result.ok ? (result.parsed || {}) : { error: result.stderr, items: [] });
380
381
  });
381
382
  // GET /api/projects/:projectId/pm/list-all
@@ -391,7 +392,7 @@ router.get("/list-all", async (req, res) => {
391
392
  args.push("--type", type);
392
393
  if (limit)
393
394
  args.push("--limit", limit);
394
- const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
395
+ const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
395
396
  res.json(result.ok ? (result.parsed || {}) : { error: result.stderr, items: [] });
396
397
  });
397
398
  // GET /api/projects/:projectId/pm/board
@@ -403,12 +404,12 @@ router.get("/board", async (req, res) => {
403
404
  res.status(404).json({ error: "Project not found" });
404
405
  return;
405
406
  }
406
- const contracts = runPm({ args: ["contracts", "--json"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
407
+ const contracts = await runPm({ args: ["contracts", "--json"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
407
408
  const rt = contracts.ok && contracts.parsed
408
409
  ? contracts.parsed["runtime_schema"]
409
410
  : undefined;
410
411
  const statuses = Array.isArray(rt?.["statuses"]) ? rt["statuses"] : [];
411
- const listed = runPm({ args: ["list-all", "--json"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
412
+ const listed = await runPm({ args: ["list-all", "--json"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
412
413
  if (!listed.ok) {
413
414
  res.json({ error: listed.stderr, columns: [] });
414
415
  return;
@@ -425,7 +426,7 @@ router.get("/search", async (req, res) => {
425
426
  return;
426
427
  }
427
428
  const query = String(req.query["q"] ?? "");
428
- const listed = runPm({ args: ["list-all", "--json", "--include-body"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
429
+ const listed = await runPm({ args: ["list-all", "--json", "--include-body"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
429
430
  if (!listed.ok) {
430
431
  res.json({ error: listed.stderr, items: [] });
431
432
  return;
@@ -508,7 +509,7 @@ router.post("/create", async (req, res) => {
508
509
  args.push("--outcome", outcome);
509
510
  if (definitionOfReady)
510
511
  args.push("--definition-of-ready", definitionOfReady);
511
- const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
512
+ const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
512
513
  if (!result.ok) {
513
514
  res.status(400).json({ error: result.stderr || "Failed to create item" });
514
515
  return;
@@ -528,7 +529,7 @@ router.get("/get/:itemId", async (req, res) => {
528
529
  res.status(404).json({ error: "Project not found" });
529
530
  return;
530
531
  }
531
- const result = runPm({
532
+ const result = await runPm({
532
533
  args: ["get", routeParam(req, "itemId")],
533
534
  userId: project.ownerUserId,
534
535
  slug: project.slug,
@@ -540,6 +541,56 @@ router.get("/get/:itemId", async (req, res) => {
540
541
  }
541
542
  res.json(result.parsed || {});
542
543
  });
544
+ // GET /api/projects/:projectId/pm/at/:itemId/:ref
545
+ // Point-in-time item view: reconstruct a verified historical item state at a
546
+ // one-based version number or ISO timestamp via the pm CLI SDK `getItemAt`
547
+ // projection (the same replay kernel that powers `pm get --at`). The read is
548
+ // mutation-free and lock-free, so it never interferes with concurrent writers.
549
+ //
550
+ // 200 — reconstructed item state at the requested ref
551
+ // 400 — invalid ref, or version/timestamp outside the available history range
552
+ // 404 — unknown item, or item with no recorded history
553
+ router.get("/at/:itemId/:ref", async (req, res) => {
554
+ const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
555
+ if (!project) {
556
+ res.status(404).json({ error: "Project not found" });
557
+ return;
558
+ }
559
+ const itemId = routeParam(req, "itemId");
560
+ const ref = routeParam(req, "ref");
561
+ try {
562
+ const result = await runGetItemAt(project.ownerUserId, project.slug, itemId, ref);
563
+ // Mirror the `pm get` envelope so existing clients can reuse the same shape:
564
+ // the reconstructed metadata is exposed as `item` (with `body` inlined),
565
+ // alongside the provenance fields `reconstructed` / `as_of_version` /
566
+ // `as_of_timestamp` / `history_length` and the resolved `target`.
567
+ const item = { ...result.document.metadata, body: result.document.body };
568
+ res.json({
569
+ item,
570
+ reconstructed: result.reconstructed,
571
+ as_of_version: result.as_of_version,
572
+ as_of_timestamp: result.as_of_timestamp,
573
+ history_length: result.history_length,
574
+ target: result.target,
575
+ });
576
+ }
577
+ catch (err) {
578
+ if (err instanceof PmCliError) {
579
+ if (err.exitCode === EXIT_CODE.NOT_FOUND) {
580
+ res.status(404).json({ error: err.message });
581
+ }
582
+ else if (err.exitCode === EXIT_CODE.USAGE) {
583
+ res.status(400).json({ error: err.message });
584
+ }
585
+ else {
586
+ res.status(500).json({ error: err.message });
587
+ }
588
+ return;
589
+ }
590
+ console.error("Point-in-time item read failed:", err);
591
+ res.status(500).json({ error: "Failed to reconstruct historical item state" });
592
+ }
593
+ });
543
594
  // PATCH /api/projects/:projectId/pm/update/:itemId
544
595
  router.patch("/update/:itemId", async (req, res) => {
545
596
  const project = await verifyProject(req.user.userId, routeParam(req, "projectId"));
@@ -575,7 +626,7 @@ router.patch("/update/:itemId", async (req, res) => {
575
626
  // Type can be set but must use --type
576
627
  if (body.type)
577
628
  args.push("--type", body.type);
578
- const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
629
+ const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
579
630
  if (!result.ok) {
580
631
  res.status(400).json({ error: result.stderr || "Failed to update item" });
581
632
  return;
@@ -600,7 +651,7 @@ router.post("/close/:itemId", async (req, res) => {
600
651
  res.status(400).json({ error: "Close reason is required" });
601
652
  return;
602
653
  }
603
- const result = runPm({
654
+ const result = await runPm({
604
655
  args: ["close", routeParam(req, "itemId"), reason.trim()],
605
656
  userId: project.ownerUserId,
606
657
  slug: project.slug,
@@ -624,7 +675,7 @@ router.delete("/delete/:itemId", async (req, res) => {
624
675
  res.status(404).json({ error: "Project not found" });
625
676
  return;
626
677
  }
627
- const result = runPm({
678
+ const result = await runPm({
628
679
  args: ["delete", routeParam(req, "itemId"), "--yes"],
629
680
  userId: project.ownerUserId,
630
681
  slug: project.slug,
@@ -652,7 +703,7 @@ router.post("/comments/:itemId", async (req, res) => {
652
703
  res.status(400).json({ error: "Comment text is required" });
653
704
  return;
654
705
  }
655
- const result = runPm({
706
+ const result = await runPm({
656
707
  args: ["comments", routeParam(req, "itemId"), text.trim()],
657
708
  userId: project.ownerUserId,
658
709
  slug: project.slug,
@@ -671,7 +722,7 @@ router.get("/comments/:itemId", async (req, res) => {
671
722
  res.status(404).json({ error: "Project not found" });
672
723
  return;
673
724
  }
674
- const result = runPm({
725
+ const result = await runPm({
675
726
  args: ["comments", routeParam(req, "itemId")],
676
727
  userId: project.ownerUserId,
677
728
  slug: project.slug,
@@ -686,7 +737,7 @@ router.get("/notes/:itemId", async (req, res) => {
686
737
  res.status(404).json({ error: "Project not found" });
687
738
  return;
688
739
  }
689
- const result = runPm({
740
+ const result = await runPm({
690
741
  args: ["notes", routeParam(req, "itemId")],
691
742
  userId: project.ownerUserId,
692
743
  slug: project.slug,
@@ -706,7 +757,7 @@ router.post("/notes/:itemId", async (req, res) => {
706
757
  res.status(400).json({ error: "Note text is required" });
707
758
  return;
708
759
  }
709
- const result = runPm({
760
+ const result = await runPm({
710
761
  args: ["notes", routeParam(req, "itemId"), text.trim()],
711
762
  userId: project.ownerUserId,
712
763
  slug: project.slug,
@@ -728,7 +779,7 @@ router.get("/context", async (req, res) => {
728
779
  const { depth } = req.query;
729
780
  const validDepths = ["brief", "standard", "deep"];
730
781
  const resolvedDepth = validDepths.includes(depth) ? depth : "standard";
731
- const result = runPm({
782
+ const result = await runPm({
732
783
  args: ["context", "--depth", resolvedDepth],
733
784
  userId: project.ownerUserId,
734
785
  slug: project.slug,
@@ -747,7 +798,7 @@ router.get("/activity", async (req, res) => {
747
798
  const args = ["activity"];
748
799
  if (limit)
749
800
  args.push("--limit", limit);
750
- const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
801
+ const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
751
802
  res.json(result.ok ? (result.parsed || {}) : { activity: [] });
752
803
  });
753
804
  // GET /api/projects/:projectId/pm/stats
@@ -757,7 +808,7 @@ router.get("/stats", async (req, res) => {
757
808
  res.status(404).json({ error: "Project not found" });
758
809
  return;
759
810
  }
760
- const result = runPm({
811
+ const result = await runPm({
761
812
  args: ["stats"],
762
813
  userId: project.ownerUserId,
763
814
  slug: project.slug,
@@ -772,7 +823,7 @@ router.get("/aggregate", async (req, res) => {
772
823
  res.status(404).json({ error: "Project not found" });
773
824
  return;
774
825
  }
775
- const result = runPm({
826
+ const result = await runPm({
776
827
  args: ["aggregate"],
777
828
  userId: project.ownerUserId,
778
829
  slug: project.slug,
@@ -794,7 +845,7 @@ router.post("/search", async (req, res) => {
794
845
  }
795
846
  const validModes = ["keyword", "semantic", "hybrid"];
796
847
  const safeMode = validModes.includes(mode || "") ? mode : "hybrid";
797
- const result = runPm({
848
+ const result = await runPm({
798
849
  args: ["search", "--mode", safeMode, ...query.trim().split(/\s+/)],
799
850
  userId: project.ownerUserId,
800
851
  slug: project.slug,
@@ -816,7 +867,7 @@ router.get("/calendar", async (req, res) => {
816
867
  res.status(404).json({ error: "Project not found" });
817
868
  return;
818
869
  }
819
- const result = runPm({
870
+ const result = await runPm({
820
871
  args: ["calendar", "--view", "month"],
821
872
  userId: project.ownerUserId,
822
873
  slug: project.slug,
@@ -835,7 +886,7 @@ router.get("/calendar.ics", async (req, res) => {
835
886
  res.status(404).json({ error: "Project not found" });
836
887
  return;
837
888
  }
838
- const listed = runPm({
889
+ const listed = await runPm({
839
890
  args: ["list-all", "--json"],
840
891
  userId: project.ownerUserId,
841
892
  slug: project.slug,
@@ -873,7 +924,7 @@ router.get("/health", async (req, res) => {
873
924
  res.status(404).json({ error: "Project not found" });
874
925
  return;
875
926
  }
876
- const result = runPm({
927
+ const result = await runPm({
877
928
  args: ["health"],
878
929
  userId: project.ownerUserId,
879
930
  slug: project.slug,
@@ -893,7 +944,7 @@ router.post("/append/:itemId", async (req, res) => {
893
944
  res.status(400).json({ error: "Text is required" });
894
945
  return;
895
946
  }
896
- const result = runPm({
947
+ const result = await runPm({
897
948
  args: ["append", routeParam(req, "itemId"), text.trim()],
898
949
  userId: project.ownerUserId,
899
950
  slug: project.slug,
@@ -913,7 +964,7 @@ router.get("/history/:itemId", async (req, res) => {
913
964
  res.status(404).json({ error: "Project not found" });
914
965
  return;
915
966
  }
916
- const result = runPm({
967
+ const result = await runPm({
917
968
  args: ["history", routeParam(req, "itemId")],
918
969
  userId: project.ownerUserId,
919
970
  slug: project.slug,
@@ -928,7 +979,7 @@ router.get("/deps/:itemId", async (req, res) => {
928
979
  res.status(404).json({ error: "Project not found" });
929
980
  return;
930
981
  }
931
- const result = runPm({
982
+ const result = await runPm({
932
983
  args: ["deps", routeParam(req, "itemId")],
933
984
  userId: project.ownerUserId,
934
985
  slug: project.slug,
@@ -949,7 +1000,7 @@ router.post("/deps/:itemId", async (req, res) => {
949
1000
  return;
950
1001
  }
951
1002
  const depRel = normalizeDependencyKind(rel);
952
- const result = runPm({
1003
+ const result = await runPm({
953
1004
  args: ["update", routeParam(req, "itemId"), "--dep", `id=${targetId.trim()},kind=${depRel}`],
954
1005
  userId: project.ownerUserId,
955
1006
  slug: project.slug,
@@ -982,7 +1033,7 @@ router.delete("/deps/:itemId", async (req, res) => {
982
1033
  }
983
1034
  const depRel = normalizeDependencyKind(rel || "relates_to");
984
1035
  const selector = `id=${targetId.trim()},kind=${depRel}`;
985
- const result = runPm({
1036
+ const result = await runPm({
986
1037
  args: ["update", routeParam(req, "itemId"), "--dep-remove", selector],
987
1038
  userId: project.ownerUserId,
988
1039
  slug: project.slug,
@@ -1014,7 +1065,7 @@ router.post("/rel", async (req, res) => {
1014
1065
  return;
1015
1066
  }
1016
1067
  const depRel = normalizeDependencyKind(relType || "relates_to");
1017
- const result = runPm({
1068
+ const result = await runPm({
1018
1069
  args: ["update", from.trim(), "--dep", `id=${to.trim()},kind=${depRel}`],
1019
1070
  userId: project.ownerUserId,
1020
1071
  slug: project.slug,
@@ -1047,7 +1098,7 @@ router.delete("/rel", async (req, res) => {
1047
1098
  }
1048
1099
  const depRel = normalizeDependencyKind(relType || "relates_to");
1049
1100
  const selector = `id=${to.trim()},kind=${depRel}`;
1050
- const result = runPm({
1101
+ const result = await runPm({
1051
1102
  args: ["update", from.trim(), "--dep-remove", selector, "--message", `Remove ${depRel} dependency on ${to.trim()}`],
1052
1103
  userId: project.ownerUserId,
1053
1104
  slug: project.slug,
@@ -1073,7 +1124,7 @@ router.get("/graph", async (req, res) => {
1073
1124
  res.status(404).json({ error: "Project not found" });
1074
1125
  return;
1075
1126
  }
1076
- const extensionGraph = pmGraphExtensionGraphForProject(project);
1127
+ const extensionGraph = await pmGraphExtensionGraphForProject(project);
1077
1128
  if (extensionGraph.graph) {
1078
1129
  res.json({
1079
1130
  ok: true,
@@ -1085,7 +1136,7 @@ router.get("/graph", async (req, res) => {
1085
1136
  try {
1086
1137
  res.json({
1087
1138
  ok: true,
1088
- graph: fallbackGraphForProject(project.ownerUserId, project.slug),
1139
+ graph: await fallbackGraphForProject(project.ownerUserId, project.slug),
1089
1140
  extensionAvailable: false,
1090
1141
  extensionError: extensionGraph.error,
1091
1142
  });
@@ -1140,7 +1191,7 @@ router.get("/graph/neighbors/:nodeId", async (req, res) => {
1140
1191
  res.status(400).json({ error: "nodeId is required" });
1141
1192
  return;
1142
1193
  }
1143
- const result = runPm({
1194
+ const result = await runPm({
1144
1195
  args: ["pm-graph", "neighbors", nodeId, "--json"],
1145
1196
  userId: project.ownerUserId,
1146
1197
  slug: project.slug,
@@ -1171,7 +1222,7 @@ router.post("/graph/query", async (req, res) => {
1171
1222
  res.status(400).json({ error: "cypher query is required" });
1172
1223
  return;
1173
1224
  }
1174
- const result = runPm({
1225
+ const result = await runPm({
1175
1226
  args: ["pm-graph", "query", cypher.trim(), "--json"],
1176
1227
  userId: project.ownerUserId,
1177
1228
  slug: project.slug,
@@ -1196,7 +1247,7 @@ router.get("/learnings/:itemId", async (req, res) => {
1196
1247
  res.status(404).json({ error: "Project not found" });
1197
1248
  return;
1198
1249
  }
1199
- const result = runPm({
1250
+ const result = await runPm({
1200
1251
  args: ["learnings", routeParam(req, "itemId")],
1201
1252
  userId: project.ownerUserId,
1202
1253
  slug: project.slug,
@@ -1216,7 +1267,7 @@ router.post("/learnings/:itemId", async (req, res) => {
1216
1267
  res.status(400).json({ error: "Learning text is required" });
1217
1268
  return;
1218
1269
  }
1219
- const result = runPm({
1270
+ const result = await runPm({
1220
1271
  args: ["learnings", routeParam(req, "itemId"), text.trim()],
1221
1272
  userId: project.ownerUserId,
1222
1273
  slug: project.slug,
@@ -1235,7 +1286,7 @@ router.post("/claim/:itemId", async (req, res) => {
1235
1286
  res.status(404).json({ error: "Project not found" });
1236
1287
  return;
1237
1288
  }
1238
- const result = runPm({
1289
+ const result = await runPm({
1239
1290
  args: ["claim", routeParam(req, "itemId")],
1240
1291
  userId: project.ownerUserId,
1241
1292
  slug: project.slug,
@@ -1255,7 +1306,7 @@ router.post("/release/:itemId", async (req, res) => {
1255
1306
  res.status(404).json({ error: "Project not found" });
1256
1307
  return;
1257
1308
  }
1258
- const result = runPm({
1309
+ const result = await runPm({
1259
1310
  args: ["release", routeParam(req, "itemId")],
1260
1311
  userId: project.ownerUserId,
1261
1312
  slug: project.slug,
@@ -1275,7 +1326,7 @@ router.post("/start-task/:itemId", async (req, res) => {
1275
1326
  res.status(404).json({ error: "Project not found" });
1276
1327
  return;
1277
1328
  }
1278
- const result = runPm({
1329
+ const result = await runPm({
1279
1330
  args: ["start-task", routeParam(req, "itemId")],
1280
1331
  userId: project.ownerUserId,
1281
1332
  slug: project.slug,
@@ -1295,7 +1346,7 @@ router.post("/pause-task/:itemId", async (req, res) => {
1295
1346
  res.status(404).json({ error: "Project not found" });
1296
1347
  return;
1297
1348
  }
1298
- const result = runPm({
1349
+ const result = await runPm({
1299
1350
  args: ["pause-task", routeParam(req, "itemId")],
1300
1351
  userId: project.ownerUserId,
1301
1352
  slug: project.slug,
@@ -1315,7 +1366,7 @@ router.get("/tests/:itemId", async (req, res) => {
1315
1366
  res.status(404).json({ error: "Project not found" });
1316
1367
  return;
1317
1368
  }
1318
- const result = runPm({
1369
+ const result = await runPm({
1319
1370
  args: ["test", routeParam(req, "itemId")],
1320
1371
  userId: project.ownerUserId,
1321
1372
  slug: project.slug,
@@ -1338,7 +1389,7 @@ router.post("/tests/:itemId", async (req, res) => {
1338
1389
  const args = ["test", routeParam(req, "itemId"), "--add", "--command", command.trim()];
1339
1390
  if (description)
1340
1391
  args.push("--description", description.trim());
1341
- const result = runPm({
1392
+ const result = await runPm({
1342
1393
  args,
1343
1394
  userId: project.ownerUserId,
1344
1395
  slug: project.slug,
@@ -1357,7 +1408,7 @@ router.get("/dedupe-audit", async (req, res) => {
1357
1408
  res.status(404).json({ error: "Project not found" });
1358
1409
  return;
1359
1410
  }
1360
- const result = runPm({
1411
+ const result = await runPm({
1361
1412
  args: ["dedupe-audit"],
1362
1413
  userId: project.ownerUserId,
1363
1414
  slug: project.slug,
@@ -1372,7 +1423,7 @@ router.get("/validate", async (req, res) => {
1372
1423
  res.status(404).json({ error: "Project not found" });
1373
1424
  return;
1374
1425
  }
1375
- const result = runPm({
1426
+ const result = await runPm({
1376
1427
  args: ["validate"],
1377
1428
  userId: project.ownerUserId,
1378
1429
  slug: project.slug,
@@ -1392,7 +1443,7 @@ router.post("/restore/:itemId", async (req, res) => {
1392
1443
  res.status(400).json({ error: "Restore target (timestamp or version) is required" });
1393
1444
  return;
1394
1445
  }
1395
- const result = runPm({
1446
+ const result = await runPm({
1396
1447
  args: ["restore", routeParam(req, "itemId"), target.trim()],
1397
1448
  userId: project.ownerUserId,
1398
1449
  slug: project.slug,
@@ -1417,7 +1468,7 @@ router.post("/close-task/:itemId", async (req, res) => {
1417
1468
  res.status(400).json({ error: "Close reason is required" });
1418
1469
  return;
1419
1470
  }
1420
- const result = runPm({
1471
+ const result = await runPm({
1421
1472
  args: ["close-task", routeParam(req, "itemId"), reason.trim()],
1422
1473
  userId: project.ownerUserId,
1423
1474
  slug: project.slug,
@@ -1440,7 +1491,7 @@ router.post("/reindex", async (req, res) => {
1440
1491
  const { mode = "keyword" } = req.body;
1441
1492
  const validModes = ["keyword", "semantic", "hybrid"];
1442
1493
  const safeMode = validModes.includes(mode) ? mode : "keyword";
1443
- const result = runPm({
1494
+ const result = await runPm({
1444
1495
  args: ["reindex", "--mode", safeMode],
1445
1496
  userId: project.ownerUserId,
1446
1497
  slug: project.slug,
@@ -1460,7 +1511,7 @@ router.post("/normalize", async (req, res) => {
1460
1511
  res.status(404).json({ error: "Project not found" });
1461
1512
  return;
1462
1513
  }
1463
- const result = runPm({
1514
+ const result = await runPm({
1464
1515
  args: ["normalize"],
1465
1516
  userId: project.ownerUserId,
1466
1517
  slug: project.slug,
@@ -1475,7 +1526,7 @@ router.get("/comments-audit", async (req, res) => {
1475
1526
  res.status(404).json({ error: "Project not found" });
1476
1527
  return;
1477
1528
  }
1478
- const result = runPm({
1529
+ const result = await runPm({
1479
1530
  args: ["comments-audit"],
1480
1531
  userId: project.ownerUserId,
1481
1532
  slug: project.slug,
@@ -1499,7 +1550,7 @@ router.post("/files/:itemId", async (req, res) => {
1499
1550
  if (scope)
1500
1551
  addVal += `,scope=${scope}`;
1501
1552
  const args = ["files", routeParam(req, "itemId"), "--add", addVal];
1502
- const result = runPm({
1553
+ const result = await runPm({
1503
1554
  args,
1504
1555
  userId: project.ownerUserId,
1505
1556
  slug: project.slug,
@@ -1519,7 +1570,7 @@ router.get("/files/:itemId", async (req, res) => {
1519
1570
  res.status(404).json({ error: "Project not found" });
1520
1571
  return;
1521
1572
  }
1522
- const result = runPm({
1573
+ const result = await runPm({
1523
1574
  args: ["files", routeParam(req, "itemId")],
1524
1575
  userId: project.ownerUserId,
1525
1576
  slug: project.slug,
@@ -1534,7 +1585,7 @@ router.get("/guide", async (req, res) => {
1534
1585
  res.status(404).json({ error: "Project not found" });
1535
1586
  return;
1536
1587
  }
1537
- const result = runPm({
1588
+ const result = await runPm({
1538
1589
  args: ["guide"],
1539
1590
  userId: project.ownerUserId,
1540
1591
  slug: project.slug,
@@ -1549,7 +1600,7 @@ router.get("/guide/:topicId", async (req, res) => {
1549
1600
  res.status(404).json({ error: "Project not found" });
1550
1601
  return;
1551
1602
  }
1552
- const result = runPm({
1603
+ const result = await runPm({
1553
1604
  args: ["guide", routeParam(req, "topicId")],
1554
1605
  userId: project.ownerUserId,
1555
1606
  slug: project.slug,
@@ -1575,7 +1626,7 @@ router.get("/export", async (req, res) => {
1575
1626
  }
1576
1627
  const format = req.query["format"] || "json";
1577
1628
  // Use --full --include-body to get the richest available list-level metadata
1578
- const result = runPm({
1629
+ const result = await runPm({
1579
1630
  args: ["list-all", "--limit", "10000", "--full", "--include-body"],
1580
1631
  userId: project.ownerUserId,
1581
1632
  slug: project.slug,
@@ -1715,7 +1766,7 @@ router.post("/import", async (req, res) => {
1715
1766
  args.push("--body", item.body);
1716
1767
  if (item.parent)
1717
1768
  args.push("--parent", item.parent);
1718
- const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
1769
+ const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
1719
1770
  if (result.ok && result.parsed) {
1720
1771
  const parsed = result.parsed;
1721
1772
  created.push(parsed.item?.id || `item[${i}]`);
@@ -1772,7 +1823,7 @@ router.post("/update-many", async (req, res) => {
1772
1823
  if (body[key])
1773
1824
  args.push(flag, body[key]);
1774
1825
  }
1775
- const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
1826
+ const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
1776
1827
  if (!result.ok) {
1777
1828
  res.status(400).json({ error: result.stderr || "update-many failed" });
1778
1829
  return;
@@ -1814,7 +1865,7 @@ router.post("/close-many", async (req, res) => {
1814
1865
  if (body[key])
1815
1866
  listArgs.push(flag, body[key]);
1816
1867
  }
1817
- const listResult = runPm({ args: listArgs, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
1868
+ const listResult = await runPm({ args: listArgs, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
1818
1869
  if (!listResult.ok) {
1819
1870
  res.status(400).json({ error: listResult.stderr || "Failed to list items for close-many" });
1820
1871
  return;
@@ -1834,7 +1885,7 @@ router.post("/close-many", async (req, res) => {
1834
1885
  const closeArgs = targetStatus === "canceled"
1835
1886
  ? ["update", itemId, "--status", "canceled"]
1836
1887
  : ["close", itemId, reason];
1837
- const closeResult = runPm({ args: closeArgs, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
1888
+ const closeResult = await runPm({ args: closeArgs, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
1838
1889
  if (closeResult.ok) {
1839
1890
  rows.push({ id: itemId, status: "ok" });
1840
1891
  closedCount++;
@@ -1866,7 +1917,7 @@ router.get("/docs/:itemId", async (req, res) => {
1866
1917
  res.status(404).json({ error: "Project not found" });
1867
1918
  return;
1868
1919
  }
1869
- const result = runPm({
1920
+ const result = await runPm({
1870
1921
  args: ["docs", routeParam(req, "itemId")],
1871
1922
  userId: project.ownerUserId,
1872
1923
  slug: project.slug,
@@ -1901,7 +1952,7 @@ router.post("/docs/:itemId", async (req, res) => {
1901
1952
  res.status(400).json({ error: "path, remove, or validatePaths is required" });
1902
1953
  return;
1903
1954
  }
1904
- const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
1955
+ const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
1905
1956
  if (!result.ok) {
1906
1957
  res.status(400).json({ error: result.stderr || "Failed to update docs" });
1907
1958
  return;
@@ -1924,7 +1975,7 @@ router.post("/test-all", async (req, res) => {
1924
1975
  args.push("--limit", body.limit);
1925
1976
  if (body.timeout)
1926
1977
  args.push("--timeout", body.timeout);
1927
- const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
1978
+ const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
1928
1979
  if (!result.ok) {
1929
1980
  res.status(400).json({ error: result.stderr || "test-all failed" });
1930
1981
  return;
@@ -1944,7 +1995,7 @@ router.get("/test-runs", async (req, res) => {
1944
1995
  args.push("--status", status);
1945
1996
  if (limit)
1946
1997
  args.push("--limit", limit);
1947
- const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
1998
+ const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
1948
1999
  res.json(result.ok ? (result.parsed || {}) : { runs: [] });
1949
2000
  });
1950
2001
  // POST /api/projects/:projectId/pm/gc
@@ -1954,7 +2005,7 @@ router.post("/gc", async (req, res) => {
1954
2005
  res.status(404).json({ error: "Project not found" });
1955
2006
  return;
1956
2007
  }
1957
- const result = runPm({ args: ["gc"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
2008
+ const result = await runPm({ args: ["gc"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
1958
2009
  res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
1959
2010
  });
1960
2011
  // GET /api/projects/:projectId/pm/templates
@@ -1964,7 +2015,7 @@ router.get("/templates", async (req, res) => {
1964
2015
  res.status(404).json({ error: "Project not found" });
1965
2016
  return;
1966
2017
  }
1967
- const result = runPm({ args: ["templates", "list"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
2018
+ const result = await runPm({ args: ["templates", "list"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
1968
2019
  res.json(result.ok ? (result.parsed || {}) : { templates: [] });
1969
2020
  });
1970
2021
  // GET /api/projects/:projectId/pm/templates/:name
@@ -1974,7 +2025,7 @@ router.get("/templates/:name", async (req, res) => {
1974
2025
  res.status(404).json({ error: "Project not found" });
1975
2026
  return;
1976
2027
  }
1977
- const result = runPm({ args: ["templates", "show", routeParam(req, "name")], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
2028
+ const result = await runPm({ args: ["templates", "show", routeParam(req, "name")], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
1978
2029
  res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
1979
2030
  });
1980
2031
  // GET /api/projects/:projectId/pm/config
@@ -1984,7 +2035,7 @@ router.get("/config", async (req, res) => {
1984
2035
  res.status(404).json({ error: "Project not found" });
1985
2036
  return;
1986
2037
  }
1987
- const result = runPm({ args: ["config", "project", "list"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
2038
+ const result = await runPm({ args: ["config", "project", "list"], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
1988
2039
  res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
1989
2040
  });
1990
2041
  // GET /api/projects/:projectId/pm/config/:key
@@ -1995,7 +2046,7 @@ router.get("/config/:key", async (req, res) => {
1995
2046
  return;
1996
2047
  }
1997
2048
  const key = routeParam(req, "key");
1998
- const result = runPm({ args: ["config", "project", "get", key], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
2049
+ const result = await runPm({ args: ["config", "project", "get", key], userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
1999
2050
  res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
2000
2051
  });
2001
2052
  // PATCH /api/projects/:projectId/pm/config/:key
@@ -2014,7 +2065,7 @@ router.patch("/config/:key", async (req, res) => {
2014
2065
  args.push("--policy", body.policy);
2015
2066
  if (body.format)
2016
2067
  args.push("--format", body.format);
2017
- const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
2068
+ const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
2018
2069
  res.json(result.ok ? (result.parsed || {}) : { error: result.stderr });
2019
2070
  });
2020
2071
  // ─── List status shortcut routes ───
@@ -2044,7 +2095,7 @@ function buildListShortcutRoute(pmCommand) {
2044
2095
  args.push("--sprint", sprint);
2045
2096
  if (release)
2046
2097
  args.push("--release", release);
2047
- const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
2098
+ const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
2048
2099
  res.json(result.ok ? (result.parsed || {}) : { items: [] });
2049
2100
  };
2050
2101
  }
@@ -2080,7 +2131,7 @@ router.post("/plan", async (req, res) => {
2080
2131
  args.push("--priority", priority);
2081
2132
  if (body)
2082
2133
  args.push("--body", body);
2083
- const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
2134
+ const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
2084
2135
  if (!result.ok) {
2085
2136
  res.status(400).json({ error: result.stderr || "Failed to create plan" });
2086
2137
  return;
@@ -2098,7 +2149,7 @@ router.get("/plan/:planId", async (req, res) => {
2098
2149
  res.status(404).json({ error: "Project not found" });
2099
2150
  return;
2100
2151
  }
2101
- const result = runPm({
2152
+ const result = await runPm({
2102
2153
  args: ["plan", "show", routeParam(req, "planId"), "--depth", "standard"],
2103
2154
  userId: project.ownerUserId,
2104
2155
  slug: project.slug,
@@ -2123,7 +2174,7 @@ router.patch("/plan/:planId", async (req, res) => {
2123
2174
  args.push("--title", title.trim());
2124
2175
  if (description !== undefined)
2125
2176
  args.push("--description", description);
2126
- const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
2177
+ const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
2127
2178
  if (!result.ok) {
2128
2179
  res.status(400).json({ error: result.stderr || "Failed to update plan" });
2129
2180
  return;
@@ -2141,7 +2192,7 @@ router.delete("/plan/:planId", async (req, res) => {
2141
2192
  res.status(404).json({ error: "Project not found" });
2142
2193
  return;
2143
2194
  }
2144
- const result = runPm({
2195
+ const result = await runPm({
2145
2196
  args: ["delete", routeParam(req, "planId")],
2146
2197
  userId: project.ownerUserId,
2147
2198
  slug: project.slug,
@@ -2174,7 +2225,7 @@ router.post("/plan/:planId/steps", async (req, res) => {
2174
2225
  args.push("--description", description);
2175
2226
  if (dependsOn)
2176
2227
  args.push("--depends-on", dependsOn);
2177
- const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
2228
+ const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
2178
2229
  if (!result.ok) {
2179
2230
  res.status(400).json({ error: result.stderr || "Failed to add step" });
2180
2231
  return;
@@ -2198,7 +2249,7 @@ router.patch("/plan/:planId/steps/:stepRef", async (req, res) => {
2198
2249
  args.push("--title", title);
2199
2250
  if (description)
2200
2251
  args.push("--description", description);
2201
- const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
2252
+ const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
2202
2253
  if (!result.ok) {
2203
2254
  res.status(400).json({ error: result.stderr || "Failed to update step" });
2204
2255
  return;
@@ -2216,7 +2267,7 @@ router.post("/plan/:planId/steps/:stepRef/complete", async (req, res) => {
2216
2267
  res.status(404).json({ error: "Project not found" });
2217
2268
  return;
2218
2269
  }
2219
- const result = runPm({
2270
+ const result = await runPm({
2220
2271
  args: ["plan", "complete-step", routeParam(req, "planId"), routeParam(req, "stepRef")],
2221
2272
  userId: project.ownerUserId,
2222
2273
  slug: project.slug,
@@ -2244,7 +2295,7 @@ router.post("/plan/:planId/steps/:stepRef/block", async (req, res) => {
2244
2295
  res.status(400).json({ error: "Block reason is required" });
2245
2296
  return;
2246
2297
  }
2247
- const result = runPm({
2298
+ const result = await runPm({
2248
2299
  args: ["plan", "block-step", routeParam(req, "planId"), routeParam(req, "stepRef"), "--step-blocked-reason", reason.trim()],
2249
2300
  userId: project.ownerUserId,
2250
2301
  slug: project.slug,
@@ -2267,7 +2318,7 @@ router.delete("/plan/:planId/steps/:stepRef", async (req, res) => {
2267
2318
  res.status(404).json({ error: "Project not found" });
2268
2319
  return;
2269
2320
  }
2270
- const result = runPm({
2321
+ const result = await runPm({
2271
2322
  args: ["plan", "remove-step", routeParam(req, "planId"), routeParam(req, "stepRef")],
2272
2323
  userId: project.ownerUserId,
2273
2324
  slug: project.slug,
@@ -2290,7 +2341,7 @@ router.post("/plan/:planId/approve", async (req, res) => {
2290
2341
  res.status(404).json({ error: "Project not found" });
2291
2342
  return;
2292
2343
  }
2293
- const result = runPm({
2344
+ const result = await runPm({
2294
2345
  args: ["plan", "approve", routeParam(req, "planId")],
2295
2346
  userId: project.ownerUserId,
2296
2347
  slug: project.slug,
@@ -2321,7 +2372,7 @@ router.post("/plan/:planId/materialize", async (req, res) => {
2321
2372
  args.push("--materialize-parent", materializeParent);
2322
2373
  if (steps)
2323
2374
  args.push("--steps", steps);
2324
- const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
2375
+ const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
2325
2376
  if (!result.ok) {
2326
2377
  res.status(400).json({ error: result.stderr || "Failed to materialize plan" });
2327
2378
  return;
@@ -2344,7 +2395,7 @@ router.post("/plan/:planId/steps/:stepRef/reorder", async (req, res) => {
2344
2395
  res.status(400).json({ error: "reorderTo (new order integer) is required" });
2345
2396
  return;
2346
2397
  }
2347
- const result = runPm({
2398
+ const result = await runPm({
2348
2399
  args: ["plan", "reorder-step", routeParam(req, "planId"), routeParam(req, "stepRef"), String(reorderTo)],
2349
2400
  userId: project.ownerUserId,
2350
2401
  slug: project.slug,
@@ -2379,7 +2430,7 @@ router.post("/plan/:planId/link", async (req, res) => {
2379
2430
  args.push("--link-note", linkNote);
2380
2431
  if (promoteToItemDep === "true")
2381
2432
  args.push("--promote-to-item-dep");
2382
- const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
2433
+ const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
2383
2434
  if (!result.ok) {
2384
2435
  res.status(400).json({ error: result.stderr || "Failed to link plan" });
2385
2436
  return;
@@ -2405,7 +2456,7 @@ router.delete("/plan/:planId/link", async (req, res) => {
2405
2456
  const args = ["plan", "unlink", routeParam(req, "planId"), "--link", link.trim()];
2406
2457
  if (linkKind)
2407
2458
  args.push("--link-kind", linkKind);
2408
- const result = runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
2459
+ const result = await runPm({ args, userId: project.ownerUserId, slug: project.slug, jsonOutput: true });
2409
2460
  if (!result.ok) {
2410
2461
  res.status(400).json({ error: result.stderr || "Failed to unlink plan" });
2411
2462
  return;
@@ -2426,7 +2477,7 @@ router.get("/upgrade", async (req, res) => {
2426
2477
  res.status(404).json({ error: "Project not found" });
2427
2478
  return;
2428
2479
  }
2429
- const result = runPm({
2480
+ const result = await runPm({
2430
2481
  args: ["upgrade", "--dry-run", "--packages-only"],
2431
2482
  userId: project.ownerUserId,
2432
2483
  slug: project.slug,
@@ -2441,7 +2492,7 @@ router.post("/upgrade", async (req, res) => {
2441
2492
  return;
2442
2493
  }
2443
2494
  // Only allow package upgrades from the web UI — never self-upgrade the CLI binary.
2444
- const result = runPm({
2495
+ const result = await runPm({
2445
2496
  args: ["upgrade", "--packages-only"],
2446
2497
  userId: project.ownerUserId,
2447
2498
  slug: project.slug,
@@ -2453,18 +2504,25 @@ router.post("/upgrade", async (req, res) => {
2453
2504
  }
2454
2505
  res.json(result.parsed || { ok: true });
2455
2506
  });
2456
- // ─── Presence endpoints ───
2457
- // GET /api/projects/:projectId/pm/presence
2458
- router.get("/presence", async (req, res) => {
2459
- const projectId = routeParam(req, "projectId");
2460
- res.json({ users: getProjectPresence(projectId) });
2461
- });
2462
2507
  // PATCH /api/projects/:projectId/pm/presence/:clientId
2463
2508
  router.patch("/presence/:clientId", async (req, res) => {
2509
+ const projectId = routeParam(req, "projectId");
2510
+ const access = await verifyProjectAccess(req.user.userId, projectId);
2511
+ if (!access) {
2512
+ res.status(404).json({ error: "Project not found" });
2513
+ return;
2514
+ }
2464
2515
  const { clientId } = req.params;
2465
2516
  const { view } = req.body;
2466
- if (view)
2467
- updateClientView(clientId, view);
2517
+ if (!view || !/^[a-z][a-z0-9-]{0,63}$/.test(view)) {
2518
+ res.status(400).json({ error: "Invalid view" });
2519
+ return;
2520
+ }
2521
+ const updated = updateClientView(clientId, req.user.userId, projectId, view);
2522
+ if (!updated) {
2523
+ res.status(404).json({ error: "Presence session not found" });
2524
+ return;
2525
+ }
2468
2526
  res.json({ ok: true });
2469
2527
  });
2470
2528
  // ─── SSE endpoint ───
@@ -2482,13 +2540,21 @@ router.get("/events", async (req, res) => {
2482
2540
  res.status(500).json({ error: "Failed to verify project for real-time sync" });
2483
2541
  return;
2484
2542
  }
2485
- setupSSEHeaders(res);
2486
2543
  const clientId = uuidv4();
2487
2544
  const projectId = routeParam(req, "projectId");
2488
2545
  const userId = req.user.userId;
2489
- // Client sends display name as query param; fall back to email
2490
- const displayName = String(req.query["dn"] ?? req.user.email ?? userId);
2546
+ let displayName;
2547
+ try {
2548
+ const userResult = await pool.query("SELECT display_name FROM pm_users WHERE id = $1", [userId]);
2549
+ displayName = userResult.rows[0]?.display_name?.trim() || "Project member";
2550
+ }
2551
+ catch (error) {
2552
+ console.error("SSE presence lookup failed", error instanceof Error ? error.name : typeof error);
2553
+ res.status(500).json({ error: "Failed to initialize real-time presence" });
2554
+ return;
2555
+ }
2491
2556
  const currentView = String(req.query["view"] ?? "items");
2557
+ setupSSEHeaders(res);
2492
2558
  const unsubscribe = addSSEClient({
2493
2559
  id: clientId,
2494
2560
  projectId,
@@ -2502,8 +2568,6 @@ router.get("/events", async (req, res) => {
2502
2568
  const heartbeat = setInterval(() => {
2503
2569
  try {
2504
2570
  res.write(": heartbeat\n\n");
2505
- // Re-broadcast presence on heartbeat to keep list fresh
2506
- broadcastPresence(projectId);
2507
2571
  }
2508
2572
  catch {
2509
2573
  clearInterval(heartbeat);