dreamcontext 0.10.2 → 0.10.5

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 (145) hide show
  1. package/README.md +39 -5
  2. package/agents/curator-auditor.md +5 -0
  3. package/agents/dreamcontext-explore.md +5 -1
  4. package/agents/initializer-ingestor.md +1 -0
  5. package/agents/initializer-scout.md +5 -0
  6. package/agents/sleep-product.md +2 -0
  7. package/agents/sleep-tasks.md +7 -2
  8. package/dist/agents/curator-auditor.md +5 -0
  9. package/dist/agents/dreamcontext-explore.md +5 -1
  10. package/dist/agents/initializer-ingestor.md +1 -0
  11. package/dist/agents/initializer-scout.md +5 -0
  12. package/dist/agents/sleep-product.md +2 -0
  13. package/dist/agents/sleep-tasks.md +7 -2
  14. package/dist/dashboard/assets/{BrainCanvas3D-BuhjqMJE.js → BrainCanvas3D-8hG96aAi.js} +1 -1
  15. package/dist/dashboard/assets/{_baseUniq-BFywhIrF.js → _baseUniq-b4AibH2-.js} +1 -1
  16. package/dist/dashboard/assets/{ar-SA-G6X2FPQ2-0wtAtjf3.js → ar-SA-G6X2FPQ2-DpGPIe5Z.js} +1 -1
  17. package/dist/dashboard/assets/{arc-CbIZg0XW.js → arc-BHNPUsz3.js} +1 -1
  18. package/dist/dashboard/assets/{architectureDiagram-Q4EWVU46-DcA2m4dL.js → architectureDiagram-Q4EWVU46-IzEd_B2U.js} +1 -1
  19. package/dist/dashboard/assets/{az-AZ-76LH7QW2-DypXzn_9.js → az-AZ-76LH7QW2-Ccf6e1ON.js} +1 -1
  20. package/dist/dashboard/assets/{bg-BG-XCXSNQG7-DqjNOvWi.js → bg-BG-XCXSNQG7-XnDUjubv.js} +1 -1
  21. package/dist/dashboard/assets/{blockDiagram-DXYQGD6D-DGUYSkmu.js → blockDiagram-DXYQGD6D-CEINLAFi.js} +1 -1
  22. package/dist/dashboard/assets/{bn-BD-2XOGV67Q-DMT15vLO.js → bn-BD-2XOGV67Q-E9FzPBnu.js} +1 -1
  23. package/dist/dashboard/assets/{c4Diagram-AHTNJAMY-D0ureHL0.js → c4Diagram-AHTNJAMY-VrHpmy8U.js} +1 -1
  24. package/dist/dashboard/assets/{ca-ES-6MX7JW3Y-BIf8pakY.js → ca-ES-6MX7JW3Y-Cnp7YBQb.js} +1 -1
  25. package/dist/dashboard/assets/channel-DwaG7WPN.js +1 -0
  26. package/dist/dashboard/assets/{chunk-4BX2VUAB-BpMo9p3D.js → chunk-4BX2VUAB-CuueCQHq.js} +1 -1
  27. package/dist/dashboard/assets/{chunk-4TB4RGXK-BjWqEumI.js → chunk-4TB4RGXK-BhlZWQoi.js} +1 -1
  28. package/dist/dashboard/assets/{chunk-55IACEB6-CoREiws2.js → chunk-55IACEB6-ClixcVTq.js} +1 -1
  29. package/dist/dashboard/assets/{chunk-EDXVE4YY-DWGx8NFS.js → chunk-EDXVE4YY-DUj1uWsJ.js} +1 -1
  30. package/dist/dashboard/assets/{chunk-FMBD7UC4-BPHXOHEi.js → chunk-FMBD7UC4-BZClSbG6.js} +1 -1
  31. package/dist/dashboard/assets/{chunk-OYMX7WX6-Cg-AvRQR.js → chunk-OYMX7WX6-CMMXG2K9.js} +1 -1
  32. package/dist/dashboard/assets/{chunk-QZHKN3VN-oNBf0ZAT.js → chunk-QZHKN3VN-CI5x9wkp.js} +1 -1
  33. package/dist/dashboard/assets/{chunk-YZCP3GAM-BWTEULwI.js → chunk-YZCP3GAM-DnpOMFO-.js} +1 -1
  34. package/dist/dashboard/assets/classDiagram-6PBFFD2Q-COGCH4LQ.js +1 -0
  35. package/dist/dashboard/assets/classDiagram-v2-HSJHXN6E-COGCH4LQ.js +1 -0
  36. package/dist/dashboard/assets/clone-DwBiydth.js +1 -0
  37. package/dist/dashboard/assets/{cose-bilkent-S5V4N54A-CQAtltI5.js → cose-bilkent-S5V4N54A-C9xnvFgj.js} +1 -1
  38. package/dist/dashboard/assets/{cs-CZ-2BRQDIVT-sgLhn2uw.js → cs-CZ-2BRQDIVT-BHebgNbB.js} +1 -1
  39. package/dist/dashboard/assets/{da-DK-5WZEPLOC-56JnY0Ou.js → da-DK-5WZEPLOC-DYuNSWM8.js} +1 -1
  40. package/dist/dashboard/assets/{dagre-KV5264BT-Czzd8AFD.js → dagre-KV5264BT-Ck8mmM53.js} +1 -1
  41. package/dist/dashboard/assets/{de-DE-XR44H4JA-CLiIEbrn.js → de-DE-XR44H4JA-CMw5lMuT.js} +1 -1
  42. package/dist/dashboard/assets/{diagram-5BDNPKRD-BYQz4Ej0.js → diagram-5BDNPKRD-L4erno22.js} +1 -1
  43. package/dist/dashboard/assets/{diagram-G4DWMVQ6-Bi69CX9y.js → diagram-G4DWMVQ6-Dkmwa2-B.js} +1 -1
  44. package/dist/dashboard/assets/{diagram-MMDJMWI5-C9rH4USL.js → diagram-MMDJMWI5-v2PnfbZO.js} +1 -1
  45. package/dist/dashboard/assets/{diagram-TYMM5635-CtGy_47Q.js → diagram-TYMM5635-1ZO4tNjc.js} +1 -1
  46. package/dist/dashboard/assets/{el-GR-BZB4AONW-DLKTMVgu.js → el-GR-BZB4AONW-PXD8ArOs.js} +1 -1
  47. package/dist/dashboard/assets/{erDiagram-SMLLAGMA-DYwZiwiF.js → erDiagram-SMLLAGMA-B8gV0kGN.js} +1 -1
  48. package/dist/dashboard/assets/{es-ES-U4NZUMDT-B8vUZNzj.js → es-ES-U4NZUMDT-CbfskKe2.js} +1 -1
  49. package/dist/dashboard/assets/{eu-ES-A7QVB2H4-DOBpB_xM.js → eu-ES-A7QVB2H4-iqrfeyOI.js} +1 -1
  50. package/dist/dashboard/assets/{fa-IR-HGAKTJCU-BSo8AkJw.js → fa-IR-HGAKTJCU-PzU2OiC1.js} +1 -1
  51. package/dist/dashboard/assets/{fi-FI-Z5N7JZ37-IXouAsQf.js → fi-FI-Z5N7JZ37-CbFLdQ9f.js} +1 -1
  52. package/dist/dashboard/assets/{flowDiagram-DWJPFMVM-BfsdXM27.js → flowDiagram-DWJPFMVM-CKx3j3bI.js} +1 -1
  53. package/dist/dashboard/assets/{fr-FR-RHASNOE6-Bq41-iWY.js → fr-FR-RHASNOE6-BuF2MviY.js} +1 -1
  54. package/dist/dashboard/assets/{ganttDiagram-T4ZO3ILL-YFxSZdBo.js → ganttDiagram-T4ZO3ILL-CAbTd_KO.js} +1 -1
  55. package/dist/dashboard/assets/{gitGraphDiagram-UUTBAWPF-CzLILfBF.js → gitGraphDiagram-UUTBAWPF-Di7htCsA.js} +1 -1
  56. package/dist/dashboard/assets/{gl-ES-HMX3MZ6V-kuzaTFsC.js → gl-ES-HMX3MZ6V-BqePaODm.js} +1 -1
  57. package/dist/dashboard/assets/{graph-BOf2JBnY.js → graph-CC7SYwBm.js} +1 -1
  58. package/dist/dashboard/assets/{he-IL-6SHJWFNN-RQ2NxAvm.js → he-IL-6SHJWFNN-w9d4AvyL.js} +1 -1
  59. package/dist/dashboard/assets/{hi-IN-IWLTKZ5I-D9xTaaPK.js → hi-IN-IWLTKZ5I-BwHo17vR.js} +1 -1
  60. package/dist/dashboard/assets/{hu-HU-A5ZG7DT2-L1HNbSdl.js → hu-HU-A5ZG7DT2-Dm80xRge.js} +1 -1
  61. package/dist/dashboard/assets/{id-ID-SAP4L64H-B33X72Ml.js → id-ID-SAP4L64H-RpPM18i0.js} +1 -1
  62. package/dist/dashboard/assets/index-CkxvR2LH.css +32 -0
  63. package/dist/dashboard/assets/index-DalMBSek.js +511 -0
  64. package/dist/dashboard/assets/{index-B-6R-7Ot.js → index-sA_NxF_Y.js} +1 -1
  65. package/dist/dashboard/assets/{infoDiagram-42DDH7IO-DTWl9Wxm.js → infoDiagram-42DDH7IO-z-1I4Qyj.js} +1 -1
  66. package/dist/dashboard/assets/{ishikawaDiagram-UXIWVN3A-C0CAoNgB.js → ishikawaDiagram-UXIWVN3A-DWGoVsQO.js} +1 -1
  67. package/dist/dashboard/assets/{it-IT-JPQ66NNP-BK_HSf6M.js → it-IT-JPQ66NNP-DhOvhKWm.js} +1 -1
  68. package/dist/dashboard/assets/{ja-JP-DBVTYXUO-BRVQwLJ-.js → ja-JP-DBVTYXUO-CEZ1K0xn.js} +1 -1
  69. package/dist/dashboard/assets/{journeyDiagram-VCZTEJTY-CADHnY3u.js → journeyDiagram-VCZTEJTY-Dqh8uQwc.js} +1 -1
  70. package/dist/dashboard/assets/{kaa-6HZHGXH3-CYGgpUSe.js → kaa-6HZHGXH3-DuFoQ8nX.js} +1 -1
  71. package/dist/dashboard/assets/{kab-KAB-ZGHBKWFO-LMEkvoCj.js → kab-KAB-ZGHBKWFO-BkQCP_3o.js} +1 -1
  72. package/dist/dashboard/assets/{kanban-definition-6JOO6SKY-D1ijOIDj.js → kanban-definition-6JOO6SKY-DCs65HUc.js} +1 -1
  73. package/dist/dashboard/assets/{kk-KZ-P5N5QNE5-JIbh9_iv.js → kk-KZ-P5N5QNE5-Dh9BXA8_.js} +1 -1
  74. package/dist/dashboard/assets/{km-KH-HSX4SM5Z-CrWx5kgt.js → km-KH-HSX4SM5Z-D1S2EcQ_.js} +1 -1
  75. package/dist/dashboard/assets/{ko-KR-MTYHY66A-BQFjXlBC.js → ko-KR-MTYHY66A-DsI3vrEx.js} +1 -1
  76. package/dist/dashboard/assets/{ku-TR-6OUDTVRD-B6u43c-f.js → ku-TR-6OUDTVRD-CyjK_3XH.js} +1 -1
  77. package/dist/dashboard/assets/{layout-CVIUlzFz.js → layout-B_y9BOh6.js} +1 -1
  78. package/dist/dashboard/assets/{linear-TmytBUBH.js → linear-BdGSdQOc.js} +1 -1
  79. package/dist/dashboard/assets/{lt-LT-XHIRWOB4-b8AKF48V.js → lt-LT-XHIRWOB4-C6tJka1D.js} +1 -1
  80. package/dist/dashboard/assets/{lv-LV-5QDEKY6T-CRt75m7a.js → lv-LV-5QDEKY6T-C_xyL_ee.js} +1 -1
  81. package/dist/dashboard/assets/{min-CMOJsVTV.js → min-U_KGhNEt.js} +1 -1
  82. package/dist/dashboard/assets/{mindmap-definition-QFDTVHPH-DkzlAkBW.js → mindmap-definition-QFDTVHPH-sXmpEpKw.js} +1 -1
  83. package/dist/dashboard/assets/{mr-IN-CRQNXWMA-Bw3MaxMe.js → mr-IN-CRQNXWMA-C0TwGtAU.js} +1 -1
  84. package/dist/dashboard/assets/{my-MM-5M5IBNSE-Dw_tWZWl.js → my-MM-5M5IBNSE-RlAQCiXW.js} +1 -1
  85. package/dist/dashboard/assets/{nb-NO-T6EIAALU-Ci_2058Y.js → nb-NO-T6EIAALU-C1JHqr1t.js} +1 -1
  86. package/dist/dashboard/assets/{nl-NL-IS3SIHDZ-DYyqePg6.js → nl-NL-IS3SIHDZ-B0BVD_2p.js} +1 -1
  87. package/dist/dashboard/assets/{nn-NO-6E72VCQL-NdjykAxN.js → nn-NO-6E72VCQL-zQTko94P.js} +1 -1
  88. package/dist/dashboard/assets/{oc-FR-POXYY2M6-DPEyc6hx.js → oc-FR-POXYY2M6-CBZRg6ku.js} +1 -1
  89. package/dist/dashboard/assets/{pa-IN-N4M65BXN-D_4V8i72.js → pa-IN-N4M65BXN-7s6qpn6h.js} +1 -1
  90. package/dist/dashboard/assets/{percentages-BXMCSKIN-4Hgy5SgQ.js → percentages-BXMCSKIN-DckRvLSG.js} +7 -7
  91. package/dist/dashboard/assets/{pica-D6rAZEqu.js → pica-ByOjSUxE.js} +1 -1
  92. package/dist/dashboard/assets/{pieDiagram-DEJITSTG-BqsalDEv.js → pieDiagram-DEJITSTG-BJJsFOH9.js} +1 -1
  93. package/dist/dashboard/assets/{pl-PL-T2D74RX3-CArl0OSe.js → pl-PL-T2D74RX3-QTzNyi10.js} +1 -1
  94. package/dist/dashboard/assets/{pt-BR-5N22H2LF-BlA9u7zY.js → pt-BR-5N22H2LF-tntUlote.js} +1 -1
  95. package/dist/dashboard/assets/{pt-PT-UZXXM6DQ-ByCcYKU2.js → pt-PT-UZXXM6DQ-B9M5aHl1.js} +1 -1
  96. package/dist/dashboard/assets/{quadrantDiagram-34T5L4WZ-BtnExwNB.js → quadrantDiagram-34T5L4WZ-BearsU1w.js} +1 -1
  97. package/dist/dashboard/assets/{requirementDiagram-MS252O5E-jDWYOtpC.js → requirementDiagram-MS252O5E-BixosHER.js} +1 -1
  98. package/dist/dashboard/assets/{ro-RO-JPDTUUEW-CafHxA38.js → ro-RO-JPDTUUEW-qbgXrb57.js} +1 -1
  99. package/dist/dashboard/assets/{ru-RU-B4JR7IUQ-BbJGwp-Z.js → ru-RU-B4JR7IUQ-CgX4A1Ib.js} +1 -1
  100. package/dist/dashboard/assets/{sankeyDiagram-XADWPNL6-BSU4w3_8.js → sankeyDiagram-XADWPNL6-DMSd55sU.js} +1 -1
  101. package/dist/dashboard/assets/{sequenceDiagram-FGHM5R23-DBHLScxa.js → sequenceDiagram-FGHM5R23-D2-Uwrtb.js} +1 -1
  102. package/dist/dashboard/assets/{si-LK-N5RQ5JYF-eDB0s9B0.js → si-LK-N5RQ5JYF-CWEkBvJB.js} +1 -1
  103. package/dist/dashboard/assets/{sk-SK-C5VTKIMK-qaYN0zkE.js → sk-SK-C5VTKIMK-CG8moyh9.js} +1 -1
  104. package/dist/dashboard/assets/{sl-SI-NN7IZMDC-BQ4pcyGF.js → sl-SI-NN7IZMDC-CsBFEfKt.js} +1 -1
  105. package/dist/dashboard/assets/{stateDiagram-FHFEXIEX-DqLkK6dC.js → stateDiagram-FHFEXIEX-vG3jGBpo.js} +1 -1
  106. package/dist/dashboard/assets/stateDiagram-v2-QKLJ7IA2-V8xCsZlI.js +1 -0
  107. package/dist/dashboard/assets/{subset-shared.chunk-Hm_CkSQB.js → subset-shared.chunk-Bin8VoC6.js} +1 -1
  108. package/dist/dashboard/assets/{subset-worker.chunk-YczznLkp.js → subset-worker.chunk-mRsEjVpS.js} +1 -1
  109. package/dist/dashboard/assets/{sv-SE-XGPEYMSR-DD7_8pqE.js → sv-SE-XGPEYMSR-BMD7GbQV.js} +1 -1
  110. package/dist/dashboard/assets/{ta-IN-2NMHFXQM-Dd-RFvWz.js → ta-IN-2NMHFXQM-CjZ7GPuA.js} +1 -1
  111. package/dist/dashboard/assets/{th-TH-HPSO5L25-C7x2tZrZ.js → th-TH-HPSO5L25-BTE57yWh.js} +1 -1
  112. package/dist/dashboard/assets/{timeline-definition-GMOUNBTQ-DILCBtIX.js → timeline-definition-GMOUNBTQ-Bz7MuXCz.js} +1 -1
  113. package/dist/dashboard/assets/{tr-TR-DEFEU3FU-BXDs1FJQ.js → tr-TR-DEFEU3FU-CKY-uY5_.js} +1 -1
  114. package/dist/dashboard/assets/{uk-UA-QMV73CPH-Bp-yEHOn.js → uk-UA-QMV73CPH-BuopbANh.js} +1 -1
  115. package/dist/dashboard/assets/{vennDiagram-DHZGUBPP-CnQH3XJu.js → vennDiagram-DHZGUBPP-TsesBmH7.js} +1 -1
  116. package/dist/dashboard/assets/{vi-VN-M7AON7JQ-DQOsitU7.js → vi-VN-M7AON7JQ-CLrvMDM6.js} +1 -1
  117. package/dist/dashboard/assets/{wardley-RL74JXVD-FdX4RiE_.js → wardley-RL74JXVD-DlttwW9K.js} +1 -1
  118. package/dist/dashboard/assets/{wardleyDiagram-NUSXRM2D-CK4rbC5W.js → wardleyDiagram-NUSXRM2D-BfZXChhy.js} +1 -1
  119. package/dist/dashboard/assets/{xychartDiagram-5P7HB3ND-B6UGr9Rl.js → xychartDiagram-5P7HB3ND-jjeDn8EJ.js} +1 -1
  120. package/dist/dashboard/assets/{zh-CN-LNUGB5OW-BMYbdc2b.js → zh-CN-LNUGB5OW-C1MYPzyx.js} +1 -1
  121. package/dist/dashboard/assets/{zh-HK-E62DVLB3-CCcVgqN7.js → zh-HK-E62DVLB3-BriaHjNV.js} +1 -1
  122. package/dist/dashboard/assets/{zh-TW-RAJ6MFWO-C52SJlwU.js → zh-TW-RAJ6MFWO-BUDimtjA.js} +1 -1
  123. package/dist/dashboard/index.html +3 -3
  124. package/dist/index.js +2586 -1065
  125. package/dist/skill-packs/excalidraw/SKILL.md +19 -1
  126. package/dist/skill-packs/excalidraw/scripts/build_excalidraw.js +47 -8
  127. package/dist/skill-packs/excalidraw/scripts/lib/style.js +111 -21
  128. package/dist/skill-packs/goal-skill/SKILL.md +6 -0
  129. package/package.json +1 -1
  130. package/skill/SKILL.md +8 -3
  131. package/skill/references/cli-reference.md +20 -2
  132. package/skill/references/knowledge-and-recall.md +1 -0
  133. package/skill/references/sleep.md +6 -5
  134. package/skill/references/tasks-and-features.md +37 -0
  135. package/skill-packs/excalidraw/SKILL.md +19 -1
  136. package/skill-packs/excalidraw/scripts/build_excalidraw.js +47 -8
  137. package/skill-packs/excalidraw/scripts/lib/style.js +111 -21
  138. package/skill-packs/goal-skill/SKILL.md +6 -0
  139. package/dist/dashboard/assets/channel-C5uBjxRg.js +0 -1
  140. package/dist/dashboard/assets/classDiagram-6PBFFD2Q-MwUwKREr.js +0 -1
  141. package/dist/dashboard/assets/classDiagram-v2-HSJHXN6E-MwUwKREr.js +0 -1
  142. package/dist/dashboard/assets/clone-CcQlDA5C.js +0 -1
  143. package/dist/dashboard/assets/index--chDvBkZ.css +0 -32
  144. package/dist/dashboard/assets/index-CwZ3YweD.js +0 -582
  145. package/dist/dashboard/assets/stateDiagram-v2-QKLJ7IA2-TpuacwBg.js +0 -1
