charness 3.3.2 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (277) hide show
  1. package/CHANGELOG.md +639 -0
  2. package/DEVELOPMENT.md +76 -0
  3. package/README.md +78 -7
  4. package/SETUP.md +65 -19
  5. package/dist/adapters/claudeCliAdapter.js +2 -2
  6. package/dist/adapters/claudeCliAdapter.js.map +1 -1
  7. package/dist/adapters/claudeConfigDir.d.ts +18 -0
  8. package/dist/adapters/claudeConfigDir.d.ts.map +1 -1
  9. package/dist/adapters/claudeJsonStreamAdapter.d.ts +8 -1
  10. package/dist/adapters/claudeJsonStreamAdapter.d.ts.map +1 -1
  11. package/dist/adapters/claudeJsonStreamAdapter.js +24 -6
  12. package/dist/adapters/claudeJsonStreamAdapter.js.map +1 -1
  13. package/dist/adapters/createAdapter.d.ts +2 -1
  14. package/dist/adapters/createAdapter.d.ts.map +1 -1
  15. package/dist/adapters/createAdapter.js +16 -0
  16. package/dist/adapters/createAdapter.js.map +1 -1
  17. package/dist/adapters/openCodeAdapter.d.ts +11 -0
  18. package/dist/adapters/openCodeAdapter.d.ts.map +1 -1
  19. package/dist/adapters/openCodeAdapter.js +24 -9
  20. package/dist/adapters/openCodeAdapter.js.map +1 -1
  21. package/dist/agentLogin/agentLogin.d.ts +19 -1
  22. package/dist/agentLogin/agentLogin.d.ts.map +1 -1
  23. package/dist/agentLogin/agentLogin.js +64 -18
  24. package/dist/agentLogin/agentLogin.js.map +1 -1
  25. package/dist/apiErrorRetry.d.ts +3 -1
  26. package/dist/apiErrorRetry.d.ts.map +1 -1
  27. package/dist/apiErrorRetry.js +11 -1
  28. package/dist/apiErrorRetry.js.map +1 -1
  29. package/dist/bot.d.ts +0 -3
  30. package/dist/bot.d.ts.map +1 -1
  31. package/dist/bot.js +487 -182
  32. package/dist/bot.js.map +1 -1
  33. package/dist/changelogEntries.d.ts +25 -0
  34. package/dist/changelogEntries.d.ts.map +1 -0
  35. package/dist/changelogEntries.js +66 -0
  36. package/dist/changelogEntries.js.map +1 -0
  37. package/dist/cli/bot.d.ts +6 -2
  38. package/dist/cli/bot.d.ts.map +1 -1
  39. package/dist/cli/bot.js +34 -2
  40. package/dist/cli/bot.js.map +1 -1
  41. package/dist/cli/claudeProfiles.d.ts +1 -1
  42. package/dist/cli/claudeProfiles.d.ts.map +1 -1
  43. package/dist/cli/claudeProfiles.js +11 -6
  44. package/dist/cli/claudeProfiles.js.map +1 -1
  45. package/dist/cli/hot.d.ts +7 -0
  46. package/dist/cli/hot.d.ts.map +1 -1
  47. package/dist/cli/hot.js +53 -3
  48. package/dist/cli/hot.js.map +1 -1
  49. package/dist/cli.d.ts +1 -1
  50. package/dist/cli.d.ts.map +1 -1
  51. package/dist/cli.js +8 -0
  52. package/dist/cli.js.map +1 -1
  53. package/dist/connectors/telegram/bindPicker.d.ts +64 -0
  54. package/dist/connectors/telegram/bindPicker.d.ts.map +1 -0
  55. package/dist/connectors/telegram/bindPicker.js +97 -0
  56. package/dist/connectors/telegram/bindPicker.js.map +1 -0
  57. package/dist/connectors/telegram/commands/claudeAccounts.d.ts +6 -2
  58. package/dist/connectors/telegram/commands/claudeAccounts.d.ts.map +1 -1
  59. package/dist/connectors/telegram/commands/claudeAccounts.js +127 -28
  60. package/dist/connectors/telegram/commands/claudeAccounts.js.map +1 -1
  61. package/dist/connectors/telegram/commands/fallbackChain.d.ts.map +1 -1
  62. package/dist/connectors/telegram/commands/fallbackChain.js +6 -2
  63. package/dist/connectors/telegram/commands/fallbackChain.js.map +1 -1
  64. package/dist/connectors/telegram/commands/selfUpdate.d.ts +16 -0
  65. package/dist/connectors/telegram/commands/selfUpdate.d.ts.map +1 -0
  66. package/dist/connectors/telegram/commands/selfUpdate.js +210 -0
  67. package/dist/connectors/telegram/commands/selfUpdate.js.map +1 -0
  68. package/dist/connectors/telegram/commands/stt.d.ts +1 -0
  69. package/dist/connectors/telegram/commands/stt.d.ts.map +1 -1
  70. package/dist/connectors/telegram/commands/stt.js +5 -0
  71. package/dist/connectors/telegram/commands/stt.js.map +1 -1
  72. package/dist/connectors/telegram/commands/topicCommand.d.ts +75 -0
  73. package/dist/connectors/telegram/commands/topicCommand.d.ts.map +1 -0
  74. package/dist/connectors/telegram/commands/topicCommand.js +391 -0
  75. package/dist/connectors/telegram/commands/topicCommand.js.map +1 -0
  76. package/dist/i18n/de.d.ts.map +1 -1
  77. package/dist/i18n/de.js +136 -3
  78. package/dist/i18n/de.js.map +1 -1
  79. package/dist/i18n/en.d.ts.map +1 -1
  80. package/dist/i18n/en.js +136 -3
  81. package/dist/i18n/en.js.map +1 -1
  82. package/dist/i18n/es.d.ts.map +1 -1
  83. package/dist/i18n/es.js +136 -3
  84. package/dist/i18n/es.js.map +1 -1
  85. package/dist/i18n/fr.d.ts.map +1 -1
  86. package/dist/i18n/fr.js +136 -3
  87. package/dist/i18n/fr.js.map +1 -1
  88. package/dist/i18n/hi.d.ts.map +1 -1
  89. package/dist/i18n/hi.js +136 -3
  90. package/dist/i18n/hi.js.map +1 -1
  91. package/dist/i18n/ja.d.ts.map +1 -1
  92. package/dist/i18n/ja.js +136 -3
  93. package/dist/i18n/ja.js.map +1 -1
  94. package/dist/i18n/ka.d.ts.map +1 -1
  95. package/dist/i18n/ka.js +136 -3
  96. package/dist/i18n/ka.js.map +1 -1
  97. package/dist/i18n/pt.d.ts.map +1 -1
  98. package/dist/i18n/pt.js +136 -3
  99. package/dist/i18n/pt.js.map +1 -1
  100. package/dist/i18n/ru.d.ts.map +1 -1
  101. package/dist/i18n/ru.js +136 -3
  102. package/dist/i18n/ru.js.map +1 -1
  103. package/dist/i18n/uk.d.ts.map +1 -1
  104. package/dist/i18n/uk.js +136 -3
  105. package/dist/i18n/uk.js.map +1 -1
  106. package/dist/i18n/uz.d.ts.map +1 -1
  107. package/dist/i18n/uz.js +136 -3
  108. package/dist/i18n/uz.js.map +1 -1
  109. package/dist/i18n/zh.d.ts.map +1 -1
  110. package/dist/i18n/zh.js +136 -3
  111. package/dist/i18n/zh.js.map +1 -1
  112. package/dist/instanceSettings.d.ts +12 -1
  113. package/dist/instanceSettings.d.ts.map +1 -1
  114. package/dist/instanceSettings.js +40 -4
  115. package/dist/instanceSettings.js.map +1 -1
  116. package/dist/scheduler/mcpSurface.d.ts.map +1 -1
  117. package/dist/scheduler/mcpSurface.js +4 -2
  118. package/dist/scheduler/mcpSurface.js.map +1 -1
  119. package/dist/selfUpdate/bootRollback.d.ts +22 -0
  120. package/dist/selfUpdate/bootRollback.d.ts.map +1 -0
  121. package/dist/selfUpdate/bootRollback.js +79 -0
  122. package/dist/selfUpdate/bootRollback.js.map +1 -0
  123. package/dist/selfUpdate/commandRunner.d.ts +21 -0
  124. package/dist/selfUpdate/commandRunner.d.ts.map +1 -0
  125. package/dist/selfUpdate/commandRunner.js +32 -0
  126. package/dist/selfUpdate/commandRunner.js.map +1 -0
  127. package/dist/selfUpdate/constants.d.ts +26 -0
  128. package/dist/selfUpdate/constants.d.ts.map +1 -0
  129. package/dist/selfUpdate/constants.js +31 -0
  130. package/dist/selfUpdate/constants.js.map +1 -0
  131. package/dist/selfUpdate/gitSource.d.ts +58 -0
  132. package/dist/selfUpdate/gitSource.d.ts.map +1 -0
  133. package/dist/selfUpdate/gitSource.js +183 -0
  134. package/dist/selfUpdate/gitSource.js.map +1 -0
  135. package/dist/selfUpdate/hostLock.d.ts +16 -0
  136. package/dist/selfUpdate/hostLock.d.ts.map +1 -0
  137. package/dist/selfUpdate/hostLock.js +101 -0
  138. package/dist/selfUpdate/hostLock.js.map +1 -0
  139. package/dist/selfUpdate/hotSupervisor.d.ts +47 -0
  140. package/dist/selfUpdate/hotSupervisor.d.ts.map +1 -0
  141. package/dist/selfUpdate/hotSupervisor.js +125 -0
  142. package/dist/selfUpdate/hotSupervisor.js.map +1 -0
  143. package/dist/selfUpdate/installKind.d.ts +45 -0
  144. package/dist/selfUpdate/installKind.d.ts.map +1 -0
  145. package/dist/selfUpdate/installKind.js +99 -0
  146. package/dist/selfUpdate/installKind.js.map +1 -0
  147. package/dist/selfUpdate/launchEnvironment.d.ts +3 -0
  148. package/dist/selfUpdate/launchEnvironment.d.ts.map +1 -0
  149. package/dist/selfUpdate/launchEnvironment.js +16 -0
  150. package/dist/selfUpdate/launchEnvironment.js.map +1 -0
  151. package/dist/selfUpdate/npmSource.d.ts +34 -0
  152. package/dist/selfUpdate/npmSource.d.ts.map +1 -0
  153. package/dist/selfUpdate/npmSource.js +107 -0
  154. package/dist/selfUpdate/npmSource.js.map +1 -0
  155. package/dist/selfUpdate/releaseNotes.d.ts +33 -0
  156. package/dist/selfUpdate/releaseNotes.d.ts.map +1 -0
  157. package/dist/selfUpdate/releaseNotes.js +115 -0
  158. package/dist/selfUpdate/releaseNotes.js.map +1 -0
  159. package/dist/selfUpdate/restart.d.ts +33 -0
  160. package/dist/selfUpdate/restart.d.ts.map +1 -0
  161. package/dist/selfUpdate/restart.js +94 -0
  162. package/dist/selfUpdate/restart.js.map +1 -0
  163. package/dist/selfUpdate/selfCheck.d.ts +20 -0
  164. package/dist/selfUpdate/selfCheck.d.ts.map +1 -0
  165. package/dist/selfUpdate/selfCheck.js +126 -0
  166. package/dist/selfUpdate/selfCheck.js.map +1 -0
  167. package/dist/selfUpdate/semver.d.ts +24 -0
  168. package/dist/selfUpdate/semver.d.ts.map +1 -0
  169. package/dist/selfUpdate/semver.js +165 -0
  170. package/dist/selfUpdate/semver.js.map +1 -0
  171. package/dist/selfUpdate/types.d.ts +51 -0
  172. package/dist/selfUpdate/types.d.ts.map +1 -0
  173. package/dist/selfUpdate/types.js +3 -0
  174. package/dist/selfUpdate/types.js.map +1 -0
  175. package/dist/selfUpdate/updateEngine.d.ts +110 -0
  176. package/dist/selfUpdate/updateEngine.d.ts.map +1 -0
  177. package/dist/selfUpdate/updateEngine.js +727 -0
  178. package/dist/selfUpdate/updateEngine.js.map +1 -0
  179. package/dist/selfUpdate/updateMessages.d.ts +84 -0
  180. package/dist/selfUpdate/updateMessages.d.ts.map +1 -0
  181. package/dist/selfUpdate/updateMessages.js +115 -0
  182. package/dist/selfUpdate/updateMessages.js.map +1 -0
  183. package/dist/selfUpdate/updateModes.d.ts +17 -0
  184. package/dist/selfUpdate/updateModes.d.ts.map +1 -0
  185. package/dist/selfUpdate/updateModes.js +14 -0
  186. package/dist/selfUpdate/updateModes.js.map +1 -0
  187. package/dist/selfUpdate/updatePending.d.ts +76 -0
  188. package/dist/selfUpdate/updatePending.d.ts.map +1 -0
  189. package/dist/selfUpdate/updatePending.js +131 -0
  190. package/dist/selfUpdate/updatePending.js.map +1 -0
  191. package/dist/selfUpdate/updatePolicy.d.ts +115 -0
  192. package/dist/selfUpdate/updatePolicy.d.ts.map +1 -0
  193. package/dist/selfUpdate/updatePolicy.js +83 -0
  194. package/dist/selfUpdate/updatePolicy.js.map +1 -0
  195. package/dist/selfUpdate/updateRuntime.d.ts +22 -0
  196. package/dist/selfUpdate/updateRuntime.d.ts.map +1 -0
  197. package/dist/selfUpdate/updateRuntime.js +148 -0
  198. package/dist/selfUpdate/updateRuntime.js.map +1 -0
  199. package/dist/startupPromptBuffer.d.ts +2 -0
  200. package/dist/startupPromptBuffer.d.ts.map +1 -1
  201. package/dist/startupPromptBuffer.js +4 -0
  202. package/dist/startupPromptBuffer.js.map +1 -1
  203. package/dist/state.d.ts +43 -0
  204. package/dist/state.d.ts.map +1 -1
  205. package/dist/state.js +54 -0
  206. package/dist/state.js.map +1 -1
  207. package/dist/types.d.ts +10 -0
  208. package/dist/types.d.ts.map +1 -1
  209. package/dist/utils/anthropicApiKeyCheck.d.ts +19 -0
  210. package/dist/utils/anthropicApiKeyCheck.d.ts.map +1 -0
  211. package/dist/utils/anthropicApiKeyCheck.js +41 -0
  212. package/dist/utils/anthropicApiKeyCheck.js.map +1 -0
  213. package/dist/utils/claudeAccountPickerPlan.d.ts +6 -1
  214. package/dist/utils/claudeAccountPickerPlan.d.ts.map +1 -1
  215. package/dist/utils/claudeAccountPickerPlan.js +5 -1
  216. package/dist/utils/claudeAccountPickerPlan.js.map +1 -1
  217. package/dist/utils/claudeApiKeyHelper.d.ts +19 -0
  218. package/dist/utils/claudeApiKeyHelper.d.ts.map +1 -0
  219. package/dist/utils/claudeApiKeyHelper.js +32 -0
  220. package/dist/utils/claudeApiKeyHelper.js.map +1 -0
  221. package/dist/utils/claudeApiSpend.d.ts +73 -0
  222. package/dist/utils/claudeApiSpend.d.ts.map +1 -0
  223. package/dist/utils/claudeApiSpend.js +82 -0
  224. package/dist/utils/claudeApiSpend.js.map +1 -0
  225. package/dist/utils/claudeCompactHook.d.ts +11 -6
  226. package/dist/utils/claudeCompactHook.d.ts.map +1 -1
  227. package/dist/utils/claudeCompactHook.js +28 -11
  228. package/dist/utils/claudeCompactHook.js.map +1 -1
  229. package/dist/utils/claudeLimitSwitch.d.ts +2 -1
  230. package/dist/utils/claudeLimitSwitch.d.ts.map +1 -1
  231. package/dist/utils/claudeLimitSwitch.js +3 -2
  232. package/dist/utils/claudeLimitSwitch.js.map +1 -1
  233. package/dist/utils/claudeProfileUsageLines.d.ts +18 -3
  234. package/dist/utils/claudeProfileUsageLines.d.ts.map +1 -1
  235. package/dist/utils/claudeProfileUsageLines.js +23 -2
  236. package/dist/utils/claudeProfileUsageLines.js.map +1 -1
  237. package/dist/utils/claudeProfiles.d.ts +57 -2
  238. package/dist/utils/claudeProfiles.d.ts.map +1 -1
  239. package/dist/utils/claudeProfiles.js +96 -3
  240. package/dist/utils/claudeProfiles.js.map +1 -1
  241. package/dist/utils/claudeStreamJson.d.ts +4 -1
  242. package/dist/utils/claudeStreamJson.d.ts.map +1 -1
  243. package/dist/utils/claudeStreamJson.js +6 -0
  244. package/dist/utils/claudeStreamJson.js.map +1 -1
  245. package/dist/utils/claudeTranscriptContent.d.ts +8 -0
  246. package/dist/utils/claudeTranscriptContent.d.ts.map +1 -1
  247. package/dist/utils/claudeTranscriptContent.js +44 -0
  248. package/dist/utils/claudeTranscriptContent.js.map +1 -1
  249. package/dist/utils/claudeUsage.d.ts +22 -6
  250. package/dist/utils/claudeUsage.d.ts.map +1 -1
  251. package/dist/utils/claudeUsage.js +12 -5
  252. package/dist/utils/claudeUsage.js.map +1 -1
  253. package/dist/utils/claudeUsageRefresher.d.ts +6 -0
  254. package/dist/utils/claudeUsageRefresher.d.ts.map +1 -1
  255. package/dist/utils/claudeUsageRefresher.js +2 -2
  256. package/dist/utils/claudeUsageRefresher.js.map +1 -1
  257. package/dist/utils/fallbackChain.d.ts +15 -8
  258. package/dist/utils/fallbackChain.d.ts.map +1 -1
  259. package/dist/utils/fallbackChain.js +30 -13
  260. package/dist/utils/fallbackChain.js.map +1 -1
  261. package/dist/utils/limitsBoardPlan.d.ts +4 -2
  262. package/dist/utils/limitsBoardPlan.d.ts.map +1 -1
  263. package/dist/utils/limitsBoardPlan.js +12 -5
  264. package/dist/utils/limitsBoardPlan.js.map +1 -1
  265. package/dist/utils/recvPreviewRedaction.d.ts +7 -2
  266. package/dist/utils/recvPreviewRedaction.d.ts.map +1 -1
  267. package/dist/utils/recvPreviewRedaction.js +10 -4
  268. package/dist/utils/recvPreviewRedaction.js.map +1 -1
  269. package/dist/utils/threadStatusReport.d.ts +12 -1
  270. package/dist/utils/threadStatusReport.d.ts.map +1 -1
  271. package/dist/utils/threadStatusReport.js +13 -4
  272. package/dist/utils/threadStatusReport.js.map +1 -1
  273. package/dist/utils/updateScreenPlan.d.ts +38 -0
  274. package/dist/utils/updateScreenPlan.d.ts.map +1 -0
  275. package/dist/utils/updateScreenPlan.js +143 -0
  276. package/dist/utils/updateScreenPlan.js.map +1 -0
  277. package/package.json +3 -1
