syntaur 0.6.0 → 0.7.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 (83) hide show
  1. package/README.md +2 -2
  2. package/dashboard/dist/assets/{_basePickBy-BQIP1Ca7.js → _basePickBy-DTYUlCEg.js} +1 -1
  3. package/dashboard/dist/assets/{_baseUniq-BnBWRwT7.js → _baseUniq-C0Y4HRd5.js} +1 -1
  4. package/dashboard/dist/assets/{arc-BYWL4eq0.js → arc-BFx2eqN9.js} +1 -1
  5. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-CD_SWPSa.js → architectureDiagram-2XIMDMQ5-Erol1JD6.js} +1 -1
  6. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-BS1ZbFBU.js → blockDiagram-WCTKOSBZ-kSkh6VkS.js} +1 -1
  7. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-D99yg-l2.js → c4Diagram-IC4MRINW-C04oKzvX.js} +1 -1
  8. package/dashboard/dist/assets/channel-C82tBKZ7.js +1 -0
  9. package/dashboard/dist/assets/{chunk-4BX2VUAB-BkN9IORC.js → chunk-4BX2VUAB-C3t0tXt-.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-55IACEB6-BQPHWefV.js → chunk-55IACEB6-2cnyEL0b.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-FMBD7UC4-CNcExMdx.js → chunk-FMBD7UC4-DIY9MTNi.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-JSJVCQXG-LXBmftkC.js → chunk-JSJVCQXG-Cw8fpqpE.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-KX2RTZJC-Tqi7zNqq.js → chunk-KX2RTZJC-BAhd66XV.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-NQ4KR5QH-DkMbx-rW.js → chunk-NQ4KR5QH-RzXwoxk3.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-QZHKN3VN-BlrRCfkJ.js → chunk-QZHKN3VN-Dgri4sGz.js} +1 -1
  16. package/dashboard/dist/assets/{chunk-WL4C6EOR-of3XBzMu.js → chunk-WL4C6EOR-DYLj9JRa.js} +1 -1
  17. package/dashboard/dist/assets/classDiagram-VBA2DB6C-STOZ51tg.js +1 -0
  18. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-STOZ51tg.js +1 -0
  19. package/dashboard/dist/assets/clone-TzhWk-Bj.js +1 -0
  20. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-BlIiyO76.js → cose-bilkent-S5V4N54A-DfY_Fnfu.js} +1 -1
  21. package/dashboard/dist/assets/{dagre-KLK3FWXG-CYQjSI9N.js → dagre-KLK3FWXG-CyTKIVSK.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-E7M64L7V-BZHzTKct.js → diagram-E7M64L7V-Krub7Xxo.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-IFDJBPK2-kMP3WqBV.js → diagram-IFDJBPK2-giUl9uHz.js} +1 -1
  24. package/dashboard/dist/assets/{diagram-P4PSJMXO-BWSHyFOv.js → diagram-P4PSJMXO-oAtnO3C9.js} +1 -1
  25. package/dashboard/dist/assets/{erDiagram-INFDFZHY-B5HrvsPP.js → erDiagram-INFDFZHY-eYaVjXqo.js} +1 -1
  26. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-Dm4ewP7w.js → flowDiagram-PKNHOUZH-or5S0_Sb.js} +1 -1
  27. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-DB3k27zu.js → ganttDiagram-A5KZAMGK-C9R1lsme.js} +1 -1
  28. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-G7y6Ey-m.js → gitGraphDiagram-K3NZZRJ6-BQwsDzvp.js} +1 -1
  29. package/dashboard/dist/assets/{graph-CaM4i6vq.js → graph-EQOX1wg8.js} +1 -1
  30. package/dashboard/dist/assets/index-Cy7yjuqO.js +500 -0
  31. package/dashboard/dist/assets/index-u80fISp0.css +1 -0
  32. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-JNTUbTjg.js → infoDiagram-LFFYTUFH-BjLlQWxk.js} +1 -1
  33. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-BZJt1ht8.js → ishikawaDiagram-PHBUUO56-BNjydh4j.js} +1 -1
  34. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-DPcqvl9A.js → journeyDiagram-4ABVD52K-DNzE7TgQ.js} +1 -1
  35. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-D1D7AuOV.js → kanban-definition-K7BYSVSG-FbNvGx6i.js} +1 -1
  36. package/dashboard/dist/assets/{layout-BTOh3EDT.js → layout-B2yZvlWs.js} +1 -1
  37. package/dashboard/dist/assets/{linear-MbCpC_Cg.js → linear-p68yY_14.js} +1 -1
  38. package/dashboard/dist/assets/{mermaid.core-CYbhqlNy.js → mermaid.core-D558akcW.js} +4 -4
  39. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-CwYCISFH.js → mindmap-definition-YRQLILUH-CZPgesSK.js} +1 -1
  40. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-5qfZ73SG.js → pieDiagram-SKSYHLDU-CdXMWspp.js} +1 -1
  41. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-WI8y1sQ_.js → quadrantDiagram-337W2JSQ-D7tq22ZY.js} +1 -1
  42. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-BFlD0ZTS.js → requirementDiagram-Z7DCOOCP-ByZxUSmd.js} +1 -1
  43. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-Bdckv1Se.js → sankeyDiagram-WA2Y5GQK-CZon9rRY.js} +1 -1
  44. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-DgzxKAlZ.js → sequenceDiagram-2WXFIKYE-nbELB6rb.js} +1 -1
  45. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-DO4OXahC.js → stateDiagram-RAJIS63D-D_OPKr5B.js} +1 -1
  46. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-4pLM5B3m.js +1 -0
  47. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-BBB01JWw.js → timeline-definition-YZTLITO2-Cxuvk1D2.js} +1 -1
  48. package/dashboard/dist/assets/{treemap-KZPCXAKY-Dr0jb8op.js → treemap-KZPCXAKY-C0CRpL92.js} +1 -1
  49. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-D40KFl2o.js → vennDiagram-LZ73GAT5-DijCj6M3.js} +1 -1
  50. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-DBUmWQfT.js → xychartDiagram-JWTSCODW-CdpE0oRi.js} +1 -1
  51. package/dashboard/dist/index.html +2 -2
  52. package/dist/dashboard/server.js +429 -39
  53. package/dist/dashboard/server.js.map +1 -1
  54. package/dist/index.js +658 -134
  55. package/dist/index.js.map +1 -1
  56. package/package.json +1 -1
  57. package/platforms/claude-code/README.md +3 -3
  58. package/platforms/claude-code/agents/syntaur-expert.md +12 -4
  59. package/platforms/claude-code/commands/save-session-summary/save-session-summary.md +24 -0
  60. package/platforms/claude-code/hooks/hooks.json +10 -0
  61. package/platforms/claude-code/hooks/session-start.sh +26 -1
  62. package/platforms/claude-code/references/file-ownership.md +2 -1
  63. package/platforms/claude-code/references/protocol-summary.md +6 -1
  64. package/platforms/claude-code/skills/track-session/SKILL.md +86 -0
  65. package/platforms/codex/README.md +2 -2
  66. package/platforms/codex/agents/syntaur-operator.md +6 -4
  67. package/platforms/codex/commands/save-session-summary.md +23 -0
  68. package/platforms/codex/references/file-ownership.md +2 -1
  69. package/platforms/codex/references/protocol-summary.md +6 -1
  70. package/vendor/syntaur-skills/skills/complete-assignment/SKILL.md +2 -0
  71. package/vendor/syntaur-skills/skills/grab-assignment/SKILL.md +7 -2
  72. package/vendor/syntaur-skills/skills/plan-assignment/SKILL.md +3 -1
  73. package/vendor/syntaur-skills/skills/save-session-summary/SKILL.md +113 -0
  74. package/vendor/syntaur-skills/skills/syntaur-protocol/SKILL.md +23 -4
  75. package/vendor/syntaur-skills/skills/syntaur-protocol/references/file-ownership.md +2 -1
  76. package/vendor/syntaur-skills/skills/syntaur-protocol/references/protocol-summary.md +6 -1
  77. package/dashboard/dist/assets/channel-Df6VrFK5.js +0 -1
  78. package/dashboard/dist/assets/classDiagram-VBA2DB6C-CyfzumTY.js +0 -1
  79. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-CyfzumTY.js +0 -1
  80. package/dashboard/dist/assets/clone-CMs4Aqrx.js +0 -1
  81. package/dashboard/dist/assets/index-B4QMu-Oq.css +0 -1
  82. package/dashboard/dist/assets/index-BBWZjPBC.js +0 -495
  83. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-o8bgX-J3.js +0 -1
package/dist/index.js CHANGED
@@ -2017,8 +2017,9 @@ ${todosSection}## Context
2017
2017
  - [Progress](./progress.md)
2018
2018
  - [Comments](./comments.md)
2019
2019
  - [Scratchpad](./scratchpad.md)
2020
- - [Handoff](./handoff.md)
2020
+ - [Handoff](./handoff.md) \u2014 cross-ticket outbound
2021
2021
  - [Decision Record](./decision-record.md)
2022
+ - [Sessions](./sessions/) \u2014 per-session continuity summaries (one \`<session-id>/summary.md\` per session)
2022
2023
  `;
2023
2024
  }
