@astrosheep/keiyaku 0.1.76 → 0.1.78

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.
Files changed (53) hide show
  1. package/README.md +88 -96
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/config/apply-argument-descriptions.js +1 -1
  4. package/build/config/base-rules.js +14 -7
  5. package/build/config/dotenv.js +17 -11
  6. package/build/config/keiyaku-home.js +9 -0
  7. package/build/config/settings.js +41 -24
  8. package/build/config/term-presets/resolver.js +0 -3
  9. package/build/errno.js +3 -0
  10. package/build/flow-error.js +2 -0
  11. package/build/generated/version.js +1 -1
  12. package/build/git/diff/constants.js +1 -0
  13. package/build/git/diff/filter.js +3 -18
  14. package/build/git/diff/parsers.js +149 -61
  15. package/build/git/diff/preview.js +16 -2
  16. package/build/git/diff/read.js +32 -20
  17. package/build/git/snapshot.js +5 -24
  18. package/build/git/worktree.js +5 -4
  19. package/build/mcp/responses.js +3 -2
  20. package/build/mcp/server.js +61 -69
  21. package/build/protocol/draft-artifacts.js +2 -1
  22. package/build/protocol/file-guards.js +2 -1
  23. package/build/protocol/markdown/lex.js +52 -14
  24. package/build/protocol/markdown/normalization.js +3 -2
  25. package/build/protocol/markdown/parser.js +2 -2
  26. package/build/protocol/response-history.js +44 -5
  27. package/build/protocol/status-previews.js +20 -8
  28. package/build/protocol/summon-draft.js +3 -2
  29. package/build/protocol/summon-input.js +1 -0
  30. package/build/protocol/trace.js +1 -1
  31. package/build/tools/amend/index.js +11 -21
  32. package/build/tools/ask/index.js +11 -18
  33. package/build/tools/ask/persist.js +60 -37
  34. package/build/tools/ask/run.js +17 -7
  35. package/build/tools/create-handler.js +31 -0
  36. package/build/tools/drive/index.js +11 -24
  37. package/build/tools/drive/run.js +9 -5
  38. package/build/tools/petition/claim-gates.js +38 -9
  39. package/build/tools/petition/claim.js +20 -2
  40. package/build/tools/petition/forfeit.js +4 -1
  41. package/build/tools/petition/index.js +43 -58
  42. package/build/tools/petition/run.js +12 -0
  43. package/build/tools/round/head-guard.js +10 -0
  44. package/build/tools/round/incremental-diff.js +6 -2
  45. package/build/tools/round/report.js +24 -2
  46. package/build/tools/round/run.js +6 -0
  47. package/build/tools/round/worktree.js +6 -2
  48. package/build/tools/status/index.js +11 -24
  49. package/build/tools/status/read.js +6 -4
  50. package/build/tools/summon/index.js +17 -27
  51. package/build/tools/summon/run.js +21 -18
  52. package/package.json +6 -6
  53. package/build/git/diff/stat.js +0 -9
@@ -1,7 +1,7 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { resolveOath } from "../protocol/oath.js";
3
- import { amendToolSchema, askToolSchemaBase, attachAskResponsePathValidation, summonToolSchema, driveToolSchema, petitionToolSchema, helpToolSchema, statusToolSchema, } from "../tools/schema.js";
4
- import { getAvailableNamesForPreset, listTermPresets, resolveTermPreset } from "../config/term-presets/resolver.js";
3
+ import { amendToolSchema, askToolSchema, summonToolSchema, driveToolSchema, petitionToolSchema, helpToolSchema, statusToolSchema, } from "../tools/schema.js";
4
+ import { getAvailableNamesForPreset, resolveTermPreset } from "../config/term-presets/resolver.js";
5
5
  import { renderPreset } from "../config/render-template.js";
6
6
  import { applyArgumentDescriptions } from "../config/apply-argument-descriptions.js";
7
7
  import { createAmendHandler } from "../tools/amend/index.js";
