mirror-mind 0.0.1 → 0.33.0

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