syntaur 0.70.0 → 0.71.0

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 (136) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dashboard/dist/assets/{_basePickBy-DKk6tHtk.js → _basePickBy-BfpzwNC3.js} +1 -1
  3. package/dashboard/dist/assets/{_baseUniq-DM-f7DWz.js → _baseUniq-wFEHBTCT.js} +1 -1
  4. package/dashboard/dist/assets/{arc-ZBlA3YdV.js → arc-Bdcw4sCg.js} +1 -1
  5. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-BUmvtGTF.js → architectureDiagram-2XIMDMQ5-BessFAtJ.js} +1 -1
  6. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-B3qxWK6s.js → blockDiagram-WCTKOSBZ-CmjyyZPp.js} +1 -1
  7. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-BEq_UJO-.js → c4Diagram-IC4MRINW-dT6p8cwc.js} +1 -1
  8. package/dashboard/dist/assets/channel-BOguQ5S4.js +1 -0
  9. package/dashboard/dist/assets/{chunk-4BX2VUAB-C-Y9ryMm.js → chunk-4BX2VUAB-BlY-k8DY.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-55IACEB6-CGdtbsjw.js → chunk-55IACEB6-CtQ0p0jb.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-FMBD7UC4-DllxJhUp.js → chunk-FMBD7UC4-DEevt_Yh.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-JSJVCQXG-jjMM8O5F.js → chunk-JSJVCQXG-CpjLL7Nh.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-KX2RTZJC-B_6BPltQ.js → chunk-KX2RTZJC-BWNRms_o.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-NQ4KR5QH-D0hJiXHp.js → chunk-NQ4KR5QH-qo2zbylv.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-QZHKN3VN-BCWo4hLS.js → chunk-QZHKN3VN-DRvojtzc.js} +1 -1
  16. package/dashboard/dist/assets/{chunk-WL4C6EOR-DH_jEAwg.js → chunk-WL4C6EOR-A40iPU5N.js} +1 -1
  17. package/dashboard/dist/assets/classDiagram-VBA2DB6C-BLSHooma.js +1 -0
  18. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-BLSHooma.js +1 -0
  19. package/dashboard/dist/assets/clone-iSEXw5tA.js +1 -0
  20. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-5ld00TOH.js → cose-bilkent-S5V4N54A-CYaO7nsk.js} +1 -1
  21. package/dashboard/dist/assets/{dagre-KLK3FWXG-Cnu6eQWy.js → dagre-KLK3FWXG-NX3inwVZ.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-E7M64L7V-_CBBKNP-.js → diagram-E7M64L7V-D9cJwhQW.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-IFDJBPK2-DE6WjIb1.js → diagram-IFDJBPK2-Bo5sFrMg.js} +1 -1
  24. package/dashboard/dist/assets/{diagram-P4PSJMXO-DpW7UzNK.js → diagram-P4PSJMXO-DfRH0XU7.js} +1 -1
  25. package/dashboard/dist/assets/{erDiagram-INFDFZHY-BD2409fE.js → erDiagram-INFDFZHY-BgP1bwgw.js} +1 -1
  26. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-1p0khhFI.js → flowDiagram-PKNHOUZH-C8Wws1ci.js} +1 -1
  27. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-B2zFyA4s.js → ganttDiagram-A5KZAMGK-BgJhUuj8.js} +1 -1
  28. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-bH-4YH7h.js → gitGraphDiagram-K3NZZRJ6-CI9FgCgo.js} +1 -1
  29. package/dashboard/dist/assets/{graph-BT24B6iQ.js → graph-B_7GyeiA.js} +1 -1
  30. package/dashboard/dist/assets/{index-Cxqr3rQB.js → index-CbndxFlO.js} +3 -3
  31. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-CMzP4Hcg.js → infoDiagram-LFFYTUFH-BEtZcRxI.js} +1 -1
  32. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-DMEmFC7M.js → ishikawaDiagram-PHBUUO56-0o9Bksuc.js} +1 -1
  33. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-CAtzYQUm.js → journeyDiagram-4ABVD52K-RyRU3as7.js} +1 -1
  34. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-d1JVbtvX.js → kanban-definition-K7BYSVSG-hLrHHizB.js} +1 -1
  35. package/dashboard/dist/assets/{layout-BVuI38I_.js → layout-w458wydd.js} +1 -1
  36. package/dashboard/dist/assets/{linear-Bc8PGMbp.js → linear-CnRlR5_w.js} +1 -1
  37. package/dashboard/dist/assets/{mermaid.core-UtwFYLNj.js → mermaid.core-DJ_9ty0Y.js} +4 -4
  38. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-DHc5RCDj.js → mindmap-definition-YRQLILUH-C7TIzJVC.js} +1 -1
  39. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-9anIsdIA.js → pieDiagram-SKSYHLDU-BZ9ysteM.js} +1 -1
  40. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-FZ0D9HnU.js → quadrantDiagram-337W2JSQ-DpaMVSO1.js} +1 -1
  41. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-BkjCvH_u.js → requirementDiagram-Z7DCOOCP-EXFcskZZ.js} +1 -1
  42. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-DzqwYHDo.js → sankeyDiagram-WA2Y5GQK-BdImdXkY.js} +1 -1
  43. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-BW4g5Ao-.js → sequenceDiagram-2WXFIKYE-CWN3faYf.js} +1 -1
  44. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-D0tKU3Z0.js → stateDiagram-RAJIS63D-DIQdn-BF.js} +1 -1
  45. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-1u62oWzq.js +1 -0
  46. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-Bu269QDX.js → timeline-definition-YZTLITO2-DMTIpUOE.js} +1 -1
  47. package/dashboard/dist/assets/{treemap-KZPCXAKY-BGu_rrva.js → treemap-KZPCXAKY-Y2AH2GB7.js} +1 -1
  48. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-Cx_n5FRZ.js → vennDiagram-LZ73GAT5-BF4bJK-l.js} +1 -1
  49. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-BOJsKV_W.js → xychartDiagram-JWTSCODW-DH-tOBC5.js} +1 -1
  50. package/dashboard/dist/index.html +1 -1
  51. package/dist/dashboard/server.js +1384 -540
  52. package/dist/dashboard/server.js.map +1 -1
  53. package/dist/index.js +2769 -1535
  54. package/dist/index.js.map +1 -1
  55. package/dist/launch/index.js +594 -109
  56. package/dist/launch/index.js.map +1 -1
  57. package/package.json +1 -1
  58. package/platforms/claude-code/.claude-plugin/plugin.json +1 -1
  59. package/platforms/claude-code/hooks/enforce-boundaries.sh +49 -16
  60. package/platforms/claude-code/hooks/session-cleanup.sh +4 -2
  61. package/platforms/claude-code/skills/add-memory/SKILL.md +3 -2
  62. package/platforms/claude-code/skills/add-resource/SKILL.md +3 -2
  63. package/platforms/claude-code/skills/bundle-worktree/SKILL.md +3 -3
  64. package/platforms/claude-code/skills/capture-artifacts/SKILL.md +2 -2
  65. package/platforms/claude-code/skills/claim-resource/SKILL.md +3 -3
  66. package/platforms/claude-code/skills/clear-assignment/SKILL.md +9 -27
  67. package/platforms/claude-code/skills/complete-assignment/SKILL.md +3 -3
  68. package/platforms/claude-code/skills/create-assignment/SKILL.md +2 -2
  69. package/platforms/claude-code/skills/extend-resource/SKILL.md +2 -2
  70. package/platforms/claude-code/skills/grab-assignment/SKILL.md +17 -12
  71. package/platforms/claude-code/skills/grab-bundle/SKILL.md +10 -6
  72. package/platforms/claude-code/skills/log-progress/SKILL.md +6 -4
  73. package/platforms/claude-code/skills/plan-assignment/SKILL.md +3 -3
  74. package/platforms/claude-code/skills/plan-bundle/SKILL.md +4 -3
  75. package/platforms/claude-code/skills/replan/SKILL.md +9 -6
  76. package/platforms/claude-code/skills/resume-session/SKILL.md +14 -8
  77. package/platforms/claude-code/skills/save-session-summary/SKILL.md +4 -4
  78. package/platforms/claude-code/skills/set-workspace/SKILL.md +9 -6
  79. package/platforms/claude-code/skills/syntaur-protocol/SKILL.md +9 -7
  80. package/platforms/claude-code/skills/syntaur-protocol/references/file-ownership.md +1 -1
  81. package/platforms/claude-code/skills/syntaur-worktree/SKILL.md +11 -7
  82. package/platforms/codex/.codex-plugin/plugin.json +1 -1
  83. package/platforms/codex/scripts/enforce-boundaries.sh +74 -12
  84. package/platforms/codex/scripts/session-cleanup.sh +20 -13
  85. package/platforms/codex/skills/add-memory/SKILL.md +3 -2
  86. package/platforms/codex/skills/add-resource/SKILL.md +3 -2
  87. package/platforms/codex/skills/bundle-worktree/SKILL.md +3 -3
  88. package/platforms/codex/skills/capture-artifacts/SKILL.md +2 -2
  89. package/platforms/codex/skills/claim-resource/SKILL.md +3 -3
  90. package/platforms/codex/skills/clear-assignment/SKILL.md +9 -27
  91. package/platforms/codex/skills/complete-assignment/SKILL.md +3 -3
  92. package/platforms/codex/skills/create-assignment/SKILL.md +2 -2
  93. package/platforms/codex/skills/extend-resource/SKILL.md +2 -2
  94. package/platforms/codex/skills/grab-assignment/SKILL.md +17 -12
  95. package/platforms/codex/skills/grab-bundle/SKILL.md +10 -6
  96. package/platforms/codex/skills/log-progress/SKILL.md +6 -4
  97. package/platforms/codex/skills/plan-assignment/SKILL.md +3 -3
  98. package/platforms/codex/skills/plan-bundle/SKILL.md +4 -3
  99. package/platforms/codex/skills/replan/SKILL.md +9 -6
  100. package/platforms/codex/skills/resume-session/SKILL.md +14 -8
  101. package/platforms/codex/skills/save-session-summary/SKILL.md +4 -4
  102. package/platforms/codex/skills/set-workspace/SKILL.md +9 -6
  103. package/platforms/codex/skills/syntaur-protocol/SKILL.md +9 -7
  104. package/platforms/codex/skills/syntaur-protocol/references/file-ownership.md +1 -1
  105. package/platforms/codex/skills/syntaur-worktree/SKILL.md +11 -7
  106. package/platforms/hermes/plugins/syntaur/__init__.py +29 -3
  107. package/platforms/hermes/plugins/syntaur/__pycache__/__init__.cpython-312.pyc +0 -0
  108. package/platforms/hermes/plugins/syntaur/__pycache__/boundary.cpython-312.pyc +0 -0
  109. package/platforms/hermes/plugins/syntaur/boundary.py +18 -9
  110. package/platforms/pi/extensions/syntaur/index.ts +84 -29
  111. package/skills/add-memory/SKILL.md +3 -2
  112. package/skills/add-resource/SKILL.md +3 -2
  113. package/skills/bundle-worktree/SKILL.md +3 -3
  114. package/skills/capture-artifacts/SKILL.md +2 -2
  115. package/skills/claim-resource/SKILL.md +3 -3
  116. package/skills/clear-assignment/SKILL.md +9 -27
  117. package/skills/complete-assignment/SKILL.md +3 -3
  118. package/skills/create-assignment/SKILL.md +2 -2
  119. package/skills/extend-resource/SKILL.md +2 -2
  120. package/skills/grab-assignment/SKILL.md +17 -12
  121. package/skills/grab-bundle/SKILL.md +10 -6
  122. package/skills/log-progress/SKILL.md +6 -4
  123. package/skills/plan-assignment/SKILL.md +3 -3
  124. package/skills/plan-bundle/SKILL.md +4 -3
  125. package/skills/replan/SKILL.md +9 -6
  126. package/skills/resume-session/SKILL.md +14 -8
  127. package/skills/save-session-summary/SKILL.md +4 -4
  128. package/skills/set-workspace/SKILL.md +9 -6
  129. package/skills/syntaur-protocol/SKILL.md +9 -7
  130. package/skills/syntaur-protocol/references/file-ownership.md +1 -1
  131. package/skills/syntaur-worktree/SKILL.md +11 -7
  132. package/dashboard/dist/assets/channel-fypxffzQ.js +0 -1
  133. package/dashboard/dist/assets/classDiagram-VBA2DB6C-1KnjQvtL.js +0 -1
  134. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-1KnjQvtL.js +0 -1
  135. package/dashboard/dist/assets/clone-CKMabBhS.js +0 -1
  136. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-DxWQhjNO.js +0 -1
@@ -36,9 +36,9 @@ function expandHome(p) {
36
36
  return p;
37
37
  }
