syntaur 0.69.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 +1427 -550
  52. package/dist/dashboard/server.js.map +1 -1
  53. package/dist/index.js +2816 -1559
  54. package/dist/index.js.map +1 -1
  55. package/dist/launch/index.js +913 -141
  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
@@ -35,9 +35,9 @@ function expandHome(p) {
35
35
  return p;
36
36
  }
37
37
  function syntaurRoot() {
38
- const override = process.env.SYNTAUR_HOME;
39
- if (override && override.length > 0) {
40
- return resolve(expandHome(override));
38
+ const override2 = process.env.SYNTAUR_HOME;
39
+ if (override2 && override2.length > 0) {
40
+ return resolve(expandHome(override2));
41
41
  }
42
42
  return resolve(homedir(), ".syntaur");
43
43
  }
@@ -317,7 +317,7 @@ function getTargetStatus(_from, command, table) {
317
317
  if (!table) {
318
318
  return DEFAULT_COMMAND_TARGETS.get(command) ?? null;
319
319
  }
320
- return table.get(command) ?? table.get(`${_from}:${command}`) ?? null;
320
+ return table.get(`${_from}:${command}`) ?? table.get(command) ?? null;
321
321
  }
322
322
  var DEFAULT_COMMAND_TARGETS, DEFAULT_TRANSITION_TABLE;
323
323
  var init_state_machine = __esm({
@@ -359,6 +359,288 @@ var init_state_machine = __esm({
359
359
  });
360
360
 
361
361
  // src/lifecycle/frontmatter.ts
362
+ function extractFrontmatter(fileContent) {
363
+ const match = fileContent.match(/^---\n([\s\S]*?)\n---/);
364
+ if (!match) {
365
+ throw new Error("No frontmatter found in file. Expected --- delimiters.");
366
+ }
367
+ const frontmatterBlock = match[1];
368
+ const body = fileContent.slice(match[0].length);
369
+ return [frontmatterBlock, body];
370
+ }
371
+ function parseSimpleValue(raw) {
372
+ const trimmed = raw.trim();
373
+ if (trimmed === "null" || trimmed === "~" || trimmed === "") return null;
374
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2) {
375
+ return trimmed.slice(1, -1).replace(/\\(["\\])/g, "$1");
376
+ }
377
+ if (trimmed.startsWith("'") && trimmed.endsWith("'") && trimmed.length >= 2) {
378
+ return trimmed.slice(1, -1);
379
+ }
380
+ return trimmed;
381
+ }
382
+ function parseDependsOn(frontmatter) {
383
+ const inlineMatch = frontmatter.match(/^dependsOn:\s*\[\s*\]/m);
384
+ if (inlineMatch) return [];
385
+ const results = [];
386
+ const blockMatch = frontmatter.match(/^dependsOn:\s*\n((?:\s+-\s+.*\n?)*)/m);
387
+ if (blockMatch) {
388
+ const items = blockMatch[1].matchAll(/^\s+-\s+(.+)$/gm);
389
+ for (const item of items) {
390
+ results.push(item[1].trim());
391
+ }
392
+ }
393
+ return results;
394
+ }
395
+ function parseLinks(frontmatter) {
396
+ const inlineMatch = frontmatter.match(/^links:\s*\[\s*\]/m);
397
+ if (inlineMatch) return [];
398
+ const results = [];
399
+ const blockMatch = frontmatter.match(/^links:\s*\n((?:\s+-\s+.*\n?)*)/m);
400
+ if (blockMatch) {
401
+ const items = blockMatch[1].matchAll(/^\s+-\s+(.+)$/gm);
402
+ for (const item of items) {
403
+ results.push(item[1].trim());
404
+ }
405
+ }
406
+ return results;
407
+ }
408
+ function parseExternalIds(frontmatter) {
409
+ const inlineMatch = frontmatter.match(/^externalIds:\s*\[\s*\]/m);
410
+ if (inlineMatch) return [];
411
+ const results = [];
412
+ const blockMatch = frontmatter.match(
413
+ /^externalIds:\s*\n((?:\s+-\s+[\s\S]*?)(?=^\w|\n---))/m
414
+ );
415
+ if (!blockMatch) return [];
416
+ const itemBlocks = blockMatch[1].split(/\n\s+-\s+/).filter(Boolean);
417
+ for (const block of itemBlocks) {
418
+ const lines = block.split("\n");
419
+ const entry = {};
420
+ for (const line of lines) {
421
+ const colonIdx = line.indexOf(":");
422
+ if (colonIdx < 0) continue;
423
+ const key = line.slice(0, colonIdx).trim().replace(/^-\s+/, "");
424
+ if (!key) continue;
425
+ entry[key] = parseSimpleValue(line.slice(colonIdx + 1));
426
+ }
427
+ if (entry["system"] && entry["id"]) {
428
+ results.push({
429
+ system: entry["system"],
430
+ id: entry["id"],
431
+ url: entry["url"] || null
432
+ });
433
+ }
434
+ }
435
+ return results;
436
+ }
437
+ function parseStatusHistory(frontmatter) {
438
+ if (/^statusHistory:\s*\[\s*\]/m.test(frontmatter)) return [];
439
+ const headerMatch = frontmatter.match(/^statusHistory:\s*$/m);
440
+ if (!headerMatch) return [];
441
+ const headerStart = headerMatch.index ?? frontmatter.indexOf(headerMatch[0]);
442
+ const bodyStart = headerStart + headerMatch[0].length + 1;
443
+ const after = frontmatter.slice(bodyStart);
444
+ const bodyLines = [];
445
+ for (const line of after.split("\n")) {
446
+ if (line.length === 0) {
447
+ bodyLines.push(line);
448
+ continue;
449
+ }
450
+ if (line[0] !== " " && line[0] !== " ") break;
451
+ bodyLines.push(line);
452
+ }
453
+ const body = bodyLines.join("\n");
454
+ const results = [];
455
+ const itemBlocks = body.split(/\n\s+-\s+/).filter((b) => b.trim().length > 0);
456
+ for (const block of itemBlocks) {
457
+ const entry = {};
458
+ for (const line of block.split("\n")) {
459
+ const colonIdx = line.indexOf(":");
460
+ if (colonIdx < 0) continue;
461
+ const key = line.slice(0, colonIdx).trim().replace(/^-\s+/, "");
462
+ if (!key) continue;
463
+ entry[key] = parseSimpleValue(line.slice(colonIdx + 1));
464
+ }
465
+ if (!entry["to"]) continue;
466
+ const result = {
467
+ at: entry["at"] ?? "",
468
+ from: entry["from"] ?? null,
469
+ to: entry["to"],
470
+ command: entry["command"] ?? "",
471
+ by: entry["by"] ?? null
472
+ };
473
+ if (entry["reason"] != null) result.reason = entry["reason"];
474
+ if ("phaseFrom" in entry) result.phaseFrom = entry["phaseFrom"];
475
+ if ("phaseTo" in entry) result.phaseTo = entry["phaseTo"];
476
+ if ("dispositionFrom" in entry) result.dispositionFrom = entry["dispositionFrom"];
477
+ if ("dispositionTo" in entry) result.dispositionTo = entry["dispositionTo"];
478
+ results.push(result);
479
+ }
480
+ return results;
481
+ }
482
+ function parseNestedBlock(frontmatter, header) {
483
+ if (new RegExp(`^${header}:\\s*(null|~)\\s*$`, "m").test(frontmatter)) return null;
484
+ const headerMatch = frontmatter.match(new RegExp(`^${header}:\\s*$`, "m"));
485
+ if (!headerMatch) return null;
486
+ const headerStart = headerMatch.index ?? frontmatter.indexOf(headerMatch[0]);
487
+ const after = frontmatter.slice(headerStart + headerMatch[0].length + 1);
488
+ const out = {};
489
+ for (const line of after.split("\n")) {
490
+ if (line.length === 0) continue;
491
+ if (line[0] !== " " && line[0] !== " ") break;
492
+ const colonIdx = line.indexOf(":");
493
+ if (colonIdx < 0) continue;
494
+ const key = line.slice(0, colonIdx).trim();
495
+ if (!key) continue;
496
+ out[key] = parseSimpleValue(line.slice(colonIdx + 1));
497
+ }
498
+ return Object.keys(out).length > 0 ? out : null;
499
+ }
500
+ function parsePlanApproval(frontmatter) {
501
+ const block = parseNestedBlock(frontmatter, "planApproval");
502
+ if (!block || !block["file"] || !block["digest"]) return null;
503
+ return {
504
+ file: block["file"],
505
+ digest: block["digest"],
506
+ by: block["by"] ?? null,
507
+ at: block["at"] ?? ""
508
+ };
509
+ }
510
+ function parseOverride(frontmatter) {
511
+ const block = parseNestedBlock(frontmatter, "override");
512
+ if (!block || !block["status"]) return null;
513
+ return {
514
+ status: block["status"],
515
+ source: block["source"] ?? "human",
516
+ reason: block["reason"] ?? null,
517
+ at: block["at"] ?? ""
518
+ };
519
+ }
520
+ function parseFactsMap(frontmatter) {
521
+ const block = parseNestedBlock(frontmatter, "facts");
522
+ if (!block) return {};
523
+ const out = {};
524
+ for (const [k, v] of Object.entries(block)) {
525
+ if (v === null) continue;
526
+ out[k] = v;
527
+ }
528
+ return out;
529
+ }
530
+ function parseAttestations(frontmatter) {
531
+ if (/^attestations:\s*\[\s*\]/m.test(frontmatter)) return [];
532
+ const headerMatch = frontmatter.match(/^attestations:\s*$/m);
533
+ if (!headerMatch) return [];
534
+ const headerStart = headerMatch.index ?? frontmatter.indexOf(headerMatch[0]);
535
+ const bodyStart = headerStart + headerMatch[0].length + 1;
536
+ const after = frontmatter.slice(bodyStart);
537
+ const bodyLines = [];
538
+ for (const line of after.split("\n")) {
539
+ if (line.length === 0) {
540
+ bodyLines.push(line);
541
+ continue;
542
+ }
543
+ if (line[0] !== " " && line[0] !== " ") break;
544
+ bodyLines.push(line);
545
+ }
546
+ const body = bodyLines.join("\n");
547
+ const results = [];
548
+ const itemBlocks = body.split(/\n\s+-\s+/).filter((b) => b.trim().length > 0);
549
+ for (const block of itemBlocks) {
550
+ const entry = {};
551
+ for (const line of block.split("\n")) {
552
+ const colonIdx = line.indexOf(":");
553
+ if (colonIdx < 0) continue;
554
+ const key = line.slice(0, colonIdx).trim().replace(/^-\s+/, "");
555
+ if (!key) continue;
556
+ entry[key] = parseSimpleValue(line.slice(colonIdx + 1));
557
+ }
558
+ const verdict = entry["verdict"];
559
+ if (!entry["fact"] || !entry["actor"] || !verdict || !entry["at"]) continue;
560
+ if (verdict !== "approved" && verdict !== "changes-requested") continue;
561
+ const record = {
562
+ fact: entry["fact"],
563
+ actor: entry["actor"],
564
+ verdict,
565
+ at: entry["at"]
566
+ };
567
+ if (entry["note"] != null) record.note = entry["note"];
568
+ if (entry["file"] != null) record.file = entry["file"];
569
+ if (entry["digest"] != null) record.digest = entry["digest"];
570
+ if (entry["commit"] != null) record.commit = entry["commit"];
571
+ results.push(record);
572
+ }
573
+ return results;
574
+ }
575
+ function parseWorkspace(frontmatter) {
576
+ const defaults = {
577
+ repository: null,
578
+ worktreePath: null,
579
+ branch: null,
580
+ parentBranch: null
581
+ };
582
+ const fields = ["repository", "worktreePath", "branch", "parentBranch"];
583
+ for (const field of fields) {
584
+ const match = frontmatter.match(new RegExp(`^\\s+${field}:\\s*(.*)$`, "m"));
585
+ if (match) {
586
+ defaults[field] = parseSimpleValue(match[1]);
587
+ }
588
+ }
589
+ return defaults;
590
+ }
591
+ function parseTags(frontmatter) {
592
+ const inlineMatch = frontmatter.match(/^tags:\s*\[\s*\]/m);
593
+ if (inlineMatch) return [];
594
+ const results = [];
595
+ const blockMatch = frontmatter.match(/^tags:\s*\n((?:\s+-\s+.*\n?)*)/m);
596
+ if (blockMatch) {
597
+ const items = blockMatch[1].matchAll(/^\s+-\s+(.+)$/gm);
598
+ for (const item of items) {
599
+ results.push(item[1].trim());
600
+ }
601
+ }
602
+ return results;
603
+ }
604
+ function parseAssignmentFrontmatter(fileContent) {
605
+ const [frontmatter] = extractFrontmatter(fileContent);
606
+ function getField2(key) {
607
+ const match = frontmatter.match(new RegExp(`^${key}:\\s*(.*)$`, "m"));
608
+ if (!match) return null;
609
+ return parseSimpleValue(match[1]);
610
+ }
611
+ return {
612
+ id: getField2("id") ?? "",
613
+ slug: getField2("slug") ?? "",
614
+ title: getField2("title") ?? "",
615
+ project: getField2("project"),
616
+ type: getField2("type"),
617
+ status: getField2("status") ?? "pending",
618
+ priority: getField2("priority") ?? "medium",
619
+ created: getField2("created") ?? "",
620
+ updated: getField2("updated") ?? "",
621
+ assignee: getField2("assignee"),
622
+ externalIds: parseExternalIds(frontmatter),
623
+ statusHistory: parseStatusHistory(frontmatter),
624
+ dependsOn: parseDependsOn(frontmatter),
625
+ links: parseLinks(frontmatter),
626
+ blockedReason: getField2("blockedReason"),
627
+ workspace: parseWorkspace(frontmatter),
628
+ tags: parseTags(frontmatter),
629
+ archived: getField2("archived") === "true",
630
+ archivedAt: getField2("archivedAt"),
631
+ archivedReason: getField2("archivedReason"),
632
+ phase: getField2("phase"),
633
+ disposition: getField2("disposition"),
634
+ planApproval: parsePlanApproval(frontmatter),
635
+ parked: getField2("parked") === "true",
636
+ reviewRequested: getField2("reviewRequested") === "true",
637
+ reworkRequested: getField2("reworkRequested") === "true",
638
+ implementationStarted: getField2("implementationStarted") === "true",
639
+ override: parseOverride(frontmatter),
640
+ facts: parseFactsMap(frontmatter),
641
+ attestations: parseAttestations(frontmatter)
642
+ };
643
+ }
362
644
  var init_frontmatter = __esm({
363
645
  "src/lifecycle/frontmatter.ts"() {
364
646
  "use strict";
@@ -393,7 +675,7 @@ var init_event_emit = __esm({
393
675
  });
394
676
 
395
677
  // src/dashboard/parser.ts
396
- function extractFrontmatter(fileContent) {
678
+ function extractFrontmatter2(fileContent) {
397
679
  const match = fileContent.match(/^---\n([\s\S]*?)\n---/);
398
680
  if (!match) {
399
681
  return ["", fileContent];
@@ -402,7 +684,7 @@ function extractFrontmatter(fileContent) {
402
684
  const body = fileContent.slice(match[0].length).trim();
403
685
  return [frontmatterBlock, body];
404
686
  }
405
- function parseSimpleValue(raw) {
687
+ function parseSimpleValue2(raw) {
406
688
  const trimmed = raw.trim();
407
689
  if (trimmed === "null" || trimmed === "~" || trimmed === "") return null;
408
690
  if (trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2) {
@@ -416,7 +698,7 @@ function parseSimpleValue(raw) {
416
698
  function getField(frontmatter, key) {
417
699
  const match = frontmatter.match(new RegExp(`^${key}:\\s*(.*)$`, "m"));
418
700
  if (!match) return null;
419
- return parseSimpleValue(match[1]);
701
+ return parseSimpleValue2(match[1]);
420
702
  }
421
703
  function getNestedField(frontmatter, parent, key) {
422
704
  const parentRegex = new RegExp(`^${parent}:\\s*\\n((?:\\s+.*\\n?)*)`, "m");
@@ -425,7 +707,7 @@ function getNestedField(frontmatter, parent, key) {
425
707
  const block = parentMatch[1];
426
708
  const fieldMatch = block.match(new RegExp(`^\\s+${key}:\\s*(.*)$`, "m"));
427
709
  if (!fieldMatch) return null;
428
- return parseSimpleValue(fieldMatch[1]);
710
+ return parseSimpleValue2(fieldMatch[1]);
429
711
  }
430
712
  function parseListField(frontmatter, fieldName) {
431
713
  const inlineMatch = frontmatter.match(new RegExp(`^${fieldName}:\\s*\\[\\s*\\]`, "m"));
@@ -450,7 +732,7 @@ function unquoteYamlString(value) {
450
732
  return value;
451
733
  }
452
734
  function parseProject(fileContent) {
453
- const [fm, body] = extractFrontmatter(fileContent);
735
+ const [fm, body] = extractFrontmatter2(fileContent);
454
736
  const slug = getField(fm, "slug") ?? getField(fm, "mission") ?? "";
455
737
  return {
456
738
  id: getField(fm, "id") ?? "",
@@ -465,12 +747,12 @@ function parseProject(fileContent) {
465
747
  tags: parseListField(fm, "tags"),
466
748
  workspace: getField(fm, "workspace"),
467
749
  repositories: parseListField(fm, "repositories").map(unquoteYamlString),
468
- externalIds: parseExternalIds(fm),
750
+ externalIds: parseExternalIds2(fm),
469
751
  body
470
752
  };
471
753
  }
472
754
  function parseStatus(fileContent) {
473
- const [fm, body] = extractFrontmatter(fileContent);
755
+ const [fm, body] = extractFrontmatter2(fileContent);
474
756
  const progress = { total: 0 };
475
757
  const progressMatch = fm.match(/^progress:\s*\n((?:\s+.*\n?)*)/m);
476
758
  if (progressMatch) {
@@ -494,7 +776,7 @@ function parseStatus(fileContent) {
494
776
  body
495
777
  };
496
778
  }
497
- function parseExternalIds(frontmatter) {
779
+ function parseExternalIds2(frontmatter) {
498
780
  const inlineMatch = frontmatter.match(/^externalIds:\s*\[\s*\]/m);
499
781
  if (inlineMatch) return [];
500
782
  const results = [];
@@ -511,7 +793,7 @@ function parseExternalIds(frontmatter) {
511
793
  if (colonIdx < 0) continue;
512
794
  const key = line.slice(0, colonIdx).trim().replace(/^-\s+/, "");
513
795
  if (!key) continue;
514
- entry[key] = parseSimpleValue(line.slice(colonIdx + 1));
796
+ entry[key] = parseSimpleValue2(line.slice(colonIdx + 1));
515
797
  }
516
798
  if (entry["system"] && entry["id"]) {
517
799
  results.push({
@@ -523,7 +805,7 @@ function parseExternalIds(frontmatter) {
523
805
  }
524
806
  return results;
525
807
  }
526
- function parseStatusHistory(frontmatter) {
808
+ function parseStatusHistory2(frontmatter) {
527
809
  if (/^statusHistory:\s*\[\s*\]/m.test(frontmatter)) return [];
528
810
  const headerMatch = frontmatter.match(/^statusHistory:\s*$/m);
529
811
  if (!headerMatch) return [];
@@ -549,7 +831,7 @@ function parseStatusHistory(frontmatter) {
549
831
  if (colonIdx < 0) continue;
550
832
  const key = line.slice(0, colonIdx).trim().replace(/^-\s+/, "");
551
833
  if (!key) continue;
552
- entry[key] = parseSimpleValue(line.slice(colonIdx + 1));
834
+ entry[key] = parseSimpleValue2(line.slice(colonIdx + 1));
553
835
  }
554
836
  if (!entry["to"]) continue;
555
837
  const result = {
@@ -568,7 +850,7 @@ function parseStatusHistory(frontmatter) {
568
850
  }
569
851
  return results;
570
852
  }
571
- function parseFactsMap(frontmatter) {
853
+ function parseFactsMap2(frontmatter) {
572
854
  const headerMatch = frontmatter.match(/^facts:\s*$/m);
573
855
  if (!headerMatch) return {};
574
856
  const headerStart = headerMatch.index ?? frontmatter.indexOf(headerMatch[0]);
@@ -581,13 +863,13 @@ function parseFactsMap(frontmatter) {
581
863
  if (colonIdx < 0) continue;
582
864
  const key = line.slice(0, colonIdx).trim();
583
865
  if (!key) continue;
584
- const value = parseSimpleValue(line.slice(colonIdx + 1));
866
+ const value = parseSimpleValue2(line.slice(colonIdx + 1));
585
867
  if (value === null) continue;
586
868
  out[key] = value;
587
869
  }
588
870
  return out;
589
871
  }
590
- function parseAttestations(frontmatter) {
872
+ function parseAttestations2(frontmatter) {
591
873
  if (/^attestations:\s*\[\s*\]/m.test(frontmatter)) return [];
592
874
  const headerMatch = frontmatter.match(/^attestations:\s*$/m);
593
875
  if (!headerMatch) return [];
@@ -613,7 +895,7 @@ function parseAttestations(frontmatter) {
613
895
  if (colonIdx < 0) continue;
614
896
  const key = line.slice(0, colonIdx).trim().replace(/^-\s+/, "");
615
897
  if (!key) continue;
616
- entry[key] = parseSimpleValue(line.slice(colonIdx + 1));
898
+ entry[key] = parseSimpleValue2(line.slice(colonIdx + 1));
617
899
  }
618
900
  const verdict = entry["verdict"];
619
901
  if (!entry["fact"] || !entry["actor"] || !verdict || !entry["at"]) continue;
@@ -633,7 +915,7 @@ function parseAttestations(frontmatter) {
633
915
  return results;
634
916
  }
635
917
  function parseAssignmentFull(fileContent) {
636
- const [fm, body] = extractFrontmatter(fileContent);
918
+ const [fm, body] = extractFrontmatter2(fileContent);
637
919
  return {
638
920
  id: getField(fm, "id") ?? "",
639
921
  slug: getField(fm, "slug") ?? "",
@@ -653,8 +935,8 @@ function parseAssignmentFull(fileContent) {
653
935
  branch: getNestedField(fm, "workspace", "branch"),
654
936
  parentBranch: getNestedField(fm, "workspace", "parentBranch")
655
937
  },
656
- externalIds: parseExternalIds(fm),
657
- statusHistory: parseStatusHistory(fm),
938
+ externalIds: parseExternalIds2(fm),
939
+ statusHistory: parseStatusHistory2(fm),
658
940
  tags: parseListField(fm, "tags"),
659
941
  archived: getField(fm, "archived") === "true",
660
942
  archivedAt: getField(fm, "archivedAt"),
@@ -666,6 +948,7 @@ function parseAssignmentFull(fileContent) {
666
948
  disposition: getField(fm, "disposition"),
667
949
  parked: getField(fm, "parked") === "true",
668
950
  reviewRequested: getField(fm, "reviewRequested") === "true",
951
+ reworkRequested: getField(fm, "reworkRequested") === "true",
669
952
  implementationStarted: getField(fm, "implementationStarted") === "true",
670
953
  planApproval: (() => {
671
954
  const file = getNestedField(fm, "planApproval", "file");
@@ -688,12 +971,12 @@ function parseAssignmentFull(fileContent) {
688
971
  at: getNestedField(fm, "override", "at") ?? ""
689
972
  };
690
973
  })(),
691
- facts: parseFactsMap(fm),
692
- attestations: parseAttestations(fm)
974
+ facts: parseFactsMap2(fm),
975
+ attestations: parseAttestations2(fm)
693
976
  };
694
977
  }
695
978
  function parsePlan(fileContent) {
696
- const [fm, body] = extractFrontmatter(fileContent);
979
+ const [fm, body] = extractFrontmatter2(fileContent);
697
980
  return {
698
981
  assignment: getField(fm, "assignment") ?? "",
699
982
  status: getField(fm, "status") ?? "",
@@ -703,7 +986,7 @@ function parsePlan(fileContent) {
703
986
  };
704
987
  }
705
988
  function parseScratchpad(fileContent) {
706
- const [fm, body] = extractFrontmatter(fileContent);
989
+ const [fm, body] = extractFrontmatter2(fileContent);
707
990
  return {
708
991
  assignment: getField(fm, "assignment") ?? "",
709
992
  updated: getField(fm, "updated") ?? "",
@@ -711,7 +994,7 @@ function parseScratchpad(fileContent) {
711
994
  };
712
995
  }
713
996
  function parseHandoff(fileContent) {
714
- const [fm, body] = extractFrontmatter(fileContent);
997
+ const [fm, body] = extractFrontmatter2(fileContent);
715
998
  return {
716
999
  assignment: getField(fm, "assignment") ?? "",
717
1000
  handoffCount: parseInt(getField(fm, "handoffCount") ?? "0", 10),
@@ -720,7 +1003,7 @@ function parseHandoff(fileContent) {
720
1003
  };
721
1004
  }
722
1005
  function parseDecisionRecord(fileContent) {
723
- const [fm, body] = extractFrontmatter(fileContent);
1006
+ const [fm, body] = extractFrontmatter2(fileContent);
724
1007
  return {
725
1008
  assignment: getField(fm, "assignment") ?? "",
726
1009
  decisionCount: parseInt(getField(fm, "decisionCount") ?? "0", 10),
@@ -729,7 +1012,7 @@ function parseDecisionRecord(fileContent) {
729
1012
  };
730
1013
  }
731
1014
  function parseComments(fileContent) {
732
- const [fm, body] = extractFrontmatter(fileContent);
1015
+ const [fm, body] = extractFrontmatter2(fileContent);
733
1016
  const entries = [];
734
1017
  const sections = body.split(
735
1018
  /^## (?=[^\n]*\n\s*\*\*Recorded:\*\*[^\n]*\n\*\*Author:\*\*[^\n]*\n\*\*Type:\*\*\s*(?:question|note|feedback)\b)/m
@@ -764,7 +1047,7 @@ function parseComments(fileContent) {
764
1047
  };
765
1048
  }
766
1049
  function parseProgress(fileContent) {
767
- const [fm, body] = extractFrontmatter(fileContent);
1050
+ const [fm, body] = extractFrontmatter2(fileContent);
768
1051
  const entries = [];
769
1052
  const sections = body.split(/^## /m).slice(1);
770
1053
  for (const section of sections) {
@@ -783,7 +1066,7 @@ function parseProgress(fileContent) {
783
1066
  };
784
1067
  }
785
1068
  function parsePlaybook(fileContent) {
786
- const [fm, body] = extractFrontmatter(fileContent);
1069
+ const [fm, body] = extractFrontmatter2(fileContent);
787
1070
  return {
788
1071
  slug: getField(fm, "slug") ?? "",
789
1072
  name: getField(fm, "name") ?? "",
@@ -1145,7 +1428,9 @@ var init_derive_config = __esm({
1145
1428
  },
1146
1429
  {
1147
1430
  phase: "review",
1148
- when: "acAllChecked:true OR reviewRequested:true",
1431
+ // Rework (a new `implement` stage after `review`) must leave review even
1432
+ // when ACs stay checked — hence `AND NOT reworkRequested` (stage-fact-status-bridge).
1433
+ when: "(acAllChecked:true OR reviewRequested:true) AND NOT reworkRequested:true",
1149
1434
  next: "Complete, or address review feedback"
1150
1435
  }
1151
1436
  ],
@@ -1212,6 +1497,7 @@ var init_fields = __esm({
1212
1497
  blocked: { kind: "bool" },
1213
1498
  parked: { kind: "bool" },
1214
1499
  reviewrequested: { kind: "bool", get: (i) => i["reviewRequested"] },
1500
+ reworkrequested: { kind: "bool", get: (i) => i["reworkRequested"] },
1215
1501
  pinned: { kind: "bool" }
1216
1502
  };
1217
1503
  }
@@ -1991,6 +2277,7 @@ function queryFieldNames(declarations) {
1991
2277
  "blocked",
1992
2278
  "parked",
1993
2279
  "reviewRequested",
2280
+ "reworkRequested",
1994
2281
  "pinned"
1995
2282
  ];
1996
2283
  const custom = [];
@@ -2029,6 +2316,7 @@ var init_fact_registry = __esm({
2029
2316
  blocked: { kind: "bool" },
2030
2317
  parked: { kind: "bool" },
2031
2318
  reviewrequested: { kind: "bool", get: (i) => i["reviewRequested"] },
2319
+ reworkrequested: { kind: "bool", get: (i) => i["reworkRequested"] },
2032
2320
  pinned: { kind: "bool" }
2033
2321
  };
2034
2322
  }
@@ -3951,7 +4239,7 @@ async function resolveAssignmentById(projectsDir, assignmentsDir, id) {
3951
4239
  let workspaceGroup = null;
3952
4240
  try {
3953
4241
  const content = await readFile6(standalonePath, "utf-8");
3954
- const [fm] = extractFrontmatter(content);
4242
+ const [fm] = extractFrontmatter2(content);
3955
4243
  workspaceGroup = getField(fm, "workspaceGroup");
3956
4244
  } catch {
3957
4245
  }
@@ -3979,7 +4267,7 @@ async function resolveAssignmentById(projectsDir, assignmentsDir, id) {
3979
4267
  if (!await fileExists(aPath)) continue;
3980
4268
  try {
3981
4269
  const content = await readFile6(aPath, "utf-8");
3982
- const [fm] = extractFrontmatter(content);
4270
+ const [fm] = extractFrontmatter2(content);
3983
4271
  const fileId = getField(fm, "id");
3984
4272
  if (fileId === id) {
3985
4273
  projectMatch = {
@@ -4063,7 +4351,7 @@ function compiledWhen(registry, when) {
4063
4351
  return pred;
4064
4352
  }
4065
4353
  function deriveDimensions(input) {
4066
- const { facts, derive, currentStatus, terminalStatuses, knownStatusIds, override } = input;
4354
+ const { facts, derive, currentStatus, terminalStatuses, knownStatusIds, override: override2 } = input;
4067
4355
  const registry = input.registry ?? DERIVE_FIELDS;
4068
4356
  if (terminalStatuses.has(currentStatus)) return null;
4069
4357
  const ctx = { now: 0 };
@@ -4097,8 +4385,8 @@ function deriveDimensions(input) {
4097
4385
  derivedStatus = phase;
4098
4386
  }
4099
4387
  let status = derivedStatus;
4100
- if (override && override.status && !terminalStatuses.has(override.status) && knownStatusIds.has(override.status)) {
4101
- status = override.status;
4388
+ if (override2 && override2.status && !terminalStatuses.has(override2.status) && knownStatusIds.has(override2.status)) {
4389
+ status = override2.status;
4102
4390
  }
4103
4391
  return { phase, disposition, derivedStatus, status, nextAction };
4104
4392
  }
@@ -4322,8 +4610,7 @@ async function areDependenciesSatisfied(projectDir, dependsOn, terminalStatuses)
4322
4610
  if (!await fileExists(depPath)) return false;
4323
4611
  try {
4324
4612
  const content = await readFile9(depPath, "utf-8");
4325
- const m = content.match(/^status:\s*(.+)$/m);
4326
- const status = m ? m[1].trim() : "";
4613
+ const { status } = parseAssignmentFrontmatter(content);
4327
4614
  if (!terminalStatuses.has(status)) return false;
4328
4615
  } catch {
4329
4616
  return false;
@@ -4390,6 +4677,7 @@ async function computeFactsDetailed(input) {
4390
4677
  blocked: frontmatter.blockedReason !== null,
4391
4678
  parked: frontmatter.parked,
4392
4679
  reviewRequested: frontmatter.reviewRequested,
4680
+ reworkRequested: frontmatter.reworkRequested,
4393
4681
  pinned: frontmatter.override !== null
4394
4682
  };
4395
4683
  const attestations = [];
@@ -4438,6 +4726,7 @@ var init_facts = __esm({
4438
4726
  init_fs();
4439
4727
  init_git_worktree();
4440
4728
  init_derive();
4729
+ init_frontmatter();
4441
4730
  HTML_COMMENT_RE = /<!--[\s\S]*?-->/g;
4442
4731
  PLAN_FILE_RE = /^plan(?:-v(\d+))?\.md$/;
4443
4732
  }
@@ -4700,6 +4989,139 @@ var init_help = __esm({
4700
4989
  }
4701
4990
  });
4702
4991
 
4992
+ // src/db/engagement-schema.ts
4993
+ var ENGAGEMENT_SCHEMA_VERSION, ENGAGEMENT_DDL;
4994
+ var init_engagement_schema = __esm({
4995
+ "src/db/engagement-schema.ts"() {
4996
+ "use strict";
4997
+ ENGAGEMENT_SCHEMA_VERSION = "1";
4998
+ ENGAGEMENT_DDL = `
4999
+ CREATE TABLE IF NOT EXISTS engagement (
5000
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
5001
+ session_id TEXT NOT NULL,
5002
+ assignment_id TEXT,
5003
+ project_slug TEXT,
5004
+ assignment_slug TEXT,
5005
+ stage TEXT NOT NULL DEFAULT 'implement',
5006
+ started_at TEXT NOT NULL,
5007
+ ended_at TEXT,
5008
+ tokens_at_open TEXT,
5009
+ tokens_at_close TEXT,
5010
+ close_reason TEXT
5011
+ );
5012
+ CREATE UNIQUE INDEX IF NOT EXISTS one_active_per_session
5013
+ ON engagement(session_id) WHERE ended_at IS NULL;
5014
+ CREATE INDEX IF NOT EXISTS idx_engagement_session ON engagement(session_id);
5015
+ CREATE INDEX IF NOT EXISTS idx_engagement_assignment ON engagement(assignment_id);
5016
+ CREATE INDEX IF NOT EXISTS idx_engagement_slug ON engagement(project_slug, assignment_slug);
5017
+ `;
5018
+ }
5019
+ });
5020
+
5021
+ // src/db/engagement-backfill.ts
5022
+ import { readdirSync, readFileSync, statSync as statSync2 } from "fs";
5023
+ import { resolve as resolve13 } from "path";
5024
+ function safeReaddir(dir) {
5025
+ try {
5026
+ return readdirSync(dir, { withFileTypes: true });
5027
+ } catch {
5028
+ return [];
5029
+ }
5030
+ }
5031
+ function readAssignmentId(assignmentMdPath) {
5032
+ try {
5033
+ const fm = parseAssignmentFrontmatter(readFileSync(assignmentMdPath, "utf-8"));
5034
+ return fm.id || null;
5035
+ } catch {
5036
+ return null;
5037
+ }
5038
+ }
5039
+ function transcriptMtimeIso(path) {
5040
+ if (!path) return null;
5041
+ try {
5042
+ return statSync2(path).mtime.toISOString();
5043
+ } catch {
5044
+ return null;
5045
+ }
5046
+ }
5047
+ function projectKey(projectSlug, assignmentSlug) {
5048
+ return JSON.stringify([projectSlug, assignmentSlug]);
5049
+ }
5050
+ function buildSlugIdMaps() {
5051
+ const root = syntaurRoot();
5052
+ const project = /* @__PURE__ */ new Map();
5053
+ const standalone = /* @__PURE__ */ new Map();
5054
+ const projectsDir = resolve13(root, "projects");
5055
+ for (const proj of safeReaddir(projectsDir)) {
5056
+ if (!proj.isDirectory()) continue;
5057
+ const asgRoot = resolve13(projectsDir, proj.name, "assignments");
5058
+ for (const asg of safeReaddir(asgRoot)) {
5059
+ if (!asg.isDirectory()) continue;
5060
+ const id = readAssignmentId(resolve13(asgRoot, asg.name, "assignment.md"));
5061
+ if (id) project.set(projectKey(proj.name, asg.name), id);
5062
+ }
5063
+ }
5064
+ const standaloneDir = resolve13(root, "assignments");
5065
+ for (const asg of safeReaddir(standaloneDir)) {
5066
+ if (!asg.isDirectory()) continue;
5067
+ const id = readAssignmentId(resolve13(standaloneDir, asg.name, "assignment.md"));
5068
+ if (id) standalone.set(asg.name, id);
5069
+ }
5070
+ return { project, standalone };
5071
+ }
5072
+ function backfillEngagements(db2) {
5073
+ const { project, standalone } = buildSlugIdMaps();
5074
+ const sessions = db2.prepare(
5075
+ `SELECT session_id, project_slug, assignment_slug, status, started, ended,
5076
+ transcript_path, updated_at
5077
+ FROM sessions`
5078
+ ).all();
5079
+ const insert = db2.prepare(
5080
+ `INSERT INTO engagement
5081
+ (session_id, assignment_id, project_slug, assignment_slug, stage,
5082
+ started_at, ended_at, tokens_at_open, tokens_at_close, close_reason)
5083
+ VALUES
5084
+ (@session_id, @assignment_id, @project_slug, @assignment_slug, 'implement',
5085
+ @started_at, @ended_at, NULL, NULL, @close_reason)`
5086
+ );
5087
+ const counts = { backfilled: 0, attributed: 0, unattributed: 0 };
5088
+ for (const s of sessions) {
5089
+ let assignmentId = null;
5090
+ if (s.project_slug && s.assignment_slug) {
5091
+ assignmentId = project.get(projectKey(s.project_slug, s.assignment_slug)) ?? null;
5092
+ } else if (!s.project_slug && s.assignment_slug) {
5093
+ assignmentId = standalone.get(s.assignment_slug) ?? null;
5094
+ }
5095
+ const isActive = s.status === "active";
5096
+ let endedAt = null;
5097
+ let closeReason = null;
5098
+ if (!isActive) {
5099
+ endedAt = s.ended ?? transcriptMtimeIso(s.transcript_path) ?? s.updated_at ?? s.started;
5100
+ closeReason = s.status === "completed" ? "completed" : "abandoned";
5101
+ }
5102
+ insert.run({
5103
+ session_id: s.session_id,
5104
+ assignment_id: assignmentId,
5105
+ project_slug: s.project_slug,
5106
+ assignment_slug: s.assignment_slug,
5107
+ started_at: s.started,
5108
+ ended_at: endedAt,
5109
+ close_reason: closeReason
5110
+ });
5111
+ counts.backfilled++;
5112
+ if (assignmentId) counts.attributed++;
5113
+ else counts.unattributed++;
5114
+ }
5115
+ return counts;
5116
+ }
5117
+ var init_engagement_backfill = __esm({
5118
+ "src/db/engagement-backfill.ts"() {
5119
+ "use strict";
5120
+ init_paths();
5121
+ init_frontmatter();
5122
+ }
5123
+ });
5124
+
4703
5125
  // src/dashboard/session-db.ts
4704
5126
  var session_db_exports = {};
4705
5127
  __export(session_db_exports, {
@@ -4711,18 +5133,23 @@ __export(session_db_exports, {
4711
5133
  resetSessionDb: () => resetSessionDb
4712
5134
  });
4713
5135
  import Database2 from "better-sqlite3";
4714
- import { resolve as resolve13 } from "path";
5136
+ import { resolve as resolve14 } from "path";
4715
5137
  import { readdir as readdir8 } from "fs/promises";
4716
5138
  function initSessionDb(dbPath) {
4717
5139
  if (db) return db;
4718
- const finalPath = dbPath ?? resolve13(syntaurRoot(), "syntaur.db");
5140
+ const finalPath = dbPath ?? resolve14(syntaurRoot(), "syntaur.db");
4719
5141
  db = new Database2(finalPath);
4720
5142
  db.pragma("journal_mode = WAL");
4721
5143
  db.exec(SCHEMA_SQL);
5144
+ db.exec(ENGAGEMENT_DDL);
4722
5145
  db.prepare("INSERT OR IGNORE INTO meta (key, value) VALUES (?, ?)").run(
4723
5146
  "schema_version",
4724
5147
  SCHEMA_VERSION
4725
5148
  );
5149
+ db.prepare("INSERT OR IGNORE INTO meta (key, value) VALUES (?, ?)").run(
5150
+ "engagement_schema_version",
5151
+ ENGAGEMENT_SCHEMA_VERSION
5152
+ );
4726
5153
  const database = db;
4727
5154
  const runMigrations = database.transaction(() => {
4728
5155
  const vBeforeV2 = database.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get()?.value;
@@ -4849,9 +5276,43 @@ function initSessionDb(dbPath) {
4849
5276
  UPDATE meta SET value = '5' WHERE key = 'schema_version';
4850
5277
  `);
4851
5278
  }
5279
+ const vBeforeV6 = database.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get()?.value;
5280
+ if (vBeforeV6 === "5") {
5281
+ database.exec(ENGAGEMENT_DDL);
5282
+ const counts = backfillEngagements(database);
5283
+ database.exec(`
5284
+ CREATE TABLE sessions_v6 (
5285
+ session_id TEXT PRIMARY KEY,
5286
+ agent TEXT NOT NULL,
5287
+ started TEXT NOT NULL,
5288
+ ended TEXT,
5289
+ status TEXT NOT NULL DEFAULT 'active',
5290
+ path TEXT,
5291
+ description TEXT,
5292
+ transcript_path TEXT,
5293
+ pid INTEGER,
5294
+ pid_started_at TEXT,
5295
+ original_head_sha TEXT,
5296
+ activity TEXT,
5297
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
5298
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
5299
+ );
5300
+ INSERT INTO sessions_v6
5301
+ SELECT session_id, agent, started, ended, status, path, description,
5302
+ transcript_path, pid, pid_started_at, original_head_sha, NULL,
5303
+ created_at, updated_at
5304
+ FROM sessions;
5305
+ DROP TABLE sessions;
5306
+ ALTER TABLE sessions_v6 RENAME TO sessions;
5307
+ CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
5308
+ UPDATE meta SET value = '6' WHERE key = 'schema_version';
5309
+ `);
5310
+ console.log(
5311
+ `engagement backfill: backfilled=${counts.backfilled} attributed=${counts.attributed} unattributed=${counts.unattributed}`
5312
+ );
5313
+ }
4852
5314
  });
4853
5315
  runMigrations.exclusive();
4854
- db.exec(POST_MIGRATION_INDEXES_SQL);
4855
5316
  return db;
4856
5317
  }
4857
5318
  function isSessionDbInitialized() {
@@ -4883,20 +5344,38 @@ async function migrateFromMarkdown(projectsDir) {
4883
5344
  const allSessions = [];
4884
5345
  for (const entry of entries) {
4885
5346
  if (!entry.isDirectory()) continue;
4886
- const projectDir = resolve13(projectsDir, entry.name);
4887
- const indexPath = resolve13(projectDir, "_index-sessions.md");
5347
+ const projectDir = resolve14(projectsDir, entry.name);
5348
+ const indexPath = resolve14(projectDir, "_index-sessions.md");
4888
5349
  if (!await fileExists(indexPath)) continue;
4889
5350
  const sessions = await parseMarkdownSessionsIndex(indexPath, entry.name);
4890
5351
  allSessions.push(...sessions);
4891
5352
  }
4892
5353
  if (allSessions.length === 0) return 0;
4893
5354
  const insert = database.prepare(`
4894
- INSERT OR IGNORE INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path)
4895
- VALUES (?, ?, ?, ?, ?, ?, ?)
5355
+ INSERT OR IGNORE INTO sessions (session_id, agent, started, status, path)
5356
+ VALUES (?, ?, ?, ?, ?)
5357
+ `);
5358
+ const insertEngagement = database.prepare(`
5359
+ INSERT INTO engagement (session_id, project_slug, assignment_slug, stage, started_at, ended_at, close_reason)
5360
+ SELECT @sid, @ps, @as, 'implement', @started, @ended, @reason
5361
+ WHERE NOT EXISTS (
5362
+ SELECT 1 FROM engagement WHERE session_id = @sid AND ended_at IS NULL
5363
+ )
4896
5364
  `);
4897
5365
  const insertAll = database.transaction((sessions) => {
4898
5366
  for (const s of sessions) {
4899
- insert.run(s.sessionId, s.projectSlug, s.assignmentSlug, s.agent, s.started, s.status, s.path);
5367
+ const res = insert.run(s.sessionId, s.agent, s.started, s.status, s.path);
5368
+ if (res.changes > 0 && (s.projectSlug || s.assignmentSlug)) {
5369
+ const terminal = s.status === "completed" || s.status === "stopped";
5370
+ insertEngagement.run({
5371
+ sid: s.sessionId,
5372
+ ps: s.projectSlug ?? null,
5373
+ as: s.assignmentSlug ?? null,
5374
+ started: s.started,
5375
+ ended: terminal ? s.started : null,
5376
+ reason: terminal ? s.status === "completed" ? "completed" : "abandoned" : null
5377
+ });
5378
+ }
4900
5379
  }
4901
5380
  });
4902
5381
  insertAll(allSessions);
@@ -4939,19 +5418,19 @@ async function parseMarkdownSessionsIndex(filePath, projectSlug) {
4939
5418
  }
4940
5419
  return sessions;
4941
5420
  }
4942
- var db, SCHEMA_VERSION, SCHEMA_SQL, POST_MIGRATION_INDEXES_SQL;
5421
+ var db, SCHEMA_VERSION, SCHEMA_SQL;
4943
5422
  var init_session_db = __esm({
4944
5423
  "src/dashboard/session-db.ts"() {
4945
5424
  "use strict";
4946
5425
  init_paths();
4947
5426
  init_fs();
5427
+ init_engagement_schema();
5428
+ init_engagement_backfill();
4948
5429
  db = null;
4949
- SCHEMA_VERSION = "5";
5430
+ SCHEMA_VERSION = "6";
4950
5431
  SCHEMA_SQL = `
4951
5432
  CREATE TABLE IF NOT EXISTS sessions (
4952
5433
  session_id TEXT PRIMARY KEY,
4953
- project_slug TEXT,
4954
- assignment_slug TEXT,
4955
5434
  agent TEXT NOT NULL,
4956
5435
  started TEXT NOT NULL,
4957
5436
  ended TEXT,
@@ -4962,34 +5441,172 @@ CREATE TABLE IF NOT EXISTS sessions (
4962
5441
  pid INTEGER,
4963
5442
  pid_started_at TEXT,
4964
5443
  original_head_sha TEXT,
5444
+ activity TEXT,
4965
5445
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
4966
5446
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
4967
5447
  );
4968
5448
  CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
4969
5449
  CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);
4970
5450
  `;
4971
- POST_MIGRATION_INDEXES_SQL = `
4972
- CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
4973
- CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
4974
- `;
5451
+ }
5452
+ });
5453
+
5454
+ // src/db/engagement-tokens.ts
5455
+ function serializeSnapshot(snapshot) {
5456
+ return snapshot === null ? null : JSON.stringify(snapshot);
5457
+ }
5458
+ function tableExists(name) {
5459
+ const row = getSessionDb().prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(name);
5460
+ return row !== void 0;
5461
+ }
5462
+ function getCumulativeTokenSource() {
5463
+ return override ?? productionCumulativeTokenSource;
5464
+ }
5465
+ var productionCumulativeTokenSource, override;
5466
+ var init_engagement_tokens = __esm({
5467
+ "src/db/engagement-tokens.ts"() {
5468
+ "use strict";
5469
+ init_session_db();
5470
+ productionCumulativeTokenSource = async (sessionId) => {
5471
+ const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
5472
+ const db2 = getSessionDb();
5473
+ const collectorRow = db2.prepare("SELECT value FROM meta WHERE key = 'usage_last_collector_run'").get();
5474
+ const collectorRunAt = collectorRow?.value ?? null;
5475
+ const models = {};
5476
+ if (tableExists("usage_events")) {
5477
+ const rows = db2.prepare(
5478
+ `SELECT model, input_tokens, output_tokens, cache_creation_tokens,
5479
+ cache_read_tokens, total_tokens, total_cost
5480
+ FROM usage_events
5481
+ WHERE session_id = ?`
5482
+ ).all(sessionId);
5483
+ for (const r of rows) {
5484
+ models[r.model] = {
5485
+ input: r.input_tokens,
5486
+ output: r.output_tokens,
5487
+ cacheCreation: r.cache_creation_tokens,
5488
+ cacheRead: r.cache_read_tokens,
5489
+ total: r.total_tokens,
5490
+ cost: r.total_cost
5491
+ };
5492
+ }
5493
+ }
5494
+ return { models, collectorRunAt, capturedAt };
5495
+ };
5496
+ override = null;
5497
+ }
5498
+ });
5499
+
5500
+ // src/db/engagement-db.ts
5501
+ function getOpenEngagement(sessionId) {
5502
+ const row = getSessionDb().prepare(
5503
+ "SELECT * FROM engagement WHERE session_id = ? AND ended_at IS NULL LIMIT 1"
5504
+ ).get(sessionId);
5505
+ return row ?? null;
5506
+ }
5507
+ function getLatestEngagement(sessionId) {
5508
+ const row = getSessionDb().prepare(
5509
+ `SELECT * FROM engagement WHERE session_id = ?
5510
+ ORDER BY (ended_at IS NULL) DESC, started_at DESC, id DESC
5511
+ LIMIT 1`
5512
+ ).get(sessionId);
5513
+ return row ?? null;
5514
+ }
5515
+ function hasAnyEngagement(sessionId) {
5516
+ return getSessionDb().prepare("SELECT 1 FROM engagement WHERE session_id = ? LIMIT 1").get(sessionId) !== void 0;
5517
+ }
5518
+ function insertClosedEngagement(input) {
5519
+ const res = getSessionDb().prepare(
5520
+ `INSERT INTO engagement
5521
+ (session_id, assignment_id, project_slug, assignment_slug, stage, started_at, ended_at, close_reason)
5522
+ VALUES (@sessionId, @assignmentId, @projectSlug, @assignmentSlug, @stage, @startedAt, @endedAt, @closeReason)`
5523
+ ).run({
5524
+ sessionId: input.sessionId,
5525
+ assignmentId: input.assignmentId ?? null,
5526
+ projectSlug: input.projectSlug ?? null,
5527
+ assignmentSlug: input.assignmentSlug ?? null,
5528
+ stage: input.stage ?? DEFAULT_STAGE,
5529
+ startedAt: input.startedAt,
5530
+ endedAt: input.endedAt,
5531
+ closeReason: input.closeReason
5532
+ });
5533
+ return getEngagementById(Number(res.lastInsertRowid));
5534
+ }
5535
+ function getEngagementById(id) {
5536
+ const row = getSessionDb().prepare("SELECT * FROM engagement WHERE id = ?").get(id);
5537
+ if (!row) throw new Error(`engagement ${id} not found after insert`);
5538
+ return row;
5539
+ }
5540
+ function openEngagement(input) {
5541
+ const res = getSessionDb().prepare(
5542
+ `INSERT INTO engagement
5543
+ (session_id, assignment_id, project_slug, assignment_slug, stage, started_at, tokens_at_open)
5544
+ VALUES (@sessionId, @assignmentId, @projectSlug, @assignmentSlug, @stage, @startedAt, @tokensAtOpen)`
5545
+ ).run({
5546
+ sessionId: input.sessionId,
5547
+ assignmentId: input.assignmentId ?? null,
5548
+ projectSlug: input.projectSlug ?? null,
5549
+ assignmentSlug: input.assignmentSlug ?? null,
5550
+ stage: input.stage ?? DEFAULT_STAGE,
5551
+ startedAt: input.startedAt,
5552
+ tokensAtOpen: serializeSnapshot(input.tokensAtOpen ?? null)
5553
+ });
5554
+ return getEngagementById(Number(res.lastInsertRowid));
5555
+ }
5556
+ function closeEngagementById(input) {
5557
+ const res = getSessionDb().prepare(
5558
+ `UPDATE engagement
5559
+ SET ended_at = @endedAt,
5560
+ tokens_at_close = @tokensAtClose,
5561
+ close_reason = @closeReason
5562
+ WHERE id = @id AND started_at = @startedAt AND ended_at IS NULL`
5563
+ ).run({
5564
+ id: input.id,
5565
+ startedAt: input.startedAt,
5566
+ endedAt: input.endedAt,
5567
+ tokensAtClose: serializeSnapshot(input.tokensAtClose ?? null),
5568
+ closeReason: input.closeReason
5569
+ });
5570
+ return res.changes > 0;
5571
+ }
5572
+ function ensureOpenEngagement(input) {
5573
+ if (getOpenEngagement(input.sessionId)) return null;
5574
+ try {
5575
+ return openEngagement(input);
5576
+ } catch (err) {
5577
+ if (err instanceof Error && /UNIQUE constraint failed/.test(err.message)) {
5578
+ return null;
5579
+ }
5580
+ throw err;
5581
+ }
5582
+ }
5583
+ var DEFAULT_STAGE;
5584
+ var init_engagement_db = __esm({
5585
+ "src/db/engagement-db.ts"() {
5586
+ "use strict";
5587
+ init_session_db();
5588
+ init_engagement_tokens();
5589
+ DEFAULT_STAGE = "implement";
4975
5590
  }
4976
5591
  });
4977
5592
 
4978
5593
  // src/dashboard/agent-sessions.ts
4979
5594
  var agent_sessions_exports = {};
4980
5595
  __export(agent_sessions_exports, {
5596
+ SessionResurrectionError: () => SessionResurrectionError,
4981
5597
  appendSession: () => appendSession,
4982
5598
  deleteSessions: () => deleteSessions,
4983
5599
  getSessionById: () => getSessionById,
4984
5600
  listAllSessions: () => listAllSessions,
4985
5601
  listProjectSessions: () => listProjectSessions,
4986
5602
  listSessionsByAssignment: () => listSessionsByAssignment,
5603
+ livenessStopSession: () => livenessStopSession,
4987
5604
  parseSessionsIndex: () => parseSessionsIndex,
4988
5605
  reconcileActiveSessions: () => reconcileActiveSessions,
4989
5606
  updateSessionStatus: () => updateSessionStatus
4990
5607
  });
4991
5608
  import { readFile as readFile11 } from "fs/promises";
4992
- import { resolve as resolve14 } from "path";
5609
+ import { resolve as resolve15 } from "path";
4993
5610
  function rowToSession(row) {
4994
5611
  return {
4995
5612
  sessionId: row.session_id,
@@ -5005,28 +5622,64 @@ function rowToSession(row) {
5005
5622
  pid: row.pid ?? null,
5006
5623
  pidStartedAt: row.pid_started_at ?? null,
5007
5624
  originalHeadSha: row.original_head_sha ?? null,
5625
+ activity: row.activity ?? null,
5008
5626
  updatedAt: row.updated_at ?? null
5009
5627
  };
5010
5628
  }
5011
5629
  async function parseSessionsIndex(_projectDir, projectSlug) {
5012
5630
  const db2 = getSessionDb();
5013
- const rows = db2.prepare("SELECT * FROM sessions WHERE project_slug = ? ORDER BY started DESC").all(projectSlug);
5631
+ const rows = db2.prepare(`${SESSION_SELECT_WITH_BINDING} WHERE e.project_slug = ? ORDER BY s.started DESC`).all(projectSlug);
5014
5632
  return rows.map(rowToSession);
5015
5633
  }
5634
+ function reopenEngagementIfMissing(sessionId, freshBinding, freshStartedAt, tokensAtOpen) {
5635
+ if (getOpenEngagement(sessionId)) return;
5636
+ if (freshBinding && (freshBinding.projectSlug || freshBinding.assignmentSlug)) {
5637
+ ensureOpenEngagement({
5638
+ sessionId,
5639
+ assignmentId: freshBinding.assignmentId ?? null,
5640
+ projectSlug: freshBinding.projectSlug,
5641
+ assignmentSlug: freshBinding.assignmentSlug,
5642
+ stage: "implement",
5643
+ startedAt: freshStartedAt,
5644
+ tokensAtOpen: tokensAtOpen ?? null
5645
+ });
5646
+ return;
5647
+ }
5648
+ const latest = getLatestEngagement(sessionId);
5649
+ if (latest && (latest.project_slug || latest.assignment_slug)) {
5650
+ ensureOpenEngagement({
5651
+ sessionId,
5652
+ assignmentId: latest.assignment_id,
5653
+ projectSlug: latest.project_slug,
5654
+ assignmentSlug: latest.assignment_slug,
5655
+ stage: "implement",
5656
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5657
+ tokensAtOpen: tokensAtOpen ?? null
5658
+ });
5659
+ }
5660
+ }
5016
5661
  async function appendSession(_projectDir, session, opts) {
5017
5662
  const db2 = getSessionDb();
5018
- db2.prepare(`
5019
- INSERT INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path, description, transcript_path, pid, pid_started_at, original_head_sha)
5020
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
5663
+ let openSnapshot = null;
5664
+ try {
5665
+ openSnapshot = await getCumulativeTokenSource()(session.sessionId);
5666
+ } catch {
5667
+ openSnapshot = null;
5668
+ }
5669
+ const upsert = db2.prepare(`
5670
+ INSERT INTO sessions (session_id, agent, started, status, path, description, transcript_path, pid, pid_started_at, original_head_sha)
5671
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
5021
5672
  ON CONFLICT(session_id) DO UPDATE SET
5022
- project_slug = COALESCE(NULLIF(excluded.project_slug, ''), project_slug),
5023
- assignment_slug = COALESCE(NULLIF(excluded.assignment_slug, ''), assignment_slug),
5024
5673
  agent = excluded.agent,
5025
5674
  status = CASE
5026
5675
  WHEN status = 'completed' THEN status
5027
5676
  WHEN status = 'stopped' AND NOT (? AND excluded.status = 'active') THEN status
5028
5677
  ELSE excluded.status
5029
5678
  END,
5679
+ ended = CASE
5680
+ WHEN status = 'stopped' AND ? AND excluded.status = 'active' THEN NULL
5681
+ ELSE ended
5682
+ END,
5030
5683
  path = COALESCE(NULLIF(excluded.path, ''), path),
5031
5684
  description = COALESCE(NULLIF(excluded.description, ''), description),
5032
5685
  transcript_path = COALESCE(NULLIF(excluded.transcript_path, ''), transcript_path),
@@ -5034,59 +5687,151 @@ async function appendSession(_projectDir, session, opts) {
5034
5687
  pid_started_at = COALESCE(NULLIF(excluded.pid_started_at, ''), pid_started_at),
5035
5688
  original_head_sha = COALESCE(NULLIF(original_head_sha, ''), NULLIF(excluded.original_head_sha, '')),
5036
5689
  updated_at = datetime('now')
5037
- `).run(
5038
- session.sessionId,
5039
- session.projectSlug ?? null,
5040
- session.assignmentSlug ?? null,
5041
- session.agent,
5042
- session.started,
5043
- session.status,
5044
- session.path,
5045
- session.description ?? null,
5046
- session.transcriptPath ?? null,
5047
- session.pid ?? null,
5048
- session.pidStartedAt ?? null,
5049
- session.originalHeadSha ?? null,
5050
- opts?.reviveStopped ? 1 : 0
5051
- );
5690
+ `);
5691
+ const apply = db2.transaction(() => {
5692
+ upsert.run(
5693
+ session.sessionId,
5694
+ session.agent,
5695
+ session.started,
5696
+ session.status,
5697
+ session.path,
5698
+ session.description ?? null,
5699
+ session.transcriptPath ?? null,
5700
+ session.pid ?? null,
5701
+ session.pidStartedAt ?? null,
5702
+ session.originalHeadSha ?? null,
5703
+ opts?.reviveStopped ? 1 : 0,
5704
+ // status CASE: revive guard
5705
+ opts?.reviveStopped ? 1 : 0
5706
+ // ended CASE: clear stale ended on revive
5707
+ );
5708
+ const persisted = db2.prepare("SELECT status, ended FROM sessions WHERE session_id = ?").get(session.sessionId);
5709
+ const freshBinding = {
5710
+ assignmentId: session.assignmentId ?? null,
5711
+ projectSlug: session.projectSlug ?? null,
5712
+ assignmentSlug: session.assignmentSlug ?? null
5713
+ };
5714
+ if (persisted?.status === "active") {
5715
+ reopenEngagementIfMissing(
5716
+ session.sessionId,
5717
+ freshBinding,
5718
+ session.started ?? (/* @__PURE__ */ new Date()).toISOString(),
5719
+ openSnapshot
5720
+ );
5721
+ } else if (persisted && (freshBinding.projectSlug || freshBinding.assignmentSlug) && !hasAnyEngagement(session.sessionId)) {
5722
+ insertClosedEngagement({
5723
+ sessionId: session.sessionId,
5724
+ projectSlug: freshBinding.projectSlug,
5725
+ assignmentSlug: freshBinding.assignmentSlug,
5726
+ startedAt: session.started ?? (/* @__PURE__ */ new Date()).toISOString(),
5727
+ endedAt: persisted.ended ?? session.started ?? (/* @__PURE__ */ new Date()).toISOString(),
5728
+ closeReason: persisted.status === "completed" ? "completed" : "abandoned"
5729
+ });
5730
+ }
5731
+ });
5732
+ apply.immediate();
5052
5733
  }
5053
5734
  async function updateSessionStatus(_projectDir, sessionId, status, endedAt) {
5054
5735
  const db2 = getSessionDb();
5055
5736
  const isTerminal = status === "completed" || status === "stopped";
5056
- const result = isTerminal ? db2.prepare(
5057
- "UPDATE sessions SET status = ?, ended = COALESCE(?, datetime('now')), updated_at = datetime('now') WHERE session_id = ?"
5058
- ).run(status, endedAt ?? null, sessionId) : db2.prepare(
5059
- "UPDATE sessions SET status = ?, updated_at = datetime('now') WHERE session_id = ?"
5060
- ).run(status, sessionId);
5061
- return result.changes > 0;
5737
+ if (!isTerminal) {
5738
+ let reviveSnapshot = null;
5739
+ try {
5740
+ reviveSnapshot = await getCumulativeTokenSource()(sessionId);
5741
+ } catch {
5742
+ reviveSnapshot = null;
5743
+ }
5744
+ const apply2 = db2.transaction(() => {
5745
+ const current = db2.prepare("SELECT status FROM sessions WHERE session_id = ?").get(sessionId);
5746
+ if (!current) return false;
5747
+ if (current.status === "completed") {
5748
+ throw new SessionResurrectionError(
5749
+ `Refusing to revive completed session "${sessionId}" to active. A completed session is final.`
5750
+ );
5751
+ }
5752
+ const res = db2.prepare(
5753
+ "UPDATE sessions SET status = ?, ended = NULL, updated_at = datetime('now') WHERE session_id = ?"
5754
+ ).run(status, sessionId);
5755
+ if (res.changes > 0) {
5756
+ reopenEngagementIfMissing(sessionId, null, (/* @__PURE__ */ new Date()).toISOString(), reviveSnapshot);
5757
+ }
5758
+ return res.changes > 0;
5759
+ });
5760
+ return apply2.immediate();
5761
+ }
5762
+ const captured = getOpenEngagement(sessionId);
5763
+ let snapshot = null;
5764
+ try {
5765
+ snapshot = await getCumulativeTokenSource()(sessionId);
5766
+ } catch {
5767
+ snapshot = null;
5768
+ }
5769
+ const apply = db2.transaction(() => {
5770
+ const stillTerminal = captured ? closeEngagementById({
5771
+ id: captured.id,
5772
+ startedAt: captured.started_at,
5773
+ closeReason: status === "completed" ? "completed" : "abandoned",
5774
+ tokensAtClose: snapshot,
5775
+ endedAt: endedAt ?? (/* @__PURE__ */ new Date()).toISOString()
5776
+ }) : getOpenEngagement(sessionId) === null;
5777
+ if (!stillTerminal) return false;
5778
+ const res = db2.prepare(
5779
+ "UPDATE sessions SET status = ?, ended = COALESCE(?, datetime('now')), updated_at = datetime('now') WHERE session_id = ?"
5780
+ ).run(status, endedAt ?? null, sessionId);
5781
+ return res.changes > 0;
5782
+ });
5783
+ return apply.immediate();
5784
+ }
5785
+ function livenessStopSession(input) {
5786
+ const db2 = getSessionDb();
5787
+ const endedAt = input.endedAt ?? (/* @__PURE__ */ new Date()).toISOString();
5788
+ const apply = db2.transaction(() => {
5789
+ const stillDead = input.engagementId != null && input.engagementStartedAt != null ? closeEngagementById({
5790
+ id: input.engagementId,
5791
+ startedAt: input.engagementStartedAt,
5792
+ closeReason: "liveness_gc",
5793
+ tokensAtClose: input.tokensAtClose ?? null,
5794
+ endedAt
5795
+ }) : getOpenEngagement(input.sessionId) === null;
5796
+ if (!stillDead) return false;
5797
+ const res = db2.prepare(
5798
+ "UPDATE sessions SET status = 'stopped', ended = COALESCE(?, datetime('now')), updated_at = datetime('now') WHERE session_id = ? AND status = 'active'"
5799
+ ).run(input.endedAt ?? null, input.sessionId);
5800
+ return res.changes > 0;
5801
+ });
5802
+ return apply.immediate();
5062
5803
  }
5063
5804
  async function listAllSessions(_projectsDir) {
5064
5805
  const db2 = getSessionDb();
5065
- const rows = db2.prepare("SELECT * FROM sessions ORDER BY started DESC").all();
5806
+ const rows = db2.prepare(`${SESSION_SELECT_WITH_BINDING} ORDER BY s.started DESC`).all();
5066
5807
  return rows.map(rowToSession);
5067
5808
  }
5068
5809
  function getSessionById(sessionId) {
5069
5810
  const db2 = getSessionDb();
5070
- const row = db2.prepare("SELECT * FROM sessions WHERE session_id = ? LIMIT 1").get(sessionId);
5811
+ const row = db2.prepare(`${SESSION_SELECT_WITH_BINDING} WHERE s.session_id = ? LIMIT 1`).get(sessionId);
5071
5812
  return row ? rowToSession(row) : null;
5072
5813
  }
5073
5814
  async function listProjectSessions(_projectsDir, projectSlug, assignmentSlug) {
5074
5815
  const db2 = getSessionDb();
5075
5816
  if (assignmentSlug) {
5076
5817
  const rows2 = db2.prepare(
5077
- "SELECT * FROM sessions WHERE project_slug = ? AND assignment_slug = ? ORDER BY started DESC"
5818
+ `${SESSION_SELECT_WITH_BINDING} WHERE e.project_slug = ? AND e.assignment_slug = ? ORDER BY s.started DESC`
5078
5819
  ).all(projectSlug, assignmentSlug);
5079
5820
  return rows2.map(rowToSession);
5080
5821
  }
5081
- const rows = db2.prepare("SELECT * FROM sessions WHERE project_slug = ? ORDER BY started DESC").all(projectSlug);
5822
+ const rows = db2.prepare(`${SESSION_SELECT_WITH_BINDING} WHERE e.project_slug = ? ORDER BY s.started DESC`).all(projectSlug);
5082
5823
  return rows.map(rowToSession);
5083
5824
  }
5084
5825
  async function deleteSessions(sessionIds) {
5085
5826
  if (sessionIds.length === 0) return 0;
5086
5827
  const db2 = getSessionDb();
5087
5828
  const placeholders = sessionIds.map(() => "?").join(", ");
5088
- const result = db2.prepare(`DELETE FROM sessions WHERE session_id IN (${placeholders})`).run(...sessionIds);
5089
- return result.changes;
5829
+ const run2 = db2.transaction(() => {
5830
+ db2.prepare(`DELETE FROM engagement WHERE session_id IN (${placeholders})`).run(...sessionIds);
5831
+ const result = db2.prepare(`DELETE FROM sessions WHERE session_id IN (${placeholders})`).run(...sessionIds);
5832
+ return result.changes;
5833
+ });
5834
+ return run2.immediate();
5090
5835
  }
5091
5836
  async function readAssignmentStatusFromPath(assignmentMdPath) {
5092
5837
  if (!await fileExists(assignmentMdPath)) return null;
@@ -5096,39 +5841,44 @@ async function readAssignmentStatusFromPath(assignmentMdPath) {
5096
5841
  }
5097
5842
  async function readAssignmentStatus(projectDir, assignmentSlug) {
5098
5843
  return readAssignmentStatusFromPath(
5099
- resolve14(projectDir, "assignments", assignmentSlug, "assignment.md")
5844
+ resolve15(projectDir, "assignments", assignmentSlug, "assignment.md")
5100
5845
  );
5101
5846
  }
5102
5847
  async function reconcileActiveSessions(projectsDir, assignmentsDir) {
5103
5848
  const db2 = getSessionDb();
5104
- const activeSessions = db2.prepare("SELECT * FROM sessions WHERE status = 'active' AND assignment_slug IS NOT NULL").all();
5849
+ const activeSessions = db2.prepare(
5850
+ `SELECT s.*, e.project_slug AS project_slug, e.assignment_slug AS assignment_slug
5851
+ FROM sessions s
5852
+ JOIN engagement e ON e.session_id = s.session_id AND e.ended_at IS NULL
5853
+ WHERE s.status = 'active' AND e.assignment_slug IS NOT NULL`
5854
+ ).all();
5105
5855
  if (activeSessions.length === 0) return 0;
5106
5856
  const assignmentStatuses = /* @__PURE__ */ new Map();
5107
5857
  const seen = /* @__PURE__ */ new Set();
5108
5858
  for (const session of activeSessions) {
5109
5859
  const aslug = session.assignment_slug;
5110
5860
  if (!aslug) continue;
5111
- const projectKey = session.project_slug ?? "__standalone__";
5112
- const key = `${projectKey}/${aslug}`;
5861
+ const projectKey2 = session.project_slug ?? "__standalone__";
5862
+ const key = `${projectKey2}/${aslug}`;
5113
5863
  if (seen.has(key)) continue;
5114
5864
  seen.add(key);
5115
5865
  if (session.project_slug) {
5116
5866
  const status = await readAssignmentStatus(
5117
- resolve14(projectsDir, session.project_slug),
5867
+ resolve15(projectsDir, session.project_slug),
5118
5868
  aslug
5119
5869
  );
5120
5870
  if (status) assignmentStatuses.set(key, status);
5121
5871
  } else if (assignmentsDir) {
5122
5872
  const status = await readAssignmentStatusFromPath(
5123
- resolve14(assignmentsDir, aslug, "assignment.md")
5873
+ resolve15(assignmentsDir, aslug, "assignment.md")
5124
5874
  );
5125
5875
  if (status) assignmentStatuses.set(key, status);
5126
5876
  }
5127
5877
  }
5128
5878
  let totalUpdated = 0;
5129
5879
  for (const session of activeSessions) {
5130
- const projectKey = session.project_slug ?? "__standalone__";
5131
- const key = `${projectKey}/${session.assignment_slug}`;
5880
+ const projectKey2 = session.project_slug ?? "__standalone__";
5881
+ const key = `${projectKey2}/${session.assignment_slug}`;
5132
5882
  const assignmentStatus = assignmentStatuses.get(key);
5133
5883
  if (!assignmentStatus || !DONE_ASSIGNMENT_STATUSES.has(assignmentStatus)) continue;
5134
5884
  const newStatus = assignmentStatus === "failed" ? "stopped" : "completed";
@@ -5140,18 +5890,34 @@ async function reconcileActiveSessions(projectsDir, assignmentsDir) {
5140
5890
  async function listSessionsByAssignment(projectSlug, assignmentSlug) {
5141
5891
  const db2 = getSessionDb();
5142
5892
  const rows = projectSlug === null ? db2.prepare(
5143
- "SELECT * FROM sessions WHERE assignment_slug = ? AND project_slug IS NULL ORDER BY started DESC"
5893
+ `${SESSION_SELECT_WITH_BINDING} WHERE e.assignment_slug = ? AND e.project_slug IS NULL ORDER BY s.started DESC`
5144
5894
  ).all(assignmentSlug) : db2.prepare(
5145
- "SELECT * FROM sessions WHERE project_slug = ? AND assignment_slug = ? ORDER BY started DESC"
5895
+ `${SESSION_SELECT_WITH_BINDING} WHERE e.project_slug = ? AND e.assignment_slug = ? ORDER BY s.started DESC`
5146
5896
  ).all(projectSlug, assignmentSlug);
5147
5897
  return rows.map(rowToSession);
5148
5898
  }
5149
- var DONE_ASSIGNMENT_STATUSES;
5899
+ var SESSION_SELECT_WITH_BINDING, SessionResurrectionError, DONE_ASSIGNMENT_STATUSES;
5150
5900
  var init_agent_sessions = __esm({
5151
5901
  "src/dashboard/agent-sessions.ts"() {
5152
5902
  "use strict";
5153
5903
  init_fs();
5154
5904
  init_session_db();
5905
+ init_engagement_db();
5906
+ init_engagement_tokens();
5907
+ SESSION_SELECT_WITH_BINDING = `
5908
+ SELECT s.*,
5909
+ e.project_slug AS project_slug,
5910
+ e.assignment_slug AS assignment_slug,
5911
+ e.assignment_id AS assignment_id
5912
+ FROM sessions s
5913
+ LEFT JOIN engagement e ON e.id = (
5914
+ SELECT e2.id FROM engagement e2
5915
+ WHERE e2.session_id = s.session_id
5916
+ ORDER BY (e2.ended_at IS NULL) DESC, e2.started_at DESC, e2.id DESC
5917
+ LIMIT 1
5918
+ )`;
5919
+ SessionResurrectionError = class extends Error {
5920
+ };
5155
5921
  DONE_ASSIGNMENT_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "review"]);
5156
5922
  }
5157
5923
  });
@@ -5181,7 +5947,7 @@ var init_classify = __esm({
5181
5947
 
5182
5948
  // src/dashboard/api.ts
5183
5949
  import { readdir as readdir9, readFile as readFile12, writeFile as writeFile3, stat as stat2 } from "fs/promises";
5184
- import { resolve as resolve15, dirname as dirname2, basename } from "path";
5950
+ import { resolve as resolve16, dirname as dirname2, basename } from "path";
5185
5951
  function activeAssignments(items) {
5186
5952
  return items.filter((item) => item.archived !== true);
5187
5953
  }
@@ -5205,8 +5971,8 @@ async function computeStandaloneRecords(assignmentsDir) {
5205
5971
  const records = [];
5206
5972
  for (const entry of entries) {
5207
5973
  if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
5208
- const assignmentDir = resolve15(assignmentsDir, entry.name);
5209
- const assignmentMdPath = resolve15(assignmentDir, "assignment.md");
5974
+ const assignmentDir = resolve16(assignmentsDir, entry.name);
5975
+ const assignmentMdPath = resolve16(assignmentDir, "assignment.md");
5210
5976
  if (!await fileExists(assignmentMdPath)) continue;
5211
5977
  try {
5212
5978
  const content = await readFile12(assignmentMdPath, "utf-8");
@@ -5310,15 +6076,15 @@ async function getStandaloneAvailableTransitions(assignment) {
5310
6076
  return actions;
5311
6077
  }
5312
6078
  async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5313
- const assignmentDir = resolve15(projectsDir, projectSlug, "assignments", assignmentSlug);
5314
- const assignmentMdPath = resolve15(assignmentDir, "assignment.md");
6079
+ const assignmentDir = resolve16(projectsDir, projectSlug, "assignments", assignmentSlug);
6080
+ const assignmentMdPath = resolve16(assignmentDir, "assignment.md");
5315
6081
  if (!await fileExists(assignmentMdPath)) {
5316
6082
  return null;
5317
6083
  }
5318
6084
  const assignmentContent = await readFile12(assignmentMdPath, "utf-8");
5319
6085
  const assignment = parseAssignmentFull(assignmentContent);
5320
6086
  let projectWorkspace = null;
5321
- const projectMdPath = resolve15(projectsDir, projectSlug, "project.md");
6087
+ const projectMdPath = resolve16(projectsDir, projectSlug, "project.md");
5322
6088
  if (await fileExists(projectMdPath)) {
5323
6089
  const projectContent = await readFile12(projectMdPath, "utf-8");
5324
6090
  projectWorkspace = parseProject(projectContent).workspace;
@@ -5326,7 +6092,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5326
6092
  let plan = null;
5327
6093
  const planFile = await latestPlanFile(assignmentDir);
5328
6094
  if (planFile) {
5329
- const planPath = resolve15(assignmentDir, planFile);
6095
+ const planPath = resolve16(assignmentDir, planFile);
5330
6096
  if (await fileExists(planPath)) {
5331
6097
  const planContent = await readFile12(planPath, "utf-8");
5332
6098
  const parsed = parsePlan(planContent);
@@ -5338,7 +6104,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5338
6104
  }
5339
6105
  }
5340
6106
  let scratchpad = null;
5341
- const scratchpadPath = resolve15(assignmentDir, "scratchpad.md");
6107
+ const scratchpadPath = resolve16(assignmentDir, "scratchpad.md");
5342
6108
  if (await fileExists(scratchpadPath)) {
5343
6109
  const scratchpadContent = await readFile12(scratchpadPath, "utf-8");
5344
6110
  const parsed = parseScratchpad(scratchpadContent);
@@ -5348,7 +6114,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5348
6114
  };
5349
6115
  }
5350
6116
  let handoff = null;
5351
- const handoffPath = resolve15(assignmentDir, "handoff.md");
6117
+ const handoffPath = resolve16(assignmentDir, "handoff.md");
5352
6118
  if (await fileExists(handoffPath)) {
5353
6119
  const handoffContent = await readFile12(handoffPath, "utf-8");
5354
6120
  const parsed = parseHandoff(handoffContent);
@@ -5359,7 +6125,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5359
6125
  };
5360
6126
  }
5361
6127
  let decisionRecord = null;
5362
- const decisionRecordPath = resolve15(assignmentDir, "decision-record.md");
6128
+ const decisionRecordPath = resolve16(assignmentDir, "decision-record.md");
5363
6129
  if (await fileExists(decisionRecordPath)) {
5364
6130
  const decisionRecordContent = await readFile12(decisionRecordPath, "utf-8");
5365
6131
  const parsed = parseDecisionRecord(decisionRecordContent);
@@ -5370,7 +6136,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5370
6136
  };
5371
6137
  }
5372
6138
  let progress = null;
5373
- const progressPath = resolve15(assignmentDir, "progress.md");
6139
+ const progressPath = resolve16(assignmentDir, "progress.md");
5374
6140
  if (await fileExists(progressPath)) {
5375
6141
  const progressContent = await readFile12(progressPath, "utf-8");
5376
6142
  const parsed = parseProgress(progressContent);
@@ -5381,7 +6147,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5381
6147
  };
5382
6148
  }
5383
6149
  let comments = null;
5384
- const commentsPath = resolve15(assignmentDir, "comments.md");
6150
+ const commentsPath = resolve16(assignmentDir, "comments.md");
5385
6151
  if (await fileExists(commentsPath)) {
5386
6152
  const commentsContent = await readFile12(commentsPath, "utf-8");
5387
6153
  const parsed = parseComments(commentsContent);
@@ -5415,7 +6181,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5415
6181
  archivedReason: assignment.archivedReason,
5416
6182
  ...deriveStatusVirtuals(assignment, terminalStatuses),
5417
6183
  override: assignment.override,
5418
- derived: await buildDerivedDetail(assignment, assignmentDir, resolve15(projectsDir, projectSlug)),
6184
+ derived: await buildDerivedDetail(assignment, assignmentDir, resolve16(projectsDir, projectSlug)),
5419
6185
  created: assignment.created,
5420
6186
  updated: assignment.updated,
5421
6187
  body: assignment.body,
@@ -5506,7 +6272,7 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir) {
5506
6272
  slug: a.slug,
5507
6273
  title: a.title,
5508
6274
  projectSlug: rec.summary.slug,
5509
- assignmentDir: resolve15(rec.projectPath, "assignments", a.slug)
6275
+ assignmentDir: resolve16(rec.projectPath, "assignments", a.slug)
5510
6276
  });
5511
6277
  }
5512
6278
  }
@@ -5539,14 +6305,14 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir) {
5539
6305
  }
5540
6306
  async function countMentionsInAssignment(sourceDir, target) {
5541
6307
  const bodies = [];
5542
- const assignmentMd = resolve15(sourceDir, "assignment.md");
6308
+ const assignmentMd = resolve16(sourceDir, "assignment.md");
5543
6309
  if (await fileExists(assignmentMd)) {
5544
6310
  const content = await readFile12(assignmentMd, "utf-8");
5545
6311
  const todosMatch = content.match(/^## Todos\s*$([\s\S]*?)(?=^## |$(?![\r\n]))/m);
5546
6312
  if (todosMatch) bodies.push(todosMatch[1]);
5547
6313
  }
5548
6314
  for (const filename of ["progress.md", "comments.md", "handoff.md"]) {
5549
- const path = resolve15(sourceDir, filename);
6315
+ const path = resolve16(sourceDir, filename);
5550
6316
  if (await fileExists(path)) {
5551
6317
  try {
5552
6318
  bodies.push(await readFile12(path, "utf-8"));
@@ -5607,45 +6373,45 @@ async function getAssignmentDetailById(projectsDir, assignmentsDir, id) {
5607
6373
  }
5608
6374
  async function buildStandaloneAssignmentDetail(resolved) {
5609
6375
  const assignmentDir = resolved.assignmentDir;
5610
- const assignmentMdPath = resolve15(assignmentDir, "assignment.md");
6376
+ const assignmentMdPath = resolve16(assignmentDir, "assignment.md");
5611
6377
  if (!await fileExists(assignmentMdPath)) return null;
5612
6378
  const assignmentContent = await readFile12(assignmentMdPath, "utf-8");
5613
6379
  const assignment = parseAssignmentFull(assignmentContent);
5614
6380
  let plan = null;
5615
6381
  const planFile = await latestPlanFile(assignmentDir);
5616
6382
  if (planFile) {
5617
- const planPath = resolve15(assignmentDir, planFile);
6383
+ const planPath = resolve16(assignmentDir, planFile);
5618
6384
  if (await fileExists(planPath)) {
5619
6385
  const parsed = parsePlan(await readFile12(planPath, "utf-8"));
5620
6386
  plan = { status: parsed.status, updated: parsed.updated, body: parsed.body };
5621
6387
  }
5622
6388
  }
5623
6389
  let scratchpad = null;
5624
- const scratchpadPath = resolve15(assignmentDir, "scratchpad.md");
6390
+ const scratchpadPath = resolve16(assignmentDir, "scratchpad.md");
5625
6391
  if (await fileExists(scratchpadPath)) {
5626
6392
  const parsed = parseScratchpad(await readFile12(scratchpadPath, "utf-8"));
5627
6393
  scratchpad = { updated: parsed.updated, body: parsed.body };
5628
6394
  }
5629
6395
  let handoff = null;
5630
- const handoffPath = resolve15(assignmentDir, "handoff.md");
6396
+ const handoffPath = resolve16(assignmentDir, "handoff.md");
5631
6397
  if (await fileExists(handoffPath)) {
5632
6398
  const parsed = parseHandoff(await readFile12(handoffPath, "utf-8"));
5633
6399
  handoff = { updated: parsed.updated, handoffCount: parsed.handoffCount, body: parsed.body };
5634
6400
  }
5635
6401
  let decisionRecord = null;
5636
- const decisionRecordPath = resolve15(assignmentDir, "decision-record.md");
6402
+ const decisionRecordPath = resolve16(assignmentDir, "decision-record.md");
5637
6403
  if (await fileExists(decisionRecordPath)) {
5638
6404
  const parsed = parseDecisionRecord(await readFile12(decisionRecordPath, "utf-8"));
5639
6405
  decisionRecord = { updated: parsed.updated, decisionCount: parsed.decisionCount, body: parsed.body };
5640
6406
  }
5641
6407
  let progress = null;
5642
- const progressPath = resolve15(assignmentDir, "progress.md");
6408
+ const progressPath = resolve16(assignmentDir, "progress.md");
5643
6409
  if (await fileExists(progressPath)) {
5644
6410
  const parsed = parseProgress(await readFile12(progressPath, "utf-8"));
5645
6411
  progress = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
5646
6412
  }
5647
6413
  let comments = null;
5648
- const commentsPath = resolve15(assignmentDir, "comments.md");
6414
+ const commentsPath = resolve16(assignmentDir, "comments.md");
5649
6415
  if (await fileExists(commentsPath)) {
5650
6416
  const parsed = parseComments(await readFile12(commentsPath, "utf-8"));
5651
6417
  comments = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
@@ -5711,8 +6477,8 @@ async function computeProjectRecords(projectsDir, traces) {
5711
6477
  const projectDirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."));
5712
6478
  const maybeRecords = await Promise.all(
5713
6479
  projectDirs.map(async (entry) => {
5714
- const projectPath = resolve15(projectsDir, entry.name);
5715
- const projectMdPath = resolve15(projectPath, "project.md");
6480
+ const projectPath = resolve16(projectsDir, entry.name);
6481
+ const projectMdPath = resolve16(projectPath, "project.md");
5716
6482
  if (!await fileExists(projectMdPath)) {
5717
6483
  return null;
5718
6484
  }
@@ -5759,7 +6525,7 @@ async function computeProjectRecords(projectsDir, traces) {
5759
6525
  return records;
5760
6526
  }
5761
6527
  async function listAssignmentRecords(projectPath, traces) {
5762
- const assignmentsDir = resolve15(projectPath, "assignments");
6528
+ const assignmentsDir = resolve16(projectPath, "assignments");
5763
6529
  if (!await fileExists(assignmentsDir)) {
5764
6530
  return [];
5765
6531
  }
@@ -5767,7 +6533,7 @@ async function listAssignmentRecords(projectPath, traces) {
5767
6533
  const dirEntries = entries.filter((entry) => entry.isDirectory());
5768
6534
  const maybeRecords = await Promise.all(
5769
6535
  dirEntries.map(async (entry) => {
5770
- const assignmentMd = resolve15(assignmentsDir, entry.name, "assignment.md");
6536
+ const assignmentMd = resolve16(assignmentsDir, entry.name, "assignment.md");
5771
6537
  if (!await fileExists(assignmentMd)) {
5772
6538
  return null;
5773
6539
  }
@@ -5783,7 +6549,7 @@ async function listAssignmentRecords(projectPath, traces) {
5783
6549
  return records;
5784
6550
  }
5785
6551
  async function loadDependencyGraph(projectPath, assignments) {
5786
- const statusPath = resolve15(projectPath, "_status.md");
6552
+ const statusPath = resolve16(projectPath, "_status.md");
5787
6553
  if (await fileExists(statusPath)) {
5788
6554
  const statusContent = await readFile12(statusPath, "utf-8");
5789
6555
  const parsed = parseStatus(statusContent);
@@ -5958,7 +6724,7 @@ async function getAvailableTransitions(projectsDir, projectSlug, assignmentSlug,
5958
6724
  const config = await getStatusConfig();
5959
6725
  const transitionDefs = getTransitionDefinitions(config);
5960
6726
  const actions = [];
5961
- const projectPath = resolve15(projectsDir, projectSlug);
6727
+ const projectPath = resolve16(projectsDir, projectSlug);
5962
6728
  const traces = options?.traces;
5963
6729
  for (const definition of transitionDefs) {
5964
6730
  const target = getTargetStatus(assignment.status, definition.command, config.transitionTable);
@@ -6006,7 +6772,7 @@ async function getUnmetDependencies(projectPath, dependsOn, terminalStatuses, de
6006
6772
  continue;
6007
6773
  }
6008
6774
  }
6009
- const dependencyPath = resolve15(projectPath, "assignments", dependency, "assignment.md");
6775
+ const dependencyPath = resolve16(projectPath, "assignments", dependency, "assignment.md");
6010
6776
  if (!await fileExists(dependencyPath)) {
6011
6777
  unmet.push(`${dependency} (missing)`);
6012
6778
  continue;
@@ -6027,7 +6793,7 @@ function parseTimestamp(timestamp) {
6027
6793
  return Number.isFinite(parsed) ? parsed : 0;
6028
6794
  }
6029
6795
  async function countOpenQuestions(projectPath, assignmentSlug) {
6030
- const commentsPath = resolve15(
6796
+ const commentsPath = resolve16(
6031
6797
  projectPath,
6032
6798
  "assignments",
6033
6799
  assignmentSlug,
@@ -6392,7 +7158,7 @@ init_agents_schema();
6392
7158
  init_cwd();
6393
7159
  import { spawn as spawn2 } from "child_process";
6394
7160
  import { mkdir as mkdir2, writeFile as writeFile4 } from "fs/promises";
6395
- import { isAbsolute as isAbsolute3, resolve as resolve16 } from "path";
7161
+ import { isAbsolute as isAbsolute3, resolve as resolve17 } from "path";
6396
7162
  init_paths();
6397
7163
  init_playbooks();
6398
7164
  init_cwd();
@@ -6595,6 +7361,12 @@ async function resolveSessionPlan(input, terminal) {
6595
7361
  }
6596
7362
  }
6597
7363
  }
7364
+ if (!cwd.trim()) {
7365
+ throw new LaunchError(
7366
+ "workspace-path-invalid",
7367
+ `Session ${input.id} has no recorded working directory and no linked assignment workspace resolved \u2014 refusing to launch in an unknown directory.`
7368
+ );
7369
+ }
6598
7370
  const agent = getAgents(input.config).find((a) => a.id === session.agent);
6599
7371
  if (!agent) {
6600
7372
  throw new LaunchError(
@@ -6623,7 +7395,7 @@ async function resolveSessionPlan(input, terminal) {
6623
7395
  // src/launch/execute.ts
6624
7396
  import { spawn as spawn3 } from "child_process";
6625
7397
  import { homedir as homedir5 } from "os";
6626
- import { basename as basename2, join as join6, resolve as resolve17 } from "path";
7398
+ import { basename as basename2, join as join6, resolve as resolve18 } from "path";
6627
7399
 
6628
7400
  // src/utils/terminal-probe.ts
6629
7401
  import { spawnSync as spawnSync2 } from "child_process";
@@ -6701,7 +7473,7 @@ init_config2();
6701
7473
 
6702
7474
  // src/utils/session-id.ts
6703
7475
  import { execFileSync as execFileSync2 } from "child_process";
6704
- import { mkdirSync, readFileSync, statSync as statSync2, writeFileSync } from "fs";
7476
+ import { mkdirSync, readFileSync as readFileSync2, statSync as statSync3, writeFileSync } from "fs";
6705
7477
  import { homedir as homedir4 } from "os";
6706
7478
  import { dirname as dirname3, join as join5 } from "path";
6707
7479
 
@@ -6785,7 +7557,7 @@ async function executeLaunchPlan(plan, spawnFn = realSpawn) {
6785
7557
  `Spawn failed: ${msg}. Verify the terminal is installed and on PATH.`
6786
7558
  );
6787
7559
  }
6788
- await new Promise((resolve19, reject) => {
7560
+ await new Promise((resolve20, reject) => {
6789
7561
  let settled = false;
6790
7562
  let stderr = "";
6791
7563
  const finishOk = () => {
@@ -6795,7 +7567,7 @@ async function executeLaunchPlan(plan, spawnFn = realSpawn) {
6795
7567
  child.unref();
6796
7568
  } catch {
6797
7569
  }
6798
- resolve19();
7570
+ resolve20();
6799
7571
  };
6800
7572
  const finishErr = (remediation) => {
6801
7573
  if (settled) return;
@@ -6856,7 +7628,7 @@ async function registerLaunchAtBirth(plan, child) {
6856
7628
  markerDir
6857
7629
  );
6858
7630
  if (!sessionId) return;
6859
- if (autoTrack === "workspaces-only" && !await fileExists(resolve17(plan.cwd, ".syntaur", "context.json"))) {
7631
+ if (autoTrack === "workspaces-only" && !await fileExists(resolve18(plan.cwd, ".syntaur", "context.json"))) {
6860
7632
  return;
6861
7633
  }
6862
7634
  const { initSessionDb: initSessionDb2 } = await Promise.resolve().then(() => (init_session_db(), session_db_exports));
@@ -7032,8 +7804,8 @@ function appleScriptString(value) {
7032
7804
  init_paths();
7033
7805
  init_fs();
7034
7806
  import { fileURLToPath } from "url";
7035
- import { dirname as dirname4, resolve as resolve18, join as join7 } from "path";
7036
- import { realpathSync, readFileSync as readFileSync2, mkdirSync as mkdirSync2 } from "fs";
7807
+ import { dirname as dirname4, resolve as resolve19, join as join7 } from "path";
7808
+ import { realpathSync, readFileSync as readFileSync3, mkdirSync as mkdirSync2 } from "fs";
7037
7809
  var NPX_PATTERNS = [
7038
7810
  { kind: "npm", re: /\/_npx\/([^/]+)\/node_modules(?:\/|$)/ },
7039
7811
  { kind: "pnpm", re: /\/pnpm\/dlx\/([^/]+)\/node_modules(?:\/|$)/ },
@@ -7058,7 +7830,7 @@ function normalizeSlashes(p) {
7058
7830
  }
7059
7831
  function detectInstallKind(scriptUrl, opts = {}) {
7060
7832
  const realpath = opts.realpath ?? realpathSync.native;
7061
- const readFile13 = opts.readFile ?? ((p) => readFileSync2(p, "utf-8"));
7833
+ const readFile13 = opts.readFile ?? ((p) => readFileSync3(p, "utf-8"));
7062
7834
  const ua = opts.envUserAgent !== void 0 ? opts.envUserAgent : process.env.npm_config_user_agent ?? "";
7063
7835
  const resolved = resolveScriptPath(scriptUrl, realpath);
7064
7836
  if (resolved === null) {
@@ -7111,7 +7883,7 @@ function extractNpxHash(scriptUrl, opts = {}) {
7111
7883
  return null;
7112
7884
  }
7113
7885
  function nudgeStampDir() {
7114
- return resolve18(syntaurRoot(), "npx-handler-nudge");
7886
+ return resolve19(syntaurRoot(), "npx-handler-nudge");
7115
7887
  }
7116
7888
  function sanitizeHash(hash) {
7117
7889
  return hash.replace(/[^A-Za-z0-9_-]/g, "_") || "_";