@kody-ade/kody-engine 0.4.471 → 0.4.472

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.472",
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()) {
@@ -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,81 @@ 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
+ async function dispatchLoopsWith(input) {
13263
+ const results = [];
13264
+ for (const loop of input.loops) {
13265
+ const slot = loopDispatchSlot(loop, input.now, input.force, input.nonce());
13266
+ if (!slot) continue;
13267
+ const reservationId = `reservation-${input.nonce()}`;
13268
+ const idempotencyKey = `${loop.id}:${slot}`;
13269
+ const claimed = await input.backend.reserveLoopDispatch(input.tenantId, {
13270
+ idempotencyKey,
13271
+ loopId: loop.id,
13272
+ decision: {
13273
+ kind: "fire",
13274
+ reason: input.force ? "manual Loop run requested" : "local Loop schedule is due",
13275
+ scheduledAt: slot
13276
+ },
13277
+ leaseUntil: new Date(input.now.getTime() + LOOP_DISPATCH_LEASE_MS).toISOString(),
13278
+ reservationId,
13279
+ correlationId: `corr-${input.nonce()}`,
13280
+ policyHash: `loop:${loop.id}`,
13281
+ effectivePolicy: { source: "repository" },
13282
+ definitionRefs: [{ kind: "loop", id: loop.id }],
13283
+ maxConcurrentRuns: 1,
13284
+ requiresApproval: false,
13285
+ approvalScopeKind: "loop",
13286
+ approvalScopeId: loop.id,
13287
+ approvalAction: `${loop.target.kind}:${loop.target.id}`,
13288
+ now: input.now.toISOString()
13289
+ });
13290
+ if (!claimed.acquired) {
13291
+ results.push({ loopId: loop.id, status: "skipped", reason: claimed.reason ?? "already claimed" });
13292
+ continue;
13293
+ }
13294
+ const runId = `loop-${loop.id}-${input.nonce()}`;
13295
+ const startedAt = input.now.toISOString();
13296
+ const running = {
13297
+ id: runId,
13298
+ status: "running",
13299
+ target: loop.target,
13300
+ agent: "kody",
13301
+ startedAt
13302
+ };
13303
+ await input.backend.createAgencyRun(input.tenantId, "loop", loop.id, running, startedAt);
13304
+ let exitCode;
13305
+ let reason;
13306
+ try {
13307
+ const result = await input.run(loopJob(loop), runId);
13308
+ exitCode = result.exitCode;
13309
+ reason = result.reason ?? (exitCode === 0 ? "dispatched" : "target failed");
13310
+ } catch (error) {
13311
+ exitCode = 1;
13312
+ reason = error instanceof Error ? error.message : String(error);
13313
+ }
13314
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
13315
+ const succeeded = exitCode === 0;
13316
+ await input.backend.finishAgencyRun(
13317
+ input.tenantId,
13318
+ {
13319
+ ...running,
13320
+ status: succeeded ? "succeeded" : "failed",
13321
+ finishedAt,
13322
+ ...succeeded ? { output: { summary: reason } } : { error: reason }
13323
+ },
13324
+ finishedAt
13325
+ );
13326
+ const status = succeeded ? "dispatched" : "failed";
13327
+ await input.backend.finishLoopDispatch(input.tenantId, idempotencyKey, reservationId, status, finishedAt, runId);
13328
+ results.push({ loopId: loop.id, status, reason });
13329
+ }
13330
+ return results;
13331
+ }
14052
13332
  function selectRunnableLoops(loops, now, options) {
14053
13333
  return loops.filter(
14054
- (loop) => loop.enabled && loop.trigger.type === "schedule" && (!options.loopId || loop.id === options.loopId) && (options.force || dueSlot(loop, now) !== null)
13334
+ (loop) => loop.enabled && (!options.loopId || loop.id === options.loopId) && (options.force || loop.trigger.type === "schedule" && dueSlot(loop, now) !== null)
14055
13335
  );
14056
13336
  }
14057
13337
  function loopDispatchSlot(loop, now, force, nonce) {
@@ -14093,21 +13373,22 @@ function loopJob(loop) {
14093
13373
  );
14094
13374
  return loop.target.kind === "workflow" ? { workflow: loop.target.id, cliArgs, flavor: "scheduled" } : { capability: loop.target.id, cliArgs, flavor: "scheduled" };
14095
13375
  }
14096
- function repositoryTenant2(config) {
13376
+ function repositoryTenant(config) {
14097
13377
  const [envOwner, envRepo] = (process.env.GITHUB_REPOSITORY ?? "").split("/");
14098
13378
  const owner = config.github?.owner?.trim() || envOwner?.trim();
14099
13379
  const repo = config.github?.repo?.trim() || envRepo?.trim();
14100
13380
  return owner && repo ? `${owner}/${repo}` : null;
14101
13381
  }
14102
- var dispatchSimpleLoops;
14103
- var init_dispatchSimpleLoops = __esm({
14104
- "src/scripts/dispatchSimpleLoops.ts"() {
13382
+ var LOOP_DISPATCH_LEASE_MS, dispatchLoops;
13383
+ var init_dispatchLoops = __esm({
13384
+ "src/scripts/dispatchLoops.ts"() {
14105
13385
  "use strict";
14106
13386
  init_job();
14107
13387
  init_loopDefinitions();
14108
13388
  init_state_backend();
14109
- dispatchSimpleLoops = async (ctx) => {
14110
- const tenantId2 = repositoryTenant2(ctx.config);
13389
+ LOOP_DISPATCH_LEASE_MS = 6 * 60 * 60 * 1e3;
13390
+ dispatchLoops = async (ctx) => {
13391
+ const tenantId2 = repositoryTenant(ctx.config);
14111
13392
  if (!tenantId2) throw new Error("Repository identity is required for Loop dispatch");
14112
13393
  const now = /* @__PURE__ */ new Date();
14113
13394
  const force = ctx.data.jobForce === true;
@@ -14119,53 +13400,93 @@ var init_dispatchSimpleLoops = __esm({
14119
13400
  process.stdout.write(`\u2192 kody: Loop scheduler found ${due.length} runnable Loop(s)${force ? " (manual)" : ""}
14120
13401
  `);
14121
13402
  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), {
13403
+ const results = await dispatchLoopsWith({
13404
+ loops: due,
13405
+ tenantId: tenantId2,
13406
+ backend,
13407
+ now,
13408
+ force,
13409
+ nonce: randomUUID,
13410
+ run: (job, parentRunId) => runJob(job, {
14154
13411
  cwd: ctx.cwd,
14155
13412
  config: ctx.config,
14156
13413
  verbose: ctx.verbose,
14157
13414
  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
- }
13415
+ chain: false,
13416
+ preloadedData: { parentRunId }
13417
+ })
13418
+ });
14164
13419
  for (const result of results) {
14165
13420
  process.stdout.write(`\u2192 kody: Loop ${result.loopId} ${result.status}: ${result.reason}
14166
13421
  `);
14167
13422
  }
14168
- ctx.data.simpleLoopDispatchResults = results;
13423
+ ctx.data.loopDispatchResults = results;
13424
+ };
13425
+ }
13426
+ });
13427
+
13428
+ // src/jobIdentity.ts
13429
+ function stableJobKey(job) {
13430
+ const capability = job.workflow ?? job.capability ?? job.action;
13431
+ const implementation = job.implementation ?? capability ?? "unknown";
13432
+ if (job.flavor === "scheduled" && job.capability) return `scheduled:${job.capability}:${implementation}`;
13433
+ const target = typeof job.target === "number" ? job.target : targetFromCliArgs(job.cliArgs);
13434
+ const work = capability && implementation && implementation !== capability ? `${capability}:${implementation}` : capability ?? implementation;
13435
+ return target === void 0 ? `${job.flavor}:${work}` : `${job.flavor}:${work}:${target}`;
13436
+ }
13437
+ function targetFromCliArgs(cliArgs) {
13438
+ if (!cliArgs) return void 0;
13439
+ for (const key of ["issue", "pr", "target", "issue_number"]) {
13440
+ const value = cliArgs[key];
13441
+ if (typeof value === "number" && Number.isFinite(value)) return value;
13442
+ }
13443
+ return void 0;
13444
+ }
13445
+ var init_jobIdentity = __esm({
13446
+ "src/jobIdentity.ts"() {
13447
+ "use strict";
13448
+ }
13449
+ });
13450
+
13451
+ // src/scripts/dispatchNextTaskJob.ts
13452
+ function taskJobToJob(job, issueArg) {
13453
+ const target = typeof job.target === "number" ? job.target : typeof issueArg === "number" ? issueArg : void 0;
13454
+ return {
13455
+ capability: job.capability ?? job.implementation,
13456
+ implementation: job.implementation,
13457
+ ...job.reason ? { why: job.reason } : {},
13458
+ ...job.agent ? { agent: job.agent } : {},
13459
+ ...job.schedule ? { schedule: job.schedule } : {},
13460
+ ...typeof target === "number" ? { target, cliArgs: { issue: target } } : { cliArgs: {} },
13461
+ flavor: job.flavor ?? "instant"
13462
+ };
13463
+ }
13464
+ function isJob(input) {
13465
+ if (!input || typeof input !== "object" || Array.isArray(input)) return false;
13466
+ const job = input;
13467
+ 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));
13468
+ }
13469
+ var dispatchNextTaskJob;
13470
+ var init_dispatchNextTaskJob = __esm({
13471
+ "src/scripts/dispatchNextTaskJob.ts"() {
13472
+ "use strict";
13473
+ init_jobIdentity();
13474
+ init_state();
13475
+ dispatchNextTaskJob = async (ctx, profile) => {
13476
+ const state = ctx.data.taskState ?? emptyState();
13477
+ const ids = Array.isArray(ctx.data.plannedTaskJobIds) ? ctx.data.plannedTaskJobIds.filter((id) => typeof id === "string") : void 0;
13478
+ const next = nextPendingTaskJob(state, ids);
13479
+ ctx.skipAgent = true;
13480
+ if (!next) {
13481
+ ctx.output.exitCode = 0;
13482
+ ctx.output.reason = "all planned task jobs are complete";
13483
+ return;
13484
+ }
13485
+ const plannedJobs = Array.isArray(ctx.data.plannedTaskJobs) ? ctx.data.plannedTaskJobs.filter(isJob) : [];
13486
+ ctx.output.nextJob = plannedJobs.find((job) => stableJobKey(job) === next.id) ?? taskJobToJob(next, ctx.args.issue);
13487
+ if (typeof ctx.args.issue === "number") {
13488
+ ctx.output.afterNextJob = { action: profile.action ?? profile.name, cliArgs: { issue: ctx.args.issue } };
13489
+ }
14169
13490
  };
14170
13491
  }
14171
13492
  });
@@ -15892,9 +15213,9 @@ var init_kodyVariables = __esm({
15892
15213
  });
15893
15214
 
15894
15215
  // src/backendVault.ts
15895
- import { createDecipheriv, createHash as createHash6 } from "crypto";
15216
+ import { createDecipheriv, createHash as createHash5 } from "crypto";
15896
15217
  function cacheKey(owner, repo, masterKey) {
15897
- const keyHash = createHash6("sha256").update(masterKey).digest("hex").slice(0, 16);
15218
+ const keyHash = createHash5("sha256").update(masterKey).digest("hex").slice(0, 16);
15898
15219
  return `${owner}/${repo}:${keyHash}`.toLowerCase();
15899
15220
  }
15900
15221
  function decryptVault(payload, masterKey) {
@@ -16681,7 +16002,7 @@ var init_notifyTerminal = __esm({
16681
16002
  });
16682
16003
 
16683
16004
  // src/scripts/openAgencyModelReviewPr.ts
16684
- import { createHash as createHash7 } from "crypto";
16005
+ import { createHash as createHash6 } from "crypto";
16685
16006
  function parseAgencyModelProposal(raw) {
16686
16007
  const text2 = raw.trim();
16687
16008
  const jsonText = (text2.match(/^```(?:json)?\s*\n([\s\S]*?)\n```$/i)?.[1] ?? text2).trim();
@@ -16733,7 +16054,7 @@ function normalizeBundleFiles(bundle) {
16733
16054
  });
16734
16055
  }
16735
16056
  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);
16057
+ const digest = createHash6("sha256").update(JSON.stringify({ issueNumber, sourceLabel, title: bundle.title, files: normalizeBundleFiles(bundle) })).digest("hex").slice(0, 16);
16737
16058
  return `issue-${issueNumber}-${digest}`;
16738
16059
  }
16739
16060
  function isDryRun(ctx) {
@@ -19063,9 +18384,9 @@ var init_reviewFlow = __esm({
19063
18384
  });
19064
18385
 
19065
18386
  // src/scripts/previewBuildHelpers.ts
19066
- import { createDecipheriv as createDecipheriv2, createHash as createHash8, hkdfSync as hkdfSync2 } from "crypto";
18387
+ import { createDecipheriv as createDecipheriv2, createHash as createHash7, hkdfSync as hkdfSync2 } from "crypto";
19067
18388
  function shortHash(s) {
19068
- return createHash8("sha256").update(s).digest("hex").slice(0, 6);
18389
+ return createHash7("sha256").update(s).digest("hex").slice(0, 6);
19069
18390
  }
19070
18391
  function previewAppName(repo, pr) {
19071
18392
  const [owner, name] = repo.split("/");
@@ -19098,7 +18419,7 @@ function formatPreviewComment(args) {
19098
18419
  ].join("\n");
19099
18420
  }
19100
18421
  function defaultImageTag(repo, ref) {
19101
- return createHash8("sha256").update(`${repo}@${ref}`).digest("hex").slice(0, 12);
18422
+ return createHash7("sha256").update(`${repo}@${ref}`).digest("hex").slice(0, 12);
19102
18423
  }
19103
18424
  var init_previewBuildHelpers = __esm({
19104
18425
  "src/scripts/previewBuildHelpers.ts"() {
@@ -20985,12 +20306,11 @@ var init_scripts = __esm({
20985
20306
  init_diagMcp();
20986
20307
  init_discoverQaContext();
20987
20308
  init_dispatch();
20988
- init_dispatchAgencyLoops();
20989
20309
  init_dispatchCapabilityFileTicks();
20990
20310
  init_dispatchCapabilityTicks();
20991
20311
  init_dispatchClassified();
20312
+ init_dispatchLoops();
20992
20313
  init_dispatchNextTaskJob();
20993
- init_dispatchSimpleLoops();
20994
20314
  init_ensurePr();
20995
20315
  init_evaluateAgencyBoundaries();
20996
20316
  init_failOnceTaskJob();
@@ -21116,8 +20436,7 @@ var init_scripts = __esm({
21116
20436
  diagMcp,
21117
20437
  warmupMcp,
21118
20438
  dispatchCapabilityTicks,
21119
- dispatchAgencyLoops,
21120
- dispatchSimpleLoops,
20439
+ dispatchLoops,
21121
20440
  dispatchCapabilityFileTicks,
21122
20441
  planTaskJobs,
21123
20442
  dispatchNextTaskJob,
@@ -22582,6 +21901,7 @@ async function runJob(job, base) {
22582
21901
  updatedAt: startedAt,
22583
21902
  workflow: workflowIdentity,
22584
21903
  kodyRunId: valid.workflowRunId,
21904
+ parentRunId: typeof base.preloadedData?.parentRunId === "string" ? base.preloadedData.parentRunId : void 0,
22585
21905
  sourceType: "job"
22586
21906
  };
22587
21907
  const persistRun = Boolean(base.config && !base.skipConfig && hasStateBackendConfig());
@@ -24413,6 +23733,7 @@ import { createHash as createHash2 } from "crypto";
24413
23733
  import * as fs16 from "fs";
24414
23734
  import * as path17 from "path";
24415
23735
  var SLUG_RE = /^[a-z0-9][a-z0-9_-]{0,127}$/;
23736
+ var REPOSITORY_OWNED_NAMESPACES = ["loops"];
24416
23737
  function assertSafeDefinitionPath(filePath) {
24417
23738
  const segments = filePath.split("/");
24418
23739
  if (!filePath || filePath.startsWith("/") || filePath.includes("\\") || filePath.includes("\0") || segments.some((segment) => !segment || segment === "." || segment === "..")) {
@@ -24469,6 +23790,13 @@ function writeDefinition(root, kind, definition) {
24469
23790
  }
24470
23791
  writeBundle(path17.join(root, "capabilities", definition.slug), bundle);
24471
23792
  }
23793
+ function preserveRepositoryDefinitions(root, staging) {
23794
+ for (const namespace of REPOSITORY_OWNED_NAMESPACES) {
23795
+ const source = path17.join(root, namespace);
23796
+ if (!fs16.existsSync(source)) continue;
23797
+ fs16.cpSync(source, path17.join(staging, namespace), { recursive: true });
23798
+ }
23799
+ }
24472
23800
  async function hydrateDefinitions(options) {
24473
23801
  const root = path17.join(options.cwd, ".kody-engine", "definitions");
24474
23802
  const staging = `${root}.tmp-${process.pid}-${Date.now()}`;
@@ -24507,6 +23835,7 @@ async function hydrateDefinitions(options) {
24507
23835
  writeDefinition(staging, "asset", definition);
24508
23836
  versions[`asset:${definition.slug}`] = definition.version;
24509
23837
  }
23838
+ preserveRepositoryDefinitions(root, staging);
24510
23839
  const manifest = {
24511
23840
  schemaVersion: 1,
24512
23841
  tenantId: options.tenantId,
@@ -26841,7 +26170,7 @@ init_config();
26841
26170
  init_fetchRepoMcp();
26842
26171
 
26843
26172
  // src/servers/mcpHttpServer.ts
26844
- import { randomUUID as randomUUID3 } from "crypto";
26173
+ import { randomUUID as randomUUID2 } from "crypto";
26845
26174
  import { createServer as createServer4 } from "http";
26846
26175
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
26847
26176
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -26850,7 +26179,7 @@ function buildMcpHttpServer(opts) {
26850
26179
  const transports = /* @__PURE__ */ new Map();
26851
26180
  for (const route of opts.routes) {
26852
26181
  const transport = new StreamableHTTPServerTransport({
26853
- sessionIdGenerator: () => randomUUID3()
26182
+ sessionIdGenerator: () => randomUUID2()
26854
26183
  });
26855
26184
  transports.set(route.path, transport);
26856
26185
  routes.set(route.path, route.name);
@@ -27428,12 +26757,12 @@ import { createServer as createServer5 } from "http";
27428
26757
 
27429
26758
  // src/pool/agency-loop-tick.ts
27430
26759
  function normalizeRepositories(repositories) {
27431
- const unique2 = /* @__PURE__ */ new Set();
26760
+ const unique = /* @__PURE__ */ new Set();
27432
26761
  for (const raw of repositories) {
27433
26762
  const repo = raw.trim().toLowerCase();
27434
- if (/^[^/\s]+\/[^/\s]+$/.test(repo)) unique2.add(repo);
26763
+ if (/^[^/\s]+\/[^/\s]+$/.test(repo)) unique.add(repo);
27435
26764
  }
27436
- return [...unique2].sort();
26765
+ return [...unique].sort();
27437
26766
  }
27438
26767
  async function runAgencyLoopTick(deps) {
27439
26768
  const repositories = normalizeRepositories(await deps.discover());