@askdkc/kiokuko 0.2.2 → 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 (121) hide show
  1. package/README.ja.md +83 -45
  2. package/README.ko.md +80 -40
  3. package/README.md +85 -45
  4. package/README.zh-CN.md +76 -39
  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 +100 -63
  32. package/dist/enno-oduno/adapters.js.map +1 -1
  33. package/dist/enno-oduno/advisory-store.d.ts +6 -0
  34. package/dist/enno-oduno/advisory-store.d.ts.map +1 -1
  35. package/dist/enno-oduno/advisory-store.js +13 -1
  36. package/dist/enno-oduno/advisory-store.js.map +1 -1
  37. package/dist/enno-oduno/advisory.d.ts +2 -0
  38. package/dist/enno-oduno/advisory.d.ts.map +1 -1
  39. package/dist/enno-oduno/advisory.js +207 -25
  40. package/dist/enno-oduno/advisory.js.map +1 -1
  41. package/dist/enno-oduno/confirmation.d.ts.map +1 -1
  42. package/dist/enno-oduno/confirmation.js +3 -0
  43. package/dist/enno-oduno/confirmation.js.map +1 -1
  44. package/dist/enno-oduno/directives.d.ts.map +1 -1
  45. package/dist/enno-oduno/directives.js +67 -36
  46. package/dist/enno-oduno/directives.js.map +1 -1
  47. package/dist/enno-oduno/experts.d.ts +1 -1
  48. package/dist/enno-oduno/experts.d.ts.map +1 -1
  49. package/dist/enno-oduno/experts.js +21 -7
  50. package/dist/enno-oduno/experts.js.map +1 -1
  51. package/dist/enno-oduno/instructions.d.ts +5 -3
  52. package/dist/enno-oduno/instructions.d.ts.map +1 -1
  53. package/dist/enno-oduno/instructions.js +4 -2
  54. package/dist/enno-oduno/instructions.js.map +1 -1
  55. package/dist/enno-oduno/plan-recovery.d.ts +15 -0
  56. package/dist/enno-oduno/plan-recovery.d.ts.map +1 -1
  57. package/dist/enno-oduno/plan-recovery.js +26 -0
  58. package/dist/enno-oduno/plan-recovery.js.map +1 -1
  59. package/dist/enno-oduno/repository-state.d.ts +8 -0
  60. package/dist/enno-oduno/repository-state.d.ts.map +1 -0
  61. package/dist/enno-oduno/repository-state.js +139 -0
  62. package/dist/enno-oduno/repository-state.js.map +1 -0
  63. package/dist/enno-oduno/role-runner.d.ts.map +1 -1
  64. package/dist/enno-oduno/role-runner.js +50 -2
  65. package/dist/enno-oduno/role-runner.js.map +1 -1
  66. package/dist/enno-oduno/sanitize.d.ts +17 -0
  67. package/dist/enno-oduno/sanitize.d.ts.map +1 -0
  68. package/dist/enno-oduno/sanitize.js +88 -0
  69. package/dist/enno-oduno/sanitize.js.map +1 -0
  70. package/dist/enno-oduno/schemas.d.ts +314 -29
  71. package/dist/enno-oduno/schemas.d.ts.map +1 -1
  72. package/dist/enno-oduno/schemas.js +267 -50
  73. package/dist/enno-oduno/schemas.js.map +1 -1
  74. package/dist/enno-oduno/service.d.ts +3 -1
  75. package/dist/enno-oduno/service.d.ts.map +1 -1
  76. package/dist/enno-oduno/service.js +416 -146
  77. package/dist/enno-oduno/service.js.map +1 -1
  78. package/dist/enno-oduno/store.d.ts +44 -5
  79. package/dist/enno-oduno/store.d.ts.map +1 -1
  80. package/dist/enno-oduno/store.js +367 -35
  81. package/dist/enno-oduno/store.js.map +1 -1
  82. package/dist/enno-oduno/types.d.ts +106 -6
  83. package/dist/enno-oduno/types.d.ts.map +1 -1
  84. package/dist/enno-oduno/types.js +2 -0
  85. package/dist/enno-oduno/types.js.map +1 -1
  86. package/dist/enno-oduno/validation-errors.d.ts +92 -0
  87. package/dist/enno-oduno/validation-errors.d.ts.map +1 -0
  88. package/dist/enno-oduno/validation-errors.js +151 -0
  89. package/dist/enno-oduno/validation-errors.js.map +1 -0
  90. package/dist/enno-oduno/verifier.d.ts +1 -0
  91. package/dist/enno-oduno/verifier.d.ts.map +1 -1
  92. package/dist/enno-oduno/verifier.js +99 -16
  93. package/dist/enno-oduno/verifier.js.map +1 -1
  94. package/dist/ledger/types.d.ts +1 -1
  95. package/dist/ledger/types.d.ts.map +1 -1
  96. package/dist/ledger/types.js +1 -1
  97. package/dist/ledger/types.js.map +1 -1
  98. package/dist/mcp/server.d.ts.map +1 -1
  99. package/dist/mcp/server.js +76 -16
  100. package/dist/mcp/server.js.map +1 -1
  101. package/dist/memory/secrets.d.ts.map +1 -1
  102. package/dist/memory/secrets.js +1 -0
  103. package/dist/memory/secrets.js.map +1 -1
  104. package/dist/server/routes/agent-capability-gate.js +2 -2
  105. package/dist/server/routes/agent-capability-gate.js.map +1 -1
  106. package/dist/setup/render.js +1 -1
  107. package/dist/setup/render.js.map +1 -1
  108. package/dist/setup/standard-skills.d.ts +12 -5
  109. package/dist/setup/standard-skills.d.ts.map +1 -1
  110. package/dist/setup/standard-skills.js +12 -2
  111. package/dist/setup/standard-skills.js.map +1 -1
  112. package/migrations/017_enno_advisory_protocol_v2.sql +101 -0
  113. package/migrations/018_enno_repository_routing.sql +5 -0
  114. package/migrations/019_enno_execution_integrity.sql +149 -0
  115. package/package.json +1 -1
  116. package/skills/kiokuko-enno-oduno/SKILL.md +19 -8
  117. package/skills/kiokuko-single-purpose-functions/SKILL.md +18 -10
  118. package/skills/kiokuko-single-purpose-functions/references/problem-shaping-and-language.md +84 -0
  119. package/skills/kiokuko-ui-design-soul/SKILL.md +5 -1
  120. package/skills/memory-reasoning/SKILL.md +52 -0
  121. 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 } from './advisory.js';
