@askdkc/kiokuko 0.2.3 → 0.2.4

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.
Files changed (107) hide show
  1. package/README.ja.md +25 -5
  2. package/README.ko.md +24 -5
  3. package/README.md +29 -5
  4. package/README.zh-CN.md +21 -4
  5. package/dist/agent-file/render.d.ts +1 -1
  6. package/dist/agent-file/render.d.ts.map +1 -1
  7. package/dist/agent-file/render.js +5 -3
  8. package/dist/agent-file/render.js.map +1 -1
  9. package/dist/akinator/agent-task.js +2 -2
  10. package/dist/akinator/agent-task.js.map +1 -1
  11. package/dist/akinator/capabilities.d.ts +5 -1
  12. package/dist/akinator/capabilities.d.ts.map +1 -1
  13. package/dist/akinator/capabilities.js +20 -3
  14. package/dist/akinator/capabilities.js.map +1 -1
  15. package/dist/commands/agent.d.ts.map +1 -1
  16. package/dist/commands/agent.js +36 -9
  17. package/dist/commands/agent.js.map +1 -1
  18. package/dist/commands/doctor.d.ts +1 -0
  19. package/dist/commands/doctor.d.ts.map +1 -1
  20. package/dist/commands/doctor.js +33 -0
  21. package/dist/commands/doctor.js.map +1 -1
  22. package/dist/commands/setup.d.ts.map +1 -1
  23. package/dist/commands/setup.js +2 -1
  24. package/dist/commands/setup.js.map +1 -1
  25. package/dist/commands/use.d.ts +1 -0
  26. package/dist/commands/use.d.ts.map +1 -1
  27. package/dist/commands/use.js +76 -16
  28. package/dist/commands/use.js.map +1 -1
  29. package/dist/enno-oduno/adapters.d.ts +4 -1
  30. package/dist/enno-oduno/adapters.d.ts.map +1 -1
  31. package/dist/enno-oduno/adapters.js +63 -14
  32. package/dist/enno-oduno/adapters.js.map +1 -1
  33. package/dist/enno-oduno/advisory.d.ts.map +1 -1
  34. package/dist/enno-oduno/advisory.js +133 -26
  35. package/dist/enno-oduno/advisory.js.map +1 -1
  36. package/dist/enno-oduno/confirmation.d.ts.map +1 -1
  37. package/dist/enno-oduno/confirmation.js +3 -0
  38. package/dist/enno-oduno/confirmation.js.map +1 -1
  39. package/dist/enno-oduno/directives.d.ts.map +1 -1
  40. package/dist/enno-oduno/directives.js +63 -34
  41. package/dist/enno-oduno/directives.js.map +1 -1
  42. package/dist/enno-oduno/experts.d.ts +1 -1
  43. package/dist/enno-oduno/experts.d.ts.map +1 -1
  44. package/dist/enno-oduno/experts.js +21 -7
  45. package/dist/enno-oduno/experts.js.map +1 -1
  46. package/dist/enno-oduno/instructions.d.ts +5 -4
  47. package/dist/enno-oduno/instructions.d.ts.map +1 -1
  48. package/dist/enno-oduno/instructions.js +4 -3
  49. package/dist/enno-oduno/instructions.js.map +1 -1
  50. package/dist/enno-oduno/plan-recovery.d.ts +15 -0
  51. package/dist/enno-oduno/plan-recovery.d.ts.map +1 -1
  52. package/dist/enno-oduno/plan-recovery.js +26 -0
  53. package/dist/enno-oduno/plan-recovery.js.map +1 -1
  54. package/dist/enno-oduno/repository-state.d.ts +8 -0
  55. package/dist/enno-oduno/repository-state.d.ts.map +1 -0
  56. package/dist/enno-oduno/repository-state.js +139 -0
  57. package/dist/enno-oduno/repository-state.js.map +1 -0
  58. package/dist/enno-oduno/role-runner.d.ts.map +1 -1
  59. package/dist/enno-oduno/role-runner.js +34 -1
  60. package/dist/enno-oduno/role-runner.js.map +1 -1
  61. package/dist/enno-oduno/sanitize.d.ts +17 -0
  62. package/dist/enno-oduno/sanitize.d.ts.map +1 -0
  63. package/dist/enno-oduno/sanitize.js +88 -0
  64. package/dist/enno-oduno/sanitize.js.map +1 -0
  65. package/dist/enno-oduno/schemas.d.ts +185 -36
  66. package/dist/enno-oduno/schemas.d.ts.map +1 -1
  67. package/dist/enno-oduno/schemas.js +172 -46
  68. package/dist/enno-oduno/schemas.js.map +1 -1
  69. package/dist/enno-oduno/service.d.ts +2 -1
  70. package/dist/enno-oduno/service.d.ts.map +1 -1
  71. package/dist/enno-oduno/service.js +310 -137
  72. package/dist/enno-oduno/service.js.map +1 -1
  73. package/dist/enno-oduno/store.d.ts +37 -4
  74. package/dist/enno-oduno/store.d.ts.map +1 -1
  75. package/dist/enno-oduno/store.js +343 -36
  76. package/dist/enno-oduno/store.js.map +1 -1
  77. package/dist/enno-oduno/types.d.ts +55 -2
  78. package/dist/enno-oduno/types.d.ts.map +1 -1
  79. package/dist/enno-oduno/types.js +1 -0
  80. package/dist/enno-oduno/types.js.map +1 -1
  81. package/dist/enno-oduno/validation-errors.d.ts +92 -0
  82. package/dist/enno-oduno/validation-errors.d.ts.map +1 -0
  83. package/dist/enno-oduno/validation-errors.js +151 -0
  84. package/dist/enno-oduno/validation-errors.js.map +1 -0
  85. package/dist/enno-oduno/verifier.d.ts +1 -0
  86. package/dist/enno-oduno/verifier.d.ts.map +1 -1
  87. package/dist/enno-oduno/verifier.js +70 -12
  88. package/dist/enno-oduno/verifier.js.map +1 -1
  89. package/dist/mcp/server.d.ts.map +1 -1
  90. package/dist/mcp/server.js +70 -16
  91. package/dist/mcp/server.js.map +1 -1
  92. package/dist/server/routes/agent-capability-gate.js +2 -2
  93. package/dist/server/routes/agent-capability-gate.js.map +1 -1
  94. package/dist/setup/render.js +1 -1
  95. package/dist/setup/render.js.map +1 -1
  96. package/dist/setup/standard-skills.d.ts +12 -5
  97. package/dist/setup/standard-skills.d.ts.map +1 -1
  98. package/dist/setup/standard-skills.js +12 -2
  99. package/dist/setup/standard-skills.js.map +1 -1
  100. package/migrations/019_enno_execution_integrity.sql +149 -0
  101. package/package.json +1 -1
  102. package/skills/kiokuko-enno-oduno/SKILL.md +19 -8
  103. package/skills/kiokuko-single-purpose-functions/SKILL.md +18 -10
  104. package/skills/kiokuko-single-purpose-functions/references/problem-shaping-and-language.md +84 -0
  105. package/skills/kiokuko-ui-design-soul/SKILL.md +5 -1
  106. package/skills/memory-reasoning/SKILL.md +52 -0
  107. package/templates/AGENTS.md +6 -4
@@ -12,17 +12,20 @@ import { canonicalContentHash, canonicalJson } from '../serialization/validate.j
12
12
  import { sanitizeJson } from '../security/sanitize.js';
13
13
  import { LedgerStore } from '../ledger/store.js';
14
14
  import { directiveForIntake, directiveForRun } from './directives.js';
15
- import { advisoryDirectiveForSnapshot, advisoryInputDigest, advisoryPhaseForStatus, normalizeAdvisoryContributions } from './advisory.js';
15
+ import { advisoryContextForSnapshot, advisoryInputDigest, advisoryPhaseForStatus, normalizeAdvisoryContributions } from './advisory.js';
16
16
  import { createAdvisoryRoundInTransaction, ensureAdvisoryRoundConsumedInTransaction, readAdvisoryRound, readSubmittedAdvisoryRound, } from './advisory-store.js';
17
17
  import { assertWorkPlanExpertCoverage } from './experts.js';
18
18
  import { buildEnnoRequestHandoff } from './handoff.js';
19
19
  import { identifyEnnoClientKind } from './harness.js';
