kankaku 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +93 -23
  2. package/dist/adapters/export-writer.d.ts +13 -0
  3. package/dist/adapters/export-writer.js +28 -0
  4. package/dist/adapters/hub-actions.d.ts +35 -0
  5. package/dist/adapters/hub-actions.js +70 -0
  6. package/dist/adapters/project-config.d.ts +42 -0
  7. package/dist/adapters/project-config.js +108 -0
  8. package/dist/adapters/report-data.d.ts +12 -0
  9. package/dist/adapters/report-data.js +8 -0
  10. package/dist/adapters/report-views.d.ts +45 -0
  11. package/dist/adapters/report-views.js +73 -0
  12. package/dist/adapters/report.d.ts +112 -0
  13. package/dist/adapters/report.js +236 -0
  14. package/dist/adapters/sync-runner.d.ts +21 -6
  15. package/dist/adapters/sync-runner.js +8 -1
  16. package/dist/domain/hub-entry.d.ts +26 -2
  17. package/dist/domain/hub-entry.js +50 -6
  18. package/dist/domain/sync-plan.js +10 -0
  19. package/dist/domain/work-record.d.ts +17 -0
  20. package/dist/domain/work-record.js +5 -1
  21. package/dist/hub/index.d.ts +6 -0
  22. package/dist/hub/index.js +6 -0
  23. package/package.json +1 -1
  24. package/src/adapters/hub-actions.ts +2 -3
  25. package/src/adapters/kankaku-command.ts +5 -9
  26. package/src/adapters/panel/kankaku-panel.ts +164 -19
  27. package/src/adapters/panel/panel-items.ts +13 -3
  28. package/src/adapters/panel/screens/doctor.ts +4 -0
  29. package/src/adapters/panel/screens/export.ts +4 -0
  30. package/src/adapters/panel/screens/report.ts +2 -0
  31. package/src/adapters/panel/screens/sync.ts +10 -0
  32. package/src/adapters/panel/screens/target.ts +18 -1
  33. package/src/adapters/pi-tracker.ts +8 -0
  34. package/src/adapters/report-data.ts +13 -0
  35. package/src/adapters/report-views.ts +1 -1
  36. package/src/adapters/sync-runner.ts +21 -7
  37. package/src/domain/hub-entry.ts +87 -8
  38. package/src/domain/panel-model.ts +10 -7
  39. package/src/domain/sync-plan.ts +10 -0
  40. package/src/domain/work-record.ts +22 -1
  41. package/src/hub/index.ts +6 -0
