syntaur 0.70.0 → 0.72.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-QfpY8LS2.js} +1 -1
  3. package/dashboard/dist/assets/{_baseUniq-DM-f7DWz.js → _baseUniq-J2dp0xSF.js} +1 -1
  4. package/dashboard/dist/assets/{arc-ZBlA3YdV.js → arc-7V3bSBdi.js} +1 -1
  5. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-BUmvtGTF.js → architectureDiagram-2XIMDMQ5-L4xF7_w_.js} +1 -1
  6. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-B3qxWK6s.js → blockDiagram-WCTKOSBZ-D85LV89B.js} +1 -1
  7. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-BEq_UJO-.js → c4Diagram-IC4MRINW-BuLOHFHk.js} +1 -1
  8. package/dashboard/dist/assets/channel-BPiUfFXP.js +1 -0
  9. package/dashboard/dist/assets/{chunk-4BX2VUAB-C-Y9ryMm.js → chunk-4BX2VUAB-PMmPIfmW.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-55IACEB6-CGdtbsjw.js → chunk-55IACEB6-CuzXx0Ve.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-FMBD7UC4-DllxJhUp.js → chunk-FMBD7UC4-DgbPJiW4.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-JSJVCQXG-jjMM8O5F.js → chunk-JSJVCQXG-BIdP1PgR.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-KX2RTZJC-B_6BPltQ.js → chunk-KX2RTZJC-Jx0-O-nW.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-NQ4KR5QH-D0hJiXHp.js → chunk-NQ4KR5QH-CZy5XF4L.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-QZHKN3VN-BCWo4hLS.js → chunk-QZHKN3VN-Clgnd2ni.js} +1 -1
  16. package/dashboard/dist/assets/{chunk-WL4C6EOR-DH_jEAwg.js → chunk-WL4C6EOR-DItFgUMd.js} +1 -1
  17. package/dashboard/dist/assets/classDiagram-VBA2DB6C-HDWgoi6J.js +1 -0
  18. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-HDWgoi6J.js +1 -0
  19. package/dashboard/dist/assets/clone-BdrN_Gqo.js +1 -0
  20. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-5ld00TOH.js → cose-bilkent-S5V4N54A-CdvW6L_R.js} +1 -1
  21. package/dashboard/dist/assets/{dagre-KLK3FWXG-Cnu6eQWy.js → dagre-KLK3FWXG-200WRn9D.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-E7M64L7V-_CBBKNP-.js → diagram-E7M64L7V-B_4QJiws.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-IFDJBPK2-DE6WjIb1.js → diagram-IFDJBPK2-DiSnANLV.js} +1 -1
  24. package/dashboard/dist/assets/{diagram-P4PSJMXO-DpW7UzNK.js → diagram-P4PSJMXO-DjtHY2zM.js} +1 -1
  25. package/dashboard/dist/assets/{erDiagram-INFDFZHY-BD2409fE.js → erDiagram-INFDFZHY-032TAwXT.js} +1 -1
  26. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-1p0khhFI.js → flowDiagram-PKNHOUZH-DGNPxjzE.js} +1 -1
  27. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-B2zFyA4s.js → ganttDiagram-A5KZAMGK-BjbxtEVv.js} +1 -1
  28. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-bH-4YH7h.js → gitGraphDiagram-K3NZZRJ6-CgFktzLw.js} +1 -1
  29. package/dashboard/dist/assets/{graph-BT24B6iQ.js → graph-CyuCoTKt.js} +1 -1
  30. package/dashboard/dist/assets/{index-Cxqr3rQB.js → index-DsFBSYQj.js} +102 -102
  31. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-CMzP4Hcg.js → infoDiagram-LFFYTUFH-Dav72O_U.js} +1 -1
  32. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-DMEmFC7M.js → ishikawaDiagram-PHBUUO56-RAyYfa7q.js} +1 -1
  33. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-CAtzYQUm.js → journeyDiagram-4ABVD52K-D_9LHvxq.js} +1 -1
  34. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-d1JVbtvX.js → kanban-definition-K7BYSVSG-BxawyHsy.js} +1 -1
  35. package/dashboard/dist/assets/{layout-BVuI38I_.js → layout-BuWGns-1.js} +1 -1
  36. package/dashboard/dist/assets/{linear-Bc8PGMbp.js → linear-Dml-S448.js} +1 -1
  37. package/dashboard/dist/assets/{mermaid.core-UtwFYLNj.js → mermaid.core-p144DJAn.js} +4 -4
  38. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-DHc5RCDj.js → mindmap-definition-YRQLILUH-ffX-VsIz.js} +1 -1
  39. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-9anIsdIA.js → pieDiagram-SKSYHLDU-CneGfBsg.js} +1 -1
  40. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-FZ0D9HnU.js → quadrantDiagram-337W2JSQ-B1fvKMwB.js} +1 -1
  41. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-BkjCvH_u.js → requirementDiagram-Z7DCOOCP-B-rOBvHF.js} +1 -1
  42. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-DzqwYHDo.js → sankeyDiagram-WA2Y5GQK-Bb_m_ROm.js} +1 -1
  43. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-BW4g5Ao-.js → sequenceDiagram-2WXFIKYE-B-H6H5KN.js} +1 -1
  44. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-D0tKU3Z0.js → stateDiagram-RAJIS63D-CsDEjjit.js} +1 -1
  45. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-DmheKY2h.js +1 -0
  46. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-Bu269QDX.js → timeline-definition-YZTLITO2-d0zf4ELB.js} +1 -1
  47. package/dashboard/dist/assets/{treemap-KZPCXAKY-BGu_rrva.js → treemap-KZPCXAKY-DZZjDZEs.js} +1 -1
  48. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-Cx_n5FRZ.js → vennDiagram-LZ73GAT5-CoxX9DoM.js} +1 -1
  49. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-BOJsKV_W.js → xychartDiagram-JWTSCODW-BN1K72KM.js} +1 -1
  50. package/dashboard/dist/index.html +1 -1
  51. package/dist/dashboard/server.js +1385 -540
  52. package/dist/dashboard/server.js.map +1 -1
  53. package/dist/index.js +2770 -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
