@astrosheep/keiyaku 0.1.0 → 0.1.1
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 +34 -10
- package/build/agents/codex-exec.js +33 -0
- package/build/agents/gemini-exec.js +37 -0
- package/build/agents/index.js +39 -0
- package/build/agents/process-runner.js +207 -0
- package/build/agents/round-runner.js +53 -0
- package/build/agents/selector.js +25 -0
- package/build/agents/string-tail-buffer.js +44 -0
- package/build/agents/types.js +19 -0
- package/build/common/constants.js +3 -0
- package/build/common/errors.js +119 -0
- package/build/config/term-presets.js +263 -0
- package/build/constants.js +1 -1
- package/build/errors.js +7 -7
- package/build/git.js +17 -1
- package/build/handlers/ask.js +44 -0
- package/build/handlers/close.js +88 -0
- package/build/handlers/delegate.js +69 -0
- package/build/handlers/drive.js +50 -0
- package/build/handlers/help.js +45 -0
- package/build/handlers/index.js +5 -0
- package/build/handlers/shared.js +14 -0
- package/build/handlers/start.js +68 -0
- package/build/index.js +47 -312
- package/build/logic.js +34 -190
- package/build/oath.js +48 -0
- package/build/response-builders.js +21 -13
- package/build/subagent-exec/index.js +4 -4
- package/build/subagent-exec/process-runner.js +1 -1
- package/build/subagent-exec/round-runner.js +55 -0
- package/build/subagent-exec/selector.js +25 -0
- package/build/term-presets.js +166 -12
- package/build/text-utils.js +83 -0
- package/build/tool-schemas.js +10 -8
- package/build/types/tool-schemas.js +93 -0
- package/build/types/workflow-types.js +1 -0
- package/build/utils/debug-log.js +36 -0
- package/build/utils/git.js +498 -0
- package/build/utils/text-utils.js +83 -0
- package/build/utils/trace.js +113 -0
- package/build/workflow/oath.js +48 -0
- package/build/workflow/orchestrator.js +431 -0
- package/build/workflow/prompts.js +60 -0
- package/build/workflow/response-builders.js +206 -0
- package/build/workflow-types.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,9 +15,11 @@ Instead of messy, unstructured chat, Keiyaku turns every task into a formal "Kei
|
|
|
15
15
|
|
|
16
16
|
The workflow is a simple, non-negotiable loop:
|
|
17
17
|
|
|
18
|
-
1.
|
|
19
|
-
2.
|
|
20
|
-
3.
|
|
18
|
+
1. **Start**: Initiate the keiyaku. Creates a branch, locks the mission in `KEIYAKU.md`, and starts Round 1.
|
|
19
|
+
2. **Iterate** (N times): Review the work, give feedback, and launch the next round.
|
|
20
|
+
3. **Verdict**: The final verdict. `DONE` (merge & clean) or `DROP` (nuke the branch).
|
|
21
|
+
|
|
22
|
+
> Note: Tool names are preset-dependent. The defaults are shown in the next section.
|
|
21
23
|
|
|
22
24
|
## 🛠 Available Tools
|
|
23
25
|
|
|
@@ -26,15 +28,37 @@ The workflow is a simple, non-negotiable loop:
|
|
|
26
28
|
| **`summon`** | Start | Define the goal, constraints, and criteria. Starts the first round. |
|
|
27
29
|
| **`drive`** | Iterate | Provide feedback based on the previous round's output. |
|
|
28
30
|
| **`ask`** | Reason | Pure read-only analysis session. No code changes, just brain power. |
|
|
29
|
-
| **`
|
|
31
|
+
| **`request_verdict`** | Finish | Finalize the task. Requires a quality check (the "Oath"). |
|
|
32
|
+
| **`help`** | Help | Show the current rules + preset usage guide. |
|
|
30
33
|
|
|
31
34
|
## 🎨 Flavor Your Workflow
|
|
32
35
|
|
|
33
|
-
Bored with generic tool names? Keiyaku supports **Term Presets**.
|
|
36
|
+
Bored with generic tool names? Keiyaku supports **Term Presets**.
|
|
37
|
+
|
|
38
|
+
### How to set
|
|
39
|
+
|
|
40
|
+
Set `KEIYAKU_TERM_PRESET` in the MCP server env (recommended), or in your shell before launching the server.
|
|
34
41
|
|
|
35
|
-
-
|
|
42
|
+
- **Valid values**: `default`, `pokemon`, `mischief` (case-insensitive). If omitted, defaults to `default`.
|
|
43
|
+
|
|
44
|
+
- **`default`**: `summon` → `drive` → `request_verdict` (Professional)
|
|
36
45
|
- **`pokemon`**: `choose_you` → `command` → `capture` (Gotta code 'em all)
|
|
37
|
-
- **`
|
|
46
|
+
- **`mischief`**: `oi` → `neh` → `hora` (For those who like a little attitude)
|
|
47
|
+
|
|
48
|
+
`ask` is also renamed by preset (`ask` / `pokedex` / `eeto`). `help` stays `help` across presets.
|
|
49
|
+
|
|
50
|
+
### What it changes (and what it doesn't)
|
|
51
|
+
|
|
52
|
+
- **Changes**: tool names/titles/descriptions, the "identity" label, and the set of allowed profile display names (see next section).
|
|
53
|
+
- **Doesn't change**: core behavior (branching, protocol files, verdict rules).
|
|
54
|
+
|
|
55
|
+
### Choose a profile name
|
|
56
|
+
|
|
57
|
+
Each run/round can pick a profile via tool input `name`, or globally via `KEIYAKU_SUBAGENT_NAME_OVERRIDE`.
|
|
58
|
+
|
|
59
|
+
- `default`: `servant-tier-b`, `servant-tier-a`, `servant-tier-s` (also accepts internal names `agent-a|agent-b|agent-c`)
|
|
60
|
+
- `pokemon`: `caterpie`, `pikachu`, `mewtwo`
|
|
61
|
+
- `mischief`: `imp`, `minion`, `mastermind`
|
|
38
62
|
|
|
39
63
|
## 📦 Setup
|
|
40
64
|
|
|
@@ -53,7 +77,7 @@ Add this to your `claude_desktop_config.json`:
|
|
|
53
77
|
"command": "npx",
|
|
54
78
|
"args": ["-y", "keiyaku"],
|
|
55
79
|
"env": {
|
|
56
|
-
"KEIYAKU_TERM_PRESET": "
|
|
80
|
+
"KEIYAKU_TERM_PRESET": "mischief"
|
|
57
81
|
}
|
|
58
82
|
}
|
|
59
83
|
}
|
|
@@ -66,7 +90,7 @@ When a keiyaku is active, two files are maintained in your repo:
|
|
|
66
90
|
- `KEIYAKU.md`: The immutable "Constitution" of the task.
|
|
67
91
|
- `KEIYAKU_TRACE.md`: The history of every round, feedback, and result.
|
|
68
92
|
|
|
69
|
-
*Note: These files are automatically cleaned up (or committed) when you `
|
|
93
|
+
*Note: These files are automatically cleaned up (or committed) when you `request_verdict` (or preset equivalent) the keiyaku.*
|
|
70
94
|
|
|
71
95
|
---
|
|
72
|
-
"Keep your branches clean and your minions in line." —
|
|
96
|
+
"Keep your branches clean and your minions in line." — Mischief preset
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { runSubagentProcess, resolvePositiveIntFromEnv, resolvePrefixedCommand } from "./process-runner.js";
|
|
2
|
+
const DEFAULT_CODEX_EXEC_TIMEOUT_MS = 20 * 60 * 1000;
|
|
3
|
+
const DEFAULT_CODEX_EXEC_MAX_CAPTURE_CHARS = 200_000;
|
|
4
|
+
export async function runCodexExec(prompt, cwd, options = {}) {
|
|
5
|
+
const timeoutMs = resolvePositiveIntFromEnv("KEIYAKU_CODEX_EXEC_TIMEOUT_MS", DEFAULT_CODEX_EXEC_TIMEOUT_MS);
|
|
6
|
+
const maxCaptureChars = resolvePositiveIntFromEnv("KEIYAKU_CODEX_EXEC_MAX_CAPTURE_CHARS", DEFAULT_CODEX_EXEC_MAX_CAPTURE_CHARS);
|
|
7
|
+
const codexArgs = ["exec", "--full-auto", "-C", cwd];
|
|
8
|
+
if (options.model) {
|
|
9
|
+
codexArgs.push("-m", options.model);
|
|
10
|
+
}
|
|
11
|
+
if (options.effort) {
|
|
12
|
+
codexArgs.push("-c", `model_reasoning_effort=${options.effort}`);
|
|
13
|
+
}
|
|
14
|
+
codexArgs.push("-");
|
|
15
|
+
const { command, args } = resolvePrefixedCommand("codex", codexArgs, process.env.KEIYAKU_CODEX_EXEC_PREFIX);
|
|
16
|
+
const { stdout, stderr } = await runSubagentProcess({
|
|
17
|
+
runnerName: "runCodexExec",
|
|
18
|
+
provider: "codex",
|
|
19
|
+
command,
|
|
20
|
+
args,
|
|
21
|
+
cwd,
|
|
22
|
+
prompt,
|
|
23
|
+
timeoutMs,
|
|
24
|
+
maxCaptureChars,
|
|
25
|
+
signal: options.signal,
|
|
26
|
+
startDetails: `model=${options.model ?? "default"} effort=${options.effort ?? "default"}`,
|
|
27
|
+
});
|
|
28
|
+
return {
|
|
29
|
+
finalMessage: stdout.trim(),
|
|
30
|
+
stdout,
|
|
31
|
+
stderr,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { runSubagentProcess, resolvePositiveIntFromEnv, resolvePrefixedCommand } from "./process-runner.js";
|
|
2
|
+
const DEFAULT_GEMINI_EXEC_TIMEOUT_MS = 20 * 60 * 1000;
|
|
3
|
+
const DEFAULT_GEMINI_EXEC_MAX_CAPTURE_CHARS = 200_000;
|
|
4
|
+
export async function runGeminiExec(prompt, cwd, options = {}) {
|
|
5
|
+
const timeoutMs = resolvePositiveIntFromEnv("KEIYAKU_GEMINI_EXEC_TIMEOUT_MS", DEFAULT_GEMINI_EXEC_TIMEOUT_MS);
|
|
6
|
+
const maxCaptureChars = resolvePositiveIntFromEnv("KEIYAKU_GEMINI_EXEC_MAX_CAPTURE_CHARS", DEFAULT_GEMINI_EXEC_MAX_CAPTURE_CHARS);
|
|
7
|
+
const geminiArgs = ["--approval-mode", "yolo", "--output-format", "json"];
|
|
8
|
+
if (options.model) {
|
|
9
|
+
geminiArgs.push("-m", options.model);
|
|
10
|
+
}
|
|
11
|
+
// Prompt is passed via stdin to avoid command-line length limits.
|
|
12
|
+
geminiArgs.push("--prompt", "-");
|
|
13
|
+
const { command, args } = resolvePrefixedCommand("gemini", geminiArgs, process.env.KEIYAKU_GEMINI_EXEC_PREFIX);
|
|
14
|
+
const { stdout, stderr } = await runSubagentProcess({
|
|
15
|
+
runnerName: "runGeminiExec",
|
|
16
|
+
provider: "gemini",
|
|
17
|
+
command,
|
|
18
|
+
args,
|
|
19
|
+
cwd,
|
|
20
|
+
prompt,
|
|
21
|
+
timeoutMs,
|
|
22
|
+
maxCaptureChars,
|
|
23
|
+
signal: options.signal,
|
|
24
|
+
startDetails: `model=${options.model ?? "default"}`,
|
|
25
|
+
});
|
|
26
|
+
let finalMessage = stdout.trim();
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(stdout);
|
|
29
|
+
if (parsed.response && typeof parsed.response === "string") {
|
|
30
|
+
finalMessage = parsed.response;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Fall back to raw stdout when Gemini output is not valid JSON.
|
|
35
|
+
}
|
|
36
|
+
return { finalMessage, stdout, stderr };
|
|
37
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { FlowError } from "../common/errors.js";
|
|
2
|
+
import { runCodexExec } from "./codex-exec.js";
|
|
3
|
+
import { runGeminiExec } from "./gemini-exec.js";
|
|
4
|
+
import { isSubagentExecError, SubagentExecError } from "./types.js";
|
|
5
|
+
const CODEX_MODEL = "gpt-5.3-codex";
|
|
6
|
+
const GEMINI_MODEL = "gemini-3-flash-preview";
|
|
7
|
+
const SUBAGENT_PROFILES = {
|
|
8
|
+
"agent-a": { provider: "codex", model: CODEX_MODEL, effort: "low" },
|
|
9
|
+
"agent-b": { provider: "codex", model: CODEX_MODEL, effort: "medium" },
|
|
10
|
+
"agent-c": { provider: "codex", model: CODEX_MODEL, effort: "high" },
|
|
11
|
+
};
|
|
12
|
+
export { SubagentExecError, isSubagentExecError };
|
|
13
|
+
export function getSubagentNames() {
|
|
14
|
+
return Object.keys(SUBAGENT_PROFILES);
|
|
15
|
+
}
|
|
16
|
+
export function resolveSubagentConfig(name) {
|
|
17
|
+
const config = SUBAGENT_PROFILES[name];
|
|
18
|
+
if (!config) {
|
|
19
|
+
throw new FlowError("UNKNOWN_SUBAGENT", `Unknown subagent '${name}'. Expected one of: ${getSubagentNames().join(", ")}`);
|
|
20
|
+
}
|
|
21
|
+
return config;
|
|
22
|
+
}
|
|
23
|
+
export async function runSubagentExec(name, prompt, cwd, options = {}) {
|
|
24
|
+
const config = resolveSubagentConfig(name);
|
|
25
|
+
if (config.provider === "codex") {
|
|
26
|
+
return runCodexExec(prompt, cwd, {
|
|
27
|
+
model: config.model,
|
|
28
|
+
effort: config.effort,
|
|
29
|
+
signal: options.signal,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
if (config.provider === "gemini") {
|
|
33
|
+
return runGeminiExec(prompt, cwd, {
|
|
34
|
+
model: config.model,
|
|
35
|
+
signal: options.signal,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
throw new SubagentExecError("SUBAGENT_EXEC_ERROR", `Unsupported subagent provider '${String(config.provider)}'.`);
|
|
39
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { appendDebugLog } from "../utils/debug-log.js";
|
|
3
|
+
import { StringTailBuffer } from "./string-tail-buffer.js";
|
|
4
|
+
import { SubagentExecError } from "./types.js";
|
|
5
|
+
function shellEscape(arg) {
|
|
6
|
+
return `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
7
|
+
}
|
|
8
|
+
function createAbortError(message) {
|
|
9
|
+
const error = new Error(message);
|
|
10
|
+
error.name = "AbortError";
|
|
11
|
+
return error;
|
|
12
|
+
}
|
|
13
|
+
function createStderrStreamId() {
|
|
14
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
15
|
+
}
|
|
16
|
+
function renderTruncationSuffix(truncatedChars) {
|
|
17
|
+
if (truncatedChars <= 0)
|
|
18
|
+
return "";
|
|
19
|
+
return `\n...[truncated ${truncatedChars} chars from start]...`;
|
|
20
|
+
}
|
|
21
|
+
function terminateChildTree(pid, detached, signal) {
|
|
22
|
+
if (!pid)
|
|
23
|
+
return;
|
|
24
|
+
if (detached && process.platform !== "win32") {
|
|
25
|
+
try {
|
|
26
|
+
process.kill(-pid, signal);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Fall through to direct child kill when process group kill fails.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
process.kill(pid, signal);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Best effort shutdown.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function getCapturedOutput(stdoutBuffer, stderrBuffer) {
|
|
41
|
+
const stdoutSnapshot = stdoutBuffer.snapshot();
|
|
42
|
+
const stderrSnapshot = stderrBuffer.snapshot();
|
|
43
|
+
return {
|
|
44
|
+
stdout: `${stdoutSnapshot.text}${renderTruncationSuffix(stdoutSnapshot.truncatedChars)}`,
|
|
45
|
+
stderr: `${stderrSnapshot.text}${renderTruncationSuffix(stderrSnapshot.truncatedChars)}`,
|
|
46
|
+
stdoutSnapshot,
|
|
47
|
+
stderrSnapshot,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function resolvePositiveIntFromEnv(envName, fallback) {
|
|
51
|
+
const value = process.env[envName]?.trim();
|
|
52
|
+
if (!value)
|
|
53
|
+
return fallback;
|
|
54
|
+
const parsed = Number(value);
|
|
55
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
56
|
+
return fallback;
|
|
57
|
+
return Math.floor(parsed);
|
|
58
|
+
}
|
|
59
|
+
export function resolvePrefixedCommand(binary, binaryArgs, commandPrefix) {
|
|
60
|
+
const prefix = commandPrefix?.trim();
|
|
61
|
+
if (!prefix) {
|
|
62
|
+
return { command: binary, args: binaryArgs };
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
command: "sh",
|
|
66
|
+
args: ["-lc", `${prefix} ${binary} ${binaryArgs.map(shellEscape).join(" ")}`],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export async function runSubagentProcess(options) {
|
|
70
|
+
const stderrStreamId = createStderrStreamId();
|
|
71
|
+
let stderrStreamed = false;
|
|
72
|
+
const stderrSection = `${options.provider}-stderr`;
|
|
73
|
+
const prefixStderrChunk = (chunk) => {
|
|
74
|
+
const normalized = chunk.replace(/\r/g, "");
|
|
75
|
+
const lines = normalized.split("\n");
|
|
76
|
+
return lines.map((line) => `[stderr:${stderrStreamId}] ${line}`).join("\n");
|
|
77
|
+
};
|
|
78
|
+
try {
|
|
79
|
+
const detailsSuffix = options.startDetails ? ` ${options.startDetails}` : "";
|
|
80
|
+
appendDebugLog(`${options.runnerName} start: command=${options.command} args=${JSON.stringify(options.args)} timeoutMs=${options.timeoutMs} maxCaptureChars=${options.maxCaptureChars}${detailsSuffix}`, { cwd: options.cwd, section: options.provider });
|
|
81
|
+
const { stdout, stderr } = await new Promise((resolve, reject) => {
|
|
82
|
+
const detached = process.platform !== "win32";
|
|
83
|
+
const child = spawn(options.command, options.args, { cwd: options.cwd, detached });
|
|
84
|
+
const stdoutBuffer = new StringTailBuffer(options.maxCaptureChars);
|
|
85
|
+
const stderrBuffer = new StringTailBuffer(options.maxCaptureChars);
|
|
86
|
+
let stdoutTotalBytes = 0;
|
|
87
|
+
let stderrTotalBytes = 0;
|
|
88
|
+
let finished = false;
|
|
89
|
+
let forceKillTimer;
|
|
90
|
+
const finishWithError = (error) => {
|
|
91
|
+
if (finished)
|
|
92
|
+
return;
|
|
93
|
+
finished = true;
|
|
94
|
+
clearTimeout(timeout);
|
|
95
|
+
if (forceKillTimer)
|
|
96
|
+
clearTimeout(forceKillTimer);
|
|
97
|
+
if (options.signal && abortHandler) {
|
|
98
|
+
options.signal.removeEventListener("abort", abortHandler);
|
|
99
|
+
}
|
|
100
|
+
reject(error);
|
|
101
|
+
};
|
|
102
|
+
const requestStop = (error) => {
|
|
103
|
+
if (finished)
|
|
104
|
+
return;
|
|
105
|
+
appendDebugLog(`${options.runnerName} stop requested: ${error.message}`, { cwd: options.cwd, section: options.provider });
|
|
106
|
+
terminateChildTree(child.pid, detached, "SIGTERM");
|
|
107
|
+
forceKillTimer = setTimeout(() => {
|
|
108
|
+
terminateChildTree(child.pid, detached, "SIGKILL");
|
|
109
|
+
}, 5000);
|
|
110
|
+
finishWithError(error);
|
|
111
|
+
};
|
|
112
|
+
const timeout = setTimeout(() => {
|
|
113
|
+
requestStop((() => {
|
|
114
|
+
const captured = getCapturedOutput(stdoutBuffer, stderrBuffer);
|
|
115
|
+
return new SubagentExecError("SUBAGENT_TIMEOUT", `${options.provider} exec timed out`, {
|
|
116
|
+
timeoutMs: options.timeoutMs,
|
|
117
|
+
stdout: captured.stdout,
|
|
118
|
+
stderr: captured.stderr,
|
|
119
|
+
});
|
|
120
|
+
})());
|
|
121
|
+
}, options.timeoutMs);
|
|
122
|
+
const abortHandler = () => {
|
|
123
|
+
requestStop(createAbortError(`${options.provider} exec cancelled by client`));
|
|
124
|
+
};
|
|
125
|
+
if (options.signal) {
|
|
126
|
+
if (options.signal.aborted) {
|
|
127
|
+
requestStop(createAbortError(`${options.provider} exec cancelled by client`));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
options.signal.addEventListener("abort", abortHandler, { once: true });
|
|
131
|
+
}
|
|
132
|
+
child.stdout.on("data", (chunk) => {
|
|
133
|
+
const text = String(chunk);
|
|
134
|
+
stdoutTotalBytes += Buffer.byteLength(text);
|
|
135
|
+
stdoutBuffer.append(text);
|
|
136
|
+
});
|
|
137
|
+
child.stderr.on("data", (chunk) => {
|
|
138
|
+
const text = String(chunk);
|
|
139
|
+
stderrTotalBytes += Buffer.byteLength(text);
|
|
140
|
+
stderrBuffer.append(text);
|
|
141
|
+
stderrStreamed = true;
|
|
142
|
+
appendDebugLog(prefixStderrChunk(text), { cwd: options.cwd, section: stderrSection });
|
|
143
|
+
});
|
|
144
|
+
child.on("error", (err) => {
|
|
145
|
+
appendDebugLog(`${options.runnerName} spawn error: ${err.message}`, { cwd: options.cwd, section: options.provider });
|
|
146
|
+
finishWithError((() => {
|
|
147
|
+
const captured = getCapturedOutput(stdoutBuffer, stderrBuffer);
|
|
148
|
+
return new SubagentExecError("SUBAGENT_SPAWN_ERROR", `${options.provider} exec spawn error: ${err.message}`, {
|
|
149
|
+
timeoutMs: options.timeoutMs,
|
|
150
|
+
stdout: captured.stdout,
|
|
151
|
+
stderr: captured.stderr,
|
|
152
|
+
cause: err,
|
|
153
|
+
});
|
|
154
|
+
})());
|
|
155
|
+
});
|
|
156
|
+
child.on("close", (code) => {
|
|
157
|
+
clearTimeout(timeout);
|
|
158
|
+
if (forceKillTimer)
|
|
159
|
+
clearTimeout(forceKillTimer);
|
|
160
|
+
if (options.signal) {
|
|
161
|
+
options.signal.removeEventListener("abort", abortHandler);
|
|
162
|
+
}
|
|
163
|
+
if (finished)
|
|
164
|
+
return;
|
|
165
|
+
finished = true;
|
|
166
|
+
const captured = getCapturedOutput(stdoutBuffer, stderrBuffer);
|
|
167
|
+
appendDebugLog(`${options.runnerName} close: code=${code} stdoutBytes=${stdoutTotalBytes} stderrBytes=${stderrTotalBytes} stdoutTruncatedChars=${captured.stdoutSnapshot.truncatedChars} stderrTruncatedChars=${captured.stderrSnapshot.truncatedChars}`, { cwd: options.cwd, section: options.provider });
|
|
168
|
+
if (code === 0) {
|
|
169
|
+
resolve({ stdout: captured.stdout, stderr: captured.stderr });
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const errorText = captured.stderr || captured.stdout || `${options.provider} exec exited with code ${code}`;
|
|
173
|
+
reject(new SubagentExecError("SUBAGENT_NON_ZERO_EXIT", errorText, {
|
|
174
|
+
timeoutMs: options.timeoutMs,
|
|
175
|
+
stdout: captured.stdout,
|
|
176
|
+
stderr: captured.stderr,
|
|
177
|
+
exitCode: code,
|
|
178
|
+
}));
|
|
179
|
+
});
|
|
180
|
+
child.stdin.end(options.prompt);
|
|
181
|
+
});
|
|
182
|
+
if (!stderrStreamed && stderr.trim()) {
|
|
183
|
+
appendDebugLog(`[stderr:${stderrStreamId}] ${stderr.trim()}`, { cwd: options.cwd, section: stderrSection });
|
|
184
|
+
}
|
|
185
|
+
return { stdout, stderr };
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
if (error instanceof Error) {
|
|
189
|
+
appendDebugLog(`${options.runnerName} error: ${error.name}: ${error.message}`, { cwd: options.cwd, section: options.provider });
|
|
190
|
+
if (error instanceof SubagentExecError) {
|
|
191
|
+
if (!stderrStreamed && error.stderr.trim()) {
|
|
192
|
+
appendDebugLog(`[stderr:${stderrStreamId}] ${error.stderr.trim()}`, { cwd: options.cwd, section: stderrSection });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
if (error instanceof SubagentExecError) {
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
const message = error instanceof Error ? error.message : `Unknown ${options.provider} exec error`;
|
|
203
|
+
throw new SubagentExecError("SUBAGENT_EXEC_ERROR", `${options.provider} exec failed: ${message}`, {
|
|
204
|
+
timeoutMs: options.timeoutMs,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { appendDebugBlock, appendDebugLog } from "../utils/debug-log.js";
|
|
2
|
+
import { asMessage } from "../common/errors.js";
|
|
3
|
+
import { resolveTermPreset } from "../config/term-presets.js";
|
|
4
|
+
import { isSubagentExecError, runSubagentExec } from "./index.js";
|
|
5
|
+
function toSnippet(raw, maxChars = 4000) {
|
|
6
|
+
const text = raw.trim();
|
|
7
|
+
if (!text)
|
|
8
|
+
return undefined;
|
|
9
|
+
if (text.length <= maxChars)
|
|
10
|
+
return text;
|
|
11
|
+
const marker = `\n...[truncated ${text.length - maxChars} chars]...\n`;
|
|
12
|
+
const side = Math.floor((maxChars - marker.length) / 2);
|
|
13
|
+
const head = text.slice(0, side);
|
|
14
|
+
const tail = text.slice(text.length - side);
|
|
15
|
+
return `${head}${marker}${tail}`;
|
|
16
|
+
}
|
|
17
|
+
export function describeSubagentFailure(error, name, round) {
|
|
18
|
+
const message = asMessage(error);
|
|
19
|
+
if (isSubagentExecError(error)) {
|
|
20
|
+
return {
|
|
21
|
+
errorCode: error.code,
|
|
22
|
+
message: error.message,
|
|
23
|
+
name,
|
|
24
|
+
round,
|
|
25
|
+
timeoutMs: error.timeoutMs,
|
|
26
|
+
exitCode: error.exitCode,
|
|
27
|
+
stderrSnippet: toSnippet(error.stderr),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
errorCode: "SUBAGENT_EXEC_ERROR",
|
|
32
|
+
message,
|
|
33
|
+
name,
|
|
34
|
+
round,
|
|
35
|
+
timeoutMs: null,
|
|
36
|
+
exitCode: null,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export async function runSubagent(subagent, prompt, cwd, round, signal) {
|
|
40
|
+
const { identity } = resolveTermPreset();
|
|
41
|
+
if (process.env.KEIYAKU_FAKE_SUBAGENT === "1") {
|
|
42
|
+
return `Mock ${identity} '${subagent.displayName}' completed round ${round}.`;
|
|
43
|
+
}
|
|
44
|
+
const startLog = `[${identity}] Running execution for '${subagent.displayName}' in ${cwd}`;
|
|
45
|
+
console.error(startLog);
|
|
46
|
+
appendDebugLog(startLog, { cwd, section: "script" });
|
|
47
|
+
const result = await runSubagentExec(subagent.profileName, prompt, cwd, { signal });
|
|
48
|
+
const summary = result.finalMessage || `${identity} completed successfully.`;
|
|
49
|
+
if (result.stderr.trim()) {
|
|
50
|
+
appendDebugBlock("subagent stderr", result.stderr, { cwd, section: "codex-stderr" });
|
|
51
|
+
}
|
|
52
|
+
return summary;
|
|
53
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { FlowError } from "../common/errors.js";
|
|
2
|
+
import { getAvailableNamesForPreset, getDefaultNameForPreset, resolveSubagentProfileName, resolveTermPreset, } from "../config/term-presets.js";
|
|
3
|
+
import { resolveSubagentConfig } from "./index.js";
|
|
4
|
+
function resolveKnownSubagent(name, preset) {
|
|
5
|
+
const profileName = resolveSubagentProfileName(name, preset);
|
|
6
|
+
if (!profileName) {
|
|
7
|
+
throw new FlowError("UNKNOWN_SUBAGENT", `Unknown subagent '${name}'. Expected one of: ${getAvailableNamesForPreset(preset).join(", ")}`);
|
|
8
|
+
}
|
|
9
|
+
resolveSubagentConfig(profileName);
|
|
10
|
+
return { displayName: name, profileName };
|
|
11
|
+
}
|
|
12
|
+
function resolveConfiguredSubagent(preset) {
|
|
13
|
+
const fromEnv = process.env.KEIYAKU_SUBAGENT_NAME_OVERRIDE?.trim();
|
|
14
|
+
if (!fromEnv)
|
|
15
|
+
return undefined;
|
|
16
|
+
return resolveKnownSubagent(fromEnv, preset);
|
|
17
|
+
}
|
|
18
|
+
export function selectSubagent(name) {
|
|
19
|
+
const preset = resolveTermPreset();
|
|
20
|
+
const configured = resolveConfiguredSubagent(preset);
|
|
21
|
+
if (configured)
|
|
22
|
+
return configured;
|
|
23
|
+
const selected = name?.trim() || getDefaultNameForPreset(preset);
|
|
24
|
+
return resolveKnownSubagent(selected, preset);
|
|
25
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export class StringTailBuffer {
|
|
2
|
+
maxChars;
|
|
3
|
+
chunks = [];
|
|
4
|
+
keptChars = 0;
|
|
5
|
+
totalChars = 0;
|
|
6
|
+
constructor(maxChars) {
|
|
7
|
+
if (!Number.isFinite(maxChars) || maxChars <= 0) {
|
|
8
|
+
throw new Error(`invalid maxChars: ${maxChars}`);
|
|
9
|
+
}
|
|
10
|
+
this.maxChars = Math.floor(maxChars);
|
|
11
|
+
}
|
|
12
|
+
append(chunk) {
|
|
13
|
+
if (!chunk)
|
|
14
|
+
return;
|
|
15
|
+
this.totalChars += chunk.length;
|
|
16
|
+
if (chunk.length >= this.maxChars) {
|
|
17
|
+
this.chunks.length = 0;
|
|
18
|
+
this.chunks.push(chunk.slice(-this.maxChars));
|
|
19
|
+
this.keptChars = this.maxChars;
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
this.chunks.push(chunk);
|
|
23
|
+
this.keptChars += chunk.length;
|
|
24
|
+
this.trimOverflow();
|
|
25
|
+
}
|
|
26
|
+
snapshot() {
|
|
27
|
+
const text = this.chunks.join("");
|
|
28
|
+
const truncatedChars = Math.max(0, this.totalChars - text.length);
|
|
29
|
+
return { text, totalChars: this.totalChars, truncatedChars };
|
|
30
|
+
}
|
|
31
|
+
trimOverflow() {
|
|
32
|
+
while (this.keptChars > this.maxChars && this.chunks.length > 0) {
|
|
33
|
+
const overflow = this.keptChars - this.maxChars;
|
|
34
|
+
const first = this.chunks[0];
|
|
35
|
+
if (first.length <= overflow) {
|
|
36
|
+
this.chunks.shift();
|
|
37
|
+
this.keptChars -= first.length;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
this.chunks[0] = first.slice(overflow);
|
|
41
|
+
this.keptChars -= overflow;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export class SubagentExecError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
timeoutMs;
|
|
4
|
+
exitCode;
|
|
5
|
+
stdout;
|
|
6
|
+
stderr;
|
|
7
|
+
constructor(code, message, options = {}) {
|
|
8
|
+
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
9
|
+
this.name = "SubagentExecError";
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.timeoutMs = options.timeoutMs ?? null;
|
|
12
|
+
this.exitCode = options.exitCode ?? null;
|
|
13
|
+
this.stdout = options.stdout ?? "";
|
|
14
|
+
this.stderr = options.stderr ?? "";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function isSubagentExecError(error) {
|
|
18
|
+
return error instanceof SubagentExecError;
|
|
19
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { getAvailableNamesForPreset, resolveTermPreset } from "../config/term-presets.js";
|
|
2
|
+
function unknownSubagentHint() {
|
|
3
|
+
const preset = resolveTermPreset();
|
|
4
|
+
const names = getAvailableNamesForPreset(preset).join(", ");
|
|
5
|
+
const { identity } = preset;
|
|
6
|
+
return `Unknown ${identity.toLowerCase()} name. Configured ${identity.toLowerCase()}s: ${names}. Choose one via tool input 'name' or env 'KEIYAKU_SUBAGENT_NAME_OVERRIDE'.`;
|
|
7
|
+
}
|
|
8
|
+
export class FlowError extends Error {
|
|
9
|
+
code;
|
|
10
|
+
constructor(code, message, cause) {
|
|
11
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
12
|
+
this.name = "FlowError";
|
|
13
|
+
this.code = code;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function isFlowError(err) {
|
|
17
|
+
return err instanceof FlowError;
|
|
18
|
+
}
|
|
19
|
+
export function asMessage(err) {
|
|
20
|
+
return err instanceof Error ? err.message : String(err);
|
|
21
|
+
}
|
|
22
|
+
export function wrapFlowError(action, err) {
|
|
23
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
24
|
+
return err;
|
|
25
|
+
}
|
|
26
|
+
if (isFlowError(err)) {
|
|
27
|
+
return new FlowError(err.code, `${action} failed: ${err.message}`, err);
|
|
28
|
+
}
|
|
29
|
+
return new Error(`${action} failed: ${asMessage(err)}`);
|
|
30
|
+
}
|
|
31
|
+
export function pickHintFromError(err, message) {
|
|
32
|
+
if (isFlowError(err)) {
|
|
33
|
+
switch (err.code) {
|
|
34
|
+
case "NOT_GIT_REPO":
|
|
35
|
+
return "The provided `cwd` is not a git repository.";
|
|
36
|
+
case "ACTIVE_KEIYAKU_EXISTS":
|
|
37
|
+
return "An active keiyaku branch already exists in this repository.";
|
|
38
|
+
case "EXISTING_KEIYAKU_BRANCH_FOUND":
|
|
39
|
+
return "At least one local `keiyaku/*` branch already exists in this repository.";
|
|
40
|
+
case "EMPTY_PARAM":
|
|
41
|
+
return "One or more required parameters are empty.";
|
|
42
|
+
case "DIRTY_WORKTREE":
|
|
43
|
+
return "The working tree has uncommitted changes.";
|
|
44
|
+
case "NOT_ACTIVE_KEIYAKU_BRANCH":
|
|
45
|
+
return message;
|
|
46
|
+
case "MISSING_KEIYAKU_BASE":
|
|
47
|
+
return "Current keiyaku branch is missing `keiyakuBase` metadata.";
|
|
48
|
+
case "MISSING_PROTOCOL_FILES":
|
|
49
|
+
return "Required protocol files are missing (`KEIYAKU.md` or `KEIYAKU_TRACE.md`).";
|
|
50
|
+
case "DONE_MERGE_CONFLICT":
|
|
51
|
+
return "DONE encountered a git merge conflict.";
|
|
52
|
+
case "CLOSE_QUALITY_GATE_FAILED":
|
|
53
|
+
return "DONE requires metPrecise/metMinimal/metIsolated/metIdiomatic/metCohesive all set to true.";
|
|
54
|
+
case "OATH_MISMATCH":
|
|
55
|
+
return err.message;
|
|
56
|
+
case "SUBAGENT_DID_NOT_ADVANCE_ROUND":
|
|
57
|
+
return "Subagent run did not append the expected new round in `KEIYAKU_TRACE.md`.";
|
|
58
|
+
case "ROUND_SUBAGENT_FAILED":
|
|
59
|
+
return "Subagent execution failed. Review KEIYAKU_TRACE.md for details, then continue with a narrower directive.";
|
|
60
|
+
case "INVALID_BRANCH_TITLE":
|
|
61
|
+
return "Title cannot be converted to a valid branch token for `keiyaku/<title>`.";
|
|
62
|
+
case "UNKNOWN_SUBAGENT":
|
|
63
|
+
return unknownSubagentHint();
|
|
64
|
+
default:
|
|
65
|
+
return "Review the error details, fix the issue, and retry.";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Fallback for untyped runtime errors.
|
|
69
|
+
if (message.includes("is not a git repository")) {
|
|
70
|
+
return "The provided `cwd` is not a git repository.";
|
|
71
|
+
}
|
|
72
|
+
if (message.includes("active keiyaku already exists")) {
|
|
73
|
+
return "An active keiyaku branch already exists in this repository.";
|
|
74
|
+
}
|
|
75
|
+
if (message.includes("existing keiyaku branch found")) {
|
|
76
|
+
return "At least one local `keiyaku/*` branch already exists in this repository.";
|
|
77
|
+
}
|
|
78
|
+
if (message.includes("parameter") && message.includes("cannot be empty")) {
|
|
79
|
+
return "One or more required parameters are empty.";
|
|
80
|
+
}
|
|
81
|
+
if (message.includes("working tree has uncommitted changes")) {
|
|
82
|
+
return "The working tree has uncommitted changes.";
|
|
83
|
+
}
|
|
84
|
+
if (message.includes("current branch is not an active keiyaku branch")) {
|
|
85
|
+
return "No active keiyaku branch (`keiyaku/*`). If this task is not using keiyaku workflow, skip this tool call.";
|
|
86
|
+
}
|
|
87
|
+
if (message.includes("is missing base metadata")) {
|
|
88
|
+
return "Current keiyaku branch is missing `keiyakuBase` metadata.";
|
|
89
|
+
}
|
|
90
|
+
if (message.includes("missing protocol files")) {
|
|
91
|
+
return "Required protocol files are missing (`KEIYAKU.md` or `KEIYAKU_TRACE.md`).";
|
|
92
|
+
}
|
|
93
|
+
if (message.includes("DONE merge conflict")) {
|
|
94
|
+
return "DONE encountered a git merge conflict.";
|
|
95
|
+
}
|
|
96
|
+
if (message.includes("requires metPrecise/metMinimal/metIsolated/metIdiomatic/metCohesive all set to true")) {
|
|
97
|
+
return "DONE requires metPrecise/metMinimal/metIsolated/metIdiomatic/metCohesive all set to true.";
|
|
98
|
+
}
|
|
99
|
+
if (message.includes("requires the sacred oath to exactly equal") ||
|
|
100
|
+
message.includes("requires oath to exactly match configured value") ||
|
|
101
|
+
message.includes("requires oath to match configured value. If template contains") ||
|
|
102
|
+
message.includes("To declare DONE, you must solemnly swear the sacred oath.") ||
|
|
103
|
+
message.includes("To declare DONE, oath mismatch.")) {
|
|
104
|
+
return message;
|
|
105
|
+
}
|
|
106
|
+
if (message.includes("subagent did not advance round") || message.includes("did not append KEIYAKU_TRACE")) {
|
|
107
|
+
return "Subagent run did not append the expected new round in `KEIYAKU_TRACE.md`.";
|
|
108
|
+
}
|
|
109
|
+
if (message.includes("failed during subagent execution")) {
|
|
110
|
+
return "Subagent execution failed. Review KEIYAKU_TRACE.md for details, then continue with a narrower directive.";
|
|
111
|
+
}
|
|
112
|
+
if (message.includes("cannot be converted to a valid branch name")) {
|
|
113
|
+
return "Title cannot be converted to a valid branch token for `keiyaku/<title>`.";
|
|
114
|
+
}
|
|
115
|
+
if (message.includes("Unknown subagent")) {
|
|
116
|
+
return unknownSubagentHint();
|
|
117
|
+
}
|
|
118
|
+
return "Review the error details, fix the issue, and retry.";
|
|
119
|
+
}
|