package/DEVELOPMENT.md CHANGED
@@ -89,6 +89,30 @@ owner = the `charness hot` supervisor (a worker reload keeps the sidecar) or the
89
89
  the sidecar polls its owner and exits within ~2 s of its death, even after a SIGKILL.
90
90
  ```
91
91
 
92
+ ### Self-update (the bot updates itself)
93
+
94
+ `src/selfUpdate/` (its README has the contracts) is an engine over ports: `updatePolicy.ts` decides, `updateEngine.ts` runs
95
+ the hourly check, the idle loop and the boot step against fakes in tests, `updateRuntime.ts` binds it to this process
96
+ (install kind, npm / git sources, host lock), `bot.ts` supplies the settings, the idle verdict and General. Three facts to
97
+ keep when touching the restart path:
98
+
99
+ - **A restart in place is `process.execve`** (the same pid) of `node <packageRoot>/dist/cli.js <original arguments>` with
100
+ the environment as it was at launch — `cli.ts`'s FIRST import snapshots it, so an edited `.env` is read afresh. Nothing
101
+ may be imported before `launchEnvironment`.
102
+ - **The hot supervisor re-executes itself** when a file it loaded changed: at start it writes `DATA_DIR/hot-supervisor.json`
103
+ (pid, start token, hash of every loaded `dist/` module); a new worker compares it with `dist/` and, at its idle point,
104
+ sends `SIGUSR2` — only when pid and start token match. The supervisor stops `tsc -w` and nodemon, leaves the OpenCode
105
+ server and the speech sidecar alone and `execve`s `cli.js hot`. A supervisor that predates this has no record and is
106
+ never signalled (an unhandled `SIGUSR2` kills a process).
107
+ - **`update-pending.json` is written BEFORE the switch**; every boot counts itself in (`cli/bot.ts`, once the instance lock is held and before the bot
108
+ module is imported), the boot after two unconfirmed ones rolls back, the running bot confirms 60 s after launch.
109
+
110
+ The flows run it for real: `selfUpdateNpmE2e` (a loopback registry — `e2e/fakeNpmRegistry.ts` — and a real global install into
111
+ a temp prefix), `selfUpdateCheckoutE2e` (a checkout copy, a bare remote and a second clone — `e2e/checkoutFixture.ts`),
112
+ `selfUpdateHotE2e` (the real `charness hot`), `selfUpdateEntrypointE2e` (the image's own `docker/entrypoint.sh` and
113
+ `restart-bot.sh` with their paths pointed into a temp folder — the one thing it cannot run is the container itself). The
114
+ harness writes `settings.json` with `autoUpdate: off` for every instance, so no flow updates the checkout it runs from.
115
+
92
116
  ## Local development
93
117
 
94
118
  ```bash
