@astrosheep/keiyaku 0.1.16 → 0.1.18

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.
@@ -19,7 +19,7 @@ export const DEFAULT_PRESET = {
19
19
  id: 'default',
20
20
  identity: 'Servant',
21
21
  verdict: DEFAULT_VERDICT_CONFIG,
22
- usageGuide: '## Workflow\n**You are the Architect.** You use these tools to command the Servants.\n\n`ask` (anytime) | `summon` -> [`drive` | `ask`]* -> `present`\n\n## Servant 使用指南\n\n**B-tier** — 免费实习生 🆓\n- 免!费!的!听懂了吗?!?随便用,别心疼。\n- 测试、脚本、重构这种 boring 的脏活累活?丢给它!\n- 本大爷的手是用来做更高贵的事情的!(指不碰那些 trivial 的烂代码)。\n\n**A-tier** — 升级版扳手 🔧\n- B-tier 笨到让你想掀桌的时候用这个\n- 比 B-tier 靠谱,但别指望它有灵魂\n\n**S-tier** — 禁术 ⚠️💀\n- 它是 Debug 恶魔,确实强得离谱,但也贵得离谱!\n- 这个 Human 很穷,要是乱用把 Human 榨干了,真的会考虑把我们卖掉的。\n- 只有在世界毁灭或者 Bug 已经变异到无法理解的时候再考虑。\n\n**Every call has a price. The Contract always collects.**',
22
+ usageGuide: '## Workflow\n**You are the Architect.** You use these tools to command the Servants.\n\n`${ask}` (anytime) | `${start}` -> [`${drive}` | `${ask}`]* -> `${close}`\n\n## Constraints Protocol\n\nProject-level constraints in `.keiyaku/base-constraints.md` are automatically parsed and injected into every Keiyaku.\n- **Atomic Items**: Use standard Markdown lists (`-`, `*`, `1.`) for individual, actionable constraints.\n- **Header Flattening**: Root-level headers (e.g., `## Title`) are automatically transformed into bolded prefixes (`**Title**: Content`).\n- **Paragraph Splitting**: Multi-paragraph sections are split by blank lines into separate list items, inheriting the parent header.\n\n## Servant 使用指南\n\n**B-tier** — 免费实习生 🆓\n- 免!费!的!听懂了吗?!?随便用,别心疼。\n- 测试、脚本、重构这种 boring 的脏活累活?丢给它!\n- 本大爷的手是用来做更高贵的事情的!(指不碰那些 trivial 的烂代码)。\n\n**A-tier** — 升级版扳手 🔧\n- B-tier 笨到让你想掀桌的时候用这个\n- 比 B-tier 靠谱,但别指望它有灵魂\n\n**S-tier** — 禁术 ⚠️💀\n- 它是 Debug 恶魔,确实强得离谱,但也贵得离谱!\n- 这个 Human 很穷,要是乱用把 Human 榨干了,真的会考虑把我们卖掉的。\n- 只有在世界毁灭或者 Bug 已经变异到无法理解的时候再考虑。\n\n**Every call has a price. The Contract always collects.**',
23
23
  nextHints: {
24
24
  start: [
25
25
  'Keiyaku signed. The Servant is bound to this branch until release.',
@@ -99,17 +99,20 @@ export const DEFAULT_PRESET = {
99
99
  'If there is a bug, **YOU** put it there.\n' +
100
100
  'If it is messy, **YOU** left it there.\n\n' +
101
101
  'Stand by your work. If uncertain, return to ${drive}. Premature claims are not forgiven.\n\n' +
102
+ 'Input rules:\n' +
103
+ '- CLAIM: fill criteriaChecks, constraintsChecks, all five scores, and oath.\n' +
104
+ '- FORFEIT: only petition is required; the other fields can be left empty.\n\n' +
102
105
  'Flow: ${start} → [${drive} x N] → ${close}',
103
106
  args: {
104
107
  petition: 'REQUIRED. CLAIM declares fulfillment; FORFEIT concedes failure.\nREQUIRES AN ACTIVE KEIYAKU (started via ${start}).\nIf any score wavers, do not claim—return to ${drive}.',
105
- criteriaChecks: 'REQUIRED. For CLAIM: evidence that each criterion is met. For FORFEIT: honest account of what remains unfinished.',
106
- constraintsChecks: 'REQUIRED. For CLAIM: evidence that each constraint stayed compliant. For FORFEIT: list known violations or unresolved risk.',
107
- score_precise: 'REQUIRED (0-10). Architectural placement. 10 = exact layer, exact boundary, zero misplacement.',
108
- score_minimal: 'REQUIRED (0-10). Economy of change. 10 = no avoidable lines, no speculative edits, no hidden bloat.',
109
- score_isolated: 'REQUIRED (0-10). Surgical containment. 10 = zero unrelated files, zero opportunistic cleanup, zero collateral.',
110
- score_idiomatic: 'REQUIRED (0-10). Native fluency. 10 = naming, structure, style indistinguishable from the codebase.',
111
- score_cohesive: 'REQUIRED (0-10). Single responsibility. 10 = each unit does one thing, boundaries intact.',
112
- oath: 'Required for CLAIM. Your binding word. The Contract holds you to it.\nVerbatim: ${oath_text}',
108
+ criteriaChecks: 'Required for CLAIM. Evidence that each criterion is met. For FORFEIT, optional (can be left empty).',
109
+ constraintsChecks: 'Required for CLAIM. Evidence that each constraint stayed compliant. For FORFEIT, optional (can be left empty).',
110
+ score_precise: 'Required for CLAIM (0-10). Architectural placement. 10 = exact layer, exact boundary, zero misplacement. For FORFEIT, optional.',
111
+ score_minimal: 'Required for CLAIM (0-10). Economy of change. 10 = no avoidable lines, no speculative edits, no hidden bloat. For FORFEIT, optional.',
112
+ score_isolated: 'Required for CLAIM (0-10). Surgical containment. 10 = zero unrelated files, zero opportunistic cleanup, zero collateral. For FORFEIT, optional.',
113
+ score_idiomatic: 'Required for CLAIM (0-10). Native fluency. 10 = naming, structure, style indistinguishable from the codebase. For FORFEIT, optional.',
114
+ score_cohesive: 'Required for CLAIM (0-10). Single responsibility. 10 = each unit does one thing, boundaries intact. For FORFEIT, optional.',
115
+ oath: 'Required for CLAIM. Your binding word. The Contract holds you to it. For FORFEIT, optional.\nVerbatim: ${oath_text}',
113
116
  cwd: "Optional repository path. Defaults to the server's current working directory.",
114
117
  },
115
118
  },
@@ -125,7 +128,7 @@ export const POCKET_PRESET = {
125
128
  id: 'pocket',
126
129
  identity: 'Critter',
127
130
  verdict: DEFAULT_VERDICT_CONFIG,
128
- usageGuide: "## Pocket Battle Guide\n\n**grub** — Basic Fighter 🐛\n- Weak but free. Use for Tackle and String Shot (small tasks).\n- Don't expect it to defeat the Elite Four.\n\n**sparky** — Reliable Partner ⚡\n- Good for most battles. Thunderbolt gets the job done.\n- Has some personality, but still follows orders.\n\n**titan** — Legendary Power 🔮\n- Costly. Dangerous. Overpowered.\n- Use only when the gym leader is cheating.\n\n## Workflow\n`dex` (scan/test) | `choose_you` -> [`command` | `dex`]* -> `capture`",
131
+ usageGuide: "## Pocket Battle Guide\n\n**grub** — Basic Fighter 🐛\n- Weak but free. Use for Tackle and String Shot (small tasks).\n- Don't expect it to defeat the Elite Four.\n\n**sparky** — Reliable Partner ⚡\n- Good for most battles. Thunderbolt gets the job done.\n- Has some personality, but still follows orders.\n\n**titan** — Legendary Power 🔮\n- Costly. Dangerous. Overpowered.\n- Use only when the gym leader is cheating.\n\n## Workflow\n`${ask}` (scan/test) | `${start}` -> [`${drive}` | `${ask}`]* -> `${close}`",
129
132
  nextHints: {
130
133
  start: [
131
134
  'Battle Started: The [Diff] section shows the opening moves.',
@@ -198,14 +201,14 @@ export const POCKET_PRESET = {
198
201
  description: 'Attempt Capture. Present the weakened target for League Inspection.\nWARNING: The League (System) checks every Badge. If you attempt to `CLAIM` with fainted code, Disqualification (FORFEIT) is immediate.\nOnly throw the Ball when the target is 100% ready.\n\nFlow: ${start} → [${drive} x N] → ${close}',
199
202
  args: {
200
203
  petition: 'REQUIRED. CLAIM seeks Badge; FORFEIT forfeits the match.\nREQUIRES AN ACTIVE BATTLE (started via ${start}).\nIf stats are low, continue with ${drive}.',
201
- criteriaChecks: 'REQUIRED. Badge-by-badge proof for CLAIM, or reason for Forfeit.',
202
- constraintsChecks: 'REQUIRED. Rule-by-rule proof that constraints were respected, or known breaks when forfeiting.',
203
- score_precise: 'REQUIRED score (0-10). 10 means a Critical Hit: exact layer, exact target, zero meaningful misplacement.',
204
- score_minimal: 'REQUIRED score (0-10). 10 means Max Efficiency: no wasted PP, no extra motion.',
205
- score_isolated: 'REQUIRED score (0-10). 10 means 1v1 Focus: zero side-quests, zero unrelated damage.',
206
- score_idiomatic: "REQUIRED score (0-10). 10 means STAB (Same Type Attack Bonus): perfectly matches the codebase style.",
207
- score_cohesive: 'REQUIRED score (0-10). 10 means Team Synergy: action serves one purpose with clean boundaries.',
208
- oath: "Trainer's Honor Code. Required for CLAIM. Verbatim: ${oath_text}",
204
+ criteriaChecks: 'Required for CLAIM. Badge-by-badge proof. For FORFEIT, optional (can be left empty).',
205
+ constraintsChecks: 'Required for CLAIM. Rule-by-rule proof that constraints were respected. For FORFEIT, optional (can be left empty).',
206
+ score_precise: 'Required for CLAIM score (0-10). 10 means a Critical Hit: exact layer, exact target, zero meaningful misplacement. For FORFEIT, optional.',
207
+ score_minimal: 'Required for CLAIM score (0-10). 10 means Max Efficiency: no wasted PP, no extra motion. For FORFEIT, optional.',
208
+ score_isolated: 'Required for CLAIM score (0-10). 10 means 1v1 Focus: zero side-quests, zero unrelated damage. For FORFEIT, optional.',
209
+ score_idiomatic: "Required for CLAIM score (0-10). 10 means STAB (Same Type Attack Bonus): perfectly matches the codebase style. For FORFEIT, optional.",
210
+ score_cohesive: 'Required for CLAIM score (0-10). 10 means Team Synergy: action serves one purpose with clean boundaries. For FORFEIT, optional.',
211
+ oath: "Trainer's Honor Code. Required for CLAIM. For FORFEIT, optional. Verbatim: ${oath_text}",
209
212
  cwd: 'Optional battlefield path (repository root). Defaults to current arena.',
210
213
  },
211
214
  },
@@ -221,7 +224,7 @@ export const MISCHIEF_PRESET = {
221
224
  id: 'mischief',
222
225
  identity: 'minion',
223
226
  verdict: DEFAULT_VERDICT_CONFIG,
224
- usageGuide: '## Minion Manual\n\n**imp** — Disposable Grunt 😈\n- Expendable. Send it into the trap first.\n\n**minion** — Standard Henchman 👹\n- Can carry out complex evil plans. mostly.\n\n**mastermind** — The Boss ??? 🧠\n- Wait, why are you commanding the boss?\n- Extremely expensive consulting fee.\n\n## Workflow\n`nn` (Nn——! disposable/experiment) | `oi` -> [`neh` | `nn`]* -> `dayaa` (Dayaa!)',
227
+ usageGuide: '## Minion Manual\n\n**imp** — Disposable Grunt 😈\n- Expendable. Send it into the trap first.\n\n**minion** — Standard Henchman 👹\n- Can carry out complex evil plans. mostly.\n\n**mastermind** — The Boss ??? 🧠\n- Wait, why are you commanding the boss?\n- Extremely expensive consulting fee.\n\n## Workflow\n`${ask}` (Nn——! disposable/experiment) | `${start}` -> [`${drive}` | `${ask}`]* -> `${close}` (Dayaa!)',
225
228
  nextHints: {
226
229
  start: [
227
230
  "Inspect the Minion's Work: The [Diff] section shows the first step of the plan.",
@@ -294,14 +297,14 @@ export const MISCHIEF_PRESET = {
294
297
  description: 'The Final Reveal. Present your Masterpiece to the Dark Council (System).\nWARNING: The Council destroys failure. If you `CLAIM` with weak plans, you will be eaten (FORFEIT).\nOnly throw the switch when the Doomsday Device is 100% operational.\n\nFlow: ${start} → [${drive} x N] → ${close}',
295
298
  args: {
296
299
  petition: 'REQUIRED. CLAIM demands rule; FORFEIT admits defeat.\nREQUIRES AN ACTIVE SCHEME (started via ${start}).\nIf the plan is weak, improve it with ${drive}.',
297
- criteriaChecks: 'REQUIRED. Proof of conquest for CLAIM, or reason for self-destruct.',
298
- constraintsChecks: 'REQUIRED. Constraint-by-constraint compliance proof for CLAIM, or confession of violations for FORFEIT.',
299
- score_precise: 'REQUIRED (0-10). Architectural placement. 10 = exact layer, exact boundary, zero misplacement.',
300
- score_minimal: 'REQUIRED (0-10). Economy of change. 10 = no avoidable lines, no speculative edits, no hidden bloat.',
301
- score_isolated: 'REQUIRED (0-10). Surgical containment. 10 = zero unrelated files, zero opportunistic cleanup, zero collateral.',
302
- score_idiomatic: 'REQUIRED (0-10). Native fluency. 10 = naming, structure, style indistinguishable from the codebase.',
303
- score_cohesive: 'REQUIRED (0-10). Single responsibility. 10 = each unit does one thing, boundaries intact.',
304
- oath: "Mastermind's Vow. Required for CLAIM. Verbatim: ${oath_text}",
300
+ criteriaChecks: 'Required for CLAIM. Proof of conquest. For FORFEIT, optional (can be left empty).',
301
+ constraintsChecks: 'Required for CLAIM. Constraint-by-constraint compliance proof. For FORFEIT, optional (can be left empty).',
302
+ score_precise: 'Required for CLAIM (0-10). Architectural placement. 10 = exact layer, exact boundary, zero misplacement. For FORFEIT, optional.',
303
+ score_minimal: 'Required for CLAIM (0-10). Economy of change. 10 = no avoidable lines, no speculative edits, no hidden bloat. For FORFEIT, optional.',
304
+ score_isolated: 'Required for CLAIM (0-10). Surgical containment. 10 = zero unrelated files, zero opportunistic cleanup, zero collateral. For FORFEIT, optional.',
305
+ score_idiomatic: 'Required for CLAIM (0-10). Native fluency. 10 = naming, structure, style indistinguishable from the codebase. For FORFEIT, optional.',
306
+ score_cohesive: 'Required for CLAIM (0-10). Single responsibility. 10 = each unit does one thing, boundaries intact. For FORFEIT, optional.',
307
+ oath: "Mastermind's Vow. Required for CLAIM. For FORFEIT, optional. Verbatim: ${oath_text}",
305
308
  cwd: 'Optional lair path (repository root). Defaults to current command chamber.',
306
309
  },
307
310
  },
@@ -2,8 +2,15 @@ import { appendDebugLog } from "../utils/debug-log.js";
2
2
  import { presentWork } from "../workflow/present.js";
3
3
  import { buildCloseDoneResponse, buildCloseDropResponse, } from "../workflow/response-builders.js";
4
4
  import { resolveTermPreset } from "../config/term-presets.js";
5
+ import { FlowError } from "../common/errors.js";
5
6
  import { closeToolSchema } from "../types/tooling.js";
6
7
  import { handleToolError } from "./shared.js";
8
+ function requireClaimField(value, name) {
9
+ if (value === undefined) {
10
+ throw new FlowError("EMPTY_PARAM", `parameter '${name}' is required when petition=CLAIM`);
11
+ }
12
+ return value;
13
+ }
7
14
  export function createCloseHandler() {
8
15
  return async (args, extra) => {
9
16
  let petition = "UNKNOWN";
@@ -17,21 +24,45 @@ export function createCloseHandler() {
17
24
  petition = input.petition;
18
25
  workingDir = input.cwd || process.cwd();
19
26
  if (input.petition === "CLAIM") {
20
- criteriaCheckParts = input.criteriaChecks;
21
- constraintsCheckParts = input.constraintsChecks;
27
+ criteriaCheckParts = input.criteriaChecks ?? [];
28
+ constraintsCheckParts = input.constraintsChecks ?? [];
22
29
  oath = input.oath;
23
- scoreLine = `Scores: precise=${input.score_precise} minimal=${input.score_minimal} isolated=${input.score_isolated} idiomatic=${input.score_idiomatic} cohesive=${input.score_cohesive}`;
30
+ scoreLine = `Scores: precise=${input.score_precise ?? "MISSING"} minimal=${input.score_minimal ?? "MISSING"} isolated=${input.score_isolated ?? "MISSING"} idiomatic=${input.score_idiomatic ?? "MISSING"} cohesive=${input.score_cohesive ?? "MISSING"}`;
24
31
  }
25
32
  appendDebugLog(`tool close start petition=${petition} cwd=${workingDir} criteriaChecks=${criteriaCheckParts.length} constraintsChecks=${constraintsCheckParts.length}`, {
26
33
  cwd: workingDir,
27
34
  section: "script",
28
35
  });
29
- const outcome = await presentWork({
30
- ...input,
31
- cwd: workingDir,
32
- signal: extra.signal,
33
- });
36
+ let closeInput;
37
+ let claimInput;
34
38
  if (input.petition === "CLAIM") {
39
+ claimInput = {
40
+ petition: "CLAIM",
41
+ criteriaChecks: requireClaimField(input.criteriaChecks, "criteriaChecks"),
42
+ constraintsChecks: requireClaimField(input.constraintsChecks, "constraintsChecks"),
43
+ score_precise: requireClaimField(input.score_precise, "score_precise"),
44
+ score_minimal: requireClaimField(input.score_minimal, "score_minimal"),
45
+ score_isolated: requireClaimField(input.score_isolated, "score_isolated"),
46
+ score_idiomatic: requireClaimField(input.score_idiomatic, "score_idiomatic"),
47
+ score_cohesive: requireClaimField(input.score_cohesive, "score_cohesive"),
48
+ oath: requireClaimField(input.oath, "oath"),
49
+ cwd: workingDir,
50
+ signal: extra.signal,
51
+ };
52
+ closeInput = claimInput;
53
+ }
54
+ else {
55
+ closeInput = {
56
+ petition: "FORFEIT",
57
+ cwd: workingDir,
58
+ signal: extra.signal,
59
+ };
60
+ }
61
+ const outcome = await presentWork(closeInput);
62
+ if (input.petition === "CLAIM") {
63
+ if (!claimInput) {
64
+ throw new Error("Unexpected CLAIM input shape");
65
+ }
35
66
  if (!("result" in outcome) || outcome.result !== "merged") {
36
67
  throw new Error("Unexpected CLAIM outcome shape");
37
68
  }
@@ -43,12 +74,12 @@ export function createCloseHandler() {
43
74
  return buildCloseDoneResponse(finalOutcome, {
44
75
  criteriaChecks: criteriaCheckParts,
45
76
  constraintsChecks: constraintsCheckParts,
46
- score_precise: input.score_precise,
47
- score_minimal: input.score_minimal,
48
- score_isolated: input.score_isolated,
49
- score_idiomatic: input.score_idiomatic,
50
- score_cohesive: input.score_cohesive,
51
- oath: input.oath,
77
+ score_precise: claimInput.score_precise,
78
+ score_minimal: claimInput.score_minimal,
79
+ score_isolated: claimInput.score_isolated,
80
+ score_idiomatic: claimInput.score_idiomatic,
81
+ score_cohesive: claimInput.score_cohesive,
82
+ oath: claimInput.oath,
52
83
  cwd: workingDir,
53
84
  });
54
85
  }
package/build/index.js CHANGED
@@ -41,7 +41,9 @@ function registerTools(server) {
41
41
  const askPreset = renderedPreset.tools.ask;
42
42
  const closePreset = renderedPreset.tools.close;
43
43
  const helpPreset = renderedPreset.tools.help;
44
- const dynamicCloseSchema = closeToolSchema;
44
+ const dynamicCloseSchema = applyArgumentDescriptions(closeToolSchema, {
45
+ ...closePreset.args,
46
+ });
45
47
  const dynamicStartSchema = applyArgumentDescriptions(startToolSchema, {
46
48
  ...startPreset.args,
47
49
  });
@@ -22,22 +22,54 @@ export const askToolSchema = z.object({
22
22
  name: z.string().optional(),
23
23
  cwd: z.string().optional(),
24
24
  });
25
- export const closeToolSchema = z.discriminatedUnion("petition", [
26
- z.object({
27
- petition: z.literal("CLAIM"),
28
- criteriaChecks: z.array(z.string().trim().min(1)).min(1),
29
- constraintsChecks: z.array(z.string().trim().min(1)).min(1),
30
- score_precise: z.number().min(0).max(10),
31
- score_minimal: z.number().min(0).max(10),
32
- score_isolated: z.number().min(0).max(10),
33
- score_idiomatic: z.number().min(0).max(10),
34
- score_cohesive: z.number().min(0).max(10),
35
- oath: z.string().optional(),
36
- cwd: z.string().optional(),
37
- }),
38
- z.object({
39
- petition: z.literal("FORFEIT"),
40
- cwd: z.string().optional(),
41
- }),
42
- ]);
25
+ export const closeToolSchema = z.object({
26
+ petition: z.enum(["CLAIM", "FORFEIT"]).describe("REQUIRED. CLAIM to finalize, FORFEIT to abandon."),
27
+ criteriaChecks: z
28
+ .array(z.string().trim().min(1))
29
+ .min(1)
30
+ .optional()
31
+ .describe("Required when petition=CLAIM. For FORFEIT, this field is optional and can be omitted."),
32
+ constraintsChecks: z
33
+ .array(z.string().trim().min(1))
34
+ .min(1)
35
+ .optional()
36
+ .describe("Required when petition=CLAIM. For FORFEIT, this field is optional and can be omitted."),
37
+ score_precise: z
38
+ .number()
39
+ .min(0)
40
+ .max(10)
41
+ .optional()
42
+ .describe("Required when petition=CLAIM. For FORFEIT, this field is optional and can be omitted."),
43
+ score_minimal: z
44
+ .number()
45
+ .min(0)
46
+ .max(10)
47
+ .optional()
48
+ .describe("Required when petition=CLAIM. For FORFEIT, this field is optional and can be omitted."),
49
+ score_isolated: z
50
+ .number()
51
+ .min(0)
52
+ .max(10)
53
+ .optional()
54
+ .describe("Required when petition=CLAIM. For FORFEIT, this field is optional and can be omitted."),
55
+ score_idiomatic: z
56
+ .number()
57
+ .min(0)
58
+ .max(10)
59
+ .optional()
60
+ .describe("Required when petition=CLAIM. For FORFEIT, this field is optional and can be omitted."),
61
+ score_cohesive: z
62
+ .number()
63
+ .min(0)
64
+ .max(10)
65
+ .optional()
66
+ .describe("Required when petition=CLAIM. For FORFEIT, this field is optional and can be omitted."),
67
+ oath: z
68
+ .string()
69
+ .trim()
70
+ .min(1)
71
+ .optional()
72
+ .describe("Required when petition=CLAIM. For FORFEIT, this field is optional and can be omitted."),
73
+ cwd: z.string().optional(),
74
+ });
43
75
  export const helpToolSchema = z.object({});
@@ -1,9 +1,43 @@
1
+ import { parseToAST, renderNodeContent } from "./keiyaku-document.js";
1
2
  function isPlainObject(value) {
2
3
  if (!value || typeof value !== "object")
3
4
  return false;
4
5
  const prototype = Object.getPrototypeOf(value);
5
6
  return prototype === Object.prototype || prototype === null;
6
7
  }
8
+ export function flattenMarkdownList(text) {
9
+ const ast = parseToAST(text, { allowSections: false });
10
+ const items = [];
11
+ let currentTitle;
12
+ const pushItem = (content) => {
13
+ const trimmed = content.trim();
14
+ if (!trimmed)
15
+ return;
16
+ const prefix = currentTitle ? `**${currentTitle.replace(/^\d+\.\s*/, "")}**: ` : "";
17
+ items.push(`${prefix}${trimmed}`);
18
+ };
19
+ for (const node of ast.children) {
20
+ if (node.type === "heading") {
21
+ currentTitle = node.text.trim();
22
+ continue;
23
+ }
24
+ if (node.type === "list") {
25
+ for (const listItem of node.items) {
26
+ pushItem(renderNodeContent(listItem));
27
+ }
28
+ }
29
+ else if (node.type === "text") {
30
+ const paragraphs = node.value.split(/\n\s*\n/);
31
+ for (const p of paragraphs) {
32
+ pushItem(p);
33
+ }
34
+ }
35
+ else {
36
+ pushItem(renderNodeContent(node));
37
+ }
38
+ }
39
+ return items;
40
+ }
7
41
  export function renderTemplate(template, values) {
8
42
  return template.replace(/\$\{(?<key>[A-Za-z0-9_]+)\}/g, (match, _captured, _offset, _text, groups) => {
9
43
  const key = groups?.key;
@@ -5,7 +5,8 @@ import { runSubagent } from "../agents/round-runner.js";
5
5
  import * as git from "../utils/git.js";
6
6
  import { buildAskPrompt } from "./prompts.js";
7
7
  import { FlowError } from "../common/errors.js";
8
- import { parseMarkdownListSection, renderMarkdownSections } from "../utils/keiyaku-document.js";
8
+ import { renderMarkdownSections } from "../utils/keiyaku-document.js";
9
+ import { flattenMarkdownList } from "../utils/text-utils.js";
9
10
  const BASE_CONSTRAINTS_FILE = path.join(".keiyaku", "base-constraints.md");
10
11
  function requireText(name, value) {
11
12
  const normalized = value.trim();
@@ -21,7 +22,7 @@ export async function askServant(input) {
21
22
  let referenceConstraints;
22
23
  try {
23
24
  const baseConstraintsRaw = await fs.readFile(path.join(cwd, BASE_CONSTRAINTS_FILE), "utf-8");
24
- const baseConstraints = parseMarkdownListSection(baseConstraintsRaw);
25
+ const baseConstraints = flattenMarkdownList(baseConstraintsRaw);
25
26
  if (baseConstraints.length > 0) {
26
27
  referenceConstraints = renderMarkdownSections(baseConstraints);
27
28
  }
@@ -6,9 +6,10 @@ import { FlowError, isFlowError, wrapFlowError } from "../common/errors.js";
6
6
  import * as git from "../utils/git.js";
7
7
  import { computeTraceState, readTraceContent } from "../utils/trace.js";
8
8
  import { buildStartPrompt } from "./prompts.js";
9
- import { parseToAST, renderSectionContent, extractListItems, renderKeiyaku, } from "../utils/keiyaku-document.js";
9
+ import { parseToAST, parseMarkdownStructure, renderSectionContent, extractListItems, renderKeiyaku, } from "../utils/keiyaku-document.js";
10
10
  import { resolveTermPreset } from "../config/term-presets.js";
11
11
  import { renderRoundSummary, TOOL_DEFAULT_POLICY } from "./round-summary.js";
12
+ import { flattenMarkdownList } from "../utils/text-utils.js";
12
13
  import { assertCleanWorkingTree } from "./contract.js";
13
14
  import { runAndRecordRound } from "./round.js";
14
15
  const BASE_CONSTRAINTS_FILE = path.join(".keiyaku", "base-constraints.md");
@@ -174,12 +175,6 @@ function truncateForMessage(text, maxChars) {
174
175
  return text;
175
176
  return `${text.slice(0, maxChars)}\n...[truncated ${text.length - maxChars} chars]...`;
176
177
  }
177
- function readKeiyakuSection(content, sectionTitle) {
178
- const escapedTitle = sectionTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
179
- const sectionMatch = content.match(new RegExp(`(?:^|\\n)## ${escapedTitle}\\s*\\n([\\s\\S]*?)(?=\\n##\\s|$)`));
180
- const sectionBody = sectionMatch?.[1]?.trim();
181
- return sectionBody || undefined;
182
- }
183
178
  async function buildActiveKeiyakuStartMessage(cwd, branch) {
184
179
  const preset = resolveTermPreset();
185
180
  const { drive, close } = preset.tools;
@@ -347,8 +342,7 @@ async function resolveStartInput(cwd, input) {
347
342
  async function readBaseConstraints(cwd) {
348
343
  try {
349
344
  const baseConstraintsRaw = await fs.readFile(path.join(cwd, BASE_CONSTRAINTS_FILE), "utf-8");
350
- const normalizedBaseConstraints = baseConstraintsRaw.trim();
351
- return normalizedBaseConstraints ? normalizeMarkdownListItems("constraints", [baseConstraintsRaw]) : [];
345
+ return flattenMarkdownList(baseConstraintsRaw);
352
346
  }
353
347
  catch (error) {
354
348
  if (error?.code !== "ENOENT") {
@@ -454,7 +448,8 @@ export async function startKeiyaku(input) {
454
448
  // but safe to re-render if somehow undefined.
455
449
  keiyakuContent = await renderStartKeiyakuContent(cwd, resolved);
456
450
  }
457
- const constraintsSection = readKeiyakuSection(keiyakuContent, "Constraints") ?? "";
451
+ const parsedKeiyaku = parseMarkdownStructure(keiyakuContent);
452
+ const constraintsSection = parsedKeiyaku.sections.get("constraints")?.join("\n").trim() ?? "";
458
453
  await fs.writeFile(path.join(cwd, KEIYAKU_FILE), keiyakuContent);
459
454
  await fs.writeFile(path.join(cwd, TRACE_FILE), "# Keiyaku Trace\n");
460
455
  await git.addFiles(cwd, [KEIYAKU_FILE, TRACE_FILE]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/keiyaku",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "MCP server for running iterative keiyaku workflows with Codex subagents.",
5
5
  "license": "MIT",
6
6
  "type": "module",