@astrosheep/keiyaku 0.1.19 → 0.1.21
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/.tsbuildinfo +1 -0
- package/build/agents/round-runner.js +22 -2
- package/build/common/constants.js +0 -1
- package/build/config/term-presets.js +3 -3
- package/build/utils/debug-log.js +3 -0
- package/build/utils/keiyaku-document.js +79 -20
- package/build/utils/keiyaku-document.test.js +7 -7
- package/build/workflow/drive.js +17 -10
- package/build/workflow/keiyaku-document-builder.js +15 -0
- package/build/workflow/keiyaku-draft.js +255 -0
- package/build/workflow/present.js +41 -24
- package/build/workflow/response-builders.js +4 -4
- package/build/workflow/round.js +12 -6
- package/build/workflow/start.js +24 -166
- package/package.json +3 -3
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { FlowError } from "../common/errors.js";
|
|
2
|
+
import { extractListItems, parseToAST, renderSectionContent, } from "../utils/keiyaku-document.js";
|
|
3
|
+
const KNOWN_DRAFT_SECTIONS = new Set([
|
|
4
|
+
"goal",
|
|
5
|
+
"directive",
|
|
6
|
+
"context",
|
|
7
|
+
"constraints",
|
|
8
|
+
"criteria",
|
|
9
|
+
"acceptance criteria",
|
|
10
|
+
]);
|
|
11
|
+
function normalizeSectionTitle(title) {
|
|
12
|
+
return title.trim().toLowerCase().replace(/\s+/g, " ");
|
|
13
|
+
}
|
|
14
|
+
function requireDraftText(name, value) {
|
|
15
|
+
const normalized = value.trim();
|
|
16
|
+
if (!normalized) {
|
|
17
|
+
throw new FlowError("EMPTY_PARAM", `parameter '${name}' cannot be empty`);
|
|
18
|
+
}
|
|
19
|
+
return normalized;
|
|
20
|
+
}
|
|
21
|
+
function parseFenceLine(trimmedLine) {
|
|
22
|
+
if (!trimmedLine.startsWith("```"))
|
|
23
|
+
return null;
|
|
24
|
+
let idx = 0;
|
|
25
|
+
while (idx < trimmedLine.length && trimmedLine[idx] === "`")
|
|
26
|
+
idx += 1;
|
|
27
|
+
if (idx < 3)
|
|
28
|
+
return null;
|
|
29
|
+
return idx;
|
|
30
|
+
}
|
|
31
|
+
function splitHeadingGroups(sectionNode) {
|
|
32
|
+
const firstMeaningful = sectionNode.children.find((child) => {
|
|
33
|
+
if (child.type !== "text")
|
|
34
|
+
return true;
|
|
35
|
+
return child.value.trim().length > 0;
|
|
36
|
+
});
|
|
37
|
+
if (!firstMeaningful || firstMeaningful.type !== "heading" || firstMeaningful.level < 2) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
const groups = [];
|
|
41
|
+
let currentChildren = [];
|
|
42
|
+
const commit = () => {
|
|
43
|
+
if (currentChildren.length === 0)
|
|
44
|
+
return;
|
|
45
|
+
groups.push({
|
|
46
|
+
type: "section",
|
|
47
|
+
level: 2,
|
|
48
|
+
title: "",
|
|
49
|
+
children: currentChildren,
|
|
50
|
+
});
|
|
51
|
+
currentChildren = [];
|
|
52
|
+
};
|
|
53
|
+
for (const child of sectionNode.children) {
|
|
54
|
+
if (child.type === "heading" && child.level >= 2) {
|
|
55
|
+
commit();
|
|
56
|
+
currentChildren = [child];
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
currentChildren.push(child);
|
|
60
|
+
}
|
|
61
|
+
commit();
|
|
62
|
+
return groups
|
|
63
|
+
.map((group) => renderSectionContent(group).trim())
|
|
64
|
+
.filter((item) => item.length > 0);
|
|
65
|
+
}
|
|
66
|
+
function collectSectionItems(ast, sectionTitle) {
|
|
67
|
+
const normalizedSectionTitle = normalizeSectionTitle(sectionTitle);
|
|
68
|
+
const targetSection = ast.children.find((node) => {
|
|
69
|
+
if (node.type !== "section")
|
|
70
|
+
return false;
|
|
71
|
+
if (node.level !== 2)
|
|
72
|
+
return false;
|
|
73
|
+
return normalizeSectionTitle(node.title) === normalizedSectionTitle;
|
|
74
|
+
});
|
|
75
|
+
if (!targetSection || targetSection.type !== "section") {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
const listItems = extractListItems(targetSection);
|
|
79
|
+
if (listItems.length > 0) {
|
|
80
|
+
return listItems;
|
|
81
|
+
}
|
|
82
|
+
const grouped = splitHeadingGroups(targetSection);
|
|
83
|
+
if (grouped.length > 0) {
|
|
84
|
+
return grouped;
|
|
85
|
+
}
|
|
86
|
+
const content = renderSectionContent(targetSection).trim();
|
|
87
|
+
return content ? [content] : [];
|
|
88
|
+
}
|
|
89
|
+
function normalizeDraftHeadings(content) {
|
|
90
|
+
const lines = content.split(/\r?\n/);
|
|
91
|
+
if (lines.length > 0) {
|
|
92
|
+
lines[0] = lines[0].replace(/^\uFEFF/, "");
|
|
93
|
+
}
|
|
94
|
+
// Keep structural title/known section headings stable, and shift payload headings together.
|
|
95
|
+
const MIN_HEADING_LEVEL = 3;
|
|
96
|
+
let sawTitleH1 = false;
|
|
97
|
+
let fence = null;
|
|
98
|
+
let shallowestNonStructural = null;
|
|
99
|
+
for (const line of lines) {
|
|
100
|
+
let leadingSpaces = 0;
|
|
101
|
+
while (leadingSpaces < line.length && line[leadingSpaces] === " ")
|
|
102
|
+
leadingSpaces += 1;
|
|
103
|
+
const trimmed = leadingSpaces <= 3 ? line.trimStart() : line;
|
|
104
|
+
if (leadingSpaces <= 3) {
|
|
105
|
+
const fenceLen = parseFenceLine(trimmed);
|
|
106
|
+
if (fenceLen !== null) {
|
|
107
|
+
if (!fence)
|
|
108
|
+
fence = { length: fenceLen };
|
|
109
|
+
else if (fenceLen >= fence.length)
|
|
110
|
+
fence = null;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (fence)
|
|
115
|
+
continue;
|
|
116
|
+
if (leadingSpaces > 3)
|
|
117
|
+
continue;
|
|
118
|
+
const headerMatch = trimmed.match(/^(#{1,6})[ \t]+(.+?)\s*$/);
|
|
119
|
+
if (!headerMatch)
|
|
120
|
+
continue;
|
|
121
|
+
const level = (headerMatch[1] ?? "").length;
|
|
122
|
+
const text = (headerMatch[2] ?? "").trim();
|
|
123
|
+
if (level === 1 && !sawTitleH1) {
|
|
124
|
+
sawTitleH1 = true;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (level === 2) {
|
|
128
|
+
// Keep ALL H2 section headers structural in from_file drafts. Unknown headers
|
|
129
|
+
// should remain visible to validation instead of being silently demoted.
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
shallowestNonStructural = shallowestNonStructural === null ? level : Math.min(shallowestNonStructural, level);
|
|
133
|
+
}
|
|
134
|
+
if (shallowestNonStructural === null || shallowestNonStructural >= MIN_HEADING_LEVEL) {
|
|
135
|
+
return lines.join("\n");
|
|
136
|
+
}
|
|
137
|
+
const delta = MIN_HEADING_LEVEL - shallowestNonStructural;
|
|
138
|
+
const out = [];
|
|
139
|
+
sawTitleH1 = false;
|
|
140
|
+
fence = null;
|
|
141
|
+
for (const line of lines) {
|
|
142
|
+
let leadingSpaces = 0;
|
|
143
|
+
while (leadingSpaces < line.length && line[leadingSpaces] === " ")
|
|
144
|
+
leadingSpaces += 1;
|
|
145
|
+
const trimmed = leadingSpaces <= 3 ? line.trimStart() : line;
|
|
146
|
+
const prefix = line.slice(0, leadingSpaces);
|
|
147
|
+
if (leadingSpaces <= 3) {
|
|
148
|
+
const fenceLen = parseFenceLine(trimmed);
|
|
149
|
+
if (fenceLen !== null) {
|
|
150
|
+
if (!fence)
|
|
151
|
+
fence = { length: fenceLen };
|
|
152
|
+
else if (fenceLen >= fence.length)
|
|
153
|
+
fence = null;
|
|
154
|
+
out.push(line);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (fence) {
|
|
159
|
+
out.push(line);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (leadingSpaces <= 3) {
|
|
163
|
+
const headerMatch = trimmed.match(/^(#{1,6})[ \t]+(.+?)\s*$/);
|
|
164
|
+
if (headerMatch) {
|
|
165
|
+
const hashes = headerMatch[1] ?? "";
|
|
166
|
+
const text = (headerMatch[2] ?? "").trim();
|
|
167
|
+
const level = hashes.length;
|
|
168
|
+
if (level === 1) {
|
|
169
|
+
if (!sawTitleH1) {
|
|
170
|
+
sawTitleH1 = true;
|
|
171
|
+
out.push(line);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
out.push(`${prefix}${"#".repeat(Math.min(6, level + delta))} ${text}`);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (level === 2) {
|
|
178
|
+
out.push(line);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
out.push(`${prefix}${"#".repeat(Math.min(6, level + delta))} ${text}`);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
out.push(line);
|
|
186
|
+
}
|
|
187
|
+
return out.join("\n");
|
|
188
|
+
}
|
|
189
|
+
export function normalizeDraftMarkdownListItems(name, values) {
|
|
190
|
+
const normalized = [];
|
|
191
|
+
for (const [index, value] of values.entries()) {
|
|
192
|
+
const normalizedValue = requireDraftText(`${name}[${index}]`, value);
|
|
193
|
+
const ast = parseToAST(normalizedValue, { allowSections: false });
|
|
194
|
+
const sectionNode = { type: "section", level: 2, title: "", children: ast.children };
|
|
195
|
+
const parsedItems = extractListItems(sectionNode);
|
|
196
|
+
if (parsedItems.length > 0) {
|
|
197
|
+
normalized.push(...parsedItems);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const grouped = splitHeadingGroups(sectionNode);
|
|
201
|
+
if (grouped.length > 0) {
|
|
202
|
+
normalized.push(...grouped);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const rendered = renderSectionContent(sectionNode).trim();
|
|
206
|
+
if (rendered.length > 0) {
|
|
207
|
+
normalized.push(rendered);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
normalized.push(normalizedValue);
|
|
211
|
+
}
|
|
212
|
+
return normalized;
|
|
213
|
+
}
|
|
214
|
+
export function parseAndValidateKeiyakuDraft(content) {
|
|
215
|
+
const normalized = normalizeDraftHeadings(content);
|
|
216
|
+
const ast = parseToAST(normalized);
|
|
217
|
+
const sections = new Map();
|
|
218
|
+
let title;
|
|
219
|
+
for (const node of ast.children) {
|
|
220
|
+
if (node.type !== "section")
|
|
221
|
+
continue;
|
|
222
|
+
if (node.level === 1 && !title) {
|
|
223
|
+
title = node.title.trim() || undefined;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (node.level !== 2)
|
|
227
|
+
continue;
|
|
228
|
+
const normalizedSection = normalizeSectionTitle(node.title);
|
|
229
|
+
if (sections.has(normalizedSection)) {
|
|
230
|
+
throw new FlowError("INVALID_KEIYAKU_DRAFT", `invalid keiyaku draft: duplicate section header '${normalizedSection}'`);
|
|
231
|
+
}
|
|
232
|
+
sections.set(normalizedSection, renderSectionContent(node).trim());
|
|
233
|
+
}
|
|
234
|
+
const unknownSections = [...sections.keys()].filter((section) => !KNOWN_DRAFT_SECTIONS.has(section));
|
|
235
|
+
if (unknownSections.length > 0) {
|
|
236
|
+
throw new FlowError("INVALID_KEIYAKU_DRAFT", `invalid keiyaku draft: unknown section header(s): ${unknownSections.map((section) => `'${section}'`).join(", ")}`);
|
|
237
|
+
}
|
|
238
|
+
const goal = sections.get("goal")?.trim();
|
|
239
|
+
const directive = sections.get("directive")?.trim();
|
|
240
|
+
const context = sections.get("context")?.trim();
|
|
241
|
+
const constraints = sections.has("constraints") ? collectSectionItems(ast, "constraints") : undefined;
|
|
242
|
+
const criteria = sections.has("acceptance criteria")
|
|
243
|
+
? collectSectionItems(ast, "acceptance criteria")
|
|
244
|
+
: sections.has("criteria")
|
|
245
|
+
? collectSectionItems(ast, "criteria")
|
|
246
|
+
: undefined;
|
|
247
|
+
return {
|
|
248
|
+
title: title?.trim() || undefined,
|
|
249
|
+
goal: goal || undefined,
|
|
250
|
+
directive: directive || undefined,
|
|
251
|
+
context: context || undefined,
|
|
252
|
+
constraints: constraints && constraints.length > 0 ? constraints : undefined,
|
|
253
|
+
criteria: criteria && criteria.length > 0 ? criteria : undefined,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as fs from "fs/promises";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import { KEIYAKU_DRAFT_FILE,
|
|
3
|
+
import { KEIYAKU_DRAFT_FILE, KEIYAKU_FILE, TRACE_FILE } from "../common/constants.js";
|
|
4
4
|
import { resolveTermPreset } from "../config/term-presets.js";
|
|
5
|
-
import { appendDebugLog } from "../utils/debug-log.js";
|
|
5
|
+
import { appendDebugLog, shouldEmitProgressLogs } from "../utils/debug-log.js";
|
|
6
6
|
import { FlowError, wrapFlowError } from "../common/errors.js";
|
|
7
7
|
import * as git from "../utils/git.js";
|
|
8
8
|
import { computeTraceState, readTraceContent } from "../utils/trace.js";
|
|
@@ -98,20 +98,6 @@ function requireChecks(name, values) {
|
|
|
98
98
|
async function removeClaimProtocolFiles(cwd) {
|
|
99
99
|
await fs.unlink(path.join(cwd, KEIYAKU_FILE));
|
|
100
100
|
await fs.unlink(path.join(cwd, TRACE_FILE));
|
|
101
|
-
const draftCandidates = [KEIYAKU_DRAFT_FILE, KEIYAKU_DRAFT_NEW_FILE];
|
|
102
|
-
for (const draftPath of draftCandidates) {
|
|
103
|
-
const tracked = await git.isPathTracked(cwd, draftPath);
|
|
104
|
-
if (tracked)
|
|
105
|
-
continue;
|
|
106
|
-
try {
|
|
107
|
-
await fs.unlink(path.join(cwd, draftPath));
|
|
108
|
-
}
|
|
109
|
-
catch (error) {
|
|
110
|
-
if (error?.code !== "ENOENT") {
|
|
111
|
-
throw error;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
101
|
}
|
|
116
102
|
function formatCloseDiffSummary(stats, baseBranch) {
|
|
117
103
|
return `Range ${baseBranch}...HEAD | Files ${stats.filesChanged} | +${stats.insertions} / -${stats.deletions}`;
|
|
@@ -134,6 +120,7 @@ export async function presentWork(input) {
|
|
|
134
120
|
const petition = input.petition;
|
|
135
121
|
if (petition === "FORFEIT") {
|
|
136
122
|
let round = 0;
|
|
123
|
+
let preservedDraftContent;
|
|
137
124
|
try {
|
|
138
125
|
const trace = await fs.readFile(path.join(cwd, TRACE_FILE), "utf-8");
|
|
139
126
|
round = computeTraceState(trace).maxRound;
|
|
@@ -141,6 +128,14 @@ export async function presentWork(input) {
|
|
|
141
128
|
catch {
|
|
142
129
|
round = 0;
|
|
143
130
|
}
|
|
131
|
+
try {
|
|
132
|
+
preservedDraftContent = await fs.readFile(path.join(cwd, KEIYAKU_DRAFT_FILE), "utf-8");
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (error?.code !== "ENOENT") {
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
144
139
|
const dirtyFiles = await git.getDirtyFiles(cwd);
|
|
145
140
|
const droppedChanges = dirtyFiles.map((file) => `${file.index}${file.working_dir} ${file.path}`);
|
|
146
141
|
try {
|
|
@@ -150,6 +145,22 @@ export async function presentWork(input) {
|
|
|
150
145
|
await git.checkoutBranch(cwd, baseBranch);
|
|
151
146
|
await git.deleteBranch(cwd, keiyakuBranch, true);
|
|
152
147
|
await git.clearKeiyakuBase(cwd, keiyakuBranch);
|
|
148
|
+
if (preservedDraftContent !== undefined) {
|
|
149
|
+
const canonicalDraftPath = path.join(cwd, KEIYAKU_DRAFT_FILE);
|
|
150
|
+
try {
|
|
151
|
+
// Restore as untracked recovery artifact on the base branch.
|
|
152
|
+
await fs.writeFile(canonicalDraftPath, preservedDraftContent, { flag: "wx" });
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (error?.code === "EEXIST") {
|
|
156
|
+
const fallbackDraftFile = "KEIYAKU.draft.forfeit.md";
|
|
157
|
+
await fs.writeFile(path.join(cwd, fallbackDraftFile), preservedDraftContent, { flag: "wx" });
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
153
164
|
}
|
|
154
165
|
catch (err) {
|
|
155
166
|
throw wrapFlowError(`execute FORFEIT (${keiyakuBranch} -> ${baseBranch})`, err);
|
|
@@ -197,35 +208,41 @@ export async function presentWork(input) {
|
|
|
197
208
|
if (!oathMatches(input.oath, expectedOath)) {
|
|
198
209
|
throw new FlowError("OATH_MISMATCH", `Oath mismatch. Correct oath: ${expectedOath}`);
|
|
199
210
|
}
|
|
200
|
-
await assertCleanWorkingTree(cwd, [KEIYAKU_DRAFT_FILE
|
|
211
|
+
await assertCleanWorkingTree(cwd, [KEIYAKU_DRAFT_FILE]);
|
|
201
212
|
try {
|
|
202
213
|
const invokeDiffLog = `[CLAIM] Collecting diff preview against base '${baseBranch}'`;
|
|
203
|
-
|
|
214
|
+
if (shouldEmitProgressLogs())
|
|
215
|
+
console.error(invokeDiffLog);
|
|
204
216
|
appendDebugLog(invokeDiffLog, { cwd, section: "script" });
|
|
205
217
|
const invokeReadLog = "[CLAIM] Reading keiyaku protocol files";
|
|
206
|
-
|
|
218
|
+
if (shouldEmitProgressLogs())
|
|
219
|
+
console.error(invokeReadLog);
|
|
207
220
|
appendDebugLog(invokeReadLog, { cwd, section: "script" });
|
|
208
221
|
const keiyakuContent = await fs.readFile(path.join(cwd, KEIYAKU_FILE), "utf-8");
|
|
209
222
|
const message = buildMergeMessage(title, keiyakuContent, traceContent);
|
|
210
223
|
const diffStats = await git.getDiffStats(cwd, baseBranch);
|
|
211
224
|
const diff = formatCloseDiffSummary(diffStats, baseBranch);
|
|
212
225
|
const invokeCleanupLog = "[CLAIM] Removing protocol files and creating cleanup commit";
|
|
213
|
-
|
|
226
|
+
if (shouldEmitProgressLogs())
|
|
227
|
+
console.error(invokeCleanupLog);
|
|
214
228
|
appendDebugLog(invokeCleanupLog, { cwd, section: "script" });
|
|
215
229
|
await removeClaimProtocolFiles(cwd);
|
|
216
|
-
await git.addFiles(cwd,
|
|
230
|
+
await git.addFiles(cwd, [KEIYAKU_FILE, TRACE_FILE]);
|
|
217
231
|
await git.commit(cwd, `keiyaku(${title}): cleanup`);
|
|
218
232
|
const invokeCheckoutLog = `[CLAIM] Checking out base branch '${baseBranch}'`;
|
|
219
|
-
|
|
233
|
+
if (shouldEmitProgressLogs())
|
|
234
|
+
console.error(invokeCheckoutLog);
|
|
220
235
|
appendDebugLog(invokeCheckoutLog, { cwd, section: "script" });
|
|
221
236
|
await git.checkoutBranch(cwd, baseBranch);
|
|
222
237
|
const invokeMergeLog = `[CLAIM] Merging '${keiyakuBranch}' into '${baseBranch}'`;
|
|
223
|
-
|
|
238
|
+
if (shouldEmitProgressLogs())
|
|
239
|
+
console.error(invokeMergeLog);
|
|
224
240
|
appendDebugLog(invokeMergeLog, { cwd, section: "script" });
|
|
225
241
|
await git.merge(cwd, keiyakuBranch, message);
|
|
226
242
|
const commit = await git.getLatestCommitHash(cwd);
|
|
227
243
|
const invokeFinalizeLog = `[CLAIM] Deleting merged branch '${keiyakuBranch}' and clearing metadata`;
|
|
228
|
-
|
|
244
|
+
if (shouldEmitProgressLogs())
|
|
245
|
+
console.error(invokeFinalizeLog);
|
|
229
246
|
appendDebugLog(invokeFinalizeLog, { cwd, section: "script" });
|
|
230
247
|
await git.deleteBranch(cwd, keiyakuBranch, true);
|
|
231
248
|
await git.clearKeiyakuBase(cwd, keiyakuBranch);
|
|
@@ -200,8 +200,8 @@ export function buildCloseDoneResponse(result, input) {
|
|
|
200
200
|
...formatMaybe("Oath", input.oath, 220),
|
|
201
201
|
];
|
|
202
202
|
const text = assembleResponse("✓ Keiyaku Fulfilled (CLAIM)", result.commit
|
|
203
|
-
? `Merged '${result.deletedBranch}' into '${result.mergedInto}' [${result.commit}]. Deleted feature branch.`
|
|
204
|
-
: `Merged '${result.deletedBranch}' into '${result.mergedInto}'. Deleted feature branch.`, [typeof diffSection === "string" ? null : diffSection].filter((section) => section !== null), infoLines);
|
|
203
|
+
? `Merged '${result.deletedBranch}' into '${result.mergedInto}' [${result.commit}]. Deleted feature branch. Note: KEIYAKU.draft.md preserved (if present).`
|
|
204
|
+
: `Merged '${result.deletedBranch}' into '${result.mergedInto}'. Deleted feature branch. Note: KEIYAKU.draft.md preserved (if present).`, [typeof diffSection === "string" ? null : diffSection].filter((section) => section !== null), infoLines);
|
|
205
205
|
return {
|
|
206
206
|
content: [{ type: "text", text }],
|
|
207
207
|
structuredContent: buildSuccessStructuredContent(closeToolName, resultData),
|
|
@@ -219,8 +219,8 @@ export function buildCloseDropResponse(result, input) {
|
|
|
219
219
|
...formatMaybe("Base Branch", result.baseBranch, 200),
|
|
220
220
|
];
|
|
221
221
|
const text = assembleResponse("✗ Keiyaku Forfeited (FORFEIT)", droppedChanges.length > 0
|
|
222
|
-
? `Deleted '${result.deletedBranch}'. Switched back to '${result.baseBranch}'. Dropped ${droppedChanges.length} local change(s).`
|
|
223
|
-
: `Deleted '${result.deletedBranch}'. Switched back to '${result.baseBranch}'.`, [warningSection].filter((section) => section !== null), infoLines);
|
|
222
|
+
? `Deleted '${result.deletedBranch}'. Switched back to '${result.baseBranch}'. Dropped ${droppedChanges.length} local change(s). Note: KEIYAKU.draft.md preserved (if present).`
|
|
223
|
+
: `Deleted '${result.deletedBranch}'. Switched back to '${result.baseBranch}'. Note: KEIYAKU.draft.md preserved (if present).`, [warningSection].filter((section) => section !== null), infoLines);
|
|
224
224
|
return {
|
|
225
225
|
content: [{ type: "text", text }],
|
|
226
226
|
structuredContent: buildSuccessStructuredContent(closeToolName, resultData),
|
package/build/workflow/round.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { selectSubagent } from "../agents/selector.js";
|
|
2
2
|
import { describeSubagentFailure, runSubagent } from "../agents/round-runner.js";
|
|
3
|
-
import { TRACE_FILE } from "../common/constants.js";
|
|
3
|
+
import { KEIYAKU_DRAFT_FILE, TRACE_FILE } from "../common/constants.js";
|
|
4
4
|
import { FlowError } from "../common/errors.js";
|
|
5
|
-
import { appendDebugBlock, appendDebugLog } from "../utils/debug-log.js";
|
|
5
|
+
import { appendDebugBlock, appendDebugLog, shouldEmitProgressLogs } from "../utils/debug-log.js";
|
|
6
6
|
import * as git from "../utils/git.js";
|
|
7
7
|
import { appendRoundReport } from "../utils/trace.js";
|
|
8
8
|
import { parseRoundSummary } from "./round-summary.js";
|
|
@@ -46,6 +46,10 @@ async function appendRoundResult(cwd, round, summary, failureMessage) {
|
|
|
46
46
|
filesModified,
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
|
+
async function stageRoundCommit(cwd) {
|
|
50
|
+
// Never stage draft recovery artifacts; they should remain local unless explicitly committed by the user.
|
|
51
|
+
await git.addFiles(cwd, ["-A", "--", ".", `:(exclude)${KEIYAKU_DRAFT_FILE}`]);
|
|
52
|
+
}
|
|
49
53
|
export async function runAndRecordRound(cwd, titleToken, round, prompt, options) {
|
|
50
54
|
const roundSubagent = selectSubagent(options.name);
|
|
51
55
|
let rawSummary;
|
|
@@ -62,11 +66,13 @@ export async function runAndRecordRound(cwd, titleToken, round, prompt, options)
|
|
|
62
66
|
}
|
|
63
67
|
const failure = describeSubagentFailure(err, roundSubagent.displayName, round);
|
|
64
68
|
const failureLog = `[Subagent failure] name=${failure.name} round=${failure.round} code=${failure.errorCode} timeoutMs=${failure.timeoutMs ?? "unknown"} exitCode=${failure.exitCode ?? "none"}`;
|
|
65
|
-
|
|
69
|
+
if (shouldEmitProgressLogs())
|
|
70
|
+
console.error(failureLog);
|
|
66
71
|
appendDebugLog(failureLog, { cwd, section: "script" });
|
|
67
72
|
if (failure.stderrSnippet) {
|
|
68
73
|
const failureStderrLog = `[Subagent failure stderr]\n${failure.stderrSnippet}`;
|
|
69
|
-
|
|
74
|
+
if (shouldEmitProgressLogs())
|
|
75
|
+
console.error(failureStderrLog);
|
|
70
76
|
appendDebugBlock("subagent failure stderr", failure.stderrSnippet, { cwd, section: "codex-stderr" });
|
|
71
77
|
}
|
|
72
78
|
failureMessage = failure.message.trim() || "Unknown error.";
|
|
@@ -74,12 +80,12 @@ export async function runAndRecordRound(cwd, titleToken, round, prompt, options)
|
|
|
74
80
|
? `Round ${round} completed with subagent execution failure recorded in trace.`
|
|
75
81
|
: "Round 1 completed with subagent execution failure recorded in trace.";
|
|
76
82
|
await appendRoundResult(cwd, round, summary, failureMessage);
|
|
77
|
-
await
|
|
83
|
+
await stageRoundCommit(cwd);
|
|
78
84
|
await git.commit(cwd, `keiyaku(${titleToken}): round ${round}`);
|
|
79
85
|
throw new FlowError("ROUND_SUBAGENT_FAILED", formatSubagentFailureMessage(failure), err);
|
|
80
86
|
}
|
|
81
87
|
await appendRoundResult(cwd, round, rawSummary);
|
|
82
|
-
await
|
|
88
|
+
await stageRoundCommit(cwd);
|
|
83
89
|
await git.commit(cwd, `keiyaku(${titleToken}): round ${round}`);
|
|
84
90
|
return { roundSummary: parseRoundSummary(rawSummary) };
|
|
85
91
|
}
|