16
- import { createAdvisoryRoundInTransaction, ensureAdvisoryRoundConsumedInTransaction, readAdvisoryRound, } from './advisory-store.js';
15
+ import { advisoryContextForSnapshot, advisoryInputDigest, advisoryPhaseForStatus, normalizeAdvisoryContributions } from './advisory.js';
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, 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;
@@ -37,7 +40,7 @@ function stateForSnapshot(snapshot) {
37
40
  else if (snapshot.status === 'goki_executing')
38
41
  nextAction = 'execute_work_unit';
39
42
  else if (snapshot.status === 'enno_verifying')
40
- nextAction = 'run_final_verification';
43
+ nextAction = snapshot.finalEvidenceReady ? 'submit_final_review' : 'run_final_verification';
41
44
  else if (snapshot.status === 'oduno_meditation')
42
45
  nextAction = 'submit_meditation';
43
46
  else if (snapshot.status === 'blocked')
@@ -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,25 +209,113 @@ 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 consumeAdvisoryRoundIfPresent(database, snapshot, phase, mutationRevision, context, inputDigest) {
190
- if (inputDigest === undefined)
212
+ function assertDispositionCoversRound(operation, round, dispositions) {
213
+ const expected = new Set(round.contributions.map((contribution) => contribution.slotId));
214
+ const covered = new Set();
215
+ for (const [index, disposition] of dispositions.entries()) {
216
+ if (!expected.has(disposition.slotId)) {
217
+ throw ennoValidationError(operation, [{
218
+ path: ['advisoryDisposition', index, 'slotId'],
219
+ reasonCode: 'advisory_slot_missing',
220
+ expected: { requiredSlotIds: [...expected] },
221
+ }]);
222
+ }
223
+ if (covered.has(disposition.slotId)) {
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
+ }]);
240
+ }
241
+ covered.add(disposition.slotId);
242
+ }
243
+ for (const slotId of expected) {
244
+ if (!covered.has(slotId)) {
245
+ throw ennoValidationError(operation, [{
246
+ path: ['advisoryDisposition'],
247
+ reasonCode: 'advisory_slot_missing',
248
+ expected: { requiredSlotIds: [...expected] },
249
+ }]);
250
+ }
251
+ }
252
+ }
253
+ function consumeAdvisoryRoundIfPresent(database, snapshot, phase, mutationRevision, context, inputDigest, disposition) {
254
+ if (inputDigest === undefined) {
255
+ const submitted = readSubmittedAdvisoryRound(database, {
256
+ runId: snapshot.runId,
257
+ contractRevision: snapshot.revision,
258
+ mutationRevision,
259
+ phase,
260
+ });
261
+ if (submitted !== undefined) {
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
+ }]);
267
+ }
191
268
  return undefined;
192
- assertAdvisoryRoundInput(snapshot, phase, mutationRevision, context, inputDigest);
193
- return ensureAdvisoryRoundConsumedInTransaction(database, {
269
+ }
270
+ if (context === undefined) {
271
+ throw new KiokukoError('CONFLICT', 'Advisory round is not valid for the current Enno phase');
272
+ }
273
+ const operation = phase === 'planning' ? 'plan_submit' : phase === 'ideal' ? 'ideal_submit' : 'finish';
274
+ assertAdvisoryRoundInput(snapshot, phase, mutationRevision, context, inputDigest, operation);
275
+ const dispositions = disposition ?? [];
276
+ if (dispositions.length === 0) {
277
+ throw ennoValidationError(phase === 'planning' ? 'plan_submit' : phase === 'ideal' ? 'ideal_submit' : 'finish', [{
278
+ path: ['advisoryDisposition'],
279
+ reasonCode: 'advisory_digest_requires_disposition',
280
+ }]);
281
+ }
282
+ const pendingRound = readAdvisoryRound(database, {
194
283
  runId: snapshot.runId,
195
284
  contractRevision: snapshot.revision,
196
285
  mutationRevision,
197
286
  phase,
198
287
  inputDigest,
199
288
  });
289
+ if (pendingRound === undefined)
290
+ throw new KiokukoError('CONFLICT', 'Required advisory round was not submitted');
291
+ assertDispositionCoversRound(operation, pendingRound, dispositions);
292
+ const round = ensureAdvisoryRoundConsumedInTransaction(database, {
293
+ runId: snapshot.runId,
294
+ contractRevision: snapshot.revision,
295
+ mutationRevision,
296
+ phase,
297
+ inputDigest,
298
+ });
299
+ appendEnnoEventInTransaction(database, snapshot.runId, 'enno.advice_disposition', 'enno-oduno', 'recorded', {
300
+ phase,
301
+ contractRevision: snapshot.revision,
302
+ mutationRevision,
303
+ inputDigest,
304
+ dispositions: dispositions.map((entry) => ({ slotId: entry.slotId, disposition: entry.disposition, rationale: entry.rationale })),
305
+ });
306
+ return round;
200
307
  }
201
308
  export function submitEnnoAdvice(database, rawInput) {
202
- 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));
203
315
  const operation = operationIdentity('advice_submit', input.idempotencyKey, input);
204
316
  const replay = readOperationReceipt(database, input.runId, operation);
205
317
  if (replay !== undefined)
206
318
  return replay;
207
- const before = readEnnoSnapshot(database, identity(input));
208
319
  const expectedPhase = advisoryPhaseForStatus(before.status);
209
320
  if (expectedPhase !== input.phase)
210
321
  throw new KiokukoError('CONFLICT', 'Advisory phase does not match the current Enno state');
