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,91 @@
|
|
|
1
|
+
/** Current schema version for {@link WorkRecord}. */
|
|
2
|
+
export const WORK_RECORD_SCHEMA = 1;
|
|
3
|
+
/** The `prompt` of a record no user prompt started — see {@link RunTrigger}. */
|
|
4
|
+
export const EXTENSION_RUN_PROMPT = "(no user prompt — run started by an extension)";
|
|
5
|
+
export function emptyUsage() {
|
|
6
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
7
|
+
}
|
|
8
|
+
/** A finite number, or `0` for `undefined`/`NaN`/`Infinity`/non-numbers. */
|
|
9
|
+
export function finiteOrZero(value) {
|
|
10
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Share of prompt input tokens served from the provider's prompt cache:
|
|
14
|
+
* `cacheRead / (input + cacheRead + cacheWrite)`. pi's `usage.input` maps to
|
|
15
|
+
* the provider's `input_tokens`, which already excludes cached tokens, so
|
|
16
|
+
* the three fields are disjoint and this sum is the true denominator.
|
|
17
|
+
* Returns `undefined` when the denominator is `0` (nothing to compute a
|
|
18
|
+
* ratio from) rather than `0`, so callers never render a misleading `0%`.
|
|
19
|
+
* Non-finite fields count as `0`, mirroring {@link finiteOrZero}.
|
|
20
|
+
*/
|
|
21
|
+
export function cacheHitRatio(usage) {
|
|
22
|
+
const input = finiteOrZero(usage.input);
|
|
23
|
+
const cacheRead = finiteOrZero(usage.cacheRead);
|
|
24
|
+
const cacheWrite = finiteOrZero(usage.cacheWrite);
|
|
25
|
+
const denominator = input + cacheRead + cacheWrite;
|
|
26
|
+
return denominator === 0 ? undefined : cacheRead / denominator;
|
|
27
|
+
}
|
|
28
|
+
const ROLES = new Set(["orchestrator", "subagent"]);
|
|
29
|
+
const STATUSES = new Set(["completed", "aborted", "interrupted"]);
|
|
30
|
+
/** A finite, non-negative number: durations such as `wallMs` can never be negative. */
|
|
31
|
+
function isNonNegativeFinite(value) {
|
|
32
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
33
|
+
}
|
|
34
|
+
function isOrchestratorRef(value) {
|
|
35
|
+
if (!value || typeof value !== "object")
|
|
36
|
+
return false;
|
|
37
|
+
const ref = value;
|
|
38
|
+
return (typeof ref["pid"] === "number" &&
|
|
39
|
+
typeof ref["project"] === "string" &&
|
|
40
|
+
typeof ref["startedAt"] === "string" &&
|
|
41
|
+
(ref["dir"] === undefined || typeof ref["dir"] === "string"));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Runtime guard for a {@link WorkRecord} read back from disk. `readAll`
|
|
45
|
+
* skips lines that parse as JSON but fail this check, so a torn write or a
|
|
46
|
+
* record from an incompatible schema does not crash task/session views.
|
|
47
|
+
*/
|
|
48
|
+
export function isWorkRecord(value) {
|
|
49
|
+
if (!value || typeof value !== "object")
|
|
50
|
+
return false;
|
|
51
|
+
const record = value;
|
|
52
|
+
return (typeof record["schema"] === "number" &&
|
|
53
|
+
typeof record["id"] === "string" &&
|
|
54
|
+
ROLES.has(record["role"]) &&
|
|
55
|
+
typeof record["pid"] === "number" &&
|
|
56
|
+
typeof record["parentPid"] === "number" &&
|
|
57
|
+
typeof record["project"] === "string" &&
|
|
58
|
+
typeof record["prompt"] === "string" &&
|
|
59
|
+
typeof record["startedAt"] === "string" &&
|
|
60
|
+
typeof record["settledAt"] === "string" &&
|
|
61
|
+
isNonNegativeFinite(record["wallMs"]) &&
|
|
62
|
+
isNonNegativeFinite(record["waitingMs"]) &&
|
|
63
|
+
isNonNegativeFinite(record["workMs"]) &&
|
|
64
|
+
typeof record["runs"] === "number" &&
|
|
65
|
+
typeof record["turns"] === "number" &&
|
|
66
|
+
typeof record["tools"] === "object" &&
|
|
67
|
+
record["tools"] !== null &&
|
|
68
|
+
Array.isArray(record["subagents"]) &&
|
|
69
|
+
typeof record["usage"] === "object" &&
|
|
70
|
+
record["usage"] !== null &&
|
|
71
|
+
STATUSES.has(record["status"]) &&
|
|
72
|
+
(record["client"] === undefined || typeof record["client"] === "string") &&
|
|
73
|
+
(record["sessionName"] === undefined || typeof record["sessionName"] === "string") &&
|
|
74
|
+
(record["sessionDir"] === undefined || typeof record["sessionDir"] === "string") &&
|
|
75
|
+
(record["thinkingLevel"] === undefined || typeof record["thinkingLevel"] === "string") &&
|
|
76
|
+
(record["clientId"] === undefined || typeof record["clientId"] === "string") &&
|
|
77
|
+
(record["clientName"] === undefined || typeof record["clientName"] === "string") &&
|
|
78
|
+
(record["projectId"] === undefined || typeof record["projectId"] === "string") &&
|
|
79
|
+
(record["projectName"] === undefined || typeof record["projectName"] === "string") &&
|
|
80
|
+
(record["hubTaskId"] === undefined || typeof record["hubTaskId"] === "string") &&
|
|
81
|
+
(record["hubTaskTitle"] === undefined || typeof record["hubTaskTitle"] === "string") &&
|
|
82
|
+
(record["machine"] === undefined || typeof record["machine"] === "string") &&
|
|
83
|
+
(record["roleConfidence"] === undefined || record["roleConfidence"] === "uncertain") &&
|
|
84
|
+
(record["orchestratorRef"] === undefined || isOrchestratorRef(record["orchestratorRef"])) &&
|
|
85
|
+
(record["costObserved"] === undefined || record["costObserved"] === true) &&
|
|
86
|
+
(record["profile"] === undefined || typeof record["profile"] === "string") &&
|
|
87
|
+
(record["agent"] === undefined || typeof record["agent"] === "string") &&
|
|
88
|
+
(record["agentVersion"] === undefined || typeof record["agentVersion"] === "string") &&
|
|
89
|
+
(record["plugin"] === undefined || typeof record["plugin"] === "string") &&
|
|
90
|
+
(record["pluginVersion"] === undefined || typeof record["pluginVersion"] === "string"));
|
|
91
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hub work-target resolution: pure, no I/O.
|
|
3
|
+
*
|
|
4
|
+
* A `WorkTarget` identifies who a piece of work is billed to (client) and,
|
|
5
|
+
* optionally, which project it belongs to, both by PocketBase record id.
|
|
6
|
+
* Names travel alongside for display; no aggregation ever keys on a name
|
|
7
|
+
* (see `kankaku-pocketbase-proposal.md` D1).
|
|
8
|
+
*
|
|
9
|
+
* Resolution mirrors `client-label.ts#resolveClient`'s precedence pattern:
|
|
10
|
+
* session > project config file > catalog `repo_paths` match for the cwd >
|
|
11
|
+
* none. An id from any source that no longer resolves to an active,
|
|
12
|
+
* non-"unassigned" catalog row is treated as absent for that source and
|
|
13
|
+
* resolution falls through to the next one.
|
|
14
|
+
*/
|
|
15
|
+
export interface Client {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
code: string;
|
|
19
|
+
active: boolean;
|
|
20
|
+
/** `true` only for the single "Sin determinar" row; never offered by the picker. */
|
|
21
|
+
unassigned?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface Project {
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
code?: string;
|
|
27
|
+
clientId: string;
|
|
28
|
+
/** Absolute paths that map to this project; enables auto-selection by cwd. */
|
|
29
|
+
repoPaths: string[];
|
|
30
|
+
active: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A hub `tasks` row: something kankaku can link a session's `task_entries`
|
|
34
|
+
* row to (`task_entries.task`), never create. See `domain/hub-entry.ts` for
|
|
35
|
+
* the create-only assignment rule this feeds.
|
|
36
|
+
*/
|
|
37
|
+
export interface HubTask {
|
|
38
|
+
id: string;
|
|
39
|
+
title: string;
|
|
40
|
+
projectId: string;
|
|
41
|
+
status: "open" | "doing" | "done";
|
|
42
|
+
externalRef?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface WorkTarget {
|
|
45
|
+
clientId: string;
|
|
46
|
+
clientCode: string;
|
|
47
|
+
clientName: string;
|
|
48
|
+
projectId?: string;
|
|
49
|
+
projectCode?: string;
|
|
50
|
+
projectName?: string;
|
|
51
|
+
/** Hub `tasks` record id, kept only when it belongs to `projectId`. See {@link resolveWorkTarget}. */
|
|
52
|
+
hubTaskId?: string;
|
|
53
|
+
/** Denormalised alongside `hubTaskId` for display. */
|
|
54
|
+
hubTaskTitle?: string;
|
|
55
|
+
}
|
|
56
|
+
/** ids picked from the session, or from the project's `.kankaku/config.json`. */
|
|
57
|
+
export interface WorkTargetCandidate {
|
|
58
|
+
clientId: string;
|
|
59
|
+
projectId?: string;
|
|
60
|
+
/**
|
|
61
|
+
* A hub task the user picked for this session (`/kankaku task pick`).
|
|
62
|
+
* Only ever set on the session-level candidate — the project config file
|
|
63
|
+
* and `repoPaths` matching never carry one (task linking is session-only,
|
|
64
|
+
* see AGENTS.md).
|
|
65
|
+
*/
|
|
66
|
+
hubTaskId?: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The session-level override: `undefined` when no session entry exists yet
|
|
70
|
+
* (resolution falls through to the project/repo-paths sources), the literal
|
|
71
|
+
* `"skipped"` when the user explicitly declined the picker (resolution
|
|
72
|
+
* stops here, at "no target", and does not fall through), or explicit ids.
|
|
73
|
+
*/
|
|
74
|
+
export type WorkTargetSessionOverride = "skipped" | WorkTargetCandidate | undefined;
|
|
75
|
+
export interface ResolveWorkTargetInput {
|
|
76
|
+
session?: WorkTargetSessionOverride;
|
|
77
|
+
/** ids from the project's `.kankaku/config.json`. */
|
|
78
|
+
project?: WorkTargetCandidate;
|
|
79
|
+
/** Current working directory, matched against `Project.repoPaths`. */
|
|
80
|
+
cwd: string;
|
|
81
|
+
clients: Client[];
|
|
82
|
+
projects: Project[];
|
|
83
|
+
/** The hub's known tasks, to validate a candidate's `hubTaskId` against. Defaults to `[]` so existing callers compile unchanged. */
|
|
84
|
+
tasks?: HubTask[];
|
|
85
|
+
}
|
|
86
|
+
export type WorkTargetSourceName = "session" | "project" | "repoPaths";
|
|
87
|
+
/** Which source (if any) {@link resolveWorkTarget} would use, computed independently so callers can display it. */
|
|
88
|
+
export declare function resolveWorkTargetSource(input: ResolveWorkTargetInput): WorkTargetSourceName | undefined;
|
|
89
|
+
/**
|
|
90
|
+
* Resolve the effective {@link WorkTarget}: session override > project
|
|
91
|
+
* config file > catalog `repo_paths` match for `cwd` > none. A `"skipped"`
|
|
92
|
+
* session override stops resolution immediately (the user explicitly
|
|
93
|
+
* declined), returning `undefined` rather than falling through.
|
|
94
|
+
*/
|
|
95
|
+
export declare function resolveWorkTarget(input: ResolveWorkTargetInput): WorkTarget | undefined;
|
|
96
|
+
/**
|
|
97
|
+
* `<clientName>`, `<clientName> · <projectName>`, or with a linked hub task
|
|
98
|
+
* appended as `... › <hubTaskTitle>` — the display label used by the
|
|
99
|
+
* status bar and the "remember" prompt.
|
|
100
|
+
*/
|
|
101
|
+
export declare function formatWorkTargetLabel(target: WorkTarget): string;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hub work-target resolution: pure, no I/O.
|
|
3
|
+
*
|
|
4
|
+
* A `WorkTarget` identifies who a piece of work is billed to (client) and,
|
|
5
|
+
* optionally, which project it belongs to, both by PocketBase record id.
|
|
6
|
+
* Names travel alongside for display; no aggregation ever keys on a name
|
|
7
|
+
* (see `kankaku-pocketbase-proposal.md` D1).
|
|
8
|
+
*
|
|
9
|
+
* Resolution mirrors `client-label.ts#resolveClient`'s precedence pattern:
|
|
10
|
+
* session > project config file > catalog `repo_paths` match for the cwd >
|
|
11
|
+
* none. An id from any source that no longer resolves to an active,
|
|
12
|
+
* non-"unassigned" catalog row is treated as absent for that source and
|
|
13
|
+
* resolution falls through to the next one.
|
|
14
|
+
*/
|
|
15
|
+
/** A client usable as a work target: exists, active, and not the "unassigned" row. */
|
|
16
|
+
function findUsableClient(clients, clientId) {
|
|
17
|
+
const client = clients.find((candidate) => candidate.id === clientId);
|
|
18
|
+
if (!client || !client.active || client.unassigned)
|
|
19
|
+
return undefined;
|
|
20
|
+
return client;
|
|
21
|
+
}
|
|
22
|
+
/** A project usable as a work target: exists, active, and belongs to `clientId`. */
|
|
23
|
+
function findUsableProject(projects, projectId, clientId) {
|
|
24
|
+
const project = projects.find((candidate) => candidate.id === projectId);
|
|
25
|
+
if (!project || !project.active || project.clientId !== clientId)
|
|
26
|
+
return undefined;
|
|
27
|
+
return project;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A hub task usable as a link target: exists and belongs to `projectId`.
|
|
31
|
+
* Any {@link HubTask.status} is accepted — a task marked `done` mid-session
|
|
32
|
+
* keeps linking — only the project match matters here.
|
|
33
|
+
*/
|
|
34
|
+
function findUsableTask(tasks, taskId, projectId) {
|
|
35
|
+
const task = tasks.find((candidate) => candidate.id === taskId);
|
|
36
|
+
if (!task || task.projectId !== projectId)
|
|
37
|
+
return undefined;
|
|
38
|
+
return task;
|
|
39
|
+
}
|
|
40
|
+
function buildTarget(client, project, task) {
|
|
41
|
+
return {
|
|
42
|
+
clientId: client.id,
|
|
43
|
+
clientCode: client.code,
|
|
44
|
+
clientName: client.name,
|
|
45
|
+
...(project !== undefined
|
|
46
|
+
? {
|
|
47
|
+
projectId: project.id,
|
|
48
|
+
...(project.code !== undefined ? { projectCode: project.code } : {}),
|
|
49
|
+
projectName: project.name,
|
|
50
|
+
}
|
|
51
|
+
: {}),
|
|
52
|
+
...(task !== undefined ? { hubTaskId: task.id, hubTaskTitle: task.title } : {}),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Resolve a candidate (from the session override or the project config
|
|
57
|
+
* file) into a target, or `undefined` when the client id does not resolve
|
|
58
|
+
* to a usable client. A projectId that does not resolve to a usable
|
|
59
|
+
* project of that client is dropped — the client-only target is still
|
|
60
|
+
* returned — rather than failing the whole candidate. A `hubTaskId` is
|
|
61
|
+
* kept only when it resolves to a task of the resolved project; a target
|
|
62
|
+
* with no project never carries one.
|
|
63
|
+
*/
|
|
64
|
+
function resolveCandidate(candidate, clients, projects, tasks) {
|
|
65
|
+
const client = findUsableClient(clients, candidate.clientId);
|
|
66
|
+
if (!client)
|
|
67
|
+
return undefined;
|
|
68
|
+
const project = candidate.projectId !== undefined ? findUsableProject(projects, candidate.projectId, client.id) : undefined;
|
|
69
|
+
const task = project !== undefined && candidate.hubTaskId !== undefined ? findUsableTask(tasks, candidate.hubTaskId, project.id) : undefined;
|
|
70
|
+
return buildTarget(client, project, task);
|
|
71
|
+
}
|
|
72
|
+
/** The longest-matching active project whose `repoPaths` contains `cwd`, exactly or as an ancestor directory. */
|
|
73
|
+
function matchByRepoPath(cwd, projects) {
|
|
74
|
+
let best;
|
|
75
|
+
let bestLength = -1;
|
|
76
|
+
for (const project of projects) {
|
|
77
|
+
if (!project.active)
|
|
78
|
+
continue;
|
|
79
|
+
for (const repoPath of project.repoPaths) {
|
|
80
|
+
if (!isCwdWithin(cwd, repoPath))
|
|
81
|
+
continue;
|
|
82
|
+
if (repoPath.length > bestLength) {
|
|
83
|
+
bestLength = repoPath.length;
|
|
84
|
+
best = project;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return best;
|
|
89
|
+
}
|
|
90
|
+
/** `true` when `cwd` equals `repoPath`, or is a subdirectory of it. */
|
|
91
|
+
function isCwdWithin(cwd, repoPath) {
|
|
92
|
+
if (cwd === repoPath)
|
|
93
|
+
return true;
|
|
94
|
+
const withSeparator = repoPath.endsWith("/") ? repoPath : `${repoPath}/`;
|
|
95
|
+
return cwd.startsWith(withSeparator);
|
|
96
|
+
}
|
|
97
|
+
/** Which source (if any) {@link resolveWorkTarget} would use, computed independently so callers can display it. */
|
|
98
|
+
export function resolveWorkTargetSource(input) {
|
|
99
|
+
const { session } = input;
|
|
100
|
+
const tasks = input.tasks ?? [];
|
|
101
|
+
if (session === "skipped")
|
|
102
|
+
return undefined;
|
|
103
|
+
if (session !== undefined && resolveCandidate(session, input.clients, input.projects, tasks) !== undefined)
|
|
104
|
+
return "session";
|
|
105
|
+
if (input.project !== undefined && resolveCandidate(input.project, input.clients, input.projects, tasks) !== undefined)
|
|
106
|
+
return "project";
|
|
107
|
+
const matched = matchByRepoPath(input.cwd, input.projects);
|
|
108
|
+
if (matched && findUsableClient(input.clients, matched.clientId))
|
|
109
|
+
return "repoPaths";
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Resolve the effective {@link WorkTarget}: session override > project
|
|
114
|
+
* config file > catalog `repo_paths` match for `cwd` > none. A `"skipped"`
|
|
115
|
+
* session override stops resolution immediately (the user explicitly
|
|
116
|
+
* declined), returning `undefined` rather than falling through.
|
|
117
|
+
*/
|
|
118
|
+
export function resolveWorkTarget(input) {
|
|
119
|
+
const { session } = input;
|
|
120
|
+
const tasks = input.tasks ?? [];
|
|
121
|
+
if (session === "skipped")
|
|
122
|
+
return undefined;
|
|
123
|
+
if (session !== undefined) {
|
|
124
|
+
const resolved = resolveCandidate(session, input.clients, input.projects, tasks);
|
|
125
|
+
if (resolved)
|
|
126
|
+
return resolved;
|
|
127
|
+
}
|
|
128
|
+
if (input.project !== undefined) {
|
|
129
|
+
const resolved = resolveCandidate(input.project, input.clients, input.projects, tasks);
|
|
130
|
+
if (resolved)
|
|
131
|
+
return resolved;
|
|
132
|
+
}
|
|
133
|
+
const matched = matchByRepoPath(input.cwd, input.projects);
|
|
134
|
+
if (matched) {
|
|
135
|
+
const client = findUsableClient(input.clients, matched.clientId);
|
|
136
|
+
if (client)
|
|
137
|
+
return buildTarget(client, matched, undefined);
|
|
138
|
+
}
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* `<clientName>`, `<clientName> · <projectName>`, or with a linked hub task
|
|
143
|
+
* appended as `... › <hubTaskTitle>` — the display label used by the
|
|
144
|
+
* status bar and the "remember" prompt.
|
|
145
|
+
*/
|
|
146
|
+
export function formatWorkTargetLabel(target) {
|
|
147
|
+
const base = target.projectName ? `${target.clientName} · ${target.projectName}` : target.clientName;
|
|
148
|
+
return target.hubTaskTitle ? `${base} › ${target.hubTaskTitle}` : base;
|
|
149
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { Clock } from "../ports/clock.ts";
|
|
2
|
+
import type { RunTrigger, UsageTotals, WorkRecordCore, WorkStatus } from "./work-record.ts";
|
|
3
|
+
import type { SegmentRule } from "./segment-rule.ts";
|
|
4
|
+
import type { SubagentProfile } from "./subagent-profile.ts";
|
|
5
|
+
export interface WorkTrackerOptions {
|
|
6
|
+
clock: Clock;
|
|
7
|
+
interactiveTools: string[];
|
|
8
|
+
/** Every active {@link SubagentProfile} (ADR 0020); a tool call opens a subagent span when its name matches any profile's `toolNames`. See `config.ts#loadConfig`. */
|
|
9
|
+
subagentProfiles: SubagentProfile[];
|
|
10
|
+
/** Rules that tag a tool execution's span under a named segment. Defaults to none. */
|
|
11
|
+
segmentRules?: SegmentRule[];
|
|
12
|
+
}
|
|
13
|
+
interface RunEndMessage {
|
|
14
|
+
role: string;
|
|
15
|
+
stopReason?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Pure domain state machine that turns pi lifecycle events into finished
|
|
19
|
+
* {@link WorkRecord} entries. Holds no I/O; timestamps come from the
|
|
20
|
+
* injected {@link Clock} so behaviour is deterministic under test.
|
|
21
|
+
*/
|
|
22
|
+
export declare class WorkTracker {
|
|
23
|
+
private readonly clock;
|
|
24
|
+
private readonly interactiveTools;
|
|
25
|
+
private readonly subagentProfiles;
|
|
26
|
+
private readonly segmentRules;
|
|
27
|
+
private state;
|
|
28
|
+
/** Set by {@link onRunStart}, consumed by {@link onAgentStart}: tells a user-announced run from one an extension started. */
|
|
29
|
+
private runAnnounced;
|
|
30
|
+
/** `true` between an agent loop's start and its `agent_end`. */
|
|
31
|
+
private loopActive;
|
|
32
|
+
/**
|
|
33
|
+
* The previous record, set aside because a new run began after its last
|
|
34
|
+
* loop ended but BEFORE its `agent_settled` reached us. pi clears its
|
|
35
|
+
* "run active" flag and only then awaits the `agent_settled` handlers, in
|
|
36
|
+
* extension load order; an extension loaded earlier (gentle-pi) can start
|
|
37
|
+
* the next run from its own handler. Whether that `agent_start` was a new
|
|
38
|
+
* run or an `agent.continue()` of the same one is only knowable at settle
|
|
39
|
+
* time — see {@link settleAll}.
|
|
40
|
+
*/
|
|
41
|
+
private closing;
|
|
42
|
+
constructor(options: WorkTrackerOptions);
|
|
43
|
+
/**
|
|
44
|
+
* An agent loop actually started. pi announces a USER prompt with
|
|
45
|
+
* `before_agent_start` ({@link onRunStart}) and then this; a run an
|
|
46
|
+
* extension starts itself (`sendCustomMessage(..., { triggerTurn: true })`,
|
|
47
|
+
* how gentle-pi wakes the orchestrator when a background subagent
|
|
48
|
+
* finishes) only ever produces this one. Without it that whole run — its
|
|
49
|
+
* time, its cost, and the subagent spans it opens, which its children need
|
|
50
|
+
* to join — was never recorded.
|
|
51
|
+
*/
|
|
52
|
+
onAgentStart(): void;
|
|
53
|
+
onRunStart(prompt: string, trigger?: RunTrigger): void;
|
|
54
|
+
private openRun;
|
|
55
|
+
onTurnEnd(usage: Partial<UsageTotals> | undefined): void;
|
|
56
|
+
onToolStart(toolCallId: string, toolName: string, args: Record<string, unknown> | undefined): void;
|
|
57
|
+
onToolEnd(toolCallId: string, result: unknown): void;
|
|
58
|
+
onUiPromptStart(_kind: string): void;
|
|
59
|
+
onUiPromptEnd(_kind: string): void;
|
|
60
|
+
onRunEnd(messages: RunEndMessage[]): void;
|
|
61
|
+
/** Single-record convenience over {@link settleAll}; production code uses `settleAll`. */
|
|
62
|
+
onSettled(): WorkRecordCore | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* `agent_settled` arrived. Normally closes the one open record. When a
|
|
65
|
+
* record was set aside (see {@link closing}):
|
|
66
|
+
* - a loop is still running → the `agent_start` was a genuinely NEW run
|
|
67
|
+
* that overtook this settle: close only the old record, at the instant
|
|
68
|
+
* the new run began, and leave the new one open;
|
|
69
|
+
* - no loop is running → it was an `agent.continue()` of the same run
|
|
70
|
+
* (retry, overflow recovery, queued message): fold it back, one record.
|
|
71
|
+
* A user-announced prompt is never folded — it keeps its own record.
|
|
72
|
+
*/
|
|
73
|
+
settleAll(): WorkRecordCore[];
|
|
74
|
+
/** Single-record convenience over {@link shutdownAll}. */
|
|
75
|
+
onShutdown(): WorkRecordCore | undefined;
|
|
76
|
+
/** The session is going away: every open record is written, the running one as `interrupted`. */
|
|
77
|
+
shutdownAll(): WorkRecordCore[];
|
|
78
|
+
/**
|
|
79
|
+
* Returns what {@link onSettled}/{@link onShutdown} would produce right
|
|
80
|
+
* now, without mutating any state: open spans are truncated only in the
|
|
81
|
+
* returned snapshot, so the tracker keeps running unaffected and a later
|
|
82
|
+
* `peek` or the eventual settle still sees the spans' true open-ended
|
|
83
|
+
* state. `undefined` when idle. The returned `id` matches the id the
|
|
84
|
+
* eventual settled record will carry, since both are generated once
|
|
85
|
+
* in {@link onRunStart}.
|
|
86
|
+
*/
|
|
87
|
+
peek(status: WorkStatus): WorkRecordCore | undefined;
|
|
88
|
+
private buildRecordFrom;
|
|
89
|
+
}
|
|
90
|
+
export {};
|