@dsh-enhanced/assistant-skills 0.1.31 → 0.1.34

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 (66) hide show
  1. package/README.md +56 -9
  2. package/lib/definition.d.ts +53 -2
  3. package/lib/definition.d.ts.map +1 -1
  4. package/lib/definition.js +96 -0
  5. package/lib/definition.js.map +1 -1
  6. package/lib/delegated-arm.d.ts +47 -0
  7. package/lib/delegated-arm.d.ts.map +1 -0
  8. package/lib/delegated-arm.js +26 -0
  9. package/lib/delegated-arm.js.map +1 -0
  10. package/lib/external-holdout.js +1 -1
  11. package/lib/external-holdout.js.map +1 -1
  12. package/lib/holdout-authority.d.ts +1 -122
  13. package/lib/holdout-authority.d.ts.map +1 -1
  14. package/lib/holdout-authority.js +5 -262
  15. package/lib/holdout-authority.js.map +1 -1
  16. package/lib/holdout-cli.d.ts.map +1 -1
  17. package/lib/holdout-cli.js +2 -0
  18. package/lib/holdout-cli.js.map +1 -1
  19. package/lib/holdout-qualification.d.ts +11 -3
  20. package/lib/holdout-qualification.d.ts.map +1 -1
  21. package/lib/holdout-qualification.js +19 -9
  22. package/lib/holdout-qualification.js.map +1 -1
  23. package/lib/index.d.ts +10 -1
  24. package/lib/index.d.ts.map +1 -1
  25. package/lib/index.js +4 -0
  26. package/lib/index.js.map +1 -1
  27. package/lib/prospective-holdout.d.ts +1 -34
  28. package/lib/prospective-holdout.d.ts.map +1 -1
  29. package/lib/prospective-holdout.js +4 -253
  30. package/lib/prospective-holdout.js.map +1 -1
  31. package/lib/repair-agent.d.ts +11 -2
  32. package/lib/repair-agent.d.ts.map +1 -1
  33. package/lib/repair-agent.js +178 -17
  34. package/lib/repair-agent.js.map +1 -1
  35. package/lib/repair-process.d.ts +20 -0
  36. package/lib/repair-process.d.ts.map +1 -0
  37. package/lib/repair-process.js +92 -0
  38. package/lib/repair-process.js.map +1 -0
  39. package/lib/repair-profile.d.ts +6 -2
  40. package/lib/repair-profile.d.ts.map +1 -1
  41. package/lib/repair-profile.js +10 -4
  42. package/lib/repair-profile.js.map +1 -1
  43. package/lib/repair-runtime.d.ts +2 -0
  44. package/lib/repair-runtime.d.ts.map +1 -1
  45. package/lib/repair-runtime.js +7 -1
  46. package/lib/repair-runtime.js.map +1 -1
  47. package/lib/repair-workspace-path.d.ts +17 -0
  48. package/lib/repair-workspace-path.d.ts.map +1 -0
  49. package/lib/repair-workspace-path.js +84 -0
  50. package/lib/repair-workspace-path.js.map +1 -0
  51. package/lib/replay.d.ts.map +1 -1
  52. package/lib/replay.js +17 -2
  53. package/lib/replay.js.map +1 -1
  54. package/lib/service.d.ts +271 -2
  55. package/lib/service.d.ts.map +1 -1
  56. package/lib/service.js +473 -35
  57. package/lib/service.js.map +1 -1
  58. package/lib/store.d.ts +47 -0
  59. package/lib/store.d.ts.map +1 -1
  60. package/lib/store.js +262 -6
  61. package/lib/store.js.map +1 -1
  62. package/lib/version.d.ts +1 -1
  63. package/lib/version.js +1 -1
  64. package/lib/watch-proof.js +2 -2
  65. package/lib/watch-proof.js.map +1 -1
  66. package/package.json +8 -8
package/lib/service.js CHANGED
@@ -5,7 +5,7 @@ import { acceptanceDigest } from '@dsh-enhanced/task-acceptance-contract';
5
5
  import Schema from '@deepseek-ai/schemastery';
6
6
  import { homedir } from 'node:os';
7
7
  import { join } from 'node:path';
8
- import { createDefinition, fileObservationSteps, instantiate } from './definition.js';
8
+ import { compensationDirective, createDefinition, evaluatePreconditions, fileObservationSteps, instantiate } from './definition.js';
9
9
  import { captureFailureCandidateProvenance, captureRunExpansions } from './capture-expansion.js';
10
10
  import { validateComparisonProfiles, SkillComparator } from './comparison.js';
11
11
  import { watchBindingCurrent, watchObservation, watchObservationResult, watchObservationRevision } from './watch-proof.js';
@@ -18,6 +18,7 @@ import { RepairContinuationRuntime } from './repair-runtime.js';
18
18
  import { OwnerRepairAgentRuntime } from './repair-agent.js';
19
19
  import { enqueueRepairFeedback } from './repair-feedback.js';
20
20
  import { SkillStore } from './store.js';
