mulmoterminal 6.8.0 → 7.1.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 (226) hide show
  1. package/README.md +36 -9
  2. package/blueprints/ask/checks/replies.mjs +24 -2
  3. package/blueprints/ask/skills/answer/SKILL.md +7 -3
  4. package/blueprints/ask/skills/keep/SKILL.md +2 -1
  5. package/blueprints/cloudflare/checks/deploy-check.sh +16 -0
  6. package/blueprints/cloudflare/checks/handover.sh +14 -0
  7. package/blueprints/cloudflare/checks/page-renders.sh +28 -0
  8. package/blueprints/cloudflare/checks/prerequisites.sh +6 -0
  9. package/blueprints/cloudflare/checks/scaffold.sh +14 -0
  10. package/blueprints/cloudflare/checks/security-report.sh +11 -0
  11. package/blueprints/cloudflare/checks/security.sh +52 -0
  12. package/blueprints/cloudflare/checks/serve.sh +24 -0
  13. package/blueprints/cloudflare/checks/smoke.sh +6 -0
  14. package/blueprints/cloudflare/checks/spec.sh +12 -0
  15. package/blueprints/cloudflare/checks/tests-pass.sh +7 -0
  16. package/blueprints/cloudflare/manifest.json +22 -0
  17. package/blueprints/cloudflare/plan.json +83 -0
  18. package/blueprints/cloudflare/security/checklist.md +10 -0
  19. package/blueprints/cloudflare/skills/api/SKILL.md +27 -0
  20. package/blueprints/cloudflare/skills/auth/SKILL.md +29 -0
  21. package/blueprints/cloudflare/skills/data/SKILL.md +22 -0
  22. package/blueprints/cloudflare/skills/deploy/SKILL.md +33 -0
  23. package/blueprints/cloudflare/skills/handover/SKILL.md +36 -0
  24. package/blueprints/cloudflare/skills/prerequisites/SKILL.md +23 -0
  25. package/blueprints/cloudflare/skills/run-check/SKILL.md +23 -0
  26. package/blueprints/cloudflare/skills/scaffold/SKILL.md +51 -0
  27. package/blueprints/cloudflare/skills/security/SKILL.md +76 -0
  28. package/blueprints/cloudflare/skills/spec/SKILL.md +31 -0
  29. package/blueprints/cloudflare/skills/ui/SKILL.md +22 -0
  30. package/blueprints/cloudflare/spec/overview.md +33 -0
  31. package/blueprints/docs/checks/chaff.sh +1 -1
  32. package/blueprints/docs/checks/places.d.mts +7 -0
  33. package/blueprints/docs/checks/places.mjs +100 -0
  34. package/blueprints/docs/manifest.json +1 -0
  35. package/blueprints/docs/skills/workspace/SKILL.md +1 -1
  36. package/blueprints/firebase/checks/spec.sh +4 -0
  37. package/blueprints/firebase/manifest.json +1 -4
  38. package/blueprints/firebase/skills/scaffold/SKILL.md +2 -1
  39. package/blueprints/from-collection/checks/actions.sh +52 -0
  40. package/blueprints/from-collection/checks/compare.mjs +97 -0
  41. package/blueprints/from-collection/checks/d1-verify.mjs +98 -0
  42. package/blueprints/from-collection/checks/decisions.mjs +69 -0
  43. package/blueprints/from-collection/checks/firestore-verify.mjs +101 -0
  44. package/blueprints/from-collection/checks/import-d1.sh +36 -0
  45. package/blueprints/from-collection/checks/import-firebase.sh +36 -0
  46. package/blueprints/from-collection/checks/import-verify.mjs +20 -0
  47. package/blueprints/from-collection/checks/import.sh +20 -0
  48. package/blueprints/from-collection/checks/spec.sh +18 -1
  49. package/blueprints/from-collection/checks/test-titles.mjs +110 -0
  50. package/blueprints/from-collection/hearing.json +9 -2
  51. package/blueprints/from-collection/manifest.json +2 -2
  52. package/blueprints/from-collection/skills/acceptance-cloudflare/SKILL.md +28 -0
  53. package/blueprints/from-collection/skills/acceptance-firebase/SKILL.md +27 -0
  54. package/blueprints/from-collection/skills/actions/SKILL.md +47 -0
  55. package/blueprints/from-collection/skills/actions-cloudflare/SKILL.md +51 -0
  56. package/blueprints/from-collection/skills/actions-firebase/SKILL.md +46 -0
  57. package/blueprints/from-collection/skills/features-firebase/SKILL.md +28 -0
  58. package/blueprints/from-collection/skills/import/SKILL.md +39 -0
  59. package/blueprints/from-collection/skills/import-cloudflare/SKILL.md +54 -0
  60. package/blueprints/from-collection/skills/import-firebase/SKILL.md +38 -0
  61. package/blueprints/from-collection/skills/import-production/SKILL.md +29 -0
  62. package/blueprints/from-collection/skills/import-production-cloudflare/SKILL.md +29 -0
  63. package/blueprints/from-collection/spec/conversion.md +63 -4
  64. package/blueprints/from-collection/spec/requirements.md +31 -2
  65. package/blueprints/from-collection/steps.json +102 -0
  66. package/blueprints/local/checks/security.sh +5 -0
  67. package/blueprints/local/skills/scaffold/SKILL.md +2 -0
  68. package/blueprints/local/skills/security/SKILL.md +1 -1
  69. package/blueprints/product/manifest.json +2 -2
  70. package/blueprints/product/steps.json +1 -1
  71. package/blueprints/review/checks/findings.mjs +2 -1
  72. package/blueprints/review/checks/findingsView.d.mts +4 -1
  73. package/blueprints/review/checks/findingsView.mjs +7 -4
  74. package/blueprints/review/skills/report/SKILL.md +2 -1
  75. package/blueprints/style/checks/configured.d.mts +3 -0
  76. package/blueprints/style/checks/configured.mjs +16 -0
  77. package/blueprints/style/checks/counter.mjs +10 -2
  78. package/blueprints/style/checks/rules.mjs +2 -2
  79. package/blueprints/style/skills/counter/SKILL.md +5 -2
  80. package/blueprints/style/skills/report/SKILL.md +1 -1
  81. package/blueprints/style/skills/rules/SKILL.md +7 -2
  82. package/blueprints/verify/checks/extract.mjs +2 -1
  83. package/blueprints/verify/checks/facts.mjs +36 -8
  84. package/blueprints/verify/checks/factsView.d.mts +1 -1
  85. package/blueprints/verify/checks/factsView.mjs +20 -8
  86. package/blueprints/verify/checks/rules.d.mts +1 -0
  87. package/blueprints/verify/checks/rules.mjs +24 -2
  88. package/blueprints/verify/skills/extract/SKILL.md +2 -1
  89. package/blueprints/verify/skills/report/SKILL.md +5 -2
  90. package/common/blueprint/collectionSource.ts +44 -5
  91. package/common/blueprint/hearing.ts +22 -5
  92. package/common/blueprint/manifest.ts +17 -0
  93. package/common/blueprint/refusal.ts +4 -0
  94. package/common/blueprint/sharedAppSource.ts +30 -0
  95. package/common/blueprint/state.ts +4 -0
  96. package/common/blueprint/stepPrompt.ts +69 -1
  97. package/common/fileBackups.ts +18 -0
  98. package/common/fileGitStatus.ts +21 -0
  99. package/common/keymap.ts +82 -2
  100. package/common/mdPreviewMessage.ts +33 -2
  101. package/common/paletteConfig.ts +31 -0
  102. package/common/paletteSearchBox.ts +11 -0
  103. package/common/promptHistory.ts +9 -0
  104. package/dist/assets/{abnfDiagram-O67JEVCF-Dy6jPGlc.js → abnfDiagram-O67JEVCF-B5xaEVpw.js} +1 -1
  105. package/dist/assets/architecture-7GRP2DOG-BxUYapfa.js +1 -0
  106. package/dist/assets/{architectureDiagram-NJMV4G6O-Y1wqce2c.js → architectureDiagram-NJMV4G6O-qBYbXFGp.js} +1 -1
  107. package/dist/assets/{blockDiagram-BEXU5L5S-B6haVvbR.js → blockDiagram-BEXU5L5S-DQioBmlX.js} +1 -1
  108. package/dist/assets/{c4Diagram-YGBWAQC7-CbqMeQ5M.js → c4Diagram-YGBWAQC7-cyT63OdJ.js} +1 -1
  109. package/dist/assets/channel-DLqe-NE3.js +1 -0
  110. package/dist/assets/{chunk-3FUC2YCW-Dql-NfIM.js → chunk-3FUC2YCW-GAIwUw3Q.js} +1 -1
  111. package/dist/assets/{chunk-5DYCD2WN-DPBLnLhJ.js → chunk-5DYCD2WN-DMiZMIIB.js} +1 -1
  112. package/dist/assets/{chunk-742MDFTN-Cfcav64I.js → chunk-742MDFTN-6zizfpHe.js} +1 -1
  113. package/dist/assets/{chunk-7INBJB4K-gd2sm2Qq.js → chunk-7INBJB4K-C2dZhhKc.js} +1 -1
  114. package/dist/assets/{chunk-7M6MHVWA-Cga-bP1d.js → chunk-7M6MHVWA-CqPNVLmK.js} +1 -1
  115. package/dist/assets/{chunk-7PRAP22T-B5CSlQEt.js → chunk-7PRAP22T-BoBqX9Yo.js} +1 -1
  116. package/dist/assets/{chunk-GTNCS2PH-BzDxh0CI.js → chunk-GTNCS2PH-0N3yAwxf.js} +1 -1
  117. package/dist/assets/{chunk-GWA4HPMP-CTVycvRw.js → chunk-GWA4HPMP-g7RRSruF.js} +1 -1
  118. package/dist/assets/{chunk-MBY4JIJT-Dj2QOy3d.js → chunk-MBY4JIJT-C3i2TuVa.js} +1 -1
  119. package/dist/assets/{chunk-NETBCI7D-CVlwwnxD.js → chunk-NETBCI7D-CH7vEM98.js} +1 -1
  120. package/dist/assets/{chunk-O7XYJQB3-CBuERAl7.js → chunk-O7XYJQB3-D1JLIcoV.js} +1 -1
  121. package/dist/assets/{chunk-UA2S7LBM-Dq3DhUK-.js → chunk-UA2S7LBM-CpyAfTb8.js} +1 -1
  122. package/dist/assets/{chunk-WEXAMYUT-D8fWG-1Z.js → chunk-WEXAMYUT-BGgzQY1u.js} +1 -1
  123. package/dist/assets/{chunk-Z7XXMR3K-zrP0_WoK.js → chunk-Z7XXMR3K-CEMmXuqC.js} +1 -1
  124. package/dist/assets/{chunk-ZIGJFQKS-Dzlt4QPL.js → chunk-ZIGJFQKS-g1Kaf-uj.js} +1 -1
  125. package/dist/assets/{classDiagram-v2-NBCMYWYE-CjA-LmiN.js → classDiagram-v2-NBCMYWYE-B54sxnuH.js} +1 -1
  126. package/dist/assets/{cynefin-OW5HDTMX-CVRZEwmv.js → cynefin-OW5HDTMX-D796zEZZ.js} +1 -1
  127. package/dist/assets/{cynefinDiagram-VND7K2PF-C9rD0DOQ.js → cynefinDiagram-VND7K2PF-WHBkoilE.js} +1 -1
  128. package/dist/assets/{dagre-6A5THRUB-BuE8Gjyb.js → dagre-6A5THRUB-2gQ61gKB.js} +1 -1
  129. package/dist/assets/{diagram-22UHCM2B-Cw3M7VEV.js → diagram-22UHCM2B-yDp0rHwO.js} +1 -1
  130. package/dist/assets/{diagram-3UASUU5V-Pu3L65WF.js → diagram-3UASUU5V-pnng1LdB.js} +1 -1
  131. package/dist/assets/{diagram-ATOU4E4O-OI0bhF_A.js → diagram-ATOU4E4O-D5Jkesja.js} +1 -1
  132. package/dist/assets/{diagram-CDSNMT55-CH39M5AD.js → diagram-CDSNMT55-DkH9QxZ2.js} +1 -1
  133. package/dist/assets/{diagram-MLGK6HIB-B2nfDfEm.js → diagram-MLGK6HIB-CDjGhKHq.js} +1 -1
  134. package/dist/assets/{diagram-MPIPVDR6-CFiLA_-F.js → diagram-MPIPVDR6-DAAr_c_l.js} +1 -1
  135. package/dist/assets/{dist-6UpZENeR.js → dist-B0sdAm9J.js} +1 -1
  136. package/dist/assets/{dist-unjTvGLV.js → dist-BSQTHmad.js} +1 -1
  137. package/dist/assets/dist-C1cqjsKD.js +1 -0
  138. package/dist/assets/{dist-BMBUd4uQ.js → dist-C1s7S4KF.js} +1 -1
  139. package/dist/assets/dist-C3s_Ij_z.js +11 -0
  140. package/dist/assets/dist-CWSTMA5n.js +1 -0
  141. package/dist/assets/dist-CeWYOYWG.js +1 -0
  142. package/dist/assets/dist-CgC34u2d.js +1 -0
  143. package/dist/assets/dist-DHSqnwKI.js +9 -0
  144. package/dist/assets/{dist-lS8XUoi1.js → dist-DbLWBcKv.js} +2 -2
  145. package/dist/assets/{ebnfDiagram-ZINNZB2B-ChqhmMzu.js → ebnfDiagram-ZINNZB2B-DKJbg6sg.js} +1 -1
  146. package/dist/assets/{elk-276RUBZZ-De2Iml3H.js → elk-276RUBZZ-CgwsWSX7.js} +1 -1
  147. package/dist/assets/{erDiagram-OPXOYQCR-CUt6-AR7.js → erDiagram-OPXOYQCR-tCyjW7gV.js} +1 -1
  148. package/dist/assets/eventmodeling-NTZA5JFV-Bj6FbHv7.js +1 -0
  149. package/dist/assets/flowDiagram-KWPJA3E3-C3CneunC.js +1 -0
  150. package/dist/assets/{ganttDiagram-FUAMR5RP-BETJ1ApO.js → ganttDiagram-FUAMR5RP-C3CZ2yCC.js} +1 -1
  151. package/dist/assets/{gitGraph-4MIJSDKK-DGOqms6x.js → gitGraph-4MIJSDKK-n2QHyQg8.js} +1 -1
  152. package/dist/assets/{gitGraphDiagram-X574FWY7-Btn58SWQ.js → gitGraphDiagram-X574FWY7-Dtk0GaqE.js} +1 -1
  153. package/dist/assets/index-BkbFlsCM.css +1 -0
  154. package/dist/assets/{index-Dw_awLi8.js → index-CyHGyZiL.js} +363 -351
  155. package/dist/assets/{info-A6RAGUB7-BIn6JHsL.js → info-A6RAGUB7-Dt0N-ywn.js} +1 -1
  156. package/dist/assets/{infoDiagram-VRGFBTTK-CLLPPwgt.js → infoDiagram-VRGFBTTK-DtjU_EzA.js} +1 -1
  157. package/dist/assets/{ishikawaDiagram-OU5B5YK6-C0dnEC2O.js → ishikawaDiagram-OU5B5YK6-xOGev5sa.js} +1 -1
  158. package/dist/assets/{journeyDiagram-ZHPQQLJL-DiOFy0_3.js → journeyDiagram-ZHPQQLJL-Disti4Wm.js} +1 -1
  159. package/dist/assets/{kanban-definition-PNTS6WVX-DL0cY3t2.js → kanban-definition-PNTS6WVX-CB5dROof.js} +1 -1
  160. package/dist/assets/{line-CInvdCWV.js → line-D6MSXT7u.js} +1 -1
  161. package/dist/assets/{mermaid-parser.core-BGDbqboQ.js → mermaid-parser.core-C29lKtTF.js} +3 -3
  162. package/dist/assets/{mermaid.core-CZVPcJ9v.js → mermaid.core-CWf9rghw.js} +4 -4
  163. package/dist/assets/{mindmap-definition-NLK3R4M7-U6yboX99.js → mindmap-definition-NLK3R4M7-BfBZCMx_.js} +1 -1
  164. package/dist/assets/{packet-AYTQ26CC-DIDr2_-n.js → packet-AYTQ26CC-BRpJC2Bl.js} +1 -1
  165. package/dist/assets/{pegDiagram-GJSIUBJH-BbpYcKtu.js → pegDiagram-GJSIUBJH-xck9NPib.js} +1 -1
  166. package/dist/assets/{pie-WAS4IAKB-BsD46nmb.js → pie-WAS4IAKB-De38-oiC.js} +1 -1
  167. package/dist/assets/{pieDiagram-5QR66LMP-BgdP7pnf.js → pieDiagram-5QR66LMP-DI7hJbHF.js} +1 -1
  168. package/dist/assets/{quadrantDiagram-O4NWA36T-mx6H7yGz.js → quadrantDiagram-O4NWA36T-Bxqm6tTX.js} +1 -1
  169. package/dist/assets/{radar-RG4KPBEZ-BxzJE1SZ.js → radar-RG4KPBEZ-CS8aCQ7S.js} +1 -1
  170. package/dist/assets/{railroad-74A4TZTK-d2ye0a_H.js → railroad-74A4TZTK-DmloOzOC.js} +1 -1
  171. package/dist/assets/railroad-abnf-HS5TGJTU-lxjdNIS_.js +1 -0
  172. package/dist/assets/railroad-ebnf-LZEXJU2U-BvnO_c-5.js +1 -0
  173. package/dist/assets/railroad-peg-WCYAUIDC-Bco3H4cO.js +1 -0
  174. package/dist/assets/{railroadDiagram-XR7U4H2S-CdHoygiZ.js → railroadDiagram-XR7U4H2S-D8ruOuhL.js} +1 -1
  175. package/dist/assets/{requirementDiagram-PLB6GJNP-DgUPBmN3.js → requirementDiagram-PLB6GJNP-zWCW1gnc.js} +1 -1
  176. package/dist/assets/{sankeyDiagram-IPEJSGJF-CCqhFBKm.js → sankeyDiagram-IPEJSGJF-D1ndcb5q.js} +1 -1
  177. package/dist/assets/{sequenceDiagram-PO4LG4MO-DsOw9dn3.js → sequenceDiagram-PO4LG4MO-zO_f3a5l.js} +1 -1
  178. package/dist/assets/{stateDiagram-v2-GCMORJYK-KgYznRou.js → stateDiagram-v2-GCMORJYK-DjHTP1Un.js} +1 -1
  179. package/dist/assets/{swimlanes-2SLR337P-cJ978qAp.js → swimlanes-2SLR337P-3TYxXfqC.js} +1 -1
  180. package/dist/assets/swimlanesDiagram-TC7HE7FX-D0Ef5eY1.js +8 -0
  181. package/dist/assets/{timeline-definition-EJHVYXUP-DVrpY7-_.js → timeline-definition-EJHVYXUP-D2rwOmgp.js} +1 -1
  182. package/dist/assets/{treeView-Q6P3EWNA-C81INZMi.js → treeView-Q6P3EWNA-pJAjZYyX.js} +1 -1
  183. package/dist/assets/{treemap-WGGIJYW6-C6m5_3M_.js → treemap-WGGIJYW6-BkBiLnEF.js} +1 -1
  184. package/dist/assets/{usecaseDiagram-POWQR4AR-MOU_fUIi.js → usecaseDiagram-POWQR4AR-DZQYTp5w.js} +1 -1
  185. package/dist/assets/{vennDiagram-UO4OBE2U-DQ03sLpX.js → vennDiagram-UO4OBE2U-CLFk8_TU.js} +1 -1
  186. package/dist/assets/{wardley-WFR3VGLG-D6oOZcfd.js → wardley-WFR3VGLG-UZLdcCMt.js} +1 -1
  187. package/dist/assets/{wardleyDiagram-VNRHLVJA-DTu4wPK_.js → wardleyDiagram-VNRHLVJA-DOCJDdFu.js} +1 -1
  188. package/dist/assets/{xychartDiagram-PMCCYNJV-BnFJQnAh.js → xychartDiagram-PMCCYNJV-DaRQvK2Z.js} +1 -1
  189. package/dist/index.html +3 -3
  190. package/package.json +5 -4
  191. package/server/blueprint/answersFile.ts +12 -0
  192. package/server/blueprint/collectionSnapshot.ts +151 -31
  193. package/server/blueprint/executor.ts +2 -1
  194. package/server/blueprint/packs.ts +13 -7
  195. package/server/blueprint/routes.ts +49 -7
  196. package/server/blueprint/sharedAppsProvider.ts +53 -0
  197. package/server/blueprint/wiring.ts +11 -2
  198. package/server/blueprint/writeAllOrNone.ts +1 -1
  199. package/server/config/app-config.ts +19 -0
  200. package/server/config/config-body.ts +2 -1
  201. package/server/config/config-routes.ts +75 -32
  202. package/server/files/backup-store.ts +51 -0
  203. package/server/files/files-browse.ts +46 -11
  204. package/server/files/files-git-status.ts +65 -0
  205. package/server/files/mdPreviewEmbed.ts +2 -103
  206. package/server/files/mdPreviewReporter.ts +154 -0
  207. package/server/files/renderedDoc.ts +10 -5
  208. package/server/git/statusEntries.ts +40 -0
  209. package/server/git/worktrees.ts +19 -3
  210. package/server/skills/mulmoterminal-config/SKILL.md +14 -1
  211. package/server/skills/mulmoterminal-keys/SKILL.md +50 -4
  212. package/dist/assets/architecture-7GRP2DOG-DyaLKyU5.js +0 -1
  213. package/dist/assets/channel-T986AWhs.js +0 -1
  214. package/dist/assets/dist-B2UfqsPh.js +0 -1
  215. package/dist/assets/dist-BSqJ_nUe.js +0 -9
  216. package/dist/assets/dist-CHPrzLIK.js +0 -1
  217. package/dist/assets/dist-D8e4q8MY.js +0 -1
  218. package/dist/assets/dist-Sj3hCOj7.js +0 -11
  219. package/dist/assets/dist-pmD9XVGa.js +0 -1
  220. package/dist/assets/eventmodeling-NTZA5JFV-DAxZbpPN.js +0 -1
  221. package/dist/assets/flowDiagram-KWPJA3E3-Bss7fLee.js +0 -1
  222. package/dist/assets/index-8B5Of03P.css +0 -1
  223. package/dist/assets/railroad-abnf-HS5TGJTU-BJ8b7QD7.js +0 -1
  224. package/dist/assets/railroad-ebnf-LZEXJU2U-YG_Upy_7.js +0 -1
  225. package/dist/assets/railroad-peg-WCYAUIDC-OgQyRk3s.js +0 -1
  226. package/dist/assets/swimlanesDiagram-TC7HE7FX-BOVOQpWl.js +0 -8