@@ -207,10 +207,12 @@ export interface TaskEntryPayload {
207
207
  legacy_client_label: string;
208
208
  repo_project: string;
209
209
  schema: number;
210
+ /** Coding agent that MEASURED this task, lowercase slug — the orchestrator record's own {@link WorkRecordMetadata.agent} when it carries one (who measured, not who syncs), else `ctx.agent` (the syncing process's own identity) as a legacy fallback. See {@link WorkRecordMetadata.agent}. */
210
211
  agent: string;
211
212
  agent_version?: string;
212
213
  /** Non-default session directory, see `TaskView.sessionDir`. A measurement field, not assignment: sent on both create and update. */
213
214
  session_dir?: string;
215
+ /** The integration that wrote this task's records, lowercase slug — same record-wins-over-ctx rule as {@link agent}. */
214
216
  plugin: string;
215
217
  plugin_version?: string;
216
218
  waiting_quality: WaitingQuality;
@@ -218,12 +220,59 @@ export interface TaskEntryPayload {
218
220
  subagent_linkage: SubagentLinkage;
219
221
  }
220
222
 
221
- /** `TaskEntryPayload` minus the assignment fields — what an update sends. See the module docs' CRITICAL rule. */
222
- export type TaskEntryUpdatePayload = Omit<TaskEntryPayload, "client" | "project" | "task" | "legacy_client_label">;
223
+ /**
224
+ * `TaskEntryPayload` minus the assignment fields — what an update sends. See
225
+ * the module docs' CRITICAL rule.
226
+ *
227
+ * `agent`/`agent_version`/`plugin`/`plugin_version` are optional here (unlike
228
+ * on `TaskEntryPayload`, where they are always sent on create): an update for
229
+ * a task whose orchestrator record carries no who-measured identity (a
230
+ * legacy record, written before this feature existed) omits them entirely,
231
+ * so a re-sync by a DIFFERENT process never overwrites a row's original
232
+ * identity with its own. See {@link buildTaskEntryUpdatePayload}.
233
+ */
234
+ export type TaskEntryUpdatePayload = Omit<
235
+ TaskEntryPayload,
236
+ "client" | "project" | "task" | "legacy_client_label" | "agent" | "agent_version" | "plugin" | "plugin_version"
237
+ > & {
238
+ agent?: string;
239
+ agent_version?: string;
240
+ plugin?: string;
241
+ plugin_version?: string;
242
+ };
243
+
244
+ /**
245
+ * The who-measured identity (`agent`/`agentVersion`/`plugin`/`pluginVersion`)
246
+ * for a task's payload: the orchestrator record's own fields, taken as a
247
+ * unit, when it carries an `agent` — a missing version on the record is
248
+ * omitted, never backfilled from `ctx` — otherwise `ctx`'s identity (the
249
+ * syncing process's own), exactly as before this feature existed. See
250
+ * `domain/work-record.ts#WorkRecordMetadata.agent`'s doc comment.
251
+ */
252
+ function resolveTaskIdentity(
253
+ task: TaskView,
254
+ ctx: HubEntryContext,
255
+ ): { agent: string; agentVersion?: string; plugin: string; pluginVersion?: string } {
256
+ const orchestrator = task.orchestrator;
257
+ if (orchestrator.agent !== undefined) {
258
+ // The record's four fields are taken as a unit. A record written by an
259
+ // integration that stamped `agent` but not `plugin` gets the syncing
260
+ // context's `plugin` ONLY on create, so the row is never blank; the
261
+ // update payload below never resends a plugin the record does not own.
262
+ return {
263
+ agent: orchestrator.agent,
264
+ agentVersion: orchestrator.agentVersion,
265
+ plugin: orchestrator.plugin ?? ctx.plugin,
266
+ pluginVersion: orchestrator.plugin !== undefined ? orchestrator.pluginVersion : undefined,
267
+ };
268
+ }
269
+ return { agent: ctx.agent, agentVersion: ctx.agentVersion, plugin: ctx.plugin, pluginVersion: ctx.pluginVersion };
270
+ }
223
271
 
224
272
  /** Build the full `task_entries` payload for a **create** request — every field, including assignment. */
225
273
  export function buildTaskEntryCreatePayload(task: TaskView, ctx: HubEntryContext): TaskEntryPayload {
226
274
  const assignment = resolveTaskAssignment(task, ctx.clients, ctx.projects, ctx.tasks);
275
+ const identity = resolveTaskIdentity(task, ctx);
227
276
  return {
228
277
  task_id: task.id,
229
278
  client: assignment.clientId,
@@ -253,11 +302,11 @@ export function buildTaskEntryCreatePayload(task: TaskView, ctx: HubEntryContext
253
302
  legacy_client_label: assignment.legacyClientLabel,
254
303
  repo_project: task.project,
255
304
  schema: task.orchestrator.schema,
256
- agent: ctx.agent,
257
- ...(ctx.agentVersion !== undefined ? { agent_version: ctx.agentVersion } : {}),
305
+ agent: identity.agent,
306
+ ...(identity.agentVersion !== undefined ? { agent_version: identity.agentVersion } : {}),
258
307
  ...(task.sessionDir !== undefined ? { session_dir: task.sessionDir } : {}),
259
- plugin: ctx.plugin,
260
- ...(ctx.pluginVersion !== undefined ? { plugin_version: ctx.pluginVersion } : {}),
308
+ plugin: identity.plugin,
309
+ ...(identity.pluginVersion !== undefined ? { plugin_version: identity.pluginVersion } : {}),
261
310
  waiting_quality: computeWaitingQuality(),
262
311
  cost_quality: computeCostQuality(task),
263
312
  subagent_linkage: computeSubagentLinkage(task),
@@ -269,10 +318,40 @@ export function buildTaskEntryCreatePayload(task: TaskView, ctx: HubEntryContext
269
318
  * fields only — never `client`, `project`, `task` or `legacy_client_label`,
270
319
  * so a re-sync can never undo a reassignment made in the web. See the
271
320
  * module docs' CRITICAL rule.
321
+ *
322
+ * `agent`/`agent_version`/`plugin`/`plugin_version` are included ONLY when
323
+ * the orchestrator record itself carries a who-measured `agent` — otherwise
324
+ * they are OMITTED entirely (never sent as `ctx`'s own identity), so a
325
+ * re-sync of a legacy record by a different process never overwrites the
326
+ * row's original identity in the hub. See `domain/work-record.ts`'s
327
+ * "Measurement rules" and {@link resolveTaskIdentity}.
272
328
  */
273
329
  export function buildTaskEntryUpdatePayload(task: TaskView, ctx: HubEntryContext): TaskEntryUpdatePayload {
274
- const { client: _client, project: _project, task: _task, legacy_client_label: _legacy, ...rest } = buildTaskEntryCreatePayload(task, ctx);
275
- return rest;
330
+ const {
331
+ client: _client,
332
+ project: _project,
333
+ task: _task,
334
+ legacy_client_label: _legacy,
335
+ agent,
336
+ agent_version,
337
+ plugin,
338
+ plugin_version,
339
+ ...rest
340
+ } = buildTaskEntryCreatePayload(task, ctx);
341
+
342
+ if (task.orchestrator.agent === undefined) return rest;
343
+
344
+ // `plugin`/`plugin_version` are resent only when the record itself carries
345
+ // them; a record with `agent` but no `plugin` must never have the syncing
346
+ // process's plugin written over the row's original one.
347
+ const recordHasPlugin = task.orchestrator.plugin !== undefined;
348
+ return {
349
+ ...rest,
350
+ agent,
351
+ ...(agent_version !== undefined ? { agent_version } : {}),
352
+ ...(recordHasPlugin ? { plugin } : {}),
353
+ ...(recordHasPlugin && plugin_version !== undefined ? { plugin_version } : {}),
354
+ };
276
355
  }
277
356
 
278
357
  /** One `work_records` row — raw per-`WorkRecord` detail, always `rollup: false`. */
@@ -83,10 +83,13 @@ const SCREEN_TITLES: Record<Exclude<PanelScreenId, "root">, string> = {
83
83
  about: "About",
84
84
  };
85
85
 
86
- /** `kankaku` at root, `kankaku · <Screen>` on every other screen. */
86
+ /** The prompt glyph that opens every panel title, the owner's mark for kankaku. */
87
+ export const PANEL_TITLE_PREFIX = ">_";
88
+
89
+ /** `>_ kankaku` at root, `>_ kankaku · <Screen>` on every other screen. */
87
90
  export function panelTitle(screen: PanelScreenId): string {
88
- if (screen === "root") return "kankaku";
89
- return `kankaku · ${SCREEN_TITLES[screen]}`;
91
+ if (screen === "root") return `${PANEL_TITLE_PREFIX} kankaku`;
92
+ return `${PANEL_TITLE_PREFIX} kankaku · ${SCREEN_TITLES[screen]}`;
90
93
  }
91
94
 
92
95
  /** One clickable/keyboard hint shown in the panel's footer. */
@@ -97,9 +100,9 @@ export interface PanelHint {
97
100
 
98
101
  /**
99
102
  * The footer hint row for a screen: navigation hints, an optional search
100
- * hint when the current body supports it, and how Escape/`q` behave — back
101
- * at root closes the panel outright, so root shows only `esc close`; every
102
- * other screen shows both `esc back` and `q close`.
103
+ * hint when the current body supports it, and how Escape/left arrow/`q`
104
+ * behave — back at root closes the panel outright, so root shows only
105
+ * `esc close`; every other screen shows both `esc/← back` and `q close`.
103
106
  */
104
107
  export function footerHints(screen: PanelScreenId, options: { searchable: boolean }): PanelHint[] {
105
108
  const hints: PanelHint[] = [{ key: "↑↓", label: "move" }];
@@ -113,7 +116,7 @@ export function footerHints(screen: PanelScreenId, options: { searchable: boolea
113
116
 
114
117
  hints.push({ key: "enter", label: "select" });
115
118
  if (options.searchable) hints.push({ key: "/", label: "search" });
116
- hints.push({ key: "esc", label: "back" }, { key: "q", label: "close" });
119
+ hints.push({ key: "esc/←", label: "back" }, { key: "q", label: "close" });
117
120
  return hints;
118
121
  }
119
122
 
@@ -134,6 +134,16 @@ export function computeTaskContentHash(task: TaskView): string {
134
134
  // `undefined` is dropped by the serialiser, so a task that never knew
135
135
  // its reasoning effort keeps the hash it had before this field existed.
136
136
  thinkingLevel: task.orchestrator.thinkingLevel,
137
+ // A task gaining a who-measured identity
138
+ // (domain/hub-entry.ts#resolveTaskIdentity) must resync, since it
139
+ // changes which agent/plugin the row is attributed to; the version
140
+ // fields are left out on purpose so a version-only bump alone does not
141
+ // force a resync. The keys are added CONDITIONALLY: `stableStringify`
142
+ // renders an `undefined` value as `"agent":undefined`, so an
143
+ // unconditional key would change every legacy task's hash and force a
144
+ // full resync right after upgrading (see the golden-hash test).
145
+ ...(task.orchestrator.agent !== undefined ? { agent: task.orchestrator.agent } : {}),
146
+ ...(task.orchestrator.plugin !== undefined ? { plugin: task.orchestrator.plugin } : {}),
137
147
  }),
138
148
  );
139
149
  }
@@ -201,6 +201,23 @@ export interface WorkRecordMetadata {
201
201
  * (SUBAGENT-REQ-017). Never set on an `orchestrator` record.
202
202
  */
203
203
  profile?: string;
204
+ /**
205
+ * Coding agent that MEASURED this record, lowercase slug (e.g. `"pi"`).
206
+ * Who measured, not who syncs — a different process (a standalone
207
+ * `kankaku` TUI, or a session for a different agent sharing the same
208
+ * `worklog.jsonl`) may later push this record to the hub, and must never
209
+ * overwrite this identity with its own. See kankaku-hub `docs/contract.md`
210
+ * "Agent and measurement quality" and `domain/hub-entry.ts`. Optional and
211
+ * additive: `WORK_RECORD_SCHEMA` is unchanged and an older record without
212
+ * it still validates.
213
+ */
214
+ agent?: string;
215
+ /** The measuring agent's own version, when it could be determined without a hot-path cost. Never guessed — omitted rather than sent wrong. */
216
+ agentVersion?: string;
217
+ /** The integration that wrote this record, lowercase slug (e.g. `"kankaku"`). Same who-measured-not-who-syncs rule as {@link agent}. */
218
+ plugin?: string;
219
+ /** This integration's own version, from its `package.json`, read once. */
220
+ pluginVersion?: string;
204
221
  }
205
222
 
206
223
  export type WorkRecord = WorkRecordCore & WorkRecordMetadata;
@@ -294,6 +311,10 @@ export function isWorkRecord(value: unknown): value is WorkRecord {
294
311
  (record["roleConfidence"] === undefined || record["roleConfidence"] === "uncertain") &&
295
312
  (record["orchestratorRef"] === undefined || isOrchestratorRef(record["orchestratorRef"])) &&
296
313
  (record["costObserved"] === undefined || record["costObserved"] === true) &&
297
- (record["profile"] === undefined || typeof record["profile"] === "string")
314
+ (record["profile"] === undefined || typeof record["profile"] === "string") &&
315
+ (record["agent"] === undefined || typeof record["agent"] === "string") &&
316
+ (record["agentVersion"] === undefined || typeof record["agentVersion"] === "string") &&
317
+ (record["plugin"] === undefined || typeof record["plugin"] === "string") &&
318
+ (record["pluginVersion"] === undefined || typeof record["pluginVersion"] === "string")
298
319
  );
299
320
  }
package/src/hub/index.ts CHANGED
@@ -7,7 +7,9 @@
7
7
  * for which ones and why.
8
8
  */
9
9
  export * from "../adapters/cached-catalog.ts";
10
+ export * from "../adapters/export-writer.ts";
10
11
  export * from "../adapters/file-modes.ts";
12
+ export * from "../adapters/hub-actions.ts";
11
13
  export * from "../adapters/hub-credentials.ts";
12
14
  export * from "../adapters/jsonl-work-log.ts";
13
15
  export * from "../adapters/kankaku-dir.ts";
@@ -15,5 +17,9 @@ export * from "../adapters/lazy-jsonl-work-log.ts";
15
17
  export * from "../adapters/pocketbase-catalog.ts";
16
18
  export * from "../adapters/pocketbase-client.ts";
17
19
  export * from "../adapters/pocketbase-sink.ts";
20
+ export * from "../adapters/project-config.ts";
21
+ export * from "../adapters/report-data.ts";
22
+ export * from "../adapters/report.ts";
23
+ export * from "../adapters/report-views.ts";
18
24
  export * from "../adapters/sync-runner.ts";
19
25
  export * from "../adapters/sync-state-store.ts";