@@ -129,3 +153,55 @@ docker compose down charness && docker compose up -d charness # examples/docke
129
153
  docker compose logs -f charness # tail logs
130
154
  docker exec charness-main charness-restart-bot # restart only the bot; agents keep running
131
155
  ```
156
+
157
+ ## Releases
158
+
159
+ Two workflows. **`.github/workflows/test.yml` (`Tests`)** runs typecheck, build and the FULL `yarn test` on every push to
160
+ `main`. **`.github/workflows/publish.yml`** releases charness to npm without anyone doing anything: every day at 09:17
161
+ (Asia/Tbilisi) it decides by itself: nothing shipped changed → a green run, no tag, no publish; otherwise it checks the tests
162
+ of the commit it would release (below), checks the package, commits `chore(release): X.Y.Z`, tags `vX.Y.Z`, publishes to the
163
+ `latest` dist-tag with provenance (npm trusted publishing from the `release` environment, no npm token anywhere) and creates
164
+ the GitHub Release from the CHANGELOG section. Red anywhere → no tag, no publish, the run is red; the next day's run tries
165
+ again by itself. The logic is in `src/release/` (pure `releasePlan.ts` and `testGate.ts` + the `cli.ts` wrapper, run by
166
+ `tsx`, never built into `dist/`).
167
+
168
+ - **Tests gate:** the release does not run the suite again when the exact commit already passed. It asks GitHub for the
169
+ `Tests` workflow's runs of that commit's SHA (the newest run of the commit decides): **passed** → go on; **no run at all**
170
+ (a manual dispatch before CI ran) → the release runs the suite itself; **failed or still running** → the daily run
171
+ releases nothing that day and ends green with the reason (a red head is not skipped over to an older green commit), a
172
+ manual dispatch or a hand-pushed tag fails with it. The release commit may differ from the tested one ONLY in
173
+ `package.json`'s `"version"` line and `CHANGELOG.md`: `verify-delta` checks the diff before the tag and again in the publish
174
+ job (the tag against the tested SHA) and refuses otherwise. A hand-pushed tag (`npm version`, then push `main` with the tag) is gated through its
175
+ PARENT: when the commit before the tag has a green `Tests` run and the tag differs from it only in the version and the CHANGELOG,
176
+ that run is the gate — the tag's own run, started by the same push, is not waited for. When the parent is not green or the tag
177
+ carries more than the bump, the tagged commit's own run decides (still running → refused: re-run the release once it is green).
178
+ - **Version** (Conventional Commits since the last `v*` tag): a `feat` that changes shipped code → minor; any other
179
+ shipped change (`fix`, `perf`, `refactor`, `revert`, `chore`, …) → patch; `chore(release)` commits never count. A `!`
180
+ after the type or a `BREAKING CHANGE:` footer in ANY commit makes the automatic run **fail**, naming the commits: a
181
+ major is released only by dispatching the workflow with `bump=major`.
182
+ - **Nothing to release** (green, nothing tagged): no commits since the last tag, `HEAD` already tagged, or no shipped
183
+ path in the net diff from the tag — `src/**/*.ts` outside `src/__tests__` and `src/release`, `stt/gigaam_sidecar.py`,
184
+ and `package.json` only when `dependencies`, `bin`, `engines`, `files` or `main` differ. A docs-only or tests-only
185
+ day, or a change and its revert, publishes nothing; README / SETUP changes reach the npm page with the next code
186
+ release.
187
+ - **Manual run** (Actions → Release to npm → Run workflow): `bump` `auto|patch|minor|major`; `force` releases even when
188
+ the skip rules say skip (never past the breaking-change guard of `auto`); `dryRun` does everything but the push, the
189
+ publish and the GitHub Release and prints the plan, the CHANGELOG diff, the notes and the package check.
190
+ - **By hand:** `npm version minor` locally (its `preversion` runs typecheck, build and the tests, its
191
+ `version` script moves the CHANGELOG heading, `.npmrc` gives the commit npm's message `chore(release): X.Y.Z`), then
192
+ push `main` with the tag; the workflow gates that tag on its commit's `Tests` run (above) and publishes it.
193
+ - **CHANGELOG:** `## Unreleased` becomes `## X.Y.Z — YYYY-MM-DD` (Tbilisi date) with a fresh empty `## Unreleased`
194
+ above; that section is the GitHub Release text and ships inside the package. Every entry of it — a top-level
195
+ paragraph or `- ` bullet, in any `###` subsection — starts with a bold first sentence (`**…**`); the run fails,
196
+ quoting the line, otherwise (`changelogFormat.test.ts` fails first on a commit that breaks it). An empty
197
+ `## Unreleased` is filled with the `feat` / `fix` subjects.
198
+ - **Package check** before any tag: the tarball holds `dist/cli.js`, `stt/gigaam_sidecar.py`, `CHANGELOG.md`,
199
+ `README.md`, `LICENSE` and no `src/` or tests, and `charness --version` of it, installed into a temp prefix, prints
200
+ the new version (`yarn tsx src/release/cli.ts check-pack`, after `yarn build`). `bin` paths are written
201
+ `dist/cli.js`, never `./dist/cli.js`: npm >= 11.10 silently drops a "./" entry from the manifest it publishes (3.3.0
202
+ reached the registry without its commands), so `check-pack` and `packageManifest.test.ts` refuse one, and
203
+ `check-publish` (`npm publish --dry-run`, run before the tag and again right before the publish) refuses a package.json
204
+ that npm reports it would drop or rewrite anything from.
205
+ - **After a release** the clone is one commit behind `main`: `git pull --rebase` before pushing. A new CHANGELOG entry
206
+ goes under `## Unreleased` (a push that races the release can conflict there; the run then fails with nothing
207
+ published).
package/README.md CHANGED
@@ -27,6 +27,7 @@ These hold whichever surface you drive it from — the same core serves all of t
27
27
  - **Sleeping agents** — a Claude json-stream topic's process is stopped when idle (or after every turn in per-turn mode) and the next message resumes the same conversation by itself, prompt cache still warm
