@kb-labs/workflow-steps 2.119.0-canary.e120bc0ee → 2.119.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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/approval.ts"],"names":[],"mappings":";AA2CO,IAAM,kBAAN,MAAsB;AAAA,EAC3B,OAAgB,IAAA,GAAO,kBAAA;AAAA,EAEvB,OAAO,MAAA,EAAkE;AACvE,IAAA,OAAO,EAAE,MAAA,EAAQ,SAAA,EAAW,MAAA,EAAQ,UAAA,EAAW;AAAA,EACjD;AACF","file":"approval.js","sourcesContent":["/**\n * @module @kb-labs/workflow-steps/approval\n * Types and handler for builtin:approval step\n *\n * Approval steps pause the pipeline and wait for human decision.\n * The worker handles polling; resolveApproval() on the engine resumes execution.\n */\n\n/**\n * Input for builtin:approval step (spec.with)\n */\nexport interface ApprovalInput {\n /** Display title for the approval request */\n title: string;\n\n /** Contextual data shown to the approver (already interpolated) */\n context?: Record<string, unknown>;\n\n /** Optional instructions for the approver */\n instructions?: string;\n}\n\n/**\n * Output produced by a resolved approval step\n */\nexport interface ApprovalOutput {\n /** Whether the approval was granted */\n approved: boolean;\n\n /** Action taken: \"approve\" or \"reject\" */\n action: 'approve' | 'reject';\n\n /** Optional comment from the approver */\n comment?: string;\n\n /** Additional data provided by the approver */\n [key: string]: unknown;\n}\n\n/**\n * ApprovalHandler — signals the worker to pause and wait for human approval.\n * The worker owns the polling loop; this handler only marks the intent.\n */\nexport class ApprovalHandler {\n static readonly uses = 'builtin:approval';\n\n handle(_input: ApprovalInput): { status: 'waiting'; reason: 'approval' } {\n return { status: 'waiting', reason: 'approval' };\n }\n}\n"]}
1
+ {"version":3,"sources":["../../home/runner/work/kb-labs/kb-labs/plugins/workflow/steps/src/approval.ts"],"names":[],"mappings":";AA2CO,IAAM,kBAAN,MAAsB;AAAA,EAC3B,OAAgB,IAAA,GAAO,kBAAA;AAAA,EAEvB,OAAO,MAAA,EAAkE;AACvE,IAAA,OAAO,EAAE,MAAA,EAAQ,SAAA,EAAW,MAAA,EAAQ,UAAA,EAAW;AAAA,EACjD;AACF","file":"approval.js","sourcesContent":["/**\n * @module @kb-labs/workflow-steps/approval\n * Types and handler for builtin:approval step\n *\n * Approval steps pause the pipeline and wait for human decision.\n * The worker handles polling; resolveApproval() on the engine resumes execution.\n */\n\n/**\n * Input for builtin:approval step (spec.with)\n */\nexport interface ApprovalInput {\n /** Display title for the approval request */\n title: string;\n\n /** Contextual data shown to the approver (already interpolated) */\n context?: Record<string, unknown>;\n\n /** Optional instructions for the approver */\n instructions?: string;\n}\n\n/**\n * Output produced by a resolved approval step\n */\nexport interface ApprovalOutput {\n /** Whether the approval was granted */\n approved: boolean;\n\n /** Action taken: \"approve\" or \"reject\" */\n action: 'approve' | 'reject';\n\n /** Optional comment from the approver */\n comment?: string;\n\n /** Additional data provided by the approver */\n [key: string]: unknown;\n}\n\n/**\n * ApprovalHandler — signals the worker to pause and wait for human approval.\n * The worker owns the polling loop; this handler only marks the intent.\n */\nexport class ApprovalHandler {\n static readonly uses = 'builtin:approval';\n\n handle(_input: ApprovalInput): { status: 'waiting'; reason: 'approval' } {\n return { status: 'waiting', reason: 'approval' };\n }\n}\n"]}
package/dist/gate.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/gate.ts"],"names":[],"mappings":"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","file":"gate.js","sourcesContent":["/**\n * @module @kb-labs/workflow-steps/gate\n * Types and handler for builtin:gate step\n *\n * Gate steps act as automatic routers — they read a decision value\n * from previous step outputs and route the pipeline accordingly:\n * - continue: proceed to next step\n * - fail: fail the pipeline\n * - restart: reset steps back to target and re-schedule with context\n * - skip: mark intermediate steps as skipped and jump forward to a target step\n */\n\nimport { resolveValue, type ExpressionContext } from '@kb-labs/workflow-contracts';\n\n/**\n * Route action for a gate decision\n */\nexport type GateRouteAction =\n | 'continue'\n | 'fail'\n | {\n /** Step ID to restart from (go backward) */\n restartFrom: string;\n /** Additional context to pass (merged into trigger.payload) */\n context?: Record<string, unknown>;\n }\n | {\n /** Step ID to skip forward to — all steps between gate and target are marked skipped */\n skipTo: string;\n };\n\n/**\n * Input for builtin:gate step (spec.with)\n */\nexport interface GateInput {\n /** Expression path to the decision value (e.g. \"steps.review.outputs.passed\") */\n decision: string;\n\n /** Route map: decision value → action */\n routes: Record<string, GateRouteAction>;\n\n /** Default action if decision value doesn't match any route */\n default?: 'continue' | 'fail';\n\n /** Maximum number of restart iterations before failing (default: 3) */\n maxIterations?: number;\n}\n\n/**\n * Output produced by a resolved gate step\n */\nexport interface GateOutput {\n /** The decision value that was evaluated */\n decisionValue: unknown;\n\n /** The action that was taken */\n action: 'continue' | 'fail' | 'restart' | 'skip';\n\n /** Step ID that was restarted from (if restart) */\n restartFrom?: string;\n\n /** Step ID that was skipped to (if skip) */\n skipTo?: string;\n\n /** Current iteration count */\n iteration: number;\n\n /** Set to true when the gate exhausted its maxIterations budget */\n maxIterationsReached?: boolean;\n\n [key: string]: unknown;\n}\n\n/**\n * Pure decision result from GateHandler.\n * Worker applies the state mutations based on the action.\n */\nexport type GateDecision =\n | { action: 'continue'; outputs: GateOutput }\n | { action: 'fail'; error: Error; outputs: GateOutput }\n | {\n action: 'restart';\n restartFrom: string;\n context?: Record<string, unknown>;\n outputs: GateOutput;\n nextIteration: number;\n }\n | {\n action: 'skip';\n skipTo: string;\n outputs: GateOutput;\n };\n\n/**\n * GateHandler — pure decision function.\n * No I/O, no state mutations. Worker owns the side effects.\n */\nexport class GateHandler {\n static readonly uses = 'builtin:gate';\n\n /**\n * @param validRestartTargets Identifiers the worker can reset on restart —\n * step ids/spec.ids within the gate's own job AND other job names (for\n * cross-job restart). When provided and `restartFrom` matches none of them,\n * the gate FAILS instead of attempting a restart the worker cannot apply —\n * which would otherwise silently degrade into \"complete the gate and\n * proceed\" (a false green).\n */\n handle(\n input: GateInput,\n exprCtx: ExpressionContext,\n currentIteration: number,\n validRestartTargets?: string[],\n ): GateDecision {\n const maxIterations = input.maxIterations ?? 3;\n const decisionValue = resolveValue(input.decision, exprCtx);\n const decisionKey = String(decisionValue);\n\n const route: GateRouteAction | undefined =\n input.routes[decisionKey] ?? input.routes[decisionValue as string];\n const action = route ?? input.default ?? 'fail';\n\n if (action === 'continue') {\n return {\n action: 'continue',\n outputs: { decisionValue, action: 'continue', iteration: currentIteration },\n };\n }\n\n if (action === 'fail') {\n const failReason =\n (route as { message?: string } | undefined)?.message ??\n `No matching route for value \"${decisionKey}\"`;\n return {\n action: 'fail',\n error: new Error(`Gate failed: ${failReason}`),\n outputs: { decisionValue, action: 'fail', iteration: currentIteration },\n };\n }\n\n // skipTo action — jump forward, marking intermediate steps as skipped\n if (typeof action === 'object' && 'skipTo' in action) {\n if (validRestartTargets && !validRestartTargets.includes(action.skipTo)) {\n return {\n action: 'fail',\n error: new Error(\n `Gate skipTo \"${action.skipTo}\" matches no step in this job — cannot skip.`,\n ),\n outputs: { decisionValue, action: 'fail', iteration: currentIteration },\n };\n }\n return {\n action: 'skip',\n skipTo: action.skipTo,\n outputs: {\n decisionValue,\n action: 'skip',\n skipTo: action.skipTo,\n iteration: currentIteration,\n },\n };\n }\n\n // restart action\n const restartAction = action as { restartFrom: string; context?: Record<string, unknown> };\n\n // Guard: restartFrom must name something the worker can reset — a step in\n // this job or another job by name. If it matches neither, the gate cannot\n // recover; fail honestly rather than let the worker silently complete the\n // gate and proceed (which would ship unverified work as a false green).\n if (validRestartTargets && !validRestartTargets.includes(restartAction.restartFrom)) {\n return {\n action: 'fail',\n error: new Error(\n `Gate restartFrom \"${restartAction.restartFrom}\" matches no step in this job ` +\n `or any job in the run — cannot restart. Failing instead of silently passing.`,\n ),\n outputs: { decisionValue, action: 'fail', iteration: currentIteration },\n };\n }\n\n const nextIteration = currentIteration + 1;\n\n if (nextIteration >= maxIterations) {\n return {\n action: 'fail',\n error: new Error(`Gate max iterations reached (${maxIterations}) for step`),\n outputs: {\n decisionValue,\n action: 'fail',\n iteration: currentIteration,\n maxIterationsReached: true,\n },\n };\n }\n\n return {\n action: 'restart',\n restartFrom: restartAction.restartFrom,\n context: restartAction.context,\n outputs: {\n decisionValue,\n action: 'restart',\n restartFrom: restartAction.restartFrom,\n iteration: nextIteration,\n },\n nextIteration,\n };\n }\n}\n"]}
1
+ {"version":3,"sources":["../../home/runner/work/kb-labs/kb-labs/plugins/workflow/steps/src/gate.ts"],"names":[],"mappings":"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","file":"gate.js","sourcesContent":["/**\n * @module @kb-labs/workflow-steps/gate\n * Types and handler for builtin:gate step\n *\n * Gate steps act as automatic routers — they read a decision value\n * from previous step outputs and route the pipeline accordingly:\n * - continue: proceed to next step\n * - fail: fail the pipeline\n * - restart: reset steps back to target and re-schedule with context\n * - skip: mark intermediate steps as skipped and jump forward to a target step\n */\n\nimport { resolveValue, type ExpressionContext } from '@kb-labs/workflow-contracts';\n\n/**\n * Route action for a gate decision\n */\nexport type GateRouteAction =\n | 'continue'\n | 'fail'\n | {\n /** Step ID to restart from (go backward) */\n restartFrom: string;\n /** Additional context to pass (merged into trigger.payload) */\n context?: Record<string, unknown>;\n }\n | {\n /** Step ID to skip forward to — all steps between gate and target are marked skipped */\n skipTo: string;\n };\n\n/**\n * Input for builtin:gate step (spec.with)\n */\nexport interface GateInput {\n /** Expression path to the decision value (e.g. \"steps.review.outputs.passed\") */\n decision: string;\n\n /** Route map: decision value → action */\n routes: Record<string, GateRouteAction>;\n\n /** Default action if decision value doesn't match any route */\n default?: 'continue' | 'fail';\n\n /** Maximum number of restart iterations before failing (default: 3) */\n maxIterations?: number;\n}\n\n/**\n * Output produced by a resolved gate step\n */\nexport interface GateOutput {\n /** The decision value that was evaluated */\n decisionValue: unknown;\n\n /** The action that was taken */\n action: 'continue' | 'fail' | 'restart' | 'skip';\n\n /** Step ID that was restarted from (if restart) */\n restartFrom?: string;\n\n /** Step ID that was skipped to (if skip) */\n skipTo?: string;\n\n /** Current iteration count */\n iteration: number;\n\n /** Set to true when the gate exhausted its maxIterations budget */\n maxIterationsReached?: boolean;\n\n [key: string]: unknown;\n}\n\n/**\n * Pure decision result from GateHandler.\n * Worker applies the state mutations based on the action.\n */\nexport type GateDecision =\n | { action: 'continue'; outputs: GateOutput }\n | { action: 'fail'; error: Error; outputs: GateOutput }\n | {\n action: 'restart';\n restartFrom: string;\n context?: Record<string, unknown>;\n outputs: GateOutput;\n nextIteration: number;\n }\n | {\n action: 'skip';\n skipTo: string;\n outputs: GateOutput;\n };\n\n/**\n * GateHandler — pure decision function.\n * No I/O, no state mutations. Worker owns the side effects.\n */\nexport class GateHandler {\n static readonly uses = 'builtin:gate';\n\n /**\n * @param validRestartTargets Identifiers the worker can reset on restart —\n * step ids/spec.ids within the gate's own job AND other job names (for\n * cross-job restart). When provided and `restartFrom` matches none of them,\n * the gate FAILS instead of attempting a restart the worker cannot apply —\n * which would otherwise silently degrade into \"complete the gate and\n * proceed\" (a false green).\n */\n handle(\n input: GateInput,\n exprCtx: ExpressionContext,\n currentIteration: number,\n validRestartTargets?: string[],\n ): GateDecision {\n const maxIterations = input.maxIterations ?? 3;\n const decisionValue = resolveValue(input.decision, exprCtx);\n const decisionKey = String(decisionValue);\n\n const route: GateRouteAction | undefined =\n input.routes[decisionKey] ?? input.routes[decisionValue as string];\n const action = route ?? input.default ?? 'fail';\n\n if (action === 'continue') {\n return {\n action: 'continue',\n outputs: { decisionValue, action: 'continue', iteration: currentIteration },\n };\n }\n\n if (action === 'fail') {\n const failReason =\n (route as { message?: string } | undefined)?.message ??\n `No matching route for value \"${decisionKey}\"`;\n return {\n action: 'fail',\n error: new Error(`Gate failed: ${failReason}`),\n outputs: { decisionValue, action: 'fail', iteration: currentIteration },\n };\n }\n\n // skipTo action — jump forward, marking intermediate steps as skipped\n if (typeof action === 'object' && 'skipTo' in action) {\n if (validRestartTargets && !validRestartTargets.includes(action.skipTo)) {\n return {\n action: 'fail',\n error: new Error(\n `Gate skipTo \"${action.skipTo}\" matches no step in this job — cannot skip.`,\n ),\n outputs: { decisionValue, action: 'fail', iteration: currentIteration },\n };\n }\n return {\n action: 'skip',\n skipTo: action.skipTo,\n outputs: {\n decisionValue,\n action: 'skip',\n skipTo: action.skipTo,\n iteration: currentIteration,\n },\n };\n }\n\n // restart action\n const restartAction = action as { restartFrom: string; context?: Record<string, unknown> };\n\n // Guard: restartFrom must name something the worker can reset — a step in\n // this job or another job by name. If it matches neither, the gate cannot\n // recover; fail honestly rather than let the worker silently complete the\n // gate and proceed (which would ship unverified work as a false green).\n if (validRestartTargets && !validRestartTargets.includes(restartAction.restartFrom)) {\n return {\n action: 'fail',\n error: new Error(\n `Gate restartFrom \"${restartAction.restartFrom}\" matches no step in this job ` +\n `or any job in the run — cannot restart. Failing instead of silently passing.`,\n ),\n outputs: { decisionValue, action: 'fail', iteration: currentIteration },\n };\n }\n\n const nextIteration = currentIteration + 1;\n\n if (nextIteration >= maxIterations) {\n return {\n action: 'fail',\n error: new Error(`Gate max iterations reached (${maxIterations}) for step`),\n outputs: {\n decisionValue,\n action: 'fail',\n iteration: currentIteration,\n maxIterationsReached: true,\n },\n };\n }\n\n return {\n action: 'restart',\n restartFrom: restartAction.restartFrom,\n context: restartAction.context,\n outputs: {\n decisionValue,\n action: 'restart',\n restartFrom: restartAction.restartFrom,\n iteration: nextIteration,\n },\n nextIteration,\n };\n }\n}\n"]}
package/dist/index.js CHANGED
@@ -16,13 +16,75 @@ var BLOCKED_COMMANDS = [
16
16
  "fdisk"
17
17
  ];
18
18
  var FAILURE_OUTPUT_TAIL_CHARS = 4e3;
19
+ var MARKER_LINE_MAX_CHARS = 2e3;
20
+ function lastOutputMarkerLine(text) {
21
+ const lines = text.split("\n");
22
+ for (let i = lines.length - 1; i >= 0; i--) {
23
+ const line = lines[i];
24
+ if (line !== void 0 && (line.includes(OUTPUT_MARKER_B64) || line.includes(OUTPUT_MARKER))) {
25
+ return line;
26
+ }
27
+ }
28
+ return null;
29
+ }
30
+ var LOG_BATCH_MAX_LINES = 50;
31
+ var LOG_BATCH_MAX_MS = 500;
19
32
  function tail(text, maxChars) {
20
33
  if (text.length <= maxChars) {
21
34
  return text;
22
35
  }
23
- return `\u2026(truncated, showing last ${maxChars} of ${text.length} chars)
36
+ const rawTail = `\u2026(truncated, showing last ${maxChars} of ${text.length} chars)
24
37
  ${text.slice(-maxChars)}`;
38
+ const markerLine = lastOutputMarkerLine(text);
39
+ if (!markerLine || rawTail.includes(markerLine)) {
40
+ return rawTail;
41
+ }
42
+ const markerSnippet = markerLine.length > MARKER_LINE_MAX_CHARS ? `${markerLine.slice(0, MARKER_LINE_MAX_CHARS)}\u2026(marker line truncated, ${markerLine.length} chars total)` : markerLine;
43
+ return `${markerSnippet}
44
+ ${rawTail}`;
25
45
  }
46
+ var LineBatcher = class {
47
+ constructor(stream, onFlush, maxLines = LOG_BATCH_MAX_LINES, maxMs = LOG_BATCH_MAX_MS) {
48
+ this.stream = stream;
49
+ this.onFlush = onFlush;
50
+ this.maxLines = maxLines;
51
+ this.maxMs = maxMs;
52
+ }
53
+ stream;
54
+ onFlush;
55
+ maxLines;
56
+ maxMs;
57
+ buffer = [];
58
+ timer;
59
+ add(line, lineNo) {
60
+ this.buffer.push({ line, lineNo });
61
+ if (this.buffer.length >= this.maxLines) {
62
+ this.flush();
63
+ return;
64
+ }
65
+ if (!this.timer) {
66
+ this.timer = setTimeout(() => this.flush(), this.maxMs);
67
+ }
68
+ }
69
+ /** Flush whatever is buffered right now, regardless of size/time thresholds. */
70
+ flush() {
71
+ if (this.timer) {
72
+ clearTimeout(this.timer);
73
+ this.timer = void 0;
74
+ }
75
+ if (this.buffer.length === 0) {
76
+ return;
77
+ }
78
+ const batch = this.buffer;
79
+ this.buffer = [];
80
+ this.onFlush({
81
+ stream: this.stream,
82
+ fromLine: batch[0].lineNo,
83
+ toLine: batch[batch.length - 1].lineNo,
84
+ text: batch.map((entry) => entry.line).join("\n")
85
+ });
86
+ }
87
+ };
26
88
  var OUTPUT_MARKER = "::kb-output::";
27
89
  var OUTPUT_MARKER_B64 = "::kb-output:base64::";
28
90
  function parseOutputMarkerLine(line) {
@@ -134,9 +196,21 @@ async function shellHandler(ctx, input) {
134
196
  let stderrBuf = "";
135
197
  let stdoutFull = "";
136
198
  let stderrFull = "";
199
+ const onBatchFlush = (batch) => {
200
+ const log = batch.stream === "stderr" ? ctx.platform.logger.warn : ctx.platform.logger.info;
201
+ log.call(ctx.platform.logger, `Shell output (${batch.stream})`, {
202
+ stream: batch.stream,
203
+ fromLine: batch.fromLine,
204
+ toLine: batch.toLine,
205
+ text: batch.text
206
+ });
207
+ };
208
+ const stdoutBatcher = new LineBatcher("stdout", onBatchFlush);
209
+ const stderrBatcher = new LineBatcher("stderr", onBatchFlush);
137
210
  const emitLine = (stream, line) => {
138
211
  lineNo++;
139
212
  void ctx.api.events.emit("log.line", { stream, line, lineNo, level: stream === "stderr" ? "warn" : "info" });
213
+ (stream === "stderr" ? stderrBatcher : stdoutBatcher).add(line, lineNo);
140
214
  };
141
215
  proc.stdout?.on("data", (chunk) => {
142
216
  const text = chunk.toString();
@@ -177,13 +251,26 @@ async function shellHandler(ctx, input) {
177
251
  if (stderrBuf) {
178
252
  emitLine("stderr", stderrBuf);
179
253
  }
254
+ stdoutBatcher.flush();
255
+ stderrBatcher.flush();
180
256
  if (timedOut) {
257
+ const stderrTail = tail(stderrFull, FAILURE_OUTPUT_TAIL_CHARS);
258
+ const stdoutTail = tail(stdoutFull, FAILURE_OUTPUT_TAIL_CHARS);
181
259
  ctx.platform.logger.warn("Shell command timed out", {
182
260
  timeoutMs: timeout,
183
- stderrTail: tail(stderrFull, FAILURE_OUTPUT_TAIL_CHARS),
184
- stdoutTail: tail(stdoutFull, FAILURE_OUTPUT_TAIL_CHARS)
261
+ stderrTail,
262
+ stdoutTail
185
263
  });
186
- throw new Error(`Shell command timed out after ${timeout}ms`);
264
+ const parts = [`Shell command timed out after ${timeout}ms: ${command.slice(0, 200)}`];
265
+ if (stdoutTail) {
266
+ parts.push(`--- stdout (tail) ---
267
+ ${stdoutTail}`);
268
+ }
269
+ if (stderrTail) {
270
+ parts.push(`--- stderr (tail) ---
271
+ ${stderrTail}`);
272
+ }
273
+ throw new Error(parts.join("\n"));
187
274
  }
188
275
  const output = {
189
276
  // Use our accumulated buffers — result.stdout/stderr are empty because
@@ -206,13 +293,36 @@ async function shellHandler(ctx, input) {
206
293
  stdoutTail: tail(output.stdout, FAILURE_OUTPUT_TAIL_CHARS)
207
294
  });
208
295
  if (throwOnError) {
209
- throw new Error(`Shell command failed with exit code ${output.exitCode}: ${output.stderr.slice(0, 500)}`);
296
+ const failParts = [`Shell command failed with exit code ${output.exitCode}: ${command.slice(0, 200)}`];
297
+ const failStderrTail = tail(output.stderr, FAILURE_OUTPUT_TAIL_CHARS);
298
+ const failStdoutTail = tail(output.stdout, FAILURE_OUTPUT_TAIL_CHARS);
299
+ if (failStdoutTail) {
300
+ failParts.push(`--- stdout (tail) ---
301
+ ${failStdoutTail}`);
302
+ }
303
+ if (failStderrTail) {
304
+ failParts.push(`--- stderr (tail) ---
305
+ ${failStderrTail}`);
306
+ }
307
+ throw new Error(failParts.join("\n"));
210
308
  }
211
309
  }
212
310
  return mergeJsonOutputs(output, (msg) => ctx.platform.logger.warn(`[shell] ${msg}`));
213
311
  } catch (error) {
214
312
  if (error && typeof error === "object" && "timedOut" in error && error.timedOut) {
215
- throw new Error(`Shell command timed out after ${timeout}ms`);
313
+ const execTimeoutError = error;
314
+ const timeoutParts = [`Shell command timed out after ${timeout}ms: ${command.slice(0, 200)}`];
315
+ const execStdoutTail = tail(execTimeoutError.stdout ?? "", FAILURE_OUTPUT_TAIL_CHARS);
316
+ const execStderrTail = tail(execTimeoutError.stderr ?? "", FAILURE_OUTPUT_TAIL_CHARS);
317
+ if (execStdoutTail) {
318
+ timeoutParts.push(`--- stdout (tail) ---
319
+ ${execStdoutTail}`);
320
+ }
321
+ if (execStderrTail) {
322
+ timeoutParts.push(`--- stderr (tail) ---
323
+ ${execStderrTail}`);
324
+ }
325
+ throw new Error(timeoutParts.join("\n"));
216
326
  }
217
327
  if (error && typeof error === "object" && "exitCode" in error) {
218
328
  const execError = error;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/shell.ts","../src/approval.ts","../src/gate.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @module @kb-labs/workflow-runtime/builtin-handlers/shell\n * Built-in shell execution handler for workflows\n *\n * Security features:\n * - Blocks dangerous commands (rm -rf /, fork bombs, etc.)\n * - Timeout enforcement (default 5 minutes)\n * - Environment variable isolation\n * - Working directory restrictions\n */\n\nimport { execaCommand } from 'execa';\nimport type { PluginContextV3 } from '@kb-labs/plugin-contracts';\n\n/**\n * Commands that are always blocked (dangerous)\n */\nconst BLOCKED_COMMANDS = [\n 'rm -rf /',\n 'rm -rf /*',\n 'mkfs',\n 'dd if=',\n ':(){:|:&};:', // Fork bomb\n 'chmod -R 777 /',\n 'chown -R',\n '> /dev/sda',\n 'mv /* ',\n 'fdisk',\n];\n\n/** Cap on how much of a failed command's output gets attached to its log entry. */\nconst FAILURE_OUTPUT_TAIL_CHARS = 4000;\n\n/**\n * Last N characters of `text`, prefixed with a marker when it was truncated.\n * Tail (not head) because the actionable error is almost always at the end —\n * a stack trace, an assertion failure, the final \"Error:\" line.\n */\nexport function tail(text: string, maxChars: number): string {\n if (text.length <= maxChars) {return text;}\n return `…(truncated, showing last ${maxChars} of ${text.length} chars)\\n${text.slice(-maxChars)}`;\n}\n\n/**\n * Split string into chunks of specified size\n */\nfunction chunkString(str: string, chunkSize: number): string[] {\n const chunks: string[] = [];\n for (let i = 0; i < str.length; i += chunkSize) {\n chunks.push(str.slice(i, i + chunkSize));\n }\n return chunks;\n}\n\n/**\n * Shell handler input\n */\nexport interface ShellInput {\n /** Command to execute */\n command: string;\n\n /** Additional environment variables */\n env?: Record<string, string>;\n\n /** Timeout in milliseconds (default: 300000 = 5 min) */\n timeout?: number;\n\n /** Throw on non-zero exit code (default: false) */\n throwOnError?: boolean;\n}\n\n/**\n * Shell handler output\n */\nexport interface ShellOutput {\n /** Standard output */\n stdout: string;\n\n /** Standard error */\n stderr: string;\n\n /** Exit code */\n exitCode: number;\n\n /** Whether command succeeded (exitCode === 0) */\n ok: boolean;\n}\n\n/**\n * Plain-JSON output marker.\n * echo '::kb-output::{\"passed\":true}'\n *\n * Only safe for values without embedded newlines, quotes, or control chars.\n * For multi-line text, binary data, or any value produced by a sub-command,\n * prefer the base64 variant below.\n */\nconst OUTPUT_MARKER = '::kb-output::';\n\n/**\n * Base64-encoded JSON output marker — JSON-safe for any payload.\n *\n * Usage (shell):\n * PAYLOAD=$(jq -cn --arg plan \"$(cat /tmp/plan.md)\" '{plan: $plan}' | base64 -w0)\n * echo \"::kb-output:base64::${PAYLOAD}\"\n *\n * This handles multi-line text, special chars, and any binary-safe content.\n * The base64 string is decoded and JSON-parsed by the runtime.\n */\nconst OUTPUT_MARKER_B64 = '::kb-output:base64::';\n\n/**\n * Parse a single ::kb-output:: or ::kb-output:base64:: marker line into key-value pairs.\n *\n * Returns the parsed object, or null if the line is not a recognized marker.\n * Throws a descriptive error if the marker payload is malformed so callers can warn.\n */\nexport function parseOutputMarkerLine(line: string): Record<string, unknown> | null {\n // Base64-encoded variant — JSON-safe for any payload\n const b64Idx = line.indexOf(OUTPUT_MARKER_B64);\n if (b64Idx !== -1) {\n const raw = line.slice(b64Idx + OUTPUT_MARKER_B64.length).trim();\n let decoded: string;\n try {\n decoded = Buffer.from(raw, 'base64').toString('utf8');\n } catch {\n throw new Error(`::kb-output:base64:: payload is not valid base64`);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(decoded);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new Error(`::kb-output:base64:: malformed JSON payload after decode: ${detail}`);\n }\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>;\n }\n throw new Error(`::kb-output:base64:: JSON must be an object, got ${Array.isArray(parsed) ? 'array' : typeof parsed}`);\n }\n\n // Plain-JSON variant — only safe for simple scalar values\n const idx = line.indexOf(OUTPUT_MARKER);\n if (idx !== -1) {\n const raw = line.slice(idx + OUTPUT_MARKER.length);\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new Error(`::kb-output:: malformed JSON payload: ${detail}`);\n }\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>;\n }\n throw new Error(`::kb-output:: JSON must be an object, got ${Array.isArray(parsed) ? 'array' : typeof parsed}`);\n }\n\n return null;\n}\n\n/**\n * Extract structured outputs from shell stdout.\n *\n * Priority:\n * 1. ::kb-output:base64:: marker lines — JSON-safe, recommended for complex values\n * 2. ::kb-output:: marker lines — plain JSON, ok for simple scalars\n * 3. Entire stdout as JSON — fallback for backward compat (simple commands)\n *\n * Malformed markers emit a warning instead of silently dropping the output.\n * Logs and other stdout content are ignored for output purposes.\n */\nexport function mergeJsonOutputs(output: ShellOutput, warn?: (msg: string) => void): Record<string, unknown> {\n const base: Record<string, unknown> = { ...output };\n const trimmed = output.stdout.trim();\n if (!trimmed) {return base;}\n\n // Priority 1 + 2: Look for ::kb-output:: or ::kb-output:base64:: marker lines\n const lines = output.stdout.split('\\n');\n let foundMarker = false;\n for (const line of lines) {\n if (!line.includes('::kb-output')) {continue;}\n foundMarker = true;\n try {\n const parsed = parseOutputMarkerLine(line);\n if (parsed) {\n Object.assign(base, parsed);\n }\n } catch (err) {\n // Malformed marker — warn instead of silently dropping\n const msg = err instanceof Error ? err.message : String(err);\n warn?.(`Malformed ::kb-output:: marker (outputs not populated): ${msg}`);\n }\n }\n\n if (foundMarker) {return base;}\n\n // Priority 3: Fallback — entire stdout as JSON (backward compat)\n try {\n const parsed: unknown = JSON.parse(trimmed);\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n Object.assign(base, parsed as Record<string, unknown>);\n }\n } catch {\n // Not JSON — return as-is\n }\n\n return base;\n}\n\n/**\n * Built-in shell execution handler.\n *\n * Executes shell commands with safety checks and timeout enforcement.\n *\n * @param ctx - Handler execution context\n * @param input - Shell command input\n * @returns Shell execution result\n * @throws Error if dangerous command detected or timeout exceeded\n */\nasync function shellHandler(\n ctx: PluginContextV3,\n input: ShellInput,\n): Promise<Record<string, unknown>> {\n const { command, env = {}, timeout = 300000, throwOnError = false } = input;\n\n // Security: Check for dangerous commands\n const normalizedCommand = command.toLowerCase().trim();\n for (const blocked of BLOCKED_COMMANDS) {\n if (normalizedCommand.includes(blocked.toLowerCase())) {\n throw new Error(\n `Dangerous command blocked: \"${blocked}\". Command attempted: ${command.slice(0, 100)}`,\n );\n }\n }\n\n // Get working directory from context (workflow workspace)\n const cwd = ctx.cwd;\n\n // Merge environment variables\n const mergedEnv = {\n ...process.env,\n ...env,\n };\n\n ctx.platform.logger.info('Executing shell command', {\n command: command.slice(0, 200),\n cwd,\n timeout,\n });\n\n try {\n // detached: true puts the subprocess in its own process group. On timeout we kill\n // the entire group (process.kill(-pid, 'SIGKILL')), which takes down both the shell\n // and any children it spawned. Without this, SIGTERM reaches only the shell; child\n // processes (e.g. `sleep N`) inherit the pipe write-end and block await indefinitely.\n const proc = execaCommand(command, {\n cwd,\n env: mergedEnv,\n shell: true,\n stdio: 'pipe',\n reject: false, // We handle exit codes ourselves\n detached: true,\n });\n\n // Stream stdout/stderr line-by-line in real-time.\n // We consume the streams ourselves via 'data' events for live log streaming.\n // Because attaching a 'data' listener drains the stream, execa's result.stdout\n // will be empty — we must reconstruct the full output from collected chunks.\n let lineNo = 0;\n let stdoutBuf = '';\n let stderrBuf = '';\n let stdoutFull = '';\n let stderrFull = '';\n\n const emitLine = (stream: 'stdout' | 'stderr', line: string) => {\n lineNo++;\n // stderr is an output stream, not an exit-status signal. Several\n // perfectly successful tools (notably command discovery) deliberately\n // write diagnostics there. The step result is evaluated from exitCode;\n // keep stderr visible without turning every diagnostic line into an\n // error event.\n void ctx.api.events.emit('log.line', { stream, line, lineNo, level: stream === 'stderr' ? 'warn' : 'info' });\n };\n\n proc.stdout?.on('data', (chunk: Buffer) => {\n const text = chunk.toString();\n stdoutFull += text;\n stdoutBuf += text;\n const lines = stdoutBuf.split('\\n');\n stdoutBuf = lines.pop() ?? '';\n for (const line of lines) {emitLine('stdout', line);}\n });\n\n proc.stderr?.on('data', (chunk: Buffer) => {\n const text = chunk.toString();\n stderrFull += text;\n stderrBuf += text;\n const lines = stderrBuf.split('\\n');\n stderrBuf = lines.pop() ?? '';\n for (const line of lines) {emitLine('stderr', line);}\n });\n\n // Self-managed timeout: kill the entire process group so orphaned children\n // release the pipe write-end and await proc resolves immediately.\n let timedOut = false;\n const killTimer = setTimeout(() => {\n timedOut = true;\n const pid = proc.pid;\n if (pid !== undefined) {\n try {\n process.kill(-pid, 'SIGKILL');\n } catch {\n // process already exited\n }\n }\n }, timeout);\n\n const result = await proc;\n clearTimeout(killTimer);\n\n // Flush remaining buffered content\n if (stdoutBuf) {emitLine('stdout', stdoutBuf);}\n if (stderrBuf) {emitLine('stderr', stderrBuf);}\n\n if (timedOut) {\n // Whatever the command printed before the kill is the only clue to\n // whether it was hung or just slow — without it the run log shows a\n // multi-hour gap between \"Executing shell command\" and this error and\n // nothing in between (see kb-labs incident: 20min Build-step timeout\n // debugged blind because of exactly this).\n ctx.platform.logger.warn('Shell command timed out', {\n timeoutMs: timeout,\n stderrTail: tail(stderrFull, FAILURE_OUTPUT_TAIL_CHARS),\n stdoutTail: tail(stdoutFull, FAILURE_OUTPUT_TAIL_CHARS),\n });\n throw new Error(`Shell command timed out after ${timeout}ms`);\n }\n\n const output: ShellOutput = {\n // Use our accumulated buffers — result.stdout/stderr are empty because\n // we consumed the streams via 'data' listeners above.\n stdout: stdoutFull || result.stdout,\n stderr: stderrFull || result.stderr,\n exitCode: result.exitCode ?? 0,\n ok: (result.exitCode ?? 0) === 0,\n };\n\n if (output.ok) {\n ctx.platform.logger.info('Shell command completed successfully', {\n exitCode: output.exitCode,\n stdoutLines: output.stdout.split('\\n').length,\n });\n } else {\n // Attach the actual output, not just its shape — this is the log entry\n // that shows up in `kb workflow runs logs`, and it used to carry only\n // exitCode + a line count. Debugging a failed step meant re-running the\n // command outside the workflow just to see why it failed.\n ctx.platform.logger.warn('Shell command failed', {\n exitCode: output.exitCode,\n stderrLines: output.stderr.split('\\n').length,\n stderrTail: tail(output.stderr, FAILURE_OUTPUT_TAIL_CHARS),\n stdoutTail: tail(output.stdout, FAILURE_OUTPUT_TAIL_CHARS),\n });\n\n if (throwOnError) {\n throw new Error(`Shell command failed with exit code ${output.exitCode}: ${output.stderr.slice(0, 500)}`);\n }\n }\n\n return mergeJsonOutputs(output, (msg) => ctx.platform.logger.warn(`[shell] ${msg}`));\n } catch (error) {\n // Handle timeout\n if (error && typeof error === 'object' && 'timedOut' in error && error.timedOut) {\n throw new Error(`Shell command timed out after ${timeout}ms`);\n }\n\n // Handle execution error\n if (error && typeof error === 'object' && 'exitCode' in error) {\n const execError = error as { exitCode?: number; stdout?: string; stderr?: string };\n const output: ShellOutput = {\n stdout: execError.stdout ?? '',\n stderr: execError.stderr ?? '',\n exitCode: execError.exitCode ?? 1,\n ok: false,\n };\n\n ctx.platform.logger.error('Shell command execution failed', undefined, {\n exitCode: output.exitCode,\n stderr: output.stderr.slice(0, 500),\n });\n\n if (!throwOnError) {\n return { ...output };\n }\n }\n\n throw error;\n }\n}\n\n// Export handler in format expected by ExecutionBackend\nexport default {\n execute: shellHandler,\n};\n","/**\n * @module @kb-labs/workflow-steps/approval\n * Types and handler for builtin:approval step\n *\n * Approval steps pause the pipeline and wait for human decision.\n * The worker handles polling; resolveApproval() on the engine resumes execution.\n */\n\n/**\n * Input for builtin:approval step (spec.with)\n */\nexport interface ApprovalInput {\n /** Display title for the approval request */\n title: string;\n\n /** Contextual data shown to the approver (already interpolated) */\n context?: Record<string, unknown>;\n\n /** Optional instructions for the approver */\n instructions?: string;\n}\n\n/**\n * Output produced by a resolved approval step\n */\nexport interface ApprovalOutput {\n /** Whether the approval was granted */\n approved: boolean;\n\n /** Action taken: \"approve\" or \"reject\" */\n action: 'approve' | 'reject';\n\n /** Optional comment from the approver */\n comment?: string;\n\n /** Additional data provided by the approver */\n [key: string]: unknown;\n}\n\n/**\n * ApprovalHandler — signals the worker to pause and wait for human approval.\n * The worker owns the polling loop; this handler only marks the intent.\n */\nexport class ApprovalHandler {\n static readonly uses = 'builtin:approval';\n\n handle(_input: ApprovalInput): { status: 'waiting'; reason: 'approval' } {\n return { status: 'waiting', reason: 'approval' };\n }\n}\n","/**\n * @module @kb-labs/workflow-steps/gate\n * Types and handler for builtin:gate step\n *\n * Gate steps act as automatic routers — they read a decision value\n * from previous step outputs and route the pipeline accordingly:\n * - continue: proceed to next step\n * - fail: fail the pipeline\n * - restart: reset steps back to target and re-schedule with context\n * - skip: mark intermediate steps as skipped and jump forward to a target step\n */\n\nimport { resolveValue, type ExpressionContext } from '@kb-labs/workflow-contracts';\n\n/**\n * Route action for a gate decision\n */\nexport type GateRouteAction =\n | 'continue'\n | 'fail'\n | {\n /** Step ID to restart from (go backward) */\n restartFrom: string;\n /** Additional context to pass (merged into trigger.payload) */\n context?: Record<string, unknown>;\n }\n | {\n /** Step ID to skip forward to — all steps between gate and target are marked skipped */\n skipTo: string;\n };\n\n/**\n * Input for builtin:gate step (spec.with)\n */\nexport interface GateInput {\n /** Expression path to the decision value (e.g. \"steps.review.outputs.passed\") */\n decision: string;\n\n /** Route map: decision value → action */\n routes: Record<string, GateRouteAction>;\n\n /** Default action if decision value doesn't match any route */\n default?: 'continue' | 'fail';\n\n /** Maximum number of restart iterations before failing (default: 3) */\n maxIterations?: number;\n}\n\n/**\n * Output produced by a resolved gate step\n */\nexport interface GateOutput {\n /** The decision value that was evaluated */\n decisionValue: unknown;\n\n /** The action that was taken */\n action: 'continue' | 'fail' | 'restart' | 'skip';\n\n /** Step ID that was restarted from (if restart) */\n restartFrom?: string;\n\n /** Step ID that was skipped to (if skip) */\n skipTo?: string;\n\n /** Current iteration count */\n iteration: number;\n\n /** Set to true when the gate exhausted its maxIterations budget */\n maxIterationsReached?: boolean;\n\n [key: string]: unknown;\n}\n\n/**\n * Pure decision result from GateHandler.\n * Worker applies the state mutations based on the action.\n */\nexport type GateDecision =\n | { action: 'continue'; outputs: GateOutput }\n | { action: 'fail'; error: Error; outputs: GateOutput }\n | {\n action: 'restart';\n restartFrom: string;\n context?: Record<string, unknown>;\n outputs: GateOutput;\n nextIteration: number;\n }\n | {\n action: 'skip';\n skipTo: string;\n outputs: GateOutput;\n };\n\n/**\n * GateHandler — pure decision function.\n * No I/O, no state mutations. Worker owns the side effects.\n */\nexport class GateHandler {\n static readonly uses = 'builtin:gate';\n\n /**\n * @param validRestartTargets Identifiers the worker can reset on restart —\n * step ids/spec.ids within the gate's own job AND other job names (for\n * cross-job restart). When provided and `restartFrom` matches none of them,\n * the gate FAILS instead of attempting a restart the worker cannot apply —\n * which would otherwise silently degrade into \"complete the gate and\n * proceed\" (a false green).\n */\n handle(\n input: GateInput,\n exprCtx: ExpressionContext,\n currentIteration: number,\n validRestartTargets?: string[],\n ): GateDecision {\n const maxIterations = input.maxIterations ?? 3;\n const decisionValue = resolveValue(input.decision, exprCtx);\n const decisionKey = String(decisionValue);\n\n const route: GateRouteAction | undefined =\n input.routes[decisionKey] ?? input.routes[decisionValue as string];\n const action = route ?? input.default ?? 'fail';\n\n if (action === 'continue') {\n return {\n action: 'continue',\n outputs: { decisionValue, action: 'continue', iteration: currentIteration },\n };\n }\n\n if (action === 'fail') {\n const failReason =\n (route as { message?: string } | undefined)?.message ??\n `No matching route for value \"${decisionKey}\"`;\n return {\n action: 'fail',\n error: new Error(`Gate failed: ${failReason}`),\n outputs: { decisionValue, action: 'fail', iteration: currentIteration },\n };\n }\n\n // skipTo action — jump forward, marking intermediate steps as skipped\n if (typeof action === 'object' && 'skipTo' in action) {\n if (validRestartTargets && !validRestartTargets.includes(action.skipTo)) {\n return {\n action: 'fail',\n error: new Error(\n `Gate skipTo \"${action.skipTo}\" matches no step in this job — cannot skip.`,\n ),\n outputs: { decisionValue, action: 'fail', iteration: currentIteration },\n };\n }\n return {\n action: 'skip',\n skipTo: action.skipTo,\n outputs: {\n decisionValue,\n action: 'skip',\n skipTo: action.skipTo,\n iteration: currentIteration,\n },\n };\n }\n\n // restart action\n const restartAction = action as { restartFrom: string; context?: Record<string, unknown> };\n\n // Guard: restartFrom must name something the worker can reset — a step in\n // this job or another job by name. If it matches neither, the gate cannot\n // recover; fail honestly rather than let the worker silently complete the\n // gate and proceed (which would ship unverified work as a false green).\n if (validRestartTargets && !validRestartTargets.includes(restartAction.restartFrom)) {\n return {\n action: 'fail',\n error: new Error(\n `Gate restartFrom \"${restartAction.restartFrom}\" matches no step in this job ` +\n `or any job in the run — cannot restart. Failing instead of silently passing.`,\n ),\n outputs: { decisionValue, action: 'fail', iteration: currentIteration },\n };\n }\n\n const nextIteration = currentIteration + 1;\n\n if (nextIteration >= maxIterations) {\n return {\n action: 'fail',\n error: new Error(`Gate max iterations reached (${maxIterations}) for step`),\n outputs: {\n decisionValue,\n action: 'fail',\n iteration: currentIteration,\n maxIterationsReached: true,\n },\n };\n }\n\n return {\n action: 'restart',\n restartFrom: restartAction.restartFrom,\n context: restartAction.context,\n outputs: {\n decisionValue,\n action: 'restart',\n restartFrom: restartAction.restartFrom,\n iteration: nextIteration,\n },\n nextIteration,\n };\n }\n}\n"]}
1
+ {"version":3,"sources":["../../home/runner/work/kb-labs/kb-labs/plugins/workflow/steps/src/shell.ts","../../home/runner/work/kb-labs/kb-labs/plugins/workflow/steps/src/approval.ts","../../home/runner/work/kb-labs/kb-labs/plugins/workflow/steps/src/gate.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @module @kb-labs/workflow-runtime/builtin-handlers/shell\n * Built-in shell execution handler for workflows\n *\n * Security features:\n * - Blocks dangerous commands (rm -rf /, fork bombs, etc.)\n * - Timeout enforcement (default 5 minutes)\n * - Environment variable isolation\n * - Working directory restrictions\n */\n\nimport { execaCommand } from 'execa';\nimport type { PluginContextV3 } from '@kb-labs/plugin-contracts';\n\n/**\n * Commands that are always blocked (dangerous)\n */\nconst BLOCKED_COMMANDS = [\n 'rm -rf /',\n 'rm -rf /*',\n 'mkfs',\n 'dd if=',\n ':(){:|:&};:', // Fork bomb\n 'chmod -R 777 /',\n 'chown -R',\n '> /dev/sda',\n 'mv /* ',\n 'fdisk',\n];\n\n/** Cap on how much of a failed command's output gets attached to its log entry. */\nconst FAILURE_OUTPUT_TAIL_CHARS = 4000;\n\n/** Cap on how much of a surfaced ::kb-output:: marker line gets attached — see `tail()`. */\nconst MARKER_LINE_MAX_CHARS = 2000;\n\n/**\n * The last `::kb-output::`/`::kb-output:base64::` line in `text`, or null.\n * A `kb` command reports its real structured result on one of these lines\n * (e.g. `{\"ok\":false,\"failed\":[\"dist-exports\"],...}`) — often followed by a\n * wrapper's own generic exit banner (pnpm's \"[ELIFECYCLE] Command failed\n * with exit code 1.\"), which then becomes the literal last bytes of output.\n */\nfunction lastOutputMarkerLine(text: string): string | null {\n const lines = text.split('\\n');\n for (let i = lines.length - 1; i >= 0; i--) {\n const line = lines[i];\n if (line !== undefined && (line.includes(OUTPUT_MARKER_B64) || line.includes(OUTPUT_MARKER))) {\n return line;\n }\n }\n return null;\n}\n\n/**\n * Batching for durable line persistence: the ring buffer's `maxSize` is a\n * fixed number of records shared across the entire daemon (not per run/step),\n * so one `ctx.platform.logger` call per output line would let a single\n * verbose step (e.g. a package install with thousands of lines) evict every\n * other run's recent logs from the hot tier. We accumulate lines and flush\n * as one logger call per window, keyed by whichever limit is hit first.\n */\nconst LOG_BATCH_MAX_LINES = 50;\nconst LOG_BATCH_MAX_MS = 500;\n\n/**\n * Last N characters of `text`, prefixed with a marker when it was truncated.\n * Tail (not head) because the actionable error is almost always at the end —\n * a stack trace, an assertion failure, the final \"Error:\" line.\n *\n * Exception: a raw byte-count tail alone silently drops the one thing that\n * actually explains a failed `kb` command. A wrapper's generic exit banner\n * (pnpm's \"[ELIFECYCLE] Command failed...\") becomes the true last bytes of\n * output, while the command's own `::kb-output::` result — the whole reason\n * `runReleaseChecks` and friends exist — sits earlier in the stream and gets\n * discarded (observed live: a failed `release checks` step logged only\n * \"[ELIFECYCLE] Command failed with exit code 1.\" with the real\n * `{\"ok\":false,\"failed\":[...]}` payload truncated away). When a marker line\n * is present, surface it ahead of the raw tail instead of dropping it.\n */\nexport function tail(text: string, maxChars: number): string {\n if (text.length <= maxChars) {return text;}\n const rawTail = `…(truncated, showing last ${maxChars} of ${text.length} chars)\\n${text.slice(-maxChars)}`;\n const markerLine = lastOutputMarkerLine(text);\n if (!markerLine || rawTail.includes(markerLine)) {return rawTail;}\n const markerSnippet = markerLine.length > MARKER_LINE_MAX_CHARS\n ? `${markerLine.slice(0, MARKER_LINE_MAX_CHARS)}…(marker line truncated, ${markerLine.length} chars total)`\n : markerLine;\n return `${markerSnippet}\\n${rawTail}`;\n}\n\n/**\n * Accumulates output lines for one stream (stdout or stderr) and flushes them\n * as a single durable log write once `maxLines` lines have queued up or\n * `maxMs` has elapsed since the first unflushed line — whichever comes first.\n *\n * This is what makes per-line shell output durable without turning every\n * line into its own `ctx.platform.logger` call: the daemon's ring buffer is a\n * fixed number of records shared across the whole daemon, so one write per\n * line would let a single verbose step evict every other run's recent logs.\n */\nexport class LineBatcher {\n private buffer: Array<{ line: string; lineNo: number }> = [];\n private timer: ReturnType<typeof setTimeout> | undefined;\n\n public constructor(\n private readonly stream: 'stdout' | 'stderr',\n private readonly onFlush: (batch: { stream: 'stdout' | 'stderr'; fromLine: number; toLine: number; text: string }) => void,\n private readonly maxLines = LOG_BATCH_MAX_LINES,\n private readonly maxMs = LOG_BATCH_MAX_MS,\n ) {}\n\n public add(line: string, lineNo: number): void {\n this.buffer.push({ line, lineNo });\n if (this.buffer.length >= this.maxLines) {\n this.flush();\n return;\n }\n if (!this.timer) {\n this.timer = setTimeout(() => this.flush(), this.maxMs);\n }\n }\n\n /** Flush whatever is buffered right now, regardless of size/time thresholds. */\n public flush(): void {\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = undefined;\n }\n if (this.buffer.length === 0) {return;}\n const batch = this.buffer;\n this.buffer = [];\n this.onFlush({\n stream: this.stream,\n fromLine: batch[0]!.lineNo,\n toLine: batch[batch.length - 1]!.lineNo,\n text: batch.map((entry) => entry.line).join('\\n'),\n });\n }\n}\n\n/**\n * Split string into chunks of specified size\n */\nfunction chunkString(str: string, chunkSize: number): string[] {\n const chunks: string[] = [];\n for (let i = 0; i < str.length; i += chunkSize) {\n chunks.push(str.slice(i, i + chunkSize));\n }\n return chunks;\n}\n\n/**\n * Shell handler input\n */\nexport interface ShellInput {\n /** Command to execute */\n command: string;\n\n /** Additional environment variables */\n env?: Record<string, string>;\n\n /** Timeout in milliseconds (default: 300000 = 5 min) */\n timeout?: number;\n\n /** Throw on non-zero exit code (default: false) */\n throwOnError?: boolean;\n}\n\n/**\n * Shell handler output\n */\nexport interface ShellOutput {\n /** Standard output */\n stdout: string;\n\n /** Standard error */\n stderr: string;\n\n /** Exit code */\n exitCode: number;\n\n /** Whether command succeeded (exitCode === 0) */\n ok: boolean;\n}\n\n/**\n * Plain-JSON output marker.\n * echo '::kb-output::{\"passed\":true}'\n *\n * Only safe for values without embedded newlines, quotes, or control chars.\n * For multi-line text, binary data, or any value produced by a sub-command,\n * prefer the base64 variant below.\n */\nconst OUTPUT_MARKER = '::kb-output::';\n\n/**\n * Base64-encoded JSON output marker — JSON-safe for any payload.\n *\n * Usage (shell):\n * PAYLOAD=$(jq -cn --arg plan \"$(cat /tmp/plan.md)\" '{plan: $plan}' | base64 -w0)\n * echo \"::kb-output:base64::${PAYLOAD}\"\n *\n * This handles multi-line text, special chars, and any binary-safe content.\n * The base64 string is decoded and JSON-parsed by the runtime.\n */\nconst OUTPUT_MARKER_B64 = '::kb-output:base64::';\n\n/**\n * Parse a single ::kb-output:: or ::kb-output:base64:: marker line into key-value pairs.\n *\n * Returns the parsed object, or null if the line is not a recognized marker.\n * Throws a descriptive error if the marker payload is malformed so callers can warn.\n */\nexport function parseOutputMarkerLine(line: string): Record<string, unknown> | null {\n // Base64-encoded variant — JSON-safe for any payload\n const b64Idx = line.indexOf(OUTPUT_MARKER_B64);\n if (b64Idx !== -1) {\n const raw = line.slice(b64Idx + OUTPUT_MARKER_B64.length).trim();\n let decoded: string;\n try {\n decoded = Buffer.from(raw, 'base64').toString('utf8');\n } catch {\n throw new Error(`::kb-output:base64:: payload is not valid base64`);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(decoded);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new Error(`::kb-output:base64:: malformed JSON payload after decode: ${detail}`);\n }\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>;\n }\n throw new Error(`::kb-output:base64:: JSON must be an object, got ${Array.isArray(parsed) ? 'array' : typeof parsed}`);\n }\n\n // Plain-JSON variant — only safe for simple scalar values\n const idx = line.indexOf(OUTPUT_MARKER);\n if (idx !== -1) {\n const raw = line.slice(idx + OUTPUT_MARKER.length);\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new Error(`::kb-output:: malformed JSON payload: ${detail}`);\n }\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>;\n }\n throw new Error(`::kb-output:: JSON must be an object, got ${Array.isArray(parsed) ? 'array' : typeof parsed}`);\n }\n\n return null;\n}\n\n/**\n * Extract structured outputs from shell stdout.\n *\n * Priority:\n * 1. ::kb-output:base64:: marker lines — JSON-safe, recommended for complex values\n * 2. ::kb-output:: marker lines — plain JSON, ok for simple scalars\n * 3. Entire stdout as JSON — fallback for backward compat (simple commands)\n *\n * Malformed markers emit a warning instead of silently dropping the output.\n * Logs and other stdout content are ignored for output purposes.\n */\nexport function mergeJsonOutputs(output: ShellOutput, warn?: (msg: string) => void): Record<string, unknown> {\n const base: Record<string, unknown> = { ...output };\n const trimmed = output.stdout.trim();\n if (!trimmed) {return base;}\n\n // Priority 1 + 2: Look for ::kb-output:: or ::kb-output:base64:: marker lines\n const lines = output.stdout.split('\\n');\n let foundMarker = false;\n for (const line of lines) {\n if (!line.includes('::kb-output')) {continue;}\n foundMarker = true;\n try {\n const parsed = parseOutputMarkerLine(line);\n if (parsed) {\n Object.assign(base, parsed);\n }\n } catch (err) {\n // Malformed marker — warn instead of silently dropping\n const msg = err instanceof Error ? err.message : String(err);\n warn?.(`Malformed ::kb-output:: marker (outputs not populated): ${msg}`);\n }\n }\n\n if (foundMarker) {return base;}\n\n // Priority 3: Fallback — entire stdout as JSON (backward compat)\n try {\n const parsed: unknown = JSON.parse(trimmed);\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n Object.assign(base, parsed as Record<string, unknown>);\n }\n } catch {\n // Not JSON — return as-is\n }\n\n return base;\n}\n\n/**\n * Built-in shell execution handler.\n *\n * Executes shell commands with safety checks and timeout enforcement.\n *\n * @param ctx - Handler execution context\n * @param input - Shell command input\n * @returns Shell execution result\n * @throws Error if dangerous command detected or timeout exceeded\n */\nasync function shellHandler(\n ctx: PluginContextV3,\n input: ShellInput,\n): Promise<Record<string, unknown>> {\n const { command, env = {}, timeout = 300000, throwOnError = false } = input;\n\n // Security: Check for dangerous commands\n const normalizedCommand = command.toLowerCase().trim();\n for (const blocked of BLOCKED_COMMANDS) {\n if (normalizedCommand.includes(blocked.toLowerCase())) {\n throw new Error(\n `Dangerous command blocked: \"${blocked}\". Command attempted: ${command.slice(0, 100)}`,\n );\n }\n }\n\n // Get working directory from context (workflow workspace)\n const cwd = ctx.cwd;\n\n // Merge environment variables\n const mergedEnv = {\n ...process.env,\n ...env,\n };\n\n ctx.platform.logger.info('Executing shell command', {\n command: command.slice(0, 200),\n cwd,\n timeout,\n });\n\n try {\n // detached: true puts the subprocess in its own process group. On timeout we kill\n // the entire group (process.kill(-pid, 'SIGKILL')), which takes down both the shell\n // and any children it spawned. Without this, SIGTERM reaches only the shell; child\n // processes (e.g. `sleep N`) inherit the pipe write-end and block await indefinitely.\n const proc = execaCommand(command, {\n cwd,\n env: mergedEnv,\n shell: true,\n stdio: 'pipe',\n reject: false, // We handle exit codes ourselves\n detached: true,\n });\n\n // Stream stdout/stderr line-by-line in real-time.\n // We consume the streams ourselves via 'data' events for live log streaming.\n // Because attaching a 'data' listener drains the stream, execa's result.stdout\n // will be empty — we must reconstruct the full output from collected chunks.\n let lineNo = 0;\n let stdoutBuf = '';\n let stderrBuf = '';\n let stdoutFull = '';\n let stderrFull = '';\n\n // Durable persistence for full per-line output, batched so a verbose step\n // (e.g. a package install emitting thousands of lines) doesn't blow away\n // the daemon's shared ring buffer with one record per line. This is\n // additive to the live 'log.line' event below, not a replacement for it.\n const onBatchFlush = (batch: { stream: 'stdout' | 'stderr'; fromLine: number; toLine: number; text: string }) => {\n const log = batch.stream === 'stderr' ? ctx.platform.logger.warn : ctx.platform.logger.info;\n log.call(ctx.platform.logger, `Shell output (${batch.stream})`, {\n stream: batch.stream,\n fromLine: batch.fromLine,\n toLine: batch.toLine,\n text: batch.text,\n });\n };\n const stdoutBatcher = new LineBatcher('stdout', onBatchFlush);\n const stderrBatcher = new LineBatcher('stderr', onBatchFlush);\n\n const emitLine = (stream: 'stdout' | 'stderr', line: string) => {\n lineNo++;\n // stderr is an output stream, not an exit-status signal. Several\n // perfectly successful tools (notably command discovery) deliberately\n // write diagnostics there. The step result is evaluated from exitCode;\n // keep stderr visible without turning every diagnostic line into an\n // error event.\n void ctx.api.events.emit('log.line', { stream, line, lineNo, level: stream === 'stderr' ? 'warn' : 'info' });\n (stream === 'stderr' ? stderrBatcher : stdoutBatcher).add(line, lineNo);\n };\n\n proc.stdout?.on('data', (chunk: Buffer) => {\n const text = chunk.toString();\n stdoutFull += text;\n stdoutBuf += text;\n const lines = stdoutBuf.split('\\n');\n stdoutBuf = lines.pop() ?? '';\n for (const line of lines) {emitLine('stdout', line);}\n });\n\n proc.stderr?.on('data', (chunk: Buffer) => {\n const text = chunk.toString();\n stderrFull += text;\n stderrBuf += text;\n const lines = stderrBuf.split('\\n');\n stderrBuf = lines.pop() ?? '';\n for (const line of lines) {emitLine('stderr', line);}\n });\n\n // Self-managed timeout: kill the entire process group so orphaned children\n // release the pipe write-end and await proc resolves immediately.\n let timedOut = false;\n const killTimer = setTimeout(() => {\n timedOut = true;\n const pid = proc.pid;\n if (pid !== undefined) {\n try {\n process.kill(-pid, 'SIGKILL');\n } catch {\n // process already exited\n }\n }\n }, timeout);\n\n const result = await proc;\n clearTimeout(killTimer);\n\n // Flush remaining buffered content\n if (stdoutBuf) {emitLine('stdout', stdoutBuf);}\n if (stderrBuf) {emitLine('stderr', stderrBuf);}\n\n // Flush any lines still waiting on the batch window/size threshold —\n // without this, the tail of a step's output (fewer than maxLines lines,\n // flushed less than maxMs ago) would never reach durable storage.\n stdoutBatcher.flush();\n stderrBatcher.flush();\n\n if (timedOut) {\n // Whatever the command printed before the kill is the only clue to\n // whether it was hung or just slow — without it the run log shows a\n // multi-hour gap between \"Executing shell command\" and this error and\n // nothing in between (see kb-labs incident: 20min Build-step timeout\n // debugged blind because of exactly this).\n const stderrTail = tail(stderrFull, FAILURE_OUTPUT_TAIL_CHARS);\n const stdoutTail = tail(stdoutFull, FAILURE_OUTPUT_TAIL_CHARS);\n ctx.platform.logger.warn('Shell command timed out', {\n timeoutMs: timeout,\n stderrTail,\n stdoutTail,\n });\n // The logger call above writes to the daemon's own log sink, but the\n // *thrown* error is what actually reaches the caller: it becomes\n // result.error in ExecutionResult, then result.error.message in\n // worker.ts's markStepFailed(), which is the only piece of this that\n // survives into the run's persisted step.error (engine.ts only keeps\n // message/stack, not arbitrary error.details). So the tails have to be\n // in the message itself, not just logged, or a step failure still\n // reads as an empty echo of the command with no signal about how far\n // it got. Command line is included too since that's often literally\n // the only thing otherwise in the message on a hung step.\n const parts = [`Shell command timed out after ${timeout}ms: ${command.slice(0, 200)}`];\n if (stdoutTail) { parts.push(`--- stdout (tail) ---\\n${stdoutTail}`); }\n if (stderrTail) { parts.push(`--- stderr (tail) ---\\n${stderrTail}`); }\n throw new Error(parts.join('\\n'));\n }\n\n const output: ShellOutput = {\n // Use our accumulated buffers — result.stdout/stderr are empty because\n // we consumed the streams via 'data' listeners above.\n stdout: stdoutFull || result.stdout,\n stderr: stderrFull || result.stderr,\n exitCode: result.exitCode ?? 0,\n ok: (result.exitCode ?? 0) === 0,\n };\n\n if (output.ok) {\n ctx.platform.logger.info('Shell command completed successfully', {\n exitCode: output.exitCode,\n stdoutLines: output.stdout.split('\\n').length,\n });\n } else {\n // Attach the actual output, not just its shape — this is the log entry\n // that shows up in `kb workflow runs logs`, and it used to carry only\n // exitCode + a line count. Debugging a failed step meant re-running the\n // command outside the workflow just to see why it failed.\n ctx.platform.logger.warn('Shell command failed', {\n exitCode: output.exitCode,\n stderrLines: output.stderr.split('\\n').length,\n stderrTail: tail(output.stderr, FAILURE_OUTPUT_TAIL_CHARS),\n stdoutTail: tail(output.stdout, FAILURE_OUTPUT_TAIL_CHARS),\n });\n\n if (throwOnError) {\n const failParts = [`Shell command failed with exit code ${output.exitCode}: ${command.slice(0, 200)}`];\n const failStderrTail = tail(output.stderr, FAILURE_OUTPUT_TAIL_CHARS);\n const failStdoutTail = tail(output.stdout, FAILURE_OUTPUT_TAIL_CHARS);\n if (failStdoutTail) { failParts.push(`--- stdout (tail) ---\\n${failStdoutTail}`); }\n if (failStderrTail) { failParts.push(`--- stderr (tail) ---\\n${failStderrTail}`); }\n throw new Error(failParts.join('\\n'));\n }\n }\n\n return mergeJsonOutputs(output, (msg) => ctx.platform.logger.warn(`[shell] ${msg}`));\n } catch (error) {\n // Handle timeout (defensive fallback: the primary path is the manually\n // managed killTimer above, which throws before this catch is reached —\n // this covers execa surfacing its own TimeoutError, e.g. if `reject`\n // behavior changes upstream). execa's TimeoutError/ExecaError carries\n // whatever stdout/stderr it captured on the error object itself.\n if (error && typeof error === 'object' && 'timedOut' in error && error.timedOut) {\n const execTimeoutError = error as { stdout?: string; stderr?: string };\n const timeoutParts = [`Shell command timed out after ${timeout}ms: ${command.slice(0, 200)}`];\n const execStdoutTail = tail(execTimeoutError.stdout ?? '', FAILURE_OUTPUT_TAIL_CHARS);\n const execStderrTail = tail(execTimeoutError.stderr ?? '', FAILURE_OUTPUT_TAIL_CHARS);\n if (execStdoutTail) { timeoutParts.push(`--- stdout (tail) ---\\n${execStdoutTail}`); }\n if (execStderrTail) { timeoutParts.push(`--- stderr (tail) ---\\n${execStderrTail}`); }\n throw new Error(timeoutParts.join('\\n'));\n }\n\n // Handle execution error\n if (error && typeof error === 'object' && 'exitCode' in error) {\n const execError = error as { exitCode?: number; stdout?: string; stderr?: string };\n const output: ShellOutput = {\n stdout: execError.stdout ?? '',\n stderr: execError.stderr ?? '',\n exitCode: execError.exitCode ?? 1,\n ok: false,\n };\n\n ctx.platform.logger.error('Shell command execution failed', undefined, {\n exitCode: output.exitCode,\n stderr: output.stderr.slice(0, 500),\n });\n\n if (!throwOnError) {\n return { ...output };\n }\n }\n\n throw error;\n }\n}\n\n// Export handler in format expected by ExecutionBackend\nexport default {\n execute: shellHandler,\n};\n","/**\n * @module @kb-labs/workflow-steps/approval\n * Types and handler for builtin:approval step\n *\n * Approval steps pause the pipeline and wait for human decision.\n * The worker handles polling; resolveApproval() on the engine resumes execution.\n */\n\n/**\n * Input for builtin:approval step (spec.with)\n */\nexport interface ApprovalInput {\n /** Display title for the approval request */\n title: string;\n\n /** Contextual data shown to the approver (already interpolated) */\n context?: Record<string, unknown>;\n\n /** Optional instructions for the approver */\n instructions?: string;\n}\n\n/**\n * Output produced by a resolved approval step\n */\nexport interface ApprovalOutput {\n /** Whether the approval was granted */\n approved: boolean;\n\n /** Action taken: \"approve\" or \"reject\" */\n action: 'approve' | 'reject';\n\n /** Optional comment from the approver */\n comment?: string;\n\n /** Additional data provided by the approver */\n [key: string]: unknown;\n}\n\n/**\n * ApprovalHandler — signals the worker to pause and wait for human approval.\n * The worker owns the polling loop; this handler only marks the intent.\n */\nexport class ApprovalHandler {\n static readonly uses = 'builtin:approval';\n\n handle(_input: ApprovalInput): { status: 'waiting'; reason: 'approval' } {\n return { status: 'waiting', reason: 'approval' };\n }\n}\n","/**\n * @module @kb-labs/workflow-steps/gate\n * Types and handler for builtin:gate step\n *\n * Gate steps act as automatic routers — they read a decision value\n * from previous step outputs and route the pipeline accordingly:\n * - continue: proceed to next step\n * - fail: fail the pipeline\n * - restart: reset steps back to target and re-schedule with context\n * - skip: mark intermediate steps as skipped and jump forward to a target step\n */\n\nimport { resolveValue, type ExpressionContext } from '@kb-labs/workflow-contracts';\n\n/**\n * Route action for a gate decision\n */\nexport type GateRouteAction =\n | 'continue'\n | 'fail'\n | {\n /** Step ID to restart from (go backward) */\n restartFrom: string;\n /** Additional context to pass (merged into trigger.payload) */\n context?: Record<string, unknown>;\n }\n | {\n /** Step ID to skip forward to — all steps between gate and target are marked skipped */\n skipTo: string;\n };\n\n/**\n * Input for builtin:gate step (spec.with)\n */\nexport interface GateInput {\n /** Expression path to the decision value (e.g. \"steps.review.outputs.passed\") */\n decision: string;\n\n /** Route map: decision value → action */\n routes: Record<string, GateRouteAction>;\n\n /** Default action if decision value doesn't match any route */\n default?: 'continue' | 'fail';\n\n /** Maximum number of restart iterations before failing (default: 3) */\n maxIterations?: number;\n}\n\n/**\n * Output produced by a resolved gate step\n */\nexport interface GateOutput {\n /** The decision value that was evaluated */\n decisionValue: unknown;\n\n /** The action that was taken */\n action: 'continue' | 'fail' | 'restart' | 'skip';\n\n /** Step ID that was restarted from (if restart) */\n restartFrom?: string;\n\n /** Step ID that was skipped to (if skip) */\n skipTo?: string;\n\n /** Current iteration count */\n iteration: number;\n\n /** Set to true when the gate exhausted its maxIterations budget */\n maxIterationsReached?: boolean;\n\n [key: string]: unknown;\n}\n\n/**\n * Pure decision result from GateHandler.\n * Worker applies the state mutations based on the action.\n */\nexport type GateDecision =\n | { action: 'continue'; outputs: GateOutput }\n | { action: 'fail'; error: Error; outputs: GateOutput }\n | {\n action: 'restart';\n restartFrom: string;\n context?: Record<string, unknown>;\n outputs: GateOutput;\n nextIteration: number;\n }\n | {\n action: 'skip';\n skipTo: string;\n outputs: GateOutput;\n };\n\n/**\n * GateHandler — pure decision function.\n * No I/O, no state mutations. Worker owns the side effects.\n */\nexport class GateHandler {\n static readonly uses = 'builtin:gate';\n\n /**\n * @param validRestartTargets Identifiers the worker can reset on restart —\n * step ids/spec.ids within the gate's own job AND other job names (for\n * cross-job restart). When provided and `restartFrom` matches none of them,\n * the gate FAILS instead of attempting a restart the worker cannot apply —\n * which would otherwise silently degrade into \"complete the gate and\n * proceed\" (a false green).\n */\n handle(\n input: GateInput,\n exprCtx: ExpressionContext,\n currentIteration: number,\n validRestartTargets?: string[],\n ): GateDecision {\n const maxIterations = input.maxIterations ?? 3;\n const decisionValue = resolveValue(input.decision, exprCtx);\n const decisionKey = String(decisionValue);\n\n const route: GateRouteAction | undefined =\n input.routes[decisionKey] ?? input.routes[decisionValue as string];\n const action = route ?? input.default ?? 'fail';\n\n if (action === 'continue') {\n return {\n action: 'continue',\n outputs: { decisionValue, action: 'continue', iteration: currentIteration },\n };\n }\n\n if (action === 'fail') {\n const failReason =\n (route as { message?: string } | undefined)?.message ??\n `No matching route for value \"${decisionKey}\"`;\n return {\n action: 'fail',\n error: new Error(`Gate failed: ${failReason}`),\n outputs: { decisionValue, action: 'fail', iteration: currentIteration },\n };\n }\n\n // skipTo action — jump forward, marking intermediate steps as skipped\n if (typeof action === 'object' && 'skipTo' in action) {\n if (validRestartTargets && !validRestartTargets.includes(action.skipTo)) {\n return {\n action: 'fail',\n error: new Error(\n `Gate skipTo \"${action.skipTo}\" matches no step in this job — cannot skip.`,\n ),\n outputs: { decisionValue, action: 'fail', iteration: currentIteration },\n };\n }\n return {\n action: 'skip',\n skipTo: action.skipTo,\n outputs: {\n decisionValue,\n action: 'skip',\n skipTo: action.skipTo,\n iteration: currentIteration,\n },\n };\n }\n\n // restart action\n const restartAction = action as { restartFrom: string; context?: Record<string, unknown> };\n\n // Guard: restartFrom must name something the worker can reset — a step in\n // this job or another job by name. If it matches neither, the gate cannot\n // recover; fail honestly rather than let the worker silently complete the\n // gate and proceed (which would ship unverified work as a false green).\n if (validRestartTargets && !validRestartTargets.includes(restartAction.restartFrom)) {\n return {\n action: 'fail',\n error: new Error(\n `Gate restartFrom \"${restartAction.restartFrom}\" matches no step in this job ` +\n `or any job in the run — cannot restart. Failing instead of silently passing.`,\n ),\n outputs: { decisionValue, action: 'fail', iteration: currentIteration },\n };\n }\n\n const nextIteration = currentIteration + 1;\n\n if (nextIteration >= maxIterations) {\n return {\n action: 'fail',\n error: new Error(`Gate max iterations reached (${maxIterations}) for step`),\n outputs: {\n decisionValue,\n action: 'fail',\n iteration: currentIteration,\n maxIterationsReached: true,\n },\n };\n }\n\n return {\n action: 'restart',\n restartFrom: restartAction.restartFrom,\n context: restartAction.context,\n outputs: {\n decisionValue,\n action: 'restart',\n restartFrom: restartAction.restartFrom,\n iteration: nextIteration,\n },\n nextIteration,\n };\n }\n}\n"]}
package/dist/shell.d.ts CHANGED
@@ -15,8 +15,45 @@ import { PluginContextV3 } from '@kb-labs/plugin-contracts';
15
15
  * Last N characters of `text`, prefixed with a marker when it was truncated.
16
16
  * Tail (not head) because the actionable error is almost always at the end —
17
17
  * a stack trace, an assertion failure, the final "Error:" line.
18
+ *
19
+ * Exception: a raw byte-count tail alone silently drops the one thing that
20
+ * actually explains a failed `kb` command. A wrapper's generic exit banner
21
+ * (pnpm's "[ELIFECYCLE] Command failed...") becomes the true last bytes of
22
+ * output, while the command's own `::kb-output::` result — the whole reason
23
+ * `runReleaseChecks` and friends exist — sits earlier in the stream and gets
24
+ * discarded (observed live: a failed `release checks` step logged only
25
+ * "[ELIFECYCLE] Command failed with exit code 1." with the real
26
+ * `{"ok":false,"failed":[...]}` payload truncated away). When a marker line
27
+ * is present, surface it ahead of the raw tail instead of dropping it.
18
28
  */
19
29
  declare function tail(text: string, maxChars: number): string;
30
+ /**
31
+ * Accumulates output lines for one stream (stdout or stderr) and flushes them
32
+ * as a single durable log write once `maxLines` lines have queued up or
33
+ * `maxMs` has elapsed since the first unflushed line — whichever comes first.
34
+ *
35
+ * This is what makes per-line shell output durable without turning every
36
+ * line into its own `ctx.platform.logger` call: the daemon's ring buffer is a
37
+ * fixed number of records shared across the whole daemon, so one write per
38
+ * line would let a single verbose step evict every other run's recent logs.
39
+ */
40
+ declare class LineBatcher {
41
+ private readonly stream;
42
+ private readonly onFlush;
43
+ private readonly maxLines;
44
+ private readonly maxMs;
45
+ private buffer;
46
+ private timer;
47
+ constructor(stream: 'stdout' | 'stderr', onFlush: (batch: {
48
+ stream: 'stdout' | 'stderr';
49
+ fromLine: number;
50
+ toLine: number;
51
+ text: string;
52
+ }) => void, maxLines?: number, maxMs?: number);
53
+ add(line: string, lineNo: number): void;
54
+ /** Flush whatever is buffered right now, regardless of size/time thresholds. */
55
+ flush(): void;
56
+ }
20
57
  /**
21
58
  * Shell handler input
22
59
  */
@@ -77,4 +114,4 @@ declare const _default: {
77
114
  execute: typeof shellHandler;
78
115
  };
79
116
 
80
- export { type ShellInput, type ShellOutput, _default as default, mergeJsonOutputs, parseOutputMarkerLine, tail };
117
+ export { LineBatcher, type ShellInput, type ShellOutput, _default as default, mergeJsonOutputs, parseOutputMarkerLine, tail };
package/dist/shell.js CHANGED
@@ -15,13 +15,75 @@ var BLOCKED_COMMANDS = [
15
15
  "fdisk"
16
16
  ];
17
17
  var FAILURE_OUTPUT_TAIL_CHARS = 4e3;
18
+ var MARKER_LINE_MAX_CHARS = 2e3;
19
+ function lastOutputMarkerLine(text) {
20
+ const lines = text.split("\n");
21
+ for (let i = lines.length - 1; i >= 0; i--) {
22
+ const line = lines[i];
23
+ if (line !== void 0 && (line.includes(OUTPUT_MARKER_B64) || line.includes(OUTPUT_MARKER))) {
24
+ return line;
25
+ }
26
+ }
27
+ return null;
28
+ }
29
+ var LOG_BATCH_MAX_LINES = 50;
30
+ var LOG_BATCH_MAX_MS = 500;
18
31
  function tail(text, maxChars) {
19
32
  if (text.length <= maxChars) {
20
33
  return text;
21
34
  }
22
- return `\u2026(truncated, showing last ${maxChars} of ${text.length} chars)
35
+ const rawTail = `\u2026(truncated, showing last ${maxChars} of ${text.length} chars)
23
36
  ${text.slice(-maxChars)}`;
37
+ const markerLine = lastOutputMarkerLine(text);
38
+ if (!markerLine || rawTail.includes(markerLine)) {
39
+ return rawTail;
40
+ }
41
+ const markerSnippet = markerLine.length > MARKER_LINE_MAX_CHARS ? `${markerLine.slice(0, MARKER_LINE_MAX_CHARS)}\u2026(marker line truncated, ${markerLine.length} chars total)` : markerLine;
42
+ return `${markerSnippet}
43
+ ${rawTail}`;
24
44
  }
45
+ var LineBatcher = class {
46
+ constructor(stream, onFlush, maxLines = LOG_BATCH_MAX_LINES, maxMs = LOG_BATCH_MAX_MS) {
47
+ this.stream = stream;
48
+ this.onFlush = onFlush;
49
+ this.maxLines = maxLines;
50
+ this.maxMs = maxMs;
51
+ }
52
+ stream;
53
+ onFlush;
54
+ maxLines;
55
+ maxMs;
56
+ buffer = [];
57
+ timer;
58
+ add(line, lineNo) {
59
+ this.buffer.push({ line, lineNo });
60
+ if (this.buffer.length >= this.maxLines) {
61
+ this.flush();
62
+ return;
63
+ }
64
+ if (!this.timer) {
65
+ this.timer = setTimeout(() => this.flush(), this.maxMs);
66
+ }
67
+ }
68
+ /** Flush whatever is buffered right now, regardless of size/time thresholds. */
69
+ flush() {
70
+ if (this.timer) {
71
+ clearTimeout(this.timer);
72
+ this.timer = void 0;
73
+ }
74
+ if (this.buffer.length === 0) {
75
+ return;
76
+ }
77
+ const batch = this.buffer;
78
+ this.buffer = [];
79
+ this.onFlush({
80
+ stream: this.stream,
81
+ fromLine: batch[0].lineNo,
82
+ toLine: batch[batch.length - 1].lineNo,
83
+ text: batch.map((entry) => entry.line).join("\n")
84
+ });
85
+ }
86
+ };
25
87
  var OUTPUT_MARKER = "::kb-output::";
26
88
  var OUTPUT_MARKER_B64 = "::kb-output:base64::";
27
89
  function parseOutputMarkerLine(line) {
@@ -133,9 +195,21 @@ async function shellHandler(ctx, input) {
133
195
  let stderrBuf = "";
134
196
  let stdoutFull = "";
135
197
  let stderrFull = "";
198
+ const onBatchFlush = (batch) => {
199
+ const log = batch.stream === "stderr" ? ctx.platform.logger.warn : ctx.platform.logger.info;
200
+ log.call(ctx.platform.logger, `Shell output (${batch.stream})`, {
201
+ stream: batch.stream,
202
+ fromLine: batch.fromLine,
203
+ toLine: batch.toLine,
204
+ text: batch.text
205
+ });
206
+ };
207
+ const stdoutBatcher = new LineBatcher("stdout", onBatchFlush);
208
+ const stderrBatcher = new LineBatcher("stderr", onBatchFlush);
136
209
  const emitLine = (stream, line) => {
137
210
  lineNo++;
138
211
  void ctx.api.events.emit("log.line", { stream, line, lineNo, level: stream === "stderr" ? "warn" : "info" });
212
+ (stream === "stderr" ? stderrBatcher : stdoutBatcher).add(line, lineNo);
139
213
  };
140
214
  proc.stdout?.on("data", (chunk) => {
141
215
  const text = chunk.toString();
@@ -176,13 +250,26 @@ async function shellHandler(ctx, input) {
176
250
  if (stderrBuf) {
177
251
  emitLine("stderr", stderrBuf);
178
252
  }
253
+ stdoutBatcher.flush();
254
+ stderrBatcher.flush();
179
255
  if (timedOut) {
256
+ const stderrTail = tail(stderrFull, FAILURE_OUTPUT_TAIL_CHARS);
257
+ const stdoutTail = tail(stdoutFull, FAILURE_OUTPUT_TAIL_CHARS);
180
258
  ctx.platform.logger.warn("Shell command timed out", {
181
259
  timeoutMs: timeout,
182
- stderrTail: tail(stderrFull, FAILURE_OUTPUT_TAIL_CHARS),
183
- stdoutTail: tail(stdoutFull, FAILURE_OUTPUT_TAIL_CHARS)
260
+ stderrTail,
261
+ stdoutTail
184
262
  });
185
- throw new Error(`Shell command timed out after ${timeout}ms`);
263
+ const parts = [`Shell command timed out after ${timeout}ms: ${command.slice(0, 200)}`];
264
+ if (stdoutTail) {
265
+ parts.push(`--- stdout (tail) ---
266
+ ${stdoutTail}`);
267
+ }
268
+ if (stderrTail) {
269
+ parts.push(`--- stderr (tail) ---
270
+ ${stderrTail}`);
271
+ }
272
+ throw new Error(parts.join("\n"));
186
273
  }
187
274
  const output = {
188
275
  // Use our accumulated buffers — result.stdout/stderr are empty because
@@ -205,13 +292,36 @@ async function shellHandler(ctx, input) {
205
292
  stdoutTail: tail(output.stdout, FAILURE_OUTPUT_TAIL_CHARS)
206
293
  });
207
294
  if (throwOnError) {
208
- throw new Error(`Shell command failed with exit code ${output.exitCode}: ${output.stderr.slice(0, 500)}`);
295
+ const failParts = [`Shell command failed with exit code ${output.exitCode}: ${command.slice(0, 200)}`];
296
+ const failStderrTail = tail(output.stderr, FAILURE_OUTPUT_TAIL_CHARS);
297
+ const failStdoutTail = tail(output.stdout, FAILURE_OUTPUT_TAIL_CHARS);
298
+ if (failStdoutTail) {
299
+ failParts.push(`--- stdout (tail) ---
300
+ ${failStdoutTail}`);
301
+ }
302
+ if (failStderrTail) {
303
+ failParts.push(`--- stderr (tail) ---
304
+ ${failStderrTail}`);
305
+ }
306
+ throw new Error(failParts.join("\n"));
209
307
  }
210
308
  }
211
309
  return mergeJsonOutputs(output, (msg) => ctx.platform.logger.warn(`[shell] ${msg}`));
212
310
  } catch (error) {
213
311
  if (error && typeof error === "object" && "timedOut" in error && error.timedOut) {
214
- throw new Error(`Shell command timed out after ${timeout}ms`);
312
+ const execTimeoutError = error;
313
+ const timeoutParts = [`Shell command timed out after ${timeout}ms: ${command.slice(0, 200)}`];
314
+ const execStdoutTail = tail(execTimeoutError.stdout ?? "", FAILURE_OUTPUT_TAIL_CHARS);
315
+ const execStderrTail = tail(execTimeoutError.stderr ?? "", FAILURE_OUTPUT_TAIL_CHARS);
316
+ if (execStdoutTail) {
317
+ timeoutParts.push(`--- stdout (tail) ---
318
+ ${execStdoutTail}`);
319
+ }
320
+ if (execStderrTail) {
321
+ timeoutParts.push(`--- stderr (tail) ---
322
+ ${execStderrTail}`);
323
+ }
324
+ throw new Error(timeoutParts.join("\n"));
215
325
  }
216
326
  if (error && typeof error === "object" && "exitCode" in error) {
217
327
  const execError = error;
@@ -236,6 +346,6 @@ var shell_default = {
236
346
  execute: shellHandler
237
347
  };
238
348
 
239
- export { shell_default as default, mergeJsonOutputs, parseOutputMarkerLine, tail };
349
+ export { LineBatcher, shell_default as default, mergeJsonOutputs, parseOutputMarkerLine, tail };
240
350
  //# sourceMappingURL=shell.js.map
241
351
  //# sourceMappingURL=shell.js.map
package/dist/shell.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/shell.ts"],"names":[],"mappings":"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","file":"shell.js","sourcesContent":["/**\n * @module @kb-labs/workflow-runtime/builtin-handlers/shell\n * Built-in shell execution handler for workflows\n *\n * Security features:\n * - Blocks dangerous commands (rm -rf /, fork bombs, etc.)\n * - Timeout enforcement (default 5 minutes)\n * - Environment variable isolation\n * - Working directory restrictions\n */\n\nimport { execaCommand } from 'execa';\nimport type { PluginContextV3 } from '@kb-labs/plugin-contracts';\n\n/**\n * Commands that are always blocked (dangerous)\n */\nconst BLOCKED_COMMANDS = [\n 'rm -rf /',\n 'rm -rf /*',\n 'mkfs',\n 'dd if=',\n ':(){:|:&};:', // Fork bomb\n 'chmod -R 777 /',\n 'chown -R',\n '> /dev/sda',\n 'mv /* ',\n 'fdisk',\n];\n\n/** Cap on how much of a failed command's output gets attached to its log entry. */\nconst FAILURE_OUTPUT_TAIL_CHARS = 4000;\n\n/**\n * Last N characters of `text`, prefixed with a marker when it was truncated.\n * Tail (not head) because the actionable error is almost always at the end —\n * a stack trace, an assertion failure, the final \"Error:\" line.\n */\nexport function tail(text: string, maxChars: number): string {\n if (text.length <= maxChars) {return text;}\n return `…(truncated, showing last ${maxChars} of ${text.length} chars)\\n${text.slice(-maxChars)}`;\n}\n\n/**\n * Split string into chunks of specified size\n */\nfunction chunkString(str: string, chunkSize: number): string[] {\n const chunks: string[] = [];\n for (let i = 0; i < str.length; i += chunkSize) {\n chunks.push(str.slice(i, i + chunkSize));\n }\n return chunks;\n}\n\n/**\n * Shell handler input\n */\nexport interface ShellInput {\n /** Command to execute */\n command: string;\n\n /** Additional environment variables */\n env?: Record<string, string>;\n\n /** Timeout in milliseconds (default: 300000 = 5 min) */\n timeout?: number;\n\n /** Throw on non-zero exit code (default: false) */\n throwOnError?: boolean;\n}\n\n/**\n * Shell handler output\n */\nexport interface ShellOutput {\n /** Standard output */\n stdout: string;\n\n /** Standard error */\n stderr: string;\n\n /** Exit code */\n exitCode: number;\n\n /** Whether command succeeded (exitCode === 0) */\n ok: boolean;\n}\n\n/**\n * Plain-JSON output marker.\n * echo '::kb-output::{\"passed\":true}'\n *\n * Only safe for values without embedded newlines, quotes, or control chars.\n * For multi-line text, binary data, or any value produced by a sub-command,\n * prefer the base64 variant below.\n */\nconst OUTPUT_MARKER = '::kb-output::';\n\n/**\n * Base64-encoded JSON output marker — JSON-safe for any payload.\n *\n * Usage (shell):\n * PAYLOAD=$(jq -cn --arg plan \"$(cat /tmp/plan.md)\" '{plan: $plan}' | base64 -w0)\n * echo \"::kb-output:base64::${PAYLOAD}\"\n *\n * This handles multi-line text, special chars, and any binary-safe content.\n * The base64 string is decoded and JSON-parsed by the runtime.\n */\nconst OUTPUT_MARKER_B64 = '::kb-output:base64::';\n\n/**\n * Parse a single ::kb-output:: or ::kb-output:base64:: marker line into key-value pairs.\n *\n * Returns the parsed object, or null if the line is not a recognized marker.\n * Throws a descriptive error if the marker payload is malformed so callers can warn.\n */\nexport function parseOutputMarkerLine(line: string): Record<string, unknown> | null {\n // Base64-encoded variant — JSON-safe for any payload\n const b64Idx = line.indexOf(OUTPUT_MARKER_B64);\n if (b64Idx !== -1) {\n const raw = line.slice(b64Idx + OUTPUT_MARKER_B64.length).trim();\n let decoded: string;\n try {\n decoded = Buffer.from(raw, 'base64').toString('utf8');\n } catch {\n throw new Error(`::kb-output:base64:: payload is not valid base64`);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(decoded);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new Error(`::kb-output:base64:: malformed JSON payload after decode: ${detail}`);\n }\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>;\n }\n throw new Error(`::kb-output:base64:: JSON must be an object, got ${Array.isArray(parsed) ? 'array' : typeof parsed}`);\n }\n\n // Plain-JSON variant — only safe for simple scalar values\n const idx = line.indexOf(OUTPUT_MARKER);\n if (idx !== -1) {\n const raw = line.slice(idx + OUTPUT_MARKER.length);\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new Error(`::kb-output:: malformed JSON payload: ${detail}`);\n }\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>;\n }\n throw new Error(`::kb-output:: JSON must be an object, got ${Array.isArray(parsed) ? 'array' : typeof parsed}`);\n }\n\n return null;\n}\n\n/**\n * Extract structured outputs from shell stdout.\n *\n * Priority:\n * 1. ::kb-output:base64:: marker lines — JSON-safe, recommended for complex values\n * 2. ::kb-output:: marker lines — plain JSON, ok for simple scalars\n * 3. Entire stdout as JSON — fallback for backward compat (simple commands)\n *\n * Malformed markers emit a warning instead of silently dropping the output.\n * Logs and other stdout content are ignored for output purposes.\n */\nexport function mergeJsonOutputs(output: ShellOutput, warn?: (msg: string) => void): Record<string, unknown> {\n const base: Record<string, unknown> = { ...output };\n const trimmed = output.stdout.trim();\n if (!trimmed) {return base;}\n\n // Priority 1 + 2: Look for ::kb-output:: or ::kb-output:base64:: marker lines\n const lines = output.stdout.split('\\n');\n let foundMarker = false;\n for (const line of lines) {\n if (!line.includes('::kb-output')) {continue;}\n foundMarker = true;\n try {\n const parsed = parseOutputMarkerLine(line);\n if (parsed) {\n Object.assign(base, parsed);\n }\n } catch (err) {\n // Malformed marker — warn instead of silently dropping\n const msg = err instanceof Error ? err.message : String(err);\n warn?.(`Malformed ::kb-output:: marker (outputs not populated): ${msg}`);\n }\n }\n\n if (foundMarker) {return base;}\n\n // Priority 3: Fallback — entire stdout as JSON (backward compat)\n try {\n const parsed: unknown = JSON.parse(trimmed);\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n Object.assign(base, parsed as Record<string, unknown>);\n }\n } catch {\n // Not JSON — return as-is\n }\n\n return base;\n}\n\n/**\n * Built-in shell execution handler.\n *\n * Executes shell commands with safety checks and timeout enforcement.\n *\n * @param ctx - Handler execution context\n * @param input - Shell command input\n * @returns Shell execution result\n * @throws Error if dangerous command detected or timeout exceeded\n */\nasync function shellHandler(\n ctx: PluginContextV3,\n input: ShellInput,\n): Promise<Record<string, unknown>> {\n const { command, env = {}, timeout = 300000, throwOnError = false } = input;\n\n // Security: Check for dangerous commands\n const normalizedCommand = command.toLowerCase().trim();\n for (const blocked of BLOCKED_COMMANDS) {\n if (normalizedCommand.includes(blocked.toLowerCase())) {\n throw new Error(\n `Dangerous command blocked: \"${blocked}\". Command attempted: ${command.slice(0, 100)}`,\n );\n }\n }\n\n // Get working directory from context (workflow workspace)\n const cwd = ctx.cwd;\n\n // Merge environment variables\n const mergedEnv = {\n ...process.env,\n ...env,\n };\n\n ctx.platform.logger.info('Executing shell command', {\n command: command.slice(0, 200),\n cwd,\n timeout,\n });\n\n try {\n // detached: true puts the subprocess in its own process group. On timeout we kill\n // the entire group (process.kill(-pid, 'SIGKILL')), which takes down both the shell\n // and any children it spawned. Without this, SIGTERM reaches only the shell; child\n // processes (e.g. `sleep N`) inherit the pipe write-end and block await indefinitely.\n const proc = execaCommand(command, {\n cwd,\n env: mergedEnv,\n shell: true,\n stdio: 'pipe',\n reject: false, // We handle exit codes ourselves\n detached: true,\n });\n\n // Stream stdout/stderr line-by-line in real-time.\n // We consume the streams ourselves via 'data' events for live log streaming.\n // Because attaching a 'data' listener drains the stream, execa's result.stdout\n // will be empty — we must reconstruct the full output from collected chunks.\n let lineNo = 0;\n let stdoutBuf = '';\n let stderrBuf = '';\n let stdoutFull = '';\n let stderrFull = '';\n\n const emitLine = (stream: 'stdout' | 'stderr', line: string) => {\n lineNo++;\n // stderr is an output stream, not an exit-status signal. Several\n // perfectly successful tools (notably command discovery) deliberately\n // write diagnostics there. The step result is evaluated from exitCode;\n // keep stderr visible without turning every diagnostic line into an\n // error event.\n void ctx.api.events.emit('log.line', { stream, line, lineNo, level: stream === 'stderr' ? 'warn' : 'info' });\n };\n\n proc.stdout?.on('data', (chunk: Buffer) => {\n const text = chunk.toString();\n stdoutFull += text;\n stdoutBuf += text;\n const lines = stdoutBuf.split('\\n');\n stdoutBuf = lines.pop() ?? '';\n for (const line of lines) {emitLine('stdout', line);}\n });\n\n proc.stderr?.on('data', (chunk: Buffer) => {\n const text = chunk.toString();\n stderrFull += text;\n stderrBuf += text;\n const lines = stderrBuf.split('\\n');\n stderrBuf = lines.pop() ?? '';\n for (const line of lines) {emitLine('stderr', line);}\n });\n\n // Self-managed timeout: kill the entire process group so orphaned children\n // release the pipe write-end and await proc resolves immediately.\n let timedOut = false;\n const killTimer = setTimeout(() => {\n timedOut = true;\n const pid = proc.pid;\n if (pid !== undefined) {\n try {\n process.kill(-pid, 'SIGKILL');\n } catch {\n // process already exited\n }\n }\n }, timeout);\n\n const result = await proc;\n clearTimeout(killTimer);\n\n // Flush remaining buffered content\n if (stdoutBuf) {emitLine('stdout', stdoutBuf);}\n if (stderrBuf) {emitLine('stderr', stderrBuf);}\n\n if (timedOut) {\n // Whatever the command printed before the kill is the only clue to\n // whether it was hung or just slow — without it the run log shows a\n // multi-hour gap between \"Executing shell command\" and this error and\n // nothing in between (see kb-labs incident: 20min Build-step timeout\n // debugged blind because of exactly this).\n ctx.platform.logger.warn('Shell command timed out', {\n timeoutMs: timeout,\n stderrTail: tail(stderrFull, FAILURE_OUTPUT_TAIL_CHARS),\n stdoutTail: tail(stdoutFull, FAILURE_OUTPUT_TAIL_CHARS),\n });\n throw new Error(`Shell command timed out after ${timeout}ms`);\n }\n\n const output: ShellOutput = {\n // Use our accumulated buffers — result.stdout/stderr are empty because\n // we consumed the streams via 'data' listeners above.\n stdout: stdoutFull || result.stdout,\n stderr: stderrFull || result.stderr,\n exitCode: result.exitCode ?? 0,\n ok: (result.exitCode ?? 0) === 0,\n };\n\n if (output.ok) {\n ctx.platform.logger.info('Shell command completed successfully', {\n exitCode: output.exitCode,\n stdoutLines: output.stdout.split('\\n').length,\n });\n } else {\n // Attach the actual output, not just its shape — this is the log entry\n // that shows up in `kb workflow runs logs`, and it used to carry only\n // exitCode + a line count. Debugging a failed step meant re-running the\n // command outside the workflow just to see why it failed.\n ctx.platform.logger.warn('Shell command failed', {\n exitCode: output.exitCode,\n stderrLines: output.stderr.split('\\n').length,\n stderrTail: tail(output.stderr, FAILURE_OUTPUT_TAIL_CHARS),\n stdoutTail: tail(output.stdout, FAILURE_OUTPUT_TAIL_CHARS),\n });\n\n if (throwOnError) {\n throw new Error(`Shell command failed with exit code ${output.exitCode}: ${output.stderr.slice(0, 500)}`);\n }\n }\n\n return mergeJsonOutputs(output, (msg) => ctx.platform.logger.warn(`[shell] ${msg}`));\n } catch (error) {\n // Handle timeout\n if (error && typeof error === 'object' && 'timedOut' in error && error.timedOut) {\n throw new Error(`Shell command timed out after ${timeout}ms`);\n }\n\n // Handle execution error\n if (error && typeof error === 'object' && 'exitCode' in error) {\n const execError = error as { exitCode?: number; stdout?: string; stderr?: string };\n const output: ShellOutput = {\n stdout: execError.stdout ?? '',\n stderr: execError.stderr ?? '',\n exitCode: execError.exitCode ?? 1,\n ok: false,\n };\n\n ctx.platform.logger.error('Shell command execution failed', undefined, {\n exitCode: output.exitCode,\n stderr: output.stderr.slice(0, 500),\n });\n\n if (!throwOnError) {\n return { ...output };\n }\n }\n\n throw error;\n }\n}\n\n// Export handler in format expected by ExecutionBackend\nexport default {\n execute: shellHandler,\n};\n"]}
1
+ {"version":3,"sources":["../../home/runner/work/kb-labs/kb-labs/plugins/workflow/steps/src/shell.ts"],"names":[],"mappings":"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","file":"shell.js","sourcesContent":["/**\n * @module @kb-labs/workflow-runtime/builtin-handlers/shell\n * Built-in shell execution handler for workflows\n *\n * Security features:\n * - Blocks dangerous commands (rm -rf /, fork bombs, etc.)\n * - Timeout enforcement (default 5 minutes)\n * - Environment variable isolation\n * - Working directory restrictions\n */\n\nimport { execaCommand } from 'execa';\nimport type { PluginContextV3 } from '@kb-labs/plugin-contracts';\n\n/**\n * Commands that are always blocked (dangerous)\n */\nconst BLOCKED_COMMANDS = [\n 'rm -rf /',\n 'rm -rf /*',\n 'mkfs',\n 'dd if=',\n ':(){:|:&};:', // Fork bomb\n 'chmod -R 777 /',\n 'chown -R',\n '> /dev/sda',\n 'mv /* ',\n 'fdisk',\n];\n\n/** Cap on how much of a failed command's output gets attached to its log entry. */\nconst FAILURE_OUTPUT_TAIL_CHARS = 4000;\n\n/** Cap on how much of a surfaced ::kb-output:: marker line gets attached — see `tail()`. */\nconst MARKER_LINE_MAX_CHARS = 2000;\n\n/**\n * The last `::kb-output::`/`::kb-output:base64::` line in `text`, or null.\n * A `kb` command reports its real structured result on one of these lines\n * (e.g. `{\"ok\":false,\"failed\":[\"dist-exports\"],...}`) — often followed by a\n * wrapper's own generic exit banner (pnpm's \"[ELIFECYCLE] Command failed\n * with exit code 1.\"), which then becomes the literal last bytes of output.\n */\nfunction lastOutputMarkerLine(text: string): string | null {\n const lines = text.split('\\n');\n for (let i = lines.length - 1; i >= 0; i--) {\n const line = lines[i];\n if (line !== undefined && (line.includes(OUTPUT_MARKER_B64) || line.includes(OUTPUT_MARKER))) {\n return line;\n }\n }\n return null;\n}\n\n/**\n * Batching for durable line persistence: the ring buffer's `maxSize` is a\n * fixed number of records shared across the entire daemon (not per run/step),\n * so one `ctx.platform.logger` call per output line would let a single\n * verbose step (e.g. a package install with thousands of lines) evict every\n * other run's recent logs from the hot tier. We accumulate lines and flush\n * as one logger call per window, keyed by whichever limit is hit first.\n */\nconst LOG_BATCH_MAX_LINES = 50;\nconst LOG_BATCH_MAX_MS = 500;\n\n/**\n * Last N characters of `text`, prefixed with a marker when it was truncated.\n * Tail (not head) because the actionable error is almost always at the end —\n * a stack trace, an assertion failure, the final \"Error:\" line.\n *\n * Exception: a raw byte-count tail alone silently drops the one thing that\n * actually explains a failed `kb` command. A wrapper's generic exit banner\n * (pnpm's \"[ELIFECYCLE] Command failed...\") becomes the true last bytes of\n * output, while the command's own `::kb-output::` result — the whole reason\n * `runReleaseChecks` and friends exist — sits earlier in the stream and gets\n * discarded (observed live: a failed `release checks` step logged only\n * \"[ELIFECYCLE] Command failed with exit code 1.\" with the real\n * `{\"ok\":false,\"failed\":[...]}` payload truncated away). When a marker line\n * is present, surface it ahead of the raw tail instead of dropping it.\n */\nexport function tail(text: string, maxChars: number): string {\n if (text.length <= maxChars) {return text;}\n const rawTail = `…(truncated, showing last ${maxChars} of ${text.length} chars)\\n${text.slice(-maxChars)}`;\n const markerLine = lastOutputMarkerLine(text);\n if (!markerLine || rawTail.includes(markerLine)) {return rawTail;}\n const markerSnippet = markerLine.length > MARKER_LINE_MAX_CHARS\n ? `${markerLine.slice(0, MARKER_LINE_MAX_CHARS)}…(marker line truncated, ${markerLine.length} chars total)`\n : markerLine;\n return `${markerSnippet}\\n${rawTail}`;\n}\n\n/**\n * Accumulates output lines for one stream (stdout or stderr) and flushes them\n * as a single durable log write once `maxLines` lines have queued up or\n * `maxMs` has elapsed since the first unflushed line — whichever comes first.\n *\n * This is what makes per-line shell output durable without turning every\n * line into its own `ctx.platform.logger` call: the daemon's ring buffer is a\n * fixed number of records shared across the whole daemon, so one write per\n * line would let a single verbose step evict every other run's recent logs.\n */\nexport class LineBatcher {\n private buffer: Array<{ line: string; lineNo: number }> = [];\n private timer: ReturnType<typeof setTimeout> | undefined;\n\n public constructor(\n private readonly stream: 'stdout' | 'stderr',\n private readonly onFlush: (batch: { stream: 'stdout' | 'stderr'; fromLine: number; toLine: number; text: string }) => void,\n private readonly maxLines = LOG_BATCH_MAX_LINES,\n private readonly maxMs = LOG_BATCH_MAX_MS,\n ) {}\n\n public add(line: string, lineNo: number): void {\n this.buffer.push({ line, lineNo });\n if (this.buffer.length >= this.maxLines) {\n this.flush();\n return;\n }\n if (!this.timer) {\n this.timer = setTimeout(() => this.flush(), this.maxMs);\n }\n }\n\n /** Flush whatever is buffered right now, regardless of size/time thresholds. */\n public flush(): void {\n if (this.timer) {\n clearTimeout(this.timer);\n this.timer = undefined;\n }\n if (this.buffer.length === 0) {return;}\n const batch = this.buffer;\n this.buffer = [];\n this.onFlush({\n stream: this.stream,\n fromLine: batch[0]!.lineNo,\n toLine: batch[batch.length - 1]!.lineNo,\n text: batch.map((entry) => entry.line).join('\\n'),\n });\n }\n}\n\n/**\n * Split string into chunks of specified size\n */\nfunction chunkString(str: string, chunkSize: number): string[] {\n const chunks: string[] = [];\n for (let i = 0; i < str.length; i += chunkSize) {\n chunks.push(str.slice(i, i + chunkSize));\n }\n return chunks;\n}\n\n/**\n * Shell handler input\n */\nexport interface ShellInput {\n /** Command to execute */\n command: string;\n\n /** Additional environment variables */\n env?: Record<string, string>;\n\n /** Timeout in milliseconds (default: 300000 = 5 min) */\n timeout?: number;\n\n /** Throw on non-zero exit code (default: false) */\n throwOnError?: boolean;\n}\n\n/**\n * Shell handler output\n */\nexport interface ShellOutput {\n /** Standard output */\n stdout: string;\n\n /** Standard error */\n stderr: string;\n\n /** Exit code */\n exitCode: number;\n\n /** Whether command succeeded (exitCode === 0) */\n ok: boolean;\n}\n\n/**\n * Plain-JSON output marker.\n * echo '::kb-output::{\"passed\":true}'\n *\n * Only safe for values without embedded newlines, quotes, or control chars.\n * For multi-line text, binary data, or any value produced by a sub-command,\n * prefer the base64 variant below.\n */\nconst OUTPUT_MARKER = '::kb-output::';\n\n/**\n * Base64-encoded JSON output marker — JSON-safe for any payload.\n *\n * Usage (shell):\n * PAYLOAD=$(jq -cn --arg plan \"$(cat /tmp/plan.md)\" '{plan: $plan}' | base64 -w0)\n * echo \"::kb-output:base64::${PAYLOAD}\"\n *\n * This handles multi-line text, special chars, and any binary-safe content.\n * The base64 string is decoded and JSON-parsed by the runtime.\n */\nconst OUTPUT_MARKER_B64 = '::kb-output:base64::';\n\n/**\n * Parse a single ::kb-output:: or ::kb-output:base64:: marker line into key-value pairs.\n *\n * Returns the parsed object, or null if the line is not a recognized marker.\n * Throws a descriptive error if the marker payload is malformed so callers can warn.\n */\nexport function parseOutputMarkerLine(line: string): Record<string, unknown> | null {\n // Base64-encoded variant — JSON-safe for any payload\n const b64Idx = line.indexOf(OUTPUT_MARKER_B64);\n if (b64Idx !== -1) {\n const raw = line.slice(b64Idx + OUTPUT_MARKER_B64.length).trim();\n let decoded: string;\n try {\n decoded = Buffer.from(raw, 'base64').toString('utf8');\n } catch {\n throw new Error(`::kb-output:base64:: payload is not valid base64`);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(decoded);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new Error(`::kb-output:base64:: malformed JSON payload after decode: ${detail}`);\n }\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>;\n }\n throw new Error(`::kb-output:base64:: JSON must be an object, got ${Array.isArray(parsed) ? 'array' : typeof parsed}`);\n }\n\n // Plain-JSON variant — only safe for simple scalar values\n const idx = line.indexOf(OUTPUT_MARKER);\n if (idx !== -1) {\n const raw = line.slice(idx + OUTPUT_MARKER.length);\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new Error(`::kb-output:: malformed JSON payload: ${detail}`);\n }\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>;\n }\n throw new Error(`::kb-output:: JSON must be an object, got ${Array.isArray(parsed) ? 'array' : typeof parsed}`);\n }\n\n return null;\n}\n\n/**\n * Extract structured outputs from shell stdout.\n *\n * Priority:\n * 1. ::kb-output:base64:: marker lines — JSON-safe, recommended for complex values\n * 2. ::kb-output:: marker lines — plain JSON, ok for simple scalars\n * 3. Entire stdout as JSON — fallback for backward compat (simple commands)\n *\n * Malformed markers emit a warning instead of silently dropping the output.\n * Logs and other stdout content are ignored for output purposes.\n */\nexport function mergeJsonOutputs(output: ShellOutput, warn?: (msg: string) => void): Record<string, unknown> {\n const base: Record<string, unknown> = { ...output };\n const trimmed = output.stdout.trim();\n if (!trimmed) {return base;}\n\n // Priority 1 + 2: Look for ::kb-output:: or ::kb-output:base64:: marker lines\n const lines = output.stdout.split('\\n');\n let foundMarker = false;\n for (const line of lines) {\n if (!line.includes('::kb-output')) {continue;}\n foundMarker = true;\n try {\n const parsed = parseOutputMarkerLine(line);\n if (parsed) {\n Object.assign(base, parsed);\n }\n } catch (err) {\n // Malformed marker — warn instead of silently dropping\n const msg = err instanceof Error ? err.message : String(err);\n warn?.(`Malformed ::kb-output:: marker (outputs not populated): ${msg}`);\n }\n }\n\n if (foundMarker) {return base;}\n\n // Priority 3: Fallback — entire stdout as JSON (backward compat)\n try {\n const parsed: unknown = JSON.parse(trimmed);\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n Object.assign(base, parsed as Record<string, unknown>);\n }\n } catch {\n // Not JSON — return as-is\n }\n\n return base;\n}\n\n/**\n * Built-in shell execution handler.\n *\n * Executes shell commands with safety checks and timeout enforcement.\n *\n * @param ctx - Handler execution context\n * @param input - Shell command input\n * @returns Shell execution result\n * @throws Error if dangerous command detected or timeout exceeded\n */\nasync function shellHandler(\n ctx: PluginContextV3,\n input: ShellInput,\n): Promise<Record<string, unknown>> {\n const { command, env = {}, timeout = 300000, throwOnError = false } = input;\n\n // Security: Check for dangerous commands\n const normalizedCommand = command.toLowerCase().trim();\n for (const blocked of BLOCKED_COMMANDS) {\n if (normalizedCommand.includes(blocked.toLowerCase())) {\n throw new Error(\n `Dangerous command blocked: \"${blocked}\". Command attempted: ${command.slice(0, 100)}`,\n );\n }\n }\n\n // Get working directory from context (workflow workspace)\n const cwd = ctx.cwd;\n\n // Merge environment variables\n const mergedEnv = {\n ...process.env,\n ...env,\n };\n\n ctx.platform.logger.info('Executing shell command', {\n command: command.slice(0, 200),\n cwd,\n timeout,\n });\n\n try {\n // detached: true puts the subprocess in its own process group. On timeout we kill\n // the entire group (process.kill(-pid, 'SIGKILL')), which takes down both the shell\n // and any children it spawned. Without this, SIGTERM reaches only the shell; child\n // processes (e.g. `sleep N`) inherit the pipe write-end and block await indefinitely.\n const proc = execaCommand(command, {\n cwd,\n env: mergedEnv,\n shell: true,\n stdio: 'pipe',\n reject: false, // We handle exit codes ourselves\n detached: true,\n });\n\n // Stream stdout/stderr line-by-line in real-time.\n // We consume the streams ourselves via 'data' events for live log streaming.\n // Because attaching a 'data' listener drains the stream, execa's result.stdout\n // will be empty — we must reconstruct the full output from collected chunks.\n let lineNo = 0;\n let stdoutBuf = '';\n let stderrBuf = '';\n let stdoutFull = '';\n let stderrFull = '';\n\n // Durable persistence for full per-line output, batched so a verbose step\n // (e.g. a package install emitting thousands of lines) doesn't blow away\n // the daemon's shared ring buffer with one record per line. This is\n // additive to the live 'log.line' event below, not a replacement for it.\n const onBatchFlush = (batch: { stream: 'stdout' | 'stderr'; fromLine: number; toLine: number; text: string }) => {\n const log = batch.stream === 'stderr' ? ctx.platform.logger.warn : ctx.platform.logger.info;\n log.call(ctx.platform.logger, `Shell output (${batch.stream})`, {\n stream: batch.stream,\n fromLine: batch.fromLine,\n toLine: batch.toLine,\n text: batch.text,\n });\n };\n const stdoutBatcher = new LineBatcher('stdout', onBatchFlush);\n const stderrBatcher = new LineBatcher('stderr', onBatchFlush);\n\n const emitLine = (stream: 'stdout' | 'stderr', line: string) => {\n lineNo++;\n // stderr is an output stream, not an exit-status signal. Several\n // perfectly successful tools (notably command discovery) deliberately\n // write diagnostics there. The step result is evaluated from exitCode;\n // keep stderr visible without turning every diagnostic line into an\n // error event.\n void ctx.api.events.emit('log.line', { stream, line, lineNo, level: stream === 'stderr' ? 'warn' : 'info' });\n (stream === 'stderr' ? stderrBatcher : stdoutBatcher).add(line, lineNo);\n };\n\n proc.stdout?.on('data', (chunk: Buffer) => {\n const text = chunk.toString();\n stdoutFull += text;\n stdoutBuf += text;\n const lines = stdoutBuf.split('\\n');\n stdoutBuf = lines.pop() ?? '';\n for (const line of lines) {emitLine('stdout', line);}\n });\n\n proc.stderr?.on('data', (chunk: Buffer) => {\n const text = chunk.toString();\n stderrFull += text;\n stderrBuf += text;\n const lines = stderrBuf.split('\\n');\n stderrBuf = lines.pop() ?? '';\n for (const line of lines) {emitLine('stderr', line);}\n });\n\n // Self-managed timeout: kill the entire process group so orphaned children\n // release the pipe write-end and await proc resolves immediately.\n let timedOut = false;\n const killTimer = setTimeout(() => {\n timedOut = true;\n const pid = proc.pid;\n if (pid !== undefined) {\n try {\n process.kill(-pid, 'SIGKILL');\n } catch {\n // process already exited\n }\n }\n }, timeout);\n\n const result = await proc;\n clearTimeout(killTimer);\n\n // Flush remaining buffered content\n if (stdoutBuf) {emitLine('stdout', stdoutBuf);}\n if (stderrBuf) {emitLine('stderr', stderrBuf);}\n\n // Flush any lines still waiting on the batch window/size threshold —\n // without this, the tail of a step's output (fewer than maxLines lines,\n // flushed less than maxMs ago) would never reach durable storage.\n stdoutBatcher.flush();\n stderrBatcher.flush();\n\n if (timedOut) {\n // Whatever the command printed before the kill is the only clue to\n // whether it was hung or just slow — without it the run log shows a\n // multi-hour gap between \"Executing shell command\" and this error and\n // nothing in between (see kb-labs incident: 20min Build-step timeout\n // debugged blind because of exactly this).\n const stderrTail = tail(stderrFull, FAILURE_OUTPUT_TAIL_CHARS);\n const stdoutTail = tail(stdoutFull, FAILURE_OUTPUT_TAIL_CHARS);\n ctx.platform.logger.warn('Shell command timed out', {\n timeoutMs: timeout,\n stderrTail,\n stdoutTail,\n });\n // The logger call above writes to the daemon's own log sink, but the\n // *thrown* error is what actually reaches the caller: it becomes\n // result.error in ExecutionResult, then result.error.message in\n // worker.ts's markStepFailed(), which is the only piece of this that\n // survives into the run's persisted step.error (engine.ts only keeps\n // message/stack, not arbitrary error.details). So the tails have to be\n // in the message itself, not just logged, or a step failure still\n // reads as an empty echo of the command with no signal about how far\n // it got. Command line is included too since that's often literally\n // the only thing otherwise in the message on a hung step.\n const parts = [`Shell command timed out after ${timeout}ms: ${command.slice(0, 200)}`];\n if (stdoutTail) { parts.push(`--- stdout (tail) ---\\n${stdoutTail}`); }\n if (stderrTail) { parts.push(`--- stderr (tail) ---\\n${stderrTail}`); }\n throw new Error(parts.join('\\n'));\n }\n\n const output: ShellOutput = {\n // Use our accumulated buffers — result.stdout/stderr are empty because\n // we consumed the streams via 'data' listeners above.\n stdout: stdoutFull || result.stdout,\n stderr: stderrFull || result.stderr,\n exitCode: result.exitCode ?? 0,\n ok: (result.exitCode ?? 0) === 0,\n };\n\n if (output.ok) {\n ctx.platform.logger.info('Shell command completed successfully', {\n exitCode: output.exitCode,\n stdoutLines: output.stdout.split('\\n').length,\n });\n } else {\n // Attach the actual output, not just its shape — this is the log entry\n // that shows up in `kb workflow runs logs`, and it used to carry only\n // exitCode + a line count. Debugging a failed step meant re-running the\n // command outside the workflow just to see why it failed.\n ctx.platform.logger.warn('Shell command failed', {\n exitCode: output.exitCode,\n stderrLines: output.stderr.split('\\n').length,\n stderrTail: tail(output.stderr, FAILURE_OUTPUT_TAIL_CHARS),\n stdoutTail: tail(output.stdout, FAILURE_OUTPUT_TAIL_CHARS),\n });\n\n if (throwOnError) {\n const failParts = [`Shell command failed with exit code ${output.exitCode}: ${command.slice(0, 200)}`];\n const failStderrTail = tail(output.stderr, FAILURE_OUTPUT_TAIL_CHARS);\n const failStdoutTail = tail(output.stdout, FAILURE_OUTPUT_TAIL_CHARS);\n if (failStdoutTail) { failParts.push(`--- stdout (tail) ---\\n${failStdoutTail}`); }\n if (failStderrTail) { failParts.push(`--- stderr (tail) ---\\n${failStderrTail}`); }\n throw new Error(failParts.join('\\n'));\n }\n }\n\n return mergeJsonOutputs(output, (msg) => ctx.platform.logger.warn(`[shell] ${msg}`));\n } catch (error) {\n // Handle timeout (defensive fallback: the primary path is the manually\n // managed killTimer above, which throws before this catch is reached —\n // this covers execa surfacing its own TimeoutError, e.g. if `reject`\n // behavior changes upstream). execa's TimeoutError/ExecaError carries\n // whatever stdout/stderr it captured on the error object itself.\n if (error && typeof error === 'object' && 'timedOut' in error && error.timedOut) {\n const execTimeoutError = error as { stdout?: string; stderr?: string };\n const timeoutParts = [`Shell command timed out after ${timeout}ms: ${command.slice(0, 200)}`];\n const execStdoutTail = tail(execTimeoutError.stdout ?? '', FAILURE_OUTPUT_TAIL_CHARS);\n const execStderrTail = tail(execTimeoutError.stderr ?? '', FAILURE_OUTPUT_TAIL_CHARS);\n if (execStdoutTail) { timeoutParts.push(`--- stdout (tail) ---\\n${execStdoutTail}`); }\n if (execStderrTail) { timeoutParts.push(`--- stderr (tail) ---\\n${execStderrTail}`); }\n throw new Error(timeoutParts.join('\\n'));\n }\n\n // Handle execution error\n if (error && typeof error === 'object' && 'exitCode' in error) {\n const execError = error as { exitCode?: number; stdout?: string; stderr?: string };\n const output: ShellOutput = {\n stdout: execError.stdout ?? '',\n stderr: execError.stderr ?? '',\n exitCode: execError.exitCode ?? 1,\n ok: false,\n };\n\n ctx.platform.logger.error('Shell command execution failed', undefined, {\n exitCode: output.exitCode,\n stderr: output.stderr.slice(0, 500),\n });\n\n if (!throwOnError) {\n return { ...output };\n }\n }\n\n throw error;\n }\n}\n\n// Export handler in format expected by ExecutionBackend\nexport default {\n execute: shellHandler,\n};\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kb-labs/workflow-steps",
3
3
  "description": "Built-in workflow step handlers (shell, approval, gate)",
4
- "version": "2.119.0-canary.e120bc0ee",
4
+ "version": "2.119.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -29,8 +29,8 @@
29
29
  ],
30
30
  "dependencies": {
31
31
  "execa": "^9.5.3",
32
- "@kb-labs/plugin-contracts": "2.119.0-canary.e120bc0ee",
33
- "@kb-labs/workflow-contracts": "2.119.0-canary.e120bc0ee"
32
+ "@kb-labs/plugin-contracts": "2.119.0",
33
+ "@kb-labs/workflow-contracts": "2.119.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^24.3.3",
@@ -38,10 +38,11 @@
38
38
  "tsup": "^8.5.0",
39
39
  "typescript": "^5.6.3",
40
40
  "vitest": "^3.2.6",
41
- "@kb-labs/devkit": "2.119.0-canary.e120bc0ee"
41
+ "@kb-labs/devkit": "2.119.0",
42
+ "@kb-labs/sdk": "2.115.4"
42
43
  },
43
44
  "engines": {
44
- "node": ">=22.0.0",
45
+ "node": ">=24.0.0",
45
46
  "pnpm": ">=9.0.0"
46
47
  },
47
48
  "scripts": {