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.
- package/LICENSE +21 -0
- package/README.md +1438 -0
- package/dist/adapters/cached-catalog.d.ts +42 -0
- package/dist/adapters/cached-catalog.js +121 -0
- package/dist/adapters/export-writer.d.ts +13 -0
- package/dist/adapters/export-writer.js +28 -0
- package/dist/adapters/file-modes.d.ts +20 -0
- package/dist/adapters/file-modes.js +34 -0
- package/dist/adapters/hub-actions.d.ts +35 -0
- package/dist/adapters/hub-actions.js +70 -0
- package/dist/adapters/hub-credentials.d.ts +35 -0
- package/dist/adapters/hub-credentials.js +58 -0
- package/dist/adapters/jsonl-work-log.d.ts +20 -0
- package/dist/adapters/jsonl-work-log.js +62 -0
- package/dist/adapters/kankaku-dir.d.ts +38 -0
- package/dist/adapters/kankaku-dir.js +85 -0
- package/dist/adapters/lazy-jsonl-work-log.d.ts +17 -0
- package/dist/adapters/lazy-jsonl-work-log.js +31 -0
- package/dist/adapters/pocketbase-catalog.d.ts +16 -0
- package/dist/adapters/pocketbase-catalog.js +56 -0
- package/dist/adapters/pocketbase-client.d.ts +81 -0
- package/dist/adapters/pocketbase-client.js +148 -0
- package/dist/adapters/pocketbase-sink.d.ts +53 -0
- package/dist/adapters/pocketbase-sink.js +181 -0
- package/dist/adapters/project-config.d.ts +42 -0
- package/dist/adapters/project-config.js +108 -0
- package/dist/adapters/report-data.d.ts +12 -0
- package/dist/adapters/report-data.js +8 -0
- package/dist/adapters/report-views.d.ts +45 -0
- package/dist/adapters/report-views.js +73 -0
- package/dist/adapters/report.d.ts +112 -0
- package/dist/adapters/report.js +236 -0
- package/dist/adapters/sync-runner.d.ts +114 -0
- package/dist/adapters/sync-runner.js +273 -0
- package/dist/adapters/sync-state-store.d.ts +62 -0
- package/dist/adapters/sync-state-store.js +188 -0
- package/dist/config.d.ts +168 -0
- package/dist/config.js +392 -0
- package/dist/domain/ancestry-match.d.ts +49 -0
- package/dist/domain/ancestry-match.js +82 -0
- package/dist/domain/client-label.d.ts +28 -0
- package/dist/domain/client-label.js +44 -0
- package/dist/domain/day.d.ts +2 -0
- package/dist/domain/day.js +8 -0
- package/dist/domain/export.d.ts +38 -0
- package/dist/domain/export.js +68 -0
- package/dist/domain/hub-entry.d.ts +234 -0
- package/dist/domain/hub-entry.js +265 -0
- package/dist/domain/index.d.ts +19 -0
- package/dist/domain/index.js +19 -0
- package/dist/domain/intervals.d.ts +17 -0
- package/dist/domain/intervals.js +43 -0
- package/dist/domain/registry-health.d.ts +49 -0
- package/dist/domain/registry-health.js +58 -0
- package/dist/domain/segment-rule.d.ts +10 -0
- package/dist/domain/segment-rule.js +1 -0
- package/dist/domain/subagent-profile.d.ts +278 -0
- package/dist/domain/subagent-profile.js +418 -0
- package/dist/domain/sync-plan.d.ts +151 -0
- package/dist/domain/sync-plan.js +196 -0
- package/dist/domain/task-view.d.ts +117 -0
- package/dist/domain/task-view.js +428 -0
- package/dist/domain/work-record.d.ts +236 -0
- package/dist/domain/work-record.js +91 -0
- package/dist/domain/work-target.d.ts +101 -0
- package/dist/domain/work-target.js +149 -0
- package/dist/domain/work-tracker.d.ts +90 -0
- package/dist/domain/work-tracker.js +405 -0
- package/dist/hub/index.d.ts +25 -0
- package/dist/hub/index.js +25 -0
- package/dist/ports/catalog.d.ts +31 -0
- package/dist/ports/catalog.js +1 -0
- package/dist/ports/clock.d.ts +3 -0
- package/dist/ports/clock.js +1 -0
- package/dist/ports/index.d.ts +11 -0
- package/dist/ports/index.js +1 -0
- package/dist/ports/inflight-store.d.ts +15 -0
- package/dist/ports/inflight-store.js +1 -0
- package/dist/ports/process-registry.d.ts +72 -0
- package/dist/ports/process-registry.js +1 -0
- package/dist/ports/work-log.d.ts +14 -0
- package/dist/ports/work-log.js +1 -0
- package/dist/ports/work-sink.d.ts +39 -0
- package/dist/ports/work-sink.js +1 -0
- package/package.json +66 -0
- package/src/adapters/agent-info.ts +86 -0
- package/src/adapters/ancestry.ts +260 -0
- package/src/adapters/cached-catalog.ts +147 -0
- package/src/adapters/export-writer.ts +33 -0
- package/src/adapters/file-inflight-store.ts +115 -0
- package/src/adapters/file-modes.ts +35 -0
- package/src/adapters/hub-actions.ts +82 -0
- package/src/adapters/hub-credentials.ts +95 -0
- package/src/adapters/jsonl-work-log.ts +67 -0
- package/src/adapters/kankaku-command.ts +717 -0
- package/src/adapters/kankaku-dir.ts +102 -0
- package/src/adapters/lazy-file-inflight-store.ts +43 -0
- package/src/adapters/lazy-jsonl-work-log.ts +39 -0
- package/src/adapters/machine-process-registry.ts +256 -0
- package/src/adapters/panel/kankaku-panel.ts +419 -0
- package/src/adapters/panel/panel-items.ts +87 -0
- package/src/adapters/panel/panel-lines.ts +13 -0
- package/src/adapters/panel/panel-theme.ts +32 -0
- package/src/adapters/panel/screens/about.ts +69 -0
- package/src/adapters/panel/screens/doctor.ts +89 -0
- package/src/adapters/panel/screens/export.ts +123 -0
- package/src/adapters/panel/screens/report.ts +143 -0
- package/src/adapters/panel/screens/sync.ts +136 -0
- package/src/adapters/panel/screens/target.ts +384 -0
- package/src/adapters/pi-tracker.ts +753 -0
- package/src/adapters/pocketbase-catalog.ts +89 -0
- package/src/adapters/pocketbase-client.ts +197 -0
- package/src/adapters/pocketbase-sink.ts +236 -0
- package/src/adapters/process-identity-memo.ts +102 -0
- package/src/adapters/process-identity.ts +162 -0
- package/src/adapters/project-config.ts +116 -0
- package/src/adapters/report-data.ts +13 -0
- package/src/adapters/report-views.ts +98 -0
- package/src/adapters/report.ts +335 -0
- package/src/adapters/session-client.ts +116 -0
- package/src/adapters/session-dir.ts +28 -0
- package/src/adapters/session-target.ts +431 -0
- package/src/adapters/status-bar.ts +86 -0
- package/src/adapters/subagent-startup.ts +66 -0
- package/src/adapters/sync-runner.ts +340 -0
- package/src/adapters/sync-state-store.ts +227 -0
- package/src/adapters/target-picker.ts +127 -0
- package/src/config.ts +536 -0
- package/src/domain/ancestry-match.ts +84 -0
- package/src/domain/client-label.ts +56 -0
- package/src/domain/day.ts +8 -0
- package/src/domain/export.ts +107 -0
- package/src/domain/hub-entry.ts +433 -0
- package/src/domain/index.ts +19 -0
- package/src/domain/intervals.ts +53 -0
- package/src/domain/panel-model.ts +270 -0
- package/src/domain/registry-health.ts +87 -0
- package/src/domain/segment-rule.ts +10 -0
- package/src/domain/subagent-profile.ts +495 -0
- package/src/domain/sync-plan.ts +266 -0
- package/src/domain/task-view.ts +526 -0
- package/src/domain/work-record.ts +320 -0
- package/src/domain/work-target.ts +234 -0
- package/src/domain/work-tracker.ts +485 -0
- package/src/extension.ts +346 -0
- package/src/hub/index.ts +25 -0
- package/src/ports/catalog.ts +33 -0
- package/src/ports/clock.ts +3 -0
- package/src/ports/index.ts +11 -0
- package/src/ports/inflight-store.ts +16 -0
- package/src/ports/process-registry.ts +75 -0
- package/src/ports/work-log.ts +15 -0
- 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.
|