syntaur 0.8.0 → 0.12.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 (126) hide show
  1. package/.claude-plugin/plugin.json +9 -1
  2. package/dashboard/dist/assets/{_basePickBy-CIZortGD.js → _basePickBy-XOV2F7dX.js} +1 -1
  3. package/dashboard/dist/assets/{_baseUniq-BHP5NBQ7.js → _baseUniq-Dccfk3fK.js} +1 -1
  4. package/dashboard/dist/assets/{arc-BLtQy7Cl.js → arc-TwJ5RF-H.js} +1 -1
  5. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-0mhIBjWy.js → architectureDiagram-2XIMDMQ5-CpkVJudw.js} +1 -1
  6. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-CM7K5Tf5.js → blockDiagram-WCTKOSBZ-Ck6VQ4Fh.js} +1 -1
  7. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-gsbeygvS.js → c4Diagram-IC4MRINW-B5V7YG03.js} +1 -1
  8. package/dashboard/dist/assets/channel-AEMTX70R.js +1 -0
  9. package/dashboard/dist/assets/{chunk-4BX2VUAB-CneRz9Nd.js → chunk-4BX2VUAB-rBdyOxAe.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-55IACEB6-uetANWHd.js → chunk-55IACEB6-DKlNXjYt.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-FMBD7UC4-3-tNszoF.js → chunk-FMBD7UC4-Cqm0PtUI.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-JSJVCQXG-DOO88rZD.js → chunk-JSJVCQXG-CQyXQcG3.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-KX2RTZJC-Bnc1CFy2.js → chunk-KX2RTZJC-DmqjHPpJ.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-NQ4KR5QH-BfViC1Ms.js → chunk-NQ4KR5QH-CHXoBcYa.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-QZHKN3VN-CgjyAt2l.js → chunk-QZHKN3VN-Dd4sChBS.js} +1 -1
  16. package/dashboard/dist/assets/{chunk-WL4C6EOR-BYFermSR.js → chunk-WL4C6EOR-D-QLnkY8.js} +1 -1
  17. package/dashboard/dist/assets/classDiagram-VBA2DB6C-LzuOAGzu.js +1 -0
  18. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-LzuOAGzu.js +1 -0
  19. package/dashboard/dist/assets/clone-Cw63QGUJ.js +1 -0
  20. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-CE-k5SLv.js → cose-bilkent-S5V4N54A-VZi6uV56.js} +1 -1
  21. package/dashboard/dist/assets/{dagre-KLK3FWXG-CX7Br8LQ.js → dagre-KLK3FWXG-CtT97Qhx.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-E7M64L7V-t-xhWjCU.js → diagram-E7M64L7V-BjYTNNHb.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-IFDJBPK2-BVhcTXsJ.js → diagram-IFDJBPK2-qj8_r6fb.js} +1 -1
  24. package/dashboard/dist/assets/{diagram-P4PSJMXO-fxt8ZSca.js → diagram-P4PSJMXO-DlfWo7Vt.js} +1 -1
  25. package/dashboard/dist/assets/{erDiagram-INFDFZHY-C20k2bxX.js → erDiagram-INFDFZHY-Ch_nApTv.js} +1 -1
  26. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-CdeygexM.js → flowDiagram-PKNHOUZH-EOVsqNyi.js} +1 -1
  27. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-CPsoOAV-.js → ganttDiagram-A5KZAMGK-CS-lx0B-.js} +1 -1
  28. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-t0tCTIZy.js → gitGraphDiagram-K3NZZRJ6-Ba6RDzPy.js} +1 -1
  29. package/dashboard/dist/assets/{graph-Bcqd31wL.js → graph-BKaAyLk-.js} +1 -1
  30. package/dashboard/dist/assets/index-BTWPZaIA.css +1 -0
  31. package/dashboard/dist/assets/index-Dk9PBm5B.js +510 -0
  32. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-DgoKvH3N.js → infoDiagram-LFFYTUFH-Cr20xEsd.js} +1 -1
  33. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-CGr4P8do.js → ishikawaDiagram-PHBUUO56-CuILV9ph.js} +1 -1
  34. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-CHYRi6wF.js → journeyDiagram-4ABVD52K-CO-l7Mpm.js} +1 -1
  35. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-kwzqrFcW.js → kanban-definition-K7BYSVSG-CcirPw_S.js} +1 -1
  36. package/dashboard/dist/assets/{layout-wud47qGs.js → layout-BXvr8dMw.js} +1 -1
  37. package/dashboard/dist/assets/{linear-DG5js90X.js → linear-B85hgYnX.js} +1 -1
  38. package/dashboard/dist/assets/{mermaid.core-BDw69uC2.js → mermaid.core-DcZN9Xp2.js} +4 -4
  39. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-Dw_uyg1-.js → mindmap-definition-YRQLILUH-Bxi6p5_3.js} +1 -1
  40. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-C7NBi6tK.js → pieDiagram-SKSYHLDU-DOSvU0UD.js} +1 -1
  41. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-_N_BSBd4.js → quadrantDiagram-337W2JSQ-Cmo-5tW9.js} +1 -1
  42. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-BqUiqfP1.js → requirementDiagram-Z7DCOOCP-Dw8alwuI.js} +1 -1
  43. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-C2Qq5ULW.js → sankeyDiagram-WA2Y5GQK-ZPB-rzOg.js} +1 -1
  44. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-BOCF2z8f.js → sequenceDiagram-2WXFIKYE-Ct8bkK2h.js} +1 -1
  45. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-BvDyTxC5.js → stateDiagram-RAJIS63D-DnGOXlmx.js} +1 -1
  46. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-COGzlZxn.js +1 -0
  47. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-DNNZxSrG.js → timeline-definition-YZTLITO2-AUFmbJjr.js} +1 -1
  48. package/dashboard/dist/assets/{treemap-KZPCXAKY-C6hOdiGH.js → treemap-KZPCXAKY-Dw5FDFAV.js} +1 -1
  49. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-FmXl6puP.js → vennDiagram-LZ73GAT5-DfxicU4N.js} +1 -1
  50. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-Bjav12WG.js → xychartDiagram-JWTSCODW--9N0V1r_.js} +1 -1
  51. package/dashboard/dist/index.html +2 -2
  52. package/dashboard/dist/syntaur-logo.svg +5 -13
  53. package/dist/dashboard/server.js +363 -3
  54. package/dist/dashboard/server.js.map +1 -1
  55. package/dist/index.js +2140 -153
  56. package/dist/index.js.map +1 -1
  57. package/examples/playbooks/keep-records-updated.md +2 -1
  58. package/package.json +1 -1
  59. package/platforms/claude-code/.claude-plugin/plugin.json +9 -1
  60. package/platforms/claude-code/agents/syntaur-expert.md +1 -1
  61. package/platforms/claude-code/commands/add-memory/add-memory.md +18 -0
  62. package/platforms/claude-code/commands/add-resource/add-resource.md +16 -0
  63. package/platforms/claude-code/commands/list-assignments/list-assignments.md +16 -0
  64. package/platforms/claude-code/commands/log-progress/log-progress.md +16 -0
  65. package/platforms/claude-code/commands/replan/replan.md +16 -0
  66. package/platforms/claude-code/commands/resume-session/resume-session.md +16 -0
  67. package/platforms/claude-code/commands/set-workspace/set-workspace.md +16 -0
  68. package/platforms/claude-code/commands/syntaur-worktree/syntaur-worktree.md +16 -0
  69. package/platforms/claude-code/references/protocol-summary.md +8 -0
  70. package/platforms/claude-code/skills/add-memory/SKILL.md +96 -0
  71. package/platforms/claude-code/skills/add-resource/SKILL.md +85 -0
  72. package/platforms/claude-code/skills/capture-artifacts/SKILL.md +84 -0
  73. package/platforms/claude-code/skills/complete-assignment/SKILL.md +2 -1
  74. package/platforms/claude-code/skills/list-assignments/SKILL.md +73 -0
  75. package/platforms/claude-code/skills/log-progress/SKILL.md +128 -0
  76. package/platforms/claude-code/skills/plan-assignment/SKILL.md +29 -8
  77. package/platforms/claude-code/skills/replan/SKILL.md +113 -0
  78. package/platforms/claude-code/skills/resume-session/SKILL.md +83 -0
  79. package/platforms/claude-code/skills/set-workspace/SKILL.md +109 -0
  80. package/platforms/claude-code/skills/syntaur-protocol/SKILL.md +4 -0
  81. package/platforms/claude-code/skills/syntaur-protocol/references/protocol-summary.md +8 -0
  82. package/platforms/claude-code/skills/syntaur-worktree/SKILL.md +117 -0
  83. package/platforms/codex/README.md +2 -2
  84. package/platforms/codex/agents/syntaur-operator.md +7 -0
  85. package/platforms/codex/commands/add-memory.md +16 -0
  86. package/platforms/codex/commands/add-resource.md +14 -0
  87. package/platforms/codex/commands/list-assignments.md +13 -0
  88. package/platforms/codex/commands/log-progress.md +14 -0
  89. package/platforms/codex/commands/replan.md +15 -0
  90. package/platforms/codex/commands/resume-session.md +15 -0
  91. package/platforms/codex/commands/set-workspace.md +15 -0
  92. package/platforms/codex/commands/syntaur-worktree.md +15 -0
  93. package/platforms/codex/references/protocol-summary.md +8 -0
  94. package/platforms/codex/skills/add-memory/SKILL.md +96 -0
  95. package/platforms/codex/skills/add-resource/SKILL.md +85 -0
  96. package/platforms/codex/skills/capture-artifacts/SKILL.md +84 -0
  97. package/platforms/codex/skills/complete-assignment/SKILL.md +2 -1
  98. package/platforms/codex/skills/list-assignments/SKILL.md +73 -0
  99. package/platforms/codex/skills/log-progress/SKILL.md +128 -0
  100. package/platforms/codex/skills/plan-assignment/SKILL.md +29 -8
  101. package/platforms/codex/skills/replan/SKILL.md +113 -0
  102. package/platforms/codex/skills/resume-session/SKILL.md +83 -0
  103. package/platforms/codex/skills/set-workspace/SKILL.md +109 -0
  104. package/platforms/codex/skills/syntaur-protocol/SKILL.md +4 -0
  105. package/platforms/codex/skills/syntaur-protocol/references/protocol-summary.md +8 -0
  106. package/platforms/codex/skills/syntaur-worktree/SKILL.md +117 -0
  107. package/skills/add-memory/SKILL.md +96 -0
  108. package/skills/add-resource/SKILL.md +85 -0
  109. package/skills/capture-artifacts/SKILL.md +84 -0
  110. package/skills/complete-assignment/SKILL.md +2 -1
  111. package/skills/list-assignments/SKILL.md +73 -0
  112. package/skills/log-progress/SKILL.md +128 -0
  113. package/skills/plan-assignment/SKILL.md +29 -8
  114. package/skills/replan/SKILL.md +113 -0
  115. package/skills/resume-session/SKILL.md +83 -0
  116. package/skills/set-workspace/SKILL.md +109 -0
  117. package/skills/syntaur-protocol/SKILL.md +4 -0
  118. package/skills/syntaur-protocol/references/protocol-summary.md +8 -0
  119. package/skills/syntaur-worktree/SKILL.md +117 -0
  120. package/dashboard/dist/assets/channel-FWLgstBg.js +0 -1
  121. package/dashboard/dist/assets/classDiagram-VBA2DB6C-BAvQTSn_.js +0 -1
  122. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-BAvQTSn_.js +0 -1
  123. package/dashboard/dist/assets/clone-rSEllQ1P.js +0 -1
  124. package/dashboard/dist/assets/index-bLWoSCLL.css +0 -1
  125. package/dashboard/dist/assets/index-eS0wtDO0.js +0 -500
  126. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-TaLLhLWD.js +0 -1
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ __export(paths_exports, {
16
16
  expandHome: () => expandHome,
17
17
  playbooksDir: () => playbooksDir,
18
18
  projectTodosDir: () => projectTodosDir,
19
+ proofDir: () => proofDir,
19
20
  serversDir: () => serversDir,
20
21
  syntaurRoot: () => syntaurRoot,
21
22
  todoPlanDir: () => todoPlanDir,
@@ -57,6 +58,9 @@ function projectTodosDir(projectsDir2, projectSlug) {
57
58
  function todoPlanDir(todosDir2, workspaceOrProject, todoId) {
58
59
  return resolve(todosDir2, "plans", workspaceOrProject, todoId);
59
60
  }
61
+ function proofDir(assignmentDir) {
62
+ return resolve(assignmentDir, "proof");
63
+ }
60
64
  var init_paths = __esm({
61
65
  "src/utils/paths.ts"() {
62
66
  "use strict";
@@ -393,6 +397,7 @@ function parseMemory(fileContent) {
393
397
  scope: getField(fm, "scope") ?? "",
394
398
  sourceAssignment: getField(fm, "sourceAssignment"),
395
399
  relatedAssignments: parseListField(fm, "relatedAssignments"),
400
+ tags: parseListField(fm, "tags"),
396
401
  created: getField(fm, "created") ?? "",
397
402
  updated: getField(fm, "updated") ?? "",
398
403
  body
@@ -2157,6 +2162,40 @@ var init_decision_record = __esm({
2157
2162
  });
2158
2163
 
2159
2164
  // src/templates/index-stubs.ts
2165
+ function renderMemoryStub(params2) {
2166
+ return `---
2167
+ type: memory
2168
+ name: ${escapeYamlString(params2.name)}
2169
+ source: claude
2170
+ sourceAssignment: null
2171
+ relatedAssignments: []
2172
+ scope: project
2173
+ created: "${params2.timestamp}"
2174
+ updated: "${params2.timestamp}"
2175
+ tags: []
2176
+ ---
2177
+
2178
+ # ${params2.name}
2179
+
2180
+ `;
2181
+ }
2182
+ function renderResourceStub(params2) {
2183
+ return `---
2184
+ type: resource
2185
+ name: ${escapeYamlString(params2.name)}
2186
+ source: claude
2187
+ category: documentation
2188
+ sourceUrl: null
2189
+ sourceAssignment: null
2190
+ relatedAssignments: []
2191
+ created: "${params2.timestamp}"
2192
+ updated: "${params2.timestamp}"
2193
+ ---
2194
+
2195
+ # ${params2.name}
2196
+
2197
+ `;
2198
+ }
2160
2199
  function renderIndexAssignments(params2) {
2161
2200
  return `---
2162
2201
  project: ${params2.slug}
@@ -2269,6 +2308,7 @@ total: 0
2269
2308
  var init_index_stubs = __esm({
2270
2309
  "src/templates/index-stubs.ts"() {
2271
2310
  "use strict";
2311
+ init_yaml();
2272
2312
  }
2273
2313
  });
2274
2314
 
@@ -2511,6 +2551,15 @@ If the global Syntaur Codex plugin is installed, prefer these workflows instead
2511
2551
  - \`plan-assignment\` -- write a versioned plan file (\`plan.md\`, \`plan-v2.md\`, ...) and link it from the \`## Todos\` section of \`assignment.md\`
2512
2552
  - \`complete-assignment\` -- write the cross-ticket \`handoff.md\` entry, append a final entry to \`progress.md\`, close the session, and transition state
2513
2553
  - \`save-session-summary\` -- write per-session continuity at \`<assignmentDir>/sessions/<sessionId>/summary.md\` for resume across sessions of the same agent. Codex has no \`PreCompact\` hook event \u2014 invoke this manually before compaction or session end.
2554
+ - \`capture-artifacts\` -- capture typed proof artifacts (screenshot/video/asciinema/http/text) for the active assignment. Criterion linkage is optional. Run \`syntaur proof build\` to render \`proof.html\`.
2555
+ - \`resume-session\` -- counterpart to \`save-session-summary\`; loads the latest summary, \`.syntaur/context.json\`, and any open handoff so a fresh session re-orients without re-reading the transcript
2556
+ - \`replan\` -- bump the active assignment to a new \`plan-v<N>.md\` per the Plan Versioning playbook (CLI does file ops, skill writes the body)
2557
+ - \`syntaur-worktree\` -- atomic worktree creation under \`<repository>/.worktrees/<branch>\` plus assign + start + context binding in one move
2558
+ - \`add-resource\` -- register a project-level resource (link to dashboard / doc / ticket); CLI regenerates \`_index.md\` server-side
2559
+ - \`add-memory\` -- capture a project-level Syntaur memory; CLI regenerates \`_index.md\` server-side (distinct from user-global Claude Code auto-memory)
2560
+ - \`list-assignments\` -- cross-project listing with filters by status, project, tag, age (scriptable; not the interactive \`browse\` TUI)
2561
+ - \`log-progress\` -- append a timestamped entry to the active \`progress.md\` and bump frontmatter (Keep Records Updated playbook)
2562
+ - \`set-workspace\` -- populate the four \`workspace.*\` fields in \`assignment.md\`; validates via \`syntaur doctor --assignment --json\` before writing
2514
2563
  - \`track-session\` -- manage tracked tmux sessions for the dashboard
2515
2564
 
2516
2565
  If the plugin is unavailable, follow the same workflow manually with the \`syntaur\` CLI and keep the protocol files current yourself.
@@ -2639,6 +2688,8 @@ Use the \`syntaur\` CLI for state transitions and coordination:
2639
2688
  - \`syntaur fail ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- mark as failed
2640
2689
  - \`syntaur comment ${params2.assignmentSlug} "body" --type question|note|feedback [--reply-to <id>]\` -- append to \`comments.md\` (use for all Q&A; questions support resolve toggle)
2641
2690
  - \`syntaur request ${params2.assignmentSlug} <target-slug-or-uuid> "text"\` -- append a todo to another assignment's \`## Todos\` annotated \`(from: ${params2.assignmentSlug})\`
2691
+ - \`syntaur capture --kind <screenshot|video|asciinema|http|text> [--file <path>] [--criterion <index>] [--note <text>] ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- record a proof artifact. \`--kind=text\` requires \`--note\` and forbids \`--file\`. Criterion linkage is optional.
2692
+ - \`syntaur proof build ${params2.assignmentSlug} --project ${params2.projectSlug}\` -- render \`proof.html\` and \`proof.md\` at the assignment dir. Atomic overwrite \u2014 safe to re-run.
2642
2693
 
2643
2694
  ## Troubleshooting
2644
2695
 
@@ -4456,7 +4507,7 @@ var init_scanner = __esm({
4456
4507
 
4457
4508
  // src/dashboard/api.ts
4458
4509
  import { readdir as readdir7, readFile as readFile9, writeFile as writeFile3 } from "fs/promises";
4459
- import { resolve as resolve12, dirname as dirname3 } from "path";
4510
+ import { resolve as resolve12, dirname as dirname3, basename } from "path";
4460
4511
  async function listStandaloneRecords(assignmentsDir2) {
4461
4512
  if (!assignmentsDir2) return [];
4462
4513
  if (!await fileExists(assignmentsDir2)) return [];
@@ -4465,10 +4516,10 @@ async function listStandaloneRecords(assignmentsDir2) {
4465
4516
  for (const entry of entries) {
4466
4517
  if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
4467
4518
  const assignmentDir = resolve12(assignmentsDir2, entry.name);
4468
- const assignmentMdPath = resolve12(assignmentDir, "assignment.md");
4469
- if (!await fileExists(assignmentMdPath)) continue;
4519
+ const assignmentMdPath2 = resolve12(assignmentDir, "assignment.md");
4520
+ if (!await fileExists(assignmentMdPath2)) continue;
4470
4521
  try {
4471
- const content = await readFile9(assignmentMdPath, "utf-8");
4522
+ const content = await readFile9(assignmentMdPath2, "utf-8");
4472
4523
  const record = parseAssignmentFull(content);
4473
4524
  records.push({ assignmentDir, id: entry.name, record });
4474
4525
  } catch {
@@ -4832,11 +4883,11 @@ async function getProjectDetail(projectsDir2, slug) {
4832
4883
  }
4833
4884
  async function getAssignmentDetail(projectsDir2, projectSlug, assignmentSlug) {
4834
4885
  const assignmentDir = resolve12(projectsDir2, projectSlug, "assignments", assignmentSlug);
4835
- const assignmentMdPath = resolve12(assignmentDir, "assignment.md");
4836
- if (!await fileExists(assignmentMdPath)) {
4886
+ const assignmentMdPath2 = resolve12(assignmentDir, "assignment.md");
4887
+ if (!await fileExists(assignmentMdPath2)) {
4837
4888
  return null;
4838
4889
  }
4839
- const assignmentContent = await readFile9(assignmentMdPath, "utf-8");
4890
+ const assignmentContent = await readFile9(assignmentMdPath2, "utf-8");
4840
4891
  const assignment = parseAssignmentFull(assignmentContent);
4841
4892
  let plan = null;
4842
4893
  const planPath = resolve12(assignmentDir, "plan.md");
@@ -5110,9 +5161,9 @@ async function getAssignmentDetailById(projectsDir2, assignmentsDir2, id) {
5110
5161
  }
5111
5162
  async function buildStandaloneAssignmentDetail(resolved) {
5112
5163
  const assignmentDir = resolved.assignmentDir;
5113
- const assignmentMdPath = resolve12(assignmentDir, "assignment.md");
5114
- if (!await fileExists(assignmentMdPath)) return null;
5115
- const assignmentContent = await readFile9(assignmentMdPath, "utf-8");
5164
+ const assignmentMdPath2 = resolve12(assignmentDir, "assignment.md");
5165
+ if (!await fileExists(assignmentMdPath2)) return null;
5166
+ const assignmentContent = await readFile9(assignmentMdPath2, "utf-8");
5116
5167
  const assignment = parseAssignmentFull(assignmentContent);
5117
5168
  let plan = null;
5118
5169
  const planPath = resolve12(assignmentDir, "plan.md");
@@ -5295,12 +5346,102 @@ async function listMemories(projectPath) {
5295
5346
  source: parsed.source,
5296
5347
  scope: parsed.scope,
5297
5348
  sourceAssignment: parsed.sourceAssignment,
5349
+ relatedAssignments: parsed.relatedAssignments,
5298
5350
  updated: parsed.updated
5299
5351
  });
5300
5352
  }
5301
5353
  results.sort((left, right) => compareTimestamps(right.updated, left.updated));
5302
5354
  return results;
5303
5355
  }
5356
+ async function listAllMemories(projectsDir2) {
5357
+ const projectRecords = await listProjectRecords(projectsDir2);
5358
+ const all = [];
5359
+ for (const record of projectRecords) {
5360
+ const memories = await listMemories(record.projectPath);
5361
+ for (const memory of memories) {
5362
+ all.push({
5363
+ ...memory,
5364
+ projectSlug: basename(record.projectPath),
5365
+ projectTitle: record.summary.title
5366
+ });
5367
+ }
5368
+ }
5369
+ all.sort((left, right) => compareTimestamps(right.updated, left.updated));
5370
+ return all;
5371
+ }
5372
+ async function listAllResources(projectsDir2) {
5373
+ const projectRecords = await listProjectRecords(projectsDir2);
5374
+ const all = [];
5375
+ for (const record of projectRecords) {
5376
+ const resources = await listResources(record.projectPath);
5377
+ for (const resource of resources) {
5378
+ all.push({
5379
+ ...resource,
5380
+ projectSlug: basename(record.projectPath),
5381
+ projectTitle: record.summary.title
5382
+ });
5383
+ }
5384
+ }
5385
+ all.sort((left, right) => compareTimestamps(right.updated, left.updated));
5386
+ return all;
5387
+ }
5388
+ async function resolveProjectPath(projectsDir2, projectSlug) {
5389
+ const direct = resolve12(projectsDir2, projectSlug);
5390
+ if (await fileExists(resolve12(direct, "project.md"))) return direct;
5391
+ const records = await listProjectRecords(projectsDir2);
5392
+ const match = records.find((r) => r.summary.slug === projectSlug);
5393
+ return match ? match.projectPath : null;
5394
+ }
5395
+ async function getMemoryDetail(projectsDir2, projectSlug, itemSlug) {
5396
+ if (itemSlug.startsWith("_")) return null;
5397
+ const projectRecords = await listProjectRecords(projectsDir2);
5398
+ const projectRecord = projectRecords.find(
5399
+ (p) => basename(p.projectPath) === projectSlug || p.summary.slug === projectSlug
5400
+ );
5401
+ if (!projectRecord) return null;
5402
+ const filePath = resolve12(projectRecord.projectPath, "memories", `${itemSlug}.md`);
5403
+ if (!await fileExists(filePath)) return null;
5404
+ const content = await readFile9(filePath, "utf-8");
5405
+ const parsed = parseMemory(content);
5406
+ return {
5407
+ name: parsed.name,
5408
+ slug: itemSlug,
5409
+ source: parsed.source,
5410
+ scope: parsed.scope,
5411
+ sourceAssignment: parsed.sourceAssignment,
5412
+ relatedAssignments: parsed.relatedAssignments,
5413
+ updated: parsed.updated,
5414
+ created: parsed.created,
5415
+ body: parsed.body,
5416
+ tags: parsed.tags,
5417
+ projectSlug: basename(projectRecord.projectPath),
5418
+ projectTitle: projectRecord.summary.title
5419
+ };
5420
+ }
5421
+ async function getResourceDetail(projectsDir2, projectSlug, itemSlug) {
5422
+ if (itemSlug.startsWith("_")) return null;
5423
+ const projectRecords = await listProjectRecords(projectsDir2);
5424
+ const projectRecord = projectRecords.find(
5425
+ (p) => basename(p.projectPath) === projectSlug || p.summary.slug === projectSlug
5426
+ );
5427
+ if (!projectRecord) return null;
5428
+ const filePath = resolve12(projectRecord.projectPath, "resources", `${itemSlug}.md`);
5429
+ if (!await fileExists(filePath)) return null;
5430
+ const content = await readFile9(filePath, "utf-8");
5431
+ const parsed = parseResource(content);
5432
+ return {
5433
+ name: parsed.name,
5434
+ slug: itemSlug,
5435
+ category: parsed.category,
5436
+ source: parsed.source,
5437
+ relatedAssignments: parsed.relatedAssignments,
5438
+ updated: parsed.updated,
5439
+ created: parsed.created,
5440
+ body: parsed.body,
5441
+ projectSlug: basename(projectRecord.projectPath),
5442
+ projectTitle: projectRecord.summary.title
5443
+ };
5444
+ }
5304
5445
  async function loadDependencyGraph(projectPath, assignments) {
5305
5446
  const statusPath = resolve12(projectPath, "_status.md");
5306
5447
  if (await fileExists(statusPath)) {
@@ -5636,6 +5777,10 @@ function getDocumentPath(projectsDir2, documentType, projectSlug, assignmentSlug
5636
5777
  return assignmentSlug ? resolve12(projectsDir2, projectSlug, "assignments", assignmentSlug, "handoff.md") : null;
5637
5778
  case "decision-record":
5638
5779
  return assignmentSlug ? resolve12(projectsDir2, projectSlug, "assignments", assignmentSlug, "decision-record.md") : null;
5780
+ case "memory":
5781
+ return assignmentSlug ? resolve12(projectsDir2, projectSlug, "memories", `${assignmentSlug}.md`) : null;
5782
+ case "resource":
5783
+ return assignmentSlug ? resolve12(projectsDir2, projectSlug, "resources", `${assignmentSlug}.md`) : null;
5639
5784
  default:
5640
5785
  return null;
5641
5786
  }
@@ -5656,6 +5801,10 @@ function getEditableDocumentTitle(documentType, projectSlug, assignmentSlug) {
5656
5801
  return `Append Decision: ${assignmentSlug || "assignment"}`;
5657
5802
  case "playbook":
5658
5803
  return `Edit Playbook: ${projectSlug}`;
5804
+ case "memory":
5805
+ return `Edit Memory: ${assignmentSlug || "memory"}`;
5806
+ case "resource":
5807
+ return `Edit Resource: ${assignmentSlug || "resource"}`;
5659
5808
  default:
5660
5809
  return projectSlug;
5661
5810
  }
@@ -6933,7 +7082,7 @@ var init_git_worktree = __esm({
6933
7082
  });
6934
7083
 
6935
7084
  // src/index.ts
6936
- import { Command as Command5 } from "commander";
7085
+ import { Command as Command12 } from "commander";
6937
7086
 
6938
7087
  // src/commands/init.ts
6939
7088
  init_paths();
@@ -7263,8 +7412,8 @@ async function migrateFromMarkdown(projectsDir2) {
7263
7412
  return allSessions.length;
7264
7413
  }
7265
7414
  async function parseMarkdownSessionsIndex(filePath, projectSlug) {
7266
- const { readFile: readFile35 } = await import("fs/promises");
7267
- const raw = await readFile35(filePath, "utf-8");
7415
+ const { readFile: readFile43 } = await import("fs/promises");
7416
+ const raw = await readFile43(filePath, "utf-8");
7268
7417
  const sessions = [];
7269
7418
  const lines = raw.split("\n");
7270
7419
  let inTable = false;
@@ -7315,8 +7464,8 @@ function rowToSession(row) {
7315
7464
  };
7316
7465
  }
7317
7466
  async function appendSession(_projectDir, session) {
7318
- const db2 = getSessionDb();
7319
- db2.prepare(`
7467
+ const db3 = getSessionDb();
7468
+ db3.prepare(`
7320
7469
  INSERT INTO sessions (session_id, project_slug, assignment_slug, agent, started, status, path, description, transcript_path)
7321
7470
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
7322
7471
  ON CONFLICT(session_id) DO UPDATE SET
@@ -7341,42 +7490,42 @@ async function appendSession(_projectDir, session) {
7341
7490
  );
7342
7491
  }
7343
7492
  async function updateSessionStatus(_projectDir, sessionId, status) {
7344
- const db2 = getSessionDb();
7493
+ const db3 = getSessionDb();
7345
7494
  const isTerminal = status === "completed" || status === "stopped";
7346
- const result = isTerminal ? db2.prepare(
7495
+ const result = isTerminal ? db3.prepare(
7347
7496
  "UPDATE sessions SET status = ?, ended = datetime('now'), updated_at = datetime('now') WHERE session_id = ?"
7348
- ).run(status, sessionId) : db2.prepare(
7497
+ ).run(status, sessionId) : db3.prepare(
7349
7498
  "UPDATE sessions SET status = ?, updated_at = datetime('now') WHERE session_id = ?"
7350
7499
  ).run(status, sessionId);
7351
7500
  return result.changes > 0;
7352
7501
  }
7353
7502
  async function listAllSessions(_projectsDir) {
7354
- const db2 = getSessionDb();
7355
- const rows = db2.prepare("SELECT * FROM sessions ORDER BY started DESC").all();
7503
+ const db3 = getSessionDb();
7504
+ const rows = db3.prepare("SELECT * FROM sessions ORDER BY started DESC").all();
7356
7505
  return rows.map(rowToSession);
7357
7506
  }
7358
7507
  async function listProjectSessions(_projectsDir, projectSlug, assignmentSlug) {
7359
- const db2 = getSessionDb();
7508
+ const db3 = getSessionDb();
7360
7509
  if (assignmentSlug) {
7361
- const rows2 = db2.prepare(
7510
+ const rows2 = db3.prepare(
7362
7511
  "SELECT * FROM sessions WHERE project_slug = ? AND assignment_slug = ? ORDER BY started DESC"
7363
7512
  ).all(projectSlug, assignmentSlug);
7364
7513
  return rows2.map(rowToSession);
7365
7514
  }
7366
- const rows = db2.prepare("SELECT * FROM sessions WHERE project_slug = ? ORDER BY started DESC").all(projectSlug);
7515
+ const rows = db3.prepare("SELECT * FROM sessions WHERE project_slug = ? ORDER BY started DESC").all(projectSlug);
7367
7516
  return rows.map(rowToSession);
7368
7517
  }
7369
7518
  async function deleteSessions(sessionIds) {
7370
7519
  if (sessionIds.length === 0) return 0;
7371
- const db2 = getSessionDb();
7520
+ const db3 = getSessionDb();
7372
7521
  const placeholders = sessionIds.map(() => "?").join(", ");
7373
- const result = db2.prepare(`DELETE FROM sessions WHERE session_id IN (${placeholders})`).run(...sessionIds);
7522
+ const result = db3.prepare(`DELETE FROM sessions WHERE session_id IN (${placeholders})`).run(...sessionIds);
7374
7523
  return result.changes;
7375
7524
  }
7376
7525
  var DONE_ASSIGNMENT_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "review"]);
7377
- async function readAssignmentStatusFromPath(assignmentMdPath) {
7378
- if (!await fileExists(assignmentMdPath)) return null;
7379
- const raw = await readFile10(assignmentMdPath, "utf-8");
7526
+ async function readAssignmentStatusFromPath(assignmentMdPath2) {
7527
+ if (!await fileExists(assignmentMdPath2)) return null;
7528
+ const raw = await readFile10(assignmentMdPath2, "utf-8");
7380
7529
  const match = raw.match(/^status:\s*(.+)$/m);
7381
7530
  return match ? match[1].trim() : null;
7382
7531
  }
@@ -7386,8 +7535,8 @@ async function readAssignmentStatus(projectDir, assignmentSlug) {
7386
7535
  );
7387
7536
  }
7388
7537
  async function reconcileActiveSessions(projectsDir2, assignmentsDir2) {
7389
- const db2 = getSessionDb();
7390
- const activeSessions = db2.prepare("SELECT * FROM sessions WHERE status = 'active' AND assignment_slug IS NOT NULL").all();
7538
+ const db3 = getSessionDb();
7539
+ const activeSessions = db3.prepare("SELECT * FROM sessions WHERE status = 'active' AND assignment_slug IS NOT NULL").all();
7391
7540
  if (activeSessions.length === 0) return 0;
7392
7541
  const assignmentStatuses = /* @__PURE__ */ new Map();
7393
7542
  const seen = /* @__PURE__ */ new Set();
@@ -7424,10 +7573,10 @@ async function reconcileActiveSessions(projectsDir2, assignmentsDir2) {
7424
7573
  return totalUpdated;
7425
7574
  }
7426
7575
  async function listSessionsByAssignment(projectSlug, assignmentSlug) {
7427
- const db2 = getSessionDb();
7428
- const rows = projectSlug === null ? db2.prepare(
7576
+ const db3 = getSessionDb();
7577
+ const rows = projectSlug === null ? db3.prepare(
7429
7578
  "SELECT * FROM sessions WHERE assignment_slug = ? AND project_slug IS NULL ORDER BY started DESC"
7430
- ).all(assignmentSlug) : db2.prepare(
7579
+ ).all(assignmentSlug) : db3.prepare(
7431
7580
  "SELECT * FROM sessions WHERE project_slug = ? AND assignment_slug = ? ORDER BY started DESC"
7432
7581
  ).all(projectSlug, assignmentSlug);
7433
7582
  return rows.map(rowToSession);
@@ -7633,8 +7782,8 @@ init_timestamp();
7633
7782
  init_fs();
7634
7783
  init_parser();
7635
7784
  import { Router } from "express";
7636
- import { resolve as resolve15 } from "path";
7637
- import { rm, readFile as readFile11 } from "fs/promises";
7785
+ import { resolve as resolve15, basename as basename2 } from "path";
7786
+ import { rm, readFile as readFile11, open as fsOpen } from "fs/promises";
7638
7787
 
7639
7788
  // src/dashboard/acceptance-criteria.ts
7640
7789
  function splitFrontmatter(content) {
@@ -7906,6 +8055,210 @@ function createWriteRouter(projectsDir2, assignmentsDir2) {
7906
8055
  }
7907
8056
  res.json(document);
7908
8057
  });
8058
+ router.get("/api/projects/:slug/memories/:itemSlug/edit", async (req, res) => {
8059
+ const slug = getParam(req.params.slug);
8060
+ const itemSlug = getParam(req.params.itemSlug);
8061
+ if (!isValidSlug(itemSlug)) {
8062
+ res.status(400).json({ error: "Invalid memory slug." });
8063
+ return;
8064
+ }
8065
+ const projectDir = await resolveProjectPath(projectsDir2, slug);
8066
+ if (!projectDir) {
8067
+ res.status(404).json({ error: `Project "${slug}" not found` });
8068
+ return;
8069
+ }
8070
+ const document = await getEditableDocument(projectsDir2, "memory", basename2(projectDir), itemSlug);
8071
+ if (!document) {
8072
+ res.status(404).json({ error: "Memory not found" });
8073
+ return;
8074
+ }
8075
+ res.json(document);
8076
+ });
8077
+ router.get("/api/projects/:slug/resources/:itemSlug/edit", async (req, res) => {
8078
+ const slug = getParam(req.params.slug);
8079
+ const itemSlug = getParam(req.params.itemSlug);
8080
+ if (!isValidSlug(itemSlug)) {
8081
+ res.status(400).json({ error: "Invalid resource slug." });
8082
+ return;
8083
+ }
8084
+ const projectDir = await resolveProjectPath(projectsDir2, slug);
8085
+ if (!projectDir) {
8086
+ res.status(404).json({ error: `Project "${slug}" not found` });
8087
+ return;
8088
+ }
8089
+ const document = await getEditableDocument(projectsDir2, "resource", basename2(projectDir), itemSlug);
8090
+ if (!document) {
8091
+ res.status(404).json({ error: "Resource not found" });
8092
+ return;
8093
+ }
8094
+ res.json(document);
8095
+ });
8096
+ function itemFolder(kind) {
8097
+ return kind === "memory" ? "memories" : "resources";
8098
+ }
8099
+ function renderItemStub(kind, params2) {
8100
+ return kind === "memory" ? renderMemoryStub(params2) : renderResourceStub(params2);
8101
+ }
8102
+ function replaceStubBody(stub, body) {
8103
+ const match = stub.match(/^(---\r?\n[\s\S]*?\r?\n---\r?\n)/);
8104
+ if (!match) return stub;
8105
+ return `${match[1]}
8106
+ ${body.startsWith("\n") ? body.slice(1) : body}${body.endsWith("\n") ? "" : "\n"}`;
8107
+ }
8108
+ function extractFrontmatterBlock(content) {
8109
+ const match = content.match(/^(---\r?\n[\s\S]*?\r?\n---\r?\n)/);
8110
+ return match ? match[1] : null;
8111
+ }
8112
+ function parseItem(kind, content) {
8113
+ return kind === "memory" ? parseMemory(content) : parseResource(content);
8114
+ }
8115
+ async function getItemDetail(kind, projectSlug, itemSlug) {
8116
+ return kind === "memory" ? await getMemoryDetail(projectsDir2, projectSlug, itemSlug) : await getResourceDetail(projectsDir2, projectSlug, itemSlug);
8117
+ }
8118
+ async function resolveProjectDir(projectSlug) {
8119
+ return resolveProjectPath(projectsDir2, projectSlug);
8120
+ }
8121
+ function rejectBadItemSlug(itemSlug, kind, res) {
8122
+ if (isValidSlug(itemSlug)) return false;
8123
+ res.status(400).json({
8124
+ error: `Invalid ${kind} slug "${itemSlug}". Slugs must be lowercase letters, numbers, and hyphens only.`
8125
+ });
8126
+ return true;
8127
+ }
8128
+ for (const kind of ["memory", "resource"]) {
8129
+ const folder = itemFolder(kind);
8130
+ router.get(`/api/projects/:slug/${folder}/:itemSlug`, async (req, res) => {
8131
+ try {
8132
+ const projectSlug = getParam(req.params.slug);
8133
+ const itemSlug = getParam(req.params.itemSlug);
8134
+ if (rejectBadItemSlug(itemSlug, kind, res)) return;
8135
+ const detail = await getItemDetail(kind, projectSlug, itemSlug);
8136
+ if (!detail) {
8137
+ res.status(404).json({ error: `${kind === "memory" ? "Memory" : "Resource"} not found` });
8138
+ return;
8139
+ }
8140
+ res.json(detail);
8141
+ } catch (error) {
8142
+ console.error(`Error fetching ${kind} detail:`, error);
8143
+ res.status(500).json({ error: `Failed to load ${kind}: ${error.message}` });
8144
+ }
8145
+ });
8146
+ router.post(`/api/projects/:slug/${folder}`, async (req, res) => {
8147
+ try {
8148
+ const projectSlug = getParam(req.params.slug);
8149
+ const projectDir = await resolveProjectDir(projectSlug);
8150
+ if (!projectDir) {
8151
+ res.status(404).json({ error: `Project "${projectSlug}" not found` });
8152
+ return;
8153
+ }
8154
+ const body = req.body ?? {};
8155
+ const name = typeof body.name === "string" ? body.name.trim() : "";
8156
+ if (!name) {
8157
+ res.status(400).json({ error: "Name is required." });
8158
+ return;
8159
+ }
8160
+ const requestedSlug = typeof body.slug === "string" && body.slug.trim() ? body.slug.trim() : slugify(name);
8161
+ if (!requestedSlug || !isValidSlug(requestedSlug)) {
8162
+ res.status(400).json({
8163
+ error: `Slug "${requestedSlug}" is invalid. Slugs must be lowercase letters, numbers, and hyphens only.`
8164
+ });
8165
+ return;
8166
+ }
8167
+ const folderPath = resolve15(projectDir, folder);
8168
+ await ensureDir(folderPath);
8169
+ const filePath = resolve15(folderPath, `${requestedSlug}.md`);
8170
+ const timestamp = nowTimestamp();
8171
+ let content = renderItemStub(kind, {
8172
+ slug: requestedSlug,
8173
+ name,
8174
+ projectSlug: basename2(projectDir),
8175
+ timestamp
8176
+ });
8177
+ const customBody = typeof body.body === "string" ? body.body : "";
8178
+ if (customBody.trim()) {
8179
+ content = replaceStubBody(content, customBody);
8180
+ }
8181
+ try {
8182
+ const handle = await fsOpen(filePath, "wx");
8183
+ try {
8184
+ await handle.writeFile(content, "utf-8");
8185
+ } finally {
8186
+ await handle.close();
8187
+ }
8188
+ } catch (err2) {
8189
+ if (err2.code === "EEXIST") {
8190
+ res.status(409).json({
8191
+ error: `${kind === "memory" ? "Memory" : "Resource"} with slug "${requestedSlug}" already exists in project "${basename2(projectDir)}".`
8192
+ });
8193
+ return;
8194
+ }
8195
+ throw err2;
8196
+ }
8197
+ res.status(201).json({ slug: requestedSlug, projectSlug: basename2(projectDir), content });
8198
+ } catch (error) {
8199
+ console.error(`Error creating ${kind}:`, error);
8200
+ res.status(500).json({ error: `Failed to create ${kind}: ${error.message}` });
8201
+ }
8202
+ });
8203
+ router.patch(`/api/projects/:slug/${folder}/:itemSlug`, async (req, res) => {
8204
+ try {
8205
+ const projectSlug = getParam(req.params.slug);
8206
+ const itemSlug = getParam(req.params.itemSlug);
8207
+ if (rejectBadItemSlug(itemSlug, kind, res)) return;
8208
+ const projectDir = await resolveProjectDir(projectSlug);
8209
+ if (!projectDir) {
8210
+ res.status(404).json({ error: `Project "${projectSlug}" not found` });
8211
+ return;
8212
+ }
8213
+ const filePath = resolve15(projectDir, folder, `${itemSlug}.md`);
8214
+ if (!await fileExists(filePath)) {
8215
+ res.status(404).json({ error: `${kind === "memory" ? "Memory" : "Resource"} not found` });
8216
+ return;
8217
+ }
8218
+ const nextContentRaw = requireContent(req, res);
8219
+ if (!nextContentRaw) return;
8220
+ const currentContent = await readFile11(filePath, "utf-8");
8221
+ const frontmatterBlock = extractFrontmatterBlock(currentContent);
8222
+ if (!frontmatterBlock) {
8223
+ res.status(500).json({ error: `${kind} file is malformed (no frontmatter)` });
8224
+ return;
8225
+ }
8226
+ const next = parseItem(kind, nextContentRaw);
8227
+ const nextBody = next.body.trimStart();
8228
+ let merged = `${frontmatterBlock}
8229
+ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
8230
+ merged = setTopLevelField(merged, "updated", nowTimestamp());
8231
+ await writeFileForce(filePath, merged);
8232
+ const detail = await getItemDetail(kind, basename2(projectDir), itemSlug);
8233
+ res.json({ [kind]: detail, content: merged });
8234
+ } catch (error) {
8235
+ console.error(`Error updating ${kind}:`, error);
8236
+ res.status(500).json({ error: `Failed to update ${kind}: ${error.message}` });
8237
+ }
8238
+ });
8239
+ router.delete(`/api/projects/:slug/${folder}/:itemSlug`, async (req, res) => {
8240
+ try {
8241
+ const projectSlug = getParam(req.params.slug);
8242
+ const itemSlug = getParam(req.params.itemSlug);
8243
+ if (rejectBadItemSlug(itemSlug, kind, res)) return;
8244
+ const projectDir = await resolveProjectDir(projectSlug);
8245
+ if (!projectDir) {
8246
+ res.status(404).json({ error: `Project "${projectSlug}" not found` });
8247
+ return;
8248
+ }
8249
+ const filePath = resolve15(projectDir, folder, `${itemSlug}.md`);
8250
+ if (!await fileExists(filePath)) {
8251
+ res.status(404).json({ error: `${kind === "memory" ? "Memory" : "Resource"} not found` });
8252
+ return;
8253
+ }
8254
+ await rm(filePath);
8255
+ res.status(204).end();
8256
+ } catch (error) {
8257
+ console.error(`Error deleting ${kind}:`, error);
8258
+ res.status(500).json({ error: `Failed to delete ${kind}: ${error.message}` });
8259
+ }
8260
+ });
8261
+ }
7909
8262
  router.post("/api/projects", async (req, res) => {
7910
8263
  try {
7911
8264
  const content = requireContent(req, res);
@@ -9736,8 +10089,8 @@ function createTodosRouter(todosDir2, broadcast, projectsDir2) {
9736
10089
  router.post("/:workspace/archive", async (req, res) => {
9737
10090
  try {
9738
10091
  const { archivePath: archivePath2 } = await Promise.resolve().then(() => (init_parser2(), parser_exports));
9739
- const { resolve: resolve50 } = await import("path");
9740
- const { readFile: readFile35 } = await import("fs/promises");
10092
+ const { resolve: resolve62 } = await import("path");
10093
+ const { readFile: readFile43 } = await import("fs/promises");
9741
10094
  const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
9742
10095
  const workspace = getWorkspaceParam(req.params.workspace);
9743
10096
  const checklist = await readChecklist(todosDir2, workspace);
@@ -9753,10 +10106,10 @@ function createTodosRouter(todosDir2, broadcast, projectsDir2) {
9753
10106
  (e) => e.itemIds.every((id) => completedIds.has(id))
9754
10107
  );
9755
10108
  const archFile = archivePath2(todosDir2, workspace, checklist.archiveInterval);
9756
- await ensureDir(resolve50(todosDir2, "archive"));
10109
+ await ensureDir(resolve62(todosDir2, "archive"));
9757
10110
  let archContent = "";
9758
10111
  if (await fileExists(archFile)) {
9759
- archContent = await readFile35(archFile, "utf-8");
10112
+ archContent = await readFile43(archFile, "utf-8");
9760
10113
  archContent = archContent.trimEnd() + "\n\n";
9761
10114
  } else {
9762
10115
  archContent = `---
@@ -10015,7 +10368,7 @@ workspace: ${workspace}
10015
10368
  const { readConfig: readConfig3 } = await Promise.resolve().then(() => (init_config2(), config_exports));
10016
10369
  const { assignmentsDir: assignmentsDirFn } = await Promise.resolve().then(() => (init_paths(), paths_exports));
10017
10370
  const { fileExists: fileExists2, writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
10018
- const { readFile: readFile35 } = await import("fs/promises");
10371
+ const { readFile: readFile43 } = await import("fs/promises");
10019
10372
  const { appendTodosToAssignmentBody: appendTodosToAssignmentBody2, touchAssignmentUpdated: touchAssignmentUpdated2 } = await Promise.resolve().then(() => (init_assignment_todos(), assignment_todos_exports));
10020
10373
  const { nowTimestamp: nowTimestamp3 } = await Promise.resolve().then(() => (init_timestamp(), timestamp_exports));
10021
10374
  let assignmentRef;
@@ -10049,11 +10402,11 @@ workspace: ${workspace}
10049
10402
  } else {
10050
10403
  return { error: `Invalid target.assignment "${tg}"` };
10051
10404
  }
10052
- const assignmentMdPath2 = resolvePath2(assignmentDir, "assignment.md");
10053
- if (!await fileExists2(assignmentMdPath2)) return { error: `Target assignment not found: ${assignmentMdPath2}` };
10405
+ const assignmentMdPath3 = resolvePath2(assignmentDir, "assignment.md");
10406
+ if (!await fileExists2(assignmentMdPath3)) return { error: `Target assignment not found: ${assignmentMdPath3}` };
10054
10407
  }
10055
- const assignmentMdPath = resolvePath2(assignmentDir, "assignment.md");
10056
- let content = await readFile35(assignmentMdPath, "utf-8");
10408
+ const assignmentMdPath2 = resolvePath2(assignmentDir, "assignment.md");
10409
+ let content = await readFile43(assignmentMdPath2, "utf-8");
10057
10410
  content = appendTodosToAssignmentBody2(
10058
10411
  content,
10059
10412
  items.map((it) => ({
@@ -10062,7 +10415,7 @@ workspace: ${workspace}
10062
10415
  }))
10063
10416
  );
10064
10417
  content = touchAssignmentUpdated2(content, nowTimestamp3());
10065
- await writeFileForce2(assignmentMdPath, content);
10418
+ await writeFileForce2(assignmentMdPath2, content);
10066
10419
  if (!keepSource) {
10067
10420
  for (const item of items) {
10068
10421
  item.status = "completed";
@@ -10902,11 +11255,11 @@ workspace: ${slug}
10902
11255
  } else {
10903
11256
  return { error: `Invalid target.assignment "${tg}"` };
10904
11257
  }
10905
- const assignmentMdPath2 = resolve19(assignmentDir, "assignment.md");
10906
- if (!await fileExists(assignmentMdPath2)) return { error: `Target assignment not found: ${assignmentMdPath2}` };
11258
+ const assignmentMdPath3 = resolve19(assignmentDir, "assignment.md");
11259
+ if (!await fileExists(assignmentMdPath3)) return { error: `Target assignment not found: ${assignmentMdPath3}` };
10907
11260
  }
10908
- const assignmentMdPath = resolve19(assignmentDir, "assignment.md");
10909
- let content = await readFile14(assignmentMdPath, "utf-8");
11261
+ const assignmentMdPath2 = resolve19(assignmentDir, "assignment.md");
11262
+ let content = await readFile14(assignmentMdPath2, "utf-8");
10910
11263
  content = appendTodosToAssignmentBody2(
10911
11264
  content,
10912
11265
  items.map((it) => ({
@@ -10915,7 +11268,7 @@ workspace: ${slug}
10915
11268
  }))
10916
11269
  );
10917
11270
  content = touchAssignmentUpdated2(content, nowTimestamp3());
10918
- await writeFileForce(assignmentMdPath, content);
11271
+ await writeFileForce(assignmentMdPath2, content);
10919
11272
  if (!keepSource) {
10920
11273
  for (const item of items) {
10921
11274
  item.status = "completed";
@@ -12077,6 +12430,24 @@ function createDashboardServer(options) {
12077
12430
  app.use("/api/servers", createServersRouter(serversDir2, projectsDir2, assignmentsDir2));
12078
12431
  app.use("/api/agent-sessions", createAgentSessionsRouter(projectsDir2, broadcast, assignmentsDir2));
12079
12432
  app.use("/api/playbooks", createPlaybooksRouter(playbooksDir3));
12433
+ app.get("/api/memories", async (_req, res) => {
12434
+ try {
12435
+ const memories = await listAllMemories(projectsDir2);
12436
+ res.json({ generatedAt: (/* @__PURE__ */ new Date()).toISOString(), memories });
12437
+ } catch (error) {
12438
+ console.error("Error listing memories:", error);
12439
+ res.status(500).json({ error: `Failed to load memories: ${error.message}` });
12440
+ }
12441
+ });
12442
+ app.get("/api/resources", async (_req, res) => {
12443
+ try {
12444
+ const resources = await listAllResources(projectsDir2);
12445
+ res.json({ generatedAt: (/* @__PURE__ */ new Date()).toISOString(), resources });
12446
+ } catch (error) {
12447
+ console.error("Error listing resources:", error);
12448
+ res.status(500).json({ error: `Failed to load resources: ${error.message}` });
12449
+ }
12450
+ });
12080
12451
  app.use("/api/todos", createTodosRouter(todosDir2, broadcast, projectsDir2));
12081
12452
  app.use("/api/projects/:projectId/todos", createProjectTodosRouter(projectsDir2, broadcast, todosDir2));
12082
12453
  app.use("/api/backup", createBackupRouter());
@@ -12454,7 +12825,7 @@ import {
12454
12825
  } from "fs/promises";
12455
12826
  import { existsSync } from "fs";
12456
12827
  import { homedir as homedir2 } from "os";
12457
- import { basename, dirname as dirname8, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve25 } from "path";
12828
+ import { basename as basename3, dirname as dirname8, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve25 } from "path";
12458
12829
 
12459
12830
  // src/utils/package-root.ts
12460
12831
  init_fs();
@@ -12916,7 +13287,7 @@ async function ensureClaudeUserMarketplace() {
12916
13287
  async function detectClaudeMarketplaceForTarget(targetDir) {
12917
13288
  const normalizedTargetDir = normalizeAbsoluteInstallPath(targetDir, "Claude plugin target");
12918
13289
  const pluginsDir = dirname8(normalizedTargetDir);
12919
- if (basename(pluginsDir) !== "plugins") {
13290
+ if (basename3(pluginsDir) !== "plugins") {
12920
13291
  return null;
12921
13292
  }
12922
13293
  const rootDir = dirname8(pluginsDir);
@@ -12925,7 +13296,7 @@ async function detectClaudeMarketplaceForTarget(targetDir) {
12925
13296
  return null;
12926
13297
  }
12927
13298
  const marketplace = await readClaudeMarketplaceFile(manifestPath);
12928
- const name = typeof marketplace.name === "string" && marketplace.name.trim() !== "" ? marketplace.name : basename(rootDir);
13299
+ const name = typeof marketplace.name === "string" && marketplace.name.trim() !== "" ? marketplace.name : basename3(rootDir);
12929
13300
  return {
12930
13301
  name,
12931
13302
  rootDir,
@@ -13396,7 +13767,16 @@ var KNOWN_SKILL_NAMES = [
13396
13767
  "clear-assignment",
13397
13768
  "save-session-summary",
13398
13769
  "track-session",
13399
- "track-server"
13770
+ "track-server",
13771
+ "capture-artifacts",
13772
+ "replan",
13773
+ "resume-session",
13774
+ "syntaur-worktree",
13775
+ "add-resource",
13776
+ "add-memory",
13777
+ "list-assignments",
13778
+ "log-progress",
13779
+ "set-workspace"
13400
13780
  ];
13401
13781
  var KNOWN_SKILLS = KNOWN_SKILL_NAMES;
13402
13782
  async function getSkillsDir() {
@@ -13459,8 +13839,8 @@ async function skillMatches(srcDir, destDir) {
13459
13839
  }
13460
13840
  async function isSymlink(path) {
13461
13841
  try {
13462
- const stat6 = await lstat2(path);
13463
- return stat6.isSymbolicLink();
13842
+ const stat9 = await lstat2(path);
13843
+ return stat9.isSymbolicLink();
13464
13844
  } catch {
13465
13845
  return false;
13466
13846
  }
@@ -13728,7 +14108,7 @@ async function installPluginCommand(options) {
13728
14108
  }
13729
14109
  }
13730
14110
  console.log("\nThe plugin is now available in Claude Code.");
13731
- console.log(" Slash commands: /grab-assignment, /plan-assignment, /complete-assignment, /create-assignment, /create-project, /track-session, /clear-assignment, /manage-statuses");
14111
+ console.log(" Slash commands: /grab-assignment, /plan-assignment, /complete-assignment, /create-assignment, /create-project, /track-session, /clear-assignment, /manage-statuses, /replan, /resume-session, /syntaur-worktree, /add-resource, /add-memory, /list-assignments, /log-progress, /set-workspace");
13732
14112
  console.log(" Background: syntaur-protocol skill (auto-invoked)");
13733
14113
  console.log(" Hook: write boundary enforcement (PreToolUse) + SessionStart/End");
13734
14114
  }
@@ -14281,10 +14661,10 @@ async function installCodexPluginCommand(options) {
14281
14661
  }
14282
14662
  console.log("\nThe plugin is now available to Codex.");
14283
14663
  console.log(
14284
- " Protocol skills: syntaur-protocol, create-project, create-assignment, grab-assignment, plan-assignment, complete-assignment, save-session-summary"
14664
+ " Protocol skills: syntaur-protocol, create-project, create-assignment, grab-assignment, plan-assignment, complete-assignment, save-session-summary, manage-statuses, clear-assignment, replan, resume-session, syntaur-worktree, add-resource, add-memory, list-assignments, log-progress, set-workspace"
14285
14665
  );
14286
14666
  console.log(" Codex-specific: track-session skill (rollout path aware)");
14287
- console.log(" Slash commands: /track-session, /save-session-summary (no PreCompact hook on Codex \u2014 invoke manually before compaction)");
14667
+ console.log(" Slash commands: /track-session, /save-session-summary, /replan, /resume-session, /syntaur-worktree, /add-resource, /add-memory, /list-assignments, /log-progress, /set-workspace (no PreCompact hook on Codex \u2014 invoke save-session-summary manually before compaction)");
14288
14668
  console.log(" Hooks: write boundary enforcement, session cleanup");
14289
14669
  }
14290
14670
 
@@ -14562,8 +14942,8 @@ async function setupAdapterCommand(framework, options) {
14562
14942
  `Project "${options.project}" not found at ${projectDir}.`
14563
14943
  );
14564
14944
  }
14565
- const assignmentMdPath = resolve31(assignmentDir, "assignment.md");
14566
- if (!await fileExists(assignmentDir) || !await fileExists(assignmentMdPath)) {
14945
+ const assignmentMdPath2 = resolve31(assignmentDir, "assignment.md");
14946
+ if (!await fileExists(assignmentDir) || !await fileExists(assignmentMdPath2)) {
14567
14947
  throw new Error(
14568
14948
  `Assignment "${options.assignment}" not found at ${assignmentDir}.`
14569
14949
  );
@@ -14821,7 +15201,7 @@ function computeWorktreeDefaults(opts) {
14821
15201
  const repository = opts.existing.repository ?? detectCurrentGitRoot();
14822
15202
  const branch = opts.projectSlug ? `syntaur/${opts.projectSlug}/${opts.assignmentSlug}` : `syntaur/${opts.assignmentSlug}`;
14823
15203
  const parentBranch = opts.existing.parentBranch ?? detectCurrentBranch() ?? "main";
14824
- const worktreeBase = resolve34(
15204
+ const worktreeBase = repository ? resolve34(repository, ".worktrees", branch) : resolve34(
14825
15205
  syntaurRoot(),
14826
15206
  "worktrees",
14827
15207
  opts.projectSlug || "standalone",
@@ -15524,9 +15904,9 @@ async function promoteTodos(ids, options) {
15524
15904
  throw new Error(`Invalid --to-assignment target "${target}". Use <project>/<slug> or a bare UUID.`);
15525
15905
  }
15526
15906
  const { fileExists: fileExists2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
15527
- const assignmentMdPath = resolvePath2(assignmentDir, "assignment.md");
15528
- if (!await fileExists2(assignmentMdPath)) {
15529
- throw new Error(`Target assignment not found: ${assignmentMdPath}`);
15907
+ const assignmentMdPath2 = resolvePath2(assignmentDir, "assignment.md");
15908
+ if (!await fileExists2(assignmentMdPath2)) {
15909
+ throw new Error(`Target assignment not found: ${assignmentMdPath2}`);
15530
15910
  }
15531
15911
  await injectPromotedTodos(assignmentDir, todos, scopeLabel);
15532
15912
  if (!options.keepSource) {
@@ -15541,12 +15921,12 @@ function describeScope(scope) {
15541
15921
  }
15542
15922
  async function injectPromotedTodos(assignmentDir, todos, scopeLabel) {
15543
15923
  const { resolve: resolvePath2 } = await import("path");
15544
- const { readFile: readFile35 } = await import("fs/promises");
15924
+ const { readFile: readFile43 } = await import("fs/promises");
15545
15925
  const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
15546
15926
  const { appendTodosToAssignmentBody: appendTodosToAssignmentBody2, touchAssignmentUpdated: touchAssignmentUpdated2 } = await Promise.resolve().then(() => (init_assignment_todos(), assignment_todos_exports));
15547
15927
  const { nowTimestamp: nowTimestamp3 } = await Promise.resolve().then(() => (init_timestamp(), timestamp_exports));
15548
- const assignmentMdPath = resolvePath2(assignmentDir, "assignment.md");
15549
- let content = await readFile35(assignmentMdPath, "utf-8");
15928
+ const assignmentMdPath2 = resolvePath2(assignmentDir, "assignment.md");
15929
+ let content = await readFile43(assignmentMdPath2, "utf-8");
15550
15930
  content = appendTodosToAssignmentBody2(
15551
15931
  content,
15552
15932
  todos.map((t) => ({
@@ -15555,7 +15935,7 @@ async function injectPromotedTodos(assignmentDir, todos, scopeLabel) {
15555
15935
  }))
15556
15936
  );
15557
15937
  content = touchAssignmentUpdated2(content, nowTimestamp3());
15558
- await writeFileForce2(assignmentMdPath, content);
15938
+ await writeFileForce2(assignmentMdPath2, content);
15559
15939
  }
15560
15940
  async function markPromotedComplete(todos, todosPath, workspace, checklist, _scope, summary) {
15561
15941
  for (const item of todos) {
@@ -15591,8 +15971,8 @@ todoCommand.command("plan").description("Create or open a plan directory for a t
15591
15971
  }
15592
15972
  const planDir = todoPlanDir(todosPath, workspace, id);
15593
15973
  await ensureDir(planDir);
15594
- const { readdir: readdir19 } = await import("fs/promises");
15595
- const existingFiles = (await readdir19(planDir).catch(() => [])).filter(
15974
+ const { readdir: readdir22 } = await import("fs/promises");
15975
+ const existingFiles = (await readdir22(planDir).catch(() => [])).filter(
15596
15976
  (f) => /^plan(?:-v\d+)?\.md$/.test(f)
15597
15977
  );
15598
15978
  let target;
@@ -15677,10 +16057,10 @@ async function moveTodo(id, options) {
15677
16057
  if (await fileExists(newPlanDir)) {
15678
16058
  throw new Error(`Plan directory already exists at target: ${newPlanDir}; refusing to move.`);
15679
16059
  }
15680
- const { rename: rename7, mkdir: mkdir7 } = await import("fs/promises");
16060
+ const { rename: rename8, mkdir: mkdir8 } = await import("fs/promises");
15681
16061
  const { dirname: dirname16 } = await import("path");
15682
- await mkdir7(dirname16(newPlanDir), { recursive: true });
15683
- await rename7(item.planDir, newPlanDir);
16062
+ await mkdir8(dirname16(newPlanDir), { recursive: true });
16063
+ await rename8(item.planDir, newPlanDir);
15684
16064
  item.planDir = newPlanDir;
15685
16065
  }
15686
16066
  sourceChecklist.items.splice(idx, 1);
@@ -15796,6 +16176,8 @@ backupCommand.command("config").description("Show or update backup configuration
15796
16176
 
15797
16177
  // src/commands/doctor.ts
15798
16178
  import { Command as Command3 } from "commander";
16179
+ import { readFile as readFile31 } from "fs/promises";
16180
+ import { isAbsolute as isAbsolute7, resolve as resolve47 } from "path";
15799
16181
 
15800
16182
  // src/utils/doctor/index.ts
15801
16183
  import { fileURLToPath as fileURLToPath6 } from "url";
@@ -15812,14 +16194,14 @@ async function buildCheckContext(cwd = process.cwd()) {
15812
16194
  const config = await readConfig();
15813
16195
  const root = syntaurRoot();
15814
16196
  const dbPath = resolve38(root, "syntaur.db");
15815
- let db2 = null;
16197
+ let db3 = null;
15816
16198
  let dbError = null;
15817
16199
  if (await fileExists(dbPath)) {
15818
16200
  try {
15819
- db2 = new Database2(dbPath, { readonly: true, fileMustExist: true });
16201
+ db3 = new Database2(dbPath, { readonly: true, fileMustExist: true });
15820
16202
  } catch (err2) {
15821
16203
  dbError = err2 instanceof Error ? err2.message : String(err2);
15822
- db2 = null;
16204
+ db3 = null;
15823
16205
  }
15824
16206
  } else {
15825
16207
  dbError = `database file not found at ${dbPath}`;
@@ -15827,7 +16209,7 @@ async function buildCheckContext(cwd = process.cwd()) {
15827
16209
  return {
15828
16210
  config,
15829
16211
  syntaurRoot: root,
15830
- db: db2,
16212
+ db: db3,
15831
16213
  dbError,
15832
16214
  cwd,
15833
16215
  now: /* @__PURE__ */ new Date()
@@ -17121,7 +17503,7 @@ function skipped(check, reason) {
17121
17503
 
17122
17504
  // src/utils/doctor/checks/integrations.ts
17123
17505
  init_fs();
17124
- import { resolve as resolve44, dirname as dirname12, basename as basename2 } from "path";
17506
+ import { resolve as resolve44, dirname as dirname12, basename as basename4 } from "path";
17125
17507
  import { readdir as readdir16, readFile as readFile27 } from "fs/promises";
17126
17508
  import { homedir as homedir6 } from "os";
17127
17509
  var CATEGORY6 = "integrations";
@@ -17224,7 +17606,7 @@ var claudeMarketplaceRegistered = {
17224
17606
  return skipped2(this, "claudePluginDir does not exist (run install-plugin)");
17225
17607
  }
17226
17608
  const pluginsParent = dirname12(dir);
17227
- if (basename2(pluginsParent) !== "plugins") {
17609
+ if (basename4(pluginsParent) !== "plugins") {
17228
17610
  return {
17229
17611
  id: this.id,
17230
17612
  category: this.category,
@@ -17272,7 +17654,7 @@ var claudeMarketplaceRegistered = {
17272
17654
  autoFixable: false
17273
17655
  };
17274
17656
  }
17275
- const marketplaceName = parsed.name ?? basename2(marketplaceRoot);
17657
+ const marketplaceName = parsed.name ?? basename4(marketplaceRoot);
17276
17658
  const hasSyntaurEntry = (parsed.plugins ?? []).some((p) => p?.name === "syntaur");
17277
17659
  const known = await readKnownMarketplaces();
17278
17660
  const registered = known[marketplaceName]?.installLocation === marketplaceRoot || Object.values(known).some((v) => v.installLocation === marketplaceRoot);
@@ -17865,7 +18247,93 @@ function renderJson(report) {
17865
18247
  // src/commands/doctor.ts
17866
18248
  init_fs();
17867
18249
  init_paths();
17868
- var doctorCommand = new Command3("doctor").description("Diagnose Syntaur state and surface issues with suggested fixes").option("--json", "Emit structured JSON for programmatic consumers").option("--fix", "Apply safe auto-fixes (v1: no-op; reserved for future checks)").option("--only <check-id>", "Run only the check with this ID").option("--verbose", "Include passing checks in human output").action(async (options) => {
18250
+ init_frontmatter();
18251
+ var REQUIRED_WORKSPACE_FIELDS = [
18252
+ "repository",
18253
+ "worktreePath",
18254
+ "branch",
18255
+ "parentBranch"
18256
+ ];
18257
+ var ISO_DATE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;
18258
+ async function validateAssignmentFile(inputPath, cwd = process.cwd()) {
18259
+ const absolute = isAbsolute7(inputPath) ? inputPath : resolve47(cwd, inputPath);
18260
+ const errors = [];
18261
+ const warnings = [];
18262
+ if (!await fileExists(absolute)) {
18263
+ return {
18264
+ ok: false,
18265
+ path: absolute,
18266
+ errors: [`File does not exist: ${absolute}`],
18267
+ warnings: []
18268
+ };
18269
+ }
18270
+ let content;
18271
+ try {
18272
+ content = await readFile31(absolute, "utf-8");
18273
+ } catch (err2) {
18274
+ return {
18275
+ ok: false,
18276
+ path: absolute,
18277
+ errors: [
18278
+ `Could not read file: ${err2 instanceof Error ? err2.message : String(err2)}`
18279
+ ],
18280
+ warnings: []
18281
+ };
18282
+ }
18283
+ if (!content.startsWith("---")) {
18284
+ errors.push("Missing YAML frontmatter \u2014 file does not start with `---`.");
18285
+ }
18286
+ let parsed;
18287
+ try {
18288
+ parsed = parseAssignmentFrontmatter(content);
18289
+ } catch (err2) {
18290
+ errors.push(
18291
+ `Frontmatter parse error: ${err2 instanceof Error ? err2.message : String(err2)}`
18292
+ );
18293
+ return { ok: false, path: absolute, errors, warnings };
18294
+ }
18295
+ if (!parsed.id) errors.push("Missing or empty `id` field.");
18296
+ if (!parsed.slug) errors.push("Missing or empty `slug` field.");
18297
+ if (!parsed.title) errors.push("Missing or empty `title` field.");
18298
+ if (!parsed.status) errors.push("Missing or empty `status` field.");
18299
+ if (!parsed.created) errors.push("Missing or empty `created` field.");
18300
+ else if (!ISO_DATE.test(parsed.created))
18301
+ warnings.push(`Field \`created\` is not ISO 8601: "${parsed.created}".`);
18302
+ if (!parsed.updated) errors.push("Missing or empty `updated` field.");
18303
+ else if (!ISO_DATE.test(parsed.updated))
18304
+ warnings.push(`Field \`updated\` is not ISO 8601: "${parsed.updated}".`);
18305
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
18306
+ const fmText = fmMatch ? fmMatch[1] : "";
18307
+ for (const key of REQUIRED_WORKSPACE_FIELDS) {
18308
+ if (!new RegExp(`^\\s+${key}:`, "m").test(fmText)) {
18309
+ errors.push(`Workspace block is missing required field \`${key}\`.`);
18310
+ }
18311
+ }
18312
+ return { ok: errors.length === 0, path: absolute, errors, warnings };
18313
+ }
18314
+ var doctorCommand = new Command3("doctor").description("Diagnose Syntaur state and surface issues with suggested fixes").option("--json", "Emit structured JSON for programmatic consumers").option("--fix", "Apply safe auto-fixes (v1: no-op; reserved for future checks)").option("--only <check-id>", "Run only the check with this ID").option("--verbose", "Include passing checks in human output").option(
18315
+ "--assignment <path>",
18316
+ "Validate a single assignment.md frontmatter and exit (used by set-workspace skill pre-write)"
18317
+ ).action(async (options) => {
18318
+ if (options.assignment) {
18319
+ const result = await validateAssignmentFile(options.assignment);
18320
+ if (options.json) {
18321
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
18322
+ } else if (result.ok) {
18323
+ process.stdout.write(`OK ${result.path}
18324
+ `);
18325
+ for (const w of result.warnings) process.stdout.write(` warn: ${w}
18326
+ `);
18327
+ } else {
18328
+ process.stderr.write(`FAIL ${result.path}
18329
+ `);
18330
+ for (const e of result.errors) process.stderr.write(` error: ${e}
18331
+ `);
18332
+ for (const w of result.warnings) process.stderr.write(` warn: ${w}
18333
+ `);
18334
+ }
18335
+ process.exit(result.ok ? 0 : 1);
18336
+ }
17869
18337
  if (!await fileExists(syntaurRoot())) {
17870
18338
  const msg = "~/.syntaur/ does not exist. Run `syntaur init` first.";
17871
18339
  if (options.json) {
@@ -18125,8 +18593,8 @@ init_uuid();
18125
18593
  init_timestamp();
18126
18594
  init_assignment_resolver();
18127
18595
  init_templates();
18128
- import { resolve as resolve47 } from "path";
18129
- import { readFile as readFile31 } from "fs/promises";
18596
+ import { resolve as resolve48 } from "path";
18597
+ import { readFile as readFile32 } from "fs/promises";
18130
18598
  function shortId() {
18131
18599
  return generateId().split("-")[0];
18132
18600
  }
@@ -18156,7 +18624,7 @@ async function commentCommand(target, text, options = {}) {
18156
18624
  if (!isValidSlug(target)) {
18157
18625
  throw new Error(`Invalid assignment slug "${target}".`);
18158
18626
  }
18159
- assignmentDir = resolve47(baseDir, options.project, "assignments", target);
18627
+ assignmentDir = resolve48(baseDir, options.project, "assignments", target);
18160
18628
  assignmentRef = target;
18161
18629
  } else {
18162
18630
  const resolved = await resolveAssignmentById(baseDir, assignmentsDir(), target);
@@ -18166,13 +18634,13 @@ async function commentCommand(target, text, options = {}) {
18166
18634
  assignmentDir = resolved.assignmentDir;
18167
18635
  assignmentRef = resolved.standalone ? resolved.id : resolved.assignmentSlug;
18168
18636
  }
18169
- const commentsPath = resolve47(assignmentDir, "comments.md");
18637
+ const commentsPath = resolve48(assignmentDir, "comments.md");
18170
18638
  const timestamp = nowTimestamp();
18171
18639
  const author = options.author ?? process.env.USER ?? "unknown";
18172
18640
  let currentContent;
18173
18641
  let currentCount = 0;
18174
18642
  if (await fileExists(commentsPath)) {
18175
- currentContent = await readFile31(commentsPath, "utf-8");
18643
+ currentContent = await readFile32(commentsPath, "utf-8");
18176
18644
  const countMatch = currentContent.match(/^entryCount:\s*(\d+)/m);
18177
18645
  if (countMatch) currentCount = parseInt(countMatch[1], 10);
18178
18646
  } else {
@@ -18205,77 +18673,1578 @@ ${entry}`;
18205
18673
  }
18206
18674
  }
18207
18675
 
18208
- // src/commands/request.ts
18676
+ // src/commands/capture.ts
18677
+ import { resolve as resolve51, relative as relative4 } from "path";
18678
+ import { copyFile as copyFile3, mkdir as mkdir7, realpath as realpath2, stat as stat6 } from "fs/promises";
18679
+
18680
+ // src/utils/assignment-target.ts
18209
18681
  init_paths();
18210
18682
  init_fs();
18211
18683
  init_config2();
18212
18684
  init_slug();
18213
- init_timestamp();
18214
18685
  init_assignment_resolver();
18215
- init_assignment_todos();
18216
- import { resolve as resolve48 } from "path";
18217
- import { readFile as readFile32 } from "fs/promises";
18218
- async function requestCommand(target, text, options = {}) {
18219
- if (!text || !text.trim()) {
18220
- throw new Error("Request text cannot be empty.");
18221
- }
18222
- const config = await readConfig();
18223
- const baseDir = options.dir ? expandHome(options.dir) : config.defaultProjectDir;
18224
- let assignmentDir;
18225
- let targetRef;
18226
- if (options.project) {
18227
- if (!isValidSlug(options.project)) {
18228
- throw new Error(`Invalid project slug "${options.project}".`);
18229
- }
18230
- if (!isValidSlug(target)) {
18231
- throw new Error(`Invalid assignment slug "${target}".`);
18232
- }
18233
- assignmentDir = resolve48(baseDir, options.project, "assignments", target);
18234
- targetRef = target;
18235
- } else {
18236
- const resolved = await resolveAssignmentById(baseDir, assignmentsDir(), target);
18237
- if (!resolved) {
18238
- throw new Error(`Target assignment "${target}" not found. Provide --project <slug> or a valid UUID.`);
18239
- }
18240
- assignmentDir = resolved.assignmentDir;
18241
- targetRef = resolved.standalone ? resolved.id : resolved.assignmentSlug;
18242
- }
18243
- const assignmentMdPath = resolve48(assignmentDir, "assignment.md");
18244
- if (!await fileExists(assignmentMdPath)) {
18245
- throw new Error(`assignment.md not found at ${assignmentMdPath}`);
18686
+ init_parser();
18687
+ import { resolve as resolve49 } from "path";
18688
+ import { readFile as readFile33 } from "fs/promises";
18689
+ var AssignmentTargetError = class extends Error {
18690
+ };
18691
+ async function readAssignmentFrontmatterId(assignmentDir) {
18692
+ const path = resolve49(assignmentDir, "assignment.md");
18693
+ if (!await fileExists(path)) return null;
18694
+ try {
18695
+ const content = await readFile33(path, "utf-8");
18696
+ const [fm] = extractFrontmatter(content);
18697
+ return getField(fm, "id");
18698
+ } catch {
18699
+ return null;
18246
18700
  }
18247
- const source = options.from ?? process.env.SYNTAUR_ASSIGNMENT ?? "unknown";
18248
- let content = await readFile32(assignmentMdPath, "utf-8");
18249
- content = appendTodosToAssignmentBody(content, [
18250
- { description: `${text.trim()} (from: ${source})` }
18251
- ]);
18252
- content = touchAssignmentUpdated(content, nowTimestamp());
18253
- await writeFileForce(assignmentMdPath, content);
18254
- console.log(`Added todo to ${targetRef}: ${text.trim()} (from: ${source})`);
18255
18701
  }
18256
-
18257
- // src/cli-default-command.ts
18258
- init_config2();
18259
- import { readdir as readdir18 } from "fs/promises";
18260
- async function hasAnyProjectContent(projectsDir2) {
18702
+ async function readContextJson(cwd) {
18703
+ const path = resolve49(cwd, ".syntaur", "context.json");
18704
+ if (!await fileExists(path)) return null;
18261
18705
  try {
18262
- const entries = await readdir18(projectsDir2, { withFileTypes: true });
18263
- return entries.some((entry) => entry.isDirectory());
18706
+ const raw = await readFile33(path, "utf-8");
18707
+ return JSON.parse(raw);
18264
18708
  } catch {
18265
- return false;
18709
+ return null;
18266
18710
  }
18267
18711
  }
18268
- async function isSetupComplete() {
18712
+ async function resolveAssignmentTarget(input4, opts = {}) {
18269
18713
  const config = await readConfig();
18270
- if (config.onboarding.completed) {
18271
- return true;
18714
+ const baseDir = opts.dir ? expandHome(opts.dir) : config.defaultProjectDir;
18715
+ if (opts.project) {
18716
+ if (!input4) {
18717
+ throw new AssignmentTargetError(
18718
+ "--project requires an assignment slug as a positional argument."
18719
+ );
18720
+ }
18721
+ if (!isValidSlug(opts.project)) {
18722
+ throw new AssignmentTargetError(`Invalid project slug "${opts.project}".`);
18723
+ }
18724
+ if (!isValidSlug(input4)) {
18725
+ throw new AssignmentTargetError(`Invalid assignment slug "${input4}".`);
18726
+ }
18727
+ const projectDir = resolve49(baseDir, opts.project);
18728
+ const projectMdPath = resolve49(projectDir, "project.md");
18729
+ if (!await fileExists(projectDir) || !await fileExists(projectMdPath)) {
18730
+ throw new AssignmentTargetError(
18731
+ `Project "${opts.project}" not found at ${projectDir}.`
18732
+ );
18733
+ }
18734
+ const assignmentDir = resolve49(projectDir, "assignments", input4);
18735
+ const assignmentMdPath2 = resolve49(assignmentDir, "assignment.md");
18736
+ if (!await fileExists(assignmentMdPath2)) {
18737
+ throw new AssignmentTargetError(
18738
+ `Assignment "${input4}" not found in project "${opts.project}".`
18739
+ );
18740
+ }
18741
+ const id = await readAssignmentFrontmatterId(assignmentDir) ?? input4;
18742
+ return {
18743
+ assignmentDir,
18744
+ projectSlug: opts.project,
18745
+ assignmentSlug: input4,
18746
+ id,
18747
+ standalone: false,
18748
+ workspaceGroup: null
18749
+ };
18272
18750
  }
18273
- const [
18274
- claudePluginDir,
18275
- codexPluginDir,
18276
- codexMarketplacePath,
18277
- hasProjectContent
18278
- ] = await Promise.all([
18751
+ if (input4) {
18752
+ const resolved = await resolveAssignmentById(baseDir, assignmentsDir(), input4);
18753
+ if (!resolved) {
18754
+ throw new AssignmentTargetError(
18755
+ `Assignment "${input4}" not found. Provide --project <slug> + <slug> or a valid standalone UUID.`
18756
+ );
18757
+ }
18758
+ return resolved;
18759
+ }
18760
+ const cwd = opts.cwd ?? process.cwd();
18761
+ const ctx = await readContextJson(cwd);
18762
+ if (!ctx) {
18763
+ throw new AssignmentTargetError(
18764
+ "No assignment specified. Provide an argument, --project + slug, or run from a directory with .syntaur/context.json."
18765
+ );
18766
+ }
18767
+ if (ctx.assignmentDir) {
18768
+ const dir = expandHome(ctx.assignmentDir);
18769
+ const assignmentMdPath2 = resolve49(dir, "assignment.md");
18770
+ if (!await fileExists(assignmentMdPath2)) {
18771
+ throw new AssignmentTargetError(
18772
+ `.syntaur/context.json points to a missing assignment dir: ${dir}.`
18773
+ );
18774
+ }
18775
+ const id = await readAssignmentFrontmatterId(dir);
18776
+ if (!id || id.trim() === "") {
18777
+ throw new AssignmentTargetError(
18778
+ `.syntaur/context.json points to an assignment with no frontmatter \`id\`: ${dir}.`
18779
+ );
18780
+ }
18781
+ const assignmentSlug = ctx.assignmentSlug ?? dir.split("/").pop() ?? "";
18782
+ const projectSlug = ctx.projectSlug ?? null;
18783
+ return {
18784
+ assignmentDir: dir,
18785
+ projectSlug,
18786
+ assignmentSlug,
18787
+ id,
18788
+ standalone: projectSlug === null,
18789
+ workspaceGroup: null
18790
+ };
18791
+ }
18792
+ if (ctx.projectSlug && ctx.assignmentSlug) {
18793
+ if (!isValidSlug(ctx.projectSlug) || !isValidSlug(ctx.assignmentSlug)) {
18794
+ throw new AssignmentTargetError(
18795
+ `.syntaur/context.json contains invalid slugs: project="${ctx.projectSlug}" assignment="${ctx.assignmentSlug}".`
18796
+ );
18797
+ }
18798
+ const assignmentDir = resolve49(baseDir, ctx.projectSlug, "assignments", ctx.assignmentSlug);
18799
+ const assignmentMdPath2 = resolve49(assignmentDir, "assignment.md");
18800
+ if (!await fileExists(assignmentMdPath2)) {
18801
+ throw new AssignmentTargetError(
18802
+ `.syntaur/context.json points to a missing assignment: ${assignmentDir}.`
18803
+ );
18804
+ }
18805
+ const id = await readAssignmentFrontmatterId(assignmentDir) ?? ctx.assignmentSlug;
18806
+ return {
18807
+ assignmentDir,
18808
+ projectSlug: ctx.projectSlug,
18809
+ assignmentSlug: ctx.assignmentSlug,
18810
+ id,
18811
+ standalone: false,
18812
+ workspaceGroup: null
18813
+ };
18814
+ }
18815
+ throw new AssignmentTargetError(
18816
+ ".syntaur/context.json exists but contains neither assignmentDir nor projectSlug+assignmentSlug."
18817
+ );
18818
+ }
18819
+
18820
+ // src/commands/capture.ts
18821
+ init_paths();
18822
+
18823
+ // src/utils/proof-artifact-id.ts
18824
+ import { randomBytes as randomBytes2 } from "crypto";
18825
+ function generateArtifactId() {
18826
+ const ts = Date.now().toString(36);
18827
+ const rand = randomBytes2(2).toString("hex");
18828
+ return `${ts}-${rand}`;
18829
+ }
18830
+ var EXTENSIONS = {
18831
+ screenshot: "png",
18832
+ video: "mp4",
18833
+ asciinema: "cast",
18834
+ http: "txt",
18835
+ text: "txt"
18836
+ };
18837
+ function extensionForKind(kind) {
18838
+ const ext = EXTENSIONS[kind];
18839
+ if (!ext) {
18840
+ throw new Error(`Unknown artifact kind: ${kind}`);
18841
+ }
18842
+ return ext;
18843
+ }
18844
+ var ARTIFACT_KINDS = [
18845
+ "screenshot",
18846
+ "video",
18847
+ "asciinema",
18848
+ "http",
18849
+ "text"
18850
+ ];
18851
+ function isArtifactKind(value) {
18852
+ return typeof value === "string" && ARTIFACT_KINDS.includes(value);
18853
+ }
18854
+
18855
+ // src/commands/capture.ts
18856
+ init_fs();
18857
+
18858
+ // src/db/proof-db.ts
18859
+ init_paths();
18860
+ import Database3 from "better-sqlite3";
18861
+ import { resolve as resolve50 } from "path";
18862
+ var db2 = null;
18863
+ var PROOF_SCHEMA_VERSION = "1";
18864
+ var SCHEMA_SQL2 = `
18865
+ CREATE TABLE IF NOT EXISTS artifacts (
18866
+ id TEXT PRIMARY KEY,
18867
+ assignment_id TEXT NOT NULL,
18868
+ assignment_dir TEXT NOT NULL,
18869
+ criterion_index INTEGER,
18870
+ kind TEXT NOT NULL,
18871
+ file_path TEXT,
18872
+ note TEXT,
18873
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
18874
+ );
18875
+ CREATE INDEX IF NOT EXISTS idx_artifacts_assignment ON artifacts(assignment_id);
18876
+ CREATE INDEX IF NOT EXISTS idx_artifacts_assignment_criterion ON artifacts(assignment_id, criterion_index);
18877
+ CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);
18878
+ `;
18879
+ function initProofDb(dbPath) {
18880
+ if (db2) return db2;
18881
+ const finalPath = dbPath ?? resolve50(syntaurRoot(), "syntaur.db");
18882
+ db2 = new Database3(finalPath);
18883
+ db2.pragma("journal_mode = WAL");
18884
+ db2.exec(SCHEMA_SQL2);
18885
+ db2.prepare("INSERT OR IGNORE INTO meta (key, value) VALUES (?, ?)").run(
18886
+ "proof_schema_version",
18887
+ PROOF_SCHEMA_VERSION
18888
+ );
18889
+ const database = db2;
18890
+ const runMigrations = database.transaction(() => {
18891
+ });
18892
+ runMigrations.exclusive();
18893
+ return db2;
18894
+ }
18895
+ function getProofDb() {
18896
+ if (!db2) {
18897
+ throw new Error("Proof database not initialized. Call initProofDb() first.");
18898
+ }
18899
+ return db2;
18900
+ }
18901
+ function insertArtifact(input4) {
18902
+ const database = getProofDb();
18903
+ database.prepare(
18904
+ `INSERT INTO artifacts (id, assignment_id, assignment_dir, criterion_index, kind, file_path, note)
18905
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
18906
+ ).run(
18907
+ input4.id,
18908
+ input4.assignmentId,
18909
+ input4.assignmentDir,
18910
+ input4.criterionIndex,
18911
+ input4.kind,
18912
+ input4.filePath,
18913
+ input4.note
18914
+ );
18915
+ }
18916
+ function listArtifactsByAssignment(assignmentId) {
18917
+ const database = getProofDb();
18918
+ const rows = database.prepare(
18919
+ `SELECT id, assignment_id, assignment_dir, criterion_index, kind, file_path, note, created_at
18920
+ FROM artifacts
18921
+ WHERE assignment_id = ?
18922
+ ORDER BY (criterion_index IS NULL), criterion_index, created_at`
18923
+ ).all(assignmentId);
18924
+ return rows;
18925
+ }
18926
+
18927
+ // src/commands/capture.ts
18928
+ var MAX_ID_RETRIES = 5;
18929
+ function normalizeCriterionIndex(raw) {
18930
+ if (raw === void 0 || raw === null || raw === "") return null;
18931
+ if (typeof raw === "number") {
18932
+ if (!Number.isInteger(raw) || raw < 0) {
18933
+ throw new Error(`--criterion must be a non-negative integer (got "${raw}")`);
18934
+ }
18935
+ return raw;
18936
+ }
18937
+ const s = String(raw).trim();
18938
+ if (!/^\d+$/.test(s)) {
18939
+ throw new Error(`--criterion must be a non-negative integer (got "${raw}")`);
18940
+ }
18941
+ return parseInt(s, 10);
18942
+ }
18943
+ function isUniqueConstraintError(err2) {
18944
+ if (!err2 || typeof err2 !== "object") return false;
18945
+ const e = err2;
18946
+ if (e.code === "SQLITE_CONSTRAINT_PRIMARYKEY" || e.code === "SQLITE_CONSTRAINT_UNIQUE") return true;
18947
+ if (e.code === "SQLITE_CONSTRAINT" && /UNIQUE|PRIMARY KEY/i.test(e.message ?? "")) return true;
18948
+ return false;
18949
+ }
18950
+ async function captureCommand(target, options = {}) {
18951
+ if (!options.kind) {
18952
+ throw new Error(
18953
+ `--kind is required. Must be one of: ${ARTIFACT_KINDS.join(", ")}.`
18954
+ );
18955
+ }
18956
+ if (!isArtifactKind(options.kind)) {
18957
+ throw new Error(
18958
+ `Invalid --kind "${options.kind}". Must be one of: ${ARTIFACT_KINDS.join(", ")}.`
18959
+ );
18960
+ }
18961
+ const kind = options.kind;
18962
+ if (kind === "text") {
18963
+ if (options.file) {
18964
+ throw new Error("--kind=text forbids --file. Use --note for text payloads.");
18965
+ }
18966
+ if (!options.note || options.note.trim() === "") {
18967
+ throw new Error("--kind=text requires --note.");
18968
+ }
18969
+ } else {
18970
+ if (kind !== "http" && !options.file) {
18971
+ throw new Error(`--kind=${kind} requires --file.`);
18972
+ }
18973
+ if (kind === "http" && !options.file && (!options.note || options.note.trim() === "")) {
18974
+ throw new Error("--kind=http requires --file or --note (one or the other).");
18975
+ }
18976
+ }
18977
+ const criterionIndex = normalizeCriterionIndex(options.criterion);
18978
+ const resolved = await resolveAssignmentTarget(target, {
18979
+ project: options.project,
18980
+ dir: options.dir,
18981
+ cwd: options.cwd
18982
+ });
18983
+ let resolvedSource = null;
18984
+ if (options.file) {
18985
+ const expanded = options.file.startsWith("~/") ? resolve51(process.env.HOME ?? "", options.file.slice(2)) : resolve51(options.file);
18986
+ if (!await fileExists(expanded)) {
18987
+ throw new Error(`--file does not exist: ${options.file}`);
18988
+ }
18989
+ let real;
18990
+ try {
18991
+ real = await realpath2(expanded);
18992
+ } catch (e) {
18993
+ throw new Error(
18994
+ `--file is unreadable: ${options.file} (${e instanceof Error ? e.message : String(e)})`
18995
+ );
18996
+ }
18997
+ const st = await stat6(real);
18998
+ if (!st.isFile()) {
18999
+ throw new Error(`--file is not a regular file: ${options.file}`);
19000
+ }
19001
+ resolvedSource = real;
19002
+ }
19003
+ if (!resolved.id || resolved.id.trim() === "") {
19004
+ throw new Error(
19005
+ `Resolved assignment is missing a frontmatter \`id\`: ${resolved.assignmentDir}. Cannot record artifact.`
19006
+ );
19007
+ }
19008
+ initProofDb();
19009
+ const subdir = criterionIndex === null ? "untagged" : String(criterionIndex);
19010
+ const destDir = resolve51(proofDir(resolved.assignmentDir), subdir);
19011
+ if (resolvedSource) await mkdir7(destDir, { recursive: true });
19012
+ const ext = resolvedSource ? extensionForKind(kind) : null;
19013
+ let id = null;
19014
+ let relativeFilePath = null;
19015
+ let absPath = null;
19016
+ let lastErr = null;
19017
+ for (let attempt = 0; attempt < MAX_ID_RETRIES; attempt += 1) {
19018
+ const candidate = generateArtifactId();
19019
+ const candidateAbsPath = resolvedSource && ext ? resolve51(destDir, `${candidate}.${ext}`) : null;
19020
+ const candidateRel = candidateAbsPath ? relative4(resolved.assignmentDir, candidateAbsPath) : null;
19021
+ try {
19022
+ insertArtifact({
19023
+ id: candidate,
19024
+ assignmentId: resolved.id,
19025
+ assignmentDir: resolved.assignmentDir,
19026
+ criterionIndex,
19027
+ kind,
19028
+ filePath: candidateRel,
19029
+ note: options.note ?? null
19030
+ });
19031
+ id = candidate;
19032
+ absPath = candidateAbsPath;
19033
+ relativeFilePath = candidateRel;
19034
+ break;
19035
+ } catch (e) {
19036
+ if (isUniqueConstraintError(e)) {
19037
+ lastErr = e;
19038
+ continue;
19039
+ }
19040
+ throw e;
19041
+ }
19042
+ }
19043
+ if (!id) {
19044
+ throw new Error(
19045
+ `Failed to generate a unique artifact id after ${MAX_ID_RETRIES} attempts: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`
19046
+ );
19047
+ }
19048
+ if (resolvedSource && absPath) {
19049
+ await copyFile3(resolvedSource, absPath);
19050
+ }
19051
+ const ref = resolved.standalone ? resolved.id : `${resolved.projectSlug}/${resolved.assignmentSlug}`;
19052
+ const tagSuffix = criterionIndex === null ? "untagged" : `criterion ${criterionIndex}`;
19053
+ console.log(`Captured artifact ${id} (${kind}) for ${ref} \u2014 ${tagSuffix}.`);
19054
+ if (relativeFilePath) {
19055
+ console.log(` file: ${resolve51(resolved.assignmentDir, relativeFilePath)}`);
19056
+ }
19057
+ }
19058
+
19059
+ // src/commands/proof.ts
19060
+ import { Command as Command5 } from "commander";
19061
+ import { readFile as readFile34, writeFile as writeFile10, rename as rename7, stat as stat7 } from "fs/promises";
19062
+ import { resolve as resolve52, relative as relative5, isAbsolute as isAbsolute8 } from "path";
19063
+ import { randomBytes as randomBytes3 } from "crypto";
19064
+
19065
+ // src/utils/acceptance-criteria-parse.ts
19066
+ var SECTION_HEADING = /^##\s+Acceptance Criteria\s*$/i;
19067
+ var NEXT_HEADING = /^#{1,2}\s+\S/;
19068
+ var CHECKBOX_LINE = /^\s*[-*]\s+\[( |x|X)\]\s+(.*)$/;
19069
+ function parseAcceptanceCriteria(content) {
19070
+ const fmMatch = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
19071
+ const body = fmMatch ? content.slice(fmMatch[0].length) : content;
19072
+ const lines = body.split("\n");
19073
+ const sectionStart = lines.findIndex((line) => SECTION_HEADING.test(line.trim()));
19074
+ if (sectionStart === -1) return [];
19075
+ let sectionEnd = lines.length;
19076
+ for (let i = sectionStart + 1; i < lines.length; i += 1) {
19077
+ if (NEXT_HEADING.test(lines[i].trim())) {
19078
+ sectionEnd = i;
19079
+ break;
19080
+ }
19081
+ }
19082
+ const criteria = [];
19083
+ for (let i = sectionStart + 1; i < sectionEnd; i += 1) {
19084
+ const m = lines[i].match(CHECKBOX_LINE);
19085
+ if (!m) continue;
19086
+ const checked = m[1].toLowerCase() === "x";
19087
+ const text = m[2].trim();
19088
+ criteria.push({ index: criteria.length, text, checked });
19089
+ }
19090
+ return criteria;
19091
+ }
19092
+
19093
+ // src/commands/proof.ts
19094
+ init_fs();
19095
+ init_paths();
19096
+
19097
+ // src/templates/proof-md.ts
19098
+ function renderArtifactBullet(a) {
19099
+ const parts = [];
19100
+ parts.push(`**${a.kind}**`);
19101
+ parts.push(`\`${a.id}\``);
19102
+ if (a.file_path) parts.push(`file: \`${a.file_path}\``);
19103
+ if (a.note) parts.push(`note: ${a.note}`);
19104
+ return `- ${parts.join(" \u2014 ")}`;
19105
+ }
19106
+ function renderProofMarkdown(params2) {
19107
+ const { assignment, title, generated, criteria, artifactsByCriterion, untagged, staleByOriginalIndex } = params2;
19108
+ const totalArtifacts = untagged.length + staleByOriginalIndex.length + Array.from(artifactsByCriterion.values()).reduce((sum, list) => sum + list.length, 0);
19109
+ const lines = [];
19110
+ lines.push("---");
19111
+ lines.push(`assignment: ${assignment}`);
19112
+ lines.push(`generated: "${generated}"`);
19113
+ lines.push("---");
19114
+ lines.push("");
19115
+ lines.push(`# Proof \u2014 ${title}`);
19116
+ lines.push("");
19117
+ if (totalArtifacts === 0) {
19118
+ lines.push("_No artifacts captured for this assignment yet._");
19119
+ lines.push("");
19120
+ return lines.join("\n");
19121
+ }
19122
+ for (const c2 of criteria) {
19123
+ const checkbox2 = c2.checked ? "[x]" : "[ ]";
19124
+ lines.push(`## ${c2.index}. ${checkbox2} ${c2.text}`);
19125
+ lines.push("");
19126
+ const tagged = artifactsByCriterion.get(c2.index) ?? [];
19127
+ if (tagged.length === 0) {
19128
+ lines.push("_no artifacts captured_");
19129
+ } else {
19130
+ for (const a of tagged) lines.push(renderArtifactBullet(a));
19131
+ }
19132
+ lines.push("");
19133
+ }
19134
+ if (untagged.length > 0 || staleByOriginalIndex.length > 0) {
19135
+ lines.push("## Other artifacts");
19136
+ lines.push("");
19137
+ for (const a of untagged) lines.push(renderArtifactBullet(a));
19138
+ for (const a of staleByOriginalIndex) {
19139
+ const note = a.note ? ` \u2014 note: ${a.note}` : "";
19140
+ const file = a.file_path ? ` \u2014 file: \`${a.file_path}\`` : "";
19141
+ lines.push(
19142
+ `- _(was tagged criterion ${a.criterion_index} \u2014 no longer present)_ **${a.kind}** \`${a.id}\`${file}${note}`
19143
+ );
19144
+ }
19145
+ lines.push("");
19146
+ }
19147
+ return lines.join("\n");
19148
+ }
19149
+
19150
+ // src/utils/escape-html.ts
19151
+ function escapeHtml(value) {
19152
+ if (value === null || value === void 0) return "";
19153
+ const s = typeof value === "string" ? value : String(value);
19154
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
19155
+ }
19156
+
19157
+ // src/templates/proof-html.ts
19158
+ var INLINE_TEXT_LIMIT_BYTES = 256 * 1024;
19159
+ var STYLE_BLOCK = `
19160
+ body { max-width: 920px; margin: 2rem auto; font-family: system-ui, -apple-system, sans-serif; padding: 0 1rem; color: #222; line-height: 1.5; }
19161
+ h1, h2, h3 { line-height: 1.25; }
19162
+ nav.toc { padding: 0.5rem 1rem; background: #f9f9f9; border-radius: 4px; margin-bottom: 2rem; }
19163
+ nav.toc ul { margin: 0; padding-left: 1.25rem; }
19164
+ section.criterion { padding: 1rem 0; border-bottom: 1px solid #eee; }
19165
+ section.criterion:last-of-type { border-bottom: none; }
19166
+ img, video { max-width: 100%; height: auto; border-radius: 4px; display: block; }
19167
+ pre { max-height: 480px; overflow: auto; padding: 1rem; background: #f5f5f5; border-radius: 4px; white-space: pre-wrap; word-break: break-word; }
19168
+ .artifact { margin: 1rem 0; }
19169
+ .artifact-meta { font-size: 0.85em; color: #666; margin-bottom: 0.25rem; }
19170
+ .untagged { color: #666; font-size: 0.9em; }
19171
+ .stale { color: #b04a4a; font-style: italic; font-size: 0.9em; }
19172
+ .empty-criterion { color: #888; font-style: italic; }
19173
+ a { color: #2563eb; }
19174
+ `;
19175
+ function renderArtifact(a, ctx) {
19176
+ const idEsc = escapeHtml(a.id);
19177
+ const noteHtml = a.note ? `<div class="artifact-meta">note: ${escapeHtml(a.note)}</div>` : "";
19178
+ const filePathEsc = a.file_path ? escapeHtml(a.file_path) : null;
19179
+ let body = "";
19180
+ switch (a.kind) {
19181
+ case "screenshot":
19182
+ body = filePathEsc ? `<img src="${filePathEsc}" alt="screenshot artifact ${idEsc}">` : "<em>missing file</em>";
19183
+ break;
19184
+ case "video":
19185
+ body = filePathEsc ? `<video controls preload="metadata" src="${filePathEsc}"></video>` : "<em>missing file</em>";
19186
+ break;
19187
+ case "asciinema":
19188
+ body = filePathEsc ? `<a href="${filePathEsc}" download>Open .cast file (${idEsc})</a> &mdash; <code>asciinema play &lt;file&gt;</code> to play locally` : "<em>missing file</em>";
19189
+ break;
19190
+ case "http":
19191
+ case "text": {
19192
+ const inline = a.file_path ? ctx.inlineFiles.get(a.file_path) : null;
19193
+ if (a.kind === "http" && a.file_path && inline === null) {
19194
+ body = `<a href="${filePathEsc}" download>Open transcript (${idEsc})</a>`;
19195
+ } else if (inline !== void 0 && inline !== null) {
19196
+ body = `<pre><code>${escapeHtml(inline)}</code></pre>`;
19197
+ } else if (a.note) {
19198
+ body = `<pre><code>${escapeHtml(a.note)}</code></pre>`;
19199
+ } else {
19200
+ body = "<em>(empty)</em>";
19201
+ }
19202
+ break;
19203
+ }
19204
+ default:
19205
+ body = `<em>unknown kind: ${escapeHtml(a.kind)}</em>`;
19206
+ }
19207
+ return `<div class="artifact">${noteHtml}${body}<div class="artifact-meta"><code>${idEsc}</code></div></div>`;
19208
+ }
19209
+ function renderProofHtml(params2, inlineFiles = /* @__PURE__ */ new Map()) {
19210
+ const { assignment, title, generated, criteria, artifactsByCriterion, untagged, staleByOriginalIndex } = params2;
19211
+ const ctx = { inlineFiles };
19212
+ const totalArtifacts = untagged.length + staleByOriginalIndex.length + Array.from(artifactsByCriterion.values()).reduce((sum, list) => sum + list.length, 0);
19213
+ const titleEsc = escapeHtml(title);
19214
+ const assignmentEsc = escapeHtml(assignment);
19215
+ const generatedEsc = escapeHtml(generated);
19216
+ const tocItems = criteria.map((c2) => `<li><a href="#criterion-${c2.index}">${c2.index}. ${escapeHtml(c2.text)}</a></li>`).join("\n ");
19217
+ const otherTocItem = untagged.length > 0 || staleByOriginalIndex.length > 0 ? '<li><a href="#other-artifacts">Other artifacts</a></li>' : "";
19218
+ const criteriaHtml = criteria.map((c2) => {
19219
+ const tagged = artifactsByCriterion.get(c2.index) ?? [];
19220
+ const heading = `<h2>${c2.index}. <input type="checkbox" disabled${c2.checked ? " checked" : ""}> ${escapeHtml(c2.text)}</h2>`;
19221
+ const body = tagged.length === 0 ? '<p class="empty-criterion">no artifacts captured</p>' : tagged.map((a) => renderArtifact(a, ctx)).join("\n ");
19222
+ return `<section id="criterion-${c2.index}" class="criterion">
19223
+ ${heading}
19224
+ ${body}
19225
+ </section>`;
19226
+ }).join("\n ");
19227
+ const otherSection = untagged.length > 0 || staleByOriginalIndex.length > 0 ? `<section id="other-artifacts" class="criterion">
19228
+ <h2>Other artifacts</h2>
19229
+ ${untagged.map((a) => `<div class="untagged">${renderArtifact(a, ctx)}</div>`).join("\n ")}
19230
+ ${staleByOriginalIndex.map(
19231
+ (a) => `<div class="stale">(was tagged criterion ${a.criterion_index ?? ""} \u2014 no longer present)
19232
+ ${renderArtifact(a, ctx)}</div>`
19233
+ ).join("\n ")}
19234
+ </section>` : "";
19235
+ const emptyState = totalArtifacts === 0 && criteria.length === 0 ? "<p>No artifacts captured for this assignment yet.</p>" : "";
19236
+ return `<!DOCTYPE html>
19237
+ <html lang="en">
19238
+ <head>
19239
+ <meta charset="utf-8">
19240
+ <meta name="viewport" content="width=device-width, initial-scale=1">
19241
+ <title>Proof \u2014 ${titleEsc}</title>
19242
+ <style>${STYLE_BLOCK}</style>
19243
+ </head>
19244
+ <body>
19245
+ <header>
19246
+ <h1>Proof \u2014 ${titleEsc}</h1>
19247
+ <p><small>Assignment: <code>${assignmentEsc}</code> \xB7 Generated: ${generatedEsc}</small></p>
19248
+ </header>
19249
+ ${criteria.length > 0 || untagged.length > 0 || staleByOriginalIndex.length > 0 ? `<nav class="toc"><ul>
19250
+ ${tocItems}
19251
+ ${otherTocItem}
19252
+ </ul></nav>` : ""}
19253
+ <main>
19254
+ ${emptyState}
19255
+ ${criteriaHtml}
19256
+ ${otherSection}
19257
+ </main>
19258
+ </body>
19259
+ </html>
19260
+ `;
19261
+ }
19262
+
19263
+ // src/commands/proof.ts
19264
+ async function readAssignmentMeta(assignmentDir) {
19265
+ const path = resolve52(assignmentDir, "assignment.md");
19266
+ if (!await fileExists(path)) {
19267
+ return { title: "", body: "" };
19268
+ }
19269
+ const content = await readFile34(path, "utf-8");
19270
+ const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
19271
+ let title = "";
19272
+ if (fmMatch) {
19273
+ const titleLine = fmMatch[1].split("\n").find((l) => /^title:\s/i.test(l));
19274
+ if (titleLine) {
19275
+ const raw = titleLine.replace(/^title:\s*/i, "").trim();
19276
+ title = raw.replace(/^["']|["']$/g, "");
19277
+ }
19278
+ }
19279
+ return { title, body: content };
19280
+ }
19281
+ async function atomicWrite(destPath, content) {
19282
+ const tmp = `${destPath}.${randomBytes3(4).toString("hex")}.tmp`;
19283
+ await writeFile10(tmp, content, "utf-8");
19284
+ await rename7(tmp, destPath);
19285
+ }
19286
+ function isWithin(root, target) {
19287
+ const rel = relative5(root, target);
19288
+ return rel === "" || !rel.startsWith("..") && !isAbsolute8(rel);
19289
+ }
19290
+ function groupArtifacts(rows, criterionCount) {
19291
+ const artifactsByCriterion = /* @__PURE__ */ new Map();
19292
+ const untagged = [];
19293
+ const staleByOriginalIndex = [];
19294
+ for (const row of rows) {
19295
+ const idx = row.criterion_index;
19296
+ if (idx === null || idx === void 0) {
19297
+ untagged.push(row);
19298
+ continue;
19299
+ }
19300
+ if (idx >= 0 && idx < criterionCount) {
19301
+ const list = artifactsByCriterion.get(idx) ?? [];
19302
+ list.push(row);
19303
+ artifactsByCriterion.set(idx, list);
19304
+ } else {
19305
+ staleByOriginalIndex.push(row);
19306
+ }
19307
+ }
19308
+ return { artifactsByCriterion, untagged, staleByOriginalIndex };
19309
+ }
19310
+ async function loadInlineFiles(rows, assignmentDir) {
19311
+ const out = /* @__PURE__ */ new Map();
19312
+ const proofRoot = proofDir(assignmentDir);
19313
+ for (const r of rows) {
19314
+ if (!r.file_path) continue;
19315
+ if (r.kind !== "http" && r.kind !== "text") continue;
19316
+ const abs = resolve52(assignmentDir, r.file_path);
19317
+ if (!isWithin(proofRoot, abs)) {
19318
+ out.set(r.file_path, null);
19319
+ continue;
19320
+ }
19321
+ if (!await fileExists(abs)) {
19322
+ out.set(r.file_path, null);
19323
+ continue;
19324
+ }
19325
+ const st = await stat7(abs);
19326
+ if (st.size > INLINE_TEXT_LIMIT_BYTES) {
19327
+ out.set(r.file_path, null);
19328
+ continue;
19329
+ }
19330
+ try {
19331
+ out.set(r.file_path, await readFile34(abs, "utf-8"));
19332
+ } catch {
19333
+ out.set(r.file_path, null);
19334
+ }
19335
+ }
19336
+ return out;
19337
+ }
19338
+ async function proofBuildCommand(target, options = {}) {
19339
+ const resolved = await resolveAssignmentTarget(target, {
19340
+ project: options.project,
19341
+ dir: options.dir,
19342
+ cwd: options.cwd
19343
+ });
19344
+ const meta = await readAssignmentMeta(resolved.assignmentDir);
19345
+ const criteria = parseAcceptanceCriteria(meta.body);
19346
+ initProofDb();
19347
+ const rows = listArtifactsByAssignment(resolved.id);
19348
+ const { artifactsByCriterion, untagged, staleByOriginalIndex } = groupArtifacts(
19349
+ rows,
19350
+ criteria.length
19351
+ );
19352
+ const inlineFiles = await loadInlineFiles(rows, resolved.assignmentDir);
19353
+ const renderParams = {
19354
+ assignment: resolved.standalone ? resolved.id : `${resolved.projectSlug}/${resolved.assignmentSlug}`,
19355
+ title: meta.title || resolved.assignmentSlug,
19356
+ generated: (/* @__PURE__ */ new Date()).toISOString(),
19357
+ criteria,
19358
+ artifactsByCriterion,
19359
+ untagged,
19360
+ staleByOriginalIndex
19361
+ };
19362
+ const md = renderProofMarkdown(renderParams);
19363
+ const html = renderProofHtml(renderParams, inlineFiles);
19364
+ const mdPath = resolve52(resolved.assignmentDir, "proof.md");
19365
+ const htmlPath = resolve52(resolved.assignmentDir, "proof.html");
19366
+ await atomicWrite(mdPath, md);
19367
+ await atomicWrite(htmlPath, html);
19368
+ console.log(`Wrote ${htmlPath}`);
19369
+ console.log(`Wrote ${mdPath}`);
19370
+ return { htmlPath, mdPath };
19371
+ }
19372
+ var proofCommand = new Command5("proof").description("Render proof artifacts for an assignment");
19373
+ proofCommand.command("build").description("Render proof.html and proof.md for an assignment").argument("[target]", "Assignment slug (with --project) or UUID; falls back to .syntaur/context.json").option("--project <slug>", "Project slug if the target is project-nested").option("--dir <path>", "Override default project directory").action(async (target, options) => {
19374
+ try {
19375
+ await proofBuildCommand(target, options);
19376
+ } catch (error) {
19377
+ console.error("Error:", error instanceof Error ? error.message : String(error));
19378
+ process.exit(1);
19379
+ }
19380
+ });
19381
+
19382
+ // src/commands/request.ts
19383
+ init_paths();
19384
+ init_fs();
19385
+ init_config2();
19386
+ init_slug();
19387
+ init_timestamp();
19388
+ init_assignment_resolver();
19389
+ init_assignment_todos();
19390
+ import { resolve as resolve53 } from "path";
19391
+ import { readFile as readFile35 } from "fs/promises";
19392
+ async function requestCommand(target, text, options = {}) {
19393
+ if (!text || !text.trim()) {
19394
+ throw new Error("Request text cannot be empty.");
19395
+ }
19396
+ const config = await readConfig();
19397
+ const baseDir = options.dir ? expandHome(options.dir) : config.defaultProjectDir;
19398
+ let assignmentDir;
19399
+ let targetRef;
19400
+ if (options.project) {
19401
+ if (!isValidSlug(options.project)) {
19402
+ throw new Error(`Invalid project slug "${options.project}".`);
19403
+ }
19404
+ if (!isValidSlug(target)) {
19405
+ throw new Error(`Invalid assignment slug "${target}".`);
19406
+ }
19407
+ assignmentDir = resolve53(baseDir, options.project, "assignments", target);
19408
+ targetRef = target;
19409
+ } else {
19410
+ const resolved = await resolveAssignmentById(baseDir, assignmentsDir(), target);
19411
+ if (!resolved) {
19412
+ throw new Error(`Target assignment "${target}" not found. Provide --project <slug> or a valid UUID.`);
19413
+ }
19414
+ assignmentDir = resolved.assignmentDir;
19415
+ targetRef = resolved.standalone ? resolved.id : resolved.assignmentSlug;
19416
+ }
19417
+ const assignmentMdPath2 = resolve53(assignmentDir, "assignment.md");
19418
+ if (!await fileExists(assignmentMdPath2)) {
19419
+ throw new Error(`assignment.md not found at ${assignmentMdPath2}`);
19420
+ }
19421
+ const source = options.from ?? process.env.SYNTAUR_ASSIGNMENT ?? "unknown";
19422
+ let content = await readFile35(assignmentMdPath2, "utf-8");
19423
+ content = appendTodosToAssignmentBody(content, [
19424
+ { description: `${text.trim()} (from: ${source})` }
19425
+ ]);
19426
+ content = touchAssignmentUpdated(content, nowTimestamp());
19427
+ await writeFileForce(assignmentMdPath2, content);
19428
+ console.log(`Added todo to ${targetRef}: ${text.trim()} (from: ${source})`);
19429
+ }
19430
+
19431
+ // src/commands/plan.ts
19432
+ init_fs();
19433
+ init_paths();
19434
+ import { Command as Command6 } from "commander";
19435
+ import { readFile as readFile36, readdir as readdir18 } from "fs/promises";
19436
+ import { resolve as resolve54 } from "path";
19437
+ async function readContextAssignmentDir(cwd) {
19438
+ const path = resolve54(cwd, ".syntaur", "context.json");
19439
+ if (!await fileExists(path)) return null;
19440
+ try {
19441
+ const raw = await readFile36(path, "utf-8");
19442
+ const ctx = JSON.parse(raw);
19443
+ if (typeof ctx.assignmentDir === "string" && ctx.assignmentDir.length > 0) {
19444
+ return ctx.assignmentDir;
19445
+ }
19446
+ return null;
19447
+ } catch {
19448
+ return null;
19449
+ }
19450
+ }
19451
+ async function resolveAssignmentDir(opts) {
19452
+ const cwd = opts.cwd ?? process.cwd();
19453
+ if (opts.assignment) {
19454
+ if (opts.project) {
19455
+ return resolve54(defaultProjectDir(), opts.project, "assignments", opts.assignment);
19456
+ }
19457
+ return resolve54(assignmentsDir(), opts.assignment);
19458
+ }
19459
+ const fromCtx = await readContextAssignmentDir(cwd);
19460
+ if (fromCtx) return fromCtx;
19461
+ throw new Error(
19462
+ "No active assignment. Pass --assignment <slug> --project <slug> or run from a workspace with .syntaur/context.json."
19463
+ );
19464
+ }
19465
+ var PLAN_PATTERN = /^plan(?:-v(\d+))?\.md$/;
19466
+ async function listPlanFiles(assignmentDir) {
19467
+ if (!await fileExists(assignmentDir)) return [];
19468
+ const entries = await readdir18(assignmentDir, { withFileTypes: true });
19469
+ const out = [];
19470
+ for (const e of entries) {
19471
+ if (!e.isFile()) continue;
19472
+ const m = e.name.match(PLAN_PATTERN);
19473
+ if (!m) continue;
19474
+ const version2 = m[1] ? parseInt(m[1], 10) : 1;
19475
+ out.push({ fileName: e.name, version: version2 });
19476
+ }
19477
+ out.sort((a, b) => a.version - b.version);
19478
+ return out;
19479
+ }
19480
+ function nextPlanFileName(currentVersion) {
19481
+ const next = currentVersion + 1;
19482
+ return { fileName: `plan-v${next}.md`, version: next };
19483
+ }
19484
+ function planLabel(version2) {
19485
+ return version2 === 1 ? "plan" : `plan v${version2}`;
19486
+ }
19487
+ function planLinkText(version2) {
19488
+ return version2 === 1 ? "plan" : `plan v${version2}`;
19489
+ }
19490
+ function planFileName(version2) {
19491
+ return version2 === 1 ? "plan.md" : `plan-v${version2}.md`;
19492
+ }
19493
+ function rewriteAssignmentTodos(content, oldVersion, newVersion) {
19494
+ const lines = content.split("\n");
19495
+ const todosHeaderIdx = lines.findIndex((l) => /^##\s+Todos\s*$/.test(l));
19496
+ if (todosHeaderIdx === -1) {
19497
+ throw new Error("assignment.md has no `## Todos` section to rewrite.");
19498
+ }
19499
+ let endIdx = lines.length;
19500
+ for (let i = todosHeaderIdx + 1; i < lines.length; i++) {
19501
+ if (/^##\s/.test(lines[i])) {
19502
+ endIdx = i;
19503
+ break;
19504
+ }
19505
+ }
19506
+ const oldFile = planFileName(oldVersion);
19507
+ const oldLink = `./${oldFile}`;
19508
+ const newFile = planFileName(newVersion);
19509
+ const newLabel = planLinkText(newVersion);
19510
+ const supersededTag = `(superseded by plan-v${newVersion})`;
19511
+ const CYCLE_VERB_PATTERN = /^(\s*-\s*\[[ xX]\]\s*)(Create|Review|Implement|Review implementation of)(\s+\[)/;
19512
+ let rewrote = 0;
19513
+ for (let i = todosHeaderIdx + 1; i < endIdx; i++) {
19514
+ const line = lines[i];
19515
+ if (!line.includes(oldLink)) continue;
19516
+ if (!CYCLE_VERB_PATTERN.test(line)) continue;
19517
+ if (line.includes("(superseded by plan-v")) continue;
19518
+ let next = line.replace(/^(\s*-\s*)\[[ xX]\]/, "$1[x]");
19519
+ next = next.replace(
19520
+ /^(\s*-\s*\[x\]\s*)(.*)$/,
19521
+ (_m, prefix, rest) => {
19522
+ const body = rest.endsWith(" ") ? rest.trimEnd() : rest;
19523
+ if (body.startsWith("~~") && body.endsWith("~~")) {
19524
+ return `${prefix}${body} ${supersededTag}`;
19525
+ }
19526
+ return `${prefix}~~${body}~~ ${supersededTag}`;
19527
+ }
19528
+ );
19529
+ lines[i] = next;
19530
+ rewrote += 1;
19531
+ }
19532
+ const newTodos = [
19533
+ `- [ ] Create [${newLabel}](./${newFile})`,
19534
+ `- [ ] Review [${newLabel}](./${newFile})`,
19535
+ `- [ ] Implement [${newLabel}](./${newFile})`,
19536
+ `- [ ] Review implementation of [${newLabel}](./${newFile})`
19537
+ ];
19538
+ const insertAt = endIdx;
19539
+ const prevLine = lines[insertAt - 1] ?? "";
19540
+ const toInsert = [];
19541
+ if (prevLine.trim() !== "") toInsert.push("");
19542
+ toInsert.push(...newTodos);
19543
+ lines.splice(insertAt, 0, ...toInsert);
19544
+ return {
19545
+ updated: lines.join("\n"),
19546
+ rewrote,
19547
+ appended: newTodos.length
19548
+ };
19549
+ }
19550
+ function extractUncheckedTodos(planBody) {
19551
+ const out = [];
19552
+ for (const line of planBody.split("\n")) {
19553
+ if (/^\s*-\s*\[\s\]\s+/.test(line)) {
19554
+ out.push(line);
19555
+ }
19556
+ }
19557
+ return out;
19558
+ }
19559
+ function isoNow() {
19560
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
19561
+ }
19562
+ function buildNewPlanStub(opts) {
19563
+ const created = isoNow();
19564
+ const oldLabel = planLabel(opts.oldVersion);
19565
+ const carriedSection = opts.uncheckedTodos.length === 0 ? "_No unchecked tasks carried forward from the prior plan._" : opts.uncheckedTodos.join("\n");
19566
+ return `---
19567
+ assignment: ${opts.assignmentSlug}
19568
+ status: draft
19569
+ created: "${created}"
19570
+ updated: "${created}"
19571
+ ---
19572
+
19573
+ # ${opts.assignmentSlug} \u2014 Implementation Plan v${opts.newVersion}
19574
+
19575
+ **Date:** ${created.slice(0, 10)}
19576
+ **Supersedes:** [${oldLabel}](./${planFileName(opts.oldVersion)})
19577
+
19578
+ ## Objective
19579
+
19580
+ <!-- Describe what changed and why a new plan is needed. -->
19581
+
19582
+ ## Carried-forward tasks
19583
+
19584
+ ${carriedSection}
19585
+
19586
+ ## Tasks
19587
+
19588
+ <!-- Add the new plan tasks here. -->
19589
+
19590
+ ## Verification
19591
+
19592
+ <!-- Add verification steps here. -->
19593
+ `;
19594
+ }
19595
+ async function runPlanVersion(options) {
19596
+ const assignmentDir = await resolveAssignmentDir(options);
19597
+ if (!await fileExists(assignmentDir)) {
19598
+ throw new Error(`Assignment directory does not exist: ${assignmentDir}`);
19599
+ }
19600
+ const assignmentMdPath2 = resolve54(assignmentDir, "assignment.md");
19601
+ if (!await fileExists(assignmentMdPath2)) {
19602
+ throw new Error(`Missing assignment.md at: ${assignmentMdPath2}`);
19603
+ }
19604
+ const planFiles = await listPlanFiles(assignmentDir);
19605
+ if (planFiles.length === 0) {
19606
+ throw new Error(
19607
+ `No plan.md (or plan-v<N>.md) found in ${assignmentDir}. Run /plan-assignment to create plan.md first.`
19608
+ );
19609
+ }
19610
+ const current = planFiles[planFiles.length - 1];
19611
+ const next = nextPlanFileName(current.version);
19612
+ const newPath = resolve54(assignmentDir, next.fileName);
19613
+ if (await fileExists(newPath) && !options.force) {
19614
+ throw new Error(`${next.fileName} already exists. Use --force to overwrite.`);
19615
+ }
19616
+ const assignmentMd = await readFile36(assignmentMdPath2, "utf-8");
19617
+ const slugMatch = assignmentMd.match(/^slug:\s*(.+?)\s*$/m);
19618
+ const slug = slugMatch ? slugMatch[1].trim() : assignmentDir.split("/").pop() ?? "";
19619
+ const oldPlanPath = resolve54(assignmentDir, current.fileName);
19620
+ const oldPlanContent = await readFile36(oldPlanPath, "utf-8");
19621
+ const oldBody = oldPlanContent.replace(/^---[\s\S]*?\n---\n?/, "");
19622
+ const carriedTodos = extractUncheckedTodos(oldBody);
19623
+ const stub = buildNewPlanStub({
19624
+ assignmentSlug: slug,
19625
+ newVersion: next.version,
19626
+ oldVersion: current.version,
19627
+ uncheckedTodos: carriedTodos
19628
+ });
19629
+ const { updated, rewrote, appended } = rewriteAssignmentTodos(
19630
+ assignmentMd,
19631
+ current.version,
19632
+ next.version
19633
+ );
19634
+ await writeFileForce(newPath, stub);
19635
+ await writeFileForce(assignmentMdPath2, updated);
19636
+ console.log(
19637
+ `Created ${next.fileName} (superseding ${current.fileName}). Rewrote ${rewrote} prior todo(s); appended ${appended} new todo(s).`
19638
+ );
19639
+ console.log(`Path: ${newPath}`);
19640
+ console.log(`Carried forward: ${carriedTodos.length} unchecked task(s).`);
19641
+ }
19642
+ var planCommand = new Command6("plan").description("Manage plan files for the active assignment");
19643
+ planCommand.command("version").description(
19644
+ "Create the next plan-v<N>.md, supersede the prior plan in assignment.md ## Todos, and carry forward unchecked tasks"
19645
+ ).option("--assignment <slug>", "Assignment slug (UUID for standalone). Defaults to .syntaur/context.json").option("--project <slug>", "Project slug. Required when --assignment is given for a project-nested assignment").option("--force", "Overwrite if the next plan-v<N>.md already exists").action(async (options) => {
19646
+ try {
19647
+ await runPlanVersion(options);
19648
+ } catch (error) {
19649
+ console.error("Error:", error instanceof Error ? error.message : String(error));
19650
+ process.exit(1);
19651
+ }
19652
+ });
19653
+
19654
+ // src/commands/session.ts
19655
+ init_fs();
19656
+ import { Command as Command7 } from "commander";
19657
+ import { readFile as readFile37, readdir as readdir19, stat as stat8 } from "fs/promises";
19658
+ import { resolve as resolve55 } from "path";
19659
+ async function readContext(cwd) {
19660
+ const path = resolve55(cwd, ".syntaur", "context.json");
19661
+ if (!await fileExists(path)) return null;
19662
+ try {
19663
+ const raw = await readFile37(path, "utf-8");
19664
+ return JSON.parse(raw);
19665
+ } catch {
19666
+ return null;
19667
+ }
19668
+ }
19669
+ async function findLatestSessionSummary(assignmentDir) {
19670
+ const sessionsRoot = resolve55(assignmentDir, "sessions");
19671
+ if (!await fileExists(sessionsRoot)) return null;
19672
+ const entries = await readdir19(sessionsRoot, { withFileTypes: true });
19673
+ let best = null;
19674
+ for (const entry of entries) {
19675
+ if (!entry.isDirectory()) continue;
19676
+ const summaryPath = resolve55(sessionsRoot, entry.name, "summary.md");
19677
+ if (!await fileExists(summaryPath)) continue;
19678
+ const st = await stat8(summaryPath);
19679
+ if (best === null || st.mtime.getTime() > best.mtime.getTime()) {
19680
+ best = { sessionId: entry.name, path: summaryPath, mtime: st.mtime };
19681
+ }
19682
+ }
19683
+ return best;
19684
+ }
19685
+ async function findOpenHandoff(assignmentDir) {
19686
+ const handoffPath = resolve55(assignmentDir, "handoff.md");
19687
+ if (!await fileExists(handoffPath)) return null;
19688
+ const content = await readFile37(handoffPath, "utf-8");
19689
+ const body = content.replace(/^---[\s\S]*?\n---\n?/, "").trim();
19690
+ if (body.length === 0) return null;
19691
+ if (/^<!--[\s\S]*-->$/.test(body)) return null;
19692
+ return handoffPath;
19693
+ }
19694
+ async function buildResumeOutput(cwd) {
19695
+ const warnings = [];
19696
+ const context = await readContext(cwd);
19697
+ if (!context) {
19698
+ return {
19699
+ ok: false,
19700
+ context: null,
19701
+ latestSession: null,
19702
+ openHandoff: null,
19703
+ warnings: [
19704
+ "No .syntaur/context.json in current directory. Run grab-assignment first."
19705
+ ]
19706
+ };
19707
+ }
19708
+ if (!context.assignmentDir) {
19709
+ return {
19710
+ ok: false,
19711
+ context,
19712
+ latestSession: null,
19713
+ openHandoff: null,
19714
+ warnings: [
19715
+ "context.json present but no assignmentDir field \u2014 only a session record exists. Nothing to resume."
19716
+ ]
19717
+ };
19718
+ }
19719
+ const latestSession = await findLatestSessionSummary(context.assignmentDir);
19720
+ if (!latestSession) {
19721
+ warnings.push(
19722
+ `No session summary found under ${context.assignmentDir}/sessions/. Run /save-session-summary in a prior session to leave a resume baton.`
19723
+ );
19724
+ }
19725
+ const openHandoff = await findOpenHandoff(context.assignmentDir);
19726
+ return {
19727
+ ok: true,
19728
+ context,
19729
+ latestSession: latestSession ? { sessionId: latestSession.sessionId, path: latestSession.path } : null,
19730
+ openHandoff,
19731
+ warnings
19732
+ };
19733
+ }
19734
+ function renderHumanOutput(out) {
19735
+ const lines = [];
19736
+ if (!out.ok) {
19737
+ lines.push("Cannot resume:");
19738
+ for (const w of out.warnings) lines.push(` - ${w}`);
19739
+ return lines.join("\n");
19740
+ }
19741
+ const ctx = out.context;
19742
+ lines.push("Resuming Syntaur session");
19743
+ lines.push("");
19744
+ lines.push(` Project: ${ctx.projectSlug ?? "(standalone)"}`);
19745
+ lines.push(` Assignment: ${ctx.assignmentSlug ?? "(unknown)"}`);
19746
+ if (ctx.title) lines.push(` Title: ${ctx.title}`);
19747
+ if (ctx.branch) lines.push(` Branch: ${ctx.branch}`);
19748
+ if (ctx.workspaceRoot) lines.push(` Workspace root: ${ctx.workspaceRoot}`);
19749
+ lines.push(` Assignment dir: ${ctx.assignmentDir}`);
19750
+ lines.push("");
19751
+ if (out.latestSession) {
19752
+ lines.push(`Latest session summary: ${out.latestSession.path}`);
19753
+ lines.push(`Read it next to load What\u2019s Next + Open Questions.`);
19754
+ } else {
19755
+ lines.push("No prior session summary on disk.");
19756
+ }
19757
+ if (out.openHandoff) {
19758
+ lines.push("");
19759
+ lines.push(`Open handoff: ${out.openHandoff}`);
19760
+ lines.push("Read it before continuing \u2014 there is an outstanding baton.");
19761
+ }
19762
+ if (out.warnings.length > 0) {
19763
+ lines.push("");
19764
+ lines.push("Warnings:");
19765
+ for (const w of out.warnings) lines.push(` - ${w}`);
19766
+ }
19767
+ return lines.join("\n");
19768
+ }
19769
+ async function runSessionResume(options, cwd = process.cwd()) {
19770
+ const out = await buildResumeOutput(cwd);
19771
+ if (options.json) {
19772
+ console.log(JSON.stringify(out, null, 2));
19773
+ } else {
19774
+ console.log(renderHumanOutput(out));
19775
+ }
19776
+ return out;
19777
+ }
19778
+ var sessionCommand = new Command7("session").description("Manage agent sessions for the active assignment");
19779
+ sessionCommand.command("resume").description(
19780
+ "Re-orient a fresh session: print active assignment context, latest saved session summary, and any open handoff. Idempotent \u2014 does not mutate state."
19781
+ ).option("--json", "Emit machine-readable JSON instead of human-readable text").action(async (options) => {
19782
+ try {
19783
+ const out = await runSessionResume(options);
19784
+ if (!out.ok) process.exit(1);
19785
+ } catch (error) {
19786
+ console.error("Error:", error instanceof Error ? error.message : String(error));
19787
+ process.exit(1);
19788
+ }
19789
+ });
19790
+
19791
+ // src/commands/worktree.ts
19792
+ init_git_worktree();
19793
+ init_fs();
19794
+ init_paths();
19795
+ import { Command as Command8 } from "commander";
19796
+ import { readFile as readFile38 } from "fs/promises";
19797
+ import { resolve as resolve56 } from "path";
19798
+ async function readContext2(cwd) {
19799
+ const path = resolve56(cwd, ".syntaur", "context.json");
19800
+ if (!await fileExists(path)) return null;
19801
+ try {
19802
+ return JSON.parse(await readFile38(path, "utf-8"));
19803
+ } catch {
19804
+ return null;
19805
+ }
19806
+ }
19807
+ async function resolveAssignmentPath2(opts) {
19808
+ if (opts.assignment) {
19809
+ if (opts.project) {
19810
+ return resolve56(
19811
+ defaultProjectDir(),
19812
+ opts.project,
19813
+ "assignments",
19814
+ opts.assignment,
19815
+ "assignment.md"
19816
+ );
19817
+ }
19818
+ return resolve56(assignmentsDir(), opts.assignment, "assignment.md");
19819
+ }
19820
+ const ctx = await readContext2(opts.cwd);
19821
+ if (ctx?.assignmentDir) return resolve56(ctx.assignmentDir, "assignment.md");
19822
+ throw new Error(
19823
+ "No active assignment. Pass --assignment <slug> [--project <slug>] or run from a workspace with .syntaur/context.json."
19824
+ );
19825
+ }
19826
+ async function runWorktreeCreate(options, cwd = process.cwd()) {
19827
+ if (!options.branch) {
19828
+ throw new Error("--branch is required.");
19829
+ }
19830
+ const repository = options.repository ?? cwd;
19831
+ const parentBranch = options.parentBranch ?? "main";
19832
+ const worktreePath = options.worktreePath ?? resolve56(repository, ".worktrees", options.branch);
19833
+ const assignmentPath = await resolveAssignmentPath2({
19834
+ assignment: options.assignment,
19835
+ project: options.project,
19836
+ cwd
19837
+ });
19838
+ if (!await fileExists(assignmentPath)) {
19839
+ throw new Error(`Assignment file not found: ${assignmentPath}`);
19840
+ }
19841
+ await createWorktreeAndRecord({
19842
+ repository,
19843
+ branch: options.branch,
19844
+ worktreePath,
19845
+ parentBranch,
19846
+ assignmentPath
19847
+ });
19848
+ return { worktreePath, assignmentPath };
19849
+ }
19850
+ var worktreeCommand = new Command8("worktree").description("Manage git worktrees bound to Syntaur assignments");
19851
+ worktreeCommand.command("create").description(
19852
+ "Create a worktree at <repository>/.worktrees/<branch> and record it in the assignment workspace block. Atomic \u2014 rolls back the worktree if writing assignment.md fails."
19853
+ ).requiredOption("--branch <name>", "Branch name to create (also used as worktree dir name)").option("--repository <path>", "Repository root (defaults to current working directory)").option("--parent-branch <name>", "Parent branch to fork from", "main").option("--assignment <slug>", "Assignment slug (UUID for standalone). Defaults to .syntaur/context.json").option("--project <slug>", "Project slug. Required when --assignment is given for a project-nested assignment").option("--worktree-path <path>", "Override the computed <repository>/.worktrees/<branch> path").action(async (options) => {
19854
+ try {
19855
+ const { worktreePath, assignmentPath } = await runWorktreeCreate(options);
19856
+ console.log(`Created worktree at ${worktreePath}`);
19857
+ console.log(`Recorded workspace fields in ${assignmentPath}`);
19858
+ } catch (error) {
19859
+ console.error("Error:", error instanceof Error ? error.message : String(error));
19860
+ process.exit(1);
19861
+ }
19862
+ });
19863
+
19864
+ // src/commands/resource.ts
19865
+ init_paths();
19866
+ init_fs();
19867
+ init_slug();
19868
+ import { Command as Command9 } from "commander";
19869
+ import { resolve as resolve58 } from "path";
19870
+
19871
+ // src/utils/project-indexes.ts
19872
+ init_parser();
19873
+ init_fs();
19874
+ import { readdir as readdir20, readFile as readFile39 } from "fs/promises";
19875
+ import { resolve as resolve57 } from "path";
19876
+ function nowIso() {
19877
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
19878
+ }
19879
+ function readProjectSlug(projectDir) {
19880
+ return projectDir.split("/").filter(Boolean).pop() ?? "";
19881
+ }
19882
+ async function listSlugFiles(dir) {
19883
+ if (!await fileExists(dir)) return [];
19884
+ const entries = await readdir20(dir, { withFileTypes: true });
19885
+ return entries.filter((e) => e.isFile() && e.name.endsWith(".md") && !e.name.startsWith("_")).map((e) => e.name).sort();
19886
+ }
19887
+ function escapeCell(value) {
19888
+ return value.replace(/\|/g, "\\|").replace(/\r?\n/g, " ").trim();
19889
+ }
19890
+ function joinList(items) {
19891
+ return items.length === 0 ? "\u2014" : items.map(escapeCell).join(", ");
19892
+ }
19893
+ async function rebuildResourcesIndex(projectDir) {
19894
+ const dir = resolve57(projectDir, "resources");
19895
+ await ensureDir(dir);
19896
+ const files = await listSlugFiles(dir);
19897
+ const slug = readProjectSlug(projectDir);
19898
+ const lines = [];
19899
+ lines.push("---");
19900
+ lines.push(`project: ${slug}`);
19901
+ lines.push(`generated: "${nowIso()}"`);
19902
+ lines.push(`total: ${files.length}`);
19903
+ lines.push("---");
19904
+ lines.push("");
19905
+ lines.push("# Resources");
19906
+ lines.push("");
19907
+ lines.push("| Name | Category | Source | Related Assignments | Updated |");
19908
+ lines.push("|------|----------|--------|---------------------|---------|");
19909
+ for (const fileName of files) {
19910
+ const content = await readFile39(resolve57(dir, fileName), "utf-8");
19911
+ const parsed = parseResource(content);
19912
+ const slugBase = fileName.replace(/\.md$/, "");
19913
+ const name = parsed.name || slugBase;
19914
+ const link = `[${escapeCell(name)}](./${fileName})`;
19915
+ lines.push(
19916
+ `| ${link} | ${escapeCell(parsed.category)} | ${escapeCell(parsed.source)} | ${joinList(parsed.relatedAssignments)} | ${escapeCell(parsed.updated)} |`
19917
+ );
19918
+ }
19919
+ lines.push("");
19920
+ const indexPath = resolve57(dir, "_index.md");
19921
+ await writeFileForce(indexPath, lines.join("\n"));
19922
+ return { total: files.length, path: indexPath };
19923
+ }
19924
+ async function rebuildMemoriesIndex(projectDir) {
19925
+ const dir = resolve57(projectDir, "memories");
19926
+ await ensureDir(dir);
19927
+ const files = await listSlugFiles(dir);
19928
+ const slug = readProjectSlug(projectDir);
19929
+ const lines = [];
19930
+ lines.push("---");
19931
+ lines.push(`project: ${slug}`);
19932
+ lines.push(`generated: "${nowIso()}"`);
19933
+ lines.push(`total: ${files.length}`);
19934
+ lines.push("---");
19935
+ lines.push("");
19936
+ lines.push("# Memories");
19937
+ lines.push("");
19938
+ lines.push("| Name | Source | Scope | Source Assignment | Updated |");
19939
+ lines.push("|------|--------|-------|-------------------|---------|");
19940
+ for (const fileName of files) {
19941
+ const content = await readFile39(resolve57(dir, fileName), "utf-8");
19942
+ const parsed = parseMemory(content);
19943
+ const slugBase = fileName.replace(/\.md$/, "");
19944
+ const name = parsed.name || slugBase;
19945
+ const link = `[${escapeCell(name)}](./${fileName})`;
19946
+ lines.push(
19947
+ `| ${link} | ${escapeCell(parsed.source)} | ${escapeCell(parsed.scope)} | ${escapeCell(parsed.sourceAssignment ?? "\u2014")} | ${escapeCell(parsed.updated)} |`
19948
+ );
19949
+ }
19950
+ lines.push("");
19951
+ const indexPath = resolve57(dir, "_index.md");
19952
+ await writeFileForce(indexPath, lines.join("\n"));
19953
+ return { total: files.length, path: indexPath };
19954
+ }
19955
+
19956
+ // src/commands/resource.ts
19957
+ function nowIso2() {
19958
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
19959
+ }
19960
+ function parseList(value) {
19961
+ if (!value) return [];
19962
+ return value.split(",").map((v) => v.trim()).filter((v) => v.length > 0);
19963
+ }
19964
+ function yamlQuote(value) {
19965
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
19966
+ }
19967
+ function renderResourceFile(opts) {
19968
+ const created = nowIso2();
19969
+ const related = opts.relatedAssignments.length === 0 ? "[]" : `
19970
+ ${opts.relatedAssignments.map((a) => ` - ${a}`).join("\n")}`;
19971
+ return `---
19972
+ name: ${yamlQuote(opts.name)}
19973
+ category: ${yamlQuote(opts.category)}
19974
+ source: ${yamlQuote(opts.source)}
19975
+ relatedAssignments:${related}
19976
+ created: "${created}"
19977
+ updated: "${created}"
19978
+ ---
19979
+
19980
+ # ${opts.name}
19981
+
19982
+ ${opts.body ?? "<!-- Add notes about this resource here. -->"}
19983
+ `;
19984
+ }
19985
+ async function runResourceAdd(options) {
19986
+ if (!isValidSlug(options.project)) {
19987
+ throw new Error(`Invalid project slug: "${options.project}".`);
19988
+ }
19989
+ const projectDir = resolve58(defaultProjectDir(), options.project);
19990
+ if (!await fileExists(resolve58(projectDir, "project.md"))) {
19991
+ throw new Error(`Project "${options.project}" not found at ${projectDir}.`);
19992
+ }
19993
+ if (!options.name) throw new Error("--name is required.");
19994
+ if (!options.source) throw new Error("--source is required.");
19995
+ const slug = options.slug ?? slugify(options.name);
19996
+ if (!isValidSlug(slug)) {
19997
+ throw new Error(`Invalid resource slug: "${slug}".`);
19998
+ }
19999
+ const filePath = resolve58(projectDir, "resources", `${slug}.md`);
20000
+ if (await fileExists(filePath) && !options.force) {
20001
+ throw new Error(
20002
+ `Resource "${slug}" already exists at ${filePath}. Use --force to overwrite.`
20003
+ );
20004
+ }
20005
+ const content = renderResourceFile({
20006
+ name: options.name,
20007
+ source: options.source,
20008
+ category: options.category ?? "",
20009
+ relatedAssignments: parseList(options.relatedAssignments)
20010
+ });
20011
+ await writeFileForce(filePath, content);
20012
+ const { path: indexPath, total } = await rebuildResourcesIndex(projectDir);
20013
+ return { filePath, indexPath, total };
20014
+ }
20015
+ var resourceCommand = new Command9("resource").description("Manage project-level resource entries");
20016
+ resourceCommand.command("add").description(
20017
+ "Add a project resource at <projectDir>/resources/<slug>.md and regenerate _index.md"
20018
+ ).requiredOption("--project <slug>", "Project slug").requiredOption("--name <name>", "Human-readable resource name").requiredOption("--source <url-or-path>", "Resource source (URL or path)").option("--category <name>", "Optional category (e.g. dashboard, doc, ticket)").option("--slug <slug>", "Override the slug (default: slugify(--name))").option("--related-assignments <slugs>", "Comma-separated related assignment slugs").option("--force", "Overwrite if the resource file already exists").action(async (options) => {
20019
+ try {
20020
+ const { filePath, indexPath, total } = await runResourceAdd(options);
20021
+ console.log(`Wrote ${filePath}`);
20022
+ console.log(`Rebuilt ${indexPath} (${total} total resources).`);
20023
+ } catch (error) {
20024
+ console.error("Error:", error instanceof Error ? error.message : String(error));
20025
+ process.exit(1);
20026
+ }
20027
+ });
20028
+
20029
+ // src/commands/memory.ts
20030
+ init_paths();
20031
+ init_fs();
20032
+ init_slug();
20033
+ import { Command as Command10 } from "commander";
20034
+ import { resolve as resolve59 } from "path";
20035
+ function nowIso3() {
20036
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
20037
+ }
20038
+ function parseList2(value) {
20039
+ if (!value) return [];
20040
+ return value.split(",").map((v) => v.trim()).filter((v) => v.length > 0);
20041
+ }
20042
+ function yamlQuote2(value) {
20043
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
20044
+ }
20045
+ function renderMemoryFile(opts) {
20046
+ const created = nowIso3();
20047
+ const related = opts.relatedAssignments.length === 0 ? "[]" : `
20048
+ ${opts.relatedAssignments.map((a) => ` - ${a}`).join("\n")}`;
20049
+ const sourceAssignment = opts.sourceAssignment === null ? "null" : yamlQuote2(opts.sourceAssignment);
20050
+ return `---
20051
+ name: ${yamlQuote2(opts.name)}
20052
+ source: ${yamlQuote2(opts.source)}
20053
+ scope: ${yamlQuote2(opts.scope)}
20054
+ sourceAssignment: ${sourceAssignment}
20055
+ relatedAssignments:${related}
20056
+ created: "${created}"
20057
+ updated: "${created}"
20058
+ ---
20059
+
20060
+ # ${opts.name}
20061
+
20062
+ ${opts.body ?? "<!-- Capture the load-bearing context for this memory here. -->"}
20063
+ `;
20064
+ }
20065
+ async function runMemoryAdd(options) {
20066
+ if (!isValidSlug(options.project)) {
20067
+ throw new Error(`Invalid project slug: "${options.project}".`);
20068
+ }
20069
+ const projectDir = resolve59(defaultProjectDir(), options.project);
20070
+ if (!await fileExists(resolve59(projectDir, "project.md"))) {
20071
+ throw new Error(`Project "${options.project}" not found at ${projectDir}.`);
20072
+ }
20073
+ if (!options.name) throw new Error("--name is required.");
20074
+ if (!options.source) throw new Error("--source is required.");
20075
+ const slug = options.slug ?? slugify(options.name);
20076
+ if (!isValidSlug(slug)) {
20077
+ throw new Error(`Invalid memory slug: "${slug}".`);
20078
+ }
20079
+ const filePath = resolve59(projectDir, "memories", `${slug}.md`);
20080
+ if (await fileExists(filePath) && !options.force) {
20081
+ throw new Error(
20082
+ `Memory "${slug}" already exists at ${filePath}. Use --force to overwrite.`
20083
+ );
20084
+ }
20085
+ const content = renderMemoryFile({
20086
+ name: options.name,
20087
+ source: options.source,
20088
+ scope: options.scope ?? "project",
20089
+ sourceAssignment: options.sourceAssignment ?? null,
20090
+ relatedAssignments: parseList2(options.relatedAssignments)
20091
+ });
20092
+ await writeFileForce(filePath, content);
20093
+ const { path: indexPath, total } = await rebuildMemoriesIndex(projectDir);
20094
+ return { filePath, indexPath, total };
20095
+ }
20096
+ var memoryCommand = new Command10("memory").description("Manage project-level memory entries");
20097
+ memoryCommand.command("add").description(
20098
+ "Add a project memory at <projectDir>/memories/<slug>.md and regenerate _index.md"
20099
+ ).requiredOption("--project <slug>", "Project slug").requiredOption("--name <name>", "Human-readable memory name").requiredOption("--source <text>", "Where this memory came from (conversation, decision, doc URL, etc.)").option("--scope <scope>", "Memory scope (default: project)", "project").option("--source-assignment <slug>", "Assignment slug this memory was captured during").option("--slug <slug>", "Override the slug (default: slugify(--name))").option("--related-assignments <slugs>", "Comma-separated related assignment slugs").option("--force", "Overwrite if the memory file already exists").action(async (options) => {
20100
+ try {
20101
+ const { filePath, indexPath, total } = await runMemoryAdd(options);
20102
+ console.log(`Wrote ${filePath}`);
20103
+ console.log(`Rebuilt ${indexPath} (${total} total memories).`);
20104
+ } catch (error) {
20105
+ console.error("Error:", error instanceof Error ? error.message : String(error));
20106
+ process.exit(1);
20107
+ }
20108
+ });
20109
+
20110
+ // src/commands/ls.ts
20111
+ init_api();
20112
+ init_paths();
20113
+ init_fs();
20114
+ init_frontmatter();
20115
+ import { Command as Command11 } from "commander";
20116
+ import { readFile as readFile40 } from "fs/promises";
20117
+ import { resolve as resolve60 } from "path";
20118
+ var AGE_PATTERN = /^(\d+)([dhwm])$/i;
20119
+ function parseAgeToCutoff(age) {
20120
+ const match = age.match(AGE_PATTERN);
20121
+ if (!match) {
20122
+ throw new Error(
20123
+ `Invalid --age value: "${age}". Use formats like 7d, 24h, 2w, 1m.`
20124
+ );
20125
+ }
20126
+ const n = parseInt(match[1], 10);
20127
+ const unit = match[2].toLowerCase();
20128
+ const ms = unit === "h" ? n * 60 * 60 * 1e3 : unit === "d" ? n * 24 * 60 * 60 * 1e3 : unit === "w" ? n * 7 * 24 * 60 * 60 * 1e3 : (
20129
+ /* m: months ≈ 30 days */
20130
+ n * 30 * 24 * 60 * 60 * 1e3
20131
+ );
20132
+ return new Date(Date.now() - ms);
20133
+ }
20134
+ function assignmentMdPath(item) {
20135
+ if (item.projectSlug) {
20136
+ return resolve60(
20137
+ defaultProjectDir(),
20138
+ item.projectSlug,
20139
+ "assignments",
20140
+ item.slug,
20141
+ "assignment.md"
20142
+ );
20143
+ }
20144
+ return resolve60(assignmentsDir(), item.id, "assignment.md");
20145
+ }
20146
+ async function loadTags(item) {
20147
+ const path = assignmentMdPath(item);
20148
+ if (!await fileExists(path)) return [];
20149
+ try {
20150
+ const content = await readFile40(path, "utf-8");
20151
+ return parseAssignmentFrontmatter(content).tags;
20152
+ } catch {
20153
+ return [];
20154
+ }
20155
+ }
20156
+ async function runLs(options) {
20157
+ const board = await listAssignmentsBoard(
20158
+ defaultProjectDir(),
20159
+ assignmentsDir()
20160
+ );
20161
+ let items = board.assignments;
20162
+ if (options.status) {
20163
+ const statuses = options.status.split(",").map((s) => s.trim()).filter(Boolean);
20164
+ items = items.filter((a) => statuses.includes(a.status));
20165
+ }
20166
+ if (options.project) {
20167
+ items = items.filter((a) => a.projectSlug === options.project);
20168
+ }
20169
+ if (options.age) {
20170
+ const cutoff = parseAgeToCutoff(options.age);
20171
+ items = items.filter((a) => {
20172
+ const updatedTs = Date.parse(a.updated);
20173
+ return Number.isFinite(updatedTs) && updatedTs >= cutoff.getTime();
20174
+ });
20175
+ }
20176
+ if (options.tag) {
20177
+ const wanted = options.tag.split(",").map((s) => s.trim()).filter(Boolean);
20178
+ const tagged = await Promise.all(
20179
+ items.map(async (a) => {
20180
+ const tags = await loadTags(a);
20181
+ return { item: a, tags };
20182
+ })
20183
+ );
20184
+ items = tagged.filter(({ tags }) => wanted.every((t) => tags.includes(t))).map(({ item }) => item);
20185
+ }
20186
+ return { items };
20187
+ }
20188
+ function pad(value, width) {
20189
+ if (value.length >= width) return value.slice(0, width);
20190
+ return value + " ".repeat(width - value.length);
20191
+ }
20192
+ function renderTable(items) {
20193
+ if (items.length === 0) return "No assignments matched.";
20194
+ const rows = items.map((a) => [
20195
+ a.projectSlug ?? "(standalone)",
20196
+ a.slug,
20197
+ a.status,
20198
+ a.priority,
20199
+ a.assignee ?? "\u2014",
20200
+ a.updated.slice(0, 10),
20201
+ a.title
20202
+ ]);
20203
+ const header = ["PROJECT", "SLUG", "STATUS", "PRIORITY", "ASSIGNEE", "UPDATED", "TITLE"];
20204
+ const all = [header, ...rows];
20205
+ const widths = header.map(
20206
+ (_, c2) => Math.min(60, Math.max(...all.map((row) => row[c2]?.length ?? 0)))
20207
+ );
20208
+ return all.map((row) => row.map((cell, c2) => pad(cell ?? "", widths[c2])).join(" ")).join("\n");
20209
+ }
20210
+ var lsCommand = new Command11("ls").description(
20211
+ "List assignments across all projects with optional filters by status, project, tag, or age."
20212
+ ).option("--status <list>", "Comma-separated status filter (e.g. pending,in_progress)").option("--project <slug>", "Filter to one project").option("--tag <list>", "Comma-separated tag filter (assignment must have ALL tags)").option("--age <duration>", "Only include assignments updated within duration (e.g. 7d, 24h, 2w, 1m)").option("--json", "Emit JSON instead of a table").action(async (options) => {
20213
+ try {
20214
+ const { items } = await runLs(options);
20215
+ if (options.json) {
20216
+ console.log(JSON.stringify({ assignments: items }, null, 2));
20217
+ } else {
20218
+ console.log(renderTable(items));
20219
+ }
20220
+ } catch (error) {
20221
+ console.error("Error:", error instanceof Error ? error.message : String(error));
20222
+ process.exit(1);
20223
+ }
20224
+ });
20225
+
20226
+ // src/cli-default-command.ts
20227
+ init_config2();
20228
+ import { readdir as readdir21 } from "fs/promises";
20229
+ async function hasAnyProjectContent(projectsDir2) {
20230
+ try {
20231
+ const entries = await readdir21(projectsDir2, { withFileTypes: true });
20232
+ return entries.some((entry) => entry.isDirectory());
20233
+ } catch {
20234
+ return false;
20235
+ }
20236
+ }
20237
+ async function isSetupComplete() {
20238
+ const config = await readConfig();
20239
+ if (config.onboarding.completed) {
20240
+ return true;
20241
+ }
20242
+ const [
20243
+ claudePluginDir,
20244
+ codexPluginDir,
20245
+ codexMarketplacePath,
20246
+ hasProjectContent
20247
+ ] = await Promise.all([
18279
20248
  getConfiguredOrLegacyManagedPluginDir("claude"),
18280
20249
  getConfiguredOrLegacyManagedPluginDir("codex"),
18281
20250
  getConfiguredOrLegacyMarketplacePath(),
@@ -18296,20 +20265,20 @@ async function getDefaultCommandName() {
18296
20265
  init_paths();
18297
20266
  init_fs();
18298
20267
  import { fileURLToPath as fileURLToPath8 } from "url";
18299
- import { readFile as readFile34 } from "fs/promises";
18300
- import { dirname as dirname15, join as join8, resolve as resolve49 } from "path";
20268
+ import { readFile as readFile42 } from "fs/promises";
20269
+ import { dirname as dirname15, join as join8, resolve as resolve61 } from "path";
18301
20270
  import { spawn as spawn4 } from "child_process";
18302
20271
  import { createInterface as createInterface2 } from "readline/promises";
18303
20272
 
18304
20273
  // src/utils/version.ts
18305
20274
  import { fileURLToPath as fileURLToPath7 } from "url";
18306
- import { readFile as readFile33 } from "fs/promises";
20275
+ import { readFile as readFile41 } from "fs/promises";
18307
20276
  import { dirname as dirname14, join as join7 } from "path";
18308
20277
  async function readPackageVersion(scriptUrl) {
18309
20278
  try {
18310
20279
  const scriptPath = fileURLToPath7(scriptUrl);
18311
20280
  const pkgRoot = dirname14(dirname14(scriptPath));
18312
- const raw = await readFile33(join7(pkgRoot, "package.json"), "utf-8");
20281
+ const raw = await readFile41(join7(pkgRoot, "package.json"), "utf-8");
18313
20282
  const parsed = JSON.parse(raw);
18314
20283
  return typeof parsed.version === "string" ? parsed.version : null;
18315
20284
  } catch {
@@ -18318,7 +20287,7 @@ async function readPackageVersion(scriptUrl) {
18318
20287
  }
18319
20288
 
18320
20289
  // src/utils/npx-prompt.ts
18321
- var STATE_FILE = resolve49(syntaurRoot(), "npx-install.json");
20290
+ var STATE_FILE = resolve61(syntaurRoot(), "npx-install.json");
18322
20291
  var META_ARGS = /* @__PURE__ */ new Set(["-h", "--help", "-V", "--version", "help"]);
18323
20292
  var GLOBAL_VERSION_TIMEOUT_MS = 2e3;
18324
20293
  function isRunningViaNpx(scriptUrl) {
@@ -18339,7 +20308,7 @@ function isRunningViaNpx(scriptUrl) {
18339
20308
  async function readState() {
18340
20309
  if (!await fileExists(STATE_FILE)) return null;
18341
20310
  try {
18342
- const raw = await readFile34(STATE_FILE, "utf-8");
20311
+ const raw = await readFile42(STATE_FILE, "utf-8");
18343
20312
  return JSON.parse(raw);
18344
20313
  } catch {
18345
20314
  return null;
@@ -18398,7 +20367,7 @@ async function readGlobalVersion() {
18398
20367
  try {
18399
20368
  const manifestPath = join8(rootPath, "syntaur", "package.json");
18400
20369
  if (!await fileExists(manifestPath)) return null;
18401
- const raw = await readFile34(manifestPath, "utf-8");
20370
+ const raw = await readFile42(manifestPath, "utf-8");
18402
20371
  const parsed = JSON.parse(raw);
18403
20372
  return typeof parsed.version === "string" ? parsed.version : null;
18404
20373
  } catch {
@@ -18517,7 +20486,7 @@ async function maybePromptInstall(scriptUrl) {
18517
20486
 
18518
20487
  // src/index.ts
18519
20488
  await maybePromptInstall(import.meta.url);
18520
- var program = new Command5();
20489
+ var program = new Command12();
18521
20490
  var version = await readPackageVersion(import.meta.url) ?? "0.0.0";
18522
20491
  program.name("syntaur").description("CLI scaffolding tool for the Syntaur protocol").version(version);
18523
20492
  program.command("init").description("Initialize ~/.syntaur/ directory structure and config").option("--force", "Overwrite existing config file").action(async (options) => {
@@ -18568,6 +20537,17 @@ program.command("comment").description("Add a comment to an assignment (CLI-medi
18568
20537
  process.exit(1);
18569
20538
  }
18570
20539
  });
20540
+ program.command("capture").description("Capture a typed proof artifact for an assignment").argument("[target]", "Assignment slug (with --project) or UUID; falls back to .syntaur/context.json").option("--kind <type>", "Artifact kind: screenshot | video | asciinema | http | text").option("--file <path>", "Source file to ingest (forbidden for --kind=text)").option("--criterion <index>", "Optional 0-based criterion index to tag").option("--note <text>", "Optional note (required for --kind=text)").option("--project <slug>", "Project slug if the target is project-nested").option("--dir <path>", "Override default project directory").action(async (target, options) => {
20541
+ try {
20542
+ await captureCommand(target, options);
20543
+ } catch (error) {
20544
+ console.error(
20545
+ "Error:",
20546
+ error instanceof Error ? error.message : String(error)
20547
+ );
20548
+ process.exit(1);
20549
+ }
20550
+ });
18571
20551
  program.command("request").description("Append a todo to another assignment (cross-assignment work request)").argument("<target>", "Target assignment slug (with --project) or UUID (standalone)").argument("<text>", "Todo text").option("--project <slug>", "Project slug if the target is project-nested").option("--from <source>", "Source assignment (default: $SYNTAUR_ASSIGNMENT)").option("--dir <path>", "Override default project directory").action(async (target, text, options) => {
18572
20552
  try {
18573
20553
  await requestCommand(target, text, options);
@@ -18878,6 +20858,13 @@ program.addCommand(todoCommand);
18878
20858
  program.addCommand(backupCommand);
18879
20859
  program.addCommand(doctorCommand);
18880
20860
  program.addCommand(agentsCommand);
20861
+ program.addCommand(proofCommand);
20862
+ program.addCommand(planCommand);
20863
+ program.addCommand(sessionCommand);
20864
+ program.addCommand(worktreeCommand);
20865
+ program.addCommand(resourceCommand);
20866
+ program.addCommand(memoryCommand);
20867
+ program.addCommand(lsCommand);
18881
20868
  if (process.argv.length <= 2) {
18882
20869
  process.argv.push(await getDefaultCommandName());
18883
20870
  }