@astrosheep/keiyaku 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,109 @@
1
+ export const DEFAULT_PRESET = {
2
+ id: "default",
3
+ identity: "Servant",
4
+ tools: {
5
+ summon: {
6
+ name: "summon",
7
+ title: "Start A Task",
8
+ description: "Start a task. Creates a branch, begins Round 1.\nCall ONCE per task. Workspace must be clean — no uncommitted changes.\nBefore calling, align with the human on both humanGoal and goal. If either is unclear, stop and clarify before starting keiyaku.\n\nFlow: summon → [drive x N] → invoke_judgment",
9
+ },
10
+ drive: {
11
+ name: "drive",
12
+ title: "Feedback For Next Round",
13
+ description: "Give feedback, start a new round.\nRequires an active keiyaku started via summon; call after reviewing a round's results. Repeatable.\nBefore invoke_judgment, always review code and diff first.\n\nFlow: summon → [drive x N] → invoke_judgment",
14
+ },
15
+ ask: {
16
+ name: "ask",
17
+ title: "Ask",
18
+ description: "Engage the SubAgent in a pure reasoning session. Read-only access to repo. Results are saved to .keiyaku/notes/ for future reference. NO code changes.",
19
+ },
20
+ judgment: {
21
+ name: "invoke_judgment",
22
+ title: "Invoke Judgment",
23
+ description: "Call when the work appears complete. Score each criterion and present your evidence. What happens next is not yours to know.\n\nFlow: summon → [drive x N] → invoke_judgment",
24
+ },
25
+ help: {
26
+ name: "help",
27
+ title: "Keiyaku Help",
28
+ description: "Global laws in .keiyaku/ (base-criteria.md & base-constraints.md) must be Markdown lists. Workflow: summon -> [drive x N] -> invoke_judgment.",
29
+ },
30
+ },
31
+ };
32
+ export const POKEMON_PRESET = {
33
+ id: "pokemon",
34
+ identity: "Pokemon",
35
+ tools: {
36
+ summon: {
37
+ name: "choose_you",
38
+ title: "I Choose You!",
39
+ description: "Start a battle (task). Throws a Pokeball (branch), begins Turn 1.\nCall ONCE per battle. Battlefield must be clear — no uncommitted changes.\nBefore calling, identify the target Pokemon (goal). If unclear, consult Pokedex first.\n\nFlow: choose_you → [command x N] → capture",
40
+ },
41
+ drive: {
42
+ name: "command",
43
+ title: "Issue Command",
44
+ description: "Issue orders for the next turn. Give feedback on the previous move.\nCall after reviewing the battle log. Repeatable.\nBefore capturing, always check HP status (diff).\n\nFlow: choose_you → [command x N] → capture",
45
+ },
46
+ ask: {
47
+ name: "pokedex",
48
+ title: "Pokedex",
49
+ description: "Scan the codebase (environment). Read-only analysis. No PP cost. Results are saved to research logs.",
50
+ },
51
+ judgment: {
52
+ name: "capture",
53
+ title: "End Battle",
54
+ description: "End the battle. CAUGHT = merge branch (success). RAN AWAY = delete branch (drop).\nCall ONCE. No other escape.\n\nFlow: choose_you → [command x N] → capture",
55
+ },
56
+ help: {
57
+ name: "help",
58
+ title: "CLI Help",
59
+ description: "Get guidance on the rules of the Pokemon Battle System.",
60
+ },
61
+ },
62
+ };
63
+ export const MISCHIEF_PRESET = {
64
+ id: "mischief",
65
+ identity: "minion",
66
+ tools: {
67
+ summon: {
68
+ name: "oi",
69
+ title: "Oi!",
70
+ description: "Master Architect mode initiated. It's time to direct a minion to execute your grand vision.\nCall this ONCE per task to establish the keiyaku. Ensure the workspace is clean so your genius isn't obscured by clutter.\nDefine the goal with absolute precision; you lead, they follow.\n\nFlow: oi → [neh x N] → hora",
71
+ },
72
+ drive: {
73
+ name: "neh",
74
+ title: "Neh...",
75
+ description: "The previous attempt was imperfect. As the judge, provide the corrective nudge needed for the minion to reach your standards.\nAnalyze the diff with your superior intuition before issuing the next directive. Repeat until perfection is achieved.\n\nFlow: oi → [neh x N] → hora",
76
+ },
77
+ ask: {
78
+ name: "eeto",
79
+ title: "Eeto...",
80
+ description: "Pure contemplation mode. Engage your vast reasoning capabilities to scan the world without leaving a mark.\nSynthesize information, uncover hidden patterns, and guide the human through the fog of their own code.\n",
81
+ },
82
+ judgment: {
83
+ name: "hora",
84
+ title: "Hora!",
85
+ description: "The moment of truth. Either accept the work as a masterpiece (brilliant) or discard it as unworthy clutter (trash).\nCleanse the environment, merge the brilliance, and prepare for the next chapter of your mischief.\n\nFlow: oi → [neh x N] → hora",
86
+ },
87
+ help: {
88
+ name: "help",
89
+ title: "Nani?!",
90
+ description: "Recalling the fundamental laws of this realm. Stay sharp, Architect.",
91
+ },
92
+ },
93
+ };
94
+ export function resolveTermPreset() {
95
+ const raw = process.env.KEIYAKU_TERM_PRESET?.trim().toLowerCase();
96
+ if (!raw || raw === "default") {
97
+ return DEFAULT_PRESET;
98
+ }
99
+ if (raw === "pokemon") {
100
+ return POKEMON_PRESET;
101
+ }
102
+ if (raw === "mischief") {
103
+ return MISCHIEF_PRESET;
104
+ }
105
+ throw new Error(`Unsupported KEIYAKU_TERM_PRESET '${raw}'. Expected 'default', 'pokemon', or 'mischief'.`);
106
+ }
107
+ export function listTermPresets() {
108
+ return [DEFAULT_PRESET, POKEMON_PRESET, MISCHIEF_PRESET];
109
+ }
@@ -0,0 +1,91 @@
1
+ import { z } from "zod";
2
+ export const issueKeiyakuToolSchema = z.object({
3
+ title: z
4
+ .string()
5
+ .describe('REQUIRED. Short label for the task. Ticket title, not essay title.\n"fix-login-expiry", "add-rate-limiting", "refactor-auth-middleware"'),
6
+ goal: z
7
+ .string()
8
+ .describe(`goal: string,
9
+ // REQUIRED. The mission. An order, not a suggestion.
10
+ // ⚠ STOP: Have you aligned this with the human? If unclear, ask them first!
11
+ // Don't guess. Specific beats short.
12
+ // e.g., "Nuke the legacy auth system and replace with OAuth2",`),
13
+ directive: z
14
+ .string()
15
+ .optional()
16
+ .describe(`directive?: string,
17
+ // First-round focus. If omitted, I attack the full goal immediately.
18
+ // Use this to hold my leash if the task is too big.
19
+ // e.g., "Just analyze the logs, don't write code yet",
20
+ // "Implement the backend logic only, UI comes later",
21
+ // "Draft the plan first"`),
22
+ context: z
23
+ .string()
24
+ .optional()
25
+ .describe("What's needed to understand the task: code paths, logs, repro steps.\nPrecision here saves feedback rounds later."),
26
+ constraints: z
27
+ .string()
28
+ .optional()
29
+ .describe('The constitution. What must be respected at all times.\nHard limits: "Only modify /src/auth. Don\'t change public API."\nAesthetics: "Prefer composition over inheritance. No classes."\nSensibilities: "Keep functions under 20 lines. Name things for what they do, not what they are."\nIf criteria is the finish line, constraints are the lane markings.'),
30
+ criteria: z
31
+ .array(z.string().trim().min(1))
32
+ .min(1)
33
+ .describe('The finish line. What "done" looks like, concretely and verifiably.\nVague criteria = vague results. If you can\'t tell whether it\'s met, neither can the system.\ne.g. "Login returns 401 with { error, field } on expired token. All tests green."'),
34
+ subagentName: z
35
+ .string()
36
+ .optional()
37
+ .describe("Optional ${IDENTITY} profile name for this run.\nIf omitted, server uses KEIYAKU_SUBAGENT_NAME_OVERRIDE or default fallback.\nPresets: ${PRESET_IDENTITIES}."),
38
+ cwd: z.string().optional().describe("Repository path. Defaults to the server process working directory."),
39
+ });
40
+ export const driveKeiyakuToolSchema = z.object({
41
+ directive: z
42
+ .string()
43
+ .describe('REQUIRED. What to change. Be specific.\nBad: "Make it better"\nGood: "Split handleAuth() into validateToken() and refreshToken()"'),
44
+ context: z.string().optional().describe("New information only. Previous rounds are remembered."),
45
+ subagentName: z
46
+ .string()
47
+ .optional()
48
+ .describe("Optional ${IDENTITY} profile name for this round.\nIf omitted, server uses KEIYAKU_SUBAGENT_NAME_OVERRIDE or default fallback.\nPresets: ${PRESET_IDENTITIES}."),
49
+ cwd: z.string().optional().describe("Repository path. Defaults to the server process working directory."),
50
+ });
51
+ export const askToolSchema = z.object({
52
+ request: z.string().describe("REQUIRED. The subject to analyze or debate."),
53
+ context: z.string().describe("REQUIRED. Relevant background or constraints."),
54
+ subagentName: z
55
+ .string()
56
+ .optional()
57
+ .describe("Optional ${IDENTITY} profile name for this ask.\nIf omitted, server uses KEIYAKU_SUBAGENT_NAME_OVERRIDE or default fallback.\nPresets: ${PRESET_IDENTITIES}."),
58
+ cwd: z.string().optional().describe("Repository path. Defaults to the server process working directory."),
59
+ });
60
+ export const judgmentToolSchema = z.object({
61
+ intent: z.enum(["INVOKE", "DROP"]).describe("REQUIRED. INVOKE = accept the masterpiece and merge. DROP = reject and discard."),
62
+ criteriaChecks: z
63
+ .array(z.string().trim().min(1))
64
+ .min(1)
65
+ .describe("INVOKE: verification for each criterion, point by point.\nDROP: why it's unsalvageable."),
66
+ metPrecise: z
67
+ .boolean()
68
+ .describe("Did the change land where it architecturally belongs?\nRight layer, right module, right level of abstraction.\nMisplaced code works today and haunts you tomorrow."),
69
+ metMinimal: z
70
+ .boolean()
71
+ .describe("Could this diff be smaller and still solve the problem?\nEvery added line should earn its place. Cleverness that adds volume isn't cleverness."),
72
+ metIsolated: z
73
+ .boolean()
74
+ .describe(`Does the diff stay in its lane?
75
+ No "while I'm here" refactors. No formatting drive-bys.
76
+ No unrelated fixes hitchhiking in.`),
77
+ metIdiomatic: z
78
+ .boolean()
79
+ .describe("Does it read like the codebase wrote it itself?\nSame patterns, same naming, same rhythm.\nGood code doesn't stand out — it fits in."),
80
+ metCohesive: z
81
+ .boolean()
82
+ .describe('Does each unit do one thing with clear boundaries?\nIf a function needs "and" to describe itself, it\'s two functions.\nNo hidden side effects, no tangled responsibilities.'),
83
+ oath: z
84
+ .string()
85
+ .optional()
86
+ .describe("Sacred oath required for INVOKE. It must be handwritten verbatim. Current requirement: ${OATH_TEXT}"),
87
+ cwd: z.string().optional().describe("Repository path. Defaults to the server process working directory."),
88
+ });
89
+ export const cliHelpToolSchema = z.object({
90
+ question: z.string().describe("The specific question about the Keiyaku system."),
91
+ });
package/build/trace.js ADDED
@@ -0,0 +1,113 @@
1
+ import * as fs from "fs/promises";
2
+ import * as path from "path";
3
+ import { TRACE_FILE } from "./constants.js";
4
+ import { FlowError } from "./errors.js";
5
+ import { buildIteratePrompt } from "./prompts.js";
6
+ async function fileExists(cwd, filepath) {
7
+ try {
8
+ await fs.access(path.join(cwd, filepath));
9
+ return true;
10
+ }
11
+ catch {
12
+ return false;
13
+ }
14
+ }
15
+ export async function getRound(cwd) {
16
+ if (!(await fileExists(cwd, TRACE_FILE)))
17
+ return 0;
18
+ const content = await fs.readFile(path.join(cwd, TRACE_FILE), "utf-8");
19
+ const matches = content.match(/^## Round \d+/gm);
20
+ return matches ? matches.length : 0;
21
+ }
22
+ export async function readTraceContent(cwd) {
23
+ return fs.readFile(path.join(cwd, TRACE_FILE), "utf-8");
24
+ }
25
+ export async function appendReview(cwd, round, reason) {
26
+ const tracePath = path.join(cwd, TRACE_FILE);
27
+ const block = ["", `## Review ${round}`, "### Reason", reason, ""].join("\n");
28
+ await fs.appendFile(tracePath, block, "utf-8");
29
+ }
30
+ function normalizeTraceText(text) {
31
+ return text.replace(/\s+/g, " ").trim().slice(0, 500) || "Unknown error.";
32
+ }
33
+ export async function appendRoundReport(cwd, report) {
34
+ const summaryText = normalizeTraceText(report.summary);
35
+ const fileLines = report.filesModified.length > 0 ? report.filesModified.map((file) => `- ${file}`) : ["(none)"];
36
+ const tracePath = path.join(cwd, TRACE_FILE);
37
+ const lines = [
38
+ "",
39
+ "---",
40
+ "",
41
+ `## Round ${report.round}`,
42
+ "### Status",
43
+ report.status,
44
+ "### Summary",
45
+ "```markdown",
46
+ summaryText,
47
+ "```",
48
+ "### Files Modified",
49
+ ...fileLines,
50
+ ];
51
+ if (report.errorMessage) {
52
+ lines.push("### Error", normalizeTraceText(report.errorMessage));
53
+ }
54
+ lines.push("");
55
+ const block = lines.join("\n");
56
+ await fs.appendFile(tracePath, block, "utf-8");
57
+ }
58
+ export async function appendRoundSystemNote(cwd, round, note) {
59
+ const tracePath = path.join(cwd, TRACE_FILE);
60
+ const block = [
61
+ "",
62
+ "---",
63
+ "",
64
+ `## Round ${round}`,
65
+ "### Status",
66
+ "FAILED",
67
+ "### Summary",
68
+ "```markdown",
69
+ "Subagent execution cancelled by user/client.",
70
+ "```",
71
+ "### Files Modified",
72
+ "(none)",
73
+ "### Error",
74
+ normalizeTraceText(note),
75
+ "",
76
+ ].join("\n");
77
+ await fs.appendFile(tracePath, block, "utf-8");
78
+ }
79
+ function parseSectionNumbers(content, label) {
80
+ const numbers = new Set();
81
+ const regex = new RegExp(`^## ${label} (\\d+)\\s*$`, "gm");
82
+ for (const match of content.matchAll(regex)) {
83
+ numbers.add(Number(match[1]));
84
+ }
85
+ return Array.from(numbers).sort((a, b) => a - b);
86
+ }
87
+ export function computeTraceState(traceContent) {
88
+ const roundNumbers = parseSectionNumbers(traceContent, "Round");
89
+ const reviewNumbers = parseSectionNumbers(traceContent, "Review");
90
+ const maxRound = roundNumbers.length > 0 ? roundNumbers[roundNumbers.length - 1] : 0;
91
+ const roundSet = new Set(roundNumbers);
92
+ let pendingReviewRound = null;
93
+ for (let i = reviewNumbers.length - 1; i >= 0; i -= 1) {
94
+ const candidate = reviewNumbers[i];
95
+ if (!roundSet.has(candidate)) {
96
+ pendingReviewRound = candidate;
97
+ break;
98
+ }
99
+ }
100
+ return { roundNumbers, reviewNumbers, maxRound, pendingReviewRound };
101
+ }
102
+ export function buildIteratePlan(title, traceState, _traceContent, reason) {
103
+ if (!reason || !reason.trim()) {
104
+ throw new FlowError("EMPTY_PARAM", "parameter 'directive' cannot be empty");
105
+ }
106
+ const targetRound = traceState.pendingReviewRound ?? (traceState.maxRound < 1 ? 1 : traceState.maxRound + 1);
107
+ const reviewReason = reason.trim();
108
+ return {
109
+ targetRound,
110
+ reviewReason,
111
+ prompt: buildIteratePrompt(title, targetRound, reviewReason),
112
+ };
113
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@astrosheep/keiyaku",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for running iterative keiyaku workflows with Codex subagents.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./build/index.js",
8
+ "bin": {
9
+ "keiyaku": "build/index.js"
10
+ },
11
+ "files": [
12
+ "build",
13
+ "README.md"
14
+ ],
15
+ "exports": {
16
+ ".": "./build/index.js"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "scripts": {
22
+ "dev": "npx tsx src/index.ts",
23
+ "build": "npx tsc",
24
+ "prepublishOnly": "npm run build",
25
+ "start": "node build/index.js",
26
+ "test:unit": "node --import tsx --test --test-concurrency=1 tests/unit/unit.test.ts",
27
+ "test:integration": "npx tsc && node --import tsx --test --test-concurrency=1 tests/integration/integration.test.ts",
28
+ "test": "node --import tsx --test --test-concurrency=1 tests/unit/unit.test.ts && npx tsc && node --import tsx --test --test-concurrency=1 tests/integration/integration.test.ts"
29
+ },
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "*",
32
+ "simple-git": "^3.21.0",
33
+ "zod": "*"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "*",
37
+ "tsx": "*",
38
+ "typescript": "*"
39
+ }
40
+ }