@tagma/sdk 0.2.1 → 0.2.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tagma/sdk",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  "plugins/*"
@@ -151,24 +151,28 @@ export const ClaudeCodeDriver: DriverPlugin = {
151
151
  const tools = resolveTools(permissions);
152
152
  const permissionMode = resolvePermissionMode(permissions);
153
153
 
154
+ // Pass the prompt via stdin instead of as a -p argument value. On Windows,
155
+ // multi-line strings in CLI arguments break cmd.exe argument parsing when
156
+ // the executable is a .cmd wrapper — newlines cause all subsequent flags
157
+ // (--output-format, --model, etc.) to be silently dropped.
158
+ const stdin = task.prompt!;
159
+
154
160
  const args: string[] = [
155
161
  'claude',
156
- '-p', task.prompt!,
162
+ '-p', // no value — prompt is piped via stdin
157
163
  '--model', model,
158
164
  '--allowedTools', tools,
159
165
  '--permission-mode', permissionMode,
160
166
  '--output-format', 'json',
161
- '--verbose',
167
+ // NOTE: do NOT use --verbose here. It changes stdout from a single JSON
168
+ // result object to a JSON event-stream array, breaking parseResult's
169
+ // session_id extraction (needed for continue_from) and normalizedOutput.
170
+ // The engine already captures stdout/stderr for pipeline logs.
162
171
  // Pin to project+local settings only; don't inherit arbitrary user-level
163
172
  // config (hooks, MCP servers, etc.) into pipeline automation.
164
173
  '--setting-sources', 'project,local',
165
174
  ];
166
175
 
167
- const profile = task.agent_profile ?? track.agent_profile;
168
- if (profile) {
169
- args.push('--append-system-prompt', profile);
170
- }
171
-
172
176
  // If the task runs in a subdirectory of the project, grant read/edit
173
177
  // access to the project root via --add-dir so Claude can still see
174
178
  // shared files (configs, types, etc.) outside task.cwd.
@@ -185,12 +189,29 @@ export const ClaudeCodeDriver: DriverPlugin = {
185
189
  }
186
190
  }
187
191
 
188
- return { args, cwd: effectiveCwd, env: resolveGitBashEnv() };
192
+ // --append-system-prompt MUST be last: its value may contain newlines,
193
+ // and on Windows cmd.exe can silently drop any flags that follow a
194
+ // newline-containing argument.
195
+ const profile = task.agent_profile ?? track.agent_profile;
196
+ if (profile) {
197
+ args.push('--append-system-prompt', profile);
198
+ }
199
+
200
+ return { args, cwd: effectiveCwd, env: resolveGitBashEnv(), stdin };
189
201
  },
190
202
 
191
203
  parseResult(stdout: string): DriverResultMeta {
192
204
  try {
193
- const json = JSON.parse(stdout);
205
+ let json = JSON.parse(stdout);
206
+
207
+ // --verbose produces a JSON array of events; extract the final "result"
208
+ // event so session_id and normalizedOutput are correctly populated.
209
+ if (Array.isArray(json)) {
210
+ const resultEvent = json.findLast((e: Record<string, unknown>) => e.type === 'result');
211
+ if (!resultEvent) return { normalizedOutput: stdout };
212
+ json = resultEvent;
213
+ }
214
+
194
215
  // Extract canonical text: strip JSON envelope so downstream drivers
195
216
  // get the actual AI response, not metadata
196
217
  const normalizedOutput = json.result ?? json.text ?? json.content ?? stdout;
package/src/runner.ts CHANGED
@@ -6,6 +6,24 @@ import { shellArgs } from './utils';
6
6
  // Delay before escalating SIGTERM to SIGKILL when killing a timed-out process.
7
7
  const SIGKILL_DELAY_MS = 3_000;
8
8
 
9
+ /**
10
+ * On Windows, proc.kill('SIGTERM') / proc.kill('SIGKILL') only terminate the
11
+ * direct child process. When the child is a .cmd/.bat wrapper (e.g. claude.cmd),
12
+ * cmd.exe spawns the real process as a grandchild — proc.kill misses it entirely.
13
+ * `taskkill /F /T /PID` kills the entire process tree rooted at the given PID.
14
+ */
15
+ function killProcessTree(pid: number): void {
16
+ if (process.platform !== 'win32') return;
17
+ try {
18
+ Bun.spawnSync(['taskkill', '/F', '/T', '/PID', String(pid)], {
19
+ stdout: 'ignore',
20
+ stderr: 'ignore',
21
+ });
22
+ } catch {
23
+ /* best-effort — process may have already exited */
24
+ }
25
+ }
26
+
9
27
  export interface RunOptions {
10
28
  readonly timeoutMs?: number;
11
29
  readonly signal?: AbortSignal; // pipeline-level abort
@@ -128,15 +146,22 @@ export async function runSpawn(
128
146
  const killGracefully = () => {
129
147
  if (killedByUs) return;
130
148
  killedByUs = true;
131
- proc.kill('SIGTERM');
132
- // If the child ignores SIGTERM, escalate to SIGKILL after 3 s.
133
- forceTimer = setTimeout(() => {
134
- try {
135
- proc.kill('SIGKILL');
136
- } catch {
137
- /* already exited */
138
- }
139
- }, SIGKILL_DELAY_MS);
149
+
150
+ if (process.platform === 'win32') {
151
+ // On Windows, kill the entire process tree via taskkill. This handles
152
+ // .cmd wrappers and nested child processes that proc.kill() misses.
153
+ killProcessTree(proc.pid);
154
+ } else {
155
+ proc.kill('SIGTERM');
156
+ // If the child ignores SIGTERM, escalate to SIGKILL after 3 s.
157
+ forceTimer = setTimeout(() => {
158
+ try {
159
+ proc.kill('SIGKILL');
160
+ } catch {
161
+ /* already exited */
162
+ }
163
+ }, SIGKILL_DELAY_MS);
164
+ }
140
165
  };
141
166
 
142
167
  if (timeoutMs && timeoutMs > 0) {