@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 +8 -8
- package/build/common/errors.js +1 -1
- package/build/config/term-presets.js +136 -127
- package/build/handlers/help.js +19 -17
- package/build/index.js +37 -36
- package/build/utils/schema-utils.js +16 -0
- package/build/utils/text-utils.js +30 -0
- package/build/workflow/oath.js +0 -8
- package/build/workflow/orchestrator.js +14 -14
- package/build/workflow/prompts.js +2 -2
- package/build/workflow/response-builders.js +82 -41
- package/package.json +1 -1
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
|
-
| **`
|
|
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`, `
|
|
42
|
+
- **Valid values**: `default`, `pocket`, `mischief` (case-insensitive). If omitted, defaults to `default`.
|
|
43
43
|
|
|
44
|
-
- **`default`**: `summon` → `drive` → `
|
|
45
|
-
- **`
|
|
46
|
-
- **`mischief`**: `oi` → `neh` → `
|
|
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`: `
|
|
60
|
-
- `
|
|
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 `
|
|
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
|
package/build/common/errors.js
CHANGED
|
@@ -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
|
|
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
|
-
'
|
|
4
|
-
'
|
|
5
|
-
'
|
|
3
|
+
'B-tier',
|
|
4
|
+
'A-tier',
|
|
5
|
+
'S-tier',
|
|
6
6
|
];
|
|
7
|
-
const
|
|
8
|
-
|
|
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
|
-
|
|
23
|
-
usageGuide: '## Servant 使用指南\n\n**
|
|
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
|
-
'
|
|
27
|
-
|
|
28
|
-
'
|
|
29
|
-
|
|
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
|
-
'
|
|
33
|
-
|
|
34
|
-
'
|
|
35
|
-
|
|
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
|
-
|
|
39
|
-
'
|
|
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
|
-
'
|
|
43
|
-
|
|
43
|
+
'Contract fulfilled. Branch merged.',
|
|
44
|
+
'You are back on base.',
|
|
45
|
+
'Next assignment: ${start} when ready.',
|
|
44
46
|
],
|
|
45
47
|
closeDrop: [
|
|
46
|
-
'
|
|
47
|
-
|
|
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: '
|
|
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.
|
|
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: "
|
|
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.
|
|
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: '
|
|
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.
|
|
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: '
|
|
91
|
-
title: '
|
|
92
|
-
description: '
|
|
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.
|
|
95
|
-
criteriaChecks: 'REQUIRED.
|
|
96
|
-
score_precise: 'REQUIRED
|
|
97
|
-
score_minimal: 'REQUIRED
|
|
98
|
-
score_isolated: 'REQUIRED
|
|
99
|
-
score_idiomatic: 'REQUIRED
|
|
100
|
-
score_cohesive: 'REQUIRED
|
|
101
|
-
oath: '
|
|
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: '
|
|
108
|
-
description: '
|
|
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
|
|
123
|
+
question: 'REQUIRED. The specific Protocol, Law, or Workflow detail to clarify.',
|
|
111
124
|
},
|
|
112
125
|
},
|
|
113
126
|
},
|
|
114
127
|
};
|
|
115
|
-
export const
|
|
116
|
-
id: '
|
|
117
|
-
identity: '
|
|
118
|
-
|
|
119
|
-
usageGuide: "##
|
|
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
|
-
'
|
|
123
|
-
"
|
|
124
|
-
|
|
125
|
-
"Next
|
|
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
|
|
129
|
-
"
|
|
130
|
-
'
|
|
131
|
-
"
|
|
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
|
-
"
|
|
135
|
-
"
|
|
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
|
-
'
|
|
139
|
-
"
|
|
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
|
-
'
|
|
143
|
-
"
|
|
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: ['
|
|
159
|
+
availableNames: ['grub', 'sparky', 'titan'],
|
|
147
160
|
tools: {
|
|
148
161
|
start: {
|
|
149
162
|
name: 'choose_you',
|
|
150
163
|
title: 'I Choose You!',
|
|
151
|
-
description: '
|
|
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
|
|
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.
|
|
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: '
|
|
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.
|
|
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.
|
|
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: '
|
|
176
|
-
title: '
|
|
177
|
-
description: 'Scan the
|
|
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
|
|
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.
|
|
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: '
|
|
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
|
|
191
|
-
criteriaChecks: 'REQUIRED. Badge-by-badge proof for INVOKE, or
|
|
192
|
-
score_precise: 'REQUIRED score (0-5). 5 means a
|
|
193
|
-
score_minimal: 'REQUIRED score (0-5). 5 means
|
|
194
|
-
score_isolated: 'REQUIRED score (0-5). 5 means
|
|
195
|
-
score_idiomatic: "REQUIRED score (0-5). 5 means
|
|
196
|
-
score_cohesive: 'REQUIRED score (0-5). 5 means
|
|
197
|
-
oath: "Trainer's
|
|
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: '
|
|
204
|
-
description: '
|
|
216
|
+
title: 'Trainer Guide',
|
|
217
|
+
description: 'Consult the League Rules. Clarify the Battle System and Turn Structure.',
|
|
205
218
|
args: {
|
|
206
|
-
question: 'REQUIRED. The
|
|
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
|
-
|
|
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
|
-
"
|
|
219
|
-
"
|
|
220
|
-
"
|
|
221
|
-
"Next
|
|
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
|
-
'
|
|
225
|
-
"Deep Scrutiny: Use 'git diff HEAD~1 -- <path>' to
|
|
226
|
-
'Validation: Ensure no
|
|
227
|
-
"
|
|
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
|
|
231
|
-
"Still Puzzled?
|
|
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
|
|
235
|
-
"Plotting something new? Use '${start}'
|
|
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
|
-
'
|
|
239
|
-
"
|
|
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: "
|
|
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
|
|
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.
|
|
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: '
|
|
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
|
|
265
|
-
context: 'Optional newly uncovered
|
|
266
|
-
name: 'Optional ${IDENTITY} to execute this
|
|
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:
|
|
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.
|
|
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: '
|
|
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
|
|
287
|
-
criteriaChecks: 'REQUIRED. Proof of
|
|
288
|
-
score_precise: 'REQUIRED score (0-5). 5 means
|
|
289
|
-
score_minimal: 'REQUIRED score (0-5). 5 means
|
|
290
|
-
score_isolated: 'REQUIRED score (0-5). 5 means
|
|
291
|
-
score_idiomatic: 'REQUIRED score (0-5). 5 means
|
|
292
|
-
score_cohesive: 'REQUIRED score (0-5). 5 means
|
|
293
|
-
oath: "
|
|
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: '
|
|
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 === '
|
|
313
|
-
return
|
|
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', '
|
|
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,
|
|
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)
|
package/build/handlers/help.js
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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(
|
|
20
|
-
return {
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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
|
|
30
|
-
.
|
|
31
|
-
|
|
32
|
-
|
|
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:
|
|
60
|
+
description: startPreset.description,
|
|
64
61
|
inputSchema: dynamicStartSchema,
|
|
65
62
|
}, createStartHandler());
|
|
66
63
|
server.registerTool(drivePreset.name, {
|
|
67
64
|
title: drivePreset.title,
|
|
68
|
-
description:
|
|
65
|
+
description: drivePreset.description,
|
|
69
66
|
inputSchema: dynamicDriveSchema,
|
|
70
67
|
}, createDriveHandler());
|
|
71
68
|
server.registerTool(askPreset.name, {
|
|
72
69
|
title: askPreset.title,
|
|
73
|
-
description:
|
|
70
|
+
description: askPreset.description,
|
|
74
71
|
inputSchema: dynamicAskSchema,
|
|
75
72
|
}, createAskHandler());
|
|
76
73
|
server.registerTool(closePreset.name, {
|
|
77
74
|
title: closePreset.title,
|
|
78
|
-
description:
|
|
75
|
+
description: closePreset.description,
|
|
79
76
|
inputSchema: dynamicCloseSchema,
|
|
80
77
|
}, createCloseHandler());
|
|
81
78
|
server.registerTool(helpPreset.name, {
|
|
82
79
|
title: helpPreset.title,
|
|
83
|
-
description: [
|
|
80
|
+
description: [helpPreset.description, renderedPreset.usageGuide].join("\n\n"),
|
|
84
81
|
inputSchema: dynamicHelpSchema,
|
|
85
|
-
}, createHelpHandler(
|
|
82
|
+
}, createHelpHandler(renderedPreset));
|
|
86
83
|
}
|
|
87
84
|
function createServer() {
|
|
88
85
|
const server = new McpServer({
|
|
89
86
|
name: "keiyaku",
|
|
90
|
-
version:
|
|
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)";
|
package/build/workflow/oath.js
CHANGED
|
@@ -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
|
|
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
|
|
106
|
-
const defaults = resolveTermPreset().
|
|
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,
|
|
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", "
|
|
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
|
|
132
|
-
if (!
|
|
131
|
+
const verdict = parsed.verdict;
|
|
132
|
+
if (!verdict || typeof verdict !== "object") {
|
|
133
133
|
return merged;
|
|
134
134
|
}
|
|
135
|
-
const 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(
|
|
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
|
|
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 =
|
|
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 =
|
|
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}/${
|
|
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", `
|
|
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.
|
|
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
|
|
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
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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("
|
|
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
|
-
...
|
|
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("
|
|
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
|
-
...
|
|
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
|
-
|
|
248
|
+
const response = {
|
|
219
249
|
content: [{ type: "text", text }],
|
|
220
250
|
structuredContent: {
|
|
221
251
|
tool: input.tool,
|
|
222
|
-
status: "
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
252
|
+
status: "failure",
|
|
253
|
+
error: {
|
|
254
|
+
code: input.errorCode,
|
|
255
|
+
message: input.message,
|
|
256
|
+
suggestion: input.hint,
|
|
257
|
+
},
|
|
228
258
|
},
|
|
229
|
-
|
|
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
|
}
|