titan-agent 6.1.0-beta.2 → 6.1.0-beta.21

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 (235) hide show
  1. package/LICENSE +1 -29
  2. package/README.md +1 -1
  3. package/dist/agent/agentLessons.js +2 -2
  4. package/dist/agent/agentLessons.js.map +1 -1
  5. package/dist/agent/autoModeClassifier.js +95 -0
  6. package/dist/agent/autoModeClassifier.js.map +1 -0
  7. package/dist/agent/durableJournal.js +2 -2
  8. package/dist/agent/durableJournal.js.map +1 -1
  9. package/dist/agent/goalDriver.js +67 -0
  10. package/dist/agent/goalDriver.js.map +1 -1
  11. package/dist/agent/goalPlanFile.js +3 -3
  12. package/dist/agent/goalPlanFile.js.map +1 -1
  13. package/dist/agent/missionLifecycle.js +2 -2
  14. package/dist/agent/missionLifecycle.js.map +1 -1
  15. package/dist/agent/subAgent.js +41 -0
  16. package/dist/agent/subAgent.js.map +1 -1
  17. package/dist/agent/toolContract.js +49 -0
  18. package/dist/agent/toolContract.js.map +1 -0
  19. package/dist/agent/toolRunner.js +72 -11
  20. package/dist/agent/toolRunner.js.map +1 -1
  21. package/dist/agent/trajectoryRecorder.js +185 -0
  22. package/dist/agent/trajectoryRecorder.js.map +1 -0
  23. package/dist/agent/verification.js +164 -0
  24. package/dist/agent/verification.js.map +1 -0
  25. package/dist/agent/worktreeExecutor.js +207 -0
  26. package/dist/agent/worktreeExecutor.js.map +1 -0
  27. package/dist/config/schema.js +28 -1
  28. package/dist/config/schema.js.map +1 -1
  29. package/dist/gateway/server.js +54 -0
  30. package/dist/gateway/server.js.map +1 -1
  31. package/dist/memory/facade.js +137 -0
  32. package/dist/memory/facade.js.map +1 -0
  33. package/dist/migrations/003-soma-profile-default.js +2 -2
  34. package/dist/migrations/003-soma-profile-default.js.map +1 -1
  35. package/dist/migrations/005-route-redirects.js +3 -1
  36. package/dist/migrations/005-route-redirects.js.map +1 -1
  37. package/dist/migrations/runner.js +2 -2
  38. package/dist/migrations/runner.js.map +1 -1
  39. package/dist/providers/defaultModel.js +14 -0
  40. package/dist/providers/defaultModel.js.map +1 -1
  41. package/dist/providers/router.js +4 -0
  42. package/dist/providers/router.js.map +1 -1
  43. package/dist/providers/stub.js +185 -0
  44. package/dist/providers/stub.js.map +1 -0
  45. package/dist/skills/builtin/backup.js +1 -2
  46. package/dist/skills/builtin/backup.js.map +1 -1
  47. package/dist/skills/builtin/contracts.js +254 -0
  48. package/dist/skills/builtin/contracts.js.map +1 -0
  49. package/dist/skills/builtin/download_image.js +9 -0
  50. package/dist/skills/builtin/download_image.js.map +1 -1
  51. package/dist/skills/builtin/filesystem.js +70 -20
  52. package/dist/skills/builtin/filesystem.js.map +1 -1
  53. package/dist/skills/registry.js +7 -0
  54. package/dist/skills/registry.js.map +1 -1
  55. package/dist/storage/patterns.js +2 -2
  56. package/dist/storage/patterns.js.map +1 -1
  57. package/dist/storage/personalGallery.js +2 -2
  58. package/dist/storage/personalGallery.js.map +1 -1
  59. package/dist/storage/somaProfile.js +2 -2
  60. package/dist/storage/somaProfile.js.map +1 -1
  61. package/dist/utils/constants.js +1 -1
  62. package/dist/utils/constants.js.map +1 -1
  63. package/package.json +2 -2
  64. package/ui/dist/assets/{AuditPanel-DrDvXZ9u.js → AuditPanel-RNrvKmfX.js} +1 -1
  65. package/ui/dist/assets/{AutonomyPanel-BfhW-PRX.js → AutonomyPanel-D0nmt79R.js} +2 -2
  66. package/ui/dist/assets/{AutopilotPanel-C3yQBxAp.js → AutopilotPanel-DlcdfRRy.js} +1 -1
  67. package/ui/dist/assets/{AutoresearchPanel-BgpR3EcK.js → AutoresearchPanel-CXeO6kGK.js} +2 -2
  68. package/ui/dist/assets/BackupPanel-CKb-cDsk.js +1 -0
  69. package/ui/dist/assets/{BrowserPanel-BNaKZOnn.js → BrowserPanel-CDaoF6n5.js} +1 -1
  70. package/ui/dist/assets/CPActivity-BZbeztg1.js +1 -0
  71. package/ui/dist/assets/CPAgentDetail-C9fvLxKi.js +1 -0
  72. package/ui/dist/assets/CPAgents-BsBA9kb2.js +1 -0
  73. package/ui/dist/assets/CPApprovals-Di5itVIK.js +1 -0
  74. package/ui/dist/assets/CPCosts-DmgckHli.js +1 -0
  75. package/ui/dist/assets/CPDashboard-CmSb7woK.js +1 -0
  76. package/ui/dist/assets/{CPFiles-CUpP5I82.js → CPFiles-Dd0NGcpX.js} +1 -1
  77. package/ui/dist/assets/CPGoals-DeILFq0A.js +1 -0
  78. package/ui/dist/assets/{CPInbox-DUW_hntB.js → CPInbox-DAtidZez.js} +2 -2
  79. package/ui/dist/assets/CPIssueDetail-BvwoGI57.js +1 -0
  80. package/ui/dist/assets/CPIssues-CZ1hIKDP.js +1 -0
  81. package/ui/dist/assets/{CPLayout-J0fa-it4.js → CPLayout-luPwhNpA.js} +3 -3
  82. package/ui/dist/assets/CPOrg-BlYp_KP6.js +8 -0
  83. package/ui/dist/assets/CPRuns-D0HMCjIo.js +1 -0
  84. package/ui/dist/assets/{CPSocial-By_FStZ4.js → CPSocial-CZaGi14s.js} +2 -2
  85. package/ui/dist/assets/ChannelsPanel-khiBHwx3.js +1 -0
  86. package/ui/dist/assets/{CheckpointsPanel-DvKUd2VF.js → CheckpointsPanel-n2y3jzzT.js} +1 -1
  87. package/ui/dist/assets/{CommandPostHub-CQqRYBjf.js → CommandPostHub-iWf-zt5p.js} +4 -4
  88. package/ui/dist/assets/{CronPanel-Brmnnu8_.js → CronPanel-BkRJl8u6.js} +1 -1
  89. package/ui/dist/assets/{DataTable-seaktPzB.js → DataTable-DZ2cG3Cv.js} +1 -1
  90. package/ui/dist/assets/{DreamPanel-CO7iSdjn.js → DreamPanel-DF6l0WB-.js} +1 -1
  91. package/ui/dist/assets/{EmptyState-CmK9fywh.js → EmptyState-D7xwm8hA.js} +1 -1
  92. package/ui/dist/assets/{EvalHarnessPanel-2RVBKbym.js → EvalHarnessPanel-BHabu49z.js} +2 -2
  93. package/ui/dist/assets/{EvalPanel-CqAwhFQ5.js → EvalPanel-yAesMLSP.js} +1 -1
  94. package/ui/dist/assets/{FileViewer-D98m-vF5.js → FileViewer-DOWUQccc.js} +52 -14
  95. package/ui/dist/assets/{FilesPanel-dRQM0apb.js → FilesPanel-BjuDhqaX.js} +2 -2
  96. package/ui/dist/assets/FleetPanel-B0ZhS2XX.js +1 -0
  97. package/ui/dist/assets/{HomelabPanel-DjMUrO2B.js → HomelabPanel-D373ddvj.js} +1 -1
  98. package/ui/dist/assets/InfraView-CI99NZbI.js +2 -0
  99. package/ui/dist/assets/InlineEditableField-fDk3eGVo.js +1 -0
  100. package/ui/dist/assets/Input-j10hONMY.js +1 -0
  101. package/ui/dist/assets/IntegrationsPanel-kPa5Qq-f.js +1 -0
  102. package/ui/dist/assets/IntelligenceView-DeKBaAlY.js +2 -0
  103. package/ui/dist/assets/LearningPanel-B8tqHeEZ.js +1 -0
  104. package/ui/dist/assets/{LogsPanel-Bs9IsTCH.js → LogsPanel-sXc__trb.js} +1 -1
  105. package/ui/dist/assets/McpPanel-DEr8pZwB.js +1 -0
  106. package/ui/dist/assets/{MemoryGraphPanel-F1P9COyY.js → MemoryGraphPanel-DZl7USJ6.js} +1 -1
  107. package/ui/dist/assets/MemoryWikiPanel-DTNVWvVT.js +11 -0
  108. package/ui/dist/assets/{MeshPanel-CgIAh5ZX.js → MeshPanel-b77zyQx4.js} +1 -1
  109. package/ui/dist/assets/MissionCanvas-D_quSl5F.js +69 -0
  110. package/ui/dist/assets/MissionChat-rQ5rUPAt.js +7 -0
  111. package/ui/dist/assets/MissionLibrary-Cxw7sLxv.js +5 -0
  112. package/ui/dist/assets/MissionStart-ChMWO1rr.js +1 -0
  113. package/ui/dist/assets/Modal-Dl5b6Yaj.js +1 -0
  114. package/ui/dist/assets/NvidiaPanel-D7qFbbeJ.js +1 -0
  115. package/ui/dist/assets/{OrganismPanel-CNZM3eof.js → OrganismPanel-EiQ9Gfqx.js} +1 -1
  116. package/ui/dist/assets/OverviewPanel-Bectj1Z5.js +1 -0
  117. package/ui/dist/assets/{PageHeader-llOXO43Z.js → PageHeader-CiuoyvDR.js} +1 -1
  118. package/ui/dist/assets/{PersonaProfilesPanel-CfsQTMra.js → PersonaProfilesPanel-BO87DSVG.js} +1 -1
  119. package/ui/dist/assets/{PersonasPanel-DIac_kZ3.js → PersonasPanel-BNogLwM0.js} +1 -1
  120. package/ui/dist/assets/RecipesPanel-DyqfWlaY.js +1 -0
  121. package/ui/dist/assets/{SecurityPanel-_gqMk8hJ.js → SecurityPanel-OMC-Tvbz.js} +1 -1
  122. package/ui/dist/assets/SelfImprovePanel-DXIfG_df.js +1 -0
  123. package/ui/dist/assets/{SelfProposalsPanel-DuA0zsGH.js → SelfProposalsPanel-CmgEz1TQ.js} +2 -2
  124. package/ui/dist/assets/SessionsPanel-gJXosEGf.js +1 -0
  125. package/ui/dist/assets/{SessionsTab-bFYaFvTZ.js → SessionsTab-BOVzzjTO.js} +1 -1
  126. package/ui/dist/assets/SettingsPanel-DifFGEvi.js +1 -0
  127. package/ui/dist/assets/SettingsView-BNpj0iym.js +2 -0
  128. package/ui/dist/assets/SkeletonLoader-CNSsfH3f.js +1 -0
  129. package/ui/dist/assets/{SkillsPanel-D08WcRGp.js → SkillsPanel-6fbRov-m.js} +1 -1
  130. package/ui/dist/assets/{SomaView-CYukE7Fp.js → SomaView-D3boPl5D.js} +1 -1
  131. package/ui/dist/assets/{StatCard-DT4Bx1mY.js → StatCard-2BmOlamJ.js} +1 -1
  132. package/ui/dist/assets/StatusBadge-CdVHF1nG.js +1 -0
  133. package/ui/dist/assets/Tabs-DW9ytf9H.js +1 -0
  134. package/ui/dist/assets/{TeamsPanel-DOBe72TK.js → TeamsPanel-CNaQ8Dbw.js} +1 -1
  135. package/ui/dist/assets/TelemetryPanel-DL81QlAW.js +1 -0
  136. package/ui/dist/assets/TitanCanvas-DLIN_wV3.js +1152 -0
  137. package/ui/dist/assets/ToolsView-BlChEE4n.js +2 -0
  138. package/ui/dist/assets/Tooltip-oR5JnwoN.js +6 -0
  139. package/ui/dist/assets/{TraceViewer-0bCz0x_a.js → TraceViewer-DtJXjCXV.js} +1 -1
  140. package/ui/dist/assets/TrainingPanel-4XMGaRN_.js +1 -0
  141. package/ui/dist/assets/{VoiceOverlay-B08Sw918.js → VoiceOverlay-B3fXJ-EC.js} +1 -1
  142. package/ui/dist/assets/{VramPanel-CJiic8y4.js → VramPanel-HisAYiqI.js} +1 -1
  143. package/ui/dist/assets/{WatchView-CaQMDEAy.js → WatchView-C92y0hU7.js} +1 -1
  144. package/ui/dist/assets/{WorkTab-D5bi2WRi.js → WorkTab-BYAWcvA1.js} +1 -1
  145. package/ui/dist/assets/{WorkflowsPanel-DdY6E93C.js → WorkflowsPanel-D2DiOBLA.js} +1 -1
  146. package/ui/dist/assets/{arrow-left-CHJyrO8S.js → arrow-left-thXQMJQ6.js} +1 -1
  147. package/ui/dist/assets/{briefcase-ROIEmiah.js → briefcase-ByMhDqlL.js} +1 -1
  148. package/ui/dist/assets/{chart-column-wFkjV3Ni.js → chart-column-CYHJVuxN.js} +1 -1
  149. package/ui/dist/assets/{check-BcOm5jPO.js → check-ChWLFWCx.js} +1 -1
  150. package/ui/dist/assets/{chevron-down-CM11GPpR.js → chevron-down-CU_O_5h3.js} +1 -1
  151. package/ui/dist/assets/{chevron-right-aFDDgFsL.js → chevron-right-DAiXsK24.js} +1 -1
  152. package/ui/dist/assets/{chevron-up-D6g7FB6i.js → chevron-up-BudRYwvy.js} +1 -1
  153. package/ui/dist/assets/{circle-check-big-DPz64kZW.js → circle-check-big-gkA-V2JW.js} +1 -1
  154. package/ui/dist/assets/{clock-BbCCaukN.js → clock-qO5kIED9.js} +1 -1
  155. package/ui/dist/assets/clsx-B-dksMZM.js +1 -0
  156. package/ui/dist/assets/{dollar-sign-Dy5-qkg3.js → dollar-sign-1gQNJK3V.js} +1 -1
  157. package/ui/dist/assets/{download-DjheLEns.js → download-DMD1m87c.js} +1 -1
  158. package/ui/dist/assets/{external-link-BgxGF7ZQ.js → external-link-ByafQkZ7.js} +1 -1
  159. package/ui/dist/assets/{folder-DY4eeMdo.js → folder-DeG4RuOD.js} +1 -1
  160. package/ui/dist/assets/{funnel-LWmcwhj-.js → funnel-wE8i5V9Y.js} +1 -1
  161. package/ui/dist/assets/{git-branch-CPZciK10.js → git-branch-BWSuK1LU.js} +1 -1
  162. package/ui/dist/assets/index-B8YEFBRH.js +277 -0
  163. package/ui/dist/assets/index-Cb5bSxRB.css +1 -0
  164. package/ui/dist/assets/{layers-Bn7bgu2Z.js → layers-tN-wIpE0.js} +1 -1
  165. package/ui/dist/assets/{legacy-BVPeht6t.js → legacy-BSZBdyop.js} +2 -2
  166. package/ui/dist/assets/{lightbulb-tzuCyEQk.js → lightbulb-TUHKCda3.js} +1 -1
  167. package/ui/dist/assets/{list-todo-say4NbSw.js → list-todo-DtxcUGz2.js} +1 -1
  168. package/ui/dist/assets/{missions-DbC8UmMc.js → missions-C3EC4uKT.js} +1 -1
  169. package/ui/dist/assets/{network-v9O7MmZT.js → network-ephW7nah.js} +1 -1
  170. package/ui/dist/assets/{pause-CZo33QlA.js → pause-DeggUVZc.js} +1 -1
  171. package/ui/dist/assets/{play-DbCJhc5H.js → play-Dfc0ia6v.js} +1 -1
  172. package/ui/dist/assets/{plug-CtpCZg2z.js → plug-Bl_6i_9u.js} +1 -1
  173. package/ui/dist/assets/{plus-O61rB_YF.js → plus-CQwCh7WY.js} +1 -1
  174. package/ui/dist/assets/{proxy-BAUTNyg6.js → proxy-BjQ4lsww.js} +1 -1
  175. package/ui/dist/assets/{rotate-ccw-n5WkWR51.js → rotate-ccw-CUuN2ebi.js} +1 -1
  176. package/ui/dist/assets/{save-D2Qh8KyN.js → save-PlTUvsN5.js} +1 -1
  177. package/ui/dist/assets/{search-C9SdcZJq.js → search-BHgLDT-j.js} +1 -1
  178. package/ui/dist/assets/{send-DZ_5M5QQ.js → send-pflQNBoT.js} +1 -1
  179. package/ui/dist/assets/{shield-check-DyP5PmaY.js → shield-check-CrD-s89i.js} +1 -1
  180. package/ui/dist/assets/{target-CWfn705s.js → target-D4oh8GME.js} +1 -1
  181. package/ui/dist/assets/{terminal-CdhfA0Kr.js → terminal-BvJZ1R3x.js} +1 -1
  182. package/ui/dist/assets/{toggle-right-CmYQLQRS.js → toggle-right-Dl7x_ed7.js} +1 -1
  183. package/ui/dist/assets/{trash-2-TQcEJEv4.js → trash-2-lPcFwnW5.js} +1 -1
  184. package/ui/dist/assets/{trending-up-Cw22bM4J.js → trending-up-DqUbZfDe.js} +1 -1
  185. package/ui/dist/assets/{trophy-DUF8Bcas.js → trophy-Dv6zFfKM.js} +1 -1
  186. package/ui/dist/assets/{users-BX_ZaByZ.js → users-NPyAWbs5.js} +1 -1
  187. package/ui/dist/assets/{wrench-Cfp7nYhG.js → wrench-BCKgeHj3.js} +1 -1
  188. package/ui/dist/index.html +2 -2
  189. package/ui/dist/sw.js +1 -1
  190. package/ui/dist/assets/BackupPanel-BNxMlNIP.js +0 -1
  191. package/ui/dist/assets/CPActivity-CjIgehOq.js +0 -1
  192. package/ui/dist/assets/CPAgentDetail-oaymYRNI.js +0 -1
  193. package/ui/dist/assets/CPAgents-CWQ6iJfO.js +0 -1
  194. package/ui/dist/assets/CPApprovals-DzATBhtM.js +0 -1
  195. package/ui/dist/assets/CPCosts-xVQR148_.js +0 -1
  196. package/ui/dist/assets/CPDashboard-Dt6Jr7Zw.js +0 -1
  197. package/ui/dist/assets/CPGoals-Bt1a3C9T.js +0 -1
  198. package/ui/dist/assets/CPIssueDetail-DHxqtiuh.js +0 -1
  199. package/ui/dist/assets/CPIssues-D0l3fgUs.js +0 -1
  200. package/ui/dist/assets/CPOrg-DQ3tbYld.js +0 -8
  201. package/ui/dist/assets/CPRuns-0vncicUW.js +0 -1
  202. package/ui/dist/assets/ChannelsPanel-CiJs4gyw.js +0 -1
  203. package/ui/dist/assets/FleetPanel-Bg-f3_ip.js +0 -1
  204. package/ui/dist/assets/InfraView-CHYaZqMK.js +0 -2
  205. package/ui/dist/assets/InlineEditableField-BOLRKQq1.js +0 -1
  206. package/ui/dist/assets/Input-4pNffAw-.js +0 -1
  207. package/ui/dist/assets/IntegrationsPanel-CFucvr-Q.js +0 -1
  208. package/ui/dist/assets/IntelligenceView-Dh-Wuf13.js +0 -2
  209. package/ui/dist/assets/LearningPanel-CtlougkF.js +0 -1
  210. package/ui/dist/assets/McpPanel-BobqpLsh.js +0 -1
  211. package/ui/dist/assets/MemoryWikiPanel-BfYksjoI.js +0 -11
  212. package/ui/dist/assets/MissionCanvas-0V_vtqiu.js +0 -50
  213. package/ui/dist/assets/MissionChat-9hGhc9Zw.js +0 -7
  214. package/ui/dist/assets/MissionLibrary-dFkT8pGp.js +0 -5
  215. package/ui/dist/assets/MissionStart-CZSEie0Q.js +0 -1
  216. package/ui/dist/assets/Modal-V2yNnkKk.js +0 -1
  217. package/ui/dist/assets/NvidiaPanel-qLJysI9M.js +0 -1
  218. package/ui/dist/assets/OverviewPanel-Be6LukJ_.js +0 -1
  219. package/ui/dist/assets/RecipesPanel-DDYOCx3w.js +0 -1
  220. package/ui/dist/assets/SelfImprovePanel-B8o_fO30.js +0 -1
  221. package/ui/dist/assets/SessionsPanel-DeCBkSgY.js +0 -1
  222. package/ui/dist/assets/SettingsPanel-BlsRDAt7.js +0 -1
  223. package/ui/dist/assets/SettingsView-Dsa7Zr4e.js +0 -2
  224. package/ui/dist/assets/SkeletonLoader-BkQoZllJ.js +0 -1
  225. package/ui/dist/assets/StatusBadge-BFEHCdgi.js +0 -1
  226. package/ui/dist/assets/Tabs-g2Gs9sK0.js +0 -1
  227. package/ui/dist/assets/TelemetryPanel-DXAGuV0R.js +0 -1
  228. package/ui/dist/assets/TitanCanvas-DCl3gxSl.js +0 -1072
  229. package/ui/dist/assets/ToolsView-CnUjq0Js.js +0 -2
  230. package/ui/dist/assets/Tooltip-DJ99YlFF.js +0 -6
  231. package/ui/dist/assets/TrainingPanel-BLf_KMaE.js +0 -1
  232. package/ui/dist/assets/eye-off-CYR4nH5I.js +0 -6
  233. package/ui/dist/assets/index-PwwKjgT3.css +0 -1
  234. package/ui/dist/assets/index-slWylU2o.js +0 -244
  235. package/ui/dist/assets/loader-circle-E8aLxmMz.js +0 -6
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ openSession,
4
+ recordDecision,
5
+ recordOpenQuestion,
6
+ resolveOpenQuestion,
7
+ recordArtifact,
8
+ addNote,
9
+ updateStatus,
10
+ closeSession,
11
+ retireStaleSessions
12
+ } from "./workingMemory.js";
13
+ import {
14
+ recordEpisode,
15
+ recallSimilarEpisodes,
16
+ recallRecent,
17
+ renderRecallBlock
18
+ } from "./episodic.js";
19
+ import {
20
+ isHindsightConnected,
21
+ retainToHindsight,
22
+ recallFromHindsight,
23
+ retainStrategy,
24
+ getHindsightHints
25
+ } from "./hindsightBridge.js";
26
+ import {
27
+ rememberFact,
28
+ recallFact,
29
+ searchMemories
30
+ } from "./memory.js";
31
+ import {
32
+ loadProfile,
33
+ saveProfile,
34
+ learnName,
35
+ rememberProject,
36
+ rememberContact,
37
+ learnPreference,
38
+ learnFact as learnUserFact,
39
+ addGoal,
40
+ calibrateTechnicalLevel
41
+ } from "./relationship.js";
42
+ import {
43
+ recordToolResult,
44
+ recordSuccessPattern,
45
+ recordUserCorrection
46
+ } from "./learning.js";
47
+ const working = {
48
+ /** Open a new working-memory session. */
49
+ openSession,
50
+ /** Record a decision the agent made during this session. */
51
+ recordDecision,
52
+ /** Note a question the agent needs answered to proceed. */
53
+ recordOpenQuestion,
54
+ /** Mark an open question as answered. */
55
+ resolveOpenQuestion,
56
+ /** Note an artifact (file, URL, fact id) the agent produced. */
57
+ recordArtifact,
58
+ /** Free-form breadcrumb. */
59
+ addNote,
60
+ /** Update the session's status. */
61
+ updateStatus,
62
+ /** Close the session. Returns the final record. */
63
+ closeSession,
64
+ /** Sweep + archive sessions idle longer than the configured TTL. */
65
+ retireStale: retireStaleSessions
66
+ };
67
+ const episodic = {
68
+ /** Record a new episode (past interaction, decision, observation). */
69
+ record: recordEpisode,
70
+ /** Recall episodes most similar to a query (vector search). */
71
+ recallSimilar: recallSimilarEpisodes,
72
+ /** Recall the N most recent episodes. */
73
+ recallRecent,
74
+ /**
75
+ * Render a markdown block of recalled episodes suitable for
76
+ * injection into a system prompt. Wraps recallRecent +
77
+ * recallSimilar with the right formatting.
78
+ */
79
+ renderRecallBlock,
80
+ /** Cross-session retention via the Hindsight bridge. */
81
+ retain: retainToHindsight,
82
+ /** Cross-session recall. Returns null if not connected. */
83
+ recallCrossSession: recallFromHindsight,
84
+ /** Retain a STRATEGY (not just a memory) cross-session. */
85
+ retainStrategy,
86
+ /** Get cross-session hints relevant to a current message. */
87
+ getCrossSessionHints: getHindsightHints,
88
+ /** True if the Hindsight bridge has a working credential. */
89
+ isCrossSessionConnected: isHindsightConnected
90
+ };
91
+ const semantic = {
92
+ // ── Generic key-value facts ──
93
+ /** Store a category/key fact. */
94
+ rememberFact,
95
+ /** Retrieve a category/key fact. */
96
+ recallFact,
97
+ /** Fuzzy search across all stored facts. */
98
+ search: searchMemories,
99
+ // ── User profile / relationship ──
100
+ /** Load the persistent user profile (defaults to 'default' user). */
101
+ getUserProfile: loadProfile,
102
+ /** Save changes back to the user profile. */
103
+ saveUserProfile: saveProfile,
104
+ /** Learn the user's name. */
105
+ learnUserName: learnName,
106
+ /** Remember a project the user is working on. */
107
+ learnUserProject: rememberProject,
108
+ /** Remember a contact the user mentioned. */
109
+ learnUserContact: rememberContact,
110
+ /** Learn a user preference. */
111
+ learnUserPreference: learnPreference,
112
+ /** Learn a fact the user told us. */
113
+ learnUserFact,
114
+ /** Add a goal the user wants help with. */
115
+ addUserGoal: addGoal,
116
+ /** Calibrate technical level (beginner / intermediate / expert). */
117
+ calibrateUserTechnicalLevel: calibrateTechnicalLevel,
118
+ // ── Learned tool patterns ──
119
+ /** Record a tool's outcome for the success-rate learner. */
120
+ recordToolResult,
121
+ /** Promote a tool sequence as a "good pattern" to prefer. */
122
+ recordSuccessPattern,
123
+ /** Record a user correction so the agent learns not to repeat it. */
124
+ recordUserCorrection
125
+ };
126
+ const memory = {
127
+ working,
128
+ episodic,
129
+ semantic
130
+ };
131
+ export {
132
+ episodic,
133
+ memory,
134
+ semantic,
135
+ working
136
+ };
137
+ //# sourceMappingURL=facade.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/memory/facade.ts"],"sourcesContent":["/**\n * TITAN — Memory Façade (v6.1.0-beta.14, Phase D.2)\n *\n * A clean three-layer API on top of TITAN's existing memory stores.\n * Hermes Agent and most reliable agent frameworks expose memory as\n * working / episodic / semantic — this façade gives TITAN the same\n * legibility without re-implementing any of the underlying stores.\n *\n * THREE LAYERS:\n *\n * memory.working — In-flight, session-scoped state.\n * Open questions, decisions, artifacts produced\n * during the current spawn. Cleared on session\n * close. Lives in workingMemory.ts.\n *\n * memory.episodic — Past interactions: \"what happened when\".\n * Recallable by time-window or vector similarity.\n * Backed by episodic.ts + Hindsight cross-session\n * bridge. Persisted to disk.\n *\n * memory.semantic — Facts, learned patterns, user model.\n * \"Tony's daughter is named Y\", \"the project uses\n * TypeScript\", \"this tool fails 30 % of the time\".\n * Backed by memory.rememberFact + relationship.ts\n * + learning.ts. Persisted.\n *\n * WHY this façade exists:\n *\n * - The Picrew/awesome-agent-harness catalog identifies three-layer\n * memory as the canonical shape. Hermes specifically calls it out\n * as their headline differentiator. TITAN's underlying stores are\n * more featureful than Hermes's (graph + briefings + Hindsight)\n * but LESS LEGIBLE — a new contributor reading the code has to\n * chase six imports to find \"where does TITAN remember things\".\n *\n * - This façade is the public surface new code uses. Internal modules\n * still call the underlying functions directly — we don't break\n * anything. New skills, new providers, new specialist prompts all\n * get `import { memory } from '../memory/facade.js'` and a clean\n * three-method API.\n *\n * - Documentation now has a single, stable export to point at. The\n * README + skill-author guide reference `memory.semantic.recallFact`\n * instead of \"wait, is that memory.ts or memory/relationship.ts?\".\n *\n * TRADE-OFFS we deliberately accept:\n *\n * - The façade is a thin wrapper, not a re-architecture. The methods\n * pass straight through to the underlying functions. If you need\n * a feature that doesn't fit one of the three layers cleanly\n * (e.g. the memory graph's edge traversal), import that module\n * directly. The façade covers the 90 % of read/write flows.\n *\n * - Some methods are duplicated semantically — e.g. semantic.search\n * and episodic.recallSimilar both do vector recall. The naming\n * follows the layer's metaphor: a SEMANTIC search returns FACTS,\n * an EPISODIC recall returns past EVENTS. They share infrastructure\n * but the intent is different.\n *\n * - Async/sync mix is preserved from the underlying functions.\n * `recallSimilar` is async (vector store); `rememberFact` is sync\n * (key-value SQLite). The façade doesn't normalize because that\n * would force unnecessary `await`s on cheap paths.\n *\n * FOLLOW-UP:\n *\n * - Skill authors guide updates to reference `memory.*` exclusively.\n * - System prompt includes can call `memory.episodic.renderRecallBlock`\n * directly instead of importing episodic.ts.\n * - `memory.consolidate()` shortcut wired to dreaming.runConsolidation.\n * - `memory.graph` namespace if anyone needs structured edge queries.\n */\n\n// ── Working layer — workingMemory.ts ──\nimport {\n openSession,\n recordDecision,\n recordOpenQuestion,\n resolveOpenQuestion,\n recordArtifact,\n addNote,\n updateStatus,\n closeSession,\n retireStaleSessions,\n} from './workingMemory.js';\n\n// ── Episodic layer — episodic.ts + hindsightBridge.ts ──\nimport {\n recordEpisode,\n recallSimilarEpisodes,\n recallRecent,\n renderRecallBlock,\n} from './episodic.js';\nimport {\n isHindsightConnected,\n retainToHindsight,\n recallFromHindsight,\n retainStrategy,\n getHindsightHints,\n} from './hindsightBridge.js';\n\n// ── Semantic layer — memory.ts + relationship.ts + learning.ts ──\nimport {\n rememberFact,\n recallFact,\n searchMemories,\n} from './memory.js';\nimport {\n loadProfile,\n saveProfile,\n learnName,\n rememberProject,\n rememberContact,\n learnPreference,\n learnFact as learnUserFact,\n addGoal,\n calibrateTechnicalLevel,\n type PersonalProfile,\n} from './relationship.js';\nimport {\n recordToolResult,\n recordSuccessPattern,\n recordUserCorrection,\n} from './learning.js';\n\n/* ────────────────────────── Working layer ────────────────────────── */\n\n/**\n * In-flight, session-scoped memory. Cleared when the session closes.\n * Use this for: \"what decisions has the agent made this turn\", \"what\n * files has it produced\", \"what's it still waiting on\".\n *\n * Sessions are usually a Mission Chat thread or a sub-agent spawn,\n * but anything with a clear start/end can open one.\n */\nexport const working = {\n /** Open a new working-memory session. */\n openSession,\n /** Record a decision the agent made during this session. */\n recordDecision,\n /** Note a question the agent needs answered to proceed. */\n recordOpenQuestion,\n /** Mark an open question as answered. */\n resolveOpenQuestion,\n /** Note an artifact (file, URL, fact id) the agent produced. */\n recordArtifact,\n /** Free-form breadcrumb. */\n addNote,\n /** Update the session's status. */\n updateStatus,\n /** Close the session. Returns the final record. */\n closeSession,\n /** Sweep + archive sessions idle longer than the configured TTL. */\n retireStale: retireStaleSessions,\n};\n\n/* ────────────────────────── Episodic layer ────────────────────────── */\n\n/**\n * Past interactions. Recallable by recency or by vector similarity\n * against the current task. Persisted to disk.\n *\n * The cross-session bridge (Hindsight) lets TITAN recall things it\n * learned in a different process — e.g. \"last week's strategy for\n * this customer\".\n */\nexport const episodic = {\n /** Record a new episode (past interaction, decision, observation). */\n record: recordEpisode,\n /** Recall episodes most similar to a query (vector search). */\n recallSimilar: recallSimilarEpisodes,\n /** Recall the N most recent episodes. */\n recallRecent,\n /**\n * Render a markdown block of recalled episodes suitable for\n * injection into a system prompt. Wraps recallRecent +\n * recallSimilar with the right formatting.\n */\n renderRecallBlock,\n /** Cross-session retention via the Hindsight bridge. */\n retain: retainToHindsight,\n /** Cross-session recall. Returns null if not connected. */\n recallCrossSession: recallFromHindsight,\n /** Retain a STRATEGY (not just a memory) cross-session. */\n retainStrategy,\n /** Get cross-session hints relevant to a current message. */\n getCrossSessionHints: getHindsightHints,\n /** True if the Hindsight bridge has a working credential. */\n isCrossSessionConnected: isHindsightConnected,\n};\n\n/* ────────────────────────── Semantic layer ────────────────────────── */\n\n/**\n * Facts, learned patterns, the user model. The agent's \"knowledge\"\n * as opposed to its \"history\". Persisted to disk.\n *\n * Two flavors of writes:\n * - rememberFact: arbitrary key-value facts (\"project.name\", \"TITAN\").\n * - learnUser*: structured user-profile facts (\"user prefers vim\").\n *\n * Both are searchable via `search(query)`.\n */\nexport const semantic = {\n // ── Generic key-value facts ──\n /** Store a category/key fact. */\n rememberFact,\n /** Retrieve a category/key fact. */\n recallFact,\n /** Fuzzy search across all stored facts. */\n search: searchMemories,\n\n // ── User profile / relationship ──\n /** Load the persistent user profile (defaults to 'default' user). */\n getUserProfile: loadProfile,\n /** Save changes back to the user profile. */\n saveUserProfile: saveProfile,\n /** Learn the user's name. */\n learnUserName: learnName,\n /** Remember a project the user is working on. */\n learnUserProject: rememberProject,\n /** Remember a contact the user mentioned. */\n learnUserContact: rememberContact,\n /** Learn a user preference. */\n learnUserPreference: learnPreference,\n /** Learn a fact the user told us. */\n learnUserFact,\n /** Add a goal the user wants help with. */\n addUserGoal: addGoal,\n /** Calibrate technical level (beginner / intermediate / expert). */\n calibrateUserTechnicalLevel: calibrateTechnicalLevel,\n\n // ── Learned tool patterns ──\n /** Record a tool's outcome for the success-rate learner. */\n recordToolResult,\n /** Promote a tool sequence as a \"good pattern\" to prefer. */\n recordSuccessPattern,\n /** Record a user correction so the agent learns not to repeat it. */\n recordUserCorrection,\n};\n\n/* ────────────────────────── Top-level facade ────────────────────────── */\n\n/**\n * The single export new code should reach for. Spelled lowercase so\n * call sites read like english:\n *\n * import { memory } from '../memory/facade.js';\n *\n * memory.working.recordDecision(sessionId, 'go with option A', 'cheaper');\n * const past = await memory.episodic.recallSimilar('birthday parties');\n * memory.semantic.learnUserPreference('tone', 'casual');\n */\nexport const memory = {\n working,\n episodic,\n semantic,\n};\n\n/* ────────────────────────── Type re-exports ────────────────────────── */\n\nexport type { PersonalProfile };\n"],"mappings":";AA0EA;AAAA,EACI;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACG;AAGP;AAAA,EACI;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACG;AACP;AAAA,EACI;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACG;AAGP;AAAA,EACI;AAAA,EACA;AAAA,EACA;AAAA,OACG;AACP;AAAA,EACI;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb;AAAA,EACA;AAAA,OAEG;AACP;AAAA,EACI;AAAA,EACA;AAAA,EACA;AAAA,OACG;AAYA,MAAM,UAAU;AAAA;AAAA,EAEnB;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA,aAAa;AACjB;AAYO,MAAM,WAAW;AAAA;AAAA,EAEpB,QAAQ;AAAA;AAAA,EAER,eAAe;AAAA;AAAA,EAEf;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA;AAAA;AAAA,EAEA,QAAQ;AAAA;AAAA,EAER,oBAAoB;AAAA;AAAA,EAEpB;AAAA;AAAA,EAEA,sBAAsB;AAAA;AAAA,EAEtB,yBAAyB;AAC7B;AAcO,MAAM,WAAW;AAAA;AAAA;AAAA,EAGpB;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA,QAAQ;AAAA;AAAA;AAAA,EAIR,gBAAgB;AAAA;AAAA,EAEhB,iBAAiB;AAAA;AAAA,EAEjB,eAAe;AAAA;AAAA,EAEf,kBAAkB;AAAA;AAAA,EAElB,kBAAkB;AAAA;AAAA,EAElB,qBAAqB;AAAA;AAAA,EAErB;AAAA;AAAA,EAEA,aAAa;AAAA;AAAA,EAEb,6BAA6B;AAAA;AAAA;AAAA,EAI7B;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AACJ;AAcO,MAAM,SAAS;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AACJ;","names":[]}
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync } from "fs";
2
+ import { existsSync, unlinkSync } from "fs";
3
3
  import { join } from "path";