@@ -161,9 +161,17 @@ Match the team's look by default. The builder now defaults text to **Excalifont
161
161
  Each is `{ fill, stroke }`. `INK` = `#1e1e1e` (the default 2px card outline). Fills are **solid**,
162
162
  rects are **always rounded**.
163
163
  - **`card({x,y,w,h,text,color,fontSize})`** → rounded filled rect + centered label, grouped (move as one).
164
+ The label **cannot overflow**: it is hard-wrapped to the card width and the font auto-shrinks (down to
165
+ `minFont`, default 9px) until it also fits the height. Optional `{ minFont, padX, padY }`. If even the
166
+ floor font won't fit, it still draws but logs a `[excalidraw]` warning — that's your cue to enlarge the
167
+ card or shorten the text.
164
168
  - **`node(...)`** → smaller card for flow steps.
165
169
  - **`sectionTitle({x,y,text,fontSize})`** → big plain header (no box), like "Configuration" / "Flow".
166
- - **`connector({from,to,label,double,dashed})`** → labeled arrow between two points.
170
+ - **`connector({from,to,label,double,dashed,via,elbow})`** → labeled arrow between two points. Route it
171
+ AROUND intervening boxes instead of through them: `elbow:'hv'` (horizontal then vertical) / `elbow:'vh'`
172
+ (vertical then horizontal) bends it once at a right angle, or `via:[[x,y],…]` threads explicit waypoints.
173
+ - **`fitText({text,w,h,fontSize,minFont})`** → the wrap+shrink primitive `card()` uses; call it directly to
174
+ pre-size a box to its label (returns `{ text, fontSize, lineCount, fits }`).
167
175
  - **`annotate({from,to,text})`** → short arrow to a side-spec note (the "90sec / deepseek…" pattern).
