@dpeek/codeless 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.ts CHANGED
@@ -25,9 +25,10 @@ const usage = `Usage:
25
25
  codeless init
26
26
  codeless create <slug>
27
27
  codeless open <slug>
28
- codeless planner <slug>
29
28
  codeless approve <planner-session>
30
29
  codeless dispatch <numbered-change-file>
30
+ codeless rework <numbered-change-file> <feedback>
31
+ codeless finish <numbered-change-file>
31
32
  codeless land <slug>
32
33
  codeless next <numbered-change-file> <landed-commit>
33
34
  codeless metrics`;
@@ -165,6 +166,37 @@ export async function runCodeless(args: string[]): Promise<void> {
165
166
  run("git", ["show-ref", "--verify", `refs/heads/${integrationBranch}`], repository);
166
167
  const primary = primaryWorktree();
167
168
  const target = canonicalPath(join(workspaceRoot, "worktree", integrationBranch));
169
+ const promptNames = ["change", "implement", "review", "commit"];
170
+ const promptDirectory = join(repository, project.prompts);
171
+ const promptTemplates = join(import.meta.dir, "../prompts");
172
+ const promptPlan = promptNames.map((name) => {
173
+ const template = join(promptTemplates, `${name}.md`);
174
+ const destination = join(promptDirectory, `${name}.md`);
175
+ if (!existsSync(template) || !statSync(template).isFile()) {
176
+ throw new Error(`Missing packaged prompt template: ${template}`);
177
+ }
178
+ if (existsSync(destination) && !statSync(destination).isFile()) {
179
+ throw new Error(`Project prompt is not a file: ${destination}`);
180
+ }
181
+ return { template, destination, exists: existsSync(destination) };
182
+ });
183
+ let promptParent = promptDirectory;
184
+ while (!existsSync(promptParent)) promptParent = dirname(promptParent);
185
+ if (!statSync(promptParent).isDirectory()) {
186
+ throw new Error(`Prompt directory ancestor is not a directory: ${promptParent}`);
187
+ }
188
+ const resolvedPromptDirectory = canonicalPath(promptDirectory);
189
+ const promptLocal = relative(repository, resolvedPromptDirectory);
190
+ if (promptLocal === ".." || promptLocal.startsWith("../") || isAbsolute(promptLocal)) {
191
+ throw new Error(
192
+ `Prompt directory must remain inside the invoking checkout: ${promptDirectory}`,
193
+ );
194
+ }
195
+ if (promptPlan.some((entry) => !entry.exists) && repository === target) {
196
+ throw new Error(
197
+ "Initialize missing prompts from an editable checkout, not the dedicated integration checkout",
198
+ );
199
+ }
168
200
  const matchingBranch = registeredWorktrees().filter(
169
201
  (worktree) => worktree.branch === `refs/heads/${integrationBranch}`,
170
202
  );
@@ -253,6 +285,15 @@ export async function runCodeless(args: string[]): Promise<void> {
253
285
  throw new Error(`Integration worktree target is occupied: ${target}`);
254
286
  }
255
287
 
288
+ for (const { template, destination, exists } of promptPlan) {
289
+ if (exists) {
290
+ console.log(`Preserved prompt: ${destination}`);
291
+ } else {
292
+ mkdirSync(dirname(destination), { recursive: true });
293
+ writeFileSync(destination, readFileSync(template), { flag: "wx" });
294
+ console.log(`Created prompt: ${destination}`);
295
+ }
296
+ }
256
297
  if (defaultWorkspace && !stateIgnored) {
257
298
  const currentIgnore = existsSync(ignoreFile) ? readFileSync(ignoreFile, "utf8") : "";
258
299
  appendFileSync(
@@ -386,9 +427,9 @@ export async function runCodeless(args: string[]): Promise<void> {
386
427
  return Number(rect["x"]) >= rightEdge && candidateTop < bottom && candidateBottom > top;
387
428
  })
388
429
  .sort((left, right) => Number(left["rect"]["x"]) - Number(right["rect"]["x"]));
389
- return candidates.length === 0
390
- ? undefined
391
- : string(candidates[0]!.pane["pane_id"], "right-hand pane id");
430
+ if (candidates.length === 0) return undefined;
431
+ if (candidates.length > 1) throw new Error("Planner layout has an ambiguous right-hand pane");
432
+ return string(candidates[0]!.pane["pane_id"], "right-hand pane id");
392
433
  }
393
434
 
394
435
  function requirePaneShell(pane: string, worktree: string): void {
@@ -443,7 +484,8 @@ export async function runCodeless(args: string[]): Promise<void> {
443
484
  id: string,
444
485
  slug: string,
445
486
  number: string,
446
- selection: { provider: string; model: string; thinking: string },
487
+ selection: { provider: string; model: string; thinking: string } | undefined,
488
+ kind: "initial" | "rework",
447
489
  ): Attempt {
448
490
  const timestamp = new Date().toISOString();
449
491
  return {
@@ -451,9 +493,10 @@ export async function runCodeless(args: string[]): Promise<void> {
451
493
  stream: slug,
452
494
  change: number,
453
495
  role: "implementer",
496
+ kind,
454
497
  startedAt: timestamp,
455
498
  endedAt: timestamp,
456
- selection,
499
+ ...(selection === undefined ? {} : { selection }),
457
500
  outcome: "unknown",
458
501
  toolCalls: 0,
459
502
  errorCount: 0,
@@ -469,6 +512,7 @@ export async function runCodeless(args: string[]): Promise<void> {
469
512
  attempt.stream !== fallback.stream ||
470
513
  attempt.change !== fallback.change ||
471
514
  attempt.role !== "implementer" ||
515
+ attempt.kind !== fallback.kind ||
472
516
  typeof attempt.startedAt !== "string" ||
473
517
  typeof attempt.endedAt !== "string" ||
474
518
  typeof attempt.outcome !== "string" ||
@@ -594,11 +638,6 @@ export async function runCodeless(args: string[]): Promise<void> {
594
638
  if (run("git", ["branch", "--show-current"], worktree).trim() !== branch)
595
639
  throw new Error(`${worktree} is not on ${branch}`);
596
640
  requireClean(worktree, branch);
597
- if (
598
- run("git", ["rev-parse", "HEAD"], worktree).trim() !==
599
- run("git", ["rev-parse", integrationBranch], worktree).trim()
600
- )
601
- throw new Error(`${branch} is not at the current ${integrationBranch} baseline`);
602
641
 
603
642
  const plannerPane = string(process.env["HERDR_PANE_ID"], "HERDR_PANE_ID");
604
643
  const agent = object(result(herdr(["agent", "get", plannerPane]))["agent"], "result.agent");
@@ -708,7 +747,7 @@ export async function runCodeless(args: string[]): Promise<void> {
708
747
  const selection = readProject(worktree).implementer;
709
748
  const attemptId = crypto.randomUUID();
710
749
  const reportPath = join(workspaceRoot, "metrics", slug, `.attempt-${attemptId}.json`);
711
- const fallbackAttempt = incompleteAttempt(attemptId, slug, number, selection);
750
+ const fallbackAttempt = incompleteAttempt(attemptId, slug, number, selection, "initial");
712
751
  observe("dispatch metrics", () => recordDispatch(workspaceRoot, slug, number));
713
752
  await validateRoleSelection("implementer", selection, worktree);
714
753
 
@@ -766,6 +805,7 @@ export async function runCodeless(args: string[]): Promise<void> {
766
805
  id: attemptId,
767
806
  stream: slug,
768
807
  change: number,
808
+ kind: "initial",
769
809
  path: reportPath,
770
810
  });
771
811
  const started = herdr([
@@ -822,41 +862,106 @@ export async function runCodeless(args: string[]): Promise<void> {
822
862
  console.log(JSON.stringify(attempt));
823
863
  }
824
864
 
825
- async function launchPlanner(slug: string): Promise<void> {
826
- if (process.env["HERDR_ENV"] !== "1") {
827
- throw new Error("Run codeless planner from the stream's Herdr-managed shell");
828
- }
829
- const documents = join(workspaceRoot, "stream", slug);
830
- const worktree = join(workspaceRoot, "worktree", slug);
831
- const branch = `stream/${slug}`;
832
- for (const file of ["planner.md", "change.md"]) {
833
- if (!existsSync(join(documents, file))) throw new Error(`Missing ${join(documents, file)}`);
834
- }
835
- if (canonicalPath(process.cwd()) !== worktree) {
836
- throw new Error(`Planner shell is in ${process.cwd()}, expected ${worktree}`);
837
- }
838
- if (run("git", ["branch", "--show-current"], worktree).trim() !== branch) {
865
+ function activeImplementer(changeArgument: string): {
866
+ change: string;
867
+ slug: string;
868
+ number: string;
869
+ worktree: string;
870
+ implementerName: string;
871
+ pane: string;
872
+ } {
873
+ if (process.env["HERDR_ENV"] !== "1")
874
+ throw new Error("Implementer control must run from a Herdr-managed planner");
875
+ const { change, slug, number, worktree, branch } = requireChange(changeArgument);
876
+ if (canonicalPath(process.cwd()) !== worktree)
877
+ throw new Error(`Implementer control cwd is ${process.cwd()}, expected ${worktree}`);
878
+ if (run("git", ["branch", "--show-current"], worktree).trim() !== branch)
839
879
  throw new Error(`${worktree} is not on ${branch}`);
840
- }
841
- const prompt = changePrompt(slug, documents, worktree);
842
- const selection = readProject(worktree).planner;
843
- await validateRoleSelection("planner", selection, worktree, plannerExtension);
844
- console.log(roleSelectionSummary("planner", selection));
845
- runVisible(
846
- "pi",
847
- [
848
- "--name",
849
- `${slug}-planner`,
850
- ...roleSelectionArguments(selection),
851
- "--extension",
852
- plannerExtension,
853
- "--prompt-template",
854
- promptDirectory(worktree),
855
- "--approve",
856
- activationPrompt(prompt),
857
- ],
858
- worktree,
880
+ const plannerPane = string(process.env["HERDR_PANE_ID"], "HERDR_PANE_ID");
881
+ const plannerProcesses = foregroundProcesses(paneProcessInfo(plannerPane));
882
+ if (
883
+ !plannerProcesses.some(
884
+ (process) => canonicalPath(string(process["cwd"], "planner cwd")) === worktree,
885
+ )
886
+ )
887
+ throw new Error(`Planner pane ${plannerPane} is not running in ${worktree}`);
888
+ const layout = object(
889
+ result(herdr(["pane", "layout", "--pane", plannerPane]))["layout"],
890
+ "result.layout",
859
891
  );
892
+ const pane = rightPane(layout, plannerPane);
893
+ if (pane === undefined) throw new Error("Planner has no right-hand implementer pane");
894
+ const implementerName = `${slug.replaceAll("-", "_")}_impl`;
895
+ const agent = object(result(herdr(["agent", "get", pane]))["agent"], "result.agent");
896
+ if (string(agent["name"], "result.agent.name") !== implementerName)
897
+ throw new Error(`Right-hand pane ${pane} is not implementer ${implementerName}`);
898
+ if (!["idle", "done"].includes(string(agent["agent_status"], "result.agent.agent_status")))
899
+ throw new Error(`Implementer ${implementerName} is not settled`);
900
+ const agentCwd = string(agent["foreground_cwd"] ?? agent["cwd"], "result.agent.foreground_cwd");
901
+ if (canonicalPath(agentCwd) !== worktree)
902
+ throw new Error(`Implementer ${implementerName} is in ${agentCwd}, expected ${worktree}`);
903
+ const processes = foregroundProcesses(paneProcessInfo(pane));
904
+ if (
905
+ !processes.some(
906
+ (process) => canonicalPath(string(process["cwd"], "implementer cwd")) === worktree,
907
+ )
908
+ )
909
+ throw new Error(`Implementer pane ${pane} is not running in ${worktree}`);
910
+ return { change, slug, number, worktree, implementerName, pane };
911
+ }
912
+
913
+ async function rework(changeArgument: string, feedback: string): Promise<void> {
914
+ if (feedback.trim().length === 0 || feedback.trim().length > 2_000)
915
+ throw new Error("Rework feedback must be concise non-empty text");
916
+ const { slug, number, implementerName } = activeImplementer(changeArgument);
917
+ const attemptId = crypto.randomUUID();
918
+ const reportPath = join(workspaceRoot, "metrics", slug, `.attempt-${attemptId}.json`);
919
+ const fallbackAttempt = incompleteAttempt(attemptId, slug, number, undefined, "rework");
920
+ const collection = JSON.stringify({
921
+ id: attemptId,
922
+ stream: slug,
923
+ change: number,
924
+ kind: "rework",
925
+ path: reportPath,
926
+ feedback: feedback.trim(),
927
+ });
928
+ run("herdr", [
929
+ "agent",
930
+ "prompt",
931
+ implementerName,
932
+ `/codeless-rework ${collection}`,
933
+ "--wait",
934
+ "--timeout",
935
+ "3600000",
936
+ ]);
937
+ let attempt = fallbackAttempt;
938
+ try {
939
+ attempt = collectedAttempt(JSON.parse(readFileSync(reportPath, "utf8")), fallbackAttempt);
940
+ if (attempt === fallbackAttempt) throw new Error("report did not match its rework attempt");
941
+ } catch (error) {
942
+ console.error(
943
+ `codeless: warning: could not collect implementer attempt: ${error instanceof Error ? error.message : String(error)}`,
944
+ );
945
+ } finally {
946
+ if (existsSync(reportPath)) unlinkSync(reportPath);
947
+ }
948
+ observe("implementer attempt", () => recordAttempt(workspaceRoot, slug, number, attempt));
949
+ console.log(JSON.stringify(attempt));
950
+ }
951
+
952
+ function finish(changeArgument: string): void {
953
+ const { slug, number, worktree, implementerName, pane } = activeImplementer(changeArgument);
954
+ run("herdr", [
955
+ "agent",
956
+ "prompt",
957
+ implementerName,
958
+ `/codeless-finish ${JSON.stringify({ stream: slug, change: number })}`,
959
+ "--wait",
960
+ "--timeout",
961
+ "30000",
962
+ ]);
963
+ requirePaneShell(pane, worktree);
964
+ console.log(JSON.stringify({ pane, worktree }));
860
965
  }
861
966
 
862
967
  function acquireLandSlot(lock: string, slug: string, base: string): "acquired" | "resumed" {
@@ -997,6 +1102,16 @@ export async function runCodeless(args: string[]): Promise<void> {
997
1102
  await dispatch(target);
998
1103
  return;
999
1104
  }
1105
+ if (action === "rework") {
1106
+ if (target === undefined || details.length !== 1) throw new Error(usage);
1107
+ await rework(target, details[0]!);
1108
+ return;
1109
+ }
1110
+ if (action === "finish") {
1111
+ if (target === undefined || details.length > 0) throw new Error(usage);
1112
+ finish(target);
1113
+ return;
1114
+ }
1000
1115
  if (action === "next") {
1001
1116
  if (target === undefined || details.length !== 1) throw new Error(usage);
1002
1117
  await nextChange(target, details[0]!);
@@ -1004,7 +1119,7 @@ export async function runCodeless(args: string[]): Promise<void> {
1004
1119
  }
1005
1120
  const slug = target;
1006
1121
  if (
1007
- (action !== "create" && action !== "open" && action !== "planner" && action !== "land") ||
1122
+ (action !== "create" && action !== "open" && action !== "land") ||
1008
1123
  slug === undefined ||
1009
1124
  !/^[a-z][a-z0-9-]{0,23}$/.test(slug) ||
1010
1125
  details.length > 0
@@ -1015,10 +1130,6 @@ export async function runCodeless(args: string[]): Promise<void> {
1015
1130
  land(slug);
1016
1131
  return;
1017
1132
  }
1018
- if (action === "planner") {
1019
- await launchPlanner(slug);
1020
- return;
1021
- }
1022
1133
  if (process.env["HERDR_ENV"] !== "1") {
1023
1134
  throw new Error("Run codeless from a Herdr-managed shell pane");
1024
1135
  }
@@ -1066,33 +1177,104 @@ export async function runCodeless(args: string[]): Promise<void> {
1066
1177
  opened = herdr(["worktree", "open", "--path", worktree, "--label", slug, "--no-focus"]);
1067
1178
  }
1068
1179
 
1180
+ const workspace = id(opened, "workspace", "workspace_id");
1181
+ const plannerPane = id(opened, "root_pane", "pane_id");
1182
+ if (plannerPane === process.env["HERDR_PANE_ID"]) {
1183
+ throw new Error(
1184
+ "Run codeless open from another Herdr shell; the planner pane must be available",
1185
+ );
1186
+ }
1187
+ const planner = `${slug.replaceAll("-", "_")}_planner`;
1188
+ const layout = object(
1189
+ result(herdr(["pane", "layout", "--pane", plannerPane]))["layout"],
1190
+ "result.layout",
1191
+ );
1192
+ const implementerPane = rightPane(layout, plannerPane);
1193
+ const panes = layout["panes"] as JsonObject[];
1194
+ if (panes.length !== (implementerPane === undefined ? 1 : 2)) {
1195
+ throw new Error(
1196
+ "Stream layout must contain only the planner and an optional right-hand implementer pane",
1197
+ );
1198
+ }
1199
+
1200
+ function requireLaunchShell(pane: string): void {
1201
+ const processes = foregroundProcesses(paneProcessInfo(pane));
1202
+ if (processes.length !== 1 || !isShell(processes[0]!)) {
1203
+ throw new Error(`Pane ${pane} must be an available shell before opening the stream`);
1204
+ }
1205
+ if (canonicalPath(string(processes[0]!["cwd"], "shell cwd")) !== worktree) {
1206
+ throw new Error(`Pane ${pane} shell is not in ${worktree}`);
1207
+ }
1208
+ }
1209
+
1210
+ function requireManagedPlanner(agent: JsonObject): void {
1211
+ if (
1212
+ agent["name"] !== planner ||
1213
+ agent["agent"] !== "pi" ||
1214
+ agent["interactive_ready"] !== true
1215
+ ) {
1216
+ throw new Error(
1217
+ `Planner pane ${plannerPane} must contain the Herdr-managed ${planner}; exit an unmanaged agent before reopening`,
1218
+ );
1219
+ }
1220
+ if (canonicalPath(string(agent["foreground_cwd"], "planner foreground cwd")) !== worktree) {
1221
+ throw new Error(`Planner ${planner} is not in ${worktree}`);
1222
+ }
1223
+ const session = object(agent["agent_session"], "planner agent_session");
1224
+ if (
1225
+ agent["screen_detection_skipped"] !== true ||
1226
+ session["source"] !== "herdr:pi" ||
1227
+ session["agent"] !== "pi"
1228
+ ) {
1229
+ throw new Error(
1230
+ "Planner requires Herdr's official Pi lifecycle integration; run herdr integration install pi before reopening",
1231
+ );
1232
+ }
1233
+ }
1234
+
1235
+ if (action === "create" || result(opened)["already_open"] === false) {
1236
+ requirePaneShell(plannerPane, worktree);
1237
+ }
1238
+ const plannerProcesses = foregroundProcesses(paneProcessInfo(plannerPane));
1239
+ if (!(plannerProcesses.length === 1 && isShell(plannerProcesses[0]!))) {
1240
+ requireManagedPlanner(
1241
+ object(result(herdr(["agent", "get", plannerPane]))["agent"], "result.agent"),
1242
+ );
1243
+ run("herdr", ["workspace", "focus", workspace]);
1244
+ console.log(`Focused existing planner ${planner}; its session and work remain unchanged.`);
1245
+ return;
1246
+ }
1247
+ requireLaunchShell(plannerPane);
1248
+ if (implementerPane !== undefined) requireLaunchShell(implementerPane);
1249
+
1069
1250
  console.log("Installing project dependencies...");
1070
1251
  const [install, ...installArgs] = readProject(worktree).install;
1071
1252
  run(install, installArgs, worktree);
1072
-
1073
1253
  const selection = readProject(worktree).planner;
1074
1254
  await validateRoleSelection("planner", selection, worktree, plannerExtension);
1075
1255
  console.log(roleSelectionSummary("planner", selection));
1076
1256
 
1077
- const workspace = id(opened, "workspace", "workspace_id");
1078
- const plannerPane = id(opened, "root_pane", "pane_id");
1079
- const split = herdr([
1080
- "pane",
1081
- "split",
1082
- "--pane",
1083
- plannerPane,
1084
- "--direction",
1085
- "right",
1086
- "--ratio",
1087
- "0.5",
1088
- "--cwd",
1089
- worktree,
1090
- "--no-focus",
1091
- ]);
1092
- id(split, "pane", "pane_id");
1093
-
1094
- const planner = `${slug.replaceAll("-", "_")}_planner`;
1095
- run("herdr", [
1257
+ // Recheck after installation/preflight; Herdr owns final interactive readiness.
1258
+ requireLaunchShell(plannerPane);
1259
+ if (implementerPane === undefined) {
1260
+ const split = herdr([
1261
+ "pane",
1262
+ "split",
1263
+ "--pane",
1264
+ plannerPane,
1265
+ "--direction",
1266
+ "right",
1267
+ "--ratio",
1268
+ "0.5",
1269
+ "--cwd",
1270
+ worktree,
1271
+ "--no-focus",
1272
+ ]);
1273
+ requirePaneShell(id(split, "pane", "pane_id"), worktree);
1274
+ } else {
1275
+ requireLaunchShell(implementerPane);
1276
+ }
1277
+ const started = herdr([
1096
1278
  "agent",
1097
1279
  "start",
1098
1280
  planner,
@@ -1110,10 +1292,11 @@ export async function runCodeless(args: string[]): Promise<void> {
1110
1292
  promptDirectory(worktree),
1111
1293
  "--approve",
1112
1294
  ]);
1295
+ requireManagedPlanner(object(result(started)["agent"], "result.agent"));
1113
1296
  run("herdr", [
1114
1297
  "agent",
1115
1298
  "prompt",
1116
- planner,
1299
+ plannerPane,
1117
1300
  activationPrompt(changePrompt(slug, documents, worktree)),
1118
1301
  ]);
1119
1302
  run("herdr", ["workspace", "focus", workspace]);
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
- "Project total",
209
- rows.flatMap(({ records }) => records),
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
  }