kankaku-pi 1.0.0

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