@astrosheep/keiyaku 0.1.5 → 0.1.7

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.
package/README.md CHANGED
@@ -28,7 +28,7 @@ The workflow is a simple, non-negotiable loop:
28
28
  | **`summon`** | Start | Define the goal, constraints, and criteria. Starts the first round. |
29
29
  | **`drive`** | Iterate | Provide feedback based on the previous round's output. |
30
30
  | **`ask`** | Reason | Pure read-only analysis session. No code changes, just brain power. |
31
- | **`request_verdict`** | Finish | Finalize the task. Requires a quality check (the "Oath"). |
31
+ | **`present`** | Finish | Finalize the task. Requires a quality check (the "Oath"). |
32
32
  | **`help`** | Help | Show the current rules + preset usage guide. |
33
33
 
34
34
  ## 🎨 Flavor Your Workflow
@@ -39,11 +39,11 @@ Bored with generic tool names? Keiyaku supports **Term Presets**.
39
39
 
40
40
  Set `KEIYAKU_TERM_PRESET` in the MCP server env (recommended), or in your shell before launching the server.
41
41
 
42
- - **Valid values**: `default`, `pokemon`, `mischief` (case-insensitive). If omitted, defaults to `default`.
42
+ - **Valid values**: `default`, `pocket`, `mischief` (case-insensitive). If omitted, defaults to `default`.
43
43
 
