@sgtbeatdown/pi-tasks 0.1.5 → 0.1.7
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 +23 -118
- package/package.json +3 -5
- package/skills/wolfpack-pi-task-delegation/SKILL.md +33 -21
- package/src/extension.ts +112 -28
- package/src/index.ts +1 -1
- package/src/task-core.ts +128 -38
- package/src/task-inbox.ts +58 -17
- package/src/task-protocol.ts +16 -0
- package/src/task-store.ts +28 -141
- package/src/gateway-client.ts +0 -409
- package/src/legacy-extension.ts +0 -241
- package/src/legacy-task-inbox.ts +0 -148
- package/src/metrics-cli.ts +0 -20
- package/src/metrics.ts +0 -288
- package/src/task-artifacts.ts +0 -153
- package/src/task-board.ts +0 -365
- package/src/v1-compat-extension.ts +0 -1
package/src/legacy-task-inbox.ts
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import type { TaskAssignment, TaskEvent, TaskStatus, WolfpackGatewayClient } from "./gateway-client";
|
|
2
|
-
|
|
3
|
-
const TASK_EVENT_CUSTOM_TYPE = "wolfpack-task-event";
|
|
4
|
-
const TASK_CURSOR_CUSTOM_TYPE = "wolfpack-task-cursor";
|
|
5
|
-
|
|
6
|
-
interface InboxContext {
|
|
7
|
-
readonly hasPendingMessages: () => boolean;
|
|
8
|
-
readonly sessionManager: {
|
|
9
|
-
readonly buildContextEntries: () => readonly unknown[];
|
|
10
|
-
readonly getEntries: () => readonly unknown[];
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface InboxPi {
|
|
15
|
-
sendMessage(message: { readonly customType: string; readonly content: string; readonly display: boolean; readonly details: TaskEventDetails }, options: { readonly triggerTurn: true; readonly deliverAs: "followUp" }): void;
|
|
16
|
-
appendEntry(customType: string, data: { readonly cursor: string }): void;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
interface TaskEventDetails {
|
|
20
|
-
readonly taskId: string;
|
|
21
|
-
readonly eventId: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface InboxClient {
|
|
25
|
-
inbox(cursor: string, includeAcknowledged: boolean): Promise<{ readonly events: readonly TaskEvent[]; readonly nextCursor: string; readonly hasMore: boolean }>;
|
|
26
|
-
status(taskId: string): Promise<TaskStatus>;
|
|
27
|
-
delivered(taskId: string, eventId: string): Promise<unknown>;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export async function deliverTaskInbox(pi: InboxPi, client: InboxClient, ctx: InboxContext, initialCursor: string): Promise<string> {
|
|
31
|
-
if (ctx.hasPendingMessages()) return initialCursor;
|
|
32
|
-
const entries = ctx.sessionManager.getEntries();
|
|
33
|
-
const incorporated = incorporatedEvents(entries);
|
|
34
|
-
const includeAcknowledged = initialCursor === "0" && !hasPersistedCursor(entries);
|
|
35
|
-
let cursor = initialCursor;
|
|
36
|
-
for (;;) {
|
|
37
|
-
const page = await client.inbox(cursor, includeAcknowledged);
|
|
38
|
-
for (const event of page.events) {
|
|
39
|
-
const disposition = inboxDisposition(event.type);
|
|
40
|
-
if (disposition === "internal") continue;
|
|
41
|
-
if (disposition === "unknown") throw new Error(`unknown task inbox event type: ${event.type}`);
|
|
42
|
-
if (ctx.hasPendingMessages()) return cursor;
|
|
43
|
-
const eventKey = key(event.taskId, event.id);
|
|
44
|
-
const snapshot = await client.status(event.taskId);
|
|
45
|
-
if (ctx.hasPendingMessages()) return cursor;
|
|
46
|
-
if (incorporated.has(eventKey)) {
|
|
47
|
-
if (event.destination.sessionId === snapshot.task.target.sessionId) await client.delivered(event.taskId, event.id);
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
if (terminalHistoryAcknowledged(snapshot)) continue;
|
|
51
|
-
pi.sendMessage({
|
|
52
|
-
customType: TASK_EVENT_CUSTOM_TYPE,
|
|
53
|
-
content: renderTaskEvent(event, snapshot),
|
|
54
|
-
display: true,
|
|
55
|
-
details: { taskId: event.taskId, eventId: event.id },
|
|
56
|
-
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
57
|
-
if (!incorporatedEvents(ctx.sessionManager.getEntries()).has(eventKey)) return cursor;
|
|
58
|
-
incorporated.add(eventKey);
|
|
59
|
-
if (event.destination.sessionId === snapshot.task.target.sessionId) await client.delivered(event.taskId, event.id);
|
|
60
|
-
}
|
|
61
|
-
// Inbox cursors are machine-local delivery sequences, never task event sequences.
|
|
62
|
-
cursor = page.nextCursor;
|
|
63
|
-
pi.appendEntry(TASK_CURSOR_CUSTOM_TYPE, { cursor });
|
|
64
|
-
if (!page.hasMore) return cursor;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function hasPersistedCursor(entries: readonly unknown[]): boolean {
|
|
69
|
-
return entries.some((entry) => isRecord(entry) && entry.type === "custom" && entry.customType === TASK_CURSOR_CUSTOM_TYPE && isRecord(entry.data) && decimal(entry.data.cursor));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function inboxDisposition(type: string): "model-visible" | "internal" | "unknown" {
|
|
73
|
-
if (type === "task.created" || type === "task.question" || type === "task.answer" || type === "task.information" || terminalEvent(type) || type === "task.cancel_requested") return "model-visible";
|
|
74
|
-
if (type === "task.received" || type === "task.receipt_confirmed" || type === "task.delivered" || type === "message.delivered" || type === "task.parent_ack_pending" || type === "task.parent_acknowledged" || type === "event.delivery_failed" || type === "task.late_terminal") return "internal";
|
|
75
|
-
return "unknown";
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function terminalHistoryAcknowledged(snapshot: TaskStatus): boolean {
|
|
79
|
-
return ["completed", "failed", "cancelled", "timed_out"].includes(snapshot.status) && snapshot.events.some((event) => event.type === "task.parent_acknowledged");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function restoredInboxCursor(entries: readonly unknown[]): string {
|
|
83
|
-
let cursor = "0";
|
|
84
|
-
for (const entry of entries) {
|
|
85
|
-
if (!isRecord(entry) || entry.type !== "custom" || entry.customType !== TASK_CURSOR_CUSTOM_TYPE || !isRecord(entry.data) || !decimal(entry.data.cursor)) continue;
|
|
86
|
-
cursor = entry.data.cursor;
|
|
87
|
-
}
|
|
88
|
-
return cursor;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function incorporatedEvents(entries: readonly unknown[]): Set<string> {
|
|
92
|
-
const events = new Set<string>();
|
|
93
|
-
for (const entry of entries) {
|
|
94
|
-
if (!isRecord(entry) || entry.type !== "custom_message" || entry.customType !== TASK_EVENT_CUSTOM_TYPE || !isRecord(entry.details) || !nonEmpty(entry.details.taskId) || !nonEmpty(entry.details.eventId)) continue;
|
|
95
|
-
events.add(key(entry.details.taskId, entry.details.eventId));
|
|
96
|
-
}
|
|
97
|
-
return events;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function renderTaskEvent(event: TaskEvent, snapshot: TaskStatus): string {
|
|
101
|
-
const header = `task: \`${event.taskId}\` · event: \`${event.id}\``;
|
|
102
|
-
if (event.type === "task.created") return renderAssignment(header, snapshot.task, snapshot.warnings);
|
|
103
|
-
const message = event.message ? `\n\n${event.message}` : "";
|
|
104
|
-
const completion = event.completion ?? snapshot.completion;
|
|
105
|
-
const completionText = completion ? `\n\n**summary:** ${completion.summary}` : "";
|
|
106
|
-
const parentPrompt = terminalEvent(event.type) && event.destination.sessionId === snapshot.task.source.sessionId && snapshot.task.onCompletePrompt
|
|
107
|
-
? `\n\n## parent follow-up\n${snapshot.task.onCompletePrompt}`
|
|
108
|
-
: "";
|
|
109
|
-
const warnings = renderWarnings(snapshot.warnings);
|
|
110
|
-
return `## task ${label(event.type)}\n${header}${message}${completionText}${parentPrompt}${warnings}`;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function renderAssignment(header: string, assignment: TaskAssignment, warnings: readonly { readonly code: string; readonly message: string }[]): string {
|
|
114
|
-
const role = assignment.role ? `\n\n## role\n${assignment.role}` : "";
|
|
115
|
-
const context = assignment.context?.summary ? `\n\n## context\n${assignment.context.summary}` : "";
|
|
116
|
-
const refs = assignment.context?.refs?.length
|
|
117
|
-
? `\n\n## selected refs\n${assignment.context.refs.map((ref) => `- \`${ref.path}\`${ref.selector ? ` — ${ref.selector}` : ""}${ref.purpose ? ` — ${ref.purpose}` : ""}`).join("\n")}`
|
|
118
|
-
: "";
|
|
119
|
-
return `## task assignment\n${header}\n\n${assignment.task}${role}${context}${refs}${renderWarnings(warnings)}`;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function renderWarnings(warnings: readonly { readonly code: string; readonly message: string }[]): string {
|
|
123
|
-
return warnings.length === 0 ? "" : `\n\n## gateway warnings\n${warnings.map((warning) => `- ${warning.code}: ${warning.message}`).join("\n")}`;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function label(type: string): string {
|
|
127
|
-
return type.replace(/^task\./, "").replaceAll("_", " ");
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function terminalEvent(type: string): boolean {
|
|
131
|
-
return type === "task.completed" || type === "task.failed" || type === "task.cancelled" || type === "task.timed_out";
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function key(taskId: string, eventId: string): string {
|
|
135
|
-
return `${taskId}\u0000${eventId}`;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
139
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function nonEmpty(value: unknown): value is string {
|
|
143
|
-
return typeof value === "string" && value.length > 0;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function decimal(value: unknown): value is string {
|
|
147
|
-
return typeof value === "string" && /^(0|[1-9][0-9]*)$/.test(value);
|
|
148
|
-
}
|
package/src/metrics-cli.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { analyzeTaskStoreMetrics } from "./metrics";
|
|
2
|
-
import { buildTaskBoard } from "./task-board";
|
|
3
|
-
|
|
4
|
-
export async function runTaskMetricsCli(args: readonly string[]): Promise<number> {
|
|
5
|
-
const tasksRoot = args[0];
|
|
6
|
-
if (!tasksRoot) {
|
|
7
|
-
console.error("usage: bun run task-metrics <tasksRoot> [--board]");
|
|
8
|
-
return 1;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const report = args.includes("--board")
|
|
12
|
-
? await buildTaskBoard(tasksRoot)
|
|
13
|
-
: await analyzeTaskStoreMetrics(tasksRoot);
|
|
14
|
-
console.log(JSON.stringify(report, null, 2));
|
|
15
|
-
return 0;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
if (import.meta.main) {
|
|
19
|
-
process.exitCode = await runTaskMetricsCli(process.argv.slice(2));
|
|
20
|
-
}
|
package/src/metrics.ts
DELETED
|
@@ -1,288 +0,0 @@
|
|
|
1
|
-
import { readTaskStoreArtifacts } from "./task-artifacts";
|
|
2
|
-
import type { TaskStoreArtifactRecord, TaskStoreDiagnostic } from "./task-artifacts";
|
|
3
|
-
|
|
4
|
-
const LARGEST_LIST_LIMIT = 10;
|
|
5
|
-
|
|
6
|
-
export interface TaskMetricCount {
|
|
7
|
-
readonly taskId: string;
|
|
8
|
-
readonly charCount: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export type TaskMetricsDiagnostic = TaskStoreDiagnostic;
|
|
12
|
-
|
|
13
|
-
export interface TaskGroupingMetrics {
|
|
14
|
-
readonly byPhaseId: Readonly<Record<string, number>>;
|
|
15
|
-
readonly byIssueId: Readonly<Record<string, number>>;
|
|
16
|
-
readonly byRootCause: Readonly<Record<string, number>>;
|
|
17
|
-
readonly loopsPerIssueId: Readonly<Record<string, number>>;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface TaskStoreMetricsReport {
|
|
21
|
-
readonly schemaVersion: 1;
|
|
22
|
-
readonly tasksRoot: string;
|
|
23
|
-
readonly totalTaskRecords: number;
|
|
24
|
-
readonly statusCounts: Readonly<Record<string, number>>;
|
|
25
|
-
readonly preflight: {
|
|
26
|
-
readonly rejectedCount: number;
|
|
27
|
-
readonly failedChecksByName: Readonly<Record<string, number>>;
|
|
28
|
-
readonly unavailableChecksByName: Readonly<Record<string, number>>;
|
|
29
|
-
};
|
|
30
|
-
readonly characterSizes: {
|
|
31
|
-
readonly totalTaskTextChars: number;
|
|
32
|
-
readonly totalAssignmentChars: number;
|
|
33
|
-
readonly totalInstructionChars: number;
|
|
34
|
-
readonly totalResultSummaryChars: number;
|
|
35
|
-
readonly totalSerializedResultPayloadChars: number;
|
|
36
|
-
};
|
|
37
|
-
readonly largest: {
|
|
38
|
-
readonly taskPrompts: readonly TaskMetricCount[];
|
|
39
|
-
readonly results: readonly TaskMetricCount[];
|
|
40
|
-
};
|
|
41
|
-
readonly grouping: TaskGroupingMetrics;
|
|
42
|
-
readonly verification: {
|
|
43
|
-
readonly entryCount: number;
|
|
44
|
-
readonly commandCounts: Readonly<Record<string, number>>;
|
|
45
|
-
readonly statusCounts: Readonly<Record<string, number>>;
|
|
46
|
-
};
|
|
47
|
-
readonly diagnostics: readonly TaskMetricsDiagnostic[];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
interface MutableTotals {
|
|
51
|
-
totalTaskRecords: number;
|
|
52
|
-
preflightRejectedCount: number;
|
|
53
|
-
totalTaskTextChars: number;
|
|
54
|
-
totalAssignmentChars: number;
|
|
55
|
-
totalInstructionChars: number;
|
|
56
|
-
totalResultSummaryChars: number;
|
|
57
|
-
totalSerializedResultPayloadChars: number;
|
|
58
|
-
verificationEntryCount: number;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export async function analyzeTaskStoreMetrics(tasksRoot: string): Promise<TaskStoreMetricsReport> {
|
|
62
|
-
const artifactRead = await readTaskStoreArtifacts(tasksRoot);
|
|
63
|
-
const diagnostics: TaskMetricsDiagnostic[] = [...artifactRead.diagnostics];
|
|
64
|
-
const taskPrompts: TaskMetricCount[] = [];
|
|
65
|
-
const results: TaskMetricCount[] = [];
|
|
66
|
-
const statusCounts: Record<string, number> = {};
|
|
67
|
-
const failedChecksByName: Record<string, number> = {};
|
|
68
|
-
const unavailableChecksByName: Record<string, number> = {};
|
|
69
|
-
const byPhaseId: Record<string, number> = {};
|
|
70
|
-
const byIssueId: Record<string, number> = {};
|
|
71
|
-
const byRootCause: Record<string, number> = {};
|
|
72
|
-
const commandCounts: Record<string, number> = {};
|
|
73
|
-
const verificationStatusCounts: Record<string, number> = {};
|
|
74
|
-
const totals: MutableTotals = {
|
|
75
|
-
totalTaskRecords: 0,
|
|
76
|
-
preflightRejectedCount: 0,
|
|
77
|
-
totalTaskTextChars: 0,
|
|
78
|
-
totalAssignmentChars: 0,
|
|
79
|
-
totalInstructionChars: 0,
|
|
80
|
-
totalResultSummaryChars: 0,
|
|
81
|
-
totalSerializedResultPayloadChars: 0,
|
|
82
|
-
verificationEntryCount: 0,
|
|
83
|
-
};
|
|
84
|
-
const analysis: TaskAnalysis = {
|
|
85
|
-
totals,
|
|
86
|
-
statusCounts,
|
|
87
|
-
failedChecksByName,
|
|
88
|
-
unavailableChecksByName,
|
|
89
|
-
byPhaseId,
|
|
90
|
-
byIssueId,
|
|
91
|
-
byRootCause,
|
|
92
|
-
commandCounts,
|
|
93
|
-
verificationStatusCounts,
|
|
94
|
-
taskPrompts,
|
|
95
|
-
results,
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
for (const record of artifactRead.records) {
|
|
99
|
-
analyzeTaskArtifact(record, analysis);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return buildReport(tasksRoot, totals, statusCounts, failedChecksByName, unavailableChecksByName, byPhaseId, byIssueId, byRootCause, commandCounts, verificationStatusCounts, taskPrompts, results, diagnostics);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
interface TaskAnalysis {
|
|
106
|
-
readonly totals: MutableTotals;
|
|
107
|
-
readonly statusCounts: Record<string, number>;
|
|
108
|
-
readonly failedChecksByName: Record<string, number>;
|
|
109
|
-
readonly unavailableChecksByName: Record<string, number>;
|
|
110
|
-
readonly byPhaseId: Record<string, number>;
|
|
111
|
-
readonly byIssueId: Record<string, number>;
|
|
112
|
-
readonly byRootCause: Record<string, number>;
|
|
113
|
-
readonly commandCounts: Record<string, number>;
|
|
114
|
-
readonly verificationStatusCounts: Record<string, number>;
|
|
115
|
-
readonly taskPrompts: TaskMetricCount[];
|
|
116
|
-
readonly results: TaskMetricCount[];
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function analyzeTaskArtifact(record: TaskStoreArtifactRecord, analysis: TaskAnalysis): void {
|
|
120
|
-
const task = record.task;
|
|
121
|
-
const taskId = typeof task.id === "string" && task.id.length > 0 ? task.id : record.taskDirectoryName;
|
|
122
|
-
analysis.totals.totalTaskRecords += 1;
|
|
123
|
-
|
|
124
|
-
const status = typeof task.status === "string" && task.status.length > 0 ? task.status : "unknown";
|
|
125
|
-
increment(analysis.statusCounts, status);
|
|
126
|
-
|
|
127
|
-
const taskText = typeof task.taskText === "string" ? task.taskText : "";
|
|
128
|
-
analysis.totals.totalTaskTextChars += taskText.length;
|
|
129
|
-
analysis.taskPrompts.push({ taskId, charCount: taskText.length });
|
|
130
|
-
|
|
131
|
-
analyzeMetadata(task.metadata, analysis);
|
|
132
|
-
analyzePreflight(task, status, analysis);
|
|
133
|
-
analyzeAssignment(record.assignment, analysis);
|
|
134
|
-
analyzeResult(record.result, taskId, analysis);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function analyzeMetadata(metadata: unknown, analysis: TaskAnalysis): void {
|
|
138
|
-
if (!isRecord(metadata)) {
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
incrementStringField(analysis.byPhaseId, metadata.phaseId);
|
|
142
|
-
incrementStringField(analysis.byIssueId, metadata.issueId);
|
|
143
|
-
incrementStringField(analysis.byRootCause, metadata.rootCause);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function analyzePreflight(task: Record<string, unknown>, status: string, analysis: TaskAnalysis): void {
|
|
147
|
-
const preflight = isRecord(task.preflight) ? task.preflight : undefined;
|
|
148
|
-
const error = isRecord(task.error) ? task.error : undefined;
|
|
149
|
-
if (status === "rejected" && (error?.code === "preflight_failed" || preflight?.ok === false)) {
|
|
150
|
-
analysis.totals.preflightRejectedCount += 1;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (!preflight || !Array.isArray(preflight.checks)) {
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
for (const check of preflight.checks) {
|
|
157
|
-
if (!isRecord(check) || typeof check.name !== "string") {
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
if (check.status === "failed") {
|
|
161
|
-
increment(analysis.failedChecksByName, check.name);
|
|
162
|
-
} else if (check.status === "unavailable") {
|
|
163
|
-
increment(analysis.unavailableChecksByName, check.name);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function analyzeAssignment(assignment: unknown | undefined, analysis: TaskAnalysis): void {
|
|
169
|
-
if (assignment === undefined) {
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
const serializedAssignment = JSON.stringify(assignment);
|
|
173
|
-
analysis.totals.totalAssignmentChars += typeof assignment === "string" ? assignment.length : serializedAssignment?.length ?? 0;
|
|
174
|
-
if (isRecord(assignment) && typeof assignment.instructions === "string") {
|
|
175
|
-
analysis.totals.totalInstructionChars += assignment.instructions.length;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function analyzeResult(
|
|
180
|
-
storedResult: Record<string, unknown> | undefined,
|
|
181
|
-
taskId: string,
|
|
182
|
-
analysis: TaskAnalysis,
|
|
183
|
-
): void {
|
|
184
|
-
if (!storedResult) {
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
const summaryChars = typeof storedResult.summary === "string" ? storedResult.summary.length : 0;
|
|
188
|
-
const payloadChars = "result" in storedResult ? (JSON.stringify(storedResult.result)?.length ?? 0) : 0;
|
|
189
|
-
analysis.totals.totalResultSummaryChars += summaryChars;
|
|
190
|
-
analysis.totals.totalSerializedResultPayloadChars += payloadChars;
|
|
191
|
-
analysis.results.push({ taskId, charCount: summaryChars + payloadChars });
|
|
192
|
-
|
|
193
|
-
if (!isRecord(storedResult.result) || !Array.isArray(storedResult.result.verification)) {
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
for (const entry of storedResult.result.verification) {
|
|
197
|
-
analysis.totals.verificationEntryCount += 1;
|
|
198
|
-
if (!isRecord(entry)) {
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
if (typeof entry.command === "string" && entry.command.length > 0) {
|
|
202
|
-
increment(analysis.commandCounts, entry.command);
|
|
203
|
-
}
|
|
204
|
-
if (typeof entry.status === "string" && entry.status.length > 0) {
|
|
205
|
-
increment(analysis.verificationStatusCounts, entry.status);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function buildReport(
|
|
211
|
-
tasksRoot: string,
|
|
212
|
-
totals: MutableTotals,
|
|
213
|
-
statusCounts: Record<string, number>,
|
|
214
|
-
failedChecksByName: Record<string, number>,
|
|
215
|
-
unavailableChecksByName: Record<string, number>,
|
|
216
|
-
byPhaseId: Record<string, number>,
|
|
217
|
-
byIssueId: Record<string, number>,
|
|
218
|
-
byRootCause: Record<string, number>,
|
|
219
|
-
commandCounts: Record<string, number>,
|
|
220
|
-
verificationStatusCounts: Record<string, number>,
|
|
221
|
-
taskPrompts: TaskMetricCount[],
|
|
222
|
-
results: TaskMetricCount[],
|
|
223
|
-
diagnostics: TaskMetricsDiagnostic[],
|
|
224
|
-
): TaskStoreMetricsReport {
|
|
225
|
-
const issueCounts = sortedCounts(byIssueId);
|
|
226
|
-
return {
|
|
227
|
-
schemaVersion: 1,
|
|
228
|
-
tasksRoot,
|
|
229
|
-
totalTaskRecords: totals.totalTaskRecords,
|
|
230
|
-
statusCounts: sortedCounts(statusCounts),
|
|
231
|
-
preflight: {
|
|
232
|
-
rejectedCount: totals.preflightRejectedCount,
|
|
233
|
-
failedChecksByName: sortedCounts(failedChecksByName),
|
|
234
|
-
unavailableChecksByName: sortedCounts(unavailableChecksByName),
|
|
235
|
-
},
|
|
236
|
-
characterSizes: {
|
|
237
|
-
totalTaskTextChars: totals.totalTaskTextChars,
|
|
238
|
-
totalAssignmentChars: totals.totalAssignmentChars,
|
|
239
|
-
totalInstructionChars: totals.totalInstructionChars,
|
|
240
|
-
totalResultSummaryChars: totals.totalResultSummaryChars,
|
|
241
|
-
totalSerializedResultPayloadChars: totals.totalSerializedResultPayloadChars,
|
|
242
|
-
},
|
|
243
|
-
largest: {
|
|
244
|
-
taskPrompts: largestFirst(taskPrompts),
|
|
245
|
-
results: largestFirst(results),
|
|
246
|
-
},
|
|
247
|
-
grouping: {
|
|
248
|
-
byPhaseId: sortedCounts(byPhaseId),
|
|
249
|
-
byIssueId: issueCounts,
|
|
250
|
-
byRootCause: sortedCounts(byRootCause),
|
|
251
|
-
loopsPerIssueId: { ...issueCounts },
|
|
252
|
-
},
|
|
253
|
-
verification: {
|
|
254
|
-
entryCount: totals.verificationEntryCount,
|
|
255
|
-
commandCounts: sortedCounts(commandCounts),
|
|
256
|
-
statusCounts: sortedCounts(verificationStatusCounts),
|
|
257
|
-
},
|
|
258
|
-
diagnostics,
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function largestFirst(values: readonly TaskMetricCount[]): readonly TaskMetricCount[] {
|
|
263
|
-
return [...values]
|
|
264
|
-
.sort((left, right) => right.charCount - left.charCount || compareStrings(left.taskId, right.taskId))
|
|
265
|
-
.slice(0, LARGEST_LIST_LIMIT);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function sortedCounts(counts: Readonly<Record<string, number>>): Record<string, number> {
|
|
269
|
-
return Object.fromEntries(Object.entries(counts).sort(([left], [right]) => compareStrings(left, right)));
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function compareStrings(left: string, right: string): number {
|
|
273
|
-
return left < right ? -1 : left > right ? 1 : 0;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function increment(counts: Record<string, number>, key: string): void {
|
|
277
|
-
counts[key] = (counts[key] ?? 0) + 1;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function incrementStringField(counts: Record<string, number>, value: unknown): void {
|
|
281
|
-
if (typeof value === "string" && value.length > 0) {
|
|
282
|
-
increment(counts, value);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
287
|
-
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
288
|
-
}
|
package/src/task-artifacts.ts
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
|
|
4
|
-
export interface TaskStoreDiagnostic {
|
|
5
|
-
readonly severity: "warning" | "error";
|
|
6
|
-
readonly code: string;
|
|
7
|
-
readonly path: string;
|
|
8
|
-
readonly message: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface TaskStoreArtifactRecord {
|
|
12
|
-
readonly taskDirectoryName: string;
|
|
13
|
-
readonly task: Record<string, unknown>;
|
|
14
|
-
readonly assignment: unknown | undefined;
|
|
15
|
-
readonly result: Record<string, unknown> | undefined;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface TaskStoreArtifactRead {
|
|
19
|
-
readonly records: readonly TaskStoreArtifactRecord[];
|
|
20
|
-
readonly diagnostics: readonly TaskStoreDiagnostic[];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
type JsonReadResult =
|
|
24
|
-
| { readonly ok: true; readonly value: unknown }
|
|
25
|
-
| { readonly ok: false; readonly missing: boolean; readonly message: string };
|
|
26
|
-
|
|
27
|
-
export async function readTaskStoreArtifacts(tasksRoot: string): Promise<TaskStoreArtifactRead> {
|
|
28
|
-
const diagnostics: TaskStoreDiagnostic[] = [];
|
|
29
|
-
const records: TaskStoreArtifactRecord[] = [];
|
|
30
|
-
let entries;
|
|
31
|
-
try {
|
|
32
|
-
entries = await readdir(tasksRoot, { withFileTypes: true });
|
|
33
|
-
} catch (error) {
|
|
34
|
-
diagnostics.push({
|
|
35
|
-
severity: "error",
|
|
36
|
-
code: "tasks_root_unreadable",
|
|
37
|
-
path: tasksRoot,
|
|
38
|
-
message: errorMessage(error),
|
|
39
|
-
});
|
|
40
|
-
return { records, diagnostics };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const directories = entries
|
|
44
|
-
.filter((entry) => entry.isDirectory())
|
|
45
|
-
.sort((left, right) => compareStrings(left.name, right.name));
|
|
46
|
-
for (const directory of directories) {
|
|
47
|
-
const taskDirectoryName = directory.name;
|
|
48
|
-
const taskRead = await readJson(join(tasksRoot, taskDirectoryName, "task.json"));
|
|
49
|
-
if (!taskRead.ok) {
|
|
50
|
-
diagnostics.push({
|
|
51
|
-
severity: taskRead.missing ? "warning" : "error",
|
|
52
|
-
code: taskRead.missing ? "missing_task_json" : "malformed_task_json",
|
|
53
|
-
path: join(taskDirectoryName, "task.json"),
|
|
54
|
-
message: taskRead.message,
|
|
55
|
-
});
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
if (!isRecord(taskRead.value)) {
|
|
59
|
-
diagnostics.push({
|
|
60
|
-
severity: "error",
|
|
61
|
-
code: "malformed_task_json",
|
|
62
|
-
path: join(taskDirectoryName, "task.json"),
|
|
63
|
-
message: "task.json must contain an object",
|
|
64
|
-
});
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const assignment = await readAssignment(tasksRoot, taskDirectoryName, diagnostics);
|
|
69
|
-
const result = await readResult(tasksRoot, taskDirectoryName, diagnostics);
|
|
70
|
-
records.push({ taskDirectoryName, task: taskRead.value, assignment, result });
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return { records, diagnostics };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async function readAssignment(
|
|
77
|
-
tasksRoot: string,
|
|
78
|
-
taskDirectoryName: string,
|
|
79
|
-
diagnostics: TaskStoreDiagnostic[],
|
|
80
|
-
): Promise<unknown | undefined> {
|
|
81
|
-
const relativePath = join(taskDirectoryName, "assignment.json");
|
|
82
|
-
const assignmentRead = await readJson(join(tasksRoot, relativePath));
|
|
83
|
-
if (assignmentRead.ok) {
|
|
84
|
-
return assignmentRead.value;
|
|
85
|
-
}
|
|
86
|
-
if (!assignmentRead.missing) {
|
|
87
|
-
diagnostics.push({
|
|
88
|
-
severity: "error",
|
|
89
|
-
code: "malformed_assignment_json",
|
|
90
|
-
path: relativePath,
|
|
91
|
-
message: assignmentRead.message,
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
return undefined;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async function readResult(
|
|
98
|
-
tasksRoot: string,
|
|
99
|
-
taskDirectoryName: string,
|
|
100
|
-
diagnostics: TaskStoreDiagnostic[],
|
|
101
|
-
): Promise<Record<string, unknown> | undefined> {
|
|
102
|
-
const relativePath = join(taskDirectoryName, "result.json");
|
|
103
|
-
const resultRead = await readJson(join(tasksRoot, relativePath));
|
|
104
|
-
if (!resultRead.ok) {
|
|
105
|
-
diagnostics.push({
|
|
106
|
-
severity: resultRead.missing ? "warning" : "error",
|
|
107
|
-
code: resultRead.missing ? "missing_result_json" : "malformed_result_json",
|
|
108
|
-
path: relativePath,
|
|
109
|
-
message: resultRead.message,
|
|
110
|
-
});
|
|
111
|
-
return undefined;
|
|
112
|
-
}
|
|
113
|
-
if (!isRecord(resultRead.value)) {
|
|
114
|
-
diagnostics.push({
|
|
115
|
-
severity: "error",
|
|
116
|
-
code: "malformed_result_json",
|
|
117
|
-
path: relativePath,
|
|
118
|
-
message: "result.json must contain an object",
|
|
119
|
-
});
|
|
120
|
-
return undefined;
|
|
121
|
-
}
|
|
122
|
-
return resultRead.value;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async function readJson(path: string): Promise<JsonReadResult> {
|
|
126
|
-
let text: string;
|
|
127
|
-
try {
|
|
128
|
-
text = await readFile(path, "utf8");
|
|
129
|
-
} catch (error) {
|
|
130
|
-
return { ok: false, missing: errorCode(error) === "ENOENT", message: errorMessage(error) };
|
|
131
|
-
}
|
|
132
|
-
try {
|
|
133
|
-
return { ok: true, value: JSON.parse(text) as unknown };
|
|
134
|
-
} catch (error) {
|
|
135
|
-
return { ok: false, missing: false, message: errorMessage(error) };
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function compareStrings(left: string, right: string): number {
|
|
140
|
-
return left < right ? -1 : left > right ? 1 : 0;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
144
|
-
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function errorCode(error: unknown): string | undefined {
|
|
148
|
-
return isRecord(error) && typeof error.code === "string" ? error.code : undefined;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function errorMessage(error: unknown): string {
|
|
152
|
-
return error instanceof Error ? error.message : String(error);
|
|
153
|
-
}
|