kankaku 0.12.1 → 1.0.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 (209) hide show
  1. package/README.md +438 -1396
  2. package/dist/adapters/app-info.js +13 -0
  3. package/dist/adapters/hub-manager/accounts.js +52 -0
  4. package/dist/adapters/hub-manager/download.js +49 -0
  5. package/dist/adapters/hub-manager/install.js +331 -0
  6. package/dist/adapters/hub-manager/package.js +35 -0
  7. package/dist/adapters/hub-manager/process.js +113 -0
  8. package/dist/adapters/hub-manager/zip.js +83 -0
  9. package/dist/adapters/hub.js +101 -0
  10. package/dist/adapters/project-discovery.js +100 -0
  11. package/dist/adapters/setup/agents.js +167 -0
  12. package/dist/adapters/setup/child-process-runner.js +33 -0
  13. package/dist/adapters/setup/claude-commands.js +122 -0
  14. package/dist/adapters/setup/claude-plugin.js +63 -0
  15. package/dist/adapters/setup/claude.js +109 -0
  16. package/dist/adapters/setup/hub.js +52 -0
  17. package/dist/adapters/setup/json-writer.js +68 -0
  18. package/dist/adapters/setup/local-hub.js +86 -0
  19. package/dist/adapters/setup/pi.js +41 -0
  20. package/dist/adapters/setup/readline-prompter.js +68 -0
  21. package/dist/adapters/setup/tui-config.js +34 -0
  22. package/dist/adapters/tui-config.js +35 -0
  23. package/dist/adapters/worklog-reader.js +9 -0
  24. package/dist/cli.js +902 -0
  25. package/dist/domain/catalog-model.js +45 -0
  26. package/dist/domain/claude-integration.js +72 -0
  27. package/dist/domain/dashboard-model.js +83 -0
  28. package/dist/domain/kankaku-package.js +121 -0
  29. package/dist/domain/list-window.js +31 -0
  30. package/dist/domain/local-hub-model.js +179 -0
  31. package/dist/domain/nav-model.js +69 -0
  32. package/dist/domain/quick-actions.js +55 -0
  33. package/dist/domain/setup-plan.js +172 -0
  34. package/dist/domain/setup-wizard.js +217 -0
  35. package/dist/domain/sync-model.js +17 -0
  36. package/dist/domain/tasks-model.js +51 -0
  37. package/dist/domain/text-wrap.js +47 -0
  38. package/dist/domain/today-model.js +75 -0
  39. package/dist/ui/app.js +74 -0
  40. package/dist/ui/catalog-screen.js +100 -0
  41. package/dist/ui/components/bar.js +20 -0
  42. package/dist/ui/components/checklist.js +34 -0
  43. package/dist/ui/components/header-bar.js +8 -0
  44. package/dist/ui/components/key-hints.js +8 -0
  45. package/dist/ui/components/panel.js +32 -0
  46. package/dist/ui/components/radio.js +29 -0
  47. package/dist/ui/components/sidebar.js +30 -0
  48. package/dist/ui/components/sparkline.js +19 -0
  49. package/dist/ui/components/table.js +55 -0
  50. package/dist/ui/components/text-input.js +64 -0
  51. package/dist/ui/dashboard-screen.js +242 -0
  52. package/dist/ui/layout.js +54 -0
  53. package/dist/ui/setup/wizard-screen.js +179 -0
  54. package/dist/ui/sync-screen.js +127 -0
  55. package/dist/ui/tasks-screen.js +166 -0
  56. package/dist/ui/theme.js +81 -0
  57. package/package.json +33 -41
  58. package/vendor/kankaku-pi/LICENSE +21 -0
  59. package/vendor/kankaku-pi/package.json +66 -0
  60. package/{src → vendor/kankaku-pi/src}/adapters/report-data.ts +1 -1
  61. package/{src → vendor/kankaku-pi/src}/adapters/sync-runner.ts +2 -2
  62. package/{src → vendor/kankaku-pi/src}/domain/index.ts +1 -1
  63. package/{src → vendor/kankaku-pi/src}/hub/index.ts +1 -1
  64. package/{src → vendor/kankaku-pi/src}/ports/index.ts +1 -1
  65. package/dist/adapters/cached-catalog.d.ts +0 -42
  66. package/dist/adapters/cached-catalog.js +0 -121
  67. package/dist/adapters/export-writer.d.ts +0 -13
  68. package/dist/adapters/export-writer.js +0 -28
  69. package/dist/adapters/file-modes.d.ts +0 -20
  70. package/dist/adapters/file-modes.js +0 -34
  71. package/dist/adapters/hub-actions.d.ts +0 -35
  72. package/dist/adapters/hub-actions.js +0 -70
  73. package/dist/adapters/hub-credentials.d.ts +0 -35
  74. package/dist/adapters/hub-credentials.js +0 -58
  75. package/dist/adapters/jsonl-work-log.d.ts +0 -20
  76. package/dist/adapters/jsonl-work-log.js +0 -62
  77. package/dist/adapters/kankaku-dir.d.ts +0 -38
  78. package/dist/adapters/kankaku-dir.js +0 -85
  79. package/dist/adapters/lazy-jsonl-work-log.d.ts +0 -17
  80. package/dist/adapters/lazy-jsonl-work-log.js +0 -31
  81. package/dist/adapters/pocketbase-catalog.d.ts +0 -16
  82. package/dist/adapters/pocketbase-catalog.js +0 -56
  83. package/dist/adapters/pocketbase-client.d.ts +0 -81
  84. package/dist/adapters/pocketbase-client.js +0 -148
  85. package/dist/adapters/pocketbase-sink.d.ts +0 -53
  86. package/dist/adapters/pocketbase-sink.js +0 -181
  87. package/dist/adapters/project-config.d.ts +0 -42
  88. package/dist/adapters/project-config.js +0 -108
  89. package/dist/adapters/report-data.d.ts +0 -12
  90. package/dist/adapters/report-data.js +0 -8
  91. package/dist/adapters/report-views.d.ts +0 -45
  92. package/dist/adapters/report-views.js +0 -73
  93. package/dist/adapters/report.d.ts +0 -112
  94. package/dist/adapters/report.js +0 -236
  95. package/dist/adapters/sync-runner.d.ts +0 -114
  96. package/dist/adapters/sync-runner.js +0 -273
  97. package/dist/adapters/sync-state-store.d.ts +0 -62
  98. package/dist/adapters/sync-state-store.js +0 -188
  99. package/dist/config.d.ts +0 -168
  100. package/dist/config.js +0 -392
  101. package/dist/domain/ancestry-match.d.ts +0 -49
  102. package/dist/domain/ancestry-match.js +0 -82
  103. package/dist/domain/client-label.d.ts +0 -28
  104. package/dist/domain/client-label.js +0 -44
  105. package/dist/domain/day.d.ts +0 -2
  106. package/dist/domain/day.js +0 -8
  107. package/dist/domain/export.d.ts +0 -38
  108. package/dist/domain/export.js +0 -68
  109. package/dist/domain/hub-entry.d.ts +0 -234
  110. package/dist/domain/hub-entry.js +0 -265
  111. package/dist/domain/index.d.ts +0 -19
  112. package/dist/domain/index.js +0 -19
  113. package/dist/domain/intervals.d.ts +0 -17
  114. package/dist/domain/intervals.js +0 -43
  115. package/dist/domain/registry-health.d.ts +0 -49
  116. package/dist/domain/registry-health.js +0 -58
  117. package/dist/domain/segment-rule.d.ts +0 -10
  118. package/dist/domain/subagent-profile.d.ts +0 -278
  119. package/dist/domain/subagent-profile.js +0 -418
  120. package/dist/domain/sync-plan.d.ts +0 -151
  121. package/dist/domain/sync-plan.js +0 -196
  122. package/dist/domain/task-view.d.ts +0 -117
  123. package/dist/domain/task-view.js +0 -428
  124. package/dist/domain/work-record.d.ts +0 -236
  125. package/dist/domain/work-record.js +0 -91
  126. package/dist/domain/work-target.d.ts +0 -101
  127. package/dist/domain/work-target.js +0 -149
  128. package/dist/domain/work-tracker.d.ts +0 -90
  129. package/dist/domain/work-tracker.js +0 -405
  130. package/dist/hub/index.d.ts +0 -25
  131. package/dist/hub/index.js +0 -25
  132. package/dist/ports/catalog.d.ts +0 -31
  133. package/dist/ports/clock.d.ts +0 -3
  134. package/dist/ports/index.d.ts +0 -11
  135. package/dist/ports/index.js +0 -1
  136. package/dist/ports/inflight-store.d.ts +0 -15
  137. package/dist/ports/inflight-store.js +0 -1
  138. package/dist/ports/process-registry.d.ts +0 -72
  139. package/dist/ports/process-registry.js +0 -1
  140. package/dist/ports/work-log.d.ts +0 -14
  141. package/dist/ports/work-log.js +0 -1
  142. package/dist/ports/work-sink.d.ts +0 -39
  143. package/dist/ports/work-sink.js +0 -1
  144. /package/dist/{domain/segment-rule.js → ports/project-source.js} +0 -0
  145. /package/dist/ports/{catalog.js → prompter.js} +0 -0
  146. /package/dist/ports/{clock.js → script-runner.js} +0 -0
  147. /package/{src → vendor/kankaku-pi/src}/adapters/agent-info.ts +0 -0
  148. /package/{src → vendor/kankaku-pi/src}/adapters/ancestry.ts +0 -0
  149. /package/{src → vendor/kankaku-pi/src}/adapters/cached-catalog.ts +0 -0
  150. /package/{src → vendor/kankaku-pi/src}/adapters/export-writer.ts +0 -0
  151. /package/{src → vendor/kankaku-pi/src}/adapters/file-inflight-store.ts +0 -0
  152. /package/{src → vendor/kankaku-pi/src}/adapters/file-modes.ts +0 -0
  153. /package/{src → vendor/kankaku-pi/src}/adapters/hub-actions.ts +0 -0
  154. /package/{src → vendor/kankaku-pi/src}/adapters/hub-credentials.ts +0 -0
  155. /package/{src → vendor/kankaku-pi/src}/adapters/jsonl-work-log.ts +0 -0
  156. /package/{src → vendor/kankaku-pi/src}/adapters/kankaku-command.ts +0 -0
  157. /package/{src → vendor/kankaku-pi/src}/adapters/kankaku-dir.ts +0 -0
  158. /package/{src → vendor/kankaku-pi/src}/adapters/lazy-file-inflight-store.ts +0 -0
  159. /package/{src → vendor/kankaku-pi/src}/adapters/lazy-jsonl-work-log.ts +0 -0
  160. /package/{src → vendor/kankaku-pi/src}/adapters/machine-process-registry.ts +0 -0
  161. /package/{src → vendor/kankaku-pi/src}/adapters/panel/kankaku-panel.ts +0 -0
  162. /package/{src → vendor/kankaku-pi/src}/adapters/panel/panel-items.ts +0 -0
  163. /package/{src → vendor/kankaku-pi/src}/adapters/panel/panel-lines.ts +0 -0
  164. /package/{src → vendor/kankaku-pi/src}/adapters/panel/panel-theme.ts +0 -0
  165. /package/{src → vendor/kankaku-pi/src}/adapters/panel/screens/about.ts +0 -0
  166. /package/{src → vendor/kankaku-pi/src}/adapters/panel/screens/doctor.ts +0 -0
  167. /package/{src → vendor/kankaku-pi/src}/adapters/panel/screens/export.ts +0 -0
  168. /package/{src → vendor/kankaku-pi/src}/adapters/panel/screens/report.ts +0 -0
  169. /package/{src → vendor/kankaku-pi/src}/adapters/panel/screens/sync.ts +0 -0
  170. /package/{src → vendor/kankaku-pi/src}/adapters/panel/screens/target.ts +0 -0
  171. /package/{src → vendor/kankaku-pi/src}/adapters/pi-tracker.ts +0 -0
  172. /package/{src → vendor/kankaku-pi/src}/adapters/pocketbase-catalog.ts +0 -0
  173. /package/{src → vendor/kankaku-pi/src}/adapters/pocketbase-client.ts +0 -0
  174. /package/{src → vendor/kankaku-pi/src}/adapters/pocketbase-sink.ts +0 -0
  175. /package/{src → vendor/kankaku-pi/src}/adapters/process-identity-memo.ts +0 -0
  176. /package/{src → vendor/kankaku-pi/src}/adapters/process-identity.ts +0 -0
  177. /package/{src → vendor/kankaku-pi/src}/adapters/project-config.ts +0 -0
  178. /package/{src → vendor/kankaku-pi/src}/adapters/report-views.ts +0 -0
  179. /package/{src → vendor/kankaku-pi/src}/adapters/report.ts +0 -0
  180. /package/{src → vendor/kankaku-pi/src}/adapters/session-client.ts +0 -0
  181. /package/{src → vendor/kankaku-pi/src}/adapters/session-dir.ts +0 -0
  182. /package/{src → vendor/kankaku-pi/src}/adapters/session-target.ts +0 -0
  183. /package/{src → vendor/kankaku-pi/src}/adapters/status-bar.ts +0 -0
  184. /package/{src → vendor/kankaku-pi/src}/adapters/subagent-startup.ts +0 -0
  185. /package/{src → vendor/kankaku-pi/src}/adapters/sync-state-store.ts +0 -0
  186. /package/{src → vendor/kankaku-pi/src}/adapters/target-picker.ts +0 -0
  187. /package/{src → vendor/kankaku-pi/src}/config.ts +0 -0
  188. /package/{src → vendor/kankaku-pi/src}/domain/ancestry-match.ts +0 -0
  189. /package/{src → vendor/kankaku-pi/src}/domain/client-label.ts +0 -0
  190. /package/{src → vendor/kankaku-pi/src}/domain/day.ts +0 -0
  191. /package/{src → vendor/kankaku-pi/src}/domain/export.ts +0 -0
  192. /package/{src → vendor/kankaku-pi/src}/domain/hub-entry.ts +0 -0
  193. /package/{src → vendor/kankaku-pi/src}/domain/intervals.ts +0 -0
  194. /package/{src → vendor/kankaku-pi/src}/domain/panel-model.ts +0 -0
  195. /package/{src → vendor/kankaku-pi/src}/domain/registry-health.ts +0 -0
  196. /package/{src → vendor/kankaku-pi/src}/domain/segment-rule.ts +0 -0
  197. /package/{src → vendor/kankaku-pi/src}/domain/subagent-profile.ts +0 -0
  198. /package/{src → vendor/kankaku-pi/src}/domain/sync-plan.ts +0 -0
  199. /package/{src → vendor/kankaku-pi/src}/domain/task-view.ts +0 -0
  200. /package/{src → vendor/kankaku-pi/src}/domain/work-record.ts +0 -0
  201. /package/{src → vendor/kankaku-pi/src}/domain/work-target.ts +0 -0
  202. /package/{src → vendor/kankaku-pi/src}/domain/work-tracker.ts +0 -0
  203. /package/{src → vendor/kankaku-pi/src}/extension.ts +0 -0
  204. /package/{src → vendor/kankaku-pi/src}/ports/catalog.ts +0 -0
  205. /package/{src → vendor/kankaku-pi/src}/ports/clock.ts +0 -0
  206. /package/{src → vendor/kankaku-pi/src}/ports/inflight-store.ts +0 -0
  207. /package/{src → vendor/kankaku-pi/src}/ports/process-registry.ts +0 -0
  208. /package/{src → vendor/kankaku-pi/src}/ports/work-log.ts +0 -0
  209. /package/{src → vendor/kankaku-pi/src}/ports/work-sink.ts +0 -0
package/README.md CHANGED
@@ -1,1430 +1,472 @@
1
1
  # kankaku
2
2
 
