@astrosheep/keiyaku 0.1.0

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/build/logic.js ADDED
@@ -0,0 +1,530 @@
1
+ import * as fs from "fs/promises";
2
+ import * as path from "path";
3
+ import { isSubagentExecError, resolveSubagentConfig, runSubagentExec } from "./subagent-exec/index.js";
4
+ import { KEIYAKU_FILE, TRACE_FILE } from "./constants.js";
5
+ import { appendDebugBlock, appendDebugLog } from "./debug-log.js";
6
+ import { asMessage, FlowError, wrapFlowError } from "./errors.js";
7
+ import * as git from "./git.js";
8
+ import { appendRoundReport, appendRoundSystemNote, appendReview, buildIteratePlan, computeTraceState, readTraceContent, } from "./trace.js";
9
+ import { buildAskPrompt, buildStartPrompt } from "./prompts.js";
10
+ import { resolveTermPreset } from "./term-presets.js";
11
+ const DEFAULT_JUDGMENT_OATH = "I, [your name], solemnly swear that I have scrutinized this change line by line with my own eyes. This oath is handwritten as a testament to my responsibility. If logic fails where I claimed it sound, I shall bear the mark of this oversight forever.\n\nSigned: [your name]";
12
+ const JUDGMENT_OATH_ENV_KEY = "KEIYAKU_JUDGMENT_OATH";
13
+ const LEGACY_JUDGMENT_OATH_ENV_KEY = "KEIYAKU_SEAL_OATH";
14
+ const JUDGMENT_OATH_NAME_PLACEHOLDER = "[your name]";
15
+ const JUDGMENT_INVOKE_FLAGS = ["metPrecise", "metMinimal", "metIsolated", "metIdiomatic", "metCohesive"];
16
+ const FALLBACK_DEFAULT_SUBAGENT_NAME = "servant-tier-A";
17
+ const BASE_CRITERIA_FILE = path.join(".keiyaku", "base-criteria.md");
18
+ const BASE_CONSTRAINTS_FILE = path.join(".keiyaku", "base-constraints.md");
19
+ function parseMarkdownList(text) {
20
+ return text
21
+ .split(/\r?\n/)
22
+ .map((line) => line.trim())
23
+ .filter((line) => /^[-*]\s+/.test(line))
24
+ .map((line) => line.replace(/^[-*]\s+/, "").trim())
25
+ .filter((line) => line.length > 0);
26
+ }
27
+ function renderBulletList(items) {
28
+ if (items.length === 0)
29
+ return "- (none)";
30
+ return items.map((item) => `- ${item}`).join("\n");
31
+ }
32
+ function renderKeiyaku(title, goal, context, baseConstraints, taskConstraints, baseCriteria, taskCriteria) {
33
+ let content = `# ${title}\n\n## Context\n${context}\n\n## Goal\n${goal}`;
34
+ content += `\n\n## Constraints`;
35
+ content += `\n\n### Base Constraints\n${renderBulletList(baseConstraints)}`;
36
+ content += `\n\n### Task Constraints\n${renderBulletList(taskConstraints)}`;
37
+ content += `\n\n## Acceptance Criteria`;
38
+ content += `\n\n### Base Criteria\n${renderBulletList(baseCriteria)}`;
39
+ content += `\n\n### Task Criteria\n${renderBulletList(taskCriteria)}`;
40
+ return content;
41
+ }
42
+ function buildMergeMessage(title, keiyakuContent, reportContent) {
43
+ return `keiyaku(${title}): done\n\n---\n${keiyakuContent}\n---\n${reportContent}\n---\n`;
44
+ }
45
+ function resolveConfiguredSubagentName() {
46
+ const fromEnv = process.env.KEIYAKU_SUBAGENT_NAME_OVERRIDE?.trim();
47
+ if (!fromEnv)
48
+ return undefined;
49
+ resolveSubagentConfig(fromEnv);
50
+ return fromEnv;
51
+ }
52
+ function resolveRoundSubagentName(subagentName) {
53
+ const configured = resolveConfiguredSubagentName();
54
+ const name = configured || subagentName?.trim() || FALLBACK_DEFAULT_SUBAGENT_NAME;
55
+ resolveSubagentConfig(name);
56
+ return name;
57
+ }
58
+ export function resolveJudgmentOath() {
59
+ const configured = process.env[JUDGMENT_OATH_ENV_KEY];
60
+ if (configured !== undefined)
61
+ return configured;
62
+ const legacyConfigured = process.env[LEGACY_JUDGMENT_OATH_ENV_KEY];
63
+ if (legacyConfigured !== undefined)
64
+ return legacyConfigured;
65
+ return DEFAULT_JUDGMENT_OATH;
66
+ }
67
+ function judgmentOathMatches(inputOath, expectedOath) {
68
+ const actual = inputOath?.trim();
69
+ if (!actual)
70
+ return false;
71
+ const expected = expectedOath.trim();
72
+ if (!expected.includes(JUDGMENT_OATH_NAME_PLACEHOLDER)) {
73
+ return actual === expected;
74
+ }
75
+ const parts = expected.split(JUDGMENT_OATH_NAME_PLACEHOLDER);
76
+ let cursor = 0;
77
+ for (let idx = 0; idx < parts.length; idx += 1) {
78
+ const part = parts[idx] ?? "";
79
+ if (!actual.startsWith(part, cursor)) {
80
+ return false;
81
+ }
82
+ cursor += part.length;
83
+ if (idx === parts.length - 1)
84
+ break;
85
+ const nextPart = parts[idx + 1] ?? "";
86
+ const nextPartIndex = nextPart ? actual.indexOf(nextPart, cursor) : actual.length;
87
+ if (nextPartIndex < 0) {
88
+ return false;
89
+ }
90
+ const replacement = actual.slice(cursor, nextPartIndex);
91
+ if (replacement.trim().length === 0) {
92
+ return false;
93
+ }
94
+ cursor = nextPartIndex;
95
+ }
96
+ return cursor === actual.length;
97
+ }
98
+ function getMergeConflictFiles(err) {
99
+ if (err && typeof err === "object" && "git" in err) {
100
+ const gitPayload = err.git;
101
+ if (gitPayload && typeof gitPayload === "object") {
102
+ const detail = gitPayload;
103
+ if (detail.failed === true && Array.isArray(detail.conflicts)) {
104
+ const files = detail.conflicts
105
+ .map((entry) => entry?.file)
106
+ .filter((value) => typeof value === "string" && value.trim().length > 0);
107
+ if (files.length > 0) {
108
+ return Array.from(new Set(files));
109
+ }
110
+ }
111
+ }
112
+ }
113
+ // Fallback for git clients that only expose merge conflict details in the message text.
114
+ const text = asMessage(err);
115
+ const matches = Array.from(text.matchAll(/CONFLICT[^\n]* in ([^\n]+)/g));
116
+ const files = matches.map((entry) => entry[1]?.trim()).filter((value) => Boolean(value));
117
+ if (files.length > 0)
118
+ return Array.from(new Set(files));
119
+ if (text.includes("CONFLICT"))
120
+ return ["(unknown)"];
121
+ return [];
122
+ }
123
+ function summarizeForTrace(message) {
124
+ return message.replace(/\s+/g, " ").trim().slice(0, 500) || "Unknown error.";
125
+ }
126
+ function toSnippet(raw, maxChars = 4000) {
127
+ const text = raw.trim();
128
+ if (!text)
129
+ return undefined;
130
+ if (text.length <= maxChars)
131
+ return text;
132
+ const marker = `\n...[truncated ${text.length - maxChars} chars]...\n`;
133
+ const side = Math.floor((maxChars - marker.length) / 2);
134
+ const head = text.slice(0, side);
135
+ const tail = text.slice(text.length - side);
136
+ return `${head}${marker}${tail}`;
137
+ }
138
+ function describeSubagentFailure(error, subagentName, round) {
139
+ const message = asMessage(error);
140
+ if (isSubagentExecError(error)) {
141
+ return {
142
+ errorType: "subagent_execution_error",
143
+ errorCode: error.code,
144
+ message: error.message,
145
+ subagentName,
146
+ round,
147
+ timeoutMs: error.timeoutMs,
148
+ exitCode: error.exitCode,
149
+ stderrSnippet: toSnippet(error.stderr),
150
+ };
151
+ }
152
+ return {
153
+ errorType: "subagent_execution_error",
154
+ errorCode: "SUBAGENT_EXEC_ERROR",
155
+ message,
156
+ subagentName,
157
+ round,
158
+ timeoutMs: null,
159
+ exitCode: null,
160
+ };
161
+ }
162
+ function isAbortError(error) {
163
+ return error instanceof Error && error.name === "AbortError";
164
+ }
165
+ function buildDriveReason(directive, context) {
166
+ const normalizedDirective = requireText("directive", directive);
167
+ const normalizedContext = context?.trim();
168
+ if (!normalizedContext) {
169
+ return normalizedDirective;
170
+ }
171
+ return `${normalizedDirective}\n\nNew context:\n${normalizedContext}`;
172
+ }
173
+ async function runSubagent(subagentName, prompt, cwd, round, signal) {
174
+ const { identity } = resolveTermPreset();
175
+ if (process.env.KEIYAKU_FAKE_SUBAGENT === "1") {
176
+ return `Mock ${identity} '${subagentName}' completed round ${round}.`;
177
+ }
178
+ const startLog = `[${identity}] Running execution for '${subagentName}' in ${cwd}`;
179
+ console.error(startLog);
180
+ appendDebugLog(startLog, { cwd, section: "script" });
181
+ const result = await runSubagentExec(subagentName, prompt, cwd, { signal });
182
+ const summary = result.finalMessage || `${identity} completed successfully.`;
183
+ if (result.stderr.trim()) {
184
+ appendDebugBlock("subagent stderr", result.stderr, { cwd, section: "codex-stderr" });
185
+ }
186
+ return summary;
187
+ }
188
+ function normalizeTitleForBranch(title) {
189
+ const normalized = title
190
+ .trim()
191
+ .toLowerCase()
192
+ .replace(/[^\p{Script=Han}a-z0-9]+/gu, "-")
193
+ .replace(/-+/g, "-")
194
+ .replace(/^-+/, "")
195
+ .replace(/-+$/, "");
196
+ if (!normalized) {
197
+ throw new FlowError("INVALID_BRANCH_TITLE", "title cannot be converted to a valid branch name; use letters/numbers");
198
+ }
199
+ return normalized;
200
+ }
201
+ async function ensureKeiyakuFiles(cwd) {
202
+ const keiyakuPath = path.join(cwd, KEIYAKU_FILE);
203
+ const tracePath = path.join(cwd, TRACE_FILE);
204
+ try {
205
+ await fs.access(keiyakuPath);
206
+ await fs.access(tracePath);
207
+ }
208
+ catch {
209
+ throw new FlowError("MISSING_PROTOCOL_FILES", `missing protocol files for current keiyaku (${KEIYAKU_FILE} or ${TRACE_FILE})`);
210
+ }
211
+ }
212
+ function requireText(name, value) {
213
+ const normalized = value.trim();
214
+ if (!normalized) {
215
+ throw new FlowError("EMPTY_PARAM", `parameter '${name}' cannot be empty`);
216
+ }
217
+ return normalized;
218
+ }
219
+ async function assertCleanWorkingTree(cwd) {
220
+ const dirtyPaths = await git.getDirtyPaths(cwd);
221
+ if (dirtyPaths.length > 0) {
222
+ const listed = dirtyPaths.slice(0, 5).join(", ");
223
+ const suffix = dirtyPaths.length > 5 ? ` and ${dirtyPaths.length} files total` : "";
224
+ throw new FlowError("DIRTY_WORKTREE", `working tree has uncommitted changes (${listed}${suffix})`);
225
+ }
226
+ }
227
+ function collectRoundFiles(dirtyPaths) {
228
+ const filtered = dirtyPaths.filter((p) => p !== TRACE_FILE);
229
+ return Array.from(new Set(filtered)).sort();
230
+ }
231
+ async function appendRoundResult(cwd, round, summary, subagentFailure) {
232
+ const dirtyPaths = await git.getDirtyPaths(cwd);
233
+ const filesModified = collectRoundFiles(dirtyPaths);
234
+ if (subagentFailure) {
235
+ const errorText = summarizeForTrace(subagentFailure.message);
236
+ await appendRoundReport(cwd, {
237
+ round,
238
+ status: "FAILED",
239
+ summary,
240
+ filesModified,
241
+ errorMessage: errorText,
242
+ });
243
+ return;
244
+ }
245
+ await appendRoundReport(cwd, {
246
+ round,
247
+ status: "SUCCESS",
248
+ summary,
249
+ filesModified,
250
+ });
251
+ }
252
+ async function runAndRecordRound(cwd, titleToken, round, prompt, options) {
253
+ const roundSubagent = resolveRoundSubagentName(options.subagentName);
254
+ let summary;
255
+ let subagentFailure;
256
+ try {
257
+ summary = await runSubagent(roundSubagent, prompt, cwd, round, options.signal);
258
+ }
259
+ catch (err) {
260
+ if (isAbortError(err)) {
261
+ if (options.onAbort) {
262
+ await options.onAbort();
263
+ }
264
+ throw err;
265
+ }
266
+ subagentFailure = describeSubagentFailure(err, roundSubagent, round);
267
+ const failureLog = `[Subagent failure] name=${subagentFailure.subagentName} round=${subagentFailure.round} code=${subagentFailure.errorCode} timeoutMs=${subagentFailure.timeoutMs ?? "unknown"} exitCode=${subagentFailure.exitCode ?? "none"}`;
268
+ console.error(failureLog);
269
+ appendDebugLog(failureLog, { cwd, section: "script" });
270
+ if (subagentFailure.stderrSnippet) {
271
+ const failureStderrLog = `[Subagent failure stderr]\n${subagentFailure.stderrSnippet}`;
272
+ console.error(failureStderrLog);
273
+ appendDebugBlock("subagent failure stderr", subagentFailure.stderrSnippet, { cwd, section: "codex-stderr" });
274
+ }
275
+ summary =
276
+ options.failureMode === "round_specific"
277
+ ? `Round ${round} completed with subagent execution failure recorded in trace.`
278
+ : "Round 1 completed with subagent execution failure recorded in trace.";
279
+ }
280
+ await appendRoundResult(cwd, round, summary, subagentFailure);
281
+ await git.addFiles(cwd, "-A");
282
+ await git.commit(cwd, `keiyaku(${titleToken}): round ${round}`);
283
+ return { summary, ...(subagentFailure ? { subagentFailure } : {}) };
284
+ }
285
+ export async function handleKeiyaku(input) {
286
+ const { cwd, signal, subagentName } = input;
287
+ const finalTitle = requireText("title", input.title);
288
+ const finalGoal = requireText("goal", input.goal);
289
+ const finalContext = requireText("context", input.context);
290
+ const finalCriteria = requireText("criteria", input.criteria);
291
+ const taskCriteria = parseMarkdownList(finalCriteria);
292
+ const normalizedTaskCriteria = taskCriteria.length > 0 ? taskCriteria : [finalCriteria];
293
+ const finalConstraints = input.constraints?.trim();
294
+ const taskConstraints = finalConstraints ? parseMarkdownList(finalConstraints) : [];
295
+ const normalizedTaskConstraints = finalConstraints && taskConstraints.length > 0 ? taskConstraints : finalConstraints ? [finalConstraints] : [];
296
+ const directive = input.directive?.trim();
297
+ const isRepo = await git.isGitRepo(cwd);
298
+ if (!isRepo) {
299
+ throw new FlowError("NOT_GIT_REPO", `${cwd} is not a git repository`);
300
+ }
301
+ const existingKeiyaku = await git.getActiveKeiyakuBranch(cwd);
302
+ if (existingKeiyaku) {
303
+ throw new FlowError("ACTIVE_KEIYAKU_EXISTS", `active keiyaku already exists (${existingKeiyaku}); run invoke_judgment first`);
304
+ }
305
+ const existingBranches = await git.listLocalKeiyakuBranches(cwd);
306
+ if (existingBranches.length > 0) {
307
+ const branchWarning = `[keiyaku] existing local keiyaku branches detected (non-blocking): ${existingBranches.join(", ")}`;
308
+ console.error(branchWarning);
309
+ appendDebugLog(branchWarning, { cwd, section: "script" });
310
+ }
311
+ await assertCleanWorkingTree(cwd);
312
+ const baseBranch = await git.getCurrentBranch(cwd);
313
+ const branchToken = normalizeTitleForBranch(finalTitle);
314
+ const keiyakuBranch = `keiyaku/${branchToken}`;
315
+ await git.assertValidBranchName(cwd, keiyakuBranch);
316
+ let createdBranch = false;
317
+ try {
318
+ await git.createAndCheckoutBranch(cwd, keiyakuBranch);
319
+ createdBranch = true;
320
+ await git.setKeiyakuBase(cwd, keiyakuBranch, baseBranch);
321
+ const switchedLog = `Switched to branch: ${keiyakuBranch} (base: ${baseBranch})`;
322
+ console.error(switchedLog);
323
+ appendDebugLog(switchedLog, { cwd, section: "script" });
324
+ let baseCriteria = [];
325
+ try {
326
+ const baseCriteriaRaw = await fs.readFile(path.join(cwd, BASE_CRITERIA_FILE), "utf-8");
327
+ baseCriteria = parseMarkdownList(baseCriteriaRaw);
328
+ }
329
+ catch (error) {
330
+ const isMissingFile = error?.code === "ENOENT";
331
+ if (!isMissingFile) {
332
+ throw error;
333
+ }
334
+ }
335
+ let baseConstraints = [];
336
+ try {
337
+ const baseConstraintsRaw = await fs.readFile(path.join(cwd, BASE_CONSTRAINTS_FILE), "utf-8");
338
+ baseConstraints = parseMarkdownList(baseConstraintsRaw);
339
+ }
340
+ catch (error) {
341
+ const isMissingFile = error?.code === "ENOENT";
342
+ if (!isMissingFile) {
343
+ throw error;
344
+ }
345
+ }
346
+ const keiyakuContent = renderKeiyaku(finalTitle, finalGoal, finalContext, baseConstraints, normalizedTaskConstraints, baseCriteria, normalizedTaskCriteria);
347
+ await fs.writeFile(path.join(cwd, KEIYAKU_FILE), keiyakuContent);
348
+ await fs.writeFile(path.join(cwd, TRACE_FILE), "# Keiyaku Trace\n");
349
+ await git.addFiles(cwd, [KEIYAKU_FILE, TRACE_FILE]);
350
+ await git.commit(cwd, `keiyaku(${branchToken}): open`);
351
+ const prompt = buildStartPrompt(finalTitle, finalGoal, directive);
352
+ const { summary, subagentFailure } = await runAndRecordRound(cwd, branchToken, 1, prompt, {
353
+ signal,
354
+ subagentName,
355
+ failureMode: "standard",
356
+ });
357
+ const trace = await fs.readFile(path.join(cwd, TRACE_FILE), "utf-8");
358
+ const diff = await git.getKeiyakuDiff(cwd, baseBranch);
359
+ return {
360
+ round: 1,
361
+ trace,
362
+ diff,
363
+ summary,
364
+ branch: keiyakuBranch,
365
+ baseBranch,
366
+ ...(subagentFailure ? { subagentFailure } : {}),
367
+ };
368
+ }
369
+ catch (err) {
370
+ if (createdBranch) {
371
+ try {
372
+ await git.checkoutBranch(cwd, baseBranch);
373
+ }
374
+ catch {
375
+ // best effort
376
+ }
377
+ try {
378
+ if (await git.hasLocalBranch(cwd, keiyakuBranch)) {
379
+ await git.deleteBranch(cwd, keiyakuBranch, true);
380
+ }
381
+ }
382
+ catch {
383
+ // best effort
384
+ }
385
+ await git.clearKeiyakuBase(cwd, keiyakuBranch);
386
+ }
387
+ throw wrapFlowError("start keiyaku", err);
388
+ }
389
+ }
390
+ export async function handleAsk(input) {
391
+ const { cwd, signal, subagentName } = input;
392
+ const request = requireText("request", input.request);
393
+ const context = requireText("context", input.context);
394
+ const prompt = buildAskPrompt(request, context);
395
+ // TODO: enforce read-only access and persist summary to .keiyaku/notes/.
396
+ const summary = await runSubagent(resolveRoundSubagentName(subagentName), prompt, cwd, 0, signal);
397
+ return { summary };
398
+ }
399
+ export async function invokeJudgment(input) {
400
+ const { cwd } = input;
401
+ const isRepo = await git.isGitRepo(cwd);
402
+ if (!isRepo) {
403
+ throw new FlowError("NOT_GIT_REPO", `${cwd} is not a git repository`);
404
+ }
405
+ const keiyakuBranch = await git.getActiveKeiyakuBranch(cwd);
406
+ if (!keiyakuBranch) {
407
+ throw new FlowError("NOT_ACTIVE_KEIYAKU_BRANCH", "current branch is not an active keiyaku branch (run invoke_judgment on keiyaku/*)");
408
+ }
409
+ const baseBranch = await git.getKeiyakuBase(cwd, keiyakuBranch);
410
+ if (!baseBranch) {
411
+ throw new FlowError("MISSING_KEIYAKU_BASE", `branch ${keiyakuBranch} is missing base metadata; cannot continue judgment`);
412
+ }
413
+ const title = keiyakuBranch.slice("keiyaku/".length);
414
+ const intent = input.intent;
415
+ if (intent === "DROP") {
416
+ await assertCleanWorkingTree(cwd);
417
+ try {
418
+ await git.checkoutBranch(cwd, baseBranch);
419
+ await git.deleteBranch(cwd, keiyakuBranch, true);
420
+ await git.clearKeiyakuBase(cwd, keiyakuBranch);
421
+ }
422
+ catch (err) {
423
+ throw wrapFlowError(`execute DROP (${keiyakuBranch} -> ${baseBranch})`, err);
424
+ }
425
+ return { status: "dropped", branch: keiyakuBranch, baseBranch };
426
+ }
427
+ await ensureKeiyakuFiles(cwd);
428
+ const traceContent = await readTraceContent(cwd);
429
+ if (intent === "INVOKE") {
430
+ const missingOrFalse = JUDGMENT_INVOKE_FLAGS.filter((name) => input[name] !== true);
431
+ if (missingOrFalse.length > 0) {
432
+ throw new FlowError("JUDGMENT_QUALITY_GATE_FAILED", `INVOKE blocked: iterate until ${missingOrFalse.join(", ")} are met`);
433
+ }
434
+ const expectedOath = resolveJudgmentOath();
435
+ if (!judgmentOathMatches(input.oath, expectedOath)) {
436
+ throw new FlowError("JUDGMENT_OATH_MISMATCH", `Oath mismatch. Correct oath: ${expectedOath}`);
437
+ }
438
+ await assertCleanWorkingTree(cwd);
439
+ try {
440
+ const invokeDiffLog = `[INVOKE] Collecting diff stats against base '${baseBranch}'`;
441
+ console.error(invokeDiffLog);
442
+ appendDebugLog(invokeDiffLog, { cwd, section: "script" });
443
+ const diffStats = await git.getKeiyakuDiffStats(cwd, baseBranch);
444
+ const invokeReadLog = "[INVOKE] Reading keiyaku protocol files";
445
+ console.error(invokeReadLog);
446
+ appendDebugLog(invokeReadLog, { cwd, section: "script" });
447
+ const keiyakuContent = await fs.readFile(path.join(cwd, KEIYAKU_FILE), "utf-8");
448
+ const message = buildMergeMessage(title, keiyakuContent, traceContent);
449
+ const invokeCleanupLog = "[INVOKE] Removing protocol files and creating cleanup commit";
450
+ console.error(invokeCleanupLog);
451
+ appendDebugLog(invokeCleanupLog, { cwd, section: "script" });
452
+ await fs.unlink(path.join(cwd, KEIYAKU_FILE));
453
+ await fs.unlink(path.join(cwd, TRACE_FILE));
454
+ await git.addFiles(cwd, "-A");
455
+ await git.commit(cwd, `keiyaku(${title}): cleanup`);
456
+ const invokeCheckoutLog = `[INVOKE] Checking out base branch '${baseBranch}'`;
457
+ console.error(invokeCheckoutLog);
458
+ appendDebugLog(invokeCheckoutLog, { cwd, section: "script" });
459
+ await git.checkoutBranch(cwd, baseBranch);
460
+ const invokeMergeLog = `[INVOKE] Merging '${keiyakuBranch}' into '${baseBranch}'`;
461
+ console.error(invokeMergeLog);
462
+ appendDebugLog(invokeMergeLog, { cwd, section: "script" });
463
+ await git.merge(cwd, keiyakuBranch, message);
464
+ const invokeFinalizeLog = `[INVOKE] Deleting merged branch '${keiyakuBranch}' and clearing metadata`;
465
+ console.error(invokeFinalizeLog);
466
+ appendDebugLog(invokeFinalizeLog, { cwd, section: "script" });
467
+ await git.deleteBranch(cwd, keiyakuBranch);
468
+ await git.clearKeiyakuBase(cwd, keiyakuBranch);
469
+ return { status: "done", branch: keiyakuBranch, baseBranch, diffStats };
470
+ }
471
+ catch (err) {
472
+ const conflictFiles = getMergeConflictFiles(err);
473
+ if (conflictFiles.length > 0) {
474
+ throw new FlowError("DONE_MERGE_CONFLICT", `INVOKE merge conflict between ${keiyakuBranch} and ${baseBranch} (CONFLICTS: ${conflictFiles.join(", ")})`, err);
475
+ }
476
+ throw wrapFlowError(`execute INVOKE (merge ${keiyakuBranch} into ${baseBranch})`, err);
477
+ }
478
+ }
479
+ throw new Error(`unsupported judgment intent: ${intent}`);
480
+ }
481
+ export async function handleDrive(input) {
482
+ const { cwd, directive, context, subagentName, signal } = input;
483
+ const isRepo = await git.isGitRepo(cwd);
484
+ if (!isRepo) {
485
+ throw new FlowError("NOT_GIT_REPO", `${cwd} is not a git repository`);
486
+ }
487
+ const keiyakuBranch = await git.getActiveKeiyakuBranch(cwd);
488
+ if (!keiyakuBranch) {
489
+ throw new FlowError("NOT_ACTIVE_KEIYAKU_BRANCH", "current branch is not an active keiyaku branch (run drive on keiyaku/*)");
490
+ }
491
+ const baseBranch = await git.getKeiyakuBase(cwd, keiyakuBranch);
492
+ if (!baseBranch) {
493
+ throw new FlowError("MISSING_KEIYAKU_BASE", `branch ${keiyakuBranch} is missing base metadata; cannot continue drive`);
494
+ }
495
+ await ensureKeiyakuFiles(cwd);
496
+ const traceContent = await readTraceContent(cwd);
497
+ const traceState = computeTraceState(traceContent);
498
+ const title = keiyakuBranch.slice("keiyaku/".length);
499
+ const reason = buildDriveReason(directive, context);
500
+ try {
501
+ const plan = buildIteratePlan(title, traceState, traceContent, reason);
502
+ await appendReview(cwd, plan.targetRound, plan.reviewReason);
503
+ await git.addFiles(cwd, TRACE_FILE);
504
+ await git.commit(cwd, `keiyaku(${title}): iterate round ${plan.targetRound}`);
505
+ const { summary, subagentFailure } = await runAndRecordRound(cwd, title, plan.targetRound, plan.prompt, {
506
+ signal,
507
+ subagentName,
508
+ failureMode: "round_specific",
509
+ onAbort: async () => {
510
+ await appendRoundSystemNote(cwd, plan.targetRound, "Subagent execution cancelled by user/client.");
511
+ await git.addFiles(cwd, TRACE_FILE);
512
+ await git.commit(cwd, `keiyaku(${title}): round ${plan.targetRound} cancelled`);
513
+ },
514
+ });
515
+ const trace = await fs.readFile(path.join(cwd, TRACE_FILE), "utf-8");
516
+ const diff = await git.getKeiyakuDiff(cwd, baseBranch);
517
+ return {
518
+ round: plan.targetRound,
519
+ trace,
520
+ diff,
521
+ summary,
522
+ branch: keiyakuBranch,
523
+ baseBranch,
524
+ ...(subagentFailure ? { subagentFailure } : {}),
525
+ };
526
+ }
527
+ catch (err) {
528
+ throw wrapFlowError(`execute DRIVE (${keiyakuBranch})`, err);
529
+ }
530
+ }
@@ -0,0 +1,77 @@
1
+ export function buildStartPrompt(title, goal, directive) {
2
+ const goalBlock = `\nGoal (immutable):\n${goal}\n`;
3
+ const directiveBlock = directive ? `\nDirective (Round 1 only):\n${directive}\n` : "";
4
+ return `You are working on a keiyaku (${title}).${goalBlock}${directiveBlock}
5
+
6
+ This is Round 1 (initial implementation).
7
+
8
+ How to interpret these fields:
9
+ - Goal = the mission (what done looks like overall). Immutable.
10
+ - Directive = Round 1 focus/scope guardrail. If present, follow it even if it means not completing the full Goal in Round 1.
11
+
12
+ 1. Read KEIYAKU.md — this is the immutable mission definition (Goal, Context, Constraints, Acceptance Criteria). If the Goal shown above differs from KEIYAKU.md, treat KEIYAKU.md as the source of truth.
13
+ 2. If KEIYAKU.md has a "Focus" section, treat it as the implementation priority for this round.
14
+ 3. Treat both Base + Task Constraints and Base + Task Criteria as mandatory mission boundaries.
15
+ 4. Read KEIYAKU_TRACE.md for prior context if present.
16
+ 5. **CRITICAL EXECUTION**:
17
+ - **Directive is your Leash**: If a Directive is provided, focus ONLY on that scope. Do NOT try to be a hero and finish the entire Goal in Round 1. We iterate.
18
+ - **Constraints are Absolute**: Base Constraints and Task Constraints are both binding. Do NOT drift.
19
+ - **Criteria is the North Star**: Base Criteria and Task Criteria jointly define done. Keep both in view while executing the Directive.
20
+ - **Architect's Vision**: Respect the existing structure. Modify with precision, not with a sledgehammer.
21
+ 6. At the end, output a clear implementation report in plain text with exactly 4 sections:
22
+ - One line summary: What was done, what's the outcome.
23
+ - Modification: Files touched. What changed — explain the logic delta, not just what was edited. Evidence — paste raw output, logs, or observed behavior.
24
+ - Aesthetics Gap
25
+ Self-critique. Identify where this diff deviates from the ideal:
26
+
27
+ precise — right place, layer, abstraction
28
+ minimal — smallest diff that solves it
29
+ isolated — no unrelated changes riding along
30
+ idiomatic — fits the codebase's voice
31
+ cohesive — each unit does one thing
32
+ - Blindspots: What you're not sure about. Untested paths, unverified assumptions, worried-about edges.
33
+
34
+ Do NOT run git commands. Do NOT edit KEIYAKU_TRACE.md directly.`;
35
+ }
36
+ export function buildIteratePrompt(title, round, reason) {
37
+ return `You are working on a keiyaku (${title}).
38
+
39
+ This is Round ${round} (iteration after review).
40
+
41
+ Latest review reason you must address in this round:
42
+ ${reason}
43
+
44
+ 1. Read KEIYAKU.md — mission definition remains immutable (including Base + Task Constraints and Base + Task Criteria).
45
+ 2. If KEIYAKU.md has a "Focus" section, treat it as the implementation priority for this round.
46
+ 3. Read KEIYAKU_TRACE.md — review previous rounds and locate the latest "## Review ${round}" section.
47
+ 4. **STRICT ITERATION**:
48
+ - **Fix the Core**: Resolve the review reason first. No excuses.
49
+ - **Zero Drift**: While fixing, do NOT violate Base/Task Constraints or miss Base/Task Criteria.
50
+ - **Clean Diff**: Keep the modification surgical. I am watching.
51
+ 5. Modify the code accordingly.
52
+ 6. At the end, output a clear implementation report in plain text with exactly 4 sections:
53
+ - One line summary: What was done, what's the outcome.
54
+ - Modification: Files touched. What changed — explain the logic delta, not just what was edited. Evidence — paste raw output, logs, or observed behavior.
55
+ - Aesthetics Gap
56
+ Self-critique. Identify where this diff deviates from the ideal:
57
+
58
+ precise — right place, layer, abstraction
59
+ minimal — smallest diff that solves it
60
+ isolated — no unrelated changes riding along
61
+ idiomatic — fits the codebase's voice
62
+ cohesive — each unit does one thing
63
+ - Blindspots: What you're not sure about. Untested paths, unverified assumptions, worried-about edges.
64
+
65
+ Do NOT run git commands. Do NOT edit KEIYAKU_TRACE.md directly.`;
66
+ }
67
+ export function buildAskPrompt(request, context) {
68
+ return `You are in an ask session. No code changes. No git commands.
69
+
70
+ Request:
71
+ ${request}
72
+
73
+ Context:
74
+ ${context}
75
+
76
+ Provide a clear reasoning summary in plain text.`;
77
+ }