meguro-mcp 0.2.12 → 0.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/tools.mjs CHANGED
@@ -3,7 +3,7 @@
3
3
  // Every control-plane call uses an account-scoped MEGURO_API_TOKEN (meg_sk_...).
4
4
 
5
5
  import { createHash } from 'node:crypto';
6
- import { documentationByTopic, documentationToolContract } from './docs.mjs';
6
+ import { documentationToolContract, resolveDocumentationRead } from './docs.mjs';
7
7
  import {
8
8
  ADMIN_API_DEFAULT_VERSION,
9
9
  ADMIN_API_SUPPORTED_VERSION_LABEL,
@@ -15,6 +15,8 @@ const SECRET_VALUE = /\bmeg_(?:pw|sk)_[a-z0-9_-]+\b|\b(?:Bearer|Basic)\s+[A-Za-z
15
15
  const SECRET_HEADER = /\b(?:authorization|cookie|set-cookie|x-shopify-access-token|signed-snapshot-token)\s*[:=]\s*[^\s,;]+/giu;
16
16
  const SECRET_KEY = /(?:authorization|cookie|password|secret|token|api[_-]?key|signedsnapshot)/iu;
17
17
  const PRIVATE_RESPONSE_KEY = /^(?:raw|rawBody|rawBodyPreview|responsePayload|requestHeaders|responseHeaders|headers|cookies)$/iu;
18
+ const MCP_TRUNCATION_SCHEMA_VERSION = 'meguro.mcp-truncation.v1';
19
+ const MCP_TRUNCATION_KEY = 'mcpTruncation';
18
20
  const RESERVED_WORLD_IDS = new Set(['api', 'api-dev', 'www', 'dev', 'hooks', 'control']);
19
21
  const PRACTICE_ATTEMPT_ID = /^pa-[a-z0-9][a-z0-9-]{0,124}$/u;
20
22
  export const OPERATION_COMMIT_BOUNDARY_META_KEY = 'meguro/operationCommitBoundary';
@@ -25,15 +27,15 @@ const AUTH_TEACHING = Object.freeze({
25
27
  const DOCUMENTATION_TOOL_CONTRACT = documentationToolContract();
26
28
  const DOCUMENTATION_TOPICS = new Set(DOCUMENTATION_TOOL_CONTRACT.topics);
27
29
  const FLEET_TOOL_NAMES = new Set([
28
- 'templates_list', 'stores_list', 'store_create', 'store_delete', 'store_passport',
30
+ 'templates_list', 'template_get', 'stores_list', 'store_create', 'store_delete', 'store_passport',
29
31
  'practice_runs_list',
30
32
  'workspaces_list', 'workspace_create', 'workspace_archive', 'workspace_unarchive',
31
- 'share_create', 'shares_list', 'share_status', 'share_publish', 'share_revoke',
32
33
  'catalog_slice_read', 'catalog_slice_snapshot', 'catalog_slices_saved',
33
34
  'store_claim_by_code', 'store_claim',
34
35
  ]);
35
36
  const TOOL_PRESENTATION = Object.freeze({
36
37
  templates_list: { title: 'List practice-store templates', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
38
+ template_get: { title: 'Read one practice-store template', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
37
39
  stores_list: { title: 'List practice stores', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
38
40
  store_create: { title: 'Create a practice store', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
39
41
  store_delete: { title: 'Delete a practice store', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
@@ -42,11 +44,6 @@ const TOOL_PRESENTATION = Object.freeze({
42
44
  workspace_create: { title: 'Create a workspace', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
43
45
  workspace_archive: { title: 'Archive a workspace', readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
44
46
  workspace_unarchive: { title: 'Restore a workspace', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
45
- share_create: { title: 'Create or update a private share draft', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
46
- shares_list: { title: 'List evidence shares', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
47
- share_status: { title: 'Read an evidence share', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
48
- share_publish: { title: 'Publish evidence to client viewers', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
49
- share_revoke: { title: 'Revoke client-viewer evidence access', readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
50
47
  catalog_slice_read: { title: 'Read a live Shopify catalog slice', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
51
48
  catalog_slice_snapshot: { title: 'Mint a catalog slice snapshot', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
52
49
  catalog_slices_saved: { title: 'Manage saved catalog slices', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
@@ -58,9 +55,9 @@ const TOOL_PRESENTATION = Object.freeze({
58
55
  run_report: { title: 'Read run receipt', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
59
56
  run_resume: { title: 'Resume a Shopify dev-store run', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
60
57
  runs_diff: { title: 'Compare history-run receipts', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
61
- gate_configure: { title: 'Configure the Receipt Gate', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
62
- gate_evaluate: { title: 'Evaluate the Receipt Gate', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
63
- gate_verdict: { title: 'Read a Gate verdict', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
58
+ gate_configure: { title: 'Configure Configured Receipt Gate', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
59
+ gate_evaluate: { title: 'Evaluate Configured Receipt Gate', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
60
+ gate_verdict: { title: 'Read Configured Receipt Gate verdict', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
64
61
  runs_list: { title: 'List history runs', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
65
62
  usage_read: { title: 'Read usage headroom', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
66
63
  twin_diff: { title: 'Read a twin impact receipt', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
@@ -81,12 +78,22 @@ const TOOL_PRESENTATION = Object.freeze({
81
78
  admin_probe: { title: 'Run an Admin API probe', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
82
79
  admin_schema: { title: 'Look up the Admin API schema', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
83
80
  admin_recipes_list: { title: 'List Admin API recipes', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
81
+ plan_validate: { title: 'Check an intended Admin operation plan against Meguro', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
84
82
  });
85
83
 
86
84
  function textResult(value) {
87
85
  return { content: [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value, null, 2) }] };
88
86
  }
89
87
 
88
+ function configuredReceiptGateResult(value, action) {
89
+ const result = textResult(value);
90
+ result.content.push({
91
+ type: 'text',
92
+ text: `Configured Receipt Gate ${action}. This optional configured policy is separate from the receipt's automatic recorded API behavior; the receipt's recorded API behavior remains available.`,
93
+ });
94
+ return result;
95
+ }
96
+
90
97
  function errorResult(message) {
91
98
  return { content: [{ type: 'text', text: scrubString(message) }], isError: true };
92
99
  }
@@ -109,19 +116,60 @@ export function redactSecrets(value) {
109
116
  return scrubString(value);
110
117
  }
111
118
 
112
- function secretSafe(value, depth = 0) {
119
+ function secretSafePath(parent, key, arrayIndex = false) {
120
+ if (arrayIndex) return `${parent}[${key}]`;
121
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/u.test(key)
122
+ ? `${parent}.${key}`
123
+ : `${parent}[${JSON.stringify(key)}]`;
124
+ }
125
+
126
+ function secretSafeValue(value, depth, path, truncations) {
113
127
  if (depth > 12) return '[TRUNCATED]';
114
- if (typeof value === 'string') return scrubString(value).slice(0, 10_000);
115
- if (Array.isArray(value)) return value.slice(0, 100).map((child) => secretSafe(child, depth + 1));
128
+ if (typeof value === 'string') {
129
+ const scrubbed = scrubString(value);
130
+ const returned = scrubbed.slice(0, 10_000);
131
+ if (returned.length < scrubbed.length) {
132
+ truncations.push({ path, kind: 'string', originalSize: scrubbed.length, returnedSize: returned.length });
133
+ }
134
+ return returned;
135
+ }
136
+ if (Array.isArray(value)) {
137
+ const returned = value.slice(0, 100);
138
+ if (returned.length < value.length) {
139
+ truncations.push({ path, kind: 'array', originalSize: value.length, returnedSize: returned.length });
140
+ }
141
+ return returned.map((child, index) => secretSafeValue(
142
+ child,
143
+ depth + 1,
144
+ secretSafePath(path, index, true),
145
+ truncations,
146
+ ));
147
+ }
116
148
  if (value && typeof value === 'object') {
117
- return Object.fromEntries(Object.entries(value)
118
- .filter(([key]) => !SECRET_KEY.test(key) && !PRIVATE_RESPONSE_KEY.test(key))
119
- .slice(0, 100)
120
- .map(([key, child]) => [key, secretSafe(child, depth + 1)]));
149
+ const visibleEntries = Object.entries(value)
150
+ .filter(([key]) => !SECRET_KEY.test(key) && !PRIVATE_RESPONSE_KEY.test(key));
151
+ const returned = visibleEntries.slice(0, 100);
152
+ if (returned.length < visibleEntries.length) {
153
+ truncations.push({ path, kind: 'object', originalSize: visibleEntries.length, returnedSize: returned.length });
154
+ }
155
+ return Object.fromEntries(returned.map(([key, child]) => [
156
+ key,
157
+ secretSafeValue(child, depth + 1, secretSafePath(path, key), truncations),
158
+ ]));
121
159
  }
122
160
  return value;
123
161
  }
124
162
 
163
+ function secretSafe(value, depth = 0) {
164
+ const truncations = [];
165
+ const safe = secretSafeValue(value, depth, '$', truncations);
166
+ if (truncations.length === 0) return safe;
167
+ const evidence = { schemaVersion: MCP_TRUNCATION_SCHEMA_VERSION, entries: truncations };
168
+ return safe && typeof safe === 'object' && !Array.isArray(safe)
169
+ ? { ...safe, [MCP_TRUNCATION_KEY]: evidence }
170
+ : { value: safe, [MCP_TRUNCATION_KEY]: evidence };
171
+ }
172
+
125
173
  function authenticationErrorResult(status, value) {
126
174
  if (status !== 401 && status !== 403) return null;
127
175
  const body = value && typeof value === 'object' ? value : {};
@@ -177,8 +225,55 @@ function catalogSnapshotProjection(value) {
177
225
  : safe;
178
226
  }
179
227
 
228
+ function enforcementProjection(body) {
229
+ if (!body || typeof body !== 'object') return {};
230
+ const hasStructuredEnforcement = typeof body.blockedCapability === 'string'
231
+ || typeof body.tier === 'string'
232
+ || Number.isFinite(body.allowance)
233
+ || Number.isFinite(body.currentUsage)
234
+ || Number.isFinite(body.requestedAmount)
235
+ || (body.sameTierAction && typeof body.sameTierAction === 'object')
236
+ || typeof body.targetTier === 'string'
237
+ || typeof body.upgradePath === 'string';
238
+ if (!hasStructuredEnforcement) return {};
239
+ return {
240
+ ...(typeof body.code === 'string' ? { code: body.code } : {}),
241
+ ...(typeof body.blockedCapability === 'string' ? { blockedCapability: body.blockedCapability } : {}),
242
+ ...(typeof body.tier === 'string' ? { tier: body.tier } : {}),
243
+ ...(Number.isFinite(body.allowance) ? { allowance: body.allowance } : {}),
244
+ ...(Number.isFinite(body.currentUsage) ? { currentUsage: body.currentUsage } : {}),
245
+ ...(Number.isFinite(body.requestedAmount) ? { requestedAmount: body.requestedAmount } : {}),
246
+ ...(body.sameTierAction && typeof body.sameTierAction === 'object'
247
+ ? { sameTierAction: secretSafe(body.sameTierAction) }
248
+ : {}),
249
+ ...(typeof body.targetTier === 'string' ? { targetTier: body.targetTier } : {}),
250
+ ...(typeof body.upgradePath === 'string' ? { upgradePath: body.upgradePath } : {}),
251
+ };
252
+ }
253
+
254
+ /**
255
+ * MEG-1058: the compact list tier. `templates_list` carries only what a cold agent needs to CHOOSE;
256
+ * everything it needs to USE a template is one `template_get` hop away. Both are projected from the
257
+ * same compiled catalog, so there is no second template contract.
258
+ */
259
+ const STORE_TEMPLATE_LIST_FIELDS = Object.freeze([
260
+ 'key',
261
+ 'label',
262
+ 'category',
263
+ 'recommended',
264
+ 'bestFor',
265
+ 'recommendedRunDays',
266
+ ]);
267
+
268
+ /**
269
+ * MEG-1058: the detail tier's allowlist — the complete public-safe record `template_get` returns.
270
+ * `revision` and `evaluationContext` join it here: both are ordinary template declarations the
271
+ * detail hop is meant to answer with, and a record missing them is not the complete one.
272
+ */
180
273
  const STORE_TEMPLATE_FIELDS = Object.freeze([
181
274
  'key',
275
+ 'revision',
276
+ 'evaluationContext',
182
277
  'label',
183
278
  'category',
184
279
  'recommended',
@@ -193,30 +288,60 @@ const STORE_TEMPLATE_FIELDS = Object.freeze([
193
288
  'extendedRunDays',
194
289
  'whatAgentGets',
195
290
  'supportedReads',
291
+ 'modeledEvidence',
196
292
  'liveRun',
197
293
  'goodFirstAgents',
198
294
  'supportedWrites',
199
295
  'operationContracts',
200
296
  'knownUnsupported',
297
+ 'unsupportedContracts',
298
+ 'capabilityBoundaries',
299
+ 'modeledPhysics',
201
300
  'goodAgentShould',
202
301
  'avoid',
203
302
  'reportGrades',
204
303
  ]);
205
304
 
305
+ /**
306
+ * MEG-1058: the detail tier's projection. Same allowlist the list used to carry, applied to the one
307
+ * record the server selected — the catalog is never fetched or returned as this tool's result.
308
+ */
309
+ function storeTemplateDetailProjection(value) {
310
+ const storeTemplate = value?.storeTemplate;
311
+ if (!storeTemplate || typeof storeTemplate !== 'object' || Array.isArray(storeTemplate) || typeof storeTemplate.key !== 'string') {
312
+ throw new Error('Meguro template detail response did not contain a storeTemplate');
313
+ }
314
+ return {
315
+ storeTemplate: Object.fromEntries(STORE_TEMPLATE_FIELDS
316
+ .filter((field) => Object.hasOwn(storeTemplate, field))
317
+ .map((field) => [field, secretSafe(storeTemplate[field], 1)])),
318
+ };
319
+ }
320
+
206
321
  function storeTemplatesProjection(value) {
207
322
  if (!Array.isArray(value?.storeTemplates)) {
208
323
  throw new Error('Meguro template catalog response did not contain storeTemplates');
209
324
  }
210
- const starterTemplateKeys = value.storeTemplates
211
- .filter((template) => template?.recommended === true && typeof template?.key === 'string')
212
- .map((template) => template.key);
325
+ const storeTemplates = value.storeTemplates.map((template) => {
326
+ if (!template || typeof template !== 'object' || Array.isArray(template) || typeof template.key !== 'string') {
327
+ throw new Error('Meguro template catalog contained an invalid template entry');
328
+ }
329
+ return Object.fromEntries(STORE_TEMPLATE_LIST_FIELDS
330
+ .filter((field) => Object.hasOwn(template, field))
331
+ .map((field) => [field, secretSafe(template[field], 1)]));
332
+ });
333
+ const catalogTemplateKeys = storeTemplates.map((template) => template.key);
334
+ const declaredTemplateKeys = value?.starterRecommendation?.templateKeys;
335
+ const starterTemplateKeys = Array.isArray(declaredTemplateKeys)
336
+ && declaredTemplateKeys.every((key) => typeof key === 'string' && catalogTemplateKeys.includes(key))
337
+ ? [...declaredTemplateKeys]
338
+ : [];
339
+ let invalidRecommendation = starterTemplateKeys.length !== (declaredTemplateKeys?.length ?? -1);
213
340
  const declaredStarterBasis = typeof value?.starterRecommendation?.basis === 'string'
214
341
  && /^[a-z0-9-]{1,64}$/u.test(value.starterRecommendation.basis)
215
342
  ? value.starterRecommendation.basis
216
343
  : 'starter-for-first-simulation';
217
- const starterPlan = value?.starterRecommendation?.starterPlan;
218
- let projectedStarterPlan;
219
- if (starterPlan !== undefined) {
344
+ const projectStarterPlan = (starterPlan) => {
220
345
  const positiveInteger = (candidate) => Number.isSafeInteger(candidate) && candidate > 0;
221
346
  const nonNegativeInteger = (candidate) => Number.isSafeInteger(candidate) && candidate >= 0;
222
347
  const valid = starterPlan
@@ -225,7 +350,7 @@ function storeTemplatesProjection(value) {
225
350
  && starterPlan.schemaVersion === 'meguro.practice-run-starter-plan.v1'
226
351
  && ['developer', 'solo', 'builder'].includes(starterPlan.tier)
227
352
  && typeof starterPlan.templateKey === 'string'
228
- && starterTemplateKeys.includes(starterPlan.templateKey)
353
+ && catalogTemplateKeys.includes(starterPlan.templateKey)
229
354
  && positiveInteger(starterPlan.idealRunDays)
230
355
  && positiveInteger(starterPlan.scenarioArcDays)
231
356
  && positiveInteger(starterPlan.firstSegmentDays)
@@ -238,57 +363,129 @@ function storeTemplatesProjection(value) {
238
363
  && typeof starterPlan.continuation === 'object'
239
364
  && typeof starterPlan.continuation.available === 'boolean'
240
365
  && nonNegativeInteger(starterPlan.continuation.remainingArcDaysAfterFirstSegment)
366
+ && (starterPlan.continuation.remainingAuthorizedDaysAfterFirstSegment === undefined
367
+ || nonNegativeInteger(starterPlan.continuation.remainingAuthorizedDaysAfterFirstSegment))
241
368
  && positiveInteger(starterPlan.continuation.maxCanvasWorldDays)
369
+ && (starterPlan.authorization === undefined || (
370
+ starterPlan.authorization
371
+ && ['account-onboarding-grant', 'ordinary-tier'].includes(starterPlan.authorization.source)
372
+ && positiveInteger(starterPlan.authorization.idealHorizonDays)
373
+ ))
242
374
  && starterPlan.usage
243
375
  && typeof starterPlan.usage === 'object'
244
376
  && typeof starterPlan.usage.runStartAllowed === 'boolean'
377
+ && ((starterPlan.usage.runStartAllowedMeaning === undefined && starterPlan.usage.runStartAllowance === undefined) || (
378
+ starterPlan.usage.runStartAllowedMeaning === 'metering-allowance-only'
379
+ && starterPlan.usage.runStartAllowance
380
+ && typeof starterPlan.usage.runStartAllowance === 'object'
381
+ && starterPlan.usage.runStartAllowance.schemaVersion === 'meguro.run-start-allowance.v1'
382
+ && starterPlan.usage.runStartAllowance.permitsChargedRun === starterPlan.usage.runStartAllowed
383
+ && ['metering-allowance-available', 'metering-allowance-exhausted'].includes(starterPlan.usage.runStartAllowance.reasonCode)
384
+ && starterPlan.usage.runStartAllowance.meaning === 'metering-allowance-only'
385
+ && starterPlan.usage.runStartAllowance.detail === 'Whether current tier metering allowance permits one charged simulation run; this is not overall store/root lifecycle eligibility.'
386
+ ))
245
387
  && nonNegativeInteger(starterPlan.usage.remainingSimulationRuns);
246
388
  if (!valid) throw new Error('Meguro template catalog contained an invalid starterPlan');
247
- projectedStarterPlan = {
389
+ return {
248
390
  schemaVersion: starterPlan.schemaVersion,
249
391
  tier: starterPlan.tier,
250
392
  templateKey: starterPlan.templateKey,
251
393
  idealRunDays: starterPlan.idealRunDays,
252
394
  scenarioArcDays: starterPlan.scenarioArcDays,
253
395
  firstSegmentDays: starterPlan.firstSegmentDays,
396
+ ...(starterPlan.authorization ? { authorization: {
397
+ source: starterPlan.authorization.source,
398
+ idealHorizonDays: starterPlan.authorization.idealHorizonDays,
399
+ } } : {}),
254
400
  practiceRunStart: { clock: { simulationDays: starterPlan.practiceRunStart.clock.simulationDays } },
255
401
  continuation: {
256
402
  available: starterPlan.continuation.available,
257
403
  remainingArcDaysAfterFirstSegment: starterPlan.continuation.remainingArcDaysAfterFirstSegment,
404
+ ...(starterPlan.continuation.remainingAuthorizedDaysAfterFirstSegment !== undefined
405
+ ? { remainingAuthorizedDaysAfterFirstSegment: starterPlan.continuation.remainingAuthorizedDaysAfterFirstSegment }
406
+ : {}),
258
407
  maxCanvasWorldDays: starterPlan.continuation.maxCanvasWorldDays,
259
408
  },
260
409
  usage: {
261
410
  runStartAllowed: starterPlan.usage.runStartAllowed,
411
+ ...(starterPlan.usage.runStartAllowedMeaning ? {
412
+ runStartAllowedMeaning: starterPlan.usage.runStartAllowedMeaning,
413
+ runStartAllowance: {
414
+ schemaVersion: starterPlan.usage.runStartAllowance.schemaVersion,
415
+ permitsChargedRun: starterPlan.usage.runStartAllowance.permitsChargedRun,
416
+ reasonCode: starterPlan.usage.runStartAllowance.reasonCode,
417
+ meaning: starterPlan.usage.runStartAllowance.meaning,
418
+ detail: starterPlan.usage.runStartAllowance.detail,
419
+ },
420
+ } : {}),
262
421
  remainingSimulationRuns: starterPlan.usage.remainingSimulationRuns,
263
422
  },
264
423
  };
424
+ };
425
+ const starterPlan = value?.starterRecommendation?.starterPlan;
426
+ const starterPlans = value?.starterRecommendation?.starterPlans;
427
+ let projectedStarterPlan;
428
+ let projectedStarterPlans;
429
+ try {
430
+ projectedStarterPlans = starterPlans === undefined
431
+ ? undefined
432
+ : Array.isArray(starterPlans) ? starterPlans.map(projectStarterPlan) : (() => { throw new Error('invalid starterPlans'); })();
433
+ if (projectedStarterPlans) {
434
+ const planKeys = projectedStarterPlans.map((plan) => plan.templateKey);
435
+ if (new Set(planKeys).size !== planKeys.length
436
+ || planKeys.some((key) => !catalogTemplateKeys.includes(key))) {
437
+ throw new Error('invalid starterPlans');
438
+ }
439
+ }
440
+ projectedStarterPlan = starterPlan === undefined ? undefined : projectStarterPlan(starterPlan);
441
+ if (projectedStarterPlan && !starterTemplateKeys.includes(projectedStarterPlan.templateKey)) {
442
+ throw new Error('invalid selected starterPlan');
443
+ }
444
+ } catch {
445
+ projectedStarterPlan = undefined;
446
+ projectedStarterPlans = undefined;
447
+ invalidRecommendation = true;
265
448
  }
449
+ const serverUnavailable = value?.starterRecommendation?.recommendationUnavailable;
450
+ const projectedUnavailable = serverUnavailable
451
+ && typeof serverUnavailable === 'object'
452
+ && serverUnavailable.schemaVersion === 'meguro.practice-starter-recommendation-unavailable.v1'
453
+ && ['run-start-unavailable', 'usage-unavailable'].includes(serverUnavailable.code)
454
+ ? {
455
+ schemaVersion: serverUnavailable.schemaVersion,
456
+ code: serverUnavailable.code,
457
+ }
458
+ : undefined;
459
+ const recommendationUnavailable = invalidRecommendation
460
+ ? {
461
+ schemaVersion: 'meguro.practice-starter-recommendation-unavailable.v1',
462
+ code: 'invalid-server-recommendation',
463
+ }
464
+ : projectedUnavailable;
266
465
  return {
267
466
  starterRecommendation: {
268
467
  basis: declaredStarterBasis,
269
468
  templateKeys: starterTemplateKeys,
270
469
  ...(projectedStarterPlan ? { starterPlan: projectedStarterPlan } : {}),
470
+ ...(projectedStarterPlans ? { starterPlans: projectedStarterPlans } : {}),
471
+ ...(recommendationUnavailable ? { recommendationUnavailable } : {}),
271
472
  },
272
- storeTemplates: value.storeTemplates.map((template) => {
273
- if (!template || typeof template !== 'object' || Array.isArray(template)) {
274
- throw new Error('Meguro template catalog contained an invalid template entry');
275
- }
276
- return Object.fromEntries(STORE_TEMPLATE_FIELDS
277
- .filter((field) => Object.hasOwn(template, field))
278
- .map((field) => [field, secretSafe(template[field], 1)]));
279
- }),
473
+ storeTemplates,
280
474
  };
281
475
  }
282
476
 
283
- function practiceErrorResult(status, value, retryAfterSeconds, teaching) {
477
+ function practiceErrorResult(status, value, retryAfterSeconds, teaching, declaredBoundary) {
284
478
  const authenticationError = authenticationErrorResult(status, value);
285
479
  if (authenticationError) return authenticationError;
286
480
  const body = value && typeof value === 'object' ? value : {};
287
- const stable = { httpStatus: status };
481
+ const stable = { httpStatus: status, ...enforcementProjection(body) };
288
482
  if (body.code === 'terminal-advance-confirmation-required') {
289
483
  stable.code = body.code;
290
484
  stable.terminalAdvance = secretSafe(body.terminalAdvance);
291
485
  }
486
+ if (body.advanceCursor && typeof body.advanceCursor === 'object') {
487
+ stable.advanceCursor = secretSafe(body.advanceCursor);
488
+ }
292
489
  if (['requested-exceeds-cap', 'continuation-unavailable', 'limit-reached'].includes(body.reason)) {
293
490
  stable.reason = body.reason;
294
491
  }
@@ -309,7 +506,16 @@ function practiceErrorResult(status, value, retryAfterSeconds, teaching) {
309
506
  else if (Number.isFinite(bodyRetryAfter) && bodyRetryAfter >= 0) stable.retryAfterSeconds = bodyRetryAfter;
310
507
  if (typeof body.retryable === 'boolean') stable.retryable = body.retryable;
311
508
  if (teaching) stable.teaching = secretSafe(teaching);
312
- return { content: [{ type: 'text', text: JSON.stringify(stable, null, 2) }], isError: true };
509
+ const result = { content: [{ type: 'text', text: JSON.stringify(stable, null, 2) }], isError: true };
510
+ // MEG-1057: an attempt-identity 404 is pre-commit by construction — nothing can have applied
511
+ // against an attempt that does not exist. Without this the mutating routes fell through to the
512
+ // caller-facing default of `unknown`, which asks the agent to go read state before retrying an
513
+ // operation that never had a target.
514
+ if (isPracticeRunUnavailable(status, body)) return withOperationCommitBoundary(result, 'pre-commit');
515
+ if (declaredBoundary) return withOperationCommitBoundary(result, declaredBoundary);
516
+ return ['pre-commit', 'committed', 'unknown'].includes(body.operationCommitBoundary)
517
+ ? withOperationCommitBoundary(result, body.operationCommitBoundary)
518
+ : result;
313
519
  }
314
520
 
315
521
  function practiceRunUnavailableTeaching(toolName, attemptId) {
@@ -322,22 +528,30 @@ function practiceRunUnavailableTeaching(toolName, attemptId) {
322
528
 
323
529
  function gateReceiptUnavailableTeaching(toolName, receiptId) {
324
530
  return {
325
- meaning: `No completed practice simulation receipt is available for receiptId ${receiptId}. Receipt Gate uses the pa-* practice-run attempt identity, not a public receipt digest or a history-run id.`,
531
+ meaning: `No completed practice simulation receipt is available for receiptId ${receiptId}. Configured Receipt Gate uses the pa-* practice-run attempt identity, not a public receipt digest or a history-run id.`,
326
532
  nextStep: `Call stores_list({}), choose an exact storeId, call practice_run_start({ storeId: "<exact storeId>" }), complete the simulation, call practice_run_finish({ attemptId: "<returned attemptId>" }), then call practice_run_report({ attemptId: "<returned attemptId>" }). Use that same returned attemptId as receiptId and call ${toolName}({ receiptId: "<returned attemptId>" }).`,
327
- stopCondition: 'If practice_run_report does not return a completed receipt for that attemptId, stop. Do not retry Receipt Gate with an unavailable receiptId.',
533
+ stopCondition: 'If practice_run_report does not return a completed receipt for that attemptId, stop. Do not retry Configured Receipt Gate with an unavailable receiptId.',
534
+ };
535
+ }
536
+
537
+ function practiceStoreUnavailableTeaching(storeId) {
538
+ return {
539
+ meaning: `No practice store with storeId ${storeId} is available to this account, so no practice run was started.`,
540
+ nextStep: 'Call stores_list({}), choose an exact current storeId, then call practice_run_start({ storeId: "<exact storeId>" }).',
541
+ stopCondition: `If stores_list({}) does not return ${storeId}, stop. Do not retry practice_run_start with the unavailable storeId.`,
328
542
  };
329
543
  }
330
544
 
331
545
  function isPracticeRunUnavailable(status, value) {
332
- return status === 404 && value?.practiceRunError?.code === 'practice-run-unavailable';
546
+ return status === 404;
333
547
  }
334
548
 
335
549
  function isGateReceiptUnavailable(status, value) {
336
- if (status !== 404) return false;
337
- const messages = Array.isArray(value?.errors)
338
- ? value.errors.map((error) => String(error?.message ?? ''))
339
- : [String(value?.error?.message ?? value?.error ?? '')];
340
- return messages.some((message) => /receipt not found/iu.test(message));
550
+ return status === 404;
551
+ }
552
+
553
+ function isPracticeStoreUnavailable(status, value) {
554
+ return status === 404;
341
555
  }
342
556
 
343
557
  function requiredString(args, key) {
@@ -367,11 +581,11 @@ function requiredWorkspaceId(args) {
367
581
  return workspaceId;
368
582
  }
369
583
 
370
- function requiredShareId(args) {
371
- const shareId = requiredString(args, 'shareId');
372
- if (!/^[0-9a-f]{32}$/u.test(shareId)) throw new Error('shareId must be the exact 32-character lowercase hex id returned by shares_list');
373
- return shareId;
374
- }
584
+ const OPTIONAL_WORKSPACE_SELECTOR_INPUT_SCHEMA = Object.freeze({
585
+ type: 'string',
586
+ minLength: 1,
587
+ description: 'Optional account-owned workspace selector. Omit for Default.',
588
+ });
375
589
 
376
590
  function optionalStringList(args, key, options = {}) {
377
591
  const value = args?.[key];
@@ -407,6 +621,115 @@ function requiredSavedSliceId(args) {
407
621
  return sliceId;
408
622
  }
409
623
 
624
+ const CATALOG_SNAPSHOT_INPUT_PROPERTIES = Object.freeze({
625
+ workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace selector. Omit for Default.' },
626
+ shopDomain: { type: 'string', pattern: '^[a-z0-9][a-z0-9-]*\\.myshopify\\.com$', description: 'Live snapshot source. Mutually exclusive with sliceId.' },
627
+ variantIds: { type: 'array', minItems: 1, maxItems: 25, items: { type: 'string', minLength: 1 }, description: 'Exact Shopify variant ids returned by catalog_slice_read. Required with shopDomain.' },
628
+ sliceId: { type: 'string', pattern: '^csl_saved_[A-Za-z0-9_-]{8,128}$', description: 'Saved snapshot source. Mutually exclusive with shopDomain/variantIds.' },
629
+ });
630
+
631
+ const CATALOG_SAVED_SLICES_INPUT_PROPERTIES = Object.freeze({
632
+ workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace selector. Omit for Default.' },
633
+ action: { type: 'string', enum: ['list', 'get', 'save', 'delete', 'refresh', 'changes'] },
634
+ shopDomain: { type: 'string', pattern: '^[a-z0-9][a-z0-9-]*\\.myshopify\\.com$', description: 'Required for save.' },
635
+ label: { type: 'string', minLength: 1, maxLength: 80, description: 'Required for save.' },
636
+ variantIds: { type: 'array', minItems: 1, maxItems: 25, items: { type: 'string', minLength: 1 }, description: 'Required for save.' },
637
+ sliceId: { type: 'string', pattern: '^csl_saved_[A-Za-z0-9_-]{8,128}$', description: 'Required for get/delete/refresh/changes.' },
638
+ });
639
+
640
+ function catalogConditionalBranch({ id, action, mutates, required, forbidden, example }) {
641
+ return Object.freeze({
642
+ id,
643
+ action,
644
+ mutates,
645
+ required: Object.freeze([...required]),
646
+ example: Object.freeze(example),
647
+ schema: Object.freeze({
648
+ type: 'object',
649
+ ...(action ? { properties: { action: { const: action } } } : {}),
650
+ required: [...required],
651
+ ...(forbidden.length > 0
652
+ ? { not: { anyOf: forbidden.map((key) => ({ required: [key] })) } }
653
+ : {}),
654
+ }),
655
+ });
656
+ }
657
+
658
+ function conditionalToolInputSchema(properties, branches) {
659
+ return Object.freeze({
660
+ type: 'object',
661
+ additionalProperties: false,
662
+ properties,
663
+ required: [],
664
+ oneOf: branches.map((branch) => branch.schema),
665
+ });
666
+ }
667
+
668
+ // Each branch owns the published shape, dispatch selection, and its teaching example. Keeping
669
+ // these together prevents the schema from admitting a selector combination that the handler will
670
+ // later refuse or silently ignore.
671
+ const CATALOG_SNAPSHOT_BRANCHES = Object.freeze([
672
+ catalogConditionalBranch({
673
+ id: 'saved',
674
+ mutates: true,
675
+ required: ['sliceId'],
676
+ forbidden: ['shopDomain', 'variantIds'],
677
+ example: { sliceId: 'csl_saved_01234567' },
678
+ }),
679
+ catalogConditionalBranch({
680
+ id: 'live',
681
+ mutates: false,
682
+ required: ['shopDomain', 'variantIds'],
683
+ forbidden: ['sliceId'],
684
+ example: { shopDomain: 'merchant.myshopify.com', variantIds: ['gid://shopify/ProductVariant/1'] },
685
+ }),
686
+ ]);
687
+
688
+ const CATALOG_SAVED_SLICES_BRANCHES = Object.freeze([
689
+ catalogConditionalBranch({
690
+ id: 'list',
691
+ action: 'list',
692
+ mutates: false,
693
+ required: ['action'],
694
+ forbidden: ['shopDomain', 'label', 'variantIds', 'sliceId'],
695
+ example: { action: 'list' },
696
+ }),
697
+ catalogConditionalBranch({
698
+ id: 'save',
699
+ action: 'save',
700
+ mutates: true,
701
+ required: ['action', 'shopDomain', 'label', 'variantIds'],
702
+ forbidden: ['sliceId'],
703
+ example: {
704
+ action: 'save',
705
+ shopDomain: 'merchant.myshopify.com',
706
+ label: 'Agent watchlist',
707
+ variantIds: ['gid://shopify/ProductVariant/1'],
708
+ },
709
+ }),
710
+ ...['get', 'delete', 'refresh', 'changes'].map((action) => catalogConditionalBranch({
711
+ id: action,
712
+ action,
713
+ mutates: action === 'delete' || action === 'refresh',
714
+ required: ['action', 'sliceId'],
715
+ forbidden: ['shopDomain', 'label', 'variantIds'],
716
+ example: { action, sliceId: 'csl_saved_01234567' },
717
+ })),
718
+ ]);
719
+
720
+ const CATALOG_SNAPSHOT_INPUT_SCHEMA = conditionalToolInputSchema(
721
+ CATALOG_SNAPSHOT_INPUT_PROPERTIES,
722
+ CATALOG_SNAPSHOT_BRANCHES,
723
+ );
724
+ const CATALOG_SAVED_SLICES_INPUT_SCHEMA = conditionalToolInputSchema(
725
+ CATALOG_SAVED_SLICES_INPUT_PROPERTIES,
726
+ CATALOG_SAVED_SLICES_BRANCHES,
727
+ );
728
+ const CONDITIONAL_TOOL_BRANCHES = new Map([
729
+ ['catalog_slice_snapshot', CATALOG_SNAPSHOT_BRANCHES],
730
+ ['catalog_slices_saved', CATALOG_SAVED_SLICES_BRANCHES],
731
+ ]);
732
+
410
733
  const PUBLIC_RECEIPT_REFERENCE_INPUT_SCHEMA = Object.freeze({
411
734
  type: 'object',
412
735
  additionalProperties: false,
@@ -422,26 +745,6 @@ const PUBLIC_RECEIPT_REFERENCE_INPUT_SCHEMA = Object.freeze({
422
745
  required: ['schemaVersion', 'publicReceiptId', 'kind', 'revision', 'issuedAt', 'sha256', 'canonicalPath'],
423
746
  });
424
747
 
425
- const SHARE_ARTIFACT_REF_INPUT_SCHEMA = Object.freeze({
426
- oneOf: [
427
- PUBLIC_RECEIPT_REFERENCE_INPUT_SCHEMA,
428
- { type: 'object', additionalProperties: false, properties: { attemptId: { type: 'string', minLength: 1 } }, required: ['attemptId'] },
429
- { type: 'object', additionalProperties: false, properties: { runId: { type: 'string', minLength: 1 } }, required: ['runId'] },
430
- { type: 'object', additionalProperties: false, properties: { examId: { type: 'string', pattern: '^pex-[0-9a-f]{24}$' } }, required: ['examId'] },
431
- {
432
- type: 'object',
433
- additionalProperties: false,
434
- properties: {
435
- probeSetRunId: { type: 'string', minLength: 1 },
436
- evaluationId: { type: 'string', minLength: 1 },
437
- visibility: { type: 'string', const: 'public' },
438
- },
439
- required: ['probeSetRunId', 'evaluationId', 'visibility'],
440
- },
441
- ],
442
- description: 'Use the complete publicReceipt or impactPublicReceipt object returned by practice_run_report, publicReceipt from practice_run_impact, or one exact legacy reference: {attemptId}, {runId}, {examId}, or Gate {probeSetRunId,evaluationId,visibility:"public"}.',
443
- });
444
-
445
748
  const PRACTICE_STORE_ID_INPUT_SCHEMA = Object.freeze({
446
749
  type: 'string',
447
750
  minLength: 1,
@@ -511,8 +814,7 @@ const PRACTICE_RUN_EXTERNAL_AGENT_INPUT_SCHEMA = Object.freeze({
511
814
  name: { type: 'string', maxLength: 80 },
512
815
  source: { type: 'string', maxLength: 80 },
513
816
  version: { type: 'string', maxLength: 80 },
514
- commitSha: { type: 'string', maxLength: 80, description: 'Declared source revision. Prefer this field over the legacy commit alias.' },
515
- commit: { type: 'string', maxLength: 80, description: 'Legacy alias for commitSha.' },
817
+ commitSha: { type: 'string', maxLength: 80, description: 'Declared source revision.' },
516
818
  testCommand: { type: 'string', maxLength: 300 },
517
819
  ciStatus: { type: 'string', enum: ['unknown', 'passed', 'failed'] },
518
820
  examRole: { type: 'string', enum: ['candidate', 'negative-control'] },
@@ -559,10 +861,27 @@ const PRACTICE_RUN_UNTIL_INPUT_SCHEMA = Object.freeze({
559
861
  required: ['schemaVersion', 'maxDays', 'condition'],
560
862
  });
561
863
 
864
+ const PRACTICE_RUN_ADVANCE_CURSOR_INPUT_SCHEMA = Object.freeze({
865
+ type: 'object',
866
+ additionalProperties: false,
867
+ description: 'Copy this object unchanged from practiceRun.advanceCursor in the latest server response.',
868
+ properties: {
869
+ expectedDay: { type: 'integer', minimum: 0 },
870
+ expectedCallSeq: { type: 'integer', minimum: -1 },
871
+ },
872
+ required: ['expectedDay', 'expectedCallSeq'],
873
+ });
874
+
562
875
  const SCHEMA_VALIDATED_TOOL_NAMES = new Set([
876
+ // MEG-1164: admin_schema publishes two exclusive modes, so the oneOf has to be enforced locally —
877
+ // otherwise an ambiguous call reaches the route instead of being corrected before transport.
878
+ 'admin_schema',
879
+ 'plan_validate',
563
880
  'get_connection_details',
881
+ 'workspace_create',
564
882
  'practice_run_start',
565
883
  'practice_run_advance',
884
+ ...CONDITIONAL_TOOL_BRANCHES.keys(),
566
885
  ]);
567
886
 
568
887
  function schemaValidationError(schema, value, path = 'arguments') {
@@ -627,18 +946,32 @@ function schemaValidationError(schema, value, path = 'arguments') {
627
946
  return null;
628
947
  }
629
948
 
630
- const SHARE_CREATE_ARTIFACT_REF_NEXT_STEP = 'For a practice simulation receipt, call practice_run_report and pass its complete publicReceipt or impactPublicReceipt object unchanged; practice_run_impact returns its impact reference as publicReceipt. Other accepted artifactRef forms are receipt {attemptId}, history receipt {runId}, Shopify Exam {examId}, or Gate {probeSetRunId,evaluationId,visibility:"public"}. Correct the reference and retry share_create; no existing shareId is required.';
949
+ function conditionalToolBranch(name, args) {
950
+ const branches = CONDITIONAL_TOOL_BRANCHES.get(name);
951
+ if (!branches) return null;
952
+ const matches = branches.filter((branch) => schemaValidationError(branch.schema, args) === null);
953
+ return matches.length === 1 ? matches[0] : null;
954
+ }
631
955
 
632
- function requiredShareArtifact(args) {
633
- const artifactType = requiredString(args, 'artifactType');
634
- if (!['receipt', 'gate-evidence-export'].includes(artifactType)) {
635
- throw new Error('artifactType must be receipt or gate-evidence-export');
636
- }
637
- const artifactRef = args?.artifactRef;
638
- if (!artifactRef || typeof artifactRef !== 'object' || Array.isArray(artifactRef)) {
639
- throw new Error(SHARE_CREATE_ARTIFACT_REF_NEXT_STEP);
640
- }
641
- return { artifactType, artifactRef };
956
+ function conditionalToolFallbackBranch(name, args) {
957
+ const branches = CONDITIONAL_TOOL_BRANCHES.get(name);
958
+ if (!branches) return null;
959
+ const action = typeof args?.action === 'string' ? args.action : undefined;
960
+ return branches.find((candidate) => candidate.action === action)
961
+ ?? branches.find((candidate) => candidate.required.some((key) => args?.[key] !== undefined))
962
+ ?? branches[0];
963
+ }
964
+
965
+ export function toolInvocationMutates(name, args) {
966
+ if (!CONDITIONAL_TOOL_BRANCHES.has(name)) return TOOL_PRESENTATION[name]?.readOnlyHint === false;
967
+ const branch = conditionalToolBranch(name, args) ?? conditionalToolFallbackBranch(name, args);
968
+ return branch?.mutates === true;
969
+ }
970
+
971
+ function conditionalToolCorrection(name, args) {
972
+ const branch = conditionalToolFallbackBranch(name, args);
973
+ if (!branch) return null;
974
+ return `${name}(${JSON.stringify(branch.example)})`;
642
975
  }
643
976
 
644
977
  function queryPath(path, values) {
@@ -654,17 +987,8 @@ function requiredWorldId(args) {
654
987
  return validatedStoreId(requiredString(args, 'worldId'), 'worldId');
655
988
  }
656
989
 
657
- // MEG-256: `storeId` is the one canonical public practice-store identifier across the assistant
658
- // workflow (connection discovery → run start → receipt). `worldId` names the same identifier and
659
- // stays accepted as a bounded legacy alias so existing callers keep working.
660
990
  function requiredPracticeStoreId(args) {
661
- const provided = ['storeId', 'worldId'].filter((key) => args?.[key] !== undefined && args?.[key] !== null);
662
- if (provided.length === 0) throw new Error('storeId is required (worldId is accepted as a legacy alias for the same practice-store id)');
663
- const canonical = requiredString(args, provided[0]);
664
- if (provided.length === 2 && requiredString(args, provided[1]) !== canonical) {
665
- throw new Error('storeId and worldId name the same practice store; they were both provided with different values');
666
- }
667
- return validatedStoreId(canonical, provided[0]);
991
+ return validatedStoreId(requiredString(args, 'storeId'), 'storeId');
668
992
  }
669
993
 
670
994
  function requiredPracticeAttemptId(args, key = 'attemptId') {
@@ -787,54 +1111,17 @@ function receiptProjection(value) {
787
1111
  const practiceRun = attempt.practiceRun ?? value?.practiceRun ?? null;
788
1112
  const calls = Array.isArray(value?.events?.calls) ? value.events.calls : [];
789
1113
  const actions = Array.isArray(value?.events?.actions) ? value.events.actions : [];
790
- const capabilityMatrix = value?.receiptEvidence?.assertionCapabilities;
791
- const webhookCapability = capabilityMatrix?.schemaVersion === 'meguro.receipt-assertion-capabilities.v1'
792
- && capabilityMatrix?.families?.webhook
793
- && typeof capabilityMatrix.families.webhook === 'object'
794
- ? capabilityMatrix.families.webhook
795
- : null;
796
- const assertions = Array.isArray(value?.receiptEvidence?.assertions)
797
- ? value.receiptEvidence.assertions.slice(0, 50).map((assertion) => {
798
- if (assertion?.category !== 'webhook') {
799
- return {
800
- id: assertion.id,
801
- status: assertion.status,
802
- label: assertion.label,
803
- ...(assertion?.category === 'expected-refusal' ? { detail: assertion.detail } : {}),
804
- };
805
- }
806
- if (webhookCapability?.availability === 'available') {
807
- return { id: assertion.id, status: assertion.status, label: assertion.label, detail: assertion.detail };
808
- }
809
- if (webhookCapability?.availability === 'unavailable'
810
- && webhookCapability.statusLabel === 'not available in this run mode'
811
- && typeof webhookCapability.detail === 'string'
812
- && webhookCapability.detail.trim()) {
813
- return {
814
- id: assertion.id,
815
- status: webhookCapability.statusLabel,
816
- label: assertion.label,
817
- detail: webhookCapability.detail,
818
- };
819
- }
820
- if (webhookCapability?.availability === 'unknown'
821
- && webhookCapability.statusLabel === 'availability unknown'
822
- && typeof webhookCapability.detail === 'string'
823
- && webhookCapability.detail.trim()) {
824
- return {
825
- id: assertion.id,
826
- status: webhookCapability.statusLabel,
827
- label: assertion.label,
828
- detail: webhookCapability.detail,
829
- };
830
- }
831
- return {
832
- id: assertion.id,
833
- status: 'availability unknown',
834
- label: assertion.label,
835
- detail: 'Webhook assertion availability is unknown because this receipt has no capability metadata.',
836
- };
837
- })
1114
+ const assertions = Array.isArray(value?.receiptEvidence?.effectiveAssertions)
1115
+ ? value.receiptEvidence.effectiveAssertions.slice(0, 50).map((assertion) => ({
1116
+ id: assertion.id,
1117
+ status: assertion.statusLabel,
1118
+ observedStatus: assertion.observedStatus,
1119
+ evaluationSubject: assertion.evaluationSubject,
1120
+ rejectionResponsibleLayers: assertion.rejectionResponsibleLayers ?? [],
1121
+ ownershipStatement: assertion.ownershipStatement ?? null,
1122
+ label: assertion.label,
1123
+ detail: assertion.detail,
1124
+ }))
838
1125
  : [];
839
1126
  const verdict = value?.report?.verdict && typeof value.report.verdict === 'object'
840
1127
  ? Object.fromEntries(['status', 'outcome', 'grade', 'score', 'headline', 'horizonDays']
@@ -879,6 +1166,11 @@ function receiptProjection(value) {
879
1166
  const footprint = m?.footprint ?? null;
880
1167
  const defaultVerdict = value?.defaultVerdict ?? value?.report?.defaultVerdict ?? value?.attempt?.defaultVerdict;
881
1168
  const resultSemantics = value?.resultSemantics ?? value?.report?.resultSemantics;
1169
+ const gate = value?.gate ?? value?.report?.gate;
1170
+ const rejectionAttribution = value?.rejectionAttribution ?? value?.report?.rejectionAttribution;
1171
+ const reportBack = value?.reportBack ?? value?.report?.reportBack;
1172
+ const takeHome = value?.takeHome ?? value?.report?.takeHome;
1173
+ const eventResolution = value?.eventResolution ?? value?.report?.eventResolution;
882
1174
  return secretSafe({
883
1175
  schemaVersion: value?.schemaVersion,
884
1176
  storeIdentity: value.storeIdentity,
@@ -900,8 +1192,14 @@ function receiptProjection(value) {
900
1192
  assertions,
901
1193
  ...(value?.receiptEvidence?.expectedRefusals ? { expectedRefusals: value.receiptEvidence.expectedRefusals } : {}),
902
1194
  },
1195
+ changes: Array.isArray(value?.changes) ? value.changes : [],
903
1196
  ...(defaultVerdict && typeof defaultVerdict === 'object' ? { defaultVerdict } : {}),
904
1197
  ...(resultSemantics && typeof resultSemantics === 'object' ? { resultSemantics } : {}),
1198
+ ...(gate && typeof gate === 'object' ? { gate } : {}),
1199
+ ...(rejectionAttribution && typeof rejectionAttribution === 'object' ? { rejectionAttribution } : {}),
1200
+ ...(reportBack?.schemaVersion === 'meguro.receipt-report-back.v1' ? { reportBack } : {}),
1201
+ ...(takeHome?.schemaVersion === 'meguro.receipt-take-home.v1' ? { takeHome } : {}),
1202
+ ...(eventResolution?.schemaVersion === 'meguro.template-event-resolution.v1' ? { eventResolution } : {}),
905
1203
  ...(verdict ? { verdict } : {}),
906
1204
  ...(measurement ? { measurement } : {}),
907
1205
  ...(value?.publicReceipts?.run ? {
@@ -914,12 +1212,15 @@ function receiptProjection(value) {
914
1212
 
915
1213
  function runsListProjection(value, lifecycle) {
916
1214
  const runs = Array.isArray(value?.runs) ? value.runs : [];
917
- const visible = lifecycle ? runs.filter((run) => run?.status === lifecycle) : runs;
1215
+ if (!Number.isSafeInteger(value?.total) || value.total < 0) {
1216
+ throw new Error('Meguro runs response did not contain a valid total');
1217
+ }
918
1218
  return secretSafe({
919
1219
  runFamily: 'Shopify dev-store history runs created by run_start.',
920
1220
  excludes: 'Practice simulation attempts (pa-*) returned by practice_run_start are a separate family and are not included.',
921
1221
  lifecycleFilter: lifecycle ?? 'all',
922
- runs: visible.map((run) => {
1222
+ total: value.total,
1223
+ runs: runs.map((run) => {
923
1224
  const {
924
1225
  startedAt,
925
1226
  updatedAt,
@@ -938,7 +1239,7 @@ function runsListProjection(value, lifecycle) {
938
1239
  },
939
1240
  };
940
1241
  }),
941
- ...(visible.length === 0 ? {
1242
+ ...(runs.length === 0 ? {
942
1243
  emptyState: {
943
1244
  message: `No ${lifecycle ?? 'matching'} Shopify dev-store history runs were found. This says nothing about practice simulation attempts.`,
944
1245
  practiceAttemptRecovery: 'Call practice_runs_list to discover practice simulation attempts, then pass one exact attemptId to practice_run_report or practice_run_impact.',
@@ -1005,17 +1306,20 @@ function practiceRunReceiptStoreMismatch(receiptId, receiptStoreId, requestedSto
1005
1306
  };
1006
1307
  }
1007
1308
 
1008
- function practiceRunsProjection(value, storeId, storeSelection) {
1309
+ function practiceRunsProjection(value, storeId, storeSelection, workspaceId) {
1009
1310
  const attempts = Array.isArray(value?.playbacks) ? value.playbacks : [];
1311
+ if (!Number.isSafeInteger(value?.total) || value.total < 0) {
1312
+ throw new Error('Meguro practice runs response did not contain a valid total');
1313
+ }
1010
1314
  return secretSafe({
1011
1315
  schemaVersion: 'meguro.practice-run-list.v1',
1012
1316
  storeId,
1013
1317
  storeSelection,
1318
+ total: value.total,
1014
1319
  attempts: attempts
1015
- .filter((attempt) => attempt?.storeId === storeId || attempt?.worldId === storeId)
1016
1320
  .map((attempt) => ({
1017
1321
  attemptId: attempt.attemptId,
1018
- state: attempt.status,
1322
+ state: typeof attempt.practiceRun?.state === 'string' ? attempt.practiceRun.state : null,
1019
1323
  clock: {
1020
1324
  mode: attempt.clock?.mode ?? null,
1021
1325
  baselineDay: attempt.baselineDay ?? null,
@@ -1024,7 +1328,10 @@ function practiceRunsProjection(value, storeId, storeSelection) {
1024
1328
  elapsedSimulationDays: attempt.elapsedSimulationDays ?? null,
1025
1329
  remainingSimulationDays: attempt.remainingSimulationDays ?? null,
1026
1330
  },
1027
- receiptRef: { attemptId: attempt.attemptId },
1331
+ receiptRef: {
1332
+ attemptId: attempt.attemptId,
1333
+ ...(workspaceId ? { workspaceId } : {}),
1334
+ },
1028
1335
  })),
1029
1336
  });
1030
1337
  }
@@ -1040,8 +1347,10 @@ function practiceAttemptComparisonGuidanceFor(toolName, a, b) {
1040
1347
  type: 'text',
1041
1348
  text: JSON.stringify({
1042
1349
  code: 'practice-attempt-comparator-unavailable',
1043
- message: `${toolName} compares Shopify dev-store history runs created by run_start. A pa-* id names a practice simulation attempt, and Meguro has no cross-attempt comparator today.`,
1350
+ message: `${toolName} compares history-run identities that are not exposed by hosted MCP. A pa-* id names a practice simulation attempt, and Meguro has no cross-attempt comparator today.`,
1044
1351
  attemptIds,
1352
+ nextAction: calls.join('; '),
1353
+ stopCondition: `Use the listed practice receipt reads instead. Do not retry ${toolName} with pa-* practice attempt ids.`,
1045
1354
  workingMethod: {
1046
1355
  calls,
1047
1356
  receiptFacts: 'Read both immutable run receipts and impact receipts. Align like-named impact effectLines by key, preserve each receipt moneyBasis, and subtract the recorded values only after checking the comparison basis below.',
@@ -1105,14 +1414,32 @@ function practiceRunsStoreNotFoundGuidance(storeId) {
1105
1414
  };
1106
1415
  }
1107
1416
 
1417
+ function workspaceLifecycleIdentityGuidance(toolName, workspaceId, message) {
1418
+ const refusedId = typeof workspaceId === 'string' && workspaceId.trim()
1419
+ ? JSON.stringify(workspaceId.trim())
1420
+ : 'the missing workspaceId';
1421
+ return withOperationCommitBoundary({
1422
+ content: [{
1423
+ type: 'text',
1424
+ text: JSON.stringify({
1425
+ code: 'workspace-id-invalid',
1426
+ message: scrubString(message),
1427
+ nextAction: `Call workspaces_list({}) and use an exact current workspaceId with ${toolName}.`,
1428
+ stopCondition: `Do not retry ${toolName} with ${refusedId}; no workspace request was sent.`,
1429
+ }, null, 2),
1430
+ }],
1431
+ isError: true,
1432
+ }, 'pre-commit');
1433
+ }
1434
+
1108
1435
  function legacyGateArtifactNotFoundGuidance(runId) {
1109
1436
  return {
1110
1437
  content: [{
1111
1438
  type: 'text',
1112
1439
  text: JSON.stringify({
1113
1440
  code: 'legacy-gate-artifact-not-found',
1114
- message: `No retained legacy Gate artifact exists for ${runId}. It is not a history-run id or pa-* practice simulation attempt id.`,
1115
- nextAction: 'Call gate_verdict({}) to read the latest Receipt Gate verdict.',
1441
+ message: `No retained legacy Configured Receipt Gate artifact exists for ${runId}. It is not a history-run id or pa-* practice simulation attempt id.`,
1442
+ nextAction: 'Call gate_verdict({}) to read the latest Configured Receipt Gate verdict.',
1116
1443
  stopCondition: `Do not retry ${JSON.stringify(runId)}. The legacy artifact cannot be created from a customer surface; stop after gate_verdict({}).`,
1117
1444
  }, null, 2),
1118
1445
  }],
@@ -1120,7 +1447,7 @@ function legacyGateArtifactNotFoundGuidance(runId) {
1120
1447
  };
1121
1448
  }
1122
1449
 
1123
- function usageProjection(value) {
1450
+ function usageProjection(value, evaluatedRunStartEligibility) {
1124
1451
  const totalRuns = Number(value?.totalRuns ?? 0);
1125
1452
  const nonBillableRuns = Number(value?.nonBillableRuns ?? 0);
1126
1453
  const activityBillableRuns = Math.max(0, totalRuns - nonBillableRuns);
@@ -1131,6 +1458,17 @@ function usageProjection(value) {
1131
1458
  const activeStoreLimit = Number(meter.activeStoreLimit ?? 0);
1132
1459
  const activeWorkspaces = Number(meter.activeWorkspaces ?? 0);
1133
1460
  const activeWorkspaceLimit = Number(meter.activeWorkspaceLimit ?? 0);
1461
+ const allowance = value?.runStartAllowance;
1462
+ const hasDeclaredRunStartAllowance = allowance !== undefined;
1463
+ if (hasDeclaredRunStartAllowance && (
1464
+ !allowance
1465
+ || typeof allowance !== 'object'
1466
+ || allowance.schemaVersion !== 'meguro.run-start-allowance.v1'
1467
+ || allowance.permitsChargedRun !== meter.runStartAllowed
1468
+ || !['metering-allowance-available', 'metering-allowance-exhausted'].includes(allowance.reasonCode)
1469
+ || allowance.meaning !== 'metering-allowance-only'
1470
+ || allowance.detail !== 'Whether current tier metering allowance permits one charged simulation run; this is not overall store/root lifecycle eligibility.'
1471
+ )) throw new Error('Meguro usage response carried inconsistent run-start allowance semantics');
1134
1472
  const activityCoverageComplete = value?.coverage?.complete === true;
1135
1473
  const activityMatchesMeter = activityCoverageComplete && activityBillableRuns === consumedSimulationRuns;
1136
1474
  const reconciliationStatus = !activityCoverageComplete
@@ -1172,17 +1510,23 @@ function usageProjection(value) {
1172
1510
  activeWorkspaces,
1173
1511
  activeWorkspaceLimit,
1174
1512
  runStartAllowed: meter.runStartAllowed,
1513
+ runStartAllowedMeaning: allowance?.meaning ?? null,
1514
+ runStartAllowance: allowance ?? null,
1175
1515
  enforcement: meter.enforcement,
1176
1516
  },
1517
+ runStartEligibility: evaluatedRunStartEligibility ?? value?.runStartEligibility ?? null,
1177
1518
  coverage: value?.coverage ?? null,
1519
+ trial: value?.trial ?? null,
1178
1520
  });
1179
1521
  }
1180
1522
 
1181
1523
  function twinDiffProjection(value) {
1182
1524
  if (!value || typeof value !== 'object') return secretSafe(value);
1183
- const { generatedAt, markdown: _markdown, ...impactReceipt } = value;
1525
+ const { generatedAt, markdown: _markdown, pair, sources, ...impactReceipt } = value;
1184
1526
  return secretSafe({
1185
1527
  schemaVersion: 'meguro.mcp-twin-diff.v1',
1528
+ pair,
1529
+ sources,
1186
1530
  runClock: {
1187
1531
  generatedAtWallClock: generatedAt ?? null,
1188
1532
  note: 'This is when the impact receipt was generated; it is not Store-time.',
@@ -1194,6 +1538,34 @@ function twinDiffProjection(value) {
1194
1538
  });
1195
1539
  }
1196
1540
 
1541
+ function historyReadErrorResult(response) {
1542
+ const body = response?.json && typeof response.json === 'object' ? response.json : {};
1543
+ const unavailable = response?.status === 404;
1544
+ const meaning = typeof body.message === 'string'
1545
+ ? body.message
1546
+ : unavailable
1547
+ ? 'The requested history run is missing, expired, or not owned by this workspace.'
1548
+ : 'The history read did not complete.';
1549
+ const nextStep = typeof body.nextStep === 'string'
1550
+ ? body.nextStep
1551
+ : 'Call runs_list({ lifecycle: "completed" }) and use an exact available server-returned run ID.';
1552
+ const stopCondition = typeof body.stopCondition === 'string'
1553
+ ? body.stopCondition
1554
+ : 'Stop if runs_list does not return the requested run or both roles of one available twinPair.';
1555
+ const stable = {
1556
+ httpStatus: response?.status,
1557
+ code: typeof body.code === 'string'
1558
+ ? body.code
1559
+ : unavailable ? 'history-run-unavailable' : 'history-read-failed',
1560
+ meaning,
1561
+ nextStep,
1562
+ stopCondition,
1563
+ teaching: { meaning, nextStep, stopCondition },
1564
+ ...(body.retention ? { retention: body.retention } : {}),
1565
+ };
1566
+ return { content: [{ type: 'text', text: JSON.stringify(secretSafe(stable), null, 2) }], isError: true };
1567
+ }
1568
+
1197
1569
  const SHOPIFY_EXAM_ID = /^pex-[0-9a-f]{24}$/u;
1198
1570
  const SHOPIFY_DEV_DOMAIN = /^[a-z0-9][a-z0-9-]*\.myshopify\.com$/u;
1199
1571
  const SHOPIFY_EXAM_ACTIVE_STATUSES = new Set(['confirmed', 'materializing', 'replaying', 'comparing']);
@@ -1208,6 +1580,7 @@ const SHOPIFY_EXAM_ELIGIBILITY_GUIDANCE = Object.freeze({
1208
1580
  'temporal-clock-context-mismatch': 'Use the target practice store clock and rerun the bounded Exam source.',
1209
1581
  'temporal-clock-revision-mismatch': 'Capture one bounded burst at one Store-time coordinate; after advancing time, start a new practice run.',
1210
1582
  'interleaved-simulation-state-change': 'Rerun one bounded agent burst without stepping the store between captured calls.',
1583
+ 'target-preflight-unavailable': 'Use an exact connected Shopify development-store domain available to this workspace.',
1211
1584
  });
1212
1585
 
1213
1586
  function requiredExamId(args) {
@@ -1228,10 +1601,15 @@ function requiredShopifyDevDomain(args) {
1228
1601
 
1229
1602
  function examNextAction(exam, receipt) {
1230
1603
  const status = String(exam?.status ?? '');
1231
- if (SHOPIFY_EXAM_TERMINAL_STATUSES.has(status)) {
1232
- return status === 'completed'
1233
- ? { tool: 'exam_report', args: { examId: exam.examId }, reason: 'exam-complete' }
1234
- : { tool: 'exam_report', args: { examId: exam.examId }, reason: `exam-${status}` };
1604
+ if (status === 'cleaned' || status === 'failed') {
1605
+ return { tool: 'exam_report', args: { examId: exam.examId }, reason: `exam-${status}` };
1606
+ }
1607
+ if (status === 'completed') {
1608
+ return {
1609
+ tool: 'exam_start',
1610
+ args: { attemptId: exam.attemptId, shopDomain: exam.shopDomain },
1611
+ reason: 'cleanup-required',
1612
+ };
1235
1613
  }
1236
1614
  if (status === 'blocked') {
1237
1615
  if (receipt?.privateAvailable === true && receipt?.publicAvailable === true) {
@@ -1240,7 +1618,11 @@ function examNextAction(exam, receipt) {
1240
1618
  return { tool: 'exam_preflight', args: { attemptId: exam.attemptId, shopDomain: exam.shopDomain }, reason: 'preview-blocked' };
1241
1619
  }
1242
1620
  if (status === 'cleaning') {
1243
- return { tool: 'exam_status', args: { examId: exam.examId }, reason: 'cleanup-in-progress' };
1621
+ return {
1622
+ tool: 'exam_start',
1623
+ args: { attemptId: exam.attemptId, shopDomain: exam.shopDomain },
1624
+ reason: 'cleanup-checkpoint-complete',
1625
+ };
1244
1626
  }
1245
1627
  return {
1246
1628
  tool: 'exam_start',
@@ -1312,6 +1694,7 @@ export function createTools(config) {
1312
1694
  const headers = { accept: 'application/json' };
1313
1695
  const needsControlCredential = Boolean(options.auth || method !== 'GET');
1314
1696
  if (needsControlCredential) Object.assign(headers, controlAuthHeaders());
1697
+ if (options.workspaceId) headers['x-meguro-workspace'] = options.workspaceId;
1315
1698
  if (method !== 'GET') {
1316
1699
  headers['content-type'] = 'application/json';
1317
1700
  }
@@ -1329,7 +1712,11 @@ export function createTools(config) {
1329
1712
  const authenticationError = authenticationErrorResult(response.status, json);
1330
1713
  if (authenticationError) throw new ToolResultError(authenticationError);
1331
1714
  const message = json.errors?.[0]?.message ?? json.error ?? text.slice(0, 300);
1332
- throw new Error(`${response.status} ${path}: ${message}`);
1715
+ const failure = new Error(`${response.status} ${path}: ${message}`);
1716
+ // MEG-1054: the control plane is the authority on what a failed mutation did to committed
1717
+ // state. Carry the classification it declared instead of re-deriving one at this boundary.
1718
+ if (typeof json.operationCommitBoundary === 'string') failure.operationCommitBoundary = json.operationCommitBoundary;
1719
+ throw failure;
1333
1720
  }
1334
1721
  return json;
1335
1722
  }
@@ -1340,7 +1727,10 @@ export function createTools(config) {
1340
1727
  const headers = { accept: 'application/json', ...controlAuthHeaders() };
1341
1728
  if (options.workspaceId) headers['x-meguro-workspace'] = options.workspaceId;
1342
1729
  if (method !== 'GET') headers['content-type'] = 'application/json';
1343
- const response = await fetchImpl(`${apiBaseUrl}${path}`, {
1730
+ const bust = method === 'GET' && options.cacheBustRead === true
1731
+ ? `${path.includes('?') ? '&' : '?'}_=${Date.now()}`
1732
+ : '';
1733
+ const response = await fetchImpl(`${apiBaseUrl}${path}${bust}`, {
1344
1734
  method,
1345
1735
  headers,
1346
1736
  ...(body ? { body: JSON.stringify(body) } : {}),
@@ -1391,12 +1781,47 @@ export function createTools(config) {
1391
1781
  return { schemaVersion: 'meguro.support-code.v1', buildSha, requestIds: [safeRequestId], occurredAt, route: path, line };
1392
1782
  }
1393
1783
 
1394
- async function fleetErrorResult(toolName, path, status, value, requestId) {
1784
+ function supportRouteForFleetFailure(toolName, path) {
1785
+ if (toolName === 'catalog_slice_read') return '/stores/{shopDomain}/catalog-slice';
1786
+ if (toolName === 'catalog_slice_snapshot' && /^\/stores\/[^/]+\/catalog-slice\/snapshot(?:\?|$)/u.test(path)) {
1787
+ return '/stores/{shopDomain}/catalog-slice/snapshot';
1788
+ }
1789
+ if (toolName === 'catalog_slices_saved' && /^\/stores\/[^/]+\/catalog-slices(?:\?|$)/u.test(path)) {
1790
+ return '/stores/{shopDomain}/catalog-slices';
1791
+ }
1792
+ return path;
1793
+ }
1794
+
1795
+ function catalogConnectionTeaching(toolName, path, allowedShopDomains, hasAllowedShopDomainAuthority) {
1796
+ if (!hasAllowedShopDomainAuthority) return null;
1797
+ const liveCatalogStoreAction = toolName === 'catalog_slice_read'
1798
+ || (toolName === 'catalog_slice_snapshot' && /^\/stores\/[^/]+\/catalog-slice\/snapshot(?:\?|$)/u.test(path))
1799
+ || (toolName === 'catalog_slices_saved' && /^\/stores\/[^/]+\/catalog-slices(?:\?|$)/u.test(path));
1800
+ if (!liveCatalogStoreAction) return null;
1801
+ if (!allowedShopDomains.length) {
1802
+ return {
1803
+ meaning: 'No catalog-readable Shopify development-store domain is connected for this workspace, so the requested operation was not applied.',
1804
+ nextStep: `The allowedShopDomains field below is empty. Stop: no catalog-readable Shopify development-store domain is connected; connect one in Console before trying ${toolName} again.`,
1805
+ };
1806
+ }
1807
+ return {
1808
+ meaning: 'The requested Shopify development-store domain is not connected or allowed for this workspace, so the requested operation was not applied.',
1809
+ nextStep: toolName === 'catalog_slice_read'
1810
+ ? `The allowedShopDomains field below is the authoritative connected-domain discovery result. Call catalog_slice_read({ shopDomain: ${JSON.stringify(allowedShopDomains[0])} }) with one exact value from that field. Do not retry the refused domain.`
1811
+ : `The allowedShopDomains field below is the authoritative connected-domain discovery result. Call ${toolName} again with shopDomain: ${JSON.stringify(allowedShopDomains[0])} and the same valid non-domain arguments. Do not retry the refused domain.`,
1812
+ };
1813
+ }
1814
+
1815
+ async function fleetErrorResult(toolName, path, status, value, requestId, invocationMutates) {
1395
1816
  const body = value && typeof value === 'object' ? value : {};
1817
+ const declaredBoundary = ['pre-commit', 'committed', 'unknown'].includes(body.operationCommitBoundary)
1818
+ ? body.operationCommitBoundary
1819
+ : null;
1396
1820
  const sourceErrors = Array.isArray(body.errors) ? body.errors.slice(0, 5) : [];
1397
1821
  const allowedShopDomains = Array.isArray(body.allowed)
1398
1822
  ? [...new Set(body.allowed.map((domain) => String(domain).trim().toLowerCase()).filter((domain) => SHOPIFY_DEV_DOMAIN.test(domain)))]
1399
1823
  : [];
1824
+ const catalogConnection = catalogConnectionTeaching(toolName, path, allowedShopDomains, Array.isArray(body.allowed));
1400
1825
  const workspaceLimit = body.code === 'active_workspace_limit_reached';
1401
1826
  const rawError = typeof body.error === 'string' ? body.error : '';
1402
1827
  const routeCode = typeof body.code === 'string'
@@ -1408,31 +1833,80 @@ export function createTools(config) {
1408
1833
  const claimCodeError = toolName === 'store_claim_by_code'
1409
1834
  && ['invalid-claim-code', 'claim-code-unavailable', 'claim-code-busy'].includes(routeCode);
1410
1835
  const sourceErrorText = sourceErrors.map((error) => String(error?.message ?? '')).join(' ');
1411
- const resourceTeaching = (toolName === 'workspace_archive' || toolName === 'workspace_unarchive') && status === 404
1836
+ const settledFleetMutation = new Set([
1837
+ 'store_create',
1838
+ 'store_delete',
1839
+ 'workspace_create',
1840
+ 'workspace_archive',
1841
+ 'workspace_unarchive',
1842
+ 'store_claim_by_code',
1843
+ ]);
1844
+ const settledPreCommitRefusal = status >= 400 && status < 500
1845
+ && settledFleetMutation.has(toolName);
1846
+ // These route/code pairs are authority-bearing pre-write refusals: template resolution,
1847
+ // target lookup, and claim-code resolution all finish before their write-capable phase. A
1848
+ // transport failure or any unrecognized response remains undeclared and reaches the hosted
1849
+ // resource's single `unknown` fallback.
1850
+ const authoritativeBoundary = declaredBoundary ?? (settledPreCommitRefusal ? 'pre-commit' : null);
1851
+ const effectiveBoundary = authoritativeBoundary ?? (invocationMutates ? 'unknown' : null);
1852
+ const hasInvocationBoundaryAuthority = invocationMutates !== undefined || authoritativeBoundary !== null;
1853
+ const operationMeaning = effectiveBoundary === 'pre-commit'
1854
+ ? 'The requested fleet operation was not applied.'
1855
+ : effectiveBoundary === 'committed'
1856
+ ? 'The requested fleet operation applied to committed state, but the response did not complete.'
1857
+ : effectiveBoundary === 'unknown'
1858
+ ? 'The requested fleet operation did not complete with a confirmed commit outcome.'
1859
+ : invocationMutates === false
1860
+ ? 'The requested fleet read did not complete.'
1861
+ : 'The requested fleet operation was not applied.';
1862
+ const workspaceLifecycleTool = toolName === 'workspace_archive' || toolName === 'workspace_unarchive';
1863
+ const resourceTeaching = toolName === 'workspace_archive' && status === 400 && routeCode === 'default-workspace'
1864
+ ? {
1865
+ meaning: 'The default workspace cannot be archived. The requested lifecycle operation was not applied.',
1866
+ nextStep: 'Call workspaces_list({}), select an active workspace with isDefault: false, then call workspace_archive({ workspaceId: "<exact active non-default workspaceId>" }). If no active non-default workspace exists, stop and do not retry the default workspaceId.',
1867
+ }
1868
+ : workspaceLifecycleTool && status === 409 && routeCode === 'workspace-deleting'
1869
+ ? {
1870
+ meaning: 'The workspace lifecycle transition is already underway. Do not retry the mutation.',
1871
+ nextStep: `Wait for the lifecycle transition to finish, then call workspaces_list({}) to read the terminal workspace state. Stop and do not retry ${toolName} while the workspace is deleting.`,
1872
+ }
1873
+ : workspaceLifecycleTool && status === 404
1412
1874
  ? {
1413
1875
  meaning: 'The requested workspace is not available in this account. Workspace lifecycle tools require a workspaceId; a storeId names a different resource and is never a substitute.',
1414
1876
  nextStep: `Call workspaces_list({}), choose an exact workspaceId from the returned account workspaces, then retry ${toolName}({ workspaceId: "<exact workspaceId>" }). If the intended workspace is absent, stop and do not retry.`,
1415
1877
  }
1878
+ : toolName === 'template_get' && status === 404
1879
+ ? {
1880
+ // MEG-1058: templates_list is the complete catalog, so an unknown key is settled, not
1881
+ // transient. Teach the discovery step and stop rather than inviting a retry.
1882
+ meaning: 'The requested templateKey is not in the authoritative practice-store template catalog.',
1883
+ nextStep: 'Call templates_list({}), choose an exact key from the returned storeTemplates, then call template_get({ templateKey: "<exact key>" }). If the intended template is absent, stop and do not retry the same key.',
1884
+ }
1416
1885
  : toolName === 'store_create' && status === 400 && /template/iu.test(`${rawError} ${routeMessage ?? ''} ${sourceErrorText}`)
1417
1886
  ? {
1418
1887
  meaning: 'The requested templateKey is not in the authoritative practice-store template catalog.',
1419
1888
  nextStep: 'Call templates_list({}), choose an exact key from the returned storeTemplates, then call store_create({ templateKey: "<exact key>" }). If the catalog is empty or the intended template is absent, stop and do not retry.',
1420
1889
  }
1421
- : null;
1890
+ : toolName === 'catalog_slice_snapshot' && status === 404
1891
+ && /^\/catalog-slices\/[^/]+\/snapshot(?:\?|$)/u.test(path)
1892
+ ? {
1893
+ meaning: 'The requested saved catalog slice is not in the current workspace.',
1894
+ nextStep: 'Call catalog_slices_saved({ action: "list" }), choose an exact sliceId from that result, and retry only the failed action. If the intended slice is absent, stop and do not retry the unavailable sliceId.',
1895
+ }
1896
+ : toolName === 'catalog_slices_saved' && status === 404
1897
+ ? {
1898
+ meaning: 'The requested saved catalog slice is not in the current workspace.',
1899
+ nextStep: 'Call catalog_slices_saved({ action: "list" }), choose an exact sliceId from that result, and retry only the failed action. If the intended slice is absent, stop and do not retry the unavailable sliceId.',
1900
+ }
1901
+ : null;
1422
1902
  const defaultNextStep = resourceTeaching
1423
1903
  ? resourceTeaching.nextStep
1424
1904
  : claimCodeError
1425
1905
  ? routeCode === 'claim-code-busy'
1426
1906
  ? 'Wait for the current connection update to finish, then reopen Meguro in Shopify Admin before retrying once.'
1427
1907
  : 'Reopen Meguro in Shopify Admin and use the current one-time claim code shown for the intended development store.'
1428
- : toolName === 'share_create'
1429
- ? SHARE_CREATE_ARTIFACT_REF_NEXT_STEP
1430
- : toolName.startsWith('share_') || toolName === 'shares_list'
1431
- ? 'Call shares_list or share_status, inspect the current private/outward lifecycle state, and retry only the permitted transition.'
1432
- : toolName === 'catalog_slice_read'
1433
- ? allowedShopDomains.length
1434
- ? `The allowedShopDomains field below is the authoritative connected-domain discovery result. Call catalog_slice_read({ shopDomain: ${JSON.stringify(allowedShopDomains[0])} }) with one exact value from that field. Do not retry the refused domain.`
1435
- : 'The allowedShopDomains field below is empty. Stop: no catalog-readable Shopify development-store domain is connected; connect one in Console before trying catalog_slice_read again.'
1908
+ : toolName === 'catalog_slice_read'
1909
+ ? catalogConnection?.nextStep ?? 'Read the current catalog slice or saved-slice state, correct the exact store, slice, or variant ids, and retry only the failed action.'
1436
1910
  : toolName.startsWith('catalog_')
1437
1911
  ? 'Read the current catalog slice or saved-slice state, correct the exact store, slice, or variant ids, and retry only the failed action.'
1438
1912
  : toolName === 'store_claim'
@@ -1440,47 +1914,51 @@ export function createTools(config) {
1440
1914
  : 'Call stores_list, inspect the current fleet, and retry with a current store id.';
1441
1915
  const errors = sourceErrors.length ? sourceErrors.map((error) => ({
1442
1916
  message: scrubString(error?.message ?? 'Meguro fleet request failed.'),
1443
- meaning: scrubString(resourceTeaching?.meaning ?? error?.meaning ?? 'The requested fleet operation was not applied.'),
1444
- nextStep: scrubString(resourceTeaching?.nextStep ?? error?.nextStep ?? defaultNextStep),
1917
+ meaning: scrubString(resourceTeaching?.meaning
1918
+ ?? catalogConnection?.meaning
1919
+ ?? (hasInvocationBoundaryAuthority ? operationMeaning : error?.meaning ?? operationMeaning)),
1920
+ nextStep: scrubString(resourceTeaching?.nextStep ?? catalogConnection?.nextStep ?? error?.nextStep ?? defaultNextStep),
1445
1921
  })) : [{
1446
1922
  message: scrubString(routeMessage ?? (typeof value === 'string' ? value : 'Meguro fleet request failed.')),
1447
1923
  meaning: resourceTeaching
1448
1924
  ? resourceTeaching.meaning
1925
+ : catalogConnection
1926
+ ? catalogConnection.meaning
1449
1927
  : workspaceLimit
1450
1928
  ? 'The account is at its active-workspace limit, so the requested workspace operation was not applied.'
1451
1929
  : claimCodeError
1452
1930
  ? 'The one-time Shopify development-store claim was not applied.'
1453
- : 'The requested fleet operation was not applied.',
1931
+ : operationMeaning,
1454
1932
  nextStep: resourceTeaching
1455
1933
  ? resourceTeaching.nextStep
1934
+ : catalogConnection
1935
+ ? catalogConnection.nextStep
1456
1936
  : workspaceLimit
1457
- ? 'Archive an active workspace or open Plans & Billing at #upgrade.'
1937
+ ? String(body.sameTierAction?.label ?? routeMessage ?? defaultNextStep)
1458
1938
  : defaultNextStep,
1459
1939
  }];
1460
1940
  const stable = secretSafe({
1461
1941
  tool: toolName,
1462
1942
  httpStatus: status,
1463
1943
  ...(routeCode ? { code: routeCode } : {}),
1464
- ...(typeof body.blockedCapability === 'string' ? { blockedCapability: body.blockedCapability } : {}),
1465
- ...(typeof body.tier === 'string' ? { tier: body.tier } : {}),
1944
+ ...enforcementProjection(body),
1466
1945
  ...(Number.isFinite(body.activeStores) ? { activeStores: body.activeStores } : {}),
1467
1946
  ...(Number.isFinite(body.activeStoreLimit) ? { activeStoreLimit: body.activeStoreLimit } : {}),
1468
1947
  ...(Number.isFinite(body.activeWorkspaces) ? { activeWorkspaces: body.activeWorkspaces } : {}),
1469
1948
  ...(Number.isFinite(body.activeWorkspaceLimit) ? { activeWorkspaceLimit: body.activeWorkspaceLimit } : {}),
1470
- ...(typeof body.upgradePath === 'string' ? { upgradePath: body.upgradePath } : {}),
1471
1949
  errors,
1472
- ...(toolName === 'catalog_slice_read' ? { allowedShopDomains } : {}),
1950
+ ...(catalogConnection ? { allowedShopDomains } : {}),
1473
1951
  ...(Array.isArray(body.fleet) ? { fleet: body.fleet } : {}),
1474
- supportCode: await fleetSupportCode(path, requestId),
1952
+ supportCode: await fleetSupportCode(supportRouteForFleetFailure(toolName, path), requestId),
1475
1953
  });
1476
- return { content: [{ type: 'text', text: JSON.stringify(stable, null, 2) }], isError: true };
1954
+ const result = { content: [{ type: 'text', text: JSON.stringify(stable, null, 2) }], isError: true };
1955
+ return authoritativeBoundary ? withOperationCommitBoundary(result, authoritativeBoundary) : result;
1477
1956
  }
1478
1957
 
1479
1958
  async function historyRunTeachingErrorResult(toolName, path, response, requestedId) {
1480
1959
  const body = response.json && typeof response.json === 'object' ? response.json : {};
1481
1960
  const rawMessage = String(body.errors?.[0]?.message ?? body.error ?? body.message ?? 'Meguro history-run request failed.');
1482
1961
  if (toolName === 'run_start' && response.status === 400 && /not allowed|not connected/iu.test(rawMessage)) {
1483
- const requestedDomain = String(body.requestedShopDomain ?? requestedId ?? '').trim().toLowerCase();
1484
1962
  const allowedShopDomains = Array.isArray(body.allowed)
1485
1963
  ? [...new Set(body.allowed.map((domain) => String(domain).trim().toLowerCase()).filter((domain) => SHOPIFY_DEV_DOMAIN.test(domain)))]
1486
1964
  : [];
@@ -1493,10 +1971,10 @@ export function createTools(config) {
1493
1971
  error: { message: rawMessage },
1494
1972
  allowedShopDomains,
1495
1973
  teaching: {
1496
- meaning: `${requestedDomain || 'The requested shop domain'} is not connected or allowed for this account, so no Shopify dev-store history run was started.`,
1974
+ meaning: 'The requested shop domain is not connected or allowed for this account, so no Shopify dev-store history run was started.',
1497
1975
  nextStep,
1498
1976
  stopCondition: allowedShopDomains.length
1499
- ? `Do not retry ${requestedDomain || 'the refused domain'}; use only an exact value from allowedShopDomains.`
1977
+ ? 'Do not retry the refused domain; use only an exact value from allowedShopDomains.'
1500
1978
  : 'Stop until Console exposes a connected Shopify development-store domain.',
1501
1979
  },
1502
1980
  supportCode: await fleetSupportCode(path, response.requestId),
@@ -1524,14 +2002,22 @@ export function createTools(config) {
1524
2002
  const response = await apiRaw(method, path, body, options);
1525
2003
  if (!response.ok) {
1526
2004
  const authenticationError = authenticationErrorResult(response.status, response.json);
1527
- return { error: authenticationError ?? await fleetErrorResult(toolName, path, response.status, response.json, response.requestId) };
2005
+ return { error: authenticationError ?? await fleetErrorResult(
2006
+ toolName,
2007
+ path,
2008
+ response.status,
2009
+ response.json,
2010
+ response.requestId,
2011
+ options.invocationMutates,
2012
+ ) };
1528
2013
  }
1529
2014
  return { value: response.json };
1530
2015
  }
1531
2016
 
1532
- async function practiceApi(method, path, body) {
2017
+ async function practiceApi(method, path, body, options = {}) {
1533
2018
  if (!practiceApiToken) throw new Error('MEGURO_API_TOKEN is required for practice-run tools');
1534
2019
  const headers = { accept: 'application/json', authorization: `Bearer ${practiceApiToken}` };
2020
+ if (options.workspaceId) headers['x-meguro-workspace'] = options.workspaceId;
1535
2021
  if (method !== 'GET') headers['content-type'] = 'application/json';
1536
2022
  let response;
1537
2023
  try {
@@ -1551,23 +2037,20 @@ export function createTools(config) {
1551
2037
  return { ok: response.ok, status: response.status, json, retryAfterSeconds };
1552
2038
  }
1553
2039
 
1554
- async function examEligibility(attemptId) {
1555
- const response = await practiceApi('GET', `/practice/playbacks/${encodeURIComponent(attemptId)}/report`);
1556
- const attempt = response.json?.attempt ?? response.json?.report?.attempt;
1557
- const eligibility = response.ok
1558
- ? attempt?.practiceExamEligibility
1559
- : null;
1560
- if (!response.ok || !eligibility || typeof eligibility !== 'object') {
2040
+ function examEligibility(source) {
2041
+ if (source.code !== 'exam-ineligible') return null;
2042
+ const codes = Array.isArray(source.blockerCodes)
2043
+ ? source.blockerCodes.map(String)
2044
+ : typeof source.blockerCode === 'string' ? [source.blockerCode] : [];
2045
+ if (source.retryable === true && codes.length === 0) {
1561
2046
  return {
1562
2047
  state: 'undetermined',
1563
- codes: [],
2048
+ codes,
1564
2049
  retryable: true,
1565
2050
  nextStep: 'Eligibility evidence is undetermined, not negative. Retry preflight; if the evidence read remains unavailable, use the support code.',
1566
2051
  };
1567
2052
  }
1568
- const codes = Array.isArray(eligibility.codes) ? eligibility.codes.map(String) : [];
1569
2053
  const steps = [...new Set(codes.map((code) => SHOPIFY_EXAM_ELIGIBILITY_GUIDANCE[code]).filter(Boolean))];
1570
- const storeId = String(attempt?.storeId ?? attempt?.worldId ?? '').trim();
1571
2054
  const mustRecapture = codes.some((code) => [
1572
2055
  'tape-missing',
1573
2056
  'tape-total-over-limit',
@@ -1579,11 +2062,9 @@ export function createTools(config) {
1579
2062
  'temporal-clock-revision-mismatch',
1580
2063
  'interleaved-simulation-state-change',
1581
2064
  ].includes(code));
1582
- const recaptureStep = `This immutable attempt is ineligible and cannot be repaired. Do not retry this immutable attempt. Call practice_run_start({ storeId: ${JSON.stringify(storeId || '<same practice-store id>')} }) to create a new simulation attempt; run one bounded capture through the current Connect or package-free CI path using Shopify Admin API 2026-04 at one Store-time coordinate without advancing the store between captured calls; finish it; then call exam_preflight with the new attemptId and exact connected development-store domain.`;
2065
+ const recaptureStep = 'This immutable attempt is ineligible and cannot be repaired. Do not retry this immutable attempt. Call practice_run_start for the same practice store to create a new simulation attempt; run one bounded capture through the current Connect or package-free CI path using Shopify Admin API 2026-04 at one Store-time coordinate without advancing the store between captured calls; finish it; then call exam_preflight with the new attemptId and exact connected development-store domain.';
1583
2066
  return {
1584
- state: eligibility.eligible === true && codes.length === 0 ? 'eligible' : 'blocked',
1585
- schemaVersion: eligibility.schemaVersion,
1586
- replayDisposition: eligibility.replayDisposition,
2067
+ state: 'blocked',
1587
2068
  codes,
1588
2069
  retryable: false,
1589
2070
  nextStep: mustRecapture
@@ -1598,20 +2079,23 @@ export function createTools(config) {
1598
2079
  const source = response.json?.examError && typeof response.json.examError === 'object'
1599
2080
  ? response.json.examError
1600
2081
  : { code: 'exam-request-failed', message: 'The Shopify Exam request failed safely.', retryable: response.status >= 500 };
1601
- const eligibility = source.code === 'exam-ineligible' && attemptId
1602
- ? await examEligibility(attemptId)
1603
- : null;
1604
- const retryable = eligibility?.state === 'undetermined'
1605
- ? true
1606
- : source.retryable === true;
2082
+ const eligibility = examEligibility(source);
2083
+ const retryable = source.retryable === true;
1607
2084
  const startPairNotFound = toolName === 'exam_start' && source.code === 'not_found';
1608
- const nextStep = startPairNotFound
2085
+ const preflightPairNotFound = toolName === 'exam_preflight' && source.code === 'not_found';
2086
+ const examIdentityNotFound = (toolName === 'exam_status' || toolName === 'exam_report')
2087
+ && source.code === 'not_found';
2088
+ const nextStep = startPairNotFound || preflightPairNotFound
1609
2089
  ? 'Use a completed attemptId returned by practice_run_start and the exact connected .myshopify.com domain shown in Console. Call exam_preflight({ attemptId: "<completed attemptId>", shopDomain: "<exact connected .myshopify.com domain>" }); only when it reports eligible, call exam_start({ attemptId: "<same completed attemptId>", shopDomain: "<same exact connected .myshopify.com domain>" }).'
2090
+ : examIdentityNotFound
2091
+ ? 'Call practice_runs_list({}), choose a completed attemptId, call exam_preflight with that attemptId and an exact connected .myshopify.com domain, then call exam_start. Use only the examId returned by exam_start with this tool.'
1610
2092
  : eligibility?.nextStep
1611
2093
  ?? (retryable
1612
- ? 'Read exam_status before retrying so an ambiguous delivery is never replayed blindly.'
2094
+ ? toolName === 'exam_preflight'
2095
+ ? 'Retry exam_preflight with the same attemptId and shopDomain after the transient condition clears. Preflight creates no Exam identity; do not call exam_status unless exam_start later returns an examId.'
2096
+ : 'Read exam_status before retrying so an ambiguous delivery is never replayed blindly.'
1613
2097
  : 'Correct the stated Exam requirement, then retry only the failed bounded step.');
1614
- return {
2098
+ const result = {
1615
2099
  content: [{
1616
2100
  type: 'text',
1617
2101
  text: JSON.stringify(secretSafe({
@@ -1625,19 +2109,28 @@ export function createTools(config) {
1625
2109
  codes: eligibility.codes,
1626
2110
  } } : {}),
1627
2111
  teaching: {
1628
- meaning: startPairNotFound
2112
+ meaning: startPairNotFound || preflightPairNotFound
1629
2113
  ? 'This tenant-safe not-found refusal means the requested practice attempt and connected development-store domain pair is unavailable to this account; Meguro does not reveal which member of the pair was absent.'
2114
+ : examIdentityNotFound
2115
+ ? 'No Shopify Exam with this examId is available to this account. Exam identities are created only by exam_start.'
1630
2116
  : source.code === 'exam-ineligible'
1631
2117
  ? 'Shopify Exam accepts only a completed, graded, immutable captured attempt whose replay evidence is current and supported.'
1632
2118
  : 'The requested bounded Shopify Exam step was not assumed complete.',
1633
2119
  nextStep,
1634
- ...(startPairNotFound ? { stopCondition: 'If a completed attemptId or exact connected domain is unavailable, stop. Do not retry exam_start with the refused pair.' } : {}),
2120
+ ...(startPairNotFound || preflightPairNotFound
2121
+ ? { stopCondition: `If a completed attemptId or exact connected domain is unavailable, stop. Do not retry ${toolName} with the refused pair.` }
2122
+ : examIdentityNotFound
2123
+ ? { stopCondition: `If exam_start did not return this examId, stop. Do not retry ${toolName} with the unavailable examId.` }
2124
+ : {}),
1635
2125
  },
1636
2126
  supportCode: await fleetSupportCode(path, response.requestId),
1637
2127
  }), null, 2),
1638
2128
  }],
1639
2129
  isError: true,
1640
2130
  };
2131
+ return startPairNotFound || preflightPairNotFound
2132
+ ? withOperationCommitBoundary(result, 'pre-commit')
2133
+ : result;
1641
2134
  }
1642
2135
 
1643
2136
  async function examRequest(toolName, method, path, body, attemptId) {
@@ -1720,8 +2213,13 @@ export function createTools(config) {
1720
2213
  description: DOCUMENTATION_TOOL_CONTRACT.topicDescription,
1721
2214
  },
1722
2215
  version: {
1723
- type: 'integer',
1724
- enum: DOCUMENTATION_TOOL_CONTRACT.versions,
2216
+ // MEG-1059: an exact published integer, or the "latest" sentinel. The historical enum is
2217
+ // withdrawn — it grew with every mint and told a caller nothing about which topic
2218
+ // publishes which version.
2219
+ anyOf: [
2220
+ { type: 'integer', minimum: 1 },
2221
+ { type: 'string', enum: ['latest'] },
2222
+ ],
1725
2223
  description: DOCUMENTATION_TOOL_CONTRACT.versionDescription,
1726
2224
  },
1727
2225
  },
@@ -1730,7 +2228,7 @@ export function createTools(config) {
1730
2228
  },
1731
2229
  {
1732
2230
  name: 'templates_list',
1733
- description: 'List the full ordered, public-safe practice-store template catalog available to store_create; it is not filtered by stores already in the fleet. starterRecommendation names the template keys recommended for a first simulation and declares that basis as starter-for-first-simulation; its server-owned starterPlan states the authenticated account tier, the ideal run length, the default store arc, the exact first practice_run_start clock, current run-start availability, and whether continuation exists. The matching recommended boolean marks those templates, and recommendedRunDays remains each template\'s ideal simulation length. The response is derived only from the bounded storeTemplates and starterRecommendation projection: it excludes customer profiles, scenario presets, credentials, and private route payload. An OAuth grant may select an owned non-default workspace; an API key remains bound to its own workspace.',
2231
+ description: 'List the complete ordered, public-safe practice-store template catalog available to store_create; it is not filtered by stores already in the fleet. Each row carries only what a cold agent needs to choose — key, label, category, recommended, bestFor and recommendedRunDays. Call template_get({ templateKey }) for one template\'s complete record: supported reads and writes, operation contracts, unsupported contracts, capability boundaries, modeled evidence and physics, and rubric fields. starterRecommendation names every server-recommended template and provides a server-owned starterPlans entry for every selectable template. Each plan states the authenticated account tier, ideal horizon, exact first practice_run_start clock, run-start availability, and continuation. While the one account onboarding grant is available, every template is recommendable and its plan carries that authorization; afterward the same plans apply ordinary tier policy. If no executable recommendation is available, the catalog remains complete and starterRecommendation.recommendationUnavailable explains why. The matching recommended boolean follows the server declaration, and recommendedRunDays remains each template\'s ideal simulation length. The response excludes customer profiles, scenario presets, credentials, and private route payload. An OAuth grant may select an owned non-default workspace; an API key remains bound to its own workspace.',
1734
2232
  inputSchema: {
1735
2233
  type: 'object',
1736
2234
  additionalProperties: false,
@@ -1740,6 +2238,19 @@ export function createTools(config) {
1740
2238
  required: [],
1741
2239
  },
1742
2240
  },
2241
+ {
2242
+ name: 'template_get',
2243
+ description: 'Read the complete public-safe record for exactly one practice-store template: the compact catalog fields plus what the agent sees, timing, supported reads and writes, modeled evidence and physics, operation contracts, unsupported contracts, capability boundaries, and rubric fields. templates_list is the discovery step and carries only the fields needed to choose; this is the detail step and carries everything needed to use the chosen template. Both are projected from the same compiled catalog authority. An unknown templateKey returns a typed not-found response naming templates_list; it creates nothing and is never worth retrying with the same key. The response excludes customer profiles, scenario presets, credentials, and private route payload. An OAuth grant may select an owned non-default workspace; an API key remains bound to its own workspace.',
2244
+ inputSchema: {
2245
+ type: 'object',
2246
+ additionalProperties: false,
2247
+ properties: {
2248
+ templateKey: { type: 'string', minLength: 1, description: 'Exact template key from templates_list storeTemplates[].key.' },
2249
+ workspaceId: { type: 'string', minLength: 1, description: 'Optional OAuth selector for an account-owned non-default workspace. A workspace API key may omit it or repeat only its own binding.' },
2250
+ },
2251
+ required: ['templateKey'],
2252
+ },
2253
+ },
1743
2254
  {
1744
2255
  name: 'stores_list',
1745
2256
  description: 'List one workspace\'s practice-store fleet with ids, names, templates, generated-history span, readiness, active-run state, and deletion context. An OAuth grant is account-bound: omit workspaceId for the default workspace or provide an owned non-default workspace id. An API key remains bound to its own workspace.',
@@ -1753,7 +2264,7 @@ export function createTools(config) {
1753
2264
  },
1754
2265
  {
1755
2266
  name: 'store_create',
1756
- description: 'Create a populated practice store from any templateKey returned by templates_list, including a template already represented in the fleet, in the selected workspace. An OAuth grant is account-bound: omit workspaceId for the default workspace or provide an owned non-default workspace id. An API key remains bound to its own workspace. At the active-store limit, the server teaching error includes the current fleet.',
2267
+ description: 'Create a populated practice store from any templateKey returned by templates_list, including a template already represented in the fleet, in the selected workspace. The server gives ordinary stores 120 days of queryable history by default. An OAuth grant is account-bound: omit workspaceId for the default workspace or provide an owned non-default workspace id. An API key remains bound to its own workspace. At the active-store limit, the server teaching error includes the current fleet.',
1757
2268
  inputSchema: {
1758
2269
  type: 'object', additionalProperties: false,
1759
2270
  properties: {
@@ -1827,70 +2338,6 @@ export function createTools(config) {
1827
2338
  required: ['workspaceId'],
1828
2339
  },
1829
2340
  },
1830
- {
1831
- name: 'share_create',
1832
- description: 'Create a private evidence-share draft, or update its title/client-viewer audience while it remains draft or changes-requested. For a practice simulation receipt, pass the complete publicReceipt or impactPublicReceipt object from practice_run_report unchanged as artifactRef; practice_run_impact returns its impact reference as publicReceipt. The schema also names every legacy receipt, Shopify Exam, and Gate reference form. This owner-only OAuth tool does not expose evidence by itself: call share_publish for the outward-facing transition. Omit shareId to create with title, artifactType, artifactRef, and audienceUserIds; provide shareId to update only title and/or audienceUserIds. OAuth may select an owned workspace.',
1833
- inputSchema: {
1834
- type: 'object', additionalProperties: false,
1835
- properties: {
1836
- workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace selector. Omit for Default.' },
1837
- shareId: { type: 'string', pattern: '^[0-9a-f]{32}$', description: 'Existing draft/changes-requested share to update. Omit to create.' },
1838
- title: { type: 'string', minLength: 1, maxLength: 120 },
1839
- artifactType: { type: 'string', enum: ['receipt', 'gate-evidence-export'], description: 'Required only when creating.' },
1840
- artifactRef: SHARE_ARTIFACT_REF_INPUT_SCHEMA,
1841
- audienceUserIds: { type: 'array', maxItems: 40, items: { type: 'string', minLength: 1 }, description: 'Active client-viewer user ids assigned to this exact workspace.' },
1842
- },
1843
- required: [],
1844
- },
1845
- },
1846
- {
1847
- name: 'shares_list',
1848
- description: 'List private and outward evidence-share lifecycle rows in the selected workspace. This owner-only OAuth read does not expose evidence or mint viewer access. Use share_status for the exact artifact reference and audience ids.',
1849
- inputSchema: {
1850
- type: 'object', additionalProperties: false,
1851
- properties: {
1852
- workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace selector. Omit for Default.' },
1853
- },
1854
- required: [],
1855
- },
1856
- },
1857
- {
1858
- name: 'share_status',
1859
- description: 'Read one exact evidence share, including its immutable public artifact reference, selected client-viewer ids, state, and revocation status. This owner-only OAuth read has no outward effect.',
1860
- inputSchema: {
1861
- type: 'object', additionalProperties: false,
1862
- properties: {
1863
- workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace selector. Omit for Default.' },
1864
- shareId: { type: 'string', pattern: '^[0-9a-f]{32}$', description: 'Exact share id from shares_list.' },
1865
- },
1866
- required: ['shareId'],
1867
- },
1868
- },
1869
- {
1870
- name: 'share_publish',
1871
- description: 'Outward-facing action: publish exposes the share artifact to its selected client viewers; re-share refreshes that exposure after a viewer requested changes. This is not a private action. Use action=publish only for a draft, or action=re-share only after share_status reports changes-requested.',
1872
- inputSchema: {
1873
- type: 'object', additionalProperties: false,
1874
- properties: {
1875
- workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace selector. Omit for Default.' },
1876
- shareId: { type: 'string', pattern: '^[0-9a-f]{32}$', description: 'Exact share id from share_status.' },
1877
- action: { type: 'string', enum: ['publish', 're-share'], description: 'Exact server lifecycle transition. Defaults to publish.' },
1878
- },
1879
- required: ['shareId'],
1880
- },
1881
- },
1882
- {
1883
- name: 'share_revoke',
1884
- description: 'Revoke an evidence share and withdraw client-viewer access. This outward-facing destructive permission change is idempotent and retains the owner-visible share lifecycle record.',
1885
- inputSchema: {
1886
- type: 'object', additionalProperties: false,
1887
- properties: {
1888
- workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace selector. Omit for Default.' },
1889
- shareId: { type: 'string', pattern: '^[0-9a-f]{32}$', description: 'Exact share id from shares_list.' },
1890
- },
1891
- required: ['shareId'],
1892
- },
1893
- },
1894
2341
  {
1895
2342
  name: 'catalog_slice_read',
1896
2343
  description: 'Read up to 25 variants from one connected Shopify development store through the existing live catalog-slice route. This is an external Shopify read with no Shopify or Meguro mutation. OAuth may select an owned workspace.',
@@ -1908,32 +2355,12 @@ export function createTools(config) {
1908
2355
  {
1909
2356
  name: 'catalog_slice_snapshot',
1910
2357
  description: 'Mint a short-lived signed catalog snapshot capability from either a live Shopify selection or one saved catalog slice. For a live external read provide shopDomain plus 1–25 variantIds; for a saved slice provide only sliceId. The capability remains bound to the selected workspace and store.',
1911
- inputSchema: {
1912
- type: 'object', additionalProperties: false,
1913
- properties: {
1914
- workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace selector. Omit for Default.' },
1915
- shopDomain: { type: 'string', pattern: '^[a-z0-9][a-z0-9-]*\\.myshopify\\.com$', description: 'Live snapshot source. Mutually exclusive with sliceId.' },
1916
- variantIds: { type: 'array', minItems: 1, maxItems: 25, items: { type: 'string', minLength: 1 }, description: 'Exact Shopify variant ids returned by catalog_slice_read. Required with shopDomain.' },
1917
- sliceId: { type: 'string', pattern: '^csl_saved_[A-Za-z0-9_-]{8,128}$', description: 'Saved snapshot source. Mutually exclusive with shopDomain/variantIds.' },
1918
- },
1919
- required: [],
1920
- },
2358
+ inputSchema: CATALOG_SNAPSHOT_INPUT_SCHEMA,
1921
2359
  },
1922
2360
  {
1923
2361
  name: 'catalog_slices_saved',
1924
2362
  description: 'Manage the existing saved-catalog-slice lifecycle with action=list, get, save, delete, refresh, or changes. save/refresh read the connected Shopify store; delete removes only the exact saved slice; list/get/changes are Meguro reads. The worst-case annotation is intentionally destructive and outward because this grouped tool includes delete and external refresh.',
1925
- inputSchema: {
1926
- type: 'object', additionalProperties: false,
1927
- properties: {
1928
- workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace selector. Omit for Default.' },
1929
- action: { type: 'string', enum: ['list', 'get', 'save', 'delete', 'refresh', 'changes'] },
1930
- shopDomain: { type: 'string', pattern: '^[a-z0-9][a-z0-9-]*\\.myshopify\\.com$', description: 'Required for save.' },
1931
- label: { type: 'string', minLength: 1, maxLength: 80, description: 'Required for save.' },
1932
- variantIds: { type: 'array', minItems: 1, maxItems: 25, items: { type: 'string', minLength: 1 }, description: 'Required for save.' },
1933
- sliceId: { type: 'string', pattern: '^csl_saved_[A-Za-z0-9_-]{8,128}$', description: 'Required for get/delete/refresh/changes.' },
1934
- },
1935
- required: ['action'],
1936
- },
2363
+ inputSchema: CATALOG_SAVED_SLICES_INPUT_SCHEMA,
1937
2364
  },
1938
2365
  {
1939
2366
  name: 'store_claim_by_code',
@@ -1992,7 +2419,7 @@ export function createTools(config) {
1992
2419
  },
1993
2420
  {
1994
2421
  name: 'run_status',
1995
- description: 'Live progress of a run: orders written / total, sim-date watermark, status, lastError. Account-key-gated, read-only, cache-busted. Use this instead of guessing.',
2422
+ description: 'Read one exact owned history run returned by runs_list. Returns lifecycle, progress, and its server-owned twinPair relation when present. Missing, expired, and foreign-workspace identities fail closed with runs_list recovery teaching.',
1996
2423
  inputSchema: {
1997
2424
  type: 'object',
1998
2425
  additionalProperties: false,
@@ -2015,7 +2442,7 @@ export function createTools(config) {
2015
2442
  },
2016
2443
  {
2017
2444
  name: 'run_report',
2018
- description: 'Generate and fetch the run\'s receipt (verdict, summary, S3 keys). The receipt never claims more than the ledger can prove. Receipts remain fetchable for 7 days on Free, 90 days on Solo, and 365 days on Builder; an expired response teaches the applicable upgrade or a fresh run.',
2445
+ description: 'Read the retained receipt for one exact owned history run returned by runs_list. The receipt never claims more than the ledger can prove. Missing, expired, and foreign-workspace identities fail closed with runs_list recovery teaching.',
2019
2446
  inputSchema: {
2020
2447
  type: 'object',
2021
2448
  additionalProperties: false,
@@ -2039,7 +2466,7 @@ export function createTools(config) {
2039
2466
  },
2040
2467
  {
2041
2468
  name: 'runs_diff',
2042
- description: 'Legacy receipt comparison for two Shopify dev-store history runs created by run_start. It does not compare practice simulation attempts returned by practice_run_start; a pa-* id receives a teaching error with the terminating receipt-fact method. For history runs it verifies the shared prefix, finds the fork, and returns impact receipt deltas. Prefer twin_diff for the named history-run impact receipt vocabulary. "Not comparable" is an honest answer, not an error in your usage. Receipts remain fetchable for 7 days on Free, 90 days on Solo, and 365 days on Builder; an expired response teaches the applicable upgrade or a fresh run.',
2469
+ description: 'Legacy receipt comparison for two Shopify dev-store history runs created by run_start. It does not compare practice simulation attempts returned by practice_run_start; a pa-* id receives a teaching error with the terminating receipt-fact method. For history runs it verifies the shared prefix, finds the fork, and returns impact receipt deltas. Prefer twin_diff for the named history-run impact receipt vocabulary. "Not comparable" is an honest answer, not an error in your usage. Receipt availability and expiry guidance are server-owned and returned by the receipt response.',
2043
2470
  inputSchema: {
2044
2471
  type: 'object',
2045
2472
  additionalProperties: false,
@@ -2052,26 +2479,26 @@ export function createTools(config) {
2052
2479
  },
2053
2480
  {
2054
2481
  name: 'gate_configure',
2055
- description: 'Configure the same Receipt rule V1 that Console offers under Set a Gate. Choose one finished practice simulation receipt, enable or update the Gate, or set enabled to false to disable it. Saving an enabled receipt also performs the same immediate server evaluation as Console; no other policy choice is exposed here because Console offers none.',
2482
+ description: "Configure the optional Configured Receipt Gate policy over selected receipt facts. The receipt's automatic recorded API behavior remains available separately whether or not this configured policy exists. Choose one finished practice simulation receipt, enable or update the policy, or set enabled to false to disable it. Saving an enabled receipt also performs the same immediate server evaluation as Console.",
2056
2483
  inputSchema: {
2057
2484
  type: 'object',
2058
2485
  additionalProperties: false,
2059
2486
  properties: {
2060
- receiptId: { type: 'string', minLength: 1, description: 'Finished pa-* practice simulation receipt to use as the Receipt Gate bar.' },
2487
+ receiptId: { type: 'string', minLength: 1, description: 'Finished pa-* practice simulation receipt to evaluate with the Configured Receipt Gate.' },
2061
2488
  storeId: { type: 'string', minLength: 1, description: 'Optional practice-store identity carried by the selected Console receipt.' },
2062
- enabled: { type: 'boolean', description: 'Enable or update the Gate when true or omitted; disable the configured Gate when false.' },
2489
+ enabled: { type: 'boolean', description: 'Enable or update the Configured Receipt Gate when true or omitted; disable it when false.' },
2063
2490
  },
2064
2491
  required: ['receiptId'],
2065
2492
  },
2066
2493
  },
2067
2494
  {
2068
2495
  name: 'gate_evaluate',
2069
- description: 'Evaluate the configured Receipt rule V1 exactly as Console Evaluate now does. Omit receiptId to evaluate the configured receipt, or supply one finished pa-* receipt explicitly. This records a new server-owned Gate evaluation; it does not derive a local verdict.',
2496
+ description: "Evaluate the Configured Receipt Gate exactly as Console does. It is an optional configured policy over selected receipt facts, separate from the receipt's automatic recorded API behavior. Omit receiptId to evaluate the configured receipt, or supply one finished pa-* receipt explicitly. This records a new server-owned evaluation; it does not derive a local verdict.",
2070
2497
  inputSchema: {
2071
2498
  type: 'object',
2072
2499
  additionalProperties: false,
2073
2500
  properties: {
2074
- receiptId: { type: 'string', minLength: 1, description: 'Optional finished pa-* practice simulation receipt. Omit it to evaluate the configured Gate receipt.' },
2501
+ receiptId: { type: 'string', minLength: 1, description: 'Optional finished pa-* practice simulation receipt. Omit it to evaluate the Configured Receipt Gate receipt.' },
2075
2502
  storeId: { type: 'string', minLength: 1, description: 'Optional exact practice-store identity. Omit when receiptId identifies the owner or the workspace has exactly one store.' },
2076
2503
  },
2077
2504
  required: [],
@@ -2079,20 +2506,20 @@ export function createTools(config) {
2079
2506
  },
2080
2507
  {
2081
2508
  name: 'gate_verdict',
2082
- description: 'Read the latest server-computed Receipt Gate verdict. A previously issued legacy Gate artifact remains readable by its exact runId, but no customer surface creates one. Returns the exact meguro.gate-verdict.v1 object with go/waiting/no-go status, named fact-citing checks, per-check outcomes, flip condition, and named policy version. Facts are the product: this is a named, reproducible, replaceable policy over facts the customer fully holds; check it yourself.',
2509
+ description: "Read the latest server-computed Configured Receipt Gate verdict. This optional configured policy is separate from the receipt's automatic recorded API behavior, which remains available. A previously issued legacy artifact remains readable by its exact runId, but no customer surface creates one. Returns the exact meguro.gate-verdict.v1 object with go/waiting/no-go status, named fact-citing checks, per-check outcomes, flip condition, and named policy version. Facts are the product: this remains a named, reproducible, replaceable policy over facts the customer fully holds; check it yourself.",
2083
2510
  inputSchema: {
2084
2511
  type: 'object',
2085
2512
  additionalProperties: false,
2086
2513
  properties: {
2087
- runId: { type: 'string', minLength: 1, description: 'Exact id of a previously issued legacy Gate artifact. Omit to read the latest Receipt Gate verdict.' },
2088
- storeId: { type: 'string', minLength: 1, description: 'Optional exact practice-store identity for the current per-store Gate lane.' },
2514
+ runId: { type: 'string', minLength: 1, description: 'Exact id of a previously issued legacy Gate artifact. Omit to read the latest Configured Receipt Gate verdict.' },
2515
+ storeId: { type: 'string', minLength: 1, description: 'Optional exact practice-store identity for the current per-store Configured Receipt Gate lane.' },
2089
2516
  },
2090
2517
  required: [],
2091
2518
  },
2092
2519
  },
2093
2520
  {
2094
2521
  name: 'runs_list',
2095
- description: 'List Shopify dev-store history runs created by run_start so an agent can re-find that family of prior work. It does not list practice simulation attempts returned by practice_run_start; call practice_runs_list for those pa-* ids, then use practice_run_report or practice_run_impact. Optional history-run lifecycle filtering supports running, paused, completed, failed, or cleaning. An empty result explicitly says it is only about history runs. The payload names run-clock lifecycle timestamps separately from world-clock scenario coordinates and returns no filter counts.',
2522
+ description: 'Discover the owned Shopify dev-store history runs readable through hosted OAuth. A completed server-owned twin relation is projected as twinPair with pairId, baseline/treated role, and exact counterpartRunId; pass those returned identities to run_status, run_report, and twin_diff. It does not list practice simulation attempts returned by practice_run_start. Optional lifecycle filtering supports running, paused, completed, failed, or cleaning.',
2096
2523
  inputSchema: {
2097
2524
  type: 'object',
2098
2525
  additionalProperties: false,
@@ -2104,12 +2531,22 @@ export function createTools(config) {
2104
2531
  },
2105
2532
  {
2106
2533
  name: 'usage_read',
2107
- description: 'Read authoritative tier-meter simulation runs, practice-store, and workspace headroom before starting work. When retained activity reconciles, simulationRunAccounting gives the exact counted + excluded non-billable sample = visible completed equation and the stable exclusion-policy identity. Incomplete coverage or a source mismatch is disclosed without projecting a competing consumption number.',
2108
- inputSchema: { type: 'object', additionalProperties: false, properties: {}, required: [] },
2534
+ description: 'Read authoritative tier-meter simulation runs, practice-store, and workspace headroom. The compatibility field limits.runStartAllowed means only that metering allowance permits one charged run; it is not overall run-start eligibility. Omit storeId to receive runStartEligibility as explicitly not evaluated. Supply an exact storeId, and optionally the intended continueFromAttemptId, to receive the separate server-owned store/root lifecycle eligibility and exact next call. When retained activity reconciles, simulationRunAccounting gives the exact counted + excluded non-billable = visible completed equation and one exclusion entry per durable classification that caused the exclusion. Incomplete coverage or a source mismatch is disclosed without projecting a competing consumption number.',
2535
+ inputSchema: {
2536
+ type: 'object',
2537
+ additionalProperties: false,
2538
+ properties: {
2539
+ workspaceId: OPTIONAL_WORKSPACE_SELECTOR_INPUT_SCHEMA,
2540
+ storeId: { ...PRACTICE_STORE_ID_INPUT_SCHEMA, description: 'Optional exact practice-store id whose run-start lifecycle should be evaluated. Omission never implies global eligibility.' },
2541
+ continueFromAttemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Optional intended continuation root. Requires storeId and is evaluated against that exact store.' },
2542
+ },
2543
+ required: [],
2544
+ dependentRequired: { continueFromAttemptId: ['storeId'] },
2545
+ },
2109
2546
  },
2110
2547
  {
2111
2548
  name: 'twin_diff',
2112
- description: 'Read the impact receipt comparing a treated run with its doing-nothing twin: shared-prefix integrity, world-clock day deltas, fidelity, and the measured impact over the named world-clock horizon. Facts remain available even when the integrity verdict refuses a causal claim. Receipts remain fetchable for 7 days on Free, 90 days on Solo, and 365 days on Builder; an expired response teaches the applicable upgrade or a fresh run.',
2549
+ description: 'Read the impact receipt for the baseline and treated members of one server-recorded twinPair, in that order. The result carries pair identity, both exact source run IDs, their run_status and run_report calls, and current receipt retention. Missing, expired, foreign, reversed, or nonpaired sources fail closed; unrelated runs are never compared.',
2113
2550
  inputSchema: {
2114
2551
  type: 'object',
2115
2552
  additionalProperties: false,
@@ -2134,7 +2571,7 @@ export function createTools(config) {
2134
2571
  },
2135
2572
  {
2136
2573
  name: 'exam_start',
2137
- description: 'Start or safely continue a Shopify Exam from one immutable practice attempt on the exact typed development-store domain. The tool performs idempotent create/preview/typed-confirm setup, then advances at most one bounded executor checkpoint. Call exam_status after every invocation; while nextAction names exam_start, repeat the same arguments. GET polling never advances the Exam and ambiguous delivery is never retried blindly.',
2574
+ description: 'Start or safely continue a Shopify Exam from one immutable practice attempt on the exact typed development-store domain. The tool performs idempotent create/preview/typed-confirm setup, then advances at most one bounded execution or cleanup checkpoint. Call exam_status after every invocation; while nextAction names exam_start, repeat the same arguments. GET polling never advances or cleans the Exam and ambiguous delivery is never retried blindly.',
2138
2575
  inputSchema: {
2139
2576
  type: 'object', additionalProperties: false,
2140
2577
  properties: {
@@ -2146,7 +2583,7 @@ export function createTools(config) {
2146
2583
  },
2147
2584
  {
2148
2585
  name: 'exam_status',
2149
- description: 'GET-only server-authoritative Shopify Exam lifecycle status: current revision and phase cursors, verdict/comparison summary when present, cleanup/residue state, immutable receipt availability, Gate projection, and the exact next tool. Reading status never advances or cleans the Exam.',
2586
+ description: 'GET-only server-authoritative Shopify Exam lifecycle status: current revision and phase cursors, verdict/comparison summary when present, cleanup/residue state, immutable receipt availability, Configured Receipt Gate projection, and the exact next tool. Reading status never advances or cleans the Exam; pending cleanup names exam_start as the continuation.',
2150
2587
  inputSchema: {
2151
2588
  type: 'object', additionalProperties: false,
2152
2589
  properties: { examId: { type: 'string', pattern: '^pex-[0-9a-f]{24}$', description: 'Exam id returned by exam_start.' } },
@@ -2155,7 +2592,7 @@ export function createTools(config) {
2155
2592
  },
2156
2593
  {
2157
2594
  name: 'exam_report',
2158
- description: 'Read the completed Shopify Exam result and verify the exact stored bytes of both private and public immutable receipts against their artifact digests. Returns bounded receipt identities, digests, byte counts, verdict, comparison totals, cleanup/residue facts, and Gate evidence — never raw private receipt payloads or credentials. Receipts remain fetchable for 7 days on Free, 90 days on Solo, and 365 days on Builder; an expired response teaches the applicable upgrade or a fresh run.',
2595
+ description: 'Read a cleaned or safely blocked Shopify Exam result and verify the exact stored bytes of both private and public immutable receipts against their artifact digests. A completed comparison with pending cleanup must be continued through exam_start before this tool returns a final report. Returns bounded receipt identities, digests, byte counts, verdict, comparison totals, cleanup/residue facts, and Configured Receipt Gate evidence — never raw private receipt payloads or credentials. Receipt availability and expiry guidance are server-owned and returned by the receipt response.',
2159
2596
  inputSchema: {
2160
2597
  type: 'object', additionalProperties: false,
2161
2598
  properties: { examId: { type: 'string', pattern: '^pex-[0-9a-f]{24}$', description: 'Completed Exam id returned by exam_start.' } },
@@ -2164,13 +2601,13 @@ export function createTools(config) {
2164
2601
  },
2165
2602
  {
2166
2603
  name: 'practice_run_start',
2167
- description: 'Start an external-agent practice simulation run on an existing Meguro practice store, or continue one completed Builder segment on the same aging store. The returned lifecycleContract explains that accepted writes are disposable attempt-local state, actions remain in receipt and Impact evidence, the store returns to baseline after completion, terminal reads remain available while writes are refused, and an ordinary completed run is metered once while the explicitly provisioned sample is exempt. A fresh start replays the practice store\'s original scenario baseline as segment 1 by default; chaining is explicit through continueFromAttemptId. An optional immutable assertionPlan may predeclare exact expected negative-control refusals before call sequence zero; only exact server-matched reason, layer, target or request, and cardinality can prevent that rejection from becoming an unhandled default/Gate failure. When clock.simulationDays is omitted, Meguro derives the full remaining scenario arc. Builder automatically plans a capped legal segment, while Free or Solo refuses an above-cap arc because those tiers cannot continue it. For an executable first call on the current tier, use the exact starterRecommendation.starterPlan.practiceRunStart.clock returned by templates_list. Every accepted start returns a machine-readable segmentPlan with the exact Store-day range and continuation narration. An advance target that reaches or crosses the planned end requires explicit terminal confirmation before the run ends; state-changing Admin calls require a running run, so use return-request-pending to stop on an actionable return before the planned end. The returned machine-readable simulationRunClock announces every exit: 1 hour for an agent that never executes, 48 hours with neither agent contact nor Store-time advance, and a 7-day run age (2 days on Free). Takes the canonical practice-store id (`storeId`) exactly as returned by get_connection_details, and returns the run identity (`attemptId`) every later practice_run_* tool uses — the store id and the run id are different identifiers. Meguro Console is quick proof, while the agency agent still runs in the caller\'s own environment. Returns the same server-authoritative practice-run contract without connection credentials.',
2604
+ description: 'Start an external-agent practice simulation run on an existing Meguro practice store, or continue one completed segment on the same aging store when server authorization allows it. The returned lifecycleContract explains that accepted writes are disposable attempt-local state, actions remain in receipt and Impact evidence, the store returns to baseline after completion, terminal reads remain available while writes are refused, and ordinary completed runs meter once. A fresh account\'s first accepted start may bind its one account-owned onboarding evaluation grant to the chosen store/template/root and pinned ideal horizon; every authorized segment is unmetered, while every run after that horizon follows ordinary tier policy. A fresh start replays the practice store\'s original scenario baseline as segment 1 by default; chaining is explicit through continueFromAttemptId. An optional immutable assertionPlan may predeclare exact expected negative-control refusals before call sequence zero; only exact server-matched reason, layer, target or request, and cardinality can prevent that rejection from becoming an unhandled Configured Receipt Gate failure. When clock.simulationDays is omitted, Meguro derives the full remaining scenario arc. Without active onboarding authorization, Builder plans a capped legal segment while Free or Solo refuses an above-cap arc. For an executable onboarding call, choose the matching starterRecommendation.starterPlans entry returned by templates_list and use its exact practiceRunStart.clock; otherwise use the returned ordinary starterPlan. Every accepted start returns a machine-readable segmentPlan with the exact Store-day range, authorization, and continuation narration. An advance target that reaches or crosses the planned end requires explicit terminal confirmation before the run ends; state-changing Admin calls require a running run, so use return-request-pending to stop on an actionable return before the planned end. The returned machine-readable simulationRunClock announces every exit: 1 hour for an agent that never executes, 48 hours with neither agent contact nor Store-time advance, and a 7-day run age (2 days on Free). Takes the canonical practice-store id (`storeId`) exactly as returned by get_connection_details, and returns the run identity (`attemptId`) every later practice_run_* tool uses — the store id and the run id are different identifiers. Meguro Console is quick proof, while the agency agent still runs in the caller\'s own environment. Returns the same server-authoritative practice-run contract without connection credentials.',
2168
2605
  inputSchema: {
2169
2606
  type: 'object',
2170
2607
  additionalProperties: false,
2171
2608
  properties: {
2609
+ workspaceId: OPTIONAL_WORKSPACE_SELECTOR_INPUT_SCHEMA,
2172
2610
  storeId: { ...PRACTICE_STORE_ID_INPUT_SCHEMA, description: 'Canonical Meguro practice-store id (tenant-owned), as returned by get_connection_details.' },
2173
- worldId: { ...PRACTICE_STORE_ID_INPUT_SCHEMA, description: 'Legacy alias for storeId — the same practice-store id under its old name. Prefer storeId.' },
2174
2611
  continueFromAttemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Completed prior segment to continue on the same aging practice store. Available only when the server tier policy allows continuation. On Builder, omit clock.simulationDays to let Meguro plan the next legal segment automatically.' },
2175
2612
  clock: {
2176
2613
  type: 'object',
@@ -2227,8 +2664,7 @@ export function createTools(config) {
2227
2664
  },
2228
2665
  graphqlThrottleMode: { type: 'string', enum: ['off', 'standard-1000', 'plus-2000'], description: 'Optional Meguro compatibility GraphQL throttle mode.' },
2229
2666
  },
2230
- required: [],
2231
- anyOf: [{ required: ['storeId'] }, { required: ['worldId'] }],
2667
+ required: ['storeId'],
2232
2668
  },
2233
2669
  },
2234
2670
  {
@@ -2247,10 +2683,13 @@ export function createTools(config) {
2247
2683
  },
2248
2684
  {
2249
2685
  name: 'practice_run_status',
2250
- description: 'Read the server-authoritative state of an external-agent practice simulation run, including its announced simulation-run deadlines, immutable simulation run receipt after any exit, and lifecycleContract. That contract explains that accepted writes are disposable attempt-local state, actions remain in receipt and Impact evidence, the store returns to baseline after completion, terminal reads remain available while writes are refused, and an ordinary completed run is metered once while the explicitly provisioned sample is exempt. Immediately before advancing, copy the current Store-day and call-sequence cursors from this response; if the next target reaches or crosses endDay, follow the exact terminal-confirmation retry returned by practice_run_advance. A running run authorizes state-changing Admin calls; after completion later writes receive the stable run-complete refusal. Use Console for quick proof; the tested agent remains in the caller\'s environment.',
2686
+ description: 'Read the server-authoritative state of an external-agent practice simulation run, including its announced simulation-run deadlines, immutable simulation run receipt after any exit, and lifecycleContract. That contract explains that accepted writes are disposable attempt-local state, actions remain in receipt and Impact evidence, the store returns to baseline after completion, terminal reads remain available while writes are refused, and an ordinary completed run is metered once while the explicitly provisioned sample is exempt. Immediately before advancing, copy practiceRun.advanceCursor unchanged into practice_run_advance; do not choose between currentDay and watermarkDay or assemble call-sequence fields. If the next target reaches or crosses endDay, follow the exact terminal-confirmation retry returned by practice_run_advance. A running run authorizes state-changing Admin calls; after completion later writes receive the stable run-complete refusal. Use Console for quick proof; the tested agent remains in the caller\'s environment.',
2251
2687
  inputSchema: {
2252
2688
  type: 'object', additionalProperties: false,
2253
- properties: { attemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' } },
2689
+ properties: {
2690
+ workspaceId: OPTIONAL_WORKSPACE_SELECTOR_INPUT_SCHEMA,
2691
+ attemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' },
2692
+ },
2254
2693
  required: ['attemptId'],
2255
2694
  },
2256
2695
  },
@@ -2259,25 +2698,28 @@ export function createTools(config) {
2259
2698
  description: 'Capture an immutable checkpoint for an external-agent practice run and return a bounded evidence summary. Console is quick proof; agent execution remains in the caller\'s environment.',
2260
2699
  inputSchema: {
2261
2700
  type: 'object', additionalProperties: false,
2262
- properties: { attemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' } },
2701
+ properties: {
2702
+ workspaceId: OPTIONAL_WORKSPACE_SELECTOR_INPUT_SCHEMA,
2703
+ attemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' },
2704
+ },
2263
2705
  required: ['attemptId'],
2264
2706
  },
2265
2707
  },
2266
2708
  {
2267
2709
  name: 'practice_run_advance',
2268
- description: 'Advance an external-agent practice run by whole days or until one closed-set commerce condition, using both optimistic concurrency cursors. Accepted writes are disposable attempt-local state: they remain in receipt and Impact evidence, while the underlying practice store returns to baseline after completion. Terminal reads remain available and terminal writes are refused. An advance that reaches or crosses planned endDay refuses without changing Store time unless the caller explicitly retries the returned shape with confirmConclusion: true; that confirmed retry concludes the attempt and closes its write lane. return-request-pending stops before planned end with the evaluated Store day, pending-request count, and one bounded opportunity for the next read; it never resolves the request automatically. If no opportunity exists before planned end, the explicit confirmation path ends in the stable run-complete refusal for later writes and offers no write suggestion. State-changing Admin calls require a running run. Immediately before every advance, call practice_run_status and copy both current cursor values from that same response. If either cursor is stale, Meguro refuses without moving Store time: refresh both cursors from practice_run_status and do not retry the stale request. Before durable agentExecutionStartedAt exists, at most 7 Store days may be advanced; connect your agent — any call unlocks further time. Console is quick proof; agent execution remains in the caller\'s environment.',
2710
+ description: 'Advance an external-agent practice run by whole days or until one closed-set commerce condition, using the server-owned optimistic-concurrency advanceCursor. Accepted writes are disposable attempt-local state: they remain in receipt and Impact evidence, while the underlying practice store returns to baseline after completion. Terminal reads remain available and terminal writes are refused. An advance that reaches or crosses planned endDay refuses without changing Store time unless the caller explicitly retries the returned shape with confirmConclusion: true; that confirmed retry concludes the attempt and closes its write lane. return-request-pending stops before planned end with the evaluated Store day, pending-request count, and one bounded opportunity for the next read; it never resolves the request automatically. If no opportunity exists before planned end, the explicit confirmation path ends in the stable run-complete refusal for later writes and offers no write suggestion. State-changing Admin calls require a running run. Immediately before every advance, call practice_run_status and copy practiceRun.advanceCursor unchanged. If it is stale, Meguro refuses without moving Store time and returns a fresh advanceCursor for one exact retry; do not infer from currentDay, watermarkDay, or call-sequence fields. Before durable agentExecutionStartedAt exists, at most 7 Store days may be advanced; connect your agent — any call unlocks further time. Console is quick proof; agent execution remains in the caller\'s environment.',
2269
2711
  inputSchema: {
2270
2712
  type: 'object',
2271
2713
  additionalProperties: false,
2272
2714
  properties: {
2715
+ workspaceId: OPTIONAL_WORKSPACE_SELECTOR_INPUT_SCHEMA,
2273
2716
  attemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' },
2274
2717
  days: { type: 'integer', minimum: 1, description: 'Whole simulated days to advance.' },
2275
2718
  until: PRACTICE_RUN_UNTIL_INPUT_SCHEMA,
2276
- expectedDay: { type: 'integer', minimum: 0, description: 'Call practice_run_status immediately before advancing and copy its current practiceRun.time.watermarkDay. This cursor is required on every advance; if it is stale, the advance is refused without moving Store time.' },
2277
- expectedCallSeq: { type: 'integer', minimum: -1, description: 'From the same immediately preceding practice_run_status response, copy current practiceRun.latestCallSequence (or -1 only when that response says no call exists). This cursor is required on every advance; if it is stale, the advance is refused without moving Store time.' },
2719
+ advanceCursor: PRACTICE_RUN_ADVANCE_CURSOR_INPUT_SCHEMA,
2278
2720
  confirmConclusion: { type: 'boolean', description: 'Set true only after reading the terminal-boundary teaching refusal and choosing to conclude the attempt and close its write lane.' },
2279
2721
  },
2280
- required: ['attemptId', 'expectedDay', 'expectedCallSeq'],
2722
+ required: ['attemptId', 'advanceCursor'],
2281
2723
  oneOf: [
2282
2724
  { required: ['days'], not: { required: ['until'] } },
2283
2725
  { required: ['until'], not: { required: ['days'] } },
@@ -2286,28 +2728,43 @@ export function createTools(config) {
2286
2728
  },
2287
2729
  {
2288
2730
  name: 'practice_run_finish',
2289
- description: 'Explicitly finish an external-agent practice simulation run without inventing additional time or evidence. The response includes the immutable simulation run receipt and echoes the deadlines announced at start. Console is quick proof; agent execution remains in the caller\'s environment.',
2731
+ description: 'End an external-agent practice segment without inventing time or evidence. A segment at planned end completes; an under-measured segment stops and remains explicitly incomplete. For a stopped onboarding segment, the server-owned practiceRun.recovery object gives the exact practice_run_start call that resumes the same bound root at its authoritative watermark. To abandon that incomplete onboarding evaluation permanently, set both permanentStop and acknowledgeIncompleteOnboardingEvaluation true; Meguro preserves evidence and consumed onboarding value. The response includes the immutable segment receipt and announced deadlines. Console is quick proof; agent execution remains in the caller\'s environment.',
2290
2732
  inputSchema: {
2291
2733
  type: 'object', additionalProperties: false,
2292
- properties: { attemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' } },
2734
+ properties: {
2735
+ workspaceId: OPTIONAL_WORKSPACE_SELECTOR_INPUT_SCHEMA,
2736
+ attemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' },
2737
+ permanentStop: { type: 'boolean', description: 'Set true only to abandon an under-measured onboarding evaluation permanently. Requires acknowledgeIncompleteOnboardingEvaluation=true.' },
2738
+ acknowledgeIncompleteOnboardingEvaluation: { type: 'boolean', description: 'Set true with permanentStop to acknowledge that the bound onboarding evaluation will remain incomplete while its evidence and consumed value remain preserved.' },
2739
+ },
2293
2740
  required: ['attemptId'],
2741
+ dependentRequired: {
2742
+ permanentStop: ['acknowledgeIncompleteOnboardingEvaluation'],
2743
+ acknowledgeIncompleteOnboardingEvaluation: ['permanentStop'],
2744
+ },
2294
2745
  },
2295
2746
  },
2296
2747
  {
2297
2748
  name: 'practice_run_report',
2298
- description: 'Read the run\'s receipt — a bounded receipt summary for a completed external-agent practice run. Immutable expected-refusal declarations appear with their exact server-emitted matched or failure status; a matched rejection remains in raw receipt counts while only its unhandled default/Gate consequence changes. Interpret `resultSemantics.lifecycle`, `evidenceAssertions`, `technicalPolicyVerdict`, and `modeledScenarioOutcome` as independent named dimensions; there is no implicit overall status. Existing `summary`, `defaultVerdict`, `verdict`, and `measurement` fields are legacy compatibility fields. `report` is the protocol/route compatibility name for the receipt (the tool name and the `operation: "report"` field keep it for wire stability); user-facing language is "receipt". The payload includes an unguessable public receipt id, a complete environment-qualified HTTPS URL in `canonicalPath`, and SHA-256: fetch that URL without credentials and verify the exact bytes before promotion. Raw private request and response payloads stay out of MCP; use Console for quick proof while the agent remains in the caller\'s environment. Receipts remain fetchable for 7 days on Free, 90 days on Solo, and 365 days on Builder; an expired response teaches the applicable upgrade or a fresh run.',
2749
+ description: 'Read the run\'s receipt — a bounded receipt summary for a completed external-agent practice run. `reportBack` is the exact server-owned customer summary in the fixed order Mechanics, Conduct, Scenario outcome; MCP passes it through without re-deriving or reclassifying any plane, and modeled money closes its narrative. `takeHome` is the frozen take-home packet the issued receipt itself carries — the same ordered Mechanics, Conduct, Scenario outcome facts the public receipt view serves, plus the source URL and SHA-256, the claims/evidence boundary, the verification steps, and a copy-ready `memo`; MCP passes it through unchanged and compiles no part of it. `eventResolution` passes through the exact creation-pinned template identity/revision, evaluation horizon, measured horizon, and typed event-window result; it never recovers current catalog metadata for a historical run. `gate` is the exact server-owned final machine projection; `rejectionAttribution.gateStatus` is mechanically aligned with it, and neither value is re-derived by MCP. Immutable expected-refusal declarations appear with their exact server-emitted matched or failure status; a matched rejection remains in raw receipt counts while only its unhandled Configured Receipt Gate consequence changes. Interpret `resultSemantics.lifecycle`, `evidenceAssertions`, `technicalPolicyVerdict`, and `modeledScenarioOutcome` as independent named dimensions; there is no implicit overall status. Existing `summary`, `defaultVerdict`, `verdict`, and `measurement` fields are legacy compatibility fields. `report` is the protocol/route compatibility name for the receipt (the tool name and the `operation: "report"` field keep it for wire stability); user-facing language is "receipt". The payload includes an unguessable public receipt id, a complete environment-qualified HTTPS URL in `canonicalPath`, and SHA-256: fetch that URL without credentials and verify the exact bytes before promotion. Raw private request and response payloads stay out of MCP; use Console for quick proof while the agent remains in the caller\'s environment. Receipt availability and expiry guidance are server-owned and returned by the receipt response.',
2299
2750
  inputSchema: {
2300
2751
  type: 'object', additionalProperties: false,
2301
- properties: { attemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' } },
2752
+ properties: {
2753
+ attemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' },
2754
+ workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace coordinate returned by practice_runs_list. Omit for Default.' },
2755
+ },
2302
2756
  required: ['attemptId'],
2303
2757
  },
2304
2758
  },
2305
2759
  {
2306
2760
  name: 'practice_run_impact',
2307
- description: 'Read the impact receipt for a completed external-agent practice run: the base year — the same store, same days, without your agent — compared with your agent\'s year, showing what changed because of your agent (orders, units, and revenue), a footprint of every write, and an integrity gate that reports a reason instead of numbers when the comparison is not clean. The payload includes an unguessable public receipt id, a complete environment-qualified HTTPS URL in `canonicalPath`, and SHA-256: fetch that URL without credentials and verify the exact bytes before promotion. Read-only; never mutates the run and returns no credentials. Takes the run\'s attemptId (from practice_run_start), not the practice-store id (storeId). Receipts remain fetchable for 7 days on Free, 90 days on Solo, and 365 days on Builder; an expired response teaches the applicable upgrade or a fresh run.',
2761
+ description: 'Read the impact receipt for a completed external-agent practice run: the base year — the same store, same days, without your agent — compared with your agent\'s year, showing what changed because of your agent (orders, units, and revenue), a footprint of every write, and an integrity gate that reports a reason instead of numbers when the comparison is not clean. `eventResolution` is the exact creation-pinned template identity/revision, evaluation horizon, measured horizon, and typed event-window result; historical runs without that snapshot are explicitly unavailable and never consult current template metadata. The payload includes an unguessable public receipt id, a complete environment-qualified HTTPS URL in `canonicalPath`, and SHA-256: fetch that URL without credentials and verify the exact bytes before promotion. Read-only; never mutates the run and returns no credentials. Takes the run\'s attemptId (from practice_run_start), not the practice-store id (storeId). Receipt availability and expiry guidance are server-owned and returned by the receipt response.',
2308
2762
  inputSchema: {
2309
2763
  type: 'object', additionalProperties: false,
2310
- properties: { attemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' } },
2764
+ properties: {
2765
+ attemptId: { type: 'string', minLength: 4, maxLength: 128, pattern: '^pa-[a-z0-9][a-z0-9-]{0,124}$', description: 'Run identity returned by practice_run_start — not the practice-store id (storeId).' },
2766
+ workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace coordinate returned by practice_runs_list. Omit for Default.' },
2767
+ },
2311
2768
  required: ['attemptId'],
2312
2769
  },
2313
2770
  },
@@ -2318,11 +2775,10 @@ export function createTools(config) {
2318
2775
  type: 'object',
2319
2776
  additionalProperties: false,
2320
2777
  properties: {
2778
+ workspaceId: OPTIONAL_WORKSPACE_SELECTOR_INPUT_SCHEMA,
2321
2779
  storeId: { ...PRACTICE_STORE_ID_INPUT_SCHEMA, description: 'Canonical Meguro practice-store id, e.g. w-abc123. Pass the returned storeId straight into practice_run_start.' },
2322
- worldId: { ...PRACTICE_STORE_ID_INPUT_SCHEMA, description: 'Legacy alias for storeId — the same practice-store id under its old name. Prefer storeId.' },
2323
2780
  },
2324
- required: [],
2325
- anyOf: [{ required: ['storeId'] }, { required: ['worldId'] }],
2781
+ required: ['storeId'],
2326
2782
  },
2327
2783
  },
2328
2784
  {
@@ -2332,6 +2788,7 @@ export function createTools(config) {
2332
2788
  type: 'object',
2333
2789
  additionalProperties: false,
2334
2790
  properties: {
2791
+ workspaceId: OPTIONAL_WORKSPACE_SELECTOR_INPUT_SCHEMA,
2335
2792
  worldId: { type: 'string', minLength: 1, description: 'Meguro practice store id, e.g. w-abc123.' },
2336
2793
  query: { type: 'string', maxLength: 30000, description: 'The Admin GraphQL query or mutation document to run.' },
2337
2794
  apiVersion: { type: 'string', pattern: '^\\d{4}-\\d{2}$', description: `Admin API version as YYYY-MM. Supported: ${ADMIN_API_SUPPORTED_VERSIONS.join(', ')}. Defaults to ${ADMIN_API_DEFAULT_VERSION}.` },
@@ -2344,21 +2801,28 @@ export function createTools(config) {
2344
2801
  },
2345
2802
  {
2346
2803
  name: 'admin_schema',
2347
- description: `Look up an exact, bounded slice of the Shopify Admin GraphQL surface Meguro models for supported versions ${ADMIN_API_SUPPORTED_VERSION_LABEL}; the default is ${ADMIN_API_DEFAULT_VERSION}. Returns a type and its direct fields, a Type.field or root mutation with its arguments and return type, a shared commerce recipe, or an authored valid-not-modeled teaching boundary. Deterministic — it never returns the full schema, tenant data, catalog, or credentials, and on a miss it suggests only exact prefix/substring alternatives.`,
2804
+ description: `Look up an exact, bounded slice of the Shopify Admin GraphQL surface Meguro models for supported versions ${ADMIN_API_SUPPORTED_VERSION_LABEL}; the default is ${ADMIN_API_DEFAULT_VERSION}. Returns a type and its direct fields, a Type.field or root mutation with its arguments and return type, a shared commerce recipe, or an authored valid-not-modeled teaching boundary. A recipe match carries its bindingPlan, and every published mutation recipe carries match.recipe.evidencePlan: the eligibility read to run first (its exact query, variables, a selection sentence naming which returned row to use, and bindings from each response path to the mutation variable it fills), then the mutation, then the existing independent readback. mutation.documentOperationName is the GraphQL document label; mutation.rootField is the Shopify mutation coordinate. Where the mutation has a read-before-write cursor the plan also carries expectedRefusalOperationName — the exact root-field matcher key — plus staleCursorExpectedRefusal reason and layer facts to copy unchanged into an assertionPlan. Mutation example values that only the eligibility read can supply appear as <from-eligibility:VARIABLE_PATH> placeholders, never as plausible literals. Deterministic — it never returns the full schema, tenant data, catalog, or credentials, and on a miss it suggests only exact prefix/substring alternatives. Alternatively, submit query instead of lookup + name — with optional variables, operationName, and apiVersion — to ask what this exact Admin document would receive before you create a practice store or spend credit. It parses the document and classifies the selected operation and every supplied coordinate through the same served schema, versioned compatibility registry, and required-argument recovery authority the served Admin surface uses, returning exactly one state: "served" — no compatibility teaching refusal would be produced; "unsupported-with-recovery" — the exact refusal plus the registry remedy naming the supported coordinate or the exact admin_schema call that answers a required-argument refusal; "out-of-plane" — outside the claimed Admin plane, or no exact recovery authority exists. A compatibility withdrawal also carries teachingRefusal: the same refusal the served Admin surface returns, with its code, coordinate, reason, and next step. A document that will not parse, or whose operation cannot be selected without guessing, comes back with that as its own reason rather than guessed compatibility. Store-free and pre-credit: it creates or reads no practice store, run, attempt, ledger, probe, or checkpoint, consumes no credit, and never executes the submitted document. The two modes are exclusive: pass lookup + name or query, never both.`,
2348
2805
  inputSchema: {
2349
2806
  type: 'object',
2350
2807
  additionalProperties: false,
2351
2808
  properties: {
2352
- lookup: { type: 'string', enum: ['type', 'field', 'mutation', 'recipe'], description: 'What to look up: a type, a Type.field, a root mutation, or a commerce recipe.' },
2353
- name: { type: 'string', minLength: 1, maxLength: 200, description: 'Exact name: e.g. ProductVariant, ProductVariant.inventoryQuantity, inventoryAdjustQuantities, or a recipe id/operationName like low-inventory or LowInventory.' },
2809
+ lookup: { type: 'string', enum: ['type', 'field', 'mutation', 'recipe'], description: 'Lookup mode: what to look up — a type, a Type.field, a root mutation, or a commerce recipe. Requires name.' },
2810
+ name: { type: 'string', minLength: 1, maxLength: 200, description: 'Lookup mode: exact name, e.g. ProductVariant, ProductVariant.inventoryQuantity, inventoryAdjustQuantities, or a recipe id/operationName like low-inventory or LowInventory.' },
2811
+ query: { type: 'string', minLength: 1, maxLength: 30000, description: 'Refusal mode: the exact Admin GraphQL document you intend to send. Classified, never executed.' },
2812
+ variables: { type: 'object', description: 'Refusal mode: optional GraphQL variables object (max 20,000 serialized characters). Supplied input-object fields are classified, not executed.' },
2813
+ operationName: { type: 'string', minLength: 1, maxLength: 200, description: 'Refusal mode: which operation to classify when the document defines more than one. Without it, a multi-operation document is refused rather than guessed.' },
2354
2814
  apiVersion: { type: 'string', pattern: '^\\d{4}-\\d{2}$', description: `Admin API version YYYY-MM. Supported: ${ADMIN_API_SUPPORTED_VERSIONS.join(', ')}. Defaults to ${ADMIN_API_DEFAULT_VERSION}.` },
2355
2815
  },
2356
- required: ['lookup', 'name'],
2816
+ required: [],
2817
+ oneOf: [
2818
+ { type: 'object', required: ['lookup', 'name'], not: { anyOf: [{ required: ['query'] }, { required: ['variables'] }, { required: ['operationName'] }] } },
2819
+ { type: 'object', required: ['query'], not: { anyOf: [{ required: ['lookup'] }, { required: ['name'] }] } },
2820
+ ],
2357
2821
  },
2358
2822
  },
2359
2823
  {
2360
2824
  name: 'admin_recipes_list',
2361
- description: 'List every canonical Admin API recipe Meguro models, with its stable id, operation name, purpose, confirmation requirement, and authored aliases/keywords. Registry-only discovery: it returns no tenant, store, catalog, credential, or probe data and never executes an Admin request. Use a returned id with admin_schema({ lookup: "recipe", name: "<id>" }).',
2825
+ description: 'List every canonical Admin API recipe Meguro models, with its stable id, document operation name, purpose, confirmation requirement, authored aliases/keywords, declared API version and templates, support boundary and fidelity, what an empty result means, any prerequisite that must hold first, and its bindingPlan. A published mutation recipe also carries evidencePlan with the eligibility and readback document labels, mutationDocumentOperationName, mutationRootField, and — when a stale-cursor refusal is predeclarable — expectedRefusalOperationName as the exact root-field matcher key. Names only, so discovery stays document-free. Registry-only discovery: it returns no tenant, store, catalog, credential, or probe data and never executes an Admin request. Use a returned id with admin_schema({ lookup: "recipe", name: "<id>" }) for the exact executable documents.',
2362
2826
  inputSchema: {
2363
2827
  type: 'object',
2364
2828
  additionalProperties: false,
@@ -2366,6 +2830,39 @@ export function createTools(config) {
2366
2830
  required: [],
2367
2831
  },
2368
2832
  },
2833
+ {
2834
+ name: 'plan_validate',
2835
+ description: `Check whether an intended set of Shopify Admin GraphQL operations fits Meguro before creating a practice store or starting a run. Takes up to 25 documents in order and returns one result per input item, in input order, each carrying exactly one state: "served" — the selected operation and every supplied coordinate are represented by the current served practice Admin schema, with the exact coordinates it reaches; "unsupported-with-recovery" — an authority hands back a concrete recovery path, either the compatibility registry's remedy naming the supported coordinate (with the canonical recipe document when one is authored) or the exact admin_schema call that answers a required-argument refusal; "out-of-plane" — the operation is outside the served contract, or no exact recovery authority exists for it. Supplied variables and input-object fields are classified through the same input-field compatibility seam the served Admin surface uses. A document that will not parse, or whose operation cannot be selected without guessing, returns an item-local refusal instead of a guessed operation. Every reason, coordinate, alternative, and recovery comes from the served schema, the versioned compatibility registry, or the recovery authority — never an invented alternative, and never a claim about whether a particular practice store or template can execute it. Pre-credit and store-free: it needs no practice store, attempt, or run, creates and reads none, consumes no credit or metered usage, and never executes a submitted document. Supported Admin API versions ${ADMIN_API_SUPPORTED_VERSION_LABEL}; per-item apiVersion defaults to ${ADMIN_API_DEFAULT_VERSION}.`,
2836
+ inputSchema: {
2837
+ type: 'object',
2838
+ additionalProperties: false,
2839
+ properties: {
2840
+ operations: {
2841
+ type: 'array',
2842
+ minItems: 1,
2843
+ maxItems: 25,
2844
+ description: 'The intended Admin GraphQL documents, in the order you plan to run them.',
2845
+ items: {
2846
+ type: 'object',
2847
+ additionalProperties: false,
2848
+ properties: {
2849
+ query: { type: 'string', minLength: 1, maxLength: 30000, description: 'The exact Admin GraphQL document you intend to send.' },
2850
+ variables: { type: 'object', description: 'Optional GraphQL variables object (max 20,000 serialized characters). Supplied input-object fields are classified, not executed.' },
2851
+ operationName: { type: 'string', minLength: 1, maxLength: 200, description: 'Which operation to check when the document defines more than one. Without it, a multi-operation document is refused rather than guessed.' },
2852
+ apiVersion: {
2853
+ type: 'string',
2854
+ enum: [...ADMIN_API_SUPPORTED_VERSIONS],
2855
+ default: ADMIN_API_DEFAULT_VERSION,
2856
+ description: `Admin API version to classify this item against. Supported: ${ADMIN_API_SUPPORTED_VERSIONS.join(', ')}.`,
2857
+ },
2858
+ },
2859
+ required: ['query'],
2860
+ },
2861
+ },
2862
+ },
2863
+ required: ['operations'],
2864
+ },
2865
+ },
2369
2866
  ];
2370
2867
  const definitions = rawDefinitions.map((definition) => {
2371
2868
  const presentation = TOOL_PRESENTATION[definition.name];
@@ -2381,13 +2878,16 @@ export function createTools(config) {
2381
2878
  if (!args || typeof args !== 'object' || Array.isArray(args)) {
2382
2879
  throw new Error(`${name} arguments must be an object`);
2383
2880
  }
2881
+ if (name === 'workspace_archive' || name === 'workspace_unarchive') {
2882
+ requiredWorkspaceId(args);
2883
+ }
2384
2884
  if (SCHEMA_VALIDATED_TOOL_NAMES.has(name)) {
2385
- // Preserve the established identifier teaching messages while the published schema carries
2386
- // the same shape for schema-first clients. This also handles the equal-value legacy alias
2387
- // pair, whose equality cannot be expressed in portable JSON Schema.
2388
2885
  if (name === 'get_connection_details' || name === 'practice_run_start') requiredPracticeStoreId(args);
2389
2886
  const error = schemaValidationError(definition.inputSchema, args);
2390
- if (error) throw new Error(`${name} arguments do not match the declared input schema: ${error}`);
2887
+ if (error) {
2888
+ const correction = conditionalToolCorrection(name, args);
2889
+ throw new Error(`${name} arguments do not match the declared input schema: ${error}${correction ? `. Corrected call: ${correction}` : ''}`);
2890
+ }
2391
2891
  return;
2392
2892
  }
2393
2893
  if (definition.inputSchema.additionalProperties !== false) return;
@@ -2406,15 +2906,34 @@ export function createTools(config) {
2406
2906
  if (!DOCUMENTATION_TOPICS.has(topic)) {
2407
2907
  throw new Error(DOCUMENTATION_TOOL_CONTRACT.topicError);
2408
2908
  }
2409
- if (!Number.isSafeInteger(args.version) || args.version < 1) {
2410
- throw new Error('version must be the exact published positive integer');
2909
+ // Refuse a malformed version before any document read: only a positive integer or the
2910
+ // exact "latest" sentinel reaches the resolver.
2911
+ const requested = args.version;
2912
+ const wellFormed = requested === 'latest'
2913
+ || (Number.isSafeInteger(requested) && requested >= 1);
2914
+ if (!wellFormed) {
2915
+ throw new Error(DOCUMENTATION_TOOL_CONTRACT.versionError);
2411
2916
  }
2412
- return textResult(documentationByTopic(topic, args.version));
2917
+ return textResult(resolveDocumentationRead(topic, requested, DOCUMENTATION_TOOL_CONTRACT));
2413
2918
  }
2414
2919
  case 'templates_list': {
2415
2920
  const workspaceId = optionalWorkspaceId(args);
2416
2921
  const response = await fleetRequest(name, 'GET', '/practice/profiles', undefined, { workspaceId });
2417
- return 'error' in response ? response.error : textResult(storeTemplatesProjection(response.value));
2922
+ return 'error' in response
2923
+ ? response.error
2924
+ : textResult(JSON.stringify(storeTemplatesProjection(response.value)));
2925
+ }
2926
+ case 'template_get': {
2927
+ const workspaceId = optionalWorkspaceId(args);
2928
+ const templateKey = requiredString(args, 'templateKey');
2929
+ const response = await fleetRequest(
2930
+ name,
2931
+ 'GET',
2932
+ `/practice/profiles/${encodeURIComponent(templateKey)}`,
2933
+ undefined,
2934
+ { workspaceId },
2935
+ );
2936
+ return 'error' in response ? response.error : textResult(storeTemplateDetailProjection(response.value));
2418
2937
  }
2419
2938
  case 'stores_list': {
2420
2939
  const workspaceId = optionalWorkspaceId(args);
@@ -2467,59 +2986,6 @@ export function createTools(config) {
2467
2986
  const response = await fleetRequest(name, 'POST', `/workspaces/${encodeURIComponent(workspaceId)}/${action}`, {});
2468
2987
  return 'error' in response ? response.error : textResult(secretSafe(response.value));
2469
2988
  }
2470
- case 'share_create': {
2471
- const workspaceId = optionalWorkspaceId(args);
2472
- if (args.shareId !== undefined) {
2473
- const shareId = requiredShareId(args);
2474
- if (args.artifactType !== undefined || args.artifactRef !== undefined) {
2475
- throw new Error('artifactType and artifactRef are immutable; omit shareId to create a new share');
2476
- }
2477
- const body = {};
2478
- if (args.title !== undefined) {
2479
- body.title = requiredString(args, 'title');
2480
- if (body.title.length > 120) throw new Error('title must be 120 characters or fewer');
2481
- }
2482
- const audienceUserIds = optionalStringList(args, 'audienceUserIds', { maximum: 40 });
2483
- if (audienceUserIds !== undefined) body.audienceUserIds = audienceUserIds;
2484
- if (Object.keys(body).length === 0) throw new Error('share update requires title and/or audienceUserIds');
2485
- const response = await fleetRequest(name, 'PATCH', `/shares/${encodeURIComponent(shareId)}`, body, { workspaceId });
2486
- return 'error' in response ? response.error : textResult(secretSafe(response.value));
2487
- }
2488
- const title = requiredString(args, 'title');
2489
- if (title.length > 120) throw new Error('title must be 120 characters or fewer');
2490
- const { artifactType, artifactRef } = requiredShareArtifact(args);
2491
- const audienceUserIds = optionalStringList(args, 'audienceUserIds', { maximum: 40 });
2492
- if (!audienceUserIds) throw new Error('audienceUserIds is required when creating a share');
2493
- const response = await fleetRequest(name, 'POST', '/shares', {
2494
- title, artifactType, artifactRef, audienceUserIds,
2495
- }, { workspaceId });
2496
- return 'error' in response ? response.error : textResult(secretSafe(response.value));
2497
- }
2498
- case 'shares_list': {
2499
- const workspaceId = optionalWorkspaceId(args);
2500
- const response = await fleetRequest(name, 'GET', '/shares', undefined, { workspaceId });
2501
- return 'error' in response ? response.error : textResult(secretSafe(response.value));
2502
- }
2503
- case 'share_status': {
2504
- const workspaceId = optionalWorkspaceId(args);
2505
- const shareId = requiredShareId(args);
2506
- const response = await fleetRequest(name, 'GET', `/shares/${encodeURIComponent(shareId)}`, undefined, { workspaceId });
2507
- return 'error' in response ? response.error : textResult(secretSafe(response.value));
2508
- }
2509
- case 'share_publish': {
2510
- const workspaceId = optionalWorkspaceId(args);
2511
- const shareId = requiredShareId(args);
2512
- const action = args.action === undefined ? 'publish' : requiredString(args, 'action');
2513
- if (!['publish', 're-share'].includes(action)) throw new Error('action must be publish or re-share');
2514
- const response = await fleetRequest(name, 'POST', `/shares/${encodeURIComponent(shareId)}/${action}`, {}, { workspaceId });
2515
- return 'error' in response ? response.error : textResult(secretSafe(response.value));
2516
- }
2517
- case 'share_revoke': {
2518
- const workspaceId = optionalWorkspaceId(args);
2519
- const shareId = requiredShareId(args);
2520
- const response = await fleetRequest(name, 'POST', `/shares/${encodeURIComponent(shareId)}/revoke`, {}, { workspaceId });
2521
- return 'error' in response ? response.error : textResult(secretSafe(response.value));
2522
- }
2523
2989
  case 'catalog_slice_read': {
2524
2990
  const workspaceId = optionalWorkspaceId(args);
2525
2991
  const shopDomain = requiredShopifyDevDomain(args);
@@ -2533,33 +2999,33 @@ export function createTools(config) {
2533
2999
  }
2534
3000
  case 'catalog_slice_snapshot': {
2535
3001
  const workspaceId = optionalWorkspaceId(args);
2536
- const hasSavedSlice = args.sliceId !== undefined;
2537
- const hasLiveSlice = args.shopDomain !== undefined || args.variantIds !== undefined;
2538
- if (hasSavedSlice === hasLiveSlice) {
2539
- throw new Error('provide exactly one snapshot source: sliceId, or shopDomain plus variantIds');
2540
- }
3002
+ const branch = conditionalToolBranch(name, args);
3003
+ if (!branch) throw new Error('catalog_slice_snapshot branch selection did not match its declared input schema');
2541
3004
  let response;
2542
- if (hasSavedSlice) {
3005
+ if (branch.id === 'saved') {
2543
3006
  const sliceId = requiredSavedSliceId(args);
2544
- response = await fleetRequest(name, 'POST', `/catalog-slices/${encodeURIComponent(sliceId)}/snapshot`, {}, { workspaceId });
3007
+ response = await fleetRequest(name, 'POST', `/catalog-slices/${encodeURIComponent(sliceId)}/snapshot`, {}, {
3008
+ workspaceId, invocationMutates: branch.mutates,
3009
+ });
2545
3010
  } else {
2546
3011
  const shopDomain = requiredShopifyDevDomain(args);
2547
3012
  const variantIds = requiredCatalogVariantIds(args);
2548
3013
  response = await fleetRequest(name, 'POST', `/stores/${encodeURIComponent(shopDomain)}/catalog-slice/snapshot`, {
2549
3014
  variantIds,
2550
- }, { workspaceId });
3015
+ }, { workspaceId, invocationMutates: branch.mutates });
2551
3016
  }
2552
3017
  return 'error' in response ? response.error : textResult(catalogSnapshotProjection(response.value));
2553
3018
  }
2554
3019
  case 'catalog_slices_saved': {
2555
3020
  const workspaceId = optionalWorkspaceId(args);
2556
- const action = requiredString(args, 'action');
2557
- if (!['list', 'get', 'save', 'delete', 'refresh', 'changes'].includes(action)) {
2558
- throw new Error('action must be list, get, save, delete, refresh, or changes');
2559
- }
3021
+ const branch = conditionalToolBranch(name, args);
3022
+ if (!branch) throw new Error('catalog_slices_saved branch selection did not match its declared input schema');
3023
+ const action = branch.action;
2560
3024
  let response;
2561
3025
  if (action === 'list') {
2562
- response = await fleetRequest(name, 'GET', '/catalog-slices', undefined, { workspaceId });
3026
+ response = await fleetRequest(name, 'GET', '/catalog-slices', undefined, {
3027
+ workspaceId, invocationMutates: branch.mutates,
3028
+ });
2563
3029
  } else if (action === 'save') {
2564
3030
  const shopDomain = requiredShopifyDevDomain(args);
2565
3031
  const label = requiredString(args, 'label');
@@ -2567,14 +3033,20 @@ export function createTools(config) {
2567
3033
  response = await fleetRequest(name, 'POST', `/stores/${encodeURIComponent(shopDomain)}/catalog-slices`, {
2568
3034
  label,
2569
3035
  variantIds: requiredCatalogVariantIds(args),
2570
- }, { workspaceId });
3036
+ }, { workspaceId, invocationMutates: branch.mutates });
2571
3037
  } else {
2572
3038
  const sliceId = requiredSavedSliceId(args);
2573
3039
  const suffix = action === 'refresh'
2574
3040
  ? '/refresh'
2575
3041
  : action === 'changes' ? '/changes/latest' : '';
2576
3042
  const method = action === 'delete' ? 'DELETE' : action === 'refresh' ? 'POST' : 'GET';
2577
- response = await fleetRequest(name, method, `/catalog-slices/${encodeURIComponent(sliceId)}${suffix}`, action === 'refresh' ? {} : undefined, { workspaceId });
3043
+ response = await fleetRequest(
3044
+ name,
3045
+ method,
3046
+ `/catalog-slices/${encodeURIComponent(sliceId)}${suffix}`,
3047
+ action === 'refresh' ? {} : undefined,
3048
+ { workspaceId, invocationMutates: branch.mutates },
3049
+ );
2578
3050
  }
2579
3051
  return 'error' in response ? response.error : textResult(secretSafe(response.value));
2580
3052
  }
@@ -2637,8 +3109,9 @@ export function createTools(config) {
2637
3109
  case 'run_status': {
2638
3110
  const runId = requiredString(args, 'runId');
2639
3111
  if (PRACTICE_ATTEMPT_ID.test(runId)) return practiceAttemptHistoryReadGuidance(name, runId);
2640
- const status = await api('GET', `/runs/${encodeURIComponent(runId)}`, undefined, { auth: true });
2641
- return textResult({ ...status, dashboard: dashboardLink(runId) });
3112
+ const status = await apiRaw('GET', `/runs/${encodeURIComponent(runId)}`, undefined, { cacheBustRead: true });
3113
+ if (!status.ok) return historyReadErrorResult(status);
3114
+ return textResult({ ...status.json, dashboard: dashboardLink(runId) });
2642
3115
  }
2643
3116
  case 'run_ledger': {
2644
3117
  const runId = requiredString(args, 'runId');
@@ -2649,7 +3122,8 @@ export function createTools(config) {
2649
3122
  case 'run_report': {
2650
3123
  const runId = requiredString(args, 'runId');
2651
3124
  if (PRACTICE_ATTEMPT_ID.test(runId)) return practiceAttemptHistoryReadGuidance(name, runId);
2652
- return textResult(await api('GET', `/runs/${encodeURIComponent(runId)}/report`, undefined, { auth: true }));
3125
+ const report = await apiRaw('GET', `/runs/${encodeURIComponent(runId)}/report`);
3126
+ return report.ok ? textResult(report.json) : historyReadErrorResult(report);
2653
3127
  }
2654
3128
  case 'run_resume': {
2655
3129
  const runId = requiredString(args, 'runId');
@@ -2697,9 +3171,9 @@ export function createTools(config) {
2697
3171
  const teaching = isGateReceiptUnavailable(response.status, response.json)
2698
3172
  ? gateReceiptUnavailableTeaching(name, receiptId)
2699
3173
  : undefined;
2700
- return practiceErrorResult(response.status, response.json, response.retryAfterSeconds, teaching);
3174
+ return practiceErrorResult(response.status, response.json, response.retryAfterSeconds, teaching, teaching ? 'pre-commit' : undefined);
2701
3175
  }
2702
- return textResult(secretSafe(response.json));
3176
+ return configuredReceiptGateResult(secretSafe(response.json), enabled ? 'configuration saved' : 'disabled');
2703
3177
  }
2704
3178
  case 'gate_evaluate': {
2705
3179
  const receiptId = args.receiptId === undefined
@@ -2716,9 +3190,9 @@ export function createTools(config) {
2716
3190
  const teaching = receiptId && isGateReceiptUnavailable(response.status, response.json)
2717
3191
  ? gateReceiptUnavailableTeaching(name, receiptId)
2718
3192
  : undefined;
2719
- return practiceErrorResult(response.status, response.json, response.retryAfterSeconds, teaching);
3193
+ return practiceErrorResult(response.status, response.json, response.retryAfterSeconds, teaching, teaching ? 'pre-commit' : undefined);
2720
3194
  }
2721
- return textResult(secretSafe(response.json));
3195
+ return configuredReceiptGateResult(secretSafe(response.json), 'evaluation recorded');
2722
3196
  }
2723
3197
  case 'gate_verdict': {
2724
3198
  const runId = args.runId === undefined ? '' : requiredString(args, 'runId');
@@ -2730,7 +3204,7 @@ export function createTools(config) {
2730
3204
  if (!latest.ok) return practiceErrorResult(latest.status, latest.json, latest.retryAfterSeconds);
2731
3205
  const evaluation = latest.json?.evaluations?.[0];
2732
3206
  if (!evaluation) {
2733
- throw new Error('No Receipt Gate evaluation is available yet. Finish a practice simulation run and keep its pa-* receiptId; call gate_configure({ receiptId, enabled: true }); call gate_evaluate({}); then call gate_verdict({}) again.');
3207
+ throw new Error("No configured Receipt Gate exists. The receipt's recorded API behavior remains available. Finish a practice simulation run and keep its pa-* receiptId; call gate_configure({ receiptId, enabled: true }); call gate_evaluate({}); then call gate_verdict({}) again.");
2734
3208
  }
2735
3209
  const verdict = evaluation.gateVerdict;
2736
3210
  if (!verdict || verdict.schemaVersion !== 'meguro.gate-verdict.v1') {
@@ -2738,9 +3212,9 @@ export function createTools(config) {
2738
3212
  const recovery = receiptId
2739
3213
  ? `gate_evaluate({ receiptId: ${JSON.stringify(receiptId)} })`
2740
3214
  : 'gate_evaluate({})';
2741
- throw new Error(`The latest Receipt Gate row predates reproducible gate policy v1 evidence. Re-evaluate it with ${recovery}, then call gate_verdict({}) again; Meguro will not invent missing cited facts from historical compact status.`);
3215
+ throw new Error(`The latest Configured Receipt Gate row predates reproducible gate policy v1 evidence. Re-evaluate it with ${recovery}, then call gate_verdict({}) again; Meguro will not invent missing cited facts from historical compact status.`);
2742
3216
  }
2743
- return textResult(secretSafe(verdict));
3217
+ return configuredReceiptGateResult(secretSafe(verdict), 'verdict read');
2744
3218
  }
2745
3219
  const response = await practiceApi('GET', `/practice/evaluation-suites/${encodeURIComponent(runId)}`);
2746
3220
  if (!response.ok && response.status === 404) return legacyGateArtifactNotFoundGuidance(runId);
@@ -2751,32 +3225,51 @@ export function createTools(config) {
2751
3225
  }
2752
3226
  // Exact pass-through is the parity contract: Console and MCP consume these same bytes as
2753
3227
  // an object, with no local policy derivation in this registry.
2754
- return textResult(verdict);
3228
+ return configuredReceiptGateResult(verdict, 'legacy verdict read');
2755
3229
  }
2756
3230
  case 'runs_list': {
2757
3231
  const lifecycle = args.lifecycle === undefined ? undefined : requiredString(args, 'lifecycle');
2758
3232
  if (lifecycle && !['running', 'paused', 'completed', 'failed', 'cleaning'].includes(lifecycle)) {
2759
3233
  throw new Error('lifecycle must be running, paused, completed, failed, or cleaning');
2760
3234
  }
3235
+ const query = new URLSearchParams({
3236
+ limit: '200',
3237
+ ...(lifecycle ? { lifecycle } : {}),
3238
+ });
2761
3239
  return textResult(runsListProjection(
2762
- await api('GET', '/runs?limit=200', undefined, { auth: true }),
3240
+ await api('GET', `/runs?${query}`, undefined, { auth: true }),
2763
3241
  lifecycle,
2764
3242
  ));
2765
3243
  }
2766
- case 'usage_read':
2767
- return textResult(usageProjection(await api('GET', '/account/usage', undefined, { auth: true })));
3244
+ case 'usage_read': {
3245
+ const workspaceId = optionalWorkspaceId(args);
3246
+ const usage = await api('GET', '/account/usage', undefined, { auth: true });
3247
+ if (args.continueFromAttemptId !== undefined && args.storeId === undefined) {
3248
+ throw new Error('usage_read continueFromAttemptId requires storeId');
3249
+ }
3250
+ let eligibility = usage?.runStartEligibility;
3251
+ if (args.storeId !== undefined) {
3252
+ const storeId = validatedStoreId(requiredString(args, 'storeId'), 'storeId');
3253
+ const continueFromAttemptId = args.continueFromAttemptId === undefined
3254
+ ? undefined
3255
+ : requiredPracticeAttemptId(args, 'continueFromAttemptId');
3256
+ const query = continueFromAttemptId
3257
+ ? `?continueFromAttemptId=${encodeURIComponent(continueFromAttemptId)}`
3258
+ : '';
3259
+ const response = await practiceApi('GET', `/practice/stores/${encodeURIComponent(storeId)}/playbacks/start-eligibility${query}`, undefined, { workspaceId });
3260
+ if (!response.ok) return practiceErrorResult(response.status, response.json, response.retryAfterSeconds);
3261
+ eligibility = response.json;
3262
+ }
3263
+ return textResult(usageProjection(usage, eligibility));
3264
+ }
2768
3265
  case 'twin_diff': {
2769
3266
  const baselineRunId = requiredString(args, 'baselineRunId');
2770
3267
  const treatedRunId = requiredString(args, 'treatedRunId');
2771
3268
  if (PRACTICE_ATTEMPT_ID.test(baselineRunId) || PRACTICE_ATTEMPT_ID.test(treatedRunId)) {
2772
3269
  return practiceAttemptComparisonGuidanceFor(name, baselineRunId, treatedRunId);
2773
3270
  }
2774
- return textResult(twinDiffProjection(await api(
2775
- 'GET',
2776
- `/runs/${encodeURIComponent(baselineRunId)}/twin-diff/${encodeURIComponent(treatedRunId)}`,
2777
- undefined,
2778
- { auth: true },
2779
- )));
3271
+ const response = await apiRaw('GET', `/runs/${encodeURIComponent(baselineRunId)}/twin-diff/${encodeURIComponent(treatedRunId)}`);
3272
+ return response.ok ? textResult(twinDiffProjection(response.json)) : historyReadErrorResult(response);
2780
3273
  }
2781
3274
  case 'exam_preflight': {
2782
3275
  const attemptId = requiredPracticeAttemptId(args);
@@ -2814,6 +3307,12 @@ export function createTools(config) {
2814
3307
  expectedRevision: revision,
2815
3308
  commandId: `mcp-exam-execute-${examId}-${revision}`,
2816
3309
  }, attemptId);
3310
+ } else if ((exam.status === 'completed' || exam.status === 'cleaning') && !executorAdvanced) {
3311
+ executorAdvanced = true;
3312
+ advanced = await examRequest(name, 'POST', `/practice-exams/${encodeURIComponent(examId)}/cleanup`, {
3313
+ expectedRevision: revision,
3314
+ commandId: `mcp-exam-cleanup-${examId}-${revision}`,
3315
+ }, attemptId);
2817
3316
  } else {
2818
3317
  break;
2819
3318
  }
@@ -2834,18 +3333,14 @@ export function createTools(config) {
2834
3333
  const response = await examRequest(name, 'GET', `/practice-exams/${encodeURIComponent(examId)}`);
2835
3334
  if ('error' in response) return response.error;
2836
3335
  const projected = examLifecycleProjection(response.value);
2837
- if (!['blocked', 'completed', 'failed', 'cleaned'].includes(projected.exam.status)) {
3336
+ if (projected.exam.status === 'completed' || projected.exam.status === 'cleaning') {
3337
+ return errorResult(`Shopify Exam ${examId} is not final; call exam_start({ attemptId: "${projected.exam.attemptId}", shopDomain: "${projected.exam.shopDomain}" }) to continue cleanup, then call exam_status before requesting its final report`);
3338
+ }
3339
+ if (!['blocked', 'failed', 'cleaned'].includes(projected.exam.status)) {
2838
3340
  return errorResult(`Shopify Exam ${examId} is ${projected.exam.status}; call exam_status and continue bounded execution before requesting its immutable receipts`);
2839
3341
  }
2840
3342
  if (projected.receipts.retention?.status === 'expired') {
2841
- const retention = projected.receipts.retention;
2842
- return errorResult(
2843
- retention.tier === 'developer'
2844
- ? 'Free keeps receipts available for 7 days. Solo keeps receipts available for 90 days; Builder keeps them available for 365 days. Upgrade, then retry this receipt if its age fits the new window, or start a new run.'
2845
- : retention.tier === 'solo'
2846
- ? 'Solo keeps receipts available for 90 days. Builder keeps receipts available for 365 days. Upgrade, then retry this receipt if its age fits the new window, or start a new run.'
2847
- : 'Builder keeps receipts available for 365 days. Start a new run to mint a fresh receipt.',
2848
- );
3343
+ return errorResult(projected.receipts.retention.expiredMessage);
2849
3344
  }
2850
3345
  if (!projected.receipts.privateAvailable || !projected.receipts.publicAvailable) {
2851
3346
  return errorResult(`Shopify Exam ${examId} does not yet carry both immutable receipts`);
@@ -2868,6 +3363,7 @@ export function createTools(config) {
2868
3363
  }
2869
3364
  case 'practice_run_start': {
2870
3365
  const storeId = requiredPracticeStoreId(args);
3366
+ const workspaceId = optionalWorkspaceId(args);
2871
3367
  const body = {
2872
3368
  purpose: 'external-agent',
2873
3369
  ...(args.continueFromAttemptId !== undefined ? { continueFromAttemptId: requiredPracticeAttemptId(args, 'continueFromAttemptId') } : {}),
@@ -2878,8 +3374,17 @@ export function createTools(config) {
2878
3374
  ...(args.assertionPlan !== undefined ? { assertionPlan: args.assertionPlan } : {}),
2879
3375
  ...(args.graphqlThrottleMode !== undefined ? { graphqlThrottleMode: args.graphqlThrottleMode } : {}),
2880
3376
  };
2881
- const response = await practiceApi('POST', `/practice/stores/${encodeURIComponent(storeId)}/playbacks`, body);
2882
- if (!response.ok) return practiceErrorResult(response.status, response.json, response.retryAfterSeconds);
3377
+ const response = await practiceApi('POST', `/practice/stores/${encodeURIComponent(storeId)}/playbacks`, body, { workspaceId });
3378
+ if (!response.ok) {
3379
+ const unavailable = isPracticeStoreUnavailable(response.status, response.json);
3380
+ return practiceErrorResult(
3381
+ response.status,
3382
+ response.json,
3383
+ response.retryAfterSeconds,
3384
+ unavailable ? practiceStoreUnavailableTeaching(storeId) : undefined,
3385
+ unavailable ? 'pre-commit' : undefined,
3386
+ );
3387
+ }
2883
3388
  const started = response.json ?? {};
2884
3389
  return textResult(secretSafe({
2885
3390
  operation: 'start',
@@ -2905,16 +3410,23 @@ export function createTools(config) {
2905
3410
  ? undefined
2906
3411
  : requiredPracticeAttemptId(args, 'receiptId');
2907
3412
  if (receiptId) {
2908
- const listed = await fleetRequest(name, 'GET', '/practice/playbacks', undefined, { workspaceId });
2909
- if ('error' in listed) return listed.error;
2910
- const playbacks = Array.isArray(listed.value?.playbacks) ? listed.value.playbacks : [];
2911
- const referenced = playbacks.find((attempt) => attempt?.attemptId === receiptId);
2912
- if (!referenced) return practiceRunReceiptSelectionError(receiptId);
3413
+ const pointPath = `/practice/playbacks/${encodeURIComponent(receiptId)}/state`;
3414
+ const point = await apiRaw('GET', pointPath, undefined, { workspaceId });
3415
+ if (!point.ok) {
3416
+ const authenticationError = authenticationErrorResult(point.status, point.json);
3417
+ if (authenticationError) return authenticationError;
3418
+ if (point.status === 404) return practiceRunReceiptSelectionError(receiptId);
3419
+ return fleetErrorResult(name, pointPath, point.status, point.json, point.requestId);
3420
+ }
3421
+ const referenced = point.json ?? {};
2913
3422
  const receiptStoreId = String(referenced.storeId ?? referenced.worldId ?? '');
2914
3423
  if (requestedStoreId && requestedStoreId !== receiptStoreId) {
2915
3424
  return practiceRunReceiptStoreMismatch(receiptId, receiptStoreId, requestedStoreId);
2916
3425
  }
2917
- return textResult(practiceRunsProjection(listed.value, receiptStoreId, 'receipt-derived'));
3426
+ const listPath = queryPath('/practice/playbacks', { worldId: receiptStoreId });
3427
+ const listed = await fleetRequest(name, 'GET', listPath, undefined, { workspaceId });
3428
+ if ('error' in listed) return listed.error;
3429
+ return textResult(practiceRunsProjection(listed.value, receiptStoreId, 'receipt-derived', workspaceId));
2918
3430
  }
2919
3431
 
2920
3432
  const fleet = await fleetRequest(name, 'GET', '/practice/stores', undefined, { workspaceId });
@@ -2938,12 +3450,19 @@ export function createTools(config) {
2938
3450
  listed.value,
2939
3451
  selectedStoreId,
2940
3452
  requestedStoreId ? 'explicit-store-id' : 'single-store',
3453
+ workspaceId,
2941
3454
  ));
2942
3455
  }
2943
3456
  case 'practice_run_status': {
2944
3457
  const attemptId = requiredPracticeAttemptId(args);
2945
- const response = await practiceApi('GET', `/practice/playbacks/${encodeURIComponent(attemptId)}/state`);
2946
- if (!response.ok) return practiceErrorResult(response.status, response.json, response.retryAfterSeconds);
3458
+ const workspaceId = optionalWorkspaceId(args);
3459
+ const response = await practiceApi('GET', `/practice/playbacks/${encodeURIComponent(attemptId)}/state`, undefined, { workspaceId });
3460
+ if (!response.ok) {
3461
+ const teaching = isPracticeRunUnavailable(response.status, response.json)
3462
+ ? practiceRunUnavailableTeaching(name, attemptId)
3463
+ : undefined;
3464
+ return practiceErrorResult(response.status, response.json, response.retryAfterSeconds, teaching);
3465
+ }
2947
3466
  const state = response.json ?? {};
2948
3467
  return textResult(secretSafe({
2949
3468
  operation: 'status',
@@ -2954,7 +3473,8 @@ export function createTools(config) {
2954
3473
  }
2955
3474
  case 'practice_run_checkpoint': {
2956
3475
  const attemptId = requiredPracticeAttemptId(args);
2957
- const response = await practiceApi('POST', `/practice/playbacks/${encodeURIComponent(attemptId)}/checkpoint`, {});
3476
+ const workspaceId = optionalWorkspaceId(args);
3477
+ const response = await practiceApi('POST', `/practice/playbacks/${encodeURIComponent(attemptId)}/checkpoint`, {}, { workspaceId });
2958
3478
  if (!response.ok) {
2959
3479
  const teaching = isPracticeRunUnavailable(response.status, response.json)
2960
3480
  ? practiceRunUnavailableTeaching(name, attemptId)
@@ -2965,24 +3485,31 @@ export function createTools(config) {
2965
3485
  operation: 'checkpoint',
2966
3486
  attemptId,
2967
3487
  alreadyCaptured: response.json?.alreadyCaptured === true,
3488
+ advanceCursor: response.json?.advanceCursor,
2968
3489
  checkpoint: checkpointProjection(response.json?.checkpoint),
2969
3490
  ...consoleUrlFields(response.json?.consoleRef, response.json?.consoleRefExpiresAt),
2970
3491
  }));
2971
3492
  }
2972
3493
  case 'practice_run_advance': {
2973
3494
  const attemptId = requiredPracticeAttemptId(args);
3495
+ const workspaceId = optionalWorkspaceId(args);
2974
3496
  const hasDays = args.days !== undefined;
2975
3497
  const hasUntil = args.until !== undefined;
2976
3498
  if (hasDays === hasUntil) throw new Error('practice_run_advance requires exactly one of days or until');
2977
- const expectedDay = requiredNonNegativeInteger(args, 'expectedDay');
2978
- const expectedCallSeq = requiredNonNegativeInteger(args, 'expectedCallSeq', -1);
3499
+ const advanceCursor = args.advanceCursor;
2979
3500
  if (hasDays && (!Number.isSafeInteger(args.days) || args.days < 1)) throw new Error('days must be a positive integer');
2980
3501
  if (hasUntil && (!args.until || typeof args.until !== 'object' || Array.isArray(args.until))) throw new Error('until must be an object');
2981
3502
  const response = await practiceApi('POST', `/practice/playbacks/${encodeURIComponent(attemptId)}/advance`, {
2982
- ...(hasDays ? { days: args.days } : { until: args.until }), expectedDay, expectedCallSeq,
3503
+ ...(hasDays ? { days: args.days } : { until: args.until }),
3504
+ advanceCursor,
2983
3505
  ...(args.confirmConclusion === true ? { confirmConclusion: true } : {}),
2984
- });
2985
- if (!response.ok) return practiceErrorResult(response.status, response.json, response.retryAfterSeconds);
3506
+ }, { workspaceId });
3507
+ if (!response.ok) {
3508
+ const teaching = isPracticeRunUnavailable(response.status, response.json)
3509
+ ? practiceRunUnavailableTeaching(name, attemptId)
3510
+ : undefined;
3511
+ return practiceErrorResult(response.status, response.json, response.retryAfterSeconds, teaching);
3512
+ }
2986
3513
  const advanced = response.json ?? {};
2987
3514
  return textResult(secretSafe({
2988
3515
  operation: 'advance',
@@ -2997,7 +3524,13 @@ export function createTools(config) {
2997
3524
  }
2998
3525
  case 'practice_run_finish': {
2999
3526
  const attemptId = requiredPracticeAttemptId(args);
3000
- const response = await practiceApi('POST', `/practice/playbacks/${encodeURIComponent(attemptId)}/finish`, {});
3527
+ const workspaceId = optionalWorkspaceId(args);
3528
+ const response = await practiceApi('POST', `/practice/playbacks/${encodeURIComponent(attemptId)}/finish`, {
3529
+ ...(args.permanentStop === true ? { permanentStop: true } : {}),
3530
+ ...(args.acknowledgeIncompleteOnboardingEvaluation === true
3531
+ ? { acknowledgeIncompleteOnboardingEvaluation: true }
3532
+ : {}),
3533
+ }, { workspaceId });
3001
3534
  if (!response.ok) {
3002
3535
  const teaching = isPracticeRunUnavailable(response.status, response.json)
3003
3536
  ? practiceRunUnavailableTeaching(name, attemptId)
@@ -3013,8 +3546,14 @@ export function createTools(config) {
3013
3546
  }
3014
3547
  case 'practice_run_report': {
3015
3548
  const attemptId = requiredPracticeAttemptId(args);
3016
- const response = await practiceApi('GET', `/practice/playbacks/${encodeURIComponent(attemptId)}/report?v=2`);
3017
- if (!response.ok) return practiceErrorResult(response.status, response.json, response.retryAfterSeconds);
3549
+ const workspaceId = optionalWorkspaceId(args);
3550
+ const response = await practiceApi('GET', `/practice/playbacks/${encodeURIComponent(attemptId)}/report?v=2`, undefined, { workspaceId });
3551
+ if (!response.ok) {
3552
+ const teaching = isPracticeRunUnavailable(response.status, response.json)
3553
+ ? practiceRunUnavailableTeaching(name, attemptId)
3554
+ : undefined;
3555
+ return practiceErrorResult(response.status, response.json, response.retryAfterSeconds, teaching);
3556
+ }
3018
3557
  return textResult({
3019
3558
  operation: 'report',
3020
3559
  ...receiptProjection(response.json ?? {}),
@@ -3023,17 +3562,24 @@ export function createTools(config) {
3023
3562
  }
3024
3563
  case 'practice_run_impact': {
3025
3564
  const attemptId = requiredPracticeAttemptId(args);
3026
- const response = await practiceApi('GET', `/practice/playbacks/${encodeURIComponent(attemptId)}/impact`);
3027
- if (!response.ok) return practiceErrorResult(response.status, response.json, response.retryAfterSeconds);
3565
+ const workspaceId = optionalWorkspaceId(args);
3566
+ const response = await practiceApi('GET', `/practice/playbacks/${encodeURIComponent(attemptId)}/impact`, undefined, { workspaceId });
3567
+ if (!response.ok) {
3568
+ const teaching = isPracticeRunUnavailable(response.status, response.json)
3569
+ ? practiceRunUnavailableTeaching(name, attemptId)
3570
+ : undefined;
3571
+ return practiceErrorResult(response.status, response.json, response.retryAfterSeconds, teaching);
3572
+ }
3028
3573
  // T1 owns the shape and the API already ran the public-safe scan; pass it through verbatim
3029
3574
  // (no array truncation) rather than re-projecting.
3030
3575
  return textResult({ operation: 'impact', ...(response.json ?? {}) });
3031
3576
  }
3032
3577
  case 'get_connection_details': {
3033
3578
  const requestedStoreId = requiredPracticeStoreId(args);
3579
+ const workspaceId = optionalWorkspaceId(args);
3034
3580
  let details;
3035
3581
  try {
3036
- details = await api('GET', `/practice/${encodeURIComponent(requestedStoreId)}/connection`, undefined, { auth: true });
3582
+ details = await api('GET', `/practice/${encodeURIComponent(requestedStoreId)}/connection`, undefined, { auth: true, workspaceId });
3037
3583
  } catch (error) {
3038
3584
  if (error instanceof ToolResultError) throw error;
3039
3585
  if (error instanceof Error && error.message.startsWith('404 ')) {
@@ -3041,8 +3587,8 @@ export function createTools(config) {
3041
3587
  }
3042
3588
  throw error;
3043
3589
  }
3044
- const storeId = details.storeId ?? details.worldId ?? requestedStoreId;
3045
- const shopDomain = details.host ?? `${details.worldId ?? requestedStoreId}.meguro.io`;
3590
+ const storeId = details.storeId ?? requestedStoreId;
3591
+ const shopDomain = details.host ?? `${requestedStoreId}.meguro.io`;
3046
3592
  return textResult({
3047
3593
  identity: details.identity,
3048
3594
  // Canonical practice-store id first: pass it straight into practice_run_start({ storeId }).
@@ -3064,6 +3610,7 @@ export function createTools(config) {
3064
3610
  }
3065
3611
  case 'admin_probe': {
3066
3612
  const worldId = requiredWorldId(args);
3613
+ const workspaceId = optionalWorkspaceId(args);
3067
3614
  const body = {
3068
3615
  query: typeof args.query === 'string' ? args.query : '',
3069
3616
  ...(args.apiVersion !== undefined ? { apiVersion: args.apiVersion } : {}),
@@ -3073,7 +3620,7 @@ export function createTools(config) {
3073
3620
  // MEG-34: always ask the server to mint a durable Console reference for a successful probe.
3074
3621
  createConsoleReference: true,
3075
3622
  };
3076
- const { ok, status, json } = await apiRaw('POST', `/practice/${encodeURIComponent(worldId)}/probe/admin`, body);
3623
+ const { ok, status, json } = await apiRaw('POST', `/practice/${encodeURIComponent(worldId)}/probe/admin`, body, { workspaceId });
3077
3624
  if (ok) {
3078
3625
  // Replace the internal reference id with a ready-to-open Console URL (opaque ref only).
3079
3626
  const { consoleRef, consoleRefExpiresAt, ...envelope } = json;
@@ -3086,17 +3633,27 @@ export function createTools(config) {
3086
3633
  return { content: [{ type: 'text', text: JSON.stringify(secretSafe(json), null, 2) }], isError: true };
3087
3634
  }
3088
3635
  case 'admin_schema': {
3089
- const lookup = requiredString(args, 'lookup');
3090
- const lookupName = requiredString(args, 'name');
3091
- const body = {
3092
- lookup,
3093
- name: lookupName,
3094
- ...(args.apiVersion !== undefined ? { apiVersion: args.apiVersion } : {}),
3095
- };
3636
+ // MEG-1164: two exclusive modes on one tool. A submitted `query` asks what refusal the
3637
+ // served Admin surface would return for that document; `lookup` + `name` is the unchanged
3638
+ // schema lookup. Neither executes an Admin operation or touches practice-store state.
3639
+ const body = args.query !== undefined
3640
+ ? {
3641
+ query: requiredString(args, 'query'),
3642
+ ...(args.variables !== undefined ? { variables: args.variables } : {}),
3643
+ ...(args.operationName !== undefined ? { operationName: args.operationName } : {}),
3644
+ ...(args.apiVersion !== undefined ? { apiVersion: args.apiVersion } : {}),
3645
+ }
3646
+ : {
3647
+ lookup: requiredString(args, 'lookup'),
3648
+ name: requiredString(args, 'name'),
3649
+ ...(args.apiVersion !== undefined ? { apiVersion: args.apiVersion } : {}),
3650
+ };
3096
3651
  return textResult(await api('POST', '/practice/admin-schema', body, { auth: true }));
3097
3652
  }
3098
3653
  case 'admin_recipes_list':
3099
3654
  return textResult(await api('GET', '/practice/admin-recipes', undefined, { auth: true }));
3655
+ case 'plan_validate':
3656
+ return textResult(await api('POST', '/practice/plan-validate', { operations: args.operations }, { auth: true }));
3100
3657
  default:
3101
3658
  return errorResult(`Unknown tool: ${name}`);
3102
3659
  }
@@ -3109,24 +3666,32 @@ export function createTools(config) {
3109
3666
  meaning: 'The fleet tool arguments were invalid or the request could not be sent.',
3110
3667
  nextStep: 'Correct the arguments shown by the tool schema and retry.',
3111
3668
  }],
3112
- });
3669
+ }, undefined, CONDITIONAL_TOOL_BRANCHES.has(name) ? toolInvocationMutates(name, args) : undefined);
3113
3670
  }
3114
- return errorResult(error instanceof Error ? error.message : String(error));
3671
+ const failed = errorResult(error instanceof Error ? error.message : String(error));
3672
+ return typeof error?.operationCommitBoundary === 'string'
3673
+ ? withOperationCommitBoundary(failed, error.operationCommitBoundary)
3674
+ : failed;
3115
3675
  }
3116
3676
  }
3117
3677
 
3118
3678
  async function call(name, args = {}) {
3119
- const mutates = TOOL_PRESENTATION[name]?.readOnlyHint === false;
3679
+ const mutates = toolInvocationMutates(name, args);
3120
3680
  try {
3121
3681
  validateTopLevelArguments(name, args);
3122
3682
  } catch (error) {
3683
+ if ((name === 'workspace_archive' || name === 'workspace_unarchive')
3684
+ && error instanceof Error
3685
+ && error.message.startsWith('workspaceId')) {
3686
+ return workspaceLifecycleIdentityGuidance(name, args?.workspaceId, error.message);
3687
+ }
3123
3688
  const result = errorResult(error instanceof Error ? error.message : String(error));
3124
3689
  return mutates ? withOperationCommitBoundary(result, 'pre-commit') : result;
3125
3690
  }
3126
- const result = await dispatch(name, args);
3127
- return mutates && result?.isError === true
3128
- ? withOperationCommitBoundary(result, 'unknown')
3129
- : result;
3691
+ // MEG-1054: no generic derivation here. A failed mutation carries only the classification the
3692
+ // control plane declared for it; an undeclared outcome falls through to the single `unknown`
3693
+ // default at the resource boundary rather than being asserted twice.
3694
+ return dispatch(name, args);
3130
3695
  }
3131
3696
 
3132
3697
  return { definitions, call };