@dpeek/codeless 0.1.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/src/metrics.ts ADDED
@@ -0,0 +1,155 @@
1
+ import {
2
+ existsSync,
3
+ linkSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ readdirSync,
7
+ renameSync,
8
+ unlinkSync,
9
+ writeFileSync,
10
+ } from "node:fs";
11
+ import { dirname, join } from "node:path";
12
+
13
+ export type Metric = {
14
+ stream: string;
15
+ change: string;
16
+ dispatchedAt?: string;
17
+ landedAt?: string;
18
+ landedCommit?: string;
19
+ };
20
+
21
+ function metricPath(workspaceRoot: string, stream: string, change: string): string {
22
+ return join(workspaceRoot, "metrics", stream, `${change}.json`);
23
+ }
24
+
25
+ function readMetric(path: string): Metric {
26
+ const value: unknown = JSON.parse(readFileSync(path, "utf8"));
27
+ if (typeof value !== "object" || value === null || Array.isArray(value))
28
+ throw new Error(`${path} is not a metric record`);
29
+ const metric = value as Record<string, unknown>;
30
+ if (
31
+ typeof metric["stream"] !== "string" ||
32
+ typeof metric["change"] !== "string" ||
33
+ (metric["dispatchedAt"] !== undefined && typeof metric["dispatchedAt"] !== "string") ||
34
+ (metric["landedAt"] !== undefined && typeof metric["landedAt"] !== "string") ||
35
+ (metric["landedCommit"] !== undefined && typeof metric["landedCommit"] !== "string")
36
+ )
37
+ throw new Error(`${path} is not a metric record`);
38
+ return metric as Metric;
39
+ }
40
+
41
+ function writeMetric(path: string, metric: Metric): void {
42
+ const temporary = `${path}.${process.pid}.${crypto.randomUUID()}`;
43
+ try {
44
+ writeFileSync(temporary, `${JSON.stringify(metric)}\n`, { flag: "wx" });
45
+ renameSync(temporary, path);
46
+ } finally {
47
+ if (existsSync(temporary)) unlinkSync(temporary);
48
+ }
49
+ }
50
+
51
+ export function recordDispatch(workspaceRoot: string, stream: string, change: string): void {
52
+ const path = metricPath(workspaceRoot, stream, change);
53
+ if (existsSync(path)) {
54
+ const metric = readMetric(path);
55
+ if (metric.stream !== stream || metric.change !== change)
56
+ throw new Error(`${path} does not match ${stream} change ${change}`);
57
+ return;
58
+ }
59
+ mkdirSync(dirname(path), { recursive: true });
60
+ const temporary = `${path}.${process.pid}.${crypto.randomUUID()}`;
61
+ writeFileSync(
62
+ temporary,
63
+ `${JSON.stringify({ stream, change, dispatchedAt: new Date().toISOString() })}\n`,
64
+ { flag: "wx" },
65
+ );
66
+ try {
67
+ linkSync(temporary, path);
68
+ } catch (error) {
69
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
70
+ } finally {
71
+ unlinkSync(temporary);
72
+ }
73
+ readMetric(path);
74
+ }
75
+
76
+ export function recordLanding(
77
+ workspaceRoot: string,
78
+ stream: string,
79
+ change: string,
80
+ commit: string,
81
+ ): void {
82
+ const path = metricPath(workspaceRoot, stream, change);
83
+ if (!existsSync(path)) {
84
+ mkdirSync(dirname(path), { recursive: true });
85
+ writeMetric(path, { stream, change, landedAt: new Date().toISOString(), landedCommit: commit });
86
+ return;
87
+ }
88
+ const metric = readMetric(path);
89
+ if (metric.stream !== stream || metric.change !== change)
90
+ throw new Error(`${path} does not match ${stream} change ${change}`);
91
+ if (metric.landedAt !== undefined) return;
92
+ writeMetric(path, { ...metric, landedAt: new Date().toISOString(), landedCommit: commit });
93
+ }
94
+
95
+ function formatElapsed(milliseconds: number): string {
96
+ const seconds = Math.floor(milliseconds / 1000);
97
+ const hours = Math.floor(seconds / 3600);
98
+ const minutes = Math.floor((seconds % 3600) / 60);
99
+ return hours > 0
100
+ ? `${hours}h ${String(minutes).padStart(2, "0")}m`
101
+ : `${minutes}m ${String(seconds % 60).padStart(2, "0")}s`;
102
+ }
103
+
104
+ export function metricReport(workspaceRoot: string): string[] {
105
+ const root = join(workspaceRoot, "metrics");
106
+ const byStream = new Map<string, Metric[]>();
107
+ if (existsSync(root)) {
108
+ for (const stream of readdirSync(root).sort()) {
109
+ const directory = join(root, stream);
110
+ if (!/^[a-z][a-z0-9-]{0,23}$/.test(stream) || !existsSync(directory)) continue;
111
+ const records = readdirSync(directory)
112
+ .filter((file) => /^\d{3}\.json$/.test(file))
113
+ .sort()
114
+ .map((file) => readMetric(join(directory, file)));
115
+ if (records.length > 0) byStream.set(stream, records);
116
+ }
117
+ }
118
+ const summary = (records: Metric[]) => {
119
+ const landed = records.filter((record) => record.landedAt !== undefined);
120
+ const elapsed = landed
121
+ .map((record) => {
122
+ const start = record.dispatchedAt === undefined ? NaN : Date.parse(record.dispatchedAt);
123
+ const end = Date.parse(record.landedAt!);
124
+ return Number.isFinite(start) && Number.isFinite(end) && end >= start
125
+ ? end - start
126
+ : undefined;
127
+ })
128
+ .filter((value): value is number => value !== undefined);
129
+ const unavailable = landed.length - elapsed.length;
130
+ return {
131
+ landed: landed.length,
132
+ unlanded: records.length - landed.length,
133
+ coverage: `${elapsed.length} measured, ${unavailable} unavailable`,
134
+ total:
135
+ elapsed.length === 0 ? "unavailable" : formatElapsed(elapsed.reduce((a, b) => a + b, 0)),
136
+ average:
137
+ elapsed.length === 0
138
+ ? "unavailable"
139
+ : formatElapsed(elapsed.reduce((a, b) => a + b, 0) / elapsed.length),
140
+ };
141
+ };
142
+ const row = (name: string, records: Metric[]) => {
143
+ const value = summary(records);
144
+ return `${name}\t${value.landed}\t${value.unlanded}\t${value.coverage}\t${value.total}\t${value.average}`;
145
+ };
146
+ const rows = [...byStream.entries()].map(([stream, records]) => ({ stream, records }));
147
+ return [
148
+ "Stream\tLanded\tNot landed\tElapsed coverage\tDispatch-to-land wall clock total\tAverage",
149
+ ...rows.map(({ stream, records }) => row(stream, records)),
150
+ row(
151
+ "Project total",
152
+ rows.flatMap(({ records }) => records),
153
+ ),
154
+ ];
155
+ }
package/src/pi.ts ADDED
@@ -0,0 +1,174 @@
1
+ import type { RoleSelection } from "./project.ts";
2
+
3
+ type Role = "planner" | "implementer";
4
+ type JsonObject = Record<string, unknown>;
5
+
6
+ const validationTimeoutMs = 30_000;
7
+
8
+ function object(value: unknown, label: string): JsonObject {
9
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
10
+ throw new Error(`Pi RPC response omitted ${label}`);
11
+ }
12
+ return value as JsonObject;
13
+ }
14
+
15
+ function strings(value: unknown, label: string): string[] {
16
+ if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) {
17
+ throw new Error(`Pi RPC response omitted ${label}`);
18
+ }
19
+ return value;
20
+ }
21
+
22
+ export function roleSelectionReference(selection: RoleSelection): string {
23
+ return `${selection.provider}/${selection.model}`;
24
+ }
25
+
26
+ export function roleSelectionSummary(role: Role, selection: RoleSelection): string {
27
+ const label = role === "planner" ? "Planner" : "Implementer";
28
+ return `${label}: ${roleSelectionReference(selection)} (thinking: ${selection.thinking})`;
29
+ }
30
+
31
+ export function roleSelectionArguments(selection: RoleSelection): string[] {
32
+ return ["--model", roleSelectionReference(selection), "--thinking", selection.thinking];
33
+ }
34
+
35
+ export async function validateRoleSelection(
36
+ role: Role,
37
+ selection: RoleSelection,
38
+ worktree: string,
39
+ extension?: string,
40
+ ): Promise<void> {
41
+ const reference = roleSelectionReference(selection);
42
+ const label = role === "planner" ? "Planner" : "Implementer";
43
+ const child = Bun.spawn(
44
+ [
45
+ "pi",
46
+ "--mode",
47
+ "rpc",
48
+ "--no-session",
49
+ "--approve",
50
+ ...(extension === undefined ? [] : ["--extension", extension]),
51
+ ],
52
+ {
53
+ cwd: worktree,
54
+ env: process.env,
55
+ stdin: "pipe",
56
+ stdout: "pipe",
57
+ stderr: "pipe",
58
+ },
59
+ );
60
+ const stderr = new Response(child.stderr).text();
61
+ const reader = child.stdout.getReader();
62
+ const decoder = new TextDecoder();
63
+ let buffer = "";
64
+ let requestNumber = 0;
65
+ const timeout = setTimeout(() => child.kill(), validationTimeoutMs);
66
+
67
+ async function line(): Promise<string> {
68
+ while (true) {
69
+ const newline = buffer.indexOf("\n");
70
+ if (newline >= 0) {
71
+ const value = buffer.slice(0, newline).replace(/\r$/, "");
72
+ buffer = buffer.slice(newline + 1);
73
+ return value;
74
+ }
75
+ const chunk = await reader.read();
76
+ if (chunk.done) {
77
+ buffer += decoder.decode();
78
+ if (buffer.length > 0) {
79
+ const value = buffer.replace(/\r$/, "");
80
+ buffer = "";
81
+ return value;
82
+ }
83
+ const diagnostic = (await stderr).trim();
84
+ throw new Error(
85
+ diagnostic || `Pi RPC exited with code ${await child.exited} before responding`,
86
+ );
87
+ }
88
+ buffer += decoder.decode(chunk.value, { stream: true });
89
+ }
90
+ }
91
+
92
+ async function request(type: string, fields: JsonObject = {}): Promise<unknown> {
93
+ const id = `streams-${++requestNumber}`;
94
+ await child.stdin.write(`${JSON.stringify({ id, type, ...fields })}\n`);
95
+ await child.stdin.flush();
96
+ while (true) {
97
+ const output = await line();
98
+ let response: JsonObject;
99
+ try {
100
+ response = object(JSON.parse(output), "object");
101
+ } catch (error) {
102
+ throw new Error(
103
+ `Pi RPC returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
104
+ );
105
+ }
106
+ if (response["type"] !== "response" || response["id"] !== id) continue;
107
+ if (response["success"] !== true) {
108
+ throw new Error(
109
+ typeof response["error"] === "string"
110
+ ? response["error"]
111
+ : `Pi RPC ${type} request failed`,
112
+ );
113
+ }
114
+ return response["data"];
115
+ }
116
+ }
117
+
118
+ try {
119
+ const available = object(await request("get_available_models"), "data")["models"];
120
+ if (!Array.isArray(available)) throw new Error("Pi RPC response omitted data.models");
121
+ const model = available
122
+ .map((value, index) => object(value, `data.models[${index}]`))
123
+ .find((value) => value["provider"] === selection.provider && value["id"] === selection.model);
124
+ if (model === undefined) {
125
+ throw new Error(
126
+ `${reference} is not available; check the exact provider/model and Pi authentication`,
127
+ );
128
+ }
129
+
130
+ await request("set_model", { provider: selection.provider, modelId: selection.model });
131
+ const levels = strings(
132
+ object(await request("get_available_thinking_levels"), "data")["levels"],
133
+ "data.levels",
134
+ );
135
+ if (!levels.includes(selection.thinking)) {
136
+ throw new Error(
137
+ `${reference} does not support thinking level ${selection.thinking}; supported levels: ${levels.join(", ")}`,
138
+ );
139
+ }
140
+
141
+ await request("set_thinking_level", { level: selection.thinking });
142
+ if (extension !== undefined) {
143
+ const commands = object(await request("get_commands"), "data")["commands"];
144
+ if (
145
+ !Array.isArray(commands) ||
146
+ !commands.some((command) => object(command, "command")["name"] === "streams-activate")
147
+ ) {
148
+ throw new Error("the Codeless planner extension did not register streams-activate");
149
+ }
150
+ }
151
+
152
+ const state = object(await request("get_state"), "data");
153
+ const effectiveModel = object(state["model"], "data.model");
154
+ if (
155
+ effectiveModel["provider"] !== selection.provider ||
156
+ effectiveModel["id"] !== selection.model ||
157
+ state["thinkingLevel"] !== selection.thinking
158
+ ) {
159
+ const effectiveReference = `${String(effectiveModel["provider"])}/${String(effectiveModel["id"])}`;
160
+ throw new Error(
161
+ `Pi applied ${effectiveReference} at ${String(state["thinkingLevel"])} instead of the requested selection`,
162
+ );
163
+ }
164
+ } catch (error) {
165
+ throw new Error(
166
+ `${label} requested ${reference} at thinking level ${selection.thinking}, but validation failed: ${error instanceof Error ? error.message : String(error)}`,
167
+ );
168
+ } finally {
169
+ clearTimeout(timeout);
170
+ await child.stdin.end();
171
+ await child.exited;
172
+ reader.releaseLock();
173
+ }
174
+ }
package/src/project.ts ADDED
@@ -0,0 +1,55 @@
1
+ import { Schema } from "effect";
2
+ import { readFileSync } from "node:fs";
3
+ import { isAbsolute, join, relative, resolve } from "node:path";
4
+
5
+ export const ThinkingLevel = Schema.Literals([
6
+ "off",
7
+ "minimal",
8
+ "low",
9
+ "medium",
10
+ "high",
11
+ "xhigh",
12
+ "max",
13
+ ]);
14
+
15
+ export const RoleSelection = Schema.Struct({
16
+ provider: Schema.NonEmptyString,
17
+ model: Schema.NonEmptyString,
18
+ thinking: ThinkingLevel,
19
+ });
20
+
21
+ export type RoleSelection = typeof RoleSelection.Type;
22
+
23
+ const Project = Schema.Struct({
24
+ integrationBranch: Schema.NonEmptyString,
25
+ directions: Schema.NonEmptyString,
26
+ prompts: Schema.NonEmptyString,
27
+ install: Schema.NonEmptyArray(Schema.String),
28
+ check: Schema.NonEmptyArray(Schema.String),
29
+ planner: RoleSelection,
30
+ implementer: RoleSelection,
31
+ });
32
+
33
+ export function readProject(worktree: string) {
34
+ const path = join(worktree, ".codeless/config.json");
35
+ try {
36
+ const project = Schema.decodeUnknownSync(Project, { onExcessProperty: "error" })(
37
+ JSON.parse(readFileSync(path, "utf8")),
38
+ );
39
+ for (const field of ["directions", "prompts"] as const) {
40
+ const value = project[field];
41
+ const local = relative(worktree, resolve(worktree, value));
42
+ if (isAbsolute(value) || local === ".." || local.startsWith("../")) {
43
+ throw new Error(`${field} must be a path inside the project`);
44
+ }
45
+ }
46
+ for (const field of ["install", "check"] as const) {
47
+ if (!project[field][0].trim()) throw new Error(`${field} needs an executable`);
48
+ }
49
+ return project;
50
+ } catch (error) {
51
+ throw new Error(
52
+ `Invalid Codeless project configuration at ${path}: ${error instanceof Error ? error.message : String(error)}`,
53
+ );
54
+ }
55
+ }