mirror-mind 0.0.1 → 0.33.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 (474) hide show
  1. package/.claude/hooks/log-assistant-reply.sh +85 -0
  2. package/.claude/hooks/log-session-end.sh +85 -0
  3. package/.claude/hooks/log-user-prompt.sh +85 -0
  4. package/.claude/hooks/mirror-inject.sh +85 -0
  5. package/.claude/hooks/session-start.sh +85 -0
  6. package/.gemini/hooks/log-assistant.sh +85 -0
  7. package/.gemini/hooks/log-user.sh +85 -0
  8. package/.gemini/hooks/session-end.sh +85 -0
  9. package/.gemini/hooks/session-start.sh +85 -0
  10. package/.pi/extensions/mirror-logger.ts +646 -0
  11. package/.pi/skills/mm-backup/SKILL.md +21 -0
  12. package/.pi/skills/mm-build/SKILL.md +1206 -0
  13. package/.pi/skills/mm-consolidate/SKILL.md +112 -0
  14. package/.pi/skills/mm-consult/SKILL.md +70 -0
  15. package/.pi/skills/mm-conversations/SKILL.md +16 -0
  16. package/.pi/skills/mm-discard/SKILL.md +39 -0
  17. package/.pi/skills/mm-explore/SKILL.md +255 -0
  18. package/.pi/skills/mm-help/SKILL.md +74 -0
  19. package/.pi/skills/mm-identity/SKILL.md +96 -0
  20. package/.pi/skills/mm-journal/SKILL.md +16 -0
  21. package/.pi/skills/mm-journey/SKILL.md +23 -0
  22. package/.pi/skills/mm-journeys/SKILL.md +15 -0
  23. package/.pi/skills/mm-memories/SKILL.md +17 -0
  24. package/.pi/skills/mm-mirror/SKILL.md +98 -0
  25. package/.pi/skills/mm-mute/SKILL.md +18 -0
  26. package/.pi/skills/mm-new/SKILL.md +18 -0
  27. package/.pi/skills/mm-recall/SKILL.md +17 -0
  28. package/.pi/skills/mm-release-notes/SKILL.md +52 -0
  29. package/.pi/skills/mm-seed/SKILL.md +19 -0
  30. package/.pi/skills/mm-shadow/SKILL.md +117 -0
  31. package/.pi/skills/mm-soul/SKILL.md +502 -0
  32. package/.pi/skills/mm-tasks/SKILL.md +24 -0
  33. package/.pi/skills/mm-update/SKILL.md +32 -0
  34. package/.pi/skills/mm-week/SKILL.md +28 -0
  35. package/.pi/skills/mm-welcome/SKILL.md +24 -0
  36. package/AGENTS.md +188 -0
  37. package/README.md +126 -9
  38. package/bin/loader.js +67 -0
  39. package/bin/mirror-hook.js +7 -0
  40. package/bin/mirror.js +7 -0
  41. package/docs/releases/index.md +100 -0
  42. package/docs/releases/pending-cutoffs.md +510 -0
  43. package/docs/releases/v0.10.0.md +123 -0
  44. package/docs/releases/v0.10.1.md +66 -0
  45. package/docs/releases/v0.10.10.md +51 -0
  46. package/docs/releases/v0.10.11.md +39 -0
  47. package/docs/releases/v0.10.12.md +57 -0
  48. package/docs/releases/v0.10.13.md +35 -0
  49. package/docs/releases/v0.10.14.md +42 -0
  50. package/docs/releases/v0.10.15.md +53 -0
  51. package/docs/releases/v0.10.16.md +35 -0
  52. package/docs/releases/v0.10.17.md +43 -0
  53. package/docs/releases/v0.10.2.md +64 -0
  54. package/docs/releases/v0.10.3.md +78 -0
  55. package/docs/releases/v0.10.4.md +42 -0
  56. package/docs/releases/v0.10.5.md +57 -0
  57. package/docs/releases/v0.10.6.md +47 -0
  58. package/docs/releases/v0.10.7.md +53 -0
  59. package/docs/releases/v0.10.8.md +49 -0
  60. package/docs/releases/v0.10.9.md +37 -0
  61. package/docs/releases/v0.11.0.md +86 -0
  62. package/docs/releases/v0.12.0.md +67 -0
  63. package/docs/releases/v0.13.0.md +64 -0
  64. package/docs/releases/v0.14.0.md +68 -0
  65. package/docs/releases/v0.15.0.md +48 -0
  66. package/docs/releases/v0.16.0.md +50 -0
  67. package/docs/releases/v0.16.1.md +55 -0
  68. package/docs/releases/v0.16.2.md +36 -0
  69. package/docs/releases/v0.17.0.md +50 -0
  70. package/docs/releases/v0.17.1.md +58 -0
  71. package/docs/releases/v0.17.2.md +42 -0
  72. package/docs/releases/v0.17.3.md +58 -0
  73. package/docs/releases/v0.18.0.md +84 -0
  74. package/docs/releases/v0.18.1.md +51 -0
  75. package/docs/releases/v0.18.2.md +69 -0
  76. package/docs/releases/v0.19.0.md +85 -0
  77. package/docs/releases/v0.19.1.md +46 -0
  78. package/docs/releases/v0.20.0.md +85 -0
  79. package/docs/releases/v0.21.0.md +61 -0
  80. package/docs/releases/v0.21.1.md +47 -0
  81. package/docs/releases/v0.22.0.md +77 -0
  82. package/docs/releases/v0.22.1.md +42 -0
  83. package/docs/releases/v0.23.0.md +105 -0
  84. package/docs/releases/v0.24.0.md +125 -0
  85. package/docs/releases/v0.25.0.md +107 -0
  86. package/docs/releases/v0.26.0.md +139 -0
  87. package/docs/releases/v0.26.1.md +65 -0
  88. package/docs/releases/v0.26.2.md +43 -0
  89. package/docs/releases/v0.27.0.md +63 -0
  90. package/docs/releases/v0.28.0.md +59 -0
  91. package/docs/releases/v0.29.0.md +61 -0
  92. package/docs/releases/v0.29.1.md +59 -0
  93. package/docs/releases/v0.29.2.md +59 -0
  94. package/docs/releases/v0.29.3.md +93 -0
  95. package/docs/releases/v0.30.0.md +75 -0
  96. package/docs/releases/v0.30.1.md +93 -0
  97. package/docs/releases/v0.30.2.md +66 -0
  98. package/docs/releases/v0.31.0.md +122 -0
  99. package/docs/releases/v0.31.1.md +142 -0
  100. package/docs/releases/v0.31.10.md +118 -0
  101. package/docs/releases/v0.31.11.md +88 -0
  102. package/docs/releases/v0.31.12.md +105 -0
  103. package/docs/releases/v0.31.13.md +99 -0
  104. package/docs/releases/v0.31.14.md +97 -0
  105. package/docs/releases/v0.31.2.md +115 -0
  106. package/docs/releases/v0.31.3.md +142 -0
  107. package/docs/releases/v0.31.4.md +117 -0
  108. package/docs/releases/v0.31.5.md +155 -0
  109. package/docs/releases/v0.31.6.md +132 -0
  110. package/docs/releases/v0.31.7.md +132 -0
  111. package/docs/releases/v0.31.8.md +120 -0
  112. package/docs/releases/v0.31.9.md +120 -0
  113. package/docs/releases/v0.32.0.md +261 -0
  114. package/docs/releases/v0.32.1.md +59 -0
  115. package/docs/releases/v0.33.0.md +164 -0
  116. package/docs/releases/v0.8.0.md +118 -0
  117. package/docs/releases/v0.9.0.md +98 -0
  118. package/docs/releases/v0.9.1.md +83 -0
  119. package/package.json +102 -6
  120. package/plugins/mirror-mind/.claude-plugin/plugin.json +17 -0
  121. package/plugins/mirror-mind/hooks/hooks.json +62 -0
  122. package/plugins/mirror-mind/hooks/log-assistant-reply.sh +98 -0
  123. package/plugins/mirror-mind/hooks/log-session-end.sh +98 -0
  124. package/plugins/mirror-mind/hooks/log-user-prompt.sh +98 -0
  125. package/plugins/mirror-mind/hooks/mirror-inject.sh +98 -0
  126. package/plugins/mirror-mind/hooks/session-start.sh +98 -0
  127. package/plugins/mirror-mind/skills/mm-backup/SKILL.md +27 -0
  128. package/plugins/mirror-mind/skills/mm-build/SKILL.md +1206 -0
  129. package/plugins/mirror-mind/skills/mm-consolidate/SKILL.md +112 -0
  130. package/plugins/mirror-mind/skills/mm-consult/SKILL.md +83 -0
  131. package/plugins/mirror-mind/skills/mm-conversations/SKILL.md +17 -0
  132. package/plugins/mirror-mind/skills/mm-discard/SKILL.md +40 -0
  133. package/plugins/mirror-mind/skills/mm-explore/SKILL.md +241 -0
  134. package/plugins/mirror-mind/skills/mm-help/SKILL.md +104 -0
  135. package/plugins/mirror-mind/skills/mm-identity/SKILL.md +96 -0
  136. package/plugins/mirror-mind/skills/mm-journal/SKILL.md +21 -0
  137. package/plugins/mirror-mind/skills/mm-journey/SKILL.md +37 -0
  138. package/plugins/mirror-mind/skills/mm-journeys/SKILL.md +15 -0
  139. package/plugins/mirror-mind/skills/mm-memories/SKILL.md +20 -0
  140. package/plugins/mirror-mind/skills/mm-mirror/SKILL.md +67 -0
  141. package/plugins/mirror-mind/skills/mm-mute/SKILL.md +18 -0
  142. package/plugins/mirror-mind/skills/mm-new/SKILL.md +18 -0
  143. package/plugins/mirror-mind/skills/mm-recall/SKILL.md +17 -0
  144. package/plugins/mirror-mind/skills/mm-release-notes/SKILL.md +31 -0
  145. package/plugins/mirror-mind/skills/mm-seed/SKILL.md +15 -0
  146. package/plugins/mirror-mind/skills/mm-shadow/SKILL.md +117 -0
  147. package/plugins/mirror-mind/skills/mm-soul/SKILL.md +488 -0
  148. package/plugins/mirror-mind/skills/mm-tasks/SKILL.md +36 -0
  149. package/plugins/mirror-mind/skills/mm-update/SKILL.md +30 -0
  150. package/plugins/mirror-mind/skills/mm-week/SKILL.md +46 -0
  151. package/plugins/mirror-mind/skills/mm-welcome/SKILL.md +22 -0
  152. package/scripts/codex-hooks/session-end.sh +85 -0
  153. package/scripts/codex-hooks/session-start.sh +85 -0
  154. package/scripts/codex-mirror.sh +48 -0
  155. package/templates/identity/README.md +119 -0
  156. package/templates/identity/ego/behavior.yaml +52 -0
  157. package/templates/identity/ego/constraints.yaml +15 -0
  158. package/templates/identity/ego/identity.yaml +43 -0
  159. package/templates/identity/journeys/personal-growth.yaml +39 -0
  160. package/templates/identity/organization/identity.yaml +24 -0
  161. package/templates/identity/organization/principles.yaml +17 -0
  162. package/templates/identity/personas/coach.yaml +94 -0
  163. package/templates/identity/personas/designer.yaml +93 -0
  164. package/templates/identity/personas/doctor.yaml +96 -0
  165. package/templates/identity/personas/engineer.yaml +77 -0
  166. package/templates/identity/personas/financial.yaml +97 -0
  167. package/templates/identity/personas/prompt-engineer.yaml +122 -0
  168. package/templates/identity/personas/researcher.yaml +88 -0
  169. package/templates/identity/personas/strategist.yaml +91 -0
  170. package/templates/identity/personas/teacher.yaml +95 -0
  171. package/templates/identity/personas/therapist.yaml +91 -0
  172. package/templates/identity/personas/thinker.yaml +74 -0
  173. package/templates/identity/personas/writer.yaml +76 -0
  174. package/templates/identity/self/config.yaml +16 -0
  175. package/templates/identity/self/soul.yaml +105 -0
  176. package/templates/identity/user/identity.yaml +30 -0
  177. package/ts/src/backup/staging.ts +62 -0
  178. package/ts/src/backup/zipBackup.ts +267 -0
  179. package/ts/src/backup/zipReader.ts +109 -0
  180. package/ts/src/backup/zipWriter.ts +148 -0
  181. package/ts/src/builder/activeCheckpoint.ts +134 -0
  182. package/ts/src/builder/aggregateStatus.ts +34 -0
  183. package/ts/src/builder/approve.ts +149 -0
  184. package/ts/src/builder/argv.ts +292 -0
  185. package/ts/src/builder/ariadMethod.ts +536 -0
  186. package/ts/src/builder/artifacts/artifactSurfaces.ts +106 -0
  187. package/ts/src/builder/artifacts/artifactWriter.ts +113 -0
  188. package/ts/src/builder/artifacts/closureArtifacts.ts +199 -0
  189. package/ts/src/builder/artifacts/deliveryStoryArtifacts.ts +316 -0
  190. package/ts/src/builder/artifacts/planArtifacts.ts +242 -0
  191. package/ts/src/builder/artifacts/scaffoldSections.ts +273 -0
  192. package/ts/src/builder/artifacts/scaffoldState.ts +227 -0
  193. package/ts/src/builder/artifacts/storyFiles.ts +61 -0
  194. package/ts/src/builder/artifacts/storyIndex.ts +166 -0
  195. package/ts/src/builder/cadence.ts +59 -0
  196. package/ts/src/builder/card.ts +192 -0
  197. package/ts/src/builder/checkpointRefused.ts +66 -0
  198. package/ts/src/builder/closure.ts +964 -0
  199. package/ts/src/builder/commands.ts +2053 -0
  200. package/ts/src/builder/cursorPosition.ts +176 -0
  201. package/ts/src/builder/cursorTransitions.ts +77 -0
  202. package/ts/src/builder/deliveryCursor.ts +577 -0
  203. package/ts/src/builder/deliveryStoryClosure.ts +521 -0
  204. package/ts/src/builder/deliveryStoryPlan.ts +536 -0
  205. package/ts/src/builder/deliveryStoryReady.ts +160 -0
  206. package/ts/src/builder/deliveryStoryRoadmapClosure.ts +151 -0
  207. package/ts/src/builder/expand.ts +336 -0
  208. package/ts/src/builder/flowUnit.ts +339 -0
  209. package/ts/src/builder/homeSurface.ts +122 -0
  210. package/ts/src/builder/implementationGuard.ts +125 -0
  211. package/ts/src/builder/index.ts +11 -0
  212. package/ts/src/builder/lifecycleRefusal.ts +92 -0
  213. package/ts/src/builder/lifecycleRibbon.ts +159 -0
  214. package/ts/src/builder/load.ts +313 -0
  215. package/ts/src/builder/methodAdoption.ts +113 -0
  216. package/ts/src/builder/methodDefinition.ts +429 -0
  217. package/ts/src/builder/methodInspection.ts +371 -0
  218. package/ts/src/builder/plan.ts +408 -0
  219. package/ts/src/builder/planPreauthorization.ts +265 -0
  220. package/ts/src/builder/prepare.ts +225 -0
  221. package/ts/src/builder/projectPaths.ts +64 -0
  222. package/ts/src/builder/pull.ts +356 -0
  223. package/ts/src/builder/pullCandidates.ts +463 -0
  224. package/ts/src/builder/pullCandidatesRender.ts +238 -0
  225. package/ts/src/builder/refinementField.ts +93 -0
  226. package/ts/src/builder/releaseIntent.ts +173 -0
  227. package/ts/src/builder/resumeState.ts +84 -0
  228. package/ts/src/builder/resumeSurface.ts +149 -0
  229. package/ts/src/builder/roadmapGrammar.ts +300 -0
  230. package/ts/src/builder/roadmapScan.ts +97 -0
  231. package/ts/src/builder/roadmapScope.ts +210 -0
  232. package/ts/src/builder/scopePhrases.ts +146 -0
  233. package/ts/src/builder/storyPaths.ts +196 -0
  234. package/ts/src/builder/storyPlanPreauthorization.ts +287 -0
  235. package/ts/src/builder/surfaceProtocol.ts +42 -0
  236. package/ts/src/builder/templateGeneration.ts +148 -0
  237. package/ts/src/builder/transition.ts +178 -0
  238. package/ts/src/consult/args.ts +99 -0
  239. package/ts/src/consult/core.ts +122 -0
  240. package/ts/src/consult/modelCatalog.ts +55 -0
  241. package/ts/src/consult/render.ts +47 -0
  242. package/ts/src/conversation/append.ts +414 -0
  243. package/ts/src/conversation/backfill.ts +387 -0
  244. package/ts/src/conversation/closeTail.ts +467 -0
  245. package/ts/src/conversation/extraction.ts +333 -0
  246. package/ts/src/conversation/extractionDriver.ts +130 -0
  247. package/ts/src/conversation/extractionRun.ts +70 -0
  248. package/ts/src/conversation/hookPayload.ts +51 -0
  249. package/ts/src/conversation/journeyInference.ts +165 -0
  250. package/ts/src/conversation/journeyRepair.ts +114 -0
  251. package/ts/src/conversation/lifecycleFaces.ts +197 -0
  252. package/ts/src/conversation/lifecycleWrites.ts +313 -0
  253. package/ts/src/conversation/listing.ts +77 -0
  254. package/ts/src/conversation/logger.ts +851 -0
  255. package/ts/src/conversation/loggerCli.ts +321 -0
  256. package/ts/src/conversation/loggerRuntime.ts +259 -0
  257. package/ts/src/conversation/metadataLifecycle.ts +443 -0
  258. package/ts/src/conversation/recall.ts +66 -0
  259. package/ts/src/conversation/sessionComposites.ts +258 -0
  260. package/ts/src/conversation/sessionImport.ts +90 -0
  261. package/ts/src/conversation/transcriptBackfill.ts +141 -0
  262. package/ts/src/cultivation/applyActions.ts +241 -0
  263. package/ts/src/cultivation/cluster.ts +86 -0
  264. package/ts/src/cultivation/consolidationStore.ts +281 -0
  265. package/ts/src/cultivation/promptContext.ts +62 -0
  266. package/ts/src/cultivation/propose.ts +305 -0
  267. package/ts/src/cultivation/scan.ts +155 -0
  268. package/ts/src/db/backupGate.ts +47 -0
  269. package/ts/src/db/bootstrap.ts +108 -0
  270. package/ts/src/db/bootstrapLock.ts +223 -0
  271. package/ts/src/db/copyGuard.ts +60 -0
  272. package/ts/src/db/database.ts +397 -0
  273. package/ts/src/db/decode.ts +66 -0
  274. package/ts/src/db/ftsIntegrity.ts +41 -0
  275. package/ts/src/db/migrateOnOpen.ts +214 -0
  276. package/ts/src/db/migrations.ts +576 -0
  277. package/ts/src/db/readyOnOpen.ts +35 -0
  278. package/ts/src/db/rowDecode.ts +42 -0
  279. package/ts/src/db/schema.ts +432 -0
  280. package/ts/src/db/schemaInventory.ts +225 -0
  281. package/ts/src/db/schemaInventorySnapshot.ts +2107 -0
  282. package/ts/src/db/schemaState.ts +91 -0
  283. package/ts/src/db/schemaTsDivergence.ts +114 -0
  284. package/ts/src/descriptor/descriptorRead.ts +49 -0
  285. package/ts/src/docs/docsLint.ts +265 -0
  286. package/ts/src/explorer/handoff.ts +522 -0
  287. package/ts/src/explorer/render.ts +335 -0
  288. package/ts/src/explorer/story.ts +769 -0
  289. package/ts/src/explorer/transition.ts +72 -0
  290. package/ts/src/extensions/bindings.ts +170 -0
  291. package/ts/src/extensions/catalog.ts +249 -0
  292. package/ts/src/extensions/catalogCommands.ts +513 -0
  293. package/ts/src/extensions/catalogWrites.ts +652 -0
  294. package/ts/src/extensions/contextRuntime.ts +325 -0
  295. package/ts/src/extensions/dispatch.ts +374 -0
  296. package/ts/src/extensions/errors.ts +24 -0
  297. package/ts/src/extensions/manifest.ts +356 -0
  298. package/ts/src/extensions/migrations.ts +350 -0
  299. package/ts/src/extraction/conversation.ts +304 -0
  300. package/ts/src/extraction/conversationMetadata.ts +206 -0
  301. package/ts/src/extraction/fencing.ts +106 -0
  302. package/ts/src/extraction/json.ts +17 -0
  303. package/ts/src/extraction/prompts.ts +470 -0
  304. package/ts/src/frontDoor/args.ts +30 -0
  305. package/ts/src/frontDoor/argvShape.ts +106 -0
  306. package/ts/src/frontDoor/buildLoadRuntime.ts +139 -0
  307. package/ts/src/frontDoor/buildRoute.ts +398 -0
  308. package/ts/src/frontDoor/cli.ts +1872 -0
  309. package/ts/src/frontDoor/cloneRoleGuard.ts +98 -0
  310. package/ts/src/frontDoor/consultRoute.ts +50 -0
  311. package/ts/src/frontDoor/contentTailRoute.ts +239 -0
  312. package/ts/src/frontDoor/conversationLoggerRoute.ts +83 -0
  313. package/ts/src/frontDoor/cultivationRoute.ts +210 -0
  314. package/ts/src/frontDoor/dbPath.ts +131 -0
  315. package/ts/src/frontDoor/dbSafetyToolsRoute.ts +240 -0
  316. package/ts/src/frontDoor/exploreRoute.ts +572 -0
  317. package/ts/src/frontDoor/extensionCatalogRoute.ts +252 -0
  318. package/ts/src/frontDoor/frontDoorLog.ts +57 -0
  319. package/ts/src/frontDoor/identityWrite.ts +36 -0
  320. package/ts/src/frontDoor/journeyWriteRoute.ts +26 -0
  321. package/ts/src/frontDoor/lifecycleWriteRoute.ts +110 -0
  322. package/ts/src/frontDoor/liveBackup.ts +100 -0
  323. package/ts/src/frontDoor/mirrorModeRoute.ts +163 -0
  324. package/ts/src/frontDoor/namedRefusal.ts +35 -0
  325. package/ts/src/frontDoor/nodeSupport.ts +24 -0
  326. package/ts/src/frontDoor/render/consolidate.ts +199 -0
  327. package/ts/src/frontDoor/render/conversations.ts +33 -0
  328. package/ts/src/frontDoor/render/cultivationShared.ts +21 -0
  329. package/ts/src/frontDoor/render/descriptor.ts +20 -0
  330. package/ts/src/frontDoor/render/detectPersona.ts +50 -0
  331. package/ts/src/frontDoor/render/icons.ts +13 -0
  332. package/ts/src/frontDoor/render/identity.ts +59 -0
  333. package/ts/src/frontDoor/render/identityRows.ts +17 -0
  334. package/ts/src/frontDoor/render/inspectPersona.ts +53 -0
  335. package/ts/src/frontDoor/render/journeyStatus.ts +78 -0
  336. package/ts/src/frontDoor/render/journeys.ts +79 -0
  337. package/ts/src/frontDoor/render/list.ts +41 -0
  338. package/ts/src/frontDoor/render/memories.ts +67 -0
  339. package/ts/src/frontDoor/render/recall.ts +49 -0
  340. package/ts/src/frontDoor/render/shadow.ts +155 -0
  341. package/ts/src/frontDoor/render/tasks.ts +99 -0
  342. package/ts/src/frontDoor/render/tasksImportSync.ts +63 -0
  343. package/ts/src/frontDoor/render/week.ts +113 -0
  344. package/ts/src/frontDoor/routing.ts +1001 -0
  345. package/ts/src/frontDoor/runtimeRoute.ts +623 -0
  346. package/ts/src/frontDoor/searchRoute.ts +166 -0
  347. package/ts/src/frontDoor/seedPaths.ts +98 -0
  348. package/ts/src/frontDoor/sessionStartWelcomeRoute.ts +166 -0
  349. package/ts/src/frontDoor/soulRoute.ts +433 -0
  350. package/ts/src/frontDoor/stdinSource.ts +37 -0
  351. package/ts/src/frontDoor/tasksImportSyncRoute.ts +110 -0
  352. package/ts/src/frontDoor/tasksWriteRoute.ts +101 -0
  353. package/ts/src/frontDoor/usage.ts +176 -0
  354. package/ts/src/guards/claudePlugin.ts +298 -0
  355. package/ts/src/guards/packContents.ts +306 -0
  356. package/ts/src/guards/piSourcedSkills.ts +70 -0
  357. package/ts/src/guards/retiredRouteShadows.ts +57 -0
  358. package/ts/src/guards/retiredSurfaces.ts +564 -0
  359. package/ts/src/hooks/claude.ts +214 -0
  360. package/ts/src/hooks/codex.ts +45 -0
  361. package/ts/src/hooks/gemini.ts +126 -0
  362. package/ts/src/hooks/main.ts +109 -0
  363. package/ts/src/hooks/mirrorState.ts +71 -0
  364. package/ts/src/hooks/payload.ts +73 -0
  365. package/ts/src/hooks/runtime.ts +179 -0
  366. package/ts/src/hooks/runtimeDetection.ts +34 -0
  367. package/ts/src/identity/applyConsolidationIdentityUpdate.ts +67 -0
  368. package/ts/src/identity/descriptorGenerate.ts +162 -0
  369. package/ts/src/identity/identityEdit.ts +143 -0
  370. package/ts/src/identity/identityKey.ts +77 -0
  371. package/ts/src/identity/identityRead.ts +69 -0
  372. package/ts/src/identity/identityStore.ts +80 -0
  373. package/ts/src/identity/journeyListing.ts +53 -0
  374. package/ts/src/identity/personaInspect.ts +44 -0
  375. package/ts/src/identity/personaListing.ts +37 -0
  376. package/ts/src/identity/setIdentity.ts +64 -0
  377. package/ts/src/index.ts +231 -0
  378. package/ts/src/init/init.ts +111 -0
  379. package/ts/src/journey/journeyOptions.ts +131 -0
  380. package/ts/src/journey/journeyRemoval.ts +158 -0
  381. package/ts/src/journey/journeyStatus.ts +149 -0
  382. package/ts/src/journey/journeySyncFile.ts +87 -0
  383. package/ts/src/journey/journeyWrite.ts +203 -0
  384. package/ts/src/journey/parentJourney.ts +41 -0
  385. package/ts/src/journey/validateParentJourney.ts +55 -0
  386. package/ts/src/mcp/boundary.ts +151 -0
  387. package/ts/src/mcp/guards.ts +244 -0
  388. package/ts/src/mcp/main.ts +121 -0
  389. package/ts/src/mcp/payload.ts +141 -0
  390. package/ts/src/mcp/protocol.ts +141 -0
  391. package/ts/src/mcp/readModels.ts +241 -0
  392. package/ts/src/mcp/registry.ts +201 -0
  393. package/ts/src/mcp/serve.ts +120 -0
  394. package/ts/src/mcp/tools/deterministic.ts +137 -0
  395. package/ts/src/mcp/tools/providerCrossing.ts +187 -0
  396. package/ts/src/mcp/wire.ts +8 -0
  397. package/ts/src/memory/journal.ts +180 -0
  398. package/ts/src/memory/listing.ts +107 -0
  399. package/ts/src/memory/memoryWrite.ts +88 -0
  400. package/ts/src/memory/reinforcement.ts +35 -0
  401. package/ts/src/mirror/context.ts +161 -0
  402. package/ts/src/mirror/defaultResolution.ts +199 -0
  403. package/ts/src/mirror/mirrorForm.ts +43 -0
  404. package/ts/src/mirror/orchestration.ts +364 -0
  405. package/ts/src/mirror/reception.ts +194 -0
  406. package/ts/src/mirror/render.ts +75 -0
  407. package/ts/src/mirror/runtimeSession.ts +205 -0
  408. package/ts/src/mode/operatingMode.ts +148 -0
  409. package/ts/src/observability/callOutcome.ts +115 -0
  410. package/ts/src/observability/inspectLedger.ts +329 -0
  411. package/ts/src/observability/ledgerHooks.ts +126 -0
  412. package/ts/src/observability/llmCalls.ts +154 -0
  413. package/ts/src/persona/detectPersona.ts +81 -0
  414. package/ts/src/planning/promptAssembly.ts +111 -0
  415. package/ts/src/planning/weekPending.ts +81 -0
  416. package/ts/src/planning/weekPlan.ts +183 -0
  417. package/ts/src/planning/weekSave.ts +117 -0
  418. package/ts/src/providers/config.ts +189 -0
  419. package/ts/src/providers/cost.ts +64 -0
  420. package/ts/src/providers/credits.ts +204 -0
  421. package/ts/src/providers/embedding.ts +321 -0
  422. package/ts/src/providers/familyProviders.ts +147 -0
  423. package/ts/src/providers/llm.ts +334 -0
  424. package/ts/src/providers/openrouter.ts +244 -0
  425. package/ts/src/providers/redaction.ts +110 -0
  426. package/ts/src/providers/replay.ts +15 -0
  427. package/ts/src/providers/transport.ts +410 -0
  428. package/ts/src/repair/encodingRepair.ts +253 -0
  429. package/ts/src/runtime/backup.ts +151 -0
  430. package/ts/src/runtime/channel.ts +146 -0
  431. package/ts/src/runtime/config.ts +168 -0
  432. package/ts/src/runtime/diagnose.ts +686 -0
  433. package/ts/src/runtime/git.ts +510 -0
  434. package/ts/src/runtime/installKind.ts +220 -0
  435. package/ts/src/runtime/migrate.ts +154 -0
  436. package/ts/src/runtime/packageIdentity.ts +102 -0
  437. package/ts/src/runtime/releaseNotes.ts +244 -0
  438. package/ts/src/runtime/status.ts +557 -0
  439. package/ts/src/runtime/strategies/clone.ts +200 -0
  440. package/ts/src/runtime/strategies/package.ts +328 -0
  441. package/ts/src/runtime/treeRoot.ts +30 -0
  442. package/ts/src/runtime/update.ts +445 -0
  443. package/ts/src/runtime/updateGate.ts +40 -0
  444. package/ts/src/runtime/updatePipeline.ts +159 -0
  445. package/ts/src/runtime/version.ts +36 -0
  446. package/ts/src/runtime/wiring.ts +130 -0
  447. package/ts/src/search/memorySearch.ts +338 -0
  448. package/ts/src/search/ranker.ts +154 -0
  449. package/ts/src/seed/seed.ts +256 -0
  450. package/ts/src/seed/seedYaml.ts +133 -0
  451. package/ts/src/soul/apply.ts +218 -0
  452. package/ts/src/soul/harvest.ts +267 -0
  453. package/ts/src/soul/prompts/soul_beauty_voice.md +103 -0
  454. package/ts/src/soul/prompts/soul_self_voice.md +219 -0
  455. package/ts/src/soul/prompts/soul_wisdom_voice.md +121 -0
  456. package/ts/src/soul/prompts.ts +85 -0
  457. package/ts/src/soul/render.ts +358 -0
  458. package/ts/src/soul/state.ts +212 -0
  459. package/ts/src/soul/transition.ts +37 -0
  460. package/ts/src/tasks/journeyPathParse.ts +113 -0
  461. package/ts/src/tasks/taskImportSync.ts +142 -0
  462. package/ts/src/tasks/taskStore.ts +312 -0
  463. package/ts/src/tasks/weekView.ts +120 -0
  464. package/ts/src/util/clipCodePoints.ts +26 -0
  465. package/ts/src/util/paths.ts +63 -0
  466. package/ts/src/util/program.ts +9 -0
  467. package/ts/src/util/pyGenerators.ts +144 -0
  468. package/ts/src/util/pythonText.ts +317 -0
  469. package/ts/src/util/semver.ts +45 -0
  470. package/ts/src/util/shellWord.ts +25 -0
  471. package/ts/src/util/slug.ts +64 -0
  472. package/ts/src/welcome/card.ts +339 -0
  473. package/ts/src/welcome/statusLine.ts +101 -0
  474. package/ts/src/welcome/updateCache.ts +129 -0