21
+ import { awaitBenchmarkSignal, benchmarkArmDefinition, sameBenchmarkArm } from './delegated-arm.js';
21
22
  export const Config = Schema.object({
22
23
  databasePath: Schema.string().default(join(homedir(), '.dsh', 'assistant-skills.sqlite')),
23
24
  allowedTools: Schema.array(Schema.string()).default(['read', 'write', 'edit']),
@@ -53,6 +54,14 @@ function exactArguments(value, allowed) {
53
54
  return undefined;
54
55
  return args;
55
56
  }
57
+ function repairModelRoute(value) {
58
+ if (!value || typeof value !== 'object' || Array.isArray(value))
59
+ return false;
60
+ const route = value;
61
+ return typeof route.provider === 'string' && route.provider.length > 0 && route.provider.length <= 256 && !route.provider.includes('\0')
62
+ && typeof route.model === 'string' && route.model.length > 0 && route.model.length <= 256 && !route.model.includes('\0')
63
+ && (route.reasoningEffort === undefined || typeof route.reasoningEffort === 'string' && route.reasoningEffort.length > 0 && route.reasoningEffort.length <= 256 && !route.reasoningEffort.includes('\0'));
64
+ }
56
65
  function failureLocator(value) {
57
66
  const locator = plainArguments(value);
58
67
  return locator !== undefined && Object.keys(locator).length === 2 && Object.hasOwn(locator, 'sessionId') && Object.hasOwn(locator, 'goalId')
@@ -91,6 +100,37 @@ function failureWindow(input) {
91
100
  .sort((left, right) => compare(left.sessionId, right.sessionId) || compare(left.goalId, right.goalId));
92
101
  return { failures: Object.freeze(canonical.map(locator => Object.freeze(locator))), minimumOccurrences: minimumOccurrences };
93
102
  }
103
+ /**
104
+ * Bounded window of model-suggested owner-root success locators for one
105
+ * autonomous growth deposit. The locator shape ({sessionId,goalId}) is
106
+ * identical to a failure locator; only its meaning differs. Discovery only —
107
+ * every locator still has to pass an independent owner verified source read.
108
+ */
109
+ function successWindow(input) {
110
+ const raw = input.successLocators;
111
+ const minimumOccurrences = input.minimumOccurrences ?? 1;
112
+ if (!Array.isArray(raw) || Object.getPrototypeOf(raw) !== Array.prototype || raw.length < 1 || raw.length > 32
113
+ || Object.getOwnPropertySymbols(raw).length !== 0 || Reflect.ownKeys(raw).length !== raw.length + 1
114
+ || !Number.isSafeInteger(minimumOccurrences) || minimumOccurrences < 1 || minimumOccurrences > 32
115
+ || minimumOccurrences > raw.length) {
116
+ throw new Error('assistant-skills: invalid bounded success locator window');
117
+ }
118
+ const descriptors = Object.getOwnPropertyDescriptors(raw), successes = [];
119
+ for (let index = 0; index < raw.length; index++) {
120
+ const descriptor = descriptors[String(index)];
121
+ if (descriptor === undefined || !descriptor.enumerable || !('value' in descriptor) || !failureLocator(descriptor.value)) {
122
+ throw new Error('assistant-skills: invalid bounded success locator window');
123
+ }
124
+ successes.push(descriptor.value);
125
+ }
126
+ const identities = successes.map(locator => acceptanceDigest(locator));
127
+ if (new Set(identities).size !== identities.length)
128
+ throw new Error('assistant-skills: duplicate success locator');
129
+ const compare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
130
+ const canonical = successes.map(locator => ({ sessionId: locator.sessionId, goalId: locator.goalId }))
131
+ .sort((left, right) => compare(left.sessionId, right.sessionId) || compare(left.goalId, right.goalId));
132
+ return { successes: Object.freeze(canonical.map(locator => Object.freeze(locator))), minimumOccurrences: minimumOccurrences };
133
+ }
94
134
  function publicDefinition(definition) {
95
135
  const source = { digest: acceptanceDigest(definition.source), goalDefinitionDigest: definition.source.goal.definition.digest, stepCount: definition.source.steps.length,
96
136
  failedObservationCount: definition.source.failedObservations?.length ?? 0, segmentCount: definition.source.segments?.length ?? 0 };
@@ -188,6 +228,17 @@ function trialProofSteps(steps, candidateId, trialRunId, goalId, invocationId, i
188
228
  /** Fixed tool compositions run in the original native Goal; no new AgentLoop or scheduler. */
189
229
  export class AssistantSkillsService extends Service {
190
230
  static Config = Config;
231
+ static { Object.defineProperty(this, 'name', { value: 'dsh-enhanced-assistant-skills' }); }
232
+ static #benchmarkCapabilities = new WeakMap();
233
+ static #benchmarkRecipients = new WeakMap();
234
+ #benchmarkIdentity = Object.freeze({});
235
+ /** A Host identity only; it carries no permission and survives fresh Cordis trace proxies. */
236
+ benchmarkRecipientIdentity = () => this.#benchmarkIdentity;
237
+ #issuedBenchmarkArms = new Set();
238
+ #issuedBenchmarkCells = new Set();
239
+ #mountedBenchmarkArms = new Map();
240
+ #benchmarkRuns = new Set();
241
+ #benchmarkGeneration = 0;
191
242
  #store;
192
243
  #allowed;
193
244
  #duration;
@@ -226,18 +277,37 @@ export class AssistantSkillsService extends Service {
226
277
  this.#externalHoldouts = validateExternalHoldoutProfiles(config.externalHoldouts ?? []);
227
278
  this.#repairProfiles = validateRepairProfiles(config.repairProfiles ?? [], this.#externalHoldouts);
228
279
  this.#store = new SkillStore(config.databasePath ?? join(homedir(), '.dsh', 'assistant-skills.sqlite'));
229
- ctx.effect(() => async () => { this.#active = false; this.#lifecycle.abort(); await Promise.all([...this.#repairStops].map(stop => stop())); await Promise.allSettled(this.#comparing); await Promise.allSettled(this.#captureTasks); await Promise.allSettled([...this.#comparators.values()].map(async (value) => (await value).close())); this.#store.close(); }, 'assistant-skills.store');
280
+ AssistantSkillsService.#benchmarkRecipients.set(this.#benchmarkIdentity, this);
281
+ ctx.effect(() => async () => { this.#active = false; this.#lifecycle.abort(); this.#mountedBenchmarkArms.clear(); await Promise.allSettled(this.#benchmarkRuns); await Promise.all([...this.#repairStops].map(stop => stop())); await Promise.allSettled(this.#comparing); await Promise.allSettled(this.#captureTasks); await Promise.allSettled([...this.#comparators.values()].map(async (value) => (await value).close())); this.#store.close(); }, 'assistant-skills.store');
282
+ ctx.inject(['assistantGoals', 'assistantPolicy', 'assistantDelivery'], runtime => {
283
+ this.#benchmarkGeneration++;
284
+ runtime.effect(() => () => {
285
+ this.#benchmarkGeneration++;
286
+ for (const controller of this.#issuedBenchmarkArms)
287
+ controller.abort();
288
+ this.#issuedBenchmarkArms.clear();
289
+ for (const arm of this.#mountedBenchmarkArms.values())
290
+ arm.revoke();
291
+ this.#mountedBenchmarkArms.clear();
292
+ }, 'assistant-skills.benchmark-service-generation');
293
+ });
230
294
  if (this.#repairProfiles.length > 0)
231
295
  this.#installRepairRuntime(ctx);
232
296
  ctx.inject(['tools', 'agents', 'assistantGoals', 'assistantPolicy', 'assistantDelivery'], runtime => {
233
297
  runtime.tools.register(defineTool({ name: 'skill_save', description: 'Save the exact successful tool trace of this owner session’s independently achieved Goal as a private versioned skill. Requires the current human request. Historical acceptance is provenance, never permission or acceptance for a future run.',
234
298
  parameters: { goal_id: { type: 'string', required: true, description: businessGoalId }, name: { type: 'string', required: true }, description: { type: 'string', required: true }, bindings_json: { type: 'string', description: 'JSON array of {name,stepId,path}; path is a scalar argument JSON pointer. Empty array keeps the original arguments.' }, expected_version: { type: 'integer' } }, output,
235
299
  execute: async (args, exec) => ({ context: JSON.stringify(this.save(exec.agent, args.goal_id, { name: args.name, description: args.description, bindings: parse(args.bindings_json ?? '[]', true) }, args.expected_version ?? 0)) }) }));
236
- runtime.tools.register(defineTool({ name: 'skill_run', description: 'Replay a saved skill’s fixed tool steps in a fresh native Goal. If the current owner Goal is active but has no admitted native round, this returns awaiting-native-round with no steps or durable run, ends the turn, and the next native round must repeat the same invocation_id. Every nested call retains native permissions, approvals, cancellation and budgets. Tool success still requires fresh independent Goal acceptance.',
300
+ const delegatedRun = defineTool({ name: 'skill_run', description: 'Replay a saved skill’s fixed tool steps in a fresh native Goal. If the current owner Goal is active but has no admitted native round, this returns awaiting-native-round with no steps or durable run, ends the turn, and the next native round must repeat the same invocation_id. Every nested call retains native permissions, approvals, cancellation and budgets. Tool success still requires fresh independent Goal acceptance.',
237
301
  parameters: { goal_id: { type: 'string', required: true, description: businessGoalId }, name: { type: 'string', required: true }, version: { type: 'integer', required: true }, inputs_json: { type: 'string', description: 'JSON object with only declared typed input parameters.' }, invocation_id: { type: 'string', required: true } }, output,
238
- execute: async (args, exec) => ({ context: JSON.stringify(await this.run(exec, args.goal_id, args.name, args.version, parse(args.inputs_json ?? '{}'), args.invocation_id)) }) }));
239
- runtime.tools.register(defineTool({ name: 'skill_status', description: 'Read this owner’s active saved skill definitions, typed inputs and source acceptance, or inspect a specific durable invocation. Success means steps executed, not Goal achievement.', parameters: { run_id: { type: 'string' } }, output,
240
- execute: async (args, exec) => ({ context: JSON.stringify(this.inspect(exec.agent, args.run_id)) }) }));
302
+ execute: async (args, exec) => ({ context: JSON.stringify(await this.run(exec, args.goal_id, args.name, args.version, parse(args.inputs_json ?? '{}'), args.invocation_id)) }) });
303
+ runtime.tools.register(delegatedRun);
304
+ if (runtime.assistantPolicy.delegatedSkillToolPreauthorizationVersion?.() === 1)
305
+ runtime.assistantPolicy.registerPreauthorizedTool(runtime, delegatedRun, execution => this.#preauthorizeBenchmarkTool(execution));
306
+ const delegatedStatus = defineTool({ name: 'skill_status', description: 'Read this owner’s active saved skill definitions, typed inputs and source acceptance, or inspect a specific durable invocation. Success means steps executed, not Goal achievement.', parameters: { run_id: { type: 'string' } }, output,
307
+ execute: async (args, exec) => ({ context: JSON.stringify(this.inspect(exec.agent, args.run_id)) }) });
308
+ runtime.tools.register(delegatedStatus);
309
+ if (runtime.assistantPolicy.delegatedSkillToolPreauthorizationVersion?.() === 1)
310
+ runtime.assistantPolicy.registerPreauthorizedTool(runtime, delegatedStatus, execution => this.#preauthorizeBenchmarkTool(execution));
241
311
  runtime.tools.register(defineTool({ name: 'skill_retire', description: 'Retire the current saved skill version following the current authenticated owner request. Pending steps recheck retirement; past effects remain and require explicit repair.', parameters: { name: { type: 'string', required: true }, expected_version: { type: 'integer', required: true } }, output,
242
312
  execute: async (args, exec) => ({ context: JSON.stringify(this.retire(exec.agent, args.name, args.expected_version)) }) }));
243
313
  });
@@ -515,9 +585,39 @@ export class AssistantSkillsService extends Service {
515
585
  #capturedDefinition(source, options, scope) {
516
586
  return createDefinition(source, options, this.#allowed, captureRunExpansions(source, scope, this.#store));
517
587
  }
588
+ /** Only a live delegated, isolation-only workflow may cross Isolation's tool gate. */
589
+ #preauthorizeBenchmarkTool(execution) {
590
+ try {
591
+ if (execution.signal.aborted || !['skill_run', 'skill_status'].includes(execution.name))
592
+ return false;
593
+ const scope = this.#scope(execution.agent, execution.name === 'skill_run' ? 'run' : 'inspect');
594
+ const mounted = this.#mountedBenchmarkArms.get(acceptanceDigest(scope));
595
+ if (!mounted || mounted.skill.steps.some(step => step.toolName !== 'isolation_run'))
596
+ return false;
597
+ mounted.assertCurrent();
598
+ const args = plainArguments(execution.arguments);
599
+ if (!args)
600
+ return false;
601
+ if (execution.name === 'skill_status')
602
+ return Object.keys(args).every(key => key === 'run_id') && (args.run_id === undefined
603
+ || typeof args.run_id === 'string' && this.#store.getRun(scope, args.run_id)?.delegationDigest === mounted.bindingDigest);
604
+ return Object.keys(args).length === 5 && typeof args.goal_id === 'string' && typeof args.inputs_json === 'string' && typeof args.invocation_id === 'string'
605
+ && args.name === mounted.skill.name && args.version === mounted.skill.version;
606
+ }
607
+ catch {
608
+ return false;
609
+ }
610
+ }
518
611
  inspect(agent, runId) {
519
612
  const scope = this.#scope(agent, 'inspect');
520
- return runId ? this.#store.getRun(scope, runId) : this.#store.list(scope);
613
+ if (runId)
614
+ return this.#store.getRun(scope, runId);
615
+ const mounted = this.#mountedBenchmarkArms.get(acceptanceDigest(scope));
616
+ if (mounted) {
617
+ mounted.assertCurrent();
618
+ return [{ ...structuredClone(mounted.skill), delegation: structuredClone(mounted.binding), delegationDigest: mounted.bindingDigest }];
619
+ }
620
+ return this.#store.list(scope);
521
621
  }
522
622
  retire(agent, name, expectedVersion) {
523
623
  const scope = this.#scope(agent, 'retire');
@@ -938,6 +1038,107 @@ export class AssistantSkillsService extends Service {
938
1038
  stageOwnerAuthorizedFailureCandidate = (exec, input, authority) => {
939
1039
  return this.stageFailureCandidate(exec, input, authority);
940
1040
  };
1041
+ /**
1042
+ * Host-only autonomous SUCCESS deposit for the growth driver. Unlike the
1043
+ * repair/failure path it is deliberately NOT anchored to a durable repair
1044
+ * continuation: a successful goal creates none. Instead the caller supplies
1045
+ * a narrow, short-lived OwnerSuccessCandidateAuthority minted from a frozen
1046
+ * configured owner scope, and this method:
1047
+ * 1. validates the authority (live background agent, exact scope, unexpired,
1048
+ * caller assertCurrent lease) and re-anchors the authenticated owner route
1049
+ * through delivery.validateOwnerRoute — never trusting a model-supplied id;
1050
+ * 2. independently re-reads EVERY model-suggested locator through
1051
+ * goals.inspectOwnerVerifiedWorkflowSource (owner-root, succeeded,
1052
+ * quiescent, double-read), counting DISTINCT (sessionId,goalId);
1053
+ * 3. stages one PENDING candidate only when the distinct verified-success
1054
+ * count meets minimumOccurrences. It never writes a current version, so
1055
+ * skill_run cannot reach it; activation stays behind the existing owner
1056
+ * gates. A retired same-name skill or any version conflict fails closed.
1057
+ */
1058
+ async stageOwnerVerifiedSuccessCandidate(exec, rawInput, authority) {
1059
+ // --- authority + exact owner scope (no repair continuation is consulted) ---
1060
+ exec.signal.throwIfAborted();
1061
+ this.#lifecycle.signal.throwIfAborted();
1062
+ if (!this.#active || !exec.agent || this.ctx.get('agents')?.get(exec.agent.id) !== exec.agent
1063
+ || !Number.isSafeInteger(authority.expiresAt) || authority.expiresAt <= Date.now()
1064
+ || exec.agent.session.header.cwd !== authority.scope.workspace
1065
+ || exec.agent.session.header.agentPreset !== authority.scope.preset) {
1066
+ throw new Error('assistant-skills: growth success authority unavailable');
1067
+ }
1068
+ authority.assertCurrent();
1069
+ const scope = { principalId: authority.scope.principalId, principalRecordId: authority.scope.principalRecordId,
1070
+ principalVersion: authority.scope.principalVersion, workspace: authority.scope.workspace, preset: authority.scope.preset };
1071
+ const route = this.#watchRoute(scope, authority.ownerRouteId);
1072
+ if (rawInput === null || typeof rawInput !== 'object' || rawInput.ownerRouteId !== authority.ownerRouteId) {
1073
+ throw new Error('assistant-skills: growth success input does not match authority');
1074
+ }
1075
+ const window = successWindow(rawInput);
1076
+ const goalsApi = this.#goals();
1077
+ if (typeof goalsApi.inspectOwnerVerifiedWorkflowSource !== 'function') {
1078
+ throw new Error('assistant-skills: upgrade assistant-goals to use growth success deposit');
1079
+ }
1080
+ const signal = AbortSignal.any([exec.signal, this.#lifecycle.signal]);
1081
+ // --- independent Host re-read of every model-suggested locator ---
1082
+ const sources = await Promise.all(window.successes.map(async (locator) => {
1083
+ const source = await goalsApi.inspectOwnerVerifiedWorkflowSource({ ownerRouteId: authority.ownerRouteId, principalId: scope.principalId, workspace: scope.workspace, preset: scope.preset,
1084
+ sessionId: locator.sessionId, goalId: locator.goalId }, signal);
1085
+ // The attestation must be for THIS exact owner scope, not a sibling route.
1086
+ if (acceptanceDigest(source.scope) !== acceptanceDigest(scope)) {
1087
+ throw new Error('assistant-skills: verified success source owner mismatch');
1088
+ }
1089
+ return { locator, source };
1090
+ }));
1091
+ // Distinct owner-root successes: different native Session AND business goal.
1092
+ // The embedded source carries no cross-goal signature, so the repeat gate is
1093
+ // a count of independently verified locators, never a model claim.
1094
+ const distinct = new Map();
1095
+ for (const item of sources) {
1096
+ const sessionId = item.source.goal.sessionId;
1097
+ if (sessionId !== item.locator.sessionId || item.source.goal.id !== item.locator.goalId) {
1098
+ throw new Error('assistant-skills: verified success locator mismatch');
1099
+ }
1100
+ distinct.set(`${sessionId}${item.locator.goalId}`, { sessionId, goalId: item.locator.goalId, source: item.source });
1101
+ }
1102
+ if (distinct.size < window.minimumOccurrences) {
1103
+ throw new Error(`assistant-skills: only ${distinct.size} distinct verified successes; ${window.minimumOccurrences} required`);
1104
+ }
1105
+ // --- resolve the current parent; a retired name blocks any new deposit ---
1106
+ const options = { name: rawInput.name, description: rawInput.description, ...(rawInput.bindings === undefined ? {} : { bindings: rawInput.bindings }) };
1107
+ const activeParent = this.#store.get(scope, options.name);
1108
+ const parentVersion = activeParent?.version ?? 0;
1109
+ // Build the definition from ONE representative verified source; provenance
1110
+ // below binds the full distinct-success set so the repeat evidence is durable.
1111
+ const representative = [...distinct.values()].sort((left, right) => left.sessionId < right.sessionId ? -1 : left.sessionId > right.sessionId ? 1
1112
+ : left.goalId < right.goalId ? -1 : left.goalId > right.goalId ? 1 : 0)[0];
1113
+ const definition = this.#capturedDefinition(representative.source, options, scope);
1114
+ // --- re-validate authority/route right before the write ---
1115
+ authority.assertCurrent();
1116
+ this.#watchRoute(scope, authority.ownerRouteId, route);
1117
+ const provenance = Object.freeze({
1118
+ protocol: 'assistant-skills/growth-success-provenance/v1',
1119
+ ownerRouteId: authority.ownerRouteId,
1120
+ distinctSuccessCount: distinct.size,
1121
+ requiredCount: window.minimumOccurrences,
1122
+ successes: [...distinct.values()]
1123
+ .map(value => ({ sessionId: value.sessionId, goalId: value.goalId,
1124
+ sourceDigest: acceptanceDigest(value.source), goalDefinitionDigest: value.source.goal.definition.digest,
1125
+ runId: value.source.runId, acceptanceContractId: value.source.acceptance.contractId }))
1126
+ .sort((left, right) => left.sourceDigest < right.sourceDigest ? -1 : left.sourceDigest > right.sourceDigest ? 1 : 0),
1127
+ });
1128
+ this.#authorize(exec.agent, 'draft', [scope, definition, parentVersion, acceptanceDigest(provenance)]);
1129
+ // stageCandidate enforces retired/version conflict and pending idempotency.
1130
+ // The distinct-success set is not stored in the failure-provenance slot
1131
+ // (that shape is repair-specific); its digest is folded into trigger, which
1132
+ // is part of the durable candidate identity formula, making the repeat
1133
+ // evidence tamper-evident without a schema migration.
1134
+ const provenanceDigest = acceptanceDigest(provenance);
1135
+ const reason = `Host-verified autonomous growth deposit after ${distinct.size} independently owner-verified repeated successes.`;
1136
+ const trigger = `growth-success:${distinct.size}:${provenanceDigest}`;
1137
+ const candidate = this.#store.stageCandidate(scope, definition, {
1138
+ expectedVersion: parentVersion, reason, trigger, expiresAt: Date.now() + this.#candidateTtl,
1139
+ });
1140
+ return this.#preview(scope, candidate);
1141
+ }
941
1142
  #preview(scope, candidate) {
942
1143
  const parent = candidate.parentVersion ? this.#store.get(scope, candidate.definition.name, candidate.parentVersion) : undefined;
943
1144
  return publicCandidate(candidate, parent);
@@ -949,6 +1150,142 @@ export class AssistantSkillsService extends Service {
949
1150
  const candidate = this.#store.getCandidate(scope, id);
950
1151
  return candidate ? this.#preview(scope, candidate) : null;
951
1152
  }
1153
+ /**
1154
+ * Host-only read of the active current skills of a delivery-anchored owner
1155
+ * scope. The caller MUST first establish the scope through
1156
+ * delivery.validateOwnerRoute (exact owner route); this enumeration consumes
1157
+ * no live owner turn and exists for bounded background growth agents. It is
1158
+ * discovery-only and cannot write.
1159
+ */
1160
+ inspectOwnerActiveSkills(scope) {
1161
+ if (!this.#active)
1162
+ throw new Error('assistant-skills: inactive');
1163
+ return this.#store.list(scope).map(publicDefinition);
1164
+ }
1165
+ /** Host-only read of every skill candidate of a delivery-anchored owner scope. */
1166
+ inspectOwnerSkillCandidates(scope) {
1167
+ if (!this.#active)
1168
+ throw new Error('assistant-skills: inactive');
1169
+ return this.#store.listCandidates(scope).map(candidate => this.#preview(scope, candidate));
1170
+ }
1171
+ #benchmarkSource(selection) {
1172
+ if (!this.#active)
1173
+ throw new Error('assistant-skills: benchmark source inactive');
1174
+ this.#watchRoute(selection.scope, selection.ownerRouteId);
1175
+ const policy = this.ctx.get('assistantPolicy', false);
1176
+ if (policy?.evaluate({ ...this.#watchPolicy(selection.scope, 'watch'), action: 'compare' }).effect !== 'allow')
1177
+ throw new Error('assistant-skills: benchmark comparison policy denied');
1178
+ const candidate = selection.candidateId === undefined ? undefined : this.#pending(selection.scope, selection.candidateId);
1179
+ const selected = candidate?.definition ?? this.#store.get(selection.scope, selection.skillName);
1180
+ const version = candidate ? candidate.parentVersion + 1 : selected?.version;
1181
+ if (!selected || selected.name !== selection.skillName || version !== selection.version
1182
+ || acceptanceDigest(selected.source.scope) !== acceptanceDigest(selection.scope))
1183
+ throw new Error('assistant-skills: exact benchmark source required');
1184
+ const definition = benchmarkArmDefinition(selected);
1185
+ const expiresAt = Math.min(definition.source.acceptance.validUntil, candidate?.expiresAt ?? Number.MAX_SAFE_INTEGER);
1186
+ if (!Number.isSafeInteger(expiresAt) || expiresAt <= Date.now())
1187
+ throw new Error('assistant-skills: benchmark source expired');
1188
+ return { definition, definitionDigest: acceptanceDigest(definition), sourceDigest: acceptanceDigest(definition.source), version, expiresAt };
1189
+ }
1190
+ /** Re-read original Goals provenance before freezing an arm. This grants no execution authority. */
1191
+ inspectOwnerBenchmarkArm = async (selection, signal) => {
1192
+ selection = structuredClone(selection);
1193
+ const combined = AbortSignal.any([this.#lifecycle.signal, AbortSignal.timeout(10000), ...(signal ? [signal] : [])]);
1194
+ combined.throwIfAborted();
1195
+ const before = this.#benchmarkSource(selection), goals = this.#goals(), generation = this.#benchmarkGeneration;
1196
+ const route = this.#watchRoute(selection.scope, selection.ownerRouteId);
1197
+ const source = await awaitBenchmarkSignal(goals.inspectOwnerVerifiedWorkflowSource({ ownerRouteId: selection.ownerRouteId,
1198
+ principalId: selection.scope.principalId, workspace: selection.scope.workspace, preset: selection.scope.preset,
1199
+ sessionId: before.definition.source.goal.sessionId, goalId: before.definition.source.goal.id }, combined), combined);
1200
+ combined.throwIfAborted();
1201
+ this.#watchRoute(selection.scope, selection.ownerRouteId, route);
1202
+ if (this.#benchmarkGeneration !== generation || !sameBenchmarkArm(before, this.#benchmarkSource(selection))
1203
+ || acceptanceDigest(source) !== before.sourceDigest)
1204
+ throw new Error('assistant-skills: benchmark provenance changed');
1205
+ return structuredClone(before);
1206
+ };
1207
+ /** Host-only, single-use delegation to one exact live recipient service and frozen cell. */
1208
+ mintBenchmarkArm = async (input, signal) => {
1209
+ const recipient = AssistantSkillsService.#benchmarkRecipients.get(input.recipient.benchmarkRecipientIdentity());
1210
+ if (!recipient)
1211
+ throw new Error('assistant-skills: unrecognized benchmark recipient');
1212
+ const selection = structuredClone(input.selection), scope = structuredClone(input.recipientScope), binding = structuredClone(input.binding);
1213
+ const recipientOwnerRouteId = input.recipientOwnerRouteId;
1214
+ const snapshot = await this.inspectOwnerBenchmarkArm(selection, signal);
1215
+ const route = this.#watchRoute(selection.scope, selection.ownerRouteId);
1216
+ const recipientRoute = recipient.#watchRoute(scope, recipientOwnerRouteId);
1217
+ if (recipient.#benchmarkIdentity === this.#benchmarkIdentity || !recipient.#active || binding.protocol !== 'assistant-skills/delegated-arm/v1'
1218
+ || binding.definitionDigest !== snapshot.definitionDigest || binding.sourceDigest !== snapshot.sourceDigest
1219
+ || binding.recipientScopeDigest !== acceptanceDigest(scope) || binding.skillName !== snapshot.definition.name || binding.version !== snapshot.version
1220
+ || !Number.isSafeInteger(binding.expiresAt) || binding.expiresAt <= Date.now() || binding.expiresAt > snapshot.expiresAt
1221
+ || binding.expiresAt - Date.now() > 300000)
1222
+ throw new Error('assistant-skills: invalid benchmark delegation');
1223
+ if (!/^[a-f0-9]{64}$/u.test(binding.planDigest) || !binding.cellId || !binding.variantId)
1224
+ throw new Error('assistant-skills: invalid benchmark cell');
1225
+ const cellKey = acceptanceDigest([binding.planDigest, binding.cellId]);
1226
+ if (this.#issuedBenchmarkCells.has(cellKey))
1227
+ throw new Error('assistant-skills: benchmark cell already delegated');
1228
+ const authorization = { idempotencyKey: `skill-benchmark-${acceptanceDigest(binding)}` };
1229
+ if (this.ctx.get('assistantPolicy', false)?.authorize({ ...this.#watchPolicy(selection.scope, 'watch'), action: 'compare' }, authorization).effect !== 'allow'
1230
+ || recipient.ctx.get('assistantPolicy', false)?.authorize({ ...recipient.#watchPolicy(scope, 'watch'), action: 'compare' }, authorization).effect !== 'allow')
1231
+ throw new Error('assistant-skills: benchmark comparison authorization denied');
1232
+ const controller = new AbortController();
1233
+ const combined = AbortSignal.any([controller.signal, this.#lifecycle.signal, recipient.#lifecycle.signal, ...(signal ? [signal] : [])]);
1234
+ const assertCurrent = () => {
1235
+ combined.throwIfAborted();
1236
+ if (!recipient.#active || Date.now() >= binding.expiresAt || !sameBenchmarkArm(snapshot, this.#benchmarkSource(selection)))
1237
+ throw new Error('assistant-skills: benchmark delegation unavailable');
1238
+ this.#watchRoute(selection.scope, selection.ownerRouteId, route);
1239
+ recipient.#watchRoute(scope, recipientOwnerRouteId, recipientRoute);
1240
+ if (recipient.ctx.get('assistantPolicy', false)?.evaluate({ ...recipient.#watchPolicy(scope, 'watch'), action: 'compare' }).effect !== 'allow')
1241
+ throw new Error('assistant-skills: recipient comparison policy denied');
1242
+ };
1243
+ assertCurrent();
1244
+ const capability = Object.freeze({ protocol: 'assistant-skills/benchmark-capability/v1' });
1245
+ AssistantSkillsService.#benchmarkCapabilities.set(capability, { recipient: recipient.#benchmarkIdentity, scope, binding, snapshot, controller, signal: combined, assertCurrent,
1246
+ refresh: async (runSignal) => {
1247
+ assertCurrent();
1248
+ const current = await this.inspectOwnerBenchmarkArm(selection, AbortSignal.any([combined, runSignal]));
1249
+ assertCurrent();
1250
+ if (!sameBenchmarkArm(snapshot, current))
1251
+ throw new Error('assistant-skills: benchmark source changed');
1252
+ }, consumed: false });
1253
+ this.#issuedBenchmarkCells.add(cellKey);
1254
+ this.#issuedBenchmarkArms.add(controller);
1255
+ this.ctx.effect(() => {
1256
+ const timer = setTimeout(() => controller.abort(), Math.max(1, binding.expiresAt - Date.now()));
1257
+ timer.unref?.();
1258
+ const stop = () => { clearTimeout(timer); this.#issuedBenchmarkArms.delete(controller); };
1259
+ combined.addEventListener('abort', stop, { once: true });
1260
+ return () => { combined.removeEventListener('abort', stop); stop(); controller.abort(); };
1261
+ }, 'assistant-skills.benchmark-grant');
1262
+ combined.addEventListener('abort', () => this.#issuedBenchmarkArms.delete(controller), { once: true });
1263
+ return capability;
1264
+ };
1265
+ /** No definition is imported into the active/candidate tables; a restart cannot restore the authority. */
1266
+ mountBenchmarkArm = async (capability, expected) => {
1267
+ const grant = AssistantSkillsService.#benchmarkCapabilities.get(capability);
1268
+ if (!grant || grant.consumed || grant.recipient !== this.#benchmarkIdentity || acceptanceDigest(expected) !== acceptanceDigest(grant.binding))
1269
+ throw new Error('assistant-skills: unrecognized benchmark capability');
1270
+ grant.consumed = true;
1271
+ await grant.refresh(this.#lifecycle.signal);
1272
+ grant.assertCurrent();
1273
+ const scopeKey = acceptanceDigest(grant.scope);
1274
+ if (this.#mountedBenchmarkArms.has(scopeKey))
1275
+ throw new Error('assistant-skills: benchmark recipient already mounted');
1276
+ this.#store.reserveDelegatedArm(grant.scope, grant.binding);
1277
+ const bindingDigest = acceptanceDigest(grant.binding), now = Date.now();
1278
+ const skill = { ...structuredClone(grant.snapshot.definition), version: grant.snapshot.version, parentVersion: null, retired: false, createdAt: now, updatedAt: now };
1279
+ const arm = { binding: grant.binding, bindingDigest, skill, scope: grant.scope, signal: grant.signal, revoke: () => grant.controller.abort(), assertCurrent: grant.assertCurrent, refresh: grant.refresh };
1280
+ this.#mountedBenchmarkArms.set(scopeKey, arm);
1281
+ const dispose = this.ctx.effect(() => {
1282
+ const timer = setTimeout(() => grant.controller.abort(), Math.max(1, grant.binding.expiresAt - Date.now()));
1283
+ timer.unref?.();
1284
+ return () => { clearTimeout(timer); grant.controller.abort(); if (this.#mountedBenchmarkArms.get(scopeKey) === arm)
1285
+ this.#mountedBenchmarkArms.delete(scopeKey); };
1286
+ }, 'assistant-skills.benchmark-arm');
1287
+ return { binding: structuredClone(grant.binding), bindingDigest, skill: { name: skill.name, version: skill.version, inputs: structuredClone(skill.inputs) }, dispose };
1288
+ };
952
1289
  #pending(scope, id) {
953
1290
  const candidate = this.#store.getCandidate(scope, id);
954
1291
  if (!candidate || candidate.state !== 'pending' || candidate.expiresAt <= Date.now())
@@ -1034,6 +1371,15 @@ export class AssistantSkillsService extends Service {
1034
1371
  return false;
1035
1372
  }
1036
1373
  };
1374
+ ownsOwnerAuthorizedRepairResume = (input, callback) => {
1375
+ if (!this.ownsOwnerAuthorizedRepair(input, callback))
1376
+ return false;
1377
+ const record = this.#store.getRepairContinuation(input.scope, input.authorizationId);
1378
+ const repair = record?.checkpoint.repair;
1379
+ return !!record && ['repairing', 'repair-achieved', 'capturing', 'candidate-staged', 'comparing', 'watching'].includes(record.state)
1380
+ && repair?.sessionId === input.repair.sessionId && repair.goalId === input.repair.goalId
1381
+ && this.#repairAgents?.ownsResume(input, callback) === true;
1382
+ };
1037
1383
  #repairProfileAt(record, iteration) {
1038
1384
  const entry = record.authorization.profileSequence?.[iteration - 1]
1039
1385
  ?? (iteration === 1 ? { id: record.authorization.profileId, digest: record.authorization.profileDigest } : undefined);
@@ -1044,6 +1390,20 @@ export class AssistantSkillsService extends Service {
1044
1390
  return profile;
1045
1391
  }
1046
1392
  #repairProfile(record) { return this.#repairProfileAt(record, record.iteration); }
1393
+ /**
1394
+ * New authorizations persist this exact route. Legacy records predate the
1395
+ * field, so they may run only when their still-digest-bound profile supplied
1396
+ * an explicit pair; inherited routes must never be selected again on resume.
1397
+ */
1398
+ #repairModelSelection(record) {
1399
+ const frozen = record.authorization.modelSelection;
1400
+ if (frozen)
1401
+ return frozen;
1402
+ const primary = this.#repairProfileAt(record, 1);
1403
+ if (primary.provider !== undefined && primary.model !== undefined)
1404
+ return { provider: primary.provider, model: primary.model };
1405
+ throw new Error('assistant-skills: legacy inherited repair model selection unavailable');
1406
+ }
1047
1407
  #repairSource(record) {
1048
1408
  if (record.iteration === 1)
1049
1409
  return record.authorization.source;
@@ -1144,6 +1504,14 @@ export class AssistantSkillsService extends Service {
1144
1504
  if (!this.#repairRuntime || !profile || !holdout?.profile.canaryAdmissionTemplate || !Number.isSafeInteger(input.expiresAt)
1145
1505
  || input.expiresAt <= Date.now() || input.expiresAt > Math.min(holdout.profile.execution.expiresAt, Date.now() + 604800000))
1146
1506
  throw new Error('assistant-skills: finite current repair profile required');
1507
+ const selected = profile.provider === undefined
1508
+ ? this.ctx.get('assistantDelivery', false)?.inspectOwnerModelSelection({ authorityId: input.ownerRouteId, principalId: scope.principalId,
1509
+ workspace: scope.workspace, agentPreset: scope.preset, sourceAgent: agent })
1510
+ : { provider: profile.provider, model: profile.model };
1511
+ if (!repairModelRoute(selected))
1512
+ throw new Error('assistant-skills: exact current repair model selection required');
1513
+ const modelSelection = { provider: selected.provider, model: selected.model,
1514
+ ...(selected.reasoningEffort === undefined ? {} : { reasoningEffort: selected.reasoningEffort }) };
1147
1515
  const parent = this.#store.get(scope, profile.skillName);
1148
1516
  const sessionId = input.sessionId ?? String(agent.session.id);
1149
1517
  const snapshot = this.#goals().inspectOwnerGoalExecution({ ownerRouteId: input.ownerRouteId, principalId: scope.principalId, workspace: scope.workspace, preset: scope.preset, sessionId, goalId: input.goalId });
@@ -1155,7 +1523,8 @@ export class AssistantSkillsService extends Service {
1155
1523
  const profileSequence = sequenceIds.map(id => {
1156
1524
  const next = this.#repairProfiles.find(value => value.id === id && acceptanceDigest(value.scope) === acceptanceDigest(scope));
1157
1525
  const nextHoldout = next && this.inspectRepairProfile(next.externalHoldoutProfileId, scope);
1158
- if (!next || !nextHoldout || next.skillName !== profile.skillName || next.provider !== profile.provider || next.model !== profile.model
1526
+ if (!next || !nextHoldout || next.skillName !== profile.skillName
1527
+ || next.provider !== undefined && (next.provider !== modelSelection.provider || next.model !== modelSelection.model)
1159
1528
  || next.allowedTools.some(tool => !profile.allowedTools.includes(tool)) || next.maxOutputTokens > profile.maxOutputTokens || next.maxGoalRounds > profile.maxGoalRounds || next.maxDurationMs > profile.maxDurationMs
1160
1529
  || input.expiresAt > nextHoldout.profile.execution.expiresAt)
1161
1530
  throw new Error('assistant-skills: followup profile exceeds the frozen repair authority');
@@ -1176,7 +1545,7 @@ export class AssistantSkillsService extends Service {
1176
1545
  const authorization = { invocationId: input.invocationId, ownerRouteId: input.ownerRouteId,
1177
1546
  source: { goalId: input.goalId, sessionId, nativeGoalId: snapshot.storedGoal.nativeAtLastObservation.goalId, definitionDigest: snapshot.storedGoal.definition.digest },
1178
1547
  profileId: profile.id, profileDigest: acceptanceDigest([profile, holdout.digest]), skillName: profile.skillName,
1179
- parentVersion: parent.version, parentDigest: acceptanceDigest(parent), maxIterations: profile.maxIterations ?? 1, expiresAt: input.expiresAt,
1548
+ parentVersion: parent.version, parentDigest: acceptanceDigest(parent), maxIterations: profile.maxIterations ?? 1, expiresAt: input.expiresAt, modelSelection,
1180
1549
  ...(profileSequence.length > 1 ? { profileSequence } : {}), ...(feedbackAuthority ? { feedbackAuthority } : {}) };
1181
1550
  for (const action of ['draft', 'compare', 'canary', 'watch']) {
1182
1551
  this.#scope(agent, action);
@@ -1268,9 +1637,21 @@ export class AssistantSkillsService extends Service {
1268
1637
  }
1269
1638
  }).catch(() => undefined).finally(() => { this.#repairTask = undefined; });
1270
1639
  }
1640
+ #repairAgentInput(record, trigger) {
1641
+ const profile = this.#repairProfile(record), primary = this.#repairProfileAt(record, 1);
1642
+ const modelSelection = this.#repairModelSelection(record);
1643
+ const usage = this.#store.repairUsage(record.scope, record.id);
1644
+ const callback = () => this.#assertRepair(record);
1645
+ this.#repairAssertions.set(record.id, { callback, triggerDigest: acceptanceDigest(trigger) });
1646
+ return { ...profile, ...modelSelection, id: record.id, iteration: record.iteration, maxModelCalls: primary.maxModelCalls, maxToolCalls: primary.maxToolCalls,
1647
+ initialModelCalls: usage.modelCalls, initialToolCalls: usage.toolCalls,
1648
+ recordUsage: kind => { this.#store.chargeRepairUsage(record.scope, record.id, kind, kind === 'model' ? primary.maxModelCalls : primary.maxToolCalls); },
1649
+ authorizationDigest: record.authorizationDigest, ownerRouteId: record.authorization.ownerRouteId,
1650
+ trigger, objective: trigger.taskFamily.objective, expiresAt: record.authorization.expiresAt, assertCurrent: callback };
1651
+ }
1271
1652
  #installRepairRuntime(ctx) {
1272
1653
  ctx.inject(['tools', 'agents', 'sessions', 'llm', 'systemPrompt', 'assistantGoals', 'assistantPolicy', 'assistantDelivery', 'assistantVerifier'], runtime => {
1273
- const agents = new OwnerRepairAgentRuntime(runtime);
1654
+ const agents = new OwnerRepairAgentRuntime(runtime, this.#store);
1274
1655
  const driver = new RepairContinuationRuntime(this.#store, {
1275
1656
  assertCurrent: record => this.#assertRepair(record),
1276
1657
  inspectTrigger: async (record, signal) => {
@@ -1281,14 +1662,18 @@ export class AssistantSkillsService extends Service {
1281
1662
  return this.#goals().inspectOwnerFailureTrigger({ ownerRouteId: locator.ownerRouteId, principalId: locator.principalId, workspace: locator.workspace, preset: locator.preset, taskFamilyId: profile.taskFamilyId, failures: [{ goalId: locator.goalId, sessionId: locator.sessionId }], minimumOccurrences: 1 }, signal);
1282
1663
  },
1283
1664
  createRepair: async (record, evidence, signal) => {
1284
- const profile = this.#repairProfile(record), primary = this.#repairProfileAt(record, 1), trigger = evidence;
1285
- const usage = this.#store.repairUsage(record.scope, record.id);
1286
- const callback = () => this.#assertRepair(record);
1287
- this.#repairAssertions.set(record.id, { callback, triggerDigest: acceptanceDigest(trigger) });
1288
- return agents.create({ ...profile, id: record.id, iteration: record.iteration, maxModelCalls: primary.maxModelCalls, maxToolCalls: primary.maxToolCalls,
1289
- initialModelCalls: usage.modelCalls, initialToolCalls: usage.toolCalls,
1290
- recordUsage: kind => { this.#store.chargeRepairUsage(record.scope, record.id, kind, kind === 'model' ? primary.maxModelCalls : primary.maxToolCalls); }, authorizationDigest: record.authorizationDigest, ownerRouteId: record.authorization.ownerRouteId,
1291
- trigger, objective: trigger.taskFamily.objective, expiresAt: record.authorization.expiresAt, assertCurrent: callback }, signal);
1665
+ return agents.create(this.#repairAgentInput(record, evidence), signal);
1666
+ },
1667
+ ensureRepair: async (record, signal) => {
1668
+ const locator = this.#repairLocator(record, true);
1669
+ if (agents.get(locator.sessionId))
1670
+ return;
1671
+ const snapshot = this.#goals().inspectOwnerGoalExecution(locator);
1672
+ const stored = snapshot.storedGoal;
1673
+ await agents.resume(this.#repairAgentInput(record, record.checkpoint.trigger), {
1674
+ sessionId: locator.sessionId, goalId: locator.goalId, nativeGoalId: stored.nativeAtLastObservation.goalId,
1675
+ definitionDigest: stored.definition.digest,
1676
+ }, signal);
1292
1677
  },
1293
1678
  inspectRepair: async (record, signal) => {
1294
1679
  const locator = this.#repairLocator(record, true), snapshot = this.#goals().inspectOwnerGoalExecution(locator);
@@ -1361,14 +1746,9 @@ export class AssistantSkillsService extends Service {
1361
1746
  this.#repairAgents = agents;
1362
1747
  this.#repairRuntime = driver;
1363
1748
  for (const record of this.#store.listRepairContinuations()) {
1364
- // An in-flight native repair cannot be recreated from a cached transcript.
1365
- // Until its Agent can be reacquired with verified quiescence, surface the
1366
- // interruption immediately rather than leaving it "running" until expiry.
1367
- if (['repairing', 'repair-achieved', 'candidate-staged'].includes(record.state)) {
1368
- this.#store.transitionRepairContinuation(record.scope, record.id, record.revision, 'unknown', { ...record.checkpoint, failure: 'repair-agent-interrupted' });
1369
- }
1370
- else
1371
- driver.recover(record.scope, record.id);
1749
+ // Dispatches without a durable result are not replayable. Existing
1750
+ // repair Sessions are reattached by ensureRepair under their fence.
1751
+ driver.recover(record.scope, record.id);
1372
1752
  }
1373
1753
  runtime.tools.register(defineTool({ name: 'skill_repair_arm', description: 'Authorize a bounded autonomous repair sequence starting from an exact owner Goal using configured profiles. After independent failure evidence, a separate native Goal synthesizes a repair; only independent acceptance and prospective comparison can deploy a finite canary. Requires the current human request. Followups require the frozen profile sequence and a new independently failed task after promotion; budgets and expiry cannot be renewed.',
1374
1754
  parameters: { goal_id: { type: 'string', required: true, description: businessGoalId }, source_session_id: { type: 'string' }, profile_id: { type: 'string', required: true }, owner_route_id: { type: 'string', required: true }, invocation_id: { type: 'string', required: true }, expires_at: { type: 'integer', required: true }, notify: { type: 'boolean', description: 'Send finite repair results to this exact current owner route Session.' } }, output,
@@ -1721,14 +2101,41 @@ export class AssistantSkillsService extends Service {
1721
2101
  }
1722
2102
  async run(exec, goalId, name, version, inputs, invocationId) {
1723
2103
  const scope = this.#scope(exec.agent, 'run');
2104
+ const delegated = this.#mountedBenchmarkArms.get(acceptanceDigest(scope));
2105
+ if (delegated) {
2106
+ delegated.assertCurrent();
2107
+ if (delegated.skill.name !== name || delegated.skill.version !== version)
2108
+ throw new Error('assistant-skills: exact delegated arm required');
2109
+ const task = this.#run(exec, goalId, delegated.skill, inputs, invocationId, undefined, delegated);
2110
+ this.#benchmarkRuns.add(task);
2111
+ try {
2112
+ return await task;
2113
+ }
2114
+ finally {
2115
+ this.#benchmarkRuns.delete(task);
2116
+ }
2117
+ }
1724
2118
  const skill = this.#store.get(scope, name);
1725
2119
  if (!skill || skill.retired || skill.version !== version)
1726
2120
  throw new Error('assistant-skills: active skill version required');
1727
2121
  return this.#run(exec, goalId, skill, inputs, invocationId);
1728
2122
  }
1729
- async #run(exec, goalId, skill, inputs, invocationId, candidateId) {
2123
+ async #run(exec, goalId, skill, inputs, invocationId, candidateId, delegated) {
1730
2124
  const action = candidateId ? 'trial' : 'run';
1731
2125
  const scope = this.#scope(exec.agent, action), { name, version } = skill;
2126
+ const assertDelegated = () => {
2127
+ if (!delegated)
2128
+ return;
2129
+ delegated.assertCurrent();
2130
+ if (this.#mountedBenchmarkArms.get(acceptanceDigest(scope)) !== delegated
2131
+ || acceptanceDigest(benchmarkArmDefinition(skill)) !== delegated.binding.definitionDigest)
2132
+ throw new Error('assistant-skills: delegated arm changed');
2133
+ };
2134
+ assertDelegated();
2135
+ if (delegated) {
2136
+ await delegated.refresh(exec.signal);
2137
+ assertDelegated();
2138
+ }
1732
2139
  const definition = instantiate(skill, inputs);
1733
2140
  const steps = definition.steps;
1734
2141
  // These declarations are part of the immutable saved definition, rather
@@ -1766,8 +2173,9 @@ export class AssistantSkillsService extends Service {
1766
2173
  const tool = candidateId ? 'skill_trial' : 'skill_run';
1767
2174
  return { state: 'awaiting-native-round', performed: false, context: 'No skill step or durable invocation has been claimed or executed; this is not a queued background task.', next: `After the Host starts this Goal's native round, call ${tool} again with this same goal, skill or candidate, inputs, and invocation_id.`, goalId, ...(candidateId ? { candidateId } : {}), skillName: name, version, invocationId, inputs };
1768
2175
  }
1769
- if (acceptanceDigest(scope) !== acceptanceDigest(current.scope) || goalId === skill.source.goal.id)
1770
- throw new Error('assistant-skills: fresh owner Goal required');
2176
+ // Consume the typed precondition family rather than an ad-hoc condition so
2177
+ // an unknown/tampered declaration fails closed before a run is claimed.
2178
+ evaluatePreconditions(skill.preconditions, { ownerScopeDigest: acceptanceDigest(scope), currentScopeDigest: acceptanceDigest(current.scope), goalId, sourceGoalId: skill.source.goal.id });
1771
2179
  const identity = acceptanceDigest(current);
1772
2180
  const goalContext = current;
1773
2181
  // claim() reconciles a deployed version while holding its transaction. Do
@@ -1793,7 +2201,9 @@ export class AssistantSkillsService extends Service {
1793
2201
  throw new Error('assistant-skills: deployment authority ended');
1794
2202
  }
1795
2203
  }
1796
- const claim = this.#store.claim(scope, { invocationId, goalId, sessionId: current.sessionId, skillName: name, version, inputs, goalExecutionRunId: current.goalExecutionRunId, goalDefinitionDigest: current.definition.digest, ...(typeof goalContext.nativeGoalId === 'string' ? { nativeGoalId: goalContext.nativeGoalId } : {}), ...(candidateId ? { candidateId } : {}) });
2204
+ assertDelegated();
2205
+ const claimInput = { invocationId, goalId, sessionId: current.sessionId, skillName: name, version, inputs, goalExecutionRunId: current.goalExecutionRunId, goalDefinitionDigest: current.definition.digest, ...(typeof goalContext.nativeGoalId === 'string' ? { nativeGoalId: goalContext.nativeGoalId } : {}), ...(candidateId ? { candidateId } : {}) };
2206
+ const claim = delegated ? this.#store.claimDelegated(scope, claimInput, delegated.bindingDigest) : this.#store.claim(scope, claimInput);
1797
2207
  if (!claim.claimed) {
1798
2208
  if (claim.run.state !== 'succeeded')
1799
2209
  throw new Error(`assistant-skills: invocation ${claim.run.id} is ${claim.run.state}; inspect skill_status, do not replay`);
@@ -1819,18 +2229,25 @@ export class AssistantSkillsService extends Service {
1819
2229
  const timeout = new AbortController();
1820
2230
  const timer = setTimeout(() => timeout.abort(), this.#duration);
1821
2231
  timer.unref?.();
1822
- const signal = AbortSignal.any([exec.signal, timeout.signal, this.#lifecycle.signal, ...(deploymentRevision ? [deploymentRevision.signal] : [])]);
2232
+ const signal = AbortSignal.any([exec.signal, timeout.signal, this.#lifecycle.signal, ...(delegated ? [delegated.signal] : []), ...(deploymentRevision ? [deploymentRevision.signal] : [])]);
2233
+ const settle = (work) => delegated ? awaitBenchmarkSignal(work, signal) : work;
1823
2234
  const completed = [];
1824
2235
  let state = 'unknown';
1825
2236
  let dispatched = false;
2237
+ // Resolve the compensation family up front; an unsupported/tampered value
2238
+ // fails closed before any step is dispatched.
2239
+ const compensation = compensationDirective(definition.compensation);
1826
2240
  const revalidate = () => {
1827
2241
  signal.throwIfAborted();
2242
+ assertDelegated();
1828
2243
  if (acceptanceDigest(this.#scope(exec.agent, action)) !== acceptanceDigest(scope)
1829
2244
  || acceptanceDigest(this.#goals().inspectWorkflowRunContext(exec.agent, goalId)) !== identity)
1830
2245
  throw new Error('assistant-skills: current authority changed');
1831
2246
  if (this.#store.getRun(scope, claim.run.id)?.state !== 'running')
1832
2247
  throw new Error('assistant-skills: invocation no longer owns dispatch');
1833
- if (candidateId)
2248
+ if (delegated)
2249
+ assertDelegated();
2250
+ else if (candidateId)
1834
2251
  this.#pending(scope, candidateId);
1835
2252
  else {
1836
2253
  const live = this.#store.get(scope, name);
@@ -1849,11 +2266,13 @@ export class AssistantSkillsService extends Service {
1849
2266
  for (const observation of observations) {
1850
2267
  if (observation.beforeStepId !== step.id)
1851
2268
  continue;
2269
+ if (delegated)
2270
+ await delegated.refresh(signal);
1852
2271
  revalidate();
1853
2272
  if (!this.#allowed.includes('read'))
1854
2273
  throw new Error('assistant-skills: current tool allowlist denied required file observation');
1855
2274
  dispatched = true;
1856
- const observed = await exec.agent.ctx.get('tools').execute({
2275
+ const observed = await settle(exec.agent.ctx.get('tools').execute({
1857
2276
  callId: ToolCallId(`${exec.callId}:skill-observation:${index + 1}`),
1858
2277
  rootCallId: exec.rootCallId,
1859
2278
  parent: exec.token,
@@ -1861,7 +2280,9 @@ export class AssistantSkillsService extends Service {
1861
2280
  name: 'read',
1862
2281
  arguments: { file_path: observation.filePath, limit: 1 },
1863
2282
  signal,
1864
- });
2283
+ }));
2284
+ if (delegated)
2285
+ await delegated.refresh(signal);
1865
2286
  revalidate();
1866
2287
  for (const context of observed.additionalContexts ?? [])
1867
2288
  exec.deferContext(context);
@@ -1883,14 +2304,29 @@ export class AssistantSkillsService extends Service {
1883
2304
  state = 'failed';
1884
2305
  break stepsLoop;
1885
2306
  }
2307
+ if (delegated)
2308
+ await delegated.refresh(signal);
1886
2309
  revalidate();
1887
2310
  dispatched = false;
1888
2311
  }
2312
+ if (delegated)
2313
+ await delegated.refresh(signal);
1889
2314
  revalidate();
2315
+ // Dependency DAG gate: every declared predecessor must have completed
2316
+ // successfully. Topological storage means a missing predecessor can only
2317
+ // result from an earlier failure, at which point the resolved compensation
2318
+ // family stops the remaining steps (the v1 family always does).
2319
+ const succeededById = new Set(completed.filter(entry => entry.state === 'succeeded').map(entry => entry.id));
2320
+ if (compensation.stopRemainingSteps && step.dependsOn.some(dependency => !succeededById.has(dependency))) {
2321
+ state = 'failed';
2322
+ break stepsLoop;
2323
+ }
1890
2324
  if (!this.#allowed.includes(step.toolName))
1891
2325
  throw new Error('assistant-skills: current tool allowlist denied');
1892
2326
  dispatched = true;
1893
- const result = await exec.agent.ctx.get('tools').execute({ callId: ToolCallId(`${exec.callId}:skill:${index + 1}`), rootCallId: exec.rootCallId, parent: exec.token, agent: exec.agent, name: step.toolName, arguments: step.arguments, signal });
2327
+ const result = await settle(exec.agent.ctx.get('tools').execute({ callId: ToolCallId(`${exec.callId}:skill:${index + 1}`), rootCallId: exec.rootCallId, parent: exec.token, agent: exec.agent, name: step.toolName, arguments: step.arguments, signal }));
2328
+ if (delegated)
2329
+ await delegated.refresh(signal);
1894
2330
  revalidate();
1895
2331
  for (const context of result.additionalContexts ?? [])
1896
2332
  exec.deferContext(context);
@@ -1903,6 +2339,8 @@ export class AssistantSkillsService extends Service {
1903
2339
  }
1904
2340
  if (result.concludesTurn)
1905
2341
  exec.concludeTurn();
2342
+ if (delegated)
2343
+ await delegated.refresh(signal);
1906
2344
  revalidate();
1907
2345
  dispatched = false;
1908
2346
  if (index === steps.length - 1)