20
- import { assertContractVerifierCwds, parseEnnoAnswer, parseAdviceSubmission, parseFinishRequest, parseIdealSubmission, parseMeditationSubmission, parseOdunoIdeal, parseOdunoMeditation, parsePlanSubmission, parseVerificationPrepare, parseWorkReport, } from './schemas.js';
20
+ import { assertContractVerifierCwds, parseEnnoAnswer, parseAdviceSubmission, parseFinishRequest, parseIdealSubmission, parseMeditationSubmission, parsePlanSubmission, parseVerificationPrepare, parseWorkReport, } from './schemas.js';
21
21
  import { completeRequiredSkillList, createSkillSetEntries, orderedUniqueSkillNames, unavailableRequiredSkills, } from './skills.js';
22
- import { appendEnnoEventInTransaction, completeOperationInTransaction, createEnnoDraft, finishVerifierRunsInTransaction, readEnnoSnapshot, readFreshFinalVerifierResults, readOperationReceipt, replaceWorkUnitsInTransaction, setWorkUnitStatusInTransaction, startOperationInTransaction, startVerifierRunsInTransaction, terminalizeLedgerRunInTransaction, updateContractInTransaction, } from './store.js';
22
+ import { appendEnnoEventInTransaction, assertExecutionLeaseInTransaction, claimExecutionLeaseInTransaction, completeOperationInTransaction, createEnnoDraft, finishVerifierRunsInTransaction, readEnnoSnapshot, readFreshFinalVerifierResults, readOperationReceipt, releaseExecutionLeaseInTransaction, renewExecutionLeaseInTransaction, resolveEnnoIdentity, replaceWorkUnitsInTransaction, setWorkUnitStatusInTransaction, startOperationInTransaction, startVerifierRunsInTransaction, operationLeaseMsForVerifiers, terminalizeLedgerRunInTransaction, updateContractInTransaction, } from './store.js';
23
23
  import { ENNO_APPLICABLE_TASK_TYPES, ENNO_MAX_EXTERNAL_SKILLS, ENNO_MAX_TOTAL_SKILL_QUERIES, ENNO_PROVENANCE_KEYS, } from './types.js';
24
24
  import { runVerifiers } from './verifier.js';
25
- import { planStartRecoveryError } from './plan-recovery.js';
25
+ import { planStartRecoveryBlocker, planStartRecoveryError, planStartRecoveryReasonFromBlocker, } from './plan-recovery.js';
26
+ import { ennoValidationError } from './validation-errors.js';
27
+ import { captureRepositoryState } from './repository-state.js';
28
+ import { sanitizeEnnoAnswer, sanitizeFinishRequest, sanitizeIdealSubmission, sanitizeMeditationSubmission, sanitizePlanSubmission, sanitizeWorkReport, } from './sanitize.js';
26
29
  import { STANDARD_FUNCTION_SKILL_NAME, STANDARD_SOUL_SKILL_NAME, STANDARD_UI_SKILL_NAME, } from '../setup/standard-skills.js';
27
30
  function stateForSnapshot(snapshot) {
28
31
  let nextAction;
@@ -56,11 +59,13 @@ function stateForSnapshot(snapshot) {
56
59
  identified: snapshot.clientKind !== null,
57
60
  },
58
61
  contractRevision: snapshot.revision,
62
+ routeEpoch: snapshot.routeEpoch ?? 0,
59
63
  ideal: snapshot.ideal,
60
64
  meditation: snapshot.meditation,
61
65
  currentRole: directive?.role ?? null,
62
66
  directive,
63
67
  nextAction,
68
+ advisoryPhaseState: snapshot.advisoryPhaseState ?? { state: 'not_started' },
64
69
  };
65
70
  }
66
71
  function intakeEnnoState(input) {
@@ -76,11 +81,13 @@ function intakeEnnoState(input) {
76
81
  identified: input.clientKind !== null,
77
82
  },
78
83
  contractRevision: null,
84
+ routeEpoch: null,
79
85
  ideal: null,
80
86
  meditation: null,
81
87
  currentRole: 'enno-oduno',
82
88
  directive,
83
89
  nextAction: 'answer_intake',
90
+ advisoryPhaseState: { state: 'not_started' },
84
91
  };
85
92
  }
86
93
  export function inapplicableEnnoState() {
@@ -90,11 +97,13 @@ export function inapplicableEnnoState() {
90
97
  orchestrationId: null,
91
98
  clientBinding: null,
92
99
  contractRevision: null,
100
+ routeEpoch: null,
93
101
  ideal: null,
94
102
  meditation: null,
95
103
  currentRole: null,
96
104
  directive: null,
97
105
  nextAction: 'complete',
106
+ advisoryPhaseState: { state: 'not_started' },
98
107
  };
99
108
  }
