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.
- package/README.md +438 -1396
- package/dist/adapters/app-info.js +13 -0
- package/dist/adapters/hub-manager/accounts.js +52 -0
- package/dist/adapters/hub-manager/download.js +49 -0
- package/dist/adapters/hub-manager/install.js +331 -0
- package/dist/adapters/hub-manager/package.js +35 -0
- package/dist/adapters/hub-manager/process.js +113 -0
- package/dist/adapters/hub-manager/zip.js +83 -0
- package/dist/adapters/hub.js +101 -0
- package/dist/adapters/project-discovery.js +100 -0
- package/dist/adapters/setup/agents.js +167 -0
- package/dist/adapters/setup/child-process-runner.js +33 -0
- package/dist/adapters/setup/claude-commands.js +122 -0
- package/dist/adapters/setup/claude-plugin.js +63 -0
- package/dist/adapters/setup/claude.js +109 -0
- package/dist/adapters/setup/hub.js +52 -0
- package/dist/adapters/setup/json-writer.js +68 -0
- package/dist/adapters/setup/local-hub.js +86 -0
- package/dist/adapters/setup/pi.js +41 -0
- package/dist/adapters/setup/readline-prompter.js +68 -0
- package/dist/adapters/setup/tui-config.js +34 -0
- package/dist/adapters/tui-config.js +35 -0
- package/dist/adapters/worklog-reader.js +9 -0
- package/dist/cli.js +902 -0
- package/dist/domain/catalog-model.js +45 -0
- package/dist/domain/claude-integration.js +72 -0
- package/dist/domain/dashboard-model.js +83 -0
- package/dist/domain/kankaku-package.js +121 -0
- package/dist/domain/list-window.js +31 -0
- package/dist/domain/local-hub-model.js +179 -0
- package/dist/domain/nav-model.js +69 -0
- package/dist/domain/quick-actions.js +55 -0
- package/dist/domain/setup-plan.js +172 -0
- package/dist/domain/setup-wizard.js +217 -0
- package/dist/domain/sync-model.js +17 -0
- package/dist/domain/tasks-model.js +51 -0
- package/dist/domain/text-wrap.js +47 -0
- package/dist/domain/today-model.js +75 -0
- package/dist/ui/app.js +74 -0
- package/dist/ui/catalog-screen.js +100 -0
- package/dist/ui/components/bar.js +20 -0
- package/dist/ui/components/checklist.js +34 -0
- package/dist/ui/components/header-bar.js +8 -0
- package/dist/ui/components/key-hints.js +8 -0
- package/dist/ui/components/panel.js +32 -0
- package/dist/ui/components/radio.js +29 -0
- package/dist/ui/components/sidebar.js +30 -0
- package/dist/ui/components/sparkline.js +19 -0
- package/dist/ui/components/table.js +55 -0
- package/dist/ui/components/text-input.js +64 -0
- package/dist/ui/dashboard-screen.js +242 -0
- package/dist/ui/layout.js +54 -0
- package/dist/ui/setup/wizard-screen.js +179 -0
- package/dist/ui/sync-screen.js +127 -0
- package/dist/ui/tasks-screen.js +166 -0
- package/dist/ui/theme.js +81 -0
- package/package.json +33 -41
- package/vendor/kankaku-pi/LICENSE +21 -0
- package/vendor/kankaku-pi/package.json +66 -0
- package/{src → vendor/kankaku-pi/src}/adapters/report-data.ts +1 -1
- package/{src → vendor/kankaku-pi/src}/adapters/sync-runner.ts +2 -2
- package/{src → vendor/kankaku-pi/src}/domain/index.ts +1 -1
- package/{src → vendor/kankaku-pi/src}/hub/index.ts +1 -1
- package/{src → vendor/kankaku-pi/src}/ports/index.ts +1 -1
- package/dist/adapters/cached-catalog.d.ts +0 -42
- package/dist/adapters/cached-catalog.js +0 -121
- package/dist/adapters/export-writer.d.ts +0 -13
- package/dist/adapters/export-writer.js +0 -28
- package/dist/adapters/file-modes.d.ts +0 -20
- package/dist/adapters/file-modes.js +0 -34
- package/dist/adapters/hub-actions.d.ts +0 -35
- package/dist/adapters/hub-actions.js +0 -70
- package/dist/adapters/hub-credentials.d.ts +0 -35
- package/dist/adapters/hub-credentials.js +0 -58
- package/dist/adapters/jsonl-work-log.d.ts +0 -20
- package/dist/adapters/jsonl-work-log.js +0 -62
- package/dist/adapters/kankaku-dir.d.ts +0 -38
- package/dist/adapters/kankaku-dir.js +0 -85
- package/dist/adapters/lazy-jsonl-work-log.d.ts +0 -17
- package/dist/adapters/lazy-jsonl-work-log.js +0 -31
- package/dist/adapters/pocketbase-catalog.d.ts +0 -16
- package/dist/adapters/pocketbase-catalog.js +0 -56
- package/dist/adapters/pocketbase-client.d.ts +0 -81
- package/dist/adapters/pocketbase-client.js +0 -148
- package/dist/adapters/pocketbase-sink.d.ts +0 -53
- package/dist/adapters/pocketbase-sink.js +0 -181
- package/dist/adapters/project-config.d.ts +0 -42
- package/dist/adapters/project-config.js +0 -108
- package/dist/adapters/report-data.d.ts +0 -12
- package/dist/adapters/report-data.js +0 -8
- package/dist/adapters/report-views.d.ts +0 -45
- package/dist/adapters/report-views.js +0 -73
- package/dist/adapters/report.d.ts +0 -112
- package/dist/adapters/report.js +0 -236
- package/dist/adapters/sync-runner.d.ts +0 -114
- package/dist/adapters/sync-runner.js +0 -273
- package/dist/adapters/sync-state-store.d.ts +0 -62
- package/dist/adapters/sync-state-store.js +0 -188
- package/dist/config.d.ts +0 -168
- package/dist/config.js +0 -392
- package/dist/domain/ancestry-match.d.ts +0 -49
- package/dist/domain/ancestry-match.js +0 -82
- package/dist/domain/client-label.d.ts +0 -28
- package/dist/domain/client-label.js +0 -44
- package/dist/domain/day.d.ts +0 -2
- package/dist/domain/day.js +0 -8
- package/dist/domain/export.d.ts +0 -38
- package/dist/domain/export.js +0 -68
- package/dist/domain/hub-entry.d.ts +0 -234
- package/dist/domain/hub-entry.js +0 -265
- package/dist/domain/index.d.ts +0 -19
- package/dist/domain/index.js +0 -19
- package/dist/domain/intervals.d.ts +0 -17
- package/dist/domain/intervals.js +0 -43
- package/dist/domain/registry-health.d.ts +0 -49
- package/dist/domain/registry-health.js +0 -58
- package/dist/domain/segment-rule.d.ts +0 -10
- package/dist/domain/subagent-profile.d.ts +0 -278
- package/dist/domain/subagent-profile.js +0 -418
- package/dist/domain/sync-plan.d.ts +0 -151
- package/dist/domain/sync-plan.js +0 -196
- package/dist/domain/task-view.d.ts +0 -117
- package/dist/domain/task-view.js +0 -428
- package/dist/domain/work-record.d.ts +0 -236
- package/dist/domain/work-record.js +0 -91
- package/dist/domain/work-target.d.ts +0 -101
- package/dist/domain/work-target.js +0 -149
- package/dist/domain/work-tracker.d.ts +0 -90
- package/dist/domain/work-tracker.js +0 -405
- package/dist/hub/index.d.ts +0 -25
- package/dist/hub/index.js +0 -25
- package/dist/ports/catalog.d.ts +0 -31
- package/dist/ports/clock.d.ts +0 -3
- package/dist/ports/index.d.ts +0 -11
- package/dist/ports/index.js +0 -1
- package/dist/ports/inflight-store.d.ts +0 -15
- package/dist/ports/inflight-store.js +0 -1
- package/dist/ports/process-registry.d.ts +0 -72
- package/dist/ports/process-registry.js +0 -1
- package/dist/ports/work-log.d.ts +0 -14
- package/dist/ports/work-log.js +0 -1
- package/dist/ports/work-sink.d.ts +0 -39
- package/dist/ports/work-sink.js +0 -1
- /package/dist/{domain/segment-rule.js → ports/project-source.js} +0 -0
- /package/dist/ports/{catalog.js → prompter.js} +0 -0
- /package/dist/ports/{clock.js → script-runner.js} +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/agent-info.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/ancestry.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/cached-catalog.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/export-writer.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/file-inflight-store.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/file-modes.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/hub-actions.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/hub-credentials.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/jsonl-work-log.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/kankaku-command.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/kankaku-dir.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/lazy-file-inflight-store.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/lazy-jsonl-work-log.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/machine-process-registry.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/panel/kankaku-panel.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/panel/panel-items.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/panel/panel-lines.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/panel/panel-theme.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/panel/screens/about.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/panel/screens/doctor.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/panel/screens/export.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/panel/screens/report.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/panel/screens/sync.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/panel/screens/target.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/pi-tracker.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/pocketbase-catalog.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/pocketbase-client.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/pocketbase-sink.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/process-identity-memo.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/process-identity.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/project-config.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/report-views.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/report.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/session-client.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/session-dir.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/session-target.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/status-bar.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/subagent-startup.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/sync-state-store.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/adapters/target-picker.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/config.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/domain/ancestry-match.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/domain/client-label.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/domain/day.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/domain/export.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/domain/hub-entry.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/domain/intervals.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/domain/panel-model.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/domain/registry-health.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/domain/segment-rule.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/domain/subagent-profile.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/domain/sync-plan.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/domain/task-view.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/domain/work-record.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/domain/work-target.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/domain/work-tracker.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/extension.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/ports/catalog.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/ports/clock.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/ports/inflight-store.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/ports/process-registry.ts +0 -0
- /package/{src → vendor/kankaku-pi/src}/ports/work-log.ts +0 -0
- /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
|
|
4
|
-
|
|
5
|
-
(
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
`
|
|
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
|
-
##
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
`
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
57
|
+
```
|
|
58
|
+
npm uninstall -g kankaku-tui
|
|
59
|
+
npm i -g kankaku
|
|
60
|
+
kankaku setup
|
|
61
|
+
```
|
|
45
62
|
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
##
|
|
193
|
+
## Configuration
|
|
78
194
|
|
|
79
|
-
|
|
195
|
+
`~/.kankaku/tui.json`:
|
|
80
196
|
|
|
81
197
|
```json
|
|
82
198
|
{
|
|
83
|
-
"
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
`
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
`
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
227
|
+
```json
|
|
228
|
+
{
|
|
229
|
+
"url": "https://your-hub.example.com",
|
|
230
|
+
"email": "you@example.com",
|
|
231
|
+
"password": "…"
|
|
232
|
+
}
|
|
515
233
|
```
|
|
516
234
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
`
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
kankaku
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
-
|
|
592
|
-
(`
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
-
|
|
874
|
-
|
|
875
|
-
|
|
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
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
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
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
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.
|