@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 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 # first dispatch, landing time, and landed commit
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. `create` starts
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, and waits for completion.
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. Retries preserve its original dispatch time. After a
191
- successful integration fast-forward, Codeless records the landed time and commit
192
- on that change's canonical record, creating a landed record without elapsed time
193
- when dispatch collection was unavailable; collection warnings never alter
194
- dispatch or landing.
195
- `codeless metrics` prints every recorded stream and a project total. Its elapsed
196
- columns are dispatch-to-land wall-clock time; among landed changes, records
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. Metrics are prospective local observations, not
199
- journal state or a recovery mechanism.
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
- `npm publish` runs the full check through `prepublishOnly` and publishes
237
- `@dpeek/codeless` with public access.
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
+ }
@@ -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: output || "Implementer settled." }],
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.0",
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
- "prepublishOnly": "bun run check",
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`, and
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 queues the expanded `/review` prompt back into the planner.
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. Remediation reuses the same implementer context. Once approved,
115
- the planner records the review result, exits the implementer so its pane returns
116
- to a shell, and follows the commit-and-land prompt without another approval
117
- round.
118
-
119
- Dispatch and remediation do not retry automatically. Dispatch currently exposes
120
- Herdr command output rather than a normalized implementation result, and
121
- remediation and implementer shutdown are still performed through prompt-owned
122
- Herdr commands.
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. Retrying dispatch preserves the original timestamp. Successful
166
- landing adds its timestamp and commit, or creates a landed record with
167
- unavailable elapsed time when dispatch collection was unavailable. Collection
168
- warnings do not change dispatch or landing outcomes.
169
-
170
- `codeless metrics` reports every recorded stream and a project total with:
171
-
172
- - landed and dispatched-but-unlanded change counts;
173
- - measured versus unavailable elapsed coverage; and
174
- - total and average dispatch-to-land wall-clock time.
175
-
176
- The measurements are prospective, local observations. They are not journal
177
- state, an approval source, or a recovery mechanism. Token usage, cost,
178
- normalized attempt results, review rework, and failure breakdowns are not yet
179
- collected.
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