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,851 @@
1
+ // CV22.DS7.US5 slice A — deterministic conversation-logger core.
2
+ //
3
+ // Ports the no-LLM paths of `src/memory/cli/conversation_logger.py`
4
+ // (log_user_message, log_assistant_message, _generate_title, mute flag) and
5
+ // the transactional `runtime_session.get_or_create_conversation` service.
6
+ // Byte-shapes follow Python exactly: title truncation, whitespace collapse,
7
+ // and `json.dumps`-style metadata separators (see `pythonJsonDumps`).
8
+ //
9
+ // The LLM-backed tails of this family (extraction, close-time metadata
10
+ // finalization) are slice C/D scope behind the replay transport; nothing in
11
+ // this module may call a provider.
12
+
13
+ import { closeSync, existsSync, mkdirSync, openSync, rmSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import {
16
+ lastAssistantMessageOf,
17
+ parseHookPayloadObject,
18
+ promptOf,
19
+ sessionIdOf,
20
+ subagentTypeOf,
21
+ turnIdOf,
22
+ } from "#conversation/hookPayload.ts";
23
+ import { backfillAssistantMessages } from "#conversation/transcriptBackfill.ts";
24
+ import { type WritableDatabase, withTransaction } from "#db/database.ts";
25
+ import { mirrorForm } from "#mirror/mirrorForm.ts";
26
+ import {
27
+ decodeMetadata,
28
+ getRuntimeSession,
29
+ type RuntimeSessionRow,
30
+ resolveRuntimeSessionId,
31
+ upsertRuntimeSession,
32
+ } from "#mirror/runtimeSession.ts";
33
+ import { expandHome } from "#util/paths.ts";
34
+ import { pythonJsonDumps } from "#util/pyGenerators.ts";
35
+ import { codePointLength, sliceCodePoints } from "#util/pythonText.ts";
36
+
37
+ export interface LoggerDeps {
38
+ newId: () => string;
39
+ nowIso: () => string;
40
+ }
41
+
42
+ /**
43
+ * The DS7/DS8 close seam. Python's `end_conversation` writes `ended_at`
44
+ * deterministically, then runs two LLM-backed tails: extraction (conditional)
45
+ * and close-time metadata finalization (always, in a `finally`). Both are
46
+ * injected so slice A stays provider-free; slice C/D supply the real
47
+ * implementations behind the replay transport.
48
+ */
49
+ export interface CloseHooks {
50
+ runExtraction?: (db: WritableDatabase, conversationId: string) => Promise<void> | void;
51
+ finalizeMetadata?: (db: WritableDatabase, conversationId: string) => Promise<void> | void;
52
+ }
53
+
54
+ // PARITY NOTE for every call site below: Python's store upsert is asymmetric.
55
+ // `closed_at`/`metadata` use an `_UNSET` sentinel, so passing None means SET
56
+ // NULL. `conversation_id`/`interface`/`persona`/`journey`/`active` default to
57
+ // None meaning PRESERVE. The TS helper uses undefined=preserve, null=clear, so
58
+ // a Python `persona=None` maps to `persona: undefined` here — never `null`.
59
+
60
+ export interface SessionConversationOptions {
61
+ interface: string;
62
+ persona?: string | null;
63
+ journey?: string | null;
64
+ title?: string | null;
65
+ }
66
+
67
+ /**
68
+ * Python `_generate_title`: first line, hard 80 cap, word-boundary 60 cut.
69
+ * Both cuts are by code point (Python slicing), so an emoji counts once.
70
+ */
71
+ export function generateTitle(content: string): string {
72
+ const text = sliceCodePoints(content.trim().split("\n")[0] ?? "", 80);
73
+ if (codePointLength(text) <= 60) return text;
74
+ const cut = sliceCodePoints(text, 60);
75
+ const boundary = cut.lastIndexOf(" ");
76
+ return `${boundary === -1 ? cut : cut.slice(0, boundary)}...`;
77
+ }
78
+
79
+ /** Python `ConversationService._clean_title`: collapse whitespace, bound length. */
80
+ export function cleanTitle(title: string): string {
81
+ const clean = title.trim().split(/\s+/).filter(Boolean).join(" ");
82
+ if (!clean) throw new Error("title is required");
83
+ if (clean.length > 160) throw new Error("title must be at most 160 characters");
84
+ return clean;
85
+ }
86
+
87
+ function titleMetadata(
88
+ rawMetadata: string | null,
89
+ source: string,
90
+ status: string,
91
+ previousTitle: string | null,
92
+ ): Record<string, unknown> {
93
+ const metadata = decodeMetadata(rawMetadata);
94
+ if (previousTitle && previousTitle !== metadata.previous_title) {
95
+ metadata.previous_title = previousTitle;
96
+ }
97
+ metadata.title_source = source;
98
+ metadata.title_status = status;
99
+ return metadata;
100
+ }
101
+
102
+ /**
103
+ * Python `ConversationService.set_provisional_title` for an exact id.
104
+ * (The Python service also resolves id prefixes; that fallback belongs to the
105
+ * title CLI operations and is not reachable from the logger paths, so it is
106
+ * deliberately not ported here.)
107
+ */
108
+ export function setProvisionalTitle(
109
+ db: WritableDatabase,
110
+ conversationId: string,
111
+ title: string,
112
+ ): void {
113
+ const clean = cleanTitle(title);
114
+ const row = db
115
+ .prepare("SELECT title, metadata FROM conversations WHERE id = ?")
116
+ .get(conversationId);
117
+ if (!row) throw new Error(`Conversation '${conversationId}' not found`);
118
+ const metadata = titleMetadata(
119
+ typeof row.metadata === "string" ? row.metadata : null,
120
+ "first_user",
121
+ "provisional",
122
+ typeof row.title === "string" ? row.title : null,
123
+ );
124
+ db.prepare("UPDATE conversations SET title = ?, metadata = ? WHERE id = ?").run(
125
+ clean,
126
+ pythonJsonDumps(metadata),
127
+ conversationId,
128
+ );
129
+ }
130
+
131
+ /**
132
+ * Python `RuntimeSessionService.get_or_create_conversation`: return the bound
133
+ * live conversation or create-and-bind atomically under BEGIN IMMEDIATE.
134
+ * Persona/journey come from the caller only — never from the session row.
135
+ */
136
+ export function getOrCreateSessionConversation(
137
+ db: WritableDatabase,
138
+ sessionId: string,
139
+ options: SessionConversationOptions,
140
+ deps: LoggerDeps,
141
+ ): string {
142
+ return withTransaction(db, () => {
143
+ const bound = db
144
+ .prepare("SELECT conversation_id FROM runtime_sessions WHERE session_id = ?")
145
+ .get(sessionId);
146
+ const boundId =
147
+ bound && typeof bound.conversation_id === "string" ? bound.conversation_id : null;
148
+ if (boundId) {
149
+ const existing = db.prepare("SELECT id FROM conversations WHERE id = ?").get(boundId);
150
+ if (existing) return boundId;
151
+ }
152
+
153
+ const id = deps.newId();
154
+ const startedAt = deps.nowIso();
155
+ db.prepare(
156
+ `INSERT INTO conversations
157
+ (id, title, started_at, ended_at, interface, persona, journey, summary, tags, metadata)
158
+ VALUES (?, ?, ?, NULL, ?, ?, ?, NULL, NULL, NULL)`,
159
+ ).run(
160
+ id,
161
+ options.title ?? null,
162
+ startedAt,
163
+ options.interface,
164
+ options.persona ?? null,
165
+ options.journey ?? null,
166
+ );
167
+ // Mirrors the service's conflict clause exactly: it does NOT touch
168
+ // mirror_active, hook_injected, started_at, or metadata on update.
169
+ db.prepare(
170
+ `INSERT INTO runtime_sessions
171
+ (session_id, conversation_id, interface, mirror_active, persona, journey,
172
+ hook_injected, active, started_at, updated_at, closed_at, metadata)
173
+ VALUES (?, ?, ?, 0, ?, ?, 0, 1, ?, ?, NULL, NULL)
174
+ ON CONFLICT(session_id) DO UPDATE SET
175
+ conversation_id = excluded.conversation_id,
176
+ interface = excluded.interface,
177
+ persona = excluded.persona,
178
+ journey = excluded.journey,
179
+ active = 1,
180
+ closed_at = NULL,
181
+ updated_at = excluded.updated_at`,
182
+ ).run(
183
+ sessionId,
184
+ id,
185
+ options.interface,
186
+ options.persona ?? null,
187
+ options.journey ?? null,
188
+ startedAt,
189
+ startedAt,
190
+ );
191
+ return id;
192
+ });
193
+ }
194
+
195
+ function discardMarkerActive(session: RuntimeSessionRow | null): boolean {
196
+ return Boolean(decodeMetadata(session?.metadata ?? null).discard_current_conversation);
197
+ }
198
+
199
+ /**
200
+ * What a message row may carry beside its text. The runtime's turn id (US8)
201
+ * is the one field today: written by the prompt and Stop hooks, read by the
202
+ * reply rule, and NULL for every message logged another way, as before.
203
+ */
204
+ export interface MessageMetadata {
205
+ readonly turn_id?: string;
206
+ }
207
+
208
+ export interface LogMessageOptions {
209
+ interface: string;
210
+ metadata?: MessageMetadata | null;
211
+ }
212
+
213
+ function addMessage(
214
+ db: WritableDatabase,
215
+ conversationId: string,
216
+ role: "user" | "assistant",
217
+ content: string,
218
+ deps: LoggerDeps,
219
+ metadata: MessageMetadata | null = null,
220
+ ): void {
221
+ db.prepare(
222
+ `INSERT INTO messages (id, conversation_id, role, content, created_at, token_count, metadata)
223
+ VALUES (?, ?, ?, ?, ?, NULL, ?)`,
224
+ ).run(
225
+ deps.newId(),
226
+ conversationId,
227
+ role,
228
+ content,
229
+ deps.nowIso(),
230
+ metadata ? pythonJsonDumps(metadata) : null,
231
+ );
232
+ }
233
+
234
+ /** Python `log_user_message`: discard-marker reset, bind, title-on-first, append. */
235
+ export function logUserMessage(
236
+ db: WritableDatabase,
237
+ sessionId: string,
238
+ content: string,
239
+ options: LogMessageOptions,
240
+ deps: LoggerDeps,
241
+ ): void {
242
+ let existing = getRuntimeSession(db, sessionId);
243
+ if (discardMarkerActive(existing)) {
244
+ upsertRuntimeSession(
245
+ db,
246
+ sessionId,
247
+ { metadata: null, active: true, closedAt: null },
248
+ deps.nowIso(),
249
+ );
250
+ existing = getRuntimeSession(db, sessionId);
251
+ }
252
+ const isNew = existing === null || existing.conversationId === null;
253
+ const conversationId = getOrCreateSessionConversation(
254
+ db,
255
+ sessionId,
256
+ { interface: options.interface },
257
+ deps,
258
+ );
259
+ upsertRuntimeSession(
260
+ db,
261
+ sessionId,
262
+ { interface: options.interface, active: true, closedAt: null },
263
+ deps.nowIso(),
264
+ );
265
+ if (isNew) {
266
+ setProvisionalTitle(db, conversationId, generateTitle(content));
267
+ }
268
+ addMessage(db, conversationId, "user", content, deps, options.metadata ?? null);
269
+ }
270
+
271
+ /** Python `log_assistant_message`: silent no-op while the discard marker holds. */
272
+ export function logAssistantMessage(
273
+ db: WritableDatabase,
274
+ sessionId: string,
275
+ content: string,
276
+ options: LogMessageOptions,
277
+ deps: LoggerDeps,
278
+ ): void {
279
+ const existing = getRuntimeSession(db, sessionId);
280
+ if (discardMarkerActive(existing)) return;
281
+ const conversationId = getOrCreateSessionConversation(
282
+ db,
283
+ sessionId,
284
+ { interface: options.interface },
285
+ deps,
286
+ );
287
+ addMessage(db, conversationId, "assistant", content, deps, options.metadata ?? null);
288
+ }
289
+
290
+ /** Python `ConversationService.start_conversation`: always a fresh row. */
291
+ function startConversation(
292
+ db: WritableDatabase,
293
+ options: SessionConversationOptions,
294
+ deps: LoggerDeps,
295
+ ): string {
296
+ const id = deps.newId();
297
+ db.prepare(
298
+ `INSERT INTO conversations
299
+ (id, title, started_at, ended_at, interface, persona, journey, summary, tags, metadata)
300
+ VALUES (?, ?, ?, NULL, ?, ?, ?, NULL, NULL, NULL)`,
301
+ ).run(
302
+ id,
303
+ options.title ?? null,
304
+ deps.nowIso(),
305
+ options.interface,
306
+ options.persona ?? null,
307
+ options.journey ?? null,
308
+ );
309
+ return id;
310
+ }
311
+
312
+ /**
313
+ * Python `ConversationService.end_conversation`: deterministic close + tails.
314
+ *
315
+ * Async since CV22.DS7.US10 slice C′: both tails are genuinely asynchronous in
316
+ * TypeScript (they reach the LLM transport), and awaiting them here keeps ONE
317
+ * authority for the ordering contract — `ended_at` first, extraction second,
318
+ * finalization in a `finally`. Duplicating that contract in a second async
319
+ * close path would let the two drift on exactly the property that must not.
320
+ */
321
+ export async function endConversation(
322
+ db: WritableDatabase,
323
+ conversationId: string,
324
+ options: { extract: boolean },
325
+ deps: LoggerDeps,
326
+ hooks: CloseHooks = {},
327
+ ): Promise<void> {
328
+ db.prepare("UPDATE conversations SET ended_at = ? WHERE id = ?").run(
329
+ deps.nowIso(),
330
+ conversationId,
331
+ );
332
+ try {
333
+ if (options.extract) await hooks.runExtraction?.(db, conversationId);
334
+ } finally {
335
+ // Python finalizes in a `finally`: the orphan is closed and non-manual
336
+ // metadata finalized even when extraction fails.
337
+ await hooks.finalizeMetadata?.(db, conversationId);
338
+ }
339
+ }
340
+
341
+ /** Python `switch_conversation`: close the old conversation, bind a new one. */
342
+ export async function switchConversation(
343
+ db: WritableDatabase,
344
+ sessionId: string | null,
345
+ options: { persona?: string | null; journey?: string | null; envSessionId?: string | null },
346
+ deps: LoggerDeps,
347
+ hooks: CloseHooks = {},
348
+ ): Promise<string | null> {
349
+ const resolved = resolveRuntimeSessionId(db, sessionId, options.envSessionId ?? null);
350
+ if (!resolved) return null;
351
+
352
+ const runtimeSession = getRuntimeSession(db, resolved);
353
+ const oldConversationId = runtimeSession?.conversationId ?? null;
354
+ if (oldConversationId) {
355
+ await endConversation(db, oldConversationId, { extract: true }, deps, hooks);
356
+ }
357
+
358
+ // Python truthiness: an empty interface also falls back to claude_code.
359
+ const existingInterface = runtimeSession?.interface;
360
+ const interfaceName = existingInterface ? existingInterface : "claude_code";
361
+ const newConversationId = startConversation(
362
+ db,
363
+ {
364
+ interface: interfaceName,
365
+ persona: options.persona ?? null,
366
+ journey: options.journey ?? null,
367
+ },
368
+ deps,
369
+ );
370
+ upsertRuntimeSession(
371
+ db,
372
+ resolved,
373
+ {
374
+ conversationId: newConversationId,
375
+ interface: interfaceName,
376
+ persona: options.persona ?? undefined,
377
+ journey: options.journey ?? undefined,
378
+ active: true,
379
+ closedAt: null,
380
+ },
381
+ deps.nowIso(),
382
+ );
383
+ return newConversationId;
384
+ }
385
+
386
+ /** Python `end_session`: close the bound conversation and deactivate. */
387
+ export async function endSession(
388
+ db: WritableDatabase,
389
+ sessionId: string,
390
+ options: { extract: boolean },
391
+ deps: LoggerDeps,
392
+ hooks: CloseHooks = {},
393
+ ): Promise<void> {
394
+ const runtimeSession = getRuntimeSession(db, sessionId);
395
+ const conversationId = runtimeSession?.conversationId ?? null;
396
+ if (!conversationId) return;
397
+ await endConversation(db, conversationId, { extract: options.extract }, deps, hooks);
398
+ upsertRuntimeSession(db, sessionId, { active: false, closedAt: deps.nowIso() }, deps.nowIso());
399
+ }
400
+
401
+ /**
402
+ * Python `ConversationService.delete_conversations` for one resolved id:
403
+ * extracted memories survive with a nulled FK rather than cascading away.
404
+ */
405
+ function deleteConversation(db: WritableDatabase, conversationId: string): void {
406
+ db.prepare("DELETE FROM messages WHERE conversation_id = ?").run(conversationId);
407
+ db.prepare("DELETE FROM conversation_embeddings WHERE conversation_id = ?").run(conversationId);
408
+ db.prepare("UPDATE memories SET conversation_id = NULL WHERE conversation_id = ?").run(
409
+ conversationId,
410
+ );
411
+ db.prepare("UPDATE llm_calls SET conversation_id = NULL WHERE conversation_id = ?").run(
412
+ conversationId,
413
+ );
414
+ db.prepare("UPDATE runtime_sessions SET conversation_id = NULL WHERE conversation_id = ?").run(
415
+ conversationId,
416
+ );
417
+ db.prepare("DELETE FROM conversations WHERE id = ?").run(conversationId);
418
+ }
419
+
420
+ /** Python `discard_current_conversation`: delete and mark the session discarded. */
421
+ export function discardCurrentConversation(
422
+ db: WritableDatabase,
423
+ sessionId: string | null,
424
+ options: { interface: string },
425
+ deps: LoggerDeps,
426
+ ): string | null {
427
+ let resolved = sessionId;
428
+ if (resolved === null) {
429
+ const row = db
430
+ .prepare(
431
+ `SELECT session_id FROM runtime_sessions
432
+ WHERE active = 1 AND interface = ?
433
+ ORDER BY updated_at DESC`,
434
+ )
435
+ .get(options.interface);
436
+ resolved = row ? String(row.session_id) : null;
437
+ }
438
+ if (resolved === null) return null;
439
+
440
+ const runtimeSession = getRuntimeSession(db, resolved);
441
+ if (!runtimeSession || runtimeSession.conversationId === null) return null;
442
+ const conversationId = runtimeSession.conversationId;
443
+
444
+ withTransaction(db, () => {
445
+ deleteConversation(db, conversationId);
446
+ return null;
447
+ });
448
+
449
+ const existingInterface = runtimeSession.interface;
450
+ upsertRuntimeSession(
451
+ db,
452
+ resolved,
453
+ {
454
+ // Python passes conversation_id=None, which the store PRESERVES; the
455
+ // delete above already nulled it, so preserve and clear coincide here.
456
+ interface: existingInterface ? existingInterface : options.interface,
457
+ active: false,
458
+ metadata: pythonJsonDumps({ discard_current_conversation: true }),
459
+ },
460
+ deps.nowIso(),
461
+ );
462
+ return conversationId;
463
+ }
464
+
465
+ // --- Runtime hook entries -------------------------------------------------
466
+ //
467
+ // `hook_user_prompt` and `hook_session_end` are the hot path: they run on
468
+ // every message and every session close. Python wraps both in
469
+ // `except Exception: pass` and always exits 0, so a logging failure can never
470
+ // take down the runtime. These ports return a described outcome instead of
471
+ // exiting, keeping stdin/env at the CLI edge and the behavior testable; the
472
+ // CLI maps every outcome to exit code 0.
473
+
474
+ export type UserPromptHookOutcome =
475
+ | { action: "muted" }
476
+ | {
477
+ action: "skipped";
478
+ reason:
479
+ | "malformed_payload"
480
+ | "missing_session"
481
+ | "missing_prompt"
482
+ | "slash_command"
483
+ | "bare_mirror_form";
484
+ }
485
+ | { action: "logged"; sessionId: string }
486
+ | { action: "failed"; error: string };
487
+
488
+ export type StopHookOutcome =
489
+ | { action: "muted" }
490
+ | {
491
+ action: "skipped";
492
+ reason:
493
+ | "malformed_payload"
494
+ | "missing_session"
495
+ | "subagent"
496
+ | "empty_message"
497
+ | "discarded"
498
+ | "already_answered"
499
+ // Expected: the turn's prompt was a command, which history skips.
500
+ | "command_turn"
501
+ // Expected: the Stop names no turn (Grok's at session end).
502
+ | "session_event"
503
+ // Unexplained: the turn's prompt was neither logged nor skipped as a
504
+ // command -- the reply is lost. The only skip the caller reports.
505
+ | "nothing_waiting";
506
+ }
507
+ | { action: "logged"; sessionId: string; pairedBy: "turn_id" | "last_message" }
508
+ | { action: "failed"; error: string };
509
+
510
+ export interface HookLoggingOptions {
511
+ mirrorHome: string;
512
+ /** The runtime the hook detected (`claude_code`, `codex`, `grok_build`). */
513
+ interface?: string;
514
+ }
515
+
516
+ export type SessionEndHookOutcome =
517
+ | { action: "skipped"; reason: "malformed_payload" | "nothing_to_do" }
518
+ | { action: "ended"; sessionId: string; transcriptPath: string | null; backfilled: boolean }
519
+ | { action: "backfilled"; transcriptPath: string }
520
+ | { action: "failed"; error: string };
521
+
522
+ const parsePayload = parseHookPayloadObject;
523
+
524
+ function payloadString(payload: Record<string, unknown>, key: string): string {
525
+ const value = payload[key];
526
+ return typeof value === "string" ? value : "";
527
+ }
528
+
529
+ function turnMetadata(turnId: string): MessageMetadata | null {
530
+ return turnId ? { turn_id: turnId } : null;
531
+ }
532
+
533
+ /**
534
+ * The session-metadata key that names the last turn the prompt hook skipped as a
535
+ * command, so the Stop hook can tell that turn's reply -- expected to go
536
+ * unrecorded -- from a reply lost because its prompt never reached Mirror.
537
+ */
538
+ const SKIPPED_TURN_KEY = "skipped_turn_id";
539
+
540
+ /**
541
+ * Record the turn a command prompt occupied. `runtime_sessions.metadata` is a
542
+ * shared JSON object (the discard marker, the operating mode, Soul, Explorer,
543
+ * and Builder keep keys there), so the key is merged in, never written over.
544
+ */
545
+ function recordSkippedTurn(
546
+ db: WritableDatabase,
547
+ sessionId: string,
548
+ turnId: string,
549
+ deps: LoggerDeps,
550
+ ): void {
551
+ if (!turnId) return;
552
+ const metadata = decodeMetadata(getRuntimeSession(db, sessionId)?.metadata ?? null);
553
+ metadata[SKIPPED_TURN_KEY] = turnId;
554
+ upsertRuntimeSession(db, sessionId, { metadata: pythonJsonDumps(metadata) }, deps.nowIso());
555
+ }
556
+
557
+ /**
558
+ * Python `hook_user_prompt`: mute gate, slash-command skip, never throw.
559
+ *
560
+ * Two additions since CV21.DS2.US8. The session's label is stamped on every
561
+ * prompt, skipped or not, before anything else can create the conversation:
562
+ * the Mirror Mode path creates one with `session?.interface ?? "claude_code"`,
563
+ * and `guessRuntimeSessionId` ignores a session row with no interface, so a
564
+ * Grok session whose first turn was a Mirror turn used to be labelled
565
+ * `claude_code` and bound to someone else's session (DS12.TS4 L7, TS2 L9).
566
+ * And a Mirror turn is a conversation turn: its question is logged without
567
+ * the form, where every other slash command stays a command.
568
+ */
569
+ export function handleUserPromptHook(
570
+ db: WritableDatabase,
571
+ rawPayload: string,
572
+ options: HookLoggingOptions,
573
+ deps: LoggerDeps,
574
+ ): UserPromptHookOutcome {
575
+ try {
576
+ const payload = parsePayload(rawPayload);
577
+ if (!payload) return { action: "skipped", reason: "malformed_payload" };
578
+
579
+ const sessionId = sessionIdOf(payload);
580
+ const prompt = promptOf(payload);
581
+ if (!sessionId) return { action: "skipped", reason: "missing_session" };
582
+
583
+ const interfaceName = options.interface ?? "claude_code";
584
+ upsertRuntimeSession(db, sessionId, { interface: interfaceName }, deps.nowIso());
585
+
586
+ if (isMuted(options.mirrorHome)) return { action: "muted" };
587
+ if (!prompt) return { action: "skipped", reason: "missing_prompt" };
588
+
589
+ const mirror = mirrorForm(prompt);
590
+ let content = prompt;
591
+ if (mirror) {
592
+ if (!mirror.query) {
593
+ recordSkippedTurn(db, sessionId, turnIdOf(payload), deps);
594
+ return { action: "skipped", reason: "bare_mirror_form" };
595
+ }
596
+ content = mirror.query;
597
+ } else if (prompt.startsWith("/")) {
598
+ // Slash commands never enter conversation history, and neither does
599
+ // their reply: the turn is recorded so the Stop hook knows why.
600
+ recordSkippedTurn(db, sessionId, turnIdOf(payload), deps);
601
+ return { action: "skipped", reason: "slash_command" };
602
+ }
603
+
604
+ logUserMessage(
605
+ db,
606
+ sessionId,
607
+ content,
608
+ { interface: interfaceName, metadata: turnMetadata(turnIdOf(payload)) },
609
+ deps,
610
+ );
611
+ return { action: "logged", sessionId };
612
+ } catch (error) {
613
+ // Python swallows everything here; the runtime must survive a logging fault.
614
+ return { action: "failed", error: error instanceof Error ? error.message : String(error) };
615
+ }
616
+ }
617
+
618
+ interface MessageRow {
619
+ rowid: number;
620
+ role: string;
621
+ created_at: string;
622
+ metadata: string | null;
623
+ }
624
+
625
+ /** The conversation's last message, in the order the rows were written. */
626
+ function lastMessage(db: WritableDatabase, conversationId: string): MessageRow | null {
627
+ const row = db
628
+ .prepare(
629
+ `SELECT rowid, role, created_at, metadata FROM messages
630
+ WHERE conversation_id = ? ORDER BY created_at DESC, rowid DESC LIMIT 1`,
631
+ )
632
+ .get(conversationId);
633
+ return (row as MessageRow | undefined) ?? null;
634
+ }
635
+
636
+ /** The latest logged prompt of the turn, if the turn's prompt was logged. */
637
+ function promptOfTurn(
638
+ db: WritableDatabase,
639
+ conversationId: string,
640
+ turnId: string,
641
+ ): MessageRow | null {
642
+ const row = db
643
+ .prepare(
644
+ `SELECT rowid, role, created_at, metadata FROM messages
645
+ WHERE conversation_id = ? AND role = 'user' AND json_extract(metadata, '$.turn_id') = ?
646
+ ORDER BY created_at DESC, rowid DESC LIMIT 1`,
647
+ )
648
+ .get(conversationId, turnId);
649
+ return (row as MessageRow | undefined) ?? null;
650
+ }
651
+
652
+ /**
653
+ * Whether the turn already has its reply: an assistant row carrying the same
654
+ * turn id, after the prompt. Only a Stop reply carries one, so an assistant row
655
+ * written another way -- a summary `mirror mirror log` appended by hand, a
656
+ * transcript backfill -- never stands in for the turn's answer.
657
+ */
658
+ function answeredAfter(
659
+ db: WritableDatabase,
660
+ conversationId: string,
661
+ prompt: MessageRow,
662
+ turnId: string,
663
+ ): boolean {
664
+ const row = db
665
+ .prepare(
666
+ `SELECT 1 FROM messages
667
+ WHERE conversation_id = ? AND role = 'assistant'
668
+ AND json_extract(metadata, '$.turn_id') = ?
669
+ AND (created_at > ? OR (created_at = ? AND rowid > ?)) LIMIT 1`,
670
+ )
671
+ .get(conversationId, turnId, prompt.created_at, prompt.created_at, prompt.rowid);
672
+ return row !== undefined;
673
+ }
674
+
675
+ function turnIdOfRow(row: MessageRow): string {
676
+ const value = decodeMetadata(row.metadata).turn_id;
677
+ return typeof value === "string" ? value : "";
678
+ }
679
+
680
+ /**
681
+ * The Stop hook (CV21.DS2.US8): log the turn's final answer beside its prompt.
682
+ *
683
+ * A reply is logged when it answers a logged prompt, and never otherwise:
684
+ *
685
+ * * by turn id, when the payload carries one and a logged prompt carries the
686
+ * same -- unless a reply with that turn id already follows it, which is a
687
+ * repeat Stop (a Stop re-fired after another hook blocked). An assistant
688
+ * row without the turn id (a hand-logged summary) does not count;
689
+ * * else by position, when the conversation's last message is a logged
690
+ * prompt -- unless both sides carry a turn id and they differ, which means
691
+ * this turn's prompt was a command and an older prompt is waiting because
692
+ * its own Stop was lost; attaching the new answer to the old question
693
+ * would record a conversation that never happened.
694
+ *
695
+ * The handler never creates a conversation. When nothing is waiting, it says
696
+ * why: `command_turn` when the prompt hook skipped this turn as a command,
697
+ * `session_event` when the Stop names no turn (Grok's at session end), and
698
+ * `nothing_waiting` only when nothing explains it -- the turn's prompt never
699
+ * reached Mirror, the async race the plan names, a reply lost. The caller
700
+ * writes a `hooks.log` line for that one alone: `runtime diagnose` reads every
701
+ * line there as a failure, and an expected skip is not one (Debt Review,
702
+ * 2026-10-13).
703
+ */
704
+ export function handleStopHook(
705
+ db: WritableDatabase,
706
+ rawPayload: string,
707
+ options: HookLoggingOptions,
708
+ deps: LoggerDeps,
709
+ ): StopHookOutcome {
710
+ try {
711
+ const payload = parsePayload(rawPayload);
712
+ if (!payload) return { action: "skipped", reason: "malformed_payload" };
713
+
714
+ const sessionId = sessionIdOf(payload);
715
+ if (!sessionId) return { action: "skipped", reason: "missing_session" };
716
+ if (isMuted(options.mirrorHome)) return { action: "muted" };
717
+ if (subagentTypeOf(payload)) return { action: "skipped", reason: "subagent" };
718
+
719
+ const message = lastAssistantMessageOf(payload);
720
+ if (!message.trim()) return { action: "skipped", reason: "empty_message" };
721
+
722
+ const session = getRuntimeSession(db, sessionId);
723
+ if (discardMarkerActive(session)) return { action: "skipped", reason: "discarded" };
724
+ const turnId = turnIdOf(payload);
725
+ const nothingWaiting = (): StopHookOutcome => {
726
+ if (!turnId) return { action: "skipped", reason: "session_event" };
727
+ if (decodeMetadata(session?.metadata ?? null)[SKIPPED_TURN_KEY] === turnId) {
728
+ return { action: "skipped", reason: "command_turn" };
729
+ }
730
+ return { action: "skipped", reason: "nothing_waiting" };
731
+ };
732
+ const conversationId = session?.conversationId ?? null;
733
+ if (!conversationId) return nothingWaiting();
734
+
735
+ const interfaceName = options.interface ?? session?.interface ?? "claude_code";
736
+ const last = lastMessage(db, conversationId);
737
+
738
+ if (turnId) {
739
+ const prompt = promptOfTurn(db, conversationId, turnId);
740
+ if (prompt) {
741
+ if (answeredAfter(db, conversationId, prompt, turnId)) {
742
+ return { action: "skipped", reason: "already_answered" };
743
+ }
744
+ logAssistantMessage(
745
+ db,
746
+ sessionId,
747
+ message,
748
+ { interface: interfaceName, metadata: turnMetadata(turnId) },
749
+ deps,
750
+ );
751
+ return { action: "logged", sessionId, pairedBy: "turn_id" };
752
+ }
753
+ if (last && last.role === "user" && !turnIdOfRow(last)) {
754
+ logAssistantMessage(
755
+ db,
756
+ sessionId,
757
+ message,
758
+ { interface: interfaceName, metadata: turnMetadata(turnId) },
759
+ deps,
760
+ );
761
+ return { action: "logged", sessionId, pairedBy: "last_message" };
762
+ }
763
+ return nothingWaiting();
764
+ }
765
+
766
+ if (last && last.role === "user") {
767
+ logAssistantMessage(db, sessionId, message, { interface: interfaceName }, deps);
768
+ return { action: "logged", sessionId, pairedBy: "last_message" };
769
+ }
770
+ return nothingWaiting();
771
+ } catch (error) {
772
+ return { action: "failed", error: error instanceof Error ? error.message : String(error) };
773
+ }
774
+ }
775
+
776
+ /**
777
+ * Python `hook_session_end`: end the session when the payload names one, then
778
+ * backfill assistant turns from the transcript when it exists -- in that
779
+ * order, so the just-ended conversation's `ended_at` bounds the backfill.
780
+ *
781
+ * The two halves are independent: an empty `session_id` with a transcript
782
+ * still backfills (the session-less route, slice E), and a session without a
783
+ * transcript still ends. Everything is swallowed; the hook always exits 0.
784
+ */
785
+ export async function handleSessionEndHook(
786
+ db: WritableDatabase,
787
+ rawPayload: string,
788
+ options: { mirrorHome: string; claudeProjectDir: string | null; homeDir: string },
789
+ deps: LoggerDeps,
790
+ hooks: CloseHooks = {},
791
+ ): Promise<SessionEndHookOutcome> {
792
+ try {
793
+ const payload = parsePayload(rawPayload);
794
+ if (!payload) return { action: "skipped", reason: "malformed_payload" };
795
+
796
+ const sessionId = payloadString(payload, "session_id");
797
+ if (sessionId) await endSession(db, sessionId, { extract: true }, deps, hooks);
798
+
799
+ const transcriptPath = resolveTranscriptPath(
800
+ payloadString(payload, "transcript_path"),
801
+ sessionId,
802
+ options.claudeProjectDir,
803
+ options.homeDir,
804
+ );
805
+ const backfilled = transcriptPath !== null && existsSync(transcriptPath);
806
+ if (backfilled) backfillAssistantMessages(db, transcriptPath, deps);
807
+
808
+ if (sessionId) return { action: "ended", sessionId, transcriptPath, backfilled };
809
+ if (backfilled) return { action: "backfilled", transcriptPath };
810
+ return { action: "skipped", reason: "nothing_to_do" };
811
+ } catch (error) {
812
+ return { action: "failed", error: error instanceof Error ? error.message : String(error) };
813
+ }
814
+ }
815
+
816
+ /**
817
+ * Python's transcript fallback: an explicit payload path wins; otherwise
818
+ * derive `~/.claude/projects/<project-hash>/<session>.jsonl`, where the hash
819
+ * is the project dir with leading slashes stripped and `/` replaced by `-`.
820
+ */
821
+ export function resolveTranscriptPath(
822
+ payloadPath: string,
823
+ sessionId: string,
824
+ claudeProjectDir: string | null,
825
+ homeDir: string,
826
+ ): string | null {
827
+ if (payloadPath) return payloadPath;
828
+ if (!claudeProjectDir) return null;
829
+ const projectHash = claudeProjectDir.replace(/^\/+/, "").replaceAll("/", "-");
830
+ return join(homeDir, ".claude", "projects", projectHash, `${sessionId}.jsonl`);
831
+ }
832
+
833
+ function muteFlagPath(mirrorHome: string): string {
834
+ return join(expandHome(mirrorHome), "mute");
835
+ }
836
+
837
+ /** Python `is_muted` with an explicit mirror home. */
838
+ export function isMuted(mirrorHome: string): boolean {
839
+ return existsSync(muteFlagPath(mirrorHome));
840
+ }
841
+
842
+ /** Python `set_mute`: touch (no truncate) or unlink with missing_ok. */
843
+ export function setMute(on: boolean, mirrorHome: string): void {
844
+ const path = muteFlagPath(mirrorHome);
845
+ if (on) {
846
+ mkdirSync(expandHome(mirrorHome), { recursive: true });
847
+ closeSync(openSync(path, "a"));
848
+ return;
849
+ }
850
+ rmSync(path, { force: true });
851
+ }