opencode-akane 0.1.1 → 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 +25 -0
- package/dist/artifacts.d.ts +2 -1
- package/dist/artifacts.js +1 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +10 -0
- package/dist/plugin.js +12 -0
- package/dist/tools/akane-implement.d.ts +15 -0
- package/dist/tools/akane-implement.js +53 -0
- package/dist/tools/akane-plan-review.d.ts +15 -0
- package/dist/tools/akane-plan-review.js +53 -0
- package/dist/tools/akane-plan.d.ts +15 -0
- package/dist/tools/akane-plan.js +50 -0
- package/dist/tools/akane-review.d.ts +21 -0
- package/dist/tools/akane-review.js +57 -0
- package/dist/tools/akane-run.d.ts +24 -0
- package/dist/tools/akane-run.js +66 -0
- package/dist/tools/akane-synthesize.d.ts +15 -0
- package/dist/tools/akane-synthesize.js +53 -0
- package/dist/types.d.ts +5 -0
- package/dist/workflow.d.ts +55 -0
- package/dist/workflow.js +727 -0
- package/package.json +1 -1
package/dist/workflow.js
ADDED
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { AKANE_SERVICE_NAME, AKANE_TOOL_IDS, } from "./constants.js";
|
|
5
|
+
import { ensureArtifactLayout, resolveProjectRoot, resolveStageArtifactPath, writeStageArtifact, } from "./artifacts.js";
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
const STAGE_ROLE_MAP = {
|
|
8
|
+
plan: "planner",
|
|
9
|
+
"plan-review": "plan_reviewer",
|
|
10
|
+
"implementation-context": "implementer",
|
|
11
|
+
"review-codex": "reviewer_codex",
|
|
12
|
+
"review-claude": "reviewer_claude",
|
|
13
|
+
"final-synthesis": "synthesizer",
|
|
14
|
+
};
|
|
15
|
+
const DEFAULT_ROLE_AGENTS = {
|
|
16
|
+
planner: "plan",
|
|
17
|
+
plan_reviewer: "general",
|
|
18
|
+
implementer: "build",
|
|
19
|
+
reviewer_codex: "general",
|
|
20
|
+
reviewer_claude: "general",
|
|
21
|
+
synthesizer: "general",
|
|
22
|
+
};
|
|
23
|
+
const IMPLEMENT_TIMEOUT_MS = 15 * 60 * 1000;
|
|
24
|
+
const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
25
|
+
const POLL_INTERVAL_MS = 800;
|
|
26
|
+
const STABILITY_POLLS_REQUIRED = 2;
|
|
27
|
+
function nowIso() {
|
|
28
|
+
return new Date().toISOString();
|
|
29
|
+
}
|
|
30
|
+
function truncate(input, length) {
|
|
31
|
+
if (input.length <= length) {
|
|
32
|
+
return input;
|
|
33
|
+
}
|
|
34
|
+
return `${input.slice(0, Math.max(0, length - 1)).trimEnd()}...`;
|
|
35
|
+
}
|
|
36
|
+
function stageTitle(stage) {
|
|
37
|
+
return stage
|
|
38
|
+
.split("-")
|
|
39
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
40
|
+
.join(" ");
|
|
41
|
+
}
|
|
42
|
+
function parseModelRef(modelRef) {
|
|
43
|
+
const [providerID, ...rest] = modelRef.split("/");
|
|
44
|
+
const modelID = rest.join("/").trim();
|
|
45
|
+
if (!providerID || !modelID) {
|
|
46
|
+
throw new Error(`Invalid Akane model mapping "${modelRef}". Expected provider/model.`);
|
|
47
|
+
}
|
|
48
|
+
return { providerID, modelID };
|
|
49
|
+
}
|
|
50
|
+
function resultErrorMessage(result, fallback) {
|
|
51
|
+
if (result.error instanceof Error) {
|
|
52
|
+
return result.error.message;
|
|
53
|
+
}
|
|
54
|
+
if (typeof result.error === "string") {
|
|
55
|
+
return result.error;
|
|
56
|
+
}
|
|
57
|
+
if (result.error) {
|
|
58
|
+
return JSON.stringify(result.error);
|
|
59
|
+
}
|
|
60
|
+
return fallback;
|
|
61
|
+
}
|
|
62
|
+
function requireResultData(result, action) {
|
|
63
|
+
if (!result.error && result.data !== undefined && result.data !== null) {
|
|
64
|
+
return result.data;
|
|
65
|
+
}
|
|
66
|
+
throw new Error(`${action} failed: ${resultErrorMessage(result, "unknown error")}`);
|
|
67
|
+
}
|
|
68
|
+
function extractAssistantText(parts) {
|
|
69
|
+
const text = parts
|
|
70
|
+
.filter((part) => part.type === "text")
|
|
71
|
+
.map((part) => part.text.trim())
|
|
72
|
+
.filter(Boolean)
|
|
73
|
+
.join("\n\n")
|
|
74
|
+
.trim();
|
|
75
|
+
if (text) {
|
|
76
|
+
return text;
|
|
77
|
+
}
|
|
78
|
+
return "";
|
|
79
|
+
}
|
|
80
|
+
function findLatestAssistantMessage(messages) {
|
|
81
|
+
const assistants = messages
|
|
82
|
+
.filter((message) => message.info.role === "assistant")
|
|
83
|
+
.sort((left, right) => (left.info.time.created ?? 0) - (right.info.time.created ?? 0));
|
|
84
|
+
return assistants.at(-1) ?? null;
|
|
85
|
+
}
|
|
86
|
+
function makeToolRestrictions(allowWorkspaceMutation) {
|
|
87
|
+
const restrictions = Object.fromEntries(AKANE_TOOL_IDS.map((toolID) => [toolID, false]));
|
|
88
|
+
if (!allowWorkspaceMutation) {
|
|
89
|
+
for (const toolID of ["bash", "edit", "patch", "write", "task", "question"]) {
|
|
90
|
+
restrictions[toolID] = false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return restrictions;
|
|
94
|
+
}
|
|
95
|
+
async function resolveAgentName(client, directory, role) {
|
|
96
|
+
const preferred = DEFAULT_ROLE_AGENTS[role];
|
|
97
|
+
if (!preferred) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const result = await client.app.agents({
|
|
102
|
+
query: { directory },
|
|
103
|
+
});
|
|
104
|
+
const agents = requireResultData(result, "agent list");
|
|
105
|
+
return agents.some((agent) => agent.name === preferred) ? preferred : undefined;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function createStageSession(input) {
|
|
112
|
+
const result = await input.client.session.create({
|
|
113
|
+
body: {
|
|
114
|
+
parentID: input.parentSessionID,
|
|
115
|
+
title: input.title,
|
|
116
|
+
},
|
|
117
|
+
query: {
|
|
118
|
+
directory: input.directory,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
return requireResultData(result, "session creation");
|
|
122
|
+
}
|
|
123
|
+
async function waitForSessionCompletion(input) {
|
|
124
|
+
const startedAt = Date.now();
|
|
125
|
+
let stablePolls = 0;
|
|
126
|
+
let lastMessageCount = -1;
|
|
127
|
+
while (Date.now() - startedAt < input.timeoutMs) {
|
|
128
|
+
if (input.abort.aborted) {
|
|
129
|
+
throw new Error("Akane stage aborted.");
|
|
130
|
+
}
|
|
131
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
132
|
+
const statusResult = await input.client.session.status({
|
|
133
|
+
query: { directory: input.directory },
|
|
134
|
+
});
|
|
135
|
+
const statusMap = requireResultData(statusResult, "session status");
|
|
136
|
+
const sessionStatus = statusMap[input.sessionID];
|
|
137
|
+
if (sessionStatus && sessionStatus.type !== "idle") {
|
|
138
|
+
stablePolls = 0;
|
|
139
|
+
lastMessageCount = -1;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const messagesResult = await input.client.session.messages({
|
|
143
|
+
path: { id: input.sessionID },
|
|
144
|
+
query: { directory: input.directory },
|
|
145
|
+
});
|
|
146
|
+
const messages = requireResultData(messagesResult, "session messages");
|
|
147
|
+
const latestAssistant = findLatestAssistantMessage(messages);
|
|
148
|
+
if (!latestAssistant) {
|
|
149
|
+
stablePolls = 0;
|
|
150
|
+
lastMessageCount = messages.length;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (messages.length === lastMessageCount) {
|
|
154
|
+
stablePolls += 1;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
stablePolls = 0;
|
|
158
|
+
lastMessageCount = messages.length;
|
|
159
|
+
}
|
|
160
|
+
if (stablePolls >= STABILITY_POLLS_REQUIRED) {
|
|
161
|
+
return messages;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
throw new Error(`Akane stage timed out after ${Math.ceil(input.timeoutMs / 60000)} minute(s).`);
|
|
165
|
+
}
|
|
166
|
+
async function runGitCommand(projectRoot, args) {
|
|
167
|
+
try {
|
|
168
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
169
|
+
cwd: projectRoot,
|
|
170
|
+
env: process.env,
|
|
171
|
+
maxBuffer: 1024 * 1024,
|
|
172
|
+
});
|
|
173
|
+
return stdout.trim();
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return "";
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async function captureWorkspaceSnapshot(projectRoot) {
|
|
180
|
+
const [statusShort, diffStat] = await Promise.all([
|
|
181
|
+
runGitCommand(projectRoot, ["status", "--short"]),
|
|
182
|
+
runGitCommand(projectRoot, ["diff", "--stat"]),
|
|
183
|
+
]);
|
|
184
|
+
return { statusShort, diffStat };
|
|
185
|
+
}
|
|
186
|
+
function renderCodeBlock(content, language = "text") {
|
|
187
|
+
const normalized = content.trim();
|
|
188
|
+
if (!normalized) {
|
|
189
|
+
return "_None_";
|
|
190
|
+
}
|
|
191
|
+
return `\`\`\`${language}\n${normalized}\n\`\`\``;
|
|
192
|
+
}
|
|
193
|
+
function renderSection(title, body) {
|
|
194
|
+
return `## ${title}\n\n${body.trim()}\n`;
|
|
195
|
+
}
|
|
196
|
+
function renderStageDocument(input) {
|
|
197
|
+
const sections = [
|
|
198
|
+
`# ${stageTitle(input.stage)}`,
|
|
199
|
+
"",
|
|
200
|
+
`- Service: ${AKANE_SERVICE_NAME}`,
|
|
201
|
+
`- Role: ${input.role}`,
|
|
202
|
+
`- Model: ${input.model}`,
|
|
203
|
+
`- Session ID: ${input.sessionID}`,
|
|
204
|
+
`- Message ID: ${input.messageID}`,
|
|
205
|
+
`- Generated At: ${nowIso()}`,
|
|
206
|
+
"",
|
|
207
|
+
renderSection("Output", input.body),
|
|
208
|
+
];
|
|
209
|
+
for (const extra of input.extraSections ?? []) {
|
|
210
|
+
sections.push(renderSection(extra.title, extra.body));
|
|
211
|
+
}
|
|
212
|
+
return sections.join("\n").trimEnd() + "\n";
|
|
213
|
+
}
|
|
214
|
+
async function readArtifactContent(projectRoot, configInfo, stage) {
|
|
215
|
+
const artifactPath = resolveStageArtifactPath(projectRoot, configInfo.config, stage);
|
|
216
|
+
try {
|
|
217
|
+
return await readFile(artifactPath, "utf8");
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
if (error.code === "ENOENT") {
|
|
221
|
+
return "";
|
|
222
|
+
}
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async function requireArtifactContent(projectRoot, configInfo, stage, instruction) {
|
|
227
|
+
const content = (await readArtifactContent(projectRoot, configInfo, stage)).trim();
|
|
228
|
+
if (!content) {
|
|
229
|
+
const artifactPath = resolveStageArtifactPath(projectRoot, configInfo.config, stage);
|
|
230
|
+
throw new Error(`Missing ${stage} artifact at ${artifactPath}. ${instruction}`);
|
|
231
|
+
}
|
|
232
|
+
return content;
|
|
233
|
+
}
|
|
234
|
+
function buildTaskBlock(task, notes) {
|
|
235
|
+
const blocks = [];
|
|
236
|
+
if (task?.trim()) {
|
|
237
|
+
blocks.push(renderSection("Task", task.trim()));
|
|
238
|
+
}
|
|
239
|
+
if (notes?.trim()) {
|
|
240
|
+
blocks.push(renderSection("Additional Notes", notes.trim()));
|
|
241
|
+
}
|
|
242
|
+
return blocks.join("\n");
|
|
243
|
+
}
|
|
244
|
+
function buildArtifactBlock(title, content) {
|
|
245
|
+
return renderSection(title, renderCodeBlock(content, "md"));
|
|
246
|
+
}
|
|
247
|
+
async function runStageSession(input) {
|
|
248
|
+
const role = STAGE_ROLE_MAP[input.stage];
|
|
249
|
+
const model = input.configInfo.config.roles[role];
|
|
250
|
+
const modelRef = parseModelRef(model);
|
|
251
|
+
const agent = await resolveAgentName(input.pluginInput.client, input.projectRoot, role);
|
|
252
|
+
const session = await createStageSession({
|
|
253
|
+
client: input.pluginInput.client,
|
|
254
|
+
parentSessionID: input.toolContext.sessionID,
|
|
255
|
+
directory: input.projectRoot,
|
|
256
|
+
title: input.title,
|
|
257
|
+
});
|
|
258
|
+
const promptResult = await input.pluginInput.client.session.promptAsync({
|
|
259
|
+
path: { id: session.id },
|
|
260
|
+
query: { directory: input.projectRoot },
|
|
261
|
+
signal: input.toolContext.abort,
|
|
262
|
+
body: {
|
|
263
|
+
...(agent ? { agent } : {}),
|
|
264
|
+
model: modelRef,
|
|
265
|
+
system: input.system,
|
|
266
|
+
tools: makeToolRestrictions(input.allowWorkspaceMutation),
|
|
267
|
+
parts: [
|
|
268
|
+
{
|
|
269
|
+
type: "text",
|
|
270
|
+
text: input.prompt,
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
if (promptResult.error) {
|
|
276
|
+
throw new Error(`${stageTitle(input.stage)} prompt failed: ${resultErrorMessage(promptResult, "unknown error")}`);
|
|
277
|
+
}
|
|
278
|
+
const messages = await waitForSessionCompletion({
|
|
279
|
+
client: input.pluginInput.client,
|
|
280
|
+
sessionID: session.id,
|
|
281
|
+
directory: input.projectRoot,
|
|
282
|
+
abort: input.toolContext.abort,
|
|
283
|
+
timeoutMs: input.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
284
|
+
});
|
|
285
|
+
const latestAssistant = findLatestAssistantMessage(messages);
|
|
286
|
+
if (!latestAssistant) {
|
|
287
|
+
throw new Error(`${stageTitle(input.stage)} completed without an assistant response.`);
|
|
288
|
+
}
|
|
289
|
+
const text = extractAssistantText(latestAssistant.parts).trim();
|
|
290
|
+
if (!text) {
|
|
291
|
+
throw new Error(`${stageTitle(input.stage)} completed without text output.`);
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
stage: input.stage,
|
|
295
|
+
role,
|
|
296
|
+
model,
|
|
297
|
+
agent,
|
|
298
|
+
sessionID: session.id,
|
|
299
|
+
messageID: latestAssistant.info.id,
|
|
300
|
+
title: input.title,
|
|
301
|
+
text,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function planSystemPrompt() {
|
|
305
|
+
return [
|
|
306
|
+
`You are the ${AKANE_SERVICE_NAME} planning stage.`,
|
|
307
|
+
"Create a deterministic implementation plan in Markdown.",
|
|
308
|
+
"Focus on steps, risks, validation, and clear sequencing.",
|
|
309
|
+
"Do not modify files.",
|
|
310
|
+
"Do not rely on hidden context. Use only the task and repository inspection you perform in this session.",
|
|
311
|
+
"Output sections: Goal, Assumptions, Plan, Risks, Validation.",
|
|
312
|
+
].join("\n");
|
|
313
|
+
}
|
|
314
|
+
function planReviewSystemPrompt() {
|
|
315
|
+
return [
|
|
316
|
+
`You are the ${AKANE_SERVICE_NAME} plan review stage.`,
|
|
317
|
+
"Critically review the plan and improve it without implementing anything.",
|
|
318
|
+
"Do not modify files.",
|
|
319
|
+
"Output sections: Verdict, Findings, Recommended Changes, Approved Plan Notes.",
|
|
320
|
+
"Be direct and specific.",
|
|
321
|
+
].join("\n");
|
|
322
|
+
}
|
|
323
|
+
function implementSystemPrompt() {
|
|
324
|
+
return [
|
|
325
|
+
`You are the ${AKANE_SERVICE_NAME} implementation stage.`,
|
|
326
|
+
"Implement the approved plan in the current repository.",
|
|
327
|
+
"Use tools when needed to inspect, edit, and validate.",
|
|
328
|
+
"At the end, output concise Markdown with sections: Completed Work, Changed Files, Validation, Remaining Risks.",
|
|
329
|
+
"Do not include full diffs in the response.",
|
|
330
|
+
].join("\n");
|
|
331
|
+
}
|
|
332
|
+
function reviewSystemPrompt(reviewer) {
|
|
333
|
+
return [
|
|
334
|
+
`You are the ${AKANE_SERVICE_NAME} ${reviewer} review stage.`,
|
|
335
|
+
"Review the current repository state after implementation.",
|
|
336
|
+
"Do not modify files.",
|
|
337
|
+
"Prioritize bugs, regressions, missing validation, and risky assumptions.",
|
|
338
|
+
"Output findings first with severity labels like P1/P2/P3 and file references when possible.",
|
|
339
|
+
'If there are no findings, explicitly say "No findings."',
|
|
340
|
+
"Then include Residual Risks and Suggested Follow-ups.",
|
|
341
|
+
].join("\n");
|
|
342
|
+
}
|
|
343
|
+
function synthesizeSystemPrompt() {
|
|
344
|
+
return [
|
|
345
|
+
`You are the ${AKANE_SERVICE_NAME} synthesis stage.`,
|
|
346
|
+
"Produce the final synthesis across plan, implementation, and reviews.",
|
|
347
|
+
"Do not modify files.",
|
|
348
|
+
"Output sections: Outcome, Validation, Review Summary, Remaining Risks, Next Steps.",
|
|
349
|
+
"Keep it decision-oriented.",
|
|
350
|
+
].join("\n");
|
|
351
|
+
}
|
|
352
|
+
export function resolveProjectRootFromArgs(input) {
|
|
353
|
+
return resolveProjectRoot({
|
|
354
|
+
directory: input.toolContext.directory,
|
|
355
|
+
worktree: input.toolContext.worktree,
|
|
356
|
+
projectRoot: input.projectRoot,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
export async function executePlanStage(input) {
|
|
360
|
+
await ensureArtifactLayout({
|
|
361
|
+
projectRoot: input.projectRoot,
|
|
362
|
+
config: input.configInfo.config,
|
|
363
|
+
configPath: input.configInfo.path,
|
|
364
|
+
});
|
|
365
|
+
const result = await runStageSession({
|
|
366
|
+
pluginInput: input.pluginInput,
|
|
367
|
+
configInfo: input.configInfo,
|
|
368
|
+
toolContext: input.toolContext,
|
|
369
|
+
projectRoot: input.projectRoot,
|
|
370
|
+
stage: "plan",
|
|
371
|
+
title: `Akane Plan: ${truncate(input.task, 60)}`,
|
|
372
|
+
system: planSystemPrompt(),
|
|
373
|
+
prompt: [
|
|
374
|
+
buildTaskBlock(input.task, input.notes),
|
|
375
|
+
renderSection("Project Context", `Repository root: ${input.projectRoot}`),
|
|
376
|
+
]
|
|
377
|
+
.filter(Boolean)
|
|
378
|
+
.join("\n"),
|
|
379
|
+
allowWorkspaceMutation: false,
|
|
380
|
+
});
|
|
381
|
+
const content = renderStageDocument({
|
|
382
|
+
stage: result.stage,
|
|
383
|
+
role: result.role,
|
|
384
|
+
model: result.model,
|
|
385
|
+
sessionID: result.sessionID,
|
|
386
|
+
messageID: result.messageID,
|
|
387
|
+
title: result.title,
|
|
388
|
+
body: result.text,
|
|
389
|
+
});
|
|
390
|
+
const writeResult = await writeStageArtifact({
|
|
391
|
+
projectRoot: input.projectRoot,
|
|
392
|
+
config: input.configInfo.config,
|
|
393
|
+
configPath: input.configInfo.path,
|
|
394
|
+
stage: "plan",
|
|
395
|
+
content,
|
|
396
|
+
mode: "replace",
|
|
397
|
+
details: {
|
|
398
|
+
role: result.role,
|
|
399
|
+
model: result.model,
|
|
400
|
+
sessionID: result.sessionID,
|
|
401
|
+
messageID: result.messageID,
|
|
402
|
+
title: result.title,
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
return {
|
|
406
|
+
...result,
|
|
407
|
+
artifactPath: writeResult.artifactPath,
|
|
408
|
+
content,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
export async function executePlanReviewStage(input) {
|
|
412
|
+
const plan = await requireArtifactContent(input.projectRoot, input.configInfo, "plan", "Run akane_plan first.");
|
|
413
|
+
const result = await runStageSession({
|
|
414
|
+
pluginInput: input.pluginInput,
|
|
415
|
+
configInfo: input.configInfo,
|
|
416
|
+
toolContext: input.toolContext,
|
|
417
|
+
projectRoot: input.projectRoot,
|
|
418
|
+
stage: "plan-review",
|
|
419
|
+
title: `Akane Plan Review${input.task ? `: ${truncate(input.task, 40)}` : ""}`,
|
|
420
|
+
system: planReviewSystemPrompt(),
|
|
421
|
+
prompt: [
|
|
422
|
+
buildTaskBlock(input.task, input.notes),
|
|
423
|
+
buildArtifactBlock("Plan Artifact", plan),
|
|
424
|
+
]
|
|
425
|
+
.filter(Boolean)
|
|
426
|
+
.join("\n"),
|
|
427
|
+
allowWorkspaceMutation: false,
|
|
428
|
+
});
|
|
429
|
+
const content = renderStageDocument({
|
|
430
|
+
stage: result.stage,
|
|
431
|
+
role: result.role,
|
|
432
|
+
model: result.model,
|
|
433
|
+
sessionID: result.sessionID,
|
|
434
|
+
messageID: result.messageID,
|
|
435
|
+
title: result.title,
|
|
436
|
+
body: result.text,
|
|
437
|
+
});
|
|
438
|
+
const writeResult = await writeStageArtifact({
|
|
439
|
+
projectRoot: input.projectRoot,
|
|
440
|
+
config: input.configInfo.config,
|
|
441
|
+
configPath: input.configInfo.path,
|
|
442
|
+
stage: "plan-review",
|
|
443
|
+
content,
|
|
444
|
+
mode: "replace",
|
|
445
|
+
details: {
|
|
446
|
+
role: result.role,
|
|
447
|
+
model: result.model,
|
|
448
|
+
sessionID: result.sessionID,
|
|
449
|
+
messageID: result.messageID,
|
|
450
|
+
title: result.title,
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
return {
|
|
454
|
+
...result,
|
|
455
|
+
artifactPath: writeResult.artifactPath,
|
|
456
|
+
content,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
export async function executeImplementStage(input) {
|
|
460
|
+
const [plan, planReview, workspace] = await Promise.all([
|
|
461
|
+
requireArtifactContent(input.projectRoot, input.configInfo, "plan", "Run akane_plan first."),
|
|
462
|
+
requireArtifactContent(input.projectRoot, input.configInfo, "plan-review", "Run akane_plan_review first."),
|
|
463
|
+
captureWorkspaceSnapshot(input.projectRoot),
|
|
464
|
+
]);
|
|
465
|
+
const result = await runStageSession({
|
|
466
|
+
pluginInput: input.pluginInput,
|
|
467
|
+
configInfo: input.configInfo,
|
|
468
|
+
toolContext: input.toolContext,
|
|
469
|
+
projectRoot: input.projectRoot,
|
|
470
|
+
stage: "implementation-context",
|
|
471
|
+
title: `Akane Implement${input.task ? `: ${truncate(input.task, 40)}` : ""}`,
|
|
472
|
+
system: implementSystemPrompt(),
|
|
473
|
+
prompt: [
|
|
474
|
+
buildTaskBlock(input.task, input.notes),
|
|
475
|
+
buildArtifactBlock("Plan Artifact", plan),
|
|
476
|
+
buildArtifactBlock("Plan Review Artifact", planReview),
|
|
477
|
+
renderSection("Workspace Snapshot Before Implementation", [
|
|
478
|
+
"### git status --short",
|
|
479
|
+
renderCodeBlock(workspace.statusShort || "Clean working tree"),
|
|
480
|
+
"### git diff --stat",
|
|
481
|
+
renderCodeBlock(workspace.diffStat || "No unstaged diff"),
|
|
482
|
+
].join("\n\n")),
|
|
483
|
+
]
|
|
484
|
+
.filter(Boolean)
|
|
485
|
+
.join("\n"),
|
|
486
|
+
allowWorkspaceMutation: true,
|
|
487
|
+
timeoutMs: IMPLEMENT_TIMEOUT_MS,
|
|
488
|
+
});
|
|
489
|
+
const diffResult = await input.pluginInput.client.session.diff({
|
|
490
|
+
path: { id: result.sessionID },
|
|
491
|
+
query: { directory: input.projectRoot },
|
|
492
|
+
});
|
|
493
|
+
const diffs = diffResult.error ? [] : diffResult.data ?? [];
|
|
494
|
+
const afterWorkspace = await captureWorkspaceSnapshot(input.projectRoot);
|
|
495
|
+
const diffSummary = diffs.length === 0
|
|
496
|
+
? "No session diff records were returned."
|
|
497
|
+
: diffs
|
|
498
|
+
.map((diff) => `- ${diff.file} (+${diff.additions} / -${diff.deletions})`)
|
|
499
|
+
.join("\n");
|
|
500
|
+
const content = renderStageDocument({
|
|
501
|
+
stage: result.stage,
|
|
502
|
+
role: result.role,
|
|
503
|
+
model: result.model,
|
|
504
|
+
sessionID: result.sessionID,
|
|
505
|
+
messageID: result.messageID,
|
|
506
|
+
title: result.title,
|
|
507
|
+
body: result.text,
|
|
508
|
+
extraSections: [
|
|
509
|
+
{
|
|
510
|
+
title: "Session Diff Summary",
|
|
511
|
+
body: diffSummary,
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
title: "Workspace Snapshot After Implementation",
|
|
515
|
+
body: [
|
|
516
|
+
"### git status --short",
|
|
517
|
+
renderCodeBlock(afterWorkspace.statusShort || "Clean working tree"),
|
|
518
|
+
"### git diff --stat",
|
|
519
|
+
renderCodeBlock(afterWorkspace.diffStat || "No unstaged diff"),
|
|
520
|
+
].join("\n\n"),
|
|
521
|
+
},
|
|
522
|
+
],
|
|
523
|
+
});
|
|
524
|
+
const writeResult = await writeStageArtifact({
|
|
525
|
+
projectRoot: input.projectRoot,
|
|
526
|
+
config: input.configInfo.config,
|
|
527
|
+
configPath: input.configInfo.path,
|
|
528
|
+
stage: "implementation-context",
|
|
529
|
+
content,
|
|
530
|
+
mode: "replace",
|
|
531
|
+
details: {
|
|
532
|
+
role: result.role,
|
|
533
|
+
model: result.model,
|
|
534
|
+
sessionID: result.sessionID,
|
|
535
|
+
messageID: result.messageID,
|
|
536
|
+
title: result.title,
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
return {
|
|
540
|
+
...result,
|
|
541
|
+
artifactPath: writeResult.artifactPath,
|
|
542
|
+
content,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
async function executeReviewerStage(input) {
|
|
546
|
+
const [plan, planReview, implementation, workspace] = await Promise.all([
|
|
547
|
+
requireArtifactContent(input.projectRoot, input.configInfo, "plan", "Run akane_plan first."),
|
|
548
|
+
requireArtifactContent(input.projectRoot, input.configInfo, "plan-review", "Run akane_plan_review first."),
|
|
549
|
+
requireArtifactContent(input.projectRoot, input.configInfo, "implementation-context", "Run akane_implement first."),
|
|
550
|
+
captureWorkspaceSnapshot(input.projectRoot),
|
|
551
|
+
]);
|
|
552
|
+
const reviewer = input.stage === "review-codex" ? "codex" : "claude";
|
|
553
|
+
const result = await runStageSession({
|
|
554
|
+
pluginInput: input.pluginInput,
|
|
555
|
+
configInfo: input.configInfo,
|
|
556
|
+
toolContext: input.toolContext,
|
|
557
|
+
projectRoot: input.projectRoot,
|
|
558
|
+
stage: input.stage,
|
|
559
|
+
title: `Akane ${reviewer === "codex" ? "Codex" : "Claude"} Review`,
|
|
560
|
+
system: reviewSystemPrompt(reviewer),
|
|
561
|
+
prompt: [
|
|
562
|
+
buildTaskBlock(input.task, input.notes),
|
|
563
|
+
buildArtifactBlock("Plan Artifact", plan),
|
|
564
|
+
buildArtifactBlock("Plan Review Artifact", planReview),
|
|
565
|
+
buildArtifactBlock("Implementation Artifact", implementation),
|
|
566
|
+
renderSection("Current Workspace Snapshot", [
|
|
567
|
+
"### git status --short",
|
|
568
|
+
renderCodeBlock(workspace.statusShort || "Clean working tree"),
|
|
569
|
+
"### git diff --stat",
|
|
570
|
+
renderCodeBlock(workspace.diffStat || "No unstaged diff"),
|
|
571
|
+
].join("\n\n")),
|
|
572
|
+
]
|
|
573
|
+
.filter(Boolean)
|
|
574
|
+
.join("\n"),
|
|
575
|
+
allowWorkspaceMutation: false,
|
|
576
|
+
});
|
|
577
|
+
const content = renderStageDocument({
|
|
578
|
+
stage: result.stage,
|
|
579
|
+
role: result.role,
|
|
580
|
+
model: result.model,
|
|
581
|
+
sessionID: result.sessionID,
|
|
582
|
+
messageID: result.messageID,
|
|
583
|
+
title: result.title,
|
|
584
|
+
body: result.text,
|
|
585
|
+
});
|
|
586
|
+
const writeResult = await writeStageArtifact({
|
|
587
|
+
projectRoot: input.projectRoot,
|
|
588
|
+
config: input.configInfo.config,
|
|
589
|
+
configPath: input.configInfo.path,
|
|
590
|
+
stage: input.stage,
|
|
591
|
+
content,
|
|
592
|
+
mode: "replace",
|
|
593
|
+
details: {
|
|
594
|
+
role: result.role,
|
|
595
|
+
model: result.model,
|
|
596
|
+
sessionID: result.sessionID,
|
|
597
|
+
messageID: result.messageID,
|
|
598
|
+
title: result.title,
|
|
599
|
+
},
|
|
600
|
+
});
|
|
601
|
+
return {
|
|
602
|
+
...result,
|
|
603
|
+
artifactPath: writeResult.artifactPath,
|
|
604
|
+
content,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
export async function executeReviewStage(input) {
|
|
608
|
+
const stages = input.reviewer === "both"
|
|
609
|
+
? ["review-codex", "review-claude"]
|
|
610
|
+
: [input.reviewer === "codex" ? "review-codex" : "review-claude"];
|
|
611
|
+
const results = await Promise.all(stages.map((stage) => executeReviewerStage({
|
|
612
|
+
...input,
|
|
613
|
+
stage,
|
|
614
|
+
})));
|
|
615
|
+
return {
|
|
616
|
+
requested: input.reviewer,
|
|
617
|
+
results,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
export async function executeSynthesizeStage(input) {
|
|
621
|
+
const [plan, planReview, implementation, reviewCodex, reviewClaude, workspace] = await Promise.all([
|
|
622
|
+
requireArtifactContent(input.projectRoot, input.configInfo, "plan", "Run akane_plan first."),
|
|
623
|
+
requireArtifactContent(input.projectRoot, input.configInfo, "plan-review", "Run akane_plan_review first."),
|
|
624
|
+
requireArtifactContent(input.projectRoot, input.configInfo, "implementation-context", "Run akane_implement first."),
|
|
625
|
+
requireArtifactContent(input.projectRoot, input.configInfo, "review-codex", "Run akane_review first."),
|
|
626
|
+
requireArtifactContent(input.projectRoot, input.configInfo, "review-claude", "Run akane_review first."),
|
|
627
|
+
captureWorkspaceSnapshot(input.projectRoot),
|
|
628
|
+
]);
|
|
629
|
+
const result = await runStageSession({
|
|
630
|
+
pluginInput: input.pluginInput,
|
|
631
|
+
configInfo: input.configInfo,
|
|
632
|
+
toolContext: input.toolContext,
|
|
633
|
+
projectRoot: input.projectRoot,
|
|
634
|
+
stage: "final-synthesis",
|
|
635
|
+
title: `Akane Final Synthesis${input.task ? `: ${truncate(input.task, 40)}` : ""}`,
|
|
636
|
+
system: synthesizeSystemPrompt(),
|
|
637
|
+
prompt: [
|
|
638
|
+
buildTaskBlock(input.task, input.notes),
|
|
639
|
+
buildArtifactBlock("Plan Artifact", plan),
|
|
640
|
+
buildArtifactBlock("Plan Review Artifact", planReview),
|
|
641
|
+
buildArtifactBlock("Implementation Artifact", implementation),
|
|
642
|
+
buildArtifactBlock("Codex Review Artifact", reviewCodex),
|
|
643
|
+
buildArtifactBlock("Claude Review Artifact", reviewClaude),
|
|
644
|
+
renderSection("Current Workspace Snapshot", [
|
|
645
|
+
"### git status --short",
|
|
646
|
+
renderCodeBlock(workspace.statusShort || "Clean working tree"),
|
|
647
|
+
"### git diff --stat",
|
|
648
|
+
renderCodeBlock(workspace.diffStat || "No unstaged diff"),
|
|
649
|
+
].join("\n\n")),
|
|
650
|
+
]
|
|
651
|
+
.filter(Boolean)
|
|
652
|
+
.join("\n"),
|
|
653
|
+
allowWorkspaceMutation: false,
|
|
654
|
+
});
|
|
655
|
+
const content = renderStageDocument({
|
|
656
|
+
stage: result.stage,
|
|
657
|
+
role: result.role,
|
|
658
|
+
model: result.model,
|
|
659
|
+
sessionID: result.sessionID,
|
|
660
|
+
messageID: result.messageID,
|
|
661
|
+
title: result.title,
|
|
662
|
+
body: result.text,
|
|
663
|
+
});
|
|
664
|
+
const writeResult = await writeStageArtifact({
|
|
665
|
+
projectRoot: input.projectRoot,
|
|
666
|
+
config: input.configInfo.config,
|
|
667
|
+
configPath: input.configInfo.path,
|
|
668
|
+
stage: "final-synthesis",
|
|
669
|
+
content,
|
|
670
|
+
mode: "replace",
|
|
671
|
+
details: {
|
|
672
|
+
role: result.role,
|
|
673
|
+
model: result.model,
|
|
674
|
+
sessionID: result.sessionID,
|
|
675
|
+
messageID: result.messageID,
|
|
676
|
+
title: result.title,
|
|
677
|
+
},
|
|
678
|
+
});
|
|
679
|
+
return {
|
|
680
|
+
...result,
|
|
681
|
+
artifactPath: writeResult.artifactPath,
|
|
682
|
+
content,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
export async function executeRunWorkflow(input) {
|
|
686
|
+
const completedStages = [];
|
|
687
|
+
const plan = await executePlanStage(input);
|
|
688
|
+
completedStages.push("plan");
|
|
689
|
+
if (input.throughStage === "plan") {
|
|
690
|
+
return { completedStages, plan };
|
|
691
|
+
}
|
|
692
|
+
const planReview = await executePlanReviewStage(input);
|
|
693
|
+
completedStages.push("plan-review");
|
|
694
|
+
if (input.throughStage === "plan-review") {
|
|
695
|
+
return { completedStages, plan, planReview };
|
|
696
|
+
}
|
|
697
|
+
const implementation = await executeImplementStage(input);
|
|
698
|
+
completedStages.push("implementation-context");
|
|
699
|
+
if (input.throughStage === "implementation-context") {
|
|
700
|
+
return { completedStages, plan, planReview, implementation };
|
|
701
|
+
}
|
|
702
|
+
const reviews = (await executeReviewStage({
|
|
703
|
+
...input,
|
|
704
|
+
reviewer: "both",
|
|
705
|
+
})).results;
|
|
706
|
+
completedStages.push("review-codex", "review-claude");
|
|
707
|
+
if (input.throughStage === "review-codex" ||
|
|
708
|
+
input.throughStage === "review-claude") {
|
|
709
|
+
return { completedStages, plan, planReview, implementation, reviews };
|
|
710
|
+
}
|
|
711
|
+
const synthesis = await executeSynthesizeStage(input);
|
|
712
|
+
completedStages.push("final-synthesis");
|
|
713
|
+
return {
|
|
714
|
+
completedStages,
|
|
715
|
+
plan,
|
|
716
|
+
planReview,
|
|
717
|
+
implementation,
|
|
718
|
+
reviews,
|
|
719
|
+
synthesis,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
export function reviewSelectionLabel(selection) {
|
|
723
|
+
if (selection === "both") {
|
|
724
|
+
return "codex + claude";
|
|
725
|
+
}
|
|
726
|
+
return selection;
|
|
727
|
+
}
|