@astrosheep/keiyaku 0.1.48 → 0.1.49

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.
@@ -38,6 +38,10 @@ class MarkdownParser {
38
38
  blocks.push(this.parseList(token.indent, token.ordered));
39
39
  continue;
40
40
  }
41
+ if (token.type === "blockquote") {
42
+ blocks.push(this.parseBlockquote());
43
+ continue;
44
+ }
41
45
  blocks.push(this.parseText(shouldStop));
42
46
  }
43
47
  return blocks;
@@ -92,6 +96,24 @@ class MarkdownParser {
92
96
  text: header.text,
93
97
  };
94
98
  }
99
+ parseBlockquote() {
100
+ const lines = [];
101
+ let marker = "> ";
102
+ while (!this.isEof()) {
103
+ const token = this.peek();
104
+ if (!token || token.type !== "blockquote")
105
+ break;
106
+ marker = token.marker;
107
+ lines.push(token.body);
108
+ this.consume();
109
+ }
110
+ return {
111
+ type: "blockquote",
112
+ marker,
113
+ lines,
114
+ value: lines.join("\n"),
115
+ };
116
+ }
95
117
  parseList(indent, ordered) {
96
118
  const items = [];
97
119
  while (!this.isEof()) {
@@ -181,6 +203,8 @@ class MarkdownParser {
181
203
  break;
182
204
  if (token.type === "list_marker" && token.indent <= 3)
183
205
  break;
206
+ if (token.type === "blockquote")
207
+ break;
184
208
  if (this.options.allowSections && isSectionHeaderToken(token))
185
209
  break;
186
210
  lines.push(token.raw);
@@ -21,6 +21,8 @@ export function renderBlock(node) {
21
21
  return node.items.map((item) => renderListItemFirstLine(item)).join("\n");
22
22
  case "code_block":
23
23
  return node.lines.join("\n");
24
+ case "blockquote":
25
+ return node.lines.map((line) => `${node.marker}${line}`).join("\n");
24
26
  case "text":
25
27
  return node.value;
26
28
  case "heading":
@@ -39,6 +41,8 @@ export function renderNodeContent(node) {
39
41
  return node.children.map((child) => renderBlock(child)).join("\n");
40
42
  case "code_block":
41
43
  return node.lines.join("\n");
44
+ case "blockquote":
45
+ return node.lines.map((line) => `${node.marker}${line}`).join("\n");
42
46
  case "text":
43
47
  return node.value;
44
48
  case "heading":
@@ -55,6 +55,17 @@ test("parseToAST keeps fenced code blocks as structured nodes", () => {
55
55
  assert.equal(section.children[0]?.type === "code_block" ? section.children[0].lines.join("\n") : "", "```md\n## Not A Section\n```");
56
56
  assert.equal(section.children[1]?.type === "text" ? section.children[1].value : "", "After fence.");
57
57
  });
58
+ test("parseToAST keeps blockquote blocks as structured nodes", () => {
59
+ const markdown = ["## Summary", "> ## Outcome", "> Implemented parser.", ">", "> ## Changes", "> - Added tests."].join("\n");
60
+ const ast = parseToAST(markdown);
61
+ const section = ast.children[0];
62
+ assert.equal(section?.type, "section");
63
+ if (section?.type !== "section")
64
+ return;
65
+ assert.equal(section.children[0]?.type, "blockquote");
66
+ assert.equal(section.children[0]?.type === "blockquote" ? section.children[0].value : "", "## Outcome\nImplemented parser.\n\n## Changes\n- Added tests.");
67
+ assert.equal(section.children[0]?.type === "blockquote" ? renderSectionContent(section) : "", "> ## Outcome\n> Implemented parser.\n> \n> ## Changes\n> - Added tests.");
68
+ });
58
69
  test("parseMarkdownListSection flattens list items and ignores source headers", () => {
59
70
  const markdown = [
60
71
  "## Group A",
@@ -6,6 +6,8 @@ import { parseToAST } from "./keiyaku-document/index.js";
6
6
  const DIFF_COORDINATES_SECTION_PREFIX = "Diff Coordinates";
7
7
  const DIFF_COORDINATES_FENCE = "```keiyaku-coords";
8
8
  const DIFF_COORDINATE_LINE_RE = /^(.*):(\d+)(?:-(\d+))?$/;
9
+ const BLOCKQUOTE_PREFIX = "> ";
10
+ const EMPTY_BLOCKQUOTE_LINE = ">";
9
11
  async function fileExists(cwd, filepath) {
10
12
  try {
11
13
  await fs.access(path.join(cwd, filepath));
@@ -33,6 +35,7 @@ export async function appendReview(cwd, round, reason) {
33
35
  export async function appendRoundReport(cwd, report) {
34
36
  const summaryText = report.summary.trim().length > 0 ? report.summary : "Unknown error.";
35
37
  const tracePath = path.join(cwd, TRACE_FILE);
38
+ const quotedSummary = summaryText.split(/\r?\n/).map((line) => (line.length > 0 ? `${BLOCKQUOTE_PREFIX}${line}` : EMPTY_BLOCKQUOTE_LINE));
36
39
  const lines = [
37
40
  "",
38
41
  "---",
@@ -41,9 +44,7 @@ export async function appendRoundReport(cwd, report) {
41
44
  "### Status",
42
45
  report.status,
43
46
  "### Summary",
44
- "```markdown",
45
- summaryText,
46
- "```",
47
+ ...quotedSummary,
47
48
  ];
48
49
  if (report.errorMessage) {
49
50
  lines.push("### Error", report.errorMessage);
@@ -62,9 +63,7 @@ export async function appendRoundSystemNote(cwd, round, note) {
62
63
  "### Status",
63
64
  "FAILED",
64
65
  "### Summary",
65
- "```markdown",
66
- "Subagent execution cancelled by user/client.",
67
- "```",
66
+ `${BLOCKQUOTE_PREFIX}Subagent execution cancelled by user/client.`,
68
67
  "### Error",
69
68
  note,
70
69
  "",
@@ -1,8 +1,17 @@
1
- import { getLatestCommitHash } from "../utils/git-ops.js";
1
+ import { TRACE_FILE } from "../common/constants.js";
2
2
  import { appendAsk } from "../utils/trace.js";
3
3
  import { appendDebugLog } from "../utils/debug-log.js";
4
+ import { logWarn } from "../utils/logger.js";
4
5
  import { runAsk } from "./ask.js";
6
+ import { assertKeiyakuProtocolFiles } from "./keiyaku.js";
5
7
  import { persistAskHistory } from "../utils/ask-history.js";
8
+ import * as git from "../utils/git.js";
9
+ const ASK_TRACE_COMMIT_PREFIX = "ask: ";
10
+ const ASK_TRACE_COMMIT_SUMMARY_MAX_CHARS = 50;
11
+ function buildAskTraceCommitMessage(request) {
12
+ const summary = request.replaceAll(/\s+/g, " ").trim().slice(0, ASK_TRACE_COMMIT_SUMMARY_MAX_CHARS);
13
+ return `${ASK_TRACE_COMMIT_PREFIX}${summary}`;
14
+ }
6
15
  export async function runAskAndPersist(input) {
7
16
  const result = await runAsk(input);
8
17
  let warning;
@@ -10,7 +19,7 @@ export async function runAskAndPersist(input) {
10
19
  try {
11
20
  let commit;
12
21
  try {
13
- commit = await getLatestCommitHash(input.cwd);
22
+ commit = await git.getLatestCommitHash(input.cwd);
14
23
  }
15
24
  catch {
16
25
  // Ask can run outside git repos; history should still be persisted.
@@ -41,6 +50,25 @@ export async function runAskAndPersist(input) {
41
50
  summary: result.summary,
42
51
  diffStats: result.diffStats,
43
52
  });
53
+ try {
54
+ const activeState = await git.getActiveKeiyakuGitState(input.cwd);
55
+ if (activeState) {
56
+ await assertKeiyakuProtocolFiles(input.cwd);
57
+ try {
58
+ await git.addFiles(input.cwd, TRACE_FILE);
59
+ await git.commit(input.cwd, buildAskTraceCommitMessage(input.request));
60
+ }
61
+ catch (error) {
62
+ const message = error instanceof Error ? error.message : String(error);
63
+ const commitWarning = `Failed to commit ask trace update: ${message}`;
64
+ logWarn(commitWarning, { cwd: input.cwd, section: "script" });
65
+ warning = warning ? `${warning}\n${commitWarning}` : commitWarning;
66
+ }
67
+ }
68
+ }
69
+ catch {
70
+ // Non-active keiyaku or missing protocol files should not block ask.
71
+ }
44
72
  }
45
73
  catch (askTraceError) {
46
74
  const askTraceErrorMessage = askTraceError instanceof Error ? askTraceError.message : String(askTraceError);
@@ -7,7 +7,7 @@ import * as git from "../utils/git.js";
7
7
  import { KeiyakuParseError, parseMarkdownStructure } from "../utils/keiyaku-document/index.js";
8
8
  import { logWarn } from "../utils/logger.js";
9
9
  import { renderToolRoundSummary } from "./round-summary.js";
10
- import { assertCleanWorkingTree, buildNoActiveKeiyakuGuidance, ensureKeiyakuFiles } from "./contract.js";
10
+ import { assertCleanWorkingTree, assertKeiyakuProtocolFiles, buildNoActiveKeiyakuGuidance } from "./keiyaku.js";
11
11
  import { buildRoundCommitMessage, runAndRecordRound } from "./round.js";
12
12
  import { resolveIncrementalDiffMode } from "../config/incremental-diff-mode.js";
13
13
  import { buildIteratePlan } from "./iterate-plan.js";
@@ -58,7 +58,7 @@ export async function driveServant(input) {
58
58
  throw new FlowError("MISSING_KEIYAKU_BASE", `branch ${keiyakuBranch} is missing base metadata; cannot continue drive`);
59
59
  }
60
60
  await assertCleanWorkingTree(cwd, [KEIYAKU_DRAFT_FILE]);
61
- await ensureKeiyakuFiles(cwd);
61
+ await assertKeiyakuProtocolFiles(cwd);
62
62
  const keiyakuContent = await fs.readFile(path.join(cwd, KEIYAKU_FILE), "utf-8");
63
63
  const parsedKeiyaku = parseKeiyakuStructure(keiyakuContent);
64
64
  const traceContent = await readTraceContent(cwd);
@@ -7,7 +7,7 @@ import * as git from "../utils/git.js";
7
7
  import { resolveOath } from "./oath.js";
8
8
  export { resolveOath };
9
9
  const DIRTY_WORKTREE_LIST_LIMIT = 10;
10
- export async function ensureKeiyakuFiles(cwd) {
10
+ export async function assertKeiyakuProtocolFiles(cwd) {
11
11
  const keiyakuPath = path.join(cwd, KEIYAKU_FILE);
12
12
  const tracePath = path.join(cwd, TRACE_FILE);
13
13
  try {
@@ -19,7 +19,7 @@ export function demoteMarkdownHeadings(text, minLevel = 3) {
19
19
  if (node.type === "heading") {
20
20
  shallowest = shallowest === null ? node.level : Math.min(shallowest, node.level);
21
21
  }
22
- if (node.type === "code_block" || node.type === "text")
22
+ if (node.type === "code_block" || node.type === "blockquote" || node.type === "text")
23
23
  continue;
24
24
  if (node.type === "document" || node.type === "section" || node.type === "list_item") {
25
25
  stack.push(...node.children);
@@ -41,6 +41,7 @@ export function demoteMarkdownHeadings(text, minLevel = 3) {
41
41
  return nextLevel === node.level ? node : { ...node, level: nextLevel };
42
42
  }
43
43
  case "code_block":
44
+ case "blockquote":
44
45
  case "text":
45
46
  return node;
46
47
  case "document":
@@ -5,10 +5,11 @@ import { KEIYAKU_ARCHIVE_TAG_PREFIX, KEIYAKU_BRANCH_PREFIX, KEIYAKU_DRAFT_FILE,
5
5
  import { resolveTermPreset } from "../config/term-presets/index.js";
6
6
  import { logInfo, logWarn } from "../utils/logger.js";
7
7
  import { FlowError, wrapFlowError } from "../common/errors.js";
8
+ import { readDraftSnapshot, restoreDraftSnapshot } from "../utils/draft.js";
8
9
  import * as git from "../utils/git.js";
9
10
  import { appendVerdict, computeTraceState, readTraceContent } from "../utils/trace.js";
10
11
  import { oathMatches } from "./oath.js";
11
- import { assertCleanWorkingTree, buildNoActiveKeiyakuGuidance, ensureKeiyakuFiles, resolveOath } from "./contract.js";
12
+ import { assertCleanWorkingTree, assertKeiyakuProtocolFiles, buildNoActiveKeiyakuGuidance, resolveOath } from "./keiyaku.js";
12
13
  const DIMENSIONS = [
13
14
  "placement",
14
15
  "exactness",
@@ -212,14 +213,7 @@ export async function presentWork(input) {
212
213
  }
213
214
  const title = keiyakuBranch.slice(KEIYAKU_BRANCH_PREFIX.length);
214
215
  const petition = input.petition;
215
- let draftKept = false;
216
- try {
217
- const stat = await fs.stat(path.join(cwd, KEIYAKU_DRAFT_FILE));
218
- draftKept = stat.isFile();
219
- }
220
- catch {
221
- // ignored
222
- }
216
+ const draftSnapshot = await readDraftSnapshot(cwd);
223
217
  if (petition === "FORFEIT") {
224
218
  const archiveTag = `${KEIYAKU_ARCHIVE_TAG_PREFIX}${title}`;
225
219
  let round = 0;
@@ -238,6 +232,9 @@ export async function presentWork(input) {
238
232
  await git.discardAllWorkingTreeChanges(cwd);
239
233
  }
240
234
  await git.checkoutBranch(cwd, baseBranch);
235
+ if (draftSnapshot) {
236
+ await restoreDraftSnapshot(cwd, draftSnapshot);
237
+ }
241
238
  await git.deleteBranch(cwd, keiyakuBranch, true);
242
239
  await git.clearKeiyakuBase(cwd, keiyakuBranch);
243
240
  }
@@ -256,10 +253,10 @@ export async function presentWork(input) {
256
253
  ? `Forfeited without merge. Dropped ${droppedChanges.length} local change(s).`
257
254
  : "Forfeited without merge.",
258
255
  droppedChanges,
259
- draftKept,
256
+ draftPath: draftSnapshot?.path ?? null,
260
257
  };
261
258
  }
262
- await ensureKeiyakuFiles(cwd);
259
+ await assertKeiyakuProtocolFiles(cwd);
263
260
  let traceContent = await readTraceContent(cwd);
264
261
  if (petition === "CLAIM") {
265
262
  const driveCommandName = resolveTermPreset().tools.drive.name;
@@ -340,7 +337,7 @@ export async function presentWork(input) {
340
337
  mergedInto: baseBranch,
341
338
  deletedBranch: keiyakuBranch,
342
339
  diff,
343
- draftKept,
340
+ draftPath: draftSnapshot?.path ?? null,
344
341
  };
345
342
  }
346
343
  catch (err) {
@@ -6,6 +6,12 @@ const FORMAT_LIST_MAX_ITEMS = 16;
6
6
  const FORMAT_LIST_MAX_ITEM_CHARS = 400;
7
7
  const RULES_SECTION_MAX_CHARS = 5000;
8
8
  const BRANCH_LABEL = "Branch";
9
+ const INFO_SECTION_TITLE = "Info";
10
+ const HINTS_SECTION_TITLE = "Hints";
11
+ const STATUS_KEIYAKU_SECTION_TITLE = "Keiyaku";
12
+ const STATUS_BLOCKED_SECTION_TITLE = "Blocked";
13
+ const STATUS_ACTIVE_SUMMARY_PREFIX = "● Active";
14
+ const STATUS_INACTIVE_SUMMARY = "○ Inactive";
9
15
  // --- Helpers ---
10
16
  function truncateForDisplay(raw, maxChars = DISPLAY_TEXT_MAX_CHARS) {
11
17
  const text = raw.trim();
@@ -47,17 +53,14 @@ function buildSection(title, content) {
47
53
  return null;
48
54
  return { title, body };
49
55
  }
50
- function formatBlockedOperation(label, readiness) {
51
- if (readiness.available)
56
+ function formatBlockedOperation(label, blockers) {
57
+ if (blockers.length === 0)
52
58
  return [];
53
59
  return [
54
- `${label}: blocked ✕`,
55
- ...formatList(`${label} Blockers`, readiness.blockers, { maxItems: 10, maxItemChars: 300 }),
60
+ `${label}:`,
61
+ ...formatList(`${label} Blockers`, blockers, { maxItems: 10, maxItemChars: 300 }),
56
62
  ];
57
63
  }
58
- function formatStatusFileSignal(signal) {
59
- return `${signal.path}: ${signal.present ? "present ✓" : "missing ✕"}`;
60
- }
61
64
  function buildSuccessStructuredContent(data) {
62
65
  return {
63
66
  status: "success",
@@ -71,9 +74,9 @@ export function buildKeiyakuSuccessResponse(result, input) {
71
74
  const responseData = normalizedRules ? { ...resultData, rules: normalizedRules } : { ...resultData };
72
75
  const summarySection = buildSection("Summary", result.summary);
73
76
  const rulesSection = buildSection("Rules", truncateForDisplay(normalizedRules, RULES_SECTION_MAX_CHARS));
74
- const hintsSection = buildSection("Hints", formatHints(result.meta?.hints ?? [], { maxItems: 12, maxItemChars: 400 }));
77
+ const hintsSection = buildSection(HINTS_SECTION_TITLE, formatHints(result.meta?.hints ?? [], { maxItems: 12, maxItemChars: 400 }));
75
78
  const diffSection = buildSection(DIFF_OVERVIEW_SECTION_TITLE, result.diff);
76
- const infoLines = [
79
+ const infoSection = buildSection(INFO_SECTION_TITLE, [
77
80
  ...formatMaybe("Identity", input.name, 120),
78
81
  ...formatMaybe("Consumed from_file and deleted", result.consumedFromFile, 300),
79
82
  ...formatList("Existing local keiyaku branches (non-blocking)", result.existingBranches ?? [], {
@@ -83,11 +86,11 @@ export function buildKeiyakuSuccessResponse(result, input) {
83
86
  ...formatMaybe(BRANCH_LABEL, result.currentBranch, 200),
84
87
  ...formatMaybe("Base Branch", result.baseBranch, 200),
85
88
  ...formatMaybe("Commit", result.commit, 100),
86
- ];
89
+ ]);
87
90
  const summaryLine = result.commit
88
91
  ? `Created branch '${result.currentBranch}' (base: '${result.baseBranch}') [${result.commit}].`
89
92
  : `Created branch '${result.currentBranch}' (base: '${result.baseBranch}').`;
90
- const text = assembleResponse(`${RESPONSE_MARKERS.round} Started (Round ${result.round})`, summaryLine, [summarySection, rulesSection, hintsSection, diffSection].filter((section) => section !== null), infoLines);
93
+ const text = assembleResponse(`${RESPONSE_MARKERS.round} Started (Round ${result.round})`, summaryLine, [summarySection, rulesSection, infoSection, diffSection, hintsSection].filter((section) => section !== null));
91
94
  return {
92
95
  content: [{ type: "text", text }],
93
96
  structuredContent: buildSuccessStructuredContent(responseData),
@@ -96,18 +99,18 @@ export function buildKeiyakuSuccessResponse(result, input) {
96
99
  export function buildDriveResponse(result, input) {
97
100
  const { status: _status, ...resultData } = result;
98
101
  const summarySection = buildSection("Summary", result.summary);
99
- const hintsSection = buildSection("Hints", formatHints(result.meta?.hints ?? [], { maxItems: 12, maxItemChars: 400 }));
102
+ const hintsSection = buildSection(HINTS_SECTION_TITLE, formatHints(result.meta?.hints ?? [], { maxItems: 12, maxItemChars: 400 }));
100
103
  const diffSection = buildSection(DIFF_OVERVIEW_SECTION_TITLE, result.diff);
101
- const infoLines = [
104
+ const infoSection = buildSection(INFO_SECTION_TITLE, [
102
105
  ...formatMaybe("Identity", input.name, 120),
103
106
  ...formatMaybe(BRANCH_LABEL, result.currentBranch, 200),
104
107
  ...formatMaybe("Base Branch", result.baseBranch, 200),
105
108
  ...formatMaybe("Commit", result.commit, 100),
106
- ];
109
+ ]);
107
110
  const summaryLine = result.commit
108
111
  ? `Updated branch '${result.currentBranch}' [${result.commit}].`
109
112
  : `Updated branch '${result.currentBranch}'.`;
110
- const text = assembleResponse(`${RESPONSE_MARKERS.round} Driven (Round ${result.round})`, summaryLine, [summarySection, hintsSection, diffSection].filter((section) => section !== null), infoLines);
113
+ const text = assembleResponse(`${RESPONSE_MARKERS.round} Driven (Round ${result.round})`, summaryLine, [summarySection, infoSection, diffSection, hintsSection].filter((section) => section !== null));
111
114
  return {
112
115
  content: [{ type: "text", text }],
113
116
  structuredContent: buildSuccessStructuredContent(resultData),
@@ -149,7 +152,8 @@ export function buildCloseDoneResponse(result, input) {
149
152
  `Scores: placement=${input.placement} exactness=${input.exactness} containment=${input.containment} idiomatic=${input.idiomatic} cohesive=${input.cohesive} [total: ${input.placement + input.exactness + input.containment + input.idiomatic + input.cohesive}/50]`,
150
153
  ...formatMaybe("Oath", input.oath, 220),
151
154
  ];
152
- const text = assembleResponse(`${RESPONSE_MARKERS.claim} Keiyaku Fulfilled (Claim)`, `Merged '${result.deletedBranch}' into '${result.mergedInto}'${result.commit ? ` [${result.commit}]` : ''}. Deleted feature branch.${result.draftKept ? " Draft 'KEIYAKU.draft.md' kept." : ''}`, [diffSection].filter((section) => section !== null), infoLines);
155
+ const draftPathMessage = result.draftPath ? ` Draft kept at '${result.draftPath}'.` : "";
156
+ const text = assembleResponse(`${RESPONSE_MARKERS.claim} Keiyaku Fulfilled (Claim)`, `Merged '${result.deletedBranch}' into '${result.mergedInto}'${result.commit ? ` [${result.commit}]` : ''}. Deleted feature branch.${draftPathMessage}`, [diffSection].filter((section) => section !== null), infoLines);
153
157
  return {
154
158
  content: [{ type: "text", text }],
155
159
  structuredContent: buildSuccessStructuredContent(resultData),
@@ -165,7 +169,8 @@ export function buildCloseDropResponse(result, _input) {
165
169
  ...formatMaybe(BRANCH_LABEL, result.currentBranch, 200),
166
170
  ...formatMaybe("Base Branch", result.baseBranch, 200),
167
171
  ];
168
- const text = assembleResponse(`${RESPONSE_MARKERS.forfeit} Keiyaku Forfeited (Forfeit)`, `Deleted '${result.deletedBranch}'. Switched back to '${result.baseBranch}'.${result.archiveTag ? ` Archived at '${result.archiveTag}'.` : ""}${droppedChanges.length > 0 ? ` Dropped ${droppedChanges.length} local change(s).` : ''}${result.draftKept ? " Draft 'KEIYAKU.draft.md' kept." : ''}`, [warningSection].filter((section) => section !== null), infoLines);
172
+ const draftPathMessage = result.draftPath ? ` Draft kept at '${result.draftPath}'.` : "";
173
+ const text = assembleResponse(`${RESPONSE_MARKERS.forfeit} Keiyaku Forfeited (Forfeit)`, `Deleted '${result.deletedBranch}'. Switched back to '${result.baseBranch}'.${result.archiveTag ? ` Archived at '${result.archiveTag}'.` : ""}${droppedChanges.length > 0 ? ` Dropped ${droppedChanges.length} local change(s).` : ''}${draftPathMessage}`, [warningSection].filter((section) => section !== null), infoLines);
169
174
  return {
170
175
  content: [{ type: "text", text }],
171
176
  structuredContent: buildSuccessStructuredContent(resultData),
@@ -202,66 +207,46 @@ export function buildHelpResponse(input) {
202
207
  };
203
208
  }
204
209
  export function buildStatusResponse(result, labels) {
205
- const { active: _active, branch: _branch, protocolFiles: _protocolFiles, keiyakuPreview: _keiyakuPreview, lastRoundPreview: _lastRoundPreview, ...resultData } = result;
206
210
  const blockedItems = {};
207
- if (result.operationReadiness.start.blockers.length > 0) {
208
- blockedItems[labels.start] = result.operationReadiness.start.blockers;
211
+ if (result.blockedByOperation.start.length > 0) {
212
+ blockedItems[labels.start] = result.blockedByOperation.start;
209
213
  }
210
- if (result.operationReadiness.drive.blockers.length > 0) {
211
- blockedItems[labels.drive] = result.operationReadiness.drive.blockers;
214
+ if (result.blockedByOperation.drive.length > 0) {
215
+ blockedItems[labels.drive] = result.blockedByOperation.drive;
212
216
  }
213
- if (result.operationReadiness.present.blockers.length > 0) {
214
- blockedItems[labels.present] = result.operationReadiness.present.blockers;
217
+ if (result.blockedByOperation.present.length > 0) {
218
+ blockedItems[labels.close] = result.blockedByOperation.present;
215
219
  }
216
- const operationReadiness = {
217
- [labels.start]: result.operationReadiness.start,
218
- [labels.drive]: result.operationReadiness.drive,
219
- [labels.present]: result.operationReadiness.present,
220
- };
221
220
  const summaryLine = result.branch
222
- ? `Active contract on '${result.branch}'.`
223
- : "No active contract branch.";
224
- const draftFileInfo = result.protocolFiles.draft.present
225
- ? [
226
- `present`,
227
- `tracked=${result.protocolFiles.draft.tracked ? "yes" : "no"}`,
228
- `dirty=${result.protocolFiles.draft.dirty ? "yes" : "no"}`,
229
- ].join(", ")
230
- : "missing";
221
+ ? `${STATUS_ACTIVE_SUMMARY_PREFIX} ${result.branch} (round ${result.round})`
222
+ : STATUS_INACTIVE_SUMMARY;
231
223
  const stateLines = [
232
- ...formatMaybe("Active Branch", result.branch, 200),
233
- ...formatMaybe("Base Branch", result.baseBranch, 200),
234
- `Round: ${result.round}`,
224
+ ...formatMaybe("Keiyaku Preview", result.keiyakuPreview, 400),
235
225
  ...formatMaybe("Last Round Preview", result.lastRoundPreview, 400),
236
- ];
237
- const protocolLines = [
238
- formatStatusFileSignal(result.protocolFiles.keiyaku),
239
- formatStatusFileSignal(result.protocolFiles.trace),
240
- `${result.protocolFiles.draft.path}: ${draftFileInfo}`,
226
+ ...formatMaybe("Draft Preview", result.draftPreview, 400),
241
227
  ];
242
228
  const readinessLines = [
243
- ...formatBlockedOperation(labels.start, result.operationReadiness.start),
244
- ...formatBlockedOperation(labels.drive, result.operationReadiness.drive),
245
- ...formatBlockedOperation(labels.present, result.operationReadiness.present),
229
+ ...formatBlockedOperation(labels.start, result.blockedByOperation.start),
230
+ ...formatBlockedOperation(labels.drive, result.blockedByOperation.drive),
231
+ ...formatBlockedOperation(labels.close, result.blockedByOperation.present),
246
232
  ];
247
- if (readinessLines.length === 0) {
248
- readinessLines.push("No blocked operations detected from repository/protocol state.");
249
- }
250
233
  const hintLines = formatHints(result.hints, { maxItems: 12, maxItemChars: 400 });
251
234
  const text = assembleResponse(`${RESPONSE_MARKERS.round} Status`, summaryLine, [
252
- buildSection("Keiyaku", stateLines),
253
- buildSection("Warning", readinessLines),
254
- buildSection("Hints", hintLines),
255
- buildSection("Info", protocolLines),
235
+ buildSection(STATUS_KEIYAKU_SECTION_TITLE, stateLines),
236
+ buildSection(STATUS_BLOCKED_SECTION_TITLE, readinessLines),
237
+ buildSection(HINTS_SECTION_TITLE, hintLines),
256
238
  ].filter((section) => section !== null));
257
239
  return {
258
240
  content: [{ type: "text", text }],
259
241
  structuredContent: buildSuccessStructuredContent({
260
- ...resultData,
242
+ active: result.active,
243
+ round: result.round,
244
+ baseBranch: result.baseBranch,
245
+ hints: result.hints,
261
246
  currentKeiyakuBranch: result.branch ?? null,
262
247
  keiyakuPreview: result.keiyakuPreview ?? null,
263
248
  lastRoundPreview: result.lastRoundPreview ?? null,
264
- draftPreview: result.protocolFiles.draft.present ? draftFileInfo : null,
249
+ draftPreview: result.draftPreview ?? null,
265
250
  blockedItems,
266
251
  }),
267
252
  };
@@ -1,4 +1,4 @@
1
- import { DIFF_OVERVIEW_SECTION_TITLE, SECTION_ICONS } from "../common/response-style.js";
1
+ import { SECTION_ICONS } from "../common/response-style.js";
2
2
  const HEADER_MAX_WIDTH = 40;
3
3
  const HEADER_DASH = "┈";
4
4
  function supportsHeaderColor() {
@@ -50,21 +50,10 @@ export function assembleResponse(status, summary, sections, infoLines = []) {
50
50
  if (summaryLine) {
51
51
  statusLines.push(`↳ ${summaryLine}`);
52
52
  }
53
- const normalSections = [];
54
- const diffSections = [];
55
- for (const section of sections) {
56
- if (section.title.trim().toLowerCase() === DIFF_OVERVIEW_SECTION_TITLE.toLowerCase()) {
57
- diffSections.push(section);
58
- }
59
- else {
60
- normalSections.push(section);
61
- }
62
- }
63
- const fullSections = [...normalSections];
53
+ const fullSections = [...sections];
64
54
  if (infoLines.length > 0) {
65
55
  fullSections.push({ title: "Info", body: infoLines.join("\n") });
66
56
  }
67
- fullSections.push(...diffSections);
68
57
  const renderedSections = fullSections
69
58
  .map((section) => {
70
59
  const body = section.body.trim();
@@ -7,7 +7,7 @@ import * as git from "../utils/git.js";
7
7
  import { parseDiffCoordinates } from "../utils/trace.js";
8
8
  import { buildStartPrompt } from "./prompts.js";
9
9
  import { renderToolRoundSummary } from "./round-summary.js";
10
- import { assertCleanWorkingTree } from "./contract.js";
10
+ import { assertCleanWorkingTree } from "./keiyaku.js";
11
11
  import { runAndRecordRound } from "./round.js";
12
12
  import { renderKeiyaku } from "./keiyaku-document-builder.js";
13
13
  import { parseAndValidateKeiyakuDraft, } from "./keiyaku-draft.js";