@dpeek/codeless 0.1.1 → 0.1.2
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 +22 -8
- package/extension/implementer-reporting.js +78 -10
- package/extension/planner.js +94 -0
- package/package.json +1 -1
- package/spec/workflow.md +33 -19
- package/src/attempt.ts +4 -1
- package/src/cli.ts +124 -6
- package/src/metrics.ts +90 -4
package/README.md
CHANGED
|
@@ -97,7 +97,7 @@ the runner does not generate or copy project instructions.
|
|
|
97
97
|
The package-owned planner extension activates every planner session. Before its
|
|
98
98
|
first project prompt, it requires the exact `<slug>-planner` Pi name, establishes
|
|
99
99
|
and verifies the `<slug>_planner` Herdr identity, and confirms `approve_stream_change`,
|
|
100
|
-
`dispatch_stream_implementer`, and `next_stream_change` are active. Missing or
|
|
100
|
+
`dispatch_stream_implementer`, `rework_stream_implementer`, `finish_stream_implementer`, and `next_stream_change` are active. Missing or
|
|
101
101
|
incompatible activation, identity mismatch, or inactive tools stops before
|
|
102
102
|
`/change`; global Pi extension installation is unnecessary.
|
|
103
103
|
The approval tool has no arguments. Its extension derives the active
|
|
@@ -182,6 +182,8 @@ codeless open <slug>
|
|
|
182
182
|
codeless planner <slug>
|
|
183
183
|
codeless approve <planner-session>
|
|
184
184
|
codeless dispatch <numbered-change-file>
|
|
185
|
+
codeless rework <numbered-change-file> <feedback>
|
|
186
|
+
codeless finish <numbered-change-file>
|
|
185
187
|
codeless land <slug>
|
|
186
188
|
codeless next <numbered-change-file> <landed-commit>
|
|
187
189
|
codeless metrics
|
|
@@ -199,9 +201,15 @@ starts Pi in an existing stream's lone shell after the same role preflight. Its
|
|
|
199
201
|
activation establishes the same identity as creation and reopening.
|
|
200
202
|
Dispatch validates the implementer selection before touching the planner's
|
|
201
203
|
right-hand pane, starts a fresh ephemeral implementer with Codeless's reporting
|
|
202
|
-
extension and its explicit Pi extension flag, and waits for completion.
|
|
203
|
-
|
|
204
|
-
|
|
204
|
+
extension and its explicit Pi extension flag, and waits for completion. Rework verifies
|
|
205
|
+
that same change's idle implementer and worktree, invokes one package-owned Pi command
|
|
206
|
+
that verifies its immutable stream/change scope, arms reporting, and submits one feedback
|
|
207
|
+
turn with the same one-hour limit, records a `rework`
|
|
208
|
+
attempt, and returns it before review is queued again. Finish verifies that identity,
|
|
209
|
+
gracefully exits it, and waits for the right pane's stream-worktree shell. Neither operation
|
|
210
|
+
replaces the agent, changes its selection, retries, or continues after a mismatch, timeout,
|
|
211
|
+
blocked agent, or ambiguous pane. Its JSON result is a normalized attempt report, which the
|
|
212
|
+
planner tool exposes before queueing review. It includes the actual settled model/thinking selection,
|
|
205
213
|
terminal text and outcome, full-session Pi usage and available Pi cost estimate,
|
|
206
214
|
timestamps, and tool/error counts; prompts, source, thinking, credentials, and
|
|
207
215
|
transcripts are not retained.
|
|
@@ -216,11 +224,17 @@ integration fast-forward, Codeless records the landed time and commit on that
|
|
|
216
224
|
change's canonical record, creating a landed record without elapsed time when
|
|
217
225
|
dispatch collection was unavailable; collection warnings never alter dispatch or
|
|
218
226
|
landing.
|
|
219
|
-
`codeless metrics` prints every recorded stream and a project total. Its
|
|
220
|
-
|
|
227
|
+
`codeless metrics` prints every recorded stream and a project total. Its first
|
|
228
|
+
table reports dispatch-to-land wall-clock time; among landed changes, records
|
|
221
229
|
without a measured duration are explicitly unavailable. Dispatched-but-unlanded
|
|
222
|
-
changes remain a separate count.
|
|
223
|
-
|
|
230
|
+
changes remain a separate count. Its second table reports implementer attempts:
|
|
231
|
+
distinct changes with rework, initial and rework turns, incomplete collection,
|
|
232
|
+
exact stored terminal outcomes, and tool errors. Usage and cost coverage count
|
|
233
|
+
measured attempts separately from unavailable collection; token totals include
|
|
234
|
+
only measured usage, and cost totals remain grouped by recorded currency without
|
|
235
|
+
conversion. These counts do not establish implementation quality or review
|
|
236
|
+
success. Metrics are prospective local observations, not journal state or a
|
|
237
|
+
recovery mechanism.
|
|
224
238
|
|
|
225
239
|
Landing requires clean stream and integration worktrees and exactly one stream
|
|
226
240
|
commit outside their merge base. It acquires `.land-lock` atomically, recording
|
|
@@ -74,23 +74,89 @@ function write(path, value) {
|
|
|
74
74
|
export default function implementerReportingExtension(pi) {
|
|
75
75
|
pi.registerFlag("codeless-attempt", { type: "string" });
|
|
76
76
|
let configuration;
|
|
77
|
+
let startupScope;
|
|
78
|
+
let rearm;
|
|
77
79
|
let startedAt;
|
|
80
|
+
let entryOffset = 0;
|
|
78
81
|
let toolCalls = 0;
|
|
79
82
|
let errorCount = 0;
|
|
83
|
+
|
|
84
|
+
function configure(value, offset = 0) {
|
|
85
|
+
if (
|
|
86
|
+
typeof value?.path !== "string" ||
|
|
87
|
+
typeof value?.id !== "string" ||
|
|
88
|
+
typeof value?.stream !== "string" ||
|
|
89
|
+
typeof value?.change !== "string" ||
|
|
90
|
+
!["initial", "rework"].includes(value?.kind)
|
|
91
|
+
)
|
|
92
|
+
throw new Error("Codeless attempt configuration is invalid");
|
|
93
|
+
return { value, offset };
|
|
94
|
+
}
|
|
80
95
|
pi.on("session_start", () => {
|
|
81
96
|
try {
|
|
82
97
|
const value = JSON.parse(pi.getFlag("codeless-attempt") ?? "");
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
typeof value?.id === "string" &&
|
|
86
|
-
typeof value?.stream === "string" &&
|
|
87
|
-
typeof value?.change === "string"
|
|
88
|
-
)
|
|
89
|
-
configuration = value;
|
|
98
|
+
configuration = configure(value).value;
|
|
99
|
+
startupScope = { stream: configuration.stream, change: configuration.change };
|
|
90
100
|
} catch {}
|
|
91
101
|
});
|
|
102
|
+
function requireScope(value) {
|
|
103
|
+
if (
|
|
104
|
+
startupScope === undefined ||
|
|
105
|
+
value?.stream !== startupScope.stream ||
|
|
106
|
+
value?.change !== startupScope.change
|
|
107
|
+
)
|
|
108
|
+
throw new Error("Codeless stream/change scope does not match this implementer session");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
pi.registerCommand("codeless-rework", {
|
|
112
|
+
description: "Submit one scope-verified Codeless implementer rework turn",
|
|
113
|
+
handler: async (args, ctx) => {
|
|
114
|
+
let value;
|
|
115
|
+
try {
|
|
116
|
+
value = JSON.parse(args);
|
|
117
|
+
} catch {
|
|
118
|
+
throw new Error("Codeless rework requires one JSON-quoted request");
|
|
119
|
+
}
|
|
120
|
+
requireScope(value);
|
|
121
|
+
if (typeof value?.feedback !== "string" || value.feedback.trim().length === 0)
|
|
122
|
+
throw new Error("Codeless rework requires concise non-empty feedback");
|
|
123
|
+
const next = configure(value, ctx.sessionManager.getEntries().length);
|
|
124
|
+
if (next.value.kind !== "rework") throw new Error("Codeless rework attempt must be rework");
|
|
125
|
+
configuration = undefined;
|
|
126
|
+
rearm = next;
|
|
127
|
+
try {
|
|
128
|
+
pi.sendUserMessage(`Review feedback: ${value.feedback.trim()}`, { deliverAs: "followUp" });
|
|
129
|
+
} catch (error) {
|
|
130
|
+
rearm = undefined;
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
pi.registerCommand("codeless-finish", {
|
|
137
|
+
description: "Gracefully finish a scope-verified Codeless implementer",
|
|
138
|
+
handler: async (args, ctx) => {
|
|
139
|
+
let value;
|
|
140
|
+
try {
|
|
141
|
+
value = JSON.parse(args);
|
|
142
|
+
} catch {
|
|
143
|
+
throw new Error("Codeless finish requires one JSON-quoted scope");
|
|
144
|
+
}
|
|
145
|
+
requireScope(value);
|
|
146
|
+
await ctx.shutdown();
|
|
147
|
+
},
|
|
148
|
+
});
|
|
92
149
|
pi.on("agent_start", () => {
|
|
93
|
-
|
|
150
|
+
if (rearm !== undefined) {
|
|
151
|
+
configuration = rearm.value;
|
|
152
|
+
entryOffset = rearm.offset;
|
|
153
|
+
rearm = undefined;
|
|
154
|
+
startedAt = new Date().toISOString();
|
|
155
|
+
toolCalls = 0;
|
|
156
|
+
errorCount = 0;
|
|
157
|
+
} else {
|
|
158
|
+
startedAt ??= new Date().toISOString();
|
|
159
|
+
}
|
|
94
160
|
});
|
|
95
161
|
pi.on("tool_execution_end", (event) => {
|
|
96
162
|
toolCalls += 1;
|
|
@@ -100,9 +166,10 @@ export default function implementerReportingExtension(pi) {
|
|
|
100
166
|
if (configuration === undefined) return;
|
|
101
167
|
const report = configuration;
|
|
102
168
|
configuration = undefined;
|
|
103
|
-
const
|
|
169
|
+
const entries = ctx.sessionManager.getEntries().slice(entryOffset);
|
|
170
|
+
const settledMessages = messages(entries);
|
|
104
171
|
const final = [...settledMessages].reverse().find((message) => message.role === "assistant");
|
|
105
|
-
const totals = sessionUsage(
|
|
172
|
+
const totals = sessionUsage(entries);
|
|
106
173
|
const model = final?.responseModel ?? final?.model ?? ctx.model?.id;
|
|
107
174
|
const provider = final?.provider ?? ctx.model?.provider;
|
|
108
175
|
const selection =
|
|
@@ -123,6 +190,7 @@ export default function implementerReportingExtension(pi) {
|
|
|
123
190
|
stream: report.stream,
|
|
124
191
|
change: report.change,
|
|
125
192
|
role: "implementer",
|
|
193
|
+
kind: report.kind,
|
|
126
194
|
startedAt: startedAt ?? new Date().toISOString(),
|
|
127
195
|
endedAt: new Date().toISOString(),
|
|
128
196
|
...(selection === undefined ? {} : { selection }),
|
package/extension/planner.js
CHANGED
|
@@ -6,6 +6,8 @@ const thinkingLevels = new Set(["off", "minimal", "low", "medium", "high", "xhig
|
|
|
6
6
|
const requiredTools = [
|
|
7
7
|
"approve_stream_change",
|
|
8
8
|
"dispatch_stream_implementer",
|
|
9
|
+
"rework_stream_implementer",
|
|
10
|
+
"finish_stream_implementer",
|
|
9
11
|
"next_stream_change",
|
|
10
12
|
];
|
|
11
13
|
|
|
@@ -290,6 +292,98 @@ export default function plannerExtension(pi) {
|
|
|
290
292
|
},
|
|
291
293
|
});
|
|
292
294
|
|
|
295
|
+
pi.registerTool({
|
|
296
|
+
name: "rework_stream_implementer",
|
|
297
|
+
label: "Remediate stream implementation",
|
|
298
|
+
description:
|
|
299
|
+
"Reuse the idle implementer for this approved change, submit one actionable feedback turn, collect its rework attempt, and queue review again.",
|
|
300
|
+
promptSnippet: "Send one concise remediation request to the existing stream implementer",
|
|
301
|
+
promptGuidelines: [
|
|
302
|
+
"Call rework_stream_implementer only when review finds actionable defects in the approved change. Use its approved changePath and concise feedback; never reproduce Herdr commands.",
|
|
303
|
+
"The tool queues review only after a settled rework attempt. Stop on any error and do not retry automatically.",
|
|
304
|
+
],
|
|
305
|
+
parameters: {
|
|
306
|
+
type: "object",
|
|
307
|
+
properties: {
|
|
308
|
+
changePath: {
|
|
309
|
+
type: "string",
|
|
310
|
+
description: "Absolute path to the approved changes/NNN.md file",
|
|
311
|
+
},
|
|
312
|
+
feedback: { type: "string", description: "Concise actionable review feedback" },
|
|
313
|
+
},
|
|
314
|
+
required: ["changePath", "feedback"],
|
|
315
|
+
additionalProperties: false,
|
|
316
|
+
},
|
|
317
|
+
async execute(_toolCallId, params, signal) {
|
|
318
|
+
const changePath = params.changePath.replace(/^@/, "");
|
|
319
|
+
const execution = await pi.exec("bun", [codeless, "rework", changePath, params.feedback], {
|
|
320
|
+
signal,
|
|
321
|
+
timeout: 3_700_000,
|
|
322
|
+
});
|
|
323
|
+
const output = [execution.stdout.trim(), execution.stderr.trim()]
|
|
324
|
+
.filter(Boolean)
|
|
325
|
+
.join("\n");
|
|
326
|
+
if (execution.code !== 0)
|
|
327
|
+
throw new Error(output || `codeless rework failed with exit code ${execution.code}`);
|
|
328
|
+
let attempt;
|
|
329
|
+
try {
|
|
330
|
+
attempt = JSON.parse(execution.stdout);
|
|
331
|
+
} catch {
|
|
332
|
+
throw new Error("Codeless returned an invalid rework attempt");
|
|
333
|
+
}
|
|
334
|
+
if (!validAttempt(attempt, attempt?.stream, attempt?.change) || attempt.kind !== "rework")
|
|
335
|
+
throw new Error("Codeless returned an invalid rework attempt");
|
|
336
|
+
pi.sendUserMessage(`/review ${JSON.stringify(changePath)}`, {
|
|
337
|
+
deliverAs: "steer",
|
|
338
|
+
expandPromptTemplates: true,
|
|
339
|
+
});
|
|
340
|
+
return {
|
|
341
|
+
content: [{ type: "text", text: attempt.text || "Implementer rework settled." }],
|
|
342
|
+
details: { changePath, attempt },
|
|
343
|
+
};
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
pi.registerTool({
|
|
348
|
+
name: "finish_stream_implementer",
|
|
349
|
+
label: "Finish stream implementer",
|
|
350
|
+
description:
|
|
351
|
+
"Gracefully exit the verified idle implementer after review approval and confirm its right-hand pane returned to the stream shell.",
|
|
352
|
+
promptSnippet: "Finish the approved stream implementer before commit and landing",
|
|
353
|
+
promptGuidelines: [
|
|
354
|
+
"Call finish_stream_implementer exactly once after recording review approval and before following commit-and-land instructions.",
|
|
355
|
+
"Stop on failure; do not use Herdr commands or continue to commit and land.",
|
|
356
|
+
],
|
|
357
|
+
parameters: {
|
|
358
|
+
type: "object",
|
|
359
|
+
properties: {
|
|
360
|
+
changePath: {
|
|
361
|
+
type: "string",
|
|
362
|
+
description: "Absolute path to the approved changes/NNN.md file",
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
required: ["changePath"],
|
|
366
|
+
additionalProperties: false,
|
|
367
|
+
},
|
|
368
|
+
async execute(_toolCallId, params, signal) {
|
|
369
|
+
const changePath = params.changePath.replace(/^@/, "");
|
|
370
|
+
const execution = await pi.exec("bun", [codeless, "finish", changePath], {
|
|
371
|
+
signal,
|
|
372
|
+
timeout: 35_000,
|
|
373
|
+
});
|
|
374
|
+
const output = [execution.stdout.trim(), execution.stderr.trim()]
|
|
375
|
+
.filter(Boolean)
|
|
376
|
+
.join("\n");
|
|
377
|
+
if (execution.code !== 0)
|
|
378
|
+
throw new Error(output || `codeless finish failed with exit code ${execution.code}`);
|
|
379
|
+
return {
|
|
380
|
+
content: [
|
|
381
|
+
{ type: "text", text: "Implementer exited and its pane returned to the stream shell." },
|
|
382
|
+
],
|
|
383
|
+
};
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
|
|
293
387
|
pi.registerTool({
|
|
294
388
|
name: "dispatch_stream_implementer",
|
|
295
389
|
label: "Dispatch stream implementer",
|
package/package.json
CHANGED
package/spec/workflow.md
CHANGED
|
@@ -103,8 +103,8 @@ post-landing replacement—uses the package-owned extension as its activation
|
|
|
103
103
|
boundary. Before its first project prompt, activation requires the exact
|
|
104
104
|
`<slug>-planner` Pi session name, establishes and verifies Herdr reports
|
|
105
105
|
`<slug-with-hyphens-replaced>_planner`, and verifies
|
|
106
|
-
`approve_stream_change`, `dispatch_stream_implementer`,
|
|
107
|
-
`next_stream_change` are active. Missing or incompatible activation, identity
|
|
106
|
+
`approve_stream_change`, `dispatch_stream_implementer`, `rework_stream_implementer`,
|
|
107
|
+
`finish_stream_implementer`, and `next_stream_change` are active. Missing or incompatible activation, identity
|
|
108
108
|
mismatch, or an incomplete tool set stops visibly before `/change`. A direct
|
|
109
109
|
restart may begin with Herdr's `pi` fallback identity; activation renames and
|
|
110
110
|
rereads only that fallback. Any other identity mismatch stops. Implementers use
|
|
@@ -139,13 +139,22 @@ repeating a settled implementation.
|
|
|
139
139
|
|
|
140
140
|
The planner inspects the full diff and relevant code, checks the approved
|
|
141
141
|
acceptance criteria, and runs focused checks when the implementation output is
|
|
142
|
-
insufficient.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
142
|
+
insufficient. The planner-only `rework_stream_implementer` tool accepts that
|
|
143
|
+
approved path and concise feedback, verifies the expected idle implementer, its
|
|
144
|
+
right-hand pane and worktree, then invokes one package-owned Pi command. That command
|
|
145
|
+
verifies the immutable startup stream/change scope, arms package reporting in the existing
|
|
146
|
+
conversation, and submits one bounded feedback turn. It records
|
|
147
|
+
and returns one `rework` attempt before queueing review again; missing or malformed
|
|
148
|
+
reports warn and yield an incomplete attempt after settlement. Prompt rejection,
|
|
149
|
+
timeout, blocked state, identity/worktree/change mismatch, or ambiguous pane stops
|
|
150
|
+
without a completed attempt or queued review. The separate planner-only
|
|
151
|
+
`finish_stream_implementer` tool first verifies that immutable stream/change scope in the
|
|
152
|
+
same idle implementer, then gracefully exits it and waits for its pane to become the
|
|
153
|
+
stream-worktree shell. Its failure
|
|
154
|
+
stops before commit or landing instructions continue.
|
|
155
|
+
|
|
156
|
+
Dispatch, remediation, and shutdown do not retry automatically or replace an
|
|
157
|
+
implementer session or its selected model.
|
|
149
158
|
|
|
150
159
|
## Commit and landing
|
|
151
160
|
|
|
@@ -194,16 +203,21 @@ unchanged. Successful landing adds its timestamp and commit, or creates a landed
|
|
|
194
203
|
record with unavailable elapsed time when dispatch collection was unavailable.
|
|
195
204
|
Collection warnings do not change dispatch or landing outcomes.
|
|
196
205
|
|
|
197
|
-
`codeless metrics` reports every recorded stream and a project total
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
206
|
+
`codeless metrics` reports every recorded stream and a project total in two
|
|
207
|
+
tables. The elapsed table reports landed and dispatched-but-unlanded change
|
|
208
|
+
counts, measured versus unavailable elapsed coverage, and total and average
|
|
209
|
+
dispatch-to-land wall-clock time. The attempt table aggregates only validated
|
|
210
|
+
canonical attempt records and reports distinct changes with rework, initial and
|
|
211
|
+
rework turns, incomplete collection, exact stored terminal-outcome labels, and
|
|
212
|
+
summed tool errors.
|
|
213
|
+
|
|
214
|
+
Usage coverage is measured versus unavailable attempts; input, output,
|
|
215
|
+
cache-read, and cache-write totals include only attempts with recorded usage.
|
|
216
|
+
Cost coverage follows the same rule, and totals are grouped by recorded currency
|
|
217
|
+
without conversion. Missing usage or cost is unavailable, never zero. These
|
|
218
|
+
measurements do not establish implementation quality or review success. They
|
|
219
|
+
are prospective local observations, not journal state, an approval source, or a
|
|
220
|
+
recovery mechanism.
|
|
207
221
|
|
|
208
222
|
## Limits
|
|
209
223
|
|
package/src/attempt.ts
CHANGED
|
@@ -3,6 +3,7 @@ export type Attempt = {
|
|
|
3
3
|
stream: string;
|
|
4
4
|
change: string;
|
|
5
5
|
role: "implementer";
|
|
6
|
+
kind: "initial" | "rework";
|
|
6
7
|
startedAt: string;
|
|
7
8
|
endedAt: string;
|
|
8
9
|
selection?: { provider: string; model: string; thinking: string };
|
|
@@ -23,6 +24,7 @@ export function validAttempt(value: unknown, stream: string, change: string): va
|
|
|
23
24
|
"stream",
|
|
24
25
|
"change",
|
|
25
26
|
"role",
|
|
27
|
+
"kind",
|
|
26
28
|
"startedAt",
|
|
27
29
|
"endedAt",
|
|
28
30
|
"selection",
|
|
@@ -34,7 +36,7 @@ export function validAttempt(value: unknown, stream: string, change: string): va
|
|
|
34
36
|
"errorCount",
|
|
35
37
|
"incomplete",
|
|
36
38
|
]);
|
|
37
|
-
const strings = ["id", "stream", "change", "role", "startedAt", "endedAt", "outcome"];
|
|
39
|
+
const strings = ["id", "stream", "change", "role", "kind", "startedAt", "endedAt", "outcome"];
|
|
38
40
|
if (
|
|
39
41
|
!Object.keys(attempt).every((key) => allowed.has(key)) ||
|
|
40
42
|
!strings.every((key) => typeof attempt[key] === "string" && attempt[key].length > 0) ||
|
|
@@ -43,6 +45,7 @@ export function validAttempt(value: unknown, stream: string, change: string): va
|
|
|
43
45
|
attempt["stream"] !== stream ||
|
|
44
46
|
attempt["change"] !== change ||
|
|
45
47
|
attempt["role"] !== "implementer" ||
|
|
48
|
+
!["initial", "rework"].includes(attempt["kind"] as string) ||
|
|
46
49
|
!Number.isSafeInteger(attempt["toolCalls"]) ||
|
|
47
50
|
(attempt["toolCalls"] as number) < 0 ||
|
|
48
51
|
!Number.isSafeInteger(attempt["errorCount"]) ||
|
package/src/cli.ts
CHANGED
|
@@ -28,6 +28,8 @@ const usage = `Usage:
|
|
|
28
28
|
codeless planner <slug>
|
|
29
29
|
codeless approve <planner-session>
|
|
30
30
|
codeless dispatch <numbered-change-file>
|
|
31
|
+
codeless rework <numbered-change-file> <feedback>
|
|
32
|
+
codeless finish <numbered-change-file>
|
|
31
33
|
codeless land <slug>
|
|
32
34
|
codeless next <numbered-change-file> <landed-commit>
|
|
33
35
|
codeless metrics`;
|
|
@@ -386,9 +388,9 @@ export async function runCodeless(args: string[]): Promise<void> {
|
|
|
386
388
|
return Number(rect["x"]) >= rightEdge && candidateTop < bottom && candidateBottom > top;
|
|
387
389
|
})
|
|
388
390
|
.sort((left, right) => Number(left["rect"]["x"]) - Number(right["rect"]["x"]));
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
391
|
+
if (candidates.length === 0) return undefined;
|
|
392
|
+
if (candidates.length > 1) throw new Error("Planner layout has an ambiguous right-hand pane");
|
|
393
|
+
return string(candidates[0]!.pane["pane_id"], "right-hand pane id");
|
|
392
394
|
}
|
|
393
395
|
|
|
394
396
|
function requirePaneShell(pane: string, worktree: string): void {
|
|
@@ -443,7 +445,8 @@ export async function runCodeless(args: string[]): Promise<void> {
|
|
|
443
445
|
id: string,
|
|
444
446
|
slug: string,
|
|
445
447
|
number: string,
|
|
446
|
-
selection: { provider: string; model: string; thinking: string },
|
|
448
|
+
selection: { provider: string; model: string; thinking: string } | undefined,
|
|
449
|
+
kind: "initial" | "rework",
|
|
447
450
|
): Attempt {
|
|
448
451
|
const timestamp = new Date().toISOString();
|
|
449
452
|
return {
|
|
@@ -451,9 +454,10 @@ export async function runCodeless(args: string[]): Promise<void> {
|
|
|
451
454
|
stream: slug,
|
|
452
455
|
change: number,
|
|
453
456
|
role: "implementer",
|
|
457
|
+
kind,
|
|
454
458
|
startedAt: timestamp,
|
|
455
459
|
endedAt: timestamp,
|
|
456
|
-
selection,
|
|
460
|
+
...(selection === undefined ? {} : { selection }),
|
|
457
461
|
outcome: "unknown",
|
|
458
462
|
toolCalls: 0,
|
|
459
463
|
errorCount: 0,
|
|
@@ -469,6 +473,7 @@ export async function runCodeless(args: string[]): Promise<void> {
|
|
|
469
473
|
attempt.stream !== fallback.stream ||
|
|
470
474
|
attempt.change !== fallback.change ||
|
|
471
475
|
attempt.role !== "implementer" ||
|
|
476
|
+
attempt.kind !== fallback.kind ||
|
|
472
477
|
typeof attempt.startedAt !== "string" ||
|
|
473
478
|
typeof attempt.endedAt !== "string" ||
|
|
474
479
|
typeof attempt.outcome !== "string" ||
|
|
@@ -708,7 +713,7 @@ export async function runCodeless(args: string[]): Promise<void> {
|
|
|
708
713
|
const selection = readProject(worktree).implementer;
|
|
709
714
|
const attemptId = crypto.randomUUID();
|
|
710
715
|
const reportPath = join(workspaceRoot, "metrics", slug, `.attempt-${attemptId}.json`);
|
|
711
|
-
const fallbackAttempt = incompleteAttempt(attemptId, slug, number, selection);
|
|
716
|
+
const fallbackAttempt = incompleteAttempt(attemptId, slug, number, selection, "initial");
|
|
712
717
|
observe("dispatch metrics", () => recordDispatch(workspaceRoot, slug, number));
|
|
713
718
|
await validateRoleSelection("implementer", selection, worktree);
|
|
714
719
|
|
|
@@ -766,6 +771,7 @@ export async function runCodeless(args: string[]): Promise<void> {
|
|
|
766
771
|
id: attemptId,
|
|
767
772
|
stream: slug,
|
|
768
773
|
change: number,
|
|
774
|
+
kind: "initial",
|
|
769
775
|
path: reportPath,
|
|
770
776
|
});
|
|
771
777
|
const started = herdr([
|
|
@@ -822,6 +828,108 @@ export async function runCodeless(args: string[]): Promise<void> {
|
|
|
822
828
|
console.log(JSON.stringify(attempt));
|
|
823
829
|
}
|
|
824
830
|
|
|
831
|
+
function activeImplementer(changeArgument: string): {
|
|
832
|
+
change: string;
|
|
833
|
+
slug: string;
|
|
834
|
+
number: string;
|
|
835
|
+
worktree: string;
|
|
836
|
+
implementerName: string;
|
|
837
|
+
pane: string;
|
|
838
|
+
} {
|
|
839
|
+
if (process.env["HERDR_ENV"] !== "1")
|
|
840
|
+
throw new Error("Implementer control must run from a Herdr-managed planner");
|
|
841
|
+
const { change, slug, number, worktree, branch } = requireChange(changeArgument);
|
|
842
|
+
if (canonicalPath(process.cwd()) !== worktree)
|
|
843
|
+
throw new Error(`Implementer control cwd is ${process.cwd()}, expected ${worktree}`);
|
|
844
|
+
if (run("git", ["branch", "--show-current"], worktree).trim() !== branch)
|
|
845
|
+
throw new Error(`${worktree} is not on ${branch}`);
|
|
846
|
+
const plannerPane = string(process.env["HERDR_PANE_ID"], "HERDR_PANE_ID");
|
|
847
|
+
const plannerProcesses = foregroundProcesses(paneProcessInfo(plannerPane));
|
|
848
|
+
if (
|
|
849
|
+
!plannerProcesses.some(
|
|
850
|
+
(process) => canonicalPath(string(process["cwd"], "planner cwd")) === worktree,
|
|
851
|
+
)
|
|
852
|
+
)
|
|
853
|
+
throw new Error(`Planner pane ${plannerPane} is not running in ${worktree}`);
|
|
854
|
+
const layout = object(
|
|
855
|
+
result(herdr(["pane", "layout", "--pane", plannerPane]))["layout"],
|
|
856
|
+
"result.layout",
|
|
857
|
+
);
|
|
858
|
+
const pane = rightPane(layout, plannerPane);
|
|
859
|
+
if (pane === undefined) throw new Error("Planner has no right-hand implementer pane");
|
|
860
|
+
const implementerName = `${slug.replaceAll("-", "_")}_impl`;
|
|
861
|
+
const agent = object(result(herdr(["agent", "get", pane]))["agent"], "result.agent");
|
|
862
|
+
if (string(agent["name"], "result.agent.name") !== implementerName)
|
|
863
|
+
throw new Error(`Right-hand pane ${pane} is not implementer ${implementerName}`);
|
|
864
|
+
if (!["idle", "done"].includes(string(agent["agent_status"], "result.agent.agent_status")))
|
|
865
|
+
throw new Error(`Implementer ${implementerName} is not settled`);
|
|
866
|
+
const agentCwd = string(agent["foreground_cwd"] ?? agent["cwd"], "result.agent.foreground_cwd");
|
|
867
|
+
if (canonicalPath(agentCwd) !== worktree)
|
|
868
|
+
throw new Error(`Implementer ${implementerName} is in ${agentCwd}, expected ${worktree}`);
|
|
869
|
+
const processes = foregroundProcesses(paneProcessInfo(pane));
|
|
870
|
+
if (
|
|
871
|
+
!processes.some(
|
|
872
|
+
(process) => canonicalPath(string(process["cwd"], "implementer cwd")) === worktree,
|
|
873
|
+
)
|
|
874
|
+
)
|
|
875
|
+
throw new Error(`Implementer pane ${pane} is not running in ${worktree}`);
|
|
876
|
+
return { change, slug, number, worktree, implementerName, pane };
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
async function rework(changeArgument: string, feedback: string): Promise<void> {
|
|
880
|
+
if (feedback.trim().length === 0 || feedback.trim().length > 2_000)
|
|
881
|
+
throw new Error("Rework feedback must be concise non-empty text");
|
|
882
|
+
const { slug, number, implementerName } = activeImplementer(changeArgument);
|
|
883
|
+
const attemptId = crypto.randomUUID();
|
|
884
|
+
const reportPath = join(workspaceRoot, "metrics", slug, `.attempt-${attemptId}.json`);
|
|
885
|
+
const fallbackAttempt = incompleteAttempt(attemptId, slug, number, undefined, "rework");
|
|
886
|
+
const collection = JSON.stringify({
|
|
887
|
+
id: attemptId,
|
|
888
|
+
stream: slug,
|
|
889
|
+
change: number,
|
|
890
|
+
kind: "rework",
|
|
891
|
+
path: reportPath,
|
|
892
|
+
feedback: feedback.trim(),
|
|
893
|
+
});
|
|
894
|
+
run("herdr", [
|
|
895
|
+
"agent",
|
|
896
|
+
"prompt",
|
|
897
|
+
implementerName,
|
|
898
|
+
`/codeless-rework ${collection}`,
|
|
899
|
+
"--wait",
|
|
900
|
+
"--timeout",
|
|
901
|
+
"3600000",
|
|
902
|
+
]);
|
|
903
|
+
let attempt = fallbackAttempt;
|
|
904
|
+
try {
|
|
905
|
+
attempt = collectedAttempt(JSON.parse(readFileSync(reportPath, "utf8")), fallbackAttempt);
|
|
906
|
+
if (attempt === fallbackAttempt) throw new Error("report did not match its rework attempt");
|
|
907
|
+
} catch (error) {
|
|
908
|
+
console.error(
|
|
909
|
+
`codeless: warning: could not collect implementer attempt: ${error instanceof Error ? error.message : String(error)}`,
|
|
910
|
+
);
|
|
911
|
+
} finally {
|
|
912
|
+
if (existsSync(reportPath)) unlinkSync(reportPath);
|
|
913
|
+
}
|
|
914
|
+
observe("implementer attempt", () => recordAttempt(workspaceRoot, slug, number, attempt));
|
|
915
|
+
console.log(JSON.stringify(attempt));
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function finish(changeArgument: string): void {
|
|
919
|
+
const { slug, number, worktree, implementerName, pane } = activeImplementer(changeArgument);
|
|
920
|
+
run("herdr", [
|
|
921
|
+
"agent",
|
|
922
|
+
"prompt",
|
|
923
|
+
implementerName,
|
|
924
|
+
`/codeless-finish ${JSON.stringify({ stream: slug, change: number })}`,
|
|
925
|
+
"--wait",
|
|
926
|
+
"--timeout",
|
|
927
|
+
"30000",
|
|
928
|
+
]);
|
|
929
|
+
requirePaneShell(pane, worktree);
|
|
930
|
+
console.log(JSON.stringify({ pane, worktree }));
|
|
931
|
+
}
|
|
932
|
+
|
|
825
933
|
async function launchPlanner(slug: string): Promise<void> {
|
|
826
934
|
if (process.env["HERDR_ENV"] !== "1") {
|
|
827
935
|
throw new Error("Run codeless planner from the stream's Herdr-managed shell");
|
|
@@ -997,6 +1105,16 @@ export async function runCodeless(args: string[]): Promise<void> {
|
|
|
997
1105
|
await dispatch(target);
|
|
998
1106
|
return;
|
|
999
1107
|
}
|
|
1108
|
+
if (action === "rework") {
|
|
1109
|
+
if (target === undefined || details.length !== 1) throw new Error(usage);
|
|
1110
|
+
await rework(target, details[0]!);
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
if (action === "finish") {
|
|
1114
|
+
if (target === undefined || details.length > 0) throw new Error(usage);
|
|
1115
|
+
finish(target);
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1000
1118
|
if (action === "next") {
|
|
1001
1119
|
if (target === undefined || details.length !== 1) throw new Error(usage);
|
|
1002
1120
|
await nextChange(target, details[0]!);
|
package/src/metrics.ts
CHANGED
|
@@ -201,12 +201,98 @@ export function metricReport(workspaceRoot: string): string[] {
|
|
|
201
201
|
return `${name}\t${value.landed}\t${value.unlanded}\t${value.coverage}\t${value.total}\t${value.average}`;
|
|
202
202
|
};
|
|
203
203
|
const rows = [...byStream.entries()].map(([stream, records]) => ({ stream, records }));
|
|
204
|
+
const allRecords = rows.flatMap(({ records }) => records);
|
|
205
|
+
const formatCost = (amounts: number[]) => {
|
|
206
|
+
const parts = amounts.map((amount) => {
|
|
207
|
+
const [coefficient, exponent = "0"] = String(amount).toLowerCase().split("e");
|
|
208
|
+
const [whole, fraction = ""] = coefficient!.split(".");
|
|
209
|
+
return { digits: BigInt(`${whole}${fraction}`), scale: fraction.length - Number(exponent) };
|
|
210
|
+
});
|
|
211
|
+
const scale = Math.max(0, ...parts.map((part) => part.scale));
|
|
212
|
+
const total = parts.reduce(
|
|
213
|
+
(sum, part) => sum + part.digits * 10n ** BigInt(scale - part.scale),
|
|
214
|
+
0n,
|
|
215
|
+
);
|
|
216
|
+
const digits = total.toString().padStart(scale + 1, "0");
|
|
217
|
+
if (scale === 0) return digits;
|
|
218
|
+
const fraction = digits.slice(-scale).replace(/0+$/, "");
|
|
219
|
+
return fraction.length === 0
|
|
220
|
+
? digits.slice(0, -scale)
|
|
221
|
+
: `${digits.slice(0, -scale)}.${fraction}`;
|
|
222
|
+
};
|
|
223
|
+
const attemptSummary = (records: Metric[]) => {
|
|
224
|
+
const attempts = records.flatMap((record) => Object.values(record.attempts ?? {}));
|
|
225
|
+
const usage = attempts.filter((attempt) => attempt.usage !== undefined);
|
|
226
|
+
const costs = attempts.filter((attempt) => attempt.cost !== undefined);
|
|
227
|
+
const outcomes = new Map<string, number>();
|
|
228
|
+
const currencies = new Map<string, number[]>();
|
|
229
|
+
for (const attempt of attempts) {
|
|
230
|
+
outcomes.set(attempt.outcome, (outcomes.get(attempt.outcome) ?? 0) + 1);
|
|
231
|
+
if (attempt.cost !== undefined)
|
|
232
|
+
currencies.set(attempt.cost.currency, [
|
|
233
|
+
...(currencies.get(attempt.cost.currency) ?? []),
|
|
234
|
+
attempt.cost.amount,
|
|
235
|
+
]);
|
|
236
|
+
}
|
|
237
|
+
const coverage = (measured: number) =>
|
|
238
|
+
`${measured} measured, ${attempts.length - measured} unavailable`;
|
|
239
|
+
return {
|
|
240
|
+
reworkedChanges: new Set(
|
|
241
|
+
records
|
|
242
|
+
.filter((record) =>
|
|
243
|
+
Object.values(record.attempts ?? {}).some((attempt) => attempt.kind === "rework"),
|
|
244
|
+
)
|
|
245
|
+
.map((record) => `${record.stream}\0${record.change}`),
|
|
246
|
+
).size,
|
|
247
|
+
initial: attempts.filter((attempt) => attempt.kind === "initial").length,
|
|
248
|
+
rework: attempts.filter((attempt) => attempt.kind === "rework").length,
|
|
249
|
+
incomplete: attempts.filter((attempt) => attempt.incomplete).length,
|
|
250
|
+
outcomes:
|
|
251
|
+
outcomes.size === 0
|
|
252
|
+
? "none"
|
|
253
|
+
: [...outcomes.entries()]
|
|
254
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
255
|
+
.map(([outcome, count]) => `${outcome}: ${count}`)
|
|
256
|
+
.join(", "),
|
|
257
|
+
toolErrors: attempts.reduce((total, attempt) => total + attempt.errorCount, 0),
|
|
258
|
+
usageCoverage: coverage(usage.length),
|
|
259
|
+
input:
|
|
260
|
+
usage.length === 0
|
|
261
|
+
? "unavailable"
|
|
262
|
+
: usage.reduce((total, attempt) => total + attempt.usage!.input, 0),
|
|
263
|
+
output:
|
|
264
|
+
usage.length === 0
|
|
265
|
+
? "unavailable"
|
|
266
|
+
: usage.reduce((total, attempt) => total + attempt.usage!.output, 0),
|
|
267
|
+
cacheRead:
|
|
268
|
+
usage.length === 0
|
|
269
|
+
? "unavailable"
|
|
270
|
+
: usage.reduce((total, attempt) => total + attempt.usage!.cacheRead, 0),
|
|
271
|
+
cacheWrite:
|
|
272
|
+
usage.length === 0
|
|
273
|
+
? "unavailable"
|
|
274
|
+
: usage.reduce((total, attempt) => total + attempt.usage!.cacheWrite, 0),
|
|
275
|
+
costCoverage: coverage(costs.length),
|
|
276
|
+
costs:
|
|
277
|
+
currencies.size === 0
|
|
278
|
+
? "unavailable"
|
|
279
|
+
: [...currencies.entries()]
|
|
280
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
281
|
+
.map(([currency, amounts]) => `${currency} ${formatCost(amounts)}`)
|
|
282
|
+
.join(", "),
|
|
283
|
+
};
|
|
284
|
+
};
|
|
285
|
+
const attemptRow = (name: string, records: Metric[]) => {
|
|
286
|
+
const value = attemptSummary(records);
|
|
287
|
+
return `${name}\t${value.reworkedChanges}\t${value.initial}\t${value.rework}\t${value.incomplete}\t${value.outcomes}\t${value.toolErrors}\t${value.usageCoverage}\t${value.input}\t${value.output}\t${value.cacheRead}\t${value.cacheWrite}\t${value.costCoverage}\t${value.costs}`;
|
|
288
|
+
};
|
|
204
289
|
return [
|
|
205
290
|
"Stream\tLanded\tNot landed\tElapsed coverage\tDispatch-to-land wall clock total\tAverage",
|
|
206
291
|
...rows.map(({ stream, records }) => row(stream, records)),
|
|
207
|
-
row(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
),
|
|
292
|
+
row("Project total", allRecords),
|
|
293
|
+
"",
|
|
294
|
+
"Stream\tChanges with rework\tInitial attempts\tRework attempts\tIncomplete collection\tTerminal outcomes\tTool errors\tUsage coverage\tInput tokens\tOutput tokens\tCache-read tokens\tCache-write tokens\tCost coverage\tCost totals",
|
|
295
|
+
...rows.map(({ stream, records }) => attemptRow(stream, records)),
|
|
296
|
+
attemptRow("Project total", allRecords),
|
|
211
297
|
];
|
|
212
298
|
}
|