4
4
  const migration = {
5
5
  id: "003-soma-profile-default",
@@ -44,7 +44,7 @@ const migration = {
44
44
  }
45
45
  if (profile.learnedAboutUser.length === 0) {
46
46
  try {
47
- require("fs").unlinkSync(join(ctx.titanHome, rel));
47
+ unlinkSync(join(ctx.titanHome, rel));
48
48
  ctx.log(`Rolled back \u2014 removed untouched soma profile for ${userId}.`);
49
49
  } catch (err) {
50
50
  ctx.log(`rollback unlink failed: ${err.message}`);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/migrations/003-soma-profile-default.ts"],"sourcesContent":["/**\n * Migration 003 — soma-profile-default\n *\n * v6.0 introduces a per-user Soma profile that tracks how TITAN attunes\n * to each user over time (working hours, preferred Spaces, response style,\n * long-term emotional baseline). This migration seeds an empty profile so\n * the runtime never has to special-case \"first encounter.\"\n *\n * Path: `~/.titan/users/<userId>/soma.json`. For single-user installs the\n * userId is `default-user` (per src/gateway/server.ts getUserIdFromReq).\n */\nimport type { Migration } from './runner.js';\nimport { existsSync } from 'fs';\nimport { join } from 'path';\n\ninterface SomaProfile {\n schema: 1;\n userId: string;\n createdAt: string;\n updatedAt: string;\n /** Soma's learned baseline for THIS user's normal drive levels. */\n baseline: {\n curiosity: number;\n focus: number;\n fatigue: number;\n satisfaction: number;\n frustration: number;\n };\n /** Working-hours heuristic populated as Soma observes the user. */\n workingHours?: {\n timezone: string;\n weekdayStartHour?: number;\n weekdayEndHour?: number;\n };\n /** Free-form notes Soma can grow over time. */\n learnedAboutUser: string[];\n}\n\nexport const migration: Migration = {\n id: '003-soma-profile-default',\n version: '6.0.0-beta.1',\n description: 'Seed per-user Soma profile (~/.titan/users/<userId>/soma.json).',\n\n async up(ctx) {\n const userId = 'default-user';\n const rel = join('users', userId, 'soma.json');\n const full = join(ctx.titanHome, rel);\n if (existsSync(full)) {\n ctx.log(`Skipped — soma profile already exists for ${userId}.`);\n return;\n }\n const now = new Date().toISOString();\n const profile: SomaProfile = {\n schema: 1,\n userId,\n createdAt: now,\n updatedAt: now,\n // 0.5 = neutral baseline. Soma adjusts these as it observes the user.\n // Curiosity defaults slightly high (0.6) so v6 leans toward suggesting\n // new Spaces / widgets out of the gate, then calibrates.\n baseline: {\n curiosity: 0.6,\n focus: 0.5,\n fatigue: 0.4,\n satisfaction: 0.5,\n frustration: 0.3,\n },\n learnedAboutUser: [],\n };\n ctx.writeJson(rel, profile);\n ctx.log(`Seeded soma profile for ${userId}.`);\n },\n\n async rollback(ctx) {\n // Soma profile is the only artifact this migration creates, and\n // wiping it loses observation history. Only remove if the file is\n // exactly the freshly-seeded version (no observations yet).\n const userId = 'default-user';\n const rel = join('users', userId, 'soma.json');\n const profile = ctx.readJson<SomaProfile>(rel);\n if (!profile) { ctx.log('rollback: nothing to undo (no profile).'); return; }\n if (profile.learnedAboutUser.length === 0) {\n // Safe to remove — Soma hasn't learned anything yet.\n try {\n require('fs').unlinkSync(join(ctx.titanHome, rel));\n ctx.log(`Rolled back — removed untouched soma profile for ${userId}.`);\n } catch (err) {\n ctx.log(`rollback unlink failed: ${(err as Error).message}`);\n }\n } else {\n ctx.log(`rollback: profile has ${profile.learnedAboutUser.length} observations — preserving.`);\n }\n },\n};\n"],"mappings":";AAYA,SAAS,kBAAkB;AAC3B,SAAS,YAAY;AAyBd,MAAM,YAAuB;AAAA,EAChC,IAAI;AAAA,EACJ,SAAS;AAAA,EACT,aAAa;AAAA,EAEb,MAAM,GAAG,KAAK;AACV,UAAM,SAAS;AACf,UAAM,MAAM,KAAK,SAAS,QAAQ,WAAW;AAC7C,UAAM,OAAO,KAAK,IAAI,WAAW,GAAG;AACpC,QAAI,WAAW,IAAI,GAAG;AAClB,UAAI,IAAI,kDAA6C,MAAM,GAAG;AAC9D;AAAA,IACJ;AACA,UAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AACnC,UAAM,UAAuB;AAAA,MACzB,QAAQ;AAAA,MACR;AAAA,MACA,WAAW;AAAA,MACX,WAAW;AAAA;AAAA;AAAA;AAAA,MAIX,UAAU;AAAA,QACN,WAAW;AAAA,QACX,OAAO;AAAA,QACP,SAAS;AAAA,QACT,cAAc;AAAA,QACd,aAAa;AAAA,MACjB;AAAA,MACA,kBAAkB,CAAC;AAAA,IACvB;AACA,QAAI,UAAU,KAAK,OAAO;AAC1B,QAAI,IAAI,2BAA2B,MAAM,GAAG;AAAA,EAChD;AAAA,EAEA,MAAM,SAAS,KAAK;AAIhB,UAAM,SAAS;AACf,UAAM,MAAM,KAAK,SAAS,QAAQ,WAAW;AAC7C,UAAM,UAAU,IAAI,SAAsB,GAAG;AAC7C,QAAI,CAAC,SAAS;AAAE,UAAI,IAAI,yCAAyC;AAAG;AAAA,IAAQ;AAC5E,QAAI,QAAQ,iBAAiB,WAAW,GAAG;AAEvC,UAAI;AACA,gBAAQ,IAAI,EAAE,WAAW,KAAK,IAAI,WAAW,GAAG,CAAC;AACjD,YAAI,IAAI,yDAAoD,MAAM,GAAG;AAAA,MACzE,SAAS,KAAK;AACV,YAAI,IAAI,2BAA4B,IAAc,OAAO,EAAE;AAAA,MAC/D;AAAA,IACJ,OAAO;AACH,UAAI,IAAI,yBAAyB,QAAQ,iBAAiB,MAAM,kCAA6B;AAAA,IACjG;AAAA,EACJ;AACJ;","names":[]}
1
+ {"version":3,"sources":["../../src/migrations/003-soma-profile-default.ts"],"sourcesContent":["/**\n * Migration 003 — soma-profile-default\n *\n * v6.0 introduces a per-user Soma profile that tracks how TITAN attunes\n * to each user over time (working hours, preferred Spaces, response style,\n * long-term emotional baseline). This migration seeds an empty profile so\n * the runtime never has to special-case \"first encounter.\"\n *\n * Path: `~/.titan/users/<userId>/soma.json`. For single-user installs the\n * userId is `default-user` (per src/gateway/server.ts getUserIdFromReq).\n */\nimport type { Migration } from './runner.js';\nimport { existsSync, unlinkSync } from 'fs';\nimport { join } from 'path';\n\ninterface SomaProfile {\n schema: 1;\n userId: string;\n createdAt: string;\n updatedAt: string;\n /** Soma's learned baseline for THIS user's normal drive levels. */\n baseline: {\n curiosity: number;\n focus: number;\n fatigue: number;\n satisfaction: number;\n frustration: number;\n };\n /** Working-hours heuristic populated as Soma observes the user. */\n workingHours?: {\n timezone: string;\n weekdayStartHour?: number;\n weekdayEndHour?: number;\n };\n /** Free-form notes Soma can grow over time. */\n learnedAboutUser: string[];\n}\n\nexport const migration: Migration = {\n id: '003-soma-profile-default',\n version: '6.0.0-beta.1',\n description: 'Seed per-user Soma profile (~/.titan/users/<userId>/soma.json).',\n\n async up(ctx) {\n const userId = 'default-user';\n const rel = join('users', userId, 'soma.json');\n const full = join(ctx.titanHome, rel);\n if (existsSync(full)) {\n ctx.log(`Skipped — soma profile already exists for ${userId}.`);\n return;\n }\n const now = new Date().toISOString();\n const profile: SomaProfile = {\n schema: 1,\n userId,\n createdAt: now,\n updatedAt: now,\n // 0.5 = neutral baseline. Soma adjusts these as it observes the user.\n // Curiosity defaults slightly high (0.6) so v6 leans toward suggesting\n // new Spaces / widgets out of the gate, then calibrates.\n baseline: {\n curiosity: 0.6,\n focus: 0.5,\n fatigue: 0.4,\n satisfaction: 0.5,\n frustration: 0.3,\n },\n learnedAboutUser: [],\n };\n ctx.writeJson(rel, profile);\n ctx.log(`Seeded soma profile for ${userId}.`);\n },\n\n async rollback(ctx) {\n // Soma profile is the only artifact this migration creates, and\n // wiping it loses observation history. Only remove if the file is\n // exactly the freshly-seeded version (no observations yet).\n const userId = 'default-user';\n const rel = join('users', userId, 'soma.json');\n const profile = ctx.readJson<SomaProfile>(rel);\n if (!profile) { ctx.log('rollback: nothing to undo (no profile).'); return; }\n if (profile.learnedAboutUser.length === 0) {\n // Safe to remove — Soma hasn't learned anything yet.\n try {\n unlinkSync(join(ctx.titanHome, rel));\n ctx.log(`Rolled back — removed untouched soma profile for ${userId}.`);\n } catch (err) {\n ctx.log(`rollback unlink failed: ${(err as Error).message}`);\n }\n } else {\n ctx.log(`rollback: profile has ${profile.learnedAboutUser.length} observations — preserving.`);\n }\n },\n};\n"],"mappings":";AAYA,SAAS,YAAY,kBAAkB;AACvC,SAAS,YAAY;AAyBd,MAAM,YAAuB;AAAA,EAChC,IAAI;AAAA,EACJ,SAAS;AAAA,EACT,aAAa;AAAA,EAEb,MAAM,GAAG,KAAK;AACV,UAAM,SAAS;AACf,UAAM,MAAM,KAAK,SAAS,QAAQ,WAAW;AAC7C,UAAM,OAAO,KAAK,IAAI,WAAW,GAAG;AACpC,QAAI,WAAW,IAAI,GAAG;AAClB,UAAI,IAAI,kDAA6C,MAAM,GAAG;AAC9D;AAAA,IACJ;AACA,UAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AACnC,UAAM,UAAuB;AAAA,MACzB,QAAQ;AAAA,MACR;AAAA,MACA,WAAW;AAAA,MACX,WAAW;AAAA;AAAA;AAAA;AAAA,MAIX,UAAU;AAAA,QACN,WAAW;AAAA,QACX,OAAO;AAAA,QACP,SAAS;AAAA,QACT,cAAc;AAAA,QACd,aAAa;AAAA,MACjB;AAAA,MACA,kBAAkB,CAAC;AAAA,IACvB;AACA,QAAI,UAAU,KAAK,OAAO;AAC1B,QAAI,IAAI,2BAA2B,MAAM,GAAG;AAAA,EAChD;AAAA,EAEA,MAAM,SAAS,KAAK;AAIhB,UAAM,SAAS;AACf,UAAM,MAAM,KAAK,SAAS,QAAQ,WAAW;AAC7C,UAAM,UAAU,IAAI,SAAsB,GAAG;AAC7C,QAAI,CAAC,SAAS;AAAE,UAAI,IAAI,yCAAyC;AAAG;AAAA,IAAQ;AAC5E,QAAI,QAAQ,iBAAiB,WAAW,GAAG;AAEvC,UAAI;AACA,mBAAW,KAAK,IAAI,WAAW,GAAG,CAAC;AACnC,YAAI,IAAI,yDAAoD,MAAM,GAAG;AAAA,MACzE,SAAS,KAAK;AACV,YAAI,IAAI,2BAA4B,IAAc,OAAO,EAAE;AAAA,MAC/D;AAAA,IACJ,OAAO;AACH,UAAI,IAAI,yBAAyB,QAAQ,iBAAiB,MAAM,kCAA6B;AAAA,IACjG;AAAA,EACJ;AACJ;","names":[]}
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ import { unlinkSync } from "fs";
3
+ import { join } from "path";
2
4
  const migration = {
3
5
  id: "005-route-redirects",
4
6
  version: "6.0.0-beta.1",
@@ -62,7 +64,7 @@ const migration = {
62
64
  },
63
65
  async rollback(ctx) {
64
66
  try {
65
- require("fs").unlinkSync(require("path").join(ctx.titanHome, "route-redirects.json"));
67
+ unlinkSync(join(ctx.titanHome, "route-redirects.json"));
66
68
  ctx.log("Removed route-redirects.json.");
67
69
  } catch {
68
70
  ctx.log("rollback: route-redirects.json already absent.");
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/migrations/005-route-redirects.ts"],"sourcesContent":["/**\n * Migration 005 — route-redirects\n *\n * v6.0's shell collapses 36 admin panels into the widget gallery. v5.x\n * users have bookmarks to `/admin/dream`, `/command-post/agents`,\n * `/admin/training`, etc. that would 404 after the shell rework. This\n * migration writes a redirect map the React Router config consumes at\n * boot to keep those bookmarks working — they land on `/space/default`\n * with the corresponding widget auto-pinned.\n *\n * No code change is forced; the file is purely declarative data the\n * frontend reads on first load.\n */\nimport type { Migration } from './runner.js';\n\ninterface RouteRedirect {\n from: string;\n /** target Space — defaults to \"default\" if absent. */\n spaceId?: string;\n /** Optional widget template id to auto-pin after redirect. */\n pinWidget?: string;\n}\n\ninterface RedirectsFile {\n schema: 1;\n redirects: RouteRedirect[];\n generatedAt: string;\n}\n\nexport const migration: Migration = {\n id: '005-route-redirects',\n version: '6.0.0-beta.1',\n description: 'Write a v5→v6 admin-route redirect map so existing bookmarks keep working.',\n\n async up(ctx) {\n const redirects: RouteRedirect[] = [\n // The 36 panels that move from fixed admin routes to widgets.\n { from: '/admin/dream', pinWidget: 'system-dream' },\n { from: '/admin/training', pinWidget: 'system-training' },\n { from: '/admin/vram', pinWidget: 'system-vram' },\n { from: '/admin/cron', pinWidget: 'system-cron' },\n { from: '/admin/logs', pinWidget: 'system-logs' },\n { from: '/admin/activity', pinWidget: 'system-activity' },\n { from: '/admin/agents', pinWidget: 'system-agents' },\n { from: '/admin/memory-graph', pinWidget: 'system-memory-graph' },\n { from: '/admin/memory-wiki', pinWidget: 'system-memory-wiki' },\n { from: '/admin/organism', pinWidget: 'system-organism' },\n { from: '/admin/fleet', pinWidget: 'system-fleet' },\n { from: '/admin/files', pinWidget: 'system-files' },\n { from: '/admin/personas', pinWidget: 'system-personas' },\n { from: '/admin/persona-profiles', pinWidget: 'system-persona-profiles' },\n { from: '/admin/recipes', pinWidget: 'system-recipes' },\n { from: '/admin/workflows', pinWidget: 'system-workflows' },\n { from: '/admin/autopilot', pinWidget: 'system-autopilot' },\n { from: '/admin/autonomy', pinWidget: 'system-autonomy' },\n { from: '/admin/autoresearch', pinWidget: 'system-autoresearch' },\n { from: '/admin/self-improve', pinWidget: 'system-self-improve' },\n { from: '/admin/self-proposals', pinWidget: 'system-self-proposals' },\n { from: '/admin/teams', pinWidget: 'system-teams' },\n { from: '/admin/telemetry', pinWidget: 'system-telemetry' },\n { from: '/admin/learning', pinWidget: 'system-learning' },\n { from: '/admin/eval-harness', pinWidget: 'system-eval-harness' },\n { from: '/admin/evals', pinWidget: 'system-evals' },\n { from: '/admin/audit', pinWidget: 'system-audit' },\n { from: '/admin/backup', pinWidget: 'system-backup' },\n { from: '/admin/browser', pinWidget: 'system-browser' },\n { from: '/admin/checkpoints', pinWidget: 'system-checkpoints' },\n { from: '/admin/homelab', pinWidget: 'system-homelab' },\n { from: '/admin/mcp', pinWidget: 'system-mcp' },\n { from: '/admin/mesh', pinWidget: 'system-mesh' },\n { from: '/admin/nvidia', pinWidget: 'system-nvidia' },\n { from: '/admin/approval-progress', pinWidget: 'system-approvals' },\n { from: '/admin/overview', pinWidget: 'system-overview' },\n // Command-post legacy routes redirect to their fixed admin home,\n // not to widget pins (Command-post stays a fixed page in v6).\n { from: '/command-post/agents', spaceId: 'admin', pinWidget: 'system-agents' },\n { from: '/command-post/activity', spaceId: 'admin', pinWidget: 'system-activity' },\n ];\n\n // Idempotent write. If the user has hand-edited the file, leave\n // their version alone — we only overwrite when our generated set\n // differs in structure (catches the v5.7→v6 boot path).\n const existing = ctx.readJson<RedirectsFile>('route-redirects.json');\n if (existing && Array.isArray(existing.redirects) && existing.redirects.length >= redirects.length) {\n ctx.log(`Skipped — route-redirects.json already has ${existing.redirects.length} entries.`);\n return;\n }\n const out: RedirectsFile = {\n schema: 1,\n redirects,\n generatedAt: new Date().toISOString(),\n };\n ctx.writeJson('route-redirects.json', out);\n ctx.log(`Wrote ${redirects.length} v5→v6 route redirects.`);\n },\n\n async rollback(ctx) {\n // Removing the redirect map causes old bookmarks to 404 again —\n // bad UX but reversible by re-running the migration. Only remove\n // when the user has reverted to a v5.x codebase that won't read it.\n try {\n require('fs').unlinkSync(require('path').join(ctx.titanHome, 'route-redirects.json'));\n ctx.log('Removed route-redirects.json.');\n } catch {\n ctx.log('rollback: route-redirects.json already absent.');\n }\n },\n};\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/migrations/005-route-redirects.ts"],"sourcesContent":["/**\n * Migration 005 — route-redirects\n *\n * v6.0's shell collapses 36 admin panels into the widget gallery. v5.x\n * users have bookmarks to `/admin/dream`, `/command-post/agents`,\n * `/admin/training`, etc. that would 404 after the shell rework. This\n * migration writes a redirect map the React Router config consumes at\n * boot to keep those bookmarks working — they land on `/space/default`\n * with the corresponding widget auto-pinned.\n *\n * No code change is forced; the file is purely declarative data the\n * frontend reads on first load.\n */\nimport { unlinkSync } from 'fs';\nimport { join } from 'path';\nimport type { Migration } from './runner.js';\n\ninterface RouteRedirect {\n from: string;\n /** target Space — defaults to \"default\" if absent. */\n spaceId?: string;\n /** Optional widget template id to auto-pin after redirect. */\n pinWidget?: string;\n}\n\ninterface RedirectsFile {\n schema: 1;\n redirects: RouteRedirect[];\n generatedAt: string;\n}\n\nexport const migration: Migration = {\n id: '005-route-redirects',\n version: '6.0.0-beta.1',\n description: 'Write a v5→v6 admin-route redirect map so existing bookmarks keep working.',\n\n async up(ctx) {\n const redirects: RouteRedirect[] = [\n // The 36 panels that move from fixed admin routes to widgets.\n { from: '/admin/dream', pinWidget: 'system-dream' },\n { from: '/admin/training', pinWidget: 'system-training' },\n { from: '/admin/vram', pinWidget: 'system-vram' },\n { from: '/admin/cron', pinWidget: 'system-cron' },\n { from: '/admin/logs', pinWidget: 'system-logs' },\n { from: '/admin/activity', pinWidget: 'system-activity' },\n { from: '/admin/agents', pinWidget: 'system-agents' },\n { from: '/admin/memory-graph', pinWidget: 'system-memory-graph' },\n { from: '/admin/memory-wiki', pinWidget: 'system-memory-wiki' },\n { from: '/admin/organism', pinWidget: 'system-organism' },\n { from: '/admin/fleet', pinWidget: 'system-fleet' },\n { from: '/admin/files', pinWidget: 'system-files' },\n { from: '/admin/personas', pinWidget: 'system-personas' },\n { from: '/admin/persona-profiles', pinWidget: 'system-persona-profiles' },\n { from: '/admin/recipes', pinWidget: 'system-recipes' },\n { from: '/admin/workflows', pinWidget: 'system-workflows' },\n { from: '/admin/autopilot', pinWidget: 'system-autopilot' },\n { from: '/admin/autonomy', pinWidget: 'system-autonomy' },\n { from: '/admin/autoresearch', pinWidget: 'system-autoresearch' },\n { from: '/admin/self-improve', pinWidget: 'system-self-improve' },\n { from: '/admin/self-proposals', pinWidget: 'system-self-proposals' },\n { from: '/admin/teams', pinWidget: 'system-teams' },\n { from: '/admin/telemetry', pinWidget: 'system-telemetry' },\n { from: '/admin/learning', pinWidget: 'system-learning' },\n { from: '/admin/eval-harness', pinWidget: 'system-eval-harness' },\n { from: '/admin/evals', pinWidget: 'system-evals' },\n { from: '/admin/audit', pinWidget: 'system-audit' },\n { from: '/admin/backup', pinWidget: 'system-backup' },\n { from: '/admin/browser', pinWidget: 'system-browser' },\n { from: '/admin/checkpoints', pinWidget: 'system-checkpoints' },\n { from: '/admin/homelab', pinWidget: 'system-homelab' },\n { from: '/admin/mcp', pinWidget: 'system-mcp' },\n { from: '/admin/mesh', pinWidget: 'system-mesh' },\n { from: '/admin/nvidia', pinWidget: 'system-nvidia' },\n { from: '/admin/approval-progress', pinWidget: 'system-approvals' },\n { from: '/admin/overview', pinWidget: 'system-overview' },\n // Command-post legacy routes redirect to their fixed admin home,\n // not to widget pins (Command-post stays a fixed page in v6).\n { from: '/command-post/agents', spaceId: 'admin', pinWidget: 'system-agents' },\n { from: '/command-post/activity', spaceId: 'admin', pinWidget: 'system-activity' },\n ];\n\n // Idempotent write. If the user has hand-edited the file, leave\n // their version alone — we only overwrite when our generated set\n // differs in structure (catches the v5.7→v6 boot path).\n const existing = ctx.readJson<RedirectsFile>('route-redirects.json');\n if (existing && Array.isArray(existing.redirects) && existing.redirects.length >= redirects.length) {\n ctx.log(`Skipped — route-redirects.json already has ${existing.redirects.length} entries.`);\n return;\n }\n const out: RedirectsFile = {\n schema: 1,\n redirects,\n generatedAt: new Date().toISOString(),\n };\n ctx.writeJson('route-redirects.json', out);\n ctx.log(`Wrote ${redirects.length} v5→v6 route redirects.`);\n },\n\n async rollback(ctx) {\n // Removing the redirect map causes old bookmarks to 404 again —\n // bad UX but reversible by re-running the migration. Only remove\n // when the user has reverted to a v5.x codebase that won't read it.\n try {\n unlinkSync(join(ctx.titanHome, 'route-redirects.json'));\n ctx.log('Removed route-redirects.json.');\n } catch {\n ctx.log('rollback: route-redirects.json already absent.');\n }\n },\n};\n"],"mappings":"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","names":[]}
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { homedir } from "os";
5
5
  import logger from "../utils/logger.js";
@@ -152,7 +152,7 @@ async function rollbackMigration(migration, titanVersion) {
152
152
  }
153
153
  function __resetStateForTests() {
154
154
  try {
155
- require("fs").unlinkSync(statePath());
155
+ unlinkSync(statePath());
156
156
  } catch {
157
157
  }
158
158
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/migrations/runner.ts"],"sourcesContent":["/**\n * TITAN — Migration Runner (U2, v5.7.2+ upgrade-safety phase)\n *\n * Why this exists\n * ---------------\n * Tony's standing rule for v6.0: \"make sure that everyone on older versions\n * of TITAN are able to update properly without breaking their old TITAN\n * settings and functions.\" That means every v6.0 step that changes data\n * shape needs a versioned, idempotent, reversible migration.\n *\n * The runner:\n * 1. Loads `~/.titan/MIGRATION_STATE.json` (which migrations have already\n * run on this install).\n * 2. Asks every registered migration whether it still needs to run.\n * 3. Runs the pending ones in order (sorted by id ascending).\n * 4. Optionally creates a pre-flight backup via the U1 backup primitives\n * so a failed migration can be auto-rolled-back (U3 wires this).\n * 5. Persists state after each successful migration so a crash mid-run\n * doesn't redo work.\n *\n * Each migration is a plain object: `{id, version, description, up, rollback?}`.\n * The id is a stable, zero-padded numeric prefix + slug (e.g.\n * `001-localstorage-spaces-to-server`). Sorting by id alphabetically gives\n * deterministic execution order — that's load-bearing.\n *\n * Reference:\n * - ~/.claude/projects/-Users-michaelelliott/memory/titan-v6-living-canvas.md\n * (U1–U6 in detail)\n * - docs/MIGRATIONS.md (lands as U6)\n */\n\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'MigrationRunner';\n\n/** Lazy-resolved path so tests can point HOME at a temp dir. Mirrors\n * src/storage/backup.ts. */\nfunction titanHome(): string {\n const env = process.env.TITAN_HOME;\n if (env) return env.startsWith('~/') ? join(homedir(), env.slice(2)) : env;\n return join(homedir(), '.titan');\n}\nfunction statePath(): string { return join(titanHome(), 'MIGRATION_STATE.json'); }\n\n/** Migration state persisted to disk between runs. */\nexport interface MigrationState {\n /** Version-2 schema for this file. Bump only on breaking shape changes. */\n schema: 1;\n /** Applied migration ids, in the order they ran. */\n applied: Array<{\n id: string;\n version: string;\n appliedAt: string; // ISO timestamp\n durationMs: number;\n }>;\n /** Last successful run id — convenience for \"where did we leave off?\". */\n lastAppliedId?: string;\n /** Last save timestamp. */\n updatedAt: string;\n}\n\n/** Context passed to migration `up()` / `rollback()` functions. */\nexport interface MigrationContext {\n /** TITAN data root (~/.titan/). */\n titanHome: string;\n /** Resolved at runtime so migrations don't accidentally cache paths. */\n titanVersion: string;\n /** Helper to write a file inside titanHome. Idempotent — writes only\n * when content differs, so re-runs are no-ops. */\n writeIfChanged: (relPath: string, content: string) => boolean;\n /** Helper to read a JSON file. Returns null when missing or malformed. */\n readJson: <T = unknown>(relPath: string) => T | null;\n /** Helper to write a JSON file (pretty-printed). Idempotent. */\n writeJson: (relPath: string, value: unknown) => void;\n /** Structured log channel — output appears in the migration runner's\n * consolidated log, not as scattered logger.info calls. */\n log: (msg: string) => void;\n}\n\n/** A single migration's contract. */\nexport interface Migration {\n /** Sortable id — zero-padded numeric prefix + kebab slug. */\n id: string;\n /** TITAN version this migration targets (informational). */\n version: string;\n /** One-sentence description of what this migration does. */\n description: string;\n /** Run the migration. MUST be idempotent — same input twice = same output. */\n up: (ctx: MigrationContext) => Promise<void> | void;\n /** Optional reverse. Called by the auto-rollback path in U3/U5 when a\n * migration's smoke check fails. */\n rollback?: (ctx: MigrationContext) => Promise<void> | void;\n}\n\n/** Result of a runner invocation. */\nexport interface MigrationRunResult {\n /** True only if every pending migration applied without throwing. */\n success: boolean;\n /** Migrations actually run this invocation (in order). */\n ran: string[];\n /** Migrations skipped because they were already applied. */\n skipped: string[];\n /** First failing migration id, if any. */\n failed?: string;\n /** Error message from the failing migration. */\n error?: string;\n /** Per-migration timing for telemetry. */\n timings: Array<{ id: string; durationMs: number; status: 'ok' | 'fail' | 'skip' }>;\n}\n\n// ─── State load / save ─────────────────────────────────────────────\n\nfunction loadState(): MigrationState {\n try {\n const p = statePath();\n if (existsSync(p)) {\n const raw = JSON.parse(readFileSync(p, 'utf-8'));\n if (raw && typeof raw === 'object' && raw.schema === 1 && Array.isArray(raw.applied)) {\n return raw as MigrationState;\n }\n }\n } catch (err) {\n logger.warn(COMPONENT, `MIGRATION_STATE.json malformed — starting fresh: ${(err as Error).message}`);\n }\n return { schema: 1, applied: [], updatedAt: new Date().toISOString() };\n}\n\nfunction saveState(state: MigrationState): void {\n try { mkdirSync(titanHome(), { recursive: true }); } catch { /* exists */ }\n state.updatedAt = new Date().toISOString();\n writeFileSync(statePath(), JSON.stringify(state, null, 2));\n}\n\n// ─── Context construction ─────────────────────────────────────────\n\nfunction makeContext(titanVersion: string, logBuffer: string[]): MigrationContext {\n const home = titanHome();\n return {\n titanHome: home,\n titanVersion,\n log: (msg: string) => logBuffer.push(msg),\n readJson<T = unknown>(relPath: string): T | null {\n try {\n const full = join(home, relPath);\n if (!existsSync(full)) return null;\n return JSON.parse(readFileSync(full, 'utf-8')) as T;\n } catch { return null; }\n },\n writeJson(relPath: string, value: unknown): void {\n const full = join(home, relPath);\n try { mkdirSync(join(full, '..'), { recursive: true }); } catch { /* exists */ }\n writeFileSync(full, JSON.stringify(value, null, 2));\n },\n writeIfChanged(relPath: string, content: string): boolean {\n const full = join(home, relPath);\n try {\n if (existsSync(full)) {\n const prev = readFileSync(full, 'utf-8');\n if (prev === content) return false;\n }\n } catch { /* fall through to write */ }\n try { mkdirSync(join(full, '..'), { recursive: true }); } catch { /* exists */ }\n writeFileSync(full, content);\n return true;\n },\n };\n}\n\n// ─── Runner ────────────────────────────────────────────────────────\n\n/**\n * Plan-only: returns the ids of pending migrations without running them.\n * Useful for `titan migrate --dry-run` and the U3 pre-flight check.\n */\nexport function planMigrations(allMigrations: Migration[]): { pending: string[]; alreadyApplied: string[] } {\n const state = loadState();\n const appliedIds = new Set(state.applied.map(a => a.id));\n const sorted = [...allMigrations].sort((a, b) => a.id.localeCompare(b.id));\n const pending: string[] = [];\n const alreadyApplied: string[] = [];\n for (const m of sorted) {\n if (appliedIds.has(m.id)) alreadyApplied.push(m.id);\n else pending.push(m.id);\n }\n return { pending, alreadyApplied };\n}\n\n/**\n * Run all pending migrations in id-ascending order. Stops on the first failure\n * — does NOT continue past a failed migration (otherwise we'd compound errors).\n *\n * The caller (U3 wiring) is responsible for the pre-flight backup and the\n * auto-rollback on failure.\n */\nexport async function runMigrations(allMigrations: Migration[], titanVersion: string): Promise<MigrationRunResult> {\n const state = loadState();\n const appliedIds = new Set(state.applied.map(a => a.id));\n const sorted = [...allMigrations].sort((a, b) => a.id.localeCompare(b.id));\n\n const result: MigrationRunResult = {\n success: true,\n ran: [],\n skipped: [],\n timings: [],\n };\n\n for (const m of sorted) {\n if (appliedIds.has(m.id)) {\n result.skipped.push(m.id);\n result.timings.push({ id: m.id, durationMs: 0, status: 'skip' });\n continue;\n }\n const logBuffer: string[] = [];\n const ctx = makeContext(titanVersion, logBuffer);\n const t0 = Date.now();\n try {\n logger.info(COMPONENT, `→ ${m.id} (${m.version}) ${m.description}`);\n await m.up(ctx);\n const dur = Date.now() - t0;\n state.applied.push({ id: m.id, version: m.version, appliedAt: new Date().toISOString(), durationMs: dur });\n state.lastAppliedId = m.id;\n // Persist after each successful migration so a crash mid-run\n // doesn't redo finished migrations.\n saveState(state);\n result.ran.push(m.id);\n result.timings.push({ id: m.id, durationMs: dur, status: 'ok' });\n if (logBuffer.length > 0) {\n logger.info(COMPONENT, ` ${m.id} log:\\n ${logBuffer.join('\\n ')}`);\n }\n logger.info(COMPONENT, `✓ ${m.id} applied in ${dur}ms`);\n } catch (err) {\n const dur = Date.now() - t0;\n result.success = false;\n result.failed = m.id;\n result.error = (err as Error).message;\n result.timings.push({ id: m.id, durationMs: dur, status: 'fail' });\n logger.error(COMPONENT, `✗ ${m.id} FAILED after ${dur}ms: ${result.error}`);\n if (logBuffer.length > 0) {\n logger.info(COMPONENT, ` ${m.id} partial log:\\n ${logBuffer.join('\\n ')}`);\n }\n return result;\n }\n }\n return result;\n}\n\n/**\n * Run a single migration's `rollback()` if defined. Used by the auto-rollback\n * path in U3/U5 when post-migration smoke checks fail. Failures here are\n * logged but never thrown — at this point we're already in error-recovery\n * and the caller has more authoritative fallback (restore from backup).\n */\nexport async function rollbackMigration(migration: Migration, titanVersion: string): Promise<void> {\n if (!migration.rollback) {\n logger.warn(COMPONENT, `Migration ${migration.id} has no rollback() — relying on backup restore.`);\n return;\n }\n const logBuffer: string[] = [];\n const ctx = makeContext(titanVersion, logBuffer);\n try {\n await migration.rollback(ctx);\n logger.info(COMPONENT, `↩ ${migration.id} rolled back`);\n } catch (err) {\n logger.error(COMPONENT, `↩ ${migration.id} rollback FAILED: ${(err as Error).message}`);\n }\n}\n\n// ─── Test surface ─────────────────────────────────────────────────\n\n/** Test-only: reset the state file. Production code never calls this. */\nexport function __resetStateForTests(): void {\n try { require('fs').unlinkSync(statePath()); } catch { /* fine */ }\n}\n\n/** Test-only: read state directly. */\nexport function __readStateForTests(): MigrationState {\n return loadState();\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/migrations/runner.ts"],"sourcesContent":["/**\n * TITAN — Migration Runner (U2, v5.7.2+ upgrade-safety phase)\n *\n * Why this exists\n * ---------------\n * Tony's standing rule for v6.0: \"make sure that everyone on older versions\n * of TITAN are able to update properly without breaking their old TITAN\n * settings and functions.\" That means every v6.0 step that changes data\n * shape needs a versioned, idempotent, reversible migration.\n *\n * The runner:\n * 1. Loads `~/.titan/MIGRATION_STATE.json` (which migrations have already\n * run on this install).\n * 2. Asks every registered migration whether it still needs to run.\n * 3. Runs the pending ones in order (sorted by id ascending).\n * 4. Optionally creates a pre-flight backup via the U1 backup primitives\n * so a failed migration can be auto-rolled-back (U3 wires this).\n * 5. Persists state after each successful migration so a crash mid-run\n * doesn't redo work.\n *\n * Each migration is a plain object: `{id, version, description, up, rollback?}`.\n * The id is a stable, zero-padded numeric prefix + slug (e.g.\n * `001-localstorage-spaces-to-server`). Sorting by id alphabetically gives\n * deterministic execution order — that's load-bearing.\n *\n * Reference:\n * - ~/.claude/projects/-Users-michaelelliott/memory/titan-v6-living-canvas.md\n * (U1–U6 in detail)\n * - docs/MIGRATIONS.md (lands as U6)\n */\n\nimport { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'MigrationRunner';\n\n/** Lazy-resolved path so tests can point HOME at a temp dir. Mirrors\n * src/storage/backup.ts. */\nfunction titanHome(): string {\n const env = process.env.TITAN_HOME;\n if (env) return env.startsWith('~/') ? join(homedir(), env.slice(2)) : env;\n return join(homedir(), '.titan');\n}\nfunction statePath(): string { return join(titanHome(), 'MIGRATION_STATE.json'); }\n\n/** Migration state persisted to disk between runs. */\nexport interface MigrationState {\n /** Version-2 schema for this file. Bump only on breaking shape changes. */\n schema: 1;\n /** Applied migration ids, in the order they ran. */\n applied: Array<{\n id: string;\n version: string;\n appliedAt: string; // ISO timestamp\n durationMs: number;\n }>;\n /** Last successful run id — convenience for \"where did we leave off?\". */\n lastAppliedId?: string;\n /** Last save timestamp. */\n updatedAt: string;\n}\n\n/** Context passed to migration `up()` / `rollback()` functions. */\nexport interface MigrationContext {\n /** TITAN data root (~/.titan/). */\n titanHome: string;\n /** Resolved at runtime so migrations don't accidentally cache paths. */\n titanVersion: string;\n /** Helper to write a file inside titanHome. Idempotent — writes only\n * when content differs, so re-runs are no-ops. */\n writeIfChanged: (relPath: string, content: string) => boolean;\n /** Helper to read a JSON file. Returns null when missing or malformed. */\n readJson: <T = unknown>(relPath: string) => T | null;\n /** Helper to write a JSON file (pretty-printed). Idempotent. */\n writeJson: (relPath: string, value: unknown) => void;\n /** Structured log channel — output appears in the migration runner's\n * consolidated log, not as scattered logger.info calls. */\n log: (msg: string) => void;\n}\n\n/** A single migration's contract. */\nexport interface Migration {\n /** Sortable id — zero-padded numeric prefix + kebab slug. */\n id: string;\n /** TITAN version this migration targets (informational). */\n version: string;\n /** One-sentence description of what this migration does. */\n description: string;\n /** Run the migration. MUST be idempotent — same input twice = same output. */\n up: (ctx: MigrationContext) => Promise<void> | void;\n /** Optional reverse. Called by the auto-rollback path in U3/U5 when a\n * migration's smoke check fails. */\n rollback?: (ctx: MigrationContext) => Promise<void> | void;\n}\n\n/** Result of a runner invocation. */\nexport interface MigrationRunResult {\n /** True only if every pending migration applied without throwing. */\n success: boolean;\n /** Migrations actually run this invocation (in order). */\n ran: string[];\n /** Migrations skipped because they were already applied. */\n skipped: string[];\n /** First failing migration id, if any. */\n failed?: string;\n /** Error message from the failing migration. */\n error?: string;\n /** Per-migration timing for telemetry. */\n timings: Array<{ id: string; durationMs: number; status: 'ok' | 'fail' | 'skip' }>;\n}\n\n// ─── State load / save ─────────────────────────────────────────────\n\nfunction loadState(): MigrationState {\n try {\n const p = statePath();\n if (existsSync(p)) {\n const raw = JSON.parse(readFileSync(p, 'utf-8'));\n if (raw && typeof raw === 'object' && raw.schema === 1 && Array.isArray(raw.applied)) {\n return raw as MigrationState;\n }\n }\n } catch (err) {\n logger.warn(COMPONENT, `MIGRATION_STATE.json malformed — starting fresh: ${(err as Error).message}`);\n }\n return { schema: 1, applied: [], updatedAt: new Date().toISOString() };\n}\n\nfunction saveState(state: MigrationState): void {\n try { mkdirSync(titanHome(), { recursive: true }); } catch { /* exists */ }\n state.updatedAt = new Date().toISOString();\n writeFileSync(statePath(), JSON.stringify(state, null, 2));\n}\n\n// ─── Context construction ─────────────────────────────────────────\n\nfunction makeContext(titanVersion: string, logBuffer: string[]): MigrationContext {\n const home = titanHome();\n return {\n titanHome: home,\n titanVersion,\n log: (msg: string) => logBuffer.push(msg),\n readJson<T = unknown>(relPath: string): T | null {\n try {\n const full = join(home, relPath);\n if (!existsSync(full)) return null;\n return JSON.parse(readFileSync(full, 'utf-8')) as T;\n } catch { return null; }\n },\n writeJson(relPath: string, value: unknown): void {\n const full = join(home, relPath);\n try { mkdirSync(join(full, '..'), { recursive: true }); } catch { /* exists */ }\n writeFileSync(full, JSON.stringify(value, null, 2));\n },\n writeIfChanged(relPath: string, content: string): boolean {\n const full = join(home, relPath);\n try {\n if (existsSync(full)) {\n const prev = readFileSync(full, 'utf-8');\n if (prev === content) return false;\n }\n } catch { /* fall through to write */ }\n try { mkdirSync(join(full, '..'), { recursive: true }); } catch { /* exists */ }\n writeFileSync(full, content);\n return true;\n },\n };\n}\n\n// ─── Runner ────────────────────────────────────────────────────────\n\n/**\n * Plan-only: returns the ids of pending migrations without running them.\n * Useful for `titan migrate --dry-run` and the U3 pre-flight check.\n */\nexport function planMigrations(allMigrations: Migration[]): { pending: string[]; alreadyApplied: string[] } {\n const state = loadState();\n const appliedIds = new Set(state.applied.map(a => a.id));\n const sorted = [...allMigrations].sort((a, b) => a.id.localeCompare(b.id));\n const pending: string[] = [];\n const alreadyApplied: string[] = [];\n for (const m of sorted) {\n if (appliedIds.has(m.id)) alreadyApplied.push(m.id);\n else pending.push(m.id);\n }\n return { pending, alreadyApplied };\n}\n\n/**\n * Run all pending migrations in id-ascending order. Stops on the first failure\n * — does NOT continue past a failed migration (otherwise we'd compound errors).\n *\n * The caller (U3 wiring) is responsible for the pre-flight backup and the\n * auto-rollback on failure.\n */\nexport async function runMigrations(allMigrations: Migration[], titanVersion: string): Promise<MigrationRunResult> {\n const state = loadState();\n const appliedIds = new Set(state.applied.map(a => a.id));\n const sorted = [...allMigrations].sort((a, b) => a.id.localeCompare(b.id));\n\n const result: MigrationRunResult = {\n success: true,\n ran: [],\n skipped: [],\n timings: [],\n };\n\n for (const m of sorted) {\n if (appliedIds.has(m.id)) {\n result.skipped.push(m.id);\n result.timings.push({ id: m.id, durationMs: 0, status: 'skip' });\n continue;\n }\n const logBuffer: string[] = [];\n const ctx = makeContext(titanVersion, logBuffer);\n const t0 = Date.now();\n try {\n logger.info(COMPONENT, `→ ${m.id} (${m.version}) ${m.description}`);\n await m.up(ctx);\n const dur = Date.now() - t0;\n state.applied.push({ id: m.id, version: m.version, appliedAt: new Date().toISOString(), durationMs: dur });\n state.lastAppliedId = m.id;\n // Persist after each successful migration so a crash mid-run\n // doesn't redo finished migrations.\n saveState(state);\n result.ran.push(m.id);\n result.timings.push({ id: m.id, durationMs: dur, status: 'ok' });\n if (logBuffer.length > 0) {\n logger.info(COMPONENT, ` ${m.id} log:\\n ${logBuffer.join('\\n ')}`);\n }\n logger.info(COMPONENT, `✓ ${m.id} applied in ${dur}ms`);\n } catch (err) {\n const dur = Date.now() - t0;\n result.success = false;\n result.failed = m.id;\n result.error = (err as Error).message;\n result.timings.push({ id: m.id, durationMs: dur, status: 'fail' });\n logger.error(COMPONENT, `✗ ${m.id} FAILED after ${dur}ms: ${result.error}`);\n if (logBuffer.length > 0) {\n logger.info(COMPONENT, ` ${m.id} partial log:\\n ${logBuffer.join('\\n ')}`);\n }\n return result;\n }\n }\n return result;\n}\n\n/**\n * Run a single migration's `rollback()` if defined. Used by the auto-rollback\n * path in U3/U5 when post-migration smoke checks fail. Failures here are\n * logged but never thrown — at this point we're already in error-recovery\n * and the caller has more authoritative fallback (restore from backup).\n */\nexport async function rollbackMigration(migration: Migration, titanVersion: string): Promise<void> {\n if (!migration.rollback) {\n logger.warn(COMPONENT, `Migration ${migration.id} has no rollback() — relying on backup restore.`);\n return;\n }\n const logBuffer: string[] = [];\n const ctx = makeContext(titanVersion, logBuffer);\n try {\n await migration.rollback(ctx);\n logger.info(COMPONENT, `↩ ${migration.id} rolled back`);\n } catch (err) {\n logger.error(COMPONENT, `↩ ${migration.id} rollback FAILED: ${(err as Error).message}`);\n }\n}\n\n// ─── Test surface ─────────────────────────────────────────────────\n\n/** Test-only: reset the state file. Production code never calls this. */\nexport function __resetStateForTests(): void {\n try { unlinkSync(statePath()); } catch { /* fine */ }\n}\n\n/** Test-only: read state directly. */\nexport function __readStateForTests(): MigrationState {\n return loadState();\n}\n"],"mappings":"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","names":[]}
@@ -2,6 +2,7 @@
2
2
  import { existsSync, readFileSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { homedir } from "os";
5
+ import { shouldUseStubProvider } from "./stub.js";
5
6
  const TITAN_HOME_DEFAULT = join(homedir(), ".titan");
6
7
  const PROVIDERS = [
7
8
  {
@@ -86,6 +87,19 @@ function pickDefaultModel() {
86
87
  provider
87
88
  };
88
89
  }
90
+ if (shouldUseStubProvider()) {
91
+ return {
92
+ model: "stub/echo",
93
+ aliases: {
94
+ fast: "stub/echo",
95
+ smart: "stub/echo",
96
+ cheap: "stub/echo",
97
+ reasoning: "stub/json"
98
+ },
99
+ reason: "CI / TITAN_STUB_PROVIDER set \u2014 using deterministic stub provider (no LLM calls)",
100
+ provider: "stub"
101
+ };
102
+ }
89
103
  for (const candidate of PROVIDERS) {
90
104
  if (hasNonEmptyEnv(candidate.envKey)) {
91
105
  return {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/providers/defaultModel.ts"],"sourcesContent":["/**\n * TITAN — Default Model Picker (v6.0.1)\n *\n * \"Model-agnostic\" promise: TITAN runs with whatever LLM provider the\n * user has credentials for. If they only have an OpenAI key, TITAN picks\n * OpenAI. If only Anthropic, TITAN picks Anthropic. If nothing's keyed,\n * TITAN falls back to a local Ollama default (still works — the package\n * runs end-to-end on a laptop with no cloud creds).\n *\n * Order of preference (highest tier first):\n *\n * 1. `agent.model` in user config — explicit override, always wins.\n * 2. `ANTHROPIC_API_KEY` → claude-sonnet-4 (strongest agent reasoning)\n * 3. `OPENAI_API_KEY` → gpt-4o\n * 4. `GOOGLE_API_KEY` → gemini-2.5-pro\n * 5. `OPENROUTER_API_KEY` → claude-sonnet via openrouter\n * 6. (anything else with `*_API_KEY` set among the 32 OpenAI-compat\n * providers TITAN ships) → use that\n * 7. Local Ollama → `qwen3.5:cloud` (the standing fast default —\n * routes through `ollama serve` on localhost:11434).\n *\n * The picker is pure (no network, no async). It only inspects\n * `process.env` + the user config blob. It's safe to call from the Zod\n * schema's `.default()` thunks AND from runtime resolution paths.\n *\n * On gateway boot we log the pick + the reason so the user knows which\n * provider TITAN selected.\n */\n\nimport { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\n\nconst TITAN_HOME_DEFAULT = join(homedir(), '.titan');\n\n// ── Provider preference table ───────────────────────────────────\n//\n// Order is intentional: stronger reasoning models first, then size,\n// then breadth. We pin to widely-available checkpoints (no preview-only\n// ids) so the default works out of the box for everyone.\n\ninterface ProviderCandidate {\n name: string;\n envKey: string;\n /** The model id we pick when this env key is present. */\n model: string;\n /** Reasonable fast / smart / cheap fallbacks within this provider. */\n aliases: { fast: string; smart: string; cheap: string; reasoning: string };\n}\n\nconst PROVIDERS: ProviderCandidate[] = [\n {\n name: 'anthropic',\n envKey: 'ANTHROPIC_API_KEY',\n model: 'anthropic/claude-sonnet-4-20250514',\n aliases: {\n fast: 'anthropic/claude-haiku-4-20250514',\n smart: 'anthropic/claude-sonnet-4-20250514',\n cheap: 'anthropic/claude-haiku-4-20250514',\n reasoning: 'anthropic/claude-sonnet-4-20250514',\n },\n },\n {\n name: 'openai',\n envKey: 'OPENAI_API_KEY',\n model: 'openai/gpt-4o',\n aliases: {\n fast: 'openai/gpt-4o-mini',\n smart: 'openai/gpt-4o',\n cheap: 'openai/gpt-4o-mini',\n reasoning: 'openai/gpt-4o',\n },\n },\n {\n name: 'google',\n envKey: 'GOOGLE_API_KEY',\n model: 'google/gemini-2.5-pro',\n aliases: {\n fast: 'google/gemini-2.5-flash',\n smart: 'google/gemini-2.5-pro',\n cheap: 'google/gemini-2.5-flash',\n reasoning: 'google/gemini-2.5-pro',\n },\n },\n {\n name: 'openrouter',\n envKey: 'OPENROUTER_API_KEY',\n model: 'openrouter/anthropic/claude-sonnet-4',\n aliases: {\n fast: 'openrouter/anthropic/claude-haiku-4',\n smart: 'openrouter/anthropic/claude-sonnet-4',\n cheap: 'openrouter/anthropic/claude-haiku-4',\n reasoning: 'openrouter/anthropic/claude-sonnet-4',\n },\n },\n];\n\n// ── Ollama-cloud fallback (works without any cloud key) ──────────\n//\n// These are the same defaults we shipped through v5.x. Kept here as the\n// last-resort floor so a user with NO cloud credentials still gets a\n// working agent out of the box (assuming `ollama serve` is up — which\n// the `titan onboard` wizard helps with).\n\nconst OLLAMA_FALLBACK = {\n model: 'ollama/qwen3.5:cloud',\n aliases: {\n fast: 'ollama/qwen3.5:cloud',\n smart: 'ollama/glm-5:cloud',\n cheap: 'ollama/qwen3.5:cloud',\n reasoning: 'ollama/kimi-k2.6:cloud',\n local: 'ollama/qwen3.5:4b',\n cloud: 'ollama/kimi-k2.6:cloud',\n },\n};\n\n// ── Credential probing ──────────────────────────────────────────\n\nfunction hasNonEmptyEnv(key: string): boolean {\n const v = process.env[key];\n return typeof v === 'string' && v.trim().length > 0;\n}\n\n/** Read user config without depending on the full Zod schema (we may be\n * called from inside the schema itself). Returns the raw JSON blob or\n * null. Best-effort — corrupted config returns null. */\nfunction readUserConfigBlob(): Record<string, unknown> | null {\n const titanHome = process.env.TITAN_HOME && process.env.TITAN_HOME.length > 0\n ? (process.env.TITAN_HOME.startsWith('~/') ? join(homedir(), process.env.TITAN_HOME.slice(2)) : process.env.TITAN_HOME)\n : TITAN_HOME_DEFAULT;\n const path = join(titanHome, 'titan.json');\n if (!existsSync(path)) return null;\n try { return JSON.parse(readFileSync(path, 'utf-8')) as Record<string, unknown>; } catch { return null; }\n}\n\n// ── Public picker ───────────────────────────────────────────────\n\nexport interface DefaultModelPick {\n /** The chosen model id (provider/model-name). */\n model: string;\n /** Model aliases for the fast / smart / cheap / reasoning tiers. */\n aliases: Record<string, string>;\n /** Human-readable reason for the pick — printed at gateway boot. */\n reason: string;\n /** Provider name (anthropic / openai / google / openrouter / ollama). */\n provider: string;\n}\n\n/**\n * Decide the default agent model + tier aliases from currently-available\n * credentials. Pure function — only reads `process.env` and (optionally)\n * the on-disk config blob.\n *\n * Returns the same shape regardless of which provider wins so the\n * config schema can plug the pick straight in.\n */\nexport function pickDefaultModel(): DefaultModelPick {\n // 1. Explicit user config wins\n const cfg = readUserConfigBlob();\n const userAgentModel = ((cfg?.agent as Record<string, unknown> | undefined)?.model);\n if (typeof userAgentModel === 'string' && userAgentModel.trim().length > 0) {\n const provider = userAgentModel.split('/')[0] || 'unknown';\n return {\n model: userAgentModel,\n aliases: defaultAliasesForProvider(provider),\n reason: `user config (agent.model=${userAgentModel})`,\n provider,\n };\n }\n\n // 2. Walk providers in preference order; first env key wins.\n for (const candidate of PROVIDERS) {\n if (hasNonEmptyEnv(candidate.envKey)) {\n return {\n model: candidate.model,\n aliases: candidate.aliases,\n reason: `${candidate.envKey} is set — picked ${candidate.name}`,\n provider: candidate.name,\n };\n }\n }\n\n // 3. Catch-all: any of the 32 OpenAI-compat providers with a key set.\n // Conservative — we only auto-pick when the key pattern is\n // unmistakable (`*_API_KEY` or known names like GROQ_API_KEY).\n const compatHits = Object.keys(process.env)\n .filter(k => k.endsWith('_API_KEY') && (process.env[k] ?? '').trim().length > 0)\n .filter(k => !['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GOOGLE_API_KEY', 'OPENROUTER_API_KEY'].includes(k));\n if (compatHits.length > 0) {\n // Pick the first one alphabetically for determinism. The user can\n // always override via config — this is just the bootstrap default.\n const providerName = compatHits[0].replace('_API_KEY', '').toLowerCase();\n // For these we don't know the exact model id; reuse Ollama\n // fallback aliases but flag the provider so the user sees the\n // log line and can correct it in config.\n return {\n model: OLLAMA_FALLBACK.model,\n aliases: OLLAMA_FALLBACK.aliases,\n reason: `detected ${compatHits[0]} but no first-class adapter mapping; falling back to Ollama defaults — override agent.model in config to use ${providerName}`,\n provider: 'ollama',\n };\n }\n\n // 4. Final fallback — local Ollama. Works on a clean laptop with\n // `ollama serve` running and the default models pulled.\n return {\n model: OLLAMA_FALLBACK.model,\n aliases: OLLAMA_FALLBACK.aliases,\n reason: 'no cloud API keys detected — defaulting to local Ollama (set ANTHROPIC_API_KEY / OPENAI_API_KEY / GOOGLE_API_KEY to switch providers)',\n provider: 'ollama',\n };\n}\n\nfunction defaultAliasesForProvider(provider: string): Record<string, string> {\n const match = PROVIDERS.find(p => p.name === provider);\n if (match) return match.aliases;\n return OLLAMA_FALLBACK.aliases;\n}\n\n/**\n * Resolve the default agent model. Thin wrapper used by the schema's\n * `.default()` thunk. Stable across calls within a process (env doesn't\n * change at runtime), so the schema's default is consistent.\n */\nexport function getDefaultModelId(): string {\n return pickDefaultModel().model;\n}\n\n/**\n * Resolve the default tier-alias map. Used by the schema's modelAliases\n * default. Merges with the Ollama floor so any tier name not provided\n * by the chosen provider still resolves to something working.\n */\nexport function getDefaultModelAliases(): Record<string, string> {\n const pick = pickDefaultModel();\n return {\n // Ollama floor — any tier the chosen provider doesn't define\n // still resolves to a working model.\n ...OLLAMA_FALLBACK.aliases,\n // Chosen-provider aliases win for fast / smart / cheap /\n // reasoning. `local` + `cloud` keys stay on the Ollama floor.\n ...pick.aliases,\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/providers/defaultModel.ts"],"sourcesContent":["/**\n * TITAN — Default Model Picker (v6.0.1)\n *\n * \"Model-agnostic\" promise: TITAN runs with whatever LLM provider the\n * user has credentials for. If they only have an OpenAI key, TITAN picks\n * OpenAI. If only Anthropic, TITAN picks Anthropic. If nothing's keyed,\n * TITAN falls back to a local Ollama default (still works — the package\n * runs end-to-end on a laptop with no cloud creds).\n *\n * Order of preference (highest tier first):\n *\n * 1. `agent.model` in user config — explicit override, always wins.\n * 2. `ANTHROPIC_API_KEY` → claude-sonnet-4 (strongest agent reasoning)\n * 3. `OPENAI_API_KEY` → gpt-4o\n * 4. `GOOGLE_API_KEY` → gemini-2.5-pro\n * 5. `OPENROUTER_API_KEY` → claude-sonnet via openrouter\n * 6. (anything else with `*_API_KEY` set among the 32 OpenAI-compat\n * providers TITAN ships) → use that\n * 7. Local Ollama → `qwen3.5:cloud` (the standing fast default —\n * routes through `ollama serve` on localhost:11434).\n *\n * The picker is pure (no network, no async). It only inspects\n * `process.env` + the user config blob. It's safe to call from the Zod\n * schema's `.default()` thunks AND from runtime resolution paths.\n *\n * On gateway boot we log the pick + the reason so the user knows which\n * provider TITAN selected.\n */\n\nimport { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { shouldUseStubProvider } from './stub.js';\n\nconst TITAN_HOME_DEFAULT = join(homedir(), '.titan');\n\n// ── Provider preference table ───────────────────────────────────\n//\n// Order is intentional: stronger reasoning models first, then size,\n// then breadth. We pin to widely-available checkpoints (no preview-only\n// ids) so the default works out of the box for everyone.\n\ninterface ProviderCandidate {\n name: string;\n envKey: string;\n /** The model id we pick when this env key is present. */\n model: string;\n /** Reasonable fast / smart / cheap fallbacks within this provider. */\n aliases: { fast: string; smart: string; cheap: string; reasoning: string };\n}\n\nconst PROVIDERS: ProviderCandidate[] = [\n {\n name: 'anthropic',\n envKey: 'ANTHROPIC_API_KEY',\n model: 'anthropic/claude-sonnet-4-20250514',\n aliases: {\n fast: 'anthropic/claude-haiku-4-20250514',\n smart: 'anthropic/claude-sonnet-4-20250514',\n cheap: 'anthropic/claude-haiku-4-20250514',\n reasoning: 'anthropic/claude-sonnet-4-20250514',\n },\n },\n {\n name: 'openai',\n envKey: 'OPENAI_API_KEY',\n model: 'openai/gpt-4o',\n aliases: {\n fast: 'openai/gpt-4o-mini',\n smart: 'openai/gpt-4o',\n cheap: 'openai/gpt-4o-mini',\n reasoning: 'openai/gpt-4o',\n },\n },\n {\n name: 'google',\n envKey: 'GOOGLE_API_KEY',\n model: 'google/gemini-2.5-pro',\n aliases: {\n fast: 'google/gemini-2.5-flash',\n smart: 'google/gemini-2.5-pro',\n cheap: 'google/gemini-2.5-flash',\n reasoning: 'google/gemini-2.5-pro',\n },\n },\n {\n name: 'openrouter',\n envKey: 'OPENROUTER_API_KEY',\n model: 'openrouter/anthropic/claude-sonnet-4',\n aliases: {\n fast: 'openrouter/anthropic/claude-haiku-4',\n smart: 'openrouter/anthropic/claude-sonnet-4',\n cheap: 'openrouter/anthropic/claude-haiku-4',\n reasoning: 'openrouter/anthropic/claude-sonnet-4',\n },\n },\n];\n\n// ── Ollama-cloud fallback (works without any cloud key) ──────────\n//\n// These are the same defaults we shipped through v5.x. Kept here as the\n// last-resort floor so a user with NO cloud credentials still gets a\n// working agent out of the box (assuming `ollama serve` is up — which\n// the `titan onboard` wizard helps with).\n\nconst OLLAMA_FALLBACK = {\n model: 'ollama/qwen3.5:cloud',\n aliases: {\n fast: 'ollama/qwen3.5:cloud',\n smart: 'ollama/glm-5:cloud',\n cheap: 'ollama/qwen3.5:cloud',\n reasoning: 'ollama/kimi-k2.6:cloud',\n local: 'ollama/qwen3.5:4b',\n cloud: 'ollama/kimi-k2.6:cloud',\n },\n};\n\n// ── Credential probing ──────────────────────────────────────────\n\nfunction hasNonEmptyEnv(key: string): boolean {\n const v = process.env[key];\n return typeof v === 'string' && v.trim().length > 0;\n}\n\n/** Read user config without depending on the full Zod schema (we may be\n * called from inside the schema itself). Returns the raw JSON blob or\n * null. Best-effort — corrupted config returns null. */\nfunction readUserConfigBlob(): Record<string, unknown> | null {\n const titanHome = process.env.TITAN_HOME && process.env.TITAN_HOME.length > 0\n ? (process.env.TITAN_HOME.startsWith('~/') ? join(homedir(), process.env.TITAN_HOME.slice(2)) : process.env.TITAN_HOME)\n : TITAN_HOME_DEFAULT;\n const path = join(titanHome, 'titan.json');\n if (!existsSync(path)) return null;\n try { return JSON.parse(readFileSync(path, 'utf-8')) as Record<string, unknown>; } catch { return null; }\n}\n\n// ── Public picker ───────────────────────────────────────────────\n\nexport interface DefaultModelPick {\n /** The chosen model id (provider/model-name). */\n model: string;\n /** Model aliases for the fast / smart / cheap / reasoning tiers. */\n aliases: Record<string, string>;\n /** Human-readable reason for the pick — printed at gateway boot. */\n reason: string;\n /** Provider name (anthropic / openai / google / openrouter / ollama). */\n provider: string;\n}\n\n/**\n * Decide the default agent model + tier aliases from currently-available\n * credentials. Pure function — only reads `process.env` and (optionally)\n * the on-disk config blob.\n *\n * Returns the same shape regardless of which provider wins so the\n * config schema can plug the pick straight in.\n */\nexport function pickDefaultModel(): DefaultModelPick {\n // 1. Explicit user config wins\n const cfg = readUserConfigBlob();\n const userAgentModel = ((cfg?.agent as Record<string, unknown> | undefined)?.model);\n if (typeof userAgentModel === 'string' && userAgentModel.trim().length > 0) {\n const provider = userAgentModel.split('/')[0] || 'unknown';\n return {\n model: userAgentModel,\n aliases: defaultAliasesForProvider(provider),\n reason: `user config (agent.model=${userAgentModel})`,\n provider,\n };\n }\n\n // 1b. beta.11 — CI stub fallback. When TITAN_STUB_PROVIDER=1 OR\n // when we're in CI without any real provider key, route everything\n // to the deterministic stub so Eval Gate runs can score. Slotted\n // BEFORE the cloud-key checks so an accidental leftover key on a\n // CI runner doesn't suddenly start spending real money. The\n // explicit user-config branch above still beats the stub.\n if (shouldUseStubProvider()) {\n return {\n model: 'stub/echo',\n aliases: {\n fast: 'stub/echo',\n smart: 'stub/echo',\n cheap: 'stub/echo',\n reasoning: 'stub/json',\n },\n reason: 'CI / TITAN_STUB_PROVIDER set — using deterministic stub provider (no LLM calls)',\n provider: 'stub',\n };\n }\n\n // 2. Walk providers in preference order; first env key wins.\n for (const candidate of PROVIDERS) {\n if (hasNonEmptyEnv(candidate.envKey)) {\n return {\n model: candidate.model,\n aliases: candidate.aliases,\n reason: `${candidate.envKey} is set — picked ${candidate.name}`,\n provider: candidate.name,\n };\n }\n }\n\n // 3. Catch-all: any of the 32 OpenAI-compat providers with a key set.\n // Conservative — we only auto-pick when the key pattern is\n // unmistakable (`*_API_KEY` or known names like GROQ_API_KEY).\n const compatHits = Object.keys(process.env)\n .filter(k => k.endsWith('_API_KEY') && (process.env[k] ?? '').trim().length > 0)\n .filter(k => !['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GOOGLE_API_KEY', 'OPENROUTER_API_KEY'].includes(k));\n if (compatHits.length > 0) {\n // Pick the first one alphabetically for determinism. The user can\n // always override via config — this is just the bootstrap default.\n const providerName = compatHits[0].replace('_API_KEY', '').toLowerCase();\n // For these we don't know the exact model id; reuse Ollama\n // fallback aliases but flag the provider so the user sees the\n // log line and can correct it in config.\n return {\n model: OLLAMA_FALLBACK.model,\n aliases: OLLAMA_FALLBACK.aliases,\n reason: `detected ${compatHits[0]} but no first-class adapter mapping; falling back to Ollama defaults — override agent.model in config to use ${providerName}`,\n provider: 'ollama',\n };\n }\n\n // 4. Final fallback — local Ollama. Works on a clean laptop with\n // `ollama serve` running and the default models pulled.\n return {\n model: OLLAMA_FALLBACK.model,\n aliases: OLLAMA_FALLBACK.aliases,\n reason: 'no cloud API keys detected — defaulting to local Ollama (set ANTHROPIC_API_KEY / OPENAI_API_KEY / GOOGLE_API_KEY to switch providers)',\n provider: 'ollama',\n };\n}\n\nfunction defaultAliasesForProvider(provider: string): Record<string, string> {\n const match = PROVIDERS.find(p => p.name === provider);\n if (match) return match.aliases;\n return OLLAMA_FALLBACK.aliases;\n}\n\n/**\n * Resolve the default agent model. Thin wrapper used by the schema's\n * `.default()` thunk. Stable across calls within a process (env doesn't\n * change at runtime), so the schema's default is consistent.\n */\nexport function getDefaultModelId(): string {\n return pickDefaultModel().model;\n}\n\n/**\n * Resolve the default tier-alias map. Used by the schema's modelAliases\n * default. Merges with the Ollama floor so any tier name not provided\n * by the chosen provider still resolves to something working.\n */\nexport function getDefaultModelAliases(): Record<string, string> {\n const pick = pickDefaultModel();\n return {\n // Ollama floor — any tier the chosen provider doesn't define\n // still resolves to a working model.\n ...OLLAMA_FALLBACK.aliases,\n // Chosen-provider aliases win for fast / smart / cheap /\n // reasoning. `local` + `cloud` keys stay on the Ollama floor.\n ...pick.aliases,\n };\n}\n"],"mappings":"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","names":[]}
@@ -6,6 +6,7 @@ import { GoogleProvider } from "./google.js";
6
6
  import { OllamaProvider } from "./ollama.js";
7
7
  import { ClaudeCodeProvider } from "./claudeCode.js";
8
8
  import { OpenAICompatProvider, PROVIDER_PRESETS } from "./openai_compat.js";
9
+ import { StubProvider, shouldUseStubProvider } from "./stub.js";
9
10
  import { loadConfig } from "../config/config.js";
10
11
  import logger from "../utils/logger.js";
11
12
  import { findModelOnMesh } from "../mesh/registry.js";
@@ -102,6 +103,9 @@ function initProviders() {
102
103
  providers.set("google", new GoogleProvider());
103
104
  providers.set("ollama", new OllamaProvider());
104
105
  providers.set("claude-code", new ClaudeCodeProvider());
106
+ if (shouldUseStubProvider()) {
107
+ providers.set("stub", new StubProvider());
108
+ }
105
109
  for (const preset of PROVIDER_PRESETS) {
106
110
  providers.set(preset.name, new OpenAICompatProvider(preset));
107
111
  }