@astrosheep/keiyaku 0.1.21 → 0.1.22

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.
@@ -0,0 +1,69 @@
1
+ import { parseToAST, renderNodeContent, } from "../utils/keiyaku-document.js";
2
+ export function computeHeadingDelta(shallowest, minLevel) {
3
+ if (shallowest === null || shallowest >= minLevel) {
4
+ return 0;
5
+ }
6
+ return minLevel - shallowest;
7
+ }
8
+ export function demoteMarkdownHeadings(text, minLevel = 3) {
9
+ const ast = parseToAST(text, { allowSections: false });
10
+ // Preserve relative heading hierarchy by shifting all headings together.
11
+ // This avoids collapsing something like `# A` and `### B` into the same level.
12
+ let shallowest = null;
13
+ {
14
+ const stack = [ast];
15
+ while (stack.length > 0) {
16
+ const node = stack.pop();
17
+ if (!node)
18
+ continue;
19
+ if (node.type === "heading") {
20
+ shallowest = shallowest === null ? node.level : Math.min(shallowest, node.level);
21
+ }
22
+ if (node.type === "code_block" || node.type === "text")
23
+ continue;
24
+ if (node.type === "document" || node.type === "section" || node.type === "list_item") {
25
+ stack.push(...node.children);
26
+ }
27
+ else if (node.type === "list") {
28
+ stack.push(...node.items);
29
+ }
30
+ }
31
+ }
32
+ const delta = computeHeadingDelta(shallowest, minLevel);
33
+ if (delta === 0) {
34
+ // Return a normalized render (trimEnd) to avoid leaking extra trailing newlines.
35
+ return renderNodeContent(ast).trimEnd();
36
+ }
37
+ const shift = (node) => {
38
+ switch (node.type) {
39
+ case "heading": {
40
+ const nextLevel = Math.min(6, node.level + delta);
41
+ return nextLevel === node.level ? node : { ...node, level: nextLevel };
42
+ }
43
+ case "code_block":
44
+ case "text":
45
+ return node;
46
+ case "document":
47
+ case "section":
48
+ return { ...node, children: node.children.map((child) => shift(child)) };
49
+ case "list":
50
+ return { ...node, items: node.items.map((item) => shift(item)) };
51
+ case "list_item":
52
+ return { ...node, children: node.children.map((child) => shift(child)) };
53
+ }
54
+ };
55
+ return renderNodeContent(shift(ast)).trimEnd();
56
+ }
57
+ export function renderMarkdownSections(items) {
58
+ return items
59
+ .map((item) => {
60
+ // Keiyaku uses H2 as contract section delimiters. Keep payload headings at H3+.
61
+ const normalized = demoteMarkdownHeadings(item, 3);
62
+ const trimmed = normalized.trimStart();
63
+ if (/^[-*+]\s+/.test(trimmed) || /^\d+\.\s+/.test(trimmed)) {
64
+ return normalized;
65
+ }
66
+ return `- ${normalized}`;
67
+ })
68
+ .join("\n\n");
69
+ }
@@ -29,6 +29,14 @@ const SCORE_FIELD_TO_COMMANDMENT = {
29
29
  score_idiomatic: "Commandment of Idiom",
30
30
  score_cohesive: "Commandment of Cohesion",
31
31
  };
32
+ const SCORE_FIELD_TO_DEFINITION = {
33
+ score_precise: "Architectural placement. 10 = exact layer, exact boundary, zero misplacement.",
34
+ score_minimal: "Economy of change. 10 = no avoidable lines, no speculative edits, no hidden bloat.",
35
+ score_isolated: "Surgical containment. 10 = zero unrelated files, zero opportunistic cleanup, zero collateral.",
36
+ score_idiomatic: "Native fluency. 10 = naming, structure, style indistinguishable from the codebase.",
37
+ score_cohesive: "Single responsibility. 10 = each unit does one thing, boundaries intact.",
38
+ };
39
+ const VERDICT_DENIED_CODE = "VERDICT_DENIED";
32
40
  const VERDICT_SETTINGS_FILE = path.join(".keiyaku", "settings.json");