168
176
  - **`column({x,y,items,color})`** → vertical stack of cards (benefit/risk lists). `items` may be
169
177
  strings or `{text,color}`.
@@ -175,6 +183,16 @@ Compose these into a `buildExcalidraw({ out, elements })` call. Runnable example
175
183
  Conventions: color nodes by ROLE, label every arrow, put specs in side-annotations, use big plain
176
184
  section titles to break the board into "slides", keep one idea per card.
177
185
 
186
+ ### Layout rules that keep boards clean
187
+ - **Size boxes for their text.** A card guarantees the label stays *inside*, but it does so by shrinking
188
+ the font — a long label in a small box ends up tiny. Give wide/long labels a wider `w` (or more `h`),
189
+ or split the idea across two cards. Watch for `[excalidraw]` fit warnings on the console.
190
+ - **Fan out through a margin bus, never as a diagonal star.** A hub wired straight to a vertical stack of
191
+ boxes draws diagonals that cut across the boxes in between. Instead, route the trunk into an empty
192
+ margin beside the stack, run one vertical "bus" line down that margin, then a short horizontal stub from
193
+ the bus into each box. With `connector()`: send the trunk `elbow:'hv'` into the bus x, then one
194
+ `elbow:'hv'` stub per branch (or `via:[[busX, branchY]]`). No arrow crosses a box it isn't pointing at.
195
+
178
196
  ## Rules & gotchas
179
197
  - **Unique image filenames.** Obsidian resolves `[[name.png]]` by basename; if two different files
180
198
  share a name anywhere in the vault the link is ambiguous. Name screenshots uniquely
@@ -55,30 +55,44 @@ function findVaultRoot(startDir) {
55
55
  }
56
56
 
57
57
  // ---------- text sizing ----------
58
+ // Excalifont glyph advance (multiples of fontSize): latin ~0.58, emoji/arrows/CJK/combining ~1.05.
59
+ // A flat factor under-counted wide glyphs, so emoji labels measured narrow and overflowed their box.
60
+ // This mirrors glyphW() in scripts/lib/style.js so card() pre-wrapping and on-board height agree.
61
+ function isWideGlyph(code) {
62
+ return (
63
+ (code >= 0x1100 && code <= 0x115f) || (code >= 0x2190 && code <= 0x21ff) ||
64
+ (code >= 0x2300 && code <= 0x23ff) || (code >= 0x25a0 && code <= 0x27bf) ||
65
+ (code >= 0x2b00 && code <= 0x2bff) || (code >= 0x2e80 && code <= 0x9fff) ||
66
+ (code >= 0xac00 && code <= 0xd7a3) || (code >= 0xf900 && code <= 0xfaff) ||
67
+ (code >= 0xfe30 && code <= 0xfe4f) || (code >= 0xff00 && code <= 0xff60) ||
68
+ (code >= 0x1f000 && code <= 0x1faff) || (code >= 0x1f1e6 && code <= 0x1f1ff)
69
+ );
70
+ }
71
+ function glyphW(ch, fontSize) { return fontSize * (isWideGlyph(ch.codePointAt(0)) ? 1.05 : 0.58); }
72
+ function measureText(s, fontSize) { let w = 0; for (const ch of String(s)) w += glyphW(ch, fontSize); return w; }
73
+
58
74
  // When a fixedWidth is given the text element is treated as WRAPPING (autoResize off downstream),
59
75
  // so height must reflect word-wrapped line count — otherwise long captions render as one wide line
60
76
  // and overlap their neighbours. Honors explicit \n and greedily wraps each logical line to width.
