mirror-mind 0.0.1 → 0.32.1

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 (467) hide show
  1. package/.claude/hooks/log-session-end.sh +85 -0
  2. package/.claude/hooks/log-user-prompt.sh +85 -0
  3. package/.claude/hooks/mirror-inject.sh +85 -0
  4. package/.claude/hooks/session-start.sh +85 -0
  5. package/.gemini/hooks/log-assistant.sh +85 -0
  6. package/.gemini/hooks/log-user.sh +85 -0
  7. package/.gemini/hooks/session-end.sh +85 -0
  8. package/.gemini/hooks/session-start.sh +85 -0
  9. package/.pi/extensions/mirror-logger.ts +561 -0
  10. package/.pi/skills/mm-backup/SKILL.md +21 -0
  11. package/.pi/skills/mm-build/SKILL.md +1206 -0
  12. package/.pi/skills/mm-consolidate/SKILL.md +112 -0
  13. package/.pi/skills/mm-consult/SKILL.md +70 -0
  14. package/.pi/skills/mm-conversations/SKILL.md +16 -0
  15. package/.pi/skills/mm-discard/SKILL.md +39 -0
  16. package/.pi/skills/mm-explore/SKILL.md +255 -0
  17. package/.pi/skills/mm-help/SKILL.md +74 -0
  18. package/.pi/skills/mm-identity/SKILL.md +96 -0
  19. package/.pi/skills/mm-journal/SKILL.md +16 -0
  20. package/.pi/skills/mm-journey/SKILL.md +23 -0
  21. package/.pi/skills/mm-journeys/SKILL.md +15 -0
  22. package/.pi/skills/mm-memories/SKILL.md +17 -0
  23. package/.pi/skills/mm-mirror/SKILL.md +98 -0
  24. package/.pi/skills/mm-mute/SKILL.md +18 -0
  25. package/.pi/skills/mm-new/SKILL.md +18 -0
  26. package/.pi/skills/mm-recall/SKILL.md +17 -0
  27. package/.pi/skills/mm-release-notes/SKILL.md +52 -0
  28. package/.pi/skills/mm-seed/SKILL.md +19 -0
  29. package/.pi/skills/mm-shadow/SKILL.md +117 -0
  30. package/.pi/skills/mm-soul/SKILL.md +502 -0
  31. package/.pi/skills/mm-tasks/SKILL.md +24 -0
  32. package/.pi/skills/mm-update/SKILL.md +32 -0
  33. package/.pi/skills/mm-week/SKILL.md +28 -0
  34. package/.pi/skills/mm-welcome/SKILL.md +24 -0
  35. package/AGENTS.md +188 -0
  36. package/README.md +126 -9
  37. package/bin/loader.js +67 -0
  38. package/bin/mirror-hook.js +7 -0
  39. package/bin/mirror.js +7 -0
  40. package/docs/releases/index.md +99 -0
  41. package/docs/releases/pending-cutoffs.md +510 -0
  42. package/docs/releases/v0.10.0.md +123 -0
  43. package/docs/releases/v0.10.1.md +66 -0
  44. package/docs/releases/v0.10.10.md +51 -0
  45. package/docs/releases/v0.10.11.md +39 -0
  46. package/docs/releases/v0.10.12.md +57 -0
  47. package/docs/releases/v0.10.13.md +35 -0
  48. package/docs/releases/v0.10.14.md +42 -0
  49. package/docs/releases/v0.10.15.md +53 -0
  50. package/docs/releases/v0.10.16.md +35 -0
  51. package/docs/releases/v0.10.17.md +43 -0
  52. package/docs/releases/v0.10.2.md +64 -0
  53. package/docs/releases/v0.10.3.md +78 -0
  54. package/docs/releases/v0.10.4.md +42 -0
  55. package/docs/releases/v0.10.5.md +57 -0
  56. package/docs/releases/v0.10.6.md +47 -0
  57. package/docs/releases/v0.10.7.md +53 -0
  58. package/docs/releases/v0.10.8.md +49 -0
  59. package/docs/releases/v0.10.9.md +37 -0
  60. package/docs/releases/v0.11.0.md +86 -0
  61. package/docs/releases/v0.12.0.md +67 -0
  62. package/docs/releases/v0.13.0.md +64 -0
  63. package/docs/releases/v0.14.0.md +68 -0
  64. package/docs/releases/v0.15.0.md +48 -0
  65. package/docs/releases/v0.16.0.md +50 -0
  66. package/docs/releases/v0.16.1.md +55 -0
  67. package/docs/releases/v0.16.2.md +36 -0
  68. package/docs/releases/v0.17.0.md +50 -0
  69. package/docs/releases/v0.17.1.md +58 -0
  70. package/docs/releases/v0.17.2.md +42 -0
  71. package/docs/releases/v0.17.3.md +58 -0
  72. package/docs/releases/v0.18.0.md +84 -0
  73. package/docs/releases/v0.18.1.md +51 -0
  74. package/docs/releases/v0.18.2.md +69 -0
  75. package/docs/releases/v0.19.0.md +85 -0
  76. package/docs/releases/v0.19.1.md +46 -0
  77. package/docs/releases/v0.20.0.md +85 -0
  78. package/docs/releases/v0.21.0.md +61 -0
  79. package/docs/releases/v0.21.1.md +47 -0
  80. package/docs/releases/v0.22.0.md +77 -0
  81. package/docs/releases/v0.22.1.md +42 -0
  82. package/docs/releases/v0.23.0.md +105 -0
  83. package/docs/releases/v0.24.0.md +125 -0
  84. package/docs/releases/v0.25.0.md +107 -0
  85. package/docs/releases/v0.26.0.md +139 -0
  86. package/docs/releases/v0.26.1.md +65 -0
  87. package/docs/releases/v0.26.2.md +43 -0
  88. package/docs/releases/v0.27.0.md +63 -0
  89. package/docs/releases/v0.28.0.md +59 -0
  90. package/docs/releases/v0.29.0.md +61 -0
  91. package/docs/releases/v0.29.1.md +59 -0
  92. package/docs/releases/v0.29.2.md +59 -0
  93. package/docs/releases/v0.29.3.md +93 -0
  94. package/docs/releases/v0.30.0.md +75 -0
  95. package/docs/releases/v0.30.1.md +93 -0
  96. package/docs/releases/v0.30.2.md +66 -0
  97. package/docs/releases/v0.31.0.md +122 -0
  98. package/docs/releases/v0.31.1.md +142 -0
  99. package/docs/releases/v0.31.10.md +118 -0
  100. package/docs/releases/v0.31.11.md +88 -0
  101. package/docs/releases/v0.31.12.md +105 -0
  102. package/docs/releases/v0.31.13.md +99 -0
  103. package/docs/releases/v0.31.14.md +97 -0
  104. package/docs/releases/v0.31.2.md +115 -0
  105. package/docs/releases/v0.31.3.md +142 -0
  106. package/docs/releases/v0.31.4.md +117 -0
  107. package/docs/releases/v0.31.5.md +155 -0
  108. package/docs/releases/v0.31.6.md +132 -0
  109. package/docs/releases/v0.31.7.md +132 -0
  110. package/docs/releases/v0.31.8.md +120 -0
  111. package/docs/releases/v0.31.9.md +120 -0
  112. package/docs/releases/v0.32.0.md +261 -0
  113. package/docs/releases/v0.32.1.md +59 -0
  114. package/docs/releases/v0.8.0.md +118 -0
  115. package/docs/releases/v0.9.0.md +98 -0
  116. package/docs/releases/v0.9.1.md +83 -0
  117. package/package.json +102 -6
  118. package/plugins/mirror-mind/.claude-plugin/plugin.json +13 -0
  119. package/plugins/mirror-mind/hooks/hooks.json +50 -0
  120. package/plugins/mirror-mind/hooks/log-session-end.sh +98 -0
  121. package/plugins/mirror-mind/hooks/log-user-prompt.sh +98 -0
  122. package/plugins/mirror-mind/hooks/mirror-inject.sh +98 -0
  123. package/plugins/mirror-mind/hooks/session-start.sh +98 -0
  124. package/plugins/mirror-mind/mcp/launch.sh +52 -0
  125. package/plugins/mirror-mind/skills/mm-backup/SKILL.md +27 -0
  126. package/plugins/mirror-mind/skills/mm-build/SKILL.md +1206 -0
  127. package/plugins/mirror-mind/skills/mm-consolidate/SKILL.md +112 -0
  128. package/plugins/mirror-mind/skills/mm-consult/SKILL.md +83 -0
  129. package/plugins/mirror-mind/skills/mm-conversations/SKILL.md +17 -0
  130. package/plugins/mirror-mind/skills/mm-discard/SKILL.md +40 -0
  131. package/plugins/mirror-mind/skills/mm-explore/SKILL.md +241 -0
  132. package/plugins/mirror-mind/skills/mm-help/SKILL.md +104 -0
  133. package/plugins/mirror-mind/skills/mm-identity/SKILL.md +96 -0
  134. package/plugins/mirror-mind/skills/mm-journal/SKILL.md +21 -0
  135. package/plugins/mirror-mind/skills/mm-journey/SKILL.md +37 -0
  136. package/plugins/mirror-mind/skills/mm-journeys/SKILL.md +15 -0
  137. package/plugins/mirror-mind/skills/mm-memories/SKILL.md +20 -0
  138. package/plugins/mirror-mind/skills/mm-mirror/SKILL.md +65 -0
  139. package/plugins/mirror-mind/skills/mm-mute/SKILL.md +18 -0
  140. package/plugins/mirror-mind/skills/mm-new/SKILL.md +18 -0
  141. package/plugins/mirror-mind/skills/mm-recall/SKILL.md +17 -0
  142. package/plugins/mirror-mind/skills/mm-release-notes/SKILL.md +31 -0
  143. package/plugins/mirror-mind/skills/mm-seed/SKILL.md +15 -0
  144. package/plugins/mirror-mind/skills/mm-shadow/SKILL.md +117 -0
  145. package/plugins/mirror-mind/skills/mm-soul/SKILL.md +488 -0
  146. package/plugins/mirror-mind/skills/mm-tasks/SKILL.md +36 -0
  147. package/plugins/mirror-mind/skills/mm-update/SKILL.md +30 -0
  148. package/plugins/mirror-mind/skills/mm-week/SKILL.md +46 -0
  149. package/plugins/mirror-mind/skills/mm-welcome/SKILL.md +22 -0
  150. package/scripts/codex-hooks/session-end.sh +85 -0
  151. package/scripts/codex-hooks/session-start.sh +85 -0
  152. package/scripts/codex-mirror.sh +48 -0
  153. package/templates/identity/README.md +119 -0
  154. package/templates/identity/ego/behavior.yaml +52 -0
  155. package/templates/identity/ego/constraints.yaml +15 -0
  156. package/templates/identity/ego/identity.yaml +43 -0
  157. package/templates/identity/journeys/personal-growth.yaml +39 -0
  158. package/templates/identity/organization/identity.yaml +24 -0
  159. package/templates/identity/organization/principles.yaml +17 -0
  160. package/templates/identity/personas/coach.yaml +94 -0
  161. package/templates/identity/personas/designer.yaml +93 -0
  162. package/templates/identity/personas/doctor.yaml +96 -0
  163. package/templates/identity/personas/engineer.yaml +77 -0
  164. package/templates/identity/personas/financial.yaml +97 -0
  165. package/templates/identity/personas/prompt-engineer.yaml +122 -0
  166. package/templates/identity/personas/researcher.yaml +88 -0
  167. package/templates/identity/personas/strategist.yaml +91 -0
  168. package/templates/identity/personas/teacher.yaml +95 -0
  169. package/templates/identity/personas/therapist.yaml +91 -0
  170. package/templates/identity/personas/thinker.yaml +74 -0
  171. package/templates/identity/personas/writer.yaml +76 -0
  172. package/templates/identity/self/config.yaml +16 -0
  173. package/templates/identity/self/soul.yaml +105 -0
  174. package/templates/identity/user/identity.yaml +30 -0
  175. package/ts/src/backup/staging.ts +62 -0
  176. package/ts/src/backup/zipBackup.ts +267 -0
  177. package/ts/src/backup/zipReader.ts +109 -0
  178. package/ts/src/backup/zipWriter.ts +148 -0
  179. package/ts/src/builder/activeCheckpoint.ts +134 -0
  180. package/ts/src/builder/aggregateStatus.ts +34 -0
  181. package/ts/src/builder/approve.ts +149 -0
  182. package/ts/src/builder/argv.ts +292 -0
  183. package/ts/src/builder/ariadMethod.ts +536 -0
  184. package/ts/src/builder/artifacts/artifactSurfaces.ts +106 -0
  185. package/ts/src/builder/artifacts/artifactWriter.ts +113 -0
  186. package/ts/src/builder/artifacts/closureArtifacts.ts +199 -0
  187. package/ts/src/builder/artifacts/deliveryStoryArtifacts.ts +316 -0
  188. package/ts/src/builder/artifacts/planArtifacts.ts +242 -0
  189. package/ts/src/builder/artifacts/scaffoldSections.ts +273 -0
  190. package/ts/src/builder/artifacts/scaffoldState.ts +227 -0
  191. package/ts/src/builder/artifacts/storyFiles.ts +61 -0
  192. package/ts/src/builder/artifacts/storyIndex.ts +166 -0
  193. package/ts/src/builder/cadence.ts +59 -0
  194. package/ts/src/builder/card.ts +192 -0
  195. package/ts/src/builder/checkpointRefused.ts +66 -0
  196. package/ts/src/builder/closure.ts +964 -0
  197. package/ts/src/builder/commands.ts +2053 -0
  198. package/ts/src/builder/cursorPosition.ts +176 -0
  199. package/ts/src/builder/cursorTransitions.ts +77 -0
  200. package/ts/src/builder/deliveryCursor.ts +577 -0
  201. package/ts/src/builder/deliveryStoryClosure.ts +521 -0
  202. package/ts/src/builder/deliveryStoryPlan.ts +536 -0
  203. package/ts/src/builder/deliveryStoryReady.ts +160 -0
  204. package/ts/src/builder/deliveryStoryRoadmapClosure.ts +151 -0
  205. package/ts/src/builder/expand.ts +336 -0
  206. package/ts/src/builder/flowUnit.ts +339 -0
  207. package/ts/src/builder/homeSurface.ts +122 -0
  208. package/ts/src/builder/implementationGuard.ts +125 -0
  209. package/ts/src/builder/index.ts +11 -0
  210. package/ts/src/builder/lifecycleRefusal.ts +92 -0
  211. package/ts/src/builder/lifecycleRibbon.ts +159 -0
  212. package/ts/src/builder/load.ts +313 -0
  213. package/ts/src/builder/methodAdoption.ts +113 -0
  214. package/ts/src/builder/methodDefinition.ts +429 -0
  215. package/ts/src/builder/methodInspection.ts +371 -0
  216. package/ts/src/builder/plan.ts +408 -0
  217. package/ts/src/builder/planPreauthorization.ts +265 -0
  218. package/ts/src/builder/prepare.ts +225 -0
  219. package/ts/src/builder/projectPaths.ts +64 -0
  220. package/ts/src/builder/pull.ts +356 -0
  221. package/ts/src/builder/pullCandidates.ts +463 -0
  222. package/ts/src/builder/pullCandidatesRender.ts +238 -0
  223. package/ts/src/builder/refinementField.ts +93 -0
  224. package/ts/src/builder/releaseIntent.ts +173 -0
  225. package/ts/src/builder/resumeState.ts +84 -0
  226. package/ts/src/builder/resumeSurface.ts +149 -0
  227. package/ts/src/builder/roadmapGrammar.ts +300 -0
  228. package/ts/src/builder/roadmapScan.ts +97 -0
  229. package/ts/src/builder/roadmapScope.ts +210 -0
  230. package/ts/src/builder/scopePhrases.ts +146 -0
  231. package/ts/src/builder/storyPaths.ts +196 -0
  232. package/ts/src/builder/storyPlanPreauthorization.ts +287 -0
  233. package/ts/src/builder/surfaceProtocol.ts +42 -0
  234. package/ts/src/builder/templateGeneration.ts +148 -0
  235. package/ts/src/builder/transition.ts +178 -0
  236. package/ts/src/consult/args.ts +99 -0
  237. package/ts/src/consult/core.ts +122 -0
  238. package/ts/src/consult/modelCatalog.ts +55 -0
  239. package/ts/src/consult/render.ts +47 -0
  240. package/ts/src/conversation/append.ts +414 -0
  241. package/ts/src/conversation/backfill.ts +387 -0
  242. package/ts/src/conversation/closeTail.ts +467 -0
  243. package/ts/src/conversation/extraction.ts +333 -0
  244. package/ts/src/conversation/extractionDriver.ts +130 -0
  245. package/ts/src/conversation/extractionRun.ts +70 -0
  246. package/ts/src/conversation/journeyInference.ts +165 -0
  247. package/ts/src/conversation/journeyRepair.ts +114 -0
  248. package/ts/src/conversation/lifecycleFaces.ts +197 -0
  249. package/ts/src/conversation/lifecycleWrites.ts +313 -0
  250. package/ts/src/conversation/listing.ts +77 -0
  251. package/ts/src/conversation/logger.ts +576 -0
  252. package/ts/src/conversation/loggerCli.ts +282 -0
  253. package/ts/src/conversation/loggerRuntime.ts +259 -0
  254. package/ts/src/conversation/metadataLifecycle.ts +443 -0
  255. package/ts/src/conversation/recall.ts +66 -0
  256. package/ts/src/conversation/sessionComposites.ts +258 -0
  257. package/ts/src/conversation/sessionImport.ts +90 -0
  258. package/ts/src/conversation/transcriptBackfill.ts +141 -0
  259. package/ts/src/cultivation/applyActions.ts +241 -0
  260. package/ts/src/cultivation/cluster.ts +86 -0
  261. package/ts/src/cultivation/consolidationStore.ts +281 -0
  262. package/ts/src/cultivation/promptContext.ts +62 -0
  263. package/ts/src/cultivation/propose.ts +305 -0
  264. package/ts/src/cultivation/scan.ts +155 -0
  265. package/ts/src/db/backupGate.ts +47 -0
  266. package/ts/src/db/bootstrap.ts +108 -0
  267. package/ts/src/db/bootstrapLock.ts +223 -0
  268. package/ts/src/db/copyGuard.ts +60 -0
  269. package/ts/src/db/database.ts +397 -0
  270. package/ts/src/db/decode.ts +66 -0
  271. package/ts/src/db/ftsIntegrity.ts +41 -0
  272. package/ts/src/db/migrateOnOpen.ts +214 -0
  273. package/ts/src/db/migrations.ts +576 -0
  274. package/ts/src/db/readyOnOpen.ts +35 -0
  275. package/ts/src/db/rowDecode.ts +42 -0
  276. package/ts/src/db/schema.ts +432 -0
  277. package/ts/src/db/schemaInventory.ts +225 -0
  278. package/ts/src/db/schemaInventorySnapshot.ts +2107 -0
  279. package/ts/src/db/schemaState.ts +91 -0
  280. package/ts/src/db/schemaTsDivergence.ts +114 -0
  281. package/ts/src/descriptor/descriptorRead.ts +49 -0
  282. package/ts/src/docs/docsLint.ts +265 -0
  283. package/ts/src/explorer/handoff.ts +522 -0
  284. package/ts/src/explorer/render.ts +335 -0
  285. package/ts/src/explorer/story.ts +769 -0
  286. package/ts/src/explorer/transition.ts +72 -0
  287. package/ts/src/extensions/bindings.ts +170 -0
  288. package/ts/src/extensions/catalog.ts +249 -0
  289. package/ts/src/extensions/catalogCommands.ts +513 -0
  290. package/ts/src/extensions/catalogWrites.ts +652 -0
  291. package/ts/src/extensions/contextRuntime.ts +325 -0
  292. package/ts/src/extensions/dispatch.ts +374 -0
  293. package/ts/src/extensions/errors.ts +24 -0
  294. package/ts/src/extensions/manifest.ts +356 -0
  295. package/ts/src/extensions/migrations.ts +350 -0
  296. package/ts/src/extraction/conversation.ts +304 -0
  297. package/ts/src/extraction/conversationMetadata.ts +206 -0
  298. package/ts/src/extraction/fencing.ts +106 -0
  299. package/ts/src/extraction/json.ts +17 -0
  300. package/ts/src/extraction/prompts.ts +470 -0
  301. package/ts/src/frontDoor/args.ts +30 -0
  302. package/ts/src/frontDoor/argvShape.ts +106 -0
  303. package/ts/src/frontDoor/buildLoadRuntime.ts +139 -0
  304. package/ts/src/frontDoor/buildRoute.ts +398 -0
  305. package/ts/src/frontDoor/cli.ts +1865 -0
  306. package/ts/src/frontDoor/cloneRoleGuard.ts +98 -0
  307. package/ts/src/frontDoor/consultRoute.ts +50 -0
  308. package/ts/src/frontDoor/contentTailRoute.ts +239 -0
  309. package/ts/src/frontDoor/conversationLoggerRoute.ts +73 -0
  310. package/ts/src/frontDoor/cultivationRoute.ts +210 -0
  311. package/ts/src/frontDoor/dbPath.ts +131 -0
  312. package/ts/src/frontDoor/dbSafetyToolsRoute.ts +240 -0
  313. package/ts/src/frontDoor/exploreRoute.ts +572 -0
  314. package/ts/src/frontDoor/extensionCatalogRoute.ts +252 -0
  315. package/ts/src/frontDoor/frontDoorLog.ts +57 -0
  316. package/ts/src/frontDoor/identityWrite.ts +36 -0
  317. package/ts/src/frontDoor/journeyWriteRoute.ts +26 -0
  318. package/ts/src/frontDoor/lifecycleWriteRoute.ts +110 -0
  319. package/ts/src/frontDoor/liveBackup.ts +100 -0
  320. package/ts/src/frontDoor/mirrorModeRoute.ts +163 -0
  321. package/ts/src/frontDoor/namedRefusal.ts +35 -0
  322. package/ts/src/frontDoor/nodeSupport.ts +24 -0
  323. package/ts/src/frontDoor/render/consolidate.ts +199 -0
  324. package/ts/src/frontDoor/render/conversations.ts +33 -0
  325. package/ts/src/frontDoor/render/cultivationShared.ts +21 -0
  326. package/ts/src/frontDoor/render/descriptor.ts +20 -0
  327. package/ts/src/frontDoor/render/detectPersona.ts +50 -0
  328. package/ts/src/frontDoor/render/icons.ts +13 -0
  329. package/ts/src/frontDoor/render/identity.ts +59 -0
  330. package/ts/src/frontDoor/render/identityRows.ts +17 -0
  331. package/ts/src/frontDoor/render/inspectPersona.ts +53 -0
  332. package/ts/src/frontDoor/render/journeyStatus.ts +78 -0
  333. package/ts/src/frontDoor/render/journeys.ts +79 -0
  334. package/ts/src/frontDoor/render/list.ts +41 -0
  335. package/ts/src/frontDoor/render/memories.ts +67 -0
  336. package/ts/src/frontDoor/render/recall.ts +49 -0
  337. package/ts/src/frontDoor/render/shadow.ts +155 -0
  338. package/ts/src/frontDoor/render/tasks.ts +99 -0
  339. package/ts/src/frontDoor/render/tasksImportSync.ts +63 -0
  340. package/ts/src/frontDoor/render/week.ts +113 -0
  341. package/ts/src/frontDoor/routing.ts +999 -0
  342. package/ts/src/frontDoor/runtimeRoute.ts +623 -0
  343. package/ts/src/frontDoor/searchRoute.ts +166 -0
  344. package/ts/src/frontDoor/seedPaths.ts +98 -0
  345. package/ts/src/frontDoor/soulRoute.ts +433 -0
  346. package/ts/src/frontDoor/tasksImportSyncRoute.ts +110 -0
  347. package/ts/src/frontDoor/tasksWriteRoute.ts +101 -0
  348. package/ts/src/frontDoor/usage.ts +176 -0
  349. package/ts/src/guards/claudePlugin.ts +252 -0
  350. package/ts/src/guards/packContents.ts +306 -0
  351. package/ts/src/guards/piSourcedSkills.ts +70 -0
  352. package/ts/src/guards/retiredRouteShadows.ts +57 -0
  353. package/ts/src/guards/retiredSurfaces.ts +564 -0
  354. package/ts/src/hooks/claude.ts +176 -0
  355. package/ts/src/hooks/codex.ts +45 -0
  356. package/ts/src/hooks/gemini.ts +126 -0
  357. package/ts/src/hooks/main.ts +109 -0
  358. package/ts/src/hooks/mirrorState.ts +71 -0
  359. package/ts/src/hooks/payload.ts +68 -0
  360. package/ts/src/hooks/runtime.ts +159 -0
  361. package/ts/src/identity/applyConsolidationIdentityUpdate.ts +67 -0
  362. package/ts/src/identity/descriptorGenerate.ts +162 -0
  363. package/ts/src/identity/identityEdit.ts +143 -0
  364. package/ts/src/identity/identityKey.ts +77 -0
  365. package/ts/src/identity/identityRead.ts +69 -0
  366. package/ts/src/identity/identityStore.ts +80 -0
  367. package/ts/src/identity/journeyListing.ts +53 -0
  368. package/ts/src/identity/personaInspect.ts +44 -0
  369. package/ts/src/identity/personaListing.ts +37 -0
  370. package/ts/src/identity/setIdentity.ts +64 -0
  371. package/ts/src/index.ts +231 -0
  372. package/ts/src/init/init.ts +111 -0
  373. package/ts/src/journey/journeyOptions.ts +131 -0
  374. package/ts/src/journey/journeyRemoval.ts +158 -0
  375. package/ts/src/journey/journeyStatus.ts +149 -0
  376. package/ts/src/journey/journeySyncFile.ts +87 -0
  377. package/ts/src/journey/journeyWrite.ts +203 -0
  378. package/ts/src/journey/parentJourney.ts +41 -0
  379. package/ts/src/journey/validateParentJourney.ts +55 -0
  380. package/ts/src/mcp/boundary.ts +151 -0
  381. package/ts/src/mcp/guards.ts +244 -0
  382. package/ts/src/mcp/main.ts +121 -0
  383. package/ts/src/mcp/payload.ts +141 -0
  384. package/ts/src/mcp/protocol.ts +141 -0
  385. package/ts/src/mcp/readModels.ts +241 -0
  386. package/ts/src/mcp/registry.ts +201 -0
  387. package/ts/src/mcp/serve.ts +120 -0
  388. package/ts/src/mcp/tools/deterministic.ts +137 -0
  389. package/ts/src/mcp/tools/providerCrossing.ts +187 -0
  390. package/ts/src/mcp/wire.ts +8 -0
  391. package/ts/src/memory/journal.ts +180 -0
  392. package/ts/src/memory/listing.ts +107 -0
  393. package/ts/src/memory/memoryWrite.ts +88 -0
  394. package/ts/src/memory/reinforcement.ts +35 -0
  395. package/ts/src/mirror/context.ts +161 -0
  396. package/ts/src/mirror/defaultResolution.ts +199 -0
  397. package/ts/src/mirror/orchestration.ts +364 -0
  398. package/ts/src/mirror/reception.ts +194 -0
  399. package/ts/src/mirror/render.ts +75 -0
  400. package/ts/src/mirror/runtimeSession.ts +205 -0
  401. package/ts/src/mode/operatingMode.ts +148 -0
  402. package/ts/src/observability/callOutcome.ts +115 -0
  403. package/ts/src/observability/inspectLedger.ts +329 -0
  404. package/ts/src/observability/ledgerHooks.ts +126 -0
  405. package/ts/src/observability/llmCalls.ts +154 -0
  406. package/ts/src/persona/detectPersona.ts +81 -0
  407. package/ts/src/planning/promptAssembly.ts +111 -0
  408. package/ts/src/planning/weekPending.ts +81 -0
  409. package/ts/src/planning/weekPlan.ts +183 -0
  410. package/ts/src/planning/weekSave.ts +117 -0
  411. package/ts/src/providers/config.ts +189 -0
  412. package/ts/src/providers/cost.ts +64 -0
  413. package/ts/src/providers/credits.ts +204 -0
  414. package/ts/src/providers/embedding.ts +321 -0
  415. package/ts/src/providers/familyProviders.ts +147 -0
  416. package/ts/src/providers/llm.ts +334 -0
  417. package/ts/src/providers/openrouter.ts +244 -0
  418. package/ts/src/providers/redaction.ts +110 -0
  419. package/ts/src/providers/replay.ts +15 -0
  420. package/ts/src/providers/transport.ts +410 -0
  421. package/ts/src/repair/encodingRepair.ts +253 -0
  422. package/ts/src/runtime/backup.ts +151 -0
  423. package/ts/src/runtime/channel.ts +146 -0
  424. package/ts/src/runtime/config.ts +168 -0
  425. package/ts/src/runtime/diagnose.ts +686 -0
  426. package/ts/src/runtime/git.ts +510 -0
  427. package/ts/src/runtime/installKind.ts +220 -0
  428. package/ts/src/runtime/migrate.ts +154 -0
  429. package/ts/src/runtime/packageIdentity.ts +102 -0
  430. package/ts/src/runtime/releaseNotes.ts +244 -0
  431. package/ts/src/runtime/status.ts +557 -0
  432. package/ts/src/runtime/strategies/clone.ts +200 -0
  433. package/ts/src/runtime/strategies/package.ts +328 -0
  434. package/ts/src/runtime/treeRoot.ts +30 -0
  435. package/ts/src/runtime/update.ts +445 -0
  436. package/ts/src/runtime/updateGate.ts +40 -0
  437. package/ts/src/runtime/updatePipeline.ts +159 -0
  438. package/ts/src/runtime/version.ts +36 -0
  439. package/ts/src/runtime/wiring.ts +130 -0
  440. package/ts/src/search/memorySearch.ts +338 -0
  441. package/ts/src/search/ranker.ts +154 -0
  442. package/ts/src/seed/seed.ts +256 -0
  443. package/ts/src/seed/seedYaml.ts +133 -0
  444. package/ts/src/soul/apply.ts +218 -0
  445. package/ts/src/soul/harvest.ts +267 -0
  446. package/ts/src/soul/prompts/soul_beauty_voice.md +103 -0
  447. package/ts/src/soul/prompts/soul_self_voice.md +219 -0
  448. package/ts/src/soul/prompts/soul_wisdom_voice.md +121 -0
  449. package/ts/src/soul/prompts.ts +85 -0
  450. package/ts/src/soul/render.ts +358 -0
  451. package/ts/src/soul/state.ts +212 -0
  452. package/ts/src/soul/transition.ts +37 -0
  453. package/ts/src/tasks/journeyPathParse.ts +113 -0
  454. package/ts/src/tasks/taskImportSync.ts +142 -0
  455. package/ts/src/tasks/taskStore.ts +312 -0
  456. package/ts/src/tasks/weekView.ts +120 -0
  457. package/ts/src/util/clipCodePoints.ts +26 -0
  458. package/ts/src/util/paths.ts +63 -0
  459. package/ts/src/util/program.ts +9 -0
  460. package/ts/src/util/pyGenerators.ts +144 -0
  461. package/ts/src/util/pythonText.ts +317 -0
  462. package/ts/src/util/semver.ts +45 -0
  463. package/ts/src/util/shellWord.ts +25 -0
  464. package/ts/src/util/slug.ts +64 -0
  465. package/ts/src/welcome/card.ts +339 -0
  466. package/ts/src/welcome/statusLine.ts +101 -0
  467. package/ts/src/welcome/updateCache.ts +129 -0
