@astrosheep/keiyaku 0.1.47 → 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.
Files changed (58) hide show
  1. package/build/.tsbuildinfo +1 -1
  2. package/build/agents/round-runner.js +1 -1
  3. package/build/agents/selector.js +1 -1
  4. package/build/common/constants.js +6 -1
  5. package/build/common/errors.js +1 -1
  6. package/build/common/response-style.js +4 -1
  7. package/build/config/term-presets/constants.js +24 -0
  8. package/build/config/term-presets/default-preset.js +119 -0
  9. package/build/config/term-presets/index.js +5 -0
  10. package/build/config/term-presets/mischief-preset.js +105 -0
  11. package/build/config/term-presets/pocket-preset.js +105 -0
  12. package/build/config/term-presets/resolver.js +52 -0
  13. package/build/config/term-presets/types.js +1 -0
  14. package/build/handlers/ask.js +10 -120
  15. package/build/handlers/close.js +2 -9
  16. package/build/handlers/drive.js +1 -15
  17. package/build/handlers/shared.js +1 -2
  18. package/build/handlers/start.js +0 -17
  19. package/build/handlers/status.js +2 -6
  20. package/build/index.js +2 -2
  21. package/build/types/git-diff.js +1 -0
  22. package/build/utils/ask-history.js +75 -0
  23. package/build/utils/draft.js +22 -0
  24. package/build/utils/git-diff/constants.js +9 -0
  25. package/build/utils/git-diff/filter.js +70 -0
  26. package/build/utils/git-diff/incremental.js +111 -0
  27. package/build/utils/git-diff/index.js +3 -0
  28. package/build/utils/git-diff/parsers.js +160 -0
  29. package/build/utils/git-diff/preview.js +157 -0
  30. package/build/utils/git-diff/stat.js +27 -0
  31. package/build/utils/git-diff/types.js +1 -0
  32. package/build/utils/git-ops.js +9 -0
  33. package/build/utils/git.js +1 -1
  34. package/build/utils/keiyaku-document/index.js +13 -0
  35. package/build/utils/keiyaku-document/lex.js +178 -0
  36. package/build/utils/keiyaku-document/parser.js +242 -0
  37. package/build/utils/keiyaku-document/render.js +68 -0
  38. package/build/utils/keiyaku-document/sections.js +105 -0
  39. package/build/utils/keiyaku-document/types.js +6 -0
  40. package/build/utils/keiyaku-document.test.js +12 -1
  41. package/build/utils/trace.js +6 -7
  42. package/build/workflow/ask-execution.js +81 -0
  43. package/build/workflow/drive.js +10 -5
  44. package/build/workflow/iterate-plan.js +1 -1
  45. package/build/workflow/keiyaku-draft.js +1 -1
  46. package/build/workflow/{contract.js → keiyaku.js} +2 -2
  47. package/build/workflow/markdown-normalization.js +3 -2
  48. package/build/workflow/present.js +68 -13
  49. package/build/workflow/response-builders.js +75 -44
  50. package/build/workflow/response-meta.js +15 -0
  51. package/build/workflow/response-renderer.js +2 -13
  52. package/build/workflow/round-summary.js +1 -1
  53. package/build/workflow/start.js +8 -3
  54. package/build/workflow/status.js +129 -62
  55. package/package.json +1 -1
  56. package/build/config/term-presets.js +0 -398
  57. package/build/utils/git-diff.js +0 -519
  58. package/build/utils/keiyaku-document.js +0 -539
@@ -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();
@@ -31,6 +37,15 @@ function formatList(label, items, opts) {
31
37
  const tail = items.length > maxItems ? [`...(+${items.length - maxItems} more)`] : [];
32
38
  return [...head, ...shown, ...tail];
33
39
  }