@@ -13,9 +13,6 @@ import { createHelpHandler } from "../tools/help.js";
13
13
  import { createStatusHandler } from "../tools/status/index.js";
14
14
  function registerTools(server, packageVersion) {
15
15
  const preset = resolveTermPreset();
16
- const presetIdentities = listTermPresets()
17
- .map((item) => `${item.id}=${item.identity}`)
18
- .join(", ");
19
16
  const availableNames = getAvailableNamesForPreset(preset).join(", ");
20
17
  const currentOath = resolveOath();
21
18
  const renderedPreset = renderPreset(preset, {
@@ -25,73 +22,68 @@ function registerTools(server, packageVersion) {
25
22
  amend: preset.tools.amend.name,
26
23
  close: preset.tools.close.name,
27
24
  identity: preset.identity,
28
- preset_identities: presetIdentities,
29
25
  available_names: availableNames,
30
26
  oath_text: `'${currentOath}'`,
31
27
  });
32
- const startPreset = renderedPreset.tools.start;
33
- const drivePreset = renderedPreset.tools.drive;
34
- const askPreset = renderedPreset.tools.ask;
35
- const amendPreset = renderedPreset.tools.amend;
36
- const closePreset = renderedPreset.tools.close;
37
- const helpPreset = renderedPreset.tools.help;
38
- const statusPreset = renderedPreset.tools.status;
39
- const dynamicCloseSchema = applyArgumentDescriptions(petitionToolSchema, {
40
- ...closePreset.args,
41
- });
42
- const dynamicStartSchema = applyArgumentDescriptions(summonToolSchema, {
43
- ...startPreset.args,
44
- });
45
- const dynamicDriveSchema = applyArgumentDescriptions(driveToolSchema, {
46
- ...drivePreset.args,
47
- });
48
- const dynamicAskSchema = attachAskResponsePathValidation(applyArgumentDescriptions(askToolSchemaBase, {
49
- ...askPreset.args,
50
- }));
51
- const dynamicAmendSchema = applyArgumentDescriptions(amendToolSchema, {
52
- ...amendPreset.args,
53
- });
54
- const dynamicHelpSchema = applyArgumentDescriptions(helpToolSchema, {
55
- ...helpPreset.args,
56
- });
57
- const dynamicStatusSchema = applyArgumentDescriptions(statusToolSchema, {
58
- ...statusPreset.args,
59
- });
60
- server.registerTool(startPreset.name, {
61
- title: startPreset.title,
62
- description: startPreset.description,
63
- inputSchema: dynamicStartSchema,
64
- }, createSummonHandler());
65
- server.registerTool(drivePreset.name, {
66
- title: drivePreset.title,
67
- description: drivePreset.description,
68
- inputSchema: dynamicDriveSchema,
69
- }, createDriveHandler());
70
- server.registerTool(askPreset.name, {
71
- title: askPreset.title,
72
- description: askPreset.description,
73
- inputSchema: dynamicAskSchema,
74
- }, createAskHandler());
75
- server.registerTool(amendPreset.name, {
76
- title: amendPreset.title,
77
- description: amendPreset.description,
78
- inputSchema: dynamicAmendSchema,
79
- }, createAmendHandler());
80
- server.registerTool(closePreset.name, {
81
- title: closePreset.title,
82
- description: closePreset.description,
83
- inputSchema: dynamicCloseSchema,
84
- }, createPetitionHandler());
85
- server.registerTool(helpPreset.name, {
86
- title: helpPreset.title,
87
- description: helpPreset.description,
88
- inputSchema: dynamicHelpSchema,
89
- }, createHelpHandler(renderedPreset, packageVersion));
90
- server.registerTool(statusPreset.name, {
91
- title: statusPreset.title,
92
- description: statusPreset.description,
93
- inputSchema: dynamicStatusSchema,
94
- }, createStatusHandler());
28
+ const tools = [
29
+ {
30
+ key: "start",
31
+ schema: applyArgumentDescriptions(summonToolSchema, {
32
+ ...renderedPreset.tools.start.args,
33
+ }),
34
+ handler: createSummonHandler(),
35
+ },
36
+ {
37
+ key: "drive",
38
+ schema: applyArgumentDescriptions(driveToolSchema, {
39
+ ...renderedPreset.tools.drive.args,
40
+ }),
41
+ handler: createDriveHandler(),
42
+ },
43
+ {
44
+ key: "ask",
45
+ schema: applyArgumentDescriptions(askToolSchema, {
46
+ ...renderedPreset.tools.ask.args,
47
+ }),
48
+ handler: createAskHandler(),
49
+ },
50
+ {
51
+ key: "amend",
52
+ schema: applyArgumentDescriptions(amendToolSchema, {
53
+ ...renderedPreset.tools.amend.args,
54
+ }),
55
+ handler: createAmendHandler(),
56
+ },
57
+ {
58
+ key: "close",
59
+ schema: applyArgumentDescriptions(petitionToolSchema, {
60
+ ...renderedPreset.tools.close.args,
61
+ }),
62
+ handler: createPetitionHandler(),
63
+ },
64
+ {
65
+ key: "help",
66
+ schema: applyArgumentDescriptions(helpToolSchema, {
67
+ ...renderedPreset.tools.help.args,
68
+ }),
69
+ handler: createHelpHandler(renderedPreset, packageVersion),
70
+ },
71
+ {
72
+ key: "status",
73
+ schema: applyArgumentDescriptions(statusToolSchema, {
74
+ ...renderedPreset.tools.status.args,
75
+ }),
76
+ handler: createStatusHandler(),
77
+ },
78
+ ];
79
+ for (const tool of tools) {
80
+ const toolPreset = renderedPreset.tools[tool.key];
81
+ server.registerTool(toolPreset.name, {
82
+ title: toolPreset.title,
83
+ description: toolPreset.description,
84
+ inputSchema: tool.schema,
85
+ }, tool.handler);
86
+ }
95
87
  }
96
88
  export function createServer(packageVersion) {
97
89
  const server = new McpServer({
@@ -1,6 +1,7 @@
1
1
  import * as fs from "fs/promises";
2
2
  import * as path from "path";
3
3
  import { DRAFT_ARTIFACT_NOTICE, KEIYAKU_DRAFT_DIR } from "../keiyaku.js";
4
+ import { isErrnoException } from "../errno.js";
4
5
  const DRAFT_SLUG_FALLBACK = "keiyaku-draft";
5
6
  function normalizeDraftRelativePath(relativePath) {
6
7
  return relativePath.split(path.sep).join("/");
@@ -63,7 +64,7 @@ export async function listDraftArtifacts(cwd) {
63
64
  .sort((a, b) => b.localeCompare(a));
64
65
  }
65
66
  catch (error) {
66
- if (error?.code === "ENOENT") {
67
+ if (isErrnoException(error) && error.code === "ENOENT") {
67
68
  return [];
68
69
  }
69
70
  throw error;
@@ -1,6 +1,7 @@
1
1
  import * as fs from "fs/promises";
2
2
  import * as path from "path";
3
3
  import { KEIYAKU_FILE, TRACE_FILE } from "../keiyaku.js";
4
+ import { isErrnoException } from "../errno.js";
4
5
  import { FlowError } from "../flow-error.js";
5
6
  async function isMissingFile(filePath) {
6
7
  try {
@@ -8,7 +9,7 @@ async function isMissingFile(filePath) {
8
9
  return false;
9
10
  }
10
11
  catch (error) {
11
- if (error?.code === "ENOENT") {
12
+ if (isErrnoException(error) && error.code === "ENOENT") {
12
13
  return true;
13
14
  }
14
15
  throw error;
@@ -1,3 +1,4 @@
1
+ const FENCE_DELIMITERS = ["`", "~"];
1
2
  function countLeadingSpaces(line) {
2
3
  let idx = 0;
3
4
  while (idx < line.length && line[idx] === " ") {
@@ -6,15 +7,17 @@ function countLeadingSpaces(line) {
6
7
  return idx;
7
8
  }
8
9
  function parseFence(trimmedLine) {
9
- if (!trimmedLine.startsWith("```"))
10
+ const delimiter = FENCE_DELIMITERS.find((candidate) => trimmedLine.startsWith(candidate.repeat(3)));
11
+ if (!delimiter)
10
12
  return null;
11
13
  let idx = 0;
12
- while (idx < trimmedLine.length && trimmedLine[idx] === "`") {
14
+ while (idx < trimmedLine.length && trimmedLine[idx] === delimiter) {
13
15
  idx += 1;
14
16
  }
15
17
  if (idx < 3)
16
18
  return null;
17
19
  return {
20
+ delimiter,
18
21
  length: idx,
19
22
  info: trimmedLine.slice(idx).trim(),
20
23
  };
@@ -39,6 +42,15 @@ function parseHeader(trimmedLine) {
39
42
  return null;
40
43
  return { level, text };
41
44
  }
45
+ function parseSetextUnderline(trimmedLine) {
46
+ if (/^=+$/.test(trimmedLine)) {
47
+ return { level: 1 };
48
+ }
49
+ if (/^-+$/.test(trimmedLine)) {
50
+ return { level: 2 };
51
+ }
52
+ return null;
53
+ }
42
54
  function parseBlockquote(line) {
43
55
  let idx = 0;
44
56
  while (idx < line.length && line[idx] === " ") {
@@ -164,17 +176,19 @@ export function isSectionHeaderToken(token) {
164
176
  return token.type === "header" && (token.level === 1 || token.level === 2);
165
177
  }
166
178
  export function isFenceClosingToken(token, fence) {
167
- return token.type === "fence" && token.length >= fence.length;
179
+ return token.type === "fence" && token.delimiter === fence.delimiter && token.length >= fence.length;
168
180
  }
169
181
  export function lexMarkdown(content) {
170
182
  const lines = stripUtf8Bom(content).split(/\r?\n/);
171
183
  const tokens = [];
172
184
  const frontMatter = consumeLeadingYamlFrontMatter(lines);
173
185
  const contentLines = frontMatter.contentLines;
186
+ let activeFence = null;
174
187
  if (frontMatter.token) {
175
188
  tokens.push(frontMatter.token);
176
189
  }
177
- for (const line of contentLines) {
190
+ for (let index = 0; index < contentLines.length; index += 1) {
191
+ const line = contentLines[index] ?? "";
178
192
  const leadingSpaces = countLeadingSpaces(line);
179
193
  if (leadingSpaces <= 3) {
180
194
  const trimmedLine = line.trimStart();
@@ -184,21 +198,45 @@ export function lexMarkdown(content) {
184
198
  type: "fence",
185
199
  raw: line,
186
200
  leadingSpaces,
201
+ delimiter: fence.delimiter,
187
202
  length: fence.length,
188
203
  info: fence.info,
189
204
  });
205
+ if (!activeFence) {
206
+ activeFence = { delimiter: fence.delimiter, length: fence.length };
207
+ }
208
+ else if (activeFence.delimiter === fence.delimiter && fence.length >= activeFence.length) {
209
+ activeFence = null;
210
+ }
190
211
  continue;
191
212
  }
192
- const header = parseHeader(trimmedLine);
193
- if (header) {
194
- tokens.push({
195
- type: "header",
196
- raw: line,
197
- leadingSpaces,
198
- level: header.level,
199
- text: header.text,
200
- });
201
- continue;
213
+ if (!activeFence) {
214
+ const header = parseHeader(trimmedLine);
215
+ if (header) {
216
+ tokens.push({
217
+ type: "header",
218
+ raw: line,
219
+ leadingSpaces,
220
+ level: header.level,
221
+ text: header.text,
222
+ });
223
+ continue;
224
+ }
225
+ const nextLine = contentLines[index + 1];
226
+ if (trimmedLine.trim().length > 0 && nextLine !== undefined && countLeadingSpaces(nextLine) <= 3) {
227
+ const setext = parseSetextUnderline(nextLine.trim());
228
+ if (setext) {
229
+ tokens.push({
230
+ type: "header",
231
+ raw: line,
232
+ leadingSpaces,
233
+ level: setext.level,
234
+ text: trimmedLine.trim(),
235
+ });
236
+ index += 1;
237
+ continue;
238
+ }
239
+ }
202
240
  }
203
241
  }
204
242
  const marker = parseListMarker(line);
@@ -1,4 +1,5 @@
1
- import { parseToAST, renderNodeContent, } from "./index.js";
1
+ import { parseToAST } from "./parser.js";
2
+ import { renderNodeContent } from "./render.js";
2
3
  export function computeHeadingDelta(shallowest, minLevel) {
3
4
  if (shallowest === null || shallowest >= minLevel) {
4
5
  return 0;
@@ -61,7 +62,7 @@ export function renderMarkdownSections(items) {
61
62
  // Keiyaku uses H2 as contract section delimiters. Keep payload headings at H3+.
62
63
  const normalized = demoteMarkdownHeadings(item, 3);
63
64
  const trimmed = normalized.trimStart();
64
- if (/^[-*+]\s+/.test(trimmed) || /^\d+\.\s+/.test(trimmed)) {
65
+ if (/^[-*+]\s+/.test(trimmed) || /^\d+[.)]\s+/.test(trimmed)) {
65
66
  return normalized;
66
67
  }
67
68
  return `- ${normalized}`;
@@ -84,7 +84,7 @@ class MarkdownParser {
84
84
  throw new KeiyakuParseError("invalid markdown parse state: expected code fence");
85
85
  }
86
86
  const lines = [opener.raw];
87
- const fence = { length: opener.length };
87
+ const fence = { delimiter: opener.delimiter, length: opener.length };
88
88
  while (!this.isEof()) {
89
89
  const token = this.consume();
90
90
  if (!token)
@@ -192,7 +192,7 @@ class MarkdownParser {
192
192
  break;
193
193
  lines.push(consumed.raw);
194
194
  if (consumed.type === "fence") {
195
- itemFence = { length: consumed.length };
195
+ itemFence = { delimiter: consumed.delimiter, length: consumed.length };
196
196
  }
197
197
  }
198
198
  const normalizedLines = stripTrailingBlankLines(lines);
@@ -1,10 +1,17 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { RESPONSE_ARTIFACT_NOTICE, RESPONSE_HISTORY_DIR } from "../keiyaku.js";
4
+ import { isErrnoException } from "../errno.js";
4
5
  import { FlowError, requireText } from "../flow-error.js";
5
6
  import { parseToAST } from "./markdown/index.js";
6
7
  const RESPONSE_HISTORY_SLUG_MAX_LENGTH = 24;
7
- const RESUMABLE_RESPONSE_HEADINGS = ["# Ask", "# Summon", "# Drive", "# Review"];
8
+ export const RESPONSE_ARTIFACT_HEADINGS = {
9
+ ask: "# Ask",
10
+ summon: "# Summon",
11
+ drive: "# Drive",
12
+ review: "# Review",
13
+ };
14
+ const RESUMABLE_RESPONSE_HEADINGS = Object.values(RESPONSE_ARTIFACT_HEADINGS);
8
15
  function toTimestampParts(now) {
9
16
  const year = String(now.getFullYear());
10
17
  const month = String(now.getMonth() + 1).padStart(2, "0");
@@ -12,8 +19,9 @@ function toTimestampParts(now) {
12
19
  const hour = String(now.getHours()).padStart(2, "0");
13
20
  const minute = String(now.getMinutes()).padStart(2, "0");
14
21
  const second = String(now.getSeconds()).padStart(2, "0");
22
+ const millisecond = String(now.getMilliseconds()).padStart(3, "0");
15
23
  return {
16
- fileStamp: `${year}${month}${day}-${hour}${minute}${second}`,
24
+ fileStamp: `${year}${month}${day}-${hour}${minute}${second}${millisecond}`,
17
25
  iso: now.toISOString(),
18
26
  };
19
27
  }
@@ -133,6 +141,28 @@ export async function persistResponseHistory(input) {
133
141
  await fs.writeFile(responsePath, markdown, "utf-8");
134
142
  return path.relative(input.cwd, responsePath);
135
143
  }
144
+ function detectArtifactHeading(document) {
145
+ const lines = document.split("\n");
146
+ let index = 0;
147
+ if (lines[0] === "---") {
148
+ index = 1;
149
+ while (index < lines.length && lines[index] !== "---") {
150
+ index += 1;
151
+ }
152
+ if (index === lines.length) {
153
+ return null;
154
+ }
155
+ index += 1;
156
+ }
157
+ for (; index < lines.length; index += 1) {
158
+ const line = lines[index].trim();
159
+ if (line.length === 0) {
160
+ continue;
161
+ }
162
+ return RESUMABLE_RESPONSE_HEADINGS.find((heading) => heading === line) ?? null;
163
+ }
164
+ return null;
165
+ }
136
166
  export async function resolveAskResponsePath(input) {
137
167
  const responsePath = requireText("responsePath", input.responsePath);
138
168
  const resolvedPath = path.isAbsolute(responsePath) ? responsePath : path.resolve(input.cwd, responsePath);
@@ -141,8 +171,7 @@ export async function resolveAskResponsePath(input) {
141
171
  document = await fs.readFile(resolvedPath, "utf-8");
142
172
  }
143
173
  catch (error) {
144
- const code = error?.code;
145
- if (code === "ENOENT" || code === "EISDIR") {
174
+ if (isErrnoException(error) && (error.code === "ENOENT" || error.code === "EISDIR")) {
146
175
  throw new FlowError("INVALID_RESPONSE_PATH", `Response path file not found: '${responsePath}'.`);
147
176
  }
148
177
  throw error;
@@ -155,9 +184,19 @@ export async function resolveAskResponsePath(input) {
155
184
  if (frontmatter.notice !== RESPONSE_ARTIFACT_NOTICE) {
156
185
  throw new FlowError("INVALID_RESPONSE_PATH", `Response path file '${responsePath}' is not a Keiyaku response artifact.`);
157
186
  }
158
- if (!RESUMABLE_RESPONSE_HEADINGS.some((heading) => new RegExp(`^${heading}\\s*$`, "m").test(document))) {
187
+ const artifactHeading = detectArtifactHeading(document);
188
+ if (!artifactHeading) {
159
189
  throw new FlowError("INVALID_RESPONSE_PATH", `Response path file '${responsePath}' is not a supported Keiyaku response artifact.`);
160
190
  }
191
+ if (input.allowedTools?.length) {
192
+ const allowedHeadings = input.allowedTools.map((tool) => RESPONSE_ARTIFACT_HEADINGS[tool]);
193
+ if (!allowedHeadings.includes(artifactHeading)) {
194
+ if (input.onArtifactTypeMismatch === "return-null") {
195
+ return undefined;
196
+ }
197
+ throw new FlowError("INVALID_RESPONSE_PATH", `Response path file '${responsePath}' is not a supported ${input.allowedTools.join("/")} response artifact.`);
198
+ }
199
+ }
161
200
  const sessionId = frontmatter.session_id?.trim();
162
201
  if (!sessionId) {
163
202
  throw new FlowError("INVALID_RESPONSE_PATH", `Response path file '${responsePath}' does not include 'session_id'; this conversation cannot be resumed.`);
@@ -1,5 +1,4 @@
1
- import { parseToAST, renderNodeContent } from "./markdown/index.js";
2
- import { parseRoundSummary } from "../tools/round/report.js";
1
+ import { parseMarkdownSections, parseToAST, renderNodeContent } from "./markdown/index.js";
3
2
  const PREVIEW_SECTION_TITLES = ["goal", "context"];
4
3
  const PREVIEW_SECTION_TITLE_SET = new Set(PREVIEW_SECTION_TITLES);
5
4
  const STATUS_PREVIEW_MAX_LINES = 2;
@@ -49,6 +48,22 @@ function extractRoundRawSummary(roundSection) {
49
48
  const summary = renderNodesForPreview(extractHeadingBody(roundSection, "summary"));
50
49
  return summary.length > 0 ? summary : undefined;
51
50
  }
51
+ function extractOutcomeFromStructuredSummary(rawSummary) {
52
+ for (const sectionMarkdown of parseMarkdownSections(rawSummary)) {
53
+ const lines = sectionMarkdown.split(/\r?\n/);
54
+ const headerLine = lines[0]?.trim();
55
+ if (!headerLine)
56
+ continue;
57
+ const headerMatch = /^#{2,3}\s+(.+?)\s*$/.exec(headerLine);
58
+ if (!headerMatch)
59
+ continue;
60
+ if (normalizeHeading(headerMatch[1]) !== "outcome")
61
+ continue;
62
+ const body = lines.slice(1).join("\n").trim();
63
+ return body.length > 0 ? body : undefined;
64
+ }
65
+ return undefined;
66
+ }
52
67
  export function extractLastRoundSummary(traceContent) {
53
68
  const ast = parseToAST(traceContent, { allowSections: true });
54
69
  let lastRoundSection;
@@ -69,12 +84,9 @@ export function extractLastRoundSummary(traceContent) {
69
84
  const rawSummary = extractRoundRawSummary(lastRoundSection)?.trim();
70
85
  if (!rawSummary)
71
86
  return undefined;
72
- const parsedSummary = parseRoundSummary(rawSummary);
73
- if (parsedSummary.kind === "structured") {
74
- const outcomeBody = parsedSummary.sections.outcome?.body.trim();
75
- if (outcomeBody) {
76
- return outcomeBody;
77
- }
87
+ const outcomeBody = extractOutcomeFromStructuredSummary(rawSummary);
88
+ if (outcomeBody) {
89
+ return outcomeBody;
78
90
  }
79
91
  return rawSummary;
80
92
  }
@@ -1,6 +1,7 @@
1
1
  import * as fs from "fs/promises";
2
2
  import * as path from "path";
3
3
  import { KEIYAKU_DRAFT_DIR } from "../keiyaku.js";
4
+ import { isErrnoException } from "../errno.js";
4
5
  import { FlowError, isFlowError, requireText, wrapFlowError } from "../flow-error.js";
5
6
  import { resolveTermPreset } from "../config/term-presets/resolver.js";
6
7
  import { writeDraftArtifact, listDraftArtifacts } from "./draft-artifacts.js";
@@ -74,7 +75,7 @@ export async function resolveSummonInput(cwd, input) {
74
75
  content = await fs.readFile(draftPath, "utf-8");
75
76
  }
76
77
  catch (error) {
77
- if (error?.code === "ENOENT") {
78
+ if (isErrnoException(error) && error.code === "ENOENT") {
78
79
  throw new FlowError("FROM_DRAFT_NOT_FOUND", `from_draft path does not exist: ${fromDraft}`);
79
80
  }
80
81
  throw error;
@@ -115,7 +116,7 @@ export async function readRollbackDraftSnapshot(cwd, candidatePath) {
115
116
  return { relativePath, content };
116
117
  }
117
118
  catch (error) {
118
- if (error?.code === "ENOENT") {
119
+ if (isErrnoException(error) && error.code === "ENOENT") {
119
120
  return null;
120
121
  }
121
122
  throw error;
@@ -0,0 +1 @@
1
+ export {};
@@ -187,7 +187,7 @@ export function renderReviewSection(review) {
187
187
  `${REVIEW_RESULT_PREFIX} ${review.result}`,
188
188
  `${REVIEW_HISTORY_POINTER_PREFIX} ${historyPath}`,
189
189
  ];
190
- if (findings && review.result === "rejected") {
190
+ if (findings) {
191
191
  lines.push("", findings);
192
192
  }
193
193
  lines.push("");
@@ -1,28 +1,18 @@
1
- import { appendDebugLog } from "../../telemetry/debug-log.js";
2
1
  import { buildAmendResponse } from "../../mcp/responses.js";
3
- import { handleToolError } from "../../mcp/tool-errors.js";
4
2
  import { amendKeiyaku } from "./run.js";
3
+ import { createToolHandler } from "../create-handler.js";
5
4
  export function createAmendHandler() {
6
- return async ({ amendment, cwd }, _extra) => {
7
- const workingDir = cwd || process.cwd();
8
- try {
9
- appendDebugLog(`tool amend start cwd=${workingDir}`, { cwd: workingDir, section: "script" });
10
- const outcome = await amendKeiyaku({
11
- cwd: workingDir,
5
+ return createToolHandler({
6
+ logLabel: "tool amend",
7
+ doneLog: (outcome) => `tool amend success branch=${outcome.currentBranch} amendment=${outcome.amendmentNumber}`,
8
+ async run({ amendment, cwd }) {
9
+ return amendKeiyaku({
10
+ cwd,
12
11
  amendment,
13
12
  });
14
- appendDebugLog(`tool amend success branch=${outcome.currentBranch} amendment=${outcome.amendmentNumber}`, {
15
- cwd: workingDir,
16
- section: "script",
17
- });
13
+ },
14
+ buildResponse(outcome) {
18
15
  return buildAmendResponse(outcome);
19
- }
20
- catch (err) {
21
- return handleToolError({
22
- error: err,
23
- cwd: workingDir,
24
- logLabel: "tool amend",
25
- });
26
- }
27
- };
16
+ },
17
+ });
28
18
  }
@@ -1,14 +1,13 @@
1
- import { appendDebugLog } from "../../telemetry/debug-log.js";
2
1
  import { runAskAndPersist } from "./persist.js";
3
2
  import { buildAskResponse } from "../../mcp/responses.js";
4
- import { handleToolError } from "../../mcp/tool-errors.js";
3
+ import { createToolHandler } from "../create-handler.js";
5
4
  export function createAskHandler() {
6
- return async ({ title, request, context, tier, responsePath, cwd }, extra) => {
7
- const workingDir = cwd || process.cwd();
8
- try {
9
- appendDebugLog(`tool ask start cwd=${workingDir}`, { cwd: workingDir, section: "script" });
10
- const outcome = await runAskAndPersist({
11
- cwd: workingDir,
5
+ return createToolHandler({
6
+ logLabel: "tool ask",
7
+ doneLog: "tool ask success",
8
+ async run({ title, request, context, tier, responsePath, cwd }, extra) {
9
+ return runAskAndPersist({
10
+ cwd,
12
11
  title,
13
12
  request,
14
13
  context,
@@ -16,18 +15,12 @@ export function createAskHandler() {
16
15
  tier,
17
16
  signal: extra.signal,
18
17
  });
19
- appendDebugLog("tool ask success", { cwd: workingDir, section: "script" });
18
+ },
19
+ buildResponse(outcome, { tier }) {
20
20
  return buildAskResponse(outcome.result, {
21
21
  tier,
22
22
  responsePath: outcome.responsePath,
23
23
  });
24
- }
25
- catch (err) {
26
- return handleToolError({
27
- error: err,
28
- cwd: workingDir,
29
- logLabel: "tool ask",
30
- });
31
- }
32
- };
24
+ },
25
+ });
33
26
  }