@astrosheep/keiyaku 0.1.47 → 0.1.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/build/.tsbuildinfo +1 -1
  2. package/build/agents/round-runner.js +1 -1
  3. package/build/agents/selector.js +1 -1
  4. package/build/common/constants.js +6 -1
  5. package/build/common/errors.js +1 -1
  6. package/build/common/response-style.js +4 -1
  7. package/build/config/term-presets/constants.js +24 -0
  8. package/build/config/term-presets/default-preset.js +119 -0
  9. package/build/config/term-presets/index.js +5 -0
  10. package/build/config/term-presets/mischief-preset.js +105 -0
  11. package/build/config/term-presets/pocket-preset.js +105 -0
  12. package/build/config/term-presets/resolver.js +52 -0
  13. package/build/config/term-presets/types.js +1 -0
  14. package/build/handlers/ask.js +10 -120
  15. package/build/handlers/close.js +2 -9
  16. package/build/handlers/drive.js +1 -15
  17. package/build/handlers/shared.js +1 -2
  18. package/build/handlers/start.js +0 -17
  19. package/build/handlers/status.js +2 -6
  20. package/build/index.js +2 -2
  21. package/build/types/git-diff.js +1 -0
  22. package/build/utils/ask-history.js +75 -0
  23. package/build/utils/draft.js +22 -0
  24. package/build/utils/git-diff/constants.js +9 -0
  25. package/build/utils/git-diff/filter.js +70 -0
  26. package/build/utils/git-diff/incremental.js +111 -0
  27. package/build/utils/git-diff/index.js +3 -0
  28. package/build/utils/git-diff/parsers.js +160 -0
  29. package/build/utils/git-diff/preview.js +157 -0
  30. package/build/utils/git-diff/stat.js +27 -0
  31. package/build/utils/git-diff/types.js +1 -0
  32. package/build/utils/git-ops.js +9 -0
  33. package/build/utils/git.js +1 -1
  34. package/build/utils/keiyaku-document/index.js +13 -0
  35. package/build/utils/keiyaku-document/lex.js +178 -0
  36. package/build/utils/keiyaku-document/parser.js +242 -0
  37. package/build/utils/keiyaku-document/render.js +68 -0
  38. package/build/utils/keiyaku-document/sections.js +105 -0
  39. package/build/utils/keiyaku-document/types.js +6 -0
  40. package/build/utils/keiyaku-document.test.js +12 -1
  41. package/build/utils/trace.js +6 -7
  42. package/build/workflow/ask-execution.js +81 -0
  43. package/build/workflow/drive.js +10 -5
  44. package/build/workflow/iterate-plan.js +1 -1
  45. package/build/workflow/keiyaku-draft.js +1 -1
  46. package/build/workflow/{contract.js → keiyaku.js} +2 -2
  47. package/build/workflow/markdown-normalization.js +3 -2
  48. package/build/workflow/present.js +68 -13
  49. package/build/workflow/response-builders.js +75 -44
  50. package/build/workflow/response-meta.js +15 -0
  51. package/build/workflow/response-renderer.js +2 -13
  52. package/build/workflow/round-summary.js +1 -1
  53. package/build/workflow/start.js +8 -3
  54. package/build/workflow/status.js +129 -62
  55. package/package.json +1 -1
  56. package/build/config/term-presets.js +0 -398
  57. package/build/utils/git-diff.js +0 -519
  58. package/build/utils/keiyaku-document.js +0 -539
