@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/README.md +79 -33
- package/extension/implementer-reporting.js +78 -10
- package/extension/planner.js +135 -15
- package/package.json +2 -1
- package/prompts/change.md +40 -0
- package/prompts/commit.md +37 -0
- package/prompts/implement.md +17 -0
- package/prompts/review.md +20 -0
- package/spec/workflow.md +215 -149
- package/src/attempt.ts +4 -1
- package/src/cli.ts +254 -71
- package/src/metrics.ts +90 -4
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
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
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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 !== "
|
|
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
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|