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,179 @@
1
+ // Shared machinery for every Mirror hook entry.
2
+ //
3
+ // Two jobs, both learned from what the Python hooks did and what they hid.
4
+ //
5
+ // **Reuse the front door rather than reimplement it.** `cli.ts` exports
6
+ // `main(argv)`, so a hook runs the exact route a skill or the Pi extension
7
+ // runs -- same routing, same front-door log, same guards -- in ONE process.
8
+ // The Claude inject hook used to start the interpreter up to five times per
9
+ // prompt; it now starts Node once.
10
+ //
11
+ // **Never fail silently.** Every Python hook ended in `2>/dev/null || true`,
12
+ // which was survivable because `python3` is on every macOS at /usr/bin. `node`
13
+ // usually is not: it lives under nvm or Homebrew, and a GUI-launched runtime
14
+ // does not inherit that PATH. The same silence would therefore hide a NEW
15
+ // failure mode -- no logging, no inject, no close tail -- discovered weeks
16
+ // later as "Mirror stopped remembering". So a hook that cannot do its job
17
+ // writes one line to `<mirror-home>/hooks.log`, and `runtime diagnose` reports
18
+ // whether Node is resolvable from hook context.
19
+
20
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
21
+ import { dirname, join } from "node:path";
22
+
23
+ import { resolveMirrorHome } from "#frontDoor/dbPath.ts";
24
+ import { provideStdin } from "#frontDoor/stdinSource.ts";
25
+ import { runningTreeRoot } from "#runtime/treeRoot.ts";
26
+
27
+ /** Where a hook records that it could not do its job. */
28
+ export function hookLogPath(env: NodeJS.ProcessEnv = process.env): string | null {
29
+ try {
30
+ const home = resolveMirrorHome(env as Parameters<typeof resolveMirrorHome>[0]);
31
+ return join(home, "hooks.log");
32
+ } catch {
33
+ // An unconfigured home has nowhere to complain to. Better than throwing
34
+ // from inside a hook, which would surface as a broken turn.
35
+ return null;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Append one line about a hook that could not complete.
41
+ *
42
+ * Redaction is the contract, not a nicety: this file records that something
43
+ * failed and which hook it was, NEVER the prompt or the response. The
44
+ * front-door log has the same rule (`hookPayloadRedaction.test.ts`), and hook
45
+ * entries bypass the CLI where that rule is enforced, so it is restated here
46
+ * and tested directly.
47
+ */
48
+ export function noteHookFailure(hook: string, reason: string, env = process.env): void {
49
+ const path = hookLogPath(env);
50
+ if (!path) return;
51
+ try {
52
+ mkdirSync(dirname(path), { recursive: true });
53
+ const line = `${new Date().toISOString()} ${hook}: ${reason.split("\n")[0]}\n`;
54
+ appendFileSync(path, line, { encoding: "utf8", mode: 0o600 });
55
+ } catch {
56
+ // A hook cannot report that it could not report. Stop here.
57
+ }
58
+ }
59
+
60
+ /**
61
+ * The Operating Instructions a SessionStart hook should hand its runtime, or
62
+ * null when the project already carries this same file.
63
+ *
64
+ * CV22.DS10.US3, decision D13. An installed package reaches Claude Code and
65
+ * Gemini CLI through hooks, skills, and an MCP server; none of those reads a
66
+ * CLAUDE.md or GEMINI.md, so a session wired to the package would answer
67
+ * `/mm-mirror` and still route no mode, sign no persona, and hold no Builder
68
+ * boundary. Both runtimes read SessionStart's
69
+ * `hookSpecificOutput.additionalContext`, so the session-start hook returns
70
+ * `AGENTS.md` from the tree THIS code runs from -- the instructions always
71
+ * match the core they ship with -- unless the project the session opened in
72
+ * is that same tree (the checkout, whose CLAUDE.md imports the same file), in
73
+ * which case a second copy is noise. A file that cannot be read is recorded:
74
+ * a Mirror session without its instructions is not a Mirror session.
75
+ */
76
+ export function operatingInstructionsFor(
77
+ hook: string,
78
+ projectDir: string | undefined,
79
+ ): string | null {
80
+ try {
81
+ const ours = join(runningTreeRoot(), "AGENTS.md");
82
+ const theirs = projectDir ? join(projectDir, "AGENTS.md") : "";
83
+ if (theirs && existsSync(theirs) && realpathSync(theirs) === realpathSync(ours)) return null;
84
+ return readFileSync(ours, "utf8");
85
+ } catch (error) {
86
+ noteHookFailure(
87
+ hook,
88
+ `operating instructions not delivered: ${error instanceof Error ? error.message : String(error)}`,
89
+ );
90
+ return null;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * The SessionStart answer both runtimes read: context as JSON, or `nothing`
96
+ * -- what each runtime's hook printed before D13 (Gemini `{}`, Claude none).
97
+ */
98
+ export function sessionStartOutput(instructions: string | null, nothing = ""): string {
99
+ if (instructions === null) return nothing;
100
+ return `${JSON.stringify({
101
+ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: instructions },
102
+ })}\n`;
103
+ }
104
+
105
+ export interface FrontDoorRun {
106
+ readonly code: number;
107
+ readonly stdout: string;
108
+ }
109
+
110
+ export interface FrontDoorRunOptions {
111
+ /**
112
+ * A payload the entry already read from fd 0, for a route that reads its
113
+ * payload from stdin (CV21.DS2.US8). The route takes it instead of fd 0.
114
+ */
115
+ readonly stdin?: string;
116
+ }
117
+
118
+ /**
119
+ * Run a front-door command in this process, capturing what it printed.
120
+ *
121
+ * The capture is what lets the inject hook decide whether context was actually
122
+ * produced -- the shell version tested `[ -n "$CONTEXT" ]` on command
123
+ * substitution, and this is the same test without a second process.
124
+ */
125
+ export async function runFrontDoor(
126
+ argv: readonly string[],
127
+ options: FrontDoorRunOptions = {},
128
+ ): Promise<FrontDoorRun> {
129
+ const { main } = await import("#frontDoor/cli.ts");
130
+ if (options.stdin !== undefined) provideStdin(options.stdin);
131
+ const chunks: string[] = [];
132
+ const original = process.stdout.write.bind(process.stdout);
133
+ // biome-ignore lint/suspicious/noExplicitAny: matching Node's overloaded write signature
134
+ (process.stdout as any).write = (chunk: any, ...rest: any[]): boolean => {
135
+ chunks.push(typeof chunk === "string" ? chunk : String(chunk));
136
+ const callback = rest.find((argument) => typeof argument === "function");
137
+ if (callback) callback();
138
+ return true;
139
+ };
140
+ try {
141
+ const code = await main([...argv]);
142
+ return { code, stdout: chunks.join("") };
143
+ } finally {
144
+ process.stdout.write = original;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * The command a failure line may name: the family and its subcommand, never
150
+ * the arguments. Arguments are where hooks put what the user said and what the
151
+ * model answered -- the Gemini logger passes the prompt and the response as
152
+ * argv -- and the first version of the failure line printed all of them (TS5
153
+ * handoff review, finding P1).
154
+ */
155
+ function commandName(argv: readonly string[]): string {
156
+ return argv.slice(0, 2).join(" ");
157
+ }
158
+
159
+ /**
160
+ * Run a front-door command for its effect, swallowing failure the way the
161
+ * shell hooks did with `|| true` -- but recording it. Returns what the route
162
+ * printed, for an entry that reads an outcome line from it.
163
+ */
164
+ export async function runFrontDoorQuietly(
165
+ hook: string,
166
+ argv: readonly string[],
167
+ options: FrontDoorRunOptions = {},
168
+ ): Promise<string> {
169
+ try {
170
+ const result = await runFrontDoor(argv, options);
171
+ if (result.code !== 0) {
172
+ noteHookFailure(hook, `\`${commandName(argv)}\` exited ${result.code}`);
173
+ }
174
+ return result.stdout;
175
+ } catch (error) {
176
+ noteHookFailure(hook, error instanceof Error ? error.message : String(error));
177
+ return "";
178
+ }
179
+ }
@@ -0,0 +1,34 @@
1
+ // Which runtime a Claude-schema hook is running in (CV21.DS2.US8).
2
+ //
3
+ // Three runtimes run the plugin's hooks as they are: Claude Code, Codex, and
4
+ // Grok. The hooks are Mirror's `claude:*` family, and until this story every
5
+ // session they logged was labelled `claude_code` (DS12.TS2 L7, TS4 L7). Each
6
+ // runtime leaves a mark the others do not:
7
+ //
8
+ // * Grok's runner sets `GROK_HOOK_EVENT` and `GROK_SESSION_ID` on every hook
9
+ // process (Grok's bundled guide, "Runner-injected variables"), and its
10
+ // payload names the event in camelCase, `hookEventName` (TS4 L8);
11
+ // * Codex's payload carries `turn_id` (TS2 L11), which Claude Code 2.1.296's
12
+ // common hook input does not: it carries `prompt_id`, read from the binary;
13
+ // * Claude Code is what remains.
14
+ //
15
+ // Grok is tested first because it aliases some fields into snake_case and may
16
+ // alias more; its own name for the event is the mark that cannot be a
17
+ // coincidence. A label is attribution, never authority: nothing privileged
18
+ // keys off it, so a payload that claimed another runtime's mark would mislabel
19
+ // its own session and nothing else.
20
+
21
+ import type { HookPayloadObject } from "#conversation/hookPayload.ts";
22
+
23
+ export type HookRuntime = "claude_code" | "codex" | "grok_build";
24
+
25
+ export function detectHookRuntime(
26
+ payload: HookPayloadObject,
27
+ env: NodeJS.ProcessEnv = process.env,
28
+ ): HookRuntime {
29
+ if (env.GROK_HOOK_EVENT || env.GROK_SESSION_ID || typeof payload.hookEventName === "string") {
30
+ return "grok_build";
31
+ }
32
+ if (typeof payload.turn_id === "string" && payload.turn_id !== "") return "codex";
33
+ return "claude_code";
34
+ }
@@ -0,0 +1,67 @@
1
+ // TS port of `IdentityService.apply_consolidation_identity_update` (AI-23,
2
+ // CV9.E2.S23) -- the identity-write allowlist gate behind `consolidate apply`'s
3
+ // `identity_update` action (CV22.DS7.US3).
4
+ //
5
+ // Unlike `setIdentity` (general-purpose, unconditional write used for seeding,
6
+ // Soul Mode integration, `identity set`, etc.), this is narrowly for the
7
+ // propose_consolidation() -> mm-consolidate accept flow: `target_layer` is
8
+ // model-chosen and untrusted, so it is checked against
9
+ // `VALID_IDENTITY_UPDATE_LAYERS` before anything is written. A rejected layer
10
+ // throws -- it never silently no-ops, never redirects, and never writes a
11
+ // partial row. This is the ONE path `consolidate apply`'s `identity_update`
12
+ // action may reach an identity write through; it must never call the ungated
13
+ // `upsertIdentity`/`setIdentity` directly for that action.
14
+ //
15
+ // Preserves the accept flow's existing append semantics for an allowed write:
16
+ // new content is appended after a blank line, never replaces existing content
17
+ // outright -- the same shape as Python's `f"{existing.content.rstrip()}\n\n{content}"`.
18
+
19
+ import type { WritableDatabase } from "#db/database.ts";
20
+ import { getIdentityContent } from "./identityRead.ts";
21
+ import { type IdentityRow, upsertIdentity } from "./identityStore.ts";
22
+
23
+ /** Mirrors `models.VALID_IDENTITY_UPDATE_LAYERS = frozenset({"self", "ego"})`. */
24
+ export const VALID_IDENTITY_UPDATE_LAYERS: ReadonlySet<string> = new Set(["self", "ego"]);
25
+
26
+ export interface ApplyConsolidationIdentityUpdateParams {
27
+ targetLayer: string;
28
+ targetKey: string;
29
+ content: string;
30
+ /** Injected id/now, the DS4 determinism idiom -- used only on INSERT. */
31
+ id: string;
32
+ nowIso: string;
33
+ }
34
+
35
+ /**
36
+ * Refuse a non-allowlisted `targetLayer` with the exact Python message and no
37
+ * write at all (no row, no partial row); otherwise append-or-create at parity.
38
+ *
39
+ * The thrown message is byte-identical to Python's:
40
+ * `Refusing identity_update to layer {target_layer!r}: not in the
41
+ * consolidation allowlist {sorted(VALID_IDENTITY_UPDATE_LAYERS)}.` --
42
+ * `sorted({"self", "ego"})` is `['ego', 'self']`, and Python's `!r` repr of a
43
+ * str is single-quoted.
44
+ */
45
+ export function applyConsolidationIdentityUpdate(
46
+ db: WritableDatabase,
47
+ params: ApplyConsolidationIdentityUpdateParams,
48
+ ): void {
49
+ if (!VALID_IDENTITY_UPDATE_LAYERS.has(params.targetLayer)) {
50
+ throw new Error(
51
+ `Refusing identity_update to layer '${params.targetLayer}': not in ` +
52
+ "the consolidation allowlist ['ego', 'self'].",
53
+ );
54
+ }
55
+ const existingContent = getIdentityContent(db, params.targetLayer, params.targetKey);
56
+ const updatedContent =
57
+ existingContent !== null ? `${existingContent.trimEnd()}\n\n${params.content}` : params.content;
58
+ const row: IdentityRow = {
59
+ id: params.id,
60
+ layer: params.targetLayer,
61
+ key: params.targetKey,
62
+ content: updatedContent,
63
+ version: "1.0.0",
64
+ metadata: null,
65
+ };
66
+ upsertIdentity(db, row, params.nowIso);
67
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * `descriptor generate` — CV22.DS7.US11 plateau 5.
3
+ *
4
+ * Generates a routing descriptor per identity and upserts it. CR068 found this
5
+ * leaf unowned: DS7.US1 kept it on Python "as the DS7↔DS8 live seam", but DS8
6
+ * flips live mode and ports nothing, so its orchestration belongs here.
7
+ *
8
+ * Graded by `ts/test/goldens/descriptor.golden.json`. Two behaviours the
9
+ * corpus pins:
10
+ *
11
+ * - **No ledger row.** `_cmd_generate` calls `generate_descriptor` WITHOUT an
12
+ * `on_llm_call`, making this the only LLM role in the product that writes
13
+ * nothing to `llm_calls`. The corpus records zero rows so a port cannot
14
+ * "helpfully" start logging and diverge. Closing the gap is a DS8 input.
15
+ * - **The 80-character preview is CODE POINTS**, and the ellipsis appears
16
+ * only when the descriptor is strictly longer than 80.
17
+ *
18
+ * `generate_descriptor` itself returns `""` for empty content BEFORE calling
19
+ * the model, and `""` on any provider exception — both land on the same
20
+ * "skipped (empty response)" line, and the target still counts in the
21
+ * denominator.
22
+ */
23
+
24
+ import type { Database, WritableDatabase } from "#db/database.ts";
25
+ import { codePointLength, sliceCodePoints } from "#util/pythonText.ts";
26
+
27
+ export interface DescriptorTarget {
28
+ layer: string;
29
+ key: string;
30
+ content: string;
31
+ }
32
+
33
+ export interface IdentityRow {
34
+ key: string;
35
+ content: string | null;
36
+ }
37
+
38
+ /** Python `store.get_identity_by_layer(layer)` ordering. */
39
+ export function identitiesInLayer(db: Database, layer: string): IdentityRow[] {
40
+ return db
41
+ .prepare("SELECT key, content FROM identity WHERE layer = ? ORDER BY key")
42
+ .all(layer) as unknown as IdentityRow[];
43
+ }
44
+
45
+ export interface ResolveTargetsInput {
46
+ layer?: string | null;
47
+ key?: string | null;
48
+ }
49
+
50
+ export type ResolveTargetsResult =
51
+ | { kind: "targets"; targets: DescriptorTarget[] }
52
+ | { kind: "missing-identity"; layer: string; key: string };
53
+
54
+ /**
55
+ * Python's target selection: a specific pair, a whole layer, or — with no
56
+ * flags — every persona THEN every journey, in that order.
57
+ */
58
+ export function resolveDescriptorTargets(
59
+ db: Database,
60
+ input: ResolveTargetsInput,
61
+ ): ResolveTargetsResult {
62
+ if (input.layer && input.key) {
63
+ const row = db
64
+ .prepare("SELECT key, content FROM identity WHERE layer = ? AND key = ?")
65
+ .get(input.layer, input.key) as unknown as IdentityRow | undefined;
66
+ if (!row) return { kind: "missing-identity", layer: input.layer, key: input.key };
67
+ return {
68
+ kind: "targets",
69
+ targets: [{ layer: input.layer, key: input.key, content: row.content ?? "" }],
70
+ };
71
+ }
72
+ if (input.layer) {
73
+ return {
74
+ kind: "targets",
75
+ targets: identitiesInLayer(db, input.layer).map((row) => ({
76
+ layer: input.layer as string,
77
+ key: row.key,
78
+ content: row.content ?? "",
79
+ })),
80
+ };
81
+ }
82
+ const targets: DescriptorTarget[] = [];
83
+ for (const layer of ["persona", "journey"]) {
84
+ for (const row of identitiesInLayer(db, layer)) {
85
+ targets.push({ layer, key: row.key, content: row.content ?? "" });
86
+ }
87
+ }
88
+ return { kind: "targets", targets };
89
+ }
90
+
91
+ /** Python `upsert_descriptor`. */
92
+ export function upsertDescriptor(
93
+ db: WritableDatabase,
94
+ layer: string,
95
+ key: string,
96
+ descriptor: string,
97
+ generatedAt: string,
98
+ ): void {
99
+ db.prepare(
100
+ "INSERT INTO identity_descriptors (layer, key, descriptor, generated_at) VALUES (?, ?, ?, ?) " +
101
+ "ON CONFLICT(layer, key) DO UPDATE SET descriptor = excluded.descriptor, generated_at = excluded.generated_at",
102
+ ).run(layer, key, descriptor, generatedAt);
103
+ }
104
+
105
+ /** Python: `f"done — {descriptor[:80]}{'...' if len(descriptor) > 80 else ''}"`. */
106
+ export function descriptorPreview(descriptor: string): string {
107
+ const head = sliceCodePoints(descriptor, 80);
108
+ return codePointLength(descriptor) > 80 ? `${head}...` : head;
109
+ }
110
+
111
+ export interface DescriptorGenerateDeps {
112
+ /** Returns the descriptor text, already stripped; `""` means skip. */
113
+ generate: (target: DescriptorTarget) => string;
114
+ nowIso: string;
115
+ print?: (line: string) => void;
116
+ printError?: (line: string) => void;
117
+ }
118
+
119
+ export interface DescriptorGenerateResult {
120
+ exitCode: number;
121
+ generated: number;
122
+ total: number;
123
+ }
124
+
125
+ export function runDescriptorGenerate(
126
+ db: WritableDatabase,
127
+ input: ResolveTargetsInput,
128
+ deps: DescriptorGenerateDeps,
129
+ ): DescriptorGenerateResult {
130
+ const print = deps.print ?? ((line: string) => process.stdout.write(line));
131
+ const printError = deps.printError ?? ((line: string) => process.stderr.write(line));
132
+
133
+ const resolved = resolveDescriptorTargets(db, input);
134
+ if (resolved.kind === "missing-identity") {
135
+ // Python's `!r` renders Python string repr: single quotes.
136
+ printError(`No identity found for layer='${resolved.layer}' key='${resolved.key}'\n`);
137
+ return { exitCode: 1, generated: 0, total: 0 };
138
+ }
139
+
140
+ const { targets } = resolved;
141
+ if (targets.length === 0) {
142
+ print("No entities found to describe.\n");
143
+ return { exitCode: 0, generated: 0, total: 0 };
144
+ }
145
+
146
+ let generated = 0;
147
+ for (const target of targets) {
148
+ // Python prints with `end=" "`, so the line stays open until the outcome.
149
+ print(` generating ${target.layer}/${target.key}... `);
150
+ const descriptor = deps.generate(target);
151
+ if (descriptor) {
152
+ upsertDescriptor(db, target.layer, target.key, descriptor, deps.nowIso);
153
+ print(`done — ${descriptorPreview(descriptor)}\n`);
154
+ generated += 1;
155
+ } else {
156
+ print("skipped (empty response)\n");
157
+ }
158
+ }
159
+
160
+ print(`\n${generated}/${targets.length} descriptors generated.\n`);
161
+ return { exitCode: 0, generated, total: targets.length };
162
+ }
@@ -0,0 +1,143 @@
1
+ // `identity edit <layer> <key>` (CV22.DS7.TS4 plateau 6, Scope F).
2
+ //
3
+ // Port of `cmd_edit` in `src/memory/cli/identity_cmd.py`. The command is small
4
+ // and its risk is not: it hands a human's identity content to an arbitrary
5
+ // program and saves whatever comes back, so the three refusals matter more than
6
+ // the happy path.
7
+ //
8
+ // * **A failed editor saves nothing.** A non-zero exit is an abort, not an
9
+ // empty buffer -- someone who quits `vim` with `:cq` means "discard".
10
+ // * **Blank content is refused, never stored.** An identity layer emptied by
11
+ // accident is a silent loss the next Mirror Mode load would inherit.
12
+ // * **An unchanged buffer writes nothing at all**, so `updated_at` does not
13
+ // move and the content's history stays honest.
14
+ //
15
+ // The temp file carries the content of a person's identity, so it is created
16
+ // 0600 (Python gets this from `NamedTemporaryFile`) and removed on EVERY path,
17
+ // including the ones that refuse. Both are asserted by tests rather than
18
+ // assumed: they are the security-engineer's plan-stage requirement.
19
+
20
+ import { spawnSync } from "node:child_process";
21
+ import { closeSync, openSync, readFileSync, rmSync, writeSync } from "node:fs";
22
+ import { tmpdir } from "node:os";
23
+ import { join } from "node:path";
24
+ import type { WritableDatabase } from "#db/database.ts";
25
+ import { applyIdentitySet } from "#frontDoor/identityWrite.ts";
26
+
27
+ export interface IdentityEditResult {
28
+ stdout: string;
29
+ stderr: string;
30
+ exitCode: number;
31
+ }
32
+
33
+ export interface IdentityEditDeps {
34
+ /** The editor argv[0], resolved the way Python resolves it. */
35
+ editor?: string | undefined;
36
+ visual?: string | undefined;
37
+ nowIso: () => string;
38
+ /** Injected so a test can watch the buffer without driving a real editor. */
39
+ spawn?: typeof spawnSync;
40
+ /** Injected so a test can assert the temp file is gone afterwards. */
41
+ onTempFile?: (path: string) => void;
42
+ newId: () => string;
43
+ }
44
+
45
+ /** Python's `os.environ.get("EDITOR") or os.environ.get("VISUAL") or "nano"`. */
46
+ export function resolveEditor(deps: IdentityEditDeps): string {
47
+ return deps.editor || deps.visual || "nano";
48
+ }
49
+
50
+ /**
51
+ * Python's `NamedTemporaryFile(prefix=..., suffix=".md", delete=False)`: a
52
+ * fresh name in the system temp directory, created 0600 by the open itself.
53
+ *
54
+ * `wx` is what makes it safe: creating with exclusive intent means the command
55
+ * can never write identity content into a path that already exists, which is
56
+ * the whole difference between a temp file and a target of someone else's
57
+ * choosing.
58
+ */
59
+ function writePrivateTempFile(prefix: string, content: string): string {
60
+ for (let attempt = 0; attempt < 10; attempt += 1) {
61
+ const suffix = Math.random().toString(36).slice(2, 10);
62
+ const path = join(tmpdir(), `${prefix}${suffix}.md`);
63
+ try {
64
+ const handle = openSync(path, "wx", 0o600);
65
+ try {
66
+ writeSync(handle, content);
67
+ } finally {
68
+ closeSync(handle);
69
+ }
70
+ return path;
71
+ } catch (error) {
72
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
73
+ }
74
+ }
75
+ throw new Error("could not create a private temporary file for identity edit");
76
+ }
77
+
78
+ /** Port of `cmd_edit`. */
79
+ export function runIdentityEdit(
80
+ db: WritableDatabase,
81
+ layer: string,
82
+ key: string,
83
+ deps: IdentityEditDeps,
84
+ ): IdentityEditResult {
85
+ const existing = db
86
+ .prepare("SELECT content FROM identity WHERE layer = ? AND key = ?")
87
+ .get(layer, key) as { content: string } | undefined;
88
+ const currentContent = existing?.content ?? "";
89
+ const editor = resolveEditor(deps);
90
+
91
+ const tempPath = writePrivateTempFile(`mirror-identity-${layer}-${key}-`, currentContent);
92
+ deps.onTempFile?.(tempPath);
93
+
94
+ let newContent: string;
95
+ try {
96
+ const spawn = deps.spawn ?? spawnSync;
97
+ const result = spawn(editor, [tempPath], { stdio: "inherit", shell: false });
98
+ if (result.error) {
99
+ // Python raises FileNotFoundError here and the traceback escapes. Same
100
+ // exit code, one line, on the same stream: the recorded divergence class.
101
+ return {
102
+ stdout: "",
103
+ stderr: `Editor '${editor}' could not be started.\n`,
104
+ exitCode: 1,
105
+ };
106
+ }
107
+ if (result.status !== 0) {
108
+ return {
109
+ stdout: "",
110
+ stderr: `Editor exited with code ${result.status}. Aborted.\n`,
111
+ exitCode: 1,
112
+ };
113
+ }
114
+ newContent = readFileSync(tempPath, "utf8");
115
+ } finally {
116
+ // Every path, including the refusals above: the buffer holds identity
117
+ // content and must not outlive the command.
118
+ rmSync(tempPath, { force: true });
119
+ }
120
+
121
+ if (!newContent.trim()) {
122
+ return {
123
+ stdout: "",
124
+ stderr: "Content is empty after editing. No changes saved.\n",
125
+ exitCode: 1,
126
+ };
127
+ }
128
+ if (newContent === currentContent) {
129
+ return { stdout: "No changes detected.\n", stderr: "", exitCode: 0 };
130
+ }
131
+
132
+ // The same write `identity set` routes through, including its created/updated
133
+ // verb: two commands that disagree about whether a key existed would be two
134
+ // truths about one row.
135
+ const outcome = applyIdentitySet(db, {
136
+ layer,
137
+ key,
138
+ content: newContent,
139
+ id: deps.newId(),
140
+ nowIso: deps.nowIso(),
141
+ });
142
+ return { stdout: `✓ ${layer}/${key} ${outcome.action}\n`, stderr: "", exitCode: 0 };
143
+ }
@@ -0,0 +1,77 @@
1
+ // CR104 — the identity keys agents put into commands.
2
+ //
3
+ // A journey slug and a persona id are identity keys, and they do not stay in the
4
+ // database: `--journey <slug>` goes into every Builder command, and the id that
5
+ // `detect-persona` prints goes into `mirror load --persona <id>`. Agents write
6
+ // those commands. A key that holds shell syntax is therefore two steps from a
7
+ // command on the user's machine: a crafted key, then a command that carries it.
8
+ // Persona ids can arrive from a stranger, in a persona bundle made to be shared
9
+ // and seeded.
10
+ //
11
+ // So a NEW key in these layers must be a kebab-case slug (`isKebabSlug`), checked
12
+ // where every creation passes: `upsertIdentity`'s INSERT branch. `identity set`
13
+ // and `identity edit` ask the same question earlier, before the write seam takes
14
+ // its snapshot and before an editor opens. A key that already exists predates
15
+ // the rule and stays writable; `runtime diagnose` reports it instead
16
+ // (`identityKeyBreaksGrammar`).
17
+ //
18
+ // Nothing here normalizes a key. A key stored differently from how it was typed
19
+ // would miss the next command that uses the typed one, so the answer is a refusal
20
+ // that names a key that would work.
21
+
22
+ import { pyRepr } from "#util/pythonText.ts";
23
+ import { isKebabSlug, kebabSlug } from "#util/slug.ts";
24
+
25
+ /** What each guarded layer calls its key, in the refusal. */
26
+ const KEY_NAMES: ReadonlyMap<string, string> = new Map([
27
+ ["journey", "journey slug"],
28
+ ["persona", "persona id"],
29
+ ]);
30
+
31
+ /** The layers whose keys agents put into commands, and so must be kebab-case slugs. */
32
+ export const SLUG_KEYED_LAYERS: readonly string[] = [...KEY_NAMES.keys()];
33
+
34
+ /** Whether `key` breaks the grammar its layer requires. Always false for other layers. */
35
+ export function identityKeyBreaksGrammar(layer: string, key: string): boolean {
36
+ return KEY_NAMES.has(layer) && !isKebabSlug(key);
37
+ }
38
+
39
+ /**
40
+ * Why a NEW row in `layer` cannot take `key`, or null when it can. The sentence
41
+ * leads with the consequence, because the reader is often an agent deciding its
42
+ * next move, then gives the rule and a key that would work. The key is printed
43
+ * escaped, so a newline or a terminal escape inside it cannot forge output.
44
+ */
45
+ export function newIdentityKeyProblem(layer: string, key: string): string | null {
46
+ const keyName = KEY_NAMES.get(layer);
47
+ if (keyName === undefined || isKebabSlug(key)) return null;
48
+ const suggestion = kebabSlug(key);
49
+ const example = suggestion ? `, for example ${pyRepr(suggestion)}` : "";
50
+ return (
51
+ `no ${layer} was created: ${pyRepr(key)} is not a ${keyName}. ` +
52
+ `Use lowercase letters, digits, and single hyphens, up to 80 characters${example}.`
53
+ );
54
+ }
55
+
56
+ /**
57
+ * Thrown when a new identity row's key breaks its layer's grammar. Nothing was written.
58
+ * A route that did not ask first lets it through, and the front door answers it as
59
+ * one `Error:` line (`#frontDoor/namedRefusal.ts`).
60
+ */
61
+ export class InvalidIdentityKeyError extends Error {
62
+ readonly layer: string;
63
+ readonly key: string;
64
+
65
+ constructor(layer: string, key: string, problem: string) {
66
+ super(problem);
67
+ this.name = "InvalidIdentityKeyError";
68
+ this.layer = layer;
69
+ this.key = key;
70
+ }
71
+ }
72
+
73
+ /** Throw `InvalidIdentityKeyError` when a new row in `layer` cannot take `key`. */
74
+ export function assertNewIdentityKey(layer: string, key: string): void {
75
+ const problem = newIdentityKeyProblem(layer, key);
76
+ if (problem !== null) throw new InvalidIdentityKeyError(layer, key, problem);
77
+ }