@kody-ade/kody-engine 0.4.642 → 0.4.644

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.642",
18
+ version: "0.4.644",
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
  repository: {
@@ -2334,7 +2334,7 @@ function parseCapabilityContract(raw) {
2334
2334
  if (parsed.execution !== void 0 && parsed.execution !== "agent" && parsed.execution !== "script") {
2335
2335
  throw new Error('contract.json execution must be "agent" or "script"');
2336
2336
  }
2337
- const requirements = parseCapabilityRequirements(parsed.requirements);
2337
+ const requirements = parseCapabilityRequirements(parsed.requirements, parsed.execution);
2338
2338
  const secrets = parsed.secrets === void 0 ? void 0 : Array.isArray(parsed.secrets) && parsed.secrets.every((name) => typeof name === "string" && /^[A-Z][A-Z0-9_]*$/.test(name)) ? [...new Set(parsed.secrets)] : null;
2339
2339
  if (secrets === null) {
2340
2340
  throw new Error("contract.json secrets must contain valid environment variable names");
@@ -2342,6 +2342,13 @@ function parseCapabilityContract(raw) {
2342
2342
  if (secrets && parsed.execution !== "script") {
2343
2343
  throw new Error('contract.json secrets are supported only when execution is "script"');
2344
2344
  }
2345
+ const connections = parsed.connections === void 0 ? void 0 : Array.isArray(parsed.connections) && parsed.connections.length > 0 && parsed.connections.every((id) => typeof id === "string" && /^[a-z0-9][a-z0-9_-]{0,79}$/.test(id)) ? [...new Set(parsed.connections)] : null;
2346
+ if (connections === null) {
2347
+ throw new Error("contract.json connections must contain valid Connection ids");
2348
+ }
2349
+ if (connections && parsed.execution !== "script") {
2350
+ throw new Error('contract.json connections are supported only when execution is "script"');
2351
+ }
2345
2352
  const timeoutMs = parsed.timeoutMs === void 0 ? void 0 : typeof parsed.timeoutMs === "number" && Number.isInteger(parsed.timeoutMs) && parsed.timeoutMs >= 1e3 && parsed.timeoutMs <= 6 * 60 * 60 * 1e3 ? parsed.timeoutMs : null;
2346
2353
  if (timeoutMs === null) {
2347
2354
  throw new Error("contract.json timeoutMs must be an integer from 1000 to 21600000");
@@ -2362,7 +2369,7 @@ function parseCapabilityContract(raw) {
2362
2369
  throw new Error("contract.json deliveryConfigAllowlist files must also be deliveryPathAllowlist entries");
2363
2370
  }
2364
2371
  const unsupported = Object.keys(parsed).filter(
2365
- (key) => key !== "execution" && key !== "deliveryPolicy" && key !== "deliveryPathAllowlist" && key !== "deliveryConfigAllowlist" && key !== "requirements" && key !== "secrets" && key !== "timeoutMs" && key !== "requiredSubagents" && key !== "input" && key !== "output"
2372
+ (key) => key !== "execution" && key !== "deliveryPolicy" && key !== "deliveryPathAllowlist" && key !== "deliveryConfigAllowlist" && key !== "requirements" && key !== "connections" && key !== "secrets" && key !== "timeoutMs" && key !== "requiredSubagents" && key !== "input" && key !== "output"
2366
2373
  );
2367
2374
  if (unsupported.length > 0) {
2368
2375
  throw new Error(`contract.json contains unsupported fields: ${unsupported.join(", ")}`);
@@ -2379,6 +2386,7 @@ function parseCapabilityContract(raw) {
2379
2386
  ...deliveryPathAllowlist ? { deliveryPathAllowlist } : {},
2380
2387
  ...deliveryConfigAllowlist ? { deliveryConfigAllowlist } : {},
2381
2388
  ...requirements ? { requirements } : {},
2389
+ ...connections ? { connections } : {},
2382
2390
  ...secrets ? { secrets } : {},
2383
2391
  ...timeoutMs !== void 0 ? { timeoutMs } : {},
2384
2392
  ...requiredSubagents ? { requiredSubagents } : {},
@@ -2425,11 +2433,11 @@ function parseDeliveryPathAllowlist(raw) {
2425
2433
  }
2426
2434
  return paths;
2427
2435
  }
2428
- function parseCapabilityRequirements(raw) {
2436
+ function parseCapabilityRequirements(raw, execution) {
2429
2437
  if (raw === void 0) return void 0;
2430
2438
  if (!isPlainObject(raw)) throw new Error("contract.json requirements must be an object");
2431
2439
  const unsupported = Object.keys(raw).filter(
2432
- (key) => key !== "cms" && key !== "browser" && key !== "qaCredentials" && key !== "githubTestToken" && key !== "qaAccountCredentials" && key !== "qaAccountModelSettings" && key !== "browserOnly"
2440
+ (key) => key !== "cms" && key !== "browser" && key !== "qaCredentials" && key !== "githubTestToken" && key !== "qaAccountCredentials" && key !== "qaAccountModelSettings" && key !== "browserOnly" && key !== "browserSession" && key !== "browserActions" && key !== "browserOrigins" && key !== "browserFileRoots"
2433
2441
  );
2434
2442
  if (unsupported.length > 0) {
2435
2443
  throw new Error(`contract.json requirements contains unsupported fields: ${unsupported.join(", ")}`);
@@ -2455,8 +2463,30 @@ function parseCapabilityRequirements(raw) {
2455
2463
  if (raw.browserOnly !== void 0 && typeof raw.browserOnly !== "boolean") {
2456
2464
  throw new Error("contract.json requirements.browserOnly must be boolean");
2457
2465
  }
2458
- if ((raw.qaCredentials === true || raw.githubTestToken === true || raw.qaAccountCredentials !== void 0 || raw.qaAccountModelSettings !== void 0 || raw.browserOnly === true) && raw.browser !== true) {
2459
- throw new Error("contract.json authentication requirements require browser");
2466
+ if (raw.browserSession !== void 0 && raw.browserSession !== "user") {
2467
+ throw new Error('contract.json requirements.browserSession must be "user"');
2468
+ }
2469
+ const browserActions = parseUserBrowserActions(raw.browserActions);
2470
+ const browserOrigins = parseUserBrowserOrigins(raw.browserOrigins);
2471
+ const browserFileRoots = parseUserBrowserFileRoots(raw.browserFileRoots);
2472
+ if ((raw.qaCredentials === true || raw.githubTestToken === true || raw.qaAccountCredentials !== void 0 || raw.qaAccountModelSettings !== void 0 || raw.browserOnly === true || raw.browserSession === "user") && raw.browser !== true) {
2473
+ throw new Error("contract.json protected browser requirement requires browser");
2474
+ }
2475
+ if (raw.browserSession === "user") {
2476
+ if (execution !== "agent") {
2477
+ throw new Error('contract.json requirements.browserSession "user" is supported only when execution is "agent"');
2478
+ }
2479
+ if (!browserActions?.length) {
2480
+ throw new Error("contract.json user browser requirements need browserActions");
2481
+ }
2482
+ if (!browserOrigins?.length) {
2483
+ throw new Error("contract.json user browser requirements need browserOrigins");
2484
+ }
2485
+ if (browserActions.includes("upload") && !browserFileRoots?.length) {
2486
+ throw new Error("contract.json browser upload requires browserFileRoots");
2487
+ }
2488
+ } else if (browserActions !== void 0 || browserOrigins !== void 0 || browserFileRoots !== void 0) {
2489
+ throw new Error('contract.json browserActions, browserOrigins, and browserFileRoots require browserSession "user"');
2460
2490
  }
2461
2491
  const requirements = {
2462
2492
  ...raw.cms === true ? { cms: true } : {},
@@ -2465,10 +2495,58 @@ function parseCapabilityRequirements(raw) {
2465
2495
  ...raw.githubTestToken === true ? { githubTestToken: true } : {},
2466
2496
  ...Array.isArray(raw.qaAccountCredentials) ? { qaAccountCredentials: [...new Set(raw.qaAccountCredentials)] } : {},
2467
2497
  ...isPlainObject(raw.qaAccountModelSettings) ? { qaAccountModelSettings: raw.qaAccountModelSettings } : {},
2468
- ...raw.browserOnly === true ? { browserOnly: true } : {}
2498
+ ...raw.browserOnly === true ? { browserOnly: true } : {},
2499
+ ...raw.browserSession === "user" ? { browserSession: "user" } : {},
2500
+ ...browserActions ? { browserActions } : {},
2501
+ ...browserOrigins ? { browserOrigins } : {},
2502
+ ...browserFileRoots ? { browserFileRoots } : {}
2469
2503
  };
2470
2504
  return Object.keys(requirements).length > 0 ? requirements : void 0;
2471
2505
  }
2506
+ function parseUserBrowserActions(value) {
2507
+ if (value === void 0) return void 0;
2508
+ if (!Array.isArray(value) || value.length === 0 || value.length > USER_BROWSER_ACTIONS.length || !value.every(
2509
+ (action) => typeof action === "string" && USER_BROWSER_ACTIONS.includes(action)
2510
+ )) {
2511
+ throw new Error(`contract.json requirements.browserActions must contain only ${USER_BROWSER_ACTIONS.join(", ")}`);
2512
+ }
2513
+ return [...new Set(value)];
2514
+ }
2515
+ function parseUserBrowserOrigins(value) {
2516
+ if (value === void 0) return void 0;
2517
+ if (!Array.isArray(value) || value.length === 0 || value.length > 20) {
2518
+ throw new Error("contract.json requirements.browserOrigins must be a non-empty array");
2519
+ }
2520
+ const origins = value.map((raw) => {
2521
+ if (typeof raw !== "string") throw new Error("invalid browser origin");
2522
+ let parsed;
2523
+ try {
2524
+ parsed = new URL(raw);
2525
+ } catch {
2526
+ throw new Error("invalid browser origin");
2527
+ }
2528
+ if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.origin !== raw.replace(/\/$/, "")) {
2529
+ throw new Error("contract.json requirements.browserOrigins must contain HTTPS origins only");
2530
+ }
2531
+ return parsed.origin;
2532
+ });
2533
+ return [...new Set(origins)];
2534
+ }
2535
+ function parseUserBrowserFileRoots(value) {
2536
+ if (value === void 0) return void 0;
2537
+ if (!Array.isArray(value) || value.length === 0 || value.length > 20) {
2538
+ throw new Error("contract.json requirements.browserFileRoots must be a non-empty array");
2539
+ }
2540
+ const roots = value.map((raw) => {
2541
+ if (typeof raw !== "string") throw new Error("invalid browser file root");
2542
+ const root = raw.replaceAll("\\", "/").replace(/^\/+|\/+$/g, "");
2543
+ if (!root || root.length > 300 || root.split("/").some((part) => !part || part === "." || part === "..")) {
2544
+ throw new Error("contract.json requirements.browserFileRoots contains an unsafe path");
2545
+ }
2546
+ return root;
2547
+ });
2548
+ return [...new Set(roots)];
2549
+ }
2472
2550
  function isRegularFile(filePath) {
2473
2551
  try {
2474
2552
  const stat = fs8.lstatSync(filePath);
@@ -2545,6 +2623,7 @@ function parseWorkflowStep(value) {
2545
2623
  const target = stringField(raw.target);
2546
2624
  const delivery = stringField(raw.delivery);
2547
2625
  const targetFact = stringField(raw.targetFact ?? raw.target_fact);
2626
+ const approval = stringField(raw.approval);
2548
2627
  const timeoutSeconds = typeof raw.timeoutSeconds === "number" && Number.isInteger(raw.timeoutSeconds) && raw.timeoutSeconds > 0 && raw.timeoutSeconds <= 3600 ? raw.timeoutSeconds : void 0;
2549
2628
  const hasInput = Object.hasOwn(raw, "input");
2550
2629
  const inputs = parseWorkflowInputBindings(raw.inputs);
@@ -2562,6 +2641,7 @@ function parseWorkflowStep(value) {
2562
2641
  ...targetFact ? { targetFact } : {},
2563
2642
  ...reason ? { reason } : {},
2564
2643
  ...timeoutSeconds ? { timeoutSeconds } : {},
2644
+ ...approval === "required" ? { approval: "required" } : {},
2565
2645
  ...next ? { next } : {},
2566
2646
  ...isPlainObject(raw.runWhen) ? { runWhen: raw.runWhen } : {},
2567
2647
  ...stringList(raw.continueOn ?? raw.continue_on).length > 0 ? { continueOn: stringList(raw.continueOn ?? raw.continue_on) } : {},
@@ -2636,7 +2716,7 @@ function isSafeSlug(value) {
2636
2716
  function isSafeStepId(value) {
2637
2717
  return /^[A-Za-z][A-Za-z0-9_-]*$/.test(value) && !value.includes("..");
2638
2718
  }
2639
- var CAPABILITY_BODY_FILE, CAPABILITY_CONTRACT_FILE, CAPABILITY_PROFILE_FILE, CANONICAL_CAPABILITY_BODY_FILE, CANONICAL_CAPABILITY_DEFINITION_FILE;
2719
+ var CAPABILITY_BODY_FILE, CAPABILITY_CONTRACT_FILE, CAPABILITY_PROFILE_FILE, CANONICAL_CAPABILITY_BODY_FILE, CANONICAL_CAPABILITY_DEFINITION_FILE, USER_BROWSER_ACTIONS;
2640
2720
  var init_capabilityFolders = __esm({
2641
2721
  "src/capabilityFolders.ts"() {
2642
2722
  "use strict";
@@ -2645,6 +2725,7 @@ var init_capabilityFolders = __esm({
2645
2725
  CAPABILITY_PROFILE_FILE = CAPABILITY_BODY_FILE;
2646
2726
  CANONICAL_CAPABILITY_BODY_FILE = "capability.md";
2647
2727
  CANONICAL_CAPABILITY_DEFINITION_FILE = "definition.json";
2728
+ USER_BROWSER_ACTIONS = ["navigate", "click", "fill", "upload", "scroll", "wait"];
2648
2729
  }
2649
2730
  });
2650
2731
 
@@ -3165,6 +3246,19 @@ async function readRuntimeSecretFromKody(name, env = process.env) {
3165
3246
  const body = await response.json();
3166
3247
  return typeof body.value === "string" ? body.value : null;
3167
3248
  }
3249
+ async function readRuntimeConnectionFromKody(id, env = process.env) {
3250
+ const token = await githubOidcToken(env);
3251
+ const response = await fetch(`${resolveKodyApiUrl(env)}/api/kody/engine/connection`, {
3252
+ method: "POST",
3253
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
3254
+ body: JSON.stringify({ id }),
3255
+ signal: AbortSignal.timeout(15e3)
3256
+ });
3257
+ if (response.status === 404) return null;
3258
+ if (!response.ok) throw new Error(`Kody Connection request failed (${response.status})`);
3259
+ const body = await response.json();
3260
+ return body.connection ?? null;
3261
+ }
3168
3262
  async function writeRuntimeSecretsToKody(secrets, env = process.env) {
3169
3263
  const token = await githubOidcToken(env);
3170
3264
  const response = await fetch(`${resolveKodyApiUrl(env)}/api/kody/engine/secret`, {
@@ -4108,7 +4202,7 @@ function capabilityToolDefinitions(opts) {
4108
4202
  description: "Idempotently create, update, close, or reopen one canonical repository Todo for a recurring problem. The Todo family is derived from reportSlug and the stable item id prevents duplicates. Repeating the same state is a no-op; unrelated items in an existing Todo are preserved.",
4109
4203
  inputSchema: {
4110
4204
  slug: z3.string().regex(/^[a-z0-9][a-z0-9_-]{0,63}$/).optional().describe("Deprecated; the canonical Todo slug is reportSlug."),
4111
- itemId: z3.string().regex(/^[a-z0-9][a-z0-9_-]{0,79}$/).optional(),
4205
+ itemId: z3.string().regex(/^[a-z0-9][a-z0-9_-]{0,79}$/),
4112
4206
  title: z3.string().min(1).max(160),
4113
4207
  description: z3.string().max(2e4).optional(),
4114
4208
  status: z3.enum(["open", "resolved"]),
@@ -4119,7 +4213,7 @@ function capabilityToolDefinitions(opts) {
4119
4213
  handler: async (args) => {
4120
4214
  const reportSlug = String(args.reportSlug);
4121
4215
  const slug = reportSlug;
4122
- const itemId = typeof args.itemId === "string" ? args.itemId : "finding";
4216
+ const itemId = String(args.itemId);
4123
4217
  const title = String(args.title).trim();
4124
4218
  const description = typeof args.description === "string" ? args.description.trim() : "";
4125
4219
  const status = args.status === "resolved" ? "resolved" : "open";
@@ -4136,10 +4230,17 @@ function capabilityToolDefinitions(opts) {
4136
4230
  const currentItems = Array.isArray(current.items) ? current.items.filter(
4137
4231
  (item) => Boolean(item && typeof item === "object" && !Array.isArray(item))
4138
4232
  ) : [];
4139
- const previous = currentItems.find((item) => item.id === itemId);
4233
+ const isLegacyFallbackForReport = (item) => {
4234
+ if (itemId === "finding" || item.id !== "finding") return false;
4235
+ const meta = item.meta && typeof item.meta === "object" && !Array.isArray(item.meta) ? item.meta : {};
4236
+ return meta.reportSlug === reportSlug;
4237
+ };
4238
+ const removedLegacyFallback = currentItems.some(isLegacyFallbackForReport);
4239
+ const canonicalItems = currentItems.filter((item) => !isLegacyFallbackForReport(item));
4240
+ const previous = canonicalItems.find((item) => item.id === itemId);
4140
4241
  const previousMeta = previous?.meta && typeof previous.meta === "object" && !Array.isArray(previous.meta) ? previous.meta : {};
4141
4242
  const unchanged = Boolean(
4142
- previous && previous.completed === completed && previous.title === title && String(previous.body ?? "") === evidence && previousMeta.reportSlug === reportSlug
4243
+ previous && !removedLegacyFallback && previous.completed === completed && previous.title === title && String(previous.body ?? "") === evidence && previousMeta.reportSlug === reportSlug
4143
4244
  );
4144
4245
  if (unchanged) {
4145
4246
  return {
@@ -4162,7 +4263,7 @@ function capabilityToolDefinitions(opts) {
4162
4263
  status
4163
4264
  }
4164
4265
  };
4165
- const items = previous ? currentItems.map((item) => item.id === itemId ? nextItem : item) : [...currentItems, nextItem];
4266
+ const items = previous ? canonicalItems.map((item) => item.id === itemId ? nextItem : item) : [...canonicalItems, nextItem];
4166
4267
  const doc = {
4167
4268
  ...current,
4168
4269
  version: 1,
@@ -5264,6 +5365,17 @@ function validateWorkflow(value, options = {}) {
5264
5365
  "workflow step timeoutSeconds must be an integer from 1 to 3600"
5265
5366
  );
5266
5367
  }
5368
+ if (step.approval !== void 0 && step.approval !== "required") {
5369
+ issue(issues, "invalid_step_approval", `${base}.approval`, 'workflow step approval must be "required"');
5370
+ }
5371
+ if (step.approval === "required" && !text(step.id)) {
5372
+ issue(
5373
+ issues,
5374
+ "approval_requires_step_id",
5375
+ `${base}.approval`,
5376
+ "an approval-gated workflow step must have a stable id"
5377
+ );
5378
+ }
5267
5379
  validateInputBindings(
5268
5380
  step.inputs,
5269
5381
  `${base}.inputs`,
@@ -5557,6 +5669,7 @@ var init_workflowValidation = __esm({
5557
5669
  "targetFact",
5558
5670
  "reason",
5559
5671
  "timeoutSeconds",
5672
+ "approval",
5560
5673
  "next",
5561
5674
  "runWhen",
5562
5675
  "continueOn",
@@ -17182,6 +17295,7 @@ var init_loadSimpleCapability = __esm({
17182
17295
  if (requiredSubagents.length > 0) ctx.data.requiredSubagents = requiredSubagents;
17183
17296
  if (capability.contract?.execution === "script") {
17184
17297
  ctx.data.capabilityScriptPath = path43.join(capability.dir, "tools", "run.sh");
17298
+ ctx.data.capabilityConnectionIds = capability.contract.connections ?? [];
17185
17299
  ctx.data.capabilitySecretNames = capability.contract.secrets ?? [];
17186
17300
  ctx.data.capabilityScriptTimeoutMs = capability.contract.timeoutMs;
17187
17301
  }
@@ -19454,6 +19568,9 @@ var init_prepareSimpleCapabilityRuntime = __esm({
19454
19568
  prepareSimpleCapabilityRuntime = async (ctx, profile) => {
19455
19569
  const requirements = requirementsFrom(ctx);
19456
19570
  if (!requirements.browser) return;
19571
+ if (requirements.browserSession === "user") {
19572
+ throw new Error("Capability requires the Dashboard user browser session and cannot run in CI");
19573
+ }
19457
19574
  configureBrowser(ctx, profile, requirements);
19458
19575
  if (requirements.qaCredentials) {
19459
19576
  await loadQaContext(ctx, profile);
@@ -21223,6 +21340,33 @@ var init_runScheduledImplementationTick = __esm({
21223
21340
  }
21224
21341
  });
21225
21342
 
21343
+ // src/scripts/runtimeConnections.ts
21344
+ async function resolveRuntimeConnections(ids, declaredSecrets, load = readRuntimeConnectionFromKody) {
21345
+ const requested = Array.isArray(ids) ? [...new Set(ids.filter((id) => typeof id === "string" && /^[a-z0-9][a-z0-9-]{0,63}$/.test(id)))] : [];
21346
+ const allowedSecrets = new Set(
21347
+ Array.isArray(declaredSecrets) ? declaredSecrets.filter((name) => typeof name === "string") : []
21348
+ );
21349
+ const connections = [];
21350
+ for (const id of requested) {
21351
+ const connection = await load(id);
21352
+ if (!connection) throw new Error(`Connection ${id} was not found`);
21353
+ if (connection.status !== "connected") throw new Error(`Connection ${id} is not connected`);
21354
+ for (const secretName of Object.values(connection.credentialRefs)) {
21355
+ if (!allowedSecrets.has(secretName)) {
21356
+ throw new Error(`Connection ${id} credential ${secretName} is not allowlisted by the Capability`);
21357
+ }
21358
+ }
21359
+ connections.push(connection);
21360
+ }
21361
+ return connections;
21362
+ }
21363
+ var init_runtimeConnections = __esm({
21364
+ "src/scripts/runtimeConnections.ts"() {
21365
+ "use strict";
21366
+ init_kody_api_client();
21367
+ }
21368
+ });
21369
+
21226
21370
  // src/scripts/runSimpleCapabilityScript.ts
21227
21371
  import { spawnSync as spawnSync3 } from "child_process";
21228
21372
  import * as fs51 from "fs";
@@ -21245,6 +21389,7 @@ var init_runSimpleCapabilityScript = __esm({
21245
21389
  "src/scripts/runSimpleCapabilityScript.ts"() {
21246
21390
  "use strict";
21247
21391
  init_capabilityResult();
21392
+ init_runtimeConnections();
21248
21393
  init_runtimeSecrets();
21249
21394
  init_tickShellRunner();
21250
21395
  DEFAULT_SCRIPT_TIMEOUT_MS = 5 * 60 * 1e3;
@@ -21258,6 +21403,14 @@ var init_runSimpleCapabilityScript = __esm({
21258
21403
  return;
21259
21404
  }
21260
21405
  const capabilityEnvironment = isStringRecord(ctx.data.capabilityEnvironment) ? ctx.data.capabilityEnvironment : {};
21406
+ let connections;
21407
+ try {
21408
+ connections = await resolveRuntimeConnections(ctx.data.capabilityConnectionIds, ctx.data.capabilitySecretNames);
21409
+ } catch (error) {
21410
+ ctx.output.exitCode = 78;
21411
+ ctx.output.reason = error instanceof Error ? error.message : "Capability Connection loading failed";
21412
+ return;
21413
+ }
21261
21414
  const capabilitySecrets = await resolveRuntimeSecrets(ctx.data.capabilitySecretNames, ctx);
21262
21415
  for (const warning of capabilitySecrets.warnings) {
21263
21416
  process.stderr.write(`\u2192 kody: WARNING ${warning}
@@ -21269,7 +21422,11 @@ var init_runSimpleCapabilityScript = __esm({
21269
21422
  env: {
21270
21423
  ...buildTickChildEnv(process.env, false),
21271
21424
  ...capabilitySecrets.environment,
21272
- ...capabilityEnvironment
21425
+ ...capabilityEnvironment,
21426
+ ...connections.length > 0 ? {
21427
+ KODY_CONNECTIONS_JSON: JSON.stringify(connections),
21428
+ ...connections.length === 1 ? { KODY_CONNECTION_JSON: JSON.stringify(connections[0]) } : {}
21429
+ } : {}
21273
21430
  },
21274
21431
  stdio: ["ignore", "pipe", "pipe"],
21275
21432
  encoding: "utf-8",
@@ -24272,7 +24429,7 @@ function workflowRunStatePath(workflowId, runId) {
24272
24429
  function parseWorkflowRunState(raw) {
24273
24430
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
24274
24431
  const state = raw;
24275
- if (state.status !== "running" && state.status !== "blocked" && state.status !== "failed" && state.status !== "done")
24432
+ if (state.status !== "running" && state.status !== "waiting-approval" && state.status !== "blocked" && state.status !== "failed" && state.status !== "done")
24276
24433
  return null;
24277
24434
  const completedStepIds = Array.isArray(state.completedStepIds) ? state.completedStepIds.filter((value) => typeof value === "string") : [];
24278
24435
  const transitionCounts = state.transitionCounts && typeof state.transitionCounts === "object" && !Array.isArray(state.transitionCounts) ? Object.fromEntries(
@@ -24292,6 +24449,7 @@ function parseWorkflowRunState(raw) {
24292
24449
  ...typeof state.currentStepId === "string" ? { currentStepId: state.currentStepId } : {},
24293
24450
  completedStepIds,
24294
24451
  transitionCounts,
24452
+ ...parseWorkflowApproval(state.approval) ? { approval: parseWorkflowApproval(state.approval) } : {},
24295
24453
  ...input ? { input: { ...input } } : {},
24296
24454
  ...typeof state.definitionHash === "string" && state.definitionHash.trim() ? { definitionHash: state.definitionHash.trim() } : {},
24297
24455
  ...steps ? { steps } : {},
@@ -24301,6 +24459,20 @@ function parseWorkflowRunState(raw) {
24301
24459
  ...typeof state.blocker === "string" ? { blocker: state.blocker } : {}
24302
24460
  };
24303
24461
  }
24462
+ function parseWorkflowApproval(value) {
24463
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
24464
+ const approval = value;
24465
+ if (typeof approval.stepId !== "string" || typeof approval.action !== "string" || typeof approval.contextHash !== "string" || approval.status !== "pending" && approval.status !== "approved" && approval.status !== "consumed")
24466
+ return void 0;
24467
+ return {
24468
+ stepId: approval.stepId,
24469
+ action: approval.action,
24470
+ contextHash: approval.contextHash,
24471
+ status: approval.status,
24472
+ ...typeof approval.approvedAt === "string" ? { approvedAt: approval.approvedAt } : {},
24473
+ ...typeof approval.approvedBy === "string" ? { approvedBy: approval.approvedBy } : {}
24474
+ };
24475
+ }
24304
24476
  function parseWorkflowSteps(value) {
24305
24477
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
24306
24478
  const steps = {};
@@ -24351,6 +24523,50 @@ var init_workflowRunState = __esm({
24351
24523
  }
24352
24524
  });
24353
24525
 
24526
+ // src/workflowStepApproval.ts
24527
+ import { createHash as createHash9 } from "crypto";
24528
+ function workflowStepApprovalContextHash(state, stepId) {
24529
+ const value = stableJson2({
24530
+ stepId,
24531
+ definitionHash: state.definitionHash ?? null,
24532
+ input: state.input ?? {},
24533
+ facts: state.facts ?? {}
24534
+ });
24535
+ return `sha256:${createHash9("sha256").update(value).digest("hex")}`;
24536
+ }
24537
+ function requireWorkflowStepApproval(state, stepId) {
24538
+ const contextHash = workflowStepApprovalContextHash(state, stepId);
24539
+ if (state.approval?.stepId === stepId && state.approval.contextHash === contextHash && state.approval.status === "approved") {
24540
+ return {
24541
+ ...state,
24542
+ status: "running",
24543
+ approval: { ...state.approval, status: "consumed" }
24544
+ };
24545
+ }
24546
+ return {
24547
+ ...state,
24548
+ status: "waiting-approval",
24549
+ approval: {
24550
+ stepId,
24551
+ action: `workflow-step:${stepId}`,
24552
+ contextHash,
24553
+ status: "pending"
24554
+ }
24555
+ };
24556
+ }
24557
+ function stableJson2(value) {
24558
+ if (Array.isArray(value)) return `[${value.map(stableJson2).join(",")}]`;
24559
+ if (value && typeof value === "object") {
24560
+ return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableJson2(item)}`).join(",")}}`;
24561
+ }
24562
+ return JSON.stringify(value) ?? "undefined";
24563
+ }
24564
+ var init_workflowStepApproval = __esm({
24565
+ "src/workflowStepApproval.ts"() {
24566
+ "use strict";
24567
+ }
24568
+ });
24569
+
24354
24570
  // src/job.ts
24355
24571
  var job_exports = {};
24356
24572
  __export(job_exports, {
@@ -24521,7 +24737,7 @@ async function runJob(job, base) {
24521
24737
  if (base.config && persistRun) {
24522
24738
  await upsertRunIndexRowBestEffortAsync(base.config, base.cwd, {
24523
24739
  ...parentRow,
24524
- status: result.exitCode === 0 ? "success" : "failed",
24740
+ status: result.workflowState?.status === "waiting-approval" ? "waiting" : result.exitCode === 0 ? "success" : "failed",
24525
24741
  summary: result.reason,
24526
24742
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
24527
24743
  });
@@ -24530,7 +24746,7 @@ async function runJob(job, base) {
24530
24746
  await lease?.checkpoint();
24531
24747
  await writeWorkflowRunState(base.config, base.cwd, workflowIdentity, valid.workflowRunId, result.workflowState);
24532
24748
  }
24533
- if (valid.workflowRunId && workflowIdentity && hasGitHubActionsIdentity()) {
24749
+ if (valid.workflowRunId && workflowIdentity && hasGitHubActionsIdentity() && result.workflowState?.status !== "waiting-approval") {
24534
24750
  const facts = result.workflowState?.facts ?? {};
24535
24751
  await notifyWorkflowCompleted({
24536
24752
  workflowId: workflowIdentity,
@@ -24822,6 +25038,7 @@ function initialWorkflowState(parent, workflow) {
24822
25038
  status: "done",
24823
25039
  completedStepIds: [...prior.completedStepIds],
24824
25040
  transitionCounts: { ...prior.transitionCounts },
25041
+ ...prior.approval ? { approval: { ...prior.approval } } : {},
24825
25042
  ...prior.input ? { input: { ...prior.input } } : {},
24826
25043
  ...prior.steps ? { steps: cloneWorkflowSteps(prior.steps) } : {},
24827
25044
  facts: { ...prior.facts },
@@ -24838,6 +25055,7 @@ function initialWorkflowState(parent, workflow) {
24838
25055
  ...currentStepId ? { currentStepId } : {},
24839
25056
  completedStepIds: [...prior?.completedStepIds ?? []],
24840
25057
  transitionCounts: { ...prior?.transitionCounts ?? {} },
25058
+ ...prior?.approval ? { approval: { ...prior.approval } } : {},
24841
25059
  steps: cloneWorkflowSteps(prior?.steps ?? {}),
24842
25060
  facts: {
24843
25061
  ...workflowInputContext(parent.cliArgs),
@@ -24896,6 +25114,20 @@ async function runGraphCapabilityWorkflow(parent, workflow, capability, base, ch
24896
25114
  return { ...result, exitCode: 64, reason, workflowState: state };
24897
25115
  }
24898
25116
  const label = step.action ?? step.capability;
25117
+ if (step.approval === "required") {
25118
+ const approvalState = requireWorkflowStepApproval(state, step.id);
25119
+ state.status = approvalState.status;
25120
+ state.approval = approvalState.approval;
25121
+ await checkpoint?.(state);
25122
+ if (state.status === "waiting-approval") {
25123
+ return {
25124
+ ...result,
25125
+ exitCode: 0,
25126
+ reason: `Approval required before workflow step ${step.id}`,
25127
+ workflowState: state
25128
+ };
25129
+ }
25130
+ }
24899
25131
  await checkpoint?.(state);
24900
25132
  let child;
24901
25133
  try {
@@ -25111,7 +25343,8 @@ function workflowStepToJob(step, parent, chainData, cwd) {
25111
25343
  const action = step.action ?? step.capability;
25112
25344
  const targetNumber = workflowStepTargetNumber(step, parent, chainData);
25113
25345
  const mappedInputs = resolveWorkflowStepInputs(step, chainData, cwd);
25114
- const rawArgs = mappedInputs ? { ...mappedInputs } : { ...parent.cliArgs };
25346
+ const genericCapability = usesGenericCapabilityInput(action, cwd);
25347
+ const rawArgs = mappedInputs ? { ...mappedInputs } : genericCapability ? inheritedGenericStepInput(step.capability, parent.cliArgs, cwd) : { ...parent.cliArgs };
25115
25348
  if (step.target === "pr") {
25116
25349
  if (typeof targetNumber !== "number") {
25117
25350
  throw new InvalidJobError(`workflow step ${action} needs a PR target but no prior PR URL is available`);
@@ -25121,11 +25354,11 @@ function workflowStepToJob(step, parent, chainData, cwd) {
25121
25354
  rawArgs.issue = targetNumber;
25122
25355
  }
25123
25356
  const genericInput = capabilityStepInput(
25124
- step.input ?? mappedInputs ?? chainData.workflowInput ?? genericInputFromArgs(rawArgs),
25357
+ step.input ?? mappedInputs ?? genericInputFromArgs(rawArgs),
25125
25358
  step.target,
25126
25359
  targetNumber
25127
25360
  );
25128
- const cliArgs = usesGenericCapabilityInput(action, cwd) ? genericInput === void 0 ? {} : { input: JSON.stringify(genericInput) } : filterCliArgsForStep(action, rawArgs);
25361
+ const cliArgs = genericCapability ? genericInput === void 0 ? {} : { input: JSON.stringify(genericInput) } : filterCliArgsForStep(action, rawArgs);
25129
25362
  const target = typeof targetNumber === "number" ? targetNumber : typeof parent.target === "number" ? parent.target : targetFromCliArgs(cliArgs);
25130
25363
  return {
25131
25364
  action,
@@ -25176,6 +25409,17 @@ function capabilityInputNames(folder) {
25176
25409
  if (!properties || typeof properties !== "object" || Array.isArray(properties)) return /* @__PURE__ */ new Set();
25177
25410
  return new Set(Object.keys(properties));
25178
25411
  }
25412
+ function inheritedGenericStepInput(capability, parentArgs, cwd) {
25413
+ const input = workflowInputContext(parentArgs);
25414
+ const folder = resolveCapabilityFolder(capability, hydratedCapabilitiesRoot(cwd));
25415
+ if (!folder) return input;
25416
+ if (!folder.contractPath) return input;
25417
+ if (folder.config.inputSchema?.additionalProperties !== false) return input;
25418
+ const accepted = capabilityInputNames(folder);
25419
+ if (accepted.size === 0) return input;
25420
+ const routing = /* @__PURE__ */ new Set(["base"]);
25421
+ return Object.fromEntries(Object.entries(input).filter(([name]) => accepted.has(name) || routing.has(name)));
25422
+ }
25179
25423
  function cloneWorkflowSteps(steps) {
25180
25424
  return Object.fromEntries(Object.entries(steps).map(([id, step]) => [id, { ...step }]));
25181
25425
  }
@@ -25447,6 +25691,7 @@ var init_job = __esm({
25447
25691
  init_workflowDefinitions();
25448
25692
  init_workflowRunLease();
25449
25693
  init_workflowRunState();
25694
+ init_workflowStepApproval();
25450
25695
  init_workflowValidation();
25451
25696
  init_jobIdentity();
25452
25697
  init_jobIdentity();
@@ -29774,7 +30019,7 @@ import { createInterface as createInterface2 } from "readline";
29774
30019
 
29775
30020
  // src/terminal/brain-terminal-adapters.ts
29776
30021
  import { spawn as spawn9 } from "child_process";
29777
- import { createHash as createHash9, randomBytes as randomBytes2 } from "crypto";
30022
+ import { createHash as createHash10, randomBytes as randomBytes2 } from "crypto";
29778
30023
  import { mkdir, readFile, rename, writeFile as writeFile2 } from "fs/promises";
29779
30024
  import * as path57 from "path";
29780
30025
  function runTerminalCommand(command, args, input) {
@@ -29797,7 +30042,7 @@ function runTerminalCommand(command, args, input) {
29797
30042
  });
29798
30043
  }
29799
30044
  function storeKey(id) {
29800
- return createHash9("sha256").update(id).digest("hex");
30045
+ return createHash10("sha256").update(id).digest("hex");
29801
30046
  }
29802
30047
  function isStoredSession(value) {
29803
30048
  if (!value || typeof value !== "object") return false;
@@ -29902,7 +30147,7 @@ var TmuxBrainTerminalRuntime = class {
29902
30147
  };
29903
30148
 
29904
30149
  // src/terminal/brain-terminal-session.ts
29905
- import { createHash as createHash10 } from "crypto";
30150
+ import { createHash as createHash11 } from "crypto";
29906
30151
  var MAX_CAPTURE_CHARS = 2e5;
29907
30152
  function requiredIdentifier(value, name, max = 240) {
29908
30153
  if (typeof value !== "string" || !value.trim() || value.length > max) {
@@ -29983,7 +30228,7 @@ function parseBrainTerminalCommand(value) {
29983
30228
  }
29984
30229
  }
29985
30230
  function sessionName(id) {
29986
- return `kody_${createHash10("sha256").update(id).digest("hex").slice(0, 32)}`;
30231
+ return `kody_${createHash11("sha256").update(id).digest("hex").slice(0, 32)}`;
29987
30232
  }
29988
30233
  function stateEvent(session) {
29989
30234
  return {
@@ -600,7 +600,7 @@ export interface Job {
600
600
  }
601
601
 
602
602
  export interface WorkflowRunState {
603
- status: "running" | "blocked" | "failed" | "done"
603
+ status: "running" | "waiting-approval" | "blocked" | "failed" | "done"
604
604
  /** Immutable input supplied when this workflow run started. */
605
605
  input?: Record<string, unknown>
606
606
  /** Hash of the workflow definition used by this run. */
@@ -608,6 +608,14 @@ export interface WorkflowRunState {
608
608
  currentStepId?: string
609
609
  completedStepIds: string[]
610
610
  transitionCounts: Record<string, number>
611
+ approval?: {
612
+ stepId: string
613
+ action: string
614
+ contextHash: string
615
+ status: "pending" | "approved" | "consumed"
616
+ approvedAt?: string
617
+ approvedBy?: string
618
+ }
611
619
  /** Exact per-step handoffs for audit, resume, and debugging. */
612
620
  steps?: Record<
613
621
  string,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.642",
3
+ "version": "0.4.644",
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
  "repository": {