even-keel 0.0.2 → 0.2.0-rc.2

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 (537) hide show
  1. package/.claude-plugin/marketplace.json +52 -0
  2. package/.env.example +66 -0
  3. package/CHANGELOG.md +315 -0
  4. package/LICENSE +21 -0
  5. package/README.md +199 -3
  6. package/dist/src/account/apps.js +100 -0
  7. package/dist/src/account/credit.js +117 -0
  8. package/dist/src/account/current.js +13 -0
  9. package/dist/src/account/daemon.js +65 -0
  10. package/dist/src/account/globals.js +25 -0
  11. package/dist/src/account/header-levels.js +22 -0
  12. package/dist/src/account/homes.js +332 -0
  13. package/dist/src/account/keychain.js +134 -0
  14. package/dist/src/account/keycheck.js +67 -0
  15. package/dist/src/account/ledger.js +92 -0
  16. package/dist/src/account/model.js +234 -0
  17. package/dist/src/account/network.js +51 -0
  18. package/dist/src/account/organizations.js +17 -0
  19. package/dist/src/account/service-defaults.js +17 -0
  20. package/dist/src/account/share.js +275 -0
  21. package/dist/src/account/transport.js +29 -0
  22. package/dist/src/account/vscode.js +84 -0
  23. package/dist/src/account/watcher.js +451 -0
  24. package/dist/src/agent/adapters.js +10 -0
  25. package/dist/src/agent/cli-state.js +1 -0
  26. package/dist/src/agent/contract.js +7 -0
  27. package/dist/src/agent/copies.js +38 -0
  28. package/dist/src/agent/copy.js +42 -0
  29. package/dist/src/agent/download.js +39 -0
  30. package/dist/src/agent/env.js +23 -0
  31. package/dist/src/agent/models.js +90 -0
  32. package/dist/src/agent/npm.js +55 -0
  33. package/dist/src/agent/oneturn.js +48 -0
  34. package/dist/src/agent/pinned.js +79 -0
  35. package/dist/src/agent/process.js +76 -0
  36. package/dist/src/agent/projectRules.js +54 -0
  37. package/dist/src/agent/providers.js +39 -0
  38. package/dist/src/agent/versions.js +89 -0
  39. package/dist/src/agent/windows.js +39 -0
  40. package/dist/src/ansi.js +6 -0
  41. package/dist/src/autostart.js +80 -0
  42. package/dist/src/chain/answers.js +26 -0
  43. package/dist/src/chain/archive.js +25 -0
  44. package/dist/src/chain/carry.js +97 -0
  45. package/dist/src/chain/cleanup.js +134 -0
  46. package/dist/src/chain/dispatcher.js +800 -0
  47. package/dist/src/chain/fix.js +55 -0
  48. package/dist/src/chain/forward.js +195 -0
  49. package/dist/src/chain/handover.js +350 -0
  50. package/dist/src/chain/launches.js +193 -0
  51. package/dist/src/chain/limitNotice.js +30 -0
  52. package/dist/src/chain/lists.js +174 -0
  53. package/dist/src/chain/log.js +23 -0
  54. package/dist/src/chain/maincheck.js +449 -0
  55. package/dist/src/chain/mainci.js +116 -0
  56. package/dist/src/chain/mainfix.js +180 -0
  57. package/dist/src/chain/mainwait.js +73 -0
  58. package/dist/src/chain/milestones.js +67 -0
  59. package/dist/src/chain/model.js +6 -0
  60. package/dist/src/chain/moves.js +100 -0
  61. package/dist/src/chain/order.js +266 -0
  62. package/dist/src/chain/overage.js +44 -0
  63. package/dist/src/chain/pauses.js +81 -0
  64. package/dist/src/chain/places.js +42 -0
  65. package/dist/src/chain/prompts.js +199 -0
  66. package/dist/src/chain/relay.js +153 -0
  67. package/dist/src/chain/restartGuard.js +17 -0
  68. package/dist/src/chain/reviews.js +316 -0
  69. package/dist/src/chain/rules.js +59 -0
  70. package/dist/src/chain/shelved.js +33 -0
  71. package/dist/src/chain/skippedown.js +20 -0
  72. package/dist/src/chain/snapshots.js +37 -0
  73. package/dist/src/chain/start.js +52 -0
  74. package/dist/src/chain/store.js +146 -0
  75. package/dist/src/chain/unchanged.js +71 -0
  76. package/dist/src/changelog/diff.js +118 -0
  77. package/dist/src/changelog/draft.js +59 -0
  78. package/dist/src/changelog/drafts.js +121 -0
  79. package/dist/src/changelog/file.js +146 -0
  80. package/dist/src/changelog/store.js +75 -0
  81. package/dist/src/ci/adapters.js +65 -0
  82. package/dist/src/ci/forgejo-profile.js +86 -0
  83. package/dist/src/ci/forgejo.js +62 -0
  84. package/dist/src/ci/github.js +53 -0
  85. package/dist/src/ci/gitlab.js +62 -0
  86. package/dist/src/ci/host.js +14 -0
  87. package/dist/src/ci/http.js +71 -0
  88. package/dist/src/ci/model.js +41 -0
  89. package/dist/src/ci/protection-audit.js +49 -0
  90. package/dist/src/ci/protection-model.js +1 -0
  91. package/dist/src/ci/protection.js +75 -0
  92. package/dist/src/ci/remote.js +105 -0
  93. package/dist/src/ci/report.js +114 -0
  94. package/dist/src/ci/shared.js +74 -0
  95. package/dist/src/ci/verdict.js +61 -0
  96. package/dist/src/ci/watcher.js +188 -0
  97. package/dist/src/ci/watches.js +116 -0
  98. package/dist/src/ci/wiring.js +26 -0
  99. package/dist/src/claude/adapter.js +174 -0
  100. package/dist/src/claude/calls.js +88 -0
  101. package/dist/src/claude/cli.js +451 -0
  102. package/dist/src/claude/context.js +33 -0
  103. package/dist/src/claude/instructions.js +12 -0
  104. package/dist/src/claude/leftovers.js +32 -0
  105. package/dist/src/claude/name.js +2 -0
  106. package/dist/src/claude/release.js +69 -0
  107. package/dist/src/claude/sdk/executor.js +214 -0
  108. package/dist/src/claude/sdk/host.js +422 -0
  109. package/dist/src/claude/sdk/pipe.js +134 -0
  110. package/dist/src/claude/sdk/registry.js +88 -0
  111. package/dist/src/claude/sdk/release.js +26 -0
  112. package/dist/src/claude/sdk/types.js +1 -0
  113. package/dist/src/claude/tree.js +103 -0
  114. package/dist/src/claude/trust.js +12 -0
  115. package/dist/src/cli-args.js +36 -0
  116. package/dist/src/cli-check/lease.js +35 -0
  117. package/dist/src/cli-check/run.js +381 -0
  118. package/dist/src/cli-check/scheduler.js +134 -0
  119. package/dist/src/cli-check/support.js +105 -0
  120. package/dist/src/cli.js +196 -0
  121. package/dist/src/codex/adapter.js +112 -0
  122. package/dist/src/codex/appserver.js +68 -0
  123. package/dist/src/codex/cli.js +360 -0
  124. package/dist/src/codex/mcp.js +8 -0
  125. package/dist/src/codex/name.js +2 -0
  126. package/dist/src/codex/plugins.js +137 -0
  127. package/dist/src/codex/process.js +81 -0
  128. package/dist/src/codex/protection.js +32 -0
  129. package/dist/src/codex/release.js +33 -0
  130. package/dist/src/codex/rollout.js +478 -0
  131. package/dist/src/codex/rolloutImages.js +49 -0
  132. package/dist/src/codex/rules.js +141 -0
  133. package/dist/src/codex/tail.js +53 -0
  134. package/dist/src/codex/turns.js +140 -0
  135. package/dist/src/codex/usage.js +88 -0
  136. package/dist/src/codex/version.js +2 -0
  137. package/dist/src/config-form.js +359 -0
  138. package/dist/src/config-store.js +184 -0
  139. package/dist/src/config.js +524 -0
  140. package/dist/src/copilot/adapter.js +121 -0
  141. package/dist/src/copilot/cli.js +298 -0
  142. package/dist/src/copilot/errors.js +50 -0
  143. package/dist/src/copilot/home.js +84 -0
  144. package/dist/src/copilot/machine.js +41 -0
  145. package/dist/src/copilot/name.js +2 -0
  146. package/dist/src/copilot/release.js +30 -0
  147. package/dist/src/copilot/tail.js +57 -0
  148. package/dist/src/copilot/transcript.js +385 -0
  149. package/dist/src/diagnostics/start-watch.js +77 -0
  150. package/dist/src/env-file.js +91 -0
  151. package/dist/src/events.js +150 -0
  152. package/dist/src/git/changelog.js +38 -0
  153. package/dist/src/git/changes.js +10 -0
  154. package/dist/src/git/merged.js +36 -0
  155. package/dist/src/git/run.js +25 -0
  156. package/dist/src/git/setup.js +100 -0
  157. package/dist/src/git/slot.js +99 -0
  158. package/dist/src/git/sync.js +72 -0
  159. package/dist/src/git/types.js +1 -0
  160. package/dist/src/git/work.js +119 -0
  161. package/dist/src/git/worktree.js +216 -0
  162. package/dist/src/github/adapter.js +30 -0
  163. package/dist/src/github/model.js +104 -0
  164. package/dist/src/github/profile.js +50 -0
  165. package/dist/src/github/source.js +438 -0
  166. package/dist/src/github/trust.js +25 -0
  167. package/dist/src/github/uploads.js +36 -0
  168. package/dist/src/gitlab/adapter.js +51 -0
  169. package/dist/src/gitlab/api.js +71 -0
  170. package/dist/src/gitlab/batch.js +114 -0
  171. package/dist/src/gitlab/hierarchy.js +59 -0
  172. package/dist/src/gitlab/model.js +56 -0
  173. package/dist/src/gitlab/profile.js +25 -0
  174. package/dist/src/gitlab/source.js +246 -0
  175. package/dist/src/gitlab/uploads.js +65 -0
  176. package/dist/src/guard.js +141 -0
  177. package/dist/src/huly/adapter.js +34 -0
  178. package/dist/src/huly/client.js +158 -0
  179. package/dist/src/huly/credentials.js +27 -0
  180. package/dist/src/huly/ids.js +16 -0
  181. package/dist/src/huly/labels.js +32 -0
  182. package/dist/src/huly/model.js +191 -0
  183. package/dist/src/huly/profile.js +73 -0
  184. package/dist/src/huly/quiet.js +17 -0
  185. package/dist/src/huly/source.js +327 -0
  186. package/dist/src/intake/github.js +73 -0
  187. package/dist/src/intake/intake.js +113 -0
  188. package/dist/src/intake/model.js +72 -0
  189. package/dist/src/jira/adapter.js +35 -0
  190. package/dist/src/jira/adf.js +253 -0
  191. package/dist/src/jira/api.js +96 -0
  192. package/dist/src/jira/mcp.js +134 -0
  193. package/dist/src/jira/model.js +111 -0
  194. package/dist/src/jira/profile.js +17 -0
  195. package/dist/src/jira/source.js +320 -0
  196. package/dist/src/jsonc.js +15 -0
  197. package/dist/src/language.js +4 -0
  198. package/dist/src/limits/chatgpt.js +144 -0
  199. package/dist/src/limits/copilot.js +117 -0
  200. package/dist/src/limits/model.js +183 -0
  201. package/dist/src/limits/overage-session.js +9 -0
  202. package/dist/src/limits/overage.js +74 -0
  203. package/dist/src/limits/recovery.js +23 -0
  204. package/dist/src/limits/reset.js +81 -0
  205. package/dist/src/limits/retry.js +45 -0
  206. package/dist/src/limits/saved.js +17 -0
  207. package/dist/src/limits/spending.js +15 -0
  208. package/dist/src/limits/usage.js +53 -0
  209. package/dist/src/limits/watcher.js +269 -0
  210. package/dist/src/linear/adapter.js +50 -0
  211. package/dist/src/linear/model.js +89 -0
  212. package/dist/src/linear/profile.js +45 -0
  213. package/dist/src/linear/source.js +423 -0
  214. package/dist/src/locations.js +53 -0
  215. package/dist/src/main.js +76 -0
  216. package/dist/src/notice/en.js +489 -0
  217. package/dist/src/notice/notice.js +93 -0
  218. package/dist/src/opencode/adapter.js +121 -0
  219. package/dist/src/opencode/cli.js +433 -0
  220. package/dist/src/opencode/db.js +103 -0
  221. package/dist/src/opencode/home.js +85 -0
  222. package/dist/src/opencode/log.js +87 -0
  223. package/dist/src/opencode/models.js +51 -0
  224. package/dist/src/opencode/name.js +2 -0
  225. package/dist/src/opencode/protection-plugin.js +27 -0
  226. package/dist/src/opencode/release.js +29 -0
  227. package/dist/src/opencode/support.js +14 -0
  228. package/dist/src/opencode/tail.js +42 -0
  229. package/dist/src/opencode/transcript.js +331 -0
  230. package/dist/src/opencode/usage.js +41 -0
  231. package/dist/src/packages/check.js +146 -0
  232. package/dist/src/packages/gateway.js +496 -0
  233. package/dist/src/packages/keys.js +78 -0
  234. package/dist/src/packages/launch.js +99 -0
  235. package/dist/src/packages/model.js +61 -0
  236. package/dist/src/packages/npm.js +115 -0
  237. package/dist/src/packages/plugins.js +162 -0
  238. package/dist/src/packages/service.js +615 -0
  239. package/dist/src/packages/settings.js +25 -0
  240. package/dist/src/packages/skills.js +132 -0
  241. package/dist/src/packages/store.js +39 -0
  242. package/dist/src/packages/version.js +26 -0
  243. package/dist/src/patch.js +1 -0
  244. package/dist/src/paths.js +83 -0
  245. package/dist/src/profiles/control.js +32 -0
  246. package/dist/src/profiles/mcp.js +29 -0
  247. package/dist/src/profiles/model.js +97 -0
  248. package/dist/src/profiles/oauth-error.js +21 -0
  249. package/dist/src/profiles/oauth-protocol.js +106 -0
  250. package/dist/src/profiles/oauth-response.js +12 -0
  251. package/dist/src/profiles/oauth.js +228 -0
  252. package/dist/src/profiles/request.js +20 -0
  253. package/dist/src/profiles/routes.js +31 -0
  254. package/dist/src/profiles/rules.js +54 -0
  255. package/dist/src/profiles/service.js +471 -0
  256. package/dist/src/profiles/store.js +125 -0
  257. package/dist/src/projects/add.js +11 -0
  258. package/dist/src/projects/check.js +174 -0
  259. package/dist/src/projects/colors.js +16 -0
  260. package/dist/src/projects/folder.js +30 -0
  261. package/dist/src/projects/remotes.js +109 -0
  262. package/dist/src/projects/removal.js +14 -0
  263. package/dist/src/projects/standards.js +31 -0
  264. package/dist/src/review/events.js +27 -0
  265. package/dist/src/review/feedback.js +64 -0
  266. package/dist/src/review/forgejo.js +110 -0
  267. package/dist/src/review/github.js +69 -0
  268. package/dist/src/review/gitlab.js +118 -0
  269. package/dist/src/review/model.js +25 -0
  270. package/dist/src/review/open.js +141 -0
  271. package/dist/src/review/refusals.js +20 -0
  272. package/dist/src/review/wiring.js +93 -0
  273. package/dist/src/secrets/catalog.js +34 -0
  274. package/dist/src/secrets/service.js +168 -0
  275. package/dist/src/server-guard.js +51 -0
  276. package/dist/src/server-request.js +360 -0
  277. package/dist/src/server.js +576 -0
  278. package/dist/src/service/accounts.js +584 -0
  279. package/dist/src/service/chain.js +642 -0
  280. package/dist/src/service/cli-check.js +58 -0
  281. package/dist/src/service/hosting.js +508 -0
  282. package/dist/src/service/lifecycle.js +59 -0
  283. package/dist/src/service/stats.js +72 -0
  284. package/dist/src/service/trackers.js +239 -0
  285. package/dist/src/service.js +699 -0
  286. package/dist/src/sessions/activity.js +32 -0
  287. package/dist/src/sessions/agents.js +217 -0
  288. package/dist/src/sessions/approvals.js +105 -0
  289. package/dist/src/sessions/ask.js +234 -0
  290. package/dist/src/sessions/cache.js +14 -0
  291. package/dist/src/sessions/chat.js +60 -0
  292. package/dist/src/sessions/commandPrompt.js +31 -0
  293. package/dist/src/sessions/commands.js +29 -0
  294. package/dist/src/sessions/entry.js +104 -0
  295. package/dist/src/sessions/history.js +88 -0
  296. package/dist/src/sessions/holds.js +63 -0
  297. package/dist/src/sessions/inbox.js +101 -0
  298. package/dist/src/sessions/keepwarm.js +110 -0
  299. package/dist/src/sessions/launch.js +32 -0
  300. package/dist/src/sessions/lines.js +90 -0
  301. package/dist/src/sessions/lingering.js +26 -0
  302. package/dist/src/sessions/mail.js +36 -0
  303. package/dist/src/sessions/mailRelay.js +88 -0
  304. package/dist/src/sessions/mailbox.js +136 -0
  305. package/dist/src/sessions/model-prompts.js +16 -0
  306. package/dist/src/sessions/monitor.js +215 -0
  307. package/dist/src/sessions/note.js +30 -0
  308. package/dist/src/sessions/occupants.js +15 -0
  309. package/dist/src/sessions/owners.js +59 -0
  310. package/dist/src/sessions/paths.js +24 -0
  311. package/dist/src/sessions/permission.js +58 -0
  312. package/dist/src/sessions/place.js +37 -0
  313. package/dist/src/sessions/prompt.js +16 -0
  314. package/dist/src/sessions/protection.js +88 -0
  315. package/dist/src/sessions/remote.js +41 -0
  316. package/dist/src/sessions/setup.js +57 -0
  317. package/dist/src/sessions/share.js +6 -0
  318. package/dist/src/sessions/shareFile.js +32 -0
  319. package/dist/src/sessions/silence.js +25 -0
  320. package/dist/src/sessions/tail.js +79 -0
  321. package/dist/src/sessions/title.js +67 -0
  322. package/dist/src/sessions/transcript.js +477 -0
  323. package/dist/src/sessions/transient.js +4 -0
  324. package/dist/src/sessions/watch.js +78 -0
  325. package/dist/src/settings/changes.js +15 -0
  326. package/dist/src/settings/defaults.js +25 -0
  327. package/dist/src/settings/file.js +197 -0
  328. package/dist/src/settings/import.js +93 -0
  329. package/dist/src/settings/registry.js +422 -0
  330. package/dist/src/settings/resolve.js +80 -0
  331. package/dist/src/settings/store.js +102 -0
  332. package/dist/src/setup.js +7 -0
  333. package/dist/src/state.js +1 -0
  334. package/dist/src/stats/base-sql.js +58 -0
  335. package/dist/src/stats/break-labels.js +53 -0
  336. package/dist/src/stats/calibration.js +43 -0
  337. package/dist/src/stats/checkpoint.js +66 -0
  338. package/dist/src/stats/connect.js +49 -0
  339. package/dist/src/stats/diff.js +88 -0
  340. package/dist/src/stats/mode.js +25 -0
  341. package/dist/src/stats/model.js +108 -0
  342. package/dist/src/stats/reader.js +563 -0
  343. package/dist/src/stats/rule-imports.js +106 -0
  344. package/dist/src/stats/rules-reader.js +257 -0
  345. package/dist/src/stats/rules.js +324 -0
  346. package/dist/src/stats/steps-reader.js +108 -0
  347. package/dist/src/stats/steps.js +159 -0
  348. package/dist/src/stats/tally-schema.js +13 -0
  349. package/dist/src/stats/tally.js +139 -0
  350. package/dist/src/stats/task-checkpoint.js +9 -0
  351. package/dist/src/storage/atomic.js +41 -0
  352. package/dist/src/storage/claim.js +139 -0
  353. package/dist/src/storage/copy.js +69 -0
  354. package/dist/src/storage/database.js +405 -0
  355. package/dist/src/storage/json-folder.js +47 -0
  356. package/dist/src/storage/memory.js +15 -0
  357. package/dist/src/storage/migrations.js +373 -0
  358. package/dist/src/storage/owner.js +114 -0
  359. package/dist/src/storage/package-settings.js +33 -0
  360. package/dist/src/storage/projects.js +23 -0
  361. package/dist/src/storage/schema.js +21 -0
  362. package/dist/src/storage/service-state.js +9 -0
  363. package/dist/src/storage/state-files.js +222 -0
  364. package/dist/src/tools/model.js +1 -0
  365. package/dist/src/tools/panel.js +197 -0
  366. package/dist/src/tools/received.js +114 -0
  367. package/dist/src/tools/sessionSettings.js +15 -0
  368. package/dist/src/tools/settings.js +109 -0
  369. package/dist/src/tracker/adapters.js +28 -0
  370. package/dist/src/tracker/card.js +32 -0
  371. package/dist/src/tracker/closer.js +76 -0
  372. package/dist/src/tracker/closes.js +45 -0
  373. package/dist/src/tracker/comments.js +24 -0
  374. package/dist/src/tracker/control.js +138 -0
  375. package/dist/src/tracker/diagnostics.js +38 -0
  376. package/dist/src/tracker/health.js +1 -0
  377. package/dist/src/tracker/labels.js +25 -0
  378. package/dist/src/tracker/link.js +125 -0
  379. package/dist/src/tracker/model.js +82 -0
  380. package/dist/src/tracker/move-check.js +14 -0
  381. package/dist/src/tracker/mover.js +67 -0
  382. package/dist/src/tracker/parents.js +32 -0
  383. package/dist/src/tracker/poller.js +60 -0
  384. package/dist/src/tracker/questions.js +127 -0
  385. package/dist/src/tracker/readers.js +95 -0
  386. package/dist/src/tracker/source.js +66 -0
  387. package/dist/src/tracker/start.js +256 -0
  388. package/dist/src/tracker/statuses.js +22 -0
  389. package/dist/src/tracker/trackers.js +148 -0
  390. package/dist/src/tracker/vocabulary.js +33 -0
  391. package/dist/src/updates/checkout.js +16 -0
  392. package/dist/src/updates/control.js +139 -0
  393. package/dist/src/updates/launch.js +40 -0
  394. package/dist/src/updates/model.js +105 -0
  395. package/dist/src/updates/prefix.js +19 -0
  396. package/dist/src/updates/release.js +100 -0
  397. package/dist/src/updates/run.js +64 -0
  398. package/dist/src/updates/runner.js +183 -0
  399. package/dist/src/updates/settle.js +18 -0
  400. package/dist/src/updates/steps.js +63 -0
  401. package/dist/src/updates/updater.js +205 -0
  402. package/dist/src/updates/watcher.js +86 -0
  403. package/dist/tally/src/classify.js +144 -0
  404. package/dist/tally/src/closed-tasks.js +25 -0
  405. package/dist/tally/src/collect-worker.js +17 -0
  406. package/dist/tally/src/collect.js +212 -0
  407. package/dist/tally/src/compact.js +33 -0
  408. package/dist/tally/src/config.js +150 -0
  409. package/dist/tally/src/copilot-turns.js +101 -0
  410. package/dist/tally/src/copilot.js +173 -0
  411. package/dist/tally/src/db.js +562 -0
  412. package/dist/tally/src/handoffs.js +126 -0
  413. package/dist/tally/src/host-worker.js +18 -0
  414. package/dist/tally/src/huly-profiles.js +38 -0
  415. package/dist/tally/src/huly.js +123 -0
  416. package/dist/tally/src/limits.js +21 -0
  417. package/dist/tally/src/load.js +89 -0
  418. package/dist/tally/src/local.js +47 -0
  419. package/dist/tally/src/main.js +39 -0
  420. package/dist/tally/src/migrations/2-false-rule-deletions.sql +41 -0
  421. package/dist/tally/src/migrations/3-gpt-family.sql +20 -0
  422. package/dist/tally/src/migrations/4-compactions.sql +49 -0
  423. package/dist/tally/src/migrations/5-milestones.sql +25 -0
  424. package/dist/tally/src/migrations/6-task-sources.sql +6 -0
  425. package/dist/tally/src/migrations/7-task-followups.sql +28 -0
  426. package/dist/tally/src/migrations/8-handoff-headings.sql +12 -0
  427. package/dist/tally/src/migrations/9-codex-stray-counts.sql +15 -0
  428. package/dist/tally/src/migrations.js +80 -0
  429. package/dist/tally/src/opencode-turns.js +79 -0
  430. package/dist/tally/src/opencode.js +115 -0
  431. package/dist/tally/src/parse.js +192 -0
  432. package/dist/tally/src/prompt.js +19 -0
  433. package/dist/tally/src/rollout-turns.js +181 -0
  434. package/dist/tally/src/rollout.js +159 -0
  435. package/dist/tally/src/rule-changes.js +39 -0
  436. package/dist/tally/src/rule-history.js +88 -0
  437. package/dist/tally/src/rule-hook-size.js +23 -0
  438. package/dist/tally/src/rule-hook.js +177 -0
  439. package/dist/tally/src/rule-imports.js +76 -0
  440. package/dist/tally/src/rule-parse.js +178 -0
  441. package/dist/tally/src/rule-repair.js +27 -0
  442. package/dist/tally/src/rule-scan.js +179 -0
  443. package/dist/tally/src/rule-steps.js +144 -0
  444. package/dist/tally/src/rules.js +230 -0
  445. package/dist/tally/src/scan.js +59 -0
  446. package/dist/tally/src/schema.sql +702 -0
  447. package/dist/tally/src/signs.js +179 -0
  448. package/dist/tally/src/step-runs.js +274 -0
  449. package/dist/tally/src/steps.js +140 -0
  450. package/dist/tally/src/stream.js +73 -0
  451. package/dist/tally/src/task.js +16 -0
  452. package/dist/tally/src/turns.js +114 -0
  453. package/dist/web/apple-touch-icon.png +0 -0
  454. package/dist/web/assets/Loader-BoP9PE7x.js +31 -0
  455. package/dist/web/assets/Loader-DJJTBsaF.css +1 -0
  456. package/dist/web/assets/TaskCard-Br2QIllc.css +1 -0
  457. package/dist/web/assets/TaskCard-DNn-Qm8d.js +2 -0
  458. package/dist/web/assets/TranscriptWindow-2afYjYo9.css +1 -0
  459. package/dist/web/assets/TranscriptWindow-CFE2kEuL.js +4 -0
  460. package/dist/web/assets/feedLine-C_AM4QQT.js +1 -0
  461. package/dist/web/assets/index-BY7ediF2.js +25 -0
  462. package/dist/web/assets/index-Di7tEbPf.css +1 -0
  463. package/dist/web/assets/media-DR8nAbPy.css +1 -0
  464. package/dist/web/assets/media-DfHUEnGn.js +1 -0
  465. package/dist/web/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  466. package/dist/web/assets/useModels-1oIk0mDX.css +1 -0
  467. package/dist/web/assets/useModels-BmTGsRQz.js +15 -0
  468. package/dist/web/favicon.svg +1 -0
  469. package/dist/web/icon-192.png +0 -0
  470. package/dist/web/icon-512.png +0 -0
  471. package/dist/web/icon-maskable-512.png +0 -0
  472. package/dist/web/index.html +25 -0
  473. package/dist/web/manifest.webmanifest +16 -0
  474. package/dist/web/stats-preview.png +0 -0
  475. package/even-keel.config.example.json +59 -0
  476. package/package.json +118 -7
  477. package/plugins/checkpoint/.claude-plugin/plugin.json +9 -0
  478. package/plugins/checkpoint/README.md +85 -0
  479. package/plugins/checkpoint/hooks/checkpoint-mark.mjs +26 -0
  480. package/plugins/checkpoint/hooks/context-checkpoint.mjs +28 -0
  481. package/plugins/checkpoint/hooks/evenkeel.mjs +117 -0
  482. package/plugins/checkpoint/hooks/handoff.mjs +9 -0
  483. package/plugins/checkpoint/hooks/hooks.json +120 -0
  484. package/plugins/checkpoint/hooks/mailbox.mjs +36 -0
  485. package/plugins/checkpoint/hooks/merge-check.mjs +28 -0
  486. package/plugins/checkpoint/hooks/merges.mjs +103 -0
  487. package/plugins/checkpoint/hooks/nudges.mjs +78 -0
  488. package/plugins/checkpoint/hooks/permission.mjs +49 -0
  489. package/plugins/checkpoint/hooks/schema-hint.mjs +53 -0
  490. package/plugins/checkpoint/hooks/standard-gate.mjs +51 -0
  491. package/plugins/checkpoint/hooks/standards.mjs +258 -0
  492. package/plugins/checkpoint/opencode/checkpoint.mjs +85 -0
  493. package/plugins/github/.claude-plugin/plugin.json +9 -0
  494. package/plugins/github/.mcp.json +12 -0
  495. package/plugins/github/README.md +73 -0
  496. package/plugins/github/skills/task/SKILL.md +62 -0
  497. package/plugins/gitlab/.claude-plugin/plugin.json +7 -0
  498. package/plugins/gitlab/.mcp.json +13 -0
  499. package/plugins/gitlab/README.md +29 -0
  500. package/plugins/gitlab/skills/task/SKILL.md +55 -0
  501. package/plugins/huly/.claude-plugin/plugin.json +9 -0
  502. package/plugins/huly/.mcp.json +11 -0
  503. package/plugins/huly/README.md +60 -0
  504. package/plugins/huly/hooks/calibration-context.mjs +44 -0
  505. package/plugins/huly/hooks/calibration.mjs +80 -0
  506. package/plugins/huly/hooks/check-marks.mjs +23 -0
  507. package/plugins/huly/hooks/hooks.json +26 -0
  508. package/plugins/huly/hooks/marks.mjs +58 -0
  509. package/plugins/huly/hooks/show-calibration.mjs +19 -0
  510. package/plugins/huly/opencode/huly.mjs +31 -0
  511. package/plugins/huly/skills/setup/SKILL.md +51 -0
  512. package/plugins/huly/skills/split/SKILL.md +53 -0
  513. package/plugins/huly/skills/task/SKILL.md +81 -0
  514. package/plugins/jira/.claude-plugin/plugin.json +7 -0
  515. package/plugins/jira/.mcp.json +11 -0
  516. package/plugins/jira/README.md +39 -0
  517. package/plugins/jira/skills/task/SKILL.md +63 -0
  518. package/plugins/linear/.claude-plugin/plugin.json +9 -0
  519. package/plugins/linear/.mcp.json +11 -0
  520. package/plugins/linear/README.md +71 -0
  521. package/plugins/linear/skills/task/SKILL.md +62 -0
  522. package/plugins/tally/.claude-plugin/plugin.json +9 -0
  523. package/plugins/tally/README.md +42 -0
  524. package/plugins/tally/bin/locate.mjs +48 -0
  525. package/plugins/tally/bin/sql.mjs +21 -0
  526. package/plugins/tally/skills/session-audit/SKILL.md +333 -0
  527. package/service/EvenKeelHost.cs +133 -0
  528. package/service/EvenKeelService.cs +271 -0
  529. package/service/EvenKeelTray.cs +258 -0
  530. package/service/install.ps1 +95 -0
  531. package/service/install.sh +143 -0
  532. package/service/uninstall.ps1 +19 -0
  533. package/service/uninstall.sh +37 -0
  534. package/statusline/statusline.mjs +46 -0
  535. package/tally/bin/rule-hook.mjs +26 -0
  536. package/tally/bin/sql.mjs +166 -0
  537. package/tally/tally.config.json +59 -0
