syntaur 0.41.2 → 0.42.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 (68) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dashboard/dist/assets/{_basePickBy-CMGil-NY.js → _basePickBy-Cd0RkcLT.js} +1 -1
  3. package/dashboard/dist/assets/{_baseUniq-DllyUaEJ.js → _baseUniq-DcVRMSTl.js} +1 -1
  4. package/dashboard/dist/assets/{arc-C6fNP_LJ.js → arc-B2m30WX5.js} +1 -1
  5. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-CxXDnbMY.js → architectureDiagram-2XIMDMQ5-CJdPqqWS.js} +1 -1
  6. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-B8UDJhxg.js → blockDiagram-WCTKOSBZ-BoThc9ue.js} +1 -1
  7. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-9XDZP3AD.js → c4Diagram-IC4MRINW-DcLdX7Gp.js} +1 -1
  8. package/dashboard/dist/assets/channel-TK3AY7tt.js +1 -0
  9. package/dashboard/dist/assets/{chunk-4BX2VUAB-D1LR7D9Y.js → chunk-4BX2VUAB-DeyTroVn.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-55IACEB6-sumE5d0X.js → chunk-55IACEB6-Pk1kQHZ3.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-FMBD7UC4-C-Iy8wke.js → chunk-FMBD7UC4-DFoS6k4t.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-JSJVCQXG-Clyrcmzt.js → chunk-JSJVCQXG-DN22e0xM.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-KX2RTZJC-BQqetgrP.js → chunk-KX2RTZJC-MNrdiNWF.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-NQ4KR5QH-Cw60fnx2.js → chunk-NQ4KR5QH-C0k2CIP7.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-QZHKN3VN-Dv40SU2-.js → chunk-QZHKN3VN-C32xUlPx.js} +1 -1
  16. package/dashboard/dist/assets/{chunk-WL4C6EOR-DFiOufrs.js → chunk-WL4C6EOR-DB7YEwdA.js} +1 -1
  17. package/dashboard/dist/assets/classDiagram-VBA2DB6C-BcpVoyRF.js +1 -0
  18. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-BcpVoyRF.js +1 -0
  19. package/dashboard/dist/assets/clone-tv-jxopI.js +1 -0
  20. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-DV306SRn.js → cose-bilkent-S5V4N54A-CMH4iDpK.js} +1 -1
  21. package/dashboard/dist/assets/{dagre-KLK3FWXG-DaQ1pWLV.js → dagre-KLK3FWXG-DdHflfb4.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-E7M64L7V-2fsjMT-T.js → diagram-E7M64L7V-DtScFCCN.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-IFDJBPK2-CoaSyKLw.js → diagram-IFDJBPK2-DqZgC_98.js} +1 -1
  24. package/dashboard/dist/assets/{diagram-P4PSJMXO-C_j6Kd6q.js → diagram-P4PSJMXO-BIjzlVHf.js} +1 -1
  25. package/dashboard/dist/assets/{erDiagram-INFDFZHY-CpOdYJWS.js → erDiagram-INFDFZHY-B_v2XAqY.js} +1 -1
  26. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-KVRjmhbG.js → flowDiagram-PKNHOUZH-BIsbt9TK.js} +1 -1
  27. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-CA_n5ynk.js → ganttDiagram-A5KZAMGK-3wW3k6UM.js} +1 -1
  28. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-DKkS_iH8.js → gitGraphDiagram-K3NZZRJ6-J5jG9Qum.js} +1 -1
  29. package/dashboard/dist/assets/{graph-C6ehraTW.js → graph-6IHp6W8J.js} +1 -1
  30. package/dashboard/dist/assets/{index-CdHziP5R.css → index-6uihSopA.css} +1 -1
  31. package/dashboard/dist/assets/{index-SW4WrQLg.js → index-BfWuhZd9.js} +59 -59
  32. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-H1Eg4YK9.js → infoDiagram-LFFYTUFH-BeUnIF7J.js} +1 -1
  33. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-DSrc4sub.js → ishikawaDiagram-PHBUUO56-CBMlrqiK.js} +1 -1
  34. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-Bl_0LgIo.js → journeyDiagram-4ABVD52K-C25hSiks.js} +1 -1
  35. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-Cq2WGyif.js → kanban-definition-K7BYSVSG-DoJVBKAf.js} +1 -1
  36. package/dashboard/dist/assets/{layout-DJv9vite.js → layout-rmqkK4ql.js} +1 -1
  37. package/dashboard/dist/assets/{linear-CAef3hQD.js → linear-Dcvh5pG3.js} +1 -1
  38. package/dashboard/dist/assets/{mermaid.core-B_gAmtAa.js → mermaid.core-BAS-3wuz.js} +4 -4
  39. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-4aIWu_CK.js → mindmap-definition-YRQLILUH-Dvs67C76.js} +1 -1
  40. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-1ThATMqf.js → pieDiagram-SKSYHLDU-DsVBwJs_.js} +1 -1
  41. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-BEq2jVyN.js → quadrantDiagram-337W2JSQ-C5dAkCkr.js} +1 -1
  42. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-DbYJrAQ9.js → requirementDiagram-Z7DCOOCP-DEqcg6A2.js} +1 -1
  43. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-DMr3kn8l.js → sankeyDiagram-WA2Y5GQK-CvrgIFyY.js} +1 -1
  44. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-BR03-l-y.js → sequenceDiagram-2WXFIKYE-Bu6tanJS.js} +1 -1
  45. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-DUj-dVll.js → stateDiagram-RAJIS63D-D16mva7g.js} +1 -1
  46. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-XMWsEM8j.js +1 -0
  47. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-DpN8jElm.js → timeline-definition-YZTLITO2-BcLXDlbF.js} +1 -1
  48. package/dashboard/dist/assets/{treemap-KZPCXAKY-CyUTDKiM.js → treemap-KZPCXAKY-BuA9iiXV.js} +1 -1
  49. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-DRJFiQmT.js → vennDiagram-LZ73GAT5-CYNPHeLe.js} +1 -1
  50. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-DcrZVnQ-.js → xychartDiagram-JWTSCODW-BJ4lD-Yr.js} +1 -1
  51. package/dashboard/dist/index.html +2 -2
  52. package/dist/dashboard/server.js +2412 -420
  53. package/dist/dashboard/server.js.map +1 -1
  54. package/dist/index.js +3783 -966
  55. package/dist/index.js.map +1 -1
  56. package/dist/launch/index.d.ts +33 -0
  57. package/dist/launch/index.js +1949 -70
  58. package/dist/launch/index.js.map +1 -1
  59. package/package.json +1 -1
  60. package/platforms/claude-code/.claude-plugin/plugin.json +1 -1
  61. package/platforms/codex/.codex-plugin/plugin.json +1 -1
  62. package/platforms/hermes/plugins/syntaur/__pycache__/__init__.cpython-312.pyc +0 -0
  63. package/platforms/hermes/plugins/syntaur/__pycache__/boundary.cpython-312.pyc +0 -0
  64. package/dashboard/dist/assets/channel-OsoeK3Lk.js +0 -1
  65. package/dashboard/dist/assets/classDiagram-VBA2DB6C-BKX6nUBp.js +0 -1
  66. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-BKX6nUBp.js +0 -1
  67. package/dashboard/dist/assets/clone-f-TTh9ms.js +0 -1
  68. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-Dzzbhq6b.js +0 -1
@@ -88,7 +88,7 @@ var init_paths = __esm({
88
88
  });
89
89
 
90
90
  // src/lifecycle/types.ts
