kankaku 0.6.0 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -8
- package/dist/adapters/cached-catalog.d.ts +8 -2
- package/dist/adapters/cached-catalog.js +13 -3
- package/dist/adapters/pocketbase-catalog.d.ts +7 -5
- package/dist/adapters/pocketbase-catalog.js +22 -5
- package/dist/adapters/pocketbase-sink.d.ts +2 -1
- package/dist/adapters/pocketbase-sink.js +2 -1
- package/dist/domain/hub-entry.d.ts +12 -3
- package/dist/domain/hub-entry.js +14 -4
- package/dist/domain/task-view.d.ts +4 -0
- package/dist/domain/task-view.js +2 -0
- package/dist/domain/work-record.d.ts +18 -0
- package/dist/domain/work-record.js +18 -0
- package/dist/domain/work-target.d.ts +30 -1
- package/dist/domain/work-target.js +33 -11
- package/dist/ports/catalog.d.ts +4 -2
- package/package.json +1 -1
- package/src/adapters/cached-catalog.ts +22 -6
- package/src/adapters/kankaku-command.ts +43 -1
- package/src/adapters/pi-tracker.ts +2 -0
- package/src/adapters/pocketbase-catalog.ts +36 -7
- package/src/adapters/pocketbase-sink.ts +15 -3
- package/src/adapters/report.ts +66 -16
- package/src/adapters/session-target.ts +64 -4
- package/src/adapters/target-picker.ts +46 -1
- package/src/domain/hub-entry.ts +20 -5
- package/src/domain/task-view.ts +6 -0
- package/src/domain/work-record.ts +23 -0
- package/src/domain/work-target.ts +60 -11
- package/src/extension.ts +1 -0
- package/src/ports/catalog.ts +4 -2
package/src/domain/hub-entry.ts
CHANGED
|
@@ -13,13 +13,15 @@
|
|
|
13
13
|
import type { TaskView } from "./task-view.ts";
|
|
14
14
|
import type { WorkRecord, WorkRole, WorkStatus } from "./work-record.ts";
|
|
15
15
|
import { finiteOrZero } from "./work-record.ts";
|
|
16
|
-
import type { Client, Project } from "./work-target.ts";
|
|
16
|
+
import type { Client, HubTask, Project } from "./work-target.ts";
|
|
17
17
|
|
|
18
18
|
export type PromptPrivacyMode = "none" | "truncated" | "full";
|
|
19
19
|
|
|
20
20
|
export interface HubEntryContext {
|
|
21
21
|
clients: Client[];
|
|
22
22
|
projects: Project[];
|
|
23
|
+
/** The hub's known tasks, to validate a task's linked `hubTaskId` against. See {@link resolveTaskAssignment}. */
|
|
24
|
+
tasks: HubTask[];
|
|
23
25
|
/** This machine's hostname or `KANKAKU_MACHINE`. */
|
|
24
26
|
machine: string;
|
|
25
27
|
/** `KANKAKU_SYNC_PROMPT`; see {@link applyPromptPrivacy}. */
|
|
@@ -39,6 +41,8 @@ export interface TaskAssignment {
|
|
|
39
41
|
clientId: string;
|
|
40
42
|
/** `projects` relation id, or `""`. */
|
|
41
43
|
projectId: string;
|
|
44
|
+
/** `tasks` relation id, or `""` when no usable hub task could be resolved. See {@link resolveTaskAssignment}. */
|
|
45
|
+
hubTaskId: string;
|
|
42
46
|
/** Only set (non-empty) for a task routed to the unassigned client. */
|
|
43
47
|
legacyClientLabel: string;
|
|
44
48
|
/** `true` when this task was routed to the catalog's unassigned ("Sin determinar") client. */
|
|
@@ -46,7 +50,8 @@ export interface TaskAssignment {
|
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
/**
|
|
49
|
-
* Resolve which client/project a task's `task_entries` row should
|
|
53
|
+
* Resolve which client/project/hub-task a task's `task_entries` row should
|
|
54
|
+
* link to.
|
|
50
55
|
*
|
|
51
56
|
* - A task whose `clientId` still exists in `clients` links to that client
|
|
52
57
|
* (regardless of its `active` flag — this is a historical fact, not a
|
|
@@ -57,16 +62,25 @@ export interface TaskAssignment {
|
|
|
57
62
|
* `unassigned: true`), carrying forward the record's free-text `client`
|
|
58
63
|
* label (or its `clientName` when the label itself is absent) as
|
|
59
64
|
* `legacyClientLabel` — the historical backfill rule (proposal §5.3).
|
|
65
|
+
* - `hubTaskId` is kept only when it still exists in `tasks` AND belongs to
|
|
66
|
+
* the resolved `projectId` (non-empty); otherwise it is `""` — including
|
|
67
|
+
* whenever the client fell back to unassigned, since there is then no
|
|
68
|
+
* resolved project for a task to belong to.
|
|
60
69
|
*/
|
|
61
|
-
export function resolveTaskAssignment(task: TaskView, clients: Client[], projects: Project[]): TaskAssignment {
|
|
70
|
+
export function resolveTaskAssignment(task: TaskView, clients: Client[], projects: Project[], tasks: HubTask[]): TaskAssignment {
|
|
62
71
|
const client = task.clientId !== undefined ? clients.find((candidate) => candidate.id === task.clientId) : undefined;
|
|
63
72
|
|
|
64
73
|
if (client) {
|
|
65
74
|
const project =
|
|
66
75
|
task.projectId !== undefined ? projects.find((candidate) => candidate.id === task.projectId && candidate.clientId === client.id) : undefined;
|
|
76
|
+
const hubTask =
|
|
77
|
+
project !== undefined && task.hubTaskId !== undefined
|
|
78
|
+
? tasks.find((candidate) => candidate.id === task.hubTaskId && candidate.projectId === project.id)
|
|
79
|
+
: undefined;
|
|
67
80
|
return {
|
|
68
81
|
clientId: client.id,
|
|
69
82
|
projectId: project ? project.id : "",
|
|
83
|
+
hubTaskId: hubTask ? hubTask.id : "",
|
|
70
84
|
legacyClientLabel: "",
|
|
71
85
|
routedToUnassigned: false,
|
|
72
86
|
};
|
|
@@ -76,6 +90,7 @@ export function resolveTaskAssignment(task: TaskView, clients: Client[], project
|
|
|
76
90
|
return {
|
|
77
91
|
clientId: unassigned ? unassigned.id : "",
|
|
78
92
|
projectId: "",
|
|
93
|
+
hubTaskId: "",
|
|
79
94
|
legacyClientLabel: task.client ?? task.clientName ?? "",
|
|
80
95
|
routedToUnassigned: true,
|
|
81
96
|
};
|
|
@@ -208,12 +223,12 @@ export type TaskEntryUpdatePayload = Omit<TaskEntryPayload, "client" | "project"
|
|
|
208
223
|
|
|
209
224
|
/** Build the full `task_entries` payload for a **create** request — every field, including assignment. */
|
|
210
225
|
export function buildTaskEntryCreatePayload(task: TaskView, ctx: HubEntryContext): TaskEntryPayload {
|
|
211
|
-
const assignment = resolveTaskAssignment(task, ctx.clients, ctx.projects);
|
|
226
|
+
const assignment = resolveTaskAssignment(task, ctx.clients, ctx.projects, ctx.tasks);
|
|
212
227
|
return {
|
|
213
228
|
task_id: task.id,
|
|
214
229
|
client: assignment.clientId,
|
|
215
230
|
project: assignment.projectId,
|
|
216
|
-
task:
|
|
231
|
+
task: assignment.hubTaskId,
|
|
217
232
|
started_at: toPbDate(task.startedAt),
|
|
218
233
|
ended_at: toPbDate(task.endedAt),
|
|
219
234
|
wall_ms: task.wallMs,
|
package/src/domain/task-view.ts
CHANGED
|
@@ -36,6 +36,10 @@ export interface TaskView {
|
|
|
36
36
|
projectId?: string;
|
|
37
37
|
/** Hub project display name, from the orchestrator record only. */
|
|
38
38
|
projectName?: string;
|
|
39
|
+
/** Hub task record id, from the orchestrator record only. See `domain/work-target.ts#HubTask`. */
|
|
40
|
+
hubTaskId?: string;
|
|
41
|
+
/** Hub task title, from the orchestrator record only. */
|
|
42
|
+
hubTaskTitle?: string;
|
|
39
43
|
/**
|
|
40
44
|
* Per-tag total milliseconds across the orchestrator and every subagent,
|
|
41
45
|
* summed rather than unioned: unlike `wallMs`, segment intervals are not
|
|
@@ -368,6 +372,8 @@ function buildTaskView(orchestrator: WorkRecord, subagents: WorkRecord[]): TaskV
|
|
|
368
372
|
...(orchestrator.clientName !== undefined ? { clientName: orchestrator.clientName } : {}),
|
|
369
373
|
...(orchestrator.projectId !== undefined ? { projectId: orchestrator.projectId } : {}),
|
|
370
374
|
...(orchestrator.projectName !== undefined ? { projectName: orchestrator.projectName } : {}),
|
|
375
|
+
...(orchestrator.hubTaskId !== undefined ? { hubTaskId: orchestrator.hubTaskId } : {}),
|
|
376
|
+
...(orchestrator.hubTaskTitle !== undefined ? { hubTaskTitle: orchestrator.hubTaskTitle } : {}),
|
|
371
377
|
project: orchestrator.project,
|
|
372
378
|
prompt: orchestrator.prompt,
|
|
373
379
|
startedAt: orchestrator.startedAt,
|
|
@@ -168,6 +168,10 @@ export interface WorkRecordMetadata {
|
|
|
168
168
|
projectId?: string;
|
|
169
169
|
/** Hub project display name, denormalised alongside `projectId`. */
|
|
170
170
|
projectName?: string;
|
|
171
|
+
/** Hub `tasks` record id linked for this session (`/kankaku task pick`), when one is active. See `domain/work-target.ts#HubTask`. */
|
|
172
|
+
hubTaskId?: string;
|
|
173
|
+
/** Hub task title, denormalised alongside `hubTaskId`. */
|
|
174
|
+
hubTaskTitle?: string;
|
|
171
175
|
/** This machine's hostname, or `KANKAKU_MACHINE`, set only when the hub is configured. */
|
|
172
176
|
machine?: string;
|
|
173
177
|
/**
|
|
@@ -210,6 +214,23 @@ export function finiteOrZero(value: unknown): number {
|
|
|
210
214
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
211
215
|
}
|
|
212
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Share of prompt input tokens served from the provider's prompt cache:
|
|
219
|
+
* `cacheRead / (input + cacheRead + cacheWrite)`. pi's `usage.input` maps to
|
|
220
|
+
* the provider's `input_tokens`, which already excludes cached tokens, so
|
|
221
|
+
* the three fields are disjoint and this sum is the true denominator.
|
|
222
|
+
* Returns `undefined` when the denominator is `0` (nothing to compute a
|
|
223
|
+
* ratio from) rather than `0`, so callers never render a misleading `0%`.
|
|
224
|
+
* Non-finite fields count as `0`, mirroring {@link finiteOrZero}.
|
|
225
|
+
*/
|
|
226
|
+
export function cacheHitRatio(usage: { input: number; cacheRead: number; cacheWrite: number }): number | undefined {
|
|
227
|
+
const input = finiteOrZero(usage.input);
|
|
228
|
+
const cacheRead = finiteOrZero(usage.cacheRead);
|
|
229
|
+
const cacheWrite = finiteOrZero(usage.cacheWrite);
|
|
230
|
+
const denominator = input + cacheRead + cacheWrite;
|
|
231
|
+
return denominator === 0 ? undefined : cacheRead / denominator;
|
|
232
|
+
}
|
|
233
|
+
|
|
213
234
|
const ROLES = new Set<WorkRole>(["orchestrator", "subagent"]);
|
|
214
235
|
const STATUSES = new Set<WorkStatus>(["completed", "aborted", "interrupted"]);
|
|
215
236
|
|
|
@@ -267,6 +288,8 @@ export function isWorkRecord(value: unknown): value is WorkRecord {
|
|
|
267
288
|
(record["clientName"] === undefined || typeof record["clientName"] === "string") &&
|
|
268
289
|
(record["projectId"] === undefined || typeof record["projectId"] === "string") &&
|
|
269
290
|
(record["projectName"] === undefined || typeof record["projectName"] === "string") &&
|
|
291
|
+
(record["hubTaskId"] === undefined || typeof record["hubTaskId"] === "string") &&
|
|
292
|
+
(record["hubTaskTitle"] === undefined || typeof record["hubTaskTitle"] === "string") &&
|
|
270
293
|
(record["machine"] === undefined || typeof record["machine"] === "string") &&
|
|
271
294
|
(record["roleConfidence"] === undefined || record["roleConfidence"] === "uncertain") &&
|
|
272
295
|
(record["orchestratorRef"] === undefined || isOrchestratorRef(record["orchestratorRef"])) &&
|
|
@@ -32,6 +32,19 @@ export interface Project {
|
|
|
32
32
|
active: boolean;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* A hub `tasks` row: something kankaku can link a session's `task_entries`
|
|
37
|
+
* row to (`task_entries.task`), never create. See `domain/hub-entry.ts` for
|
|
38
|
+
* the create-only assignment rule this feeds.
|
|
39
|
+
*/
|
|
40
|
+
export interface HubTask {
|
|
41
|
+
id: string;
|
|
42
|
+
title: string;
|
|
43
|
+
projectId: string;
|
|
44
|
+
status: "open" | "doing" | "done";
|
|
45
|
+
externalRef?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
export interface WorkTarget {
|
|
36
49
|
clientId: string;
|
|
37
50
|
clientCode: string;
|
|
@@ -39,12 +52,23 @@ export interface WorkTarget {
|
|
|
39
52
|
projectId?: string;
|
|
40
53
|
projectCode?: string;
|
|
41
54
|
projectName?: string;
|
|
55
|
+
/** Hub `tasks` record id, kept only when it belongs to `projectId`. See {@link resolveWorkTarget}. */
|
|
56
|
+
hubTaskId?: string;
|
|
57
|
+
/** Denormalised alongside `hubTaskId` for display. */
|
|
58
|
+
hubTaskTitle?: string;
|
|
42
59
|
}
|
|
43
60
|
|
|
44
61
|
/** ids picked from the session, or from the project's `.kankaku/config.json`. */
|
|
45
62
|
export interface WorkTargetCandidate {
|
|
46
63
|
clientId: string;
|
|
47
64
|
projectId?: string;
|
|
65
|
+
/**
|
|
66
|
+
* A hub task the user picked for this session (`/kankaku task pick`).
|
|
67
|
+
* Only ever set on the session-level candidate — the project config file
|
|
68
|
+
* and `repoPaths` matching never carry one (task linking is session-only,
|
|
69
|
+
* see AGENTS.md).
|
|
70
|
+
*/
|
|
71
|
+
hubTaskId?: string;
|
|
48
72
|
}
|
|
49
73
|
|
|
50
74
|
/**
|
|
@@ -63,6 +87,8 @@ export interface ResolveWorkTargetInput {
|
|
|
63
87
|
cwd: string;
|
|
64
88
|
clients: Client[];
|
|
65
89
|
projects: Project[];
|
|
90
|
+
/** The hub's known tasks, to validate a candidate's `hubTaskId` against. Defaults to `[]` so existing callers compile unchanged. */
|
|
91
|
+
tasks?: HubTask[];
|
|
66
92
|
}
|
|
67
93
|
|
|
68
94
|
export type WorkTargetSourceName = "session" | "project" | "repoPaths";
|
|
@@ -81,7 +107,18 @@ function findUsableProject(projects: Project[], projectId: string, clientId: str
|
|
|
81
107
|
return project;
|
|
82
108
|
}
|
|
83
109
|
|
|
84
|
-
|
|
110
|
+
/**
|
|
111
|
+
* A hub task usable as a link target: exists and belongs to `projectId`.
|
|
112
|
+
* Any {@link HubTask.status} is accepted — a task marked `done` mid-session
|
|
113
|
+
* keeps linking — only the project match matters here.
|
|
114
|
+
*/
|
|
115
|
+
function findUsableTask(tasks: HubTask[], taskId: string, projectId: string): HubTask | undefined {
|
|
116
|
+
const task = tasks.find((candidate) => candidate.id === taskId);
|
|
117
|
+
if (!task || task.projectId !== projectId) return undefined;
|
|
118
|
+
return task;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function buildTarget(client: Client, project: Project | undefined, task: HubTask | undefined): WorkTarget {
|
|
85
122
|
return {
|
|
86
123
|
clientId: client.id,
|
|
87
124
|
clientCode: client.code,
|
|
@@ -93,6 +130,7 @@ function buildTarget(client: Client, project: Project | undefined): WorkTarget {
|
|
|
93
130
|
projectName: project.name,
|
|
94
131
|
}
|
|
95
132
|
: {}),
|
|
133
|
+
...(task !== undefined ? { hubTaskId: task.id, hubTaskTitle: task.title } : {}),
|
|
96
134
|
};
|
|
97
135
|
}
|
|
98
136
|
|
|
@@ -101,13 +139,17 @@ function buildTarget(client: Client, project: Project | undefined): WorkTarget {
|
|
|
101
139
|
* file) into a target, or `undefined` when the client id does not resolve
|
|
102
140
|
* to a usable client. A projectId that does not resolve to a usable
|
|
103
141
|
* project of that client is dropped — the client-only target is still
|
|
104
|
-
* returned — rather than failing the whole candidate.
|
|
142
|
+
* returned — rather than failing the whole candidate. A `hubTaskId` is
|
|
143
|
+
* kept only when it resolves to a task of the resolved project; a target
|
|
144
|
+
* with no project never carries one.
|
|
105
145
|
*/
|
|
106
|
-
function resolveCandidate(candidate: WorkTargetCandidate, clients: Client[], projects: Project[]): WorkTarget | undefined {
|
|
146
|
+
function resolveCandidate(candidate: WorkTargetCandidate, clients: Client[], projects: Project[], tasks: HubTask[]): WorkTarget | undefined {
|
|
107
147
|
const client = findUsableClient(clients, candidate.clientId);
|
|
108
148
|
if (!client) return undefined;
|
|
109
149
|
const project = candidate.projectId !== undefined ? findUsableProject(projects, candidate.projectId, client.id) : undefined;
|
|
110
|
-
|
|
150
|
+
const task =
|
|
151
|
+
project !== undefined && candidate.hubTaskId !== undefined ? findUsableTask(tasks, candidate.hubTaskId, project.id) : undefined;
|
|
152
|
+
return buildTarget(client, project, task);
|
|
111
153
|
}
|
|
112
154
|
|
|
113
155
|
/** The longest-matching active project whose `repoPaths` contains `cwd`, exactly or as an ancestor directory. */
|
|
@@ -139,10 +181,11 @@ function isCwdWithin(cwd: string, repoPath: string): boolean {
|
|
|
139
181
|
/** Which source (if any) {@link resolveWorkTarget} would use, computed independently so callers can display it. */
|
|
140
182
|
export function resolveWorkTargetSource(input: ResolveWorkTargetInput): WorkTargetSourceName | undefined {
|
|
141
183
|
const { session } = input;
|
|
184
|
+
const tasks = input.tasks ?? [];
|
|
142
185
|
if (session === "skipped") return undefined;
|
|
143
|
-
if (session !== undefined && resolveCandidate(session, input.clients, input.projects) !== undefined) return "session";
|
|
186
|
+
if (session !== undefined && resolveCandidate(session, input.clients, input.projects, tasks) !== undefined) return "session";
|
|
144
187
|
|
|
145
|
-
if (input.project !== undefined && resolveCandidate(input.project, input.clients, input.projects) !== undefined) return "project";
|
|
188
|
+
if (input.project !== undefined && resolveCandidate(input.project, input.clients, input.projects, tasks) !== undefined) return "project";
|
|
146
189
|
|
|
147
190
|
const matched = matchByRepoPath(input.cwd, input.projects);
|
|
148
191
|
if (matched && findUsableClient(input.clients, matched.clientId)) return "repoPaths";
|
|
@@ -158,28 +201,34 @@ export function resolveWorkTargetSource(input: ResolveWorkTargetInput): WorkTarg
|
|
|
158
201
|
*/
|
|
159
202
|
export function resolveWorkTarget(input: ResolveWorkTargetInput): WorkTarget | undefined {
|
|
160
203
|
const { session } = input;
|
|
204
|
+
const tasks = input.tasks ?? [];
|
|
161
205
|
if (session === "skipped") return undefined;
|
|
162
206
|
|
|
163
207
|
if (session !== undefined) {
|
|
164
|
-
const resolved = resolveCandidate(session, input.clients, input.projects);
|
|
208
|
+
const resolved = resolveCandidate(session, input.clients, input.projects, tasks);
|
|
165
209
|
if (resolved) return resolved;
|
|
166
210
|
}
|
|
167
211
|
|
|
168
212
|
if (input.project !== undefined) {
|
|
169
|
-
const resolved = resolveCandidate(input.project, input.clients, input.projects);
|
|
213
|
+
const resolved = resolveCandidate(input.project, input.clients, input.projects, tasks);
|
|
170
214
|
if (resolved) return resolved;
|
|
171
215
|
}
|
|
172
216
|
|
|
173
217
|
const matched = matchByRepoPath(input.cwd, input.projects);
|
|
174
218
|
if (matched) {
|
|
175
219
|
const client = findUsableClient(input.clients, matched.clientId);
|
|
176
|
-
if (client) return buildTarget(client, matched);
|
|
220
|
+
if (client) return buildTarget(client, matched, undefined);
|
|
177
221
|
}
|
|
178
222
|
|
|
179
223
|
return undefined;
|
|
180
224
|
}
|
|
181
225
|
|
|
182
|
-
/**
|
|
226
|
+
/**
|
|
227
|
+
* `<clientName>`, `<clientName> · <projectName>`, or with a linked hub task
|
|
228
|
+
* appended as `... › <hubTaskTitle>` — the display label used by the
|
|
229
|
+
* status bar and the "remember" prompt.
|
|
230
|
+
*/
|
|
183
231
|
export function formatWorkTargetLabel(target: WorkTarget): string {
|
|
184
|
-
|
|
232
|
+
const base = target.projectName ? `${target.clientName} · ${target.projectName}` : target.clientName;
|
|
233
|
+
return target.hubTaskTitle ? `${base} › ${target.hubTaskTitle}` : base;
|
|
185
234
|
}
|
package/src/extension.ts
CHANGED
|
@@ -240,6 +240,7 @@ export default function kankaku(pi: ExtensionAPI): void {
|
|
|
240
240
|
client,
|
|
241
241
|
clients: snapshot?.clients ?? [],
|
|
242
242
|
projects: snapshot?.projects ?? [],
|
|
243
|
+
tasks: snapshot?.tasks ?? [],
|
|
243
244
|
machine: machineName,
|
|
244
245
|
promptMode: syncConfig.promptMode,
|
|
245
246
|
syncRecords: syncConfig.syncRecords,
|
package/src/ports/catalog.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import type { Client, Project } from "../domain/work-target.ts";
|
|
1
|
+
import type { Client, HubTask, Project } from "../domain/work-target.ts";
|
|
2
2
|
|
|
3
|
-
/** A cached read of the hub's clients/projects, plus when and against which hub URL it was fetched. */
|
|
3
|
+
/** A cached read of the hub's clients/projects/tasks, plus when and against which hub URL it was fetched. */
|
|
4
4
|
export interface CatalogSnapshot {
|
|
5
5
|
fetchedAt: number;
|
|
6
6
|
url: string;
|
|
7
7
|
clients: Client[];
|
|
8
8
|
projects: Project[];
|
|
9
|
+
/** Optional because a cache written by an older build (before hub task linking) never had this field; a missing value reads as "no tasks known yet." */
|
|
10
|
+
tasks?: HubTask[];
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
/**
|