syntaur 0.2.0 → 0.3.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 (87) hide show
  1. package/dashboard/dist/assets/{_basePickBy-CHKX1r7P.js → _basePickBy-BhaCV7eH.js} +1 -1
  2. package/dashboard/dist/assets/{_baseUniq-CTxTc4MS.js → _baseUniq-CDPcqrs2.js} +1 -1
  3. package/dashboard/dist/assets/{arc-BUo5zftd.js → arc-BP0RxLwl.js} +1 -1
  4. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-CrJLm-P0.js → architectureDiagram-2XIMDMQ5-BDzvaeJp.js} +1 -1
  5. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-BK60lBBJ.js → blockDiagram-WCTKOSBZ-ZeL9mROo.js} +1 -1
  6. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-C7oJEvA0.js → c4Diagram-IC4MRINW-7S5bvFLp.js} +1 -1
  7. package/dashboard/dist/assets/channel-CcB_wcgb.js +1 -0
  8. package/dashboard/dist/assets/{chunk-4BX2VUAB-CjUPlzHz.js → chunk-4BX2VUAB-Ca7R4nv5.js} +1 -1
  9. package/dashboard/dist/assets/{chunk-55IACEB6-6HmWguiO.js → chunk-55IACEB6-flEv13FB.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-FMBD7UC4-CLuJnd1b.js → chunk-FMBD7UC4-CfcYWBM6.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-JSJVCQXG-B4d62qWV.js → chunk-JSJVCQXG-Dw4yL0VS.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-KX2RTZJC-AsEKRPq2.js → chunk-KX2RTZJC-B2cDe40G.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-NQ4KR5QH-DQhHHvwY.js → chunk-NQ4KR5QH-LZVm0IWg.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-QZHKN3VN-Ds1TtI3E.js → chunk-QZHKN3VN-Dg0EeHNI.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-WL4C6EOR-C7jE3-cR.js → chunk-WL4C6EOR-v3rXNwXc.js} +1 -1
  16. package/dashboard/dist/assets/classDiagram-VBA2DB6C-BJr38z2g.js +1 -0
  17. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-BJr38z2g.js +1 -0
  18. package/dashboard/dist/assets/clone-Cfs2GUGt.js +1 -0
  19. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-C9ka5v1m.js → cose-bilkent-S5V4N54A-D-3JzLoS.js} +1 -1
  20. package/dashboard/dist/assets/{dagre-KLK3FWXG-BbgPQBKy.js → dagre-KLK3FWXG-d_mbczhU.js} +1 -1
  21. package/dashboard/dist/assets/{diagram-E7M64L7V-DpdeZFD4.js → diagram-E7M64L7V-BUyAp8pW.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-IFDJBPK2-FlHLQzOV.js → diagram-IFDJBPK2-C8doXcyQ.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-P4PSJMXO-B22NkEF_.js → diagram-P4PSJMXO-BUSmHa55.js} +1 -1
  24. package/dashboard/dist/assets/{erDiagram-INFDFZHY-zSqmtDid.js → erDiagram-INFDFZHY-Bn5_0LPU.js} +1 -1
  25. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-BP_0XmVV.js → flowDiagram-PKNHOUZH-CnEjerQM.js} +1 -1
  26. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-8uRyYgZV.js → ganttDiagram-A5KZAMGK-CL94fbyy.js} +1 -1
  27. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-JFqg8sv4.js → gitGraphDiagram-K3NZZRJ6-4i_PeG8V.js} +1 -1
  28. package/dashboard/dist/assets/{graph-a-PAH599.js → graph-BtoFhoAd.js} +1 -1
  29. package/dashboard/dist/assets/index-DZUGYrvE.css +1 -0
  30. package/dashboard/dist/assets/index-Dv_-SxuL.js +481 -0
  31. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-C3kq7Nbv.js → infoDiagram-LFFYTUFH-CdUsuNgZ.js} +1 -1
  32. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-Kqi4EZ-n.js → ishikawaDiagram-PHBUUO56-BjggRlUx.js} +1 -1
  33. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-CTfv0Wcr.js → journeyDiagram-4ABVD52K-V4AgexlR.js} +1 -1
  34. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-Dmx0lgvR.js → kanban-definition-K7BYSVSG-ChlylQRf.js} +1 -1
  35. package/dashboard/dist/assets/{layout-KKRbT2Od.js → layout-DLcz9AmA.js} +1 -1
  36. package/dashboard/dist/assets/{linear-5egaBiw7.js → linear-l2xnSHze.js} +1 -1
  37. package/dashboard/dist/assets/{mermaid.core-C9pF_oFQ.js → mermaid.core-DKO1ytRW.js} +4 -4
  38. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-C7HXYEXt.js → mindmap-definition-YRQLILUH-DTmTPHrT.js} +1 -1
  39. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-DkdZm-YP.js → pieDiagram-SKSYHLDU-CwK80y8Y.js} +1 -1
  40. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-DkcRJs5F.js → quadrantDiagram-337W2JSQ-Be1xqW_w.js} +1 -1
  41. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-BaTDVYTl.js → requirementDiagram-Z7DCOOCP-JcspXCs0.js} +1 -1
  42. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-DvPLbGV5.js → sankeyDiagram-WA2Y5GQK-nJb1BInq.js} +1 -1
  43. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-DQoZ2xMK.js → sequenceDiagram-2WXFIKYE-DUrclEgA.js} +1 -1
  44. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-CS4l0OjM.js → stateDiagram-RAJIS63D-CjinnNtF.js} +1 -1
  45. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-yfclw-nM.js +1 -0
  46. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-aC0iCFCW.js → timeline-definition-YZTLITO2-kM-oVLNz.js} +1 -1
  47. package/dashboard/dist/assets/{treemap-KZPCXAKY-Ie-PFjgx.js → treemap-KZPCXAKY-CYziFlrQ.js} +1 -1
  48. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-CJN3ExTQ.js → vennDiagram-LZ73GAT5-DX0DbxBN.js} +1 -1
  49. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-DSiDu1CN.js → xychartDiagram-JWTSCODW-BGqM42ZM.js} +1 -1
  50. package/dashboard/dist/index.html +2 -2
  51. package/dist/dashboard/server.d.ts +5 -0
  52. package/dist/dashboard/server.js +2579 -1185
  53. package/dist/dashboard/server.js.map +1 -1
  54. package/dist/index.js +2092 -650
  55. package/dist/index.js.map +1 -1
  56. package/examples/playbooks/keep-records-updated.md +14 -8
  57. package/examples/playbooks/read-before-plan.md +8 -5
  58. package/examples/sample-project/_status.md +1 -1
  59. package/examples/sample-project/assignments/design-auth-schema/assignment.md +4 -17
  60. package/examples/sample-project/assignments/design-auth-schema/comments.md +26 -0
  61. package/examples/sample-project/assignments/design-auth-schema/progress.md +20 -0
  62. package/examples/sample-project/assignments/implement-jwt-middleware/assignment.md +4 -17
  63. package/examples/sample-project/assignments/implement-jwt-middleware/comments.md +17 -0
  64. package/examples/sample-project/assignments/implement-jwt-middleware/progress.md +20 -0
  65. package/examples/sample-project/assignments/write-auth-tests/assignment.md +4 -8
  66. package/examples/sample-project/assignments/write-auth-tests/comments.md +10 -0
  67. package/examples/sample-project/assignments/write-auth-tests/progress.md +10 -0
  68. package/package.json +1 -1
  69. package/platforms/claude-code/agents/syntaur-expert.md +40 -12
  70. package/platforms/claude-code/references/file-ownership.md +15 -3
  71. package/platforms/claude-code/references/protocol-summary.md +19 -5
  72. package/platforms/claude-code/skills/complete-assignment/SKILL.md +14 -0
  73. package/platforms/claude-code/skills/create-assignment/SKILL.md +12 -10
  74. package/platforms/claude-code/skills/syntaur-protocol/SKILL.md +21 -11
  75. package/platforms/codex/agents/syntaur-operator.md +33 -21
  76. package/platforms/codex/references/file-ownership.md +14 -3
  77. package/platforms/codex/references/protocol-summary.md +19 -5
  78. package/platforms/codex/skills/complete-assignment/SKILL.md +1 -0
  79. package/platforms/codex/skills/create-assignment/SKILL.md +13 -8
  80. package/platforms/codex/skills/syntaur-protocol/SKILL.md +26 -13
  81. package/dashboard/dist/assets/channel-DdltvFFH.js +0 -1
  82. package/dashboard/dist/assets/classDiagram-VBA2DB6C-BHqdFE-8.js +0 -1
  83. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-BHqdFE-8.js +0 -1
  84. package/dashboard/dist/assets/clone-CBJOOeOm.js +0 -1
  85. package/dashboard/dist/assets/index-CoVCLSh2.css +0 -1
  86. package/dashboard/dist/assets/index-yyAIuzrP.js +0 -471
  87. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-DkBtE1WJ.js +0 -1
package/dist/index.js CHANGED
@@ -302,6 +302,58 @@ function parseDecisionRecord(fileContent) {
302
302
  body
303
303
  };
304
304
  }
305
+ function parseComments(fileContent) {
306
+ const [fm, body] = extractFrontmatter(fileContent);
307
+ const entries = [];
308
+ const sections = body.split(/^## /m).slice(1);
309
+ for (const section of sections) {
310
+ const newlineIdx = section.indexOf("\n");
311
+ if (newlineIdx === -1) continue;
312
+ const id = section.slice(0, newlineIdx).trim();
313
+ const rest = section.slice(newlineIdx + 1);
314
+ const headerMatch = rest.match(
315
+ /^\s*\*\*Recorded:\*\*\s*(.*)\n\*\*Author:\*\*\s*(.*)\n\*\*Type:\*\*\s*(question|note|feedback)(?:\n\*\*Reply to:\*\*\s*(.*))?(?:\n\*\*Resolved:\*\*\s*(true|false))?\n+([\s\S]*)$/
316
+ );
317
+ if (!headerMatch) continue;
318
+ const [, timestamp, author, type, replyTo, resolvedStr, entryBody] = headerMatch;
319
+ const entry = {
320
+ id,
321
+ timestamp: timestamp.trim(),
322
+ author: author.trim(),
323
+ type,
324
+ body: entryBody.trim()
325
+ };
326
+ if (replyTo) entry.replyTo = replyTo.trim();
327
+ if (resolvedStr) entry.resolved = resolvedStr === "true";
328
+ entries.push(entry);
329
+ }
330
+ return {
331
+ assignment: getField(fm, "assignment") ?? "",
332
+ entryCount: parseInt(getField(fm, "entryCount") ?? "0", 10),
333
+ updated: getField(fm, "updated") ?? "",
334
+ entries,
335
+ body
336
+ };
337
+ }
338
+ function parseProgress(fileContent) {
339
+ const [fm, body] = extractFrontmatter(fileContent);
340
+ const entries = [];
341
+ const sections = body.split(/^## /m).slice(1);
342
+ for (const section of sections) {
343
+ const newlineIdx = section.indexOf("\n");
344
+ if (newlineIdx === -1) continue;
345
+ const timestamp = section.slice(0, newlineIdx).trim();
346
+ const entryBody = section.slice(newlineIdx + 1).trim();
347
+ entries.push({ timestamp, body: entryBody });
348
+ }
349
+ return {
350
+ assignment: getField(fm, "assignment") ?? "",
351
+ entryCount: parseInt(getField(fm, "entryCount") ?? "0", 10),
352
+ updated: getField(fm, "updated") ?? "",
353
+ entries,
354
+ body
355
+ };
356
+ }
305
357
  function parseResource(fileContent) {
306
358
  const [fm, body] = extractFrontmatter(fileContent);
307
359
  return {
@@ -1206,6 +1258,74 @@ var init_lifecycle = __esm({
1206
1258
  }
1207
1259
  });
1208
1260
 
1261
+ // src/utils/assignment-resolver.ts
1262
+ import { resolve as resolve8 } from "path";
1263
+ import { readdir as readdir3, readFile as readFile5 } from "fs/promises";
1264
+ async function resolveAssignmentById(projectsDir2, assignmentsDir2, id) {
1265
+ let standaloneMatch = null;
1266
+ let projectMatch = null;
1267
+ const standaloneDir = resolve8(assignmentsDir2, id);
1268
+ const standalonePath = resolve8(standaloneDir, "assignment.md");
1269
+ if (await fileExists(standalonePath)) {
1270
+ standaloneMatch = {
1271
+ assignmentDir: standaloneDir,
1272
+ projectSlug: null,
1273
+ assignmentSlug: id,
1274
+ id,
1275
+ standalone: true
1276
+ };
1277
+ }
1278
+ if (await fileExists(projectsDir2)) {
1279
+ try {
1280
+ const projects = await readdir3(projectsDir2, { withFileTypes: true });
1281
+ for (const p of projects) {
1282
+ if (!p.isDirectory()) continue;
1283
+ if (p.name.startsWith(".") || p.name.startsWith("_")) continue;
1284
+ const assignmentsPath = resolve8(projectsDir2, p.name, "assignments");
1285
+ if (!await fileExists(assignmentsPath)) continue;
1286
+ const entries = await readdir3(assignmentsPath, { withFileTypes: true });
1287
+ for (const a of entries) {
1288
+ if (!a.isDirectory()) continue;
1289
+ const aPath = resolve8(assignmentsPath, a.name, "assignment.md");
1290
+ if (!await fileExists(aPath)) continue;
1291
+ try {
1292
+ const content = await readFile5(aPath, "utf-8");
1293
+ const [fm] = extractFrontmatter(content);
1294
+ const fileId = getField(fm, "id");
1295
+ if (fileId === id) {
1296
+ projectMatch = {
1297
+ assignmentDir: resolve8(assignmentsPath, a.name),
1298
+ projectSlug: p.name,
1299
+ assignmentSlug: a.name,
1300
+ id,
1301
+ standalone: false
1302
+ };
1303
+ break;
1304
+ }
1305
+ } catch {
1306
+ }
1307
+ }
1308
+ if (projectMatch) break;
1309
+ }
1310
+ } catch {
1311
+ }
1312
+ }
1313
+ if (standaloneMatch && projectMatch) {
1314
+ console.warn(
1315
+ `Duplicate assignment ID ${id} found in both standalone and project-nested locations; using standalone`
1316
+ );
1317
+ return standaloneMatch;
1318
+ }
1319
+ return standaloneMatch ?? projectMatch ?? null;
1320
+ }
1321
+ var init_assignment_resolver = __esm({
1322
+ "src/utils/assignment-resolver.ts"() {
1323
+ "use strict";
1324
+ init_fs();
1325
+ init_parser();
1326
+ }
1327
+ });
1328
+
1209
1329
  // src/dashboard/help.ts
1210
1330
  async function buildStatusGuide() {
1211
1331
  const config = await getStatusConfig();
@@ -1622,8 +1742,8 @@ var init_help = __esm({
1622
1742
  });
1623
1743
 
1624
1744
  // src/dashboard/servers.ts
1625
- import { readdir as readdir3, readFile as readFile5, unlink } from "fs/promises";
1626
- import { resolve as resolve8 } from "path";
1745
+ import { readdir as readdir4, readFile as readFile6, unlink } from "fs/promises";
1746
+ import { resolve as resolve9 } from "path";
1627
1747
  function sanitizeSessionName(name) {
1628
1748
  return name.replace(/[^a-zA-Z0-9_-]/g, "-");
1629
1749
  }
@@ -1671,18 +1791,18 @@ async function registerSession(dir, rawName) {
1671
1791
  lastRefreshed: now,
1672
1792
  overrides: {}
1673
1793
  });
1674
- await writeFileForce(resolve8(dir, `${name}.md`), content);
1794
+ await writeFileForce(resolve9(dir, `${name}.md`), content);
1675
1795
  return name;
1676
1796
  }
1677
1797
  async function listSessionFiles(dir) {
1678
1798
  if (!await fileExists(dir)) return [];
1679
- const entries = await readdir3(dir);
1799
+ const entries = await readdir4(dir);
1680
1800
  return entries.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, ""));
1681
1801
  }
1682
1802
  async function readSessionFile(dir, name) {
1683
- const filePath = resolve8(dir, `${sanitizeSessionName(name)}.md`);
1803
+ const filePath = resolve9(dir, `${sanitizeSessionName(name)}.md`);
1684
1804
  if (!await fileExists(filePath)) return null;
1685
- const raw = await readFile5(filePath, "utf-8");
1805
+ const raw = await readFile6(filePath, "utf-8");
1686
1806
  const [frontmatter] = extractFrontmatter(raw);
1687
1807
  if (!frontmatter) return null;
1688
1808
  const session = getField(frontmatter, "session") ?? name;
@@ -1722,7 +1842,7 @@ async function readSessionFile(dir, name) {
1722
1842
  };
1723
1843
  }
1724
1844
  async function removeSession(dir, name) {
1725
- const filePath = resolve8(dir, `${sanitizeSessionName(name)}.md`);
1845
+ const filePath = resolve9(dir, `${sanitizeSessionName(name)}.md`);
1726
1846
  if (await fileExists(filePath)) {
1727
1847
  await unlink(filePath);
1728
1848
  }
@@ -1731,7 +1851,7 @@ async function updateLastRefreshed(dir, name) {
1731
1851
  const data = await readSessionFile(dir, name);
1732
1852
  if (!data) return;
1733
1853
  const content = buildSessionContent({ ...data, lastRefreshed: nowTimestamp2() });
1734
- await writeFileForce(resolve8(dir, `${sanitizeSessionName(name)}.md`), content);
1854
+ await writeFileForce(resolve9(dir, `${sanitizeSessionName(name)}.md`), content);
1735
1855
  }
1736
1856
  async function setOverride(dir, sessionName, windowIndex, paneIndex, assignment) {
1737
1857
  const data = await readSessionFile(dir, sessionName);
@@ -1743,7 +1863,7 @@ async function setOverride(dir, sessionName, windowIndex, paneIndex, assignment)
1743
1863
  delete data.overrides[key];
1744
1864
  }
1745
1865
  const content = buildSessionContent({ ...data });
1746
- await writeFileForce(resolve8(dir, `${sanitizeSessionName(sessionName)}.md`), content);
1866
+ await writeFileForce(resolve9(dir, `${sanitizeSessionName(sessionName)}.md`), content);
1747
1867
  }
1748
1868
  async function registerAutoSession(dir, rawName, opts) {
1749
1869
  const name = sanitizeSessionName(rawName);
@@ -1760,7 +1880,7 @@ async function registerAutoSession(dir, rawName, opts) {
1760
1880
  ports: opts.ports,
1761
1881
  cwd: opts.cwd
1762
1882
  });
1763
- await writeFileForce(resolve8(dir, `${name}.md`), content);
1883
+ await writeFileForce(resolve9(dir, `${name}.md`), content);
1764
1884
  return name;
1765
1885
  }