3
- A [pi](https://pi.dev) extension that measures how long an agent actually
4
- spends working on each prompt, so the time can later be accounted for
5
- (billing, reporting).
3
+ A standalone terminal app, `kankaku`, that reads every project's
4
+ `.kankaku/worklog.jsonl` under a configurable list of roots and shows the
5
+ day across projects — the one view the [kankaku](https://kankaku.io) pi
6
+ panel cannot give, since it only ever sees the one project pi is running
7
+ in. Built with [Ink](https://github.com/vadimdemedes/ink) on Node 24. Four
8
+ screens — Dashboard, Tasks, Catalog and Sync — share one tab bar, and each
9
+ has a plain-text subcommand for scripts and cron.
6
10
 
7
- Docs and guide: [kankaku.io](https://kankaku.io). This package lives in
8
- the [kankaku monorepo](https://github.com/soyunninja/kankaku), under
9
- `packages/kankaku`.
11
+ This package lives in the [kankaku monorepo](https://github.com/soyunninja/kankaku),
12
+ under `packages/cli`. It was published as `kankaku-tui` up to 0.12.1; the
13
+ `kankaku` npm name is now this package (the pi extension moved to
14
+ `kankaku-pi`).
10
15
 
11
- ## What it measures
12
-
13
- For every prompt, kankaku tracks the span from `before_agent_start` to
14
- `agent_settled` (or to `session_shutdown` if pi exits mid-run) and splits it
15
- into:
16
-
17
- - **`waitingMs`**: time pi spent blocked on the user — the union of
18
- `ui_prompt_start/end` spans and the execution spans of configured
19
- interactive tools (default `ask_user_question`, `ask_user_choice`). Union
20
- avoids double-counting when a tool internally triggers a UI prompt.
21
- - **`workMs`**: `wallMs - waitingMs`, the actual work time.
22
-
23
- Every pi process — the orchestrator and any subagent child spawned by
24
- `subagent_run` — records its own prompt-to-idle spans, tagged with a `role`
25
- (`orchestrator` or `subagent`) and its `pid`/`parentPid`, so records can be
26
- joined later.
27
-
28
- ## Install
29
-
30
- kankaku is a pi package. Pick one source:
16
+ ## Install everything
31
17
 
32
18
  ```
33
- pi install npm:kankaku # from npm
34
- pi install git:github.com/soyunninja/kankaku # from git (add @v0.12.1 or later to pin)
35
- pi install /absolute/path/to/kankaku/packages/kankaku # local checkout, no copy
19
+ npm i -g kankaku
20
+ kankaku setup
36
21
  ```
37
22
 
38
- `pi install` writes to your global `~/.pi/agent/settings.json`, so the
39
- extension loads in every pi process, including the subagent children that
40
- `subagent_run` spawns. Use `-l` to install into a project's `.pi/settings.json`
41
- instead; note that project-local resources load only after the project is
42
- trusted, which a subagent child may not inherit.
23
+ `npm i -g kankaku` installs the `kankaku` command, the bundled Claude Code
24
+ plugin (`kankaku-claude`) and the pi extension. The extension is carried
25
+ inside the tarball, under `vendor/kankaku-pi/`, and the package's `pi`
26
+ manifest points at it, so `pi install npm:kankaku` also works and loads
27
+ exactly the extension `kankaku-pi` ships. It is vendored rather than
28
+ declared as a dependency because pi installs npm packages into one shared
29
+ directory with a flat `node_modules`: a dependency of `kankaku` would not
30
+ sit next to it, and pi could not find the extension.
31
+
32
+ If you only use pi and do not want the dashboard, install the light
33
+ package instead: `pi install npm:kankaku-pi`.
34
+
35
+ ### Do not load the extension twice
36
+
37
+ The extension is reachable through several sources: `npm:kankaku-pi`,
38
+ `npm:kankaku`, the `git:github.com/soyunninja/kankaku` repository and a
39
+ local checkout. Listing more than one of them in the same pi settings file
40
+ loads the extension more than once and doubles every measurement.
41
+ `kankaku doctor` reports it (`pi: todo — loaded 2 times: npm:kankaku,
42
+ npm:kankaku-pi`), and `kankaku setup` repairs it, keeping exactly one
43
+ source by this precedence: a local path, then `npm:kankaku-pi`, then a
44
+ `git:` source, then `npm:kankaku`. Nothing else in the file is touched and
45
+ the original is saved once as `settings.json.bak`. An install with only
46
+ `npm:kankaku` is configured and is left as it is; new installs write
47
+ `npm:kankaku-pi`. Entries in pi's object form
48
+ (`{ "source": "npm:kankaku-pi", "extensions": [...] }`) count as well.
49
+ Unchecking pi in the wizard removes every kankaku source.
50
+
51
+ ### Migrating from `kankaku-tui`
52
+
53
+ `kankaku-tui` is retired and its package is deprecated in favour of
54
+ `kankaku`. Both packages provide the `kankaku` command, so uninstall the
55
+ old one first, then install the new one and run setup again:
43
56
 
44
- To try it without installing: `pi -e /absolute/path/to/kankaku/packages/kankaku`.
57
+ ```
58
+ npm uninstall -g kankaku-tui
59
+ npm i -g kankaku
60
+ kankaku setup
61
+ ```
45
62
 
46
- ## Quick start
63
+ On a real terminal, `kankaku setup` opens as a full-screen wizard — one
64
+ step at a time, in the same header/panel/footer look as the rest of the
65
+ app (there's no sidebar in the wizard itself: the panel's own title
66
+ tracks progress instead, e.g. `Setup · Agents 1/4`). `kankaku` with no
67
+ arguments does the same the very first time (no `~/.kankaku/tui.json`
68
+ yet); after that first run it opens straight into the Dashboard as usual.
69
+
70
+ The wizard's steps, `enter` to advance and `esc` to go back throughout
71
+ (`esc` at the first step, Agents, quits):
72
+
73
+ 1. **Agents** — a checklist (`space` toggles) that also carries detection
74
+ for every agent kankaku knows about: pi, gentle-shell and Claude Code
75
+ each show `configured (<path, shortened with ~>)` or `not configured`;
76
+ pre-checked means already configured, and unchecking a configured
77
+ agent schedules removing kankaku from it, not just skipping it. Codex
78
+ and OpenCode are listed but disabled, showing `no adapter yet` (found,
79
+ but kankaku can't write its config) or `not installed` (not found).
80
+ Checking Claude Code needs no extra step or path — see "Claude Code"
81
+ below.
82
+ 2. **Hub** — `use an existing hub` (URL, email, masked password, a `c`
83
+ inline health check, reusing the current credentials as the default),
84
+ `install locally`, or `skip`. Installing locally asks only for the
85
+ owner's email and password and then runs the same installer as
86
+ `kankaku hub install` (see "Local hub" below): no `kankaku-hub`
87
+ checkout is needed, and the wizard writes the local hub's service
88
+ account as this machine's hub credentials. The wizard always uses the
89
+ default port; if another process already holds it the step fails with
90
+ the port-in-use message, and `kankaku hub install --port <N>` is the
91
+ way to pick another one.
92
+ 3. **Roots** — the comma-separated project roots, defaulting to the
93
+ current `tui.json` (or the parent of the current directory the first
94
+ time). See "Configuration" below for how deep each root is searched.
95
+ 4. **Review** — the plan: one line per change, with the exact file it
96
+ touches. `enter` applies it.
97
+ 5. **Apply** — runs each change and shows its result
98
+ (`wrote`/`unchanged`/`removed`/`started`/`error: …`) as it happens.
99
+ 6. **Done** — a summary, then `enter` opens the Dashboard in place — no
100
+ restart.
101
+
102
+ ### Claude Code
103
+
104
+ Checking Claude Code (in the wizard, or answering yes in `kankaku setup`'s
105
+ non-interactive flow) writes two things into `~/.claude/settings.json`,
106
+ merged in — every other key, every foreign hook and every other event is
107
+ left untouched — and installs the slash commands:
108
+
109
+ - `statusLine.command`, so Claude Code reports per-prompt cost.
110
+ - `hooks` for every event the bundled `kankaku-claude` plugin declares
111
+ (`packages/claude/hooks/hooks.json`), so the plugin actually measures
112
+ time even when Claude Code is started as plain `claude` — **no
113
+ `--plugin-dir` flag needed**.
114
+
115
+ It also installs the `/kankaku:*` slash commands (`/kankaku:report`,
116
+ `/kankaku:status`, `/kankaku:task`, `/kankaku:sync`, …) as user commands under
117
+ `~/.claude/commands/kankaku/<name>.md`, generated from the plugin's own
118
+ `commands/*.md` with the plugin's absolute install path filled in. Setup
119
+ lists each file it wrote or left unchanged; `kankaku doctor` reports Claude
120
+ Code as configured only when the installed state equals what the plugin
121
+ expects: the exact statusLine command, every hook event in the plugin's
122
+ `hooks/hooks.json` with the same command, matcher and timeout, and every
123
+ command file byte-identical to the generated one, with no stale generated
124
+ file left. Anything else is `todo`, and the note names what is wrong (for
125
+ example `hooks missing: Stop, SessionEnd; commands missing: sync; commands
126
+ outdated: status`), including after an upgrade that adds a command or hook
127
+ event. `kankaku setup --yes` reconciles a selected Claude Code every time,
128
+ so a damaged or outdated install is repaired. A file in that directory that kankaku did not
129
+ generate is never overwritten or removed, and setup says so. **Re-run
130
+ `kankaku setup` if the install path changes** — for example after switching
131
+ Node versions, which moves the global `node_modules`.
132
+
133
+ `kankaku` depends on `kankaku-claude` directly (lockstep, same as its
134
+ `kankaku-pi`/`kankaku-hub` dependencies), so the plugin's files ship inside
135
+ every `kankaku` install; setup resolves their location on disk itself.
136
+ There is nothing to check out and no second `npm install`.
137
+
138
+ **If you previously ran Claude Code with `--plugin-dir <checkout>` to load
139
+ kankaku-claude, drop that flag once `kankaku setup` has configured Claude
140
+ Code** — the hooks it now writes into `settings.json` run on every Claude
141
+ Code launch regardless, so a `--plugin-dir` load on top of that would run
142
+ the hooks twice and double-write worklog records. The `/kankaku:*` slash
143
+ commands do not need it either: setup installs them as user commands.
144
+
145
+ Unchecking Claude Code (or removing it from an already-configured
146
+ machine) removes exactly kankaku's own statusLine and hooks entries,
147
+ leaving everything else in `settings.json` untouched, and removes the
148
+ generated command files (plus the `kankaku/` directory once it is empty,
149
+ and never anything else under `~/.claude/commands`).
150
+
151
+ **Overriding the plugin root.** For local development, or to point at a
152
+ different kankaku-claude checkout, pass `--claude-plugin-dir <dir>` to
153
+ `kankaku setup`/`kankaku setup --yes`/`kankaku setup --dry-run`, or set
154
+ `KANKAKU_CLAUDE_PLUGIN_DIR`. The flag/env value must be a directory
155
+ containing `hooks/hooks.json` and a built `dist/hook.js` (i.e. a
156
+ `kankaku-claude` checkout or `packages/claude` in a kankaku monorepo
157
+ checkout, after `npm install && npm run build` in it — Node cannot run a
158
+ plugin's `.ts` sources directly once they are outside a fresh checkout,
159
+ so setup reports "run npm run build in `<dir>` first" when the build is
160
+ missing). Without either, setup resolves the bundled package
161
+ automatically — most users never need this.
162
+
163
+ `kankaku setup --yes` and `kankaku setup --dry-run` stay exactly as
164
+ before: non-interactive, driven by argv/env only, never opening the
165
+ wizard (even on a TTY). `--yes` accepts every question's own default
166
+ without asking; `--dry-run` prints the plan — each step's state
167
+ (`done`/`todo`/`unavailable`) and the exact file it would change —
168
+ without writing anything. Nothing is ever written without either an
169
+ explicit answer (in the wizard or the `--yes`/readline flow) or `--yes`
170
+ itself. Before the first change to any file, kankaku setup creates a
171
+ `<file>.bak` next to it; re-running `kankaku setup` in any form is always
172
+ safe, since it only ever writes what is still missing or what you
173
+ explicitly change.
174
+
175
+ `kankaku setup` ends with, and `kankaku doctor` prints on its own, the
176
+ same read-only report: one line per agent, one for the hub, one for
177
+ `tui.json`, and a `next: …` hint for anything still `todo`.
47
178
 
48
- Once installed, kankaku records every prompt on its own; there is nothing
49
- to start. Inside pi's TUI, type `/kankaku` to open the panel, the one
50
- place everything is managed from:
179
+ ## Install
51
180
 
52
181
  ```
53
- ╭─ >_ kankaku ─────────────────────────────────────────╮
54
- │ │
55
- │ → Target Billing client, project, hub task │
56
- │ Report Today/all totals, tasks, sessions │
57
- │ Sync Status, sync now, sync all, backfill │
58
- │ Export Write today's or every task as csv │
59
- │ Doctor Orphan/uncertain subagent counts │
60
- │ About Versions, KANKAKU_DIR, hub URL │
61
- │ │
62
- │ ↑↓ move · enter open · esc close │
63
- ╰──────────────────────────────────────────────────────╯
182
+ npm install -g kankaku
64
183
  ```
65
184
 
66
- - **Target** is where you pick the client and project the time is billed
67
- to and, with a hub, the task you are working on right now.
68
- - **Report** shows today's work, waiting and cost, per task or grouped by
69
- client or project.
70
- - **Sync** pushes the consolidated tasks to your hub when one is configured.
71
-
72
- Every panel action is also a subcommand (`/kankaku tasks`, `/kankaku sync`,
73
- …) for scripts and headless runs — see "The `/kankaku` command" below. The
74
- footer clock (`🕒 03:12 · acme`) shows the running prompt's elapsed time
75
- and billing client while an agent works.
185
+ Then run `kankaku setup` (or just `kankaku` the first time) to configure
186
+ the coding agents on this machine, the hub and your project roots. The
187
+ package depends on the published `kankaku-pi` library (`kankaku-pi/domain`,
188
+ `kankaku-pi/hub`), on `kankaku-hub` for the local hub installer, and on
189
+ `kankaku-claude` for the bundled Claude Code plugin files (see "Claude
190
+ Code" above); nothing else needs to be checked out. To work on this repo
191
+ itself, run `npm install` inside it and `npm run dev`.
76
192
 
77
- ## Record schema
193
+ ## Configuration
78
194
 
79
- Each line in `worklog.jsonl` is one JSON object:
195
+ `~/.kankaku/tui.json`:
80
196
 
81
197
  ```json
82
198
  {
83
- "schema": 1,
84
- "id": "uuid",
85
- "role": "orchestrator",
86
- "pid": 4242,
87
- "parentPid": 4000,
88
- "project": "/abs/project/path",
89
- "sessionId": "…",
90
- "sessionFile": "…",
91
- "mode": "tui",
92
- "model": "anthropic/claude-opus",
93
- "client": "acme",
94
- "sessionName": "billing sprint",
95
- "sessionDir": "/abs/custom/session/dir",
96
- "clientId": "pocketbase-record-id",
97
- "clientName": "Acme",
98
- "projectId": "pocketbase-record-id",
99
- "projectName": "Portal",
100
- "machine": "laptop",
101
- "agent": "pi",
102
- "agentVersion": "0.87.1",
103
- "plugin": "kankaku",
104
- "pluginVersion": "0.7.1",
105
- "prompt": "first 200 chars of the first prompt",
106
- "startedAt": "2026-09-10T16:00:00.000Z",
107
- "settledAt": "2026-09-10T16:04:10.000Z",
108
- "wallMs": 250000,
109
- "waitingMs": 30000,
110
- "workMs": 220000,
111
- "runs": 2,
112
- "turns": 9,
113
- "tools": { "bash": 4, "read": 3, "subagent_run": 1, "ask_user_question": 1 },
114
- "subagents": [{ "toolCallId": "…", "agent": "sdd-explore", "mode": "task", "taskId": "t1", "ms": 90000 }],
115
- "segments": { "review": 62000 },
116
- "usage": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0, "cost": 0 },
117
- "status": "completed",
118
- "roleConfidence": "uncertain",
119
- "orchestratorRef": { "pid": 4000, "project": "/abs/other-worktree", "startedAt": "2026-09-10T15:59:00.000Z", "dir": "/abs/other-worktree/.kankaku" }
199
+ "roots": ["/absolute/path/to/workspace", "~/another-workspace"]
120
200
  }
121
201
  ```
122
202
 
123
- `status` is one of `completed`, `aborted` (the last assistant message had
124
- `stopReason: "aborted"`), or `interrupted` (pi shut down while still
125
- running).
126
-
127
- `runs` counts the agent loops inside the record: the first one plus every
128
- continuation pi ran before settling it (an automatic retry after a provider
129
- error, overflow recovery, a queued steer or follow-up). Informational only.
130
-
131
- `trigger` is optional. `"extension"` marks a record no user prompt started:
132
- an extension woke the agent itself — this is how gentle-pi resumes the
133
- orchestrator when a background subagent finishes. Its `prompt` is the fixed
134
- text `(no user prompt — run started by an extension)`. Without it that work
135
- would not be recorded at all, since pi only announces user prompts.
136
-
137
- `roleConfidence` and `orchestratorRef` are both optional and normally
138
- absent — see "Subagents" below. `roleConfidence` is only ever set to
139
- `"uncertain"`, and only on an `orchestrator`-role record kankaku could not
140
- positively prove top-level; `orchestratorRef` is only ever set on a
141
- `subagent`-role record that discovered its tracked ancestor via the
142
- machine-wide process registry. Its optional `dir` field carries that
143
- orchestrator's resolved kankaku directory — the real top-level one even
144
- across a subagent-of-subagent chain — and is what this process's own
145
- work log and inflight checkpoints were actually routed into when it
146
- differs from this process's own (see "Subagents" > "Cross-worktree write
147
- routing"). Neither field, nor `orchestratorRef.dir`, bumps
148
- `WORK_RECORD_SCHEMA` — a record without them (from an older kankaku build)
149
- remains valid.
150
-
151
- `clientId`, `clientName`, `projectId`, `projectName` and `machine` are only
152
- present once a hub is configured (see "Hub (PocketBase)"); every report and
153
- export written before this feature, or by a user without a hub, is
154
- unaffected. `hubTaskId`/`hubTaskTitle` are set alongside them only when a
155
- hub task is linked to the session (`/kankaku task pick`) — see "Linking to
156
- a hub task" below.
157
-
158
- `sessionDir` is present only when pi's session manager reports a
159
- *non-default* session directory (`--session-dir`, or a resumed session
160
- started that way) — exactly the condition under which pi's own printed "To
161
- resume this session: ..." line includes `--session-dir`. Most records never
162
- carry it. `/kankaku doctor` shows it for the current session when set, and
163
- it is available on a task's orchestrator record (`TaskView.sessionDir`) for
164
- anything that wants to reconstruct the exact `pi --session-dir <dir>
165
- --session <id>` resume command locally. When a hub is configured it is also
166
- sent as `session_dir` on every sync (see "Hub (PocketBase)" > "Sync" >
167
- "Agent and measurement quality").
168
-
169
- `agent`, `agentVersion`, `plugin` and `pluginVersion` are optional and
170
- identify who *measured* this record, not who later syncs it: a worklog can
171
- be synced by a process that did not write it (a standalone `kankaku` TUI
172
- syncing pi's records; a session syncing a directory another agent also
173
- wrote to), so this identity travels with the record instead of being
174
- resolved fresh by whichever process happens to push it to the hub. For a
175
- record this package writes, `agent` is always `"pi"` and `plugin` is always
176
- `"kankaku"`; `agentVersion`/`pluginVersion` are included when known and
177
- omitted rather than guessed. Neither field bumps `WORK_RECORD_SCHEMA` — an
178
- older record without them stays valid and falls back to the syncing
179
- process's own identity on create only (see "Hub (PocketBase)" > "Sync" >
180
- "Agent and measurement quality").
181
-
182
- ## Task and session views
183
-
184
- Each `WorkRecord` still measures one pi process's own prompt-to-idle span.
185
- But a `subagent_run` in `background` mode returns immediately while its
186
- child process keeps working, so the orchestrator's own `wallMs` can
187
- under-report how long the task actually took. Two derived, read-only views
188
- correct for that, built purely from `pid`/`parentPid`/`startedAt`/`settledAt`
189
- already present on every record — no new fields are persisted to
190
- `worklog.jsonl`.
191
-
192
- - **Task**: one *confirmed* orchestrator record (see "Subagents" below —
193
- an orchestrator-role record flagged uncertain never anchors a task) plus
194
- every subagent record matched to it — `parentPid === orchestrator.pid`
195
- and the child's `startedAt` falling inside the orchestrator's
196
- `[startedAt, settledAt]` window; `project` is only a **hint**, preferred
197
- when it matches but never a hard filter (see "Subagents"). (If a pid is
198
- reused across runs and several orchestrator records match, a same-project
199
- candidate is preferred, then the latest-starting one.) A task's `wallMs`
200
- is the **union** of the orchestrator's interval and every matched child's
201
- interval — never their sum — so parallel background children are not
202
- double-counted, and a child that outlives the orchestrator's own settle
203
- time correctly extends the task's span. `waitingMs` is the orchestrator's
204
- own waiting time, `workMs = wallMs - waitingMs`, and `usage` is the sum of
205
- the orchestrator's and every child's token/cost totals.
206
- - **Session**: tasks grouped by `sessionId` (tasks with no `sessionId` are
207
- grouped under `"unknown"`). A session's `wallMs` is the union of every
208
- interval — orchestrator and subagent alike — across all of its tasks;
209
- `waitingMs` is the sum of each task's `waitingMs`, and `workMs = wallMs -
210
- waitingMs`.
211
- - **Orphan subagents**: a subagent record with no matching orchestrator
212
- record (for example, its parent's record was lost, or a cross-worktree
213
- registry entry had already expired) is excluded from every task but is
214
- not silently dropped — it stays visible so gaps in the log are noticeable
215
- rather than hidden. See "Subagents" for how a cross-worktree child is
216
- usually reunited *before* it ever becomes an orphan.
217
-
218
- ## Subagents
219
-
220
- kankaku recognises gentle-pi's `subagent_run` tool as opening a subagent
221
- span (unchanged from before this section); this describes how it decides,
222
- for a process that shows no such marker, whether it is a genuine top-level
223
- session or actually someone's subagent — and how a gentle-pi subagent
224
- running in a *different git worktree* than its orchestrator still gets
225
- correctly counted.
226
-
227
- ### The role/state model
228
-
229
- Every record still carries the same binary persisted `role`
230
- (`"orchestrator"` | `"subagent"`, unchanged — see "Record schema"). On top
231
- of it, kankaku's task/session views and the hub sync apply a four-state
232
- classification:
233
-
234
- - **orchestrator** — confirmed top-level: no recognised child-env-marker
235
- (`GENTLE_PI_AGENTS_CHILD=1`, or an explicit `KANKAKU_ROLE=orchestrator` —
236
- see "Interactive sessions and `KANKAKU_ROLE`" below) is present, and
237
- either no live tracked ancestor process was found, or this session is
238
- itself interactive (see "The registry" and "Interactive sessions" below).
239
- This is the default for a plain, ordinary `pi` session — unaffected by
240
- any of this.
241
- - **subagent (joined)** — a gentle-pi child matched to its orchestrator, as
242
- described in "Task and session views" above.
243
- - **subagent (orphan)** — a gentle-pi child that could not be matched to
244
- any orchestrator (shown separately, never dropped — `orphanSubagents`).
245
- - **uncertain** — no recognised child-env-marker, a live tracked ancestor
246
- process *was* found, **and** this process is not itself an interactive
247
- TUI session: it cannot be proven top-level, so it is never counted as a
248
- new task locally and never synced to the hub as one, but it is not
249
- dropped either — `WorkRecord.roleConfidence` is set to `"uncertain"` on
250
- it, and `/kankaku doctor` (and a one-line hint on the plain `/kankaku`
251
- summary) surface it so the gap is visible instead of silently wrong.
252
- This is the fix for a real bug: a subagent mechanism kankaku does not
253
- specifically recognise (for example, pi's own bundled reference
254
- `subagent` example, which sets no env marker at all) used to default to
255
- `"orchestrator"` outright — a phantom top-level task on top of the time
256
- already measured inside its parent's own tool-call span, billed twice.
257
- An unrecognised process now degrades to a safe, visible **undercount**
258
- instead of a silent, unrecoverable **overcount**. An *interactive*
259
- session is never demoted this way, no matter what its ancestry looks
260
- like — see "Interactive sessions and `KANKAKU_ROLE`" below for why, and
261
- for the escape hatch when kankaku still gets it wrong.
262
-
263
- An `uncertain` classification is recoverable going forward: once the
264
- mechanism is recognised (for example, by upgrading kankaku, setting
265
- `KANKAKU_ROLE` explicitly, or — in a later version — registering it via a
266
- configured tool/env marker), a later `/kankaku sync all` or `backfill`
267
- picks up the record correctly. It never resolves itself by guessing. A
268
- record that was *already written* `uncertain`, however, cannot be rewritten
269
- after the fact — `worklog.jsonl` is append-only and kankaku never edits a
270
- past line (see AGENTS.md) — so only a run *after* the fix correctly
271
- anchors a task; there is no migration that goes back and reclassifies old
272
- lines.
273
-
274
- ### The registry
275
-
276
- Every kankaku process writes a small entry to
277
- `~/.kankaku/run/<pid>.json` at startup — `pid`, `parentPid`, `role`,
278
- `project`, its resolved (and, for a routed subagent, actually-used —
279
- see "Cross-worktree write routing" below) `KANKAKU_DIR`, `startedAt`, and
280
- `processStartId` (below) — independent of any project's own `KANKAKU_DIR`,
281
- so it survives a project boundary. Both `~/.kankaku/run` and its entry
282
- files are created owner-only (`0700`/`0600` — an existing looser mode, left
283
- by an older kankaku build, is tightened on the next write, best-effort);
284
- they name absolute project paths and session ids. **The registry is a
285
- startup-time lookup only** — "who is my tracked ancestor, and where does
286
- it keep its log" — resolved once, at process factory time, and never
287
- consulted again later as a live pointer (this used to matter: see
288
- "Cross-worktree write routing" below for why it no longer does). This is
289
- what powers both of the following:
290
-
291
- - **Uncertain detection**: a process with no child-env-marker walks its own
292
- OS ancestor chain (one snapshot, see "Ancestor-chain detection" below)
293
- looking for *any* live registry entry whose identity it can actually
294
- **prove** — see "Identity, not just pid" below. Finding one means some
295
- other tracked kankaku process is an ancestor of this one; combined with
296
- this process *not* being an interactive TUI session (see "Interactive
297
- sessions and `KANKAKU_ROLE`" below), it is classified `uncertain` rather
298
- than defaulting to `orchestrator`.
299
- - **Cross-worktree write routing** (ADR 0023, rewritten for a real bug —
300
- see below): a gentle-pi subagent running in a different git worktree than
301
- its orchestrator walks its ancestor chain, finds its orchestrator's
302
- registry entry (identity-verified), and resolves it to an
303
- `orchestratorRef` (`{ pid, project, startedAt, dir }` — `dir` also
304
- resolves through a subagent-of-subagent chain to the real, top-level
305
- orchestrator, never a middle hop). When that orchestrator's directory
306
- differs from this process's own, the child writes its work log **and**
307
- its inflight crash-recovery checkpoints straight into the orchestrator's
308
- directory instead of its own cwd-relative one — so parent and child
309
- records end up in the *same* `worklog.jsonl` from the moment the child's
310
- first record is appended, not merely discovered there later. The
311
- orchestrator's later `buildTasks` call joins them with the same
312
- `pid`/`parentPid`/project-hint keys it always has; the interval-union
313
- rule itself is still computed in exactly one place (`buildTasks`) — this
314
- only changes *where the bytes physically live*, never how they are
315
- joined. If the orchestrator's directory cannot be created or written to
316
- (gone, or no permission), the child falls back to its own local
317
- directory instead of losing the record, and `/kankaku doctor` reports the
318
- fallback so it can be reunited manually; a record is always written to
319
- **exactly one** log, never both. Because reunification no longer depends
320
- on any pointer still being alive at read time, it survives the child's
321
- own exit cleanup removing its registry entry — which, for gentle-pi's
322
- main case (a blocking `subagent_run` in task mode), has already happened
323
- by the time the parent regains control. If ancestry could not be
324
- established at all (or the write genuinely could not go anywhere), the
325
- child stays a visible orphan instead — undercounted, never lost, and
326
- never compensated for by summing two independently synced rows: **the
327
- hub never sums two unions to recover a missing one**, since that would
328
- double-count the overlap between parent and child. `project` is
329
- therefore only ever a *hint* for the join (preferred when it matches),
330
- never a hard filter.
331
- - The registry is swept opportunistically (when a process writes its own
332
- entry) — see "Registry cleanup and health" below — so it does not grow
333
- unbounded and never keeps serving a stale identity.
334
-
335
- #### Identity, not just pid — the PID-reuse fix
336
-
337
- Matching an ancestor pid to a registry entry by **pid number alone** is not
338
- safe: operating systems reuse pids. A kankaku process that dies without
339
- cleanup (a crash, `kill -9`) can leave its `~/.kankaku/run/<pid>.json`
340
- entry behind; the OS can later hand that same pid to the user's own
341
- interactive shell, and every *genuine* top-level pi session launched from
342
- that shell would then falsely resolve a "tracked ancestor" — silently
343
- misclassified `uncertain` forever, its task never synced. This inverts the
344
- whole guarantee this feature exists for, so identity is proven, not
345
- assumed:
346
-
347
- - Every registry entry also carries `processStartId`: an approximate,
348
- self-consistent epoch-ms estimate of that process's actual OS start time.
349
- **This process's own** `processStartId` (the one it records about
350
- itself) is derived cheaply and portably — `Date.now() - process.uptime()
351
- * 1000`, sampled once at factory time — with **no subprocess spawn and no
352
- `/proc` read at all**, so it is available on every platform, Windows
353
- included, and never adds startup cost (see "Startup cost" below).
354
- Verifying *another* process's (an ancestor's) live identity still needs a
355
- fresh reading of that specific pid from an OS ancestor-chain snapshot: on
356
- macOS/BSD, `ps -eo pid,ppid,etime` (`[[dd-]hh:]mm:ss` elapsed time,
357
- forced through the portable `etime` keyword — BSD `ps` has no `etimes`);
358
- on Linux, `/proc/<pid>/stat`'s `starttime` (clock ticks since boot)
359
- combined with `/proc/uptime`, assuming the near-universal `USER_HZ=100` —
360
- a wrong assumption never causes a false match, since the same (possibly
361
- wrong) constant is used both when an entry is written and whenever it is
362
- re-verified, and a process's `starttime` ticks never change during its
363
- life. `process.uptime()`-derived and `ps`/`/proc`-derived readings of the
364
- *same* process instance agree within the same tolerance (2000ms, which
365
- also absorbs each source's own second-granularity rounding) — this is
366
- cross-checked against a real OS reading by
367
- `scripts/e2e-cross-worktree-real-processes.ts`. Windows has no supported
368
- source for a *live ancestor's* start time — see "Ancestor-chain
369
- detection" — so an ancestor still cannot be identity-verified there, even
370
- though this process's own id is now always available.
371
- - A match is only trusted when **both** sides prove the same identity: the
372
- registry entry's own `processStartId` **and** a fresh re-derivation of
373
- that live pid's start time (from the ancestor's own current snapshot)
374
- agree within tolerance. A pid with a registry entry but a mismatched — or
375
- unprovable, on either side — identity is walked past exactly like an
376
- untracked hop, not treated as a match; if nothing further up the chain is
377
- provable either, ancestry detection reports "no tracked ancestor," which
378
- is the same safe fallback as if the registry were empty (this process
379
- classifies as a confirmed `orchestrator`, never `uncertain`, from an
380
- unprovable candidate alone).
381
- - A legacy entry with no `processStartId` at all (written by a kankaku
382
- build predating this field) is never trusted for identity matching or
383
- kept around: it reads as stale and is removed by the normal sweep the
384
- next time any process writes its own entry.
385
-
386
- #### Registry cleanup and health
387
-
388
- - Every kankaku process removes its own entry file on a normal exit and on
389
- `session_shutdown` (best-effort, verifying the on-disk file's `pid` and
390
- `processStartId` still match its own before unlinking, so it can never
391
- remove a file it does not verifiably own) — a crash still leaves the
392
- entry for the next sweep. Immediately before unlinking a *discarded*
393
- entry, the sweep also re-reads that file and compares it byte-for-byte
394
- against what it judged stale: if the pid was reused and a fresh entry
395
- already written to the same path in the meantime, the file is left alone
396
- instead of destroying a live registration the sweep never actually
397
- evaluated.
398
- - The opportunistic sweep (run whenever any process writes its own entry)
399
- removes: entries for a dead pid; entries whose pid is alive but whose
400
- recorded identity no longer matches that live process (pid reuse); and,
401
- as a last resort, entries older than 7 days regardless of
402
- aliveness/identity. An entry with **no verifiable identity at all**
403
- (legacy/malformed, no `processStartId`) is *never itself* grounds for
404
- deletion while its pid is alive and within the age ceiling — such an
405
- entry is never *used* for ancestor matching either way (matching always
406
- requires a verifiable `processStartId` on both sides), but deleting it
407
- outright used to risk un-registering a genuinely live orchestrator whose
408
- own start-time read happened to fail, at the mercy of an unrelated
409
- sibling process's sweep. It still gets cleaned up the ordinary way, once
410
- its pid dies or it ages out. The sweep never removes the entry the
411
- writing process itself just wrote.
412
- - `/kankaku doctor` reports registry health: how many entries it currently
413
- trusts, how many it would discard, and why (dead / stale-reuse /
414
- over-age).
415
-
416
- ### Ancestor-chain detection
417
-
418
- Reading "a live tracked ancestor process" above requires one OS-level
419
- ancestor-chain snapshot. On Linux this is a set of `/proc/<pid>/stat` reads
420
- (ppid and start-time ticks together, plus one `/proc/uptime` read); on
421
- macOS, one `ps -eo pid,ppid,etime` snapshot (ppid and
422
- elapsed-time-since-start together); a shell-wrapper hop with no registry
423
- entry of its own is walked past, not stopped at.
424
-
425
- **Startup cost.** This snapshot is taken at most once per process, at
426
- extension startup, never on a later hot path — and, since it is the only
427
- part of startup that ever spawns anything, it is skipped entirely unless
428
- there is something for it to find: the machine-wide registry is read
429
- *first*, and the snapshot is only taken when at least one other entry
430
- exists that could possibly be this process's ancestor. The common case (no
431
- other kankaku process running on the machine at all) therefore never
432
- spawns `ps` or reads `/proc` — this process's own identity
433
- (`processStartId`) is unaffected, since it comes from `process.uptime()`
434
- instead (see "Identity, not just pid" above).
435
-
436
- **On a platform or environment where this mechanism cannot run at all** —
437
- Windows (no supported mechanism in this version), or any platform where a
438
- fresh attempt still fails (`ps`/`/proc` missing, timing out, or producing
439
- unreadable output) — ancestor-chain detection degrades gracefully to "no
440
- ancestor found" (never a spawn attempt beyond the one failed try, never a
441
- crash). Critically, this does **not** mean every unmarked process there is
442
- classified `uncertain`: with no way to check, kankaku falls back to the
443
- same marker-only detection it used before this feature existed
444
- (`GENTLE_PI_AGENTS_CHILD=1`/`KANKAKU_ROLE=subagent` → subagent, anything
445
- else → confirmed orchestrator) — the deliberately chosen default, because
446
- marking *every* genuine top-level session `uncertain` on such a platform
447
- would drop all of that user's work, which is far worse than the narrow
448
- overcount risk this guards against elsewhere. The trade-off is visible, not
449
- silent: `/kankaku doctor` reports ancestor-chain detection as unavailable
450
- whenever this happens (distinguishing it from "checked, no tracked
451
- ancestor found" — a separate, always-accurate report never folded into
452
- `roleConfidence`) and names `KANKAKU_ROLE` as the remedy for a genuine
453
- subagent system that needs marking explicitly on such a platform — see
454
- "Interactive sessions and `KANKAKU_ROLE`" below.
455
-
456
- ### The `/kankaku doctor` diagnostic
457
-
458
- `/kankaku doctor` reports, with no network call:
459
-
460
- - How many records are orphaned subagents, and why.
461
- - How many are `uncertain`, and why.
462
- - Whether ancestor-chain detection is actually usable right now (see
463
- above) — and, when it is not, a reminder that an unmarked subagent
464
- system on this platform/environment may be counted twice, with
465
- `KANKAKU_ROLE` named as the fix.
466
- - `KANKAKU_ROLE`, when it decided this process's role, as the deciding
467
- signal — or, when it did not (a confirmed child marker took precedence,
468
- or an interactive session's `subagent` override was ignored — see
469
- "Interactive sessions and `KANKAKU_ROLE`" below), the contradiction and
470
- the resolved outcome instead.
471
- - Whether this process is a subagent that could not write to its
472
- orchestrator's directory and fell back to its own local one (see
473
- "Cross-worktree write routing" above) — a hint to go reunite that record
474
- manually, since `worklog.jsonl` can never be rewritten after the fact.
475
- - Registry health (see "Registry cleanup and health" above).
476
- - The current session's non-default session directory, when set.
477
-
478
- The plain `/kankaku` summary also appends a one-line hint (`N uncertain
479
- record(s) excluded from tasks — run /kankaku doctor`) whenever any exist,
480
- so an undercount is never silent.
481
-
482
- ### Interactive sessions and `KANKAKU_ROLE`
483
-
484
- Every subagent mechanism kankaku recognises today launches its child
485
- **non-interactively**, over pipes (gentle-pi's `--mode rpc`, pi's own
486
- bundled `subagent` example's `--mode json -p`, `pi-subagents`) — a human
487
- never sits in front of one. A process running as an **interactive TUI
488
- session** (`ctx.mode === "tui"`, pi's own signal for "a real terminal, a
489
- human is here") is therefore always treated as a genuine top-level session
490
- and is **never** classified `uncertain`, even when some ancestor in its
491
- process chain happens to be a tracked pi process (for example, pi launched
492
- from inside another pi's `bash` tool). Interactivity can only be known once
493
- pi's own `ExtensionContext` is available, at `session_start` — later than
494
- this process's binary `role` (orchestrator vs. subagent) is decided, but
495
- `roleConfidence` is deferred and finalised exactly once, then, and stays
496
- stable for the rest of the process's life.
497
-
498
- **`KANKAKU_ROLE=orchestrator` or `KANKAKU_ROLE=subagent`** is an explicit
499
- escape hatch — validated; any other value is ignored, falling back to
500
- normal detection. Use it to force a session kankaku still gets wrong: mark
501
- a genuine subagent system it does not recognise as `subagent` (this is
502
- also the remedy `/kankaku doctor` names when ancestor-chain detection is
503
- unavailable on the current platform), or force a session `orchestrator`
504
- regardless of what its ancestry looks like. It has no effect on a record
505
- already written — see "The role/state model" above.
506
-
507
- **Scope it to one invocation. Never export it in a shell rc, tmux config,
508
- or CI environment file.** `process.env` is inherited by every OS child by
509
- default: an exported `KANKAKU_ROLE` reaches every `pi` invocation that
510
- shell/session ever starts, subagents included. Set it only on the one
511
- command it is meant for:
203
+ ### Projects across roots
204
+
205
+ Each root is searched recursively for projects, up to 5 directory levels
206
+ below it by default: a directory is a project once it has its own
207
+ `.kankaku/worklog.jsonl` — including the root itself — and the search
208
+ still continues below it, so a stray worklog in a parent directory (a pi
209
+ session run once in `~/desarrollo`) never hides the projects beneath;
210
+ every directory is listed at most once. Subdirectories are searched one
211
+ level deeper, skipping `node_modules`, `.git` and any hidden
212
+ (dot-prefixed) directory. This lets one root cover a whole workspace, e.g.
213
+ `~/desarrollo` finding every project under `~/desarrollo/<client>/<project>`
214
+ without listing each one. Projects are deduped by real (symlink-resolved)
215
+ path and sorted by name — the directory's basename, or the last two path
216
+ segments joined with `/` when two discovered projects share a basename
217
+ (e.g. `clientA/shared` and `clientB/shared`). `~` expands to the home
218
+ directory. Missing or malformed config falls back to the current working
219
+ directory as the only root.
220
+
221
+ ### Hub credentials (Catalog and Sync)
222
+
223
+ The Catalog and Sync screens (and their subcommands) talk to the same
224
+ PocketBase hub kankaku itself syncs to, through kankaku's own
225
+ `resolveHubCredentials`: `~/.kankaku/credentials.json`
512
226
 
513
- ```
514
- KANKAKU_ROLE=orchestrator pi ...
227
+ ```json
228
+ {
229
+ "url": "https://your-hub.example.com",
230
+ "email": "you@example.com",
231
+ "password": "…"
232
+ }
515
233
  ```
516
234
 
517
- **Precedence (rewritten for a real bug — a BLOCKER fix).** `KANKAKU_ROLE`
518
- no longer overrides every other signal unconditionally:
519
-
520
- 1. A **confirmed child marker** (`GENTLE_PI_AGENTS_CHILD=1`, set only by
521
- the subagent runner itself, never something a shell rc/tmux/CI
522
- environment would export) **always wins**, even over an explicit
523
- `KANKAKU_ROLE=orchestrator`. Without this, a `KANKAKU_ROLE=orchestrator`
524
- export that leaked into a shell rc — the natural thing to do after
525
- hitting a false `uncertain` once — would turn every one of that shell's
526
- later subagent invocations into a confirmed, independently-billed
527
- orchestrator: systematic multi-counting, invisible until someone
528
- compares the hub totals against what actually happened.
529
- 2. `KANKAKU_ROLE=subagent`, with no confirmed marker, is **ignored for an
530
- interactive session** (`ctx.mode === "tui"`). No subagent mechanism
531
- kankaku recognises ever launches its child interactively, so this is
532
- almost always the *mirror* leak — a globally exported
533
- `KANKAKU_ROLE=subagent` reaching a genuine top-level terminal session —
534
- and honouring it would silently drop that session's own work from every
535
- report and the hub (an orphaned subagent record that never anchors a
536
- task), with no way to recover it later, since `worklog.jsonl` is
537
- append-only. Between kankaku's two guiding rules — "undercount is
538
- recoverable, overcount is not" (which governs the *opposite* risk,
539
- inventing extra billing, and does not apply to this contradiction) and
540
- "never silently drop genuine work" — this one is governed by the
541
- second: the override is ignored, the session is classified
542
- `orchestrator` (what it structurally must be), and the contradiction is
543
- surfaced once via `ctx.ui.notify` (a warning) at `session_start` and in
544
- `/kankaku doctor` — never resolved silently. `KANKAKU_ROLE=orchestrator`
545
- has no such exception: forcing a session `orchestrator` can never drop
546
- work, only (rarely) invent a task that should not exist, a risk the
547
- user accepted by setting it explicitly.
548
- 3. Otherwise `KANKAKU_ROLE`, when set to a recognised value, decides — as
549
- before.
550
-
551
- `/kankaku doctor` reports `KANKAKU_ROLE` as the deciding signal only when
552
- it actually decided anything: it flags "override present AND child marker
553
- present" with the resolved outcome (`subagent`, per rule 1) when both are
554
- set, and reports the resolved `orchestrator` outcome (per rule 2) when a
555
- `subagent` override was ignored for an interactive session — in neither
556
- case does it claim the override was the deciding signal.
557
-
558
- **Non-propagation.** `KANKAKU_ROLE` decides only the process that reads
559
- it. kankaku strips it from its own `process.env` right after reading it
560
- (before spawning anything), so a child it spawns — a subagent runner, a
561
- tool shell — never inherits it, even when this process's own copy came
562
- from something outside kankaku's control (a shell rc, tmux, CI). This is
563
- a second, independent layer on top of rule 1 above: rule 1 already
564
- neutralises a leaked `KANKAKU_ROLE=orchestrator` for any *recognised*
565
- subagent mechanism (its confirmed marker always wins regardless), but
566
- stripping means the leak can never reach an *unrecognised* one, or any
567
- other child process, either.
568
-
569
- **Captured once per process, survives `/new`/`/resume`/`/fork`/`/reload`.**
570
- pi re-invokes an extension's factory function in the SAME OS process for
571
- each of those (it "reloads and rebinds extensions" for the new session);
572
- kankaku reads `KANKAKU_ROLE` and decides `role` from the very first
573
- invocation and reuses that exact result for every later one in the same
574
- process, so a `KANKAKU_ROLE=orchestrator` you set for one `pi` command
575
- stays honoured across every `/new`/`/resume`/`/fork`/`/reload` you run
576
- inside that same session, not just the first. This does **not** widen the
577
- "scope it to one invocation" rule above — it still applies only to the one
578
- `pi` process you set it on, and is still stripped from that process's own
579
- `process.env` right after the first read, so it is still never inherited
580
- by anything that process spawns. It only means "one invocation" is
581
- honoured for as long as that OS process stays alive, across every reload,
582
- rather than being silently forgotten the moment pi reloads extensions
583
- internally.
584
-
585
- ### Subagent profiles (phase 6b)
586
-
587
- kankaku recognises a subagent-opening tool call through a `SubagentProfile`
588
- (one per ecosystem package), not a single hardcoded tool name. Three
589
- profiles are built in:
590
-
591
- - **gentle-pi** (first-class): `subagent_run`, joined by explicit `taskId`
592
- (`result.details.gentleAgents`), confirmed by `GENTLE_PI_AGENTS_CHILD=1`.
593
- Nothing about gentle-pi changes — every field it already exposed (agent,
594
- mode, taskId, live status, cross-worktree `cwd`) still does.
595
- - **pi's bundled reference example**: the `subagent` tool, no env marker at
596
- all — recognised only through ancestry, always starts `uncertain` until
597
- the registry/ancestor-chain mechanism above corroborates it.
598
- - **pi-subagents**: also registers a tool named `subagent`, confirmed by
599
- `PI_SUBAGENT_DEPTH` (present with any value — its own recursion-depth
600
- counter, not a fixed sentinel).
601
-
602
- Two packages registering a tool with the exact same name (`subagent`) is a
603
- real ambiguity kankaku never guesses through: which ecosystem package
604
- actually made a given call can only be told apart by its child-env marker
605
- (present in the *child* process, not visible from the parent's tool-call
606
- alone), so a call to `subagent` still opens a span (best-effort agent/mode,
607
- kept only when every candidate profile that reports one agrees), but is
608
- never attributed to one specific profile unless a marker resolves it.
609
- **Nothing money- or join-affecting is ever taken from an ambiguous call
610
- either** — no `usage`, no `taskId` — even when one of the colliding
611
- profiles would normally forward one, because kankaku cannot tell whether
612
- that specific call actually came from that profile. `/kankaku doctor`
613
- reports this as an "ambiguous tool name" line.
614
-
615
- **`KANKAKU_SUBAGENT_TOOLS`** registers one or more additional tool names as
616
- subagent-opening spans, comma-separated, parsed exactly like
617
- `KANKAKU_INTERACTIVE_TOOLS` — always additive to the built-ins, never
618
- replacing gentle-pi's own recognition.
619
-
620
- **`KANKAKU_SUBAGENT_CHILD_ENV`** registers one or more child-process env
621
- markers that confirm a process as this configured tool's subagent,
622
- `;`-separated `NAME=VALUE` (exact match) or a bare `NAME` (presence-only,
623
- any non-empty value) — mirrors `KANKAKU_SEGMENTS`'s tolerant parsing:
624
- malformed entries are skipped, not fatal.
235
+ or the environment (env takes precedence per field over the file):
236
+
237
+ - `KANKAKU_PB_URL`, `KANKAKU_PB_EMAIL`, `KANKAKU_PB_PASSWORD`
238
+ - `KANKAKU_SYNC_WINDOW_HOURS` — revisit window for `sync`/`sync status` (default 24)
239
+ - `KANKAKU_SYNC_PROMPT` — `none` (default), `truncated` or `full`
240
+ - `KANKAKU_SYNC_RECORDS` — set to `0` to skip uploading individual `work_records`
241
+ - `KANKAKU_MACHINE` — overrides the reported hostname
242
+
243
+ Without credentials, the Catalog and Sync screens show a one-line note
244
+ instead of a list; `kankaku catalog` and `kankaku sync status` print the
245
+ same note and exit 0 (no network attempted); `kankaku catalog refresh` and
246
+ `kankaku sync`/`kankaku sync all` print an error and exit 1.
247
+
248
+ Every sync uploaded from here is stamped `plugin: kankaku-tui` (the identifier is kept from before the rename so existing rows stay consistent); a task's
249
+ `agent` comes from its own orchestrator record when it carries one (see
250
+ kankaku's `hub-entry.ts`), else falls back to `agent: unknown` — this app
251
+ never guesses which coding agent produced someone else's worklog.
252
+
253
+ ### Local hub
254
+
255
+ Instead of pointing at someone else's PocketBase, `kankaku hub install`
256
+ sets up and runs your own hub on this machine, under `~/.kankaku/hub/`:
257
+
258
+ - `bin/pocketbase` — the PocketBase binary for this OS/CPU, downloaded
259
+ from the `kankaku-hub` npm package's manifest and SHA256-verified.
260
+ - `pb_data/` — the hub's own database; never touched by an upgrade.
261
+ - `app/<version>/` — a fresh copy of that package version's migrations,
262
+ hooks and static files (never a symlink, so `npm update` can't change a
263
+ running hub out from under it); `current` names the active version.
264
+ - `hub.json` — the installed port and versions.
265
+ - `accounts.json` (owner-only, `0600`) — the PocketBase superuser email
266
+ and generated password, and the owner account's email. The owner logs
267
+ into the hub's own web admin UI with that owner account.
268
+ - `~/.kankaku/credentials.json` — the generated `service` account
269
+ (`kankaku-sync@kankaku.local`) this app and kankaku's own sync already
270
+ read, exactly like a remote hub's credentials.
271
+
272
+ Commands (macOS and Linux only — PocketBase ships no other build):
273
+
274
+ - `kankaku hub install [--port N] [--owner-email E] [--owner-password P]`
275
+ — installs (or, run again, verifies) the hub and leaves it running. On
276
+ a real terminal, a missing owner email/password is prompted for
277
+ (masked); without a TTY, both flags are required. Idempotent: re-running
278
+ with everything already in place changes nothing. If another process
279
+ already answers on the target port, `install`/`start`/`upgrade` refuse
280
+ with `port <N> is already in use by another process — pass --port <N>
281
+ or stop it` instead of provisioning accounts against it; pass a
282
+ different `--port` or free the port and retry.
283
+ - `kankaku hub start` / `kankaku hub stop` — start or stop the server
284
+ process; `stop` is a no-op when it isn't running.
285
+ - `kankaku hub status` — `local hub: running 0.2.0 (PocketBase 0.40.4) at
286
+ http://127.0.0.1:8090 · pb_data 1.2 MB`, `stopped`, or `not installed`.
287
+ - `kankaku hub upgrade` — copies a fresh `app/<version>/` from the
288
+ currently installed `kankaku-hub` package, downloads a new PocketBase
289
+ binary only if that version changed, and restarts — `pb_data` is never
290
+ touched.
291
+ - `kankaku hub logs [-n N]` — the last `N` (default 50) lines of
292
+ `hub.log`.
293
+
294
+ The Dashboard's Hub card shows `local hub · running`/`stopped` when the
295
+ configured hub is this machine's own local install, with a matching `h`
296
+ quick action to start or stop it. The setup wizard's Hub step's
297
+ `install locally` option runs this same installer (asking for the owner
298
+ email/password inline); the older checkout-based dev install
299
+ (`kankaku-hub`'s own `scripts/dev.sh`) is still available for hub
300
+ developers via `kankaku setup --from-checkout <dir>`.
301
+
302
+ ## Usage
303
+
304
+ - `kankaku` — opens the interactive TUI on the Dashboard screen.
305
+ - `kankaku today [--roots a,b]` — today's work per project, plain text.
306
+ - `kankaku tasks [--all]` — every task's line (kankaku's own `formatTasks`),
307
+ grouped under a `== <project> ==` header per project; restricted to
308
+ today unless `--all`.
309
+ - `kankaku catalog [refresh]` — without `refresh`, reports the locally
310
+ cached client/project counts (no network); `refresh` fetches a fresh
311
+ snapshot from the hub and caches it to `~/.kankaku/catalog.json`.
312
+ - `kankaku sync [status|all] [--project <dir>]` — `status` reports the
313
+ pending count and last sync per project, no network; with no argument,
314
+ syncs the pending window; `all` does a full resync. Defaults to every
315
+ discovered project, sequentially; `--project <dir>` restricts to one.
316
+ - `kankaku setup [--yes] [--dry-run] [--from-checkout <dir>]` — see
317
+ "Install everything" above; `--from-checkout` is the hub-developer-only
318
+ checkout-based local hub install, see "Local hub" above.
319
+ - `kankaku doctor` — the same read-only report `kankaku setup` ends with,
320
+ without prompting or writing anything.
321
+ - `kankaku hub install|start|stop|status|upgrade|logs` — the local hub's
322
+ lifecycle; see "Local hub" above.
323
+
324
+ `--roots` (on `today`/`tasks`) overrides the configured roots for that run.
325
+
326
+ `--theme <name>` picks one of the three built-in colour presets for the
327
+ interactive TUI; `KANKAKU_TUI_THEME=<name>` does the same through the
328
+ environment (the flag wins when both are given). The valid names are
329
+ `gentleman-sexy` (the default), `gentleman-cute` and `gentle` — resolved
330
+ hex values copied from [gentle-pi](https://github.com/Gentleman-Programming/gentle-pi)'s
331
+ own themes (MIT), so this TUI matches the owner's pi panel instead of an
332
+ unrelated default. An unknown name prints a usage error listing the valid
333
+ names and exits 1 without opening the TUI.
334
+
335
+ ## Screens
336
+
337
+ One visual system drives all four screens: a left sidebar for navigation,
338
+ titled bordered panels, aligned tables with a highlighted selection, text
339
+ bars and sparklines, a header line and a footer of key hints — all driven
340
+ by a single theme of colour roles (`src/ui/theme.ts`, see `--theme` above
341
+ for the three built-in presets). The app runs fullscreen, in the
342
+ terminal's alternate screen buffer: the frame fills the whole terminal
343
+ height, resizing live with the terminal. The sidebar sits beside the
344
+ screen at 100+ terminal columns, stacks full-width above it at 70-99
345
+ columns, and collapses to a one-line tab strip below 70 columns; a
346
+ selected row or card is always marked with a visible `›`, never colour
347
+ alone. The sidebar itself shows which zone has focus: its border switches
348
+ to the active border colour and the active item gets a full-row highlight
349
+ when it has focus, dropping back to a plain `›` marker with no highlight
350
+ once focus moves to the screen's own content.
351
+
352
+ Every panel in the main area is sized to a fixed height derived from the
353
+ terminal's own height, so it never grows with its content and shifts the
354
+ rest of the screen — a long value (e.g. the Tasks screen's full prompt)
355
+ is wrapped and, if it still doesn't fit the panel's fixed height, clipped
356
+ with a trailing `… N more lines` note instead of silently overflowing or
357
+ pushing the header out of view.
358
+
359
+ Any list that can grow past the available height (the Tasks table, the
360
+ Catalog Clients/Projects lists, the Dashboard Projects table, the Sync
361
+ card grid) scrolls instead of overflowing the terminal: the viewport
362
+ follows the current selection, and a `↑ N more` / `↓ N more` line marks
363
+ rows hidden above or below it.
364
+
365
+ Dashboard is the app's home screen: a Today card (work/wait/cost/tasks/
366
+ cache hit — it shows today's numbers, hence its own title), a Last 7 days
367
+ card (work and cost sparklines with weekday labels), a Projects table
368
+ (work, cost and a share bar per project), a Hub card (pending/stale, last
369
+ sync time, catalog summary) and a Quick actions panel (`c` refresh the
370
+ catalog, `s` sync every project, `S` full-sync every project, `r` reload):
625
371
 
626
372
  ```
627
- KANKAKU_SUBAGENT_TOOLS=my_subagent_tool
628
- KANKAKU_SUBAGENT_CHILD_ENV=MY_TOOL_CHILD=1
629
- ```
630
-
631
- **What NOT to use as a marker.** A configured marker must be exclusive to
632
- the child process your subagent tool actually spawns — never an ambient
633
- variable pi, your shell, npm, or the OS sets on *every* process. kankaku
634
- rejects an obviously-ambient name outright at load time (case-insensitive):
635
- `PI_CODING_AGENT` and `AI_AGENT` (pi sets both on every process it runs,
636
- not just a subagent's child), the generic shell/OS variables `PATH`,
637
- `HOME`, `USER`, `SHELL`, `PWD`, `CI`, `LANG`, `TMUX`, and anything prefixed
638
- `PI_`, `TERM`, `LC_`, `NODE_`, `NPM_`, or `KANKAKU_`. A rejected marker
639
- never reaches the configured profile — it is reported once via
640
- `ctx.ui.notify` and listed in `/kankaku doctor`, never silently accepted.
641
- This denylist cannot enumerate every possible ambient variable, though, so
642
- there is a second, runtime layer: **a configured marker never demotes an
643
- interactive session**, exactly like `KANKAKU_ROLE=subagent` already does
644
- not (see "Interactive sessions and `KANKAKU_ROLE`" above) — if a configured
645
- marker matches on a session that turns out to be interactive, kankaku
646
- treats it as the orchestrator it structurally must be and warns once
647
- (escalated to a stronger warning when that session also has no tracked
648
- ancestor at all, the clearest sign the "marker" is actually ambient). A
649
- **built-in** marker (`GENTLE_PI_AGENTS_CHILD`, `PI_SUBAGENT_DEPTH`) keeps
650
- the unconditional precedence it always had — no built-in mechanism kankaku
651
- recognises ever launches its child interactively, so this exception never
652
- actually applies to it in practice.
653
-
654
- **Verify with `/kankaku doctor`.** After configuring
655
- `KANKAKU_SUBAGENT_CHILD_ENV`, run `/kankaku doctor` from an ordinary
656
- top-level session: it must **not** report a "configured marker" or
657
- "rejected marker" line for a ordinary interactive session. If it does, the
658
- chosen name is either denylisted or ambient enough to trip the interactive
659
- guard — pick something the third-party tool's own child process sets that
660
- nothing else on the system would ever set.
661
-
662
- A confirmed marker from a configured profile that passes both layers above
663
- still takes the same "always wins over `KANKAKU_ROLE`" precedence gentle-pi's
664
- own marker already had for a **non-interactive** process — see "Interactive
665
- sessions and `KANKAKU_ROLE`" above.
666
-
667
- `/kankaku doctor` reports the active profile set, any configured tools/
668
- markers, which profile matched each subagent record (or "unmatched" when
669
- no marker resolved it), any rejected marker names with why, and a
670
- configured-marker-ignored-for-interactivity contradiction when one occurs.
671
-
672
- ### In-process subagents (phase 6c)
673
-
674
- A subagent tool result's `usage` field — pi's own documented convention
675
- for "a tool making nested LLM calls should return their combined `Usage`
676
- as `usage`" — is recorded on the span itself (never folded into the
677
- triggering record's own usage totals at write time any more), and added to
678
- the *task's* aggregate total by `buildTasks` — the one place per-task
679
- usage is ever assembled — except when this same task also has a joined
680
- child record confirmed by the **same** profile: that child's own usage
681
- already carries this cost through its own confirmed-marker/ancestry join,
682
- so the span's forwarded figure is excluded instead of counted a second
683
- time. gentle-pi is unaffected (its result never carries one — cost for its
684
- children is, and stays, tracked through the registry/ancestry join above).
685
- A profile whose marker can also produce an ancestry-joined child record
686
- with its own usage (pi-subagents) never forwards `usage` even when its
687
- result happens to carry one, to avoid counting the same nested work twice
688
- by construction; a *configured* profile that declares **both** a marker
689
- and forwards usage relies on the runtime reconciliation above instead (see
690
- "Subagent profiles (phase 6b)"). **Usage is never forwarded for an
691
- ambiguous tool-name match** (2+ profiles registering the same name, e.g.
692
- `subagent`) — see "Subagent profiles (phase 6b)" above.
693
-
694
- Real in-process (same-OS-process, no separate `pid`) subagent nesting was
695
- investigated directly against pi's own source and documented API
696
- (`docs/extensions.md`) for this release: none of gentle-pi, pi's bundled
697
- reference example, or pi-subagents actually run a child *inside* the
698
- parent's process — every one of them spawns a real, separate OS process.
699
- pi's own in-process mechanism (`ctx.newSession`/`ctx.fork`) replaces one
700
- session with another *sequentially* in the same process (the old session's
701
- `session_shutdown` fires, then the new one's `session_start` — never
702
- concurrently), which is exactly what "kankaku reads/writes a fresh record
703
- per session_start, same pid" already handles correctly. As a defensive
704
- guard for the pattern true concurrent nesting *would* leave behind,
705
- `/kankaku doctor` flags two confirmed-orchestrator records sharing a pid
706
- with **overlapping** `[startedAt, settledAt]` windows as "likely
707
- in-process nesting", unioning (never summing) their wall time via the
708
- same interval-union primitive `buildTasks` itself uses — informational
709
- only, it never changes a task's own numbers. This has not been observed
710
- from any real subagent mechanism in this codebase's research; if pi (or an
711
- extension built on its SDK) grows genuine concurrent in-process nesting in
712
- the future, this is the signal that would surface it.
713
-
714
- ### Limitations, honestly
715
-
716
- - **Windows has no ancestor-chain detection** (an ancestor can never be
717
- identity-verified there), though this process's own `processStartId` is
718
- always available regardless of platform — see "Identity, not just pid"
719
- above. Mark a genuine subagent system explicitly with `KANKAKU_ROLE` on
720
- such a platform; see "Interactive sessions and `KANKAKU_ROLE`" above.
721
- - **gentle-pi's child cannot currently read its own task id** — the
722
- cross-worktree join above relies on ancestry plus the registry, not on an
723
- explicit shared id, because upstream gentle-pi does not hand the child
724
- process its task id today. If that changes upstream, a future kankaku
725
- version can upgrade this join to a higher-confidence explicit-id match.
726
- - **Ancestor-chain detection only sees the chain as it exists when a
727
- process looks.** A detached child reparented to init/launchd before that
728
- point cannot recover its original ancestry this way — the same limitation
729
- the existing `pid`/`parentPid` capture already has (see AGENTS.md).
730
- - **A record already written `uncertain` (or already routed to a fallback
731
- local directory) cannot be rewritten.** `worklog.jsonl` is append-only;
732
- fixing the underlying cause (upgrading kankaku, setting `KANKAKU_ROLE`,
733
- restoring access to an orchestrator's directory) only helps a *later*
734
- run's records, never edits a line already on disk. There is no migration
735
- planned for this — it follows directly from "never rewrite the log" (see
736
- AGENTS.md).
737
-
738
- ## The `/kankaku` command
739
-
740
- `/kankaku` with no arguments, run inside pi's TUI, opens an overlay panel
741
- modelled on pi's own `/settings` (the same pi-tui `SettingsList`/
742
- `SelectList` widgets, the same keys) from which every kankaku view and
743
- action is reachable:
744
-
745
- - **Target** — billing client, project, hub task, and the legacy label
746
- (see "Billing labels" and "Hub (PocketBase)" below). The Task row lists
747
- the open/doing hub tasks of the current project; picking one links the
748
- session, exactly like `/kankaku task pick` below — **session-only**,
749
- never persisted (see "Linking to a hub task").
750
- - **Report** — today/all totals, tasks, sessions, clients, and projects:
751
- the same five views `/kankaku`'s subcommands produce.
752
- - **Sync** (hub only) — status, sync now, sync all, backfill, and a
753
- catalog refresh.
754
- - **Export** — write today's or every task as csv/json.
755
- - **Doctor** — orphan/uncertain subagent counts and ancestor-detection
756
- availability.
757
- - **About** — versions, the resolved `KANKAKU_DIR`, the hub URL, and every
758
- env-only setting, read-only.
759
-
760
- Keys: `↑↓` move, `Enter` open a section or select a value, `Esc` or `←` go
761
- back (or close the panel at the root), `q` close from anywhere. Mouse: the
762
- footer hints and list rows are clickable, but only in pi's fullscreen
763
- mode — pi does not dispatch mouse events in its regular (non-fullscreen)
764
- mode, so there the panel is keyboard-only.
765
-
766
- Every subcommand below is unchanged, and is exactly what headless (print
767
- or RPC) mode still uses — `/kankaku` there keeps showing today's totals
768
- directly, never the panel, since there is no UI to open one in. Run
769
- `/kankaku <subcommand>` (with arguments) in the TUI to skip the panel and
770
- go straight to that subcommand's output, exactly as before the panel
771
- existed.
772
-
773
- Plain `/kankaku` (headless, or any subcommand below) shows today's totals
774
- (work, waiting, record count) per role, plus a union-based tasks segment.
775
- Each totals line also shows `cache hit NN%` when tokens were recorded: the
776
- share of prompt input tokens served from the provider's prompt cache, cache
777
- reads over input plus cache reads plus cache writes; the segment is
778
- omitted, not shown as `0%`, when no tokens were recorded. In the
779
- interactive TUI the report is appended to the chat transcript as a durable
780
- card that is never sent to the LLM; without a UI (print or RPC mode) it
781
- falls back to a notification. Arguments are whitespace-separated and
782
- order-insensitive:
783
-
784
- - `/kankaku` (headless only — the TUI opens the panel instead, whose
785
- Report screen defaults to the same view) — today's role totals and
786
- tasks segment, each with its estimated cost.
787
- - `/kankaku all` — same, but across every record.
788
- - `/kankaku tasks` — one line per task (time, union wall/work, cost,
789
- subagent count, truncated prompt) for the **current pi session**. Add `all` for
790
- every session. If the current session has no `sessionId`, tasks from every
791
- session are shown instead.
792
- - `/kankaku sessions` — one line per session (id, time range, union
793
- wall/work, cost, task count) for today. Add `all` for every day.
794
- - `/kankaku client <name>` — set the billing client for the current pi
795
- session. `/kankaku client` alone shows the effective client and which
796
- source it came from; `/kankaku client --clear` removes the session-level
797
- override. See "Billing labels" below. When a hub is configured, `<name>`
798
- must match a catalog client's code or name (case-insensitive) instead of
799
- being free text — see "Hub (PocketBase)".
800
- - `/kankaku clients` — one line per client (work/waiting/wall time, cost,
801
- task count) for today. Add `all` for every day. Tasks with no resolved
802
- client are grouped under `(none)`.
803
- - `/kankaku doctor` — orphan/uncertain subagent record counts and why,
804
- plus ancestor-detection platform availability. No network call. See
805
- "Subagents".
806
-
807
- The following are available only when a hub is configured (see "Hub
808
- (PocketBase)" below):
809
-
810
- - `/kankaku target` — show the effective client/project and which source
811
- produced it. `/kankaku target pick` runs the picker again (works
812
- mid-session; the new target applies to records settled afterwards).
813
- `/kankaku target clear` clears the session-level target.
814
- - `/kankaku task` (or `/kankaku task pick`) — link this session to an
815
- open/doing hub task of the effective project. `/kankaku task clear`
816
- drops the link. See "Linking to a hub task" below.
817
- - `/kankaku catalog refresh` — force a catalog refresh and report the
818
- client/project counts.
819
- - `/kankaku projects` — one line per project (work/waiting/wall time, cost,
820
- task count) for today. Add `all` for every day. Tasks with no resolved
821
- project are grouped under `(no project)`.
822
-
823
- Cost figures are the sum of `usage.cost` as priced by pi's model table
824
- (per-million-token rates in `models.json`, adjustable with `modelOverrides`).
825
- For subscription-based providers this is an estimate at API list prices, not
826
- an invoice.
827
-
828
- While an agent is running, pi's status bar shows a `🕒 mm:ss · <client>` indicator (the client part appears only when one resolves); while idle it shows `💼 <client>`, or nothing when no client resolves. The entry is keyed `zz-kankaku` so it sorts last among extension statuses. The running indicator carries
829
- the elapsed time for the current run.
830
-
831
- ## Billing labels
832
-
833
- Every `WorkRecord` can carry a `client` — who the work is billed to — so
834
- reports and exports can be grouped by client. The effective client is
835
- resolved from three sources, in decreasing precedence:
836
-
837
- 1. **Session** — set with `/kankaku client <name>` (see above), persisted as
838
- a `kankaku-client` custom session entry and restored on session reload.
839
- 2. **`KANKAKU_CLIENT`** — the environment variable, a per-process default.
840
- 3. **Project** — `client` in `<KANKAKU_DIR>/config.json` (e.g.
841
- `{"client": "acme"}`), the project's own default.
842
-
843
- A client name must match `/^[A-Za-z0-9._-]{1,64}$/`; anything else (empty,
844
- too long, containing spaces or other characters) is ignored and resolution
845
- falls through to the next source.
846
-
847
- A `subagent_run` child process does not resolve its own client — a
848
- subagent's own `WorkRecord` never carries `client`. Instead, the **task**
849
- view (see "Task and session views") exposes the client from its
850
- orchestrator record only, so `/kankaku tasks`, `/kankaku clients`, and the
851
- export all see subagent work grouped under the task's (i.e. the
852
- orchestrator's) client.
853
-
854
- `sessionName` is also attached to every record from `pi.getSessionName()`,
855
- so reports can show which named session produced a task.
856
-
857
- ## Hub (PocketBase)
858
-
859
- kankaku can optionally resolve the billing client (and a project) **from a
860
- PocketBase instance** instead of free text, so `cajamar`/`Cajamar`/`cjamar`
861
- can no longer become three different clients. This is phase 1 of the hub
862
- integration (catalog + selection only): nothing is uploaded anywhere.
863
-
864
- ### Configuration
865
-
866
- Set `KANKAKU_PB_URL`, `KANKAKU_PB_EMAIL`, `KANKAKU_PB_PASSWORD`, or write
867
- `~/.kankaku/credentials.json`:
868
-
869
- ```json
870
- { "url": "https://pb.example.com", "email": "bot@example.com", "password": "secret" }
373
+ >_ kankaku 0.1.0 hub ● kankaku.soyun.ninja · synced 08:20
374
+ ┌──────────────┐ ╭─[ Today ]────────────────────╮ ╭─[ Last 7 days ]──────────────────╮
375
+ │ › Dashboard │ │ work 1h 42m │ │ work ▂▅▇▃▁▆█ cost ▁▃▆▂▁▅█ │
376
+ │ Tasks │ │ wait 6m cost $9.83 │ │ mon tue wed thu fri sat sun │
377
+ │ Catalog │ │ tasks 12 cache hit 68%│ ╰──────────────────────────────────╯
378
+ │ Sync │ ╰──────────────────────────────╯ ╭─[ Hub ]──────────────────────────╮
379
+ │ │ ╭─[ Projects ]────────────────────────────╮ │ pending 1 · stale 0 │
380
+ │ │ │ project work cost share │ │ last sync ok 08:20 │
381
+ │ │ │ kankaku 1h 02m $6.49 ████████░░ │ │ catalog 9 clients · │
382
+ │ │ │ kankaku-tui 31m $2.10 █████░░░░░ │ │ 17 projects │
383
+ │ │ │ kankaku-hub 9m $1.24 ██░░░░░░░░ │ ╰─────────────────────────╯
384
+ │ │ ╰─────────────────────────────────────────╯
385
+ ├──────────────┤
386
+ │ roots 1 │
387
+ │ projects 3 │
388
+ └──────────────┘
389
+ ↑↓ move enter open r refresh 1-4 screens q quit
871
390
  ```
872
391
 
873
- Environment variables take precedence over the file, field by field. The
874
- hub URL must be HTTPS unless it points at `localhost`/`127.0.0.1`/`::1`; a
875
- plain-HTTP URL for any other host is refused (surfaced once via a
876
- notification). The project's own `<KANKAKU_DIR>/config.json` is never read
877
- for credentials — it is project-local and frequently committed.
878
-
879
- `KANKAKU_MACHINE` optionally names this machine (for a multi-machine setup
880
- later); it defaults to the OS hostname and is attached to every record as
881
- `machine` once the hub is configured.
882
-
883
- **When no hub is configured, kankaku behaves exactly as it does today** —
884
- this whole feature is additive and every existing behaviour, record shape,
885
- and report stays unchanged.
886
-
887
- ### Selection
888
-
889
- On `session_start`, for the orchestrator role with a UI available:
890
-
891
- 1. **Session** — restored from the last `kankaku-target` session entry
892
- (including a remembered "skipped" choice, so a reload does not ask
893
- again).
894
- 2. **Project config** — `clientId`/`projectId` in `<KANKAKU_DIR>/config.json`.
895
- 3. **`repo_paths`** — the current working directory matched against each
896
- project's `repo_paths` (exact match, or a subdirectory of one; the
897
- longest match wins).
898
- 4. Otherwise, a picker: `ctx.ui.select` for the client (active clients,
899
- sorted by name, plus "— skip —"), then for the project (active projects
900
- of that client, plus "(no project)" and "— skip —"). Declining at either
901
- step — "— skip —" or dismissing the dialog — cancels the whole pick and
902
- is remembered for the session. The picker shows the freshly refreshed
903
- catalog when the hub answered within the deadline described in "Caching
904
- and offline behaviour" below; otherwise it falls back to the cache.
905
-
906
- After a pick, kankaku asks whether to remember it for this repository; a
907
- "yes" merges `clientId`/`projectId` into `<KANKAKU_DIR>/config.json`.
908
-
909
- An id from any source that no longer resolves to an active, non-"unassigned"
910
- catalog entry is treated as absent for that source and resolution falls
911
- through to the next one, exactly like the legacy client precedence.
912
-
913
- Once a hub target is active for a run, the legacy `client` label is set to
914
- the target's client `code` (so every existing report/export keeps grouping
915
- correctly), and the record additionally carries `clientId`, `clientName`,
916
- and — when a project is selected — `projectId`/`projectName`. A subagent
917
- never resolves its own target, exactly like the legacy `client` label — the
918
- task view exposes it from the orchestrator record only.
919
-
920
- The status bar shows `💼 <client> · <project>` (or just `💼 <client>` without
921
- a project) in place of the legacy client label, both idle and during a run.
922
-
923
- Mid-session, the `/kankaku` panel's Target screen (see "The `/kankaku`
924
- command" above) is the interactive way to change the client or project
925
- without going through `/kankaku target pick`'s picker dialog — it edits the
926
- same session-level target, applies through the same `SessionTarget`, and
927
- has its own "Remember" row for `<KANKAKU_DIR>/config.json`.
928
-
929
- ### Linking to a hub task
930
-
931
- `/kankaku task` (or `/kankaku task pick`) links the current session to one
932
- of the effective project's existing hub `tasks` rows: a `ctx.ui.select`
933
- picker lists the project's `open`/`doing` tasks, sorted by title (colliding
934
- titles are disambiguated with the task's external reference, or its id).
935
- `/kankaku task clear` drops the link, keeping the rest of the session
936
- target. kankaku never creates a task from pi — this only links to one
937
- that already exists in the hub.
938
-
939
- The link is **session-only**: unlike `clientId`/`projectId`, it is never
940
- persisted to `<KANKAKU_DIR>/config.json`, and it is never asked for at
941
- `session_start` — you always link a task explicitly, with `/kankaku task`.
942
- Any target change (`/kankaku target pick`, `/kankaku target clear`, or the
943
- legacy `/kankaku client <name>`) drops the linked task, since a new client
944
- or project makes the old task's link meaningless. A task whose project no
945
- longer matches the effective project (e.g. after a target change or a
946
- reassignment in the hub) is also dropped by the domain resolver, never
947
- silently linked across projects. A task already marked `done` when linked
948
- keeps linking for the rest of the session — only the picker itself hides
949
- `done` tasks, so you cannot accidentally pick a closed one, but finishing
950
- the picked task in the hub mid-session does not break the link. Subagent
951
- records never carry a linked task, exactly like `clientId`/`projectId` —
952
- the task view exposes it from the orchestrator record only, and
953
- `formatWorkTargetLabel` appends it to the status-bar/report label as
954
- `<client> · <project> › <task title>`.
955
-
956
- The `/kankaku` panel's Target screen's Task row is the interactive
957
- equivalent of `/kankaku task pick`/`clear`: it lists the same open/doing
958
- tasks, applies the link through the same `SessionTarget.setTask`, and
959
- stays just as session-only — picking a task there is never persisted to
960
- `config.json` either.
961
-
962
- ### Caching and offline behaviour
963
-
964
- The catalog (clients/projects) is cached machine-wide at
965
- `~/.kankaku/catalog.json`. On `session_start`, for the orchestrator role
966
- with a UI available, kankaku always starts a background refresh when a
967
- cache already exists — regardless of the cache's age — so a client or
968
- project created in the hub minutes ago shows up without waiting for a TTL
969
- to expire (the 6-hour TTL and `isStale()` still exist and still gate other
970
- callers, but session start no longer depends on them). If the target
971
- resolves silently from the project config file or `repo_paths` against
972
- the cached snapshot, `ensurePicked` returns immediately without waiting
973
- for that refresh at all; it keeps running in the background and
974
- `catalog.read()` reflects it once it lands, exactly as before. Only when
975
- the picker is actually about to be shown does kankaku wait for the
976
- in-flight refresh, bounded by a short deadline (1.5s by default,
977
- `pickerRefreshDeadlineMs`): if the hub answers in time, the picker offers
978
- the fresh clients/projects; otherwise (or if the refresh fails) it falls
979
- back to the cached snapshot silently, and the refresh keeps running
980
- in the background rather than being aborted. `/kankaku target pick` (the
981
- explicit re-pick command) follows the same wait-then-fall-back rule. When
982
- there is no cache at all, one refresh is still awaited (bounded by the hub
983
- client's own request timeout, 3s by default) before falling back — this
984
- path is unchanged. If the hub is unreachable and there is no cache,
985
- kankaku notifies once (`kankaku: hub unreachable, using local labels`) and
986
- continues exactly as it would without a hub configured; a background
987
- refresh that merely fails once a cache already exists is silent, with no
988
- notification. `/kankaku catalog refresh` still forces a refresh on demand
989
- independently of any of this. The cache file is always written owner-only
990
- (`0600`); if kankaku is the first thing to ever create `~/.kankaku` itself
991
- (no project has put its own `.kankaku` there), the directory is created
992
- owner-only (`0700`) too — but an already-existing `~/.kankaku` is never
993
- chmod'd, since it may be a project's own kankaku directory (see "The
994
- registry" below for the same rule applied to `run/`).
995
-
996
- ### Privacy (catalog)
997
-
998
- The catalog itself (clients/projects) is read-only — nothing about *that*
999
- data is ever written back. Whether your own work records ever leave the
1000
- machine is a separate, opt-in decision: see "Sync" below.
1001
-
1002
- ### Sync
1003
-
1004
- Once a hub is configured, kankaku can push consolidated **task** rows (see
1005
- "Task and session views" above) to PocketBase, so a project/task manager
1006
- can report AI time and cost per project. This is an outbox pattern:
1007
- `worklog.jsonl` stays the local source of truth, append-only and never
1008
- rewritten, exactly as without a hub. A separate sync step reads it and
1009
- uploads what is pending — nothing in a pi event handler ever waits on the
1010
- network.
1011
-
1012
- **What gets uploaded.** One `task_entries` row per task — never raw
1013
- `WorkRecord`s re-aggregated on the server. The union-of-intervals rule
1014
- (`wallMs`, "Task and session views") is computed exactly once, locally, by
1015
- `buildTasks`; the hub only ever sums already-consolidated rows. When
1016
- `KANKAKU_SYNC_RECORDS` is not `0` (the default), each task's underlying
1017
- `WorkRecord`s are also uploaded as `work_records`, raw per-run detail for
1018
- drilling into a task — these rows overlap each other and must never be
1019
- summed, unlike `task_entries`.
1020
-
1021
- **Idempotency and the revisit window.** Every task is upserted by its id
1022
- (the orchestrator record's `id`), never blindly created — safe to
1023
- re-send. A task is not final the moment its orchestrator settles: a
1024
- background subagent can settle *after* it and extend the task's union
1025
- (`wallMs`, cost, subagent count) for a task that may already be in
1026
- PocketBase. So every sync revisits a trailing window behind its own
1027
- watermark — `KANKAKU_SYNC_WINDOW_HOURS`, 24h by default — and re-evaluates
1028
- every task whose `endedAt` falls inside it. A cheap content hash per task
1029
- (`<KANKAKU_DIR>/sync-state.json`) means an unchanged task inside the window
1030
- costs nothing: running `/kankaku sync` twice in a row performs zero writes.
1031
-
1032
- The window is anchored to `syncedThrough` (the watermark), never to
1033
- current wall-clock time — see "Limitations" below for what that means for
1034
- a background subagent that settles long after its orchestrator, and after
1035
- the directory has otherwise gone quiet.
1036
-
1037
- **Assignment is create-only.** You (or whoever reassigns work in the hub's
1038
- web app) can move a task from one client/project to another directly in
1039
- PocketBase — for example, moving a "Sin determinar" row to its real
1040
- client once you have identified it. A later re-sync of that same task
1041
- **must never undo that**: on create kankaku sends the full row, including
1042
- `client`/`project`/`task`/`legacy_client_label`; on every subsequent update
1043
- it sends measurement fields only (`wall_ms`, `cost`, `status`, ...) and
1044
- never touches assignment fields again. `task` (the linked `tasks` relation)
1045
- is create-only for the exact same reason: reassigning which task a row
1046
- belongs to in the web app is never undone by a later sync. If you need
1047
- kankaku itself to change a task's assignment, do it in the web app, not by
1048
- re-syncing.
1049
-
1050
- **Historical ("Sin determinar") records.** A record with no `clientId`, or
1051
- whose `clientId` no longer resolves in the catalog, is routed to the hub's
1052
- "Sin determinar" (unassigned) client, carrying its old free-text `client`
1053
- label (or `clientName`) forward as `legacy_client_label` — the exact
1054
- mechanism that lets you bulk-reassign "everything that said `cjamar`" once,
1055
- in the web app, from the unassigned queue.
1056
-
1057
- **Agent and measurement quality.** Every `task_entries` row also carries
1058
- who produced it and how well each figure was measured, so the hub can
1059
- label what it has instead of silently blending incompatible numbers from
1060
- different agents: `agent` (`"pi"` for this package), `agent_version` (the
1061
- agent's own version, when it could be determined — never guessed, omitted
1062
- otherwise), `plugin` (`"kankaku"`), `plugin_version` (this package's own
1063
- version), `waiting_quality` (always `"measured"` for kankaku/pi — it
1064
- always instruments waiting time), `cost_quality` (`"measured"` when the
1065
- task's own record or any joined subagent observed a real provider cost
1066
- figure on at least one turn; `"unknown"` when none did, e.g. a
1067
- subscription/OAuth provider that reports no cost — kankaku has no
1068
- token-price estimator, so it never sends `"estimated"`), and
1069
- `subagent_linkage` (`"not_applicable"` when the task opened no subagent
1070
- spans; `"linked"` when at least as many child records were joined as spans
1071
- were opened; `"unlinked"` otherwise — a task-level approximation, since
1072
- there is no per-span correlation id today, see "Subagents" >
1073
- "Limitations"). These are measurement fields, not assignment, but
1074
- `agent`/`agent_version`/`plugin`/`plugin_version` specifically identify
1075
- who *measured* the task, not who *syncs* it: they are taken from the
1076
- orchestrator record's own `agent`/`agentVersion`/`plugin`/`pluginVersion`
1077
- (see "Record schema") when it carries one, and only fall back to the
1078
- syncing process's own identity for a legacy record written before this
1079
- field existed. On create, `agent`/`plugin` are always sent (from the
1080
- record or the fallback). On update, all four are sent when the record
1081
- carries an identity, and OMITTED ENTIRELY for a legacy record — so a
1082
- re-sync by a *different* process (a standalone `kankaku` TUI, or a
1083
- different agent syncing a shared directory) can never overwrite a row's
1084
- original identity with its own. `waiting_quality`/`cost_quality`/
1085
- `subagent_linkage` are unaffected by this and are always sent on both
1086
- create and update. All of these are included in the sync content hash
1087
- (the record's own `agent`/`plugin`, not their versions), so a background
1088
- subagent that joins later, or a task that first gains a who-measured
1089
- identity — improving `cost_quality`/`subagent_linkage`/`agent` without
1090
- changing any other number — still triggers a resync. An older hub
1091
- predating these fields simply ignores them (PocketBase silently drops
1092
- unrecognized fields on write); no capability probing is needed.
1093
-
1094
- **Session directory.** `session_dir` carries a task's non-default session
1095
- directory (`TaskView.sessionDir`, see "Record schema") to the hub, so a
1096
- resumable session can be resumed from the web, not just locally via
1097
- `/kankaku doctor`. It is optional — only present when pi reports a
1098
- non-default session directory — and, like the fields above, a measurement
1099
- field: sent on both create and update, and included in the sync content
1100
- hash so a session dir change alone triggers a resync. Like `repo_project`,
1101
- it is an absolute local filesystem path (username, disk layout) — the same
1102
- category of exposure the hub already accepts for `repo_project`, not a new
1103
- one. An older hub predating this field simply ignores it (PocketBase
1104
- silently drops unrecognized fields on write).
1105
-
1106
- **Privacy.** `KANKAKU_SYNC_PROMPT` controls whether a task's prompt text
1107
- leaves the machine at all: `none` (default — omitted entirely), `truncated`
1108
- (first 120 chars plus `…`), or `full`.
1109
-
1110
- **Commands:**
1111
-
1112
- - `/kankaku sync` — push everything pending (new tasks, plus anything
1113
- inside the revisit window that changed).
1114
- - `/kankaku sync all` — a full re-evaluation: every task, not just the
1115
- window. Safe and cheap to run — the content hash still skips anything
1116
- unchanged.
1117
- - `/kankaku sync status` — the current watermark, a locally-computed
1118
- pending count (no network), how many never-synced tasks fall outside the
1119
- current revisit window (needs `sync all` — see "Limitations" below), and
1120
- the last sync error, if any.
1121
- - `/kankaku backfill` — a full sync, reported grouped by
1122
- `legacy_client_label`: how many tasks went to "Sin determinar" and under
1123
- which old label, so you know what to reassign in the web app's
1124
- unassigned queue. This never rewrites `worklog.jsonl` locally — the
1125
- reassignment happens once, in PocketBase, and survives every future sync
1126
- (see "Assignment is create-only" above).
1127
-
1128
- **Automatic sync.** Unless `KANKAKU_SYNC_AUTO=0`, kankaku also syncs
1129
- automatically on three triggers (orchestrator role only): fire-and-forget
1130
- (never awaited, errors never surface as a failure of the run that
1131
- triggered them) on `session_start` (after crash recovery) and again after
1132
- `agent_settled`; and, on `session_shutdown`, one **awaited**, time-bounded
1133
- sync — pi awaits its `session_shutdown` handlers with no timeout of its
1134
- own, so this is the one place kankaku's own handler awaits the network, up
1135
- to `shutdownSyncTimeoutMs` (default 3 s). This is what makes the last
1136
- prompt(s) of a session reach the hub when the session ends, rather than
1137
- only on the next session's `session_start`: quitting with an unreachable
1138
- hub costs at most that timeout longer, never more, and cleanup (status
1139
- bar, session-client bookkeeping) still runs even if the sync times out or
1140
- fails. All three triggers share one single-flight guard, so they never
1141
- race each other within a process — a `session_shutdown` sync that arrives
1142
- while one is already in flight awaits that same one rather than starting a
1143
- second — and a lock file (`<KANKAKU_DIR>/sync.lock`, an atomic
1144
- exclusive-create so two racing processes can never both acquire it, stale
1145
- after 5 minutes) keeps two pi processes from syncing the same directory
1146
- concurrently. Subagents never sync. None of the three triggers notify on
1147
- success; on failure (including a shutdown timeout) they notify at most
1148
- once per session (`kankaku: sync failed: ...` / `kankaku: shutdown sync
1149
- timed out`) — check `/kankaku sync status` for the details, including on a
1150
- later run.
1151
-
1152
- The automatic path is cheap on every prompt, not just fire-and-forget: it
1153
- skips entirely (no read of `worklog.jsonl`, no network) when the log has
1154
- not changed since the last successful sync, for all three triggers.
1155
- Otherwise, only `agent_settled` — fired once per prompt — is throttled, to
1156
- at most once per `KANKAKU_SYNC_MIN_INTERVAL_MINUTES` (default 5; `0`
1157
- disables the throttle); since right after `agent_settled` the log *has*
1158
- just changed (a record was just appended), this throttle is what actually
1159
- keeps that trigger cheap. `session_start` and `session_shutdown` never
1160
- throttle: a session boundary is worth catching up on regardless of how
1161
- recently the last automatic run happened, so a stuck hub does not stay
1162
- silently unsynced across restarts, and the shutdown sync is already
1163
- bounded by its own timeout. None of this ever applies to a manual
1164
- `/kankaku sync`, `sync all`, or `backfill`.
1165
-
1166
- **Network/validation failures.** A network or server (5xx) error stops a
1167
- sync run where it is and does not advance its watermark past the failing
1168
- task — nothing is lost, and the next sync (manual or automatic) picks up
1169
- exactly there. A task that fails **validation** (e.g. a genuinely malformed
1170
- payload) is recorded with its reason and skipped — not retried on every
1171
- single run — but is retried automatically the moment its content changes.
1172
-
1173
- **Limitations:**
1174
-
1175
- - Sync state (`sync-state.json`) is per repository/machine, not
1176
- centralized; there is no standalone CLI entry point yet (`npx kankaku
1177
- sync` outside of pi) — see "Roadmap".
1178
- - **A late background child, and the revisit window (R3).** A background
1179
- subagent can settle well after its (possibly cross-worktree)
1180
- orchestrator process has already exited — its record still writes
1181
- correctly into the orchestrator's `worklog.jsonl` (see "Subagents" >
1182
- "Cross-worktree write routing"), but nothing *syncs* it until that
1183
- directory is next visited: pi opened there again (`session_start`'s
1184
- auto-sync), or `/kankaku sync`/`sync all` run there manually. Subagents
1185
- themselves never sync (see "Automatic sync" above). An ordinary
1186
- incremental sync then picks the late child up wherever the task sits: a
1187
- task the hub **already holds** is re-synced whenever its content changed,
1188
- inside the revisit window or not, so a row on the hub never goes stale —
1189
- including a task that *shrank* because a child moved to another task. The
1190
- window (`syncedThrough - windowHours`) only bounds how far back work that
1191
- was **never synced** is looked for; `/kankaku sync status` reports how
1192
- many such tasks there are, and `/kankaku sync all` (or `backfill`)
1193
- uploads them.
1194
-
1195
- **What the stale count means.** Only tasks outside the window that were
1196
- never synced to this hub. A task the hub already holds never appears
1197
- here: if it changed it is simply re-synced.
1198
-
1199
- ## Tagged segments
1200
-
1201
- While a run is open, kankaku can also time tool executions that match a
1202
- configured rule and tag the resulting span with a name — for example,
1203
- knowing how much of a task went to gentle-ai's review-with-receipts step,
1204
- which runs as `gentle-ai review ...` commands through the `bash` tool inside
1205
- the prompt's run.
1206
-
1207
- The default rule tags `review`: tool `bash` running a command matching
1208
- `/\bgentle-ai review\b/`. Configure rules with `KANKAKU_SEGMENTS`, a
1209
- `;`-separated list of `tag=tool:regex` entries, e.g.:
392
+ The Quick actions panel sits below the Hub card in wide mode (100+
393
+ columns), or right after the Projects table in stacked mode (70-99
394
+ columns):
1210
395
 
1211
396
  ```
1212
- KANKAKU_SEGMENTS="review=bash:gentle-ai review;commit=bash:git commit"
1213
- ```
1214
-
1215
- Setting `KANKAKU_SEGMENTS` replaces the default rule entirely; malformed
1216
- entries (missing tag, tool or regex, or an invalid regex) are skipped.
1217
- When several rules could match the same tool call, only the first one
1218
- applies. A `WorkRecord`'s `segments` field is the **union** of milliseconds
1219
- per tag within that one record, so overlapping matching calls are not
1220
- double-counted. `TaskView.segments` and `SessionView.segments` are instead
1221
- the **sum** of `segments` across the orchestrator and its children (or
1222
- across a session's tasks): segment spans are not persisted to
1223
- `worklog.jsonl`, so once a record settles there is nothing left to union
1224
- across records, only per-record totals to add up.
1225
-
1226
- Note that the reviewer's own token cost is not observable here: gentle-pi
1227
- runs it with `--no-extensions`, so kankaku never sees the reviewer's own
1228
- prompt/tool events, only the `bash` call the orchestrator makes to invoke
1229
- it.
1230
-
1231
- ## Crash recovery
1232
-
1233
- While a run is open, each pi process writes a checkpoint of its current
1234
- record to `<KANKAKU_DIR>/inflight/<pid>.json` — first as soon as the run
1235
- starts (`before_agent_start`), so even a crash on the very first turn still
1236
- leaves a checkpoint, and then again after every `turn_end` and
1237
- `tool_execution_end` — and removes it on a normal
1238
- `agent_settled`/`session_shutdown`. If the process is killed outright
1239
- (`kill -9`, power loss) before it can settle, the checkpoint file survives
1240
- it. On the next pi start, `session_start` scans `inflight/` for checkpoints
1241
- whose owning pid is no longer alive, appends each one to `worklog.jsonl` as
1242
- `interrupted`, deletes the checkpoint file, and shows a
1243
- `kankaku: recovered N interrupted record(s)` notice. `settledAt` on a
1244
- recovered record is the time of its last checkpoint, not the actual crash
1245
- time, so `wallMs`/`workMs` are a **lower bound** on the real duration.
1246
-
1247
- The same scan also sweeps `inflight/` for orphaned `.tmp` files: `save`
1248
- writes to a temp file before renaming it into place, and a process killed
1249
- between those two steps leaves the temp file behind. A stray `.tmp` file is
1250
- deleted once its writer pid is no longer alive (or its name cannot be
1251
- parsed); one still owned by a live writer — including this very process's
1252
- own in-progress write — is left alone.
1253
-
1254
- ## Export
1255
-
1256
- `/kankaku export [csv|json] [all]` writes one flat row per task (today's
1257
- tasks by default, or every task with `all`) to
1258
- `<KANKAKU_DIR>/export/tasks-<YYYY-MM-DD or all>.<csv|json>`, and confirms
1259
- with the file's path and row count via the durable report card. Format
1260
- defaults to `csv`; each subagent's own time is folded into its task's row
1261
- rather than exported separately (see "Task and session views").
1262
-
1263
- Columns (in this order for CSV; the same fields for JSON):
1264
-
1265
- | Column | Meaning |
1266
- | --- | --- |
1267
- | `id` | Task id (the orchestrator record's `id`). |
1268
- | `day` | Local calendar day (`YYYY-MM-DD`) the task started on. |
1269
- | `startedAt` / `endedAt` | ISO timestamps of the task's span. |
1270
- | `client` | Billing client, or empty when unresolved. |
1271
- | `sessionName` | pi session display name, or empty. |
1272
- | `sessionId` | pi session id, or empty. |
1273
- | `project` | Project cwd. |
1274
- | `status` | `completed`, `aborted`, or `interrupted`. |
1275
- | `prompt` | First 200 chars of the prompt, newlines collapsed to spaces. |
1276
- | `wallMs` / `waitingMs` / `workMs` | Union-based task timings (see "Task and session views"). |
1277
- | `cost` | Estimated USD cost, orchestrator plus subagents. |
1278
- | `tokensIn` / `tokensOut` / `cacheRead` | Token usage totals. |
1279
- | `subagentCount` | Number of subagent records matched to the task. |
1280
- | `segments` | JSON-encoded per-tag segment totals (see "Tagged segments"). |
1281
- | `model` | The orchestrator record's model, or empty. |
1282
-
1283
- ## Environment variables
1284
-
1285
- - `KANKAKU_DIR`: directory for the work log (`worklog.jsonl`) and the
1286
- crash-recovery checkpoints (`inflight/`, see above), relative to the
1287
- project cwd unless given as an absolute path. Defaults to `.kankaku`.
1288
- - `KANKAKU_INTERACTIVE_TOOLS`: comma-separated list of tool names whose
1289
- execution span counts as waiting time. Defaults to
1290
- `ask_user_question,ask_user_choice`.
1291
- - `KANKAKU_SEGMENTS`: `;`-separated `tag=tool:regex` rules for tagged
1292
- segments (see above). Defaults to the single `review` rule.
1293
- - `KANKAKU_SUBAGENT_TOOLS`: comma-separated list of additional tool names
1294
- treated as subagent-opening spans, parsed exactly like
1295
- `KANKAKU_INTERACTIVE_TOOLS`. Always additive to the built-in profiles
1296
- (gentle-pi, pi's bundled reference example, pi-subagents) — never
1297
- replaces gentle-pi's own recognition. See "Subagents" > "Subagent
1298
- profiles (phase 6b)". Unset by default (built-in profiles' tool names
1299
- only).
1300
- - `KANKAKU_SUBAGENT_CHILD_ENV`: `;`-separated `NAME=VALUE` (exact match) or
1301
- bare `NAME` (presence-only) child-process env markers that confirm a
1302
- process as the configured tool's subagent — parsed like `KANKAKU_SEGMENTS`,
1303
- malformed entries skipped. The separator is `;`, **not** the `,` that
1304
- `KANKAKU_SUBAGENT_TOOLS` takes: a name that is not a valid environment
1305
- variable name (such as `A,B`) is rejected and reported, never silently
1306
- accepted. A name that looks pi/shell/OS/npm-owned
1307
- (`PI_CODING_AGENT`, `AI_AGENT`, `PATH`, `HOME`, `USER`, `SHELL`, `PWD`,
1308
- `CI`, `LANG`, `TMUX`, or a `PI_`/`TERM`/`LC_`/`NODE_`/`NPM_`/`KANKAKU_`
1309
- prefix, case-insensitive) is rejected outright, and even an accepted
1310
- marker never demotes an interactive session — see "Subagents" > "Subagent
1311
- profiles (phase 6b)" for both layers, and verify with `/kankaku doctor`.
1312
- Unset by default.
1313
- - `KANKAKU_ROLE`: `orchestrator` or `subagent` — an explicit escape hatch
1314
- for this process. Any other value is ignored. Scope it to one
1315
- invocation (`KANKAKU_ROLE=orchestrator pi ...`) — **never export it in
1316
- a shell rc, tmux config, or CI environment file**: a confirmed child
1317
- marker always wins over `KANKAKU_ROLE=orchestrator`, `KANKAKU_ROLE=
1318
- subagent` is ignored for an interactive session, and kankaku strips it
1319
- from the environment it passes to any child it spawns, but none of that
1320
- helps if it reaches a session it was never meant for in the first
1321
- place. See "Subagents" > "Interactive sessions and `KANKAKU_ROLE`" for
1322
- the full precedence.
1323
- - `KANKAKU_CLIENT`: default billing client for this project (see "Billing
1324
- labels" above). Lower precedence than the session-level
1325
- `/kankaku client` override, higher than `<KANKAKU_DIR>/config.json`.
1326
- - `KANKAKU_PB_URL`, `KANKAKU_PB_EMAIL`, `KANKAKU_PB_PASSWORD`: hub
1327
- (PocketBase) credentials (see "Hub (PocketBase)" above). Take precedence,
1328
- field by field, over `~/.kankaku/credentials.json`.
1329
- - `KANKAKU_MACHINE`: this machine's display name for the hub, attached to
1330
- every record as `machine` once the hub is configured. Defaults to the OS
1331
- hostname.
1332
- - `KANKAKU_SYNC_PROMPT`: prompt privacy for sync — `none` (default, omitted
1333
- entirely), `truncated` (first 120 chars + `…`), or `full`. See "Hub
1334
- (PocketBase)" > "Sync" > "Privacy".
1335
- - `KANKAKU_SYNC_WINDOW_HOURS`: how far behind the sync watermark to revisit
1336
- on every run, so a subagent that settles after its orchestrator still
1337
- reaches its task. Defaults to 24; a non-positive or non-numeric value
1338
- falls back to the default.
1339
- - `KANKAKU_SYNC_RECORDS`: `0` disables uploading `work_records` (raw
1340
- per-`WorkRecord` detail); `task_entries` are always uploaded regardless.
1341
- Defaults to enabled.
1342
- - `KANKAKU_SYNC_AUTO`: `0` disables the automatic `session_start`/
1343
- `agent_settled`/`session_shutdown` sync; `/kankaku sync` still works.
1344
- Defaults to enabled.
1345
- - `KANKAKU_SYNC_MIN_INTERVAL_MINUTES`: how often the automatic
1346
- `agent_settled` sync is allowed to actually run, at most — see
1347
- "Automatic sync" above. Defaults to 5; `0` disables the throttle. Only
1348
- ever applies to `agent_settled`: `session_start` and `session_shutdown`
1349
- are never throttled, and none of this applies to a manual `/kankaku
1350
- sync`, `sync all`, or `backfill`.
1351
-
1352
- ## Using kankaku as a library
1353
-
1354
- Besides the pi extension, `kankaku` publishes three compiled, pi-free entry
1355
- points for a plain Node consumer — no pi, no TypeScript loader — such as a
1356
- separate CLI or another agent's plugin (e.g. the `kankaku-claude` package):
1357
-
1358
- - `kankaku/domain` — the pure domain layer: `WorkTracker`, `buildTasks`,
1359
- `unionMs`, and the rest of `src/domain/`.
1360
- - `kankaku/ports` — the port interfaces only (`Clock`, `WorkLog`, `Catalog`,
1361
- `WorkSink`, `ProcessRegistry`, `InflightStore`), for writing your own
1362
- adapters against.
1363
- - `kankaku/hub` — the pi-free adapters: the PocketBase HTTP client and
1364
- catalog/sink, `runSync`, the JSONL work log, the cached catalog, hub
1365
- credentials, and related filesystem helpers; also the report formatters
1366
- and the five report view builders (`formatReport`, `summarize`,
1367
- `buildSummaryView`, `buildTasksView`, etc.), the hub action line-builders
1368
- (`buildSyncStatusLines`, `formatSyncSummaryLines`, ...), the export
1369
- writer (`writeExport`) and the project config reader/writer
1370
- (`readProjectTargetIds`, `writeProjectTargetIds`). These exist so a
1371
- standalone CLI or TUI (e.g. a future Ink-based one) can render the exact
1372
- same reports and hub actions as the `/kankaku` subcommands and panel,
1373
- without reimplementing them. Nothing reachable from this entry point ever
1374
- imports a pi package type.
1375
-
1376
- ```js
1377
- import { runSync } from "kankaku/hub";
1378
- import { buildTasks } from "kankaku/domain";
397
+ ╭─[ Quick actions ]────────────────╮
398
+ │ c refresh catalog │
399
+ │ s sync all projects │
400
+ │ S full sync all │
401
+ │ r reload │
402
+ │ catalog: 9 clients · 17 projects │
403
+ ╰──────────────────────────────────╯
1379
404
  ```
1380
405
 
1381
- Each entry point is compiled ahead of time (`npm run build`, part of `npm
1382
- run check`) to `dist/<domain|ports|hub>/index.{js,d.ts}` and resolved
1383
- through `package.json`'s `exports` map, so importing it never needs type
1384
- stripping or a `.ts` loader. `kankaku/src/*` is how pi itself loads
1385
- `./src/extension.ts` and is not a public API — its shape can change without
1386
- notice; import only `kankaku/domain`, `kankaku/ports`, or `kankaku/hub`.
1387
-
1388
- ## Limitations
1389
-
1390
- - A prompt shown by a tool that does not go through `ctx.ui` and is not
1391
- listed in `KANKAKU_INTERACTIVE_TOOLS` counts as work, not waiting time.
1392
- - Subagent totals are reported separately in the per-role summary and are
1393
- **not** summed into the orchestrator's `wallMs` there: task-mode subagents
1394
- run inside the parent's wall clock, and background subagents can outlive
1395
- the parent's idle moment, so naively adding them would double-count or
1396
- misrepresent billable time. Use the task/session views above (union-based,
1397
- never a sum) for a correct combined figure.
1398
-
1399
- ## Roadmap
1400
-
1401
- - Hub sync (phase 2): push consolidated task rows to PocketBase (outbox
1402
- pattern, idempotent upsert by task id) so a task/project manager can
1403
- report AI time and cost per project. The catalog/selection layer in "Hub
1404
- (PocketBase)" above is phase 1; sync itself ("Hub (PocketBase)" > "Sync")
1405
- is phase 2 — both already shipped.
1406
- - A standalone CLI entry point (`npx kankaku sync`, for a cron/launchd job
1407
- outside of any pi session) is deliberately not included yet. The build
1408
- step it needs now exists — `sync-runner.ts` and its adapters are
1409
- published pi-free and pre-compiled as `kankaku/hub` (see "Using kankaku
1410
- as a library") — but a `bin` script that wires that up as a runnable CLI
1411
- is not; the compiled entry points are today consumed as a library, not a
1412
- binary.
1413
- - Linking a `task_entries` row to an existing `tasks` record (phase 3 in the
1414
- hub's own data model) has shipped: `/kankaku task pick`/`clear` (see
1415
- "Linking to a hub task" above). Creating a task from pi
1416
- (`/kankaku task new`) is deliberately not included — kankaku never
1417
- invents tasks; it only ever links to one already created in the manager.
1418
- - Generic subagent detection (phase 6): 6a fixed the two correctness bugs
1419
- described in "Subagents" above (a phantom-orchestrator double count; a
1420
- gentle-pi cross-worktree child's work going missing). 6b added the
1421
- `SubagentProfile` abstraction, built-in profiles for pi's bundled
1422
- reference example and pi-subagents alongside gentle-pi, and
1423
- `KANKAKU_SUBAGENT_TOOLS`/`KANKAKU_SUBAGENT_CHILD_ENV` for a third-party
1424
- tool kankaku does not recognise out of the box. 6c added subagent-result
1425
- `usage` forwarding and the same-pid overlapping-orchestrator guard — see
1426
- "Subagents" > "Subagent profiles (phase 6b)" / "In-process subagents
1427
- (phase 6c)" above. Built-in profiles beyond these three
1428
- (`pi-background-tasks`, `@d3ara1n/pi-subagent`), gentle-pi handing a
1429
- child its own task id, and the cosmetic `linked_task_id` hub
1430
- self-relation remain out of scope.
406
+ The bottom line is the status line: empty until the first action runs,
407
+ `… <label>` while one is running, its result message once it settles
408
+ (e.g. the catalog refresh above, or a sync summary), `error: <message>`
409
+ if it failed, or `hub not configured (~/.kankaku/credentials.json)` when
410
+ the hub has no credentials — in which case `c`/`s`/`S` do nothing.
411
+
412
+ - **Tasks** — a table (time, project, work, cost, prompt) with a
413
+ highlighted row on the left, and a `[ Task ]` detail panel on the right
414
+ showing the selected row's full prompt, client, project, hub task,
415
+ wall/work/wait time, cost, cache hit and subagent count.
416
+ - **Catalog** — `[ Clients ]` on the left; the selected client's
417
+ `[ Projects ]`, with open/doing hub task counts, on the right. The
418
+ Clients panel header shows the cache's age and a `(stale)` flag.
419
+ - **Sync** — one card per project in a wrapping grid; the selected card is
420
+ highlighted, and each action's result line shows inside its card while
421
+ it runs and once it settles.
422
+
423
+ ## Keys (TUI)
424
+
425
+ The app has two focus zones — the sidebar and the active screen's own main
426
+ content — and one of them always has focus (`domain/nav-model.ts`'s
427
+ `NavState.focus`, starting on the sidebar). `1`-`4` switch the Dashboard/
428
+ Tasks/Catalog/Sync tab bar and `q` quits from anywhere, in either zone; every
429
+ other key belongs to whichever zone currently has focus, so a screen's own
430
+ list never moves by accident while you are still picking a screen.
431
+
432
+ - **Sidebar focused** (the app's own starting state) — `↑`/`↓` move
433
+ between screens, and the screen switches as you move, so you see each
434
+ one before committing to it. `enter`, `→` or `Tab` focus the main zone
435
+ (the screen you last landed on).
436
+ - **Main zone focused** — the active screen's own keys work as below.
437
+ `←` or `Tab` return focus to the sidebar. `esc` also returns to the
438
+ sidebar, unless the screen consumes it first: on Tasks with a project
439
+ filter set (from Dashboard's `enter`), the first `esc` clears the filter
440
+ and the next `esc` returns to the sidebar.
441
+
442
+ The focused zone is visible in the frame: the sidebar's active-item marker
443
+ is in the accent colour when the sidebar is focused and muted otherwise,
444
+ the focused screen's primary panel gets the accent border, and the footer
445
+ key hints change — the sidebar's own hints while it is focused, the
446
+ screen's hints plus `← menu` while the main zone is focused.
447
+
448
+ The app fills the whole terminal; every scrolling list (Tasks, Catalog's
449
+ Clients/Projects, Dashboard's Projects, Sync's cards) additionally takes
450
+ `PageUp`/`PageDown` to move a full window at a time and `Home`/`End` to
451
+ jump to the first/last row.
452
+
453
+ - **Dashboard** — `↑`/`↓`/`PageUp`/`PageDown`/`Home`/`End` move the
454
+ Projects selection, `enter` opens the selected project in Tasks
455
+ (filtered to it), `r` refresh; the Quick actions panel additionally
456
+ takes `c` (refresh catalog), `s` (sync all projects) and `S` (full sync
457
+ all) — one at a time, ignored while another is running.
458
+ - **Tasks** — `a` toggle today/all, `↑`/`↓`/`PageUp`/`PageDown`/`Home`/`End`
459
+ move the selection, `r` refresh, `esc` clears a project filter set from
460
+ Dashboard.
461
+ - **Catalog** — `↑`/`↓`/`PageUp`/`PageDown`/`Home`/`End` move the client
462
+ selection, `r` refresh from the hub.
463
+ - **Sync** — `↑`/`↓`/`PageUp`/`PageDown`/`Home`/`End` move the selection,
464
+ `s` sync the selected project, `f` full-sync the selected project, `S`
465
+ sync every project. Each action's summary shows inline in its card
466
+ while it runs and once it settles.
467
+
468
+ The TUI never writes to disk on its own — Dashboard, Tasks and read-only
469
+ Catalog views write nothing at all; Catalog's `refresh`, Sync's
470
+ `s`/`f`/`S` and Dashboard's Quick actions `c`/`s`/`S` write only through
471
+ kankaku's own adapters (`CachedCatalog`, `SyncStateStore`, the hub
472
+ itself), exactly as kankaku's own sync paths do.