@dsh-enhanced/assistant-skills 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +211 -0
  3. package/cordis.patch.yml +3 -0
  4. package/lib/capture-expansion.d.ts +18 -0
  5. package/lib/capture-expansion.d.ts.map +1 -0
  6. package/lib/capture-expansion.js +187 -0
  7. package/lib/capture-expansion.js.map +1 -0
  8. package/lib/comparison.d.ts +69 -0
  9. package/lib/comparison.d.ts.map +1 -0
  10. package/lib/comparison.js +173 -0
  11. package/lib/comparison.js.map +1 -0
  12. package/lib/definition.d.ts +163 -0
  13. package/lib/definition.d.ts.map +1 -0
  14. package/lib/definition.js +450 -0
  15. package/lib/definition.js.map +1 -0
  16. package/lib/external-holdout.d.ts +34 -0
  17. package/lib/external-holdout.d.ts.map +1 -0
  18. package/lib/external-holdout.js +212 -0
  19. package/lib/external-holdout.js.map +1 -0
  20. package/lib/holdout-authority.d.ts +123 -0
  21. package/lib/holdout-authority.d.ts.map +1 -0
  22. package/lib/holdout-authority.js +263 -0
  23. package/lib/holdout-authority.js.map +1 -0
  24. package/lib/holdout-cli.d.ts +4 -0
  25. package/lib/holdout-cli.d.ts.map +1 -0
  26. package/lib/holdout-cli.js +221 -0
  27. package/lib/holdout-cli.js.map +1 -0
  28. package/lib/holdout-qualification.d.ts +83 -0
  29. package/lib/holdout-qualification.d.ts.map +1 -0
  30. package/lib/holdout-qualification.js +306 -0
  31. package/lib/holdout-qualification.js.map +1 -0
  32. package/lib/index.d.ts +22 -0
  33. package/lib/index.d.ts.map +1 -0
  34. package/lib/index.js +9 -0
  35. package/lib/index.js.map +1 -0
  36. package/lib/prospective-holdout.d.ts +35 -0
  37. package/lib/prospective-holdout.d.ts.map +1 -0
  38. package/lib/prospective-holdout.js +254 -0
  39. package/lib/prospective-holdout.js.map +1 -0
  40. package/lib/repair-admission.d.ts +11 -0
  41. package/lib/repair-admission.d.ts.map +1 -0
  42. package/lib/repair-admission.js +25 -0
  43. package/lib/repair-admission.js.map +1 -0
  44. package/lib/repair-agent.d.ts +44 -0
  45. package/lib/repair-agent.d.ts.map +1 -0
  46. package/lib/repair-agent.js +241 -0
  47. package/lib/repair-agent.js.map +1 -0
  48. package/lib/repair-feedback.d.ts +70 -0
  49. package/lib/repair-feedback.d.ts.map +1 -0
  50. package/lib/repair-feedback.js +79 -0
  51. package/lib/repair-feedback.js.map +1 -0
  52. package/lib/repair-profile.d.ts +27 -0
  53. package/lib/repair-profile.d.ts.map +1 -0
  54. package/lib/repair-profile.js +51 -0
  55. package/lib/repair-profile.js.map +1 -0
  56. package/lib/repair-runtime.d.ts +35 -0
  57. package/lib/repair-runtime.d.ts.map +1 -0
  58. package/lib/repair-runtime.js +181 -0
  59. package/lib/repair-runtime.js.map +1 -0
  60. package/lib/replay.d.ts +38 -0
  61. package/lib/replay.d.ts.map +1 -0
  62. package/lib/replay.js +240 -0
  63. package/lib/replay.js.map +1 -0
  64. package/lib/sealed-holdout.d.ts +20 -0
  65. package/lib/sealed-holdout.d.ts.map +1 -0
  66. package/lib/sealed-holdout.js +12 -0
  67. package/lib/sealed-holdout.js.map +1 -0
  68. package/lib/service.d.ts +968 -0
  69. package/lib/service.d.ts.map +1 -0
  70. package/lib/service.js +1929 -0
  71. package/lib/service.js.map +1 -0
  72. package/lib/store.d.ts +366 -0
  73. package/lib/store.d.ts.map +1 -0
  74. package/lib/store.js +1517 -0
  75. package/lib/store.js.map +1 -0
  76. package/lib/version.d.ts +2 -0
  77. package/lib/version.d.ts.map +1 -0
  78. package/lib/version.js +2 -0
  79. package/lib/version.js.map +1 -0
  80. package/lib/watch-proof.d.ts +30 -0
  81. package/lib/watch-proof.d.ts.map +1 -0
  82. package/lib/watch-proof.js +209 -0
  83. package/lib/watch-proof.js.map +1 -0
  84. package/package.json +141 -0
