@tagma/driver-claude-code 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +55 -55
  3. package/package.json +2 -2
  4. package/src/index.ts +254 -254
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Tagma
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.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tagma
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 CHANGED
@@ -1,55 +1,55 @@
1
- # @tagma/driver-claude-code
2
-
3
- [Claude Code](https://code.claude.com/docs/en/cli-reference) driver plugin for [@tagma/sdk](https://www.npmjs.com/package/@tagma/sdk).
4
-
5
- Translates pipeline tasks into `claude -p` invocations with JSON output parsing and native session resume.
6
-
7
- ## Install
8
-
9
- ```bash
10
- bun add @tagma/driver-claude-code
11
- ```
12
-
13
- Requires the `claude` CLI to be installed and available in your PATH.
14
-
15
- ## Usage
16
-
17
- Declare the plugin in your `pipeline.yaml`:
18
-
19
- ```yaml
20
- pipeline:
21
- name: my-pipeline
22
- plugins:
23
- - '@tagma/driver-claude-code'
24
- tracks:
25
- - id: backend
26
- name: Backend
27
- driver: claude-code
28
- tasks:
29
- - id: implement
30
- name: Implement feature
31
- prompt: 'Refactor the database layer to use connection pooling'
32
- ```
33
-
34
- Or load it programmatically:
35
-
36
- ```ts
37
- import { bootstrapBuiltins, loadPlugins } from '@tagma/sdk';
38
-
39
- bootstrapBuiltins();
40
- await loadPlugins(['@tagma/driver-claude-code']);
41
- ```
42
-
43
- ## Behavior
44
-
45
- - **Model**: all tiers; default `sonnet`
46
- - **Output format**: `--output-format json` — `parseResult` extracts session ID and normalized text from the result envelope
47
- - **Session resume**: native support via `--resume <session_id>` when `continue_from` references a task with a known session ID
48
- - **System prompt**: supported via `--append-system-prompt` (driven by `agent_profile`)
49
- - **Permissions**: mapped to `--permission-mode` (`bypassPermissions` when `execute: true`, otherwise `dontAsk` with an explicit `--allowedTools` whitelist)
50
- - **Reasoning effort**: passed through via `--effort` (accepts `low|medium|high|max`)
51
- - **Windows**: auto-discovers Git Bash and sets `CLAUDE_CODE_GIT_BASH_PATH` when needed
52
-
53
- ## License
54
-
55
- MIT
1
+ # @tagma/driver-claude-code
2
+
3
+ [Claude Code](https://code.claude.com/docs/en/cli-reference) driver plugin for [@tagma/sdk](https://www.npmjs.com/package/@tagma/sdk).
4
+
5
+ Translates pipeline tasks into `claude -p` invocations with JSON output parsing and native session resume.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ bun add @tagma/driver-claude-code
11
+ ```
12
+
13
+ Requires the `claude` CLI to be installed and available in your PATH.
14
+
15
+ ## Usage
16
+
17
+ Declare the plugin in your `pipeline.yaml`:
18
+
19
+ ```yaml
20
+ pipeline:
21
+ name: my-pipeline
22
+ plugins:
23
+ - '@tagma/driver-claude-code'
24
+ tracks:
25
+ - id: backend
26
+ name: Backend
27
+ driver: claude-code
28
+ tasks:
29
+ - id: implement
30
+ name: Implement feature
31
+ prompt: 'Refactor the database layer to use connection pooling'
32
+ ```
33
+
34
+ Or load it programmatically:
35
+
36
+ ```ts
37
+ import { bootstrapBuiltins, loadPlugins } from '@tagma/sdk';
38
+
39
+ bootstrapBuiltins();
40
+ await loadPlugins(['@tagma/driver-claude-code']);
41
+ ```
42
+
43
+ ## Behavior
44
+
45
+ - **Model**: all tiers; default `sonnet`
46
+ - **Output format**: `--output-format json` — `parseResult` extracts session ID and normalized text from the result envelope
47
+ - **Session resume**: native support via `--resume <session_id>` when `continue_from` references a task with a known session ID
48
+ - **System prompt**: supported via `--append-system-prompt` (driven by `agent_profile`)
49
+ - **Permissions**: mapped to `--permission-mode` (`bypassPermissions` when `execute: true`, otherwise `dontAsk` with an explicit `--allowedTools` whitelist)
50
+ - **Reasoning effort**: passed through via `--effort` (accepts `low|medium|high|max`)
51
+ - **Windows**: auto-discovers Git Bash and sets `CLAUDE_CODE_GIT_BASH_PATH` when needed
52
+
53
+ ## License
54
+
55
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tagma/driver-claude-code",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Claude Code CLI driver for tagma-sdk pipelines",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -47,7 +47,7 @@
47
47
  "prepublishOnly": "bun run build"
48
48
  },
49
49
  "peerDependencies": {
50
- "@tagma/types": "0.3.0"
50
+ "@tagma/types": "0.4.0"
51
51
  },
52
52
  "devDependencies": {
53
53
  "bun-types": "^1.3.11",
package/src/index.ts CHANGED
@@ -1,254 +1,254 @@
1
- import { existsSync } from 'node:fs';
2
- import { isAbsolute, relative, dirname, join } from 'node:path';
3
- import type {
4
- DriverPlugin,
5
- DriverCapabilities,
6
- DriverResultMeta,
7
- TaskConfig,
8
- TrackConfig,
9
- DriverContext,
10
- SpawnSpec,
11
- Permissions,
12
- } from '@tagma/types';
13
-
14
- // Claude Code CLI reference: https://code.claude.com/docs/en/cli-reference
15
-
16
- const DEFAULT_MODEL = 'sonnet';
17
-
18
- // Claude Code CLI accepts --effort low|medium|high|max. tagma's vocabulary
19
- // is low|medium|high, so low/medium/high pass through unchanged; users who
20
- // want the claude-specific "max" tier can also set it explicitly.
21
- const VALID_EFFORT = new Set(['low', 'medium', 'high', 'max']);
22
-
23
- function resolveModel(): string {
24
- return DEFAULT_MODEL;
25
- }
26
-
27
- function resolveTools(permissions: Permissions): string {
28
- const tools = ['Grep', 'Glob'];
29
- if (permissions.read) tools.push('Read');
30
- if (permissions.write) tools.push('Edit', 'Write');
31
- if (permissions.execute) tools.push('Bash');
32
- return tools.join(',');
33
- }
34
-
35
- // Maps our Permissions to Claude Code's --permission-mode. In print (-p) mode
36
- // Claude needs non-interactive permission handling:
37
- // - `bypassPermissions` skips all checks (required for reliable Bash automation
38
- // under `execute: true`, matches the "full trust" semantics of that tier).
39
- // - `dontAsk` auto-denies anything outside `--allowedTools`, which is exactly
40
- // what we want for read/write tiers: the allowedTools whitelist already
41
- // enumerates what Claude may do, and dontAsk makes violations fail fast
42
- // instead of hanging on a prompt no one can answer in headless mode.
43
- // See: https://code.claude.com/docs/en/permission-modes
44
- function resolvePermissionMode(permissions: Permissions): string {
45
- if (permissions.execute) return 'bypassPermissions';
46
- return 'dontAsk';
47
- }
48
-
49
- // Returns true if `sub` is inside `root` (or equal to it).
50
- function isInside(root: string, sub: string): boolean {
51
- const rel = relative(root, sub);
52
- return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
53
- }
54
-
55
- // Claude Code requires CLAUDE_CODE_GIT_BASH_PATH on Windows pointing to
56
- // Git Bash (bin\bash.exe under a Git for Windows install). See:
57
- // https://code.claude.com/docs/en/troubleshooting#windows-claude-code-on-windows-requires-git-bash
58
- // The path must use native Windows backslashes — forward slashes are rejected
59
- // by Claude Code's path validation.
60
- function resolveGitBashEnv(): Record<string, string> {
61
- if (process.platform !== 'win32') return {};
62
-
63
- // Respect user-provided value if it points to an actual file. If the user
64
- // set it to a non-existent path, fall through to discovery rather than
65
- // propagating the broken config.
66
- const existing = process.env.CLAUDE_CODE_GIT_BASH_PATH;
67
- if (existing && existsSync(existing)) return {};
68
-
69
- const discovered = discoverGitBash();
70
- return discovered ? { CLAUDE_CODE_GIT_BASH_PATH: discovered } : {};
71
- }
72
-
73
- function discoverGitBash(): string | null {
74
- // Strategy 1: find git.exe in PATH (equivalent to `where.exe git`) and
75
- // walk up looking for bin\bash.exe under a Git install root. Git for
76
- // Windows may expose multiple git.exe locations (cmd\git.exe,
77
- // mingw64\bin\git.exe, mingw64\libexec\git-core\git.exe), so we walk up
78
- // several levels rather than assuming a fixed depth.
79
- const gitExe = findExeInPath('git.exe');
80
- if (gitExe) {
81
- let dir = dirname(gitExe);
82
- for (let depth = 0; depth < 5; depth++) {
83
- const candidate = join(dir, 'bin', 'bash.exe');
84
- if (existsSync(candidate)) return candidate;
85
- const parent = dirname(dir);
86
- if (parent === dir) break;
87
- dir = parent;
88
- }
89
- }
90
-
91
- // Strategy 2: check common Git for Windows install locations.
92
- // Uses %ProgramFiles%/%LOCALAPPDATA%/%USERPROFILE% env vars so it works on
93
- // systems where those aren't mapped to C:\ (e.g. localized Windows).
94
- const programFiles = process.env['ProgramFiles'] ?? 'C:\\Program Files';
95
- const programFilesX86 = process.env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)';
96
- const localAppData = process.env['LOCALAPPDATA'];
97
- const userProfile = process.env['USERPROFILE'];
98
-
99
- const candidates = [
100
- join(programFiles, 'Git', 'bin', 'bash.exe'),
101
- join(programFilesX86, 'Git', 'bin', 'bash.exe'),
102
- // Git for Windows user-level install
103
- localAppData && join(localAppData, 'Programs', 'Git', 'bin', 'bash.exe'),
104
- // Scoop
105
- userProfile && join(userProfile, 'scoop', 'apps', 'git', 'current', 'bin', 'bash.exe'),
106
- // Chocolatey default
107
- 'C:\\tools\\git\\bin\\bash.exe',
108
- ].filter((p): p is string => Boolean(p));
109
-
110
- for (const c of candidates) {
111
- if (existsSync(c)) return c;
112
- }
113
-
114
- // Strategy 3: scan PATH for any entry containing "git" (e.g. Git's
115
- // mingw64/bin or usr/bin already in PATH), walk up to find bash.exe.
116
- // Catches custom install locations.
117
- const pathEntries = (process.env.PATH ?? '').split(';');
118
- for (const entry of pathEntries) {
119
- if (!/git/i.test(entry)) continue;
120
- const normalized = entry.replace(/\//g, '\\').replace(/\\+$/, '');
121
- const parts = normalized.split('\\');
122
- for (let depth = 1; depth <= 4; depth++) {
123
- const root = parts.slice(0, parts.length - depth).join('\\');
124
- if (!root) continue;
125
- const candidate = root + '\\bin\\bash.exe';
126
- if (existsSync(candidate)) return candidate;
127
- }
128
- }
129
-
130
- return null;
131
- }
132
-
133
- function findExeInPath(exe: string): string | null {
134
- const pathDirs = (process.env.PATH ?? '').split(';');
135
- for (const dir of pathDirs) {
136
- if (!dir) continue;
137
- const full = join(dir, exe);
138
- if (existsSync(full)) return full;
139
- }
140
- return null;
141
- }
142
-
143
- const ClaudeCodeDriver: DriverPlugin = {
144
- name: 'claude-code',
145
-
146
- capabilities: {
147
- sessionResume: true,
148
- systemPrompt: true,
149
- outputFormat: true,
150
- } satisfies DriverCapabilities,
151
-
152
- resolveModel,
153
- resolveTools,
154
-
155
- async buildCommand(task: TaskConfig, track: TrackConfig, ctx: DriverContext): Promise<SpawnSpec> {
156
- const permissions = task.permissions ?? track.permissions!;
157
- const model = task.model ?? track.model ?? DEFAULT_MODEL;
158
- // SDK schema layer already resolved task → track → pipeline inheritance.
159
- // Drop unknown effort values so a typo can't break `claude -p` startup;
160
- // validateRaw / the UI should prevent this from reaching us in practice.
161
- const rawEffort = task.reasoning_effort ?? track.reasoning_effort;
162
- const effort = rawEffort && VALID_EFFORT.has(rawEffort) ? rawEffort : null;
163
- const tools = resolveTools(permissions);
164
- const permissionMode = resolvePermissionMode(permissions);
165
-
166
- // Pass the prompt via stdin instead of as a -p argument value. On Windows,
167
- // multi-line strings in CLI arguments break cmd.exe argument parsing when
168
- // the executable is a .cmd wrapper — newlines cause all subsequent flags
169
- // (--output-format, --model, etc.) to be silently dropped.
170
- const stdin = task.prompt!;
171
-
172
- const args: string[] = [
173
- 'claude',
174
- '-p', // no value — prompt is piped via stdin
175
- '--model',
176
- model,
177
- '--allowedTools',
178
- tools,
179
- '--permission-mode',
180
- permissionMode,
181
- '--output-format',
182
- 'json',
183
- // NOTE: do NOT use --verbose here. It changes stdout from a single JSON
184
- // result object to a JSON event-stream array, breaking parseResult's
185
- // session_id extraction (needed for continue_from) and normalizedOutput.
186
- // The engine already captures stdout/stderr for pipeline logs.
187
- // Pin to project+local settings only; don't inherit arbitrary user-level
188
- // config (hooks, MCP servers, etc.) into pipeline automation.
189
- '--setting-sources',
190
- 'project,local',
191
- ];
192
-
193
- if (effort) {
194
- args.push('--effort', effort);
195
- }
196
-
197
- // If the task runs in a subdirectory of the project, grant read/edit
198
- // access to the project root via --add-dir so Claude can still see
199
- // shared files (configs, types, etc.) outside task.cwd.
200
- const effectiveCwd = task.cwd ?? ctx.workDir;
201
- if (effectiveCwd !== ctx.workDir && isInside(ctx.workDir, effectiveCwd)) {
202
- args.push('--add-dir', ctx.workDir);
203
- }
204
-
205
- // Native session resume
206
- if (task.continue_from) {
207
- const sessionId = ctx.sessionMap.get(task.continue_from);
208
- if (sessionId) {
209
- args.push('--resume', sessionId);
210
- }
211
- }
212
-
213
- // --append-system-prompt MUST be last: its value may contain newlines,
214
- // and on Windows cmd.exe can silently drop any flags that follow a
215
- // newline-containing argument.
216
- const profile = task.agent_profile ?? track.agent_profile;
217
- if (profile) {
218
- args.push('--append-system-prompt', profile);
219
- }
220
-
221
- return { args, cwd: effectiveCwd, env: resolveGitBashEnv(), stdin };
222
- },
223
-
224
- parseResult(stdout: string): DriverResultMeta {
225
- try {
226
- let json = JSON.parse(stdout);
227
-
228
- // --verbose produces a JSON array of events; extract the final "result"
229
- // event so session_id and normalizedOutput are correctly populated.
230
- if (Array.isArray(json)) {
231
- const resultEvent = json.findLast((e: Record<string, unknown>) => e.type === 'result');
232
- if (!resultEvent) return { normalizedOutput: stdout };
233
- json = resultEvent;
234
- }
235
-
236
- // Extract canonical text: strip JSON envelope so downstream drivers
237
- // get the actual AI response, not metadata
238
- const normalizedOutput = json.result ?? json.text ?? json.content ?? stdout;
239
- return {
240
- sessionId: json.session_id,
241
- normalizedOutput:
242
- typeof normalizedOutput === 'string'
243
- ? normalizedOutput
244
- : JSON.stringify(normalizedOutput),
245
- };
246
- } catch {
247
- return { normalizedOutput: stdout };
248
- }
249
- },
250
- };
251
-
252
- export const pluginCategory = 'drivers';
253
- export const pluginType = 'claude-code';
254
- export default ClaudeCodeDriver;
1
+ import { existsSync } from 'node:fs';
2
+ import { isAbsolute, relative, dirname, join } from 'node:path';
3
+ import type {
4
+ DriverPlugin,
5
+ DriverCapabilities,
6
+ DriverResultMeta,
7
+ TaskConfig,
8
+ TrackConfig,
9
+ DriverContext,
10
+ SpawnSpec,
11
+ Permissions,
12
+ } from '@tagma/types';
13
+
14
+ // Claude Code CLI reference: https://code.claude.com/docs/en/cli-reference
15
+
16
+ const DEFAULT_MODEL = 'sonnet';
17
+
18
+ // Claude Code CLI accepts --effort low|medium|high|max. tagma's vocabulary
19
+ // is low|medium|high, so low/medium/high pass through unchanged; users who
20
+ // want the claude-specific "max" tier can also set it explicitly.
21
+ const VALID_EFFORT = new Set(['low', 'medium', 'high', 'max']);
22
+
23
+ function resolveModel(): string {
24
+ return DEFAULT_MODEL;
25
+ }
26
+
27
+ function resolveTools(permissions: Permissions): string {
28
+ const tools = ['Grep', 'Glob'];
29
+ if (permissions.read) tools.push('Read');
30
+ if (permissions.write) tools.push('Edit', 'Write');
31
+ if (permissions.execute) tools.push('Bash');
32
+ return tools.join(',');
33
+ }
34
+
35
+ // Maps our Permissions to Claude Code's --permission-mode. In print (-p) mode
36
+ // Claude needs non-interactive permission handling:
37
+ // - `bypassPermissions` skips all checks (required for reliable Bash automation
38
+ // under `execute: true`, matches the "full trust" semantics of that tier).
39
+ // - `dontAsk` auto-denies anything outside `--allowedTools`, which is exactly
40
+ // what we want for read/write tiers: the allowedTools whitelist already
41
+ // enumerates what Claude may do, and dontAsk makes violations fail fast
42
+ // instead of hanging on a prompt no one can answer in headless mode.
43
+ // See: https://code.claude.com/docs/en/permission-modes
44
+ function resolvePermissionMode(permissions: Permissions): string {
45
+ if (permissions.execute) return 'bypassPermissions';
46
+ return 'dontAsk';
47
+ }
48
+
49
+ // Returns true if `sub` is inside `root` (or equal to it).
50
+ function isInside(root: string, sub: string): boolean {
51
+ const rel = relative(root, sub);
52
+ return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
53
+ }
54
+
55
+ // Claude Code requires CLAUDE_CODE_GIT_BASH_PATH on Windows pointing to
56
+ // Git Bash (bin\bash.exe under a Git for Windows install). See:
57
+ // https://code.claude.com/docs/en/troubleshooting#windows-claude-code-on-windows-requires-git-bash
58
+ // The path must use native Windows backslashes — forward slashes are rejected
59
+ // by Claude Code's path validation.
60
+ function resolveGitBashEnv(): Record<string, string> {
61
+ if (process.platform !== 'win32') return {};
62
+
63
+ // Respect user-provided value if it points to an actual file. If the user
64
+ // set it to a non-existent path, fall through to discovery rather than
65
+ // propagating the broken config.
66
+ const existing = process.env.CLAUDE_CODE_GIT_BASH_PATH;
67
+ if (existing && existsSync(existing)) return {};
68
+
69
+ const discovered = discoverGitBash();
70
+ return discovered ? { CLAUDE_CODE_GIT_BASH_PATH: discovered } : {};
71
+ }
72
+
73
+ function discoverGitBash(): string | null {
74
+ // Strategy 1: find git.exe in PATH (equivalent to `where.exe git`) and
75
+ // walk up looking for bin\bash.exe under a Git install root. Git for
76
+ // Windows may expose multiple git.exe locations (cmd\git.exe,
77
+ // mingw64\bin\git.exe, mingw64\libexec\git-core\git.exe), so we walk up
78
+ // several levels rather than assuming a fixed depth.
79
+ const gitExe = findExeInPath('git.exe');
80
+ if (gitExe) {
81
+ let dir = dirname(gitExe);
82
+ for (let depth = 0; depth < 5; depth++) {
83
+ const candidate = join(dir, 'bin', 'bash.exe');
84
+ if (existsSync(candidate)) return candidate;
85
+ const parent = dirname(dir);
86
+ if (parent === dir) break;
87
+ dir = parent;
88
+ }
89
+ }
90
+
91
+ // Strategy 2: check common Git for Windows install locations.
92
+ // Uses %ProgramFiles%/%LOCALAPPDATA%/%USERPROFILE% env vars so it works on
93
+ // systems where those aren't mapped to C:\ (e.g. localized Windows).
94
+ const programFiles = process.env['ProgramFiles'] ?? 'C:\\Program Files';
95
+ const programFilesX86 = process.env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)';
96
+ const localAppData = process.env['LOCALAPPDATA'];
97
+ const userProfile = process.env['USERPROFILE'];
98
+
99
+ const candidates = [
100
+ join(programFiles, 'Git', 'bin', 'bash.exe'),
101
+ join(programFilesX86, 'Git', 'bin', 'bash.exe'),
102
+ // Git for Windows user-level install
103
+ localAppData && join(localAppData, 'Programs', 'Git', 'bin', 'bash.exe'),
104
+ // Scoop
105
+ userProfile && join(userProfile, 'scoop', 'apps', 'git', 'current', 'bin', 'bash.exe'),
106
+ // Chocolatey default
107
+ 'C:\\tools\\git\\bin\\bash.exe',
108
+ ].filter((p): p is string => Boolean(p));
109
+
110
+ for (const c of candidates) {
111
+ if (existsSync(c)) return c;
112
+ }
113
+
114
+ // Strategy 3: scan PATH for any entry containing "git" (e.g. Git's
115
+ // mingw64/bin or usr/bin already in PATH), walk up to find bash.exe.
116
+ // Catches custom install locations.
117
+ const pathEntries = (process.env.PATH ?? '').split(';');
118
+ for (const entry of pathEntries) {
119
+ if (!/git/i.test(entry)) continue;
120
+ const normalized = entry.replace(/\//g, '\\').replace(/\\+$/, '');
121
+ const parts = normalized.split('\\');
122
+ for (let depth = 1; depth <= 4; depth++) {
123
+ const root = parts.slice(0, parts.length - depth).join('\\');
124
+ if (!root) continue;
125
+ const candidate = root + '\\bin\\bash.exe';
126
+ if (existsSync(candidate)) return candidate;
127
+ }
128
+ }
129
+
130
+ return null;
131
+ }
132
+
133
+ function findExeInPath(exe: string): string | null {
134
+ const pathDirs = (process.env.PATH ?? '').split(';');
135
+ for (const dir of pathDirs) {
136
+ if (!dir) continue;
137
+ const full = join(dir, exe);
138
+ if (existsSync(full)) return full;
139
+ }
140
+ return null;
141
+ }
142
+
143
+ const ClaudeCodeDriver: DriverPlugin = {
144
+ name: 'claude-code',
145
+
146
+ capabilities: {
147
+ sessionResume: true,
148
+ systemPrompt: true,
149
+ outputFormat: true,
150
+ } satisfies DriverCapabilities,
151
+
152
+ resolveModel,
153
+ resolveTools,
154
+
155
+ async buildCommand(task: TaskConfig, track: TrackConfig, ctx: DriverContext): Promise<SpawnSpec> {
156
+ const permissions = task.permissions ?? track.permissions!;
157
+ const model = task.model ?? track.model ?? DEFAULT_MODEL;
158
+ // SDK schema layer already resolved task → track → pipeline inheritance.
159
+ // Drop unknown effort values so a typo can't break `claude -p` startup;
160
+ // validateRaw / the UI should prevent this from reaching us in practice.
161
+ const rawEffort = task.reasoning_effort ?? track.reasoning_effort;
162
+ const effort = rawEffort && VALID_EFFORT.has(rawEffort) ? rawEffort : null;
163
+ const tools = resolveTools(permissions);
164
+ const permissionMode = resolvePermissionMode(permissions);
165
+
166
+ // Pass the prompt via stdin instead of as a -p argument value. On Windows,
167
+ // multi-line strings in CLI arguments break cmd.exe argument parsing when
168
+ // the executable is a .cmd wrapper — newlines cause all subsequent flags
169
+ // (--output-format, --model, etc.) to be silently dropped.
170
+ const stdin = task.prompt!;
171
+
172
+ const args: string[] = [
173
+ 'claude',
174
+ '-p', // no value — prompt is piped via stdin
175
+ '--model',
176
+ model,
177
+ '--allowedTools',
178
+ tools,
179
+ '--permission-mode',
180
+ permissionMode,
181
+ '--output-format',
182
+ 'json',
183
+ // NOTE: do NOT use --verbose here. It changes stdout from a single JSON
184
+ // result object to a JSON event-stream array, breaking parseResult's
185
+ // session_id extraction (needed for continue_from) and normalizedOutput.
186
+ // The engine already captures stdout/stderr for pipeline logs.
187
+ // Pin to project+local settings only; don't inherit arbitrary user-level
188
+ // config (hooks, MCP servers, etc.) into pipeline automation.
189
+ '--setting-sources',
190
+ 'project,local',
191
+ ];
192
+
193
+ if (effort) {
194
+ args.push('--effort', effort);
195
+ }
196
+
197
+ // If the task runs in a subdirectory of the project, grant read/edit
198
+ // access to the project root via --add-dir so Claude can still see
199
+ // shared files (configs, types, etc.) outside task.cwd.
200
+ const effectiveCwd = task.cwd ?? ctx.workDir;
201
+ if (effectiveCwd !== ctx.workDir && isInside(ctx.workDir, effectiveCwd)) {
202
+ args.push('--add-dir', ctx.workDir);
203
+ }
204
+
205
+ // Native session resume
206
+ if (task.continue_from) {
207
+ const sessionId = ctx.sessionMap.get(task.continue_from);
208
+ if (sessionId) {
209
+ args.push('--resume', sessionId);
210
+ }
211
+ }
212
+
213
+ // --append-system-prompt MUST be last: its value may contain newlines,
214
+ // and on Windows cmd.exe can silently drop any flags that follow a
215
+ // newline-containing argument.
216
+ const profile = task.agent_profile ?? track.agent_profile;
217
+ if (profile) {
218
+ args.push('--append-system-prompt', profile);
219
+ }
220
+
221
+ return { args, cwd: effectiveCwd, env: resolveGitBashEnv(), stdin };
222
+ },
223
+
224
+ parseResult(stdout: string): DriverResultMeta {
225
+ try {
226
+ let json = JSON.parse(stdout);
227
+
228
+ // --verbose produces a JSON array of events; extract the final "result"
229
+ // event so session_id and normalizedOutput are correctly populated.
230
+ if (Array.isArray(json)) {
231
+ const resultEvent = json.findLast((e: Record<string, unknown>) => e.type === 'result');
232
+ if (!resultEvent) return { normalizedOutput: stdout };
233
+ json = resultEvent;
234
+ }
235
+
236
+ // Extract canonical text: strip JSON envelope so downstream drivers
237
+ // get the actual AI response, not metadata
238
+ const normalizedOutput = json.result ?? json.text ?? json.content ?? stdout;
239
+ return {
240
+ sessionId: json.session_id,
241
+ normalizedOutput:
242
+ typeof normalizedOutput === 'string'
243
+ ? normalizedOutput
244
+ : JSON.stringify(normalizedOutput),
245
+ };
246
+ } catch {
247
+ return { normalizedOutput: stdout };
248
+ }
249
+ },
250
+ };
251
+
252
+ export const pluginCategory = 'drivers';
253
+ export const pluginType = 'claude-code';
254
+ export default ClaudeCodeDriver;