kankaku 0.6.5 → 0.7.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 +59 -11
- package/package.json +1 -1
- package/src/adapters/hub-actions.ts +83 -0
- package/src/adapters/kankaku-command.ts +193 -243
- package/src/adapters/panel/kankaku-panel.ts +274 -0
- package/src/adapters/panel/panel-items.ts +77 -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 +85 -0
- package/src/adapters/panel/screens/export.ts +119 -0
- package/src/adapters/panel/screens/report.ts +141 -0
- package/src/adapters/panel/screens/sync.ts +126 -0
- package/src/adapters/panel/screens/target.ts +367 -0
- package/src/adapters/pi-tracker.ts +108 -6
- package/src/adapters/report-views.ts +98 -0
- package/src/adapters/session-target.ts +51 -4
- package/src/domain/panel-model.ts +267 -0
- package/src/extension.ts +18 -8
|
@@ -2,8 +2,7 @@ import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { Box, Text } from "@earendil-works/pi-tui";
|
|
4
4
|
import { isValidClient } from "../domain/client-label.ts";
|
|
5
|
-
import {
|
|
6
|
-
import { buildSessions, buildTasks, detectSameProcessOverlaps, orphanSubagents, uncertainRecords } from "../domain/task-view.ts";
|
|
5
|
+
import { detectSameProcessOverlaps, orphanSubagents, uncertainRecords } from "../domain/task-view.ts";
|
|
7
6
|
import { formatWorkTargetLabel } from "../domain/work-target.ts";
|
|
8
7
|
import { findAmbiguousToolNames } from "../domain/subagent-profile.ts";
|
|
9
8
|
import type { SubagentProfile } from "../domain/subagent-profile.ts";
|
|
@@ -12,19 +11,9 @@ import type { SyncState } from "../domain/sync-plan.ts";
|
|
|
12
11
|
import type { RegistryClassification } from "../domain/registry-health.ts";
|
|
13
12
|
import type { Catalog } from "../ports/catalog.ts";
|
|
14
13
|
import type { WorkLog } from "../ports/work-log.ts";
|
|
14
|
+
import { buildSyncStatusLines, formatBackfillLines, formatCatalogRefreshLines, formatSyncSummaryLines } from "./hub-actions.ts";
|
|
15
|
+
import { buildClientsView, buildExportContent, buildProjectsView, buildSessionsView, buildSummaryView, buildTasksView } from "./report-views.ts";
|
|
15
16
|
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";
|
|
28
17
|
import type { SessionClient } from "./session-client.ts";
|
|
29
18
|
import { readNonDefaultSessionDir } from "./session-dir.ts";
|
|
30
19
|
import type { SessionTarget } from "./session-target.ts";
|
|
@@ -44,6 +33,167 @@ export function notifyError(ctx: ExtensionContext, error: unknown): void {
|
|
|
44
33
|
ctx.ui.notify(`kankaku: ${message}`, "error");
|
|
45
34
|
}
|
|
46
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Append a {@link KankakuReportData} as a durable entry in the chat
|
|
38
|
+
* transcript when a UI is attached, or fall back to a one-shot notify
|
|
39
|
+
* otherwise. Exact body of `/kankaku`'s own `showReport` (which now
|
|
40
|
+
* delegates here), so the panel's "pin to chat" action
|
|
41
|
+
* (`screens/report.ts`, `screens/doctor.ts`) renders identically to every
|
|
42
|
+
* existing subcommand's report.
|
|
43
|
+
*/
|
|
44
|
+
export function appendReportEntry(pi: ExtensionAPI, ctx: ExtensionContext, report: KankakuReportData): void {
|
|
45
|
+
if (ctx.hasUI) {
|
|
46
|
+
pi.appendEntry<KankakuReportData>(REPORT_ENTRY_TYPE, report);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
ctx.ui.notify(`${report.title}\n${report.lines.join("\n")}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Handle `/kankaku doctor`'s line-building: a no-network diagnostic
|
|
54
|
+
* (SUBAGENT-REQ-017) reporting orphan/uncertain record counts (ADR 0022)
|
|
55
|
+
* with why, and whether ancestor-chain detection is available on this
|
|
56
|
+
* platform, so silent undercount stays visible (see README "Subagents").
|
|
57
|
+
* Extracted from `handleDoctorCommand` so the panel's doctor screen
|
|
58
|
+
* (`screens/doctor.ts`) can never drift from `/kankaku doctor`'s own output.
|
|
59
|
+
*/
|
|
60
|
+
export function buildDoctorLines(deps: KankakuCommandDeps, ctx: ExtensionContext): string[] {
|
|
61
|
+
const records = deps.log.readAll();
|
|
62
|
+
const orphans = orphanSubagents(records);
|
|
63
|
+
const uncertain = uncertainRecords(records);
|
|
64
|
+
const ancestorDetectionAvailable = deps.ancestorDetectionAvailable ? deps.ancestorDetectionAvailable() : process.platform !== "win32";
|
|
65
|
+
|
|
66
|
+
const lines = [
|
|
67
|
+
`ancestor-chain detection: ${ancestorDetectionAvailable ? "available" : "unavailable"}`,
|
|
68
|
+
`orphan subagent record(s): ${orphans.length}` +
|
|
69
|
+
(orphans.length > 0 ? " — recognised as someone's child (an env marker matched), but no orchestrator could be matched" : ""),
|
|
70
|
+
`uncertain record(s): ${uncertain.length}` +
|
|
71
|
+
(uncertain.length > 0
|
|
72
|
+
? " — no recognised child-env-marker, but a live tracked ancestor process was found; not counted as a new task, not synced"
|
|
73
|
+
: ""),
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
if (!ancestorDetectionAvailable) {
|
|
77
|
+
lines.push(
|
|
78
|
+
"on this platform/environment, ancestor-chain detection could not run (Windows, or a failed/unavailable ps/proc read): " +
|
|
79
|
+
"an unmarked subagent system may be counted twice (a genuine child with no recognised marker looks like a fresh top-level " +
|
|
80
|
+
"session). Mark it explicitly with KANKAKU_ROLE=subagent in the child's environment (or KANKAKU_ROLE=orchestrator to force " +
|
|
81
|
+
"the other way).",
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (deps.roleOverride) {
|
|
86
|
+
if (deps.childMarkerPresent) {
|
|
87
|
+
// R1: both signals present — the confirmed child marker always
|
|
88
|
+
// wins (config.ts#detectRole), so the override did not decide
|
|
89
|
+
// anything, whatever it said.
|
|
90
|
+
lines.push(
|
|
91
|
+
`role override: KANKAKU_ROLE=${deps.roleOverride} was present, but the confirmed child marker (GENTLE_PI_AGENTS_CHILD=1) takes precedence — resolved role: subagent`,
|
|
92
|
+
);
|
|
93
|
+
} else if (deps.overrideIgnoredInteractive) {
|
|
94
|
+
lines.push(
|
|
95
|
+
"role override: KANKAKU_ROLE=subagent was ignored for this interactive session (likely a leaked shell export) — resolved role: orchestrator",
|
|
96
|
+
);
|
|
97
|
+
} else {
|
|
98
|
+
lines.push(`role override: KANKAKU_ROLE=${deps.roleOverride} (deciding signal for this process's role)`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// C2 item 2/3: a configured child-env marker matched but was ignored
|
|
103
|
+
// for this process's role because it looked interactive — a
|
|
104
|
+
// configured marker, unlike a built-in one, never demotes an
|
|
105
|
+
// interactive session. Escalated when this process also has no
|
|
106
|
+
// tracked ancestor at all (C2 item 3's self-check: the strongest
|
|
107
|
+
// signal the marker is genuinely ambient, not a real subagent
|
|
108
|
+
// mechanism).
|
|
109
|
+
if (deps.configuredMarkerIgnoredInteractive) {
|
|
110
|
+
lines.push(
|
|
111
|
+
deps.hasTrackedAncestor
|
|
112
|
+
? "configured marker: a KANKAKU_SUBAGENT_CHILD_ENV marker was present but ignored for this interactive session — resolved role: orchestrator"
|
|
113
|
+
: "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",
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// C2 item 1: markers rejected at config load time as looking
|
|
118
|
+
// pi/shell/OS/npm-owned rather than genuinely child-only.
|
|
119
|
+
if (deps.rejectedSubagentChildEnvMarkers && deps.rejectedSubagentChildEnvMarkers.length > 0) {
|
|
120
|
+
for (const rejected of deps.rejectedSubagentChildEnvMarkers) {
|
|
121
|
+
lines.push(`rejected KANKAKU_SUBAGENT_CHILD_ENV marker "${rejected.name}": ${rejected.reason}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// SUBAGENT-REQ-001/002/003/005/017 (6b): active profiles, any configured
|
|
126
|
+
// tool names/markers, which profile matched each subagent record, and
|
|
127
|
+
// any tool-name ambiguity among the active set. Omitted entirely when
|
|
128
|
+
// deps.subagentProfiles was not wired (back-compat).
|
|
129
|
+
if (deps.subagentProfiles) {
|
|
130
|
+
const profiles = deps.subagentProfiles;
|
|
131
|
+
lines.push(`subagent profiles active: ${profiles.map((p) => p.id).join(", ")}`);
|
|
132
|
+
|
|
133
|
+
const configured = profiles.find((p) => p.id === "configured");
|
|
134
|
+
if (configured) {
|
|
135
|
+
if (configured.toolNames.length > 0) lines.push(`configured subagent tools: ${configured.toolNames.join(", ")}`);
|
|
136
|
+
if (configured.childEnvMarkers.length > 0) {
|
|
137
|
+
lines.push(`configured child-env markers: ${configured.childEnvMarkers.map((m) => (m.value !== undefined ? `${m.name}=${m.value}` : m.name)).join(", ")}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const subagentRecords = records.filter((record) => record.role === "subagent");
|
|
142
|
+
if (subagentRecords.length > 0) {
|
|
143
|
+
const counts = new Map<string, number>();
|
|
144
|
+
let unmatched = 0;
|
|
145
|
+
for (const record of subagentRecords) {
|
|
146
|
+
if (record.profile) {
|
|
147
|
+
counts.set(record.profile, (counts.get(record.profile) ?? 0) + 1);
|
|
148
|
+
} else {
|
|
149
|
+
unmatched++;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const parts = profiles.filter((p) => counts.has(p.id)).map((p) => `${p.id}: ${counts.get(p.id)}`);
|
|
153
|
+
if (unmatched > 0) parts.push(`unmatched: ${unmatched}`);
|
|
154
|
+
lines.push(`profile matches: ${parts.join(", ")}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const { toolName, profileIds } of findAmbiguousToolNames(profiles)) {
|
|
158
|
+
lines.push(`ambiguous tool name "${toolName}": registered by ${profileIds.join(", ")} — never guessed, resolved by child-env marker or left uncertain`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// SUBAGENT-REQ-015 (6c): same-pid overlapping orchestrator records —
|
|
163
|
+
// never observed from any real subagent mechanism today, but flagged
|
|
164
|
+
// here (informational only, never changing buildTasks' own numbers) as
|
|
165
|
+
// the observable signature an in-process nested session would leave.
|
|
166
|
+
for (const overlap of detectSameProcessOverlaps(records)) {
|
|
167
|
+
lines.push(
|
|
168
|
+
`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`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (deps.workLogRouting?.usedFallback) {
|
|
173
|
+
lines.push(
|
|
174
|
+
`kankaku: this subagent could not write to its orchestrator's directory (${deps.workLogRouting.parentDir}); ` +
|
|
175
|
+
"fell back to its own local worklog — this record may show as an orphan until reunited manually",
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (deps.registryHealth) {
|
|
180
|
+
const { keep, discard } = deps.registryHealth();
|
|
181
|
+
const counts = new Map<string, number>();
|
|
182
|
+
for (const { reason } of discard) counts.set(reason, (counts.get(reason) ?? 0) + 1);
|
|
183
|
+
const byReason = Array.from(counts.entries())
|
|
184
|
+
.map(([reason, count]) => `${reason}: ${count}`)
|
|
185
|
+
.join(", ");
|
|
186
|
+
lines.push(`registry (~/.kankaku/run): ${keep.length} entrie(s) trusted${discard.length > 0 ? `, ${discard.length} discarded (${byReason})` : ""}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const sessionDir = readNonDefaultSessionDir(ctx.sessionManager);
|
|
190
|
+
if (sessionDir !== undefined) {
|
|
191
|
+
lines.push(`session dir (non-default): ${sessionDir}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return lines;
|
|
195
|
+
}
|
|
196
|
+
|
|
47
197
|
const COMMAND_TOKENS = ["all", "tasks", "sessions", "client", "clients", "export", "doctor"];
|
|
48
198
|
/** Only offered when the hub is configured, so completions are unchanged for users without one. */
|
|
49
199
|
const HUB_COMMAND_TOKENS = ["target", "task", "projects", "catalog", "sync", "backfill"];
|
|
@@ -175,6 +325,14 @@ export interface KankakuCommandDeps {
|
|
|
175
325
|
* a caller has not wired this — back-compat with an older embedder.
|
|
176
326
|
*/
|
|
177
327
|
subagentProfiles?: SubagentProfile[];
|
|
328
|
+
/**
|
|
329
|
+
* Opens the `/kankaku` overlay panel (`adapters/panel/kankaku-panel.ts`)
|
|
330
|
+
* when present. `/kankaku` with no arguments prefers this over the plain
|
|
331
|
+
* summary report whenever a UI is attached; absent (or without a UI),
|
|
332
|
+
* behaviour is unchanged — the summary report, exactly as before this
|
|
333
|
+
* panel existed.
|
|
334
|
+
*/
|
|
335
|
+
openPanel?: (ctx: ExtensionContext) => Promise<void>;
|
|
178
336
|
}
|
|
179
337
|
|
|
180
338
|
export interface KankakuCommand {
|
|
@@ -233,11 +391,7 @@ export function registerKankakuCommand(pi: ExtensionAPI, deps: KankakuCommandDep
|
|
|
233
391
|
});
|
|
234
392
|
|
|
235
393
|
function showReport(ctx: ExtensionContext, report: KankakuReportData): void {
|
|
236
|
-
|
|
237
|
-
pi.appendEntry<KankakuReportData>(REPORT_ENTRY_TYPE, report);
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
ctx.ui.notify(`${report.title}\n${report.lines.join("\n")}`);
|
|
394
|
+
appendReportEntry(pi, ctx, report);
|
|
241
395
|
}
|
|
242
396
|
|
|
243
397
|
/**
|
|
@@ -372,38 +526,13 @@ export function registerKankakuCommand(pi: ExtensionAPI, deps: KankakuCommandDep
|
|
|
372
526
|
notifyError(ctx, new Error("hub unreachable; catalog not refreshed"));
|
|
373
527
|
return;
|
|
374
528
|
}
|
|
375
|
-
showReport(ctx, {
|
|
376
|
-
title: "catalog",
|
|
377
|
-
lines: [`refreshed: ${snapshot.clients.length} client(s), ${snapshot.projects.length} project(s)`],
|
|
378
|
-
});
|
|
529
|
+
showReport(ctx, { title: "catalog", lines: formatCatalogRefreshLines(snapshot) });
|
|
379
530
|
return;
|
|
380
531
|
}
|
|
381
532
|
|
|
382
533
|
notifyError(ctx, new Error(`unknown catalog subcommand: ${rest.join(" ")}`));
|
|
383
534
|
}
|
|
384
535
|
|
|
385
|
-
/** Render a {@link SyncSummary} as report lines: counts, any stop reason, the new watermark, and the unassigned breakdown. */
|
|
386
|
-
function formatSyncSummary(summary: SyncSummary): string[] {
|
|
387
|
-
const lines = [`uploaded ${summary.uploaded}, updated ${summary.updated}, skipped ${summary.skipped}, failed ${summary.failed.length}`];
|
|
388
|
-
|
|
389
|
-
if (summary.locked) lines.push("another sync is already in progress; nothing was attempted");
|
|
390
|
-
if (summary.error) lines.push(`stopped early: ${summary.error}`);
|
|
391
|
-
if (summary.syncedThrough) lines.push(`synced through ${summary.syncedThrough}`);
|
|
392
|
-
|
|
393
|
-
const unassignedEntries = Object.entries(summary.unassigned).sort(([a], [b]) => a.localeCompare(b));
|
|
394
|
-
if (unassignedEntries.length > 0) {
|
|
395
|
-
lines.push("unassigned (Sin determinar):");
|
|
396
|
-
for (const [label, count] of unassignedEntries) lines.push(` ${label}: ${count}`);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
if (summary.failed.length > 0) {
|
|
400
|
-
lines.push("failed:");
|
|
401
|
-
for (const entry of summary.failed) lines.push(` ${entry.id}: ${entry.reason}`);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
return lines;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
536
|
/** Handle `/kankaku sync [all|status]`; `rest` excludes the leading `sync` token. */
|
|
408
537
|
async function handleSyncCommand(rest: string[], ctx: ExtensionContext): Promise<void> {
|
|
409
538
|
const sync = deps.sync;
|
|
@@ -413,13 +542,7 @@ export function registerKankakuCommand(pi: ExtensionAPI, deps: KankakuCommandDep
|
|
|
413
542
|
}
|
|
414
543
|
|
|
415
544
|
if (rest.length === 1 && rest[0] === "status") {
|
|
416
|
-
|
|
417
|
-
const lines = [state?.syncedThrough ? `synced through ${state.syncedThrough}` : "never synced", `pending: ${pending}`];
|
|
418
|
-
if (staleOutsideWindow > 0) {
|
|
419
|
-
lines.push(`${staleOutsideWindow} task(s) never synced fall outside the sync window — run '/kankaku sync all' to upload them`);
|
|
420
|
-
}
|
|
421
|
-
if (state?.lastError) lines.push(`last error: ${state.lastError.message} (at ${state.lastError.at})`);
|
|
422
|
-
showReport(ctx, { title: "sync status", lines });
|
|
545
|
+
showReport(ctx, { title: "sync status", lines: buildSyncStatusLines(sync.status()) });
|
|
423
546
|
return;
|
|
424
547
|
}
|
|
425
548
|
|
|
@@ -430,7 +553,7 @@ export function registerKankakuCommand(pi: ExtensionAPI, deps: KankakuCommandDep
|
|
|
430
553
|
|
|
431
554
|
const full = rest[0] === "all";
|
|
432
555
|
const summary = await sync.run({ full });
|
|
433
|
-
showReport(ctx, { title: full ? "sync (all)" : "sync", lines:
|
|
556
|
+
showReport(ctx, { title: full ? "sync (all)" : "sync", lines: formatSyncSummaryLines(summary) });
|
|
434
557
|
}
|
|
435
558
|
|
|
436
559
|
/** Handle `/kankaku backfill`: a full sync, reported as the "Sin determinar" breakdown that needs reassigning in the web. */
|
|
@@ -442,161 +565,16 @@ export function registerKankakuCommand(pi: ExtensionAPI, deps: KankakuCommandDep
|
|
|
442
565
|
}
|
|
443
566
|
|
|
444
567
|
const summary = await sync.run({ full: true });
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const lines =
|
|
448
|
-
unassignedEntries.length > 0
|
|
449
|
-
? [
|
|
450
|
-
...unassignedEntries.map(([label, count]) => `${label}: ${count} task(s) -> Sin determinar`),
|
|
451
|
-
"reassign these in the hub web app's unassigned queue",
|
|
452
|
-
]
|
|
453
|
-
: ["no unassigned tasks"];
|
|
454
|
-
|
|
455
|
-
if (summary.error) lines.push(`stopped early: ${summary.error}`);
|
|
456
|
-
showReport(ctx, { title: "backfill", lines });
|
|
568
|
+
showReport(ctx, { title: "backfill", lines: formatBackfillLines(summary) });
|
|
457
569
|
}
|
|
458
570
|
|
|
459
571
|
/**
|
|
460
|
-
* Handle `/kankaku doctor`:
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
* silent undercount stays visible (see README "Subagents").
|
|
572
|
+
* Handle `/kankaku doctor`: builds the lines (shared with the panel's
|
|
573
|
+
* doctor screen — see {@link buildDoctorLines}) and shows them as the
|
|
574
|
+
* durable report.
|
|
464
575
|
*/
|
|
465
576
|
function handleDoctorCommand(ctx: ExtensionContext): void {
|
|
466
|
-
|
|
467
|
-
const orphans = orphanSubagents(records);
|
|
468
|
-
const uncertain = uncertainRecords(records);
|
|
469
|
-
const ancestorDetectionAvailable = deps.ancestorDetectionAvailable ? deps.ancestorDetectionAvailable() : process.platform !== "win32";
|
|
470
|
-
|
|
471
|
-
const lines = [
|
|
472
|
-
`ancestor-chain detection: ${ancestorDetectionAvailable ? "available" : "unavailable"}`,
|
|
473
|
-
`orphan subagent record(s): ${orphans.length}` +
|
|
474
|
-
(orphans.length > 0 ? " — recognised as someone's child (an env marker matched), but no orchestrator could be matched" : ""),
|
|
475
|
-
`uncertain record(s): ${uncertain.length}` +
|
|
476
|
-
(uncertain.length > 0
|
|
477
|
-
? " — no recognised child-env-marker, but a live tracked ancestor process was found; not counted as a new task, not synced"
|
|
478
|
-
: ""),
|
|
479
|
-
];
|
|
480
|
-
|
|
481
|
-
if (!ancestorDetectionAvailable) {
|
|
482
|
-
lines.push(
|
|
483
|
-
"on this platform/environment, ancestor-chain detection could not run (Windows, or a failed/unavailable ps/proc read): " +
|
|
484
|
-
"an unmarked subagent system may be counted twice (a genuine child with no recognised marker looks like a fresh top-level " +
|
|
485
|
-
"session). Mark it explicitly with KANKAKU_ROLE=subagent in the child's environment (or KANKAKU_ROLE=orchestrator to force " +
|
|
486
|
-
"the other way).",
|
|
487
|
-
);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
if (deps.roleOverride) {
|
|
491
|
-
if (deps.childMarkerPresent) {
|
|
492
|
-
// R1: both signals present — the confirmed child marker always
|
|
493
|
-
// wins (config.ts#detectRole), so the override did not decide
|
|
494
|
-
// anything, whatever it said.
|
|
495
|
-
lines.push(
|
|
496
|
-
`role override: KANKAKU_ROLE=${deps.roleOverride} was present, but the confirmed child marker (GENTLE_PI_AGENTS_CHILD=1) takes precedence — resolved role: subagent`,
|
|
497
|
-
);
|
|
498
|
-
} else if (deps.overrideIgnoredInteractive) {
|
|
499
|
-
lines.push(
|
|
500
|
-
"role override: KANKAKU_ROLE=subagent was ignored for this interactive session (likely a leaked shell export) — resolved role: orchestrator",
|
|
501
|
-
);
|
|
502
|
-
} else {
|
|
503
|
-
lines.push(`role override: KANKAKU_ROLE=${deps.roleOverride} (deciding signal for this process's role)`);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// C2 item 2/3: a configured child-env marker matched but was ignored
|
|
508
|
-
// for this process's role because it looked interactive — a
|
|
509
|
-
// configured marker, unlike a built-in one, never demotes an
|
|
510
|
-
// interactive session. Escalated when this process also has no
|
|
511
|
-
// tracked ancestor at all (C2 item 3's self-check: the strongest
|
|
512
|
-
// signal the marker is genuinely ambient, not a real subagent
|
|
513
|
-
// mechanism).
|
|
514
|
-
if (deps.configuredMarkerIgnoredInteractive) {
|
|
515
|
-
lines.push(
|
|
516
|
-
deps.hasTrackedAncestor
|
|
517
|
-
? "configured marker: a KANKAKU_SUBAGENT_CHILD_ENV marker was present but ignored for this interactive session — resolved role: orchestrator"
|
|
518
|
-
: "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",
|
|
519
|
-
);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// C2 item 1: markers rejected at config load time as looking
|
|
523
|
-
// pi/shell/OS/npm-owned rather than genuinely child-only.
|
|
524
|
-
if (deps.rejectedSubagentChildEnvMarkers && deps.rejectedSubagentChildEnvMarkers.length > 0) {
|
|
525
|
-
for (const rejected of deps.rejectedSubagentChildEnvMarkers) {
|
|
526
|
-
lines.push(`rejected KANKAKU_SUBAGENT_CHILD_ENV marker "${rejected.name}": ${rejected.reason}`);
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// SUBAGENT-REQ-001/002/003/005/017 (6b): active profiles, any configured
|
|
531
|
-
// tool names/markers, which profile matched each subagent record, and
|
|
532
|
-
// any tool-name ambiguity among the active set. Omitted entirely when
|
|
533
|
-
// deps.subagentProfiles was not wired (back-compat).
|
|
534
|
-
if (deps.subagentProfiles) {
|
|
535
|
-
const profiles = deps.subagentProfiles;
|
|
536
|
-
lines.push(`subagent profiles active: ${profiles.map((p) => p.id).join(", ")}`);
|
|
537
|
-
|
|
538
|
-
const configured = profiles.find((p) => p.id === "configured");
|
|
539
|
-
if (configured) {
|
|
540
|
-
if (configured.toolNames.length > 0) lines.push(`configured subagent tools: ${configured.toolNames.join(", ")}`);
|
|
541
|
-
if (configured.childEnvMarkers.length > 0) {
|
|
542
|
-
lines.push(`configured child-env markers: ${configured.childEnvMarkers.map((m) => (m.value !== undefined ? `${m.name}=${m.value}` : m.name)).join(", ")}`);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
const subagentRecords = records.filter((record) => record.role === "subagent");
|
|
547
|
-
if (subagentRecords.length > 0) {
|
|
548
|
-
const counts = new Map<string, number>();
|
|
549
|
-
let unmatched = 0;
|
|
550
|
-
for (const record of subagentRecords) {
|
|
551
|
-
if (record.profile) {
|
|
552
|
-
counts.set(record.profile, (counts.get(record.profile) ?? 0) + 1);
|
|
553
|
-
} else {
|
|
554
|
-
unmatched++;
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
const parts = profiles.filter((p) => counts.has(p.id)).map((p) => `${p.id}: ${counts.get(p.id)}`);
|
|
558
|
-
if (unmatched > 0) parts.push(`unmatched: ${unmatched}`);
|
|
559
|
-
lines.push(`profile matches: ${parts.join(", ")}`);
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
for (const { toolName, profileIds } of findAmbiguousToolNames(profiles)) {
|
|
563
|
-
lines.push(`ambiguous tool name "${toolName}": registered by ${profileIds.join(", ")} — never guessed, resolved by child-env marker or left uncertain`);
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// SUBAGENT-REQ-015 (6c): same-pid overlapping orchestrator records —
|
|
568
|
-
// never observed from any real subagent mechanism today, but flagged
|
|
569
|
-
// here (informational only, never changing buildTasks' own numbers) as
|
|
570
|
-
// the observable signature an in-process nested session would leave.
|
|
571
|
-
for (const overlap of detectSameProcessOverlaps(records)) {
|
|
572
|
-
lines.push(
|
|
573
|
-
`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`,
|
|
574
|
-
);
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
if (deps.workLogRouting?.usedFallback) {
|
|
578
|
-
lines.push(
|
|
579
|
-
`kankaku: this subagent could not write to its orchestrator's directory (${deps.workLogRouting.parentDir}); ` +
|
|
580
|
-
"fell back to its own local worklog — this record may show as an orphan until reunited manually",
|
|
581
|
-
);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
if (deps.registryHealth) {
|
|
585
|
-
const { keep, discard } = deps.registryHealth();
|
|
586
|
-
const counts = new Map<string, number>();
|
|
587
|
-
for (const { reason } of discard) counts.set(reason, (counts.get(reason) ?? 0) + 1);
|
|
588
|
-
const byReason = Array.from(counts.entries())
|
|
589
|
-
.map(([reason, count]) => `${reason}: ${count}`)
|
|
590
|
-
.join(", ");
|
|
591
|
-
lines.push(`registry (~/.kankaku/run): ${keep.length} entrie(s) trusted${discard.length > 0 ? `, ${discard.length} discarded (${byReason})` : ""}`);
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
const sessionDir = readNonDefaultSessionDir(ctx.sessionManager);
|
|
595
|
-
if (sessionDir !== undefined) {
|
|
596
|
-
lines.push(`session dir (non-default): ${sessionDir}`);
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
showReport(ctx, { title: "doctor", lines });
|
|
577
|
+
showReport(ctx, { title: "doctor", lines: buildDoctorLines(deps, ctx) });
|
|
600
578
|
}
|
|
601
579
|
|
|
602
580
|
/** Handle `/kankaku export [csv|json] [all]`; `rest` excludes the leading `export` token. Default format is csv. */
|
|
@@ -608,14 +586,9 @@ export function registerKankakuCommand(pi: ExtensionAPI, deps: KankakuCommandDep
|
|
|
608
586
|
|
|
609
587
|
const all = rest.includes("all");
|
|
610
588
|
const format: "csv" | "json" = rest.includes("json") ? "json" : "csv";
|
|
611
|
-
const
|
|
612
|
-
const today = localDay(new Date().toISOString());
|
|
613
|
-
const tasks = buildTasks(records).filter((task) => all || localDay(task.startedAt) === today);
|
|
614
|
-
const rows = exportRows(tasks);
|
|
615
|
-
const content = format === "json" ? toJson(rows) : toCsv(rows);
|
|
616
|
-
const name = `tasks-${all ? "all" : today}.${format}`;
|
|
589
|
+
const { name, content, rowCount } = buildExportContent(log.readAll(), { format, all });
|
|
617
590
|
const path = deps.writeExportFile(name, content);
|
|
618
|
-
showReport(ctx, { title: "export", lines: [`wrote ${
|
|
591
|
+
showReport(ctx, { title: "export", lines: [`wrote ${rowCount} row(s) to ${path}`] });
|
|
619
592
|
}
|
|
620
593
|
|
|
621
594
|
pi.registerCommand("kankaku", {
|
|
@@ -709,58 +682,35 @@ export function registerKankakuCommand(pi: ExtensionAPI, deps: KankakuCommandDep
|
|
|
709
682
|
return;
|
|
710
683
|
}
|
|
711
684
|
|
|
685
|
+
if (tokens.length === 0 && ctx.hasUI && deps.openPanel) {
|
|
686
|
+
await deps.openPanel(ctx);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
|
|
712
690
|
const all = tokens.includes("all");
|
|
713
691
|
const records = log.readAll();
|
|
714
|
-
const today = localDay(new Date().toISOString());
|
|
715
692
|
|
|
716
693
|
if (tokens.includes("clients")) {
|
|
717
|
-
|
|
718
|
-
showReport(ctx, {
|
|
719
|
-
title: all ? "clients (all days)" : "clients (today)",
|
|
720
|
-
lines: formatClients(summarizeByClient(tasks)).split("\n"),
|
|
721
|
-
});
|
|
694
|
+
showReport(ctx, buildClientsView(records, { all }));
|
|
722
695
|
return;
|
|
723
696
|
}
|
|
724
697
|
|
|
725
698
|
if (tokens.includes("projects")) {
|
|
726
|
-
|
|
727
|
-
showReport(ctx, {
|
|
728
|
-
title: all ? "projects (all days)" : "projects (today)",
|
|
729
|
-
lines: formatProjects(summarizeByProject(tasks)).split("\n"),
|
|
730
|
-
});
|
|
699
|
+
showReport(ctx, buildProjectsView(records, { all }));
|
|
731
700
|
return;
|
|
732
701
|
}
|
|
733
702
|
|
|
734
703
|
if (tokens.includes("tasks")) {
|
|
735
|
-
|
|
736
|
-
const scoped = all || !sessionId;
|
|
737
|
-
const tasks = buildTasks(records).filter((task) => scoped || task.sessionId === sessionId);
|
|
738
|
-
showReport(ctx, {
|
|
739
|
-
title: scoped ? "tasks (every session)" : "tasks (this session)",
|
|
740
|
-
lines: formatTasks(tasks).split("\n"),
|
|
741
|
-
});
|
|
704
|
+
showReport(ctx, buildTasksView(records, { all, sessionId: ctx.sessionManager.getSessionId() }));
|
|
742
705
|
return;
|
|
743
706
|
}
|
|
744
707
|
|
|
745
708
|
if (tokens.includes("sessions")) {
|
|
746
|
-
|
|
747
|
-
showReport(ctx, {
|
|
748
|
-
title: all ? "sessions (all days)" : "sessions (today)",
|
|
749
|
-
lines: formatSessions(buildSessions(tasks)).split("\n"),
|
|
750
|
-
});
|
|
709
|
+
showReport(ctx, buildSessionsView(records, { all }));
|
|
751
710
|
return;
|
|
752
711
|
}
|
|
753
712
|
|
|
754
|
-
|
|
755
|
-
const lines = formatReport(summary).split(" | ");
|
|
756
|
-
const uncertainCount = countUncertain(records, { all });
|
|
757
|
-
if (uncertainCount > 0) {
|
|
758
|
-
lines.push(`kankaku: ${uncertainCount} uncertain record(s) excluded from tasks — run /kankaku doctor`);
|
|
759
|
-
}
|
|
760
|
-
showReport(ctx, {
|
|
761
|
-
title: all ? "summary (all days)" : "summary (today)",
|
|
762
|
-
lines,
|
|
763
|
-
});
|
|
713
|
+
showReport(ctx, buildSummaryView(records, { all }));
|
|
764
714
|
} catch (error) {
|
|
765
715
|
notifyError(ctx, error);
|
|
766
716
|
}
|