@@ -0,0 +1,769 @@
1
+ // CV22.DS7.US7 plateau 2 — Exploratory Story state, ported from
2
+ // `src/memory/services/explorer_story.py` and
3
+ // `src/memory/storage/explorer_stories.py`.
4
+ //
5
+ // A story lives in TWO places at once and both are graded by
6
+ // `explorer-story.golden.json`:
7
+ //
8
+ // * the durable `exploratory_stories` row, whose four `*_json` columns are
9
+ // written `ensure_ascii=False` in INSERTION order (never sorted), and whose
10
+ // absent experiment/handoff is SQL NULL rather than the string `"null"`;
11
+ // * the `__explorer_story__:<journey>` runtime session, which every mutation
12
+ // still rewrites. The service docstring calls that payload "pre-DS8"; it
13
+ // understates it. `_store_story` writes both stores on every mutation, so
14
+ // it is a live dual write, and the read falls back to it whenever no
15
+ // durable row exists.
16
+ //
17
+ // Dropping that fallback is the failure this module exists to avoid: an install
18
+ // that has not written a story since DS8 keeps its work only there, and a port
19
+ // that reads the table alone returns "no story" and exits 0 — the
20
+ // `conversations append` class (CR055) arriving through a read.
21
+ //
22
+ // Absent from this module since CV22.DS10.TS1: the Journey projection. Mirror no
23
+ // longer publishes `.mirror/projections`, so a story mutation no longer carries a
24
+ // decision about refreshing one.
25
+
26
+ import type { Database, WritableDatabase } from "#db/database.ts";
27
+ import { getRuntimeSession, upsertRuntimeSession } from "#mirror/runtimeSession.ts";
28
+ import { pythonJsonDumps } from "#util/pyGenerators.ts";
29
+ import { pySplitLines, pyStrip, sliceCodePoints } from "#util/pythonText.ts";
30
+
31
+ export const EXPLORER_STORY_SESSION_PREFIX = "__explorer_story__:";
32
+
33
+ const ATTRACTOR_STATUSES = new Set(["proposed", "accepted"]);
34
+ const HANDOFF_READINESS = new Set(["proposed", "confirmed"]);
35
+ const STORY_STATUSES = new Set(["active", "archived", "promoted"]);
36
+
37
+ /** Python raises `ValueError`; the CLI catches it and exits 1. */
38
+ export class ExplorerStoryError extends Error {}
39
+
40
+ export interface ExplorerAttractor {
41
+ readonly label: string;
42
+ readonly description: string | null;
43
+ readonly status: string;
44
+ }
45
+
46
+ export interface ExplorerExperimentProposal {
47
+ readonly title: string;
48
+ readonly description: string | null;
49
+ readonly status: string;
50
+ }
51
+
52
+ export interface ExplorerBuilderHandoff {
53
+ readonly title: string;
54
+ readonly summary: string | null;
55
+ readonly readiness: string;
56
+ readonly artifactDir: string | null;
57
+ readonly indexPath: string | null;
58
+ readonly exploratoryStoryPath: string | null;
59
+ readonly handoffInfoPath: string | null;
60
+ readonly productDesignProposalPath: string | null;
61
+ readonly fullConversationPath: string | null;
62
+ }
63
+
64
+ export interface ExplorerSourceConversation {
65
+ readonly conversationId: string;
66
+ readonly title: string | null;
67
+ readonly role: string;
68
+ }
69
+
70
+ export interface ExplorerStory {
71
+ readonly journey: string;
72
+ readonly currentExploratoryStory: string | null;
73
+ readonly narrativeFieldSummary: string | null;
74
+ readonly lastStoryCard: string | null;
75
+ readonly attractors: readonly ExplorerAttractor[];
76
+ readonly experimentProposal: ExplorerExperimentProposal | null;
77
+ readonly builderHandoff: ExplorerBuilderHandoff | null;
78
+ readonly sourceConversations: readonly ExplorerSourceConversation[];
79
+ readonly id: string | null;
80
+ readonly title: string | null;
81
+ readonly status: string;
82
+ readonly createdAt: string | null;
83
+ readonly updatedAt: string | null;
84
+ readonly promotedAt: string | null;
85
+ readonly archivedAt: string | null;
86
+ }
87
+
88
+ /**
89
+ * Python's `_UNSET` sentinel. The three optional scalars distinguish "not
90
+ * passed" (keep the existing value) from "passed None or blank" (clear it), and
91
+ * a port that maps both to `undefined` turns every clear into a keep.
92
+ */
93
+ export const UNSET = Symbol("explorer.unset");
94
+ export type Settable = string | null | typeof UNSET;
95
+
96
+ /** Injected so the golden can advance them deterministically, as Python's are. */
97
+ export interface StoryClock {
98
+ now(): string;
99
+ uuid(): string;
100
+ }
101
+
102
+ // --- normalization ---------------------------------------------------------
103
+
104
+ /** Python `_string_or_none`: non-strings become null; blanks become null. */
105
+ function stringOrNone(value: unknown): string | null {
106
+ if (typeof value !== "string") return null;
107
+ const cleaned = pyStrip(value);
108
+ return cleaned === "" ? null : cleaned;
109
+ }
110
+
111
+ function validStatus(value: unknown): string {
112
+ return typeof value === "string" && ATTRACTOR_STATUSES.has(value) ? value : "proposed";
113
+ }
114
+
115
+ function validHandoffReadiness(value: unknown): string {
116
+ return typeof value === "string" && HANDOFF_READINESS.has(value) ? value : "proposed";
117
+ }
118
+
119
+ function validStoryStatus(value: unknown): string {
120
+ return typeof value === "string" && STORY_STATUSES.has(value) ? value : "active";
121
+ }
122
+
123
+ function normalizeJourney(journey: string): string {
124
+ const normalized = pyStrip(journey);
125
+ if (normalized === "") throw new ExplorerStoryError("journey must not be empty");
126
+ return normalized;
127
+ }
128
+
129
+ function sessionId(journey: string): string {
130
+ const normalized = pyStrip(journey);
131
+ if (normalized === "") throw new ExplorerStoryError("journey must not be empty");
132
+ return `${EXPLORER_STORY_SESSION_PREFIX}${normalized}`;
133
+ }
134
+
135
+ /** Python `_clean` then `_resolve_scalar`, composed. */
136
+ function resolveScalar(
137
+ value: Settable,
138
+ existing: string | null,
139
+ hasExisting: boolean,
140
+ ): string | null {
141
+ if (value === UNSET) return hasExisting ? existing : null;
142
+ return stringOrNone(value);
143
+ }
144
+
145
+ // --- parsing ---------------------------------------------------------------
146
+
147
+ function isRecord(value: unknown): value is Record<string, unknown> {
148
+ return typeof value === "object" && value !== null && !Array.isArray(value);
149
+ }
150
+
151
+ function parseAttractors(value: unknown): ExplorerAttractor[] {
152
+ if (!Array.isArray(value)) return [];
153
+ const attractors: ExplorerAttractor[] = [];
154
+ for (const item of value) {
155
+ if (!isRecord(item)) continue;
156
+ const label = stringOrNone(item.label);
157
+ if (!label) continue;
158
+ attractors.push({
159
+ label,
160
+ description: stringOrNone(item.description),
161
+ status: validStatus(item.status),
162
+ });
163
+ }
164
+ return attractors;
165
+ }
166
+
167
+ function parseExperiment(value: unknown): ExplorerExperimentProposal | null {
168
+ if (!isRecord(value)) return null;
169
+ const title = stringOrNone(value.title);
170
+ if (!title) return null;
171
+ return {
172
+ title,
173
+ description: stringOrNone(value.description),
174
+ status: validStatus(value.status),
175
+ };
176
+ }
177
+
178
+ function parseHandoff(value: unknown): ExplorerBuilderHandoff | null {
179
+ if (!isRecord(value)) return null;
180
+ const title = stringOrNone(value.title);
181
+ if (!title) return null;
182
+ return {
183
+ title,
184
+ summary: stringOrNone(value.summary),
185
+ readiness: validHandoffReadiness(value.readiness),
186
+ artifactDir: stringOrNone(value.artifact_dir),
187
+ indexPath: stringOrNone(value.index_path),
188
+ exploratoryStoryPath: stringOrNone(value.exploratory_story_path),
189
+ handoffInfoPath: stringOrNone(value.handoff_info_path),
190
+ productDesignProposalPath: stringOrNone(value.product_design_proposal_path),
191
+ fullConversationPath: stringOrNone(value.full_conversation_path),
192
+ };
193
+ }
194
+
195
+ function parseSourceConversations(value: unknown): ExplorerSourceConversation[] {
196
+ if (!Array.isArray(value)) return [];
197
+ const sources: ExplorerSourceConversation[] = [];
198
+ for (const item of value) {
199
+ if (!isRecord(item)) continue;
200
+ const conversationId = stringOrNone(item.conversation_id);
201
+ if (!conversationId) continue;
202
+ sources.push({
203
+ conversationId,
204
+ title: stringOrNone(item.title),
205
+ role: stringOrNone(item.role) ?? "source evidence",
206
+ });
207
+ }
208
+ return sources;
209
+ }
210
+
211
+ /** Python `_load_json`: a non-string, blank, or invalid column yields default. */
212
+ function loadJson(value: unknown, fallback: unknown): unknown {
213
+ if (typeof value !== "string" || pyStrip(value) === "") return fallback;
214
+ try {
215
+ return JSON.parse(value);
216
+ } catch {
217
+ return fallback;
218
+ }
219
+ }
220
+
221
+ // --- serialization ---------------------------------------------------------
222
+
223
+ function attractorToDict(attractor: ExplorerAttractor): Record<string, unknown> {
224
+ return { label: attractor.label, description: attractor.description, status: attractor.status };
225
+ }
226
+
227
+ function experimentToDict(
228
+ proposal: ExplorerExperimentProposal | null,
229
+ ): Record<string, unknown> | null {
230
+ if (proposal === null) return null;
231
+ return { title: proposal.title, description: proposal.description, status: proposal.status };
232
+ }
233
+
234
+ function handoffToDict(handoff: ExplorerBuilderHandoff | null): Record<string, unknown> | null {
235
+ if (handoff === null) return null;
236
+ return {
237
+ title: handoff.title,
238
+ summary: handoff.summary,
239
+ readiness: handoff.readiness,
240
+ artifact_dir: handoff.artifactDir,
241
+ index_path: handoff.indexPath,
242
+ exploratory_story_path: handoff.exploratoryStoryPath,
243
+ handoff_info_path: handoff.handoffInfoPath,
244
+ product_design_proposal_path: handoff.productDesignProposalPath,
245
+ full_conversation_path: handoff.fullConversationPath,
246
+ };
247
+ }
248
+
249
+ function sourceToDict(source: ExplorerSourceConversation): Record<string, unknown> {
250
+ return { conversation_id: source.conversationId, title: source.title, role: source.role };
251
+ }
252
+
253
+ // --- reads -----------------------------------------------------------------
254
+
255
+ function storyFromRow(row: Record<string, unknown>): ExplorerStory {
256
+ return {
257
+ journey: String(row.journey),
258
+ currentExploratoryStory: stringOrNone(row.current_story),
259
+ narrativeFieldSummary: stringOrNone(row.narrative_summary),
260
+ lastStoryCard: stringOrNone(row.last_story_card),
261
+ attractors: parseAttractors(loadJson(row.attractors_json, [])),
262
+ experimentProposal: parseExperiment(loadJson(row.experiment_proposal_json, null)),
263
+ builderHandoff: parseHandoff(loadJson(row.builder_handoff_json, null)),
264
+ sourceConversations: parseSourceConversations(loadJson(row.source_conversations_json, [])),
265
+ id: stringOrNone(row.id),
266
+ title: stringOrNone(row.title),
267
+ status: validStoryStatus(row.status),
268
+ createdAt: stringOrNone(row.created_at),
269
+ updatedAt: stringOrNone(row.updated_at),
270
+ promotedAt: stringOrNone(row.promoted_at),
271
+ archivedAt: stringOrNone(row.archived_at),
272
+ };
273
+ }
274
+
275
+ function activeRow(db: Database, journey: string): Record<string, unknown> | null {
276
+ const row = db
277
+ .prepare(
278
+ `SELECT * FROM exploratory_stories
279
+ WHERE journey = ? AND status = 'active'
280
+ ORDER BY updated_at DESC
281
+ LIMIT 1`,
282
+ )
283
+ .get(journey);
284
+ return row ?? null;
285
+ }
286
+
287
+ /**
288
+ * Port of `_get_runtime_explorer_story` — the fallback that keeps pre-DS8
289
+ * stories readable. Every malformed shape resolves to Python's result rather
290
+ * than throwing: invalid JSON, a non-object payload, and non-string fields all
291
+ * degrade quietly, because a corrupt column must not take down a read.
292
+ */
293
+ function runtimeStory(db: Database, journey: string): ExplorerStory | null {
294
+ const session = getRuntimeSession(db, sessionId(journey));
295
+ if (!session?.active || !session.metadata) return null;
296
+
297
+ let data: unknown;
298
+ try {
299
+ data = JSON.parse(session.metadata);
300
+ } catch {
301
+ return null;
302
+ }
303
+ if (!isRecord(data)) return null;
304
+
305
+ return {
306
+ journey,
307
+ currentExploratoryStory: stringOrNone(data.current_exploratory_story),
308
+ narrativeFieldSummary: stringOrNone(data.narrative_field_summary),
309
+ lastStoryCard: stringOrNone(data.last_story_card),
310
+ attractors: parseAttractors(data.attractors),
311
+ experimentProposal: parseExperiment(data.experiment_proposal),
312
+ builderHandoff: parseHandoff(data.builder_handoff),
313
+ sourceConversations: parseSourceConversations(data.source_conversations),
314
+ id: null,
315
+ title: null,
316
+ status: "active",
317
+ createdAt: null,
318
+ updatedAt: null,
319
+ promotedAt: null,
320
+ archivedAt: null,
321
+ };
322
+ }
323
+
324
+ /** Port of `get_explorer_story`: durable row first, legacy payload second. */
325
+ export function getExplorerStory(db: Database, journey: string): ExplorerStory | null {
326
+ const normalized = normalizeJourney(journey);
327
+ const row = activeRow(db, normalized);
328
+ if (row) return storyFromRow(row);
329
+ return runtimeStory(db, normalized);
330
+ }
331
+
332
+ /** Port of `list_explorer_stories`: active first, then promoted, then archived. */
333
+ export function listExplorerStories(db: Database, journey: string): ExplorerStory[] {
334
+ const normalized = normalizeJourney(journey);
335
+ return db
336
+ .prepare(
337
+ `SELECT * FROM exploratory_stories
338
+ WHERE journey = ?
339
+ ORDER BY
340
+ CASE status
341
+ WHEN 'active' THEN 0
342
+ WHEN 'promoted' THEN 1
343
+ WHEN 'archived' THEN 2
344
+ ELSE 3
345
+ END,
346
+ updated_at DESC`,
347
+ )
348
+ .all(normalized)
349
+ .map(storyFromRow);
350
+ }
351
+
352
+ // --- writes ----------------------------------------------------------------
353
+
354
+ /** Python `_derive_title`: first line of the story, 80 code points, stripped. */
355
+ function deriveTitle(story: ExplorerStory): string {
356
+ const text = story.currentExploratoryStory ?? story.narrativeFieldSummary ?? "Exploratory Story";
357
+ const firstLine = pySplitLines(pyStrip(text))[0] ?? "";
358
+ return pyStrip(sliceCodePoints(firstLine, 80)) || "Exploratory Story";
359
+ }
360
+
361
+ function storyPayload(story: ExplorerStory): Record<string, unknown> {
362
+ return {
363
+ id: story.id,
364
+ title: story.title,
365
+ status: story.status,
366
+ current_exploratory_story: story.currentExploratoryStory,
367
+ narrative_field_summary: story.narrativeFieldSummary,
368
+ last_story_card: story.lastStoryCard,
369
+ attractors: story.attractors.map(attractorToDict),
370
+ experiment_proposal: experimentToDict(story.experimentProposal),
371
+ builder_handoff: handoffToDict(story.builderHandoff),
372
+ source_conversations: story.sourceConversations.map(sourceToDict),
373
+ };
374
+ }
375
+
376
+ // The runtime-session row carries its OWN timestamp in Python: the session
377
+ // store calls its own `_now()`, not the one `storage/explorer_stories.py`
378
+ // imported. So these writes reuse the timestamp already minted for the durable
379
+ // row instead of asking the clock again — one mutation, one tick, exactly as
380
+ // the oracle consumes it. `runtime_sessions.updated_at` is not part of the
381
+ // graded contract; the metadata bytes and the `active` flag are.
382
+ function writeRuntimeStory(db: WritableDatabase, story: ExplorerStory, now: string): void {
383
+ upsertRuntimeSession(
384
+ db,
385
+ sessionId(story.journey),
386
+ {
387
+ interface: "explorer_story",
388
+ journey: story.journey,
389
+ active: true,
390
+ metadata: pythonJsonDumps(storyPayload(story)),
391
+ },
392
+ now,
393
+ );
394
+ }
395
+
396
+ function clearRuntimeStory(db: WritableDatabase, journey: string, now: string): void {
397
+ upsertRuntimeSession(
398
+ db,
399
+ sessionId(journey),
400
+ { interface: "explorer_story", journey, active: false, metadata: null },
401
+ now,
402
+ );
403
+ }
404
+
405
+ /**
406
+ * Port of `upsert_active_explorer_story_record`. `id` and `created_at` are
407
+ * inherited from the existing active row; only `updated_at` moves. The four
408
+ * JSON columns go through `pythonJsonDumps` for `ensure_ascii=False` insertion
409
+ * order, and an absent experiment/handoff is written as SQL NULL — the string
410
+ * `"null"` would be invisible to a parsed comparison and wrong on the wire.
411
+ */
412
+ function upsertActiveRow(
413
+ db: WritableDatabase,
414
+ story: ExplorerStory,
415
+ clock: StoryClock,
416
+ ): { story: ExplorerStory; now: string } {
417
+ const now = clock.now();
418
+ const existing = activeRow(db, story.journey);
419
+ const recordId = existing ? String(existing.id) : clock.uuid();
420
+ const createdAt = existing ? String(existing.created_at) : now;
421
+ const experiment = experimentToDict(story.experimentProposal);
422
+ const handoff = handoffToDict(story.builderHandoff);
423
+
424
+ db.prepare(
425
+ `INSERT INTO exploratory_stories
426
+ (id, journey, title, status, current_story, narrative_summary,
427
+ last_story_card, attractors_json, experiment_proposal_json,
428
+ builder_handoff_json, source_conversations_json, created_at, updated_at)
429
+ VALUES (?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?, ?, ?)
430
+ ON CONFLICT(id) DO UPDATE SET
431
+ title = excluded.title,
432
+ current_story = excluded.current_story,
433
+ narrative_summary = excluded.narrative_summary,
434
+ last_story_card = excluded.last_story_card,
435
+ attractors_json = excluded.attractors_json,
436
+ experiment_proposal_json = excluded.experiment_proposal_json,
437
+ builder_handoff_json = excluded.builder_handoff_json,
438
+ source_conversations_json = excluded.source_conversations_json,
439
+ updated_at = excluded.updated_at`,
440
+ ).run(
441
+ recordId,
442
+ story.journey,
443
+ story.title ?? deriveTitle(story),
444
+ story.currentExploratoryStory,
445
+ story.narrativeFieldSummary,
446
+ story.lastStoryCard,
447
+ pythonJsonDumps(story.attractors.map(attractorToDict)),
448
+ experiment === null ? null : pythonJsonDumps(experiment),
449
+ handoff === null ? null : pythonJsonDumps(handoff),
450
+ pythonJsonDumps(story.sourceConversations.map(sourceToDict)),
451
+ createdAt,
452
+ now,
453
+ );
454
+
455
+ const persisted = activeRow(db, story.journey);
456
+ if (persisted === null) {
457
+ // Defensive, and reachable: an id collision updates a non-active row
458
+ // without restoring `status='active'`. Python raises here too.
459
+ throw new ExplorerStoryError("failed to persist active Exploratory Story");
460
+ }
461
+ return { story: storyFromRow(persisted), now };
462
+ }
463
+
464
+ function emptyStory(journey: string): ExplorerStory {
465
+ return {
466
+ journey,
467
+ currentExploratoryStory: null,
468
+ narrativeFieldSummary: null,
469
+ lastStoryCard: null,
470
+ attractors: [],
471
+ experimentProposal: null,
472
+ builderHandoff: null,
473
+ sourceConversations: [],
474
+ id: null,
475
+ title: null,
476
+ status: "active",
477
+ createdAt: null,
478
+ updatedAt: null,
479
+ promotedAt: null,
480
+ archivedAt: null,
481
+ };
482
+ }
483
+
484
+ /** Port of `_store_story`: durable row, then the runtime payload. */
485
+ function storeStory(db: WritableDatabase, story: ExplorerStory, clock: StoryClock): StoryMutation {
486
+ const { story: persisted, now } = upsertActiveRow(db, story, clock);
487
+ writeRuntimeStory(db, persisted, now);
488
+ return { story: persisted };
489
+ }
490
+
491
+ function carryForward(existing: ExplorerStory | null, journey: string): ExplorerStory {
492
+ return existing ?? emptyStory(journey);
493
+ }
494
+
495
+ export interface StoryMutation {
496
+ readonly story: ExplorerStory;
497
+ }
498
+
499
+ /** Port of `update_explorer_story`. Omitted scalars keep; explicit ones clear. */
500
+ export function updateExplorerStory(
501
+ db: WritableDatabase,
502
+ journey: string,
503
+ clock: StoryClock,
504
+ fields: {
505
+ currentExploratoryStory?: Settable;
506
+ narrativeFieldSummary?: Settable;
507
+ lastStoryCard?: Settable;
508
+ } = {},
509
+ ): StoryMutation {
510
+ const normalized = normalizeJourney(journey);
511
+ const existing = getExplorerStory(db, normalized);
512
+ const base = carryForward(existing, normalized);
513
+ const hasExisting = existing !== null;
514
+
515
+ // Presence, never nullishness. `fields.x ?? UNSET` looks equivalent and is
516
+ // the bug: an explicit `null` means CLEAR, and `??` would fold it into the
517
+ // absent case, turning every clear into a keep. Python distinguishes the two
518
+ // with a sentinel default; the only faithful JavaScript equivalent is `in`.
519
+ const settable = (key: keyof typeof fields): Settable =>
520
+ key in fields ? (fields[key] as Settable) : UNSET;
521
+
522
+ return storeStory(
523
+ db,
524
+ {
525
+ ...base,
526
+ journey: normalized,
527
+ currentExploratoryStory: resolveScalar(
528
+ settable("currentExploratoryStory"),
529
+ base.currentExploratoryStory,
530
+ hasExisting,
531
+ ),
532
+ narrativeFieldSummary: resolveScalar(
533
+ settable("narrativeFieldSummary"),
534
+ base.narrativeFieldSummary,
535
+ hasExisting,
536
+ ),
537
+ lastStoryCard: resolveScalar(settable("lastStoryCard"), base.lastStoryCard, hasExisting),
538
+ title: hasExisting ? base.title : null,
539
+ },
540
+ clock,
541
+ );
542
+ }
543
+
544
+ function normalizeAttractor(attractor: {
545
+ label: string;
546
+ description?: string | null;
547
+ status?: string;
548
+ }): ExplorerAttractor {
549
+ const label = stringOrNone(attractor.label);
550
+ if (!label) throw new ExplorerStoryError("attractor label must not be empty");
551
+ return {
552
+ label,
553
+ description: stringOrNone(attractor.description),
554
+ status: validStatus(attractor.status),
555
+ };
556
+ }
557
+
558
+ export function setExplorerAttractors(
559
+ db: WritableDatabase,
560
+ journey: string,
561
+ clock: StoryClock,
562
+ attractors: readonly { label: string; description?: string | null; status?: string }[],
563
+ ): StoryMutation {
564
+ const normalized = normalizeJourney(journey);
565
+ const base = carryForward(getExplorerStory(db, normalized), normalized);
566
+ return storeStory(
567
+ db,
568
+ { ...base, journey: normalized, attractors: attractors.map(normalizeAttractor) },
569
+ clock,
570
+ );
571
+ }
572
+
573
+ export function setExplorerExperimentProposal(
574
+ db: WritableDatabase,
575
+ journey: string,
576
+ clock: StoryClock,
577
+ proposal: { title: string; description?: string | null; status?: string },
578
+ ): StoryMutation {
579
+ const normalized = normalizeJourney(journey);
580
+ const title = stringOrNone(proposal.title);
581
+ if (!title) throw new ExplorerStoryError("experiment title must not be empty");
582
+ const base = carryForward(getExplorerStory(db, normalized), normalized);
583
+ return storeStory(
584
+ db,
585
+ {
586
+ ...base,
587
+ journey: normalized,
588
+ experimentProposal: {
589
+ title,
590
+ description: stringOrNone(proposal.description),
591
+ status: validStatus(proposal.status),
592
+ },
593
+ },
594
+ clock,
595
+ );
596
+ }
597
+
598
+ export function setExplorerBuilderHandoff(
599
+ db: WritableDatabase,
600
+ journey: string,
601
+ clock: StoryClock,
602
+ handoff: {
603
+ title: string;
604
+ summary?: string | null;
605
+ readiness?: string;
606
+ artifactDir?: string | null;
607
+ indexPath?: string | null;
608
+ exploratoryStoryPath?: string | null;
609
+ handoffInfoPath?: string | null;
610
+ productDesignProposalPath?: string | null;
611
+ fullConversationPath?: string | null;
612
+ },
613
+ ): StoryMutation {
614
+ const normalized = normalizeJourney(journey);
615
+ const title = stringOrNone(handoff.title);
616
+ if (!title) throw new ExplorerStoryError("builder handoff title must not be empty");
617
+ const base = carryForward(getExplorerStory(db, normalized), normalized);
618
+ return storeStory(
619
+ db,
620
+ {
621
+ ...base,
622
+ journey: normalized,
623
+ builderHandoff: {
624
+ title,
625
+ summary: stringOrNone(handoff.summary),
626
+ readiness: validHandoffReadiness(handoff.readiness),
627
+ artifactDir: stringOrNone(handoff.artifactDir),
628
+ indexPath: stringOrNone(handoff.indexPath),
629
+ exploratoryStoryPath: stringOrNone(handoff.exploratoryStoryPath),
630
+ handoffInfoPath: stringOrNone(handoff.handoffInfoPath),
631
+ productDesignProposalPath: stringOrNone(handoff.productDesignProposalPath),
632
+ fullConversationPath: stringOrNone(handoff.fullConversationPath),
633
+ },
634
+ },
635
+ clock,
636
+ );
637
+ }
638
+
639
+ export function setExplorerSourceConversations(
640
+ db: WritableDatabase,
641
+ journey: string,
642
+ clock: StoryClock,
643
+ sources: readonly { conversationId: string; title?: string | null; role?: string }[],
644
+ ): StoryMutation {
645
+ const normalized = normalizeJourney(journey);
646
+ const base = carryForward(getExplorerStory(db, normalized), normalized);
647
+ const normalizedSources = sources.map((source) => {
648
+ const conversationId = stringOrNone(source.conversationId);
649
+ if (!conversationId) {
650
+ throw new ExplorerStoryError("source conversation id must not be empty");
651
+ }
652
+ return {
653
+ conversationId,
654
+ title: stringOrNone(source.title),
655
+ role: stringOrNone(source.role) ?? "source evidence",
656
+ };
657
+ });
658
+ return storeStory(
659
+ db,
660
+ { ...base, journey: normalized, sourceConversations: normalizedSources },
661
+ clock,
662
+ );
663
+ }
664
+
665
+ function transitionActive(
666
+ db: WritableDatabase,
667
+ journey: string,
668
+ clock: StoryClock,
669
+ status: "archived" | "promoted",
670
+ timestampColumn: "archived_at" | "promoted_at",
671
+ ): { story: ExplorerStory | null } {
672
+ const normalized = normalizeJourney(journey);
673
+ const existing = activeRow(db, normalized);
674
+
675
+ if (!existing) {
676
+ // Python clears the runtime payload even when nothing was active, and
677
+ // returns before minting a story timestamp.
678
+ clearRuntimeStory(db, normalized, clock.now());
679
+ return { story: null };
680
+ }
681
+
682
+ const now = clock.now();
683
+ db.prepare(
684
+ `UPDATE exploratory_stories
685
+ SET status = ?, ${timestampColumn} = ?, updated_at = ?
686
+ WHERE id = ?`,
687
+ ).run(status, now, now, String(existing.id));
688
+
689
+ const row = db.prepare("SELECT * FROM exploratory_stories WHERE id = ?").get(String(existing.id));
690
+ clearRuntimeStory(db, normalized, now);
691
+ return { story: row ? storyFromRow(row) : null };
692
+ }
693
+
694
+ export function archiveExplorerStory(
695
+ db: WritableDatabase,
696
+ journey: string,
697
+ clock: StoryClock,
698
+ ): { story: ExplorerStory | null } {
699
+ return transitionActive(db, journey, clock, "archived", "archived_at");
700
+ }
701
+
702
+ export function markExplorerStoryPromoted(
703
+ db: WritableDatabase,
704
+ journey: string,
705
+ clock: StoryClock,
706
+ ): { story: ExplorerStory | null } {
707
+ return transitionActive(db, journey, clock, "promoted", "promoted_at");
708
+ }
709
+
710
+ /** Port of `clear_explorer_story`: archive under the pre-DS8 command's name. */
711
+ export function clearExplorerStory(db: WritableDatabase, journey: string, clock: StoryClock): void {
712
+ archiveExplorerStory(db, journey, clock);
713
+ }
714
+
715
+ // --- projection refresh decision -------------------------------------------
716
+
717
+ // --- context render --------------------------------------------------------
718
+
719
+ /** Port of `render_explorer_story_context` — prompt injection, not a card. */
720
+ export function renderExplorerStoryContext(story: ExplorerStory): string {
721
+ const lines: string[] = ["=== △ Exploratory Story ===", `journey: ${story.journey}`];
722
+ if (story.id) lines.push(`id: ${story.id}`);
723
+ if (story.status) lines.push(`status: ${story.status}`);
724
+ if (story.title) lines.push(`title: ${story.title}`);
725
+ if (story.currentExploratoryStory) {
726
+ lines.push("", "current exploratory story:", story.currentExploratoryStory);
727
+ }
728
+ if (story.narrativeFieldSummary) {
729
+ lines.push("", "narrative field summary:", story.narrativeFieldSummary);
730
+ }
731
+ if (story.lastStoryCard) lines.push("", "last story card:", story.lastStoryCard);
732
+ if (story.attractors.length > 0) {
733
+ lines.push("", "attractors:");
734
+ for (const attractor of story.attractors) {
735
+ lines.push(`- ${attractor.label} [${attractor.status}]`);
736
+ if (attractor.description) lines.push(` ${attractor.description}`);
737
+ }
738
+ }
739
+ if (story.experimentProposal) {
740
+ lines.push(
741
+ "",
742
+ "experiment proposal:",
743
+ `${story.experimentProposal.title} [${story.experimentProposal.status}]`,
744
+ );
745
+ if (story.experimentProposal.description) lines.push(story.experimentProposal.description);
746
+ }
747
+ if (story.sourceConversations.length > 0) {
748
+ lines.push("", "source conversations:");
749
+ for (const source of story.sourceConversations) {
750
+ const title = source.title ? ` — ${source.title}` : "";
751
+ lines.push(`- ${source.conversationId}${title} [${source.role}]`);
752
+ }
753
+ }
754
+ if (story.builderHandoff) {
755
+ lines.push(
756
+ "",
757
+ "builder handoff:",
758
+ `${story.builderHandoff.title} [${story.builderHandoff.readiness}]`,
759
+ );
760
+ if (story.builderHandoff.artifactDir) {
761
+ lines.push(`artifact_dir: ${story.builderHandoff.artifactDir}`);
762
+ }
763
+ if (story.builderHandoff.indexPath) lines.push(`index: ${story.builderHandoff.indexPath}`);
764
+ if (story.builderHandoff.fullConversationPath) {
765
+ lines.push(`full conversation: ${story.builderHandoff.fullConversationPath}`);
766
+ }
767
+ }
768
+ return lines.join("\n");
769
+ }