38
38
  function syntaurRoot() {
39
- const override = process.env.SYNTAUR_HOME;
40
- if (override && override.length > 0) {
41
- return resolve(expandHome(override));
39
+ const override3 = process.env.SYNTAUR_HOME;
40
+ if (override3 && override3.length > 0) {
41
+ return resolve(expandHome(override3));
42
42
  }
43
43
  return resolve(homedir(), ".syntaur");
44
44
  }
@@ -452,6 +452,7 @@ function parseAssignmentFrontmatter(fileContent) {
452
452
  planApproval: parsePlanApproval(frontmatter),
453
453
  parked: getField2("parked") === "true",
454
454
  reviewRequested: getField2("reviewRequested") === "true",
455
+ reworkRequested: getField2("reworkRequested") === "true",
455
456
  implementationStarted: getField2("implementationStarted") === "true",
456
457
  override: parseOverride(frontmatter),
457
458
  facts: parseFactsMap(frontmatter),
@@ -634,11 +635,11 @@ function updatePlanApproval(fileContent, approval) {
634
635
  approval === null ? null : { file: approval.file, digest: approval.digest, by: approval.by, at: approval.at }
635
636
  );
636
637
  }
637
- function updateOverride(fileContent, override) {
638
+ function updateOverride(fileContent, override3) {
638
639
  return updateNestedBlock(
639
640
  fileContent,
640
641
  "override",
641
- override === null ? null : { status: override.status, source: override.source, reason: override.reason, at: override.at }
642
+ override3 === null ? null : { status: override3.status, source: override3.source, reason: override3.reason, at: override3.at }
642
643
  );
643
644
  }
644
645
  function updateFactsMap(fileContent, name, value) {
@@ -1271,6 +1272,7 @@ function parseAssignmentFull(fileContent) {
1271
1272
  disposition: getField(fm, "disposition"),
1272
1273
  parked: getField(fm, "parked") === "true",
1273
1274
  reviewRequested: getField(fm, "reviewRequested") === "true",
1275
+ reworkRequested: getField(fm, "reworkRequested") === "true",
1274
1276
  implementationStarted: getField(fm, "implementationStarted") === "true",
1275
1277
  planApproval: (() => {
1276
1278
  const file = getNestedField(fm, "planApproval", "file");
@@ -2737,7 +2739,9 @@ var init_derive_config = __esm({
2737
2739
  },
2738
2740
  {
2739
2741
  phase: "review",
2740
- when: "acAllChecked:true OR reviewRequested:true",
2742
+ // Rework (a new `implement` stage after `review`) must leave review even
2743
+ // when ACs stay checked — hence `AND NOT reworkRequested` (stage-fact-status-bridge).
2744
+ when: "(acAllChecked:true OR reviewRequested:true) AND NOT reworkRequested:true",
2741
2745
  next: "Complete, or address review feedback"
2742
2746
  }
2743
2747
  ],
@@ -2804,6 +2808,7 @@ var init_fields = __esm({
2804
2808
  blocked: { kind: "bool" },
2805
2809
  parked: { kind: "bool" },
2806
2810
  reviewrequested: { kind: "bool", get: (i) => i["reviewRequested"] },
2811
+ reworkrequested: { kind: "bool", get: (i) => i["reworkRequested"] },
2807
2812
  pinned: { kind: "bool" }
2808
2813
  };
2809
2814
  }
@@ -3597,6 +3602,7 @@ function queryFieldNames(declarations) {
3597
3602
  "blocked",
3598
3603
  "parked",
3599
3604
  "reviewRequested",
3605
+ "reworkRequested",
3600
3606
  "pinned"
3601
3607
  ];
3602
3608
  const custom = [];
@@ -3635,6 +3641,7 @@ var init_fact_registry = __esm({
3635
3641
  blocked: { kind: "bool" },
3636
3642
  parked: { kind: "bool" },
3637
3643
  reviewrequested: { kind: "bool", get: (i) => i["reviewRequested"] },
3644
+ reworkrequested: { kind: "bool", get: (i) => i["reworkRequested"] },
3638
3645
  pinned: { kind: "bool" }
3639
3646
  };
3640
3647
  }
@@ -5633,7 +5640,7 @@ function compiledWhen(registry, when) {
5633
5640
  return pred;
5634
5641
  }
5635
5642
  function deriveDimensions(input) {
5636
- const { facts, derive, currentStatus, terminalStatuses, knownStatusIds, override } = input;
5643
+ const { facts, derive, currentStatus, terminalStatuses, knownStatusIds, override: override3 } = input;
5637
5644
  const registry = input.registry ?? DERIVE_FIELDS;
5638
5645
  if (terminalStatuses.has(currentStatus)) return null;
5639
5646
  const ctx = { now: 0 };
@@ -5667,8 +5674,8 @@ function deriveDimensions(input) {
5667
5674
  derivedStatus = phase;
5668
5675
  }
5669
5676
  let status = derivedStatus;
5670
- if (override && override.status && !terminalStatuses.has(override.status) && knownStatusIds.has(override.status)) {
5671
- status = override.status;
5677
+ if (override3 && override3.status && !terminalStatuses.has(override3.status) && knownStatusIds.has(override3.status)) {
5678
+ status = override3.status;
5672
5679
  }
5673
5680
  return { phase, disposition, derivedStatus, status, nextAction };
5674
5681
  }
@@ -5954,6 +5961,18 @@ async function resolveAssignmentById(projectsDir, assignmentsDir2, id) {
5954
5961
  }
5955
5962
  return standaloneMatch ?? projectMatch ?? null;
5956
5963
  }
5964
+ async function resolveAssignmentBySlug(projectsDir, assignmentsDir2, projectSlug, assignmentSlug) {
5965
+ const path = projectSlug ? resolve9(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md") : resolve9(assignmentsDir2, assignmentSlug, "assignment.md");
5966
+ if (!await fileExists(path)) return { exists: false, id: null };
5967
+ try {
5968
+ const content = await readFile7(path, "utf-8");
5969
+ const [fm] = extractFrontmatter2(content);
5970
+ const id = getField(fm, "id");
5971
+ return { exists: true, id: id ?? null };
5972
+ } catch {
5973
+ return { exists: false, id: null };
5974
+ }
5975
+ }
5957
5976
  var init_assignment_resolver = __esm({
5958
5977
  "src/utils/assignment-resolver.ts"() {
5959
5978
  "use strict";
@@ -6389,6 +6408,7 @@ async function computeFactsDetailed(input) {
6389
6408
  blocked: frontmatter.blockedReason !== null,
6390
6409
  parked: frontmatter.parked,
6391
6410
  reviewRequested: frontmatter.reviewRequested,
6411
+ reworkRequested: frontmatter.reworkRequested,
6392
6412
  pinned: frontmatter.override !== null
6393
6413
  };
6394
6414
  const attestations = [];
@@ -7443,6 +7463,139 @@ var init_help = __esm({
7443
7463
  }
7444
7464
  });
7445
7465
 
7466
+ // src/db/engagement-schema.ts
7467
+ var ENGAGEMENT_SCHEMA_VERSION, ENGAGEMENT_DDL;
7468
+ var init_engagement_schema = __esm({
7469
+ "src/db/engagement-schema.ts"() {
7470
+ "use strict";
7471
+ ENGAGEMENT_SCHEMA_VERSION = "1";
7472
+ ENGAGEMENT_DDL = `
7473
+ CREATE TABLE IF NOT EXISTS engagement (
7474
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
7475
+ session_id TEXT NOT NULL,
7476
+ assignment_id TEXT,
7477
+ project_slug TEXT,
7478
+ assignment_slug TEXT,
7479
+ stage TEXT NOT NULL DEFAULT 'implement',
7480
+ started_at TEXT NOT NULL,
7481
+ ended_at TEXT,
7482
+ tokens_at_open TEXT,
7483
+ tokens_at_close TEXT,
7484
+ close_reason TEXT
7485
+ );
7486
+ CREATE UNIQUE INDEX IF NOT EXISTS one_active_per_session
7487
+ ON engagement(session_id) WHERE ended_at IS NULL;
7488
+ CREATE INDEX IF NOT EXISTS idx_engagement_session ON engagement(session_id);
7489
+ CREATE INDEX IF NOT EXISTS idx_engagement_assignment ON engagement(assignment_id);
7490
+ CREATE INDEX IF NOT EXISTS idx_engagement_slug ON engagement(project_slug, assignment_slug);
7491
+ `;
7492
+ }
7493
+ });
7494
+
7495
+ // src/db/engagement-backfill.ts
7496
+ import { readdirSync, readFileSync, statSync as statSync2 } from "fs";
7497
+ import { resolve as resolve13 } from "path";
7498
+ function safeReaddir(dir) {
7499
+ try {
7500
+ return readdirSync(dir, { withFileTypes: true });
7501
+ } catch {
7502
+ return [];
7503
+ }
7504
+ }
7505
+ function readAssignmentId(assignmentMdPath) {
7506
+ try {
7507
+ const fm = parseAssignmentFrontmatter(readFileSync(assignmentMdPath, "utf-8"));
7508
+ return fm.id || null;
7509
+ } catch {
7510
+ return null;
7511
+ }
7512
+ }
7513
+ function transcriptMtimeIso(path) {
7514
+ if (!path) return null;
7515
+ try {
7516
+ return statSync2(path).mtime.toISOString();
7517
+ } catch {
7518
+ return null;
7519
+ }
7520
+ }
7521
+ function projectKey(projectSlug, assignmentSlug) {
7522
+ return JSON.stringify([projectSlug, assignmentSlug]);
7523
+ }
7524
+ function buildSlugIdMaps() {
7525
+ const root = syntaurRoot();
7526
+ const project = /* @__PURE__ */ new Map();
7527
+ const standalone = /* @__PURE__ */ new Map();
7528
+ const projectsDir = resolve13(root, "projects");
7529
+ for (const proj of safeReaddir(projectsDir)) {
7530
+ if (!proj.isDirectory()) continue;
7531
+ const asgRoot = resolve13(projectsDir, proj.name, "assignments");
7532
+ for (const asg of safeReaddir(asgRoot)) {
7533
+ if (!asg.isDirectory()) continue;
7534
+ const id = readAssignmentId(resolve13(asgRoot, asg.name, "assignment.md"));
7535
+ if (id) project.set(projectKey(proj.name, asg.name), id);
7536
+ }
7537
+ }
7538
+ const standaloneDir = resolve13(root, "assignments");
7539
+ for (const asg of safeReaddir(standaloneDir)) {
7540
+ if (!asg.isDirectory()) continue;
7541
+ const id = readAssignmentId(resolve13(standaloneDir, asg.name, "assignment.md"));
7542
+ if (id) standalone.set(asg.name, id);
7543
+ }
7544
+ return { project, standalone };
7545
+ }
7546
+ function backfillEngagements(db5) {
7547
+ const { project, standalone } = buildSlugIdMaps();
7548
+ const sessions = db5.prepare(
7549
+ `SELECT session_id, project_slug, assignment_slug, status, started, ended,
7550
+ transcript_path, updated_at
7551
+ FROM sessions`
7552
+ ).all();
7553
+ const insert = db5.prepare(
7554
+ `INSERT INTO engagement
7555
+ (session_id, assignment_id, project_slug, assignment_slug, stage,
7556
+ started_at, ended_at, tokens_at_open, tokens_at_close, close_reason)
7557
+ VALUES
7558
+ (@session_id, @assignment_id, @project_slug, @assignment_slug, 'implement',
7559
+ @started_at, @ended_at, NULL, NULL, @close_reason)`
7560
+ );
7561
+ const counts = { backfilled: 0, attributed: 0, unattributed: 0 };
7562
+ for (const s of sessions) {
7563
+ let assignmentId = null;
7564
+ if (s.project_slug && s.assignment_slug) {
7565
+ assignmentId = project.get(projectKey(s.project_slug, s.assignment_slug)) ?? null;
7566
+ } else if (!s.project_slug && s.assignment_slug) {
7567
+ assignmentId = standalone.get(s.assignment_slug) ?? null;
7568
+ }
7569
+ const isActive = s.status === "active";
7570
+ let endedAt = null;
7571
+ let closeReason = null;
7572
+ if (!isActive) {
7573
+ endedAt = s.ended ?? transcriptMtimeIso(s.transcript_path) ?? s.updated_at ?? s.started;
7574
+ closeReason = s.status === "completed" ? "completed" : "abandoned";
7575
+ }
7576
+ insert.run({
7577
+ session_id: s.session_id,
7578
+ assignment_id: assignmentId,
7579
+ project_slug: s.project_slug,
7580
+ assignment_slug: s.assignment_slug,
7581
+ started_at: s.started,
7582
+ ended_at: endedAt,
7583
+ close_reason: closeReason
7584
+ });
7585
+ counts.backfilled++;
7586
+ if (assignmentId) counts.attributed++;
7587
+ else counts.unattributed++;
7588
+ }
7589
+ return counts;
7590
+ }
7591
+ var init_engagement_backfill = __esm({
7592
+ "src/db/engagement-backfill.ts"() {
7593
+ "use strict";
7594
+ init_paths();
7595
+ init_frontmatter();
7596
+ }
7597
+ });
7598
+
7446
7599
  // src/dashboard/session-db.ts
7447
7600
  var session_db_exports = {};
7448
7601
  __export(session_db_exports, {
@@ -7454,18 +7607,23 @@ __export(session_db_exports, {
7454
7607
  resetSessionDb: () => resetSessionDb
7455
7608
  });
7456
7609
  import Database2 from "better-sqlite3";
7457
- import { resolve as resolve13 } from "path";
7610
+ import { resolve as resolve14 } from "path";
7458
7611
  import { readdir as readdir8 } from "fs/promises";
7459
7612
  function initSessionDb(dbPath) {
7460
7613
  if (db2) return db2;
7461
- const finalPath = dbPath ?? resolve13(syntaurRoot(), "syntaur.db");
7614
+ const finalPath = dbPath ?? resolve14(syntaurRoot(), "syntaur.db");
7462
7615
  db2 = new Database2(finalPath);
7463
7616
  db2.pragma("journal_mode = WAL");
7464
7617
  db2.exec(SCHEMA_SQL2);
7618
+ db2.exec(ENGAGEMENT_DDL);
7465
7619
  db2.prepare("INSERT OR IGNORE INTO meta (key, value) VALUES (?, ?)").run(
7466
7620
  "schema_version",
7467
7621
  SCHEMA_VERSION
7468
7622
  );
7623
+ db2.prepare("INSERT OR IGNORE INTO meta (key, value) VALUES (?, ?)").run(
7624
+ "engagement_schema_version",
7625
+ ENGAGEMENT_SCHEMA_VERSION
7626
+ );
7469
7627
  const database = db2;
7470
7628
  const runMigrations = database.transaction(() => {
7471
7629
  const vBeforeV2 = database.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get()?.value;
@@ -7592,9 +7750,43 @@ function initSessionDb(dbPath) {
7592
7750
  UPDATE meta SET value = '5' WHERE key = 'schema_version';
7593
7751
  `);
7594
7752
  }
7753
+ const vBeforeV6 = database.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get()?.value;
7754
+ if (vBeforeV6 === "5") {
7755
+ database.exec(ENGAGEMENT_DDL);
7756
+ const counts = backfillEngagements(database);
7757
+ database.exec(`
7758
+ CREATE TABLE sessions_v6 (
7759
+ session_id TEXT PRIMARY KEY,
7760
+ agent TEXT NOT NULL,
7761
+ started TEXT NOT NULL,
7762
+ ended TEXT,
7763
+ status TEXT NOT NULL DEFAULT 'active',
7764
+ path TEXT,
7765
+ description TEXT,
7766
+ transcript_path TEXT,
7767
+ pid INTEGER,
7768
+ pid_started_at TEXT,
7769
+ original_head_sha TEXT,
7770
+ activity TEXT,
7771
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
7772
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
7773
+ );
7774
+ INSERT INTO sessions_v6
7775
+ SELECT session_id, agent, started, ended, status, path, description,
7776
+ transcript_path, pid, pid_started_at, original_head_sha, NULL,
7777
+ created_at, updated_at
7778
+ FROM sessions;
7779
+ DROP TABLE sessions;
7780
+ ALTER TABLE sessions_v6 RENAME TO sessions;
7781
+ CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
7782
+ UPDATE meta SET value = '6' WHERE key = 'schema_version';
7783
+ `);
7784
+ console.log(
7785
+ `engagement backfill: backfilled=${counts.backfilled} attributed=${counts.attributed} unattributed=${counts.unattributed}`
7786
+ );
7787
+ }
7595
7788
  });
7596
7789
  runMigrations.exclusive();
7597
- db2.exec(POST_MIGRATION_INDEXES_SQL);
7598
7790
  return db2;
7599
7791
  }
7600
7792
  function isSessionDbInitialized() {
@@ -7626,20 +7818,38 @@ async function migrateFromMarkdown(projectsDir) {
7626
7818
  const allSessions = [];
7627
7819
  for (const entry of entries) {
7628
7820
  if (!entry.isDirectory()) continue;
7629
- const projectDir = resolve13(projectsDir, entry.name);
7630
- const indexPath = resolve13(projectDir, "_index-sessions.md");
7821
+ const projectDir = resolve14(projectsDir, entry.name);
7822
+ const indexPath = resolve14(projectDir, "_index-sessions.md");
7631
7823
  if (!await fileExists(indexPath)) continue;
7632
7824
  const sessions = await parseMarkdownSessionsIndex(indexPath, entry.name);
7633
7825
  allSessions.push(...sessions);
7634
7826
  }
7635
7827
  if (allSessions.length === 0) return 0;
7636
7828
  const insert = database.prepare(`
7637
- INSERT OR IGNORE INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path)
7638
- VALUES (?, ?, ?, ?, ?, ?, ?)
7829
+ INSERT OR IGNORE INTO sessions (session_id, agent, started, status, path)
7830
+ VALUES (?, ?, ?, ?, ?)
7831
+ `);
7832
+ const insertEngagement = database.prepare(`
7833
+ INSERT INTO engagement (session_id, project_slug, assignment_slug, stage, started_at, ended_at, close_reason)
7834
+ SELECT @sid, @ps, @as, 'implement', @started, @ended, @reason
7835
+ WHERE NOT EXISTS (
7836
+ SELECT 1 FROM engagement WHERE session_id = @sid AND ended_at IS NULL
7837
+ )
7639
7838
  `);
7640
7839
  const insertAll = database.transaction((sessions) => {
7641
7840
  for (const s of sessions) {
7642
- insert.run(s.sessionId, s.projectSlug, s.assignmentSlug, s.agent, s.started, s.status, s.path);
7841
+ const res = insert.run(s.sessionId, s.agent, s.started, s.status, s.path);
7842
+ if (res.changes > 0 && (s.projectSlug || s.assignmentSlug)) {
7843
+ const terminal = s.status === "completed" || s.status === "stopped";
7844
+ insertEngagement.run({
7845
+ sid: s.sessionId,
7846
+ ps: s.projectSlug ?? null,
7847
+ as: s.assignmentSlug ?? null,
7848
+ started: s.started,
7849
+ ended: terminal ? s.started : null,
7850
+ reason: terminal ? s.status === "completed" ? "completed" : "abandoned" : null
7851
+ });
7852
+ }
7643
7853
  }
7644
7854
  });
7645
7855
  insertAll(allSessions);
@@ -7647,8 +7857,8 @@ async function migrateFromMarkdown(projectsDir) {
7647
7857
  return allSessions.length;
7648
7858
  }
7649
7859
  async function parseMarkdownSessionsIndex(filePath, projectSlug) {
7650
- const { readFile: readFile32 } = await import("fs/promises");
7651
- const raw2 = await readFile32(filePath, "utf-8");
7860
+ const { readFile: readFile31 } = await import("fs/promises");
7861
+ const raw2 = await readFile31(filePath, "utf-8");
7652
7862
  const sessions = [];
7653
7863
  const lines = raw2.split("\n");
7654
7864
  let inTable = false;
@@ -7682,19 +7892,19 @@ async function parseMarkdownSessionsIndex(filePath, projectSlug) {
7682
7892
  }
7683
7893
  return sessions;
7684
7894
  }
7685
- var db2, SCHEMA_VERSION, SCHEMA_SQL2, POST_MIGRATION_INDEXES_SQL;
7895
+ var db2, SCHEMA_VERSION, SCHEMA_SQL2;
7686
7896
  var init_session_db = __esm({
7687
7897
  "src/dashboard/session-db.ts"() {
7688
7898
  "use strict";
7689
7899
  init_paths();
7690
7900
  init_fs();
7901
+ init_engagement_schema();
7902
+ init_engagement_backfill();
7691
7903
  db2 = null;
7692
- SCHEMA_VERSION = "5";
7904
+ SCHEMA_VERSION = "6";
7693
7905
  SCHEMA_SQL2 = `
7694
7906
  CREATE TABLE IF NOT EXISTS sessions (
7695
7907
  session_id TEXT PRIMARY KEY,
7696
- project_slug TEXT,
7697
- assignment_slug TEXT,
7698
7908
  agent TEXT NOT NULL,
7699
7909
  started TEXT NOT NULL,
7700
7910
  ended TEXT,
@@ -7705,34 +7915,175 @@ CREATE TABLE IF NOT EXISTS sessions (
7705
7915
  pid INTEGER,
7706
7916
  pid_started_at TEXT,
7707
7917
  original_head_sha TEXT,
7918
+ activity TEXT,
7708
7919
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
7709
7920
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
7710
7921
  );
7711
7922
  CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
7712
7923
  CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);
7713
7924
  `;
7714
- POST_MIGRATION_INDEXES_SQL = `
7715
- CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
7716
- CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
7717
- `;
7925
+ }
7926
+ });
7927
+
7928
+ // src/db/engagement-tokens.ts
7929
+ function serializeSnapshot(snapshot) {
7930
+ return snapshot === null ? null : JSON.stringify(snapshot);
7931
+ }
7932
+ function parseSnapshot(json) {
7933
+ return json === null ? null : JSON.parse(json);
7934
+ }
7935
+ function tableExists(name) {
7936
+ const row = getSessionDb().prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(name);
7937
+ return row !== void 0;
7938
+ }
7939
+ function getCumulativeTokenSource() {
7940
+ return override ?? productionCumulativeTokenSource;
7941
+ }
7942
+ var productionCumulativeTokenSource, override;
7943
+ var init_engagement_tokens = __esm({
7944
+ "src/db/engagement-tokens.ts"() {
7945
+ "use strict";
7946
+ init_session_db();
7947
+ productionCumulativeTokenSource = async (sessionId) => {
7948
+ const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
7949
+ const db5 = getSessionDb();
7950
+ const collectorRow = db5.prepare("SELECT value FROM meta WHERE key = 'usage_last_collector_run'").get();
7951
+ const collectorRunAt = collectorRow?.value ?? null;
7952
+ const models = {};
7953
+ if (tableExists("usage_events")) {
7954
+ const rows = db5.prepare(
7955
+ `SELECT model, input_tokens, output_tokens, cache_creation_tokens,
7956
+ cache_read_tokens, total_tokens, total_cost
7957
+ FROM usage_events
7958
+ WHERE session_id = ?`
7959
+ ).all(sessionId);
7960
+ for (const r of rows) {
7961
+ models[r.model] = {
7962
+ input: r.input_tokens,
7963
+ output: r.output_tokens,
7964
+ cacheCreation: r.cache_creation_tokens,
7965
+ cacheRead: r.cache_read_tokens,
7966
+ total: r.total_tokens,
7967
+ cost: r.total_cost
7968
+ };
7969
+ }
7970
+ }
7971
+ return { models, collectorRunAt, capturedAt };
7972
+ };
7973
+ override = null;
7974
+ }
7975
+ });
7976
+
7977
+ // src/db/engagement-db.ts
7978
+ function getOpenEngagement(sessionId) {
7979
+ const row = getSessionDb().prepare(
7980
+ "SELECT * FROM engagement WHERE session_id = ? AND ended_at IS NULL LIMIT 1"
7981
+ ).get(sessionId);
7982
+ return row ?? null;
7983
+ }
7984
+ function getLatestEngagement(sessionId) {
7985
+ const row = getSessionDb().prepare(
7986
+ `SELECT * FROM engagement WHERE session_id = ?
7987
+ ORDER BY (ended_at IS NULL) DESC, started_at DESC, id DESC
7988
+ LIMIT 1`
7989
+ ).get(sessionId);
7990
+ return row ?? null;
7991
+ }
7992
+ function hasAnyEngagement(sessionId) {
7993
+ return getSessionDb().prepare("SELECT 1 FROM engagement WHERE session_id = ? LIMIT 1").get(sessionId) !== void 0;
7994
+ }
7995
+ function insertClosedEngagement(input) {
7996
+ const res = getSessionDb().prepare(
7997
+ `INSERT INTO engagement
7998
+ (session_id, assignment_id, project_slug, assignment_slug, stage, started_at, ended_at, close_reason)
7999
+ VALUES (@sessionId, @assignmentId, @projectSlug, @assignmentSlug, @stage, @startedAt, @endedAt, @closeReason)`
8000
+ ).run({
8001
+ sessionId: input.sessionId,
8002
+ assignmentId: input.assignmentId ?? null,
8003
+ projectSlug: input.projectSlug ?? null,
8004
+ assignmentSlug: input.assignmentSlug ?? null,
8005
+ stage: input.stage ?? DEFAULT_STAGE,
8006
+ startedAt: input.startedAt,
8007
+ endedAt: input.endedAt,
8008
+ closeReason: input.closeReason
8009
+ });
8010
+ return getEngagementById(Number(res.lastInsertRowid));
8011
+ }
8012
+ function getEngagementById(id) {
8013
+ const row = getSessionDb().prepare("SELECT * FROM engagement WHERE id = ?").get(id);
8014
+ if (!row) throw new Error(`engagement ${id} not found after insert`);
8015
+ return row;
8016
+ }
8017
+ function openEngagement(input) {
8018
+ const res = getSessionDb().prepare(
8019
+ `INSERT INTO engagement
8020
+ (session_id, assignment_id, project_slug, assignment_slug, stage, started_at, tokens_at_open)
8021
+ VALUES (@sessionId, @assignmentId, @projectSlug, @assignmentSlug, @stage, @startedAt, @tokensAtOpen)`
8022
+ ).run({
8023
+ sessionId: input.sessionId,
8024
+ assignmentId: input.assignmentId ?? null,
8025
+ projectSlug: input.projectSlug ?? null,
8026
+ assignmentSlug: input.assignmentSlug ?? null,
8027
+ stage: input.stage ?? DEFAULT_STAGE,
8028
+ startedAt: input.startedAt,
8029
+ tokensAtOpen: serializeSnapshot(input.tokensAtOpen ?? null)
8030
+ });
8031
+ return getEngagementById(Number(res.lastInsertRowid));
8032
+ }
8033
+ function closeEngagementById(input) {
8034
+ const res = getSessionDb().prepare(
8035
+ `UPDATE engagement
8036
+ SET ended_at = @endedAt,
8037
+ tokens_at_close = @tokensAtClose,
8038
+ close_reason = @closeReason
8039
+ WHERE id = @id AND started_at = @startedAt AND ended_at IS NULL`
8040
+ ).run({
8041
+ id: input.id,
8042
+ startedAt: input.startedAt,
8043
+ endedAt: input.endedAt,
8044
+ tokensAtClose: serializeSnapshot(input.tokensAtClose ?? null),
8045
+ closeReason: input.closeReason
8046
+ });
8047
+ return res.changes > 0;
8048
+ }
8049
+ function ensureOpenEngagement(input) {
8050
+ if (getOpenEngagement(input.sessionId)) return null;
8051
+ try {
8052
+ return openEngagement(input);
8053
+ } catch (err) {
8054
+ if (err instanceof Error && /UNIQUE constraint failed/.test(err.message)) {
8055
+ return null;
8056
+ }
8057
+ throw err;
8058
+ }
8059
+ }
8060
+ var DEFAULT_STAGE;
8061
+ var init_engagement_db = __esm({
8062
+ "src/db/engagement-db.ts"() {
8063
+ "use strict";
8064
+ init_session_db();
8065
+ init_engagement_tokens();
8066
+ DEFAULT_STAGE = "implement";
7718
8067
  }
7719
8068
  });
7720
8069
 
7721
8070
  // src/dashboard/agent-sessions.ts
7722
8071
  var agent_sessions_exports = {};
7723
8072
  __export(agent_sessions_exports, {
8073
+ SessionResurrectionError: () => SessionResurrectionError,
7724
8074
  appendSession: () => appendSession,
7725
8075
  deleteSessions: () => deleteSessions,
7726
8076
  getSessionById: () => getSessionById,
7727
8077
  listAllSessions: () => listAllSessions,
7728
8078
  listProjectSessions: () => listProjectSessions,
7729
8079
  listSessionsByAssignment: () => listSessionsByAssignment,
8080
+ livenessStopSession: () => livenessStopSession,
7730
8081
  parseSessionsIndex: () => parseSessionsIndex,
7731
8082
  reconcileActiveSessions: () => reconcileActiveSessions,
7732
8083
  updateSessionStatus: () => updateSessionStatus
7733
8084
  });
7734
8085
  import { readFile as readFile11 } from "fs/promises";
7735
- import { resolve as resolve14 } from "path";
8086
+ import { resolve as resolve15 } from "path";
7736
8087
  function rowToSession(row) {
7737
8088
  return {
7738
8089
  sessionId: row.session_id,
@@ -7748,28 +8099,64 @@ function rowToSession(row) {
7748
8099
  pid: row.pid ?? null,
7749
8100
  pidStartedAt: row.pid_started_at ?? null,
7750
8101
  originalHeadSha: row.original_head_sha ?? null,
8102
+ activity: row.activity ?? null,
7751
8103
  updatedAt: row.updated_at ?? null
7752
8104
  };
7753
8105
  }
7754
8106
  async function parseSessionsIndex(_projectDir, projectSlug) {
7755
8107
  const db5 = getSessionDb();
7756
- const rows = db5.prepare("SELECT * FROM sessions WHERE project_slug = ? ORDER BY started DESC").all(projectSlug);
8108
+ const rows = db5.prepare(`${SESSION_SELECT_WITH_BINDING} WHERE e.project_slug = ? ORDER BY s.started DESC`).all(projectSlug);
7757
8109
  return rows.map(rowToSession);
7758
8110
  }
8111
+ function reopenEngagementIfMissing(sessionId, freshBinding, freshStartedAt, tokensAtOpen) {
8112
+ if (getOpenEngagement(sessionId)) return;
8113
+ if (freshBinding && (freshBinding.projectSlug || freshBinding.assignmentSlug)) {
8114
+ ensureOpenEngagement({
8115
+ sessionId,
8116
+ assignmentId: freshBinding.assignmentId ?? null,
8117
+ projectSlug: freshBinding.projectSlug,
8118
+ assignmentSlug: freshBinding.assignmentSlug,
8119
+ stage: "implement",
8120
+ startedAt: freshStartedAt,
8121
+ tokensAtOpen: tokensAtOpen ?? null
8122
+ });
8123
+ return;
8124
+ }
8125
+ const latest = getLatestEngagement(sessionId);
8126
+ if (latest && (latest.project_slug || latest.assignment_slug)) {
8127
+ ensureOpenEngagement({
8128
+ sessionId,
8129
+ assignmentId: latest.assignment_id,
8130
+ projectSlug: latest.project_slug,
8131
+ assignmentSlug: latest.assignment_slug,
8132
+ stage: "implement",
8133
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
8134
+ tokensAtOpen: tokensAtOpen ?? null
8135
+ });
8136
+ }
8137
+ }
7759
8138
  async function appendSession(_projectDir, session, opts) {
7760
8139
  const db5 = getSessionDb();
7761
- db5.prepare(`
7762
- INSERT INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path, description, transcript_path, pid, pid_started_at, original_head_sha)
7763
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
8140
+ let openSnapshot = null;
8141
+ try {
8142
+ openSnapshot = await getCumulativeTokenSource()(session.sessionId);
8143
+ } catch {
8144
+ openSnapshot = null;
8145
+ }
8146
+ const upsert = db5.prepare(`
8147
+ INSERT INTO sessions (session_id, agent, started, status, path, description, transcript_path, pid, pid_started_at, original_head_sha)
8148
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
7764
8149
  ON CONFLICT(session_id) DO UPDATE SET
7765
- project_slug = COALESCE(NULLIF(excluded.project_slug, ''), project_slug),
7766
- assignment_slug = COALESCE(NULLIF(excluded.assignment_slug, ''), assignment_slug),
7767
8150
  agent = excluded.agent,
7768
8151
  status = CASE
7769
8152
  WHEN status = 'completed' THEN status
7770
8153
  WHEN status = 'stopped' AND NOT (? AND excluded.status = 'active') THEN status
7771
8154
  ELSE excluded.status
7772
8155
  END,
8156
+ ended = CASE
8157
+ WHEN status = 'stopped' AND ? AND excluded.status = 'active' THEN NULL
8158
+ ELSE ended
8159
+ END,
7773
8160
  path = COALESCE(NULLIF(excluded.path, ''), path),
7774
8161
  description = COALESCE(NULLIF(excluded.description, ''), description),
7775
8162
  transcript_path = COALESCE(NULLIF(excluded.transcript_path, ''), transcript_path),
@@ -7777,59 +8164,151 @@ async function appendSession(_projectDir, session, opts) {
7777
8164
  pid_started_at = COALESCE(NULLIF(excluded.pid_started_at, ''), pid_started_at),
7778
8165
  original_head_sha = COALESCE(NULLIF(original_head_sha, ''), NULLIF(excluded.original_head_sha, '')),
7779
8166
  updated_at = datetime('now')
7780
- `).run(
7781
- session.sessionId,
7782
- session.projectSlug ?? null,
7783
- session.assignmentSlug ?? null,
7784
- session.agent,
7785
- session.started,
7786
- session.status,
7787
- session.path,
7788
- session.description ?? null,
7789
- session.transcriptPath ?? null,
7790
- session.pid ?? null,
7791
- session.pidStartedAt ?? null,
7792
- session.originalHeadSha ?? null,
7793
- opts?.reviveStopped ? 1 : 0
7794
- );
8167
+ `);
8168
+ const apply = db5.transaction(() => {
8169
+ upsert.run(
8170
+ session.sessionId,
8171
+ session.agent,
8172
+ session.started,
8173
+ session.status,
8174
+ session.path,
8175
+ session.description ?? null,
8176
+ session.transcriptPath ?? null,
8177
+ session.pid ?? null,
8178
+ session.pidStartedAt ?? null,
8179
+ session.originalHeadSha ?? null,
8180
+ opts?.reviveStopped ? 1 : 0,
8181
+ // status CASE: revive guard
8182
+ opts?.reviveStopped ? 1 : 0
8183
+ // ended CASE: clear stale ended on revive
8184
+ );
8185
+ const persisted = db5.prepare("SELECT status, ended FROM sessions WHERE session_id = ?").get(session.sessionId);
8186
+ const freshBinding = {
8187
+ assignmentId: session.assignmentId ?? null,
8188
+ projectSlug: session.projectSlug ?? null,
8189
+ assignmentSlug: session.assignmentSlug ?? null
8190
+ };
8191
+ if (persisted?.status === "active") {
8192
+ reopenEngagementIfMissing(
8193
+ session.sessionId,
8194
+ freshBinding,
8195
+ session.started ?? (/* @__PURE__ */ new Date()).toISOString(),
8196
+ openSnapshot
8197
+ );
8198
+ } else if (persisted && (freshBinding.projectSlug || freshBinding.assignmentSlug) && !hasAnyEngagement(session.sessionId)) {
8199
+ insertClosedEngagement({
8200
+ sessionId: session.sessionId,
8201
+ projectSlug: freshBinding.projectSlug,
8202
+ assignmentSlug: freshBinding.assignmentSlug,
8203
+ startedAt: session.started ?? (/* @__PURE__ */ new Date()).toISOString(),
8204
+ endedAt: persisted.ended ?? session.started ?? (/* @__PURE__ */ new Date()).toISOString(),
8205
+ closeReason: persisted.status === "completed" ? "completed" : "abandoned"
8206
+ });
8207
+ }
8208
+ });
8209
+ apply.immediate();
7795
8210
  }
7796
8211
  async function updateSessionStatus(_projectDir, sessionId, status, endedAt) {
7797
8212
  const db5 = getSessionDb();
7798
8213
  const isTerminal = status === "completed" || status === "stopped";
7799
- const result = isTerminal ? db5.prepare(
7800
- "UPDATE sessions SET status = ?, ended = COALESCE(?, datetime('now')), updated_at = datetime('now') WHERE session_id = ?"
7801
- ).run(status, endedAt ?? null, sessionId) : db5.prepare(
7802
- "UPDATE sessions SET status = ?, updated_at = datetime('now') WHERE session_id = ?"
7803
- ).run(status, sessionId);
7804
- return result.changes > 0;
8214
+ if (!isTerminal) {
8215
+ let reviveSnapshot = null;
8216
+ try {
8217
+ reviveSnapshot = await getCumulativeTokenSource()(sessionId);
8218
+ } catch {
8219
+ reviveSnapshot = null;
8220
+ }
8221
+ const apply2 = db5.transaction(() => {
8222
+ const current = db5.prepare("SELECT status FROM sessions WHERE session_id = ?").get(sessionId);
8223
+ if (!current) return false;
8224
+ if (current.status === "completed") {
8225
+ throw new SessionResurrectionError(
8226
+ `Refusing to revive completed session "${sessionId}" to active. A completed session is final.`
8227
+ );
8228
+ }
8229
+ const res = db5.prepare(
8230
+ "UPDATE sessions SET status = ?, ended = NULL, updated_at = datetime('now') WHERE session_id = ?"
8231
+ ).run(status, sessionId);
8232
+ if (res.changes > 0) {
8233
+ reopenEngagementIfMissing(sessionId, null, (/* @__PURE__ */ new Date()).toISOString(), reviveSnapshot);
8234
+ }
8235
+ return res.changes > 0;
8236
+ });
8237
+ return apply2.immediate();
8238
+ }
8239
+ const captured = getOpenEngagement(sessionId);
8240
+ let snapshot = null;
8241
+ try {
8242
+ snapshot = await getCumulativeTokenSource()(sessionId);
8243
+ } catch {
8244
+ snapshot = null;
8245
+ }
8246
+ const apply = db5.transaction(() => {
8247
+ const stillTerminal = captured ? closeEngagementById({
8248
+ id: captured.id,
8249
+ startedAt: captured.started_at,
8250
+ closeReason: status === "completed" ? "completed" : "abandoned",
8251
+ tokensAtClose: snapshot,
8252
+ endedAt: endedAt ?? (/* @__PURE__ */ new Date()).toISOString()
8253
+ }) : getOpenEngagement(sessionId) === null;
8254
+ if (!stillTerminal) return false;
8255
+ const res = db5.prepare(
8256
+ "UPDATE sessions SET status = ?, ended = COALESCE(?, datetime('now')), updated_at = datetime('now') WHERE session_id = ?"
8257
+ ).run(status, endedAt ?? null, sessionId);
8258
+ return res.changes > 0;
8259
+ });
8260
+ return apply.immediate();
8261
+ }
8262
+ function livenessStopSession(input) {
8263
+ const db5 = getSessionDb();
8264
+ const endedAt = input.endedAt ?? (/* @__PURE__ */ new Date()).toISOString();
8265
+ const apply = db5.transaction(() => {
8266
+ const stillDead = input.engagementId != null && input.engagementStartedAt != null ? closeEngagementById({
8267
+ id: input.engagementId,
8268
+ startedAt: input.engagementStartedAt,
8269
+ closeReason: "liveness_gc",
8270
+ tokensAtClose: input.tokensAtClose ?? null,
8271
+ endedAt
8272
+ }) : getOpenEngagement(input.sessionId) === null;
8273
+ if (!stillDead) return false;
8274
+ const res = db5.prepare(
8275
+ "UPDATE sessions SET status = 'stopped', ended = COALESCE(?, datetime('now')), updated_at = datetime('now') WHERE session_id = ? AND status = 'active'"
8276
+ ).run(input.endedAt ?? null, input.sessionId);
8277
+ return res.changes > 0;
8278
+ });
8279
+ return apply.immediate();
7805
8280
  }
7806
8281
  async function listAllSessions(_projectsDir) {
7807
8282
  const db5 = getSessionDb();
7808
- const rows = db5.prepare("SELECT * FROM sessions ORDER BY started DESC").all();
8283
+ const rows = db5.prepare(`${SESSION_SELECT_WITH_BINDING} ORDER BY s.started DESC`).all();
7809
8284
  return rows.map(rowToSession);
7810
8285
  }
7811
8286
  function getSessionById(sessionId) {
7812
8287
  const db5 = getSessionDb();
7813
- const row = db5.prepare("SELECT * FROM sessions WHERE session_id = ? LIMIT 1").get(sessionId);
8288
+ const row = db5.prepare(`${SESSION_SELECT_WITH_BINDING} WHERE s.session_id = ? LIMIT 1`).get(sessionId);
7814
8289
  return row ? rowToSession(row) : null;
7815
8290
  }
7816
8291
  async function listProjectSessions(_projectsDir, projectSlug, assignmentSlug) {
7817
8292
  const db5 = getSessionDb();
7818
8293
  if (assignmentSlug) {
7819
8294
  const rows2 = db5.prepare(
7820
- "SELECT * FROM sessions WHERE project_slug = ? AND assignment_slug = ? ORDER BY started DESC"
8295
+ `${SESSION_SELECT_WITH_BINDING} WHERE e.project_slug = ? AND e.assignment_slug = ? ORDER BY s.started DESC`
7821
8296
  ).all(projectSlug, assignmentSlug);
7822
8297
  return rows2.map(rowToSession);
7823
8298
  }
7824
- const rows = db5.prepare("SELECT * FROM sessions WHERE project_slug = ? ORDER BY started DESC").all(projectSlug);
8299
+ const rows = db5.prepare(`${SESSION_SELECT_WITH_BINDING} WHERE e.project_slug = ? ORDER BY s.started DESC`).all(projectSlug);
7825
8300
  return rows.map(rowToSession);
7826
8301
  }
7827
8302
  async function deleteSessions(sessionIds) {
7828
8303
  if (sessionIds.length === 0) return 0;
7829
8304
  const db5 = getSessionDb();
7830
8305
  const placeholders = sessionIds.map(() => "?").join(", ");
7831
- const result = db5.prepare(`DELETE FROM sessions WHERE session_id IN (${placeholders})`).run(...sessionIds);
7832
- return result.changes;
8306
+ const run3 = db5.transaction(() => {
8307
+ db5.prepare(`DELETE FROM engagement WHERE session_id IN (${placeholders})`).run(...sessionIds);
8308
+ const result = db5.prepare(`DELETE FROM sessions WHERE session_id IN (${placeholders})`).run(...sessionIds);
8309
+ return result.changes;
8310
+ });
8311
+ return run3.immediate();
7833
8312
  }
7834
8313
  async function readAssignmentStatusFromPath(assignmentMdPath) {
7835
8314
  if (!await fileExists(assignmentMdPath)) return null;
@@ -7839,39 +8318,44 @@ async function readAssignmentStatusFromPath(assignmentMdPath) {
7839
8318
  }
7840
8319
  async function readAssignmentStatus(projectDir, assignmentSlug) {
7841
8320
  return readAssignmentStatusFromPath(
7842
- resolve14(projectDir, "assignments", assignmentSlug, "assignment.md")
8321
+ resolve15(projectDir, "assignments", assignmentSlug, "assignment.md")
7843
8322
  );
7844
8323
  }
7845
8324
  async function reconcileActiveSessions(projectsDir, assignmentsDir2) {
7846
8325
  const db5 = getSessionDb();
7847
- const activeSessions = db5.prepare("SELECT * FROM sessions WHERE status = 'active' AND assignment_slug IS NOT NULL").all();
8326
+ const activeSessions = db5.prepare(
8327
+ `SELECT s.*, e.project_slug AS project_slug, e.assignment_slug AS assignment_slug
8328
+ FROM sessions s
8329
+ JOIN engagement e ON e.session_id = s.session_id AND e.ended_at IS NULL
8330
+ WHERE s.status = 'active' AND e.assignment_slug IS NOT NULL`
8331
+ ).all();
7848
8332
  if (activeSessions.length === 0) return 0;
7849
8333
  const assignmentStatuses = /* @__PURE__ */ new Map();
7850
8334
  const seen = /* @__PURE__ */ new Set();
7851
8335
  for (const session of activeSessions) {
7852
8336
  const aslug = session.assignment_slug;
7853
8337
  if (!aslug) continue;
7854
- const projectKey = session.project_slug ?? "__standalone__";
7855
- const key = `${projectKey}/${aslug}`;
8338
+ const projectKey2 = session.project_slug ?? "__standalone__";
8339
+ const key = `${projectKey2}/${aslug}`;
7856
8340
  if (seen.has(key)) continue;
7857
8341
  seen.add(key);
7858
8342
  if (session.project_slug) {
7859
8343
  const status = await readAssignmentStatus(
7860
- resolve14(projectsDir, session.project_slug),
8344
+ resolve15(projectsDir, session.project_slug),
7861
8345
  aslug
7862
8346
  );
7863
8347
  if (status) assignmentStatuses.set(key, status);
7864
8348
  } else if (assignmentsDir2) {
7865
8349
  const status = await readAssignmentStatusFromPath(
7866
- resolve14(assignmentsDir2, aslug, "assignment.md")
8350
+ resolve15(assignmentsDir2, aslug, "assignment.md")
7867
8351
  );
7868
8352
  if (status) assignmentStatuses.set(key, status);
7869
8353
  }
7870
8354
  }
7871
8355
  let totalUpdated = 0;
7872
8356
  for (const session of activeSessions) {
7873
- const projectKey = session.project_slug ?? "__standalone__";
7874
- const key = `${projectKey}/${session.assignment_slug}`;
8357
+ const projectKey2 = session.project_slug ?? "__standalone__";
8358
+ const key = `${projectKey2}/${session.assignment_slug}`;
7875
8359
  const assignmentStatus = assignmentStatuses.get(key);
7876
8360
  if (!assignmentStatus || !DONE_ASSIGNMENT_STATUSES.has(assignmentStatus)) continue;
7877
8361
  const newStatus = assignmentStatus === "failed" ? "stopped" : "completed";
@@ -7883,18 +8367,34 @@ async function reconcileActiveSessions(projectsDir, assignmentsDir2) {
7883
8367
  async function listSessionsByAssignment(projectSlug, assignmentSlug) {
7884
8368
  const db5 = getSessionDb();
7885
8369
  const rows = projectSlug === null ? db5.prepare(
7886
- "SELECT * FROM sessions WHERE assignment_slug = ? AND project_slug IS NULL ORDER BY started DESC"
8370
+ `${SESSION_SELECT_WITH_BINDING} WHERE e.assignment_slug = ? AND e.project_slug IS NULL ORDER BY s.started DESC`
7887
8371
  ).all(assignmentSlug) : db5.prepare(
7888
- "SELECT * FROM sessions WHERE project_slug = ? AND assignment_slug = ? ORDER BY started DESC"
8372
+ `${SESSION_SELECT_WITH_BINDING} WHERE e.project_slug = ? AND e.assignment_slug = ? ORDER BY s.started DESC`
7889
8373
  ).all(projectSlug, assignmentSlug);
7890
8374
  return rows.map(rowToSession);
7891
8375
  }
7892
- var DONE_ASSIGNMENT_STATUSES;
8376
+ var SESSION_SELECT_WITH_BINDING, SessionResurrectionError, DONE_ASSIGNMENT_STATUSES;
7893
8377
  var init_agent_sessions = __esm({
7894
8378
  "src/dashboard/agent-sessions.ts"() {
7895
8379
  "use strict";
7896
8380
  init_fs();
7897
8381
  init_session_db();
8382
+ init_engagement_db();
8383
+ init_engagement_tokens();
8384
+ SESSION_SELECT_WITH_BINDING = `
8385
+ SELECT s.*,
8386
+ e.project_slug AS project_slug,
8387
+ e.assignment_slug AS assignment_slug,
8388
+ e.assignment_id AS assignment_id
8389
+ FROM sessions s
8390
+ LEFT JOIN engagement e ON e.id = (
8391
+ SELECT e2.id FROM engagement e2
8392
+ WHERE e2.session_id = s.session_id
8393
+ ORDER BY (e2.ended_at IS NULL) DESC, e2.started_at DESC, e2.id DESC
8394
+ LIMIT 1
8395
+ )`;
8396
+ SessionResurrectionError = class extends Error {
8397
+ };
7898
8398
  DONE_ASSIGNMENT_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "review"]);
7899
8399
  }
7900
8400
  });
@@ -7987,7 +8487,7 @@ var init_classify = __esm({
7987
8487
 
7988
8488
  // src/dashboard/servers.ts
7989
8489
  import { readdir as readdir9, readFile as readFile12, unlink as unlink2 } from "fs/promises";
7990
- import { resolve as resolve15 } from "path";
8490
+ import { resolve as resolve16 } from "path";
7991
8491
  function sanitizeSessionName(name) {
7992
8492
  return name.replace(/[^a-zA-Z0-9_-]/g, "-");
7993
8493
  }
@@ -8035,7 +8535,7 @@ async function registerSession(dir, rawName) {
8035
8535
  lastRefreshed: now,
8036
8536
  overrides: {}
8037
8537
  });
8038
- await writeFileForce(resolve15(dir, `${name}.md`), content);
8538
+ await writeFileForce(resolve16(dir, `${name}.md`), content);
8039
8539
  return name;
8040
8540
  }
8041
8541
  async function listSessionFiles(dir) {
@@ -8044,7 +8544,7 @@ async function listSessionFiles(dir) {
8044
8544
  return entries.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, ""));
8045
8545
  }
8046
8546
  async function readSessionFile(dir, name) {
8047
- const filePath = resolve15(dir, `${sanitizeSessionName(name)}.md`);
8547
+ const filePath = resolve16(dir, `${sanitizeSessionName(name)}.md`);
8048
8548
  if (!await fileExists(filePath)) return null;
8049
8549
  const raw2 = await readFile12(filePath, "utf-8");
8050
8550
  const [frontmatter] = extractFrontmatter2(raw2);
@@ -8086,7 +8586,7 @@ async function readSessionFile(dir, name) {
8086
8586
  };
8087
8587
  }
8088
8588
  async function removeSession(dir, name) {
8089
- const filePath = resolve15(dir, `${sanitizeSessionName(name)}.md`);
8589
+ const filePath = resolve16(dir, `${sanitizeSessionName(name)}.md`);
8090
8590
  if (await fileExists(filePath)) {
8091
8591
  await unlink2(filePath);
8092
8592
  }
@@ -8095,7 +8595,7 @@ async function updateLastRefreshed(dir, name) {
8095
8595
  const data = await readSessionFile(dir, name);
8096
8596
  if (!data) return;
8097
8597
  const content = buildSessionContent({ ...data, lastRefreshed: nowTimestamp2() });
8098
- await writeFileForce(resolve15(dir, `${sanitizeSessionName(name)}.md`), content);
8598
+ await writeFileForce(resolve16(dir, `${sanitizeSessionName(name)}.md`), content);
8099
8599
  }
8100
8600
  async function setOverride(dir, sessionName, windowIndex, paneIndex, assignment) {
8101
8601
  const data = await readSessionFile(dir, sessionName);
@@ -8107,7 +8607,7 @@ async function setOverride(dir, sessionName, windowIndex, paneIndex, assignment)
8107
8607
  delete data.overrides[key];
8108
8608
  }
8109
8609
  const content = buildSessionContent({ ...data });
8110
- await writeFileForce(resolve15(dir, `${sanitizeSessionName(sessionName)}.md`), content);
8610
+ await writeFileForce(resolve16(dir, `${sanitizeSessionName(sessionName)}.md`), content);
8111
8611
  }
8112
8612
  async function registerAutoSession(dir, rawName, opts) {
8113
8613
  const name = sanitizeSessionName(rawName);
@@ -8124,7 +8624,7 @@ async function registerAutoSession(dir, rawName, opts) {
8124
8624
  ports: opts.ports,
8125
8625
  cwd: opts.cwd
8126
8626
  });
8127
- await writeFileForce(resolve15(dir, `${name}.md`), content);
8627
+ await writeFileForce(resolve16(dir, `${name}.md`), content);
8128
8628
  return name;
8129
8629
  }
8130
8630
  var init_servers = __esm({
@@ -8159,7 +8659,7 @@ __export(scanner_exports, {
8159
8659
  });
8160
8660
  import { execFile } from "child_process";
8161
8661
  import { promisify } from "util";
8162
- import { resolve as resolve16 } from "path";
8662
+ import { resolve as resolve17 } from "path";
8163
8663
  import { realpath } from "fs/promises";
8164
8664
  function clearScanCache() {
8165
8665
  cache2 = null;
@@ -8175,8 +8675,8 @@ function scanKey(serversDir2, projectsDir, assignmentsDir2) {
8175
8675
  return `${serversDir2}\0${projectsDir}\0${assignmentsDir2 ?? ""}`;
8176
8676
  }
8177
8677
  function delay(ms) {
8178
- return new Promise((resolve48) => {
8179
- const timer3 = setTimeout(resolve48, ms);
8678
+ return new Promise((resolve49) => {
8679
+ const timer3 = setTimeout(resolve49, ms);
8180
8680
  if (typeof timer3.unref === "function") {
8181
8681
  timer3.unref();
8182
8682
  }
@@ -8312,8 +8812,8 @@ async function getGitInfo(cwd) {
8312
8812
  let isWorktree = false;
8313
8813
  if (commonDir && gitDir && commonDir !== gitDir) {
8314
8814
  try {
8315
- const resolvedCommon = await realpath(resolve16(cwd, commonDir));
8316
- const resolvedGit = await realpath(resolve16(cwd, gitDir));
8815
+ const resolvedCommon = await realpath(resolve17(cwd, commonDir));
8816
+ const resolvedGit = await realpath(resolve17(cwd, gitDir));
8317
8817
  isWorktree = resolvedCommon !== resolvedGit;
8318
8818
  } catch {
8319
8819
  isWorktree = false;
@@ -8415,16 +8915,16 @@ async function scanSession(sessionData, lsofOutput, workspaceRecords, procSnapsh
8415
8915
  const gitInfo = gitInfoCache.get(rp.cwd) ?? { branch: null, worktree: false };
8416
8916
  const ports = panePorts.get(key) ?? [];
8417
8917
  const urls = ports.map((p) => `http://localhost:${p}`);
8418
- const override = sessionData.overrides[key];
8918
+ const override3 = sessionData.overrides[key];
8419
8919
  let assignment = null;
8420
- if (override) {
8920
+ if (override3) {
8421
8921
  const rec = workspaceRecords.find(
8422
- (r) => r.projectSlug === override.project && r.assignmentSlug === override.assignment
8922
+ (r) => r.projectSlug === override3.project && r.assignmentSlug === override3.assignment
8423
8923
  );
8424
8924
  assignment = {
8425
- project: override.project,
8426
- slug: override.assignment,
8427
- title: rec?.assignmentTitle ?? override.assignment
8925
+ project: override3.project,
8926
+ slug: override3.assignment,
8927
+ title: rec?.assignmentTitle ?? override3.assignment
8428
8928
  };
8429
8929
  } else {
8430
8930
  assignment = await autoLinkPane(rp.cwd, gitInfo.branch, workspaceRecords);
@@ -8478,16 +8978,16 @@ async function scanProcessSession(sessionData, lsofOutput, workspaceRecords) {
8478
8978
  }
8479
8979
  const ports = findListeningPorts(lsofOutput, /* @__PURE__ */ new Set([sessionData.pid]));
8480
8980
  const gitInfo = await getGitInfo(sessionData.cwd);
8481
- const override = sessionData.overrides["0:0"];
8981
+ const override3 = sessionData.overrides["0:0"];
8482
8982
  let assignment = null;
8483
- if (override) {
8983
+ if (override3) {
8484
8984
  const rec = workspaceRecords.find(
8485
- (r) => r.projectSlug === override.project && r.assignmentSlug === override.assignment
8985
+ (r) => r.projectSlug === override3.project && r.assignmentSlug === override3.assignment
8486
8986
  );
8487
8987
  assignment = {
8488
- project: override.project,
8489
- slug: override.assignment,
8490
- title: rec?.assignmentTitle ?? override.assignment
8988
+ project: override3.project,
8989
+ slug: override3.assignment,
8990
+ title: rec?.assignmentTitle ?? override3.assignment
8491
8991
  };
8492
8992
  } else {
8493
8993
  assignment = await autoLinkPane(sessionData.cwd, gitInfo.branch, workspaceRecords);
@@ -8651,7 +9151,7 @@ __export(api_exports, {
8651
9151
  resolveWorkspaceMembers: () => resolveWorkspaceMembers
8652
9152
  });
8653
9153
  import { readdir as readdir10, readFile as readFile13, writeFile as writeFile3, stat as stat2 } from "fs/promises";
8654
- import { resolve as resolve17, dirname as dirname2, basename } from "path";
9154
+ import { resolve as resolve18, dirname as dirname2, basename } from "path";
8655
9155
  function clearFrontmatterField(content, key) {
8656
9156
  const fieldRegex = new RegExp(`^(${escapeRegExp2(key)}:)\\s*.*$`, "m");
8657
9157
  return content.replace(fieldRegex, `$1 null`);
@@ -8735,8 +9235,8 @@ async function computeStandaloneRecords(assignmentsDir2) {
8735
9235
  const records = [];
8736
9236
  for (const entry of entries) {
8737
9237
  if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
8738
- const assignmentDir = resolve17(assignmentsDir2, entry.name);
8739
- const assignmentMdPath = resolve17(assignmentDir, "assignment.md");
9238
+ const assignmentDir = resolve18(assignmentsDir2, entry.name);
9239
+ const assignmentMdPath = resolve18(assignmentDir, "assignment.md");
8740
9240
  if (!await fileExists(assignmentMdPath)) continue;
8741
9241
  try {
8742
9242
  const content = await readFile13(assignmentMdPath, "utf-8");
@@ -8823,7 +9323,7 @@ async function listProjects(projectsDir) {
8823
9323
  return projectRecords.filter((record) => !isProjectArchived(record.summary)).map((record) => record.summary);
8824
9324
  }
8825
9325
  async function readWorkspaceRegistry(projectsDir) {
8826
- const registryPath = resolve17(dirname2(projectsDir), "workspaces.json");
9326
+ const registryPath = resolve18(dirname2(projectsDir), "workspaces.json");
8827
9327
  try {
8828
9328
  const raw2 = await readFile13(registryPath, "utf-8");
8829
9329
  const parsed = JSON.parse(raw2);
@@ -8833,7 +9333,7 @@ async function readWorkspaceRegistry(projectsDir) {
8833
9333
  }
8834
9334
  }
8835
9335
  async function writeWorkspaceRegistry(projectsDir, workspaces) {
8836
- const registryPath = resolve17(dirname2(projectsDir), "workspaces.json");
9336
+ const registryPath = resolve18(dirname2(projectsDir), "workspaces.json");
8837
9337
  await writeFile3(registryPath, JSON.stringify(workspaces, null, 2) + "\n", "utf-8");
8838
9338
  }
8839
9339
  async function listWorkspaces(projectsDir, assignmentsDir2) {
@@ -8924,7 +9424,7 @@ async function deleteWorkspace(projectsDir, name, opts = {}) {
8924
9424
  if (cascade) {
8925
9425
  const timestamp = nowTimestamp();
8926
9426
  for (const slug of projectsReferencing) {
8927
- const path = resolve17(projectsDir, slug, "project.md");
9427
+ const path = resolve18(projectsDir, slug, "project.md");
8928
9428
  const raw2 = await readFile13(path, "utf-8");
8929
9429
  let next = clearFrontmatterField(raw2, "workspace");
8930
9430
  next = setUpdatedField(next, timestamp);
@@ -8933,7 +9433,7 @@ async function deleteWorkspace(projectsDir, name, opts = {}) {
8933
9433
  }
8934
9434
  for (const id of standalonesReferencing) {
8935
9435
  if (!opts.assignmentsDir) break;
8936
- const path = resolve17(opts.assignmentsDir, id, "assignment.md");
9436
+ const path = resolve18(opts.assignmentsDir, id, "assignment.md");
8937
9437
  const raw2 = await readFile13(path, "utf-8");
8938
9438
  let next = clearFrontmatterField(raw2, "workspaceGroup");
8939
9439
  next = setUpdatedField(next, timestamp);
@@ -9212,7 +9712,7 @@ async function getEditableDocumentById(projectsDir, assignmentsDir2, documentTyp
9212
9712
  }
9213
9713
  const fileName = documentType === "assignment" ? "assignment.md" : documentType === "plan" ? "plan.md" : documentType === "scratchpad" ? "scratchpad.md" : documentType === "handoff" ? "handoff.md" : documentType === "decision-record" ? "decision-record.md" : null;
9214
9714
  if (!fileName) return null;
9215
- const filePath = resolve17(resolved.assignmentDir, fileName);
9715
+ const filePath = resolve18(resolved.assignmentDir, fileName);
9216
9716
  if (!await fileExists(filePath)) return null;
9217
9717
  const content = await readFile13(filePath, "utf-8");
9218
9718
  const label = resolved.id;
@@ -9228,8 +9728,8 @@ async function getEditableDocumentById(projectsDir, assignmentsDir2, documentTyp
9228
9728
  };
9229
9729
  }
9230
9730
  async function getProjectDetail(projectsDir, slug) {
9231
- const projectPath = resolve17(projectsDir, slug);
9232
- const projectMdPath = resolve17(projectPath, "project.md");
9731
+ const projectPath = resolve18(projectsDir, slug);
9732
+ const projectMdPath = resolve18(projectPath, "project.md");
9233
9733
  if (!await fileExists(projectMdPath)) {
9234
9734
  return null;
9235
9735
  }
@@ -9266,15 +9766,15 @@ async function getProjectDetail(projectsDir, slug) {
9266
9766
  };
9267
9767
  }
9268
9768
  async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
9269
- const assignmentDir = resolve17(projectsDir, projectSlug, "assignments", assignmentSlug);
9270
- const assignmentMdPath = resolve17(assignmentDir, "assignment.md");
9769
+ const assignmentDir = resolve18(projectsDir, projectSlug, "assignments", assignmentSlug);
9770
+ const assignmentMdPath = resolve18(assignmentDir, "assignment.md");
9271
9771
  if (!await fileExists(assignmentMdPath)) {
9272
9772
  return null;
9273
9773
  }
9274
9774
  const assignmentContent = await readFile13(assignmentMdPath, "utf-8");
9275
9775
  const assignment = parseAssignmentFull(assignmentContent);
9276
9776
  let projectWorkspace = null;
9277
- const projectMdPath = resolve17(projectsDir, projectSlug, "project.md");
9777
+ const projectMdPath = resolve18(projectsDir, projectSlug, "project.md");
9278
9778
  if (await fileExists(projectMdPath)) {
9279
9779
  const projectContent = await readFile13(projectMdPath, "utf-8");
9280
9780
  projectWorkspace = parseProject(projectContent).workspace;
@@ -9282,7 +9782,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
9282
9782
  let plan = null;
9283
9783
  const planFile = await latestPlanFile(assignmentDir);
9284
9784
  if (planFile) {
9285
- const planPath = resolve17(assignmentDir, planFile);
9785
+ const planPath = resolve18(assignmentDir, planFile);
9286
9786
  if (await fileExists(planPath)) {
9287
9787
  const planContent = await readFile13(planPath, "utf-8");
9288
9788
  const parsed = parsePlan(planContent);
@@ -9294,7 +9794,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
9294
9794
  }
9295
9795
  }
9296
9796
  let scratchpad = null;
9297
- const scratchpadPath = resolve17(assignmentDir, "scratchpad.md");
9797
+ const scratchpadPath = resolve18(assignmentDir, "scratchpad.md");
9298
9798
  if (await fileExists(scratchpadPath)) {
9299
9799
  const scratchpadContent = await readFile13(scratchpadPath, "utf-8");
9300
9800
  const parsed = parseScratchpad(scratchpadContent);
@@ -9304,7 +9804,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
9304
9804
  };
9305
9805
  }
9306
9806
  let handoff = null;
9307
- const handoffPath = resolve17(assignmentDir, "handoff.md");
9807
+ const handoffPath = resolve18(assignmentDir, "handoff.md");
9308
9808
  if (await fileExists(handoffPath)) {
9309
9809
  const handoffContent = await readFile13(handoffPath, "utf-8");
9310
9810
  const parsed = parseHandoff(handoffContent);
@@ -9315,7 +9815,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
9315
9815
  };
9316
9816
  }
9317
9817
  let decisionRecord = null;
9318
- const decisionRecordPath = resolve17(assignmentDir, "decision-record.md");
9818
+ const decisionRecordPath = resolve18(assignmentDir, "decision-record.md");
9319
9819
  if (await fileExists(decisionRecordPath)) {
9320
9820
  const decisionRecordContent = await readFile13(decisionRecordPath, "utf-8");
9321
9821
  const parsed = parseDecisionRecord(decisionRecordContent);
@@ -9326,7 +9826,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
9326
9826
  };
9327
9827
  }
9328
9828
  let progress = null;
9329
- const progressPath = resolve17(assignmentDir, "progress.md");
9829
+ const progressPath = resolve18(assignmentDir, "progress.md");
9330
9830
  if (await fileExists(progressPath)) {
9331
9831
  const progressContent = await readFile13(progressPath, "utf-8");
9332
9832
  const parsed = parseProgress(progressContent);
@@ -9337,7 +9837,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
9337
9837
  };
9338
9838
  }
9339
9839
  let comments = null;
9340
- const commentsPath = resolve17(assignmentDir, "comments.md");
9840
+ const commentsPath = resolve18(assignmentDir, "comments.md");
9341
9841
  if (await fileExists(commentsPath)) {
9342
9842
  const commentsContent = await readFile13(commentsPath, "utf-8");
9343
9843
  const parsed = parseComments(commentsContent);
@@ -9371,7 +9871,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
9371
9871
  archivedReason: assignment.archivedReason,
9372
9872
  ...deriveStatusVirtuals(assignment, terminalStatuses),
9373
9873
  override: assignment.override,
9374
- derived: await buildDerivedDetail(assignment, assignmentDir, resolve17(projectsDir, projectSlug)),
9874
+ derived: await buildDerivedDetail(assignment, assignmentDir, resolve18(projectsDir, projectSlug)),
9375
9875
  created: assignment.created,
9376
9876
  updated: assignment.updated,
9377
9877
  body: assignment.body,
@@ -9462,7 +9962,7 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir2) {
9462
9962
  slug: a.slug,
9463
9963
  title: a.title,
9464
9964
  projectSlug: rec.summary.slug,
9465
- assignmentDir: resolve17(rec.projectPath, "assignments", a.slug)
9965
+ assignmentDir: resolve18(rec.projectPath, "assignments", a.slug)
9466
9966
  });
9467
9967
  }
9468
9968
  }
@@ -9495,14 +9995,14 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir2) {
9495
9995
  }
9496
9996
  async function countMentionsInAssignment(sourceDir, target) {
9497
9997
  const bodies = [];
9498
- const assignmentMd = resolve17(sourceDir, "assignment.md");
9998
+ const assignmentMd = resolve18(sourceDir, "assignment.md");
9499
9999
  if (await fileExists(assignmentMd)) {
9500
10000
  const content = await readFile13(assignmentMd, "utf-8");
9501
10001
  const todosMatch = content.match(/^## Todos\s*$([\s\S]*?)(?=^## |$(?![\r\n]))/m);
9502
10002
  if (todosMatch) bodies.push(todosMatch[1]);
9503
10003
  }
9504
10004
  for (const filename of ["progress.md", "comments.md", "handoff.md"]) {
9505
- const path = resolve17(sourceDir, filename);
10005
+ const path = resolve18(sourceDir, filename);
9506
10006
  if (await fileExists(path)) {
9507
10007
  try {
9508
10008
  bodies.push(await readFile13(path, "utf-8"));
@@ -9563,45 +10063,45 @@ async function getAssignmentDetailById(projectsDir, assignmentsDir2, id) {
9563
10063
  }
9564
10064
  async function buildStandaloneAssignmentDetail(resolved) {
9565
10065
  const assignmentDir = resolved.assignmentDir;
9566
- const assignmentMdPath = resolve17(assignmentDir, "assignment.md");
10066
+ const assignmentMdPath = resolve18(assignmentDir, "assignment.md");
9567
10067
  if (!await fileExists(assignmentMdPath)) return null;
9568
10068
  const assignmentContent = await readFile13(assignmentMdPath, "utf-8");
9569
10069
  const assignment = parseAssignmentFull(assignmentContent);
9570
10070
  let plan = null;
9571
10071
  const planFile = await latestPlanFile(assignmentDir);
9572
10072
  if (planFile) {
9573
- const planPath = resolve17(assignmentDir, planFile);
10073
+ const planPath = resolve18(assignmentDir, planFile);
9574
10074
  if (await fileExists(planPath)) {
9575
10075
  const parsed = parsePlan(await readFile13(planPath, "utf-8"));
9576
10076
  plan = { status: parsed.status, updated: parsed.updated, body: parsed.body };
9577
10077
  }
9578
10078
  }
9579
10079
  let scratchpad = null;
9580
- const scratchpadPath = resolve17(assignmentDir, "scratchpad.md");
10080
+ const scratchpadPath = resolve18(assignmentDir, "scratchpad.md");
9581
10081
  if (await fileExists(scratchpadPath)) {
9582
10082
  const parsed = parseScratchpad(await readFile13(scratchpadPath, "utf-8"));
9583
10083
  scratchpad = { updated: parsed.updated, body: parsed.body };
9584
10084
  }
9585
10085
  let handoff = null;
9586
- const handoffPath = resolve17(assignmentDir, "handoff.md");
10086
+ const handoffPath = resolve18(assignmentDir, "handoff.md");
9587
10087
  if (await fileExists(handoffPath)) {
9588
10088
  const parsed = parseHandoff(await readFile13(handoffPath, "utf-8"));
9589
10089
  handoff = { updated: parsed.updated, handoffCount: parsed.handoffCount, body: parsed.body };
9590
10090
  }
9591
10091
  let decisionRecord = null;
9592
- const decisionRecordPath = resolve17(assignmentDir, "decision-record.md");
10092
+ const decisionRecordPath = resolve18(assignmentDir, "decision-record.md");
9593
10093
  if (await fileExists(decisionRecordPath)) {
9594
10094
  const parsed = parseDecisionRecord(await readFile13(decisionRecordPath, "utf-8"));
9595
10095
  decisionRecord = { updated: parsed.updated, decisionCount: parsed.decisionCount, body: parsed.body };
9596
10096
  }
9597
10097
  let progress = null;
9598
- const progressPath = resolve17(assignmentDir, "progress.md");
10098
+ const progressPath = resolve18(assignmentDir, "progress.md");
9599
10099
  if (await fileExists(progressPath)) {
9600
10100
  const parsed = parseProgress(await readFile13(progressPath, "utf-8"));
9601
10101
  progress = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
9602
10102
  }
9603
10103
  let comments = null;
9604
- const commentsPath = resolve17(assignmentDir, "comments.md");
10104
+ const commentsPath = resolve18(assignmentDir, "comments.md");
9605
10105
  if (await fileExists(commentsPath)) {
9606
10106
  const parsed = parseComments(await readFile13(commentsPath, "utf-8"));
9607
10107
  comments = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
@@ -9667,8 +10167,8 @@ async function computeProjectRecords(projectsDir, traces) {
9667
10167
  const projectDirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."));
9668
10168
  const maybeRecords = await Promise.all(
9669
10169
  projectDirs.map(async (entry) => {
9670
- const projectPath = resolve17(projectsDir, entry.name);
9671
- const projectMdPath = resolve17(projectPath, "project.md");
10170
+ const projectPath = resolve18(projectsDir, entry.name);
10171
+ const projectMdPath = resolve18(projectPath, "project.md");
9672
10172
  if (!await fileExists(projectMdPath)) {
9673
10173
  return null;
9674
10174
  }
@@ -9715,7 +10215,7 @@ async function computeProjectRecords(projectsDir, traces) {
9715
10215
  return records;
9716
10216
  }
9717
10217
  async function listAssignmentRecords(projectPath, traces) {
9718
- const assignmentsDir2 = resolve17(projectPath, "assignments");
10218
+ const assignmentsDir2 = resolve18(projectPath, "assignments");
9719
10219
  if (!await fileExists(assignmentsDir2)) {
9720
10220
  return [];
9721
10221
  }
@@ -9723,7 +10223,7 @@ async function listAssignmentRecords(projectPath, traces) {
9723
10223
  const dirEntries = entries.filter((entry) => entry.isDirectory());
9724
10224
  const maybeRecords = await Promise.all(
9725
10225
  dirEntries.map(async (entry) => {
9726
- const assignmentMd = resolve17(assignmentsDir2, entry.name, "assignment.md");
10226
+ const assignmentMd = resolve18(assignmentsDir2, entry.name, "assignment.md");
9727
10227
  if (!await fileExists(assignmentMd)) {
9728
10228
  return null;
9729
10229
  }
@@ -9739,7 +10239,7 @@ async function listAssignmentRecords(projectPath, traces) {
9739
10239
  return records;
9740
10240
  }
9741
10241
  async function listResources(projectPath) {
9742
- const resourcesDir = resolve17(projectPath, "resources");
10242
+ const resourcesDir = resolve18(projectPath, "resources");
9743
10243
  if (!await fileExists(resourcesDir)) {
9744
10244
  return [];
9745
10245
  }
@@ -9749,7 +10249,7 @@ async function listResources(projectPath) {
9749
10249
  if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_")) {
9750
10250
  continue;
9751
10251
  }
9752
- const filePath = resolve17(resourcesDir, entry.name);
10252
+ const filePath = resolve18(resourcesDir, entry.name);
9753
10253
  const content = await readFile13(filePath, "utf-8");
9754
10254
  const parsed = parseResource(content);
9755
10255
  results.push({
@@ -9765,7 +10265,7 @@ async function listResources(projectPath) {
9765
10265
  return results;
9766
10266
  }
9767
10267
  async function listMemories(projectPath) {
9768
- const memoriesDir = resolve17(projectPath, "memories");
10268
+ const memoriesDir = resolve18(projectPath, "memories");
9769
10269
  if (!await fileExists(memoriesDir)) {
9770
10270
  return [];
9771
10271
  }
@@ -9775,7 +10275,7 @@ async function listMemories(projectPath) {
9775
10275
  if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_")) {
9776
10276
  continue;
9777
10277
  }
9778
- const filePath = resolve17(memoriesDir, entry.name);
10278
+ const filePath = resolve18(memoriesDir, entry.name);
9779
10279
  const content = await readFile13(filePath, "utf-8");
9780
10280
  const parsed = parseMemory(content);
9781
10281
  results.push({
@@ -9824,8 +10324,8 @@ async function listAllResources(projectsDir) {
9824
10324
  return all;
9825
10325
  }
9826
10326
  async function resolveProjectPath(projectsDir, projectSlug) {
9827
- const direct = resolve17(projectsDir, projectSlug);
9828
- if (await fileExists(resolve17(direct, "project.md"))) return direct;
10327
+ const direct = resolve18(projectsDir, projectSlug);
10328
+ if (await fileExists(resolve18(direct, "project.md"))) return direct;
9829
10329
  const records = await listProjectRecords(projectsDir);
9830
10330
  const match = records.find((r) => r.summary.slug === projectSlug);
9831
10331
  return match ? match.projectPath : null;
@@ -9837,7 +10337,7 @@ async function getMemoryDetail(projectsDir, projectSlug, itemSlug) {
9837
10337
  (p) => basename(p.projectPath) === projectSlug || p.summary.slug === projectSlug
9838
10338
  );
9839
10339
  if (!projectRecord) return null;
9840
- const filePath = resolve17(projectRecord.projectPath, "memories", `${itemSlug}.md`);
10340
+ const filePath = resolve18(projectRecord.projectPath, "memories", `${itemSlug}.md`);
9841
10341
  if (!await fileExists(filePath)) return null;
9842
10342
  const content = await readFile13(filePath, "utf-8");
9843
10343
  const parsed = parseMemory(content);
@@ -9863,7 +10363,7 @@ async function getResourceDetail(projectsDir, projectSlug, itemSlug) {
9863
10363
  (p) => basename(p.projectPath) === projectSlug || p.summary.slug === projectSlug
9864
10364
  );
9865
10365
  if (!projectRecord) return null;
9866
- const filePath = resolve17(projectRecord.projectPath, "resources", `${itemSlug}.md`);
10366
+ const filePath = resolve18(projectRecord.projectPath, "resources", `${itemSlug}.md`);
9867
10367
  if (!await fileExists(filePath)) return null;
9868
10368
  const content = await readFile13(filePath, "utf-8");
9869
10369
  const parsed = parseResource(content);
@@ -9881,7 +10381,7 @@ async function getResourceDetail(projectsDir, projectSlug, itemSlug) {
9881
10381
  };
9882
10382
  }
9883
10383
  async function loadDependencyGraph(projectPath, assignments) {
9884
- const statusPath = resolve17(projectPath, "_status.md");
10384
+ const statusPath = resolve18(projectPath, "_status.md");
9885
10385
  if (await fileExists(statusPath)) {
9886
10386
  const statusContent = await readFile13(statusPath, "utf-8");
9887
10387
  const parsed = parseStatus(statusContent);
@@ -10050,7 +10550,7 @@ function toAssignmentSummary(assignment, terminalStatuses) {
10050
10550
  async function toAssignmentBoardItem(projectsDir, projectRecord, assignment) {
10051
10551
  const config = await getStatusConfig();
10052
10552
  const { terminalStatuses } = config;
10053
- const assignmentDir = resolve17(projectRecord.projectPath, "assignments", assignment.slug);
10553
+ const assignmentDir = resolve18(projectRecord.projectPath, "assignments", assignment.slug);
10054
10554
  const projectDir = projectRecord.projectPath;
10055
10555
  let facts;
10056
10556
  try {
@@ -10111,7 +10611,7 @@ async function getAvailableTransitions(projectsDir, projectSlug, assignmentSlug,
10111
10611
  const config = await getStatusConfig();
10112
10612
  const transitionDefs = getTransitionDefinitions(config);
10113
10613
  const actions = [];
10114
- const projectPath = resolve17(projectsDir, projectSlug);
10614
+ const projectPath = resolve18(projectsDir, projectSlug);
10115
10615
  const traces = options?.traces;
10116
10616
  for (const definition of transitionDefs) {
10117
10617
  const target = getTargetStatus(assignment.status, definition.command, config.transitionTable);
@@ -10159,7 +10659,7 @@ async function getUnmetDependencies(projectPath, dependsOn, terminalStatuses, de
10159
10659
  continue;
10160
10660
  }
10161
10661
  }
10162
- const dependencyPath = resolve17(projectPath, "assignments", dependency, "assignment.md");
10662
+ const dependencyPath = resolve18(projectPath, "assignments", dependency, "assignment.md");
10163
10663
  if (!await fileExists(dependencyPath)) {
10164
10664
  unmet.push(`${dependency} (missing)`);
10165
10665
  continue;
@@ -10239,13 +10739,13 @@ async function collectStaleCandidates(projectsDir, assignmentsDir2) {
10239
10739
  const out = [];
10240
10740
  for (const record of projectRecords) {
10241
10741
  if (isProjectArchived(record.summary)) continue;
10242
- const projectPath = resolve17(projectsDir, record.summary.slug);
10742
+ const projectPath = resolve18(projectsDir, record.summary.slug);
10243
10743
  const depMap = /* @__PURE__ */ new Map();
10244
10744
  for (const a of record.assignments) depMap.set(a.slug, a.status);
10245
10745
  for (const assignment of activeAssignments(record.assignments)) {
10246
10746
  const depsSatisfied = assignment.dependsOn.length === 0 ? true : (await getUnmetDependencies(projectPath, assignment.dependsOn, terminalStatuses, depMap)).length === 0;
10247
10747
  const lastActivityMs = await readProgressActivityMs(
10248
- resolve17(projectPath, "assignments", assignment.slug, "progress.md"),
10748
+ resolve18(projectPath, "assignments", assignment.slug, "progress.md"),
10249
10749
  now
10250
10750
  );
10251
10751
  const reasons = classifyAssignmentRecord(assignment, terminalStatuses, depsSatisfied, lastActivityMs, thresholds);
@@ -10256,7 +10756,7 @@ async function collectStaleCandidates(projectsDir, assignmentsDir2) {
10256
10756
  }
10257
10757
  for (const sr of standaloneRecords) {
10258
10758
  if (sr.record.archived === true) continue;
10259
- const lastActivityMs = await readProgressActivityMs(resolve17(sr.assignmentDir, "progress.md"), now);
10759
+ const lastActivityMs = await readProgressActivityMs(resolve18(sr.assignmentDir, "progress.md"), now);
10260
10760
  const reasons = classifyAssignmentRecord(sr.record, terminalStatuses, true, lastActivityMs, thresholds);
10261
10761
  if (reasons.length > 0) out.push({ assignmentId: sr.record.id, projectSlug: null, reasons });
10262
10762
  }
@@ -10275,7 +10775,7 @@ async function buildOverviewSegmentBuckets(projectsDir, projectRecords, standalo
10275
10775
  depMap.set(a.slug, a.status);
10276
10776
  }
10277
10777
  const visibleAssignments = activeAssignments(record.assignments);
10278
- const projectPath = resolve17(projectsDir, record.summary.slug);
10778
+ const projectPath = resolve18(projectsDir, record.summary.slug);
10279
10779
  const resolvedTransitions = await Promise.all(
10280
10780
  visibleAssignments.map(async (assignment) => {
10281
10781
  const t0 = traces ? performance.now() : 0;
@@ -10289,7 +10789,7 @@ async function buildOverviewSegmentBuckets(projectsDir, projectRecords, standalo
10289
10789
  if (traces) accumulatePhase(traces, "get-available-transitions", performance.now() - t0);
10290
10790
  const depsSatisfied = assignment.dependsOn.length === 0 ? true : (await getUnmetDependencies(projectPath, assignment.dependsOn, terminalStatuses, depMap)).length === 0;
10291
10791
  const lastActivityMs = await readProgressActivityMs(
10292
- resolve17(projectPath, "assignments", assignment.slug, "progress.md"),
10792
+ resolve18(projectPath, "assignments", assignment.slug, "progress.md"),
10293
10793
  now
10294
10794
  );
10295
10795
  return { assignment, availableTransitions, depsSatisfied, lastActivityMs };
@@ -10363,7 +10863,7 @@ async function buildOverviewSegmentBuckets(projectsDir, projectRecords, standalo
10363
10863
  const t0 = traces ? performance.now() : 0;
10364
10864
  const availableTransitions = await getStandaloneAvailableTransitions(sr.record);
10365
10865
  if (traces) accumulatePhase(traces, "get-available-transitions", performance.now() - t0);
10366
- const lastActivityMs = await readProgressActivityMs(resolve17(sr.assignmentDir, "progress.md"), now);
10866
+ const lastActivityMs = await readProgressActivityMs(resolve18(sr.assignmentDir, "progress.md"), now);
10367
10867
  return { sr, availableTransitions, lastActivityMs };
10368
10868
  })
10369
10869
  );
@@ -10533,7 +11033,7 @@ function parseTimestamp(timestamp) {
10533
11033
  return Number.isFinite(parsed) ? parsed : 0;
10534
11034
  }
10535
11035
  async function countOpenQuestions(projectPath, assignmentSlug) {
10536
- const commentsPath = resolve17(
11036
+ const commentsPath = resolve18(
10537
11037
  projectPath,
10538
11038
  "assignments",
10539
11039
  assignmentSlug,
@@ -10564,21 +11064,21 @@ function getProjectActivityTimestamp(projectUpdated, assignments) {
10564
11064
  function getDocumentPath(projectsDir, documentType, projectSlug, assignmentSlug) {
10565
11065
  switch (documentType) {
10566
11066
  case "project":
10567
- return resolve17(projectsDir, projectSlug, "project.md");
11067
+ return resolve18(projectsDir, projectSlug, "project.md");
10568
11068
  case "assignment":
10569
- return assignmentSlug ? resolve17(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md") : null;
11069
+ return assignmentSlug ? resolve18(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md") : null;
10570
11070
  case "plan":
10571
- return assignmentSlug ? resolve17(projectsDir, projectSlug, "assignments", assignmentSlug, "plan.md") : null;
11071
+ return assignmentSlug ? resolve18(projectsDir, projectSlug, "assignments", assignmentSlug, "plan.md") : null;
10572
11072
  case "scratchpad":
10573
- return assignmentSlug ? resolve17(projectsDir, projectSlug, "assignments", assignmentSlug, "scratchpad.md") : null;
11073
+ return assignmentSlug ? resolve18(projectsDir, projectSlug, "assignments", assignmentSlug, "scratchpad.md") : null;
10574
11074
  case "handoff":
10575
- return assignmentSlug ? resolve17(projectsDir, projectSlug, "assignments", assignmentSlug, "handoff.md") : null;
11075
+ return assignmentSlug ? resolve18(projectsDir, projectSlug, "assignments", assignmentSlug, "handoff.md") : null;
10576
11076
  case "decision-record":
10577
- return assignmentSlug ? resolve17(projectsDir, projectSlug, "assignments", assignmentSlug, "decision-record.md") : null;
11077
+ return assignmentSlug ? resolve18(projectsDir, projectSlug, "assignments", assignmentSlug, "decision-record.md") : null;
10578
11078
  case "memory":
10579
- return assignmentSlug ? resolve17(projectsDir, projectSlug, "memories", `${assignmentSlug}.md`) : null;
11079
+ return assignmentSlug ? resolve18(projectsDir, projectSlug, "memories", `${assignmentSlug}.md`) : null;
10580
11080
  case "resource":
10581
- return assignmentSlug ? resolve17(projectsDir, projectSlug, "resources", `${assignmentSlug}.md`) : null;
11081
+ return assignmentSlug ? resolve18(projectsDir, projectSlug, "resources", `${assignmentSlug}.md`) : null;
10582
11082
  default:
10583
11083
  return null;
10584
11084
  }
@@ -10615,7 +11115,7 @@ async function listPlaybooks(playbooksDir2) {
10615
11115
  const playbooks = [];
10616
11116
  for (const entry of entries) {
10617
11117
  if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_") || entry.name === "manifest.md") continue;
10618
- const filePath = resolve17(playbooksDir2, entry.name);
11118
+ const filePath = resolve18(playbooksDir2, entry.name);
10619
11119
  const raw2 = await readFile13(filePath, "utf-8");
10620
11120
  const parsed = parsePlaybook(raw2);
10621
11121
  const slug = parsed.slug || entry.name.replace(/\.md$/, "");
@@ -11220,12 +11720,12 @@ __export(recompute_exports, {
11220
11720
  });
11221
11721
  import { createHash as createHash2 } from "crypto";
11222
11722
  import { open, readdir as readdir12, readFile as readFile17, unlink as unlink5, stat as stat3 } from "fs/promises";
11223
- import { basename as basename3, dirname as dirname4, resolve as resolve22 } from "path";
11723
+ import { basename as basename3, dirname as dirname4, resolve as resolve23 } from "path";
11224
11724
  async function isDeriveMigrated() {
11225
- return fileExists(resolve22(syntaurRoot(), MIGRATION_MARKER));
11725
+ return fileExists(resolve23(syntaurRoot(), MIGRATION_MARKER));
11226
11726
  }
11227
11727
  async function markDeriveMigrated() {
11228
- await writeFileForce(resolve22(syntaurRoot(), MIGRATION_MARKER), `${nowTimestamp()}
11728
+ await writeFileForce(resolve23(syntaurRoot(), MIGRATION_MARKER), `${nowTimestamp()}
11229
11729
  `);
11230
11730
  }
11231
11731
  async function resolveDeriveContext() {
@@ -11242,7 +11742,7 @@ async function resolveDeriveContext() {
11242
11742
  };
11243
11743
  }
11244
11744
  async function acquireLock(assignmentDir) {
11245
- const lockPath = resolve22(assignmentDir, LOCK_FILE);
11745
+ const lockPath = resolve23(assignmentDir, LOCK_FILE);
11246
11746
  const token = `${process.pid}:${createHash2("sha256").update(`${Math.random()}${Date.now()}`).digest("hex").slice(0, 12)}`;
11247
11747
  for (let attempt = 0; attempt <= LOCK_MAX_WAITS; attempt++) {
11248
11748
  try {
@@ -11386,7 +11886,7 @@ async function recomputeAndWrite(assignmentPath, opts) {
11386
11886
  }
11387
11887
  }
11388
11888
  async function recomputeDependents(projectDir, changedSlug, opts) {
11389
- const assignmentsDir2 = resolve22(projectDir, "assignments");
11889
+ const assignmentsDir2 = resolve23(projectDir, "assignments");
11390
11890
  let entries;
11391
11891
  try {
11392
11892
  entries = await readdir12(assignmentsDir2);
@@ -11396,7 +11896,7 @@ async function recomputeDependents(projectDir, changedSlug, opts) {
11396
11896
  const results = [];
11397
11897
  for (const slug of entries) {
11398
11898
  if (slug === changedSlug) continue;
11399
- const path = resolve22(assignmentsDir2, slug, "assignment.md");
11899
+ const path = resolve23(assignmentsDir2, slug, "assignment.md");
11400
11900
  if (!await fileExists(path)) continue;
11401
11901
  try {
11402
11902
  const fm = parseAssignmentFrontmatter(await readFile17(path, "utf-8"));
@@ -11426,8 +11926,8 @@ async function recomputeAll(projectsDir, standaloneDir, opts) {
11426
11926
  } catch {
11427
11927
  }
11428
11928
  for (const project of projects) {
11429
- const projectDir = resolve22(projectsDir, project);
11430
- const assignmentsDir2 = resolve22(projectDir, "assignments");
11929
+ const projectDir = resolve23(projectsDir, project);
11930
+ const assignmentsDir2 = resolve23(projectDir, "assignments");
11431
11931
  let slugs = [];
11432
11932
  try {
11433
11933
  slugs = await readdir12(assignmentsDir2);
@@ -11435,7 +11935,7 @@ async function recomputeAll(projectsDir, standaloneDir, opts) {
11435
11935
  continue;
11436
11936
  }
11437
11937
  for (const slug of slugs) {
11438
- const path = resolve22(assignmentsDir2, slug, "assignment.md");
11938
+ const path = resolve23(assignmentsDir2, slug, "assignment.md");
11439
11939
  if (await fileExists(path)) await sweepOne(path, projectDir);
11440
11940
  }
11441
11941
  }
@@ -11446,7 +11946,7 @@ async function recomputeAll(projectsDir, standaloneDir, opts) {
11446
11946
  } catch {
11447
11947
  }
11448
11948
  for (const id of ids) {
11449
- const path = resolve22(standaloneDir, id, "assignment.md");
11949
+ const path = resolve23(standaloneDir, id, "assignment.md");
11450
11950
  if (await fileExists(path)) await sweepOne(path, null);
11451
11951
  }
11452
11952
  }
@@ -11454,7 +11954,7 @@ async function recomputeAll(projectsDir, standaloneDir, opts) {
11454
11954
  }
11455
11955
  async function recomputeAssignmentDir(assignmentDir, cause, by) {
11456
11956
  try {
11457
- const assignmentPath = resolve22(assignmentDir, "assignment.md");
11957
+ const assignmentPath = resolve23(assignmentDir, "assignment.md");
11458
11958
  if (!await fileExists(assignmentPath)) return null;
11459
11959
  const parent = dirname4(assignmentDir);
11460
11960
  const projectDir = basename3(parent) === "assignments" ? dirname4(parent) : null;
@@ -11486,6 +11986,27 @@ var init_recompute = __esm({
11486
11986
  }
11487
11987
  });
11488
11988
 
11989
+ // src/utils/process-info.ts
11990
+ import { execFileSync as execFileSync2 } from "child_process";
11991
+ function captureProcessStartedAt(pid) {
11992
+ if (!Number.isFinite(pid) || pid <= 0) return null;
11993
+ try {
11994
+ const out = execFileSync2("ps", ["-o", "lstart=", "-p", String(pid)], {
11995
+ encoding: "utf8",
11996
+ stdio: ["ignore", "pipe", "ignore"]
11997
+ });
11998
+ const trimmed = out.trim();
11999
+ return trimmed === "" ? null : trimmed;
12000
+ } catch {
12001
+ return null;
12002
+ }
12003
+ }
12004
+ var init_process_info = __esm({
12005
+ "src/utils/process-info.ts"() {
12006
+ "use strict";
12007
+ }
12008
+ });
12009
+
11489
12010
  // src/utils/transcript.ts
11490
12011
  import { open as open2 } from "fs/promises";
11491
12012
  async function derivePathFromTranscript(transcriptPath) {
@@ -11549,31 +12070,10 @@ var init_transcript = __esm({
11549
12070
  }
11550
12071
  });
11551
12072
 
11552
- // src/utils/process-info.ts
11553
- import { execFileSync as execFileSync2 } from "child_process";
11554
- function captureProcessStartedAt(pid) {
11555
- if (!Number.isFinite(pid) || pid <= 0) return null;
11556
- try {
11557
- const out = execFileSync2("ps", ["-o", "lstart=", "-p", String(pid)], {
11558
- encoding: "utf8",
11559
- stdio: ["ignore", "pipe", "ignore"]
11560
- });
11561
- const trimmed = out.trim();
11562
- return trimmed === "" ? null : trimmed;
11563
- } catch {
11564
- return null;
11565
- }
11566
- }
11567
- var init_process_info = __esm({
11568
- "src/utils/process-info.ts"() {
11569
- "use strict";
11570
- }
11571
- });
11572
-
11573
12073
  // src/usage/cwd-extractor.ts
11574
12074
  import { open as open3, readdir as readdir13, stat as stat4 } from "fs/promises";
11575
- import { join as join4 } from "path";
11576
- import { homedir as homedir3 } from "os";
12075
+ import { join as join3 } from "path";
12076
+ import { homedir as homedir2 } from "os";
11577
12077
  async function extractClaudeSessionMeta(jsonlPath) {
11578
12078
  const cwd = await derivePathFromTranscript(jsonlPath);
11579
12079
  if (!cwd) return null;
@@ -11646,12 +12146,12 @@ async function* walkClaudeProjects(opts = {}) {
11646
12146
  const dirs = await listDirSafe(root);
11647
12147
  for (const dirent of dirs) {
11648
12148
  if (!dirent.isDirectory) continue;
11649
- const dirPath = join4(root, dirent.name);
12149
+ const dirPath = join3(root, dirent.name);
11650
12150
  const files = await listDirSafe(dirPath);
11651
12151
  let cachedCwd = null;
11652
12152
  for (const f of files) {
11653
12153
  if (!f.isFile || !f.name.endsWith(".jsonl")) continue;
11654
- const filePath = join4(dirPath, f.name);
12154
+ const filePath = join3(dirPath, f.name);
11655
12155
  if (opts.sinceMtimeMs !== void 0) {
11656
12156
  const mtime = await mtimeMs(filePath);
11657
12157
  if (mtime !== null && mtime < opts.sinceMtimeMs) continue;
@@ -11683,13 +12183,13 @@ async function* walkCodexSessions(opts = {}) {
11683
12183
  if (meta) yield meta;
11684
12184
  }
11685
12185
  }
11686
- function resolveCodexSessionsRoot(override) {
11687
- if (override) return expandHome(override);
12186
+ function resolveCodexSessionsRoot(override3) {
12187
+ if (override3) return expandHome(override3);
11688
12188
  const fromSessionsEnv = process.env.CODEX_SESSIONS_DIR;
11689
12189
  if (fromSessionsEnv && fromSessionsEnv.length > 0) return expandHome(fromSessionsEnv);
11690
12190
  const fromHomeEnv = process.env.CODEX_HOME;
11691
- if (fromHomeEnv && fromHomeEnv.length > 0) return join4(expandHome(fromHomeEnv), "sessions");
11692
- return join4(homedir3(), ".codex", "sessions");
12191
+ if (fromHomeEnv && fromHomeEnv.length > 0) return join3(expandHome(fromHomeEnv), "sessions");
12192
+ return join3(homedir2(), ".codex", "sessions");
11693
12193
  }
11694
12194
  async function extractPiSessionMeta(jsonlPath) {
11695
12195
  const basename7 = jsonlPath.split("/").pop() ?? "";
@@ -11743,23 +12243,23 @@ async function extractPiSessionMeta(jsonlPath) {
11743
12243
  });
11744
12244
  }
11745
12245
  }
11746
- function resolvePiSessionsRoot(override) {
11747
- if (override) return expandHome(override);
12246
+ function resolvePiSessionsRoot(override3) {
12247
+ if (override3) return expandHome(override3);
11748
12248
  const fromHomeEnv = process.env.PI_AGENT_DIR;
11749
- if (fromHomeEnv && fromHomeEnv.length > 0) return join4(expandHome(fromHomeEnv), "sessions");
11750
- return join4(homedir3(), ".pi", "agent", "sessions");
12249
+ if (fromHomeEnv && fromHomeEnv.length > 0) return join3(expandHome(fromHomeEnv), "sessions");
12250
+ return join3(homedir2(), ".pi", "agent", "sessions");
11751
12251
  }
11752
12252
  async function* walkPiSessions(opts = {}) {
11753
12253
  const root = resolvePiSessionsRoot(opts.root);
11754
12254
  const dirs = await listDirSafe(root);
11755
12255
  for (const dirent of dirs) {
11756
12256
  if (!dirent.isDirectory) continue;
11757
- const dirPath = join4(root, dirent.name);
12257
+ const dirPath = join3(root, dirent.name);
11758
12258
  const files = await listDirSafe(dirPath);
11759
12259
  let cachedCwd = null;
11760
12260
  for (const f of files) {
11761
12261
  if (!f.isFile || !f.name.endsWith(".jsonl")) continue;
11762
- const filePath = join4(dirPath, f.name);
12262
+ const filePath = join3(dirPath, f.name);
11763
12263
  if (opts.sinceMtimeMs !== void 0) {
11764
12264
  const mtime = await mtimeMs(filePath);
11765
12265
  if (mtime !== null && mtime < opts.sinceMtimeMs) continue;
@@ -11799,7 +12299,7 @@ async function* walkJsonlRecursive(root) {
11799
12299
  const current = stack.pop();
11800
12300
  const entries = await listDirSafe(current);
11801
12301
  for (const e of entries) {
11802
- const full = join4(current, e.name);
12302
+ const full = join3(current, e.name);
11803
12303
  if (e.isDirectory) {
11804
12304
  stack.push(full);
11805
12305
  } else if (e.isFile && e.name.endsWith(".jsonl")) {
@@ -11910,17 +12410,17 @@ var init_cwd_extractor = __esm({
11910
12410
 
11911
12411
  // src/utils/session-id.ts
11912
12412
  import { execFileSync as execFileSync3 } from "child_process";
11913
- import { mkdirSync, readFileSync, statSync as statSync3, writeFileSync } from "fs";
11914
- import { homedir as homedir4 } from "os";
11915
- import { dirname as dirname5, join as join5 } from "path";
12413
+ import { mkdirSync, readFileSync as readFileSync2, statSync as statSync4, writeFileSync } from "fs";
12414
+ import { homedir as homedir3 } from "os";
12415
+ import { dirname as dirname5, join as join4 } from "path";
11916
12416
  function isSafeSessionId(value) {
11917
12417
  return typeof value === "string" && value.length > 0 && value.length <= 256 && SAFE_SESSION_ID.test(value);
11918
12418
  }
11919
12419
  function readRuntimeMarker(pid, dir) {
11920
12420
  if (!Number.isInteger(pid) || pid <= 0) return null;
11921
- const path = join5(dir, `${pid}.json`);
12421
+ const path = join4(dir, `${pid}.json`);
11922
12422
  try {
11923
- const parsed = JSON.parse(readFileSync(path, "utf8"));
12423
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
11924
12424
  if (parsed && typeof parsed === "object" && typeof parsed.sessionId === "string" && parsed.sessionId.length > 0) {
11925
12425
  return parsed;
11926
12426
  }
@@ -11930,7 +12430,7 @@ function readRuntimeMarker(pid, dir) {
11930
12430
  }
11931
12431
  }
11932
12432
  function writeRuntimeMarker(pid, marker, dir) {
11933
- const path = join5(dir, `${pid}.json`);
12433
+ const path = join4(dir, `${pid}.json`);
11934
12434
  mkdirSync(dirname5(path), { recursive: true });
11935
12435
  writeFileSync(path, JSON.stringify(marker));
11936
12436
  }
@@ -11993,12 +12493,12 @@ var init_assignment_todos = __esm({
11993
12493
 
11994
12494
  // src/utils/path-canon.ts
11995
12495
  import { realpathSync as realpathSync2 } from "fs";
11996
- import { resolve as resolve43 } from "path";
12496
+ import { resolve as resolve44 } from "path";
11997
12497
  function canonicalPath(p) {
11998
12498
  try {
11999
- return realpathSync2(resolve43(p));
12499
+ return realpathSync2(resolve44(p));
12000
12500
  } catch {
12001
- return resolve43(p).replace(/\/+$/, "");
12501
+ return resolve44(p).replace(/\/+$/, "");
12002
12502
  }
12003
12503
  }
12004
12504
  var init_path_canon = __esm({
@@ -12027,7 +12527,7 @@ var init_renderers = __esm({
12027
12527
  });
12028
12528
 
12029
12529
  // src/targets/user-descriptors.ts
12030
- import { resolve as resolve44 } from "path";
12530
+ import { resolve as resolve45 } from "path";
12031
12531
  import { readFile as readFile30, readdir as readdir18 } from "fs/promises";
12032
12532
  var VALID_RENDERER_KEYS;
12033
12533
  var init_user_descriptors = __esm({
@@ -12042,20 +12542,20 @@ var init_user_descriptors = __esm({
12042
12542
 
12043
12543
  // src/targets/registry.ts
12044
12544
  import { homedir as homedir7 } from "os";
12045
- import { join as join10, resolve as resolve45 } from "path";
12545
+ import { join as join10, resolve as resolve46 } from "path";
12046
12546
  function home(...segments) {
12047
- return resolve45(homedir7(), ...segments);
12547
+ return resolve46(homedir7(), ...segments);
12048
12548
  }
12049
12549
  function hermesHome() {
12050
12550
  const env = process.env.HERMES_HOME;
12051
- return env && env.length > 0 ? resolve45(env) : home(".hermes");
12551
+ return env && env.length > 0 ? resolve46(env) : home(".hermes");
12052
12552
  }
12053
12553
  function hermesSkillsDir() {
12054
- return resolve45(hermesHome(), "skills");
12554
+ return resolve46(hermesHome(), "skills");
12055
12555
  }
12056
12556
  function codexHome() {
12057
12557
  const env = process.env.CODEX_HOME;
12058
- return env && env.length > 0 ? resolve45(env) : home(".codex");
12558
+ return env && env.length > 0 ? resolve46(env) : home(".codex");
12059
12559
  }
12060
12560
  function toDiscovered(meta) {
12061
12561
  if (!meta) return null;
@@ -12126,7 +12626,7 @@ var init_registry = __esm({
12126
12626
  skillsShAgentId: "codex",
12127
12627
  nativePlugin: "codex",
12128
12628
  detect: detectDir(codexHome()),
12129
- skillsDir: { global: resolve45(codexHome(), "skills") },
12629
+ skillsDir: { global: resolve46(codexHome(), "skills") },
12130
12630
  instructions: { files: [{ path: "AGENTS.md", renderer: "codexAgents" }] },
12131
12631
  sessions: codexSessions
12132
12632
  },
@@ -12195,7 +12695,7 @@ var init_registry = __esm({
12195
12695
  tier3: {
12196
12696
  kind: "hermes-plugin",
12197
12697
  source: "platforms/hermes/plugins/syntaur",
12198
- installDir: () => resolve45(hermesHome(), "plugins", "syntaur"),
12698
+ installDir: () => resolve46(hermesHome(), "plugins", "syntaur"),
12199
12699
  entry: "plugin.yaml"
12200
12700
  }
12201
12701
  }
@@ -12206,22 +12706,75 @@ var init_registry = __esm({
12206
12706
  }
12207
12707
  });
12208
12708
 
12709
+ // src/sessions/agent-view.ts
12710
+ import { execFile as execFile3 } from "child_process";
12711
+ import { promisify as promisify3 } from "util";
12712
+ function toActivityState(raw2) {
12713
+ switch (raw2) {
12714
+ case "working":
12715
+ case "idle":
12716
+ case "awaiting-input":
12717
+ return raw2;
12718
+ default:
12719
+ return null;
12720
+ }
12721
+ }
12722
+ function parseAgentView(json) {
12723
+ const live = /* @__PURE__ */ new Map();
12724
+ let parsed;
12725
+ try {
12726
+ parsed = JSON.parse(json);
12727
+ } catch {
12728
+ return live;
12729
+ }
12730
+ const entries = Array.isArray(parsed) ? parsed : Array.isArray(parsed.agents) ? parsed.agents : [];
12731
+ for (const entry of entries) {
12732
+ if (entry === null || typeof entry !== "object") continue;
12733
+ const e = entry;
12734
+ const id = typeof e.session_id === "string" ? e.session_id : typeof e.sessionId === "string" ? e.sessionId : null;
12735
+ if (!id) continue;
12736
+ live.set(id, toActivityState(e.activity ?? e.status) ?? "idle");
12737
+ }
12738
+ return live;
12739
+ }
12740
+ function getAgentViewSource() {
12741
+ return override2 ?? productionAgentViewSource;
12742
+ }
12743
+ var execFileAsync, productionAgentViewSource, override2;
12744
+ var init_agent_view = __esm({
12745
+ "src/sessions/agent-view.ts"() {
12746
+ "use strict";
12747
+ execFileAsync = promisify3(execFile3);
12748
+ productionAgentViewSource = async () => {
12749
+ try {
12750
+ const { stdout } = await execFileAsync("claude", ["agents", "--json"], {
12751
+ timeout: 5e3,
12752
+ maxBuffer: 8 * 1024 * 1024
12753
+ });
12754
+ return parseAgentView(stdout);
12755
+ } catch {
12756
+ return /* @__PURE__ */ new Map();
12757
+ }
12758
+ };
12759
+ override2 = null;
12760
+ }
12761
+ });
12762
+
12209
12763
  // src/sessions/scanner.ts
12210
12764
  var scanner_exports2 = {};
12211
12765
  __export(scanner_exports2, {
12212
12766
  scanSessions: () => scanSessions
12213
12767
  });
12214
- import { execFile as execFile3, execFileSync as execFileSync4 } from "child_process";
12215
- import { promisify as promisify3 } from "util";
12216
- import { statSync as statSync4 } from "fs";
12217
- import { readFile as readFile31 } from "fs/promises";
12218
- import { resolve as resolve46 } from "path";
12768
+ import { execFile as execFile4, execFileSync as execFileSync4 } from "child_process";
12769
+ import { promisify as promisify4 } from "util";
12770
+ import { statSync as statSync5 } from "fs";
12771
+ import { resolve as resolve47 } from "path";
12219
12772
  function emptySummary() {
12220
12773
  return { discovered: 0, inserted: 0, revived: 0, swept: 0, skipped: 0, changed: false };
12221
12774
  }
12222
12775
  function defaultStatMtimeMs(path) {
12223
12776
  try {
12224
- return statSync4(path).mtimeMs;
12777
+ return statSync5(path).mtimeMs;
12225
12778
  } catch {
12226
12779
  return null;
12227
12780
  }
@@ -12254,7 +12807,7 @@ async function defaultOpenFiles(files) {
12254
12807
  const chunk = files.slice(i, i + LSOF_CHUNK);
12255
12808
  let stdout = "";
12256
12809
  try {
12257
- const result = await execFileAsync("lsof", ["-Fn", "--", ...chunk], {
12810
+ const result = await execFileAsync2("lsof", ["-Fn", "--", ...chunk], {
12258
12811
  maxBuffer: 8 * 1024 * 1024
12259
12812
  });
12260
12813
  stdout = result.stdout;
@@ -12273,23 +12826,12 @@ function canonicalizeOpenSet(open6) {
12273
12826
  for (const p of open6) out.add(canonicalPath(p));
12274
12827
  return out;
12275
12828
  }
12276
- async function readContextLink(cwd, cache3) {
12829
+ async function isWorkspace(cwd, cache3) {
12277
12830
  if (cache3.has(cwd)) return cache3.get(cwd);
12278
- let link = null;
12279
- const path = resolve46(cwd, ".syntaur", "context.json");
12280
- if (await fileExists(path)) {
12281
- try {
12282
- const parsed = JSON.parse(await readFile31(path, "utf-8"));
12283
- link = {
12284
- projectSlug: typeof parsed.projectSlug === "string" ? parsed.projectSlug : null,
12285
- assignmentSlug: typeof parsed.assignmentSlug === "string" ? parsed.assignmentSlug : null
12286
- };
12287
- } catch {
12288
- link = { projectSlug: null, assignmentSlug: null };
12289
- }
12290
- }
12291
- cache3.set(cwd, link);
12292
- return link;
12831
+ const path = resolve47(cwd, ".syntaur", "context.json");
12832
+ const present = await fileExists(path);
12833
+ cache3.set(cwd, present);
12834
+ return present;
12293
12835
  }
12294
12836
  function readWatermark() {
12295
12837
  const db5 = getSessionDb();
@@ -12313,9 +12855,16 @@ async function scanSessions(opts = {}, deps = {}) {
12313
12855
  const openFiles = deps.openFiles ?? defaultOpenFiles;
12314
12856
  const isPidAlive = deps.isPidAlive ?? defaultIsPidAlive;
12315
12857
  const pidStartedAt = deps.pidStartedAt ?? defaultPidStartedAt;
12858
+ const agentView = deps.agentView ?? getAgentViewSource();
12316
12859
  const targets = (deps.targets ?? AGENT_TARGETS).filter((t) => t.sessions !== void 0);
12317
12860
  const scanStartMs = now();
12318
12861
  const watermark = opts.full ? null : readWatermark();
12862
+ let liveActivity;
12863
+ try {
12864
+ liveActivity = await agentView();
12865
+ } catch {
12866
+ liveActivity = /* @__PURE__ */ new Map();
12867
+ }
12319
12868
  const discovered = [];
12320
12869
  for (const target of targets) {
12321
12870
  const walk = target.sessions.walk({
@@ -12331,16 +12880,17 @@ async function scanSessions(opts = {}, deps = {}) {
12331
12880
  const openSet = canonicalizeOpenSet(
12332
12881
  await openFiles(discovered.map((d) => d.transcriptPath))
12333
12882
  );
12334
- const contextCache = /* @__PURE__ */ new Map();
12883
+ const workspaceCache = /* @__PURE__ */ new Map();
12335
12884
  for (const d of discovered) {
12336
- const link = await readContextLink(d.cwd, contextCache);
12337
- if (autoTrack === "workspaces-only" && link === null) {
12885
+ const isWs = await isWorkspace(d.cwd, workspaceCache);
12886
+ if (autoTrack === "workspaces-only" && !isWs) {
12338
12887
  summary.skipped += 1;
12339
12888
  continue;
12340
12889
  }
12341
12890
  const mtime = statMtimeMs(d.transcriptPath);
12342
12891
  const heldOpen = openSet.has(canonicalPath(d.transcriptPath));
12343
- const isLive = heldOpen || mtime !== null && now() - mtime < FRESH_MTIME_MS;
12892
+ const agentViewLive = liveActivity.has(d.sessionId);
12893
+ const isLive = heldOpen || agentViewLive || mtime !== null && now() - mtime < FRESH_MTIME_MS;
12344
12894
  const prev = getSessionById(d.sessionId);
12345
12895
  const status = isLive ? "active" : prev?.status ?? "stopped";
12346
12896
  const started = d.startedAt ?? (mtime !== null ? new Date(mtime).toISOString() : new Date(now()).toISOString());
@@ -12348,8 +12898,11 @@ async function scanSessions(opts = {}, deps = {}) {
12348
12898
  "",
12349
12899
  {
12350
12900
  sessionId: d.sessionId,
12351
- projectSlug: link?.projectSlug ?? null,
12352
- assignmentSlug: link?.assignmentSlug ?? null,
12901
+ // Auto-discovered sessions are UNATTRIBUTED: the scanner no longer binds
12902
+ // them to the cwd context.json assignment scalar (see isWorkspace). An
12903
+ // explicit grab/track opens an engagement edge to attribute them.
12904
+ projectSlug: null,
12905
+ assignmentSlug: null,
12353
12906
  agent: d.agent,
12354
12907
  started,
12355
12908
  status,
@@ -12361,11 +12914,12 @@ async function scanSessions(opts = {}, deps = {}) {
12361
12914
  originalHeadSha: null
12362
12915
  },
12363
12916
  // Narrow revival rule: only LIVE-PROCESS evidence (a process holding the
12364
- // transcript open) may flip a stopped row back to active. mtime freshness
12365
- // alone must not — a session stopped moments ago by its SessionEnd hook
12366
- // still has a fresh transcript for up to 5 minutes and would flap back to
12367
- // active. `completed` always sticks (appendSession enforces).
12368
- { reviveStopped: heldOpen }
12917
+ // transcript open) OR Agent-View live evidence may flip a stopped row back
12918
+ // to active. mtime freshness alone must not — a session stopped moments ago
12919
+ // by its SessionEnd hook still has a fresh transcript for up to 5 minutes
12920
+ // and would flap back to active. `completed` always sticks (appendSession
12921
+ // enforces).
12922
+ { reviveStopped: heldOpen || agentViewLive }
12369
12923
  );
12370
12924
  if (!isLive) {
12371
12925
  const after = getSessionById(d.sessionId);
@@ -12378,19 +12932,25 @@ async function scanSessions(opts = {}, deps = {}) {
12378
12932
  summary.inserted += 1;
12379
12933
  summary.changed = true;
12380
12934
  } else {
12381
- if (prev.status === "stopped" && heldOpen) {
12935
+ if (prev.status === "stopped" && (heldOpen || agentViewLive)) {
12382
12936
  summary.revived += 1;
12383
12937
  summary.changed = true;
12384
12938
  }
12385
- if (link?.projectSlug && !prev.projectSlug || link?.assignmentSlug && !prev.assignmentSlug) {
12386
- summary.changed = true;
12387
- }
12388
12939
  }
12389
12940
  }
12390
12941
  const db5 = getSessionDb();
12942
+ if (liveActivity.size > 0) {
12943
+ const setActivity = db5.prepare(
12944
+ "UPDATE sessions SET activity = ?, updated_at = datetime('now') WHERE session_id = ? AND activity IS NOT ?"
12945
+ );
12946
+ for (const [sid, activity] of liveActivity) {
12947
+ if (setActivity.run(activity, sid, activity).changes > 0) summary.changed = true;
12948
+ }
12949
+ }
12391
12950
  const activeRows = db5.prepare("SELECT session_id, pid, pid_started_at, transcript_path FROM sessions WHERE status = 'active'").all();
12392
12951
  const sweepCandidates = [];
12393
12952
  for (const row of activeRows) {
12953
+ if (liveActivity.has(row.session_id)) continue;
12394
12954
  if (row.pid !== null) {
12395
12955
  const alive = isPidAlive(row.pid) && (!row.pid_started_at || (pidStartedAt(row.pid) ?? row.pid_started_at) === row.pid_started_at);
12396
12956
  if (alive) continue;
@@ -12407,16 +12967,27 @@ async function scanSessions(opts = {}, deps = {}) {
12407
12967
  )
12408
12968
  );
12409
12969
  for (const candidate of sweepCandidates) {
12970
+ let endedAt;
12410
12971
  if (candidate.transcriptPath) {
12411
12972
  if (sweepOpenSet.has(canonicalPath(candidate.transcriptPath))) continue;
12412
12973
  const mtime = statMtimeMs(candidate.transcriptPath);
12413
12974
  if (mtime !== null && now() - mtime < FRESH_MTIME_MS) continue;
12414
- const endedAt = mtime !== null ? new Date(mtime).toISOString() : void 0;
12415
- if (await updateSessionStatus("", candidate.sessionId, "stopped", endedAt)) {
12416
- summary.swept += 1;
12417
- summary.changed = true;
12418
- }
12419
- } else if (await updateSessionStatus("", candidate.sessionId, "stopped")) {
12975
+ endedAt = mtime !== null ? new Date(mtime).toISOString() : void 0;
12976
+ }
12977
+ const open6 = getOpenEngagement(candidate.sessionId);
12978
+ let snap = null;
12979
+ try {
12980
+ snap = await getCumulativeTokenSource()(candidate.sessionId);
12981
+ } catch {
12982
+ snap = null;
12983
+ }
12984
+ if (livenessStopSession({
12985
+ sessionId: candidate.sessionId,
12986
+ engagementId: open6?.id ?? null,
12987
+ engagementStartedAt: open6?.started_at ?? null,
12988
+ endedAt,
12989
+ tokensAtClose: snap
12990
+ })) {
12420
12991
  summary.swept += 1;
12421
12992
  summary.changed = true;
12422
12993
  }
@@ -12424,7 +12995,7 @@ async function scanSessions(opts = {}, deps = {}) {
12424
12995
  writeWatermark(scanStartMs);
12425
12996
  return summary;
12426
12997
  }
12427
- var execFileAsync, FRESH_MTIME_MS, LSOF_CHUNK, WATERMARK_KEY;
12998
+ var execFileAsync2, FRESH_MTIME_MS, LSOF_CHUNK, WATERMARK_KEY;
12428
12999
  var init_scanner2 = __esm({
12429
13000
  "src/sessions/scanner.ts"() {
12430
13001
  "use strict";
@@ -12435,7 +13006,10 @@ var init_scanner2 = __esm({
12435
13006
  init_registry();
12436
13007
  init_session_db();
12437
13008
  init_agent_sessions();
12438
- execFileAsync = promisify3(execFile3);
13009
+ init_engagement_db();
13010
+ init_engagement_tokens();
13011
+ init_agent_view();
13012
+ execFileAsync2 = promisify4(execFile4);
12439
13013
  FRESH_MTIME_MS = 5 * 60 * 1e3;
12440
13014
  LSOF_CHUNK = 64;
12441
13015
  WATERMARK_KEY = "sessions_scan_last_ms";
@@ -12483,19 +13057,19 @@ init_assignment_resolver();
12483
13057
  init_agent_sessions();
12484
13058
  import express from "express";
12485
13059
  import { createServer } from "http";
12486
- import { resolve as resolve47 } from "path";
13060
+ import { resolve as resolve48 } from "path";
12487
13061
  import { writeFile as writeFile8, unlink as unlink9 } from "fs/promises";
12488
13062
  import { WebSocketServer, WebSocket } from "ws";
12489
13063
 
12490
13064
  // src/dashboard/session-liveness.ts
12491
13065
  import { execFileSync } from "child_process";
12492
- import { statSync as statSync2 } from "fs";
13066
+ import { statSync as statSync3 } from "fs";
12493
13067
  var FIVE_MINUTES_MS = 5 * 60 * 1e3;
12494
13068
  var DEFAULT_DEPS = {
12495
13069
  now: () => Date.now(),
12496
13070
  statMtimeMs: (path) => {
12497
13071
  try {
12498
- return statSync2(path).mtimeMs;
13072
+ return statSync3(path).mtimeMs;
12499
13073
  } catch {
12500
13074
  return null;
12501
13075
  }
@@ -12991,10 +13565,10 @@ function mergePatch(current, patch) {
12991
13565
  init_paths();
12992
13566
  init_fs();
12993
13567
  import { readFile as readFile14, rename as rename3, unlink as unlink3 } from "fs/promises";
12994
- import { resolve as resolve18 } from "path";
13568
+ import { resolve as resolve19 } from "path";
12995
13569
  function corruptFilePath() {
12996
13570
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
12997
- return resolve18(syntaurRoot(), `view-prefs.corrupt-${ts}.json`);
13571
+ return resolve19(syntaurRoot(), `view-prefs.corrupt-${ts}.json`);
12998
13572
  }
12999
13573
  function isViewPrefsFileShape(value) {
13000
13574
  if (!value || typeof value !== "object") return false;
@@ -13266,10 +13840,10 @@ init_paths();
13266
13840
  init_fs();
13267
13841
  import { readFile as readFile15, rename as rename4, unlink as unlink4 } from "fs/promises";
13268
13842
  import { randomUUID as randomUUID2 } from "crypto";
13269
- import { resolve as resolve19 } from "path";
13843
+ import { resolve as resolve20 } from "path";
13270
13844
  function corruptFilePath2() {
13271
13845
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
13272
- return resolve19(syntaurRoot(), `saved-views.corrupt-${ts}.json`);
13846
+ return resolve20(syntaurRoot(), `saved-views.corrupt-${ts}.json`);
13273
13847
  }
13274
13848
  function isSavedViewsFileShape(value) {
13275
13849
  if (!value || typeof value !== "object") return false;
@@ -13644,19 +14218,19 @@ init_timestamp();
13644
14218
  init_fs();
13645
14219
  init_git_worktree();
13646
14220
  import { Router as Router2 } from "express";
13647
- import { resolve as resolve23, basename as basename4, isAbsolute as isAbsolute4 } from "path";
14221
+ import { resolve as resolve24, basename as basename4, isAbsolute as isAbsolute4 } from "path";
13648
14222
  import { rm, readFile as readFile18, open as fsOpen, stat as fsStat, realpath as fsRealpath } from "fs/promises";
13649
14223
  import { spawnSync as spawnSync3 } from "child_process";
13650
14224
 
13651
14225
  // src/utils/worktree-defaults.ts
13652
14226
  init_paths();
13653
14227
  import { spawnSync as spawnSync2 } from "child_process";
13654
- import { resolve as resolve20 } from "path";
14228
+ import { resolve as resolve21 } from "path";
13655
14229
  function computeWorktreeDefaults(opts) {
13656
14230
  const repository = opts.existing.repository ?? detectCurrentGitRoot(opts.cwd);
13657
14231
  const branch = opts.projectSlug ? `syntaur/${opts.projectSlug}/${opts.assignmentSlug}` : `syntaur/${opts.assignmentSlug}`;
13658
14232
  const parentBranch = opts.existing.parentBranch ?? detectCurrentBranch(opts.cwd) ?? "main";
13659
- const worktreeBase = repository ? resolve20(repository, ".worktrees", branch) : resolve20(
14233
+ const worktreeBase = repository ? resolve21(repository, ".worktrees", branch) : resolve21(
13660
14234
  syntaurRoot(),
13661
14235
  "worktrees",
13662
14236
  opts.projectSlug || "standalone",
@@ -13875,7 +14449,7 @@ function recreateOutcomeToHttp(outcome) {
13875
14449
  init_fs();
13876
14450
  init_parser();
13877
14451
  import { readdir as readdir11, readFile as readFile16 } from "fs/promises";
13878
- import { resolve as resolve21 } from "path";
14452
+ import { resolve as resolve22 } from "path";
13879
14453
  function toSourceAssignment(parsed, fallbackId) {
13880
14454
  const repository = parsed.workspace.repository?.trim();
13881
14455
  const branch = parsed.workspace.branch?.trim();
@@ -13892,29 +14466,29 @@ function toSourceAssignment(parsed, fallbackId) {
13892
14466
  async function getProjectRepositoryCandidates(projectsDir, projectSlug) {
13893
14467
  const seen = /* @__PURE__ */ new Set();
13894
14468
  const out = [];
13895
- const projectPath = resolve21(projectsDir, projectSlug, "project.md");
14469
+ const projectPath = resolve22(projectsDir, projectSlug, "project.md");
13896
14470
  if (await fileExists(projectPath)) {
13897
14471
  const project = parseProject(await readFile16(projectPath, "utf-8"));
13898
14472
  for (const raw2 of project.repositories) {
13899
14473
  const path = raw2.trim();
13900
14474
  if (!path) continue;
13901
- const abs = resolve21(path);
14475
+ const abs = resolve22(path);
13902
14476
  if (seen.has(abs)) continue;
13903
14477
  seen.add(abs);
13904
14478
  out.push({ path: abs, source: "project", sourceAssignmentSlug: null });
13905
14479
  }
13906
14480
  }
13907
- const assignmentsDir2 = resolve21(projectsDir, projectSlug, "assignments");
14481
+ const assignmentsDir2 = resolve22(projectsDir, projectSlug, "assignments");
13908
14482
  if (await fileExists(assignmentsDir2)) {
13909
14483
  const entries = await readdir11(assignmentsDir2, { withFileTypes: true });
13910
14484
  for (const entry of entries) {
13911
14485
  if (!entry.isDirectory()) continue;
13912
- const assignmentMd = resolve21(assignmentsDir2, entry.name, "assignment.md");
14486
+ const assignmentMd = resolve22(assignmentsDir2, entry.name, "assignment.md");
13913
14487
  if (!await fileExists(assignmentMd)) continue;
13914
14488
  const parsed = parseAssignmentFull(await readFile16(assignmentMd, "utf-8"));
13915
14489
  const repo = parsed.workspace.repository?.trim();
13916
14490
  if (!repo) continue;
13917
- const abs = resolve21(repo);
14491
+ const abs = resolve22(repo);
13918
14492
  if (seen.has(abs)) continue;
13919
14493
  seen.add(abs);
13920
14494
  out.push({ path: abs, source: "sibling", sourceAssignmentSlug: parsed.slug });
@@ -13932,12 +14506,12 @@ async function getStandaloneRepositoryCandidates(assignmentsDir2, excludeAssignm
13932
14506
  for (const entry of entries) {
13933
14507
  if (!entry.isDirectory()) continue;
13934
14508
  if (entry.name === excludeAssignmentId) continue;
13935
- const assignmentMd = resolve21(assignmentsDir2, entry.name, "assignment.md");
14509
+ const assignmentMd = resolve22(assignmentsDir2, entry.name, "assignment.md");
13936
14510
  if (!await fileExists(assignmentMd)) continue;
13937
14511
  const parsed = parseAssignmentFull(await readFile16(assignmentMd, "utf-8"));
13938
14512
  const repo = parsed.workspace.repository?.trim();
13939
14513
  if (!repo) continue;
13940
- const abs = resolve21(repo);
14514
+ const abs = resolve22(repo);
13941
14515
  if (seen.has(abs)) continue;
13942
14516
  seen.add(abs);
13943
14517
  out.push({ path: abs, source: "sibling", sourceAssignmentSlug: parsed.slug });
@@ -13945,7 +14519,7 @@ async function getStandaloneRepositoryCandidates(assignmentsDir2, excludeAssignm
13945
14519
  return out;
13946
14520
  }
13947
14521
  async function getProjectSourceAssignments(projectsDir, projectSlug, excludeSlug) {
13948
- const assignmentsDir2 = resolve21(projectsDir, projectSlug, "assignments");
14522
+ const assignmentsDir2 = resolve22(projectsDir, projectSlug, "assignments");
13949
14523
  if (!await fileExists(assignmentsDir2)) return [];
13950
14524
  const seen = /* @__PURE__ */ new Set();
13951
14525
  const out = [];
@@ -13953,7 +14527,7 @@ async function getProjectSourceAssignments(projectsDir, projectSlug, excludeSlug
13953
14527
  for (const entry of entries) {
13954
14528
  if (!entry.isDirectory()) continue;
13955
14529
  if (entry.name === excludeSlug) continue;
13956
- const assignmentMd = resolve21(assignmentsDir2, entry.name, "assignment.md");
14530
+ const assignmentMd = resolve22(assignmentsDir2, entry.name, "assignment.md");
13957
14531
  if (!await fileExists(assignmentMd)) continue;
13958
14532
  const parsed = parseAssignmentFull(await readFile16(assignmentMd, "utf-8"));
13959
14533
  const source = toSourceAssignment(parsed, entry.name);
@@ -13972,7 +14546,7 @@ async function getStandaloneSourceAssignments(assignmentsDir2, excludeAssignment
13972
14546
  for (const entry of entries) {
13973
14547
  if (!entry.isDirectory()) continue;
13974
14548
  if (entry.name === excludeAssignmentId) continue;
13975
- const assignmentMd = resolve21(assignmentsDir2, entry.name, "assignment.md");
14549
+ const assignmentMd = resolve22(assignmentsDir2, entry.name, "assignment.md");
13976
14550
  if (!await fileExists(assignmentMd)) continue;
13977
14551
  const parsed = parseAssignmentFull(await readFile16(assignmentMd, "utf-8"));
13978
14552
  const source = toSourceAssignment(parsed, entry.name);
@@ -14667,7 +15241,7 @@ async function handleWorktreeCreate(req2, res, ctx) {
14667
15241
  });
14668
15242
  const branch = typeof bodyBranch === "string" && bodyBranch.trim() ? bodyBranch.trim() : defaults.branch;
14669
15243
  const parentBranch = typeof bodyParent === "string" && bodyParent.trim() ? bodyParent.trim() : defaults.parentBranch;
14670
- const worktreePath = resolve23(repo, ".worktrees", branch);
15244
+ const worktreePath = resolve24(repo, ".worktrees", branch);
14671
15245
  const refCheck = spawnSync3("git", ["-C", repo, "check-ref-format", "--branch", branch], {
14672
15246
  encoding: "utf-8"
14673
15247
  });
@@ -14962,9 +15536,9 @@ ${body.startsWith("\n") ? body.slice(1) : body}${body.endsWith("\n") ? "" : "\n"
14962
15536
  });
14963
15537
  return;
14964
15538
  }
14965
- const folderPath = resolve23(projectDir, folder);
15539
+ const folderPath = resolve24(projectDir, folder);
14966
15540
  await ensureDir(folderPath);
14967
- const filePath = resolve23(folderPath, `${requestedSlug}.md`);
15541
+ const filePath = resolve24(folderPath, `${requestedSlug}.md`);
14968
15542
  const timestamp = nowTimestamp();
14969
15543
  let content = renderItemStub(kind, {
14970
15544
  slug: requestedSlug,
@@ -15008,7 +15582,7 @@ ${body.startsWith("\n") ? body.slice(1) : body}${body.endsWith("\n") ? "" : "\n"
15008
15582
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
15009
15583
  return;
15010
15584
  }
15011
- const filePath = resolve23(projectDir, folder, `${itemSlug}.md`);
15585
+ const filePath = resolve24(projectDir, folder, `${itemSlug}.md`);
15012
15586
  if (!await fileExists(filePath)) {
15013
15587
  res.status(404).json({ error: `${kind === "memory" ? "Memory" : "Resource"} not found` });
15014
15588
  return;
@@ -15044,7 +15618,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
15044
15618
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
15045
15619
  return;
15046
15620
  }
15047
- const filePath = resolve23(projectDir, folder, `${itemSlug}.md`);
15621
+ const filePath = resolve24(projectDir, folder, `${itemSlug}.md`);
15048
15622
  if (!await fileExists(filePath)) {
15049
15623
  res.status(404).json({ error: `${kind === "memory" ? "Memory" : "Resource"} not found` });
15050
15624
  return;
@@ -15078,26 +15652,26 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
15078
15652
  res.status(400).json({ error: `Invalid slug "${slug}". Must be lowercase and hyphen-separated.` });
15079
15653
  return;
15080
15654
  }
15081
- const projectDir = resolve23(projectsDir, slug);
15655
+ const projectDir = resolve24(projectsDir, slug);
15082
15656
  if (await fileExists(projectDir)) {
15083
15657
  res.status(409).json({ error: `Project "${slug}" already exists` });
15084
15658
  return;
15085
15659
  }
15086
15660
  const title = fields.title;
15087
15661
  const timestamp = fields.created || nowTimestamp();
15088
- await ensureDir(resolve23(projectDir, "assignments"));
15089
- await ensureDir(resolve23(projectDir, "resources"));
15090
- await ensureDir(resolve23(projectDir, "memories"));
15091
- await writeFileForce(resolve23(projectDir, "project.md"), content);
15662
+ await ensureDir(resolve24(projectDir, "assignments"));
15663
+ await ensureDir(resolve24(projectDir, "resources"));
15664
+ await ensureDir(resolve24(projectDir, "memories"));
15665
+ await writeFileForce(resolve24(projectDir, "project.md"), content);
15092
15666
  try {
15093
15667
  const companions = [
15094
- [resolve23(projectDir, "manifest.md"), renderManifest({ slug, timestamp })],
15095
- [resolve23(projectDir, "_index-assignments.md"), renderIndexAssignments({ slug, title, timestamp })],
15096
- [resolve23(projectDir, "_index-plans.md"), renderIndexPlans({ slug, title, timestamp })],
15097
- [resolve23(projectDir, "_index-decisions.md"), renderIndexDecisions({ slug, title, timestamp })],
15098
- [resolve23(projectDir, "_status.md"), renderStatus({ slug, title, timestamp })],
15099
- [resolve23(projectDir, "resources", "_index.md"), renderResourcesIndex({ slug, title, timestamp })],
15100
- [resolve23(projectDir, "memories", "_index.md"), renderMemoriesIndex({ slug, title, timestamp })]
15668
+ [resolve24(projectDir, "manifest.md"), renderManifest({ slug, timestamp })],
15669
+ [resolve24(projectDir, "_index-assignments.md"), renderIndexAssignments({ slug, title, timestamp })],
15670
+ [resolve24(projectDir, "_index-plans.md"), renderIndexPlans({ slug, title, timestamp })],
15671
+ [resolve24(projectDir, "_index-decisions.md"), renderIndexDecisions({ slug, title, timestamp })],
15672
+ [resolve24(projectDir, "_status.md"), renderStatus({ slug, title, timestamp })],
15673
+ [resolve24(projectDir, "resources", "_index.md"), renderResourcesIndex({ slug, title, timestamp })],
15674
+ [resolve24(projectDir, "memories", "_index.md"), renderMemoriesIndex({ slug, title, timestamp })]
15101
15675
  ];
15102
15676
  for (const [filePath, fileContent] of companions) {
15103
15677
  await writeFileForce(filePath, fileContent);
@@ -15118,8 +15692,8 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
15118
15692
  router.post("/api/projects/:slug/assignments", async (req2, res) => {
15119
15693
  try {
15120
15694
  const projectSlug = getParam(req2.params.slug);
15121
- const projectDir = resolve23(projectsDir, projectSlug);
15122
- const projectMdPath = resolve23(projectDir, "project.md");
15695
+ const projectDir = resolve24(projectsDir, projectSlug);
15696
+ const projectMdPath = resolve24(projectDir, "project.md");
15123
15697
  if (!await fileExists(projectMdPath)) {
15124
15698
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
15125
15699
  return;
@@ -15149,7 +15723,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
15149
15723
  res.status(400).json({ error: `Invalid priority "${priority}". Must be low, medium, high, or critical.` });
15150
15724
  return;
15151
15725
  }
15152
- const assignmentDir = resolve23(projectDir, "assignments", assignmentSlug);
15726
+ const assignmentDir = resolve24(projectDir, "assignments", assignmentSlug);
15153
15727
  if (await fileExists(assignmentDir)) {
15154
15728
  res.status(409).json({
15155
15729
  error: `Assignment "${assignmentSlug}" already exists in project "${projectSlug}"`
@@ -15167,12 +15741,12 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
15167
15741
  command: "create",
15168
15742
  by: null
15169
15743
  }) : content;
15170
- await writeFileForce(resolve23(assignmentDir, "assignment.md"), seededContent);
15744
+ await writeFileForce(resolve24(assignmentDir, "assignment.md"), seededContent);
15171
15745
  try {
15172
15746
  const companions = [
15173
- [resolve23(assignmentDir, "scratchpad.md"), renderScratchpad({ assignmentSlug, timestamp })],
15174
- [resolve23(assignmentDir, "handoff.md"), renderHandoff({ assignmentSlug, timestamp })],
15175
- [resolve23(assignmentDir, "decision-record.md"), renderDecisionRecord({ assignmentSlug, timestamp })]
15747
+ [resolve24(assignmentDir, "scratchpad.md"), renderScratchpad({ assignmentSlug, timestamp })],
15748
+ [resolve24(assignmentDir, "handoff.md"), renderHandoff({ assignmentSlug, timestamp })],
15749
+ [resolve24(assignmentDir, "decision-record.md"), renderDecisionRecord({ assignmentSlug, timestamp })]
15176
15750
  ];
15177
15751
  for (const [filePath, fileContent] of companions) {
15178
15752
  await writeFileForce(filePath, fileContent);
@@ -15200,7 +15774,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
15200
15774
  router.patch("/api/projects/:slug", async (req2, res) => {
15201
15775
  try {
15202
15776
  const projectSlug = getParam(req2.params.slug);
15203
- const projectPath = resolve23(projectsDir, projectSlug, "project.md");
15777
+ const projectPath = resolve24(projectsDir, projectSlug, "project.md");
15204
15778
  const currentContent = await readCurrentDocument(projectPath);
15205
15779
  if (!currentContent) {
15206
15780
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
@@ -15233,7 +15807,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
15233
15807
  try {
15234
15808
  const projectSlug = getParam(req2.params.slug);
15235
15809
  const assignmentSlug = getParam(req2.params.aslug);
15236
- const assignmentPath = resolve23(
15810
+ const assignmentPath = resolve24(
15237
15811
  projectsDir,
15238
15812
  projectSlug,
15239
15813
  "assignments",
@@ -15299,7 +15873,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
15299
15873
  try {
15300
15874
  const projectSlug = getParam(req2.params.slug);
15301
15875
  const assignmentSlug = getParam(req2.params.aslug);
15302
- const assignmentPath = resolve23(
15876
+ const assignmentPath = resolve24(
15303
15877
  projectsDir,
15304
15878
  projectSlug,
15305
15879
  "assignments",
@@ -15335,7 +15909,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
15335
15909
  try {
15336
15910
  const projectSlug = getParam(req2.params.slug);
15337
15911
  const assignmentSlug = getParam(req2.params.aslug);
15338
- const planPath = resolve23(
15912
+ const planPath = resolve24(
15339
15913
  projectsDir,
15340
15914
  projectSlug,
15341
15915
  "assignments",
@@ -15373,7 +15947,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
15373
15947
  try {
15374
15948
  const projectSlug = getParam(req2.params.slug);
15375
15949
  const assignmentSlug = getParam(req2.params.aslug);
15376
- const scratchpadPath = resolve23(
15950
+ const scratchpadPath = resolve24(
15377
15951
  projectsDir,
15378
15952
  projectSlug,
15379
15953
  "assignments",
@@ -15411,7 +15985,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
15411
15985
  try {
15412
15986
  const projectSlug = getParam(req2.params.slug);
15413
15987
  const assignmentSlug = getParam(req2.params.aslug);
15414
- const handoffPath = resolve23(
15988
+ const handoffPath = resolve24(
15415
15989
  projectsDir,
15416
15990
  projectSlug,
15417
15991
  "assignments",
@@ -15449,7 +16023,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
15449
16023
  try {
15450
16024
  const projectSlug = getParam(req2.params.slug);
15451
16025
  const assignmentSlug = getParam(req2.params.aslug);
15452
- const decisionPath = resolve23(
16026
+ const decisionPath = resolve24(
15453
16027
  projectsDir,
15454
16028
  projectSlug,
15455
16029
  "assignments",
@@ -15487,7 +16061,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
15487
16061
  try {
15488
16062
  const projectSlug = getParam(req2.params.slug);
15489
16063
  const assignmentSlug = getParam(req2.params.aslug);
15490
- const commentsPath = resolve23(
16064
+ const commentsPath = resolve24(
15491
16065
  projectsDir,
15492
16066
  projectSlug,
15493
16067
  "assignments",
@@ -15543,7 +16117,7 @@ ${entry}`;
15543
16117
  }
15544
16118
  await writeFileForce(commentsPath, next);
15545
16119
  try {
15546
- const assignmentMdPath = resolve23(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md");
16120
+ const assignmentMdPath = resolve24(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md");
15547
16121
  if (await fileExists(assignmentMdPath)) {
15548
16122
  const fm = parseAssignmentFull(await readFile18(assignmentMdPath, "utf-8"));
15549
16123
  emitDashboardEvent(fm.id, projectSlug, "comment-added", {
@@ -15568,7 +16142,7 @@ ${entry}`;
15568
16142
  const projectSlug = getParam(req2.params.slug);
15569
16143
  const assignmentSlug = getParam(req2.params.aslug);
15570
16144
  const commentId = getParam(req2.params.commentId);
15571
- const commentsPath = resolve23(
16145
+ const commentsPath = resolve24(
15572
16146
  projectsDir,
15573
16147
  projectSlug,
15574
16148
  "assignments",
@@ -15611,7 +16185,7 @@ ${entry}`;
15611
16185
  await writeFileForce(commentsPath, withUpdated);
15612
16186
  if (target.resolved !== true && resolved === true) {
15613
16187
  try {
15614
- const assignmentMdPath = resolve23(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md");
16188
+ const assignmentMdPath = resolve24(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md");
15615
16189
  if (await fileExists(assignmentMdPath)) {
15616
16190
  const fm = parseAssignmentFull(await readFile18(assignmentMdPath, "utf-8"));
15617
16191
  emitDashboardEvent(fm.id, projectSlug, "comment-resolved", { commentId });
@@ -15629,7 +16203,7 @@ ${entry}`;
15629
16203
  router.post("/api/projects/:slug/move-workspace", async (req2, res) => {
15630
16204
  try {
15631
16205
  const projectSlug = getParam(req2.params.slug);
15632
- const projectPath = resolve23(projectsDir, projectSlug, "project.md");
16206
+ const projectPath = resolve24(projectsDir, projectSlug, "project.md");
15633
16207
  if (!await fileExists(projectPath)) {
15634
16208
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
15635
16209
  return;
@@ -15677,7 +16251,7 @@ ${entry}`;
15677
16251
  });
15678
16252
  return;
15679
16253
  }
15680
- const assignmentPath = resolve23(resolved.assignmentDir, "assignment.md");
16254
+ const assignmentPath = resolve24(resolved.assignmentDir, "assignment.md");
15681
16255
  let content = await readFile18(assignmentPath, "utf-8");
15682
16256
  content = setTopLevelField(content, "workspaceGroup", workspaceGroup ?? null);
15683
16257
  content = setTopLevelField(content, "updated", nowTimestamp());
@@ -15694,7 +16268,7 @@ ${entry}`;
15694
16268
  async (req2, res) => {
15695
16269
  try {
15696
16270
  const projectSlug = getParam(req2.params.slug);
15697
- const projectPath = resolve23(projectsDir, projectSlug, "project.md");
16271
+ const projectPath = resolve24(projectsDir, projectSlug, "project.md");
15698
16272
  if (!await fileExists(projectPath)) {
15699
16273
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
15700
16274
  return;
@@ -15753,7 +16327,7 @@ ${entry}`;
15753
16327
  try {
15754
16328
  const projectSlug = getParam(req2.params.slug);
15755
16329
  const assignmentSlug = getParam(req2.params.aslug);
15756
- const assignmentPath = resolve23(
16330
+ const assignmentPath = resolve24(
15757
16331
  projectsDir,
15758
16332
  projectSlug,
15759
16333
  "assignments",
@@ -15802,7 +16376,7 @@ ${entry}`;
15802
16376
  try {
15803
16377
  const projectSlug = getParam(req2.params.slug);
15804
16378
  const assignmentSlug = getParam(req2.params.aslug);
15805
- const assignmentPath = resolve23(
16379
+ const assignmentPath = resolve24(
15806
16380
  projectsDir,
15807
16381
  projectSlug,
15808
16382
  "assignments",
@@ -15861,7 +16435,7 @@ ${entry}`;
15861
16435
  try {
15862
16436
  const projectSlug = getParam(req2.params.slug);
15863
16437
  const assignmentSlug = getParam(req2.params.aslug);
15864
- const assignmentPath = resolve23(
16438
+ const assignmentPath = resolve24(
15865
16439
  projectsDir,
15866
16440
  projectSlug,
15867
16441
  "assignments",
@@ -15894,7 +16468,7 @@ ${entry}`;
15894
16468
  res.status(404).json({ error: `Assignment "${id}" not found` });
15895
16469
  return;
15896
16470
  }
15897
- const assignmentPath = resolve23(resolved.assignmentDir, "assignment.md");
16471
+ const assignmentPath = resolve24(resolved.assignmentDir, "assignment.md");
15898
16472
  const parsedForSlug = parseAssignmentFull(await readFile18(assignmentPath, "utf-8"));
15899
16473
  const assignmentSlugForBranch = parsedForSlug.slug || resolved.id;
15900
16474
  await handleWorktreeCreate(req2, res, {
@@ -15951,7 +16525,7 @@ ${entry}`;
15951
16525
  router.post("/api/projects/:slug/status-override", async (req2, res) => {
15952
16526
  try {
15953
16527
  const projectSlug = getParam(req2.params.slug);
15954
- const projectPath = resolve23(projectsDir, projectSlug, "project.md");
16528
+ const projectPath = resolve24(projectsDir, projectSlug, "project.md");
15955
16529
  if (!await fileExists(projectPath)) {
15956
16530
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
15957
16531
  return;
@@ -15978,7 +16552,7 @@ ${entry}`;
15978
16552
  try {
15979
16553
  const projectSlug = getParam(req2.params.slug);
15980
16554
  const assignmentSlug = getParam(req2.params.aslug);
15981
- const assignmentPath = resolve23(
16555
+ const assignmentPath = resolve24(
15982
16556
  projectsDir,
15983
16557
  projectSlug,
15984
16558
  "assignments",
@@ -16009,7 +16583,7 @@ ${entry}`;
16009
16583
  const result = await recomputeAndWrite2(assignmentPath, {
16010
16584
  cause: clearing ? "unpin" : "pin",
16011
16585
  by: "human",
16012
- projectDir: resolve23(projectsDir, projectSlug),
16586
+ projectDir: resolve24(projectsDir, projectSlug),
16013
16587
  context,
16014
16588
  mutate: (content) => {
16015
16589
  if (clearing) return updateOverride2(content, null);
@@ -16040,7 +16614,7 @@ ${entry}`;
16040
16614
  router.post("/api/projects/:slug/archive", async (req2, res) => {
16041
16615
  try {
16042
16616
  const projectSlug = getParam(req2.params.slug);
16043
- const projectPath = resolve23(projectsDir, projectSlug, "project.md");
16617
+ const projectPath = resolve24(projectsDir, projectSlug, "project.md");
16044
16618
  if (!await fileExists(projectPath)) {
16045
16619
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
16046
16620
  return;
@@ -16057,7 +16631,7 @@ ${entry}`;
16057
16631
  router.post("/api/projects/:slug/unarchive", async (req2, res) => {
16058
16632
  try {
16059
16633
  const projectSlug = getParam(req2.params.slug);
16060
- const projectPath = resolve23(projectsDir, projectSlug, "project.md");
16634
+ const projectPath = resolve24(projectsDir, projectSlug, "project.md");
16061
16635
  if (!await fileExists(projectPath)) {
16062
16636
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
16063
16637
  return;
@@ -16074,7 +16648,7 @@ ${entry}`;
16074
16648
  async function handleAssignmentArchive(req2, res, archived) {
16075
16649
  const projectSlug = getParam(req2.params.slug);
16076
16650
  const assignmentSlug = getParam(req2.params.aslug);
16077
- const assignmentPath = resolve23(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md");
16651
+ const assignmentPath = resolve24(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md");
16078
16652
  if (!await fileExists(assignmentPath)) {
16079
16653
  res.status(404).json({ error: "Assignment not found" });
16080
16654
  return;
@@ -16114,7 +16688,7 @@ ${entry}`;
16114
16688
  res.status(404).json({ error: `Assignment "${id}" not found` });
16115
16689
  return;
16116
16690
  }
16117
- const assignmentPath = resolve23(resolved.assignmentDir, "assignment.md");
16691
+ const assignmentPath = resolve24(resolved.assignmentDir, "assignment.md");
16118
16692
  const content = await readFile18(assignmentPath, "utf-8");
16119
16693
  const reason = archived ? archiveReason(req2.body) : null;
16120
16694
  await writeFileForce(assignmentPath, applyArchiveFields(content, archived, reason));
@@ -16143,7 +16717,7 @@ ${entry}`;
16143
16717
  try {
16144
16718
  const projectSlug = getParam(req2.params.slug);
16145
16719
  const assignmentSlug = getParam(req2.params.aslug);
16146
- const assignmentPath = resolve23(
16720
+ const assignmentPath = resolve24(
16147
16721
  projectsDir,
16148
16722
  projectSlug,
16149
16723
  "assignments",
@@ -16181,7 +16755,7 @@ ${entry}`;
16181
16755
  try {
16182
16756
  const projectSlug = getParam(req2.params.slug);
16183
16757
  const assignmentSlug = getParam(req2.params.aslug);
16184
- const assignmentPath = resolve23(
16758
+ const assignmentPath = resolve24(
16185
16759
  projectsDir,
16186
16760
  projectSlug,
16187
16761
  "assignments",
@@ -16219,8 +16793,8 @@ ${entry}`;
16219
16793
  res.status(400).json({ error: `Unsupported transition command "${req2.params.command}"` });
16220
16794
  return;
16221
16795
  }
16222
- const projectDir = resolve23(projectsDir, projectSlug);
16223
- const assignmentPath = resolve23(projectDir, "assignments", assignmentSlug, "assignment.md");
16796
+ const projectDir = resolve24(projectsDir, projectSlug);
16797
+ const assignmentPath = resolve24(projectDir, "assignments", assignmentSlug, "assignment.md");
16224
16798
  if (!await fileExists(assignmentPath)) {
16225
16799
  res.status(404).json({ error: "Assignment not found" });
16226
16800
  return;
@@ -16301,8 +16875,8 @@ ${entry}`;
16301
16875
  try {
16302
16876
  const projectSlug = getParam(req2.params.slug);
16303
16877
  const assignmentSlug = getParam(req2.params.aslug);
16304
- const assignmentDir = resolve23(projectsDir, projectSlug, "assignments", assignmentSlug);
16305
- const assignmentPath = resolve23(assignmentDir, "assignment.md");
16878
+ const assignmentDir = resolve24(projectsDir, projectSlug, "assignments", assignmentSlug);
16879
+ const assignmentPath = resolve24(assignmentDir, "assignment.md");
16306
16880
  if (!await fileExists(assignmentPath)) {
16307
16881
  res.status(404).json({ error: `Assignment "${assignmentSlug}" not found in project "${projectSlug}"` });
16308
16882
  return;
@@ -16357,7 +16931,7 @@ ${entry}`;
16357
16931
  return;
16358
16932
  }
16359
16933
  const id2 = generateId();
16360
- const assignmentDir2 = resolve23(assignmentsDir2, id2);
16934
+ const assignmentDir2 = resolve24(assignmentsDir2, id2);
16361
16935
  if (await fileExists(assignmentDir2)) {
16362
16936
  res.status(500).json({ error: "UUID collision \u2014 try again" });
16363
16937
  return;
@@ -16376,25 +16950,25 @@ ${entry}`;
16376
16950
  by: null
16377
16951
  });
16378
16952
  }
16379
- await writeFileForce(resolve23(assignmentDir2, "assignment.md"), normalizedContent);
16953
+ await writeFileForce(resolve24(assignmentDir2, "assignment.md"), normalizedContent);
16380
16954
  await writeFileForce(
16381
- resolve23(assignmentDir2, "scratchpad.md"),
16955
+ resolve24(assignmentDir2, "scratchpad.md"),
16382
16956
  renderScratchpad({ assignmentSlug: id2, timestamp: timestamp2 })
16383
16957
  );
16384
16958
  await writeFileForce(
16385
- resolve23(assignmentDir2, "handoff.md"),
16959
+ resolve24(assignmentDir2, "handoff.md"),
16386
16960
  renderHandoff({ assignmentSlug: id2, timestamp: timestamp2 })
16387
16961
  );
16388
16962
  await writeFileForce(
16389
- resolve23(assignmentDir2, "decision-record.md"),
16963
+ resolve24(assignmentDir2, "decision-record.md"),
16390
16964
  renderDecisionRecord({ assignmentSlug: id2, timestamp: timestamp2 })
16391
16965
  );
16392
16966
  await writeFileForce(
16393
- resolve23(assignmentDir2, "progress.md"),
16967
+ resolve24(assignmentDir2, "progress.md"),
16394
16968
  renderProgress({ assignment: id2, timestamp: timestamp2 })
16395
16969
  );
16396
16970
  await writeFileForce(
16397
- resolve23(assignmentDir2, "comments.md"),
16971
+ resolve24(assignmentDir2, "comments.md"),
16398
16972
  renderComments({ assignment: id2, timestamp: timestamp2 })
16399
16973
  );
16400
16974
  if (seededHere) {
@@ -16419,7 +16993,7 @@ ${entry}`;
16419
16993
  return;
16420
16994
  }
16421
16995
  const id = generateId();
16422
- const assignmentDir = resolve23(assignmentsDir2, id);
16996
+ const assignmentDir = resolve24(assignmentsDir2, id);
16423
16997
  if (await fileExists(assignmentDir)) {
16424
16998
  res.status(500).json({ error: "UUID collision \u2014 try again" });
16425
16999
  return;
@@ -16439,25 +17013,25 @@ ${entry}`;
16439
17013
  project: null,
16440
17014
  type: typeof type === "string" ? type : void 0
16441
17015
  });
16442
- await writeFileForce(resolve23(assignmentDir, "assignment.md"), assignmentContent);
17016
+ await writeFileForce(resolve24(assignmentDir, "assignment.md"), assignmentContent);
16443
17017
  await writeFileForce(
16444
- resolve23(assignmentDir, "scratchpad.md"),
17018
+ resolve24(assignmentDir, "scratchpad.md"),
16445
17019
  renderScratchpad({ assignmentSlug: id, timestamp })
16446
17020
  );
16447
17021
  await writeFileForce(
16448
- resolve23(assignmentDir, "handoff.md"),
17022
+ resolve24(assignmentDir, "handoff.md"),
16449
17023
  renderHandoff({ assignmentSlug: id, timestamp })
16450
17024
  );
16451
17025
  await writeFileForce(
16452
- resolve23(assignmentDir, "decision-record.md"),
17026
+ resolve24(assignmentDir, "decision-record.md"),
16453
17027
  renderDecisionRecord({ assignmentSlug: id, timestamp })
16454
17028
  );
16455
17029
  await writeFileForce(
16456
- resolve23(assignmentDir, "progress.md"),
17030
+ resolve24(assignmentDir, "progress.md"),
16457
17031
  renderProgress({ assignment: id, timestamp })
16458
17032
  );
16459
17033
  await writeFileForce(
16460
- resolve23(assignmentDir, "comments.md"),
17034
+ resolve24(assignmentDir, "comments.md"),
16461
17035
  renderComments({ assignment: id, timestamp })
16462
17036
  );
16463
17037
  emitDashboardEvent(id, null, "status-change", {
@@ -16590,7 +17164,7 @@ ${entry}`;
16590
17164
  res.status(404).json({ error: `Assignment "${id}" not found` });
16591
17165
  return;
16592
17166
  }
16593
- const assignmentPath = resolve23(resolved.assignmentDir, "assignment.md");
17167
+ const assignmentPath = resolve24(resolved.assignmentDir, "assignment.md");
16594
17168
  const currentContent = await readCurrentDocument(assignmentPath);
16595
17169
  if (!currentContent) {
16596
17170
  res.status(404).json({ error: "Assignment not found" });
@@ -16655,7 +17229,7 @@ ${entry}`;
16655
17229
  res.status(404).json({ error: `Assignment "${id}" not found` });
16656
17230
  return;
16657
17231
  }
16658
- const planPath = resolve23(resolved.assignmentDir, "plan.md");
17232
+ const planPath = resolve24(resolved.assignmentDir, "plan.md");
16659
17233
  const currentContent = await readCurrentDocument(planPath);
16660
17234
  if (!currentContent) {
16661
17235
  res.status(404).json({ error: "Plan not found" });
@@ -16689,7 +17263,7 @@ ${entry}`;
16689
17263
  res.status(404).json({ error: `Assignment "${id}" not found` });
16690
17264
  return;
16691
17265
  }
16692
- const scratchpadPath = resolve23(resolved.assignmentDir, "scratchpad.md");
17266
+ const scratchpadPath = resolve24(resolved.assignmentDir, "scratchpad.md");
16693
17267
  const currentContent = await readCurrentDocument(scratchpadPath);
16694
17268
  if (!currentContent) {
16695
17269
  res.status(404).json({ error: "Scratchpad not found" });
@@ -16723,7 +17297,7 @@ ${entry}`;
16723
17297
  res.status(404).json({ error: `Assignment "${id}" not found` });
16724
17298
  return;
16725
17299
  }
16726
- const handoffPath = resolve23(resolved.assignmentDir, "handoff.md");
17300
+ const handoffPath = resolve24(resolved.assignmentDir, "handoff.md");
16727
17301
  const currentContent = await readCurrentDocument(handoffPath);
16728
17302
  if (!currentContent) {
16729
17303
  res.status(404).json({ error: "Handoff log not found" });
@@ -16763,7 +17337,7 @@ ${entry}`;
16763
17337
  res.status(404).json({ error: `Assignment "${id}" not found` });
16764
17338
  return;
16765
17339
  }
16766
- const decisionPath = resolve23(resolved.assignmentDir, "decision-record.md");
17340
+ const decisionPath = resolve24(resolved.assignmentDir, "decision-record.md");
16767
17341
  const currentContent = await readCurrentDocument(decisionPath);
16768
17342
  if (!currentContent) {
16769
17343
  res.status(404).json({ error: "Decision record not found" });
@@ -16803,7 +17377,7 @@ ${entry}`;
16803
17377
  res.status(404).json({ error: `Assignment "${id}" not found` });
16804
17378
  return;
16805
17379
  }
16806
- const assignmentPath = resolve23(resolved.assignmentDir, "assignment.md");
17380
+ const assignmentPath = resolve24(resolved.assignmentDir, "assignment.md");
16807
17381
  if (!await fileExists(assignmentPath)) {
16808
17382
  res.status(404).json({ error: "Assignment not found" });
16809
17383
  return;
@@ -16825,7 +17399,7 @@ ${entry}`;
16825
17399
  const { recomputeAndWrite: recomputeAndWrite2, resolveDeriveContext: resolveDeriveContext2 } = await Promise.resolve().then(() => (init_recompute(), recompute_exports));
16826
17400
  const { updateOverride: updateOverride2 } = await Promise.resolve().then(() => (init_frontmatter(), frontmatter_exports));
16827
17401
  const context = await resolveDeriveContext2();
16828
- const projectDirForId = resolved.standalone ? null : resolve23(resolved.assignmentDir, "..", "..");
17402
+ const projectDirForId = resolved.standalone ? null : resolve24(resolved.assignmentDir, "..", "..");
16829
17403
  const result = await recomputeAndWrite2(assignmentPath, {
16830
17404
  cause: clearing ? "unpin" : "pin",
16831
17405
  by: "human",
@@ -16865,7 +17439,7 @@ ${entry}`;
16865
17439
  res.status(404).json({ error: `Assignment "${id}" not found` });
16866
17440
  return;
16867
17441
  }
16868
- const assignmentPath = resolve23(resolved.assignmentDir, "assignment.md");
17442
+ const assignmentPath = resolve24(resolved.assignmentDir, "assignment.md");
16869
17443
  if (!await fileExists(assignmentPath)) {
16870
17444
  res.status(404).json({ error: "Assignment not found" });
16871
17445
  return;
@@ -16905,7 +17479,7 @@ ${entry}`;
16905
17479
  res.status(404).json({ error: `Assignment "${id}" not found` });
16906
17480
  return;
16907
17481
  }
16908
- const assignmentPath = resolve23(resolved.assignmentDir, "assignment.md");
17482
+ const assignmentPath = resolve24(resolved.assignmentDir, "assignment.md");
16909
17483
  if (!await fileExists(assignmentPath)) {
16910
17484
  res.status(404).json({ error: "Assignment not found" });
16911
17485
  return;
@@ -16938,7 +17512,7 @@ ${entry}`;
16938
17512
  res.status(404).json({ error: `Assignment "${id}" not found` });
16939
17513
  return;
16940
17514
  }
16941
- const assignmentPath = resolve23(resolved.assignmentDir, "assignment.md");
17515
+ const assignmentPath = resolve24(resolved.assignmentDir, "assignment.md");
16942
17516
  const currentContent = await readCurrentDocument(assignmentPath);
16943
17517
  if (!currentContent) {
16944
17518
  res.status(404).json({ error: "Assignment not found" });
@@ -16986,8 +17560,8 @@ ${entry}`;
16986
17560
  }
16987
17561
  const { recomputeAndWrite: recomputeAndWrite2, recomputeDependents: recomputeDependents2, resolveDeriveContext: resolveDeriveContext2 } = await Promise.resolve().then(() => (init_recompute(), recompute_exports));
16988
17562
  const context = await resolveDeriveContext2();
16989
- const byIdPath = resolve23(resolved.assignmentDir, "assignment.md");
16990
- const byIdProjectDir = resolved.standalone ? null : resolve23(resolved.assignmentDir, "..", "..");
17563
+ const byIdPath = resolve24(resolved.assignmentDir, "assignment.md");
17564
+ const byIdProjectDir = resolved.standalone ? null : resolve24(resolved.assignmentDir, "..", "..");
16991
17565
  if (command === "block" || command === "unblock") {
16992
17566
  const { updateAssignmentFile: updateAssignmentFile2 } = await Promise.resolve().then(() => (init_frontmatter(), frontmatter_exports));
16993
17567
  const result = await recomputeAndWrite2(byIdPath, {
@@ -17108,7 +17682,7 @@ function validateTitleBody(body) {
17108
17682
  return { ok: true, value: trimmed };
17109
17683
  }
17110
17684
  async function appendCommentTo(assignmentDir, assignmentRef, req2, res, reloadDetail) {
17111
- const commentsPath = resolve23(assignmentDir, "comments.md");
17685
+ const commentsPath = resolve24(assignmentDir, "comments.md");
17112
17686
  const { body, author, type, replyTo } = req2.body || {};
17113
17687
  if (!body || typeof body !== "string" || !body.trim()) {
17114
17688
  res.status(400).json({ error: "body is required" });
@@ -17155,7 +17729,7 @@ ${entry}`;
17155
17729
  }
17156
17730
  await writeFileForce(commentsPath, next);
17157
17731
  try {
17158
- const assignmentMdPath = resolve23(assignmentDir, "assignment.md");
17732
+ const assignmentMdPath = resolve24(assignmentDir, "assignment.md");
17159
17733
  if (await fileExists(assignmentMdPath)) {
17160
17734
  const fm = parseAssignmentFull(await readFile18(assignmentMdPath, "utf-8"));
17161
17735
  emitDashboardEvent(fm.id, fm.project, "comment-added", {
@@ -17172,7 +17746,7 @@ ${entry}`;
17172
17746
  res.status(201).json({ assignment, comment: { id: comment.id } });
17173
17747
  }
17174
17748
  async function toggleCommentResolvedAt(assignmentDir, commentId, req2, res, reloadDetail) {
17175
- const commentsPath = resolve23(assignmentDir, "comments.md");
17749
+ const commentsPath = resolve24(assignmentDir, "comments.md");
17176
17750
  if (!await fileExists(commentsPath)) {
17177
17751
  res.status(404).json({ error: "Comments file not found" });
17178
17752
  return;
@@ -17206,7 +17780,7 @@ async function toggleCommentResolvedAt(assignmentDir, commentId, req2, res, relo
17206
17780
  await writeFileForce(commentsPath, withUpdated);
17207
17781
  if (target.resolved !== true && desired === true) {
17208
17782
  try {
17209
- const assignmentMdPath = resolve23(assignmentDir, "assignment.md");
17783
+ const assignmentMdPath = resolve24(assignmentDir, "assignment.md");
17210
17784
  if (await fileExists(assignmentMdPath)) {
17211
17785
  const fm = parseAssignmentFull(await readFile18(assignmentMdPath, "utf-8"));
17212
17786
  emitDashboardEvent(fm.id, fm.project, "comment-resolved", { commentId });
@@ -17338,9 +17912,12 @@ function createServersRouter(serversDir2, projectsDir, assignmentsDir2) {
17338
17912
  // src/dashboard/api-agent-sessions.ts
17339
17913
  init_agent_sessions();
17340
17914
  init_fs();
17915
+ init_session_id();
17916
+ init_assignment_resolver();
17917
+ init_paths();
17341
17918
  init_transcript();
17342
17919
  import { Router as Router4 } from "express";
17343
- import { resolve as resolve24 } from "path";
17920
+ import { resolve as resolve25 } from "path";
17344
17921
  init_config2();
17345
17922
  init_process_info();
17346
17923
  init_git_worktree();
@@ -17364,7 +17941,7 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
17364
17941
  try {
17365
17942
  const { projectSlug } = req2.params;
17366
17943
  const assignment = req2.query.assignment;
17367
- const projectDir = resolve24(projectsDir, projectSlug);
17944
+ const projectDir = resolve25(projectsDir, projectSlug);
17368
17945
  if (!await fileExists(projectDir)) {
17369
17946
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
17370
17947
  return;
@@ -17393,21 +17970,45 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
17393
17970
  });
17394
17971
  return;
17395
17972
  }
17973
+ if (!isSafeSessionId(sessionId)) {
17974
+ res.status(400).json({
17975
+ error: "sessionId is malformed. Pass the real agent-generated session id."
17976
+ });
17977
+ return;
17978
+ }
17396
17979
  if (projectSlug) {
17397
- const projectDir = resolve24(projectsDir, projectSlug);
17980
+ const projectDir = resolve25(projectsDir, projectSlug);
17398
17981
  if (!await fileExists(projectDir)) {
17399
17982
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
17400
17983
  return;
17401
17984
  }
17402
17985
  }
17986
+ let assignmentId = null;
17987
+ if (assignmentSlug) {
17988
+ const resolvedAssignment = await resolveAssignmentBySlug(
17989
+ projectsDir,
17990
+ assignmentsDir2 ?? assignmentsDir(),
17991
+ projectSlug || null,
17992
+ assignmentSlug
17993
+ );
17994
+ if (!resolvedAssignment.exists) {
17995
+ res.status(404).json({ error: `Assignment "${assignmentSlug}" not found` });
17996
+ return;
17997
+ }
17998
+ assignmentId = resolvedAssignment.id;
17999
+ }
17403
18000
  const derivedPath = await derivePathFromTranscript(transcriptPath);
17404
18001
  const recordedPath = derivedPath ?? path ?? "";
17405
18002
  const pid = typeof rawPid === "number" && Number.isFinite(rawPid) && rawPid > 0 ? rawPid : null;
17406
18003
  const pidStartedAt = pid !== null ? captureProcessStartedAt(pid) : null;
17407
18004
  const originalHeadSha = isExistingDir(recordedPath) ? await captureHeadSha(recordedPath) : null;
17408
18005
  const session = {
17409
- projectSlug: projectSlug || null,
18006
+ // L: a POST with no assignmentSlug is registration-only (unbound) — do
18007
+ // NOT open a project-bound engagement for an arbitrary session. Binding
18008
+ // requires a validated assignment selector (existence-checked above).
18009
+ projectSlug: assignmentSlug ? projectSlug || null : null,
17410
18010
  assignmentSlug: assignmentSlug || null,
18011
+ assignmentId,
17411
18012
  agent,
17412
18013
  sessionId,
17413
18014
  started: (/* @__PURE__ */ new Date()).toISOString(),
@@ -17486,6 +18087,10 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
17486
18087
  broadcast?.({ type: "agent-sessions-updated", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
17487
18088
  res.json({ updated: true });
17488
18089
  } catch (error) {
18090
+ if (error instanceof SessionResurrectionError) {
18091
+ res.status(409).json({ error: error.message });
18092
+ return;
18093
+ }
17489
18094
  res.status(500).json({ error: error instanceof Error ? error.message : "Update failed" });
17490
18095
  }
17491
18096
  });
@@ -17810,8 +18415,8 @@ import { Router as Router6 } from "express";
17810
18415
  // src/utils/terminal-probe.ts
17811
18416
  import { spawnSync as spawnSync4 } from "child_process";
17812
18417
  import { existsSync as existsSync2 } from "fs";
17813
- import { homedir as homedir2 } from "os";
17814
- import { join as join3 } from "path";
18418
+ import { homedir as homedir4 } from "os";
18419
+ import { join as join5 } from "path";
17815
18420
  var APP_BUNDLE_IDS = {
17816
18421
  "terminal-app": "com.apple.Terminal",
17817
18422
  iterm: "com.googlecode.iterm2",
@@ -17828,7 +18433,7 @@ var APP_FIXED_PATHS = {
17828
18433
  "terminal-app": "/System/Applications/Utilities/Terminal.app"
17829
18434
  };
17830
18435
  function defaultApplicationsDirs() {
17831
- return ["/Applications", join3(homedir2(), "Applications")];
18436
+ return ["/Applications", join5(homedir4(), "Applications")];
17832
18437
  }
17833
18438
  function findAppBundle(terminal, dirs = defaultApplicationsDirs()) {
17834
18439
  const fixed = APP_FIXED_PATHS[terminal];
@@ -17836,7 +18441,7 @@ function findAppBundle(terminal, dirs = defaultApplicationsDirs()) {
17836
18441
  const bundleName = APP_BUNDLE_NAMES[terminal];
17837
18442
  if (bundleName) {
17838
18443
  for (const dir of dirs) {
17839
- const candidate = join3(dir, bundleName);
18444
+ const candidate = join5(dir, bundleName);
17840
18445
  if (existsSync2(candidate)) return candidate;
17841
18446
  }
17842
18447
  }
@@ -17866,11 +18471,11 @@ var defaultCmuxLsappinfoRunner = () => {
17866
18471
  function resolveCmuxCli(applicationsDirsOverride, cliDirsOverride, lsappinfoRunnerOverride) {
17867
18472
  const bundle = findAppBundle("cmux", applicationsDirsOverride);
17868
18473
  if (bundle) {
17869
- const cli = join3(bundle, "Contents/Resources/bin/cmux");
18474
+ const cli = join5(bundle, "Contents/Resources/bin/cmux");
17870
18475
  if (existsSync2(cli)) return cli;
17871
18476
  }
17872
18477
  for (const dir of cliDirsOverride ?? CMUX_CLI_DIRS) {
17873
- const cli = join3(dir, "cmux");
18478
+ const cli = join5(dir, "cmux");
17874
18479
  if (existsSync2(cli)) return cli;
17875
18480
  }
17876
18481
  if (process.platform === "darwin") {
@@ -17879,7 +18484,7 @@ function resolveCmuxCli(applicationsDirsOverride, cliDirsOverride, lsappinfoRunn
17879
18484
  if (stdout) {
17880
18485
  const match = stdout.match(/"LSBundlePath"="([^"]+)"/);
17881
18486
  if (match) {
17882
- const cli = join3(match[1], "Contents/Resources/bin/cmux");
18487
+ const cli = join5(match[1], "Contents/Resources/bin/cmux");
17883
18488
  if (existsSync2(cli)) return cli;
17884
18489
  }
17885
18490
  }
@@ -18018,7 +18623,7 @@ init_agents_schema();
18018
18623
  init_cwd();
18019
18624
  import { spawn as spawn2 } from "child_process";
18020
18625
  import { mkdir as mkdir2, writeFile as writeFile4 } from "fs/promises";
18021
- import { isAbsolute as isAbsolute5, resolve as resolve25 } from "path";
18626
+ import { isAbsolute as isAbsolute5, resolve as resolve26 } from "path";
18022
18627
  init_paths();
18023
18628
  init_playbooks();
18024
18629
  init_cwd();
@@ -18255,7 +18860,7 @@ async function resolveSessionPlan(input, terminal) {
18255
18860
  // src/launch/execute.ts
18256
18861
  import { spawn as spawn3 } from "child_process";
18257
18862
  import { homedir as homedir5 } from "os";
18258
- import { basename as basename5, join as join6, resolve as resolve26 } from "path";
18863
+ import { basename as basename5, join as join6, resolve as resolve27 } from "path";
18259
18864
  init_fs();
18260
18865
  init_config2();
18261
18866
  init_session_id();
@@ -18303,7 +18908,7 @@ async function executeLaunchPlan(plan, spawnFn = realSpawn) {
18303
18908
  `Spawn failed: ${msg}. Verify the terminal is installed and on PATH.`
18304
18909
  );
18305
18910
  }
18306
- await new Promise((resolve48, reject) => {
18911
+ await new Promise((resolve49, reject) => {
18307
18912
  let settled = false;
18308
18913
  let stderr = "";
18309
18914
  const finishOk = () => {
@@ -18313,7 +18918,7 @@ async function executeLaunchPlan(plan, spawnFn = realSpawn) {
18313
18918
  child.unref();
18314
18919
  } catch {
18315
18920
  }
18316
- resolve48();
18921
+ resolve49();
18317
18922
  };
18318
18923
  const finishErr = (remediation) => {
18319
18924
  if (settled) return;
@@ -18374,7 +18979,7 @@ async function registerLaunchAtBirth(plan, child) {
18374
18979
  markerDir
18375
18980
  );
18376
18981
  if (!sessionId) return;
18377
- if (autoTrack === "workspaces-only" && !await fileExists(resolve26(plan.cwd, ".syntaur", "context.json"))) {
18982
+ if (autoTrack === "workspaces-only" && !await fileExists(resolve27(plan.cwd, ".syntaur", "context.json"))) {
18378
18983
  return;
18379
18984
  }
18380
18985
  const { initSessionDb: initSessionDb2 } = await Promise.resolve().then(() => (init_session_db(), session_db_exports));
@@ -18550,8 +19155,8 @@ function appleScriptString(value) {
18550
19155
  init_paths();
18551
19156
  init_fs();
18552
19157
  import { fileURLToPath } from "url";
18553
- import { dirname as dirname6, resolve as resolve27, join as join7 } from "path";
18554
- import { realpathSync, readFileSync as readFileSync2, mkdirSync as mkdirSync2 } from "fs";
19158
+ import { dirname as dirname6, resolve as resolve28, join as join7 } from "path";
19159
+ import { realpathSync, readFileSync as readFileSync3, mkdirSync as mkdirSync2 } from "fs";
18555
19160
 
18556
19161
  // src/dashboard/api-launch-preflight.ts
18557
19162
  init_assignment_resolver();
@@ -19617,7 +20222,7 @@ import { Router as Router12 } from "express";
19617
20222
  init_paths();
19618
20223
  import Database3 from "better-sqlite3";
19619
20224
  import { randomUUID as randomUUID3 } from "crypto";
19620
- import { resolve as resolve28 } from "path";
20225
+ import { resolve as resolve29 } from "path";
19621
20226
  var db3 = null;
19622
20227
  var LEASE_SCHEMA_VERSION = "1";
19623
20228
  var SCHEMA_SQL3 = `
@@ -19704,7 +20309,7 @@ function isBusyError(err) {
19704
20309
  }
19705
20310
  function initLeasesDb(dbPath) {
19706
20311
  if (db3) return db3;
19707
- const finalPath = dbPath ?? resolve28(syntaurRoot(), "syntaur.db");
20312
+ const finalPath = dbPath ?? resolve29(syntaurRoot(), "syntaur.db");
19708
20313
  db3 = new Database3(finalPath);
19709
20314
  db3.pragma("journal_mode = WAL");
19710
20315
  db3.pragma("busy_timeout = 5000");
@@ -19857,14 +20462,14 @@ init_paths();
19857
20462
  init_timestamp();
19858
20463
  import { randomUUID as randomUUID4 } from "crypto";
19859
20464
  import { readFile as readFile20, readdir as readdir14, rm as rm3 } from "fs/promises";
19860
- import { resolve as resolve29 } from "path";
20465
+ import { resolve as resolve30 } from "path";
19861
20466
  function schedulesDir() {
19862
- const override = process.env.SYNTAUR_SCHEDULES_DIR;
19863
- if (override && override.length > 0) return resolve29(override);
19864
- return resolve29(syntaurRoot(), "schedules");
20467
+ const override3 = process.env.SYNTAUR_SCHEDULES_DIR;
20468
+ if (override3 && override3.length > 0) return resolve30(override3);
20469
+ return resolve30(syntaurRoot(), "schedules");
19865
20470
  }
19866
20471
  function jobPath(id) {
19867
- return resolve29(schedulesDir(), `${jobFileSafe(id)}.md`);
20472
+ return resolve30(schedulesDir(), `${jobFileSafe(id)}.md`);
19868
20473
  }
19869
20474
  function jobFileSafe(id) {
19870
20475
  if (!/^[A-Za-z0-9_-]+$/.test(id)) {
@@ -20007,7 +20612,7 @@ async function listJobs() {
20007
20612
  for (const name of names) {
20008
20613
  if (!name.endsWith(".md")) continue;
20009
20614
  try {
20010
- jobs.push(parseJobFile(await readFile20(resolve29(schedulesDir(), name), "utf-8")));
20615
+ jobs.push(parseJobFile(await readFile20(resolve30(schedulesDir(), name), "utf-8")));
20011
20616
  } catch {
20012
20617
  }
20013
20618
  }
@@ -20018,12 +20623,12 @@ async function listJobs() {
20018
20623
  // src/schedules/event-log.ts
20019
20624
  init_timestamp();
20020
20625
  import { appendFile, readFile as readFile21, mkdir as mkdir3 } from "fs/promises";
20021
- import { dirname as dirname8, resolve as resolve30 } from "path";
20626
+ import { dirname as dirname8, resolve as resolve31 } from "path";
20022
20627
  function eventLogPath(jobId) {
20023
20628
  if (!/^[A-Za-z0-9_-]+$/.test(jobId)) {
20024
20629
  throw new Error(`Unsafe schedule id: ${JSON.stringify(jobId)}`);
20025
20630
  }
20026
- return resolve30(schedulesDir(), `${jobId}.jsonl`);
20631
+ return resolve31(schedulesDir(), `${jobId}.jsonl`);
20027
20632
  }
20028
20633
  async function appendEvent(jobId, type, data) {
20029
20634
  const event = { type, at: nowTimestamp(), ...data ? { data } : {} };
@@ -20056,7 +20661,7 @@ async function readEvents(jobId) {
20056
20661
  // src/schedules/attempt.ts
20057
20662
  import { createHash as createHash3 } from "crypto";
20058
20663
  import { open as open4, readFile as readFile22, stat as stat5, unlink as unlink6 } from "fs/promises";
20059
- import { resolve as resolve31 } from "path";
20664
+ import { resolve as resolve32 } from "path";
20060
20665
 
20061
20666
  // src/schedules/triggers.ts
20062
20667
  import { Cron } from "croner";
@@ -20235,7 +20840,7 @@ var LOCK_WAIT_MS2 = 50;
20235
20840
  var LOCK_MAX_WAITS2 = 100;
20236
20841
  var MAX_CONSUMED_EDGES = 50;
20237
20842
  async function acquireJobLock(id) {
20238
- const lockPath = resolve31(schedulesDir(), `${id}.lock`);
20843
+ const lockPath = resolve32(schedulesDir(), `${id}.lock`);
20239
20844
  const token = `${process.pid}:${createHash3("sha256").update(`${Math.random()}${Date.now()}`).digest("hex").slice(0, 12)}`;
20240
20845
  for (let attempt = 0; attempt <= LOCK_MAX_WAITS2; attempt++) {
20241
20846
  try {
@@ -20550,7 +21155,7 @@ async function reapStale(jobs, deps) {
20550
21155
 
20551
21156
  // src/schedules/unattended.ts
20552
21157
  import { existsSync as existsSync3 } from "fs";
20553
- import { resolve as resolve32 } from "path";
21158
+ import { resolve as resolve33 } from "path";
20554
21159
  var UnattendedRefusalError = class extends Error {
20555
21160
  constructor(message) {
20556
21161
  super(message);
@@ -20566,7 +21171,7 @@ function assertUnattendedTerminalSupported(terminal) {
20566
21171
  }
20567
21172
  function isKillSwitchEngaged() {
20568
21173
  if (process.env.SYNTAUR_SCHEDULES_DISABLED === "1") return true;
20569
- return existsSync3(resolve32(schedulesDir(), "KILL"));
21174
+ return existsSync3(resolve33(schedulesDir(), "KILL"));
20570
21175
  }
20571
21176
  function canFire(job, deps) {
20572
21177
  const killSwitch = deps.killSwitch ?? isKillSwitchEngaged;
@@ -20718,25 +21323,25 @@ init_paths();
20718
21323
  init_assignment_resolver();
20719
21324
  init_frontmatter();
20720
21325
  import { readFile as readFile23 } from "fs/promises";
20721
- import { resolve as resolve33 } from "path";
21326
+ import { resolve as resolve34 } from "path";
20722
21327
 
20723
21328
  // src/schedules/launch-ack.ts
20724
21329
  init_session_id();
20725
- import { readdirSync } from "fs";
21330
+ import { readdirSync as readdirSync2 } from "fs";
20726
21331
  import { homedir as homedir6 } from "os";
20727
21332
  import { join as join8 } from "path";
20728
21333
  function defaultSleep(ms) {
20729
21334
  return new Promise((r) => setTimeout(r, ms));
20730
21335
  }
20731
21336
  function runtimeSessionsDir() {
20732
- const override = process.env.SYNTAUR_RUNTIME_SESSIONS_DIR;
20733
- return override && override.length > 0 ? override : join8(homedir6(), ".syntaur", "runtime", "sessions");
21337
+ const override3 = process.env.SYNTAUR_RUNTIME_SESSIONS_DIR;
21338
+ return override3 && override3.length > 0 ? override3 : join8(homedir6(), ".syntaur", "runtime", "sessions");
20734
21339
  }
20735
21340
  function defaultAckProbe(handle) {
20736
21341
  const dir = runtimeSessionsDir();
20737
21342
  let names;
20738
21343
  try {
20739
- names = readdirSync(dir);
21344
+ names = readdirSync2(dir);
20740
21345
  } catch {
20741
21346
  return null;
20742
21347
  }
@@ -20778,7 +21383,7 @@ async function defaultReadAssignment(assignmentId) {
20778
21383
  const resolved = await resolveAssignmentById(projectsDir, assignmentsDir(), assignmentId);
20779
21384
  if (!resolved) return null;
20780
21385
  try {
20781
- const content = await readFile23(resolve33(resolved.assignmentDir, "assignment.md"), "utf-8");
21386
+ const content = await readFile23(resolve34(resolved.assignmentDir, "assignment.md"), "utf-8");
20782
21387
  return parseAssignmentFrontmatter(content);
20783
21388
  } catch {
20784
21389
  return null;
@@ -20870,7 +21475,7 @@ import { Router as Router14 } from "express";
20870
21475
  // src/db/usage-db.ts
20871
21476
  init_paths();
20872
21477
  import Database4 from "better-sqlite3";
20873
- import { resolve as resolve34 } from "path";
21478
+ import { resolve as resolve35 } from "path";
20874
21479
  var db4 = null;
20875
21480
  var USAGE_SCHEMA_VERSION = "1";
20876
21481
  var SCHEMA_SQL4 = `
@@ -20944,7 +21549,7 @@ function nowIso2() {
20944
21549
  }
20945
21550
  function initUsageDb(dbPath) {
20946
21551
  if (db4) return db4;
20947
- const finalPath = dbPath ?? resolve34(syntaurRoot(), "syntaur.db");
21552
+ const finalPath = dbPath ?? resolve35(syntaurRoot(), "syntaur.db");
20948
21553
  db4 = new Database4(finalPath);
20949
21554
  db4.pragma("journal_mode = WAL");
20950
21555
  db4.pragma("busy_timeout = 5000");
@@ -21121,6 +21726,199 @@ function listDistinctTools() {
21121
21726
 
21122
21727
  // src/dashboard/api-usage.ts
21123
21728
  init_api();
21729
+
21730
+ // src/usage/engagement-cost.ts
21731
+ init_session_db();
21732
+ init_engagement_tokens();
21733
+
21734
+ // src/usage/pricing.ts
21735
+ var PER_MILLION = 1e6;
21736
+ var MODEL_PRICING = {
21737
+ // Moonshot Kimi K2.6 — the model pi emits today. Official Moonshot list price.
21738
+ // source: https://platform.moonshot.ai/ (official) — cross-checked
21739
+ // https://openrouter.ai/moonshotai/kimi-k2.6 (retrieved 2026-06-17)
21740
+ "moonshotai/kimi-k2.6": { input: 0.95, output: 4, cacheRead: 0.16, cacheWrite: 0.95 },
21741
+ // Moonshot Kimi K2.5 — prior Kimi model. Official Moonshot list price.
21742
+ // source: https://platform.moonshot.ai/ — cross-checked
21743
+ // https://openrouter.ai/moonshotai/kimi-k2.5 (retrieved 2026-06-17)
21744
+ "moonshotai/kimi-k2.5": { input: 0.6, output: 3, cacheRead: 0.1, cacheWrite: 0.6 },
21745
+ // Z.ai (Zhipu) GLM-5.2 — official z.ai platform list price.
21746
+ // source: https://docs.z.ai/guides/overview/pricing — cross-checked
21747
+ // https://openrouter.ai/z-ai/glm-5 (retrieved 2026-06-18)
21748
+ "zai-org/glm-5.2": { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 1.4 },
21749
+ // MiniMax M2.5 — official MiniMax platform list price. No separately-pinned
21750
+ // cached-read rate is published, so cacheRead is set conservatively to the
21751
+ // input rate (upper bound); revise if MiniMax publishes a cache rate.
21752
+ // source: https://platform.minimax.io/docs/guides/pricing-token-plan
21753
+ // — cross-checked https://openrouter.ai/minimax/minimax-m2.5 (retrieved 2026-06-18)
21754
+ "minimaxai/minimax-m2.5": { input: 0.15, output: 0.9, cacheRead: 0.15, cacheWrite: 0.15 }
21755
+ // NOTE: opaque Synthetic tier aliases like `syn:large:text` have no public
21756
+ // per-token rate (they route to whatever Synthetic assigns), so they remain
21757
+ // unpriced (→ $0). Reseller discounts (e.g. DeepInfra K2.6 0.75/3.50/0.15) are
21758
+ // rejected by the canonical-source rule and are NOT used here.
21759
+ };
21760
+ function normalizeModelKey(model) {
21761
+ return model.replace(/^\s*\[[^\]]*\]\s*/, "").replace(/^hf:/i, "").trim().toLowerCase();
21762
+ }
21763
+ function priceForModel(model, tokens) {
21764
+ const rate = MODEL_PRICING[normalizeModelKey(model)];
21765
+ if (!rate) return null;
21766
+ return (tokens.inputTokens * rate.input + tokens.outputTokens * rate.output + tokens.cacheReadTokens * rate.cacheRead + tokens.cacheCreationTokens * rate.cacheWrite) / PER_MILLION;
21767
+ }
21768
+
21769
+ // src/usage/engagement-cost.ts
21770
+ var EMPTY = {
21771
+ cost: 0,
21772
+ pricedWindowCount: 0,
21773
+ uncomputableWindowCount: 0,
21774
+ negativeDeltaCount: 0
21775
+ };
21776
+ function engagementDb() {
21777
+ let db5;
21778
+ try {
21779
+ db5 = getSessionDb();
21780
+ } catch {
21781
+ return null;
21782
+ }
21783
+ const has = db5.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'engagement'").get();
21784
+ return has ? db5 : null;
21785
+ }
21786
+ function sinceBound(since) {
21787
+ return since ? `${since}T00:00:00.000Z` : void 0;
21788
+ }
21789
+ function untilBound(until) {
21790
+ return until ? `${until}T23:59:59.999Z` : void 0;
21791
+ }
21792
+ function toBuckets(open6, close) {
21793
+ const o = open6 ?? { input: 0, output: 0, cacheCreation: 0, cacheRead: 0, total: 0, cost: 0 };
21794
+ const c = close ?? { input: 0, output: 0, cacheCreation: 0, cacheRead: 0, total: 0, cost: 0 };
21795
+ return {
21796
+ inputTokens: Math.max(0, c.input - o.input),
21797
+ outputTokens: Math.max(0, c.output - o.output),
21798
+ cacheCreationTokens: Math.max(0, c.cacheCreation - o.cacheCreation),
21799
+ cacheReadTokens: Math.max(0, c.cacheRead - o.cacheRead)
21800
+ };
21801
+ }
21802
+ function windowCost(openJson, closeJson, modelFilter) {
21803
+ const open6 = parseSnapshot(openJson);
21804
+ const close = parseSnapshot(closeJson);
21805
+ if (!open6 || !close) return null;
21806
+ let cost = 0;
21807
+ let hadNegative = false;
21808
+ const models = /* @__PURE__ */ new Set([
21809
+ ...Object.keys(open6.models),
21810
+ ...Object.keys(close.models)
21811
+ ]);
21812
+ for (const model of models) {
21813
+ if (modelFilter && model !== modelFilter) continue;
21814
+ const openCost = open6.models[model]?.cost ?? 0;
21815
+ const closeCost = close.models[model]?.cost ?? 0;
21816
+ let contribution = closeCost - openCost;
21817
+ if (contribution < 0) {
21818
+ hadNegative = true;
21819
+ contribution = 0;
21820
+ } else if (contribution === 0) {
21821
+ const buckets = toBuckets(open6.models[model], close.models[model]);
21822
+ if (buckets.inputTokens + buckets.outputTokens + buckets.cacheCreationTokens + buckets.cacheReadTokens > 0) {
21823
+ const priced = priceForModel(model, buckets);
21824
+ if (priced && priced > 0) contribution = priced;
21825
+ }
21826
+ }
21827
+ cost += contribution;
21828
+ }
21829
+ return { cost, hadNegative };
21830
+ }
21831
+ function rollupWindows(rows, modelFilter) {
21832
+ const out = { ...EMPTY };
21833
+ for (const row of rows) {
21834
+ const wc = windowCost(row.tokens_at_open, row.tokens_at_close, modelFilter);
21835
+ if (wc === null) {
21836
+ out.uncomputableWindowCount += 1;
21837
+ continue;
21838
+ }
21839
+ out.pricedWindowCount += 1;
21840
+ out.cost += wc.cost;
21841
+ if (wc.hadNegative) out.negativeDeltaCount += 1;
21842
+ }
21843
+ return out;
21844
+ }
21845
+ function assignmentWindowCost(opts) {
21846
+ const db5 = engagementDb();
21847
+ if (!db5) return { ...EMPTY };
21848
+ const clauses = ["ended_at IS NOT NULL"];
21849
+ const params2 = [];
21850
+ if (opts.assignmentId) {
21851
+ clauses.push("assignment_id = ?");
21852
+ params2.push(opts.assignmentId);
21853
+ } else {
21854
+ const proj = opts.projectSlug && opts.projectSlug.length > 0 ? opts.projectSlug : null;
21855
+ if (proj === null) {
21856
+ clauses.push("project_slug IS NULL");
21857
+ } else {
21858
+ clauses.push("project_slug = ?");
21859
+ params2.push(proj);
21860
+ }
21861
+ clauses.push("assignment_slug = ?");
21862
+ params2.push(opts.assignmentSlug ?? null);
21863
+ }
21864
+ const since = sinceBound(opts.since);
21865
+ const until = untilBound(opts.until);
21866
+ if (since) {
21867
+ clauses.push("ended_at >= ?");
21868
+ params2.push(since);
21869
+ }
21870
+ if (until) {
21871
+ clauses.push("ended_at <= ?");
21872
+ params2.push(until);
21873
+ }
21874
+ const rows = db5.prepare(
21875
+ `SELECT assignment_id, assignment_slug, tokens_at_open, tokens_at_close
21876
+ FROM engagement
21877
+ WHERE ${clauses.join(" AND ")}`
21878
+ ).all(...params2);
21879
+ return rollupWindows(rows, opts.model);
21880
+ }
21881
+ function projectWindowCosts(opts) {
21882
+ const db5 = engagementDb();
21883
+ if (!db5) return /* @__PURE__ */ new Map();
21884
+ const clauses = ["ended_at IS NOT NULL", "project_slug = ?", "assignment_slug IS NOT NULL"];
21885
+ const params2 = [opts.projectSlug];
21886
+ const since = sinceBound(opts.since);
21887
+ const until = untilBound(opts.until);
21888
+ if (since) {
21889
+ clauses.push("ended_at >= ?");
21890
+ params2.push(since);
21891
+ }
21892
+ if (until) {
21893
+ clauses.push("ended_at <= ?");
21894
+ params2.push(until);
21895
+ }
21896
+ const rows = db5.prepare(
21897
+ `SELECT assignment_id, assignment_slug, tokens_at_open, tokens_at_close
21898
+ FROM engagement
21899
+ WHERE ${clauses.join(" AND ")}`
21900
+ ).all(...params2);
21901
+ const grouped = /* @__PURE__ */ new Map();
21902
+ const idForSlug = /* @__PURE__ */ new Map();
21903
+ for (const row of rows) {
21904
+ const slug = row.assignment_slug;
21905
+ const bucket = grouped.get(slug);
21906
+ if (bucket) bucket.push(row);
21907
+ else grouped.set(slug, [row]);
21908
+ if (row.assignment_id && !idForSlug.get(slug)) idForSlug.set(slug, row.assignment_id);
21909
+ }
21910
+ const out = /* @__PURE__ */ new Map();
21911
+ for (const [slug, bucket] of grouped) {
21912
+ out.set(slug, {
21913
+ assignmentSlug: slug,
21914
+ assignmentId: idForSlug.get(slug) ?? null,
21915
+ ...rollupWindows(bucket, opts.model)
21916
+ });
21917
+ }
21918
+ return out;
21919
+ }
21920
+
21921
+ // src/dashboard/api-usage.ts
21124
21922
  function createUsageRouter(projectsDir, assignmentsDir2) {
21125
21923
  const router = Router14();
21126
21924
  router.get("/facets", (_req, res) => {
@@ -21160,14 +21958,12 @@ function createUsageRouter(projectsDir, assignmentsDir2) {
21160
21958
  try {
21161
21959
  initUsageDb();
21162
21960
  const projectSlug = req2.params.projectSlug;
21163
- const rows = listDaily({
21164
- ...extractCommonFilter(req2.query),
21165
- projectSlug
21166
- });
21961
+ const common = extractCommonFilter(req2.query);
21962
+ const rows = listDaily({ ...common, projectSlug });
21167
21963
  res.json({
21168
21964
  projectSlug,
21169
21965
  daily: rows,
21170
- summary: summarize(rows, "assignment")
21966
+ summary: projectAssignmentRollup(projectSlug, rows, common)
21171
21967
  });
21172
21968
  } catch (error) {
21173
21969
  res.status(500).json({
@@ -21193,7 +21989,13 @@ function createUsageRouter(projectsDir, assignmentsDir2) {
21193
21989
  assignmentSlug,
21194
21990
  daily: dailyRows,
21195
21991
  events: eventRows,
21196
- summary: buildAssignmentSummary(dailyRows)
21992
+ summary: buildAssignmentSummary(dailyRows, {
21993
+ projectSlug,
21994
+ assignmentSlug,
21995
+ since: common.since,
21996
+ until: common.until,
21997
+ model: common.model
21998
+ })
21197
21999
  });
21198
22000
  } catch (error) {
21199
22001
  res.status(500).json({
@@ -21218,7 +22020,15 @@ function createUsageRouter(projectsDir, assignmentsDir2) {
21218
22020
  assignmentId: assignmentSlug,
21219
22021
  daily: dailyRows,
21220
22022
  events: eventRows,
21221
- summary: buildAssignmentSummary(dailyRows)
22023
+ // Standalone: engagement stores `project_slug IS NULL` (the reader maps
22024
+ // a null/empty projectSlug to the NULL match).
22025
+ summary: buildAssignmentSummary(dailyRows, {
22026
+ projectSlug: null,
22027
+ assignmentSlug,
22028
+ since: common.since,
22029
+ until: common.until,
22030
+ model: common.model
22031
+ })
21222
22032
  });
21223
22033
  } catch (error) {
21224
22034
  res.status(500).json({
@@ -21268,13 +22078,17 @@ function byModelBreakdown(rows) {
21268
22078
  }
21269
22079
  return [...map.values()].sort((a, b) => b.totalTokens - a.totalTokens);
21270
22080
  }
21271
- function buildAssignmentSummary(rows) {
22081
+ function buildAssignmentSummary(rows, costKey) {
21272
22082
  const totals = summarize(rows, "assignment")[0];
22083
+ const windows = assignmentWindowCost(costKey);
21273
22084
  return {
21274
22085
  totalTokens: totals?.totalTokens ?? 0,
21275
- totalCost: totals?.totalCost ?? 0,
22086
+ totalCost: windows.cost,
21276
22087
  lastEventDay: totals?.lastEventDay ?? null,
21277
- byModel: byModelBreakdown(rows)
22088
+ byModel: byModelBreakdown(rows),
22089
+ pricedWindowCount: windows.pricedWindowCount,
22090
+ uncomputableWindowCount: windows.uncomputableWindowCount,
22091
+ negativeDeltaCount: windows.negativeDeltaCount
21278
22092
  };
21279
22093
  }
21280
22094
  function summarize(rows, mode) {
@@ -21298,27 +22112,70 @@ function summarize(rows, mode) {
21298
22112
  }
21299
22113
  return [...map.values()].sort((a, b) => b.totalTokens - a.totalTokens);
21300
22114
  }
22115
+ function projectAssignmentRollup(projectSlug, rows, common) {
22116
+ const byAssignment = /* @__PURE__ */ new Map();
22117
+ for (const row of summarize(rows, "assignment")) {
22118
+ byAssignment.set(row.assignmentSlug, {
22119
+ ...row,
22120
+ totalCost: 0,
22121
+ // Counts present on EVERY per-assignment row (a daily-only assignment with
22122
+ // no closed window stays at 0/0/0); window overlay below replaces them.
22123
+ pricedWindowCount: 0,
22124
+ uncomputableWindowCount: 0,
22125
+ negativeDeltaCount: 0
22126
+ });
22127
+ }
22128
+ const windows = projectWindowCosts({
22129
+ projectSlug,
22130
+ since: common.since,
22131
+ until: common.until,
22132
+ model: common.model
22133
+ });
22134
+ for (const [assignmentSlug, w] of windows) {
22135
+ const existing = byAssignment.get(assignmentSlug);
22136
+ if (existing) {
22137
+ existing.totalCost = w.cost;
22138
+ existing.pricedWindowCount = w.pricedWindowCount;
22139
+ existing.uncomputableWindowCount = w.uncomputableWindowCount;
22140
+ existing.negativeDeltaCount = w.negativeDeltaCount;
22141
+ } else {
22142
+ byAssignment.set(assignmentSlug, {
22143
+ projectSlug,
22144
+ assignmentSlug,
22145
+ totalTokens: 0,
22146
+ totalCost: w.cost,
22147
+ lastEventDay: "",
22148
+ pricedWindowCount: w.pricedWindowCount,
22149
+ uncomputableWindowCount: w.uncomputableWindowCount,
22150
+ negativeDeltaCount: w.negativeDeltaCount
22151
+ });
22152
+ }
22153
+ }
22154
+ return [...byAssignment.values()].sort(
22155
+ (a, b) => b.totalCost - a.totalCost || b.totalTokens - a.totalTokens
22156
+ );
22157
+ }
21301
22158
 
21302
22159
  // src/dashboard/api-events.ts
21303
22160
  init_fs();
21304
22161
  init_parser();
21305
22162
  init_events_db();
21306
22163
  import { Router as Router15 } from "express";
21307
- import { resolve as resolve35 } from "path";
22164
+ import { resolve as resolve36 } from "path";
21308
22165
  import { readFile as readFile24 } from "fs/promises";
21309
22166
  function createEventsRouter(projectsDir, assignmentsDir2) {
21310
22167
  const router = Router15();
21311
22168
  router.get("/projects/:slug/assignments/:aslug/events", async (req2, res) => {
21312
22169
  try {
21313
22170
  const { slug, aslug } = req2.params;
21314
- const assignmentMdPath = resolve35(
22171
+ const assignmentMdPath = resolve36(
21315
22172
  projectsDir,
21316
22173
  slug,
21317
22174
  "assignments",
21318
22175
  aslug,
21319
22176
  "assignment.md"
21320
22177
  );
21321
- const id = await readAssignmentId(assignmentMdPath);
22178
+ const id = await readAssignmentId2(assignmentMdPath);
21322
22179
  if (!id) {
21323
22180
  res.json({ events: [] });
21324
22181
  return;
@@ -21332,8 +22189,8 @@ function createEventsRouter(projectsDir, assignmentsDir2) {
21332
22189
  router.get("/standalone/assignments/:id/events", async (req2, res) => {
21333
22190
  try {
21334
22191
  const { id: dirId } = req2.params;
21335
- const assignmentMdPath = resolve35(assignmentsDir2, dirId, "assignment.md");
21336
- const id = await readAssignmentId(assignmentMdPath) ?? dirId;
22192
+ const assignmentMdPath = resolve36(assignmentsDir2, dirId, "assignment.md");
22193
+ const id = await readAssignmentId2(assignmentMdPath) ?? dirId;
21337
22194
  res.json({ events: loadEvents(id, req2.query) });
21338
22195
  } catch (error) {
21339
22196
  console.warn("[events] failed to list standalone events:", error);
@@ -21342,7 +22199,7 @@ function createEventsRouter(projectsDir, assignmentsDir2) {
21342
22199
  });
21343
22200
  return router;
21344
22201
  }
21345
- async function readAssignmentId(assignmentMdPath) {
22202
+ async function readAssignmentId2(assignmentMdPath) {
21346
22203
  if (!await fileExists(assignmentMdPath)) return null;
21347
22204
  const content = await readFile24(assignmentMdPath, "utf-8");
21348
22205
  const [fm] = extractFrontmatter2(content);
@@ -21389,7 +22246,7 @@ init_assignment_walk();
21389
22246
  init_parser();
21390
22247
  init_facts();
21391
22248
  init_state_machine();
21392
- import { resolve as resolve36 } from "path";
22249
+ import { resolve as resolve37 } from "path";
21393
22250
  import { readFile as readFile25 } from "fs/promises";
21394
22251
 
21395
22252
  // src/inbox/types.ts
@@ -21570,7 +22427,7 @@ async function computeInbox(opts) {
21570
22427
  if (opts.project !== void 0 && entry.projectSlug !== opts.project) continue;
21571
22428
  let parsed;
21572
22429
  try {
21573
- const content = await readFile25(resolve36(entry.assignmentDir, "assignment.md"), "utf-8");
22430
+ const content = await readFile25(resolve37(entry.assignmentDir, "assignment.md"), "utf-8");
21574
22431
  parsed = parseAssignmentFull(content);
21575
22432
  } catch {
21576
22433
  continue;
@@ -21613,7 +22470,7 @@ async function computeInbox(opts) {
21613
22470
  });
21614
22471
  }
21615
22472
  if (!typeFilter || typeFilter.has("question")) {
21616
- const commentsPath = resolve36(entry.assignmentDir, "comments.md");
22473
+ const commentsPath = resolve37(entry.assignmentDir, "comments.md");
21617
22474
  if (await fileExists(commentsPath)) {
21618
22475
  try {
21619
22476
  const content = await readFile25(commentsPath, "utf-8");
@@ -21727,7 +22584,7 @@ init_slug();
21727
22584
  init_timestamp();
21728
22585
  init_fs();
21729
22586
  import { Router as Router17 } from "express";
21730
- import { resolve as resolve37 } from "path";
22587
+ import { resolve as resolve38 } from "path";
21731
22588
  import { readFile as readFile26 } from "fs/promises";
21732
22589
  init_playbooks();
21733
22590
  function statusForPlaybookError(code) {
@@ -21810,7 +22667,7 @@ function createPlaybooksRouter(playbooksDir2) {
21810
22667
  res.status(404).json({ error: `Playbook "${req2.params.slug}" not found` });
21811
22668
  return;
21812
22669
  }
21813
- const filePath = resolve37(playbooksDir2, resolved.filename);
22670
+ const filePath = resolve38(playbooksDir2, resolved.filename);
21814
22671
  const content = await readFile26(filePath, "utf-8");
21815
22672
  res.json({
21816
22673
  documentType: "playbook",
@@ -21836,7 +22693,7 @@ function createPlaybooksRouter(playbooksDir2) {
21836
22693
  return;
21837
22694
  }
21838
22695
  await ensureDir(playbooksDir2);
21839
- const filePath = resolve37(playbooksDir2, `${slug}.md`);
22696
+ const filePath = resolve38(playbooksDir2, `${slug}.md`);
21840
22697
  if (await fileExists(filePath)) {
21841
22698
  res.status(409).json({ error: `Playbook "${slug}" already exists` });
21842
22699
  return;
@@ -21860,7 +22717,7 @@ function createPlaybooksRouter(playbooksDir2) {
21860
22717
  res.status(404).json({ error: `Playbook "${req2.params.slug}" not found` });
21861
22718
  return;
21862
22719
  }
21863
- const filePath = resolve37(playbooksDir2, resolved.filename);
22720
+ const filePath = resolve38(playbooksDir2, resolved.filename);
21864
22721
  await writeFileForce(filePath, content);
21865
22722
  await rebuildPlaybookManifest(playbooksDir2);
21866
22723
  res.json({ slug: resolved.slug, path: filePath });
@@ -21923,7 +22780,7 @@ init_uuid();
21923
22780
  init_paths();
21924
22781
  init_fs();
21925
22782
  init_config2();
21926
- import { resolve as resolve38 } from "path";
22783
+ import { resolve as resolve39 } from "path";
21927
22784
  async function createAssignmentCommand(title, options) {
21928
22785
  if (!title.trim()) {
21929
22786
  throw new Error("Assignment title cannot be empty.");
@@ -21998,14 +22855,14 @@ async function createAssignmentCommand(title, options) {
21998
22855
  if (options.oneOff) {
21999
22856
  const standaloneRoot = assignmentsDir();
22000
22857
  folderName = id;
22001
- assignmentDir = resolve38(standaloneRoot, folderName);
22858
+ assignmentDir = resolve39(standaloneRoot, folderName);
22002
22859
  projectSlug = null;
22003
22860
  await ensureDir(standaloneRoot);
22004
22861
  } else {
22005
22862
  const baseDir = options.dir ? expandHome(options.dir) : config.defaultProjectDir;
22006
22863
  projectSlug = options.project;
22007
- const projectDir = resolve38(baseDir, projectSlug);
22008
- const projectMdPath = resolve38(projectDir, "project.md");
22864
+ const projectDir = resolve39(baseDir, projectSlug);
22865
+ const projectMdPath = resolve39(projectDir, "project.md");
22009
22866
  if (!await fileExists(projectDir) || !await fileExists(projectMdPath)) {
22010
22867
  throw new Error(
22011
22868
  `Project "${projectSlug}" not found at ${projectDir}.
@@ -22013,9 +22870,9 @@ Run 'syntaur create-project' first or use --one-off.`
22013
22870
  );
22014
22871
  }
22015
22872
  if (dependsOn.length > 0) {
22016
- const depDirBase = resolve38(projectDir, "assignments");
22873
+ const depDirBase = resolve39(projectDir, "assignments");
22017
22874
  for (const dep of dependsOn) {
22018
- const depDir = resolve38(depDirBase, dep);
22875
+ const depDir = resolve39(depDirBase, dep);
22019
22876
  if (!await fileExists(depDir)) {
22020
22877
  console.warn(
22021
22878
  `Warning: dependency "${dep}" does not exist in project "${projectSlug}" yet.`
@@ -22024,7 +22881,7 @@ Run 'syntaur create-project' first or use --one-off.`
22024
22881
  }
22025
22882
  }
22026
22883
  folderName = assignmentSlug;
22027
- assignmentDir = resolve38(projectDir, "assignments", folderName);
22884
+ assignmentDir = resolve39(projectDir, "assignments", folderName);
22028
22885
  }
22029
22886
  if (await fileExists(assignmentDir)) {
22030
22887
  throw new Error(
@@ -22036,7 +22893,7 @@ Use --slug to specify a different slug.`
22036
22893
  const companionAssignmentRef = projectSlug === null ? id : assignmentSlug;
22037
22894
  const files = [
22038
22895
  [
22039
- resolve38(assignmentDir, "assignment.md"),
22896
+ resolve39(assignmentDir, "assignment.md"),
22040
22897
  renderAssignment({
22041
22898
  id,
22042
22899
  slug: assignmentSlug,
@@ -22054,35 +22911,35 @@ Use --slug to specify a different slug.`
22054
22911
  })
22055
22912
  ],
22056
22913
  [
22057
- resolve38(assignmentDir, "scratchpad.md"),
22914
+ resolve39(assignmentDir, "scratchpad.md"),
22058
22915
  renderScratchpad({
22059
22916
  assignmentSlug: companionAssignmentRef,
22060
22917
  timestamp
22061
22918
  })
22062
22919
  ],
22063
22920
  [
22064
- resolve38(assignmentDir, "handoff.md"),
22921
+ resolve39(assignmentDir, "handoff.md"),
22065
22922
  renderHandoff({
22066
22923
  assignmentSlug: companionAssignmentRef,
22067
22924
  timestamp
22068
22925
  })
22069
22926
  ],
22070
22927
  [
22071
- resolve38(assignmentDir, "decision-record.md"),
22928
+ resolve39(assignmentDir, "decision-record.md"),
22072
22929
  renderDecisionRecord({
22073
22930
  assignmentSlug: companionAssignmentRef,
22074
22931
  timestamp
22075
22932
  })
22076
22933
  ],
22077
22934
  [
22078
- resolve38(assignmentDir, "progress.md"),
22935
+ resolve39(assignmentDir, "progress.md"),
22079
22936
  renderProgress({
22080
22937
  assignment: companionAssignmentRef,
22081
22938
  timestamp
22082
22939
  })
22083
22940
  ],
22084
22941
  [
22085
- resolve38(assignmentDir, "comments.md"),
22942
+ resolve39(assignmentDir, "comments.md"),
22086
22943
  renderComments({
22087
22944
  assignment: companionAssignmentRef,
22088
22945
  timestamp
@@ -22256,7 +23113,7 @@ import { raw } from "express";
22256
23113
 
22257
23114
  // src/todos/attachments.ts
22258
23115
  import { mkdir as mkdir4, readdir as readdir15, stat as stat6, rename as rename5, rm as rm4, unlink as unlink7, writeFile as writeFile6, cp } from "fs/promises";
22259
- import { resolve as resolve39, basename as basename6, dirname as dirname9, extname } from "path";
23116
+ import { resolve as resolve40, basename as basename6, dirname as dirname9, extname } from "path";
22260
23117
 
22261
23118
  // src/utils/proof-artifact-id.ts
22262
23119
  import { randomBytes as randomBytes2 } from "crypto";
@@ -22343,12 +23200,12 @@ function sanitizeAttachmentName(name) {
22343
23200
  return n;
22344
23201
  }
22345
23202
  function attachmentsRootDir(todosDir2) {
22346
- return resolve39(todosDir2, "attachments");
23203
+ return resolve40(todosDir2, "attachments");
22347
23204
  }
22348
23205
  function attachmentDirFor(todosDir2, scopeId, todoId) {
22349
23206
  assertScope(scopeId);
22350
23207
  assertTodoId(todoId);
22351
- return resolve39(attachmentsRootDir(todosDir2), scopeId, todoId);
23208
+ return resolve40(attachmentsRootDir(todosDir2), scopeId, todoId);
22352
23209
  }
22353
23210
  async function dirExists(p) {
22354
23211
  try {
@@ -22362,7 +23219,7 @@ async function writeAttachment(todosDir2, scopeId, todoId, originalName, bytes)
22362
23219
  await mkdir4(dir, { recursive: true });
22363
23220
  const id = generateArtifactId();
22364
23221
  const filename = sanitizeAttachmentName(originalName);
22365
- await writeFile6(resolve39(dir, `${id}__${filename}`), bytes);
23222
+ await writeFile6(resolve40(dir, `${id}__${filename}`), bytes);
22366
23223
  return {
22367
23224
  id,
22368
23225
  filename,
@@ -22387,7 +23244,7 @@ async function listAttachments(todosDir2, scopeId, todoId) {
22387
23244
  if (!ATTACHMENT_ID_RE.test(id)) continue;
22388
23245
  const filename = stored.slice(sep2 + 2);
22389
23246
  try {
22390
- const st = await stat6(resolve39(dir, stored));
23247
+ const st = await stat6(resolve40(dir, stored));
22391
23248
  if (!st.isFile()) continue;
22392
23249
  out.push({ id, filename, mime: mimeForName(filename), size: st.size, createdAt: st.mtime.toISOString() });
22393
23250
  } catch {
@@ -22398,7 +23255,7 @@ async function listAttachments(todosDir2, scopeId, todoId) {
22398
23255
  }
22399
23256
  async function readScopeAttachments(todosDir2, scopeId) {
22400
23257
  assertScope(scopeId);
22401
- const scopeDir = resolve39(attachmentsRootDir(todosDir2), scopeId);
23258
+ const scopeDir = resolve40(attachmentsRootDir(todosDir2), scopeId);
22402
23259
  let todoIds;
22403
23260
  try {
22404
23261
  todoIds = await readdir15(scopeDir);
@@ -22426,7 +23283,7 @@ async function resolveAttachmentFile(todosDir2, scopeId, todoId, attachmentId) {
22426
23283
  const stored = names.find((n) => n.startsWith(prefix));
22427
23284
  if (!stored) return null;
22428
23285
  const filename = stored.slice(prefix.length);
22429
- return { path: resolve39(dir, stored), filename, mime: mimeForName(filename) };
23286
+ return { path: resolve40(dir, stored), filename, mime: mimeForName(filename) };
22430
23287
  }
22431
23288
  async function deleteAttachment(todosDir2, scopeId, todoId, attachmentId) {
22432
23289
  const resolved = await resolveAttachmentFile(todosDir2, scopeId, todoId, attachmentId);
@@ -22842,8 +23699,8 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
22842
23699
  router.post("/:workspace/archive", async (req2, res) => {
22843
23700
  try {
22844
23701
  const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
22845
- const { resolve: resolve48 } = await import("path");
22846
- const { readFile: readFile32 } = await import("fs/promises");
23702
+ const { resolve: resolve49 } = await import("path");
23703
+ const { readFile: readFile31 } = await import("fs/promises");
22847
23704
  const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
22848
23705
  const workspace = getWorkspaceParam(req2.params.workspace);
22849
23706
  const outcome = await wsLock(workspace, async () => {
@@ -22859,10 +23716,10 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
22859
23716
  (e) => e.itemIds.every((id) => completedIds.has(id))
22860
23717
  );
22861
23718
  const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
22862
- await ensureDir(resolve48(todosDir2, "archive"));
23719
+ await ensureDir(resolve49(todosDir2, "archive"));
22863
23720
  let archContent = "";
22864
23721
  if (await fileExists(archFile)) {
22865
- archContent = await readFile32(archFile, "utf-8");
23722
+ archContent = await readFile31(archFile, "utf-8");
22866
23723
  archContent = archContent.trimEnd() + "\n\n";
22867
23724
  } else {
22868
23725
  archContent = `---
@@ -23163,7 +24020,7 @@ workspace: ${workspace}
23163
24020
  const { readConfig: readConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
23164
24021
  const { assignmentsDir: assignmentsDirFn } = await Promise.resolve().then(() => (init_paths(), paths_exports));
23165
24022
  const { fileExists: fileExists2, writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
23166
- const { readFile: readFile32 } = await import("fs/promises");
24023
+ const { readFile: readFile31 } = await import("fs/promises");
23167
24024
  const { appendTodosToAssignmentBody: appendTodosToAssignmentBody2, touchAssignmentUpdated: touchAssignmentUpdated2 } = await Promise.resolve().then(() => (init_assignment_todos(), assignment_todos_exports));
23168
24025
  const { nowTimestamp: nowTimestamp3 } = await Promise.resolve().then(() => (init_timestamp(), timestamp_exports));
23169
24026
  let assignmentRef;
@@ -23184,7 +24041,7 @@ workspace: ${workspace}
23184
24041
  }
23185
24042
  const assignmentMdPath = resolvePath2(assignmentDir, "assignment.md");
23186
24043
  if (!await fileExists2(assignmentMdPath)) return { error: `Target assignment not found: ${assignmentMdPath}` };
23187
- let content = await readFile32(assignmentMdPath, "utf-8");
24044
+ let content = await readFile31(assignmentMdPath, "utf-8");
23188
24045
  content = appendTodosToAssignmentBody2(
23189
24046
  content,
23190
24047
  items.map((it) => ({
@@ -23380,7 +24237,7 @@ init_paths();
23380
24237
  init_slug();
23381
24238
  import { Router as Router20 } from "express";
23382
24239
  import { mkdir as mkdir6, readFile as readFile27, rename as rename7 } from "fs/promises";
23383
- import { resolve as resolve40, dirname as dirname11 } from "path";
24240
+ import { resolve as resolve41, dirname as dirname11 } from "path";
23384
24241
  init_api();
23385
24242
  var WORKSPACE_REGEX2 = /^[a-z0-9_][a-z0-9-]*$/;
23386
24243
  function touchItem4(item) {
@@ -23396,7 +24253,7 @@ function params(req2) {
23396
24253
  return req2.params;
23397
24254
  }
23398
24255
  async function projectExists(projectsDir, slug) {
23399
- return fileExists(resolve40(projectsDir, slug, "project.md"));
24256
+ return fileExists(resolve41(projectsDir, slug, "project.md"));
23400
24257
  }
23401
24258
  async function ensureProjectTodosDir(projectsDir, slug) {
23402
24259
  const todosDir2 = projectTodosDir(projectsDir, slug);
@@ -23413,7 +24270,7 @@ async function ensureProjectTodosDir(projectsDir, slug) {
23413
24270
  throw err;
23414
24271
  }
23415
24272
  try {
23416
- await mkdir6(resolve40(todosDir2, "archive"), { recursive: false });
24273
+ await mkdir6(resolve41(todosDir2, "archive"), { recursive: false });
23417
24274
  } catch (err) {
23418
24275
  const code = err.code;
23419
24276
  if (code === "EEXIST") return;
@@ -24092,15 +24949,15 @@ workspace: ${slug}
24092
24949
  if (tg.includes("/")) {
24093
24950
  const parts = tg.split("/");
24094
24951
  if (parts.length !== 2) return { error: `Invalid target.assignment "${tg}"` };
24095
- assignmentDir = resolve40(projectsDir, parts[0], "assignments", parts[1]);
24952
+ assignmentDir = resolve41(projectsDir, parts[0], "assignments", parts[1]);
24096
24953
  assignmentRef = `${parts[0]}/${parts[1]}`;
24097
24954
  } else if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(tg)) {
24098
- assignmentDir = resolve40(assignmentsDirFn(), tg);
24955
+ assignmentDir = resolve41(assignmentsDirFn(), tg);
24099
24956
  assignmentRef = tg;
24100
24957
  } else {
24101
24958
  return { error: `Invalid target.assignment "${tg}"` };
24102
24959
  }
24103
- const assignmentMdPath = resolve40(assignmentDir, "assignment.md");
24960
+ const assignmentMdPath = resolve41(assignmentDir, "assignment.md");
24104
24961
  if (!await fileExists(assignmentMdPath)) return { error: `Target assignment not found: ${assignmentMdPath}` };
24105
24962
  let content = await readFile27(assignmentMdPath, "utf-8");
24106
24963
  content = appendTodosToAssignmentBody2(
@@ -24482,7 +25339,7 @@ init_fs();
24482
25339
  init_paths();
24483
25340
  init_slug();
24484
25341
  import { Router as Router22 } from "express";
24485
- import { resolve as resolve41 } from "path";
25342
+ import { resolve as resolve42 } from "path";
24486
25343
  init_parser2();
24487
25344
  function deriveStatus2(bundle, items) {
24488
25345
  const members = bundle.todoIds.map((id) => items.find((i) => i.id === id)).filter((i) => i !== void 0);
@@ -24524,7 +25381,7 @@ function createProjectBundlesRouter(projectsDir, broadcast) {
24524
25381
  router.get("/", async (req2, res) => {
24525
25382
  try {
24526
25383
  const slug = getProjectIdParam2(req2.params.projectId);
24527
- const projectMd = resolve41(projectsDir, slug, "project.md");
25384
+ const projectMd = resolve42(projectsDir, slug, "project.md");
24528
25385
  if (!await fileExists(projectMd)) {
24529
25386
  notFound2(res, slug);
24530
25387
  return;
@@ -24554,7 +25411,7 @@ init_config2();
24554
25411
  import { execFile as execFile2 } from "child_process";
24555
25412
  import { promisify as promisify2 } from "util";
24556
25413
  import { cp as cp2, mkdtemp, rm as rm5, readFile as readFile29, writeFile as writeFile7, unlink as unlink8, stat as stat7, open as open5, rename as rename8 } from "fs/promises";
24557
- import { resolve as resolve42, join as join9 } from "path";
25414
+ import { resolve as resolve43, join as join9 } from "path";
24558
25415
  import { tmpdir } from "os";
24559
25416
  var exec2 = promisify2(execFile2);
24560
25417
  var VALID_CATEGORIES = ["projects", "playbooks", "todos", "servers", "config"];
@@ -24594,7 +25451,7 @@ async function resolveCategoryPath(category) {
24594
25451
  case "servers":
24595
25452
  return { sourcePath: serversDir(), repoPath: "servers", isFile: false };
24596
25453
  case "config":
24597
- return { sourcePath: resolve42(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
25454
+ return { sourcePath: resolve43(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
24598
25455
  }
24599
25456
  }
24600
25457
  async function checkGitInstalled() {
@@ -24605,7 +25462,7 @@ async function checkGitInstalled() {
24605
25462
  }
24606
25463
  }
24607
25464
  async function acquireLock2() {
24608
- const lockPath = resolve42(syntaurRoot(), LOCK_FILE_NAME);
25465
+ const lockPath = resolve43(syntaurRoot(), LOCK_FILE_NAME);
24609
25466
  await ensureDir(syntaurRoot());
24610
25467
  try {
24611
25468
  const handle = await open5(lockPath, "wx");
@@ -24652,7 +25509,7 @@ async function copyRecursive(src, dest) {
24652
25509
  await ensureDir(dest);
24653
25510
  await cp2(src, dest, { recursive: true, force: true });
24654
25511
  } else {
24655
- await ensureDir(resolve42(dest, ".."));
25512
+ await ensureDir(resolve43(dest, ".."));
24656
25513
  await cp2(src, dest, { force: true });
24657
25514
  }
24658
25515
  }
@@ -24700,7 +25557,7 @@ async function backupToGithub(overrides) {
24700
25557
  }
24701
25558
  if (category === "config") {
24702
25559
  const sanitized = await readSanitizedConfig(sourcePath);
24703
- await ensureDir(resolve42(destPath, ".."));
25560
+ await ensureDir(resolve43(destPath, ".."));
24704
25561
  await writeFile7(destPath, sanitized, "utf-8");
24705
25562
  } else {
24706
25563
  await copyRecursive(sourcePath, destPath);
@@ -24754,7 +25611,7 @@ async function backupToGithub(overrides) {
24754
25611
  }
24755
25612
  async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
24756
25613
  if (isFile) {
24757
- await ensureDir(resolve42(localPath, ".."));
25614
+ await ensureDir(resolve43(localPath, ".."));
24758
25615
  await cp2(repoSrcPath, localPath, { force: true });
24759
25616
  return;
24760
25617
  }
@@ -24855,7 +25712,7 @@ async function restoreFromGithub(overrides) {
24855
25712
  }
24856
25713
  async function getBackupStatus() {
24857
25714
  const config = await readConfig();
24858
- const lockPath = resolve42(syntaurRoot(), LOCK_FILE_NAME);
25715
+ const lockPath = resolve43(syntaurRoot(), LOCK_FILE_NAME);
24859
25716
  const locked = await fileExists(lockPath);
24860
25717
  return {
24861
25718
  repo: config.backup?.repo ?? null,
@@ -25338,7 +26195,7 @@ async function runCcusage(opts = {}) {
25338
26195
  };
25339
26196
  }
25340
26197
  function runOnce(binary, args, env, timeoutMs, maxOutputBytes) {
25341
- return new Promise((resolve48) => {
26198
+ return new Promise((resolve49) => {
25342
26199
  const child = spawn4(binary, args, {
25343
26200
  env: env ?? process.env,
25344
26201
  stdio: ["ignore", "pipe", "pipe"]
@@ -25352,7 +26209,7 @@ function runOnce(binary, args, env, timeoutMs, maxOutputBytes) {
25352
26209
  if (settled) return;
25353
26210
  settled = true;
25354
26211
  clearTimeout(timer3);
25355
- resolve48(result);
26212
+ resolve49(result);
25356
26213
  };
25357
26214
  const timer3 = setTimeout(() => {
25358
26215
  timedOut = true;
@@ -25403,9 +26260,13 @@ function resolveAttribution(input, db5) {
25403
26260
  const database = db5 ?? getSessionDb();
25404
26261
  const direct = database.prepare(
25405
26262
  `SELECT project_slug, assignment_slug
25406
- FROM sessions
25407
- WHERE session_id = ?`
25408
- ).get(input.sessionId);
26263
+ FROM engagement
26264
+ WHERE session_id = ?
26265
+ AND julianday(started_at) <= julianday(?)
26266
+ AND (ended_at IS NULL OR julianday(ended_at) > julianday(?))
26267
+ ORDER BY julianday(started_at) DESC
26268
+ LIMIT 1`
26269
+ ).get(input.sessionId, input.eventTs, input.eventTs);
25409
26270
  if (direct) {
25410
26271
  return {
25411
26272
  projectSlug: direct.project_slug,
@@ -25414,25 +26275,43 @@ function resolveAttribution(input, db5) {
25414
26275
  }
25415
26276
  if (input.cwd) {
25416
26277
  const exact = database.prepare(
25417
- `SELECT project_slug, assignment_slug
25418
- FROM sessions
25419
- WHERE path = ?
25420
- AND julianday(started) <= julianday(?)
25421
- AND (ended IS NULL OR julianday(ended) >= julianday(?))
25422
- ORDER BY started DESC
26278
+ `SELECT e.project_slug AS project_slug, e.assignment_slug AS assignment_slug
26279
+ FROM sessions s
26280
+ JOIN engagement e ON e.session_id = s.session_id
26281
+ WHERE s.path = ?
26282
+ AND julianday(s.started) <= julianday(?)
26283
+ AND (s.ended IS NULL OR julianday(s.ended) >= julianday(?))
26284
+ AND julianday(e.started_at) <= julianday(?)
26285
+ AND (e.ended_at IS NULL OR julianday(e.ended_at) > julianday(?))
26286
+ ORDER BY julianday(s.started) DESC, julianday(e.started_at) DESC
25423
26287
  LIMIT 1`
25424
- ).get(input.cwd, input.eventTs, input.eventTs);
26288
+ ).get(
26289
+ input.cwd,
26290
+ input.eventTs,
26291
+ input.eventTs,
26292
+ input.eventTs,
26293
+ input.eventTs
26294
+ );
25425
26295
  if (exact) {
25426
26296
  return { projectSlug: exact.project_slug, assignmentSlug: exact.assignment_slug };
25427
26297
  }
25428
26298
  if (input.eventTs.endsWith("T00:00:00.000Z")) {
25429
26299
  const sameDay = database.prepare(
25430
- `SELECT DISTINCT project_slug, assignment_slug
25431
- FROM sessions
25432
- WHERE path = ?
25433
- AND date(started) <= date(?)
25434
- AND (ended IS NULL OR date(ended) >= date(?))`
25435
- ).all(input.cwd, input.eventTs, input.eventTs);
26300
+ `SELECT DISTINCT e.project_slug AS project_slug, e.assignment_slug AS assignment_slug
26301
+ FROM sessions s
26302
+ JOIN engagement e ON e.session_id = s.session_id
26303
+ WHERE s.path = ?
26304
+ AND date(s.started) <= date(?)
26305
+ AND (s.ended IS NULL OR date(s.ended) >= date(?))
26306
+ AND date(e.started_at) <= date(?)
26307
+ AND (e.ended_at IS NULL OR date(e.ended_at) >= date(?))`
26308
+ ).all(
26309
+ input.cwd,
26310
+ input.eventTs,
26311
+ input.eventTs,
26312
+ input.eventTs,
26313
+ input.eventTs
26314
+ );
25436
26315
  if (sameDay.length === 1) {
25437
26316
  return {
25438
26317
  projectSlug: sameDay[0].project_slug,
@@ -25503,41 +26382,6 @@ function runRollup() {
25503
26382
  return { daysComputed, rowsWritten };
25504
26383
  }
25505
26384
 
25506
- // src/usage/pricing.ts
25507
- var PER_MILLION = 1e6;
25508
- var MODEL_PRICING = {
25509
- // Moonshot Kimi K2.6 — the model pi emits today. Official Moonshot list price.
25510
- // source: https://platform.moonshot.ai/ (official) — cross-checked
25511
- // https://openrouter.ai/moonshotai/kimi-k2.6 (retrieved 2026-06-17)
25512
- "moonshotai/kimi-k2.6": { input: 0.95, output: 4, cacheRead: 0.16, cacheWrite: 0.95 },
25513
- // Moonshot Kimi K2.5 — prior Kimi model. Official Moonshot list price.
25514
- // source: https://platform.moonshot.ai/ — cross-checked
25515
- // https://openrouter.ai/moonshotai/kimi-k2.5 (retrieved 2026-06-17)
25516
- "moonshotai/kimi-k2.5": { input: 0.6, output: 3, cacheRead: 0.1, cacheWrite: 0.6 },
25517
- // Z.ai (Zhipu) GLM-5.2 — official z.ai platform list price.
25518
- // source: https://docs.z.ai/guides/overview/pricing — cross-checked
25519
- // https://openrouter.ai/z-ai/glm-5 (retrieved 2026-06-18)
25520
- "zai-org/glm-5.2": { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 1.4 },
25521
- // MiniMax M2.5 — official MiniMax platform list price. No separately-pinned
25522
- // cached-read rate is published, so cacheRead is set conservatively to the
25523
- // input rate (upper bound); revise if MiniMax publishes a cache rate.
25524
- // source: https://platform.minimax.io/docs/guides/pricing-token-plan
25525
- // — cross-checked https://openrouter.ai/minimax/minimax-m2.5 (retrieved 2026-06-18)
25526
- "minimaxai/minimax-m2.5": { input: 0.15, output: 0.9, cacheRead: 0.15, cacheWrite: 0.15 }
25527
- // NOTE: opaque Synthetic tier aliases like `syn:large:text` have no public
25528
- // per-token rate (they route to whatever Synthetic assigns), so they remain
25529
- // unpriced (→ $0). Reseller discounts (e.g. DeepInfra K2.6 0.75/3.50/0.15) are
25530
- // rejected by the canonical-source rule and are NOT used here.
25531
- };
25532
- function normalizeModelKey(model) {
25533
- return model.replace(/^\s*\[[^\]]*\]\s*/, "").replace(/^hf:/i, "").trim().toLowerCase();
25534
- }
25535
- function priceForModel(model, tokens) {
25536
- const rate = MODEL_PRICING[normalizeModelKey(model)];
25537
- if (!rate) return null;
25538
- return (tokens.inputTokens * rate.input + tokens.outputTokens * rate.output + tokens.cacheReadTokens * rate.cacheRead + tokens.cacheCreationTokens * rate.cacheWrite) / PER_MILLION;
25539
- }
25540
-
25541
26385
  // src/usage/collect.ts
25542
26386
  var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
25543
26387
  function formatDayFromCcusageDate(yyyymmdd) {
@@ -25756,7 +26600,7 @@ function createDashboardServer(options) {
25756
26600
  (async () => {
25757
26601
  try {
25758
26602
  const configResult = await migrateLegacyConfig(
25759
- resolve47(syntaurRoot(), "config.md")
26603
+ resolve48(syntaurRoot(), "config.md")
25760
26604
  );
25761
26605
  const projectResult = await migrateLegacyProjectFiles(projectsDir);
25762
26606
  const summary = summarizeMigration(projectResult, configResult);
@@ -26279,14 +27123,14 @@ function createDashboardServer(options) {
26279
27123
  app.use("/api/backup", createBackupRouter());
26280
27124
  if (serveStaticUi && dashboardDistPath) {
26281
27125
  const sendOpts = { dotfiles: "allow" };
26282
- app.use("/assets", express.static(resolve47(dashboardDistPath, "assets"), sendOpts));
27126
+ app.use("/assets", express.static(resolve48(dashboardDistPath, "assets"), sendOpts));
26283
27127
  app.use(express.static(dashboardDistPath, { ...sendOpts, index: false, fallthrough: true }));
26284
27128
  app.get("{*path}", async (req2, res) => {
26285
27129
  if (req2.path.startsWith("/api") || req2.path === "/ws" || req2.path.startsWith("/assets")) {
26286
27130
  res.status(404).json({ error: "Not Found" });
26287
27131
  return;
26288
27132
  }
26289
- const indexPath = resolve47(dashboardDistPath, "index.html");
27133
+ const indexPath = resolve48(dashboardDistPath, "index.html");
26290
27134
  if (!await fileExists(indexPath)) {
26291
27135
  res.status(503).send(
26292
27136
  'Dashboard not built. Run "npm run build:dashboard" first.'
@@ -26322,8 +27166,8 @@ function createDashboardServer(options) {
26322
27166
  if (!await migrationGate()) return;
26323
27167
  try {
26324
27168
  const context = await resolveDeriveContext2();
26325
- const projectDir = projectSlug ? resolve47(projectsDir, projectSlug) : null;
26326
- const path = projectDir ? resolve47(projectDir, "assignments", assignmentSlug, "assignment.md") : resolve47(assignmentsDir2, assignmentSlug, "assignment.md");
27169
+ const projectDir = projectSlug ? resolve48(projectsDir, projectSlug) : null;
27170
+ const path = projectDir ? resolve48(projectDir, "assignments", assignmentSlug, "assignment.md") : resolve48(assignmentsDir2, assignmentSlug, "assignment.md");
26327
27171
  if (!await fileExists(path)) return;
26328
27172
  const result = await recomputeAndWrite2(path, {
26329
27173
  cause: "derive",
@@ -26376,8 +27220,8 @@ function createDashboardServer(options) {
26376
27220
  serversDir: serversDir2,
26377
27221
  playbooksDir: playbooksDir2,
26378
27222
  todosDir: todosDir2,
26379
- dbPath: resolve47(syntaurRoot(), "syntaur.db"),
26380
- configPath: resolve47(syntaurRoot(), "config.md"),
27223
+ dbPath: resolve48(syntaurRoot(), "syntaur.db"),
27224
+ configPath: resolve48(syntaurRoot(), "config.md"),
26381
27225
  onMessage: broadcast,
26382
27226
  onAssignmentChanged: (projectSlug, assignmentSlug) => {
26383
27227
  void recomputeOne(projectSlug, assignmentSlug);
@@ -26445,7 +27289,7 @@ function createDashboardServer(options) {
26445
27289
  }
26446
27290
  });
26447
27291
  server.listen(port, () => {
26448
- const portFile = resolve47(syntaurRoot(), "dashboard-port");
27292
+ const portFile = resolve48(syntaurRoot(), "dashboard-port");
26449
27293
  writeFile8(portFile, String(port), "utf-8").catch(() => {
26450
27294
  });
26451
27295
  resolvePromise();
@@ -26469,7 +27313,7 @@ function createDashboardServer(options) {
26469
27313
  client.terminate();
26470
27314
  }
26471
27315
  clients.clear();
26472
- const portFile = resolve47(syntaurRoot(), "dashboard-port");
27316
+ const portFile = resolve48(syntaurRoot(), "dashboard-port");
26473
27317
  await unlink9(portFile).catch(() => {
26474
27318
  });
26475
27319
  server.closeAllConnections?.();