@popoverai/dotrequirements 0.26.0 → 0.26.1
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/dist/codebase-to-spec/area-name.d.ts +13 -0
- package/dist/codebase-to-spec/area-name.js +18 -0
- package/dist/codebase-to-spec/cache.d.ts +31 -0
- package/dist/codebase-to-spec/cache.js +29 -1
- package/dist/codebase-to-spec/compose.d.ts +7 -4
- package/dist/codebase-to-spec/compose.js +10 -21
- package/dist/codebase-to-spec/dispatch.js +1 -1
- package/dist/codebase-to-spec/present.js +7 -2
- package/dist/codebase-to-spec/prompts/planner-initial.d.ts +3 -2
- package/dist/codebase-to-spec/prompts/planner-initial.js +3 -2
- package/dist/codebase-to-spec/renumber.d.ts +52 -0
- package/dist/codebase-to-spec/renumber.js +105 -0
- package/dist/codebase-to-spec/schemas.d.ts +35 -240
- package/dist/codebase-to-spec/schemas.js +5 -173
- package/dist/commands/codebase-to-spec/dispatch-editor.js +1 -1
- package/dist/commands/codebase-to-spec/dispatch-spec.js +1 -1
- package/dist/commands/codebase-to-spec/index.js +7 -103
- package/dist/commands/codebase-to-spec/pack.js +8 -1
- package/dist/commands/codebase-to-spec/present-orchestrator.d.ts +3 -1
- package/dist/commands/codebase-to-spec/present-orchestrator.js +11 -4
- package/dist/templates/skills/codebase-to-spec/SKILL.md +10 -4
- package/package.json +1 -1
- package/dist/codebase-to-spec/edit-loop.d.ts +0 -54
- package/dist/codebase-to-spec/edit-loop.js +0 -195
- package/dist/codebase-to-spec/editor.d.ts +0 -54
- package/dist/codebase-to-spec/editor.js +0 -74
- package/dist/codebase-to-spec/fan-out.d.ts +0 -63
- package/dist/codebase-to-spec/fan-out.js +0 -215
- package/dist/codebase-to-spec/outline-review-loop.d.ts +0 -51
- package/dist/codebase-to-spec/outline-review-loop.js +0 -187
- package/dist/codebase-to-spec/planner.d.ts +0 -41
- package/dist/codebase-to-spec/planner.js +0 -76
- package/dist/codebase-to-spec/prompts/outline-reviewer.d.ts +0 -12
- package/dist/codebase-to-spec/prompts/outline-reviewer.js +0 -89
- package/dist/codebase-to-spec/slice.d.ts +0 -49
- package/dist/codebase-to-spec/slice.js +0 -111
- package/dist/codebase-to-spec/specifier.d.ts +0 -60
- package/dist/codebase-to-spec/specifier.js +0 -85
- package/dist/codebase-to-spec/summary.d.ts +0 -51
- package/dist/codebase-to-spec/summary.js +0 -183
- package/dist/commands/codebase-to-spec/compose.d.ts +0 -14
- package/dist/commands/codebase-to-spec/compose.js +0 -57
- package/dist/commands/codebase-to-spec/edit-loop.d.ts +0 -16
- package/dist/commands/codebase-to-spec/edit-loop.js +0 -83
- package/dist/commands/codebase-to-spec/fan-out.d.ts +0 -19
- package/dist/commands/codebase-to-spec/fan-out.js +0 -77
- package/dist/commands/codebase-to-spec/plan-loop.d.ts +0 -26
- package/dist/commands/codebase-to-spec/plan-loop.js +0 -105
- package/dist/commands/codebase-to-spec/present.d.ts +0 -26
- package/dist/commands/codebase-to-spec/present.js +0 -97
- package/dist/commands/codebase-to-spec/run.d.ts +0 -20
- package/dist/commands/codebase-to-spec/run.js +0 -86
- package/dist/commands/codebase-to-spec/specify-area.d.ts +0 -18
- package/dist/commands/codebase-to-spec/specify-area.js +0 -82
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Editor worker: revise the composed spec in place based on a reviewer's
|
|
3
|
-
* critique.
|
|
4
|
-
*
|
|
5
|
-
* Two modes:
|
|
6
|
-
* - apply: apply mechanical revisions verbatim (when verdict is `approved-with-revisions`)
|
|
7
|
-
* - revise: address the critique using the editor's own judgment (when verdict is `requires-another-review`)
|
|
8
|
-
*
|
|
9
|
-
* Requirements covered:
|
|
10
|
-
* - CTS-EDIT-3: approved-with-revisions triggers apply mode
|
|
11
|
-
* - CTS-EDIT-4: requires-another-review triggers revise mode
|
|
12
|
-
* - CTS-EDIT-5: editor operates on the cohesive document, reads codebase only on demand
|
|
13
|
-
*/
|
|
14
|
-
import { copyFileSync, existsSync, statSync } from "node:fs";
|
|
15
|
-
import { runClaude } from "./claude.js";
|
|
16
|
-
import { EDITOR_PROMPT } from "./prompts/editor.js";
|
|
17
|
-
function userMessage(ctx) {
|
|
18
|
-
const mode = ctx.mode === "apply" ? "APPLY" : "REVISE";
|
|
19
|
-
const lines = [
|
|
20
|
-
`Mode: ${mode}`,
|
|
21
|
-
``,
|
|
22
|
-
`Spec to edit (in place via Edit tool): ${ctx.outputSpecPath}`,
|
|
23
|
-
`Codebase pack (consult only on demand): ${ctx.fullPackPath}`,
|
|
24
|
-
``,
|
|
25
|
-
`## Reviewer critique`,
|
|
26
|
-
`\`\`\`json`,
|
|
27
|
-
JSON.stringify(ctx.critique, null, 2),
|
|
28
|
-
`\`\`\``,
|
|
29
|
-
``,
|
|
30
|
-
];
|
|
31
|
-
if (ctx.mode === "apply") {
|
|
32
|
-
lines.push(`You are in APPLY mode. The reviewer's verdict is \`approved-with-revisions\`. Apply each revision in the \`revisions\` array verbatim. Do not restructure. When done, output a one-line confirmation.`);
|
|
33
|
-
}
|
|
34
|
-
else {
|
|
35
|
-
lines.push(`You are in REVISE mode. The reviewer's verdict is \`requires-another-review\`. Address each finding using your judgment. Use the Edit tool for targeted changes. Consult the codebase only to verify specific findings before adding requirements.`);
|
|
36
|
-
}
|
|
37
|
-
return lines.join("\n");
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Run the editor. If outputSpecPath differs from inputSpecPath, copy first
|
|
41
|
-
* so the in-place edits land on the output.
|
|
42
|
-
*/
|
|
43
|
-
export async function runEditor(ctx) {
|
|
44
|
-
if (ctx.inputSpecPath !== ctx.outputSpecPath) {
|
|
45
|
-
copyFileSync(ctx.inputSpecPath, ctx.outputSpecPath);
|
|
46
|
-
}
|
|
47
|
-
const runner = ctx.runner ?? runClaude;
|
|
48
|
-
const result = await runner({
|
|
49
|
-
systemPrompt: EDITOR_PROMPT,
|
|
50
|
-
userMessage: userMessage(ctx),
|
|
51
|
-
model: ctx.model,
|
|
52
|
-
tools: ["Read", "Edit", "Grep", "Glob"],
|
|
53
|
-
addDirs: ctx.addDirs,
|
|
54
|
-
});
|
|
55
|
-
let wroteSpec = false;
|
|
56
|
-
let outputBytes = 0;
|
|
57
|
-
if (existsSync(ctx.outputSpecPath)) {
|
|
58
|
-
try {
|
|
59
|
-
outputBytes = statSync(ctx.outputSpecPath).size;
|
|
60
|
-
wroteSpec = outputBytes > 0;
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
// Treat as not written.
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return {
|
|
67
|
-
wroteSpec,
|
|
68
|
-
outputBytes,
|
|
69
|
-
exitCode: result.exitCode,
|
|
70
|
-
stdout: result.stdout,
|
|
71
|
-
stderr: result.stderr,
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
//# sourceMappingURL=editor.js.map
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Specifier fan-out orchestrator.
|
|
3
|
-
*
|
|
4
|
-
* For each area in the outline, extract a slice, spawn a specifier worker,
|
|
5
|
-
* and write the resulting partial to the cache. Workers run in parallel up
|
|
6
|
-
* to a bounded concurrency limit. On per-area failure (no partial written,
|
|
7
|
-
* empty partial), the worker is retried once; on second failure, the area
|
|
8
|
-
* is recorded as failed for surfacing to the user.
|
|
9
|
-
*
|
|
10
|
-
* Requirements covered:
|
|
11
|
-
* - CTS-SPEC-1.2: bounded concurrent execution
|
|
12
|
-
* - CTS-SPEC-5: failed/empty specifier output is detected and retried
|
|
13
|
-
* - CTS-OBSERVE-2.0: failure details are logged with area name + cause
|
|
14
|
-
* - CTS-RESUME-1.2: existing non-empty partial means the specifier is skipped
|
|
15
|
-
*/
|
|
16
|
-
import type { ClaudeRunner } from "./claude.js";
|
|
17
|
-
import type { ProgressEmitter } from "./progress.js";
|
|
18
|
-
import type { Area, Outline } from "./schemas.js";
|
|
19
|
-
export declare const DEFAULT_FAN_OUT_CONCURRENCY = 8;
|
|
20
|
-
export interface FanOutOptions {
|
|
21
|
-
outline: Outline;
|
|
22
|
-
/** Path to the uncompressed pack (slices come from here). */
|
|
23
|
-
fullPackPath: string;
|
|
24
|
-
/** Path resolver: given a sanitized area name, return the partial path. */
|
|
25
|
-
partialPathFor: (sanitizedAreaName: string) => string;
|
|
26
|
-
/** Path resolver: given a sanitized area name, return the slice path. */
|
|
27
|
-
slicePathFor: (sanitizedAreaName: string) => string;
|
|
28
|
-
/** Command (path + args) the specifier should run to invoke schema validation. Run before style-check. */
|
|
29
|
-
validateCommand: string;
|
|
30
|
-
/** Command (path + args) the specifier should run to invoke style-check. */
|
|
31
|
-
styleCheckCommand: string;
|
|
32
|
-
/** Directories the specifier needs filesystem access to. */
|
|
33
|
-
addDirs: string[];
|
|
34
|
-
concurrency?: number;
|
|
35
|
-
runner?: ClaudeRunner;
|
|
36
|
-
model?: string;
|
|
37
|
-
progress?: ProgressEmitter;
|
|
38
|
-
}
|
|
39
|
-
export interface AreaOutcome {
|
|
40
|
-
area: Area;
|
|
41
|
-
status: "completed" | "skipped-resume" | "failed";
|
|
42
|
-
/** Number of attempts made (1 = first attempt succeeded; 2 = retry succeeded; 2 with failed = retried and still failed). */
|
|
43
|
-
attempts: number;
|
|
44
|
-
/** Reason the area failed, if applicable. */
|
|
45
|
-
failureCause?: string;
|
|
46
|
-
/** Path to the partial (whether or not it was written successfully). */
|
|
47
|
-
partialPath: string;
|
|
48
|
-
}
|
|
49
|
-
export interface FanOutResult {
|
|
50
|
-
outcomes: AreaOutcome[];
|
|
51
|
-
/** True if every area produced a non-empty partial (either now or from cache). */
|
|
52
|
-
allSucceeded: boolean;
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Convert an area name to a stable filesystem-safe identifier.
|
|
56
|
-
* E.g., "Reading & Editing Files" → "reading-editing-files"
|
|
57
|
-
*/
|
|
58
|
-
export declare function sanitizeAreaName(name: string): string;
|
|
59
|
-
/**
|
|
60
|
-
* Run the fan-out: extract slices, invoke specifiers, retry on failure.
|
|
61
|
-
*/
|
|
62
|
-
export declare function runFanOut(options: FanOutOptions): Promise<FanOutResult>;
|
|
63
|
-
//# sourceMappingURL=fan-out.d.ts.map
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Specifier fan-out orchestrator.
|
|
3
|
-
*
|
|
4
|
-
* For each area in the outline, extract a slice, spawn a specifier worker,
|
|
5
|
-
* and write the resulting partial to the cache. Workers run in parallel up
|
|
6
|
-
* to a bounded concurrency limit. On per-area failure (no partial written,
|
|
7
|
-
* empty partial), the worker is retried once; on second failure, the area
|
|
8
|
-
* is recorded as failed for surfacing to the user.
|
|
9
|
-
*
|
|
10
|
-
* Requirements covered:
|
|
11
|
-
* - CTS-SPEC-1.2: bounded concurrent execution
|
|
12
|
-
* - CTS-SPEC-5: failed/empty specifier output is detected and retried
|
|
13
|
-
* - CTS-OBSERVE-2.0: failure details are logged with area name + cause
|
|
14
|
-
* - CTS-RESUME-1.2: existing non-empty partial means the specifier is skipped
|
|
15
|
-
*/
|
|
16
|
-
import { existsSync, mkdirSync, statSync, writeFileSync } from "node:fs";
|
|
17
|
-
import { dirname } from "node:path";
|
|
18
|
-
import { extractSlice } from "./slice.js";
|
|
19
|
-
import { runSpecifier } from "./specifier.js";
|
|
20
|
-
export const DEFAULT_FAN_OUT_CONCURRENCY = 8;
|
|
21
|
-
/**
|
|
22
|
-
* Convert an area name to a stable filesystem-safe identifier.
|
|
23
|
-
* E.g., "Reading & Editing Files" → "reading-editing-files"
|
|
24
|
-
*/
|
|
25
|
-
export function sanitizeAreaName(name) {
|
|
26
|
-
return (name
|
|
27
|
-
.replace(/[^A-Za-z0-9]+/g, "-")
|
|
28
|
-
.replace(/^-+|-+$/g, "")
|
|
29
|
-
.toLowerCase() || "area");
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Run a bounded-concurrency loop over items. Workers pull items off a shared
|
|
33
|
-
* counter; at most `limit` workers run at a time.
|
|
34
|
-
*/
|
|
35
|
-
async function withLimit(items, limit, fn) {
|
|
36
|
-
let next = 0;
|
|
37
|
-
async function worker() {
|
|
38
|
-
while (true) {
|
|
39
|
-
const i = next++;
|
|
40
|
-
if (i >= items.length)
|
|
41
|
-
return;
|
|
42
|
-
await fn(items[i], i);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
const workerCount = Math.min(Math.max(1, limit), items.length);
|
|
46
|
-
const workers = Array.from({ length: workerCount }, () => worker());
|
|
47
|
-
await Promise.all(workers);
|
|
48
|
-
}
|
|
49
|
-
function partialIsUsable(path) {
|
|
50
|
-
if (!existsSync(path))
|
|
51
|
-
return false;
|
|
52
|
-
try {
|
|
53
|
-
return statSync(path).size > 0;
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Run the fan-out: extract slices, invoke specifiers, retry on failure.
|
|
61
|
-
*/
|
|
62
|
-
export async function runFanOut(options) {
|
|
63
|
-
const { outline, fullPackPath, partialPathFor, slicePathFor, validateCommand, styleCheckCommand, addDirs, concurrency = DEFAULT_FAN_OUT_CONCURRENCY, runner, model, progress, } = options;
|
|
64
|
-
if (!existsSync(fullPackPath)) {
|
|
65
|
-
throw new Error(`Missing full pack at ${fullPackPath}`);
|
|
66
|
-
}
|
|
67
|
-
// Sanitization is lossy ("Read & Write" and "read/write" both become
|
|
68
|
-
// "read-write"). If two areas collide, they'd share a partial file path and
|
|
69
|
-
// silently overwrite each other downstream. Catch this up front.
|
|
70
|
-
const sanitizedNames = new Map();
|
|
71
|
-
for (const area of outline.areas) {
|
|
72
|
-
const key = sanitizeAreaName(area.name);
|
|
73
|
-
const bucket = sanitizedNames.get(key) ?? [];
|
|
74
|
-
bucket.push(area.name);
|
|
75
|
-
sanitizedNames.set(key, bucket);
|
|
76
|
-
}
|
|
77
|
-
const collisions = [...sanitizedNames.entries()].filter(([, names]) => names.length > 1);
|
|
78
|
-
if (collisions.length > 0) {
|
|
79
|
-
const detail = collisions
|
|
80
|
-
.map(([key, names]) => ` "${key}" ← ${names.map((n) => `"${n}"`).join(", ")}`)
|
|
81
|
-
.join("\n");
|
|
82
|
-
throw new Error(`Outline contains area names that collide after sanitization. Rename them so each produces a unique partial file path:\n${detail}`);
|
|
83
|
-
}
|
|
84
|
-
const outcomes = outline.areas.map((area) => ({
|
|
85
|
-
area,
|
|
86
|
-
status: "failed",
|
|
87
|
-
attempts: 0,
|
|
88
|
-
partialPath: partialPathFor(sanitizeAreaName(area.name)),
|
|
89
|
-
}));
|
|
90
|
-
// Ensure partials directory exists for each area.
|
|
91
|
-
for (const outcome of outcomes) {
|
|
92
|
-
mkdirSync(dirname(outcome.partialPath), { recursive: true });
|
|
93
|
-
}
|
|
94
|
-
await withLimit(outcomes, concurrency, async (outcome) => {
|
|
95
|
-
const sanitized = sanitizeAreaName(outcome.area.name);
|
|
96
|
-
const slicePath = slicePathFor(sanitized);
|
|
97
|
-
// CTS-RESUME-1.2: skip if a non-empty partial exists from a prior run.
|
|
98
|
-
if (partialIsUsable(outcome.partialPath)) {
|
|
99
|
-
outcome.status = "skipped-resume";
|
|
100
|
-
outcome.attempts = 0;
|
|
101
|
-
progress?.emit({
|
|
102
|
-
stage: "specify",
|
|
103
|
-
step: outcome.area.prefix,
|
|
104
|
-
message: `(resume) skipping ${outcome.area.name} — partial exists`,
|
|
105
|
-
});
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
// Extract this area's slice from the full pack.
|
|
109
|
-
if (outcome.area.files.length === 0) {
|
|
110
|
-
// No files assigned — write a stub partial so compose has something.
|
|
111
|
-
writeFileSync(outcome.partialPath, "_(no files assigned to this area)_\n", "utf-8");
|
|
112
|
-
outcome.status = "failed";
|
|
113
|
-
outcome.attempts = 0;
|
|
114
|
-
outcome.failureCause = "no files assigned in outline";
|
|
115
|
-
progress?.emit({
|
|
116
|
-
stage: "specify",
|
|
117
|
-
step: outcome.area.prefix,
|
|
118
|
-
message: `!! ${outcome.area.name} has no files; marking as failed`,
|
|
119
|
-
});
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
let sliceMatched = 0;
|
|
123
|
-
try {
|
|
124
|
-
const sliceResult = extractSlice(fullPackPath, slicePath, outcome.area.files);
|
|
125
|
-
sliceMatched = sliceResult.matched;
|
|
126
|
-
}
|
|
127
|
-
catch (err) {
|
|
128
|
-
outcome.failureCause = `slice extraction failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
129
|
-
progress?.emit({
|
|
130
|
-
stage: "specify",
|
|
131
|
-
step: outcome.area.prefix,
|
|
132
|
-
message: `!! ${outcome.failureCause}`,
|
|
133
|
-
});
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
if (sliceMatched === 0) {
|
|
137
|
-
progress?.emit({
|
|
138
|
-
stage: "specify",
|
|
139
|
-
step: outcome.area.prefix,
|
|
140
|
-
message: `(warning) slice for ${outcome.area.name} matched 0 files; specifier will need to fall back to full-pack reads`,
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
// Attempt 1
|
|
144
|
-
progress?.emit({
|
|
145
|
-
stage: "specify",
|
|
146
|
-
step: outcome.area.prefix,
|
|
147
|
-
message: `Running specifier for ${outcome.area.name} (attempt 1/2)`,
|
|
148
|
-
});
|
|
149
|
-
outcome.attempts = 1;
|
|
150
|
-
const r1 = await runSpecifier({
|
|
151
|
-
outline,
|
|
152
|
-
area: outcome.area,
|
|
153
|
-
slicePath,
|
|
154
|
-
fullPackPath,
|
|
155
|
-
partialPath: outcome.partialPath,
|
|
156
|
-
validateCommand,
|
|
157
|
-
styleCheckCommand,
|
|
158
|
-
addDirs,
|
|
159
|
-
runner,
|
|
160
|
-
model,
|
|
161
|
-
});
|
|
162
|
-
if (r1.wrotePartial) {
|
|
163
|
-
outcome.status = "completed";
|
|
164
|
-
progress?.emit({
|
|
165
|
-
stage: "specify",
|
|
166
|
-
step: outcome.area.prefix,
|
|
167
|
-
message: `${outcome.area.name} done (${r1.partialBytes} bytes)`,
|
|
168
|
-
});
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
// Attempt 2 (retry once on no/empty partial)
|
|
172
|
-
progress?.emit({
|
|
173
|
-
stage: "specify",
|
|
174
|
-
step: outcome.area.prefix,
|
|
175
|
-
message: `!! ${outcome.area.name} produced no partial; retrying (attempt 2/2)`,
|
|
176
|
-
});
|
|
177
|
-
outcome.attempts = 2;
|
|
178
|
-
const r2 = await runSpecifier({
|
|
179
|
-
outline,
|
|
180
|
-
area: outcome.area,
|
|
181
|
-
slicePath,
|
|
182
|
-
fullPackPath,
|
|
183
|
-
partialPath: outcome.partialPath,
|
|
184
|
-
validateCommand,
|
|
185
|
-
styleCheckCommand,
|
|
186
|
-
addDirs,
|
|
187
|
-
runner,
|
|
188
|
-
model,
|
|
189
|
-
});
|
|
190
|
-
if (r2.wrotePartial) {
|
|
191
|
-
outcome.status = "completed";
|
|
192
|
-
progress?.emit({
|
|
193
|
-
stage: "specify",
|
|
194
|
-
step: outcome.area.prefix,
|
|
195
|
-
message: `${outcome.area.name} done on retry (${r2.partialBytes} bytes)`,
|
|
196
|
-
});
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
// Two failures — record and continue. CTS-SPEC-5 says other areas still proceed.
|
|
200
|
-
outcome.status = "failed";
|
|
201
|
-
outcome.failureCause =
|
|
202
|
-
r2.exitCode !== 0
|
|
203
|
-
? `specifier exited with code ${r2.exitCode}: ${r2.stderr.slice(0, 200)}`
|
|
204
|
-
: "specifier wrote no partial (or empty partial) on both attempts";
|
|
205
|
-
writeFileSync(outcome.partialPath, `_(missing partial — specifier failed after retry: ${outcome.failureCause})_\n`, "utf-8");
|
|
206
|
-
progress?.emit({
|
|
207
|
-
stage: "specify",
|
|
208
|
-
step: outcome.area.prefix,
|
|
209
|
-
message: `!! ${outcome.area.name} failed: ${outcome.failureCause}`,
|
|
210
|
-
});
|
|
211
|
-
});
|
|
212
|
-
const allSucceeded = outcomes.every((o) => o.status === "completed" || o.status === "skipped-resume");
|
|
213
|
-
return { outcomes, allSucceeded };
|
|
214
|
-
}
|
|
215
|
-
//# sourceMappingURL=fan-out.js.map
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Outline review loop: planner → reviewer → revise → reviewer → ... → approved.
|
|
3
|
-
*
|
|
4
|
-
* The reviewer runs in a SINGLE stateful claude session across turns (so it
|
|
5
|
-
* can compare prior outlines to the latest one). The planner is stateless —
|
|
6
|
-
* each invocation is fresh.
|
|
7
|
-
*
|
|
8
|
-
* Requirements covered:
|
|
9
|
-
* - CTS-PLAN-2: stateful reviewer session
|
|
10
|
-
* - CTS-PLAN-3: approved → proceed
|
|
11
|
-
* - CTS-PLAN-4: approved-with-revisions → one revision pass, then proceed
|
|
12
|
-
* - CTS-PLAN-5: requires-another-review → revision loop
|
|
13
|
-
* - CTS-PLAN-6: max-turns + convergence nudge
|
|
14
|
-
*/
|
|
15
|
-
import { type ClaudeRunner } from "./claude.js";
|
|
16
|
-
import type { ProgressEmitter } from "./progress.js";
|
|
17
|
-
import { type Outline, type OutlineReview } from "./schemas.js";
|
|
18
|
-
export declare const DEFAULT_OUTLINE_LOOP_MAX_TURNS = 3;
|
|
19
|
-
export interface OutlineReviewLoopOptions {
|
|
20
|
-
overviewPath: string;
|
|
21
|
-
addDirs: string[];
|
|
22
|
-
maxTurns?: number;
|
|
23
|
-
runner?: ClaudeRunner;
|
|
24
|
-
model?: string;
|
|
25
|
-
progress?: ProgressEmitter;
|
|
26
|
-
/** Override session id (for tests and reproducibility). Otherwise random. */
|
|
27
|
-
sessionId?: string;
|
|
28
|
-
}
|
|
29
|
-
export interface OutlineReviewLoopResult {
|
|
30
|
-
/** The final outline after the loop converges (or hits max-turns). */
|
|
31
|
-
outline: Outline;
|
|
32
|
-
/** Per-turn outline snapshots (turn 1 is the initial, then each revision). */
|
|
33
|
-
outlinesByTurn: Outline[];
|
|
34
|
-
/** Per-turn reviewer results. */
|
|
35
|
-
reviewsByTurn: OutlineReview[];
|
|
36
|
-
/** The final reviewer verdict. */
|
|
37
|
-
finalVerdict: OutlineReview["verdict"];
|
|
38
|
-
/** Number of review turns used (= length of reviewsByTurn). */
|
|
39
|
-
turnsUsed: number;
|
|
40
|
-
/** True if the loop hit max-turns without converging. */
|
|
41
|
-
hitMaxTurns: boolean;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Run the outline review loop end-to-end.
|
|
45
|
-
*
|
|
46
|
-
* Returns the final outline (after applying mechanical revisions if the
|
|
47
|
-
* verdict was `approved-with-revisions`), the per-turn trace, and the final
|
|
48
|
-
* verdict.
|
|
49
|
-
*/
|
|
50
|
-
export declare function runOutlineReviewLoop(options: OutlineReviewLoopOptions): Promise<OutlineReviewLoopResult>;
|
|
51
|
-
//# sourceMappingURL=outline-review-loop.d.ts.map
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Outline review loop: planner → reviewer → revise → reviewer → ... → approved.
|
|
3
|
-
*
|
|
4
|
-
* The reviewer runs in a SINGLE stateful claude session across turns (so it
|
|
5
|
-
* can compare prior outlines to the latest one). The planner is stateless —
|
|
6
|
-
* each invocation is fresh.
|
|
7
|
-
*
|
|
8
|
-
* Requirements covered:
|
|
9
|
-
* - CTS-PLAN-2: stateful reviewer session
|
|
10
|
-
* - CTS-PLAN-3: approved → proceed
|
|
11
|
-
* - CTS-PLAN-4: approved-with-revisions → one revision pass, then proceed
|
|
12
|
-
* - CTS-PLAN-5: requires-another-review → revision loop
|
|
13
|
-
* - CTS-PLAN-6: max-turns + convergence nudge
|
|
14
|
-
*/
|
|
15
|
-
import { randomUUID } from "node:crypto";
|
|
16
|
-
import { runClaude } from "./claude.js";
|
|
17
|
-
import { runInitialPlanner, runRevisingPlanner, } from "./planner.js";
|
|
18
|
-
import { OUTLINE_REVIEWER_PROMPT } from "./prompts/outline-reviewer.js";
|
|
19
|
-
import { OUTLINE_REVIEW_JSON_SCHEMA, parseOutlineReview, } from "./schemas.js";
|
|
20
|
-
export const DEFAULT_OUTLINE_LOOP_MAX_TURNS = 3;
|
|
21
|
-
function reviewerUserMessageInitial(overviewPath, outline) {
|
|
22
|
-
return [
|
|
23
|
-
`Initial outline review.`,
|
|
24
|
-
``,
|
|
25
|
-
`The compressed packed codebase is at: ${overviewPath}`,
|
|
26
|
-
``,
|
|
27
|
-
`## Outline`,
|
|
28
|
-
`\`\`\`json`,
|
|
29
|
-
JSON.stringify(outline, null, 2),
|
|
30
|
-
`\`\`\``,
|
|
31
|
-
``,
|
|
32
|
-
`Read the codebase if you need to verify file paths or coverage gaps, then produce your critique JSON.`,
|
|
33
|
-
].join("\n");
|
|
34
|
-
}
|
|
35
|
-
function reviewerUserMessageRevised(turn, overviewPath, outline, nudge) {
|
|
36
|
-
const parts = [
|
|
37
|
-
`Review turn ${turn}. The planner has revised the outline based on your prior feedback.`,
|
|
38
|
-
``,
|
|
39
|
-
];
|
|
40
|
-
if (nudge) {
|
|
41
|
-
parts.push(nudge, ``);
|
|
42
|
-
}
|
|
43
|
-
parts.push(`The codebase pack at ${overviewPath} is unchanged.`, ``, `## Revised outline`, `\`\`\`json`, JSON.stringify(outline, null, 2), `\`\`\``, ``, `Compare this outline to the prior one. Report on what was fixed, what is still open, and what (if anything) was introduced as a new issue. Then produce your critique JSON.`);
|
|
44
|
-
return parts.join("\n");
|
|
45
|
-
}
|
|
46
|
-
function convergenceNudge(turn, maxTurns) {
|
|
47
|
-
return `CONVERGENCE NUDGE: This is review turn ${turn} of ${maxTurns} maximum. Please prioritize convergence. Your job is still to only approve a genuinely-ready outline, but if it is close, lean toward \`approved-with-revisions\` over \`requires-another-review\`. Reserve \`requires-another-review\` for genuine structural problems that the planner has not addressed despite prior feedback.`;
|
|
48
|
-
}
|
|
49
|
-
async function callReviewer(args) {
|
|
50
|
-
const result = await args.runner({
|
|
51
|
-
systemPrompt: OUTLINE_REVIEWER_PROMPT,
|
|
52
|
-
userMessage: args.userMessage,
|
|
53
|
-
model: args.model,
|
|
54
|
-
tools: ["Read", "Grep"],
|
|
55
|
-
addDirs: args.addDirs,
|
|
56
|
-
jsonSchema: OUTLINE_REVIEW_JSON_SCHEMA,
|
|
57
|
-
session: {
|
|
58
|
-
kind: args.isFirstTurn ? "fresh" : "resume",
|
|
59
|
-
sessionId: args.sessionId,
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
if (result.exitCode !== 0) {
|
|
63
|
-
throw new Error(`Outline reviewer failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`);
|
|
64
|
-
}
|
|
65
|
-
// On parse failure, retry the same turn once before failing.
|
|
66
|
-
// CTS-PLAN-2 requirement: "on validation failure the CLI logs the parse error
|
|
67
|
-
// and retries the same turn once before failing the loop"
|
|
68
|
-
try {
|
|
69
|
-
return parseOutlineReview(result.stdout);
|
|
70
|
-
}
|
|
71
|
-
catch (err) {
|
|
72
|
-
// Retry once
|
|
73
|
-
const retry = await args.runner({
|
|
74
|
-
systemPrompt: OUTLINE_REVIEWER_PROMPT,
|
|
75
|
-
userMessage: `${args.userMessage}\n\n(Your prior output failed schema validation: ${err instanceof Error ? err.message : String(err)}. Please try again, emitting JSON only.)`,
|
|
76
|
-
model: args.model,
|
|
77
|
-
tools: ["Read", "Grep"],
|
|
78
|
-
addDirs: args.addDirs,
|
|
79
|
-
jsonSchema: OUTLINE_REVIEW_JSON_SCHEMA,
|
|
80
|
-
session: { kind: "resume", sessionId: args.sessionId },
|
|
81
|
-
});
|
|
82
|
-
if (retry.exitCode !== 0) {
|
|
83
|
-
throw new Error(`Outline reviewer retry failed (exit ${retry.exitCode}): ${retry.stderr || retry.stdout}`);
|
|
84
|
-
}
|
|
85
|
-
return parseOutlineReview(retry.stdout);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Run the outline review loop end-to-end.
|
|
90
|
-
*
|
|
91
|
-
* Returns the final outline (after applying mechanical revisions if the
|
|
92
|
-
* verdict was `approved-with-revisions`), the per-turn trace, and the final
|
|
93
|
-
* verdict.
|
|
94
|
-
*/
|
|
95
|
-
export async function runOutlineReviewLoop(options) {
|
|
96
|
-
const { overviewPath, addDirs, maxTurns = DEFAULT_OUTLINE_LOOP_MAX_TURNS, runner = runClaude, model, progress, sessionId = randomUUID(), } = options;
|
|
97
|
-
const plannerCtx = { overviewPath, addDirs, runner, model };
|
|
98
|
-
// Turn 1 — initial planner + initial reviewer
|
|
99
|
-
progress?.emit({
|
|
100
|
-
stage: "plan",
|
|
101
|
-
step: "planner-initial",
|
|
102
|
-
message: "Generating initial outline",
|
|
103
|
-
});
|
|
104
|
-
const outline1 = await runInitialPlanner(plannerCtx);
|
|
105
|
-
const outlinesByTurn = [outline1];
|
|
106
|
-
const reviewsByTurn = [];
|
|
107
|
-
progress?.emit({
|
|
108
|
-
stage: "outline-review",
|
|
109
|
-
step: "turn-1",
|
|
110
|
-
message: `Reviewing initial outline (${outline1.areas.length} areas)`,
|
|
111
|
-
});
|
|
112
|
-
let currentOutline = outline1;
|
|
113
|
-
let review = await callReviewer({
|
|
114
|
-
runner,
|
|
115
|
-
sessionId,
|
|
116
|
-
isFirstTurn: true,
|
|
117
|
-
userMessage: reviewerUserMessageInitial(overviewPath, outline1),
|
|
118
|
-
addDirs,
|
|
119
|
-
model,
|
|
120
|
-
});
|
|
121
|
-
reviewsByTurn.push(review);
|
|
122
|
-
progress?.emit({
|
|
123
|
-
stage: "outline-review",
|
|
124
|
-
step: "turn-1",
|
|
125
|
-
message: `Verdict: ${review.verdict}`,
|
|
126
|
-
data: { verdict: review.verdict },
|
|
127
|
-
});
|
|
128
|
-
let turn = 1;
|
|
129
|
-
while (review.verdict === "requires-another-review" && turn < maxTurns) {
|
|
130
|
-
turn++;
|
|
131
|
-
progress?.emit({
|
|
132
|
-
stage: "plan",
|
|
133
|
-
step: `revise-turn-${turn}`,
|
|
134
|
-
message: "Revising outline based on critique",
|
|
135
|
-
});
|
|
136
|
-
currentOutline = await runRevisingPlanner(plannerCtx, currentOutline, review);
|
|
137
|
-
outlinesByTurn.push(currentOutline);
|
|
138
|
-
const nudge = turn >= maxTurns - 1 && turn !== maxTurns
|
|
139
|
-
? convergenceNudge(turn, maxTurns)
|
|
140
|
-
: undefined;
|
|
141
|
-
progress?.emit({
|
|
142
|
-
stage: "outline-review",
|
|
143
|
-
step: `turn-${turn}`,
|
|
144
|
-
message: `Reviewing revised outline (turn ${turn}/${maxTurns})`,
|
|
145
|
-
data: { turn, maxTurns, nudge: !!nudge },
|
|
146
|
-
});
|
|
147
|
-
review = await callReviewer({
|
|
148
|
-
runner,
|
|
149
|
-
sessionId,
|
|
150
|
-
isFirstTurn: false,
|
|
151
|
-
userMessage: reviewerUserMessageRevised(turn, overviewPath, currentOutline, nudge),
|
|
152
|
-
addDirs,
|
|
153
|
-
model,
|
|
154
|
-
});
|
|
155
|
-
reviewsByTurn.push(review);
|
|
156
|
-
progress?.emit({
|
|
157
|
-
stage: "outline-review",
|
|
158
|
-
step: `turn-${turn}`,
|
|
159
|
-
message: `Verdict: ${review.verdict}`,
|
|
160
|
-
data: { verdict: review.verdict },
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
const hitMaxTurns = review.verdict === "requires-another-review" && turn >= maxTurns;
|
|
164
|
-
// If the verdict ended on approved-with-revisions, run the planner once
|
|
165
|
-
// more to apply the listed revisions. Same agent as the revise loop; the
|
|
166
|
-
// difference is that we proceed to fan-out instead of looping back to
|
|
167
|
-
// review.
|
|
168
|
-
if (review.verdict === "approved-with-revisions" &&
|
|
169
|
-
review.revisions.length > 0) {
|
|
170
|
-
progress?.emit({
|
|
171
|
-
stage: "plan",
|
|
172
|
-
step: "apply-revisions",
|
|
173
|
-
message: `Applying ${review.revisions.length} revision(s)`,
|
|
174
|
-
});
|
|
175
|
-
currentOutline = await runRevisingPlanner(plannerCtx, currentOutline, review);
|
|
176
|
-
outlinesByTurn.push(currentOutline);
|
|
177
|
-
}
|
|
178
|
-
return {
|
|
179
|
-
outline: currentOutline,
|
|
180
|
-
outlinesByTurn,
|
|
181
|
-
reviewsByTurn,
|
|
182
|
-
finalVerdict: review.verdict,
|
|
183
|
-
turnsUsed: turn,
|
|
184
|
-
hitMaxTurns,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
//# sourceMappingURL=outline-review-loop.js.map
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Planner functions (initial and revise).
|
|
3
|
-
*
|
|
4
|
-
* Two modes correspond to two places the planner is invoked:
|
|
5
|
-
* - initial: first outline from the compressed pack (CTS-PLAN-1)
|
|
6
|
-
* - revise: address reviewer feedback — works for both
|
|
7
|
-
* `requires-another-review` (CTS-PLAN-5) and
|
|
8
|
-
* `approved-with-revisions` (CTS-PLAN-4). The orchestration
|
|
9
|
-
* decides whether to loop back to the reviewer or proceed to
|
|
10
|
-
* fan-out.
|
|
11
|
-
*
|
|
12
|
-
* Each mode is stateless — a fresh claude -p session per call.
|
|
13
|
-
*/
|
|
14
|
-
import { type ClaudeRunner } from "./claude.js";
|
|
15
|
-
import { type Outline, type OutlineReview } from "./schemas.js";
|
|
16
|
-
export interface PlannerContext {
|
|
17
|
-
/** Path to the compressed pack file (readable by the spawned agent). */
|
|
18
|
-
overviewPath: string;
|
|
19
|
-
/** Directories the agent should have read access to (e.g., the cache dir). */
|
|
20
|
-
addDirs: string[];
|
|
21
|
-
/** Optional claude runner override (for tests). */
|
|
22
|
-
runner?: ClaudeRunner;
|
|
23
|
-
/** Optional model override. */
|
|
24
|
-
model?: string;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Run the planner in initial mode and return a parsed outline.
|
|
28
|
-
* Requirement: CTS-PLAN-1
|
|
29
|
-
*/
|
|
30
|
-
export declare function runInitialPlanner(ctx: PlannerContext): Promise<Outline>;
|
|
31
|
-
/**
|
|
32
|
-
* Run the planner in revise mode — produce a new outline that addresses the
|
|
33
|
-
* reviewer's output. Handles both verdicts that trigger another planner pass:
|
|
34
|
-
* `requires-another-review` (categorized findings, judgment-based) and
|
|
35
|
-
* `approved-with-revisions` (explicit revisions list). The same prompt handles
|
|
36
|
-
* both shapes; the prompt switches behavior based on what's in the review.
|
|
37
|
-
*
|
|
38
|
-
* Requirements: CTS-PLAN-4, CTS-PLAN-5
|
|
39
|
-
*/
|
|
40
|
-
export declare function runRevisingPlanner(ctx: PlannerContext, priorOutline: Outline, review: OutlineReview): Promise<Outline>;
|
|
41
|
-
//# sourceMappingURL=planner.d.ts.map
|