61
77
  function sizeText(text, fontSize, fixedWidth) {
62
78
  const rawLines = String(text).split('\n');
63
79
  if (fixedWidth != null) {
64
- const charW = fontSize * 0.52; // approx Excalifont glyph advance
65
- const perLine = Math.max(1, Math.floor(fixedWidth / charW));
66
80
  let totalLines = 0;
67
81
  for (const ln of rawLines) {
68
82
  const words = ln.split(/\s+/).filter(Boolean);
69
83
  if (!words.length) { totalLines += 1; continue; }
70
- let cur = 0, lc = 1;
84
+ let cur = '', lc = 1;
71
85
  for (const w of words) {
72
- const add = (cur ? 1 : 0) + w.length;
73
- if (cur + add > perLine && cur > 0) { lc++; cur = w.length; }
74
- else cur += add;
86
+ if (!cur) { cur = w; continue; }
87
+ if (measureText(cur + ' ' + w, fontSize) > fixedWidth) { lc++; cur = w; }
88
+ else cur += ' ' + w;
75
89
  }
76
90
  totalLines += lc;
77
91
  }
78
92
  return { width: fixedWidth, height: Math.max(Math.round(fontSize * 1.25), Math.round(totalLines * fontSize * 1.25)) };
79
93
  }
80
- const maxLen = rawLines.reduce((m, l) => Math.max(m, l.length), 0);
81
- return { width: Math.max(10, Math.round(maxLen * fontSize * 0.6)), height: Math.round(rawLines.length * fontSize * 1.25) };
94
+ const maxW = rawLines.reduce((m, l) => Math.max(m, measureText(l, fontSize)), 0);
95
+ return { width: Math.max(10, Math.round(maxW)), height: Math.round(rawLines.length * fontSize * 1.25) };
82
96
  }
83
97
 
84
98
  // ---------- element factory ----------