100
109
  export function ennoStateForPreparedTask(database, prepared, client) {
@@ -143,20 +152,27 @@ export function ennoStateForPreparedTask(database, prepared, client) {
143
152
  return stateForSnapshot(snapshot);
144
153
  }
145
154
  function operationIdentity(operation, idempotencyKey, input) {
155
+ const semanticInput = typeof input === 'object' && input !== null && !Array.isArray(input)
156
+ ? Object.fromEntries(Object.entries(input)
157
+ .filter(([key]) => !['workspace', 'orchestrationId', 'resumeToken', 'idempotencyKey'].includes(key)))
158
+ : input;
159
+ const requestDigest = canonicalContentHash({ version: 1, operation, input: semanticInput });
160
+ const legacyRequestDigest = canonicalContentHash({ version: 1, operation, input });
146
161
  return {
147
162
  operation,
148
163
  idempotencyKey,
149
- requestDigest: canonicalContentHash({ version: 1, operation, input }),
164
+ requestDigest,
165
+ ...(legacyRequestDigest === requestDigest ? {} : { legacyRequestDigests: [legacyRequestDigest] }),
150
166
  };
151
167
  }
152
168
  function advisoryContextForSubmission(snapshot, phase) {
153
- const directive = advisoryDirectiveForSnapshot(snapshot);
154
- if (directive === undefined || directive.phase !== phase) {
169
+ if (advisoryPhaseForStatus(snapshot.status) !== phase
170
+ || (phase === 'final_review' && !snapshot.finalEvidenceReady)) {
155
171
  throw new KiokukoError('CONFLICT', 'Advisory round is not valid for the current Enno phase');
156
172
  }
157
- return directive.context;
173
+ return advisoryContextForSnapshot(snapshot, phase);
158
174
  }
159
- function assertAdvisoryRoundInput(snapshot, phase, mutationRevision, context, inputDigest) {
175
+ function assertAdvisoryRoundInput(snapshot, phase, mutationRevision, context, inputDigest, validationOperation) {
160
176
  if (snapshot.mutationRevision !== mutationRevision) {
161
177
  throw new KiokukoError('CONFLICT', 'Enno mutation revision changed');
162
178
  }
@@ -171,11 +187,18 @@ function assertAdvisoryRoundInput(snapshot, phase, mutationRevision, context, in
171
187
  allowlistedContext: context,
172
188
  });
173
189
  if (expectedDigest !== inputDigest) {
190
+ if (validationOperation !== undefined) {
191
+ throw ennoValidationError(validationOperation, [{
192
+ path: ['advisoryRoundDigest'],
193
+ reasonCode: 'advisory_digest_stale',
194
+ }], 'refresh_state');
195
+ }
174
196
  throw new KiokukoError('CONFLICT', 'Advisory round digest does not match the current contract');
175
197
  }
176
198
  }
177
199
  function requireAdvisoryRound(database, snapshot, phase, mutationRevision, context, inputDigest) {
178
- assertAdvisoryRoundInput(snapshot, phase, mutationRevision, context, inputDigest);
200
+ const operation = phase === 'planning' ? 'plan_submit' : phase === 'ideal' ? 'ideal_submit' : 'finish';
201
+ assertAdvisoryRoundInput(snapshot, phase, mutationRevision, context, inputDigest, operation);
179
202
  const round = readAdvisoryRound(database, {
180
203
  runId: snapshot.runId,
181
204
  contractRevision: snapshot.revision,
@@ -186,21 +209,44 @@ function requireAdvisoryRound(database, snapshot, phase, mutationRevision, conte
186
209
  if (round === undefined)
187
210
  throw new KiokukoError('CONFLICT', 'Required advisory round was not submitted');
188
211
  }
189
- function assertDispositionCoversRound(round, dispositions) {
212
+ function assertDispositionCoversRound(operation, round, dispositions) {
190
213
  const expected = new Set(round.contributions.map((contribution) => contribution.slotId));
191
214
  const covered = new Set();
192
- for (const disposition of dispositions) {
215
+ for (const [index, disposition] of dispositions.entries()) {
193
216
  if (!expected.has(disposition.slotId)) {
194
- throw new KiokukoError('VALIDATION_ERROR', 'advisoryDisposition references a slot outside the advisory round');
217
+ throw ennoValidationError(operation, [{
218
+ path: ['advisoryDisposition', index, 'slotId'],
219
+ reasonCode: 'advisory_slot_missing',
220
+ expected: { requiredSlotIds: [...expected] },
221
+ }]);
195
222
  }
196
223
  if (covered.has(disposition.slotId)) {
197
- throw new KiokukoError('VALIDATION_ERROR', 'advisoryDisposition must provide at most one entry per slot');
224
+ throw ennoValidationError(operation, [{
225
+ path: ['advisoryDisposition', index, 'slotId'],
226
+ reasonCode: 'advisory_slot_duplicate',
227
+ expected: { requiredSlotIds: [...expected] },
228
+ }]);
229
+ }
230
+ const contribution = round.contributions.find((item) => item.slotId === disposition.slotId);
231
+ const compatible = contribution.outcome === 'completed'
232
+ ? disposition.disposition === 'adopted' || disposition.disposition === 'not_adopted'
233
+ : disposition.disposition === 'unavailable';
234
+ if (!compatible) {
235
+ throw ennoValidationError(operation, [{
236
+ path: ['advisoryDisposition', index, 'disposition'],
237
+ reasonCode: 'invalid_enum',
238
+ expected: { allowedValues: contribution.outcome === 'completed' ? ['adopted', 'not_adopted'] : ['unavailable'] },
239
+ }]);
198
240
  }
199
241
  covered.add(disposition.slotId);
200
242
  }
201
243
  for (const slotId of expected) {
202
244
  if (!covered.has(slotId)) {
203
- throw new KiokukoError('VALIDATION_ERROR', 'advisoryDisposition must cover every slot in the advisory round');
245
+ throw ennoValidationError(operation, [{
246
+ path: ['advisoryDisposition'],
247
+ reasonCode: 'advisory_slot_missing',
248
+ expected: { requiredSlotIds: [...expected] },
249
+ }]);
204
250
  }
205
251
  }
206
252
  }
@@ -213,18 +259,36 @@ function consumeAdvisoryRoundIfPresent(database, snapshot, phase, mutationRevisi
213
259
  phase,
214
260
  });
215
261
  if (submitted !== undefined) {
216
- throw new KiokukoError('CONFLICT', 'Advisory round was submitted; pass its advisory digest and disposition before advancing the phase');
262
+ throw ennoValidationError(phase === 'planning' ? 'plan_submit' : phase === 'ideal' ? 'ideal_submit' : 'finish', [{
263
+ path: ['advisoryRoundDigest'],
264
+ reasonCode: 'advisory_consumption_required',
265
+ expected: { requiredSlotIds: submitted.contributions.map((item) => item.slotId) },
266
+ }]);
217
267
  }
218
268
  return undefined;
219
269
  }
220
270
  if (context === undefined) {
221
271
  throw new KiokukoError('CONFLICT', 'Advisory round is not valid for the current Enno phase');
222
272
  }
223
- assertAdvisoryRoundInput(snapshot, phase, mutationRevision, context, inputDigest);
273
+ const operation = phase === 'planning' ? 'plan_submit' : phase === 'ideal' ? 'ideal_submit' : 'finish';
274
+ assertAdvisoryRoundInput(snapshot, phase, mutationRevision, context, inputDigest, operation);
224
275
  const dispositions = disposition ?? [];
225
276
  if (dispositions.length === 0) {
226
- throw new KiokukoError('VALIDATION_ERROR', 'advisoryDisposition is required when consuming an advisory round');
277
+ throw ennoValidationError(phase === 'planning' ? 'plan_submit' : phase === 'ideal' ? 'ideal_submit' : 'finish', [{
278
+ path: ['advisoryDisposition'],
279
+ reasonCode: 'advisory_digest_requires_disposition',
280
+ }]);
227
281
  }
282
+ const pendingRound = readAdvisoryRound(database, {
283
+ runId: snapshot.runId,
284
+ contractRevision: snapshot.revision,
285
+ mutationRevision,
286
+ phase,
287
+ inputDigest,
288
+ });
289
+ if (pendingRound === undefined)
290
+ throw new KiokukoError('CONFLICT', 'Required advisory round was not submitted');
291
+ assertDispositionCoversRound(operation, pendingRound, dispositions);
228
292
  const round = ensureAdvisoryRoundConsumedInTransaction(database, {
229
293
  runId: snapshot.runId,
230
294
  contractRevision: snapshot.revision,
@@ -232,7 +296,6 @@ function consumeAdvisoryRoundIfPresent(database, snapshot, phase, mutationRevisi
232
296
  phase,
233
297
  inputDigest,
234
298
  });
235
- assertDispositionCoversRound(round, dispositions);
236
299
  appendEnnoEventInTransaction(database, snapshot.runId, 'enno.advice_disposition', 'enno-oduno', 'recorded', {
237
300
  phase,
238
301
  contractRevision: snapshot.revision,
@@ -243,12 +306,16 @@ function consumeAdvisoryRoundIfPresent(database, snapshot, phase, mutationRevisi
243
306
  return round;
244
307
  }
245
308
  export function submitEnnoAdvice(database, rawInput) {
246
- const input = parseAdviceSubmission(rawInput);
309
+ const parsedInput = parseAdviceSubmission(rawInput);
310
+ const input = {
311
+ ...parsedInput,
312
+ contributions: normalizeAdvisoryContributions(parsedInput.phase, parsedInput.contributions),
313
+ };
314
+ const before = readEnnoSnapshot(database, identity(database, input));
247
315
  const operation = operationIdentity('advice_submit', input.idempotencyKey, input);
248
316
  const replay = readOperationReceipt(database, input.runId, operation);
249
317
  if (replay !== undefined)
250
318
  return replay;
251
- const before = readEnnoSnapshot(database, identity(input));
252
319
  const expectedPhase = advisoryPhaseForStatus(before.status);
253
320
  if (expectedPhase !== input.phase)
254
321
  throw new KiokukoError('CONFLICT', 'Advisory phase does not match the current Enno state');
@@ -263,12 +330,12 @@ export function submitEnnoAdvice(database, rawInput) {
263
330
  const replayed = readOperationReceipt(database, input.runId, operation);
264
331
  if (replayed !== undefined)
265
332
  return replayed;
266
- const current = readEnnoSnapshot(database, identity(input));
333
+ const current = readEnnoSnapshot(database, identity(database, input));
267
334
  assertExpected(current, input.expectedRevision, [
268
335
  input.phase === 'ideal' ? 'oduno_ideal' : input.phase === 'planning' ? 'zenki_planning' : 'enno_verifying',
269
336
  ]);
270
337
  assertAdvisoryRoundInput(current, input.phase, input.mutationRevision, context, inputDigest);
271
- startOperationInTransaction(database, input.runId, operation);
338
+ const operationOwner = startOperationInTransaction(database, input.runId, operation);
272
339
  const existing = readAdvisoryRound(database, {
273
340
  runId: input.runId,
274
341
  contractRevision: current.revision,
@@ -301,17 +368,16 @@ export function submitEnnoAdvice(database, rawInput) {
301
368
  inputDigest,
302
369
  degraded: round.degraded,
303
370
  });
304
- const response = { ennoOduno: stateForSnapshot(current), advisoryRound: round };
305
- completeOperationInTransaction(database, input.runId, operation, response);
371
+ const response = {
372
+ ennoOduno: stateForSnapshot(readEnnoSnapshot(database, identity(database, input))),
373
+ advisoryRound: round,
374
+ };
375
+ completeOperationInTransaction(database, input.runId, operation, operationOwner, response);
306
376
  return response;
307
377
  });
308
378
  }
309
- function identity(input) {
310
- return {
311
- runId: input.runId,
312
- workspace: input.workspace,
313
- orchestrationId: input.orchestrationId,
314
- };
379
+ function identity(database, input) {
380
+ return resolveEnnoIdentity(database, input);
315
381
  }
316
382
  function assertExpected(snapshot, expectedRevision, statuses) {
317
383
  if (snapshot.revision !== expectedRevision)
@@ -328,7 +394,7 @@ function startFirstReadyUnit(database, snapshot) {
328
394
  const next = firstReadyUnit(snapshot);
329
395
  if (next === undefined) {
330
396
  if (snapshot.workUnits.every((unit) => unit.status === 'completed'))
331
- return;
397
+ return undefined;
332
398
  throw new KiokukoError('CONFLICT', 'Enno WorkPlan dependencies cannot advance');
333
399
  }
334
400
  setWorkUnitStatusInTransaction(database, {
@@ -342,6 +408,12 @@ function startFirstReadyUnit(database, snapshot) {
342
408
  workUnitId: next.workUnit.id,
343
409
  contractRevision: snapshot.revision,
344
410
  });
411
+ if (snapshot.clientKind === null || snapshot.clientSessionId === null)
412
+ return undefined;
413
+ return claimExecutionLeaseInTransaction(database, snapshot, next.workUnit.id, {
414
+ clientKind: snapshot.clientKind,
415
+ sessionId: snapshot.clientSessionId,
416
+ });
345
417
  }
346
418
  function requiresConfirmation(provenance) {
347
419
  return ENNO_PROVENANCE_KEYS.some((key) => provenance[key] !== 'explicit_user');
@@ -426,7 +498,7 @@ async function discoverZenkiSkills(database, snapshot, plan, dependencies) {
426
498
  return claimed.summary;
427
499
  if (claimed.queryBudget === 0 || claimed.selectionBudget === 0) {
428
500
  return completeAgentTaskSkillDiscoveryAttempt(database, attempt, emptyDiscovery(intake.mode), () => {
429
- assertExpected(readEnnoSnapshot(database, identity(plan)), plan.expectedRevision, ['zenki_planning']);
501
+ assertExpected(readEnnoSnapshot(database, identity(database, plan)), plan.expectedRevision, ['zenki_planning']);
430
502
  });
431
503
  }
432
504
  try {
@@ -440,10 +512,10 @@ async function discoverZenkiSkills(database, snapshot, plan, dependencies) {
440
512
  ...(dependencies.fetchImpl === undefined ? {} : { fetchImpl: dependencies.fetchImpl }),
441
513
  }, {
442
514
  ...(dependencies.fetchImpl === undefined ? {} : { fetchImpl: dependencies.fetchImpl }),
443
- assertBeforePersist: () => assertExpected(readEnnoSnapshot(database, identity(plan)), plan.expectedRevision, ['zenki_planning']),
515
+ assertBeforePersist: () => assertExpected(readEnnoSnapshot(database, identity(database, plan)), plan.expectedRevision, ['zenki_planning']),
444
516
  });
445
517
  return completeAgentTaskSkillDiscoveryAttempt(database, attempt, summary, () => {
446
- assertExpected(readEnnoSnapshot(database, identity(plan)), plan.expectedRevision, ['zenki_planning']);
518
+ assertExpected(readEnnoSnapshot(database, identity(database, plan)), plan.expectedRevision, ['zenki_planning']);
447
519
  });
448
520
  }
449
521
  catch (error) {
@@ -451,13 +523,11 @@ async function discoverZenkiSkills(database, snapshot, plan, dependencies) {
451
523
  }
452
524
  }
453
525
  function planChangesCode(plan, taskType) {
454
- return taskType !== 'review'
455
- || plan.skillRequirements.some((skill) => skill.purposes.includes('implementation'))
456
- || plan.workPlan.units.some((unit) => unit.skillNames.includes('kiokuko-single-purpose-functions'));
526
+ return plan.workPlan.units.some((unit) => unit.routes.includes('code') || unit.routes.includes('ui'))
527
+ || (taskType !== 'review' && plan.skillRequirements.some((skill) => skill.purposes.includes('implementation')));
457
528
  }
458
529
  function planHasUi(plan) {
459
- return plan.skillRequirements.some((skill) => skill.purposes.includes('ui'))
460
- || plan.workPlan.units.some((unit) => unit.skillNames.includes('kiokuko-ui-design-soul'));
530
+ return plan.workPlan.units.some((unit) => unit.routes.includes('ui'));
461
531
  }
462
532
  function assertIdealSkillCoverage(snapshot, ideal) {
463
533
  const expected = snapshot.contract.skillSet.intakeDiscovery.selected.map((skill) => skill.name);
@@ -466,26 +536,43 @@ function assertIdealSkillCoverage(snapshot, ideal) {
466
536
  }
467
537
  const actual = ideal.skillContributions.map((contribution) => contribution.skillName);
468
538
  if (actual.length !== expected.length || expected.some((name) => !actual.includes(name))) {
469
- throw new KiokukoError('VALIDATION_ERROR', 'Oduno ideal must account for every Akinator-discovered Skill exactly once');
539
+ throw ennoValidationError('ideal_submit', [{
540
+ path: ['ideal', 'skillContributions'],
541
+ reasonCode: actual.length < expected.length ? 'too_few_items' : 'undeclared_skill',
542
+ ...(actual.length < expected.length ? { expected: { minItems: expected.length } } : {}),
543
+ }]);
470
544
  }
471
545
  }
472
- function assertPlanCapabilityCatalog(database, input) {
473
- if (input.capabilities === undefined) {
474
- throw planStartRecoveryError('environment_information_missing');
475
- }
476
- const run = new LedgerStore(database).readRun(input.runId, input.workspace);
546
+ function planCapabilityRecoveryReason(database, input, workspace) {
547
+ if (input.capabilities === undefined)
548
+ return 'environment_information_missing';
549
+ const run = new LedgerStore(database).readRun(input.runId, workspace);
477
550
  if (run === undefined)
478
551
  throw new KiokukoError('INTEGRITY_ERROR', 'Enno ledger run disappeared during plan validation');
479
552
  try {
480
553
  assertCapabilityCatalogBinding(run.metadata, input.capabilities);
554
+ return null;
481
555
  }
482
556
  catch (error) {
483
557
  if (error instanceof KiokukoError && error.code === 'CONFLICT') {
484
- throw planStartRecoveryError('environment_changed');
558
+ return 'environment_changed';
485
559
  }
486
560
  throw error;
487
561
  }
488
562
  }
563
+ function pausePlanStartRecovery(database, snapshot, reason) {
564
+ withImmediateTransaction(database, () => {
565
+ const updated = database.prepare(`
566
+ UPDATE enno_contracts
567
+ SET blocker = ?, updated_at = ?
568
+ WHERE run_id = ? AND revision = ? AND status = 'zenki_planning'
569
+ RETURNING run_id AS runId
570
+ `).get(planStartRecoveryBlocker(reason), new Date().toISOString(), snapshot.runId, snapshot.revision);
571
+ if (updated?.runId !== snapshot.runId)
572
+ throw new KiokukoError('CONFLICT', 'Enno plan state changed concurrently');
573
+ });
574
+ throw planStartRecoveryError(reason);
575
+ }
489
576
  function endedBeforeWorkBecausePlanCatalogWasLost(database, snapshot, capabilities) {
490
577
  if (capabilities === undefined
491
578
  || snapshot.status !== 'blocked'
@@ -513,30 +600,28 @@ function endedBeforeWorkBecausePlanCatalogWasLost(database, snapshot, capabiliti
513
600
  throw error;
514
601
  }
515
602
  }
516
- function sanitizedIdeal(ideal, repositoryRoot) {
517
- return parseOdunoIdeal(sanitizeJson(ideal, { workspace: repositoryRoot }).value);
518
- }
519
603
  export function submitOdunoIdeal(database, rawInput) {
520
- const input = parseIdealSubmission(rawInput);
604
+ const parsedInput = parseIdealSubmission(rawInput);
605
+ const before = readEnnoSnapshot(database, identity(database, parsedInput));
606
+ const input = sanitizeIdealSubmission(parsedInput, before.repositoryRoot);
521
607
  const operation = operationIdentity('ideal_submit', input.idempotencyKey, input);
522
608
  const replay = readOperationReceipt(database, input.runId, operation);
523
609
  if (replay !== undefined)
524
610
  return replay;
525
- const before = readEnnoSnapshot(database, identity(input));
526
611
  assertExpected(before, input.expectedRevision, ['oduno_ideal']);
527
612
  if (input.advisoryRoundDigest !== undefined) {
528
613
  requireAdvisoryRound(database, before, 'ideal', before.mutationRevision, advisoryContextForSubmission(before, 'ideal'), input.advisoryRoundDigest);
529
614
  }
530
- const ideal = sanitizedIdeal(input.ideal, before.repositoryRoot);
615
+ const ideal = input.ideal;
531
616
  assertIdealSkillCoverage(before, ideal);
532
617
  return withImmediateTransaction(database, () => {
533
618
  const replayed = readOperationReceipt(database, input.runId, operation);
534
619
  if (replayed !== undefined)
535
620
  return replayed;
536
- const current = readEnnoSnapshot(database, identity(input));
621
+ const current = readEnnoSnapshot(database, identity(database, input));
537
622
  assertExpected(current, input.expectedRevision, ['oduno_ideal']);
538
623
  assertIdealSkillCoverage(current, ideal);
539
- startOperationInTransaction(database, input.runId, operation);
624
+ const operationOwner = startOperationInTransaction(database, input.runId, operation);
540
625
  const advisoryRound = consumeAdvisoryRoundIfPresent(database, current, 'ideal', current.mutationRevision, advisoryContextForSubmission(current, 'ideal'), input.advisoryRoundDigest, input.advisoryDisposition);
541
626
  updateContractInTransaction(database, current, {
542
627
  contract: current.contract,
@@ -551,16 +636,17 @@ export function submitOdunoIdeal(database, rawInput) {
551
636
  successSignalCount: ideal.successSignals.length,
552
637
  });
553
638
  const response = {
554
- ennoOduno: stateForSnapshot(readEnnoSnapshot(database, identity(input))),
639
+ ennoOduno: stateForSnapshot(readEnnoSnapshot(database, identity(database, input))),
555
640
  ...(advisoryRound === undefined ? {} : { advisoryRound }),
556
641
  };
557
- completeOperationInTransaction(database, input.runId, operation, response);
642
+ completeOperationInTransaction(database, input.runId, operation, operationOwner, response);
558
643
  return response;
559
644
  });
560
645
  }
561
646
  export async function submitEnnoPlan(database, rawInput, dependencies = {}) {
562
- const input = parsePlanSubmission(rawInput);
563
- const current = readEnnoSnapshot(database, identity(input));
647
+ const parsedInput = parsePlanSubmission(rawInput);
648
+ const current = readEnnoSnapshot(database, identity(database, parsedInput));
649
+ const input = sanitizePlanSubmission(parsedInput, current.repositoryRoot);
564
650
  if (endedBeforeWorkBecausePlanCatalogWasLost(database, current, input.capabilities)) {
565
651
  throw planStartRecoveryError('previous_attempt_ended');
566
652
  }
@@ -570,13 +656,33 @@ export async function submitEnnoPlan(database, rawInput, dependencies = {}) {
570
656
  return replay;
571
657
  const before = current;
572
658
  assertExpected(before, input.expectedRevision, ['zenki_planning']);
573
- assertPlanCapabilityCatalog(database, input);
659
+ const pendingRecovery = planStartRecoveryReasonFromBlocker(before.blocker);
660
+ if (pendingRecovery === 'environment_information_missing') {
661
+ if (input.recoveryAction === undefined)
662
+ throw planStartRecoveryError(pendingRecovery);
663
+ }
664
+ else if (pendingRecovery !== null) {
665
+ throw planStartRecoveryError(pendingRecovery);
666
+ }
667
+ else if (input.recoveryAction !== undefined) {
668
+ throw new KiokukoError('CONFLICT', 'Enno plan has no pending same-run recovery choice');
669
+ }
670
+ if (input.maxAttempts <= before.attempts) {
671
+ throw ennoValidationError('plan_submit', [{
672
+ path: ['maxAttempts'],
673
+ reasonCode: 'too_few_items',
674
+ expected: { minItems: before.attempts + 1 },
675
+ }]);
676
+ }
677
+ const recoveryReason = planCapabilityRecoveryReason(database, input, before.workspace);
678
+ if (recoveryReason !== null)
679
+ pausePlanStartRecovery(database, before, recoveryReason);
574
680
  if (input.advisoryRoundDigest !== undefined) {
575
681
  requireAdvisoryRound(database, before, 'planning', before.mutationRevision, advisoryContextForSubmission(before, 'planning'), input.advisoryRoundDigest);
576
682
  }
577
683
  const includesCodeChanges = planChangesCode(input, before.taskType);
578
684
  const includesUiWork = planHasUi(input);
579
- assertWorkPlanExpertCoverage(input.workPlan, { includesCodeChanges, includesUiWork });
685
+ assertWorkPlanExpertCoverage(input.workPlan);
580
686
  assertContractVerifierCwds(before.repositoryRoot, {
581
687
  workPlan: input.workPlan,
582
688
  finalVerifiers: input.finalVerifiers,
@@ -593,11 +699,6 @@ export async function submitEnnoPlan(database, rawInput, dependencies = {}) {
593
699
  discoveries: [before.contract.skillSet.intakeDiscovery, zenkiDiscovery],
594
700
  });
595
701
  const unavailable = unavailableRequiredSkills(entries);
596
- const mandatoryWorkSkills = [
597
- STANDARD_SOUL_SKILL_NAME,
598
- ...(includesCodeChanges ? [STANDARD_FUNCTION_SKILL_NAME] : []),
599
- ...(includesUiWork ? [STANDARD_UI_SKILL_NAME] : []),
600
- ];
601
702
  const nextRevision = input.expectedRevision + 1;
602
703
  const contract = {
603
704
  revision: nextRevision,
@@ -610,7 +711,12 @@ export async function submitEnnoPlan(database, rawInput, dependencies = {}) {
610
711
  ...unit,
611
712
  scope: [...unit.scope],
612
713
  dependencies: [...unit.dependencies],
613
- skillNames: orderedUniqueSkillNames(mandatoryWorkSkills, unit.skillNames),
714
+ routes: [...unit.routes],
715
+ skillNames: orderedUniqueSkillNames([
716
+ STANDARD_SOUL_SKILL_NAME,
717
+ ...(unit.routes.includes('code') || unit.routes.includes('ui') ? [STANDARD_FUNCTION_SKILL_NAME] : []),
718
+ ...(unit.routes.includes('ui') ? [STANDARD_UI_SKILL_NAME] : []),
719
+ ], unit.skillNames),
614
720
  expertRefs: unit.expertRefs.map((reference) => ({ ...reference })),
615
721
  acceptanceCriteria: [...unit.acceptanceCriteria],
616
722
  focusedVerifiers: unit.focusedVerifiers.map((verifier) => ({ ...verifier, args: [...verifier.args] })),
@@ -631,9 +737,9 @@ export async function submitEnnoPlan(database, rawInput, dependencies = {}) {
631
737
  const replayed = readOperationReceipt(database, input.runId, operation);
632
738
  if (replayed !== undefined)
633
739
  return replayed;
634
- const current = readEnnoSnapshot(database, identity(input));
740
+ const current = readEnnoSnapshot(database, identity(database, input));
635
741
  assertExpected(current, input.expectedRevision, ['zenki_planning']);
636
- startOperationInTransaction(database, input.runId, operation);
742
+ const operationOwner = startOperationInTransaction(database, input.runId, operation);
637
743
  const advisoryRound = consumeAdvisoryRoundIfPresent(database, current, 'planning', current.mutationRevision, advisoryContextForSubmission(current, 'planning'), input.advisoryRoundDigest, input.advisoryDisposition);
638
744
  const status = unavailable.length > 0
639
745
  ? 'blocked'
@@ -653,7 +759,8 @@ export async function submitEnnoPlan(database, rawInput, dependencies = {}) {
653
759
  contractRevision: nextRevision,
654
760
  workUnitIds: contract.workPlan.units.map((unit) => unit.id),
655
761
  });
656
- let updated = readEnnoSnapshot(database, identity(input));
762
+ let updated = readEnnoSnapshot(database, identity(database, input));
763
+ let executionLease;
657
764
  if (status === 'blocked') {
658
765
  appendEnnoEventInTransaction(database, input.runId, 'enno.blocked', 'enno-oduno', 'blocked', {
659
766
  reason: blocker,
@@ -665,28 +772,32 @@ export async function submitEnnoPlan(database, rawInput, dependencies = {}) {
665
772
  appendEnnoEventInTransaction(database, input.runId, 'enno.plan_confirmed', 'enno-oduno', 'not_required', {
666
773
  contractRevision: nextRevision,
667
774
  });
668
- startFirstReadyUnit(database, updated);
669
- updated = readEnnoSnapshot(database, identity(input));
775
+ executionLease = startFirstReadyUnit(database, updated);
776
+ updated = readEnnoSnapshot(database, identity(database, input));
670
777
  }
671
778
  const response = {
672
779
  ennoOduno: stateForSnapshot(updated),
673
780
  ...(advisoryRound === undefined ? {} : { advisoryRound }),
781
+ ...(executionLease === undefined ? {} : { executionLease }),
674
782
  };
675
- completeOperationInTransaction(database, input.runId, operation, response);
783
+ completeOperationInTransaction(database, input.runId, operation, operationOwner, response);
676
784
  return response;
677
785
  });
678
786
  }
679
787
  export function answerEnno(database, rawInput) {
680
- const input = parseEnnoAnswer(rawInput);
788
+ const parsedInput = parseEnnoAnswer(rawInput);
789
+ const before = readEnnoSnapshot(database, identity(database, parsedInput));
790
+ const input = sanitizeEnnoAnswer(parsedInput, before.repositoryRoot);
681
791
  const operation = operationIdentity('answer', input.idempotencyKey, input);
682
792
  const replay = readOperationReceipt(database, input.runId, operation);
683
793
  if (replay !== undefined)
684
794
  return replay;
685
795
  return withImmediateTransaction(database, () => {
686
- const current = readEnnoSnapshot(database, identity(input));
796
+ const current = readEnnoSnapshot(database, identity(database, input));
687
797
  assertExpected(current, input.expectedRevision, input.action === 'cancel' ? ['needs_confirmation', 'zenki_planning'] : ['needs_confirmation']);
688
- startOperationInTransaction(database, input.runId, operation);
798
+ const operationOwner = startOperationInTransaction(database, input.runId, operation);
689
799
  let next;
800
+ let executionLease;
690
801
  if (input.action === 'approve') {
691
802
  updateContractInTransaction(database, current, {
692
803
  contract: current.contract,
@@ -696,9 +807,9 @@ export function answerEnno(database, rawInput) {
696
807
  appendEnnoEventInTransaction(database, input.runId, 'enno.plan_confirmed', 'enno-oduno', 'approved', {
697
808
  contractRevision: current.revision,
698
809
  });
699
- next = readEnnoSnapshot(database, identity(input));
700
- startFirstReadyUnit(database, next);
701
- next = readEnnoSnapshot(database, identity(input));
810
+ next = readEnnoSnapshot(database, identity(database, input));
811
+ executionLease = startFirstReadyUnit(database, next);
812
+ next = readEnnoSnapshot(database, identity(database, input));
702
813
  }
703
814
  else if (input.action === 'revise') {
704
815
  const revisedContract = {
@@ -711,7 +822,7 @@ export function answerEnno(database, rawInput) {
711
822
  confirmationState: 'revision_requested',
712
823
  blocker: input.requestedChanges ?? null,
713
824
  });
714
- next = readEnnoSnapshot(database, identity(input));
825
+ next = readEnnoSnapshot(database, identity(database, input));
715
826
  }
716
827
  else {
717
828
  updateContractInTransaction(database, current, {
@@ -723,10 +834,13 @@ export function answerEnno(database, rawInput) {
723
834
  contractRevision: current.revision,
724
835
  });
725
836
  terminalizeLedgerRunInTransaction(database, input.runId, 'cancelled');
726
- next = readEnnoSnapshot(database, identity(input));
837
+ next = readEnnoSnapshot(database, identity(database, input));
727
838
  }
728
- const response = { ennoOduno: stateForSnapshot(next) };
729
- completeOperationInTransaction(database, input.runId, operation, response);
839
+ const response = {
840
+ ennoOduno: stateForSnapshot(next),
841
+ ...(executionLease === undefined ? {} : { executionLease }),
842
+ };
843
+ completeOperationInTransaction(database, input.runId, operation, operationOwner, response);
730
844
  return response;
731
845
  });
732
846
  }
@@ -772,7 +886,20 @@ function sanitizedReviewSummary(summary, repositoryRoot) {
772
886
  return sanitizeJson({ summary }, { workspace: repositoryRoot }).value.summary;
773
887
  }
774
888
  function blockedForAttemptLimit(database, snapshot, operation) {
775
- startOperationInTransaction(database, snapshot.runId, operation);
889
+ const operationOwner = startOperationInTransaction(database, snapshot.runId, operation);
890
+ const activeUnit = snapshot.workUnits.find((candidate) => candidate.status === 'in_progress');
891
+ if (activeUnit !== undefined) {
892
+ setWorkUnitStatusInTransaction(database, {
893
+ runId: snapshot.runId,
894
+ contractRevision: snapshot.revision,
895
+ workUnitId: activeUnit.workUnit.id,
896
+ from: 'in_progress',
897
+ to: 'blocked',
898
+ attemptCount: activeUnit.attemptCount,
899
+ result: activeUnit.result,
900
+ });
901
+ }
902
+ releaseExecutionLeaseInTransaction(database, snapshot.runId);
776
903
  updateContractInTransaction(database, snapshot, {
777
904
  contract: snapshot.contract,
778
905
  status: 'blocked',
@@ -789,28 +916,36 @@ function blockedForAttemptLimit(database, snapshot, operation) {
789
916
  workspace: snapshot.workspace,
790
917
  orchestrationId: snapshot.orchestrationId,
791
918
  })) };
792
- completeOperationInTransaction(database, snapshot.runId, operation, response);
919
+ completeOperationInTransaction(database, snapshot.runId, operation, operationOwner, response);
793
920
  return response;
794
921
  }
795
922
  export async function reportEnnoWork(database, rawInput, dependencies = {}) {
796
- const input = parseWorkReport(rawInput);
923
+ const parsedInput = parseWorkReport(rawInput);
924
+ const initial = readEnnoSnapshot(database, identity(database, parsedInput));
925
+ const input = sanitizeWorkReport(parsedInput, initial.repositoryRoot);
797
926
  const operation = operationIdentity('work_report', input.idempotencyKey, input);
798
927
  const replay = readOperationReceipt(database, input.runId, operation);
799
928
  if (replay !== undefined)
800
929
  return replay;
801
- const before = readEnnoSnapshot(database, identity(input));
930
+ const before = initial;
802
931
  assertExpected(before, input.expectedRevision, ['goki_executing']);
803
932
  const unit = before.workUnits.find((candidate) => candidate.workUnit.id === input.workUnitId);
804
933
  if (unit === undefined || unit.status !== 'in_progress')
805
934
  throw new KiokukoError('CONFLICT', 'Reported Enno WorkUnit is not active');
806
935
  if (before.attempts >= before.contract.maxAttempts) {
807
- return withImmediateTransaction(database, () => blockedForAttemptLimit(database, readEnnoSnapshot(database, identity(input)), operation));
936
+ return withImmediateTransaction(database, () => {
937
+ const current = readEnnoSnapshot(database, identity(database, input));
938
+ assertExpected(current, input.expectedRevision, ['goki_executing']);
939
+ assertExecutionLeaseInTransaction(database, current, input);
940
+ return blockedForAttemptLimit(database, current, operation);
941
+ });
808
942
  }
809
943
  if (input.result.outcome !== 'completed') {
810
944
  return withImmediateTransaction(database, () => {
811
- const current = readEnnoSnapshot(database, identity(input));
945
+ const current = readEnnoSnapshot(database, identity(database, input));
812
946
  assertExpected(current, input.expectedRevision, ['goki_executing']);
813
- startOperationInTransaction(database, input.runId, operation);
947
+ assertExecutionLeaseInTransaction(database, current, input);
948
+ const operationOwner = startOperationInTransaction(database, input.runId, operation);
814
949
  const currentUnit = current.workUnits.find((candidate) => candidate.workUnit.id === input.workUnitId);
815
950
  const attempts = current.attempts + 1;
816
951
  const mustBlock = input.result.outcome === 'blocked' || attempts >= current.contract.maxAttempts;
@@ -835,21 +970,25 @@ export async function reportEnnoWork(database, rawInput, dependencies = {}) {
835
970
  attempt: attempts,
836
971
  });
837
972
  if (mustBlock) {
973
+ releaseExecutionLeaseInTransaction(database, input.runId);
838
974
  appendEnnoEventInTransaction(database, input.runId, 'enno.blocked', 'enno-oduno', 'blocked', {
839
975
  reason: input.result.summary,
840
976
  });
841
977
  terminalizeLedgerRunInTransaction(database, input.runId, 'failed');
842
978
  }
843
- const response = { ennoOduno: stateForSnapshot(readEnnoSnapshot(database, identity(input))) };
844
- completeOperationInTransaction(database, input.runId, operation, response);
979
+ const response = { ennoOduno: stateForSnapshot(readEnnoSnapshot(database, identity(database, input))) };
980
+ completeOperationInTransaction(database, input.runId, operation, operationOwner, response);
845
981
  return response;
846
982
  });
847
983
  }
848
984
  let verifierRunIds = [];
985
+ let operationOwner = '';
986
+ const verifierLeaseMs = operationLeaseMsForVerifiers(unit.workUnit.focusedVerifiers);
849
987
  withImmediateTransaction(database, () => {
850
- const current = readEnnoSnapshot(database, identity(input));
988
+ const current = readEnnoSnapshot(database, identity(database, input));
851
989
  assertExpected(current, input.expectedRevision, ['goki_executing']);
852
- startOperationInTransaction(database, input.runId, operation);
990
+ renewExecutionLeaseInTransaction(database, current, input, verifierLeaseMs);
991
+ operationOwner = startOperationInTransaction(database, input.runId, operation, verifierLeaseMs);
853
992
  const currentUnit = current.workUnits.find((candidate) => candidate.workUnit.id === input.workUnitId);
854
993
  if (currentUnit === undefined || currentUnit.status !== 'in_progress') {
855
994
  throw new KiokukoError('CONFLICT', 'Reported Enno WorkUnit changed concurrently');
@@ -871,8 +1010,9 @@ export async function reportEnnoWork(database, rawInput, dependencies = {}) {
871
1010
  }
872
1011
  const results = sanitizedVerifierResults(rawResults, before.repositoryRoot);
873
1012
  return withImmediateTransaction(database, () => {
874
- const current = readEnnoSnapshot(database, identity(input));
1013
+ const current = readEnnoSnapshot(database, identity(database, input));
875
1014
  assertExpected(current, input.expectedRevision, ['goki_executing']);
1015
+ assertExecutionLeaseInTransaction(database, current, input);
876
1016
  finishVerifierRunsInTransaction(database, verifierRunIds, results);
877
1017
  const passed = results.every((result) => result.status === 'passed');
878
1018
  const unsafe = results.some((result) => result.status === 'spawn_failed');
@@ -903,7 +1043,9 @@ export async function reportEnnoWork(database, rawInput, dependencies = {}) {
903
1043
  mutated: input.result.mutated,
904
1044
  changedPaths: input.result.changedPaths,
905
1045
  });
906
- let next = readEnnoSnapshot(database, identity(input));
1046
+ releaseExecutionLeaseInTransaction(database, input.runId);
1047
+ let next = readEnnoSnapshot(database, identity(database, input));
1048
+ let executionLease;
907
1049
  if (mustBlock) {
908
1050
  appendEnnoEventInTransaction(database, input.runId, 'enno.blocked', 'enno-oduno', 'blocked', {
909
1051
  reason: unsafe ? 'spawn_failed' : 'focused_verification_failed',
@@ -919,37 +1061,46 @@ export async function reportEnnoWork(database, rawInput, dependencies = {}) {
919
1061
  });
920
1062
  }
921
1063
  else {
922
- startFirstReadyUnit(database, next);
1064
+ executionLease = startFirstReadyUnit(database, next);
923
1065
  }
924
- next = readEnnoSnapshot(database, identity(input));
1066
+ next = readEnnoSnapshot(database, identity(database, input));
925
1067
  }
926
- const response = { ennoOduno: stateForSnapshot(next), verifierResults: results };
927
- completeOperationInTransaction(database, input.runId, operation, response);
1068
+ const response = {
1069
+ ennoOduno: stateForSnapshot(next),
1070
+ verifierResults: results,
1071
+ ...(executionLease === undefined ? {} : { executionLease }),
1072
+ };
1073
+ completeOperationInTransaction(database, input.runId, operation, operationOwner, response);
928
1074
  return response;
929
1075
  });
930
1076
  }
931
1077
  export async function prepareEnnoVerification(database, rawInput, dependencies = {}) {
932
1078
  const input = parseVerificationPrepare(rawInput);
1079
+ const before = readEnnoSnapshot(database, identity(database, input));
933
1080
  const operation = operationIdentity('verify_prepare', input.idempotencyKey, input);
934
1081
  const replay = readOperationReceipt(database, input.runId, operation);
935
1082
  if (replay !== undefined)
936
1083
  return replay;
937
- const before = readEnnoSnapshot(database, identity(input));
938
1084
  assertExpected(before, input.expectedRevision, ['enno_verifying']);
1085
+ const preRepositoryState = captureRepositoryState(before.repositoryRoot);
1086
+ const verifierSpecDigest = canonicalContentHash(before.contract.finalVerifiers);
939
1087
  if (before.attempts >= before.contract.maxAttempts) {
940
- return withImmediateTransaction(database, () => blockedForAttemptLimit(database, readEnnoSnapshot(database, identity(input)), operation));
1088
+ return withImmediateTransaction(database, () => blockedForAttemptLimit(database, readEnnoSnapshot(database, identity(database, input)), operation));
941
1089
  }
942
1090
  let verifierRunIds = [];
1091
+ let operationOwner = '';
943
1092
  let cachedResults;
1093
+ const verifierLeaseMs = operationLeaseMsForVerifiers(before.contract.finalVerifiers);
944
1094
  withImmediateTransaction(database, () => {
945
- const current = readEnnoSnapshot(database, identity(input));
1095
+ const current = readEnnoSnapshot(database, identity(database, input));
946
1096
  assertExpected(current, input.expectedRevision, ['enno_verifying']);
947
- startOperationInTransaction(database, input.runId, operation);
1097
+ operationOwner = startOperationInTransaction(database, input.runId, operation, verifierLeaseMs);
948
1098
  cachedResults = readFreshFinalVerifierResults(database, {
949
1099
  runId: input.runId,
950
1100
  revision: current.revision,
951
1101
  mutationRevision: current.mutationRevision,
952
1102
  verifiers: current.contract.finalVerifiers,
1103
+ repositoryDigest: preRepositoryState.digest,
953
1104
  });
954
1105
  if (cachedResults === undefined) {
955
1106
  verifierRunIds = startVerifierRunsInTransaction(database, {
@@ -958,6 +1109,11 @@ export async function prepareEnnoVerification(database, rawInput, dependencies =
958
1109
  revision: current.revision,
959
1110
  mutationRevision: current.mutationRevision,
960
1111
  verifiers: current.contract.finalVerifiers,
1112
+ repositoryEvidence: {
1113
+ policyVersion: preRepositoryState.policyVersion,
1114
+ preDigest: preRepositoryState.digest,
1115
+ verifierSpecDigest,
1116
+ },
961
1117
  });
962
1118
  }
963
1119
  appendEnnoEventInTransaction(database, input.runId, 'enno.verification_started', 'enno-oduno', cachedResults === undefined ? 'started' : 'reused', {
@@ -978,51 +1134,74 @@ export async function prepareEnnoVerification(database, rawInput, dependencies =
978
1134
  results = spawnFailedVerifierResults(before.contract.finalVerifiers);
979
1135
  }
980
1136
  }
1137
+ const postRepositoryState = captureRepositoryState(before.repositoryRoot);
1138
+ const changedDuringVerification = postRepositoryState.digest !== preRepositoryState.digest
1139
+ || results.some((result) => result.changedDuringVerification === true);
1140
+ results = results.map((result) => ({
1141
+ ...result,
1142
+ repositoryStatePolicyVersion: postRepositoryState.policyVersion,
1143
+ repositoryStateDigest: postRepositoryState.digest,
1144
+ changedDuringVerification,
1145
+ }));
981
1146
  return withImmediateTransaction(database, () => {
982
- const current = readEnnoSnapshot(database, identity(input));
1147
+ const current = readEnnoSnapshot(database, identity(database, input));
983
1148
  assertExpected(current, input.expectedRevision, ['enno_verifying']);
984
1149
  if (verifierRunIds.length > 0)
985
- finishVerifierRunsInTransaction(database, verifierRunIds, results);
1150
+ finishVerifierRunsInTransaction(database, verifierRunIds, results, {
1151
+ postDigest: postRepositoryState.digest,
1152
+ changedDuringVerification,
1153
+ });
986
1154
  const response = {
987
- ennoOduno: stateForSnapshot(readEnnoSnapshot(database, identity(input))),
1155
+ ennoOduno: stateForSnapshot(readEnnoSnapshot(database, identity(database, input))),
988
1156
  verifierResults: results,
989
1157
  };
990
- completeOperationInTransaction(database, input.runId, operation, response);
1158
+ completeOperationInTransaction(database, input.runId, operation, operationOwner, response);
991
1159
  return response;
992
1160
  });
993
1161
  }
994
1162
  export async function finishEnno(database, rawInput, dependencies = {}) {
995
- const input = parseFinishRequest(rawInput);
1163
+ const parsedInput = parseFinishRequest(rawInput);
1164
+ const initial = readEnnoSnapshot(database, identity(database, parsedInput));
1165
+ const input = sanitizeFinishRequest(parsedInput, initial.repositoryRoot);
996
1166
  const operation = operationIdentity('finish', input.idempotencyKey, input);
997
1167
  const replay = readOperationReceipt(database, input.runId, operation);
998
1168
  if (replay !== undefined)
999
1169
  return replay;
1000
- const before = readEnnoSnapshot(database, identity(input));
1170
+ const before = initial;
1001
1171
  assertExpected(before, input.expectedRevision, ['enno_verifying']);
1002
1172
  if (input.advisoryRoundDigest !== undefined) {
1173
+ if (!before.finalEvidenceReady) {
1174
+ throw ennoValidationError('finish', [{
1175
+ path: ['advisoryRoundDigest'],
1176
+ reasonCode: 'advisory_digest_stale',
1177
+ }], 'refresh_state');
1178
+ }
1003
1179
  requireAdvisoryRound(database, before, 'final_review', before.mutationRevision, advisoryContextForSubmission(before, 'final_review'), input.advisoryRoundDigest);
1004
1180
  }
1005
1181
  const reviewSummary = sanitizedReviewSummary(input.review.summary, before.repositoryRoot);
1006
1182
  if (before.attempts >= before.contract.maxAttempts) {
1007
- return withImmediateTransaction(database, () => blockedForAttemptLimit(database, readEnnoSnapshot(database, identity(input)), operation));
1183
+ return withImmediateTransaction(database, () => blockedForAttemptLimit(database, readEnnoSnapshot(database, identity(database, input)), operation));
1008
1184
  }
1009
- let advisoryRound;
1010
- let results;
1011
- withImmediateTransaction(database, () => {
1012
- const current = readEnnoSnapshot(database, identity(input));
1185
+ return withImmediateTransaction(database, () => {
1186
+ const current = readEnnoSnapshot(database, identity(database, input));
1013
1187
  assertExpected(current, input.expectedRevision, ['enno_verifying']);
1014
- advisoryRound = consumeAdvisoryRoundIfPresent(database, current, 'final_review', current.mutationRevision, input.advisoryRoundDigest === undefined ? undefined : advisoryContextForSubmission(current, 'final_review'), input.advisoryRoundDigest, input.advisoryDisposition);
1015
- const fresh = readFreshFinalVerifierResults(database, {
1188
+ const currentRepositoryDigest = captureRepositoryState(current.repositoryRoot).digest;
1189
+ const results = readFreshFinalVerifierResults(database, {
1016
1190
  runId: input.runId,
1017
1191
  revision: current.revision,
1018
1192
  mutationRevision: current.mutationRevision,
1019
1193
  verifiers: current.contract.finalVerifiers,
1194
+ repositoryDigest: currentRepositoryDigest,
1020
1195
  });
1021
- if (fresh === undefined) {
1196
+ if (results === undefined) {
1022
1197
  throw new KiokukoError('CONFLICT', 'Final verification evidence is not prepared; call enno_verify_prepare first');
1023
1198
  }
1024
- results = fresh;
1025
- startOperationInTransaction(database, input.runId, operation);
1199
+ if (results.some((result) => result.repositoryStateDigest !== currentRepositoryDigest
1200
+ || result.changedDuringVerification !== false)) {
1201
+ throw new KiokukoError('CONFLICT', 'Final verification evidence is stale for the current repository state');
1202
+ }
1203
+ const advisoryRound = consumeAdvisoryRoundIfPresent(database, current, 'final_review', current.mutationRevision, input.advisoryRoundDigest === undefined ? undefined : advisoryContextForSubmission(current, 'final_review'), input.advisoryRoundDigest, input.advisoryDisposition);
1204
+ const operationOwner = startOperationInTransaction(database, input.runId, operation);
1026
1205
  appendEnnoEventInTransaction(database, input.runId, 'enno.review_started', 'enno-oduno', 'started', {
1027
1206
  contractRevision: current.revision,
1028
1207
  mutationRevision: current.mutationRevision,
@@ -1033,10 +1212,6 @@ export async function finishEnno(database, rawInput, dependencies = {}) {
1033
1212
  mutationRevision: current.mutationRevision,
1034
1213
  verifierIds: current.contract.finalVerifiers.map((verifier) => verifier.id),
1035
1214
  });
1036
- });
1037
- return withImmediateTransaction(database, () => {
1038
- const current = readEnnoSnapshot(database, identity(input));
1039
- assertExpected(current, input.expectedRevision, ['enno_verifying']);
1040
1215
  const passed = results.length > 0 && results.every((result) => result.status === 'passed');
1041
1216
  const unsafe = results.some((result) => result.status === 'spawn_failed');
1042
1217
  const attempts = current.attempts + 1;
@@ -1068,7 +1243,7 @@ export async function finishEnno(database, rawInput, dependencies = {}) {
1068
1243
  mutationRevision: current.mutationRevision,
1069
1244
  statuses: results.map((result) => ({ verifierId: result.verifier.id, status: result.status })),
1070
1245
  });
1071
- let next = readEnnoSnapshot(database, identity(input));
1246
+ let next = readEnnoSnapshot(database, identity(database, input));
1072
1247
  if (accepted) {
1073
1248
  appendEnnoEventInTransaction(database, input.runId, 'enno.review_accepted', 'enno-oduno', 'accepted', {
1074
1249
  contractRevision: current.revision,
@@ -1096,40 +1271,38 @@ export async function finishEnno(database, rawInput, dependencies = {}) {
1096
1271
  toContractRevision: next.revision,
1097
1272
  reason: reviewFeedback,
1098
1273
  });
1099
- next = readEnnoSnapshot(database, identity(input));
1274
+ next = readEnnoSnapshot(database, identity(database, input));
1100
1275
  }
1101
1276
  const response = {
1102
1277
  ennoOduno: stateForSnapshot(next),
1103
1278
  verifierResults: results,
1104
1279
  ...(advisoryRound === undefined ? {} : { advisoryRound }),
1105
1280
  };
1106
- completeOperationInTransaction(database, input.runId, operation, response);
1281
+ completeOperationInTransaction(database, input.runId, operation, operationOwner, response);
1107
1282
  return response;
1108
1283
  });
1109
1284
  }
1110
- function sanitizedMeditation(meditation, repositoryRoot) {
1111
- return parseOdunoMeditation(sanitizeJson(meditation, { workspace: repositoryRoot }).value);
1112
- }
1113
1285
  export function submitOdunoMeditation(database, rawInput) {
1114
- const input = parseMeditationSubmission(rawInput);
1286
+ const parsedInput = parseMeditationSubmission(rawInput);
1287
+ const before = readEnnoSnapshot(database, identity(database, parsedInput));
1288
+ const input = sanitizeMeditationSubmission(parsedInput, before.repositoryRoot);
1115
1289
  const operation = operationIdentity('meditation_submit', input.idempotencyKey, input);
1116
1290
  const replay = readOperationReceipt(database, input.runId, operation);
1117
1291
  if (replay !== undefined)
1118
1292
  return replay;
1119
- const before = readEnnoSnapshot(database, identity(input));
1120
1293
  assertExpected(before, input.expectedRevision, ['oduno_meditation']);
1121
1294
  if (before.ideal === null)
1122
1295
  throw new KiokukoError('INTEGRITY_ERROR', 'Oduno meditation requires a persisted ideal');
1123
- const meditation = sanitizedMeditation(input.meditation, before.repositoryRoot);
1296
+ const meditation = input.meditation;
1124
1297
  return withImmediateTransaction(database, () => {
1125
1298
  const replayed = readOperationReceipt(database, input.runId, operation);
1126
1299
  if (replayed !== undefined)
1127
1300
  return replayed;
1128
- const current = readEnnoSnapshot(database, identity(input));
1301
+ const current = readEnnoSnapshot(database, identity(database, input));
1129
1302
  assertExpected(current, input.expectedRevision, ['oduno_meditation']);
1130
1303
  if (current.ideal === null)
1131
1304
  throw new KiokukoError('INTEGRITY_ERROR', 'Oduno meditation requires a persisted ideal');
1132
- startOperationInTransaction(database, input.runId, operation);
1305
+ const operationOwner = startOperationInTransaction(database, input.runId, operation);
1133
1306
  updateContractInTransaction(database, current, {
1134
1307
  contract: current.contract,
1135
1308
  status: 'completed',
@@ -1147,8 +1320,8 @@ export function submitOdunoMeditation(database, rawInput) {
1147
1320
  mutationRevision: current.mutationRevision,
1148
1321
  });
1149
1322
  terminalizeLedgerRunInTransaction(database, input.runId, 'completed');
1150
- const response = { ennoOduno: stateForSnapshot(readEnnoSnapshot(database, identity(input))) };
1151
- completeOperationInTransaction(database, input.runId, operation, response);
1323
+ const response = { ennoOduno: stateForSnapshot(readEnnoSnapshot(database, identity(database, input))) };
1324
+ completeOperationInTransaction(database, input.runId, operation, operationOwner, response);
1152
1325
  return response;
1153
1326
  });
1154
1327
  }