@@ -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
  }
@@ -634,6 +634,7 @@ function parseAssignmentFrontmatter(fileContent) {
634
634
  planApproval: parsePlanApproval(frontmatter),
635
635
  parked: getField2("parked") === "true",
636
636
  reviewRequested: getField2("reviewRequested") === "true",
637
+ reworkRequested: getField2("reworkRequested") === "true",
637
638
  implementationStarted: getField2("implementationStarted") === "true",
638
639
  override: parseOverride(frontmatter),
639
640
  facts: parseFactsMap(frontmatter),
@@ -947,6 +948,7 @@ function parseAssignmentFull(fileContent) {
947
948
  disposition: getField(fm, "disposition"),
948
949
  parked: getField(fm, "parked") === "true",
949
950
  reviewRequested: getField(fm, "reviewRequested") === "true",
951
+ reworkRequested: getField(fm, "reworkRequested") === "true",
950
952
  implementationStarted: getField(fm, "implementationStarted") === "true",
951
953
  planApproval: (() => {
952
954
  const file = getNestedField(fm, "planApproval", "file");
@@ -1426,7 +1428,9 @@ var init_derive_config = __esm({
1426
1428
  },
1427
1429
  {
1428
1430
  phase: "review",
1429
- 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",
1430
1434
  next: "Complete, or address review feedback"
1431
1435
  }
1432
1436
  ],
@@ -1493,6 +1497,7 @@ var init_fields = __esm({
1493
1497
  blocked: { kind: "bool" },
1494
1498
  parked: { kind: "bool" },
1495
1499
  reviewrequested: { kind: "bool", get: (i) => i["reviewRequested"] },
1500
+ reworkrequested: { kind: "bool", get: (i) => i["reworkRequested"] },
1496
1501
  pinned: { kind: "bool" }
1497
1502
  };
1498
1503
  }
@@ -2272,6 +2277,7 @@ function queryFieldNames(declarations) {
2272
2277
  "blocked",
2273
2278
  "parked",
2274
2279
  "reviewRequested",
2280
+ "reworkRequested",
2275
2281
  "pinned"
2276
2282
  ];
2277
2283
  const custom = [];
@@ -2310,6 +2316,7 @@ var init_fact_registry = __esm({
2310
2316
  blocked: { kind: "bool" },
2311
2317
  parked: { kind: "bool" },
2312
2318
  reviewrequested: { kind: "bool", get: (i) => i["reviewRequested"] },
2319
+ reworkrequested: { kind: "bool", get: (i) => i["reworkRequested"] },
2313
2320
  pinned: { kind: "bool" }
2314
2321
  };
2315
2322
  }
@@ -4344,7 +4351,7 @@ function compiledWhen(registry, when) {
4344
4351
  return pred;
4345
4352
  }
4346
4353
  function deriveDimensions(input) {
4347
- const { facts, derive, currentStatus, terminalStatuses, knownStatusIds, override } = input;
4354
+ const { facts, derive, currentStatus, terminalStatuses, knownStatusIds, override: override2 } = input;
4348
4355
  const registry = input.registry ?? DERIVE_FIELDS;
4349
4356
  if (terminalStatuses.has(currentStatus)) return null;
4350
4357
  const ctx = { now: 0 };
@@ -4378,8 +4385,8 @@ function deriveDimensions(input) {
4378
4385
  derivedStatus = phase;
4379
4386
  }
4380
4387
  let status = derivedStatus;
4381
- if (override && override.status && !terminalStatuses.has(override.status) && knownStatusIds.has(override.status)) {
4382
- status = override.status;
4388
+ if (override2 && override2.status && !terminalStatuses.has(override2.status) && knownStatusIds.has(override2.status)) {
4389
+ status = override2.status;
4383
4390
  }
4384
4391
  return { phase, disposition, derivedStatus, status, nextAction };
4385
4392
  }
@@ -4670,6 +4677,7 @@ async function computeFactsDetailed(input) {
4670
4677
  blocked: frontmatter.blockedReason !== null,
4671
4678
  parked: frontmatter.parked,
4672
4679
  reviewRequested: frontmatter.reviewRequested,
4680
+ reworkRequested: frontmatter.reworkRequested,
4673
4681
  pinned: frontmatter.override !== null
4674
4682
  };
4675
4683
  const attestations = [];
@@ -4981,6 +4989,139 @@ var init_help = __esm({
4981
4989
  }
4982
4990
  });
4983
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
+
4984
5125
  // src/dashboard/session-db.ts
4985
5126
  var session_db_exports = {};
4986
5127
  __export(session_db_exports, {
@@ -4992,18 +5133,23 @@ __export(session_db_exports, {
4992
5133
  resetSessionDb: () => resetSessionDb
4993
5134
  });
4994
5135
  import Database2 from "better-sqlite3";
4995
- import { resolve as resolve13 } from "path";
5136
+ import { resolve as resolve14 } from "path";
4996
5137
  import { readdir as readdir8 } from "fs/promises";
4997
5138
  function initSessionDb(dbPath) {
4998
5139
  if (db) return db;
4999
- const finalPath = dbPath ?? resolve13(syntaurRoot(), "syntaur.db");
5140
+ const finalPath = dbPath ?? resolve14(syntaurRoot(), "syntaur.db");
5000
5141
  db = new Database2(finalPath);
5001
5142
  db.pragma("journal_mode = WAL");
5002
5143
  db.exec(SCHEMA_SQL);
5144
+ db.exec(ENGAGEMENT_DDL);
5003
5145
  db.prepare("INSERT OR IGNORE INTO meta (key, value) VALUES (?, ?)").run(
5004
5146
  "schema_version",
5005
5147
  SCHEMA_VERSION
5006
5148
  );
5149
+ db.prepare("INSERT OR IGNORE INTO meta (key, value) VALUES (?, ?)").run(
5150
+ "engagement_schema_version",
5151
+ ENGAGEMENT_SCHEMA_VERSION
5152
+ );
5007
5153
  const database = db;
5008
5154
  const runMigrations = database.transaction(() => {
5009
5155
  const vBeforeV2 = database.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get()?.value;
@@ -5130,9 +5276,43 @@ function initSessionDb(dbPath) {
5130
5276
  UPDATE meta SET value = '5' WHERE key = 'schema_version';
5131
5277
  `);
5132
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
+ }
5133
5314
  });
5134
5315
  runMigrations.exclusive();
5135
- db.exec(POST_MIGRATION_INDEXES_SQL);
5136
5316
  return db;
5137
5317
  }
5138
5318
  function isSessionDbInitialized() {
@@ -5164,20 +5344,38 @@ async function migrateFromMarkdown(projectsDir) {
5164
5344
  const allSessions = [];
5165
5345
  for (const entry of entries) {
5166
5346
  if (!entry.isDirectory()) continue;
5167
- const projectDir = resolve13(projectsDir, entry.name);
5168
- const indexPath = resolve13(projectDir, "_index-sessions.md");
5347
+ const projectDir = resolve14(projectsDir, entry.name);
5348
+ const indexPath = resolve14(projectDir, "_index-sessions.md");
5169
5349
  if (!await fileExists(indexPath)) continue;
5170
5350
  const sessions = await parseMarkdownSessionsIndex(indexPath, entry.name);
5171
5351
  allSessions.push(...sessions);
5172
5352
  }
5173
5353
  if (allSessions.length === 0) return 0;
5174
5354
  const insert = database.prepare(`
5175
- INSERT OR IGNORE INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path)
5176
- 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
+ )
5177
5364
  `);
5178
5365
  const insertAll = database.transaction((sessions) => {
5179
5366
  for (const s of sessions) {
5180
- 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
+ }
5181
5379
  }
5182
5380
  });
5183
5381
  insertAll(allSessions);
@@ -5220,19 +5418,19 @@ async function parseMarkdownSessionsIndex(filePath, projectSlug) {
5220
5418
  }
5221
5419
  return sessions;
5222
5420
  }
5223
- var db, SCHEMA_VERSION, SCHEMA_SQL, POST_MIGRATION_INDEXES_SQL;
5421
+ var db, SCHEMA_VERSION, SCHEMA_SQL;
5224
5422
  var init_session_db = __esm({
5225
5423
  "src/dashboard/session-db.ts"() {
5226
5424
  "use strict";
5227
5425
  init_paths();
5228
5426
  init_fs();
5427
+ init_engagement_schema();
5428
+ init_engagement_backfill();
5229
5429
  db = null;
5230
- SCHEMA_VERSION = "5";
5430
+ SCHEMA_VERSION = "6";
5231
5431
  SCHEMA_SQL = `
5232
5432
  CREATE TABLE IF NOT EXISTS sessions (
5233
5433
  session_id TEXT PRIMARY KEY,
5234
- project_slug TEXT,
5235
- assignment_slug TEXT,
5236
5434
  agent TEXT NOT NULL,
5237
5435
  started TEXT NOT NULL,
5238
5436
  ended TEXT,
@@ -5243,34 +5441,172 @@ CREATE TABLE IF NOT EXISTS sessions (
5243
5441
  pid INTEGER,
5244
5442
  pid_started_at TEXT,
5245
5443
  original_head_sha TEXT,
5444
+ activity TEXT,
5246
5445
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
5247
5446
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
5248
5447
  );
5249
5448
  CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
5250
5449
  CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);
5251
- `;
5252
- POST_MIGRATION_INDEXES_SQL = `
5253
- CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
5254
- CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
5255
5450
  `;
5256
5451
  }
5257
5452
  });
5258
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";
5590
+ }
5591
+ });
5592
+
5259
5593
  // src/dashboard/agent-sessions.ts
5260
5594
  var agent_sessions_exports = {};
5261
5595
  __export(agent_sessions_exports, {
5596
+ SessionResurrectionError: () => SessionResurrectionError,
5262
5597
  appendSession: () => appendSession,
5263
5598
  deleteSessions: () => deleteSessions,
5264
5599
  getSessionById: () => getSessionById,
5265
5600
  listAllSessions: () => listAllSessions,
5266
5601
  listProjectSessions: () => listProjectSessions,
5267
5602
  listSessionsByAssignment: () => listSessionsByAssignment,
5603
+ livenessStopSession: () => livenessStopSession,
5268
5604
  parseSessionsIndex: () => parseSessionsIndex,
5269
5605
  reconcileActiveSessions: () => reconcileActiveSessions,
5270
5606
  updateSessionStatus: () => updateSessionStatus
5271
5607
  });
5272
5608
  import { readFile as readFile11 } from "fs/promises";
5273
- import { resolve as resolve14 } from "path";
5609
+ import { resolve as resolve15 } from "path";
5274
5610
  function rowToSession(row) {
5275
5611
  return {
5276
5612
  sessionId: row.session_id,
@@ -5286,28 +5622,64 @@ function rowToSession(row) {
5286
5622
  pid: row.pid ?? null,
5287
5623
  pidStartedAt: row.pid_started_at ?? null,
5288
5624
  originalHeadSha: row.original_head_sha ?? null,
5625
+ activity: row.activity ?? null,
5289
5626
  updatedAt: row.updated_at ?? null
5290
5627
  };
5291
5628
  }
5292
5629
  async function parseSessionsIndex(_projectDir, projectSlug) {
5293
5630
  const db2 = getSessionDb();
5294
- 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);
5295
5632
  return rows.map(rowToSession);
5296
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
+ }
5297
5661
  async function appendSession(_projectDir, session, opts) {
5298
5662
  const db2 = getSessionDb();
5299
- db2.prepare(`
5300
- INSERT INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path, description, transcript_path, pid, pid_started_at, original_head_sha)
5301
- 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
5302
5672
  ON CONFLICT(session_id) DO UPDATE SET
5303
- project_slug = COALESCE(NULLIF(excluded.project_slug, ''), project_slug),
5304
- assignment_slug = COALESCE(NULLIF(excluded.assignment_slug, ''), assignment_slug),
5305
5673
  agent = excluded.agent,
5306
5674
  status = CASE
5307
5675
  WHEN status = 'completed' THEN status
5308
5676
  WHEN status = 'stopped' AND NOT (? AND excluded.status = 'active') THEN status
5309
5677
  ELSE excluded.status
5310
5678
  END,
5679
+ ended = CASE
5680
+ WHEN status = 'stopped' AND ? AND excluded.status = 'active' THEN NULL
5681
+ ELSE ended
5682
+ END,
5311
5683
  path = COALESCE(NULLIF(excluded.path, ''), path),
5312
5684
  description = COALESCE(NULLIF(excluded.description, ''), description),
5313
5685
  transcript_path = COALESCE(NULLIF(excluded.transcript_path, ''), transcript_path),
@@ -5315,59 +5687,151 @@ async function appendSession(_projectDir, session, opts) {
5315
5687
  pid_started_at = COALESCE(NULLIF(excluded.pid_started_at, ''), pid_started_at),
5316
5688
  original_head_sha = COALESCE(NULLIF(original_head_sha, ''), NULLIF(excluded.original_head_sha, '')),
5317
5689
  updated_at = datetime('now')
5318
- `).run(
5319
- session.sessionId,
5320
- session.projectSlug ?? null,
5321
- session.assignmentSlug ?? null,
5322
- session.agent,
5323
- session.started,
5324
- session.status,
5325
- session.path,
5326
- session.description ?? null,
5327
- session.transcriptPath ?? null,
5328
- session.pid ?? null,
5329
- session.pidStartedAt ?? null,
5330
- session.originalHeadSha ?? null,
5331
- opts?.reviveStopped ? 1 : 0
5332
- );
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();
5333
5733
  }
5334
5734
  async function updateSessionStatus(_projectDir, sessionId, status, endedAt) {
5335
5735
  const db2 = getSessionDb();
5336
5736
  const isTerminal = status === "completed" || status === "stopped";
5337
- const result = isTerminal ? db2.prepare(
5338
- "UPDATE sessions SET status = ?, ended = COALESCE(?, datetime('now')), updated_at = datetime('now') WHERE session_id = ?"
5339
- ).run(status, endedAt ?? null, sessionId) : db2.prepare(
5340
- "UPDATE sessions SET status = ?, updated_at = datetime('now') WHERE session_id = ?"
5341
- ).run(status, sessionId);
5342
- 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();
5343
5803
  }
5344
5804
  async function listAllSessions(_projectsDir) {
5345
5805
  const db2 = getSessionDb();
5346
- 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();
5347
5807
  return rows.map(rowToSession);
5348
5808
  }
5349
5809
  function getSessionById(sessionId) {
5350
5810
  const db2 = getSessionDb();
5351
- 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);
5352
5812
  return row ? rowToSession(row) : null;
5353
5813
  }
5354
5814
  async function listProjectSessions(_projectsDir, projectSlug, assignmentSlug) {
5355
5815
  const db2 = getSessionDb();
5356
5816
  if (assignmentSlug) {
5357
5817
  const rows2 = db2.prepare(
5358
- "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`
5359
5819
  ).all(projectSlug, assignmentSlug);
5360
5820
  return rows2.map(rowToSession);
5361
5821
  }
5362
- 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);
5363
5823
  return rows.map(rowToSession);
5364
5824
  }
5365
5825
  async function deleteSessions(sessionIds) {
5366
5826
  if (sessionIds.length === 0) return 0;
5367
5827
  const db2 = getSessionDb();
5368
5828
  const placeholders = sessionIds.map(() => "?").join(", ");
5369
- const result = db2.prepare(`DELETE FROM sessions WHERE session_id IN (${placeholders})`).run(...sessionIds);
5370
- 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();
5371
5835
  }
5372
5836
  async function readAssignmentStatusFromPath(assignmentMdPath) {
5373
5837
  if (!await fileExists(assignmentMdPath)) return null;
@@ -5377,39 +5841,44 @@ async function readAssignmentStatusFromPath(assignmentMdPath) {
5377
5841
  }
5378
5842
  async function readAssignmentStatus(projectDir, assignmentSlug) {
5379
5843
  return readAssignmentStatusFromPath(
5380
- resolve14(projectDir, "assignments", assignmentSlug, "assignment.md")
5844
+ resolve15(projectDir, "assignments", assignmentSlug, "assignment.md")
5381
5845
  );
5382
5846
  }
5383
5847
  async function reconcileActiveSessions(projectsDir, assignmentsDir) {
5384
5848
  const db2 = getSessionDb();
5385
- 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();
5386
5855
  if (activeSessions.length === 0) return 0;
5387
5856
  const assignmentStatuses = /* @__PURE__ */ new Map();
5388
5857
  const seen = /* @__PURE__ */ new Set();
5389
5858
  for (const session of activeSessions) {
5390
5859
  const aslug = session.assignment_slug;
5391
5860
  if (!aslug) continue;
5392
- const projectKey = session.project_slug ?? "__standalone__";
5393
- const key = `${projectKey}/${aslug}`;
5861
+ const projectKey2 = session.project_slug ?? "__standalone__";
5862
+ const key = `${projectKey2}/${aslug}`;
5394
5863
  if (seen.has(key)) continue;
5395
5864
  seen.add(key);
5396
5865
  if (session.project_slug) {
5397
5866
  const status = await readAssignmentStatus(
5398
- resolve14(projectsDir, session.project_slug),
5867
+ resolve15(projectsDir, session.project_slug),
5399
5868
  aslug
5400
5869
  );
5401
5870
  if (status) assignmentStatuses.set(key, status);
5402
5871
  } else if (assignmentsDir) {
5403
5872
  const status = await readAssignmentStatusFromPath(
5404
- resolve14(assignmentsDir, aslug, "assignment.md")
5873
+ resolve15(assignmentsDir, aslug, "assignment.md")
5405
5874
  );
5406
5875
  if (status) assignmentStatuses.set(key, status);
5407
5876
  }
5408
5877
  }
5409
5878
  let totalUpdated = 0;
5410
5879
  for (const session of activeSessions) {
5411
- const projectKey = session.project_slug ?? "__standalone__";
5412
- const key = `${projectKey}/${session.assignment_slug}`;
5880
+ const projectKey2 = session.project_slug ?? "__standalone__";
5881
+ const key = `${projectKey2}/${session.assignment_slug}`;
5413
5882
  const assignmentStatus = assignmentStatuses.get(key);
5414
5883
  if (!assignmentStatus || !DONE_ASSIGNMENT_STATUSES.has(assignmentStatus)) continue;
5415
5884
  const newStatus = assignmentStatus === "failed" ? "stopped" : "completed";
@@ -5421,18 +5890,34 @@ async function reconcileActiveSessions(projectsDir, assignmentsDir) {
5421
5890
  async function listSessionsByAssignment(projectSlug, assignmentSlug) {
5422
5891
  const db2 = getSessionDb();
5423
5892
  const rows = projectSlug === null ? db2.prepare(
5424
- "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`
5425
5894
  ).all(assignmentSlug) : db2.prepare(
5426
- "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`
5427
5896
  ).all(projectSlug, assignmentSlug);
5428
5897
  return rows.map(rowToSession);
5429
5898
  }
5430
- var DONE_ASSIGNMENT_STATUSES;
5899
+ var SESSION_SELECT_WITH_BINDING, SessionResurrectionError, DONE_ASSIGNMENT_STATUSES;
5431
5900
  var init_agent_sessions = __esm({
5432
5901
  "src/dashboard/agent-sessions.ts"() {
5433
5902
  "use strict";
5434
5903
  init_fs();
5435
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
+ };
5436
5921
  DONE_ASSIGNMENT_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "review"]);
5437
5922
  }
5438
5923
  });
@@ -5462,7 +5947,7 @@ var init_classify = __esm({
5462
5947
 
5463
5948
  // src/dashboard/api.ts
5464
5949
  import { readdir as readdir9, readFile as readFile12, writeFile as writeFile3, stat as stat2 } from "fs/promises";
5465
- import { resolve as resolve15, dirname as dirname2, basename } from "path";
5950
+ import { resolve as resolve16, dirname as dirname2, basename } from "path";
5466
5951
  function activeAssignments(items) {
5467
5952
  return items.filter((item) => item.archived !== true);
5468
5953
  }
@@ -5486,8 +5971,8 @@ async function computeStandaloneRecords(assignmentsDir) {
5486
5971
  const records = [];
5487
5972
  for (const entry of entries) {
5488
5973
  if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
5489
- const assignmentDir = resolve15(assignmentsDir, entry.name);
5490
- const assignmentMdPath = resolve15(assignmentDir, "assignment.md");
5974
+ const assignmentDir = resolve16(assignmentsDir, entry.name);
5975
+ const assignmentMdPath = resolve16(assignmentDir, "assignment.md");
5491
5976
  if (!await fileExists(assignmentMdPath)) continue;
5492
5977
  try {
5493
5978
  const content = await readFile12(assignmentMdPath, "utf-8");
@@ -5591,15 +6076,15 @@ async function getStandaloneAvailableTransitions(assignment) {
5591
6076
  return actions;
5592
6077
  }
5593
6078
  async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5594
- const assignmentDir = resolve15(projectsDir, projectSlug, "assignments", assignmentSlug);
5595
- const assignmentMdPath = resolve15(assignmentDir, "assignment.md");
6079
+ const assignmentDir = resolve16(projectsDir, projectSlug, "assignments", assignmentSlug);
6080
+ const assignmentMdPath = resolve16(assignmentDir, "assignment.md");
5596
6081
  if (!await fileExists(assignmentMdPath)) {
5597
6082
  return null;
5598
6083
  }
5599
6084
  const assignmentContent = await readFile12(assignmentMdPath, "utf-8");
5600
6085
  const assignment = parseAssignmentFull(assignmentContent);
5601
6086
  let projectWorkspace = null;
5602
- const projectMdPath = resolve15(projectsDir, projectSlug, "project.md");
6087
+ const projectMdPath = resolve16(projectsDir, projectSlug, "project.md");
5603
6088
  if (await fileExists(projectMdPath)) {
5604
6089
  const projectContent = await readFile12(projectMdPath, "utf-8");
5605
6090
  projectWorkspace = parseProject(projectContent).workspace;
@@ -5607,7 +6092,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5607
6092
  let plan = null;
5608
6093
  const planFile = await latestPlanFile(assignmentDir);
5609
6094
  if (planFile) {
5610
- const planPath = resolve15(assignmentDir, planFile);
6095
+ const planPath = resolve16(assignmentDir, planFile);
5611
6096
  if (await fileExists(planPath)) {
5612
6097
  const planContent = await readFile12(planPath, "utf-8");
5613
6098
  const parsed = parsePlan(planContent);
@@ -5619,7 +6104,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5619
6104
  }
5620
6105
  }
5621
6106
  let scratchpad = null;
5622
- const scratchpadPath = resolve15(assignmentDir, "scratchpad.md");
6107
+ const scratchpadPath = resolve16(assignmentDir, "scratchpad.md");
5623
6108
  if (await fileExists(scratchpadPath)) {
5624
6109
  const scratchpadContent = await readFile12(scratchpadPath, "utf-8");
5625
6110
  const parsed = parseScratchpad(scratchpadContent);
@@ -5629,7 +6114,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5629
6114
  };
5630
6115
  }
5631
6116
  let handoff = null;
5632
- const handoffPath = resolve15(assignmentDir, "handoff.md");
6117
+ const handoffPath = resolve16(assignmentDir, "handoff.md");
5633
6118
  if (await fileExists(handoffPath)) {
5634
6119
  const handoffContent = await readFile12(handoffPath, "utf-8");
5635
6120
  const parsed = parseHandoff(handoffContent);
@@ -5640,7 +6125,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5640
6125
  };
5641
6126
  }
5642
6127
  let decisionRecord = null;
5643
- const decisionRecordPath = resolve15(assignmentDir, "decision-record.md");
6128
+ const decisionRecordPath = resolve16(assignmentDir, "decision-record.md");
5644
6129
  if (await fileExists(decisionRecordPath)) {
5645
6130
  const decisionRecordContent = await readFile12(decisionRecordPath, "utf-8");
5646
6131
  const parsed = parseDecisionRecord(decisionRecordContent);
@@ -5651,7 +6136,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5651
6136
  };
5652
6137
  }
5653
6138
  let progress = null;
5654
- const progressPath = resolve15(assignmentDir, "progress.md");
6139
+ const progressPath = resolve16(assignmentDir, "progress.md");
5655
6140
  if (await fileExists(progressPath)) {
5656
6141
  const progressContent = await readFile12(progressPath, "utf-8");
5657
6142
  const parsed = parseProgress(progressContent);
@@ -5662,7 +6147,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5662
6147
  };
5663
6148
  }
5664
6149
  let comments = null;
5665
- const commentsPath = resolve15(assignmentDir, "comments.md");
6150
+ const commentsPath = resolve16(assignmentDir, "comments.md");
5666
6151
  if (await fileExists(commentsPath)) {
5667
6152
  const commentsContent = await readFile12(commentsPath, "utf-8");
5668
6153
  const parsed = parseComments(commentsContent);
@@ -5696,7 +6181,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5696
6181
  archivedReason: assignment.archivedReason,
5697
6182
  ...deriveStatusVirtuals(assignment, terminalStatuses),
5698
6183
  override: assignment.override,
5699
- derived: await buildDerivedDetail(assignment, assignmentDir, resolve15(projectsDir, projectSlug)),
6184
+ derived: await buildDerivedDetail(assignment, assignmentDir, resolve16(projectsDir, projectSlug)),
5700
6185
  created: assignment.created,
5701
6186
  updated: assignment.updated,
5702
6187
  body: assignment.body,
@@ -5787,7 +6272,7 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir) {
5787
6272
  slug: a.slug,
5788
6273
  title: a.title,
5789
6274
  projectSlug: rec.summary.slug,
5790
- assignmentDir: resolve15(rec.projectPath, "assignments", a.slug)
6275
+ assignmentDir: resolve16(rec.projectPath, "assignments", a.slug)
5791
6276
  });
5792
6277
  }
5793
6278
  }
@@ -5820,14 +6305,14 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir) {
5820
6305
  }
5821
6306
  async function countMentionsInAssignment(sourceDir, target) {
5822
6307
  const bodies = [];
5823
- const assignmentMd = resolve15(sourceDir, "assignment.md");
6308
+ const assignmentMd = resolve16(sourceDir, "assignment.md");
5824
6309
  if (await fileExists(assignmentMd)) {
5825
6310
  const content = await readFile12(assignmentMd, "utf-8");
5826
6311
  const todosMatch = content.match(/^## Todos\s*$([\s\S]*?)(?=^## |$(?![\r\n]))/m);
5827
6312
  if (todosMatch) bodies.push(todosMatch[1]);
5828
6313
  }
5829
6314
  for (const filename of ["progress.md", "comments.md", "handoff.md"]) {
5830
- const path = resolve15(sourceDir, filename);
6315
+ const path = resolve16(sourceDir, filename);
5831
6316
  if (await fileExists(path)) {
5832
6317
  try {
5833
6318
  bodies.push(await readFile12(path, "utf-8"));
@@ -5888,45 +6373,45 @@ async function getAssignmentDetailById(projectsDir, assignmentsDir, id) {
5888
6373
  }
5889
6374
  async function buildStandaloneAssignmentDetail(resolved) {
5890
6375
  const assignmentDir = resolved.assignmentDir;
5891
- const assignmentMdPath = resolve15(assignmentDir, "assignment.md");
6376
+ const assignmentMdPath = resolve16(assignmentDir, "assignment.md");
5892
6377
  if (!await fileExists(assignmentMdPath)) return null;
5893
6378
  const assignmentContent = await readFile12(assignmentMdPath, "utf-8");
5894
6379
  const assignment = parseAssignmentFull(assignmentContent);
5895
6380
  let plan = null;
5896
6381
  const planFile = await latestPlanFile(assignmentDir);
5897
6382
  if (planFile) {
5898
- const planPath = resolve15(assignmentDir, planFile);
6383
+ const planPath = resolve16(assignmentDir, planFile);
5899
6384
  if (await fileExists(planPath)) {
5900
6385
  const parsed = parsePlan(await readFile12(planPath, "utf-8"));
5901
6386
  plan = { status: parsed.status, updated: parsed.updated, body: parsed.body };
5902
6387
  }
5903
6388
  }
5904
6389
  let scratchpad = null;
5905
- const scratchpadPath = resolve15(assignmentDir, "scratchpad.md");
6390
+ const scratchpadPath = resolve16(assignmentDir, "scratchpad.md");
5906
6391
  if (await fileExists(scratchpadPath)) {
5907
6392
  const parsed = parseScratchpad(await readFile12(scratchpadPath, "utf-8"));
5908
6393
  scratchpad = { updated: parsed.updated, body: parsed.body };
5909
6394
  }
5910
6395
  let handoff = null;
5911
- const handoffPath = resolve15(assignmentDir, "handoff.md");
6396
+ const handoffPath = resolve16(assignmentDir, "handoff.md");
5912
6397
  if (await fileExists(handoffPath)) {
5913
6398
  const parsed = parseHandoff(await readFile12(handoffPath, "utf-8"));
5914
6399
  handoff = { updated: parsed.updated, handoffCount: parsed.handoffCount, body: parsed.body };
5915
6400
  }
5916
6401
  let decisionRecord = null;
5917
- const decisionRecordPath = resolve15(assignmentDir, "decision-record.md");
6402
+ const decisionRecordPath = resolve16(assignmentDir, "decision-record.md");
5918
6403
  if (await fileExists(decisionRecordPath)) {
5919
6404
  const parsed = parseDecisionRecord(await readFile12(decisionRecordPath, "utf-8"));
5920
6405
  decisionRecord = { updated: parsed.updated, decisionCount: parsed.decisionCount, body: parsed.body };
5921
6406
  }
5922
6407
  let progress = null;
5923
- const progressPath = resolve15(assignmentDir, "progress.md");
6408
+ const progressPath = resolve16(assignmentDir, "progress.md");
5924
6409
  if (await fileExists(progressPath)) {
5925
6410
  const parsed = parseProgress(await readFile12(progressPath, "utf-8"));
5926
6411
  progress = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
5927
6412
  }
5928
6413
  let comments = null;
5929
- const commentsPath = resolve15(assignmentDir, "comments.md");
6414
+ const commentsPath = resolve16(assignmentDir, "comments.md");
5930
6415
  if (await fileExists(commentsPath)) {
5931
6416
  const parsed = parseComments(await readFile12(commentsPath, "utf-8"));
5932
6417
  comments = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
@@ -5992,8 +6477,8 @@ async function computeProjectRecords(projectsDir, traces) {
5992
6477
  const projectDirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."));
5993
6478
  const maybeRecords = await Promise.all(
5994
6479
  projectDirs.map(async (entry) => {
5995
- const projectPath = resolve15(projectsDir, entry.name);
5996
- const projectMdPath = resolve15(projectPath, "project.md");
6480
+ const projectPath = resolve16(projectsDir, entry.name);
6481
+ const projectMdPath = resolve16(projectPath, "project.md");
5997
6482
  if (!await fileExists(projectMdPath)) {
5998
6483
  return null;
5999
6484
  }
@@ -6040,7 +6525,7 @@ async function computeProjectRecords(projectsDir, traces) {
6040
6525
  return records;
6041
6526
  }
6042
6527
  async function listAssignmentRecords(projectPath, traces) {
6043
- const assignmentsDir = resolve15(projectPath, "assignments");
6528
+ const assignmentsDir = resolve16(projectPath, "assignments");
6044
6529
  if (!await fileExists(assignmentsDir)) {
6045
6530
  return [];
6046
6531
  }
@@ -6048,7 +6533,7 @@ async function listAssignmentRecords(projectPath, traces) {
6048
6533
  const dirEntries = entries.filter((entry) => entry.isDirectory());
6049
6534
  const maybeRecords = await Promise.all(
6050
6535
  dirEntries.map(async (entry) => {
6051
- const assignmentMd = resolve15(assignmentsDir, entry.name, "assignment.md");
6536
+ const assignmentMd = resolve16(assignmentsDir, entry.name, "assignment.md");
6052
6537
  if (!await fileExists(assignmentMd)) {
6053
6538
  return null;
6054
6539
  }
@@ -6064,7 +6549,7 @@ async function listAssignmentRecords(projectPath, traces) {
6064
6549
  return records;
6065
6550
  }
6066
6551
  async function loadDependencyGraph(projectPath, assignments) {
6067
- const statusPath = resolve15(projectPath, "_status.md");
6552
+ const statusPath = resolve16(projectPath, "_status.md");
6068
6553
  if (await fileExists(statusPath)) {
6069
6554
  const statusContent = await readFile12(statusPath, "utf-8");
6070
6555
  const parsed = parseStatus(statusContent);
@@ -6239,7 +6724,7 @@ async function getAvailableTransitions(projectsDir, projectSlug, assignmentSlug,
6239
6724
  const config = await getStatusConfig();
6240
6725
  const transitionDefs = getTransitionDefinitions(config);
6241
6726
  const actions = [];
6242
- const projectPath = resolve15(projectsDir, projectSlug);
6727
+ const projectPath = resolve16(projectsDir, projectSlug);
6243
6728
  const traces = options?.traces;
6244
6729
  for (const definition of transitionDefs) {
6245
6730
  const target = getTargetStatus(assignment.status, definition.command, config.transitionTable);
@@ -6287,7 +6772,7 @@ async function getUnmetDependencies(projectPath, dependsOn, terminalStatuses, de
6287
6772
  continue;
6288
6773
  }
6289
6774
  }
6290
- const dependencyPath = resolve15(projectPath, "assignments", dependency, "assignment.md");
6775
+ const dependencyPath = resolve16(projectPath, "assignments", dependency, "assignment.md");
6291
6776
  if (!await fileExists(dependencyPath)) {
6292
6777
  unmet.push(`${dependency} (missing)`);
6293
6778
  continue;
@@ -6308,7 +6793,7 @@ function parseTimestamp(timestamp) {
6308
6793
  return Number.isFinite(parsed) ? parsed : 0;
6309
6794
  }
6310
6795
  async function countOpenQuestions(projectPath, assignmentSlug) {
6311
- const commentsPath = resolve15(
6796
+ const commentsPath = resolve16(
6312
6797
  projectPath,
6313
6798
  "assignments",
6314
6799
  assignmentSlug,
@@ -6673,7 +7158,7 @@ init_agents_schema();
6673
7158
  init_cwd();
6674
7159
  import { spawn as spawn2 } from "child_process";
6675
7160
  import { mkdir as mkdir2, writeFile as writeFile4 } from "fs/promises";
6676
- import { isAbsolute as isAbsolute3, resolve as resolve16 } from "path";
7161
+ import { isAbsolute as isAbsolute3, resolve as resolve17 } from "path";
6677
7162
  init_paths();
6678
7163
  init_playbooks();
6679
7164
  init_cwd();
@@ -6910,7 +7395,7 @@ async function resolveSessionPlan(input, terminal) {
6910
7395
  // src/launch/execute.ts
6911
7396
  import { spawn as spawn3 } from "child_process";
6912
7397
  import { homedir as homedir5 } from "os";
6913
- 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";
6914
7399
 
6915
7400
  // src/utils/terminal-probe.ts
6916
7401
  import { spawnSync as spawnSync2 } from "child_process";
@@ -6988,7 +7473,7 @@ init_config2();
6988
7473
 
6989
7474
  // src/utils/session-id.ts
6990
7475
  import { execFileSync as execFileSync2 } from "child_process";
6991
- import { mkdirSync, readFileSync, statSync as statSync2, writeFileSync } from "fs";
7476
+ import { mkdirSync, readFileSync as readFileSync2, statSync as statSync3, writeFileSync } from "fs";
6992
7477
  import { homedir as homedir4 } from "os";
6993
7478
  import { dirname as dirname3, join as join5 } from "path";
6994
7479
 
@@ -7072,7 +7557,7 @@ async function executeLaunchPlan(plan, spawnFn = realSpawn) {
7072
7557
  `Spawn failed: ${msg}. Verify the terminal is installed and on PATH.`
7073
7558
  );
7074
7559
  }
7075
- await new Promise((resolve19, reject) => {
7560
+ await new Promise((resolve20, reject) => {
7076
7561
  let settled = false;
7077
7562
  let stderr = "";
7078
7563
  const finishOk = () => {
@@ -7082,7 +7567,7 @@ async function executeLaunchPlan(plan, spawnFn = realSpawn) {
7082
7567
  child.unref();
7083
7568
  } catch {
7084
7569
  }
7085
- resolve19();
7570
+ resolve20();
7086
7571
  };
7087
7572
  const finishErr = (remediation) => {
7088
7573
  if (settled) return;
@@ -7143,7 +7628,7 @@ async function registerLaunchAtBirth(plan, child) {
7143
7628
  markerDir
7144
7629
  );
7145
7630
  if (!sessionId) return;
7146
- 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"))) {
7147
7632
  return;
7148
7633
  }
7149
7634
  const { initSessionDb: initSessionDb2 } = await Promise.resolve().then(() => (init_session_db(), session_db_exports));
@@ -7319,8 +7804,8 @@ function appleScriptString(value) {
7319
7804
  init_paths();
7320
7805
  init_fs();
7321
7806
  import { fileURLToPath } from "url";
7322
- import { dirname as dirname4, resolve as resolve18, join as join7 } from "path";
7323
- 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";
7324
7809
  var NPX_PATTERNS = [
7325
7810
  { kind: "npm", re: /\/_npx\/([^/]+)\/node_modules(?:\/|$)/ },
7326
7811
  { kind: "pnpm", re: /\/pnpm\/dlx\/([^/]+)\/node_modules(?:\/|$)/ },
@@ -7345,7 +7830,7 @@ function normalizeSlashes(p) {
7345
7830
  }
7346
7831
  function detectInstallKind(scriptUrl, opts = {}) {
7347
7832
  const realpath = opts.realpath ?? realpathSync.native;
7348
- const readFile13 = opts.readFile ?? ((p) => readFileSync2(p, "utf-8"));
7833
+ const readFile13 = opts.readFile ?? ((p) => readFileSync3(p, "utf-8"));
7349
7834
  const ua = opts.envUserAgent !== void 0 ? opts.envUserAgent : process.env.npm_config_user_agent ?? "";
7350
7835
  const resolved = resolveScriptPath(scriptUrl, realpath);
7351
7836
  if (resolved === null) {
@@ -7398,7 +7883,7 @@ function extractNpxHash(scriptUrl, opts = {}) {
7398
7883
  return null;
7399
7884
  }
7400
7885
  function nudgeStampDir() {
7401
- return resolve18(syntaurRoot(), "npx-handler-nudge");
7886
+ return resolve19(syntaurRoot(), "npx-handler-nudge");
7402
7887
  }
7403
7888
  function sanitizeHash(hash) {
7404
7889
  return hash.replace(/[^A-Za-z0-9_-]/g, "_") || "_";