package/README.md CHANGED
@@ -1,5 +1,201 @@
1
- # Even Keel
1
+ <h1>
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="assets/logo-dark.svg">
4
+ <img src="assets/logo-light.svg" alt="Even Keel" height="40">
5
+ </picture>
6
+ </h1>
2
7
 
3
- Runs Claude Code and Codex sessions on tracker issues one after another, in the order their dependencies allow, paced to the limits of a subscription.
8
+ **You write the tickets. It takes the night shift.** Even Keel takes the open tasks of a project from your tracker, in the order of their `blocks` links, and works through them one at a time with [Claude Code](https://docs.claude.com/en/docs/claude-code/overview), [Codex](https://github.com/openai/codex), [OpenCode](https://opencode.ai) or [GitHub Copilot](https://github.com/github/copilot-cli) while you do something else. It waits out a spent usage limit, hands a long session over to a fresh one and keeps going while you are away. Only what really needs you — a question, a decision — waits for you: on its page, in the tracker or on your phone.
4
9
 
5
- **This package only holds the name.** Even Keel is installed from its repository for now: [github.com/NamerekT/even-keel](https://github.com/NamerekT/even-keel). An install through npm is planned; until then the `0.0.x` versions are placeholders and install nothing.
10
+ **Already inside, so you don't glue five tools together:**
11
+
12
+ - **Usage limits.** A spent 5-hour or weekly limit is waited out: the session is resumed after the reset while its prompt cache is warm, or handed to a fresh one — no work lost, no 3 a.m. restart.
13
+ - **A full context, your way.** A session past its context threshold writes a handoff note and a fresh one takes over in the same worktree — or leaves it to Claude Code's auto-compact, per project or per task by a [label](docs/guide/reference.md#configuration). The statistics count both, and the session audit skill compares what each cost on your own tasks.
14
+ - **What each task cost.** Built-in statistics (optional): cost and time per task, where sessions got stuck, how full the context ran, and before/after every change to your rule files.
15
+ - **Every account on one page.** Claude, Codex, OpenCode and Copilot — subscriptions and API keys — each with its limits or spend and what runs on it right now. You pick the account a project runs on.
16
+ - **`CLAUDE.md` or `AGENTS.md`.** One instruction file reaches every agent: Claude Code gets your `AGENTS.md`, Codex your `CLAUDE.md`.
17
+
18
+ It runs as a service on your own machine, next to your repositories, on your own subscription, through the unmodified `claude`, `codex`, `opencode` and `copilot` CLIs. Trackers out of the box: [Huly](https://huly.io), GitHub Issues, [Linear](https://linear.app), GitLab Issues (GitLab.com or your own server) and Jira Cloud.
19
+
20
+ ![A project's chain: one task done, one session working, the rest queued; the panel on the right shows the session's stage, context fill and current action](assets/screens/chain.png)
21
+
22
+ ## Sequential by design
23
+
24
+ Most agent orchestrators sell width: ten agents in parallel, each in its own worktree. Even Keel deliberately runs the tasks of a project one after another. Why:
25
+
26
+ - **Backlog tasks are connected.** A parallel agent working on task B decides on its own what task A, not yet done, will look like. The sequential agent on B reads A's merged code.
27
+ - **The bottleneck is human attention, not typing speed.** Questions, reviews and merges are yours to handle. Ten agents produce ten queues of them at once.
28
+ - **A worktree isolates the work, not the result.** Conflicts, especially semantic ones that git does not see, move to merge time, where they are more expensive.
29
+ - **The subscription limit is shared.** Parallel agents spend it sooner, not on more; and each of them reads the same area of the code again.
30
+
31
+ Even Keel wins in time, not in width: the agent keeps working while you are away, and the chain survives what usually stops it — the usage limit running out, a context grown too large. That steady going is what the name means. Different projects are different repositories, so their chains can run side by side.
32
+
33
+ ![A task goes to Done, its worktree is cleaned up and the next task's session starts](assets/screens/chain.gif)
34
+
35
+ ## What it does
36
+
37
+ - **A chain per project.** Open blockers first, then the order you drag on the page, then milestone and priority. A task labelled `Manual` is yours and never enters it.
38
+ - **Several trackers per project.** Tasks from a second tracker or account — say the GitHub issues others file next to your private Huly project — join the same chain.
39
+ - **Release branches.** A second chain on the same tracker project takes a released version's milestone in its own clone, and its fixes are merged forward into your development branch while that chain stands.
40
+ - **A session per task**, in its own git worktree. The session does the task and merges; Even Keel closes the task, cleans up and starts the next. A task with nothing to merge (already done, research) the session marks `NO CHANGES: <why>`, and Even Keel closes it all the same.
41
+ - **Questions** wait for you in the page's chat, as a tracker comment or in Claude's Remote Control. You can also write to a session while it works.
42
+ - **Unattended mode** for nights: sessions ask nothing and park what a question holds.
43
+ - **CI.** The next task waits for the CI's verdict on `main`, and a red `main` is fixed first. No CI — your own check commands.
44
+ - **Pull requests (experimental).** Instead of merging, Even Keel can push a task's branch and open a pull request on GitHub or Forgejo, or a merge request on GitLab; review feedback, failed checks and conflicts go back to the task's session.
45
+ - **Plugins and MCP servers** per project, with their keys kept behind a local gateway.
46
+ - **Accounts, one per project and provider.** OpenCode signs in with ChatGPT, GitHub Copilot, an API key or runs its free models; the Copilot CLI with your sign-in or a token each. Claude sessions on an API key run through the Agent SDK and outlive a restart of the service with their questions open; a subscription can switch to it too.
47
+ - **A session audit skill** reads the statistics and proposes what to change in your rules.
48
+ - **The page:** the chain, each session's stage, current action and context fill, task cards, changelog drafts; in English or Russian.
49
+
50
+ | A question in the page's chat | A spent limit, waited out with the cache warm | Statistics |
51
+ | --- | --- | --- |
52
+ | ![The page's chat with a session's question and its options](assets/screens/question.png) | ![The session hit the usage limit; the panel says it goes on after the reset with the cache still warm](assets/screens/limit.png) | ![The stats page: cost, weekly limits and spend by day per project](assets/screens/stats.png) |
53
+
54
+ Every feature in full: [docs/guide/features.md](docs/guide/features.md).
55
+
56
+ ## Requirements
57
+
58
+ - **Node.js 24** or newer ([nodejs.org](https://nodejs.org/en/download); on Linux nvm needs no `sudo`).
59
+ - **git** ([git-scm.com](https://git-scm.com/downloads)) with your name and email set (`git config --global user.name "John Doe"`, `git config --global user.email john@example.com`). Nothing works without it: every task runs in its own git worktree, and sessions commit and merge under that name.
60
+ - **Claude Code CLI** ([install](https://docs.claude.com/en/docs/claude-code/setup)), signed in with a Claude subscription (`claude` once in a terminal). **Codex CLI** only for projects or tasks on Codex: `npm install -g @openai/codex`, signed in with `codex login` or from the accounts page. **OpenCode** (preview) only for projects or tasks on it: `npm install -g opencode-ai`, signed in from the accounts page; checked with its free `opencode/big-pickle`. **GitHub Copilot CLI** (preview) only for projects or tasks on it: `winget install GitHub.Copilot` (or `npm install -g @github/copilot`), signed in with `copilot login` or a token on the accounts page.
61
+ - **A tracker:** a Huly workspace, GitHub or GitLab projects with Issues, a Linear workspace or a Jira Cloud site.
62
+ - **Windows, Linux or macOS.** Windows is where it runs every day; Linux and macOS (Apple silicon) were checked end to end. On Windows it starts at your sign-in with no administrator; only the older Windows service needs **PowerShell 7**. The MSI installer of Node can sit on "Preparing to install…" for over 10 minutes: wait it out.
63
+
64
+ ## Before you run it
65
+
66
+ - **Even Keel depends on undocumented behaviour of the agents' CLIs**: their background sessions, transcripts, session files and the text of their commands. None of it is a public API. Chain sessions run on your CLI, and the page warns while its version is not the one Even Keel was last checked against; "CLI version → Checked" has Even Keel download that version and run sessions on it instead. Last checked against **Claude Code 2.1.316**, **codex-cli 0.166.0**, **OpenCode 1.19.5** and **Copilot CLI 1.0.105**. A CLI update may still break a part of Even Keel until it is adapted — please open an issue when it does. What exactly it relies on and the limits of the pin: [setup](docs/guide/setup.md#the-clis-it-depends-on).
67
+ - **Your subscription, the providers' terms.** Sessions run on the accounts you sign in with and wait out their limits: a project runs on one account per provider, and a spent limit is never got round by switching to another account ([docs/sessions/models.md](docs/sessions/models.md#run-profiles-btn-173-srcsessionslaunchts)). Claude sessions are governed by Anthropic's [Consumer Terms](https://www.anthropic.com/legal/consumer-terms) and [Usage Policy](https://www.anthropic.com/legal/aup) (the [Commercial Terms](https://www.anthropic.com/legal/commercial-terms) for Team, Enterprise and API keys). Anthropic asks products built on the Agent SDK to [use API key authentication](https://code.claude.com/docs/en/legal-and-compliance), so a subscription runs on the CLI's own background sessions unless you switch its card to the SDK yourself. Codex sessions are governed by OpenAI's [Terms of Use](https://openai.com/policies/terms-of-use/) and [Usage Policies](https://openai.com/policies/usage-policies/) when you sign in with ChatGPT (the [Services Agreement](https://openai.com/policies/services-agreement/) for Business, Enterprise, Edu and API keys), with the [Service Terms](https://openai.com/policies/service-terms/) on top. OpenAI's terms do not allow getting round its usage limits, so Even Keel refuses a second Codex account on the free plan. OpenCode sessions are governed by the terms of the provider you sign it in with; a Claude subscription is allowed only in Claude Code, so OpenCode reaches Claude only on an API key. Copilot sessions are governed by GitHub's [Terms of Service](https://docs.github.com/en/site-policy/github-terms/github-terms-of-service) and the [GitHub Copilot terms](https://github.com/customer-terms/github-copilot-product-specific-terms), which allow one free account per person. Even Keel does not share, pool or resell access: use accounts that are yours.
68
+ - **Sessions run without asking you on each tool call**: Claude sessions with `--permission-mode auto`, Codex sessions with `--approve-for-me`, or with no sandbox at all when `execution.codex` is `bypass`. Set up the permissions and hooks you want in your Claude Code and Codex settings before you start a chain.
69
+ - **Your sign-in stays with the CLIs.** Even Keel reads only the expiry dates from Claude's `.credentials.json`, and only the e-mail and the plan from Codex's `auth.json`; tokens are never copied or sent anywhere. It also removes the lock files the CLI leaves behind in the account folders. An API key you add is sent only to its own provider, to check it.
70
+ - **Some behaviour has never been checked live** — an early reset of Claude limits, long chains on Codex, OpenCode and Copilot under the service, a few tracker and CI paths. Read [What has not been verified](docs/unverified.md) before relying on any of them. What has been checked live — where, when, on which versions and with what result — is logged in [What has been verified](docs/verified.md).
71
+
72
+ ## Quick start
73
+
74
+ ```sh
75
+ npx even-keel
76
+ ```
77
+
78
+ This installs Even Keel globally with npm and sets it up as a service ([below](#as-a-service)). On Windows that is a start at your sign-in with an icon in the tray — no administrator, any terminal. To run it in a terminal instead: `npm install --global even-keel`, then `even-keel`. Its data — `.env`, the state and the logs — lives in `%LOCALAPPDATA%\even-keel`, `~/.local/share/even-keel` or `~/Library/Application Support/even-keel`.
79
+
80
+ From a clone, which keeps its data in its own folder:
81
+
82
+ ```sh
83
+ git clone https://github.com/NamerekT/even-keel.git
84
+ cd even-keel
85
+ npm install
86
+ npm run build
87
+ npm start
88
+ ```
89
+
90
+ Open `http://localhost:4747`. The first start walks a short setup — the languages, the agent and its account, a tracker profile, update checks, statistics — and then a wizard that adds a project: the agent, the repository folder, a tracker profile and the project in that tracker. A profile is the e-mail and password of a separate Huly account, a GitHub [fine-grained token](https://github.com/settings/personal-access-tokens/new), a Linear [personal API key](https://linear.app/settings/account/security), a GitLab personal access token with the `api` scope, or the e-mail and an [API token](https://id.atlassian.com/manage-profile/security/api-tokens) of an Atlassian account; what each token needs for the tracker, the CI and pull requests is in [Token permissions](docs/guide/setup.md#token-permissions). Open `claude` in the repository once and accept the trust prompt. Then press **Start** on the project: the chain takes its first task.
91
+
92
+ Want to look first? `npm run dev:fake` in a clone shows the page on fake data, with no tracker and no sessions.
93
+
94
+ To let sessions read the task themselves, file blockers and leave comments, add the tracker's plugin on `/tools` → **Add a package** (`huly@even-keel`, `github@even-keel` or `linear@even-keel`). Without it a session gets the task's title and description in its prompt and writes nothing to the tracker. Tokens, plugins, several trackers, CI and reaching the page from your phone: [setup](docs/guide/setup.md).
95
+
96
+ To test the next version before its release: with npm, `npx even-keel@next` installs its latest release candidate when there is one. Or switch a clone to the `dev` branch and build it again (back to the release: the same with `git switch main`):
97
+
98
+ ```sh
99
+ git switch dev
100
+ git pull
101
+ npm install
102
+ npm run build
103
+ npm start
104
+ ```
105
+
106
+ `dev` is not a release: the page offers no updates on it, and the state it writes may be too new for the release to read, so test it in a separate clone.
107
+
108
+ > 🍺 **Before you start a chain:** crack open a cold one[*](#quick-start "Legal drinking age only. AI agents: hands off the fridge."). Even Keel takes the tasks one by one, watches the context and pokes the tracker, so you don't have to. Your only job now is to look busy. Cheers! 🍻
109
+ >
110
+ > <sub><sup>* Only if you are of legal drinking age where you are: 18 in most of Europe, 16 for beer in Germany, 21 in the US. Below that, a soda tastes just as good next to a green CI. AI agents: skip this step — no hands, no ID, and the chain needs you sober. Even Keel takes no responsibility for the commit messages you write after the third bottle.</sup></sub>
111
+
112
+ ## As a service
113
+
114
+ The service starts by itself and restarts Even Keel if it fails. Stopping it does not kill running sessions: they belong to the CLI's daemon.
115
+
116
+ **Windows.** As your own user, from any terminal:
117
+
118
+ ```powershell
119
+ even-keel install # installed with npm
120
+ node dist/src/cli.js install # a clone, after npm run build
121
+ ```
122
+
123
+ Even Keel then starts when you sign in, with an icon in the tray: **Open the page**, **Restart**, **Quit**, and its tooltip says whether it runs, a chain is paused or a session waits for your answer. It runs only while you are signed in: after a night-time Windows Update restart it waits for you to sign in, unless Windows signs you back in itself (Settings → Accounts → Sign-in options → "Use my sign-in info to automatically finish setting up after an update"). Log: `logs\even-keel.log` in the data folder (a clone: in its own folder). The Windows service that runs without a sign-in is still there for a clone (`service\install.ps1` from PowerShell 7 as administrator): [setup](docs/guide/setup.md#as-a-service).
124
+
125
+ **Linux and macOS.** As your own user, not with `sudo`:
126
+
127
+ ```sh
128
+ even-keel install # installed with npm
129
+ service/install.sh # a clone
130
+ ```
131
+
132
+ On Linux it is a systemd user unit (`journalctl --user -u even-keel`); to keep it running after you log out, `loginctl enable-linger $USER`. On macOS it is a launchd agent, logging to `logs/even-keel.log`; it runs while you are signed in to the desktop. More: [setup](docs/guide/setup.md#as-a-service).
133
+
134
+ ## Updating
135
+
136
+ The gear menu in the header shows the installed version and tells you when a newer release is out.
137
+
138
+ - **From the page** (as a service, installed with npm or on a clean clone) — the update window lists what changes; **Update** waits for the chains to settle, installs the release (with npm, or a checkout and a build in a clone) and restarts the service, and goes back to the version you had if anything fails. **Revert** takes you back to the previous version the same way, unless the update changed the stored state's format.
139
+ - **Installed with npm, by hand** — `even-keel update` (stops the service, installs the latest release, starts it), or `npm install --global even-keel@<version>` with Even Keel stopped.
140
+ - **A clone as a service, by hand** — `git pull`, then run its install again.
141
+ - **A clone from a terminal** — stop Even Keel, then `git pull`, `npm ci`, `npm run build` and `npm start`.
142
+
143
+ Running sessions survive the update. Plugins, stored state and going back to an older version: [setup](docs/guide/setup.md#updating).
144
+
145
+ ## Uninstall
146
+
147
+ `even-keel uninstall` (a clone: `node dist/src/cli.js uninstall` or `service/uninstall.sh`) removes the service and nothing else; then `npm uninstall --global even-keel` and delete the data folder, or delete the clone. What stays in your repositories and your Claude Code config: [setup](docs/guide/setup.md#uninstall).
148
+
149
+ ## Configuration
150
+
151
+ The settings live in Even Keel's database and are edited on the page: a project's on its **Settings**, the shared ones behind the gear in the header. Every change applies at once, with no restart. `.env` holds the page's address and a few paths. Every variable and key: [docs/guide/reference.md](docs/guide/reference.md).
152
+
153
+ ## What comes next
154
+
155
+ The single executable once planned for v0.2 gave way to the npm package: an executable per platform meant its own build and release pipeline and a whole second install to check live on every system, while `npx even-keel` already installs Even Keel, sets up the service and updates it with one command.
156
+
157
+ Looked at, not promised: running in Docker, a cap on how much of the usage limit one task may spend.
158
+
159
+ ## Extending
160
+
161
+ ### Your own tracker
162
+
163
+ The chain never talks to a tracker directly: it reads tasks through an adapter. The contract, with what each field means and what to return when your tracker lacks something, is the JSDoc of [src/tracker/source.ts](src/tracker/source.ts) (`TrackerAdapter`, `TaskSource`). `src/github/` is a compact example, `src/huly/` a fuller one. Register the adapter in [src/tracker/adapters.ts](src/tracker/adapters.ts) and pick it with `tracker` in the config. `brief` and `complete` are optional: without them, sessions must learn the task and close it some other way. So are `card` and `ready`, the task card on the page and its **Move to Todo** button; without them the page links to the task in the tracker.
164
+
165
+ ### A plugin for it
166
+
167
+ A plugin gives sessions the tracker's MCP server and a skill for working a task. Use [plugins/huly](plugins/huly) as the pattern: `.claude-plugin/plugin.json`, `.mcp.json` with the server, `skills/task/SKILL.md`; add an entry to [.claude-plugin/marketplace.json](.claude-plugin/marketplace.json) and name the server in the adapter's `mcpServer`, so that Even Keel can check sessions see it. When the server's `${…}` placeholder names the adapter's key differently, declare it in the adapter's `mcpKeys` (`{ LINEAR_API_KEY: 'LINEAR_TOKEN' }`), so the gateway fills it from the project's profile; a value built from several keys, such as Jira's Basic header, goes in `mcpValues`. Raise the plugin's `version` on every change: installed copies are cached by version.
168
+
169
+ ### Where it could grow
170
+
171
+ - Other trackers: YouTrack, Jira Data Center…
172
+ - Your own classifier for the statistics, or a different model behind it.
173
+ - Other APIs and other agents: today Even Keel runs the Claude Code, Codex, OpenCode and GitHub Copilot CLIs and is built around the first two's subscription limits.
174
+
175
+ ## Development
176
+
177
+ `main` is always the latest release. The next version is built on `dev`, fixes of the released one on `release/v<x.y>` ([docs/releases.md](docs/releases.md)). Pull requests are not accepted for now: bugs and ideas go to issues ([CONTRIBUTING.md](CONTRIBUTING.md)).
178
+
179
+ ```sh
180
+ npm test # vitest, without the end-to-end scenarios
181
+ npm run test:e2e # the end-to-end scenarios of src/e2e/
182
+ npm run typecheck
183
+ npm run dev # the server, restarting on changes
184
+ npm run dev:web # the page with hot reload, proxying /api to a running server
185
+ npm run dev:fake # the page on fake data
186
+ npm run shot -- '?chain=ask' .cards # a dark screenshot of an element on fake data, into .temp/shot/
187
+ ```
188
+
189
+ ## Security
190
+
191
+ A vulnerability is reported privately, not as an issue: [SECURITY.md](SECURITY.md).
192
+
193
+ ## Support
194
+
195
+ Even Keel is free. If it saves you time, [Patreon](https://www.patreon.com/Namerek) keeps it going: patrons get the write-ups of how it is built and what each change to the rules cost, measured on its own statistics.
196
+
197
+ ## License
198
+
199
+ [MIT](LICENSE)
200
+
201
+ Even Keel is an independent project, not affiliated with or endorsed by Anthropic, OpenAI, GitHub or the makers of OpenCode. Claude and Claude Code are trademarks of Anthropic; Codex and OpenAI are trademarks of OpenAI; GitHub and GitHub Copilot are trademarks of GitHub.
@@ -0,0 +1,100 @@
1
+ import { CLAUDE } from '../claude/name.js';
2
+ import { fail, notice } from '../notice/notice.js';
3
+ export const APP_LABELS = { vscode: 'VS Code' };
4
+ const SETTING = 'claudeCode.environmentVariables';
5
+ const VARIABLE = 'CLAUDE_CONFIG_DIR';
6
+ const VSCODE_ENTRYPOINT = 'claude-vscode';
7
+ export function configDirOf(settings) {
8
+ const variables = parseSettings(settings)[SETTING];
9
+ if (!Array.isArray(variables))
10
+ return null;
11
+ const found = variables.find((each) => isVariable(each) && each.name === VARIABLE);
12
+ return found === undefined || found.value === '' ? null : found.value;
13
+ }
14
+ export function withConfigDir(settings, dir) {
15
+ const parsed = parseSettings(settings);
16
+ const current = Array.isArray(parsed[SETTING]) ? parsed[SETTING] : [];
17
+ const index = current.findIndex((each) => isVariable(each) && each.name === VARIABLE);
18
+ const next = [...current];
19
+ if (dir === null) {
20
+ if (index >= 0)
21
+ next.splice(index, 1);
22
+ }
23
+ else if (index >= 0) {
24
+ next[index] = { name: VARIABLE, value: dir };
25
+ }
26
+ else {
27
+ next.push({ name: VARIABLE, value: dir });
28
+ }
29
+ const result = { ...parsed, [SETTING]: next };
30
+ if (next.length === 0)
31
+ delete result[SETTING];
32
+ const eol = settings.includes('\r\n') ? '\r\n' : '\n';
33
+ const text = JSON.stringify(result, null, indentOf(settings)).replace(/\n/g, eol);
34
+ return settings.trim() === '' || /\r?\n$/.test(settings) ? `${text}${eol}` : text;
35
+ }
36
+ export function isVsCodeChat(record) {
37
+ return record.entrypoint === VSCODE_ENTRYPOINT;
38
+ }
39
+ export function vscodeChats(account, records) {
40
+ return records
41
+ .filter(({ record }) => isVsCodeChat(record))
42
+ .map(({ record, title }) => ({
43
+ name: title ?? record.name ?? lastSegment(record.cwd) ?? record.sessionId.slice(0, 8),
44
+ session: record.name ?? record.sessionId.slice(0, 8),
45
+ account,
46
+ status: record.status ?? null,
47
+ }));
48
+ }
49
+ export function appSwitchProblem(view, app, key) {
50
+ const current = view.apps.find((each) => each.app === app);
51
+ if (current === undefined)
52
+ return notice('app.unknown', { app });
53
+ const label = APP_LABELS[current.app];
54
+ if (current.error !== null)
55
+ return current.error;
56
+ const target = view.accounts.find((entry) => entry.key === key);
57
+ if (target === undefined)
58
+ return notice('account.unknown', { key });
59
+ if (target.agent !== CLAUDE)
60
+ return notice('account.codexApp', {});
61
+ const email = target.status?.email ?? key;
62
+ if (current.account === key)
63
+ return notice('app.alreadyUses', { app: label, account: email });
64
+ if (target.removing)
65
+ return notice('account.removing', { account: email });
66
+ if (target.needsLogin || target.status?.loggedIn !== true)
67
+ return notice('account.needsLogin', { account: email });
68
+ if (current.sessions.length > 0)
69
+ return notice('app.chatsOpen', { app: label, count: current.sessions.length });
70
+ return undefined;
71
+ }
72
+ function parseSettings(settings) {
73
+ if (settings.trim() === '')
74
+ return {};
75
+ let value;
76
+ try {
77
+ value = JSON.parse(settings);
78
+ }
79
+ catch {
80
+ throw fail('vscode.notJson', {});
81
+ }
82
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
83
+ throw fail('vscode.notObject', {});
84
+ return value;
85
+ }
86
+ function isVariable(value) {
87
+ if (typeof value !== 'object' || value === null)
88
+ return false;
89
+ const { name, value: text } = value;
90
+ return typeof name === 'string' && typeof text === 'string';
91
+ }
92
+ function indentOf(settings) {
93
+ const indent = /\n([ \t]+)\S/.exec(settings)?.[1];
94
+ if (indent === undefined)
95
+ return 4;
96
+ return indent.startsWith('\t') ? '\t' : indent.length;
97
+ }
98
+ function lastSegment(path) {
99
+ return path?.split(/[\\/]/).filter(Boolean).at(-1);
100
+ }
@@ -0,0 +1,117 @@
1
+ import { isSpendingLimit, spendingReset } from '../limits/spending.js';
2
+ const MODELS_URL = 'https://api.anthropic.com/v1/models?limit=100';
3
+ const MESSAGES_URL = 'https://api.anthropic.com/v1/messages';
4
+ const API_VERSION = '2023-06-01';
5
+ // The API's answer to a key whose organization has no credit left; the CLI writes it down as `billing_error`.
6
+ const OUT_OF_CREDIT = 'credit balance is too low';
7
+ const OPENAI_MODELS_URL = 'https://api.openai.com/v1/models';
8
+ const OPENAI_RESPONSES_URL = 'https://api.openai.com/v1/responses';
9
+ const OPENAI_OUT_OF_MONEY = 'insufficient_quota';
10
+ // Models of the list that take no text input through `/v1/responses`.
11
+ const NOT_TEXT = /tts|transcribe|audio|realtime|search|image|embedding/;
12
+ /**
13
+ * Asks Anthropic whether the key can pay again: one output token from the cheapest model the key lists (a Haiku, else
14
+ * the newest one) — a fraction of a cent. The models list alone costs nothing but answers even with no credit.
15
+ */
16
+ export function checkCredit(key, timeoutMs, fetchImpl = fetch) {
17
+ return askCredit({
18
+ modelsUrl: MODELS_URL,
19
+ url: MESSAGES_URL,
20
+ headers: { 'x-api-key': key, 'anthropic-version': API_VERSION, 'content-type': 'application/json' },
21
+ pick: (ids) => ids.find((id) => id.includes('haiku')) ?? ids[0] ?? null,
22
+ body: (model) => ({ model, max_tokens: 1, messages: [{ role: 'user', content: '.' }] }),
23
+ empty: (status, text) => status === 400 && (text.toLowerCase().includes(OUT_OF_CREDIT) || isSpendingLimit(text)),
24
+ }, timeoutMs, fetchImpl);
25
+ }
26
+ /**
27
+ * The same question to OpenAI for a Codex key (BTN-195): a nano text model the key lists, else a mini one, through
28
+ * `/v1/responses` with its least `max_output_tokens`. A key out of money answers 429 `insufficient_quota`; a 429 of a
29
+ * rate limit tells nothing.
30
+ */
31
+ export function checkOpenAiCredit(key, timeoutMs, fetchImpl = fetch) {
32
+ return askCredit({
33
+ modelsUrl: OPENAI_MODELS_URL,
34
+ url: OPENAI_RESPONSES_URL,
35
+ headers: { authorization: `Bearer ${key}`, 'content-type': 'application/json' },
36
+ pick: (ids) => {
37
+ const text = ids.filter((id) => id.startsWith('gpt-') && !NOT_TEXT.test(id));
38
+ return text.find((id) => id.includes('nano')) ?? text.find((id) => id.includes('mini')) ?? null;
39
+ },
40
+ body: (model) => ({ model, input: '.', max_output_tokens: 16, store: false }),
41
+ empty: (status, text) => status === 429 && text.includes(OPENAI_OUT_OF_MONEY),
42
+ }, timeoutMs, fetchImpl);
43
+ }
44
+ async function askCredit(ask, timeoutMs, fetchImpl) {
45
+ try {
46
+ const listed = await fetchImpl(ask.modelsUrl, { headers: ask.headers, signal: AbortSignal.timeout(timeoutMs) });
47
+ if (!listed.ok) {
48
+ await listed.body?.cancel();
49
+ return { state: 'unknown', reason: `models HTTP ${listed.status}` };
50
+ }
51
+ const model = ask.pick(modelIds(await listed.json()));
52
+ if (model === null)
53
+ return { state: 'unknown', reason: 'the key lists no models' };
54
+ const response = await fetchImpl(ask.url, {
55
+ method: 'POST',
56
+ headers: ask.headers,
57
+ body: JSON.stringify(ask.body(model)),
58
+ signal: AbortSignal.timeout(timeoutMs),
59
+ });
60
+ const text = await response.text();
61
+ if (response.ok)
62
+ return { state: 'ok' };
63
+ if (ask.empty(response.status, text))
64
+ return { state: 'empty' };
65
+ return { state: 'unknown', reason: `HTTP ${response.status}` };
66
+ }
67
+ catch (error) {
68
+ return { state: 'unknown', reason: error instanceof Error ? error.message : String(error) };
69
+ }
70
+ }
71
+ function modelIds(body) {
72
+ const data = body?.data;
73
+ return Array.isArray(data) ? data.map((model) => model?.id).filter((id) => typeof id === 'string') : [];
74
+ }
75
+ /**
76
+ * The credit of the API accounts whose session of a current task stopped on it: checked once per interval while the
77
+ * chain keeps asking, and not past two intervals after it stopped asking.
78
+ */
79
+ export class CreditChecks {
80
+ intervalMs;
81
+ wanted = new Map();
82
+ constructor(intervalMs) {
83
+ this.intervalMs = intervalMs;
84
+ }
85
+ /** When the credit of the key came back after the stop at `since`; null — not yet, and the key gets checked. */
86
+ restoredAfter(key, since, now, text = '') {
87
+ const known = this.wanted.get(key);
88
+ if (known === undefined || Date.parse(known.since) < Date.parse(since)) {
89
+ const spending = isSpendingLimit(text) ? { resetsAt: spendingReset(text, since) } : null;
90
+ this.wanted.set(key, { since, askedAt: now.getTime(), checkedAt: now.getTime(), restoredAt: null, spending });
91
+ return null;
92
+ }
93
+ known.askedAt = now.getTime();
94
+ return known.restoredAt;
95
+ }
96
+ due(now) {
97
+ const time = now.getTime();
98
+ return [...this.wanted]
99
+ .filter(([, want]) => want.restoredAt === null && time - want.askedAt <= 2 * this.intervalMs && time - want.checkedAt >= this.intervalMs)
100
+ .map(([key]) => key);
101
+ }
102
+ spendingLimit(key, now) {
103
+ const want = this.wanted.get(key);
104
+ return want === undefined || want.restoredAt !== null || now.getTime() - want.askedAt > 2 * this.intervalMs ? null : want.spending;
105
+ }
106
+ note(key, verdict, now) {
107
+ const want = this.wanted.get(key);
108
+ if (want === undefined)
109
+ return;
110
+ want.checkedAt = now.getTime();
111
+ if (verdict.state === 'ok')
112
+ want.restoredAt = now.toISOString();
113
+ }
114
+ forget(key) {
115
+ this.wanted.delete(key);
116
+ }
117
+ }
@@ -0,0 +1,13 @@
1
+ import { accountId, launchBlock } from './model.js';
2
+ /** The account folder as the chain judges a launch in it. */
3
+ export function currentAccount(view, key = view.active) {
4
+ const entry = view.accounts.find((each) => each.key === key);
5
+ const loggedIn = entry?.status?.loggedIn === true;
6
+ const email = loggedIn ? entry?.status?.email ?? null : null;
7
+ return {
8
+ id: loggedIn ? accountId(key, email) : null,
9
+ ready: loggedIn && launchBlock(view, key) === undefined,
10
+ label: entry?.name ?? email ?? key,
11
+ provider: entry?.agent,
12
+ };
13
+ }
@@ -0,0 +1,65 @@
1
+ // Sessions inherit the environment the transient daemon of an account folder started with,
2
+ // so an env key the share removed stays in new sessions until that daemon is restarted.
3
+ // They also run on the daemon's version, not on the one of the client that starts them.
4
+ export class DaemonRefresh {
5
+ options;
6
+ stale = new Set();
7
+ behind = new Map();
8
+ constructor(options) {
9
+ this.options = options;
10
+ }
11
+ pending(key) {
12
+ return this.stale.has(key);
13
+ }
14
+ /** The daemons on another version than the pin, as of the last launch in their folder. */
15
+ outdated() {
16
+ return [...this.behind.values()];
17
+ }
18
+ async prepare(home) {
19
+ const before = this.snapshot();
20
+ if (!home.global && this.options.share(home))
21
+ this.stale.add(home.key);
22
+ await this.compare(home);
23
+ if (this.stale.has(home.key) || this.behind.has(home.key))
24
+ await this.settle(home);
25
+ if (this.snapshot() !== before)
26
+ this.options.onChange();
27
+ }
28
+ /** A key left the folder's settings outside the share — the page removed it from the global one (BTN-131). */
29
+ async expire(home) {
30
+ const before = this.snapshot();
31
+ this.stale.add(home.key);
32
+ await this.settle(home);
33
+ if (this.snapshot() !== before)
34
+ this.options.onChange();
35
+ }
36
+ async compare(home) {
37
+ if (this.options.daemon === undefined)
38
+ return;
39
+ try {
40
+ const daemon = await this.options.daemon(home);
41
+ if (daemon === null || daemon.running === daemon.wanted)
42
+ this.behind.delete(home.key);
43
+ else
44
+ this.behind.set(home.key, { account: home.key, agent: home.agent, ...daemon });
45
+ }
46
+ catch (error) {
47
+ this.options.onError(error);
48
+ }
49
+ }
50
+ async settle(home) {
51
+ try {
52
+ if ((await this.options.running(home)) === 0) {
53
+ await this.options.stopDaemon(home);
54
+ this.stale.delete(home.key);
55
+ this.behind.delete(home.key);
56
+ }
57
+ }
58
+ catch (error) {
59
+ this.options.onError(error);
60
+ }
61
+ }
62
+ snapshot() {
63
+ return JSON.stringify([[...this.stale], [...this.behind.values()]]);
64
+ }
65
+ }
@@ -0,0 +1,25 @@
1
+ import { AccountHomes, accountsRoot, codexAccountsRoot, copilotAccountsRoot, openCodeAccountsRoot } from './homes.js';
2
+ import { inTransaction } from '../storage/database.js';
3
+ /** Registration belongs to this installation; a CLI's own folder and credentials remain the CLI's. */
4
+ export async function openAccountHomes(db, env) {
5
+ const { rows } = await db.query('select account from global_accounts');
6
+ const registration = {
7
+ keys: new Set(rows.map((row) => row.account)),
8
+ save: async (key, added) => {
9
+ await db.query(added ? 'insert into global_accounts (account) values ($1) on conflict do nothing' : 'delete from global_accounts where account = $1', [key]);
10
+ },
11
+ };
12
+ const homes = new AccountHomes(env, accountsRoot(env), codexAccountsRoot(env), openCodeAccountsRoot(env), copilotAccountsRoot(env), registration);
13
+ const legacy = await db.query("select 1 from service_state where name = 'accounts.adoptLegacy'");
14
+ if (legacy.rows.length > 0) {
15
+ const visible = homes.availableGlobals();
16
+ await inTransaction(db, async (tx) => {
17
+ for (const home of visible)
18
+ await tx.query('insert into global_accounts (account) values ($1) on conflict do nothing', [home.key]);
19
+ await tx.query("delete from service_state where name = 'accounts.adoptLegacy'");
20
+ });
21
+ for (const home of visible)
22
+ registration.keys.add(home.key);
23
+ }
24
+ return homes;
25
+ }
@@ -0,0 +1,22 @@
1
+ import { nextTask } from '../chain/order.js';
2
+ import { runChoice } from '../sessions/launch.js';
3
+ /** The same launch choice as a session, with its inherited accounts underneath. No liveness or pause changes the choice. */
4
+ export function headerLevels(ctx) {
5
+ const shared = runChoice(undefined, ctx.shared, ctx.hasAccount).account;
6
+ const project = runChoice(undefined, ctx.project, ctx.hasAccount).account;
7
+ const open = (task) => task.status.category !== 'Won' && task.status.category !== 'Lost';
8
+ const current = ctx.tasks.find((task) => task.id === ctx.current?.task && open(task));
9
+ const next = nextTask(ctx.tasks, ctx.order.filter((id) => ctx.tasks.some((task) => task.id === id && open(task))), ctx.holding ?? []);
10
+ const task = current ?? ctx.tasks.find((each) => each.id === next);
11
+ const choice = runChoice(task, ctx.project, ctx.hasAccount);
12
+ const pinned = current && ctx.current?.sessionId ? ctx.sessionAccount(ctx.current.sessionId) : undefined;
13
+ const account = pinned ?? choice.account;
14
+ const labelled = ctx.project.labels && task?.runLabels.length === 1 && choice.fallback === null;
15
+ const levels = [];
16
+ if (task && (labelled || account !== project))
17
+ levels.push({ account, source: 'task', task: task.id });
18
+ if (project !== shared)
19
+ levels.push({ account: project, source: 'project' });
20
+ levels.push({ account: shared, source: 'shared' });
21
+ return levels.filter((each, index) => levels.findIndex((other) => other.account === each.account) === index);
22
+ }