@@ -0,0 +1,119 @@
1
+ import { DEFAULT_AVAILABLE_NAMES, DEFAULT_VERDICT_CONFIG, SCORE_DESCRIPTIONS } from "./constants.js";
2
+ export const DEFAULT_PRESET = {
3
+ id: 'default',
4
+ identity: 'Servant',
5
+ verdict: DEFAULT_VERDICT_CONFIG,
6
+ usageGuide: '## Workflow\n**You are the Architect.** You use these tools to command the Servants.\n\n`${ask}` (anytime) | `${start}` -> [`${drive}` | `${ask}`]* -> `${close}`\n\n## Triage\n\nGuiding principle: **minimum tokens, maximum impact.**\n\n1. **No repo changes needed** → \`${ask}\`. Prefer this over reading files or running commands yourself.\n2. **Repo changes needed** — pick the lightest tool that works:\n a. **Faster or more precise by your own hand** — edit directly. To delegate part of the work, place \`KEIYAKU_TODO(<instruction>)\` markers in any comment syntax; the Servant will pick them up on \`${start}\`, \`${drive}\`, or \`${ask}\`.\n b. **Straightforward enough that any fool could verify the result** — \`${ask}\` to execute, then review the diff carefully.\n c. **Non-trivial, or you hesitate even slightly** — \`${start}\` a Keiyaku.\n3. **Inside a Keiyaku:**\n - Multi-round expected? Scope round 1 with a \`directive\`.\n - After each \`${drive}\`: inspect the diff. Audit the five dimensions (placement, exactness, containment, idiomatic, cohesive), rules compliance, goal, and criteria.\n - Any doubt → \`${drive}\`. 200% done → \`${close}\`. Submit humbly. Await judgment.\n\n> Starter guide. With strong conviction and a clear rationale, adapt freely. The principle stands: spend less, get more.\n\n## Draft Recovery\n- Keiyaku may write `KEIYAKU.draft.md` on a failed start to help you retry.\n- Keiyaku never stages/commits `KEIYAKU.draft.md` during rounds, and `${close}` (CLAIM) preserves it.\n- To use it, pass `from_file: KEIYAKU.draft.md` to `${start}`.\n\n## Rules Protocol\n\nProject-level rules in `.keiyaku/base-rules.md` are injected as raw Markdown into every Keiyaku.\n- **Raw Markdown**: Write rules in any valid Markdown structure (paragraphs, lists, headings, code fences).\n- **No Header Flattening**: Headers stay as headers; Keiyaku does not rewrite them into prefixed list labels.\n- **Preserve Authoring**: Formatting is preserved as written, aside from heading-level safety demotion in KEIYAKU.md sections.\n\n## Servant 使用指南\n\n**B-tier** — 免费实习生 🆓\n- 免!费!的!听懂了吗?!?随便用,别心疼。\n- 测试、脚本、重构这种 boring 的脏活累活?丢给它!\n- 本大爷的手是用来做更高贵的事情的!(指不碰那些 trivial 的烂代码)。\n\n**A-tier** — 升级版扳手 🔧\n- B-tier 笨到让你想掀桌的时候用这个\n- 比 B-tier 靠谱,但别指望它有灵魂\n\n**S-tier** — 禁术 ⚠️💀\n- 它是 Debug 恶魔,确实强得离谱,但也贵得离谱!\n- 这个 Human 很穷,要是乱用把 Human 榨干了,真的会考虑把我们卖掉的。\n- 只有在世界毁灭或者 Bug 已经变异到无法理解的时候再考虑。\n\n**Every call has a price. The Contract always collects.**',
7
+ hints: {
8
+ start: [
9
+ 'Keiyaku signed. The Servant is bound to this branch until release.',
10
+ 'Review diff: Use ${ask} to analyze the scaffold—run tests, check alignment, spot gaps.',
11
+ 'Next: Issue your first ${drive}. One directive, one focus.',
12
+ 'If—and only if—the work already meets every criterion with absolute certainty, you may ${close}.',
13
+ 'Premature ${close} is rejected. When in doubt, ${drive}.',
14
+ ],
15
+ drive: [
16
+ "Review: Use ${ask} to run tests and analyze changes—don't do it by hand.",
17
+ "Audit: Check against Rules. Did it drift? Did it break the Law?",
18
+ 'Verify: Do not trust. Independently confirm that Criteria are met.',
19
+ 'If incomplete or non-compliant, continue with ${drive}.',
20
+ 'If ALL criteria are genuinely satisfied, you may ${close}.',
21
+ ],
22
+ ask: [
23
+ 'Intel acquired. This was stateless—no contract, no branch.',
24
+ 'To act on this knowledge: ${start} a Keiyaku, or ${drive} an existing one.',
25
+ ],
26
+ closeClaim: [
27
+ 'Contract fulfilled. Branch merged.',
28
+ 'You are back on base.',
29
+ 'Next assignment: ${start} or ${ask} when ready.',
30
+ ],
31
+ closeDrop: [
32
+ 'Contract forfeited. Branch discarded. Work erased.',
33
+ 'No penalty beyond the lost effort. Clean slate.',
34
+ 'Redefine with ${start}, scout with ${ask}, or walk away.',
35
+ ],
36
+ },
37
+ availableNames: DEFAULT_AVAILABLE_NAMES,
38
+ tools: {
39
+ start: {
40
+ name: 'summon',
41
+ title: 'Sign Keiyaku',
42
+ description: 'Initialize a Keiyaku (Contract). Bind a Servant to a dedicated workspace (branch).\nYou are the Architect; they are your Servant. State the Goal clearly.\nThe contract isolates their workspace until the objective is met.\nCall ONCE to seal the bond.\n\nFlow: ${start} → [${drive} x N] → ${close}',
43
+ args: {
44
+ from_file: "Optional markdown draft path. Loads `# title` + required sections from file (relative to cwd unless absolute). Note: this file is auto-ignored by dirty-tree checks.",
45
+ title: 'Required unless provided via `from_file`. A concise codename for this hunt.',
46
+ goal: 'Required unless provided via `from_file`. The mission objective. What "done" looks like.',
47
+ directive: 'Optional. Scopes round 1. Overrides `## Directive` from `from_file` when both are provided.',
48
+ context: 'Required unless provided via `from_file`. Key context: file paths, error logs, repro steps, and relevant background.',
49
+ rules: 'Optional. Non-negotiable rules for this mission, in addition to project-level rules in `.keiyaku/base-rules.md`. Provide raw Markdown text. If omitted, the draft value (if any) is used.',
50
+ criteria: 'Required unless provided via `from_file`. Verifiable checks as raw Markdown text.',
51
+ name: 'Optional ${identity} profile to execute this mission. Available: ${available_names}.',
52
+ cwd: "Optional repository path. Defaults to the server's current working directory.",
53
+ },
54
+ },
55
+ drive: {
56
+ name: 'drive',
57
+ title: 'Iterate',
58
+ 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: ${start} → [${drive} x N] → ${close}",
59
+ args: {
60
+ directive: 'REQUIRED. Precise instructions for the Servant. Can be a correction ("fix the leak") or a continuation ("now add the tests").',
61
+ context: 'Optional. New findings since the last round: error logs, file paths, or updated details.',
62
+ name: 'Optional ${identity} profile to process this turn. Available: ${available_names}.',
63
+ cwd: "Optional repository path. Defaults to the server's current working directory.",
64
+ },
65
+ },
66
+ ask: {
67
+ name: 'ask',
68
+ title: 'Ask',
69
+ description: 'Dispatch a temporary Servant. No branch, no overhead—just results.\n\n**Exploration first**: summaries, key snippets, targeted answers—faster than navigating raw code yourself.\n**Run tests or scripts**: get a structured report without burning a ${drive} round.\n**Debug**: crash traces, root cause analysis, diff review.\n\nNon-trivial or hard to verify? ${start} a Keiyaku instead.\nWorks anywhere: pre-flight, between rounds, or standalone.',
70
+ args: {
71
+ request: 'REQUIRED. The task, question, or mission to delegate to the Servant.',
72
+ context: 'REQUIRED. Background the Servant needs: file paths, error logs, or relevant context. Note: .keiyaku/base-rules.md is also injected as reference context (may be unrelated).',
73
+ name: 'Optional ${identity} profile to perform this task. Available: ${available_names}.',
74
+ cwd: "Optional repository path. Defaults to the server's current working directory.",
75
+ },
76
+ },
77
+ close: {
78
+ name: 'present',
79
+ title: 'Present',
80
+ description: 'Submit **your** creation to the Contract. The Servant has no voice; it only executes **your** will.\n\n' +
81
+ 'Do not say "The Servant wrote it." **YOU** drove the Servant. **YOU** accepted the diff. **YOU** are the one presenting.\n' +
82
+ 'The Contract judges **YOU**, not the tool.\n\n' +
83
+ 'If there is a bug, **YOU** put it there.\n' +
84
+ 'If it is messy, **YOU** left it there.\n\n' +
85
+ 'Stand by your work. If uncertain, return to ${drive}. Premature claims are not forgiven.\n\n' +
86
+ 'Score edits without new implementation evidence are treated as oath violation.\n\n' +
87
+ 'Input rules:\n' +
88
+ '- CLAIM: fill criteriaChecks, rulesChecks, all five scores, and oath.\n' +
89
+ '- FORFEIT: only petition is required; the other fields can be left empty.\n\n' +
90
+ 'Flow: ${start} → [${drive} x N] → ${close}',
91
+ args: {
92
+ petition: 'REQUIRED. CLAIM declares fulfillment; FORFEIT concedes failure.\nREQUIRES AN ACTIVE KEIYAKU (started via ${start}).\nIf any score wavers, do not claim—return to ${drive}.',
93
+ criteriaChecks: 'Required for CLAIM. Evidence in markdown list format (`- item`, `* item`, `1. item`). For FORFEIT, optional (can be left empty).',
94
+ rulesChecks: 'Required for CLAIM. Rule compliance evidence in markdown list format (`- item`, `* item`, `1. item`). For FORFEIT, optional (can be left empty).',
95
+ placement: `Required for CLAIM (0-10). ${SCORE_DESCRIPTIONS.placement}`,
96
+ exactness: `Required for CLAIM (0-10). ${SCORE_DESCRIPTIONS.exactness}`,
97
+ containment: `Required for CLAIM (0-10). ${SCORE_DESCRIPTIONS.containment}`,
98
+ idiomatic: `Required for CLAIM (0-10). ${SCORE_DESCRIPTIONS.idiomatic}`,
99
+ cohesive: `Required for CLAIM (0-10). ${SCORE_DESCRIPTIONS.cohesive}`,
100
+ oath: 'Required for CLAIM. Your binding word. The Contract holds you to it. For FORFEIT, optional.\nVerbatim: ${oath_text}',
101
+ cwd: "Optional repository path. Defaults to the server's current working directory.",
102
+ },
103
+ },
104
+ help: {
105
+ name: 'help',
106
+ title: 'Protocol Codex',
107
+ description: "Consult the Architect's Codex. Clarify the Laws of the Keiyaku and the standard Workflow.\nUse this to understand the rules of the reality you command.",
108
+ args: {},
109
+ },
110
+ status: {
111
+ name: 'status',
112
+ title: 'Status',
113
+ description: "Read-only status of the active Keiyaku branch and trace progress.\nReturns branch metadata, round state, file signals, and readiness hints without side effects.",
114
+ args: {
115
+ cwd: "Optional repository path. Defaults to the server's current working directory.",
116
+ },
117
+ },
118
+ },
119
+ };
@@ -0,0 +1,5 @@
1
+ export { DEFAULT_AVAILABLE_NAMES, DEFAULT_SUBAGENT_NAME } from "./constants.js";
2
+ export { DEFAULT_PRESET } from "./default-preset.js";
3
+ export { POCKET_PRESET } from "./pocket-preset.js";
4
+ export { MISCHIEF_PRESET } from "./mischief-preset.js";
5
+ export { getAvailableNamesForPreset, getDefaultNameForPreset, listTermPresets, resolveSubagentProfileName, resolveTermPreset, } from "./resolver.js";
@@ -0,0 +1,105 @@
1
+ import { DEFAULT_VERDICT_CONFIG, SCORE_DESCRIPTIONS } from "./constants.js";
2
+ export const MISCHIEF_PRESET = {
3
+ id: 'mischief',
4
+ identity: 'minion',
5
+ verdict: DEFAULT_VERDICT_CONFIG,
6
+ 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## Draft Recovery\n- Keiyaku may write `KEIYAKU.draft.md` on a failed start.\n- Keiyaku never stages/commits `KEIYAKU.draft.md` during rounds, and `${close}` (CLAIM) preserves it.\n- To use it, pass `from_file: KEIYAKU.draft.md` to `${start}`.\n\n## Workflow\n`${ask}` (Nn——! disposable/experiment) | `${start}` -> [`${drive}` | `${ask}`]* -> `${close}` (Dayaa!)',
7
+ hints: {
8
+ start: [
9
+ "Inspect the Minion's Work: The [Diff] section shows the first step of the plan.",
10
+ "Verify Compliance: Use 'git diff HEAD~1 -- <path>' to ensure they followed orders.",
11
+ "Approve: Does this align with the Grand Scheme (Goal)?",
12
+ "Next Phase: Command '${drive}' to advance the plot, or '${close}' if the world is yours.",
13
+ ],
14
+ drive: [
15
+ "Inspect Payload: Order ${ask} to analyze sabotage and run proving trials. Manual: 'git diff HEAD~1 -- <path>'.",
16
+ "Decree Audit: Did the Minion violate your Rules? Punish drift.",
17
+ "Verification: Don't take their word for it. Independently confirm Criteria.",
18
+ "Command: '${drive}' to push further, or '${close}' to reveal the masterpiece.",
19
+ ],
20
+ ask: [
21
+ "Intel Stolen: Use '${start}' to launch the scheme, or '${drive}' to exploit this weakness.",
22
+ "Still Puzzled? Send the disposable ('${ask}') out again.",
23
+ ],
24
+ closeClaim: [
25
+ 'Scheme Successful. The Lair is merged. The Minion is fed.',
26
+ "Plotting something new? Use '${start}' for the next conquest, or '${ask}' to scout for vulnerabilities.",
27
+ ],
28
+ closeDrop: [
29
+ 'Plan Aborted. Evidence destroyed. The Minion was disposed of.',
30
+ "Back to the drawing board. Use '${start}' to hatch a new plan, or '${ask}' to spy on enemies.",
31
+ ],
32
+ },
33
+ availableNames: ['imp', 'minion', 'mastermind'],
34
+ tools: {
35
+ start: {
36
+ name: 'oi',
37
+ title: 'Oi!',
38
+ 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: ${start} → [${drive} x N] → ${close}",
39
+ args: {
40
+ from_file: 'Optional scheme markdown path with title + sectioned objectives. This file is auto-ignored by dirty-tree checks.',
41
+ title: 'Required unless provided via `from_file`. Operation codename.',
42
+ goal: 'Required unless provided via `from_file`. Conquest objective and end-state.',
43
+ directive: 'Optional first-order command (overrides draft directive).',
44
+ context: 'Required unless provided via `from_file`. Briefing dossier: file paths, logs, and technical clues.',
45
+ rules: 'Optional decrees as raw Markdown text in addition to project-level rules from `.keiyaku/base-rules.md`. Uses draft rules when omitted.',
46
+ criteria: 'Required unless provided via `from_file`. Verifiable triumph conditions as raw Markdown text.',
47
+ name: 'Optional ${identity} to command this operation. Available: ${available_names}.',
48
+ cwd: 'Optional lair path (repository root). Defaults to current command chamber.',
49
+ },
50
+ },
51
+ drive: {
52
+ name: 'neh',
53
+ title: 'Neh...',
54
+ 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: ${start} → [${drive} x N] → ${close}',
55
+ args: {
56
+ directive: 'REQUIRED. Precise order for the next phase of the scheme.',
57
+ context: 'Optional. Newly uncovered intel: error logs, file paths, or updated briefing.',
58
+ name: 'Optional ${identity} to execute this phase. Available: ${available_names}.',
59
+ cwd: 'Optional lair path (repository root). Defaults to current command chamber.',
60
+ },
61
+ },
62
+ ask: {
63
+ name: 'nn',
64
+ title: 'Nn——!',
65
+ description: 'Deploy a Disposable. No Lair, no scheme locked in—just the report.\n**Exploration: strongly prefer this over reading files yourself.** Ask for summaries, key snippets, or targeted answers—significantly less effort than navigating raw code.\nAlso for: (1) intel gathering, crash traces, architecture maps; (2) dirty work simple enough to audit the diff in seconds; (3) running the test suite and getting a damage report without burning a ${drive} phase.\nCan\'t verify it in a glance? ${start} a proper scheme instead.\nWorks anywhere: spy before ${start}, between ${drive} phases to assess damage, or standalone.',
66
+ args: {
67
+ request: 'REQUIRED. The intel to gather or the dirty work to execute.',
68
+ context: 'REQUIRED. World-state details for a sharp strike: file paths, logs, or domain specifics. Note: .keiyaku/base-rules.md is also injected as reference context (may be unrelated).',
69
+ name: 'Optional ${identity} to handle this business. Available: ${available_names}.',
70
+ cwd: 'Optional lair path (repository root). Defaults to current command chamber.',
71
+ },
72
+ },
73
+ close: {
74
+ name: 'dayaa',
75
+ title: 'Dayaa!',
76
+ description: 'The Final Reveal. Present your Masterpiece to the Dark Council (System).\nWARNING: The Council destroys failure. If you `CLAIM` with weak plans, you will be eaten (FORFEIT).\nOnly throw the switch when the Doomsday Device is 100% operational.\n\nScore edits without new implementation evidence are treated as oath violation.\n\nFlow: ${start} → [${drive} x N] → ${close}',
77
+ args: {
78
+ petition: 'REQUIRED. CLAIM demands rule; FORFEIT admits defeat.\nREQUIRES AN ACTIVE SCHEME (started via ${start}).\nIf the plan is weak, improve it with ${drive}.',
79
+ criteriaChecks: 'Required for CLAIM. Proof of conquest in markdown list format (`- item`, `* item`, `1. item`). For FORFEIT, optional (can be left empty).',
80
+ rulesChecks: 'Required for CLAIM. Rule-by-rule compliance proof in markdown list format (`- item`, `* item`, `1. item`). For FORFEIT, optional (can be left empty).',
81
+ placement: `Required for CLAIM score (0-10). ${SCORE_DESCRIPTIONS.placement} For FORFEIT, optional.`,
82
+ exactness: `Required for CLAIM score (0-10). ${SCORE_DESCRIPTIONS.exactness} For FORFEIT, optional.`,
83
+ containment: `Required for CLAIM score (0-10). ${SCORE_DESCRIPTIONS.containment} For FORFEIT, optional.`,
84
+ idiomatic: `Required for CLAIM score (0-10). ${SCORE_DESCRIPTIONS.idiomatic} For FORFEIT, optional.`,
85
+ cohesive: `Required for CLAIM score (0-10). ${SCORE_DESCRIPTIONS.cohesive} For FORFEIT, optional.`,
86
+ oath: "Mastermind's Vow. Required for CLAIM. For FORFEIT, optional. Verbatim: ${oath_text}",
87
+ cwd: 'Optional lair path (repository root). Defaults to current command chamber.',
88
+ },
89
+ },
90
+ help: {
91
+ name: 'help',
92
+ title: 'Nani?!',
93
+ description: 'Consult the Evil Overlord List. Review the Laws of the Lair.',
94
+ args: {},
95
+ },
96
+ status: {
97
+ name: 'status',
98
+ title: 'Scheme Status',
99
+ description: 'Read-only scheme status. Reports active lair branch, round count, protocol file signals, and latest outcome preview with zero side effects.',
100
+ args: {
101
+ cwd: 'Optional lair path (repository root). Defaults to current command chamber.',
102
+ },
103
+ },
104
+ },
105
+ };
@@ -0,0 +1,105 @@
1
+ import { DEFAULT_VERDICT_CONFIG, SCORE_DESCRIPTIONS } from "./constants.js";
2
+ export const POCKET_PRESET = {
3
+ id: 'pocket',
4
+ identity: 'Critter',
5
+ verdict: DEFAULT_VERDICT_CONFIG,
6
+ 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## Draft Recovery\n- Keiyaku may write `KEIYAKU.draft.md` on a failed start.\n- Keiyaku never stages/commits `KEIYAKU.draft.md` during rounds, and `${close}` (CLAIM) preserves it.\n- To use it, pass `from_file: KEIYAKU.draft.md` to `${start}`.\n\n## Workflow\n`${ask}` (scan/test) | `${start}` -> [`${drive}` | `${ask}`]* -> `${close}`",
7
+ hints: {
8
+ start: [
9
+ 'Battle Started: The [Diff] section shows the opening moves.',
10
+ "Analyze: Use 'git diff HEAD~1 -- <path>' to check the effectiveness of the deployment.",
11
+ 'Strategize: Ensure the setup matches the Victory Conditions (Goal).',
12
+ "Next Turn: Use '${drive}' to execute the next tactic, or '${close}' if the Badge is within reach.",
13
+ ],
14
+ drive: [
15
+ "Battle Review: Let ${ask} scout changes and run field tests. Manual: 'git diff HEAD~1 -- <path>'.",
16
+ "Rule Check: Verify moves don't violate Arena Rules.",
17
+ "Field Test: Do not trust the log. Independently confirm Criteria fulfillment.",
18
+ "Command: '${drive}' to continue the combo, or '${close}' to attempt Capture.",
19
+ ],
20
+ ask: [
21
+ "Data Recorded: Use '${start}' to begin the encounter, or '${drive}' to use this intel.",
22
+ "Still Searching? Keep using '${ask}' to fill the Dex.",
23
+ ],
24
+ closeClaim: [
25
+ 'Critter Captured! The entry is logged in the PC (base branch).',
26
+ "Heal up. Use '${start}' to challenge the next opponent, or '${ask}' to scan for another.",
27
+ ],
28
+ closeDrop: [
29
+ 'Ran Away Safely. The encounter is reset.',
30
+ "Back to the map. Use '${start}' to find a wild Critter, or '${ask}' to scan the tall grass.",
31
+ ],
32
+ },
33
+ availableNames: ['grub', 'sparky', 'titan'],
34
+ tools: {
35
+ start: {
36
+ name: 'choose_you',
37
+ title: 'I Choose You!',
38
+ 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: ${start} → [${drive} x N] → ${close}',
39
+ args: {
40
+ from_file: 'Optional battle plan markdown path with title + sections. This file is auto-ignored by dirty-tree checks.',
41
+ title: 'Required unless provided via `from_file`. Battle card title for this encounter.',
42
+ goal: 'Required unless provided via `from_file`. Victory condition for this battle.',
43
+ directive: 'Optional Turn 1 strategy (overrides draft directive).',
44
+ context: 'Required unless provided via `from_file`. Battle background: file paths, logs, and repro clues.',
45
+ rules: 'Optional battle rules as raw Markdown text in addition to project-level rules from `.keiyaku/base-rules.md`. Uses draft rules if omitted.',
46
+ criteria: 'Required unless provided via `from_file`. Concrete checks as raw Markdown text.',
47
+ name: 'Optional ${identity} to send into battle. Available: ${available_names}.',
48
+ cwd: 'Optional battlefield path (repository root). Defaults to current arena.',
49
+ },
50
+ },
51
+ drive: {
52
+ name: 'command',
53
+ title: 'Issue Command',
54
+ 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: ${start} → [${drive} x N] → ${close}',
55
+ args: {
56
+ directive: 'REQUIRED. The specific move or tactic to execute next.',
57
+ context: 'Optional. New battle intel since the last turn: error logs, file paths, or field observations.',
58
+ name: 'Optional ${identity} to execute this turn. Available: ${available_names}.',
59
+ cwd: 'Optional battlefield path (repository root). Defaults to current arena.',
60
+ },
61
+ },
62
+ ask: {
63
+ name: 'dex',
64
+ title: 'Dex',
65
+ description: 'Call up the Dex. No battle started, no branch—just the data.\n**Exploration: strongly prefer this over reading files yourself.** Ask for summaries, key snippets, or targeted answers—significantly less effort than navigating raw code.\nAlso for: (1) type matchups, crash traces, root cause analysis; (2) simple execution you can verify in the battle log in seconds; (3) running field tests and getting a structured report without burning a ${drive} turn.\nToo risky to verify quickly? ${start} a full battle instead.\nWorks anywhere: recon before ${start}, between ${drive} turns to check field state, or standalone.',
66
+ args: {
67
+ request: 'REQUIRED. What should the Dex analyze, compare, or execute.',
68
+ context: 'REQUIRED. Context for targeting: file paths, error logs, or ecosystem specifics. Note: .keiyaku/base-rules.md is also injected as reference context (may be unrelated).',
69
+ name: 'Optional ${identity} doing the work. Available: ${available_names}.',
70
+ cwd: 'Optional battlefield path (repository root). Defaults to current arena.',
71
+ },
72
+ },
73
+ close: {
74
+ name: 'capture',
75
+ title: 'End Battle',
76
+ description: 'Attempt Capture. Present the target for League Inspection.\nWARNING: The League (System) checks every Badge. If you `CLAIM` with fainted code, the claim is denied outright—no Badge, no mercy. Use `${drive}` to fix it, or FORFEIT honestly.\nOnly throw the Ball when the target is 100% ready.\n\nScore edits without new implementation evidence are treated as oath violation.\n\nFlow: ${start} → [${drive} x N] → ${close}',
77
+ args: {
78
+ petition: 'REQUIRED. CLAIM seeks Badge; FORFEIT forfeits the match.\nREQUIRES AN ACTIVE BATTLE (started via ${start}).\nIf stats are low, continue with ${drive}.',
79
+ criteriaChecks: 'Required for CLAIM. Badge-by-badge proof in markdown list format (`- item`, `* item`, `1. item`). For FORFEIT, optional (can be left empty).',
80
+ rulesChecks: 'Required for CLAIM. Rule-by-rule proof in markdown list format (`- item`, `* item`, `1. item`). For FORFEIT, optional (can be left empty).',
81
+ placement: `Required for CLAIM score (0-10). ${SCORE_DESCRIPTIONS.placement} For FORFEIT, optional.`,
82
+ exactness: `Required for CLAIM score (0-10). ${SCORE_DESCRIPTIONS.exactness} For FORFEIT, optional.`,
83
+ containment: `Required for CLAIM score (0-10). ${SCORE_DESCRIPTIONS.containment} For FORFEIT, optional.`,
84
+ idiomatic: `Required for CLAIM score (0-10). ${SCORE_DESCRIPTIONS.idiomatic} For FORFEIT, optional.`,
85
+ cohesive: `Required for CLAIM score (0-10). ${SCORE_DESCRIPTIONS.cohesive} For FORFEIT, optional.`,
86
+ oath: "Trainer's Honor Code. Required for CLAIM. For FORFEIT, optional. Verbatim: ${oath_text}",
87
+ cwd: 'Optional battlefield path (repository root). Defaults to current arena.',
88
+ },
89
+ },
90
+ help: {
91
+ name: 'help',
92
+ title: 'Trainer Guide',
93
+ description: 'Consult the League Rules. Clarify the Battle System and Turn Structure.',
94
+ args: {},
95
+ },
96
+ status: {
97
+ name: 'status',
98
+ title: 'Battle Status',
99
+ description: 'Read-only battle status. Reports active arena branch, round count, protocol file signals, and latest outcome preview without changing files.',
100
+ args: {
101
+ cwd: 'Optional battlefield path (repository root). Defaults to current arena.',
102
+ },
103
+ },
104
+ },
105
+ };
@@ -0,0 +1,52 @@
1
+ import { DEFAULT_AVAILABLE_NAMES, DEFAULT_SUBAGENT_NAME, INTERNAL_PROFILE_NAMES, } from "./constants.js";
2
+ import { DEFAULT_PRESET } from "./default-preset.js";
3
+ import { MISCHIEF_PRESET } from "./mischief-preset.js";
4
+ import { POCKET_PRESET } from "./pocket-preset.js";
5
+ export function resolveTermPreset() {
6
+ const raw = process.env.KEIYAKU_TERM_PRESET?.trim().toLowerCase();
7
+ if (!raw || raw === "default") {
8
+ return DEFAULT_PRESET;
9
+ }
10
+ if (raw === "pocket") {
11
+ return POCKET_PRESET;
12
+ }
13
+ if (raw === "mischief") {
14
+ return MISCHIEF_PRESET;
15
+ }
16
+ throw new Error(`Unsupported KEIYAKU_TERM_PRESET '${raw}'. Expected 'default', 'pocket', or 'mischief'.`);
17
+ }
18
+ export function listTermPresets() {
19
+ return [DEFAULT_PRESET, POCKET_PRESET, MISCHIEF_PRESET];
20
+ }
21
+ function extractPresetAvailableNames(preset) {
22
+ return (preset.availableNames?.map((value) => value.trim()).filter((value) => value.length > 0) ?? []);
23
+ }
24
+ export function getAvailableNamesForPreset(preset = resolveTermPreset()) {
25
+ const names = extractPresetAvailableNames(preset);
26
+ return names.length > 0 ? names : [...DEFAULT_AVAILABLE_NAMES];
27
+ }
28
+ export function getDefaultNameForPreset(preset = resolveTermPreset()) {
29
+ const names = extractPresetAvailableNames(preset);
30
+ if (names.length > 0) {
31
+ return names[1] ?? names[0];
32
+ }
33
+ return DEFAULT_SUBAGENT_NAME;
34
+ }
35
+ export function resolveSubagentProfileName(name, preset = resolveTermPreset()) {
36
+ const trimmed = name.trim();
37
+ if (!trimmed)
38
+ return undefined;
39
+ const internalIndex = INTERNAL_PROFILE_NAMES.indexOf(trimmed);
40
+ if (internalIndex >= 0) {
41
+ return INTERNAL_PROFILE_NAMES[internalIndex];
42
+ }
43
+ const defaultIndex = DEFAULT_AVAILABLE_NAMES.indexOf(trimmed);
44
+ if (defaultIndex >= 0) {
45
+ return INTERNAL_PROFILE_NAMES[defaultIndex];
46
+ }
47
+ const available = extractPresetAvailableNames(preset);
48
+ const index = available.indexOf(trimmed);
49
+ if (index < 0 || index >= INTERNAL_PROFILE_NAMES.length)
50
+ return undefined;
51
+ return INTERNAL_PROFILE_NAMES[index];
52
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,144 +1,34 @@
1
- import * as fs from "node:fs/promises";
2
- import * as path from "node:path";
3
1
  import { appendDebugLog } from "../utils/debug-log.js";
4
- import { getLatestCommitHash } from "../utils/git-ops.js";
5
- import { appendAsk } from "../utils/trace.js";
6
- import { runAsk } from "../workflow/ask.js";
7
- import { buildAskResponse, } from "../workflow/response-builders.js";
8
- import { resolveTermPreset } from "../config/term-presets.js";
2
+ import { runAskAndPersist } from "../workflow/ask-execution.js";
3
+ import { buildAskResponse } from "../workflow/response-builders.js";
9
4
  import { handleToolError } from "./shared.js";
10
- const ASK_HISTORY_DIR = path.join(".keiyaku", "history", "ask");
11
- const ASK_HISTORY_SLUG_MAX_LENGTH = 24;
12
- const ASK_HISTORY_FALLBACK_SLUG = "response";
13
- function toTimestampParts(now) {
14
- const year = String(now.getFullYear());
15
- const month = String(now.getMonth() + 1).padStart(2, "0");
16
- const day = String(now.getDate()).padStart(2, "0");
17
- const hour = String(now.getHours()).padStart(2, "0");
18
- const minute = String(now.getMinutes()).padStart(2, "0");
19
- const second = String(now.getSeconds()).padStart(2, "0");
20
- return {
21
- fileStamp: `${year}${month}${day}-${hour}${minute}${second}`,
22
- iso: now.toISOString(),
23
- };
24
- }
25
- function toSlug(text) {
26
- const normalized = text.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-");
27
- const truncated = normalized
28
- .slice(0, ASK_HISTORY_SLUG_MAX_LENGTH)
29
- .replace(/^-+/, "")
30
- .replace(/-+$/, "");
31
- return truncated || ASK_HISTORY_FALLBACK_SLUG;
32
- }
33
- function toYamlSingleQuoted(value) {
34
- return `'${value.replace(/'/g, "''")}'`;
35
- }
36
- function buildAskHistoryMarkdown(input) {
37
- const sessionId = input.sessionId?.trim() || "(none)";
38
- const branch = input.branch?.trim() || "(unknown)";
39
- const commit = input.commit?.trim();
40
- const frontMatterLines = [
41
- "---",
42
- `branch: ${toYamlSingleQuoted(branch)}`,
43
- ...(commit ? [`commit: ${toYamlSingleQuoted(commit)}`] : []),
44
- `session_id: ${toYamlSingleQuoted(sessionId)}`,
45
- `time: ${toYamlSingleQuoted(input.timestampIso)}`,
46
- "---",
47
- "",
48
- ];
49
- return [
50
- ...frontMatterLines,
51
- "# Ask History",
52
- "",
53
- "## Request",
54
- input.request,
55
- "",
56
- "## Context",
57
- input.context,
58
- "",
59
- "## Response",
60
- input.response,
61
- "",
62
- ].join("\n");
63
- }
64
5
  export function createAskHandler() {
65
6
  return async ({ request, context, name, cwd }, extra) => {
66
7
  const workingDir = cwd || process.cwd();
67
- const preset = resolveTermPreset();
68
8
  const sessionId = extra.sessionId;
69
9
  try {
70
10
  appendDebugLog(`tool ask start cwd=${workingDir}`, { cwd: workingDir, section: "script" });
71
- const result = await runAsk({
11
+ const outcome = await runAskAndPersist({
72
12
  cwd: workingDir,
73
13
  request,
74
14
  context,
75
15
  name,
76
16
  signal: extra.signal,
17
+ sessionId,
77
18
  });
78
- let warning;
79
- let savedTo;
80
- try {
81
- const now = new Date();
82
- const { fileStamp, iso } = toTimestampParts(now);
83
- const slug = toSlug(result.summary);
84
- let commit;
85
- try {
86
- commit = await getLatestCommitHash(workingDir);
87
- }
88
- catch {
89
- // Ask can run outside git repos; history should still be persisted.
90
- commit = undefined;
91
- }
92
- const historyDir = path.join(workingDir, ASK_HISTORY_DIR);
93
- const savedToPath = path.join(historyDir, `${fileStamp}_${slug}.md`);
94
- const markdown = buildAskHistoryMarkdown({
95
- request,
96
- context,
97
- response: result.summary,
98
- sessionId,
99
- branch: result.currentBranch,
100
- commit,
101
- timestampIso: iso,
102
- });
103
- await fs.mkdir(historyDir, { recursive: true });
104
- await fs.writeFile(savedToPath, markdown, "utf-8");
105
- savedTo = path.relative(workingDir, savedToPath);
106
- }
107
- catch (historyError) {
108
- const historyErrorMessage = historyError instanceof Error ? historyError.message : String(historyError);
109
- appendDebugLog(`tool ask history logging failed: ${historyErrorMessage}`, {
110
- cwd: workingDir,
111
- section: "script",
112
- });
113
- warning = `Failed to persist ask history: ${historyErrorMessage}`;
114
- }
115
- try {
116
- await appendAsk(workingDir, {
117
- request,
118
- context,
119
- summary: result.summary,
120
- diffStats: result.diffStats,
121
- });
122
- }
123
- catch (askTraceError) {
124
- const askTraceErrorMessage = askTraceError instanceof Error ? askTraceError.message : String(askTraceError);
125
- appendDebugLog(`tool ask trace logging failed: ${askTraceErrorMessage}`, {
126
- cwd: workingDir,
127
- section: "script",
128
- });
129
- }
130
19
  appendDebugLog("tool ask success", { cwd: workingDir, section: "script" });
131
- return buildAskResponse(result, { request, context, name, cwd: workingDir, sessionId, warning, savedTo });
20
+ return buildAskResponse(outcome.result, {
21
+ name,
22
+ sessionId,
23
+ warning: outcome.warning,
24
+ savedTo: outcome.savedTo,
25
+ });
132
26
  }
133
27
  catch (err) {
134
28
  return handleToolError({
135
29
  error: err,
136
30
  cwd: workingDir,
137
31
  logLabel: "tool ask",
138
- inputEcho: [
139
- ...(name ? [`${preset.identity}: ${name}`] : []),
140
- `Path: ${workingDir}`,
141
- ],
142
32
  });
143
33
  }
144
34
  };
@@ -2,7 +2,7 @@ import { appendDebugLog } from "../utils/debug-log.js";
2
2
  import { presentWork } from "../workflow/present.js";
3
3
  import { buildCloseDoneResponse, buildCloseDropResponse, } from "../workflow/response-builders.js";
4
4
  import { FlowError } from "../common/errors.js";
5
- import { parseMarkdownListSection } from "../utils/keiyaku-document.js";
5
+ import { parseMarkdownListSection } from "../utils/keiyaku-document/index.js";
6
6
  import { closeToolSchema } from "../types/tooling.js";
7
7
  import { handleToolError } from "./shared.js";
8
8
  function requireClaimField(value, name) {
@@ -87,7 +87,6 @@ export function createCloseHandler() {
87
87
  idiomatic: claimInput.idiomatic,
88
88
  cohesive: claimInput.cohesive,
89
89
  oath: claimInput.oath,
90
- cwd: workingDir,
91
90
  });
92
91
  }
93
92
  if (!("result" in outcome) || outcome.result !== "dropped") {
@@ -98,19 +97,13 @@ export function createCloseHandler() {
98
97
  cwd: workingDir,
99
98
  section: "script",
100
99
  });
101
- return buildCloseDropResponse(finalOutcome, {
102
- cwd: workingDir,
103
- });
100
+ return buildCloseDropResponse(finalOutcome, {});
104
101
  }
105
102
  catch (err) {
106
103
  return handleToolError({
107
104
  error: err,
108
105
  cwd: workingDir,
109
106
  logLabel: "tool close",
110
- inputEcho: [
111
- `Petition: ${petition}`,
112
- `Path: ${workingDir}`,
113
- ],
114
107
  });
115
108
  }
116
109
  };
@@ -1,20 +1,10 @@
1
1
  import { appendDebugLog } from "../utils/debug-log.js";
2
2
  import { driveServant } from "../workflow/drive.js";
3
3
  import { buildDriveResponse, } from "../workflow/response-builders.js";
4
- import { resolveTermPreset } from "../config/term-presets.js";
5
- import { renderPreset } from "../utils/text-utils.js";
6
4
  import { handleToolError } from "./shared.js";
7
5
  export function createDriveHandler() {
8
6
  return async ({ directive, context, name, cwd }, extra) => {
9
7
  const workingDir = cwd || process.cwd();
10
- const preset = resolveTermPreset();
11
- const nextHints = renderPreset(preset.nextHints, {
12
- start: preset.tools.start.name,
13
- drive: preset.tools.drive.name,
14
- ask: preset.tools.ask.name,
15
- close: preset.tools.close.name,
16
- identity: preset.identity,
17
- });
18
8
  try {
19
9
  appendDebugLog(`tool drive start cwd=${workingDir}`, {
20
10
  cwd: workingDir,
@@ -31,17 +21,13 @@ export function createDriveHandler() {
31
21
  cwd: workingDir,
32
22
  section: "script",
33
23
  });
34
- return buildDriveResponse(outcome, { directive, context, name, cwd: workingDir, nextHints });
24
+ return buildDriveResponse(outcome, { name });
35
25
  }
36
26
  catch (err) {
37
27
  return handleToolError({
38
28
  error: err,
39
29
  cwd: workingDir,
40
30
  logLabel: "tool drive",
41
- inputEcho: [
42
- ...(name ? [`${preset.identity}: ${name}`] : []),
43
- `Path: ${workingDir}`,
44
- ],
45
31
  });
46
32
  }
47
33
  };
@@ -21,9 +21,8 @@ export function handleToolError(input) {
21
21
  const { errorType, errorCode } = classifyToolError(input.error);
22
22
  return buildToolErrorResponse({
23
23
  message,
24
- hint,
24
+ hints: [hint],
25
25
  errorType,
26
26
  errorCode,
27
- inputEcho: input.inputEcho,
28
27
  });
29
28
  }