taskflow-hosts 0.1.6

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 heggria
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # taskflow-hosts
2
+
3
+ > Shared host-runner collection for [taskflow](https://github.com/heggria/taskflow).
4
+
5
+ This package holds the `SubagentRunner` implementations for taskflow's non-pi
6
+ hosts — **codex**, **claude**, and **opencode** — plus their pure argv builders
7
+ (`buildCodexArgs` / `buildClaudeArgs` / `buildOpencodeArgs`) and event-stream
8
+ parsers. It is the **single place** host runners live; a new host adds a
9
+ `<host>-runner.ts` here.
10
+
11
+ ## Why a separate package
12
+
13
+ Each host has two halves:
14
+
15
+ 1. **The runner** — spawn that host's CLI, fold its JSON event stream into a
16
+ host-neutral `RunResult`, classify the outcome. + the pure argv builder +
17
+ permission/model helpers.
18
+ 2. **The delivery** — the per-host MCP server + bin + plugin scaffold, which is
19
+ that host ecosystem's install target (`codex plugin add`, `claude plugin
20
+ install`, OpenCode config).
21
+
22
+ Half #1 is nearly identical in *shape* across hosts and changes for the same
23
+ reasons (a `taskflow-core` contract change, or a host CLI flag change). Half #2
24
+ is genuinely host-specific (different install mechanisms, different plugin
25
+ manifests). So #1 is collected here; #2 stays in `codex-taskflow` /
26
+ `claude-taskflow` / `opencode-taskflow`, which import their runner from this
27
+ package.
28
+
29
+ ## Install
30
+
31
+ You usually don't install this directly — install the host delivery package:
32
+
33
+ ```bash
34
+ npm install -g codex-taskflow # or claude-taskflow / opencode-taskflow
35
+ ```
36
+
37
+ For code-level use:
38
+
39
+ ```bash
40
+ npm install taskflow-hosts
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ ```ts
46
+ // one host, tree-shaken:
47
+ import { codexSubagentRunner, buildCodexArgs } from "taskflow-hosts/codex";
48
+
49
+ // or the barrel:
50
+ import { claudeSubagentRunner, opencodeSubagentRunner } from "taskflow-hosts";
51
+ ```
52
+
53
+ Each runner export includes: the `SubagentRunner` (`codexSubagentRunner` / …),
54
+ the `runXxxAgentTask` function, the pure `buildXxxArgs` builder, the
55
+ `foldXxxEventLine` parser + `newXxxAccumulator`, and the permission/model
56
+ helpers (`sandboxForTools`, `permissionArgsForTools`, `isReadOnlyPhase`,
57
+ `resolveXxxModel`, `xxxBin`).
58
+
59
+ ## Adding a host
60
+
61
+ 1. Add `<host>-runner.ts` in `src/` (model it on an existing one — a pure
62
+ `buildXxxArgs` + the event parser + a `SubagentRunner` export).
63
+ 2. Add an export entry in `src/index.ts`.
64
+ 3. Add `*-runner.test.ts` (event-stream parser) + `*-args.test.ts` (argv
65
+ contract — CI-locked) in `test/`.
66
+ 4. The host's *delivery* (MCP server/bin + plugin scaffold) goes in a
67
+ `taskflow-hosts`-depending package of its own.
68
+
69
+ See [`AGENTS.md`](../../AGENTS.md) and [`DECISIONS.md`](../../DECISIONS.md) in
70
+ the repo root for the full architecture and the packaging decision.
71
+
72
+ ## License
73
+
74
+ MIT
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Claude Code subagent runner — the Claude host's `SubagentRunner` implementation.
3
+ *
4
+ * Spawns an isolated `claude -p --output-format stream-json` process per task
5
+ * and folds its JSONL event stream into the same host-neutral `RunResult` the
6
+ * pi and codex runners produce, so the engine (runtime.ts) treats a Claude
7
+ * subagent identically to a pi or codex one.
8
+ *
9
+ * Real claude stream-json schema (claude-code ≥ 2.1), observed empirically:
10
+ * {"type":"system","subtype":"init","model":"claude-…","tools":[…],…}
11
+ * {"type":"assistant","message":{"model":"…","content":[{"type":"text","text":"…"},
12
+ * {"type":"tool_use","name":"Bash","input":{…}}],"usage":{"input_tokens":…,
13
+ * "output_tokens":…,"cache_read_input_tokens":…,"cache_creation_input_tokens":…}},…}
14
+ * {"type":"user","message":{…tool_result…}}
15
+ * {"type":"result","subtype":"success"|"error_max_turns"|…,"is_error":bool,
16
+ * "num_turns":N,"result":"…","total_cost_usd":X,"usage":{…}} ← authoritative
17
+ *
18
+ * Mapping to the host-neutral contract:
19
+ * - output = `result` field of the result event (fallback: last assistant text)
20
+ * - usage = the result event's cumulative usage + num_turns + total_cost_usd
21
+ * (assistant events accumulate per-turn usage for live streaming;
22
+ * the result event overwrites with the authoritative totals)
23
+ * - lastActivity = latest assistant text or a one-line tool summary
24
+ * - failure = result.is_error, a stream-level error, or a non-zero exit
25
+ *
26
+ * Permission mapping (the codex `sandboxForTools` analogue): Claude Code has no
27
+ * OS-level sandbox in -p mode — a tool call is either whitelisted or denied. A
28
+ * phase whose tool whitelist is read-only gets `--allowedTools <read-only set>`
29
+ * (mutating tools are denied outright); anything else gets
30
+ * `--permission-mode bypassPermissions`, which behaves like codex's
31
+ * workspace-write but WITHOUT an OS sandbox backstop — document accordingly.
32
+ *
33
+ * Process handling (idle watchdog, abort, signal-kill detection, stderr cap,
34
+ * error sanitization) mirrors the pi/codex runners so behavior is uniform.
35
+ */
36
+ import { type AgentConfig, type LiveUpdate, type RunOptions, type RunResult, type SubagentRunner, type UsageStats } from "taskflow-core";
37
+ /** Accumulated state folded from a claude stream-json event stream. */
38
+ export interface ClaudeAccumulator {
39
+ usage: UsageStats;
40
+ model?: string;
41
+ /** Final answer = the result event's `result`, else the last assistant text. */
42
+ finalText: string;
43
+ lastActivity: string;
44
+ /** Set when the stream reported a fatal error (result.is_error / stream error). */
45
+ fatalError?: string;
46
+ /** True once the authoritative `result` event has been folded. */
47
+ sawResult: boolean;
48
+ }
49
+ export declare function newClaudeAccumulator(model?: string): ClaudeAccumulator;
50
+ /**
51
+ * Fold one claude stream-json line into the accumulator. Returns a LiveUpdate
52
+ * when the stream produced new activity (for streaming), else null. Empty or
53
+ * malformed lines are ignored — robust to partial buffers and noise,
54
+ * unit-testable without spawning a process.
55
+ */
56
+ export declare function foldClaudeEventLine(acc: ClaudeAccumulator, line: string): LiveUpdate | null;
57
+ /** Override the claude binary (tests / unusual installs). */
58
+ export declare function claudeBin(): string;
59
+ /**
60
+ * Map a phase's tool whitelist to claude permission flags — the codex
61
+ * `sandboxForTools` analogue. A phase that only reads (no write/edit/bash in
62
+ * its whitelist) gets a read-only `--allowedTools` set; anything else gets
63
+ * `--permission-mode bypassPermissions` (non-interactive -p runs cannot answer
64
+ * permission prompts, so the codex workspace-write equivalent is bypass —
65
+ * WITHOUT an OS sandbox backstop). No whitelist → bypass (the engine's
66
+ * default-capable agent).
67
+ */
68
+ export declare function permissionArgsForTools(tools: string[] | undefined): string[];
69
+ /** Resolve a modelRoles/pi model id for `claude --model`, or `undefined` to let
70
+ * claude fall back to its own default. Claude model ids are FLAT (aliases like
71
+ * "sonnet"/"haiku" or full ids like "claude-sonnet-4-6"), so — same rule as
72
+ * codex — a pi-provider path (contains "/") or an unresolved {{placeholder}}
73
+ * is dropped. Exported for unit-testing the contract. */
74
+ export declare function resolveClaudeModel(model: string | undefined): string | undefined;
75
+ /** Context for {@link buildClaudeArgs} — the pure inputs to argv construction. */
76
+ export interface ClaudeArgsCtx {
77
+ systemPrompt: string;
78
+ task: string;
79
+ /** Already-resolved model (opts.model ?? agent.model). */
80
+ model?: string;
81
+ tools?: string[];
82
+ }
83
+ /**
84
+ * Build the full `claude -p` argv from a phase's resolved context — PURE
85
+ * (no process.env, no spawn). Extracted from `runClaudeAgentTask` so the host's
86
+ * CLI flag contract is unit-testable in CI without a live claude session.
87
+ *
88
+ * claude -p --output-format stream-json --verbose --strict-mcp-config
89
+ * [--permission-mode bypassPermissions | --allowedTools ...]
90
+ * [--model m] [--append-system-prompt ...] <task>
91
+ */
92
+ export declare function buildClaudeArgs(ctx: ClaudeArgsCtx): string[];
93
+ /**
94
+ * Run a single subagent task via `claude -p --output-format stream-json`.
95
+ * Resolves the agent from `agents` by name; returns the same structured
96
+ * `RunResult` the pi and codex runners do.
97
+ */
98
+ export declare function runClaudeAgentTask(defaultCwd: string, agents: AgentConfig[], agentName: string, task: string, opts: RunOptions, globalThinking?: string): Promise<RunResult>;
99
+ /**
100
+ * The Claude host's `SubagentRunner`. Drops into `RuntimeDeps.runTask` exactly
101
+ * like the pi/codex/opencode runners, so the engine runs unchanged on Claude Code.
102
+ */
103
+ export declare const claudeSubagentRunner: SubagentRunner<AgentConfig>;
104
+ //# sourceMappingURL=claude-runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-runner.d.ts","sourceRoot":"","sources":["../src/claude-runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAIN,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,MAAM,eAAe,CAAC;AASvB,uEAAuE;AACvE,MAAM,WAAW,iBAAiB;IACjC,KAAK,EAAE,UAAU,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gFAAgF;IAChF,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,mFAAmF;IACnF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kEAAkE;IAClE,SAAS,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,oBAAoB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,iBAAiB,CAEtE;AA+BD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,iBAAiB,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAyE3F;AAED,6DAA6D;AAC7D,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,SAAS,GAAG,MAAM,EAAE,CAM5E;AAED;;;;0DAI0D;AAC1D,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAKhF;AAED,kFAAkF;AAClF,MAAM,WAAW,aAAa;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,EAAE,CAQ5D;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CACvC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,WAAW,EAAE,EACrB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,UAAU,EAChB,cAAc,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,SAAS,CAAC,CA4BpB;AAED;;;GAGG;AACH,eAAO,MAAM,oBAAoB,EAAE,cAAc,CAAC,WAAW,CAE5D,CAAC"}
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Claude Code subagent runner — the Claude host's `SubagentRunner` implementation.
3
+ *
4
+ * Spawns an isolated `claude -p --output-format stream-json` process per task
5
+ * and folds its JSONL event stream into the same host-neutral `RunResult` the
6
+ * pi and codex runners produce, so the engine (runtime.ts) treats a Claude
7
+ * subagent identically to a pi or codex one.
8
+ *
9
+ * Real claude stream-json schema (claude-code ≥ 2.1), observed empirically:
10
+ * {"type":"system","subtype":"init","model":"claude-…","tools":[…],…}
11
+ * {"type":"assistant","message":{"model":"…","content":[{"type":"text","text":"…"},
12
+ * {"type":"tool_use","name":"Bash","input":{…}}],"usage":{"input_tokens":…,
13
+ * "output_tokens":…,"cache_read_input_tokens":…,"cache_creation_input_tokens":…}},…}
14
+ * {"type":"user","message":{…tool_result…}}
15
+ * {"type":"result","subtype":"success"|"error_max_turns"|…,"is_error":bool,
16
+ * "num_turns":N,"result":"…","total_cost_usd":X,"usage":{…}} ← authoritative
17
+ *
18
+ * Mapping to the host-neutral contract:
19
+ * - output = `result` field of the result event (fallback: last assistant text)
20
+ * - usage = the result event's cumulative usage + num_turns + total_cost_usd
21
+ * (assistant events accumulate per-turn usage for live streaming;
22
+ * the result event overwrites with the authoritative totals)
23
+ * - lastActivity = latest assistant text or a one-line tool summary
24
+ * - failure = result.is_error, a stream-level error, or a non-zero exit
25
+ *
26
+ * Permission mapping (the codex `sandboxForTools` analogue): Claude Code has no
27
+ * OS-level sandbox in -p mode — a tool call is either whitelisted or denied. A
28
+ * phase whose tool whitelist is read-only gets `--allowedTools <read-only set>`
29
+ * (mutating tools are denied outright); anything else gets
30
+ * `--permission-mode bypassPermissions`, which behaves like codex's
31
+ * workspace-write but WITHOUT an OS sandbox backstop — document accordingly.
32
+ *
33
+ * Process handling (idle watchdog, abort, signal-kill detection, stderr cap,
34
+ * error sanitization) mirrors the pi/codex runners so behavior is uniform.
35
+ */
36
+ import { runSubagentProcess, num, unknownAgentResult, } from "taskflow-core";
37
+ import { emptyUsage } from "taskflow-core";
38
+ /** The Claude tools a read-only phase may use. `Bash` is excluded — Claude has
39
+ * no read-only shell (unlike codex's read-only OS sandbox, which still allows
40
+ * read-only commands), so a listed-tools phase without write/edit/bash gets
41
+ * file reads + search + web only. */
42
+ const READ_ONLY_TOOLS = ["Read", "Grep", "Glob", "WebFetch", "WebSearch"];
43
+ export function newClaudeAccumulator(model) {
44
+ return { usage: emptyUsage(), model, finalText: "", lastActivity: "", sawResult: false };
45
+ }
46
+ /** Summarize a claude tool_use for the live activity stream — parity with the
47
+ * pi runner's summarizeToolCall and codex's shortCmd (which cover all tools,
48
+ * not just Bash). Without this a Write/Edit/Read shows as just the bare tool
49
+ * name, a UX regression vs the other hosts. */
50
+ function shortTool(name, input) {
51
+ const n = String(name ?? "tool");
52
+ if (!input || typeof input !== "object")
53
+ return n;
54
+ const obj = input;
55
+ const trim = (v) => String(v ?? "").replace(/\s+/g, " ").trim();
56
+ const brief = (v) => {
57
+ const s = trim(v);
58
+ return s.length > 64 ? `${s.slice(0, 64)}…` : s;
59
+ };
60
+ switch (n) {
61
+ case "Bash":
62
+ return `$ ${brief(obj.command)}`;
63
+ case "Read":
64
+ case "Write":
65
+ case "Edit":
66
+ case "NotebookEdit":
67
+ return `${n}: ${brief(obj.file_path ?? obj.path)}`;
68
+ case "Grep":
69
+ case "Glob":
70
+ return `${n}: ${brief(obj.pattern)}`;
71
+ default:
72
+ return n;
73
+ }
74
+ }
75
+ /**
76
+ * Fold one claude stream-json line into the accumulator. Returns a LiveUpdate
77
+ * when the stream produced new activity (for streaming), else null. Empty or
78
+ * malformed lines are ignored — robust to partial buffers and noise,
79
+ * unit-testable without spawning a process.
80
+ */
81
+ export function foldClaudeEventLine(acc, line) {
82
+ if (!line.trim())
83
+ return null;
84
+ let event;
85
+ try {
86
+ event = JSON.parse(line);
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ let activity = "";
92
+ if (event.type === "system" && event.subtype === "init") {
93
+ if (typeof event.model === "string" && event.model)
94
+ acc.model = event.model;
95
+ return null;
96
+ }
97
+ else if (event.type === "assistant" && event.message) {
98
+ const msg = event.message;
99
+ // Claude injects synthetic assistant messages (model "<synthetic>") for
100
+ // harness-level errors (auth failures, aborts) — their text is diagnostic,
101
+ // not an answer. Surface it as activity but never as the final answer.
102
+ const synthetic = msg.model === "<synthetic>";
103
+ if (!synthetic && typeof msg.model === "string" && msg.model)
104
+ acc.model = msg.model;
105
+ for (const part of Array.isArray(msg.content) ? msg.content : []) {
106
+ if (part?.type === "text" && typeof part.text === "string" && part.text.trim()) {
107
+ if (!synthetic)
108
+ acc.finalText = part.text;
109
+ activity = part.text.trim();
110
+ }
111
+ else if (part?.type === "tool_use") {
112
+ activity = shortTool(part.name, part.input);
113
+ }
114
+ }
115
+ if (typeof event.error === "string" && event.error) {
116
+ acc.fatalError = activity || event.error;
117
+ }
118
+ // Per-turn usage for live progress; the result event overwrites with the
119
+ // run's authoritative totals (never double-counted).
120
+ const u = msg.usage;
121
+ if (!acc.sawResult && u && !synthetic) {
122
+ acc.usage.turns++;
123
+ acc.usage.input += num(u.input_tokens);
124
+ acc.usage.output += num(u.output_tokens);
125
+ acc.usage.cacheRead += num(u.cache_read_input_tokens);
126
+ acc.usage.cacheWrite += num(u.cache_creation_input_tokens);
127
+ // contextTokens is a host-specific gauge (NOT additive): claude reports input_tokens
128
+ // EXCLUDING cache read, so input+cache_read+output = full last-turn context.
129
+ acc.usage.contextTokens =
130
+ num(u.input_tokens) + num(u.cache_read_input_tokens) + num(u.output_tokens);
131
+ }
132
+ }
133
+ else if (event.type === "result") {
134
+ acc.sawResult = true;
135
+ const u = event.usage;
136
+ if (u) {
137
+ const prev = acc.usage.contextTokens;
138
+ acc.usage = emptyUsage();
139
+ acc.usage.input = num(u.input_tokens);
140
+ acc.usage.output = num(u.output_tokens);
141
+ acc.usage.cacheRead = num(u.cache_read_input_tokens);
142
+ acc.usage.cacheWrite = num(u.cache_creation_input_tokens);
143
+ acc.usage.contextTokens = prev;
144
+ }
145
+ acc.usage.turns = typeof event.num_turns === "number" ? event.num_turns : acc.usage.turns;
146
+ acc.usage.cost = typeof event.total_cost_usd === "number" ? event.total_cost_usd : 0;
147
+ if (event.is_error) {
148
+ acc.fatalError =
149
+ (typeof event.result === "string" && event.result.trim()) ||
150
+ `claude run failed (${event.subtype ?? "unknown"})`;
151
+ activity = `error: ${acc.fatalError}`;
152
+ }
153
+ else if (typeof event.result === "string" && event.result.trim()) {
154
+ acc.finalText = event.result;
155
+ }
156
+ }
157
+ else {
158
+ return null; // user (tool results) / stream_event — nothing to fold.
159
+ }
160
+ if (activity)
161
+ acc.lastActivity = activity.replace(/\s+/g, " ").trim();
162
+ return { text: acc.lastActivity, usage: { ...acc.usage }, model: acc.model };
163
+ }
164
+ /** Override the claude binary (tests / unusual installs). */
165
+ export function claudeBin() {
166
+ return process.env.PI_TASKFLOW_CLAUDE_BIN || "claude";
167
+ }
168
+ /**
169
+ * Map a phase's tool whitelist to claude permission flags — the codex
170
+ * `sandboxForTools` analogue. A phase that only reads (no write/edit/bash in
171
+ * its whitelist) gets a read-only `--allowedTools` set; anything else gets
172
+ * `--permission-mode bypassPermissions` (non-interactive -p runs cannot answer
173
+ * permission prompts, so the codex workspace-write equivalent is bypass —
174
+ * WITHOUT an OS sandbox backstop). No whitelist → bypass (the engine's
175
+ * default-capable agent).
176
+ */
177
+ export function permissionArgsForTools(tools) {
178
+ if (!tools || tools.length === 0)
179
+ return ["--permission-mode", "bypassPermissions"];
180
+ const mutating = new Set(["write", "edit", "bash", "apply_patch"]);
181
+ const canMutate = tools.some((t) => mutating.has(t));
182
+ if (canMutate)
183
+ return ["--permission-mode", "bypassPermissions"];
184
+ return ["--allowedTools", READ_ONLY_TOOLS.join(",")];
185
+ }
186
+ /** Resolve a modelRoles/pi model id for `claude --model`, or `undefined` to let
187
+ * claude fall back to its own default. Claude model ids are FLAT (aliases like
188
+ * "sonnet"/"haiku" or full ids like "claude-sonnet-4-6"), so — same rule as
189
+ * codex — a pi-provider path (contains "/") or an unresolved {{placeholder}}
190
+ * is dropped. Exported for unit-testing the contract. */
191
+ export function resolveClaudeModel(model) {
192
+ if (!model)
193
+ return undefined;
194
+ if (model.includes("/"))
195
+ return undefined;
196
+ if (/^\{\{.*\}\}$/.test(model))
197
+ return undefined;
198
+ return model;
199
+ }
200
+ /**
201
+ * Build the full `claude -p` argv from a phase's resolved context — PURE
202
+ * (no process.env, no spawn). Extracted from `runClaudeAgentTask` so the host's
203
+ * CLI flag contract is unit-testable in CI without a live claude session.
204
+ *
205
+ * claude -p --output-format stream-json --verbose --strict-mcp-config
206
+ * [--permission-mode bypassPermissions | --allowedTools ...]
207
+ * [--model m] [--append-system-prompt ...] <task>
208
+ */
209
+ export function buildClaudeArgs(ctx) {
210
+ const claudeModel = resolveClaudeModel(ctx.model);
211
+ const args = ["-p", "--output-format", "stream-json", "--verbose", "--strict-mcp-config"];
212
+ args.push(...permissionArgsForTools(ctx.tools));
213
+ if (claudeModel)
214
+ args.push("--model", claudeModel);
215
+ if (ctx.systemPrompt.trim())
216
+ args.push("--append-system-prompt", ctx.systemPrompt.trim());
217
+ args.push(ctx.task);
218
+ return args;
219
+ }
220
+ /**
221
+ * Run a single subagent task via `claude -p --output-format stream-json`.
222
+ * Resolves the agent from `agents` by name; returns the same structured
223
+ * `RunResult` the pi and codex runners do.
224
+ */
225
+ export async function runClaudeAgentTask(defaultCwd, agents, agentName, task, opts, globalThinking) {
226
+ const agent = agents.find((a) => a.name === agentName);
227
+ if (!agent)
228
+ return unknownAgentResult(agentName, task, agents);
229
+ const model = opts.model ?? agent.model;
230
+ const tools = opts.tools ?? agent.tools;
231
+ void globalThinking; // claude -p has no thinking-level flag; reserved.
232
+ const args = buildClaudeArgs({
233
+ systemPrompt: agent.systemPrompt,
234
+ task,
235
+ model,
236
+ tools,
237
+ });
238
+ return runSubagentProcess({
239
+ agent: agentName,
240
+ task,
241
+ model,
242
+ bin: claudeBin(),
243
+ args,
244
+ cwd: opts.cwd ?? defaultCwd,
245
+ idleTimeoutMs: opts.idleTimeoutMs,
246
+ signal: opts.signal,
247
+ onLive: opts.onLive,
248
+ acc: newClaudeAccumulator(model),
249
+ foldLine: foldClaudeEventLine,
250
+ });
251
+ }
252
+ /**
253
+ * The Claude host's `SubagentRunner`. Drops into `RuntimeDeps.runTask` exactly
254
+ * like the pi/codex/opencode runners, so the engine runs unchanged on Claude Code.
255
+ */
256
+ export const claudeSubagentRunner = {
257
+ runTask: runClaudeAgentTask,
258
+ };
259
+ //# sourceMappingURL=claude-runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-runner.js","sourceRoot":"","sources":["../src/claude-runner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Codex subagent runner — the Codex host's `SubagentRunner` implementation.
3
+ *
4
+ * Spawns an isolated `codex exec --json` process per task and folds its JSONL
5
+ * event stream into the same host-neutral `RunResult` the pi runner produces,
6
+ * so the engine (runtime.ts) treats a Codex subagent identically to a pi one.
7
+ *
8
+ * Real codex JSONL schema (codex-cli ≥ 0.142), observed empirically:
9
+ * {"type":"thread.started","thread_id":"…"}
10
+ * {"type":"turn.started"}
11
+ * {"type":"item.started", "item":{"id","type":"command_execution","command","status":"in_progress",…}}
12
+ * {"type":"item.completed","item":{"id","type":"command_execution","command","exit_code","status":"completed",…}}
13
+ * {"type":"item.completed","item":{"id","type":"agent_message","text":"…"}} ← final answer
14
+ * {"type":"item.completed","item":{"id","type":"error","message":"…"}} ← warning OR fatal
15
+ * {"type":"turn.completed","usage":{"input_tokens","cached_input_tokens","output_tokens","reasoning_output_tokens"}}
16
+ *
17
+ * Mapping to the pi contract:
18
+ * - output = text of the LAST `agent_message` item
19
+ * - usage.input = input_tokens; usage.output = output_tokens + reasoning_output_tokens;
20
+ * usage.cacheRead = cached_input_tokens; turns = number of `turn.completed`
21
+ * - lastActivity for streaming = latest agent_message text or a one-line
22
+ * command summary
23
+ * - benign warnings (the "Under-development features" / "Skill descriptions"
24
+ * notices) are NOT treated as failures — only a fatal error item or a
25
+ * non-zero exit is.
26
+ *
27
+ * Process handling (idle watchdog, abort, signal-kill detection, stderr cap,
28
+ * error sanitization) is delegated to the shared `runSubagentProcess` in
29
+ * taskflow-core, so behavior is uniform across hosts.
30
+ */
31
+ import { type AgentConfig, type LiveUpdate, type RunOptions, type RunResult, type SubagentRunner, type UsageStats } from "taskflow-core";
32
+ /** Accumulated state folded from a codex JSONL event stream. */
33
+ export interface CodexAccumulator {
34
+ usage: UsageStats;
35
+ model?: string;
36
+ /** Final answer = text of the last agent_message item seen. */
37
+ finalText: string;
38
+ lastActivity: string;
39
+ /** Set when a fatal (non-benign) error item is seen. */
40
+ fatalError?: string;
41
+ }
42
+ export declare function newCodexAccumulator(model?: string): CodexAccumulator;
43
+ /**
44
+ * Fold one codex JSONL line into the accumulator. Returns a LiveUpdate when the
45
+ * stream produced new activity (for streaming), else null. Empty/malformed
46
+ * lines are ignored — robust to partial buffers and noise, unit-testable
47
+ * without spawning a process.
48
+ */
49
+ export declare function foldCodexEventLine(acc: CodexAccumulator, line: string): LiveUpdate | null;
50
+ /** Override the codex binary (tests / unusual installs). */
51
+ export declare function codexBin(): string;
52
+ /**
53
+ * Map a phase's tool whitelist to a codex sandbox mode. Codex has no per-tool
54
+ * whitelist; it has sandbox policies. The conservative mapping: a phase that
55
+ * only reads (no write/edit/bash in its whitelist) gets `read-only`; anything
56
+ * else gets `workspace-write`. No whitelist → workspace-write (the engine's
57
+ * default-capable agent). `danger-full-access` is never selected automatically.
58
+ */
59
+ export declare function sandboxForTools(tools: string[] | undefined): "read-only" | "workspace-write";
60
+ /** Resolve a modelRoles/pi model id for `codex exec -m`, or `undefined` to let
61
+ * codex fall back to its own default. Codex model ids are FLAT (e.g.
62
+ * "gpt-5.5"), so a pi-provider path (contains "/") or an unresolved role
63
+ * placeholder ({{...}}) is dropped. Exported so the model-id contract is
64
+ * unit-testable without a live codex session. */
65
+ export declare function resolveCodexModel(model: string | undefined): string | undefined;
66
+ /** Context for {@link buildCodexArgs} — the pure inputs to argv construction. */
67
+ export interface CodexArgsCtx {
68
+ systemPrompt: string;
69
+ task: string;
70
+ /** Already-resolved model (opts.model ?? agent.model). */
71
+ model?: string;
72
+ tools?: string[];
73
+ cwd?: string;
74
+ }
75
+ /**
76
+ * Build the full `codex exec` argv from a phase's resolved context — PURE
77
+ * (no process.env, no spawn). Extracted from `runCodexAgentTask` so the host's
78
+ * CLI flag contract is unit-testable in CI without a live codex session.
79
+ *
80
+ * codex exec --json --skip-git-repo-check -s <sandbox> [-m model] [-C cwd] <prompt>
81
+ */
82
+ export declare function buildCodexArgs(ctx: CodexArgsCtx): string[];
83
+ /**
84
+ * Run a single subagent task via `codex exec --json`. Resolves the agent from
85
+ * `agents` by name; returns the same structured `RunResult` the pi runner does.
86
+ */
87
+ export declare function runCodexAgentTask(defaultCwd: string, agents: AgentConfig[], agentName: string, task: string, opts: RunOptions, globalThinking?: string): Promise<RunResult>;
88
+ /**
89
+ * The Codex host's `SubagentRunner`. Drops into `RuntimeDeps.runTask` exactly
90
+ * like the pi/codex/claude/opencode runners, so the engine runs unchanged on Codex.
91
+ */
92
+ export declare const codexSubagentRunner: SubagentRunner<AgentConfig>;
93
+ //# sourceMappingURL=codex-runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex-runner.d.ts","sourceRoot":"","sources":["../src/codex-runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,EAIN,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,MAAM,eAAe,CAAC;AAcvB,gEAAgE;AAChE,MAAM,WAAW,gBAAgB;IAChC,KAAK,EAAE,UAAU,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+DAA+D;IAC/D,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,wDAAwD;IACxD,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,mBAAmB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAEpE;AAOD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAkDzF;AAED,4DAA4D;AAC5D,wBAAgB,QAAQ,IAAI,MAAM,CAEjC;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,SAAS,GAAG,WAAW,GAAG,iBAAiB,CAK5F;AAED;;;;kDAIkD;AAClD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAK/E;AAED,iFAAiF;AACjF,MAAM,WAAW,YAAY;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,EAAE,CAW1D;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CACtC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,WAAW,EAAE,EACrB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,UAAU,EAChB,cAAc,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,SAAS,CAAC,CA8BpB;AAED;;;GAGG;AACH,eAAO,MAAM,mBAAmB,EAAE,cAAc,CAAC,WAAW,CAE3D,CAAC"}
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Codex subagent runner — the Codex host's `SubagentRunner` implementation.
3
+ *
4
+ * Spawns an isolated `codex exec --json` process per task and folds its JSONL
5
+ * event stream into the same host-neutral `RunResult` the pi runner produces,
6
+ * so the engine (runtime.ts) treats a Codex subagent identically to a pi one.
7
+ *
8
+ * Real codex JSONL schema (codex-cli ≥ 0.142), observed empirically:
9
+ * {"type":"thread.started","thread_id":"…"}
10
+ * {"type":"turn.started"}
11
+ * {"type":"item.started", "item":{"id","type":"command_execution","command","status":"in_progress",…}}
12
+ * {"type":"item.completed","item":{"id","type":"command_execution","command","exit_code","status":"completed",…}}
13
+ * {"type":"item.completed","item":{"id","type":"agent_message","text":"…"}} ← final answer
14
+ * {"type":"item.completed","item":{"id","type":"error","message":"…"}} ← warning OR fatal
15
+ * {"type":"turn.completed","usage":{"input_tokens","cached_input_tokens","output_tokens","reasoning_output_tokens"}}
16
+ *
17
+ * Mapping to the pi contract:
18
+ * - output = text of the LAST `agent_message` item
19
+ * - usage.input = input_tokens; usage.output = output_tokens + reasoning_output_tokens;
20
+ * usage.cacheRead = cached_input_tokens; turns = number of `turn.completed`
21
+ * - lastActivity for streaming = latest agent_message text or a one-line
22
+ * command summary
23
+ * - benign warnings (the "Under-development features" / "Skill descriptions"
24
+ * notices) are NOT treated as failures — only a fatal error item or a
25
+ * non-zero exit is.
26
+ *
27
+ * Process handling (idle watchdog, abort, signal-kill detection, stderr cap,
28
+ * error sanitization) is delegated to the shared `runSubagentProcess` in
29
+ * taskflow-core, so behavior is uniform across hosts.
30
+ */
31
+ import { runSubagentProcess, num, unknownAgentResult, } from "taskflow-core";
32
+ import { emptyUsage } from "taskflow-core";
33
+ /** Benign codex `error` items that are warnings, not failures. Matched as a
34
+ * prefix/substring so version drift in the message tail still classifies. */
35
+ const BENIGN_ERROR_MARKERS = [
36
+ "Under-development features",
37
+ "Skill descriptions were shortened",
38
+ ];
39
+ function isBenignCodexError(message) {
40
+ return BENIGN_ERROR_MARKERS.some((m) => message.includes(m));
41
+ }
42
+ export function newCodexAccumulator(model) {
43
+ return { usage: emptyUsage(), model, finalText: "", lastActivity: "" };
44
+ }
45
+ function shortCmd(cmd) {
46
+ const s = String(cmd ?? "").replace(/\s+/g, " ").trim();
47
+ return s.length > 64 ? `${s.slice(0, 64)}…` : s;
48
+ }
49
+ /**
50
+ * Fold one codex JSONL line into the accumulator. Returns a LiveUpdate when the
51
+ * stream produced new activity (for streaming), else null. Empty/malformed
52
+ * lines are ignored — robust to partial buffers and noise, unit-testable
53
+ * without spawning a process.
54
+ */
55
+ export function foldCodexEventLine(acc, line) {
56
+ if (!line.trim())
57
+ return null;
58
+ let event;
59
+ try {
60
+ event = JSON.parse(line);
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ let activity = "";
66
+ if (event.type === "turn.completed" && event.usage) {
67
+ const u = event.usage;
68
+ acc.usage.turns++;
69
+ acc.usage.input += num(u.input_tokens);
70
+ acc.usage.output += num(u.output_tokens) + num(u.reasoning_output_tokens);
71
+ acc.usage.cacheRead += num(u.cached_input_tokens);
72
+ // contextTokens is a host-specific point-in-time gauge (NOT additive — excluded from aggregateUsage):
73
+ // each host's formula differs because each accounts for cache differently. Codex's input_tokens
74
+ // already includes cached tokens, so input+output = full last-turn context.
75
+ acc.usage.contextTokens = num(u.input_tokens) + num(u.output_tokens);
76
+ }
77
+ else if (event.type === "item.completed" || event.type === "item.started") {
78
+ const item = event.item;
79
+ if (!item)
80
+ return null;
81
+ switch (item.type) {
82
+ case "agent_message":
83
+ if (typeof item.text === "string" && item.text.trim()) {
84
+ // Final answer is the LAST agent_message; keep overwriting.
85
+ if (event.type === "item.completed")
86
+ acc.finalText = item.text;
87
+ activity = item.text.trim();
88
+ }
89
+ break;
90
+ case "command_execution":
91
+ activity = `$ ${shortCmd(item.command)}`;
92
+ break;
93
+ case "error":
94
+ if (typeof item.message === "string" && !isBenignCodexError(item.message)) {
95
+ acc.fatalError = item.message;
96
+ activity = `error: ${item.message}`;
97
+ }
98
+ break;
99
+ default:
100
+ // file_change / reasoning / mcp_tool_call / etc. — note generically.
101
+ if (typeof item.type === "string")
102
+ activity = item.type;
103
+ }
104
+ }
105
+ else {
106
+ return null; // thread.started / turn.started — nothing to fold.
107
+ }
108
+ if (activity)
109
+ acc.lastActivity = activity.replace(/\s+/g, " ").trim();
110
+ return { text: acc.lastActivity, usage: { ...acc.usage }, model: acc.model };
111
+ }
112
+ /** Override the codex binary (tests / unusual installs). */
113
+ export function codexBin() {
114
+ return process.env.PI_TASKFLOW_CODEX_BIN || "codex";
115
+ }
116
+ /**
117
+ * Map a phase's tool whitelist to a codex sandbox mode. Codex has no per-tool
118
+ * whitelist; it has sandbox policies. The conservative mapping: a phase that
119
+ * only reads (no write/edit/bash in its whitelist) gets `read-only`; anything
120
+ * else gets `workspace-write`. No whitelist → workspace-write (the engine's
121
+ * default-capable agent). `danger-full-access` is never selected automatically.
122
+ */
123
+ export function sandboxForTools(tools) {
124
+ if (!tools || tools.length === 0)
125
+ return "workspace-write";
126
+ const mutating = new Set(["write", "edit", "bash", "apply_patch"]);
127
+ const canMutate = tools.some((t) => mutating.has(t));
128
+ return canMutate ? "workspace-write" : "read-only";
129
+ }
130
+ /** Resolve a modelRoles/pi model id for `codex exec -m`, or `undefined` to let
131
+ * codex fall back to its own default. Codex model ids are FLAT (e.g.
132
+ * "gpt-5.5"), so a pi-provider path (contains "/") or an unresolved role
133
+ * placeholder ({{...}}) is dropped. Exported so the model-id contract is
134
+ * unit-testable without a live codex session. */
135
+ export function resolveCodexModel(model) {
136
+ if (!model)
137
+ return undefined;
138
+ if (model.includes("/"))
139
+ return undefined;
140
+ if (/^\{\{.*\}\}$/.test(model))
141
+ return undefined;
142
+ return model;
143
+ }
144
+ /**
145
+ * Build the full `codex exec` argv from a phase's resolved context — PURE
146
+ * (no process.env, no spawn). Extracted from `runCodexAgentTask` so the host's
147
+ * CLI flag contract is unit-testable in CI without a live codex session.
148
+ *
149
+ * codex exec --json --skip-git-repo-check -s <sandbox> [-m model] [-C cwd] <prompt>
150
+ */
151
+ export function buildCodexArgs(ctx) {
152
+ const sandbox = sandboxForTools(ctx.tools);
153
+ const codexModel = resolveCodexModel(ctx.model);
154
+ const fullPrompt = ctx.systemPrompt.trim()
155
+ ? `${ctx.systemPrompt.trim()}\n\n---\n\nTask: ${ctx.task}`
156
+ : `Task: ${ctx.task}`;
157
+ const args = ["exec", "--json", "--skip-git-repo-check", "-s", sandbox];
158
+ if (codexModel)
159
+ args.push("-m", codexModel);
160
+ if (ctx.cwd)
161
+ args.push("-C", ctx.cwd);
162
+ args.push(fullPrompt);
163
+ return args;
164
+ }
165
+ /**
166
+ * Run a single subagent task via `codex exec --json`. Resolves the agent from
167
+ * `agents` by name; returns the same structured `RunResult` the pi runner does.
168
+ */
169
+ export async function runCodexAgentTask(defaultCwd, agents, agentName, task, opts, globalThinking) {
170
+ const agent = agents.find((a) => a.name === agentName);
171
+ if (!agent)
172
+ return unknownAgentResult(agentName, task, agents);
173
+ const model = opts.model ?? agent.model;
174
+ const tools = opts.tools ?? agent.tools;
175
+ void globalThinking; // codex has no thinking-level flag on exec; reserved.
176
+ const cwd = opts.cwd ?? defaultCwd;
177
+ const args = buildCodexArgs({
178
+ systemPrompt: agent.systemPrompt,
179
+ task,
180
+ model,
181
+ tools,
182
+ cwd,
183
+ });
184
+ return runSubagentProcess({
185
+ agent: agentName,
186
+ task,
187
+ model,
188
+ bin: codexBin(),
189
+ args,
190
+ cwd,
191
+ idleTimeoutMs: opts.idleTimeoutMs,
192
+ signal: opts.signal,
193
+ onLive: opts.onLive,
194
+ acc: newCodexAccumulator(model),
195
+ foldLine: foldCodexEventLine,
196
+ });
197
+ }
198
+ /**
199
+ * The Codex host's `SubagentRunner`. Drops into `RuntimeDeps.runTask` exactly
200
+ * like the pi/codex/claude/opencode runners, so the engine runs unchanged on Codex.
201
+ */
202
+ export const codexSubagentRunner = {
203
+ runTask: runCodexAgentTask,
204
+ };
205
+ //# sourceMappingURL=codex-runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex-runner.js","sourceRoot":"","sources":["../src/codex-runner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * taskflow-hosts — the shared collection of host-runner implementations.
3
+ *
4
+ * Each non-pi host (codex / claude / opencode) has two halves:
5
+ * 1. the **runner** — a `SubagentRunner` that spawns that host's CLI, folds
6
+ * its event stream, and classifies the outcome into a host-neutral
7
+ * `RunResult`. PLUS the pure argv builders (`buildXxxArgs`) and
8
+ * permission/model helpers.
9
+ * 2. the **delivery** — the per-host MCP server + bin + plugin scaffold,
10
+ * which lives in that host's own package (codex-taskflow /
11
+ * claude-taskflow / opencode-taskflow) because it's the host-ecosystem
12
+ * install target (`codex plugin add`, `claude plugin install`, …).
13
+ *
14
+ * This package holds only #1 — the runners — so all host runners live in one
15
+ * place. A new host adds a `<host>-runner.ts` here; its delivery package
16
+ * imports the runner from `taskflow-hosts`. The three legacy host packages
17
+ * re-export from here (or import the runner directly into their `mcp/server`).
18
+ *
19
+ * The barrel re-exports the full public surface of each runner so a consumer
20
+ * can `import { codexSubagentRunner, buildCodexArgs } from "taskflow-hosts"`,
21
+ * or use a sub-path (`taskflow-hosts/codex`) to tree-shake to one host.
22
+ */
23
+ export * from "./codex-runner.ts";
24
+ export * from "./claude-runner.ts";
25
+ export * from "./opencode-runner.ts";
26
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * taskflow-hosts — the shared collection of host-runner implementations.
3
+ *
4
+ * Each non-pi host (codex / claude / opencode) has two halves:
5
+ * 1. the **runner** — a `SubagentRunner` that spawns that host's CLI, folds
6
+ * its event stream, and classifies the outcome into a host-neutral
7
+ * `RunResult`. PLUS the pure argv builders (`buildXxxArgs`) and
8
+ * permission/model helpers.
9
+ * 2. the **delivery** — the per-host MCP server + bin + plugin scaffold,
10
+ * which lives in that host's own package (codex-taskflow /
11
+ * claude-taskflow / opencode-taskflow) because it's the host-ecosystem
12
+ * install target (`codex plugin add`, `claude plugin install`, …).
13
+ *
14
+ * This package holds only #1 — the runners — so all host runners live in one
15
+ * place. A new host adds a `<host>-runner.ts` here; its delivery package
16
+ * imports the runner from `taskflow-hosts`. The three legacy host packages
17
+ * re-export from here (or import the runner directly into their `mcp/server`).
18
+ *
19
+ * The barrel re-exports the full public surface of each runner so a consumer
20
+ * can `import { codexSubagentRunner, buildCodexArgs } from "taskflow-hosts"`,
21
+ * or use a sub-path (`taskflow-hosts/codex`) to tree-shake to one host.
22
+ */
23
+ export * from "./codex-runner.js";
24
+ export * from "./claude-runner.js";
25
+ export * from "./opencode-runner.js";
26
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC"}
@@ -0,0 +1,115 @@
1
+ /**
2
+ * OpenCode subagent runner — the OpenCode host's `SubagentRunner` implementation.
3
+ *
4
+ * Spawns an isolated `opencode run --format json` process per task and folds its
5
+ * JSON event stream into the same host-neutral `RunResult` the pi, codex, and
6
+ * claude runners produce, so the engine (runtime.ts) treats an OpenCode subagent
7
+ * identically.
8
+ *
9
+ * Real opencode `run --format json` schema (opencode ≥ 1.17), observed
10
+ * empirically — one JSON object per line:
11
+ * {"type":"step_start", "part":{"type":"step-start",…}}
12
+ * {"type":"text", "part":{"type":"text","text":"…"}} ← assistant text
13
+ * {"type":"tool_use", "part":{"type":"tool","tool":"bash",
14
+ * "state":{"status":"completed","input":{"command":"…"},"title":"…"}}}
15
+ * {"type":"step_finish","part":{"type":"step-finish","reason":"stop"|"tool-calls",
16
+ * "tokens":{"total","input","output","reasoning","cache":{"read","write"}},"cost":0}}
17
+ * {"type":"error", "error":{"name":"…","data":{"message":"…"}}} ← fatal
18
+ *
19
+ * Mapping to the host-neutral contract:
20
+ * - output = accumulated `text` parts of the LAST step (text before a
21
+ * tool call is intermediate reasoning → reset on tool_use)
22
+ * - usage.input = Σ step tokens.input; usage.output = Σ (output + reasoning);
23
+ * cacheRead/cacheWrite from tokens.cache; cost = Σ part.cost;
24
+ * turns = number of step_finish events; contextTokens = latest tokens.total
25
+ * - failure = an `error` event, or a non-zero exit
26
+ *
27
+ * Permission mapping (the codex `sandboxForTools` analogue): OpenCode has no
28
+ * per-run tool-whitelist flag, but it honours a per-process config injected via
29
+ * the `OPENCODE_CONFIG_CONTENT` env var. A read-only phase (no write/edit/bash
30
+ * in its whitelist) is launched with a permission policy that DENIES
31
+ * bash/write/edit — genuinely enforced (a denied tool call is rejected, not
32
+ * merely un-approved). A mutating phase (or no whitelist) runs with `--auto`
33
+ * (auto-approve every permission), the workspace-write analogue.
34
+ *
35
+ * Process handling (idle watchdog, abort, signal-kill detection, stderr cap,
36
+ * error sanitization) mirrors the other runners so behavior is uniform.
37
+ */
38
+ import { type AgentConfig, type LiveUpdate, type RunOptions, type RunResult, type SubagentRunner, type UsageStats } from "taskflow-core";
39
+ /** Accumulated state folded from an opencode JSON event stream. */
40
+ export interface OpencodeAccumulator {
41
+ usage: UsageStats;
42
+ model?: string;
43
+ /** Final answer = text parts of the last step (reset when a tool runs). */
44
+ finalText: string;
45
+ lastActivity: string;
46
+ /** Set when the stream reported a fatal `error` event. */
47
+ fatalError?: string;
48
+ }
49
+ export declare function newOpencodeAccumulator(model?: string): OpencodeAccumulator;
50
+ /**
51
+ * Fold one opencode JSON line into the accumulator. Returns a LiveUpdate when
52
+ * the stream produced new activity (for streaming), else null. Empty/malformed
53
+ * lines are ignored — robust to partial buffers and noise, unit-testable
54
+ * without spawning a process.
55
+ */
56
+ export declare function foldOpencodeEventLine(acc: OpencodeAccumulator, line: string): LiveUpdate | null;
57
+ /** Override the opencode binary (tests / unusual installs). */
58
+ export declare function opencodeBin(): string;
59
+ /**
60
+ * Decide whether a phase is read-only from its tool whitelist — the codex
61
+ * `sandboxForTools` analogue. No whitelist → not read-only (default-capable).
62
+ */
63
+ export declare function isReadOnlyPhase(tools: string[] | undefined): boolean;
64
+ /**
65
+ * Resolve a taskflow/pi model id to something `opencode run -m` accepts, or
66
+ * `undefined` to let opencode use its configured default.
67
+ *
68
+ * Unlike codex/claude (whose model ids are flat, so "contains `/`" ⇒ a pi
69
+ * provider path to drop), a *valid* opencode model IS `provider/model` (one
70
+ * slash). So we only drop ids that clearly aren't opencode models:
71
+ * - an unresolved role placeholder `{{fast}}`
72
+ * - a pi thinking suffix (`…:xhigh`)
73
+ * - a multi-segment openrouter path (`openrouter/vendor/model`, ≥ 2 slashes)
74
+ * A clean `provider/model` passes straight through.
75
+ */
76
+ export declare function resolveOpencodeModel(model: string | undefined): string | undefined;
77
+ /** Context for {@link buildOpencodeArgs} — the pure inputs to argv construction. */
78
+ export interface OpencodeArgsCtx {
79
+ systemPrompt: string;
80
+ task: string;
81
+ /** Already-resolved model (opts.model ?? agent.model). */
82
+ model?: string;
83
+ tools?: string[];
84
+ cwd?: string;
85
+ }
86
+ /** Result of {@link buildOpencodeArgs}: the argv plus whether a read-only
87
+ * permission policy should be injected via OPENCODE_CONFIG_CONTENT (the env
88
+ * side-effect is left to the caller, which owns `process.env`). */
89
+ export interface OpencodeArgs {
90
+ args: string[];
91
+ /** True for a read-only phase — the caller injects the deny-mutations config. */
92
+ readOnly: boolean;
93
+ }
94
+ /**
95
+ * Build the full `opencode run` argv from a phase's resolved context — PURE
96
+ * (no process.env, no spawn). Extracted from `runOpencodeAgentTask` so the
97
+ * host's CLI flag contract is unit-testable in CI without a live opencode
98
+ * session. Returns whether the phase is read-only so the caller can inject the
99
+ * matching OPENCODE_CONFIG_CONTENT env without re-deriving it.
100
+ *
101
+ * opencode run <prompt> --format json [--dir cwd] [-m model] [--auto]
102
+ */
103
+ export declare function buildOpencodeArgs(ctx: OpencodeArgsCtx): OpencodeArgs;
104
+ /**
105
+ * Run a single subagent task via `opencode run --format json`. Resolves the
106
+ * agent from `agents` by name; returns the same structured `RunResult` the pi,
107
+ * codex, and claude runners do.
108
+ */
109
+ export declare function runOpencodeAgentTask(defaultCwd: string, agents: AgentConfig[], agentName: string, task: string, opts: RunOptions, globalThinking?: string): Promise<RunResult>;
110
+ /**
111
+ * The OpenCode host's `SubagentRunner`. Drops into `RuntimeDeps.runTask` exactly
112
+ * like the pi/codex/claude runners, so the engine runs unchanged on OpenCode.
113
+ */
114
+ export declare const opencodeSubagentRunner: SubagentRunner<AgentConfig>;
115
+ //# sourceMappingURL=opencode-runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opencode-runner.d.ts","sourceRoot":"","sources":["../src/opencode-runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,EAIN,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,MAAM,eAAe,CAAC;AAQvB,mEAAmE;AACnE,MAAM,WAAW,mBAAmB;IACnC,KAAK,EAAE,UAAU,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,0DAA0D;IAC1D,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,sBAAsB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,mBAAmB,CAE1E;AAcD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,mBAAmB,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAwD/F;AAED,+DAA+D;AAC/D,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,SAAS,GAAG,OAAO,CAIpE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CASlF;AAED,oFAAoF;AACpF,MAAM,WAAW,eAAe;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED;;oEAEoE;AACpE,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,iFAAiF;IACjF,QAAQ,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,eAAe,GAAG,YAAY,CAWpE;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CACzC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,WAAW,EAAE,EACrB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,UAAU,EAChB,cAAc,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,SAAS,CAAC,CAiCpB;AAED;;;GAGG;AACH,eAAO,MAAM,sBAAsB,EAAE,cAAc,CAAC,WAAW,CAE9D,CAAC"}
@@ -0,0 +1,232 @@
1
+ /**
2
+ * OpenCode subagent runner — the OpenCode host's `SubagentRunner` implementation.
3
+ *
4
+ * Spawns an isolated `opencode run --format json` process per task and folds its
5
+ * JSON event stream into the same host-neutral `RunResult` the pi, codex, and
6
+ * claude runners produce, so the engine (runtime.ts) treats an OpenCode subagent
7
+ * identically.
8
+ *
9
+ * Real opencode `run --format json` schema (opencode ≥ 1.17), observed
10
+ * empirically — one JSON object per line:
11
+ * {"type":"step_start", "part":{"type":"step-start",…}}
12
+ * {"type":"text", "part":{"type":"text","text":"…"}} ← assistant text
13
+ * {"type":"tool_use", "part":{"type":"tool","tool":"bash",
14
+ * "state":{"status":"completed","input":{"command":"…"},"title":"…"}}}
15
+ * {"type":"step_finish","part":{"type":"step-finish","reason":"stop"|"tool-calls",
16
+ * "tokens":{"total","input","output","reasoning","cache":{"read","write"}},"cost":0}}
17
+ * {"type":"error", "error":{"name":"…","data":{"message":"…"}}} ← fatal
18
+ *
19
+ * Mapping to the host-neutral contract:
20
+ * - output = accumulated `text` parts of the LAST step (text before a
21
+ * tool call is intermediate reasoning → reset on tool_use)
22
+ * - usage.input = Σ step tokens.input; usage.output = Σ (output + reasoning);
23
+ * cacheRead/cacheWrite from tokens.cache; cost = Σ part.cost;
24
+ * turns = number of step_finish events; contextTokens = latest tokens.total
25
+ * - failure = an `error` event, or a non-zero exit
26
+ *
27
+ * Permission mapping (the codex `sandboxForTools` analogue): OpenCode has no
28
+ * per-run tool-whitelist flag, but it honours a per-process config injected via
29
+ * the `OPENCODE_CONFIG_CONTENT` env var. A read-only phase (no write/edit/bash
30
+ * in its whitelist) is launched with a permission policy that DENIES
31
+ * bash/write/edit — genuinely enforced (a denied tool call is rejected, not
32
+ * merely un-approved). A mutating phase (or no whitelist) runs with `--auto`
33
+ * (auto-approve every permission), the workspace-write analogue.
34
+ *
35
+ * Process handling (idle watchdog, abort, signal-kill detection, stderr cap,
36
+ * error sanitization) mirrors the other runners so behavior is uniform.
37
+ */
38
+ import { runSubagentProcess, num, unknownAgentResult, } from "taskflow-core";
39
+ import { emptyUsage } from "taskflow-core";
40
+ /** The permission policy injected (via OPENCODE_CONFIG_CONTENT) for a read-only
41
+ * phase: deny every mutating capability so a listed-tools phase without
42
+ * write/edit/bash cannot change the workspace. */
43
+ const READ_ONLY_CONFIG = JSON.stringify({ permission: { bash: "deny", write: "deny", edit: "deny" } });
44
+ export function newOpencodeAccumulator(model) {
45
+ return { usage: emptyUsage(), model, finalText: "", lastActivity: "" };
46
+ }
47
+ function shortTool(part) {
48
+ const tool = String(part?.tool ?? "tool");
49
+ const cmd = part?.state?.input?.command;
50
+ if (tool === "bash" && typeof cmd === "string" && cmd.trim()) {
51
+ const s = cmd.replace(/\s+/g, " ").trim();
52
+ return `$ ${s.length > 64 ? `${s.slice(0, 64)}…` : s}`;
53
+ }
54
+ const title = part?.state?.title;
55
+ if (typeof title === "string" && title.trim())
56
+ return `${tool}: ${title.trim()}`;
57
+ return tool;
58
+ }
59
+ /**
60
+ * Fold one opencode JSON line into the accumulator. Returns a LiveUpdate when
61
+ * the stream produced new activity (for streaming), else null. Empty/malformed
62
+ * lines are ignored — robust to partial buffers and noise, unit-testable
63
+ * without spawning a process.
64
+ */
65
+ export function foldOpencodeEventLine(acc, line) {
66
+ if (!line.trim())
67
+ return null;
68
+ let event;
69
+ try {
70
+ event = JSON.parse(line);
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ let activity = "";
76
+ const part = event.part;
77
+ switch (event.type) {
78
+ case "text":
79
+ if (part && typeof part.text === "string" && part.text) {
80
+ // Text parts stream; concatenate within the current step. A tool call
81
+ // (below) resets this, so only the LAST step's text is the answer.
82
+ acc.finalText += part.text;
83
+ activity = acc.finalText.trim();
84
+ }
85
+ break;
86
+ case "tool_use":
87
+ // A tool call means any text so far was intermediate reasoning, not the
88
+ // final answer — drop it and keep only text that follows the last tool.
89
+ acc.finalText = "";
90
+ if (part)
91
+ activity = shortTool(part);
92
+ break;
93
+ case "step_finish": {
94
+ acc.usage.turns++;
95
+ const tk = part?.tokens;
96
+ if (tk) {
97
+ acc.usage.input += num(tk.input);
98
+ acc.usage.output += num(tk.output) + num(tk.reasoning);
99
+ acc.usage.cacheRead += num(tk.cache?.read);
100
+ acc.usage.cacheWrite += num(tk.cache?.write);
101
+ // contextTokens is a host-specific gauge (NOT additive): opencode's tokens.total is
102
+ // the full last-turn context (input+output+reasoning), already cache-accounted.
103
+ acc.usage.contextTokens = num(tk.total) || acc.usage.contextTokens;
104
+ }
105
+ if (typeof part?.cost === "number")
106
+ acc.usage.cost += part.cost;
107
+ break;
108
+ }
109
+ case "error": {
110
+ const msg = (event.error?.data?.message && String(event.error.data.message)) ||
111
+ (event.error?.name && String(event.error.name)) ||
112
+ "opencode run failed";
113
+ acc.fatalError = msg;
114
+ activity = `error: ${msg}`;
115
+ break;
116
+ }
117
+ default:
118
+ return null; // step_start / other — nothing to fold.
119
+ }
120
+ if (activity)
121
+ acc.lastActivity = activity.replace(/\s+/g, " ").trim();
122
+ return { text: acc.lastActivity, usage: { ...acc.usage }, model: acc.model };
123
+ }
124
+ /** Override the opencode binary (tests / unusual installs). */
125
+ export function opencodeBin() {
126
+ return process.env.PI_TASKFLOW_OPENCODE_BIN || "opencode";
127
+ }
128
+ /**
129
+ * Decide whether a phase is read-only from its tool whitelist — the codex
130
+ * `sandboxForTools` analogue. No whitelist → not read-only (default-capable).
131
+ */
132
+ export function isReadOnlyPhase(tools) {
133
+ if (!tools || tools.length === 0)
134
+ return false;
135
+ const mutating = new Set(["write", "edit", "bash", "apply_patch"]);
136
+ return !tools.some((t) => mutating.has(t));
137
+ }
138
+ /**
139
+ * Resolve a taskflow/pi model id to something `opencode run -m` accepts, or
140
+ * `undefined` to let opencode use its configured default.
141
+ *
142
+ * Unlike codex/claude (whose model ids are flat, so "contains `/`" ⇒ a pi
143
+ * provider path to drop), a *valid* opencode model IS `provider/model` (one
144
+ * slash). So we only drop ids that clearly aren't opencode models:
145
+ * - an unresolved role placeholder `{{fast}}`
146
+ * - a pi thinking suffix (`…:xhigh`)
147
+ * - a multi-segment openrouter path (`openrouter/vendor/model`, ≥ 2 slashes)
148
+ * A clean `provider/model` passes straight through.
149
+ */
150
+ export function resolveOpencodeModel(model) {
151
+ if (!model)
152
+ return undefined;
153
+ if (/^\{\{.*\}\}$/.test(model))
154
+ return undefined; // unresolved role placeholder
155
+ // pi thinking suffix: `<model>:<level>` (e.g. "anthropic/glm-5.2:xhigh"). Only the
156
+ // colon-delimited form is dropped — a real `:tag` (no level keyword) passes
157
+ // through so opencode itself can reject it with a clear error if invalid.
158
+ if (/:\s*(?:xhigh|high|medium|low|off)$/.test(model))
159
+ return undefined;
160
+ if ((model.match(/\//g)?.length ?? 0) >= 2)
161
+ return undefined; // openrouter path (openrouter/vendor/model)
162
+ return model;
163
+ }
164
+ /**
165
+ * Build the full `opencode run` argv from a phase's resolved context — PURE
166
+ * (no process.env, no spawn). Extracted from `runOpencodeAgentTask` so the
167
+ * host's CLI flag contract is unit-testable in CI without a live opencode
168
+ * session. Returns whether the phase is read-only so the caller can inject the
169
+ * matching OPENCODE_CONFIG_CONTENT env without re-deriving it.
170
+ *
171
+ * opencode run <prompt> --format json [--dir cwd] [-m model] [--auto]
172
+ */
173
+ export function buildOpencodeArgs(ctx) {
174
+ const opencodeModel = resolveOpencodeModel(ctx.model);
175
+ const readOnly = isReadOnlyPhase(ctx.tools);
176
+ const fullPrompt = ctx.systemPrompt.trim()
177
+ ? `${ctx.systemPrompt.trim()}\n\n---\n\nTask: ${ctx.task}`
178
+ : `Task: ${ctx.task}`;
179
+ const args = ["run", fullPrompt, "--format", "json"];
180
+ if (ctx.cwd)
181
+ args.push("--dir", ctx.cwd);
182
+ if (opencodeModel)
183
+ args.push("-m", opencodeModel);
184
+ if (!readOnly)
185
+ args.push("--auto");
186
+ return { args, readOnly };
187
+ }
188
+ /**
189
+ * Run a single subagent task via `opencode run --format json`. Resolves the
190
+ * agent from `agents` by name; returns the same structured `RunResult` the pi,
191
+ * codex, and claude runners do.
192
+ */
193
+ export async function runOpencodeAgentTask(defaultCwd, agents, agentName, task, opts, globalThinking) {
194
+ const agent = agents.find((a) => a.name === agentName);
195
+ if (!agent)
196
+ return unknownAgentResult(agentName, task, agents);
197
+ const model = opts.model ?? agent.model;
198
+ const tools = opts.tools ?? agent.tools;
199
+ void globalThinking; // opencode's --variant is provider-specific; reserved.
200
+ const cwd = opts.cwd ?? defaultCwd;
201
+ const { args, readOnly } = buildOpencodeArgs({
202
+ systemPrompt: agent.systemPrompt,
203
+ task,
204
+ model,
205
+ tools,
206
+ cwd,
207
+ });
208
+ return runSubagentProcess({
209
+ agent: agentName,
210
+ task,
211
+ model,
212
+ bin: opencodeBin(),
213
+ args,
214
+ // A read-only phase injects the deny-mutations permission policy; the
215
+ // shared runner merges this over the parent env.
216
+ env: readOnly ? { ...process.env, OPENCODE_CONFIG_CONTENT: READ_ONLY_CONFIG } : undefined,
217
+ cwd,
218
+ idleTimeoutMs: opts.idleTimeoutMs,
219
+ signal: opts.signal,
220
+ onLive: opts.onLive,
221
+ acc: newOpencodeAccumulator(model),
222
+ foldLine: foldOpencodeEventLine,
223
+ });
224
+ }
225
+ /**
226
+ * The OpenCode host's `SubagentRunner`. Drops into `RuntimeDeps.runTask` exactly
227
+ * like the pi/codex/claude runners, so the engine runs unchanged on OpenCode.
228
+ */
229
+ export const opencodeSubagentRunner = {
230
+ runTask: runOpencodeAgentTask,
231
+ };
232
+ //# sourceMappingURL=opencode-runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opencode-runner.js","sourceRoot":"","sources":["../src/opencode-runner.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "taskflow-hosts",
3
+ "version": "0.1.6",
4
+ "description": "Shared host-runner collection for taskflow — the codex, claude, and opencode SubagentRunner implementations + their argv builders and event-stream parsers. The per-host MCP servers, plugin scaffolds, and bins live in codex-taskflow / claude-taskflow / opencode-taskflow; this package holds just the runners so a new host can be added in one place.",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22.19.0"
8
+ },
9
+ "exports": {
10
+ ".": {
11
+ "development": "./src/index.ts",
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./codex": {
16
+ "development": "./src/codex-runner.ts",
17
+ "types": "./dist/codex-runner.d.ts",
18
+ "default": "./dist/codex-runner.js"
19
+ },
20
+ "./claude": {
21
+ "development": "./src/claude-runner.ts",
22
+ "types": "./dist/claude-runner.d.ts",
23
+ "default": "./dist/claude-runner.js"
24
+ },
25
+ "./opencode": {
26
+ "development": "./src/opencode-runner.ts",
27
+ "types": "./dist/opencode-runner.d.ts",
28
+ "default": "./dist/opencode-runner.js"
29
+ },
30
+ "./*": {
31
+ "development": "./src/*.ts",
32
+ "types": "./dist/*.d.ts",
33
+ "default": "./dist/*.js"
34
+ }
35
+ },
36
+ "main": "./dist/index.js",
37
+ "types": "./dist/index.d.ts",
38
+ "files": [
39
+ "dist",
40
+ "README.md"
41
+ ],
42
+ "dependencies": {
43
+ "taskflow-core": "0.1.6"
44
+ },
45
+ "devDependencies": {
46
+ "typescript": "^6.0.3"
47
+ },
48
+ "keywords": [
49
+ "taskflow",
50
+ "subagent",
51
+ "codex",
52
+ "claude",
53
+ "opencode",
54
+ "orchestration"
55
+ ],
56
+ "license": "MIT",
57
+ "scripts": {
58
+ "build": "rm -rf dist && tsc -p tsconfig.build.json"
59
+ }
60
+ }