@@ -219,12 +330,12 @@ export function submitEnnoAdvice(database, rawInput) {
219
330
  const replayed = readOperationReceipt(database, input.runId, operation);
220
331
  if (replayed !== undefined)
221
332
  return replayed;
222
- const current = readEnnoSnapshot(database, identity(input));
333
+ const current = readEnnoSnapshot(database, identity(database, input));
223
334
  assertExpected(current, input.expectedRevision, [
224
335
  input.phase === 'ideal' ? 'oduno_ideal' : input.phase === 'planning' ? 'zenki_planning' : 'enno_verifying',
225
336
  ]);
226
337
  assertAdvisoryRoundInput(current, input.phase, input.mutationRevision, context, inputDigest);
227
- startOperationInTransaction(database, input.runId, operation);
338
+ const operationOwner = startOperationInTransaction(database, input.runId, operation);
228
339
  const existing = readAdvisoryRound(database, {
229
340
  runId: input.runId,
230
341
  contractRevision: current.revision,
@@ -232,14 +343,24 @@ export function submitEnnoAdvice(database, rawInput) {
232
343
  phase: input.phase,
233
344
  inputDigest,
234
345
  });
235
- const round = existing ?? createAdvisoryRoundInTransaction(database, {
236
- runId: input.runId,
237
- contractRevision: current.revision,
238
- mutationRevision: input.mutationRevision,
239
- phase: input.phase,
240
- inputDigest,
241
- contributions: input.contributions,
242
- });
346
+ let round;
347
+ if (existing !== undefined) {
348
+ const incoming = normalizeAdvisoryContributions(input.phase, input.contributions);
349
+ if (canonicalJson(existing.contributions) !== canonicalJson(incoming)) {
350
+ throw new KiokukoError('CONFLICT', 'Advisory round was already submitted with different contributions');
351
+ }
352
+ round = existing;
353
+ }
354
+ else {
355
+ round = createAdvisoryRoundInTransaction(database, {
356
+ runId: input.runId,
357
+ contractRevision: current.revision,
358
+ mutationRevision: input.mutationRevision,
359
+ phase: input.phase,
360
+ inputDigest,
361
+ contributions: input.contributions,
362
+ });
363
+ }
243
364
  appendEnnoEventInTransaction(database, input.runId, 'enno.advice_submitted', 'enno-oduno', 'aggregated', {
244
365
  phase: input.phase,
245
366
  contractRevision: current.revision,
@@ -247,17 +368,16 @@ export function submitEnnoAdvice(database, rawInput) {
247
368
  inputDigest,
248
369
  degraded: round.degraded,
249
370
  });
250
- const response = { ennoOduno: stateForSnapshot(current), advisoryRound: round };
251
- 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);
252
376
  return response;
253
377
  });
254
378
  }