package/lib/store.js ADDED
@@ -0,0 +1,1517 @@
1
+ import { closeSync, constants, lstatSync, mkdirSync, openSync, chmodSync } from 'node:fs';
2
+ import { dirname, isAbsolute } from 'node:path';
3
+ import { DatabaseSync } from 'node:sqlite';
4
+ import { acceptanceDigest } from '@dsh-enhanced/task-acceptance-contract';
5
+ import { validateFailureCaptureProvenance } from './definition.js';
6
+ function fail(message = 'assistant-skills: store operation rejected') { throw new Error(message); }
7
+ function json(value) {
8
+ if (value === null || typeof value === 'string' || typeof value === 'boolean')
9
+ return true;
10
+ if (typeof value === 'number')
11
+ return Number.isFinite(value);
12
+ if (Array.isArray(value)) {
13
+ for (let index = 0; index < value.length; index++) {
14
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
15
+ if (descriptor === undefined || !('value' in descriptor) || !json(descriptor.value))
16
+ return false;
17
+ }
18
+ return true;
19
+ }
20
+ if (!value || typeof value !== 'object' || Object.getPrototypeOf(value) !== Object.prototype || Object.getOwnPropertySymbols(value).length !== 0)
21
+ return false;
22
+ return Object.keys(value).every(key => {
23
+ if (key === '__proto__' || key === 'prototype' || key === 'constructor')
24
+ return false;
25
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
26
+ return descriptor !== undefined && 'value' in descriptor && json(descriptor.value);
27
+ });
28
+ }
29
+ function clone(value) { if (!json(value))
30
+ fail('assistant-skills: invalid JSON value'); return JSON.parse(JSON.stringify(value)); }
31
+ function scopeKey(scope) { if (!scope || typeof scope !== 'object' || Array.isArray(scope) || !json(scope))
32
+ fail('assistant-skills: invalid scope'); return acceptanceDigest(scope); }
33
+ function name(value) { return typeof value === 'string' && /^[a-z](?:[a-z0-9-]{0,62}[a-z0-9])?$/u.test(value); }
34
+ function version(value, allowZero = false) { return typeof value === 'number' && Number.isSafeInteger(value) && value >= (allowZero ? 0 : 1) && value <= 1_000_000_000; }
35
+ function text(value, maximum = 512) { return typeof value === 'string' && value.length > 0 && value.length <= maximum && !/[\p{Cc}]/u.test(value); }
36
+ function runId(scope, sessionId, invocationId) { return `skill-run-${acceptanceDigest([scope, sessionId, invocationId])}`; }
37
+ function comparisonId(scope, sessionId, invocationId) { return `skill-comparison-${acceptanceDigest([scope, sessionId, invocationId])}`; }
38
+ function watchId(scope, input) { return `skill-watch-${acceptanceDigest([scope, input])}`; }
39
+ function deploymentId(scope, candidateId, comparisonId, qualificationDigest, admission, input, routeReceipt) { return `skill-deployment-${acceptanceDigest([scope, candidateId, comparisonId, qualificationDigest, admission, input, routeReceipt])}`; }
40
+ function repairContinuationId(scope, input) { return `skill-repair-${acceptanceDigest([scope, input.ownerRouteId, input.invocationId])}`; }
41
+ function definitionValid(definition) { return !!definition && typeof definition === 'object' && definition.protocol === 'assistant-skills/definition/v1' && name(definition.name) && json(definition); }
42
+ function digest(value) { return typeof value === 'string' && /^[a-f0-9]{64}$/u.test(value); }
43
+ function canonicalRevision(value) {
44
+ if (!value || typeof value !== 'object' || Array.isArray(value) || !json(value))
45
+ return false;
46
+ const revision = value;
47
+ return Object.keys(revision).length === 6 && revision.subjectKind === 'goal-outcome' && text(revision.subjectRef, 1_000)
48
+ && version(revision.version) && digest(revision.digest) && ['upsert', 'retract'].includes(revision.disposition)
49
+ && Number.isSafeInteger(revision.scopeWatermark) && revision.scopeWatermark >= revision.version;
50
+ }
51
+ function observationBinding(observation, subjectRef) {
52
+ if (!text(observation.runId, 128) || !digest(observation.receiptDigest) || !Number.isSafeInteger(observation.verifiedAt)
53
+ || !Number.isSafeInteger(observation.validUntil) || !digest(observation.executionTraceDigest) || !digest(observation.taskFamilyDigest) || !text(subjectRef, 1_000))
54
+ return;
55
+ return { runId: observation.runId, subjectRef, receiptDigest: observation.receiptDigest, verifiedAt: observation.verifiedAt, validUntil: observation.validUntil,
56
+ executionTraceDigest: observation.executionTraceDigest, taskFamilyDigest: observation.taskFamilyDigest };
57
+ }
58
+ function validObservationBinding(value) {
59
+ if (!value || typeof value !== 'object' || Array.isArray(value) || !json(value) || Object.keys(value).length !== 7)
60
+ return false;
61
+ const candidate = value;
62
+ return text(candidate.runId, 128) && text(candidate.subjectRef, 1_000) && digest(candidate.receiptDigest)
63
+ && Number.isSafeInteger(candidate.verifiedAt) && Number.isSafeInteger(candidate.validUntil)
64
+ && digest(candidate.executionTraceDigest) && digest(candidate.taskFamilyDigest);
65
+ }
66
+ function validCanonicalState(value) {
67
+ if (!value || typeof value !== 'object' || Array.isArray(value) || !json(value) || Object.keys(value).length !== 8)
68
+ return false;
69
+ const state = value;
70
+ const { runId: _runId, binding, ...revision } = state;
71
+ return text(state.runId, 128) && canonicalRevision(revision) && validObservationBinding(binding)
72
+ && binding.runId === state.runId && binding.subjectRef === state.subjectRef;
73
+ }
74
+ function validRunIds(value) {
75
+ return Array.isArray(value) && value.length <= 100 && value.every(item => text(item, 128))
76
+ && new Set(value).size === value.length;
77
+ }
78
+ function exactCanonicalObservationState(watch, deployment) {
79
+ const admission = { protocol: 'assistant-skills/canary-admission/v1', skillName: deployment.skillName,
80
+ parentDefinitionDigest: watch.fallbackDigest, candidateDefinitionDigest: deployment.candidateDefinitionDigest, taskFamily: deployment.taskFamily };
81
+ if (watch.state !== 'watching' || watch.id !== deployment.watchId || watch.skillName !== deployment.skillName
82
+ || watch.version !== deployment.version || watch.definitionDigest !== deployment.definitionDigest
83
+ || watch.maxRuns !== deployment.maxRuns || watch.failureThreshold !== 1 || !validRunIds(watch.runIds)
84
+ || !validRunIds(deployment.runIds) || !Number.isSafeInteger(deployment.canaryRuns) || deployment.canaryRuns < 1
85
+ || deployment.canaryRuns > deployment.maxRuns || deployment.maxRuns < 1 || deployment.maxRuns > 100
86
+ || watch.runIds.length > watch.maxRuns || deployment.runIds.length > deployment.maxRuns
87
+ || watch.runIds.some(run => !deployment.runIds.includes(run)) || !deploymentAdmission(admission)
88
+ || deployment.admissionDigest !== acceptanceDigest(admission)
89
+ || watch.proofVersion !== 'canonical-goal-outcome/v2' || !watch.taskFamily
90
+ || acceptanceDigest(watch.taskFamily) !== acceptanceDigest(deployment.taskFamily)
91
+ || !Array.isArray(watch.observations) || !Array.isArray(watch.canonicalRevisions))
92
+ return false;
93
+ const familyDigest = acceptanceDigest(watch.taskFamily), revisions = new Map();
94
+ for (const revision of watch.canonicalRevisions) {
95
+ if (!validCanonicalState(revision) || revisions.has(revision.runId) || !watch.runIds.includes(revision.runId)
96
+ || !deployment.runIds.includes(revision.runId) || revision.binding.taskFamilyDigest !== familyDigest)
97
+ return false;
98
+ revisions.set(revision.runId, revision);
99
+ }
100
+ const observedRuns = new Set();
101
+ for (const observation of watch.observations) {
102
+ if (observedRuns.has(observation.runId) || !observation.canonical || !canonicalRevision(observation.canonical)
103
+ || !digest(observation.receiptDigest) || !Number.isSafeInteger(observation.verifiedAt) || !Number.isSafeInteger(observation.validUntil)
104
+ || !digest(observation.executionTraceDigest) || !['achieved', 'not-achieved'].includes(observation.objectiveStatus)
105
+ || observation.taskFamilyDigest !== familyDigest)
106
+ return false;
107
+ observedRuns.add(observation.runId);
108
+ const revision = revisions.get(observation.runId);
109
+ if (!revision || revision.disposition !== 'upsert' || acceptanceDigest(observation.canonical) !== acceptanceDigest({
110
+ subjectKind: revision.subjectKind, subjectRef: revision.subjectRef, version: revision.version, digest: revision.digest,
111
+ disposition: revision.disposition, scopeWatermark: revision.scopeWatermark,
112
+ }) || acceptanceDigest(revision.binding) !== acceptanceDigest({
113
+ runId: observation.runId, subjectRef: observation.canonical.subjectRef, receiptDigest: observation.receiptDigest, verifiedAt: observation.verifiedAt,
114
+ validUntil: observation.validUntil, executionTraceDigest: observation.executionTraceDigest, taskFamilyDigest: observation.taskFamilyDigest,
115
+ }))
116
+ return false;
117
+ }
118
+ if (![...revisions.values()].every(revision => revision.disposition === 'upsert' && observedRuns.has(revision.runId))
119
+ || watch.observations.some(observation => observation.objectiveStatus !== 'achieved'))
120
+ return false;
121
+ // A previously promoted deployment keeps its historical first-proof
122
+ // binding after that receipt expires; currentness comes from the canonical
123
+ // Evaluation tuple. Freshness is still required at the canary -> promoted
124
+ // transition below.
125
+ return deployment.state !== 'promoted' || new Set(watch.observations.filter(observation => observation.objectiveStatus === 'achieved')
126
+ .map(observation => observation.runId)).size >= deployment.canaryRuns;
127
+ }
128
+ function recordWithDigest(value, field, expected) {
129
+ return value !== null && typeof value === 'object' && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype
130
+ && Object.hasOwn(value, field) && value[field] === expected;
131
+ }
132
+ function deploymentAdmission(value) {
133
+ if (!value || typeof value !== 'object' || Array.isArray(value) || !json(value))
134
+ return false;
135
+ const admission = value;
136
+ return Object.keys(admission).length === 5 && admission.protocol === 'assistant-skills/canary-admission/v1' && name(admission.skillName)
137
+ && digest(admission.parentDefinitionDigest) && digest(admission.candidateDefinitionDigest) && !!admission.taskFamily
138
+ && Object.keys(admission.taskFamily).length === 2 && digest(admission.taskFamily.goalDefinitionDigest) && !!admission.taskFamily.outcomeProfile
139
+ && Object.keys(admission.taskFamily.outcomeProfile).length === 3 && text(admission.taskFamily.outcomeProfile.id, 256)
140
+ && version(admission.taskFamily.outcomeProfile.version) && digest(admission.taskFamily.outcomeProfile.digest);
141
+ }
142
+ function candidateOptions(value) {
143
+ if (!value || typeof value !== 'object' || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype || Object.getOwnPropertySymbols(value).length !== 0)
144
+ return false;
145
+ const descriptors = Object.getOwnPropertyDescriptors(value), keys = Object.keys(descriptors);
146
+ return keys.every(key => ['expectedVersion', 'reason', 'trigger', 'expiresAt', 'failureProvenance'].includes(key))
147
+ && ['expectedVersion', 'reason', 'trigger', 'expiresAt'].every(key => keys.includes(key))
148
+ && Object.values(descriptors).every(descriptor => descriptor.enumerable && 'value' in descriptor);
149
+ }
150
+ function repairAuthorization(value) {
151
+ if (!value || typeof value !== 'object' || Array.isArray(value) || !json(value))
152
+ return false;
153
+ const input = value;
154
+ if (!Object.keys(input).every(key => ['invocationId', 'ownerRouteId', 'source', 'profileId', 'profileDigest', 'skillName', 'parentVersion', 'parentDigest', 'maxIterations', 'expiresAt', 'profileSequence', 'feedbackAuthority'].includes(key))
155
+ || !['invocationId', 'ownerRouteId', 'source', 'profileId', 'profileDigest', 'skillName', 'parentVersion', 'parentDigest', 'maxIterations', 'expiresAt'].every(key => Object.hasOwn(input, key))
156
+ || !text(input.invocationId, 256) || !text(input.ownerRouteId, 256) || !text(input.profileId, 256) || !digest(input.profileDigest)
157
+ || !name(input.skillName) || !version(input.parentVersion) || !digest(input.parentDigest) || !Number.isSafeInteger(input.maxIterations) || input.maxIterations < 1 || input.maxIterations > 4
158
+ || !Number.isSafeInteger(input.expiresAt) || input.expiresAt <= Date.now() || input.expiresAt > Date.now() + 7 * 86400000)
159
+ return false;
160
+ if (input.profileSequence !== undefined && (!Array.isArray(input.profileSequence) || input.profileSequence.length !== input.maxIterations
161
+ || input.profileSequence.some(item => !item || typeof item !== 'object' || Object.keys(item).length !== 2 || !text(item.id, 128) || !digest(item.digest))
162
+ || input.profileSequence[0]?.id !== input.profileId || input.profileSequence[0]?.digest !== input.profileDigest))
163
+ return false;
164
+ if (input.feedbackAuthority !== undefined) {
165
+ const authority = input.feedbackAuthority;
166
+ if (!authority || typeof authority !== 'object' || Object.keys(authority).length !== 3 || !text(authority.sessionId, 512)
167
+ || !Number.isSafeInteger(authority.expiresAt) || authority.expiresAt <= Date.now() || authority.expiresAt > input.expiresAt || !json(authority.routeReceipt))
168
+ return false;
169
+ }
170
+ const source = input.source;
171
+ return !!source && typeof source === 'object' && !Array.isArray(source) && json(source) && Object.keys(source).length === 4
172
+ && text(source.goalId, 256) && text(source.sessionId, 512) && text(source.nativeGoalId, 256) && digest(source.definitionDigest);
173
+ }
174
+ function repairSource(value) {
175
+ return !!value && typeof value === 'object' && !Array.isArray(value) && json(value) && Object.keys(value).length === 4
176
+ && text(value.goalId, 256) && text(value.sessionId, 512)
177
+ && text(value.nativeGoalId, 256) && digest(value.definitionDigest);
178
+ }
179
+ function repairNextIteration(value) {
180
+ return !!value && typeof value === 'object' && !Array.isArray(value) && json(value) && Object.keys(value).length === 4
181
+ && repairSource(value.source) && Object.hasOwn(value, 'trigger')
182
+ && text(value.predecessorDeploymentId, 256) && text(value.profileId, 128);
183
+ }
184
+ function repairCheckpoint(value) {
185
+ if (!value || typeof value !== 'object' || Array.isArray(value) || !json(value))
186
+ return false;
187
+ try {
188
+ return Buffer.byteLength(JSON.stringify(value), 'utf8') <= 128 * 1024;
189
+ }
190
+ catch {
191
+ return false;
192
+ }
193
+ }
194
+ function repairState(value) {
195
+ return typeof value === 'string' && ['armed', 'source-confirmed', 'creating-repair', 'repairing', 'repair-achieved', 'capturing', 'candidate-staged', 'comparing', 'watching', 'complete', 'rejected', 'revoked', 'expired', 'unknown'].includes(value);
196
+ }
197
+ function repairTransition(from, to) {
198
+ if (['complete', 'rejected', 'revoked', 'expired', 'unknown'].includes(from))
199
+ return false;
200
+ if (['rejected', 'revoked', 'expired', 'unknown'].includes(to))
201
+ return true;
202
+ return { armed: 'source-confirmed', 'source-confirmed': 'creating-repair', 'creating-repair': 'repairing', repairing: 'repair-achieved', 'repair-achieved': 'capturing', capturing: 'candidate-staged', 'candidate-staged': 'comparing', comparing: 'watching', watching: 'complete' }[from] === to;
203
+ }
204
+ function privatePath(path) {
205
+ if (!isAbsolute(path))
206
+ fail('assistant-skills: database path must be absolute');
207
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
208
+ const directory = lstatSync(dirname(path));
209
+ if (!directory.isDirectory() || directory.isSymbolicLink() || (directory.mode & 0o022) !== 0)
210
+ fail('assistant-skills: unsafe database directory');
211
+ try {
212
+ lstatSync(path);
213
+ }
214
+ catch (error) {
215
+ if (!error || typeof error !== 'object' || !('code' in error) || error.code !== 'ENOENT')
216
+ throw error;
217
+ closeSync(openSync(path, constants.O_CREAT | constants.O_EXCL | constants.O_RDWR | constants.O_NOFOLLOW, 0o600));
218
+ chmodSync(path, 0o600);
219
+ }
220
+ for (const item of [path, `${path}-wal`, `${path}-shm`]) {
221
+ try {
222
+ const stat = lstatSync(item);
223
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || (stat.mode & 0o077) !== 0)
224
+ fail('assistant-skills: unsafe database file');
225
+ }
226
+ catch (error) {
227
+ if (!error || typeof error !== 'object' || !('code' in error) || error.code !== 'ENOENT')
228
+ throw error;
229
+ }
230
+ }
231
+ }
232
+ /** Owner-scoped immutable definitions and no-replay invocation receipts. */
233
+ export class SkillStore {
234
+ #db;
235
+ constructor(path) {
236
+ if (path !== ':memory:')
237
+ privatePath(path);
238
+ this.#db = new DatabaseSync(path);
239
+ this.#db.exec(`PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; PRAGMA busy_timeout=250;
240
+ CREATE TABLE IF NOT EXISTS skill_definitions(scope_key TEXT NOT NULL, name TEXT NOT NULL, version INTEGER NOT NULL, retired INTEGER NOT NULL, definition_json TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, PRIMARY KEY(scope_key,name,version)) STRICT, WITHOUT ROWID;
241
+ CREATE TABLE IF NOT EXISTS skill_runs(id TEXT PRIMARY KEY, scope_key TEXT NOT NULL, identity_json TEXT NOT NULL, run_json TEXT NOT NULL, state TEXT NOT NULL CHECK(state IN ('running','succeeded','failed','unknown'))) STRICT;
242
+ CREATE TABLE IF NOT EXISTS skill_candidates(scope_key TEXT NOT NULL, id TEXT NOT NULL, candidate_json TEXT NOT NULL, PRIMARY KEY(scope_key,id)) STRICT, WITHOUT ROWID;
243
+ CREATE TABLE IF NOT EXISTS skill_comparisons(scope_key TEXT NOT NULL,id TEXT NOT NULL,profile_id TEXT NOT NULL,identity_json TEXT NOT NULL,comparison_json TEXT NOT NULL,state TEXT NOT NULL CHECK(state IN ('running','complete','unknown')),PRIMARY KEY(scope_key,id)) STRICT, WITHOUT ROWID;
244
+ CREATE TABLE IF NOT EXISTS skill_watches(scope_key TEXT NOT NULL,id TEXT NOT NULL,watch_json TEXT NOT NULL,state TEXT NOT NULL CHECK(state IN ('watching','rolled-back','expired','revoked','superseded','exhausted')),PRIMARY KEY(scope_key,id)) STRICT, WITHOUT ROWID;
245
+ CREATE TABLE IF NOT EXISTS skill_deployments(scope_key TEXT NOT NULL,id TEXT NOT NULL,deployment_json TEXT NOT NULL,state TEXT NOT NULL CHECK(state IN ('canary','promoted','blocked','expired','revoked','rolled-back','superseded')),PRIMARY KEY(scope_key,id)) STRICT, WITHOUT ROWID;
246
+ CREATE TABLE IF NOT EXISTS skill_captures(scope_key TEXT NOT NULL,id TEXT NOT NULL,capture_json TEXT NOT NULL,state TEXT NOT NULL CHECK(state IN ('pending','captured','revoked','expired','unsupported','unknown')),PRIMARY KEY(scope_key,id)) STRICT, WITHOUT ROWID;
247
+ CREATE TABLE IF NOT EXISTS skill_repair_continuations(scope_key TEXT NOT NULL,id TEXT NOT NULL,authorization_digest TEXT NOT NULL,route_receipt_digest TEXT NOT NULL,revision INTEGER NOT NULL,state TEXT NOT NULL CHECK(state IN ('armed','source-confirmed','creating-repair','repairing','repair-achieved','capturing','candidate-staged','comparing','watching','complete','rejected','revoked','expired','unknown')),continuation_json TEXT NOT NULL,PRIMARY KEY(scope_key,id)) STRICT, WITHOUT ROWID;
248
+ CREATE TABLE IF NOT EXISTS skill_repair_usage(scope_key TEXT NOT NULL,id TEXT NOT NULL,authorization_digest TEXT NOT NULL,model_calls INTEGER NOT NULL CHECK(model_calls>=0),tool_calls INTEGER NOT NULL CHECK(tool_calls>=0),PRIMARY KEY(scope_key,id)) STRICT, WITHOUT ROWID;
249
+ CREATE INDEX IF NOT EXISTS skill_definitions_current ON skill_definitions(scope_key,name,version DESC);
250
+ CREATE INDEX IF NOT EXISTS skill_runs_scope ON skill_runs(scope_key,id);
251
+ CREATE INDEX IF NOT EXISTS skill_watches_scope_state ON skill_watches(scope_key,state);
252
+ CREATE INDEX IF NOT EXISTS skill_deployments_scope_state ON skill_deployments(scope_key,state);
253
+ CREATE INDEX IF NOT EXISTS skill_captures_scope_state ON skill_captures(scope_key,state);
254
+ CREATE INDEX IF NOT EXISTS skill_repair_continuations_scope_state ON skill_repair_continuations(scope_key,state);
255
+ `);
256
+ this.#db.prepare("UPDATE skill_runs SET state='unknown', run_json=json_set(run_json, '$.state', 'unknown', '$.updatedAt', ?) WHERE state='running'").run(Date.now());
257
+ this.#db.exec('BEGIN IMMEDIATE');
258
+ try {
259
+ const migrationTime = Date.now();
260
+ this.#db.prepare("UPDATE skill_watches SET state='revoked', watch_json=json_set(watch_json, '$.state', 'revoked', '$.updatedAt', ?) WHERE state='watching' AND (coalesce(json_extract(watch_json, '$.proofVersion'), '') NOT IN ('sole-skill-run/v1','canonical-goal-outcome/v2') OR EXISTS (SELECT 1 FROM skill_deployments deployment WHERE deployment.scope_key=skill_watches.scope_key AND json_extract(deployment.deployment_json, '$.watchId')=skill_watches.id AND deployment.state IN ('canary','promoted') AND (length(coalesce(json_extract(deployment.deployment_json, '$.admissionDigest'), ''))<>64 OR coalesce(json_extract(deployment.deployment_json, '$.admissionDigest'), '') GLOB '*[^0-9a-f]*' OR length(coalesce(json_extract(deployment.deployment_json, '$.candidateDefinitionDigest'), ''))<>64 OR coalesce(json_extract(deployment.deployment_json, '$.candidateDefinitionDigest'), '') GLOB '*[^0-9a-f]*' OR coalesce(json_type(deployment.deployment_json, '$.taskFamily'), '')<>'object' OR coalesce(json_type(skill_watches.watch_json, '$.taskFamily'), '')<>'object')))").run(migrationTime);
261
+ const active = this.#db.prepare("SELECT scope_key, deployment_json FROM skill_deployments WHERE state IN ('canary','promoted')").all();
262
+ for (const row of active) {
263
+ const deployment = JSON.parse(row.deployment_json);
264
+ const watch = this.#watch(row.scope_key, deployment.watchId);
265
+ const legacyShape = !digest(deployment.admissionDigest) || !digest(deployment.candidateDefinitionDigest) || !deployment.taskFamily;
266
+ if (!watch || legacyShape || !exactCanonicalObservationState(watch, deployment)) {
267
+ this.#putDeployment(row.scope_key, { ...deployment, state: 'blocked', updatedAt: migrationTime });
268
+ }
269
+ }
270
+ this.#db.exec('COMMIT');
271
+ }
272
+ catch (error) {
273
+ try {
274
+ this.#db.exec('ROLLBACK');
275
+ }
276
+ catch { }
277
+ throw error;
278
+ }
279
+ this.#db.prepare("UPDATE skill_deployments SET state='blocked', deployment_json=json_set(deployment_json, '$.state', 'blocked', '$.updatedAt', ?) WHERE state IN ('canary','promoted') AND EXISTS (SELECT 1 FROM json_each(skill_deployments.deployment_json, '$.runIds') claimed JOIN skill_runs run ON run.id=claimed.value AND run.scope_key=skill_deployments.scope_key WHERE run.state IN ('unknown','failed'))").run(Date.now());
280
+ this.#db.prepare("UPDATE skill_comparisons SET state='unknown', comparison_json=json_set(comparison_json, '$.state', 'unknown', '$.updatedAt', ?) WHERE state='running'").run(Date.now());
281
+ this.#db.exec("CREATE UNIQUE INDEX IF NOT EXISTS skill_runs_one_active ON skill_runs(scope_key,json_extract(identity_json,'$.sessionId'),json_extract(identity_json,'$.goalId')) WHERE state='running'");
282
+ this.#db.exec("CREATE UNIQUE INDEX IF NOT EXISTS skill_comparisons_one_active ON skill_comparisons(scope_key) WHERE state='running'");
283
+ }
284
+ close() { this.#db.close(); }
285
+ createRepairContinuation(scope, input, routeReceipt) {
286
+ const key = scopeKey(scope);
287
+ if (!repairAuthorization(input) || !json(routeReceipt))
288
+ fail('assistant-skills: invalid repair continuation');
289
+ const authorization = clone(input), receipt = clone(routeReceipt), id = repairContinuationId(scope, authorization);
290
+ const authorizationDigest = acceptanceDigest(authorization), routeReceiptDigest = acceptanceDigest(receipt);
291
+ if (authorization.feedbackAuthority !== undefined && acceptanceDigest(authorization.feedbackAuthority.routeReceipt) !== routeReceiptDigest)
292
+ fail('assistant-skills: repair feedback authority route mismatch');
293
+ this.#db.exec('BEGIN IMMEDIATE');
294
+ try {
295
+ const existing = this.#repairContinuation(key, id);
296
+ if (existing) {
297
+ if (existing.authorizationDigest !== authorizationDigest || acceptanceDigest(existing.routeReceipt) !== routeReceiptDigest)
298
+ fail('assistant-skills: repair continuation conflict');
299
+ this.#db.exec('COMMIT');
300
+ return clone(existing);
301
+ }
302
+ const now = Date.now(), continuation = { id, scope: clone(scope), authorization, authorizationDigest, routeReceipt: receipt, iteration: 1, revision: 1, state: 'armed', checkpoint: {}, createdAt: now, updatedAt: now };
303
+ this.#db.prepare('INSERT INTO skill_repair_continuations VALUES(?,?,?,?,?,?,?)').run(key, id, authorizationDigest, routeReceiptDigest, continuation.revision, continuation.state, JSON.stringify(continuation));
304
+ this.#db.prepare('INSERT INTO skill_repair_usage VALUES(?,?,?,?,?)').run(key, id, authorizationDigest, 0, 0);
305
+ this.#db.exec('COMMIT');
306
+ return clone(continuation);
307
+ }
308
+ catch (error) {
309
+ try {
310
+ this.#db.exec('ROLLBACK');
311
+ }
312
+ catch { }
313
+ throw error;
314
+ }
315
+ }
316
+ repairUsage(scope, id) {
317
+ const key = scopeKey(scope);
318
+ if (!text(id, 128))
319
+ fail('assistant-skills: invalid repair usage reference');
320
+ const current = this.#repairContinuation(key, id);
321
+ if (!current)
322
+ fail('assistant-skills: repair continuation missing');
323
+ const row = this.#db.prepare('SELECT authorization_digest,model_calls,tool_calls FROM skill_repair_usage WHERE scope_key=? AND id=?').get(key, id);
324
+ if (!row || row.authorization_digest !== current.authorizationDigest)
325
+ fail('assistant-skills: repair usage unavailable');
326
+ return { modelCalls: row.model_calls, toolCalls: row.tool_calls };
327
+ }
328
+ chargeRepairUsage(scope, id, kind, limit) {
329
+ const key = scopeKey(scope);
330
+ if (!text(id, 128) || !['model', 'tool'].includes(kind) || !Number.isSafeInteger(limit) || limit < 1 || limit > 1_000_000_000)
331
+ fail('assistant-skills: invalid repair usage charge');
332
+ this.#db.exec('BEGIN IMMEDIATE');
333
+ try {
334
+ const current = this.#repairContinuation(key, id);
335
+ if (!current)
336
+ fail('assistant-skills: repair continuation missing');
337
+ const row = this.#db.prepare('SELECT authorization_digest,model_calls,tool_calls FROM skill_repair_usage WHERE scope_key=? AND id=?').get(key, id);
338
+ if (!row || row.authorization_digest !== current.authorizationDigest || (kind === 'model' ? row.model_calls : row.tool_calls) >= limit)
339
+ fail('assistant-skills: repair usage exhausted');
340
+ const field = kind === 'model' ? 'model_calls' : 'tool_calls';
341
+ if (this.#db.prepare(`UPDATE skill_repair_usage SET ${field}=${field}+1 WHERE scope_key=? AND id=? AND authorization_digest=? AND ${field}<?`).run(key, id, current.authorizationDigest, limit).changes !== 1)
342
+ fail('assistant-skills: repair usage exhausted');
343
+ const saved = this.#db.prepare('SELECT model_calls,tool_calls FROM skill_repair_usage WHERE scope_key=? AND id=?').get(key, id);
344
+ this.#db.exec('COMMIT');
345
+ return { modelCalls: saved.model_calls, toolCalls: saved.tool_calls };
346
+ }
347
+ catch (error) {
348
+ try {
349
+ this.#db.exec('ROLLBACK');
350
+ }
351
+ catch { }
352
+ throw error;
353
+ }
354
+ }
355
+ getRepairContinuation(scope, id) {
356
+ const key = scopeKey(scope);
357
+ if (!text(id, 128))
358
+ fail('assistant-skills: invalid repair continuation reference');
359
+ const continuation = this.#repairContinuation(key, id);
360
+ return continuation === undefined ? undefined : clone(continuation);
361
+ }
362
+ listRepairContinuations(scope) {
363
+ const rows = scope === undefined
364
+ ? this.#db.prepare('SELECT continuation_json FROM skill_repair_continuations ORDER BY id').all()
365
+ : this.#db.prepare('SELECT continuation_json FROM skill_repair_continuations WHERE scope_key=? ORDER BY id').all(scopeKey(scope));
366
+ return rows.map(row => clone(JSON.parse(row.continuation_json)));
367
+ }
368
+ repairSourceRuns(scope, skillName, skillVersion, since) {
369
+ const key = scopeKey(scope);
370
+ if (!name(skillName) || !version(skillVersion) || !Number.isSafeInteger(since) || since < 0)
371
+ fail('assistant-skills: invalid repair source run query');
372
+ const rows = this.#db.prepare("SELECT run_json FROM skill_runs WHERE scope_key=? AND state='succeeded' AND json_extract(run_json,'$.skillName')=? AND json_extract(run_json,'$.version')=? AND json_extract(run_json,'$.createdAt')>? ORDER BY json_extract(run_json,'$.createdAt') ASC,id ASC LIMIT 50")
373
+ .all(key, skillName, skillVersion, since);
374
+ return Object.freeze(rows.map(row => clone(JSON.parse(row.run_json))));
375
+ }
376
+ transitionRepairContinuation(scope, id, expectedRevision, nextState, checkpoint) {
377
+ const key = scopeKey(scope);
378
+ if (!text(id, 128) || !version(expectedRevision) || !repairState(nextState) || !repairCheckpoint(checkpoint))
379
+ fail('assistant-skills: invalid repair continuation transition');
380
+ this.#db.exec('BEGIN IMMEDIATE');
381
+ try {
382
+ const current = this.#repairContinuation(key, id);
383
+ if (!current)
384
+ fail('assistant-skills: repair continuation missing');
385
+ if (current.revision !== expectedRevision || !repairTransition(current.state, nextState))
386
+ fail('assistant-skills: repair continuation state conflict');
387
+ const saved = { ...current, state: nextState, checkpoint: clone(checkpoint), revision: current.revision + 1, updatedAt: Date.now() };
388
+ if (this.#db.prepare('UPDATE skill_repair_continuations SET revision=?,state=?,continuation_json=? WHERE scope_key=? AND id=? AND revision=? AND state=?')
389
+ .run(saved.revision, saved.state, JSON.stringify(saved), key, id, current.revision, current.state).changes !== 1)
390
+ fail('assistant-skills: repair continuation state conflict');
391
+ this.#db.exec('COMMIT');
392
+ return clone(saved);
393
+ }
394
+ catch (error) {
395
+ try {
396
+ this.#db.exec('ROLLBACK');
397
+ }
398
+ catch { }
399
+ throw error;
400
+ }
401
+ }
402
+ nextRepairIteration(scope, id, expectedRevision, input) {
403
+ const key = scopeKey(scope);
404
+ if (!text(id, 128) || !version(expectedRevision) || !repairNextIteration(input))
405
+ fail('assistant-skills: invalid repair continuation iteration');
406
+ this.#db.exec('BEGIN IMMEDIATE');
407
+ try {
408
+ const current = this.#repairContinuation(key, id);
409
+ if (!current)
410
+ fail('assistant-skills: repair continuation missing');
411
+ const deploymentId = current.checkpoint.deploymentId;
412
+ const previous = Array.isArray(current.checkpoint.iterationHistory) ? current.checkpoint.iterationHistory : [];
413
+ const sourceDigest = acceptanceDigest(input.source), triggerDigest = acceptanceDigest(input.trigger);
414
+ const previousSources = [acceptanceDigest(current.authorization.source), ...previous.map(value => acceptanceDigest(value.source))];
415
+ const previousTriggers = [current.checkpoint.trigger === undefined ? undefined : acceptanceDigest(current.checkpoint.trigger), ...previous.map(value => value.triggerDigest)];
416
+ if (current.revision !== expectedRevision || current.state !== 'watching' || current.iteration >= current.authorization.maxIterations || current.authorization.expiresAt <= Date.now()
417
+ || current.authorization.profileSequence?.[current.iteration]?.id !== input.profileId || deploymentId !== input.predecessorDeploymentId || previousSources.includes(sourceDigest) || previousTriggers.includes(triggerDigest))
418
+ fail('assistant-skills: repair continuation unavailable');
419
+ const history = [...previous, { profileId: input.profileId, source: clone(input.source), triggerDigest, predecessorDeploymentId: input.predecessorDeploymentId }];
420
+ const checkpoint = { iterationHistory: history, profileId: input.profileId, source: clone(input.source), trigger: clone(input.trigger), predecessorDeploymentId: input.predecessorDeploymentId };
421
+ const saved = { ...current, iteration: current.iteration + 1, revision: current.revision + 1, state: 'armed', checkpoint, updatedAt: Date.now() };
422
+ if (this.#db.prepare("UPDATE skill_repair_continuations SET revision=?,state='armed',continuation_json=? WHERE scope_key=? AND id=? AND revision=? AND state='watching'")
423
+ .run(saved.revision, JSON.stringify(saved), key, id, current.revision).changes !== 1)
424
+ fail('assistant-skills: repair continuation state conflict');
425
+ this.#db.exec('COMMIT');
426
+ return clone(saved);
427
+ }
428
+ catch (error) {
429
+ try {
430
+ this.#db.exec('ROLLBACK');
431
+ }
432
+ catch { }
433
+ throw error;
434
+ }
435
+ }
436
+ save(scope, definition, expectedVersion = 0) {
437
+ const key = scopeKey(scope);
438
+ if (!definitionValid(definition) || !version(expectedVersion, true))
439
+ fail('assistant-skills: invalid definition');
440
+ this.#db.exec('BEGIN IMMEDIATE');
441
+ try {
442
+ const current = this.#latest(key, definition.name);
443
+ if ((current?.version ?? 0) !== expectedVersion)
444
+ fail('assistant-skills: version conflict');
445
+ const saved = this.#newDefinition(definition, expectedVersion + 1, current?.version ?? null);
446
+ this.#insertDefinition(key, saved);
447
+ this.#db.exec('COMMIT');
448
+ return clone(saved);
449
+ }
450
+ catch (error) {
451
+ try {
452
+ this.#db.exec('ROLLBACK');
453
+ }
454
+ catch { }
455
+ throw error;
456
+ }
457
+ }
458
+ list(scope) {
459
+ const key = scopeKey(scope);
460
+ return this.#db.prepare(`SELECT definition_json FROM skill_definitions current WHERE scope_key=? AND retired=0
461
+ AND version=(SELECT MAX(version) FROM skill_definitions versions WHERE versions.scope_key=current.scope_key AND versions.name=current.name) ORDER BY name`).all(key)
462
+ .map(row => clone(JSON.parse(row.definition_json)));
463
+ }
464
+ get(scope, skillName, wantedVersion) {
465
+ const key = scopeKey(scope);
466
+ if (!name(skillName) || wantedVersion !== undefined && !version(wantedVersion))
467
+ fail('assistant-skills: invalid skill reference');
468
+ if (wantedVersion === undefined) {
469
+ const current = this.#latest(key, skillName);
470
+ return current === undefined || current.retired ? undefined : clone(current);
471
+ }
472
+ const row = this.#db.prepare('SELECT definition_json FROM skill_definitions WHERE scope_key=? AND name=? AND version=?').get(key, skillName, wantedVersion);
473
+ return row === undefined ? undefined : clone(JSON.parse(row.definition_json));
474
+ }
475
+ retire(scope, skillName, expectedVersion) {
476
+ const key = scopeKey(scope);
477
+ if (!name(skillName) || !version(expectedVersion))
478
+ fail('assistant-skills: invalid skill reference');
479
+ this.#db.exec('BEGIN IMMEDIATE');
480
+ try {
481
+ const current = this.#latest(key, skillName);
482
+ if (!current || current.version !== expectedVersion || current.retired)
483
+ fail('assistant-skills: version conflict');
484
+ const now = Date.now();
485
+ const retired = { ...current, retired: true, updatedAt: now };
486
+ if (this.#db.prepare('UPDATE skill_definitions SET retired=1, definition_json=?, updated_at=? WHERE scope_key=? AND name=? AND version=? AND retired=0').run(JSON.stringify(retired), now, key, skillName, expectedVersion).changes !== 1)
487
+ fail('assistant-skills: version conflict');
488
+ this.#db.exec('COMMIT');
489
+ return clone(retired);
490
+ }
491
+ catch (error) {
492
+ try {
493
+ this.#db.exec('ROLLBACK');
494
+ }
495
+ catch { }
496
+ throw error;
497
+ }
498
+ }
499
+ stageCandidate(scope, definition, options) {
500
+ const key = scopeKey(scope);
501
+ if (!definitionValid(definition) || !candidateOptions(options) || !version(options.expectedVersion, true) || !text(options.reason, 1024) || !text(options.trigger, 1024)
502
+ || !Number.isSafeInteger(options.expiresAt) || options.expiresAt <= Date.now() || options.expiresAt > Date.now() + 7 * 24 * 60 * 60 * 1000)
503
+ fail('assistant-skills: invalid candidate');
504
+ this.#db.exec('BEGIN IMMEDIATE');
505
+ try {
506
+ const current = this.#latest(key, definition.name);
507
+ if (current?.retired || (current?.version ?? 0) !== options.expectedVersion || options.expectedVersion === 0 && current)
508
+ fail('assistant-skills: version conflict');
509
+ const parentDigest = current ? acceptanceDigest(current) : null;
510
+ const failureProvenance = options.failureProvenance === undefined ? undefined : this.#candidateFailureProvenance(scope, definition, options.expectedVersion, parentDigest, current, options.failureProvenance);
511
+ // Keep the legacy identity formula byte-for-byte when provenance is absent.
512
+ const identity = [scope, definition, options.expectedVersion, parentDigest, options.reason, options.trigger];
513
+ const id = `skill-candidate-${acceptanceDigest(failureProvenance === undefined ? identity : [...identity, failureProvenance])}`;
514
+ const existing = this.#candidate(key, id);
515
+ if (existing) {
516
+ if (existing.state !== 'pending')
517
+ fail('assistant-skills: candidate conflict');
518
+ this.#db.exec('COMMIT');
519
+ return clone(existing);
520
+ }
521
+ const now = Date.now();
522
+ const candidate = { id, definition: clone(definition), parentVersion: options.expectedVersion, parentDigest, reason: options.reason, trigger: options.trigger,
523
+ ...(failureProvenance === undefined ? {} : { failureProvenance }), expiresAt: options.expiresAt, state: 'pending', createdAt: now, updatedAt: now };
524
+ this.#putCandidate(key, candidate);
525
+ this.#db.exec('COMMIT');
526
+ return clone(candidate);
527
+ }
528
+ catch (error) {
529
+ try {
530
+ this.#db.exec('ROLLBACK');
531
+ }
532
+ catch { }
533
+ throw error;
534
+ }
535
+ }
536
+ getCandidate(scope, id) {
537
+ const key = scopeKey(scope);
538
+ if (!text(id, 128))
539
+ fail('assistant-skills: invalid candidate reference');
540
+ const candidate = this.#candidate(key, id);
541
+ if (candidate !== undefined)
542
+ this.#assertCandidateFailureProvenance(scope, candidate);
543
+ return candidate === undefined ? undefined : clone(candidate);
544
+ }
545
+ listCandidates(scope) {
546
+ const key = scopeKey(scope);
547
+ return this.#db.prepare('SELECT candidate_json FROM skill_candidates WHERE scope_key=? ORDER BY id').all(key).map(row => {
548
+ const candidate = JSON.parse(row.candidate_json);
549
+ this.#assertCandidateFailureProvenance(scope, candidate);
550
+ return clone(candidate);
551
+ });
552
+ }
553
+ createCapture(scope, input, routeReceipt, definitionDigest) {
554
+ const key = scopeKey(scope);
555
+ if (!input || !text(input.ownerRouteId, 256) || !text(input.goalId, 256) || !text(input.sessionId, 256) || !text(input.nativeGoalId, 256) || !name(input.name) || !text(input.description, 512)
556
+ || !version(input.parentVersion, true) || !Number.isSafeInteger(input.expiresAt) || input.expiresAt <= Date.now() || input.expiresAt > Date.now() + 7 * 86400000 || !/^[a-f0-9]{64}$/u.test(definitionDigest))
557
+ fail('assistant-skills: invalid capture');
558
+ this.#db.exec('BEGIN IMMEDIATE');
559
+ try {
560
+ const current = this.#latest(key, input.name), parentDigest = current ? acceptanceDigest(current) : null;
561
+ if (current?.retired || (current?.version ?? 0) !== input.parentVersion || (input.parentVersion === 0 && current) || parentDigest !== (input.parentVersion ? parentDigest : null))
562
+ fail('assistant-skills: version conflict');
563
+ const id = `skill-capture-${acceptanceDigest([scope, input, parentDigest, definitionDigest])}`, existing = this.#capture(key, id);
564
+ if (existing) {
565
+ this.#db.exec('COMMIT');
566
+ return clone(existing);
567
+ }
568
+ const now = Date.now(), capture = { id, scope: clone(scope), routeReceipt: clone(routeReceipt), ownerRouteId: input.ownerRouteId, goalId: input.goalId, sessionId: input.sessionId, nativeGoalId: input.nativeGoalId, name: input.name, description: input.description, parentVersion: input.parentVersion, parentDigest, definitionDigest, expiresAt: input.expiresAt, state: 'pending', createdAt: now, updatedAt: now };
569
+ this.#putCapture(key, capture);
570
+ this.#db.exec('COMMIT');
571
+ return clone(capture);
572
+ }
573
+ catch (error) {
574
+ try {
575
+ this.#db.exec('ROLLBACK');
576
+ }
577
+ catch { }
578
+ throw error;
579
+ }
580
+ }
581
+ listCaptures(scope) {
582
+ const rows = scope === undefined ? this.#db.prepare("SELECT capture_json FROM skill_captures WHERE state='pending' ORDER BY id").all() : this.#db.prepare('SELECT capture_json FROM skill_captures WHERE scope_key=? ORDER BY id').all(scopeKey(scope));
583
+ return rows.map(row => clone(JSON.parse(row.capture_json)));
584
+ }
585
+ finishCapture(scope, id, state, detail) {
586
+ const key = scopeKey(scope);
587
+ this.#db.exec('BEGIN IMMEDIATE');
588
+ try {
589
+ const capture = this.#capture(key, id);
590
+ if (!capture)
591
+ fail('assistant-skills: capture missing');
592
+ if (capture.state === 'pending')
593
+ this.#putCapture(key, { ...capture, state, ...(detail ? { detail } : {}), updatedAt: Date.now() });
594
+ this.#db.exec('COMMIT');
595
+ return clone(this.#capture(key, id));
596
+ }
597
+ catch (error) {
598
+ try {
599
+ this.#db.exec('ROLLBACK');
600
+ }
601
+ catch { }
602
+ throw error;
603
+ }
604
+ }
605
+ captureCandidate(scope, id, definition) {
606
+ const key = scopeKey(scope);
607
+ if (!definitionValid(definition))
608
+ fail('assistant-skills: invalid capture definition');
609
+ this.#db.exec('BEGIN IMMEDIATE');
610
+ try {
611
+ const capture = this.#capture(key, id);
612
+ if (!capture)
613
+ fail('assistant-skills: capture missing');
614
+ if (capture.state === 'captured') {
615
+ this.#db.exec('COMMIT');
616
+ return clone(capture);
617
+ }
618
+ if (capture.state !== 'pending' || capture.expiresAt <= Date.now())
619
+ fail('assistant-skills: capture unavailable');
620
+ const current = this.#latest(key, capture.name);
621
+ if (current?.retired || (current?.version ?? 0) !== capture.parentVersion || (current ? acceptanceDigest(current) : null) !== capture.parentDigest || definition.source.goal.id !== capture.goalId || definition.source.goal.sessionId !== capture.sessionId || definition.source.goal.nativeGoalId !== capture.nativeGoalId || definition.source.goal.definition.digest !== capture.definitionDigest)
622
+ fail('assistant-skills: capture changed');
623
+ const reason = 'Owner-preauthorized automatic capture.', trigger = `owner-route:${capture.ownerRouteId}`;
624
+ const candidateId = `skill-candidate-${acceptanceDigest([scope, definition, capture.parentVersion, capture.parentDigest, reason, trigger])}`;
625
+ const candidate = this.#candidate(key, candidateId) ?? { id: candidateId, definition: clone(definition), parentVersion: capture.parentVersion, parentDigest: capture.parentDigest, reason, trigger, expiresAt: capture.expiresAt, state: 'pending', createdAt: Date.now(), updatedAt: Date.now() };
626
+ if (candidate.state !== 'pending')
627
+ fail('assistant-skills: candidate conflict');
628
+ this.#putCandidate(key, candidate);
629
+ const saved = { ...capture, state: 'captured', candidateId: candidate.id, updatedAt: Date.now() };
630
+ this.#putCapture(key, saved);
631
+ this.#db.exec('COMMIT');
632
+ return clone(saved);
633
+ }
634
+ catch (error) {
635
+ try {
636
+ this.#db.exec('ROLLBACK');
637
+ }
638
+ catch { }
639
+ throw error;
640
+ }
641
+ }
642
+ rejectCandidate(scope, id) {
643
+ const key = scopeKey(scope);
644
+ if (!text(id, 128))
645
+ fail('assistant-skills: invalid candidate reference');
646
+ this.#db.exec('BEGIN IMMEDIATE');
647
+ try {
648
+ const candidate = this.#candidate(key, id);
649
+ if (!candidate)
650
+ fail('assistant-skills: candidate missing');
651
+ this.#assertCandidateFailureProvenance(scope, candidate);
652
+ if (candidate.state === 'activated')
653
+ fail('assistant-skills: candidate conflict');
654
+ if (candidate.state === 'pending') {
655
+ candidate.state = 'rejected';
656
+ candidate.updatedAt = Date.now();
657
+ this.#putCandidate(key, candidate);
658
+ }
659
+ this.#db.exec('COMMIT');
660
+ return clone(candidate);
661
+ }
662
+ catch (error) {
663
+ try {
664
+ this.#db.exec('ROLLBACK');
665
+ }
666
+ catch { }
667
+ throw error;
668
+ }
669
+ }
670
+ activateCandidate(scope, id, trialRunId, receipt, watch) {
671
+ const key = scopeKey(scope);
672
+ const watchDigest = watch === undefined ? undefined : acceptanceDigest(clone(watch));
673
+ if (!text(id, 128) || !text(trialRunId, 128) || !text(receipt, 1024))
674
+ fail('assistant-skills: invalid candidate activation');
675
+ this.#db.exec('BEGIN IMMEDIATE');
676
+ try {
677
+ const candidate = this.#candidate(key, id);
678
+ if (!candidate)
679
+ fail('assistant-skills: candidate missing');
680
+ this.#assertCandidateFailureProvenance(scope, candidate);
681
+ if (candidate.state === 'activated') {
682
+ if (candidate.trialRunId !== trialRunId || candidate.acceptanceDigest !== receipt || !candidate.activatedVersion
683
+ || candidate.activationWatchDigest !== watchDigest)
684
+ fail('assistant-skills: candidate conflict');
685
+ const activated = this.get(scope, candidate.definition.name, candidate.activatedVersion);
686
+ if (!activated)
687
+ fail('assistant-skills: candidate conflict');
688
+ this.#db.exec('COMMIT');
689
+ return activated;
690
+ }
691
+ if (candidate.state !== 'pending' || candidate.expiresAt <= Date.now())
692
+ fail('assistant-skills: candidate unavailable');
693
+ const current = this.#latest(key, candidate.definition.name);
694
+ if (current?.retired || (current?.version ?? 0) !== candidate.parentVersion || (current ? acceptanceDigest(current) : null) !== candidate.parentDigest)
695
+ fail('assistant-skills: version conflict');
696
+ const run = this.#run(key, trialRunId);
697
+ if (!run || run.state !== 'succeeded' || run.candidateId !== id || !run.goalExecutionRunId)
698
+ fail('assistant-skills: trial acceptance required');
699
+ const activated = this.#newDefinition(candidate.definition, candidate.parentVersion + 1, candidate.parentVersion || null);
700
+ this.#insertDefinition(key, activated);
701
+ if (watch && (watch.input.skillName !== activated.name || watch.input.version !== activated.version || watch.input.fallbackVersion !== candidate.parentVersion))
702
+ fail('assistant-skills: activation watch conflict');
703
+ // The definition, candidate and exact-version watch share this writer
704
+ // transaction. A failed watch cannot leave an unobserved active version.
705
+ const createdWatch = watch && this.#createWatch(scope, watch.input, watch.routeReceipt, 'sole-skill-run/v1');
706
+ const saved = { ...candidate, state: 'activated', activatedVersion: activated.version, trialRunId, acceptanceDigest: receipt, updatedAt: Date.now(),
707
+ ...(createdWatch ? { activationWatchDigest: watchDigest, activationWatchId: createdWatch.id } : {}) };
708
+ this.#putCandidate(key, saved);
709
+ this.#db.exec('COMMIT');
710
+ return clone(activated);
711
+ }
712
+ catch (error) {
713
+ try {
714
+ this.#db.exec('ROLLBACK');
715
+ }
716
+ catch { }
717
+ throw error;
718
+ }
719
+ }
720
+ activateQualifiedCandidate(scope, candidateId, comparisonId, qualificationDigest, admission, input, routeReceipt) {
721
+ const key = scopeKey(scope);
722
+ if (!text(candidateId, 128) || !text(comparisonId, 128) || !/^[a-f0-9]{64}$/u.test(qualificationDigest) || !deploymentAdmission(admission) || !input || !text(input.ownerRouteId, 128)
723
+ || !Number.isSafeInteger(input.expiresAt) || input.expiresAt <= Date.now() || input.expiresAt > Date.now() + 7 * 86400000
724
+ || !Number.isSafeInteger(input.maxRuns) || input.maxRuns < 1 || input.maxRuns > 100 || !Number.isSafeInteger(input.canaryRuns) || input.canaryRuns < 1 || input.canaryRuns > input.maxRuns
725
+ || !routeReceipt || !json(routeReceipt))
726
+ fail('assistant-skills: invalid qualified activation');
727
+ const admissionDigest = acceptanceDigest(admission), id = deploymentId(scope, candidateId, comparisonId, qualificationDigest, admission, input, routeReceipt);
728
+ this.#db.exec('BEGIN IMMEDIATE');
729
+ try {
730
+ const existing = this.#deployment(key, id);
731
+ if (existing) {
732
+ const definition = this.get(scope, existing.skillName, existing.version);
733
+ if (!definition)
734
+ fail('assistant-skills: deployment conflict');
735
+ this.#db.exec('COMMIT');
736
+ return { definition, deployment: clone(existing) };
737
+ }
738
+ const candidate = this.#candidate(key, candidateId);
739
+ const comparison = this.#comparison(key, comparisonId);
740
+ if (candidate)
741
+ this.#assertCandidateFailureProvenance(scope, candidate);
742
+ if (!candidate || candidate.state !== 'pending' || candidate.expiresAt <= Date.now() || candidate.parentVersion <= 0 || !comparison || comparison.state !== 'complete'
743
+ || comparison.candidateId !== candidateId || !json(comparison.result) || acceptanceDigest(comparison.result) !== qualificationDigest
744
+ || !recordWithDigest(comparison.result, 'admissionDigest', admissionDigest)
745
+ || candidate.definition.name !== admission.skillName || acceptanceDigest(candidate.definition) !== admission.candidateDefinitionDigest
746
+ || candidate.definition.source.goal.definition.digest !== admission.taskFamily.goalDefinitionDigest)
747
+ fail('assistant-skills: qualification unavailable');
748
+ const current = this.#latest(key, candidate.definition.name);
749
+ if (!current || current.retired || current.version !== candidate.parentVersion || acceptanceDigest(current) !== candidate.parentDigest
750
+ || admission.parentDefinitionDigest !== candidate.parentDigest)
751
+ fail('assistant-skills: version conflict');
752
+ const definition = this.#newDefinition(candidate.definition, candidate.parentVersion + 1, candidate.parentVersion);
753
+ this.#insertDefinition(key, definition);
754
+ const watch = this.#createWatch(scope, { ownerRouteId: input.ownerRouteId, skillName: definition.name, version: definition.version, fallbackVersion: candidate.parentVersion, expiresAt: input.expiresAt, maxRuns: input.maxRuns, failureThreshold: 1 }, routeReceipt, 'canonical-goal-outcome/v2', admission.taskFamily);
755
+ const now = Date.now();
756
+ const deployment = { id, scope: clone(scope), candidateId, comparisonId, qualificationDigest, admissionDigest, candidateDefinitionDigest: admission.candidateDefinitionDigest, taskFamily: clone(admission.taskFamily), routeReceipt: clone(routeReceipt), ownerRouteId: input.ownerRouteId, skillName: definition.name, version: definition.version,
757
+ definitionDigest: acceptanceDigest(definition), parentVersion: candidate.parentVersion, watchId: watch.id, expiresAt: input.expiresAt, maxRuns: input.maxRuns, canaryRuns: input.canaryRuns, runIds: [], state: 'canary', createdAt: now, updatedAt: now };
758
+ this.#putDeployment(key, deployment);
759
+ this.#putCandidate(key, { ...candidate, state: 'activated', activatedVersion: definition.version, activationComparisonId: comparisonId, deploymentId: id, updatedAt: now });
760
+ this.#db.exec('COMMIT');
761
+ return { definition: clone(definition), deployment: clone(deployment) };
762
+ }
763
+ catch (error) {
764
+ try {
765
+ this.#db.exec('ROLLBACK');
766
+ }
767
+ catch { }
768
+ throw error;
769
+ }
770
+ }
771
+ getDeployment(scope, id) {
772
+ const key = scopeKey(scope);
773
+ if (!text(id, 128))
774
+ fail('assistant-skills: invalid deployment reference');
775
+ const deployment = this.#deployment(key, id);
776
+ return deployment === undefined ? undefined : clone(deployment);
777
+ }
778
+ deploymentForVersion(scope, skillName, wantedVersion) {
779
+ const key = scopeKey(scope);
780
+ if (!name(skillName) || !version(wantedVersion))
781
+ fail('assistant-skills: invalid deployment reference');
782
+ const rows = this.#db.prepare('SELECT deployment_json FROM skill_deployments WHERE scope_key=?').all(key);
783
+ const deployment = rows.map(row => JSON.parse(row.deployment_json)).find(value => value.skillName === skillName && value.version === wantedVersion);
784
+ return deployment === undefined ? undefined : clone(deployment);
785
+ }
786
+ listDeployments(scope) {
787
+ const rows = scope === undefined ? this.#db.prepare('SELECT deployment_json FROM skill_deployments ORDER BY id').all() : this.#db.prepare('SELECT deployment_json FROM skill_deployments WHERE scope_key=? ORDER BY id').all(scopeKey(scope));
788
+ return rows.map(row => clone(JSON.parse(row.deployment_json)));
789
+ }
790
+ reconcileDeployment(scope, id) {
791
+ const key = scopeKey(scope);
792
+ if (!text(id, 128))
793
+ fail('assistant-skills: invalid deployment reference');
794
+ this.#db.exec('BEGIN IMMEDIATE');
795
+ try {
796
+ const deployment = this.#deployment(key, id);
797
+ const saved = deployment && this.#reconcileDeployment(key, deployment, false);
798
+ this.#db.exec('COMMIT');
799
+ return saved && clone(saved);
800
+ }
801
+ catch (error) {
802
+ try {
803
+ this.#db.exec('ROLLBACK');
804
+ }
805
+ catch { }
806
+ throw error;
807
+ }
808
+ }
809
+ /** Promotion-capable reconciliation; call only while holding Evaluation's exact canonical writer fence. */
810
+ reconcileDeploymentWithCanonicalPromotion(scope, id) {
811
+ const key = scopeKey(scope);
812
+ if (!text(id, 128))
813
+ fail('assistant-skills: invalid deployment reference');
814
+ this.#db.exec('BEGIN IMMEDIATE');
815
+ try {
816
+ const deployment = this.#deployment(key, id);
817
+ const saved = deployment && this.#reconcileDeployment(key, deployment, true);
818
+ this.#db.exec('COMMIT');
819
+ return saved && clone(saved);
820
+ }
821
+ catch (error) {
822
+ try {
823
+ this.#db.exec('ROLLBACK');
824
+ }
825
+ catch { }
826
+ throw error;
827
+ }
828
+ }
829
+ stopDeployment(scope, id, state) {
830
+ const key = scopeKey(scope);
831
+ if (!text(id, 128))
832
+ fail('assistant-skills: invalid deployment reference');
833
+ this.#db.exec('BEGIN IMMEDIATE');
834
+ try {
835
+ const deployment = this.#deployment(key, id);
836
+ if (!deployment || deployment.state !== 'canary' && deployment.state !== 'promoted') {
837
+ this.#db.exec('COMMIT');
838
+ return deployment && clone(deployment);
839
+ }
840
+ ;
841
+ const saved = { ...deployment, state, updatedAt: Date.now() };
842
+ this.#putDeployment(key, saved);
843
+ this.#db.exec('COMMIT');
844
+ return clone(saved);
845
+ }
846
+ catch (error) {
847
+ try {
848
+ this.#db.exec('ROLLBACK');
849
+ }
850
+ catch { }
851
+ throw error;
852
+ }
853
+ }
854
+ assertDeploymentRun(scope, id) {
855
+ const key = scopeKey(scope);
856
+ if (!text(id, 128))
857
+ fail('assistant-skills: invalid deployment run');
858
+ const deployment = this.#deploymentsForRun(key, id)[0];
859
+ if (!deployment || deployment.state !== 'canary' && deployment.state !== 'promoted' || deployment.expiresAt <= Date.now())
860
+ fail('assistant-skills: deployment unavailable');
861
+ const watch = this.#watch(key, deployment.watchId);
862
+ if (!watch || !exactCanonicalObservationState(watch, deployment))
863
+ fail('assistant-skills: deployment unavailable');
864
+ const active = this.#latest(key, deployment.skillName);
865
+ if (!active || active.retired || active.version !== deployment.version || acceptanceDigest(active) !== deployment.definitionDigest)
866
+ fail('assistant-skills: deployment unavailable');
867
+ return clone(deployment);
868
+ }
869
+ rollback(scope, skillName, expectedVersion, targetVersion) {
870
+ const key = scopeKey(scope);
871
+ if (!name(skillName) || !version(expectedVersion) || !version(targetVersion))
872
+ fail('assistant-skills: invalid skill reference');
873
+ this.#db.exec('BEGIN IMMEDIATE');
874
+ try {
875
+ const current = this.#latest(key, skillName);
876
+ const failureCandidate = this.#activatedFailureCandidate(key, skillName, expectedVersion);
877
+ const target = this.get(scope, skillName, targetVersion);
878
+ if (failureCandidate) {
879
+ const provenance = this.#assertCandidateFailureProvenance(scope, failureCandidate);
880
+ if (!provenance || !target || target.retired || provenance.rollbackTarget.name !== skillName || provenance.rollbackTarget.version !== targetVersion
881
+ || provenance.rollbackTarget.digest !== acceptanceDigest(target))
882
+ fail('assistant-skills: failure rollback target changed');
883
+ }
884
+ if (current && !current.retired && current.version === expectedVersion + 1 && current.parentVersion === expectedVersion && current.restoredFromVersion === targetVersion) {
885
+ this.#db.exec('COMMIT');
886
+ return clone(current);
887
+ }
888
+ if (!current || current.retired || current.version !== expectedVersion || current.parentVersion !== targetVersion)
889
+ fail('assistant-skills: version conflict');
890
+ if (!target || target.retired)
891
+ fail('assistant-skills: version conflict');
892
+ const restored = this.#newDefinition(target, expectedVersion + 1, expectedVersion, targetVersion);
893
+ this.#insertDefinition(key, restored);
894
+ this.#db.exec('COMMIT');
895
+ return clone(restored);
896
+ }
897
+ catch (error) {
898
+ try {
899
+ this.#db.exec('ROLLBACK');
900
+ }
901
+ catch { }
902
+ throw error;
903
+ }
904
+ }
905
+ createWatch(scope, input, routeReceipt) {
906
+ this.#db.exec('BEGIN IMMEDIATE');
907
+ try {
908
+ const watch = this.#createWatch(scope, input, routeReceipt, 'sole-skill-run/v1');
909
+ this.#db.exec('COMMIT');
910
+ return watch;
911
+ }
912
+ catch (error) {
913
+ try {
914
+ this.#db.exec('ROLLBACK');
915
+ }
916
+ catch { }
917
+ throw error;
918
+ }
919
+ }
920
+ #createWatch(scope, input, routeReceipt, proofVersion, taskFamily) {
921
+ const key = scopeKey(scope);
922
+ if (!input || !routeReceipt || !json(routeReceipt) || !text(input.ownerRouteId, 128) || !name(input.skillName) || !version(input.version) || !version(input.fallbackVersion)
923
+ || !Number.isSafeInteger(input.expiresAt) || input.expiresAt <= Date.now() || input.expiresAt > Date.now() + 7 * 24 * 60 * 60 * 1000
924
+ || !Number.isSafeInteger(input.maxRuns) || input.maxRuns < 1 || input.maxRuns > 100 || !Number.isSafeInteger(input.failureThreshold) || input.failureThreshold < 1 || input.failureThreshold > input.maxRuns)
925
+ fail('assistant-skills: invalid watch');
926
+ const active = this.#latest(key, input.skillName), fallback = this.get(scope, input.skillName, input.fallbackVersion);
927
+ if (!active || active.retired || active.version !== input.version || active.parentVersion !== input.fallbackVersion || !fallback || fallback.retired)
928
+ fail('assistant-skills: watch version conflict');
929
+ const id = watchId(scope, input), existing = this.#watch(key, id);
930
+ if (taskFamily !== undefined && (!deploymentAdmission({ protocol: 'assistant-skills/canary-admission/v1', skillName: input.skillName, parentDefinitionDigest: acceptanceDigest(fallback), candidateDefinitionDigest: acceptanceDigest(active), taskFamily })))
931
+ fail('assistant-skills: invalid watch task family');
932
+ if (existing) {
933
+ if (existing.proofVersion !== proofVersion || acceptanceDigest(existing.taskFamily ?? null) !== acceptanceDigest(taskFamily ?? null))
934
+ fail('assistant-skills: watch proof conflict');
935
+ return clone(existing);
936
+ }
937
+ const now = Date.now();
938
+ const afterRunRowId = this.#db.prepare('SELECT coalesce(max(rowid),0) AS rowId FROM skill_runs').get().rowId;
939
+ const watch = { id, scope: clone(scope), routeReceipt: clone(routeReceipt), afterRunRowId, ownerRouteId: input.ownerRouteId, skillName: input.skillName, version: input.version, definitionDigest: acceptanceDigest(active), fallbackVersion: input.fallbackVersion, fallbackDigest: acceptanceDigest(fallback), expiresAt: input.expiresAt, maxRuns: input.maxRuns, failureThreshold: input.failureThreshold, state: 'watching', runIds: [], observations: [], createdAt: now, updatedAt: now, proofVersion, ...(proofVersion === 'canonical-goal-outcome/v2' ? { canonicalRevisions: [] } : {}), ...(taskFamily === undefined ? {} : { taskFamily: clone(taskFamily) }) };
940
+ this.#putWatch(key, watch);
941
+ return clone(watch);
942
+ }
943
+ listWatches(scope) {
944
+ const rows = scope === undefined ? this.#db.prepare('SELECT watch_json FROM skill_watches WHERE state=\'watching\' ORDER BY id').all() : this.#db.prepare('SELECT watch_json FROM skill_watches WHERE scope_key=? ORDER BY id').all(scopeKey(scope));
945
+ return rows.map(row => clone(JSON.parse(row.watch_json)));
946
+ }
947
+ #attachWatchRun(scope, run) {
948
+ const key = scopeKey(scope);
949
+ if (run.state !== 'succeeded' || !run.goalExecutionRunId || run.candidateId !== undefined)
950
+ return;
951
+ for (const watch of this.#watches(key, 'watching')) {
952
+ if (watch.expiresAt <= Date.now()) {
953
+ this.#putWatch(key, { ...watch, state: 'expired', updatedAt: Date.now() });
954
+ continue;
955
+ }
956
+ const rowId = this.#db.prepare('SELECT rowid AS rowId FROM skill_runs WHERE id=?').get(run.id)?.rowId;
957
+ if (!Number.isSafeInteger(watch.afterRunRowId) || rowId === undefined || rowId <= watch.afterRunRowId || run.createdAt < watch.createdAt || watch.skillName !== run.skillName || watch.version !== run.version || watch.definitionDigest !== acceptanceDigest(this.get(scope, run.skillName, run.version)))
958
+ continue;
959
+ if (watch.runIds.includes(run.id))
960
+ continue;
961
+ if (watch.runIds.length >= watch.maxRuns)
962
+ continue;
963
+ this.#putWatch(key, { ...watch, runIds: [...watch.runIds, run.id], updatedAt: Date.now() });
964
+ }
965
+ }
966
+ observeWatch(scope, id, observation) {
967
+ const key = scopeKey(scope);
968
+ if (!text(id, 128) || !text(observation.runId, 128) || !/^[a-f0-9]{64}$/u.test(observation.receiptDigest) || !['achieved', 'not-achieved'].includes(observation.objectiveStatus) || !Number.isSafeInteger(observation.verifiedAt) || !Number.isSafeInteger(observation.validUntil)
969
+ || !/^[a-f0-9]{64}$/u.test(observation.executionTraceDigest) || observation.taskFamilyDigest !== undefined && !/^[a-f0-9]{64}$/u.test(observation.taskFamilyDigest))
970
+ fail('assistant-skills: invalid watch observation');
971
+ this.#db.exec('BEGIN IMMEDIATE');
972
+ try {
973
+ const watch = this.#watch(key, id);
974
+ if (!watch || watch.state !== 'watching') {
975
+ this.#db.exec('COMMIT');
976
+ return watch && clone(watch);
977
+ }
978
+ const deployment = this.#deploymentForWatch(key, watch.id);
979
+ if (deployment !== undefined && (watch.taskFamily === undefined || observation.taskFamilyDigest !== acceptanceDigest(watch.taskFamily)) || !watch.runIds.includes(observation.runId)
980
+ || watch.observations.some(value => value.receiptDigest === observation.receiptDigest || value.runId === observation.runId
981
+ || observation.executionTraceDigest !== undefined && value.executionTraceDigest === observation.executionTraceDigest)
982
+ || observation.verifiedAt < watch.createdAt || observation.verifiedAt > Date.now() || observation.validUntil <= Date.now()) {
983
+ this.#db.exec('COMMIT');
984
+ return clone(watch);
985
+ }
986
+ const saved = { ...watch, observations: [...watch.observations, clone(observation)], updatedAt: Date.now() };
987
+ this.#putWatch(key, saved);
988
+ this.#db.exec('COMMIT');
989
+ return clone(saved);
990
+ }
991
+ catch (error) {
992
+ try {
993
+ this.#db.exec('ROLLBACK');
994
+ }
995
+ catch { }
996
+ throw error;
997
+ }
998
+ }
999
+ replaceWatchObservation(scope, id, result) {
1000
+ return this.#replaceWatchObservation(scope, id, result, false);
1001
+ }
1002
+ /** Commit the canonical replacement, deployment transition and any required exact-version rollback in one writer transaction. */
1003
+ replaceWatchObservationAndRollback(scope, id, result) {
1004
+ return this.#replaceWatchObservation(scope, id, result, true);
1005
+ }
1006
+ /** Commit a positive canonical replacement and promotion-only reconciliation under one Evaluation writer fence. */
1007
+ replaceWatchObservationAndPromote(scope, id, deploymentId, result) {
1008
+ const key = scopeKey(scope);
1009
+ if (!text(id, 128) || !text(deploymentId, 128) || !result || typeof result !== 'object' || Array.isArray(result)
1010
+ || result.kind !== 'current' || result.observation.objectiveStatus !== 'achieved') {
1011
+ fail('assistant-skills: invalid canonical promotion');
1012
+ }
1013
+ this.#db.exec('BEGIN IMMEDIATE');
1014
+ try {
1015
+ const deployment = this.#deployment(key, deploymentId);
1016
+ if (!deployment || deployment.watchId !== id || !validRunIds(deployment.runIds))
1017
+ fail('assistant-skills: canonical promotion conflict');
1018
+ const watch = this.#replaceWatchObservationInTransaction(scope, key, id, result, false);
1019
+ if (!watch || watch.state !== 'watching')
1020
+ fail('assistant-skills: canonical promotion unavailable');
1021
+ const reconciled = this.#reconcileDeployment(key, deployment, true);
1022
+ this.#db.exec('COMMIT');
1023
+ return { watch: clone(watch), deployment: clone(reconciled) };
1024
+ }
1025
+ catch (error) {
1026
+ try {
1027
+ this.#db.exec('ROLLBACK');
1028
+ }
1029
+ catch { }
1030
+ throw error;
1031
+ }
1032
+ }
1033
+ #replaceWatchObservation(scope, id, result, rollbackOnFailure) {
1034
+ const key = scopeKey(scope);
1035
+ this.#db.exec('BEGIN IMMEDIATE');
1036
+ try {
1037
+ const saved = this.#replaceWatchObservationInTransaction(scope, key, id, result, rollbackOnFailure);
1038
+ this.#db.exec('COMMIT');
1039
+ return saved === undefined ? undefined : clone(saved);
1040
+ }
1041
+ catch (error) {
1042
+ try {
1043
+ this.#db.exec('ROLLBACK');
1044
+ }
1045
+ catch { }
1046
+ throw error;
1047
+ }
1048
+ }
1049
+ #replaceWatchObservationInTransaction(scope, key, id, result, rollbackOnFailure) {
1050
+ if (!text(id, 128) || !result || typeof result !== 'object' || Array.isArray(result) || !json(result)
1051
+ || !['current', 'invalidated'].includes(result.kind))
1052
+ fail('assistant-skills: invalid watch observation revision');
1053
+ const run = result.kind === 'current' ? result.observation.runId : result.runId;
1054
+ const canonical = result.kind === 'current' ? result.observation.canonical : result.canonical;
1055
+ if (!text(run, 128) || !canonicalRevision(canonical)
1056
+ || result.kind === 'current' && (canonical.disposition !== 'upsert' || !digest(result.observation.receiptDigest)
1057
+ || !['achieved', 'not-achieved'].includes(result.observation.objectiveStatus) || !Number.isSafeInteger(result.observation.verifiedAt)
1058
+ || !Number.isSafeInteger(result.observation.validUntil) || !digest(result.observation.executionTraceDigest)
1059
+ || result.observation.taskFamilyDigest !== undefined && !digest(result.observation.taskFamilyDigest))
1060
+ || result.kind === 'invalidated' && canonical.disposition !== 'retract')
1061
+ fail('assistant-skills: invalid watch observation revision');
1062
+ const watch = this.#watch(key, id);
1063
+ if (!watch || watch.state !== 'watching')
1064
+ return watch;
1065
+ // v1 qualified observations predate revisioned Evaluation evidence. Never
1066
+ // mutate them into a v2 shape opportunistically; constructor quarantine is
1067
+ // irreversible and a fresh qualified deployment is required.
1068
+ if (watch.proofVersion !== 'canonical-goal-outcome/v2')
1069
+ return watch;
1070
+ const deployment = this.#deploymentForWatch(key, watch.id);
1071
+ const taskFamilyDigest = watch.taskFamily === undefined ? undefined : acceptanceDigest(watch.taskFamily);
1072
+ if (!deployment || taskFamilyDigest === undefined || acceptanceDigest(deployment.taskFamily) !== taskFamilyDigest
1073
+ || !watch.runIds.includes(run) || result.kind === 'current' && result.observation.taskFamilyDigest !== taskFamilyDigest) {
1074
+ return watch;
1075
+ }
1076
+ const finish = (candidate) => {
1077
+ let final = candidate;
1078
+ if (rollbackOnFailure) {
1079
+ const reconciled = this.#reconcileDeployment(key, deployment, false);
1080
+ final = this.#rollbackWatch(scope, key, candidate);
1081
+ if (final.state === 'rolled-back')
1082
+ this.#reconcileDeployment(key, reconciled, false);
1083
+ }
1084
+ return final;
1085
+ };
1086
+ const revisions = watch.canonicalRevisions ?? [];
1087
+ const previous = revisions.find(value => value.runId === run);
1088
+ // Old direct callers did not carry an explicit binding. Permit deriving
1089
+ // it once; after that the persisted first proof is authoritative.
1090
+ const suppliedBinding = result.binding ?? (previous === undefined && result.kind === 'current'
1091
+ ? observationBinding(result.observation, canonical.subjectRef) : undefined);
1092
+ const binding = previous?.binding ?? suppliedBinding;
1093
+ if (!validObservationBinding(binding) || binding.runId !== run || binding.subjectRef !== canonical.subjectRef
1094
+ || binding.taskFamilyDigest !== taskFamilyDigest)
1095
+ fail('assistant-skills: invalid watch observation binding');
1096
+ if (suppliedBinding !== undefined && acceptanceDigest(suppliedBinding) !== acceptanceDigest(binding))
1097
+ fail('assistant-skills: canonical binding conflict');
1098
+ if (result.kind === 'current' && result.binding !== undefined && (result.observation.runId !== binding.runId || result.observation.receiptDigest !== binding.receiptDigest
1099
+ || result.observation.verifiedAt !== binding.verifiedAt || result.observation.validUntil !== binding.validUntil
1100
+ || result.observation.executionTraceDigest !== binding.executionTraceDigest || result.observation.taskFamilyDigest !== binding.taskFamilyDigest)) {
1101
+ fail('assistant-skills: canonical binding conflict');
1102
+ }
1103
+ if (previous !== undefined) {
1104
+ if (canonical.subjectRef !== previous.subjectRef)
1105
+ fail('assistant-skills: canonical revision conflict');
1106
+ if (canonical.version < previous.version)
1107
+ return finish(watch);
1108
+ if (canonical.version === previous.version) {
1109
+ if (canonical.digest !== previous.digest || canonical.disposition !== previous.disposition)
1110
+ fail('assistant-skills: canonical revision conflict');
1111
+ const priorObservation = watch.observations.find(value => value.runId === run);
1112
+ const { subjectRef: _subjectRef, ...observationBinding } = binding;
1113
+ const expectedObservation = result.kind === 'current'
1114
+ ? { ...observationBinding, objectiveStatus: result.observation.objectiveStatus, canonical: {
1115
+ subjectKind: previous.subjectKind, subjectRef: previous.subjectRef, version: previous.version, digest: previous.digest,
1116
+ disposition: previous.disposition, scopeWatermark: previous.scopeWatermark,
1117
+ } } : undefined;
1118
+ if (result.kind === 'current' && (priorObservation === undefined || acceptanceDigest(priorObservation) !== acceptanceDigest(expectedObservation))
1119
+ || result.kind === 'invalidated' && priorObservation !== undefined)
1120
+ fail('assistant-skills: canonical revision conflict');
1121
+ return finish(watch);
1122
+ }
1123
+ if (canonical.scopeWatermark <= previous.scopeWatermark)
1124
+ fail('assistant-skills: canonical revision conflict');
1125
+ }
1126
+ if (previous === undefined && (binding.verifiedAt < watch.createdAt || binding.verifiedAt > Date.now()
1127
+ || binding.validUntil <= Date.now())
1128
+ || result.kind === 'current' && (watch.observations.some(value => value.runId !== run && (value.receiptDigest === result.observation.receiptDigest
1129
+ || value.executionTraceDigest === result.observation.executionTraceDigest)))) {
1130
+ return watch;
1131
+ }
1132
+ const state = { runId: run, ...clone(canonical), binding: clone(binding) };
1133
+ const observations = watch.observations.filter(value => value.runId !== run);
1134
+ const { subjectRef: _subjectRef, ...observationFields } = binding;
1135
+ const currentObservation = result.kind === 'current' ? { ...observationFields, objectiveStatus: result.observation.objectiveStatus, canonical: clone(canonical) } : undefined;
1136
+ const saved = { ...watch, canonicalRevisions: [...revisions.filter(value => value.runId !== run), state],
1137
+ observations: currentObservation === undefined ? observations : [...observations, currentObservation], updatedAt: Date.now() };
1138
+ this.#putWatch(key, saved);
1139
+ return finish(saved);
1140
+ }
1141
+ stopWatch(scope, id, state) {
1142
+ const key = scopeKey(scope);
1143
+ this.#db.exec('BEGIN IMMEDIATE');
1144
+ try {
1145
+ const watch = this.#watch(key, id);
1146
+ if (!watch || watch.state !== 'watching') {
1147
+ this.#db.exec('COMMIT');
1148
+ return watch && clone(watch);
1149
+ }
1150
+ ;
1151
+ const saved = { ...watch, state, updatedAt: Date.now() };
1152
+ this.#putWatch(key, saved);
1153
+ this.#db.exec('COMMIT');
1154
+ return clone(saved);
1155
+ }
1156
+ catch (error) {
1157
+ try {
1158
+ this.#db.exec('ROLLBACK');
1159
+ }
1160
+ catch { }
1161
+ throw error;
1162
+ }
1163
+ }
1164
+ rollbackWatch(scope, id) {
1165
+ const key = scopeKey(scope);
1166
+ this.#db.exec('BEGIN IMMEDIATE');
1167
+ try {
1168
+ const watch = this.#watch(key, id);
1169
+ if (!watch || watch.state !== 'watching') {
1170
+ this.#db.exec('COMMIT');
1171
+ return watch && clone(watch);
1172
+ }
1173
+ const saved = this.#rollbackWatch(scope, key, watch);
1174
+ this.#db.exec('COMMIT');
1175
+ return clone(saved);
1176
+ }
1177
+ catch (error) {
1178
+ try {
1179
+ this.#db.exec('ROLLBACK');
1180
+ }
1181
+ catch { }
1182
+ throw error;
1183
+ }
1184
+ }
1185
+ #rollbackWatch(scope, key, watch) {
1186
+ if (watch.expiresAt <= Date.now()) {
1187
+ const saved = { ...watch, state: 'expired', updatedAt: Date.now() };
1188
+ this.#putWatch(key, saved);
1189
+ return saved;
1190
+ }
1191
+ const deployment = this.#deploymentForWatch(key, watch.id);
1192
+ // Public standalone watches have no operator-pinned task family. They
1193
+ // retain useful outcome observations, but can never supply mutation
1194
+ // authority. Only the exact watch created by a qualified canary
1195
+ // deployment may append a rollback version.
1196
+ if (!deployment || !watch.taskFamily || !['canary', 'promoted', 'blocked'].includes(deployment.state)
1197
+ || deployment.watchId !== watch.id || deployment.skillName !== watch.skillName || deployment.version !== watch.version
1198
+ || deployment.definitionDigest !== watch.definitionDigest || acceptanceDigest(deployment.taskFamily) !== acceptanceDigest(watch.taskFamily)) {
1199
+ return watch;
1200
+ }
1201
+ const taskFamilyDigest = acceptanceDigest(watch.taskFamily);
1202
+ const currentCanonical = (value) => value.canonical !== undefined
1203
+ && watch.canonicalRevisions?.some(revision => revision.runId === value.runId && revision.disposition === 'upsert'
1204
+ && revision.subjectRef === value.canonical.subjectRef && revision.version === value.canonical.version
1205
+ && revision.digest === value.canonical.digest && revision.scopeWatermark === value.canonical.scopeWatermark);
1206
+ const failures = watch.observations.filter(value => value.objectiveStatus === 'not-achieved' && value.taskFamilyDigest === taskFamilyDigest && currentCanonical(value)).length
1207
+ + (watch.canonicalRevisions?.filter(value => value.disposition === 'retract' && watch.runIds.includes(value.runId)).length ?? 0);
1208
+ if (failures < watch.failureThreshold)
1209
+ return watch;
1210
+ const current = this.#latest(key, watch.skillName), target = this.get(scope, watch.skillName, watch.fallbackVersion);
1211
+ const failureCandidate = this.#activatedFailureCandidate(key, watch.skillName, watch.version);
1212
+ if (failureCandidate) {
1213
+ const provenance = this.#assertCandidateFailureProvenance(scope, failureCandidate);
1214
+ if (!provenance || !target || provenance.rollbackTarget.name !== watch.skillName || provenance.rollbackTarget.version !== watch.fallbackVersion
1215
+ || provenance.rollbackTarget.digest !== acceptanceDigest(target)) {
1216
+ const saved = { ...watch, state: 'superseded', updatedAt: Date.now() };
1217
+ this.#putWatch(key, saved);
1218
+ return saved;
1219
+ }
1220
+ }
1221
+ if (!current || current.retired || current.version !== watch.version || acceptanceDigest(current) !== watch.definitionDigest || current.parentVersion !== watch.fallbackVersion || !target || target.retired || acceptanceDigest(target) !== watch.fallbackDigest) {
1222
+ const saved = { ...watch, state: 'superseded', updatedAt: Date.now() };
1223
+ this.#putWatch(key, saved);
1224
+ return saved;
1225
+ }
1226
+ const restored = this.#newDefinition(target, current.version + 1, current.version, watch.fallbackVersion);
1227
+ this.#insertDefinition(key, restored);
1228
+ const saved = { ...watch, state: 'rolled-back', rollbackVersion: restored.version, updatedAt: Date.now() };
1229
+ this.#putWatch(key, saved);
1230
+ return saved;
1231
+ }
1232
+ claim(scope, input) {
1233
+ const key = scopeKey(scope);
1234
+ this.#validateClaim(scope, input);
1235
+ const id = runId(scope, input.sessionId, input.invocationId);
1236
+ const identity = clone({ invocationId: input.invocationId, goalId: input.goalId, sessionId: input.sessionId, skillName: input.skillName, version: input.version, inputs: input.inputs, ...(input.goalExecutionRunId === undefined ? {} : { goalExecutionRunId: input.goalExecutionRunId }),
1237
+ ...(input.goalDefinitionDigest === undefined ? {} : { goalDefinitionDigest: input.goalDefinitionDigest }), ...(input.nativeGoalId === undefined ? {} : { nativeGoalId: input.nativeGoalId }), ...(input.candidateId === undefined ? {} : { candidateId: input.candidateId }) });
1238
+ this.#db.exec('BEGIN IMMEDIATE');
1239
+ try {
1240
+ if (input.candidateId !== undefined)
1241
+ this.#validateTrialClaim(key, input);
1242
+ const existing = this.#db.prepare('SELECT identity_json,run_json FROM skill_runs WHERE id=? AND scope_key=?').get(id, key);
1243
+ if (existing) {
1244
+ if (acceptanceDigest(identity) !== acceptanceDigest(JSON.parse(existing.identity_json)))
1245
+ fail('assistant-skills: invocation conflict');
1246
+ this.#db.exec('COMMIT');
1247
+ return { claimed: false, run: clone(JSON.parse(existing.run_json)) };
1248
+ }
1249
+ if (input.candidateId === undefined) {
1250
+ const active = this.#latest(key, input.skillName);
1251
+ if (!active || active.retired || active.version !== input.version)
1252
+ fail('assistant-skills: inactive skill version');
1253
+ const deployment = this.#deploymentForVersion(key, input.skillName, input.version);
1254
+ if (deployment) {
1255
+ const reconciled = this.#reconcileDeployment(key, deployment, false);
1256
+ if (reconciled.state !== 'canary' && reconciled.state !== 'promoted' || reconciled.expiresAt <= Date.now())
1257
+ fail('assistant-skills: deployment unavailable');
1258
+ if (acceptanceDigest(active) !== reconciled.definitionDigest || reconciled.runIds.length >= reconciled.maxRuns || reconciled.state === 'canary' && reconciled.runIds.length >= reconciled.canaryRuns)
1259
+ fail('assistant-skills: deployment quota exhausted');
1260
+ this.#putDeployment(key, { ...reconciled, runIds: [...reconciled.runIds, id], updatedAt: Date.now() });
1261
+ }
1262
+ }
1263
+ const unresolved = this.#db.prepare("SELECT run_json FROM skill_runs WHERE scope_key=? AND json_extract(identity_json,'$.sessionId')=? AND json_extract(identity_json,'$.goalId')=? AND json_extract(identity_json,'$.skillName')=? AND json_extract(identity_json,'$.version')=? AND coalesce(json_extract(identity_json,'$.candidateId'),'')=coalesce(?, '') AND state IN ('running','unknown') LIMIT 1")
1264
+ .get(key, input.sessionId, input.goalId, input.skillName, input.version, input.candidateId ?? null);
1265
+ if (unresolved)
1266
+ fail('assistant-skills: unresolved invocation for this Goal; inspect skill_status, do not replay');
1267
+ const now = Date.now();
1268
+ const run = { id, ...identity, state: 'running', steps: [], createdAt: now, updatedAt: now };
1269
+ this.#db.prepare('INSERT INTO skill_runs VALUES(?,?,?,?,?)').run(id, key, JSON.stringify(identity), JSON.stringify(run), run.state);
1270
+ this.#db.exec('COMMIT');
1271
+ return { claimed: true, run: clone(run) };
1272
+ }
1273
+ catch (error) {
1274
+ try {
1275
+ this.#db.exec('ROLLBACK');
1276
+ }
1277
+ catch { }
1278
+ throw error;
1279
+ }
1280
+ }
1281
+ finish(scope, id, state, steps) {
1282
+ const key = scopeKey(scope);
1283
+ if (!text(id, 128) || !['succeeded', 'failed', 'unknown'].includes(state))
1284
+ fail('assistant-skills: invalid run completion');
1285
+ this.#validateSteps(steps);
1286
+ this.#db.exec('BEGIN IMMEDIATE');
1287
+ try {
1288
+ const current = this.getRun(scope, id);
1289
+ if (!current || current.state !== 'running')
1290
+ fail('assistant-skills: run state conflict');
1291
+ const completed = { ...current, state, steps: clone(steps), updatedAt: Date.now() };
1292
+ if (this.#db.prepare("UPDATE skill_runs SET state=?,run_json=? WHERE id=? AND scope_key=? AND state='running'").run(state, JSON.stringify(completed), id, key).changes !== 1)
1293
+ fail('assistant-skills: run state conflict');
1294
+ this.#attachWatchRun(scope, completed);
1295
+ if (state === 'failed' || state === 'unknown') {
1296
+ for (const deployment of this.#deploymentsForRun(key, id)) {
1297
+ if (deployment.state === 'canary' || deployment.state === 'promoted')
1298
+ this.#putDeployment(key, { ...deployment, state: 'blocked', updatedAt: Date.now() });
1299
+ }
1300
+ }
1301
+ this.#db.exec('COMMIT');
1302
+ return clone(completed);
1303
+ }
1304
+ catch (error) {
1305
+ try {
1306
+ this.#db.exec('ROLLBACK');
1307
+ }
1308
+ catch { }
1309
+ throw error;
1310
+ }
1311
+ }
1312
+ checkpoint(scope, id, steps) {
1313
+ const key = scopeKey(scope);
1314
+ this.#validateSteps(steps);
1315
+ const current = this.getRun(scope, id);
1316
+ if (!current || current.state !== 'running')
1317
+ fail('assistant-skills: run state conflict');
1318
+ const updated = { ...current, steps: clone(steps), updatedAt: Date.now() };
1319
+ if (this.#db.prepare("UPDATE skill_runs SET run_json=? WHERE id=? AND scope_key=? AND state='running'").run(JSON.stringify(updated), id, key).changes !== 1)
1320
+ fail('assistant-skills: run state conflict');
1321
+ return clone(updated);
1322
+ }
1323
+ getRun(scope, id) {
1324
+ const key = scopeKey(scope);
1325
+ if (!text(id, 128))
1326
+ fail('assistant-skills: invalid run reference');
1327
+ const row = this.#db.prepare('SELECT run_json FROM skill_runs WHERE id=? AND scope_key=?').get(id, key);
1328
+ return row === undefined ? undefined : clone(JSON.parse(row.run_json));
1329
+ }
1330
+ claimComparison(scope, identity, maxComparisons) {
1331
+ const key = scopeKey(scope);
1332
+ if (!identity || !text(identity.sessionId) || !text(identity.candidateId, 128) || !/^[a-f0-9]{64}$/u.test(identity.parentDigest) || !text(identity.profileId) || !/^[a-f0-9]{64}$/u.test(identity.profileDigest) || !text(identity.invocationId) || !Number.isSafeInteger(maxComparisons) || maxComparisons < 1 || maxComparisons > 100)
1333
+ fail('assistant-skills: invalid comparison');
1334
+ const id = comparisonId(scope, identity.sessionId, identity.invocationId);
1335
+ this.#db.exec('BEGIN IMMEDIATE');
1336
+ try {
1337
+ const existing = this.#db.prepare('SELECT identity_json,comparison_json FROM skill_comparisons WHERE scope_key=? AND id=?').get(key, id);
1338
+ if (existing) {
1339
+ if (acceptanceDigest(JSON.parse(existing.identity_json)) !== acceptanceDigest(identity))
1340
+ fail('assistant-skills: comparison conflict');
1341
+ this.#db.exec('COMMIT');
1342
+ return { claimed: false, comparison: clone(JSON.parse(existing.comparison_json)) };
1343
+ }
1344
+ const candidate = this.#candidate(key, identity.candidateId);
1345
+ const current = candidate && this.#latest(key, candidate.definition.name);
1346
+ if (!candidate || candidate.state !== 'pending' || candidate.expiresAt <= Date.now() || candidate.parentVersion <= 0 || candidate.parentDigest !== identity.parentDigest || !current || current.retired || current.version !== candidate.parentVersion || acceptanceDigest(current) !== candidate.parentDigest)
1347
+ fail('assistant-skills: candidate unavailable');
1348
+ const used = this.#db.prepare('SELECT count(*) AS count FROM skill_comparisons WHERE scope_key=? AND profile_id=?').get(key, identity.profileId).count;
1349
+ if (used >= maxComparisons)
1350
+ fail('assistant-skills: comparison budget exhausted');
1351
+ const now = Date.now();
1352
+ const comparison = { id, ...clone(identity), state: 'running', result: null, createdAt: now, updatedAt: now };
1353
+ this.#db.prepare('INSERT INTO skill_comparisons VALUES(?,?,?,?,?,?)').run(key, id, identity.profileId, JSON.stringify(identity), JSON.stringify(comparison), 'running');
1354
+ this.#db.exec('COMMIT');
1355
+ return { claimed: true, comparison: clone(comparison) };
1356
+ }
1357
+ catch (error) {
1358
+ try {
1359
+ this.#db.exec('ROLLBACK');
1360
+ }
1361
+ catch { }
1362
+ throw error;
1363
+ }
1364
+ }
1365
+ getComparison(scope, id) { const key = scopeKey(scope); if (!text(id, 128))
1366
+ fail('assistant-skills: invalid comparison reference'); const row = this.#db.prepare('SELECT comparison_json FROM skill_comparisons WHERE scope_key=? AND id=?').get(key, id); return row ? clone(JSON.parse(row.comparison_json)) : undefined; }
1367
+ finishComparison(scope, id, state, result) { const key = scopeKey(scope); if (!text(id, 128) || !['complete', 'unknown'].includes(state) || !json(result))
1368
+ fail('assistant-skills: invalid comparison'); const current = this.getComparison(scope, id); if (!current || current.state !== 'running')
1369
+ fail('assistant-skills: comparison state conflict'); const saved = { ...current, state, result: clone(result), updatedAt: Date.now() }; if (this.#db.prepare("UPDATE skill_comparisons SET state=?,comparison_json=? WHERE scope_key=? AND id=? AND state='running'").run(state, JSON.stringify(saved), key, id).changes !== 1)
1370
+ fail('assistant-skills: comparison state conflict'); return clone(saved); }
1371
+ #latest(key, skillName) {
1372
+ const row = this.#db.prepare('SELECT definition_json FROM skill_definitions WHERE scope_key=? AND name=? ORDER BY version DESC LIMIT 1').get(key, skillName);
1373
+ return row === undefined ? undefined : JSON.parse(row.definition_json);
1374
+ }
1375
+ #newDefinition(definition, savedVersion, parentVersion, restoredFromVersion) {
1376
+ const now = Date.now();
1377
+ const clean = { protocol: definition.protocol, name: definition.name, description: definition.description, source: definition.source, inputs: definition.inputs, steps: definition.steps,
1378
+ preconditions: definition.preconditions, compensation: definition.compensation,
1379
+ ...(definition.fileObservations === undefined ? {} : { fileObservations: definition.fileObservations }),
1380
+ ...(definition.runExpansions === undefined ? {} : { runExpansions: definition.runExpansions }) };
1381
+ return { ...clone(clean), version: savedVersion, parentVersion, retired: false, createdAt: now, updatedAt: now, ...(restoredFromVersion === undefined ? {} : { restoredFromVersion }) };
1382
+ }
1383
+ #insertDefinition(key, saved) {
1384
+ this.#db.prepare('INSERT INTO skill_definitions VALUES(?,?,?,?,?,?,?)').run(key, saved.name, saved.version, 0, JSON.stringify(saved), saved.createdAt, saved.updatedAt);
1385
+ }
1386
+ #candidate(key, id) {
1387
+ const row = this.#db.prepare('SELECT candidate_json FROM skill_candidates WHERE scope_key=? AND id=?').get(key, id);
1388
+ return row === undefined ? undefined : JSON.parse(row.candidate_json);
1389
+ }
1390
+ #candidateFailureProvenance(scope, definition, expectedVersion, parentDigest, parent, value) {
1391
+ const provenance = validateFailureCaptureProvenance(value);
1392
+ const parentTools = [...new Set(parent?.steps.map(step => step.toolName) ?? [])].sort();
1393
+ const candidateTools = [...new Set(definition.steps.map(step => step.toolName))].sort();
1394
+ if (!parent || expectedVersion < 1 || parentDigest === null || acceptanceDigest(parent) !== parentDigest || acceptanceDigest(provenance.trigger.scope) !== acceptanceDigest(scope)
1395
+ || provenance.parent.name !== definition.name || provenance.parent.version !== expectedVersion || provenance.parent.digest !== parentDigest
1396
+ || provenance.rollbackTarget.name !== definition.name || provenance.rollbackTarget.version !== expectedVersion || provenance.rollbackTarget.digest !== parentDigest
1397
+ || provenance.candidate.name !== definition.name || provenance.candidate.definitionDigest !== acceptanceDigest(definition)
1398
+ || provenance.repair.sourceDigest !== acceptanceDigest(definition.source) || provenance.repair.acceptanceDigest !== acceptanceDigest(definition.source.acceptance)
1399
+ || acceptanceDigest(provenance.repair.goal) !== acceptanceDigest(definition.source.goal)
1400
+ || acceptanceDigest(provenance.permissionDelta.parent) !== acceptanceDigest(parentTools)
1401
+ || acceptanceDigest(provenance.permissionDelta.candidate) !== acceptanceDigest(candidateTools))
1402
+ fail('assistant-skills: invalid failure candidate provenance');
1403
+ return clone(provenance);
1404
+ }
1405
+ #assertCandidateFailureProvenance(scope, candidate) {
1406
+ if (candidate.failureProvenance === undefined)
1407
+ return undefined;
1408
+ const parent = this.#latest(scopeKey(scope), candidate.definition.name);
1409
+ return this.#candidateFailureProvenance(scope, candidate.definition, candidate.parentVersion, candidate.parentDigest, parent?.version === candidate.parentVersion ? parent : this.get(scope, candidate.definition.name, candidate.parentVersion), candidate.failureProvenance);
1410
+ }
1411
+ #activatedFailureCandidate(key, skillName, activatedVersion) {
1412
+ const rows = this.#db.prepare('SELECT candidate_json FROM skill_candidates WHERE scope_key=?').all(key);
1413
+ const matches = rows.map(row => JSON.parse(row.candidate_json)).filter(candidate => candidate.state === 'activated' && candidate.activatedVersion === activatedVersion
1414
+ && candidate.definition.name === skillName && candidate.failureProvenance !== undefined);
1415
+ if (matches.length > 1)
1416
+ fail('assistant-skills: failure candidate audit conflict');
1417
+ return matches[0];
1418
+ }
1419
+ #capture(key, id) { const row = this.#db.prepare('SELECT capture_json FROM skill_captures WHERE scope_key=? AND id=?').get(key, id); return row ? JSON.parse(row.capture_json) : undefined; }
1420
+ #repairContinuation(key, id) {
1421
+ const row = this.#db.prepare('SELECT continuation_json FROM skill_repair_continuations WHERE scope_key=? AND id=?').get(key, id);
1422
+ return row === undefined ? undefined : JSON.parse(row.continuation_json);
1423
+ }
1424
+ #watch(key, id) { const row = this.#db.prepare('SELECT watch_json FROM skill_watches WHERE scope_key=? AND id=?').get(key, id); return row ? JSON.parse(row.watch_json) : undefined; }
1425
+ #watches(key, state) { return this.#db.prepare('SELECT watch_json FROM skill_watches WHERE scope_key=? AND state=?').all(key, state).map(row => JSON.parse(row.watch_json)); }
1426
+ #putWatch(key, watch) { this.#db.prepare('INSERT INTO skill_watches(scope_key,id,watch_json,state) VALUES(?,?,?,?) ON CONFLICT(scope_key,id) DO UPDATE SET watch_json=excluded.watch_json,state=excluded.state').run(key, watch.id, JSON.stringify(watch), watch.state); }
1427
+ #deployment(key, id) { const row = this.#db.prepare('SELECT deployment_json FROM skill_deployments WHERE scope_key=? AND id=?').get(key, id); return row ? JSON.parse(row.deployment_json) : undefined; }
1428
+ #deploymentForVersion(key, skillName, wantedVersion) {
1429
+ const rows = this.#db.prepare('SELECT deployment_json FROM skill_deployments WHERE scope_key=?').all(key);
1430
+ return rows.map(row => JSON.parse(row.deployment_json)).find(value => value.skillName === skillName && value.version === wantedVersion);
1431
+ }
1432
+ #deploymentForWatch(key, id) {
1433
+ const rows = this.#db.prepare('SELECT deployment_json FROM skill_deployments WHERE scope_key=?').all(key);
1434
+ return rows.map(row => JSON.parse(row.deployment_json)).find(value => value.watchId === id);
1435
+ }
1436
+ #deploymentsForRun(key, run) {
1437
+ const rows = this.#db.prepare('SELECT deployment_json FROM skill_deployments WHERE scope_key=?').all(key);
1438
+ return rows.map(row => JSON.parse(row.deployment_json)).filter(value => value.runIds.includes(run));
1439
+ }
1440
+ #putDeployment(key, deployment) { this.#db.prepare('INSERT INTO skill_deployments(scope_key,id,deployment_json,state) VALUES(?,?,?,?) ON CONFLICT(scope_key,id) DO UPDATE SET deployment_json=excluded.deployment_json,state=excluded.state').run(key, deployment.id, JSON.stringify(deployment), deployment.state); }
1441
+ #comparison(key, id) { const row = this.#db.prepare('SELECT comparison_json FROM skill_comparisons WHERE scope_key=? AND id=?').get(key, id); return row ? JSON.parse(row.comparison_json) : undefined; }
1442
+ #reconcileDeployment(key, deployment, allowPromotion) {
1443
+ if (deployment.state !== 'canary' && deployment.state !== 'promoted' && deployment.state !== 'blocked')
1444
+ return deployment;
1445
+ let state;
1446
+ const watch = this.#watch(key, deployment.watchId);
1447
+ const active = this.#latest(key, deployment.skillName);
1448
+ if (watch?.state === 'rolled-back' && watch.skillName === deployment.skillName && watch.version === deployment.version && watch.definitionDigest === deployment.definitionDigest)
1449
+ state = 'rolled-back';
1450
+ else if (deployment.state === 'blocked')
1451
+ return deployment;
1452
+ else if (deployment.expiresAt <= Date.now())
1453
+ state = 'expired';
1454
+ else if (!active || active.retired || active.version !== deployment.version || acceptanceDigest(active) !== deployment.definitionDigest)
1455
+ state = 'superseded';
1456
+ else if (!watch || watch.skillName !== deployment.skillName || watch.version !== deployment.version || watch.definitionDigest !== deployment.definitionDigest || watch.maxRuns !== deployment.maxRuns || watch.failureThreshold !== 1)
1457
+ state = 'superseded';
1458
+ else if (!exactCanonicalObservationState(watch, deployment))
1459
+ state = 'blocked';
1460
+ else if (watch.state === 'expired')
1461
+ state = 'expired';
1462
+ else if (watch.state === 'revoked')
1463
+ state = 'revoked';
1464
+ else if (watch.state === 'superseded')
1465
+ state = 'superseded';
1466
+ else if (deployment.runIds.some(id => { const run = this.#run(key, id); return !run || run.state === 'failed' || run.state === 'unknown'; }))
1467
+ state = 'blocked';
1468
+ else if (watch.canonicalRevisions?.some(value => value.disposition === 'retract' && deployment.runIds.includes(value.runId))
1469
+ || watch.observations.some(value => value.objectiveStatus === 'not-achieved' && value.taskFamilyDigest === acceptanceDigest(deployment.taskFamily)
1470
+ && value.canonical !== undefined && watch.canonicalRevisions?.some(revision => revision.runId === value.runId && revision.disposition === 'upsert'
1471
+ && revision.subjectRef === value.canonical.subjectRef && revision.version === value.canonical.version && revision.digest === value.canonical.digest
1472
+ && revision.scopeWatermark === value.canonical.scopeWatermark)))
1473
+ state = 'blocked';
1474
+ else if (allowPromotion && deployment.state === 'canary' && new Set(watch.observations.filter(value => value.objectiveStatus === 'achieved' && value.taskFamilyDigest === acceptanceDigest(deployment.taskFamily)
1475
+ && value.validUntil > Date.now() && deployment.runIds.includes(value.runId) && value.canonical !== undefined
1476
+ && watch.canonicalRevisions?.some(revision => revision.runId === value.runId && revision.disposition === 'upsert'
1477
+ && revision.subjectRef === value.canonical.subjectRef && revision.version === value.canonical.version && revision.digest === value.canonical.digest
1478
+ && revision.scopeWatermark === value.canonical.scopeWatermark)).map(value => value.runId)).size >= deployment.canaryRuns)
1479
+ state = 'promoted';
1480
+ if (!state || state === deployment.state)
1481
+ return deployment;
1482
+ const now = Date.now();
1483
+ const saved = { ...deployment, state, updatedAt: now, ...(state === 'promoted' && deployment.promotedAt === undefined ? { promotedAt: now } : {}) };
1484
+ this.#putDeployment(key, saved);
1485
+ return saved;
1486
+ }
1487
+ #putCandidate(key, candidate) {
1488
+ this.#db.prepare('INSERT INTO skill_candidates(scope_key,id,candidate_json) VALUES(?,?,?) ON CONFLICT(scope_key,id) DO UPDATE SET candidate_json=excluded.candidate_json').run(key, candidate.id, JSON.stringify(candidate));
1489
+ }
1490
+ #putCapture(key, capture) { this.#db.prepare('INSERT INTO skill_captures(scope_key,id,capture_json,state) VALUES(?,?,?,?) ON CONFLICT(scope_key,id) DO UPDATE SET capture_json=excluded.capture_json,state=excluded.state').run(key, capture.id, JSON.stringify(capture), capture.state); }
1491
+ #run(key, id) {
1492
+ const row = this.#db.prepare('SELECT run_json FROM skill_runs WHERE scope_key=? AND id=?').get(key, id);
1493
+ return row === undefined ? undefined : JSON.parse(row.run_json);
1494
+ }
1495
+ #validateClaim(_scope, input) {
1496
+ if (!input || !text(input.invocationId, 256) || !text(input.goalId, 256) || !text(input.sessionId, 512) || !name(input.skillName) || !version(input.version) || !input.inputs || typeof input.inputs !== 'object' || Array.isArray(input.inputs) || !json(input.inputs)
1497
+ || input.goalExecutionRunId !== undefined && !text(input.goalExecutionRunId, 256) || input.goalDefinitionDigest !== undefined && !/^[a-f0-9]{64}$/u.test(input.goalDefinitionDigest) || input.nativeGoalId !== undefined && !text(input.nativeGoalId, 256) || input.candidateId !== undefined && !text(input.candidateId, 128)
1498
+ || input.candidateId !== undefined && input.goalExecutionRunId === undefined)
1499
+ fail('assistant-skills: invalid invocation');
1500
+ if (input.candidateId !== undefined)
1501
+ return;
1502
+ }
1503
+ #validateTrialClaim(key, input) {
1504
+ const candidate = this.#candidate(key, input.candidateId);
1505
+ if (!candidate || candidate.state !== 'pending' || candidate.expiresAt <= Date.now())
1506
+ fail('assistant-skills: candidate unavailable');
1507
+ const current = this.#latest(key, candidate.definition.name);
1508
+ if (current?.retired || (current?.version ?? 0) !== candidate.parentVersion || (current ? acceptanceDigest(current) : null) !== candidate.parentDigest
1509
+ || input.skillName !== candidate.definition.name || input.version !== candidate.parentVersion + 1)
1510
+ fail('assistant-skills: candidate conflict');
1511
+ }
1512
+ #validateSteps(steps) {
1513
+ if (!Array.isArray(steps) || steps.length > 32 || steps.some(step => !step || !text(step.id, 256) || !['succeeded', 'failed', 'unknown'].includes(step.state) || step.detail !== undefined && !text(step.detail, 4096)) || new Set(steps.map(step => step.id)).size !== steps.length)
1514
+ fail('assistant-skills: invalid run completion');
1515
+ }
1516
+ }
1517
+ //# sourceMappingURL=store.js.map