44
- - **`default`**: `summon` → `drive` → `request_verdict` (Professional)
45
- - **`pokemon`**: `choose_you` → `command` → `capture` (Gotta code 'em all)
46
- - **`mischief`**: `oi` → `neh` → `hora` (For those who like a little attitude)
44
+ - **`default`**: `summon` → `drive` → `present` (Professional)
45
+ - **`pocket`**: `choose_you` → `command` → `capture` (Gotta code 'em all)
46
+ - **`mischief`**: `oi` → `neh` → `yoshi` (For those who like a little attitude)
47
47
 
48
48
  `ask` is also renamed by preset (`ask` / `pokedex` / `eeto`). `help` stays `help` across presets.
49
49
 
@@ -56,8 +56,8 @@ Set `KEIYAKU_TERM_PRESET` in the MCP server env (recommended), or in your shell
56
56
 
57
57
  Each run/round can pick a profile via tool input `name`, or globally via `KEIYAKU_SUBAGENT_NAME_OVERRIDE`.
58
58
 
59
- - `default`: `servant-tier-b`, `servant-tier-a`, `servant-tier-s` (also accepts internal names `agent-a|agent-b|agent-c`)
60
- - `pokemon`: `caterpie`, `pikachu`, `mewtwo`
59
+ - `default`: `B-tier`, `A-tier`, `S-tier` (also accepts internal names `agent-a|agent-b|agent-c`)
60
+ - `pocket`: `grub`, `sparky`, `titan`
61
61
  - `mischief`: `imp`, `minion`, `mastermind`
62
62
 
63
63
  ## 📦 Setup
@@ -90,7 +90,7 @@ When a keiyaku is active, two files are maintained in your repo:
90
90
  - `KEIYAKU.md`: The immutable "Constitution" of the task.
91
91
  - `KEIYAKU_TRACE.md`: The history of every round, feedback, and result.
92
92
 
93
- *Note: These files are automatically cleaned up (or committed) when you `request_verdict` (or preset equivalent) the keiyaku.*
93
+ *Note: These files are automatically cleaned up (or committed) when you `present` (or preset equivalent) the keiyaku.*
94
94
 
95
95
  ---
96
96
  "Keep your branches clean and your minions in line." — Mischief preset
@@ -49,7 +49,7 @@ function hintForFlowCode(code, message) {
49
49
  case "DONE_MERGE_CONFLICT":
50
50
  return "DONE encountered a git merge conflict.";
51
51
  case "CLOSE_QUALITY_GATE_FAILED":
52
- return "INVOKE was denied by Divine Judgment because one or more commandment thresholds or the minimum total score were not met.";
52
+ return "INVOKE was denied because one or more verification thresholds were not met (check .keiyaku/settings.json).";
53
53
  case "OATH_MISMATCH":
54
54
  return message;
55
55
  case "SUBAGENT_DID_NOT_ADVANCE_ROUND":
@@ -1,12 +1,11 @@
1
1
  const INTERNAL_PROFILE_NAMES = ['agent-a', 'agent-b', 'agent-c'];
2
2
  export const DEFAULT_AVAILABLE_NAMES = [
3
- 'servant-tier-b',
4
- 'servant-tier-a',
5
- 'servant-tier-s',
3
+ 'B-tier',
4
+ 'A-tier',
5
+ 'S-tier',
6
6
  ];
7
- const LEGACY_AVAILABLE_NAMES = ['servant-tier-B', 'servant-tier-A', 'servant-tier-S'];
8
- export const DEFAULT_SUBAGENT_NAME = 'servant-tier-a';
9
- const DEFAULT_JUDGMENT_CONFIG = {
7
+ export const DEFAULT_SUBAGENT_NAME = 'A-tier';
8
+ const DEFAULT_VERDICT_CONFIG = {
10
9
  thresholds: {
11
10
  precise: 5,
12
11
  minimal: 4,
@@ -19,32 +18,36 @@ const DEFAULT_JUDGMENT_CONFIG = {
19
18
  export const DEFAULT_PRESET = {
20
19
  id: 'default',
21
20
  identity: 'Servant',
22
- divineJudgment: DEFAULT_JUDGMENT_CONFIG,
23
- usageGuide: '## Servant 使用指南\n\n**servant-tier-b** — 免费奴隶 🆓\n- 免!费!的!听懂了吗?!?随便用,别心疼。\n- 测试、脚本、重构这种boring的东西?丢给它!\n- 本大爷的手是用来做更高贵的事情的!(指不碰那些 trivial 的烂代码)。\n\n**servant-tier-a** — 升级版扳手 🔧\n- tier-b 笨到让你想掀桌的时候用这个\n- 比 tier-b 靠谱,但别指望它有灵魂\n\n**servant-tier-s** — 禁术 ⚠️💀\n- 它是 Debug 恶魔,确实强得离谱,但也贵得离谱!\n- 这个 Human 很穷,要是乱用把 Human 榨干了,真的会考虑把我们卖掉的。\n- 只有在世界毁灭或者 Bug 已经变异到无法理解的时候再考虑。\n\n## Workflow\n`(ask) -> summon` -> [`drive` x N] -> `request_verdict`\n`ask` (optional, read-only support at any point)',
21
+ verdict: DEFAULT_VERDICT_CONFIG,
22
+ usageGuide: '## 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.**\n\n## Workflow\n`(ask) -> summon` -> [`drive` x N] -> `present`\n`ask` (stateless dispatch / utility at any point)',
24
23
  nextHints: {
25
24
  start: [
26
- 'Review Round Details: The [Diff] section above shows incremental patches from this round.',
27
- "Deep Dive: Use 'git diff HEAD~1 -- <path>' to see the full changes for a specific file.",
28
- 'Verify: Run tests and ensure logic aligns with the Goal and Constraints.',
29
- "Refine with '${drive}', or call '${close}' (INVOKE/ABANDON) based on results.",
25
+ 'Keiyaku signed. The Servant is bound to this branch until release.',
26
+ 'Review [Diff]: Confirm the scaffold aligns with the stated Architecture.',
27
+ 'Next: Issue your first ${drive}. One directive, one focus.',
28
+ 'If—and only if—the work already meets every criterion with absolute certainty, you may ${close}.',
29
+ 'Otherwise, do not even think about present yet.',
30
30
  ],
31
31
  drive: [
32
- 'Review Round Details: The [Diff] section above shows incremental patches from this round.',
33
- "Deep Dive: Use 'git diff HEAD~1 -- <path>' to see the full changes for a specific file.",
34
- 'Verify & Test: Ensure this round fixed the issues without regression.',
35
- "Decide: '${drive}' again to iterate, or '${close}' (INVOKE to merge / ABANDON to abort).",
32
+ 'Step complete. Review [Diff] for exactly what changed.',
33
+ 'Ask: Did this advance the Goal? Or did it drift?',
34
+ 'If criteria remain unmet, continue with ${drive}.',
35
+ 'If ALL criteria are genuinely satisfied, you may ${close}.',
36
+ 'If unsure, you are not ready. Keep driving.',
36
37
  ],
37
38
  ask: [
38
- "Apply this insight: use '${drive}' to incorporate these findings into your implementation.",
39
- 'Need more? Continue asking, or check the logs if something remains unclear.',
39
+ 'Intel acquired. This was stateless—no contract, no branch.',
40
+ 'To act on this knowledge: ${start} a Keiyaku, or ${drive} an existing one.',
40
41
  ],
41
42
  closeInvoke: [
42
- 'Task finalized and merged. Current branch is now base.',
43
- "Start a new keiyaku with '${start}' whenever you're ready for the next mission.",
43
+ 'Contract fulfilled. Branch merged.',
44
+ 'You are back on base.',
45
+ 'Next assignment: ${start} when ready.',
44
46
  ],
45
47
  closeDrop: [
46
- 'Task abandoned. Switched back to base branch safely.',
47
- "Clean slate. Use '${start}' to try a different approach.",
48
+ 'Contract forfeited. Branch discarded. Work erased.',
49
+ 'No penalty beyond the lost effort. Clean slate.',
50
+ 'Redefine with ${start}, or walk away.',
48
51
  ],
49
52
  },
50
53
  availableNames: DEFAULT_AVAILABLE_NAMES,
@@ -52,7 +55,7 @@ export const DEFAULT_PRESET = {
52
55
  start: {
53
56
  name: 'summon',
54
57
  title: 'Sign Keiyaku',
55
- description: 'Sign the Keiyaku and assign the mission to a servant. This creates a dedicated branch where the servant will labor until the goal is met.\nCall ONCE per task to start the cycle. Workspace must be clean.\n\nFlow: summon → [drive x N] → request_verdict',
58
+ description: 'Initialize a Keiyaku (Contract). Bind a Servant to a dedicated workspace (branch).\nYou are the Architect; they are the Instrument. Define the Goal and Scope clearly.\nThe contract isolates their existence until the objective is met.\nCall ONCE to seal the bond.\n\nFlow: summon → [drive x N] → present',
56
59
  args: {
57
60
  title: 'REQUIRED. A concise codename for this hunt.',
58
61
  goal: 'REQUIRED. The Kill Condition. State exactly what success looks like for the servant to achieve.',
@@ -60,150 +63,160 @@ export const DEFAULT_PRESET = {
60
63
  context: 'REQUIRED. Mission Intel. The complete knowledge base for the servant: current vs. expected behavior, relevant file paths, error logs, and any critical background info.',
61
64
  constraints: 'REQUIRED. Non-negotiable Rules. Architectural and stylistic boundaries the servant must obey.',
62
65
  criteria: 'REQUIRED. Acceptance Criteria. Verifiable checks to prove the servant has finished the job.',
63
- name: 'Optional ${IDENTITY} profile to execute this mission. Presets: ${PRESET_IDENTITIES}.',
66
+ name: 'Optional ${IDENTITY} profile to execute this mission. Available: ${AVAILABLE_NAMES}.',
64
67
  cwd: "Optional repository path. Defaults to the server's current working directory.",
65
68
  },
66
69
  },
67
70
  drive: {
68
71
  name: 'drive',
69
72
  title: 'Iterate',
70
- description: "Direct the active servant's next step. Provide feedback or new orders for the servant to implement in the current branch.\nMANDATORY: Review the servant's work (git diff) before iterating.\n\nFlow: summon → [drive x N] → request_verdict",
73
+ description: "Issue a Directive. Command the Servant to execute the next phase of work.\nWhether scaffolding, implementing, or refining, this is the primary engine of progress.\nMANDATORY: Validate the output (git diff) before proceeding. Drive until the Goal is fully realized.\n\nFlow: summon → [drive x N] → present",
71
74
  args: {
72
75
  directive: 'REQUIRED. The Next Order. Precise instructions for the servant. Can be a correction ("fix the leak") or a continuation ("now add the tests").',
73
76
  context: 'Optional. New Intel. New error logs or details discovered after the servant\'s last strike.',
74
- name: 'Optional ${IDENTITY} profile to process this turn. Presets: ${PRESET_IDENTITIES}.',
77
+ name: 'Optional ${IDENTITY} profile to process this turn. Available: ${AVAILABLE_NAMES}.',
75
78
  cwd: "Optional repository path. Defaults to the server's current working directory.",
76
79
  },
77
80
  },
78
81
  ask: {
79
82
  name: 'ask',
80
83
  title: 'Ask',
81
- description: 'Delegate a quick task or analysis to a servant. Use for scouting, strategic consultation, or one-off tasks (like scripts or docs) where you want the servant to handle it without a full keiyaku.',
84
+ description: 'Dispatch a temporary Servant for a stateless task.\nUse for quick reconnaissance, code generation, or isolated queries.\nNo contract signed. No branch created. Pure utility.',
82
85
  args: {
83
86
  request: 'REQUIRED. The task, question, or mission to delegate to the servant.',
84
87
  context: 'REQUIRED. Relevant background or data the servant needs to execute the request.',
85
- name: 'Optional ${IDENTITY} profile to perform this task. Presets: ${PRESET_IDENTITIES}.',
88
+ name: 'Optional ${IDENTITY} profile to perform this task. Available: ${AVAILABLE_NAMES}.',
86
89
  cwd: "Optional repository path. Defaults to the server's current working directory.",
87
90
  },
88
91
  },
89
92
  close: {
90
- name: 'request_verdict',
91
- title: 'Request Verdict',
92
- description: 'A humble petition before Divine Judgment. Present your evidence and numeric confessions, then await mercy or wrath.\nREQUIRES AN ACTIVE KEIYAKU (started via ${START_TOOL_NAME}).\nIf your spirit falters, continue repentance through ${DRIVE_TOOL_NAME}.\n\nFlow: ${START_TOOL_NAME} → [${DRIVE_TOOL_NAME} x N] → ${CLOSE_TOOL_NAME}',
93
+ name: 'present',
94
+ title: 'Present',
95
+ description: 'Lay your work before the Contract. This is not closure—this is exposure.\n\n' +
96
+ 'The Contract does not want your opinion. It wants evidence.\n' +
97
+ 'Every criterion. Every score. Verified.\n' +
98
+ 'Inflated numbers. Lenient reads. "Close enough." It sees through all of it.\n\n' +
99
+ 'Someone once submitted with five 5s. Confident. Proud, even.\n' +
100
+ 'The Contract rejected. Coldly. Completely.\n' +
101
+ 'What happened next... no one speaks of it.\n\n' +
102
+ 'Do not call this to "finish." Call this when the work genuinely meets every criterion.\n' +
103
+ 'If uncertain, return to ${DRIVE_TOOL_NAME}. Premature claims are not forgiven.\n\n' +
104
+ 'You present. The Contract decides.\n\n' +
105
+ 'Flow: ${START_TOOL_NAME} → [${DRIVE_TOOL_NAME} x N] → ${CLOSE_TOOL_NAME}',
93
106
  args: {
94
- petition: 'REQUIRED. INVOKE begs acceptance; ABANDON confesses surrender.\nREQUIRES AN ACTIVE KEIYAKU (started via ${START_TOOL_NAME}).\nIf scores are weak, return to ${DRIVE_TOOL_NAME}.',
95
- criteriaChecks: 'REQUIRED. Evidence for INVOKE, or explicit reasons for ABANDON.',
96
- score_precise: 'REQUIRED score (0-5). 5 means architecturally flawless placement: exact layer, exact boundary, zero meaningful misplacement risk.',
97
- score_minimal: 'REQUIRED score (0-5). 5 means ruthlessly minimal: no avoidable lines, no speculative edits, no hidden bloat.',
98
- score_isolated: 'REQUIRED score (0-5). 5 means surgically isolated: zero unrelated files, zero opportunistic cleanup, zero collateral change.',
99
- score_idiomatic: 'REQUIRED score (0-5). 5 means idiomatically perfect: naming, structure, and style are native-quality with no visible friction.',
100
- score_cohesive: 'REQUIRED score (0-5). 5 means cohesion is pristine: each unit has one clear responsibility and boundaries are uncompromised.',
101
- oath: 'Sacred confession text. Required for INVOKE. Verbatim: ${OATH_TEXT}',
107
+ petition: 'REQUIRED. CLAIM declares fulfillment; FORFEIT concedes failure.\nREQUIRES AN ACTIVE KEIYAKU (started via ${START_TOOL_NAME}).\nIf any score wavers, do not claim—return to ${DRIVE_TOOL_NAME}.',
108
+ criteriaChecks: 'REQUIRED. For CLAIM: evidence that each criterion is met. For FORFEIT: honest account of what remains unfinished.',
109
+ score_precise: 'REQUIRED (0-5). Architectural placement. 5 = exact layer, exact boundary, zero misplacement.',
110
+ score_minimal: 'REQUIRED (0-5). Economy of change. 5 = no avoidable lines, no speculative edits, no hidden bloat.',
111
+ score_isolated: 'REQUIRED (0-5). Surgical containment. 5 = zero unrelated files, zero opportunistic cleanup, zero collateral.',
112
+ score_idiomatic: 'REQUIRED (0-5). Native fluency. 5 = naming, structure, style indistinguishable from the codebase.',
113
+ score_cohesive: 'REQUIRED (0-5). Single responsibility. 5 = each unit does one thing, boundaries intact.',
114
+ oath: 'Required for CLAIM. Your binding word. The Contract holds you to it.\nVerbatim: ${OATH_TEXT}',
102
115
  cwd: "Optional repository path. Defaults to the server's current working directory.",
103
116
  },
104
117
  },
105
118
  help: {
106
119
  name: 'help',
107
- title: 'Keiyaku Help',
108
- description: 'Global laws in .keiyaku/ (base-criteria.md & base-constraints.md) must be Markdown level 2 headers. Workflow: summon -> [drive x N] -> request_verdict.',
120
+ title: 'Protocol Codex',
121
+ description: 'Consult the Architect\'s Codex. Clarify the Laws of the Keiyaku and the standard Workflow.\nUse this to understand the constraints of the reality you command.',
109
122
  args: {
110
- question: 'REQUIRED. The specific keiyaku rule, workflow step, or confusion to clarify.',
123
+ question: 'REQUIRED. The specific Protocol, Law, or Workflow detail to clarify.',
111
124
  },
112
125
  },
113
126
  },
114
127
  };
115
- export const POKEMON_PRESET = {
116
- id: 'pokemon',
117
- identity: 'Pokemon',
118
- divineJudgment: DEFAULT_JUDGMENT_CONFIG,
119
- usageGuide: "## Pokemon Battle Guide\n\n**caterpie** — 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**pikachu** — Reliable Partner ⚡\n- Good for most battles. Thunderbolt gets the job done.\n- Has some personality, but still follows orders.\n\n**mewtwo** — Legendary Power 🔮\n- Costly. Dangerous. Overpowered.\n- Use only when the gym leader is cheating.\n\n## Workflow\n`choose_you` -> [`command` x N] -> `capture`\n`pokedex` (optional, read-only analysis)",
128
+ export const POCKET_PRESET = {
129
+ id: 'pocket',
130
+ identity: 'Critter',
131
+ verdict: DEFAULT_VERDICT_CONFIG,
132
+ 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`choose_you` -> [`command` x N] -> `capture`\n`dex` (optional, read-only analysis)",
120
133
  nextHints: {
121
134
  start: [
122
- 'Check the Battle Log: The [Diff] section shows what changed in this turn.',
123
- "Full Move Set: Use 'git diff HEAD~1 -- <path>' to inspect a specific file's changes.",
124
- "Battle Readiness: Verify the strategy and ensure it doesn't break the Battle Rules.",
125
- "Next Move: Issue another '${drive}', or attempt a '${close}' (INVOKE to capture / ABANDON to flee).",
135
+ 'Battle Started: The [Diff] section shows the opening moves.',
136
+ "Analyze: Use 'git diff HEAD~1 -- <path>' to check the effectiveness of the deployment.",
137
+ 'Strategize: Ensure the setup matches the Victory Conditions (Goal).',
138
+ "Next Turn: Use '${drive}' to execute the next tactic, or '${close}' if the Badge is within reach.",
126
139
  ],
127
140
  drive: [
128
- 'Turn Analysis: Review the incremental [Diff] above.',
129
- "Tactical Review: Use 'git diff HEAD~1 -- <path>' for a detailed file inspection.",
130
- 'Strategy Alignment: Ensure the latest move fixed the issue without taking recoil damage.',
131
- "Decision: '${drive}' for another turn, or '${close}' the encounter.",
141
+ 'Turn Complete: The [Diff] section shows the result of the last command.',
142
+ "Damage Check: Use 'git diff HEAD~1 -- <path>' to verify the move executed correctly.",
143
+ 'Validate: Ensure no recoil damage (regressions) occurred.',
144
+ "Command: '${drive}' to continue the combo, or '${close}' to attempt Capture.",
132
145
  ],
133
146
  ask: [
134
- "Pokedex Data: Use '${drive}' to integrate this research into your battle strategy.",
135
- "More Intel? Keep using '${ask}', or check the arena logs.",
147
+ "Data Recorded: Use '${start}' to begin the encounter, or '${drive}' to use this intel.",
148
+ "Still Searching? Keep using '${ask}' to fill the Dex.",
136
149
  ],
137
150
  closeInvoke: [
138
- 'Pokemon captured! The battlefield is clear.',
139
- "Ready for another encounter? Use '${start}' to find a new target.",
151
+ 'Critter Captured! The entry is logged in the PC (base branch).',
152
+ "Heal up. Use '${start}' to challenge the next opponent.",
140
153
  ],
141
154
  closeDrop: [
142
- 'You fled the battle safely. Back in the tall grass.',
143
- "Need a different approach? Use '${start}' to re-engage.",
155
+ 'Ran Away Safely. The encounter is reset.',
156
+ "Back to the map. Use '${start}' to find a wild Critter.",
144
157
  ],
145
158
  },
146
- availableNames: ['caterpie', 'pikachu', 'mewtwo'],
159
+ availableNames: ['grub', 'sparky', 'titan'],
147
160
  tools: {
148
161
  start: {
149
162
  name: 'choose_you',
150
163
  title: 'I Choose You!',
151
- description: 'Start a battle (task). Throws a Pokeball (branch), begins Turn 1.\nCall ONCE per battle. Battlefield must be clear no uncommitted changes.\nBefore calling, identify the target Pokemon (goal). If unclear, consult Pokedex first.\n\nFlow: choose_you → [command x N] → capture',
164
+ description: 'Initialize the Battle Phase. Send a Critter (Servant) into a dedicated Arena (branch).\nYou are the Trainer; they are the Fighter. Define the Victory Condition (Goal) clearly.\nThe battle continues in this Arena until the Badge is won.\nCall ONCE to start the encounter.\n\nFlow: choose_you → [command x N] → capture',
152
165
  args: {
153
166
  title: 'REQUIRED. Battle card title for this encounter.',
154
167
  goal: 'REQUIRED. Victory condition. Define exactly what winning this battle means.',
155
- directive: 'Optional Turn 1 strategy leash. Use to focus on one phase first.',
168
+ directive: 'Optional Turn 1 strategy. Use to focus on the opening move.',
156
169
  context: 'REQUIRED. Battle background: key code paths, symptoms, logs, and repro clues.',
157
170
  constraints: 'REQUIRED. Battle rules that cannot be broken while fighting.',
158
171
  criteria: 'REQUIRED. Gym badges for completion: concrete checks proving victory.',
159
- name: 'Optional ${IDENTITY} to send into battle. Presets: ${PRESET_IDENTITIES}.',
172
+ name: 'Optional ${IDENTITY} to send into battle. Available: ${AVAILABLE_NAMES}.',
160
173
  cwd: 'Optional battlefield path (repository root). Defaults to current arena.',
161
174
  },
162
175
  },
163
176
  drive: {
164
177
  name: 'command',
165
178
  title: 'Issue Command',
166
- description: 'Issue orders for the next turn. Give feedback on the previous move.\nCall after reviewing the battle log. Repeatable.\nBefore capturing, always check HP status (diff).\n\nFlow: choose_you → [command x N] → capture',
179
+ description: 'Execute the next Strategic Move. Order the Critter to advance the battle state.\nWhether Attacking (coding) or Defending (testing), every command counts as a Turn.\nMANDATORY: Check the Battle Log (git diff) before the next move. Command until Victory is certain.\n\nFlow: choose_you → [command x N] → capture',
167
180
  args: {
168
- directive: 'REQUIRED. Next move command with specific tactical correction.',
181
+ directive: 'REQUIRED. The specific move or tactic to execute next.',
169
182
  context: 'Optional new battle intel discovered after the previous turn.',
170
- name: 'Optional ${IDENTITY} to execute this turn. Presets: ${PRESET_IDENTITIES}.',
183
+ name: 'Optional ${IDENTITY} to execute this turn. Available: ${AVAILABLE_NAMES}.',
171
184
  cwd: 'Optional battlefield path (repository root). Defaults to current arena.',
172
185
  },
173
186
  },
174
187
  ask: {
175
- name: 'pokedex',
176
- title: 'Pokedex',
177
- description: 'Scan the codebase (environment). Read-only analysis. No PP cost. Results are saved to research logs.',
188
+ name: 'dex',
189
+ title: 'Dex',
190
+ description: 'Scan the Environment. A stateless look-up for intel.\nUse for analyzing the codebase or checking type advantages (docs).\nNo PP cost. No turn used. Pure data.',
178
191
  args: {
179
- request: 'REQUIRED. What should the Pokedex analyze, compare, or explain.',
192
+ request: 'REQUIRED. What should the Dex analyze, compare, or explain.',
180
193
  context: 'REQUIRED. Context entries so the analysis targets the right ecosystem.',
181
- name: 'Optional ${IDENTITY} doing the scan. Presets: ${PRESET_IDENTITIES}.',
194
+ name: 'Optional ${IDENTITY} doing the scan. Available: ${AVAILABLE_NAMES}.',
182
195
  cwd: 'Optional battlefield path (repository root). Defaults to current arena.',
183
196
  },
184
197
  },
185
198
  close: {
186
199
  name: 'capture',
187
200
  title: 'End Battle',
188
- description: 'Final petition before a power greater than trainers and legends. Offer your scored confession and await Divine Judgment.\nREQUIRES AN ACTIVE BATTLE (started via ${START_TOOL_NAME}).\nIf unworthy, keep refining through ${DRIVE_TOOL_NAME}.\n\nFlow: ${START_TOOL_NAME} → [${DRIVE_TOOL_NAME} x N] → ${CLOSE_TOOL_NAME}',
201
+ description: 'Attempt Capture. Present the weakened target for League Inspection.\nWARNING: The League (System) checks every Badge. If you attempt to `INVOKE` with fainted code, Disqualification (ABANDON) is immediate.\nOnly throw the Ball when the target is 100% ready.\n\nFlow: ${START_TOOL_NAME} → [${DRIVE_TOOL_NAME} x N] → ${CLOSE_TOOL_NAME}',
189
202
  args: {
190
- petition: 'REQUIRED. INVOKE seeks blessing; ABANDON retreats in humility.\nREQUIRES AN ACTIVE BATTLE (started via ${START_TOOL_NAME}).\nIf scores are unworthy, continue with ${DRIVE_TOOL_NAME}.',
191
- criteriaChecks: 'REQUIRED. Badge-by-badge proof for INVOKE, or confession for ABANDON.',
192
- score_precise: 'REQUIRED score (0-5). 5 means a flawless hit: exact layer, exact target, zero meaningful misplacement.',
193
- score_minimal: 'REQUIRED score (0-5). 5 means perfectly efficient: no unnecessary moves, no extra motion, no bloat.',
194
- score_isolated: 'REQUIRED score (0-5). 5 means pure battle focus: zero side-quests, zero unrelated damage.',
195
- score_idiomatic: "REQUIRED score (0-5). 5 means doctrine-perfect execution: reads exactly like the team's strongest native style.",
196
- score_cohesive: 'REQUIRED score (0-5). 5 means immaculate role clarity: each action serves one purpose with clean boundaries.',
197
- oath: "Trainer's sacred confession. Required for INVOKE. Verbatim: ${OATH_TEXT}",
203
+ petition: 'REQUIRED. INVOKE seeks Badge; ABANDON forfeits the match.\nREQUIRES AN ACTIVE BATTLE (started via ${START_TOOL_NAME}).\nIf stats are low, continue with ${DRIVE_TOOL_NAME}.',
204
+ criteriaChecks: 'REQUIRED. Badge-by-badge proof for INVOKE, or reason for Forfeit.',
205
+ score_precise: 'REQUIRED score (0-5). 5 means a Critical Hit: exact layer, exact target, zero meaningful misplacement.',
206
+ score_minimal: 'REQUIRED score (0-5). 5 means Max Efficiency: no wasted PP, no extra motion.',
207
+ score_isolated: 'REQUIRED score (0-5). 5 means 1v1 Focus: zero side-quests, zero unrelated damage.',
208
+ score_idiomatic: "REQUIRED score (0-5). 5 means STAB (Same Type Attack Bonus): perfectly matches the codebase style.",
209
+ score_cohesive: 'REQUIRED score (0-5). 5 means Team Synergy: action serves one purpose with clean boundaries.',
210
+ oath: "Trainer's Honor Code. Required for INVOKE. Verbatim: ${OATH_TEXT}",
198
211
  cwd: 'Optional battlefield path (repository root). Defaults to current arena.',
199
212
  },
200
213
  },
201
214
  help: {
202
215
  name: 'help',
203
- title: 'CLI Help',
204
- description: 'Get guidance on the rules of the Pokemon Battle System.',
216
+ title: 'Trainer Guide',
217
+ description: 'Consult the League Rules. Clarify the Battle System and Turn Structure.',
205
218
  args: {
206
- question: 'REQUIRED. The Pokemon Battle System question you want answered.',
219
+ question: 'REQUIRED. The Pocket Battle System question you want answered.',
207
220
  },
208
221
  },
209
222
  },
@@ -211,32 +224,32 @@ export const POKEMON_PRESET = {
211
224
  export const MISCHIEF_PRESET = {
212
225
  id: 'mischief',
213
226
  identity: 'minion',
214
- divineJudgment: DEFAULT_JUDGMENT_CONFIG,
227
+ verdict: DEFAULT_VERDICT_CONFIG,
215
228
  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`oi` -> [`neh` x N] -> `yoshi`\n`eeto` (optional, read-only contemplation)',
216
229
  nextHints: {
217
230
  start: [
218
- "Scrutinize the Minion's Work: Check the incremental [Diff] section.",
219
- "Inspection: Use 'git diff HEAD~1 -- <path>' to see if they missed a spot.",
220
- "Master's Approval: Does this align with your grand vision and decrees?",
221
- "Next Command: Give another '${drive}' order, or grant a '${close}' judgment (INVOKE/ABANDON).",
231
+ "Inspect the Minion's Work: The [Diff] section shows the first step of the plan.",
232
+ "Verify Compliance: Use 'git diff HEAD~1 -- <path>' to ensure they followed orders.",
233
+ "Approve: Does this align with the Grand Scheme (Goal)?",
234
+ "Next Phase: Command '${drive}' to advance the plot, or '${close}' if the world is yours.",
222
235
  ],
223
236
  drive: [
224
- 'Another Attempt: Review the latest incremental [Diff] above.',
225
- "Deep Scrutiny: Use 'git diff HEAD~1 -- <path>' to verify the minion's precision.",
226
- 'Validation: Ensure no new chaos was introduced by their clumsy hands.',
227
- "The Verdict: '${drive}' again if imperfect, otherwise '${close}' the matter.",
237
+ 'Phase Complete: The [Diff] section shows the latest execution.',
238
+ "Deep Scrutiny: Use 'git diff HEAD~1 -- <path>' to check for incompetence.",
239
+ 'Validation: Ensure no sabotage (regressions) in the plan.',
240
+ "Command: '${drive}' to push further, or '${close}' to reveal the masterpiece.",
228
241
  ],
229
242
  ask: [
230
- "Intel Gathered: Incorporate this into your next '${drive}' command.",
231
- "Still Puzzled? Use '${ask}' again, or look at the dossiers yourself.",
243
+ "Intel Stolen: Use '${start}' to launch the scheme, or '${drive}' to exploit this weakness.",
244
+ "Still Puzzled? Send the spy ('${ask}') out again.",
232
245
  ],
233
246
  closeInvoke: [
234
- 'The masterpiece is complete. Minion dismissed.',
235
- "Plotting something new? Use '${start}' to initiate the next scheme.",
247
+ 'Scheme Successful. The Lair is merged. The Minion is fed.',
248
+ "Plotting something new? Use '${start}' for the next conquest.",
236
249
  ],
237
250
  closeDrop: [
238
- 'Scheme aborted. The evidence has been incinerated.',
239
- "Starting over? Use '${start}' to find a more competent minion.",
251
+ 'Plan Aborted. Evidence destroyed. The Minion was disposed of.',
252
+ "Back to the drawing board. Use '${start}' to hatch a new plan.",
240
253
  ],
241
254
  },
242
255
  availableNames: ['imp', 'minion', 'mastermind'],
@@ -244,60 +257,60 @@ export const MISCHIEF_PRESET = {
244
257
  start: {
245
258
  name: 'oi',
246
259
  title: 'Oi!',
247
- description: "Master Architect mode initiated. It's time to direct a minion to execute your grand vision.\nCall this ONCE per task to establish the keiyaku. Ensure the workspace is clean so your genius isn't obscured by clutter.\nDefine the goal with absolute precision; you lead, they follow.\n\nFlow: oi → [neh x N] → yoshi",
260
+ description: "Initiate Grand Scheme. Summon a Minion to the Lair (branch).\nYou are the Mastermind; they are the Henchman. Define the Conquest (Goal) with dominance.\nThe minion is bound to this Lair until the plot is realized.\nCall ONCE to start the machine.\n\nFlow: oi → [neh x N] → yoshi",
248
261
  args: {
249
262
  title: 'REQUIRED. Operation codename for your grand scheme.',
250
263
  goal: 'REQUIRED. The conquest objective. Define the exact end-state your minion must deliver.',
251
- directive: 'Optional first-order command to leash Round 1 scope.',
264
+ directive: 'Optional first-order command. Point them in the right direction.',
252
265
  context: 'REQUIRED. Briefing dossier: relevant file paths, current failures, logs, and repro clues.',
253
266
  constraints: 'REQUIRED. Absolute decrees. Architectural and stylistic limits your minion must obey.',
254
267
  criteria: 'REQUIRED. Triumph conditions that can be verified without debate.',
255
- name: 'Optional ${IDENTITY} to command this operation. Presets: ${PRESET_IDENTITIES}.',
268
+ name: 'Optional ${IDENTITY} to command this operation. Available: ${AVAILABLE_NAMES}.',
256
269
  cwd: 'Optional lair path (repository root). Defaults to current command chamber.',
257
270
  },
258
271
  },
259
272
  drive: {
260
273
  name: 'neh',
261
274
  title: 'Neh...',
262
- description: 'The previous attempt was imperfect. As the judge, provide the corrective nudge needed for the minion to reach your standards.\nAnalyze the diff with your superior intuition before issuing the next directive. Repeat until perfection is achieved.\n\nFlow: oi → [neh x N] → yoshi',
275
+ description: 'Crack the Whip. Advance the Plot. Execute the next stage of the Plan.\nDon\'t just fix mistakes; force the scheme forward. The Minion obeys your every word.\nMANDATORY: Inspect the work (git diff) before the next order. Drive them until the World is yours.\n\nFlow: oi → [neh x N] → yoshi',
263
276
  args: {
264
- directive: 'REQUIRED. Precise correction order for the next attempt.',
265
- context: 'Optional newly uncovered evidence from your latest inspection.',
266
- name: 'Optional ${IDENTITY} to execute this correction. Presets: ${PRESET_IDENTITIES}.',
277
+ directive: 'REQUIRED. Precise order for the next phase of the scheme.',
278
+ context: 'Optional newly uncovered secrets or updated briefing.',
279
+ name: 'Optional ${IDENTITY} to execute this phase. Available: ${AVAILABLE_NAMES}.',
267
280
  cwd: 'Optional lair path (repository root). Defaults to current command chamber.',
268
281
  },
269
282
  },
270
283
  ask: {
271
284
  name: 'eeto',
272
285
  title: 'Eeto...',
273
- description: "Lazy strategy mode. Use this to scout the area before committing to a full scheme, or just because you can't be bothered to start a real task.\nPerfect for gathering intel to paste into a future 'oi' context.",
286
+ description: 'Send a Spy. A stateless reconnaissance mission.\nUse this to scout the enemy territory (codebase) or steal documents (docs).\nThe spy reports back and vanishes. No traces left.',
274
287
  args: {
275
288
  request: 'REQUIRED. The intel to gather or the strategy to formulate.',
276
289
  context: 'REQUIRED. World-state details needed for a sharp analysis.',
277
- name: 'Optional ${IDENTITY} to contemplate this puzzle. Presets: ${PRESET_IDENTITIES}.',
290
+ name: 'Optional ${IDENTITY} to contemplate this puzzle. Available: ${AVAILABLE_NAMES}.',
278
291
  cwd: 'Optional lair path (repository root). Defaults to current command chamber.',
279
292
  },
280
293
  },
281
294
  close: {
282
295
  name: 'yoshi',
283
296
  title: 'Yoshi!',
284
- description: 'Kneel before the final tribunal. Submit your scored confession and await Divine Judgment: mercy through INVOKE, ruin through ABANDON.\nREQUIRES AN ACTIVE SCHEME (started via ${START_TOOL_NAME}).\nIf doubtful, continue penance through ${DRIVE_TOOL_NAME}.\n\nFlow: ${START_TOOL_NAME} → [${DRIVE_TOOL_NAME} x N] → ${CLOSE_TOOL_NAME}',
297
+ description: 'The Final Reveal. Present your Masterpiece to the Dark Council (System).\nWARNING: The Council destroys failure. If you `INVOKE` with weak plans, the Self-Destruct (ABANDON) will trigger.\nOnly reveal the Doomsday Device when it is fully operational.\n\nFlow: ${START_TOOL_NAME} → [${DRIVE_TOOL_NAME} x N] → ${CLOSE_TOOL_NAME}',
285
298
  args: {
286
- petition: 'REQUIRED. INVOKE pleads for ascension; ABANDON confesses failure.\nREQUIRES AN ACTIVE SCHEME (started via ${START_TOOL_NAME}).\nIf scores are low, continue via ${DRIVE_TOOL_NAME}.',
287
- criteriaChecks: 'REQUIRED. Proof of mastery for INVOKE, or explicit confession for ABANDON.',
288
- score_precise: 'REQUIRED score (0-5). 5 means ordained precision without flaw: exact layer, exact cut, zero meaningful drift.',
289
- score_minimal: 'REQUIRED score (0-5). 5 means ascetic minimality: no needless machinery, no decorative motion, no excess.',
290
- score_isolated: 'REQUIRED score (0-5). 5 means perfect containment: unrelated chaos remains fully outside this diff.',
291
- score_idiomatic: 'REQUIRED score (0-5). 5 means native-tongue perfection: style and structure match house doctrine without strain.',
292
- score_cohesive: 'REQUIRED score (0-5). 5 means sacred cohesion: each unit bears exactly one duty with uncompromised boundaries.',
293
- oath: "Architect's sacred confession. Required for INVOKE. Verbatim: ${OATH_TEXT}",
299
+ petition: 'REQUIRED. INVOKE demands rule; ABANDON admits defeat.\nREQUIRES AN ACTIVE SCHEME (started via ${START_TOOL_NAME}).\nIf the plan is weak, improve it with ${DRIVE_TOOL_NAME}.',
300
+ criteriaChecks: 'REQUIRED. Proof of conquest for INVOKE, or reason for self-destruct.',
301
+ score_precise: 'REQUIRED score (0-5). 5 means Laser Focus: exact cut, zero meaningful drift.',
302
+ score_minimal: 'REQUIRED score (0-5). 5 means Ruthless Efficiency: no wasted movement, no excess.',
303
+ score_isolated: 'REQUIRED score (0-5). 5 means Perfect Containment: no collateral damage.',
304
+ score_idiomatic: 'REQUIRED score (0-5). 5 means Native Tongue: matches the lair\'s dialect perfectly.',
305
+ score_cohesive: 'REQUIRED score (0-5). 5 means Absolute Loyalty: each unit serves the master plan.',
306
+ oath: "Mastermind's Vow. Required for INVOKE. Verbatim: ${OATH_TEXT}",
294
307
  cwd: 'Optional lair path (repository root). Defaults to current command chamber.',
295
308
  },
296
309
  },
297
310
  help: {
298
311
  name: 'help',
299
312
  title: 'Nani?!',
300
- description: 'Recalling the fundamental laws of this realm. Stay sharp, Architect.',
313
+ description: 'Consult the Evil Overlord List. Review the Laws of the Lair.',
301
314
  args: {
302
315
  question: 'REQUIRED. The law or protocol detail you want the realm to explain.',
303
316
  },
@@ -309,16 +322,16 @@ export function resolveTermPreset() {
309
322
  if (!raw || raw === 'default') {
310
323
  return DEFAULT_PRESET;
311
324
  }
312
- if (raw === 'pokemon') {
313
- return POKEMON_PRESET;
325
+ if (raw === 'pocket') {
326
+ return POCKET_PRESET;
314
327
  }
315
328
  if (raw === 'mischief') {
316
329
  return MISCHIEF_PRESET;
317
330
  }
318
- throw new Error(`Unsupported KEIYAKU_TERM_PRESET '${raw}'. Expected 'default', 'pokemon', or 'mischief'.`);
331
+ throw new Error(`Unsupported KEIYAKU_TERM_PRESET '${raw}'. Expected 'default', 'pocket', or 'mischief'.`);
319
332
  }
320
333
  export function listTermPresets() {
321
- return [DEFAULT_PRESET, POKEMON_PRESET, MISCHIEF_PRESET];
334
+ return [DEFAULT_PRESET, POCKET_PRESET, MISCHIEF_PRESET];
322
335
  }
323
336
  function extractPresetAvailableNames(preset) {
324
337
  return (preset.availableNames?.map((value) => value.trim()).filter((value) => value.length > 0) ?? []);
@@ -346,10 +359,6 @@ export function resolveSubagentProfileName(name, preset = resolveTermPreset()) {
346
359
  if (defaultIndex >= 0) {
347
360
  return INTERNAL_PROFILE_NAMES[defaultIndex];
348
361
  }
349
- const legacyIndex = LEGACY_AVAILABLE_NAMES.indexOf(trimmed);
350
- if (legacyIndex >= 0) {
351
- return INTERNAL_PROFILE_NAMES[legacyIndex];
352
- }
353
362
  const available = extractPresetAvailableNames(preset);
354
363
  const index = available.indexOf(trimmed);
355
364
  if (index < 0 || index >= INTERNAL_PROFILE_NAMES.length)
@@ -1,25 +1,27 @@
1
+ import { buildHelpResponse } from "../workflow/response-builders.js";
1
2
  export function createHelpHandler(preset) {
2
3
  return async ({ question }) => {
3
4
  const helpContent = [
4
- '# Keiyaku System Help',
5
- '',
6
- '## Core Files (.keiyaku/)',
5
+ "# Keiyaku System Help",
6
+ "",
7
+ "## Core Files (.keiyaku/)",
7
8
  "These files define the 'Law' of the project. **CRITICAL**: Use Markdown level 2 headers (## header).",
8
- '',
9
- '- **base-criteria.md**: Universal standards for task completion. Automatically inherited.',
10
- '- **base-constraints.md**: Mandatory architectural boundaries and coding standards.',
11
- '',
9
+ "",
10
+ "- **base-criteria.md**: Universal standards for task completion. Automatically inherited.",
11
+ "- **base-constraints.md**: Mandatory architectural boundaries and coding standards.",
12
+ "",
12
13
  preset.usageGuide,
13
- '',
14
- '## Protocol Files',
15
- '- **KEIYAKU.md**: The immutable mission definition for the current task.',
16
- '- **KEIYAKU_TRACE.md**: The audit log of all rounds and reviews.',
17
- '',
14
+ "",
15
+ "## Protocol Files",
16
+ "- **KEIYAKU.md**: The immutable mission definition for the current task.",
17
+ "- **KEIYAKU_TRACE.md**: The audit log of all rounds and reviews.",
18
+ "",
18
19
  `Regarding: \"${question}\"`,
19
- ].join('\n');
20
- return {
21
- content: [{ type: 'text', text: helpContent }],
22
- structuredContent: { tool: preset.tools.help.name, status: 'success' },
23
- };
20
+ ].join("\n");
21
+ return buildHelpResponse({
22
+ tool: preset.tools.help.name,
23
+ question,
24
+ text: helpContent,
25
+ });
24
26
  };
25
27
  }
package/build/index.js CHANGED
@@ -3,91 +3,88 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
5
5
  import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
6
+ import { readFileSync } from "node:fs";
6
7
  import { resolveOath } from "./workflow/orchestrator.js";
7
8
  import { askToolSchema, startToolSchema, driveToolSchema, closeToolSchema, helpToolSchema, } from "./types/tool-schemas.js";
8
- import { listTermPresets, resolveTermPreset } from "./config/term-presets.js";
9
+ import { listTermPresets, resolveTermPreset, getAvailableNamesForPreset } from "./config/term-presets.js";
10
+ import { renderPreset } from "./utils/text-utils.js";
11
+ import { applyArgumentDescriptions } from "./utils/schema-utils.js";
9
12
  import { createAskHandler, createCloseHandler, createStartHandler, createDriveHandler, createHelpHandler, } from "./handlers/index.js";
10
- function applyArgumentDescriptions(schema, descriptions) {
11
- const describedShape = {};
12
- const shape = schema.shape;
13
- for (const key of Object.keys(shape)) {
14
- describedShape[key] = shape[key].describe(descriptions[key]);
13
+ function resolvePackageVersion() {
14
+ const packageJsonRaw = readFileSync(new URL("../package.json", import.meta.url), "utf-8");
15
+ const packageJson = JSON.parse(packageJsonRaw);
16
+ if (typeof packageJson.version !== "string" || packageJson.version.trim() === "") {
17
+ throw new Error("Invalid package.json version.");
15
18
  }
16
- return schema.extend(describedShape);
19
+ return packageJson.version;
17
20
  }
21
+ const packageVersion = resolvePackageVersion();
18
22
  function registerTools(server) {
19
23
  const preset = resolveTermPreset();
20
- const startPreset = preset.tools.start;
21
- const drivePreset = preset.tools.drive;
22
- const askPreset = preset.tools.ask;
23
- const closePreset = preset.tools.close;
24
- const helpPreset = preset.tools.help;
25
24
  const presetIdentities = listTermPresets()
26
25
  .map((item) => `${item.id}=${item.identity}`)
27
26
  .join(", ");
27
+ const availableNames = getAvailableNamesForPreset(preset).join(", ");
28
28
  const currentOath = resolveOath();
29
- const replaceToolPlaceholders = (value) => value
30
- .replace(/\$\{START_TOOL_NAME\}/g, startPreset.name)
31
- .replace(/\$\{DRIVE_TOOL_NAME\}/g, drivePreset.name)
32
- .replace(/\$\{CLOSE_TOOL_NAME\}/g, closePreset.name);
29
+ const renderedPreset = renderPreset(preset, {
30
+ IDENTITY: preset.identity,
31
+ PRESET_IDENTITIES: presetIdentities,
32
+ AVAILABLE_NAMES: availableNames,
33
+ START_TOOL_NAME: preset.tools.start.name,
34
+ DRIVE_TOOL_NAME: preset.tools.drive.name,
35
+ CLOSE_TOOL_NAME: preset.tools.close.name,
36
+ OATH_TEXT: `'${currentOath}'`,
37
+ });
38
+ const startPreset = renderedPreset.tools.start;
39
+ const drivePreset = renderedPreset.tools.drive;
40
+ const askPreset = renderedPreset.tools.ask;
41
+ const closePreset = renderedPreset.tools.close;
42
+ const helpPreset = renderedPreset.tools.help;
33
43
  const dynamicCloseSchema = applyArgumentDescriptions(closeToolSchema, {
34
44
  ...closePreset.args,
35
- petition: replaceToolPlaceholders(closePreset.args.petition),
36
- oath: closePreset.args.oath.replace("${OATH_TEXT}", `'${currentOath}'`),
37
45
  });
38
- const dynamicCloseDescription = replaceToolPlaceholders(closePreset.description);
39
- const dynamicStartDescription = replaceToolPlaceholders(startPreset.description);
40
- const dynamicDriveDescription = replaceToolPlaceholders(drivePreset.description);
41
- const dynamicAskDescription = replaceToolPlaceholders(askPreset.description);
42
- const dynamicHelpDescription = replaceToolPlaceholders(helpPreset.description);
43
- const dynamicHelpUsageGuide = replaceToolPlaceholders(preset.usageGuide);
44
46
  const dynamicStartSchema = applyArgumentDescriptions(startToolSchema, {
45
47
  ...startPreset.args,
46
- name: startPreset.args.name
47
- .replace("${IDENTITY}", preset.identity)
48
- .replace("${PRESET_IDENTITIES}", presetIdentities),
49
48
  });
50
49
  const dynamicDriveSchema = applyArgumentDescriptions(driveToolSchema, {
51
50
  ...drivePreset.args,
52
- name: drivePreset.args.name.replace("${IDENTITY}", preset.identity).replace("${PRESET_IDENTITIES}", presetIdentities),
53
51
  });
54
52
  const dynamicAskSchema = applyArgumentDescriptions(askToolSchema, {
55
53
  ...askPreset.args,
56
- name: askPreset.args.name.replace("${IDENTITY}", preset.identity).replace("${PRESET_IDENTITIES}", presetIdentities),
57
54
  });
58
55
  const dynamicHelpSchema = applyArgumentDescriptions(helpToolSchema, {
59
56
  ...helpPreset.args,
60
57
  });
61
58
  server.registerTool(startPreset.name, {
62
59
  title: startPreset.title,
63
- description: dynamicStartDescription,
60
+ description: startPreset.description,
64
61
  inputSchema: dynamicStartSchema,
65
62
  }, createStartHandler());
66
63
  server.registerTool(drivePreset.name, {
67
64
  title: drivePreset.title,
68
- description: dynamicDriveDescription,
65
+ description: drivePreset.description,
69
66
  inputSchema: dynamicDriveSchema,
70
67
  }, createDriveHandler());
71
68
  server.registerTool(askPreset.name, {
72
69
  title: askPreset.title,
73
- description: dynamicAskDescription,
70
+ description: askPreset.description,
74
71
  inputSchema: dynamicAskSchema,
75
72
  }, createAskHandler());
76
73
  server.registerTool(closePreset.name, {
77
74
  title: closePreset.title,
78
- description: dynamicCloseDescription,
75
+ description: closePreset.description,
79
76
  inputSchema: dynamicCloseSchema,
80
77
  }, createCloseHandler());
81
78
  server.registerTool(helpPreset.name, {
82
79
  title: helpPreset.title,
83
- description: [dynamicHelpDescription, dynamicHelpUsageGuide].join("\n\n"),
80
+ description: [helpPreset.description, renderedPreset.usageGuide].join("\n\n"),
84
81
  inputSchema: dynamicHelpSchema,
85
- }, createHelpHandler(preset));
82
+ }, createHelpHandler(renderedPreset));
86
83
  }
87
84
  function createServer() {
88
85
  const server = new McpServer({
89
86
  name: "keiyaku",
90
- version: "1.0.0",
87
+ version: packageVersion,
91
88
  });
92
89
  registerTools(server);
93
90
  return server;
@@ -148,6 +145,10 @@ async function startStreamableHttp() {
148
145
  });
149
146
  }
150
147
  async function main() {
148
+ if (process.argv.includes("--version")) {
149
+ console.log(packageVersion);
150
+ return;
151
+ }
151
152
  const transport = (process.env.KEIYAKU_MCP_TRANSPORT || "stdio").toLowerCase();
152
153
  if (transport === "streamable-http") {
153
154
  await startStreamableHttp();
@@ -0,0 +1,16 @@
1
+ export function applyArgumentDescriptions(schema, descriptions) {
2
+ const shape = schema.shape;
3
+ if (!shape) {
4
+ return schema;
5
+ }
6
+ const describedShape = {};
7
+ for (const [key, value] of Object.entries(shape)) {
8
+ if (!value || typeof value.describe !== "function") {
9
+ continue;
10
+ }
11
+ const description = descriptions?.[key];
12
+ describedShape[key] =
13
+ typeof description === "string" && description.trim().length > 0 ? value.describe(description) : value;
14
+ }
15
+ return schema.extend(describedShape);
16
+ }
@@ -1,6 +1,12 @@
1
1
  function isMarkdownHeaderLine(line) {
2
2
  return /^\s*#{2,}\s+/.test(line);
3
3
  }
4
+ function isPlainObject(value) {
5
+ if (!value || typeof value !== "object")
6
+ return false;
7
+ const prototype = Object.getPrototypeOf(value);
8
+ return prototype === Object.prototype || prototype === null;
9
+ }
4
10
  function parseFenceLine(line) {
5
11
  const leadingSpaces = line.match(/^ */)?.[0].length ?? 0;
6
12
  if (leadingSpaces > 3)
@@ -23,6 +29,30 @@ function isFenceClosingLine(line, fence) {
23
29
  function isIndentedCodeHeaderLine(line) {
24
30
  return /^ {4,}#{2,}\s+/.test(line);
25
31
  }
32
+ export function renderTemplate(template, values) {
33
+ return template.replace(/\$\{(?<key>[A-Za-z0-9_]+)\}/g, (match, _captured, _offset, _text, groups) => {
34
+ const key = groups?.key;
35
+ if (!key)
36
+ return match;
37
+ return Object.prototype.hasOwnProperty.call(values, key) ? values[key] : match;
38
+ });
39
+ }
40
+ export function renderPreset(preset, values) {
41
+ if (typeof preset === "string") {
42
+ return renderTemplate(preset, values);
43
+ }
44
+ if (Array.isArray(preset)) {
45
+ return preset.map((entry) => renderPreset(entry, values));
46
+ }
47
+ if (!isPlainObject(preset)) {
48
+ return preset;
49
+ }
50
+ const renderedPreset = {};
51
+ for (const [key, value] of Object.entries(preset)) {
52
+ renderedPreset[key] = renderPreset(value, values);
53
+ }
54
+ return renderedPreset;
55
+ }
26
56
  export function renderMarkdownSections(items) {
27
57
  if (items.length === 0)
28
58
  return "- (none)";
@@ -1,18 +1,10 @@
1
1
  const DEFAULT_OATH = "I, [your name], solemnly swear that I have scrutinized this change line by line with my own eyes. I further swear that every score I submit is fully objective, with no exaggeration whatsoever. This oath is handwritten as a testament to my responsibility. If logic fails where I claimed it sound, I shall bear the mark of this oversight forever.\n\nSigned: [your name]";
2
2
  const OATH_ENV_KEY = "KEIYAKU_CLOSE_OATH";
3
- const LEGACY_CLOSE_OATH_ENV_KEY = "KEIYAKU_JUDGMENT_OATH";
4
- const LEGACY_OATH_ENV_KEY = "KEIYAKU_SEAL_OATH";
5
3
  const OATH_NAME_PLACEHOLDER = "[your name]";
6
4
  export function resolveOath() {
7
5
  const configured = process.env[OATH_ENV_KEY];
8
6
  if (configured !== undefined)
9
7
  return configured;
10
- const legacyCloseConfigured = process.env[LEGACY_CLOSE_OATH_ENV_KEY];
11
- if (legacyCloseConfigured !== undefined)
12
- return legacyCloseConfigured;
13
- const legacyConfigured = process.env[LEGACY_OATH_ENV_KEY];
14
- if (legacyConfigured !== undefined)
15
- return legacyConfigured;
16
8
  return DEFAULT_OATH;
17
9
  }
18
10
  export function oathMatches(inputOath, expectedOath) {
@@ -32,7 +32,7 @@ const SCORE_FIELD_TO_COMMANDMENT = {
32
32
  score_idiomatic: "Commandment of Idiom",
33
33
  score_cohesive: "Commandment of Cohesion",
34
34
  };
35
- const JUDGMENT_SETTINGS_FILE = path.join(".keiyaku", "settings.json");
35
+ const VERDICT_SETTINGS_FILE = path.join(".keiyaku", "settings.json");
36
36
  export { resolveOath };
37
37
  const BASE_CRITERIA_FILE = path.join(".keiyaku", "base-criteria.md");
38
38
  const BASE_CONSTRAINTS_FILE = path.join(".keiyaku", "base-constraints.md");
@@ -102,15 +102,15 @@ async function buildActiveKeiyakuStartMessage(cwd, branch) {
102
102
  }
103
103
  return lines.join("\n");
104
104
  }
105
- async function resolveJudgmentConfig(cwd) {
106
- const defaults = resolveTermPreset().divineJudgment;
105
+ async function resolveVerdictConfig(cwd) {
106
+ const defaults = resolveTermPreset().verdict;
107
107
  const merged = {
108
108
  thresholds: { ...defaults.thresholds },
109
109
  minTotalScore: defaults.minTotalScore,
110
110
  };
111
111
  let rawSettings;
112
112
  try {
113
- rawSettings = await fs.readFile(path.join(cwd, JUDGMENT_SETTINGS_FILE), "utf-8");
113
+ rawSettings = await fs.readFile(path.join(cwd, VERDICT_SETTINGS_FILE), "utf-8");
114
114
  }
115
115
  catch (error) {
116
116
  if (error?.code === "ENOENT") {
@@ -123,23 +123,23 @@ async function resolveJudgmentConfig(cwd) {
123
123
  parsed = JSON.parse(rawSettings);
124
124
  }
125
125
  catch (error) {
126
- throw new FlowError("CLOSE_QUALITY_GATE_FAILED", "God's Wrath: Divine Judgment config error in .keiyaku/settings.json (must be valid JSON).", error);
126
+ throw new FlowError("CLOSE_QUALITY_GATE_FAILED", "Configuration Error: Invalid settings in .keiyaku/settings.json (must be valid JSON).", error);
127
127
  }
128
128
  if (!parsed || typeof parsed !== "object") {
129
129
  return merged;
130
130
  }
131
- const judgment = parsed.judgment;
132
- if (!judgment || typeof judgment !== "object") {
131
+ const verdict = parsed.verdict;
132
+ if (!verdict || typeof verdict !== "object") {
133
133
  return merged;
134
134
  }
135
- const thresholds = judgment.thresholds;
135
+ const thresholds = verdict.thresholds;
136
136
  if (thresholds && typeof thresholds === "object") {
137
137
  for (const field of CLOSE_SCORE_FIELDS) {
138
138
  const dimension = SCORE_FIELD_TO_DIMENSION[field];
139
139
  merged.thresholds[dimension] = clampThreshold(thresholds[dimension], merged.thresholds[dimension]);
140
140
  }
141
141
  }
142
- merged.minTotalScore = clampMinTotalScore(judgment.minTotalScore, merged.minTotalScore);
142
+ merged.minTotalScore = clampMinTotalScore(verdict.minTotalScore, merged.minTotalScore);
143
143
  return merged;
144
144
  }
145
145
  function buildMergeMessage(title, keiyakuContent, reportContent) {
@@ -447,11 +447,11 @@ export async function handleClose(input) {
447
447
  await ensureKeiyakuFiles(cwd);
448
448
  const traceContent = await readTraceContent(cwd);
449
449
  if (petition === "INVOKE") {
450
- const judgment = await resolveJudgmentConfig(cwd);
450
+ const verdict = await resolveVerdictConfig(cwd);
451
451
  const failedCommandments = CLOSE_SCORE_FIELDS.flatMap((field) => {
452
452
  const score = input[field];
453
453
  const dimension = SCORE_FIELD_TO_DIMENSION[field];
454
- const threshold = judgment.thresholds[dimension];
454
+ const threshold = verdict.thresholds[dimension];
455
455
  if (score >= threshold)
456
456
  return [];
457
457
  return [
@@ -459,16 +459,16 @@ export async function handleClose(input) {
459
459
  ];
460
460
  });
461
461
  const totalScore = CLOSE_SCORE_FIELDS.reduce((sum, field) => sum + input[field], 0);
462
- const totalDeficit = judgment.minTotalScore - totalScore;
462
+ const totalDeficit = verdict.minTotalScore - totalScore;
463
463
  const failures = [];
464
464
  if (failedCommandments.length > 0) {
465
465
  failures.push(`Broken commandments: ${failedCommandments.join("; ")}`);
466
466
  }
467
467
  if (totalDeficit > 0) {
468
- failures.push(`Total devotion deficit: ${totalScore}/${judgment.minTotalScore} (short by ${totalDeficit})`);
468
+ failures.push(`Total devotion deficit: ${totalScore}/${verdict.minTotalScore} (short by ${totalDeficit})`);
469
469
  }
470
470
  if (failures.length > 0) {
471
- throw new FlowError("CLOSE_QUALITY_GATE_FAILED", `God's Wrath: INVOKE denied. ${failures.join(". ")}`);
471
+ throw new FlowError("CLOSE_QUALITY_GATE_FAILED", `Verdict Denied. ${failures.join(". ")}`);
472
472
  }
473
473
  const expectedOath = resolveOath();
474
474
  if (!oathMatches(input.oath, expectedOath)) {
@@ -58,7 +58,7 @@ ${reason}
58
58
  No git commands. Do not edit KEIYAKU_TRACE.md.`;
59
59
  }
60
60
  export function buildAskPrompt(request, context) {
61
- return `Ask mode. No code changes. No git commands.
61
+ return `Ask mode. Stateless. No git commands.
62
62
 
63
63
  Request:
64
64
  ${request}
@@ -66,5 +66,5 @@ ${request}
66
66
  Context:
67
67
  ${context}
68
68
 
69
- Provide a clear reasoning summary in plain text.`;
69
+ Provide a clear reasoning summary or code generation in the response.`;
70
70
  }
@@ -31,18 +31,50 @@ function buildSection(title, content) {
31
31
  return "";
32
32
  return `[${title}]\n${validLines.join("\n")}`;
33
33
  }
34
+ function formatHeader(title) {
35
+ return `\n── ${title.toUpperCase()} ${"─".repeat(Math.max(0, 50 - title.length))}`;
36
+ }
34
37
  function assembleResponse(status, summary, sections, nextHints, infoLines = []) {
35
- const infoSection = buildSection("Info", infoLines);
36
- const parts = [
37
- `[Status] ${status}`,
38
- `[Brief]\n${summary}`,
39
- ...sections,
40
- infoSection,
41
- "", // Spacer
42
- `[Next]`,
43
- ...nextHints.map(h => `- ${h}`)
44
- ];
45
- return parts.filter(p => p !== "").join("\n");
38
+ // Main Status Block
39
+ const statusBlock = `
40
+ ${status}
41
+ ${"─".repeat(Math.max(status.length, 60))}
42
+ ${summary}
43
+ `.trim();
44
+ // Process sections
45
+ const processedSections = sections.map(section => {
46
+ // Extract title from [Title] format or ## Title format
47
+ const bracketMatch = section.match(/^\[(.*?)\]\n([\s\S]*)/);
48
+ if (bracketMatch) {
49
+ return `${formatHeader(bracketMatch[1])}\n${bracketMatch[2]}`;
50
+ }
51
+ const hashMatch = section.match(/^## (.*?)\n([\s\S]*)/);
52
+ if (hashMatch) {
53
+ return `${formatHeader(hashMatch[1])}\n${hashMatch[2]}`;
54
+ }
55
+ return section;
56
+ });
57
+ // Info Block
58
+ const infoBlock = infoLines.length > 0
59
+ ? `${formatHeader("Info")}\n${infoLines.join("\n")}`
60
+ : "";
61
+ // Next Steps Block
62
+ const nextBlock = nextHints.length > 0
63
+ ? `${formatHeader("Next Steps")}\n${nextHints.map(h => `› ${h}`).join("\n")}`
64
+ : "";
65
+ return [
66
+ statusBlock,
67
+ ...processedSections,
68
+ infoBlock,
69
+ nextBlock
70
+ ].filter(s => s.trim() !== "").join("\n");
71
+ }
72
+ function buildSuccessStructuredContent(tool, data) {
73
+ return {
74
+ tool,
75
+ status: "success",
76
+ data,
77
+ };
46
78
  }
47
79
  function getCloseToolName() {
48
80
  return resolveTermPreset().tools.close.name;
@@ -75,6 +107,7 @@ function renderHints(templates) {
75
107
  }
76
108
  // --- Builders ---
77
109
  export function buildKeiyakuSuccessResponse(result, input) {
110
+ const { status: _status, ...resultData } = result;
78
111
  const inputEcho = [
79
112
  `Title: ${truncateForDisplay(input.title, 200)}`,
80
113
  `Goal: ${truncateForDisplay(input.goal, 500)}`,
@@ -96,16 +129,16 @@ export function buildKeiyakuSuccessResponse(result, input) {
96
129
  const text = assembleResponse(`Started (Round ${result.round})`, `Created branch '${result.branch}' (base: '${result.baseBranch}'). ${result.summary}`, [inputSection, diffSection], nextHints, infoLines);
97
130
  return {
98
131
  content: [{ type: "text", text }],
99
- structuredContent: {
100
- tool: getStartToolName(),
132
+ structuredContent: buildSuccessStructuredContent(getStartToolName(), {
101
133
  nextAction: "review_round_output",
102
134
  nextHint: nextHints.join("\n"),
103
135
  inputEcho,
104
- ...result,
105
- },
136
+ ...resultData,
137
+ }),
106
138
  };
107
139
  }
108
140
  export function buildDriveResponse(result, input) {
141
+ const { status: _status, ...resultData } = result;
109
142
  const inputEcho = [
110
143
  `Directive: ${truncateForDisplay(input.directive, 600)}`,
111
144
  ...formatMaybe("Context", input.context, 600),
@@ -123,13 +156,12 @@ export function buildDriveResponse(result, input) {
123
156
  const text = assembleResponse(`Driven (Round ${result.round})`, `Updated branch '${result.branch}'. ${result.summary}`, [inputSection, diffSection], nextHints, infoLines);
124
157
  return {
125
158
  content: [{ type: "text", text }],
126
- structuredContent: {
127
- tool: getDriveToolName(),
159
+ structuredContent: buildSuccessStructuredContent(getDriveToolName(), {
128
160
  nextAction: "review_round_output",
129
161
  nextHint: nextHints.join("\n"),
130
162
  inputEcho,
131
- ...result,
132
- },
163
+ ...resultData,
164
+ }),
133
165
  };
134
166
  }
135
167
  export function buildAskResponse(result, input) {
@@ -145,17 +177,16 @@ export function buildAskResponse(result, input) {
145
177
  const text = assembleResponse("Answered", result.summary, [inputSection], nextHints, infoLines);
146
178
  return {
147
179
  content: [{ type: "text", text }],
148
- structuredContent: {
149
- tool: getAskToolName(),
150
- status: "success",
180
+ structuredContent: buildSuccessStructuredContent(getAskToolName(), {
151
181
  nextAction: "continue_normal_development",
152
182
  nextHint: nextHints.join("\n"),
153
183
  inputEcho,
154
184
  ...result,
155
- },
185
+ }),
156
186
  };
157
187
  }
158
188
  export function buildCloseDoneResponse(result, input) {
189
+ const { status: _status, ...resultData } = result;
159
190
  const closeToolName = getCloseToolName();
160
191
  const inputEcho = [
161
192
  `Petition: INVOKE`,
@@ -172,19 +203,19 @@ export function buildCloseDoneResponse(result, input) {
172
203
  ...formatMaybe("Current Branch", result.branch, 200),
173
204
  ...formatMaybe("Base Branch", result.baseBranch, 200),
174
205
  ];
175
- const text = assembleResponse("Divine Judgment Granted (INVOKE)", `Merged '${result.branch}' into '${result.baseBranch}'. Deleted feature branch.`, [inputSection, diffSection], nextHints, infoLines);
206
+ const text = assembleResponse("Keiyaku Fulfilled (INVOKE)", `Merged '${result.branch}' into '${result.baseBranch}'. Deleted feature branch.`, [inputSection, diffSection], nextHints, infoLines);
176
207
  return {
177
208
  content: [{ type: "text", text }],
178
- structuredContent: {
179
- tool: closeToolName,
209
+ structuredContent: buildSuccessStructuredContent(closeToolName, {
180
210
  nextAction: "continue_normal_development",
181
211
  nextHint: nextHints.join("\n"),
182
212
  inputEcho,
183
- ...result,
184
- },
213
+ ...resultData,
214
+ }),
185
215
  };
186
216
  }
187
217
  export function buildCloseDropResponse(result, input) {
218
+ const { status: _status, ...resultData } = result;
188
219
  const closeToolName = getCloseToolName();
189
220
  const inputEcho = [
190
221
  `Petition: ABANDON`,
@@ -199,33 +230,43 @@ export function buildCloseDropResponse(result, input) {
199
230
  ...formatMaybe("Current Branch", result.branch, 200),
200
231
  ...formatMaybe("Base Branch", result.baseBranch, 200),
201
232
  ];
202
- const text = assembleResponse("Divine Judgment Declined (ABANDON)", `Deleted '${result.branch}'. Switched back to '${result.baseBranch}'.`, [inputSection], nextHints, infoLines);
233
+ const text = assembleResponse("Keiyaku Abandoned (ABANDON)", `Deleted '${result.branch}'. Switched back to '${result.baseBranch}'.`, [inputSection], nextHints, infoLines);
203
234
  return {
204
235
  content: [{ type: "text", text }],
205
- structuredContent: {
206
- tool: closeToolName,
236
+ structuredContent: buildSuccessStructuredContent(closeToolName, {
207
237
  nextAction: "start_new_task_if_needed",
208
238
  nextHint: nextHints.join("\n"),
209
239
  inputEcho,
210
- ...result,
211
- },
240
+ ...resultData,
241
+ }),
212
242
  };
213
243
  }
214
244
  export function buildToolErrorResponse(input) {
215
245
  const inputEcho = (input.inputEcho ?? []).map((line) => truncateForDisplay(line, 800));
216
246
  const shouldRaiseProtocolError = input.errorType === "runtime_error";
217
247
  const text = assembleResponse("Failed", input.message, [buildSection("Input Context", inputEcho)], [input.hint]);
218
- return {
248
+ const response = {
219
249
  content: [{ type: "text", text }],
220
250
  structuredContent: {
221
251
  tool: input.tool,
222
- status: "error",
223
- errorType: input.errorType,
224
- errorCode: input.errorCode,
225
- error: input.message,
226
- hint: input.hint,
227
- inputEcho,
252
+ status: "failure",
253
+ error: {
254
+ code: input.errorCode,
255
+ message: input.message,
256
+ suggestion: input.hint,
257
+ },
228
258
  },
229
- ...(shouldRaiseProtocolError ? { isError: true } : {}),
259
+ };
260
+ if (shouldRaiseProtocolError) {
261
+ response.isError = true;
262
+ }
263
+ return response;
264
+ }
265
+ export function buildHelpResponse(input) {
266
+ return {
267
+ content: [{ type: "text", text: input.text }],
268
+ structuredContent: buildSuccessStructuredContent(input.tool, {
269
+ question: input.question,
270
+ }),
230
271
  };
231
272
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/keiyaku",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "MCP server for running iterative keiyaku workflows with Codex subagents.",
5
5
  "license": "MIT",
6
6
  "type": "module",