@kody-ade/kody-engine 0.4.565 → 0.4.567

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/kody.js CHANGED
@@ -15,7 +15,7 @@ var init_package = __esm({
15
15
  "package.json"() {
16
16
  package_default = {
17
17
  name: "@kody-ade/kody-engine",
18
- version: "0.4.565",
18
+ version: "0.4.567",
19
19
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative implementation profiles.",
20
20
  license: "MIT",
21
21
  type: "module",
@@ -1143,14 +1143,23 @@ function buildVerifyEnv(source = process.env) {
1143
1143
  env.CI = source.CI ?? "1";
1144
1144
  return env;
1145
1145
  }
1146
- function runCommand(command, cwd) {
1146
+ function abortMessage(signal) {
1147
+ const reason = signal.reason;
1148
+ return reason instanceof Error ? reason.message : typeof reason === "string" ? reason : "verification aborted";
1149
+ }
1150
+ function runCommand(command, cwd, signal) {
1147
1151
  return new Promise((resolve23) => {
1148
1152
  const start = Date.now();
1153
+ if (signal?.aborted) {
1154
+ resolve23({ exitCode: -1, durationMs: 0, tail: abortMessage(signal) });
1155
+ return;
1156
+ }
1149
1157
  const child = spawn(command, {
1150
1158
  cwd,
1151
1159
  shell: true,
1152
1160
  env: buildVerifyEnv(),
1153
- stdio: ["ignore", "pipe", "pipe"]
1161
+ stdio: ["ignore", "pipe", "pipe"],
1162
+ detached: process.platform !== "win32"
1154
1163
  });
1155
1164
  const buffers = [];
1156
1165
  let totalSize = 0;
@@ -1164,24 +1173,46 @@ function runCommand(command, cwd) {
1164
1173
  };
1165
1174
  child.stdout?.on("data", collect);
1166
1175
  child.stderr?.on("data", collect);
1176
+ let settled = false;
1177
+ const killTree = (killSignal) => {
1178
+ try {
1179
+ if (process.platform !== "win32" && child.pid) process.kill(-child.pid, killSignal);
1180
+ else child.kill(killSignal);
1181
+ } catch {
1182
+ child.kill(killSignal);
1183
+ }
1184
+ };
1185
+ const finish = (exitCode, extraTail = "") => {
1186
+ if (settled) return;
1187
+ settled = true;
1188
+ clearTimeout(timer);
1189
+ signal?.removeEventListener("abort", onAbort);
1190
+ const output = Buffer.concat(buffers).toString("utf-8");
1191
+ const tail = [output, extraTail].filter(Boolean).join("\n").slice(-TAIL_CHARS);
1192
+ resolve23({ exitCode, durationMs: Date.now() - start, tail });
1193
+ };
1194
+ const terminate = () => {
1195
+ killTree("SIGTERM");
1196
+ setTimeout(() => killTree("SIGKILL"), 5e3).unref();
1197
+ };
1198
+ const onAbort = () => {
1199
+ terminate();
1200
+ finish(-1, signal ? abortMessage(signal) : "verification aborted");
1201
+ };
1202
+ signal?.addEventListener("abort", onAbort, { once: true });
1167
1203
  const timer = setTimeout(() => {
1168
- child.kill("SIGTERM");
1169
- setTimeout(() => {
1170
- if (!child.killed) child.kill("SIGKILL");
1171
- }, 5e3);
1204
+ terminate();
1205
+ finish(-1, "verification command timed out");
1172
1206
  }, COMMAND_TIMEOUT_MS);
1173
1207
  child.on("exit", (code) => {
1174
- clearTimeout(timer);
1175
- const tail = Buffer.concat(buffers).toString("utf-8").slice(-TAIL_CHARS);
1176
- resolve23({ exitCode: code ?? -1, durationMs: Date.now() - start, tail });
1208
+ finish(code ?? -1);
1177
1209
  });
1178
1210
  child.on("error", (err) => {
1179
- clearTimeout(timer);
1180
- resolve23({ exitCode: -1, durationMs: Date.now() - start, tail: err.message });
1211
+ finish(-1, err.message);
1181
1212
  });
1182
1213
  });
1183
1214
  }
1184
- async function verifyAll(config, cwd) {
1215
+ async function verifyAll(config, cwd, opts) {
1185
1216
  const commands = [];
1186
1217
  if (config.quality.typecheck) commands.push({ name: "typecheck", cmd: config.quality.typecheck });
1187
1218
  if (config.quality.testUnit) commands.push({ name: "test", cmd: config.quality.testUnit });
@@ -1190,20 +1221,21 @@ async function verifyAll(config, cwd) {
1190
1221
  const failed = [];
1191
1222
  const details = {};
1192
1223
  for (const { name, cmd } of commands) {
1193
- const result = await runCommand(cmd, cwd);
1224
+ const result = await runCommand(cmd, cwd, opts?.signal);
1194
1225
  details[name] = result;
1195
1226
  if (result.exitCode !== 0) failed.push(name);
1196
1227
  }
1197
1228
  return { ok: failed.length === 0, failed, details };
1198
1229
  }
1199
- async function applyTestRetries(initial, testCommand, cwd, runner, testRetries = DEFAULT_TEST_RETRIES) {
1230
+ async function applyTestRetries(initial, testCommand, cwd, runner, testRetries = DEFAULT_TEST_RETRIES, signal) {
1200
1231
  if (initial.ok) return { ...initial, recovered: [] };
1201
1232
  const recovered = [];
1202
1233
  const details = { ...initial.details };
1203
1234
  let failed = [...initial.failed];
1204
1235
  if (failed.includes("test") && testCommand && testRetries > 0) {
1205
1236
  for (let attempt = 1; attempt <= testRetries; attempt++) {
1206
- const retry = await runner(testCommand, cwd);
1237
+ if (signal?.aborted) break;
1238
+ const retry = await runner(testCommand, cwd, signal);
1207
1239
  details[`test (retry ${attempt})`] = retry;
1208
1240
  if (retry.exitCode === 0) {
1209
1241
  failed = failed.filter((f) => f !== "test");
@@ -1215,8 +1247,8 @@ async function applyTestRetries(initial, testCommand, cwd, runner, testRetries =
1215
1247
  return { ok: failed.length === 0, failed, details, recovered };
1216
1248
  }
1217
1249
  async function verifyAllWithRetry(config, cwd, opts) {
1218
- const initial = await verifyAll(config, cwd);
1219
- return applyTestRetries(initial, config.quality.testUnit, cwd, runCommand, opts?.testRetries);
1250
+ const initial = await verifyAll(config, cwd, { signal: opts?.signal });
1251
+ return applyTestRetries(initial, config.quality.testUnit, cwd, runCommand, opts?.testRetries, opts?.signal);
1220
1252
  }
1221
1253
  function stripAnsi(s) {
1222
1254
  return s.replace(ANSI_RE, "");
@@ -2289,6 +2321,7 @@ function parseWorkflowStep(value) {
2289
2321
  const target = stringField(raw.target);
2290
2322
  const delivery = stringField(raw.delivery);
2291
2323
  const targetFact = stringField(raw.targetFact ?? raw.target_fact);
2324
+ const timeoutSeconds = typeof raw.timeoutSeconds === "number" && Number.isInteger(raw.timeoutSeconds) && raw.timeoutSeconds > 0 && raw.timeoutSeconds <= 3600 ? raw.timeoutSeconds : void 0;
2292
2325
  const hasInput = Object.hasOwn(raw, "input");
2293
2326
  const inputs = parseWorkflowInputBindings(raw.inputs);
2294
2327
  const next = parseWorkflowTransitions(raw.next);
@@ -2304,6 +2337,7 @@ function parseWorkflowStep(value) {
2304
2337
  ...delivery === "pull-request" ? { delivery } : {},
2305
2338
  ...targetFact ? { targetFact } : {},
2306
2339
  ...reason ? { reason } : {},
2340
+ ...timeoutSeconds ? { timeoutSeconds } : {},
2307
2341
  ...next ? { next } : {},
2308
2342
  ...isPlainObject(raw.runWhen) ? { runWhen: raw.runWhen } : {},
2309
2343
  ...stringList(raw.continueOn ?? raw.continue_on).length > 0 ? { continueOn: stringList(raw.continueOn ?? raw.continue_on) } : {},
@@ -4722,6 +4756,15 @@ function validateWorkflow(value, options = {}) {
4722
4756
  if (step.input !== void 0 && step.inputs !== void 0) {
4723
4757
  issue(issues, "conflicting_inputs", base, "workflow step cannot declare both input and inputs");
4724
4758
  }
4759
+ const timeoutSeconds = step.timeoutSeconds;
4760
+ if (timeoutSeconds !== void 0 && (typeof timeoutSeconds !== "number" || !Number.isInteger(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 3600)) {
4761
+ issue(
4762
+ issues,
4763
+ "invalid_step_timeout",
4764
+ `${base}.timeoutSeconds`,
4765
+ "workflow step timeoutSeconds must be an integer from 1 to 3600"
4766
+ );
4767
+ }
4725
4768
  validateInputBindings(
4726
4769
  step.inputs,
4727
4770
  `${base}.inputs`,
@@ -5005,6 +5048,7 @@ var init_workflowValidation = __esm({
5005
5048
  "delivery",
5006
5049
  "targetFact",
5007
5050
  "reason",
5051
+ "timeoutSeconds",
5008
5052
  "next",
5009
5053
  "runWhen",
5010
5054
  "continueOn",
@@ -20877,7 +20921,7 @@ var init_verifyReproFails = __esm({
20877
20921
  // src/scripts/verifyWithRetry.ts
20878
20922
  async function runVerify(ctx) {
20879
20923
  try {
20880
- const result = await verifyAllWithRetry(ctx.config, ctx.cwd);
20924
+ const result = await verifyAllWithRetry(ctx.config, ctx.cwd, { signal: ctx.abortSignal });
20881
20925
  ctx.data.verifyOk = result.ok;
20882
20926
  ctx.data.verifyReason = result.ok ? "" : summarizeFailure(result);
20883
20927
  ctx.data.verifyRecovered = result.recovered ?? [];
@@ -20922,6 +20966,10 @@ var init_verifyWithRetry = __esm({
20922
20966
  verifyWithRetry = async (ctx) => {
20923
20967
  await runVerify(ctx);
20924
20968
  if (ctx.data.verifyOk !== false) return;
20969
+ if (ctx.abortSignal?.aborted) {
20970
+ downgradeActionOnFailure(ctx);
20971
+ return;
20972
+ }
20925
20973
  if (!ctx.data.agentDone) {
20926
20974
  downgradeActionOnFailure(ctx);
20927
20975
  return;
@@ -20958,6 +21006,10 @@ var init_verifyWithRetry = __esm({
20958
21006
  process.stderr.write(`[kody] verify retry crashed: ${err instanceof Error ? err.message : String(err)}
20959
21007
  `);
20960
21008
  }
21009
+ if (ctx.abortSignal?.aborted) {
21010
+ downgradeActionOnFailure(ctx);
21011
+ return;
21012
+ }
20961
21013
  await runVerify(ctx);
20962
21014
  if (ctx.data.verifyOk === true) {
20963
21015
  upgradeActionOnPass(ctx);
@@ -21961,6 +22013,7 @@ async function runImplementation(profileName, input) {
21961
22013
  config,
21962
22014
  verbose: input.verbose,
21963
22015
  quiet: input.quiet,
22016
+ abortSignal: input.abortController?.signal,
21964
22017
  // Phase 5 foundation: seed ctx.data with any preloaded values handed
21965
22018
  // in by a parent (typically a container loop). Loaders that see
21966
22019
  // their field already populated take the fast path and skip the
@@ -22042,6 +22095,10 @@ async function runImplementation(profileName, input) {
22042
22095
  const jobWhyBlock = typeof ctx.data.jobWhy === "string" ? operatorRequestBlock(ctx.data.jobWhy) : null;
22043
22096
  const jobRefBlock = jobReferenceBlock(profileName, profile, ctx.data);
22044
22097
  const invokeAgent = async (prompt) => {
22098
+ if (input.abortController?.signal.aborted) {
22099
+ const reason = input.abortController.signal.reason;
22100
+ throw reason instanceof Error ? reason : new Error("agent invocation aborted");
22101
+ }
22045
22102
  const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path51.isAbsolute(p) ? p : path51.resolve(profile.dir, p)).filter((p) => p.length > 0);
22046
22103
  const syntheticPath = ctx.data.syntheticPluginPath;
22047
22104
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
@@ -23574,25 +23631,31 @@ async function runGraphCapabilityWorkflow(parent, workflow, capability, base, ch
23574
23631
 
23575
23632
  `
23576
23633
  );
23577
- result = await runJob(child, {
23578
- ...base,
23579
- preloadedData: {
23580
- ...chainData,
23581
- runSubjectType: "capability",
23582
- runSubjectId: step.capability,
23583
- runSubjectLabel: step.id,
23584
- workflowStep: step.id,
23585
- workflowStepIndex: index + 1,
23586
- workflowExecutionKey: graphWorkflowExecutionKey(
23587
- base.preloadedData?.workflowExecutionKey,
23588
- capability.slug,
23589
- step.id,
23590
- state.transitionCounts
23591
- ),
23592
- workflowStepReason: step.reason,
23593
- workflowContinueOn: step.continueOn ?? []
23594
- }
23595
- });
23634
+ const stepAbort = workflowStepAbortController(base.abortController, step.timeoutSeconds);
23635
+ try {
23636
+ result = await runJob(child, {
23637
+ ...base,
23638
+ abortController: stepAbort.controller,
23639
+ preloadedData: {
23640
+ ...chainData,
23641
+ runSubjectType: "capability",
23642
+ runSubjectId: step.capability,
23643
+ runSubjectLabel: step.id,
23644
+ workflowStep: step.id,
23645
+ workflowStepIndex: index + 1,
23646
+ workflowExecutionKey: graphWorkflowExecutionKey(
23647
+ base.preloadedData?.workflowExecutionKey,
23648
+ capability.slug,
23649
+ step.id,
23650
+ state.transitionCounts
23651
+ ),
23652
+ workflowStepReason: step.reason,
23653
+ workflowContinueOn: step.continueOn ?? []
23654
+ }
23655
+ });
23656
+ } finally {
23657
+ stepAbort.cleanup();
23658
+ }
23596
23659
  finishWorkflowStep(state, step, result);
23597
23660
  mergeWorkflowResults(state, result.capabilityResults);
23598
23661
  if (result.capabilityOutput && typeof result.capabilityOutput === "object" && !Array.isArray(result.capabilityOutput)) {
@@ -23900,6 +23963,24 @@ function canContinueWorkflow(step, outcome) {
23900
23963
  if (!outcome || !step.continueOn || step.continueOn.length === 0) return false;
23901
23964
  return step.continueOn.includes(outcome.type);
23902
23965
  }
23966
+ function workflowStepAbortController(parent, timeoutSeconds) {
23967
+ if (!timeoutSeconds) return { controller: parent, cleanup: () => void 0 };
23968
+ const controller = new AbortController();
23969
+ const forwardParentAbort = () => controller.abort(parent?.signal.reason);
23970
+ if (parent?.signal.aborted) forwardParentAbort();
23971
+ else parent?.signal.addEventListener("abort", forwardParentAbort, { once: true });
23972
+ const timer = setTimeout(() => {
23973
+ controller.abort(new Error(`workflow step timed out after ${timeoutSeconds}s`));
23974
+ }, timeoutSeconds * 1e3);
23975
+ timer.unref?.();
23976
+ return {
23977
+ controller,
23978
+ cleanup: () => {
23979
+ clearTimeout(timer);
23980
+ parent?.signal.removeEventListener("abort", forwardParentAbort);
23981
+ }
23982
+ };
23983
+ }
23903
23984
  function workflowOutcome(result) {
23904
23985
  return result.taskState?.core.lastOutcome ?? null;
23905
23986
  }
@@ -456,6 +456,8 @@ export interface Context {
456
456
  /** Stream-output verbosity. */
457
457
  verbose?: boolean
458
458
  quiet?: boolean
459
+ /** Cancellation owned by the enclosing job or workflow step. */
460
+ abortSignal?: AbortSignal
459
461
  /** Opaque bag scripts populate during preflight (issue, pr, diff, logs, …). */
460
462
  data: Record<string, unknown>
461
463
  /** Final output the executor returns. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.565",
3
+ "version": "0.4.567",
4
4
  "description": "kody — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative implementation profiles.",
5
5
  "license": "MIT",
6
6
  "type": "module",