@@ -119,6 +133,31 @@ function buildExcalidraw(spec) {
119
133
  const wikilinkMode = spec.wikilinkMode || 'basename';
120
134
  const background = spec.background || '#ffffff';
121
135
 
136
+ // Pre-flight: every referenced image MUST exist on disk before we write the
137
+ // board. A missing asset would otherwise be emitted as a dangling `## Embedded
138
+ // Files` wikilink (`<sha1>: [[missing.png]]`) that can never resolve in any
139
+ // renderer — the board ships with blank images. Collect ALL missing paths and
140
+ // fail once with the full list (and the board path), so the author fixes every
141
+ // gap in a single pass instead of discovering them one throw at a time.
142
+ const missingImages = [];
143
+ for (const e of (spec.elements || [])) {
144
+ if (e && e.type === 'image' && e.path) {
145
+ // A missing file OR a non-file (directory / broken symlink) is unusable —
146
+ // statSync throws for a broken link / absent path, so default to missing.
147
+ let isFile = false;
148
+ try { isFile = fs.statSync(path.resolve(e.path)).isFile(); } catch { isFile = false; }
149
+ if (!isFile) missingImages.push(e.path);
150
+ }
151
+ }
152
+ if (missingImages.length) {
153
+ throw new Error(
154
+ `build_excalidraw: ${missingImages.length} referenced image(s) missing or not a file — ` +
155
+ `refusing to write a board with dangling embeds:\n ` +
156
+ missingImages.join('\n ') +
157
+ `\n(board: ${outPath})`,
158
+ );
159
+ }
160
+
122
161
  const rng = mulberry32(strHash(path.resolve(outPath)));
123
162
  const elements = [];
124
163
  const textEntries = []; // {text, id}
@@ -23,30 +23,112 @@ function pal(c) { return PALETTE[c] || { fill: c, stroke: INK }; }
23
23
  let _gc = 0;
24
24
  const newGroup = (hint) => `g${++_gc}-${hint || ''}`;
25
25
  const lines = (t) => String(t).split('\n').length;
26
- // word-wrapped line count at a given width — keeps card labels vertically centered when they wrap
27
- function wrapLineCount(text, fontSize, width) {
28
- const perLine = Math.max(1, Math.floor(width / (fontSize * 0.52)));
26
+
27
+ // --- text fitting --------------------------------------------------------
28
+ // Excalifont glyph advance, in multiples of fontSize. Latin is ~0.58; emoji, arrows, CJK and
29
+ // combining marks render roughly square (~1.05) — the old flat 0.52 under-counted them, so wide
30
+ // labels measured narrow and spilled out of their card. Mirrored in build_excalidraw.js sizeText().
31
+ function isWideGlyph(code) {
32
+ return (
33
+ (code >= 0x1100 && code <= 0x115f) || // Hangul Jamo
34
+ (code >= 0x2190 && code <= 0x21ff) || // arrows
35
+ (code >= 0x2300 && code <= 0x23ff) || // misc technical (⏰ ⏳ …)
36
+ (code >= 0x25a0 && code <= 0x27bf) || // geometric shapes, misc symbols, dingbats (✅ ★ …)
37
+ (code >= 0x2b00 && code <= 0x2bff) || // extra arrows / symbols
38
+ (code >= 0x2e80 && code <= 0x9fff) || // CJK radicals … unified ideographs
39
+ (code >= 0xac00 && code <= 0xd7a3) || // Hangul syllables
40
+ (code >= 0xf900 && code <= 0xfaff) || // CJK compatibility ideographs
41
+ (code >= 0xfe30 && code <= 0xfe4f) || // CJK compatibility forms
42
+ (code >= 0xff00 && code <= 0xff60) || // fullwidth forms
43
+ (code >= 0x1f000 && code <= 0x1faff) || // emoji & pictographs
44
+ (code >= 0x1f1e6 && code <= 0x1f1ff) // regional indicators (flags)
45
+ );
46
+ }
47
+ function glyphW(ch, fontSize) { return fontSize * (isWideGlyph(ch.codePointAt(0)) ? 1.05 : 0.58); }
48
+ function measureText(s, fontSize) { let w = 0; for (const ch of String(s)) w += glyphW(ch, fontSize); return w; }
49
+
50
+ // Greedy word-wrap to a pixel width, honoring existing \n and HARD-breaking any single word that is
51
+ // itself wider than the box (e.g. a long URL). Returns the text WITH the newlines baked in — the
52
+ // Obsidian plugin keeps them instead of re-flowing the label into one over-wide centered line.
53
+ function wrapToWidth(text, fontSize, width) {
54
+ const out = [];
55
+ for (const logical of String(text).split('\n')) {
56
+ const words = logical.split(/\s+/).filter(Boolean);
57
+ if (!words.length) { out.push(''); continue; }
58
+ let cur = '';
59
+ for (let word of words) {
60
+ while (measureText(word, fontSize) > width) {
61
+ let acc = '';
62
+ for (const ch of word) {
63
+ if (acc && measureText(acc + ch, fontSize) > width) break;
64
+ acc += ch;
65
+ }
66
+ if (cur) { out.push(cur); cur = ''; }
67
+ out.push(acc);
68
+ word = word.slice(acc.length);
69
+ if (!word) break;
70
+ }
71
+ if (!word) continue;
72
+ const candidate = cur ? cur + ' ' + word : word;
73
+ if (cur && measureText(candidate, fontSize) > width) { out.push(cur); cur = word; }
74
+ else cur = candidate;
75
+ }
76
+ if (cur) out.push(cur);
77
+ }
78
+ return out.join('\n');
79
+ }
80
+
81
+ const _warned = new Set();
82
+ function warnFit(msg) { if (!_warned.has(msg)) { _warned.add(msg); try { console.warn('[excalidraw] ' + msg); } catch (e) {} } }
83
+
84
+ // Wrap `text` to the card's inner width, then shrink the font (down to minFont) until the wrapped
85
+ // label also fits the card height. Returns { text (with baked-in newlines), fontSize, lineCount, fits }.
86
+ function fitText(o) {
87
+ const { text = '', w = 260, h = 92, fontSize = 20, minFont = 9, lineHeight = 1.25, padX = 10, padY = 8 } = o;
88
+ const innerW = Math.max(1, w - 2 * padX);
89
+ const innerH = Math.max(1, h - 2 * padY);
90
+ let fs = Math.max(minFont, Math.round(fontSize));
91
+ let wrapped = wrapToWidth(text, fs, innerW);
92
+ let lineCount = wrapped.split('\n').length;
93
+ while (fs > minFont && lineCount * fs * lineHeight > innerH) {
94
+ fs -= 1;
95
+ wrapped = wrapToWidth(text, fs, innerW);
96
+ lineCount = wrapped.split('\n').length;
97
+ }
98
+ return { text: wrapped, fontSize: fs, lineCount, fits: lineCount * fs * lineHeight <= innerH };
99
+ }
100
+
101
+ // arc-length midpoint of a polyline — keeps a connector label on the line even when it elbows
102
+ function polyMid(pts) {
103
+ if (!pts || pts.length < 2) return (pts && pts[0]) || [0, 0];
104
+ const seg = [];
29
105
  let total = 0;
30
- for (const ln of String(text).split('\n')) {
31
- const words = ln.split(/\s+/).filter(Boolean);
32
- if (!words.length) { total += 1; continue; }
33
- let cur = 0, lc = 1;
34
- for (const w of words) { const add = (cur ? 1 : 0) + w.length; if (cur + add > perLine && cur > 0) { lc++; cur = w.length; } else cur += add; }
35
- total += lc;
106
+ for (let i = 1; i < pts.length; i++) { const l = Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]); seg.push(l); total += l; }
107
+ let half = total / 2;
108
+ for (let i = 1; i < pts.length; i++) {
109
+ if (half <= seg[i - 1] || i === pts.length - 1) {
110
+ const t = seg[i - 1] ? half / seg[i - 1] : 0;
111
+ return [pts[i - 1][0] + (pts[i][0] - pts[i - 1][0]) * t, pts[i - 1][1] + (pts[i][1] - pts[i - 1][1]) * t];
112
+ }
113
+ half -= seg[i - 1];
36
114
  }
37
- return total;
115
+ return pts[0];
38
116
  }
39
117
 
40
- // rounded card: filled rect + centered label, grouped so they move together
118
+ // rounded card: filled rect + centered label, grouped so they move together. The label is
119
+ // hard-wrapped to the card width and the font auto-shrinks (to `minFont`) until it also fits the
120
+ // height, so text can never spill outside the box. Warns if even the floor font won't fit.
41
121
  function card(o) {
42
- const { x, y, w = 260, h = 92, text = '', color = 'blue', fontSize = 20, stroke = INK, strokeWidth = 2, textColor = INK, roundness = true } = o;
122
+ const { x, y, w = 260, h = 92, text = '', color = 'blue', fontSize = 20, stroke = INK, strokeWidth = 2, textColor = INK, roundness = true, minFont = 9, padX = 10, padY = 8 } = o;
43
123
  const group = o.group || newGroup('card');
44
124
  const p = pal(color);
45
- const textH = wrapLineCount(text, fontSize, w) * fontSize * 1.25;
46
- const ty = y + Math.max(6, (h - textH) / 2);
125
+ const fit = fitText({ text, w, h, fontSize, minFont, padX, padY });
126
+ if (!fit.fits) warnFit(`label ${JSON.stringify(String(text).slice(0, 32))} won't fit a ${w}×${h} card even at ${minFont}px — enlarge the card or shorten the text.`);
127
+ const textH = fit.lineCount * fit.fontSize * 1.25;
128
+ const ty = y + Math.max(padY, (h - textH) / 2);
47
129
  return [
48
130
  { type: 'rectangle', x, y, width: w, height: h, strokeColor: stroke, backgroundColor: p.fill, fillStyle: 'solid', strokeWidth, roundness, group },
49
- { type: 'text', x, y: ty, text, fontSize, color: textColor, width: w, align: 'center', fontFamily: 5, group },
131
+ { type: 'text', x, y: ty, text: fit.text, fontSize: fit.fontSize, color: textColor, width: w, align: 'center', fontFamily: 5, group },
50
132
  ];
51
133
  }
52
134
  // small node (flow step) — card with tighter defaults
@@ -58,13 +140,21 @@ function sectionTitle(o) {
58
140
  return [{ type: 'text', x, y, text, fontSize, color, fontFamily: 5, width: Math.max(200, String(text).length * fontSize * 0.6) }];
59
141
  }
60
142
 
61
- // labeled connector arrow between two points (centers of cards, etc.)
143
+ // labeled connector arrow between two points (centers of cards, etc.).
144
+ // Routing: pass `via:[[x,y],…]` for explicit waypoints, or `elbow:'hv'|'vh'` for an auto right-angle
145
+ // bend ('hv' = horizontal then vertical, 'vh' = vertical then horizontal). Use these to route a link
146
+ // AROUND intervening boxes instead of cutting a diagonal straight through them.
62
147
  function connector(o) {
63
- const { from, to, label, double = false, strokeColor = INK, strokeWidth = 2, dashed = false, labelColor = '#495057', fontSize = 16 } = o;
64
- const out = [{ type: 'arrow', points: [from, to], strokeColor, strokeWidth, strokeStyle: dashed ? 'dashed' : 'solid', endArrow: 'arrow', startArrow: double ? 'arrow' : null }];
148
+ const { from, to, label, double = false, strokeColor = INK, strokeWidth = 2, dashed = false, labelColor = '#495057', fontSize = 16, via = null, elbow = null } = o;
149
+ let pts;
150
+ if (via && via.length) pts = [from, ...via, to];
151
+ else if (elbow === 'hv') pts = [from, [to[0], from[1]], to];
152
+ else if (elbow === 'vh') pts = [from, [from[0], to[1]], to];
153
+ else pts = [from, to];
154
+ const out = [{ type: 'arrow', points: pts, strokeColor, strokeWidth, strokeStyle: dashed ? 'dashed' : 'solid', endArrow: 'arrow', startArrow: double ? 'arrow' : null }];
65
155
  if (label) {
66
- const mx = (from[0] + to[0]) / 2, my = (from[1] + to[1]) / 2;
67
- const w = Math.max(40, String(label).length * fontSize * 0.62);
156
+ const [mx, my] = polyMid(pts);
157
+ const w = Math.max(40, measureText(String(label), fontSize) + fontSize);
68
158
  out.push({ type: 'text', x: mx - w / 2, y: my - fontSize - 4, text: label, fontSize, color: labelColor, width: w, align: 'center', fontFamily: 5 });
69
159
  }
70
160
  return out;
@@ -127,4 +217,4 @@ const leftOf = (x, y, w, h) => [x, y + h / 2];
127
217
  const bottomOf = (x, y, w, h) => [x + w / 2, y + h];
128
218
  const topOf = (x, y, w, h) => [x + w / 2, y];
129
219
 
130
- module.exports = { INK, PALETTE, pal, card, node, sectionTitle, connector, annotate, column, hub, bullets, center, rightOf, leftOf, bottomOf, topOf };
220
+ module.exports = { INK, PALETTE, pal, card, node, sectionTitle, connector, annotate, column, hub, bullets, fitText, measureText, center, rightOf, leftOf, bottomOf, topOf };
@@ -122,6 +122,12 @@ dreamcontext tasks status <slug> in_progress "plan validated; implementing"
122
122
 
123
123
  If `<slug>` already exists, de-collide (append a short suffix) rather than clobbering.
124
124
 
125
+ If the project has **roadmap objectives** (`_dream_context/core/objectives/` non-empty — they appear
126
+ in your snapshot), link the task to the objective(s) this goal serves: `tasks create --objectives a,b`
127
+ (or `dreamcontext tasks objectives <slug> a,b` after). One goal often serves several objectives —
128
+ that's expected. If unsure which, ask the user in Phase 0; never leave an obviously-serving task
129
+ unlinked, and never overwrite an existing non-empty `objectives:` list.
130
+
125
131
  If the project declares **custom task fields** (`_dream_context/overrides/task.md`), `tasks create`
126
132
  hard-fails (exit 1) on an unset `required` field — set each with `--field key=value` on create. For
127
133
  any field marked `ask: true`, ask the user for the value back in Phase 0 (it's a human judgment) rather
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dreamcontext",
3
- "version": "0.10.2",
3
+ "version": "0.10.5",
4
4
  "description": "dreamcontext — the persistent brain for your AI agents. Remembers what you built, knows how your project works.",
5
5
  "type": "module",
6
6
  "bin": {
package/skill/SKILL.md CHANGED
@@ -76,6 +76,7 @@ dreamcontext is **more than memory files**. Every capability below is real and s
76
76
  |---|---|---|
77
77
  | **Structured memory** | soul/user/memory + knowledge + tasks, auto-loaded each session | this file |
78
78
  | **Tasks** | Working documents with changelog, RICE, status lifecycle, start/due date ranges, resolved assignees, and project-declared custom fields (`overrides/task.md`) | [tasks-and-features.md](references/tasks-and-features.md) |
79
+ | **Roadmap / Objectives** | PO-authored OKR board: objectives in `core/objectives/`, many-to-many task links (`objectives:` frontmatter), dependency DAG with full forecast cascade, target vs forecast slip detection, `dreamcontext roadmap` (+ `--json`) | [tasks-and-features.md](references/tasks-and-features.md) |
79
80
  | **Features (PRDs)** | Retrospective product docs, updated only during sleep | [tasks-and-features.md](references/tasks-and-features.md) |
80
81
  | **Knowledge** | Tagged deep docs, pinning, staleness, Excalidraw diagrams | [knowledge-and-recall.md](references/knowledge-and-recall.md) |
81
82
  | **Memory recall** | Haiku/BM25 search over the whole corpus; auto-injected on prompts | [knowledge-and-recall.md](references/knowledge-and-recall.md) |
@@ -105,7 +106,8 @@ The SessionStart hook injects this automatically every session — answer from i
105
106
 
106
107
  - **Soul, User, Memory** — full content (`core/0.soul.md`, `1.user.md`, `2.memory.md`)
107
108
  - **Extended core files index** — names/types of style guide, tech stack, system flow
108
- - **Active tasks** — status, priority, last updated (answer "which tasks are active?" from this)
109
+ - **Active tasks** — status, priority, last updated, and the objectives each serves (answer "which tasks are active?" from this)
110
+ - **Objectives (roadmap)** — active + recently-finished objectives with progress %, target vs forecast, and slip flags. **Weigh decisions against these outcomes** — they are WHAT the project is driving toward
109
111
  - **Bookmarks** — tagged important moments from prior sessions, by salience
110
112
  - **Contextual reminders** — triggers matching active tasks (prospective memory)
111
113
  - **Sleep state** — current debt level, sessions since last sleep, history
@@ -256,8 +258,9 @@ A ★★★ bookmark or 5+ sessions since last sleep also triggers an advisory.
256
258
  3. Build a brief inline (cheap CLI): read `state/.sleep.json`, `git status --short`, `git log` since last sleep, `dreamcontext core releases active`.
257
259
  4. Dispatch specialists **in parallel** (one message, multiple Agent calls): always `sleep-tasks` + `sleep-state`; fire `sleep-product` when knowledge/features/research signals warrant (over-fire — it no-ops cheaply); fire `sleep-migration` only if `dreamcontext migrations pending` has output.
258
260
  5. Wait for reports, then `dreamcontext reflect` (promote only genuinely load-bearing terms).
259
- 6. `dreamcontext sleep done "<one-paragraph summary>"` — clears pre-epoch state, resets debt.
260
- 7. Report the consolidated summary to the user.
261
+ 6. If `_dream_context/core/objectives/` is non-empty, run `dreamcontext roadmap` a cheap deterministic call that refreshes the auto-generated board (`knowledge/roadmap/board.md`) from the reconciled tasks. Surface any 🔴 SLIPPING objectives in your summary.
262
+ 7. `dreamcontext sleep done "<one-paragraph summary>"` clears pre-epoch state, resets debt.
263
+ 8. Report the consolidated summary to the user.
261
264
 
262
265
  For non-file-change work (decisions, architecture talk): `dreamcontext sleep add <score> "<reason>"`.
263
266
 
@@ -282,6 +285,8 @@ Status: `todo → in_progress → in_review → completed`. Sections: `why`, `us
282
285
 
283
286
  **Custom fields (if this project declares them).** When `_dream_context/overrides/task.md` exists, every task carries project-defined custom fields. Their **values are surfaced to you inline** — in the snapshot's Active Tasks block and in `dreamcontext tasks list --long` — so you can see them without opening the file; unset **required** fields show as `⚠ UNSET (required)`. When you create or reconcile a task, **set every declared field** (`dreamcontext tasks field <slug> <key> <value>` or `tasks create --field key=value`). **REQUIRED fields are mandatory — never create or complete a task with a required field left empty.** Fields marked **[ASK THE USER]** (`ask: true`) capture a human judgment (e.g. a time estimate) — **ask the user for the value when creating the task instead of guessing it.** The full schema + sync behavior → [tasks-and-features.md](references/tasks-and-features.md).
284
287
 
288
+ **Objectives (roadmap links).** When the project has objectives (`core/objectives/` non-empty — they're in your snapshot), every task should declare which it serves: `objectives: [slug-a, slug-b]` in frontmatter (many-to-many — one task often lifts several outcomes). Set at creation (`tasks create --objectives a,b`) or later (`dreamcontext tasks objectives <task> a,b`). **Propose objective links for tasks you create; never overwrite a non-empty `objectives:` list — an existing value is a PO decision that sticks.** The field is local-only (never synced to a cloud backend). Rollups, forecasts, and slip detection are computed — `dreamcontext roadmap` / `--json`. Full model → [tasks-and-features.md](references/tasks-and-features.md).
289
+
285
290
  **RICE, due dates, tags/people, the Workflow flowchart, versioning, and multi-product** → [tasks-and-features.md](references/tasks-and-features.md).
286
291
  **Syncing tasks to a cloud backend (ClickUp _or_ GitHub — one at a time)** → [integrations.md](references/integrations.md).
287
292
 
@@ -34,12 +34,13 @@ Every command and flag, grouped. All commands are prefixed with `dreamcontext`.
34
34
 
35
35
  | Command | Description |
36
36
  |---|---|
37
- | `tasks list` | List/filter/group tasks (excludes completed by default). Flags: `-s/--status`, `-a/--all`, `--tag <t>` (repeatable, AND), `--any-tag <t>` (repeatable, OR), `--version <id>`, `--priority <level>`, `--feature <slug>`, `-g/--group-by tag\|version\|priority\|status`, `--long`, `--tags`, `--json`. Filters compose (AND), case-insensitive. |
37
+ | `tasks list` | List/filter/group tasks (excludes completed by default). Flags: `-s/--status`, `-a/--all`, `--tag <t>` (repeatable, AND), `--any-tag <t>` (repeatable, OR), `--version <id>`, `--priority <level>`, `--feature <slug>`, `--objective <slug>`, `-g/--group-by tag\|version\|priority\|status`, `--long`, `--tags`, `--json`. Filters compose (AND), case-insensitive. |
38
38
  | `tasks tags` | Distinct task tags with counts. `-a/--all`, `--json`. |
39
- | `tasks create <name>` | Create a task. Flags: `-d/--description`, `-p/--priority critical\|high\|medium\|low`, `-u/--urgency …`, `-s/--status`, `-t/--tags <csv>`, `-w/--why`, `-v/--version`, `--person <name>`, `--reach <1-10>`, `--impact <1-5>`, `--confidence 25\|50\|75\|100`, `--effort <weeks>`, `--start YYYY-MM-DD`, `--due YYYY-MM-DD`, `--field <key=value>` (repeatable; sets declared custom fields), `--allow-missing-required` (create a draft even when a required custom field is unset). **Fails** if a required custom field is unset and `--allow-missing-required` is not given. |
39
+ | `tasks create <name>` | Create a task. Flags: `-d/--description`, `-p/--priority critical\|high\|medium\|low`, `-u/--urgency …`, `-s/--status`, `-t/--tags <csv>`, `-w/--why`, `-v/--version`, `--person <name>`, `--reach <1-10>`, `--impact <1-5>`, `--confidence 25\|50\|75\|100`, `--effort <weeks>`, `--start YYYY-MM-DD`, `--due YYYY-MM-DD`, `--objectives <csv>` (roadmap objective slugs this task serves; slugs must exist), `--field <key=value>` (repeatable; sets declared custom fields), `--allow-missing-required` (create a draft even when a required custom field is unset). **Fails** if a required custom field is unset and `--allow-missing-required` is not given. |
40
40
  | `tasks rice <name>` | Print or update RICE values. `--reach`/`--impact`/`--confidence`/`--effort`, `--clear`. |
41
41
  | `tasks start <name> <YYYY-MM-DD\|clear>` | Set or clear a planned start date (range start). Must be ≤ the due date; setting it removes the `backlog` tag. |
42
42
  | `tasks due <name> <YYYY-MM-DD\|clear>` | Set or clear a due/end date (range end). |
43
+ | `tasks objectives <name> [slugs\|clear]` | Print, set (comma-separated, validated against `core/objectives/`), or clear the roadmap objectives a task serves. LOCAL-ONLY — never synced to a cloud backend. |
43
44
  | `tasks tag <name> <tags...>` | Add (or `--remove`) tags. `person:<slug>` assigns a person. |
44
45
  | `tasks field <name> <key> [value\|clear]` | Set or clear a user-defined custom field declared in `overrides/task.md` (synced to ClickUp/GitHub). Validates select options + number types. |
45
46
  | `tasks insert <name> <section> <content...>` | Insert into a section: `why`, `user_stories`, `acceptance_criteria`, `constraints`, `technical_details`, `notes`, `changelog`. |
@@ -58,6 +59,23 @@ Sections for `tasks insert`: `why`, `user_stories`, `acceptance_criteria`, `cons
58
59
 
59
60
  ---
60
61
 
62
+ ## Roadmap (objectives — the OKR board)
63
+
64
+ | Command | Description |
65
+ |---|---|
66
+ | `roadmap` | Render the objective board (rollups, target vs forecast, slip flags) and regenerate `knowledge/roadmap/board.md`. `--json` emits the typed RoadmapModel instead (no writes). |
67
+ | `roadmap objective create <slug>` | Create `core/objectives/<slug>.md`. `--title <str>` (required), `--target YYYY-MM-DD`, `--depends-on <csv>`, `--feature <prd-slug>`, `--why <text>`. |
68
+ | `roadmap objective list` | All objectives with progress %, status, forecast. `--json`. |
69
+ | `roadmap objective show <slug>` | One objective: member tasks, direct dependents, and the transitive "if this slips, so do" set. `--json`. |
70
+ | `roadmap objective edit <slug>` | `--title`, `--target <date\|clear>`, `--feature <slug\|clear>`, `--status not_started\|active\|review\|done\|clear` (manual PO override; `clear` returns to computed). |
71
+ | `roadmap objective delete <slug>` | Delete; other objectives' `depends_on` are healed automatically. `--yes`. |
72
+ | `roadmap objective depend <A> <B>` | A depends on B — **rejected at write time** if it would create a circular dependency. |
73
+ | `roadmap objective undepend <A> <B>` | Remove the dependency edge. |
74
+
75
+ Task-side linkage: `tasks create --objectives a,b` · `tasks objectives <task> a,b|clear` · `tasks list --objective <slug>`. Objectives are recallable: `memory recall "<query>" --types objective`.
76
+
77
+ ---
78
+
61
79
  ## Features
62
80
 
63
81
  | Command | Description |
@@ -67,6 +67,7 @@ dreamcontext memory recall "how did we decide on the sleep fan-out"
67
67
  dreamcontext memory recall "auth flow" --types knowledge,feature
68
68
  dreamcontext memory recall "deprecated" --types changelog # ship history
69
69
  dreamcontext memory recall "rate limit" --types task # in-flight work
70
+ dreamcontext memory recall "retention goal" --types objective # roadmap objectives (OKR outcomes)
70
71
 
71
72
  # Tune result count / machine-readable output
72
73
  dreamcontext memory recall "rice prioritization" --top 3 --json
@@ -52,10 +52,11 @@ For non-file-change work (architecture discussion, a decision with no edits): `d
52
52
  - Pass each specialist a small text brief: epoch, session IDs, active task slugs, planning version, the signals relevant to it, optional user hint. Do **not** paste transcript content — specialists call `dreamcontext transcript distill <id>` themselves.
53
53
  5. **Wait for all reports** (each returns a short structured report).
54
54
  6. **`dreamcontext reflect`** — each candidate is a term seen across multiple sessions not yet in soul/user/memory/knowledge. Promote into `2.memory.md` or a knowledge file ONLY if genuinely load-bearing; most are noise — discard. Never auto-promote.
55
- 7. **Marketing pass** if `_dream_context/marketing/` exists: `dreamcontext mk rem-sleep`.
56
- 8. **Council promote check:** `dreamcontext council list --unpromoted` promote if the user engaged positively.
57
- 9. **`dreamcontext sleep done "<one-paragraph summary stitched from specialist reports>"`** clears pre-epoch state, resets debt, writes a history entry. (If a remote backend — ClickUp or GitHub — is active and any task pushes failed, this auto-retries once, then errors loudly with the failed slugs.)
58
- 10. **Report** the consolidated summary to the user.
55
+ 7. **Roadmap refresh** if `_dream_context/core/objectives/` is non-empty: run `dreamcontext roadmap` — a cheap deterministic call that recomputes rollups/forecasts from the reconciled tasks and rewrites the auto-generated `knowledge/roadmap/board.md`. Surface any 🔴 SLIPPING objectives (and objectives whose member tasks are now all complete — suggest the PO confirm them done) in your summary. Sleep never edits objective files themselves — they are PO-authored.
56
+ 8. **Marketing pass** if `_dream_context/marketing/` exists: `dreamcontext mk rem-sleep`.
57
+ 9. **Council promote check:** `dreamcontext council list --unpromoted`promote if the user engaged positively.
58
+ 10. **`dreamcontext sleep done "<one-paragraph summary stitched from specialist reports>"`** — clears pre-epoch state, resets debt, writes a history entry. (If a remote backend — ClickUp or GitHub — is active and any task pushes failed, this auto-retries once, then errors loudly with the failed slugs.)
59
+ 11. **Report** the consolidated summary to the user.
59
60
 
60
61
  ---
61
62
 
@@ -63,7 +64,7 @@ For non-file-change work (architecture discussion, a decision with no edits): `d
63
64
 
64
65
  | Specialist | Owns | Notes |
65
66
  |---|---|---|
66
- | `sleep-tasks` | Task files (`state/*.md`) | Reconciles task bodies to truth, bumps statuses, creates tasks for untracked work, attaches to the planning version, sets the start/due range, and keeps declared custom fields current (never fabricating `ask` fields in the no-user sleep context). |
67
+ | `sleep-tasks` | Task files (`state/*.md`) | Reconciles task bodies to truth, bumps statuses, creates tasks for untracked work, attaches to the planning version, sets the start/due range, and keeps declared custom fields current (never fabricating `ask` fields in the no-user sleep context). When objectives exist, proposes `objectives:` links for tasks with an EMPTY/absent list (multiple slugs when a task serves several outcomes) — **never overwrites a non-empty list** (that's a PO decision). |
67
68
  | `sleep-state` | Core identity (soul, user, memory, core 3–6), CHANGELOG, RELEASES | Records patterns/decisions/preferences, writes a changelog entry per meaningful change since the epoch, surfaces release readiness, enforces anti-bloat ceilings, flags stale knowledge for `sleep-product`. |
68
69
  | `sleep-product` | Knowledge files + feature PRDs | Creates/reconciles `knowledge/*.md` and `core/features/*.md`, processes staleness flags, maintains the knowledge index + taxonomy. |
69
70
  | `sleep-migration` | Structure only | Moves/renames folders, normalizes frontmatter, wraps fences. Never alters body prose. |
@@ -100,6 +100,42 @@ dreamcontext tasks tag <name> person:ada --remove # unassign
100
100
 
101
101
  ---
102
102
 
103
+ ## Objectives — the OKR roadmap (task ↔ objective, many-to-many)
104
+
105
+ Objectives are **PO-authored outcomes** ("increase retention 20%", "ship v0.2.3", "launch mobile") stored one file each in `_dream_context/core/objectives/<slug>.md` — first-class, durable, recallable (`memory recall --types objective`), independent of any task. The roadmap is NOT a derived shadow of tasks and NOT a list of releases: the PO owns the structure; computation is the assist layer (rollups, forecast, slip detection).
106
+
107
+ **The load-bearing relationship:** a task declares the objectives it serves via `objectives: [a, b]` frontmatter — **many-to-many** (one shipped task often lifts revenue AND retention). The reverse direction (objective → member tasks, objective → dependents) is always **computed**, never stored — so the two sides cannot drift.
108
+
109
+ ```bash
110
+ dreamcontext roadmap # text board + regenerate knowledge/roadmap/board.md
111
+ dreamcontext roadmap --json # the typed RoadmapModel (query surface; no writes)
112
+ dreamcontext roadmap objective create <slug> --title "..." [--target YYYY-MM-DD] [--depends-on a,b] [--feature <prd-slug>] [--why "..."]
113
+ dreamcontext roadmap objective list|show <slug> # show = members + dependents + "if this slips, so do: …"
114
+ dreamcontext roadmap objective edit <slug> [--title] [--target <date>|clear] [--status not_started|active|review|done|clear] [--feature <slug>|clear]
115
+ dreamcontext roadmap objective depend <A> <B> # A depends on B — REJECTED at write time if it would create a cycle
116
+ dreamcontext roadmap objective undepend <A> <B>
117
+ dreamcontext roadmap objective delete <slug> --yes # also heals other objectives' depends_on
118
+ dreamcontext tasks create <name> --objectives a,b # link at creation (slugs must exist)
119
+ dreamcontext tasks objectives <task> a,b|clear # set/clear on an existing task
120
+ dreamcontext tasks list --objective <slug> # all tasks serving an objective
121
+ ```
122
+
123
+ **The computed model** (per objective, from `roadmap --json`):
124
+ - **Progress** = completed ÷ total member tasks (each objective counts over its OWN member set — a shared task contributes to each independently).
125
+ - **Rollup status** (real enum): all `completed`→`done` 🟢 · any `in_progress`→`active` 🔵 · any `in_review`→`review` 🟡 · else `not_started` ⚪. A manual `--status` override wins (`status_source: override`).
126
+ - **Forecast cascade — full transitive DAG:** `forecast_start = max(earliest member start, max(forecast_end of dependencies))`; `forecast_end = max(latest member due, forecast_start)`. A slip anywhere propagates to ALL transitive dependents (diamond shapes included).
127
+ - **Null rule:** no dated member tasks → `forecast = null` ("unforecastable") and it imposes NO constraint on dependents.
128
+ - **Slipping** 🔴 = `forecast_end > target_date` (the PO's committed date). This is the live on-track signal — surfaced in the snapshot and the board.
129
+
130
+ **Rules for agents:**
131
+ 1. **Propose, never overwrite.** Suggest `objectives:` for tasks you create or find unlabeled; an existing non-empty list is a PO decision — never change it unless the user asks.
132
+ 2. **Local-only field.** `objectives` is never pushed/pulled by cloud sync backends. Do not try to map it to remote labels.
133
+ 3. **Objectives are orthogonal to versions/cycles.** `version` = WHEN (the time-box); `objectives` = WHAT outcomes it serves. Both live on the task independently.
134
+ 4. **`knowledge/roadmap/board.md` is auto-generated** — regenerate with `dreamcontext roadmap`, never hand-edit it. Objective files themselves are PO-authored prose — edit `## Why`/`## Notes` freely, but rollups/members are computed and don't belong in them.
135
+ 5. A feature PRD *may* back an objective via the objective's `feature:` field — a convenience link, not a requirement.
136
+
137
+ ---
138
+
103
139
  ## The Workflow flowchart (keep it in sync)
104
140
 
105
141
  Every task file has a `## Workflow` mermaid block near the top: one node per acceptance criterion, grouped under milestone subgraphs, with status classes `done` / `active` / `todo` / `blocked`. It is the load-bearing summary of the task — drift makes future sessions misread progress.
@@ -130,6 +166,7 @@ related_feature: null # feature slug for cross-link
130
166
  product: null # multi-product scoping (optional)
131
167
  start_date: null # YYYY-MM-DD or null — planned start (range start)
132
168
  due_date: null # YYYY-MM-DD or null — due / planned end (range end)
169
+ objectives: [] # roadmap objective slugs this task serves (many-to-many, LOCAL-ONLY — never synced)
133
170
  rice: { reach: 5, impact: 3, confidence: 75, effort: 2, score: 5.625 }
134
171
  custom_fields: {} # project-declared fields (only when overrides/task.md exists)
135
172
  ---
@@ -161,9 +161,17 @@ Match the team's look by default. The builder now defaults text to **Excalifont
161
161
  Each is `{ fill, stroke }`. `INK` = `#1e1e1e` (the default 2px card outline). Fills are **solid**,
162
162
  rects are **always rounded**.
163
163
  - **`card({x,y,w,h,text,color,fontSize})`** → rounded filled rect + centered label, grouped (move as one).
164
+ The label **cannot overflow**: it is hard-wrapped to the card width and the font auto-shrinks (down to
165
+ `minFont`, default 9px) until it also fits the height. Optional `{ minFont, padX, padY }`. If even the
166
+ floor font won't fit, it still draws but logs a `[excalidraw]` warning — that's your cue to enlarge the
167
+ card or shorten the text.
164
168
  - **`node(...)`** → smaller card for flow steps.
165
169
  - **`sectionTitle({x,y,text,fontSize})`** → big plain header (no box), like "Configuration" / "Flow".
166
- - **`connector({from,to,label,double,dashed})`** → labeled arrow between two points.
170
+ - **`connector({from,to,label,double,dashed,via,elbow})`** → labeled arrow between two points. Route it
171
+ AROUND intervening boxes instead of through them: `elbow:'hv'` (horizontal then vertical) / `elbow:'vh'`
172
+ (vertical then horizontal) bends it once at a right angle, or `via:[[x,y],…]` threads explicit waypoints.
173
+ - **`fitText({text,w,h,fontSize,minFont})`** → the wrap+shrink primitive `card()` uses; call it directly to
174
+ pre-size a box to its label (returns `{ text, fontSize, lineCount, fits }`).
167
175
  - **`annotate({from,to,text})`** → short arrow to a side-spec note (the "90sec / deepseek…" pattern).
168
176
  - **`column({x,y,items,color})`** → vertical stack of cards (benefit/risk lists). `items` may be
169
177
  strings or `{text,color}`.
@@ -175,6 +183,16 @@ Compose these into a `buildExcalidraw({ out, elements })` call. Runnable example
175
183
  Conventions: color nodes by ROLE, label every arrow, put specs in side-annotations, use big plain
176
184
  section titles to break the board into "slides", keep one idea per card.
177
185
 
186
+ ### Layout rules that keep boards clean
187
+ - **Size boxes for their text.** A card guarantees the label stays *inside*, but it does so by shrinking
188
+ the font — a long label in a small box ends up tiny. Give wide/long labels a wider `w` (or more `h`),
189
+ or split the idea across two cards. Watch for `[excalidraw]` fit warnings on the console.
190
+ - **Fan out through a margin bus, never as a diagonal star.** A hub wired straight to a vertical stack of
191
+ boxes draws diagonals that cut across the boxes in between. Instead, route the trunk into an empty
192
+ margin beside the stack, run one vertical "bus" line down that margin, then a short horizontal stub from
193
+ the bus into each box. With `connector()`: send the trunk `elbow:'hv'` into the bus x, then one
194
+ `elbow:'hv'` stub per branch (or `via:[[busX, branchY]]`). No arrow crosses a box it isn't pointing at.
195
+
178
196
  ## Rules & gotchas
179
197
  - **Unique image filenames.** Obsidian resolves `[[name.png]]` by basename; if two different files
180
198
  share a name anywhere in the vault the link is ambiguous. Name screenshots uniquely
@@ -55,30 +55,44 @@ function findVaultRoot(startDir) {
55
55
  }
56
56
 
57
57
  // ---------- text sizing ----------
58
+ // Excalifont glyph advance (multiples of fontSize): latin ~0.58, emoji/arrows/CJK/combining ~1.05.
59
+ // A flat factor under-counted wide glyphs, so emoji labels measured narrow and overflowed their box.
60
+ // This mirrors glyphW() in scripts/lib/style.js so card() pre-wrapping and on-board height agree.
61
+ function isWideGlyph(code) {
62
+ return (
63
+ (code >= 0x1100 && code <= 0x115f) || (code >= 0x2190 && code <= 0x21ff) ||
64
+ (code >= 0x2300 && code <= 0x23ff) || (code >= 0x25a0 && code <= 0x27bf) ||
65
+ (code >= 0x2b00 && code <= 0x2bff) || (code >= 0x2e80 && code <= 0x9fff) ||
66
+ (code >= 0xac00 && code <= 0xd7a3) || (code >= 0xf900 && code <= 0xfaff) ||
67
+ (code >= 0xfe30 && code <= 0xfe4f) || (code >= 0xff00 && code <= 0xff60) ||
68
+ (code >= 0x1f000 && code <= 0x1faff) || (code >= 0x1f1e6 && code <= 0x1f1ff)
69
+ );
70
+ }
71
+ function glyphW(ch, fontSize) { return fontSize * (isWideGlyph(ch.codePointAt(0)) ? 1.05 : 0.58); }
72
+ function measureText(s, fontSize) { let w = 0; for (const ch of String(s)) w += glyphW(ch, fontSize); return w; }
73
+
58
74
  // When a fixedWidth is given the text element is treated as WRAPPING (autoResize off downstream),
59
75
  // so height must reflect word-wrapped line count — otherwise long captions render as one wide line
60
76
  // and overlap their neighbours. Honors explicit \n and greedily wraps each logical line to width.
61
77
  function sizeText(text, fontSize, fixedWidth) {
62
78
  const rawLines = String(text).split('\n');
63
79
  if (fixedWidth != null) {
64
- const charW = fontSize * 0.52; // approx Excalifont glyph advance
65
- const perLine = Math.max(1, Math.floor(fixedWidth / charW));
66
80
  let totalLines = 0;
67
81
  for (const ln of rawLines) {
68
82
  const words = ln.split(/\s+/).filter(Boolean);
69
83
  if (!words.length) { totalLines += 1; continue; }
70
- let cur = 0, lc = 1;
84
+ let cur = '', lc = 1;
71
85
  for (const w of words) {
72
- const add = (cur ? 1 : 0) + w.length;
73
- if (cur + add > perLine && cur > 0) { lc++; cur = w.length; }
74
- else cur += add;
86
+ if (!cur) { cur = w; continue; }
87
+ if (measureText(cur + ' ' + w, fontSize) > fixedWidth) { lc++; cur = w; }
88
+ else cur += ' ' + w;
75
89
  }
76
90
  totalLines += lc;
77
91
  }
78
92
  return { width: fixedWidth, height: Math.max(Math.round(fontSize * 1.25), Math.round(totalLines * fontSize * 1.25)) };
79
93
  }
80
- const maxLen = rawLines.reduce((m, l) => Math.max(m, l.length), 0);
81
- return { width: Math.max(10, Math.round(maxLen * fontSize * 0.6)), height: Math.round(rawLines.length * fontSize * 1.25) };
94
+ const maxW = rawLines.reduce((m, l) => Math.max(m, measureText(l, fontSize)), 0);
95
+ return { width: Math.max(10, Math.round(maxW)), height: Math.round(rawLines.length * fontSize * 1.25) };
82
96
  }
83
97
 
84
98
  // ---------- element factory ----------
@@ -119,6 +133,31 @@ function buildExcalidraw(spec) {
119
133
  const wikilinkMode = spec.wikilinkMode || 'basename';
120
134
  const background = spec.background || '#ffffff';
121
135
 
136
+ // Pre-flight: every referenced image MUST exist on disk before we write the
137
+ // board. A missing asset would otherwise be emitted as a dangling `## Embedded
138
+ // Files` wikilink (`<sha1>: [[missing.png]]`) that can never resolve in any
139
+ // renderer — the board ships with blank images. Collect ALL missing paths and
140
+ // fail once with the full list (and the board path), so the author fixes every
141
+ // gap in a single pass instead of discovering them one throw at a time.
142
+ const missingImages = [];
143
+ for (const e of (spec.elements || [])) {
144
+ if (e && e.type === 'image' && e.path) {
145
+ // A missing file OR a non-file (directory / broken symlink) is unusable —
146
+ // statSync throws for a broken link / absent path, so default to missing.
147
+ let isFile = false;
148
+ try { isFile = fs.statSync(path.resolve(e.path)).isFile(); } catch { isFile = false; }
149
+ if (!isFile) missingImages.push(e.path);
150
+ }
151
+ }
152
+ if (missingImages.length) {
153
+ throw new Error(
154
+ `build_excalidraw: ${missingImages.length} referenced image(s) missing or not a file — ` +
155
+ `refusing to write a board with dangling embeds:\n ` +
156
+ missingImages.join('\n ') +
157
+ `\n(board: ${outPath})`,
158
+ );
159
+ }
160
+
122
161
  const rng = mulberry32(strHash(path.resolve(outPath)));
123
162
  const elements = [];
124
163
  const textEntries = []; // {text, id}