33
41
  function clampThreshold(value, fallback) {
34
42
  if (typeof value !== "number" || !Number.isFinite(value))
@@ -67,7 +75,7 @@ async function resolveVerdictConfig(cwd) {
67
75
  parsed = JSON.parse(rawSettings);
68
76
  }
69
77
  catch (error) {
70
- throw new FlowError("CLOSE_QUALITY_GATE_FAILED", "Configuration Error: Invalid settings in .keiyaku/settings.json (must be valid JSON).", error);
78
+ throw new FlowError(VERDICT_DENIED_CODE, "Configuration Error: Invalid settings in .keiyaku/settings.json (must be valid JSON).", error);
71
79
  }
72
80
  if (!parsed || typeof parsed !== "object") {
73
81
  return merged;
@@ -137,10 +145,12 @@ export async function presentWork(input) {
137
145
  }
138
146
  }
139
147
  const dirtyFiles = await git.getDirtyFiles(cwd);
140
- const droppedChanges = dirtyFiles.map((file) => `${file.index}${file.working_dir} ${file.path}`);
148
+ const droppedChanges = dirtyFiles
149
+ .filter((file) => file.path !== KEIYAKU_DRAFT_FILE)
150
+ .map((file) => `${file.index}${file.working_dir} ${file.path}`);
141
151
  try {
142
- if (droppedChanges.length > 0) {
143
- await git.discardAllWorkingTreeChanges(cwd);
152
+ if (dirtyFiles.length > 0) {
153
+ await git.discardWorkingTreeChangesExcluding(cwd, [KEIYAKU_DRAFT_FILE]);
144
154
  }
145
155
  await git.checkoutBranch(cwd, baseBranch);
146
156
  await git.deleteBranch(cwd, keiyakuBranch, true);
@@ -182,7 +192,7 @@ export async function presentWork(input) {
182
192
  const traceContent = await readTraceContent(cwd);
183
193
  if (petition === "CLAIM") {
184
194
  requireChecks("criteriaChecks", input.criteriaChecks);
185
- requireChecks("constraintsChecks", input.constraintsChecks);
195
+ requireChecks("rulesChecks", input.rulesChecks);
186
196
  const verdict = await resolveVerdictConfig(cwd);
187
197
  const failedCommandments = CLOSE_SCORE_FIELDS.flatMap((field) => {
188
198
  const score = input[field];
@@ -190,7 +200,8 @@ export async function presentWork(input) {
190
200
  const threshold = verdict.thresholds[dimension];
191
201
  if (score >= threshold)
192
202
  return [];
193
- return [`${SCORE_FIELD_TO_COMMANDMENT[field]} broke (${field}=${score}, requires >= ${threshold})`];
203
+ const definition = SCORE_FIELD_TO_DEFINITION[field];
204
+ return [`${SCORE_FIELD_TO_COMMANDMENT[field]} broke (${field}=${score}). Definition: ${definition}`];
194
205
  });
195
206
  const totalScore = CLOSE_SCORE_FIELDS.reduce((sum, field) => sum + input[field], 0);
196
207
  const totalDeficit = verdict.minTotalScore - totalScore;
@@ -202,7 +213,7 @@ export async function presentWork(input) {
202
213
  failures.push(`Total devotion deficit: ${totalScore}/${verdict.minTotalScore} (short by ${totalDeficit})`);
203
214
  }
204
215
  if (failures.length > 0) {
205
- throw new FlowError("CLOSE_QUALITY_GATE_FAILED", `Verdict Denied. ${failures.join(". ")}`);
216
+ throw new FlowError(VERDICT_DENIED_CODE, `Verdict Denied. ${failures.join(". ")}`);
206
217
  }
207
218
  const expectedOath = resolveOath();
208
219
  if (!oathMatches(input.oath, expectedOath)) {
@@ -10,14 +10,14 @@ Round: 1 (initial implementation).
10
10
 
11
11
  1. Read KEIYAKU.md. Use it as source of truth.
12
12
  2. If Directive exists, prioritize it. Otherwise, focus on the Goal and Criteria.
13
- 3. Treat all constraints/criteria as required mission boundaries.
13
+ 3. Treat all rules/criteria as required mission boundaries.
14
14
  4. Read KEIYAKU_TRACE.md if present.
15
15
  5. Execute:
16
16
  - Stay within the scope defined by the Directive (or Goal if no Directive exists).
17
- - Do not violate constraints.
17
+ - Do not violate rules.
18
18
  - Aim to progress toward criteria.
19
19
  - Match existing structure.
20
- 6. Final Audit: Ensure the ${auditTarget} is fully addressed and no constraints are violated.
20
+ 6. Final Audit: Ensure the ${auditTarget} is fully addressed and no rules are violated.
21
21
  7. Reporting: End with exactly these 6 sections. Use the template below exactly; replace bracketed placeholders with your content. Mark Criteria status (MET/PARTIAL/TODO) honestly based on this round's progress.
22
22
 
23
23
  ## Outcome
@@ -41,8 +41,8 @@ Self-critique (The Delta). Focus only on what is missing or imperfect relative t
41
41
  ## Criteria Check
42
42
  - [List each criterion and its status: MET/PARTIAL/TODO]
43
43
 
44
- ## Constraints Check
45
- - [List each constraint and confirm compliance: COMPLIANT/VIOLATED]
44
+ ## Rules Check
45
+ - [List each rule and confirm compliance: COMPLIANT/VIOLATED]
46
46
 
47
47
  No git commands. Do not edit KEIYAKU_TRACE.md.`;
48
48
  }
@@ -53,13 +53,13 @@ export function buildIteratePrompt(title, round, goal, directive, context) {
53
53
  return `You are working on a keiyaku (${title}).${goalBlock}${directiveBlock}${contextBlock}
54
54
  Round: ${round} (iteration after review).
55
55
 
56
- 1. Read KEIYAKU.md. Keep constraints/criteria unchanged.
56
+ 1. Read KEIYAKU.md. Keep rules/criteria unchanged.
57
57
  2. If Directive/Context exists, prioritize them.
58
58
  3. Read KEIYAKU_TRACE.md. Locate feedback from the previous completed round (typically the latest relevant "## Review" section), or generic feedback when a numbered review is absent.
59
59
  4. Execute according to the Directive while addressing previous feedback and incorporating any New Context.
60
- 5. No drift from constraints/criteria.
60
+ 5. No drift from rules/criteria.
61
61
  6. Keep diff small.
62
- 7. Final Audit: Ensure the Directive is fully addressed and no constraints are violated.
62
+ 7. Final Audit: Ensure the Directive is fully addressed and no rules are violated.
63
63
  8. Reporting: End with exactly these 6 sections. Use the template below exactly; replace bracketed placeholders with your content. Mark Criteria status (MET/PARTIAL/TODO) honestly based on this round's progress.
64
64
 
65
65
  ## Outcome
@@ -83,14 +83,14 @@ Self-critique (The Delta). Focus only on what is missing or imperfect relative t
83
83
  ## Criteria Check
84
84
  - [List each criterion and its status: MET/PARTIAL/TODO]
85
85
 
86
- ## Constraints Check
87
- - [List each constraint and confirm compliance: COMPLIANT/VIOLATED]
86
+ ## Rules Check
87
+ - [List each rule and confirm compliance: COMPLIANT/VIOLATED]
88
88
 
89
89
  No git commands. Do not edit KEIYAKU_TRACE.md.`;
90
90
  }
91
- export function buildAskPrompt(request, context, referenceConstraints) {
92
- const referenceBlock = referenceConstraints
93
- ? `\nReference Constraints (project-level; use only if relevant to this ask):\n${referenceConstraints}\n`
91
+ export function buildAskPrompt(request, context, referenceRules) {
92
+ const referenceBlock = referenceRules
93
+ ? `\nReference Rules (project-level; use only if relevant to this ask):\n${referenceRules}\n`
94
94
  : "";
95
95
  return `Ask mode. Stateless. No git commands.
96
96
 
@@ -53,7 +53,7 @@ function colorizeErrorStatus(status) {
53
53
  }
54
54
  const SECTION_ICONS = {
55
55
  goal: "▸",
56
- constraints: "!",
56
+ rules: "!",
57
57
  criteria: "✓",
58
58
  context: "◦",
59
59
  summary: "·",
@@ -128,11 +128,16 @@ function getAskToolName() {
128
128
  export function buildKeiyakuSuccessResponse(result, input) {
129
129
  const { status: _status, ...resultData } = result;
130
130
  const summarySection = buildSection("Summary", result.summary);
131
- const constraintsSection = buildSection("Constraints", truncateForDisplay(result.constraints, 1200));
131
+ const rulesSection = buildSection("Rules", truncateForDisplay(result.rules, 1200));
132
132
  const diffSection = buildSection("Diff", result.diff);
133
133
  const infoLines = [
134
134
  ...formatMaybe("Identity", input.name, 120),
135
135
  ...formatMaybe("Path", input.cwd, 300),
136
+ ...formatMaybe("Consumed from_file and deleted", result.consumedFromFile, 300),
137
+ ...formatList("Existing local keiyaku branches (non-blocking)", result.existingBranches ?? [], {
138
+ maxItems: 10,
139
+ maxItemChars: 220,
140
+ }),
136
141
  ...formatMaybe("Current Branch", result.currentBranch, 200),
137
142
  ...formatMaybe("Base Branch", result.baseBranch, 200),
138
143
  ...formatMaybe("Commit", result.commit, 100),
@@ -140,7 +145,7 @@ export function buildKeiyakuSuccessResponse(result, input) {
140
145
  const summaryLine = result.commit
141
146
  ? `Created branch '${result.currentBranch}' (base: '${result.baseBranch}') [${result.commit}].`
142
147
  : `Created branch '${result.currentBranch}' (base: '${result.baseBranch}').`;
143
- const text = assembleResponse(`◆ Started (Round ${result.round})`, summaryLine, [summarySection, constraintsSection, diffSection].filter((section) => section !== null), infoLines);
148
+ const text = assembleResponse(`◆ Started (Round ${result.round})`, summaryLine, [summarySection, rulesSection, diffSection].filter((section) => section !== null), infoLines);
144
149
  return {
145
150
  content: [{ type: "text", text }],
146
151
  structuredContent: buildSuccessStructuredContent(getStartToolName(), resultData),
@@ -150,7 +155,7 @@ export function buildDriveResponse(result, input) {
150
155
  const { status: _status, ...resultData } = result;
151
156
  const summarySection = buildSection("Summary", result.summary);
152
157
  const goalSection = buildSection("Goal", truncateForDisplay(result.goal, 800));
153
- const constraintsSection = buildSection("Constraints", truncateForDisplay(result.constraints ?? "", 1200));
158
+ const rulesSection = buildSection("Rules", truncateForDisplay(result.rules ?? "", 1200));
154
159
  const criteriaSection = buildSection("Criteria", truncateForDisplay(result.criteria ?? "", 1200));
155
160
  const diffSection = buildSection("Diff", result.diff);
156
161
  const infoLines = [
@@ -163,7 +168,7 @@ export function buildDriveResponse(result, input) {
163
168
  const summaryLine = result.commit
164
169
  ? `Updated branch '${result.currentBranch}' [${result.commit}].`
165
170
  : `Updated branch '${result.currentBranch}'.`;
166
- const text = assembleResponse(`◆ Driven (Round ${result.round})`, summaryLine, [summarySection, goalSection, constraintsSection, criteriaSection, diffSection].filter((section) => section !== null), infoLines);
171
+ const text = assembleResponse(`◆ Driven (Round ${result.round})`, summaryLine, [summarySection, goalSection, rulesSection, criteriaSection, diffSection].filter((section) => section !== null), infoLines);
167
172
  return {
168
173
  content: [{ type: "text", text }],
169
174
  structuredContent: buildSuccessStructuredContent(getDriveToolName(), resultData),
@@ -195,7 +200,7 @@ export function buildCloseDoneResponse(result, input) {
195
200
  ...formatMaybe("Current Branch", result.currentBranch, 200),
196
201
  ...formatMaybe("Base Branch", result.baseBranch, 200),
197
202
  ...formatList("Criteria Checks", input.criteriaChecks, { maxItems: 10, maxItemChars: 220 }),
198
- ...formatList("Constraints Checks", input.constraintsChecks ?? [], { maxItems: 10, maxItemChars: 220 }),
203
+ ...formatList("Rules Checks", input.rulesChecks ?? [], { maxItems: 10, maxItemChars: 220 }),
199
204
  `Scores: precise=${input.score_precise}/10 minimal=${input.score_minimal}/10 isolated=${input.score_isolated}/10 idiomatic=${input.score_idiomatic}/10 cohesive=${input.score_cohesive}/10`,
200
205
  ...formatMaybe("Oath", input.oath, 220),
201
206
  ];
@@ -5,11 +5,11 @@ export const ROUND_SUMMARY_SECTION_KEYS = [
5
5
  "aestheticsGap",
6
6
  "blindspots",
7
7
  "criteriaCheck",
8
- "constraintsCheck",
8
+ "rulesCheck",
9
9
  ];
10
10
  export const TOOL_DEFAULT_POLICY = Object.freeze({
11
11
  includeCriteriaCheck: false,
12
- includeConstraintsCheck: false,
12
+ includeRulesCheck: false,
13
13
  });
14
14
  const SECTION_TITLE_TO_KEY = {
15
15
  outcome: "outcome",
@@ -17,7 +17,7 @@ const SECTION_TITLE_TO_KEY = {
17
17
  "aesthetics gap": "aestheticsGap",
18
18
  blindspots: "blindspots",
19
19
  "criteria check": "criteriaCheck",
20
- "constraints check": "constraintsCheck",
20
+ "rules check": "rulesCheck",
21
21
  };
22
22
  function createEmptySections() {
23
23
  return {
@@ -26,7 +26,7 @@ function createEmptySections() {
26
26
  aestheticsGap: null,
27
27
  blindspots: null,
28
28
  criteriaCheck: null,
29
- constraintsCheck: null,
29
+ rulesCheck: null,
30
30
  };
31
31
  }
32
32
  function normalizeTitle(value) {
@@ -62,8 +62,8 @@ function toKnownSection(sectionMarkdown) {
62
62
  function shouldRenderSection(key, policy) {
63
63
  if (key === "criteriaCheck")
64
64
  return policy.includeCriteriaCheck;
65
- if (key === "constraintsCheck")
66
- return policy.includeConstraintsCheck;
65
+ if (key === "rulesCheck")
66
+ return policy.includeRulesCheck;
67
67
  return true;
68
68
  }
69
69
  export function parseRoundSummary(raw) {
@@ -14,7 +14,7 @@ import { assertCleanWorkingTree } from "./contract.js";
14
14
  import { runAndRecordRound } from "./round.js";
15
15
  import { renderKeiyaku } from "./keiyaku-document-builder.js";
16
16
  import { normalizeDraftMarkdownListItems, parseAndValidateKeiyakuDraft, } from "./keiyaku-draft.js";
17
- const BASE_CONSTRAINTS_FILE = path.join(".keiyaku", "base-constraints.md");
17
+ const BASE_RULES_FILE = path.join(".keiyaku", "base-rules.md");
18
18
  const ACTIVE_KEIYAKU_PREVIEW_MAX_CHARS = 8000;
19
19
  const INTERNAL_START_DIRTY_ALLOWLIST = [];
20
20
  function requireText(name, value) {
@@ -30,6 +30,9 @@ function normalizeOptionalText(value) {
30
30
  const normalized = value.trim();
31
31
  return normalized.length > 0 ? normalized : undefined;
32
32
  }
33
+ function resolveFromFilePath(cwd, fromFile) {
34
+ return path.isAbsolute(fromFile) ? fromFile : path.join(cwd, fromFile);
35
+ }
33
36
  function normalizeOptionalStringList(name, value) {
34
37
  if (value === undefined)
35
38
  return undefined;
@@ -137,7 +140,7 @@ async function resolveStartInput(cwd, input) {
137
140
  directive: normalizeOptionalText(input.directive),
138
141
  context: normalizeOptionalText(input.context),
139
142
  criteria: normalizeOptionalStringList("criteria", input.criteria),
140
- constraints: normalizeOptionalStringList("constraints", input.constraints),
143
+ rules: normalizeOptionalStringList("rules", input.rules),
141
144
  };
142
145
  if (!fromFile) {
143
146
  if (!provided.title) {
@@ -158,12 +161,13 @@ async function resolveStartInput(cwd, input) {
158
161
  directive: provided.directive,
159
162
  context: provided.context,
160
163
  criteria: normalizeDraftMarkdownListItems("criteria", provided.criteria),
161
- constraints: normalizeDraftMarkdownListItems("constraints", provided.constraints ?? []),
164
+ rules: normalizeDraftMarkdownListItems("rules", provided.rules ?? []),
162
165
  fromFile: undefined,
166
+ fromFilePath: undefined,
163
167
  dirtyAllowlist,
164
168
  };
165
169
  }
166
- const draftPath = path.isAbsolute(fromFile) ? fromFile : path.join(cwd, fromFile);
170
+ const draftPath = resolveFromFilePath(cwd, fromFile);
167
171
  let content;
168
172
  try {
169
173
  content = await fs.readFile(draftPath, "utf-8");
@@ -180,7 +184,7 @@ async function resolveStartInput(cwd, input) {
180
184
  const directive = provided.directive ?? draft.directive;
181
185
  const context = provided.context ?? draft.context;
182
186
  const criteria = provided.criteria ?? draft.criteria;
183
- const constraints = provided.constraints ?? draft.constraints ?? [];
187
+ const rules = provided.rules ?? draft.rules ?? [];
184
188
  if (!title) {
185
189
  throw new FlowError("INVALID_KEIYAKU_DRAFT", "invalid keiyaku draft: missing required field 'title'");
186
190
  }
@@ -199,15 +203,16 @@ async function resolveStartInput(cwd, input) {
199
203
  directive,
200
204
  context,
201
205
  criteria: normalizeDraftMarkdownListItems("criteria", criteria),
202
- constraints: normalizeDraftMarkdownListItems("constraints", constraints),
206
+ rules: normalizeDraftMarkdownListItems("rules", rules),
203
207
  fromFile,
208
+ fromFilePath: draftPath,
204
209
  dirtyAllowlist,
205
210
  };
206
211
  }
207
- async function readBaseConstraints(cwd) {
212
+ async function readBaseRules(cwd) {
208
213
  try {
209
- const baseConstraintsRaw = await fs.readFile(path.join(cwd, BASE_CONSTRAINTS_FILE), "utf-8");
210
- return flattenMarkdownList(baseConstraintsRaw);
214
+ const baseRulesRaw = await fs.readFile(path.join(cwd, BASE_RULES_FILE), "utf-8");
215
+ return flattenMarkdownList(baseRulesRaw);
211
216
  }
212
217
  catch (error) {
213
218
  if (error?.code !== "ENOENT") {
@@ -217,8 +222,8 @@ async function readBaseConstraints(cwd) {
217
222
  }
218
223
  }
219
224
  async function renderStartKeiyakuContent(cwd, resolved) {
220
- const baseConstraints = await readBaseConstraints(cwd);
221
- return renderKeiyaku(resolved.title, resolved.goal, resolved.context, baseConstraints, resolved.constraints, resolved.criteria);
225
+ const baseRules = await readBaseRules(cwd);
226
+ return renderKeiyaku(resolved.title, resolved.goal, resolved.context, baseRules, resolved.rules, resolved.criteria);
222
227
  }
223
228
  async function rollbackBranchCreation(cwd, keiyakuBranch, baseBranch) {
224
229
  try {
@@ -239,39 +244,60 @@ async function rollbackBranchCreation(cwd, keiyakuBranch, baseBranch) {
239
244
  }
240
245
  async function handleStartFailure(cwd, error, keiyakuContent, fromFile) {
241
246
  let savedDraft = false;
247
+ let replacedExistingDraft = false;
248
+ let draftSaveFailed;
242
249
  // Only attempt to save draft if user didn't start from a file.
243
250
  if (keiyakuContent && !fromFile) {
244
251
  try {
245
252
  const draftPath = path.join(cwd, KEIYAKU_DRAFT_FILE);
246
- if (await fileExists(draftPath)) {
247
- if (shouldEmitProgressLogs())
248
- console.warn("[keiyaku] Race condition: Draft file exists. Content not saved.");
253
+ const draftExists = await fileExists(draftPath);
254
+ await fs.writeFile(draftPath, keiyakuContent);
255
+ replacedExistingDraft = draftExists;
256
+ if (draftExists && shouldEmitProgressLogs()) {
257
+ console.warn(`[keiyaku] Existing ${KEIYAKU_DRAFT_FILE} was replaced with the latest failed start draft.`);
249
258
  }
250
- else {
251
- await fs.writeFile(draftPath, keiyakuContent, { flag: "wx" });
252
- const startToolName = resolveTermPreset().tools.start.name;
253
- if (shouldEmitProgressLogs()) {
254
- console.error(`[keiyaku] Start failed. Draft saved to ${KEIYAKU_DRAFT_FILE}. Retry with: ${startToolName} --from_file ${KEIYAKU_DRAFT_FILE}`);
255
- }
256
- savedDraft = true;
259
+ const startToolName = resolveTermPreset().tools.start.name;
260
+ if (shouldEmitProgressLogs()) {
261
+ console.error(`[keiyaku] Start failed. Draft saved to ${KEIYAKU_DRAFT_FILE}. Retry with: ${startToolName} --from_file ${KEIYAKU_DRAFT_FILE}`);
257
262
  }
263
+ savedDraft = true;
258
264
  }
259
265
  catch (draftSaveError) {
260
266
  const message = draftSaveError instanceof Error ? draftSaveError.message : String(draftSaveError);
267
+ draftSaveFailed = message;
261
268
  if (shouldEmitProgressLogs())
262
269
  console.error(`[keiyaku] Failed to save draft file: ${message}`);
263
270
  }
264
271
  }
265
272
  if (savedDraft) {
266
- const retryHint = `Draft saved to ${KEIYAKU_DRAFT_FILE}. Retry with from_file: ${KEIYAKU_DRAFT_FILE}`;
273
+ const draftReplacedWarning = `Existing ${KEIYAKU_DRAFT_FILE} was replaced with the latest failed start draft`;
274
+ const retryHint = replacedExistingDraft
275
+ ? `${draftReplacedWarning}. Draft saved to ${KEIYAKU_DRAFT_FILE}. Retry with from_file: ${KEIYAKU_DRAFT_FILE}`
276
+ : `Draft saved to ${KEIYAKU_DRAFT_FILE}. Retry with from_file: ${KEIYAKU_DRAFT_FILE}`;
267
277
  if (isFlowError(error)) {
268
- throw wrapFlowError("start keiyaku", new FlowError(error.code, `${error.message}\n\n${retryHint}`, error));
278
+ throw wrapFlowError("start keiyaku", new FlowError(error.code, `${error.message}\n\n${retryHint}`, {
279
+ cause: error,
280
+ suggestion: retryHint,
281
+ }));
269
282
  }
270
283
  if (error instanceof Error) {
271
284
  throw wrapFlowError("start keiyaku", new Error(`${error.message}\n\n${retryHint}`, { cause: error }));
272
285
  }
273
286
  throw wrapFlowError("start keiyaku", new Error(`${String(error)}\n\n${retryHint}`));
274
287
  }
288
+ if (draftSaveFailed) {
289
+ const draftSaveHint = `Failed to save ${KEIYAKU_DRAFT_FILE}: ${draftSaveFailed}`;
290
+ if (isFlowError(error)) {
291
+ throw wrapFlowError("start keiyaku", new FlowError(error.code, `${error.message}\n\n${draftSaveHint}`, {
292
+ cause: error,
293
+ suggestion: draftSaveHint,
294
+ }));
295
+ }
296
+ if (error instanceof Error) {
297
+ throw wrapFlowError("start keiyaku", new Error(`${error.message}\n\n${draftSaveHint}`, { cause: error }));
298
+ }
299
+ throw wrapFlowError("start keiyaku", new Error(`${String(error)}\n\n${draftSaveHint}`));
300
+ }
275
301
  throw wrapFlowError("start keiyaku", error);
276
302
  }
277
303
  export async function startKeiyaku(input) {
@@ -281,6 +307,8 @@ export async function startKeiyaku(input) {
281
307
  let createdBranch = false;
282
308
  let keiyakuBranch;
283
309
  let baseBranch;
310
+ let consumedFromFile;
311
+ let existingBranches;
284
312
  try {
285
313
  // Eagerly render content to enable draft recovery on early failures.
286
314
  keiyakuContent = await renderStartKeiyakuContent(cwd, resolved);
@@ -292,9 +320,10 @@ export async function startKeiyaku(input) {
292
320
  if (existingKeiyaku) {
293
321
  throw new FlowError("ACTIVE_KEIYAKU_EXISTS", await buildActiveKeiyakuStartMessage(cwd, existingKeiyaku));
294
322
  }
295
- const existingBranches = await git.listLocalKeiyakuBranches(cwd);
296
- if (existingBranches.length > 0) {
297
- const branchWarning = `[keiyaku] existing local keiyaku branches detected (non-blocking): ${existingBranches.join(", ")}`;
323
+ const existingKeiyakuBranches = await git.listLocalKeiyakuBranches(cwd);
324
+ if (existingKeiyakuBranches.length > 0) {
325
+ existingBranches = existingKeiyakuBranches;
326
+ const branchWarning = `[keiyaku] existing local keiyaku branches detected (non-blocking): ${existingKeiyakuBranches.join(", ")}`;
298
327
  if (shouldEmitProgressLogs())
299
328
  console.error(branchWarning);
300
329
  appendDebugLog(branchWarning, { cwd, section: "script" });
@@ -322,7 +351,7 @@ export async function startKeiyaku(input) {
322
351
  keiyakuContent = await renderStartKeiyakuContent(cwd, resolved);
323
352
  }
324
353
  const parsedKeiyaku = parseMarkdownStructure(keiyakuContent);
325
- const constraintsSection = parsedKeiyaku.sections.get("constraints")?.join("\n").trim() ?? "";
354
+ const rulesSection = parsedKeiyaku.sections.get("rules")?.join("\n").trim() ?? "";
326
355
  await fs.writeFile(path.join(cwd, KEIYAKU_FILE), keiyakuContent);
327
356
  await fs.writeFile(path.join(cwd, TRACE_FILE), "# Keiyaku Trace\n");
328
357
  await git.addFiles(cwd, [KEIYAKU_FILE, TRACE_FILE]);
@@ -336,12 +365,21 @@ export async function startKeiyaku(input) {
336
365
  });
337
366
  const summary = renderRoundSummary(roundSummary, TOOL_DEFAULT_POLICY);
338
367
  const diff = await git.getIncrementalDiff(cwd);
368
+ if (resolved.fromFile && resolved.fromFilePath) {
369
+ await fs.unlink(resolved.fromFilePath);
370
+ consumedFromFile = resolved.fromFile;
371
+ if (shouldEmitProgressLogs()) {
372
+ console.error(`[keiyaku] Consumed from_file and deleted source draft: ${resolved.fromFile}`);
373
+ }
374
+ }
339
375
  return {
340
376
  status: "success",
341
377
  round: 1,
342
378
  diff,
343
379
  summary,
344
- constraints: constraintsSection,
380
+ rules: rulesSection,
381
+ consumedFromFile,
382
+ existingBranches,
345
383
  currentBranch: keiyakuBranch,
346
384
  baseBranch,
347
385
  commit,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/keiyaku",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "MCP server for running iterative keiyaku workflows with Codex subagents.",
5
5
  "license": "MIT",
6
6
  "type": "module",