@askdkc/kiokuko 0.1.32 → 0.2.1
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 +59 -4
- package/README.ko.md +54 -4
- package/README.md +66 -4
- package/README.zh-CN.md +54 -4
- 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 +13 -6
- package/dist/agent-file/render.js.map +1 -1
- package/dist/akinator/agent-task.d.ts +2 -0
- package/dist/akinator/agent-task.d.ts.map +1 -1
- package/dist/akinator/agent-task.js +49 -13
- package/dist/akinator/agent-task.js.map +1 -1
- package/dist/akinator/capabilities.d.ts.map +1 -1
- package/dist/akinator/capabilities.js +21 -14
- package/dist/akinator/capabilities.js.map +1 -1
- package/dist/akinator/skill-discovery-attempt.d.ts +7 -1
- package/dist/akinator/skill-discovery-attempt.d.ts.map +1 -1
- package/dist/akinator/skill-discovery-attempt.js +104 -25
- package/dist/akinator/skill-discovery-attempt.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +5 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +18 -9
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/enno.d.ts +7 -0
- package/dist/commands/enno.d.ts.map +1 -0
- package/dist/commands/enno.js +76 -0
- package/dist/commands/enno.js.map +1 -0
- package/dist/commands/setup.d.ts +5 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +83 -18
- package/dist/commands/setup.js.map +1 -1
- package/dist/config/paths.d.ts +6 -0
- package/dist/config/paths.d.ts.map +1 -1
- package/dist/config/paths.js +40 -0
- package/dist/config/paths.js.map +1 -1
- package/dist/enno-oduno/adapters.d.ts +18 -0
- package/dist/enno-oduno/adapters.d.ts.map +1 -0
- package/dist/enno-oduno/adapters.js +216 -0
- package/dist/enno-oduno/adapters.js.map +1 -0
- package/dist/enno-oduno/advisory-store.d.ts +25 -0
- package/dist/enno-oduno/advisory-store.d.ts.map +1 -0
- package/dist/enno-oduno/advisory-store.js +93 -0
- package/dist/enno-oduno/advisory-store.js.map +1 -0
- package/dist/enno-oduno/advisory.d.ts +16 -0
- package/dist/enno-oduno/advisory.d.ts.map +1 -0
- package/dist/enno-oduno/advisory.js +128 -0
- package/dist/enno-oduno/advisory.js.map +1 -0
- package/dist/enno-oduno/confirmation.d.ts +4 -0
- package/dist/enno-oduno/confirmation.d.ts.map +1 -0
- package/dist/enno-oduno/confirmation.js +124 -0
- package/dist/enno-oduno/confirmation.js.map +1 -0
- package/dist/enno-oduno/directives.d.ts +12 -0
- package/dist/enno-oduno/directives.d.ts.map +1 -0
- package/dist/enno-oduno/directives.js +241 -0
- package/dist/enno-oduno/directives.js.map +1 -0
- package/dist/enno-oduno/enno-oduno.d.mts +2 -0
- package/dist/enno-oduno/enno-oduno.d.mts.map +1 -0
- package/dist/enno-oduno/enno-oduno.mjs +3 -0
- package/dist/enno-oduno/enno-oduno.mjs.map +1 -0
- package/dist/enno-oduno/experts.d.ts +6 -0
- package/dist/enno-oduno/experts.d.ts.map +1 -0
- package/dist/enno-oduno/experts.js +18 -0
- package/dist/enno-oduno/experts.js.map +1 -0
- package/dist/enno-oduno/goki.d.mts +2 -0
- package/dist/enno-oduno/goki.d.mts.map +1 -0
- package/dist/enno-oduno/goki.mjs +3 -0
- package/dist/enno-oduno/goki.mjs.map +1 -0
- package/dist/enno-oduno/handoff.d.ts +4 -0
- package/dist/enno-oduno/handoff.d.ts.map +1 -0
- package/dist/enno-oduno/handoff.js +18 -0
- package/dist/enno-oduno/handoff.js.map +1 -0
- package/dist/enno-oduno/harness.d.ts +20 -0
- package/dist/enno-oduno/harness.d.ts.map +1 -0
- package/dist/enno-oduno/harness.js +65 -0
- package/dist/enno-oduno/harness.js.map +1 -0
- package/dist/enno-oduno/instructions.d.ts +5 -0
- package/dist/enno-oduno/instructions.d.ts.map +1 -0
- package/dist/enno-oduno/instructions.js +10 -0
- package/dist/enno-oduno/instructions.js.map +1 -0
- package/dist/enno-oduno/role-runner.d.ts +9 -0
- package/dist/enno-oduno/role-runner.d.ts.map +1 -0
- package/dist/enno-oduno/role-runner.js +171 -0
- package/dist/enno-oduno/role-runner.js.map +1 -0
- package/dist/enno-oduno/schemas.d.ts +668 -0
- package/dist/enno-oduno/schemas.d.ts.map +1 -0
- package/dist/enno-oduno/schemas.js +381 -0
- package/dist/enno-oduno/schemas.js.map +1 -0
- package/dist/enno-oduno/service.d.ts +49 -0
- package/dist/enno-oduno/service.d.ts.map +1 -0
- package/dist/enno-oduno/service.js +1005 -0
- package/dist/enno-oduno/service.js.map +1 -0
- package/dist/enno-oduno/skills.d.ts +22 -0
- package/dist/enno-oduno/skills.d.ts.map +1 -0
- package/dist/enno-oduno/skills.js +127 -0
- package/dist/enno-oduno/skills.js.map +1 -0
- package/dist/enno-oduno/store.d.ts +67 -0
- package/dist/enno-oduno/store.d.ts.map +1 -0
- package/dist/enno-oduno/store.js +380 -0
- package/dist/enno-oduno/store.js.map +1 -0
- package/dist/enno-oduno/types.d.ts +357 -0
- package/dist/enno-oduno/types.d.ts.map +1 -0
- package/dist/enno-oduno/types.js +64 -0
- package/dist/enno-oduno/types.js.map +1 -0
- package/dist/enno-oduno/verifier.d.ts +9 -0
- package/dist/enno-oduno/verifier.d.ts.map +1 -0
- package/dist/enno-oduno/verifier.js +101 -0
- package/dist/enno-oduno/verifier.js.map +1 -0
- package/dist/enno-oduno/zenki.d.mts +2 -0
- package/dist/enno-oduno/zenki.d.mts.map +1 -0
- package/dist/enno-oduno/zenki.mjs +3 -0
- package/dist/enno-oduno/zenki.mjs.map +1 -0
- package/dist/ledger/types.d.ts +1 -1
- package/dist/ledger/types.d.ts.map +1 -1
- package/dist/ledger/types.js +5 -0
- package/dist/ledger/types.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +76 -26
- package/dist/mcp/server.js.map +1 -1
- package/dist/setup/claude-config.d.ts +2 -0
- package/dist/setup/claude-config.d.ts.map +1 -1
- package/dist/setup/claude-config.js +12 -0
- package/dist/setup/claude-config.js.map +1 -1
- package/dist/setup/enno-client-config.d.ts +9 -0
- package/dist/setup/enno-client-config.d.ts.map +1 -0
- package/dist/setup/enno-client-config.js +172 -0
- package/dist/setup/enno-client-config.js.map +1 -0
- package/dist/setup/opencode-config.d.ts +2 -0
- package/dist/setup/opencode-config.d.ts.map +1 -1
- package/dist/setup/opencode-config.js +12 -0
- package/dist/setup/opencode-config.js.map +1 -1
- package/dist/setup/render.d.ts +2 -0
- package/dist/setup/render.d.ts.map +1 -1
- package/dist/setup/render.js +23 -12
- package/dist/setup/render.js.map +1 -1
- package/dist/setup/standard-skills.d.ts +23 -4
- package/dist/setup/standard-skills.d.ts.map +1 -1
- package/dist/setup/standard-skills.js +51 -1
- package/dist/setup/standard-skills.js.map +1 -1
- package/dist/skills/discovery-service.d.ts.map +1 -1
- package/dist/skills/discovery-service.js +7 -3
- package/dist/skills/discovery-service.js.map +1 -1
- package/dist/skills/types.d.ts +1 -0
- package/dist/skills/types.d.ts.map +1 -1
- package/migrations/013_enno_oduno.sql +176 -0
- package/migrations/014_oduno_reflection_phases.sql +40 -0
- package/migrations/015_skill_discovery_attempt_digests.sql +90 -0
- package/migrations/016_enno_advisory_rounds.sql +67 -0
- package/package.json +7 -1
- package/skills/kiokuko-enno-oduno/SKILL.md +143 -0
- package/skills/kiokuko-enno-oduno/enno-oduno.png +0 -0
- package/skills/kiokuko-single-purpose-functions/SKILL.md +44 -258
- package/skills/kiokuko-single-purpose-functions/references/boundaries-and-ownership.md +50 -0
- package/skills/kiokuko-single-purpose-functions/references/domain-and-types.md +48 -0
- package/skills/kiokuko-single-purpose-functions/references/effects-and-data.md +43 -0
- package/skills/kiokuko-single-purpose-functions/references/protocols-and-idempotency.md +42 -0
- package/skills/kiokuko-single-purpose-functions/references/verification.md +41 -0
- package/skills/kiokuko-soul/SKILL.md +72 -0
- package/skills/kiokuko-ui-design-soul/SKILL.md +39 -1178
- package/skills/kiokuko-ui-design-soul/references/accessibility-and-navigation.md +31 -0
- package/skills/kiokuko-ui-design-soul/references/async-recovery.md +49 -0
- package/skills/kiokuko-ui-design-soul/references/forms-and-controls.md +35 -0
- package/skills/kiokuko-ui-design-soul/references/interaction-feedback.md +34 -0
- package/skills/kiokuko-ui-design-soul/references/responsive-and-platform.md +38 -0
- package/skills/kiokuko-ui-design-soul/references/safety-and-review.md +39 -0
- package/templates/AGENTS.md +11 -6
|
@@ -0,0 +1,1005 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { withImmediateTransaction } from '../db/transaction.js';
|
|
3
|
+
import { KiokukoError } from '../errors.js';
|
|
4
|
+
import { getAkinatorContextService } from '../akinator/service.js';
|
|
5
|
+
import { claimAgentTaskSkillDiscoveryAttempt, completeAgentTaskSkillDiscoveryAttempt, failAgentTaskSkillDiscoveryAttempt, readAgentTaskSkillDiscoveryAttempt, } from '../akinator/skill-discovery-attempt.js';
|
|
6
|
+
import { captureProjectManifestSnapshot, resolveProjectFingerprint, } from '../repository/project-fingerprint.js';
|
|
7
|
+
import { discoverSkills } from '../skills/discovery-service.js';
|
|
8
|
+
import { SkillProviderError } from '../skills/providers/schema.js';
|
|
9
|
+
import { canonicalContentHash, canonicalJson } from '../serialization/validate.js';
|
|
10
|
+
import { sanitizeJson } from '../security/sanitize.js';
|
|
11
|
+
import { directiveForIntake, directiveForRun } from './directives.js';
|
|
12
|
+
import { advisoryDirectiveForSnapshot, advisoryInputDigest, advisoryPhaseForStatus } from './advisory.js';
|
|
13
|
+
import { createAdvisoryRoundInTransaction, ensureAdvisoryRoundConsumedInTransaction, readAdvisoryRound, } from './advisory-store.js';
|
|
14
|
+
import { assertWorkPlanExpertCoverage } from './experts.js';
|
|
15
|
+
import { buildEnnoRequestHandoff } from './handoff.js';
|
|
16
|
+
import { identifyEnnoClientKind } from './harness.js';
|
|
17
|
+
import { assertContractVerifierCwds, parseEnnoAnswer, parseAdviceSubmission, parseFinishRequest, parseIdealSubmission, parseMeditationSubmission, parseOdunoIdeal, parseOdunoMeditation, parsePlanSubmission, parseWorkReport, } from './schemas.js';
|
|
18
|
+
import { completeRequiredSkillList, createSkillSetEntries, orderedUniqueSkillNames, unavailableRequiredSkills, } from './skills.js';
|
|
19
|
+
import { appendEnnoEventInTransaction, completeOperationInTransaction, createEnnoDraft, finishVerifierRunsInTransaction, readEnnoSnapshot, readFreshFinalVerifierResults, readOperationReceipt, replaceWorkUnitsInTransaction, setWorkUnitStatusInTransaction, startOperationInTransaction, startVerifierRunsInTransaction, terminalizeLedgerRunInTransaction, updateContractInTransaction, } from './store.js';
|
|
20
|
+
import { ENNO_APPLICABLE_TASK_TYPES, ENNO_MAX_EXTERNAL_SKILLS, ENNO_MAX_TOTAL_SKILL_QUERIES, ENNO_PROVENANCE_KEYS, } from './types.js';
|
|
21
|
+
import { runVerifiers } from './verifier.js';
|
|
22
|
+
import { STANDARD_FUNCTION_SKILL_NAME, STANDARD_SOUL_SKILL_NAME, STANDARD_UI_SKILL_NAME, } from '../setup/standard-skills.js';
|
|
23
|
+
function stateForSnapshot(snapshot) {
|
|
24
|
+
let nextAction;
|
|
25
|
+
if (snapshot.status === 'intake')
|
|
26
|
+
nextAction = 'answer_intake';
|
|
27
|
+
else if (snapshot.status === 'oduno_ideal')
|
|
28
|
+
nextAction = 'submit_ideal';
|
|
29
|
+
else if (snapshot.status === 'zenki_planning')
|
|
30
|
+
nextAction = 'submit_plan';
|
|
31
|
+
else if (snapshot.status === 'needs_confirmation')
|
|
32
|
+
nextAction = 'ask_user_confirmation';
|
|
33
|
+
else if (snapshot.status === 'goki_executing')
|
|
34
|
+
nextAction = 'execute_work_unit';
|
|
35
|
+
else if (snapshot.status === 'enno_verifying')
|
|
36
|
+
nextAction = 'run_final_verification';
|
|
37
|
+
else if (snapshot.status === 'oduno_meditation')
|
|
38
|
+
nextAction = 'submit_meditation';
|
|
39
|
+
else if (snapshot.status === 'blocked')
|
|
40
|
+
nextAction = 'report_blocker';
|
|
41
|
+
else
|
|
42
|
+
nextAction = 'complete';
|
|
43
|
+
const directive = directiveForRun(snapshot);
|
|
44
|
+
return {
|
|
45
|
+
applicable: true,
|
|
46
|
+
status: snapshot.status,
|
|
47
|
+
orchestrationId: snapshot.orchestrationId,
|
|
48
|
+
clientBinding: {
|
|
49
|
+
status: snapshot.clientSessionId === null ? 'pending' : 'bound',
|
|
50
|
+
clientKind: snapshot.clientKind,
|
|
51
|
+
clientVersion: snapshot.clientVersion,
|
|
52
|
+
identified: snapshot.clientKind !== null,
|
|
53
|
+
},
|
|
54
|
+
contractRevision: snapshot.revision,
|
|
55
|
+
ideal: snapshot.ideal,
|
|
56
|
+
meditation: snapshot.meditation,
|
|
57
|
+
currentRole: directive?.role ?? null,
|
|
58
|
+
directive,
|
|
59
|
+
nextAction,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function intakeEnnoState(input) {
|
|
63
|
+
const directive = directiveForIntake(input);
|
|
64
|
+
return {
|
|
65
|
+
applicable: true,
|
|
66
|
+
status: 'intake',
|
|
67
|
+
orchestrationId: input.orchestrationId,
|
|
68
|
+
clientBinding: {
|
|
69
|
+
status: input.clientSessionId === null ? 'pending' : 'bound',
|
|
70
|
+
clientKind: input.clientKind,
|
|
71
|
+
clientVersion: input.clientVersion,
|
|
72
|
+
identified: input.clientKind !== null,
|
|
73
|
+
},
|
|
74
|
+
contractRevision: null,
|
|
75
|
+
ideal: null,
|
|
76
|
+
meditation: null,
|
|
77
|
+
currentRole: 'enno-oduno',
|
|
78
|
+
directive,
|
|
79
|
+
nextAction: 'answer_intake',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export function inapplicableEnnoState() {
|
|
83
|
+
return {
|
|
84
|
+
applicable: false,
|
|
85
|
+
status: 'intake',
|
|
86
|
+
orchestrationId: null,
|
|
87
|
+
clientBinding: null,
|
|
88
|
+
contractRevision: null,
|
|
89
|
+
ideal: null,
|
|
90
|
+
meditation: null,
|
|
91
|
+
currentRole: null,
|
|
92
|
+
directive: null,
|
|
93
|
+
nextAction: 'complete',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export function ennoStateForPreparedTask(database, prepared, client) {
|
|
97
|
+
const taskType = prepared.intake.profile.taskType;
|
|
98
|
+
const clientKind = identifyEnnoClientKind(client?.kind);
|
|
99
|
+
const clientVersion = clientKind === null ? null : client?.version ?? null;
|
|
100
|
+
const clientSessionId = clientKind === null ? null : client?.sessionId ?? null;
|
|
101
|
+
if (prepared.intake.status === 'needs_answer' && taskType === null) {
|
|
102
|
+
return intakeEnnoState({
|
|
103
|
+
runId: prepared.run.runId,
|
|
104
|
+
orchestrationId: prepared.intake.sessionId,
|
|
105
|
+
clientKind,
|
|
106
|
+
clientVersion,
|
|
107
|
+
clientSessionId,
|
|
108
|
+
question: prepared.intake.question,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (taskType === null || !ENNO_APPLICABLE_TASK_TYPES.includes(taskType)) {
|
|
112
|
+
return inapplicableEnnoState();
|
|
113
|
+
}
|
|
114
|
+
if (prepared.intake.status === 'needs_answer') {
|
|
115
|
+
return intakeEnnoState({
|
|
116
|
+
runId: prepared.run.runId,
|
|
117
|
+
orchestrationId: prepared.intake.sessionId,
|
|
118
|
+
clientKind,
|
|
119
|
+
clientVersion,
|
|
120
|
+
clientSessionId,
|
|
121
|
+
question: prepared.intake.question,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
const handoff = buildEnnoRequestHandoff(prepared.intake.profile, prepared.intake.reasoning);
|
|
125
|
+
const snapshot = createEnnoDraft(database, {
|
|
126
|
+
runId: prepared.run.runId,
|
|
127
|
+
workspace: prepared.project.workspace,
|
|
128
|
+
orchestrationId: prepared.intake.sessionId,
|
|
129
|
+
repositoryRoot: prepared.project.repositoryRoot,
|
|
130
|
+
taskType: taskType,
|
|
131
|
+
taskTarget: prepared.intake.profile.target,
|
|
132
|
+
taskExpected: prepared.intake.profile.expected,
|
|
133
|
+
handoff,
|
|
134
|
+
skillDiscovery: prepared.skillDiscovery,
|
|
135
|
+
...(clientKind === null ? {} : { initialClientKind: clientKind }),
|
|
136
|
+
...(clientVersion === null ? {} : { initialClientVersion: clientVersion }),
|
|
137
|
+
...(clientSessionId === null ? {} : { initialClientSessionId: clientSessionId }),
|
|
138
|
+
});
|
|
139
|
+
return stateForSnapshot(snapshot);
|
|
140
|
+
}
|
|
141
|
+
function operationIdentity(operation, idempotencyKey, input) {
|
|
142
|
+
return {
|
|
143
|
+
operation,
|
|
144
|
+
idempotencyKey,
|
|
145
|
+
requestDigest: canonicalContentHash({ version: 1, operation, input }),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function advisoryContextForSubmission(snapshot, phase) {
|
|
149
|
+
const directive = advisoryDirectiveForSnapshot(snapshot);
|
|
150
|
+
if (directive === undefined || directive.phase !== phase) {
|
|
151
|
+
throw new KiokukoError('CONFLICT', 'Advisory round is not valid for the current Enno phase');
|
|
152
|
+
}
|
|
153
|
+
return directive.context;
|
|
154
|
+
}
|
|
155
|
+
function assertAdvisoryRoundInput(snapshot, phase, mutationRevision, context, inputDigest) {
|
|
156
|
+
if (snapshot.mutationRevision !== mutationRevision) {
|
|
157
|
+
throw new KiokukoError('CONFLICT', 'Enno mutation revision changed');
|
|
158
|
+
}
|
|
159
|
+
const expectedContext = advisoryContextForSubmission(snapshot, phase);
|
|
160
|
+
if (canonicalJson(expectedContext) !== canonicalJson(context)) {
|
|
161
|
+
throw new KiokukoError('CONFLICT', 'Advisory context does not match the current Enno phase');
|
|
162
|
+
}
|
|
163
|
+
const expectedDigest = advisoryInputDigest({
|
|
164
|
+
phase,
|
|
165
|
+
contractRevision: snapshot.revision,
|
|
166
|
+
mutationRevision,
|
|
167
|
+
allowlistedContext: context,
|
|
168
|
+
});
|
|
169
|
+
if (expectedDigest !== inputDigest) {
|
|
170
|
+
throw new KiokukoError('CONFLICT', 'Advisory round digest does not match the current contract');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function requireAdvisoryRound(database, snapshot, phase, mutationRevision, context, inputDigest) {
|
|
174
|
+
assertAdvisoryRoundInput(snapshot, phase, mutationRevision, context, inputDigest);
|
|
175
|
+
const round = readAdvisoryRound(database, {
|
|
176
|
+
runId: snapshot.runId,
|
|
177
|
+
contractRevision: snapshot.revision,
|
|
178
|
+
mutationRevision,
|
|
179
|
+
phase,
|
|
180
|
+
inputDigest,
|
|
181
|
+
});
|
|
182
|
+
if (round === undefined)
|
|
183
|
+
throw new KiokukoError('CONFLICT', 'Required advisory round was not submitted');
|
|
184
|
+
}
|
|
185
|
+
function consumeAdvisoryRoundIfPresent(database, snapshot, phase, mutationRevision, context, inputDigest) {
|
|
186
|
+
if (inputDigest === undefined)
|
|
187
|
+
return undefined;
|
|
188
|
+
assertAdvisoryRoundInput(snapshot, phase, mutationRevision, context, inputDigest);
|
|
189
|
+
return ensureAdvisoryRoundConsumedInTransaction(database, {
|
|
190
|
+
runId: snapshot.runId,
|
|
191
|
+
contractRevision: snapshot.revision,
|
|
192
|
+
mutationRevision,
|
|
193
|
+
phase,
|
|
194
|
+
inputDigest,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
export function submitEnnoAdvice(database, rawInput) {
|
|
198
|
+
const input = parseAdviceSubmission(rawInput);
|
|
199
|
+
const operation = operationIdentity('advice_submit', input.idempotencyKey, input);
|
|
200
|
+
const replay = readOperationReceipt(database, input.runId, operation);
|
|
201
|
+
if (replay !== undefined)
|
|
202
|
+
return replay;
|
|
203
|
+
const before = readEnnoSnapshot(database, identity(input));
|
|
204
|
+
const expectedPhase = advisoryPhaseForStatus(before.status);
|
|
205
|
+
if (expectedPhase !== input.phase)
|
|
206
|
+
throw new KiokukoError('CONFLICT', 'Advisory phase does not match the current Enno state');
|
|
207
|
+
const context = input.allowlistedContext;
|
|
208
|
+
const inputDigest = advisoryInputDigest({
|
|
209
|
+
phase: input.phase,
|
|
210
|
+
contractRevision: before.revision,
|
|
211
|
+
mutationRevision: input.mutationRevision,
|
|
212
|
+
allowlistedContext: context,
|
|
213
|
+
});
|
|
214
|
+
return withImmediateTransaction(database, () => {
|
|
215
|
+
const replayed = readOperationReceipt(database, input.runId, operation);
|
|
216
|
+
if (replayed !== undefined)
|
|
217
|
+
return replayed;
|
|
218
|
+
const current = readEnnoSnapshot(database, identity(input));
|
|
219
|
+
assertExpected(current, input.expectedRevision, [
|
|
220
|
+
input.phase === 'ideal' ? 'oduno_ideal' : input.phase === 'planning' ? 'zenki_planning' : 'enno_verifying',
|
|
221
|
+
]);
|
|
222
|
+
assertAdvisoryRoundInput(current, input.phase, input.mutationRevision, context, inputDigest);
|
|
223
|
+
startOperationInTransaction(database, input.runId, operation);
|
|
224
|
+
const existing = readAdvisoryRound(database, {
|
|
225
|
+
runId: input.runId,
|
|
226
|
+
contractRevision: current.revision,
|
|
227
|
+
mutationRevision: input.mutationRevision,
|
|
228
|
+
phase: input.phase,
|
|
229
|
+
inputDigest,
|
|
230
|
+
});
|
|
231
|
+
const round = existing ?? createAdvisoryRoundInTransaction(database, {
|
|
232
|
+
runId: input.runId,
|
|
233
|
+
contractRevision: current.revision,
|
|
234
|
+
mutationRevision: input.mutationRevision,
|
|
235
|
+
phase: input.phase,
|
|
236
|
+
inputDigest,
|
|
237
|
+
contributions: input.contributions,
|
|
238
|
+
});
|
|
239
|
+
appendEnnoEventInTransaction(database, input.runId, 'enno.advice_submitted', 'enno-oduno', 'aggregated', {
|
|
240
|
+
phase: input.phase,
|
|
241
|
+
contractRevision: current.revision,
|
|
242
|
+
mutationRevision: input.mutationRevision,
|
|
243
|
+
inputDigest,
|
|
244
|
+
degraded: round.degraded,
|
|
245
|
+
});
|
|
246
|
+
const response = { ennoOduno: stateForSnapshot(current), advisoryRound: round };
|
|
247
|
+
completeOperationInTransaction(database, input.runId, operation, response);
|
|
248
|
+
return response;
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
function identity(input) {
|
|
252
|
+
return {
|
|
253
|
+
runId: input.runId,
|
|
254
|
+
workspace: input.workspace,
|
|
255
|
+
orchestrationId: input.orchestrationId,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function assertExpected(snapshot, expectedRevision, statuses) {
|
|
259
|
+
if (snapshot.revision !== expectedRevision)
|
|
260
|
+
throw new KiokukoError('CONFLICT', 'Enno contract revision changed');
|
|
261
|
+
if (!statuses.includes(snapshot.status))
|
|
262
|
+
throw new KiokukoError('CONFLICT', 'Enno run is not in the required state');
|
|
263
|
+
}
|
|
264
|
+
function firstReadyUnit(snapshot) {
|
|
265
|
+
const completed = new Set(snapshot.workUnits.filter((unit) => unit.status === 'completed').map((unit) => unit.workUnit.id));
|
|
266
|
+
return snapshot.workUnits.find((unit) => unit.status === 'pending'
|
|
267
|
+
&& unit.workUnit.dependencies.every((dependency) => completed.has(dependency)));
|
|
268
|
+
}
|
|
269
|
+
function startFirstReadyUnit(database, snapshot) {
|
|
270
|
+
const next = firstReadyUnit(snapshot);
|
|
271
|
+
if (next === undefined) {
|
|
272
|
+
if (snapshot.workUnits.every((unit) => unit.status === 'completed'))
|
|
273
|
+
return;
|
|
274
|
+
throw new KiokukoError('CONFLICT', 'Enno WorkPlan dependencies cannot advance');
|
|
275
|
+
}
|
|
276
|
+
setWorkUnitStatusInTransaction(database, {
|
|
277
|
+
runId: snapshot.runId,
|
|
278
|
+
contractRevision: snapshot.revision,
|
|
279
|
+
workUnitId: next.workUnit.id,
|
|
280
|
+
from: 'pending',
|
|
281
|
+
to: 'in_progress',
|
|
282
|
+
});
|
|
283
|
+
appendEnnoEventInTransaction(database, snapshot.runId, 'goki.work_started', 'goki', 'started', {
|
|
284
|
+
workUnitId: next.workUnit.id,
|
|
285
|
+
contractRevision: snapshot.revision,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
function requiresConfirmation(provenance) {
|
|
289
|
+
return ENNO_PROVENANCE_KEYS.some((key) => provenance[key] !== 'explicit_user');
|
|
290
|
+
}
|
|
291
|
+
async function readRunTaskContext(database, snapshot) {
|
|
292
|
+
const row = database.prepare(`
|
|
293
|
+
SELECT r.repository_id AS repositoryId, ri.session_id AS sessionId
|
|
294
|
+
FROM repositories AS r
|
|
295
|
+
JOIN repository_locations AS l ON l.repository_id = r.repository_id
|
|
296
|
+
JOIN run_intakes AS ri ON ri.run_id = ?
|
|
297
|
+
WHERE r.workspace = ? AND l.canonical_root = ?
|
|
298
|
+
LIMIT 1
|
|
299
|
+
`).get(snapshot.runId, snapshot.workspace, snapshot.repositoryRoot);
|
|
300
|
+
if (row === undefined)
|
|
301
|
+
throw new KiokukoError('CONFLICT', 'Enno repository or intake binding changed');
|
|
302
|
+
const context = await getAkinatorContextService(database, {
|
|
303
|
+
workspace: snapshot.workspace,
|
|
304
|
+
sessionId: row.sessionId,
|
|
305
|
+
});
|
|
306
|
+
const project = {
|
|
307
|
+
workspace: snapshot.workspace,
|
|
308
|
+
repositoryRoot: snapshot.repositoryRoot,
|
|
309
|
+
repositoryId: row.repositoryId,
|
|
310
|
+
source: 'location',
|
|
311
|
+
};
|
|
312
|
+
const manifest = captureProjectManifestSnapshot(project);
|
|
313
|
+
return {
|
|
314
|
+
project,
|
|
315
|
+
fingerprint: resolveProjectFingerprint(database, project, manifest),
|
|
316
|
+
task: context.session.task,
|
|
317
|
+
profile: context.session.profile,
|
|
318
|
+
recommendedTags: context.recommendedTags,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function emptyDiscovery(mode) {
|
|
322
|
+
return { attempted: false, mode, requirements: [], queries: [], cacheHits: 0, candidates: 0, selected: [], failures: [] };
|
|
323
|
+
}
|
|
324
|
+
function legacyOptionalDiscoverySummary(mode, error) {
|
|
325
|
+
if (!(error instanceof SkillProviderError) || error.code !== 'registry_invalid_response')
|
|
326
|
+
return undefined;
|
|
327
|
+
return {
|
|
328
|
+
...emptyDiscovery(mode),
|
|
329
|
+
failures: [{ stage: 'search', code: error.code }],
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
async function discoverZenkiSkills(database, snapshot, plan, dependencies) {
|
|
333
|
+
const intake = snapshot.contract.skillSet.intakeDiscovery;
|
|
334
|
+
if (intake.mode === 'off')
|
|
335
|
+
return emptyDiscovery(intake.mode);
|
|
336
|
+
const context = await readRunTaskContext(database, snapshot);
|
|
337
|
+
const attempt = {
|
|
338
|
+
runId: snapshot.runId,
|
|
339
|
+
phase: 'zenki',
|
|
340
|
+
mode: intake.mode,
|
|
341
|
+
requestDigest: canonicalContentHash({
|
|
342
|
+
version: 2,
|
|
343
|
+
runId: snapshot.runId,
|
|
344
|
+
revision: snapshot.revision,
|
|
345
|
+
mode: intake.mode,
|
|
346
|
+
workPlan: plan.workPlan,
|
|
347
|
+
capabilities: plan.capabilities ?? null,
|
|
348
|
+
skillRequirements: plan.skillRequirements,
|
|
349
|
+
}),
|
|
350
|
+
};
|
|
351
|
+
let replay;
|
|
352
|
+
try {
|
|
353
|
+
replay = readAgentTaskSkillDiscoveryAttempt(database, attempt);
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
const legacySummary = legacyOptionalDiscoverySummary(intake.mode, error);
|
|
357
|
+
if (legacySummary === undefined)
|
|
358
|
+
throw error;
|
|
359
|
+
return legacySummary;
|
|
360
|
+
}
|
|
361
|
+
if (replay !== undefined)
|
|
362
|
+
return replay.summary;
|
|
363
|
+
const claimed = claimAgentTaskSkillDiscoveryAttempt(database, attempt, {
|
|
364
|
+
queryBudget: ENNO_MAX_TOTAL_SKILL_QUERIES,
|
|
365
|
+
selectionBudget: ENNO_MAX_EXTERNAL_SKILLS,
|
|
366
|
+
});
|
|
367
|
+
if (claimed.kind === 'replay')
|
|
368
|
+
return claimed.summary;
|
|
369
|
+
if (claimed.queryBudget === 0 || claimed.selectionBudget === 0) {
|
|
370
|
+
return completeAgentTaskSkillDiscoveryAttempt(database, attempt, emptyDiscovery(intake.mode), () => {
|
|
371
|
+
assertExpected(readEnnoSnapshot(database, identity(plan)), plan.expectedRevision, ['zenki_planning']);
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
try {
|
|
375
|
+
const summary = await (dependencies.discoverSkills ?? discoverSkills)(database, {
|
|
376
|
+
...context,
|
|
377
|
+
task: `${context.task}\n${plan.workPlan.objective}`,
|
|
378
|
+
capabilities: plan.capabilities,
|
|
379
|
+
mode: intake.mode,
|
|
380
|
+
maxQueries: claimed.queryBudget,
|
|
381
|
+
maxSelectedSkills: claimed.selectionBudget,
|
|
382
|
+
...(dependencies.fetchImpl === undefined ? {} : { fetchImpl: dependencies.fetchImpl }),
|
|
383
|
+
}, {
|
|
384
|
+
...(dependencies.fetchImpl === undefined ? {} : { fetchImpl: dependencies.fetchImpl }),
|
|
385
|
+
assertBeforePersist: () => assertExpected(readEnnoSnapshot(database, identity(plan)), plan.expectedRevision, ['zenki_planning']),
|
|
386
|
+
});
|
|
387
|
+
return completeAgentTaskSkillDiscoveryAttempt(database, attempt, summary, () => {
|
|
388
|
+
assertExpected(readEnnoSnapshot(database, identity(plan)), plan.expectedRevision, ['zenki_planning']);
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
failAgentTaskSkillDiscoveryAttempt(database, attempt, error);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function planChangesCode(plan, taskType) {
|
|
396
|
+
return taskType !== 'review'
|
|
397
|
+
|| plan.skillRequirements.some((skill) => skill.purposes.includes('implementation'))
|
|
398
|
+
|| plan.workPlan.units.some((unit) => unit.skillNames.includes('kiokuko-single-purpose-functions'));
|
|
399
|
+
}
|
|
400
|
+
function planHasUi(plan) {
|
|
401
|
+
return plan.skillRequirements.some((skill) => skill.purposes.includes('ui'))
|
|
402
|
+
|| plan.workPlan.units.some((unit) => unit.skillNames.includes('kiokuko-ui-design-soul'));
|
|
403
|
+
}
|
|
404
|
+
function assertIdealSkillCoverage(snapshot, ideal) {
|
|
405
|
+
const expected = snapshot.contract.skillSet.intakeDiscovery.selected.map((skill) => skill.name);
|
|
406
|
+
if (new Set(expected).size !== expected.length) {
|
|
407
|
+
throw new KiokukoError('INTEGRITY_ERROR', 'Akinator-discovered Skill identities are inconsistent');
|
|
408
|
+
}
|
|
409
|
+
const actual = ideal.skillContributions.map((contribution) => contribution.skillName);
|
|
410
|
+
if (actual.length !== expected.length || expected.some((name) => !actual.includes(name))) {
|
|
411
|
+
throw new KiokukoError('VALIDATION_ERROR', 'Oduno ideal must account for every Akinator-discovered Skill exactly once');
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function sanitizedIdeal(ideal, repositoryRoot) {
|
|
415
|
+
return parseOdunoIdeal(sanitizeJson(ideal, { workspace: repositoryRoot }).value);
|
|
416
|
+
}
|
|
417
|
+
export function submitOdunoIdeal(database, rawInput) {
|
|
418
|
+
const input = parseIdealSubmission(rawInput);
|
|
419
|
+
const operation = operationIdentity('ideal_submit', input.idempotencyKey, input);
|
|
420
|
+
const replay = readOperationReceipt(database, input.runId, operation);
|
|
421
|
+
if (replay !== undefined)
|
|
422
|
+
return replay;
|
|
423
|
+
const before = readEnnoSnapshot(database, identity(input));
|
|
424
|
+
assertExpected(before, input.expectedRevision, ['oduno_ideal']);
|
|
425
|
+
if (input.advisoryRoundDigest !== undefined) {
|
|
426
|
+
requireAdvisoryRound(database, before, 'ideal', before.mutationRevision, advisoryContextForSubmission(before, 'ideal'), input.advisoryRoundDigest);
|
|
427
|
+
}
|
|
428
|
+
const ideal = sanitizedIdeal(input.ideal, before.repositoryRoot);
|
|
429
|
+
assertIdealSkillCoverage(before, ideal);
|
|
430
|
+
return withImmediateTransaction(database, () => {
|
|
431
|
+
const replayed = readOperationReceipt(database, input.runId, operation);
|
|
432
|
+
if (replayed !== undefined)
|
|
433
|
+
return replayed;
|
|
434
|
+
const current = readEnnoSnapshot(database, identity(input));
|
|
435
|
+
assertExpected(current, input.expectedRevision, ['oduno_ideal']);
|
|
436
|
+
assertIdealSkillCoverage(current, ideal);
|
|
437
|
+
startOperationInTransaction(database, input.runId, operation);
|
|
438
|
+
const advisoryRound = consumeAdvisoryRoundIfPresent(database, current, 'ideal', current.mutationRevision, advisoryContextForSubmission(current, 'ideal'), input.advisoryRoundDigest);
|
|
439
|
+
updateContractInTransaction(database, current, {
|
|
440
|
+
contract: current.contract,
|
|
441
|
+
status: 'zenki_planning',
|
|
442
|
+
confirmationState: current.confirmationState,
|
|
443
|
+
ideal,
|
|
444
|
+
});
|
|
445
|
+
appendEnnoEventInTransaction(database, input.runId, 'oduno.ideal_derived', 'enno-oduno', 'derived', {
|
|
446
|
+
contractRevision: current.revision,
|
|
447
|
+
discoveredSkillCount: ideal.skillContributions.length,
|
|
448
|
+
principleCount: ideal.principles.length,
|
|
449
|
+
successSignalCount: ideal.successSignals.length,
|
|
450
|
+
});
|
|
451
|
+
const response = {
|
|
452
|
+
ennoOduno: stateForSnapshot(readEnnoSnapshot(database, identity(input))),
|
|
453
|
+
...(advisoryRound === undefined ? {} : { advisoryRound }),
|
|
454
|
+
};
|
|
455
|
+
completeOperationInTransaction(database, input.runId, operation, response);
|
|
456
|
+
return response;
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
export async function submitEnnoPlan(database, rawInput, dependencies = {}) {
|
|
460
|
+
const input = parsePlanSubmission(rawInput);
|
|
461
|
+
const operation = operationIdentity('plan_submit', input.idempotencyKey, input);
|
|
462
|
+
const replay = readOperationReceipt(database, input.runId, operation);
|
|
463
|
+
if (replay !== undefined)
|
|
464
|
+
return replay;
|
|
465
|
+
const before = readEnnoSnapshot(database, identity(input));
|
|
466
|
+
assertExpected(before, input.expectedRevision, ['zenki_planning']);
|
|
467
|
+
if (input.advisoryRoundDigest !== undefined) {
|
|
468
|
+
requireAdvisoryRound(database, before, 'planning', before.mutationRevision, advisoryContextForSubmission(before, 'planning'), input.advisoryRoundDigest);
|
|
469
|
+
}
|
|
470
|
+
const includesCodeChanges = planChangesCode(input, before.taskType);
|
|
471
|
+
const includesUiWork = planHasUi(input);
|
|
472
|
+
assertWorkPlanExpertCoverage(input.workPlan, { includesCodeChanges, includesUiWork });
|
|
473
|
+
assertContractVerifierCwds(before.repositoryRoot, {
|
|
474
|
+
workPlan: input.workPlan,
|
|
475
|
+
finalVerifiers: input.finalVerifiers,
|
|
476
|
+
});
|
|
477
|
+
const zenkiDiscovery = await discoverZenkiSkills(database, before, input, dependencies);
|
|
478
|
+
const requirements = completeRequiredSkillList({
|
|
479
|
+
requested: input.skillRequirements,
|
|
480
|
+
includesCodeChanges,
|
|
481
|
+
includesUiWork,
|
|
482
|
+
});
|
|
483
|
+
const entries = createSkillSetEntries(database, {
|
|
484
|
+
requirements,
|
|
485
|
+
capabilities: input.capabilities,
|
|
486
|
+
discoveries: [before.contract.skillSet.intakeDiscovery, zenkiDiscovery],
|
|
487
|
+
});
|
|
488
|
+
const unavailable = unavailableRequiredSkills(entries);
|
|
489
|
+
const mandatoryWorkSkills = [
|
|
490
|
+
STANDARD_SOUL_SKILL_NAME,
|
|
491
|
+
...(includesCodeChanges ? [STANDARD_FUNCTION_SKILL_NAME] : []),
|
|
492
|
+
...(includesUiWork ? [STANDARD_UI_SKILL_NAME] : []),
|
|
493
|
+
];
|
|
494
|
+
const nextRevision = input.expectedRevision + 1;
|
|
495
|
+
const contract = {
|
|
496
|
+
revision: nextRevision,
|
|
497
|
+
scope: [...input.scope],
|
|
498
|
+
exclusions: [...input.exclusions],
|
|
499
|
+
acceptanceCriteria: input.acceptanceCriteria.map((item) => ({ ...item })),
|
|
500
|
+
workPlan: {
|
|
501
|
+
objective: input.workPlan.objective,
|
|
502
|
+
units: input.workPlan.units.map((unit) => ({
|
|
503
|
+
...unit,
|
|
504
|
+
scope: [...unit.scope],
|
|
505
|
+
dependencies: [...unit.dependencies],
|
|
506
|
+
skillNames: orderedUniqueSkillNames(mandatoryWorkSkills, unit.skillNames),
|
|
507
|
+
expertRefs: unit.expertRefs.map((reference) => ({ ...reference })),
|
|
508
|
+
acceptanceCriteria: [...unit.acceptanceCriteria],
|
|
509
|
+
focusedVerifiers: unit.focusedVerifiers.map((verifier) => ({ ...verifier, args: [...verifier.args] })),
|
|
510
|
+
})),
|
|
511
|
+
},
|
|
512
|
+
skillSet: {
|
|
513
|
+
entries,
|
|
514
|
+
intakeDiscovery: before.contract.skillSet.intakeDiscovery,
|
|
515
|
+
zenkiDiscovery,
|
|
516
|
+
},
|
|
517
|
+
finalVerifiers: input.finalVerifiers.map((verifier) => ({ ...verifier, args: [...verifier.args] })),
|
|
518
|
+
maxAttempts: input.maxAttempts,
|
|
519
|
+
provenance: { ...input.provenance },
|
|
520
|
+
};
|
|
521
|
+
assertContractVerifierCwds(before.repositoryRoot, contract);
|
|
522
|
+
const needsConfirmation = requiresConfirmation(contract.provenance);
|
|
523
|
+
return withImmediateTransaction(database, () => {
|
|
524
|
+
const replayed = readOperationReceipt(database, input.runId, operation);
|
|
525
|
+
if (replayed !== undefined)
|
|
526
|
+
return replayed;
|
|
527
|
+
const current = readEnnoSnapshot(database, identity(input));
|
|
528
|
+
assertExpected(current, input.expectedRevision, ['zenki_planning']);
|
|
529
|
+
startOperationInTransaction(database, input.runId, operation);
|
|
530
|
+
const advisoryRound = consumeAdvisoryRoundIfPresent(database, current, 'planning', current.mutationRevision, advisoryContextForSubmission(current, 'planning'), input.advisoryRoundDigest);
|
|
531
|
+
const status = unavailable.length > 0
|
|
532
|
+
? 'blocked'
|
|
533
|
+
: needsConfirmation ? 'needs_confirmation' : 'goki_executing';
|
|
534
|
+
const blocker = unavailable.length === 0
|
|
535
|
+
? null
|
|
536
|
+
: `Required Skills unavailable: ${unavailable.map((skill) => skill.name).join(', ')}`;
|
|
537
|
+
updateContractInTransaction(database, current, {
|
|
538
|
+
contract,
|
|
539
|
+
status,
|
|
540
|
+
confirmationState: needsConfirmation ? 'pending' : 'not_required',
|
|
541
|
+
blocker,
|
|
542
|
+
planDigest: canonicalContentHash(contract.workPlan),
|
|
543
|
+
});
|
|
544
|
+
replaceWorkUnitsInTransaction(database, input.runId, nextRevision, contract.workPlan);
|
|
545
|
+
appendEnnoEventInTransaction(database, input.runId, 'zenki.plan_created', 'zenki', 'created', {
|
|
546
|
+
contractRevision: nextRevision,
|
|
547
|
+
workUnitIds: contract.workPlan.units.map((unit) => unit.id),
|
|
548
|
+
});
|
|
549
|
+
let updated = readEnnoSnapshot(database, identity(input));
|
|
550
|
+
if (status === 'blocked') {
|
|
551
|
+
appendEnnoEventInTransaction(database, input.runId, 'enno.blocked', 'enno-oduno', 'blocked', {
|
|
552
|
+
reason: blocker,
|
|
553
|
+
contractRevision: nextRevision,
|
|
554
|
+
});
|
|
555
|
+
terminalizeLedgerRunInTransaction(database, input.runId, 'failed');
|
|
556
|
+
}
|
|
557
|
+
else if (status === 'goki_executing') {
|
|
558
|
+
appendEnnoEventInTransaction(database, input.runId, 'enno.plan_confirmed', 'enno-oduno', 'not_required', {
|
|
559
|
+
contractRevision: nextRevision,
|
|
560
|
+
});
|
|
561
|
+
startFirstReadyUnit(database, updated);
|
|
562
|
+
updated = readEnnoSnapshot(database, identity(input));
|
|
563
|
+
}
|
|
564
|
+
const response = {
|
|
565
|
+
ennoOduno: stateForSnapshot(updated),
|
|
566
|
+
...(advisoryRound === undefined ? {} : { advisoryRound }),
|
|
567
|
+
};
|
|
568
|
+
completeOperationInTransaction(database, input.runId, operation, response);
|
|
569
|
+
return response;
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
export function answerEnno(database, rawInput) {
|
|
573
|
+
const input = parseEnnoAnswer(rawInput);
|
|
574
|
+
const operation = operationIdentity('answer', input.idempotencyKey, input);
|
|
575
|
+
const replay = readOperationReceipt(database, input.runId, operation);
|
|
576
|
+
if (replay !== undefined)
|
|
577
|
+
return replay;
|
|
578
|
+
return withImmediateTransaction(database, () => {
|
|
579
|
+
const current = readEnnoSnapshot(database, identity(input));
|
|
580
|
+
assertExpected(current, input.expectedRevision, ['needs_confirmation']);
|
|
581
|
+
startOperationInTransaction(database, input.runId, operation);
|
|
582
|
+
let next;
|
|
583
|
+
if (input.action === 'approve') {
|
|
584
|
+
updateContractInTransaction(database, current, {
|
|
585
|
+
contract: current.contract,
|
|
586
|
+
status: 'goki_executing',
|
|
587
|
+
confirmationState: 'approved',
|
|
588
|
+
});
|
|
589
|
+
appendEnnoEventInTransaction(database, input.runId, 'enno.plan_confirmed', 'enno-oduno', 'approved', {
|
|
590
|
+
contractRevision: current.revision,
|
|
591
|
+
});
|
|
592
|
+
next = readEnnoSnapshot(database, identity(input));
|
|
593
|
+
startFirstReadyUnit(database, next);
|
|
594
|
+
next = readEnnoSnapshot(database, identity(input));
|
|
595
|
+
}
|
|
596
|
+
else if (input.action === 'revise') {
|
|
597
|
+
const revisedContract = {
|
|
598
|
+
...current.contract,
|
|
599
|
+
revision: current.revision + 1,
|
|
600
|
+
};
|
|
601
|
+
updateContractInTransaction(database, current, {
|
|
602
|
+
contract: revisedContract,
|
|
603
|
+
status: 'zenki_planning',
|
|
604
|
+
confirmationState: 'revision_requested',
|
|
605
|
+
blocker: input.requestedChanges ?? null,
|
|
606
|
+
});
|
|
607
|
+
next = readEnnoSnapshot(database, identity(input));
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
updateContractInTransaction(database, current, {
|
|
611
|
+
contract: current.contract,
|
|
612
|
+
status: 'cancelled',
|
|
613
|
+
confirmationState: 'cancelled',
|
|
614
|
+
});
|
|
615
|
+
appendEnnoEventInTransaction(database, input.runId, 'enno.cancelled', 'enno-oduno', 'cancelled', {
|
|
616
|
+
contractRevision: current.revision,
|
|
617
|
+
});
|
|
618
|
+
terminalizeLedgerRunInTransaction(database, input.runId, 'cancelled');
|
|
619
|
+
next = readEnnoSnapshot(database, identity(input));
|
|
620
|
+
}
|
|
621
|
+
const response = { ennoOduno: stateForSnapshot(next) };
|
|
622
|
+
completeOperationInTransaction(database, input.runId, operation, response);
|
|
623
|
+
return response;
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
function sanitizedVerifierResults(results, repositoryRoot) {
|
|
627
|
+
return results.map((result) => {
|
|
628
|
+
const sanitized = sanitizeJson({
|
|
629
|
+
stdoutPreview: result.stdoutPreview,
|
|
630
|
+
stderrPreview: result.stderrPreview,
|
|
631
|
+
}, { workspace: repositoryRoot }).value;
|
|
632
|
+
return {
|
|
633
|
+
...result,
|
|
634
|
+
stdoutPreview: sanitized.stdoutPreview,
|
|
635
|
+
stderrPreview: sanitized.stderrPreview,
|
|
636
|
+
};
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
function spawnFailedVerifierResults(verifiers) {
|
|
640
|
+
const emptyDigest = createHash('sha256').digest('hex');
|
|
641
|
+
return verifiers.map((verifier) => ({
|
|
642
|
+
verifier: { ...verifier, args: [...verifier.args] },
|
|
643
|
+
status: 'spawn_failed',
|
|
644
|
+
exitCode: null,
|
|
645
|
+
signal: null,
|
|
646
|
+
durationMs: 0,
|
|
647
|
+
stdoutPreview: '',
|
|
648
|
+
stderrPreview: '',
|
|
649
|
+
stdoutDigest: emptyDigest,
|
|
650
|
+
stderrDigest: emptyDigest,
|
|
651
|
+
}));
|
|
652
|
+
}
|
|
653
|
+
function finalReviewFeedback(revision, reviewSummary, results) {
|
|
654
|
+
const failed = results.filter((result) => result.status !== 'passed');
|
|
655
|
+
const evidence = failed.length === 0
|
|
656
|
+
? 'all final verifiers passed'
|
|
657
|
+
: failed.map((result) => {
|
|
658
|
+
const exit = result.exitCode === null ? '' : `,exit=${result.exitCode}`;
|
|
659
|
+
return `${result.verifier.id}=${result.status}${exit}`;
|
|
660
|
+
}).join('; ');
|
|
661
|
+
return `Enno-Oduno review rejected contract revision ${revision}: ${reviewSummary}. Verifier evidence: ${evidence}. Zenki must revise the WorkPlan; Goki must not resume the rejected plan.`
|
|
662
|
+
.slice(0, 16_384);
|
|
663
|
+
}
|
|
664
|
+
function sanitizedReviewSummary(summary, repositoryRoot) {
|
|
665
|
+
return sanitizeJson({ summary }, { workspace: repositoryRoot }).value.summary;
|
|
666
|
+
}
|
|
667
|
+
function blockedForAttemptLimit(database, snapshot, operation) {
|
|
668
|
+
startOperationInTransaction(database, snapshot.runId, operation);
|
|
669
|
+
updateContractInTransaction(database, snapshot, {
|
|
670
|
+
contract: snapshot.contract,
|
|
671
|
+
status: 'blocked',
|
|
672
|
+
confirmationState: snapshot.confirmationState,
|
|
673
|
+
blocker: 'Enno attempt limit reached',
|
|
674
|
+
});
|
|
675
|
+
appendEnnoEventInTransaction(database, snapshot.runId, 'enno.blocked', 'enno-oduno', 'blocked', {
|
|
676
|
+
reason: 'attempt_limit',
|
|
677
|
+
attempts: snapshot.attempts,
|
|
678
|
+
});
|
|
679
|
+
terminalizeLedgerRunInTransaction(database, snapshot.runId, 'failed');
|
|
680
|
+
const response = { ennoOduno: stateForSnapshot(readEnnoSnapshot(database, {
|
|
681
|
+
runId: snapshot.runId,
|
|
682
|
+
workspace: snapshot.workspace,
|
|
683
|
+
orchestrationId: snapshot.orchestrationId,
|
|
684
|
+
})) };
|
|
685
|
+
completeOperationInTransaction(database, snapshot.runId, operation, response);
|
|
686
|
+
return response;
|
|
687
|
+
}
|
|
688
|
+
export async function reportEnnoWork(database, rawInput, dependencies = {}) {
|
|
689
|
+
const input = parseWorkReport(rawInput);
|
|
690
|
+
const operation = operationIdentity('work_report', input.idempotencyKey, input);
|
|
691
|
+
const replay = readOperationReceipt(database, input.runId, operation);
|
|
692
|
+
if (replay !== undefined)
|
|
693
|
+
return replay;
|
|
694
|
+
const before = readEnnoSnapshot(database, identity(input));
|
|
695
|
+
assertExpected(before, input.expectedRevision, ['goki_executing']);
|
|
696
|
+
const unit = before.workUnits.find((candidate) => candidate.workUnit.id === input.workUnitId);
|
|
697
|
+
if (unit === undefined || unit.status !== 'in_progress')
|
|
698
|
+
throw new KiokukoError('CONFLICT', 'Reported Enno WorkUnit is not active');
|
|
699
|
+
if (before.attempts >= before.contract.maxAttempts) {
|
|
700
|
+
return withImmediateTransaction(database, () => blockedForAttemptLimit(database, readEnnoSnapshot(database, identity(input)), operation));
|
|
701
|
+
}
|
|
702
|
+
if (input.result.outcome !== 'completed') {
|
|
703
|
+
return withImmediateTransaction(database, () => {
|
|
704
|
+
const current = readEnnoSnapshot(database, identity(input));
|
|
705
|
+
assertExpected(current, input.expectedRevision, ['goki_executing']);
|
|
706
|
+
startOperationInTransaction(database, input.runId, operation);
|
|
707
|
+
const currentUnit = current.workUnits.find((candidate) => candidate.workUnit.id === input.workUnitId);
|
|
708
|
+
const attempts = current.attempts + 1;
|
|
709
|
+
const mustBlock = input.result.outcome === 'blocked' || attempts >= current.contract.maxAttempts;
|
|
710
|
+
setWorkUnitStatusInTransaction(database, {
|
|
711
|
+
runId: input.runId,
|
|
712
|
+
contractRevision: current.revision,
|
|
713
|
+
workUnitId: input.workUnitId,
|
|
714
|
+
from: 'in_progress',
|
|
715
|
+
to: mustBlock ? 'blocked' : 'in_progress',
|
|
716
|
+
attemptCount: currentUnit.attemptCount + 1,
|
|
717
|
+
result: input.result,
|
|
718
|
+
});
|
|
719
|
+
updateContractInTransaction(database, current, {
|
|
720
|
+
contract: current.contract,
|
|
721
|
+
status: mustBlock ? 'blocked' : 'goki_executing',
|
|
722
|
+
confirmationState: current.confirmationState,
|
|
723
|
+
attempts,
|
|
724
|
+
blocker: mustBlock ? input.result.summary : null,
|
|
725
|
+
});
|
|
726
|
+
appendEnnoEventInTransaction(database, input.runId, 'goki.work_failed', 'goki', input.result.outcome, {
|
|
727
|
+
workUnitId: input.workUnitId,
|
|
728
|
+
attempt: attempts,
|
|
729
|
+
});
|
|
730
|
+
if (mustBlock) {
|
|
731
|
+
appendEnnoEventInTransaction(database, input.runId, 'enno.blocked', 'enno-oduno', 'blocked', {
|
|
732
|
+
reason: input.result.summary,
|
|
733
|
+
});
|
|
734
|
+
terminalizeLedgerRunInTransaction(database, input.runId, 'failed');
|
|
735
|
+
}
|
|
736
|
+
const response = { ennoOduno: stateForSnapshot(readEnnoSnapshot(database, identity(input))) };
|
|
737
|
+
completeOperationInTransaction(database, input.runId, operation, response);
|
|
738
|
+
return response;
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
let verifierRunIds = [];
|
|
742
|
+
withImmediateTransaction(database, () => {
|
|
743
|
+
const current = readEnnoSnapshot(database, identity(input));
|
|
744
|
+
assertExpected(current, input.expectedRevision, ['goki_executing']);
|
|
745
|
+
startOperationInTransaction(database, input.runId, operation);
|
|
746
|
+
const currentUnit = current.workUnits.find((candidate) => candidate.workUnit.id === input.workUnitId);
|
|
747
|
+
if (currentUnit === undefined || currentUnit.status !== 'in_progress') {
|
|
748
|
+
throw new KiokukoError('CONFLICT', 'Reported Enno WorkUnit changed concurrently');
|
|
749
|
+
}
|
|
750
|
+
verifierRunIds = startVerifierRunsInTransaction(database, {
|
|
751
|
+
runId: input.runId,
|
|
752
|
+
workUnitId: input.workUnitId,
|
|
753
|
+
revision: current.revision,
|
|
754
|
+
mutationRevision: current.mutationRevision + (input.result.mutated ? 1 : 0),
|
|
755
|
+
verifiers: currentUnit.workUnit.focusedVerifiers,
|
|
756
|
+
});
|
|
757
|
+
});
|
|
758
|
+
let rawResults;
|
|
759
|
+
try {
|
|
760
|
+
rawResults = await runVerifiers(unit.workUnit.focusedVerifiers, before.repositoryRoot, dependencies);
|
|
761
|
+
}
|
|
762
|
+
catch {
|
|
763
|
+
rawResults = spawnFailedVerifierResults(unit.workUnit.focusedVerifiers);
|
|
764
|
+
}
|
|
765
|
+
const results = sanitizedVerifierResults(rawResults, before.repositoryRoot);
|
|
766
|
+
return withImmediateTransaction(database, () => {
|
|
767
|
+
const current = readEnnoSnapshot(database, identity(input));
|
|
768
|
+
assertExpected(current, input.expectedRevision, ['goki_executing']);
|
|
769
|
+
finishVerifierRunsInTransaction(database, verifierRunIds, results);
|
|
770
|
+
const passed = results.every((result) => result.status === 'passed');
|
|
771
|
+
const unsafe = results.some((result) => result.status === 'spawn_failed');
|
|
772
|
+
const attempts = current.attempts + 1;
|
|
773
|
+
const mustBlock = unsafe || (!passed && attempts >= current.contract.maxAttempts);
|
|
774
|
+
const currentUnit = current.workUnits.find((candidate) => candidate.workUnit.id === input.workUnitId);
|
|
775
|
+
setWorkUnitStatusInTransaction(database, {
|
|
776
|
+
runId: input.runId,
|
|
777
|
+
contractRevision: current.revision,
|
|
778
|
+
workUnitId: input.workUnitId,
|
|
779
|
+
from: 'in_progress',
|
|
780
|
+
to: passed ? 'completed' : mustBlock ? 'blocked' : 'in_progress',
|
|
781
|
+
attemptCount: currentUnit.attemptCount + 1,
|
|
782
|
+
result: input.result,
|
|
783
|
+
});
|
|
784
|
+
updateContractInTransaction(database, current, {
|
|
785
|
+
contract: current.contract,
|
|
786
|
+
status: mustBlock ? 'blocked' : 'goki_executing',
|
|
787
|
+
confirmationState: current.confirmationState,
|
|
788
|
+
attempts,
|
|
789
|
+
mutationRevision: current.mutationRevision + (input.result.mutated ? 1 : 0),
|
|
790
|
+
blocker: mustBlock
|
|
791
|
+
? unsafe ? 'Focused verifier could not be started safely' : 'Focused verification failed at the attempt limit'
|
|
792
|
+
: null,
|
|
793
|
+
});
|
|
794
|
+
appendEnnoEventInTransaction(database, input.runId, passed ? 'goki.work_completed' : 'goki.work_failed', 'goki', passed ? 'completed' : 'verification_failed', {
|
|
795
|
+
workUnitId: input.workUnitId,
|
|
796
|
+
mutated: input.result.mutated,
|
|
797
|
+
changedPaths: input.result.changedPaths,
|
|
798
|
+
});
|
|
799
|
+
let next = readEnnoSnapshot(database, identity(input));
|
|
800
|
+
if (mustBlock) {
|
|
801
|
+
appendEnnoEventInTransaction(database, input.runId, 'enno.blocked', 'enno-oduno', 'blocked', {
|
|
802
|
+
reason: unsafe ? 'spawn_failed' : 'focused_verification_failed',
|
|
803
|
+
});
|
|
804
|
+
terminalizeLedgerRunInTransaction(database, input.runId, 'failed');
|
|
805
|
+
}
|
|
806
|
+
else if (passed) {
|
|
807
|
+
if (next.workUnits.every((candidate) => candidate.status === 'completed')) {
|
|
808
|
+
updateContractInTransaction(database, next, {
|
|
809
|
+
contract: next.contract,
|
|
810
|
+
status: 'enno_verifying',
|
|
811
|
+
confirmationState: next.confirmationState,
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
startFirstReadyUnit(database, next);
|
|
816
|
+
}
|
|
817
|
+
next = readEnnoSnapshot(database, identity(input));
|
|
818
|
+
}
|
|
819
|
+
const response = { ennoOduno: stateForSnapshot(next), verifierResults: results };
|
|
820
|
+
completeOperationInTransaction(database, input.runId, operation, response);
|
|
821
|
+
return response;
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
export async function finishEnno(database, rawInput, dependencies = {}) {
|
|
825
|
+
const input = parseFinishRequest(rawInput);
|
|
826
|
+
const operation = operationIdentity('finish', input.idempotencyKey, input);
|
|
827
|
+
const replay = readOperationReceipt(database, input.runId, operation);
|
|
828
|
+
if (replay !== undefined)
|
|
829
|
+
return replay;
|
|
830
|
+
const before = readEnnoSnapshot(database, identity(input));
|
|
831
|
+
assertExpected(before, input.expectedRevision, ['enno_verifying']);
|
|
832
|
+
if (input.advisoryRoundDigest !== undefined) {
|
|
833
|
+
requireAdvisoryRound(database, before, 'final_review', before.mutationRevision, advisoryContextForSubmission(before, 'final_review'), input.advisoryRoundDigest);
|
|
834
|
+
}
|
|
835
|
+
const reviewSummary = sanitizedReviewSummary(input.review.summary, before.repositoryRoot);
|
|
836
|
+
if (before.attempts >= before.contract.maxAttempts) {
|
|
837
|
+
return withImmediateTransaction(database, () => blockedForAttemptLimit(database, readEnnoSnapshot(database, identity(input)), operation));
|
|
838
|
+
}
|
|
839
|
+
let verifierRunIds = [];
|
|
840
|
+
let cachedVerifierResults;
|
|
841
|
+
let advisoryRound;
|
|
842
|
+
withImmediateTransaction(database, () => {
|
|
843
|
+
const current = readEnnoSnapshot(database, identity(input));
|
|
844
|
+
assertExpected(current, input.expectedRevision, ['enno_verifying']);
|
|
845
|
+
advisoryRound = consumeAdvisoryRoundIfPresent(database, current, 'final_review', current.mutationRevision, advisoryContextForSubmission(current, 'final_review'), input.advisoryRoundDigest);
|
|
846
|
+
startOperationInTransaction(database, input.runId, operation);
|
|
847
|
+
cachedVerifierResults = readFreshFinalVerifierResults(database, {
|
|
848
|
+
runId: input.runId,
|
|
849
|
+
revision: current.revision,
|
|
850
|
+
mutationRevision: current.mutationRevision,
|
|
851
|
+
verifiers: current.contract.finalVerifiers,
|
|
852
|
+
});
|
|
853
|
+
if (cachedVerifierResults === undefined) {
|
|
854
|
+
verifierRunIds = startVerifierRunsInTransaction(database, {
|
|
855
|
+
runId: input.runId,
|
|
856
|
+
workUnitId: null,
|
|
857
|
+
revision: current.revision,
|
|
858
|
+
mutationRevision: current.mutationRevision,
|
|
859
|
+
verifiers: current.contract.finalVerifiers,
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
appendEnnoEventInTransaction(database, input.runId, 'enno.review_started', 'enno-oduno', 'started', {
|
|
863
|
+
contractRevision: current.revision,
|
|
864
|
+
mutationRevision: current.mutationRevision,
|
|
865
|
+
requestedDecision: input.review.decision,
|
|
866
|
+
});
|
|
867
|
+
appendEnnoEventInTransaction(database, input.runId, 'enno.verification_started', 'enno-oduno', cachedVerifierResults === undefined ? 'started' : 'reused', {
|
|
868
|
+
contractRevision: current.revision,
|
|
869
|
+
mutationRevision: current.mutationRevision,
|
|
870
|
+
verifierIds: current.contract.finalVerifiers.map((verifier) => verifier.id),
|
|
871
|
+
});
|
|
872
|
+
});
|
|
873
|
+
let results;
|
|
874
|
+
if (cachedVerifierResults !== undefined) {
|
|
875
|
+
results = cachedVerifierResults;
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
try {
|
|
879
|
+
results = sanitizedVerifierResults(await runVerifiers(before.contract.finalVerifiers, before.repositoryRoot, dependencies), before.repositoryRoot);
|
|
880
|
+
}
|
|
881
|
+
catch {
|
|
882
|
+
results = spawnFailedVerifierResults(before.contract.finalVerifiers);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
return withImmediateTransaction(database, () => {
|
|
886
|
+
const current = readEnnoSnapshot(database, identity(input));
|
|
887
|
+
assertExpected(current, input.expectedRevision, ['enno_verifying']);
|
|
888
|
+
if (verifierRunIds.length > 0)
|
|
889
|
+
finishVerifierRunsInTransaction(database, verifierRunIds, results);
|
|
890
|
+
const passed = results.length > 0 && results.every((result) => result.status === 'passed');
|
|
891
|
+
const unsafe = results.some((result) => result.status === 'spawn_failed');
|
|
892
|
+
const attempts = current.attempts + 1;
|
|
893
|
+
const accepted = passed && input.review.decision === 'accept';
|
|
894
|
+
const completesLegacyRun = accepted && current.ideal === null;
|
|
895
|
+
const mustBlock = unsafe || (!accepted && attempts >= current.contract.maxAttempts);
|
|
896
|
+
const reviewFeedback = accepted || mustBlock
|
|
897
|
+
? null
|
|
898
|
+
: finalReviewFeedback(current.revision, reviewSummary, results);
|
|
899
|
+
const nextContract = reviewFeedback === null
|
|
900
|
+
? current.contract
|
|
901
|
+
: { ...current.contract, revision: current.revision + 1 };
|
|
902
|
+
const blockedReason = unsafe
|
|
903
|
+
? 'Final verifier could not be started safely'
|
|
904
|
+
: input.review.decision === 'replan' && passed
|
|
905
|
+
? 'Enno review requested replanning at the attempt limit'
|
|
906
|
+
: 'Final verification reached the attempt limit';
|
|
907
|
+
updateContractInTransaction(database, current, {
|
|
908
|
+
contract: nextContract,
|
|
909
|
+
status: accepted
|
|
910
|
+
? completesLegacyRun ? 'completed' : 'oduno_meditation'
|
|
911
|
+
: mustBlock ? 'blocked' : 'zenki_planning',
|
|
912
|
+
confirmationState: reviewFeedback === null ? current.confirmationState : 'revision_requested',
|
|
913
|
+
attempts,
|
|
914
|
+
blocker: mustBlock ? blockedReason : reviewFeedback,
|
|
915
|
+
});
|
|
916
|
+
appendEnnoEventInTransaction(database, input.runId, passed ? 'enno.verification_passed' : 'enno.verification_failed', 'enno-oduno', passed ? 'passed' : 'failed', {
|
|
917
|
+
contractRevision: current.revision,
|
|
918
|
+
mutationRevision: current.mutationRevision,
|
|
919
|
+
statuses: results.map((result) => ({ verifierId: result.verifier.id, status: result.status })),
|
|
920
|
+
});
|
|
921
|
+
let next = readEnnoSnapshot(database, identity(input));
|
|
922
|
+
if (accepted) {
|
|
923
|
+
appendEnnoEventInTransaction(database, input.runId, 'enno.review_accepted', 'enno-oduno', 'accepted', {
|
|
924
|
+
contractRevision: current.revision,
|
|
925
|
+
mutationRevision: current.mutationRevision,
|
|
926
|
+
summary: reviewSummary,
|
|
927
|
+
});
|
|
928
|
+
if (completesLegacyRun) {
|
|
929
|
+
appendEnnoEventInTransaction(database, input.runId, 'enno.completed', 'enno-oduno', 'completed', {
|
|
930
|
+
contractRevision: current.revision,
|
|
931
|
+
mutationRevision: current.mutationRevision,
|
|
932
|
+
compatibility: 'pre_oduno_ideal_run',
|
|
933
|
+
});
|
|
934
|
+
terminalizeLedgerRunInTransaction(database, input.runId, 'completed');
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
else if (mustBlock) {
|
|
938
|
+
appendEnnoEventInTransaction(database, input.runId, 'enno.blocked', 'enno-oduno', 'blocked', {
|
|
939
|
+
reason: unsafe ? 'spawn_failed' : 'attempt_limit',
|
|
940
|
+
});
|
|
941
|
+
terminalizeLedgerRunInTransaction(database, input.runId, 'failed');
|
|
942
|
+
}
|
|
943
|
+
else {
|
|
944
|
+
appendEnnoEventInTransaction(database, input.runId, 'enno.replan_requested', 'enno-oduno', 'requested', {
|
|
945
|
+
fromContractRevision: current.revision,
|
|
946
|
+
toContractRevision: next.revision,
|
|
947
|
+
reason: reviewFeedback,
|
|
948
|
+
});
|
|
949
|
+
next = readEnnoSnapshot(database, identity(input));
|
|
950
|
+
}
|
|
951
|
+
const response = {
|
|
952
|
+
ennoOduno: stateForSnapshot(next),
|
|
953
|
+
verifierResults: results,
|
|
954
|
+
...(advisoryRound === undefined ? {} : { advisoryRound }),
|
|
955
|
+
};
|
|
956
|
+
completeOperationInTransaction(database, input.runId, operation, response);
|
|
957
|
+
return response;
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
function sanitizedMeditation(meditation, repositoryRoot) {
|
|
961
|
+
return parseOdunoMeditation(sanitizeJson(meditation, { workspace: repositoryRoot }).value);
|
|
962
|
+
}
|
|
963
|
+
export function submitOdunoMeditation(database, rawInput) {
|
|
964
|
+
const input = parseMeditationSubmission(rawInput);
|
|
965
|
+
const operation = operationIdentity('meditation_submit', input.idempotencyKey, input);
|
|
966
|
+
const replay = readOperationReceipt(database, input.runId, operation);
|
|
967
|
+
if (replay !== undefined)
|
|
968
|
+
return replay;
|
|
969
|
+
const before = readEnnoSnapshot(database, identity(input));
|
|
970
|
+
assertExpected(before, input.expectedRevision, ['oduno_meditation']);
|
|
971
|
+
if (before.ideal === null)
|
|
972
|
+
throw new KiokukoError('INTEGRITY_ERROR', 'Oduno meditation requires a persisted ideal');
|
|
973
|
+
const meditation = sanitizedMeditation(input.meditation, before.repositoryRoot);
|
|
974
|
+
return withImmediateTransaction(database, () => {
|
|
975
|
+
const replayed = readOperationReceipt(database, input.runId, operation);
|
|
976
|
+
if (replayed !== undefined)
|
|
977
|
+
return replayed;
|
|
978
|
+
const current = readEnnoSnapshot(database, identity(input));
|
|
979
|
+
assertExpected(current, input.expectedRevision, ['oduno_meditation']);
|
|
980
|
+
if (current.ideal === null)
|
|
981
|
+
throw new KiokukoError('INTEGRITY_ERROR', 'Oduno meditation requires a persisted ideal');
|
|
982
|
+
startOperationInTransaction(database, input.runId, operation);
|
|
983
|
+
updateContractInTransaction(database, current, {
|
|
984
|
+
contract: current.contract,
|
|
985
|
+
status: 'completed',
|
|
986
|
+
confirmationState: current.confirmationState,
|
|
987
|
+
meditation,
|
|
988
|
+
});
|
|
989
|
+
appendEnnoEventInTransaction(database, input.runId, 'oduno.meditation_completed', 'enno-oduno', 'completed', {
|
|
990
|
+
contractRevision: current.revision,
|
|
991
|
+
mutationRevision: current.mutationRevision,
|
|
992
|
+
inspectedPathCount: meditation.inspectedPaths.length,
|
|
993
|
+
deletionCandidateCount: meditation.deletionCandidates.length,
|
|
994
|
+
});
|
|
995
|
+
appendEnnoEventInTransaction(database, input.runId, 'enno.completed', 'enno-oduno', 'completed', {
|
|
996
|
+
contractRevision: current.revision,
|
|
997
|
+
mutationRevision: current.mutationRevision,
|
|
998
|
+
});
|
|
999
|
+
terminalizeLedgerRunInTransaction(database, input.runId, 'completed');
|
|
1000
|
+
const response = { ennoOduno: stateForSnapshot(readEnnoSnapshot(database, identity(input))) };
|
|
1001
|
+
completeOperationInTransaction(database, input.runId, operation, response);
|
|
1002
|
+
return response;
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
//# sourceMappingURL=service.js.map
|