40
+ function formatHints(hints, opts) {
41
+ const maxItems = opts?.maxItems ?? FORMAT_LIST_MAX_ITEMS;
42
+ const maxItemChars = opts?.maxItemChars ?? FORMAT_LIST_MAX_ITEM_CHARS;
43
+ if (hints.length === 0)
44
+ return [];
45
+ const shown = hints.slice(0, maxItems).map((hint) => `• ${truncateForDisplay(hint, maxItemChars)}`);
46
+ const tail = hints.length > maxItems ? [`...(+${hints.length - maxItems} more)`] : [];
47
+ return [...shown, ...tail];
48
+ }
34
49
  function buildSection(title, content) {
35
50
  const body = Array.isArray(content) ? content.join("\n") : content;
36
51
  const normalized = body.trim();
@@ -38,10 +53,12 @@ function buildSection(title, content) {
38
53
  return null;
39
54
  return { title, body };
40
55
  }
41
- function formatOperationReadiness(label, readiness) {
56
+ function formatBlockedOperation(label, blockers) {
57
+ if (blockers.length === 0)
58
+ return [];
42
59
  return [
43
- `${label} Available: ${readiness.available ? "yes" : "no"}`,
44
- ...formatList(`${label} Blockers`, readiness.blockers, { maxItems: 10, maxItemChars: 300 }),
60
+ `${label}:`,
61
+ ...formatList(`${label} Blockers`, blockers, { maxItems: 10, maxItemChars: 300 }),
45
62
  ];
46
63
  }
47
64
  function buildSuccessStructuredContent(data) {
@@ -57,8 +74,9 @@ export function buildKeiyakuSuccessResponse(result, input) {
57
74
  const responseData = normalizedRules ? { ...resultData, rules: normalizedRules } : { ...resultData };
58
75
  const summarySection = buildSection("Summary", result.summary);
59
76
  const rulesSection = buildSection("Rules", truncateForDisplay(normalizedRules, RULES_SECTION_MAX_CHARS));
77
+ const hintsSection = buildSection(HINTS_SECTION_TITLE, formatHints(result.meta?.hints ?? [], { maxItems: 12, maxItemChars: 400 }));
60
78
  const diffSection = buildSection(DIFF_OVERVIEW_SECTION_TITLE, result.diff);
61
- const infoLines = [
79
+ const infoSection = buildSection(INFO_SECTION_TITLE, [
62
80
  ...formatMaybe("Identity", input.name, 120),
63
81
  ...formatMaybe("Consumed from_file and deleted", result.consumedFromFile, 300),
64
82
  ...formatList("Existing local keiyaku branches (non-blocking)", result.existingBranches ?? [], {
@@ -68,12 +86,11 @@ export function buildKeiyakuSuccessResponse(result, input) {
68
86
  ...formatMaybe(BRANCH_LABEL, result.currentBranch, 200),
69
87
  ...formatMaybe("Base Branch", result.baseBranch, 200),
70
88
  ...formatMaybe("Commit", result.commit, 100),
71
- ...(input.nextHints?.start ?? []),
72
- ];
89
+ ]);
73
90
  const summaryLine = result.commit
74
91
  ? `Created branch '${result.currentBranch}' (base: '${result.baseBranch}') [${result.commit}].`
75
92
  : `Created branch '${result.currentBranch}' (base: '${result.baseBranch}').`;
76
- const text = assembleResponse(`${RESPONSE_MARKERS.round} Started (Round ${result.round})`, summaryLine, [summarySection, rulesSection, 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));
77
94
  return {
78
95
  content: [{ type: "text", text }],
79
96
  structuredContent: buildSuccessStructuredContent(responseData),
@@ -82,18 +99,18 @@ export function buildKeiyakuSuccessResponse(result, input) {
82
99
  export function buildDriveResponse(result, input) {
83
100
  const { status: _status, ...resultData } = result;
84
101
  const summarySection = buildSection("Summary", result.summary);
102
+ const hintsSection = buildSection(HINTS_SECTION_TITLE, formatHints(result.meta?.hints ?? [], { maxItems: 12, maxItemChars: 400 }));
85
103
  const diffSection = buildSection(DIFF_OVERVIEW_SECTION_TITLE, result.diff);
86
- const infoLines = [
104
+ const infoSection = buildSection(INFO_SECTION_TITLE, [
87
105
  ...formatMaybe("Identity", input.name, 120),
88
106
  ...formatMaybe(BRANCH_LABEL, result.currentBranch, 200),
89
107
  ...formatMaybe("Base Branch", result.baseBranch, 200),
90
108
  ...formatMaybe("Commit", result.commit, 100),
91
- ...(input.nextHints?.drive ?? []),
92
- ];
109
+ ]);
93
110
  const summaryLine = result.commit
94
111
  ? `Updated branch '${result.currentBranch}' [${result.commit}].`
95
112
  : `Updated branch '${result.currentBranch}'.`;
96
- const text = assembleResponse(`${RESPONSE_MARKERS.round} Driven (Round ${result.round})`, summaryLine, [summarySection, 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));
97
114
  return {
98
115
  content: [{ type: "text", text }],
99
116
  structuredContent: buildSuccessStructuredContent(resultData),
@@ -135,13 +152,14 @@ export function buildCloseDoneResponse(result, input) {
135
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]`,
136
153
  ...formatMaybe("Oath", input.oath, 220),
137
154
  ];
138
- 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);
139
157
  return {
140
158
  content: [{ type: "text", text }],
141
159
  structuredContent: buildSuccessStructuredContent(resultData),
142
160
  };
143
161
  }
144
- export function buildCloseDropResponse(result, input) {
162
+ export function buildCloseDropResponse(result, _input) {
145
163
  const { status: _status, ...resultData } = result;
146
164
  const droppedChanges = result.droppedChanges ?? [];
147
165
  const warningSection = buildSection("Warning", formatList("Dropped Local Changes", droppedChanges, { maxItems: 40, maxItemChars: 400 }));
@@ -151,7 +169,8 @@ export function buildCloseDropResponse(result, input) {
151
169
  ...formatMaybe(BRANCH_LABEL, result.currentBranch, 200),
152
170
  ...formatMaybe("Base Branch", result.baseBranch, 200),
153
171
  ];
154
- 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);
155
174
  return {
156
175
  content: [{ type: "text", text }],
157
176
  structuredContent: buildSuccessStructuredContent(resultData),
@@ -167,7 +186,7 @@ export function buildToolErrorResponse(input) {
167
186
  error: {
168
187
  code: input.errorCode,
169
188
  message: input.message,
170
- suggestion: input.hint,
189
+ suggestion: input.hints[0] ?? "",
171
190
  },
172
191
  },
173
192
  };
@@ -178,9 +197,8 @@ export function buildToolErrorResponse(input) {
178
197
  }
179
198
  function buildToolErrorText(input) {
180
199
  const errorSection = buildSection("Error", input.message);
181
- const hintSection = buildSection("Hint", input.hint);
182
- const contextSection = buildSection("Context", input.inputEcho ?? []);
183
- return assembleResponse(colorizeErrorStatus(`${RESPONSE_MARKERS.failure} Failed [${input.errorCode}]`), "Request could not be completed.", [errorSection, hintSection, contextSection].filter((section) => section !== null));
200
+ const hintsSection = buildSection("Hints", formatHints(input.hints, { maxItems: 12, maxItemChars: 400 }));
201
+ return assembleResponse(colorizeErrorStatus(`${RESPONSE_MARKERS.failure} Failed [${input.errorCode}]`), "Request could not be completed.", [errorSection, hintsSection].filter((section) => section !== null));
184
202
  }
185
203
  export function buildHelpResponse(input) {
186
204
  return {
@@ -189,34 +207,47 @@ export function buildHelpResponse(input) {
189
207
  };
190
208
  }
191
209
  export function buildStatusResponse(result, labels) {
192
- const summaryLine = result.active
193
- ? `Active keiyaku branch '${result.branch ?? "(unknown)"}'.`
194
- : "No active keiyaku branch.";
195
- const draftFileInfo = result.protocolFiles.draft.present
196
- ? [
197
- `present`,
198
- `tracked=${result.protocolFiles.draft.tracked ? "yes" : "no"}`,
199
- `dirty=${result.protocolFiles.draft.dirty ? "yes" : "no"}`,
200
- ].join(", ")
201
- : "missing";
202
- const infoLines = [
203
- `Active: ${result.active ? "yes" : "no"}`,
204
- ...formatMaybe("Branch", result.branch, 200),
205
- ...formatMaybe("Base Branch", result.baseBranch, 200),
206
- `Round: ${result.round}`,
207
- `Pending Review: ${result.pendingReview ? "yes" : "no"}`,
208
- `${result.protocolFiles.keiyaku.path}: ${result.protocolFiles.keiyaku.present ? "present" : "missing"}`,
209
- `${result.protocolFiles.trace.path}: ${result.protocolFiles.trace.present ? "present" : "missing"}`,
210
- `${result.protocolFiles.draft.path}: ${draftFileInfo}`,
211
- ...formatOperationReadiness(labels.start, result.operationReadiness.start),
212
- ...formatOperationReadiness(labels.drive, result.operationReadiness.drive),
213
- ...formatOperationReadiness(labels.present, result.operationReadiness.present),
214
- ...formatMaybe("Last Round Outcome", result.lastRoundSummary, 400),
215
- ...formatList("Hints", result.hints, { maxItems: 12, maxItemChars: 400 }),
210
+ const blockedItems = {};
211
+ if (result.blockedByOperation.start.length > 0) {
212
+ blockedItems[labels.start] = result.blockedByOperation.start;
213
+ }
214
+ if (result.blockedByOperation.drive.length > 0) {
215
+ blockedItems[labels.drive] = result.blockedByOperation.drive;
216
+ }
217
+ if (result.blockedByOperation.present.length > 0) {
218
+ blockedItems[labels.close] = result.blockedByOperation.present;
219
+ }
220
+ const summaryLine = result.branch
221
+ ? `${STATUS_ACTIVE_SUMMARY_PREFIX} — ${result.branch} (round ${result.round})`
222
+ : STATUS_INACTIVE_SUMMARY;
223
+ const stateLines = [
224
+ ...formatMaybe("Keiyaku Preview", result.keiyakuPreview, 400),
225
+ ...formatMaybe("Last Round Preview", result.lastRoundPreview, 400),
226
+ ...formatMaybe("Draft Preview", result.draftPreview, 400),
216
227
  ];
217
- const text = assembleResponse(`${RESPONSE_MARKERS.round} Status`, summaryLine, [], infoLines);
228
+ const readinessLines = [
229
+ ...formatBlockedOperation(labels.start, result.blockedByOperation.start),
230
+ ...formatBlockedOperation(labels.drive, result.blockedByOperation.drive),
231
+ ...formatBlockedOperation(labels.close, result.blockedByOperation.present),
232
+ ];
233
+ const hintLines = formatHints(result.hints, { maxItems: 12, maxItemChars: 400 });
234
+ const text = assembleResponse(`${RESPONSE_MARKERS.round} Status`, summaryLine, [
235
+ buildSection(STATUS_KEIYAKU_SECTION_TITLE, stateLines),
236
+ buildSection(STATUS_BLOCKED_SECTION_TITLE, readinessLines),
237
+ buildSection(HINTS_SECTION_TITLE, hintLines),
238
+ ].filter((section) => section !== null));
218
239
  return {
219
240
  content: [{ type: "text", text }],
220
- structuredContent: buildSuccessStructuredContent(result),
241
+ structuredContent: buildSuccessStructuredContent({
242
+ active: result.active,
243
+ round: result.round,
244
+ baseBranch: result.baseBranch,
245
+ hints: result.hints,
246
+ currentKeiyakuBranch: result.branch ?? null,
247
+ keiyakuPreview: result.keiyakuPreview ?? null,
248
+ lastRoundPreview: result.lastRoundPreview ?? null,
249
+ draftPreview: result.draftPreview ?? null,
250
+ blockedItems,
251
+ }),
221
252
  };
222
253
  }
@@ -0,0 +1,15 @@
1
+ import { resolveTermPreset } from "../config/term-presets/index.js";
2
+ import { renderPreset } from "../utils/text-utils.js";
3
+ function renderPresetHints() {
4
+ const preset = resolveTermPreset();
5
+ return renderPreset(preset.hints, {
6
+ start: preset.tools.start.name,
7
+ drive: preset.tools.drive.name,
8
+ ask: preset.tools.ask.name,
9
+ close: preset.tools.close.name,
10
+ identity: preset.identity,
11
+ });
12
+ }
13
+ export function resolveWorkflowHints(kind) {
14
+ return renderPresetHints()[kind];
15
+ }
@@ -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();
@@ -1,4 +1,4 @@
1
- import { parseMarkdownSections } from "../utils/keiyaku-document.js";
1
+ import { parseMarkdownSections } from "../utils/keiyaku-document/index.js";
2
2
  export const ROUND_SUMMARY_SECTION_KEYS = [
3
3
  "outcome",
4
4
  "changes",
@@ -1,18 +1,19 @@
1
1
  import * as fs from "fs/promises";
2
2
  import * as path from "path";
3
- import { KEIYAKU_BRANCH_PREFIX, KEIYAKU_DRAFT_FILE, KEIYAKU_FILE, TRACE_FILE } from "../common/constants.js";
3
+ import { INCREMENTAL_DIFF_MAX_CHARS, KEIYAKU_BRANCH_PREFIX, KEIYAKU_DRAFT_FILE, KEIYAKU_FILE, TRACE_FILE, } from "../common/constants.js";
4
4
  import { logInfo, logWarn } from "../utils/logger.js";
5
5
  import { FlowError, isFlowError, requireText, wrapFlowError } from "../common/errors.js";
6
6
  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";
14
14
  import { resolveIncrementalDiffMode } from "../config/incremental-diff-mode.js";
15
15
  import { readBaseRules } from "./base-rules.js";
16
+ import { resolveWorkflowHints } from "./response-meta.js";
16
17
  const INTERNAL_START_DIRTY_ALLOWLIST = [];
17
18
  function normalizeOptionalText(value) {
18
19
  if (value === undefined)
@@ -289,7 +290,10 @@ export async function startKeiyaku(input) {
289
290
  coordinates = undefined;
290
291
  }
291
292
  }
292
- const diff = await git.getIncrementalDiff(cwd, incrementalDiffMode, coordinates);
293
+ const diff = await git.getIncrementalDiff(cwd, incrementalDiffMode, {
294
+ coordinates,
295
+ maxChars: INCREMENTAL_DIFF_MAX_CHARS,
296
+ });
293
297
  if (resolved.fromFile && resolved.fromFilePath) {
294
298
  await fs.unlink(resolved.fromFilePath);
295
299
  consumedFromFile = resolved.fromFile;
@@ -310,6 +314,7 @@ export async function startKeiyaku(input) {
310
314
  currentBranch: keiyakuBranch,
311
315
  baseBranch,
312
316
  commit,
317
+ meta: { hints: resolveWorkflowHints("start") },
313
318
  };
314
319
  }
315
320
  catch (err) {
@@ -2,7 +2,7 @@ import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { KEIYAKU_BRANCH_PREFIX, KEIYAKU_DRAFT_FILE, KEIYAKU_FILE, TRACE_FILE, } from "../common/constants.js";
4
4
  import { FlowError } from "../common/errors.js";
5
- import { parseToAST, renderNodeContent } from "../utils/keiyaku-document.js";
5
+ import { parseToAST, renderNodeContent } from "../utils/keiyaku-document/index.js";
6
6
  import * as git from "../utils/git.js";
7
7
  import { computeTraceState } from "../utils/trace.js";
8
8
  import { parseRoundSummary } from "./round-summary.js";
@@ -16,45 +16,41 @@ function parseRoundSectionNumber(title) {
16
16
  function normalizeHeading(value) {
17
17
  return value.trim().toLowerCase();
18
18
  }
19
- function unwrapMarkdownFence(content) {
20
- const trimmed = content.trim();
21
- const fencedMatch = /^```(?:markdown)?\s*\n([\s\S]*?)\n```$/i.exec(trimmed);
22
- return fencedMatch ? fencedMatch[1].trim() : trimmed;
23
- }
24
- function renderSummarySection(summaryNodes) {
25
- if (summaryNodes.length === 0)
19
+ const PREVIEW_SECTION_TITLES = ["goal", "context"];
20
+ const PREVIEW_SECTION_TITLE_SET = new Set(PREVIEW_SECTION_TITLES);
21
+ function renderNodesForPreview(nodes) {
22
+ if (nodes.length === 0)
26
23
  return "";
27
- for (const node of summaryNodes) {
28
- if (node.type !== "code_block")
29
- continue;
30
- const body = unwrapMarkdownFence(node.lines.join("\n"));
31
- if (body.length > 0) {
32
- return body;
24
+ return nodes
25
+ .map((node) => {
26
+ if (node.type === "blockquote") {
27
+ return node.value.trim();
33
28
  }
34
- }
35
- const rendered = summaryNodes
36
- .map((node) => renderNodeContent(node).trim())
37
- .filter((node) => node.length > 0)
29
+ return renderNodeContent(node).trim();
30
+ })
31
+ .filter((text) => text.length > 0)
38
32
  .join("\n")
39
33
  .trim();
40
- return unwrapMarkdownFence(rendered);
41
34
  }
42
- function extractRoundRawSummary(roundSection) {
43
- let captureSummary = false;
44
- const summaryNodes = [];
45
- for (const child of roundSection.children) {
35
+ function extractHeadingBody(section, heading) {
36
+ let capture = false;
37
+ const captured = [];
38
+ for (const child of section.children) {
46
39
  if (child.type === "heading" && child.level === 3) {
47
- if (captureSummary) {
40
+ if (capture) {
48
41
  break;
49
42
  }
50
- captureSummary = normalizeHeading(child.text) === "summary";
43
+ capture = normalizeHeading(child.text) === heading;
51
44
  continue;
52
45
  }
53
- if (!captureSummary)
46
+ if (!capture)
54
47
  continue;
55
- summaryNodes.push(child);
48
+ captured.push(child);
56
49
  }
57
- const summary = renderSummarySection(summaryNodes);
50
+ return captured;
51
+ }
52
+ function extractRoundRawSummary(roundSection) {
53
+ const summary = renderNodesForPreview(extractHeadingBody(roundSection, "summary"));
58
54
  return summary.length > 0 ? summary : undefined;
59
55
  }
60
56
  function extractLastRoundSummary(traceContent) {
@@ -86,9 +82,57 @@ function extractLastRoundSummary(traceContent) {
86
82
  }
87
83
  return rawSummary;
88
84
  }
89
- async function readTrace(cwd) {
85
+ function extractDocumentPreviewBySections(content, fallbackToDocument) {
86
+ const ast = parseToAST(content, { allowSections: true });
87
+ const sectionBodies = new Map();
88
+ for (const node of ast.children) {
89
+ if (node.type !== "section" || node.level !== 2)
90
+ continue;
91
+ const normalizedTitle = normalizeHeading(node.title);
92
+ if (!PREVIEW_SECTION_TITLE_SET.has(normalizedTitle))
93
+ continue;
94
+ if (sectionBodies.has(normalizedTitle))
95
+ continue;
96
+ const body = renderNodesForPreview(node.children);
97
+ if (body.length === 0)
98
+ continue;
99
+ sectionBodies.set(normalizedTitle, body);
100
+ }
101
+ const preferredBody = PREVIEW_SECTION_TITLES
102
+ .map((title) => sectionBodies.get(title))
103
+ .filter((value) => Boolean(value))
104
+ .join("\n")
105
+ .trim();
106
+ if (preferredBody.length > 0) {
107
+ return preferredBody;
108
+ }
109
+ if (!fallbackToDocument)
110
+ return undefined;
111
+ const fullDocument = renderNodeContent(ast).trim();
112
+ return fullDocument.length > 0 ? fullDocument : undefined;
113
+ }
114
+ function toStatusPreview(text) {
115
+ if (!text)
116
+ return undefined;
117
+ const MAX_LINES = 2;
118
+ const MAX_LINE_CHARS = 180;
119
+ const lines = text
120
+ .split(/\r?\n/)
121
+ .map((line) => line.trim())
122
+ .filter((line) => line.length > 0);
123
+ if (lines.length === 0)
124
+ return undefined;
125
+ const previewLines = lines
126
+ .slice(0, MAX_LINES)
127
+ .map((line) => (line.length > MAX_LINE_CHARS ? `${line.slice(0, MAX_LINE_CHARS - 1)}…` : line));
128
+ if (lines.length > MAX_LINES) {
129
+ previewLines.push("…");
130
+ }
131
+ return previewLines.join("\n");
132
+ }
133
+ async function readOptionalFile(cwd, fileName) {
90
134
  try {
91
- return await fs.readFile(path.join(cwd, TRACE_FILE), "utf-8");
135
+ return await fs.readFile(path.join(cwd, fileName), "utf-8");
92
136
  }
93
137
  catch (error) {
94
138
  if (error?.code === "ENOENT") {
@@ -124,19 +168,35 @@ const STATUS_BLOCKERS = {
124
168
  missingBaseMetadata: "Active keiyaku branch is missing base metadata (branch.<keiyaku-branch>.keiyakuBase).",
125
169
  missingProtocolFiles: "Active keiyaku branch is missing KEIYAKU.md or KEIYAKU_TRACE.md; restore protocol files before continuing.",
126
170
  };
127
- function buildOperationReadiness(input) {
171
+ function toGitStatusLine(file) {
172
+ const indexCode = file.index.length > 0 ? file.index : " ";
173
+ const worktreeCode = file.working_dir.length > 0 ? file.working_dir : " ";
174
+ return `${indexCode}${worktreeCode} ${file.path}`;
175
+ }
176
+ function formatDirtyWorktreeBlocker(dirtyStatusLines) {
177
+ if (dirtyStatusLines.length === 0) {
178
+ return STATUS_BLOCKERS.dirtyWorktree;
179
+ }
180
+ const MAX_ITEMS = 5;
181
+ const shownLines = dirtyStatusLines.slice(0, MAX_ITEMS);
182
+ const overflowLine = dirtyStatusLines.length > MAX_ITEMS ? [`... (+${dirtyStatusLines.length - MAX_ITEMS} more)`] : [];
183
+ return [STATUS_BLOCKERS.dirtyWorktree, ...shownLines, ...overflowLine].join("\n");
184
+ }
185
+ function buildBlockedByOperation(input) {
128
186
  const startBlockers = [];
129
187
  const driveBlockers = [];
130
188
  const presentBlockers = [];
131
189
  const hasDirtyWorktree = input.dirtyPaths.length > 0;
132
190
  const hasBlockingDirtyForRoundOps = input.dirtyPaths.some((dirtyPath) => dirtyPath !== KEIYAKU_DRAFT_FILE);
133
191
  const missingProtocolFiles = !input.protocolFiles.keiyaku.present || !input.protocolFiles.trace.present;
192
+ const startDirtyBlocker = formatDirtyWorktreeBlocker(input.dirtyStatusLines);
193
+ const roundOpsDirtyBlocker = formatDirtyWorktreeBlocker(input.blockingDirtyStatusLines);
134
194
  if (hasDirtyWorktree) {
135
- startBlockers.push(STATUS_BLOCKERS.dirtyWorktree);
195
+ startBlockers.push(startDirtyBlocker);
136
196
  }
137
197
  if (hasBlockingDirtyForRoundOps) {
138
- driveBlockers.push(STATUS_BLOCKERS.dirtyWorktree);
139
- presentBlockers.push(STATUS_BLOCKERS.dirtyWorktree);
198
+ driveBlockers.push(roundOpsDirtyBlocker);
199
+ presentBlockers.push(roundOpsDirtyBlocker);
140
200
  }
141
201
  if (input.active) {
142
202
  startBlockers.push(STATUS_BLOCKERS.startActiveBranch);
@@ -156,14 +216,10 @@ function buildOperationReadiness(input) {
156
216
  startBlockers.push(STATUS_BLOCKERS.startProtocolOutsideActive);
157
217
  }
158
218
  }
159
- const readiness = (blockers) => ({
160
- available: blockers.length === 0,
161
- blockers,
162
- });
163
219
  return {
164
- start: readiness(startBlockers),
165
- drive: readiness(driveBlockers),
166
- present: readiness(presentBlockers),
220
+ start: startBlockers,
221
+ drive: driveBlockers,
222
+ present: presentBlockers,
167
223
  };
168
224
  }
169
225
  function buildStatusHints(input) {
@@ -178,12 +234,7 @@ function buildStatusHints(input) {
178
234
  if (!input.baseBranch) {
179
235
  hints.push("Base branch metadata is missing; set branch.<keiyaku-branch>.keiyakuBase to avoid close-time failures.");
180
236
  }
181
- if (input.pendingReview) {
182
- hints.push("Pending review detected; run the next round to address the latest review feedback.");
183
- }
184
- else {
185
- hints.push("No pending review detected; if criteria are done, proceed to close with CLAIM.");
186
- }
237
+ hints.push("Run another round with drive, or close with CLAIM when criteria and rules checks are complete.");
187
238
  }
188
239
  else if (input.localKeiyakuBranches.length > 0) {
189
240
  hints.push(`No active keiyaku branch; checkout one to resume: ${formatBranchCandidates(input.localKeiyakuBranches)}.`);
@@ -209,7 +260,12 @@ export async function readKeiyakuStatus(input) {
209
260
  isPresentFile(cwd, TRACE_FILE),
210
261
  isPresentFile(cwd, KEIYAKU_DRAFT_FILE),
211
262
  ]);
212
- const dirtyPaths = await git.getDirtyPaths(cwd);
263
+ const dirtyFiles = await git.getDirtyFiles(cwd);
264
+ const dirtyPaths = Array.from(new Set(dirtyFiles.map((file) => file.path)));
265
+ const dirtyStatusLines = dirtyFiles.map((file) => toGitStatusLine(file));
266
+ const blockingDirtyStatusLines = dirtyFiles
267
+ .filter((file) => file.path !== KEIYAKU_DRAFT_FILE)
268
+ .map((file) => toGitStatusLine(file));
213
269
  const draftDirty = dirtyPaths.includes(KEIYAKU_DRAFT_FILE);
214
270
  const draftTracked = draftPresent ? await git.isPathTracked(cwd, KEIYAKU_DRAFT_FILE) : undefined;
215
271
  const protocolFiles = {
@@ -223,34 +279,46 @@ export async function readKeiyakuStatus(input) {
223
279
  };
224
280
  const currentBranch = await git.getCurrentBranch(cwd);
225
281
  if (!currentBranch.startsWith(KEIYAKU_BRANCH_PREFIX)) {
282
+ const [keiyakuContent, draftContent] = await Promise.all([
283
+ readOptionalFile(cwd, KEIYAKU_FILE),
284
+ readOptionalFile(cwd, KEIYAKU_DRAFT_FILE),
285
+ ]);
226
286
  const localKeiyakuBranches = await git.listLocalKeiyakuBranches(cwd);
227
- const operationReadiness = buildOperationReadiness({
287
+ const blockedByOperation = buildBlockedByOperation({
228
288
  active: false,
229
289
  dirtyPaths,
290
+ dirtyStatusLines,
291
+ blockingDirtyStatusLines,
230
292
  protocolFiles,
231
293
  });
232
294
  return {
233
295
  active: false,
234
296
  round: 0,
235
- pendingReview: false,
236
- protocolFiles,
237
- operationReadiness,
297
+ keiyakuPreview: toStatusPreview(keiyakuContent ? extractDocumentPreviewBySections(keiyakuContent, false) : undefined),
298
+ draftPreview: toStatusPreview(draftContent ? extractDocumentPreviewBySections(draftContent, true) : undefined),
299
+ blockedByOperation,
238
300
  hints: buildStatusHints({
239
301
  active: false,
240
- pendingReview: false,
241
302
  protocolFiles,
242
303
  localKeiyakuBranches,
243
304
  }),
244
305
  };
245
306
  }
246
- const traceContent = await readTrace(cwd);
307
+ const [traceContent, keiyakuContent, draftContent] = await Promise.all([
308
+ readOptionalFile(cwd, TRACE_FILE),
309
+ readOptionalFile(cwd, KEIYAKU_FILE),
310
+ readOptionalFile(cwd, KEIYAKU_DRAFT_FILE),
311
+ ]);
247
312
  const traceState = traceContent ? computeTraceState(traceContent) : { maxRound: 0, pendingReviewRound: null };
248
313
  const baseBranch = await git.getKeiyakuBase(cwd, currentBranch);
249
- const lastRoundSummary = traceContent ? extractLastRoundSummary(traceContent) : undefined;
250
- const pendingReview = traceState.pendingReviewRound !== null;
251
- const operationReadiness = buildOperationReadiness({
314
+ const keiyakuPreview = toStatusPreview(keiyakuContent ? extractDocumentPreviewBySections(keiyakuContent, false) : undefined);
315
+ const lastRoundPreview = toStatusPreview(traceContent ? extractLastRoundSummary(traceContent) : undefined);
316
+ const draftPreview = toStatusPreview(draftContent ? extractDocumentPreviewBySections(draftContent, true) : undefined);
317
+ const blockedByOperation = buildBlockedByOperation({
252
318
  active: true,
253
319
  dirtyPaths,
320
+ dirtyStatusLines,
321
+ blockingDirtyStatusLines,
254
322
  baseBranch: baseBranch ?? undefined,
255
323
  protocolFiles,
256
324
  });
@@ -259,13 +327,12 @@ export async function readKeiyakuStatus(input) {
259
327
  branch: currentBranch,
260
328
  baseBranch: baseBranch ?? undefined,
261
329
  round: traceState.maxRound,
262
- pendingReview,
263
- lastRoundSummary,
264
- protocolFiles,
265
- operationReadiness,
330
+ keiyakuPreview,
331
+ lastRoundPreview,
332
+ draftPreview,
333
+ blockedByOperation,
266
334
  hints: buildStatusHints({
267
335
  active: true,
268
- pendingReview,
269
336
  baseBranch: baseBranch ?? undefined,
270
337
  protocolFiles,
271
338
  localKeiyakuBranches: [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/keiyaku",
3
- "version": "0.1.47",
3
+ "version": "0.1.49",
4
4
  "description": "MCP server for running iterative keiyaku workflows with Codex subagents.",
5
5
  "license": "MIT",
6
6
  "type": "module",