kankaku-pi 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1438 -0
- package/dist/adapters/cached-catalog.d.ts +42 -0
- package/dist/adapters/cached-catalog.js +121 -0
- package/dist/adapters/export-writer.d.ts +13 -0
- package/dist/adapters/export-writer.js +28 -0
- package/dist/adapters/file-modes.d.ts +20 -0
- package/dist/adapters/file-modes.js +34 -0
- package/dist/adapters/hub-actions.d.ts +35 -0
- package/dist/adapters/hub-actions.js +70 -0
- package/dist/adapters/hub-credentials.d.ts +35 -0
- package/dist/adapters/hub-credentials.js +58 -0
- package/dist/adapters/jsonl-work-log.d.ts +20 -0
- package/dist/adapters/jsonl-work-log.js +62 -0
- package/dist/adapters/kankaku-dir.d.ts +38 -0
- package/dist/adapters/kankaku-dir.js +85 -0
- package/dist/adapters/lazy-jsonl-work-log.d.ts +17 -0
- package/dist/adapters/lazy-jsonl-work-log.js +31 -0
- package/dist/adapters/pocketbase-catalog.d.ts +16 -0
- package/dist/adapters/pocketbase-catalog.js +56 -0
- package/dist/adapters/pocketbase-client.d.ts +81 -0
- package/dist/adapters/pocketbase-client.js +148 -0
- package/dist/adapters/pocketbase-sink.d.ts +53 -0
- package/dist/adapters/pocketbase-sink.js +181 -0
- package/dist/adapters/project-config.d.ts +42 -0
- package/dist/adapters/project-config.js +108 -0
- package/dist/adapters/report-data.d.ts +12 -0
- package/dist/adapters/report-data.js +8 -0
- package/dist/adapters/report-views.d.ts +45 -0
- package/dist/adapters/report-views.js +73 -0
- package/dist/adapters/report.d.ts +112 -0
- package/dist/adapters/report.js +236 -0
- package/dist/adapters/sync-runner.d.ts +114 -0
- package/dist/adapters/sync-runner.js +273 -0
- package/dist/adapters/sync-state-store.d.ts +62 -0
- package/dist/adapters/sync-state-store.js +188 -0
- package/dist/config.d.ts +168 -0
- package/dist/config.js +392 -0
- package/dist/domain/ancestry-match.d.ts +49 -0
- package/dist/domain/ancestry-match.js +82 -0
- package/dist/domain/client-label.d.ts +28 -0
- package/dist/domain/client-label.js +44 -0
- package/dist/domain/day.d.ts +2 -0
- package/dist/domain/day.js +8 -0
- package/dist/domain/export.d.ts +38 -0
- package/dist/domain/export.js +68 -0
- package/dist/domain/hub-entry.d.ts +234 -0
- package/dist/domain/hub-entry.js +265 -0
- package/dist/domain/index.d.ts +19 -0
- package/dist/domain/index.js +19 -0
- package/dist/domain/intervals.d.ts +17 -0
- package/dist/domain/intervals.js +43 -0
- package/dist/domain/registry-health.d.ts +49 -0
- package/dist/domain/registry-health.js +58 -0
- package/dist/domain/segment-rule.d.ts +10 -0
- package/dist/domain/segment-rule.js +1 -0
- package/dist/domain/subagent-profile.d.ts +278 -0
- package/dist/domain/subagent-profile.js +418 -0
- package/dist/domain/sync-plan.d.ts +151 -0
- package/dist/domain/sync-plan.js +196 -0
- package/dist/domain/task-view.d.ts +117 -0
- package/dist/domain/task-view.js +428 -0
- package/dist/domain/work-record.d.ts +236 -0
- package/dist/domain/work-record.js +91 -0
- package/dist/domain/work-target.d.ts +101 -0
- package/dist/domain/work-target.js +149 -0
- package/dist/domain/work-tracker.d.ts +90 -0
- package/dist/domain/work-tracker.js +405 -0
- package/dist/hub/index.d.ts +25 -0
- package/dist/hub/index.js +25 -0
- package/dist/ports/catalog.d.ts +31 -0
- package/dist/ports/catalog.js +1 -0
- package/dist/ports/clock.d.ts +3 -0
- package/dist/ports/clock.js +1 -0
- package/dist/ports/index.d.ts +11 -0
- package/dist/ports/index.js +1 -0
- package/dist/ports/inflight-store.d.ts +15 -0
- package/dist/ports/inflight-store.js +1 -0
- package/dist/ports/process-registry.d.ts +72 -0
- package/dist/ports/process-registry.js +1 -0
- package/dist/ports/work-log.d.ts +14 -0
- package/dist/ports/work-log.js +1 -0
- package/dist/ports/work-sink.d.ts +39 -0
- package/dist/ports/work-sink.js +1 -0
- package/package.json +66 -0
- package/src/adapters/agent-info.ts +86 -0
- package/src/adapters/ancestry.ts +260 -0
- package/src/adapters/cached-catalog.ts +147 -0
- package/src/adapters/export-writer.ts +33 -0
- package/src/adapters/file-inflight-store.ts +115 -0
- package/src/adapters/file-modes.ts +35 -0
- package/src/adapters/hub-actions.ts +82 -0
- package/src/adapters/hub-credentials.ts +95 -0
- package/src/adapters/jsonl-work-log.ts +67 -0
- package/src/adapters/kankaku-command.ts +717 -0
- package/src/adapters/kankaku-dir.ts +102 -0
- package/src/adapters/lazy-file-inflight-store.ts +43 -0
- package/src/adapters/lazy-jsonl-work-log.ts +39 -0
- package/src/adapters/machine-process-registry.ts +256 -0
- package/src/adapters/panel/kankaku-panel.ts +419 -0
- package/src/adapters/panel/panel-items.ts +87 -0
- package/src/adapters/panel/panel-lines.ts +13 -0
- package/src/adapters/panel/panel-theme.ts +32 -0
- package/src/adapters/panel/screens/about.ts +69 -0
- package/src/adapters/panel/screens/doctor.ts +89 -0
- package/src/adapters/panel/screens/export.ts +123 -0
- package/src/adapters/panel/screens/report.ts +143 -0
- package/src/adapters/panel/screens/sync.ts +136 -0
- package/src/adapters/panel/screens/target.ts +384 -0
- package/src/adapters/pi-tracker.ts +753 -0
- package/src/adapters/pocketbase-catalog.ts +89 -0
- package/src/adapters/pocketbase-client.ts +197 -0
- package/src/adapters/pocketbase-sink.ts +236 -0
- package/src/adapters/process-identity-memo.ts +102 -0
- package/src/adapters/process-identity.ts +162 -0
- package/src/adapters/project-config.ts +116 -0
- package/src/adapters/report-data.ts +13 -0
- package/src/adapters/report-views.ts +98 -0
- package/src/adapters/report.ts +335 -0
- package/src/adapters/session-client.ts +116 -0
- package/src/adapters/session-dir.ts +28 -0
- package/src/adapters/session-target.ts +431 -0
- package/src/adapters/status-bar.ts +86 -0
- package/src/adapters/subagent-startup.ts +66 -0
- package/src/adapters/sync-runner.ts +340 -0
- package/src/adapters/sync-state-store.ts +227 -0
- package/src/adapters/target-picker.ts +127 -0
- package/src/config.ts +536 -0
- package/src/domain/ancestry-match.ts +84 -0
- package/src/domain/client-label.ts +56 -0
- package/src/domain/day.ts +8 -0
- package/src/domain/export.ts +107 -0
- package/src/domain/hub-entry.ts +433 -0
- package/src/domain/index.ts +19 -0
- package/src/domain/intervals.ts +53 -0
- package/src/domain/panel-model.ts +270 -0
- package/src/domain/registry-health.ts +87 -0
- package/src/domain/segment-rule.ts +10 -0
- package/src/domain/subagent-profile.ts +495 -0
- package/src/domain/sync-plan.ts +266 -0
- package/src/domain/task-view.ts +526 -0
- package/src/domain/work-record.ts +320 -0
- package/src/domain/work-target.ts +234 -0
- package/src/domain/work-tracker.ts +485 -0
- package/src/extension.ts +346 -0
- package/src/hub/index.ts +25 -0
- package/src/ports/catalog.ts +33 -0
- package/src/ports/clock.ts +3 -0
- package/src/ports/index.ts +11 -0
- package/src/ports/inflight-store.ts +16 -0
- package/src/ports/process-registry.ts +75 -0
- package/src/ports/work-log.ts +15 -0
- package/src/ports/work-sink.ts +35 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public library entrypoint (`kankaku-pi/domain`): the pure domain layer, with
|
|
3
|
+
* no I/O and no pi imports. See AGENTS.md "Architecture (hexagonal)" and
|
|
4
|
+
* odd/tasks/library-exports.md.
|
|
5
|
+
*/
|
|
6
|
+
export * from "./ancestry-match.ts";
|
|
7
|
+
export * from "./client-label.ts";
|
|
8
|
+
export * from "./day.ts";
|
|
9
|
+
export * from "./export.ts";
|
|
10
|
+
export * from "./hub-entry.ts";
|
|
11
|
+
export * from "./intervals.ts";
|
|
12
|
+
export * from "./registry-health.ts";
|
|
13
|
+
export * from "./segment-rule.ts";
|
|
14
|
+
export * from "./subagent-profile.ts";
|
|
15
|
+
export * from "./sync-plan.ts";
|
|
16
|
+
export * from "./task-view.ts";
|
|
17
|
+
export * from "./work-record.ts";
|
|
18
|
+
export * from "./work-target.ts";
|
|
19
|
+
export * from "./work-tracker.ts";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export interface Interval {
|
|
2
|
+
start: number;
|
|
3
|
+
end: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Restrict every interval to `[windowStart, windowEnd]` and drop any that
|
|
8
|
+
* become empty (or invalid) after clamping.
|
|
9
|
+
*/
|
|
10
|
+
export function clampIntervals(intervals: Interval[], windowStart: number, windowEnd: number): Interval[] {
|
|
11
|
+
return intervals
|
|
12
|
+
.map((interval) => ({
|
|
13
|
+
start: Math.max(interval.start, windowStart),
|
|
14
|
+
end: Math.min(interval.end, windowEnd),
|
|
15
|
+
}))
|
|
16
|
+
.filter((interval) => interval.end > interval.start);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Total duration covered by the union of the given intervals — never the
|
|
21
|
+
* sum, so overlapping (e.g. parallel subagent) spans are not double-counted.
|
|
22
|
+
*/
|
|
23
|
+
export function unionMs(intervals: Array<{ start: number; end: number }>): number {
|
|
24
|
+
const sorted = [...intervals].sort((a, b) => a.start - b.start);
|
|
25
|
+
|
|
26
|
+
let total = 0;
|
|
27
|
+
let currentStart: number | undefined;
|
|
28
|
+
let currentEnd: number | undefined;
|
|
29
|
+
|
|
30
|
+
for (const interval of sorted) {
|
|
31
|
+
if (interval.end <= interval.start) continue;
|
|
32
|
+
|
|
33
|
+
if (currentStart === undefined || currentEnd === undefined) {
|
|
34
|
+
currentStart = interval.start;
|
|
35
|
+
currentEnd = interval.end;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (interval.start <= currentEnd) {
|
|
40
|
+
currentEnd = Math.max(currentEnd, interval.end);
|
|
41
|
+
} else {
|
|
42
|
+
total += currentEnd - currentStart;
|
|
43
|
+
currentStart = interval.start;
|
|
44
|
+
currentEnd = interval.end;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (currentStart !== undefined && currentEnd !== undefined) {
|
|
49
|
+
total += currentEnd - currentStart;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return total;
|
|
53
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure model for the `/kankaku` panel (see odd/tasks/kankaku-panel.md): the
|
|
3
|
+
* set of screens, the root menu, an immutable navigation stack, the
|
|
4
|
+
* title/footer-hint text every adapter screen renders from, and pure row
|
|
5
|
+
* builders (e.g. {@link buildTargetRows}) that turn kankaku state into row
|
|
6
|
+
* models for a screen's `SettingsList`. No I/O, no pi imports — see
|
|
7
|
+
* AGENTS.md "Architecture (hexagonal)".
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { WorkTarget } from "./work-target.ts";
|
|
11
|
+
|
|
12
|
+
export type PanelScreenId = "root" | "target" | "report" | "sync" | "export" | "doctor" | "about";
|
|
13
|
+
|
|
14
|
+
/** One row of the root menu (`rootMenu`): a section the panel can navigate to. */
|
|
15
|
+
export interface PanelMenuItem {
|
|
16
|
+
id: PanelScreenId;
|
|
17
|
+
label: string;
|
|
18
|
+
description: string;
|
|
19
|
+
/** Offered only when the hub (PocketBase) is configured; see {@link rootMenu}. */
|
|
20
|
+
hubOnly: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const ROOT_MENU_ITEMS: PanelMenuItem[] = [
|
|
24
|
+
// Not hub-only: the legacy billing label (`/kankaku client <name>`) is
|
|
25
|
+
// set from this same screen and works with no hub configured at all.
|
|
26
|
+
{ id: "target", label: "Target", description: "Billing client, project, hub task, legacy label", hubOnly: false },
|
|
27
|
+
{ id: "report", label: "Report", description: "Today/all totals, tasks, sessions, clients, projects", hubOnly: false },
|
|
28
|
+
{ id: "sync", label: "Sync", description: "Status, sync now, sync all, backfill, catalog refresh", hubOnly: true },
|
|
29
|
+
{ id: "export", label: "Export", description: "Write today's or every task as csv/json", hubOnly: false },
|
|
30
|
+
{ id: "doctor", label: "Doctor", description: "Orphan/uncertain subagent counts, ancestor detection", hubOnly: false },
|
|
31
|
+
{ id: "about", label: "About", description: "Versions, KANKAKU_DIR, env-only config", hubOnly: false },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The root menu's rows, in a fixed order. `hubOnly` rows (only `sync`;
|
|
36
|
+
* `target` is not hub-only — see `ROOT_MENU_ITEMS`'s comment) are omitted
|
|
37
|
+
* entirely when the hub is not configured, so the panel offers exactly what
|
|
38
|
+
* `/kankaku` itself would today.
|
|
39
|
+
*/
|
|
40
|
+
export function rootMenu(options: { hubConfigured: boolean }): PanelMenuItem[] {
|
|
41
|
+
return ROOT_MENU_ITEMS.filter((item) => options.hubConfigured || !item.hubOnly);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Immutable navigation stack: `stack[0]` is always `"root"`. */
|
|
45
|
+
export interface PanelNav {
|
|
46
|
+
stack: PanelScreenId[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A fresh navigation stack, positioned at the root screen. */
|
|
50
|
+
export function navRoot(): PanelNav {
|
|
51
|
+
return { stack: ["root"] };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Push a screen onto the stack, returning a new {@link PanelNav}; the input is never mutated. */
|
|
55
|
+
export function navPush(nav: PanelNav, id: PanelScreenId): PanelNav {
|
|
56
|
+
return { stack: [...nav.stack, id] };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Pop the current screen, returning a new {@link PanelNav} and whether the
|
|
61
|
+
* panel should close. At the root, there is nothing left to pop: the stack
|
|
62
|
+
* is returned unchanged and `closed` is `true` — the caller closes the
|
|
63
|
+
* overlay instead of navigating.
|
|
64
|
+
*/
|
|
65
|
+
export function navBack(nav: PanelNav): { nav: PanelNav; closed: boolean } {
|
|
66
|
+
if (nav.stack.length <= 1) {
|
|
67
|
+
return { nav, closed: true };
|
|
68
|
+
}
|
|
69
|
+
return { nav: { stack: nav.stack.slice(0, -1) }, closed: false };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The screen currently on top of the stack. */
|
|
73
|
+
export function navCurrent(nav: PanelNav): PanelScreenId {
|
|
74
|
+
return nav.stack[nav.stack.length - 1] ?? "root";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const SCREEN_TITLES: Record<Exclude<PanelScreenId, "root">, string> = {
|
|
78
|
+
target: "Target",
|
|
79
|
+
report: "Report",
|
|
80
|
+
sync: "Sync",
|
|
81
|
+
export: "Export",
|
|
82
|
+
doctor: "Doctor",
|
|
83
|
+
about: "About",
|
|
84
|
+
};
|
|
85
|
+
|
|
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. */
|
|
90
|
+
export function panelTitle(screen: PanelScreenId): string {
|
|
91
|
+
if (screen === "root") return `${PANEL_TITLE_PREFIX} kankaku`;
|
|
92
|
+
return `${PANEL_TITLE_PREFIX} kankaku · ${SCREEN_TITLES[screen]}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** One clickable/keyboard hint shown in the panel's footer. */
|
|
96
|
+
export interface PanelHint {
|
|
97
|
+
key: string;
|
|
98
|
+
label: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The footer hint row for a screen: navigation hints, an optional search
|
|
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`.
|
|
106
|
+
*/
|
|
107
|
+
export function footerHints(screen: PanelScreenId, options: { searchable: boolean }): PanelHint[] {
|
|
108
|
+
const hints: PanelHint[] = [{ key: "↑↓", label: "move" }];
|
|
109
|
+
|
|
110
|
+
if (screen === "root") {
|
|
111
|
+
hints.push({ key: "enter", label: "open" });
|
|
112
|
+
if (options.searchable) hints.push({ key: "/", label: "search" });
|
|
113
|
+
hints.push({ key: "esc", label: "close" });
|
|
114
|
+
return hints;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
hints.push({ key: "enter", label: "select" });
|
|
118
|
+
if (options.searchable) hints.push({ key: "/", label: "search" });
|
|
119
|
+
hints.push({ key: "esc/←", label: "back" }, { key: "q", label: "close" });
|
|
120
|
+
return hints;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** One row of a `SettingsList`-backed screen (e.g. {@link buildTargetRows}'s output). */
|
|
124
|
+
export interface PanelRow {
|
|
125
|
+
id: string;
|
|
126
|
+
label: string;
|
|
127
|
+
value: string;
|
|
128
|
+
description?: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const NONE_VALUE = "— none —";
|
|
132
|
+
|
|
133
|
+
/** Pure input for {@link buildTargetRows}. */
|
|
134
|
+
export interface TargetRowsInput {
|
|
135
|
+
/** Whether the hub (PocketBase) is configured; see `rootMenu`'s `target` row, which is offered either way. */
|
|
136
|
+
hubConfigured: boolean;
|
|
137
|
+
/** The current effective hub target, if any (`SessionTarget#effectiveTarget()`). */
|
|
138
|
+
target?: WorkTarget;
|
|
139
|
+
/** Which source produced {@link target} (already formatted by the caller as `"session" | "config" | "repoPaths"`), or `undefined` for none. */
|
|
140
|
+
source?: string;
|
|
141
|
+
/** The current effective legacy billing label (`SessionClient#effectiveClient()`), if any. */
|
|
142
|
+
legacyLabel?: string;
|
|
143
|
+
/** Which source produced {@link legacyLabel} (already formatted by the caller), or `undefined` for none. */
|
|
144
|
+
legacySource?: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Build the target screen's rows from kankaku's current billing-target
|
|
149
|
+
* state. Pure: the caller (`adapters/panel/screens/target.ts`) resolves
|
|
150
|
+
* `target`/`source`/`legacyLabel`/`legacySource` from `SessionTarget`/
|
|
151
|
+
* `SessionClient` and formats each source name as a plain string, so this
|
|
152
|
+
* function never depends on their concrete types.
|
|
153
|
+
*
|
|
154
|
+
* `client`/`project`/`task`/`source`/`remember` are included only when
|
|
155
|
+
* {@link TargetRowsInput.hubConfigured} is `true` — the panel's `target`
|
|
156
|
+
* screen is offered either way (see `rootMenu`), but those rows only make
|
|
157
|
+
* sense once a hub exists to resolve them against. `legacy` is always
|
|
158
|
+
* included: the legacy billing label works with no hub at all.
|
|
159
|
+
*/
|
|
160
|
+
export function buildTargetRows(input: TargetRowsInput): PanelRow[] {
|
|
161
|
+
const rows: PanelRow[] = [];
|
|
162
|
+
const target = input.target;
|
|
163
|
+
const hasClient = target !== undefined;
|
|
164
|
+
const hasProject = target !== undefined && target.projectId !== undefined;
|
|
165
|
+
|
|
166
|
+
if (input.hubConfigured) {
|
|
167
|
+
rows.push({
|
|
168
|
+
id: "client",
|
|
169
|
+
label: "Client",
|
|
170
|
+
value: hasClient ? `${target.clientName} (${target.clientCode})` : NONE_VALUE,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
rows.push({
|
|
174
|
+
id: "project",
|
|
175
|
+
label: "Project",
|
|
176
|
+
value: hasProject ? (target.projectName ?? NONE_VALUE) : NONE_VALUE,
|
|
177
|
+
...(hasClient ? {} : { description: "pick a client first" }),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
rows.push({
|
|
181
|
+
id: "task",
|
|
182
|
+
label: "Task",
|
|
183
|
+
value: hasProject && target.hubTaskTitle !== undefined ? target.hubTaskTitle : NONE_VALUE,
|
|
184
|
+
...(hasProject ? {} : { description: "pick a project first" }),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
rows.push({
|
|
188
|
+
id: "source",
|
|
189
|
+
label: "Source",
|
|
190
|
+
value: input.source ?? "none",
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
rows.push({
|
|
194
|
+
id: "remember",
|
|
195
|
+
label: "Remember",
|
|
196
|
+
value: "save to config.json",
|
|
197
|
+
description: "Saves the client and project (never the linked task) to this repository's .kankaku/config.json.",
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
rows.push({
|
|
202
|
+
id: "legacy",
|
|
203
|
+
label: "Legacy label",
|
|
204
|
+
value: input.legacyLabel ?? NONE_VALUE,
|
|
205
|
+
description: `Source: ${input.legacySource ?? "none"}`,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return rows;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Pure input for {@link buildAboutRows}. Every env-only value is passed in
|
|
213
|
+
* already extracted from `KankakuConfig` (see `config.ts`) rather than the
|
|
214
|
+
* config object itself: `KankakuConfig` lives outside `src/domain/`, and
|
|
215
|
+
* this module must import nothing but `src/domain/`/`src/ports/` (see
|
|
216
|
+
* AGENTS.md "Architecture (hexagonal)").
|
|
217
|
+
*/
|
|
218
|
+
export interface AboutRowsInput {
|
|
219
|
+
/** kankaku's own version (`adapters/agent-info.ts#resolvePluginVersion`). */
|
|
220
|
+
pluginVersion?: string;
|
|
221
|
+
/** pi's version (`adapters/agent-info.ts#resolveAgentVersion`). */
|
|
222
|
+
agentVersion?: string;
|
|
223
|
+
/** This session's resolved kankaku directory (`adapters/kankaku-dir.ts#resolveKankakuDir`). */
|
|
224
|
+
kankakuDir: string;
|
|
225
|
+
/** The hub (PocketBase) URL, when configured. */
|
|
226
|
+
hubUrl?: string;
|
|
227
|
+
/** `KankakuConfig.interactiveTools`. */
|
|
228
|
+
interactiveTools: string[];
|
|
229
|
+
/** `KankakuConfig.segmentRules.length`. */
|
|
230
|
+
segmentRuleCount: number;
|
|
231
|
+
/** `KankakuConfig.subagentProfiles`' ids. */
|
|
232
|
+
subagentProfileNames: string[];
|
|
233
|
+
/** `KankakuConfig.client`. */
|
|
234
|
+
client?: string;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Build the panel's `about` screen rows: versions, the resolved directory,
|
|
239
|
+
* the hub URL, and every env-only setting (`KANKAKU_INTERACTIVE_TOOLS`,
|
|
240
|
+
* `KANKAKU_SEGMENTS`, `KANKAKU_SUBAGENT_TOOLS`/`KANKAKU_SUBAGENT_CHILD_ENV`,
|
|
241
|
+
* `KANKAKU_CLIENT`) with its env var name as the row's description — every
|
|
242
|
+
* row is read-only (see `screens/about.ts`).
|
|
243
|
+
*/
|
|
244
|
+
export function buildAboutRows(input: AboutRowsInput): PanelRow[] {
|
|
245
|
+
return [
|
|
246
|
+
{ id: "kankaku", label: "kankaku", value: input.pluginVersion ?? "unknown" },
|
|
247
|
+
{ id: "pi", label: "pi", value: input.agentVersion ?? "unknown" },
|
|
248
|
+
{ id: "directory", label: "Directory", value: input.kankakuDir },
|
|
249
|
+
{ id: "hub", label: "Hub", value: input.hubUrl ?? "not configured" },
|
|
250
|
+
{
|
|
251
|
+
id: "interactive-tools",
|
|
252
|
+
label: "Interactive tools",
|
|
253
|
+
value: input.interactiveTools.length > 0 ? input.interactiveTools.join(", ") : NONE_VALUE,
|
|
254
|
+
description: "KANKAKU_INTERACTIVE_TOOLS",
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
id: "segments",
|
|
258
|
+
label: "Segments",
|
|
259
|
+
value: `${input.segmentRuleCount} rule(s)`,
|
|
260
|
+
description: "KANKAKU_SEGMENTS",
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
id: "subagent-profiles",
|
|
264
|
+
label: "Subagent profiles",
|
|
265
|
+
value: input.subagentProfileNames.length > 0 ? input.subagentProfileNames.join(", ") : NONE_VALUE,
|
|
266
|
+
description: "KANKAKU_SUBAGENT_TOOLS / KANKAKU_SUBAGENT_CHILD_ENV",
|
|
267
|
+
},
|
|
268
|
+
{ id: "client", label: "Client", value: input.client ?? NONE_VALUE, description: "KANKAKU_CLIENT" },
|
|
269
|
+
];
|
|
270
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide which machine-wide {@link RegistryEntry} rows a sweep should keep
|
|
3
|
+
* vs discard, and why. Pure, no I/O — `adapters/machine-process-registry.ts`
|
|
4
|
+
* drives this with real `isAlive`/`liveStartId`/`now`, and
|
|
5
|
+
* `adapters/kankaku-command.ts`'s `/kankaku doctor` reuses it (cheap mode,
|
|
6
|
+
* no fresh identity re-verification) to report registry health.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { START_ID_TOLERANCE_MS } from "./ancestry-match.ts";
|
|
10
|
+
import type { RegistryEntry } from "../ports/process-registry.ts";
|
|
11
|
+
|
|
12
|
+
/** Sane last-resort ceiling on an entry's age, regardless of aliveness/identity: 7 days. */
|
|
13
|
+
export const DEFAULT_MAX_ENTRY_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
14
|
+
|
|
15
|
+
export type DiscardReason =
|
|
16
|
+
| "dead"
|
|
17
|
+
/** The pid is alive, but its live start identity no longer matches what this entry recorded: the OS has reused this pid for a different process instance. */
|
|
18
|
+
| "stale-reuse"
|
|
19
|
+
| "over-age";
|
|
20
|
+
|
|
21
|
+
export interface RegistryClassifyDeps {
|
|
22
|
+
isAlive: (pid: number) => boolean;
|
|
23
|
+
/** Live start identity for a pid, from the same ancestry snapshot the caller already took. `undefined` means "unknown" — never treated as evidence of reuse. */
|
|
24
|
+
liveStartId: (pid: number) => number | undefined;
|
|
25
|
+
now: number;
|
|
26
|
+
maxAgeMs: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface RegistryClassification {
|
|
30
|
+
keep: RegistryEntry[];
|
|
31
|
+
discard: Array<{ entry: RegistryEntry; reason: DiscardReason }>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Classify every entry except `ownPid`'s (the caller's own, just-written
|
|
36
|
+
* entry — always kept, never re-evaluated against its own freshly-recorded
|
|
37
|
+
* data). An entry is discarded the first reason that applies, in this
|
|
38
|
+
* order: dead pid; alive but identity mismatched beyond
|
|
39
|
+
* {@link START_ID_TOLERANCE_MS} (pid reuse) — only ever checked when the
|
|
40
|
+
* entry actually carries a `processStartId`; older than `maxAgeMs`.
|
|
41
|
+
* Anything else is kept.
|
|
42
|
+
*
|
|
43
|
+
* An entry with no verifiable `processStartId` at all (written by a build
|
|
44
|
+
* predating this field, or a torn/partial write) is **never used for
|
|
45
|
+
* identity matching** (`domain/ancestry-match.ts#findAncestorEntry` already
|
|
46
|
+
* requires both sides to carry a start id) — but that alone is no longer
|
|
47
|
+
* grounds for deletion here (F4): a live, in-age entry that merely cannot be
|
|
48
|
+
* verified is kept, exactly like a verified one, so a sweep run by an
|
|
49
|
+
* unrelated sibling process can never un-register a genuinely live
|
|
50
|
+
* orchestrator whose own start-time read happened to fail. It still gets
|
|
51
|
+
* cleaned up the ordinary way once its pid dies or it ages out — dead and
|
|
52
|
+
* over-age entries are discarded regardless of whether they carry a
|
|
53
|
+
* `processStartId`.
|
|
54
|
+
*/
|
|
55
|
+
export function classifyRegistryEntries(entries: RegistryEntry[], ownPid: number, deps: RegistryClassifyDeps): RegistryClassification {
|
|
56
|
+
const keep: RegistryEntry[] = [];
|
|
57
|
+
const discard: RegistryClassification["discard"] = [];
|
|
58
|
+
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
if (entry.pid === ownPid) {
|
|
61
|
+
keep.push(entry);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!deps.isAlive(entry.pid)) {
|
|
66
|
+
discard.push({ entry, reason: "dead" });
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (entry.processStartId !== undefined) {
|
|
71
|
+
const liveId = deps.liveStartId(entry.pid);
|
|
72
|
+
if (liveId !== undefined && Math.abs(liveId - entry.processStartId) > START_ID_TOLERANCE_MS) {
|
|
73
|
+
discard.push({ entry, reason: "stale-reuse" });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (deps.now - Date.parse(entry.startedAt) > deps.maxAgeMs) {
|
|
79
|
+
discard.push({ entry, reason: "over-age" });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
keep.push(entry);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { keep, discard };
|
|
87
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A rule that tags a tool execution as belonging to a named segment (e.g.
|
|
3
|
+
* `review`) when the tool name matches `tool` and the tool's argument text
|
|
4
|
+
* matches `pattern`.
|
|
5
|
+
*/
|
|
6
|
+
export interface SegmentRule {
|
|
7
|
+
tag: string;
|
|
8
|
+
tool: string;
|
|
9
|
+
pattern: RegExp;
|
|
10
|
+
}
|