@tagma/sdk 0.2.0 → 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 +1 -1
- package/src/drivers/claude-code.ts +30 -9
- package/src/engine.ts +3 -3
- package/src/pipeline-runner.ts +3 -3
- package/src/runner.ts +34 -9
package/package.json
CHANGED
|
@@ -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',
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/engine.ts
CHANGED
|
@@ -750,10 +750,10 @@ function freezeStates(states: Map<string, TaskState>): ReadonlyMap<string, TaskS
|
|
|
750
750
|
const copy = new Map<string, TaskState>();
|
|
751
751
|
for (const [id, s] of states) {
|
|
752
752
|
copy.set(id, {
|
|
753
|
-
config: s.config,
|
|
754
|
-
trackConfig: s.trackConfig,
|
|
753
|
+
config: { ...s.config },
|
|
754
|
+
trackConfig: { ...s.trackConfig },
|
|
755
755
|
status: s.status,
|
|
756
|
-
result: s.result,
|
|
756
|
+
result: s.result ? { ...s.result } : null,
|
|
757
757
|
startedAt: s.startedAt,
|
|
758
758
|
finishedAt: s.finishedAt,
|
|
759
759
|
});
|
package/src/pipeline-runner.ts
CHANGED
|
@@ -129,10 +129,10 @@ function snapshotStates(src: ReadonlyMap<string, TaskState>): ReadonlyMap<string
|
|
|
129
129
|
const copy = new Map<string, TaskState>();
|
|
130
130
|
for (const [id, s] of src) {
|
|
131
131
|
copy.set(id, {
|
|
132
|
-
config: s.config,
|
|
133
|
-
trackConfig: s.trackConfig,
|
|
132
|
+
config: { ...s.config },
|
|
133
|
+
trackConfig: { ...s.trackConfig },
|
|
134
134
|
status: s.status,
|
|
135
|
-
result: s.result,
|
|
135
|
+
result: s.result ? { ...s.result } : null,
|
|
136
136
|
startedAt: s.startedAt,
|
|
137
137
|
finishedAt: s.finishedAt,
|
|
138
138
|
});
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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) {
|