91
- var DEFAULT_STATUSES;
91
+ var DEFAULT_STATUSES, DEFAULT_TERMINAL_STATUSES;
92
92
  var init_types = __esm({
93
93
  "src/lifecycle/types.ts"() {
94
94
  "use strict";
@@ -103,6 +103,10 @@ var init_types = __esm({
103
103
  "completed",
104
104
  "failed"
105
105
  ];
106
+ DEFAULT_TERMINAL_STATUSES = /* @__PURE__ */ new Set([
107
+ "completed",
108
+ "failed"
109
+ ]);
106
110
  }
107
111
  });
108
112
 
@@ -160,6 +164,17 @@ var init_state_machine = __esm({
160
164
  });
161
165
 
162
166
  // src/lifecycle/frontmatter.ts
167
+ var frontmatter_exports = {};
168
+ __export(frontmatter_exports, {
169
+ appendStatusHistoryEntry: () => appendStatusHistoryEntry,
170
+ parseAssignmentFrontmatter: () => parseAssignmentFrontmatter,
171
+ renameStatusInHistory: () => renameStatusInHistory,
172
+ updateAssignmentFile: () => updateAssignmentFile,
173
+ updateAssignmentWorkspace: () => updateAssignmentWorkspace,
174
+ updateNestedBlock: () => updateNestedBlock,
175
+ updateOverride: () => updateOverride,
176
+ updatePlanApproval: () => updatePlanApproval
177
+ });
163
178
  function extractFrontmatter(fileContent) {
164
179
  const match = fileContent.match(/^---\n([\s\S]*?)\n---/);
165
180
  if (!match) {
@@ -172,7 +187,10 @@ function extractFrontmatter(fileContent) {
172
187
  function parseSimpleValue(raw2) {
173
188
  const trimmed = raw2.trim();
174
189
  if (trimmed === "null" || trimmed === "~" || trimmed === "") return null;
175
- if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
190
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.length >= 2) {
191
+ return trimmed.slice(1, -1).replace(/\\(["\\])/g, "$1");
192
+ }
193
+ if (trimmed.startsWith("'") && trimmed.endsWith("'") && trimmed.length >= 2) {
176
194
  return trimmed.slice(1, -1);
177
195
  }
178
196
  return trimmed;
@@ -232,6 +250,89 @@ function parseExternalIds(frontmatter) {
232
250
  }
233
251
  return results;
234
252
  }
253
+ function parseStatusHistory(frontmatter) {
254
+ if (/^statusHistory:\s*\[\s*\]/m.test(frontmatter)) return [];
255
+ const headerMatch = frontmatter.match(/^statusHistory:\s*$/m);
256
+ if (!headerMatch) return [];
257
+ const headerStart = headerMatch.index ?? frontmatter.indexOf(headerMatch[0]);
258
+ const bodyStart = headerStart + headerMatch[0].length + 1;
259
+ const after = frontmatter.slice(bodyStart);
260
+ const bodyLines = [];
261
+ for (const line of after.split("\n")) {
262
+ if (line.length === 0) {
263
+ bodyLines.push(line);
264
+ continue;
265
+ }
266
+ if (line[0] !== " " && line[0] !== " ") break;
267
+ bodyLines.push(line);
268
+ }
269
+ const body = bodyLines.join("\n");
270
+ const results = [];
271
+ const itemBlocks = body.split(/\n\s+-\s+/).filter((b) => b.trim().length > 0);
272
+ for (const block of itemBlocks) {
273
+ const entry = {};
274
+ for (const line of block.split("\n")) {
275
+ const colonIdx = line.indexOf(":");
276
+ if (colonIdx < 0) continue;
277
+ const key = line.slice(0, colonIdx).trim().replace(/^-\s+/, "");
278
+ if (!key) continue;
279
+ entry[key] = parseSimpleValue(line.slice(colonIdx + 1));
280
+ }
281
+ if (!entry["to"]) continue;
282
+ const result = {
283
+ at: entry["at"] ?? "",
284
+ from: entry["from"] ?? null,
285
+ to: entry["to"],
286
+ command: entry["command"] ?? "",
287
+ by: entry["by"] ?? null
288
+ };
289
+ if (entry["reason"] != null) result.reason = entry["reason"];
290
+ if ("phaseFrom" in entry) result.phaseFrom = entry["phaseFrom"];
291
+ if ("phaseTo" in entry) result.phaseTo = entry["phaseTo"];
292
+ if ("dispositionFrom" in entry) result.dispositionFrom = entry["dispositionFrom"];
293
+ if ("dispositionTo" in entry) result.dispositionTo = entry["dispositionTo"];
294
+ results.push(result);
295
+ }
296
+ return results;
297
+ }
298
+ function parseNestedBlock(frontmatter, header) {
299
+ if (new RegExp(`^${header}:\\s*(null|~)\\s*$`, "m").test(frontmatter)) return null;
300
+ const headerMatch = frontmatter.match(new RegExp(`^${header}:\\s*$`, "m"));
301
+ if (!headerMatch) return null;
302
+ const headerStart = headerMatch.index ?? frontmatter.indexOf(headerMatch[0]);
303
+ const after = frontmatter.slice(headerStart + headerMatch[0].length + 1);
304
+ const out = {};
305
+ for (const line of after.split("\n")) {
306
+ if (line.length === 0) continue;
307
+ if (line[0] !== " " && line[0] !== " ") break;
308
+ const colonIdx = line.indexOf(":");
309
+ if (colonIdx < 0) continue;
310
+ const key = line.slice(0, colonIdx).trim();
311
+ if (!key) continue;
312
+ out[key] = parseSimpleValue(line.slice(colonIdx + 1));
313
+ }
314
+ return Object.keys(out).length > 0 ? out : null;
315
+ }
316
+ function parsePlanApproval(frontmatter) {
317
+ const block = parseNestedBlock(frontmatter, "planApproval");
318
+ if (!block || !block["file"] || !block["digest"]) return null;
319
+ return {
320
+ file: block["file"],
321
+ digest: block["digest"],
322
+ by: block["by"] ?? null,
323
+ at: block["at"] ?? ""
324
+ };
325
+ }
326
+ function parseOverride(frontmatter) {
327
+ const block = parseNestedBlock(frontmatter, "override");
328
+ if (!block || !block["status"]) return null;
329
+ return {
330
+ status: block["status"],
331
+ source: block["source"] ?? "human",
332
+ reason: block["reason"] ?? null,
333
+ at: block["at"] ?? ""
334
+ };
335
+ }
235
336
  function parseWorkspace(frontmatter) {
236
337
  const defaults = {
237
338
  repository: null,
@@ -280,6 +381,7 @@ function parseAssignmentFrontmatter(fileContent) {
280
381
  updated: getField2("updated") ?? "",
281
382
  assignee: getField2("assignee"),
282
383
  externalIds: parseExternalIds(frontmatter),
384
+ statusHistory: parseStatusHistory(frontmatter),
283
385
  dependsOn: parseDependsOn(frontmatter),
284
386
  links: parseLinks(frontmatter),
285
387
  blockedReason: getField2("blockedReason"),
@@ -287,16 +389,29 @@ function parseAssignmentFrontmatter(fileContent) {
287
389
  tags: parseTags(frontmatter),
288
390
  archived: getField2("archived") === "true",
289
391
  archivedAt: getField2("archivedAt"),
290
- archivedReason: getField2("archivedReason")
392
+ archivedReason: getField2("archivedReason"),
393
+ phase: getField2("phase"),
394
+ disposition: getField2("disposition"),
395
+ planApproval: parsePlanApproval(frontmatter),
396
+ parked: getField2("parked") === "true",
397
+ reviewRequested: getField2("reviewRequested") === "true",
398
+ implementationStarted: getField2("implementationStarted") === "true",
399
+ override: parseOverride(frontmatter)
291
400
  };
292
401
  }
293
402
  function formatYamlValue(value) {
294
403
  if (typeof value === "boolean") return value ? "true" : "false";
295
404
  if (value === null) return "null";
405
+ if (/[\r\n]/.test(value)) {
406
+ value = value.replace(/\s*[\r\n]+\s*/g, " ").trim();
407
+ }
296
408
  if (/^\d{4}-\d{2}-\d{2}T/.test(value)) {
297
409
  return `"${value}"`;
298
410
  }
299
- if (/[:#{}[\],&*?|>!%@`]/.test(value) || /^\s|\s$/.test(value) || value === "") {
411
+ if (/^(null|~|true|false|-?\d+(\.\d+)?)$/i.test(value)) {
412
+ return `"${value}"`;
413
+ }
414
+ if (/[:#{}[\],&*?|>!%@\`]/.test(value) || /^\s|\s$/.test(value) || value === "") {
300
415
  const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
301
416
  return `"${escaped}"`;
302
417
  }
@@ -376,6 +491,123 @@ ${lines.join("\n")}`;
376
491
  }
377
492
  return `${fmMatch[1]}${newFm}${fmMatch[3]}${fileContent.slice(fmMatch[0].length)}`;
378
493
  }
494
+ function renameStatusInHistory(content, oldId, newId) {
495
+ const fmMatch = content.match(/^(---\n)([\s\S]*?)(\n---)/);
496
+ if (!fmMatch) return content;
497
+ const esc = oldId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
498
+ const re = new RegExp(`^(\\s+(?:from|to|phaseFrom|phaseTo):[ \\t]*)("?)${esc}\\2[ \\t]*$`, "gm");
499
+ const newFm = fmMatch[2].replace(re, (_m, prefix, quote) => `${prefix}${quote}${newId}${quote}`);
500
+ return `${fmMatch[1]}${newFm}${fmMatch[3]}${content.slice(fmMatch[0].length)}`;
501
+ }
502
+ function findStatusHistoryBlock(fmBlock) {
503
+ const headerMatch = fmBlock.match(/^statusHistory:\s*$/m);
504
+ if (!headerMatch) return null;
505
+ const headerStart = headerMatch.index ?? fmBlock.indexOf(headerMatch[0]);
506
+ const bodyStart = headerStart + headerMatch[0].length + 1;
507
+ const after = fmBlock.slice(bodyStart);
508
+ const lines = after.split("\n");
509
+ let consumed = 0;
510
+ for (const line of lines) {
511
+ if (line.length === 0) {
512
+ consumed += line.length + 1;
513
+ continue;
514
+ }
515
+ if (line[0] !== " " && line[0] !== " ") break;
516
+ consumed += line.length + 1;
517
+ }
518
+ const bodyEnd = Math.min(bodyStart + consumed, fmBlock.length);
519
+ return { headerStart, bodyStart, bodyEnd };
520
+ }
521
+ function renderStatusHistoryItem(entry) {
522
+ const lines = [
523
+ ` - at: ${formatYamlValue(entry.at)}`,
524
+ ` from: ${formatYamlValue(entry.from)}`,
525
+ ` to: ${formatYamlValue(entry.to)}`,
526
+ ` command: ${formatYamlValue(entry.command)}`,
527
+ ` by: ${formatYamlValue(entry.by)}`
528
+ ];
529
+ if (entry.reason !== void 0 && entry.reason !== null) {
530
+ lines.push(` reason: ${formatYamlValue(entry.reason)}`);
531
+ }
532
+ for (const key of ["phaseFrom", "phaseTo", "dispositionFrom", "dispositionTo"]) {
533
+ if (entry[key] !== void 0) {
534
+ lines.push(` ${key}: ${formatYamlValue(entry[key] ?? null)}`);
535
+ }
536
+ }
537
+ return lines.join("\n");
538
+ }
539
+ function updateNestedBlock(fileContent, header, record) {
540
+ const fmMatch = fileContent.match(/^(---\n)([\s\S]*?)(\n---)/);
541
+ if (!fmMatch) {
542
+ throw new Error("No frontmatter found in assignment file. Expected --- delimiters.");
543
+ }
544
+ const fmBlock = fmMatch[2];
545
+ const rendered = record === null ? `${header}: null` : [`${header}:`, ...Object.entries(record).map(([k, v]) => ` ${k}: ${formatYamlValue(v)}`)].join("\n");
546
+ const headerRe = new RegExp(`^${header}:.*$`, "m");
547
+ const headerMatch = fmBlock.match(headerRe);
548
+ let newFm;
549
+ if (headerMatch) {
550
+ const start = headerMatch.index ?? 0;
551
+ let end = start + headerMatch[0].length;
552
+ const after = fmBlock.slice(end);
553
+ let scanned = 0;
554
+ for (const line of after.split("\n").slice(1)) {
555
+ if (line.length === 0) {
556
+ scanned += 1 + line.length;
557
+ continue;
558
+ }
559
+ if (line[0] !== " " && line[0] !== " ") break;
560
+ scanned += 1 + line.length;
561
+ end += scanned;
562
+ scanned = 0;
563
+ }
564
+ newFm = fmBlock.slice(0, start) + rendered + fmBlock.slice(end);
565
+ } else {
566
+ newFm = `${fmBlock.replace(/\n+$/, "")}
567
+ ${rendered}`;
568
+ }
569
+ return `${fmMatch[1]}${newFm}${fmMatch[3]}${fileContent.slice(fmMatch[0].length)}`;
570
+ }
571
+ function updatePlanApproval(fileContent, approval) {
572
+ return updateNestedBlock(
573
+ fileContent,
574
+ "planApproval",
575
+ approval === null ? null : { file: approval.file, digest: approval.digest, by: approval.by, at: approval.at }
576
+ );
577
+ }
578
+ function updateOverride(fileContent, override) {
579
+ return updateNestedBlock(
580
+ fileContent,
581
+ "override",
582
+ override === null ? null : { status: override.status, source: override.source, reason: override.reason, at: override.at }
583
+ );
584
+ }
585
+ function appendStatusHistoryEntry(fileContent, entry) {
586
+ const fmMatch = fileContent.match(/^(---\n)([\s\S]*?)(\n---)/);
587
+ if (!fmMatch) {
588
+ throw new Error("No frontmatter found in assignment file. Expected --- delimiters.");
589
+ }
590
+ const fmBlock = fmMatch[2];
591
+ const item = renderStatusHistoryItem(entry);
592
+ const inlineRegex = /^statusHistory:[ \t]*\[[ \t]*\][ \t]*$/m;
593
+ const block = findStatusHistoryBlock(fmBlock);
594
+ let newFm;
595
+ if (inlineRegex.test(fmBlock)) {
596
+ newFm = fmBlock.replace(inlineRegex, `statusHistory:
597
+ ${item}`);
598
+ } else if (block) {
599
+ const before = fmBlock.slice(0, block.bodyEnd);
600
+ const rest = fmBlock.slice(block.bodyEnd);
601
+ const sep1 = before.endsWith("\n") ? "" : "\n";
602
+ const sep2 = rest.length > 0 && !rest.startsWith("\n") ? "\n" : "";
603
+ newFm = `${before}${sep1}${item}${sep2}${rest}`;
604
+ } else {
605
+ newFm = `${fmBlock.replace(/\n+$/, "")}
606
+ statusHistory:
607
+ ${item}`;
608
+ }
609
+ return `${fmMatch[1]}${newFm}${fmMatch[3]}${fileContent.slice(fmMatch[0].length)}`;
610
+ }
379
611
  var init_frontmatter = __esm({
380
612
  "src/lifecycle/frontmatter.ts"() {
381
613
  "use strict";
@@ -586,6 +818,51 @@ function parseExternalIds2(frontmatter) {
586
818
  }
587
819
  return results;
588
820
  }
821
+ function parseStatusHistory2(frontmatter) {
822
+ if (/^statusHistory:\s*\[\s*\]/m.test(frontmatter)) return [];
823
+ const headerMatch = frontmatter.match(/^statusHistory:\s*$/m);
824
+ if (!headerMatch) return [];
825
+ const headerStart = headerMatch.index ?? frontmatter.indexOf(headerMatch[0]);
826
+ const bodyStart = headerStart + headerMatch[0].length + 1;
827
+ const after = frontmatter.slice(bodyStart);
828
+ const bodyLines = [];
829
+ for (const line of after.split("\n")) {
830
+ if (line.length === 0) {
831
+ bodyLines.push(line);
832
+ continue;
833
+ }
834
+ if (line[0] !== " " && line[0] !== " ") break;
835
+ bodyLines.push(line);
836
+ }
837
+ const body = bodyLines.join("\n");
838
+ const results = [];
839
+ const itemBlocks = body.split(/\n\s+-\s+/).filter((b) => b.trim().length > 0);
840
+ for (const block of itemBlocks) {
841
+ const entry = {};
842
+ for (const line of block.split("\n")) {
843
+ const colonIdx = line.indexOf(":");
844
+ if (colonIdx < 0) continue;
845
+ const key = line.slice(0, colonIdx).trim().replace(/^-\s+/, "");
846
+ if (!key) continue;
847
+ entry[key] = parseSimpleValue2(line.slice(colonIdx + 1));
848
+ }
849
+ if (!entry["to"]) continue;
850
+ const result = {
851
+ at: entry["at"] ?? "",
852
+ from: entry["from"] ?? null,
853
+ to: entry["to"],
854
+ command: entry["command"] ?? "",
855
+ by: entry["by"] ?? null
856
+ };
857
+ if (entry["reason"] != null) result.reason = entry["reason"];
858
+ if ("phaseFrom" in entry) result.phaseFrom = entry["phaseFrom"];
859
+ if ("phaseTo" in entry) result.phaseTo = entry["phaseTo"];
860
+ if ("dispositionFrom" in entry) result.dispositionFrom = entry["dispositionFrom"];
861
+ if ("dispositionTo" in entry) result.dispositionTo = entry["dispositionTo"];
862
+ results.push(result);
863
+ }
864
+ return results;
865
+ }
589
866
  function parseAssignmentFull(fileContent) {
590
867
  const [fm, body] = extractFrontmatter2(fileContent);
591
868
  return {
@@ -608,13 +885,40 @@ function parseAssignmentFull(fileContent) {
608
885
  parentBranch: getNestedField(fm, "workspace", "parentBranch")
609
886
  },
610
887
  externalIds: parseExternalIds2(fm),
888
+ statusHistory: parseStatusHistory2(fm),
611
889
  tags: parseListField(fm, "tags"),
612
890
  archived: getField(fm, "archived") === "true",
613
891
  archivedAt: getField(fm, "archivedAt"),
614
892
  archivedReason: getField(fm, "archivedReason"),
615
893
  created: getField(fm, "created") ?? "",
616
894
  updated: getField(fm, "updated") ?? "",
617
- body
895
+ body,
896
+ phase: getField(fm, "phase"),
897
+ disposition: getField(fm, "disposition"),
898
+ parked: getField(fm, "parked") === "true",
899
+ reviewRequested: getField(fm, "reviewRequested") === "true",
900
+ implementationStarted: getField(fm, "implementationStarted") === "true",
901
+ planApproval: (() => {
902
+ const file = getNestedField(fm, "planApproval", "file");
903
+ const digest = getNestedField(fm, "planApproval", "digest");
904
+ if (!file || !digest) return null;
905
+ return {
906
+ file,
907
+ digest,
908
+ by: getNestedField(fm, "planApproval", "by"),
909
+ at: getNestedField(fm, "planApproval", "at") ?? ""
910
+ };
911
+ })(),
912
+ override: (() => {
913
+ const status = getNestedField(fm, "override", "status");
914
+ if (!status) return null;
915
+ return {
916
+ status,
917
+ source: getNestedField(fm, "override", "source") ?? "human",
918
+ reason: getNestedField(fm, "override", "reason"),
919
+ at: getNestedField(fm, "override", "at") ?? ""
920
+ };
921
+ })()
618
922
  };
619
923
  }
620
924
  function parsePlan(fileContent) {
@@ -1309,7 +1613,10 @@ async function checkDependencies(projectDir, dependsOn, terminalStatuses) {
1309
1613
  async function executeTransition(projectDir, assignmentSlug, command, options = {}) {
1310
1614
  const filePath = resolveAssignmentPath(projectDir, assignmentSlug);
1311
1615
  const { content, frontmatter } = await readAssignment(filePath);
1312
- const targetStatus = getTargetStatus(frontmatter.status, command, options.transitionTable);
1616
+ const targetStatus = (options.transitionTable ? getTargetStatus(frontmatter.status, command, options.transitionTable) : null) ?? options.commandTargets?.get(command) ?? // Built-ins apply only when NEITHER custom mechanism was supplied — a
1617
+ // provided-but-miss commandTargets means "custom config had no answer",
1618
+ // which must refuse, not silently fall back (codex r4).
1619
+ (!options.transitionTable && !options.commandTargets ? getTargetStatus(frontmatter.status, command) : null);
1313
1620
  if (!targetStatus) {
1314
1621
  return {
1315
1622
  success: false,
@@ -1324,20 +1631,37 @@ async function executeTransition(projectDir, assignmentSlug, command, options =
1324
1631
  warnings.push(`Starting with unmet dependencies: ${depCheck.unmet.join(", ")}`);
1325
1632
  }
1326
1633
  }
1634
+ const now = nowTimestamp();
1327
1635
  const updates = {
1328
1636
  status: targetStatus,
1329
- updated: nowTimestamp()
1637
+ updated: now
1330
1638
  };
1331
1639
  if (ASSIGNEE_SETTING_COMMANDS.has(command) && options.agent && !frontmatter.assignee) {
1332
1640
  updates.assignee = options.agent;
1333
1641
  }
1334
1642
  if (command === "block") {
1335
- updates.blockedReason = options.reason ?? null;
1643
+ updates.blockedReason = options.reason ?? "(unspecified)";
1336
1644
  }
1337
1645
  if (command === "unblock") {
1338
1646
  updates.blockedReason = null;
1339
1647
  }
1340
- const updatedContent = updateAssignmentFile(content, updates);
1648
+ const terminalSet = options.terminalStatuses ?? /* @__PURE__ */ new Set(["completed", "failed"]);
1649
+ const enteringTerminal = terminalSet.has(targetStatus) && frontmatter.disposition !== "terminal";
1650
+ if (enteringTerminal) {
1651
+ updates.disposition = "terminal";
1652
+ }
1653
+ let updatedContent = updateAssignmentFile(content, updates);
1654
+ if (targetStatus !== frontmatter.status) {
1655
+ updatedContent = appendStatusHistoryEntry(updatedContent, {
1656
+ at: now,
1657
+ from: frontmatter.status,
1658
+ to: targetStatus,
1659
+ command,
1660
+ by: options.agent ?? frontmatter.assignee ?? null,
1661
+ reason: command === "block" ? options.reason : void 0,
1662
+ ...enteringTerminal ? { dispositionFrom: frontmatter.disposition, dispositionTo: "terminal" } : {}
1663
+ });
1664
+ }
1341
1665
  await writeFileForce(filePath, updatedContent);
1342
1666
  await applyLinkedTodosSideEffect(options.linkedTodosLookup, command, targetStatus, frontmatter);
1343
1667
  return {
@@ -1351,7 +1675,10 @@ async function executeTransition(projectDir, assignmentSlug, command, options =
1351
1675
  async function executeTransitionByDir(assignmentDir, command, options = {}) {
1352
1676
  const filePath = resolve4(assignmentDir, "assignment.md");
1353
1677
  const { content, frontmatter } = await readAssignment(filePath);
1354
- const targetStatus = getTargetStatus(frontmatter.status, command, options.transitionTable);
1678
+ const targetStatus = (options.transitionTable ? getTargetStatus(frontmatter.status, command, options.transitionTable) : null) ?? options.commandTargets?.get(command) ?? // Built-ins apply only when NEITHER custom mechanism was supplied — a
1679
+ // provided-but-miss commandTargets means "custom config had no answer",
1680
+ // which must refuse, not silently fall back (codex r4).
1681
+ (!options.transitionTable && !options.commandTargets ? getTargetStatus(frontmatter.status, command) : null);
1355
1682
  if (!targetStatus) {
1356
1683
  return {
1357
1684
  success: false,
@@ -1371,20 +1698,37 @@ async function executeTransitionByDir(assignmentDir, command, options = {}) {
1371
1698
  warnings.push(`Starting with unmet dependencies: ${depCheck.unmet.join(", ")}`);
1372
1699
  }
1373
1700
  }
1701
+ const now = nowTimestamp();
1374
1702
  const updates = {
1375
1703
  status: targetStatus,
1376
- updated: nowTimestamp()
1704
+ updated: now
1377
1705
  };
1378
1706
  if (ASSIGNEE_SETTING_COMMANDS.has(command) && options.agent && !frontmatter.assignee) {
1379
1707
  updates.assignee = options.agent;
1380
1708
  }
1381
1709
  if (command === "block") {
1382
- updates.blockedReason = options.reason ?? null;
1710
+ updates.blockedReason = options.reason ?? "(unspecified)";
1383
1711
  }
1384
1712
  if (command === "unblock") {
1385
1713
  updates.blockedReason = null;
1386
1714
  }
1387
- const updatedContent = updateAssignmentFile(content, updates);
1715
+ const terminalSetByDir = options.terminalStatuses ?? /* @__PURE__ */ new Set(["completed", "failed"]);
1716
+ const enteringTerminalByDir = terminalSetByDir.has(targetStatus) && frontmatter.disposition !== "terminal";
1717
+ if (enteringTerminalByDir) {
1718
+ updates.disposition = "terminal";
1719
+ }
1720
+ let updatedContent = updateAssignmentFile(content, updates);
1721
+ if (targetStatus !== frontmatter.status) {
1722
+ updatedContent = appendStatusHistoryEntry(updatedContent, {
1723
+ at: now,
1724
+ from: frontmatter.status,
1725
+ to: targetStatus,
1726
+ command,
1727
+ by: options.agent ?? frontmatter.assignee ?? null,
1728
+ reason: command === "block" ? options.reason : void 0,
1729
+ ...enteringTerminalByDir ? { dispositionFrom: frontmatter.disposition, dispositionTo: "terminal" } : {}
1730
+ });
1731
+ }
1388
1732
  await writeFileForce(filePath, updatedContent);
1389
1733
  await applyLinkedTodosSideEffect(options.linkedTodosLookup, command, targetStatus, frontmatter);
1390
1734
  return {
@@ -1862,6 +2206,7 @@ __export(config_exports, {
1862
2206
  AgentConfigError: () => AgentConfigError,
1863
2207
  BUILTIN_AGENTS: () => BUILTIN_AGENTS,
1864
2208
  DEFAULT_ASSIGNMENT_TYPES: () => DEFAULT_ASSIGNMENT_TYPES,
2209
+ DEFAULT_DERIVE_CONFIG: () => DEFAULT_DERIVE_CONFIG,
1865
2210
  DEFAULT_STATUS_COLORS: () => DEFAULT_STATUS_COLORS,
1866
2211
  PROMPT_ARG_POSITIONS: () => PROMPT_ARG_POSITIONS,
1867
2212
  TERMINAL_CHOICES: () => TERMINAL_CHOICES,
@@ -1888,6 +2233,7 @@ __export(config_exports, {
1888
2233
  updateOnboardingConfig: () => updateOnboardingConfig,
1889
2234
  updatePlaybooksConfig: () => updatePlaybooksConfig,
1890
2235
  validateAgentList: () => validateAgentList,
2236
+ validateDeriveConfig: () => validateDeriveConfig,
1891
2237
  writeAgentsConfig: () => writeAgentsConfig,
1892
2238
  writeHotkeyBindingsConfig: () => writeHotkeyBindingsConfig,
1893
2239
  writeStatusConfig: () => writeStatusConfig,
@@ -2059,6 +2405,16 @@ function parseStatusConfig(content) {
2059
2405
  const statuses = [];
2060
2406
  const order = [];
2061
2407
  const transitions = [];
2408
+ const phaseLadder = [];
2409
+ const disposition = [];
2410
+ const headline = {};
2411
+ const unquote = (v) => {
2412
+ const t = v.trim();
2413
+ if (t.startsWith('"') && t.endsWith('"') || t.startsWith("'") && t.endsWith("'")) {
2414
+ return t.slice(1, -1);
2415
+ }
2416
+ return t;
2417
+ };
2062
2418
  let currentSection = null;
2063
2419
  const lines = remaining.split("\n");
2064
2420
  function parseListEntry(lineIdx, baseIndent) {
@@ -2091,6 +2447,9 @@ function parseStatusConfig(content) {
2091
2447
  if (key === "definitions") currentSection = "definitions";
2092
2448
  else if (key === "order") currentSection = "order";
2093
2449
  else if (key === "transitions") currentSection = "transitions";
2450
+ else if (key === "phaseLadder") currentSection = "phaseLadder";
2451
+ else if (key === "disposition") currentSection = "disposition";
2452
+ else if (key === "headline") currentSection = "headline";
2094
2453
  else currentSection = null;
2095
2454
  continue;
2096
2455
  }
@@ -2129,12 +2488,52 @@ function parseStatusConfig(content) {
2129
2488
  lineIdx += consumed - 1;
2130
2489
  continue;
2131
2490
  }
2491
+ if (currentSection === "phaseLadder" && indent >= 4 && trimmed.startsWith("- ")) {
2492
+ const { entry, consumed } = parseListEntry(lineIdx, indent);
2493
+ if (entry["phase"] && entry["when"] !== void 0) {
2494
+ phaseLadder.push({
2495
+ phase: unquote(entry["phase"]),
2496
+ when: unquote(entry["when"]),
2497
+ next: entry["next"] !== void 0 ? unquote(entry["next"]) : void 0
2498
+ });
2499
+ }
2500
+ lineIdx += consumed - 1;
2501
+ continue;
2502
+ }
2503
+ if (currentSection === "disposition" && indent >= 4 && trimmed.startsWith("- ")) {
2504
+ const { entry, consumed } = parseListEntry(lineIdx, indent);
2505
+ if (entry["else"] !== void 0) {
2506
+ disposition.push({ when: null, is: unquote(entry["else"]) });
2507
+ } else if (entry["when"] !== void 0 && entry["is"]) {
2508
+ disposition.push({ when: unquote(entry["when"]), is: unquote(entry["is"]) });
2509
+ }
2510
+ lineIdx += consumed - 1;
2511
+ continue;
2512
+ }
2513
+ if (currentSection === "headline" && indent >= 4 && !trimmed.startsWith("- ")) {
2514
+ const ci = trimmed.indexOf(":");
2515
+ if (ci > 0) {
2516
+ headline[trimmed.slice(0, ci).trim()] = unquote(trimmed.slice(ci + 1));
2517
+ }
2518
+ continue;
2519
+ }
2132
2520
  }
2133
2521
  if (statuses.length === 0) return null;
2522
+ const derive = phaseLadder.length > 0 || disposition.length > 0 || Object.keys(headline).length > 0 ? {
2523
+ phaseLadder: phaseLadder.length > 0 ? phaseLadder : DEFAULT_DERIVE_CONFIG.phaseLadder,
2524
+ disposition: disposition.length > 0 ? disposition : DEFAULT_DERIVE_CONFIG.disposition,
2525
+ headline: {
2526
+ terminal: "passthrough",
2527
+ parked: headline["parked"] ?? DEFAULT_DERIVE_CONFIG.headline.parked,
2528
+ blocked: headline["blocked"] ?? DEFAULT_DERIVE_CONFIG.headline.blocked,
2529
+ active: "phase"
2530
+ }
2531
+ } : null;
2134
2532
  return {
2135
2533
  statuses,
2136
2534
  order: order.length > 0 ? order : statuses.map((s) => s.id),
2137
- transitions
2535
+ transitions,
2536
+ derive
2138
2537
  };
2139
2538
  }
2140
2539
  function toTitleCase(s) {
@@ -2182,8 +2581,68 @@ function serializeStatusConfig(statuses) {
2182
2581
  if (t.requiresReason) lines.push(` requiresReason: true`);
2183
2582
  }
2184
2583
  }
2584
+ if (statuses.derive) {
2585
+ const d = statuses.derive;
2586
+ lines.push(" phaseLadder:");
2587
+ for (const rung of d.phaseLadder) {
2588
+ lines.push(` - phase: ${rung.phase}`);
2589
+ lines.push(` when: "${rung.when.replace(/"/g, '\\"')}"`);
2590
+ if (rung.next) lines.push(` next: "${rung.next.replace(/"/g, '\\"')}"`);
2591
+ }
2592
+ lines.push(" disposition:");
2593
+ for (const rule of d.disposition) {
2594
+ if (rule.when === null) {
2595
+ lines.push(` - else: ${rule.is}`);
2596
+ } else {
2597
+ lines.push(` - when: "${rule.when.replace(/"/g, '\\"')}"`);
2598
+ lines.push(` is: ${rule.is}`);
2599
+ }
2600
+ }
2601
+ lines.push(" headline:");
2602
+ lines.push(` terminal: passthrough`);
2603
+ lines.push(` parked: ${d.headline.parked}`);
2604
+ lines.push(` blocked: ${d.headline.blocked}`);
2605
+ lines.push(` active: phase`);
2606
+ }
2185
2607
  return lines.join("\n");
2186
2608
  }
2609
+ function validateDeriveConfig(derive, statusConfig, validateWhen = () => null) {
2610
+ const problems = [];
2611
+ const ids = new Set(statusConfig.statuses.map((s) => s.id));
2612
+ if (derive.phaseLadder.length === 0) {
2613
+ problems.push("phaseLadder must have at least one rung");
2614
+ }
2615
+ for (const rung of derive.phaseLadder) {
2616
+ if (!ids.has(rung.phase)) {
2617
+ problems.push(`phaseLadder rung "${rung.phase}" is not a defined status id`);
2618
+ }
2619
+ const err = rung.when === "*" ? null : validateWhen(rung.when);
2620
+ if (err) problems.push(`phaseLadder rung "${rung.phase}": invalid condition \u2014 ${err}`);
2621
+ }
2622
+ const VALID_DISPOSITIONS = /* @__PURE__ */ new Set(["active", "blocked", "parked"]);
2623
+ let sawElse = false;
2624
+ for (const rule of derive.disposition) {
2625
+ if (!VALID_DISPOSITIONS.has(rule.is)) {
2626
+ problems.push(
2627
+ `disposition "${rule.is}" is not valid (expected active, blocked, or parked \u2014 terminal is never a rule)`
2628
+ );
2629
+ }
2630
+ if (rule.when === null) sawElse = true;
2631
+ else {
2632
+ const err = validateWhen(rule.when);
2633
+ if (err) problems.push(`disposition rule "${rule.is}": invalid condition \u2014 ${err}`);
2634
+ }
2635
+ }
2636
+ if (!sawElse) problems.push("disposition rules must end with an `else:` arm");
2637
+ for (const key of ["parked", "blocked"]) {
2638
+ if (!ids.has(derive.headline[key])) {
2639
+ problems.push(
2640
+ `headline.${key} \u2192 "${derive.headline[key]}" is not a defined status id (add the definition or run migrate-derive)`
2641
+ );
2642
+ }
2643
+ }
2644
+ return problems;
2645
+ }
2187
2646
  function serializeIntegrationConfig(integrations) {
2188
2647
  const lines = [];
2189
2648
  if (integrations.claudePluginDir) {
@@ -3238,7 +3697,7 @@ async function updateAgentsConfig(mutation, options = {}) {
3238
3697
  await writeAgentsConfig(next);
3239
3698
  return { previous, next, written: true };
3240
3699
  }
3241
- var DEFAULT_ASSIGNMENT_TYPES, DEFAULT_CONFIG, AUTO_CREATE_WORKTREE_VALUES, AgentConfigError, DEFAULT_STATUS_COLORS, KNOWN_AGENT_SCALAR_FIELDS, migratedConfigPaths, TerminalConfigError;
3700
+ var DEFAULT_DERIVE_CONFIG, DEFAULT_ASSIGNMENT_TYPES, DEFAULT_CONFIG, AUTO_CREATE_WORKTREE_VALUES, AgentConfigError, DEFAULT_STATUS_COLORS, KNOWN_AGENT_SCALAR_FIELDS, migratedConfigPaths, TerminalConfigError;
3242
3701
  var init_config2 = __esm({
3243
3702
  "src/utils/config.ts"() {
3244
3703
  "use strict";
@@ -3252,6 +3711,35 @@ var init_config2 = __esm({
3252
3711
  init_slug();
3253
3712
  init_terminal_schema();
3254
3713
  init_workspace_visibility_schema();
3714
+ DEFAULT_DERIVE_CONFIG = {
3715
+ phaseLadder: [
3716
+ { phase: "draft", when: "*", next: "Fill in the objective and acceptance criteria" },
3717
+ {
3718
+ // planExists-but-not-approved also sits here: the default status set has
3719
+ // no `planning` id. Users who define one add a `planExists:true` rung.
3720
+ phase: "ready_for_planning",
3721
+ when: "hasRealObjective:true AND acRealTotal > 0",
3722
+ next: "Write a plan and get it approved"
3723
+ },
3724
+ { phase: "ready_to_implement", when: "planApproved:true", next: "Start implementing" },
3725
+ {
3726
+ phase: "in_progress",
3727
+ when: "planApproved:true AND implementationStarted:true",
3728
+ next: "Finish acceptance criteria, then request review"
3729
+ },
3730
+ {
3731
+ phase: "review",
3732
+ when: "acAllChecked:true OR reviewRequested:true",
3733
+ next: "Complete, or address review feedback"
3734
+ }
3735
+ ],
3736
+ disposition: [
3737
+ { when: "parked:true", is: "parked" },
3738
+ { when: "blocked:true", is: "blocked" },
3739
+ { when: null, is: "active" }
3740
+ ],
3741
+ headline: { terminal: "passthrough", parked: "parked", blocked: "blocked", active: "phase" }
3742
+ };
3255
3743
  DEFAULT_ASSIGNMENT_TYPES = {
3256
3744
  definitions: [
3257
3745
  { id: "feature", label: "Feature" },
@@ -4230,8 +4718,8 @@ async function migrateFromMarkdown(projectsDir) {
4230
4718
  return allSessions.length;
4231
4719
  }
4232
4720
  async function parseMarkdownSessionsIndex(filePath, projectSlug) {
4233
- const { readFile: readFile21 } = await import("fs/promises");
4234
- const raw2 = await readFile21(filePath, "utf-8");
4721
+ const { readFile: readFile23 } = await import("fs/promises");
4722
+ const raw2 = await readFile23(filePath, "utf-8");
4235
4723
  const sessions = [];
4236
4724
  const lines = raw2.split("\n");
4237
4725
  let inTable = false;
@@ -4667,8 +5155,8 @@ function scanKey(serversDir2, projectsDir, assignmentsDir2) {
4667
5155
  return `${serversDir2}\0${projectsDir}\0${assignmentsDir2 ?? ""}`;
4668
5156
  }
4669
5157
  function delay(ms) {
4670
- return new Promise((resolve32) => {
4671
- const timer2 = setTimeout(resolve32, ms);
5158
+ return new Promise((resolve34) => {
5159
+ const timer2 = setTimeout(resolve34, ms);
4672
5160
  if (typeof timer2.unref === "function") {
4673
5161
  timer2.unref();
4674
5162
  }
@@ -5074,45 +5562,911 @@ async function scanAllSessions(serversDir2, projectsDir, options) {
5074
5562
  delay(COLD_WAIT_BUDGET_MS).then(() => emptyScan())
5075
5563
  ]);
5076
5564
  }
5077
- return refresh;
5078
- }
5079
- async function scanSingleSession(serversDir2, projectsDir, name, options) {
5080
- const data = await readSessionFile(serversDir2, name);
5081
- if (!data) return null;
5082
- const [lsofOutput, workspaceRecords, procSnapshot] = await Promise.all([
5083
- getLsofOutput(),
5084
- loadWorkspaceRecords(projectsDir, options?.assignmentsDir),
5085
- getProcessSnapshot()
5086
- ]);
5087
- if (data.kind === "process") {
5088
- return scanProcessSession(data, lsofOutput, workspaceRecords);
5565
+ return refresh;
5566
+ }
5567
+ async function scanSingleSession(serversDir2, projectsDir, name, options) {
5568
+ const data = await readSessionFile(serversDir2, name);
5569
+ if (!data) return null;
5570
+ const [lsofOutput, workspaceRecords, procSnapshot] = await Promise.all([
5571
+ getLsofOutput(),
5572
+ loadWorkspaceRecords(projectsDir, options?.assignmentsDir),
5573
+ getProcessSnapshot()
5574
+ ]);
5575
+ if (data.kind === "process") {
5576
+ return scanProcessSession(data, lsofOutput, workspaceRecords);
5577
+ }
5578
+ return scanSession(data, lsofOutput, workspaceRecords, procSnapshot);
5579
+ }
5580
+ var exec, cache, lastKnown, inFlight, forceFreshNext, scanEpoch, cacheKey, CACHE_TTL_MS, COLD_WAIT_BUDGET_MS, PROBE_TIMEOUT_MS, PROBE_MAX_BUFFER, tmuxAvailableCache;
5581
+ var init_scanner = __esm({
5582
+ "src/dashboard/scanner.ts"() {
5583
+ "use strict";
5584
+ init_api();
5585
+ init_servers();
5586
+ exec = promisify(execFile);
5587
+ cache = null;
5588
+ lastKnown = null;
5589
+ inFlight = null;
5590
+ forceFreshNext = false;
5591
+ scanEpoch = 0;
5592
+ cacheKey = null;
5593
+ CACHE_TTL_MS = 1e4;
5594
+ COLD_WAIT_BUDGET_MS = 2500;
5595
+ PROBE_TIMEOUT_MS = 15e3;
5596
+ PROBE_MAX_BUFFER = 32 * 1024 * 1024;
5597
+ tmuxAvailableCache = null;
5598
+ }
5599
+ });
5600
+
5601
+ // src/lifecycle/facts.ts
5602
+ var facts_exports = {};
5603
+ __export(facts_exports, {
5604
+ areDependenciesSatisfied: () => areDependenciesSatisfied,
5605
+ computeFacts: () => computeFacts,
5606
+ countRealAcceptanceCriteria: () => countRealAcceptanceCriteria,
5607
+ countUnresolvedQuestions: () => countUnresolvedQuestions,
5608
+ hasRealObjective: () => hasRealObjective,
5609
+ isPlanApproved: () => isPlanApproved,
5610
+ latestPlanFile: () => latestPlanFile,
5611
+ planDigest: () => planDigest
5612
+ });
5613
+ import { createHash } from "crypto";
5614
+ import { readdir as readdir7, readFile as readFile10 } from "fs/promises";
5615
+ import { resolve as resolve13 } from "path";
5616
+ function sectionBody(body, heading) {
5617
+ const re = new RegExp(`^##\\s+${heading}\\s*$`, "m");
5618
+ const m = body.match(re);
5619
+ if (!m || m.index === void 0) return null;
5620
+ const start = m.index + m[0].length;
5621
+ const rest = body.slice(start);
5622
+ const next = rest.search(/^##\s+/m);
5623
+ return next >= 0 ? rest.slice(0, next) : rest;
5624
+ }
5625
+ function hasRealObjective(body) {
5626
+ const section = sectionBody(body, "Objective");
5627
+ if (section === null) return false;
5628
+ return section.replace(HTML_COMMENT_RE, "").trim().length > 0;
5629
+ }
5630
+ function countRealAcceptanceCriteria(body) {
5631
+ const section = sectionBody(body, "Acceptance Criteria");
5632
+ if (section === null) return { total: 0, checked: 0 };
5633
+ let total = 0;
5634
+ let checked = 0;
5635
+ for (const line of section.split("\n")) {
5636
+ const m = line.match(/^\s*-\s*\[([ xX])\]\s*(.*)$/);
5637
+ if (!m) continue;
5638
+ const content = m[2].replace(HTML_COMMENT_RE, "").trim();
5639
+ if (content.length === 0) continue;
5640
+ total++;
5641
+ if (m[1].toLowerCase() === "x") checked++;
5642
+ }
5643
+ return { total, checked };
5644
+ }
5645
+ async function latestPlanFile(assignmentDir) {
5646
+ let entries;
5647
+ try {
5648
+ entries = await readdir7(assignmentDir);
5649
+ } catch {
5650
+ return null;
5651
+ }
5652
+ let best = null;
5653
+ for (const name of entries) {
5654
+ const m = name.match(PLAN_FILE_RE);
5655
+ if (!m) continue;
5656
+ const version = m[1] ? parseInt(m[1], 10) : 1;
5657
+ if (!best || version > best.version) best = { name, version };
5658
+ }
5659
+ return best?.name ?? null;
5660
+ }
5661
+ function planDigest(content) {
5662
+ return createHash("sha256").update(content, "utf-8").digest("hex");
5663
+ }
5664
+ async function isPlanApproved(assignmentDir, frontmatter) {
5665
+ const approval = frontmatter.planApproval;
5666
+ if (!approval) return false;
5667
+ const latest = await latestPlanFile(assignmentDir);
5668
+ if (!latest || latest !== approval.file) return false;
5669
+ try {
5670
+ const content = await readFile10(resolve13(assignmentDir, latest), "utf-8");
5671
+ return planDigest(content) === approval.digest;
5672
+ } catch {
5673
+ return false;
5674
+ }
5675
+ }
5676
+ async function countUnresolvedQuestions(assignmentDir) {
5677
+ const commentsPath = resolve13(assignmentDir, "comments.md");
5678
+ if (!await fileExists(commentsPath)) return 0;
5679
+ try {
5680
+ const content = await readFile10(commentsPath, "utf-8");
5681
+ let count = 0;
5682
+ for (const block of content.split(/^##\s+/m).slice(1)) {
5683
+ if (/^\*\*Type:\*\*\s*question\s*$/m.test(block) && /^\*\*Resolved:\*\*\s*false\s*$/m.test(block)) {
5684
+ count++;
5685
+ }
5686
+ }
5687
+ return count;
5688
+ } catch {
5689
+ return 0;
5690
+ }
5691
+ }
5692
+ async function areDependenciesSatisfied(projectDir, dependsOn, terminalStatuses) {
5693
+ if (dependsOn.length === 0 || projectDir === null) return true;
5694
+ for (const depSlug of dependsOn) {
5695
+ const depPath = resolve13(projectDir, "assignments", depSlug, "assignment.md");
5696
+ if (!await fileExists(depPath)) return false;
5697
+ try {
5698
+ const content = await readFile10(depPath, "utf-8");
5699
+ const m = content.match(/^status:\s*(.+)$/m);
5700
+ const status = m ? m[1].trim() : "";
5701
+ if (!terminalStatuses.has(status)) return false;
5702
+ } catch {
5703
+ return false;
5704
+ }
5705
+ }
5706
+ return true;
5707
+ }
5708
+ async function computeFacts(input) {
5709
+ const { assignmentDir, frontmatter, body, projectDir, terminalStatuses } = input;
5710
+ const ac = countRealAcceptanceCriteria(body);
5711
+ const [planFile, planApproved, unresolvedQuestions, depsSatisfied] = await Promise.all([
5712
+ latestPlanFile(assignmentDir),
5713
+ isPlanApproved(assignmentDir, frontmatter),
5714
+ countUnresolvedQuestions(assignmentDir),
5715
+ areDependenciesSatisfied(projectDir, frontmatter.dependsOn, terminalStatuses)
5716
+ ]);
5717
+ return {
5718
+ hasRealObjective: hasRealObjective(body),
5719
+ acRealTotal: ac.total,
5720
+ acRealChecked: ac.checked,
5721
+ acAllChecked: ac.total > 0 && ac.checked === ac.total,
5722
+ planExists: planFile !== null,
5723
+ planApproved,
5724
+ workspaceSet: frontmatter.workspace.repository !== null && frontmatter.workspace.branch !== null,
5725
+ implementationStarted: frontmatter.implementationStarted,
5726
+ depsSatisfied,
5727
+ unresolvedQuestions,
5728
+ blocked: frontmatter.blockedReason !== null,
5729
+ parked: frontmatter.parked,
5730
+ reviewRequested: frontmatter.reviewRequested,
5731
+ pinned: frontmatter.override !== null
5732
+ };
5733
+ }
5734
+ var HTML_COMMENT_RE, PLAN_FILE_RE;
5735
+ var init_facts = __esm({
5736
+ "src/lifecycle/facts.ts"() {
5737
+ "use strict";
5738
+ init_fs();
5739
+ HTML_COMMENT_RE = /<!--[\s\S]*?-->/g;
5740
+ PLAN_FILE_RE = /^plan(?:-v(\d+))?\.md$/;
5741
+ }
5742
+ });
5743
+
5744
+ // src/utils/query/fields.ts
5745
+ function resolveField(registry, name) {
5746
+ return registry[name.toLowerCase()] ?? null;
5747
+ }
5748
+ function readField(def, fieldName, item) {
5749
+ if (def.get) return def.get(item);
5750
+ return item[fieldName] ?? item[fieldName.toLowerCase()];
5751
+ }
5752
+ var init_fields = __esm({
5753
+ "src/utils/query/fields.ts"() {
5754
+ "use strict";
5755
+ }
5756
+ });
5757
+
5758
+ // src/utils/query/evaluate.ts
5759
+ function localDayBounds(date) {
5760
+ const [y, m, d] = date.split("-").map((n) => parseInt(n, 10));
5761
+ const start = new Date(y, m - 1, d).getTime();
5762
+ const end = new Date(y, m - 1, d + 1).getTime();
5763
+ return [start, end];
5764
+ }
5765
+ function toEpoch(value) {
5766
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
5767
+ if (typeof value === "string" && value.length > 0) {
5768
+ const t = Date.parse(value);
5769
+ return Number.isNaN(t) ? null : t;
5770
+ }
5771
+ return null;
5772
+ }
5773
+ function toNumber(value) {
5774
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
5775
+ if (typeof value === "string" && value.trim() !== "") {
5776
+ const n = Number(value);
5777
+ return Number.isFinite(n) ? n : null;
5778
+ }
5779
+ return null;
5780
+ }
5781
+ function ciEquals(a, b) {
5782
+ return typeof a === "string" && a.toLowerCase() === b.toLowerCase();
5783
+ }
5784
+ function isNone(value) {
5785
+ return value === null || value === void 0 || value === "";
5786
+ }
5787
+ function compileEquality(def, field, value, atomPos) {
5788
+ switch (def.kind) {
5789
+ case "enum":
5790
+ case "string":
5791
+ if (def.noneSentinel && value.raw.toLowerCase() === "none") {
5792
+ return (item) => isNone(readField(def, field, item));
5793
+ }
5794
+ return (item) => ciEquals(readField(def, field, item), value.raw);
5795
+ case "substring":
5796
+ return (item) => {
5797
+ const v = readField(def, field, item);
5798
+ return typeof v === "string" && v.toLowerCase().includes(value.raw.toLowerCase());
5799
+ };
5800
+ case "bool": {
5801
+ const want = value.raw.toLowerCase();
5802
+ if (want !== "true" && want !== "false") {
5803
+ throw new CompileError([
5804
+ { pos: value.pos, message: `Field "${field}" is boolean \u2014 use ${field}:true or ${field}:false` }
5805
+ ]);
5806
+ }
5807
+ const expected = want === "true";
5808
+ return (item) => {
5809
+ const v = readField(def, field, item);
5810
+ const b = typeof v === "boolean" ? v : v === "true" ? true : v === "false" || v === null || v === void 0 || v === "" ? false : null;
5811
+ return b !== null && b === expected;
5812
+ };
5813
+ }
5814
+ case "number": {
5815
+ const n = value.num ?? toNumber(value.raw);
5816
+ if (n === null) {
5817
+ throw new CompileError([{ pos: value.pos, message: `Field "${field}" is numeric \u2014 "${value.raw}" is not a number` }]);
5818
+ }
5819
+ return (item) => toNumber(readField(def, field, item)) === n;
5820
+ }
5821
+ case "ordinal":
5822
+ return (item) => ciEquals(readField(def, field, item), value.raw);
5823
+ case "list":
5824
+ return (item) => {
5825
+ const v = readField(def, field, item);
5826
+ return Array.isArray(v) && v.some((el) => ciEquals(el, value.raw));
5827
+ };
5828
+ case "timestamp": {
5829
+ if (value.type === "date") {
5830
+ const [start, end] = localDayBounds(value.raw);
5831
+ return (item) => {
5832
+ const t = toEpoch(readField(def, field, item));
5833
+ return t !== null && t >= start && t < end;
5834
+ };
5835
+ }
5836
+ throw new CompileError([
5837
+ { pos: value.pos, message: `Field "${field}" is a timestamp \u2014 use a comparison (e.g. ${field} > -36h) or an absolute date (${field}:2026-06-01)` }
5838
+ ]);
5839
+ }
5840
+ case "duration":
5841
+ throw new CompileError([
5842
+ { pos: atomPos, message: `Field "${field}" is a duration \u2014 use a comparison (e.g. ${field} > 3d)` }
5843
+ ]);
5844
+ }
5845
+ }
5846
+ function compileComparison(def, field, op, value) {
5847
+ const cmp = (a, b) => {
5848
+ switch (op) {
5849
+ case "<":
5850
+ return a < b;
5851
+ case ">":
5852
+ return a > b;
5853
+ case "<=":
5854
+ return a <= b;
5855
+ case ">=":
5856
+ return a >= b;
5857
+ case "=":
5858
+ return a === b;
5859
+ case "!=":
5860
+ return a !== b;
5861
+ default:
5862
+ return false;
5863
+ }
5864
+ };
5865
+ switch (def.kind) {
5866
+ case "number": {
5867
+ const n = value.num ?? toNumber(value.raw);
5868
+ if (n === null) {
5869
+ throw new CompileError([{ pos: value.pos, message: `"${value.raw}" is not a number (field "${field}")` }]);
5870
+ }
5871
+ return (item) => {
5872
+ const v = toNumber(readField(def, field, item));
5873
+ return v !== null && cmp(v, n);
5874
+ };
5875
+ }
5876
+ case "ordinal": {
5877
+ const order = def.order ?? [];
5878
+ const idx = order.findIndex((o) => o.toLowerCase() === value.raw.toLowerCase());
5879
+ if (idx < 0) {
5880
+ throw new CompileError([
5881
+ { pos: value.pos, message: `"${value.raw}" is not a valid ${field} (expected one of: ${order.join(", ")})` }
5882
+ ]);
5883
+ }
5884
+ return (item) => {
5885
+ const raw2 = readField(def, field, item);
5886
+ const vIdx = typeof raw2 === "string" ? order.findIndex((o) => o.toLowerCase() === raw2.toLowerCase()) : -1;
5887
+ return vIdx >= 0 && cmp(vIdx, idx);
5888
+ };
5889
+ }
5890
+ case "timestamp": {
5891
+ if (value.type === "duration") {
5892
+ const sign = value.sign === 0 ? -1 : value.sign ?? -1;
5893
+ const offset = sign * (value.num ?? 0);
5894
+ return (item, ctx) => {
5895
+ const t = toEpoch(readField(def, field, item));
5896
+ return t !== null && cmp(t, ctx.now + offset);
5897
+ };
5898
+ }
5899
+ if (value.type === "date") {
5900
+ const [start, end] = localDayBounds(value.raw);
5901
+ return (item) => {
5902
+ const t = toEpoch(readField(def, field, item));
5903
+ if (t === null) return false;
5904
+ switch (op) {
5905
+ case "<":
5906
+ return t < start;
5907
+ case "<=":
5908
+ return t < end;
5909
+ case ">":
5910
+ return t >= end;
5911
+ case ">=":
5912
+ return t >= start;
5913
+ case "=":
5914
+ return t >= start && t < end;
5915
+ case "!=":
5916
+ return t < start || t >= end;
5917
+ default:
5918
+ return false;
5919
+ }
5920
+ };
5921
+ }
5922
+ throw new CompileError([
5923
+ { pos: value.pos, message: `Compare timestamp field "${field}" to a duration (e.g. -36h) or a date (YYYY-MM-DD)` }
5924
+ ]);
5925
+ }
5926
+ case "duration": {
5927
+ if (value.type !== "duration") {
5928
+ throw new CompileError([
5929
+ { pos: value.pos, message: `Compare duration field "${field}" to a duration literal (e.g. 3d)` }
5930
+ ]);
5931
+ }
5932
+ const magnitude = value.num ?? 0;
5933
+ return (item) => {
5934
+ const v = toNumber(readField(def, field, item));
5935
+ return v !== null && cmp(v, magnitude);
5936
+ };
5937
+ }
5938
+ case "enum":
5939
+ case "string":
5940
+ case "substring":
5941
+ case "list": {
5942
+ if (op === "=") {
5943
+ return compileEquality(def, field, value, value.pos);
5944
+ }
5945
+ if (op === "!=") {
5946
+ const eq = compileEquality(def, field, value, value.pos);
5947
+ return (item, ctx) => !eq(item, ctx);
5948
+ }
5949
+ throw new CompileError([
5950
+ { pos: value.pos, message: `Field "${field}" does not support ordering comparisons (use ":" or "=").` }
5951
+ ]);
5952
+ }
5953
+ case "bool": {
5954
+ if (op === "=" || op === "!=") {
5955
+ const eq = compileEquality(def, field, value, value.pos);
5956
+ return op === "=" ? eq : (item, ctx) => !eq(item, ctx);
5957
+ }
5958
+ throw new CompileError([{ pos: value.pos, message: `Field "${field}" is boolean \u2014 use ${field}:true / ${field}:false` }]);
5959
+ }
5960
+ }
5961
+ }
5962
+ function compileAtom(atom, registry) {
5963
+ const def = resolveField(registry, atom.field);
5964
+ if (!def) {
5965
+ throw new CompileError([{ pos: atom.pos, message: `Unknown field "${atom.field}"` }]);
5966
+ }
5967
+ if (atom.op === ":") {
5968
+ const preds = atom.values.map((v) => compileEquality(def, atom.field, v, atom.pos));
5969
+ if (preds.length === 1) return preds[0];
5970
+ return (item, ctx) => preds.some((p) => p(item, ctx));
5971
+ }
5972
+ return compileComparison(def, atom.field, atom.op, atom.values[0]);
5973
+ }
5974
+ function compileNode(node, registry) {
5975
+ switch (node.kind) {
5976
+ case "all":
5977
+ return () => true;
5978
+ case "atom":
5979
+ return compileAtom(node, registry);
5980
+ case "not": {
5981
+ const inner = compileNode(node.child, registry);
5982
+ return (item, ctx) => !inner(item, ctx);
5983
+ }
5984
+ case "and": {
5985
+ const preds = node.children.map((c) => compileNode(c, registry));
5986
+ return (item, ctx) => preds.every((p) => p(item, ctx));
5987
+ }
5988
+ case "or": {
5989
+ const preds = node.children.map((c) => compileNode(c, registry));
5990
+ return (item, ctx) => preds.some((p) => p(item, ctx));
5991
+ }
5992
+ }
5993
+ }
5994
+ var CompileError;
5995
+ var init_evaluate = __esm({
5996
+ "src/utils/query/evaluate.ts"() {
5997
+ "use strict";
5998
+ init_fields();
5999
+ CompileError = class extends Error {
6000
+ constructor(errors) {
6001
+ super(errors.map((e) => `${e.message} (at ${e.pos})`).join("; "));
6002
+ this.errors = errors;
6003
+ this.name = "CompileError";
6004
+ }
6005
+ };
6006
+ }
6007
+ });
6008
+
6009
+ // src/utils/query/lexer.ts
6010
+ function lex(input) {
6011
+ const tokens = [];
6012
+ let i = 0;
6013
+ const numberOrDuration = (start, sign) => {
6014
+ let j = i;
6015
+ while (j < input.length && /\d/.test(input[j])) j++;
6016
+ const digits = input.slice(i, j);
6017
+ let unit = "";
6018
+ while (j < input.length && /[a-z]/i.test(input[j])) {
6019
+ unit += input[j];
6020
+ j++;
6021
+ }
6022
+ i = j;
6023
+ if (unit.length > 0) {
6024
+ const ms = DURATION_MS[unit.toLowerCase()];
6025
+ if (ms === void 0) {
6026
+ throw new LexError(start, `Unknown duration unit "${unit}" (expected h, d, w, m, mo, or y)`);
6027
+ }
6028
+ return {
6029
+ type: "DURATION",
6030
+ text: input.slice(start, j),
6031
+ pos: start,
6032
+ num: parseInt(digits, 10) * ms,
6033
+ sign
6034
+ };
6035
+ }
6036
+ if (sign !== 0) {
6037
+ return { type: "NUMBER", text: input.slice(start, j), pos: start, num: sign * parseInt(digits, 10) };
6038
+ }
6039
+ return { type: "NUMBER", text: digits, pos: start, num: parseInt(digits, 10) };
6040
+ };
6041
+ while (i < input.length) {
6042
+ const c = input[i];
6043
+ const start = i;
6044
+ if (c === " " || c === " " || c === "\n" || c === "\r") {
6045
+ i++;
6046
+ continue;
6047
+ }
6048
+ if (c === "(") {
6049
+ tokens.push({ type: "LPAREN", text: c, pos: start });
6050
+ i++;
6051
+ continue;
6052
+ }
6053
+ if (c === ")") {
6054
+ tokens.push({ type: "RPAREN", text: c, pos: start });
6055
+ i++;
6056
+ continue;
6057
+ }
6058
+ if (c === ",") {
6059
+ tokens.push({ type: "COMMA", text: c, pos: start });
6060
+ i++;
6061
+ continue;
6062
+ }
6063
+ if (c === ":") {
6064
+ tokens.push({ type: "COLON", text: c, pos: start });
6065
+ i++;
6066
+ continue;
6067
+ }
6068
+ if (c === "*") {
6069
+ tokens.push({ type: "STAR", text: c, pos: start });
6070
+ i++;
6071
+ continue;
6072
+ }
6073
+ if (c === "<" || c === ">") {
6074
+ if (input[i + 1] === "=") {
6075
+ tokens.push({ type: "OP", text: c + "=", pos: start });
6076
+ i += 2;
6077
+ } else {
6078
+ tokens.push({ type: "OP", text: c, pos: start });
6079
+ i++;
6080
+ }
6081
+ continue;
6082
+ }
6083
+ if (c === "!") {
6084
+ if (input[i + 1] === "=") {
6085
+ tokens.push({ type: "OP", text: "!=", pos: start });
6086
+ i += 2;
6087
+ continue;
6088
+ }
6089
+ throw new LexError(start, `Unexpected "!" (did you mean "!="?)`);
6090
+ }
6091
+ if (c === "=") {
6092
+ i += input[i + 1] === "=" ? 2 : 1;
6093
+ tokens.push({ type: "OP", text: "=", pos: start });
6094
+ continue;
6095
+ }
6096
+ if (c === '"' || c === "'") {
6097
+ const quote = c;
6098
+ let j = i + 1;
6099
+ let out = "";
6100
+ while (j < input.length && input[j] !== quote) {
6101
+ if (input[j] === "\\" && j + 1 < input.length) {
6102
+ out += input[j + 1];
6103
+ j += 2;
6104
+ } else {
6105
+ out += input[j];
6106
+ j++;
6107
+ }
6108
+ }
6109
+ if (j >= input.length) throw new LexError(start, "Unterminated string literal");
6110
+ tokens.push({ type: "STRING", text: out, pos: start });
6111
+ i = j + 1;
6112
+ continue;
6113
+ }
6114
+ if (c === "-" || c === "+") {
6115
+ if (/\d/.test(input[i + 1] ?? "")) {
6116
+ const sign = c === "-" ? -1 : 1;
6117
+ i++;
6118
+ tokens.push(numberOrDuration(start, sign));
6119
+ continue;
6120
+ }
6121
+ if (c === "-") {
6122
+ tokens.push({ type: "MINUS", text: "-", pos: start });
6123
+ i++;
6124
+ continue;
6125
+ }
6126
+ throw new LexError(start, 'Unexpected "+"');
6127
+ }
6128
+ if (/\d/.test(c)) {
6129
+ const dateMatch = input.slice(i).match(DATE_RE);
6130
+ if (dateMatch) {
6131
+ tokens.push({ type: "DATE", text: dateMatch[0], pos: start });
6132
+ i += dateMatch[0].length;
6133
+ continue;
6134
+ }
6135
+ tokens.push(numberOrDuration(start, 0));
6136
+ continue;
6137
+ }
6138
+ if (IDENT_START.test(c)) {
6139
+ let j = i + 1;
6140
+ while (j < input.length && IDENT_CHAR.test(input[j])) j++;
6141
+ const word = input.slice(i, j);
6142
+ const kw = word.toLowerCase();
6143
+ if (kw === "and") tokens.push({ type: "AND", text: word, pos: start });
6144
+ else if (kw === "or") tokens.push({ type: "OR", text: word, pos: start });
6145
+ else if (kw === "not") tokens.push({ type: "NOT", text: word, pos: start });
6146
+ else tokens.push({ type: "IDENT", text: word, pos: start });
6147
+ i = j;
6148
+ continue;
6149
+ }
6150
+ throw new LexError(start, `Unexpected character "${c}"`);
6151
+ }
6152
+ tokens.push({ type: "EOF", text: "", pos: input.length });
6153
+ return tokens;
6154
+ }
6155
+ var LexError, DURATION_MS, IDENT_START, IDENT_CHAR, DATE_RE;
6156
+ var init_lexer = __esm({
6157
+ "src/utils/query/lexer.ts"() {
6158
+ "use strict";
6159
+ LexError = class extends Error {
6160
+ constructor(pos, message) {
6161
+ super(message);
6162
+ this.pos = pos;
6163
+ this.name = "LexError";
6164
+ }
6165
+ };
6166
+ DURATION_MS = {
6167
+ h: 36e5,
6168
+ d: 864e5,
6169
+ w: 7 * 864e5,
6170
+ m: 30 * 864e5,
6171
+ mo: 30 * 864e5,
6172
+ y: 365 * 864e5
6173
+ };
6174
+ IDENT_START = /[A-Za-z_]/;
6175
+ IDENT_CHAR = /[A-Za-z0-9_-]/;
6176
+ DATE_RE = /^\d{4}-\d{2}-\d{2}/;
6177
+ }
6178
+ });
6179
+
6180
+ // src/utils/query/parser.ts
6181
+ function parseQuery(input) {
6182
+ try {
6183
+ const tokens = lex(input);
6184
+ const ast = new Parser(tokens).parseQuery();
6185
+ return { ast, errors: [] };
6186
+ } catch (err) {
6187
+ if (err instanceof LexError || err instanceof ParseError) {
6188
+ return { ast: null, errors: [{ pos: err.pos, message: err.message }] };
6189
+ }
6190
+ throw err;
6191
+ }
6192
+ }
6193
+ var ParseError, VALUE_TOKENS, TERM_START, Parser;
6194
+ var init_parser3 = __esm({
6195
+ "src/utils/query/parser.ts"() {
6196
+ "use strict";
6197
+ init_lexer();
6198
+ ParseError = class extends Error {
6199
+ constructor(pos, message) {
6200
+ super(message);
6201
+ this.pos = pos;
6202
+ this.name = "ParseError";
6203
+ }
6204
+ };
6205
+ VALUE_TOKENS = /* @__PURE__ */ new Set(["IDENT", "STRING", "NUMBER", "DATE", "DURATION"]);
6206
+ TERM_START = /* @__PURE__ */ new Set(["IDENT", "NOT", "MINUS", "LPAREN", "STAR"]);
6207
+ Parser = class {
6208
+ constructor(tokens) {
6209
+ this.tokens = tokens;
6210
+ }
6211
+ pos = 0;
6212
+ peek() {
6213
+ return this.tokens[this.pos];
6214
+ }
6215
+ next() {
6216
+ return this.tokens[this.pos++];
6217
+ }
6218
+ expect(type, what) {
6219
+ const tok = this.peek();
6220
+ if (tok.type !== type) {
6221
+ throw new ParseError(tok.pos, `Expected ${what}, got "${tok.text || tok.type}"`);
6222
+ }
6223
+ return this.next();
6224
+ }
6225
+ parseQuery() {
6226
+ if (this.peek().type === "EOF") return { kind: "all" };
6227
+ const node = this.orExpr();
6228
+ const tok = this.peek();
6229
+ if (tok.type !== "EOF") {
6230
+ throw new ParseError(tok.pos, `Unexpected "${tok.text}" \u2014 unbalanced parentheses or stray token`);
6231
+ }
6232
+ return node;
6233
+ }
6234
+ orExpr() {
6235
+ const children = [this.andExpr()];
6236
+ while (this.peek().type === "OR") {
6237
+ this.next();
6238
+ children.push(this.andExpr());
6239
+ }
6240
+ return children.length === 1 ? children[0] : { kind: "or", children };
6241
+ }
6242
+ andExpr() {
6243
+ const children = [this.unary()];
6244
+ for (; ; ) {
6245
+ const tok = this.peek();
6246
+ if (tok.type === "AND") {
6247
+ this.next();
6248
+ children.push(this.unary());
6249
+ } else if (TERM_START.has(tok.type)) {
6250
+ children.push(this.unary());
6251
+ } else {
6252
+ break;
6253
+ }
6254
+ }
6255
+ return children.length === 1 ? children[0] : { kind: "and", children };
6256
+ }
6257
+ unary() {
6258
+ const tok = this.peek();
6259
+ if (tok.type === "NOT") {
6260
+ this.next();
6261
+ return { kind: "not", child: this.unary() };
6262
+ }
6263
+ if (tok.type === "MINUS") {
6264
+ this.next();
6265
+ const inner = this.peek();
6266
+ if (inner.type !== "IDENT") {
6267
+ throw new ParseError(inner.pos, 'Expected a field atom after "-" negation');
6268
+ }
6269
+ return { kind: "not", child: this.atom() };
6270
+ }
6271
+ return this.primary();
6272
+ }
6273
+ primary() {
6274
+ const tok = this.peek();
6275
+ if (tok.type === "LPAREN") {
6276
+ this.next();
6277
+ const node = this.orExpr();
6278
+ this.expect("RPAREN", '")"');
6279
+ return node;
6280
+ }
6281
+ if (tok.type === "STAR") {
6282
+ this.next();
6283
+ return { kind: "all" };
6284
+ }
6285
+ if (tok.type === "IDENT") {
6286
+ return this.atom();
6287
+ }
6288
+ throw new ParseError(tok.pos, `Expected a field, "(", "*", or NOT \u2014 got "${tok.text || "end of query"}"`);
6289
+ }
6290
+ atom() {
6291
+ const fieldTok = this.expect("IDENT", "a field name");
6292
+ const opTok = this.peek();
6293
+ if (opTok.type === "COLON") {
6294
+ this.next();
6295
+ const values = this.valueOrList();
6296
+ return { kind: "atom", field: fieldTok.text, op: ":", values, pos: fieldTok.pos };
6297
+ }
6298
+ if (opTok.type === "OP") {
6299
+ this.next();
6300
+ const value = this.value();
6301
+ return {
6302
+ kind: "atom",
6303
+ field: fieldTok.text,
6304
+ op: opTok.text,
6305
+ values: [value],
6306
+ pos: fieldTok.pos
6307
+ };
6308
+ }
6309
+ throw new ParseError(
6310
+ opTok.pos,
6311
+ `Expected ":" or a comparison operator after field "${fieldTok.text}"`
6312
+ );
6313
+ }
6314
+ valueOrList() {
6315
+ if (this.peek().type === "LPAREN") {
6316
+ this.next();
6317
+ const values = [this.value()];
6318
+ while (this.peek().type === "COMMA") {
6319
+ this.next();
6320
+ values.push(this.value());
6321
+ }
6322
+ this.expect("RPAREN", '")" to close the value list');
6323
+ return values;
6324
+ }
6325
+ return [this.value()];
6326
+ }
6327
+ value() {
6328
+ const tok = this.peek();
6329
+ if (!VALUE_TOKENS.has(tok.type)) {
6330
+ throw new ParseError(tok.pos, `Expected a value, got "${tok.text || tok.type}"`);
6331
+ }
6332
+ this.next();
6333
+ switch (tok.type) {
6334
+ case "STRING":
6335
+ return { type: "string", raw: tok.text, pos: tok.pos };
6336
+ case "NUMBER":
6337
+ return { type: "number", raw: tok.text, num: tok.num, pos: tok.pos };
6338
+ case "DATE":
6339
+ return { type: "date", raw: tok.text, pos: tok.pos };
6340
+ case "DURATION":
6341
+ return { type: "duration", raw: tok.text, num: tok.num, sign: tok.sign ?? 0, pos: tok.pos };
6342
+ default:
6343
+ return { type: "word", raw: tok.text, pos: tok.pos };
6344
+ }
6345
+ }
6346
+ };
6347
+ }
6348
+ });
6349
+
6350
+ // src/utils/query/index.ts
6351
+ var init_query = __esm({
6352
+ "src/utils/query/index.ts"() {
6353
+ "use strict";
6354
+ init_evaluate();
6355
+ init_fields();
6356
+ init_parser3();
6357
+ init_parser3();
6358
+ init_evaluate();
6359
+ init_fields();
6360
+ }
6361
+ });
6362
+
6363
+ // src/lifecycle/derive.ts
6364
+ var derive_exports = {};
6365
+ __export(derive_exports, {
6366
+ DERIVE_FIELDS: () => DERIVE_FIELDS,
6367
+ deriveDimensions: () => deriveDimensions,
6368
+ validateDeriveCondition: () => validateDeriveCondition
6369
+ });
6370
+ function validateDeriveCondition(when) {
6371
+ if (when === "*") return null;
6372
+ const parsed = parseQuery(when);
6373
+ if (!parsed.ast) return parsed.errors[0]?.message ?? "unparseable condition";
6374
+ try {
6375
+ compileNode(parsed.ast, DERIVE_FIELDS);
6376
+ return null;
6377
+ } catch (err) {
6378
+ if (err instanceof CompileError) return err.errors[0]?.message ?? "invalid condition";
6379
+ throw err;
6380
+ }
6381
+ }
6382
+ function compiledWhen(derive, when) {
6383
+ let cache2 = conditionCache.get(derive);
6384
+ if (!cache2) {
6385
+ cache2 = /* @__PURE__ */ new Map();
6386
+ conditionCache.set(derive, cache2);
6387
+ }
6388
+ let pred = cache2.get(when);
6389
+ if (!pred) {
6390
+ if (when === "*") {
6391
+ pred = () => true;
6392
+ } else {
6393
+ const parsed = parseQuery(when);
6394
+ if (!parsed.ast) {
6395
+ throw new CompileError(parsed.errors);
6396
+ }
6397
+ pred = compileNode(parsed.ast, DERIVE_FIELDS);
6398
+ }
6399
+ cache2.set(when, pred);
6400
+ }
6401
+ return pred;
6402
+ }
6403
+ function deriveDimensions(input) {
6404
+ const { facts, derive, currentStatus, terminalStatuses, knownStatusIds, override } = input;
6405
+ if (terminalStatuses.has(currentStatus)) return null;
6406
+ const ctx = { now: 0 };
6407
+ const item = facts;
6408
+ let phase = derive.phaseLadder[0]?.phase ?? currentStatus;
6409
+ let nextAction = derive.phaseLadder[0]?.next ?? null;
6410
+ for (let i = derive.phaseLadder.length - 1; i >= 0; i--) {
6411
+ const rung = derive.phaseLadder[i];
6412
+ if (compiledWhen(derive, rung.when)(item, ctx)) {
6413
+ phase = rung.phase;
6414
+ nextAction = rung.next ?? null;
6415
+ break;
6416
+ }
6417
+ }
6418
+ let disposition = "active";
6419
+ for (const rule of derive.disposition) {
6420
+ if (rule.when === null || compiledWhen(derive, rule.when)(item, ctx)) {
6421
+ disposition = rule.is;
6422
+ break;
6423
+ }
6424
+ }
6425
+ let derivedStatus;
6426
+ switch (disposition) {
6427
+ case "parked":
6428
+ derivedStatus = knownStatusIds.has(derive.headline.parked) ? derive.headline.parked : phase;
6429
+ break;
6430
+ case "blocked":
6431
+ derivedStatus = knownStatusIds.has(derive.headline.blocked) ? derive.headline.blocked : phase;
6432
+ break;
6433
+ default:
6434
+ derivedStatus = phase;
6435
+ }
6436
+ let status = derivedStatus;
6437
+ if (override && override.status && !terminalStatuses.has(override.status) && knownStatusIds.has(override.status)) {
6438
+ status = override.status;
5089
6439
  }
5090
- return scanSession(data, lsofOutput, workspaceRecords, procSnapshot);
6440
+ return { phase, disposition, derivedStatus, status, nextAction };
5091
6441
  }
5092
- var exec, cache, lastKnown, inFlight, forceFreshNext, scanEpoch, cacheKey, CACHE_TTL_MS, COLD_WAIT_BUDGET_MS, PROBE_TIMEOUT_MS, PROBE_MAX_BUFFER, tmuxAvailableCache;
5093
- var init_scanner = __esm({
5094
- "src/dashboard/scanner.ts"() {
6442
+ var DERIVE_FIELDS, conditionCache;
6443
+ var init_derive = __esm({
6444
+ "src/lifecycle/derive.ts"() {
5095
6445
  "use strict";
5096
- init_api();
5097
- init_servers();
5098
- exec = promisify(execFile);
5099
- cache = null;
5100
- lastKnown = null;
5101
- inFlight = null;
5102
- forceFreshNext = false;
5103
- scanEpoch = 0;
5104
- cacheKey = null;
5105
- CACHE_TTL_MS = 1e4;
5106
- COLD_WAIT_BUDGET_MS = 2500;
5107
- PROBE_TIMEOUT_MS = 15e3;
5108
- PROBE_MAX_BUFFER = 32 * 1024 * 1024;
5109
- tmuxAvailableCache = null;
6446
+ init_query();
6447
+ DERIVE_FIELDS = {
6448
+ hasrealobjective: { kind: "bool", get: (i) => i["hasRealObjective"] },
6449
+ acrealtotal: { kind: "number", get: (i) => i["acRealTotal"] },
6450
+ acrealchecked: { kind: "number", get: (i) => i["acRealChecked"] },
6451
+ acallchecked: { kind: "bool", get: (i) => i["acAllChecked"] },
6452
+ planexists: { kind: "bool", get: (i) => i["planExists"] },
6453
+ planapproved: { kind: "bool", get: (i) => i["planApproved"] },
6454
+ workspaceset: { kind: "bool", get: (i) => i["workspaceSet"] },
6455
+ implementationstarted: { kind: "bool", get: (i) => i["implementationStarted"] },
6456
+ depssatisfied: { kind: "bool", get: (i) => i["depsSatisfied"] },
6457
+ unresolvedquestions: { kind: "number", get: (i) => i["unresolvedQuestions"] },
6458
+ blocked: { kind: "bool" },
6459
+ parked: { kind: "bool" },
6460
+ reviewrequested: { kind: "bool", get: (i) => i["reviewRequested"] },
6461
+ pinned: { kind: "bool" }
6462
+ };
6463
+ conditionCache = /* @__PURE__ */ new WeakMap();
5110
6464
  }
5111
6465
  });
5112
6466
 
5113
6467
  // src/dashboard/api.ts
5114
- import { readdir as readdir7, readFile as readFile10, writeFile as writeFile3 } from "fs/promises";
5115
- import { resolve as resolve13, dirname as dirname2, basename } from "path";
6468
+ import { readdir as readdir8, readFile as readFile11, writeFile as writeFile3 } from "fs/promises";
6469
+ import { resolve as resolve14, dirname as dirname2, basename } from "path";
5116
6470
  function clearFrontmatterField(content, key) {
5117
6471
  const fieldRegex = new RegExp(`^(${escapeRegExp2(key)}:)\\s*.*$`, "m");
5118
6472
  return content.replace(fieldRegex, `$1 null`);
@@ -5191,15 +6545,15 @@ async function listStandaloneRecords(assignmentsDir2) {
5191
6545
  async function computeStandaloneRecords(assignmentsDir2) {
5192
6546
  if (!assignmentsDir2) return [];
5193
6547
  if (!await fileExists(assignmentsDir2)) return [];
5194
- const entries = await readdir7(assignmentsDir2, { withFileTypes: true });
6548
+ const entries = await readdir8(assignmentsDir2, { withFileTypes: true });
5195
6549
  const records = [];
5196
6550
  for (const entry of entries) {
5197
6551
  if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
5198
- const assignmentDir = resolve13(assignmentsDir2, entry.name);
5199
- const assignmentMdPath = resolve13(assignmentDir, "assignment.md");
6552
+ const assignmentDir = resolve14(assignmentsDir2, entry.name);
6553
+ const assignmentMdPath = resolve14(assignmentDir, "assignment.md");
5200
6554
  if (!await fileExists(assignmentMdPath)) continue;
5201
6555
  try {
5202
- const content = await readFile10(assignmentMdPath, "utf-8");
6556
+ const content = await readFile11(assignmentMdPath, "utf-8");
5203
6557
  const record = parseAssignmentFull(content);
5204
6558
  records.push({ assignmentDir, id: entry.name, record });
5205
6559
  } catch {
@@ -5240,7 +6594,8 @@ async function getStatusConfig() {
5240
6594
  order: config.statuses.order,
5241
6595
  transitions: effectiveTransitions,
5242
6596
  transitionTable: buildTransitionTable(effectiveTransitions),
5243
- terminalStatuses: terminalSet.size > 0 ? terminalSet : /* @__PURE__ */ new Set(["completed", "failed"])
6597
+ terminalStatuses: terminalSet.size > 0 ? terminalSet : /* @__PURE__ */ new Set(["completed", "failed"]),
6598
+ derive: config.statuses.derive ?? null
5244
6599
  };
5245
6600
  } else {
5246
6601
  const def = buildDefaultStatusConfig();
@@ -5250,7 +6605,8 @@ async function getStatusConfig() {
5250
6605
  order: def.order,
5251
6606
  transitions: def.transitions,
5252
6607
  transitionTable: DEFAULT_TRANSITION_TABLE,
5253
- terminalStatuses: /* @__PURE__ */ new Set(["completed", "failed"])
6608
+ terminalStatuses: /* @__PURE__ */ new Set(["completed", "failed"]),
6609
+ derive: null
5254
6610
  };
5255
6611
  }
5256
6612
  return _cachedConfig;
@@ -5263,9 +6619,9 @@ async function listProjects(projectsDir) {
5263
6619
  return projectRecords.filter((record) => !isProjectArchived(record.summary)).map((record) => record.summary);
5264
6620
  }
5265
6621
  async function readWorkspaceRegistry(projectsDir) {
5266
- const registryPath = resolve13(dirname2(projectsDir), "workspaces.json");
6622
+ const registryPath = resolve14(dirname2(projectsDir), "workspaces.json");
5267
6623
  try {
5268
- const raw2 = await readFile10(registryPath, "utf-8");
6624
+ const raw2 = await readFile11(registryPath, "utf-8");
5269
6625
  const parsed = JSON.parse(raw2);
5270
6626
  return Array.isArray(parsed) ? parsed.filter((w) => typeof w === "string") : [];
5271
6627
  } catch {
@@ -5273,7 +6629,7 @@ async function readWorkspaceRegistry(projectsDir) {
5273
6629
  }
5274
6630
  }
5275
6631
  async function writeWorkspaceRegistry(projectsDir, workspaces) {
5276
- const registryPath = resolve13(dirname2(projectsDir), "workspaces.json");
6632
+ const registryPath = resolve14(dirname2(projectsDir), "workspaces.json");
5277
6633
  await writeFile3(registryPath, JSON.stringify(workspaces, null, 2) + "\n", "utf-8");
5278
6634
  }
5279
6635
  async function listWorkspaces(projectsDir, assignmentsDir2) {
@@ -5353,8 +6709,8 @@ async function deleteWorkspace(projectsDir, name, opts = {}) {
5353
6709
  if (cascade) {
5354
6710
  const timestamp = nowTimestamp();
5355
6711
  for (const slug of projectsReferencing) {
5356
- const path = resolve13(projectsDir, slug, "project.md");
5357
- const raw2 = await readFile10(path, "utf-8");
6712
+ const path = resolve14(projectsDir, slug, "project.md");
6713
+ const raw2 = await readFile11(path, "utf-8");
5358
6714
  let next = clearFrontmatterField(raw2, "workspace");
5359
6715
  next = setUpdatedField(next, timestamp);
5360
6716
  await writeFileForce(path, next);
@@ -5362,8 +6718,8 @@ async function deleteWorkspace(projectsDir, name, opts = {}) {
5362
6718
  }
5363
6719
  for (const id of standalonesReferencing) {
5364
6720
  if (!opts.assignmentsDir) break;
5365
- const path = resolve13(opts.assignmentsDir, id, "assignment.md");
5366
- const raw2 = await readFile10(path, "utf-8");
6721
+ const path = resolve14(opts.assignmentsDir, id, "assignment.md");
6722
+ const raw2 = await readFile11(path, "utf-8");
5367
6723
  let next = clearFrontmatterField(raw2, "workspaceGroup");
5368
6724
  next = setUpdatedField(next, timestamp);
5369
6725
  await writeFileForce(path, next);
@@ -5559,8 +6915,9 @@ async function listArchived(projectsDir, assignmentsDir2) {
5559
6915
  return { projects, assignments: individuallyArchived };
5560
6916
  }
5561
6917
  async function toStandaloneBoardItem(sr) {
6918
+ const { terminalStatuses } = await getStatusConfig();
5562
6919
  return {
5563
- ...toAssignmentSummary(sr.record),
6920
+ ...toAssignmentSummary(sr.record, terminalStatuses),
5564
6921
  projectSlug: null,
5565
6922
  projectTitle: null,
5566
6923
  blockedReason: sr.record.blockedReason,
@@ -5600,7 +6957,7 @@ async function getEditableDocument(projectsDir, documentType, projectSlug, assig
5600
6957
  if (!filePath || !await fileExists(filePath)) {
5601
6958
  return null;
5602
6959
  }
5603
- const content = await readFile10(filePath, "utf-8");
6960
+ const content = await readFile11(filePath, "utf-8");
5604
6961
  const title = getEditableDocumentTitle(documentType, projectSlug, assignmentSlug);
5605
6962
  return {
5606
6963
  documentType,
@@ -5624,9 +6981,9 @@ async function getEditableDocumentById(projectsDir, assignmentsDir2, documentTyp
5624
6981
  }
5625
6982
  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;
5626
6983
  if (!fileName) return null;
5627
- const filePath = resolve13(resolved.assignmentDir, fileName);
6984
+ const filePath = resolve14(resolved.assignmentDir, fileName);
5628
6985
  if (!await fileExists(filePath)) return null;
5629
- const content = await readFile10(filePath, "utf-8");
6986
+ const content = await readFile11(filePath, "utf-8");
5630
6987
  const label = resolved.id;
5631
6988
  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}`;
5632
6989
  return {
@@ -5640,12 +6997,12 @@ async function getEditableDocumentById(projectsDir, assignmentsDir2, documentTyp
5640
6997
  };
5641
6998
  }
5642
6999
  async function getProjectDetail(projectsDir, slug) {
5643
- const projectPath = resolve13(projectsDir, slug);
5644
- const projectMdPath = resolve13(projectPath, "project.md");
7000
+ const projectPath = resolve14(projectsDir, slug);
7001
+ const projectMdPath = resolve14(projectPath, "project.md");
5645
7002
  if (!await fileExists(projectMdPath)) {
5646
7003
  return null;
5647
7004
  }
5648
- const projectContent = await readFile10(projectMdPath, "utf-8");
7005
+ const projectContent = await readFile11(projectMdPath, "utf-8");
5649
7006
  const project = parseProject(projectContent);
5650
7007
  const assignments = await listAssignmentRecords(projectPath);
5651
7008
  const rollup = await buildProjectRollup(projectPath, project, assignments);
@@ -5653,6 +7010,7 @@ async function getProjectDetail(projectsDir, slug) {
5653
7010
  const resources = await listResources(projectPath);
5654
7011
  const memories = await listMemories(projectPath);
5655
7012
  const updated = getProjectActivityTimestamp(project.updated, activeAssignments(assignments));
7013
+ const { terminalStatuses } = await getStatusConfig();
5656
7014
  return {
5657
7015
  slug: project.slug || slug,
5658
7016
  title: project.title,
@@ -5668,7 +7026,7 @@ async function getProjectDetail(projectsDir, slug) {
5668
7026
  body: project.body,
5669
7027
  progress: rollup.progress,
5670
7028
  needsAttention: rollup.needsAttention,
5671
- assignments: assignments.map(toAssignmentSummary).sort((left, right) => compareTimestamps(right.updated, left.updated)),
7029
+ assignments: assignments.map((a) => toAssignmentSummary(a, terminalStatuses)).sort((left, right) => compareTimestamps(right.updated, left.updated)),
5672
7030
  resources,
5673
7031
  memories,
5674
7032
  dependencyGraph,
@@ -5677,23 +7035,23 @@ async function getProjectDetail(projectsDir, slug) {
5677
7035
  };
5678
7036
  }
5679
7037
  async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5680
- const assignmentDir = resolve13(projectsDir, projectSlug, "assignments", assignmentSlug);
5681
- const assignmentMdPath = resolve13(assignmentDir, "assignment.md");
7038
+ const assignmentDir = resolve14(projectsDir, projectSlug, "assignments", assignmentSlug);
7039
+ const assignmentMdPath = resolve14(assignmentDir, "assignment.md");
5682
7040
  if (!await fileExists(assignmentMdPath)) {
5683
7041
  return null;
5684
7042
  }
5685
- const assignmentContent = await readFile10(assignmentMdPath, "utf-8");
7043
+ const assignmentContent = await readFile11(assignmentMdPath, "utf-8");
5686
7044
  const assignment = parseAssignmentFull(assignmentContent);
5687
7045
  let projectWorkspace = null;
5688
- const projectMdPath = resolve13(projectsDir, projectSlug, "project.md");
7046
+ const projectMdPath = resolve14(projectsDir, projectSlug, "project.md");
5689
7047
  if (await fileExists(projectMdPath)) {
5690
- const projectContent = await readFile10(projectMdPath, "utf-8");
7048
+ const projectContent = await readFile11(projectMdPath, "utf-8");
5691
7049
  projectWorkspace = parseProject(projectContent).workspace;
5692
7050
  }
5693
7051
  let plan = null;
5694
- const planPath = resolve13(assignmentDir, "plan.md");
7052
+ const planPath = resolve14(assignmentDir, "plan.md");
5695
7053
  if (await fileExists(planPath)) {
5696
- const planContent = await readFile10(planPath, "utf-8");
7054
+ const planContent = await readFile11(planPath, "utf-8");
5697
7055
  const parsed = parsePlan(planContent);
5698
7056
  plan = {
5699
7057
  status: parsed.status,
@@ -5702,9 +7060,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5702
7060
  };
5703
7061
  }
5704
7062
  let scratchpad = null;
5705
- const scratchpadPath = resolve13(assignmentDir, "scratchpad.md");
7063
+ const scratchpadPath = resolve14(assignmentDir, "scratchpad.md");
5706
7064
  if (await fileExists(scratchpadPath)) {
5707
- const scratchpadContent = await readFile10(scratchpadPath, "utf-8");
7065
+ const scratchpadContent = await readFile11(scratchpadPath, "utf-8");
5708
7066
  const parsed = parseScratchpad(scratchpadContent);
5709
7067
  scratchpad = {
5710
7068
  updated: parsed.updated,
@@ -5712,9 +7070,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5712
7070
  };
5713
7071
  }
5714
7072
  let handoff = null;
5715
- const handoffPath = resolve13(assignmentDir, "handoff.md");
7073
+ const handoffPath = resolve14(assignmentDir, "handoff.md");
5716
7074
  if (await fileExists(handoffPath)) {
5717
- const handoffContent = await readFile10(handoffPath, "utf-8");
7075
+ const handoffContent = await readFile11(handoffPath, "utf-8");
5718
7076
  const parsed = parseHandoff(handoffContent);
5719
7077
  handoff = {
5720
7078
  updated: parsed.updated,
@@ -5723,9 +7081,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5723
7081
  };
5724
7082
  }
5725
7083
  let decisionRecord = null;
5726
- const decisionRecordPath = resolve13(assignmentDir, "decision-record.md");
7084
+ const decisionRecordPath = resolve14(assignmentDir, "decision-record.md");
5727
7085
  if (await fileExists(decisionRecordPath)) {
5728
- const decisionRecordContent = await readFile10(decisionRecordPath, "utf-8");
7086
+ const decisionRecordContent = await readFile11(decisionRecordPath, "utf-8");
5729
7087
  const parsed = parseDecisionRecord(decisionRecordContent);
5730
7088
  decisionRecord = {
5731
7089
  updated: parsed.updated,
@@ -5734,9 +7092,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5734
7092
  };
5735
7093
  }
5736
7094
  let progress = null;
5737
- const progressPath = resolve13(assignmentDir, "progress.md");
7095
+ const progressPath = resolve14(assignmentDir, "progress.md");
5738
7096
  if (await fileExists(progressPath)) {
5739
- const progressContent = await readFile10(progressPath, "utf-8");
7097
+ const progressContent = await readFile11(progressPath, "utf-8");
5740
7098
  const parsed = parseProgress(progressContent);
5741
7099
  progress = {
5742
7100
  updated: parsed.updated,
@@ -5745,9 +7103,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5745
7103
  };
5746
7104
  }
5747
7105
  let comments = null;
5748
- const commentsPath = resolve13(assignmentDir, "comments.md");
7106
+ const commentsPath = resolve14(assignmentDir, "comments.md");
5749
7107
  if (await fileExists(commentsPath)) {
5750
- const commentsContent = await readFile10(commentsPath, "utf-8");
7108
+ const commentsContent = await readFile11(commentsPath, "utf-8");
5751
7109
  const parsed = parseComments(commentsContent);
5752
7110
  comments = {
5753
7111
  updated: parsed.updated,
@@ -5755,6 +7113,7 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5755
7113
  entries: parsed.entries
5756
7114
  };
5757
7115
  }
7116
+ const { terminalStatuses } = await getStatusConfig();
5758
7117
  const detail = {
5759
7118
  id: assignment.id,
5760
7119
  projectSlug,
@@ -5776,6 +7135,9 @@ async function getAssignmentDetail(projectsDir, projectSlug, assignmentSlug) {
5776
7135
  archived: assignment.archived,
5777
7136
  archivedAt: assignment.archivedAt,
5778
7137
  archivedReason: assignment.archivedReason,
7138
+ ...deriveStatusVirtuals(assignment, terminalStatuses),
7139
+ override: assignment.override,
7140
+ derived: await buildDerivedDetail(assignment, assignmentDir, resolve14(projectsDir, projectSlug)),
5779
7141
  created: assignment.created,
5780
7142
  updated: assignment.updated,
5781
7143
  body: assignment.body,
@@ -5866,7 +7228,7 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir2) {
5866
7228
  slug: a.slug,
5867
7229
  title: a.title,
5868
7230
  projectSlug: rec.summary.slug,
5869
- assignmentDir: resolve13(rec.projectPath, "assignments", a.slug)
7231
+ assignmentDir: resolve14(rec.projectPath, "assignments", a.slug)
5870
7232
  });
5871
7233
  }
5872
7234
  }
@@ -5899,17 +7261,17 @@ async function computeReferencedBy(target, projectsDir, assignmentsDir2) {
5899
7261
  }
5900
7262
  async function countMentionsInAssignment(sourceDir, target) {
5901
7263
  const bodies = [];
5902
- const assignmentMd = resolve13(sourceDir, "assignment.md");
7264
+ const assignmentMd = resolve14(sourceDir, "assignment.md");
5903
7265
  if (await fileExists(assignmentMd)) {
5904
- const content = await readFile10(assignmentMd, "utf-8");
7266
+ const content = await readFile11(assignmentMd, "utf-8");
5905
7267
  const todosMatch = content.match(/^## Todos\s*$([\s\S]*?)(?=^## |$(?![\r\n]))/m);
5906
7268
  if (todosMatch) bodies.push(todosMatch[1]);
5907
7269
  }
5908
7270
  for (const filename of ["progress.md", "comments.md", "handoff.md"]) {
5909
- const path = resolve13(sourceDir, filename);
7271
+ const path = resolve14(sourceDir, filename);
5910
7272
  if (await fileExists(path)) {
5911
7273
  try {
5912
- bodies.push(await readFile10(path, "utf-8"));
7274
+ bodies.push(await readFile11(path, "utf-8"));
5913
7275
  } catch {
5914
7276
  }
5915
7277
  }
@@ -5967,46 +7329,47 @@ async function getAssignmentDetailById(projectsDir, assignmentsDir2, id) {
5967
7329
  }
5968
7330
  async function buildStandaloneAssignmentDetail(resolved) {
5969
7331
  const assignmentDir = resolved.assignmentDir;
5970
- const assignmentMdPath = resolve13(assignmentDir, "assignment.md");
7332
+ const assignmentMdPath = resolve14(assignmentDir, "assignment.md");
5971
7333
  if (!await fileExists(assignmentMdPath)) return null;
5972
- const assignmentContent = await readFile10(assignmentMdPath, "utf-8");
7334
+ const assignmentContent = await readFile11(assignmentMdPath, "utf-8");
5973
7335
  const assignment = parseAssignmentFull(assignmentContent);
5974
7336
  let plan = null;
5975
- const planPath = resolve13(assignmentDir, "plan.md");
7337
+ const planPath = resolve14(assignmentDir, "plan.md");
5976
7338
  if (await fileExists(planPath)) {
5977
- const parsed = parsePlan(await readFile10(planPath, "utf-8"));
7339
+ const parsed = parsePlan(await readFile11(planPath, "utf-8"));
5978
7340
  plan = { status: parsed.status, updated: parsed.updated, body: parsed.body };
5979
7341
  }
5980
7342
  let scratchpad = null;
5981
- const scratchpadPath = resolve13(assignmentDir, "scratchpad.md");
7343
+ const scratchpadPath = resolve14(assignmentDir, "scratchpad.md");
5982
7344
  if (await fileExists(scratchpadPath)) {
5983
- const parsed = parseScratchpad(await readFile10(scratchpadPath, "utf-8"));
7345
+ const parsed = parseScratchpad(await readFile11(scratchpadPath, "utf-8"));
5984
7346
  scratchpad = { updated: parsed.updated, body: parsed.body };
5985
7347
  }
5986
7348
  let handoff = null;
5987
- const handoffPath = resolve13(assignmentDir, "handoff.md");
7349
+ const handoffPath = resolve14(assignmentDir, "handoff.md");
5988
7350
  if (await fileExists(handoffPath)) {
5989
- const parsed = parseHandoff(await readFile10(handoffPath, "utf-8"));
7351
+ const parsed = parseHandoff(await readFile11(handoffPath, "utf-8"));
5990
7352
  handoff = { updated: parsed.updated, handoffCount: parsed.handoffCount, body: parsed.body };
5991
7353
  }
5992
7354
  let decisionRecord = null;
5993
- const decisionRecordPath = resolve13(assignmentDir, "decision-record.md");
7355
+ const decisionRecordPath = resolve14(assignmentDir, "decision-record.md");
5994
7356
  if (await fileExists(decisionRecordPath)) {
5995
- const parsed = parseDecisionRecord(await readFile10(decisionRecordPath, "utf-8"));
7357
+ const parsed = parseDecisionRecord(await readFile11(decisionRecordPath, "utf-8"));
5996
7358
  decisionRecord = { updated: parsed.updated, decisionCount: parsed.decisionCount, body: parsed.body };
5997
7359
  }
5998
7360
  let progress = null;
5999
- const progressPath = resolve13(assignmentDir, "progress.md");
7361
+ const progressPath = resolve14(assignmentDir, "progress.md");
6000
7362
  if (await fileExists(progressPath)) {
6001
- const parsed = parseProgress(await readFile10(progressPath, "utf-8"));
7363
+ const parsed = parseProgress(await readFile11(progressPath, "utf-8"));
6002
7364
  progress = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
6003
7365
  }
6004
7366
  let comments = null;
6005
- const commentsPath = resolve13(assignmentDir, "comments.md");
7367
+ const commentsPath = resolve14(assignmentDir, "comments.md");
6006
7368
  if (await fileExists(commentsPath)) {
6007
- const parsed = parseComments(await readFile10(commentsPath, "utf-8"));
7369
+ const parsed = parseComments(await readFile11(commentsPath, "utf-8"));
6008
7370
  comments = { updated: parsed.updated, entryCount: parsed.entryCount, entries: parsed.entries };
6009
7371
  }
7372
+ const { terminalStatuses } = await getStatusConfig();
6010
7373
  const detail = {
6011
7374
  id: assignment.id,
6012
7375
  projectSlug: null,
@@ -6029,6 +7392,9 @@ async function buildStandaloneAssignmentDetail(resolved) {
6029
7392
  archived: assignment.archived,
6030
7393
  archivedAt: assignment.archivedAt,
6031
7394
  archivedReason: assignment.archivedReason,
7395
+ ...deriveStatusVirtuals(assignment, terminalStatuses),
7396
+ override: assignment.override,
7397
+ derived: await buildDerivedDetail(assignment, assignmentDir, null),
6032
7398
  created: assignment.created,
6033
7399
  updated: assignment.updated,
6034
7400
  body: assignment.body,
@@ -6060,17 +7426,17 @@ async function computeProjectRecords(projectsDir, traces) {
6060
7426
  await migrateLegacyProjectFiles(projectsDir);
6061
7427
  await migrateLegacyArchivedProjects(projectsDir);
6062
7428
  }
6063
- const entries = await readdir7(projectsDir, { withFileTypes: true });
7429
+ const entries = await readdir8(projectsDir, { withFileTypes: true });
6064
7430
  const projectDirs = entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."));
6065
7431
  const maybeRecords = await Promise.all(
6066
7432
  projectDirs.map(async (entry) => {
6067
- const projectPath = resolve13(projectsDir, entry.name);
6068
- const projectMdPath = resolve13(projectPath, "project.md");
7433
+ const projectPath = resolve14(projectsDir, entry.name);
7434
+ const projectMdPath = resolve14(projectPath, "project.md");
6069
7435
  if (!await fileExists(projectMdPath)) {
6070
7436
  return null;
6071
7437
  }
6072
7438
  const t0 = traces ? performance.now() : 0;
6073
- const projectContent = await readFile10(projectMdPath, "utf-8");
7439
+ const projectContent = await readFile11(projectMdPath, "utf-8");
6074
7440
  const project = parseProject(projectContent);
6075
7441
  if (traces) accumulatePhase(traces, "parse-project-md", performance.now() - t0);
6076
7442
  const t1 = traces ? performance.now() : 0;
@@ -6111,20 +7477,20 @@ async function computeProjectRecords(projectsDir, traces) {
6111
7477
  return records;
6112
7478
  }
6113
7479
  async function listAssignmentRecords(projectPath, traces) {
6114
- const assignmentsDir2 = resolve13(projectPath, "assignments");
7480
+ const assignmentsDir2 = resolve14(projectPath, "assignments");
6115
7481
  if (!await fileExists(assignmentsDir2)) {
6116
7482
  return [];
6117
7483
  }
6118
- const entries = await readdir7(assignmentsDir2, { withFileTypes: true });
7484
+ const entries = await readdir8(assignmentsDir2, { withFileTypes: true });
6119
7485
  const dirEntries = entries.filter((entry) => entry.isDirectory());
6120
7486
  const maybeRecords = await Promise.all(
6121
7487
  dirEntries.map(async (entry) => {
6122
- const assignmentMd = resolve13(assignmentsDir2, entry.name, "assignment.md");
7488
+ const assignmentMd = resolve14(assignmentsDir2, entry.name, "assignment.md");
6123
7489
  if (!await fileExists(assignmentMd)) {
6124
7490
  return null;
6125
7491
  }
6126
7492
  const t0 = traces ? performance.now() : 0;
6127
- const content = await readFile10(assignmentMd, "utf-8");
7493
+ const content = await readFile11(assignmentMd, "utf-8");
6128
7494
  const parsed = parseAssignmentFull(content);
6129
7495
  if (traces) accumulatePhase(traces, "read-assignment-md", performance.now() - t0);
6130
7496
  return parsed;
@@ -6135,18 +7501,18 @@ async function listAssignmentRecords(projectPath, traces) {
6135
7501
  return records;
6136
7502
  }
6137
7503
  async function listResources(projectPath) {
6138
- const resourcesDir = resolve13(projectPath, "resources");
7504
+ const resourcesDir = resolve14(projectPath, "resources");
6139
7505
  if (!await fileExists(resourcesDir)) {
6140
7506
  return [];
6141
7507
  }
6142
- const entries = await readdir7(resourcesDir, { withFileTypes: true });
7508
+ const entries = await readdir8(resourcesDir, { withFileTypes: true });
6143
7509
  const results = [];
6144
7510
  for (const entry of entries) {
6145
7511
  if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_")) {
6146
7512
  continue;
6147
7513
  }
6148
- const filePath = resolve13(resourcesDir, entry.name);
6149
- const content = await readFile10(filePath, "utf-8");
7514
+ const filePath = resolve14(resourcesDir, entry.name);
7515
+ const content = await readFile11(filePath, "utf-8");
6150
7516
  const parsed = parseResource(content);
6151
7517
  results.push({
6152
7518
  name: parsed.name,
@@ -6161,18 +7527,18 @@ async function listResources(projectPath) {
6161
7527
  return results;
6162
7528
  }
6163
7529
  async function listMemories(projectPath) {
6164
- const memoriesDir = resolve13(projectPath, "memories");
7530
+ const memoriesDir = resolve14(projectPath, "memories");
6165
7531
  if (!await fileExists(memoriesDir)) {
6166
7532
  return [];
6167
7533
  }
6168
- const entries = await readdir7(memoriesDir, { withFileTypes: true });
7534
+ const entries = await readdir8(memoriesDir, { withFileTypes: true });
6169
7535
  const results = [];
6170
7536
  for (const entry of entries) {
6171
7537
  if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_")) {
6172
7538
  continue;
6173
7539
  }
6174
- const filePath = resolve13(memoriesDir, entry.name);
6175
- const content = await readFile10(filePath, "utf-8");
7540
+ const filePath = resolve14(memoriesDir, entry.name);
7541
+ const content = await readFile11(filePath, "utf-8");
6176
7542
  const parsed = parseMemory(content);
6177
7543
  results.push({
6178
7544
  name: parsed.name,
@@ -6220,8 +7586,8 @@ async function listAllResources(projectsDir) {
6220
7586
  return all;
6221
7587
  }
6222
7588
  async function resolveProjectPath(projectsDir, projectSlug) {
6223
- const direct = resolve13(projectsDir, projectSlug);
6224
- if (await fileExists(resolve13(direct, "project.md"))) return direct;
7589
+ const direct = resolve14(projectsDir, projectSlug);
7590
+ if (await fileExists(resolve14(direct, "project.md"))) return direct;
6225
7591
  const records = await listProjectRecords(projectsDir);
6226
7592
  const match = records.find((r) => r.summary.slug === projectSlug);
6227
7593
  return match ? match.projectPath : null;
@@ -6233,9 +7599,9 @@ async function getMemoryDetail(projectsDir, projectSlug, itemSlug) {
6233
7599
  (p) => basename(p.projectPath) === projectSlug || p.summary.slug === projectSlug
6234
7600
  );
6235
7601
  if (!projectRecord) return null;
6236
- const filePath = resolve13(projectRecord.projectPath, "memories", `${itemSlug}.md`);
7602
+ const filePath = resolve14(projectRecord.projectPath, "memories", `${itemSlug}.md`);
6237
7603
  if (!await fileExists(filePath)) return null;
6238
- const content = await readFile10(filePath, "utf-8");
7604
+ const content = await readFile11(filePath, "utf-8");
6239
7605
  const parsed = parseMemory(content);
6240
7606
  return {
6241
7607
  name: parsed.name,
@@ -6259,9 +7625,9 @@ async function getResourceDetail(projectsDir, projectSlug, itemSlug) {
6259
7625
  (p) => basename(p.projectPath) === projectSlug || p.summary.slug === projectSlug
6260
7626
  );
6261
7627
  if (!projectRecord) return null;
6262
- const filePath = resolve13(projectRecord.projectPath, "resources", `${itemSlug}.md`);
7628
+ const filePath = resolve14(projectRecord.projectPath, "resources", `${itemSlug}.md`);
6263
7629
  if (!await fileExists(filePath)) return null;
6264
- const content = await readFile10(filePath, "utf-8");
7630
+ const content = await readFile11(filePath, "utf-8");
6265
7631
  const parsed = parseResource(content);
6266
7632
  return {
6267
7633
  name: parsed.name,
@@ -6277,9 +7643,9 @@ async function getResourceDetail(projectsDir, projectSlug, itemSlug) {
6277
7643
  };
6278
7644
  }
6279
7645
  async function loadDependencyGraph(projectPath, assignments) {
6280
- const statusPath = resolve13(projectPath, "_status.md");
7646
+ const statusPath = resolve14(projectPath, "_status.md");
6281
7647
  if (await fileExists(statusPath)) {
6282
- const statusContent = await readFile10(statusPath, "utf-8");
7648
+ const statusContent = await readFile11(statusPath, "utf-8");
6283
7649
  const parsed = parseStatus(statusContent);
6284
7650
  const derivedGraph = extractMermaidGraph(parsed.body);
6285
7651
  if (derivedGraph) {
@@ -6329,7 +7695,79 @@ async function buildProjectRollup(projectPath, project, assignments, traces) {
6329
7695
  }
6330
7696
  return { progress, needsAttention, status };
6331
7697
  }
6332
- function toAssignmentSummary(assignment) {
7698
+ function deriveStatusVirtuals(assignment, terminalStatuses) {
7699
+ const hist = assignment.statusHistory ?? [];
7700
+ let completedAt = null;
7701
+ if (terminalStatuses.has(assignment.status)) {
7702
+ for (const entry of hist) {
7703
+ if (entry.to === assignment.status) completedAt = entry.at;
7704
+ }
7705
+ }
7706
+ let statusAge = null;
7707
+ for (let i = hist.length - 1; i >= 0; i--) {
7708
+ const entry = hist[i];
7709
+ if (entry.from !== entry.to || entry.from === null) {
7710
+ const t = Date.parse(entry.at);
7711
+ statusAge = Number.isNaN(t) ? null : Date.now() - t;
7712
+ break;
7713
+ }
7714
+ }
7715
+ let phaseAge = null;
7716
+ for (let i = hist.length - 1; i >= 0; i--) {
7717
+ const entry = hist[i];
7718
+ if (entry.phaseTo !== void 0 && entry.phaseFrom !== entry.phaseTo) {
7719
+ const t = Date.parse(entry.at);
7720
+ phaseAge = Number.isNaN(t) ? null : Date.now() - t;
7721
+ break;
7722
+ }
7723
+ }
7724
+ return {
7725
+ completedAt,
7726
+ statusAge,
7727
+ phaseAge,
7728
+ phase: assignment.phase,
7729
+ disposition: assignment.disposition,
7730
+ pinned: assignment.override !== null
7731
+ };
7732
+ }
7733
+ async function buildDerivedDetail(assignment, assignmentDir, projectDir) {
7734
+ const config = await getStatusConfig();
7735
+ if (config.terminalStatuses.has(assignment.status)) return null;
7736
+ try {
7737
+ const { computeFacts: computeFacts2 } = await Promise.resolve().then(() => (init_facts(), facts_exports));
7738
+ const { deriveDimensions: deriveDimensions2 } = await Promise.resolve().then(() => (init_derive(), derive_exports));
7739
+ const { DEFAULT_DERIVE_CONFIG: DEFAULT_DERIVE_CONFIG2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
7740
+ const facts = await computeFacts2({
7741
+ assignmentDir,
7742
+ frontmatter: {
7743
+ ...assignment
7744
+ // AssignmentRecord ⊃ the fields computeFacts reads; statusHistory and
7745
+ // derived caches ride along untouched.
7746
+ },
7747
+ body: assignment.body,
7748
+ projectDir,
7749
+ terminalStatuses: config.terminalStatuses
7750
+ });
7751
+ const dims = deriveDimensions2({
7752
+ facts,
7753
+ derive: config.derive ?? DEFAULT_DERIVE_CONFIG2,
7754
+ currentStatus: assignment.status,
7755
+ terminalStatuses: config.terminalStatuses,
7756
+ knownStatusIds: new Set(config.statuses.map((s) => s.id)),
7757
+ override: assignment.override
7758
+ });
7759
+ if (!dims) return null;
7760
+ return {
7761
+ derivedStatus: dims.derivedStatus,
7762
+ nextAction: dims.nextAction,
7763
+ facts
7764
+ };
7765
+ } catch (err) {
7766
+ console.warn(`buildDerivedDetail failed for ${assignmentDir}:`, err);
7767
+ return null;
7768
+ }
7769
+ }
7770
+ function toAssignmentSummary(assignment, terminalStatuses) {
6333
7771
  return {
6334
7772
  id: assignment.id,
6335
7773
  slug: assignment.slug,
@@ -6345,12 +7783,14 @@ function toAssignmentSummary(assignment) {
6345
7783
  updated: assignment.updated,
6346
7784
  archived: assignment.archived,
6347
7785
  archivedAt: assignment.archivedAt,
6348
- archivedReason: assignment.archivedReason
7786
+ archivedReason: assignment.archivedReason,
7787
+ ...deriveStatusVirtuals(assignment, terminalStatuses)
6349
7788
  };
6350
7789
  }
6351
7790
  async function toAssignmentBoardItem(projectsDir, projectRecord, assignment) {
7791
+ const { terminalStatuses } = await getStatusConfig();
6352
7792
  return {
6353
- ...toAssignmentSummary(assignment),
7793
+ ...toAssignmentSummary(assignment, terminalStatuses),
6354
7794
  projectSlug: projectRecord.summary.slug,
6355
7795
  projectTitle: projectRecord.summary.title,
6356
7796
  blockedReason: assignment.blockedReason,
@@ -6393,7 +7833,7 @@ async function getAvailableTransitions(projectsDir, projectSlug, assignmentSlug,
6393
7833
  const config = await getStatusConfig();
6394
7834
  const transitionDefs = getTransitionDefinitions(config);
6395
7835
  const actions = [];
6396
- const projectPath = resolve13(projectsDir, projectSlug);
7836
+ const projectPath = resolve14(projectsDir, projectSlug);
6397
7837
  const traces = options?.traces;
6398
7838
  for (const definition of transitionDefs) {
6399
7839
  const target = getTargetStatus(assignment.status, definition.command, config.transitionTable);
@@ -6441,12 +7881,12 @@ async function getUnmetDependencies(projectPath, dependsOn, terminalStatuses, de
6441
7881
  continue;
6442
7882
  }
6443
7883
  }
6444
- const dependencyPath = resolve13(projectPath, "assignments", dependency, "assignment.md");
7884
+ const dependencyPath = resolve14(projectPath, "assignments", dependency, "assignment.md");
6445
7885
  if (!await fileExists(dependencyPath)) {
6446
7886
  unmet.push(`${dependency} (missing)`);
6447
7887
  continue;
6448
7888
  }
6449
- const content = await readFile10(dependencyPath, "utf-8");
7889
+ const content = await readFile11(dependencyPath, "utf-8");
6450
7890
  const parsed = parseAssignmentFull(content);
6451
7891
  if (!terminals.has(parsed.status)) {
6452
7892
  unmet.push(`${dependency} (${parsed.status})`);
@@ -6731,7 +8171,7 @@ function isStale(updated) {
6731
8171
  return Date.now() - timestamp > STALE_ASSIGNMENT_MS;
6732
8172
  }
6733
8173
  async function countOpenQuestions(projectPath, assignmentSlug) {
6734
- const commentsPath = resolve13(
8174
+ const commentsPath = resolve14(
6735
8175
  projectPath,
6736
8176
  "assignments",
6737
8177
  assignmentSlug,
@@ -6741,7 +8181,7 @@ async function countOpenQuestions(projectPath, assignmentSlug) {
6741
8181
  return 0;
6742
8182
  }
6743
8183
  try {
6744
- const content = await readFile10(commentsPath, "utf-8");
8184
+ const content = await readFile11(commentsPath, "utf-8");
6745
8185
  const parsed = parseComments(content);
6746
8186
  return parsed.entries.filter(
6747
8187
  (e) => e.type === "question" && e.resolved !== true
@@ -6762,21 +8202,21 @@ function getProjectActivityTimestamp(projectUpdated, assignments) {
6762
8202
  function getDocumentPath(projectsDir, documentType, projectSlug, assignmentSlug) {
6763
8203
  switch (documentType) {
6764
8204
  case "project":
6765
- return resolve13(projectsDir, projectSlug, "project.md");
8205
+ return resolve14(projectsDir, projectSlug, "project.md");
6766
8206
  case "assignment":
6767
- return assignmentSlug ? resolve13(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md") : null;
8207
+ return assignmentSlug ? resolve14(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md") : null;
6768
8208
  case "plan":
6769
- return assignmentSlug ? resolve13(projectsDir, projectSlug, "assignments", assignmentSlug, "plan.md") : null;
8209
+ return assignmentSlug ? resolve14(projectsDir, projectSlug, "assignments", assignmentSlug, "plan.md") : null;
6770
8210
  case "scratchpad":
6771
- return assignmentSlug ? resolve13(projectsDir, projectSlug, "assignments", assignmentSlug, "scratchpad.md") : null;
8211
+ return assignmentSlug ? resolve14(projectsDir, projectSlug, "assignments", assignmentSlug, "scratchpad.md") : null;
6772
8212
  case "handoff":
6773
- return assignmentSlug ? resolve13(projectsDir, projectSlug, "assignments", assignmentSlug, "handoff.md") : null;
8213
+ return assignmentSlug ? resolve14(projectsDir, projectSlug, "assignments", assignmentSlug, "handoff.md") : null;
6774
8214
  case "decision-record":
6775
- return assignmentSlug ? resolve13(projectsDir, projectSlug, "assignments", assignmentSlug, "decision-record.md") : null;
8215
+ return assignmentSlug ? resolve14(projectsDir, projectSlug, "assignments", assignmentSlug, "decision-record.md") : null;
6776
8216
  case "memory":
6777
- return assignmentSlug ? resolve13(projectsDir, projectSlug, "memories", `${assignmentSlug}.md`) : null;
8217
+ return assignmentSlug ? resolve14(projectsDir, projectSlug, "memories", `${assignmentSlug}.md`) : null;
6778
8218
  case "resource":
6779
- return assignmentSlug ? resolve13(projectsDir, projectSlug, "resources", `${assignmentSlug}.md`) : null;
8219
+ return assignmentSlug ? resolve14(projectsDir, projectSlug, "resources", `${assignmentSlug}.md`) : null;
6780
8220
  default:
6781
8221
  return null;
6782
8222
  }
@@ -6809,12 +8249,12 @@ async function listPlaybooks(playbooksDir2) {
6809
8249
  if (!await fileExists(playbooksDir2)) return [];
6810
8250
  const config = await readConfig();
6811
8251
  const disabledSet = new Set(config.playbooks.disabled);
6812
- const entries = await readdir7(playbooksDir2, { withFileTypes: true });
8252
+ const entries = await readdir8(playbooksDir2, { withFileTypes: true });
6813
8253
  const playbooks = [];
6814
8254
  for (const entry of entries) {
6815
8255
  if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name.startsWith("_") || entry.name === "manifest.md") continue;
6816
- const filePath = resolve13(playbooksDir2, entry.name);
6817
- const raw2 = await readFile10(filePath, "utf-8");
8256
+ const filePath = resolve14(playbooksDir2, entry.name);
8257
+ const raw2 = await readFile11(filePath, "utf-8");
6818
8258
  const parsed = parsePlaybook(raw2);
6819
8259
  const slug = parsed.slug || entry.name.replace(/\.md$/, "");
6820
8260
  playbooks.push({
@@ -6977,6 +8417,244 @@ var init_api = __esm({
6977
8417
  }
6978
8418
  });
6979
8419
 
8420
+ // src/lifecycle/recompute.ts
8421
+ var recompute_exports = {};
8422
+ __export(recompute_exports, {
8423
+ isDeriveMigrated: () => isDeriveMigrated,
8424
+ markDeriveMigrated: () => markDeriveMigrated,
8425
+ recomputeAll: () => recomputeAll,
8426
+ recomputeAndWrite: () => recomputeAndWrite,
8427
+ recomputeDependents: () => recomputeDependents,
8428
+ resolveDeriveContext: () => resolveDeriveContext
8429
+ });
8430
+ import { createHash as createHash2 } from "crypto";
8431
+ import { open, readdir as readdir10, readFile as readFile16, unlink as unlink5, stat } from "fs/promises";
8432
+ import { dirname as dirname4, resolve as resolve19 } from "path";
8433
+ async function isDeriveMigrated() {
8434
+ return fileExists(resolve19(syntaurRoot(), MIGRATION_MARKER));
8435
+ }
8436
+ async function markDeriveMigrated() {
8437
+ await writeFileForce(resolve19(syntaurRoot(), MIGRATION_MARKER), `${nowTimestamp()}
8438
+ `);
8439
+ }
8440
+ async function resolveDeriveContext() {
8441
+ const config = await readConfig();
8442
+ const statusConfig = config.statuses ?? buildDefaultStatusConfig();
8443
+ const terminal = new Set(statusConfig.statuses.filter((s) => s.terminal).map((s) => s.id));
8444
+ return {
8445
+ derive: config.statuses?.derive ?? DEFAULT_DERIVE_CONFIG,
8446
+ terminalStatuses: terminal.size > 0 ? terminal : DEFAULT_TERMINAL_STATUSES,
8447
+ knownStatusIds: new Set(statusConfig.statuses.map((s) => s.id))
8448
+ };
8449
+ }
8450
+ async function acquireLock(assignmentDir) {
8451
+ const lockPath = resolve19(assignmentDir, LOCK_FILE);
8452
+ const token = `${process.pid}:${createHash2("sha256").update(`${Math.random()}${Date.now()}`).digest("hex").slice(0, 12)}`;
8453
+ for (let attempt = 0; attempt <= LOCK_MAX_WAITS; attempt++) {
8454
+ try {
8455
+ const handle = await open(lockPath, "wx");
8456
+ await handle.writeFile(token, "utf-8");
8457
+ await handle.close();
8458
+ return async () => {
8459
+ try {
8460
+ const current = await readFile16(lockPath, "utf-8");
8461
+ if (current === token) await unlink5(lockPath);
8462
+ } catch {
8463
+ }
8464
+ };
8465
+ } catch (err) {
8466
+ const code = err.code;
8467
+ if (code !== "EEXIST") throw err;
8468
+ try {
8469
+ const info = await stat(lockPath);
8470
+ if (Date.now() - info.mtimeMs > LOCK_STALE_MS) {
8471
+ await unlink5(lockPath).catch(() => {
8472
+ });
8473
+ continue;
8474
+ }
8475
+ } catch {
8476
+ continue;
8477
+ }
8478
+ await new Promise((r) => setTimeout(r, LOCK_WAIT_MS));
8479
+ }
8480
+ }
8481
+ throw new Error(`Timed out waiting for ${lockPath} (held > ${LOCK_WAIT_MS * LOCK_MAX_WAITS / 1e3}s)`);
8482
+ }
8483
+ function contentHash(content) {
8484
+ return createHash2("sha256").update(content, "utf-8").digest("hex");
8485
+ }
8486
+ function extractBody(content) {
8487
+ const m = content.match(/^---\n[\s\S]*?\n---/);
8488
+ return m ? content.slice(m[0].length) : content;
8489
+ }
8490
+ async function recomputeAndWrite(assignmentPath, opts) {
8491
+ const assignmentDir = dirname4(assignmentPath);
8492
+ const release = await acquireLock(assignmentDir);
8493
+ try {
8494
+ for (let attempt = 0; attempt < CAS_RETRIES; attempt++) {
8495
+ const original = await readFile16(assignmentPath, "utf-8");
8496
+ const hash = contentHash(original);
8497
+ const originalFm = parseAssignmentFrontmatter(original);
8498
+ if (opts.context.terminalStatuses.has(originalFm.status)) {
8499
+ return { changed: false, status: originalFm.status, dimensions: null, deferredTerminal: true };
8500
+ }
8501
+ const content = opts.mutate ? await opts.mutate(original) : original;
8502
+ const mutated = content !== original;
8503
+ const frontmatter2 = mutated ? parseAssignmentFrontmatter(content) : originalFm;
8504
+ const facts = await computeFacts({
8505
+ assignmentDir,
8506
+ frontmatter: frontmatter2,
8507
+ body: extractBody(content),
8508
+ projectDir: opts.projectDir,
8509
+ terminalStatuses: opts.context.terminalStatuses
8510
+ });
8511
+ const dims = deriveDimensions({
8512
+ facts,
8513
+ derive: opts.context.derive,
8514
+ currentStatus: frontmatter2.status,
8515
+ terminalStatuses: opts.context.terminalStatuses,
8516
+ knownStatusIds: opts.context.knownStatusIds,
8517
+ override: frontmatter2.override
8518
+ });
8519
+ if (dims === null) {
8520
+ return { changed: false, status: frontmatter2.status, dimensions: null, deferredTerminal: true };
8521
+ }
8522
+ const statusChanged = dims.status !== frontmatter2.status;
8523
+ const phaseChanged = dims.phase !== frontmatter2.phase;
8524
+ const dispositionChanged = dims.disposition !== frontmatter2.disposition;
8525
+ if (!statusChanged && !phaseChanged && !dispositionChanged) {
8526
+ if (mutated) {
8527
+ const current2 = await readFile16(assignmentPath, "utf-8");
8528
+ if (contentHash(current2) !== hash) continue;
8529
+ await writeFileForce(assignmentPath, content);
8530
+ return { changed: true, status: frontmatter2.status, dimensions: dims, deferredTerminal: false };
8531
+ }
8532
+ return { changed: false, status: frontmatter2.status, dimensions: dims, deferredTerminal: false };
8533
+ }
8534
+ const at = nowTimestamp();
8535
+ let next = updateAssignmentFile(content, {
8536
+ status: dims.status,
8537
+ phase: dims.phase,
8538
+ disposition: dims.disposition,
8539
+ updated: at
8540
+ });
8541
+ next = appendStatusHistoryEntry(next, {
8542
+ at,
8543
+ from: frontmatter2.status,
8544
+ to: dims.status,
8545
+ command: opts.cause,
8546
+ by: opts.by,
8547
+ ...opts.reason !== void 0 ? { reason: opts.reason } : {},
8548
+ ...phaseChanged ? { phaseFrom: frontmatter2.phase, phaseTo: dims.phase } : {},
8549
+ ...dispositionChanged ? { dispositionFrom: frontmatter2.disposition, dispositionTo: dims.disposition } : {}
8550
+ });
8551
+ const current = await readFile16(assignmentPath, "utf-8");
8552
+ if (contentHash(current) !== hash) {
8553
+ continue;
8554
+ }
8555
+ await writeFileForce(assignmentPath, next);
8556
+ return { changed: true, status: dims.status, dimensions: dims, deferredTerminal: false };
8557
+ }
8558
+ const frontmatter = parseAssignmentFrontmatter(await readFile16(assignmentPath, "utf-8"));
8559
+ return {
8560
+ changed: false,
8561
+ status: frontmatter.status,
8562
+ dimensions: null,
8563
+ deferredTerminal: false,
8564
+ warning: `recompute skipped after ${CAS_RETRIES} concurrent-edit retries: ${assignmentPath}`
8565
+ };
8566
+ } finally {
8567
+ await release();
8568
+ }
8569
+ }
8570
+ async function recomputeDependents(projectDir, changedSlug, opts) {
8571
+ const assignmentsDir2 = resolve19(projectDir, "assignments");
8572
+ let entries;
8573
+ try {
8574
+ entries = await readdir10(assignmentsDir2);
8575
+ } catch {
8576
+ return [];
8577
+ }
8578
+ const results = [];
8579
+ for (const slug of entries) {
8580
+ if (slug === changedSlug) continue;
8581
+ const path = resolve19(assignmentsDir2, slug, "assignment.md");
8582
+ if (!await fileExists(path)) continue;
8583
+ try {
8584
+ const fm = parseAssignmentFrontmatter(await readFile16(path, "utf-8"));
8585
+ if (!fm.dependsOn.includes(changedSlug)) continue;
8586
+ results.push(await recomputeAndWrite(path, { ...opts, projectDir }));
8587
+ } catch {
8588
+ }
8589
+ }
8590
+ return results;
8591
+ }
8592
+ async function recomputeAll(projectsDir, standaloneDir, opts) {
8593
+ const summary = { scanned: 0, changed: 0, deferredTerminal: 0, warnings: [] };
8594
+ const sweepOne = async (path, projectDir) => {
8595
+ summary.scanned++;
8596
+ try {
8597
+ const result = await recomputeAndWrite(path, { ...opts, projectDir });
8598
+ if (result.changed) summary.changed++;
8599
+ if (result.deferredTerminal) summary.deferredTerminal++;
8600
+ if (result.warning) summary.warnings.push(result.warning);
8601
+ } catch (err) {
8602
+ summary.warnings.push(`${path}: ${err instanceof Error ? err.message : String(err)}`);
8603
+ }
8604
+ };
8605
+ let projects = [];
8606
+ try {
8607
+ projects = await readdir10(projectsDir);
8608
+ } catch {
8609
+ }
8610
+ for (const project of projects) {
8611
+ const projectDir = resolve19(projectsDir, project);
8612
+ const assignmentsDir2 = resolve19(projectDir, "assignments");
8613
+ let slugs = [];
8614
+ try {
8615
+ slugs = await readdir10(assignmentsDir2);
8616
+ } catch {
8617
+ continue;
8618
+ }
8619
+ for (const slug of slugs) {
8620
+ const path = resolve19(assignmentsDir2, slug, "assignment.md");
8621
+ if (await fileExists(path)) await sweepOne(path, projectDir);
8622
+ }
8623
+ }
8624
+ if (standaloneDir) {
8625
+ let ids = [];
8626
+ try {
8627
+ ids = await readdir10(standaloneDir);
8628
+ } catch {
8629
+ }
8630
+ for (const id of ids) {
8631
+ const path = resolve19(standaloneDir, id, "assignment.md");
8632
+ if (await fileExists(path)) await sweepOne(path, null);
8633
+ }
8634
+ }
8635
+ return summary;
8636
+ }
8637
+ var LOCK_FILE, LOCK_STALE_MS, LOCK_WAIT_MS, LOCK_MAX_WAITS, CAS_RETRIES, MIGRATION_MARKER;
8638
+ var init_recompute = __esm({
8639
+ "src/lifecycle/recompute.ts"() {
8640
+ "use strict";
8641
+ init_config2();
8642
+ init_fs();
8643
+ init_paths();
8644
+ init_timestamp();
8645
+ init_facts();
8646
+ init_derive();
8647
+ init_frontmatter();
8648
+ init_types();
8649
+ LOCK_FILE = ".derive.lock";
8650
+ LOCK_STALE_MS = 3e4;
8651
+ LOCK_WAIT_MS = 50;
8652
+ LOCK_MAX_WAITS = 100;
8653
+ CAS_RETRIES = 3;
8654
+ MIGRATION_MARKER = "derive-migrated";
8655
+ }
8656
+ });
8657
+
6980
8658
  // src/utils/assignment-todos.ts
6981
8659
  var assignment_todos_exports = {};
6982
8660
  __export(assignment_todos_exports, {
@@ -7031,8 +8709,8 @@ init_assignment_resolver();
7031
8709
  init_agent_sessions();
7032
8710
  import express from "express";
7033
8711
  import { createServer } from "http";
7034
- import { resolve as resolve31 } from "path";
7035
- import { writeFile as writeFile8, unlink as unlink7 } from "fs/promises";
8712
+ import { resolve as resolve33 } from "path";
8713
+ import { writeFile as writeFile8, unlink as unlink8 } from "fs/promises";
7036
8714
  import { WebSocketServer, WebSocket } from "ws";
7037
8715
 
7038
8716
  // src/dashboard/session-liveness.ts
@@ -7131,7 +8809,19 @@ function ignoreDotSegmentsBelow(root, pathApi = defaultPathApi) {
7131
8809
  };
7132
8810
  }
7133
8811
  function createWatcher(options) {
7134
- const { projectsDir, assignmentsDir: assignmentsDir2, serversDir: serversDir2, playbooksDir: playbooksDir2, todosDir: todosDir2, dbPath, onMessage, debounceMs = 300 } = options;
8812
+ const {
8813
+ projectsDir,
8814
+ assignmentsDir: assignmentsDir2,
8815
+ serversDir: serversDir2,
8816
+ playbooksDir: playbooksDir2,
8817
+ todosDir: todosDir2,
8818
+ dbPath,
8819
+ configPath,
8820
+ onMessage,
8821
+ onAssignmentChanged,
8822
+ onConfigChanged,
8823
+ debounceMs = 300
8824
+ } = options;
7135
8825
  const pendingEvents = /* @__PURE__ */ new Map();
7136
8826
  const projectsWatcher = watch(projectsDir, {
7137
8827
  ignoreInitial: true,
@@ -7170,6 +8860,9 @@ function createWatcher(options) {
7170
8860
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
7171
8861
  };
7172
8862
  onMessage(message);
8863
+ if (assignmentSlug && onAssignmentChanged) {
8864
+ onAssignmentChanged(projectSlug, assignmentSlug);
8865
+ }
7173
8866
  }, debounceMs)
7174
8867
  );
7175
8868
  }
@@ -7198,6 +8891,7 @@ function createWatcher(options) {
7198
8891
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
7199
8892
  };
7200
8893
  onMessage(message);
8894
+ if (onAssignmentChanged) onAssignmentChanged(null, assignmentId);
7201
8895
  }, debounceMs)
7202
8896
  );
7203
8897
  };
@@ -7331,6 +9025,29 @@ function createWatcher(options) {
7331
9025
  leasesDbWatcher.on("add", handleDbChange2);
7332
9026
  leasesDbWatcher.on("unlink", handleDbChange2);
7333
9027
  }
9028
+ let configWatcher = null;
9029
+ if (configPath && onConfigChanged) {
9030
+ let handleConfigChange2 = function() {
9031
+ const debounceKey = "__config__";
9032
+ const existing = pendingEvents.get(debounceKey);
9033
+ if (existing) clearTimeout(existing);
9034
+ pendingEvents.set(
9035
+ debounceKey,
9036
+ setTimeout(() => {
9037
+ pendingEvents.delete(debounceKey);
9038
+ onConfigChanged();
9039
+ }, debounceMs)
9040
+ );
9041
+ };
9042
+ var handleConfigChange = handleConfigChange2;
9043
+ configWatcher = watch(configPath, {
9044
+ ignoreInitial: true,
9045
+ persistent: true,
9046
+ depth: 0
9047
+ });
9048
+ configWatcher.on("change", handleConfigChange2);
9049
+ configWatcher.on("add", handleConfigChange2);
9050
+ }
7334
9051
  return {
7335
9052
  close: async () => {
7336
9053
  pendingEvents.forEach((timeout) => {
@@ -7343,6 +9060,7 @@ function createWatcher(options) {
7343
9060
  if (playbooksWatcher) await playbooksWatcher.close();
7344
9061
  if (todosWatcher) await todosWatcher.close();
7345
9062
  if (leasesDbWatcher) await leasesDbWatcher.close();
9063
+ if (configWatcher) await configWatcher.close();
7346
9064
  }
7347
9065
  };
7348
9066
  }
@@ -7496,11 +9214,11 @@ function mergePatch(current, patch) {
7496
9214
  // src/utils/view-prefs.ts
7497
9215
  init_paths();
7498
9216
  init_fs();
7499
- import { readFile as readFile11, rename as rename3, unlink as unlink3 } from "fs/promises";
7500
- import { resolve as resolve14 } from "path";
9217
+ import { readFile as readFile12, rename as rename3, unlink as unlink3 } from "fs/promises";
9218
+ import { resolve as resolve15 } from "path";
7501
9219
  function corruptFilePath() {
7502
9220
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7503
- return resolve14(syntaurRoot(), `view-prefs.corrupt-${ts}.json`);
9221
+ return resolve15(syntaurRoot(), `view-prefs.corrupt-${ts}.json`);
7504
9222
  }
7505
9223
  function isViewPrefsFileShape(value) {
7506
9224
  if (!value || typeof value !== "object") return false;
@@ -7519,7 +9237,7 @@ async function readViewPrefsFile() {
7519
9237
  }
7520
9238
  let raw2;
7521
9239
  try {
7522
- raw2 = await readFile11(path, "utf-8");
9240
+ raw2 = await readFile12(path, "utf-8");
7523
9241
  } catch {
7524
9242
  return { ...DEFAULT_VIEW_PREFS_FILE };
7525
9243
  }
@@ -7707,12 +9425,12 @@ function isDashboardSlot(value) {
7707
9425
  // src/utils/saved-views.ts
7708
9426
  init_paths();
7709
9427
  init_fs();
7710
- import { readFile as readFile12, rename as rename4, unlink as unlink4 } from "fs/promises";
9428
+ import { readFile as readFile13, rename as rename4, unlink as unlink4 } from "fs/promises";
7711
9429
  import { randomUUID } from "crypto";
7712
- import { resolve as resolve15 } from "path";
9430
+ import { resolve as resolve16 } from "path";
7713
9431
  function corruptFilePath2() {
7714
9432
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7715
- return resolve15(syntaurRoot(), `saved-views.corrupt-${ts}.json`);
9433
+ return resolve16(syntaurRoot(), `saved-views.corrupt-${ts}.json`);
7716
9434
  }
7717
9435
  function isSavedViewsFileShape(value) {
7718
9436
  if (!value || typeof value !== "object") return false;
@@ -7737,7 +9455,7 @@ async function readSavedViewsFile() {
7737
9455
  }
7738
9456
  let raw2;
7739
9457
  try {
7740
- raw2 = await readFile12(path, "utf-8");
9458
+ raw2 = await readFile13(path, "utf-8");
7741
9459
  } catch {
7742
9460
  return cloneDefault();
7743
9461
  }
@@ -8053,10 +9771,11 @@ function createDashboardLayoutRouter() {
8053
9771
 
8054
9772
  // src/dashboard/api-write.ts
8055
9773
  init_lifecycle();
9774
+ init_frontmatter();
8056
9775
  init_slug();
8057
9776
  import { Router as Router2 } from "express";
8058
- import { resolve as resolve18, basename as basename3, isAbsolute as isAbsolute4 } from "path";
8059
- import { rm, readFile as readFile15, open as fsOpen, stat as fsStat, realpath as fsRealpath } from "fs/promises";
9777
+ import { resolve as resolve20, basename as basename3, isAbsolute as isAbsolute4 } from "path";
9778
+ import { rm, readFile as readFile17, open as fsOpen, stat as fsStat, realpath as fsRealpath } from "fs/promises";
8060
9779
  import { spawnSync as spawnSync3 } from "child_process";
8061
9780
 
8062
9781
  // src/utils/uuid.ts
@@ -8073,7 +9792,7 @@ init_fs();
8073
9792
  init_frontmatter();
8074
9793
  init_fs();
8075
9794
  import { spawn } from "child_process";
8076
- import { readFile as readFile13 } from "fs/promises";
9795
+ import { readFile as readFile14 } from "fs/promises";
8077
9796
 
8078
9797
  // src/launch/cwd.ts
8079
9798
  import { existsSync, statSync as statSync2 } from "fs";
@@ -8276,7 +9995,7 @@ async function createWorktreeAndRecord(opts) {
8276
9995
  const { assignmentPath, repository, branch, worktreePath, parentBranch } = opts;
8277
9996
  await createWorktree({ repository, branch, worktreePath, parentBranch });
8278
9997
  try {
8279
- const content = await readFile13(assignmentPath, "utf-8");
9998
+ const content = await readFile14(assignmentPath, "utf-8");
8280
9999
  const updated = updateAssignmentWorkspace(content, {
8281
10000
  repository,
8282
10001
  worktreePath,
@@ -8319,12 +10038,12 @@ function formatRollbackError(opts) {
8319
10038
  // src/utils/worktree-defaults.ts
8320
10039
  init_paths();
8321
10040
  import { spawnSync as spawnSync2 } from "child_process";
8322
- import { resolve as resolve16 } from "path";
10041
+ import { resolve as resolve17 } from "path";
8323
10042
  function computeWorktreeDefaults(opts) {
8324
10043
  const repository = opts.existing.repository ?? detectCurrentGitRoot(opts.cwd);
8325
10044
  const branch = opts.projectSlug ? `syntaur/${opts.projectSlug}/${opts.assignmentSlug}` : `syntaur/${opts.assignmentSlug}`;
8326
10045
  const parentBranch = opts.existing.parentBranch ?? detectCurrentBranch(opts.cwd) ?? "main";
8327
- const worktreeBase = repository ? resolve16(repository, ".worktrees", branch) : resolve16(
10046
+ const worktreeBase = repository ? resolve17(repository, ".worktrees", branch) : resolve17(
8328
10047
  syntaurRoot(),
8329
10048
  "worktrees",
8330
10049
  opts.projectSlug || "standalone",
@@ -8537,8 +10256,8 @@ function recreateOutcomeToHttp(outcome) {
8537
10256
  // src/dashboard/repository-candidates.ts
8538
10257
  init_fs();
8539
10258
  init_parser();
8540
- import { readdir as readdir8, readFile as readFile14 } from "fs/promises";
8541
- import { resolve as resolve17 } from "path";
10259
+ import { readdir as readdir9, readFile as readFile15 } from "fs/promises";
10260
+ import { resolve as resolve18 } from "path";
8542
10261
  function toSourceAssignment(parsed, fallbackId) {
8543
10262
  const repository = parsed.workspace.repository?.trim();
8544
10263
  const branch = parsed.workspace.branch?.trim();
@@ -8555,29 +10274,29 @@ function toSourceAssignment(parsed, fallbackId) {
8555
10274
  async function getProjectRepositoryCandidates(projectsDir, projectSlug) {
8556
10275
  const seen = /* @__PURE__ */ new Set();
8557
10276
  const out = [];
8558
- const projectPath = resolve17(projectsDir, projectSlug, "project.md");
10277
+ const projectPath = resolve18(projectsDir, projectSlug, "project.md");
8559
10278
  if (await fileExists(projectPath)) {
8560
- const project = parseProject(await readFile14(projectPath, "utf-8"));
10279
+ const project = parseProject(await readFile15(projectPath, "utf-8"));
8561
10280
  for (const raw2 of project.repositories) {
8562
10281
  const path = raw2.trim();
8563
10282
  if (!path) continue;
8564
- const abs = resolve17(path);
10283
+ const abs = resolve18(path);
8565
10284
  if (seen.has(abs)) continue;
8566
10285
  seen.add(abs);
8567
10286
  out.push({ path: abs, source: "project", sourceAssignmentSlug: null });
8568
10287
  }
8569
10288
  }
8570
- const assignmentsDir2 = resolve17(projectsDir, projectSlug, "assignments");
10289
+ const assignmentsDir2 = resolve18(projectsDir, projectSlug, "assignments");
8571
10290
  if (await fileExists(assignmentsDir2)) {
8572
- const entries = await readdir8(assignmentsDir2, { withFileTypes: true });
10291
+ const entries = await readdir9(assignmentsDir2, { withFileTypes: true });
8573
10292
  for (const entry of entries) {
8574
10293
  if (!entry.isDirectory()) continue;
8575
- const assignmentMd = resolve17(assignmentsDir2, entry.name, "assignment.md");
10294
+ const assignmentMd = resolve18(assignmentsDir2, entry.name, "assignment.md");
8576
10295
  if (!await fileExists(assignmentMd)) continue;
8577
- const parsed = parseAssignmentFull(await readFile14(assignmentMd, "utf-8"));
10296
+ const parsed = parseAssignmentFull(await readFile15(assignmentMd, "utf-8"));
8578
10297
  const repo = parsed.workspace.repository?.trim();
8579
10298
  if (!repo) continue;
8580
- const abs = resolve17(repo);
10299
+ const abs = resolve18(repo);
8581
10300
  if (seen.has(abs)) continue;
8582
10301
  seen.add(abs);
8583
10302
  out.push({ path: abs, source: "sibling", sourceAssignmentSlug: parsed.slug });
@@ -8591,16 +10310,16 @@ async function getStandaloneRepositoryCandidates(assignmentsDir2, excludeAssignm
8591
10310
  }
8592
10311
  const seen = /* @__PURE__ */ new Set();
8593
10312
  const out = [];
8594
- const entries = await readdir8(assignmentsDir2, { withFileTypes: true });
10313
+ const entries = await readdir9(assignmentsDir2, { withFileTypes: true });
8595
10314
  for (const entry of entries) {
8596
10315
  if (!entry.isDirectory()) continue;
8597
10316
  if (entry.name === excludeAssignmentId) continue;
8598
- const assignmentMd = resolve17(assignmentsDir2, entry.name, "assignment.md");
10317
+ const assignmentMd = resolve18(assignmentsDir2, entry.name, "assignment.md");
8599
10318
  if (!await fileExists(assignmentMd)) continue;
8600
- const parsed = parseAssignmentFull(await readFile14(assignmentMd, "utf-8"));
10319
+ const parsed = parseAssignmentFull(await readFile15(assignmentMd, "utf-8"));
8601
10320
  const repo = parsed.workspace.repository?.trim();
8602
10321
  if (!repo) continue;
8603
- const abs = resolve17(repo);
10322
+ const abs = resolve18(repo);
8604
10323
  if (seen.has(abs)) continue;
8605
10324
  seen.add(abs);
8606
10325
  out.push({ path: abs, source: "sibling", sourceAssignmentSlug: parsed.slug });
@@ -8608,17 +10327,17 @@ async function getStandaloneRepositoryCandidates(assignmentsDir2, excludeAssignm
8608
10327
  return out;
8609
10328
  }
8610
10329
  async function getProjectSourceAssignments(projectsDir, projectSlug, excludeSlug) {
8611
- const assignmentsDir2 = resolve17(projectsDir, projectSlug, "assignments");
10330
+ const assignmentsDir2 = resolve18(projectsDir, projectSlug, "assignments");
8612
10331
  if (!await fileExists(assignmentsDir2)) return [];
8613
10332
  const seen = /* @__PURE__ */ new Set();
8614
10333
  const out = [];
8615
- const entries = await readdir8(assignmentsDir2, { withFileTypes: true });
10334
+ const entries = await readdir9(assignmentsDir2, { withFileTypes: true });
8616
10335
  for (const entry of entries) {
8617
10336
  if (!entry.isDirectory()) continue;
8618
10337
  if (entry.name === excludeSlug) continue;
8619
- const assignmentMd = resolve17(assignmentsDir2, entry.name, "assignment.md");
10338
+ const assignmentMd = resolve18(assignmentsDir2, entry.name, "assignment.md");
8620
10339
  if (!await fileExists(assignmentMd)) continue;
8621
- const parsed = parseAssignmentFull(await readFile14(assignmentMd, "utf-8"));
10340
+ const parsed = parseAssignmentFull(await readFile15(assignmentMd, "utf-8"));
8622
10341
  const source = toSourceAssignment(parsed, entry.name);
8623
10342
  if (!source) continue;
8624
10343
  if (seen.has(entry.name)) continue;
@@ -8631,13 +10350,13 @@ async function getStandaloneSourceAssignments(assignmentsDir2, excludeAssignment
8631
10350
  if (!await fileExists(assignmentsDir2)) return [];
8632
10351
  const seen = /* @__PURE__ */ new Set();
8633
10352
  const out = [];
8634
- const entries = await readdir8(assignmentsDir2, { withFileTypes: true });
10353
+ const entries = await readdir9(assignmentsDir2, { withFileTypes: true });
8635
10354
  for (const entry of entries) {
8636
10355
  if (!entry.isDirectory()) continue;
8637
10356
  if (entry.name === excludeAssignmentId) continue;
8638
- const assignmentMd = resolve17(assignmentsDir2, entry.name, "assignment.md");
10357
+ const assignmentMd = resolve18(assignmentsDir2, entry.name, "assignment.md");
8639
10358
  if (!await fileExists(assignmentMd)) continue;
8640
- const parsed = parseAssignmentFull(await readFile14(assignmentMd, "utf-8"));
10359
+ const parsed = parseAssignmentFull(await readFile15(assignmentMd, "utf-8"));
8641
10360
  const source = toSourceAssignment(parsed, entry.name);
8642
10361
  if (!source) continue;
8643
10362
  if (seen.has(entry.name)) continue;
@@ -8785,6 +10504,7 @@ function renderAssignment(params2) {
8785
10504
  const workspaceGroupLine = params2.workspaceGroup ? `
8786
10505
  workspaceGroup: ${params2.workspaceGroup}` : "";
8787
10506
  const typeYaml = `type: ${params2.type ?? "feature"}`;
10507
+ const seedStatus = params2.status ?? "draft";
8788
10508
  const criteriaLines = params2.acceptanceCriteria && params2.acceptanceCriteria.length > 0 ? params2.acceptanceCriteria.map((c) => `- [ ] ${c.replace(/\n/g, " ").trim()}`).join("\n") : `- [ ] <!-- criterion 1 -->
8789
10509
  - [ ] <!-- criterion 2 -->
8790
10510
  - [ ] <!-- criterion 3 -->`;
@@ -8805,12 +10525,18 @@ slug: ${params2.slug}
8805
10525
  title: ${safeTitle}
8806
10526
  ${projectYaml}${workspaceGroupLine}
8807
10527
  ${typeYaml}
8808
- status: ${params2.status ?? "draft"}
10528
+ status: ${seedStatus}
8809
10529
  priority: ${params2.priority}
8810
10530
  created: "${params2.timestamp}"
8811
10531
  updated: "${params2.timestamp}"
8812
10532
  assignee: null
8813
10533
  externalIds: []
10534
+ statusHistory:
10535
+ - at: "${params2.timestamp}"
10536
+ from: null
10537
+ to: ${seedStatus}
10538
+ command: create
10539
+ by: null
8814
10540
  ${dependsOnYaml}
8815
10541
  ${linksYaml}
8816
10542
  blockedReason: null
@@ -9212,7 +10938,7 @@ async function readCurrentDocument(filePath) {
9212
10938
  if (!await fileExists(filePath)) {
9213
10939
  return null;
9214
10940
  }
9215
- return readFile15(filePath, "utf-8");
10941
+ return readFile17(filePath, "utf-8");
9216
10942
  }
9217
10943
  var worktreeInFlight = /* @__PURE__ */ new Set();
9218
10944
  async function assertRepoRoot(repoInput) {
@@ -9266,7 +10992,7 @@ async function handleWorktreeCreate(req, res, ctx) {
9266
10992
  }
9267
10993
  worktreeInFlight.add(ctx.assignmentPath);
9268
10994
  try {
9269
- const parsed = parseAssignmentFull(await readFile15(ctx.assignmentPath, "utf-8"));
10995
+ const parsed = parseAssignmentFull(await readFile17(ctx.assignmentPath, "utf-8"));
9270
10996
  if (parsed.workspace.worktreePath) {
9271
10997
  res.status(409).json({ error: "Worktree already configured for this assignment" });
9272
10998
  return;
@@ -9293,7 +11019,7 @@ async function handleWorktreeCreate(req, res, ctx) {
9293
11019
  });
9294
11020
  const branch = typeof bodyBranch === "string" && bodyBranch.trim() ? bodyBranch.trim() : defaults.branch;
9295
11021
  const parentBranch = typeof bodyParent === "string" && bodyParent.trim() ? bodyParent.trim() : defaults.parentBranch;
9296
- const worktreePath = resolve18(repo, ".worktrees", branch);
11022
+ const worktreePath = resolve20(repo, ".worktrees", branch);
9297
11023
  const refCheck = spawnSync3("git", ["-C", repo, "check-ref-format", "--branch", branch], {
9298
11024
  encoding: "utf-8"
9299
11025
  });
@@ -9355,6 +11081,11 @@ async function handleWorktreeCreate(req, res, ctx) {
9355
11081
  worktreeInFlight.delete(ctx.assignmentPath);
9356
11082
  }
9357
11083
  }
11084
+ function unambiguousCommandTarget(transitions, command) {
11085
+ const targets = new Set(transitions.filter((t) => t.command === command).map((t) => t.to));
11086
+ if (targets.size === 1) return [...targets][0];
11087
+ return void 0;
11088
+ }
9358
11089
  function createWriteRouter(projectsDir, assignmentsDir2, todosDir2) {
9359
11090
  const linkedTodosLookup = todosDir2 ? { todosDir: todosDir2, projectsDir } : void 0;
9360
11091
  const router = Router2();
@@ -9583,9 +11314,9 @@ ${body.startsWith("\n") ? body.slice(1) : body}${body.endsWith("\n") ? "" : "\n"
9583
11314
  });
9584
11315
  return;
9585
11316
  }
9586
- const folderPath = resolve18(projectDir, folder);
11317
+ const folderPath = resolve20(projectDir, folder);
9587
11318
  await ensureDir(folderPath);
9588
- const filePath = resolve18(folderPath, `${requestedSlug}.md`);
11319
+ const filePath = resolve20(folderPath, `${requestedSlug}.md`);
9589
11320
  const timestamp = nowTimestamp();
9590
11321
  let content = renderItemStub(kind, {
9591
11322
  slug: requestedSlug,
@@ -9629,14 +11360,14 @@ ${body.startsWith("\n") ? body.slice(1) : body}${body.endsWith("\n") ? "" : "\n"
9629
11360
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
9630
11361
  return;
9631
11362
  }
9632
- const filePath = resolve18(projectDir, folder, `${itemSlug}.md`);
11363
+ const filePath = resolve20(projectDir, folder, `${itemSlug}.md`);
9633
11364
  if (!await fileExists(filePath)) {
9634
11365
  res.status(404).json({ error: `${kind === "memory" ? "Memory" : "Resource"} not found` });
9635
11366
  return;
9636
11367
  }
9637
11368
  const nextContentRaw = requireContent(req, res);
9638
11369
  if (!nextContentRaw) return;
9639
- const currentContent = await readFile15(filePath, "utf-8");
11370
+ const currentContent = await readFile17(filePath, "utf-8");
9640
11371
  const frontmatterBlock = extractFrontmatterBlock(currentContent);
9641
11372
  if (!frontmatterBlock) {
9642
11373
  res.status(500).json({ error: `${kind} file is malformed (no frontmatter)` });
@@ -9665,7 +11396,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
9665
11396
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
9666
11397
  return;
9667
11398
  }
9668
- const filePath = resolve18(projectDir, folder, `${itemSlug}.md`);
11399
+ const filePath = resolve20(projectDir, folder, `${itemSlug}.md`);
9669
11400
  if (!await fileExists(filePath)) {
9670
11401
  res.status(404).json({ error: `${kind === "memory" ? "Memory" : "Resource"} not found` });
9671
11402
  return;
@@ -9699,26 +11430,26 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
9699
11430
  res.status(400).json({ error: `Invalid slug "${slug}". Must be lowercase and hyphen-separated.` });
9700
11431
  return;
9701
11432
  }
9702
- const projectDir = resolve18(projectsDir, slug);
11433
+ const projectDir = resolve20(projectsDir, slug);
9703
11434
  if (await fileExists(projectDir)) {
9704
11435
  res.status(409).json({ error: `Project "${slug}" already exists` });
9705
11436
  return;
9706
11437
  }
9707
11438
  const title = fields.title;
9708
11439
  const timestamp = fields.created || nowTimestamp();
9709
- await ensureDir(resolve18(projectDir, "assignments"));
9710
- await ensureDir(resolve18(projectDir, "resources"));
9711
- await ensureDir(resolve18(projectDir, "memories"));
9712
- await writeFileForce(resolve18(projectDir, "project.md"), content);
11440
+ await ensureDir(resolve20(projectDir, "assignments"));
11441
+ await ensureDir(resolve20(projectDir, "resources"));
11442
+ await ensureDir(resolve20(projectDir, "memories"));
11443
+ await writeFileForce(resolve20(projectDir, "project.md"), content);
9713
11444
  try {
9714
11445
  const companions = [
9715
- [resolve18(projectDir, "manifest.md"), renderManifest({ slug, timestamp })],
9716
- [resolve18(projectDir, "_index-assignments.md"), renderIndexAssignments({ slug, title, timestamp })],
9717
- [resolve18(projectDir, "_index-plans.md"), renderIndexPlans({ slug, title, timestamp })],
9718
- [resolve18(projectDir, "_index-decisions.md"), renderIndexDecisions({ slug, title, timestamp })],
9719
- [resolve18(projectDir, "_status.md"), renderStatus({ slug, title, timestamp })],
9720
- [resolve18(projectDir, "resources", "_index.md"), renderResourcesIndex({ slug, title, timestamp })],
9721
- [resolve18(projectDir, "memories", "_index.md"), renderMemoriesIndex({ slug, title, timestamp })]
11446
+ [resolve20(projectDir, "manifest.md"), renderManifest({ slug, timestamp })],
11447
+ [resolve20(projectDir, "_index-assignments.md"), renderIndexAssignments({ slug, title, timestamp })],
11448
+ [resolve20(projectDir, "_index-plans.md"), renderIndexPlans({ slug, title, timestamp })],
11449
+ [resolve20(projectDir, "_index-decisions.md"), renderIndexDecisions({ slug, title, timestamp })],
11450
+ [resolve20(projectDir, "_status.md"), renderStatus({ slug, title, timestamp })],
11451
+ [resolve20(projectDir, "resources", "_index.md"), renderResourcesIndex({ slug, title, timestamp })],
11452
+ [resolve20(projectDir, "memories", "_index.md"), renderMemoriesIndex({ slug, title, timestamp })]
9722
11453
  ];
9723
11454
  for (const [filePath, fileContent] of companions) {
9724
11455
  await writeFileForce(filePath, fileContent);
@@ -9739,8 +11470,8 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
9739
11470
  router.post("/api/projects/:slug/assignments", async (req, res) => {
9740
11471
  try {
9741
11472
  const projectSlug = getParam(req.params.slug);
9742
- const projectDir = resolve18(projectsDir, projectSlug);
9743
- const projectMdPath = resolve18(projectDir, "project.md");
11473
+ const projectDir = resolve20(projectsDir, projectSlug);
11474
+ const projectMdPath = resolve20(projectDir, "project.md");
9744
11475
  if (!await fileExists(projectMdPath)) {
9745
11476
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
9746
11477
  return;
@@ -9770,7 +11501,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
9770
11501
  res.status(400).json({ error: `Invalid priority "${priority}". Must be low, medium, high, or critical.` });
9771
11502
  return;
9772
11503
  }
9773
- const assignmentDir = resolve18(projectDir, "assignments", assignmentSlug);
11504
+ const assignmentDir = resolve20(projectDir, "assignments", assignmentSlug);
9774
11505
  if (await fileExists(assignmentDir)) {
9775
11506
  res.status(409).json({
9776
11507
  error: `Assignment "${assignmentSlug}" already exists in project "${projectSlug}"`
@@ -9779,12 +11510,19 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
9779
11510
  }
9780
11511
  const timestamp = fields.created || nowTimestamp();
9781
11512
  await ensureDir(assignmentDir);
9782
- await writeFileForce(resolve18(assignmentDir, "assignment.md"), content);
11513
+ const seededContent = parseAssignmentFull(content).statusHistory.length > 0 ? content : appendStatusHistoryEntry(content, {
11514
+ at: timestamp,
11515
+ from: null,
11516
+ to: parseAssignmentFull(content).status,
11517
+ command: "create",
11518
+ by: null
11519
+ });
11520
+ await writeFileForce(resolve20(assignmentDir, "assignment.md"), seededContent);
9783
11521
  try {
9784
11522
  const companions = [
9785
- [resolve18(assignmentDir, "scratchpad.md"), renderScratchpad({ assignmentSlug, timestamp })],
9786
- [resolve18(assignmentDir, "handoff.md"), renderHandoff({ assignmentSlug, timestamp })],
9787
- [resolve18(assignmentDir, "decision-record.md"), renderDecisionRecord({ assignmentSlug, timestamp })]
11523
+ [resolve20(assignmentDir, "scratchpad.md"), renderScratchpad({ assignmentSlug, timestamp })],
11524
+ [resolve20(assignmentDir, "handoff.md"), renderHandoff({ assignmentSlug, timestamp })],
11525
+ [resolve20(assignmentDir, "decision-record.md"), renderDecisionRecord({ assignmentSlug, timestamp })]
9788
11526
  ];
9789
11527
  for (const [filePath, fileContent] of companions) {
9790
11528
  await writeFileForce(filePath, fileContent);
@@ -9805,7 +11543,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
9805
11543
  router.patch("/api/projects/:slug", async (req, res) => {
9806
11544
  try {
9807
11545
  const projectSlug = getParam(req.params.slug);
9808
- const projectPath = resolve18(projectsDir, projectSlug, "project.md");
11546
+ const projectPath = resolve20(projectsDir, projectSlug, "project.md");
9809
11547
  const currentContent = await readCurrentDocument(projectPath);
9810
11548
  if (!currentContent) {
9811
11549
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
@@ -9838,7 +11576,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
9838
11576
  try {
9839
11577
  const projectSlug = getParam(req.params.slug);
9840
11578
  const assignmentSlug = getParam(req.params.aslug);
9841
- const assignmentPath = resolve18(
11579
+ const assignmentPath = resolve20(
9842
11580
  projectsDir,
9843
11581
  projectSlug,
9844
11582
  "assignments",
@@ -9865,10 +11603,20 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
9865
11603
  return;
9866
11604
  }
9867
11605
  let nextContent = nextContentRaw;
11606
+ const now = nowTimestamp();
9868
11607
  if (next.status !== current.status && current.status === "blocked" && next.status !== "blocked") {
9869
11608
  nextContent = setTopLevelField(nextContent, "blockedReason", null);
9870
11609
  }
9871
- nextContent = setTopLevelField(nextContent, "updated", nowTimestamp());
11610
+ nextContent = setTopLevelField(nextContent, "updated", now);
11611
+ if (next.status !== current.status) {
11612
+ nextContent = appendStatusHistoryEntry(nextContent, {
11613
+ at: now,
11614
+ from: current.status,
11615
+ to: next.status,
11616
+ command: "edit",
11617
+ by: null
11618
+ });
11619
+ }
9872
11620
  await writeFileForce(assignmentPath, nextContent);
9873
11621
  const assignment = await getAssignmentDetail(projectsDir, projectSlug, assignmentSlug);
9874
11622
  res.json({ assignment, content: nextContent });
@@ -9881,7 +11629,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
9881
11629
  try {
9882
11630
  const projectSlug = getParam(req.params.slug);
9883
11631
  const assignmentSlug = getParam(req.params.aslug);
9884
- const assignmentPath = resolve18(
11632
+ const assignmentPath = resolve20(
9885
11633
  projectsDir,
9886
11634
  projectSlug,
9887
11635
  "assignments",
@@ -9917,7 +11665,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
9917
11665
  try {
9918
11666
  const projectSlug = getParam(req.params.slug);
9919
11667
  const assignmentSlug = getParam(req.params.aslug);
9920
- const planPath = resolve18(
11668
+ const planPath = resolve20(
9921
11669
  projectsDir,
9922
11670
  projectSlug,
9923
11671
  "assignments",
@@ -9955,7 +11703,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
9955
11703
  try {
9956
11704
  const projectSlug = getParam(req.params.slug);
9957
11705
  const assignmentSlug = getParam(req.params.aslug);
9958
- const scratchpadPath = resolve18(
11706
+ const scratchpadPath = resolve20(
9959
11707
  projectsDir,
9960
11708
  projectSlug,
9961
11709
  "assignments",
@@ -9993,7 +11741,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
9993
11741
  try {
9994
11742
  const projectSlug = getParam(req.params.slug);
9995
11743
  const assignmentSlug = getParam(req.params.aslug);
9996
- const handoffPath = resolve18(
11744
+ const handoffPath = resolve20(
9997
11745
  projectsDir,
9998
11746
  projectSlug,
9999
11747
  "assignments",
@@ -10031,7 +11779,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
10031
11779
  try {
10032
11780
  const projectSlug = getParam(req.params.slug);
10033
11781
  const assignmentSlug = getParam(req.params.aslug);
10034
- const decisionPath = resolve18(
11782
+ const decisionPath = resolve20(
10035
11783
  projectsDir,
10036
11784
  projectSlug,
10037
11785
  "assignments",
@@ -10069,7 +11817,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
10069
11817
  try {
10070
11818
  const projectSlug = getParam(req.params.slug);
10071
11819
  const assignmentSlug = getParam(req.params.aslug);
10072
- const commentsPath = resolve18(
11820
+ const commentsPath = resolve20(
10073
11821
  projectsDir,
10074
11822
  projectSlug,
10075
11823
  "assignments",
@@ -10087,7 +11835,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
10087
11835
  let currentContent;
10088
11836
  let currentCount = 0;
10089
11837
  if (await fileExists(commentsPath)) {
10090
- currentContent = await readFile15(commentsPath, "utf-8");
11838
+ currentContent = await readFile17(commentsPath, "utf-8");
10091
11839
  const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
10092
11840
  if (countMatch) currentCount = parseInt(countMatch[1], 10);
10093
11841
  } else {
@@ -10128,7 +11876,7 @@ ${entry}`;
10128
11876
  const projectSlug = getParam(req.params.slug);
10129
11877
  const assignmentSlug = getParam(req.params.aslug);
10130
11878
  const commentId = getParam(req.params.commentId);
10131
- const commentsPath = resolve18(
11879
+ const commentsPath = resolve20(
10132
11880
  projectsDir,
10133
11881
  projectSlug,
10134
11882
  "assignments",
@@ -10144,7 +11892,7 @@ ${entry}`;
10144
11892
  res.status(400).json({ error: "resolved (boolean) is required" });
10145
11893
  return;
10146
11894
  }
10147
- const content = await readFile15(commentsPath, "utf-8");
11895
+ const content = await readFile17(commentsPath, "utf-8");
10148
11896
  const parsed = parseComments(content);
10149
11897
  const target = parsed.entries.find((e) => e.id === commentId);
10150
11898
  if (!target) {
@@ -10179,7 +11927,7 @@ ${entry}`;
10179
11927
  router.post("/api/projects/:slug/move-workspace", async (req, res) => {
10180
11928
  try {
10181
11929
  const projectSlug = getParam(req.params.slug);
10182
- const projectPath = resolve18(projectsDir, projectSlug, "project.md");
11930
+ const projectPath = resolve20(projectsDir, projectSlug, "project.md");
10183
11931
  if (!await fileExists(projectPath)) {
10184
11932
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
10185
11933
  return;
@@ -10191,7 +11939,7 @@ ${entry}`;
10191
11939
  });
10192
11940
  return;
10193
11941
  }
10194
- let content = await readFile15(projectPath, "utf-8");
11942
+ let content = await readFile17(projectPath, "utf-8");
10195
11943
  content = setTopLevelField(content, "workspace", workspace ?? null);
10196
11944
  content = setTopLevelField(content, "updated", nowTimestamp());
10197
11945
  await writeFileForce(projectPath, content);
@@ -10227,8 +11975,8 @@ ${entry}`;
10227
11975
  });
10228
11976
  return;
10229
11977
  }
10230
- const assignmentPath = resolve18(resolved.assignmentDir, "assignment.md");
10231
- let content = await readFile15(assignmentPath, "utf-8");
11978
+ const assignmentPath = resolve20(resolved.assignmentDir, "assignment.md");
11979
+ let content = await readFile17(assignmentPath, "utf-8");
10232
11980
  content = setTopLevelField(content, "workspaceGroup", workspaceGroup ?? null);
10233
11981
  content = setTopLevelField(content, "updated", nowTimestamp());
10234
11982
  await writeFileForce(assignmentPath, content);
@@ -10244,7 +11992,7 @@ ${entry}`;
10244
11992
  async (req, res) => {
10245
11993
  try {
10246
11994
  const projectSlug = getParam(req.params.slug);
10247
- const projectPath = resolve18(projectsDir, projectSlug, "project.md");
11995
+ const projectPath = resolve20(projectsDir, projectSlug, "project.md");
10248
11996
  if (!await fileExists(projectPath)) {
10249
11997
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
10250
11998
  return;
@@ -10303,7 +12051,7 @@ ${entry}`;
10303
12051
  try {
10304
12052
  const projectSlug = getParam(req.params.slug);
10305
12053
  const assignmentSlug = getParam(req.params.aslug);
10306
- const assignmentPath = resolve18(
12054
+ const assignmentPath = resolve20(
10307
12055
  projectsDir,
10308
12056
  projectSlug,
10309
12057
  "assignments",
@@ -10352,7 +12100,7 @@ ${entry}`;
10352
12100
  try {
10353
12101
  const projectSlug = getParam(req.params.slug);
10354
12102
  const assignmentSlug = getParam(req.params.aslug);
10355
- const assignmentPath = resolve18(
12103
+ const assignmentPath = resolve20(
10356
12104
  projectsDir,
10357
12105
  projectSlug,
10358
12106
  "assignments",
@@ -10411,7 +12159,7 @@ ${entry}`;
10411
12159
  try {
10412
12160
  const projectSlug = getParam(req.params.slug);
10413
12161
  const assignmentSlug = getParam(req.params.aslug);
10414
- const assignmentPath = resolve18(
12162
+ const assignmentPath = resolve20(
10415
12163
  projectsDir,
10416
12164
  projectSlug,
10417
12165
  "assignments",
@@ -10444,8 +12192,8 @@ ${entry}`;
10444
12192
  res.status(404).json({ error: `Assignment "${id}" not found` });
10445
12193
  return;
10446
12194
  }
10447
- const assignmentPath = resolve18(resolved.assignmentDir, "assignment.md");
10448
- const parsedForSlug = parseAssignmentFull(await readFile15(assignmentPath, "utf-8"));
12195
+ const assignmentPath = resolve20(resolved.assignmentDir, "assignment.md");
12196
+ const parsedForSlug = parseAssignmentFull(await readFile17(assignmentPath, "utf-8"));
10449
12197
  const assignmentSlugForBranch = parsedForSlug.slug || resolved.id;
10450
12198
  await handleWorktreeCreate(req, res, {
10451
12199
  assignmentPath,
@@ -10501,7 +12249,7 @@ ${entry}`;
10501
12249
  router.post("/api/projects/:slug/status-override", async (req, res) => {
10502
12250
  try {
10503
12251
  const projectSlug = getParam(req.params.slug);
10504
- const projectPath = resolve18(projectsDir, projectSlug, "project.md");
12252
+ const projectPath = resolve20(projectsDir, projectSlug, "project.md");
10505
12253
  if (!await fileExists(projectPath)) {
10506
12254
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
10507
12255
  return;
@@ -10513,7 +12261,7 @@ ${entry}`;
10513
12261
  res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}, or null to clear.` });
10514
12262
  return;
10515
12263
  }
10516
- let content = await readFile15(projectPath, "utf-8");
12264
+ let content = await readFile17(projectPath, "utf-8");
10517
12265
  content = setTopLevelField(content, "statusOverride", status ?? null);
10518
12266
  content = setTopLevelField(content, "updated", nowTimestamp());
10519
12267
  await writeFileForce(projectPath, content);
@@ -10528,7 +12276,7 @@ ${entry}`;
10528
12276
  try {
10529
12277
  const projectSlug = getParam(req.params.slug);
10530
12278
  const assignmentSlug = getParam(req.params.aslug);
10531
- const assignmentPath = resolve18(
12279
+ const assignmentPath = resolve20(
10532
12280
  projectsDir,
10533
12281
  projectSlug,
10534
12282
  "assignments",
@@ -10542,17 +12290,40 @@ ${entry}`;
10542
12290
  const { status } = req.body || {};
10543
12291
  const config = await getStatusConfig();
10544
12292
  const validStatuses = config.statuses.map((s) => s.id);
10545
- if (typeof status !== "string" || !validStatuses.includes(status)) {
12293
+ const clearing = status === null;
12294
+ if (!clearing && (typeof status !== "string" || !validStatuses.includes(status))) {
10546
12295
  res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}.` });
10547
12296
  return;
10548
12297
  }
10549
- let content = await readFile15(assignmentPath, "utf-8");
10550
- content = setTopLevelField(content, "status", status);
10551
- content = setTopLevelField(content, "updated", nowTimestamp());
10552
- if (status !== "blocked") {
10553
- content = setTopLevelField(content, "blockedReason", null);
12298
+ if (!clearing && config.terminalStatuses.has(status)) {
12299
+ res.status(400).json({
12300
+ error: `"${status}" is terminal \u2014 use the complete/fail transition (gated), not an override.`
12301
+ });
12302
+ return;
12303
+ }
12304
+ const { recomputeAndWrite: recomputeAndWrite2, resolveDeriveContext: resolveDeriveContext2 } = await Promise.resolve().then(() => (init_recompute(), recompute_exports));
12305
+ const { updateOverride: updateOverride2 } = await Promise.resolve().then(() => (init_frontmatter(), frontmatter_exports));
12306
+ const context = await resolveDeriveContext2();
12307
+ const result = await recomputeAndWrite2(assignmentPath, {
12308
+ cause: clearing ? "unpin" : "pin",
12309
+ by: "human",
12310
+ projectDir: resolve20(projectsDir, projectSlug),
12311
+ context,
12312
+ mutate: (content) => {
12313
+ if (clearing) return updateOverride2(content, null);
12314
+ const current = parseAssignmentFull(content);
12315
+ if (current.override?.status === status) return content;
12316
+ return updateOverride2(content, { status, source: "human", reason: null, at: nowTimestamp() });
12317
+ }
12318
+ });
12319
+ if (result.deferredTerminal) {
12320
+ res.status(409).json({ error: "Assignment is terminal \u2014 reopen it first." });
12321
+ return;
12322
+ }
12323
+ if (result.warning) {
12324
+ res.status(503).json({ error: result.warning });
12325
+ return;
10554
12326
  }
10555
- await writeFileForce(assignmentPath, content);
10556
12327
  const assignment = await getAssignmentDetail(projectsDir, projectSlug, assignmentSlug);
10557
12328
  res.json({ assignment });
10558
12329
  } catch (error) {
@@ -10567,12 +12338,12 @@ ${entry}`;
10567
12338
  router.post("/api/projects/:slug/archive", async (req, res) => {
10568
12339
  try {
10569
12340
  const projectSlug = getParam(req.params.slug);
10570
- const projectPath = resolve18(projectsDir, projectSlug, "project.md");
12341
+ const projectPath = resolve20(projectsDir, projectSlug, "project.md");
10571
12342
  if (!await fileExists(projectPath)) {
10572
12343
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
10573
12344
  return;
10574
12345
  }
10575
- const content = await readFile15(projectPath, "utf-8");
12346
+ const content = await readFile17(projectPath, "utf-8");
10576
12347
  await writeFileForce(projectPath, applyArchiveFields(content, true, archiveReason(req.body)));
10577
12348
  const project = await getProjectDetail(projectsDir, projectSlug);
10578
12349
  res.json({ project });
@@ -10584,12 +12355,12 @@ ${entry}`;
10584
12355
  router.post("/api/projects/:slug/unarchive", async (req, res) => {
10585
12356
  try {
10586
12357
  const projectSlug = getParam(req.params.slug);
10587
- const projectPath = resolve18(projectsDir, projectSlug, "project.md");
12358
+ const projectPath = resolve20(projectsDir, projectSlug, "project.md");
10588
12359
  if (!await fileExists(projectPath)) {
10589
12360
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
10590
12361
  return;
10591
12362
  }
10592
- const content = await readFile15(projectPath, "utf-8");
12363
+ const content = await readFile17(projectPath, "utf-8");
10593
12364
  await writeFileForce(projectPath, applyArchiveFields(content, false, null));
10594
12365
  const project = await getProjectDetail(projectsDir, projectSlug);
10595
12366
  res.json({ project });
@@ -10601,12 +12372,12 @@ ${entry}`;
10601
12372
  async function handleAssignmentArchive(req, res, archived) {
10602
12373
  const projectSlug = getParam(req.params.slug);
10603
12374
  const assignmentSlug = getParam(req.params.aslug);
10604
- const assignmentPath = resolve18(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md");
12375
+ const assignmentPath = resolve20(projectsDir, projectSlug, "assignments", assignmentSlug, "assignment.md");
10605
12376
  if (!await fileExists(assignmentPath)) {
10606
12377
  res.status(404).json({ error: "Assignment not found" });
10607
12378
  return;
10608
12379
  }
10609
- const content = await readFile15(assignmentPath, "utf-8");
12380
+ const content = await readFile17(assignmentPath, "utf-8");
10610
12381
  await writeFileForce(assignmentPath, applyArchiveFields(content, archived, archived ? archiveReason(req.body) : null));
10611
12382
  const assignment = await getAssignmentDetail(projectsDir, projectSlug, assignmentSlug);
10612
12383
  res.json({ assignment });
@@ -10638,8 +12409,8 @@ ${entry}`;
10638
12409
  res.status(404).json({ error: `Assignment "${id}" not found` });
10639
12410
  return;
10640
12411
  }
10641
- const assignmentPath = resolve18(resolved.assignmentDir, "assignment.md");
10642
- const content = await readFile15(assignmentPath, "utf-8");
12412
+ const assignmentPath = resolve20(resolved.assignmentDir, "assignment.md");
12413
+ const content = await readFile17(assignmentPath, "utf-8");
10643
12414
  await writeFileForce(assignmentPath, applyArchiveFields(content, archived, archived ? archiveReason(req.body) : null));
10644
12415
  const assignment = await getAssignmentDetailById(projectsDir, assignmentsDir2, id);
10645
12416
  res.json({ assignment });
@@ -10664,7 +12435,7 @@ ${entry}`;
10664
12435
  try {
10665
12436
  const projectSlug = getParam(req.params.slug);
10666
12437
  const assignmentSlug = getParam(req.params.aslug);
10667
- const assignmentPath = resolve18(
12438
+ const assignmentPath = resolve20(
10668
12439
  projectsDir,
10669
12440
  projectSlug,
10670
12441
  "assignments",
@@ -10680,7 +12451,7 @@ ${entry}`;
10680
12451
  res.status(400).json({ error: validation.error });
10681
12452
  return;
10682
12453
  }
10683
- let content = await readFile15(assignmentPath, "utf-8");
12454
+ let content = await readFile17(assignmentPath, "utf-8");
10684
12455
  content = setTopLevelField(content, "assignee", validation.value);
10685
12456
  content = setTopLevelField(content, "updated", nowTimestamp());
10686
12457
  await writeFileForce(assignmentPath, content);
@@ -10695,7 +12466,7 @@ ${entry}`;
10695
12466
  try {
10696
12467
  const projectSlug = getParam(req.params.slug);
10697
12468
  const assignmentSlug = getParam(req.params.aslug);
10698
- const assignmentPath = resolve18(
12469
+ const assignmentPath = resolve20(
10699
12470
  projectsDir,
10700
12471
  projectSlug,
10701
12472
  "assignments",
@@ -10711,7 +12482,7 @@ ${entry}`;
10711
12482
  res.status(400).json({ error: validation.error });
10712
12483
  return;
10713
12484
  }
10714
- let content = await readFile15(assignmentPath, "utf-8");
12485
+ let content = await readFile17(assignmentPath, "utf-8");
10715
12486
  content = setTopLevelField(content, "title", validation.value);
10716
12487
  content = setTopLevelField(content, "updated", nowTimestamp());
10717
12488
  await writeFileForce(assignmentPath, content);
@@ -10733,16 +12504,49 @@ ${entry}`;
10733
12504
  res.status(400).json({ error: `Unsupported transition command "${req.params.command}"` });
10734
12505
  return;
10735
12506
  }
10736
- const projectDir = resolve18(projectsDir, projectSlug);
10737
- const assignmentPath = resolve18(projectDir, "assignments", assignmentSlug, "assignment.md");
12507
+ const projectDir = resolve20(projectsDir, projectSlug);
12508
+ const assignmentPath = resolve20(projectDir, "assignments", assignmentSlug, "assignment.md");
10738
12509
  if (!await fileExists(assignmentPath)) {
10739
12510
  res.status(404).json({ error: "Assignment not found" });
10740
12511
  return;
10741
12512
  }
10742
12513
  const { reason } = req.body || {};
12514
+ const { recomputeAndWrite: recomputeAndWrite2, recomputeDependents: recomputeDependents2, resolveDeriveContext: resolveDeriveContext2 } = await Promise.resolve().then(() => (init_recompute(), recompute_exports));
12515
+ const context = await resolveDeriveContext2();
12516
+ if (command === "block" || command === "unblock") {
12517
+ const { updateAssignmentFile: updateAssignmentFile2 } = await Promise.resolve().then(() => (init_frontmatter(), frontmatter_exports));
12518
+ const result2 = await recomputeAndWrite2(assignmentPath, {
12519
+ cause: command,
12520
+ by: "human",
12521
+ projectDir,
12522
+ context,
12523
+ reason: typeof reason === "string" ? reason : void 0,
12524
+ mutate: (content) => updateAssignmentFile2(content, {
12525
+ blockedReason: command === "block" ? typeof reason === "string" && reason ? reason : "(unspecified)" : null
12526
+ })
12527
+ });
12528
+ if (result2.deferredTerminal) {
12529
+ res.status(409).json({ error: "Assignment is terminal \u2014 reopen it first." });
12530
+ return;
12531
+ }
12532
+ if (result2.warning) {
12533
+ res.status(503).json({ error: result2.warning });
12534
+ return;
12535
+ }
12536
+ const assignment2 = await getAssignmentDetail(projectsDir, projectSlug, assignmentSlug);
12537
+ res.json({ assignment: assignment2, transition: { success: true, message: command, fromStatus: "", toStatus: result2.status } });
12538
+ return;
12539
+ }
12540
+ const GATED_TERMINAL = /* @__PURE__ */ new Set(["complete", "fail", "reopen"]);
12541
+ const gatedFallback = GATED_TERMINAL.has(command) ? unambiguousCommandTarget(config.transitions, command) : void 0;
10743
12542
  const result = await executeTransition(projectDir, assignmentSlug, command, {
10744
12543
  reason: typeof reason === "string" ? reason : void 0,
12544
+ // Gated terminal commands: the from-specific custom mapping wins
12545
+ // (passed via the table), with the unambiguous command target as the
12546
+ // guard-free fallback for legacy/undefined statuses (codex r3
12547
+ // finding 1 — ambiguous configs must not pick an arbitrary target).
10745
12548
  transitionTable: config.custom ? config.transitionTable : void 0,
12549
+ commandTargets: config.custom && gatedFallback ? /* @__PURE__ */ new Map([[command, gatedFallback]]) : void 0,
10746
12550
  terminalStatuses: config.custom ? config.terminalStatuses : void 0,
10747
12551
  linkedTodosLookup
10748
12552
  });
@@ -10750,6 +12554,25 @@ ${entry}`;
10750
12554
  res.status(400).json({ error: result.message });
10751
12555
  return;
10752
12556
  }
12557
+ const settled = await recomputeAndWrite2(assignmentPath, {
12558
+ cause: command,
12559
+ by: "human",
12560
+ projectDir,
12561
+ context
12562
+ });
12563
+ if (settled.warning) {
12564
+ res.status(503).json({ error: settled.warning });
12565
+ return;
12566
+ }
12567
+ const wasTerminal = config.terminalStatuses.has(result.fromStatus);
12568
+ const isTerminal = result.toStatus ? config.terminalStatuses.has(result.toStatus) : false;
12569
+ if (wasTerminal !== isTerminal) {
12570
+ await recomputeDependents2(projectDir, assignmentSlug, {
12571
+ cause: "dep-terminal",
12572
+ by: "system",
12573
+ context
12574
+ });
12575
+ }
10753
12576
  const assignment = await getAssignmentDetail(projectsDir, projectSlug, assignmentSlug);
10754
12577
  res.json({ assignment, transition: result });
10755
12578
  } catch (error) {
@@ -10761,8 +12584,8 @@ ${entry}`;
10761
12584
  try {
10762
12585
  const projectSlug = getParam(req.params.slug);
10763
12586
  const assignmentSlug = getParam(req.params.aslug);
10764
- const assignmentDir = resolve18(projectsDir, projectSlug, "assignments", assignmentSlug);
10765
- const assignmentPath = resolve18(assignmentDir, "assignment.md");
12587
+ const assignmentDir = resolve20(projectsDir, projectSlug, "assignments", assignmentSlug);
12588
+ const assignmentPath = resolve20(assignmentDir, "assignment.md");
10766
12589
  if (!await fileExists(assignmentPath)) {
10767
12590
  res.status(404).json({ error: `Assignment "${assignmentSlug}" not found in project "${projectSlug}"` });
10768
12591
  return;
@@ -10817,33 +12640,42 @@ ${entry}`;
10817
12640
  return;
10818
12641
  }
10819
12642
  const id2 = generateId();
10820
- const assignmentDir2 = resolve18(assignmentsDir2, id2);
12643
+ const assignmentDir2 = resolve20(assignmentsDir2, id2);
10821
12644
  if (await fileExists(assignmentDir2)) {
10822
12645
  res.status(500).json({ error: "UUID collision \u2014 try again" });
10823
12646
  return;
10824
12647
  }
10825
12648
  const timestamp2 = fields.created || nowTimestamp();
10826
12649
  await ensureDir(assignmentDir2);
10827
- const normalizedContent = setTopLevelField(rawContent, "id", id2);
10828
- await writeFileForce(resolve18(assignmentDir2, "assignment.md"), normalizedContent);
12650
+ let normalizedContent = setTopLevelField(rawContent, "id", id2);
12651
+ if (parseAssignmentFull(normalizedContent).statusHistory.length === 0) {
12652
+ normalizedContent = appendStatusHistoryEntry(normalizedContent, {
12653
+ at: timestamp2,
12654
+ from: null,
12655
+ to: parseAssignmentFull(normalizedContent).status,
12656
+ command: "create",
12657
+ by: null
12658
+ });
12659
+ }
12660
+ await writeFileForce(resolve20(assignmentDir2, "assignment.md"), normalizedContent);
10829
12661
  await writeFileForce(
10830
- resolve18(assignmentDir2, "scratchpad.md"),
12662
+ resolve20(assignmentDir2, "scratchpad.md"),
10831
12663
  renderScratchpad({ assignmentSlug: id2, timestamp: timestamp2 })
10832
12664
  );
10833
12665
  await writeFileForce(
10834
- resolve18(assignmentDir2, "handoff.md"),
12666
+ resolve20(assignmentDir2, "handoff.md"),
10835
12667
  renderHandoff({ assignmentSlug: id2, timestamp: timestamp2 })
10836
12668
  );
10837
12669
  await writeFileForce(
10838
- resolve18(assignmentDir2, "decision-record.md"),
12670
+ resolve20(assignmentDir2, "decision-record.md"),
10839
12671
  renderDecisionRecord({ assignmentSlug: id2, timestamp: timestamp2 })
10840
12672
  );
10841
12673
  await writeFileForce(
10842
- resolve18(assignmentDir2, "progress.md"),
12674
+ resolve20(assignmentDir2, "progress.md"),
10843
12675
  renderProgress({ assignment: id2, timestamp: timestamp2 })
10844
12676
  );
10845
12677
  await writeFileForce(
10846
- resolve18(assignmentDir2, "comments.md"),
12678
+ resolve20(assignmentDir2, "comments.md"),
10847
12679
  renderComments({ assignment: id2, timestamp: timestamp2 })
10848
12680
  );
10849
12681
  const detail2 = await getAssignmentDetailById(projectsDir, assignmentsDir2, id2);
@@ -10861,7 +12693,7 @@ ${entry}`;
10861
12693
  return;
10862
12694
  }
10863
12695
  const id = generateId();
10864
- const assignmentDir = resolve18(assignmentsDir2, id);
12696
+ const assignmentDir = resolve20(assignmentsDir2, id);
10865
12697
  if (await fileExists(assignmentDir)) {
10866
12698
  res.status(500).json({ error: "UUID collision \u2014 try again" });
10867
12699
  return;
@@ -10881,25 +12713,25 @@ ${entry}`;
10881
12713
  project: null,
10882
12714
  type: typeof type === "string" ? type : void 0
10883
12715
  });
10884
- await writeFileForce(resolve18(assignmentDir, "assignment.md"), assignmentContent);
12716
+ await writeFileForce(resolve20(assignmentDir, "assignment.md"), assignmentContent);
10885
12717
  await writeFileForce(
10886
- resolve18(assignmentDir, "scratchpad.md"),
12718
+ resolve20(assignmentDir, "scratchpad.md"),
10887
12719
  renderScratchpad({ assignmentSlug: id, timestamp })
10888
12720
  );
10889
12721
  await writeFileForce(
10890
- resolve18(assignmentDir, "handoff.md"),
12722
+ resolve20(assignmentDir, "handoff.md"),
10891
12723
  renderHandoff({ assignmentSlug: id, timestamp })
10892
12724
  );
10893
12725
  await writeFileForce(
10894
- resolve18(assignmentDir, "decision-record.md"),
12726
+ resolve20(assignmentDir, "decision-record.md"),
10895
12727
  renderDecisionRecord({ assignmentSlug: id, timestamp })
10896
12728
  );
10897
12729
  await writeFileForce(
10898
- resolve18(assignmentDir, "progress.md"),
12730
+ resolve20(assignmentDir, "progress.md"),
10899
12731
  renderProgress({ assignment: id, timestamp })
10900
12732
  );
10901
12733
  await writeFileForce(
10902
- resolve18(assignmentDir, "comments.md"),
12734
+ resolve20(assignmentDir, "comments.md"),
10903
12735
  renderComments({ assignment: id, timestamp })
10904
12736
  );
10905
12737
  const detail = await getAssignmentDetailById(projectsDir, assignmentsDir2, id);
@@ -11027,7 +12859,7 @@ ${entry}`;
11027
12859
  res.status(404).json({ error: `Assignment "${id}" not found` });
11028
12860
  return;
11029
12861
  }
11030
- const assignmentPath = resolve18(resolved.assignmentDir, "assignment.md");
12862
+ const assignmentPath = resolve20(resolved.assignmentDir, "assignment.md");
11031
12863
  const currentContent = await readCurrentDocument(assignmentPath);
11032
12864
  if (!currentContent) {
11033
12865
  res.status(404).json({ error: "Assignment not found" });
@@ -11045,10 +12877,20 @@ ${entry}`;
11045
12877
  if (current.id) nextContent = setTopLevelField(nextContent, "id", current.id);
11046
12878
  nextContent = setTopLevelField(nextContent, "project", null);
11047
12879
  if (current.slug) nextContent = setTopLevelField(nextContent, "slug", current.slug);
12880
+ const now = nowTimestamp();
11048
12881
  if (next.status !== current.status && current.status === "blocked" && next.status !== "blocked") {
11049
12882
  nextContent = setTopLevelField(nextContent, "blockedReason", null);
11050
12883
  }
11051
- nextContent = setTopLevelField(nextContent, "updated", nowTimestamp());
12884
+ nextContent = setTopLevelField(nextContent, "updated", now);
12885
+ if (next.status !== current.status) {
12886
+ nextContent = appendStatusHistoryEntry(nextContent, {
12887
+ at: now,
12888
+ from: current.status,
12889
+ to: next.status,
12890
+ command: "edit",
12891
+ by: null
12892
+ });
12893
+ }
11052
12894
  await writeFileForce(assignmentPath, nextContent);
11053
12895
  const assignment = await getAssignmentDetailById(projectsDir, assignmentsDir2, id);
11054
12896
  res.json({ assignment, content: nextContent });
@@ -11069,7 +12911,7 @@ ${entry}`;
11069
12911
  res.status(404).json({ error: `Assignment "${id}" not found` });
11070
12912
  return;
11071
12913
  }
11072
- const planPath = resolve18(resolved.assignmentDir, "plan.md");
12914
+ const planPath = resolve20(resolved.assignmentDir, "plan.md");
11073
12915
  const currentContent = await readCurrentDocument(planPath);
11074
12916
  if (!currentContent) {
11075
12917
  res.status(404).json({ error: "Plan not found" });
@@ -11103,7 +12945,7 @@ ${entry}`;
11103
12945
  res.status(404).json({ error: `Assignment "${id}" not found` });
11104
12946
  return;
11105
12947
  }
11106
- const scratchpadPath = resolve18(resolved.assignmentDir, "scratchpad.md");
12948
+ const scratchpadPath = resolve20(resolved.assignmentDir, "scratchpad.md");
11107
12949
  const currentContent = await readCurrentDocument(scratchpadPath);
11108
12950
  if (!currentContent) {
11109
12951
  res.status(404).json({ error: "Scratchpad not found" });
@@ -11137,7 +12979,7 @@ ${entry}`;
11137
12979
  res.status(404).json({ error: `Assignment "${id}" not found` });
11138
12980
  return;
11139
12981
  }
11140
- const handoffPath = resolve18(resolved.assignmentDir, "handoff.md");
12982
+ const handoffPath = resolve20(resolved.assignmentDir, "handoff.md");
11141
12983
  const currentContent = await readCurrentDocument(handoffPath);
11142
12984
  if (!currentContent) {
11143
12985
  res.status(404).json({ error: "Handoff log not found" });
@@ -11177,7 +13019,7 @@ ${entry}`;
11177
13019
  res.status(404).json({ error: `Assignment "${id}" not found` });
11178
13020
  return;
11179
13021
  }
11180
- const decisionPath = resolve18(resolved.assignmentDir, "decision-record.md");
13022
+ const decisionPath = resolve20(resolved.assignmentDir, "decision-record.md");
11181
13023
  const currentContent = await readCurrentDocument(decisionPath);
11182
13024
  if (!currentContent) {
11183
13025
  res.status(404).json({ error: "Decision record not found" });
@@ -11217,7 +13059,7 @@ ${entry}`;
11217
13059
  res.status(404).json({ error: `Assignment "${id}" not found` });
11218
13060
  return;
11219
13061
  }
11220
- const assignmentPath = resolve18(resolved.assignmentDir, "assignment.md");
13062
+ const assignmentPath = resolve20(resolved.assignmentDir, "assignment.md");
11221
13063
  if (!await fileExists(assignmentPath)) {
11222
13064
  res.status(404).json({ error: "Assignment not found" });
11223
13065
  return;
@@ -11225,17 +13067,41 @@ ${entry}`;
11225
13067
  const { status } = req.body || {};
11226
13068
  const config = await getStatusConfig();
11227
13069
  const validStatuses = config.statuses.map((s) => s.id);
11228
- if (typeof status !== "string" || !validStatuses.includes(status)) {
13070
+ const clearing = status === null;
13071
+ if (!clearing && (typeof status !== "string" || !validStatuses.includes(status))) {
11229
13072
  res.status(400).json({ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}.` });
11230
13073
  return;
11231
13074
  }
11232
- let content = await readFile15(assignmentPath, "utf-8");
11233
- content = setTopLevelField(content, "status", status);
11234
- content = setTopLevelField(content, "updated", nowTimestamp());
11235
- if (status !== "blocked") {
11236
- content = setTopLevelField(content, "blockedReason", null);
13075
+ if (!clearing && config.terminalStatuses.has(status)) {
13076
+ res.status(400).json({
13077
+ error: `"${status}" is terminal \u2014 use the complete/fail transition (gated), not an override.`
13078
+ });
13079
+ return;
13080
+ }
13081
+ const { recomputeAndWrite: recomputeAndWrite2, resolveDeriveContext: resolveDeriveContext2 } = await Promise.resolve().then(() => (init_recompute(), recompute_exports));
13082
+ const { updateOverride: updateOverride2 } = await Promise.resolve().then(() => (init_frontmatter(), frontmatter_exports));
13083
+ const context = await resolveDeriveContext2();
13084
+ const projectDirForId = resolved.standalone ? null : resolve20(resolved.assignmentDir, "..", "..");
13085
+ const result = await recomputeAndWrite2(assignmentPath, {
13086
+ cause: clearing ? "unpin" : "pin",
13087
+ by: "human",
13088
+ projectDir: projectDirForId,
13089
+ context,
13090
+ mutate: (content) => {
13091
+ if (clearing) return updateOverride2(content, null);
13092
+ const current = parseAssignmentFull(content);
13093
+ if (current.override?.status === status) return content;
13094
+ return updateOverride2(content, { status, source: "human", reason: null, at: nowTimestamp() });
13095
+ }
13096
+ });
13097
+ if (result.deferredTerminal) {
13098
+ res.status(409).json({ error: "Assignment is terminal \u2014 reopen it first." });
13099
+ return;
13100
+ }
13101
+ if (result.warning) {
13102
+ res.status(503).json({ error: result.warning });
13103
+ return;
11237
13104
  }
11238
- await writeFileForce(assignmentPath, content);
11239
13105
  const assignment = await getAssignmentDetailById(projectsDir, assignmentsDir2, id);
11240
13106
  res.json({ assignment });
11241
13107
  } catch (error) {
@@ -11255,7 +13121,7 @@ ${entry}`;
11255
13121
  res.status(404).json({ error: `Assignment "${id}" not found` });
11256
13122
  return;
11257
13123
  }
11258
- const assignmentPath = resolve18(resolved.assignmentDir, "assignment.md");
13124
+ const assignmentPath = resolve20(resolved.assignmentDir, "assignment.md");
11259
13125
  if (!await fileExists(assignmentPath)) {
11260
13126
  res.status(404).json({ error: "Assignment not found" });
11261
13127
  return;
@@ -11265,7 +13131,7 @@ ${entry}`;
11265
13131
  res.status(400).json({ error: validation.error });
11266
13132
  return;
11267
13133
  }
11268
- let content = await readFile15(assignmentPath, "utf-8");
13134
+ let content = await readFile17(assignmentPath, "utf-8");
11269
13135
  content = setTopLevelField(content, "assignee", validation.value);
11270
13136
  content = setTopLevelField(content, "updated", nowTimestamp());
11271
13137
  await writeFileForce(assignmentPath, content);
@@ -11288,7 +13154,7 @@ ${entry}`;
11288
13154
  res.status(404).json({ error: `Assignment "${id}" not found` });
11289
13155
  return;
11290
13156
  }
11291
- const assignmentPath = resolve18(resolved.assignmentDir, "assignment.md");
13157
+ const assignmentPath = resolve20(resolved.assignmentDir, "assignment.md");
11292
13158
  if (!await fileExists(assignmentPath)) {
11293
13159
  res.status(404).json({ error: "Assignment not found" });
11294
13160
  return;
@@ -11298,7 +13164,7 @@ ${entry}`;
11298
13164
  res.status(400).json({ error: validation.error });
11299
13165
  return;
11300
13166
  }
11301
- let content = await readFile15(assignmentPath, "utf-8");
13167
+ let content = await readFile17(assignmentPath, "utf-8");
11302
13168
  content = setTopLevelField(content, "title", validation.value);
11303
13169
  content = setTopLevelField(content, "updated", nowTimestamp());
11304
13170
  await writeFileForce(assignmentPath, content);
@@ -11321,7 +13187,7 @@ ${entry}`;
11321
13187
  res.status(404).json({ error: `Assignment "${id}" not found` });
11322
13188
  return;
11323
13189
  }
11324
- const assignmentPath = resolve18(resolved.assignmentDir, "assignment.md");
13190
+ const assignmentPath = resolve20(resolved.assignmentDir, "assignment.md");
11325
13191
  const currentContent = await readCurrentDocument(assignmentPath);
11326
13192
  if (!currentContent) {
11327
13193
  res.status(404).json({ error: "Assignment not found" });
@@ -11361,12 +13227,57 @@ ${entry}`;
11361
13227
  return;
11362
13228
  }
11363
13229
  const { reason } = req.body || {};
13230
+ const config = await getStatusConfig();
13231
+ const validCommandsById = [...new Set(config.transitions.map((t) => t.command))];
13232
+ if (!validCommandsById.includes(command)) {
13233
+ res.status(400).json({ error: `Unsupported transition command "${command}"` });
13234
+ return;
13235
+ }
13236
+ const { recomputeAndWrite: recomputeAndWrite2, recomputeDependents: recomputeDependents2, resolveDeriveContext: resolveDeriveContext2 } = await Promise.resolve().then(() => (init_recompute(), recompute_exports));
13237
+ const context = await resolveDeriveContext2();
13238
+ const byIdPath = resolve20(resolved.assignmentDir, "assignment.md");
13239
+ const byIdProjectDir = resolved.standalone ? null : resolve20(resolved.assignmentDir, "..", "..");
13240
+ if (command === "block" || command === "unblock") {
13241
+ const { updateAssignmentFile: updateAssignmentFile2 } = await Promise.resolve().then(() => (init_frontmatter(), frontmatter_exports));
13242
+ const result = await recomputeAndWrite2(byIdPath, {
13243
+ cause: command,
13244
+ by: "human",
13245
+ projectDir: byIdProjectDir,
13246
+ context,
13247
+ reason: typeof reason === "string" ? reason : void 0,
13248
+ mutate: (content) => updateAssignmentFile2(content, {
13249
+ blockedReason: command === "block" ? typeof reason === "string" && reason ? reason : "(unspecified)" : null
13250
+ })
13251
+ });
13252
+ if (result.deferredTerminal) {
13253
+ res.status(409).json({ error: "Assignment is terminal \u2014 reopen it first." });
13254
+ return;
13255
+ }
13256
+ if (result.warning) {
13257
+ res.status(503).json({ error: result.warning });
13258
+ return;
13259
+ }
13260
+ const detail2 = resolved.standalone ? await getAssignmentDetailById(projectsDir, assignmentsDir2, id) : await getAssignmentDetail(projectsDir, resolved.projectSlug, resolved.assignmentSlug);
13261
+ res.json({ assignment: detail2, warnings: [] });
13262
+ return;
13263
+ }
13264
+ const GATED_TERMINAL = /* @__PURE__ */ new Set(["complete", "fail", "reopen"]);
13265
+ const gatedFallbackById = GATED_TERMINAL.has(command) ? unambiguousCommandTarget(config.transitions, command) : void 0;
11364
13266
  const transitionResult = await executeTransitionByDir(
11365
13267
  resolved.assignmentDir,
11366
13268
  command,
11367
13269
  {
11368
13270
  standalone: resolved.standalone,
11369
13271
  reason: typeof reason === "string" ? reason : void 0,
13272
+ // Same resolution as the project route: from-specific mapping wins,
13273
+ // unambiguous command target as guard-free fallback for gated
13274
+ // terminal commands. NOTE: by-id historically ran guard-free for
13275
+ // all commands; the custom from-table now applies only to gated
13276
+ // commands' resolution (their fallback), keeping non-terminal by-id
13277
+ // behavior guard-free as before.
13278
+ commandTargets: config.custom && gatedFallbackById ? /* @__PURE__ */ new Map([[command, gatedFallbackById]]) : void 0,
13279
+ transitionTable: config.custom && GATED_TERMINAL.has(command) ? config.transitionTable : void 0,
13280
+ terminalStatuses: config.custom ? config.terminalStatuses : void 0,
11370
13281
  linkedTodosLookup
11371
13282
  }
11372
13283
  );
@@ -11374,6 +13285,27 @@ ${entry}`;
11374
13285
  res.status(400).json({ error: transitionResult.message, fromStatus: transitionResult.fromStatus });
11375
13286
  return;
11376
13287
  }
13288
+ const settledById = await recomputeAndWrite2(byIdPath, {
13289
+ cause: command,
13290
+ by: "human",
13291
+ projectDir: byIdProjectDir,
13292
+ context
13293
+ });
13294
+ if (settledById.warning) {
13295
+ res.status(503).json({ error: settledById.warning });
13296
+ return;
13297
+ }
13298
+ if (byIdProjectDir) {
13299
+ const wasTerminal = config.terminalStatuses.has(transitionResult.fromStatus);
13300
+ const isTerminal = transitionResult.toStatus ? config.terminalStatuses.has(transitionResult.toStatus) : false;
13301
+ if (wasTerminal !== isTerminal) {
13302
+ await recomputeDependents2(byIdProjectDir, resolved.assignmentSlug, {
13303
+ cause: "dep-terminal",
13304
+ by: "system",
13305
+ context
13306
+ });
13307
+ }
13308
+ }
11377
13309
  const detail = resolved.standalone ? await getAssignmentDetailById(projectsDir, assignmentsDir2, id) : await getAssignmentDetail(projectsDir, resolved.projectSlug, resolved.assignmentSlug);
11378
13310
  res.json({ assignment: detail, warnings: transitionResult.warnings ?? [] });
11379
13311
  } catch (error) {
@@ -11423,7 +13355,7 @@ function validateTitleBody(body) {
11423
13355
  return { ok: true, value: trimmed };
11424
13356
  }
11425
13357
  async function appendCommentTo(assignmentDir, assignmentRef, req, res, reloadDetail) {
11426
- const commentsPath = resolve18(assignmentDir, "comments.md");
13358
+ const commentsPath = resolve20(assignmentDir, "comments.md");
11427
13359
  const { body, author, type, replyTo } = req.body || {};
11428
13360
  if (!body || typeof body !== "string" || !body.trim()) {
11429
13361
  res.status(400).json({ error: "body is required" });
@@ -11435,7 +13367,7 @@ async function appendCommentTo(assignmentDir, assignmentRef, req, res, reloadDet
11435
13367
  let currentContent;
11436
13368
  let currentCount = 0;
11437
13369
  if (await fileExists(commentsPath)) {
11438
- currentContent = await readFile15(commentsPath, "utf-8");
13370
+ currentContent = await readFile17(commentsPath, "utf-8");
11439
13371
  const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
11440
13372
  if (countMatch) currentCount = parseInt(countMatch[1], 10);
11441
13373
  } else {
@@ -11465,7 +13397,7 @@ ${entry}`;
11465
13397
  res.status(201).json({ assignment, comment: { id: comment.id } });
11466
13398
  }
11467
13399
  async function toggleCommentResolvedAt(assignmentDir, commentId, req, res, reloadDetail) {
11468
- const commentsPath = resolve18(assignmentDir, "comments.md");
13400
+ const commentsPath = resolve20(assignmentDir, "comments.md");
11469
13401
  if (!await fileExists(commentsPath)) {
11470
13402
  res.status(404).json({ error: "Comments file not found" });
11471
13403
  return;
@@ -11475,7 +13407,7 @@ async function toggleCommentResolvedAt(assignmentDir, commentId, req, res, reloa
11475
13407
  res.status(400).json({ error: "resolved (boolean) is required" });
11476
13408
  return;
11477
13409
  }
11478
- const content = await readFile15(commentsPath, "utf-8");
13410
+ const content = await readFile17(commentsPath, "utf-8");
11479
13411
  const parsed = parseComments(content);
11480
13412
  const target = parsed.entries.find((e) => e.id === commentId);
11481
13413
  if (!target) {
@@ -11622,16 +13554,16 @@ function createServersRouter(serversDir2, projectsDir, assignmentsDir2) {
11622
13554
  init_agent_sessions();
11623
13555
  init_fs();
11624
13556
  import { Router as Router4 } from "express";
11625
- import { resolve as resolve19 } from "path";
13557
+ import { resolve as resolve21 } from "path";
11626
13558
 
11627
13559
  // src/utils/transcript.ts
11628
- import { open } from "fs/promises";
13560
+ import { open as open2 } from "fs/promises";
11629
13561
  var MAX_LINES_SCANNED = 50;
11630
13562
  async function derivePathFromTranscript(transcriptPath) {
11631
13563
  if (!transcriptPath) return null;
11632
13564
  let handle;
11633
13565
  try {
11634
- handle = await open(transcriptPath, "r");
13566
+ handle = await open2(transcriptPath, "r");
11635
13567
  } catch {
11636
13568
  return null;
11637
13569
  }
@@ -11720,7 +13652,7 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
11720
13652
  try {
11721
13653
  const { projectSlug } = req.params;
11722
13654
  const assignment = req.query.assignment;
11723
- const projectDir = resolve19(projectsDir, projectSlug);
13655
+ const projectDir = resolve21(projectsDir, projectSlug);
11724
13656
  if (!await fileExists(projectDir)) {
11725
13657
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
11726
13658
  return;
@@ -11750,7 +13682,7 @@ function createAgentSessionsRouter(projectsDir, broadcast, assignmentsDir2) {
11750
13682
  return;
11751
13683
  }
11752
13684
  if (projectSlug) {
11753
- const projectDir = resolve19(projectsDir, projectSlug);
13685
+ const projectDir = resolve21(projectsDir, projectSlug);
11754
13686
  if (!await fileExists(projectDir)) {
11755
13687
  res.status(404).json({ error: `Project "${projectSlug}" not found` });
11756
13688
  return;
@@ -12255,7 +14187,7 @@ init_api();
12255
14187
  init_agents_schema();
12256
14188
  import { spawn as spawn2 } from "child_process";
12257
14189
  import { mkdir as mkdir2, writeFile as writeFile4 } from "fs/promises";
12258
- import { isAbsolute as isAbsolute5, resolve as resolve20 } from "path";
14190
+ import { isAbsolute as isAbsolute5, resolve as resolve22 } from "path";
12259
14191
  var INITIAL_PROMPT = (params2) => {
12260
14192
  const playbook = params2.playbook?.trim();
12261
14193
  if (!playbook) {
@@ -12516,7 +14448,7 @@ function buildShellCommandLine(plan) {
12516
14448
  init_paths();
12517
14449
  init_fs();
12518
14450
  import { fileURLToPath } from "url";
12519
- import { dirname as dirname4, resolve as resolve21, join as join3 } from "path";
14451
+ import { dirname as dirname5, resolve as resolve23, join as join3 } from "path";
12520
14452
  import { realpathSync, readFileSync, mkdirSync } from "fs";
12521
14453
 
12522
14454
  // src/dashboard/api-launch-preflight.ts
@@ -12740,33 +14672,33 @@ import { Router as Router9 } from "express";
12740
14672
 
12741
14673
  // src/utils/status-config-resolution.ts
12742
14674
  init_frontmatter();
12743
- import { readFile as readFile16, writeFile as writeFile5, rm as rm2 } from "fs/promises";
12744
- import { dirname as dirname5 } from "path";
14675
+ import { readFile as readFile18, writeFile as writeFile5, rm as rm2 } from "fs/promises";
14676
+ import { dirname as dirname6 } from "path";
12745
14677
 
12746
14678
  // src/utils/assignment-walk.ts
12747
14679
  init_fs();
12748
- import { resolve as resolve22 } from "path";
12749
- import { readdir as readdir9 } from "fs/promises";
14680
+ import { resolve as resolve24 } from "path";
14681
+ import { readdir as readdir11 } from "fs/promises";
12750
14682
  async function listAssignmentsByProject(projectsDir, standaloneDir) {
12751
14683
  const result = {
12752
14684
  withAssignmentMd: [],
12753
14685
  orphanFolders: []
12754
14686
  };
12755
14687
  if (await fileExists(projectsDir)) {
12756
- const projects = await readdir9(projectsDir, { withFileTypes: true });
14688
+ const projects = await readdir11(projectsDir, { withFileTypes: true });
12757
14689
  for (const m of projects) {
12758
14690
  if (!m.isDirectory()) continue;
12759
14691
  if (m.name.startsWith(".") || m.name.startsWith("_")) continue;
12760
- const assignmentsDir2 = resolve22(projectsDir, m.name, "assignments");
14692
+ const assignmentsDir2 = resolve24(projectsDir, m.name, "assignments");
12761
14693
  if (!await fileExists(assignmentsDir2)) continue;
12762
- const entries = await readdir9(assignmentsDir2, { withFileTypes: true });
14694
+ const entries = await readdir11(assignmentsDir2, { withFileTypes: true });
12763
14695
  for (const a of entries) {
12764
14696
  if (!a.isDirectory()) continue;
12765
14697
  if (a.name.startsWith(".") || a.name.startsWith("_")) continue;
12766
- const assignmentDir = resolve22(assignmentsDir2, a.name);
12767
- const assignmentMd = resolve22(assignmentDir, "assignment.md");
14698
+ const assignmentDir = resolve24(assignmentsDir2, a.name);
14699
+ const assignmentMd = resolve24(assignmentDir, "assignment.md");
12768
14700
  const entry = {
12769
- projectDir: resolve22(projectsDir, m.name),
14701
+ projectDir: resolve24(projectsDir, m.name),
12770
14702
  projectSlug: m.name,
12771
14703
  assignmentDir,
12772
14704
  assignmentSlug: a.name,
@@ -12781,12 +14713,12 @@ async function listAssignmentsByProject(projectsDir, standaloneDir) {
12781
14713
  }
12782
14714
  }
12783
14715
  if (standaloneDir !== null && await fileExists(standaloneDir)) {
12784
- const entries = await readdir9(standaloneDir, { withFileTypes: true });
14716
+ const entries = await readdir11(standaloneDir, { withFileTypes: true });
12785
14717
  for (const a of entries) {
12786
14718
  if (!a.isDirectory()) continue;
12787
14719
  if (a.name.startsWith(".") || a.name.startsWith("_")) continue;
12788
- const assignmentDir = resolve22(standaloneDir, a.name);
12789
- const assignmentMd = resolve22(assignmentDir, "assignment.md");
14720
+ const assignmentDir = resolve24(standaloneDir, a.name);
14721
+ const assignmentMd = resolve24(assignmentDir, "assignment.md");
12790
14722
  const entry = {
12791
14723
  projectDir: standaloneDir,
12792
14724
  projectSlug: null,
@@ -12823,7 +14755,7 @@ async function scanAssignmentsByStatus(projectsDir, standaloneDir, ids) {
12823
14755
  const assignmentPath = `${entry.assignmentDir}/assignment.md`;
12824
14756
  let content;
12825
14757
  try {
12826
- content = await readFile16(assignmentPath, "utf-8");
14758
+ content = await readFile18(assignmentPath, "utf-8");
12827
14759
  } catch (err) {
12828
14760
  const code = err?.code;
12829
14761
  if (code === "ENOENT") {
@@ -12886,7 +14818,7 @@ async function applyStatusResolutions(resolutions, affected, validTargets) {
12886
14818
  const list = affected.get(r.id) ?? [];
12887
14819
  for (const a of list) {
12888
14820
  try {
12889
- const content = await readFile16(a.path, "utf-8");
14821
+ const content = await readFile18(a.path, "utf-8");
12890
14822
  buffer.set(a.path, content);
12891
14823
  } catch (err) {
12892
14824
  const code = err?.code;
@@ -12914,7 +14846,7 @@ async function applyStatusResolutions(resolutions, affected, validTargets) {
12914
14846
  for (const a of list) {
12915
14847
  let current;
12916
14848
  try {
12917
- current = await readFile16(a.path, "utf-8");
14849
+ current = await readFile18(a.path, "utf-8");
12918
14850
  } catch (err) {
12919
14851
  const code = err?.code;
12920
14852
  if (code === "ENOENT") {
@@ -12929,10 +14861,14 @@ async function applyStatusResolutions(resolutions, affected, validTargets) {
12929
14861
  );
12930
14862
  continue;
12931
14863
  }
12932
- const next = updateAssignmentFile(current, {
12933
- status: r.target,
12934
- updated: nowTimestamp()
12935
- });
14864
+ const now = nowTimestamp();
14865
+ const next = appendStatusHistoryEntry(
14866
+ updateAssignmentFile(current, {
14867
+ status: r.target,
14868
+ updated: now
14869
+ }),
14870
+ { at: now, from: r.id, to: r.target, command: "remap", by: null }
14871
+ );
12936
14872
  await writeFile5(a.path, next, "utf-8");
12937
14873
  writtenPaths.push(a.path);
12938
14874
  remapped++;
@@ -12963,7 +14899,7 @@ async function applyStatusResolutions(resolutions, affected, validTargets) {
12963
14899
  const list = affected.get(r.id) ?? [];
12964
14900
  for (const a of list) {
12965
14901
  try {
12966
- const current = await readFile16(a.path, "utf-8");
14902
+ const current = await readFile18(a.path, "utf-8");
12967
14903
  const fm = parseAssignmentFrontmatter(current);
12968
14904
  if (fm.status !== r.id) {
12969
14905
  console.warn(
@@ -12974,7 +14910,7 @@ async function applyStatusResolutions(resolutions, affected, validTargets) {
12974
14910
  } catch {
12975
14911
  continue;
12976
14912
  }
12977
- const assignmentDir = dirname5(a.path);
14913
+ const assignmentDir = dirname6(a.path);
12978
14914
  try {
12979
14915
  await rm2(assignmentDir, { recursive: true, force: true });
12980
14916
  deleted++;
@@ -13231,7 +15167,7 @@ function createStatusConfigRouter(projectsDir, assignmentsDir2) {
13231
15167
  throw err;
13232
15168
  }
13233
15169
  try {
13234
- await writeStatusConfig({ statuses, order, transitions });
15170
+ await writeStatusConfig({ statuses, order, transitions, derive: currentConfig.derive ?? null });
13235
15171
  } catch (err) {
13236
15172
  console.error("Error saving status config after applying resolutions:", err);
13237
15173
  res.status(500).json({
@@ -13285,7 +15221,7 @@ import { Router as Router10 } from "express";
13285
15221
  init_paths();
13286
15222
  import Database2 from "better-sqlite3";
13287
15223
  import { randomUUID as randomUUID3 } from "crypto";
13288
- import { resolve as resolve23 } from "path";
15224
+ import { resolve as resolve25 } from "path";
13289
15225
  var db2 = null;
13290
15226
  var LEASE_SCHEMA_VERSION = "1";
13291
15227
  var SCHEMA_SQL2 = `
@@ -13372,7 +15308,7 @@ function isBusyError(err) {
13372
15308
  }
13373
15309
  function initLeasesDb(dbPath) {
13374
15310
  if (db2) return db2;
13375
- const finalPath = dbPath ?? resolve23(syntaurRoot(), "syntaur.db");
15311
+ const finalPath = dbPath ?? resolve25(syntaurRoot(), "syntaur.db");
13376
15312
  db2 = new Database2(finalPath);
13377
15313
  db2.pragma("journal_mode = WAL");
13378
15314
  db2.pragma("busy_timeout = 5000");
@@ -13521,7 +15457,7 @@ import { Router as Router11 } from "express";
13521
15457
  // src/db/usage-db.ts
13522
15458
  init_paths();
13523
15459
  import Database3 from "better-sqlite3";
13524
- import { resolve as resolve24 } from "path";
15460
+ import { resolve as resolve26 } from "path";
13525
15461
  var db3 = null;
13526
15462
  var USAGE_SCHEMA_VERSION = "1";
13527
15463
  var SCHEMA_SQL3 = `
@@ -13578,7 +15514,7 @@ CREATE INDEX IF NOT EXISTS idx_usage_daily_day
13578
15514
  `;
13579
15515
  function initUsageDb(dbPath) {
13580
15516
  if (db3) return db3;
13581
- const finalPath = dbPath ?? resolve24(syntaurRoot(), "syntaur.db");
15517
+ const finalPath = dbPath ?? resolve26(syntaurRoot(), "syntaur.db");
13582
15518
  db3 = new Database3(finalPath);
13583
15519
  db3.pragma("journal_mode = WAL");
13584
15520
  db3.pragma("busy_timeout = 5000");
@@ -13837,8 +15773,8 @@ init_slug();
13837
15773
  init_timestamp();
13838
15774
  init_fs();
13839
15775
  import { Router as Router12 } from "express";
13840
- import { resolve as resolve25 } from "path";
13841
- import { readFile as readFile17 } from "fs/promises";
15776
+ import { resolve as resolve27 } from "path";
15777
+ import { readFile as readFile19 } from "fs/promises";
13842
15778
  init_playbooks();
13843
15779
  function statusForPlaybookError(code) {
13844
15780
  switch (code) {
@@ -13920,8 +15856,8 @@ function createPlaybooksRouter(playbooksDir2) {
13920
15856
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
13921
15857
  return;
13922
15858
  }
13923
- const filePath = resolve25(playbooksDir2, resolved.filename);
13924
- const content = await readFile17(filePath, "utf-8");
15859
+ const filePath = resolve27(playbooksDir2, resolved.filename);
15860
+ const content = await readFile19(filePath, "utf-8");
13925
15861
  res.json({
13926
15862
  documentType: "playbook",
13927
15863
  title: `Edit Playbook: ${resolved.slug}`,
@@ -13946,7 +15882,7 @@ function createPlaybooksRouter(playbooksDir2) {
13946
15882
  return;
13947
15883
  }
13948
15884
  await ensureDir(playbooksDir2);
13949
- const filePath = resolve25(playbooksDir2, `${slug}.md`);
15885
+ const filePath = resolve27(playbooksDir2, `${slug}.md`);
13950
15886
  if (await fileExists(filePath)) {
13951
15887
  res.status(409).json({ error: `Playbook "${slug}" already exists` });
13952
15888
  return;
@@ -13970,7 +15906,7 @@ function createPlaybooksRouter(playbooksDir2) {
13970
15906
  res.status(404).json({ error: `Playbook "${req.params.slug}" not found` });
13971
15907
  return;
13972
15908
  }
13973
- const filePath = resolve25(playbooksDir2, resolved.filename);
15909
+ const filePath = resolve27(playbooksDir2, resolved.filename);
13974
15910
  await writeFileForce(filePath, content);
13975
15911
  await rebuildPlaybookManifest(playbooksDir2);
13976
15912
  res.json({ slug: resolved.slug, path: filePath });
@@ -14018,8 +15954,8 @@ init_parser2();
14018
15954
  init_fs();
14019
15955
  init_paths();
14020
15956
  import { Router as Router14 } from "express";
14021
- import { readdir as readdir11 } from "fs/promises";
14022
- import { resolve as resolvePath, dirname as dirname7 } from "path";
15957
+ import { readdir as readdir13 } from "fs/promises";
15958
+ import { resolve as resolvePath, dirname as dirname8 } from "path";
14023
15959
  import { rename as rename6, mkdir as mkdir4 } from "fs/promises";
14024
15960
  init_slug();
14025
15961
 
@@ -14029,7 +15965,7 @@ init_parser2();
14029
15965
  // src/commands/create-assignment.ts
14030
15966
  init_slug();
14031
15967
  init_timestamp();
14032
- import { resolve as resolve26 } from "path";
15968
+ import { resolve as resolve28 } from "path";
14033
15969
  init_paths();
14034
15970
  init_fs();
14035
15971
  init_config2();
@@ -14107,14 +16043,14 @@ async function createAssignmentCommand(title, options) {
14107
16043
  if (options.oneOff) {
14108
16044
  const standaloneRoot = assignmentsDir();
14109
16045
  folderName = id;
14110
- assignmentDir = resolve26(standaloneRoot, folderName);
16046
+ assignmentDir = resolve28(standaloneRoot, folderName);
14111
16047
  projectSlug = null;
14112
16048
  await ensureDir(standaloneRoot);
14113
16049
  } else {
14114
16050
  const baseDir = options.dir ? expandHome(options.dir) : config.defaultProjectDir;
14115
16051
  projectSlug = options.project;
14116
- const projectDir = resolve26(baseDir, projectSlug);
14117
- const projectMdPath = resolve26(projectDir, "project.md");
16052
+ const projectDir = resolve28(baseDir, projectSlug);
16053
+ const projectMdPath = resolve28(projectDir, "project.md");
14118
16054
  if (!await fileExists(projectDir) || !await fileExists(projectMdPath)) {
14119
16055
  throw new Error(
14120
16056
  `Project "${projectSlug}" not found at ${projectDir}.
@@ -14122,9 +16058,9 @@ Run 'syntaur create-project' first or use --one-off.`
14122
16058
  );
14123
16059
  }
14124
16060
  if (dependsOn.length > 0) {
14125
- const depDirBase = resolve26(projectDir, "assignments");
16061
+ const depDirBase = resolve28(projectDir, "assignments");
14126
16062
  for (const dep of dependsOn) {
14127
- const depDir = resolve26(depDirBase, dep);
16063
+ const depDir = resolve28(depDirBase, dep);
14128
16064
  if (!await fileExists(depDir)) {
14129
16065
  console.warn(
14130
16066
  `Warning: dependency "${dep}" does not exist in project "${projectSlug}" yet.`
@@ -14133,7 +16069,7 @@ Run 'syntaur create-project' first or use --one-off.`
14133
16069
  }
14134
16070
  }
14135
16071
  folderName = assignmentSlug;
14136
- assignmentDir = resolve26(projectDir, "assignments", folderName);
16072
+ assignmentDir = resolve28(projectDir, "assignments", folderName);
14137
16073
  }
14138
16074
  if (await fileExists(assignmentDir)) {
14139
16075
  throw new Error(
@@ -14145,7 +16081,7 @@ Use --slug to specify a different slug.`
14145
16081
  const companionAssignmentRef = projectSlug === null ? id : assignmentSlug;
14146
16082
  const files = [
14147
16083
  [
14148
- resolve26(assignmentDir, "assignment.md"),
16084
+ resolve28(assignmentDir, "assignment.md"),
14149
16085
  renderAssignment({
14150
16086
  id,
14151
16087
  slug: assignmentSlug,
@@ -14163,35 +16099,35 @@ Use --slug to specify a different slug.`
14163
16099
  })
14164
16100
  ],
14165
16101
  [
14166
- resolve26(assignmentDir, "scratchpad.md"),
16102
+ resolve28(assignmentDir, "scratchpad.md"),
14167
16103
  renderScratchpad({
14168
16104
  assignmentSlug: companionAssignmentRef,
14169
16105
  timestamp
14170
16106
  })
14171
16107
  ],
14172
16108
  [
14173
- resolve26(assignmentDir, "handoff.md"),
16109
+ resolve28(assignmentDir, "handoff.md"),
14174
16110
  renderHandoff({
14175
16111
  assignmentSlug: companionAssignmentRef,
14176
16112
  timestamp
14177
16113
  })
14178
16114
  ],
14179
16115
  [
14180
- resolve26(assignmentDir, "decision-record.md"),
16116
+ resolve28(assignmentDir, "decision-record.md"),
14181
16117
  renderDecisionRecord({
14182
16118
  assignmentSlug: companionAssignmentRef,
14183
16119
  timestamp
14184
16120
  })
14185
16121
  ],
14186
16122
  [
14187
- resolve26(assignmentDir, "progress.md"),
16123
+ resolve28(assignmentDir, "progress.md"),
14188
16124
  renderProgress({
14189
16125
  assignment: companionAssignmentRef,
14190
16126
  timestamp
14191
16127
  })
14192
16128
  ],
14193
16129
  [
14194
- resolve26(assignmentDir, "comments.md"),
16130
+ resolve28(assignmentDir, "comments.md"),
14195
16131
  renderComments({
14196
16132
  assignment: companionAssignmentRef,
14197
16133
  timestamp
@@ -14364,8 +16300,8 @@ init_api();
14364
16300
  import { raw } from "express";
14365
16301
 
14366
16302
  // src/todos/attachments.ts
14367
- import { mkdir as mkdir3, readdir as readdir10, stat, rename as rename5, rm as rm3, unlink as unlink5, writeFile as writeFile6, cp } from "fs/promises";
14368
- import { resolve as resolve27, basename as basename5, dirname as dirname6, extname } from "path";
16303
+ import { mkdir as mkdir3, readdir as readdir12, stat as stat2, rename as rename5, rm as rm3, unlink as unlink6, writeFile as writeFile6, cp } from "fs/promises";
16304
+ import { resolve as resolve29, basename as basename5, dirname as dirname7, extname } from "path";
14369
16305
 
14370
16306
  // src/utils/proof-artifact-id.ts
14371
16307
  import { randomBytes as randomBytes2 } from "crypto";
@@ -14452,16 +16388,16 @@ function sanitizeAttachmentName(name) {
14452
16388
  return n;
14453
16389
  }
14454
16390
  function attachmentsRootDir(todosDir2) {
14455
- return resolve27(todosDir2, "attachments");
16391
+ return resolve29(todosDir2, "attachments");
14456
16392
  }
14457
16393
  function attachmentDirFor(todosDir2, scopeId, todoId) {
14458
16394
  assertScope(scopeId);
14459
16395
  assertTodoId(todoId);
14460
- return resolve27(attachmentsRootDir(todosDir2), scopeId, todoId);
16396
+ return resolve29(attachmentsRootDir(todosDir2), scopeId, todoId);
14461
16397
  }
14462
16398
  async function dirExists(p) {
14463
16399
  try {
14464
- return (await stat(p)).isDirectory();
16400
+ return (await stat2(p)).isDirectory();
14465
16401
  } catch {
14466
16402
  return false;
14467
16403
  }
@@ -14471,7 +16407,7 @@ async function writeAttachment(todosDir2, scopeId, todoId, originalName, bytes)
14471
16407
  await mkdir3(dir, { recursive: true });
14472
16408
  const id = generateArtifactId();
14473
16409
  const filename = sanitizeAttachmentName(originalName);
14474
- await writeFile6(resolve27(dir, `${id}__${filename}`), bytes);
16410
+ await writeFile6(resolve29(dir, `${id}__${filename}`), bytes);
14475
16411
  return {
14476
16412
  id,
14477
16413
  filename,
@@ -14484,7 +16420,7 @@ async function listAttachments(todosDir2, scopeId, todoId) {
14484
16420
  const dir = attachmentDirFor(todosDir2, scopeId, todoId);
14485
16421
  let names;
14486
16422
  try {
14487
- names = await readdir10(dir);
16423
+ names = await readdir12(dir);
14488
16424
  } catch {
14489
16425
  return [];
14490
16426
  }
@@ -14496,7 +16432,7 @@ async function listAttachments(todosDir2, scopeId, todoId) {
14496
16432
  if (!ATTACHMENT_ID_RE.test(id)) continue;
14497
16433
  const filename = stored.slice(sep2 + 2);
14498
16434
  try {
14499
- const st = await stat(resolve27(dir, stored));
16435
+ const st = await stat2(resolve29(dir, stored));
14500
16436
  if (!st.isFile()) continue;
14501
16437
  out.push({ id, filename, mime: mimeForName(filename), size: st.size, createdAt: st.mtime.toISOString() });
14502
16438
  } catch {
@@ -14507,10 +16443,10 @@ async function listAttachments(todosDir2, scopeId, todoId) {
14507
16443
  }
14508
16444
  async function readScopeAttachments(todosDir2, scopeId) {
14509
16445
  assertScope(scopeId);
14510
- const scopeDir = resolve27(attachmentsRootDir(todosDir2), scopeId);
16446
+ const scopeDir = resolve29(attachmentsRootDir(todosDir2), scopeId);
14511
16447
  let todoIds;
14512
16448
  try {
14513
- todoIds = await readdir10(scopeDir);
16449
+ todoIds = await readdir12(scopeDir);
14514
16450
  } catch {
14515
16451
  return {};
14516
16452
  }
@@ -14527,7 +16463,7 @@ async function resolveAttachmentFile(todosDir2, scopeId, todoId, attachmentId) {
14527
16463
  const dir = attachmentDirFor(todosDir2, scopeId, todoId);
14528
16464
  let names;
14529
16465
  try {
14530
- names = await readdir10(dir);
16466
+ names = await readdir12(dir);
14531
16467
  } catch {
14532
16468
  return null;
14533
16469
  }
@@ -14535,12 +16471,12 @@ async function resolveAttachmentFile(todosDir2, scopeId, todoId, attachmentId) {
14535
16471
  const stored = names.find((n) => n.startsWith(prefix));
14536
16472
  if (!stored) return null;
14537
16473
  const filename = stored.slice(prefix.length);
14538
- return { path: resolve27(dir, stored), filename, mime: mimeForName(filename) };
16474
+ return { path: resolve29(dir, stored), filename, mime: mimeForName(filename) };
14539
16475
  }
14540
16476
  async function deleteAttachment(todosDir2, scopeId, todoId, attachmentId) {
14541
16477
  const resolved = await resolveAttachmentFile(todosDir2, scopeId, todoId, attachmentId);
14542
16478
  if (!resolved) return false;
14543
- await unlink5(resolved.path);
16479
+ await unlink6(resolved.path);
14544
16480
  return true;
14545
16481
  }
14546
16482
  async function deleteAllAttachments(todosDir2, scopeId, todoId) {
@@ -14555,7 +16491,7 @@ async function moveAttachments(srcTodosDir, srcScopeId, dstTodosDir, dstScopeId,
14555
16491
  const src = attachmentDirFor(srcTodosDir, srcScopeId, todoId);
14556
16492
  if (!await dirExists(src)) return;
14557
16493
  const dst = attachmentDirFor(dstTodosDir, dstScopeId, todoId);
14558
- await mkdir3(dirname6(dst), { recursive: true });
16494
+ await mkdir3(dirname7(dst), { recursive: true });
14559
16495
  try {
14560
16496
  await rename5(src, dst);
14561
16497
  } catch (err) {
@@ -14831,7 +16767,7 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
14831
16767
  router.get("/", async (_req, res) => {
14832
16768
  try {
14833
16769
  await ensureDir(todosDir2);
14834
- const files = await readdir11(todosDir2).catch(() => []);
16770
+ const files = await readdir13(todosDir2).catch(() => []);
14835
16771
  const workspaces = [];
14836
16772
  for (const file of files) {
14837
16773
  if (typeof file !== "string") continue;
@@ -14947,8 +16883,8 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
14947
16883
  router.post("/:workspace/archive", async (req, res) => {
14948
16884
  try {
14949
16885
  const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
14950
- const { resolve: resolve32 } = await import("path");
14951
- const { readFile: readFile21 } = await import("fs/promises");
16886
+ const { resolve: resolve34 } = await import("path");
16887
+ const { readFile: readFile23 } = await import("fs/promises");
14952
16888
  const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
14953
16889
  const workspace = getWorkspaceParam(req.params.workspace);
14954
16890
  const outcome = await wsLock(workspace, async () => {
@@ -14964,10 +16900,10 @@ function createTodosRouter(todosDir2, broadcast, projectsDir) {
14964
16900
  (e) => e.itemIds.every((id) => completedIds.has(id))
14965
16901
  );
14966
16902
  const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
14967
- await ensureDir(resolve32(todosDir2, "archive"));
16903
+ await ensureDir(resolve34(todosDir2, "archive"));
14968
16904
  let archContent = "";
14969
16905
  if (await fileExists(archFile)) {
14970
- archContent = await readFile21(archFile, "utf-8");
16906
+ archContent = await readFile23(archFile, "utf-8");
14971
16907
  archContent = archContent.trimEnd() + "\n\n";
14972
16908
  } else {
14973
16909
  archContent = `---
@@ -15256,7 +17192,7 @@ workspace: ${workspace}
15256
17192
  const { readConfig: readConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
15257
17193
  const { assignmentsDir: assignmentsDirFn } = await Promise.resolve().then(() => (init_paths(), paths_exports));
15258
17194
  const { fileExists: fileExists2, writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
15259
- const { readFile: readFile21 } = await import("fs/promises");
17195
+ const { readFile: readFile23 } = await import("fs/promises");
15260
17196
  const { appendTodosToAssignmentBody: appendTodosToAssignmentBody2, touchAssignmentUpdated: touchAssignmentUpdated2 } = await Promise.resolve().then(() => (init_assignment_todos(), assignment_todos_exports));
15261
17197
  const { nowTimestamp: nowTimestamp3 } = await Promise.resolve().then(() => (init_timestamp(), timestamp_exports));
15262
17198
  let assignmentRef;
@@ -15277,7 +17213,7 @@ workspace: ${workspace}
15277
17213
  }
15278
17214
  const assignmentMdPath = resolvePath2(assignmentDir, "assignment.md");
15279
17215
  if (!await fileExists2(assignmentMdPath)) return { error: `Target assignment not found: ${assignmentMdPath}` };
15280
- let content = await readFile21(assignmentMdPath, "utf-8");
17216
+ let content = await readFile23(assignmentMdPath, "utf-8");
15281
17217
  content = appendTodosToAssignmentBody2(
15282
17218
  content,
15283
17219
  items.map((it) => ({
@@ -15394,7 +17330,7 @@ workspace: ${workspace}
15394
17330
  return { status: 409, error: "attachments already exist in target" };
15395
17331
  }
15396
17332
  if (item.planDir && newPlanDir) {
15397
- await mkdir4(dirname7(newPlanDir), { recursive: true });
17333
+ await mkdir4(dirname8(newPlanDir), { recursive: true });
15398
17334
  await rename6(item.planDir, newPlanDir);
15399
17335
  item.planDir = newPlanDir;
15400
17336
  }
@@ -15472,8 +17408,8 @@ init_fs();
15472
17408
  init_paths();
15473
17409
  init_slug();
15474
17410
  import { Router as Router15 } from "express";
15475
- import { mkdir as mkdir5, readFile as readFile18, rename as rename7 } from "fs/promises";
15476
- import { resolve as resolve28, dirname as dirname8 } from "path";
17411
+ import { mkdir as mkdir5, readFile as readFile20, rename as rename7 } from "fs/promises";
17412
+ import { resolve as resolve30, dirname as dirname9 } from "path";
15477
17413
  init_api();
15478
17414
  var WORKSPACE_REGEX2 = /^[a-z0-9_][a-z0-9-]*$/;
15479
17415
  function touchItem4(item) {
@@ -15489,7 +17425,7 @@ function params(req) {
15489
17425
  return req.params;
15490
17426
  }
15491
17427
  async function projectExists(projectsDir, slug) {
15492
- return fileExists(resolve28(projectsDir, slug, "project.md"));
17428
+ return fileExists(resolve30(projectsDir, slug, "project.md"));
15493
17429
  }
15494
17430
  async function ensureProjectTodosDir(projectsDir, slug) {
15495
17431
  const todosDir2 = projectTodosDir(projectsDir, slug);
@@ -15506,7 +17442,7 @@ async function ensureProjectTodosDir(projectsDir, slug) {
15506
17442
  throw err;
15507
17443
  }
15508
17444
  try {
15509
- await mkdir5(resolve28(todosDir2, "archive"), { recursive: false });
17445
+ await mkdir5(resolve30(todosDir2, "archive"), { recursive: false });
15510
17446
  } catch (err) {
15511
17447
  const code = err.code;
15512
17448
  if (code === "EEXIST") return;
@@ -15710,7 +17646,7 @@ function createProjectTodosRouter(projectsDir, broadcast, workspaceTodosDir) {
15710
17646
  const archFile = archivePath(todosDir2, slug, checklist.archiveInterval);
15711
17647
  let archContent = "";
15712
17648
  if (await fileExists(archFile)) {
15713
- archContent = await readFile18(archFile, "utf-8");
17649
+ archContent = await readFile20(archFile, "utf-8");
15714
17650
  archContent = archContent.trimEnd() + "\n\n";
15715
17651
  } else {
15716
17652
  archContent = `---
@@ -16169,17 +18105,17 @@ workspace: ${slug}
16169
18105
  if (tg.includes("/")) {
16170
18106
  const parts = tg.split("/");
16171
18107
  if (parts.length !== 2) return { error: `Invalid target.assignment "${tg}"` };
16172
- assignmentDir = resolve28(projectsDir, parts[0], "assignments", parts[1]);
18108
+ assignmentDir = resolve30(projectsDir, parts[0], "assignments", parts[1]);
16173
18109
  assignmentRef = `${parts[0]}/${parts[1]}`;
16174
18110
  } else if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(tg)) {
16175
- assignmentDir = resolve28(assignmentsDirFn(), tg);
18111
+ assignmentDir = resolve30(assignmentsDirFn(), tg);
16176
18112
  assignmentRef = tg;
16177
18113
  } else {
16178
18114
  return { error: `Invalid target.assignment "${tg}"` };
16179
18115
  }
16180
- const assignmentMdPath = resolve28(assignmentDir, "assignment.md");
18116
+ const assignmentMdPath = resolve30(assignmentDir, "assignment.md");
16181
18117
  if (!await fileExists(assignmentMdPath)) return { error: `Target assignment not found: ${assignmentMdPath}` };
16182
- let content = await readFile18(assignmentMdPath, "utf-8");
18118
+ let content = await readFile20(assignmentMdPath, "utf-8");
16183
18119
  content = appendTodosToAssignmentBody2(
16184
18120
  content,
16185
18121
  items.map((it) => ({
@@ -16318,7 +18254,7 @@ workspace: ${slug}
16318
18254
  return { status: 409, error: "attachments already exist in target" };
16319
18255
  }
16320
18256
  if (item.planDir && newPlanDir) {
16321
- await mkdir5(dirname8(newPlanDir), { recursive: true });
18257
+ await mkdir5(dirname9(newPlanDir), { recursive: true });
16322
18258
  await rename7(item.planDir, newPlanDir);
16323
18259
  item.planDir = newPlanDir;
16324
18260
  }
@@ -16382,7 +18318,7 @@ workspace: ${slug}
16382
18318
 
16383
18319
  // src/dashboard/api-bundles.ts
16384
18320
  import { Router as Router16 } from "express";
16385
- import { readdir as readdir12 } from "fs/promises";
18321
+ import { readdir as readdir14 } from "fs/promises";
16386
18322
 
16387
18323
  // src/todos/bundle-parser.ts
16388
18324
  init_parser();
@@ -16390,7 +18326,7 @@ init_fs();
16390
18326
  init_paths();
16391
18327
  init_parser2();
16392
18328
  import { randomBytes as randomBytes3 } from "crypto";
16393
- import { readFile as readFile19 } from "fs/promises";
18329
+ import { readFile as readFile21 } from "fs/promises";
16394
18330
  var BUNDLE_ID_REGEX = /^[a-f0-9]{4}$/;
16395
18331
  var SCOPE_VALUES = /* @__PURE__ */ new Set(["workspace", "project", "global"]);
16396
18332
  var SCOPE_ID_REGEX = /^[a-z0-9_][a-z0-9_-]*$/;
@@ -16457,7 +18393,7 @@ function parseBundles(content) {
16457
18393
  async function readBundles(todosDir2) {
16458
18394
  const path = bundlesPath(todosDir2);
16459
18395
  if (!await fileExists(path)) return [];
16460
- const content = await readFile19(path, "utf-8");
18396
+ const content = await readFile21(path, "utf-8");
16461
18397
  return parseBundles(content).bundles;
16462
18398
  }
16463
18399
 
@@ -16506,7 +18442,7 @@ function createBundlesRouter(todosDir2, broadcast) {
16506
18442
  try {
16507
18443
  await ensureDir(todosDir2);
16508
18444
  const bundles = await readBundles(todosDir2);
16509
- const workspaceFiles = await readdir12(todosDir2).catch(() => []);
18445
+ const workspaceFiles = await readdir14(todosDir2).catch(() => []);
16510
18446
  const itemsByKey = /* @__PURE__ */ new Map();
16511
18447
  for (const f of workspaceFiles) {
16512
18448
  if (typeof f !== "string") continue;
@@ -16559,7 +18495,7 @@ init_fs();
16559
18495
  init_paths();
16560
18496
  init_slug();
16561
18497
  import { Router as Router17 } from "express";
16562
- import { resolve as resolve29 } from "path";
18498
+ import { resolve as resolve31 } from "path";
16563
18499
  init_parser2();
16564
18500
  function deriveStatus2(bundle, items) {
16565
18501
  const members = bundle.todoIds.map((id) => items.find((i) => i.id === id)).filter((i) => i !== void 0);
@@ -16601,7 +18537,7 @@ function createProjectBundlesRouter(projectsDir, broadcast) {
16601
18537
  router.get("/", async (req, res) => {
16602
18538
  try {
16603
18539
  const slug = getProjectIdParam2(req.params.projectId);
16604
- const projectMd = resolve29(projectsDir, slug, "project.md");
18540
+ const projectMd = resolve31(projectsDir, slug, "project.md");
16605
18541
  if (!await fileExists(projectMd)) {
16606
18542
  notFound2(res, slug);
16607
18543
  return;
@@ -16630,8 +18566,8 @@ init_fs();
16630
18566
  init_config2();
16631
18567
  import { execFile as execFile2 } from "child_process";
16632
18568
  import { promisify as promisify2 } from "util";
16633
- import { cp as cp2, mkdtemp, rm as rm4, readFile as readFile20, writeFile as writeFile7, unlink as unlink6, stat as stat2, open as open2, rename as rename8 } from "fs/promises";
16634
- import { resolve as resolve30, join as join4 } from "path";
18569
+ import { cp as cp2, mkdtemp, rm as rm4, readFile as readFile22, writeFile as writeFile7, unlink as unlink7, stat as stat3, open as open3, rename as rename8 } from "fs/promises";
18570
+ import { resolve as resolve32, join as join4 } from "path";
16635
18571
  import { tmpdir } from "os";
16636
18572
  var exec2 = promisify2(execFile2);
16637
18573
  var VALID_CATEGORIES = ["projects", "playbooks", "todos", "servers", "config"];
@@ -16671,7 +18607,7 @@ async function resolveCategoryPath(category) {
16671
18607
  case "servers":
16672
18608
  return { sourcePath: serversDir(), repoPath: "servers", isFile: false };
16673
18609
  case "config":
16674
- return { sourcePath: resolve30(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
18610
+ return { sourcePath: resolve32(syntaurRoot(), "config.md"), repoPath: "config.md", isFile: true };
16675
18611
  }
16676
18612
  }
16677
18613
  async function checkGitInstalled() {
@@ -16681,17 +18617,17 @@ async function checkGitInstalled() {
16681
18617
  throw new Error("git is not installed or not on PATH. Install git and try again.");
16682
18618
  }
16683
18619
  }
16684
- async function acquireLock() {
16685
- const lockPath = resolve30(syntaurRoot(), LOCK_FILE_NAME);
18620
+ async function acquireLock2() {
18621
+ const lockPath = resolve32(syntaurRoot(), LOCK_FILE_NAME);
16686
18622
  await ensureDir(syntaurRoot());
16687
18623
  try {
16688
- const handle = await open2(lockPath, "wx");
18624
+ const handle = await open3(lockPath, "wx");
16689
18625
  await handle.write(String(process.pid));
16690
18626
  await handle.close();
16691
18627
  return lockPath;
16692
18628
  } catch (err) {
16693
18629
  if (err.code === "EEXIST") {
16694
- const pid = await readFile20(lockPath, "utf-8").catch(() => "");
18630
+ const pid = await readFile22(lockPath, "utf-8").catch(() => "");
16695
18631
  throw new Error(
16696
18632
  `Backup operation already in progress (lock file at ${lockPath}, pid ${pid.trim() || "unknown"}). If stale, delete the file and retry.`
16697
18633
  );
@@ -16701,7 +18637,7 @@ async function acquireLock() {
16701
18637
  }
16702
18638
  async function releaseLock(lockPath) {
16703
18639
  try {
16704
- await unlink6(lockPath);
18640
+ await unlink7(lockPath);
16705
18641
  } catch {
16706
18642
  }
16707
18643
  }
@@ -16724,12 +18660,12 @@ async function cloneOrInit(repoUrl, destDir) {
16724
18660
  }
16725
18661
  async function copyRecursive(src, dest) {
16726
18662
  if (!await fileExists(src)) return;
16727
- const s = await stat2(src);
18663
+ const s = await stat3(src);
16728
18664
  if (s.isDirectory()) {
16729
18665
  await ensureDir(dest);
16730
18666
  await cp2(src, dest, { recursive: true, force: true });
16731
18667
  } else {
16732
- await ensureDir(resolve30(dest, ".."));
18668
+ await ensureDir(resolve32(dest, ".."));
16733
18669
  await cp2(src, dest, { force: true });
16734
18670
  }
16735
18671
  }
@@ -16738,7 +18674,7 @@ function resolveCategoriesStrict(csv) {
16738
18674
  return parseCategoriesStrict(parts);
16739
18675
  }
16740
18676
  async function readSanitizedConfig(configPath) {
16741
- const content = await readFile20(configPath, "utf-8");
18677
+ const content = await readFile22(configPath, "utf-8");
16742
18678
  return content.replace(/^(\s*lastBackup:\s*).*$/m, "$1null").replace(/^(\s*lastRestore:\s*).*$/m, "$1null");
16743
18679
  }
16744
18680
  async function backupToGithub(overrides) {
@@ -16757,7 +18693,7 @@ async function backupToGithub(overrides) {
16757
18693
  if (categories.length === 0) {
16758
18694
  throw new Error("No valid backup categories selected.");
16759
18695
  }
16760
- const lockPath = await acquireLock();
18696
+ const lockPath = await acquireLock2();
16761
18697
  let tmpDir = null;
16762
18698
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
16763
18699
  try {
@@ -16777,7 +18713,7 @@ async function backupToGithub(overrides) {
16777
18713
  }
16778
18714
  if (category === "config") {
16779
18715
  const sanitized = await readSanitizedConfig(sourcePath);
16780
- await ensureDir(resolve30(destPath, ".."));
18716
+ await ensureDir(resolve32(destPath, ".."));
16781
18717
  await writeFile7(destPath, sanitized, "utf-8");
16782
18718
  } else {
16783
18719
  await copyRecursive(sourcePath, destPath);
@@ -16831,7 +18767,7 @@ async function backupToGithub(overrides) {
16831
18767
  }
16832
18768
  async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
16833
18769
  if (isFile) {
16834
- await ensureDir(resolve30(localPath, ".."));
18770
+ await ensureDir(resolve32(localPath, ".."));
16835
18771
  await cp2(repoSrcPath, localPath, { force: true });
16836
18772
  return;
16837
18773
  }
@@ -16886,7 +18822,7 @@ async function restoreFromGithub(overrides) {
16886
18822
  if (categories.length === 0) {
16887
18823
  throw new Error("No valid restore categories selected.");
16888
18824
  }
16889
- const lockPath = await acquireLock();
18825
+ const lockPath = await acquireLock2();
16890
18826
  let tmpDir = null;
16891
18827
  const restored = [];
16892
18828
  const failed = [];
@@ -16932,7 +18868,7 @@ async function restoreFromGithub(overrides) {
16932
18868
  }
16933
18869
  async function getBackupStatus() {
16934
18870
  const config = await readConfig();
16935
- const lockPath = resolve30(syntaurRoot(), LOCK_FILE_NAME);
18871
+ const lockPath = resolve32(syntaurRoot(), LOCK_FILE_NAME);
16936
18872
  const locked = await fileExists(lockPath);
16937
18873
  return {
16938
18874
  repo: config.backup?.repo ?? null,
@@ -17280,7 +19216,7 @@ function createDashboardServer(options) {
17280
19216
  (async () => {
17281
19217
  try {
17282
19218
  const configResult = await migrateLegacyConfig(
17283
- resolve31(syntaurRoot(), "config.md")
19219
+ resolve33(syntaurRoot(), "config.md")
17284
19220
  );
17285
19221
  const projectResult = await migrateLegacyProjectFiles(projectsDir);
17286
19222
  const summary = summarizeMigration(projectResult, configResult);
@@ -17798,14 +19734,14 @@ function createDashboardServer(options) {
17798
19734
  app.use("/api/backup", createBackupRouter());
17799
19735
  if (serveStaticUi && dashboardDistPath) {
17800
19736
  const sendOpts = { dotfiles: "allow" };
17801
- app.use("/assets", express.static(resolve31(dashboardDistPath, "assets"), sendOpts));
19737
+ app.use("/assets", express.static(resolve33(dashboardDistPath, "assets"), sendOpts));
17802
19738
  app.use(express.static(dashboardDistPath, { ...sendOpts, index: false, fallthrough: true }));
17803
19739
  app.get("{*path}", async (req, res) => {
17804
19740
  if (req.path.startsWith("/api") || req.path === "/ws" || req.path.startsWith("/assets")) {
17805
19741
  res.status(404).json({ error: "Not Found" });
17806
19742
  return;
17807
19743
  }
17808
- const indexPath = resolve31(dashboardDistPath, "index.html");
19744
+ const indexPath = resolve33(dashboardDistPath, "index.html");
17809
19745
  if (!await fileExists(indexPath)) {
17810
19746
  res.status(503).send(
17811
19747
  'Dashboard not built. Run "npm run build:dashboard" first.'
@@ -17823,15 +19759,71 @@ function createDashboardServer(options) {
17823
19759
  let watcherHandle = null;
17824
19760
  return {
17825
19761
  async start() {
19762
+ const { recomputeAndWrite: recomputeAndWrite2, recomputeAll: recomputeAll2, resolveDeriveContext: resolveDeriveContext2, isDeriveMigrated: isDeriveMigrated2 } = await Promise.resolve().then(() => (init_recompute(), recompute_exports));
19763
+ let warnedMigrationPending = false;
19764
+ const migrationGate = async () => {
19765
+ if (await isDeriveMigrated2()) return true;
19766
+ if (!warnedMigrationPending) {
19767
+ warnedMigrationPending = true;
19768
+ console.log(
19769
+ "derived-status: migration pending \u2014 implicit recomputes are dormant until `syntaur migrate-derive` runs."
19770
+ );
19771
+ }
19772
+ return false;
19773
+ };
19774
+ const recomputeOne = async (projectSlug, assignmentSlug) => {
19775
+ if (!await migrationGate()) return;
19776
+ try {
19777
+ const context = await resolveDeriveContext2();
19778
+ const projectDir = projectSlug ? resolve33(projectsDir, projectSlug) : null;
19779
+ const path = projectDir ? resolve33(projectDir, "assignments", assignmentSlug, "assignment.md") : resolve33(assignmentsDir2, assignmentSlug, "assignment.md");
19780
+ if (!await fileExists(path)) return;
19781
+ const result = await recomputeAndWrite2(path, {
19782
+ cause: "derive",
19783
+ by: "system",
19784
+ projectDir,
19785
+ context
19786
+ });
19787
+ if (result.warning) console.warn(result.warning);
19788
+ } catch (err) {
19789
+ console.error(`derive recompute failed for ${projectSlug ?? ""}/${assignmentSlug}:`, err);
19790
+ }
19791
+ };
19792
+ const sweepAll = async (cause) => {
19793
+ if (!await migrationGate()) return;
19794
+ try {
19795
+ const context = await resolveDeriveContext2();
19796
+ const summary = await recomputeAll2(projectsDir, assignmentsDir2, {
19797
+ cause,
19798
+ by: "system",
19799
+ context
19800
+ });
19801
+ if (summary.changed > 0) {
19802
+ console.log(`derive ${cause}: ${summary.changed}/${summary.scanned} assignment(s) re-derived.`);
19803
+ }
19804
+ for (const w of summary.warnings) console.warn(w);
19805
+ } catch (err) {
19806
+ console.error(`derive ${cause} sweep failed:`, err);
19807
+ }
19808
+ };
17826
19809
  watcherHandle = createWatcher({
17827
19810
  projectsDir,
17828
19811
  assignmentsDir: assignmentsDir2,
17829
19812
  serversDir: serversDir2,
17830
19813
  playbooksDir: playbooksDir2,
17831
19814
  todosDir: todosDir2,
17832
- dbPath: resolve31(syntaurRoot(), "syntaur.db"),
17833
- onMessage: broadcast
19815
+ dbPath: resolve33(syntaurRoot(), "syntaur.db"),
19816
+ configPath: resolve33(syntaurRoot(), "config.md"),
19817
+ onMessage: broadcast,
19818
+ onAssignmentChanged: (projectSlug, assignmentSlug) => {
19819
+ void recomputeOne(projectSlug, assignmentSlug);
19820
+ },
19821
+ onConfigChanged: () => {
19822
+ clearStatusConfigCache();
19823
+ void sweepAll("config-change");
19824
+ }
17834
19825
  });
19826
+ void sweepAll("boot-reconcile");
17835
19827
  startAutodiscovery({ serversDir: serversDir2, projectsDir, assignmentsDir: assignmentsDir2, excludePids: /* @__PURE__ */ new Set([process.pid]) });
17836
19828
  return new Promise((resolvePromise, reject) => {
17837
19829
  server.on("error", (err) => {
@@ -17844,7 +19836,7 @@ function createDashboardServer(options) {
17844
19836
  }
17845
19837
  });
17846
19838
  server.listen(port, () => {
17847
- const portFile = resolve31(syntaurRoot(), "dashboard-port");
19839
+ const portFile = resolve33(syntaurRoot(), "dashboard-port");
17848
19840
  writeFile8(portFile, String(port), "utf-8").catch(() => {
17849
19841
  });
17850
19842
  resolvePromise();
@@ -17863,8 +19855,8 @@ function createDashboardServer(options) {
17863
19855
  client.terminate();
17864
19856
  }
17865
19857
  clients.clear();
17866
- const portFile = resolve31(syntaurRoot(), "dashboard-port");
17867
- await unlink7(portFile).catch(() => {
19858
+ const portFile = resolve33(syntaurRoot(), "dashboard-port");
19859
+ await unlink8(portFile).catch(() => {
17868
19860
  });
17869
19861
  server.closeAllConnections?.();
17870
19862
  return new Promise((resolvePromise) => {