@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.
- package/README.ja.md +83 -45
- package/README.ko.md +80 -40
- package/README.md +85 -45
- package/README.zh-CN.md +76 -39
- package/dist/agent-file/render.d.ts +1 -1
- package/dist/agent-file/render.d.ts.map +1 -1
- package/dist/agent-file/render.js +5 -3
- package/dist/agent-file/render.js.map +1 -1
- package/dist/akinator/agent-task.js +2 -2
- package/dist/akinator/agent-task.js.map +1 -1
- package/dist/akinator/capabilities.d.ts +5 -1
- package/dist/akinator/capabilities.d.ts.map +1 -1
- package/dist/akinator/capabilities.js +20 -3
- package/dist/akinator/capabilities.js.map +1 -1
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +36 -9
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +33 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +2 -1
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/use.d.ts +1 -0
- package/dist/commands/use.d.ts.map +1 -1
- package/dist/commands/use.js +76 -16
- package/dist/commands/use.js.map +1 -1
- package/dist/enno-oduno/adapters.d.ts +4 -1
- package/dist/enno-oduno/adapters.d.ts.map +1 -1
- package/dist/enno-oduno/adapters.js +100 -63
- package/dist/enno-oduno/adapters.js.map +1 -1
- package/dist/enno-oduno/advisory-store.d.ts +6 -0
- package/dist/enno-oduno/advisory-store.d.ts.map +1 -1
- package/dist/enno-oduno/advisory-store.js +13 -1
- package/dist/enno-oduno/advisory-store.js.map +1 -1
- package/dist/enno-oduno/advisory.d.ts +2 -0
- package/dist/enno-oduno/advisory.d.ts.map +1 -1
- package/dist/enno-oduno/advisory.js +207 -25
- package/dist/enno-oduno/advisory.js.map +1 -1
- package/dist/enno-oduno/confirmation.d.ts.map +1 -1
- package/dist/enno-oduno/confirmation.js +3 -0
- package/dist/enno-oduno/confirmation.js.map +1 -1
- package/dist/enno-oduno/directives.d.ts.map +1 -1
- package/dist/enno-oduno/directives.js +67 -36
- package/dist/enno-oduno/directives.js.map +1 -1
- package/dist/enno-oduno/experts.d.ts +1 -1
- package/dist/enno-oduno/experts.d.ts.map +1 -1
- package/dist/enno-oduno/experts.js +21 -7
- package/dist/enno-oduno/experts.js.map +1 -1
- package/dist/enno-oduno/instructions.d.ts +5 -3
- package/dist/enno-oduno/instructions.d.ts.map +1 -1
- package/dist/enno-oduno/instructions.js +4 -2
- package/dist/enno-oduno/instructions.js.map +1 -1
- package/dist/enno-oduno/plan-recovery.d.ts +15 -0
- package/dist/enno-oduno/plan-recovery.d.ts.map +1 -1
- package/dist/enno-oduno/plan-recovery.js +26 -0
- package/dist/enno-oduno/plan-recovery.js.map +1 -1
- package/dist/enno-oduno/repository-state.d.ts +8 -0
- package/dist/enno-oduno/repository-state.d.ts.map +1 -0
- package/dist/enno-oduno/repository-state.js +139 -0
- package/dist/enno-oduno/repository-state.js.map +1 -0
- package/dist/enno-oduno/role-runner.d.ts.map +1 -1
- package/dist/enno-oduno/role-runner.js +50 -2
- package/dist/enno-oduno/role-runner.js.map +1 -1
- package/dist/enno-oduno/sanitize.d.ts +17 -0
- package/dist/enno-oduno/sanitize.d.ts.map +1 -0
- package/dist/enno-oduno/sanitize.js +88 -0
- package/dist/enno-oduno/sanitize.js.map +1 -0
- package/dist/enno-oduno/schemas.d.ts +314 -29
- package/dist/enno-oduno/schemas.d.ts.map +1 -1
- package/dist/enno-oduno/schemas.js +267 -50
- package/dist/enno-oduno/schemas.js.map +1 -1
- package/dist/enno-oduno/service.d.ts +3 -1
- package/dist/enno-oduno/service.d.ts.map +1 -1
- package/dist/enno-oduno/service.js +416 -146
- package/dist/enno-oduno/service.js.map +1 -1
- package/dist/enno-oduno/store.d.ts +44 -5
- package/dist/enno-oduno/store.d.ts.map +1 -1
- package/dist/enno-oduno/store.js +367 -35
- package/dist/enno-oduno/store.js.map +1 -1
- package/dist/enno-oduno/types.d.ts +106 -6
- package/dist/enno-oduno/types.d.ts.map +1 -1
- package/dist/enno-oduno/types.js +2 -0
- package/dist/enno-oduno/types.js.map +1 -1
- package/dist/enno-oduno/validation-errors.d.ts +92 -0
- package/dist/enno-oduno/validation-errors.d.ts.map +1 -0
- package/dist/enno-oduno/validation-errors.js +151 -0
- package/dist/enno-oduno/validation-errors.js.map +1 -0
- package/dist/enno-oduno/verifier.d.ts +1 -0
- package/dist/enno-oduno/verifier.d.ts.map +1 -1
- package/dist/enno-oduno/verifier.js +99 -16
- package/dist/enno-oduno/verifier.js.map +1 -1
- package/dist/ledger/types.d.ts +1 -1
- package/dist/ledger/types.d.ts.map +1 -1
- package/dist/ledger/types.js +1 -1
- package/dist/ledger/types.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +76 -16
- package/dist/mcp/server.js.map +1 -1
- package/dist/memory/secrets.d.ts.map +1 -1
- package/dist/memory/secrets.js +1 -0
- package/dist/memory/secrets.js.map +1 -1
- package/dist/server/routes/agent-capability-gate.js +2 -2
- package/dist/server/routes/agent-capability-gate.js.map +1 -1
- package/dist/setup/render.js +1 -1
- package/dist/setup/render.js.map +1 -1
- package/dist/setup/standard-skills.d.ts +12 -5
- package/dist/setup/standard-skills.d.ts.map +1 -1
- package/dist/setup/standard-skills.js +12 -2
- package/dist/setup/standard-skills.js.map +1 -1
- package/migrations/017_enno_advisory_protocol_v2.sql +101 -0
- package/migrations/018_enno_repository_routing.sql +5 -0
- package/migrations/019_enno_execution_integrity.sql +149 -0
- package/package.json +1 -1
- package/skills/kiokuko-enno-oduno/SKILL.md +19 -8
- package/skills/kiokuko-single-purpose-functions/SKILL.md +18 -10
- package/skills/kiokuko-single-purpose-functions/references/problem-shaping-and-language.md +84 -0
- package/skills/kiokuko-ui-design-soul/SKILL.md +5 -1
- package/skills/memory-reasoning/SKILL.md +52 -0
- 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 {
|
|
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,
|
|
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
|
|
164
|
+
requestDigest,
|
|
165
|
+
...(legacyRequestDigest === requestDigest ? {} : { legacyRequestDigests: [legacyRequestDigest] }),
|
|
150
166
|
};
|
|
151
167
|
}
|
|
152
168
|
function advisoryContextForSubmission(snapshot, phase) {
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
190
|
-
|
|
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
|
-
|
|
193
|
-
|
|
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
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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 = {
|
|
251
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
419
|
-
if (input.capabilities === undefined)
|
|
420
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
509
|
-
const current = readEnnoSnapshot(database, identity(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 = {
|
|
675
|
-
|
|
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
|
|
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 =
|
|
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, () =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = {
|
|
873
|
-
|
|
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
|
|
878
|
-
const input =
|
|
879
|
-
const
|
|
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
|
-
|
|
886
|
-
|
|
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
|
|
894
|
-
let
|
|
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
|
-
|
|
899
|
-
|
|
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 (
|
|
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.
|
|
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 (
|
|
928
|
-
results =
|
|
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
|
|
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 =
|
|
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
|
}
|