28
28
  - **Several Claude accounts** — keep more than one Claude subscription signed in side by side and choose per topic which one a session runs on (`/accounts`); every account's usage windows are shown next to it, and `/limits` is a one-tap switch board over all of them and over the OpenCode providers. See [Several accounts](#several-accounts-accounts)
29
29
  - **Waits out usage limits** — when the agent hits a usage / session limit the bot reads the reset time out of the error (honouring the timezone the provider quotes; a mere warning such as `Approaching session limit · resets …` or the switch onto extra usage is not a limit), waits, then tells the agent to continue by itself and pins the "work resumed" message so a muted topic still notifies you; `/limits_autoswitch` turns waiting off per topic and `/limits` skips a single resume. With a **fallback chain** (`/limits_autoswitch`: other Claude accounts, OpenCode providers and models, in your order of preference) the limit notice offers «🔁 Switch to …» buttons to move the topic to the next entry with room and continue at once — or the bot does it by itself when you turn switching on; across backends the new agent gets a context bridge with the previous one's last turns
30
+ - **Updates itself** — checks for a new version hourly, installs it when no agent is working, restarts in place (same process, agents keep running) and says in General what changed; npm installs and git checkouts, hot mode and Docker included. Off with `/update` in General. See [Updating](SETUP.md#updating)
30
31
  - **Restart-safe** — agents run as external processes (tmux, `opencode serve`) that the bot re-adopts after a restart instead of killing them; a prompt sent while a session boots is buffered and replayed. See [Restart behaviour](#restart-behaviour)
31
32
 
32
33
  ### Telegram
@@ -94,7 +95,7 @@ actually start an agent or terminal in the folder.
94
95
  | `/terminal` | Open a raw `$SHELL` in the bound folder — see [Raw terminal](#raw-terminal-terminal-telegram) |
95
96
  | `/claude_mode` | Switch this topic's Claude backend (json-stream ⇄ per-turn ⇄ tmux-scrape); bare shows a picker — see [Claude Code backends](#claude-code-backends-claude_mode) |
96
97
  | `/model` | Switch model through a two-level picker: providers, then that provider's models (10 per page). On OpenCode the first row is 🕘 Recently used — a flat list of the models picked before, across providers (bot-wide, most recent first, up to 10), one tap to switch; it appears once something has been picked. Each provider row has a 🙈 to hide it from the picker (bot-wide, persisted; 👁 brings it back) — `/model <provider/model>` still reaches a hidden provider. If OpenCode is waiting on an old provider's retry, the next prompt interrupts that wait and starts with the selected model instead of sitting queued behind it |
97
- | `/accounts` | Every account the bot works with, on one screen. **Claude accounts** (when you keep several subscriptions signed in): each profile with its email, plan (personal Pro/Max or corporate Team/Enterprise), how many topics use it and its usage windows, ✓ on the active one; tap a profile to switch this topic (a running session is restarted under it, same conversation — at the end of its turn if it is working; on a topic that runs OpenCode the tap switches the topic to Claude on that account, after a confirmation when there is a conversation to hand over); in **General** the pick becomes the default for all topics. The title says where the topic really runs (`Claude «work»`, `OpenCode openai/gpt-5`) and the ✓ sits on that row — on no account while OpenCode runs. Per profile: 🔑 signs it in again, 🗑 removes it (after a confirmation; for `default`, which IS `~/.claude`, «remove» signs it out — see below). The usage lines give each window's percentage USED and when it resets as a time from now (`5h 5% 🔄 in 4h · week 20% 🔄 in 5d 8h · Fable 4% 🔄 in 5d 8h`; «🔄 in 2h», «🔄 in 2d 1h», «🔄 in 30m»; Fable only on a plan that has its own weekly window); when the 5h window's reset is unknown the weekly one is shown in its place, and with no reset at all only the percentage. The hint paragraph under the account list says the percentages are used. «➕ Claude account» asks for a name and runs the sign-in. **OpenCode providers**: the connected ones are listed with how each is signed in (`ChatGPT subscription` with its ~5 h / ~7 d usage windows like an account's, `API key`). In a topic a provider row is `[provider] [🔑] [🔌]`: a tap switches the topic to the provider through its model page (✓ on the model it runs, ⛔ on a limited one; a provider with one model is picked at once), 🔑 connects it again, 🔌 disconnects it (it asks, then removes the stored credentials; a provider OpenCode enables from an environment variable, e.g. `OPENROUTER_API_KEY`, stays active — the reply says so and points at hiding it in `/model`); in **General** a provider row is «🔌 Disconnect <provider>» and 🔑 only; «➕ OpenCode provider» asks for the provider id and continues with the connect flow (OAuth methods when available, otherwise an API key whose message is deleted from Telegram). What happens when an account hits a usage limit is set in `/limits_autoswitch`. Text forms: `/accounts add <name>`, `/accounts use <name>`, `/accounts use <provider>/<model>` (an OpenCode model, in a topic), `/accounts login <name>`, `/accounts remove <name>`, `/accounts autoswitch on\|off`. `/account`, `/connect [provider [key]]` and `/disconnect [provider]` still work as hidden aliases (the menu lists only `/accounts`). See [Several accounts](#several-accounts-accounts) |
98
+ | `/accounts` | Every account the bot works with, on one screen. **Claude accounts** (when you keep several subscriptions signed in): each profile with its email, plan (personal Pro/Max or corporate Team/Enterprise), how many topics use it and its usage windows, ✓ on the active one; tap a profile to switch this topic (a running session is restarted under it, same conversation — at the end of its turn if it is working; on a topic that runs OpenCode the tap switches the topic to Claude on that account, after a confirmation when there is a conversation to hand over); in **General** the pick becomes the default for all topics. The title says where the topic really runs (`Claude «work»`, `OpenCode openai/gpt-5`) and the ✓ sits on that row — on no account while OpenCode runs. Per profile: 🔑 signs it in again, 🗑 removes it (after a confirmation; for `default`, which IS `~/.claude`, «remove» signs it out — see below). The usage lines give each window's percentage USED and when it resets as a time from now (`5h 5% 🔄 in 4h · week 20% 🔄 in 5d 8h · Fable 4% 🔄 in 5d 8h`; «🔄 in 2h», «🔄 in 2d 1h», «🔄 in 30m»; Fable only on a plan that has its own weekly window); when the 5h window's reset is unknown the weekly one is shown in its place, and with no reset at all only the percentage. The hint paragraph under the account list says the percentages are used. «➕ Claude account» asks for a name and runs the sign-in; «➕ Claude API account (Console credits)» adds an account billed to a Claude Console organization's API credits (`💳`, see [An account billed to API credits](#an-account-billed-to-api-credits-claude-console)). **OpenCode providers**: the connected ones are listed with how each is signed in (`ChatGPT subscription` with its ~5 h / ~7 d usage windows like an account's, `API key`). In a topic a provider row is `[provider] [🔑] [🔌]`: a tap switches the topic to the provider through its model page (✓ on the model it runs, ⛔ on a limited one; a provider with one model is picked at once), 🔑 connects it again, 🔌 disconnects it (it asks, then removes the stored credentials; a provider OpenCode enables from an environment variable, e.g. `OPENROUTER_API_KEY`, stays active — the reply says so and points at hiding it in `/model`); in **General** a provider row is «🔌 Disconnect <provider>» and 🔑 only; «➕ OpenCode provider» asks for the provider id and continues with the connect flow (OAuth methods when available, otherwise an API key whose message is deleted from Telegram). What happens when an account hits a usage limit is set in `/limits_autoswitch`. Text forms: `/accounts add <name>`, `/accounts add <name> api`, `/accounts use <name>`, `/accounts use <provider>/<model>` (an OpenCode model, in a topic), `/accounts login <name>`, `/accounts remove <name>`, `/accounts autoswitch on\|off`. `/account`, `/connect [provider [key]]` and `/disconnect [provider]` still work as hidden aliases (the menu lists only `/accounts`). See [Several accounts](#several-accounts-accounts) |
98
99
  | `/effort` | Reasoning effort. **In a topic:** shows the level its session runs and where the next start's level comes from («medium — as in General» / «high — set for this topic»; a running session keeps the level it started with when General moves on: «xhigh (medium from the next start) — as in General») with a button per level — Claude: the native `/effort` levels (`low…ultracode`); OpenCode: the current model's variants, applied per-prompt; General's `ultracode` is the model's highest variant and `auto` sends no variant — and «As in General (medium)», named with the level the topic will really get (OpenCode: rounded to the model's variants), which drops the topic's own pick (a live session is brought to the default at once). **In General:** the default effort for every topic that has no effort of its own (✓ on the current one; `medium` until you pick), saved to `settings.json` and applied from each topic's next session start or wake-up — running sessions are not restarted. No env configuration — see [Instance settings](SETUP.md#instance-settings-settingsjson) |
99
100
  | `/verbosity` | Output-verbosity macro (`minimal\|short\|full`): sets the thinking, tool-results and sub-agent display prefs at once; `/thinking`, `/tool_results`, `/subagent` point-override afterwards. Mixed prefs show as "custom" in the picker. The picker's second row (or `/verbosity stream\|stream_answers\|answers`) sets what the topic SHOWS — "Stream only" (default), "Stream + answers" (the stream plus the agent's pinned answers) or "Answers only" (the pinned answers, no stream; an agent that keeps writing after its final answer is reminded once to send what matters); the answers are written like for a manager or QA — brief and to the point; a switch applies from the next message, and `/status` names the view |
100
101
  | `/thinking` | Chain-of-thought display: `full` keeps the reasoning, `short` collapses to "💭 thought for Ns", `minimal` keeps only the live working cue |
@@ -104,11 +105,11 @@ actually start an agent or terminal in the folder.
104
105
  | `/rename_session` | Rename the current live session (OpenCode; Claude transcripts have no title) |
105
106
  | `/quit`, `/q` | End the session — Claude: graceful double Ctrl+C; OpenCode/terminal: `stopSession`. Releases the persisted session id, so a bot restart won't auto-reattach it (resume later via `/sessions`). A topic whose agent process is gone but whose session is kept (it "sleeps": the next message, file, voice note, `/compact`, `/model` or `/effort` resumes it by itself) is released the same way |
106
107
  | `/new`, `/clear_session` | End the current session and start a fresh one (same adapter) |
107
- | `/status` | This thread's status: agent, bound folder, the Claude account it runs on and the resolved workdir; a live session also shows the model it is actually running, the effort it runs (when the default moved on under it: «xhigh (medium from the next start)»), start time, runtime version and context use (anything the backend cannot report renders as unknown). On a Claude topic the account is its profile (name · email as in `/accounts`, marked ⛔ while it is at its limit, with a second row for a switch still pending); an OpenCode topic's model already names its provider |
108
+ | `/status` | This thread's status: agent, bound folder, the Claude account it runs on and the resolved workdir; a live session also shows the model it is actually running, the effort it runs (when the default moved on under it: «xhigh (medium from the next start)»), start time, runtime version and context use (anything the backend cannot report renders as unknown). On a Claude topic the account is its profile (name · email as in `/accounts`, `· 💳 API` for an account billed to API credits — with a line of what it spent through the bot — marked ⛔ while it is at its limit, with a second row for a switch still pending); an OpenCode topic's model already names its provider |
108
109
  | `/output` | Last 500 lines of agent output (sent as at most 5 chunks; the overflow is reported as omitted) |
109
110
  | `/c`, `/y`, `/n` | Ctrl+C / "y" / "n" |
110
111
  | `/enter`, `/up`, `/down`, `/tab` | tmux key passthrough |
111
- | `/esc`, `/escape` | Send a raw Escape — interrupt the current turn / dismiss a selector |
112
+ | `/interrupt`, `/esc`, `/escape` | Interrupt the running turn — Claude gets a raw Escape (which also dismisses a selector), OpenCode aborts the generation; the conversation stays and your next message steers the agent. A question the agent is waiting on is closed first (rejected on OpenCode, declined on the Claude stream-json backends) and its buttons are retired. A pending sign-in, account-name prompt or `/topic` wizard is cancelled first instead (the agent is not interrupted). Not supported in a `/terminal` topic |
112
113
  | `/schedule` | Schedule a prompt in free text — the agent parses the time and owns the job; see [Scheduler](#scheduler-schedule-telegram) |
113
114
  | `/stt` | Voice recognition, from any topic (General included); instance-wide, applied to the next voice note — no restart. The main screen holds only what applies to recognition as a whole: the ordered list of **engines** (the first that works answers; when one fails the next takes over) — one full-width button each (`1. GigaAM (local) · running · 10s+25% · ✓` — name, the local engine's short state, timeout `base s + % of the voice`, ✓ usable / ✗ disabled or skipped) — then «➕ Add engine», «🌐 Language» (auto or `ru en uk de fr es`) and «✖️ Close». A tap on an engine opens its **card**: ⬆️ Up / ⬇️ Down, ✅ Use / 🚫 Don't use, ⏱ Timeout (presets), then what the kind adds — 🔑 key for **Groq**, **OpenAI** or a **custom** OpenAI-compatible server (its address, a base like `http://host:8000` is completed; then «does it need a key?»), and 🗑 Remove. A pasted key is deleted from the topic the moment it is accepted, never shown again and kept out of the logs; an address with a login in it is refused. **GigaAM (local)** (Russian only, runs on this machine) is added through «➕ Add engine» like the others: installed, it joins the list at once; not installed, a confirmation (≈ 400 MB on disk; ≈ 0.5 GB RAM while it runs, up to ≈ 0.9 GB on a long voice, nothing while stopped) leads to the install with a live progress message, and the message becomes its card. Its card holds everything local: the status line (ready · running / stopped / paused after failed starts / installing / not installed), «⏱ Exit after idle» (5 / 15 / 30 / 60 min or never), «⚙️ Workers» (1 / 2 / 4 / 8), «🔄 Try again now» (while paused), «Remove from list» (the files stay) and «🗑 Remove model files». Everything lives in one file, `stt.json` — see [Voice input](SETUP.md#voice-input). `/voice` and `/speech` are hidden aliases |
114
115
  | `/reminders` | Reminders set with buttons only — the bot posts and pins them itself, with no agent involved; works in any topic, bound or not. See [Reminders](#reminders-reminders-telegram) |
@@ -117,9 +118,10 @@ actually start an agent or terminal in the folder.
117
118
  | `/compact` | Compact the agent's context — a real, confirmed compaction on OpenCode and on the default (stream) Claude backend; the tmux Claude backend has the literal command forwarded (its TUI compacts); terminal: not supported |
118
119
  | `/compact_on_idle` | Toggle auto-compaction after ~55 min idle (Enable/Disable picker; `AGENT_IDLE_MINUTES` sets the window). Per topic; run it in **General** to set the default for all topics. On by default. Each idle compaction posts one short notice; the picker's «Show summary» / «Hide summary» row (the `/compact_summary` switch) adds the agent's full summary after it. Fires at most once per active period (until you write again); if a question was pending it is re-asked with tappable buttons after the compaction. On the json-stream backends the idle mark also STOPS the agent process, compaction or not — the topic sleeps and your next message resumes it (`/status` says so) |
119
120
  | `/compact_summary` | Toggle posting the agent's full summary after a compaction — `/compact`, the idle compaction and the agent's own `compact_conversation` (Enable/Disable picker). Per topic; run it in **General** to set the default for all topics. Off by default: a compaction ends in a short line. On OpenCode it also hides the summary OpenCode streams itself |
120
- | `/limits` | The **switch board**: every signed-in Claude account and every connected OpenCode provider on one screen. The **message** carries the figures — `Usage (% used)` and a line per account in the same wording `/accounts` uses (`default — ⏱ 5h 14% 🔄 in 3h · week 3% 🔄 in 6d`; `⛔ at its limit until …` while the account is limited; `(stale)` when the last poll could not refresh), a provider signed in through a ChatGPT subscription after them (`openai (ChatGPT) — ⏱ 5h 12% 🔄 in 3h · week 40% 🔄 in 4d`), the two automatic-switching settings' state with a link to `/limits_autoswitch`, and the caption «Choose the active model:». The **buttons** are one full-width name each — `✓ Claude · work`, `Claude · default`, `OpenCode · openai` — with no figure on them (the figures do not fit a phone's button); ✓ marks where the topic runs; a signed-out account is not listed. **A tap switches this topic**: another Claude account (the running session restarts under it, same conversation — at the end of its turn when it is working), an OpenCode provider (opens its model page, 10 models per page with ⛔ on a limited one; a provider with one model switches at once), the ✓ row (the model page of where the topic runs: change the model). Across backends with a conversation to hand over the bot asks first (✅ Continue / ✖️ Cancel); while a usage-limit wait is armed the tap is the limit move — the wait ends and the work continues, no question — and «⏭ Skip once» is offered. An unbound topic is told to bind a folder first. In **General** a Claude row sets the default account for all topics (the caption reads «Choose the default Claude account:»), and an OpenCode row only says that a model is picked inside a topic. The limit notice names this command. The old text forms `/limits rebuild`, `/limits clear`, `/limits switch …`, `/limits wait …` still work (they are `/limits_autoswitch`'s verbs). See [Usage limits and the fallback models](#usage-limits-and-the-fallback-models-limits-and-limits_autoswitch) |
121
- | `/limits_autoswitch` | The setup of automatic switching on a usage limit, with a short text: a title, what the topic runs on and where its two settings come from. **Fallback models** — an ordered list of Claude accounts (with an optional model) and OpenCode providers with a model, top = preferred, where a topic continues when its agent hits a limit. One full-width button per entry (`✓ 1. Claude · work · opus`, `2. Claude · default ⛔ 14:00`, `3. OpenCode · openai · gpt-5 🚫` — ✓ where the topic runs, ⛔ limited until, 🔒 not signed in, ❓ no longer offered, 🚫 left out of automatic switching); a tap opens that entry in the same message with ⬆️ Up / ⬇️ Down, 🚫 Don't use / ✅ Use (a left-out entry stays usable by hand through `/model` and `/accounts`), 🗑 Remove and ⬅️ Back. Below the list: «➕ Add» (account or provider, then the model; an entry of a backend the list does not use yet — an OpenCode model next to Claude accounts, or the reverse — asks first, because a limit then hands a conversation over in compacted form; including a 🚫 entry that does that asks the same way), «🔄 Fill from connected» (a draft from what is connected; entries you left out stay left out; asks first when a list exists), «🗑 Clear». The list is ONE instance-wide list and is editable from every topic. **Two settings** decide what a limit does: «🔁 Auto switch» — auto switch to a fallback model (OFF by default: the limit notice carries «🔁 Switch to …» buttons instead) and «⏳ Wait for reset» — wait for the reset and continue (ON by default; while a wait is armed the topic screen also offers «⏭ Skip once»). In General they are the defaults for all topics and each is one button that flips the value. In a regular topic each is one short full-width button showing the effective value (`🔁 Auto switch: ON`; one line of text above the list says where each value comes from — `🔁 as in General · ⏳ set for this topic`); a tap opens the setting in the same message with ON / OFF / «As in General (<the General value>)» — the topic's own choice marked ✓ — and ⬅️ Back, and a choice applies and returns to the screen. Switch ON: the topic moves to the first available entry at once. OFF: buttons on the notice, and if none is pressed the bot waits for the reset (waiting ON); both OFF: it only reports the limit. When every enabled entry, the current one included, is limited, the wait ends at the EARLIEST reset and the topic continues on that entry (the notice names it and the time). Text forms: `/limits_autoswitch rebuild`, `/limits_autoswitch clear`, `/limits_autoswitch switch on\|off\|same`, `/limits_autoswitch wait on\|off\|same` (the same verbs work after `/limits`). `/fallback` and `/auto_continue_limits [on\|off]` still work as hidden aliases. See [Usage limits and the fallback models](#usage-limits-and-the-fallback-models-limits-and-limits_autoswitch) |
121
+ | `/limits` | The **switch board**: every signed-in Claude account and every connected OpenCode provider on one screen. The **message** carries the figures — `Usage (% used)` and a line per account in the same wording `/accounts` uses (`default — ⏱ 5h 14% 🔄 in 3h · week 3% 🔄 in 6d`; `⛔ at its limit until …` while the account is limited; `(stale)` when the last poll could not refresh), a provider signed in through a ChatGPT subscription after them (`openai (ChatGPT) — ⏱ 5h 12% 🔄 in 3h · week 40% 🔄 in 4d`), the two automatic-switching settings' state with a link to `/limits_autoswitch`, and the caption «Choose the active model:». The **buttons** are one full-width name each — `✓ Claude · work`, `Claude · default`, `OpenCode · openai`, `Claude · console 💳` for an account billed to API credits — with no figure on them (the figures do not fit a phone's button); ✓ marks where the topic runs; a signed-out account is not listed. **A tap switches this topic**: another Claude account (the running session restarts under it, same conversation — at the end of its turn when it is working), an OpenCode provider (opens its model page, 10 models per page with ⛔ on a limited one; a provider with one model switches at once), the ✓ row (the model page of where the topic runs: change the model). Across backends with a conversation to hand over the bot asks first (✅ Continue / ✖️ Cancel); while a usage-limit wait is armed the tap is the limit move — the wait ends and the work continues, no question — and «⏭ Skip once» is offered. An unbound topic is told to bind a folder first. In **General** a Claude row sets the default account for all topics (the caption reads «Choose the default Claude account:»), and an OpenCode row only says that a model is picked inside a topic. The limit notice names this command. The old text forms `/limits rebuild`, `/limits clear`, `/limits switch …`, `/limits wait …` still work (they are `/limits_autoswitch`'s verbs). See [Usage limits and the fallback models](#usage-limits-and-the-fallback-models-limits-and-limits_autoswitch) |
122
+ | `/limits_autoswitch` | The setup of automatic switching on a usage limit, with a short text: a title, what the topic runs on and where its two settings come from. **Fallback models** — an ordered list of Claude accounts (with an optional model) and OpenCode providers with a model, top = preferred, where a topic continues when its agent hits a limit. One full-width button per entry (`✓ 1. Claude · work · opus`, `2. Claude · default ⛔ 14:00`, `3. OpenCode · openai · gpt-5 🚫` — ✓ where the topic runs, ⛔ limited until, 🔒 not signed in, ❓ no longer offered, 💸 API credits used up, 🚫 left out of automatic switching); a tap opens that entry in the same message with ⬆️ Up / ⬇️ Down, 🚫 Don't use / ✅ Use (a left-out entry stays usable by hand through `/model` and `/accounts`), 🗑 Remove and ⬅️ Back. Below the list: «➕ Add» (account or provider, then the model; an entry of a backend the list does not use yet — an OpenCode model next to Claude accounts, or the reverse — asks first, because a limit then hands a conversation over in compacted form; including a 🚫 entry that does that asks the same way), «🔄 Fill from connected» (a draft from what is connected; entries you left out stay left out; an account billed to API credits comes last and left out unless you had included it; asks first when a list exists), «🗑 Clear». The list is ONE instance-wide list and is editable from every topic. **Two settings** decide what a limit does: «🔁 Auto switch» — auto switch to a fallback model (OFF by default: the limit notice carries «🔁 Switch to …» buttons instead) and «⏳ Wait for reset» — wait for the reset and continue (ON by default; while a wait is armed the topic screen also offers «⏭ Skip once»). In General they are the defaults for all topics and each is one button that flips the value. In a regular topic each is one short full-width button showing the effective value (`🔁 Auto switch: ON`; one line of text above the list says where each value comes from — `🔁 as in General · ⏳ set for this topic`); a tap opens the setting in the same message with ON / OFF / «As in General (<the General value>)» — the topic's own choice marked ✓ — and ⬅️ Back, and a choice applies and returns to the screen. Switch ON: the topic moves to the first available entry at once. OFF: buttons on the notice, and if none is pressed the bot waits for the reset (waiting ON); both OFF: it only reports the limit. When every enabled entry, the current one included, is limited, the wait ends at the EARLIEST reset and the topic continues on that entry (the notice names it and the time). Text forms: `/limits_autoswitch rebuild`, `/limits_autoswitch clear`, `/limits_autoswitch switch on\|off\|same`, `/limits_autoswitch wait on\|off\|same` (the same verbs work after `/limits`). `/fallback` and `/auto_continue_limits [on\|off]` still work as hidden aliases. See [Usage limits and the fallback models](#usage-limits-and-the-fallback-models-limits-and-limits_autoswitch) |
122
123
  | `/bind` | Bare: current binding + folder picker, with «leave current dir» (the old `/unbind`) and «create new folder» buttons |
124
+ | `/topic [name]` | Open another forum topic on a folder, without leaving this one. Asks the new topic's name (`/topic <name>` takes it from the command), then the folder: «📁 <folder> (as here)» — the folder this topic is bound to — or «📂 Other folder», which is the `/bind` picker with «create new folder»; in General and in an unbound topic the picker comes at once. Then the bot creates the topic (it needs the `can_manage_topics` right — `/doctor` checks it), binds it, shows the usual binding welcome there and turns the question into the answer with an «Open topic →» button. «✖️ Cancel» on the question and in the picker, and `/esc` at any step, end it. No agent starts by itself: start one in the new topic as in any bound topic. Works in the group only — in the bot's private chat it says so |
123
125
  | `/mcp` | List MCP servers active for this thread |
124
126
 
125
127
  #### Info / ops (work anywhere)
@@ -132,7 +134,8 @@ actually start an agent or terminal in the folder.
132
134
  | `/list` | List existing topics and their bindings |
133
135
  | `/status` | This topic's status; in General — a global view of all topics + active agents. A non-General topic also reports the account it runs on (Claude profile · email, ⛔ at its limit) and the resolved workdir; a live one the model actually running, runtime version and context use (unknown when the backend cannot report it) |
134
136
  | `/doctor` | Self-diagnose: admin rights, privacy mode, paths, CLIs |
135
- | `/version` | Versions: bot, claude, opencode, node, tmux |
137
+ | `/version` | Versions: bot, claude, opencode, node, tmux; a line when an update is available |
138
+ | `/update` | Self-update screen: what this instance runs (npm / git, hot), automatic updates `Automatic` · `Notify only` · `Off` (default automatic), the «what's new» switch, Check now, Update now. Controls in General, read-only elsewhere; typed forms `/update on\|notify\|off`, `/update whatsnew on\|off`, `/update check`, `/update now` — see [Updating](SETUP.md#updating) |
136
139
  | `/whoami` | Show userId, chatId, threadId, isAllowed, binding |
137
140
  | `/trace` | Output-trace recorder (`on`/`off`, `on all`/`off all`; bare = status) — see [Observability](#observability) |
138
141
  | `/timestamps` | Prepend the send time to prompts forwarded to the agent (`on`/`off`; bare = status) |
@@ -292,7 +295,8 @@ so one account's usage limit does not stop every topic.
292
295
  `claude_profiles` — optionally with the model it will run — and gets every
293
296
  profile with its windows plus `recommended`: the signed-in, unlimited profile
294
297
  with the most headroom (the session window and the shared weekly window bound
295
- every model; Fable additionally drains its own weekly window). It then
298
+ every model; Fable additionally drains its own weekly window) — never an
299
+ account billed to API credits. It then
296
300
  starts the child with `CLAUDE_CONFIG_DIR=<configDir>` (`null` = the default
297
301
  profile: set nothing). The same answer from a shell, for a worker without
298
302
  MCP: `charness claude-profiles [--best] [--model <name>] [--json]` —
@@ -300,6 +304,73 @@ so one account's usage limit does not stop every topic.
300
304
  `CLAUDE_CONFIG_DIR="$(charness claude-profiles --best --model fable)"`
301
305
  works as is; it reads the bot's cache off `DATA_DIR/state.json`.
302
306
 
307
+ #### An account billed to API credits (Claude Console)
308
+
309
+ Besides subscriptions, an account can bill the **API credits of a Claude Console
310
+ organization** — useful as a fallback when every subscription is at its limit.
311
+ Such an account pays per request (no 5-hour or weekly windows: its limit is the
312
+ balance), so the bot never uses it without you choosing it.
313
+
314
+ - **Adding one:** `/accounts` → «➕ Claude API account (Console credits)» (or
315
+ `/accounts add console api`) — give it a name. The bot runs
316
+ `claude auth login --console` for it: open the link, sign in to the Console
317
+ organization whose credits it should spend and paste the code back. Claude
318
+ Code then creates an API key in that organization's workspace and keeps it in
319
+ the account's own directory (`DATA_DIR/claude-profiles/<name>/`), where only
320
+ that account's sessions read it. Your Console role must allow creating a key
321
+ («Claude Code» or «Developer»).
322
+ - **Or paste a key:** while that sign-in waits, paste an existing API key
323
+ (`sk-ant-api…`, on its own or as an `ANTHROPIC_API_KEY=…` line) instead of the
324
+ code. The message is deleted at once (so is a key pasted where the name is
325
+ asked, or typed on the `/accounts` command line), the key
326
+ is checked with the API (a refused key is not stored), then kept in the
327
+ account's directory (`0600`) and handed to its sessions only, through an
328
+ `apiKeyHelper` in that account's launch settings — never in `state.json`, never
329
+ in an environment variable an agent or a child process could read, never in
330
+ the trace (any `sk-ant-…` text is redacted from it). `/accounts login <name>`
331
+ of an API account runs the same flow again; a later Console sign-in replaces
332
+ a pasted key.
333
+ - The Console's keyless sign-in («Sign in with your Console account» in
334
+ Claude Code's own `/login`) is **not** used: it is stored outside the account's
335
+ directory, for the whole machine, and signs the subscriptions out.
336
+ - Every other account stays a subscription exactly as before: the bot still
337
+ removes any `ANTHROPIC_API_KEY` from the environment of every session and of
338
+ every sign-in.
339
+ - **Recognising one:** `/accounts` shows it as `console — you@example.com, 💳 API
340
+ · pay per use` (`💳 API key · pay per use` for a pasted key), `/status` marks
341
+ the account `· 💳 API`, and picking it — in a topic, or as the default in
342
+ General — says that every request is billed. The bot reads the billing from
343
+ `claude auth status` each time it checks the account, so a mistaken
344
+ subscription login shows as a subscription.
345
+ - **Never used by itself.** It has no windows, so to every headroom rule it
346
+ would look inexhaustible: the `claude_profiles` recommendation (and
347
+ `charness claude-profiles --best`) never names it, the within-Claude switch
348
+ never offers it, and «🔄 Fill from connected» in `/limits_autoswitch` puts it
349
+ LAST in the fallback list and left out of automatic switching (🚫). To use it,
350
+ tap it on the `/limits` board (`Claude · console 💳`) — or include its entry
351
+ («✅ Use») so a limit notice offers it, or switches to it by itself with
352
+ switching on. A rebuilt list keeps it included once you did.
353
+ - **When the credits run out** («Credit balance is too low») the bot does not
354
+ wait for a reset — there is none — and never retries hourly. The topic gets
355
+ one `💸` notice, the account is marked as out of credits (`💸` in
356
+ `/limits_autoswitch`, `/accounts`, `/limits` and `/status`) and the fallback
357
+ list decides as for a limit: switching on moves the topic at once, otherwise
358
+ the notice carries «🔁 Switch to …» buttons. If nothing can take over now but
359
+ another entry comes back at a known time (a subscription's weekly reset), the
360
+ topic waits for that and continues there; otherwise the notice is all. The
361
+ mark lasts until you act: pick the account again (after topping the balance
362
+ up), or sign it in again — or until a request on it goes through again. A
363
+ rate limit of the API (429) is still a short retry.
364
+ - **What it spent.** On the default (stream) backend every turn reports the
365
+ conversation's running cost; the bot counts the part each turn on the API
366
+ account added (a subscription turn of the same conversation is never
367
+ counted) and shows the total under the account in `/accounts` and `/limits`
368
+ (`💳 $12.34 spent since 23.10 · last turn $0.21`), on `/status`, in the line a
369
+ limit switch onto the account adds («billed per request … $X spent through
370
+ the bot so far») and in its «credits used up» notice. It is what the bot
371
+ saw, not the organization's balance — that one is on the Console's Billing
372
+ page. The tmux backend reports no cost.
373
+
303
374
  ### Usage limits and the fallback models (`/limits` and `/limits_autoswitch`)
304
375
 
305
376
  When a topic's agent stops on a usage limit — a Claude account window, an
package/SETUP.md CHANGED
@@ -103,36 +103,76 @@ hostnames: their ports and tmux servers are their own.
103
103
 
104
104
  ### Updating
105
105
 
106
- An npm install updates with `npm install -g charness@latest`; then restart `charness` (what survives a restart:
107
- [Restart behaviour](README.md#restart-behaviour)).
106
+ **The bot updates itself.** With `autoUpdate` on (the default) an instance checks for a new version every hour (the first
107
+ check 10 minutes after it starts), installs it when nobody is using the bot, restarts **in place** — the same process id, so
108
+ systemd, pm2, `nohup` or a terminal see nothing, and every agent keeps running (what survives:
109
+ [Restart behaviour](README.md#restart-behaviour)) — and says in General what changed:
108
110
 
109
- **An update that renames something ships a migration guide** — read it before the restart. The names that were
110
- renamed from TelegramCode to Charness (environment variables, the data dir, the Docker names, …) still work for
111
- now and print a deprecation line at start; the steps are in
111
+ ```
112
+ 🔄 Charness updated: 3.4.0 → 3.5.0
113
+ • Charness now updates itself.
114
+ • A reset written in several parts is read whole.
115
+ • …and 3 more — CHANGELOG: <link>
116
+ ```
117
+
118
+ Control it from **`/update` in General** (Automatic · Notify only · Off, the «what's new» switch, Check now, Update to X
119
+ now), or by hand in `settings.json` ([Instance settings](#instance-settings-settingsjson)). **Notify only** says that a new
120
+ version is out and puts an «Update to X» button under it; **Off** never looks. In any other topic `/update` is the same
121
+ status without the controls.
122
+
123
+ What it does depends on how this instance is installed — `/update` names the kind:
124
+
125
+ | Install | What the updater does |
126
+ |---|---|
127
+ | **npm global** (`npm install -g charness`, a prefix the bot's user can write) | Takes a new version from the registry **4 hours after the bot first saw it**, so a hastily fixed release never reaches you first; installs it with the `npm` beside the running `node`, runs `charness self-check` on the NEW install before switching, then restarts onto it. Never `sudo`. |
128
+ | **git checkout** (a clone, an `npm link`, the one the Docker container mounts) | Fast-forwards through `scripts/self-update.sh`, with no hold — a checkout follows `main` — and rebuilds. In hot mode the watcher reloads the worker by itself (a pull that changes no compiled source — docs, the CHANGELOG, scripts — rebuilds nothing, so the updater asks the worker to reload); when a file the hot supervisor loaded changed, the worker asks the supervisor to re-execute itself (same pid; the OpenCode server and the speech engine are left alone). |
129
+ | anything else — a read-only folder (a root-owned npm prefix), pnpm / yarn / bun, a project-local install, a Node without `process.execve` (needs 22.15+, not Windows), `npx` | Says that a new version is out and how to update by hand; `npx` gets nothing to update. |
130
+
131
+ **When it installs.** Only when no agent is working, no input waits to be consumed, no sign-in or wizard is open and no
132
+ person has written to the bot for 10 minutes. A tap on «Update to X now» skips the quiet period, never a running turn.
133
+ **What it offers instead of installing:** a major version, a version that needs a newer Node than this one, and a
134
+ checkout range that adds a [migration guide](MIGRATION_RENAME_TO_CHARNESS.md) — each is announced in General and waits
135
+ for your tap. A deprecated version is skipped.
136
+
137
+ **A version that does not start is put back.** The install is written to `update-pending.json` before the switch; the new
138
+ version is confirmed 60 seconds after it launched, and one that fails to boot twice is rolled back to the previous version
139
+ (npm reinstalls it, a checkout resets and rebuilds), the target is not retried, and General says so. In hot mode a pull
140
+ that has not produced a working worker within 10 minutes is rolled back the same way.
141
+
142
+ **After the update** General gets «Updated a → b» with the CHANGELOG entries the update brought (each opens with a bold
143
+ sentence; `whatsNew: false` leaves the list out). **An update that renames something ships a migration guide** — it is
144
+ named in that message; the names renamed from TelegramCode to Charness (environment variables, the data dir, the Docker
145
+ names, …) still work for now and print a deprecation line at start; the steps are in
112
146
  [MIGRATION_RENAME_TO_CHARNESS.md](MIGRATION_RENAME_TO_CHARNESS.md), the summary in the top entry of
113
- [CHANGELOG.md](CHANGELOG.md). `scripts/self-update.sh` names a newly added migration guide in its log.
147
+ [CHANGELOG.md](CHANGELOG.md).
148
+
149
+ **By hand** nothing changed: `npm install -g charness@latest` (an instance that finds a newer version installed on disk
150
+ restarts onto it by itself at its first idle moment), or `scripts/self-update.sh` in a checkout. Two instances of one npm
151
+ install on a host take turns — the second finds the version the first installed and restarts onto it.
114
152
 
115
- #### Updating a checkout (self-update, no root)
153
+ #### Updating a checkout (`scripts/self-update.sh`, no root)
116
154
 
117
155
  A checkout — the one the [Docker container](#run-with-docker) runs, or a clone from
118
- [DEVELOPMENT.md](DEVELOPMENT.md#local-development) — updates with `scripts/self-update.sh` (not in the npm package).
119
- It refreshes the checkout in place, running as the checkout's owning user — no root, no cross-user copying. It is
120
- safe to run blindly (e.g. from cron):
156
+ [DEVELOPMENT.md](DEVELOPMENT.md#local-development) — updates with `scripts/self-update.sh` (not in the npm package); the
157
+ bot's own updater drives it, and it can still be run by hand or from cron (then set `autoUpdate` to `off` so the two do not
158
+ race). It refreshes the checkout in place, running as the checkout's owning user — no root, no cross-user copying. It is
159
+ safe to run blindly:
121
160
 
122
161
  - fast-forward only: it skips silently when the tree has tracked changes, a
123
162
  merge/rebase is in progress, or local history diverged from the upstream —
124
163
  a dev clone that is ahead of origin is never touched;
164
+ - `--check` fetches and changes nothing, `--hold-breaking` updates unless the incoming range adds a `MIGRATION_*.md`;
125
165
  - `yarn install --immutable` runs only when `yarn.lock` / `package.json`
126
166
  changed;
127
167
  - a hot-mode instance (`charness hot`) rebuilds and restarts the worker
128
168
  by itself (after a dependency install the script touches `tsconfig.json`
129
169
  so `tsc -w` recompiles with the new modules); a non-hot instance gets
130
- `yarn build` plus a restart notice — the script never kills processes;
170
+ `yarn build` — the script never kills processes (the bot's updater restarts it);
131
171
  - changes to the hot supervisor itself (`src/cli.ts`, `src/cli/hot.ts`,
132
- `nodemon.json`) are flagged with a warning: those need a manual
133
- `charness hot` restart (nodemon reloads only the worker).
172
+ `nodemon.json`) are flagged with a warning: with the bot's updater the supervisor
173
+ re-executes itself, run by hand they need a manual `charness hot` restart (nodemon reloads only the worker).
134
174
 
135
- For unattended updates give the deploying user read access to the repo (e.g.
175
+ For unattended updates **without** the bot's updater give the deploying user read access to the repo (e.g.
136
176
  a read-only GitLab deploy key on a passphrase-less SSH key) and add a cron
137
177
  entry:
138
178
 
@@ -257,7 +297,9 @@ This step is **[manual]** — it all happens inside the Telegram app.
257
297
 
258
298
  1. Open the group; in the General topic send `/ls` — the bot lists subfolders
259
299
  under the work root.
260
- 2. Create a topic (the `+` button).
300
+ 2. Create a topic (the `+` button) — or run `/topic` in any topic of the group
301
+ (General included): it asks for the name and the folder, creates the topic,
302
+ binds it and links to it, and you can skip step 3.
261
303
  3. In the new topic: `/bind <subdir>` (auto-bound if the topic name matches a
262
304
  subdir).
263
305
  4. `/claude` or `/opencode` → talk to the agent (text or voice).
@@ -358,12 +400,14 @@ be written, the tap that needed it says so and names the path. The file is optio
358
400
  a hand edit applies to the next session start, no restart.
359
401
 
360
402
  ```json
361
- { "defaultEffort": "medium" }
403
+ { "defaultEffort": "medium", "autoUpdate": "on", "whatsNew": true }
362
404
  ```
363
405
 
364
406
  | Field | Meaning |
365
407
  |---|---|
366
408
  | `defaultEffort` | The reasoning effort of every topic that has no `/effort` pick of its own: `low`, `medium`, `high`, `xhigh`, `max`, `auto` or `ultracode`. Set it from **`/effort` in the General topic** (a tap writes the file), or by hand. Leave it out and the built-in default, `medium`, applies |
409
+ | `autoUpdate` | What the instance does about a new charness version: `on` installs it by itself when the bot is idle, `notify` says it is out and offers a button, `off` never looks. Set it from **`/update` in General**, or by hand. Leave it out and `on` applies ([Updating](#updating)) |
410
+ | `whatsNew` | `true` (default): after an update General lists the CHANGELOG entries it brought; `false`: only the «Updated a → b» line. Set from `/update` in General, or by hand |
367
411
 
368
412
  What a new Claude session starts with, first that exists: the topic's own `/effort` pick, the platform's default
369
413
  (a Jira conversation: `effort` in `jira.json`, `high` when absent — Jira never reads `defaultEffort`), `defaultEffort`,
@@ -625,9 +669,11 @@ Running it:
625
669
 
626
670
  - **Restart the bot only** (agents keep running and are adopted): `docker exec charness-main
627
671
  charness-restart-bot`. A crashed bot is restarted by the container after 20 s.
628
- - **Update the bot**: `docker exec charness-main /opt/charness/scripts/self-update.sh` (by hand or from
629
- a timer on the host); hot mode picks the change up. A change to `src/cli.ts`, `src/cli/hot.ts` or `nodemon.json`
630
- needs the bot restart above. **Update Claude Code / OpenCode**: they update themselves in the agents' home, or
672
+ - **Update the bot**: it updates itself (`/update` in General, [Updating](#updating)): the mounted checkout is pulled when
673
+ no agent is working, hot mode reloads the worker, and a change to `src/cli.ts`, `src/cli/hot.ts` or `nodemon.json`
674
+ makes the supervisor re-execute itself — the same process, so the container's entrypoint sees no exit and no restart
675
+ delay. By hand: `docker exec charness-main /opt/charness/scripts/self-update.sh`; an old supervisor (one that predates
676
+ re-exec) says once that it needs the bot restart above. **Update Claude Code / OpenCode**: they update themselves in the agents' home, or
631
677
  `docker exec charness-main npm install -g @anthropic-ai/claude-code` / `… opencode-ai`.
632
678
  - **A container restart is a reboot**: every agent process ends; each conversation resumes with its next message.
633
679
  - **Rootless Docker** (recommended): the container's root is your user, so everything written on the mounts is
@@ -2899,7 +2899,7 @@ class ClaudeCliAdapter extends events_1.EventEmitter {
2899
2899
  ...claudePermissionArgs,
2900
2900
  '--session-id', claudeSessionId,
2901
2901
  ...mcpFlagsArr,
2902
- ...(0, claudeCompactHook_1.prepareClaudeCompactHookFlags)((0, state_1.resolveDataDir)()),
2902
+ ...(0, claudeCompactHook_1.prepareClaudeCompactHookFlags)((0, state_1.resolveDataDir)(), configDir),
2903
2903
  ...(args ? [args] : []),
2904
2904
  ];
2905
2905
  const claudeShellCmd = claudeArgv.map(tmuxExec_1.shellSingleQuote).join(' ');
@@ -3530,7 +3530,7 @@ class ClaudeCliAdapter extends events_1.EventEmitter {
3530
3530
  ...claudePermissionArgs,
3531
3531
  '--resume', sessionId,
3532
3532
  ...mcpFlagsArr,
3533
- ...(0, claudeCompactHook_1.prepareClaudeCompactHookFlags)((0, state_1.resolveDataDir)()),
3533
+ ...(0, claudeCompactHook_1.prepareClaudeCompactHookFlags)((0, state_1.resolveDataDir)(), configDir),
3534
3534
  ];
3535
3535
  const claudeShellCmd = claudeArgv.map(tmuxExec_1.shellSingleQuote).join(' ');
3536
3536
  try {