255
- function identity(input) {
256
- return {
257
- runId: input.runId,
258
- workspace: input.workspace,
259
- orchestrationId: input.orchestrationId,
260
- };
379
+ function identity(database, input) {
380
+ return resolveEnnoIdentity(database, input);
261
381
  }
262
382
  function assertExpected(snapshot, expectedRevision, statuses) {
263
383
  if (snapshot.revision !== expectedRevision)
@@ -274,7 +394,7 @@ function startFirstReadyUnit(database, snapshot) {
274
394
  const next = firstReadyUnit(snapshot);
275
395
  if (next === undefined) {
276
396
  if (snapshot.workUnits.every((unit) => unit.status === 'completed'))
277
- return;
397
+ return undefined;
278
398
  throw new KiokukoError('CONFLICT', 'Enno WorkPlan dependencies cannot advance');
279
399
  }
280
400
  setWorkUnitStatusInTransaction(database, {
@@ -288,6 +408,12 @@ function startFirstReadyUnit(database, snapshot) {
288
408
  workUnitId: next.workUnit.id,
289
409
  contractRevision: snapshot.revision,
290
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
+ });
291
417
  }
292
418
  function requiresConfirmation(provenance) {
293
419
  return ENNO_PROVENANCE_KEYS.some((key) => provenance[key] !== 'explicit_user');
@@ -372,7 +498,7 @@ async function discoverZenkiSkills(database, snapshot, plan, dependencies) {
372
498
  return claimed.summary;
373
499
  if (claimed.queryBudget === 0 || claimed.selectionBudget === 0) {
374
500
  return completeAgentTaskSkillDiscoveryAttempt(database, attempt, emptyDiscovery(intake.mode), () => {
375
- assertExpected(readEnnoSnapshot(database, identity(plan)), plan.expectedRevision, ['zenki_planning']);
501
+ assertExpected(readEnnoSnapshot(database, identity(database, plan)), plan.expectedRevision, ['zenki_planning']);
376
502
  });
377
503
  }
378
504
  try {
@@ -386,10 +512,10 @@ async function discoverZenkiSkills(database, snapshot, plan, dependencies) {
386
512
  ...(dependencies.fetchImpl === undefined ? {} : { fetchImpl: dependencies.fetchImpl }),
387
513
  }, {
388
514
  ...(dependencies.fetchImpl === undefined ? {} : { fetchImpl: dependencies.fetchImpl }),
389
- assertBeforePersist: () => assertExpected(readEnnoSnapshot(database, identity(plan)), plan.expectedRevision, ['zenki_planning']),
515
+ assertBeforePersist: () => assertExpected(readEnnoSnapshot(database, identity(database, plan)), plan.expectedRevision, ['zenki_planning']),
390
516
  });
391
517
  return completeAgentTaskSkillDiscoveryAttempt(database, attempt, summary, () => {
392
- assertExpected(readEnnoSnapshot(database, identity(plan)), plan.expectedRevision, ['zenki_planning']);
518
+ assertExpected(readEnnoSnapshot(database, identity(database, plan)), plan.expectedRevision, ['zenki_planning']);
393
519
  });
394
520
  }
395
521
  catch (error) {
@@ -397,13 +523,11 @@ async function discoverZenkiSkills(database, snapshot, plan, dependencies) {
397
523
  }
398
524
  }
399
525
  function planChangesCode(plan, taskType) {
400
- return taskType !== 'review'
401
- || plan.skillRequirements.some((skill) => skill.purposes.includes('implementation'))
402
- || 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')));
403
528
  }
404
529
  function planHasUi(plan) {
405
- return plan.skillRequirements.some((skill) => skill.purposes.includes('ui'))
406
- || plan.workPlan.units.some((unit) => unit.skillNames.includes('kiokuko-ui-design-soul'));
530
+ return plan.workPlan.units.some((unit) => unit.routes.includes('ui'));
407
531
  }
408
532
  function assertIdealSkillCoverage(snapshot, ideal) {
409
533
  const expected = snapshot.contract.skillSet.intakeDiscovery.selected.map((skill) => skill.name);
@@ -412,26 +536,43 @@ function assertIdealSkillCoverage(snapshot, ideal) {
412
536
  }
413
537
  const actual = ideal.skillContributions.map((contribution) => contribution.skillName);
414
538
  if (actual.length !== expected.length || expected.some((name) => !actual.includes(name))) {
415
- 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
+ }]);
416
544
  }
417
545
  }
418
- function assertPlanCapabilityCatalog(database, input) {
419
- if (input.capabilities === undefined) {
420
- throw planStartRecoveryError('environment_information_missing');
421
- }
422
- 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);
423
550
  if (run === undefined)
424
551
  throw new KiokukoError('INTEGRITY_ERROR', 'Enno ledger run disappeared during plan validation');
425
552
  try {
426
553
  assertCapabilityCatalogBinding(run.metadata, input.capabilities);
554
+ return null;
427
555
  }
428
556
  catch (error) {
429
557
  if (error instanceof KiokukoError && error.code === 'CONFLICT') {
430
- throw planStartRecoveryError('environment_changed');
558
+ return 'environment_changed';
431
559
  }
432
560
  throw error;
433
561
  }
434
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
+ }
435
576
  function endedBeforeWorkBecausePlanCatalogWasLost(database, snapshot, capabilities) {
436
577
  if (capabilities === undefined
437
578
  || snapshot.status !== 'blocked'
@@ -459,31 +600,29 @@ function endedBeforeWorkBecausePlanCatalogWasLost(database, snapshot, capabiliti
459
600
  throw error;
460
601
  }
461
602
  }
462
- function sanitizedIdeal(ideal, repositoryRoot) {
463
- return parseOdunoIdeal(sanitizeJson(ideal, { workspace: repositoryRoot }).value);
464
- }
465
603
  export function submitOdunoIdeal(database, rawInput) {
466
- const input = parseIdealSubmission(rawInput);
604
+ const parsedInput = parseIdealSubmission(rawInput);
605
+ const before = readEnnoSnapshot(database, identity(database, parsedInput));
606
+ const input = sanitizeIdealSubmission(parsedInput, before.repositoryRoot);
467
607
  const operation = operationIdentity('ideal_submit', input.idempotencyKey, input);
468
608
  const replay = readOperationReceipt(database, input.runId, operation);
469
609
  if (replay !== undefined)
470
610
  return replay;
471
- const before = readEnnoSnapshot(database, identity(input));
472
611
  assertExpected(before, input.expectedRevision, ['oduno_ideal']);
473
612
  if (input.advisoryRoundDigest !== undefined) {
474
613
  requireAdvisoryRound(database, before, 'ideal', before.mutationRevision, advisoryContextForSubmission(before, 'ideal'), input.advisoryRoundDigest);
475
614
  }
476
- const ideal = sanitizedIdeal(input.ideal, before.repositoryRoot);
615
+ const ideal = input.ideal;
477
616
  assertIdealSkillCoverage(before, ideal);
478
617
  return withImmediateTransaction(database, () => {
479
618
  const replayed = readOperationReceipt(database, input.runId, operation);
480
619
  if (replayed !== undefined)
481
620
  return replayed;
482
- const current = readEnnoSnapshot(database, identity(input));
621
+ const current = readEnnoSnapshot(database, identity(database, input));
483
622
  assertExpected(current, input.expectedRevision, ['oduno_ideal']);
484
623
  assertIdealSkillCoverage(current, ideal);
485
- startOperationInTransaction(database, input.runId, operation);
486
- const advisoryRound = consumeAdvisoryRoundIfPresent(database, current, 'ideal', current.mutationRevision, advisoryContextForSubmission(current, 'ideal'), input.advisoryRoundDigest);
624
+ const operationOwner = startOperationInTransaction(database, input.runId, operation);
625
+ const advisoryRound = consumeAdvisoryRoundIfPresent(database, current, 'ideal', current.mutationRevision, advisoryContextForSubmission(current, 'ideal'), input.advisoryRoundDigest, input.advisoryDisposition);
487
626
  updateContractInTransaction(database, current, {
488
627
  contract: current.contract,
489
628
  status: 'zenki_planning',
@@ -497,16 +636,17 @@ export function submitOdunoIdeal(database, rawInput) {
497
636
  successSignalCount: ideal.successSignals.length,
498
637
  });
499
638
  const response = {
500
- ennoOduno: stateForSnapshot(readEnnoSnapshot(database, identity(input))),
639
+ ennoOduno: stateForSnapshot(readEnnoSnapshot(database, identity(database, input))),
501
640
  ...(advisoryRound === undefined ? {} : { advisoryRound }),
502
641
  };
503
- completeOperationInTransaction(database, input.runId, operation, response);
642
+ completeOperationInTransaction(database, input.runId, operation, operationOwner, response);
504
643
  return response;
505
644
  });
506
645
  }
507
646
  export async function submitEnnoPlan(database, rawInput, dependencies = {}) {
508
- const input = parsePlanSubmission(rawInput);
509
- 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);
510
650
  if (endedBeforeWorkBecausePlanCatalogWasLost(database, current, input.capabilities)) {
511
651
  throw planStartRecoveryError('previous_attempt_ended');
512
652
  }
@@ -516,13 +656,33 @@ export async function submitEnnoPlan(database, rawInput, dependencies = {}) {
516
656
  return replay;
517
657
  const before = current;
518
658
  assertExpected(before, input.expectedRevision, ['zenki_planning']);
519
- 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);
520
680
  if (input.advisoryRoundDigest !== undefined) {
521
681
  requireAdvisoryRound(database, before, 'planning', before.mutationRevision, advisoryContextForSubmission(before, 'planning'), input.advisoryRoundDigest);
522
682
  }
523
683
  const includesCodeChanges = planChangesCode(input, before.taskType);
524
684
  const includesUiWork = planHasUi(input);
525
- assertWorkPlanExpertCoverage(input.workPlan, { includesCodeChanges, includesUiWork });
685
+ assertWorkPlanExpertCoverage(input.workPlan);
526
686
  assertContractVerifierCwds(before.repositoryRoot, {
527
687
  workPlan: input.workPlan,
528
688
  finalVerifiers: input.finalVerifiers,
@@ -539,11 +699,6 @@ export async function submitEnnoPlan(database, rawInput, dependencies = {}) {
539
699
  discoveries: [before.contract.skillSet.intakeDiscovery, zenkiDiscovery],
540
700
  });
541
701
  const unavailable = unavailableRequiredSkills(entries);
542
- const mandatoryWorkSkills = [
543
- STANDARD_SOUL_SKILL_NAME,
544
- ...(includesCodeChanges ? [STANDARD_FUNCTION_SKILL_NAME] : []),
545
- ...(includesUiWork ? [STANDARD_UI_SKILL_NAME] : []),
546
- ];
547
702
  const nextRevision = input.expectedRevision + 1;
548
703
  const contract = {
549
704
  revision: nextRevision,
@@ -556,7 +711,12 @@ export async function submitEnnoPlan(database, rawInput, dependencies = {}) {
556
711
  ...unit,
557
712
  scope: [...unit.scope],
558
713
  dependencies: [...unit.dependencies],
559
- 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),
560
720
  expertRefs: unit.expertRefs.map((reference) => ({ ...reference })),
561
721
  acceptanceCriteria: [...unit.acceptanceCriteria],
562
722
  focusedVerifiers: unit.focusedVerifiers.map((verifier) => ({ ...verifier, args: [...verifier.args] })),
@@ -577,10 +737,10 @@ export async function submitEnnoPlan(database, rawInput, dependencies = {}) {
577
737
  const replayed = readOperationReceipt(database, input.runId, operation);
578
738
  if (replayed !== undefined)
579
739
  return replayed;
580
- const current = readEnnoSnapshot(database, identity(input));
740
+ const current = readEnnoSnapshot(database, identity(database, input));
581
741
  assertExpected(current, input.expectedRevision, ['zenki_planning']);
582
- startOperationInTransaction(database, input.runId, operation);
583
- const advisoryRound = consumeAdvisoryRoundIfPresent(database, current, 'planning', current.mutationRevision, advisoryContextForSubmission(current, 'planning'), input.advisoryRoundDigest);
742
+ const operationOwner = startOperationInTransaction(database, input.runId, operation);
743
+ const advisoryRound = consumeAdvisoryRoundIfPresent(database, current, 'planning', current.mutationRevision, advisoryContextForSubmission(current, 'planning'), input.advisoryRoundDigest, input.advisoryDisposition);
584
744
  const status = unavailable.length > 0
585
745
  ? 'blocked'
586
746
  : needsConfirmation ? 'needs_confirmation' : 'goki_executing';
@@ -599,7 +759,8 @@ export async function submitEnnoPlan(database, rawInput, dependencies = {}) {
599
759
  contractRevision: nextRevision,
600
760
  workUnitIds: contract.workPlan.units.map((unit) => unit.id),
601
761
  });
602
- let updated = readEnnoSnapshot(database, identity(input));
762
+ let updated = readEnnoSnapshot(database, identity(database, input));
763
+ let executionLease;
603
764
  if (status === 'blocked') {
604
765
  appendEnnoEventInTransaction(database, input.runId, 'enno.blocked', 'enno-oduno', 'blocked', {
605
766
  reason: blocker,
@@ -611,28 +772,32 @@ export async function submitEnnoPlan(database, rawInput, dependencies = {}) {
611
772
  appendEnnoEventInTransaction(database, input.runId, 'enno.plan_confirmed', 'enno-oduno', 'not_required', {
612
773
  contractRevision: nextRevision,
613
774
  });
614
- startFirstReadyUnit(database, updated);
615
- updated = readEnnoSnapshot(database, identity(input));
775
+ executionLease = startFirstReadyUnit(database, updated);
776
+ updated = readEnnoSnapshot(database, identity(database, input));
616
777
  }
617
778
  const response = {
618
779
  ennoOduno: stateForSnapshot(updated),
619
780
  ...(advisoryRound === undefined ? {} : { advisoryRound }),
781
+ ...(executionLease === undefined ? {} : { executionLease }),
620
782
  };
621
- completeOperationInTransaction(database, input.runId, operation, response);
783
+ completeOperationInTransaction(database, input.runId, operation, operationOwner, response);
622
784
  return response;
623
785
  });
624
786
  }
625
787
  export function answerEnno(database, rawInput) {
626
- const input = parseEnnoAnswer(rawInput);
788
+ const parsedInput = parseEnnoAnswer(rawInput);
789
+ const before = readEnnoSnapshot(database, identity(database, parsedInput));
790
+ const input = sanitizeEnnoAnswer(parsedInput, before.repositoryRoot);
627
791
  const operation = operationIdentity('answer', input.idempotencyKey, input);
628
792
  const replay = readOperationReceipt(database, input.runId, operation);
629
793
  if (replay !== undefined)
630
794
  return replay;
631
795
  return withImmediateTransaction(database, () => {
632
- const current = readEnnoSnapshot(database, identity(input));
796
+ const current = readEnnoSnapshot(database, identity(database, input));
633
797
  assertExpected(current, input.expectedRevision, input.action === 'cancel' ? ['needs_confirmation', 'zenki_planning'] : ['needs_confirmation']);
634
- startOperationInTransaction(database, input.runId, operation);
798
+ const operationOwner = startOperationInTransaction(database, input.runId, operation);
635
799
  let next;
800
+ let executionLease;
636
801
  if (input.action === 'approve') {
637
802
  updateContractInTransaction(database, current, {
638
803
  contract: current.contract,
@@ -642,9 +807,9 @@ export function answerEnno(database, rawInput) {
642
807
  appendEnnoEventInTransaction(database, input.runId, 'enno.plan_confirmed', 'enno-oduno', 'approved', {
643
808
  contractRevision: current.revision,
644
809
  });
645
- next = readEnnoSnapshot(database, identity(input));
646
- startFirstReadyUnit(database, next);
647
- next = readEnnoSnapshot(database, identity(input));
810
+ next = readEnnoSnapshot(database, identity(database, input));
811
+ executionLease = startFirstReadyUnit(database, next);
812
+ next = readEnnoSnapshot(database, identity(database, input));
648
813
  }
649
814
  else if (input.action === 'revise') {
650
815
  const revisedContract = {
@@ -657,7 +822,7 @@ export function answerEnno(database, rawInput) {
657
822
  confirmationState: 'revision_requested',
658
823
  blocker: input.requestedChanges ?? null,
659
824
  });
660
- next = readEnnoSnapshot(database, identity(input));
825
+ next = readEnnoSnapshot(database, identity(database, input));
661
826
  }
662
827
  else {
663
828
  updateContractInTransaction(database, current, {
@@ -669,10 +834,13 @@ export function answerEnno(database, rawInput) {
669
834
  contractRevision: current.revision,
670
835
  });
671
836
  terminalizeLedgerRunInTransaction(database, input.runId, 'cancelled');
672
- next = readEnnoSnapshot(database, identity(input));
837
+ next = readEnnoSnapshot(database, identity(database, input));
673
838
  }
674
- const response = { ennoOduno: stateForSnapshot(next) };
675
- 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);
676
844
  return response;
677
845
  });
678
846
  }
@@ -718,7 +886,20 @@ function sanitizedReviewSummary(summary, repositoryRoot) {
718
886
  return sanitizeJson({ summary }, { workspace: repositoryRoot }).value.summary;
719
887
  }
720
888
  function blockedForAttemptLimit(database, snapshot, operation) {
721
- 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);
722
903
  updateContractInTransaction(database, snapshot, {
723
904
  contract: snapshot.contract,
724
905
  status: 'blocked',
@@ -735,28 +916,36 @@ function blockedForAttemptLimit(database, snapshot, operation) {
735
916
  workspace: snapshot.workspace,
736
917
  orchestrationId: snapshot.orchestrationId,
737
918
  })) };
738
- completeOperationInTransaction(database, snapshot.runId, operation, response);
919
+ completeOperationInTransaction(database, snapshot.runId, operation, operationOwner, response);
739
920
  return response;
740
921
  }
741
922
  export async function reportEnnoWork(database, rawInput, dependencies = {}) {
742
- const input = parseWorkReport(rawInput);
923
+ const parsedInput = parseWorkReport(rawInput);
924
+ const initial = readEnnoSnapshot(database, identity(database, parsedInput));
925
+ const input = sanitizeWorkReport(parsedInput, initial.repositoryRoot);
743
926
  const operation = operationIdentity('work_report', input.idempotencyKey, input);
744
927
  const replay = readOperationReceipt(database, input.runId, operation);
745
928
  if (replay !== undefined)
746
929
  return replay;
747
- const before = readEnnoSnapshot(database, identity(input));
930
+ const before = initial;
748
931
  assertExpected(before, input.expectedRevision, ['goki_executing']);
749
932
  const unit = before.workUnits.find((candidate) => candidate.workUnit.id === input.workUnitId);
750
933
  if (unit === undefined || unit.status !== 'in_progress')
751
934
  throw new KiokukoError('CONFLICT', 'Reported Enno WorkUnit is not active');
752
935
  if (before.attempts >= before.contract.maxAttempts) {
753
- 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
+ });
754
942
  }
755
943
  if (input.result.outcome !== 'completed') {
756
944
  return withImmediateTransaction(database, () => {
757
- const current = readEnnoSnapshot(database, identity(input));
945
+ const current = readEnnoSnapshot(database, identity(database, input));
758
946
  assertExpected(current, input.expectedRevision, ['goki_executing']);
759
- startOperationInTransaction(database, input.runId, operation);
947
+ assertExecutionLeaseInTransaction(database, current, input);
948
+ const operationOwner = startOperationInTransaction(database, input.runId, operation);
760
949
  const currentUnit = current.workUnits.find((candidate) => candidate.workUnit.id === input.workUnitId);
761
950
  const attempts = current.attempts + 1;
762
951
  const mustBlock = input.result.outcome === 'blocked' || attempts >= current.contract.maxAttempts;
@@ -781,21 +970,25 @@ export async function reportEnnoWork(database, rawInput, dependencies = {}) {
781
970
  attempt: attempts,
782
971
  });
783
972
  if (mustBlock) {
973
+ releaseExecutionLeaseInTransaction(database, input.runId);
784
974
  appendEnnoEventInTransaction(database, input.runId, 'enno.blocked', 'enno-oduno', 'blocked', {
785
975
  reason: input.result.summary,
786
976
  });
787
977
  terminalizeLedgerRunInTransaction(database, input.runId, 'failed');
788
978
  }
789
- const response = { ennoOduno: stateForSnapshot(readEnnoSnapshot(database, identity(input))) };
790
- completeOperationInTransaction(database, input.runId, operation, response);
979
+ const response = { ennoOduno: stateForSnapshot(readEnnoSnapshot(database, identity(database, input))) };
980
+ completeOperationInTransaction(database, input.runId, operation, operationOwner, response);
791
981
  return response;
792
982
  });
793
983
  }
794
984
  let verifierRunIds = [];
985
+ let operationOwner = '';
986
+ const verifierLeaseMs = operationLeaseMsForVerifiers(unit.workUnit.focusedVerifiers);
795
987
  withImmediateTransaction(database, () => {
796
- const current = readEnnoSnapshot(database, identity(input));
988
+ const current = readEnnoSnapshot(database, identity(database, input));
797
989
  assertExpected(current, input.expectedRevision, ['goki_executing']);
798
- startOperationInTransaction(database, input.runId, operation);
990
+ renewExecutionLeaseInTransaction(database, current, input, verifierLeaseMs);
991
+ operationOwner = startOperationInTransaction(database, input.runId, operation, verifierLeaseMs);
799
992
  const currentUnit = current.workUnits.find((candidate) => candidate.workUnit.id === input.workUnitId);
800
993
  if (currentUnit === undefined || currentUnit.status !== 'in_progress') {
801
994
  throw new KiokukoError('CONFLICT', 'Reported Enno WorkUnit changed concurrently');
@@ -817,8 +1010,9 @@ export async function reportEnnoWork(database, rawInput, dependencies = {}) {
817
1010
  }
818
1011
  const results = sanitizedVerifierResults(rawResults, before.repositoryRoot);
819
1012
  return withImmediateTransaction(database, () => {
820
- const current = readEnnoSnapshot(database, identity(input));
1013
+ const current = readEnnoSnapshot(database, identity(database, input));
821
1014
  assertExpected(current, input.expectedRevision, ['goki_executing']);
1015
+ assertExecutionLeaseInTransaction(database, current, input);
822
1016
  finishVerifierRunsInTransaction(database, verifierRunIds, results);
823
1017
  const passed = results.every((result) => result.status === 'passed');
824
1018
  const unsafe = results.some((result) => result.status === 'spawn_failed');
@@ -849,7 +1043,9 @@ export async function reportEnnoWork(database, rawInput, dependencies = {}) {
849
1043
  mutated: input.result.mutated,
850
1044
  changedPaths: input.result.changedPaths,
851
1045
  });
852
- let next = readEnnoSnapshot(database, identity(input));
1046
+ releaseExecutionLeaseInTransaction(database, input.runId);
1047
+ let next = readEnnoSnapshot(database, identity(database, input));
1048
+ let executionLease;
853
1049
  if (mustBlock) {
854
1050
  appendEnnoEventInTransaction(database, input.runId, 'enno.blocked', 'enno-oduno', 'blocked', {
855
1051
  reason: unsafe ? 'spawn_failed' : 'focused_verification_failed',
@@ -865,67 +1061,70 @@ export async function reportEnnoWork(database, rawInput, dependencies = {}) {
865
1061
  });
866
1062
  }
867
1063
  else {
868
- startFirstReadyUnit(database, next);
1064
+ executionLease = startFirstReadyUnit(database, next);
869
1065
  }
870
- next = readEnnoSnapshot(database, identity(input));
1066
+ next = readEnnoSnapshot(database, identity(database, input));
871
1067
  }
872
- const response = { ennoOduno: stateForSnapshot(next), verifierResults: results };
873
- 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);
874
1074
  return response;
875
1075
  });
876
1076
  }
877
- export async function finishEnno(database, rawInput, dependencies = {}) {
878
- const input = parseFinishRequest(rawInput);
879
- const operation = operationIdentity('finish', input.idempotencyKey, input);
1077
+ export async function prepareEnnoVerification(database, rawInput, dependencies = {}) {
1078
+ const input = parseVerificationPrepare(rawInput);
1079
+ const before = readEnnoSnapshot(database, identity(database, input));
1080
+ const operation = operationIdentity('verify_prepare', input.idempotencyKey, input);
880
1081
  const replay = readOperationReceipt(database, input.runId, operation);
881
1082
  if (replay !== undefined)
882
1083
  return replay;
883
- const before = readEnnoSnapshot(database, identity(input));
884
1084
  assertExpected(before, input.expectedRevision, ['enno_verifying']);
885
- if (input.advisoryRoundDigest !== undefined) {
886
- requireAdvisoryRound(database, before, 'final_review', before.mutationRevision, advisoryContextForSubmission(before, 'final_review'), input.advisoryRoundDigest);
887
- }
888
- const reviewSummary = sanitizedReviewSummary(input.review.summary, before.repositoryRoot);
1085
+ const preRepositoryState = captureRepositoryState(before.repositoryRoot);
1086
+ const verifierSpecDigest = canonicalContentHash(before.contract.finalVerifiers);
889
1087
  if (before.attempts >= before.contract.maxAttempts) {
890
- return withImmediateTransaction(database, () => blockedForAttemptLimit(database, readEnnoSnapshot(database, identity(input)), operation));
1088
+ return withImmediateTransaction(database, () => blockedForAttemptLimit(database, readEnnoSnapshot(database, identity(database, input)), operation));
891
1089
  }
892
1090
  let verifierRunIds = [];
893
- let cachedVerifierResults;
894
- let advisoryRound;
1091
+ let operationOwner = '';
1092
+ let cachedResults;
1093
+ const verifierLeaseMs = operationLeaseMsForVerifiers(before.contract.finalVerifiers);
895
1094
  withImmediateTransaction(database, () => {
896
- const current = readEnnoSnapshot(database, identity(input));
1095
+ const current = readEnnoSnapshot(database, identity(database, input));
897
1096
  assertExpected(current, input.expectedRevision, ['enno_verifying']);
898
- advisoryRound = consumeAdvisoryRoundIfPresent(database, current, 'final_review', current.mutationRevision, advisoryContextForSubmission(current, 'final_review'), input.advisoryRoundDigest);
899
- startOperationInTransaction(database, input.runId, operation);
900
- cachedVerifierResults = readFreshFinalVerifierResults(database, {
1097
+ operationOwner = startOperationInTransaction(database, input.runId, operation, verifierLeaseMs);
1098
+ cachedResults = readFreshFinalVerifierResults(database, {
901
1099
  runId: input.runId,
902
1100
  revision: current.revision,
903
1101
  mutationRevision: current.mutationRevision,
904
1102
  verifiers: current.contract.finalVerifiers,
1103
+ repositoryDigest: preRepositoryState.digest,
905
1104
  });
906
- if (cachedVerifierResults === undefined) {
1105
+ if (cachedResults === undefined) {
907
1106
  verifierRunIds = startVerifierRunsInTransaction(database, {
908
1107
  runId: input.runId,
909
1108
  workUnitId: null,
910
1109
  revision: current.revision,
911
1110
  mutationRevision: current.mutationRevision,
912
1111
  verifiers: current.contract.finalVerifiers,
1112
+ repositoryEvidence: {
1113
+ policyVersion: preRepositoryState.policyVersion,
1114
+ preDigest: preRepositoryState.digest,
1115
+ verifierSpecDigest,
1116
+ },
913
1117
  });
914
1118
  }
915
- appendEnnoEventInTransaction(database, input.runId, 'enno.review_started', 'enno-oduno', 'started', {
916
- contractRevision: current.revision,
917
- mutationRevision: current.mutationRevision,
918
- requestedDecision: input.review.decision,
919
- });
920
- appendEnnoEventInTransaction(database, input.runId, 'enno.verification_started', 'enno-oduno', cachedVerifierResults === undefined ? 'started' : 'reused', {
1119
+ appendEnnoEventInTransaction(database, input.runId, 'enno.verification_started', 'enno-oduno', cachedResults === undefined ? 'started' : 'reused', {
921
1120
  contractRevision: current.revision,
922
1121
  mutationRevision: current.mutationRevision,
923
1122
  verifierIds: current.contract.finalVerifiers.map((verifier) => verifier.id),
924
1123
  });
925
1124
  });
926
1125
  let results;
927
- if (cachedVerifierResults !== undefined) {
928
- results = cachedVerifierResults;
1126
+ if (cachedResults !== undefined) {
1127
+ results = cachedResults;
929
1128
  }
930
1129
  else {
931
1130
  try {
@@ -935,11 +1134,84 @@ export async function finishEnno(database, rawInput, dependencies = {}) {
935
1134
  results = spawnFailedVerifierResults(before.contract.finalVerifiers);
936
1135
  }
937
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
+ }));
938
1146
  return withImmediateTransaction(database, () => {
939
- const current = readEnnoSnapshot(database, identity(input));
1147
+ const current = readEnnoSnapshot(database, identity(database, input));
940
1148
  assertExpected(current, input.expectedRevision, ['enno_verifying']);
941
1149
  if (verifierRunIds.length > 0)
942
- finishVerifierRunsInTransaction(database, verifierRunIds, results);
1150
+ finishVerifierRunsInTransaction(database, verifierRunIds, results, {
1151
+ postDigest: postRepositoryState.digest,
1152
+ changedDuringVerification,
1153
+ });
1154
+ const response = {
1155
+ ennoOduno: stateForSnapshot(readEnnoSnapshot(database, identity(database, input))),
1156
+ verifierResults: results,
1157
+ };
1158
+ completeOperationInTransaction(database, input.runId, operation, operationOwner, response);
1159
+ return response;
1160
+ });
1161
+ }
1162
+ export async function finishEnno(database, rawInput, dependencies = {}) {
1163
+ const parsedInput = parseFinishRequest(rawInput);
1164
+ const initial = readEnnoSnapshot(database, identity(database, parsedInput));
1165
+ const input = sanitizeFinishRequest(parsedInput, initial.repositoryRoot);
1166
+ const operation = operationIdentity('finish', input.idempotencyKey, input);
1167
+ const replay = readOperationReceipt(database, input.runId, operation);
1168
+ if (replay !== undefined)
1169
+ return replay;
1170
+ const before = initial;
1171
+ assertExpected(before, input.expectedRevision, ['enno_verifying']);
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
+ }
1179
+ requireAdvisoryRound(database, before, 'final_review', before.mutationRevision, advisoryContextForSubmission(before, 'final_review'), input.advisoryRoundDigest);
1180
+ }
1181
+ const reviewSummary = sanitizedReviewSummary(input.review.summary, before.repositoryRoot);
1182
+ if (before.attempts >= before.contract.maxAttempts) {
1183
+ return withImmediateTransaction(database, () => blockedForAttemptLimit(database, readEnnoSnapshot(database, identity(database, input)), operation));
1184
+ }
1185
+ return withImmediateTransaction(database, () => {
1186
+ const current = readEnnoSnapshot(database, identity(database, input));
1187
+ assertExpected(current, input.expectedRevision, ['enno_verifying']);
1188
+ const currentRepositoryDigest = captureRepositoryState(current.repositoryRoot).digest;
1189
+ const results = readFreshFinalVerifierResults(database, {
1190
+ runId: input.runId,
1191
+ revision: current.revision,
1192
+ mutationRevision: current.mutationRevision,
1193
+ verifiers: current.contract.finalVerifiers,
1194
+ repositoryDigest: currentRepositoryDigest,
1195
+ });
1196
+ if (results === undefined) {
1197
+ throw new KiokukoError('CONFLICT', 'Final verification evidence is not prepared; call enno_verify_prepare first');
1198
+ }
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);
1205
+ appendEnnoEventInTransaction(database, input.runId, 'enno.review_started', 'enno-oduno', 'started', {
1206
+ contractRevision: current.revision,
1207
+ mutationRevision: current.mutationRevision,
1208
+ requestedDecision: input.review.decision,
1209
+ });
1210
+ appendEnnoEventInTransaction(database, input.runId, 'enno.verification_started', 'enno-oduno', 'reused', {
1211
+ contractRevision: current.revision,
1212
+ mutationRevision: current.mutationRevision,
1213
+ verifierIds: current.contract.finalVerifiers.map((verifier) => verifier.id),
1214
+ });
943
1215
  const passed = results.length > 0 && results.every((result) => result.status === 'passed');
944
1216
  const unsafe = results.some((result) => result.status === 'spawn_failed');
945
1217
  const attempts = current.attempts + 1;
@@ -971,7 +1243,7 @@ export async function finishEnno(database, rawInput, dependencies = {}) {
971
1243
  mutationRevision: current.mutationRevision,
972
1244
  statuses: results.map((result) => ({ verifierId: result.verifier.id, status: result.status })),
973
1245
  });
974
- let next = readEnnoSnapshot(database, identity(input));
1246
+ let next = readEnnoSnapshot(database, identity(database, input));
975
1247
  if (accepted) {
976
1248
  appendEnnoEventInTransaction(database, input.runId, 'enno.review_accepted', 'enno-oduno', 'accepted', {
977
1249
  contractRevision: current.revision,
@@ -999,40 +1271,38 @@ export async function finishEnno(database, rawInput, dependencies = {}) {
999
1271
  toContractRevision: next.revision,
1000
1272
  reason: reviewFeedback,
1001
1273
  });
1002
- next = readEnnoSnapshot(database, identity(input));
1274
+ next = readEnnoSnapshot(database, identity(database, input));
1003
1275
  }
1004
1276
  const response = {
1005
1277
  ennoOduno: stateForSnapshot(next),
1006
1278
  verifierResults: results,
1007
1279
  ...(advisoryRound === undefined ? {} : { advisoryRound }),
1008
1280
  };
1009
- completeOperationInTransaction(database, input.runId, operation, response);
1281
+ completeOperationInTransaction(database, input.runId, operation, operationOwner, response);
1010
1282
  return response;
1011
1283
  });
1012
1284
  }
1013
- function sanitizedMeditation(meditation, repositoryRoot) {
1014
- return parseOdunoMeditation(sanitizeJson(meditation, { workspace: repositoryRoot }).value);
1015
- }
1016
1285
  export function submitOdunoMeditation(database, rawInput) {
1017
- const input = parseMeditationSubmission(rawInput);
1286
+ const parsedInput = parseMeditationSubmission(rawInput);
1287
+ const before = readEnnoSnapshot(database, identity(database, parsedInput));
1288
+ const input = sanitizeMeditationSubmission(parsedInput, before.repositoryRoot);
1018
1289
  const operation = operationIdentity('meditation_submit', input.idempotencyKey, input);
1019
1290
  const replay = readOperationReceipt(database, input.runId, operation);
1020
1291
  if (replay !== undefined)
1021
1292
  return replay;
1022
- const before = readEnnoSnapshot(database, identity(input));
1023
1293
  assertExpected(before, input.expectedRevision, ['oduno_meditation']);
1024
1294
  if (before.ideal === null)
1025
1295
  throw new KiokukoError('INTEGRITY_ERROR', 'Oduno meditation requires a persisted ideal');
1026
- const meditation = sanitizedMeditation(input.meditation, before.repositoryRoot);
1296
+ const meditation = input.meditation;
1027
1297
  return withImmediateTransaction(database, () => {
1028
1298
  const replayed = readOperationReceipt(database, input.runId, operation);
1029
1299
  if (replayed !== undefined)
1030
1300
  return replayed;
1031
- const current = readEnnoSnapshot(database, identity(input));
1301
+ const current = readEnnoSnapshot(database, identity(database, input));
1032
1302
  assertExpected(current, input.expectedRevision, ['oduno_meditation']);
1033
1303
  if (current.ideal === null)
1034
1304
  throw new KiokukoError('INTEGRITY_ERROR', 'Oduno meditation requires a persisted ideal');
1035
- startOperationInTransaction(database, input.runId, operation);
1305
+ const operationOwner = startOperationInTransaction(database, input.runId, operation);
1036
1306
  updateContractInTransaction(database, current, {
1037
1307
  contract: current.contract,
1038
1308
  status: 'completed',
@@ -1050,8 +1320,8 @@ export function submitOdunoMeditation(database, rawInput) {
1050
1320
  mutationRevision: current.mutationRevision,
1051
1321
  });
1052
1322
  terminalizeLedgerRunInTransaction(database, input.runId, 'completed');
1053
- const response = { ennoOduno: stateForSnapshot(readEnnoSnapshot(database, identity(input))) };
1054
- completeOperationInTransaction(database, input.runId, operation, response);
1323
+ const response = { ennoOduno: stateForSnapshot(readEnnoSnapshot(database, identity(database, input))) };
1324
+ completeOperationInTransaction(database, input.runId, operation, operationOwner, response);
1055
1325
  return response;
1056
1326
  });
1057
1327
  }