@dpeek/codeless 0.1.0 → 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 +80 -15
- package/extension/implementer-reporting.js +215 -0
- package/extension/planner.js +106 -2
- package/package.json +2 -2
- package/spec/workflow.md +68 -27
- package/src/attempt.ts +104 -0
- package/src/cli.ts +340 -25
- package/src/metrics.ts +148 -5
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
|
|
@@ -154,9 +154,18 @@ planners or worktree shells are running.
|
|
|
154
154
|
worktree/<slug>/ # stream/<slug> branch
|
|
155
155
|
worktree/main/ # example integration checkout location
|
|
156
156
|
.land-lock/ # shared landing owner and recorded integration commit
|
|
157
|
-
metrics/<slug>/NNN.json #
|
|
157
|
+
metrics/<slug>/NNN.json # dispatch/landing times and deduplicated implementer attempts
|
|
158
158
|
```
|
|
159
159
|
|
|
160
|
+
Run `codeless init` once after configuring a project. It creates the shared state
|
|
161
|
+
layout and the dedicated integration worktree at
|
|
162
|
+
`<workspace>/worktree/<integration-branch>` without requiring Herdr. For the
|
|
163
|
+
default workspace it adds only `/.codeless/state/` to the primary checkout's
|
|
164
|
+
`.gitignore`; an absolute workspace override does not modify repository ignores.
|
|
165
|
+
It is safe to repeat when that exact worktree is registered. It stops rather
|
|
166
|
+
than moving a branch checkout, replacing an occupied target, or broadening a
|
|
167
|
+
repository ignore rule that covers configuration or prompts.
|
|
168
|
+
|
|
160
169
|
Normal `git clean -fd` preserves ignored state. `git clean -fdx` removes ignored
|
|
161
170
|
files and can therefore destroy local Codeless journals, metrics, and worktrees;
|
|
162
171
|
inspect its targets before using it.
|
|
@@ -167,36 +176,65 @@ Run creation, opening, and planner launch from a Herdr-managed shell. Landing
|
|
|
167
176
|
needs no Herdr session.
|
|
168
177
|
|
|
169
178
|
```sh
|
|
179
|
+
codeless init
|
|
170
180
|
codeless create <slug>
|
|
171
181
|
codeless open <slug>
|
|
172
182
|
codeless planner <slug>
|
|
173
183
|
codeless approve <planner-session>
|
|
174
184
|
codeless dispatch <numbered-change-file>
|
|
185
|
+
codeless rework <numbered-change-file> <feedback>
|
|
186
|
+
codeless finish <numbered-change-file>
|
|
175
187
|
codeless land <slug>
|
|
176
188
|
codeless next <numbered-change-file> <landed-commit>
|
|
177
189
|
codeless metrics
|
|
178
190
|
```
|
|
179
191
|
|
|
180
|
-
Slugs are lowercase kebab-case, at most 24 characters. `
|
|
192
|
+
Slugs are lowercase kebab-case, at most 24 characters. `init` validates the
|
|
193
|
+
existing configuration and integration branch, reports the branch, primary
|
|
194
|
+
checkout, workspace, and integration worktree, then creates only the shared
|
|
195
|
+
state directories and canonical integration worktree when absent. All other
|
|
196
|
+
commands validate their prerequisites and never bootstrap this setup. `create` starts
|
|
181
197
|
`stream/<slug>` from the integration branch and creates its local documents;
|
|
182
198
|
it refuses existing streams. `open` resumes a stream. Both run the configured
|
|
183
199
|
install command, then validate and open a planner beside an idle shell. `planner`
|
|
184
200
|
starts Pi in an existing stream's lone shell after the same role preflight. Its
|
|
185
201
|
activation establishes the same identity as creation and reopening.
|
|
186
202
|
Dispatch validates the implementer selection before touching the planner's
|
|
187
|
-
right-hand pane, starts a fresh ephemeral implementer
|
|
203
|
+
right-hand pane, starts a fresh ephemeral implementer with Codeless's reporting
|
|
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,
|
|
213
|
+
terminal text and outcome, full-session Pi usage and available Pi cost estimate,
|
|
214
|
+
timestamps, and tool/error counts; prompts, source, thinking, credentials, and
|
|
215
|
+
transcripts are not retained.
|
|
188
216
|
|
|
189
217
|
The first valid dispatch creates one atomic local JSON metric record for its
|
|
190
|
-
stream and numbered change.
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
218
|
+
stream and numbered change. Each accepted run receives a new attempt ID and is
|
|
219
|
+
added idempotently under that record; re-ingesting an ID preserves the original
|
|
220
|
+
attempt and retries preserve the original dispatch time. Missing, malformed, or
|
|
221
|
+
unwritable collection data warns and stores an incomplete attempt when possible
|
|
222
|
+
without retrying or failing a settled implementation. After a successful
|
|
223
|
+
integration fast-forward, Codeless records the landed time and commit on that
|
|
224
|
+
change's canonical record, creating a landed record without elapsed time when
|
|
225
|
+
dispatch collection was unavailable; collection warnings never alter dispatch or
|
|
226
|
+
landing.
|
|
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
|
|
197
229
|
without a measured duration are explicitly unavailable. Dispatched-but-unlanded
|
|
198
|
-
changes remain a separate count.
|
|
199
|
-
|
|
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.
|
|
200
238
|
|
|
201
239
|
Landing requires clean stream and integration worktrees and exactly one stream
|
|
202
240
|
commit outside their merge base. It acquires `.land-lock` atomically, recording
|
|
@@ -233,8 +271,35 @@ From this package directory, run `bun run check` for formatting, lint, types,
|
|
|
233
271
|
and tests, or `bun run test` for tests alone. The integration tests use real Git
|
|
234
272
|
worktrees and mock Herdr/Pi commands; they never launch actual agents.
|
|
235
273
|
|
|
236
|
-
`
|
|
237
|
-
|
|
274
|
+
From a clean `main` checkout, publish the next patch release with:
|
|
275
|
+
|
|
276
|
+
```sh
|
|
277
|
+
bun run release
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
The script verifies npm authentication, runs the full check and an npm package
|
|
281
|
+
dry run, increments the patch version, commits `package.json` with the version as
|
|
282
|
+
the complete commit message, creates the matching version tag, and publishes
|
|
283
|
+
`@dpeek/codeless`. It intentionally does not push the commit or tag.
|
|
284
|
+
|
|
285
|
+
For non-interactive local publishing, create an npm granular access token with
|
|
286
|
+
read/write access to `@dpeek/codeless` (or the `@dpeek` scope) and **Bypass 2FA**
|
|
287
|
+
enabled. Put the token in the repository's ignored `.env` file:
|
|
288
|
+
|
|
289
|
+
```sh
|
|
290
|
+
NPM_TOKEN=npm_your_token_here
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Reference that variable from your user-level `~/.npmrc`:
|
|
294
|
+
|
|
295
|
+
```ini
|
|
296
|
+
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
`bun run release` loads `.env` and passes the token to npm. Never put the token
|
|
300
|
+
directly in a committed `.npmrc` or source file. If npm package settings disallow
|
|
301
|
+
tokens, publishing will still require an OTP. For hosted CI, prefer npm trusted
|
|
302
|
+
publishing instead of a long-lived token.
|
|
238
303
|
|
|
239
304
|
Keep source, tests, executable, extension, and dependencies inside this project.
|
|
240
305
|
Keep project policies and real prompts outside it. Add automation only for
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { existsSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
function text(message) {
|
|
4
|
+
if (message?.role !== "assistant" || !Array.isArray(message.content)) return undefined;
|
|
5
|
+
const value = message.content
|
|
6
|
+
.filter((part) => part?.type === "text" && typeof part.text === "string")
|
|
7
|
+
.map((part) => part.text)
|
|
8
|
+
.join("");
|
|
9
|
+
return value || undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function validUsage(value) {
|
|
13
|
+
return (
|
|
14
|
+
typeof value === "object" &&
|
|
15
|
+
value !== null &&
|
|
16
|
+
["input", "output", "cacheRead", "cacheWrite"].every(
|
|
17
|
+
(key) => Number.isFinite(value[key]) && value[key] >= 0,
|
|
18
|
+
)
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function sessionUsage(entries) {
|
|
23
|
+
const usages = [];
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
if (
|
|
26
|
+
entry?.type === "message" &&
|
|
27
|
+
(entry.message?.role === "assistant" || entry.message?.role === "toolResult") &&
|
|
28
|
+
validUsage(entry.message.usage)
|
|
29
|
+
)
|
|
30
|
+
usages.push(entry.message.usage);
|
|
31
|
+
if (
|
|
32
|
+
(entry?.type === "compaction" || entry?.type === "branch_summary") &&
|
|
33
|
+
validUsage(entry.usage)
|
|
34
|
+
)
|
|
35
|
+
usages.push(entry.usage);
|
|
36
|
+
}
|
|
37
|
+
if (usages.length === 0) return undefined;
|
|
38
|
+
const total = (key) => usages.reduce((sum, usage) => sum + usage[key], 0);
|
|
39
|
+
const costs = usages.map((usage) => usage.cost?.total);
|
|
40
|
+
return {
|
|
41
|
+
input: total("input"),
|
|
42
|
+
output: total("output"),
|
|
43
|
+
cacheRead: total("cacheRead"),
|
|
44
|
+
cacheWrite: total("cacheWrite"),
|
|
45
|
+
...(costs.every((cost) => Number.isFinite(cost) && cost >= 0)
|
|
46
|
+
? {
|
|
47
|
+
cost: {
|
|
48
|
+
amount: costs.reduce((sum, cost) => sum + cost, 0),
|
|
49
|
+
currency: "USD",
|
|
50
|
+
source: "pi-model-estimate",
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
: {}),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function messages(entries) {
|
|
58
|
+
return entries
|
|
59
|
+
.filter((entry) => entry?.type === "message")
|
|
60
|
+
.map((entry) => entry.message)
|
|
61
|
+
.filter((message) => message?.role === "assistant" || message?.role === "toolResult");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function write(path, value) {
|
|
65
|
+
const temporary = `${path}.${process.pid}.${crypto.randomUUID()}`;
|
|
66
|
+
try {
|
|
67
|
+
writeFileSync(temporary, `${JSON.stringify(value)}\n`, { flag: "wx" });
|
|
68
|
+
renameSync(temporary, path);
|
|
69
|
+
} finally {
|
|
70
|
+
if (existsSync(temporary)) unlinkSync(temporary);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default function implementerReportingExtension(pi) {
|
|
75
|
+
pi.registerFlag("codeless-attempt", { type: "string" });
|
|
76
|
+
let configuration;
|
|
77
|
+
let startupScope;
|
|
78
|
+
let rearm;
|
|
79
|
+
let startedAt;
|
|
80
|
+
let entryOffset = 0;
|
|
81
|
+
let toolCalls = 0;
|
|
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
|
+
}
|
|
95
|
+
pi.on("session_start", () => {
|
|
96
|
+
try {
|
|
97
|
+
const value = JSON.parse(pi.getFlag("codeless-attempt") ?? "");
|
|
98
|
+
configuration = configure(value).value;
|
|
99
|
+
startupScope = { stream: configuration.stream, change: configuration.change };
|
|
100
|
+
} catch {}
|
|
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
|
+
});
|
|
149
|
+
pi.on("agent_start", () => {
|
|
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
|
+
}
|
|
160
|
+
});
|
|
161
|
+
pi.on("tool_execution_end", (event) => {
|
|
162
|
+
toolCalls += 1;
|
|
163
|
+
if (event.isError) errorCount += 1;
|
|
164
|
+
});
|
|
165
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
166
|
+
if (configuration === undefined) return;
|
|
167
|
+
const report = configuration;
|
|
168
|
+
configuration = undefined;
|
|
169
|
+
const entries = ctx.sessionManager.getEntries().slice(entryOffset);
|
|
170
|
+
const settledMessages = messages(entries);
|
|
171
|
+
const final = [...settledMessages].reverse().find((message) => message.role === "assistant");
|
|
172
|
+
const totals = sessionUsage(entries);
|
|
173
|
+
const model = final?.responseModel ?? final?.model ?? ctx.model?.id;
|
|
174
|
+
const provider = final?.provider ?? ctx.model?.provider;
|
|
175
|
+
const selection =
|
|
176
|
+
typeof provider === "string" &&
|
|
177
|
+
typeof model === "string" &&
|
|
178
|
+
typeof pi.getThinkingLevel() === "string"
|
|
179
|
+
? { provider, model, thinking: pi.getThinkingLevel() }
|
|
180
|
+
: undefined;
|
|
181
|
+
const finalText = text(final);
|
|
182
|
+
const complete =
|
|
183
|
+
selection !== undefined &&
|
|
184
|
+
typeof final?.stopReason === "string" &&
|
|
185
|
+
final.stopReason.length > 0 &&
|
|
186
|
+
finalText !== undefined &&
|
|
187
|
+
totals !== undefined;
|
|
188
|
+
write(report.path, {
|
|
189
|
+
id: report.id,
|
|
190
|
+
stream: report.stream,
|
|
191
|
+
change: report.change,
|
|
192
|
+
role: "implementer",
|
|
193
|
+
kind: report.kind,
|
|
194
|
+
startedAt: startedAt ?? new Date().toISOString(),
|
|
195
|
+
endedAt: new Date().toISOString(),
|
|
196
|
+
...(selection === undefined ? {} : { selection }),
|
|
197
|
+
outcome: final?.stopReason ?? "unknown",
|
|
198
|
+
...(finalText === undefined ? {} : { text: finalText }),
|
|
199
|
+
...(totals === undefined
|
|
200
|
+
? {}
|
|
201
|
+
: {
|
|
202
|
+
usage: {
|
|
203
|
+
input: totals.input,
|
|
204
|
+
output: totals.output,
|
|
205
|
+
cacheRead: totals.cacheRead,
|
|
206
|
+
cacheWrite: totals.cacheWrite,
|
|
207
|
+
},
|
|
208
|
+
...(totals.cost === undefined ? {} : { cost: totals.cost }),
|
|
209
|
+
}),
|
|
210
|
+
toolCalls,
|
|
211
|
+
errorCount,
|
|
212
|
+
incomplete: !complete,
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
}
|
package/extension/planner.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { validAttempt } from "../src/attempt.ts";
|
|
2
3
|
|
|
3
4
|
const codeless = fileURLToPath(new URL("../bin/codeless", import.meta.url));
|
|
4
5
|
const thinkingLevels = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
5
6
|
const requiredTools = [
|
|
6
7
|
"approve_stream_change",
|
|
7
8
|
"dispatch_stream_implementer",
|
|
9
|
+
"rework_stream_implementer",
|
|
10
|
+
"finish_stream_implementer",
|
|
8
11
|
"next_stream_change",
|
|
9
12
|
];
|
|
10
13
|
|
|
@@ -289,6 +292,98 @@ export default function plannerExtension(pi) {
|
|
|
289
292
|
},
|
|
290
293
|
});
|
|
291
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
|
+
|
|
292
387
|
pi.registerTool({
|
|
293
388
|
name: "dispatch_stream_implementer",
|
|
294
389
|
label: "Dispatch stream implementer",
|
|
@@ -322,13 +417,22 @@ export default function plannerExtension(pi) {
|
|
|
322
417
|
if (execution.code !== 0) {
|
|
323
418
|
throw new Error(output || `codeless dispatch failed with exit code ${execution.code}`);
|
|
324
419
|
}
|
|
420
|
+
let attempt;
|
|
421
|
+
try {
|
|
422
|
+
attempt = JSON.parse(execution.stdout);
|
|
423
|
+
} catch {
|
|
424
|
+
throw new Error("Codeless returned an invalid implementer attempt");
|
|
425
|
+
}
|
|
426
|
+
if (!validAttempt(attempt, attempt?.stream, attempt?.change)) {
|
|
427
|
+
throw new Error("Codeless returned an invalid implementer attempt");
|
|
428
|
+
}
|
|
325
429
|
pi.sendUserMessage(`/review ${JSON.stringify(changePath)}`, {
|
|
326
430
|
deliverAs: "steer",
|
|
327
431
|
expandPromptTemplates: true,
|
|
328
432
|
});
|
|
329
433
|
return {
|
|
330
|
-
content: [{ type: "text", text:
|
|
331
|
-
details: { changePath },
|
|
434
|
+
content: [{ type: "text", text: attempt.text || "Implementer settled." }],
|
|
435
|
+
details: { changePath, attempt },
|
|
332
436
|
};
|
|
333
437
|
},
|
|
334
438
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dpeek/codeless",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "An attended planner and implementer workflow for parallel capability development",
|
|
5
5
|
"homepage": "https://github.com/dpeek/codeless#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
29
|
"check": "oxfmt --config .oxfmtrc.json --write . && oxlint --config .oxlintrc.json --fix --type-aware --type-check . && bun run test",
|
|
30
|
-
"
|
|
30
|
+
"release": "bun ./scripts/release.ts",
|
|
31
31
|
"test": "bun test ./test --dots"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
package/spec/workflow.md
CHANGED
|
@@ -49,6 +49,18 @@ Git-ignored, and every linked worktree resolves the same primary-checkout state:
|
|
|
49
49
|
metrics/<slug>/NNN.json
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
`codeless init` is the explicit, idempotent bootstrap for a configured project.
|
|
53
|
+
It requires the configured integration branch to exist and creates the shared
|
|
54
|
+
state directories plus that branch's worktree only at
|
|
55
|
+
`<workspace>/worktree/<integration-branch>`. It reuses only the exact registered
|
|
56
|
+
canonical checkout. An occupied target, a branch registered elsewhere, invalid
|
|
57
|
+
checkout, or ambiguous Git registration stops unchanged; init never switches
|
|
58
|
+
branches, moves worktrees, or repairs conflicts. With the default workspace,
|
|
59
|
+
it accepts a repository ignore rule only when it ignores the state path without
|
|
60
|
+
covering configuration or configured prompts, otherwise appending the narrow
|
|
61
|
+
`/.codeless/state/` rule. An absolute workspace never changes repository
|
|
62
|
+
ignores. No other command bootstraps this layout.
|
|
63
|
+
|
|
52
64
|
`planner.md` owns decisions, approvals, review outcomes, landing history, and
|
|
53
65
|
the context needed by a fresh planner. `change.md` is the editable current
|
|
54
66
|
proposal. `changes/NNN.md` is the immutable-by-policy approved input to one
|
|
@@ -91,8 +103,8 @@ post-landing replacement—uses the package-owned extension as its activation
|
|
|
91
103
|
boundary. Before its first project prompt, activation requires the exact
|
|
92
104
|
`<slug>-planner` Pi session name, establishes and verifies Herdr reports
|
|
93
105
|
`<slug-with-hyphens-replaced>_planner`, and verifies
|
|
94
|
-
`approve_stream_change`, `dispatch_stream_implementer`,
|
|
95
|
-
`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
|
|
96
108
|
mismatch, or an incomplete tool set stops visibly before `/change`. A direct
|
|
97
109
|
restart may begin with Herdr's `pi` fallback identity; activation renames and
|
|
98
110
|
rereads only that fallback. Any other identity mismatch stops. Implementers use
|
|
@@ -108,18 +120,41 @@ right-hand Herdr pane only when that pane is an available shell or the expected
|
|
|
108
120
|
idle implementer, starts a fresh ephemeral Pi implementer in the stream
|
|
109
121
|
worktree, submits `/implement`, and waits for at most one hour.
|
|
110
122
|
|
|
111
|
-
Successful dispatch
|
|
123
|
+
Successful dispatch loads the package-owned reporting extension while retaining
|
|
124
|
+
`--no-session` and passes its report configuration through that extension's
|
|
125
|
+
explicit Pi string flag, then returns one normalized attempt to the planner tool
|
|
126
|
+
before it queues the expanded `/review` prompt. Attempts have a stable ID and
|
|
127
|
+
capture only stream/change/role, start and settlement timestamps, Pi's actual
|
|
128
|
+
settled provider/model/thinking selection, terminal outcome and final text,
|
|
129
|
+
full-session Pi input/output/cache usage (including tool results, compaction,
|
|
130
|
+
and branch summaries), available Pi model cost estimate with USD currency and
|
|
131
|
+
source, and tool/error counts. Cost is omitted when Pi did not supply valid
|
|
132
|
+
cost totals. They do not retain prompts, source, credentials, thinking, or a
|
|
133
|
+
transcript. The extension writes its narrow report atomically once, then
|
|
134
|
+
remains disarmed for remediation; Codeless atomically deduplicates it inside the
|
|
135
|
+
per-change metric record, rejecting a
|
|
136
|
+
conflicting duplicate ID. Missing, malformed, or unwritable collection warns
|
|
137
|
+
and yields an explicitly incomplete attempt when possible without failing or
|
|
138
|
+
repeating a settled implementation.
|
|
139
|
+
|
|
112
140
|
The planner inspects the full diff and relevant code, checks the approved
|
|
113
141
|
acceptance criteria, and runs focused checks when the implementation output is
|
|
114
|
-
insufficient.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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.
|
|
123
158
|
|
|
124
159
|
## Commit and landing
|
|
125
160
|
|
|
@@ -162,21 +197,27 @@ before another implementation.
|
|
|
162
197
|
## Local workflow metrics
|
|
163
198
|
|
|
164
199
|
The first dispatch for a stream and numbered change creates one atomic local
|
|
165
|
-
metric record.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
200
|
+
metric record. Every accepted dispatch creates a new attempt ID; re-ingesting an
|
|
201
|
+
attempt ID is atomic and idempotent, while the original dispatch timestamp stays
|
|
202
|
+
unchanged. Successful landing adds its timestamp and commit, or creates a landed
|
|
203
|
+
record with unavailable elapsed time when dispatch collection was unavailable.
|
|
204
|
+
Collection warnings do not change dispatch or landing outcomes.
|
|
205
|
+
|
|
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.
|
|
180
221
|
|
|
181
222
|
## Limits
|
|
182
223
|
|