1766
1886
  var init_servers = __esm({
@@ -1792,8 +1912,8 @@ __export(scanner_exports, {
1792
1912
  });
1793
1913
  import { execFile } from "child_process";
1794
1914
  import { promisify } from "util";
1795
- import { resolve as resolve9 } from "path";
1796
- import { realpath, readdir as readdir4, readFile as readFile6 } from "fs/promises";
1915
+ import { resolve as resolve10 } from "path";
1916
+ import { realpath, readdir as readdir5, readFile as readFile7 } from "fs/promises";
1797
1917
  function clearScanCache() {
1798
1918
  cache = null;
1799
1919
  }
@@ -1888,8 +2008,8 @@ async function getGitInfo(cwd) {
1888
2008
  let isWorktree = false;
1889
2009
  if (commonDir && gitDir && commonDir !== gitDir) {
1890
2010
  try {
1891
- const resolvedCommon = await realpath(resolve9(cwd, commonDir));
1892
- const resolvedGit = await realpath(resolve9(cwd, gitDir));
2011
+ const resolvedCommon = await realpath(resolve10(cwd, commonDir));
2012
+ const resolvedGit = await realpath(resolve10(cwd, gitDir));
1893
2013
  isWorktree = resolvedCommon !== resolvedGit;
1894
2014
  } catch {
1895
2015
  isWorktree = false;
@@ -1897,22 +2017,22 @@ async function getGitInfo(cwd) {
1897
2017
  }
1898
2018
  return { branch: branch || null, worktree: isWorktree };
1899
2019
  }
1900
- async function loadWorkspaceRecords(projectsDir2) {
2020
+ async function loadWorkspaceRecords(projectsDir2, assignmentsDir2) {
1901
2021
  const records = [];
1902
2022
  try {
1903
2023
  const projects = await listProjects(projectsDir2);
1904
2024
  for (const project of projects) {
1905
- const assignmentsDir2 = resolve9(projectsDir2, project.slug, "assignments");
2025
+ const projectAssignmentsDir = resolve10(projectsDir2, project.slug, "assignments");
1906
2026
  let slugs;
1907
2027
  try {
1908
- slugs = await readdir4(assignmentsDir2);
2028
+ slugs = await readdir5(projectAssignmentsDir);
1909
2029
  } catch {
1910
2030
  continue;
1911
2031
  }
1912
2032
  for (const aslug of slugs) {
1913
- const aFile = resolve9(assignmentsDir2, aslug, "assignment.md");
2033
+ const aFile = resolve10(projectAssignmentsDir, aslug, "assignment.md");
1914
2034
  try {
1915
- const raw = await readFile6(aFile, "utf-8");
2035
+ const raw = await readFile7(aFile, "utf-8");
1916
2036
  const [fm] = extractFrontmatter(raw);
1917
2037
  if (!fm) continue;
1918
2038
  records.push({
@@ -1929,6 +2049,30 @@ async function loadWorkspaceRecords(projectsDir2) {
1929
2049
  }
1930
2050
  } catch {
1931
2051
  }
2052
+ if (assignmentsDir2) {
2053
+ try {
2054
+ const entries = await readdir5(assignmentsDir2);
2055
+ for (const id of entries) {
2056
+ if (id.startsWith(".") || id.startsWith("_")) continue;
2057
+ const aFile = resolve10(assignmentsDir2, id, "assignment.md");
2058
+ try {
2059
+ const raw = await readFile7(aFile, "utf-8");
2060
+ const [fm] = extractFrontmatter(raw);
2061
+ if (!fm) continue;
2062
+ records.push({
2063
+ projectSlug: null,
2064
+ assignmentSlug: id,
2065
+ assignmentTitle: getField(fm, "title") ?? id,
2066
+ worktreePath: getNestedField(fm, "workspace", "worktreePath") ?? null,
2067
+ branch: getNestedField(fm, "workspace", "branch") ?? null
2068
+ });
2069
+ } catch {
2070
+ continue;
2071
+ }
2072
+ }
2073
+ } catch {
2074
+ }
2075
+ }
1932
2076
  return records;
1933
2077
  }
1934
2078
  async function resolveAndNormalize(p) {
@@ -2121,7 +2265,7 @@ async function scanAllSessions(serversDir2, projectsDir2, options) {
2121
2265
  const tmuxAvailable = await checkTmuxAvailable();
2122
2266
  const names = await listSessionFiles(serversDir2);
2123
2267
  const lsofOutput = await getLsofOutput();
2124
- const workspaceRecords = await loadWorkspaceRecords(projectsDir2);
2268
+ const workspaceRecords = await loadWorkspaceRecords(projectsDir2, options?.assignmentsDir);
2125
2269
  const sessions = [];
2126
2270
  for (const name of names) {
2127
2271
  const data = await readSessionFile(serversDir2, name);
@@ -2136,11 +2280,11 @@ async function scanAllSessions(serversDir2, projectsDir2, options) {
2136
2280
  cache = { data: result, expiry: Date.now() + CACHE_TTL_MS };
2137
2281
  return result;
2138
2282
  }
2139
- async function scanSingleSession(serversDir2, projectsDir2, name) {
2283
+ async function scanSingleSession(serversDir2, projectsDir2, name, options) {
2140
2284
  const data = await readSessionFile(serversDir2, name);
2141
2285
  if (!data) return null;
2142
2286
  const lsofOutput = await getLsofOutput();
2143
- const workspaceRecords = await loadWorkspaceRecords(projectsDir2);
2287
+ const workspaceRecords = await loadWorkspaceRecords(projectsDir2, options?.assignmentsDir);
2144
2288
  if (data.kind === "process") {
2145
2289
  return scanProcessSession(data, lsofOutput, workspaceRecords);
2146
2290
  }
@@ -2160,8 +2304,28 @@ var init_scanner = __esm({
2160
2304
  });
2161
2305
 
2162
2306
  // src/dashboard/api.ts
2163
- import { readdir as readdir5, readFile as readFile7, writeFile as writeFile2 } from "fs/promises";
2164
- import { resolve as resolve10, dirname as dirname3 } from "path";
2307
+ import { readdir as readdir6, readFile as readFile8, writeFile as writeFile2 } from "fs/promises";
2308
+ import { resolve as resolve11, dirname as dirname3 } from "path";
2309
+ async function listStandaloneRecords(assignmentsDir2) {
2310
+ if (!assignmentsDir2) return [];
2311
+ if (!await fileExists(assignmentsDir2)) return [];
2312
+ const entries = await readdir6(assignmentsDir2, { withFileTypes: true });
2313
+ const records = [];
2314
+ for (const entry of entries) {
2315
+ if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
2316
+ const assignmentDir = resolve11(assignmentsDir2, entry.name);
2317
+ const assignmentMdPath = resolve11(assignmentDir, "assignment.md");
2318
+ if (!await fileExists(assignmentMdPath)) continue;
2319
+ try {
2320
+ const content = await readFile8(assignmentMdPath, "utf-8");
2321
+ const record = parseAssignmentFull(content);
2322
+ records.push({ assignmentDir, id: entry.name, record });
2323
+ } catch {
2324
+ }
2325
+ }
2326
+ records.sort((left, right) => compareTimestamps(right.record.updated, left.record.updated));
2327
+ return records;
2328
+ }
2165
2329
  function toTitleCase(s) {
2166
2330
  return s.replace(/_/g, " ").replace(/\b\w/g, (c2) => c2.toUpperCase());
2167
2331
  }
@@ -2222,9 +2386,9 @@ async function listProjects(projectsDir2) {
2222
2386
  return projectRecords.map((record) => record.summary);
2223
2387
  }
2224
2388
  async function readWorkspaceRegistry(projectsDir2) {
2225
- const registryPath = resolve10(dirname3(projectsDir2), "workspaces.json");
2389
+ const registryPath = resolve11(dirname3(projectsDir2), "workspaces.json");
2226
2390
  try {
2227
- const raw = await readFile7(registryPath, "utf-8");
2391
+ const raw = await readFile8(registryPath, "utf-8");
2228
2392
  const parsed = JSON.parse(raw);
2229
2393
  return Array.isArray(parsed) ? parsed.filter((w) => typeof w === "string") : [];
2230
2394
  } catch {
@@ -2232,7 +2396,7 @@ async function readWorkspaceRegistry(projectsDir2) {
2232
2396
  }
2233
2397
  }
2234
2398
  async function writeWorkspaceRegistry(projectsDir2, workspaces) {
2235
- const registryPath = resolve10(dirname3(projectsDir2), "workspaces.json");
2399
+ const registryPath = resolve11(dirname3(projectsDir2), "workspaces.json");
2236
2400
  await writeFile2(registryPath, JSON.stringify(workspaces, null, 2) + "\n", "utf-8");
2237
2401
  }
2238
2402
  async function listWorkspaces(projectsDir2) {
@@ -2265,15 +2429,16 @@ async function deleteWorkspace(projectsDir2, name) {
2265
2429
  const filtered = registered.filter((w) => w !== name);
2266
2430
  await writeWorkspaceRegistry(projectsDir2, filtered);
2267
2431
  }
2268
- async function getOverview(projectsDir2, serversDir2) {
2432
+ async function getOverview(projectsDir2, serversDir2, assignmentsDir2) {
2269
2433
  const projectRecords = await listProjectRecords(projectsDir2);
2270
- const attention = buildAttentionItems(projectRecords);
2271
- const recentActivity = buildRecentActivity(projectRecords);
2434
+ const standaloneRecords = await listStandaloneRecords(assignmentsDir2);
2435
+ const attention = buildAttentionItems(projectRecords, standaloneRecords);
2436
+ const recentActivity = buildRecentActivity(projectRecords, standaloneRecords);
2272
2437
  let serverStats;
2273
2438
  if (serversDir2) {
2274
2439
  try {
2275
2440
  const { scanAllSessions: scanAllSessions2 } = await Promise.resolve().then(() => (init_scanner(), scanner_exports));
2276
- const servers = await scanAllSessions2(serversDir2, projectsDir2);
2441
+ const servers = await scanAllSessions2(serversDir2, projectsDir2, { assignmentsDir: assignmentsDir2 });
2277
2442
  if (servers.tmuxAvailable) {
2278
2443
  const alive = servers.sessions.filter((s) => s.alive).length;
2279
2444
  const totalPorts = servers.sessions.reduce((sum, s) => sum + s.windows.reduce((ws, w) => ws + w.panes.reduce((ps, p) => ps + p.ports.length, 0), 0), 0);
@@ -2289,7 +2454,7 @@ async function getOverview(projectsDir2, serversDir2) {
2289
2454
  }
2290
2455
  return {
2291
2456
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2292
- firstRun: projectRecords.length === 0,
2457
+ firstRun: projectRecords.length === 0 && standaloneRecords.length === 0,
2293
2458
  stats: {
2294
2459
  activeProjects: projectRecords.filter((record) => record.summary.status === "active").length,
2295
2460
  inProgressAssignments: projectRecords.reduce(
@@ -2311,7 +2476,7 @@ async function getOverview(projectsDir2, serversDir2) {
2311
2476
  staleAssignments: projectRecords.reduce(
2312
2477
  (total, record) => total + record.assignments.filter((assignment) => isStale(assignment.updated)).length,
2313
2478
  0
2314
- )
2479
+ ) + standaloneRecords.filter((sr) => isStale(sr.record.updated)).length
2315
2480
  },
2316
2481
  attention: attention.slice(0, OVERVIEW_ATTENTION_LIMIT),
2317
2482
  recentProjects: projectRecords.map((record) => record.summary).sort((left, right) => compareTimestamps(right.updated, left.updated)).slice(0, RECENT_PROJECTS_LIMIT),
@@ -2319,13 +2484,14 @@ async function getOverview(projectsDir2, serversDir2) {
2319
2484
  serverStats
2320
2485
  };
2321
2486
  }
2322
- async function getAttention(projectsDir2, serversDir2) {
2487
+ async function getAttention(projectsDir2, serversDir2, assignmentsDir2) {
2323
2488
  const projectRecords = await listProjectRecords(projectsDir2);
2324
- const items = buildAttentionItems(projectRecords);
2489
+ const standaloneRecords = await listStandaloneRecords(assignmentsDir2);
2490
+ const items = buildAttentionItems(projectRecords, standaloneRecords);
2325
2491
  if (serversDir2) {
2326
2492
  try {
2327
2493
  const { scanAllSessions: scanAllSessions2 } = await Promise.resolve().then(() => (init_scanner(), scanner_exports));
2328
- const servers = await scanAllSessions2(serversDir2, projectsDir2);
2494
+ const servers = await scanAllSessions2(serversDir2, projectsDir2, { assignmentsDir: assignmentsDir2 });
2329
2495
  for (const session of servers.sessions) {
2330
2496
  if (!session.alive) {
2331
2497
  items.push({
@@ -2369,9 +2535,9 @@ async function getAttention(projectsDir2, serversDir2) {
2369
2535
  items: pagedItems
2370
2536
  };
2371
2537
  }
2372
- async function listAssignmentsBoard(projectsDir2) {
2538
+ async function listAssignmentsBoard(projectsDir2, assignmentsDir2) {
2373
2539
  const projectRecords = await listProjectRecords(projectsDir2);
2374
- const assignments = await Promise.all(
2540
+ const projectItems = await Promise.all(
2375
2541
  projectRecords.flatMap(
2376
2542
  async (record) => Promise.all(
2377
2543
  record.assignments.map(
@@ -2380,11 +2546,48 @@ async function listAssignmentsBoard(projectsDir2) {
2380
2546
  )
2381
2547
  )
2382
2548
  );
2549
+ const standaloneRecords = await listStandaloneRecords(assignmentsDir2);
2550
+ const standaloneItems = await Promise.all(
2551
+ standaloneRecords.map(async (sr) => toStandaloneBoardItem(sr))
2552
+ );
2383
2553
  return {
2384
2554
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2385
- assignments: assignments.flat().sort((left, right) => compareTimestamps(right.updated, left.updated))
2555
+ assignments: [...projectItems.flat(), ...standaloneItems].sort((left, right) => compareTimestamps(right.updated, left.updated))
2556
+ };
2557
+ }
2558
+ async function toStandaloneBoardItem(sr) {
2559
+ return {
2560
+ ...toAssignmentSummary(sr.record),
2561
+ projectSlug: null,
2562
+ projectTitle: null,
2563
+ blockedReason: sr.record.blockedReason,
2564
+ projectWorkspace: null,
2565
+ availableTransitions: await getStandaloneAvailableTransitions(sr.record)
2386
2566
  };
2387
2567
  }
2568
+ async function getStandaloneAvailableTransitions(assignment) {
2569
+ const config = await getStatusConfig();
2570
+ const transitionDefs = getTransitionDefinitions(config);
2571
+ const actions = [];
2572
+ for (const definition of transitionDefs) {
2573
+ let warning = null;
2574
+ if (definition.command === "start" && !assignment.assignee) {
2575
+ warning = "No assignee set \u2014 consider assigning before starting.";
2576
+ }
2577
+ const target = getTargetStatus(assignment.status, definition.command, config.transitionTable);
2578
+ actions.push({
2579
+ command: definition.command,
2580
+ label: definition.label,
2581
+ description: definition.description,
2582
+ targetStatus: target ?? definition.command,
2583
+ disabled: false,
2584
+ disabledReason: null,
2585
+ warning,
2586
+ requiresReason: definition.requiresReason
2587
+ });
2588
+ }
2589
+ return actions;
2590
+ }
2388
2591
  async function getHelp() {
2389
2592
  return getDashboardHelp();
2390
2593
  }
@@ -2393,7 +2596,7 @@ async function getEditableDocument(projectsDir2, documentType, projectSlug, assi
2393
2596
  if (!filePath || !await fileExists(filePath)) {
2394
2597
  return null;
2395
2598
  }
2396
- const content = await readFile7(filePath, "utf-8");
2599
+ const content = await readFile8(filePath, "utf-8");
2397
2600
  const title = getEditableDocumentTitle(documentType, projectSlug, assignmentSlug);
2398
2601
  return {
2399
2602
  documentType,
@@ -2404,16 +2607,44 @@ async function getEditableDocument(projectsDir2, documentType, projectSlug, assi
2404
2607
  appendOnly: documentType === "handoff" || documentType === "decision-record"
2405
2608
  };
2406
2609
  }
2610
+ async function getEditableDocumentById(projectsDir2, assignmentsDir2, documentType, id) {
2611
+ const resolved = await resolveAssignmentById(projectsDir2, assignmentsDir2, id);
2612
+ if (!resolved) return null;
2613
+ if (!resolved.standalone && resolved.projectSlug) {
2614
+ return getEditableDocument(
2615
+ projectsDir2,
2616
+ documentType,
2617
+ resolved.projectSlug,
2618
+ resolved.assignmentSlug
2619
+ );
2620
+ }
2621
+ const fileName = documentType === "assignment" ? "assignment.md" : documentType === "plan" ? "plan.md" : documentType === "scratchpad" ? "scratchpad.md" : documentType === "handoff" ? "handoff.md" : documentType === "decision-record" ? "decision-record.md" : null;
2622
+ if (!fileName) return null;
2623
+ const filePath = resolve11(resolved.assignmentDir, fileName);
2624
+ if (!await fileExists(filePath)) return null;
2625
+ const content = await readFile8(filePath, "utf-8");
2626
+ const label = resolved.id;
2627
+ const title = documentType === "assignment" ? `Edit Assignment: ${label}` : documentType === "plan" ? `Edit Plan: ${label}` : documentType === "scratchpad" ? `Edit Scratchpad: ${label}` : documentType === "handoff" ? `Append Handoff: ${label}` : `Append Decision: ${label}`;
2628
+ return {
2629
+ documentType,
2630
+ title,
2631
+ content,
2632
+ projectSlug: null,
2633
+ assignmentSlug: void 0,
2634
+ assignmentId: resolved.id,
2635
+ appendOnly: documentType === "handoff" || documentType === "decision-record"
2636
+ };
2637
+ }
2407
2638
  async function getProjectDetail(projectsDir2, slug) {
2408
- const projectPath = resolve10(projectsDir2, slug);
2409
- const projectMdPath = resolve10(projectPath, "project.md");
2639
+ const projectPath = resolve11(projectsDir2, slug);
2640
+ const projectMdPath = resolve11(projectPath, "project.md");
2410
2641
  if (!await fileExists(projectMdPath)) {
2411
2642
  return null;
2412
2643
  }
2413
- const projectContent = await readFile7(projectMdPath, "utf-8");
2644
+ const projectContent = await readFile8(projectMdPath, "utf-8");
2414
2645
  const project = parseProject(projectContent);
2415
2646
  const assignments = await listAssignmentRecords(projectPath);
2416
- const rollup = buildProjectRollup(project, assignments);
2647
+ const rollup = await buildProjectRollup(projectPath, project, assignments);
2417
2648
  const dependencyGraph = await loadDependencyGraph(projectPath, assignments);
2418
2649
  const resources = await listResources(projectPath);
2419
2650
  const memories = await listMemories(projectPath);
@@ -2440,17 +2671,17 @@ async function getProjectDetail(projectsDir2, slug) {
2440
2671
  };
2441
2672
  }
2442
2673
  async function getAssignmentDetail(projectsDir2, projectSlug, assignmentSlug) {
2443
- const assignmentDir = resolve10(projectsDir2, projectSlug, "assignments", assignmentSlug);
2444
- const assignmentMdPath = resolve10(assignmentDir, "assignment.md");
2674
+ const assignmentDir = resolve11(projectsDir2, projectSlug, "assignments", assignmentSlug);
2675
+ const assignmentMdPath = resolve11(assignmentDir, "assignment.md");
2445
2676
  if (!await fileExists(assignmentMdPath)) {
2446
2677
  return null;
2447
2678
  }
2448
- const assignmentContent = await readFile7(assignmentMdPath, "utf-8");
2679
+ const assignmentContent = await readFile8(assignmentMdPath, "utf-8");
2449
2680
  const assignment = parseAssignmentFull(assignmentContent);
2450
2681
  let plan = null;
2451
- const planPath = resolve10(assignmentDir, "plan.md");
2682
+ const planPath = resolve11(assignmentDir, "plan.md");
2452
2683
  if (await fileExists(planPath)) {
2453
- const planContent = await readFile7(planPath, "utf-8");
2684
+ const planContent = await readFile8(planPath, "utf-8");
2454
2685
  const parsed = parsePlan(planContent);
2455
2686
  plan = {
2456
2687
  status: parsed.status,
@@ -2459,9 +2690,9 @@ async function getAssignmentDetail(projectsDir2, projectSlug, assignmentSlug) {
2459
2690
  };
2460
2691
  }
2461
2692
  let scratchpad = null;
2462
- const scratchpadPath = resolve10(assignmentDir, "scratchpad.md");
2693
+ const scratchpadPath = resolve11(assignmentDir, "scratchpad.md");
2463
2694
  if (await fileExists(scratchpadPath)) {
2464
- const scratchpadContent = await readFile7(scratchpadPath, "utf-8");
2695
+ const scratchpadContent = await readFile8(scratchpadPath, "utf-8");
2465
2696
  const parsed = parseScratchpad(scratchpadContent);
2466
2697
  scratchpad = {
2467
2698
  updated: parsed.updated,
@@ -2469,9 +2700,9 @@ async function getAssignmentDetail(projectsDir2, projectSlug, assignmentSlug) {
2469
2700
  };
2470
2701
  }
2471
2702
  let handoff = null;
2472
- const handoffPath = resolve10(assignmentDir, "handoff.md");
2703
+ const handoffPath = resolve11(assignmentDir, "handoff.md");
2473
2704
  if (await fileExists(handoffPath)) {
2474
- const handoffContent = await readFile7(handoffPath, "utf-8");
2705
+ const handoffContent = await readFile8(handoffPath, "utf-8");
2475
2706
  const parsed = parseHandoff(handoffContent);
2476
2707
  handoff = {
2477
2708
  updated: parsed.updated,
@@ -2480,9 +2711,9 @@ async function getAssignmentDetail(projectsDir2, projectSlug, assignmentSlug) {
2480
2711
  };
2481
2712
  }
2482
2713
  let decisionRecord = null;
2483
- const decisionRecordPath = resolve10(assignmentDir, "decision-record.md");
2714
+ const decisionRecordPath = resolve11(assignmentDir, "decision-record.md");
2484
2715
  if (await fileExists(decisionRecordPath)) {
2485
- const decisionRecordContent = await readFile7(decisionRecordPath, "utf-8");
2716
+ const decisionRecordContent = await readFile8(decisionRecordPath, "utf-8");
2486
2717
  const parsed = parseDecisionRecord(decisionRecordContent);
2487
2718
  decisionRecord = {
2488
2719
  updated: parsed.updated,
@@ -2490,6 +2721,28 @@ async function getAssignmentDetail(projectsDir2, projectSlug, assignmentSlug) {
2490
2721
  body: parsed.body
2491
2722
  };
2492
2723
  }
2724
+ let progress = null;
2725
+ const progressPath = resolve11(assignmentDir, "progress.md");
2726
+ if (await fileExists(progressPath)) {
2727
+ const progressContent = await readFile8(progressPath, "utf-8");
2728
+ const parsed = parseProgress(progressContent);
2729
+ progress = {
2730
+ updated: parsed.updated,
2731
+ entryCount: parsed.entryCount,
2732
+ entries: parsed.entries
2733
+ };
2734
+ }
2735
+ let comments = null;
2736
+ const commentsPath = resolve11(assignmentDir, "comments.md");
2737
+ if (await fileExists(commentsPath)) {
2738
+ const commentsContent = await readFile8(commentsPath, "utf-8");
2739
+ const parsed = parseComments(commentsContent);
2740
+ comments = {
2741
+ updated: parsed.updated,
2742
+ entryCount: parsed.entryCount,
2743
+ entries: parsed.entries
2744
+ };
2745
+ }
2493
2746
  const detail = {
2494
2747
  id: assignment.id,
2495
2748
  projectSlug,
@@ -2513,6 +2766,9 @@ async function getAssignmentDetail(projectsDir2, projectSlug, assignmentSlug) {
2513
2766
  scratchpad,
2514
2767
  handoff,
2515
2768
  decisionRecord,
2769
+ progress,
2770
+ comments,
2771
+ referencedBy: [],
2516
2772
  availableTransitions: await getAvailableTransitions(
2517
2773
  projectsDir2,
2518
2774
  projectSlug,
@@ -2576,25 +2832,212 @@ async function getAssignmentDetail(projectsDir2, projectSlug, assignmentSlug) {
2576
2832
  });
2577
2833
  }
2578
2834
  detail.enrichedLinks = enrichedLinks;
2835
+ detail.referencedBy = await computeReferencedBy(
2836
+ { id: assignment.id, projectSlug, slug: detail.slug },
2837
+ projectsDir2,
2838
+ void 0
2839
+ );
2840
+ return detail;
2841
+ }
2842
+ async function computeReferencedBy(target, projectsDir2, assignmentsDir2) {
2843
+ const sources = [];
2844
+ const projectRecords = await listProjectRecords(projectsDir2);
2845
+ for (const rec of projectRecords) {
2846
+ for (const a of rec.assignments) {
2847
+ sources.push({
2848
+ id: a.id,
2849
+ slug: a.slug,
2850
+ title: a.title,
2851
+ projectSlug: rec.summary.slug,
2852
+ assignmentDir: resolve11(rec.projectPath, "assignments", a.slug)
2853
+ });
2854
+ }
2855
+ }
2856
+ const standaloneRecords = await listStandaloneRecords(assignmentsDir2);
2857
+ for (const sr of standaloneRecords) {
2858
+ sources.push({
2859
+ id: sr.id,
2860
+ slug: sr.record.slug || sr.id,
2861
+ title: sr.record.title,
2862
+ projectSlug: null,
2863
+ assignmentDir: sr.assignmentDir
2864
+ });
2865
+ }
2866
+ const references = [];
2867
+ for (const source of sources) {
2868
+ if (source.id === target.id) continue;
2869
+ const mentions = await countMentionsInAssignment(source.assignmentDir, target);
2870
+ if (mentions > 0) {
2871
+ references.push({
2872
+ sourceId: source.id,
2873
+ sourceSlug: source.slug,
2874
+ sourceTitle: source.title,
2875
+ sourceProjectSlug: source.projectSlug,
2876
+ mentions
2877
+ });
2878
+ }
2879
+ if (references.length >= REFERENCED_BY_LIMIT) break;
2880
+ }
2881
+ return references.slice(0, REFERENCED_BY_LIMIT);
2882
+ }
2883
+ async function countMentionsInAssignment(sourceDir, target) {
2884
+ const bodies = [];
2885
+ const assignmentMd = resolve11(sourceDir, "assignment.md");
2886
+ if (await fileExists(assignmentMd)) {
2887
+ const content = await readFile8(assignmentMd, "utf-8");
2888
+ const todosMatch = content.match(/^## Todos\s*$([\s\S]*?)(?=^## |$(?![\r\n]))/m);
2889
+ if (todosMatch) bodies.push(todosMatch[1]);
2890
+ }
2891
+ for (const filename of ["progress.md", "comments.md", "handoff.md"]) {
2892
+ const path = resolve11(sourceDir, filename);
2893
+ if (await fileExists(path)) {
2894
+ try {
2895
+ bodies.push(await readFile8(path, "utf-8"));
2896
+ } catch {
2897
+ }
2898
+ }
2899
+ }
2900
+ let total = 0;
2901
+ const patterns = buildLinkPatternsForTarget(target);
2902
+ for (const body of bodies) {
2903
+ for (const pattern of patterns) {
2904
+ const matches = body.match(pattern);
2905
+ if (matches) total += matches.length;
2906
+ }
2907
+ }
2908
+ return total;
2909
+ }
2910
+ function buildLinkPatternsForTarget(target) {
2911
+ const patterns = [];
2912
+ patterns.push(new RegExp(`/assignments/${escapeRegExpLocal(target.id)}(?:/|\\b)`, "g"));
2913
+ if (target.projectSlug) {
2914
+ patterns.push(
2915
+ new RegExp(
2916
+ `/projects/${escapeRegExpLocal(target.projectSlug)}/assignments/${escapeRegExpLocal(target.slug)}(?:/|\\b)`,
2917
+ "g"
2918
+ )
2919
+ );
2920
+ patterns.push(
2921
+ new RegExp(`\\.\\./${escapeRegExpLocal(target.slug)}(?:/|\\b)`, "g")
2922
+ );
2923
+ }
2924
+ return patterns;
2925
+ }
2926
+ function escapeRegExpLocal(value) {
2927
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2928
+ }
2929
+ async function getAssignmentDetailById(projectsDir2, assignmentsDir2, id) {
2930
+ const resolved = await resolveAssignmentById(projectsDir2, assignmentsDir2, id);
2931
+ if (!resolved) return null;
2932
+ if (!resolved.standalone && resolved.projectSlug) {
2933
+ const detail = await getAssignmentDetail(projectsDir2, resolved.projectSlug, resolved.assignmentSlug);
2934
+ if (!detail) return null;
2935
+ detail.referencedBy = await computeReferencedBy(
2936
+ { id: detail.id, projectSlug: detail.projectSlug, slug: detail.slug },
2937
+ projectsDir2,
2938
+ assignmentsDir2
2939
+ );
2940
+ return detail;
2941
+ }
2942
+ const standaloneDetail = await buildStandaloneAssignmentDetail(resolved);
2943
+ if (!standaloneDetail) return null;
2944
+ standaloneDetail.referencedBy = await computeReferencedBy(
2945
+ { id: standaloneDetail.id, projectSlug: null, slug: standaloneDetail.slug },
2946
+ projectsDir2,
2947
+ assignmentsDir2
2948
+ );
2949
+ return standaloneDetail;
2950
+ }
2951
+ async function buildStandaloneAssignmentDetail(resolved) {
2952
+ const assignmentDir = resolved.assignmentDir;
2953
+ const assignmentMdPath = resolve11(assignmentDir, "assignment.md");
2954
+ if (!await fileExists(assignmentMdPath)) return null;
2955
+ const assignmentContent = await readFile8(assignmentMdPath, "utf-8");
2956
+ const assignment = parseAssignmentFull(assignmentContent);
2957
+ let plan = null;
2958
+ const planPath = resolve11(assignmentDir, "plan.md");
2959
+ if (await fileExists(planPath)) {
2960
+ const parsed = parsePlan(await readFile8(planPath, "utf-8"));
2961
+ plan = { status: parsed.status, updated: parsed.updated, body: parsed.body };
2962
+ }
2963
+ let scratchpad = null;
2964
+ const scratchpadPath = resolve11(assignmentDir, "scratchpad.md");
2965
+ if (await fileExists(scratchpadPath)) {
2966
+ const parsed = parseScratchpad(await readFile8(scratchpadPath, "utf-8"));
2967
+ scratchpad = { updated: parsed.updated, body: parsed.body };
2968
+ }
2969
+ let handoff = null;
2970
+ const handoffPath = resolve11(assignmentDir, "handoff.md");
2971
+ if (await fileExists(handoffPath)) {
2972
+ const parsed = parseHandoff(await readFile8(handoffPath, "utf-8"));
2973
+ handoff = { updated: parsed.updated, handoffCount: parsed.handoffCount, body: parsed.body };
2974
+ }
2975
+ let decisionRecord = null;
2976
+ const decisionRecordPath = resolve11(assignmentDir, "decision-record.md");
2977
+ if (await fileExists(decisionRecordPath)) {
2978
+ const parsed = parseDecisionRecord(await readFile8(decisionRecordPath, "utf-8"));
2979
+ decisionRecord = { updated: parsed.updated, decisionCount: parsed.decisionCount, body: parsed.body };
2980
+ }
2981
+ let progress = null;
2982
+ const progressPath = resolve11(assignmentDir, "progress.md");
2983
+ if (await fileExists(progressPath)) {
2984
+ const parsed = parseProgress(await readFile8(progressPath, "utf-8"));
2985
+ progress = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
2986
+ }
2987
+ let comments = null;
2988
+ const commentsPath = resolve11(assignmentDir, "comments.md");
2989
+ if (await fileExists(commentsPath)) {
2990
+ const parsed = parseComments(await readFile8(commentsPath, "utf-8"));
2991
+ comments = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
2992
+ }
2993
+ const detail = {
2994
+ id: assignment.id,
2995
+ projectSlug: null,
2996
+ slug: assignment.slug || resolved.id,
2997
+ title: assignment.title,
2998
+ status: assignment.status,
2999
+ priority: assignment.priority,
3000
+ assignee: assignment.assignee,
3001
+ dependsOn: [],
3002
+ // standalone cannot declare dependencies
3003
+ links: [],
3004
+ reverseLinks: [],
3005
+ enrichedLinks: [],
3006
+ blockedReason: assignment.blockedReason,
3007
+ workspace: assignment.workspace,
3008
+ externalIds: assignment.externalIds,
3009
+ tags: assignment.tags,
3010
+ created: assignment.created,
3011
+ updated: assignment.updated,
3012
+ body: assignment.body,
3013
+ plan,
3014
+ scratchpad,
3015
+ handoff,
3016
+ decisionRecord,
3017
+ progress,
3018
+ comments,
3019
+ referencedBy: [],
3020
+ availableTransitions: await getStandaloneAvailableTransitions(assignment)
3021
+ };
2579
3022
  return detail;
2580
3023
  }
2581
3024
  async function listProjectRecords(projectsDir2) {
2582
3025
  if (!await fileExists(projectsDir2)) {
2583
3026
  return [];
2584
3027
  }
2585
- const entries = await readdir5(projectsDir2, { withFileTypes: true });
3028
+ const entries = await readdir6(projectsDir2, { withFileTypes: true });
2586
3029
  const projectDirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."));
2587
3030
  const records = [];
2588
3031
  for (const entry of projectDirs) {
2589
- const projectPath = resolve10(projectsDir2, entry.name);
2590
- const projectMdPath = resolve10(projectPath, "project.md");
3032
+ const projectPath = resolve11(projectsDir2, entry.name);
3033
+ const projectMdPath = resolve11(projectPath, "project.md");
2591
3034
  if (!await fileExists(projectMdPath)) {
2592
3035
  continue;
2593
3036
  }
2594
- const projectContent = await readFile7(projectMdPath, "utf-8");
3037
+ const projectContent = await readFile8(projectMdPath, "utf-8");
2595
3038
  const project = parseProject(projectContent);
2596
3039
  const assignments = await listAssignmentRecords(projectPath);
2597
- const rollup = buildProjectRollup(project, assignments);
3040
+ const rollup = await buildProjectRollup(projectPath, project, assignments);
2598
3041
  const updated = getProjectActivityTimestamp(project.updated, assignments);
2599
3042
  records.push({
2600
3043
  projectPath,
@@ -2622,39 +3065,39 @@ async function listProjectRecords(projectsDir2) {
2622
3065
  return records;
2623
3066
  }
2624
3067
  async function listAssignmentRecords(projectPath) {
2625
- const assignmentsDir2 = resolve10(projectPath, "assignments");
3068
+ const assignmentsDir2 = resolve11(projectPath, "assignments");
2626
3069
  if (!await fileExists(assignmentsDir2)) {
2627
3070
  return [];
2628
3071
  }
2629
- const entries = await readdir5(assignmentsDir2, { withFileTypes: true });
3072
+ const entries = await readdir6(assignmentsDir2, { withFileTypes: true });
2630
3073
  const records = [];
2631
3074
  for (const entry of entries) {
2632
3075
  if (!entry.isDirectory()) {
2633
3076
  continue;
2634
3077
  }
2635
- const assignmentMd = resolve10(assignmentsDir2, entry.name, "assignment.md");
3078
+ const assignmentMd = resolve11(assignmentsDir2, entry.name, "assignment.md");
2636
3079
  if (!await fileExists(assignmentMd)) {
2637
3080
  continue;
2638
3081
  }
2639
- const content = await readFile7(assignmentMd, "utf-8");
3082
+ const content = await readFile8(assignmentMd, "utf-8");
2640
3083
  records.push(parseAssignmentFull(content));
2641
3084
  }
2642
3085
  records.sort((left, right) => compareTimestamps(right.updated, left.updated));
2643
3086
  return records;
2644
3087
  }
2645
3088
  async function listResources(projectPath) {
2646
- const resourcesDir = resolve10(projectPath, "resources");
3089
+ const resourcesDir = resolve11(projectPath, "resources");
2647
3090
  if (!await fileExists(resourcesDir)) {
2648
3091
  return [];
2649
3092
  }
2650
- const entries = await readdir5(resourcesDir, { withFileTypes: true });
3093
+ const entries = await readdir6(resourcesDir, { withFileTypes: true });
2651
3094
  const results = [];
2652
3095
  for (const entry of entries) {
2653
3096
  if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_")) {
2654
3097
  continue;
2655
3098
  }
2656
- const filePath = resolve10(resourcesDir, entry.name);
2657
- const content = await readFile7(filePath, "utf-8");
3099
+ const filePath = resolve11(resourcesDir, entry.name);
3100
+ const content = await readFile8(filePath, "utf-8");
2658
3101
  const parsed = parseResource(content);
2659
3102
  results.push({
2660
3103
  name: parsed.name,
@@ -2669,18 +3112,18 @@ async function listResources(projectPath) {
2669
3112
  return results;
2670
3113
  }
2671
3114
  async function listMemories(projectPath) {
2672
- const memoriesDir = resolve10(projectPath, "memories");
3115
+ const memoriesDir = resolve11(projectPath, "memories");
2673
3116
  if (!await fileExists(memoriesDir)) {
2674
3117
  return [];
2675
3118
  }
2676
- const entries = await readdir5(memoriesDir, { withFileTypes: true });
3119
+ const entries = await readdir6(memoriesDir, { withFileTypes: true });
2677
3120
  const results = [];
2678
3121
  for (const entry of entries) {
2679
3122
  if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_")) {
2680
3123
  continue;
2681
3124
  }
2682
- const filePath = resolve10(memoriesDir, entry.name);
2683
- const content = await readFile7(filePath, "utf-8");
3125
+ const filePath = resolve11(memoriesDir, entry.name);
3126
+ const content = await readFile8(filePath, "utf-8");
2684
3127
  const parsed = parseMemory(content);
2685
3128
  results.push({
2686
3129
  name: parsed.name,
@@ -2695,9 +3138,9 @@ async function listMemories(projectPath) {
2695
3138
  return results;
2696
3139
  }
2697
3140
  async function loadDependencyGraph(projectPath, assignments) {
2698
- const statusPath = resolve10(projectPath, "_status.md");
3141
+ const statusPath = resolve11(projectPath, "_status.md");
2699
3142
  if (await fileExists(statusPath)) {
2700
- const statusContent = await readFile7(statusPath, "utf-8");
3143
+ const statusContent = await readFile8(statusPath, "utf-8");
2701
3144
  const parsed = parseStatus(statusContent);
2702
3145
  const derivedGraph = extractMermaidGraph(parsed.body);
2703
3146
  if (derivedGraph) {
@@ -2706,13 +3149,13 @@ async function loadDependencyGraph(projectPath, assignments) {
2706
3149
  }
2707
3150
  return buildDependencyGraph(assignments);
2708
3151
  }
2709
- function buildProjectRollup(project, assignments) {
3152
+ async function buildProjectRollup(projectPath, project, assignments) {
2710
3153
  const progress = { total: assignments.length };
2711
3154
  let openQuestions = 0;
2712
3155
  for (const assignment of assignments) {
2713
3156
  const s = assignment.status;
2714
3157
  progress[s] = (progress[s] ?? 0) + 1;
2715
- openQuestions += countPendingAnswers(assignment.body);
3158
+ openQuestions += await countOpenQuestions(projectPath, assignment.slug);
2716
3159
  }
2717
3160
  const needsAttention = {
2718
3161
  blockedCount: progress["blocked"] ?? 0,
@@ -2797,7 +3240,7 @@ async function getAvailableTransitions(projectsDir2, projectSlug, assignmentSlug
2797
3240
  const config = await getStatusConfig();
2798
3241
  const transitionDefs = getTransitionDefinitions(config);
2799
3242
  const actions = [];
2800
- const projectPath = resolve10(projectsDir2, projectSlug);
3243
+ const projectPath = resolve11(projectsDir2, projectSlug);
2801
3244
  for (const definition of transitionDefs) {
2802
3245
  let warning = null;
2803
3246
  if (definition.command === "start" && !assignment.assignee) {
@@ -2827,12 +3270,12 @@ async function getUnmetDependencies(projectPath, dependsOn, terminalStatuses3) {
2827
3270
  const terminals = terminalStatuses3 ?? /* @__PURE__ */ new Set(["completed"]);
2828
3271
  const unmet = [];
2829
3272
  for (const dependency of dependsOn) {
2830
- const dependencyPath = resolve10(projectPath, "assignments", dependency, "assignment.md");
3273
+ const dependencyPath = resolve11(projectPath, "assignments", dependency, "assignment.md");
2831
3274
  if (!await fileExists(dependencyPath)) {
2832
3275
  unmet.push(`${dependency} (missing)`);
2833
3276
  continue;
2834
3277
  }
2835
- const content = await readFile7(dependencyPath, "utf-8");
3278
+ const content = await readFile8(dependencyPath, "utf-8");
2836
3279
  const parsed = parseAssignmentFull(content);
2837
3280
  if (!terminals.has(parsed.status)) {
2838
3281
  unmet.push(`${dependency} (${parsed.status})`);
@@ -2840,7 +3283,7 @@ async function getUnmetDependencies(projectPath, dependsOn, terminalStatuses3) {
2840
3283
  }
2841
3284
  return unmet;
2842
3285
  }
2843
- function buildAttentionItems(projectRecords) {
3286
+ function buildAttentionItems(projectRecords, standaloneRecords = []) {
2844
3287
  const items = [];
2845
3288
  for (const record of projectRecords) {
2846
3289
  for (const assignment of record.assignments) {
@@ -2890,9 +3333,36 @@ function buildAttentionItems(projectRecords) {
2890
3333
  }
2891
3334
  }
2892
3335
  }
3336
+ for (const sr of standaloneRecords) {
3337
+ const assignment = sr.record;
3338
+ const stale = isStale(assignment.updated);
3339
+ const base = {
3340
+ projectSlug: null,
3341
+ projectTitle: null,
3342
+ assignmentSlug: assignment.slug || sr.id,
3343
+ assignmentTitle: assignment.title,
3344
+ status: assignment.status,
3345
+ updated: assignment.updated,
3346
+ href: `/assignments/${sr.id}`,
3347
+ blockedReason: assignment.blockedReason,
3348
+ stale
3349
+ };
3350
+ if (assignment.status === "failed") {
3351
+ items.push({ id: `standalone:${sr.id}:failed`, severity: "critical", reason: "Marked failed and needs a recovery decision.", ...base });
3352
+ }
3353
+ if (assignment.status === "blocked") {
3354
+ items.push({ id: `standalone:${sr.id}:blocked`, severity: "high", reason: assignment.blockedReason || "Blocked and waiting for intervention.", ...base });
3355
+ }
3356
+ if (assignment.status === "review") {
3357
+ items.push({ id: `standalone:${sr.id}:review`, severity: "medium", reason: "Ready for review.", ...base });
3358
+ }
3359
+ if (stale) {
3360
+ items.push({ id: `standalone:${sr.id}:stale`, severity: "low", reason: "No source updates have been recorded in the last 7 days.", ...base });
3361
+ }
3362
+ }
2893
3363
  return items.sort(compareAttentionItems);
2894
3364
  }
2895
- function buildRecentActivity(projectRecords) {
3365
+ function buildRecentActivity(projectRecords, standaloneRecords = []) {
2896
3366
  const activity = [];
2897
3367
  for (const record of projectRecords) {
2898
3368
  activity.push({
@@ -2920,6 +3390,20 @@ function buildRecentActivity(projectRecords) {
2920
3390
  });
2921
3391
  }
2922
3392
  }
3393
+ for (const sr of standaloneRecords) {
3394
+ const assignment = sr.record;
3395
+ activity.push({
3396
+ id: `standalone-assignment:${sr.id}`,
3397
+ type: "assignment",
3398
+ title: assignment.title,
3399
+ updated: assignment.updated,
3400
+ href: `/assignments/${sr.id}`,
3401
+ projectSlug: null,
3402
+ projectTitle: null,
3403
+ assignmentSlug: assignment.slug || sr.id,
3404
+ summary: `Standalone assignment is ${assignment.status} with ${assignment.priority} priority.`
3405
+ });
3406
+ }
2923
3407
  activity.sort((left, right) => compareTimestamps(right.updated, left.updated));
2924
3408
  return activity;
2925
3409
  }
@@ -2945,9 +3429,25 @@ function isStale(updated) {
2945
3429
  }
2946
3430
  return Date.now() - timestamp > STALE_ASSIGNMENT_MS;
2947
3431
  }
2948
- function countPendingAnswers(body) {
2949
- const matches = body.match(/^\*\*A:\*\*\s+pending\s*$/gim);
2950
- return matches ? matches.length : 0;
3432
+ async function countOpenQuestions(projectPath, assignmentSlug) {
3433
+ const commentsPath = resolve11(
3434
+ projectPath,
3435
+ "assignments",
3436
+ assignmentSlug,
3437
+ "comments.md"
3438
+ );
3439
+ if (!await fileExists(commentsPath)) {
3440
+ return 0;
3441
+ }
3442
+ try {
3443
+ const content = await readFile8(commentsPath, "utf-8");
3444
+ const parsed = parseComments(content);
3445
+ return parsed.entries.filter(
3446
+ (e) => e.type === "question" && e.resolved !== true
3447
+ ).length;
3448
+ } catch {
3449
+ return 0;
3450
+ }
2951
3451
  }
2952
3452
  function getProjectActivityTimestamp(projectUpdated, assignments) {
2953
3453
  let latest = projectUpdated;
@@ -2961,17 +3461,17 @@ function getProjectActivityTimestamp(projectUpdated, assignments) {
2961
3461
  function getDocumentPath(projectsDir2, documentType, projectSlug, assignmentSlug) {
2962
3462
  switch (documentType) {
2963
3463
  case "project":
2964
- return resolve10(projectsDir2, projectSlug, "project.md");
3464
+ return resolve11(projectsDir2, projectSlug, "project.md");
2965
3465
  case "assignment":
2966
- return assignmentSlug ? resolve10(projectsDir2, projectSlug, "assignments", assignmentSlug, "assignment.md") : null;
3466
+ return assignmentSlug ? resolve11(projectsDir2, projectSlug, "assignments", assignmentSlug, "assignment.md") : null;
2967
3467
  case "plan":
2968
- return assignmentSlug ? resolve10(projectsDir2, projectSlug, "assignments", assignmentSlug, "plan.md") : null;
3468
+ return assignmentSlug ? resolve11(projectsDir2, projectSlug, "assignments", assignmentSlug, "plan.md") : null;
2969
3469
  case "scratchpad":
2970
- return assignmentSlug ? resolve10(projectsDir2, projectSlug, "assignments", assignmentSlug, "scratchpad.md") : null;
3470
+ return assignmentSlug ? resolve11(projectsDir2, projectSlug, "assignments", assignmentSlug, "scratchpad.md") : null;
2971
3471
  case "handoff":
2972
- return assignmentSlug ? resolve10(projectsDir2, projectSlug, "assignments", assignmentSlug, "handoff.md") : null;
3472
+ return assignmentSlug ? resolve11(projectsDir2, projectSlug, "assignments", assignmentSlug, "handoff.md") : null;
2973
3473
  case "decision-record":
2974
- return assignmentSlug ? resolve10(projectsDir2, projectSlug, "assignments", assignmentSlug, "decision-record.md") : null;
3474
+ return assignmentSlug ? resolve11(projectsDir2, projectSlug, "assignments", assignmentSlug, "decision-record.md") : null;
2975
3475
  default:
2976
3476
  return null;
2977
3477
  }
@@ -2998,12 +3498,12 @@ function getEditableDocumentTitle(documentType, projectSlug, assignmentSlug) {
2998
3498
  }
2999
3499
  async function listPlaybooks(playbooksDir3) {
3000
3500
  if (!await fileExists(playbooksDir3)) return [];
3001
- const entries = await readdir5(playbooksDir3, { withFileTypes: true });
3501
+ const entries = await readdir6(playbooksDir3, { withFileTypes: true });
3002
3502
  const playbooks = [];
3003
3503
  for (const entry of entries) {
3004
3504
  if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_") || entry.name === "manifest.md") continue;
3005
- const filePath = resolve10(playbooksDir3, entry.name);
3006
- const raw = await readFile7(filePath, "utf-8");
3505
+ const filePath = resolve11(playbooksDir3, entry.name);
3506
+ const raw = await readFile8(filePath, "utf-8");
3007
3507
  const parsed = parsePlaybook(raw);
3008
3508
  const slug = parsed.slug || entry.name.replace(/\.md$/, "");
3009
3509
  playbooks.push({
@@ -3019,9 +3519,9 @@ async function listPlaybooks(playbooksDir3) {
3019
3519
  return playbooks.sort((a, b) => (b.updated || b.created).localeCompare(a.updated || a.created));
3020
3520
  }
3021
3521
  async function getPlaybookDetail(playbooksDir3, slug) {
3022
- const filePath = resolve10(playbooksDir3, `${slug}.md`);
3522
+ const filePath = resolve11(playbooksDir3, `${slug}.md`);
3023
3523
  if (!await fileExists(filePath)) return null;
3024
- const raw = await readFile7(filePath, "utf-8");
3524
+ const raw = await readFile8(filePath, "utf-8");
3025
3525
  const parsed = parsePlaybook(raw);
3026
3526
  return {
3027
3527
  slug: parsed.slug || slug,
@@ -3034,13 +3534,14 @@ async function getPlaybookDetail(playbooksDir3, slug) {
3034
3534
  body: parsed.body
3035
3535
  };
3036
3536
  }
3037
- var STALE_ASSIGNMENT_MS, ATTENTION_PAGE_LIMIT, OVERVIEW_ATTENTION_LIMIT, RECENT_PROJECTS_LIMIT, RECENT_ACTIVITY_LIMIT, DEFAULT_TRANSITION_DEFINITIONS, DEFAULT_STATUS_COLORS, _cachedConfig, DEFAULT_GRAPH_COLORS;
3537
+ var STALE_ASSIGNMENT_MS, ATTENTION_PAGE_LIMIT, OVERVIEW_ATTENTION_LIMIT, RECENT_PROJECTS_LIMIT, RECENT_ACTIVITY_LIMIT, DEFAULT_TRANSITION_DEFINITIONS, DEFAULT_STATUS_COLORS, _cachedConfig, REFERENCED_BY_LIMIT, DEFAULT_GRAPH_COLORS;
3038
3538
  var init_api = __esm({
3039
3539
  "src/dashboard/api.ts"() {
3040
3540
  "use strict";
3041
3541
  init_lifecycle();
3042
3542
  init_fs();
3043
3543
  init_config2();
3544
+ init_assignment_resolver();
3044
3545
  init_parser();
3045
3546
  init_help();
3046
3547
  STALE_ASSIGNMENT_MS = 7 * 24 * 60 * 60 * 1e3;
@@ -3101,6 +3602,7 @@ var init_api = __esm({
3101
3602
  failed: "rose"
3102
3603
  };
3103
3604
  _cachedConfig = null;
3605
+ REFERENCED_BY_LIMIT = 50;
3104
3606
  DEFAULT_GRAPH_COLORS = {
3105
3607
  completed: "fill:#4ea84f,stroke:#1f6b29,color:#ffffff",
3106
3608
  in_progress: "fill:#1e6fd9,stroke:#0f3f8f,color:#ffffff",
@@ -3133,8 +3635,8 @@ __export(parser_exports, {
3133
3635
  writeChecklist: () => writeChecklist
3134
3636
  });
3135
3637
  import { randomBytes } from "crypto";
3136
- import { readFile as readFile11 } from "fs/promises";
3137
- import { resolve as resolve16 } from "path";
3638
+ import { readFile as readFile12 } from "fs/promises";
3639
+ import { resolve as resolve17 } from "path";
3138
3640
  function generateShortId() {
3139
3641
  return randomBytes(2).toString("hex");
3140
3642
  }
@@ -3294,10 +3796,10 @@ function serializeLogEntry(entry) {
3294
3796
  return lines.join("\n");
3295
3797
  }
3296
3798
  function checklistPath(todosDir2, workspace) {
3297
- return resolve16(todosDir2, `${workspace}.md`);
3799
+ return resolve17(todosDir2, `${workspace}.md`);
3298
3800
  }
3299
3801
  function logPath(todosDir2, workspace) {
3300
- return resolve16(todosDir2, `${workspace}-log.md`);
3802
+ return resolve17(todosDir2, `${workspace}-log.md`);
3301
3803
  }
3302
3804
  function archivePath(todosDir2, workspace, interval, now = /* @__PURE__ */ new Date()) {
3303
3805
  const year = now.getFullYear();
@@ -3321,14 +3823,14 @@ function archivePath(todosDir2, workspace, interval, now = /* @__PURE__ */ new D
3321
3823
  default:
3322
3824
  suffix = `${year}-${month}-${day}`;
3323
3825
  }
3324
- return resolve16(todosDir2, "archive", `${workspace}-${suffix}.md`);
3826
+ return resolve17(todosDir2, "archive", `${workspace}-${suffix}.md`);
3325
3827
  }
3326
3828
  async function readChecklist(todosDir2, workspace) {
3327
3829
  const path = checklistPath(todosDir2, workspace);
3328
3830
  if (!await fileExists(path)) {
3329
3831
  return { workspace, archiveInterval: "weekly", items: [] };
3330
3832
  }
3331
- const content = await readFile11(path, "utf-8");
3833
+ const content = await readFile12(path, "utf-8");
3332
3834
  return parseChecklist(content);
3333
3835
  }
3334
3836
  async function writeChecklist(todosDir2, checklist) {
@@ -3341,7 +3843,7 @@ async function readLog(todosDir2, workspace) {
3341
3843
  if (!await fileExists(path)) {
3342
3844
  return { workspace, entries: [] };
3343
3845
  }
3344
- const content = await readFile11(path, "utf-8");
3846
+ const content = await readFile12(path, "utf-8");
3345
3847
  return parseLog(content);
3346
3848
  }
3347
3849
  async function appendLogEntry2(todosDir2, workspace, entry) {
@@ -3349,7 +3851,7 @@ async function appendLogEntry2(todosDir2, workspace, entry) {
3349
3851
  const path = logPath(todosDir2, workspace);
3350
3852
  let content;
3351
3853
  if (await fileExists(path)) {
3352
- content = await readFile11(path, "utf-8");
3854
+ content = await readFile12(path, "utf-8");
3353
3855
  content = content.trimEnd() + "\n\n" + serializeLogEntry(entry) + "\n";
3354
3856
  } else {
3355
3857
  const fm = `---
@@ -4474,12 +4976,12 @@ You are working within the Syntaur protocol for multi-agent project coordination
4474
4976
  _index-plans.md # Derived (read-only)
4475
4977
  _index-decisions.md # Derived (read-only)
4476
4978
  _status.md # Derived (read-only)
4477
- claude.md # Human-authored: Claude-specific instructions (read-only)
4478
- agent.md # Human-authored: universal agent instructions (read-only)
4479
4979
  assignments/
4480
4980
  <assignment-slug>/
4481
4981
  assignment.md # Agent-writable: source of truth for state (includes ## Todos)
4482
4982
  plan*.md # Agent-writable: versioned implementation plans (optional, one per ## Todos entry)
4983
+ progress.md # Agent-writable, append-only: timestamped progress log
4984
+ comments.md # CLI-mediated: threaded questions/notes/feedback (via \`syntaur comment\`)
4483
4985
  scratchpad.md # Agent-writable: working notes
4484
4986
  handoff.md # Agent-writable: append-only handoff log
4485
4987
  decision-record.md # Agent-writable: append-only decision log
@@ -4489,14 +4991,24 @@ You are working within the Syntaur protocol for multi-agent project coordination
4489
4991
  memories/
4490
4992
  _index.md # Derived (read-only)
4491
4993
  <memory-slug>.md # Shared-writable
4994
+ assignments/
4995
+ <assignment-id>/ # Standalone assignments \u2014 folder = UUID, \`project: null\`, slug display-only
4996
+ assignment.md
4997
+ plan*.md
4998
+ progress.md
4999
+ comments.md
5000
+ scratchpad.md
5001
+ handoff.md
5002
+ decision-record.md
4492
5003
  \`\`\`
4493
5004
 
4494
5005
  ## Write Boundary Rules (CRITICAL)
4495
5006
 
4496
5007
  ### Files you may WRITE:
4497
5008
  1. **Your assignment folder** -- only the assignment you are currently working on:
4498
- - \`assignment.md\`, \`plan*.md\` (0 or more versioned plan files), \`scratchpad.md\`, \`handoff.md\`, \`decision-record.md\`
4499
- - Path: \`~/.syntaur/projects/<project>/assignments/<your-assignment>/\`
5009
+ - \`assignment.md\`, \`plan*.md\` (0 or more versioned plan files), \`progress.md\`, \`scratchpad.md\`, \`handoff.md\`, \`decision-record.md\`
5010
+ - Path (project-nested): \`~/.syntaur/projects/<project>/assignments/<your-assignment>/\`
5011
+ - Path (standalone): \`~/.syntaur/assignments/<your-assignment-uuid>/\`
4500
5012
  2. **Shared resources and memories** at the project level:
4501
5013
  - \`~/.syntaur/projects/<project>/resources/<slug>.md\`
4502
5014
  - \`~/.syntaur/projects/<project>/memories/<slug>.md\`
@@ -4504,11 +5016,15 @@ You are working within the Syntaur protocol for multi-agent project coordination
4504
5016
 
4505
5017
  > **Note:** The \`setup-adapter\` command does not parse assignment frontmatter for workspace paths. Workspace boundaries are resolved by the agent at runtime by reading \`assignment.md\` frontmatter. If no \`workspace\` field is set, treat the current working directory as your workspace.
4506
5018
 
5019
+ ### Files written only via CLI (never edit directly):
5020
+ - \`comments.md\` (any assignment) -- use \`syntaur comment <slug-or-uuid> "body" [--type question|note|feedback] [--reply-to <id>]\`
5021
+ - Another assignment's \`## Todos\` section -- use \`syntaur request <source> <target> "text"\` to request cross-assignment work
5022
+
4507
5023
  ### Files you must NEVER write:
4508
- 1. \`project.md\`, \`agent.md\`, \`claude.md\` -- human-authored, read-only
5024
+ 1. \`project.md\` -- human-authored, read-only
4509
5025
  2. \`manifest.md\` -- derived, rebuilt by tooling
4510
5026
  3. Any file prefixed with \`_\` -- derived
4511
- 4. Other agents' assignment folders
5027
+ 4. Other agents' assignment folders (except via the CLI-mediated channels above)
4512
5028
  5. Any files outside your workspace boundary
4513
5029
 
4514
5030
  ## Assignment Lifecycle
@@ -4539,7 +5055,7 @@ You are working within the Syntaur protocol for multi-agent project coordination
4539
5055
 
4540
5056
  ## Lifecycle Commands
4541
5057
 
4542
- Use the \`syntaur\` CLI for state transitions:
5058
+ Use the \`syntaur\` CLI for state transitions and coordination:
4543
5059
  - \`syntaur assign <slug> --agent <name> --project <project>\` -- set assignee
4544
5060
  - \`syntaur start <slug> --project <project>\` -- pending -> in_progress
4545
5061
  - \`syntaur review <slug> --project <project>\` -- in_progress -> review
@@ -4547,6 +5063,9 @@ Use the \`syntaur\` CLI for state transitions:
4547
5063
  - \`syntaur block <slug> --project <project> --reason <text>\` -- block an assignment
4548
5064
  - \`syntaur unblock <slug> --project <project>\` -- unblock
4549
5065
  - \`syntaur fail <slug> --project <project>\` -- mark as failed
5066
+ - \`syntaur create-assignment "Title" [--type <type>] [--project <slug> | --one-off]\` -- create project-nested or standalone assignment
5067
+ - \`syntaur comment <slug-or-uuid> "body" --type question|note|feedback [--reply-to <id>]\` -- append to \`comments.md\` (questions support resolve toggle via dashboard)
5068
+ - \`syntaur request <source> <target> "text"\` -- append a todo to another assignment's \`## Todos\` annotated \`(from: <source>)\`
4550
5069
 
4551
5070
  ## Playbooks
4552
5071
 
@@ -4560,11 +5079,13 @@ Follow the rules in each playbook. They take precedence over default conventions
4560
5079
 
4561
5080
  ## Conventions
4562
5081
 
4563
- - Assignment frontmatter is the single source of truth for state
4564
- - Slugs are lowercase, hyphen-separated
4565
- - Always read \`agent.md\` at the project level before starting work
4566
- - Add unanswered questions to the Q&A section of assignment.md
4567
- - Commit frequently with messages referencing the assignment slug
5082
+ - Assignment frontmatter is the single source of truth for state. \`project\` is the containing project slug (\`null\` for standalone); \`type\` is a classification validated against \`config.md\` \`types.definitions\` when present.
5083
+ - Slugs are lowercase, hyphen-separated. Standalone assignment folders are named by UUID; \`slug\` is display-only in that case.
5084
+ - Always read \`project.md\` at the project level (when project-nested) before starting work.
5085
+ - Append timestamped entries to \`progress.md\` (never to \`assignment.md\`).
5086
+ - Record questions, notes, and feedback via \`syntaur comment\`. Never edit \`comments.md\` directly.
5087
+ - To route work to another assignment, use \`syntaur request\`.
5088
+ - Commit frequently with messages referencing the assignment slug.
4568
5089
  `;
4569
5090
  }
4570
5091
  function renderCursorAssignment(params) {
@@ -4584,21 +5105,25 @@ alwaysApply: true
4584
5105
  ## Reading Order
4585
5106
 
4586
5107
  Before starting work, read these files in order:
4587
- 1. \`${params.projectDir}/agent.md\` -- universal agent instructions and boundaries
4588
- 2. \`${params.projectDir}/project.md\` -- project overview and goals
4589
- 3. \`${params.assignmentDir}/assignment.md\` -- your assignment details, acceptance criteria, todos, current status
4590
- 4. any \`${params.assignmentDir}/plan*.md\` files linked from active todos in the \`## Todos\` section (may be 0, 1, or many)
4591
- 5. \`${params.assignmentDir}/handoff.md\` -- previous session handoff notes
5108
+ 1. \`${params.projectDir}/project.md\` -- project overview and goals (project-nested assignments only)
5109
+ 2. \`${params.assignmentDir}/assignment.md\` -- your assignment details, acceptance criteria, todos, current status. Frontmatter includes \`project: <slug> | null\` (null for standalone) and \`type: <classification> | null\`.
5110
+ 3. any \`${params.assignmentDir}/plan*.md\` files linked from active todos in the \`## Todos\` section (may be 0, 1, or many)
5111
+ 4. \`${params.assignmentDir}/progress.md\` -- reverse-chron progress log (if present)
5112
+ 5. \`${params.assignmentDir}/comments.md\` -- threaded questions/notes/feedback (if present)
5113
+ 6. \`${params.assignmentDir}/handoff.md\` -- previous session handoff notes
4592
5114
 
4593
5115
  ## Your Writable Files
4594
5116
 
4595
- You may ONLY write to files inside your assignment folder:
5117
+ You may write directly to these files inside your assignment folder:
4596
5118
  - \`${params.assignmentDir}/assignment.md\`
4597
5119
  - \`${params.assignmentDir}/plan*.md\` (0 or more versioned plan files, e.g., \`plan.md\`, \`plan-v2.md\`)
5120
+ - \`${params.assignmentDir}/progress.md\` (append timestamped entries, newest first)
4598
5121
  - \`${params.assignmentDir}/scratchpad.md\`
4599
5122
  - \`${params.assignmentDir}/handoff.md\`
4600
5123
  - \`${params.assignmentDir}/decision-record.md\`
4601
5124
 
5125
+ Do NOT edit \`${params.assignmentDir}/comments.md\` directly \u2014 use \`syntaur comment\`. Do NOT edit other assignments' files \u2014 use \`syntaur request\` for cross-assignment todos.
5126
+
4602
5127
  And source code files in your workspace. Read the \`workspace\` field from your assignment's frontmatter to determine the exact boundary. If not set, the current working directory is your workspace.
4603
5128
  `;
4604
5129
  }
@@ -4623,10 +5148,10 @@ If the global Syntaur Codex plugin is installed, prefer these workflows instead
4623
5148
  - \`syntaur-operator\` agent -- use for broad Syntaur protocol work or when a task spans multiple lifecycle steps
4624
5149
  - \`syntaur-protocol\` -- background protocol and write-boundary rules
4625
5150
  - \`create-project\` -- scaffold a project
4626
- - \`create-assignment\` -- create a new assignment
5151
+ - \`create-assignment\` -- create a new assignment (use \`--type <bug|feature|chore|...>\` to classify; use \`--one-off\` to create a standalone assignment at \`~/.syntaur/assignments/<uuid>/\` with no parent project)
4627
5152
  - \`grab-assignment\` -- claim work, create \`.syntaur/context.json\`, and register a session
4628
5153
  - \`plan-assignment\` -- write a versioned plan file (\`plan.md\`, \`plan-v2.md\`, ...) and link it from the \`## Todos\` section of \`assignment.md\`
4629
- - \`complete-assignment\` -- append the handoff, close the session, and transition state
5154
+ - \`complete-assignment\` -- append the handoff, append a final entry to \`progress.md\`, close the session, and transition state
4630
5155
  - \`track-session\` -- manage tracked tmux sessions for the dashboard
4631
5156
 
4632
5157
  If the plugin is unavailable, follow the same workflow manually with the \`syntaur\` CLI and keep the protocol files current yourself.
@@ -4634,12 +5159,12 @@ If the plugin is unavailable, follow the same workflow manually with the \`synta
4634
5159
  ## Reading Order
4635
5160
 
4636
5161
  Before starting work, read these files in order:
4637
- 1. \`${params.projectDir}/manifest.md\` -- root navigation entry point
4638
- 2. \`${params.projectDir}/agent.md\` -- universal agent instructions and boundaries
4639
- 3. \`${params.projectDir}/project.md\` -- project overview and goals
4640
- 4. \`${params.projectDir}/claude.md\` if it exists -- extra project context that may still be relevant
4641
- 5. \`${params.assignmentDir}/assignment.md\` -- your assignment details, acceptance criteria, todos, current status
4642
- 6. any \`${params.assignmentDir}/plan*.md\` files linked from active todos in the \`## Todos\` section (may be 0, 1, or many)
5162
+ 1. \`${params.projectDir}/manifest.md\` -- root navigation entry point (project-nested assignments only)
5163
+ 2. \`${params.projectDir}/project.md\` -- project overview and goals (project-nested assignments only)
5164
+ 3. \`${params.assignmentDir}/assignment.md\` -- your assignment details, acceptance criteria, todos, current status. Frontmatter now includes \`project: <slug> | null\` (null for standalone) and \`type: <classification> | null\`.
5165
+ 4. any \`${params.assignmentDir}/plan*.md\` files linked from active todos in the \`## Todos\` section (may be 0, 1, or many)
5166
+ 5. \`${params.assignmentDir}/progress.md\` -- reverse-chron progress log (if present)
5167
+ 6. \`${params.assignmentDir}/comments.md\` -- threaded questions/notes/feedback (if present)
4643
5168
  7. \`${params.assignmentDir}/handoff.md\` -- previous session handoff notes
4644
5169
 
4645
5170
  ## Context File
@@ -4661,12 +5186,12 @@ Before starting work, read these files in order:
4661
5186
  _index-plans.md # Derived (read-only)
4662
5187
  _index-decisions.md # Derived (read-only)
4663
5188
  _status.md # Derived (read-only)
4664
- claude.md # Human-authored: Claude-specific instructions (read-only)
4665
- agent.md # Human-authored: universal agent instructions (read-only)
4666
5189
  assignments/
4667
5190
  <assignment-slug>/
4668
5191
  assignment.md # Agent-writable: source of truth for state (includes ## Todos)
4669
5192
  plan*.md # Agent-writable: versioned implementation plans (optional, one per ## Todos entry)
5193
+ progress.md # Agent-writable, append-only: timestamped progress log
5194
+ comments.md # CLI-mediated: threaded questions/notes/feedback (via \`syntaur comment\`)
4670
5195
  scratchpad.md # Agent-writable: working notes
4671
5196
  handoff.md # Agent-writable: append-only handoff log
4672
5197
  decision-record.md # Agent-writable: append-only decision log
@@ -4676,13 +5201,22 @@ Before starting work, read these files in order:
4676
5201
  memories/
4677
5202
  _index.md # Derived (read-only)
4678
5203
  <memory-slug>.md # Shared-writable
5204
+ assignments/
5205
+ <assignment-id>/ # Standalone assignments \u2014 folder = UUID, \`project: null\`, slug display-only
5206
+ assignment.md
5207
+ plan*.md
5208
+ progress.md
5209
+ comments.md
5210
+ scratchpad.md
5211
+ handoff.md
5212
+ decision-record.md
4679
5213
  \`\`\`
4680
5214
 
4681
5215
  ## Write Boundary Rules (CRITICAL)
4682
5216
 
4683
5217
  ### Files you may WRITE:
4684
5218
  1. **Your assignment folder** -- only the assignment you are currently working on:
4685
- - \`assignment.md\`, \`plan*.md\` (0 or more versioned plan files), \`scratchpad.md\`, \`handoff.md\`, \`decision-record.md\`
5219
+ - \`assignment.md\`, \`plan*.md\` (0 or more versioned plan files), \`progress.md\`, \`scratchpad.md\`, \`handoff.md\`, \`decision-record.md\`
4686
5220
  - Path: \`${params.assignmentDir}/\`
4687
5221
  2. **Shared resources and memories** at the project level:
4688
5222
  - \`${params.projectDir}/resources/<slug>.md\`
@@ -4691,11 +5225,15 @@ Before starting work, read these files in order:
4691
5225
 
4692
5226
  > **Note:** Workspace boundaries are resolved by the agent at runtime by reading \`assignment.md\` frontmatter. If no \`workspace\` field is set, treat the current working directory as your workspace.
4693
5227
 
5228
+ ### Files written only via CLI (never edit directly):
5229
+ - \`comments.md\` (any assignment) -- use \`syntaur comment <slug-or-uuid> "body" [--type question|note|feedback] [--reply-to <id>]\`
5230
+ - Another assignment's \`## Todos\` section -- use \`syntaur request <source> <target> "text"\` to request cross-assignment work
5231
+
4694
5232
  ### Files you must NEVER write:
4695
- 1. \`project.md\`, \`agent.md\`, \`claude.md\` -- human-authored, read-only
5233
+ 1. \`project.md\` -- human-authored, read-only
4696
5234
  2. \`manifest.md\` -- derived, rebuilt by tooling
4697
5235
  3. Any file prefixed with \`_\` -- derived
4698
- 4. Other agents' assignment folders
5236
+ 4. Other agents' assignment folders (except via the CLI-mediated channels above)
4699
5237
  5. Any files outside your workspace boundary
4700
5238
 
4701
5239
  ## Assignment Lifecycle
@@ -4726,7 +5264,7 @@ Before starting work, read these files in order:
4726
5264
 
4727
5265
  ## Lifecycle Commands
4728
5266
 
4729
- Use the \`syntaur\` CLI for state transitions:
5267
+ Use the \`syntaur\` CLI for state transitions and coordination:
4730
5268
  - \`syntaur assign ${params.assignmentSlug} --agent <name> --project ${params.projectSlug}\` -- set assignee
4731
5269
  - \`syntaur start ${params.assignmentSlug} --project ${params.projectSlug}\` -- pending -> in_progress
4732
5270
  - \`syntaur review ${params.assignmentSlug} --project ${params.projectSlug}\` -- in_progress -> review
@@ -4734,6 +5272,8 @@ Use the \`syntaur\` CLI for state transitions:
4734
5272
  - \`syntaur block ${params.assignmentSlug} --project ${params.projectSlug} --reason <text>\` -- block
4735
5273
  - \`syntaur unblock ${params.assignmentSlug} --project ${params.projectSlug}\` -- unblock
4736
5274
  - \`syntaur fail ${params.assignmentSlug} --project ${params.projectSlug}\` -- mark as failed
5275
+ - \`syntaur comment ${params.assignmentSlug} "body" --type question|note|feedback [--reply-to <id>]\` -- append to \`comments.md\` (use for all Q&A; questions support resolve toggle)
5276
+ - \`syntaur request ${params.assignmentSlug} <target-slug-or-uuid> "text"\` -- append a todo to another assignment's \`## Todos\` annotated \`(from: ${params.assignmentSlug})\`
4737
5277
 
4738
5278
  ## Troubleshooting
4739
5279
 
@@ -4751,14 +5291,15 @@ Read each linked playbook and follow the rules in its body section. The \`when_t
4751
5291
 
4752
5292
  ## Conventions
4753
5293
 
4754
- - Assignment frontmatter is the single source of truth for state
4755
- - Slugs are lowercase, hyphen-separated
4756
- - Always read \`agent.md\` at the project level before starting work
4757
- - Keep \`assignment.md\` progress, acceptance criteria, and \`## Todos\` updated as work lands
4758
- - Keep active plan file(s) current after planning changes and \`handoff.md\` current before leaving the task
4759
- - When requirements shift, supersede the prior plan todo (\`- [x] ~~...~~ (superseded by plan-v<N>)\`) and write a new plan file instead of rewriting the old one
4760
- - Add unanswered questions to the Q&A section of assignment.md
4761
- - Commit frequently with messages referencing the assignment slug
5294
+ - Assignment frontmatter is the single source of truth for state. \`project\` is the containing project slug (\`null\` for standalone); \`type\` is a classification validated against \`config.md\` \`types.definitions\` when present.
5295
+ - Slugs are lowercase, hyphen-separated. For standalone assignments, \`slug\` is display-only; the folder is named by the UUID.
5296
+ - Always read \`project.md\` at the project level (when project-nested) before starting work.
5297
+ - Keep \`assignment.md\` acceptance criteria and \`## Todos\` updated as work lands; append timestamped entries to \`progress.md\` (never to \`assignment.md\`).
5298
+ - Keep active plan file(s) current after planning changes and \`handoff.md\` current before leaving the task.
5299
+ - When requirements shift, supersede the prior plan todo (\`- [x] ~~...~~ (superseded by plan-v<N>)\`) and write a new plan file instead of rewriting the old one.
5300
+ - Record questions, notes, and feedback via \`syntaur comment\`. Never edit \`comments.md\` directly. Resolve questions via the dashboard UI (toggle on the question entry).
5301
+ - To route work to another assignment, use \`syntaur request\`.
5302
+ - Commit frequently with messages referencing the assignment slug.
4762
5303
  `;
4763
5304
  }
4764
5305
 
@@ -4766,8 +5307,12 @@ Read each linked playbook and follow the rules in its body section. The \`when_t
4766
5307
  function renderOpenCodeConfig(params) {
4767
5308
  const config = {
4768
5309
  instructions: [
4769
- `Read AGENTS.md in this directory for Syntaur protocol instructions.`,
4770
- `Also read ${params.projectDir}/agent.md for universal agent conventions.`
5310
+ `Read AGENTS.md in this directory for Syntaur protocol (v2.0) instructions.`,
5311
+ `Read ${params.projectDir}/project.md for project overview (project-nested assignments only).`,
5312
+ `Append timestamped progress entries to the assignment's progress.md (not to assignment.md).`,
5313
+ `Use 'syntaur comment <slug-or-uuid> "body" --type question|note|feedback' to append to comments.md \u2014 never edit it directly.`,
5314
+ `Use 'syntaur request <source> <target> "text"' to append a todo to another assignment's ## Todos.`,
5315
+ `Assignment folders are project-nested at ~/.syntaur/projects/<slug>/assignments/<aslug>/ or standalone at ~/.syntaur/assignments/<uuid>/ (project: null, slug display-only).`
4771
5316
  ]
4772
5317
  };
4773
5318
  return JSON.stringify(config, null, 2) + "\n";
@@ -5044,23 +5589,334 @@ Use --slug to specify a different slug.`
5044
5589
  init_config2();
5045
5590
  import { spawn } from "child_process";
5046
5591
  import { createServer as createNetServer } from "net";
5047
- import { resolve as resolve19, dirname as dirname4 } from "path";
5592
+ import { resolve as resolve20, dirname as dirname4 } from "path";
5048
5593
  import { fileURLToPath as fileURLToPath2 } from "url";
5049
5594
 
5050
5595
  // src/dashboard/server.ts
5051
5596
  init_paths();
5052
5597
  init_api();
5598
+ init_assignment_resolver();
5053
5599
  import express from "express";
5054
5600
  import { createServer } from "http";
5055
- import { resolve as resolve18 } from "path";
5601
+ import { resolve as resolve19 } from "path";
5056
5602
  import { writeFile as writeFile4, unlink as unlink4 } from "fs/promises";
5057
5603
  import { WebSocketServer, WebSocket } from "ws";
5058
5604
 
5059
- // src/dashboard/watcher.ts
5060
- import { watch } from "chokidar";
5061
- import { relative, sep } from "path";
5605
+ // src/dashboard/agent-sessions.ts
5606
+ init_fs();
5607
+ import { readFile as readFile9 } from "fs/promises";
5608
+ import { resolve as resolve13 } from "path";
5609
+
5610
+ // src/dashboard/session-db.ts
5611
+ init_paths();
5612
+ init_fs();
5613
+ import Database from "better-sqlite3";
5614
+ import { resolve as resolve12 } from "path";
5615
+ import { readdir as readdir7 } from "fs/promises";
5616
+ var db = null;
5617
+ var SCHEMA_VERSION = "3";
5618
+ var SCHEMA_SQL = `
5619
+ CREATE TABLE IF NOT EXISTS sessions (
5620
+ session_id TEXT PRIMARY KEY,
5621
+ project_slug TEXT,
5622
+ assignment_slug TEXT,
5623
+ agent TEXT NOT NULL,
5624
+ started TEXT NOT NULL,
5625
+ ended TEXT,
5626
+ status TEXT NOT NULL DEFAULT 'active',
5627
+ path TEXT,
5628
+ description TEXT,
5629
+ transcript_path TEXT,
5630
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
5631
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
5632
+ );
5633
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
5634
+ CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
5635
+ CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
5636
+ CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);
5637
+ `;
5638
+ function initSessionDb(dbPath) {
5639
+ if (db) return db;
5640
+ const finalPath = dbPath ?? resolve12(syntaurRoot(), "syntaur.db");
5641
+ db = new Database(finalPath);
5642
+ db.pragma("journal_mode = WAL");
5643
+ db.exec(SCHEMA_SQL);
5644
+ db.prepare("INSERT OR IGNORE INTO meta (key, value) VALUES (?, ?)").run(
5645
+ "schema_version",
5646
+ SCHEMA_VERSION
5647
+ );
5648
+ const currentVersion = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
5649
+ if (currentVersion?.value === "1") {
5650
+ db.exec(`
5651
+ CREATE TABLE sessions_v2 (
5652
+ session_id TEXT PRIMARY KEY,
5653
+ project_slug TEXT,
5654
+ assignment_slug TEXT,
5655
+ agent TEXT NOT NULL,
5656
+ started TEXT NOT NULL,
5657
+ ended TEXT,
5658
+ status TEXT NOT NULL DEFAULT 'active',
5659
+ path TEXT,
5660
+ description TEXT,
5661
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
5662
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
5663
+ );
5664
+ INSERT INTO sessions_v2 SELECT session_id, project_slug, assignment_slug, agent, started, ended, status, path, NULL, created_at, updated_at FROM sessions;
5665
+ DROP TABLE sessions;
5666
+ ALTER TABLE sessions_v2 RENAME TO sessions;
5667
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
5668
+ CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
5669
+ CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
5670
+ UPDATE meta SET value = '2' WHERE key = 'schema_version';
5671
+ `);
5672
+ }
5673
+ const versionAfterV1 = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
5674
+ if (versionAfterV1?.value === "2") {
5675
+ db.exec(`
5676
+ CREATE TABLE sessions_v3 (
5677
+ session_id TEXT PRIMARY KEY,
5678
+ project_slug TEXT,
5679
+ assignment_slug TEXT,
5680
+ agent TEXT NOT NULL,
5681
+ started TEXT NOT NULL,
5682
+ ended TEXT,
5683
+ status TEXT NOT NULL DEFAULT 'active',
5684
+ path TEXT,
5685
+ description TEXT,
5686
+ transcript_path TEXT,
5687
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
5688
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
5689
+ );
5690
+ INSERT INTO sessions_v3 SELECT session_id, project_slug, assignment_slug, agent, started, ended, status, path, description, NULL, created_at, updated_at FROM sessions;
5691
+ DROP TABLE sessions;
5692
+ ALTER TABLE sessions_v3 RENAME TO sessions;
5693
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
5694
+ CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
5695
+ CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
5696
+ UPDATE meta SET value = '3' WHERE key = 'schema_version';
5697
+ `);
5698
+ }
5699
+ return db;
5700
+ }
5701
+ function getSessionDb() {
5702
+ if (!db) {
5703
+ throw new Error(
5704
+ "Session database not initialized. Call initSessionDb() first."
5705
+ );
5706
+ }
5707
+ return db;
5708
+ }
5709
+ function closeSessionDb() {
5710
+ if (db) {
5711
+ db.close();
5712
+ db = null;
5713
+ }
5714
+ }
5715
+ async function migrateFromMarkdown(projectsDir2) {
5716
+ const database = getSessionDb();
5717
+ const count = database.prepare("SELECT COUNT(*) as count FROM sessions").get();
5718
+ if (count.count > 0) return 0;
5719
+ if (!await fileExists(projectsDir2)) return 0;
5720
+ const entries = await readdir7(projectsDir2, { withFileTypes: true });
5721
+ const allSessions = [];
5722
+ for (const entry of entries) {
5723
+ if (!entry.isDirectory()) continue;
5724
+ const projectDir = resolve12(projectsDir2, entry.name);
5725
+ const indexPath = resolve12(projectDir, "_index-sessions.md");
5726
+ if (!await fileExists(indexPath)) continue;
5727
+ const sessions = await parseMarkdownSessionsIndex(indexPath, entry.name);
5728
+ allSessions.push(...sessions);
5729
+ }
5730
+ if (allSessions.length === 0) return 0;
5731
+ const insert = database.prepare(`
5732
+ INSERT OR IGNORE INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path)
5733
+ VALUES (?, ?, ?, ?, ?, ?, ?)
5734
+ `);
5735
+ const insertAll = database.transaction((sessions) => {
5736
+ for (const s of sessions) {
5737
+ insert.run(s.sessionId, s.projectSlug, s.assignmentSlug, s.agent, s.started, s.status, s.path);
5738
+ }
5739
+ });
5740
+ insertAll(allSessions);
5741
+ console.log(`Migrated ${allSessions.length} sessions from markdown to SQLite.`);
5742
+ return allSessions.length;
5743
+ }
5744
+ async function parseMarkdownSessionsIndex(filePath, projectSlug) {
5745
+ const { readFile: readFile25 } = await import("fs/promises");
5746
+ const raw = await readFile25(filePath, "utf-8");
5747
+ const sessions = [];
5748
+ const lines = raw.split("\n");
5749
+ let inTable = false;
5750
+ let headerSeen = false;
5751
+ for (const line of lines) {
5752
+ const trimmed = line.trim();
5753
+ if (!trimmed) continue;
5754
+ if (trimmed.startsWith("| Assignment") || trimmed.startsWith("|Assignment")) {
5755
+ inTable = true;
5756
+ headerSeen = false;
5757
+ continue;
5758
+ }
5759
+ if (inTable && !headerSeen && trimmed.match(/^\|[-\s|]+\|$/)) {
5760
+ headerSeen = true;
5761
+ continue;
5762
+ }
5763
+ if (inTable && headerSeen && trimmed.startsWith("|")) {
5764
+ const cells = trimmed.split("|").slice(1, -1).map((c2) => c2.trim());
5765
+ if (cells.length >= 6) {
5766
+ sessions.push({
5767
+ assignmentSlug: cells[0],
5768
+ agent: cells[1],
5769
+ sessionId: cells[2],
5770
+ started: cells[3],
5771
+ status: cells[4] || "active",
5772
+ path: cells[5],
5773
+ projectSlug
5774
+ });
5775
+ }
5776
+ }
5777
+ }
5778
+ return sessions;
5779
+ }
5780
+
5781
+ // src/dashboard/agent-sessions.ts
5782
+ function rowToSession(row) {
5783
+ return {
5784
+ sessionId: row.session_id,
5785
+ projectSlug: row.project_slug ?? null,
5786
+ assignmentSlug: row.assignment_slug ?? null,
5787
+ agent: row.agent,
5788
+ started: row.started,
5789
+ ended: row.ended ?? null,
5790
+ status: row.status,
5791
+ path: row.path ?? "",
5792
+ description: row.description ?? null,
5793
+ transcriptPath: row.transcript_path ?? null
5794
+ };
5795
+ }
5796
+ async function appendSession(_projectDir, session) {
5797
+ const db2 = getSessionDb();
5798
+ db2.prepare(`
5799
+ INSERT INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path, description, transcript_path)
5800
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
5801
+ ON CONFLICT(session_id) DO UPDATE SET
5802
+ project_slug = COALESCE(excluded.project_slug, project_slug),
5803
+ assignment_slug = COALESCE(excluded.assignment_slug, assignment_slug),
5804
+ agent = excluded.agent,
5805
+ status = CASE WHEN status IN ('completed','stopped') THEN status ELSE excluded.status END,
5806
+ path = COALESCE(excluded.path, path),
5807
+ description = COALESCE(excluded.description, description),
5808
+ transcript_path = COALESCE(excluded.transcript_path, transcript_path),
5809
+ updated_at = datetime('now')
5810
+ `).run(
5811
+ session.sessionId,
5812
+ session.projectSlug ?? null,
5813
+ session.assignmentSlug ?? null,
5814
+ session.agent,
5815
+ session.started,
5816
+ session.status,
5817
+ session.path,
5818
+ session.description ?? null,
5819
+ session.transcriptPath ?? null
5820
+ );
5821
+ }
5822
+ async function updateSessionStatus(_projectDir, sessionId, status) {
5823
+ const db2 = getSessionDb();
5824
+ const isTerminal = status === "completed" || status === "stopped";
5825
+ const result = isTerminal ? db2.prepare(
5826
+ "UPDATE sessions SET status = ?, ended = datetime('now'), updated_at = datetime('now') WHERE session_id = ?"
5827
+ ).run(status, sessionId) : db2.prepare(
5828
+ "UPDATE sessions SET status = ?, updated_at = datetime('now') WHERE session_id = ?"
5829
+ ).run(status, sessionId);
5830
+ return result.changes > 0;
5831
+ }
5832
+ async function listAllSessions(_projectsDir) {
5833
+ const db2 = getSessionDb();
5834
+ const rows = db2.prepare("SELECT * FROM sessions ORDER BY started DESC").all();
5835
+ return rows.map(rowToSession);
5836
+ }
5837
+ async function listProjectSessions(_projectsDir, projectSlug, assignmentSlug) {
5838
+ const db2 = getSessionDb();
5839
+ if (assignmentSlug) {
5840
+ const rows2 = db2.prepare(
5841
+ "SELECT * FROM sessions WHERE project_slug = ? AND assignment_slug = ? ORDER BY started DESC"
5842
+ ).all(projectSlug, assignmentSlug);
5843
+ return rows2.map(rowToSession);
5844
+ }
5845
+ const rows = db2.prepare("SELECT * FROM sessions WHERE project_slug = ? ORDER BY started DESC").all(projectSlug);
5846
+ return rows.map(rowToSession);
5847
+ }
5848
+ async function deleteSessions(sessionIds) {
5849
+ if (sessionIds.length === 0) return 0;
5850
+ const db2 = getSessionDb();
5851
+ const placeholders = sessionIds.map(() => "?").join(", ");
5852
+ const result = db2.prepare(`DELETE FROM sessions WHERE session_id IN (${placeholders})`).run(...sessionIds);
5853
+ return result.changes;
5854
+ }
5855
+ var DONE_ASSIGNMENT_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "review"]);
5856
+ async function readAssignmentStatusFromPath(assignmentMdPath) {
5857
+ if (!await fileExists(assignmentMdPath)) return null;
5858
+ const raw = await readFile9(assignmentMdPath, "utf-8");
5859
+ const match = raw.match(/^status:\s*(.+)$/m);
5860
+ return match ? match[1].trim() : null;
5861
+ }
5862
+ async function readAssignmentStatus(projectDir, assignmentSlug) {
5863
+ return readAssignmentStatusFromPath(
5864
+ resolve13(projectDir, "assignments", assignmentSlug, "assignment.md")
5865
+ );
5866
+ }
5867
+ async function reconcileActiveSessions(projectsDir2, assignmentsDir2) {
5868
+ const db2 = getSessionDb();
5869
+ const activeSessions = db2.prepare("SELECT * FROM sessions WHERE status = 'active' AND assignment_slug IS NOT NULL").all();
5870
+ if (activeSessions.length === 0) return 0;
5871
+ const assignmentStatuses = /* @__PURE__ */ new Map();
5872
+ const seen = /* @__PURE__ */ new Set();
5873
+ for (const session of activeSessions) {
5874
+ const aslug = session.assignment_slug;
5875
+ if (!aslug) continue;
5876
+ const projectKey = session.project_slug ?? "__standalone__";
5877
+ const key = `${projectKey}/${aslug}`;
5878
+ if (seen.has(key)) continue;
5879
+ seen.add(key);
5880
+ if (session.project_slug) {
5881
+ const status = await readAssignmentStatus(
5882
+ resolve13(projectsDir2, session.project_slug),
5883
+ aslug
5884
+ );
5885
+ if (status) assignmentStatuses.set(key, status);
5886
+ } else if (assignmentsDir2) {
5887
+ const status = await readAssignmentStatusFromPath(
5888
+ resolve13(assignmentsDir2, aslug, "assignment.md")
5889
+ );
5890
+ if (status) assignmentStatuses.set(key, status);
5891
+ }
5892
+ }
5893
+ let totalUpdated = 0;
5894
+ for (const session of activeSessions) {
5895
+ const projectKey = session.project_slug ?? "__standalone__";
5896
+ const key = `${projectKey}/${session.assignment_slug}`;
5897
+ const assignmentStatus = assignmentStatuses.get(key);
5898
+ if (!assignmentStatus || !DONE_ASSIGNMENT_STATUSES.has(assignmentStatus)) continue;
5899
+ const newStatus = assignmentStatus === "failed" ? "stopped" : "completed";
5900
+ await updateSessionStatus("", session.session_id, newStatus);
5901
+ totalUpdated++;
5902
+ }
5903
+ return totalUpdated;
5904
+ }
5905
+ async function listSessionsByAssignment(projectSlug, assignmentSlug) {
5906
+ const db2 = getSessionDb();
5907
+ const rows = projectSlug === null ? db2.prepare(
5908
+ "SELECT * FROM sessions WHERE assignment_slug = ? AND project_slug IS NULL ORDER BY started DESC"
5909
+ ).all(assignmentSlug) : db2.prepare(
5910
+ "SELECT * FROM sessions WHERE project_slug = ? AND assignment_slug = ? ORDER BY started DESC"
5911
+ ).all(projectSlug, assignmentSlug);
5912
+ return rows.map(rowToSession);
5913
+ }
5914
+
5915
+ // src/dashboard/watcher.ts
5916
+ import { watch } from "chokidar";
5917
+ import { relative, sep } from "path";
5062
5918
  function createWatcher(options) {
5063
- const { projectsDir: projectsDir2, serversDir: serversDir2, playbooksDir: playbooksDir3, todosDir: todosDir2, onMessage, debounceMs = 300 } = options;
5919
+ const { projectsDir: projectsDir2, assignmentsDir: assignmentsDir2, serversDir: serversDir2, playbooksDir: playbooksDir3, todosDir: todosDir2, onMessage, debounceMs = 300 } = options;
5064
5920
  const pendingEvents = /* @__PURE__ */ new Map();
5065
5921
  const projectsWatcher = watch(projectsDir2, {
5066
5922
  ignoreInitial: true,
@@ -5098,6 +5954,42 @@ function createWatcher(options) {
5098
5954
  projectsWatcher.on("change", handleProjectChange);
5099
5955
  projectsWatcher.on("add", handleProjectChange);
5100
5956
  projectsWatcher.on("unlink", handleProjectChange);
5957
+ let standaloneWatcher = null;
5958
+ if (assignmentsDir2) {
5959
+ let handleStandaloneChange2 = function(filePath) {
5960
+ const rel = relative(assignmentsDir2, filePath);
5961
+ const parts = rel.split(sep);
5962
+ if (parts.length === 0) return;
5963
+ const assignmentId = parts[0];
5964
+ if (!assignmentId) return;
5965
+ const debounceKey = `__standalone__/${assignmentId}`;
5966
+ const existing = pendingEvents.get(debounceKey);
5967
+ if (existing) clearTimeout(existing);
5968
+ pendingEvents.set(
5969
+ debounceKey,
5970
+ setTimeout(() => {
5971
+ pendingEvents.delete(debounceKey);
5972
+ const message = {
5973
+ type: "assignment-updated",
5974
+ projectSlug: null,
5975
+ assignmentSlug: assignmentId,
5976
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
5977
+ };
5978
+ onMessage(message);
5979
+ }, debounceMs)
5980
+ );
5981
+ };
5982
+ var handleStandaloneChange = handleStandaloneChange2;
5983
+ standaloneWatcher = watch(assignmentsDir2, {
5984
+ ignoreInitial: true,
5985
+ persistent: true,
5986
+ depth: 5,
5987
+ ignored: /(^|[\/\\])\../
5988
+ });
5989
+ standaloneWatcher.on("change", handleStandaloneChange2);
5990
+ standaloneWatcher.on("add", handleStandaloneChange2);
5991
+ standaloneWatcher.on("unlink", handleStandaloneChange2);
5992
+ }
5101
5993
  let serversWatcher = null;
5102
5994
  if (serversDir2) {
5103
5995
  let handleServerChange2 = function() {
@@ -5192,6 +6084,7 @@ function createWatcher(options) {
5192
6084
  });
5193
6085
  pendingEvents.clear();
5194
6086
  await projectsWatcher.close();
6087
+ if (standaloneWatcher) await standaloneWatcher.close();
5195
6088
  if (serversWatcher) await serversWatcher.close();
5196
6089
  if (playbooksWatcher) await playbooksWatcher.close();
5197
6090
  if (todosWatcher) await todosWatcher.close();
@@ -5206,8 +6099,8 @@ init_config2();
5206
6099
  // src/dashboard/api-write.ts
5207
6100
  init_lifecycle();
5208
6101
  import { Router } from "express";
5209
- import { resolve as resolve11 } from "path";
5210
- import { rm, readFile as readFile8 } from "fs/promises";
6102
+ import { resolve as resolve14 } from "path";
6103
+ import { rm, readFile as readFile10 } from "fs/promises";
5211
6104
  init_timestamp();
5212
6105
  init_fs();
5213
6106
  init_parser();
@@ -5259,6 +6152,9 @@ function toggleAcceptanceCriterion(content, index, checked) {
5259
6152
 
5260
6153
  // src/dashboard/api-write.ts
5261
6154
  init_api();
6155
+ init_assignment_resolver();
6156
+ init_lifecycle();
6157
+ init_parser();
5262
6158
  function extractFrontmatter3(content) {
5263
6159
  const trimmed = content.trimStart();
5264
6160
  if (!trimmed.startsWith("---\n") && !trimmed.startsWith("---\r\n")) {
@@ -5358,9 +6254,9 @@ async function readCurrentDocument(filePath) {
5358
6254
  if (!await fileExists(filePath)) {
5359
6255
  return null;
5360
6256
  }
5361
- return readFile8(filePath, "utf-8");
6257
+ return readFile10(filePath, "utf-8");
5362
6258
  }
5363
- function createWriteRouter(projectsDir2) {
6259
+ function createWriteRouter(projectsDir2, assignmentsDir2) {
5364
6260
  const router = Router();
5365
6261
  router.get("/api/templates/project", (_req, res) => {
5366
6262
  const content = renderProject({
@@ -5488,26 +6384,26 @@ function createWriteRouter(projectsDir2) {
5488
6384
  res.status(400).json({ error: `Invalid slug "${slug}". Must be lowercase and hyphen-separated.` });
5489
6385
  return;
5490
6386
  }
5491
- const projectDir = resolve11(projectsDir2, slug);
6387
+ const projectDir = resolve14(projectsDir2, slug);
5492
6388
  if (await fileExists(projectDir)) {
5493
6389
  res.status(409).json({ error: `Project "${slug}" already exists` });
5494
6390
  return;
5495
6391
  }
5496
6392
  const title = fields.title;
5497
6393
  const timestamp = fields.created || nowTimestamp();
5498
- await ensureDir(resolve11(projectDir, "assignments"));
5499
- await ensureDir(resolve11(projectDir, "resources"));
5500
- await ensureDir(resolve11(projectDir, "memories"));
5501
- await writeFileForce(resolve11(projectDir, "project.md"), content);
6394
+ await ensureDir(resolve14(projectDir, "assignments"));
6395
+ await ensureDir(resolve14(projectDir, "resources"));
6396
+ await ensureDir(resolve14(projectDir, "memories"));
6397
+ await writeFileForce(resolve14(projectDir, "project.md"), content);
5502
6398
  try {
5503
6399
  const companions = [
5504
- [resolve11(projectDir, "manifest.md"), renderManifest({ slug, timestamp })],
5505
- [resolve11(projectDir, "_index-assignments.md"), renderIndexAssignments({ slug, title, timestamp })],
5506
- [resolve11(projectDir, "_index-plans.md"), renderIndexPlans({ slug, title, timestamp })],
5507
- [resolve11(projectDir, "_index-decisions.md"), renderIndexDecisions({ slug, title, timestamp })],
5508
- [resolve11(projectDir, "_status.md"), renderStatus({ slug, title, timestamp })],
5509
- [resolve11(projectDir, "resources", "_index.md"), renderResourcesIndex({ slug, title, timestamp })],
5510
- [resolve11(projectDir, "memories", "_index.md"), renderMemoriesIndex({ slug, title, timestamp })]
6400
+ [resolve14(projectDir, "manifest.md"), renderManifest({ slug, timestamp })],
6401
+ [resolve14(projectDir, "_index-assignments.md"), renderIndexAssignments({ slug, title, timestamp })],
6402
+ [resolve14(projectDir, "_index-plans.md"), renderIndexPlans({ slug, title, timestamp })],
6403
+ [resolve14(projectDir, "_index-decisions.md"), renderIndexDecisions({ slug, title, timestamp })],
6404
+ [resolve14(projectDir, "_status.md"), renderStatus({ slug, title, timestamp })],
6405
+ [resolve14(projectDir, "resources", "_index.md"), renderResourcesIndex({ slug, title, timestamp })],
6406
+ [resolve14(projectDir, "memories", "_index.md"), renderMemoriesIndex({ slug, title, timestamp })]
5511
6407
  ];
5512
6408
  for (const [filePath, fileContent] of companions) {
5513
6409
  await writeFileForce(filePath, fileContent);
@@ -5528,8 +6424,8 @@ function createWriteRouter(projectsDir2) {
5528
6424
  router.post("/api/projects/:slug/assignments", async (req, res) => {
5529
6425
  try {
5530
6426
  const projectSlug = getParam(req.params.slug);
5531
- const projectDir = resolve11(projectsDir2, projectSlug);
5532
- const projectMdPath = resolve11(projectDir, "project.md");
6427
+ const projectDir = resolve14(projectsDir2, projectSlug);
6428
+ const projectMdPath = resolve14(projectDir, "project.md");
5533
6429
  if (!await fileExists(projectMdPath)) {
5534
6430
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
5535
6431
  return;
@@ -5559,7 +6455,7 @@ function createWriteRouter(projectsDir2) {
5559
6455
  res.status(400).json({ error: `Invalid priority "${priority}". Must be low, medium, high, or critical.` });
5560
6456
  return;
5561
6457
  }
5562
- const assignmentDir = resolve11(projectDir, "assignments", assignmentSlug);
6458
+ const assignmentDir = resolve14(projectDir, "assignments", assignmentSlug);
5563
6459
  if (await fileExists(assignmentDir)) {
5564
6460
  res.status(409).json({
5565
6461
  error: `Assignment "${assignmentSlug}" already exists in project "${projectSlug}"`
@@ -5568,12 +6464,12 @@ function createWriteRouter(projectsDir2) {
5568
6464
  }
5569
6465
  const timestamp = fields.created || nowTimestamp();
5570
6466
  await ensureDir(assignmentDir);
5571
- await writeFileForce(resolve11(assignmentDir, "assignment.md"), content);
6467
+ await writeFileForce(resolve14(assignmentDir, "assignment.md"), content);
5572
6468
  try {
5573
6469
  const companions = [
5574
- [resolve11(assignmentDir, "scratchpad.md"), renderScratchpad({ assignmentSlug, timestamp })],
5575
- [resolve11(assignmentDir, "handoff.md"), renderHandoff({ assignmentSlug, timestamp })],
5576
- [resolve11(assignmentDir, "decision-record.md"), renderDecisionRecord({ assignmentSlug, timestamp })]
6470
+ [resolve14(assignmentDir, "scratchpad.md"), renderScratchpad({ assignmentSlug, timestamp })],
6471
+ [resolve14(assignmentDir, "handoff.md"), renderHandoff({ assignmentSlug, timestamp })],
6472
+ [resolve14(assignmentDir, "decision-record.md"), renderDecisionRecord({ assignmentSlug, timestamp })]
5577
6473
  ];
5578
6474
  for (const [filePath, fileContent] of companions) {
5579
6475
  await writeFileForce(filePath, fileContent);
@@ -5594,7 +6490,7 @@ function createWriteRouter(projectsDir2) {
5594
6490
  router.patch("/api/projects/:slug", async (req, res) => {
5595
6491
  try {
5596
6492
  const projectSlug = getParam(req.params.slug);
5597
- const projectPath = resolve11(projectsDir2, projectSlug, "project.md");
6493
+ const projectPath = resolve14(projectsDir2, projectSlug, "project.md");
5598
6494
  const currentContent = await readCurrentDocument(projectPath);
5599
6495
  if (!currentContent) {
5600
6496
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
@@ -5627,7 +6523,7 @@ function createWriteRouter(projectsDir2) {
5627
6523
  try {
5628
6524
  const projectSlug = getParam(req.params.slug);
5629
6525
  const assignmentSlug = getParam(req.params.aslug);
5630
- const assignmentPath = resolve11(
6526
+ const assignmentPath = resolve14(
5631
6527
  projectsDir2,
5632
6528
  projectSlug,
5633
6529
  "assignments",
@@ -5670,7 +6566,7 @@ function createWriteRouter(projectsDir2) {
5670
6566
  try {
5671
6567
  const projectSlug = getParam(req.params.slug);
5672
6568
  const assignmentSlug = getParam(req.params.aslug);
5673
- const assignmentPath = resolve11(
6569
+ const assignmentPath = resolve14(
5674
6570
  projectsDir2,
5675
6571
  projectSlug,
5676
6572
  "assignments",
@@ -5706,7 +6602,7 @@ function createWriteRouter(projectsDir2) {
5706
6602
  try {
5707
6603
  const projectSlug = getParam(req.params.slug);
5708
6604
  const assignmentSlug = getParam(req.params.aslug);
5709
- const planPath = resolve11(
6605
+ const planPath = resolve14(
5710
6606
  projectsDir2,
5711
6607
  projectSlug,
5712
6608
  "assignments",
@@ -5744,7 +6640,7 @@ function createWriteRouter(projectsDir2) {
5744
6640
  try {
5745
6641
  const projectSlug = getParam(req.params.slug);
5746
6642
  const assignmentSlug = getParam(req.params.aslug);
5747
- const scratchpadPath = resolve11(
6643
+ const scratchpadPath = resolve14(
5748
6644
  projectsDir2,
5749
6645
  projectSlug,
5750
6646
  "assignments",
@@ -5782,7 +6678,7 @@ function createWriteRouter(projectsDir2) {
5782
6678
  try {
5783
6679
  const projectSlug = getParam(req.params.slug);
5784
6680
  const assignmentSlug = getParam(req.params.aslug);
5785
- const handoffPath = resolve11(
6681
+ const handoffPath = resolve14(
5786
6682
  projectsDir2,
5787
6683
  projectSlug,
5788
6684
  "assignments",
@@ -5820,7 +6716,7 @@ function createWriteRouter(projectsDir2) {
5820
6716
  try {
5821
6717
  const projectSlug = getParam(req.params.slug);
5822
6718
  const assignmentSlug = getParam(req.params.aslug);
5823
- const decisionPath = resolve11(
6719
+ const decisionPath = resolve14(
5824
6720
  projectsDir2,
5825
6721
  projectSlug,
5826
6722
  "assignments",
@@ -5854,10 +6750,121 @@ function createWriteRouter(projectsDir2) {
5854
6750
  res.status(500).json({ error: `Failed to append decision entry: ${error.message}` });
5855
6751
  }
5856
6752
  });
6753
+ router.post("/api/projects/:slug/assignments/:aslug/comments", async (req, res) => {
6754
+ try {
6755
+ const projectSlug = getParam(req.params.slug);
6756
+ const assignmentSlug = getParam(req.params.aslug);
6757
+ const commentsPath = resolve14(
6758
+ projectsDir2,
6759
+ projectSlug,
6760
+ "assignments",
6761
+ assignmentSlug,
6762
+ "comments.md"
6763
+ );
6764
+ const { body, author, type, replyTo } = req.body || {};
6765
+ if (!body || typeof body !== "string" || !body.trim()) {
6766
+ res.status(400).json({ error: "body is required" });
6767
+ return;
6768
+ }
6769
+ const commentType = type && ["question", "note", "feedback"].includes(type) ? type : "note";
6770
+ const timestamp = nowTimestamp();
6771
+ const entryAuthor = typeof author === "string" && author.trim() ? author.trim() : "human";
6772
+ let currentContent;
6773
+ let currentCount = 0;
6774
+ if (await fileExists(commentsPath)) {
6775
+ currentContent = await readFile10(commentsPath, "utf-8");
6776
+ const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
6777
+ if (countMatch) currentCount = parseInt(countMatch[1], 10);
6778
+ } else {
6779
+ currentContent = renderComments({
6780
+ assignment: assignmentSlug,
6781
+ timestamp
6782
+ });
6783
+ }
6784
+ const comment = {
6785
+ id: generateId().split("-")[0],
6786
+ timestamp,
6787
+ author: entryAuthor,
6788
+ type: commentType,
6789
+ body,
6790
+ replyTo: typeof replyTo === "string" && replyTo.trim() ? replyTo.trim() : void 0,
6791
+ resolved: commentType === "question" ? false : void 0
6792
+ };
6793
+ const entry = formatCommentEntry(comment);
6794
+ let next = setTopLevelField(currentContent, "entryCount", String(currentCount + 1));
6795
+ next = setTopLevelField(next, "updated", `"${timestamp}"`);
6796
+ if (next.includes("No comments yet.")) {
6797
+ next = next.replace("No comments yet.", entry.trimEnd());
6798
+ } else {
6799
+ next = `${next.trimEnd()}
6800
+
6801
+ ${entry}`;
6802
+ }
6803
+ await writeFileForce(commentsPath, next);
6804
+ const assignment = await getAssignmentDetail(projectsDir2, projectSlug, assignmentSlug);
6805
+ res.status(201).json({ assignment, comment: { id: comment.id } });
6806
+ } catch (error) {
6807
+ console.error("Error appending comment:", error);
6808
+ res.status(500).json({ error: `Failed to append comment: ${error.message}` });
6809
+ }
6810
+ });
6811
+ router.patch("/api/projects/:slug/assignments/:aslug/comments/:commentId/resolved", async (req, res) => {
6812
+ try {
6813
+ const projectSlug = getParam(req.params.slug);
6814
+ const assignmentSlug = getParam(req.params.aslug);
6815
+ const commentId = getParam(req.params.commentId);
6816
+ const commentsPath = resolve14(
6817
+ projectsDir2,
6818
+ projectSlug,
6819
+ "assignments",
6820
+ assignmentSlug,
6821
+ "comments.md"
6822
+ );
6823
+ if (!await fileExists(commentsPath)) {
6824
+ res.status(404).json({ error: "Comments file not found" });
6825
+ return;
6826
+ }
6827
+ const { resolved } = req.body || {};
6828
+ if (typeof resolved !== "boolean") {
6829
+ res.status(400).json({ error: "resolved (boolean) is required" });
6830
+ return;
6831
+ }
6832
+ const content = await readFile10(commentsPath, "utf-8");
6833
+ const parsed = parseComments(content);
6834
+ const target = parsed.entries.find((e) => e.id === commentId);
6835
+ if (!target) {
6836
+ res.status(404).json({ error: `Comment ${commentId} not found` });
6837
+ return;
6838
+ }
6839
+ if (target.type !== "question") {
6840
+ res.status(400).json({ error: "Only questions can be resolved" });
6841
+ return;
6842
+ }
6843
+ const entryBlockRegex = new RegExp(
6844
+ `(^## ${commentId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?)(\\*\\*Resolved:\\*\\*\\s*(?:true|false))`,
6845
+ "m"
6846
+ );
6847
+ const next = content.replace(
6848
+ entryBlockRegex,
6849
+ (_m, preamble) => `${preamble}**Resolved:** ${resolved ? "true" : "false"}`
6850
+ );
6851
+ if (next === content) {
6852
+ res.status(500).json({ error: "Failed to update resolved flag" });
6853
+ return;
6854
+ }
6855
+ const withUpdated = setTopLevelField(next, "updated", `"${nowTimestamp()}"`);
6856
+ await writeFileForce(commentsPath, withUpdated);
6857
+ const assignment = await getAssignmentDetail(projectsDir2, projectSlug, assignmentSlug);
6858
+ res.json({ assignment });
6859
+ } catch (error) {
6860
+ console.error("Error toggling comment resolved flag:", error);
6861
+ res.status(500).json({ error: `Failed to toggle resolved: ${error.message}` });
6862
+ }
6863
+ });
5857
6864
  router.post("/api/projects/:slug/move-workspace", async (req, res) => {
5858
6865
  try {
5859
6866
  const projectSlug = getParam(req.params.slug);
5860
- const projectPath = resolve11(projectsDir2, projectSlug, "project.md");
6867
+ const projectPath = resolve14(projectsDir2, projectSlug, "project.md");
5861
6868
  if (!await fileExists(projectPath)) {
5862
6869
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
5863
6870
  return;
@@ -5867,7 +6874,7 @@ function createWriteRouter(projectsDir2) {
5867
6874
  res.status(400).json({ error: "workspace must be a non-empty string or null (for ungrouped)." });
5868
6875
  return;
5869
6876
  }
5870
- let content = await readFile8(projectPath, "utf-8");
6877
+ let content = await readFile10(projectPath, "utf-8");
5871
6878
  content = setTopLevelField(content, "workspace", workspace ?? null);
5872
6879
  content = setTopLevelField(content, "updated", nowTimestamp());
5873
6880
  await writeFileForce(projectPath, content);
@@ -5881,7 +6888,7 @@ function createWriteRouter(projectsDir2) {
5881
6888
  router.post("/api/projects/:slug/status-override", async (req, res) => {
5882
6889
  try {
5883
6890
  const projectSlug = getParam(req.params.slug);
5884
- const projectPath = resolve11(projectsDir2, projectSlug, "project.md");
6891
+ const projectPath = resolve14(projectsDir2, projectSlug, "project.md");
5885
6892
  if (!await fileExists(projectPath)) {
5886
6893
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
5887
6894
  return;
@@ -5893,7 +6900,7 @@ function createWriteRouter(projectsDir2) {
5893
6900
  res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}, or null to clear.` });
5894
6901
  return;
5895
6902
  }
5896
- let content = await readFile8(projectPath, "utf-8");
6903
+ let content = await readFile10(projectPath, "utf-8");
5897
6904
  content = setTopLevelField(content, "statusOverride", status ?? null);
5898
6905
  content = setTopLevelField(content, "updated", nowTimestamp());
5899
6906
  await writeFileForce(projectPath, content);
@@ -5908,7 +6915,7 @@ function createWriteRouter(projectsDir2) {
5908
6915
  try {
5909
6916
  const projectSlug = getParam(req.params.slug);
5910
6917
  const assignmentSlug = getParam(req.params.aslug);
5911
- const assignmentPath = resolve11(
6918
+ const assignmentPath = resolve14(
5912
6919
  projectsDir2,
5913
6920
  projectSlug,
5914
6921
  "assignments",
@@ -5926,7 +6933,7 @@ function createWriteRouter(projectsDir2) {
5926
6933
  res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}.` });
5927
6934
  return;
5928
6935
  }
5929
- let content = await readFile8(assignmentPath, "utf-8");
6936
+ let content = await readFile10(assignmentPath, "utf-8");
5930
6937
  content = setTopLevelField(content, "status", status);
5931
6938
  content = setTopLevelField(content, "updated", nowTimestamp());
5932
6939
  if (status !== "blocked") {
@@ -5951,8 +6958,8 @@ function createWriteRouter(projectsDir2) {
5951
6958
  res.status(400).json({ error: `Unsupported transition command "${req.params.command}"` });
5952
6959
  return;
5953
6960
  }
5954
- const projectDir = resolve11(projectsDir2, projectSlug);
5955
- const assignmentPath = resolve11(projectDir, "assignments", assignmentSlug, "assignment.md");
6961
+ const projectDir = resolve14(projectsDir2, projectSlug);
6962
+ const assignmentPath = resolve14(projectDir, "assignments", assignmentSlug, "assignment.md");
5956
6963
  if (!await fileExists(assignmentPath)) {
5957
6964
  res.status(404).json({ error: "Assignment not found" });
5958
6965
  return;
@@ -5978,8 +6985,8 @@ function createWriteRouter(projectsDir2) {
5978
6985
  try {
5979
6986
  const projectSlug = getParam(req.params.slug);
5980
6987
  const assignmentSlug = getParam(req.params.aslug);
5981
- const assignmentDir = resolve11(projectsDir2, projectSlug, "assignments", assignmentSlug);
5982
- const assignmentPath = resolve11(assignmentDir, "assignment.md");
6988
+ const assignmentDir = resolve14(projectsDir2, projectSlug, "assignments", assignmentSlug);
6989
+ const assignmentPath = resolve14(assignmentDir, "assignment.md");
5983
6990
  if (!await fileExists(assignmentPath)) {
5984
6991
  res.status(404).json({ error: `Assignment "${assignmentSlug}" not found in project "${projectSlug}"` });
5985
6992
  return;
@@ -5991,388 +6998,686 @@ function createWriteRouter(projectsDir2) {
5991
6998
  res.status(500).json({ error: `Failed to delete assignment: ${error.message}` });
5992
6999
  }
5993
7000
  });
5994
- return router;
5995
- }
5996
-
5997
- // src/dashboard/api-servers.ts
5998
- init_servers();
5999
- init_scanner();
6000
- import { Router as Router2 } from "express";
6001
- function createServersRouter(serversDir2, projectsDir2) {
6002
- const router = Router2();
6003
- router.get("/", async (_req, res) => {
7001
+ router.post("/api/assignments", async (req, res) => {
6004
7002
  try {
6005
- const result = await scanAllSessions(serversDir2, projectsDir2);
6006
- res.json(result);
7003
+ if (!assignmentsDir2) {
7004
+ res.status(501).json({ error: "Standalone assignments not configured on this server" });
7005
+ return;
7006
+ }
7007
+ const { title, slug, priority, type } = req.body || {};
7008
+ if (!title || typeof title !== "string" || !title.trim()) {
7009
+ res.status(400).json({ error: "title is required" });
7010
+ return;
7011
+ }
7012
+ const { dependsOn } = req.body || {};
7013
+ if (Array.isArray(dependsOn) && dependsOn.length > 0) {
7014
+ res.status(400).json({ error: "Standalone assignments cannot declare dependsOn." });
7015
+ return;
7016
+ }
7017
+ const id = generateId();
7018
+ const assignmentDir = resolve14(assignmentsDir2, id);
7019
+ if (await fileExists(assignmentDir)) {
7020
+ res.status(500).json({ error: "UUID collision \u2014 try again" });
7021
+ return;
7022
+ }
7023
+ const timestamp = nowTimestamp();
7024
+ const resolvedSlug = typeof slug === "string" && slug.trim() ? slug.trim() : slugifyLocal(title);
7025
+ const resolvedPriority = typeof priority === "string" && ["low", "medium", "high", "critical"].includes(priority) ? priority : "medium";
7026
+ await ensureDir(assignmentDir);
7027
+ const assignmentContent = renderAssignment({
7028
+ id,
7029
+ slug: resolvedSlug,
7030
+ title: title.trim(),
7031
+ timestamp,
7032
+ priority: resolvedPriority,
7033
+ dependsOn: [],
7034
+ links: [],
7035
+ project: null,
7036
+ type: typeof type === "string" ? type : void 0
7037
+ });
7038
+ await writeFileForce(resolve14(assignmentDir, "assignment.md"), assignmentContent);
7039
+ await writeFileForce(
7040
+ resolve14(assignmentDir, "scratchpad.md"),
7041
+ renderScratchpad({ assignmentSlug: id, timestamp })
7042
+ );
7043
+ await writeFileForce(
7044
+ resolve14(assignmentDir, "handoff.md"),
7045
+ renderHandoff({ assignmentSlug: id, timestamp })
7046
+ );
7047
+ await writeFileForce(
7048
+ resolve14(assignmentDir, "decision-record.md"),
7049
+ renderDecisionRecord({ assignmentSlug: id, timestamp })
7050
+ );
7051
+ await writeFileForce(
7052
+ resolve14(assignmentDir, "progress.md"),
7053
+ renderProgress({ assignment: id, timestamp })
7054
+ );
7055
+ await writeFileForce(
7056
+ resolve14(assignmentDir, "comments.md"),
7057
+ renderComments({ assignment: id, timestamp })
7058
+ );
7059
+ const detail = await getAssignmentDetailById(projectsDir2, assignmentsDir2, id);
7060
+ res.status(201).json({ assignment: detail });
6007
7061
  } catch (error) {
6008
- res.status(500).json({ error: error instanceof Error ? error.message : "Scan failed" });
7062
+ console.error("Error creating standalone assignment:", error);
7063
+ res.status(500).json({ error: `Failed to create standalone assignment: ${error.message}` });
6009
7064
  }
6010
7065
  });
6011
- router.get("/:name", async (req, res) => {
7066
+ router.post("/api/assignments/:id/comments", async (req, res) => {
6012
7067
  try {
6013
- const session = await scanSingleSession(serversDir2, projectsDir2, req.params.name);
6014
- if (!session) {
6015
- res.status(404).json({ error: "Session not found" });
7068
+ if (!assignmentsDir2) {
7069
+ res.status(501).json({ error: "Standalone assignments not configured on this server" });
7070
+ return;
7071
+ }
7072
+ const id = getParam(req.params.id);
7073
+ const resolved = await resolveAssignmentById(projectsDir2, assignmentsDir2, id);
7074
+ if (!resolved) {
7075
+ res.status(404).json({ error: `Assignment "${id}" not found` });
7076
+ return;
7077
+ }
7078
+ await appendCommentTo(resolved.assignmentDir, resolved.standalone ? resolved.id : resolved.assignmentSlug, req, res, async () => {
7079
+ return resolved.standalone ? getAssignmentDetailById(projectsDir2, assignmentsDir2, id) : getAssignmentDetail(projectsDir2, resolved.projectSlug, resolved.assignmentSlug);
7080
+ });
7081
+ } catch (error) {
7082
+ console.error("Error appending comment (by id):", error);
7083
+ res.status(500).json({ error: `Failed to append comment: ${error.message}` });
7084
+ }
7085
+ });
7086
+ router.patch("/api/assignments/:id/comments/:commentId/resolved", async (req, res) => {
7087
+ try {
7088
+ if (!assignmentsDir2) {
7089
+ res.status(501).json({ error: "Standalone assignments not configured on this server" });
7090
+ return;
7091
+ }
7092
+ const id = getParam(req.params.id);
7093
+ const commentId = getParam(req.params.commentId);
7094
+ const resolved = await resolveAssignmentById(projectsDir2, assignmentsDir2, id);
7095
+ if (!resolved) {
7096
+ res.status(404).json({ error: `Assignment "${id}" not found` });
7097
+ return;
7098
+ }
7099
+ await toggleCommentResolvedAt(resolved.assignmentDir, commentId, req, res, async () => {
7100
+ return resolved.standalone ? getAssignmentDetailById(projectsDir2, assignmentsDir2, id) : getAssignmentDetail(projectsDir2, resolved.projectSlug, resolved.assignmentSlug);
7101
+ });
7102
+ } catch (error) {
7103
+ console.error("Error toggling comment resolved (by id):", error);
7104
+ res.status(500).json({ error: `Failed to toggle resolved: ${error.message}` });
7105
+ }
7106
+ });
7107
+ router.get("/api/assignments/:id/edit", async (req, res) => {
7108
+ if (!assignmentsDir2) {
7109
+ res.status(501).json({ error: "Standalone assignments not configured on this server" });
7110
+ return;
7111
+ }
7112
+ const id = getParam(req.params.id);
7113
+ const doc = await getEditableDocumentById(projectsDir2, assignmentsDir2, "assignment", id);
7114
+ if (!doc) {
7115
+ res.status(404).json({ error: "Assignment not found" });
7116
+ return;
7117
+ }
7118
+ res.json(doc);
7119
+ });
7120
+ router.get("/api/assignments/:id/plan/edit", async (req, res) => {
7121
+ if (!assignmentsDir2) {
7122
+ res.status(501).json({ error: "Standalone assignments not configured on this server" });
7123
+ return;
7124
+ }
7125
+ const id = getParam(req.params.id);
7126
+ const doc = await getEditableDocumentById(projectsDir2, assignmentsDir2, "plan", id);
7127
+ if (!doc) {
7128
+ res.status(404).json({ error: "Plan not found" });
7129
+ return;
7130
+ }
7131
+ res.json(doc);
7132
+ });
7133
+ router.get("/api/assignments/:id/scratchpad/edit", async (req, res) => {
7134
+ if (!assignmentsDir2) {
7135
+ res.status(501).json({ error: "Standalone assignments not configured on this server" });
7136
+ return;
7137
+ }
7138
+ const id = getParam(req.params.id);
7139
+ const doc = await getEditableDocumentById(projectsDir2, assignmentsDir2, "scratchpad", id);
7140
+ if (!doc) {
7141
+ res.status(404).json({ error: "Scratchpad not found" });
7142
+ return;
7143
+ }
7144
+ res.json(doc);
7145
+ });
7146
+ router.get("/api/assignments/:id/handoff/edit", async (req, res) => {
7147
+ if (!assignmentsDir2) {
7148
+ res.status(501).json({ error: "Standalone assignments not configured on this server" });
7149
+ return;
7150
+ }
7151
+ const id = getParam(req.params.id);
7152
+ const doc = await getEditableDocumentById(projectsDir2, assignmentsDir2, "handoff", id);
7153
+ if (!doc) {
7154
+ res.status(404).json({ error: "Handoff log not found" });
7155
+ return;
7156
+ }
7157
+ res.json(doc);
7158
+ });
7159
+ router.get("/api/assignments/:id/decision-record/edit", async (req, res) => {
7160
+ if (!assignmentsDir2) {
7161
+ res.status(501).json({ error: "Standalone assignments not configured on this server" });
7162
+ return;
7163
+ }
7164
+ const id = getParam(req.params.id);
7165
+ const doc = await getEditableDocumentById(projectsDir2, assignmentsDir2, "decision-record", id);
7166
+ if (!doc) {
7167
+ res.status(404).json({ error: "Decision record not found" });
7168
+ return;
7169
+ }
7170
+ res.json(doc);
7171
+ });
7172
+ router.patch("/api/assignments/:id", async (req, res) => {
7173
+ try {
7174
+ if (!assignmentsDir2) {
7175
+ res.status(501).json({ error: "Standalone assignments not configured on this server" });
7176
+ return;
7177
+ }
7178
+ const id = getParam(req.params.id);
7179
+ const resolved = await resolveAssignmentById(projectsDir2, assignmentsDir2, id);
7180
+ if (!resolved) {
7181
+ res.status(404).json({ error: `Assignment "${id}" not found` });
7182
+ return;
7183
+ }
7184
+ const assignmentPath = resolve14(resolved.assignmentDir, "assignment.md");
7185
+ const currentContent = await readCurrentDocument(assignmentPath);
7186
+ if (!currentContent) {
7187
+ res.status(404).json({ error: "Assignment not found" });
6016
7188
  return;
6017
7189
  }
6018
- res.json(session);
7190
+ const nextContentRaw = requireContent(req, res);
7191
+ if (!nextContentRaw) return;
7192
+ const current = parseAssignmentFull(currentContent);
7193
+ const next = parseAssignmentFull(nextContentRaw);
7194
+ if (!next.title) {
7195
+ res.status(400).json({ error: "Assignment content must include a title." });
7196
+ return;
7197
+ }
7198
+ let nextContent = nextContentRaw;
7199
+ if (current.id) nextContent = setTopLevelField(nextContent, "id", current.id);
7200
+ nextContent = setTopLevelField(nextContent, "project", null);
7201
+ if (current.slug) nextContent = setTopLevelField(nextContent, "slug", current.slug);
7202
+ if (next.status !== current.status && current.status === "blocked" && next.status !== "blocked") {
7203
+ nextContent = setTopLevelField(nextContent, "blockedReason", null);
7204
+ }
7205
+ nextContent = setTopLevelField(nextContent, "updated", nowTimestamp());
7206
+ await writeFileForce(assignmentPath, nextContent);
7207
+ const assignment = await getAssignmentDetailById(projectsDir2, assignmentsDir2, id);
7208
+ res.json({ assignment, content: nextContent });
6019
7209
  } catch (error) {
6020
- res.status(500).json({ error: error instanceof Error ? error.message : "Scan failed" });
7210
+ console.error("Error updating standalone assignment:", error);
7211
+ res.status(500).json({ error: `Failed to update assignment: ${error.message}` });
6021
7212
  }
6022
7213
  });
6023
- router.post("/", async (req, res) => {
7214
+ router.patch("/api/assignments/:id/plan", async (req, res) => {
6024
7215
  try {
6025
- const { name } = req.body;
6026
- if (!name || typeof name !== "string") {
6027
- res.status(400).json({ error: "name is required" });
7216
+ if (!assignmentsDir2) {
7217
+ res.status(501).json({ error: "Standalone assignments not configured on this server" });
6028
7218
  return;
6029
7219
  }
6030
- const sanitized = sanitizeSessionName(name);
6031
- const existing = await readSessionFile(serversDir2, sanitized);
6032
- if (existing) {
6033
- res.status(409).json({ error: `Session "${sanitized}" already registered` });
7220
+ const id = getParam(req.params.id);
7221
+ const resolved = await resolveAssignmentById(projectsDir2, assignmentsDir2, id);
7222
+ if (!resolved) {
7223
+ res.status(404).json({ error: `Assignment "${id}" not found` });
6034
7224
  return;
6035
7225
  }
6036
- await registerSession(serversDir2, name);
6037
- clearScanCache();
6038
- res.status(201).json({ name: sanitized });
6039
- } catch (error) {
6040
- res.status(500).json({ error: error instanceof Error ? error.message : "Registration failed" });
6041
- }
6042
- });
6043
- router.delete("/:name", async (req, res) => {
6044
- try {
6045
- const data = await readSessionFile(serversDir2, req.params.name);
6046
- if (!data) {
6047
- res.status(404).json({ error: "Session not found" });
7226
+ const planPath = resolve14(resolved.assignmentDir, "plan.md");
7227
+ const currentContent = await readCurrentDocument(planPath);
7228
+ if (!currentContent) {
7229
+ res.status(404).json({ error: "Plan not found" });
6048
7230
  return;
6049
7231
  }
6050
- await removeSession(serversDir2, req.params.name);
6051
- clearScanCache();
6052
- res.json({ removed: req.params.name });
7232
+ const nextContentRaw = requireContent(req, res);
7233
+ if (!nextContentRaw) return;
7234
+ const parsed = parsePlan(nextContentRaw);
7235
+ if (!parsed.assignment) {
7236
+ res.status(400).json({ error: "Plan content must include the assignment field." });
7237
+ return;
7238
+ }
7239
+ const nextContent = setTopLevelField(nextContentRaw, "updated", nowTimestamp());
7240
+ await writeFileForce(planPath, nextContent);
7241
+ const assignment = await getAssignmentDetailById(projectsDir2, assignmentsDir2, id);
7242
+ res.json({ assignment, content: nextContent });
6053
7243
  } catch (error) {
6054
- res.status(500).json({ error: error instanceof Error ? error.message : "Removal failed" });
7244
+ console.error("Error updating standalone plan:", error);
7245
+ res.status(500).json({ error: `Failed to update plan: ${error.message}` });
6055
7246
  }
6056
7247
  });
6057
- router.post("/refresh", async (_req, res) => {
7248
+ router.patch("/api/assignments/:id/scratchpad", async (req, res) => {
6058
7249
  try {
6059
- const names = await listSessionFiles(serversDir2);
6060
- for (const name of names) {
6061
- await updateLastRefreshed(serversDir2, name);
7250
+ if (!assignmentsDir2) {
7251
+ res.status(501).json({ error: "Standalone assignments not configured on this server" });
7252
+ return;
6062
7253
  }
6063
- clearScanCache();
6064
- const result = await scanAllSessions(serversDir2, projectsDir2, { bypassCache: true });
6065
- res.json(result);
7254
+ const id = getParam(req.params.id);
7255
+ const resolved = await resolveAssignmentById(projectsDir2, assignmentsDir2, id);
7256
+ if (!resolved) {
7257
+ res.status(404).json({ error: `Assignment "${id}" not found` });
7258
+ return;
7259
+ }
7260
+ const scratchpadPath = resolve14(resolved.assignmentDir, "scratchpad.md");
7261
+ const currentContent = await readCurrentDocument(scratchpadPath);
7262
+ if (!currentContent) {
7263
+ res.status(404).json({ error: "Scratchpad not found" });
7264
+ return;
7265
+ }
7266
+ const nextContentRaw = requireContent(req, res);
7267
+ if (!nextContentRaw) return;
7268
+ const parsed = parseScratchpad(nextContentRaw);
7269
+ if (!parsed.assignment) {
7270
+ res.status(400).json({ error: "Scratchpad content must include the assignment field." });
7271
+ return;
7272
+ }
7273
+ const nextContent = setTopLevelField(nextContentRaw, "updated", nowTimestamp());
7274
+ await writeFileForce(scratchpadPath, nextContent);
7275
+ const assignment = await getAssignmentDetailById(projectsDir2, assignmentsDir2, id);
7276
+ res.json({ assignment, content: nextContent });
6066
7277
  } catch (error) {
6067
- res.status(500).json({ error: error instanceof Error ? error.message : "Refresh failed" });
7278
+ console.error("Error updating standalone scratchpad:", error);
7279
+ res.status(500).json({ error: `Failed to update scratchpad: ${error.message}` });
6068
7280
  }
6069
7281
  });
6070
- router.post("/:name/refresh", async (req, res) => {
7282
+ router.post("/api/assignments/:id/handoff/entries", async (req, res) => {
6071
7283
  try {
6072
- const data = await readSessionFile(serversDir2, req.params.name);
6073
- if (!data) {
6074
- res.status(404).json({ error: "Session not found" });
7284
+ if (!assignmentsDir2) {
7285
+ res.status(501).json({ error: "Standalone assignments not configured on this server" });
6075
7286
  return;
6076
7287
  }
6077
- await updateLastRefreshed(serversDir2, req.params.name);
6078
- clearScanCache();
6079
- const session = await scanSingleSession(serversDir2, projectsDir2, req.params.name);
6080
- res.json(session);
7288
+ const id = getParam(req.params.id);
7289
+ const resolved = await resolveAssignmentById(projectsDir2, assignmentsDir2, id);
7290
+ if (!resolved) {
7291
+ res.status(404).json({ error: `Assignment "${id}" not found` });
7292
+ return;
7293
+ }
7294
+ const handoffPath = resolve14(resolved.assignmentDir, "handoff.md");
7295
+ const currentContent = await readCurrentDocument(handoffPath);
7296
+ if (!currentContent) {
7297
+ res.status(404).json({ error: "Handoff log not found" });
7298
+ return;
7299
+ }
7300
+ const { title, body } = req.body || {};
7301
+ if (!body || typeof body !== "string" || !body.trim()) {
7302
+ res.status(400).json({ error: "body is required" });
7303
+ return;
7304
+ }
7305
+ const parsed = parseHandoff(currentContent);
7306
+ const nextContent = appendLogEntry(
7307
+ currentContent,
7308
+ "handoffCount",
7309
+ parsed.handoffCount + 1,
7310
+ title && typeof title === "string" && title.trim() ? title.trim() : `Handoff ${parsed.handoffCount + 1}`,
7311
+ body,
7312
+ "No handoffs recorded yet."
7313
+ );
7314
+ await writeFileForce(handoffPath, nextContent);
7315
+ const assignment = await getAssignmentDetailById(projectsDir2, assignmentsDir2, id);
7316
+ res.status(201).json({ assignment, content: nextContent });
6081
7317
  } catch (error) {
6082
- res.status(500).json({ error: error instanceof Error ? error.message : "Refresh failed" });
7318
+ console.error("Error appending standalone handoff entry:", error);
7319
+ res.status(500).json({ error: `Failed to append handoff entry: ${error.message}` });
6083
7320
  }
6084
7321
  });
6085
- router.patch("/:name/panes/:windowIndex/:paneIndex/assignment", async (req, res) => {
7322
+ router.post("/api/assignments/:id/decision-record/entries", async (req, res) => {
6086
7323
  try {
6087
- const { name, windowIndex, paneIndex } = req.params;
6088
- const data = await readSessionFile(serversDir2, name);
6089
- if (!data) {
6090
- res.status(404).json({ error: "Session not found" });
7324
+ if (!assignmentsDir2) {
7325
+ res.status(501).json({ error: "Standalone assignments not configured on this server" });
6091
7326
  return;
6092
7327
  }
6093
- const body = req.body;
6094
- if (body === null || body && body.project && body.assignment) {
6095
- await setOverride(
6096
- serversDir2,
6097
- name,
6098
- parseInt(windowIndex, 10),
6099
- parseInt(paneIndex, 10),
6100
- body
6101
- );
6102
- clearScanCache();
6103
- res.json({ updated: true });
6104
- } else {
6105
- res.status(400).json({ error: "Body must be { project, assignment } or null" });
7328
+ const id = getParam(req.params.id);
7329
+ const resolved = await resolveAssignmentById(projectsDir2, assignmentsDir2, id);
7330
+ if (!resolved) {
7331
+ res.status(404).json({ error: `Assignment "${id}" not found` });
7332
+ return;
6106
7333
  }
6107
- } catch (error) {
6108
- res.status(500).json({ error: error instanceof Error ? error.message : "Update failed" });
6109
- }
6110
- });
6111
- return router;
6112
- }
6113
-
6114
- // src/dashboard/api-agent-sessions.ts
6115
- import { Router as Router3 } from "express";
6116
- import { resolve as resolve14 } from "path";
6117
- import { randomUUID as randomUUID2 } from "crypto";
6118
-
6119
- // src/dashboard/agent-sessions.ts
6120
- init_fs();
6121
- import { readFile as readFile9 } from "fs/promises";
6122
- import { resolve as resolve13 } from "path";
6123
-
6124
- // src/dashboard/session-db.ts
6125
- init_paths();
6126
- init_fs();
6127
- import Database from "better-sqlite3";
6128
- import { resolve as resolve12 } from "path";
6129
- import { readdir as readdir6 } from "fs/promises";
6130
- var db = null;
6131
- var SCHEMA_VERSION = "2";
6132
- var SCHEMA_SQL = `
6133
- CREATE TABLE IF NOT EXISTS sessions (
6134
- session_id TEXT PRIMARY KEY,
6135
- project_slug TEXT,
6136
- assignment_slug TEXT,
6137
- agent TEXT NOT NULL,
6138
- started TEXT NOT NULL,
6139
- ended TEXT,
6140
- status TEXT NOT NULL DEFAULT 'active',
6141
- path TEXT,
6142
- description TEXT,
6143
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
6144
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
6145
- );
6146
- CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
6147
- CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
6148
- CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
6149
- CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);
6150
- `;
6151
- function initSessionDb(dbPath) {
6152
- if (db) return db;
6153
- const finalPath = dbPath ?? resolve12(syntaurRoot(), "syntaur.db");
6154
- db = new Database(finalPath);
6155
- db.pragma("journal_mode = WAL");
6156
- db.exec(SCHEMA_SQL);
6157
- db.prepare("INSERT OR IGNORE INTO meta (key, value) VALUES (?, ?)").run(
6158
- "schema_version",
6159
- SCHEMA_VERSION
6160
- );
6161
- const currentVersion = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
6162
- if (currentVersion?.value === "1") {
6163
- db.exec(`
6164
- CREATE TABLE sessions_v2 (
6165
- session_id TEXT PRIMARY KEY,
6166
- project_slug TEXT,
6167
- assignment_slug TEXT,
6168
- agent TEXT NOT NULL,
6169
- started TEXT NOT NULL,
6170
- ended TEXT,
6171
- status TEXT NOT NULL DEFAULT 'active',
6172
- path TEXT,
6173
- description TEXT,
6174
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
6175
- updated_at TEXT NOT NULL DEFAULT (datetime('now'))
6176
- );
6177
- INSERT INTO sessions_v2 SELECT session_id, project_slug, assignment_slug, agent, started, ended, status, path, NULL, created_at, updated_at FROM sessions;
6178
- DROP TABLE sessions;
6179
- ALTER TABLE sessions_v2 RENAME TO sessions;
6180
- CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_slug);
6181
- CREATE INDEX IF NOT EXISTS idx_sessions_assignment ON sessions(project_slug, assignment_slug);
6182
- CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
6183
- UPDATE meta SET value = '2' WHERE key = 'schema_version';
6184
- `);
6185
- }
6186
- return db;
6187
- }
6188
- function getSessionDb() {
6189
- if (!db) {
6190
- throw new Error(
6191
- "Session database not initialized. Call initSessionDb() first."
6192
- );
6193
- }
6194
- return db;
6195
- }
6196
- function closeSessionDb() {
6197
- if (db) {
6198
- db.close();
6199
- db = null;
6200
- }
6201
- }
6202
- async function migrateFromMarkdown(projectsDir2) {
6203
- const database = getSessionDb();
6204
- const count = database.prepare("SELECT COUNT(*) as count FROM sessions").get();
6205
- if (count.count > 0) return 0;
6206
- if (!await fileExists(projectsDir2)) return 0;
6207
- const entries = await readdir6(projectsDir2, { withFileTypes: true });
6208
- const allSessions = [];
6209
- for (const entry of entries) {
6210
- if (!entry.isDirectory()) continue;
6211
- const projectDir = resolve12(projectsDir2, entry.name);
6212
- const indexPath = resolve12(projectDir, "_index-sessions.md");
6213
- if (!await fileExists(indexPath)) continue;
6214
- const sessions = await parseMarkdownSessionsIndex(indexPath, entry.name);
6215
- allSessions.push(...sessions);
6216
- }
6217
- if (allSessions.length === 0) return 0;
6218
- const insert = database.prepare(`
6219
- INSERT OR IGNORE INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path)
6220
- VALUES (?, ?, ?, ?, ?, ?, ?)
6221
- `);
6222
- const insertAll = database.transaction((sessions) => {
6223
- for (const s of sessions) {
6224
- insert.run(s.sessionId, s.projectSlug, s.assignmentSlug, s.agent, s.started, s.status, s.path);
7334
+ const decisionPath = resolve14(resolved.assignmentDir, "decision-record.md");
7335
+ const currentContent = await readCurrentDocument(decisionPath);
7336
+ if (!currentContent) {
7337
+ res.status(404).json({ error: "Decision record not found" });
7338
+ return;
7339
+ }
7340
+ const { title, body } = req.body || {};
7341
+ if (!body || typeof body !== "string" || !body.trim()) {
7342
+ res.status(400).json({ error: "body is required" });
7343
+ return;
7344
+ }
7345
+ const parsed = parseDecisionRecord(currentContent);
7346
+ const nextContent = appendLogEntry(
7347
+ currentContent,
7348
+ "decisionCount",
7349
+ parsed.decisionCount + 1,
7350
+ title && typeof title === "string" && title.trim() ? title.trim() : `Decision ${parsed.decisionCount + 1}`,
7351
+ body,
7352
+ "No decisions recorded yet."
7353
+ );
7354
+ await writeFileForce(decisionPath, nextContent);
7355
+ const assignment = await getAssignmentDetailById(projectsDir2, assignmentsDir2, id);
7356
+ res.status(201).json({ assignment, content: nextContent });
7357
+ } catch (error) {
7358
+ console.error("Error appending standalone decision entry:", error);
7359
+ res.status(500).json({ error: `Failed to append decision entry: ${error.message}` });
6225
7360
  }
6226
7361
  });
6227
- insertAll(allSessions);
6228
- console.log(`Migrated ${allSessions.length} sessions from markdown to SQLite.`);
6229
- return allSessions.length;
6230
- }
6231
- async function parseMarkdownSessionsIndex(filePath, projectSlug) {
6232
- const { readFile: readFile25 } = await import("fs/promises");
6233
- const raw = await readFile25(filePath, "utf-8");
6234
- const sessions = [];
6235
- const lines = raw.split("\n");
6236
- let inTable = false;
6237
- let headerSeen = false;
6238
- for (const line of lines) {
6239
- const trimmed = line.trim();
6240
- if (!trimmed) continue;
6241
- if (trimmed.startsWith("| Assignment") || trimmed.startsWith("|Assignment")) {
6242
- inTable = true;
6243
- headerSeen = false;
6244
- continue;
7362
+ router.post("/api/assignments/:id/status-override", async (req, res) => {
7363
+ try {
7364
+ if (!assignmentsDir2) {
7365
+ res.status(501).json({ error: "Standalone assignments not configured on this server" });
7366
+ return;
7367
+ }
7368
+ const id = getParam(req.params.id);
7369
+ const resolved = await resolveAssignmentById(projectsDir2, assignmentsDir2, id);
7370
+ if (!resolved) {
7371
+ res.status(404).json({ error: `Assignment "${id}" not found` });
7372
+ return;
7373
+ }
7374
+ const assignmentPath = resolve14(resolved.assignmentDir, "assignment.md");
7375
+ if (!await fileExists(assignmentPath)) {
7376
+ res.status(404).json({ error: "Assignment not found" });
7377
+ return;
7378
+ }
7379
+ const { status } = req.body || {};
7380
+ const config = await getStatusConfig();
7381
+ const validStatuses = config.statuses.map((s) => s.id);
7382
+ if (typeof status !== "string" || !validStatuses.includes(status)) {
7383
+ res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}.` });
7384
+ return;
7385
+ }
7386
+ let content = await readFile10(assignmentPath, "utf-8");
7387
+ content = setTopLevelField(content, "status", status);
7388
+ content = setTopLevelField(content, "updated", nowTimestamp());
7389
+ if (status !== "blocked") {
7390
+ content = setTopLevelField(content, "blockedReason", null);
7391
+ }
7392
+ await writeFileForce(assignmentPath, content);
7393
+ const assignment = await getAssignmentDetailById(projectsDir2, assignmentsDir2, id);
7394
+ res.json({ assignment });
7395
+ } catch (error) {
7396
+ console.error("Error overriding standalone status:", error);
7397
+ res.status(500).json({ error: `Failed to override status: ${error.message}` });
6245
7398
  }
6246
- if (inTable && !headerSeen && trimmed.match(/^\|[-\s|]+\|$/)) {
6247
- headerSeen = true;
6248
- continue;
7399
+ });
7400
+ router.patch("/api/assignments/:id/acceptance-criteria/:index", async (req, res) => {
7401
+ try {
7402
+ if (!assignmentsDir2) {
7403
+ res.status(501).json({ error: "Standalone assignments not configured on this server" });
7404
+ return;
7405
+ }
7406
+ const id = getParam(req.params.id);
7407
+ const resolved = await resolveAssignmentById(projectsDir2, assignmentsDir2, id);
7408
+ if (!resolved) {
7409
+ res.status(404).json({ error: `Assignment "${id}" not found` });
7410
+ return;
7411
+ }
7412
+ const assignmentPath = resolve14(resolved.assignmentDir, "assignment.md");
7413
+ const currentContent = await readCurrentDocument(assignmentPath);
7414
+ if (!currentContent) {
7415
+ res.status(404).json({ error: "Assignment not found" });
7416
+ return;
7417
+ }
7418
+ const { checked } = req.body || {};
7419
+ if (typeof checked !== "boolean") {
7420
+ res.status(400).json({ error: "checked must be a boolean" });
7421
+ return;
7422
+ }
7423
+ const index = Number.parseInt(getParam(req.params.index), 10);
7424
+ const result = toggleAcceptanceCriterion(currentContent, index, checked);
7425
+ if ("error" in result) {
7426
+ res.status(400).json({ error: result.error });
7427
+ return;
7428
+ }
7429
+ const nextContent = setTopLevelField(result.content, "updated", nowTimestamp());
7430
+ await writeFileForce(assignmentPath, nextContent);
7431
+ const assignment = await getAssignmentDetailById(projectsDir2, assignmentsDir2, id);
7432
+ res.json({ assignment, content: nextContent });
7433
+ } catch (error) {
7434
+ console.error("Error toggling standalone acceptance criterion:", error);
7435
+ res.status(500).json({ error: `Failed to toggle acceptance criterion: ${error.message}` });
6249
7436
  }
6250
- if (inTable && headerSeen && trimmed.startsWith("|")) {
6251
- const cells = trimmed.split("|").slice(1, -1).map((c2) => c2.trim());
6252
- if (cells.length >= 6) {
6253
- sessions.push({
6254
- assignmentSlug: cells[0],
6255
- agent: cells[1],
6256
- sessionId: cells[2],
6257
- started: cells[3],
6258
- status: cells[4] || "active",
6259
- path: cells[5],
6260
- projectSlug
6261
- });
7437
+ });
7438
+ router.post("/api/assignments/:id/transitions/:command", async (req, res) => {
7439
+ try {
7440
+ if (!assignmentsDir2) {
7441
+ res.status(501).json({ error: "Standalone assignments not configured on this server" });
7442
+ return;
7443
+ }
7444
+ const id = getParam(req.params.id);
7445
+ const command = getParam(req.params.command);
7446
+ const resolved = await resolveAssignmentById(projectsDir2, assignmentsDir2, id);
7447
+ if (!resolved) {
7448
+ res.status(404).json({ error: `Assignment "${id}" not found` });
7449
+ return;
7450
+ }
7451
+ const { reason } = req.body || {};
7452
+ const transitionResult = await executeTransitionByDir(
7453
+ resolved.assignmentDir,
7454
+ command,
7455
+ {
7456
+ standalone: resolved.standalone,
7457
+ reason: typeof reason === "string" ? reason : void 0
7458
+ }
7459
+ );
7460
+ if (!transitionResult.success) {
7461
+ res.status(400).json({ error: transitionResult.message, fromStatus: transitionResult.fromStatus });
7462
+ return;
6262
7463
  }
7464
+ const detail = resolved.standalone ? await getAssignmentDetailById(projectsDir2, assignmentsDir2, id) : await getAssignmentDetail(projectsDir2, resolved.projectSlug, resolved.assignmentSlug);
7465
+ res.json({ assignment: detail, warnings: transitionResult.warnings ?? [] });
7466
+ } catch (error) {
7467
+ console.error("Error transitioning by id:", error);
7468
+ res.status(500).json({ error: `Failed to transition: ${error.message}` });
6263
7469
  }
6264
- }
6265
- return sessions;
7470
+ });
7471
+ return router;
6266
7472
  }
6267
-
6268
- // src/dashboard/agent-sessions.ts
6269
- function rowToSession(row) {
6270
- return {
6271
- sessionId: row.session_id,
6272
- projectSlug: row.project_slug ?? null,
6273
- assignmentSlug: row.assignment_slug ?? null,
6274
- agent: row.agent,
6275
- started: row.started,
6276
- ended: row.ended ?? null,
6277
- status: row.status,
6278
- path: row.path ?? "",
6279
- description: row.description ?? null
7473
+ function slugifyLocal(input2) {
7474
+ return input2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "untitled";
7475
+ }
7476
+ async function appendCommentTo(assignmentDir, assignmentRef, req, res, reloadDetail) {
7477
+ const commentsPath = resolve14(assignmentDir, "comments.md");
7478
+ const { body, author, type, replyTo } = req.body || {};
7479
+ if (!body || typeof body !== "string" || !body.trim()) {
7480
+ res.status(400).json({ error: "body is required" });
7481
+ return;
7482
+ }
7483
+ const commentType = type && ["question", "note", "feedback"].includes(type) ? type : "note";
7484
+ const timestamp = nowTimestamp();
7485
+ const entryAuthor = typeof author === "string" && author.trim() ? author.trim() : "human";
7486
+ let currentContent;
7487
+ let currentCount = 0;
7488
+ if (await fileExists(commentsPath)) {
7489
+ currentContent = await readFile10(commentsPath, "utf-8");
7490
+ const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
7491
+ if (countMatch) currentCount = parseInt(countMatch[1], 10);
7492
+ } else {
7493
+ currentContent = renderComments({ assignment: assignmentRef, timestamp });
7494
+ }
7495
+ const comment = {
7496
+ id: generateId().split("-")[0],
7497
+ timestamp,
7498
+ author: entryAuthor,
7499
+ type: commentType,
7500
+ body,
7501
+ replyTo: typeof replyTo === "string" && replyTo.trim() ? replyTo.trim() : void 0,
7502
+ resolved: commentType === "question" ? false : void 0
6280
7503
  };
7504
+ const entry = formatCommentEntry(comment);
7505
+ let next = setTopLevelField(currentContent, "entryCount", String(currentCount + 1));
7506
+ next = setTopLevelField(next, "updated", `"${timestamp}"`);
7507
+ if (next.includes("No comments yet.")) {
7508
+ next = next.replace("No comments yet.", entry.trimEnd());
7509
+ } else {
7510
+ next = `${next.trimEnd()}
7511
+
7512
+ ${entry}`;
7513
+ }
7514
+ await writeFileForce(commentsPath, next);
7515
+ const assignment = await reloadDetail();
7516
+ res.status(201).json({ assignment, comment: { id: comment.id } });
6281
7517
  }
6282
- async function appendSession(_projectDir, session) {
6283
- const db2 = getSessionDb();
6284
- db2.prepare(`
6285
- INSERT INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path, description)
6286
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
6287
- `).run(
6288
- session.sessionId,
6289
- session.projectSlug ?? null,
6290
- session.assignmentSlug ?? null,
6291
- session.agent,
6292
- session.started,
6293
- session.status,
6294
- session.path,
6295
- session.description ?? null
7518
+ async function toggleCommentResolvedAt(assignmentDir, commentId, req, res, reloadDetail) {
7519
+ const commentsPath = resolve14(assignmentDir, "comments.md");
7520
+ if (!await fileExists(commentsPath)) {
7521
+ res.status(404).json({ error: "Comments file not found" });
7522
+ return;
7523
+ }
7524
+ const { resolved: desired } = req.body || {};
7525
+ if (typeof desired !== "boolean") {
7526
+ res.status(400).json({ error: "resolved (boolean) is required" });
7527
+ return;
7528
+ }
7529
+ const content = await readFile10(commentsPath, "utf-8");
7530
+ const parsed = parseComments(content);
7531
+ const target = parsed.entries.find((e) => e.id === commentId);
7532
+ if (!target) {
7533
+ res.status(404).json({ error: `Comment ${commentId} not found` });
7534
+ return;
7535
+ }
7536
+ if (target.type !== "question") {
7537
+ res.status(400).json({ error: "Only questions can be resolved" });
7538
+ return;
7539
+ }
7540
+ const entryBlockRegex = new RegExp(
7541
+ `(^## ${commentId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?)(\\*\\*Resolved:\\*\\*\\s*(?:true|false))`,
7542
+ "m"
6296
7543
  );
6297
- }
6298
- async function updateSessionStatus(_projectDir, sessionId, status) {
6299
- const db2 = getSessionDb();
6300
- const isTerminal = status === "completed" || status === "stopped";
6301
- const result = isTerminal ? db2.prepare(
6302
- "UPDATE sessions SET status = ?, ended = datetime('now'), updated_at = datetime('now') WHERE session_id = ?"
6303
- ).run(status, sessionId) : db2.prepare(
6304
- "UPDATE sessions SET status = ?, updated_at = datetime('now') WHERE session_id = ?"
6305
- ).run(status, sessionId);
6306
- return result.changes > 0;
6307
- }
6308
- async function listAllSessions(_projectsDir) {
6309
- const db2 = getSessionDb();
6310
- const rows = db2.prepare("SELECT * FROM sessions ORDER BY started DESC").all();
6311
- return rows.map(rowToSession);
6312
- }
6313
- async function listProjectSessions(_projectsDir, projectSlug, assignmentSlug) {
6314
- const db2 = getSessionDb();
6315
- if (assignmentSlug) {
6316
- const rows2 = db2.prepare(
6317
- "SELECT * FROM sessions WHERE project_slug = ? AND assignment_slug = ? ORDER BY started DESC"
6318
- ).all(projectSlug, assignmentSlug);
6319
- return rows2.map(rowToSession);
7544
+ const next = content.replace(entryBlockRegex, (_m, preamble) => `${preamble}**Resolved:** ${desired ? "true" : "false"}`);
7545
+ if (next === content) {
7546
+ res.status(500).json({ error: "Failed to update resolved flag" });
7547
+ return;
6320
7548
  }
6321
- const rows = db2.prepare("SELECT * FROM sessions WHERE project_slug = ? ORDER BY started DESC").all(projectSlug);
6322
- return rows.map(rowToSession);
6323
- }
6324
- async function deleteSessions(sessionIds) {
6325
- if (sessionIds.length === 0) return 0;
6326
- const db2 = getSessionDb();
6327
- const placeholders = sessionIds.map(() => "?").join(", ");
6328
- const result = db2.prepare(`DELETE FROM sessions WHERE session_id IN (${placeholders})`).run(...sessionIds);
6329
- return result.changes;
6330
- }
6331
- var DONE_ASSIGNMENT_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "review"]);
6332
- async function readAssignmentStatus(projectDir, assignmentSlug) {
6333
- const assignmentPath = resolve13(projectDir, "assignments", assignmentSlug, "assignment.md");
6334
- if (!await fileExists(assignmentPath)) return null;
6335
- const raw = await readFile9(assignmentPath, "utf-8");
6336
- const match = raw.match(/^status:\s*(.+)$/m);
6337
- return match ? match[1].trim() : null;
7549
+ const withUpdated = setTopLevelField(next, "updated", `"${nowTimestamp()}"`);
7550
+ await writeFileForce(commentsPath, withUpdated);
7551
+ const assignment = await reloadDetail();
7552
+ res.json({ assignment });
6338
7553
  }
6339
- async function reconcileActiveSessions(projectsDir2) {
6340
- const db2 = getSessionDb();
6341
- const activeSessions = db2.prepare("SELECT * FROM sessions WHERE status = 'active' AND project_slug IS NOT NULL AND assignment_slug IS NOT NULL").all();
6342
- if (activeSessions.length === 0) return 0;
6343
- const toCheck = /* @__PURE__ */ new Map();
6344
- for (const session of activeSessions) {
6345
- const slugs = toCheck.get(session.project_slug) ?? /* @__PURE__ */ new Set();
6346
- slugs.add(session.assignment_slug);
6347
- toCheck.set(session.project_slug, slugs);
6348
- }
6349
- const assignmentStatuses = /* @__PURE__ */ new Map();
6350
- for (const [projectSlug, slugs] of toCheck) {
6351
- const projectDir = resolve13(projectsDir2, projectSlug);
6352
- for (const slug of slugs) {
6353
- const status = await readAssignmentStatus(projectDir, slug);
6354
- if (status) assignmentStatuses.set(`${projectSlug}/${slug}`, status);
7554
+
7555
+ // src/dashboard/api-servers.ts
7556
+ init_servers();
7557
+ init_scanner();
7558
+ import { Router as Router2 } from "express";
7559
+ function createServersRouter(serversDir2, projectsDir2, assignmentsDir2) {
7560
+ const router = Router2();
7561
+ router.get("/", async (_req, res) => {
7562
+ try {
7563
+ const result = await scanAllSessions(serversDir2, projectsDir2, { assignmentsDir: assignmentsDir2 });
7564
+ res.json(result);
7565
+ } catch (error) {
7566
+ res.status(500).json({ error: error instanceof Error ? error.message : "Scan failed" });
7567
+ }
7568
+ });
7569
+ router.get("/:name", async (req, res) => {
7570
+ try {
7571
+ const session = await scanSingleSession(serversDir2, projectsDir2, req.params.name, { assignmentsDir: assignmentsDir2 });
7572
+ if (!session) {
7573
+ res.status(404).json({ error: "Session not found" });
7574
+ return;
7575
+ }
7576
+ res.json(session);
7577
+ } catch (error) {
7578
+ res.status(500).json({ error: error instanceof Error ? error.message : "Scan failed" });
7579
+ }
7580
+ });
7581
+ router.post("/", async (req, res) => {
7582
+ try {
7583
+ const { name } = req.body;
7584
+ if (!name || typeof name !== "string") {
7585
+ res.status(400).json({ error: "name is required" });
7586
+ return;
7587
+ }
7588
+ const sanitized = sanitizeSessionName(name);
7589
+ const existing = await readSessionFile(serversDir2, sanitized);
7590
+ if (existing) {
7591
+ res.status(409).json({ error: `Session "${sanitized}" already registered` });
7592
+ return;
7593
+ }
7594
+ await registerSession(serversDir2, name);
7595
+ clearScanCache();
7596
+ res.status(201).json({ name: sanitized });
7597
+ } catch (error) {
7598
+ res.status(500).json({ error: error instanceof Error ? error.message : "Registration failed" });
7599
+ }
7600
+ });
7601
+ router.delete("/:name", async (req, res) => {
7602
+ try {
7603
+ const data = await readSessionFile(serversDir2, req.params.name);
7604
+ if (!data) {
7605
+ res.status(404).json({ error: "Session not found" });
7606
+ return;
7607
+ }
7608
+ await removeSession(serversDir2, req.params.name);
7609
+ clearScanCache();
7610
+ res.json({ removed: req.params.name });
7611
+ } catch (error) {
7612
+ res.status(500).json({ error: error instanceof Error ? error.message : "Removal failed" });
6355
7613
  }
6356
- }
6357
- let totalUpdated = 0;
6358
- for (const session of activeSessions) {
6359
- const key = `${session.project_slug}/${session.assignment_slug}`;
6360
- const assignmentStatus = assignmentStatuses.get(key);
6361
- if (!assignmentStatus || !DONE_ASSIGNMENT_STATUSES.has(assignmentStatus)) continue;
6362
- const newStatus = assignmentStatus === "failed" ? "stopped" : "completed";
6363
- await updateSessionStatus("", session.session_id, newStatus);
6364
- totalUpdated++;
6365
- }
6366
- return totalUpdated;
7614
+ });
7615
+ router.post("/refresh", async (_req, res) => {
7616
+ try {
7617
+ const names = await listSessionFiles(serversDir2);
7618
+ for (const name of names) {
7619
+ await updateLastRefreshed(serversDir2, name);
7620
+ }
7621
+ clearScanCache();
7622
+ const result = await scanAllSessions(serversDir2, projectsDir2, { bypassCache: true, assignmentsDir: assignmentsDir2 });
7623
+ res.json(result);
7624
+ } catch (error) {
7625
+ res.status(500).json({ error: error instanceof Error ? error.message : "Refresh failed" });
7626
+ }
7627
+ });
7628
+ router.post("/:name/refresh", async (req, res) => {
7629
+ try {
7630
+ const data = await readSessionFile(serversDir2, req.params.name);
7631
+ if (!data) {
7632
+ res.status(404).json({ error: "Session not found" });
7633
+ return;
7634
+ }
7635
+ await updateLastRefreshed(serversDir2, req.params.name);
7636
+ clearScanCache();
7637
+ const session = await scanSingleSession(serversDir2, projectsDir2, req.params.name, { assignmentsDir: assignmentsDir2 });
7638
+ res.json(session);
7639
+ } catch (error) {
7640
+ res.status(500).json({ error: error instanceof Error ? error.message : "Refresh failed" });
7641
+ }
7642
+ });
7643
+ router.patch("/:name/panes/:windowIndex/:paneIndex/assignment", async (req, res) => {
7644
+ try {
7645
+ const { name, windowIndex, paneIndex } = req.params;
7646
+ const data = await readSessionFile(serversDir2, name);
7647
+ if (!data) {
7648
+ res.status(404).json({ error: "Session not found" });
7649
+ return;
7650
+ }
7651
+ const body = req.body;
7652
+ if (body === null || body && body.project && body.assignment) {
7653
+ await setOverride(
7654
+ serversDir2,
7655
+ name,
7656
+ parseInt(windowIndex, 10),
7657
+ parseInt(paneIndex, 10),
7658
+ body
7659
+ );
7660
+ clearScanCache();
7661
+ res.json({ updated: true });
7662
+ } else {
7663
+ res.status(400).json({ error: "Body must be { project, assignment } or null" });
7664
+ }
7665
+ } catch (error) {
7666
+ res.status(500).json({ error: error instanceof Error ? error.message : "Update failed" });
7667
+ }
7668
+ });
7669
+ return router;
6367
7670
  }
6368
7671
 
6369
7672
  // src/dashboard/api-agent-sessions.ts
7673
+ import { Router as Router3 } from "express";
7674
+ import { resolve as resolve15 } from "path";
6370
7675
  init_fs();
6371
- function createAgentSessionsRouter(projectsDir2, broadcast) {
7676
+ function createAgentSessionsRouter(projectsDir2, broadcast, assignmentsDir2) {
6372
7677
  const router = Router3();
6373
7678
  router.get("/", async (_req, res) => {
6374
7679
  try {
6375
- await reconcileActiveSessions(projectsDir2);
7680
+ await reconcileActiveSessions(projectsDir2, assignmentsDir2);
6376
7681
  const sessions = await listAllSessions(projectsDir2);
6377
7682
  res.json({ sessions, generatedAt: (/* @__PURE__ */ new Date()).toISOString() });
6378
7683
  } catch (error) {
@@ -6383,12 +7688,12 @@ function createAgentSessionsRouter(projectsDir2, broadcast) {
6383
7688
  try {
6384
7689
  const { projectSlug } = req.params;
6385
7690
  const assignment = req.query.assignment;
6386
- const projectDir = resolve14(projectsDir2, projectSlug);
7691
+ const projectDir = resolve15(projectsDir2, projectSlug);
6387
7692
  if (!await fileExists(projectDir)) {
6388
7693
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
6389
7694
  return;
6390
7695
  }
6391
- await reconcileActiveSessions(projectsDir2);
7696
+ await reconcileActiveSessions(projectsDir2, assignmentsDir2);
6392
7697
  const sessions = await listProjectSessions(projectsDir2, projectSlug, assignment);
6393
7698
  res.json({ sessions, generatedAt: (/* @__PURE__ */ new Date()).toISOString() });
6394
7699
  } catch (error) {
@@ -6397,32 +7702,38 @@ function createAgentSessionsRouter(projectsDir2, broadcast) {
6397
7702
  });
6398
7703
  router.post("/", async (req, res) => {
6399
7704
  try {
6400
- const { projectSlug, assignmentSlug, agent, sessionId, path, description } = req.body;
7705
+ const { projectSlug, assignmentSlug, agent, sessionId, path, description, transcriptPath } = req.body;
6401
7706
  if (!agent) {
6402
7707
  res.status(400).json({ error: "agent is required" });
6403
7708
  return;
6404
7709
  }
7710
+ if (!sessionId) {
7711
+ res.status(400).json({
7712
+ error: "sessionId is required. Pass the real agent-generated session id \u2014 do not synthesize one."
7713
+ });
7714
+ return;
7715
+ }
6405
7716
  if (projectSlug) {
6406
- const projectDir = resolve14(projectsDir2, projectSlug);
7717
+ const projectDir = resolve15(projectsDir2, projectSlug);
6407
7718
  if (!await fileExists(projectDir)) {
6408
7719
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
6409
7720
  return;
6410
7721
  }
6411
7722
  }
6412
- const id = sessionId || randomUUID2();
6413
7723
  const session = {
6414
7724
  projectSlug: projectSlug || null,
6415
7725
  assignmentSlug: assignmentSlug || null,
6416
7726
  agent,
6417
- sessionId: id,
7727
+ sessionId,
6418
7728
  started: (/* @__PURE__ */ new Date()).toISOString(),
6419
7729
  status: "active",
6420
7730
  path: path || "",
6421
- description: description || null
7731
+ description: description || null,
7732
+ transcriptPath: transcriptPath || null
6422
7733
  };
6423
7734
  await appendSession("", session);
6424
7735
  broadcast?.({ type: "agent-sessions-updated", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
6425
- res.status(201).json({ sessionId: id });
7736
+ res.status(201).json({ sessionId });
6426
7737
  } catch (error) {
6427
7738
  res.status(500).json({ error: error instanceof Error ? error.message : "Registration failed" });
6428
7739
  }
@@ -6471,8 +7782,8 @@ function createAgentSessionsRouter(projectsDir2, broadcast) {
6471
7782
  init_api();
6472
7783
  init_parser();
6473
7784
  import { Router as Router4 } from "express";
6474
- import { resolve as resolve15 } from "path";
6475
- import { readFile as readFile10, unlink as unlink2 } from "fs/promises";
7785
+ import { resolve as resolve16 } from "path";
7786
+ import { readFile as readFile11, unlink as unlink2 } from "fs/promises";
6476
7787
  init_timestamp();
6477
7788
  init_fs();
6478
7789
  function createPlaybooksRouter(playbooksDir3) {
@@ -6512,12 +7823,12 @@ function createPlaybooksRouter(playbooksDir3) {
6512
7823
  });
6513
7824
  router.get("/:slug/edit", async (req, res) => {
6514
7825
  try {
6515
- const filePath = resolve15(playbooksDir3, `${req.params.slug}.md`);
7826
+ const filePath = resolve16(playbooksDir3, `${req.params.slug}.md`);
6516
7827
  if (!await fileExists(filePath)) {
6517
7828
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
6518
7829
  return;
6519
7830
  }
6520
- const content = await readFile10(filePath, "utf-8");
7831
+ const content = await readFile11(filePath, "utf-8");
6521
7832
  res.json({
6522
7833
  documentType: "playbook",
6523
7834
  title: `Edit Playbook: ${req.params.slug}`,
@@ -6542,7 +7853,7 @@ function createPlaybooksRouter(playbooksDir3) {
6542
7853
  return;
6543
7854
  }
6544
7855
  await ensureDir(playbooksDir3);
6545
- const filePath = resolve15(playbooksDir3, `${slug}.md`);
7856
+ const filePath = resolve16(playbooksDir3, `${slug}.md`);
6546
7857
  if (await fileExists(filePath)) {
6547
7858
  res.status(409).json({ error: `Playbook "${slug}" already exists` });
6548
7859
  return;
@@ -6561,7 +7872,7 @@ function createPlaybooksRouter(playbooksDir3) {
6561
7872
  res.status(400).json({ error: "content is required" });
6562
7873
  return;
6563
7874
  }
6564
- const filePath = resolve15(playbooksDir3, `${req.params.slug}.md`);
7875
+ const filePath = resolve16(playbooksDir3, `${req.params.slug}.md`);
6565
7876
  if (!await fileExists(filePath)) {
6566
7877
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
6567
7878
  return;
@@ -6579,7 +7890,7 @@ function createPlaybooksRouter(playbooksDir3) {
6579
7890
  res.status(403).json({ error: "The playbook manifest cannot be deleted" });
6580
7891
  return;
6581
7892
  }
6582
- const filePath = resolve15(playbooksDir3, `${req.params.slug}.md`);
7893
+ const filePath = resolve16(playbooksDir3, `${req.params.slug}.md`);
6583
7894
  if (!await fileExists(filePath)) {
6584
7895
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
6585
7896
  return;
@@ -6598,7 +7909,7 @@ function createPlaybooksRouter(playbooksDir3) {
6598
7909
  init_parser2();
6599
7910
  init_fs();
6600
7911
  import { Router as Router5 } from "express";
6601
- import { readdir as readdir7 } from "fs/promises";
7912
+ import { readdir as readdir8 } from "fs/promises";
6602
7913
  var WORKSPACE_REGEX = /^[a-z0-9_][a-z0-9-]*$/;
6603
7914
  function getWorkspaceParam(value) {
6604
7915
  if (Array.isArray(value)) {
@@ -6632,7 +7943,7 @@ function createTodosRouter(todosDir2, broadcast) {
6632
7943
  router.get("/", async (_req, res) => {
6633
7944
  try {
6634
7945
  await ensureDir(todosDir2);
6635
- const files = await readdir7(todosDir2).catch(() => []);
7946
+ const files = await readdir8(todosDir2).catch(() => []);
6636
7947
  const workspaces = [];
6637
7948
  for (const file of files) {
6638
7949
  if (typeof file !== "string") continue;
@@ -7017,8 +8328,8 @@ init_fs();
7017
8328
  init_config2();
7018
8329
  import { execFile as execFile2 } from "child_process";
7019
8330
  import { promisify as promisify2 } from "util";
7020
- import { cp, mkdtemp, rm as rm2, readFile as readFile12, writeFile as writeFile3, unlink as unlink3, stat, open, rename as rename2 } from "fs/promises";
7021
- import { resolve as resolve17, join as join2 } from "path";
8331
+ import { cp, mkdtemp, rm as rm2, readFile as readFile13, writeFile as writeFile3, unlink as unlink3, stat, open, rename as rename2 } from "fs/promises";
8332
+ import { resolve as resolve18, join as join2 } from "path";
7022
8333
  import { tmpdir } from "os";
7023
8334
  var exec2 = promisify2(execFile2);
7024
8335
  var VALID_CATEGORIES = ["projects", "playbooks", "todos", "servers", "config"];
@@ -7058,7 +8369,7 @@ async function resolveCategoryPath(category) {
7058
8369
  case "servers":
7059
8370
  return { sourcePath: serversDir(), repoPath: "servers", isFile: false };
7060
8371
  case "config":
7061
- return { sourcePath: resolve17(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
8372
+ return { sourcePath: resolve18(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
7062
8373
  }
7063
8374
  }
7064
8375
  async function checkGitInstalled() {
@@ -7069,7 +8380,7 @@ async function checkGitInstalled() {
7069
8380
  }
7070
8381
  }
7071
8382
  async function acquireLock() {
7072
- const lockPath = resolve17(syntaurRoot(), LOCK_FILE_NAME);
8383
+ const lockPath = resolve18(syntaurRoot(), LOCK_FILE_NAME);
7073
8384
  await ensureDir(syntaurRoot());
7074
8385
  try {
7075
8386
  const handle = await open(lockPath, "wx");
@@ -7078,7 +8389,7 @@ async function acquireLock() {
7078
8389
  return lockPath;
7079
8390
  } catch (err2) {
7080
8391
  if (err2.code === "EEXIST") {
7081
- const pid = await readFile12(lockPath, "utf-8").catch(() => "");
8392
+ const pid = await readFile13(lockPath, "utf-8").catch(() => "");
7082
8393
  throw new Error(
7083
8394
  `Backup operation already in progress (lock file at ${lockPath}, pid ${pid.trim() || "unknown"}). If stale, delete the file and retry.`
7084
8395
  );
@@ -7116,7 +8427,7 @@ async function copyRecursive(src, dest) {
7116
8427
  await ensureDir(dest);
7117
8428
  await cp(src, dest, { recursive: true, force: true });
7118
8429
  } else {
7119
- await ensureDir(resolve17(dest, ".."));
8430
+ await ensureDir(resolve18(dest, ".."));
7120
8431
  await cp(src, dest, { force: true });
7121
8432
  }
7122
8433
  }
@@ -7125,7 +8436,7 @@ function resolveCategoriesStrict(csv) {
7125
8436
  return parseCategoriesStrict(parts);
7126
8437
  }
7127
8438
  async function readSanitizedConfig(configPath) {
7128
- const content = await readFile12(configPath, "utf-8");
8439
+ const content = await readFile13(configPath, "utf-8");
7129
8440
  return content.replace(/^(\s*lastBackup:\s*).*$/m, "$1null").replace(/^(\s*lastRestore:\s*).*$/m, "$1null");
7130
8441
  }
7131
8442
  async function backupToGithub(overrides) {
@@ -7164,7 +8475,7 @@ async function backupToGithub(overrides) {
7164
8475
  }
7165
8476
  if (category === "config") {
7166
8477
  const sanitized = await readSanitizedConfig(sourcePath);
7167
- await ensureDir(resolve17(destPath, ".."));
8478
+ await ensureDir(resolve18(destPath, ".."));
7168
8479
  await writeFile3(destPath, sanitized, "utf-8");
7169
8480
  } else {
7170
8481
  await copyRecursive(sourcePath, destPath);
@@ -7218,7 +8529,7 @@ async function backupToGithub(overrides) {
7218
8529
  }
7219
8530
  async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
7220
8531
  if (isFile) {
7221
- await ensureDir(resolve17(localPath, ".."));
8532
+ await ensureDir(resolve18(localPath, ".."));
7222
8533
  await cp(repoSrcPath, localPath, { force: true });
7223
8534
  return;
7224
8535
  }
@@ -7319,7 +8630,7 @@ async function restoreFromGithub(overrides) {
7319
8630
  }
7320
8631
  async function getBackupStatus() {
7321
8632
  const config = await readConfig();
7322
- const lockPath = resolve17(syntaurRoot(), LOCK_FILE_NAME);
8633
+ const lockPath = resolve18(syntaurRoot(), LOCK_FILE_NAME);
7323
8634
  const locked = await fileExists(lockPath);
7324
8635
  return {
7325
8636
  repo: config.backup?.repo ?? null,
@@ -7474,7 +8785,7 @@ async function stopAutodiscovery() {
7474
8785
  function runReconcile() {
7475
8786
  if (activeReconcile || !savedOptions) return;
7476
8787
  const opts = savedOptions;
7477
- activeReconcile = reconcile(opts.serversDir, opts.projectsDir, opts.excludePids).catch((err2) => {
8788
+ activeReconcile = reconcile(opts.serversDir, opts.projectsDir, opts.excludePids, opts.assignmentsDir).catch((err2) => {
7478
8789
  console.error("[autodiscovery] reconcile failed:", err2);
7479
8790
  }).finally(() => {
7480
8791
  activeReconcile = null;
@@ -7485,10 +8796,10 @@ async function listAllTmuxSessions() {
7485
8796
  if (!output2) return [];
7486
8797
  return output2.split("\n").filter((line) => line.length > 0);
7487
8798
  }
7488
- async function discoverTmuxSessions(serversDir2, projectsDir2, existingNames) {
8799
+ async function discoverTmuxSessions(serversDir2, projectsDir2, existingNames, assignmentsDir2) {
7489
8800
  const tmuxAvailable = await checkTmuxAvailable();
7490
8801
  if (!tmuxAvailable) return false;
7491
- const workspaceRecords = await loadWorkspaceRecords(projectsDir2);
8802
+ const workspaceRecords = await loadWorkspaceRecords(projectsDir2, assignmentsDir2);
7492
8803
  if (workspaceRecords.length === 0) return false;
7493
8804
  const sessions = await listAllTmuxSessions();
7494
8805
  let changed = false;
@@ -7529,8 +8840,8 @@ async function getProcessCwd(pid) {
7529
8840
  }
7530
8841
  return null;
7531
8842
  }
7532
- async function discoverProcesses(serversDir2, projectsDir2, existingFiles, excludePids) {
7533
- const workspaceRecords = await loadWorkspaceRecords(projectsDir2);
8843
+ async function discoverProcesses(serversDir2, projectsDir2, existingFiles, excludePids, assignmentsDir2) {
8844
+ const workspaceRecords = await loadWorkspaceRecords(projectsDir2, assignmentsDir2);
7534
8845
  if (workspaceRecords.length === 0) return false;
7535
8846
  const lsofOutput = await getLsofOutput();
7536
8847
  if (!lsofOutput) return false;
@@ -7595,7 +8906,7 @@ async function isProcessAlive(pid) {
7595
8906
  return false;
7596
8907
  }
7597
8908
  }
7598
- async function reconcile(serversDir2, projectsDir2, excludePids) {
8909
+ async function reconcile(serversDir2, projectsDir2, excludePids, assignmentsDir2) {
7599
8910
  const names = await listSessionFiles(serversDir2);
7600
8911
  const existingFiles = /* @__PURE__ */ new Map();
7601
8912
  for (const name of names) {
@@ -7607,8 +8918,8 @@ async function reconcile(serversDir2, projectsDir2, excludePids) {
7607
8918
  existingFiles.delete(name);
7608
8919
  }
7609
8920
  const existingNames = new Set(existingFiles.keys());
7610
- const tmuxChanged = await discoverTmuxSessions(serversDir2, projectsDir2, existingNames);
7611
- const processChanged = await discoverProcesses(serversDir2, projectsDir2, existingFiles, excludePids);
8921
+ const tmuxChanged = await discoverTmuxSessions(serversDir2, projectsDir2, existingNames, assignmentsDir2);
8922
+ const processChanged = await discoverProcesses(serversDir2, projectsDir2, existingFiles, excludePids, assignmentsDir2);
7612
8923
  if (tmuxChanged || processChanged || cleanupChanged) {
7613
8924
  clearScanCache();
7614
8925
  }
@@ -7616,7 +8927,7 @@ async function reconcile(serversDir2, projectsDir2, excludePids) {
7616
8927
 
7617
8928
  // src/dashboard/server.ts
7618
8929
  function createDashboardServer(options) {
7619
- const { port, projectsDir: projectsDir2, serversDir: serversDir2, playbooksDir: playbooksDir3, todosDir: todosDir2, serveStaticUi, dashboardDistPath } = options;
8930
+ const { port, projectsDir: projectsDir2, assignmentsDir: assignmentsDir2, serversDir: serversDir2, playbooksDir: playbooksDir3, todosDir: todosDir2, serveStaticUi, dashboardDistPath } = options;
7620
8931
  const app = express();
7621
8932
  const server = createServer(app);
7622
8933
  const wss = new WebSocketServer({ noServer: true });
@@ -7656,7 +8967,7 @@ function createDashboardServer(options) {
7656
8967
  app.use(express.json());
7657
8968
  app.get("/api/overview", async (_req, res) => {
7658
8969
  try {
7659
- const overview = await getOverview(projectsDir2, serversDir2);
8970
+ const overview = await getOverview(projectsDir2, serversDir2, assignmentsDir2);
7660
8971
  res.json(overview);
7661
8972
  } catch (error) {
7662
8973
  console.error("Error getting overview:", error);
@@ -7665,7 +8976,7 @@ function createDashboardServer(options) {
7665
8976
  });
7666
8977
  app.get("/api/attention", async (_req, res) => {
7667
8978
  try {
7668
- const attention = await getAttention(projectsDir2, serversDir2);
8979
+ const attention = await getAttention(projectsDir2, serversDir2, assignmentsDir2);
7669
8980
  res.json(attention);
7670
8981
  } catch (error) {
7671
8982
  console.error("Error getting attention queue:", error);
@@ -7785,7 +9096,7 @@ function createDashboardServer(options) {
7785
9096
  });
7786
9097
  app.get("/api/assignments", async (req, res) => {
7787
9098
  try {
7788
- const result = await listAssignmentsBoard(projectsDir2);
9099
+ const result = await listAssignmentsBoard(projectsDir2, assignmentsDir2);
7789
9100
  const workspaceParam = req.query.workspace;
7790
9101
  if (workspaceParam) {
7791
9102
  if (workspaceParam === "_ungrouped") {
@@ -7813,6 +9124,37 @@ function createDashboardServer(options) {
7813
9124
  res.status(500).json({ error: "Failed to get project detail" });
7814
9125
  }
7815
9126
  });
9127
+ app.get("/api/assignments/:id", async (req, res) => {
9128
+ try {
9129
+ const detail = await getAssignmentDetailById(projectsDir2, assignmentsDir2, req.params.id);
9130
+ if (!detail) {
9131
+ res.status(404).json({ error: `Assignment "${req.params.id}" not found` });
9132
+ return;
9133
+ }
9134
+ res.json(detail);
9135
+ } catch (error) {
9136
+ console.error("Error getting assignment by id:", error);
9137
+ res.status(500).json({ error: "Failed to get assignment" });
9138
+ }
9139
+ });
9140
+ app.get("/api/assignments/:id/sessions", async (req, res) => {
9141
+ try {
9142
+ const resolved = await resolveAssignmentById(projectsDir2, assignmentsDir2, req.params.id);
9143
+ if (!resolved) {
9144
+ res.status(404).json({ error: `Assignment "${req.params.id}" not found` });
9145
+ return;
9146
+ }
9147
+ await reconcileActiveSessions(projectsDir2, assignmentsDir2);
9148
+ const sessions = await listSessionsByAssignment(
9149
+ resolved.standalone ? null : resolved.projectSlug,
9150
+ resolved.standalone ? resolved.id : resolved.assignmentSlug
9151
+ );
9152
+ res.json({ sessions, generatedAt: (/* @__PURE__ */ new Date()).toISOString() });
9153
+ } catch (error) {
9154
+ console.error("Error listing sessions by id:", error);
9155
+ res.status(500).json({ error: "Failed to list sessions" });
9156
+ }
9157
+ });
7816
9158
  app.get("/api/projects/:slug/assignments/:aslug", async (req, res) => {
7817
9159
  try {
7818
9160
  const detail = await getAssignmentDetail(
@@ -7832,16 +9174,16 @@ function createDashboardServer(options) {
7832
9174
  res.status(500).json({ error: "Failed to get assignment detail" });
7833
9175
  }
7834
9176
  });
7835
- app.use(createWriteRouter(projectsDir2));
7836
- app.use("/api/servers", createServersRouter(serversDir2, projectsDir2));
7837
- app.use("/api/agent-sessions", createAgentSessionsRouter(projectsDir2, broadcast));
9177
+ app.use(createWriteRouter(projectsDir2, assignmentsDir2));
9178
+ app.use("/api/servers", createServersRouter(serversDir2, projectsDir2, assignmentsDir2));
9179
+ app.use("/api/agent-sessions", createAgentSessionsRouter(projectsDir2, broadcast, assignmentsDir2));
7838
9180
  app.use("/api/playbooks", createPlaybooksRouter(playbooksDir3));
7839
9181
  app.use("/api/todos", createTodosRouter(todosDir2, broadcast));
7840
9182
  app.use("/api/backup", createBackupRouter());
7841
9183
  if (serveStaticUi && dashboardDistPath) {
7842
9184
  app.use(express.static(dashboardDistPath));
7843
9185
  app.get("{*path}", async (_req, res) => {
7844
- const indexPath = resolve18(dashboardDistPath, "index.html");
9186
+ const indexPath = resolve19(dashboardDistPath, "index.html");
7845
9187
  if (await fileExists(indexPath)) {
7846
9188
  res.sendFile(indexPath);
7847
9189
  } else {
@@ -7856,12 +9198,13 @@ function createDashboardServer(options) {
7856
9198
  async start() {
7857
9199
  watcherHandle = createWatcher({
7858
9200
  projectsDir: projectsDir2,
9201
+ assignmentsDir: assignmentsDir2,
7859
9202
  serversDir: serversDir2,
7860
9203
  playbooksDir: playbooksDir3,
7861
9204
  todosDir: todosDir2,
7862
9205
  onMessage: broadcast
7863
9206
  });
7864
- startAutodiscovery({ serversDir: serversDir2, projectsDir: projectsDir2, excludePids: /* @__PURE__ */ new Set([process.pid]) });
9207
+ startAutodiscovery({ serversDir: serversDir2, projectsDir: projectsDir2, assignmentsDir: assignmentsDir2, excludePids: /* @__PURE__ */ new Set([process.pid]) });
7865
9208
  return new Promise((resolvePromise, reject) => {
7866
9209
  server.on("error", (err2) => {
7867
9210
  if (err2.code === "EADDRINUSE") {
@@ -7873,7 +9216,7 @@ function createDashboardServer(options) {
7873
9216
  }
7874
9217
  });
7875
9218
  server.listen(port, () => {
7876
- const portFile = resolve18(syntaurRoot(), "dashboard-port");
9219
+ const portFile = resolve19(syntaurRoot(), "dashboard-port");
7877
9220
  writeFile4(portFile, String(port), "utf-8").catch(() => {
7878
9221
  });
7879
9222
  resolvePromise();
@@ -7890,7 +9233,7 @@ function createDashboardServer(options) {
7890
9233
  client.terminate();
7891
9234
  }
7892
9235
  clients.clear();
7893
- const portFile = resolve18(syntaurRoot(), "dashboard-port");
9236
+ const portFile = resolve19(syntaurRoot(), "dashboard-port");
7894
9237
  await unlink4(portFile).catch(() => {
7895
9238
  });
7896
9239
  server.closeAllConnections?.();
@@ -7970,11 +9313,12 @@ async function dashboardCommand(options) {
7970
9313
  port = availablePort;
7971
9314
  }
7972
9315
  const thisFile = fileURLToPath2(import.meta.url);
7973
- const packageRoot = resolve19(dirname4(thisFile), "..");
7974
- const dashboardDist = resolve19(packageRoot, "dashboard", "dist");
9316
+ const packageRoot = resolve20(dirname4(thisFile), "..");
9317
+ const dashboardDist = resolve20(packageRoot, "dashboard", "dist");
7975
9318
  const server = createDashboardServer({
7976
9319
  port,
7977
9320
  projectsDir: projectsDir2,
9321
+ assignmentsDir: assignmentsDir(),
7978
9322
  serversDir: serversDir(),
7979
9323
  playbooksDir: playbooksDir(),
7980
9324
  todosDir: todosDir(),
@@ -7984,8 +9328,8 @@ async function dashboardCommand(options) {
7984
9328
  await server.start();
7985
9329
  let viteProcess = null;
7986
9330
  if (mode === "dev") {
7987
- const dashboardDir = resolve19(packageRoot, "dashboard");
7988
- const viteBin = resolve19(dashboardDir, "node_modules", ".bin", "vite");
9331
+ const dashboardDir = resolve20(packageRoot, "dashboard");
9332
+ const viteBin = resolve20(dashboardDir, "node_modules", ".bin", "vite");
7989
9333
  if (!await fileExists(viteBin)) {
7990
9334
  console.error(
7991
9335
  'Vite not found. Run "npm ci --prefix dashboard" first, or use the default bundled dashboard mode.'
@@ -8059,71 +9403,7 @@ init_fs();
8059
9403
  init_config2();
8060
9404
  import { resolve as resolve21 } from "path";
8061
9405
  init_lifecycle();
8062
-
8063
- // src/utils/assignment-resolver.ts
8064
- init_fs();
8065
- init_parser();
8066
- import { resolve as resolve20 } from "path";
8067
- import { readdir as readdir8, readFile as readFile13 } from "fs/promises";
8068
- async function resolveAssignmentById(projectsDir2, assignmentsDir2, id) {
8069
- let standaloneMatch = null;
8070
- let projectMatch = null;
8071
- const standaloneDir = resolve20(assignmentsDir2, id);
8072
- const standalonePath = resolve20(standaloneDir, "assignment.md");
8073
- if (await fileExists(standalonePath)) {
8074
- standaloneMatch = {
8075
- assignmentDir: standaloneDir,
8076
- projectSlug: null,
8077
- assignmentSlug: id,
8078
- id,
8079
- standalone: true
8080
- };
8081
- }
8082
- if (await fileExists(projectsDir2)) {
8083
- try {
8084
- const projects = await readdir8(projectsDir2, { withFileTypes: true });
8085
- for (const p of projects) {
8086
- if (!p.isDirectory()) continue;
8087
- if (p.name.startsWith(".") || p.name.startsWith("_")) continue;
8088
- const assignmentsPath = resolve20(projectsDir2, p.name, "assignments");
8089
- if (!await fileExists(assignmentsPath)) continue;
8090
- const entries = await readdir8(assignmentsPath, { withFileTypes: true });
8091
- for (const a of entries) {
8092
- if (!a.isDirectory()) continue;
8093
- const aPath = resolve20(assignmentsPath, a.name, "assignment.md");
8094
- if (!await fileExists(aPath)) continue;
8095
- try {
8096
- const content = await readFile13(aPath, "utf-8");
8097
- const [fm] = extractFrontmatter(content);
8098
- const fileId = getField(fm, "id");
8099
- if (fileId === id) {
8100
- projectMatch = {
8101
- assignmentDir: resolve20(assignmentsPath, a.name),
8102
- projectSlug: p.name,
8103
- assignmentSlug: a.name,
8104
- id,
8105
- standalone: false
8106
- };
8107
- break;
8108
- }
8109
- } catch {
8110
- }
8111
- }
8112
- if (projectMatch) break;
8113
- }
8114
- } catch {
8115
- }
8116
- }
8117
- if (standaloneMatch && projectMatch) {
8118
- console.warn(
8119
- `Duplicate assignment ID ${id} found in both standalone and project-nested locations; using standalone`
8120
- );
8121
- return standaloneMatch;
8122
- }
8123
- return standaloneMatch ?? projectMatch ?? null;
8124
- }
8125
-
8126
- // src/commands/_lifecycle-helper.ts
9406
+ init_assignment_resolver();
8127
9407
  async function runTransition(assignment, command, options = {}) {
8128
9408
  const config = await readConfig();
8129
9409
  const baseDir = options.dir ? expandHome(options.dir) : config.defaultProjectDir;
@@ -9501,7 +10781,7 @@ init_paths();
9501
10781
  init_fs();
9502
10782
  init_config2();
9503
10783
  import { resolve as resolve26 } from "path";
9504
- import { randomUUID as randomUUID3 } from "crypto";
10784
+ import { randomUUID as randomUUID2 } from "crypto";
9505
10785
  async function trackSessionCommand(options) {
9506
10786
  if (!options.agent) {
9507
10787
  throw new Error("--agent <name> is required.");
@@ -9517,7 +10797,7 @@ async function trackSessionCommand(options) {
9517
10797
  }
9518
10798
  }
9519
10799
  initSessionDb();
9520
- const sessionId = options.sessionId || randomUUID3();
10800
+ const sessionId = options.sessionId || randomUUID2();
9521
10801
  await appendSession("", {
9522
10802
  projectSlug: options.project || null,
9523
10803
  assignmentSlug: options.assignment || null,
@@ -10895,6 +12175,7 @@ function pass3(check) {
10895
12175
  init_fs();
10896
12176
  init_parser();
10897
12177
  init_types();
12178
+ init_paths();
10898
12179
  import { resolve as resolve35 } from "path";
10899
12180
  import { readdir as readdir13, readFile as readFile18 } from "fs/promises";
10900
12181
  var CATEGORY4 = "assignment";
@@ -10902,24 +12183,48 @@ var STATUSES_REQUIRING_HANDOFF = /* @__PURE__ */ new Set(["review", "completed"]
10902
12183
  async function listAssignments(ctx) {
10903
12184
  const result = { withAssignmentMd: [], orphanFolders: [] };
10904
12185
  const projectsDir2 = ctx.config.defaultProjectDir;
10905
- if (!await fileExists(projectsDir2)) return result;
10906
- const projects = await readdir13(projectsDir2, { withFileTypes: true });
10907
- for (const m of projects) {
10908
- if (!m.isDirectory()) continue;
10909
- if (m.name.startsWith(".") || m.name.startsWith("_")) continue;
10910
- const assignmentsDir2 = resolve35(projectsDir2, m.name, "assignments");
10911
- if (!await fileExists(assignmentsDir2)) continue;
10912
- const entries = await readdir13(assignmentsDir2, { withFileTypes: true });
12186
+ if (await fileExists(projectsDir2)) {
12187
+ const projects = await readdir13(projectsDir2, { withFileTypes: true });
12188
+ for (const m of projects) {
12189
+ if (!m.isDirectory()) continue;
12190
+ if (m.name.startsWith(".") || m.name.startsWith("_")) continue;
12191
+ const assignmentsDir2 = resolve35(projectsDir2, m.name, "assignments");
12192
+ if (!await fileExists(assignmentsDir2)) continue;
12193
+ const entries = await readdir13(assignmentsDir2, { withFileTypes: true });
12194
+ for (const a of entries) {
12195
+ if (!a.isDirectory()) continue;
12196
+ if (a.name.startsWith(".") || a.name.startsWith("_")) continue;
12197
+ const assignmentDir = resolve35(assignmentsDir2, a.name);
12198
+ const assignmentMd = resolve35(assignmentDir, "assignment.md");
12199
+ const entry = {
12200
+ projectDir: resolve35(projectsDir2, m.name),
12201
+ projectSlug: m.name,
12202
+ assignmentDir,
12203
+ assignmentSlug: a.name,
12204
+ standalone: false
12205
+ };
12206
+ if (await fileExists(assignmentMd)) {
12207
+ result.withAssignmentMd.push(entry);
12208
+ } else {
12209
+ result.orphanFolders.push(entry);
12210
+ }
12211
+ }
12212
+ }
12213
+ }
12214
+ const standaloneRoot = assignmentsDir();
12215
+ if (await fileExists(standaloneRoot)) {
12216
+ const entries = await readdir13(standaloneRoot, { withFileTypes: true });
10913
12217
  for (const a of entries) {
10914
12218
  if (!a.isDirectory()) continue;
10915
12219
  if (a.name.startsWith(".") || a.name.startsWith("_")) continue;
10916
- const assignmentDir = resolve35(assignmentsDir2, a.name);
12220
+ const assignmentDir = resolve35(standaloneRoot, a.name);
10917
12221
  const assignmentMd = resolve35(assignmentDir, "assignment.md");
10918
12222
  const entry = {
10919
- projectDir: resolve35(projectsDir2, m.name),
10920
- projectSlug: m.name,
12223
+ projectDir: standaloneRoot,
12224
+ projectSlug: null,
10921
12225
  assignmentDir,
10922
- assignmentSlug: a.name
12226
+ assignmentSlug: a.name,
12227
+ standalone: true
10923
12228
  };
10924
12229
  if (await fileExists(assignmentMd)) {
10925
12230
  result.withAssignmentMd.push(entry);
@@ -11099,12 +12404,147 @@ var requiredFilesByStatus = {
11099
12404
  return results;
11100
12405
  }
11101
12406
  };
12407
+ var companionFilesScaffolded = {
12408
+ id: "assignment.companion-files",
12409
+ category: CATEGORY4,
12410
+ title: "progress.md and comments.md scaffolded (v2.0)",
12411
+ async run(ctx) {
12412
+ const { withAssignmentMd } = await listAssignments(ctx);
12413
+ const results = [];
12414
+ for (const a of withAssignmentMd) {
12415
+ const missing = [];
12416
+ for (const filename of ["progress.md", "comments.md"]) {
12417
+ if (!await fileExists(resolve35(a.assignmentDir, filename))) {
12418
+ missing.push(filename);
12419
+ }
12420
+ }
12421
+ if (missing.length === 0) continue;
12422
+ const label = a.standalone ? `standalone/${a.assignmentSlug}` : `${a.projectSlug}/${a.assignmentSlug}`;
12423
+ results.push({
12424
+ id: this.id,
12425
+ category: this.category,
12426
+ title: this.title,
12427
+ status: "warn",
12428
+ detail: `${label} is missing ${missing.join(" and ")} (pre-v2.0 assignment \u2014 not required, but scaffolding them keeps the dashboard and CLIs consistent)`,
12429
+ affected: missing.map((m) => resolve35(a.assignmentDir, m)),
12430
+ remediation: {
12431
+ kind: "manual",
12432
+ suggestion: `Create ${missing.join(" and ")} with the renderProgress/renderComments templates, or re-scaffold via the CLI`,
12433
+ command: null
12434
+ },
12435
+ autoFixable: false
12436
+ });
12437
+ }
12438
+ if (results.length === 0) return pass4(this);
12439
+ return results;
12440
+ }
12441
+ };
12442
+ var typeDefinition = {
12443
+ id: "assignment.type-definition",
12444
+ category: CATEGORY4,
12445
+ title: "Assignment `type` is in config.types.definitions",
12446
+ async run(ctx) {
12447
+ const typesConfig = ctx.config.types;
12448
+ if (!typesConfig) {
12449
+ return {
12450
+ id: this.id,
12451
+ category: this.category,
12452
+ title: this.title,
12453
+ status: "skipped",
12454
+ detail: "config.types is not set; applying defaults \u2014 skipping strict validation",
12455
+ autoFixable: false
12456
+ };
12457
+ }
12458
+ const allowed = new Set(typesConfig.definitions.map((d) => d.id));
12459
+ const { withAssignmentMd } = await listAssignments(ctx);
12460
+ const results = [];
12461
+ for (const a of withAssignmentMd) {
12462
+ const path = resolve35(a.assignmentDir, "assignment.md");
12463
+ const parsed = await parseSafe(path);
12464
+ if (!parsed) continue;
12465
+ if (!parsed.type) continue;
12466
+ if (!allowed.has(parsed.type)) {
12467
+ const label = a.standalone ? `standalone/${a.assignmentSlug}` : `${a.projectSlug}/${a.assignmentSlug}`;
12468
+ results.push({
12469
+ id: this.id,
12470
+ category: this.category,
12471
+ title: this.title,
12472
+ status: "warn",
12473
+ detail: `${label}: type "${parsed.type}" is not in config.types.definitions (${[...allowed].join(", ")})`,
12474
+ affected: [path],
12475
+ remediation: {
12476
+ kind: "manual",
12477
+ suggestion: `Either add "${parsed.type}" to config.types.definitions or change the assignment's type to one of the configured values`,
12478
+ command: null
12479
+ },
12480
+ autoFixable: false
12481
+ });
12482
+ }
12483
+ }
12484
+ if (results.length === 0) return pass4(this);
12485
+ return results;
12486
+ }
12487
+ };
12488
+ var projectFrontmatterMatchesContainer = {
12489
+ id: "assignment.project-matches-container",
12490
+ category: CATEGORY4,
12491
+ title: "`project` frontmatter matches containing project slug (or null for standalone)",
12492
+ async run(ctx) {
12493
+ const { withAssignmentMd } = await listAssignments(ctx);
12494
+ const results = [];
12495
+ for (const a of withAssignmentMd) {
12496
+ const path = resolve35(a.assignmentDir, "assignment.md");
12497
+ const parsed = await parseSafe(path);
12498
+ if (!parsed) continue;
12499
+ if (a.standalone) {
12500
+ if (parsed.project !== null) {
12501
+ results.push({
12502
+ id: this.id,
12503
+ category: this.category,
12504
+ title: this.title,
12505
+ status: "error",
12506
+ detail: `standalone/${a.assignmentSlug}: frontmatter declares project "${parsed.project}" but the folder is under ~/.syntaur/assignments/ (project must be null)`,
12507
+ affected: [path],
12508
+ remediation: {
12509
+ kind: "manual",
12510
+ suggestion: "Set `project: null` in the frontmatter, or move the folder into a project.",
12511
+ command: null
12512
+ },
12513
+ autoFixable: false
12514
+ });
12515
+ }
12516
+ } else {
12517
+ if (parsed.project !== a.projectSlug) {
12518
+ results.push({
12519
+ id: this.id,
12520
+ category: this.category,
12521
+ title: this.title,
12522
+ status: "error",
12523
+ detail: `${a.projectSlug}/${a.assignmentSlug}: frontmatter declares project "${parsed.project ?? "null"}" but the folder is inside project "${a.projectSlug}"`,
12524
+ affected: [path],
12525
+ remediation: {
12526
+ kind: "manual",
12527
+ suggestion: `Set \`project: ${a.projectSlug}\` in the frontmatter.`,
12528
+ command: null
12529
+ },
12530
+ autoFixable: false
12531
+ });
12532
+ }
12533
+ }
12534
+ }
12535
+ if (results.length === 0) return pass4(this);
12536
+ return results;
12537
+ }
12538
+ };
11102
12539
  var assignmentChecks = [
11103
12540
  requiredFiles2,
11104
12541
  orphanedFolder,
11105
12542
  invalidStatus,
11106
12543
  workspaceMissing,
11107
- requiredFilesByStatus
12544
+ requiredFilesByStatus,
12545
+ companionFilesScaffolded,
12546
+ typeDefinition,
12547
+ projectFrontmatterMatchesContainer
11108
12548
  ];
11109
12549
  async function parseSafe(path) {
11110
12550
  try {
@@ -11785,6 +13225,7 @@ init_config2();
11785
13225
  import { resolve as resolve38 } from "path";
11786
13226
  import { readFile as readFile21 } from "fs/promises";
11787
13227
  init_timestamp();
13228
+ init_assignment_resolver();
11788
13229
  function shortId() {
11789
13230
  return generateId().split("-")[0];
11790
13231
  }
@@ -11870,6 +13311,7 @@ init_config2();
11870
13311
  import { resolve as resolve39 } from "path";
11871
13312
  import { readFile as readFile22 } from "fs/promises";
11872
13313
  init_timestamp();
13314
+ init_assignment_resolver();
11873
13315
  function setTopLevelField3(content, key, value) {
11874
13316
  const fieldRegex = new RegExp(`^(${key}:)\\s*.*$`, "m");
11875
13317
  if (fieldRegex.test(content)) {
@@ -11912,7 +13354,7 @@ async function requestCommand(target, text, options = {}) {
11912
13354
  const todosHeading = /^## Todos\s*$/m;
11913
13355
  if (todosHeading.test(content)) {
11914
13356
  content = content.replace(
11915
- /(^## Todos[\s\S]*?)(\n##\s|\n*$)/,
13357
+ /(^## Todos[\s\S]*?)(\n## |\n*$)/m,
11916
13358
  (_m, section, nextHeading) => {
11917
13359
  return `${section.trimEnd()}
11918
13360
  ${todoLine}