@kody-ade/kody-engine 0.4.471 → 0.4.473

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.471",
18
+ version: "0.4.473",
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",
@@ -53,7 +53,6 @@ var init_package = __esm({
53
53
  dependencies: {
54
54
  "@actions/cache": "^6.0.0",
55
55
  "@anthropic-ai/claude-agent-sdk": "0.2.119",
56
- "@kody-ade/agency-domain": "0.5.1",
57
56
  "@kody-ade/engine-contracts": "0.1.0",
58
57
  "@modelcontextprotocol/sdk": "^1.29.0",
59
58
  ajv: "^8.18.0",
@@ -2059,14 +2058,14 @@ function resolveImplementationCandidates(name, roots = getRuntimeProfileRootsFor
2059
2058
  function listCapabilityActions(projectCapabilitiesRoot = getProjectCapabilitiesRoot()) {
2060
2059
  const seen = /* @__PURE__ */ new Set();
2061
2060
  const out = [];
2062
- const add2 = (action) => {
2061
+ const add = (action) => {
2063
2062
  if (!isSafeName(action.action) || !isSafeName(action.capability) || !isSafeName(action.implementation)) return;
2064
2063
  if (seen.has(action.action)) return;
2065
2064
  seen.add(action.action);
2066
2065
  out.push(action);
2067
2066
  };
2068
- for (const action of listFolderCapabilityActions(projectCapabilitiesRoot, "project-folder")) add2(action);
2069
- for (const action of listBuiltinCapabilityActions(getBuiltinCapabilitiesRoot())) add2(action);
2067
+ for (const action of listFolderCapabilityActions(projectCapabilitiesRoot, "project-folder")) add(action);
2068
+ for (const action of listBuiltinCapabilityActions(getBuiltinCapabilitiesRoot())) add(action);
2070
2069
  return out.sort((a, b) => a.action.localeCompare(b.action));
2071
2070
  }
2072
2071
  function resolveCapabilityAction(action, projectCapabilitiesRoot = getProjectCapabilitiesRoot()) {
@@ -2317,7 +2316,7 @@ var init_convex_client = __esm({
2317
2316
 
2318
2317
  // src/kody-api-client.ts
2319
2318
  import { getFunctionName } from "convex/server";
2320
- function apiUrl(env) {
2319
+ function resolveKodyApiUrl(env) {
2321
2320
  return (env.KODY_API_URL?.trim() || env.KODY_DASHBOARD_URL?.trim() || env.DASHBOARD_URL?.trim() || DEFAULT_KODY_API_URL).replace(/\/$/, "");
2322
2321
  }
2323
2322
  function oidcRequestUrl(env) {
@@ -2356,7 +2355,7 @@ function operationName(fn) {
2356
2355
  async function callKodyApi(kind, fn, args, env) {
2357
2356
  const call = async (forceToken) => {
2358
2357
  const token = await githubOidcToken(env, forceToken);
2359
- return fetch(`${apiUrl(env)}/api/kody/engine/backend`, {
2358
+ return fetch(`${resolveKodyApiUrl(env)}/api/kody/engine/backend`, {
2360
2359
  method: "POST",
2361
2360
  headers: {
2362
2361
  Authorization: `Bearer ${token}`,
@@ -2386,7 +2385,7 @@ function createKodyApiBackendClient(env = process.env) {
2386
2385
  }
2387
2386
  async function readRuntimeSecretFromKody(name, env = process.env) {
2388
2387
  const token = await githubOidcToken(env);
2389
- const response = await fetch(`${apiUrl(env)}/api/kody/engine/secret`, {
2388
+ const response = await fetch(`${resolveKodyApiUrl(env)}/api/kody/engine/secret`, {
2390
2389
  method: "POST",
2391
2390
  headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
2392
2391
  body: JSON.stringify({ name }),
@@ -2399,7 +2398,7 @@ async function readRuntimeSecretFromKody(name, env = process.env) {
2399
2398
  }
2400
2399
  async function readPreviewContextFromKody(env = process.env) {
2401
2400
  const token = await githubOidcToken(env);
2402
- const response = await fetch(`${apiUrl(env)}/api/kody/engine/preview-context`, {
2401
+ const response = await fetch(`${resolveKodyApiUrl(env)}/api/kody/engine/preview-context`, {
2403
2402
  method: "POST",
2404
2403
  headers: { Authorization: `Bearer ${token}` },
2405
2404
  signal: AbortSignal.timeout(3e4)
@@ -2411,7 +2410,7 @@ var DEFAULT_KODY_API_URL, OIDC_AUDIENCE, cachedToken;
2411
2410
  var init_kody_api_client = __esm({
2412
2411
  "src/kody-api-client.ts"() {
2413
2412
  "use strict";
2414
- DEFAULT_KODY_API_URL = "https://dashboard-six-alpha-46.vercel.app";
2413
+ DEFAULT_KODY_API_URL = "https://kody-dashboard-aguy.vercel.app";
2415
2414
  OIDC_AUDIENCE = "kody-api";
2416
2415
  cachedToken = null;
2417
2416
  }
@@ -2419,12 +2418,11 @@ var init_kody_api_client = __esm({
2419
2418
 
2420
2419
  // src/state-backend.ts
2421
2420
  import { anyApi } from "convex/server";
2422
- function serializeAgencyDispatchDecision(decision) {
2421
+ function serializeLoopDispatchDecision(decision) {
2423
2422
  return {
2424
2423
  kind: decision.kind,
2425
2424
  reason: decision.reason,
2426
- ...decision.scheduledAt ? { scheduledAt: decision.scheduledAt } : {},
2427
- ...decision.nextEligibleAt ? { nextEligibleAt: decision.nextEligibleAt } : {}
2425
+ ...decision.scheduledAt ? { scheduledAt: decision.scheduledAt } : {}
2428
2426
  };
2429
2427
  }
2430
2428
  function requireTenant(tenantId2) {
@@ -2539,52 +2537,11 @@ function createStateBackendFromEnv(env = process.env, client) {
2539
2537
  updatedAt
2540
2538
  });
2541
2539
  },
2542
- async listAgencyDefinitions(tenantId2) {
2543
- const result = await transport.query(anyApi.agencyModel.listDefinitions, {
2544
- tenantId: requireTenant(tenantId2)
2545
- });
2546
- return Array.isArray(result) ? result : [];
2547
- },
2548
- async getAgencyState(tenantId2, kind, definitionId2) {
2549
- const result = await transport.query(anyApi.agencyModel.getState, {
2550
- tenantId: requireTenant(tenantId2),
2551
- kind,
2552
- definitionId: requireNonEmpty(definitionId2, "definitionId")
2553
- });
2554
- return result ?? null;
2555
- },
2556
- async putAgencyState(tenantId2, definitionId2, kind, schemaVersion, data, updatedAt) {
2557
- await transport.mutation(anyApi.agencyModel.putState, {
2558
- tenantId: requireTenant(tenantId2),
2559
- definitionId: requireNonEmpty(definitionId2, "definitionId"),
2560
- kind,
2561
- schemaVersion,
2562
- data,
2563
- updatedAt
2564
- });
2565
- },
2566
- async appendAgencyOutput(tenantId2, recordId, schemaVersion, data) {
2567
- await transport.mutation(anyApi.agencyModel.appendOutput, {
2568
- tenantId: requireTenant(tenantId2),
2569
- envelope: {
2570
- schemaVersion,
2571
- recordId: requireNonEmpty(recordId, "recordId"),
2572
- data
2573
- }
2574
- });
2575
- },
2576
- async listAgencyOutputs(tenantId2, runId) {
2577
- const result = await transport.query(anyApi.agencyModel.listOutputs, {
2578
- tenantId: requireTenant(tenantId2),
2579
- ...runId ? { runId: requireNonEmpty(runId, "runId") } : {}
2580
- });
2581
- return Array.isArray(result) ? result : [];
2582
- },
2583
- async reserveAgencyDispatch(tenantId2, reservation) {
2540
+ async reserveLoopDispatch(tenantId2, reservation) {
2584
2541
  const result = await transport.mutation(anyApi.agencyModel.reserveDispatch, {
2585
2542
  tenantId: requireTenant(tenantId2),
2586
2543
  ...reservation,
2587
- decision: serializeAgencyDispatchDecision(reservation.decision),
2544
+ decision: serializeLoopDispatchDecision(reservation.decision),
2588
2545
  idempotencyKey: requireNonEmpty(reservation.idempotencyKey, "idempotencyKey"),
2589
2546
  loopId: requireNonEmpty(reservation.loopId, "loopId"),
2590
2547
  reservationId: requireNonEmpty(reservation.reservationId, "reservationId"),
@@ -2593,16 +2550,7 @@ function createStateBackendFromEnv(env = process.env, client) {
2593
2550
  });
2594
2551
  return result;
2595
2552
  },
2596
- async recordSkippedAgencyDispatch(tenantId2, idempotencyKey, loopId, decision, now) {
2597
- await transport.mutation(anyApi.agencyModel.recordSkippedDispatch, {
2598
- tenantId: requireTenant(tenantId2),
2599
- idempotencyKey: requireNonEmpty(idempotencyKey, "idempotencyKey"),
2600
- loopId: requireNonEmpty(loopId, "loopId"),
2601
- decision: serializeAgencyDispatchDecision(decision),
2602
- now
2603
- });
2604
- },
2605
- async finishAgencyDispatch(tenantId2, idempotencyKey, reservationId, status, now, runId) {
2553
+ async finishLoopDispatch(tenantId2, idempotencyKey, reservationId, status, now, runId) {
2606
2554
  await transport.mutation(anyApi.agencyModel.finishDispatch, {
2607
2555
  tenantId: requireTenant(tenantId2),
2608
2556
  idempotencyKey: requireNonEmpty(idempotencyKey, "idempotencyKey"),
@@ -2612,7 +2560,7 @@ function createStateBackendFromEnv(env = process.env, client) {
2612
2560
  ...runId ? { runId: requireNonEmpty(runId, "runId") } : {}
2613
2561
  });
2614
2562
  },
2615
- async createAgencyModelRun(tenantId2, subjectType, subjectId, run, now) {
2563
+ async createAgencyRun(tenantId2, subjectType, subjectId, run, now) {
2616
2564
  await transport.mutation(anyApi.agencyModel.createRunRecord, {
2617
2565
  tenantId: requireTenant(tenantId2),
2618
2566
  subjectType,
@@ -2621,7 +2569,7 @@ function createStateBackendFromEnv(env = process.env, client) {
2621
2569
  now
2622
2570
  });
2623
2571
  },
2624
- async finishAgencyModelRun(tenantId2, run, now) {
2572
+ async finishAgencyRun(tenantId2, run, now) {
2625
2573
  await transport.mutation(anyApi.agencyModel.finishRunRecord, {
2626
2574
  tenantId: requireTenant(tenantId2),
2627
2575
  run,
@@ -13086,678 +13034,6 @@ var init_dispatch = __esm({
13086
13034
  }
13087
13035
  });
13088
13036
 
13089
- // src/goal/agencyModelRepository.ts
13090
- import {
13091
- createAgentDefinition,
13092
- createCapabilityDefinition,
13093
- createGoalDefinition,
13094
- createGoalState,
13095
- createIntentDefinition,
13096
- createLoopDefinition,
13097
- createLoopState,
13098
- createOperationDefinition,
13099
- createRunOutput,
13100
- createWorkflowDefinition,
13101
- relationshipIssues
13102
- } from "@kody-ade/agency-domain";
13103
- function goalProgressFromOutputs(definition, revision, outputs) {
13104
- const required2 = definition.objective.requiredEvidence;
13105
- if (required2.length === 0) return 1;
13106
- const satisfied = new Set(
13107
- outputs.filter(
13108
- (output) => output.kind === "evidence" && output.value === true && output.parentRef?.kind === "goal" && output.parentRef.id === definition.id && output.parentRef.revision === revision
13109
- ).map((output) => output.key)
13110
- );
13111
- return required2.filter((key) => satisfied.has(key)).length / required2.length;
13112
- }
13113
- function definitionId(document) {
13114
- const data = document.data;
13115
- if (typeof data.id !== "string" || !data.id.trim()) {
13116
- throw new Error(`Agency Definition is missing id: ${document.recordId}`);
13117
- }
13118
- return data.id;
13119
- }
13120
- function parseState(document, definition, kind) {
13121
- if (!document) return null;
13122
- if (document.schemaVersion !== 1) throw new Error(`Unsupported Agency State schema: ${document.schemaVersion}`);
13123
- if (document.kind !== kind || document.definitionId !== definition.id) {
13124
- throw new Error(`Agency State does not match Definition: ${definition.id}`);
13125
- }
13126
- return document.kind === "goal" ? createGoalState(document.data) : createLoopState(document.data);
13127
- }
13128
- function emptyCatalog() {
13129
- return {
13130
- intents: /* @__PURE__ */ new Map(),
13131
- operations: /* @__PURE__ */ new Map(),
13132
- goals: /* @__PURE__ */ new Map(),
13133
- loops: /* @__PURE__ */ new Map(),
13134
- workflows: /* @__PURE__ */ new Map(),
13135
- capabilities: /* @__PURE__ */ new Map(),
13136
- agents: /* @__PURE__ */ new Map()
13137
- };
13138
- }
13139
- function addDefinition(catalog, document) {
13140
- if (document.schemaVersion !== 1) throw new Error(`Unsupported Agency Definition schema: ${document.schemaVersion}`);
13141
- if (document.kind === "intent") add(catalog.intents, createIntentDefinition(document.data), document.recordId);
13142
- else if (document.kind === "operation")
13143
- add(catalog.operations, createOperationDefinition(document.data), document.recordId);
13144
- else if (document.kind === "goal") add(catalog.goals, createGoalDefinition(document.data), document.recordId);
13145
- else if (document.kind === "loop") add(catalog.loops, createLoopDefinition(document.data), document.recordId);
13146
- else if (document.kind === "workflow")
13147
- add(catalog.workflows, createWorkflowDefinition(document.data), document.recordId);
13148
- else if (document.kind === "capability")
13149
- add(catalog.capabilities, createCapabilityDefinition(document.data), document.recordId);
13150
- else add(catalog.agents, createAgentDefinition(document.data), document.recordId);
13151
- }
13152
- function add(collection, definition, revision) {
13153
- const mutable = collection;
13154
- mutable.set(definition.id, { definition, revision });
13155
- }
13156
- function validateRelationships(catalog) {
13157
- const relationshipCatalog = {
13158
- operations: [...catalog.operations.keys()],
13159
- goals: [...catalog.goals.keys()],
13160
- workflows: [...catalog.workflows.keys()],
13161
- capabilities: [...catalog.capabilities.keys()]
13162
- };
13163
- const issues = [...catalog.goals.values(), ...catalog.loops.values()].flatMap(
13164
- ({ definition }) => relationshipIssues(definition, relationshipCatalog).map((issue2) => `${definition.id}: ${issue2}`)
13165
- );
13166
- for (const { definition } of catalog.operations.values()) {
13167
- for (const intentId of definition.intentIds) {
13168
- if (!catalog.intents.has(intentId)) issues.push(`${definition.id}: Missing Intent "${intentId}"`);
13169
- }
13170
- }
13171
- for (const { definition } of catalog.workflows.values()) {
13172
- for (const step of definition.steps) {
13173
- if (!catalog.capabilities.has(step.capabilityRef.id)) {
13174
- issues.push(`${definition.id}: Missing Capability "${step.capabilityRef.id}"`);
13175
- }
13176
- }
13177
- }
13178
- if (issues.length > 0) throw new Error(`Invalid Agency relationships:
13179
- ${issues.join("\n")}`);
13180
- }
13181
- var AgencyModelRepository;
13182
- var init_agencyModelRepository = __esm({
13183
- "src/goal/agencyModelRepository.ts"() {
13184
- "use strict";
13185
- AgencyModelRepository = class {
13186
- constructor(backend, tenantId2) {
13187
- this.backend = backend;
13188
- this.tenantId = tenantId2;
13189
- }
13190
- backend;
13191
- tenantId;
13192
- async listManagedWork(catalog) {
13193
- const definitions = catalog ?? await this.loadCatalog();
13194
- const managed = [
13195
- ...Array.from(definitions.goals.values(), ({ definition, revision }) => ({
13196
- definition,
13197
- revision,
13198
- kind: "goal"
13199
- })),
13200
- ...Array.from(definitions.loops.values(), ({ definition, revision }) => ({
13201
- definition,
13202
- revision,
13203
- kind: "loop"
13204
- }))
13205
- ];
13206
- return Promise.all(
13207
- managed.map(async (record2) => ({
13208
- definition: record2.definition,
13209
- revision: record2.revision,
13210
- state: parseState(
13211
- await this.backend.getAgencyState(this.tenantId, record2.kind, record2.definition.id),
13212
- record2.definition,
13213
- record2.kind
13214
- )
13215
- }))
13216
- );
13217
- }
13218
- async loadCatalog() {
13219
- const documents = await this.backend.listAgencyDefinitions(this.tenantId);
13220
- const catalog = emptyCatalog();
13221
- const ordered = [...documents].sort(
13222
- (left, right) => left.createdAt.localeCompare(right.createdAt) || left.recordId.localeCompare(right.recordId)
13223
- );
13224
- const latest = /* @__PURE__ */ new Map();
13225
- for (const document of ordered) {
13226
- latest.set(`${document.kind}:${definitionId(document)}`, document);
13227
- }
13228
- for (const document of latest.values()) addDefinition(catalog, document);
13229
- validateRelationships(catalog);
13230
- return catalog;
13231
- }
13232
- async saveState(state, kind, updatedAt) {
13233
- const data = kind === "goal" ? createGoalState(state) : createLoopState(state);
13234
- await this.backend.putAgencyState(this.tenantId, state.definitionId, kind, 1, data, updatedAt);
13235
- }
13236
- async appendOutput(recordId, output) {
13237
- await this.backend.appendAgencyOutput(this.tenantId, recordId, 1, createRunOutput(output));
13238
- }
13239
- async listOutputs(runId) {
13240
- const documents = await this.backend.listAgencyOutputs(this.tenantId, runId);
13241
- return documents.map((document) => {
13242
- if (document.schemaVersion !== 1) {
13243
- throw new Error(`Unsupported Agency Output schema: ${document.schemaVersion}`);
13244
- }
13245
- const output = createRunOutput(document.data);
13246
- if (output.runId !== document.runId) throw new Error(`Agency Output does not match Run: ${document.recordId}`);
13247
- return output;
13248
- });
13249
- }
13250
- async refreshGoalProgress(record2, updatedAt) {
13251
- if (!("executionRef" in record2.definition)) throw new Error("Only a Goal has progress");
13252
- const previous = record2.state;
13253
- if (previous && !("progress" in previous)) throw new Error("Goal Definition has Loop State");
13254
- const state = createGoalState({
13255
- definitionId: record2.definition.id,
13256
- lifecycle: previous?.lifecycle ?? "draft",
13257
- progress: goalProgressFromOutputs(record2.definition, record2.revision, await this.listOutputs()),
13258
- blockers: previous?.blockers ?? [],
13259
- updatedAt
13260
- });
13261
- await this.saveState(state, "goal", updatedAt);
13262
- return state;
13263
- }
13264
- };
13265
- }
13266
- });
13267
-
13268
- // src/goal/policyResolver.ts
13269
- import { createHash as createHash5 } from "crypto";
13270
- function resolveDispatchPolicy(input) {
13271
- const operation = input.catalog.operations.get(input.owner.definition.operationId);
13272
- if (!operation) throw new Error(`Dispatch blocked: Operation "${input.owner.definition.operationId}" is unresolved`);
13273
- if (operation.definition.intentIds.length === 0) {
13274
- throw new Error(`Dispatch blocked: Operation "${operation.definition.id}" has no Intent`);
13275
- }
13276
- const intents = operation.definition.intentIds.map((intentId) => {
13277
- const intent = input.catalog.intents.get(intentId);
13278
- if (!intent) throw new Error(`Dispatch blocked: Intent "${intentId}" is unresolved`);
13279
- return intent;
13280
- });
13281
- const policy = mergePolicies(intents.map(({ definition }) => definition.policy));
13282
- const constraints = intents.flatMap(({ definition }) => definition.constraints);
13283
- const requiresApproval = assertAuthorized(policy, constraints, input.target);
13284
- const snapshotValue = { policy, constraints };
13285
- return {
13286
- snapshot: {
13287
- hash: createHash5("sha256").update(stableJson(snapshotValue)).digest("hex"),
13288
- ...snapshotValue
13289
- },
13290
- operation,
13291
- intents,
13292
- trace: [pinned("trigger" in input.owner.definition ? "loop" : "goal", input.owner), input.target],
13293
- requiresApproval
13294
- };
13295
- }
13296
- function mergePolicies(policies) {
13297
- const approvalOrder = ["none", "risky-actions", "all-actions"];
13298
- return {
13299
- approval: policies.reduce(
13300
- (strictest, policy) => approvalOrder.indexOf(policy.approval) > approvalOrder.indexOf(strictest) ? policy.approval : strictest,
13301
- "none"
13302
- ),
13303
- authority: {
13304
- allow: intersect(policies.map(({ authority }) => authority.allow)),
13305
- deny: unique(policies.flatMap(({ authority }) => authority.deny))
13306
- },
13307
- budget: {
13308
- maxRuns: Math.min(...policies.map(({ budget }) => budget.maxRuns)),
13309
- maxTokens: Math.min(...policies.map(({ budget }) => budget.maxTokens)),
13310
- maxCostUsd: Math.min(...policies.map(({ budget }) => budget.maxCostUsd)),
13311
- maxDurationSeconds: Math.min(...policies.map(({ budget }) => budget.maxDurationSeconds))
13312
- },
13313
- maxConcurrentRuns: Math.min(...policies.map(({ maxConcurrentRuns }) => maxConcurrentRuns)),
13314
- riskyActions: unique(policies.flatMap(({ riskyActions }) => riskyActions))
13315
- };
13316
- }
13317
- function assertAuthorized(policy, constraints, target) {
13318
- const action = `${target.kind}:${target.id}`;
13319
- const matches = (patterns) => patterns.some((pattern) => pattern === "*" || pattern === target.id || pattern === action);
13320
- if (matches(policy.authority.deny)) throw new Error(`Dispatch blocked: authority denies "${action}"`);
13321
- if (!matches(policy.authority.allow)) throw new Error(`Dispatch blocked: authority does not allow "${action}"`);
13322
- const matchingConstraints = constraints.filter(({ actions }) => matches(actions));
13323
- const denied = matchingConstraints.find(({ effect }) => effect === "deny");
13324
- if (denied) throw new Error(`Dispatch blocked by constraint "${denied.id}": ${denied.rule}`);
13325
- const requiresApproval = policy.approval === "all-actions" || policy.approval === "risky-actions" && matches(policy.riskyActions) || matchingConstraints.some(({ effect }) => effect === "require-approval");
13326
- return requiresApproval;
13327
- }
13328
- function pinned(kind, record2) {
13329
- return { kind, id: record2.definition.id, revision: record2.revision };
13330
- }
13331
- function intersect(groups) {
13332
- if (groups.length === 0) return [];
13333
- if (groups.every((group) => group.includes("*"))) return ["*"];
13334
- const candidates = unique(groups.flatMap((group) => group.filter((item) => item !== "*")));
13335
- return candidates.filter((candidate) => groups.every((group) => group.includes("*") || group.includes(candidate)));
13336
- }
13337
- function unique(values) {
13338
- return [...new Set(values)].sort();
13339
- }
13340
- function stableJson(value) {
13341
- if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
13342
- if (value && typeof value === "object") {
13343
- return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`).join(",")}}`;
13344
- }
13345
- return JSON.stringify(value);
13346
- }
13347
- var init_policyResolver = __esm({
13348
- "src/goal/policyResolver.ts"() {
13349
- "use strict";
13350
- }
13351
- });
13352
-
13353
- // src/goal/triggerDispatcher.ts
13354
- function decideTrigger(input) {
13355
- if (!input.state) return { kind: "skip", reason: "loop has no runtime state" };
13356
- if (input.state.lifecycle !== "active") {
13357
- return { kind: "skip", reason: `loop is ${input.state.lifecycle}` };
13358
- }
13359
- const trigger = input.definition.trigger;
13360
- if (input.manualRequestId?.trim()) {
13361
- return {
13362
- kind: "fire",
13363
- reason: "manual trigger was requested",
13364
- scheduledAt: input.now.toISOString(),
13365
- idempotencyKey: `${input.definition.id}:manual:${input.manualRequestId.trim()}`
13366
- };
13367
- }
13368
- if (trigger.type === "manual") {
13369
- return { kind: "skip", reason: "manual trigger was not requested" };
13370
- }
13371
- if (trigger.type !== "schedule") {
13372
- return { kind: "skip", reason: `${trigger.type} trigger is not enabled yet` };
13373
- }
13374
- const interval = parseInterval(trigger.every);
13375
- const anchor = input.state.lastFiredAt ? Date.parse(input.state.lastFiredAt) : input.now.getTime() - interval;
13376
- const dueAt = anchor + interval;
13377
- if (input.now.getTime() < dueAt) {
13378
- return { kind: "skip", reason: "scheduled trigger is not due", nextEligibleAt: new Date(dueAt).toISOString() };
13379
- }
13380
- const elapsedIntervals = Math.max(1, Math.floor((input.now.getTime() - anchor) / interval));
13381
- const scheduledAt = new Date(anchor + elapsedIntervals * interval).toISOString();
13382
- return {
13383
- kind: "fire",
13384
- reason: "scheduled trigger is due",
13385
- scheduledAt,
13386
- idempotencyKey: `${input.definition.id}:schedule:${scheduledAt}`
13387
- };
13388
- }
13389
- function parseInterval(value) {
13390
- const match = value.trim().match(/^(\d+)(m|h|d)$/);
13391
- if (!match) throw new Error(`Unsupported schedule interval: ${value}`);
13392
- const amount = Number(match[1]);
13393
- if (!Number.isSafeInteger(amount) || amount < 1) throw new Error(`Unsupported schedule interval: ${value}`);
13394
- const unit = match[2] === "m" ? 6e4 : match[2] === "h" ? 36e5 : 864e5;
13395
- return amount * unit;
13396
- }
13397
- var init_triggerDispatcher = __esm({
13398
- "src/goal/triggerDispatcher.ts"() {
13399
- "use strict";
13400
- }
13401
- });
13402
-
13403
- // src/scripts/dispatchAgencyLoops.ts
13404
- import { randomUUID } from "crypto";
13405
- import {
13406
- createLoopState as createLoopState2,
13407
- createRun
13408
- } from "@kody-ade/agency-domain";
13409
- async function dispatchAgencyLoopsWith(input) {
13410
- const repository = new AgencyModelRepository(input.backend, input.tenantId);
13411
- const catalog = await repository.loadCatalog();
13412
- const records = await repository.listManagedWork(catalog);
13413
- const loops = records.flatMap(
13414
- (record2) => "trigger" in record2.definition ? [{ ...record2, definition: record2.definition, state: record2.state }] : []
13415
- );
13416
- const results = [];
13417
- for (const record2 of loops) {
13418
- if (input.manualRequest && record2.definition.id !== input.manualRequest.loopId) {
13419
- continue;
13420
- }
13421
- const decision = decideTrigger({
13422
- definition: record2.definition,
13423
- state: record2.state,
13424
- now: input.now,
13425
- ...input.manualRequest ? { manualRequestId: input.manualRequest.requestId } : {}
13426
- });
13427
- const now = input.now.toISOString();
13428
- if (decision.kind === "skip") {
13429
- const key = `${record2.definition.id}:skip:${now}`;
13430
- await input.backend.recordSkippedAgencyDispatch(input.tenantId, key, record2.definition.id, decision, now);
13431
- if (record2.state && decision.nextEligibleAt) {
13432
- await repository.saveState(
13433
- createLoopState2({ ...record2.state, nextEligibleAt: decision.nextEligibleAt, updatedAt: now }),
13434
- "loop",
13435
- now
13436
- );
13437
- }
13438
- results.push({ loopId: record2.definition.id, decision: "skipped", reason: decision.reason });
13439
- continue;
13440
- }
13441
- let target;
13442
- let policy;
13443
- try {
13444
- target = resolveTarget(record2.definition, catalog);
13445
- policy = resolveDispatchPolicy({
13446
- catalog,
13447
- owner: { definition: record2.definition, revision: record2.revision },
13448
- target: target.reference
13449
- });
13450
- } catch (error) {
13451
- const reason = error instanceof Error ? error.message : String(error);
13452
- await input.backend.recordSkippedAgencyDispatch(
13453
- input.tenantId,
13454
- decision.idempotencyKey,
13455
- record2.definition.id,
13456
- { kind: "skip", reason, scheduledAt: decision.scheduledAt },
13457
- now
13458
- );
13459
- results.push({ loopId: record2.definition.id, decision: "skipped", reason });
13460
- continue;
13461
- }
13462
- const failurePolicy = record2.definition.reconciliationPolicy.failure;
13463
- const budget = policy.snapshot.policy.budget;
13464
- const maxAttempts = Math.min(failurePolicy.maxAttempts, budget.maxRuns);
13465
- const timeoutSeconds = Math.min(failurePolicy.timeoutSeconds, budget.maxDurationSeconds);
13466
- const backoffBudgetSeconds = Array.from(
13467
- { length: Math.max(0, maxAttempts - 1) },
13468
- (_, index) => failurePolicy.backoffSeconds * 2 ** index
13469
- ).reduce((sum, seconds) => sum + seconds, 0);
13470
- const leaseSeconds = Math.min(budget.maxDurationSeconds, maxAttempts * timeoutSeconds + backoffBudgetSeconds);
13471
- const leaseUntil = new Date(input.now.getTime() + leaseSeconds * 1e3).toISOString();
13472
- const reservationId = `reservation-${randomUUID()}`;
13473
- const correlationId = `corr-${randomUUID()}`;
13474
- const trace = [policy.trace[0], ...target.intermediate, target.reference];
13475
- const reservation = await input.backend.reserveAgencyDispatch(input.tenantId, {
13476
- idempotencyKey: decision.idempotencyKey,
13477
- loopId: record2.definition.id,
13478
- decision,
13479
- leaseUntil,
13480
- reservationId,
13481
- correlationId,
13482
- policyHash: policy.snapshot.hash,
13483
- effectivePolicy: policy.snapshot,
13484
- definitionRefs: trace,
13485
- maxConcurrentRuns: policy.snapshot.policy.maxConcurrentRuns,
13486
- requiresApproval: policy.requiresApproval,
13487
- approvalScopeKind: "loop",
13488
- approvalScopeId: record2.definition.id,
13489
- approvalAction: `${target.reference.kind}:${target.reference.id}`,
13490
- now
13491
- });
13492
- if (!reservation.acquired) {
13493
- const duplicate = reservation.reason === "duplicate";
13494
- const reason = reservation.reason === "approval-required" ? "dispatch is waiting for approval" : reservation.reason === "concurrency-limit" ? "dispatch is waiting for policy capacity" : "trigger firing already reserved";
13495
- results.push({
13496
- loopId: record2.definition.id,
13497
- decision: duplicate ? "duplicate" : "skipped",
13498
- reason
13499
- });
13500
- continue;
13501
- }
13502
- const runningState = createLoopState2({
13503
- definitionId: record2.definition.id,
13504
- lifecycle: record2.state?.lifecycle ?? "active",
13505
- health: record2.state?.health ?? "unknown",
13506
- failures: record2.state?.failures ?? 0,
13507
- lastFiredAt: decision.scheduledAt,
13508
- updatedAt: now
13509
- });
13510
- await repository.saveState(runningState, "loop", now);
13511
- try {
13512
- let attempts = 0;
13513
- let tokens = 0;
13514
- let costUsd = 0;
13515
- let succeeded = false;
13516
- let reason = "target failed";
13517
- let finalRunId;
13518
- let finalCapabilityResults = [];
13519
- const budgetDeadline = Date.now() + budget.maxDurationSeconds * 1e3;
13520
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
13521
- const remainingMilliseconds = budgetDeadline - Date.now();
13522
- if (remainingMilliseconds <= 0) {
13523
- reason = "policy duration budget exhausted";
13524
- break;
13525
- }
13526
- attempts = attempt;
13527
- const startedAt = (/* @__PURE__ */ new Date()).toISOString();
13528
- const runId = `run-${randomUUID()}`;
13529
- finalRunId = runId;
13530
- const activeRun = createRun({
13531
- id: runId,
13532
- status: "running",
13533
- origin: { kind: "loop", id: record2.definition.id, revision: record2.revision },
13534
- target: target.reference,
13535
- trace,
13536
- effectivePolicy: policy.snapshot,
13537
- correlationId,
13538
- startedAt
13539
- });
13540
- await input.backend.createAgencyModelRun(
13541
- input.tenantId,
13542
- target.reference.kind,
13543
- target.reference.id,
13544
- activeRun,
13545
- startedAt
13546
- );
13547
- const attemptResult = await runAttempt(
13548
- input.run,
13549
- target.job,
13550
- Math.min(timeoutSeconds, remainingMilliseconds / 1e3)
13551
- );
13552
- tokens += attemptResult.usage?.tokens ?? 0;
13553
- costUsd += attemptResult.usage?.costUsd ?? 0;
13554
- const finishedAt2 = (/* @__PURE__ */ new Date()).toISOString();
13555
- succeeded = attemptResult.exitCode === 0;
13556
- finalCapabilityResults = attemptResult.capabilityResults ?? [];
13557
- reason = attemptResult.reason ?? (succeeded ? "target dispatched" : "target failed");
13558
- const usage = {
13559
- tokens: attemptResult.usage?.tokens ?? 0,
13560
- costUsd: attemptResult.usage?.costUsd ?? 0,
13561
- durationSeconds: Math.max(0, (Date.parse(finishedAt2) - Date.parse(startedAt)) / 1e3)
13562
- };
13563
- if (tokens > budget.maxTokens || costUsd > budget.maxCostUsd) {
13564
- succeeded = false;
13565
- reason = tokens > budget.maxTokens ? "policy token budget exhausted" : "policy cost budget exhausted";
13566
- }
13567
- await input.backend.finishAgencyModelRun(
13568
- input.tenantId,
13569
- terminalRun(activeRun, succeeded ? "succeeded" : "failed", finishedAt2, usage),
13570
- finishedAt2
13571
- );
13572
- if (tokens > budget.maxTokens || costUsd > budget.maxCostUsd) break;
13573
- if (succeeded || attempt === maxAttempts) break;
13574
- const backoffMilliseconds = failurePolicy.backoffSeconds * 2 ** (attempt - 1) * 1e3;
13575
- if (Date.now() + backoffMilliseconds >= budgetDeadline) {
13576
- reason = "policy duration budget exhausted during retry backoff";
13577
- break;
13578
- }
13579
- await wait(backoffMilliseconds);
13580
- }
13581
- const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
13582
- const goalRecord = record2.definition.targetRef.kind === "goal" ? records.find(
13583
- (candidate) => "executionRef" in candidate.definition && candidate.definition.id === record2.definition.targetRef.id
13584
- ) : void 0;
13585
- if (succeeded && finalRunId) {
13586
- await appendCapabilityOutputs(
13587
- repository,
13588
- finalRunId,
13589
- target.reference,
13590
- goalRecord ? {
13591
- kind: "goal",
13592
- id: goalRecord.definition.id,
13593
- revision: goalRecord.revision
13594
- } : {
13595
- kind: "loop",
13596
- id: record2.definition.id,
13597
- revision: record2.revision
13598
- },
13599
- finalCapabilityResults,
13600
- finishedAt
13601
- );
13602
- }
13603
- if (succeeded && goalRecord) {
13604
- await repository.refreshGoalProgress(goalRecord, finishedAt);
13605
- }
13606
- await input.backend.finishAgencyDispatch(
13607
- input.tenantId,
13608
- decision.idempotencyKey,
13609
- reservationId,
13610
- succeeded ? "dispatched" : "dead-letter",
13611
- finishedAt,
13612
- finalRunId
13613
- );
13614
- await repository.saveState(
13615
- createLoopState2({
13616
- ...runningState,
13617
- health: succeeded ? "healthy" : "degraded",
13618
- failures: succeeded ? 0 : runningState.failures + 1,
13619
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
13620
- }),
13621
- "loop",
13622
- (/* @__PURE__ */ new Date()).toISOString()
13623
- );
13624
- results.push({
13625
- loopId: record2.definition.id,
13626
- decision: succeeded ? "dispatched" : "failed",
13627
- reason: succeeded ? reason : `${reason}; dead-lettered after ${attempts} attempt${attempts === 1 ? "" : "s"}`
13628
- });
13629
- } catch (error) {
13630
- const reason = error instanceof Error ? error.message : String(error);
13631
- const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
13632
- await input.backend.finishAgencyDispatch(
13633
- input.tenantId,
13634
- decision.idempotencyKey,
13635
- reservationId,
13636
- "dead-letter",
13637
- finishedAt
13638
- );
13639
- results.push({ loopId: record2.definition.id, decision: "failed", reason });
13640
- }
13641
- }
13642
- return results;
13643
- }
13644
- function resolveTarget(loop, catalog) {
13645
- const goal = loop.targetRef.kind === "goal" ? catalog.goals.get(loop.targetRef.id) : void 0;
13646
- if (loop.targetRef.kind === "goal" && !goal) throw new Error(`Loop target Goal is missing: ${loop.targetRef.id}`);
13647
- if (goal && goal.definition.operationId !== loop.operationId) {
13648
- throw new Error(`Loop and target Goal must belong to the same Operation`);
13649
- }
13650
- const target = goal?.definition.executionRef ?? loop.targetRef;
13651
- if (target.kind === "goal") throw new Error("Nested Goal target is invalid");
13652
- const record2 = target.kind === "workflow" ? catalog.workflows.get(target.id) : catalog.capabilities.get(target.id);
13653
- if (!record2) throw new Error(`Loop execution target is missing: ${target.kind}:${target.id}`);
13654
- const reference = { kind: target.kind, id: target.id, revision: record2.revision };
13655
- return {
13656
- reference,
13657
- intermediate: goal ? [{ kind: "goal", id: goal.definition.id, revision: goal.revision }] : [],
13658
- job: target.kind === "workflow" ? { workflow: target.id, cliArgs: {}, flavor: "scheduled" } : { capability: target.id, cliArgs: {}, flavor: "scheduled" }
13659
- };
13660
- }
13661
- function terminalRun(active, status, finishedAt, usage) {
13662
- return createRun({ ...active, status, finishedAt, usage });
13663
- }
13664
- async function runAttempt(run, job, timeoutSeconds) {
13665
- const abortController = new AbortController();
13666
- let timer;
13667
- try {
13668
- return await Promise.race([
13669
- run(job, abortController).catch((error) => ({
13670
- exitCode: 99,
13671
- reason: error instanceof Error ? error.message : String(error)
13672
- })),
13673
- new Promise((resolve19) => {
13674
- timer = setTimeout(() => {
13675
- abortController.abort();
13676
- resolve19({ exitCode: 124, reason: `target timed out after ${formatSeconds(timeoutSeconds)}s` });
13677
- }, timeoutSeconds * 1e3);
13678
- })
13679
- ]);
13680
- } finally {
13681
- if (timer) clearTimeout(timer);
13682
- }
13683
- }
13684
- async function appendCapabilityOutputs(repository, runId, producer, parentRef, results, createdAt) {
13685
- for (const result of results) {
13686
- const outputs = [
13687
- ...Object.entries(result.facts).map(([key, value]) => ({ kind: "fact", key, value })),
13688
- ...Object.entries(result.evidence ?? {}).map(([key, value]) => ({
13689
- kind: "evidence",
13690
- key,
13691
- value
13692
- })),
13693
- ...result.artifacts.map((artifact, index) => ({
13694
- kind: "artifact",
13695
- key: artifact.label || `artifact-${index + 1}`,
13696
- value: artifact
13697
- }))
13698
- ];
13699
- for (const output of outputs) {
13700
- await repository.appendOutput(`output-${randomUUID()}`, {
13701
- ...output,
13702
- runId,
13703
- producer: { kind: producer.kind, id: producer.id },
13704
- parentRef,
13705
- contract: "capability-result/v1",
13706
- createdAt
13707
- });
13708
- }
13709
- }
13710
- }
13711
- function formatSeconds(seconds) {
13712
- return Number.isInteger(seconds) ? String(seconds) : seconds.toFixed(3).replace(/0+$/, "").replace(/\.$/, "");
13713
- }
13714
- async function wait(milliseconds) {
13715
- if (milliseconds <= 0) return;
13716
- await new Promise((resolve19) => setTimeout(resolve19, milliseconds));
13717
- }
13718
- function repositoryTenant(config) {
13719
- const owner = config.github?.owner?.trim() || process.env.GITHUB_REPOSITORY?.split("/")[0]?.trim();
13720
- const repo = config.github?.repo?.trim() || process.env.GITHUB_REPOSITORY?.split("/")[1]?.trim();
13721
- return owner && repo ? `${owner}/${repo}` : null;
13722
- }
13723
- var dispatchAgencyLoops;
13724
- var init_dispatchAgencyLoops = __esm({
13725
- "src/scripts/dispatchAgencyLoops.ts"() {
13726
- "use strict";
13727
- init_agencyModelRepository();
13728
- init_policyResolver();
13729
- init_triggerDispatcher();
13730
- init_job();
13731
- init_state_backend();
13732
- dispatchAgencyLoops = async (ctx) => {
13733
- const tenantId2 = repositoryTenant(ctx.config);
13734
- if (!tenantId2) throw new Error("Repository identity is required for Agency Loop dispatch");
13735
- const backend = createStateBackendFromEnv();
13736
- const requestedLoopId = typeof ctx.args.loop === "string" ? ctx.args.loop.trim() : "";
13737
- const results = await dispatchAgencyLoopsWith({
13738
- tenantId: tenantId2,
13739
- backend,
13740
- now: /* @__PURE__ */ new Date(),
13741
- ...requestedLoopId ? {
13742
- manualRequest: {
13743
- loopId: requestedLoopId,
13744
- requestId: process.env.GITHUB_RUN_ID?.trim() || `local-${randomUUID()}`
13745
- }
13746
- } : {},
13747
- run: (job, abortController) => runJob(job, {
13748
- cwd: ctx.cwd,
13749
- config: ctx.config,
13750
- verbose: ctx.verbose,
13751
- quiet: ctx.quiet,
13752
- chain: false,
13753
- abortController
13754
- })
13755
- });
13756
- ctx.data.agencyLoopDispatchResults = results;
13757
- };
13758
- }
13759
- });
13760
-
13761
13037
  // src/scripts/dispatchCapabilityFileTicks.ts
13762
13038
  var dispatchCapabilityFileTicks;
13763
13039
  var init_dispatchCapabilityFileTicks = __esm({
@@ -13876,72 +13152,6 @@ var init_dispatchClassified = __esm({
13876
13152
  }
13877
13153
  });
13878
13154
 
13879
- // src/jobIdentity.ts
13880
- function stableJobKey(job) {
13881
- const capability = job.workflow ?? job.capability ?? job.action;
13882
- const implementation = job.implementation ?? capability ?? "unknown";
13883
- if (job.flavor === "scheduled" && job.capability) return `scheduled:${job.capability}:${implementation}`;
13884
- const target = typeof job.target === "number" ? job.target : targetFromCliArgs(job.cliArgs);
13885
- const work = capability && implementation && implementation !== capability ? `${capability}:${implementation}` : capability ?? implementation;
13886
- return target === void 0 ? `${job.flavor}:${work}` : `${job.flavor}:${work}:${target}`;
13887
- }
13888
- function targetFromCliArgs(cliArgs) {
13889
- if (!cliArgs) return void 0;
13890
- for (const key of ["issue", "pr", "target", "issue_number"]) {
13891
- const value = cliArgs[key];
13892
- if (typeof value === "number" && Number.isFinite(value)) return value;
13893
- }
13894
- return void 0;
13895
- }
13896
- var init_jobIdentity = __esm({
13897
- "src/jobIdentity.ts"() {
13898
- "use strict";
13899
- }
13900
- });
13901
-
13902
- // src/scripts/dispatchNextTaskJob.ts
13903
- function taskJobToJob(job, issueArg) {
13904
- const target = typeof job.target === "number" ? job.target : typeof issueArg === "number" ? issueArg : void 0;
13905
- return {
13906
- capability: job.capability ?? job.implementation,
13907
- implementation: job.implementation,
13908
- ...job.reason ? { why: job.reason } : {},
13909
- ...job.agent ? { agent: job.agent } : {},
13910
- ...job.schedule ? { schedule: job.schedule } : {},
13911
- ...typeof target === "number" ? { target, cliArgs: { issue: target } } : { cliArgs: {} },
13912
- flavor: job.flavor ?? "instant"
13913
- };
13914
- }
13915
- function isJob(input) {
13916
- if (!input || typeof input !== "object" || Array.isArray(input)) return false;
13917
- const job = input;
13918
- return (typeof job.capability === "string" || typeof job.action === "string") && (job.flavor === "instant" || job.flavor === "scheduled") && (!job.cliArgs || typeof job.cliArgs === "object" && !Array.isArray(job.cliArgs));
13919
- }
13920
- var dispatchNextTaskJob;
13921
- var init_dispatchNextTaskJob = __esm({
13922
- "src/scripts/dispatchNextTaskJob.ts"() {
13923
- "use strict";
13924
- init_jobIdentity();
13925
- init_state();
13926
- dispatchNextTaskJob = async (ctx, profile) => {
13927
- const state = ctx.data.taskState ?? emptyState();
13928
- const ids = Array.isArray(ctx.data.plannedTaskJobIds) ? ctx.data.plannedTaskJobIds.filter((id) => typeof id === "string") : void 0;
13929
- const next = nextPendingTaskJob(state, ids);
13930
- ctx.skipAgent = true;
13931
- if (!next) {
13932
- ctx.output.exitCode = 0;
13933
- ctx.output.reason = "all planned task jobs are complete";
13934
- return;
13935
- }
13936
- const plannedJobs = Array.isArray(ctx.data.plannedTaskJobs) ? ctx.data.plannedTaskJobs.filter(isJob) : [];
13937
- ctx.output.nextJob = plannedJobs.find((job) => stableJobKey(job) === next.id) ?? taskJobToJob(next, ctx.args.issue);
13938
- if (typeof ctx.args.issue === "number") {
13939
- ctx.output.afterNextJob = { action: profile.action ?? profile.name, cliArgs: { issue: ctx.args.issue } };
13940
- }
13941
- };
13942
- }
13943
- });
13944
-
13945
13155
  // src/loopDefinitions.ts
13946
13156
  import * as fs36 from "fs";
13947
13157
  import * as path34 from "path";
@@ -13976,15 +13186,15 @@ function readLoopDefinition(cwd, id) {
13976
13186
  try {
13977
13187
  const loop = normalizeLoopDefinition(JSON.parse(fs36.readFileSync(filePath, "utf8")));
13978
13188
  if (loop?.id === id) return loop;
13979
- process.stderr.write(`[kody] invalid simple Loop definition: ${filePath}
13189
+ process.stderr.write(`[kody] invalid Loop definition: ${filePath}
13980
13190
  `);
13981
13191
  } catch {
13982
- process.stderr.write(`[kody] unreadable simple Loop definition: ${filePath}
13192
+ process.stderr.write(`[kody] unreadable Loop definition: ${filePath}
13983
13193
  `);
13984
13194
  }
13985
13195
  }
13986
13196
  process.stderr.write(
13987
- `[kody] simple Loop not found: ${id} (${roots.map((root) => path34.join(root, "loops", id, "loop.json")).join(", ")})
13197
+ `[kody] Loop not found: ${id} (${roots.map((root) => path34.join(root, "loops", id, "loop.json")).join(", ")})
13988
13198
  `
13989
13199
  );
13990
13200
  return null;
@@ -14003,7 +13213,7 @@ function listLoopDefinitions(cwd) {
14003
13213
  const loop = normalizeLoopDefinition(JSON.parse(fs36.readFileSync(filePath, "utf8")));
14004
13214
  if (loop?.id === id) byId.set(id, loop);
14005
13215
  } catch {
14006
- process.stderr.write(`[kody] unreadable simple Loop definition: ${filePath}
13216
+ process.stderr.write(`[kody] unreadable Loop definition: ${filePath}
14007
13217
  `);
14008
13218
  }
14009
13219
  }
@@ -14047,11 +13257,88 @@ var init_loopDefinitions = __esm({
14047
13257
  }
14048
13258
  });
14049
13259
 
14050
- // src/scripts/dispatchSimpleLoops.ts
14051
- import { randomUUID as randomUUID2 } from "crypto";
13260
+ // src/scripts/dispatchLoops.ts
13261
+ import { randomUUID } from "crypto";
13262
+ function assertLoopDispatchesSucceeded(results) {
13263
+ const failed = results.filter((result) => result.status === "failed");
13264
+ if (failed.length === 0) return;
13265
+ throw new Error(
13266
+ `Loop dispatch failed: ${failed.map((result) => `${result.loopId}: ${result.reason}`).join("; ")}`
13267
+ );
13268
+ }
13269
+ async function dispatchLoopsWith(input) {
13270
+ const results = [];
13271
+ for (const loop of input.loops) {
13272
+ const slot = loopDispatchSlot(loop, input.now, input.force, input.nonce());
13273
+ if (!slot) continue;
13274
+ const reservationId = `reservation-${input.nonce()}`;
13275
+ const idempotencyKey = `${loop.id}:${slot}`;
13276
+ const claimed = await input.backend.reserveLoopDispatch(input.tenantId, {
13277
+ idempotencyKey,
13278
+ loopId: loop.id,
13279
+ decision: {
13280
+ kind: "fire",
13281
+ reason: input.force ? "manual Loop run requested" : "local Loop schedule is due",
13282
+ scheduledAt: slot
13283
+ },
13284
+ leaseUntil: new Date(input.now.getTime() + LOOP_DISPATCH_LEASE_MS).toISOString(),
13285
+ reservationId,
13286
+ correlationId: `corr-${input.nonce()}`,
13287
+ policyHash: `loop:${loop.id}`,
13288
+ effectivePolicy: { source: "repository" },
13289
+ definitionRefs: [{ kind: "loop", id: loop.id }],
13290
+ maxConcurrentRuns: 1,
13291
+ requiresApproval: false,
13292
+ approvalScopeKind: "loop",
13293
+ approvalScopeId: loop.id,
13294
+ approvalAction: `${loop.target.kind}:${loop.target.id}`,
13295
+ now: input.now.toISOString()
13296
+ });
13297
+ if (!claimed.acquired) {
13298
+ results.push({ loopId: loop.id, status: "skipped", reason: claimed.reason ?? "already claimed" });
13299
+ continue;
13300
+ }
13301
+ const runId = `loop-${loop.id}-${input.nonce()}`;
13302
+ const startedAt = input.now.toISOString();
13303
+ const running = {
13304
+ id: runId,
13305
+ status: "running",
13306
+ target: loop.target,
13307
+ agent: "kody",
13308
+ startedAt
13309
+ };
13310
+ await input.backend.createAgencyRun(input.tenantId, "loop", loop.id, running, startedAt);
13311
+ let exitCode;
13312
+ let reason;
13313
+ try {
13314
+ const result = await input.run(loopJob(loop), runId);
13315
+ exitCode = result.exitCode;
13316
+ reason = result.reason ?? (exitCode === 0 ? "dispatched" : "target failed");
13317
+ } catch (error) {
13318
+ exitCode = 1;
13319
+ reason = error instanceof Error ? error.message : String(error);
13320
+ }
13321
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
13322
+ const succeeded = exitCode === 0;
13323
+ await input.backend.finishAgencyRun(
13324
+ input.tenantId,
13325
+ {
13326
+ ...running,
13327
+ status: succeeded ? "succeeded" : "failed",
13328
+ finishedAt,
13329
+ ...succeeded ? { output: { summary: reason } } : { error: reason }
13330
+ },
13331
+ finishedAt
13332
+ );
13333
+ const status = succeeded ? "dispatched" : "failed";
13334
+ await input.backend.finishLoopDispatch(input.tenantId, idempotencyKey, reservationId, status, finishedAt, runId);
13335
+ results.push({ loopId: loop.id, status, reason });
13336
+ }
13337
+ return results;
13338
+ }
14052
13339
  function selectRunnableLoops(loops, now, options) {
14053
13340
  return loops.filter(
14054
- (loop) => loop.enabled && loop.trigger.type === "schedule" && (!options.loopId || loop.id === options.loopId) && (options.force || dueSlot(loop, now) !== null)
13341
+ (loop) => loop.enabled && (!options.loopId || loop.id === options.loopId) && (options.force || loop.trigger.type === "schedule" && dueSlot(loop, now) !== null)
14055
13342
  );
14056
13343
  }
14057
13344
  function loopDispatchSlot(loop, now, force, nonce) {
@@ -14093,21 +13380,22 @@ function loopJob(loop) {
14093
13380
  );
14094
13381
  return loop.target.kind === "workflow" ? { workflow: loop.target.id, cliArgs, flavor: "scheduled" } : { capability: loop.target.id, cliArgs, flavor: "scheduled" };
14095
13382
  }
14096
- function repositoryTenant2(config) {
13383
+ function repositoryTenant(config) {
14097
13384
  const [envOwner, envRepo] = (process.env.GITHUB_REPOSITORY ?? "").split("/");
14098
13385
  const owner = config.github?.owner?.trim() || envOwner?.trim();
14099
13386
  const repo = config.github?.repo?.trim() || envRepo?.trim();
14100
13387
  return owner && repo ? `${owner}/${repo}` : null;
14101
13388
  }
14102
- var dispatchSimpleLoops;
14103
- var init_dispatchSimpleLoops = __esm({
14104
- "src/scripts/dispatchSimpleLoops.ts"() {
13389
+ var LOOP_DISPATCH_LEASE_MS, dispatchLoops;
13390
+ var init_dispatchLoops = __esm({
13391
+ "src/scripts/dispatchLoops.ts"() {
14105
13392
  "use strict";
14106
13393
  init_job();
14107
13394
  init_loopDefinitions();
14108
13395
  init_state_backend();
14109
- dispatchSimpleLoops = async (ctx) => {
14110
- const tenantId2 = repositoryTenant2(ctx.config);
13396
+ LOOP_DISPATCH_LEASE_MS = 6 * 60 * 60 * 1e3;
13397
+ dispatchLoops = async (ctx) => {
13398
+ const tenantId2 = repositoryTenant(ctx.config);
14111
13399
  if (!tenantId2) throw new Error("Repository identity is required for Loop dispatch");
14112
13400
  const now = /* @__PURE__ */ new Date();
14113
13401
  const force = ctx.data.jobForce === true;
@@ -14119,53 +13407,94 @@ var init_dispatchSimpleLoops = __esm({
14119
13407
  process.stdout.write(`\u2192 kody: Loop scheduler found ${due.length} runnable Loop(s)${force ? " (manual)" : ""}
14120
13408
  `);
14121
13409
  const backend = createStateBackendFromEnv();
14122
- const results = [];
14123
- for (const loop of due) {
14124
- const slot = loopDispatchSlot(loop, now, force, randomUUID2());
14125
- if (!slot) continue;
14126
- const reservationId = `reservation-${randomUUID2()}`;
14127
- const idempotencyKey = `${loop.id}:${slot}`;
14128
- const claimed = await backend.reserveAgencyDispatch(tenantId2, {
14129
- idempotencyKey,
14130
- loopId: loop.id,
14131
- decision: {
14132
- kind: "fire",
14133
- reason: force ? "manual Loop run requested" : "local Loop schedule is due",
14134
- scheduledAt: slot
14135
- },
14136
- leaseUntil: new Date(now.getTime() + 6 * 60 * 60 * 1e3).toISOString(),
14137
- reservationId,
14138
- correlationId: `corr-${randomUUID2()}`,
14139
- policyHash: `loop:${loop.id}`,
14140
- effectivePolicy: { source: "repository" },
14141
- definitionRefs: [{ kind: "loop", id: loop.id }],
14142
- maxConcurrentRuns: 1,
14143
- requiresApproval: false,
14144
- approvalScopeKind: "loop",
14145
- approvalScopeId: loop.id,
14146
- approvalAction: `${loop.target.kind}:${loop.target.id}`,
14147
- now: now.toISOString()
14148
- });
14149
- if (!claimed.acquired) {
14150
- results.push({ loopId: loop.id, status: "skipped", reason: claimed.reason ?? "already claimed" });
14151
- continue;
14152
- }
14153
- const result = await runJob(loopJob(loop), {
13410
+ const results = await dispatchLoopsWith({
13411
+ loops: due,
13412
+ tenantId: tenantId2,
13413
+ backend,
13414
+ now,
13415
+ force,
13416
+ nonce: randomUUID,
13417
+ run: (job, parentRunId) => runJob(job, {
14154
13418
  cwd: ctx.cwd,
14155
13419
  config: ctx.config,
14156
13420
  verbose: ctx.verbose,
14157
13421
  quiet: ctx.quiet,
14158
- chain: false
14159
- });
14160
- const status = result.exitCode === 0 ? "dispatched" : "failed";
14161
- await backend.finishAgencyDispatch(tenantId2, idempotencyKey, reservationId, status, (/* @__PURE__ */ new Date()).toISOString());
14162
- results.push({ loopId: loop.id, status, reason: result.reason ?? status });
14163
- }
13422
+ chain: false,
13423
+ preloadedData: { parentRunId }
13424
+ })
13425
+ });
14164
13426
  for (const result of results) {
14165
13427
  process.stdout.write(`\u2192 kody: Loop ${result.loopId} ${result.status}: ${result.reason}
14166
13428
  `);
14167
13429
  }
14168
- ctx.data.simpleLoopDispatchResults = results;
13430
+ ctx.data.loopDispatchResults = results;
13431
+ assertLoopDispatchesSucceeded(results);
13432
+ };
13433
+ }
13434
+ });
13435
+
13436
+ // src/jobIdentity.ts
13437
+ function stableJobKey(job) {
13438
+ const capability = job.workflow ?? job.capability ?? job.action;
13439
+ const implementation = job.implementation ?? capability ?? "unknown";
13440
+ if (job.flavor === "scheduled" && job.capability) return `scheduled:${job.capability}:${implementation}`;
13441
+ const target = typeof job.target === "number" ? job.target : targetFromCliArgs(job.cliArgs);
13442
+ const work = capability && implementation && implementation !== capability ? `${capability}:${implementation}` : capability ?? implementation;
13443
+ return target === void 0 ? `${job.flavor}:${work}` : `${job.flavor}:${work}:${target}`;
13444
+ }
13445
+ function targetFromCliArgs(cliArgs) {
13446
+ if (!cliArgs) return void 0;
13447
+ for (const key of ["issue", "pr", "target", "issue_number"]) {
13448
+ const value = cliArgs[key];
13449
+ if (typeof value === "number" && Number.isFinite(value)) return value;
13450
+ }
13451
+ return void 0;
13452
+ }
13453
+ var init_jobIdentity = __esm({
13454
+ "src/jobIdentity.ts"() {
13455
+ "use strict";
13456
+ }
13457
+ });
13458
+
13459
+ // src/scripts/dispatchNextTaskJob.ts
13460
+ function taskJobToJob(job, issueArg) {
13461
+ const target = typeof job.target === "number" ? job.target : typeof issueArg === "number" ? issueArg : void 0;
13462
+ return {
13463
+ capability: job.capability ?? job.implementation,
13464
+ implementation: job.implementation,
13465
+ ...job.reason ? { why: job.reason } : {},
13466
+ ...job.agent ? { agent: job.agent } : {},
13467
+ ...job.schedule ? { schedule: job.schedule } : {},
13468
+ ...typeof target === "number" ? { target, cliArgs: { issue: target } } : { cliArgs: {} },
13469
+ flavor: job.flavor ?? "instant"
13470
+ };
13471
+ }
13472
+ function isJob(input) {
13473
+ if (!input || typeof input !== "object" || Array.isArray(input)) return false;
13474
+ const job = input;
13475
+ return (typeof job.capability === "string" || typeof job.action === "string") && (job.flavor === "instant" || job.flavor === "scheduled") && (!job.cliArgs || typeof job.cliArgs === "object" && !Array.isArray(job.cliArgs));
13476
+ }
13477
+ var dispatchNextTaskJob;
13478
+ var init_dispatchNextTaskJob = __esm({
13479
+ "src/scripts/dispatchNextTaskJob.ts"() {
13480
+ "use strict";
13481
+ init_jobIdentity();
13482
+ init_state();
13483
+ dispatchNextTaskJob = async (ctx, profile) => {
13484
+ const state = ctx.data.taskState ?? emptyState();
13485
+ const ids = Array.isArray(ctx.data.plannedTaskJobIds) ? ctx.data.plannedTaskJobIds.filter((id) => typeof id === "string") : void 0;
13486
+ const next = nextPendingTaskJob(state, ids);
13487
+ ctx.skipAgent = true;
13488
+ if (!next) {
13489
+ ctx.output.exitCode = 0;
13490
+ ctx.output.reason = "all planned task jobs are complete";
13491
+ return;
13492
+ }
13493
+ const plannedJobs = Array.isArray(ctx.data.plannedTaskJobs) ? ctx.data.plannedTaskJobs.filter(isJob) : [];
13494
+ ctx.output.nextJob = plannedJobs.find((job) => stableJobKey(job) === next.id) ?? taskJobToJob(next, ctx.args.issue);
13495
+ if (typeof ctx.args.issue === "number") {
13496
+ ctx.output.afterNextJob = { action: profile.action ?? profile.name, cliArgs: { issue: ctx.args.issue } };
13497
+ }
14169
13498
  };
14170
13499
  }
14171
13500
  });
@@ -15892,9 +15221,9 @@ var init_kodyVariables = __esm({
15892
15221
  });
15893
15222
 
15894
15223
  // src/backendVault.ts
15895
- import { createDecipheriv, createHash as createHash6 } from "crypto";
15224
+ import { createDecipheriv, createHash as createHash5 } from "crypto";
15896
15225
  function cacheKey(owner, repo, masterKey) {
15897
- const keyHash = createHash6("sha256").update(masterKey).digest("hex").slice(0, 16);
15226
+ const keyHash = createHash5("sha256").update(masterKey).digest("hex").slice(0, 16);
15898
15227
  return `${owner}/${repo}:${keyHash}`.toLowerCase();
15899
15228
  }
15900
15229
  function decryptVault(payload, masterKey) {
@@ -16681,7 +16010,7 @@ var init_notifyTerminal = __esm({
16681
16010
  });
16682
16011
 
16683
16012
  // src/scripts/openAgencyModelReviewPr.ts
16684
- import { createHash as createHash7 } from "crypto";
16013
+ import { createHash as createHash6 } from "crypto";
16685
16014
  function parseAgencyModelProposal(raw) {
16686
16015
  const text2 = raw.trim();
16687
16016
  const jsonText = (text2.match(/^```(?:json)?\s*\n([\s\S]*?)\n```$/i)?.[1] ?? text2).trim();
@@ -16733,7 +16062,7 @@ function normalizeBundleFiles(bundle) {
16733
16062
  });
16734
16063
  }
16735
16064
  function buildProposalId(issueNumber, bundle, sourceLabel) {
16736
- const digest = createHash7("sha256").update(JSON.stringify({ issueNumber, sourceLabel, title: bundle.title, files: normalizeBundleFiles(bundle) })).digest("hex").slice(0, 16);
16065
+ const digest = createHash6("sha256").update(JSON.stringify({ issueNumber, sourceLabel, title: bundle.title, files: normalizeBundleFiles(bundle) })).digest("hex").slice(0, 16);
16737
16066
  return `issue-${issueNumber}-${digest}`;
16738
16067
  }
16739
16068
  function isDryRun(ctx) {
@@ -19063,9 +18392,9 @@ var init_reviewFlow = __esm({
19063
18392
  });
19064
18393
 
19065
18394
  // src/scripts/previewBuildHelpers.ts
19066
- import { createDecipheriv as createDecipheriv2, createHash as createHash8, hkdfSync as hkdfSync2 } from "crypto";
18395
+ import { createDecipheriv as createDecipheriv2, createHash as createHash7, hkdfSync as hkdfSync2 } from "crypto";
19067
18396
  function shortHash(s) {
19068
- return createHash8("sha256").update(s).digest("hex").slice(0, 6);
18397
+ return createHash7("sha256").update(s).digest("hex").slice(0, 6);
19069
18398
  }
19070
18399
  function previewAppName(repo, pr) {
19071
18400
  const [owner, name] = repo.split("/");
@@ -19098,7 +18427,7 @@ function formatPreviewComment(args) {
19098
18427
  ].join("\n");
19099
18428
  }
19100
18429
  function defaultImageTag(repo, ref) {
19101
- return createHash8("sha256").update(`${repo}@${ref}`).digest("hex").slice(0, 12);
18430
+ return createHash7("sha256").update(`${repo}@${ref}`).digest("hex").slice(0, 12);
19102
18431
  }
19103
18432
  var init_previewBuildHelpers = __esm({
19104
18433
  "src/scripts/previewBuildHelpers.ts"() {
@@ -20985,12 +20314,11 @@ var init_scripts = __esm({
20985
20314
  init_diagMcp();
20986
20315
  init_discoverQaContext();
20987
20316
  init_dispatch();
20988
- init_dispatchAgencyLoops();
20989
20317
  init_dispatchCapabilityFileTicks();
20990
20318
  init_dispatchCapabilityTicks();
20991
20319
  init_dispatchClassified();
20320
+ init_dispatchLoops();
20992
20321
  init_dispatchNextTaskJob();
20993
- init_dispatchSimpleLoops();
20994
20322
  init_ensurePr();
20995
20323
  init_evaluateAgencyBoundaries();
20996
20324
  init_failOnceTaskJob();
@@ -21116,8 +20444,7 @@ var init_scripts = __esm({
21116
20444
  diagMcp,
21117
20445
  warmupMcp,
21118
20446
  dispatchCapabilityTicks,
21119
- dispatchAgencyLoops,
21120
- dispatchSimpleLoops,
20447
+ dispatchLoops,
21121
20448
  dispatchCapabilityFileTicks,
21122
20449
  planTaskJobs,
21123
20450
  dispatchNextTaskJob,
@@ -22582,6 +21909,7 @@ async function runJob(job, base) {
22582
21909
  updatedAt: startedAt,
22583
21910
  workflow: workflowIdentity,
22584
21911
  kodyRunId: valid.workflowRunId,
21912
+ parentRunId: typeof base.preloadedData?.parentRunId === "string" ? base.preloadedData.parentRunId : void 0,
22585
21913
  sourceType: "job"
22586
21914
  };
22587
21915
  const persistRun = Boolean(base.config && !base.skipConfig && hasStateBackendConfig());
@@ -24413,6 +23741,7 @@ import { createHash as createHash2 } from "crypto";
24413
23741
  import * as fs16 from "fs";
24414
23742
  import * as path17 from "path";
24415
23743
  var SLUG_RE = /^[a-z0-9][a-z0-9_-]{0,127}$/;
23744
+ var REPOSITORY_OWNED_NAMESPACES = ["loops"];
24416
23745
  function assertSafeDefinitionPath(filePath) {
24417
23746
  const segments = filePath.split("/");
24418
23747
  if (!filePath || filePath.startsWith("/") || filePath.includes("\\") || filePath.includes("\0") || segments.some((segment) => !segment || segment === "." || segment === "..")) {
@@ -24469,6 +23798,13 @@ function writeDefinition(root, kind, definition) {
24469
23798
  }
24470
23799
  writeBundle(path17.join(root, "capabilities", definition.slug), bundle);
24471
23800
  }
23801
+ function preserveRepositoryDefinitions(root, staging) {
23802
+ for (const namespace of REPOSITORY_OWNED_NAMESPACES) {
23803
+ const source = path17.join(root, namespace);
23804
+ if (!fs16.existsSync(source)) continue;
23805
+ fs16.cpSync(source, path17.join(staging, namespace), { recursive: true });
23806
+ }
23807
+ }
24472
23808
  async function hydrateDefinitions(options) {
24473
23809
  const root = path17.join(options.cwd, ".kody-engine", "definitions");
24474
23810
  const staging = `${root}.tmp-${process.pid}-${Date.now()}`;
@@ -24507,6 +23843,7 @@ async function hydrateDefinitions(options) {
24507
23843
  writeDefinition(staging, "asset", definition);
24508
23844
  versions[`asset:${definition.slug}`] = definition.version;
24509
23845
  }
23846
+ preserveRepositoryDefinitions(root, staging);
24510
23847
  const manifest = {
24511
23848
  schemaVersion: 1,
24512
23849
  tenantId: options.tenantId,
@@ -26841,7 +26178,7 @@ init_config();
26841
26178
  init_fetchRepoMcp();
26842
26179
 
26843
26180
  // src/servers/mcpHttpServer.ts
26844
- import { randomUUID as randomUUID3 } from "crypto";
26181
+ import { randomUUID as randomUUID2 } from "crypto";
26845
26182
  import { createServer as createServer4 } from "http";
26846
26183
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
26847
26184
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -26850,7 +26187,7 @@ function buildMcpHttpServer(opts) {
26850
26187
  const transports = /* @__PURE__ */ new Map();
26851
26188
  for (const route of opts.routes) {
26852
26189
  const transport = new StreamableHTTPServerTransport({
26853
- sessionIdGenerator: () => randomUUID3()
26190
+ sessionIdGenerator: () => randomUUID2()
26854
26191
  });
26855
26192
  transports.set(route.path, transport);
26856
26193
  routes.set(route.path, route.name);
@@ -27428,12 +26765,12 @@ import { createServer as createServer5 } from "http";
27428
26765
 
27429
26766
  // src/pool/agency-loop-tick.ts
27430
26767
  function normalizeRepositories(repositories) {
27431
- const unique2 = /* @__PURE__ */ new Set();
26768
+ const unique = /* @__PURE__ */ new Set();
27432
26769
  for (const raw of repositories) {
27433
26770
  const repo = raw.trim().toLowerCase();
27434
- if (/^[^/\s]+\/[^/\s]+$/.test(repo)) unique2.add(repo);
26771
+ if (/^[^/\s]+\/[^/\s]+$/.test(repo)) unique.add(repo);
27435
26772
  }
27436
- return [...unique2].sort();
26773
+ return [...unique].sort();
27437
26774
  }
27438
26775
  async function runAgencyLoopTick(deps) {
27439
26776
  const repositories = normalizeRepositories(await deps.discover());