2024
2025
  var init_assignment = __esm({
@@ -2072,6 +2073,13 @@ var init_handoff = __esm({
2072
2073
  }
2073
2074
  });
2074
2075
 
2076
+ // src/templates/session-summary.ts
2077
+ var init_session_summary = __esm({
2078
+ "src/templates/session-summary.ts"() {
2079
+ "use strict";
2080
+ }
2081
+ });
2082
+
2075
2083
  // src/templates/progress.ts
2076
2084
  function renderProgress(params2) {
2077
2085
  return `---
@@ -2322,8 +2330,11 @@ You are working within the Syntaur protocol for multi-agent project coordination
2322
2330
  progress.md # Agent-writable, append-only: timestamped progress log
2323
2331
  comments.md # CLI-mediated: threaded questions/notes/feedback (via \`syntaur comment\`)
2324
2332
  scratchpad.md # Agent-writable: working notes
2325
- handoff.md # Agent-writable: append-only handoff log
2333
+ handoff.md # Agent-writable: append-only cross-ticket outbound at completion
2326
2334
  decision-record.md # Agent-writable: append-only decision log
2335
+ sessions/
2336
+ <session-id>/
2337
+ summary.md # Agent-writable: per-session continuity (single doc, overwritten)
2327
2338
  resources/
2328
2339
  _index.md # Derived (read-only)
2329
2340
  <resource-slug>.md # Shared-writable
@@ -2339,13 +2350,15 @@ You are working within the Syntaur protocol for multi-agent project coordination
2339
2350
  scratchpad.md
2340
2351
  handoff.md
2341
2352
  decision-record.md
2353
+ sessions/<session-id>/summary.md # Per-session continuity (same as project-nested)
2342
2354
  \`\`\`
2343
2355
 
2344
2356
  ## Write Boundary Rules (CRITICAL)
2345
2357
 
2346
2358
  ### Files you may WRITE:
2347
2359
  1. **Your assignment folder** -- only the assignment you are currently working on:
2348
- - \`assignment.md\`, \`plan*.md\` (0 or more versioned plan files), \`progress.md\`, \`scratchpad.md\`, \`handoff.md\`, \`decision-record.md\`
2360
+ - \`assignment.md\`, \`plan*.md\` (0 or more versioned plan files), \`progress.md\`, \`scratchpad.md\`, \`handoff.md\` (cross-ticket outbound at completion), \`decision-record.md\`
2361
+ - \`sessions/<session-id>/summary.md\` -- per-session continuity (single doc per session id, overwritten on save). Distinct from \`handoff.md\`.
2349
2362
  - Path (project-nested): \`~/.syntaur/projects/<project>/assignments/<your-assignment>/\`
2350
2363
  - Path (standalone): \`~/.syntaur/assignments/<your-assignment-uuid>/\`
2351
2364
  2. **Shared resources and memories** at the project level:
@@ -2449,7 +2462,8 @@ Before starting work, read these files in order:
2449
2462
  3. any \`${params2.assignmentDir}/plan*.md\` files linked from active todos in the \`## Todos\` section (may be 0, 1, or many)
2450
2463
  4. \`${params2.assignmentDir}/progress.md\` -- reverse-chron progress log (if present)
2451
2464
  5. \`${params2.assignmentDir}/comments.md\` -- threaded questions/notes/feedback (if present)
2452
- 6. \`${params2.assignmentDir}/handoff.md\` -- previous session handoff notes
2465
+ 6. \`${params2.assignmentDir}/handoff.md\` -- cross-ticket outbound history (entries from prior agents/humans handing this assignment off)
2466
+ 7. The latest \`${params2.assignmentDir}/sessions/<sid>/summary.md\` if present -- previous-session continuity (selected by \`summary.md\` file mtime; read it for "what was done / what's next" before resuming work in flight)
2453
2467
 
2454
2468
  ## Your Writable Files
2455
2469
 
@@ -2460,6 +2474,7 @@ You may write directly to these files inside your assignment folder:
2460
2474
  - \`${params2.assignmentDir}/scratchpad.md\`
2461
2475
  - \`${params2.assignmentDir}/handoff.md\`
2462
2476
  - \`${params2.assignmentDir}/decision-record.md\`
2477
+ - \`${params2.assignmentDir}/sessions/<session-id>/summary.md\` (per-session continuity)
2463
2478
 
2464
2479
  Do NOT edit \`${params2.assignmentDir}/comments.md\` directly \u2014 use \`syntaur comment\`. Do NOT edit other assignments' files \u2014 use \`syntaur request\` for cross-assignment todos.
2465
2480
 
@@ -2495,7 +2510,8 @@ If the global Syntaur Codex plugin is installed, prefer these workflows instead
2495
2510
  - \`create-assignment\` -- create a new assignment (use \`--type <bug|feature|chore|...>\` to classify; use \`--one-off\` to create a standalone assignment at \`~/.syntaur/assignments/<uuid>/\` with no parent project)
2496
2511
  - \`grab-assignment\` -- claim work, create \`.syntaur/context.json\`, and register a session
2497
2512
  - \`plan-assignment\` -- write a versioned plan file (\`plan.md\`, \`plan-v2.md\`, ...) and link it from the \`## Todos\` section of \`assignment.md\`
2498
- - \`complete-assignment\` -- append the handoff, append a final entry to \`progress.md\`, close the session, and transition state
2513
+ - \`complete-assignment\` -- write the cross-ticket \`handoff.md\` entry, append a final entry to \`progress.md\`, close the session, and transition state
2514
+ - \`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.
2499
2515
  - \`track-session\` -- manage tracked tmux sessions for the dashboard
2500
2516
 
2501
2517
  If the plugin is unavailable, follow the same workflow manually with the \`syntaur\` CLI and keep the protocol files current yourself.
@@ -2509,7 +2525,8 @@ Before starting work, read these files in order:
2509
2525
  4. any \`${params2.assignmentDir}/plan*.md\` files linked from active todos in the \`## Todos\` section (may be 0, 1, or many)
2510
2526
  5. \`${params2.assignmentDir}/progress.md\` -- reverse-chron progress log (if present)
2511
2527
  6. \`${params2.assignmentDir}/comments.md\` -- threaded questions/notes/feedback (if present)
2512
- 7. \`${params2.assignmentDir}/handoff.md\` -- previous session handoff notes
2528
+ 7. \`${params2.assignmentDir}/handoff.md\` -- cross-ticket outbound history (entries from prior agents/humans handing this assignment off)
2529
+ 8. The latest \`${params2.assignmentDir}/sessions/<sid>/summary.md\` if present -- previous-session continuity (read it for "what was done / what's next" before resuming work in flight)
2513
2530
 
2514
2531
  ## Context File
2515
2532
 
@@ -2537,8 +2554,11 @@ Before starting work, read these files in order:
2537
2554
  progress.md # Agent-writable, append-only: timestamped progress log
2538
2555
  comments.md # CLI-mediated: threaded questions/notes/feedback (via \`syntaur comment\`)
2539
2556
  scratchpad.md # Agent-writable: working notes
2540
- handoff.md # Agent-writable: append-only handoff log
2557
+ handoff.md # Agent-writable: append-only cross-ticket outbound at completion
2541
2558
  decision-record.md # Agent-writable: append-only decision log
2559
+ sessions/
2560
+ <session-id>/
2561
+ summary.md # Agent-writable: per-session continuity (single doc, overwritten)
2542
2562
  resources/
2543
2563
  _index.md # Derived (read-only)
2544
2564
  <resource-slug>.md # Shared-writable
@@ -2554,13 +2574,15 @@ Before starting work, read these files in order:
2554
2574
  scratchpad.md
2555
2575
  handoff.md
2556
2576
  decision-record.md
2577
+ sessions/<session-id>/summary.md # Per-session continuity (same as project-nested)
2557
2578
  \`\`\`
2558
2579
 
2559
2580
  ## Write Boundary Rules (CRITICAL)
2560
2581
 
2561
2582
  ### Files you may WRITE:
2562
2583
  1. **Your assignment folder** -- only the assignment you are currently working on:
2563
- - \`assignment.md\`, \`plan*.md\` (0 or more versioned plan files), \`progress.md\`, \`scratchpad.md\`, \`handoff.md\`, \`decision-record.md\`
2584
+ - \`assignment.md\`, \`plan*.md\` (0 or more versioned plan files), \`progress.md\`, \`scratchpad.md\`, \`handoff.md\` (cross-ticket outbound at completion), \`decision-record.md\`
2585
+ - \`sessions/<session-id>/summary.md\` -- per-session continuity (single doc per session id, overwritten on save). Distinct from \`handoff.md\`.
2564
2586
  - Path: \`${params2.assignmentDir}/\`
2565
2587
  2. **Shared resources and memories** at the project level:
2566
2588
  - \`${params2.projectDir}/resources/<slug>.md\`
@@ -2639,7 +2661,7 @@ Read each linked playbook and follow the rules in its body section. The \`when_t
2639
2661
  - Slugs are lowercase, hyphen-separated. For standalone assignments, \`slug\` is display-only; the folder is named by the UUID.
2640
2662
  - Always read \`project.md\` at the project level (when project-nested) before starting work.
2641
2663
  - Keep \`assignment.md\` acceptance criteria and \`## Todos\` updated as work lands; append timestamped entries to \`progress.md\` (never to \`assignment.md\`).
2642
- - Keep active plan file(s) current after planning changes and \`handoff.md\` current before leaving the task.
2664
+ - Keep active plan file(s) current after planning changes. Write \`handoff.md\` (via \`complete-assignment\`) at the cross-ticket boundary; write \`sessions/<sid>/summary.md\` (via \`/save-session-summary\`) before compaction or before ending a session mid-assignment so a future session can resume cleanly.
2643
2665
  - When requirements shift, supersede the prior plan todo (\`- [x] ~~...~~ (superseded by plan-v<N>)\`) and write a new plan file instead of rewriting the old one.
2644
2666
  - Record questions, notes, and feedback via \`syntaur comment\`. Never edit \`comments.md\` directly. Resolve questions via the dashboard UI (toggle on the question entry).
2645
2667
  - To route work to another assignment, use \`syntaur request\`.
@@ -2683,6 +2705,7 @@ var init_templates = __esm({
2683
2705
  init_plan();
2684
2706
  init_scratchpad();
2685
2707
  init_handoff();
2708
+ init_session_summary();
2686
2709
  init_progress();
2687
2710
  init_comments();
2688
2711
  init_decision_record();
@@ -6684,7 +6707,7 @@ __export(launch_exports, {
6684
6707
  shellQuote: () => shellQuote
6685
6708
  });
6686
6709
  import { spawn as spawn2 } from "child_process";
6687
- import { mkdir as mkdir5, writeFile as writeFile9 } from "fs/promises";
6710
+ import { mkdir as mkdir6, writeFile as writeFile9 } from "fs/promises";
6688
6711
  import { isAbsolute as isAbsolute3, resolve as resolve32 } from "path";
6689
6712
  function formatFallbackCwdWarning(opts) {
6690
6713
  const missing = [];
@@ -6743,7 +6766,7 @@ async function launchAgent(options) {
6743
6766
  if (warning) console.warn(warning);
6744
6767
  }
6745
6768
  const contextDir = resolve32(workspaceDir, ".syntaur");
6746
- await mkdir5(contextDir, { recursive: true });
6769
+ await mkdir6(contextDir, { recursive: true });
6747
6770
  const context = {
6748
6771
  projectSlug,
6749
6772
  assignmentSlug,
@@ -7070,7 +7093,7 @@ init_create_assignment();
7070
7093
  init_config2();
7071
7094
  import { spawn } from "child_process";
7072
7095
  import { createServer as createNetServer } from "net";
7073
- import { resolve as resolve22, dirname as dirname4 } from "path";
7096
+ import { resolve as resolve22, dirname as dirname6 } from "path";
7074
7097
  import { fileURLToPath as fileURLToPath2 } from "url";
7075
7098
 
7076
7099
  // src/dashboard/server.ts
@@ -9536,37 +9559,62 @@ init_fs_migration();
9536
9559
  // src/dashboard/api-todos.ts
9537
9560
  init_parser2();
9538
9561
  init_fs();
9562
+ init_paths();
9539
9563
  import { Router as Router5 } from "express";
9540
9564
  import { readdir as readdir9 } from "fs/promises";
9541
- var WORKSPACE_REGEX = /^[a-z0-9_][a-z0-9-]*$/;
9542
- function getWorkspaceParam(value) {
9543
- if (Array.isArray(value)) {
9544
- return value[0] ?? "";
9545
- }
9546
- return value ?? "";
9547
- }
9565
+ import { resolve as resolvePath, dirname as dirname4 } from "path";
9566
+ import { rename as rename3, mkdir as mkdir2 } from "fs/promises";
9567
+
9568
+ // src/dashboard/todos-locks.ts
9548
9569
  var writeLocks = /* @__PURE__ */ new Map();
9549
9570
  function withLock(lockKey, fn) {
9550
9571
  const prev = writeLocks.get(lockKey) ?? Promise.resolve();
9551
9572
  const next = prev.then(fn);
9552
- writeLocks.set(lockKey, next.then(() => {
9553
- }, () => {
9554
- }));
9573
+ writeLocks.set(
9574
+ lockKey,
9575
+ next.then(
9576
+ () => {
9577
+ },
9578
+ () => {
9579
+ }
9580
+ )
9581
+ );
9555
9582
  return next;
9556
9583
  }
9557
9584
  function wsLock(workspace, fn) {
9558
9585
  return withLock(`ws:${workspace}`, fn);
9559
9586
  }
9587
+ function projLock(slug, fn) {
9588
+ return withLock(`proj:${slug}`, fn);
9589
+ }
9590
+ function withTwoLocks(keyA, keyB, fn) {
9591
+ if (keyA === keyB) return withLock(keyA, fn);
9592
+ const [first, second] = keyA < keyB ? [keyA, keyB] : [keyB, keyA];
9593
+ return withLock(first, () => withLock(second, fn));
9594
+ }
9595
+
9596
+ // src/dashboard/api-todos.ts
9597
+ init_slug();
9598
+ var WORKSPACE_REGEX = /^[a-z0-9_][a-z0-9-]*$/;
9599
+ function getWorkspaceParam(value) {
9600
+ if (Array.isArray(value)) {
9601
+ return value[0] ?? "";
9602
+ }
9603
+ return value ?? "";
9604
+ }
9560
9605
  function touchItem(item) {
9561
9606
  const now = (/* @__PURE__ */ new Date()).toISOString();
9562
9607
  if (item.createdAt === null) item.createdAt = now;
9563
9608
  item.updatedAt = now;
9564
9609
  }
9565
- function createTodosRouter(todosDir2, broadcast) {
9610
+ function createTodosRouter(todosDir2, broadcast, projectsDir2) {
9566
9611
  const router = Router5();
9567
9612
  function broadcastUpdate() {
9568
9613
  broadcast({ type: "todos-updated", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
9569
9614
  }
9615
+ function broadcastProject(slug) {
9616
+ broadcast({ type: "todos-updated", projectSlug: slug, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
9617
+ }
9570
9618
  function validateWorkspace(req, res, next) {
9571
9619
  const workspace = getWorkspaceParam(req.params.workspace);
9572
9620
  if (workspace && !WORKSPACE_REGEX.test(workspace)) {
@@ -9965,7 +10013,7 @@ workspace: ${workspace}
9965
10013
  items.push(item);
9966
10014
  }
9967
10015
  const scopeLabel = workspace === "_global" ? "_global" : `workspace:${workspace}`;
9968
- const { resolve: resolvePath } = await import("path");
10016
+ const { resolve: resolvePath2 } = await import("path");
9969
10017
  const { readConfig: readConfig3 } = await Promise.resolve().then(() => (init_config2(), config_exports));
9970
10018
  const { assignmentsDir: assignmentsDirFn } = await Promise.resolve().then(() => (init_paths(), paths_exports));
9971
10019
  const { fileExists: fileExists2, writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
@@ -9995,18 +10043,18 @@ workspace: ${workspace}
9995
10043
  const parts = tg.split("/");
9996
10044
  if (parts.length !== 2) return { error: `Invalid target.assignment "${tg}"` };
9997
10045
  const config = await readConfig3();
9998
- assignmentDir = resolvePath(config.defaultProjectDir, parts[0], "assignments", parts[1]);
10046
+ assignmentDir = resolvePath2(config.defaultProjectDir, parts[0], "assignments", parts[1]);
9999
10047
  assignmentRef = `${parts[0]}/${parts[1]}`;
10000
10048
  } else if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(tg)) {
10001
- assignmentDir = resolvePath(assignmentsDirFn(), tg);
10049
+ assignmentDir = resolvePath2(assignmentsDirFn(), tg);
10002
10050
  assignmentRef = tg;
10003
10051
  } else {
10004
10052
  return { error: `Invalid target.assignment "${tg}"` };
10005
10053
  }
10006
- const assignmentMdPath2 = resolvePath(assignmentDir, "assignment.md");
10054
+ const assignmentMdPath2 = resolvePath2(assignmentDir, "assignment.md");
10007
10055
  if (!await fileExists2(assignmentMdPath2)) return { error: `Target assignment not found: ${assignmentMdPath2}` };
10008
10056
  }
10009
- const assignmentMdPath = resolvePath(assignmentDir, "assignment.md");
10057
+ const assignmentMdPath = resolvePath2(assignmentDir, "assignment.md");
10010
10058
  let content = await readFile32(assignmentMdPath, "utf-8");
10011
10059
  content = appendTodosToAssignmentBody2(
10012
10060
  content,
@@ -10050,6 +10098,115 @@ workspace: ${workspace}
10050
10098
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to promote todos" });
10051
10099
  }
10052
10100
  });
10101
+ router.post("/:workspace/:id/move", async (req, res) => {
10102
+ try {
10103
+ const sourceWs = getWorkspaceParam(req.params.workspace);
10104
+ const id = req.params.id;
10105
+ const to = req.body?.to;
10106
+ if (!to || typeof to !== "object") {
10107
+ res.status(400).json({ error: "body.to is required" });
10108
+ return;
10109
+ }
10110
+ const targetCount = [Boolean(to.workspace), Boolean(to.project), Boolean(to.global)].filter(Boolean).length;
10111
+ if (targetCount !== 1) {
10112
+ res.status(400).json({ error: "body.to must specify exactly one of workspace, project, or global" });
10113
+ return;
10114
+ }
10115
+ if (to.project && !isValidSlug(to.project)) {
10116
+ res.status(400).json({ error: `Invalid target project slug: "${to.project}"` });
10117
+ return;
10118
+ }
10119
+ if (to.workspace && !WORKSPACE_REGEX.test(to.workspace)) {
10120
+ res.status(400).json({ error: `Invalid target workspace name: "${to.workspace}"` });
10121
+ return;
10122
+ }
10123
+ let target;
10124
+ if (to.global) {
10125
+ target = { kind: "workspace", id: "_global", todosPath: todosDir2, lockKey: "ws:_global" };
10126
+ } else if (to.workspace) {
10127
+ target = { kind: "workspace", id: to.workspace, todosPath: todosDir2, lockKey: `ws:${to.workspace}` };
10128
+ } else {
10129
+ if (!projectsDir2) {
10130
+ res.status(500).json({ error: "Server not configured with projectsDir; cannot move to project scope" });
10131
+ return;
10132
+ }
10133
+ const slug = to.project;
10134
+ const projectMd = resolvePath(projectsDir2, slug, "project.md");
10135
+ if (!await fileExists(projectMd)) {
10136
+ res.status(404).json({ error: `Target project "${slug}" not found` });
10137
+ return;
10138
+ }
10139
+ target = {
10140
+ kind: "project",
10141
+ id: slug,
10142
+ todosPath: projectTodosDir(projectsDir2, slug),
10143
+ lockKey: `proj:${slug}`
10144
+ };
10145
+ }
10146
+ const sourceLockKey = `ws:${sourceWs}`;
10147
+ if (sourceLockKey === target.lockKey) {
10148
+ res.status(400).json({ error: "cannot move to the same scope" });
10149
+ return;
10150
+ }
10151
+ const result = await withTwoLocks(sourceLockKey, target.lockKey, async () => {
10152
+ const sourceChecklist = await readChecklist(todosDir2, sourceWs);
10153
+ const targetChecklist = await readChecklist(target.todosPath, target.id);
10154
+ const idx = sourceChecklist.items.findIndex((i) => i.id === id);
10155
+ if (idx === -1) return { status: 404, error: `Todo "${id}" not found` };
10156
+ if (targetChecklist.items.some((i) => i.id === id)) {
10157
+ return { status: 409, error: "id already exists in target" };
10158
+ }
10159
+ const item = sourceChecklist.items[idx];
10160
+ if (item.planDir) {
10161
+ const newPlanDir = todoPlanDir(target.todosPath, target.id, id);
10162
+ if (await fileExists(newPlanDir)) {
10163
+ return { status: 409, error: "plan dir already exists in target" };
10164
+ }
10165
+ await mkdir2(dirname4(newPlanDir), { recursive: true });
10166
+ await rename3(item.planDir, newPlanDir);
10167
+ item.planDir = newPlanDir;
10168
+ }
10169
+ sourceChecklist.items.splice(idx, 1);
10170
+ targetChecklist.items.push(item);
10171
+ await writeChecklist(todosDir2, sourceChecklist);
10172
+ await writeChecklist(target.todosPath, targetChecklist);
10173
+ const sourceLabel = sourceWs === "_global" ? "_global" : `workspace:${sourceWs}`;
10174
+ const targetLabel = target.kind === "project" ? `project:${target.id}` : target.id === "_global" ? "_global" : `workspace:${target.id}`;
10175
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
10176
+ await appendLogEntry2(todosDir2, sourceWs, {
10177
+ timestamp: ts,
10178
+ itemIds: [id],
10179
+ items: item.description,
10180
+ session: null,
10181
+ branch: item.branch || null,
10182
+ summary: `Moved to ${targetLabel}`,
10183
+ blockers: null,
10184
+ status: null
10185
+ });
10186
+ await appendLogEntry2(target.todosPath, target.id, {
10187
+ timestamp: ts,
10188
+ itemIds: [id],
10189
+ items: item.description,
10190
+ session: null,
10191
+ branch: item.branch || null,
10192
+ summary: `Moved from ${sourceLabel}`,
10193
+ blockers: null,
10194
+ status: null
10195
+ });
10196
+ return { status: 200, item };
10197
+ });
10198
+ if (result.status !== 200) {
10199
+ res.status(result.status).json({ error: result.error });
10200
+ return;
10201
+ }
10202
+ broadcastUpdate();
10203
+ if (target.kind === "project") broadcastProject(target.id);
10204
+ else broadcastUpdate();
10205
+ res.json({ moved: id, to: target });
10206
+ } catch (error) {
10207
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to move todo" });
10208
+ }
10209
+ });
10053
10210
  router.post("/:workspace/:id/unblock", async (req, res) => {
10054
10211
  try {
10055
10212
  const workspace = getWorkspaceParam(req.params.workspace);
@@ -10082,18 +10239,9 @@ init_fs();
10082
10239
  init_paths();
10083
10240
  init_slug();
10084
10241
  import { Router as Router6 } from "express";
10085
- import { mkdir as mkdir2, readFile as readFile14 } from "fs/promises";
10086
- import { resolve as resolve19 } from "path";
10087
- var writeLocks2 = /* @__PURE__ */ new Map();
10088
- function projLock(slug, fn) {
10089
- const key = `proj:${slug}`;
10090
- const prev = writeLocks2.get(key) ?? Promise.resolve();
10091
- const next = prev.then(fn);
10092
- writeLocks2.set(key, next.then(() => {
10093
- }, () => {
10094
- }));
10095
- return next;
10096
- }
10242
+ import { mkdir as mkdir3, readFile as readFile14, rename as rename4 } from "fs/promises";
10243
+ import { resolve as resolve19, dirname as dirname5 } from "path";
10244
+ var WORKSPACE_REGEX2 = /^[a-z0-9_][a-z0-9-]*$/;
10097
10245
  function touchItem2(item) {
10098
10246
  const now = (/* @__PURE__ */ new Date()).toISOString();
10099
10247
  if (item.createdAt === null) item.createdAt = now;
@@ -10112,7 +10260,7 @@ async function projectExists(projectsDir2, slug) {
10112
10260
  async function ensureProjectTodosDir(projectsDir2, slug) {
10113
10261
  const todosDir2 = projectTodosDir(projectsDir2, slug);
10114
10262
  try {
10115
- await mkdir2(todosDir2, { recursive: false });
10263
+ await mkdir3(todosDir2, { recursive: false });
10116
10264
  } catch (err2) {
10117
10265
  const code = err2.code;
10118
10266
  if (code === "EEXIST") return;
@@ -10124,7 +10272,7 @@ async function ensureProjectTodosDir(projectsDir2, slug) {
10124
10272
  throw err2;
10125
10273
  }
10126
10274
  try {
10127
- await mkdir2(resolve19(todosDir2, "archive"), { recursive: false });
10275
+ await mkdir3(resolve19(todosDir2, "archive"), { recursive: false });
10128
10276
  } catch (err2) {
10129
10277
  const code = err2.code;
10130
10278
  if (code === "EEXIST") return;
@@ -10139,11 +10287,14 @@ async function ensureProjectTodosDir(projectsDir2, slug) {
10139
10287
  function notFound(res, slug) {
10140
10288
  res.status(404).json({ error: `Project "${slug}" not found` });
10141
10289
  }
10142
- function createProjectTodosRouter(projectsDir2, broadcast) {
10290
+ function createProjectTodosRouter(projectsDir2, broadcast, workspaceTodosDir) {
10143
10291
  const router = Router6({ mergeParams: true });
10144
10292
  function broadcastUpdate(projectSlug) {
10145
10293
  broadcast({ type: "todos-updated", projectSlug, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
10146
10294
  }
10295
+ function broadcastWorkspace() {
10296
+ broadcast({ type: "todos-updated", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
10297
+ }
10147
10298
  function validateProjectId(req, res, next) {
10148
10299
  const slug = getProjectIdParam(params(req).projectId);
10149
10300
  if (!slug || !isValidSlug(slug)) {
@@ -10691,6 +10842,259 @@ workspace: ${slug}
10691
10842
  res.status(500).json({ error: error instanceof Error ? error.message : "Failed to unblock todo" });
10692
10843
  }
10693
10844
  });
10845
+ router.post("/promote", async (req, res) => {
10846
+ try {
10847
+ const slug = getProjectIdParam(params(req).projectId);
10848
+ if (!await projectExists(projectsDir2, slug)) {
10849
+ notFound(res, slug);
10850
+ return;
10851
+ }
10852
+ const { todoIds, mode, target, title, type, priority, keepSource } = req.body ?? {};
10853
+ if (!Array.isArray(todoIds) || todoIds.length === 0) {
10854
+ res.status(400).json({ error: "todoIds (non-empty array of strings) is required" });
10855
+ return;
10856
+ }
10857
+ if (mode !== "new-assignment" && mode !== "to-assignment") {
10858
+ res.status(400).json({ error: 'mode must be "new-assignment" or "to-assignment"' });
10859
+ return;
10860
+ }
10861
+ const result = await projLock(slug, async () => {
10862
+ if (!await projectExists(projectsDir2, slug)) return { gone: true };
10863
+ await ensureProjectTodosDir(projectsDir2, slug);
10864
+ const todosDir2 = projectTodosDir(projectsDir2, slug);
10865
+ const checklist = await readChecklist(todosDir2, slug);
10866
+ const items = [];
10867
+ for (const id of todoIds) {
10868
+ const item = checklist.items.find((i) => i.id === id);
10869
+ if (!item) return { error: `Todo "${id}" not found` };
10870
+ if (item.status === "completed") return { error: `Todo "${id}" is already completed` };
10871
+ items.push(item);
10872
+ }
10873
+ const scopeLabel = `project:${slug}`;
10874
+ const { assignmentsDir: assignmentsDirFn } = await Promise.resolve().then(() => (init_paths(), paths_exports));
10875
+ const { appendTodosToAssignmentBody: appendTodosToAssignmentBody2, touchAssignmentUpdated: touchAssignmentUpdated2 } = await Promise.resolve().then(() => (init_assignment_todos(), assignment_todos_exports));
10876
+ const { nowTimestamp: nowTimestamp3 } = await Promise.resolve().then(() => (init_timestamp(), timestamp_exports));
10877
+ let assignmentRef;
10878
+ let assignmentDir;
10879
+ if (mode === "new-assignment") {
10880
+ const targetProject = target?.project ?? slug;
10881
+ if (!targetProject) return { error: "target.project is required for new-assignment mode" };
10882
+ if (items.length > 1 && !title) return { error: "title is required when promoting multiple todos" };
10883
+ const { createAssignmentCommand: createAssignmentCommand2 } = await Promise.resolve().then(() => (init_create_assignment(), create_assignment_exports));
10884
+ const created = await createAssignmentCommand2(title || items[0].description, {
10885
+ project: targetProject,
10886
+ type,
10887
+ priority,
10888
+ withTodos: true,
10889
+ silent: true
10890
+ });
10891
+ assignmentDir = created.assignmentDir;
10892
+ assignmentRef = `${created.projectSlug}/${created.slug}`;
10893
+ } else {
10894
+ const tg = target?.assignment || "";
10895
+ if (!tg) return { error: "target.assignment is required for to-assignment mode" };
10896
+ if (tg.includes("/")) {
10897
+ const parts = tg.split("/");
10898
+ if (parts.length !== 2) return { error: `Invalid target.assignment "${tg}"` };
10899
+ assignmentDir = resolve19(projectsDir2, parts[0], "assignments", parts[1]);
10900
+ assignmentRef = `${parts[0]}/${parts[1]}`;
10901
+ } else if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(tg)) {
10902
+ assignmentDir = resolve19(assignmentsDirFn(), tg);
10903
+ assignmentRef = tg;
10904
+ } else {
10905
+ return { error: `Invalid target.assignment "${tg}"` };
10906
+ }
10907
+ const assignmentMdPath2 = resolve19(assignmentDir, "assignment.md");
10908
+ if (!await fileExists(assignmentMdPath2)) return { error: `Target assignment not found: ${assignmentMdPath2}` };
10909
+ }
10910
+ const assignmentMdPath = resolve19(assignmentDir, "assignment.md");
10911
+ let content = await readFile14(assignmentMdPath, "utf-8");
10912
+ content = appendTodosToAssignmentBody2(
10913
+ content,
10914
+ items.map((it) => ({
10915
+ description: it.description,
10916
+ trace: `promoted from t:${it.id} in ${scopeLabel}`
10917
+ }))
10918
+ );
10919
+ content = touchAssignmentUpdated2(content, nowTimestamp3());
10920
+ await writeFileForce(assignmentMdPath, content);
10921
+ if (!keepSource) {
10922
+ for (const item of items) {
10923
+ item.status = "completed";
10924
+ item.session = null;
10925
+ touchItem2(item);
10926
+ }
10927
+ checklist.workspace = slug;
10928
+ await writeChecklist(todosDir2, checklist);
10929
+ for (const item of items) {
10930
+ const entry = {
10931
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10932
+ itemIds: [item.id],
10933
+ items: item.description,
10934
+ session: null,
10935
+ branch: item.branch || null,
10936
+ summary: `Promoted to assignment ${assignmentRef}`,
10937
+ blockers: null,
10938
+ status: null
10939
+ };
10940
+ await appendLogEntry2(todosDir2, slug, entry);
10941
+ }
10942
+ }
10943
+ return { assignmentRef, assignmentDir, promoted: items.map((i) => i.id) };
10944
+ });
10945
+ if ("gone" in result) {
10946
+ notFound(res, slug);
10947
+ return;
10948
+ }
10949
+ if ("error" in result) {
10950
+ res.status(400).json({ error: result.error });
10951
+ return;
10952
+ }
10953
+ broadcastUpdate(slug);
10954
+ res.json(result);
10955
+ } catch (error) {
10956
+ if (error.code === "PROJECT_GONE") {
10957
+ notFound(res, getProjectIdParam(params(req).projectId));
10958
+ return;
10959
+ }
10960
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to promote todos" });
10961
+ }
10962
+ });
10963
+ router.post("/:id/move", async (req, res) => {
10964
+ try {
10965
+ const sourceSlug = getProjectIdParam(params(req).projectId);
10966
+ const id = params(req).id ?? "";
10967
+ if (!await projectExists(projectsDir2, sourceSlug)) {
10968
+ notFound(res, sourceSlug);
10969
+ return;
10970
+ }
10971
+ const to = req.body?.to;
10972
+ if (!to || typeof to !== "object") {
10973
+ res.status(400).json({ error: "body.to is required" });
10974
+ return;
10975
+ }
10976
+ const targetCount = [Boolean(to.workspace), Boolean(to.project), Boolean(to.global)].filter(Boolean).length;
10977
+ if (targetCount !== 1) {
10978
+ res.status(400).json({ error: "body.to must specify exactly one of workspace, project, or global" });
10979
+ return;
10980
+ }
10981
+ if (to.project && !isValidSlug(to.project)) {
10982
+ res.status(400).json({ error: `Invalid target project slug: "${to.project}"` });
10983
+ return;
10984
+ }
10985
+ if (to.workspace && !WORKSPACE_REGEX2.test(to.workspace)) {
10986
+ res.status(400).json({ error: `Invalid target workspace name: "${to.workspace}"` });
10987
+ return;
10988
+ }
10989
+ let target;
10990
+ if (to.global) {
10991
+ if (!workspaceTodosDir) {
10992
+ res.status(500).json({ error: "Server not configured with workspaceTodosDir; cannot move to global scope" });
10993
+ return;
10994
+ }
10995
+ target = { kind: "workspace", id: "_global", todosPath: workspaceTodosDir, lockKey: "ws:_global" };
10996
+ } else if (to.workspace) {
10997
+ if (!workspaceTodosDir) {
10998
+ res.status(500).json({ error: "Server not configured with workspaceTodosDir; cannot move to workspace scope" });
10999
+ return;
11000
+ }
11001
+ target = { kind: "workspace", id: to.workspace, todosPath: workspaceTodosDir, lockKey: `ws:${to.workspace}` };
11002
+ } else {
11003
+ const tslug = to.project;
11004
+ if (!await projectExists(projectsDir2, tslug)) {
11005
+ res.status(404).json({ error: `Target project "${tslug}" not found` });
11006
+ return;
11007
+ }
11008
+ target = {
11009
+ kind: "project",
11010
+ id: tslug,
11011
+ todosPath: projectTodosDir(projectsDir2, tslug),
11012
+ lockKey: `proj:${tslug}`
11013
+ };
11014
+ }
11015
+ const sourceLockKey = `proj:${sourceSlug}`;
11016
+ if (sourceLockKey === target.lockKey) {
11017
+ res.status(400).json({ error: "cannot move to the same scope" });
11018
+ return;
11019
+ }
11020
+ const result = await withTwoLocks(sourceLockKey, target.lockKey, async () => {
11021
+ if (!await projectExists(projectsDir2, sourceSlug)) return { status: "gone" };
11022
+ if (target.kind === "project" && !await projectExists(projectsDir2, target.id)) {
11023
+ return { status: "targetGone" };
11024
+ }
11025
+ await ensureProjectTodosDir(projectsDir2, sourceSlug);
11026
+ const sourceTodosDir = projectTodosDir(projectsDir2, sourceSlug);
11027
+ const sourceChecklist = await readChecklist(sourceTodosDir, sourceSlug);
11028
+ const targetChecklist = await readChecklist(target.todosPath, target.id);
11029
+ const idx = sourceChecklist.items.findIndex((i) => i.id === id);
11030
+ if (idx === -1) return { status: 404, error: `Todo "${id}" not found` };
11031
+ if (targetChecklist.items.some((i) => i.id === id)) {
11032
+ return { status: 409, error: "id already exists in target" };
11033
+ }
11034
+ const item = sourceChecklist.items[idx];
11035
+ if (item.planDir) {
11036
+ const newPlanDir = todoPlanDir(target.todosPath, target.id, id);
11037
+ if (await fileExists(newPlanDir)) {
11038
+ return { status: 409, error: "plan dir already exists in target" };
11039
+ }
11040
+ await mkdir3(dirname5(newPlanDir), { recursive: true });
11041
+ await rename4(item.planDir, newPlanDir);
11042
+ item.planDir = newPlanDir;
11043
+ }
11044
+ sourceChecklist.items.splice(idx, 1);
11045
+ targetChecklist.items.push(item);
11046
+ sourceChecklist.workspace = sourceSlug;
11047
+ await writeChecklist(sourceTodosDir, sourceChecklist);
11048
+ await writeChecklist(target.todosPath, targetChecklist);
11049
+ const sourceLabel = `project:${sourceSlug}`;
11050
+ const targetLabel = target.kind === "project" ? `project:${target.id}` : target.id === "_global" ? "_global" : `workspace:${target.id}`;
11051
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
11052
+ await appendLogEntry2(sourceTodosDir, sourceSlug, {
11053
+ timestamp: ts,
11054
+ itemIds: [id],
11055
+ items: item.description,
11056
+ session: null,
11057
+ branch: item.branch || null,
11058
+ summary: `Moved to ${targetLabel}`,
11059
+ blockers: null,
11060
+ status: null
11061
+ });
11062
+ await appendLogEntry2(target.todosPath, target.id, {
11063
+ timestamp: ts,
11064
+ itemIds: [id],
11065
+ items: item.description,
11066
+ session: null,
11067
+ branch: item.branch || null,
11068
+ summary: `Moved from ${sourceLabel}`,
11069
+ blockers: null,
11070
+ status: null
11071
+ });
11072
+ return { status: 200, item };
11073
+ });
11074
+ if (result.status === "gone") {
11075
+ notFound(res, sourceSlug);
11076
+ return;
11077
+ }
11078
+ if (result.status === "targetGone") {
11079
+ res.status(404).json({ error: `Target project "${target.id}" not found` });
11080
+ return;
11081
+ }
11082
+ if (result.status !== 200) {
11083
+ res.status(result.status).json({ error: result.error });
11084
+ return;
11085
+ }
11086
+ broadcastUpdate(sourceSlug);
11087
+ if (target.kind === "project") broadcastUpdate(target.id);
11088
+ else broadcastWorkspace();
11089
+ res.json({ moved: id, to: target });
11090
+ } catch (error) {
11091
+ if (error.code === "PROJECT_GONE") {
11092
+ notFound(res, getProjectIdParam(params(req).projectId));
11093
+ return;
11094
+ }
11095
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to move todo" });
11096
+ }
11097
+ });
10694
11098
  return router;
10695
11099
  }
10696
11100
 
@@ -10704,7 +11108,7 @@ init_fs();
10704
11108
  init_config2();
10705
11109
  import { execFile as execFile2 } from "child_process";
10706
11110
  import { promisify as promisify2 } from "util";
10707
- import { cp, mkdtemp, rm as rm2, readFile as readFile15, writeFile as writeFile4, unlink as unlink3, stat, open, rename as rename3 } from "fs/promises";
11111
+ import { cp, mkdtemp, rm as rm2, readFile as readFile15, writeFile as writeFile4, unlink as unlink3, stat, open, rename as rename5 } from "fs/promises";
10708
11112
  import { resolve as resolve20, join as join2 } from "path";
10709
11113
  import { tmpdir } from "os";
10710
11114
  var exec2 = promisify2(execFile2);
@@ -10916,7 +11320,7 @@ async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
10916
11320
  const localExistsBefore = await fileExists(localPath);
10917
11321
  if (backupExistsBefore) {
10918
11322
  if (!localExistsBefore) {
10919
- await rename3(backupPath, localPath);
11323
+ await rename5(backupPath, localPath);
10920
11324
  } else {
10921
11325
  throw new Error(
10922
11326
  `Cannot restore "${localPath}": a stale crash-recovery backup exists at ${backupPath} while the current path also exists. Inspect both and remove the one you don't need, then retry.`
@@ -10928,15 +11332,15 @@ async function safeRestoreCategory(localPath, repoSrcPath, isFile) {
10928
11332
  await cp(repoSrcPath, stagingPath, { recursive: true, force: true });
10929
11333
  const localExists = await fileExists(localPath);
10930
11334
  if (localExists) {
10931
- await rename3(localPath, backupPath);
11335
+ await rename5(localPath, backupPath);
10932
11336
  localMovedAside = true;
10933
11337
  }
10934
- await rename3(stagingPath, localPath);
11338
+ await rename5(stagingPath, localPath);
10935
11339
  await rm2(backupPath, { recursive: true, force: true }).catch(() => {
10936
11340
  });
10937
11341
  } catch (err2) {
10938
11342
  if (localMovedAside && await fileExists(backupPath)) {
10939
- await rename3(backupPath, localPath).catch(() => {
11343
+ await rename5(backupPath, localPath).catch(() => {
10940
11344
  });
10941
11345
  }
10942
11346
  await rm2(stagingPath, { recursive: true, force: true }).catch(() => {
@@ -11675,8 +12079,8 @@ function createDashboardServer(options) {
11675
12079
  app.use("/api/servers", createServersRouter(serversDir2, projectsDir2, assignmentsDir2));
11676
12080
  app.use("/api/agent-sessions", createAgentSessionsRouter(projectsDir2, broadcast, assignmentsDir2));
11677
12081
  app.use("/api/playbooks", createPlaybooksRouter(playbooksDir3));
11678
- app.use("/api/todos", createTodosRouter(todosDir2, broadcast));
11679
- app.use("/api/projects/:projectId/todos", createProjectTodosRouter(projectsDir2, broadcast));
12082
+ app.use("/api/todos", createTodosRouter(todosDir2, broadcast, projectsDir2));
12083
+ app.use("/api/projects/:projectId/todos", createProjectTodosRouter(projectsDir2, broadcast, todosDir2));
11680
12084
  app.use("/api/backup", createBackupRouter());
11681
12085
  if (serveStaticUi && dashboardDistPath) {
11682
12086
  const sendOpts = { dotfiles: "allow" };
@@ -11822,7 +12226,7 @@ async function dashboardCommand(options) {
11822
12226
  port = availablePort;
11823
12227
  }
11824
12228
  const thisFile = fileURLToPath2(import.meta.url);
11825
- const packageRoot = resolve22(dirname4(thisFile), "..");
12229
+ const packageRoot = resolve22(dirname6(thisFile), "..");
11826
12230
  const dashboardDist = resolve22(packageRoot, "dashboard", "dist");
11827
12231
  const server = createDashboardServer({
11828
12232
  port,
@@ -12051,14 +12455,14 @@ import {
12051
12455
  } from "fs/promises";
12052
12456
  import { existsSync } from "fs";
12053
12457
  import { homedir as homedir2 } from "os";
12054
- import { basename, dirname as dirname6, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve25 } from "path";
12458
+ import { basename, dirname as dirname8, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve25 } from "path";
12055
12459
 
12056
12460
  // src/utils/package-root.ts
12057
12461
  init_fs();
12058
- import { dirname as dirname5, resolve as resolve24 } from "path";
12462
+ import { dirname as dirname7, resolve as resolve24 } from "path";
12059
12463
  import { fileURLToPath as fileURLToPath3 } from "url";
12060
12464
  async function findPackageRoot(expectedRelativePath) {
12061
- let currentDir = dirname5(fileURLToPath3(import.meta.url));
12465
+ let currentDir = dirname7(fileURLToPath3(import.meta.url));
12062
12466
  while (true) {
12063
12467
  const candidate = resolve24(currentDir, expectedRelativePath);
12064
12468
  if (await fileExists(candidate)) {
@@ -12150,7 +12554,7 @@ async function getInstallStatus(targetDir, pluginKind) {
12150
12554
  const info = await lstat(targetDir);
12151
12555
  if (info.isSymbolicLink()) {
12152
12556
  const symlinkTarget = await readlink(targetDir);
12153
- const resolvedTarget = resolve25(dirname6(targetDir), symlinkTarget);
12557
+ const resolvedTarget = resolve25(dirname8(targetDir), symlinkTarget);
12154
12558
  const manifestName2 = await readPluginManifestName(resolvedTarget, pluginKind);
12155
12559
  return {
12156
12560
  exists: true,
@@ -12187,15 +12591,15 @@ async function writeInstallMetadata(targetDir, pluginKind, installMode, packageM
12187
12591
  );
12188
12592
  }
12189
12593
  async function installCopy(paths, pluginKind) {
12190
- await ensureDir(dirname6(paths.targetDir));
12594
+ await ensureDir(dirname8(paths.targetDir));
12191
12595
  await cp2(paths.sourceDir, paths.targetDir, { recursive: true });
12192
12596
  const packageManifest = await readPackageManifest(paths.packageRoot);
12193
12597
  await writeInstallMetadata(paths.targetDir, pluginKind, "copy", packageManifest);
12194
12598
  }
12195
12599
  async function installLink(paths) {
12196
- await ensureDir(dirname6(paths.targetDir));
12600
+ await ensureDir(dirname8(paths.targetDir));
12197
12601
  await rm3(paths.targetDir, { recursive: true, force: true });
12198
- await ensureDir(dirname6(paths.targetDir));
12602
+ await ensureDir(dirname8(paths.targetDir));
12199
12603
  await symlink(resolve25(paths.sourceDir), paths.targetDir, "dir");
12200
12604
  }
12201
12605
  async function removeInstallMarker(targetDir) {
@@ -12250,7 +12654,7 @@ async function readClaudeMarketplaceFile(manifestPath) {
12250
12654
  };
12251
12655
  }
12252
12656
  async function writeClaudeMarketplaceFile(manifestPath, marketplace) {
12253
- await ensureDir(dirname6(manifestPath));
12657
+ await ensureDir(dirname8(manifestPath));
12254
12658
  await writeFile6(manifestPath, `${JSON.stringify(marketplace, null, 2)}
12255
12659
  `, "utf-8");
12256
12660
  }
@@ -12398,7 +12802,7 @@ async function registerKnownClaudeMarketplace(name, rootDir) {
12398
12802
  };
12399
12803
  existing[name].lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
12400
12804
  existing[name].autoUpdate = true;
12401
- await ensureDir(dirname6(manifestPath));
12805
+ await ensureDir(dirname8(manifestPath));
12402
12806
  await writeFile6(manifestPath, `${JSON.stringify(existing, null, 2)}
12403
12807
  `, "utf-8");
12404
12808
  }
@@ -12433,11 +12837,11 @@ async function ensureClaudeUserMarketplace() {
12433
12837
  }
12434
12838
  async function detectClaudeMarketplaceForTarget(targetDir) {
12435
12839
  const normalizedTargetDir = normalizeAbsoluteInstallPath(targetDir, "Claude plugin target");
12436
- const pluginsDir = dirname6(normalizedTargetDir);
12840
+ const pluginsDir = dirname8(normalizedTargetDir);
12437
12841
  if (basename(pluginsDir) !== "plugins") {
12438
12842
  return null;
12439
12843
  }
12440
- const rootDir = dirname6(pluginsDir);
12844
+ const rootDir = dirname8(pluginsDir);
12441
12845
  const manifestPath = resolve25(rootDir, ".claude-plugin", "marketplace.json");
12442
12846
  if (!await fileExists(manifestPath)) {
12443
12847
  return null;
@@ -12603,7 +13007,7 @@ async function installManagedPlugin(options) {
12603
13007
  };
12604
13008
  }
12605
13009
  function buildMarketplaceSourcePath(pluginTargetDir, marketplacePath) {
12606
- const relPath = relative2(dirname6(marketplacePath), pluginTargetDir).replaceAll("\\", "/");
13010
+ const relPath = relative2(dirname8(marketplacePath), pluginTargetDir).replaceAll("\\", "/");
12607
13011
  if (relPath === "") {
12608
13012
  return ".";
12609
13013
  }
@@ -12640,7 +13044,7 @@ async function readMarketplaceFile(marketplacePath) {
12640
13044
  };
12641
13045
  }
12642
13046
  async function writeMarketplaceFile(marketplacePath, marketplace) {
12643
- await ensureDir(dirname6(marketplacePath));
13047
+ await ensureDir(dirname8(marketplacePath));
12644
13048
  await writeFile6(marketplacePath, `${JSON.stringify(marketplace, null, 2)}
12645
13049
  `, "utf-8");
12646
13050
  }
@@ -12867,8 +13271,8 @@ async function textPrompt(question, defaultValue) {
12867
13271
 
12868
13272
  // src/utils/install-skills.ts
12869
13273
  init_fs();
12870
- import { readFile as readFile17, readdir as readdir11, mkdir as mkdir3, copyFile, rm as rm4 } from "fs/promises";
12871
- import { dirname as dirname7, resolve as resolve26, relative as relative3, join as join3 } from "path";
13274
+ import { readFile as readFile17, readdir as readdir11, mkdir as mkdir4, copyFile, rm as rm4 } from "fs/promises";
13275
+ import { dirname as dirname9, resolve as resolve26, relative as relative3, join as join3 } from "path";
12872
13276
  import { fileURLToPath as fileURLToPath4 } from "url";
12873
13277
  import { homedir as homedir3 } from "os";
12874
13278
  var REQUIRED_SKILLS = [
@@ -12877,12 +13281,18 @@ var REQUIRED_SKILLS = [
12877
13281
  "plan-assignment",
12878
13282
  "complete-assignment",
12879
13283
  "create-assignment",
12880
- "create-project"
13284
+ "create-project",
13285
+ "save-session-summary"
12881
13286
  ];
12882
13287
  function getVendoredSkillsDir() {
12883
- const here = dirname7(fileURLToPath4(import.meta.url));
13288
+ const here = dirname9(fileURLToPath4(import.meta.url));
12884
13289
  return resolve26(here, "..", "vendor", "syntaur-skills", "skills");
12885
13290
  }
13291
+ function getPlatformSkillsDir(target) {
13292
+ const here = dirname9(fileURLToPath4(import.meta.url));
13293
+ const kind = target === "claude" ? "claude-code" : "codex";
13294
+ return resolve26(here, "..", "platforms", kind, "skills");
13295
+ }
12886
13296
  function defaultSkillTargetDir(target) {
12887
13297
  if (target === "claude") return resolve26(homedir3(), ".claude", "skills");
12888
13298
  return resolve26(homedir3(), ".codex", "skills");
@@ -12913,7 +13323,7 @@ async function filesEqual(a, b) {
12913
13323
  }
12914
13324
  }
12915
13325
  async function copyDir(srcDir, destDir) {
12916
- await mkdir3(destDir, { recursive: true });
13326
+ await mkdir4(destDir, { recursive: true });
12917
13327
  const entries = await readdir11(srcDir, { withFileTypes: true });
12918
13328
  for (const entry of entries) {
12919
13329
  const src = join3(srcDir, entry.name);
@@ -12937,8 +13347,24 @@ async function skillMatches(srcDir, destDir) {
12937
13347
  if (destFiles.length !== srcFiles.length) return false;
12938
13348
  return true;
12939
13349
  }
13350
+ async function installSkillDir(srcDir, destDir, skillName, source, force) {
13351
+ if (!await fileExists(destDir)) {
13352
+ await copyDir(srcDir, destDir);
13353
+ return { skill: skillName, status: "installed", targetPath: destDir, source };
13354
+ }
13355
+ if (await skillMatches(srcDir, destDir)) {
13356
+ return { skill: skillName, status: "already-current", targetPath: destDir, source };
13357
+ }
13358
+ if (force) {
13359
+ await rm4(destDir, { recursive: true, force: true });
13360
+ await copyDir(srcDir, destDir);
13361
+ return { skill: skillName, status: "overwritten", targetPath: destDir, source };
13362
+ }
13363
+ return { skill: skillName, status: "differs-preserved", targetPath: destDir, source };
13364
+ }
12940
13365
  async function installSkills(options) {
12941
13366
  const source = options.sourceDir ?? getVendoredSkillsDir();
13367
+ const platformSource = options.platformSkillsDir ?? getPlatformSkillsDir(options.target);
12942
13368
  const targetRoot = options.targetDir ?? defaultSkillTargetDir(options.target);
12943
13369
  const force = options.force ?? false;
12944
13370
  if (!await fileExists(source)) {
@@ -12947,42 +13373,22 @@ async function installSkills(options) {
12947
13373
  );
12948
13374
  }
12949
13375
  const results = [];
12950
- await mkdir3(targetRoot, { recursive: true });
13376
+ await mkdir4(targetRoot, { recursive: true });
12951
13377
  for (const skill of REQUIRED_SKILLS) {
12952
13378
  const srcDir = join3(source, skill);
12953
- const destDir = join3(targetRoot, skill);
12954
13379
  if (!await fileExists(srcDir)) continue;
12955
- if (!await fileExists(destDir)) {
12956
- await copyDir(srcDir, destDir);
12957
- results.push({
12958
- skill,
12959
- status: "installed",
12960
- targetPath: destDir
12961
- });
12962
- continue;
12963
- }
12964
- if (await skillMatches(srcDir, destDir)) {
12965
- results.push({
12966
- skill,
12967
- status: "already-current",
12968
- targetPath: destDir
12969
- });
12970
- continue;
12971
- }
12972
- if (force) {
12973
- await rm4(destDir, { recursive: true, force: true });
12974
- await copyDir(srcDir, destDir);
12975
- results.push({
12976
- skill,
12977
- status: "overwritten",
12978
- targetPath: destDir
12979
- });
12980
- } else {
12981
- results.push({
12982
- skill,
12983
- status: "differs-preserved",
12984
- targetPath: destDir
12985
- });
13380
+ const destDir = join3(targetRoot, skill);
13381
+ results.push(await installSkillDir(srcDir, destDir, skill, "shared", force));
13382
+ }
13383
+ if (options.target === "claude" && await fileExists(platformSource)) {
13384
+ const entries = await readdir11(platformSource, { withFileTypes: true });
13385
+ for (const entry of entries) {
13386
+ if (!entry.isDirectory()) continue;
13387
+ const skill = entry.name;
13388
+ if (REQUIRED_SKILLS.includes(skill)) continue;
13389
+ const srcDir = join3(platformSource, skill);
13390
+ const destDir = join3(targetRoot, skill);
13391
+ results.push(await installSkillDir(srcDir, destDir, skill, "platform", force));
12986
13392
  }
12987
13393
  }
12988
13394
  return results;
@@ -12990,8 +13396,16 @@ async function installSkills(options) {
12990
13396
  async function uninstallSkills(options) {
12991
13397
  const targetRoot = options.targetDir ?? defaultSkillTargetDir(options.target);
12992
13398
  if (!await fileExists(targetRoot)) return [];
13399
+ const known = new Set(REQUIRED_SKILLS);
13400
+ const platformSource = options.platformSkillsDir ?? getPlatformSkillsDir(options.target);
13401
+ if (options.target === "claude" && await fileExists(platformSource)) {
13402
+ const entries = await readdir11(platformSource, { withFileTypes: true });
13403
+ for (const entry of entries) {
13404
+ if (entry.isDirectory()) known.add(entry.name);
13405
+ }
13406
+ }
12993
13407
  const removed = [];
12994
- for (const skill of REQUIRED_SKILLS) {
13408
+ for (const skill of known) {
12995
13409
  const destDir = join3(targetRoot, skill);
12996
13410
  if (!await fileExists(destDir)) continue;
12997
13411
  const skillMd = join3(destDir, "SKILL.md");
@@ -13125,16 +13539,17 @@ async function installPluginCommand(options) {
13125
13539
  }
13126
13540
  }
13127
13541
  console.log("\nThe plugin is now available in Claude Code.");
13128
- console.log(" Slash commands: /grab-assignment, /plan-assignment, /complete-assignment, /create-assignment, /create-project");
13542
+ console.log(" Slash commands: /grab-assignment, /plan-assignment, /complete-assignment, /create-assignment, /create-project, /track-session, /save-session-summary");
13129
13543
  console.log(" Background: syntaur-protocol skill (auto-invoked)");
13130
- console.log(" Hook: write boundary enforcement (PreToolUse) + SessionStart/End");
13544
+ console.log(" Claude-specific skill: track-session (agent session registration)");
13545
+ console.log(" Hook: write boundary enforcement (PreToolUse) + SessionStart/End + PreCompact (prompts /save-session-summary)");
13131
13546
  }
13132
13547
 
13133
13548
  // src/commands/install-statusline.ts
13134
13549
  init_paths();
13135
13550
  init_fs();
13136
13551
  import { readFile as readFile19, writeFile as writeFile8, copyFile as copyFile2, rm as rm5, stat as stat3, symlink as symlink2, unlink as unlink6, lstat as lstat2 } from "fs/promises";
13137
- import { resolve as resolve28, dirname as dirname9 } from "path";
13552
+ import { resolve as resolve28, dirname as dirname11 } from "path";
13138
13553
  import { homedir as homedir4 } from "os";
13139
13554
  import { fileURLToPath as fileURLToPath5 } from "url";
13140
13555
 
@@ -13142,7 +13557,7 @@ import { fileURLToPath as fileURLToPath5 } from "url";
13142
13557
  init_paths();
13143
13558
  init_fs();
13144
13559
  import { readFile as readFile18, writeFile as writeFile7 } from "fs/promises";
13145
- import { resolve as resolve27, dirname as dirname8 } from "path";
13560
+ import { resolve as resolve27, dirname as dirname10 } from "path";
13146
13561
  import { spawnSync } from "child_process";
13147
13562
  import { checkbox, input as input2, confirm } from "@inquirer/prompts";
13148
13563
  var AVAILABLE_SEGMENTS = [
@@ -13263,7 +13678,7 @@ function renderPreview(config, statuslineScript, cwd) {
13263
13678
  env: {
13264
13679
  ...process.env,
13265
13680
  // Force the child to pick up the freshly-written config from install root.
13266
- HOME: dirname8(dirname8(statuslineScript))
13681
+ HOME: dirname10(dirname10(statuslineScript))
13267
13682
  }
13268
13683
  });
13269
13684
  if (res.status !== 0) return null;
@@ -13314,7 +13729,7 @@ async function configureStatuslineCommand(options = {}) {
13314
13729
  console.log("(preview mode \u2014 config NOT written)");
13315
13730
  return;
13316
13731
  }
13317
- await ensureDir(dirname8(configPath));
13732
+ await ensureDir(dirname10(configPath));
13318
13733
  await writeFile7(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
13319
13734
  console.log("Wrote statusline config:");
13320
13735
  console.log(` path: ${configPath}`);
@@ -13341,7 +13756,7 @@ async function configureStatuslineCommand(options = {}) {
13341
13756
  async function writeDefaultConfigIfMissing(installRoot) {
13342
13757
  const path = getConfigPath(installRoot);
13343
13758
  if (await fileExists(path)) return;
13344
- await ensureDir(dirname8(path));
13759
+ await ensureDir(dirname10(path));
13345
13760
  const defaultConfig = {
13346
13761
  segments: ["git", "assignment", "session"],
13347
13762
  separator: " \xB7 "
@@ -13351,7 +13766,7 @@ async function writeDefaultConfigIfMissing(installRoot) {
13351
13766
 
13352
13767
  // src/commands/install-statusline.ts
13353
13768
  function getPackageStatuslineSource() {
13354
- const here = dirname9(fileURLToPath5(import.meta.url));
13769
+ const here = dirname11(fileURLToPath5(import.meta.url));
13355
13770
  return resolve28(here, "..", "statusline", "statusline.sh");
13356
13771
  }
13357
13772
  async function readSettingsJson(settingsPath) {
@@ -13368,7 +13783,7 @@ async function readSettingsJson(settingsPath) {
13368
13783
  }
13369
13784
  }
13370
13785
  async function writeSettingsJson(settingsPath, data) {
13371
- await ensureDir(dirname9(settingsPath));
13786
+ await ensureDir(dirname11(settingsPath));
13372
13787
  await writeFile8(settingsPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
13373
13788
  }
13374
13789
  async function resolveMode(mode, existingCommand, ourCommand) {
@@ -13403,7 +13818,7 @@ function extractExistingCommand(settings) {
13403
13818
  };
13404
13819
  }
13405
13820
  async function backupSettings(settingsSnapshot, backupPath) {
13406
- await ensureDir(dirname9(backupPath));
13821
+ await ensureDir(dirname11(backupPath));
13407
13822
  await writeFile8(
13408
13823
  backupPath,
13409
13824
  JSON.stringify(
@@ -13420,7 +13835,7 @@ async function backupSettings(settingsSnapshot, backupPath) {
13420
13835
  );
13421
13836
  }
13422
13837
  async function installScript(sourceScript, destScript, link) {
13423
- await ensureDir(dirname9(destScript));
13838
+ await ensureDir(dirname11(destScript));
13424
13839
  try {
13425
13840
  const s = await lstat2(destScript);
13426
13841
  if (s.isSymbolicLink() || s.isFile()) {
@@ -13488,7 +13903,7 @@ exec ${existingCommand}
13488
13903
  wrapTarget = wrapperPath;
13489
13904
  }
13490
13905
  }
13491
- await ensureDir(dirname9(confPath));
13906
+ await ensureDir(dirname11(confPath));
13492
13907
  await writeFile8(
13493
13908
  confPath,
13494
13909
  wrapTarget ? `# Wrap target \u2014 the command below is invoked with the same stdin; its
@@ -13677,9 +14092,10 @@ async function installCodexPluginCommand(options) {
13677
14092
  }
13678
14093
  console.log("\nThe plugin is now available to Codex.");
13679
14094
  console.log(
13680
- " Protocol skills: syntaur-protocol, create-project, create-assignment, grab-assignment, plan-assignment, complete-assignment"
14095
+ " Protocol skills: syntaur-protocol, create-project, create-assignment, grab-assignment, plan-assignment, complete-assignment, save-session-summary"
13681
14096
  );
13682
14097
  console.log(" Codex-specific: track-session skill (rollout path aware)");
14098
+ console.log(" Slash commands: /track-session, /save-session-summary (no PreCompact hook on Codex \u2014 invoke manually before compaction)");
13683
14099
  console.log(" Hooks: write boundary enforcement, session cleanup");
13684
14100
  }
13685
14101
 
@@ -14408,6 +14824,19 @@ async function disablePlaybookCommand(slug) {
14408
14824
  }
14409
14825
  }
14410
14826
 
14827
+ // src/commands/regen-playbook-manifest.ts
14828
+ init_paths();
14829
+ init_playbooks();
14830
+ init_fs();
14831
+ async function regenPlaybookManifestCommand() {
14832
+ const dir = playbooksDir();
14833
+ if (!await fileExists(dir)) {
14834
+ throw new Error(`Playbooks directory not found at ${dir}. Run "syntaur init" first.`);
14835
+ }
14836
+ await rebuildPlaybookManifest(dir);
14837
+ console.log(`Rebuilt playbook manifest at ${dir}/manifest.md`);
14838
+ }
14839
+
14411
14840
  // src/commands/todo.ts
14412
14841
  init_paths();
14413
14842
  init_parser2();
@@ -14417,7 +14846,7 @@ init_slug();
14417
14846
  import { Command } from "commander";
14418
14847
  import { readFile as readFile23 } from "fs/promises";
14419
14848
  import { resolve as resolve36 } from "path";
14420
- var WORKSPACE_REGEX2 = /^[a-z0-9_][a-z0-9-]*$/;
14849
+ var WORKSPACE_REGEX3 = /^[a-z0-9_][a-z0-9-]*$/;
14421
14850
  async function resolveScope(options) {
14422
14851
  const flagCount = [Boolean(options.project), Boolean(options.workspace), Boolean(options.global)].filter(Boolean).length;
14423
14852
  if (flagCount > 1) {
@@ -14435,7 +14864,7 @@ async function resolveScope(options) {
14435
14864
  return { kind: "project", id: options.project, todosPath: projectTodosDir(config.defaultProjectDir, options.project) };
14436
14865
  }
14437
14866
  if (options.workspace) {
14438
- if (!WORKSPACE_REGEX2.test(options.workspace)) {
14867
+ if (!WORKSPACE_REGEX3.test(options.workspace)) {
14439
14868
  throw new Error(`Invalid workspace name: "${options.workspace}". Use lowercase letters, numbers, hyphens, and underscores.`);
14440
14869
  }
14441
14870
  return { kind: "workspace", id: options.workspace, todosPath: todosDir() };
@@ -14886,7 +15315,7 @@ async function promoteTodos(ids, options) {
14886
15315
  return;
14887
15316
  }
14888
15317
  const target = options.toAssignment;
14889
- const { resolve: resolvePath } = await import("path");
15318
+ const { resolve: resolvePath2 } = await import("path");
14890
15319
  const { readConfig: readConfig3 } = await Promise.resolve().then(() => (init_config2(), config_exports));
14891
15320
  const { assignmentsDir: assignmentsDirFn } = await Promise.resolve().then(() => (init_paths(), paths_exports));
14892
15321
  const config = await readConfig3();
@@ -14897,16 +15326,16 @@ async function promoteTodos(ids, options) {
14897
15326
  if (parts.length !== 2 || !isValidSlug(parts[0]) || !isValidSlug(parts[1])) {
14898
15327
  throw new Error(`Invalid --to-assignment target "${target}". Use <project>/<slug> or a bare UUID.`);
14899
15328
  }
14900
- assignmentDir = resolvePath(config.defaultProjectDir, parts[0], "assignments", parts[1]);
15329
+ assignmentDir = resolvePath2(config.defaultProjectDir, parts[0], "assignments", parts[1]);
14901
15330
  displayRef = `${parts[0]}/${parts[1]}`;
14902
15331
  } else if (UUID_REGEX.test(target)) {
14903
- assignmentDir = resolvePath(assignmentsDirFn(), target);
15332
+ assignmentDir = resolvePath2(assignmentsDirFn(), target);
14904
15333
  displayRef = target;
14905
15334
  } else {
14906
15335
  throw new Error(`Invalid --to-assignment target "${target}". Use <project>/<slug> or a bare UUID.`);
14907
15336
  }
14908
15337
  const { fileExists: fileExists2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
14909
- const assignmentMdPath = resolvePath(assignmentDir, "assignment.md");
15338
+ const assignmentMdPath = resolvePath2(assignmentDir, "assignment.md");
14910
15339
  if (!await fileExists2(assignmentMdPath)) {
14911
15340
  throw new Error(`Target assignment not found: ${assignmentMdPath}`);
14912
15341
  }
@@ -14922,12 +15351,12 @@ function describeScope(scope) {
14922
15351
  return `workspace:${scope.id}`;
14923
15352
  }
14924
15353
  async function injectPromotedTodos(assignmentDir, todos, scopeLabel) {
14925
- const { resolve: resolvePath } = await import("path");
15354
+ const { resolve: resolvePath2 } = await import("path");
14926
15355
  const { readFile: readFile32 } = await import("fs/promises");
14927
15356
  const { writeFileForce: writeFileForce2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
14928
15357
  const { appendTodosToAssignmentBody: appendTodosToAssignmentBody2, touchAssignmentUpdated: touchAssignmentUpdated2 } = await Promise.resolve().then(() => (init_assignment_todos(), assignment_todos_exports));
14929
15358
  const { nowTimestamp: nowTimestamp3 } = await Promise.resolve().then(() => (init_timestamp(), timestamp_exports));
14930
- const assignmentMdPath = resolvePath(assignmentDir, "assignment.md");
15359
+ const assignmentMdPath = resolvePath2(assignmentDir, "assignment.md");
14931
15360
  let content = await readFile32(assignmentMdPath, "utf-8");
14932
15361
  content = appendTodosToAssignmentBody2(
14933
15362
  content,
@@ -15014,6 +15443,90 @@ ${item.description}
15014
15443
  process.exit(1);
15015
15444
  }
15016
15445
  });
15446
+ todoCommand.command("move").description("Move a todo between scopes (workspace \u2194 project \u2194 global) without converting it").argument("<id>", "Todo short ID").option("--to-workspace <slug>", "Target workspace slug").option("--to-project <slug>", "Target project slug").option("--to-global", "Move to global todos").option("--workspace <slug>", "Source workspace slug").option("--project <slug>", "Source project slug (mutually exclusive with --workspace/--global)").option("--global", "Source: global todos").action(async (id, options) => {
15447
+ try {
15448
+ await moveTodo(id, options);
15449
+ } catch (error) {
15450
+ console.error("Error:", error instanceof Error ? error.message : String(error));
15451
+ process.exit(1);
15452
+ }
15453
+ });
15454
+ async function moveTodo(id, options) {
15455
+ const targetCount = [
15456
+ Boolean(options.toWorkspace),
15457
+ Boolean(options.toProject),
15458
+ Boolean(options.toGlobal)
15459
+ ].filter(Boolean).length;
15460
+ if (targetCount !== 1) {
15461
+ throw new Error("Specify exactly one of --to-workspace <slug>, --to-project <slug>, --to-global.");
15462
+ }
15463
+ const sourceScope = await resolveScope({
15464
+ project: options.project,
15465
+ workspace: options.workspace,
15466
+ global: options.global
15467
+ });
15468
+ const targetScope = await resolveScope({
15469
+ project: options.toProject,
15470
+ workspace: options.toWorkspace,
15471
+ global: options.toGlobal
15472
+ });
15473
+ if (sourceScope.kind === targetScope.kind && sourceScope.id === targetScope.id) {
15474
+ throw new Error("Source and target scopes are the same; nothing to move.");
15475
+ }
15476
+ const sourceChecklist = await readChecklist(sourceScope.todosPath, sourceScope.id);
15477
+ const targetChecklist = sourceScope.todosPath === targetScope.todosPath && sourceScope.id === targetScope.id ? sourceChecklist : await readChecklist(targetScope.todosPath, targetScope.id);
15478
+ const idx = sourceChecklist.items.findIndex((i) => i.id === id);
15479
+ if (idx === -1) {
15480
+ throw new Error(`Todo [t:${id}] not found in scope ${describeScope(sourceScope)}.`);
15481
+ }
15482
+ const item = sourceChecklist.items[idx];
15483
+ if (targetChecklist.items.some((i) => i.id === id)) {
15484
+ throw new Error(`Todo id [t:${id}] already exists in target scope ${describeScope(targetScope)}; refusing to move (collision).`);
15485
+ }
15486
+ if (item.planDir) {
15487
+ const newPlanDir = todoPlanDir(targetScope.todosPath, targetScope.id, id);
15488
+ if (await fileExists(newPlanDir)) {
15489
+ throw new Error(`Plan directory already exists at target: ${newPlanDir}; refusing to move.`);
15490
+ }
15491
+ const { rename: rename6, mkdir: mkdir7 } = await import("fs/promises");
15492
+ const { dirname: dirname16 } = await import("path");
15493
+ await mkdir7(dirname16(newPlanDir), { recursive: true });
15494
+ await rename6(item.planDir, newPlanDir);
15495
+ item.planDir = newPlanDir;
15496
+ }
15497
+ sourceChecklist.items.splice(idx, 1);
15498
+ targetChecklist.items.push(item);
15499
+ await writeChecklist(sourceScope.todosPath, sourceChecklist);
15500
+ if (targetChecklist !== sourceChecklist) {
15501
+ await writeChecklist(targetScope.todosPath, targetChecklist);
15502
+ }
15503
+ const sourceLabel = describeScope(sourceScope);
15504
+ const targetLabel = describeScope(targetScope);
15505
+ const ts = nowISO();
15506
+ const sourceEntry = {
15507
+ timestamp: ts,
15508
+ itemIds: [id],
15509
+ items: item.description,
15510
+ session: null,
15511
+ branch: item.branch || null,
15512
+ summary: `Moved to ${targetLabel}`,
15513
+ blockers: null,
15514
+ status: null
15515
+ };
15516
+ const targetEntry = {
15517
+ timestamp: ts,
15518
+ itemIds: [id],
15519
+ items: item.description,
15520
+ session: null,
15521
+ branch: item.branch || null,
15522
+ summary: `Moved from ${sourceLabel}`,
15523
+ blockers: null,
15524
+ status: null
15525
+ };
15526
+ await appendLogEntry2(sourceScope.todosPath, sourceScope.id, sourceEntry);
15527
+ await appendLogEntry2(targetScope.todosPath, targetScope.id, targetEntry);
15528
+ console.log(`Moved [t:${id}] from ${sourceLabel} to ${targetLabel}`);
15529
+ }
15017
15530
 
15018
15531
  // src/commands/backup.ts
15019
15532
  init_config2();
@@ -15098,7 +15611,7 @@ import { Command as Command3 } from "commander";
15098
15611
  // src/utils/doctor/index.ts
15099
15612
  import { fileURLToPath as fileURLToPath7 } from "url";
15100
15613
  import { readFile as readFile27 } from "fs/promises";
15101
- import { dirname as dirname11, join as join5 } from "path";
15614
+ import { dirname as dirname13, join as join5 } from "path";
15102
15615
 
15103
15616
  // src/utils/doctor/context.ts
15104
15617
  init_config2();
@@ -15146,7 +15659,7 @@ init_paths();
15146
15659
  import { resolve as resolve38, isAbsolute as isAbsolute5 } from "path";
15147
15660
  import { readFile as readFile24, stat as stat4 } from "fs/promises";
15148
15661
  import { fileURLToPath as fileURLToPath6 } from "url";
15149
- import { dirname as dirname10, join as join4 } from "path";
15662
+ import { dirname as dirname12, join as join4 } from "path";
15150
15663
  var CATEGORY = "env";
15151
15664
  var syntaurRootExists = {
15152
15665
  id: "env.syntaur-root-exists",
@@ -15477,14 +15990,14 @@ async function readLocalVersion() {
15477
15990
  async function readLocalPkg() {
15478
15991
  try {
15479
15992
  const here = fileURLToPath6(import.meta.url);
15480
- let dir = dirname10(here);
15993
+ let dir = dirname12(here);
15481
15994
  for (let i = 0; i < 6; i++) {
15482
15995
  const candidate = join4(dir, "package.json");
15483
15996
  try {
15484
15997
  const text = await readFile24(candidate, "utf-8");
15485
15998
  return JSON.parse(text);
15486
15999
  } catch {
15487
- dir = dirname10(dir);
16000
+ dir = dirname12(dir);
15488
16001
  }
15489
16002
  }
15490
16003
  return null;
@@ -16885,14 +17398,14 @@ async function finalize(checks) {
16885
17398
  async function readVersion() {
16886
17399
  try {
16887
17400
  const here = fileURLToPath7(import.meta.url);
16888
- let dir = dirname11(here);
17401
+ let dir = dirname13(here);
16889
17402
  for (let i = 0; i < 6; i++) {
16890
17403
  try {
16891
17404
  const raw = await readFile27(join5(dir, "package.json"), "utf-8");
16892
17405
  const parsed = JSON.parse(raw);
16893
17406
  return typeof parsed.version === "string" ? parsed.version : null;
16894
17407
  } catch {
16895
- dir = dirname11(dir);
17408
+ dir = dirname13(dir);
16896
17409
  }
16897
17410
  }
16898
17411
  return null;
@@ -17408,18 +17921,18 @@ init_paths();
17408
17921
  init_fs();
17409
17922
  import { fileURLToPath as fileURLToPath9 } from "url";
17410
17923
  import { readFile as readFile31 } from "fs/promises";
17411
- import { dirname as dirname13, join as join7, resolve as resolve46 } from "path";
17924
+ import { dirname as dirname15, join as join7, resolve as resolve46 } from "path";
17412
17925
  import { spawn as spawn4 } from "child_process";
17413
17926
  import { createInterface as createInterface2 } from "readline/promises";
17414
17927
 
17415
17928
  // src/utils/version.ts
17416
17929
  import { fileURLToPath as fileURLToPath8 } from "url";
17417
17930
  import { readFile as readFile30 } from "fs/promises";
17418
- import { dirname as dirname12, join as join6 } from "path";
17931
+ import { dirname as dirname14, join as join6 } from "path";
17419
17932
  async function readPackageVersion(scriptUrl) {
17420
17933
  try {
17421
17934
  const scriptPath = fileURLToPath8(scriptUrl);
17422
- const pkgRoot = dirname12(dirname12(scriptPath));
17935
+ const pkgRoot = dirname14(dirname14(scriptPath));
17423
17936
  const raw = await readFile30(join6(pkgRoot, "package.json"), "utf-8");
17424
17937
  const parsed = JSON.parse(raw);
17425
17938
  return typeof parsed.version === "string" ? parsed.version : null;
@@ -17461,7 +17974,7 @@ async function writeState(state) {
17461
17974
  `);
17462
17975
  }
17463
17976
  async function resolveNpmBin() {
17464
- const nodeDir = dirname13(process.execPath);
17977
+ const nodeDir = dirname15(process.execPath);
17465
17978
  const isWin = process.platform === "win32";
17466
17979
  const npmName = isWin ? "npm.cmd" : "npm";
17467
17980
  const nearNode = join7(nodeDir, npmName);
@@ -17974,6 +18487,17 @@ program.command("disable-playbook").description("Disable a playbook so agents no
17974
18487
  process.exit(1);
17975
18488
  }
17976
18489
  });
18490
+ program.command("regen-playbook-manifest").description("Rebuild ~/.syntaur/playbooks/manifest.md from current playbook files").action(async () => {
18491
+ try {
18492
+ await regenPlaybookManifestCommand();
18493
+ } catch (error) {
18494
+ console.error(
18495
+ "Error:",
18496
+ error instanceof Error ? error.message : String(error)
18497
+ );
18498
+ process.exit(1);
18499
+ }
18500
+ });
17977
18501
  program.addCommand(todoCommand);
17978
18502
  program.addCommand(backupCommand);
17979
18503
  program.addCommand(doctorCommand);