@@ -0,0 +1,1865 @@
1
+ #!/usr/bin/env node
2
+
3
+ //
4
+ // Front-door entry: route a command, answer it, and log the outcome. Argument
5
+ // parsing lives in `args.ts`, DB-path resolution in `dbPath.ts`, and the read
6
+ // renderers in `render/`. The module is importable (the entry guard at the
7
+ // bottom runs `main` only when invoked directly), so its pieces can be tested
8
+ // in-process.
9
+ //
10
+ // Every route answers from TypeScript. Until CV22.DS10.TS5 an unported or
11
+ // reverted command fell back to the Python engine by spawning the
12
+ // interpreter; the fallback, the revert gates that chose it, and the engine
13
+ // itself are gone. A name nothing answers gets the front door's own usage
14
+ // answer.
15
+ //
16
+ // Configuration and the node:sqlite ExperimentalWarning are both handled
17
+ // in-process, inside the entry guard (CV22.DS10.US3, D3): the program reads
18
+ // its own `.env` and `~/.config/mirror/env` and silences the warning itself,
19
+ // so neither a `bin` shim nor a skill line needs `--env-file` or
20
+ // `NODE_OPTIONS=--no-warnings`. Importing this module does neither.
21
+
22
+ import { existsSync, readFileSync } from "node:fs";
23
+ import { basename, dirname } from "node:path";
24
+ import { pathToFileURL } from "node:url";
25
+ import {
26
+ AppendRejectedError,
27
+ type AppendRejectionReason,
28
+ appendConversationMessages,
29
+ type ConversationAppendRequest,
30
+ canonicalJson,
31
+ MAX_PAYLOAD_BYTES,
32
+ parseAppendRequest,
33
+ } from "#conversation/append.ts";
34
+ import { DEFAULT_CLUSTER_THRESHOLD } from "#cultivation/cluster.ts";
35
+ import { listConsolidations } from "#cultivation/consolidationStore.ts";
36
+ import {
37
+ consolidateScan,
38
+ DEFAULT_CONSOLIDATE_SCAN_LIMIT,
39
+ DEFAULT_SHADOW_SCAN_LIMIT,
40
+ shadowScan,
41
+ } from "#cultivation/scan.ts";
42
+ import { type Database, openDatabaseReadOnly, type WritableDatabase } from "#db/database.ts";
43
+ import { ensureDatabaseReady } from "#db/readyOnOpen.ts";
44
+ import { assertSchemaState, SchemaStateError } from "#db/schemaState.ts";
45
+ import { allDescriptors, descriptorsByLayer } from "#descriptor/descriptorRead.ts";
46
+ import { newIdentityKeyProblem } from "#identity/identityKey.ts";
47
+ import { identityRowExists, listIdentityByLayer } from "#identity/identityRead.ts";
48
+ import { listJourneysForListCommand } from "#identity/journeyListing.ts";
49
+ import { listPersonas } from "#identity/personaListing.ts";
50
+ import { setIdentity } from "#identity/setIdentity.ts";
51
+ import { IdentityRootExistsError, initUserHome, TemplatesNotFoundError } from "#init/init.ts";
52
+ import { JOURNEY_PATH_LAYER } from "#journey/journeyStatus.ts";
53
+ import { JourneyNotFoundError } from "#journey/journeyWrite.ts";
54
+ import {
55
+ CallOutcomeTally,
56
+ formatCallOutcome,
57
+ type ProviderCallReport,
58
+ } from "#observability/callOutcome.ts";
59
+ import { chatLedgerHook } from "#observability/ledgerHooks.ts";
60
+ import { runWeekSave } from "#planning/weekSave.ts";
61
+ import type { EmbeddingProvider } from "#providers/embedding.ts";
62
+ import { resolveFamilyProviders } from "#providers/familyProviders.ts";
63
+ import type { LlmProvider } from "#providers/llm.ts";
64
+ import {
65
+ CULTIVATION_APPLY_TRANSPORT,
66
+ CULTIVATION_SCAN_TRANSPORT,
67
+ DESCRIPTOR_TRANSPORT,
68
+ JOURNAL_TRANSPORT,
69
+ WEEK_PLAN_TRANSPORT,
70
+ } from "#providers/transport.ts";
71
+ import {
72
+ configFilePath,
73
+ loadConfiguration,
74
+ silenceExperimentalWarnings,
75
+ writeConfigValue,
76
+ } from "#runtime/config.ts";
77
+ import { runningTreeRoot } from "#runtime/treeRoot.ts";
78
+ import { detectRuntimes, runtimeWiringLines } from "#runtime/wiring.ts";
79
+ import { runSeed } from "#seed/seed.ts";
80
+ import { getTasksForWeek, listTasks } from "#tasks/taskStore.ts";
81
+ import { computeWeekRange } from "#tasks/weekView.ts";
82
+ import { isExecutable } from "#util/paths.ts";
83
+ import { PROGRAM } from "#util/program.ts";
84
+ import { newId, nowIso } from "#util/pyGenerators.ts";
85
+ import { hasOption, optionValue, stripOptionWithValue } from "./args.ts";
86
+ import { canonicalArgv } from "./argvShape.ts";
87
+ import { createBuildLoadRuntime } from "./buildLoadRuntime.ts";
88
+ import { runConsultRoute } from "./consultRoute.ts";
89
+ import {
90
+ runDescriptorGenerateRoute,
91
+ runJournalRoute,
92
+ runLifecycleFaceRead,
93
+ runWeekPlanRoute,
94
+ } from "./contentTailRoute.ts";
95
+ import {
96
+ isConversationLoggerCommand,
97
+ runConversationLoggerRoute,
98
+ } from "./conversationLoggerRoute.ts";
99
+ import {
100
+ runConsolidateApply as runConsolidateApplyRoute,
101
+ runReject,
102
+ runShadowApply as runShadowApplyRoute,
103
+ } from "./cultivationRoute.ts";
104
+ import { MirrorHomeNotConfiguredError, resolveDbPath } from "./dbPath.ts";
105
+ import { runBackupRoute, runRepairEncodingRoute } from "./dbSafetyToolsRoute.ts";
106
+ import { defaultExploreRouteDeps, runExploreRoute } from "./exploreRoute.ts";
107
+ import { frontDoorLogPath, logFrontDoor } from "./frontDoorLog.ts";
108
+ import { applyIdentitySet } from "./identityWrite.ts";
109
+ import { applyJourneySetPath } from "./journeyWriteRoute.ts";
110
+ import { openLiveWriteDatabase } from "./liveBackup.ts";
111
+ import {
112
+ isMirrorWrite,
113
+ isModeWrite,
114
+ mirrorDeactivateMissingSession,
115
+ runMirrorRead,
116
+ runMirrorWriteRoute,
117
+ runModeRead,
118
+ runModeWriteRoute,
119
+ } from "./mirrorModeRoute.ts";
120
+ import { namedRefusal } from "./namedRefusal.ts";
121
+ import { nodeVersionError } from "./nodeSupport.ts";
122
+ import {
123
+ renderConsolidateApply,
124
+ renderConsolidateList,
125
+ renderConsolidateReject,
126
+ renderConsolidateScan,
127
+ } from "./render/consolidate.ts";
128
+ import { renderConversationsListing } from "./render/conversations.ts";
129
+ import { renderDescriptorList } from "./render/descriptor.ts";
130
+ import { renderDetectPersona } from "./render/detectPersona.ts";
131
+ import {
132
+ IdentityEntryNotFoundError,
133
+ identityListRows,
134
+ renderIdentityGet,
135
+ renderIdentityList,
136
+ } from "./render/identity.ts";
137
+ import { PersonaNotFoundError, renderInspectPersona } from "./render/inspectPersona.ts";
138
+ import { renderJourneyStatus, resolveJourneyStatusSlug } from "./render/journeyStatus.ts";
139
+ import { renderJourneys } from "./render/journeys.ts";
140
+ import { renderListJourneys, renderListPersonas } from "./render/list.ts";
141
+ import { renderMemories } from "./render/memories.ts";
142
+ import { ConversationNotFoundError, renderRecall } from "./render/recall.ts";
143
+ import {
144
+ renderShadowApply,
145
+ renderShadowList,
146
+ renderShadowReject,
147
+ renderShadowScan,
148
+ renderShadowShow,
149
+ } from "./render/shadow.ts";
150
+ import {
151
+ renderTasksAdd,
152
+ renderTasksDelete,
153
+ renderTasksList,
154
+ renderTasksStatusChange,
155
+ } from "./render/tasks.ts";
156
+ import {
157
+ renderTasksImport,
158
+ renderTasksSyncConfig,
159
+ renderTasksSyncNoJourneysConfigured,
160
+ renderTasksSyncOutcome,
161
+ } from "./render/tasksImportSync.ts";
162
+ import { renderWeekView } from "./render/week.ts";
163
+ import { retiredRefusal, routeMemoryCommand } from "./routing.ts";
164
+ import { runRuntimeReadRoute, runWelcomeRoute } from "./runtimeRoute.ts";
165
+ import { runMemorySearchRoute } from "./searchRoute.ts";
166
+ import { resolveSeedPaths } from "./seedPaths.ts";
167
+ import { runSoulRoute } from "./soulRoute.ts";
168
+ import {
169
+ applyTasksImport,
170
+ applyTasksSyncConfig,
171
+ applyTasksSyncForJourney,
172
+ resolveSyncJourneys,
173
+ } from "./tasksImportSyncRoute.ts";
174
+ import {
175
+ applyTasksAdd,
176
+ applyTasksDelete,
177
+ applyTasksStatusChange,
178
+ type TaskStatusTarget,
179
+ } from "./tasksWriteRoute.ts";
180
+ import { answerUsage } from "./usage.ts";
181
+
182
+ /**
183
+ * Resolve the database path for a CLI invocation, mapping a configuration
184
+ * failure (no home/env resolvable) to a printed error and null — callers
185
+ * translate null into exit code 2.
186
+ */
187
+ function resolveDbPathForCli(args: readonly string[]): string | null {
188
+ try {
189
+ return resolveDbPath(args);
190
+ } catch (error) {
191
+ if (error instanceof MirrorHomeNotConfiguredError) {
192
+ console.error(`Mirror TS front door: ${error.message}`);
193
+ return null;
194
+ }
195
+ throw error;
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Prepare a database for TS serving and record what happened in the front-door
201
+ * log: a redacted `migrate_on_open` event (migration ids + backup file name,
202
+ * never content). The activation itself lives in `db/readyOnOpen.ts`, shared
203
+ * with the MCP entry point, which writes its note to stderr instead.
204
+ */
205
+ function ensureDatabaseReadyForCli(dbPath: string, command: string | null): void {
206
+ const migration = ensureDatabaseReady(dbPath);
207
+ if (migration.migrated) {
208
+ logFrontDoor(frontDoorLogPath(dbPath), {
209
+ command,
210
+ route: "ts",
211
+ exitCode: 0,
212
+ detail: `migrate_on_open applied=${migration.appliedIds.join(",")} backup=${
213
+ migration.backupPath ? basename(migration.backupPath) : "none"
214
+ }`,
215
+ });
216
+ }
217
+ }
218
+
219
+ /** Serve a ported read command from the TS core, bootstrapping a missing DB via the TS core first. */
220
+ function runTs(argv: readonly string[]): number {
221
+ const command = argv[0];
222
+ const args = argv.slice(1);
223
+ const dbPath = resolveDbPathForCli(args);
224
+ if (dbPath === null) return 2;
225
+ // First-run contract (CV22.DS6.TS4): a missing database means an
226
+ // unbootstrapped install. TS now owns bootstrap — create the directory,
227
+ // schema, and migrations under the cross-process lock — then serve read-only
228
+ // from the fresh file. (This replaces the DS3 stopgap that delegated a
229
+ // missing DB to Python; see docs/project/decisions.md.)
230
+ ensureDatabaseReadyForCli(dbPath, command ?? null);
231
+ const db = openDatabaseReadOnly(dbPath);
232
+ try {
233
+ assertSchemaState(db);
234
+ if (command === "detect-persona") process.stdout.write(renderDetectPersona(db, args));
235
+ else if (command === "journeys") process.stdout.write(renderJourneys(db));
236
+ else if (command === "memories") process.stdout.write(renderMemories(db, args));
237
+ else if (command === "identity") return runIdentityRead(db, args);
238
+ else if (command === "descriptor") return runDescriptorRead(db, args);
239
+ else if (command === "list") return runListRead(db, args);
240
+ else if (command === "inspect") return runInspectRead(db, args);
241
+ else if (command === "recall") return runRecallRead(db, args);
242
+ else if (command === "conversations") {
243
+ // CV22.DS7.US11: the two ES-001 read faces share the read seam with the
244
+ // plain listing; routing.ts has already refused the write faces.
245
+ const lifecycleFace =
246
+ args.includes("--metadata-lifecycle-dry-run") ||
247
+ args.includes("--metadata-lifecycle-preview-at-message");
248
+ return lifecycleFace ? runLifecycleFaceRead(db, args) : runConversationsRead(db, args);
249
+ } else if (command === "journey") return runJourneyStatusRead(db, args);
250
+ else if (command === "tasks") return runTasksRead(db, args);
251
+ else if (command === "week") return runWeekRead(db, args);
252
+ else if (command === "consolidate") return runConsolidateRead(db, args);
253
+ else if (command === "shadow") return runShadowRead(db, args);
254
+ else if (command === "mirror") return runMirrorRead(db, args);
255
+ else if (command === "mode") return runModeRead(db, args);
256
+ else throw new Error(`Unsupported TS route: ${command}`);
257
+ return 0;
258
+ } catch (error) {
259
+ if (error instanceof SchemaStateError) {
260
+ console.error(`Mirror TS front door: ${error.message}`);
261
+ return 2;
262
+ }
263
+ throw error;
264
+ } finally {
265
+ db.close();
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Serve `identity list` / `identity get` (DS7.US1). `args` excludes the
271
+ * `identity` token itself, e.g. `["list", "--layer", "ego"]` or
272
+ * `["get", "ego", "behavior"]`. `identity set` and `identity edit` never reach
273
+ * here — routed elsewhere (write seam) or to Python (interactive) respectively.
274
+ */
275
+ function runIdentityRead(db: Database, args: readonly string[]): number {
276
+ const sub = args[0];
277
+ const rest = stripOptionWithValue(
278
+ stripOptionWithValue(args.slice(1), "--mirror-home"),
279
+ "--db-path",
280
+ );
281
+ if (sub === "list") {
282
+ process.stdout.write(renderIdentityList(identityListRows(db, optionValue(args, "--layer"))));
283
+ return 0;
284
+ }
285
+ // sub === "get" (the only other route.ts-allowed subcommand).
286
+ const [layer, key] = rest;
287
+ if (!layer || !key) {
288
+ console.error("Usage: identity get <layer> <key>");
289
+ return 2;
290
+ }
291
+ try {
292
+ process.stdout.write(renderIdentityGet(db, layer, key));
293
+ return 0;
294
+ } catch (error) {
295
+ if (error instanceof IdentityEntryNotFoundError) {
296
+ console.error(error.message);
297
+ return 1;
298
+ }
299
+ throw error;
300
+ }
301
+ }
302
+
303
+ /** Serve `descriptor list` (DS7.US1). `descriptor generate` never reaches here. */
304
+ function runDescriptorRead(db: Database, args: readonly string[]): number {
305
+ const layer = optionValue(args, "--layer");
306
+ const rows = layer ? descriptorsByLayer(db, layer) : allDescriptors(db);
307
+ process.stdout.write(renderDescriptorList(rows));
308
+ return 0;
309
+ }
310
+
311
+ /** Serve `list personas` / `list journeys` (DS7.US1). `extensions`/`all` never reach here. */
312
+ function runListRead(db: Database, args: readonly string[]): number {
313
+ if (args[0] === "personas") {
314
+ process.stdout.write(renderListPersonas(listPersonas(db), hasOption(args, "--verbose")));
315
+ return 0;
316
+ }
317
+ process.stdout.write(renderListJourneys(listJourneysForListCommand(db)));
318
+ return 0;
319
+ }
320
+
321
+ /** Serve `inspect persona <id>` (DS7.US1). Other inspect targets never reach here. */
322
+ function runInspectRead(db: Database, args: readonly string[]): number {
323
+ const rest = stripOptionWithValue(
324
+ stripOptionWithValue(args.slice(1), "--mirror-home"),
325
+ "--extensions-root",
326
+ );
327
+ const personaId = rest[0];
328
+ if (!personaId) {
329
+ console.error("Usage: inspect persona <id>");
330
+ return 2;
331
+ }
332
+ try {
333
+ process.stdout.write(renderInspectPersona(db, personaId));
334
+ return 0;
335
+ } catch (error) {
336
+ if (error instanceof PersonaNotFoundError) {
337
+ // Matches Python: this goes to STDOUT, not stderr.
338
+ process.stdout.write(`${error.message}\n`);
339
+ return 1;
340
+ }
341
+ throw error;
342
+ }
343
+ }
344
+
345
+ /** Serve `recall <conv_id> [--limit N]` (DS7.US1). */
346
+ function runRecallRead(db: Database, args: readonly string[]): number {
347
+ const limitRaw = optionValue(args, "--limit");
348
+ const limit = limitRaw !== null ? Number(limitRaw) : 50;
349
+ const positionals = stripOptionWithValue(
350
+ stripOptionWithValue(stripOptionWithValue(args, "--limit"), "--mirror-home"),
351
+ "--db-path",
352
+ );
353
+ const convId = positionals[0];
354
+ if (!convId) {
355
+ console.error("Usage: recall <conv_id> [--limit N]");
356
+ return 2;
357
+ }
358
+ try {
359
+ process.stdout.write(renderRecall(db, convId, limit));
360
+ return 0;
361
+ } catch (error) {
362
+ if (error instanceof ConversationNotFoundError) {
363
+ console.error(error.message);
364
+ return 1;
365
+ }
366
+ throw error;
367
+ }
368
+ }
369
+
370
+ /** Serve `conversations` (plain listing only; DS7.US1). */
371
+ function runConversationsRead(db: Database, args: readonly string[]): number {
372
+ const limitRaw = optionValue(args, "--limit");
373
+ process.stdout.write(
374
+ renderConversationsListing(db, {
375
+ limit: limitRaw !== null ? Number(limitRaw) : 20,
376
+ journey: optionValue(args, "--journey"),
377
+ persona: optionValue(args, "--persona"),
378
+ }),
379
+ );
380
+ return 0;
381
+ }
382
+
383
+ /**
384
+ * Serve `journey` status reads (DS7.US1): `journey`, `journey <slug>`,
385
+ * `journey status`, `journey status <slug>`. `journey set-path`/`update` never
386
+ * reach here (their own write routes).
387
+ */
388
+ function runJourneyStatusRead(db: Database, args: readonly string[]): number {
389
+ const remaining = stripOptionWithValue(stripOptionWithValue(args, "--mirror-home"), "--db-path");
390
+ process.stdout.write(renderJourneyStatus(db, resolveJourneyStatusSlug(remaining)));
391
+ return 0;
392
+ }
393
+
394
+ /**
395
+ * Serve `tasks list` (and the bare `tasks`/`tasks --journey ...` default,
396
+ * DS7.US2 slice 3a). `args` excludes the `tasks` token itself. Flags are
397
+ * scanned across the whole slice (the existing recall/conversations
398
+ * convention) rather than requiring them after a `list` token -- Python's own
399
+ * argparse subparser quirk (a flag given only BEFORE the subcommand can be
400
+ * overwritten back to the subparser's default) is a narrow, unreproduced edge
401
+ * case; the common "flag after/around the subcommand" usage matches exactly.
402
+ */
403
+ function runTasksRead(db: Database, args: readonly string[]): number {
404
+ const journey = optionValue(args, "--journey");
405
+ const status = optionValue(args, "--status");
406
+ const all = hasOption(args, "--all");
407
+ const filters = all ? { journey } : status ? { journey, status } : { journey, openOnly: true };
408
+ const tasks = listTasks(db, filters);
409
+ process.stdout.write(renderTasksList(tasks, { all, status }));
410
+ return 0;
411
+ }
412
+
413
+ /**
414
+ * Serve `week view` (and the bare `week` default, DS7.US2 slice 3b). `plan`/
415
+ * `save` never reach here (their own routes). `now` is the real current time
416
+ * in production; tests inject a frozen instant by calling `renderWeekView`
417
+ * directly rather than through this CLI entry point.
418
+ */
419
+ /**
420
+ * `week save` (CV22.DS7.US11 plateau 1) is the only `week` leaf that writes.
421
+ * `view` stays on the read seam; `plan` is Python's until it lands behind the
422
+ * replay transport.
423
+ */
424
+ function isWeekSaveWrite(argv: readonly string[]): boolean {
425
+ return argv[0] === "week" && argv[1] === "save";
426
+ }
427
+
428
+ function runWeekSaveWrite(argv: readonly string[]): number {
429
+ return withLiveWriteDb(argv, (db) => {
430
+ runWeekSave(db, {});
431
+ return 0;
432
+ });
433
+ }
434
+
435
+ /**
436
+ * The content-tail routes (CV22.DS7.US11, live under CV22.DS8.US3).
437
+ *
438
+ * Each builds its providers from the same spec `routing.ts` decided with, so
439
+ * the route cannot disagree with the router about which transport is in play.
440
+ * Half a replay fixture throws `ReplayFixtureIncompleteError` from the factory,
441
+ * which the front door answers as a refusal; a missing provider here is defense
442
+ * in depth.
443
+ */
444
+ async function runJournalContentRoute(argv: readonly string[]): Promise<number> {
445
+ const family = await resolveFamilyProviders(process.env, JOURNAL_TRANSPORT);
446
+ if (!family?.llm || !family.embedding) {
447
+ throw new Error("journal requires an LLM and an embedding provider");
448
+ }
449
+ return withLiveWriteDbAsync(argv, (db) =>
450
+ runJournalRoute(db, argv.slice(1), {
451
+ llm: family.llm as LlmProvider,
452
+ embedding: family.embedding as EmbeddingProvider,
453
+ }),
454
+ );
455
+ }
456
+
457
+ async function runWeekPlanContentRoute(argv: readonly string[]): Promise<number> {
458
+ const family = await resolveFamilyProviders(process.env, WEEK_PLAN_TRANSPORT);
459
+ if (!family?.llm) throw new Error("week plan requires an LLM provider");
460
+ const llm = family.llm;
461
+ return withLiveWriteDbAsync(argv, (db) => runWeekPlanRoute(db, argv.slice(2), llm));
462
+ }
463
+
464
+ async function runDescriptorContentRoute(argv: readonly string[]): Promise<number> {
465
+ const family = await resolveFamilyProviders(process.env, DESCRIPTOR_TRANSPORT);
466
+ if (!family?.llm) throw new Error("descriptor generate requires an LLM provider");
467
+ const llm = family.llm;
468
+ const reporter = outcomeReporter(argv, "descriptor");
469
+ return withLiveWriteDbAsync(argv, async (db) => {
470
+ const exitCode = await runDescriptorGenerateRoute(db, argv.slice(2), llm, reporter.onOutcome);
471
+ reporter.logSummary();
472
+ return exitCode;
473
+ });
474
+ }
475
+
476
+ function runWeekRead(db: Database, _args: readonly string[]): number {
477
+ const now = new Date();
478
+ const range = computeWeekRange(now);
479
+ const tasks = getTasksForWeek(db, range.start, range.end);
480
+ process.stdout.write(renderWeekView(tasks, now));
481
+ return 0;
482
+ }
483
+
484
+ /** Serve `consolidate list` (CV22.DS7.US3). `args` excludes the `consolidate` token itself. */
485
+ function runConsolidateRead(db: Database, args: readonly string[]): number {
486
+ const status = optionValue(args, "--status");
487
+ const limitRaw = optionValue(args, "--limit");
488
+ const limit = limitRaw !== null ? Number(limitRaw) : 20;
489
+ process.stdout.write(renderConsolidateList(listConsolidations(db, { status, limit }), status));
490
+ return 0;
491
+ }
492
+
493
+ /** Serve `shadow list` / `shadow show` (CV22.DS7.US3). `args` excludes the `shadow` token itself. */
494
+ function runShadowRead(db: Database, args: readonly string[]): number {
495
+ if (args[0] === "show") {
496
+ process.stdout.write(renderShadowShow(listIdentityByLayer(db, "shadow")));
497
+ return 0;
498
+ }
499
+ // args[0] === "list" (the only other route.ts-allowed read subcommand).
500
+ const status = optionValue(args, "--status");
501
+ const limitRaw = optionValue(args, "--limit");
502
+ const limit = limitRaw !== null ? Number(limitRaw) : 20;
503
+ // Port of Python's own over-fetch-then-filter: list_consolidations(limit*5),
504
+ // then keep only shadow_observation rows, THEN cap at limit.
505
+ const pool = listConsolidations(db, { status, limit: limit * 5 });
506
+ const items = pool.filter((c) => c.action === "shadow_observation").slice(0, limit);
507
+ process.stdout.write(renderShadowList(items, status));
508
+ return 0;
509
+ }
510
+
511
+ function readStdinContent(): string {
512
+ try {
513
+ return readFileSync(0, "utf8");
514
+ } catch {
515
+ return "";
516
+ }
517
+ }
518
+
519
+ /** `conversations append` (CV22.DS7.US10 slice B′). */
520
+ function isConversationsAppend(argv: readonly string[]): boolean {
521
+ return argv[0] === "conversations" && argv[1] === "append";
522
+ }
523
+
524
+ /**
525
+ * Serve `conversations append`: the published external-shell write boundary.
526
+ *
527
+ * Mirrors Python's `_append_main` exactly, including its two exit codes — 0 with
528
+ * a success receipt, 2 with a bounded rejection receipt — because the contract
529
+ * is consumed by third-party shells that branch on them. Receipts are emitted
530
+ * as canonical JSON (sorted keys, tight separators) so the bytes match the
531
+ * oracle's.
532
+ *
533
+ * The stdin read is bounded before parsing: Python reads one byte past the
534
+ * limit and rejects `limit_exceeded` without ever decoding an oversized
535
+ * payload, so an attacker cannot force the parser to walk 100 MB of JSON.
536
+ */
537
+ function runConversationsAppend(argv: readonly string[]): number {
538
+ const emitRejection = (reason: AppendRejectionReason): number => {
539
+ process.stdout.write(`${canonicalJson(new AppendRejectedError(reason).receipt())}\n`);
540
+ return 2;
541
+ };
542
+
543
+ const raw = readStdinBytes(MAX_PAYLOAD_BYTES + 1);
544
+ if (raw.byteLength > MAX_PAYLOAD_BYTES) return emitRejection("limit_exceeded");
545
+
546
+ let request: ConversationAppendRequest;
547
+ try {
548
+ request = parseAppendRequest(JSON.parse(raw.toString("utf8")));
549
+ } catch (error) {
550
+ if (error instanceof AppendRejectedError) return emitRejection(error.reason);
551
+ // Malformed UTF-8/JSON and recursion overflow are all `malformed_request`.
552
+ return emitRejection("malformed_request");
553
+ }
554
+
555
+ try {
556
+ return withLiveWriteDb(argv, (db) => {
557
+ const receipt = appendConversationMessages(db, request);
558
+ process.stdout.write(`${canonicalJson(receipt)}\n`);
559
+ return 0;
560
+ });
561
+ } catch (error) {
562
+ if (error instanceof AppendRejectedError) return emitRejection(error.reason);
563
+ return emitRejection("persistence_failure");
564
+ }
565
+ }
566
+
567
+ /** Read at most `limit` bytes from stdin, matching Python's bounded read. */
568
+ function readStdinBytes(limit: number): Buffer {
569
+ try {
570
+ const buffer = readFileSync(0);
571
+ return buffer.byteLength > limit ? buffer.subarray(0, limit) : buffer;
572
+ } catch {
573
+ return Buffer.alloc(0);
574
+ }
575
+ }
576
+
577
+ /**
578
+ * Shared skeleton for the sanctioned live-write commands: resolve the DB (exit 2
579
+ * on config failure), bootstrap a missing DB via the TS core, open the
580
+ * backup-gated live-write seam, assert schema state, run `write`, and always
581
+ * close. Schema
582
+ * drift maps to exit 2; every other error propagates for the caller's own
583
+ * handling. Callers do their argument pre-validation before invoking this.
584
+ */
585
+ function withLiveWriteDb(argv: readonly string[], write: (db: WritableDatabase) => number): number {
586
+ const dbPath = resolveDbPathForCli(argv.slice(2));
587
+ if (dbPath === null) return 2;
588
+ return withLiveWriteDbAt(dbPath, argv[0] ?? null, write);
589
+ }
590
+
591
+ /** The live-write seam for an already-resolved database path. */
592
+ function withLiveWriteDbAt(
593
+ dbPath: string,
594
+ command: string | null,
595
+ write: (db: WritableDatabase) => number,
596
+ ): number {
597
+ // Missing DB => unbootstrapped install; TS bootstraps it (CV22.DS6.TS4) and
598
+ // applies any pending TS-authored migration (US3), then the backup-gated
599
+ // live-write seam opens the now-current file.
600
+ ensureDatabaseReadyForCli(dbPath, command);
601
+ const db = openLiveWriteDatabase(dbPath);
602
+ try {
603
+ assertSchemaState(db);
604
+ return write(db);
605
+ } catch (error) {
606
+ if (error instanceof SchemaStateError) {
607
+ console.error(`Mirror TS front door: ${error.message}`);
608
+ return 2;
609
+ }
610
+ throw error;
611
+ } finally {
612
+ db.close();
613
+ }
614
+ }
615
+
616
+ /**
617
+ * Async counterpart to `withLiveWriteDb`, for the CV22.DS7.US3 replay-gated
618
+ * cultivation writes (`consolidate apply`/`scan`, `shadow scan`) whose `write`
619
+ * callback must `await` a replay LLM/embedding provider call. Same skeleton,
620
+ * schema-drift handling, and always-close guarantee.
621
+ */
622
+ async function withLiveWriteDbAsync(
623
+ argv: readonly string[],
624
+ write: (db: WritableDatabase) => Promise<number>,
625
+ ): Promise<number> {
626
+ const dbPath = resolveDbPathForCli(argv.slice(2));
627
+ if (dbPath === null) return 2;
628
+ ensureDatabaseReadyForCli(dbPath, argv[0] ?? null);
629
+ const db = openLiveWriteDatabase(dbPath);
630
+ try {
631
+ assertSchemaState(db);
632
+ return await write(db);
633
+ } catch (error) {
634
+ if (error instanceof SchemaStateError) {
635
+ console.error(`Mirror TS front door: ${error.message}`);
636
+ return 2;
637
+ }
638
+ throw error;
639
+ } finally {
640
+ db.close();
641
+ }
642
+ }
643
+
644
+ /** Write `rendered.text` to the correct stream and return its exit code -- the
645
+ * shared endpoint for every `RenderedCommand`-shaped cultivation outcome. */
646
+ function writeRendered(rendered: { text: string; stderr: boolean; exitCode: number }): number {
647
+ if (rendered.stderr) process.stderr.write(rendered.text);
648
+ else process.stdout.write(rendered.text);
649
+ return rendered.exitCode;
650
+ }
651
+
652
+ function isIdentityWrite(argv: readonly string[]): boolean {
653
+ return argv[0] === "identity" && argv[1] === "set";
654
+ }
655
+
656
+ function isIdentityEdit(argv: readonly string[]): boolean {
657
+ return argv[0] === "identity" && argv[1] === "edit";
658
+ }
659
+
660
+ /**
661
+ * The leaves CV22.DS7.TS4 answers, once `routing.ts` has allowed them.
662
+ *
663
+ * `list` and `inspect` are SHARED commands: `list personas|journeys` and
664
+ * `inspect persona` are DS7.US1's ported reads and must keep reaching their own
665
+ * handlers. The first version of this predicate claimed both commands whole and
666
+ * sent all four of those leaves through the catalog route — the same
667
+ * inheritance the routing comments warn about, committed one file away from the
668
+ * warning. Named leaves only.
669
+ */
670
+ function isExtensionCatalogCommand(argv: readonly string[]): boolean {
671
+ const command = argv[0];
672
+ if (command === "extensions" || command === "ext") return true;
673
+ const target = firstCatalogPositional(argv.slice(1));
674
+ if (command === "list") return target === null || target === "extensions" || target === "all";
675
+ if (command === "inspect") {
676
+ return (
677
+ target === "extension" ||
678
+ target === "runtime-catalog" ||
679
+ target === "llm-calls" ||
680
+ target === "embedding-provenance"
681
+ );
682
+ }
683
+ return false;
684
+ }
685
+
686
+ /** The first non-option token, skipping the valued options these commands take. */
687
+ function firstCatalogPositional(args: readonly string[]): string | null {
688
+ const valued = new Set([
689
+ "--mirror-home",
690
+ "--db-path",
691
+ "--extensions-root",
692
+ "--runtime",
693
+ "--target-root",
694
+ ]);
695
+ for (let index = 0; index < args.length; index += 1) {
696
+ const token = args[index] as string;
697
+ if (valued.has(token)) {
698
+ index += 1;
699
+ continue;
700
+ }
701
+ if (!token.startsWith("--")) return token;
702
+ }
703
+ return null;
704
+ }
705
+
706
+ function isLifecycleWriteFace(argv: readonly string[]): boolean {
707
+ return (
708
+ argv[0] === "conversations" &&
709
+ (argv.includes("--metadata-lifecycle-apply") || argv.includes("--metadata-lifecycle-demo"))
710
+ );
711
+ }
712
+
713
+ /**
714
+ * `identity edit <layer> <key>`: drive $EDITOR, then write through the same
715
+ * seam `identity set` uses.
716
+ *
717
+ * The editor inherits this process's streams, so the write handle is taken
718
+ * AFTER it returns — a human staring at vim for ten minutes should not be
719
+ * holding the database open, and an editor that never exits should not be
720
+ * holding it forever.
721
+ */
722
+ async function runIdentityEditRoute(argv: readonly string[]): Promise<number> {
723
+ const { runIdentityEdit } = await import("#identity/identityEdit.ts");
724
+ const positionals = stripOptionWithValue(
725
+ stripOptionWithValue(argv.slice(2), "--db-path"),
726
+ "--mirror-home",
727
+ );
728
+ const layer = positionals[0];
729
+ const key = positionals[1];
730
+ if (!layer || !key) {
731
+ console.error("identity edit requires <layer> <key>");
732
+ return 2;
733
+ }
734
+ const refused = refusedNewIdentityKey(argv.slice(1), layer, key);
735
+ if (refused !== null) {
736
+ console.error(`Error: ${refused}`);
737
+ return 1;
738
+ }
739
+ return withMirrorWriteDb(argv, (db) => {
740
+ const result = runIdentityEdit(db, layer, key, {
741
+ editor: process.env.EDITOR,
742
+ visual: process.env.VISUAL,
743
+ nowIso,
744
+ newId,
745
+ });
746
+ if (result.stdout) process.stdout.write(result.stdout);
747
+ if (result.stderr) process.stderr.write(result.stderr);
748
+ return result.exitCode;
749
+ });
750
+ }
751
+
752
+ /** `conversations --metadata-lifecycle-apply|--metadata-lifecycle-demo`. */
753
+ async function runLifecycleWriteFace(argv: readonly string[]): Promise<number> {
754
+ const { runLifecycleWriteRoute } = await import("./lifecycleWriteRoute.ts");
755
+ return withMirrorWriteDb(argv, (db) => runLifecycleWriteRoute(db, argv.slice(1)));
756
+ }
757
+
758
+ /** The extension catalog family, answered from the TS core. */
759
+ async function runExtensionCatalog(argv: readonly string[]): Promise<DispatchOutcome> {
760
+ const { runExtensionCatalogRoute } = await import("./extensionCatalogRoute.ts");
761
+ const dbPath = resolveDbPathForCli(argv.slice(1));
762
+ if (dbPath === null) return { exitCode: 2 };
763
+ ensureDatabaseReadyForCli(dbPath, argv[0] ?? null);
764
+ const { pythonUtcIsoformat } = await import("#extensions/bindings.ts");
765
+ return runExtensionCatalogRoute(argv, {
766
+ mirrorHome: dirname(dbPath),
767
+ databasePath: dbPath,
768
+ deps: { nowIso: () => pythonUtcIsoformat() },
769
+ withReadOnlyDatabase: (run) => withMirrorReadDb(argv, run),
770
+ withWritableDatabase: (run) => withMirrorWriteDb(argv, run),
771
+ });
772
+ }
773
+
774
+ /**
775
+ * CR104: why `identity set` or `identity edit` must refuse to create `key` in
776
+ * `layer`, decided before the write seam opens, or null to go ahead.
777
+ *
778
+ * The store refuses such a create on its own (`upsertIdentity`), but only after
779
+ * the seam has taken its pre-write snapshot, and after `identity edit` has had the
780
+ * user type into an editor. Asked here first, a refusal costs neither.
781
+ *
782
+ * A key outside the grammar may still name a row that predates the grammar, and
783
+ * that row stays writable, so only for such a key is the row looked up, read-only.
784
+ * When the database cannot be resolved or read, the answer is left to the write
785
+ * path, which reports the resolution error and where the store still refuses.
786
+ */
787
+ function refusedNewIdentityKey(args: readonly string[], layer: string, key: string): string | null {
788
+ const problem = newIdentityKeyProblem(layer, key);
789
+ if (problem === null) return null;
790
+ let dbPath: string;
791
+ try {
792
+ dbPath = resolveDbPath(args);
793
+ } catch {
794
+ return null;
795
+ }
796
+ if (!existsSync(dbPath)) return problem;
797
+ try {
798
+ const db = openDatabaseReadOnly(dbPath);
799
+ try {
800
+ return identityRowExists(db, layer, key) ? null : problem;
801
+ } finally {
802
+ db.close();
803
+ }
804
+ } catch {
805
+ return null;
806
+ }
807
+ }
808
+
809
+ /**
810
+ * Route `identity set <layer> <key> --content ... | stdin` to the TS core. Mirrors
811
+ * the Python `identity set` interface and output, but writes through the sanctioned
812
+ * live-write seam after a backup, reusing the ported `setIdentity`. A new journey
813
+ * slug or persona id outside the grammar is refused first (CR104).
814
+ */
815
+ function runIdentityWrite(argv: readonly string[]): number {
816
+ const args = argv.slice(2);
817
+ const positionals = stripOptionWithValue(
818
+ stripOptionWithValue(stripOptionWithValue(args, "--content"), "--db-path"),
819
+ "--mirror-home",
820
+ );
821
+ const layer = positionals[0];
822
+ const key = positionals[1];
823
+ if (!layer || !key) {
824
+ console.error("identity set requires <layer> <key>");
825
+ return 2;
826
+ }
827
+ const refused = refusedNewIdentityKey(args, layer, key);
828
+ if (refused !== null) {
829
+ console.error(`Error: ${refused}`);
830
+ return 1;
831
+ }
832
+ const content = optionValue(args, "--content") ?? readStdinContent();
833
+ if (!content.trim()) {
834
+ console.error("Error: content is empty.");
835
+ return 1;
836
+ }
837
+ return withLiveWriteDb(argv, (db) => {
838
+ const outcome = applyIdentitySet(db, { layer, key, content, id: newId(), nowIso: nowIso() });
839
+ process.stdout.write(`\u2713 ${outcome.layer}/${outcome.key} ${outcome.action}\n`);
840
+ return 0;
841
+ });
842
+ }
843
+
844
+ function isJourneyWrite(argv: readonly string[]): boolean {
845
+ return argv[0] === "journey" && argv[1] === "set-path";
846
+ }
847
+
848
+ function isJourneyUpdateWrite(argv: readonly string[]): boolean {
849
+ return argv[0] === "journey" && argv[1] === "update";
850
+ }
851
+
852
+ function isMemorySearch(argv: readonly string[]): boolean {
853
+ return argv[0] === "memories" && argv.includes("--search");
854
+ }
855
+
856
+ function isConsult(argv: readonly string[]): boolean {
857
+ return argv[0] === "consult";
858
+ }
859
+
860
+ function isInit(argv: readonly string[]): boolean {
861
+ return argv[0] === "init";
862
+ }
863
+
864
+ /**
865
+ * Serve `init <user>` (DS7.US1 Slice B). Filesystem-only — no database is
866
+ * opened. Python's argparse defines only the `user` positional (no
867
+ * --mirror-home/--db-path), so none are stripped here.
868
+ *
869
+ * Deliberate divergence: an already-populated identity root or a missing
870
+ * templates root are uncaught exceptions in Python (a raw traceback, exit 1) --
871
+ * not a designed message. This preserves the contract (exit 1, a stderr
872
+ * message naming the failure) without fabricating a fake traceback.
873
+ *
874
+ * CV22.DS10.US3 (D3): when no source configured a user, `init` writes
875
+ * `MIRROR_USER=<user>` into `~/.config/mirror/env` (0600 in 0700) and says
876
+ * where to add `OPENROUTER_API_KEY` by hand. The key is never read from argv.
877
+ * When a user IS configured -- the checkout's `.env`, or a shell running a
878
+ * second mirror -- the file is left alone and the person is told how to
879
+ * select the new home.
880
+ *
881
+ * Plateau 3: it ends with the wiring step for each runtime found on the PATH
882
+ * (`#runtime/wiring.ts`), with this tree's path in it, so the person is handed
883
+ * the next line rather than a page. Printing is not automating.
884
+ */
885
+ function runInit(argv: readonly string[], env: NodeJS.ProcessEnv = process.env): number {
886
+ const user = argv[1];
887
+ if (!user) {
888
+ console.error("Usage: init <user>");
889
+ return 2;
890
+ }
891
+ try {
892
+ const identityRoot = initUserHome(user);
893
+ const mirrorHome = dirname(identityRoot);
894
+ const configured = Boolean(env.MIRROR_USER || env.MIRROR_HOME);
895
+ const configuration = configured
896
+ ? [
897
+ ` 1. This machine already selects a user; to use this one, run with MIRROR_USER=${user}`,
898
+ ` or set it in ${configFilePath(env)}`,
899
+ ]
900
+ : [
901
+ `Configuration: ${writeConfigValue("MIRROR_USER", user, { env })} (MIRROR_USER=${user})`,
902
+ "",
903
+ "Next steps:",
904
+ " 1. Add OPENROUTER_API_KEY=<your key> to that file (it is created readable by you only)",
905
+ ];
906
+ const prints = [
907
+ `Created user home: ${mirrorHome}`,
908
+ `Identity ready at: ${identityRoot}`,
909
+ ...(configured ? ["", "Next steps:"] : []),
910
+ ...configuration,
911
+ ` 2. Run: ${PROGRAM} seed`,
912
+ "\nYour identity is ready to use. Deepen it over time with:",
913
+ ` ${PROGRAM} identity edit user identity`,
914
+ ...runtimeWiringSection(env),
915
+ ];
916
+ process.stdout.write(prints.map((line) => `${line}\n`).join(""));
917
+ return 0;
918
+ } catch (error) {
919
+ if (error instanceof IdentityRootExistsError || error instanceof TemplatesNotFoundError) {
920
+ console.error(error.message);
921
+ return 1;
922
+ }
923
+ throw error;
924
+ }
925
+ }
926
+
927
+ /** The "Wire your runtime" tail of `init`, empty when no runtime is on the PATH. */
928
+ function runtimeWiringSection(env: NodeJS.ProcessEnv): string[] {
929
+ const detected = detectRuntimes(env, isExecutable);
930
+ if (detected.length === 0) return [];
931
+ const root = runningTreeRoot();
932
+ return [
933
+ "\nWire your runtime to this install (each step is one you run and can read first):",
934
+ ...detected.flatMap((runtime) => ["", ...runtimeWiringLines(runtime, root)]),
935
+ ];
936
+ }
937
+
938
+ function isSeed(argv: readonly string[]): boolean {
939
+ return argv[0] === "seed";
940
+ }
941
+
942
+ /**
943
+ * Serve `seed [--env E] [--mirror-home PATH] [--force]` (DS7.US1 Slice B).
944
+ * Uses its own resolution (seedPaths.ts) rather than the shared
945
+ * withLiveWriteDb skeleton: `seed`'s Python source resolves the database path
946
+ * differently from every other command (see seedPaths.ts's module comment for
947
+ * the verified, surprising divergence), though the actual backup-gated
948
+ * write-open below is the same sanctioned seam every other write uses.
949
+ */
950
+ function runSeedCommand(argv: readonly string[]): number {
951
+ const args = argv.slice(1);
952
+ const force = hasOption(args, "--force");
953
+ const envFlag = optionValue(args, "--env");
954
+ const ambientEnv = process.env.MEMORY_ENV || "production";
955
+ const headerPrints = [`Seeding identity into [${envFlag || ambientEnv}]...\n`];
956
+ if (force) {
957
+ headerPrints.push(" (--force: existing entries will be overwritten from YAML files)\n");
958
+ }
959
+ process.stdout.write(headerPrints.map((line) => `${line}\n`).join(""));
960
+
961
+ let paths: ReturnType<typeof resolveSeedPaths>;
962
+ try {
963
+ paths = resolveSeedPaths(args);
964
+ } catch (error) {
965
+ if (error instanceof MirrorHomeNotConfiguredError) {
966
+ console.error(`Mirror TS front door: ${error.message}`);
967
+ return 2;
968
+ }
969
+ throw error;
970
+ }
971
+
972
+ ensureDatabaseReadyForCli(paths.dbPath, "seed");
973
+ const db = openLiveWriteDatabase(paths.dbPath);
974
+ try {
975
+ assertSchemaState(db);
976
+ const prints = [`Mirror home: ${paths.mirrorHome}`, `Identity root: ${paths.identityRoot}`];
977
+ const result = runSeed(db, paths.identityRoot, { force });
978
+ prints.push(...result.lines);
979
+ prints.push(
980
+ `\nResult: ${result.created} created, ${result.updated} updated, ${result.skipped} skipped`,
981
+ );
982
+ if (result.errors.length > 0) {
983
+ prints.push(`Errors: ${result.errors.length}`);
984
+ for (const err of result.errors) prints.push(` - ${err}`);
985
+ }
986
+ process.stdout.write(prints.map((line) => `${line}\n`).join(""));
987
+ return result.errors.length > 0 ? 1 : 0;
988
+ } catch (error) {
989
+ if (error instanceof SchemaStateError) {
990
+ console.error(`Mirror TS front door: ${error.message}`);
991
+ return 2;
992
+ }
993
+ throw error;
994
+ } finally {
995
+ db.close();
996
+ }
997
+ }
998
+
999
+ /** `consult credits` never logs to llm_calls (Python doesn't either, AI-09) --
1000
+ * so it skips the live-write db-open entirely, matching its current
1001
+ * read-only-external-API shape. */
1002
+ function isConsultCredits(argv: readonly string[]): boolean {
1003
+ return argv[1] === "credits";
1004
+ }
1005
+
1006
+ /**
1007
+ * Route `journey set-path <slug> <path>` to the TS core. Normalizes the path like
1008
+ * Python (`Path.expanduser().resolve()`), writes through the live-write seam after a
1009
+ * backup via the ported setProjectPath, and mirrors Python's output: the resolved
1010
+ * path on stdout, a status line on stderr, and a not-found error + exit 1.
1011
+ */
1012
+ function runJourneyWrite(argv: readonly string[]): number {
1013
+ const args = argv.slice(2);
1014
+ const positionals = stripOptionWithValue(
1015
+ stripOptionWithValue(args, "--db-path"),
1016
+ "--mirror-home",
1017
+ );
1018
+ const slug = positionals[0];
1019
+ const rawPath = positionals[1];
1020
+ if (!slug || !rawPath) {
1021
+ console.error("Usage: journey set-path <slug> <path>");
1022
+ return 2;
1023
+ }
1024
+ return withLiveWriteDb(argv, (db) => {
1025
+ try {
1026
+ const resolved = applyJourneySetPath(db, slug, rawPath, nowIso());
1027
+ console.error(`project_path set for '${slug}': ${resolved}`);
1028
+ process.stdout.write(`${resolved}\n`);
1029
+ return 0;
1030
+ } catch (error) {
1031
+ if (error instanceof JourneyNotFoundError) {
1032
+ console.error(`Error: journey '${slug}' not found.`);
1033
+ return 1;
1034
+ }
1035
+ throw error;
1036
+ }
1037
+ });
1038
+ }
1039
+
1040
+ /**
1041
+ * Route `journey update <slug> <content>` to the TS core (DS7.US1 Slice B).
1042
+ * A thin wrapper over the already-ported `setIdentity`: Python's own
1043
+ * `set_journey_path` is the SAME upsert-identity primitive with a fixed
1044
+ * `journey_path` layer, no created/updated verb distinction (always prints
1045
+ * "updated", matching Python exactly), and no check that the journey slug
1046
+ * itself exists.
1047
+ *
1048
+ * CR073: the stdin sentinel is exactly `-`. A FLAG-SHAPED argument -- one or
1049
+ * two hyphens then a letter -- is a mistyped sentinel (`-stdin`, `--stdin`) or
1050
+ * a stray option, never path text, and is refused before anything is written.
1051
+ * A markdown list (`- item`) or an em-dash lead is not flag-shaped and stays
1052
+ * valid: four of six real journey paths carry lists. Empty content is refused
1053
+ * as `identity set` already does. Messages and exit codes are byte-exact to
1054
+ * the Python oracle (`ts/test/goldens/journey-update.golden.json`), including
1055
+ * the usage path's exit 1, which this route previously returned as 2.
1056
+ */
1057
+ const JOURNEY_UPDATE_USAGE =
1058
+ `Usage: ${PROGRAM} journey update <slug> <content>\n` +
1059
+ " Pass '-' as <content> to read it from stdin.";
1060
+ const FLAG_SHAPED_CONTENT = /^--?[A-Za-z]/;
1061
+
1062
+ function runJourneyUpdateWrite(argv: readonly string[]): number {
1063
+ const args = argv.slice(2);
1064
+ const positionals = stripOptionWithValue(
1065
+ stripOptionWithValue(args, "--db-path"),
1066
+ "--mirror-home",
1067
+ );
1068
+ const slug = positionals[0];
1069
+ let content = positionals[1];
1070
+ if (!slug || content === undefined) {
1071
+ console.error(JOURNEY_UPDATE_USAGE);
1072
+ return 1;
1073
+ }
1074
+ if (content === "-") {
1075
+ content = readStdinContent();
1076
+ } else if (FLAG_SHAPED_CONTENT.test(content)) {
1077
+ console.error(
1078
+ `Error: '${content}' looks like an option, not journey path text. ` +
1079
+ "Pass '-' as <content> to read it from stdin.",
1080
+ );
1081
+ return 1;
1082
+ }
1083
+ if (!content.trim()) {
1084
+ console.error("Error: content is empty.");
1085
+ return 1;
1086
+ }
1087
+ return withLiveWriteDb(argv, (db) => {
1088
+ setIdentity(db, { id: newId(), layer: JOURNEY_PATH_LAYER, key: slug, content }, nowIso());
1089
+ console.error(`Journey path '${slug}' updated.`);
1090
+ return 0;
1091
+ });
1092
+ }
1093
+
1094
+ function isTasksSubcommandWrite(argv: readonly string[]): boolean {
1095
+ return (
1096
+ argv[0] === "tasks" &&
1097
+ (argv[1] === "add" ||
1098
+ argv[1] === "done" ||
1099
+ argv[1] === "doing" ||
1100
+ argv[1] === "block" ||
1101
+ argv[1] === "delete" ||
1102
+ argv[1] === "import" ||
1103
+ argv[1] === "sync" ||
1104
+ argv[1] === "sync-config")
1105
+ );
1106
+ }
1107
+
1108
+ /**
1109
+ * Route `tasks add <title> [--journey J] [--due D] [--stage S]` to the TS core
1110
+ * (DS7.US2 slice 3a). Python's `title` positional accepts exactly one argv
1111
+ * token (no `nargs`); extra unrecognized positionals are an argparse error in
1112
+ * Python, not reproduced here beyond a generic usage message.
1113
+ */
1114
+ function runTasksAddWrite(argv: readonly string[]): number {
1115
+ const args = argv.slice(2);
1116
+ const journey = optionValue(args, "--journey");
1117
+ const due = optionValue(args, "--due");
1118
+ const stage = optionValue(args, "--stage");
1119
+ const title = stripOptionWithValue(
1120
+ stripOptionWithValue(
1121
+ stripOptionWithValue(
1122
+ stripOptionWithValue(stripOptionWithValue(args, "--journey"), "--due"),
1123
+ "--stage",
1124
+ ),
1125
+ "--db-path",
1126
+ ),
1127
+ "--mirror-home",
1128
+ )[0];
1129
+ if (!title) {
1130
+ console.error("Usage: tasks add <title> [--journey J] [--due D] [--stage S]");
1131
+ return 2;
1132
+ }
1133
+ return withLiveWriteDb(argv, (db) => {
1134
+ const task = applyTasksAdd(db, { title, journey, due, stage }, newId(), nowIso());
1135
+ process.stdout.write(renderTasksAdd(task));
1136
+ return 0;
1137
+ });
1138
+ }
1139
+
1140
+ /**
1141
+ * Route `tasks done|doing|block <task_id>` to the TS core (DS7.US2 slice 3a).
1142
+ * Matches Python's `cmd_status_change`: business-logic outcomes (ambiguous,
1143
+ * not found) print to STDOUT and exit 0 -- Python never calls `sys.exit()` in
1144
+ * this command, so no branch here maps to a nonzero exit code either.
1145
+ */
1146
+ function runTasksStatusChangeWrite(argv: readonly string[]): number {
1147
+ // argv[1] is "done" | "doing" | "block" here (isTasksSubcommandWrite already
1148
+ // narrowed it); only "block" needs translating to the Task model's status.
1149
+ const newStatus: TaskStatusTarget =
1150
+ argv[1] === "block" ? "blocked" : (argv[1] as TaskStatusTarget);
1151
+ const args = argv.slice(2);
1152
+ const idOrPrefix = stripOptionWithValue(
1153
+ stripOptionWithValue(args, "--db-path"),
1154
+ "--mirror-home",
1155
+ )[0];
1156
+ if (!idOrPrefix) {
1157
+ console.error(`Usage: tasks ${argv[1]} <task_id>`);
1158
+ return 2;
1159
+ }
1160
+ return withLiveWriteDb(argv, (db) => {
1161
+ const outcome = applyTasksStatusChange(db, idOrPrefix, newStatus, nowIso());
1162
+ process.stdout.write(renderTasksStatusChange(outcome));
1163
+ return 0;
1164
+ });
1165
+ }
1166
+
1167
+ /**
1168
+ * Route `tasks delete <task_id>` to the TS core (DS7.US2 slice 3a). Matches
1169
+ * Python's `cmd_delete`: business-logic "not found" prints to STDOUT and exits
1170
+ * 0, same as status-change.
1171
+ */
1172
+ function runTasksDeleteWrite(argv: readonly string[]): number {
1173
+ const args = argv.slice(2);
1174
+ const idOrPrefix = stripOptionWithValue(
1175
+ stripOptionWithValue(args, "--db-path"),
1176
+ "--mirror-home",
1177
+ )[0];
1178
+ if (!idOrPrefix) {
1179
+ console.error("Usage: tasks delete <task_id>");
1180
+ return 2;
1181
+ }
1182
+ return withLiveWriteDb(argv, (db) => {
1183
+ const outcome = applyTasksDelete(db, idOrPrefix);
1184
+ process.stdout.write(renderTasksDelete(outcome));
1185
+ return 0;
1186
+ });
1187
+ }
1188
+
1189
+ /**
1190
+ * Route `tasks import [journey]` to the TS core (DS7.US2 slice 3c). The
1191
+ * `journey` positional is optional in Python (`nargs="?"`); when absent, every
1192
+ * known journey is attempted.
1193
+ */
1194
+ function runTasksImportWrite(argv: readonly string[]): number {
1195
+ const args = argv.slice(2);
1196
+ const journey =
1197
+ stripOptionWithValue(stripOptionWithValue(args, "--db-path"), "--mirror-home")[0] ?? null;
1198
+ return withLiveWriteDb(argv, (db) => {
1199
+ const results = applyTasksImport(db, journey);
1200
+ process.stdout.write(renderTasksImport(results));
1201
+ return 0;
1202
+ });
1203
+ }
1204
+
1205
+ /**
1206
+ * Route `tasks sync [journey]` to the TS core (DS7.US2 slice 3c). Matches
1207
+ * Python's `cmd_sync`: per-journey errors (no sync file, sync file missing on
1208
+ * disk, or any other failure) print `❌ {journey}: {message}` and move on to
1209
+ * the next journey rather than aborting the whole command; exit is always 0
1210
+ * (no `sys.exit()` anywhere in `cmd_sync`).
1211
+ */
1212
+ function runTasksSyncWrite(argv: readonly string[]): number {
1213
+ const args = argv.slice(2);
1214
+ const journey =
1215
+ stripOptionWithValue(stripOptionWithValue(args, "--db-path"), "--mirror-home")[0] ?? null;
1216
+ return withLiveWriteDb(argv, (db) => {
1217
+ const journeys = resolveSyncJourneys(db, journey);
1218
+ if (journeys.length === 0) {
1219
+ process.stdout.write(renderTasksSyncNoJourneysConfigured());
1220
+ return 0;
1221
+ }
1222
+ const lines: string[] = [];
1223
+ for (const j of journeys) {
1224
+ lines.push(renderTasksSyncOutcome(applyTasksSyncForJourney(db, j)));
1225
+ }
1226
+ process.stdout.write(lines.join(""));
1227
+ return 0;
1228
+ });
1229
+ }
1230
+
1231
+ /**
1232
+ * Route `tasks sync-config <journey> <file_path>` to the TS core (DS7.US2
1233
+ * slice 3c). Matches Python's `cmd_sync_config`: an unknown journey is an
1234
+ * uncaught `ValueError` in Python (no try/except around `set_sync_file`); this
1235
+ * maps it to a clear stderr message and exit 1 instead of fabricating a fake
1236
+ * traceback (the same DS7.US1 `init` precedent).
1237
+ */
1238
+ function runTasksSyncConfigWrite(argv: readonly string[]): number {
1239
+ const args = argv.slice(2);
1240
+ const positionals = stripOptionWithValue(
1241
+ stripOptionWithValue(args, "--db-path"),
1242
+ "--mirror-home",
1243
+ );
1244
+ const journey = positionals[0];
1245
+ const filePath = positionals[1];
1246
+ if (!journey || !filePath) {
1247
+ console.error("Usage: tasks sync-config <journey> <file_path>");
1248
+ return 2;
1249
+ }
1250
+ return withLiveWriteDb(argv, (db) => {
1251
+ try {
1252
+ const outcome = applyTasksSyncConfig(db, journey, filePath, nowIso());
1253
+ process.stdout.write(renderTasksSyncConfig(outcome));
1254
+ return 0;
1255
+ } catch (error) {
1256
+ if (error instanceof JourneyNotFoundError) {
1257
+ console.error(`Error: journey '${journey}' not found.`);
1258
+ return 1;
1259
+ }
1260
+ throw error;
1261
+ }
1262
+ });
1263
+ }
1264
+
1265
+ /** Dispatch a `tasks add|done|doing|block|delete|import|sync|sync-config` write to its sub-handler. */
1266
+ function runTasksWrite(argv: readonly string[]): number {
1267
+ if (argv[1] === "add") return runTasksAddWrite(argv);
1268
+ if (argv[1] === "delete") return runTasksDeleteWrite(argv);
1269
+ if (argv[1] === "import") return runTasksImportWrite(argv);
1270
+ if (argv[1] === "sync-config") return runTasksSyncConfigWrite(argv);
1271
+ if (argv[1] === "sync") return runTasksSyncWrite(argv);
1272
+ return runTasksStatusChangeWrite(argv);
1273
+ }
1274
+
1275
+ // --- consolidate / shadow writes (CV22.DS7.US3) ------------------------------
1276
+
1277
+ /** Route `consolidate reject <proposal_id>` to the TS core. */
1278
+ function runConsolidateRejectWrite(argv: readonly string[]): number {
1279
+ const args = argv.slice(2);
1280
+ const proposalId = stripOptionWithValue(
1281
+ stripOptionWithValue(args, "--db-path"),
1282
+ "--mirror-home",
1283
+ )[0];
1284
+ if (!proposalId) {
1285
+ console.error("Usage: consolidate reject <proposal_id>");
1286
+ return 2;
1287
+ }
1288
+ return withLiveWriteDb(argv, (db) =>
1289
+ writeRendered(renderConsolidateReject(runReject(db, proposalId, nowIso()))),
1290
+ );
1291
+ }
1292
+
1293
+ /** Route `shadow reject <proposal_id>` to the TS core. */
1294
+ function runShadowRejectWrite(argv: readonly string[]): number {
1295
+ const args = argv.slice(2);
1296
+ const proposalId = stripOptionWithValue(
1297
+ stripOptionWithValue(args, "--db-path"),
1298
+ "--mirror-home",
1299
+ )[0];
1300
+ if (!proposalId) {
1301
+ console.error("Usage: shadow reject <proposal_id>");
1302
+ return 2;
1303
+ }
1304
+ return withLiveWriteDb(argv, (db) =>
1305
+ writeRendered(renderShadowReject(runReject(db, proposalId, nowIso()))),
1306
+ );
1307
+ }
1308
+
1309
+ /** Route `shadow apply <proposal_id> [--content "..."]` to the TS core. Deterministic --
1310
+ * no replay gate needed (the write is a hardcoded-layer identity append). */
1311
+ function runShadowApplyWrite(argv: readonly string[]): number {
1312
+ const args = argv.slice(2);
1313
+ const positionals = stripOptionWithValue(
1314
+ stripOptionWithValue(stripOptionWithValue(args, "--content"), "--db-path"),
1315
+ "--mirror-home",
1316
+ );
1317
+ const proposalId = positionals[0];
1318
+ if (!proposalId) {
1319
+ console.error('Usage: shadow apply <proposal_id> [--content "..."]');
1320
+ return 2;
1321
+ }
1322
+ const overrideContent = optionValue(args, "--content");
1323
+ return withLiveWriteDb(argv, (db) => {
1324
+ const outcome = runShadowApplyRoute(db, proposalId, overrideContent, {
1325
+ id: newId(),
1326
+ nowIso: nowIso(),
1327
+ });
1328
+ return writeRendered(renderShadowApply(outcome));
1329
+ });
1330
+ }
1331
+
1332
+ /**
1333
+ * Route `consolidate apply <proposal_id> [--content "..."]` to the TS core.
1334
+ * The whole command is gated on `MIRROR_TS_CULTIVATION_EMBEDDING_REPLAY`
1335
+ * (routing.ts already refuses to route here otherwise) because the action is
1336
+ * read from the DB, not argv -- a `merge` proposal needs the replay embedding
1337
+ * provider even though `identity_update`/`shadow_candidate` do not.
1338
+ */
1339
+ async function runConsolidateApplyWrite(argv: readonly string[]): Promise<number> {
1340
+ const args = argv.slice(2);
1341
+ const positionals = stripOptionWithValue(
1342
+ stripOptionWithValue(stripOptionWithValue(args, "--content"), "--db-path"),
1343
+ "--mirror-home",
1344
+ );
1345
+ const proposalId = positionals[0];
1346
+ if (!proposalId) {
1347
+ console.error('Usage: consolidate apply <proposal_id> [--content "..."]');
1348
+ return 2;
1349
+ }
1350
+ const overrideContent = optionValue(args, "--content");
1351
+ const family = await resolveFamilyProviders(process.env, CULTIVATION_APPLY_TRANSPORT);
1352
+ if (!family?.embedding) throw new Error("consolidate apply requires an embedding provider");
1353
+ const embeddingProvider = family.embedding;
1354
+ return withLiveWriteDbAsync(argv, async (db) => {
1355
+ const outcome = await runConsolidateApplyRoute(
1356
+ db,
1357
+ proposalId,
1358
+ overrideContent,
1359
+ { identityId: newId(), mergeMemoryId: newId(), nowIso: nowIso() },
1360
+ embeddingProvider,
1361
+ );
1362
+ return writeRendered(renderConsolidateApply(outcome));
1363
+ });
1364
+ }
1365
+
1366
+ /**
1367
+ * Route `consolidate scan [--journey J] [--layer L] [--limit N] [--threshold F]`
1368
+ * to the TS core, gated on `MIRROR_TS_CULTIVATION_LLM_REPLAY` (routing.ts).
1369
+ */
1370
+ async function runConsolidateScanWrite(argv: readonly string[]): Promise<number> {
1371
+ const args = argv.slice(2);
1372
+ const journey = optionValue(args, "--journey");
1373
+ const layer = optionValue(args, "--layer");
1374
+ const limitRaw = optionValue(args, "--limit");
1375
+ const limit = limitRaw !== null ? Number(limitRaw) : DEFAULT_CONSOLIDATE_SCAN_LIMIT;
1376
+ const thresholdRaw = optionValue(args, "--threshold");
1377
+ const threshold = thresholdRaw !== null ? Number(thresholdRaw) : DEFAULT_CLUSTER_THRESHOLD;
1378
+ const family = await resolveFamilyProviders(process.env, CULTIVATION_SCAN_TRANSPORT);
1379
+ if (!family?.llm) throw new Error("consolidate scan requires an LLM provider");
1380
+ const provider = family.llm;
1381
+ const reporter = outcomeReporter(argv, "consolidation");
1382
+ return withLiveWriteDbAsync(argv, async (db) => {
1383
+ const result = await consolidateScan(db, {
1384
+ journey,
1385
+ layer,
1386
+ limit,
1387
+ threshold,
1388
+ provider,
1389
+ id: newId,
1390
+ nowIso,
1391
+ onLlmCall: chatLedgerHook(db, "consolidation"),
1392
+ onOutcome: reporter.onOutcome,
1393
+ });
1394
+ reporter.logSummary();
1395
+ process.stdout.write(renderConsolidateScan(result, threshold));
1396
+ return 0;
1397
+ });
1398
+ }
1399
+
1400
+ /** Route `shadow scan [--limit N]` to the TS core, gated on `MIRROR_TS_CULTIVATION_LLM_REPLAY`. */
1401
+ async function runShadowScanWrite(argv: readonly string[]): Promise<number> {
1402
+ const args = argv.slice(2);
1403
+ const limitRaw = optionValue(args, "--limit");
1404
+ const limit = limitRaw !== null ? Number(limitRaw) : DEFAULT_SHADOW_SCAN_LIMIT;
1405
+ const family = await resolveFamilyProviders(process.env, CULTIVATION_SCAN_TRANSPORT);
1406
+ if (!family?.llm) throw new Error("shadow scan requires an LLM provider");
1407
+ const provider = family.llm;
1408
+ const reporter = outcomeReporter(argv, "shadow_scan");
1409
+ return withLiveWriteDbAsync(argv, async (db) => {
1410
+ const result = await shadowScan(db, {
1411
+ limit,
1412
+ provider,
1413
+ id: newId,
1414
+ nowIso,
1415
+ onLlmCall: chatLedgerHook(db, "shadow_scan"),
1416
+ onOutcome: reporter.onOutcome,
1417
+ });
1418
+ reporter.logSummary();
1419
+ process.stdout.write(renderShadowScan(result));
1420
+ return 0;
1421
+ });
1422
+ }
1423
+
1424
+ function isConsolidateSubcommandWrite(argv: readonly string[]): boolean {
1425
+ return (
1426
+ argv[0] === "consolidate" && (argv[1] === "reject" || argv[1] === "apply" || argv[1] === "scan")
1427
+ );
1428
+ }
1429
+
1430
+ function isShadowSubcommandWrite(argv: readonly string[]): boolean {
1431
+ return (
1432
+ argv[0] === "shadow" && (argv[1] === "reject" || argv[1] === "apply" || argv[1] === "scan")
1433
+ );
1434
+ }
1435
+
1436
+ /** Dispatch a `consolidate reject|apply|scan` write to its sub-handler. */
1437
+ function runConsolidateWrite(argv: readonly string[]): number | Promise<number> {
1438
+ if (argv[1] === "reject") return runConsolidateRejectWrite(argv);
1439
+ if (argv[1] === "apply") return runConsolidateApplyWrite(argv);
1440
+ return runConsolidateScanWrite(argv); // "scan"
1441
+ }
1442
+
1443
+ /** Dispatch a `shadow reject|apply|scan` write to its sub-handler. */
1444
+ function runShadowWrite(argv: readonly string[]): number | Promise<number> {
1445
+ if (argv[1] === "reject") return runShadowRejectWrite(argv);
1446
+ if (argv[1] === "apply") return runShadowApplyWrite(argv);
1447
+ return runShadowScanWrite(argv); // "scan"
1448
+ }
1449
+
1450
+ function withMirrorReadDb(argv: readonly string[], read: (db: Database) => number): number {
1451
+ const dbPath = resolveDbPathForCli(argv.slice(1));
1452
+ if (dbPath === null) return 2;
1453
+ ensureDatabaseReadyForCli(dbPath, argv[0] ?? null);
1454
+ const db = openDatabaseReadOnly(dbPath);
1455
+ try {
1456
+ assertSchemaState(db);
1457
+ return read(db);
1458
+ } catch (error) {
1459
+ if (error instanceof SchemaStateError) {
1460
+ console.error(`Mirror TS front door: ${error.message}`);
1461
+ return 2;
1462
+ }
1463
+ throw error;
1464
+ } finally {
1465
+ db.close();
1466
+ }
1467
+ }
1468
+
1469
+ async function withMirrorWriteDb(
1470
+ argv: readonly string[],
1471
+ write: (db: WritableDatabase, dbPath: string) => Promise<number> | number,
1472
+ ): Promise<number> {
1473
+ const dbPath = resolveDbPathForCli(argv.slice(1));
1474
+ if (dbPath === null) return 2;
1475
+ ensureDatabaseReadyForCli(dbPath, argv[0] ?? null);
1476
+ const db = openLiveWriteDatabase(dbPath);
1477
+ try {
1478
+ assertSchemaState(db);
1479
+ return await write(db, dbPath);
1480
+ } catch (error) {
1481
+ if (error instanceof SchemaStateError) {
1482
+ console.error(`Mirror TS front door: ${error.message}`);
1483
+ return 2;
1484
+ }
1485
+ throw error;
1486
+ } finally {
1487
+ db.close();
1488
+ }
1489
+ }
1490
+
1491
+ function runMirrorWrite(argv: readonly string[]): Promise<number> | number {
1492
+ if (mirrorDeactivateMissingSession(argv)) {
1493
+ console.error("warning: mirror deactivate invoked without --session-id; no state change.");
1494
+ return 0;
1495
+ }
1496
+ const reporter = outcomeReporter(argv, "reception");
1497
+ return withMirrorWriteDb(argv, async (db, dbPath) => {
1498
+ const exitCode = await runMirrorWriteRoute(db, dbPath, argv, process.env, {
1499
+ onReceptionOutcome: reporter.onOutcome,
1500
+ });
1501
+ // Only a `--query` run reaches reception; a deterministic `mirror load`
1502
+ // makes no call, and a `calls=0` line for it would be noise.
1503
+ if (reporter.calls > 0) reporter.logSummary();
1504
+ return exitCode;
1505
+ });
1506
+ }
1507
+
1508
+ function runModeWrite(argv: readonly string[]): Promise<number> {
1509
+ return withMirrorWriteDb(argv, (db) => runModeWriteRoute(db, argv));
1510
+ }
1511
+
1512
+ function runSoulWrite(argv: readonly string[]): Promise<number> {
1513
+ return withMirrorWriteDb(argv, (db) => runSoulRoute(db, argv));
1514
+ }
1515
+
1516
+ function runExploreWrite(argv: readonly string[]): Promise<number> {
1517
+ return withMirrorWriteDb(argv, (db, dbPath) =>
1518
+ runExploreRoute(db, argv, {
1519
+ ...defaultExploreRouteDeps(),
1520
+ // `story promote` ends in a Builder session start, so it needs the same
1521
+ // wiring plateau 8's `build` route will use: the composed transport, the
1522
+ // close tail, and the clone-role guard. Lazy, so the other eleven story
1523
+ // actions never touch the provider seam.
1524
+ buildLoadRuntime: () => createBuildLoadRuntime({ db, dbPath }),
1525
+ }),
1526
+ );
1527
+ }
1528
+
1529
+ /** CV22.DS7.US5: the conversation logger, the product's primary write path. */
1530
+ function runConversationLoggerWrite(argv: readonly string[]): Promise<number> {
1531
+ return withMirrorWriteDb(argv, (db, dbPath) => runConversationLoggerRoute(db, dbPath, argv));
1532
+ }
1533
+
1534
+ /**
1535
+ * Surface a provider-backed call's outcome into the front-door log
1536
+ * (CV22.DS8.US3).
1537
+ *
1538
+ * One SUMMARY line per invocation, always -- it carries `calls=N` and the full
1539
+ * outcome distribution, which is what makes a fan-out visible after the fact
1540
+ * and what the live smoke reads instead of counting ledger rows. Plus one line
1541
+ * per NON-answered call, because only those carry a `kind=` worth having; a
1542
+ * line per successful call would triple the log for no diagnostic gain.
1543
+ *
1544
+ * Category only: no cluster content, no query, no model output.
1545
+ */
1546
+ function outcomeReporter(
1547
+ argv: readonly string[],
1548
+ surface: string,
1549
+ ): {
1550
+ onOutcome: (report: ProviderCallReport) => void;
1551
+ logSummary: () => void;
1552
+ readonly calls: number;
1553
+ } {
1554
+ const logPath = resolveLogPath(argv);
1555
+ const tally = new CallOutcomeTally();
1556
+ return {
1557
+ get calls() {
1558
+ return tally.calls;
1559
+ },
1560
+ onOutcome: (report) => {
1561
+ tally.record(report);
1562
+ if (report.outcome === "answered") return;
1563
+ logFrontDoor(logPath, {
1564
+ command: argv[0] ?? null,
1565
+ route: "ts",
1566
+ exitCode: 0,
1567
+ detail: formatCallOutcome(surface, report),
1568
+ });
1569
+ },
1570
+ logSummary: () => {
1571
+ logFrontDoor(logPath, {
1572
+ command: argv[0] ?? null,
1573
+ route: "ts",
1574
+ exitCode: 0,
1575
+ detail: `${surface} ${tally.summary()}`,
1576
+ });
1577
+ },
1578
+ };
1579
+ }
1580
+
1581
+ /** Best-effort log path from the same resolver; null when unconfigured. */
1582
+ function resolveLogPath(argv: readonly string[]): string | null {
1583
+ try {
1584
+ return frontDoorLogPath(resolveDbPath(argv));
1585
+ } catch {
1586
+ return null;
1587
+ }
1588
+ }
1589
+
1590
+ /**
1591
+ * `consult <model> <question>` logs to llm_calls on a best-effort basis
1592
+ * (AI-09). Unlike `runMemorySearch`/`runIdentityWrite`/`runJourneyWrite`,
1593
+ * consult's established contract does NOT require a database to exist --
1594
+ * it is fundamentally an external-API command, and logging is an added,
1595
+ * optional side effect. If a db cannot be resolved, opened, backed up, or
1596
+ * schema-checked for any reason, consult must still run; it just logs
1597
+ * nothing -- the same fail-soft principle `logLlmCall` applies to the write
1598
+ * itself, extended to db resolution.
1599
+ */
1600
+ async function runConsultAskWithDb(argv: readonly string[]): Promise<number> {
1601
+ const db = tryOpenDbForConsultLogging(argv);
1602
+ try {
1603
+ process.stdout.write(await runConsultRoute(db, argv.slice(1)));
1604
+ return 0;
1605
+ } finally {
1606
+ db?.close();
1607
+ }
1608
+ }
1609
+
1610
+ export function tryOpenDbForConsultLogging(argv: readonly string[]): WritableDatabase | null {
1611
+ let db: WritableDatabase | null = null;
1612
+ try {
1613
+ // Uses resolveDbPath directly, NOT resolveDbPathForCli -- that wrapper
1614
+ // prints a user-facing error on an unconfigured mirror home, which is
1615
+ // correct for commands that require a db but wrong here: consult without
1616
+ // a configured home must stay completely silent about logging, exactly
1617
+ // as it behaved before this CR.
1618
+ const dbPath = resolveDbPath(argv.slice(1));
1619
+ if (!existsSync(dbPath)) return null;
1620
+ db = openLiveWriteDatabase(dbPath);
1621
+ assertSchemaState(db);
1622
+ return db;
1623
+ } catch {
1624
+ db?.close();
1625
+ return null;
1626
+ }
1627
+ }
1628
+
1629
+ /**
1630
+ * `mcp` (CV22.DS10.TS5, F9): the oracle's command, answered by the server
1631
+ * CV22.DS9 ported. The module IS the server -- importing it serves stdio, and
1632
+ * the import settles when the client closes stdin. The plugin launches the same
1633
+ * module directly; this is the front door's way in, not a second server.
1634
+ */
1635
+ async function runMcpServer(): Promise<number> {
1636
+ await import("#mcp/main.ts");
1637
+ return 0;
1638
+ }
1639
+
1640
+ async function runMemorySearch(argv: readonly string[]): Promise<number> {
1641
+ const dbPath = resolveDbPathForCli(argv.slice(1));
1642
+ if (dbPath === null) return 2;
1643
+ // Missing DB => unbootstrapped install; TS bootstraps it (CV22.DS6.TS4) and
1644
+ // applies any pending TS-authored migration (US3) before the backup-gated
1645
+ // search read/log path opens it.
1646
+ ensureDatabaseReadyForCli(dbPath, argv[0] ?? null);
1647
+ const db = openLiveWriteDatabase(dbPath);
1648
+ try {
1649
+ assertSchemaState(db);
1650
+ process.stdout.write(
1651
+ await runMemorySearchRoute(db, argv.slice(1), {
1652
+ onDegraded: (detail) =>
1653
+ logFrontDoor(frontDoorLogPath(dbPath), {
1654
+ command: argv[0] ?? null,
1655
+ route: "ts",
1656
+ exitCode: 0,
1657
+ detail,
1658
+ }),
1659
+ }),
1660
+ );
1661
+ return 0;
1662
+ } catch (error) {
1663
+ if (error instanceof SchemaStateError) {
1664
+ console.error(`Mirror TS front door: ${error.message}`);
1665
+ return 2;
1666
+ }
1667
+ throw error;
1668
+ } finally {
1669
+ db.close();
1670
+ }
1671
+ }
1672
+
1673
+ /**
1674
+ * The exit code, with the content-free route metadata a route may add for the
1675
+ * durable front-door log.
1676
+ *
1677
+ * (Until CV22.DS10.TS5 this also carried which ENGINE answered, because a
1678
+ * TypeScript route could still fall back to Python inside dispatch and the log
1679
+ * records the outcome, not the intent -- RS009 CR059/CR064. Nothing falls back
1680
+ * now; every outcome is TypeScript's.)
1681
+ */
1682
+ interface DispatchOutcome {
1683
+ exitCode: number;
1684
+ /** Content-free route metadata for the durable front-door log. */
1685
+ detail?: string;
1686
+ }
1687
+
1688
+ async function dispatch(argv: readonly string[]): Promise<DispatchOutcome> {
1689
+ const result = await dispatchTs(argv);
1690
+ return typeof result === "number" ? { exitCode: result } : result;
1691
+ }
1692
+
1693
+ /** Every route the front door answers once routing has chosen it. */
1694
+ async function dispatchTs(argv: readonly string[]): Promise<number | DispatchOutcome> {
1695
+ if (isConversationLoggerCommand(argv)) return runConversationLoggerWrite(argv);
1696
+ if (argv[0] === "build") {
1697
+ // The Builder command tree is deliberately lazy: the other commands never
1698
+ // pay for loading it, and a broken Builder core cannot take them down.
1699
+ const { runBuildRoute } = await import("./buildRoute.ts");
1700
+ return runBuildRoute(argv, {
1701
+ withReadOnlyDatabase: (run) => withMirrorReadDb(argv, run),
1702
+ withWritableDatabase: (run) => withMirrorWriteDb(argv, run),
1703
+ createLoadRuntime: (db, dbPath, ignoreProductionRole) =>
1704
+ createBuildLoadRuntime({ db, dbPath, ignoreProductionRole }),
1705
+ nowIso,
1706
+ environmentSessionId: process.env.MIRROR_SESSION_ID ?? null,
1707
+ });
1708
+ }
1709
+ if (isInit(argv)) return runInit(argv);
1710
+ if (isSeed(argv)) return runSeedCommand(argv);
1711
+ if (isIdentityWrite(argv)) return runIdentityWrite(argv);
1712
+ if (isIdentityEdit(argv)) return runIdentityEditRoute(argv);
1713
+ // CV22.DS7.TS4: the extension catalog family. Lazy like the Builder tree, and
1714
+ // for the same reason: the other commands never pay for loading it.
1715
+ if (isExtensionCatalogCommand(argv)) return runExtensionCatalog(argv);
1716
+ if (isLifecycleWriteFace(argv)) return runLifecycleWriteFace(argv);
1717
+ if (isJourneyWrite(argv)) return runJourneyWrite(argv);
1718
+ if (isJourneyUpdateWrite(argv)) return runJourneyUpdateWrite(argv);
1719
+ if (isTasksSubcommandWrite(argv)) return runTasksWrite(argv);
1720
+ if (isWeekSaveWrite(argv)) return runWeekSaveWrite(argv);
1721
+ if (argv[0] === "week" && argv[1] === "plan") return runWeekPlanContentRoute(argv);
1722
+ if (argv[0] === "journal") return runJournalContentRoute(argv);
1723
+ if (argv[0] === "descriptor" && argv[1] === "generate") return runDescriptorContentRoute(argv);
1724
+ if (isConsolidateSubcommandWrite(argv)) return runConsolidateWrite(argv);
1725
+ if (isShadowSubcommandWrite(argv)) return runShadowWrite(argv);
1726
+ if (isMirrorWrite(argv)) return runMirrorWrite(argv);
1727
+ if (isModeWrite(argv)) return runModeWrite(argv);
1728
+ // CV22.DS7.US6: the Soul ritual; `routing.ts` allowlists its subcommands by
1729
+ // name.
1730
+ if (argv[0] === "soul") return runSoulWrite(argv);
1731
+ if (argv[0] === "explore") return runExploreWrite(argv);
1732
+ if (isConversationsAppend(argv)) return runConversationsAppend(argv);
1733
+ // CV22.DS7.TS1: the DB safety tools. `backup` never opens or bootstraps the
1734
+ // database; `repair-encoding --apply` rides the live-write seam.
1735
+ if (argv[0] === "backup") {
1736
+ return runBackupRoute(argv, {
1737
+ // CR060: the backup reports an unresolved home itself, in one line.
1738
+ resolveDbPath: (args) => resolveDbPath(args),
1739
+ withLiveWriteDb: (dbPath, write) => withLiveWriteDbAt(dbPath, "backup", write),
1740
+ });
1741
+ }
1742
+ if (argv[0] === "repair-encoding") {
1743
+ return runRepairEncodingRoute(argv, {
1744
+ resolveDbPath: resolveDbPathForCli,
1745
+ withLiveWriteDb: (dbPath, write) => withLiveWriteDbAt(dbPath, "repair-encoding", write),
1746
+ });
1747
+ }
1748
+ // CV22.DS7.TS3: the daily-visible tail. Both are read-only and open the
1749
+ // database, when at all, through a read-only handle -- no bootstrap, no
1750
+ // migrate-on-open, and for `welcome --status-line` no subprocess whatsoever.
1751
+ if (argv[0] === "welcome") return runWelcomeRoute(argv);
1752
+ if (argv[0] === "runtime") return runRuntimeReadRoute(argv);
1753
+ if (argv[0] === "mcp") return runMcpServer();
1754
+ if (isMemorySearch(argv)) return runMemorySearch(argv);
1755
+ if (isConsult(argv)) {
1756
+ if (isConsultCredits(argv)) {
1757
+ process.stdout.write(await runConsultRoute(null, argv.slice(1)));
1758
+ return 0;
1759
+ }
1760
+ return runConsultAskWithDb(argv);
1761
+ }
1762
+ return runTs(argv);
1763
+ }
1764
+
1765
+ export async function main(rawArgv = process.argv.slice(2)): Promise<number> {
1766
+ const nodeError = nodeVersionError(process.versions.node);
1767
+ if (nodeError) {
1768
+ console.error(nodeError);
1769
+ return 1;
1770
+ }
1771
+ // Once, before routing, and the SAME argv goes to dispatch: a family's
1772
+ // leading options move after its subcommand (CV22.DS10.TS5, finding F5), so
1773
+ // the router and every handler read one shape.
1774
+ const argv = canonicalArgv(rawArgv);
1775
+ const decision = routeMemoryCommand(argv);
1776
+ const logPath = resolveLogPath(argv);
1777
+ // CV22.DS10.TS4: a surface this migration removed answers here, before any
1778
+ // dispatch -- so nothing is spawned, no database is opened, and NO STDIN IS
1779
+ // READ. `journey mutate` took JSON on stdin; refusing before the read is what
1780
+ // makes the refusal immediate instead of a hang, and what keeps the caller's
1781
+ // payload out of this process entirely.
1782
+ if (decision.engine === "retired") {
1783
+ process.stderr.write(retiredRefusal(decision));
1784
+ logFrontDoor(logPath, {
1785
+ command: decision.command,
1786
+ route: "retired",
1787
+ exitCode: 1,
1788
+ // Metadata only, and constant per entry: the anchor names the cutoff,
1789
+ // never the arguments the caller passed to the removed command.
1790
+ detail: `cutoff=${decision.anchor}`,
1791
+ });
1792
+ return 1;
1793
+ }
1794
+ // CV22.DS10.TS5 (D2): a name the front door does not answer. Same posture as
1795
+ // a retired surface -- answered before dispatch, so nothing is opened, read
1796
+ // from stdin, or spawned -- and logged by SCOPE only: an unknown top-level
1797
+ // name is whatever the caller typed, and it stays out of the log.
1798
+ if (decision.engine === "usage") {
1799
+ const env = process.env;
1800
+ const answer = answerUsage(decision.request, {
1801
+ userConfigured: Boolean(env.MIRROR_USER || env.MIRROR_HOME || env.MEMORY_DIR || env.DB_PATH),
1802
+ configFile: configFilePath(env),
1803
+ });
1804
+ (answer.stream === "stdout" ? process.stdout : process.stderr).write(answer.text);
1805
+ logFrontDoor(logPath, {
1806
+ command: decision.command,
1807
+ route: "usage",
1808
+ exitCode: answer.exitCode,
1809
+ detail: `usage=${decision.request.scope}`,
1810
+ });
1811
+ return answer.exitCode;
1812
+ }
1813
+ try {
1814
+ const outcome = await dispatch(argv);
1815
+ logFrontDoor(logPath, {
1816
+ command: decision.command,
1817
+ route: decision.engine,
1818
+ exitCode: outcome.exitCode,
1819
+ ...(outcome.detail ? { detail: outcome.detail } : {}),
1820
+ });
1821
+ return outcome.exitCode;
1822
+ } catch (error) {
1823
+ // Metadata-only: the error's name/category, never argument values.
1824
+ const detail = error instanceof Error ? error.name : "unknown error";
1825
+ // A refusal no route answered is still a refusal: one line, by name, and
1826
+ // never a stack trace (`namedRefusal.ts`).
1827
+ const refusal = namedRefusal(error);
1828
+ if (refusal !== null) {
1829
+ console.error(refusal.line);
1830
+ logFrontDoor(logPath, {
1831
+ command: decision.command,
1832
+ route: decision.engine,
1833
+ exitCode: refusal.exitCode,
1834
+ detail,
1835
+ });
1836
+ return refusal.exitCode;
1837
+ }
1838
+ logFrontDoor(logPath, {
1839
+ command: decision.command,
1840
+ route: decision.engine,
1841
+ exitCode: 1,
1842
+ detail,
1843
+ });
1844
+ throw error;
1845
+ }
1846
+ }
1847
+
1848
+ /**
1849
+ * What the entry does, for the two ways the program is started: this file run
1850
+ * directly (a checkout, `npm link`), or the `bin/mirror.js` shim that an
1851
+ * installed package needs because Node will not strip types under
1852
+ * `node_modules` (CV22.DS10.US3, D15). Configuration and the warning filter
1853
+ * run here and nowhere else, so importing this module does neither.
1854
+ */
1855
+ export async function runAsEntry(rawArgv = process.argv.slice(2)): Promise<number> {
1856
+ silenceExperimentalWarnings();
1857
+ loadConfiguration({ entryPath: import.meta.filename });
1858
+ return main(rawArgv);
1859
+ }
1860
+
1861
+ // Run only when invoked as the CLI entry, not when imported (keeps the module
1862
+ // importable for tests and tooling, and keeps `process.env` untouched there).
1863
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
1864
+ process.exitCode = await runAsEntry();
1865
+ }