kankaku 0.4.6 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +952 -13
- package/package.json +4 -2
- package/src/adapters/agent-info.ts +86 -0
- package/src/adapters/ancestry.ts +260 -0
- package/src/adapters/cached-catalog.ts +131 -0
- package/src/adapters/file-modes.ts +35 -0
- package/src/adapters/hub-credentials.ts +95 -0
- package/src/adapters/jsonl-work-log.ts +4 -1
- package/src/adapters/kankaku-command.ts +510 -7
- package/src/adapters/kankaku-dir.ts +92 -0
- package/src/adapters/machine-process-registry.ts +256 -0
- package/src/adapters/pi-tracker.ts +408 -14
- package/src/adapters/pocketbase-catalog.ts +60 -0
- package/src/adapters/pocketbase-client.ts +197 -0
- package/src/adapters/pocketbase-sink.ts +224 -0
- package/src/adapters/process-identity-memo.ts +102 -0
- package/src/adapters/process-identity.ts +162 -0
- package/src/adapters/project-config.ts +73 -1
- package/src/adapters/report.ts +79 -6
- package/src/adapters/session-dir.ts +28 -0
- package/src/adapters/session-target.ts +262 -0
- package/src/adapters/subagent-startup.ts +66 -0
- package/src/adapters/sync-runner.ts +301 -0
- package/src/adapters/sync-state-store.ts +227 -0
- package/src/adapters/target-picker.ts +82 -0
- package/src/config.ts +444 -6
- package/src/domain/ancestry-match.ts +84 -0
- package/src/domain/hub-entry.ts +339 -0
- package/src/domain/registry-health.ts +87 -0
- package/src/domain/subagent-profile.ts +495 -0
- package/src/domain/sync-plan.ts +251 -0
- package/src/domain/task-view.ts +307 -23
- package/src/domain/work-record.ts +157 -1
- package/src/domain/work-target.ts +185 -0
- package/src/domain/work-tracker.ts +248 -51
- package/src/extension.ts +303 -6
- package/src/ports/catalog.ts +31 -0
- package/src/ports/process-registry.ts +75 -0
- package/src/ports/work-sink.ts +35 -0
|
@@ -3,10 +3,31 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
3
3
|
import { Box, Text } from "@earendil-works/pi-tui";
|
|
4
4
|
import { isValidClient } from "../domain/client-label.ts";
|
|
5
5
|
import { exportRows, toCsv, toJson } from "../domain/export.ts";
|
|
6
|
-
import { buildSessions, buildTasks } from "../domain/task-view.ts";
|
|
6
|
+
import { buildSessions, buildTasks, detectSameProcessOverlaps, orphanSubagents, uncertainRecords } from "../domain/task-view.ts";
|
|
7
|
+
import { formatWorkTargetLabel } from "../domain/work-target.ts";
|
|
8
|
+
import { findAmbiguousToolNames } from "../domain/subagent-profile.ts";
|
|
9
|
+
import type { SubagentProfile } from "../domain/subagent-profile.ts";
|
|
10
|
+
import type { RejectedChildEnvMarker } from "../config.ts";
|
|
11
|
+
import type { SyncState } from "../domain/sync-plan.ts";
|
|
12
|
+
import type { RegistryClassification } from "../domain/registry-health.ts";
|
|
13
|
+
import type { Catalog } from "../ports/catalog.ts";
|
|
7
14
|
import type { WorkLog } from "../ports/work-log.ts";
|
|
8
|
-
import {
|
|
15
|
+
import type { SyncSummary, SyncTrigger } from "./sync-runner.ts";
|
|
16
|
+
import {
|
|
17
|
+
countUncertain,
|
|
18
|
+
formatClients,
|
|
19
|
+
formatProjects,
|
|
20
|
+
formatReport,
|
|
21
|
+
formatSessions,
|
|
22
|
+
formatTasks,
|
|
23
|
+
localDay,
|
|
24
|
+
summarize,
|
|
25
|
+
summarizeByClient,
|
|
26
|
+
summarizeByProject,
|
|
27
|
+
} from "./report.ts";
|
|
9
28
|
import type { SessionClient } from "./session-client.ts";
|
|
29
|
+
import { readNonDefaultSessionDir } from "./session-dir.ts";
|
|
30
|
+
import type { SessionTarget } from "./session-target.ts";
|
|
10
31
|
|
|
11
32
|
const REPORT_ENTRY_TYPE = "kankaku-report";
|
|
12
33
|
|
|
@@ -23,7 +44,30 @@ export function notifyError(ctx: ExtensionContext, error: unknown): void {
|
|
|
23
44
|
ctx.ui.notify(`kankaku: ${message}`, "error");
|
|
24
45
|
}
|
|
25
46
|
|
|
26
|
-
const COMMAND_TOKENS = ["all", "tasks", "sessions", "client", "clients", "export"];
|
|
47
|
+
const COMMAND_TOKENS = ["all", "tasks", "sessions", "client", "clients", "export", "doctor"];
|
|
48
|
+
/** Only offered when the hub is configured, so completions are unchanged for users without one. */
|
|
49
|
+
const HUB_COMMAND_TOKENS = ["target", "projects", "catalog", "sync", "backfill"];
|
|
50
|
+
const TARGET_TOKENS = ["pick", "clear"];
|
|
51
|
+
const CATALOG_TOKENS = ["refresh"];
|
|
52
|
+
const SYNC_TOKENS = ["all", "status"];
|
|
53
|
+
|
|
54
|
+
/** Drives `/kankaku sync [all|status]` and `/kankaku backfill`. Present only when the hub is configured. */
|
|
55
|
+
export interface SyncCommandDeps {
|
|
56
|
+
/**
|
|
57
|
+
* Run one sync pass; `full: true` re-evaluates every task (`/kankaku
|
|
58
|
+
* sync all`, `/kankaku backfill`). `trigger`, left unset here (a manual
|
|
59
|
+
* command), marks the automatic `session_start`/`agent_settled` path
|
|
60
|
+
* (`pi-tracker.ts`) so its version short-circuit and throttle never
|
|
61
|
+
* apply to a manual sync. Never throws.
|
|
62
|
+
*/
|
|
63
|
+
run: (options?: { full?: boolean; trigger?: SyncTrigger }) => Promise<SyncSummary>;
|
|
64
|
+
/**
|
|
65
|
+
* `/kankaku sync status`: the persisted state, a locally-computed pending
|
|
66
|
+
* count, and (R3) how many tasks changed since their last sync but fall
|
|
67
|
+
* outside this run's revisit window — needs `sync all`. No network.
|
|
68
|
+
*/
|
|
69
|
+
status: () => { state: SyncState | undefined; pending: number; staleOutsideWindow: number };
|
|
70
|
+
}
|
|
27
71
|
|
|
28
72
|
export interface KankakuCommandDeps {
|
|
29
73
|
log: WorkLog;
|
|
@@ -36,6 +80,100 @@ export interface KankakuCommandDeps {
|
|
|
36
80
|
* configured.
|
|
37
81
|
*/
|
|
38
82
|
writeExportFile?: (name: string, content: string) => string;
|
|
83
|
+
/**
|
|
84
|
+
* Present only when the hub (PocketBase) is configured. Drives `/kankaku
|
|
85
|
+
* target [pick|clear]` and makes `/kankaku client <name>` validate
|
|
86
|
+
* against the catalog instead of accepting free text.
|
|
87
|
+
*/
|
|
88
|
+
sessionTarget?: SessionTarget;
|
|
89
|
+
/** Present only when the hub is configured. Drives `/kankaku catalog refresh` and the hub-aware `/kankaku client <name>`. */
|
|
90
|
+
catalog?: Catalog;
|
|
91
|
+
/** Present only when the hub is configured. Drives `/kankaku sync [all|status]` and `/kankaku backfill`. */
|
|
92
|
+
sync?: SyncCommandDeps;
|
|
93
|
+
/**
|
|
94
|
+
* Read-only machine-wide process-registry health snapshot (see
|
|
95
|
+
* `adapters/machine-process-registry.ts#health`), for `/kankaku doctor`
|
|
96
|
+
* (SUBAGENT-REQ-017): how many entries would be kept vs discarded, and
|
|
97
|
+
* why. Absent entirely when the registry is unavailable for some reason
|
|
98
|
+
* kankaku itself could not construct (never expected in practice, since
|
|
99
|
+
* `MachineProcessRegistry` always degrades gracefully on its own) —
|
|
100
|
+
* doctor then simply omits that section rather than guessing.
|
|
101
|
+
*/
|
|
102
|
+
registryHealth?: () => RegistryClassification;
|
|
103
|
+
/**
|
|
104
|
+
* Whether the OS ancestor-chain mechanism itself is actually usable right
|
|
105
|
+
* now (F2): `false` on a platform with no supported mechanism (Windows)
|
|
106
|
+
* *or* when the mechanism is available but a fresh attempt still fails
|
|
107
|
+
* (`ps`/`/proc` missing, timing out, or producing unreadable output) —
|
|
108
|
+
* both collapse to the same "could not check" state, distinct from
|
|
109
|
+
* "checked, no tracked ancestor found". Never folded into
|
|
110
|
+
* `WorkRecord.roleConfidence`: an unprovable ancestor never demotes a
|
|
111
|
+
* process to `uncertain` (see `config.ts#detectRole`'s doc comment) — this
|
|
112
|
+
* is purely a visibility signal for the doctor's own report. Falls back
|
|
113
|
+
* to a bare `process.platform !== "win32"` check when not provided
|
|
114
|
+
* (back-compat with a caller that has not wired the real, ps/proc-aware
|
|
115
|
+
* check yet).
|
|
116
|
+
*/
|
|
117
|
+
ancestorDetectionAvailable?: () => boolean;
|
|
118
|
+
/**
|
|
119
|
+
* `KANKAKU_ROLE`, when it held a recognised value for this process (F3's
|
|
120
|
+
* explicit escape hatch) — reported by doctor as the deciding signal for
|
|
121
|
+
* this process's role, UNLESS `childMarkerPresent` or
|
|
122
|
+
* `overrideIgnoredInteractive` below says otherwise (R1): the override no
|
|
123
|
+
* longer beats every other detection signal unconditionally.
|
|
124
|
+
*/
|
|
125
|
+
roleOverride?: "orchestrator" | "subagent";
|
|
126
|
+
/**
|
|
127
|
+
* Whether `GENTLE_PI_AGENTS_CHILD=1` (the confirmed child marker) was
|
|
128
|
+
* also present on this process (R1) — when both it and `roleOverride`
|
|
129
|
+
* are set, the marker always wins (`config.ts#detectRole`'s precedence),
|
|
130
|
+
* so doctor flags the contradiction with the resolved outcome instead of
|
|
131
|
+
* claiming the override decided anything.
|
|
132
|
+
*/
|
|
133
|
+
childMarkerPresent?: boolean;
|
|
134
|
+
/**
|
|
135
|
+
* Set when `KANKAKU_ROLE=subagent` was present, with no confirmed child
|
|
136
|
+
* marker, but was ignored because this process looked interactive (R1) —
|
|
137
|
+
* doctor reports the resolved outcome (orchestrator) instead of claiming
|
|
138
|
+
* the override decided this process's role.
|
|
139
|
+
*/
|
|
140
|
+
overrideIgnoredInteractive?: boolean;
|
|
141
|
+
/**
|
|
142
|
+
* C2 (CRITICAL fix): set when a USER-CONFIGURED child-env marker
|
|
143
|
+
* (`KANKAKU_SUBAGENT_CHILD_ENV`) matched, but was ignored because this
|
|
144
|
+
* process looked interactive — a configured marker, unlike a built-in
|
|
145
|
+
* one, never demotes an interactive session. Doctor escalates the
|
|
146
|
+
* wording when `hasTrackedAncestor` is also `false` (C2 item 3's
|
|
147
|
+
* self-check: the strongest signal the marker is genuinely ambient).
|
|
148
|
+
*/
|
|
149
|
+
configuredMarkerIgnoredInteractive?: boolean;
|
|
150
|
+
/** C2 item 3: whether a live tracked ancestor was found for this process — see `configuredMarkerIgnoredInteractive` above. */
|
|
151
|
+
hasTrackedAncestor?: boolean;
|
|
152
|
+
/**
|
|
153
|
+
* C2 (CRITICAL fix, item 1): every `KANKAKU_SUBAGENT_CHILD_ENV` marker
|
|
154
|
+
* `config.ts#validateSubagentChildEnvMarkers` rejected as looking
|
|
155
|
+
* pi/shell/OS/npm-owned rather than genuinely child-only.
|
|
156
|
+
*/
|
|
157
|
+
rejectedSubagentChildEnvMarkers?: RejectedChildEnvMarker[];
|
|
158
|
+
/**
|
|
159
|
+
* Set only when this process is itself a subagent whose work log/inflight
|
|
160
|
+
* checkpoints were routed to its orchestrator's kankaku directory (F1, ADR
|
|
161
|
+
* 0023's rewrite): `usedFallback: true` means the orchestrator's
|
|
162
|
+
* directory could not be written to (gone, or no permission) and this
|
|
163
|
+
* process fell back to its own local directory instead — surfaced here so
|
|
164
|
+
* a human can notice and go reunite the record manually, since the
|
|
165
|
+
* append-only log can never be rewritten to fix it after the fact.
|
|
166
|
+
*/
|
|
167
|
+
workLogRouting?: { usedFallback: boolean; parentDir: string };
|
|
168
|
+
/**
|
|
169
|
+
* The full active {@link SubagentProfile} set (`config.ts#loadConfig`'s
|
|
170
|
+
* `subagentProfiles`), for `/kankaku doctor` (SUBAGENT-REQ-001/002/003/005/017):
|
|
171
|
+
* which profiles are active, any configured tool names/child-env markers,
|
|
172
|
+
* which profile matched each subagent record, and any tool-name ambiguity
|
|
173
|
+
* among the active set. Omitted entirely (no profile section at all) when
|
|
174
|
+
* a caller has not wired this — back-compat with an older embedder.
|
|
175
|
+
*/
|
|
176
|
+
subagentProfiles?: SubagentProfile[];
|
|
39
177
|
}
|
|
40
178
|
|
|
41
179
|
export interface KankakuCommand {
|
|
@@ -101,7 +239,15 @@ export function registerKankakuCommand(pi: ExtensionAPI, deps: KankakuCommandDep
|
|
|
101
239
|
ctx.ui.notify(`${report.title}\n${report.lines.join("\n")}`);
|
|
102
240
|
}
|
|
103
241
|
|
|
104
|
-
/**
|
|
242
|
+
/**
|
|
243
|
+
* Handle `/kankaku client [<name> | --clear]`; `rest` excludes the
|
|
244
|
+
* leading `client` token. When the hub is configured, setting a name
|
|
245
|
+
* (not `--clear` or empty) is the legacy compatibility path: it
|
|
246
|
+
* validates against the catalog (case-insensitive exact match of a
|
|
247
|
+
* client `code` or `name`) and sets the session hub target with no
|
|
248
|
+
* project, instead of the free-text legacy client. `--clear` and the
|
|
249
|
+
* no-argument report stay on the legacy client for both cases.
|
|
250
|
+
*/
|
|
105
251
|
function handleClientCommand(rest: string[], ctx: ExtensionContext): void {
|
|
106
252
|
if (rest.length === 1 && rest[0] === "--clear") {
|
|
107
253
|
sessionClient.set(pi, undefined);
|
|
@@ -119,6 +265,25 @@ export function registerKankakuCommand(pi: ExtensionAPI, deps: KankakuCommandDep
|
|
|
119
265
|
}
|
|
120
266
|
|
|
121
267
|
const name = rest.join(" ");
|
|
268
|
+
|
|
269
|
+
if (deps.sessionTarget) {
|
|
270
|
+
const clients = (deps.catalog?.read()?.clients ?? []).filter((client) => client.active && !client.unassigned);
|
|
271
|
+
const lowerName = name.toLowerCase();
|
|
272
|
+
const match = clients.find((client) => client.code.toLowerCase() === lowerName || client.name.toLowerCase() === lowerName);
|
|
273
|
+
if (!match) {
|
|
274
|
+
const validCodes = clients
|
|
275
|
+
.map((client) => client.code)
|
|
276
|
+
.sort()
|
|
277
|
+
.join(", ");
|
|
278
|
+
notifyError(ctx, new Error(`unknown client: ${name}${validCodes ? ` (valid: ${validCodes})` : ""}`));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
deps.sessionTarget.setExplicit(pi, { clientId: match.id });
|
|
282
|
+
deps.refreshIdleStatus(ctx);
|
|
283
|
+
showReport(ctx, { title: "client", lines: [`client set to ${match.name} (${match.code})`] });
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
122
287
|
if (!isValidClient(name)) {
|
|
123
288
|
notifyError(ctx, new Error(`invalid client name: ${name}`));
|
|
124
289
|
return;
|
|
@@ -128,6 +293,282 @@ export function registerKankakuCommand(pi: ExtensionAPI, deps: KankakuCommandDep
|
|
|
128
293
|
showReport(ctx, { title: "client", lines: [`client set to ${name}`] });
|
|
129
294
|
}
|
|
130
295
|
|
|
296
|
+
/** Handle `/kankaku target [pick|clear]`; `rest` excludes the leading `target` token. */
|
|
297
|
+
async function handleTargetCommand(rest: string[], ctx: ExtensionContext): Promise<void> {
|
|
298
|
+
const sessionTarget = deps.sessionTarget;
|
|
299
|
+
if (!sessionTarget) {
|
|
300
|
+
notifyError(ctx, new Error("hub is not configured"));
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (rest.length === 1 && rest[0] === "clear") {
|
|
305
|
+
sessionTarget.clear(pi);
|
|
306
|
+
deps.refreshIdleStatus(ctx);
|
|
307
|
+
showReport(ctx, { title: "target", lines: ["target cleared for this session"] });
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (rest.length === 1 && rest[0] === "pick") {
|
|
312
|
+
await sessionTarget.pick(pi, ctx);
|
|
313
|
+
deps.refreshIdleStatus(ctx);
|
|
314
|
+
const target = sessionTarget.effectiveTarget();
|
|
315
|
+
const line = target ? `target set to ${formatWorkTargetLabel(target)}` : "target skipped";
|
|
316
|
+
showReport(ctx, { title: "target", lines: [line] });
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (rest.length === 0) {
|
|
321
|
+
const target = sessionTarget.effectiveTarget();
|
|
322
|
+
const source = sessionTarget.effectiveSource();
|
|
323
|
+
const line = target !== undefined ? `target: ${formatWorkTargetLabel(target)} (from ${source})` : "target: none";
|
|
324
|
+
showReport(ctx, { title: "target", lines: [line] });
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
notifyError(ctx, new Error(`unknown target subcommand: ${rest.join(" ")}`));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Handle `/kankaku catalog refresh`; `rest` excludes the leading `catalog` token. */
|
|
332
|
+
async function handleCatalogCommand(rest: string[], ctx: ExtensionContext): Promise<void> {
|
|
333
|
+
const catalog = deps.catalog;
|
|
334
|
+
if (!catalog) {
|
|
335
|
+
notifyError(ctx, new Error("hub is not configured"));
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (rest.length === 1 && rest[0] === "refresh") {
|
|
340
|
+
const snapshot = await catalog.refresh();
|
|
341
|
+
if (!snapshot) {
|
|
342
|
+
notifyError(ctx, new Error("hub unreachable; catalog not refreshed"));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
showReport(ctx, {
|
|
346
|
+
title: "catalog",
|
|
347
|
+
lines: [`refreshed: ${snapshot.clients.length} client(s), ${snapshot.projects.length} project(s)`],
|
|
348
|
+
});
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
notifyError(ctx, new Error(`unknown catalog subcommand: ${rest.join(" ")}`));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Render a {@link SyncSummary} as report lines: counts, any stop reason, the new watermark, and the unassigned breakdown. */
|
|
356
|
+
function formatSyncSummary(summary: SyncSummary): string[] {
|
|
357
|
+
const lines = [`uploaded ${summary.uploaded}, updated ${summary.updated}, skipped ${summary.skipped}, failed ${summary.failed.length}`];
|
|
358
|
+
|
|
359
|
+
if (summary.locked) lines.push("another sync is already in progress; nothing was attempted");
|
|
360
|
+
if (summary.error) lines.push(`stopped early: ${summary.error}`);
|
|
361
|
+
if (summary.syncedThrough) lines.push(`synced through ${summary.syncedThrough}`);
|
|
362
|
+
|
|
363
|
+
const unassignedEntries = Object.entries(summary.unassigned).sort(([a], [b]) => a.localeCompare(b));
|
|
364
|
+
if (unassignedEntries.length > 0) {
|
|
365
|
+
lines.push("unassigned (Sin determinar):");
|
|
366
|
+
for (const [label, count] of unassignedEntries) lines.push(` ${label}: ${count}`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (summary.failed.length > 0) {
|
|
370
|
+
lines.push("failed:");
|
|
371
|
+
for (const entry of summary.failed) lines.push(` ${entry.id}: ${entry.reason}`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return lines;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Handle `/kankaku sync [all|status]`; `rest` excludes the leading `sync` token. */
|
|
378
|
+
async function handleSyncCommand(rest: string[], ctx: ExtensionContext): Promise<void> {
|
|
379
|
+
const sync = deps.sync;
|
|
380
|
+
if (!sync) {
|
|
381
|
+
notifyError(ctx, new Error("hub is not configured"));
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (rest.length === 1 && rest[0] === "status") {
|
|
386
|
+
const { state, pending, staleOutsideWindow } = sync.status();
|
|
387
|
+
const lines = [state?.syncedThrough ? `synced through ${state.syncedThrough}` : "never synced", `pending: ${pending}`];
|
|
388
|
+
if (staleOutsideWindow > 0) {
|
|
389
|
+
lines.push(`${staleOutsideWindow} task(s) never synced fall outside the sync window — run '/kankaku sync all' to upload them`);
|
|
390
|
+
}
|
|
391
|
+
if (state?.lastError) lines.push(`last error: ${state.lastError.message} (at ${state.lastError.at})`);
|
|
392
|
+
showReport(ctx, { title: "sync status", lines });
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (rest.length > 0 && !(rest.length === 1 && rest[0] === "all")) {
|
|
397
|
+
notifyError(ctx, new Error(`unknown sync subcommand: ${rest.join(" ")}`));
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const full = rest[0] === "all";
|
|
402
|
+
const summary = await sync.run({ full });
|
|
403
|
+
showReport(ctx, { title: full ? "sync (all)" : "sync", lines: formatSyncSummary(summary) });
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Handle `/kankaku backfill`: a full sync, reported as the "Sin determinar" breakdown that needs reassigning in the web. */
|
|
407
|
+
async function handleBackfillCommand(ctx: ExtensionContext): Promise<void> {
|
|
408
|
+
const sync = deps.sync;
|
|
409
|
+
if (!sync) {
|
|
410
|
+
notifyError(ctx, new Error("hub is not configured"));
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const summary = await sync.run({ full: true });
|
|
415
|
+
const unassignedEntries = Object.entries(summary.unassigned).sort(([a], [b]) => a.localeCompare(b));
|
|
416
|
+
|
|
417
|
+
const lines =
|
|
418
|
+
unassignedEntries.length > 0
|
|
419
|
+
? [
|
|
420
|
+
...unassignedEntries.map(([label, count]) => `${label}: ${count} task(s) -> Sin determinar`),
|
|
421
|
+
"reassign these in the hub web app's unassigned queue",
|
|
422
|
+
]
|
|
423
|
+
: ["no unassigned tasks"];
|
|
424
|
+
|
|
425
|
+
if (summary.error) lines.push(`stopped early: ${summary.error}`);
|
|
426
|
+
showReport(ctx, { title: "backfill", lines });
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Handle `/kankaku doctor`: a no-network diagnostic (SUBAGENT-REQ-017)
|
|
431
|
+
* reporting orphan/uncertain record counts (ADR 0022) with why, and
|
|
432
|
+
* whether ancestor-chain detection is available on this platform, so
|
|
433
|
+
* silent undercount stays visible (see README "Subagents").
|
|
434
|
+
*/
|
|
435
|
+
function handleDoctorCommand(ctx: ExtensionContext): void {
|
|
436
|
+
const records = log.readAll();
|
|
437
|
+
const orphans = orphanSubagents(records);
|
|
438
|
+
const uncertain = uncertainRecords(records);
|
|
439
|
+
const ancestorDetectionAvailable = deps.ancestorDetectionAvailable ? deps.ancestorDetectionAvailable() : process.platform !== "win32";
|
|
440
|
+
|
|
441
|
+
const lines = [
|
|
442
|
+
`ancestor-chain detection: ${ancestorDetectionAvailable ? "available" : "unavailable"}`,
|
|
443
|
+
`orphan subagent record(s): ${orphans.length}` +
|
|
444
|
+
(orphans.length > 0 ? " — recognised as someone's child (an env marker matched), but no orchestrator could be matched" : ""),
|
|
445
|
+
`uncertain record(s): ${uncertain.length}` +
|
|
446
|
+
(uncertain.length > 0
|
|
447
|
+
? " — no recognised child-env-marker, but a live tracked ancestor process was found; not counted as a new task, not synced"
|
|
448
|
+
: ""),
|
|
449
|
+
];
|
|
450
|
+
|
|
451
|
+
if (!ancestorDetectionAvailable) {
|
|
452
|
+
lines.push(
|
|
453
|
+
"on this platform/environment, ancestor-chain detection could not run (Windows, or a failed/unavailable ps/proc read): " +
|
|
454
|
+
"an unmarked subagent system may be counted twice (a genuine child with no recognised marker looks like a fresh top-level " +
|
|
455
|
+
"session). Mark it explicitly with KANKAKU_ROLE=subagent in the child's environment (or KANKAKU_ROLE=orchestrator to force " +
|
|
456
|
+
"the other way).",
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (deps.roleOverride) {
|
|
461
|
+
if (deps.childMarkerPresent) {
|
|
462
|
+
// R1: both signals present — the confirmed child marker always
|
|
463
|
+
// wins (config.ts#detectRole), so the override did not decide
|
|
464
|
+
// anything, whatever it said.
|
|
465
|
+
lines.push(
|
|
466
|
+
`role override: KANKAKU_ROLE=${deps.roleOverride} was present, but the confirmed child marker (GENTLE_PI_AGENTS_CHILD=1) takes precedence — resolved role: subagent`,
|
|
467
|
+
);
|
|
468
|
+
} else if (deps.overrideIgnoredInteractive) {
|
|
469
|
+
lines.push(
|
|
470
|
+
"role override: KANKAKU_ROLE=subagent was ignored for this interactive session (likely a leaked shell export) — resolved role: orchestrator",
|
|
471
|
+
);
|
|
472
|
+
} else {
|
|
473
|
+
lines.push(`role override: KANKAKU_ROLE=${deps.roleOverride} (deciding signal for this process's role)`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// C2 item 2/3: a configured child-env marker matched but was ignored
|
|
478
|
+
// for this process's role because it looked interactive — a
|
|
479
|
+
// configured marker, unlike a built-in one, never demotes an
|
|
480
|
+
// interactive session. Escalated when this process also has no
|
|
481
|
+
// tracked ancestor at all (C2 item 3's self-check: the strongest
|
|
482
|
+
// signal the marker is genuinely ambient, not a real subagent
|
|
483
|
+
// mechanism).
|
|
484
|
+
if (deps.configuredMarkerIgnoredInteractive) {
|
|
485
|
+
lines.push(
|
|
486
|
+
deps.hasTrackedAncestor
|
|
487
|
+
? "configured marker: a KANKAKU_SUBAGENT_CHILD_ENV marker was present but ignored for this interactive session — resolved role: orchestrator"
|
|
488
|
+
: "configured marker: a KANKAKU_SUBAGENT_CHILD_ENV marker was present on this interactive, TOP-LEVEL session (no tracked ancestor) — the marker is likely ambient (set on every process of its kind, not just a subagent's child), not a real subagent mechanism; resolved role: orchestrator",
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// C2 item 1: markers rejected at config load time as looking
|
|
493
|
+
// pi/shell/OS/npm-owned rather than genuinely child-only.
|
|
494
|
+
if (deps.rejectedSubagentChildEnvMarkers && deps.rejectedSubagentChildEnvMarkers.length > 0) {
|
|
495
|
+
for (const rejected of deps.rejectedSubagentChildEnvMarkers) {
|
|
496
|
+
lines.push(`rejected KANKAKU_SUBAGENT_CHILD_ENV marker "${rejected.name}": ${rejected.reason}`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// SUBAGENT-REQ-001/002/003/005/017 (6b): active profiles, any configured
|
|
501
|
+
// tool names/markers, which profile matched each subagent record, and
|
|
502
|
+
// any tool-name ambiguity among the active set. Omitted entirely when
|
|
503
|
+
// deps.subagentProfiles was not wired (back-compat).
|
|
504
|
+
if (deps.subagentProfiles) {
|
|
505
|
+
const profiles = deps.subagentProfiles;
|
|
506
|
+
lines.push(`subagent profiles active: ${profiles.map((p) => p.id).join(", ")}`);
|
|
507
|
+
|
|
508
|
+
const configured = profiles.find((p) => p.id === "configured");
|
|
509
|
+
if (configured) {
|
|
510
|
+
if (configured.toolNames.length > 0) lines.push(`configured subagent tools: ${configured.toolNames.join(", ")}`);
|
|
511
|
+
if (configured.childEnvMarkers.length > 0) {
|
|
512
|
+
lines.push(`configured child-env markers: ${configured.childEnvMarkers.map((m) => (m.value !== undefined ? `${m.name}=${m.value}` : m.name)).join(", ")}`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const subagentRecords = records.filter((record) => record.role === "subagent");
|
|
517
|
+
if (subagentRecords.length > 0) {
|
|
518
|
+
const counts = new Map<string, number>();
|
|
519
|
+
let unmatched = 0;
|
|
520
|
+
for (const record of subagentRecords) {
|
|
521
|
+
if (record.profile) {
|
|
522
|
+
counts.set(record.profile, (counts.get(record.profile) ?? 0) + 1);
|
|
523
|
+
} else {
|
|
524
|
+
unmatched++;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const parts = profiles.filter((p) => counts.has(p.id)).map((p) => `${p.id}: ${counts.get(p.id)}`);
|
|
528
|
+
if (unmatched > 0) parts.push(`unmatched: ${unmatched}`);
|
|
529
|
+
lines.push(`profile matches: ${parts.join(", ")}`);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
for (const { toolName, profileIds } of findAmbiguousToolNames(profiles)) {
|
|
533
|
+
lines.push(`ambiguous tool name "${toolName}": registered by ${profileIds.join(", ")} — never guessed, resolved by child-env marker or left uncertain`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// SUBAGENT-REQ-015 (6c): same-pid overlapping orchestrator records —
|
|
538
|
+
// never observed from any real subagent mechanism today, but flagged
|
|
539
|
+
// here (informational only, never changing buildTasks' own numbers) as
|
|
540
|
+
// the observable signature an in-process nested session would leave.
|
|
541
|
+
for (const overlap of detectSameProcessOverlaps(records)) {
|
|
542
|
+
lines.push(
|
|
543
|
+
`likely in-process nesting: pid ${overlap.pid} has ${overlap.recordIds.length} overlapping orchestrator records (${overlap.recordIds.join(", ")}) — union of their wall time is ${overlap.unionedWallMs}ms`,
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (deps.workLogRouting?.usedFallback) {
|
|
548
|
+
lines.push(
|
|
549
|
+
`kankaku: this subagent could not write to its orchestrator's directory (${deps.workLogRouting.parentDir}); ` +
|
|
550
|
+
"fell back to its own local worklog — this record may show as an orphan until reunited manually",
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (deps.registryHealth) {
|
|
555
|
+
const { keep, discard } = deps.registryHealth();
|
|
556
|
+
const counts = new Map<string, number>();
|
|
557
|
+
for (const { reason } of discard) counts.set(reason, (counts.get(reason) ?? 0) + 1);
|
|
558
|
+
const byReason = Array.from(counts.entries())
|
|
559
|
+
.map(([reason, count]) => `${reason}: ${count}`)
|
|
560
|
+
.join(", ");
|
|
561
|
+
lines.push(`registry (~/.kankaku/run): ${keep.length} entrie(s) trusted${discard.length > 0 ? `, ${discard.length} discarded (${byReason})` : ""}`);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const sessionDir = readNonDefaultSessionDir(ctx.sessionManager);
|
|
565
|
+
if (sessionDir !== undefined) {
|
|
566
|
+
lines.push(`session dir (non-default): ${sessionDir}`);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
showReport(ctx, { title: "doctor", lines });
|
|
570
|
+
}
|
|
571
|
+
|
|
131
572
|
/** Handle `/kankaku export [csv|json] [all]`; `rest` excludes the leading `export` token. Default format is csv. */
|
|
132
573
|
function handleExportCommand(rest: string[], ctx: ExtensionContext): void {
|
|
133
574
|
if (!deps.writeExportFile) {
|
|
@@ -153,7 +594,14 @@ export function registerKankakuCommand(pi: ExtensionAPI, deps: KankakuCommandDep
|
|
|
153
594
|
"'tasks' for this session's tasks ('tasks all' for every session), 'sessions' for today's sessions, " +
|
|
154
595
|
"'client <name>' to set the session billing client, 'client' to show the effective one and its source, " +
|
|
155
596
|
"'client --clear' to clear it, 'clients' for per-client totals today ('clients all' for every day), " +
|
|
156
|
-
"'export [csv|json] [all]' to write today's (or every) task as a file."
|
|
597
|
+
"'export [csv|json] [all]' to write today's (or every) task as a file. " +
|
|
598
|
+
"'doctor' to report orphan/uncertain subagent counts and ancestor-detection availability (no network). " +
|
|
599
|
+
"When a hub (PocketBase) is configured: 'target' to show the effective client/project and its source, " +
|
|
600
|
+
"'target pick' to run the picker again, 'target clear' to clear the session target, " +
|
|
601
|
+
"'catalog refresh' to force a catalog refresh, 'projects' for per-project totals today ('projects all' for every day), " +
|
|
602
|
+
"'sync' to push pending tasks to the hub ('sync all' for a full re-evaluation, 'sync status' for the watermark/pending count/last error), " +
|
|
603
|
+
"'backfill' to run a full sync and report how many tasks went to Sin determinar, grouped by their old label. " +
|
|
604
|
+
"With a hub configured, 'client <name>' instead validates against the catalog (code or name) and sets the target.",
|
|
157
605
|
getArgumentCompletions: (argumentPrefix: string): AutocompleteItem[] => {
|
|
158
606
|
const clientMatch = /^client\s+(\S*)$/.exec(argumentPrefix);
|
|
159
607
|
if (clientMatch) {
|
|
@@ -162,7 +610,23 @@ export function registerKankakuCommand(pi: ExtensionAPI, deps: KankakuCommandDep
|
|
|
162
610
|
.filter((name) => name.startsWith(prefix))
|
|
163
611
|
.map((name) => ({ value: name, label: name }));
|
|
164
612
|
}
|
|
165
|
-
|
|
613
|
+
const targetMatch = /^target\s+(\S*)$/.exec(argumentPrefix);
|
|
614
|
+
if (targetMatch) {
|
|
615
|
+
const prefix = targetMatch[1] ?? "";
|
|
616
|
+
return TARGET_TOKENS.filter((value) => value.startsWith(prefix)).map((value) => ({ value, label: value }));
|
|
617
|
+
}
|
|
618
|
+
const catalogMatch = /^catalog\s+(\S*)$/.exec(argumentPrefix);
|
|
619
|
+
if (catalogMatch) {
|
|
620
|
+
const prefix = catalogMatch[1] ?? "";
|
|
621
|
+
return CATALOG_TOKENS.filter((value) => value.startsWith(prefix)).map((value) => ({ value, label: value }));
|
|
622
|
+
}
|
|
623
|
+
const syncMatch = /^sync\s+(\S*)$/.exec(argumentPrefix);
|
|
624
|
+
if (syncMatch) {
|
|
625
|
+
const prefix = syncMatch[1] ?? "";
|
|
626
|
+
return SYNC_TOKENS.filter((value) => value.startsWith(prefix)).map((value) => ({ value, label: value }));
|
|
627
|
+
}
|
|
628
|
+
const tokens = deps.sessionTarget ? [...COMMAND_TOKENS, ...HUB_COMMAND_TOKENS] : COMMAND_TOKENS;
|
|
629
|
+
return tokens.filter((value) => value.startsWith(argumentPrefix)).map((value) => ({ value, label: value }));
|
|
166
630
|
},
|
|
167
631
|
handler: async (args, ctx) => {
|
|
168
632
|
try {
|
|
@@ -178,6 +642,31 @@ export function registerKankakuCommand(pi: ExtensionAPI, deps: KankakuCommandDep
|
|
|
178
642
|
return;
|
|
179
643
|
}
|
|
180
644
|
|
|
645
|
+
if (tokens[0] === "target") {
|
|
646
|
+
await handleTargetCommand(tokens.slice(1), ctx);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (tokens[0] === "catalog") {
|
|
651
|
+
await handleCatalogCommand(tokens.slice(1), ctx);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (tokens[0] === "sync") {
|
|
656
|
+
await handleSyncCommand(tokens.slice(1), ctx);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (tokens[0] === "backfill") {
|
|
661
|
+
await handleBackfillCommand(ctx);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (tokens[0] === "doctor") {
|
|
666
|
+
handleDoctorCommand(ctx);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
181
670
|
const all = tokens.includes("all");
|
|
182
671
|
const records = log.readAll();
|
|
183
672
|
const today = localDay(new Date().toISOString());
|
|
@@ -191,6 +680,15 @@ export function registerKankakuCommand(pi: ExtensionAPI, deps: KankakuCommandDep
|
|
|
191
680
|
return;
|
|
192
681
|
}
|
|
193
682
|
|
|
683
|
+
if (tokens.includes("projects")) {
|
|
684
|
+
const tasks = buildTasks(records).filter((task) => all || localDay(task.startedAt) === today);
|
|
685
|
+
showReport(ctx, {
|
|
686
|
+
title: all ? "projects (all days)" : "projects (today)",
|
|
687
|
+
lines: formatProjects(summarizeByProject(tasks)).split("\n"),
|
|
688
|
+
});
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
|
|
194
692
|
if (tokens.includes("tasks")) {
|
|
195
693
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
196
694
|
const scoped = all || !sessionId;
|
|
@@ -212,9 +710,14 @@ export function registerKankakuCommand(pi: ExtensionAPI, deps: KankakuCommandDep
|
|
|
212
710
|
}
|
|
213
711
|
|
|
214
712
|
const summary = summarize(records, { all });
|
|
713
|
+
const lines = formatReport(summary).split(" | ");
|
|
714
|
+
const uncertainCount = countUncertain(records, { all });
|
|
715
|
+
if (uncertainCount > 0) {
|
|
716
|
+
lines.push(`kankaku: ${uncertainCount} uncertain record(s) excluded from tasks — run /kankaku doctor`);
|
|
717
|
+
}
|
|
215
718
|
showReport(ctx, {
|
|
216
719
|
title: all ? "summary (all days)" : "summary (today)",
|
|
217
|
-
lines
|
|
720
|
+
lines,
|
|
218
721
|
});
|
|
219
722
|
} catch (error) {
|
|
220
723
|
notifyError(ctx, error);
|