package/README.md CHANGED
@@ -176,15 +176,25 @@ than as bytes (files within the session's working directory only):
176
176
  | `.json` | **indented** in a new tab (Chrome and Safari otherwise show one long line) |
177
177
  | `.csv` `.tsv` | a **table** in a new tab, with a sticky header that scrolls inside its own box |
178
178
  | source, config, logs, and `.txt` — 46 extensions | the app's own **Files** view (`/files?path=`), where CodeMirror highlights it, the tree is right there, and it can be edited |
179
- | images, PDF, SVG, HTML, video | raw bytes in a new tab, which the browser renders better than an editor would |
179
+ | `.html` `.htm` | the **rendered page** in a new tab, under the same sandboxed policy as the Files pane's Preview: its scripts run at an opaque origin with no fetch or XHR (so they cannot reach the app or its API), and an image beside it loads by its relative path (a relative stylesheet or script does not). The raw route answers `.html` as plain text, so opening one there showed its source |
180
+ | images, PDF, SVG, video, audio | raw bytes in a new tab, which the browser renders better than an editor would |
180
181
  | everything else — `.xlsx`, `.docx`, `.zip`, a `Makefile` | the app's own **Files** view. A tab cannot display these, so opening one there is not a view — it is a download starting with no warning. The pane names the file and offers **Open in OS**, which hands it to the application that owns it (Excel for an `.xlsx`) |
181
182
 
183
+ **A line after the path is followed.** `src/a.ts:42`, `src/a.ts:42:7` (gcc, clang, `eslint -f unix`,
184
+ stack traces) and `src/a.ts(12,5)` (tsc) are one link. In the pane, and in the Files view, the file
185
+ opens as text with the caret on that line and column — an HTML page, an SVG or a table included, and a
186
+ tab reading in Preview switches to Edit. A route that renders the file in a new tab (`.md`, `.json`,
187
+ `.csv`, `.html` with no cell enlarged) ignores the line. When the pane beside an enlarged cell takes
188
+ the click, the keyboard stays in the terminal; the full-screen Files view, with no terminal beside it,
189
+ puts it in the editor. (ESLint's default output puts the path on a line of its own, with no line number to follow.)
190
+
182
191
  **While a grid cell is enlarged, the [Files pane](#files-view-browse--edit) takes the click first** — every
183
- row above except the last one, since the pane is the same editor plus a Markdown preview. The
184
- file opens *beside* the terminal that printed it, and the pane opens itself if it was closed.
185
- It declines, leaving the routing above untouched, when nothing is enlarged, when the path is
186
- not under that cell's own directory (the pane cannot walk above its root), or for the raw-bytes
187
- row, where it would only show an empty editor.
192
+ row above except PDF, video and audio, since the pane is the same editor with a Preview for
193
+ Markdown, HTML and SVG, and shows an image as a picture. The file opens *beside* the terminal
194
+ that printed it, and the pane opens itself if it was closed. It declines, leaving the routing
195
+ above untouched, when nothing is enlarged, when the path is not under that cell's own directory
196
+ (the pane cannot walk above its root), or for a PDF, a video or an audio file, which only a
197
+ browser tab can play.
188
198
 
189
199
  Highlighting in the Files view covers the JS/TS family, JSON and Markdown (the modes
190
200
  `cmEditor.ts` bundles); other languages open as plain text.
@@ -1244,7 +1254,12 @@ rooted at **that terminal's project directory** — so after Claude says "wrote
1244
1254
  you can jump straight there to read or edit it. The left pane is a lazy-loaded directory
1245
1255
  tree; clicking a file opens it in a **CodeMirror** editor (Markdown / JS-TS / JSON
1246
1256
  highlighting, everything else as plain text). Markdown files get a **Preview** toggle
1247
- that renders via the server's sandboxed `…/md` HTML. **Save** (or ⌘/Ctrl-S) writes back.
1257
+ that renders via the server's sandboxed `…/md` HTML; a `.csv` / `.tsv` previews as a table (the
1258
+ same `…/table` document a clicked path opens, in the app's colours), and an `.html` / `.svg` as the
1259
+ page or picture it is. **Save** (or ⌘/Ctrl-S) writes back. **History** lists the versions the pane
1260
+ kept of the file (in `~/.mulmoterminal/backups`, taken when it is opened or reloaded here and before a save
1261
+ here replaces it — the newest few), to compare with the buffer or restore as an undoable edit (unsaved edits
1262
+ are kept as a version first).
1248
1263
 
1249
1264
  **The open view follows the file on disk.** When an agent in another cell — or any editor —
1250
1265
  rewrites what you are looking at, the editor and the preview catch up on their own; there is
@@ -1266,11 +1281,11 @@ shrinks the pane rather than reflowing xterm into garbage. It works in both zoom
1266
1281
  terminals, and whether it's open plus how wide it is are remembered per browser.
1267
1282
 
1268
1283
  **Coming back looks the way you left it.** The pane remembers the open file, the directories
1269
- you had expanded, and — for a Markdown file — whether you were reading it in **Preview** or
1284
+ you had expanded, and — for a file with a Preview — whether you were reading it in **Preview** or
1270
1285
  editing it: per cell while the session lasts, and per directory across a browser reload (the
1271
1286
  first pane to open on that directory claims it, so a second terminal in the same repository
1272
1287
  starts on its own empty tree). A remembered Preview only comes back over that same file while
1273
- it is still Markdown the server can render; anything else opens in the editor.
1288
+ it is still a file the pane can preview; anything else opens in the editor.
1274
1289
 
1275
1290
  It comes back to **where** you were, too: the line at the top of the editor, the line the caret was
1276
1291
  on, and how far down the tree was scrolled. The top line matters on its own — scrolling moves
@@ -1296,6 +1311,18 @@ finder, the search or a clicked terminal path alike. Switching tabs saves the on
1296
1311
  opening another file does, so only the front tab can hold unsaved edits; each tab keeps its own place
1297
1312
  (Preview or editor, caret, scroll) and the whole strip is remembered with the rest of the pane. Close a
1298
1313
  tab with its **×**, a middle click, or **Delete** while it has focus; ←/→, Home and End move between them.
1314
+ The tree follows the tab in front, as VS Code's explorer does: its folders open and its row comes into
1315
+ view (a reload still puts the tree back where you left it).
1316
+
1317
+ **The tree shows what git sees.** In a git repository a changed file is tinted and lettered as in VS
1318
+ Code's explorer — `M` modified, `A` added, `U` untracked, `R` renamed — and a folder holding changes
1319
+ carries a dot, so a collapsed tree still says where the agent has been. It is read when the tree
1320
+ loads or reloads, after a save or an outside change to the open file, and every thirty seconds.
1321
+
1322
+ **The editor marks what changed since the last commit.** A bar beside each line that is new (green)
1323
+ or changed (amber), and a notch where lines were removed, as VS Code's gutter shows them — kept up
1324
+ to date as you type. **Changes** in the header also shows the removed lines in place, as a unified
1325
+ diff. Nothing is marked for a file outside git or one not yet committed.
1299
1326
 
1300
1327
  **HTML pages and images show in the pane too.** An `.html` file has a **Preview** like a Markdown
1301
1328
  file: the page itself, sandboxed as presentHtml pages are — its scripts run on an opaque origin with
@@ -1,7 +1,7 @@
1
1
  // Reads .blueprint/replies.json — an answer to each question the person asked — and answers one question
2
2
  // per mode. Exit 0 is yes.
3
3
  // answer every question has exactly one reply; a reply found in the documents quotes them (chaff cite)
4
- // and names every address it quotes; a reply not found says what was searched. Records the
4
+ // and names every place it quotes (the address, or the heading of the section there); a reply not found says what was searched. Records the
5
5
  // documents' and FAQ.md's fingerprints.
6
6
  // keep the documents are unchanged; FAQ.md holds every question after what it held before when the
7
7
  // person asked to keep the answers, and is untouched otherwise
@@ -9,6 +9,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
9
9
  import { fromBase } from "./base.mjs";
10
10
  const { fail, quotationProblems, readJson } = await import(fromBase("chaff.mjs"));
11
11
  const { digest, documentSource, documentsNamed, fingerprint } = await import(fromBase("documents.mjs"));
12
+ const { headingsIn, namesPlace, placeNamesIn, treeOf } = await import(fromBase("places.mjs"));
12
13
 
13
14
  const REPLIES = ".blueprint/replies.json";
14
15
  const REPLIES_PAGE = ".blueprint/replies.md";
@@ -29,6 +30,27 @@ if (answers?.keep === KEEP && documents.includes(FAQ)) fail(`${FAQ} is one of th
29
30
  const indented = (line) => " " + line;
30
31
  const nonEmpty = (value) => typeof value === "string" && value.trim() !== "";
31
32
 
33
+ // Each document's tree is read once, and only for one of the documents: a source that is not is refused by the
34
+ // quotation check. From it come a section's heading and a place's name (第4条第2項), either of which names the place.
35
+ const placeCache = new Map();
36
+ const placeAt = (source, address) => {
37
+ const resolved = documentSource(documents)(source);
38
+ if (!resolved.path) return {};
39
+ if (!placeCache.has(resolved.path)) {
40
+ const tree = treeOf(resolved.path);
41
+ placeCache.set(resolved.path, { headings: headingsIn(tree), names: placeNamesIn(tree) });
42
+ }
43
+ const { headings, names } = placeCache.get(resolved.path);
44
+ return { heading: headings.get(address.trim()), name: names.get(address.trim()) };
45
+ };
46
+
47
+ // The address as written needs no tree; otherwise the heading or the place's name, from the document's tree.
48
+ const answerNames = (answer, citation) => {
49
+ if (answer.includes(citation.address.trim())) return true;
50
+ const { heading, name } = placeAt(String(citation.source), citation.address);
51
+ return namesPlace(answer, citation.address, heading, name);
52
+ };
53
+
32
54
  const replyProblem = (reply) => {
33
55
  if (typeof reply !== "object" || reply === null) return "a reply is not an object";
34
56
  if (!nonEmpty(reply.question)) return "a reply names no question";
@@ -39,7 +61,7 @@ const replyProblem = (reply) => {
39
61
  if (!Array.isArray(citations)) return `${which}: citations must be an array`;
40
62
  if (reply.found && citations.length === 0) return `${which}: an answer found in the documents quotes them`;
41
63
  if (!citations.every((citation) => nonEmpty(citation?.address))) return `${which}: every quotation needs an "address"`;
42
- const unnamed = citations.filter((citation) => !reply.answer.includes(citation.address.trim()));
64
+ const unnamed = citations.filter((citation) => !answerNames(reply.answer, citation));
43
65
  if (unnamed.length > 0) return `${which}: the answer does not name ${unnamed.map((citation) => citation.address).join(", ")}, which it quotes`;
44
66
  if (!reply.found && (!Array.isArray(reply.searched) || !reply.searched.some(nonEmpty))) return `${which}: say what was searched ("searched")`;
45
67
  return null;
@@ -48,11 +48,15 @@ the answers before anything is kept.
48
48
  - `question`: the question exactly as it is in `questions`. One reply per question, none extra.
49
49
  - `found`: `true` when the documents answer it. Then at least one citation.
50
50
  - `citations`: `source` is the document's path as `documents` names it; `address` is where; `quote` is
51
- copied character for character — the check runs `chaff cite` on every one. **Write every cited address in
52
- the `answer` text** so the person can open the document there.
51
+ copied character for character — the check runs `chaff cite` on every one. **Name every cited place in
52
+ the `answer` text** so the person can open the document there: an article or item as the document numbers it
53
+ (第4条第2項, Section 3.1), and a section of a Markdown document by its heading, word for word, in 「」 (「作業用フォルダを信頼しておく」;
54
+ in an English answer, "Getting started") — the check looks for it in those marks.
55
+ chaff's index for a section (`h1.3`) goes in `address` only — it means nothing to the person.
53
56
  - `searched`: when `found` is `false`, the words and places you looked for.
54
57
 
55
- Also write `.blueprint/replies.md` for the person: each question as a heading, the answer, and the quotations.
58
+ Also write `.blueprint/replies.md` for the person: each question as a heading, the answer, and the quotations,
59
+ each with its place named as in the answer (never `h1.3`).
56
60
 
57
61
  ## Done when
58
62
 
@@ -17,7 +17,8 @@ run the check and stop.
17
17
  Otherwise add to `FAQ.md` in this folder (create it if there is none). **Add after what it already holds;
18
18
  never edit or reorder what is there** — the check compares the beginning of the file with what it held
19
19
  before. For each question: the question as a `##` heading, word for word; the answer; and where it is
20
- written (document and address). For a question the documents do not answer, say so — that is worth keeping
20
+ written (the document, and the place named as in the answer: 第4条第2項, or a section's heading in 「」 —
21
+ never chaff's `h1.3`). For a question the documents do not answer, say so — that is worth keeping
21
22
  too.
22
23
 
23
24
  ## Done when
@@ -0,0 +1,16 @@
1
+ #!/bin/sh
2
+ # The published Worker serves the build this deploy made: the deploy writes a fresh id to .blueprint/build-id and ships
3
+ # the same id as /blueprint-build.txt, and records the URL it published to in .blueprint/deploy-url. The published app
4
+ # answers /api/health, sends the security headers, and its page renders.
5
+ set -eu
6
+ [ -s .blueprint/deploy-url ] || { echo "missing .blueprint/deploy-url; the deploy step writes the URL it published to" >&2; exit 1; }
7
+ url=$(tr -d ' \n\r' < .blueprint/deploy-url)
8
+ url=${url%/}
9
+ case "$url" in https://*) ;; *) echo ".blueprint/deploy-url is not an https URL: $url" >&2; exit 1 ;; esac
10
+ [ -s .blueprint/build-id ] || { echo "missing .blueprint/build-id; the deploy step writes it before deploying" >&2; exit 1; }
11
+ expected=$(cat .blueprint/build-id)
12
+ served=$(curl -fsS --max-time 20 "$url/blueprint-build.txt")
13
+ [ "$served" = "$expected" ] || { echo "$url serves build $served, this deploy made $expected" >&2; exit 1; }
14
+ curl -fsS --max-time 20 "$url/api/health" | grep -q '"ok":true' || { echo "$url/api/health does not answer {\"ok\":true}" >&2; exit 1; }
15
+ curl -s -D - -o /dev/null --max-time 20 "$url/" | tr -d '\r' | grep -qi '^content-security-policy:' || { echo "$url/ is published without a Content-Security-Policy" >&2; exit 1; }
16
+ sh "$(dirname "$0")/page-renders.sh" "$url/"
@@ -0,0 +1,14 @@
1
+ #!/bin/sh
2
+ # The README tells a non-engineer how to run the app on this computer, how to publish a change, and how to back up the
3
+ # data; the first page the finished build shows says where the published app is and what to try to see it works.
4
+ set -eu
5
+ [ -s README.md ] || { echo "missing README.md" >&2; exit 1; }
6
+ grep -q "yarn start" README.md || { echo "README.md does not say how to run it on this computer (yarn start)" >&2; exit 1; }
7
+ grep -q "wrangler deploy\|yarn deploy" README.md || { echo "README.md does not say how to publish a change (yarn deploy)" >&2; exit 1; }
8
+ grep -q "d1 export" README.md || { echo "README.md does not say how to back up the data (wrangler d1 export)" >&2; exit 1; }
9
+ start=.blueprint/start-here.md
10
+ [ -s "$start" ] || { echo "missing $start" >&2; exit 1; }
11
+ [ -s .blueprint/deploy-url ] || { echo "missing .blueprint/deploy-url" >&2; exit 1; }
12
+ url=$(tr -d ' \n\r' < .blueprint/deploy-url)
13
+ grep -qF "${url%/}" "$start" || { echo "$start does not name the published URL (${url%/})" >&2; exit 1; }
14
+ grep -q '^- \[ \] ' "$start" || { echo "$start has no checklist of what to try (- [ ] …)" >&2; exit 1; }
@@ -0,0 +1,28 @@
1
+ #!/bin/sh
2
+ # The served page actually starts: a build can deploy, report its id and answer 200, and still be a
3
+ # blank page because the app threw on load (a missing config does exactly that). Rendered in headless
4
+ # Chrome, the page must show some text. Without a Chrome this cannot be judged, and says so.
5
+ set -eu
6
+ url="$1"
7
+ chrome=""
8
+ for candidate in "${CHROME:-}" "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" google-chrome chromium chromium-browser; do
9
+ [ -n "$candidate" ] || continue
10
+ if [ -x "$candidate" ] || command -v "$candidate" >/dev/null 2>&1; then chrome="$candidate"; break; fi
11
+ done
12
+ [ -n "$chrome" ] || { echo "no Chrome found (set CHROME=...); could not confirm that $url renders" >&2; exit 0; }
13
+ # Headless Chrome can print the DOM and then not exit, so it is waited on for the DOM, not for exit.
14
+ RENDER_LIMIT_SECONDS=60
15
+ work=$(mktemp -d)
16
+ "$chrome" --headless=new --disable-gpu --no-first-run --user-data-dir="$work/profile" --virtual-time-budget=15000 --dump-dom "$url" >"$work/dom.html" 2>/dev/null &
17
+ pid=$!
18
+ trap 'kill "$pid" 2>/dev/null || true; rm -rf "$work"' EXIT
19
+ waited=0
20
+ until grep -q "</html>" "$work/dom.html" 2>/dev/null || [ "$waited" -ge "$RENDER_LIMIT_SECONDS" ] || ! kill -0 "$pid" 2>/dev/null; do
21
+ sleep 1
22
+ waited=$((waited + 1))
23
+ done
24
+ node -e 'let s="";process.stdin.on("data",(c)=>s+=c).on("end",()=>{
25
+ const body = (s.match(/<body[^>]*>([\s\S]*)<\/body>/i) || [, ""])[1];
26
+ const text = body.replace(/<(script|style|noscript)[\s\S]*?<\/\1>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
27
+ if (!text) { console.error(process.argv[1] + " renders a blank page: the app failed to start. Open it in a browser and read the console error."); process.exit(1); }
28
+ })' "$url" <"$work/dom.html"
@@ -0,0 +1,6 @@
1
+ #!/bin/sh
2
+ # Node is new enough for wrangler, and yarn answers. wrangler itself is a dev dependency of the project.
3
+ set -eu
4
+ node -e 'process.exit(Number(process.versions.node.split(".")[0]) >= 22 ? 0 : 1)' ||
5
+ { echo "Node.js 22 or later is needed (wrangler requires it); this is $(node -v)" >&2; exit 1; }
6
+ yarn --version >/dev/null 2>&1 || { echo "yarn is missing (corepack enable)" >&2; exit 1; }
@@ -0,0 +1,14 @@
1
+ #!/bin/sh
2
+ # The layout the later checks rely on exists, and the skeleton builds and its tests pass inside the Workers runtime.
3
+ set -eu
4
+ for f in package.json wrangler.jsonc src/worker/index.ts index.html vitest.config.ts; do
5
+ [ -f "$f" ] || { echo "missing $f" >&2; exit 1; }
6
+ done
7
+ for d in migrations test; do
8
+ [ -d "$d" ] || { echo "missing $d/" >&2; exit 1; }
9
+ done
10
+ grep -q '"start"' package.json || { echo "package.json has no start script" >&2; exit 1; }
11
+ grep -q '"d1_databases"' wrangler.jsonc || { echo "wrangler.jsonc binds no D1 database" >&2; exit 1; }
12
+ grep -q '"assets"' wrangler.jsonc || { echo "wrangler.jsonc serves no assets" >&2; exit 1; }
13
+ yarn build
14
+ yarn test
@@ -0,0 +1,11 @@
1
+ #!/bin/sh
2
+ # The security review's report exists, walks every OWASP Top 10:2025 category, and leaves no HIGH or
3
+ # MEDIUM finding open or merely accepted. A finding is one line: `- <HIGH|MEDIUM|LOW> <fixed|open|accepted>: …`.
4
+ set -eu
5
+ report=.blueprint/security-review.md
6
+ [ -s "$report" ] || { echo "missing $report" >&2; exit 1; }
7
+ for category in A01 A02 A03 A04 A05 A06 A07 A08 A09 A10; do
8
+ grep -q "^#.*$category" "$report" || { echo "$report has no section for $category (OWASP Top 10:2025)" >&2; exit 1; }
9
+ done
10
+ open=$(grep -nE '^- (HIGH|MEDIUM) (open|accepted)' "$report" || true)
11
+ [ -z "$open" ] || { printf 'HIGH / MEDIUM findings must be fixed, but are open or accepted in %s:\n%s\n' "$report" "$open" >&2; exit 1; }
@@ -0,0 +1,52 @@
1
+ #!/bin/sh
2
+ # The security review is proven by the running Worker, not by the review's word: the app started with wrangler dev is
3
+ # sent the requests an attack would send (a cross-site state change, a malformed body) and must refuse them, its pages
4
+ # and its API carry the headers a browser needs, its secrets stay out of any repository, its dependencies have no known
5
+ # high-severity hole, and the review's own report and tests are in place.
6
+ set -eu
7
+ sh "$(dirname "$0")/security-report.sh"
8
+ [ -f test/security.test.ts ] || { echo "missing test/security.test.ts" >&2; exit 1; }
9
+ # Local secrets live in .dev.vars (or .env); the folder becomes a repository whenever its person adds git.
10
+ for secrets in .dev.vars .env; do
11
+ if [ -f "$secrets" ] && ! grep -qxE "/?$(printf '%s' "$secrets" | sed 's/\./\\./g')(\\*)?" .gitignore 2>/dev/null; then
12
+ echo "$secrets exists and .gitignore does not ignore it; add a line $secrets to .gitignore" >&2
13
+ exit 1
14
+ fi
15
+ done
16
+ yarn test >/dev/null || { echo "yarn test fails" >&2; yarn test >&2 || true; exit 1; }
17
+ # Read from the summary, not the exit code: an audit that could not reach the registry must not pass.
18
+ yarn audit --groups dependencies --json 2>/dev/null | node -e 'let s="";process.stdin.on("data",(c)=>s+=c).on("end",()=>{
19
+ const summary = s.split("\n").filter(Boolean).map((line) => JSON.parse(line)).find((entry) => entry.type === "auditSummary");
20
+ if (!summary) { console.error("yarn audit did not report a summary (is the registry reachable?)"); process.exit(1); }
21
+ const { high = 0, critical = 0 } = summary.data.vulnerabilities;
22
+ if (high + critical > 0) { console.error(`yarn audit: ${high} high and ${critical} critical vulnerabilities in dependencies; run yarn audit --groups dependencies`); process.exit(1); }
23
+ })'
24
+
25
+ . "$(dirname "$0")/serve.sh"
26
+ base="http://127.0.0.1:$port"
27
+ status() { curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$@"; }
28
+ fail() { echo "$1" >&2; exit 1; }
29
+
30
+ # A state change sent from another site's page.
31
+ code=$(status -X POST -H "Origin: https://attacker.example" -H "Content-Type: application/json" -d '{}' "$base/api/blueprint-security-probe")
32
+ [ "$code" = 403 ] || fail "POST /api/… from Origin https://attacker.example answered $code; a state change from another origin must be refused with 403 before routing and sign-in"
33
+
34
+ # The page is served as a static asset, which does not go through the Worker: its headers come from public/_headers.
35
+ for target in / /api/health; do
36
+ headers=$(curl -s -D - -o /dev/null --max-time 5 "$base$target" | tr -d '\r')
37
+ has() { printf '%s' "$headers" | grep -qiE "$1"; }
38
+ has '^x-content-type-options: *nosniff' || fail "$target is served without X-Content-Type-Options: nosniff"
39
+ has '^content-security-policy:' || fail "$target is served without a Content-Security-Policy"
40
+ has "^content-security-policy:.*frame-ancestors|^x-frame-options: *(deny|sameorigin)" || fail "$target can be framed by another site (no frame-ancestors in the CSP, no X-Frame-Options)"
41
+ ! has '^x-powered-by:' || fail "$target carries X-Powered-By"
42
+ done
43
+
44
+ # A malformed body is refused at the entry, before routing — a Worker reads JSON per route, so a check against a path no
45
+ # route handles proves nothing unless the body is read once, up front — and is answered without the Worker's insides.
46
+ response=$(curl -s --max-time 5 -w '\n%{http_code}' -X POST -H "Origin: $base" -H "Content-Type: application/json" --data '{"broken' "$base/api/blueprint-security-probe")
47
+ code=$(printf '%s' "$response" | tail -n 1)
48
+ body=$(printf '%s' "$response" | sed '$d')
49
+ [ "$code" = 400 ] || fail "a malformed JSON body to /api/… answered $code; it must be refused with 400 where the Worker reads JSON bodies, before routing"
50
+ if printf '%s' "$body" | grep -qE 'node_modules|at [^ ]+ \(|SyntaxError: |src/worker/'; then
51
+ fail "a malformed JSON body is answered with a stack trace or the parser's error: $(printf '%s' "$body" | head -c 300)"
52
+ fi
@@ -0,0 +1,24 @@
1
+ # Sourced, not run: builds, then starts the app on this computer with `yarn start --port <port>` (wrangler dev) and waits
2
+ # for /api/health. Sets `port`. wrangler dev runs as a tree of processes (yarn, wrangler, workerd, esbuild) and a parent
3
+ # left behind restarts its workerd, so the EXIT trap stops the whole tree this check started, and nothing else.
4
+ yarn build >/dev/null
5
+ # process.stdout.write, not console.log: with FORCE_COLOR set, console.log colours a number.
6
+ port=$(node -e 'const s = require("node:net").createServer().listen(0, "127.0.0.1", () => { process.stdout.write(String(s.address().port)); s.close(); })')
7
+ log=$(mktemp)
8
+ yarn -s start --port "$port" >"$log" 2>&1 &
9
+ started=$!
10
+ stop_tree() {
11
+ for child in $(pgrep -P "$1" 2>/dev/null); do stop_tree "$child"; done
12
+ kill "$1" 2>/dev/null || true
13
+ }
14
+ stop() {
15
+ stop_tree "$started"
16
+ rm -f "$log"
17
+ }
18
+ trap stop EXIT INT TERM
19
+ tries=0
20
+ until curl -fsS --max-time 2 "http://127.0.0.1:$port/api/health" >/dev/null 2>&1; do
21
+ tries=$((tries + 1))
22
+ [ "$tries" -lt 120 ] || { echo "the app did not answer /api/health on port $port (yarn start --port $port)" >&2; cat "$log" >&2; exit 1; }
23
+ sleep 0.5
24
+ done
@@ -0,0 +1,6 @@
1
+ #!/bin/sh
2
+ # Builds, starts the app with wrangler dev on a spare port, and checks that /api/health and / answer.
3
+ set -eu
4
+ . "$(dirname "$0")/serve.sh"
5
+ curl -fsS --max-time 5 "http://127.0.0.1:$port/api/health" | grep -q '"ok":true' || { echo '/api/health did not answer {"ok":true}' >&2; exit 1; }
6
+ curl -fsS --max-time 5 "http://127.0.0.1:$port/" | grep -q 'id="app"' || { echo "/ does not serve the app" >&2; exit 1; }
@@ -0,0 +1,12 @@
1
+ #!/bin/sh
2
+ # The spec exists and every template placeholder in it was filled or marked undecided.
3
+ set -eu
4
+ [ -s .blueprint/spec.md ] || { echo "missing .blueprint/spec.md" >&2; exit 1; }
5
+ if grep -n '{{' .blueprint/spec.md >&2; then
6
+ echo "unfilled placeholders are left in .blueprint/spec.md" >&2
7
+ exit 1
8
+ fi
9
+ # A usecase may hold its spec to more than the template: when it ships its own spec check, that runs too.
10
+ if [ -n "${BLUEPRINT_USECASE:-}" ] && [ -f "$BLUEPRINT_USECASE/checks/spec.sh" ]; then
11
+ sh "$BLUEPRINT_USECASE/checks/spec.sh"
12
+ fi
@@ -0,0 +1,7 @@
1
+ #!/bin/sh
2
+ # The area's own tests exist (test/<area>.test.ts), and everything builds and passes.
3
+ set -eu
4
+ area="$1"
5
+ [ -f "test/$area.test.ts" ] || { echo "missing test/$area.test.ts" >&2; exit 1; }
6
+ yarn build
7
+ yarn test
@@ -0,0 +1,22 @@
1
+ {
2
+ "kind": "base",
3
+ "slug": "cloudflare",
4
+ "title": "Cloudflare(Workers + D1 + Vue)",
5
+ "version": "0.1.0",
6
+ "description": "Runs on Cloudflare: a Worker for the API, D1 (SQLite) for data, and a Vue screen served as static assets from the same Worker. Developed and checked on this computer with wrangler dev, then published with wrangler deploy after approval. A free Cloudflare account is enough to start.",
7
+ "platform": "cloudflare",
8
+ "requires": [
9
+ {
10
+ "command": "node",
11
+ "probe": "node -e \"process.exit(Number(process.versions.node.split('.')[0]) >= 22 ? 0 : 1)\"",
12
+ "installHint": "Node.js 22 or later (https://nodejs.org/). wrangler needs it."
13
+ },
14
+ {
15
+ "command": "yarn",
16
+ "probe": "yarn --version",
17
+ "installHint": "corepack enable"
18
+ }
19
+ ],
20
+ "credentials": ["wrangler login"],
21
+ "report": ".blueprint/start-here.md"
22
+ }
@@ -0,0 +1,83 @@
1
+ {
2
+ "steps": [
3
+ {
4
+ "id": "spec",
5
+ "title": "仕様書を書く",
6
+ "description": "ヒアリングの答えから、土台と用途の仕様の雛形を埋める。決まっていない点は一覧にする。",
7
+ "skill": "skills/spec",
8
+ "check": "sh \"$BLUEPRINT_BASE/checks/spec.sh\""
9
+ },
10
+ {
11
+ "id": "prerequisites",
12
+ "title": "道具の確認",
13
+ "description": "Node(22 以上)と yarn を確かめる。wrangler はプロジェクトに入れるので、ここでは要らない。",
14
+ "skill": "skills/prerequisites",
15
+ "check": "sh \"$BLUEPRINT_BASE/checks/prerequisites.sh\"",
16
+ "gates": ["review"]
17
+ },
18
+ {
19
+ "id": "scaffold",
20
+ "title": "アプリの雛形",
21
+ "description": "Worker の API、D1、Vue の画面、Workers の上で動く試験の土台を一つのフォルダに作る。",
22
+ "skill": "skills/scaffold",
23
+ "check": "sh \"$BLUEPRINT_BASE/checks/scaffold.sh\""
24
+ },
25
+ {
26
+ "id": "data",
27
+ "title": "データの形",
28
+ "description": "仕様の「扱うもの」を D1 の表にし、マイグレーションと試験を書く。",
29
+ "skill": "skills/data",
30
+ "check": "sh \"$BLUEPRINT_BASE/checks/tests-pass.sh\" data"
31
+ },
32
+ {
33
+ "id": "api",
34
+ "title": "API",
35
+ "description": "仕様の操作を Worker の API にし、入力を検査し、試験を書く。",
36
+ "skill": "skills/api",
37
+ "check": "sh \"$BLUEPRINT_BASE/checks/tests-pass.sh\" api"
38
+ },
39
+ {
40
+ "id": "ui",
41
+ "title": "画面",
42
+ "description": "仕様の画面を Vue で作り、API とつなぐ。",
43
+ "skill": "skills/ui",
44
+ "check": "sh \"$BLUEPRINT_BASE/checks/tests-pass.sh\" ui"
45
+ },
46
+ {
47
+ "id": "auth",
48
+ "title": "ログイン",
49
+ "description": "仕様のログインの方式を入れる。公開したアプリは誰でも開けるので、ログインなしなら何を公開してよいかを仕様どおりに絞る。",
50
+ "skill": "skills/auth",
51
+ "check": "sh \"$BLUEPRINT_BASE/checks/tests-pass.sh\" auth"
52
+ },
53
+ {
54
+ "id": "run-check",
55
+ "title": "起動して確かめる",
56
+ "description": "ビルドして wrangler dev で手元に起動し、画面と API が応えることを確かめる。",
57
+ "skill": "skills/run-check",
58
+ "check": "sh \"$BLUEPRINT_BASE/checks/smoke.sh\""
59
+ },
60
+ {
61
+ "id": "security",
62
+ "title": "セキュリティ診断",
63
+ "description": "OWASP Top 10:2025 に沿って作ったものを攻撃する側の目で読み、突ける穴を直して試験で確かめ、報告を書く。判定は手元に起動した Worker に、攻撃と同じ形のリクエスト(別のサイトからの変更、壊れた JSON)を送って確かめる。",
64
+ "skill": "skills/security",
65
+ "check": "sh \"$BLUEPRINT_BASE/checks/security.sh\""
66
+ },
67
+ {
68
+ "id": "deploy",
69
+ "title": "Cloudflare に公開",
70
+ "description": "D1 のデータベースを作ってマイグレーションを当て、wrangler deploy で公開する。公開した URL が今回のビルドを配っていることを確かめる。",
71
+ "skill": "skills/deploy",
72
+ "check": "sh \"$BLUEPRINT_BASE/checks/deploy-check.sh\"",
73
+ "gates": ["deploy-production", "credential"]
74
+ },
75
+ {
76
+ "id": "handover",
77
+ "title": "使い方の説明と引き継ぎ",
78
+ "description": "公開した URL、手元での動かし方、次に直すときの手順、データのバックアップを README と使い始め方に書く。",
79
+ "skill": "skills/handover",
80
+ "check": "sh \"$BLUEPRINT_BASE/checks/handover.sh\""
81
+ }
82
+ ]
83
+ }
@@ -0,0 +1,10 @@
1
+ # Cloudflare 構成 — 必ず詰める点
2
+
3
+ - [ ] **公開すると誰でも開ける**。ログインなしにするなら、何を誰に見せてよいかを仕様どおりに絞る。身内だけで使うなら、ログインか Cloudflare Access で入口を閉じる
4
+ - [ ] **SQL は必ずバインド**(`prepare("… ?").bind(value)`)。文字列をつないで SQL を作らない
5
+ - [ ] **入力は API の入口で検査する**(型・長さ・必須)。画面側の検査だけに頼らない
6
+ - [ ] **状態を変える API は、同じオリジンからの呼び出しだけ受ける**(別のサイトから勝手に呼ばれないように)
7
+ - [ ] **セキュリティヘッダーを付ける**。API の応答は Worker で、画面のファイルは `public/_headers` で(静的なファイルは Worker を通らない)。`Content-Security-Policy` に `frame-ancestors 'none'`、`X-Content-Type-Options: nosniff`
8
+ - [ ] **パスワードを保存するなら、平文で保存しない**(Web Crypto の PBKDF2 で塩付きのハッシュにする)
9
+ - [ ] **秘密の値は wrangler の secret に置く**(`wrangler secret put`)。手元では `.dev.vars` に置き、`.dev.vars` を `.gitignore` に入れる。コードや `wrangler.jsonc` に書かない
10
+ - [ ] **データのバックアップ**の手順を README に書く(`wrangler d1 export`)
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: blueprint-cloudflare-api
3
+ description: "Build the spec's operations as the Worker's API, with validation and tests."
4
+ ---
5
+
6
+ # API
7
+
8
+ 1. One route per operation in the spec's API table, in the Worker's `/api/*` handler.
9
+ 2. At the entry, before routing and before sign-in:
10
+ - a POST / PUT / PATCH / DELETE whose `Origin` (or, without one, `Referer`) is not this Worker's own origin is
11
+ refused with **403**;
12
+ - a JSON body is read **once**, and a malformed one is refused with **400** for every path. A Worker reads bodies
13
+ per route by default, which leaves a malformed body to whatever the route does with it.
14
+ 3. Validate every body and parameter (zod is fine) and answer 400 with a message when it is wrong; 404 for a missing
15
+ row. Every error reaches one handler that answers JSON without a stack trace.
16
+ 4. `test/api.test.ts`: every route in both directions — the valid request succeeds with the right body, an invalid one
17
+ is refused with 400 — through `SELF.fetch`.
18
+
19
+ Done when the check passes: `yarn build` and `yarn test` succeed and `test/` has the api tests.
20
+
21
+ ## Always
22
+
23
+ - Read `.blueprint/spec.md` first. It is the agreed specification; do not widen it.
24
+ - When you need a decision, ask it through the blueprint question tool and stop. Do not guess.
25
+ - Do not run `git init`: a new repository loses the folder's trust and the next unattended step stops at Claude
26
+ Code's trust prompt. The user adds git themselves after the build if they want it.
27
+ - Say you are done by stopping; the executor runs the check. Do not claim success yourself.
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: blueprint-cloudflare-auth
3
+ description: "Put in the sign-in the spec chose; a published Worker is open to anyone."
4
+ ---
5
+
6
+ # Sign-in
7
+
8
+ A published Worker can be opened by anyone who has its URL. Read the spec's sign-in choice.
9
+
10
+ - **None**: only what the spec says a visitor may see and do is reachable. Say so on the main screen's footer.
11
+ `test/auth.test.ts` proves an operation the spec reserves (if any) is refused without a session.
12
+ - **One shared password**: a sign-in screen; the password's PBKDF2 hash (Web Crypto, per-install salt) as a wrangler
13
+ secret (`.dev.vars` locally, in `.gitignore`); a signed, HttpOnly, Secure, SameSite=Lax session cookie; every `/api`
14
+ route except `/api/health` and sign-in refuses without it.
15
+ - **One account per person**: a `users` table (PBKDF2 hash with a per-user salt), sign-in and sign-out, roles if the
16
+ spec lists them, checked on the server for every route. If the spec says only a company's people may sign in,
17
+ Cloudflare Access in front of the Worker is the simplest door: say so in `.blueprint/open-questions.md`.
18
+
19
+ `test/auth.test.ts`: a request without a session is refused, with one it passes; a wrong password is refused.
20
+
21
+ Done when the check passes: `yarn build` and `yarn test` succeed and `test/` has the auth tests.
22
+
23
+ ## Always
24
+
25
+ - Read `.blueprint/spec.md` first. It is the agreed specification; do not widen it.
26
+ - When you need a decision, ask it through the blueprint question tool and stop. Do not guess.
27
+ - Do not run `git init`: a new repository loses the folder's trust and the next unattended step stops at Claude
28
+ Code's trust prompt. The user adds git themselves after the build if they want it.
29
+ - Say you are done by stopping; the executor runs the check. Do not claim success yourself.
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: blueprint-cloudflare-data
3
+ description: "Turn the spec's tables into D1 migrations with tests."
4
+ ---
5
+
6
+ # Data
7
+
8
+ 1. One migration file per change in `migrations/` (`0001_….sql`, …), applied by wrangler. Primary keys, NOT NULL and
9
+ UNIQUE as the spec's table says; foreign keys as the spec's relations say.
10
+ 2. A small data-access module per table with bound statements only (`env.DB.prepare("… ?").bind(value)`).
11
+ 3. `test/data.test.ts`: in the Workers runtime, every table can be written and read back, and every NOT NULL and UNIQUE
12
+ is refused when broken.
13
+
14
+ Done when the check passes: `yarn build` and `yarn test` succeed and `test/` has the data tests.
15
+
16
+ ## Always
17
+
18
+ - Read `.blueprint/spec.md` first. It is the agreed specification; do not widen it.
19
+ - When you need a decision, ask it through the blueprint question tool and stop. Do not guess.
20
+ - Do not run `git init`: a new repository loses the folder's trust and the next unattended step stops at Claude
21
+ Code's trust prompt. The user adds git themselves after the build if they want it.
22
+ - Say you are done by stopping; the executor runs the check. Do not claim success yourself.
@@ -0,0 +1,33 @@
1
+ ---
2
+ name: blueprint-cloudflare-deploy
3
+ description: "With approval, create the D1 database, apply the migrations, and publish with wrangler deploy."
4
+ ---
5
+
6
+ # Publish to Cloudflare
7
+
8
+ This step's gates are **deploy-production** and **credential**: the user has approved publishing, and signing in to
9
+ their Cloudflare account.
10
+
11
+ 1. `yarn wrangler whoami`. If not signed in, ask the person, through the blueprint question tool, to run
12
+ `yarn wrangler login` in this folder themselves and tell you when it is done. Never handle an API token.
13
+ 2. Create the database once: `yarn wrangler d1 create <name>` and put the `database_id` it prints into
14
+ `wrangler.jsonc`. Apply the migrations to it: `yarn wrangler d1 migrations apply DB --remote`. If `wrangler.jsonc`
15
+ binds R2 buckets, create each once too (`yarn wrangler r2 bucket create <bucket_name>`): the deploy fails on a
16
+ binding to a bucket that does not exist.
17
+ 3. Secrets the app needs: ask the person to run `yarn wrangler secret put <NAME>` themselves.
18
+ 4. `yarn build`, then give this build an id and ship it with the site:
19
+ `node -e 'console.log(require("crypto").randomUUID())' > .blueprint/build-id && cp .blueprint/build-id dist/client/blueprint-build.txt`,
20
+ then `yarn wrangler deploy`.
21
+ 5. Write the URL it published to (`https://…workers.dev`, or the custom domain the spec names) to
22
+ `.blueprint/deploy-url`, and tell the person the URL.
23
+
24
+ Done when the check passes: the URL in `.blueprint/deploy-url` serves the id this deploy wrote, answers
25
+ `/api/health`, sends a Content-Security-Policy, and its page renders text in headless Chrome.
26
+
27
+ ## Always
28
+
29
+ - Read `.blueprint/spec.md` first. It is the agreed specification; do not widen it.
30
+ - When you need a decision, ask it through the blueprint question tool and stop. Do not guess.
31
+ - Never enable billing, deploy to production, delete data or handle a credential unless this step's gates say the
32
+ user approved exactly that.
33
+ - Say you are done by stopping; the executor runs the check. Do not claim success yourself.