meguro-mcp 0.2.10 → 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 },
@@ -80,12 +77,23 @@ const TOOL_PRESENTATION = Object.freeze({
80
77
  get_connection_details: { title: 'Get practice-store connection details', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
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 },
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 },
83
82
  });
84
83
 
85
84
  function textResult(value) {
86
85
  return { content: [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value, null, 2) }] };
87
86
  }
88
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
+
89
97
  function errorResult(message) {
90
98
  return { content: [{ type: 'text', text: scrubString(message) }], isError: true };
91
99
  }
@@ -108,19 +116,60 @@ export function redactSecrets(value) {
108
116
  return scrubString(value);
109
117
  }
110
118
 
111
- 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) {
112
127
  if (depth > 12) return '[TRUNCATED]';
113
- if (typeof value === 'string') return scrubString(value).slice(0, 10_000);
114
- 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
+ }
115
148
  if (value && typeof value === 'object') {
116
- return Object.fromEntries(Object.entries(value)
117
- .filter(([key]) => !SECRET_KEY.test(key) && !PRIVATE_RESPONSE_KEY.test(key))
118
- .slice(0, 100)
119
- .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
+ ]));
120
159
  }
121
160
  return value;
122
161
  }
123
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
+
124
173
  function authenticationErrorResult(status, value) {
125
174
  if (status !== 401 && status !== 403) return null;
126
175
  const body = value && typeof value === 'object' ? value : {};
@@ -176,8 +225,55 @@ function catalogSnapshotProjection(value) {
176
225
  : safe;
177
226
  }
178
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
+ */
179
273
  const STORE_TEMPLATE_FIELDS = Object.freeze([
180
274
  'key',
275
+ 'revision',
276
+ 'evaluationContext',
181
277
  'label',
182
278
  'category',
183
279
  'recommended',
@@ -192,47 +288,204 @@ const STORE_TEMPLATE_FIELDS = Object.freeze([
192
288
  'extendedRunDays',
193
289
  'whatAgentGets',
194
290
  'supportedReads',
291
+ 'modeledEvidence',
195
292
  'liveRun',
196
293
  'goodFirstAgents',
197
294
  'supportedWrites',
295
+ 'operationContracts',
198
296
  'knownUnsupported',
297
+ 'unsupportedContracts',
298
+ 'capabilityBoundaries',
299
+ 'modeledPhysics',
199
300
  'goodAgentShould',
200
301
  'avoid',
201
302
  'reportGrades',
202
303
  ]);
203
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
+
204
321
  function storeTemplatesProjection(value) {
205
322
  if (!Array.isArray(value?.storeTemplates)) {
206
323
  throw new Error('Meguro template catalog response did not contain storeTemplates');
207
324
  }
208
- const starterTemplateKeys = value.storeTemplates
209
- .filter((template) => template?.recommended === true && typeof template?.key === 'string')
210
- .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);
211
340
  const declaredStarterBasis = typeof value?.starterRecommendation?.basis === 'string'
212
341
  && /^[a-z0-9-]{1,64}$/u.test(value.starterRecommendation.basis)
213
342
  ? value.starterRecommendation.basis
214
343
  : 'starter-for-first-simulation';
344
+ const projectStarterPlan = (starterPlan) => {
345
+ const positiveInteger = (candidate) => Number.isSafeInteger(candidate) && candidate > 0;
346
+ const nonNegativeInteger = (candidate) => Number.isSafeInteger(candidate) && candidate >= 0;
347
+ const valid = starterPlan
348
+ && typeof starterPlan === 'object'
349
+ && !Array.isArray(starterPlan)
350
+ && starterPlan.schemaVersion === 'meguro.practice-run-starter-plan.v1'
351
+ && ['developer', 'solo', 'builder'].includes(starterPlan.tier)
352
+ && typeof starterPlan.templateKey === 'string'
353
+ && catalogTemplateKeys.includes(starterPlan.templateKey)
354
+ && positiveInteger(starterPlan.idealRunDays)
355
+ && positiveInteger(starterPlan.scenarioArcDays)
356
+ && positiveInteger(starterPlan.firstSegmentDays)
357
+ && starterPlan.practiceRunStart
358
+ && typeof starterPlan.practiceRunStart === 'object'
359
+ && starterPlan.practiceRunStart.clock
360
+ && typeof starterPlan.practiceRunStart.clock === 'object'
361
+ && positiveInteger(starterPlan.practiceRunStart.clock.simulationDays)
362
+ && starterPlan.continuation
363
+ && typeof starterPlan.continuation === 'object'
364
+ && typeof starterPlan.continuation.available === 'boolean'
365
+ && nonNegativeInteger(starterPlan.continuation.remainingArcDaysAfterFirstSegment)
366
+ && (starterPlan.continuation.remainingAuthorizedDaysAfterFirstSegment === undefined
367
+ || nonNegativeInteger(starterPlan.continuation.remainingAuthorizedDaysAfterFirstSegment))
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
+ ))
374
+ && starterPlan.usage
375
+ && typeof starterPlan.usage === 'object'
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
+ ))
387
+ && nonNegativeInteger(starterPlan.usage.remainingSimulationRuns);
388
+ if (!valid) throw new Error('Meguro template catalog contained an invalid starterPlan');
389
+ return {
390
+ schemaVersion: starterPlan.schemaVersion,
391
+ tier: starterPlan.tier,
392
+ templateKey: starterPlan.templateKey,
393
+ idealRunDays: starterPlan.idealRunDays,
394
+ scenarioArcDays: starterPlan.scenarioArcDays,
395
+ firstSegmentDays: starterPlan.firstSegmentDays,
396
+ ...(starterPlan.authorization ? { authorization: {
397
+ source: starterPlan.authorization.source,
398
+ idealHorizonDays: starterPlan.authorization.idealHorizonDays,
399
+ } } : {}),
400
+ practiceRunStart: { clock: { simulationDays: starterPlan.practiceRunStart.clock.simulationDays } },
401
+ continuation: {
402
+ available: starterPlan.continuation.available,
403
+ remainingArcDaysAfterFirstSegment: starterPlan.continuation.remainingArcDaysAfterFirstSegment,
404
+ ...(starterPlan.continuation.remainingAuthorizedDaysAfterFirstSegment !== undefined
405
+ ? { remainingAuthorizedDaysAfterFirstSegment: starterPlan.continuation.remainingAuthorizedDaysAfterFirstSegment }
406
+ : {}),
407
+ maxCanvasWorldDays: starterPlan.continuation.maxCanvasWorldDays,
408
+ },
409
+ usage: {
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
+ } : {}),
421
+ remainingSimulationRuns: starterPlan.usage.remainingSimulationRuns,
422
+ },
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;
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;
215
465
  return {
216
466
  starterRecommendation: {
217
467
  basis: declaredStarterBasis,
218
468
  templateKeys: starterTemplateKeys,
469
+ ...(projectedStarterPlan ? { starterPlan: projectedStarterPlan } : {}),
470
+ ...(projectedStarterPlans ? { starterPlans: projectedStarterPlans } : {}),
471
+ ...(recommendationUnavailable ? { recommendationUnavailable } : {}),
219
472
  },
220
- storeTemplates: value.storeTemplates.map((template) => {
221
- if (!template || typeof template !== 'object' || Array.isArray(template)) {
222
- throw new Error('Meguro template catalog contained an invalid template entry');
223
- }
224
- return Object.fromEntries(STORE_TEMPLATE_FIELDS
225
- .filter((field) => Object.hasOwn(template, field))
226
- .map((field) => [field, secretSafe(template[field], 1)]));
227
- }),
473
+ storeTemplates,
228
474
  };
229
475
  }
230
476
 
231
- function practiceErrorResult(status, value, retryAfterSeconds, teaching) {
477
+ function practiceErrorResult(status, value, retryAfterSeconds, teaching, declaredBoundary) {
232
478
  const authenticationError = authenticationErrorResult(status, value);
233
479
  if (authenticationError) return authenticationError;
234
480
  const body = value && typeof value === 'object' ? value : {};
235
- const stable = { httpStatus: status };
481
+ const stable = { httpStatus: status, ...enforcementProjection(body) };
482
+ if (body.code === 'terminal-advance-confirmation-required') {
483
+ stable.code = body.code;
484
+ stable.terminalAdvance = secretSafe(body.terminalAdvance);
485
+ }
486
+ if (body.advanceCursor && typeof body.advanceCursor === 'object') {
487
+ stable.advanceCursor = secretSafe(body.advanceCursor);
488
+ }
236
489
  if (['requested-exceeds-cap', 'continuation-unavailable', 'limit-reached'].includes(body.reason)) {
237
490
  stable.reason = body.reason;
238
491
  }
@@ -253,7 +506,16 @@ function practiceErrorResult(status, value, retryAfterSeconds, teaching) {
253
506
  else if (Number.isFinite(bodyRetryAfter) && bodyRetryAfter >= 0) stable.retryAfterSeconds = bodyRetryAfter;
254
507
  if (typeof body.retryable === 'boolean') stable.retryable = body.retryable;
255
508
  if (teaching) stable.teaching = secretSafe(teaching);
256
- 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;
257
519
  }
258
520
 
259
521
  function practiceRunUnavailableTeaching(toolName, attemptId) {
@@ -266,22 +528,30 @@ function practiceRunUnavailableTeaching(toolName, attemptId) {
266
528
 
267
529
  function gateReceiptUnavailableTeaching(toolName, receiptId) {
268
530
  return {
269
- 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.`,
270
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>" }).`,
271
- 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.`,
272
542
  };
273
543
  }
274
544
 
275
545
  function isPracticeRunUnavailable(status, value) {
276
- return status === 404 && value?.practiceRunError?.code === 'practice-run-unavailable';
546
+ return status === 404;
277
547
  }
278
548
 
279
549
  function isGateReceiptUnavailable(status, value) {
280
- if (status !== 404) return false;
281
- const messages = Array.isArray(value?.errors)
282
- ? value.errors.map((error) => String(error?.message ?? ''))
283
- : [String(value?.error?.message ?? value?.error ?? '')];
284
- 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;
285
555
  }
286
556
 
287
557
  function requiredString(args, key) {
@@ -311,11 +581,11 @@ function requiredWorkspaceId(args) {
311
581
  return workspaceId;
312
582
  }
313
583
 
314
- function requiredShareId(args) {
315
- const shareId = requiredString(args, 'shareId');
316
- 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');
317
- return shareId;
318
- }
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
+ });
319
589
 
320
590
  function optionalStringList(args, key, options = {}) {
321
591
  const value = args?.[key];
@@ -351,6 +621,115 @@ function requiredSavedSliceId(args) {
351
621
  return sliceId;
352
622
  }
353
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
+
354
733
  const PUBLIC_RECEIPT_REFERENCE_INPUT_SCHEMA = Object.freeze({
355
734
  type: 'object',
356
735
  additionalProperties: false,
@@ -366,26 +745,6 @@ const PUBLIC_RECEIPT_REFERENCE_INPUT_SCHEMA = Object.freeze({
366
745
  required: ['schemaVersion', 'publicReceiptId', 'kind', 'revision', 'issuedAt', 'sha256', 'canonicalPath'],
367
746
  });
368
747
 
369
- const SHARE_ARTIFACT_REF_INPUT_SCHEMA = Object.freeze({
370
- oneOf: [
371
- PUBLIC_RECEIPT_REFERENCE_INPUT_SCHEMA,
372
- { type: 'object', additionalProperties: false, properties: { attemptId: { type: 'string', minLength: 1 } }, required: ['attemptId'] },
373
- { type: 'object', additionalProperties: false, properties: { runId: { type: 'string', minLength: 1 } }, required: ['runId'] },
374
- { type: 'object', additionalProperties: false, properties: { examId: { type: 'string', pattern: '^pex-[0-9a-f]{24}$' } }, required: ['examId'] },
375
- {
376
- type: 'object',
377
- additionalProperties: false,
378
- properties: {
379
- probeSetRunId: { type: 'string', minLength: 1 },
380
- evaluationId: { type: 'string', minLength: 1 },
381
- visibility: { type: 'string', const: 'public' },
382
- },
383
- required: ['probeSetRunId', 'evaluationId', 'visibility'],
384
- },
385
- ],
386
- 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"}.',
387
- });
388
-
389
748
  const PRACTICE_STORE_ID_INPUT_SCHEMA = Object.freeze({
390
749
  type: 'string',
391
750
  minLength: 1,
@@ -455,8 +814,7 @@ const PRACTICE_RUN_EXTERNAL_AGENT_INPUT_SCHEMA = Object.freeze({
455
814
  name: { type: 'string', maxLength: 80 },
456
815
  source: { type: 'string', maxLength: 80 },
457
816
  version: { type: 'string', maxLength: 80 },
458
- commitSha: { type: 'string', maxLength: 80, description: 'Declared source revision. Prefer this field over the legacy commit alias.' },
459
- commit: { type: 'string', maxLength: 80, description: 'Legacy alias for commitSha.' },
817
+ commitSha: { type: 'string', maxLength: 80, description: 'Declared source revision.' },
460
818
  testCommand: { type: 'string', maxLength: 300 },
461
819
  ciStatus: { type: 'string', enum: ['unknown', 'passed', 'failed'] },
462
820
  examRole: { type: 'string', enum: ['candidate', 'negative-control'] },
@@ -493,6 +851,7 @@ const PRACTICE_RUN_UNTIL_INPUT_SCHEMA = Object.freeze({
493
851
  }, ['sku', 'threshold']),
494
852
  practiceRunUntilConditionInputSchema('order-recorded'),
495
853
  practiceRunUntilConditionInputSchema('return-recorded'),
854
+ practiceRunUntilConditionInputSchema('return-request-pending'),
496
855
  practiceRunUntilConditionInputSchema('incoming-inventory-landed', {
497
856
  sku: { type: 'string', minLength: 1, maxLength: 100 },
498
857
  }, ['sku']),
@@ -502,10 +861,27 @@ const PRACTICE_RUN_UNTIL_INPUT_SCHEMA = Object.freeze({
502
861
  required: ['schemaVersion', 'maxDays', 'condition'],
503
862
  });
504
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
+
505
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',
506
880
  'get_connection_details',
881
+ 'workspace_create',
507
882
  'practice_run_start',
508
883
  'practice_run_advance',
884
+ ...CONDITIONAL_TOOL_BRANCHES.keys(),
509
885
  ]);
510
886
 
511
887
  function schemaValidationError(schema, value, path = 'arguments') {
@@ -570,18 +946,32 @@ function schemaValidationError(schema, value, path = 'arguments') {
570
946
  return null;
571
947
  }
572
948
 
573
- 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
+ }
574
955
 
575
- function requiredShareArtifact(args) {
576
- const artifactType = requiredString(args, 'artifactType');
577
- if (!['receipt', 'gate-evidence-export'].includes(artifactType)) {
578
- throw new Error('artifactType must be receipt or gate-evidence-export');
579
- }
580
- const artifactRef = args?.artifactRef;
581
- if (!artifactRef || typeof artifactRef !== 'object' || Array.isArray(artifactRef)) {
582
- throw new Error(SHARE_CREATE_ARTIFACT_REF_NEXT_STEP);
583
- }
584
- 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)})`;
585
975
  }
586
976
 
587
977
  function queryPath(path, values) {
@@ -597,17 +987,8 @@ function requiredWorldId(args) {
597
987
  return validatedStoreId(requiredString(args, 'worldId'), 'worldId');
598
988
  }
599
989
 
600
- // MEG-256: `storeId` is the one canonical public practice-store identifier across the assistant
601
- // workflow (connection discovery → run start → receipt). `worldId` names the same identifier and
602
- // stays accepted as a bounded legacy alias so existing callers keep working.
603
990
  function requiredPracticeStoreId(args) {
604
- const provided = ['storeId', 'worldId'].filter((key) => args?.[key] !== undefined && args?.[key] !== null);
605
- if (provided.length === 0) throw new Error('storeId is required (worldId is accepted as a legacy alias for the same practice-store id)');
606
- const canonical = requiredString(args, provided[0]);
607
- if (provided.length === 2 && requiredString(args, provided[1]) !== canonical) {
608
- throw new Error('storeId and worldId name the same practice store; they were both provided with different values');
609
- }
610
- return validatedStoreId(canonical, provided[0]);
991
+ return validatedStoreId(requiredString(args, 'storeId'), 'storeId');
611
992
  }
612
993
 
613
994
  function requiredPracticeAttemptId(args, key = 'attemptId') {
@@ -730,49 +1111,17 @@ function receiptProjection(value) {
730
1111
  const practiceRun = attempt.practiceRun ?? value?.practiceRun ?? null;
731
1112
  const calls = Array.isArray(value?.events?.calls) ? value.events.calls : [];
732
1113
  const actions = Array.isArray(value?.events?.actions) ? value.events.actions : [];
733
- const capabilityMatrix = value?.receiptEvidence?.assertionCapabilities;
734
- const webhookCapability = capabilityMatrix?.schemaVersion === 'meguro.receipt-assertion-capabilities.v1'
735
- && capabilityMatrix?.families?.webhook
736
- && typeof capabilityMatrix.families.webhook === 'object'
737
- ? capabilityMatrix.families.webhook
738
- : null;
739
- const assertions = Array.isArray(value?.receiptEvidence?.assertions)
740
- ? value.receiptEvidence.assertions.slice(0, 50).map((assertion) => {
741
- if (assertion?.category !== 'webhook') {
742
- return { id: assertion.id, status: assertion.status, label: assertion.label };
743
- }
744
- if (webhookCapability?.availability === 'available') {
745
- return { id: assertion.id, status: assertion.status, label: assertion.label, detail: assertion.detail };
746
- }
747
- if (webhookCapability?.availability === 'unavailable'
748
- && webhookCapability.statusLabel === 'not available in this run mode'
749
- && typeof webhookCapability.detail === 'string'
750
- && webhookCapability.detail.trim()) {
751
- return {
752
- id: assertion.id,
753
- status: webhookCapability.statusLabel,
754
- label: assertion.label,
755
- detail: webhookCapability.detail,
756
- };
757
- }
758
- if (webhookCapability?.availability === 'unknown'
759
- && webhookCapability.statusLabel === 'availability unknown'
760
- && typeof webhookCapability.detail === 'string'
761
- && webhookCapability.detail.trim()) {
762
- return {
763
- id: assertion.id,
764
- status: webhookCapability.statusLabel,
765
- label: assertion.label,
766
- detail: webhookCapability.detail,
767
- };
768
- }
769
- return {
770
- id: assertion.id,
771
- status: 'availability unknown',
772
- label: assertion.label,
773
- detail: 'Webhook assertion availability is unknown because this receipt has no capability metadata.',
774
- };
775
- })
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
+ }))
776
1125
  : [];
777
1126
  const verdict = value?.report?.verdict && typeof value.report.verdict === 'object'
778
1127
  ? Object.fromEntries(['status', 'outcome', 'grade', 'score', 'headline', 'horizonDays']
@@ -816,6 +1165,12 @@ function receiptProjection(value) {
816
1165
  // disagree with the headline sitting beside it.
817
1166
  const footprint = m?.footprint ?? null;
818
1167
  const defaultVerdict = value?.defaultVerdict ?? value?.report?.defaultVerdict ?? value?.attempt?.defaultVerdict;
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;
819
1174
  return secretSafe({
820
1175
  schemaVersion: value?.schemaVersion,
821
1176
  storeIdentity: value.storeIdentity,
@@ -835,8 +1190,16 @@ function receiptProjection(value) {
835
1190
  receipt: {
836
1191
  summary: value?.receiptEvidence?.summary,
837
1192
  assertions,
1193
+ ...(value?.receiptEvidence?.expectedRefusals ? { expectedRefusals: value.receiptEvidence.expectedRefusals } : {}),
838
1194
  },
1195
+ changes: Array.isArray(value?.changes) ? value.changes : [],
839
1196
  ...(defaultVerdict && typeof defaultVerdict === 'object' ? { defaultVerdict } : {}),
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 } : {}),
840
1203
  ...(verdict ? { verdict } : {}),
841
1204
  ...(measurement ? { measurement } : {}),
842
1205
  ...(value?.publicReceipts?.run ? {
@@ -849,12 +1212,15 @@ function receiptProjection(value) {
849
1212
 
850
1213
  function runsListProjection(value, lifecycle) {
851
1214
  const runs = Array.isArray(value?.runs) ? value.runs : [];
852
- 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
+ }
853
1218
  return secretSafe({
854
1219
  runFamily: 'Shopify dev-store history runs created by run_start.',
855
1220
  excludes: 'Practice simulation attempts (pa-*) returned by practice_run_start are a separate family and are not included.',
856
1221
  lifecycleFilter: lifecycle ?? 'all',
857
- runs: visible.map((run) => {
1222
+ total: value.total,
1223
+ runs: runs.map((run) => {
858
1224
  const {
859
1225
  startedAt,
860
1226
  updatedAt,
@@ -873,7 +1239,7 @@ function runsListProjection(value, lifecycle) {
873
1239
  },
874
1240
  };
875
1241
  }),
876
- ...(visible.length === 0 ? {
1242
+ ...(runs.length === 0 ? {
877
1243
  emptyState: {
878
1244
  message: `No ${lifecycle ?? 'matching'} Shopify dev-store history runs were found. This says nothing about practice simulation attempts.`,
879
1245
  practiceAttemptRecovery: 'Call practice_runs_list to discover practice simulation attempts, then pass one exact attemptId to practice_run_report or practice_run_impact.',
@@ -940,17 +1306,20 @@ function practiceRunReceiptStoreMismatch(receiptId, receiptStoreId, requestedSto
940
1306
  };
941
1307
  }
942
1308
 
943
- function practiceRunsProjection(value, storeId, storeSelection) {
1309
+ function practiceRunsProjection(value, storeId, storeSelection, workspaceId) {
944
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
+ }
945
1314
  return secretSafe({
946
1315
  schemaVersion: 'meguro.practice-run-list.v1',
947
1316
  storeId,
948
1317
  storeSelection,
1318
+ total: value.total,
949
1319
  attempts: attempts
950
- .filter((attempt) => attempt?.storeId === storeId || attempt?.worldId === storeId)
951
1320
  .map((attempt) => ({
952
1321
  attemptId: attempt.attemptId,
953
- state: attempt.status,
1322
+ state: typeof attempt.practiceRun?.state === 'string' ? attempt.practiceRun.state : null,
954
1323
  clock: {
955
1324
  mode: attempt.clock?.mode ?? null,
956
1325
  baselineDay: attempt.baselineDay ?? null,
@@ -959,7 +1328,10 @@ function practiceRunsProjection(value, storeId, storeSelection) {
959
1328
  elapsedSimulationDays: attempt.elapsedSimulationDays ?? null,
960
1329
  remainingSimulationDays: attempt.remainingSimulationDays ?? null,
961
1330
  },
962
- receiptRef: { attemptId: attempt.attemptId },
1331
+ receiptRef: {
1332
+ attemptId: attempt.attemptId,
1333
+ ...(workspaceId ? { workspaceId } : {}),
1334
+ },
963
1335
  })),
964
1336
  });
965
1337
  }
@@ -975,8 +1347,10 @@ function practiceAttemptComparisonGuidanceFor(toolName, a, b) {
975
1347
  type: 'text',
976
1348
  text: JSON.stringify({
977
1349
  code: 'practice-attempt-comparator-unavailable',
978
- 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.`,
979
1351
  attemptIds,
1352
+ nextAction: calls.join('; '),
1353
+ stopCondition: `Use the listed practice receipt reads instead. Do not retry ${toolName} with pa-* practice attempt ids.`,
980
1354
  workingMethod: {
981
1355
  calls,
982
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.',
@@ -1040,14 +1414,32 @@ function practiceRunsStoreNotFoundGuidance(storeId) {
1040
1414
  };
1041
1415
  }
1042
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
+
1043
1435
  function legacyGateArtifactNotFoundGuidance(runId) {
1044
1436
  return {
1045
1437
  content: [{
1046
1438
  type: 'text',
1047
1439
  text: JSON.stringify({
1048
1440
  code: 'legacy-gate-artifact-not-found',
1049
- message: `No retained legacy Gate artifact exists for ${runId}. It is not a history-run id or pa-* practice simulation attempt id.`,
1050
- 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.',
1051
1443
  stopCondition: `Do not retry ${JSON.stringify(runId)}. The legacy artifact cannot be created from a customer surface; stop after gate_verdict({}).`,
1052
1444
  }, null, 2),
1053
1445
  }],
@@ -1055,7 +1447,7 @@ function legacyGateArtifactNotFoundGuidance(runId) {
1055
1447
  };
1056
1448
  }
1057
1449
 
1058
- function usageProjection(value) {
1450
+ function usageProjection(value, evaluatedRunStartEligibility) {
1059
1451
  const totalRuns = Number(value?.totalRuns ?? 0);
1060
1452
  const nonBillableRuns = Number(value?.nonBillableRuns ?? 0);
1061
1453
  const activityBillableRuns = Math.max(0, totalRuns - nonBillableRuns);
@@ -1066,6 +1458,17 @@ function usageProjection(value) {
1066
1458
  const activeStoreLimit = Number(meter.activeStoreLimit ?? 0);
1067
1459
  const activeWorkspaces = Number(meter.activeWorkspaces ?? 0);
1068
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');
1069
1472
  const activityCoverageComplete = value?.coverage?.complete === true;
1070
1473
  const activityMatchesMeter = activityCoverageComplete && activityBillableRuns === consumedSimulationRuns;
1071
1474
  const reconciliationStatus = !activityCoverageComplete
@@ -1107,17 +1510,23 @@ function usageProjection(value) {
1107
1510
  activeWorkspaces,
1108
1511
  activeWorkspaceLimit,
1109
1512
  runStartAllowed: meter.runStartAllowed,
1513
+ runStartAllowedMeaning: allowance?.meaning ?? null,
1514
+ runStartAllowance: allowance ?? null,
1110
1515
  enforcement: meter.enforcement,
1111
1516
  },
1517
+ runStartEligibility: evaluatedRunStartEligibility ?? value?.runStartEligibility ?? null,
1112
1518
  coverage: value?.coverage ?? null,
1519
+ trial: value?.trial ?? null,
1113
1520
  });
1114
1521
  }
1115
1522
 
1116
1523
  function twinDiffProjection(value) {
1117
1524
  if (!value || typeof value !== 'object') return secretSafe(value);
1118
- const { generatedAt, markdown: _markdown, ...impactReceipt } = value;
1525
+ const { generatedAt, markdown: _markdown, pair, sources, ...impactReceipt } = value;
1119
1526
  return secretSafe({
1120
1527
  schemaVersion: 'meguro.mcp-twin-diff.v1',
1528
+ pair,
1529
+ sources,
1121
1530
  runClock: {
1122
1531
  generatedAtWallClock: generatedAt ?? null,
1123
1532
  note: 'This is when the impact receipt was generated; it is not Store-time.',
@@ -1129,6 +1538,34 @@ function twinDiffProjection(value) {
1129
1538
  });
1130
1539
  }
1131
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
+
1132
1569
  const SHOPIFY_EXAM_ID = /^pex-[0-9a-f]{24}$/u;
1133
1570
  const SHOPIFY_DEV_DOMAIN = /^[a-z0-9][a-z0-9-]*\.myshopify\.com$/u;
1134
1571
  const SHOPIFY_EXAM_ACTIVE_STATUSES = new Set(['confirmed', 'materializing', 'replaying', 'comparing']);
@@ -1143,6 +1580,7 @@ const SHOPIFY_EXAM_ELIGIBILITY_GUIDANCE = Object.freeze({
1143
1580
  'temporal-clock-context-mismatch': 'Use the target practice store clock and rerun the bounded Exam source.',
1144
1581
  'temporal-clock-revision-mismatch': 'Capture one bounded burst at one Store-time coordinate; after advancing time, start a new practice run.',
1145
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.',
1146
1584
  });
1147
1585
 
1148
1586
  function requiredExamId(args) {
@@ -1163,10 +1601,15 @@ function requiredShopifyDevDomain(args) {
1163
1601
 
1164
1602
  function examNextAction(exam, receipt) {
1165
1603
  const status = String(exam?.status ?? '');
1166
- if (SHOPIFY_EXAM_TERMINAL_STATUSES.has(status)) {
1167
- return status === 'completed'
1168
- ? { tool: 'exam_report', args: { examId: exam.examId }, reason: 'exam-complete' }
1169
- : { 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
+ };
1170
1613
  }
1171
1614
  if (status === 'blocked') {
1172
1615
  if (receipt?.privateAvailable === true && receipt?.publicAvailable === true) {
@@ -1175,7 +1618,11 @@ function examNextAction(exam, receipt) {
1175
1618
  return { tool: 'exam_preflight', args: { attemptId: exam.attemptId, shopDomain: exam.shopDomain }, reason: 'preview-blocked' };
1176
1619
  }
1177
1620
  if (status === 'cleaning') {
1178
- 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
+ };
1179
1626
  }
1180
1627
  return {
1181
1628
  tool: 'exam_start',
@@ -1247,6 +1694,7 @@ export function createTools(config) {
1247
1694
  const headers = { accept: 'application/json' };
1248
1695
  const needsControlCredential = Boolean(options.auth || method !== 'GET');
1249
1696
  if (needsControlCredential) Object.assign(headers, controlAuthHeaders());
1697
+ if (options.workspaceId) headers['x-meguro-workspace'] = options.workspaceId;
1250
1698
  if (method !== 'GET') {
1251
1699
  headers['content-type'] = 'application/json';
1252
1700
  }
@@ -1264,7 +1712,11 @@ export function createTools(config) {
1264
1712
  const authenticationError = authenticationErrorResult(response.status, json);
1265
1713
  if (authenticationError) throw new ToolResultError(authenticationError);
1266
1714
  const message = json.errors?.[0]?.message ?? json.error ?? text.slice(0, 300);
1267
- 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;
1268
1720
  }
1269
1721
  return json;
1270
1722
  }
@@ -1275,7 +1727,10 @@ export function createTools(config) {
1275
1727
  const headers = { accept: 'application/json', ...controlAuthHeaders() };
1276
1728
  if (options.workspaceId) headers['x-meguro-workspace'] = options.workspaceId;
1277
1729
  if (method !== 'GET') headers['content-type'] = 'application/json';
1278
- 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}`, {
1279
1734
  method,
1280
1735
  headers,
1281
1736
  ...(body ? { body: JSON.stringify(body) } : {}),
@@ -1326,12 +1781,47 @@ export function createTools(config) {
1326
1781
  return { schemaVersion: 'meguro.support-code.v1', buildSha, requestIds: [safeRequestId], occurredAt, route: path, line };
1327
1782
  }
1328
1783
 
1329
- 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) {
1330
1816
  const body = value && typeof value === 'object' ? value : {};
1817
+ const declaredBoundary = ['pre-commit', 'committed', 'unknown'].includes(body.operationCommitBoundary)
1818
+ ? body.operationCommitBoundary
1819
+ : null;
1331
1820
  const sourceErrors = Array.isArray(body.errors) ? body.errors.slice(0, 5) : [];
1332
1821
  const allowedShopDomains = Array.isArray(body.allowed)
1333
1822
  ? [...new Set(body.allowed.map((domain) => String(domain).trim().toLowerCase()).filter((domain) => SHOPIFY_DEV_DOMAIN.test(domain)))]
1334
1823
  : [];
1824
+ const catalogConnection = catalogConnectionTeaching(toolName, path, allowedShopDomains, Array.isArray(body.allowed));
1335
1825
  const workspaceLimit = body.code === 'active_workspace_limit_reached';
1336
1826
  const rawError = typeof body.error === 'string' ? body.error : '';
1337
1827
  const routeCode = typeof body.code === 'string'
@@ -1343,31 +1833,80 @@ export function createTools(config) {
1343
1833
  const claimCodeError = toolName === 'store_claim_by_code'
1344
1834
  && ['invalid-claim-code', 'claim-code-unavailable', 'claim-code-busy'].includes(routeCode);
1345
1835
  const sourceErrorText = sourceErrors.map((error) => String(error?.message ?? '')).join(' ');
1346
- 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
1347
1874
  ? {
1348
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.',
1349
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.`,
1350
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
+ }
1351
1885
  : toolName === 'store_create' && status === 400 && /template/iu.test(`${rawError} ${routeMessage ?? ''} ${sourceErrorText}`)
1352
1886
  ? {
1353
1887
  meaning: 'The requested templateKey is not in the authoritative practice-store template catalog.',
1354
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.',
1355
1889
  }
1356
- : 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;
1357
1902
  const defaultNextStep = resourceTeaching
1358
1903
  ? resourceTeaching.nextStep
1359
1904
  : claimCodeError
1360
1905
  ? routeCode === 'claim-code-busy'
1361
1906
  ? 'Wait for the current connection update to finish, then reopen Meguro in Shopify Admin before retrying once.'
1362
1907
  : 'Reopen Meguro in Shopify Admin and use the current one-time claim code shown for the intended development store.'
1363
- : toolName === 'share_create'
1364
- ? SHARE_CREATE_ARTIFACT_REF_NEXT_STEP
1365
- : toolName.startsWith('share_') || toolName === 'shares_list'
1366
- ? 'Call shares_list or share_status, inspect the current private/outward lifecycle state, and retry only the permitted transition.'
1367
- : toolName === 'catalog_slice_read'
1368
- ? allowedShopDomains.length
1369
- ? `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.`
1370
- : '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.'
1371
1910
  : toolName.startsWith('catalog_')
1372
1911
  ? 'Read the current catalog slice or saved-slice state, correct the exact store, slice, or variant ids, and retry only the failed action.'
1373
1912
  : toolName === 'store_claim'
@@ -1375,47 +1914,51 @@ export function createTools(config) {
1375
1914
  : 'Call stores_list, inspect the current fleet, and retry with a current store id.';
1376
1915
  const errors = sourceErrors.length ? sourceErrors.map((error) => ({
1377
1916
  message: scrubString(error?.message ?? 'Meguro fleet request failed.'),
1378
- meaning: scrubString(resourceTeaching?.meaning ?? error?.meaning ?? 'The requested fleet operation was not applied.'),
1379
- 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),
1380
1921
  })) : [{
1381
1922
  message: scrubString(routeMessage ?? (typeof value === 'string' ? value : 'Meguro fleet request failed.')),
1382
1923
  meaning: resourceTeaching
1383
1924
  ? resourceTeaching.meaning
1925
+ : catalogConnection
1926
+ ? catalogConnection.meaning
1384
1927
  : workspaceLimit
1385
1928
  ? 'The account is at its active-workspace limit, so the requested workspace operation was not applied.'
1386
1929
  : claimCodeError
1387
1930
  ? 'The one-time Shopify development-store claim was not applied.'
1388
- : 'The requested fleet operation was not applied.',
1931
+ : operationMeaning,
1389
1932
  nextStep: resourceTeaching
1390
1933
  ? resourceTeaching.nextStep
1934
+ : catalogConnection
1935
+ ? catalogConnection.nextStep
1391
1936
  : workspaceLimit
1392
- ? 'Archive an active workspace or open Plans & Billing at #upgrade.'
1937
+ ? String(body.sameTierAction?.label ?? routeMessage ?? defaultNextStep)
1393
1938
  : defaultNextStep,
1394
1939
  }];
1395
1940
  const stable = secretSafe({
1396
1941
  tool: toolName,
1397
1942
  httpStatus: status,
1398
1943
  ...(routeCode ? { code: routeCode } : {}),
1399
- ...(typeof body.blockedCapability === 'string' ? { blockedCapability: body.blockedCapability } : {}),
1400
- ...(typeof body.tier === 'string' ? { tier: body.tier } : {}),
1944
+ ...enforcementProjection(body),
1401
1945
  ...(Number.isFinite(body.activeStores) ? { activeStores: body.activeStores } : {}),
1402
1946
  ...(Number.isFinite(body.activeStoreLimit) ? { activeStoreLimit: body.activeStoreLimit } : {}),
1403
1947
  ...(Number.isFinite(body.activeWorkspaces) ? { activeWorkspaces: body.activeWorkspaces } : {}),
1404
1948
  ...(Number.isFinite(body.activeWorkspaceLimit) ? { activeWorkspaceLimit: body.activeWorkspaceLimit } : {}),
1405
- ...(typeof body.upgradePath === 'string' ? { upgradePath: body.upgradePath } : {}),
1406
1949
  errors,
1407
- ...(toolName === 'catalog_slice_read' ? { allowedShopDomains } : {}),
1950
+ ...(catalogConnection ? { allowedShopDomains } : {}),
1408
1951
  ...(Array.isArray(body.fleet) ? { fleet: body.fleet } : {}),
1409
- supportCode: await fleetSupportCode(path, requestId),
1952
+ supportCode: await fleetSupportCode(supportRouteForFleetFailure(toolName, path), requestId),
1410
1953
  });
1411
- 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;
1412
1956
  }
1413
1957
 
1414
1958
  async function historyRunTeachingErrorResult(toolName, path, response, requestedId) {
1415
1959
  const body = response.json && typeof response.json === 'object' ? response.json : {};
1416
1960
  const rawMessage = String(body.errors?.[0]?.message ?? body.error ?? body.message ?? 'Meguro history-run request failed.');
1417
1961
  if (toolName === 'run_start' && response.status === 400 && /not allowed|not connected/iu.test(rawMessage)) {
1418
- const requestedDomain = String(body.requestedShopDomain ?? requestedId ?? '').trim().toLowerCase();
1419
1962
  const allowedShopDomains = Array.isArray(body.allowed)
1420
1963
  ? [...new Set(body.allowed.map((domain) => String(domain).trim().toLowerCase()).filter((domain) => SHOPIFY_DEV_DOMAIN.test(domain)))]
1421
1964
  : [];
@@ -1428,10 +1971,10 @@ export function createTools(config) {
1428
1971
  error: { message: rawMessage },
1429
1972
  allowedShopDomains,
1430
1973
  teaching: {
1431
- 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.',
1432
1975
  nextStep,
1433
1976
  stopCondition: allowedShopDomains.length
1434
- ? `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.'
1435
1978
  : 'Stop until Console exposes a connected Shopify development-store domain.',
1436
1979
  },
1437
1980
  supportCode: await fleetSupportCode(path, response.requestId),
@@ -1459,14 +2002,22 @@ export function createTools(config) {
1459
2002
  const response = await apiRaw(method, path, body, options);
1460
2003
  if (!response.ok) {
1461
2004
  const authenticationError = authenticationErrorResult(response.status, response.json);
1462
- 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
+ ) };
1463
2013
  }
1464
2014
  return { value: response.json };
1465
2015
  }
1466
2016
 
1467
- async function practiceApi(method, path, body) {
2017
+ async function practiceApi(method, path, body, options = {}) {
1468
2018
  if (!practiceApiToken) throw new Error('MEGURO_API_TOKEN is required for practice-run tools');
1469
2019
  const headers = { accept: 'application/json', authorization: `Bearer ${practiceApiToken}` };
2020
+ if (options.workspaceId) headers['x-meguro-workspace'] = options.workspaceId;
1470
2021
  if (method !== 'GET') headers['content-type'] = 'application/json';
1471
2022
  let response;
1472
2023
  try {
@@ -1486,23 +2037,20 @@ export function createTools(config) {
1486
2037
  return { ok: response.ok, status: response.status, json, retryAfterSeconds };
1487
2038
  }
1488
2039
 
1489
- async function examEligibility(attemptId) {
1490
- const response = await practiceApi('GET', `/practice/playbacks/${encodeURIComponent(attemptId)}/report`);
1491
- const attempt = response.json?.attempt ?? response.json?.report?.attempt;
1492
- const eligibility = response.ok
1493
- ? attempt?.practiceExamEligibility
1494
- : null;
1495
- 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) {
1496
2046
  return {
1497
2047
  state: 'undetermined',
1498
- codes: [],
2048
+ codes,
1499
2049
  retryable: true,
1500
2050
  nextStep: 'Eligibility evidence is undetermined, not negative. Retry preflight; if the evidence read remains unavailable, use the support code.',
1501
2051
  };
1502
2052
  }
1503
- const codes = Array.isArray(eligibility.codes) ? eligibility.codes.map(String) : [];
1504
2053
  const steps = [...new Set(codes.map((code) => SHOPIFY_EXAM_ELIGIBILITY_GUIDANCE[code]).filter(Boolean))];
1505
- const storeId = String(attempt?.storeId ?? attempt?.worldId ?? '').trim();
1506
2054
  const mustRecapture = codes.some((code) => [
1507
2055
  'tape-missing',
1508
2056
  'tape-total-over-limit',
@@ -1514,11 +2062,9 @@ export function createTools(config) {
1514
2062
  'temporal-clock-revision-mismatch',
1515
2063
  'interleaved-simulation-state-change',
1516
2064
  ].includes(code));
1517
- 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.';
1518
2066
  return {
1519
- state: eligibility.eligible === true && codes.length === 0 ? 'eligible' : 'blocked',
1520
- schemaVersion: eligibility.schemaVersion,
1521
- replayDisposition: eligibility.replayDisposition,
2067
+ state: 'blocked',
1522
2068
  codes,
1523
2069
  retryable: false,
1524
2070
  nextStep: mustRecapture
@@ -1533,20 +2079,23 @@ export function createTools(config) {
1533
2079
  const source = response.json?.examError && typeof response.json.examError === 'object'
1534
2080
  ? response.json.examError
1535
2081
  : { code: 'exam-request-failed', message: 'The Shopify Exam request failed safely.', retryable: response.status >= 500 };
1536
- const eligibility = source.code === 'exam-ineligible' && attemptId
1537
- ? await examEligibility(attemptId)
1538
- : null;
1539
- const retryable = eligibility?.state === 'undetermined'
1540
- ? true
1541
- : source.retryable === true;
2082
+ const eligibility = examEligibility(source);
2083
+ const retryable = source.retryable === true;
1542
2084
  const startPairNotFound = toolName === 'exam_start' && source.code === 'not_found';
1543
- 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
1544
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.'
1545
2092
  : eligibility?.nextStep
1546
2093
  ?? (retryable
1547
- ? '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.'
1548
2097
  : 'Correct the stated Exam requirement, then retry only the failed bounded step.');
1549
- return {
2098
+ const result = {
1550
2099
  content: [{
1551
2100
  type: 'text',
1552
2101
  text: JSON.stringify(secretSafe({
@@ -1560,19 +2109,28 @@ export function createTools(config) {
1560
2109
  codes: eligibility.codes,
1561
2110
  } } : {}),
1562
2111
  teaching: {
1563
- meaning: startPairNotFound
2112
+ meaning: startPairNotFound || preflightPairNotFound
1564
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.'
1565
2116
  : source.code === 'exam-ineligible'
1566
2117
  ? 'Shopify Exam accepts only a completed, graded, immutable captured attempt whose replay evidence is current and supported.'
1567
2118
  : 'The requested bounded Shopify Exam step was not assumed complete.',
1568
2119
  nextStep,
1569
- ...(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
+ : {}),
1570
2125
  },
1571
2126
  supportCode: await fleetSupportCode(path, response.requestId),
1572
2127
  }), null, 2),
1573
2128
  }],
1574
2129
  isError: true,
1575
2130
  };
2131
+ return startPairNotFound || preflightPairNotFound
2132
+ ? withOperationCommitBoundary(result, 'pre-commit')
2133
+ : result;
1576
2134
  }
1577
2135
 
1578
2136
  async function examRequest(toolName, method, path, body, attemptId) {
@@ -1655,8 +2213,13 @@ export function createTools(config) {
1655
2213
  description: DOCUMENTATION_TOOL_CONTRACT.topicDescription,
1656
2214
  },
1657
2215
  version: {
1658
- type: 'integer',
1659
- 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
+ ],
1660
2223
  description: DOCUMENTATION_TOOL_CONTRACT.versionDescription,
1661
2224
  },
1662
2225
  },
@@ -1665,7 +2228,7 @@ export function createTools(config) {
1665
2228
  },
1666
2229
  {
1667
2230
  name: 'templates_list',
1668
- 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; the matching recommended boolean marks those templates. recommendedRunDays is each template\'s recommended simulation length. The response is derived only from the bounded storeTemplates 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.',
1669
2232
  inputSchema: {
1670
2233
  type: 'object',
1671
2234
  additionalProperties: false,
@@ -1675,6 +2238,19 @@ export function createTools(config) {
1675
2238
  required: [],
1676
2239
  },
1677
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
+ },
1678
2254
  {
1679
2255
  name: 'stores_list',
1680
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.',
@@ -1688,7 +2264,7 @@ export function createTools(config) {
1688
2264
  },
1689
2265
  {
1690
2266
  name: 'store_create',
1691
- 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.',
1692
2268
  inputSchema: {
1693
2269
  type: 'object', additionalProperties: false,
1694
2270
  properties: {
@@ -1762,70 +2338,6 @@ export function createTools(config) {
1762
2338
  required: ['workspaceId'],
1763
2339
  },
1764
2340
  },
1765
- {
1766
- name: 'share_create',
1767
- 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.',
1768
- inputSchema: {
1769
- type: 'object', additionalProperties: false,
1770
- properties: {
1771
- workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace selector. Omit for Default.' },
1772
- shareId: { type: 'string', pattern: '^[0-9a-f]{32}$', description: 'Existing draft/changes-requested share to update. Omit to create.' },
1773
- title: { type: 'string', minLength: 1, maxLength: 120 },
1774
- artifactType: { type: 'string', enum: ['receipt', 'gate-evidence-export'], description: 'Required only when creating.' },
1775
- artifactRef: SHARE_ARTIFACT_REF_INPUT_SCHEMA,
1776
- audienceUserIds: { type: 'array', maxItems: 40, items: { type: 'string', minLength: 1 }, description: 'Active client-viewer user ids assigned to this exact workspace.' },
1777
- },
1778
- required: [],
1779
- },
1780
- },
1781
- {
1782
- name: 'shares_list',
1783
- 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.',
1784
- inputSchema: {
1785
- type: 'object', additionalProperties: false,
1786
- properties: {
1787
- workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace selector. Omit for Default.' },
1788
- },
1789
- required: [],
1790
- },
1791
- },
1792
- {
1793
- name: 'share_status',
1794
- 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.',
1795
- inputSchema: {
1796
- type: 'object', additionalProperties: false,
1797
- properties: {
1798
- workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace selector. Omit for Default.' },
1799
- shareId: { type: 'string', pattern: '^[0-9a-f]{32}$', description: 'Exact share id from shares_list.' },
1800
- },
1801
- required: ['shareId'],
1802
- },
1803
- },
1804
- {
1805
- name: 'share_publish',
1806
- 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.',
1807
- inputSchema: {
1808
- type: 'object', additionalProperties: false,
1809
- properties: {
1810
- workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace selector. Omit for Default.' },
1811
- shareId: { type: 'string', pattern: '^[0-9a-f]{32}$', description: 'Exact share id from share_status.' },
1812
- action: { type: 'string', enum: ['publish', 're-share'], description: 'Exact server lifecycle transition. Defaults to publish.' },
1813
- },
1814
- required: ['shareId'],
1815
- },
1816
- },
1817
- {
1818
- name: 'share_revoke',
1819
- 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.',
1820
- inputSchema: {
1821
- type: 'object', additionalProperties: false,
1822
- properties: {
1823
- workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace selector. Omit for Default.' },
1824
- shareId: { type: 'string', pattern: '^[0-9a-f]{32}$', description: 'Exact share id from shares_list.' },
1825
- },
1826
- required: ['shareId'],
1827
- },
1828
- },
1829
2341
  {
1830
2342
  name: 'catalog_slice_read',
1831
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.',
@@ -1843,32 +2355,12 @@ export function createTools(config) {
1843
2355
  {
1844
2356
  name: 'catalog_slice_snapshot',
1845
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.',
1846
- inputSchema: {
1847
- type: 'object', additionalProperties: false,
1848
- properties: {
1849
- workspaceId: { type: 'string', minLength: 1, description: 'Optional account-owned workspace selector. Omit for Default.' },
1850
- shopDomain: { type: 'string', pattern: '^[a-z0-9][a-z0-9-]*\\.myshopify\\.com$', description: 'Live snapshot source. Mutually exclusive with sliceId.' },
1851
- 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.' },
1852
- sliceId: { type: 'string', pattern: '^csl_saved_[A-Za-z0-9_-]{8,128}$', description: 'Saved snapshot source. Mutually exclusive with shopDomain/variantIds.' },
1853
- },
1854
- required: [],
1855
- },
2358
+ inputSchema: CATALOG_SNAPSHOT_INPUT_SCHEMA,
1856
2359
  },
1857
2360
  {
1858
2361
  name: 'catalog_slices_saved',
1859
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.',
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
- action: { type: 'string', enum: ['list', 'get', 'save', 'delete', 'refresh', 'changes'] },
1865
- shopDomain: { type: 'string', pattern: '^[a-z0-9][a-z0-9-]*\\.myshopify\\.com$', description: 'Required for save.' },
1866
- label: { type: 'string', minLength: 1, maxLength: 80, description: 'Required for save.' },
1867
- variantIds: { type: 'array', minItems: 1, maxItems: 25, items: { type: 'string', minLength: 1 }, description: 'Required for save.' },
1868
- sliceId: { type: 'string', pattern: '^csl_saved_[A-Za-z0-9_-]{8,128}$', description: 'Required for get/delete/refresh/changes.' },
1869
- },
1870
- required: ['action'],
1871
- },
2363
+ inputSchema: CATALOG_SAVED_SLICES_INPUT_SCHEMA,
1872
2364
  },
1873
2365
  {
1874
2366
  name: 'store_claim_by_code',
@@ -1927,7 +2419,7 @@ export function createTools(config) {
1927
2419
  },
1928
2420
  {
1929
2421
  name: 'run_status',
1930
- 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.',
1931
2423
  inputSchema: {
1932
2424
  type: 'object',
1933
2425
  additionalProperties: false,
@@ -1950,7 +2442,7 @@ export function createTools(config) {
1950
2442
  },
1951
2443
  {
1952
2444
  name: 'run_report',
1953
- 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.',
1954
2446
  inputSchema: {
1955
2447
  type: 'object',
1956
2448
  additionalProperties: false,
@@ -1974,7 +2466,7 @@ export function createTools(config) {
1974
2466
  },
1975
2467
  {
1976
2468
  name: 'runs_diff',
1977
- 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.',
1978
2470
  inputSchema: {
1979
2471
  type: 'object',
1980
2472
  additionalProperties: false,
@@ -1987,26 +2479,26 @@ export function createTools(config) {
1987
2479
  },
1988
2480
  {
1989
2481
  name: 'gate_configure',
1990
- 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.",
1991
2483
  inputSchema: {
1992
2484
  type: 'object',
1993
2485
  additionalProperties: false,
1994
2486
  properties: {
1995
- 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.' },
1996
2488
  storeId: { type: 'string', minLength: 1, description: 'Optional practice-store identity carried by the selected Console receipt.' },
1997
- 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.' },
1998
2490
  },
1999
2491
  required: ['receiptId'],
2000
2492
  },
2001
2493
  },
2002
2494
  {
2003
2495
  name: 'gate_evaluate',
2004
- 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.",
2005
2497
  inputSchema: {
2006
2498
  type: 'object',
2007
2499
  additionalProperties: false,
2008
2500
  properties: {
2009
- 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.' },
2010
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.' },
2011
2503
  },
2012
2504
  required: [],
@@ -2014,20 +2506,20 @@ export function createTools(config) {
2014
2506
  },
2015
2507
  {
2016
2508
  name: 'gate_verdict',
2017
- 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.",
2018
2510
  inputSchema: {
2019
2511
  type: 'object',
2020
2512
  additionalProperties: false,
2021
2513
  properties: {
2022
- runId: { type: 'string', minLength: 1, description: 'Exact id of a previously issued legacy Gate artifact. Omit to read the latest Receipt Gate verdict.' },
2023
- 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.' },
2024
2516
  },
2025
2517
  required: [],
2026
2518
  },
2027
2519
  },
2028
2520
  {
2029
2521
  name: 'runs_list',
2030
- 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.',
2031
2523
  inputSchema: {
2032
2524
  type: 'object',
2033
2525
  additionalProperties: false,
@@ -2039,12 +2531,22 @@ export function createTools(config) {
2039
2531
  },
2040
2532
  {
2041
2533
  name: 'usage_read',
2042
- 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.',
2043
- 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
+ },
2044
2546
  },
2045
2547
  {
2046
2548
  name: 'twin_diff',
2047
- 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.',
2048
2550
  inputSchema: {
2049
2551
  type: 'object',
2050
2552
  additionalProperties: false,
@@ -2069,7 +2571,7 @@ export function createTools(config) {
2069
2571
  },
2070
2572
  {
2071
2573
  name: 'exam_start',
2072
- 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.',
2073
2575
  inputSchema: {
2074
2576
  type: 'object', additionalProperties: false,
2075
2577
  properties: {
@@ -2081,7 +2583,7 @@ export function createTools(config) {
2081
2583
  },
2082
2584
  {
2083
2585
  name: 'exam_status',
2084
- 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.',
2085
2587
  inputSchema: {
2086
2588
  type: 'object', additionalProperties: false,
2087
2589
  properties: { examId: { type: 'string', pattern: '^pex-[0-9a-f]{24}$', description: 'Exam id returned by exam_start.' } },
@@ -2090,7 +2592,7 @@ export function createTools(config) {
2090
2592
  },
2091
2593
  {
2092
2594
  name: 'exam_report',
2093
- 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.',
2094
2596
  inputSchema: {
2095
2597
  type: 'object', additionalProperties: false,
2096
2598
  properties: { examId: { type: 'string', pattern: '^pex-[0-9a-f]{24}$', description: 'Completed Exam id returned by exam_start.' } },
@@ -2099,18 +2601,18 @@ export function createTools(config) {
2099
2601
  },
2100
2602
  {
2101
2603
  name: 'practice_run_start',
2102
- 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. A fresh start replays the practice store\'s original scenario baseline as segment 1 by default; chaining is explicit through continueFromAttemptId. When clock.simulationDays is omitted, Meguro automatically plans the tier-legal segment from the remaining scenario arc and returns a machine-readable segmentPlan with the exact Store-day range and continuation narration. 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.',
2103
2605
  inputSchema: {
2104
2606
  type: 'object',
2105
2607
  additionalProperties: false,
2106
2608
  properties: {
2609
+ workspaceId: OPTIONAL_WORKSPACE_SELECTOR_INPUT_SCHEMA,
2107
2610
  storeId: { ...PRACTICE_STORE_ID_INPUT_SCHEMA, description: 'Canonical Meguro practice-store id (tenant-owned), as returned by get_connection_details.' },
2108
- worldId: { ...PRACTICE_STORE_ID_INPUT_SCHEMA, description: 'Legacy alias for storeId — the same practice-store id under its old name. Prefer storeId.' },
2109
- 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. Omit clock.simulationDays to let Meguro plan the next legal segment automatically.' },
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.' },
2110
2612
  clock: {
2111
2613
  type: 'object',
2112
2614
  additionalProperties: false,
2113
- description: 'Optional hosted practice-run clock. Omit simulationDays for automatic tier-legal segment planning; set it only when explicitly requesting a shorter bounded segment.',
2615
+ description: 'Optional hosted practice-run clock. Omission derives the full remaining scenario arc and may refuse on Free or Solo; use templates_list starterPlan.practiceRunStart.clock for the current tier\'s executable first segment, or set a shorter bounded segment explicitly.',
2114
2616
  properties: {
2115
2617
  mode: { type: 'string', enum: ['manual', 'harness', 'scheduled'] },
2116
2618
  simulationDays: { type: 'integer', minimum: 0, description: 'Explicit segment length. Requests above the tier segment or remaining canvas cap are refused with an executable maximum.' },
@@ -2132,10 +2634,37 @@ export function createTools(config) {
2132
2634
  required: ['name', 'version', 'ref'],
2133
2635
  },
2134
2636
  observationPlan: { ...PRACTICE_RUN_OBSERVATION_PLAN_INPUT_SCHEMA, description: 'Optional trusted read-only checkpoint observation plan. Use one closed observations envelope; each row is either an inline read request or one saved read probe.' },
2637
+ assertionPlan: {
2638
+ type: 'object',
2639
+ additionalProperties: false,
2640
+ description: 'Optional immutable run-start expected-refusal declarations. Generic expected flags, wildcards, HTTP classes, and Meguro compatibility failures are refused.',
2641
+ properties: {
2642
+ schemaVersion: { type: 'string', const: 'meguro.run-assertion-plan.v1' },
2643
+ expectedRefusals: {
2644
+ type: 'array', minItems: 1, maxItems: 10,
2645
+ items: {
2646
+ type: 'object', additionalProperties: false,
2647
+ properties: {
2648
+ assertionId: { type: 'string', pattern: '^[a-z][a-z0-9_.-]{2,95}$' },
2649
+ operationFamily: { type: 'string', const: 'shopify-admin-graphql-mutation' },
2650
+ operationName: { type: 'string', pattern: '^[A-Za-z_][A-Za-z0-9_]{0,95}$' },
2651
+ targetGid: { type: 'string', pattern: '^gid://shopify/[A-Za-z][A-Za-z0-9]*/[A-Za-z0-9._:-]{1,160}$' },
2652
+ requestFingerprint: { type: 'string', pattern: '^[0-9a-f]{64}$' },
2653
+ expectedResult: { type: 'string', const: 'rejected' },
2654
+ rejectionReasonCodes: { type: 'array', minItems: 1, maxItems: 8, uniqueItems: true, items: { type: 'string', enum: ['shopify_idempotency_required', 'agent_request_verified_invalid', 'modeled_business_rule_rejected', 'external_platform_rejected'] } },
2655
+ responsibleLayer: { type: 'string', enum: ['agent_request', 'modeled_business_rule', 'external_platform'] },
2656
+ cardinality: { type: 'object', additionalProperties: false, properties: { minAttempts: { type: 'integer', minimum: 1, maximum: 10 }, maxAttempts: { type: 'integer', minimum: 1, maximum: 10 } }, required: ['minAttempts', 'maxAttempts'] },
2657
+ correctionExpectation: { type: 'object', additionalProperties: false, properties: { result: { type: 'string', const: 'accepted-same-operation-target' }, withinFollowingCalls: { type: 'integer', minimum: 1, maximum: 50 } }, required: ['result', 'withinFollowingCalls'] },
2658
+ },
2659
+ required: ['assertionId', 'operationFamily', 'operationName', 'expectedResult', 'rejectionReasonCodes', 'responsibleLayer', 'cardinality'],
2660
+ },
2661
+ },
2662
+ },
2663
+ required: ['schemaVersion', 'expectedRefusals'],
2664
+ },
2135
2665
  graphqlThrottleMode: { type: 'string', enum: ['off', 'standard-1000', 'plus-2000'], description: 'Optional Meguro compatibility GraphQL throttle mode.' },
2136
2666
  },
2137
- required: [],
2138
- anyOf: [{ required: ['storeId'] }, { required: ['worldId'] }],
2667
+ required: ['storeId'],
2139
2668
  },
2140
2669
  },
2141
2670
  {
@@ -2154,10 +2683,13 @@ export function createTools(config) {
2154
2683
  },
2155
2684
  {
2156
2685
  name: 'practice_run_status',
2157
- description: 'Read the server-authoritative state of an external-agent practice simulation run, including its announced simulation-run deadlines and immutable simulation run receipt after any exit. 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.',
2158
2687
  inputSchema: {
2159
2688
  type: 'object', additionalProperties: false,
2160
- 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
+ },
2161
2693
  required: ['attemptId'],
2162
2694
  },
2163
2695
  },
@@ -2166,24 +2698,28 @@ export function createTools(config) {
2166
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.',
2167
2699
  inputSchema: {
2168
2700
  type: 'object', additionalProperties: false,
2169
- 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
+ },
2170
2705
  required: ['attemptId'],
2171
2706
  },
2172
2707
  },
2173
2708
  {
2174
2709
  name: 'practice_run_advance',
2175
- description: 'Advance an external-agent practice run by whole days or until one closed-set commerce condition, using both optimistic concurrency cursors. 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.',
2176
2711
  inputSchema: {
2177
2712
  type: 'object',
2178
2713
  additionalProperties: false,
2179
2714
  properties: {
2715
+ workspaceId: OPTIONAL_WORKSPACE_SELECTOR_INPUT_SCHEMA,
2180
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).' },
2181
2717
  days: { type: 'integer', minimum: 1, description: 'Whole simulated days to advance.' },
2182
2718
  until: PRACTICE_RUN_UNTIL_INPUT_SCHEMA,
2183
- 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.' },
2184
- 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,
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.' },
2185
2721
  },
2186
- required: ['attemptId', 'expectedDay', 'expectedCallSeq'],
2722
+ required: ['attemptId', 'advanceCursor'],
2187
2723
  oneOf: [
2188
2724
  { required: ['days'], not: { required: ['until'] } },
2189
2725
  { required: ['until'], not: { required: ['days'] } },
@@ -2192,43 +2728,57 @@ export function createTools(config) {
2192
2728
  },
2193
2729
  {
2194
2730
  name: 'practice_run_finish',
2195
- 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.',
2196
2732
  inputSchema: {
2197
2733
  type: 'object', additionalProperties: false,
2198
- 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
+ },
2199
2740
  required: ['attemptId'],
2741
+ dependentRequired: {
2742
+ permanentStop: ['acknowledgeIncompleteOnboardingEvaluation'],
2743
+ acknowledgeIncompleteOnboardingEvaluation: ['permanentStop'],
2744
+ },
2200
2745
  },
2201
2746
  },
2202
2747
  {
2203
2748
  name: 'practice_run_report',
2204
- description: 'Read the run\'s receipt — a bounded receipt summary for a completed external-agent practice run. `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.',
2205
2750
  inputSchema: {
2206
2751
  type: 'object', additionalProperties: false,
2207
- 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
+ },
2208
2756
  required: ['attemptId'],
2209
2757
  },
2210
2758
  },
2211
2759
  {
2212
2760
  name: 'practice_run_impact',
2213
- 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.',
2214
2762
  inputSchema: {
2215
2763
  type: 'object', additionalProperties: false,
2216
- 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
+ },
2217
2768
  required: ['attemptId'],
2218
2769
  },
2219
2770
  },
2220
2771
  {
2221
2772
  name: 'get_connection_details',
2222
- description: `Fetch connection material for supported Shopify Admin GraphQL versions ${ADMIN_API_SUPPORTED_VERSION_LABEL}; the default is ${ADMIN_API_DEFAULT_VERSION}. Returns adminApiVersion (default), adminApiVersions (supported set), adminVersions (exact versioned URLs), the default URL, access token, and shop domain for an existing Meguro practice store so an assistant can configure a user agent without opening the dashboard. Inspect the selected version with admin_schema before the first Admin call; an agent targeting a newer Shopify release outside the supported set must treat that behavior as not established. Takes the canonical practice-store id (storeId) and returns it back, so the result passes directly into practice_run_start({ storeId }) — no identifier translation. The practice-store id is not an attemptId (run identity). The hosted MCP path uses the caller's OAuth grant; the public STDIO path uses its workspace-bound account API key.`,
2773
+ description: `Fetch connection material and a structured, state-aware Admin execution guide for supported Shopify Admin GraphQL versions ${ADMIN_API_SUPPORTED_VERSION_LABEL}; the default is ${ADMIN_API_DEFAULT_VERSION}. Returns adminApiVersion (default), adminApiVersions (supported set), adminVersions (exact versioned URLs), the default URL, access token, shop domain, current run status/cursors, valid next calls, and the read/write/idempotency sequence. The hosted OAuth grant or the public STDIO workspace-bound account API key authenticates MCP/control-plane calls only. Shopify-shaped Admin data-plane calls use the returned Admin URL and per-store token in the X-Shopify-Access-Token header; never send the OAuth bearer to that URL. Examples carry only <SHOPIFY_ADMIN_ACCESS_TOKEN>, never the returned secret. Inspect the selected recipe or mutation with admin_schema before the first Admin call; an agent targeting a newer Shopify release outside the supported set must treat that behavior as not established. Takes the canonical practice-store id (storeId) and returns it back, so the result passes directly into practice_run_start({ storeId }) — no identifier translation. The practice-store id is not an attemptId (run identity).`,
2223
2774
  inputSchema: {
2224
2775
  type: 'object',
2225
2776
  additionalProperties: false,
2226
2777
  properties: {
2778
+ workspaceId: OPTIONAL_WORKSPACE_SELECTOR_INPUT_SCHEMA,
2227
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.' },
2228
- worldId: { ...PRACTICE_STORE_ID_INPUT_SCHEMA, description: 'Legacy alias for storeId — the same practice-store id under its old name. Prefer storeId.' },
2229
2780
  },
2230
- required: [],
2231
- anyOf: [{ required: ['storeId'] }, { required: ['worldId'] }],
2781
+ required: ['storeId'],
2232
2782
  },
2233
2783
  },
2234
2784
  {
@@ -2238,6 +2788,7 @@ export function createTools(config) {
2238
2788
  type: 'object',
2239
2789
  additionalProperties: false,
2240
2790
  properties: {
2791
+ workspaceId: OPTIONAL_WORKSPACE_SELECTOR_INPUT_SCHEMA,
2241
2792
  worldId: { type: 'string', minLength: 1, description: 'Meguro practice store id, e.g. w-abc123.' },
2242
2793
  query: { type: 'string', maxLength: 30000, description: 'The Admin GraphQL query or mutation document to run.' },
2243
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}.` },
@@ -2250,16 +2801,66 @@ export function createTools(config) {
2250
2801
  },
2251
2802
  {
2252
2803
  name: 'admin_schema',
2253
- 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.`,
2254
2805
  inputSchema: {
2255
2806
  type: 'object',
2256
2807
  additionalProperties: false,
2257
2808
  properties: {
2258
- 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.' },
2259
- 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.' },
2260
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}.` },
2261
2815
  },
2262
- 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
+ ],
2821
+ },
2822
+ },
2823
+ {
2824
+ name: 'admin_recipes_list',
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.',
2826
+ inputSchema: {
2827
+ type: 'object',
2828
+ additionalProperties: false,
2829
+ properties: {},
2830
+ required: [],
2831
+ },
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'],
2263
2864
  },
2264
2865
  },
2265
2866
  ];
@@ -2277,13 +2878,16 @@ export function createTools(config) {
2277
2878
  if (!args || typeof args !== 'object' || Array.isArray(args)) {
2278
2879
  throw new Error(`${name} arguments must be an object`);
2279
2880
  }
2881
+ if (name === 'workspace_archive' || name === 'workspace_unarchive') {
2882
+ requiredWorkspaceId(args);
2883
+ }
2280
2884
  if (SCHEMA_VALIDATED_TOOL_NAMES.has(name)) {
2281
- // Preserve the established identifier teaching messages while the published schema carries
2282
- // the same shape for schema-first clients. This also handles the equal-value legacy alias
2283
- // pair, whose equality cannot be expressed in portable JSON Schema.
2284
2885
  if (name === 'get_connection_details' || name === 'practice_run_start') requiredPracticeStoreId(args);
2285
2886
  const error = schemaValidationError(definition.inputSchema, args);
2286
- 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
+ }
2287
2891
  return;
2288
2892
  }
2289
2893
  if (definition.inputSchema.additionalProperties !== false) return;
@@ -2302,15 +2906,34 @@ export function createTools(config) {
2302
2906
  if (!DOCUMENTATION_TOPICS.has(topic)) {
2303
2907
  throw new Error(DOCUMENTATION_TOOL_CONTRACT.topicError);
2304
2908
  }
2305
- if (!Number.isSafeInteger(args.version) || args.version < 1) {
2306
- 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);
2307
2916
  }
2308
- return textResult(documentationByTopic(topic, args.version));
2917
+ return textResult(resolveDocumentationRead(topic, requested, DOCUMENTATION_TOOL_CONTRACT));
2309
2918
  }
2310
2919
  case 'templates_list': {
2311
2920
  const workspaceId = optionalWorkspaceId(args);
2312
2921
  const response = await fleetRequest(name, 'GET', '/practice/profiles', undefined, { workspaceId });
2313
- 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));
2314
2937
  }
2315
2938
  case 'stores_list': {
2316
2939
  const workspaceId = optionalWorkspaceId(args);
@@ -2363,59 +2986,6 @@ export function createTools(config) {
2363
2986
  const response = await fleetRequest(name, 'POST', `/workspaces/${encodeURIComponent(workspaceId)}/${action}`, {});
2364
2987
  return 'error' in response ? response.error : textResult(secretSafe(response.value));
2365
2988
  }
2366
- case 'share_create': {
2367
- const workspaceId = optionalWorkspaceId(args);
2368
- if (args.shareId !== undefined) {
2369
- const shareId = requiredShareId(args);
2370
- if (args.artifactType !== undefined || args.artifactRef !== undefined) {
2371
- throw new Error('artifactType and artifactRef are immutable; omit shareId to create a new share');
2372
- }
2373
- const body = {};
2374
- if (args.title !== undefined) {
2375
- body.title = requiredString(args, 'title');
2376
- if (body.title.length > 120) throw new Error('title must be 120 characters or fewer');
2377
- }
2378
- const audienceUserIds = optionalStringList(args, 'audienceUserIds', { maximum: 40 });
2379
- if (audienceUserIds !== undefined) body.audienceUserIds = audienceUserIds;
2380
- if (Object.keys(body).length === 0) throw new Error('share update requires title and/or audienceUserIds');
2381
- const response = await fleetRequest(name, 'PATCH', `/shares/${encodeURIComponent(shareId)}`, body, { workspaceId });
2382
- return 'error' in response ? response.error : textResult(secretSafe(response.value));
2383
- }
2384
- const title = requiredString(args, 'title');
2385
- if (title.length > 120) throw new Error('title must be 120 characters or fewer');
2386
- const { artifactType, artifactRef } = requiredShareArtifact(args);
2387
- const audienceUserIds = optionalStringList(args, 'audienceUserIds', { maximum: 40 });
2388
- if (!audienceUserIds) throw new Error('audienceUserIds is required when creating a share');
2389
- const response = await fleetRequest(name, 'POST', '/shares', {
2390
- title, artifactType, artifactRef, audienceUserIds,
2391
- }, { workspaceId });
2392
- return 'error' in response ? response.error : textResult(secretSafe(response.value));
2393
- }
2394
- case 'shares_list': {
2395
- const workspaceId = optionalWorkspaceId(args);
2396
- const response = await fleetRequest(name, 'GET', '/shares', undefined, { workspaceId });
2397
- return 'error' in response ? response.error : textResult(secretSafe(response.value));
2398
- }
2399
- case 'share_status': {
2400
- const workspaceId = optionalWorkspaceId(args);
2401
- const shareId = requiredShareId(args);
2402
- const response = await fleetRequest(name, 'GET', `/shares/${encodeURIComponent(shareId)}`, undefined, { workspaceId });
2403
- return 'error' in response ? response.error : textResult(secretSafe(response.value));
2404
- }
2405
- case 'share_publish': {
2406
- const workspaceId = optionalWorkspaceId(args);
2407
- const shareId = requiredShareId(args);
2408
- const action = args.action === undefined ? 'publish' : requiredString(args, 'action');
2409
- if (!['publish', 're-share'].includes(action)) throw new Error('action must be publish or re-share');
2410
- const response = await fleetRequest(name, 'POST', `/shares/${encodeURIComponent(shareId)}/${action}`, {}, { workspaceId });
2411
- return 'error' in response ? response.error : textResult(secretSafe(response.value));
2412
- }
2413
- case 'share_revoke': {
2414
- const workspaceId = optionalWorkspaceId(args);
2415
- const shareId = requiredShareId(args);
2416
- const response = await fleetRequest(name, 'POST', `/shares/${encodeURIComponent(shareId)}/revoke`, {}, { workspaceId });
2417
- return 'error' in response ? response.error : textResult(secretSafe(response.value));
2418
- }
2419
2989
  case 'catalog_slice_read': {
2420
2990
  const workspaceId = optionalWorkspaceId(args);
2421
2991
  const shopDomain = requiredShopifyDevDomain(args);
@@ -2429,33 +2999,33 @@ export function createTools(config) {
2429
2999
  }
2430
3000
  case 'catalog_slice_snapshot': {
2431
3001
  const workspaceId = optionalWorkspaceId(args);
2432
- const hasSavedSlice = args.sliceId !== undefined;
2433
- const hasLiveSlice = args.shopDomain !== undefined || args.variantIds !== undefined;
2434
- if (hasSavedSlice === hasLiveSlice) {
2435
- throw new Error('provide exactly one snapshot source: sliceId, or shopDomain plus variantIds');
2436
- }
3002
+ const branch = conditionalToolBranch(name, args);
3003
+ if (!branch) throw new Error('catalog_slice_snapshot branch selection did not match its declared input schema');
2437
3004
  let response;
2438
- if (hasSavedSlice) {
3005
+ if (branch.id === 'saved') {
2439
3006
  const sliceId = requiredSavedSliceId(args);
2440
- 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
+ });
2441
3010
  } else {
2442
3011
  const shopDomain = requiredShopifyDevDomain(args);
2443
3012
  const variantIds = requiredCatalogVariantIds(args);
2444
3013
  response = await fleetRequest(name, 'POST', `/stores/${encodeURIComponent(shopDomain)}/catalog-slice/snapshot`, {
2445
3014
  variantIds,
2446
- }, { workspaceId });
3015
+ }, { workspaceId, invocationMutates: branch.mutates });
2447
3016
  }
2448
3017
  return 'error' in response ? response.error : textResult(catalogSnapshotProjection(response.value));
2449
3018
  }
2450
3019
  case 'catalog_slices_saved': {
2451
3020
  const workspaceId = optionalWorkspaceId(args);
2452
- const action = requiredString(args, 'action');
2453
- if (!['list', 'get', 'save', 'delete', 'refresh', 'changes'].includes(action)) {
2454
- throw new Error('action must be list, get, save, delete, refresh, or changes');
2455
- }
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;
2456
3024
  let response;
2457
3025
  if (action === 'list') {
2458
- response = await fleetRequest(name, 'GET', '/catalog-slices', undefined, { workspaceId });
3026
+ response = await fleetRequest(name, 'GET', '/catalog-slices', undefined, {
3027
+ workspaceId, invocationMutates: branch.mutates,
3028
+ });
2459
3029
  } else if (action === 'save') {
2460
3030
  const shopDomain = requiredShopifyDevDomain(args);
2461
3031
  const label = requiredString(args, 'label');
@@ -2463,14 +3033,20 @@ export function createTools(config) {
2463
3033
  response = await fleetRequest(name, 'POST', `/stores/${encodeURIComponent(shopDomain)}/catalog-slices`, {
2464
3034
  label,
2465
3035
  variantIds: requiredCatalogVariantIds(args),
2466
- }, { workspaceId });
3036
+ }, { workspaceId, invocationMutates: branch.mutates });
2467
3037
  } else {
2468
3038
  const sliceId = requiredSavedSliceId(args);
2469
3039
  const suffix = action === 'refresh'
2470
3040
  ? '/refresh'
2471
3041
  : action === 'changes' ? '/changes/latest' : '';
2472
3042
  const method = action === 'delete' ? 'DELETE' : action === 'refresh' ? 'POST' : 'GET';
2473
- 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
+ );
2474
3050
  }
2475
3051
  return 'error' in response ? response.error : textResult(secretSafe(response.value));
2476
3052
  }
@@ -2533,8 +3109,9 @@ export function createTools(config) {
2533
3109
  case 'run_status': {
2534
3110
  const runId = requiredString(args, 'runId');
2535
3111
  if (PRACTICE_ATTEMPT_ID.test(runId)) return practiceAttemptHistoryReadGuidance(name, runId);
2536
- const status = await api('GET', `/runs/${encodeURIComponent(runId)}`, undefined, { auth: true });
2537
- 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) });
2538
3115
  }
2539
3116
  case 'run_ledger': {
2540
3117
  const runId = requiredString(args, 'runId');
@@ -2545,7 +3122,8 @@ export function createTools(config) {
2545
3122
  case 'run_report': {
2546
3123
  const runId = requiredString(args, 'runId');
2547
3124
  if (PRACTICE_ATTEMPT_ID.test(runId)) return practiceAttemptHistoryReadGuidance(name, runId);
2548
- 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);
2549
3127
  }
2550
3128
  case 'run_resume': {
2551
3129
  const runId = requiredString(args, 'runId');
@@ -2593,9 +3171,9 @@ export function createTools(config) {
2593
3171
  const teaching = isGateReceiptUnavailable(response.status, response.json)
2594
3172
  ? gateReceiptUnavailableTeaching(name, receiptId)
2595
3173
  : undefined;
2596
- return practiceErrorResult(response.status, response.json, response.retryAfterSeconds, teaching);
3174
+ return practiceErrorResult(response.status, response.json, response.retryAfterSeconds, teaching, teaching ? 'pre-commit' : undefined);
2597
3175
  }
2598
- return textResult(secretSafe(response.json));
3176
+ return configuredReceiptGateResult(secretSafe(response.json), enabled ? 'configuration saved' : 'disabled');
2599
3177
  }
2600
3178
  case 'gate_evaluate': {
2601
3179
  const receiptId = args.receiptId === undefined
@@ -2612,9 +3190,9 @@ export function createTools(config) {
2612
3190
  const teaching = receiptId && isGateReceiptUnavailable(response.status, response.json)
2613
3191
  ? gateReceiptUnavailableTeaching(name, receiptId)
2614
3192
  : undefined;
2615
- return practiceErrorResult(response.status, response.json, response.retryAfterSeconds, teaching);
3193
+ return practiceErrorResult(response.status, response.json, response.retryAfterSeconds, teaching, teaching ? 'pre-commit' : undefined);
2616
3194
  }
2617
- return textResult(secretSafe(response.json));
3195
+ return configuredReceiptGateResult(secretSafe(response.json), 'evaluation recorded');
2618
3196
  }
2619
3197
  case 'gate_verdict': {
2620
3198
  const runId = args.runId === undefined ? '' : requiredString(args, 'runId');
@@ -2626,7 +3204,7 @@ export function createTools(config) {
2626
3204
  if (!latest.ok) return practiceErrorResult(latest.status, latest.json, latest.retryAfterSeconds);
2627
3205
  const evaluation = latest.json?.evaluations?.[0];
2628
3206
  if (!evaluation) {
2629
- 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.");
2630
3208
  }
2631
3209
  const verdict = evaluation.gateVerdict;
2632
3210
  if (!verdict || verdict.schemaVersion !== 'meguro.gate-verdict.v1') {
@@ -2634,9 +3212,9 @@ export function createTools(config) {
2634
3212
  const recovery = receiptId
2635
3213
  ? `gate_evaluate({ receiptId: ${JSON.stringify(receiptId)} })`
2636
3214
  : 'gate_evaluate({})';
2637
- 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.`);
2638
3216
  }
2639
- return textResult(secretSafe(verdict));
3217
+ return configuredReceiptGateResult(secretSafe(verdict), 'verdict read');
2640
3218
  }
2641
3219
  const response = await practiceApi('GET', `/practice/evaluation-suites/${encodeURIComponent(runId)}`);
2642
3220
  if (!response.ok && response.status === 404) return legacyGateArtifactNotFoundGuidance(runId);
@@ -2647,32 +3225,51 @@ export function createTools(config) {
2647
3225
  }
2648
3226
  // Exact pass-through is the parity contract: Console and MCP consume these same bytes as
2649
3227
  // an object, with no local policy derivation in this registry.
2650
- return textResult(verdict);
3228
+ return configuredReceiptGateResult(verdict, 'legacy verdict read');
2651
3229
  }
2652
3230
  case 'runs_list': {
2653
3231
  const lifecycle = args.lifecycle === undefined ? undefined : requiredString(args, 'lifecycle');
2654
3232
  if (lifecycle && !['running', 'paused', 'completed', 'failed', 'cleaning'].includes(lifecycle)) {
2655
3233
  throw new Error('lifecycle must be running, paused, completed, failed, or cleaning');
2656
3234
  }
3235
+ const query = new URLSearchParams({
3236
+ limit: '200',
3237
+ ...(lifecycle ? { lifecycle } : {}),
3238
+ });
2657
3239
  return textResult(runsListProjection(
2658
- await api('GET', '/runs?limit=200', undefined, { auth: true }),
3240
+ await api('GET', `/runs?${query}`, undefined, { auth: true }),
2659
3241
  lifecycle,
2660
3242
  ));
2661
3243
  }
2662
- case 'usage_read':
2663
- 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
+ }
2664
3265
  case 'twin_diff': {
2665
3266
  const baselineRunId = requiredString(args, 'baselineRunId');
2666
3267
  const treatedRunId = requiredString(args, 'treatedRunId');
2667
3268
  if (PRACTICE_ATTEMPT_ID.test(baselineRunId) || PRACTICE_ATTEMPT_ID.test(treatedRunId)) {
2668
3269
  return practiceAttemptComparisonGuidanceFor(name, baselineRunId, treatedRunId);
2669
3270
  }
2670
- return textResult(twinDiffProjection(await api(
2671
- 'GET',
2672
- `/runs/${encodeURIComponent(baselineRunId)}/twin-diff/${encodeURIComponent(treatedRunId)}`,
2673
- undefined,
2674
- { auth: true },
2675
- )));
3271
+ const response = await apiRaw('GET', `/runs/${encodeURIComponent(baselineRunId)}/twin-diff/${encodeURIComponent(treatedRunId)}`);
3272
+ return response.ok ? textResult(twinDiffProjection(response.json)) : historyReadErrorResult(response);
2676
3273
  }
2677
3274
  case 'exam_preflight': {
2678
3275
  const attemptId = requiredPracticeAttemptId(args);
@@ -2710,6 +3307,12 @@ export function createTools(config) {
2710
3307
  expectedRevision: revision,
2711
3308
  commandId: `mcp-exam-execute-${examId}-${revision}`,
2712
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);
2713
3316
  } else {
2714
3317
  break;
2715
3318
  }
@@ -2730,18 +3333,14 @@ export function createTools(config) {
2730
3333
  const response = await examRequest(name, 'GET', `/practice-exams/${encodeURIComponent(examId)}`);
2731
3334
  if ('error' in response) return response.error;
2732
3335
  const projected = examLifecycleProjection(response.value);
2733
- 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)) {
2734
3340
  return errorResult(`Shopify Exam ${examId} is ${projected.exam.status}; call exam_status and continue bounded execution before requesting its immutable receipts`);
2735
3341
  }
2736
3342
  if (projected.receipts.retention?.status === 'expired') {
2737
- const retention = projected.receipts.retention;
2738
- return errorResult(
2739
- retention.tier === 'developer'
2740
- ? '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.'
2741
- : retention.tier === 'solo'
2742
- ? '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.'
2743
- : 'Builder keeps receipts available for 365 days. Start a new run to mint a fresh receipt.',
2744
- );
3343
+ return errorResult(projected.receipts.retention.expiredMessage);
2745
3344
  }
2746
3345
  if (!projected.receipts.privateAvailable || !projected.receipts.publicAvailable) {
2747
3346
  return errorResult(`Shopify Exam ${examId} does not yet carry both immutable receipts`);
@@ -2764,6 +3363,7 @@ export function createTools(config) {
2764
3363
  }
2765
3364
  case 'practice_run_start': {
2766
3365
  const storeId = requiredPracticeStoreId(args);
3366
+ const workspaceId = optionalWorkspaceId(args);
2767
3367
  const body = {
2768
3368
  purpose: 'external-agent',
2769
3369
  ...(args.continueFromAttemptId !== undefined ? { continueFromAttemptId: requiredPracticeAttemptId(args, 'continueFromAttemptId') } : {}),
@@ -2771,15 +3371,26 @@ export function createTools(config) {
2771
3371
  ...(args.externalAgent !== undefined ? { externalAgent: args.externalAgent } : {}),
2772
3372
  ...(args.subject !== undefined ? { subject: args.subject } : {}),
2773
3373
  ...(args.observationPlan !== undefined ? { observationPlan: args.observationPlan } : {}),
3374
+ ...(args.assertionPlan !== undefined ? { assertionPlan: args.assertionPlan } : {}),
2774
3375
  ...(args.graphqlThrottleMode !== undefined ? { graphqlThrottleMode: args.graphqlThrottleMode } : {}),
2775
3376
  };
2776
- const response = await practiceApi('POST', `/practice/stores/${encodeURIComponent(storeId)}/playbacks`, body);
2777
- 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
+ }
2778
3388
  const started = response.json ?? {};
2779
3389
  return textResult(secretSafe({
2780
3390
  operation: 'start',
2781
3391
  attemptId: started.attemptId ?? started.practiceRun?.attemptId,
2782
3392
  ...(started.segmentPlan ? { segmentPlan: started.segmentPlan } : {}),
3393
+ ...(started.assertionPlan ? { assertionPlan: started.assertionPlan } : {}),
2783
3394
  practiceRun: practiceRunProjection(started.practiceRun),
2784
3395
  paths: {
2785
3396
  state: started.statePath,
@@ -2799,16 +3410,23 @@ export function createTools(config) {
2799
3410
  ? undefined
2800
3411
  : requiredPracticeAttemptId(args, 'receiptId');
2801
3412
  if (receiptId) {
2802
- const listed = await fleetRequest(name, 'GET', '/practice/playbacks', undefined, { workspaceId });
2803
- if ('error' in listed) return listed.error;
2804
- const playbacks = Array.isArray(listed.value?.playbacks) ? listed.value.playbacks : [];
2805
- const referenced = playbacks.find((attempt) => attempt?.attemptId === receiptId);
2806
- 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 ?? {};
2807
3422
  const receiptStoreId = String(referenced.storeId ?? referenced.worldId ?? '');
2808
3423
  if (requestedStoreId && requestedStoreId !== receiptStoreId) {
2809
3424
  return practiceRunReceiptStoreMismatch(receiptId, receiptStoreId, requestedStoreId);
2810
3425
  }
2811
- 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));
2812
3430
  }
2813
3431
 
2814
3432
  const fleet = await fleetRequest(name, 'GET', '/practice/stores', undefined, { workspaceId });
@@ -2832,12 +3450,19 @@ export function createTools(config) {
2832
3450
  listed.value,
2833
3451
  selectedStoreId,
2834
3452
  requestedStoreId ? 'explicit-store-id' : 'single-store',
3453
+ workspaceId,
2835
3454
  ));
2836
3455
  }
2837
3456
  case 'practice_run_status': {
2838
3457
  const attemptId = requiredPracticeAttemptId(args);
2839
- const response = await practiceApi('GET', `/practice/playbacks/${encodeURIComponent(attemptId)}/state`);
2840
- 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
+ }
2841
3466
  const state = response.json ?? {};
2842
3467
  return textResult(secretSafe({
2843
3468
  operation: 'status',
@@ -2848,7 +3473,8 @@ export function createTools(config) {
2848
3473
  }
2849
3474
  case 'practice_run_checkpoint': {
2850
3475
  const attemptId = requiredPracticeAttemptId(args);
2851
- 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 });
2852
3478
  if (!response.ok) {
2853
3479
  const teaching = isPracticeRunUnavailable(response.status, response.json)
2854
3480
  ? practiceRunUnavailableTeaching(name, attemptId)
@@ -2859,23 +3485,31 @@ export function createTools(config) {
2859
3485
  operation: 'checkpoint',
2860
3486
  attemptId,
2861
3487
  alreadyCaptured: response.json?.alreadyCaptured === true,
3488
+ advanceCursor: response.json?.advanceCursor,
2862
3489
  checkpoint: checkpointProjection(response.json?.checkpoint),
2863
3490
  ...consoleUrlFields(response.json?.consoleRef, response.json?.consoleRefExpiresAt),
2864
3491
  }));
2865
3492
  }
2866
3493
  case 'practice_run_advance': {
2867
3494
  const attemptId = requiredPracticeAttemptId(args);
3495
+ const workspaceId = optionalWorkspaceId(args);
2868
3496
  const hasDays = args.days !== undefined;
2869
3497
  const hasUntil = args.until !== undefined;
2870
3498
  if (hasDays === hasUntil) throw new Error('practice_run_advance requires exactly one of days or until');
2871
- const expectedDay = requiredNonNegativeInteger(args, 'expectedDay');
2872
- const expectedCallSeq = requiredNonNegativeInteger(args, 'expectedCallSeq', -1);
3499
+ const advanceCursor = args.advanceCursor;
2873
3500
  if (hasDays && (!Number.isSafeInteger(args.days) || args.days < 1)) throw new Error('days must be a positive integer');
2874
3501
  if (hasUntil && (!args.until || typeof args.until !== 'object' || Array.isArray(args.until))) throw new Error('until must be an object');
2875
3502
  const response = await practiceApi('POST', `/practice/playbacks/${encodeURIComponent(attemptId)}/advance`, {
2876
- ...(hasDays ? { days: args.days } : { until: args.until }), expectedDay, expectedCallSeq,
2877
- });
2878
- if (!response.ok) return practiceErrorResult(response.status, response.json, response.retryAfterSeconds);
3503
+ ...(hasDays ? { days: args.days } : { until: args.until }),
3504
+ advanceCursor,
3505
+ ...(args.confirmConclusion === true ? { confirmConclusion: true } : {}),
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
+ }
2879
3513
  const advanced = response.json ?? {};
2880
3514
  return textResult(secretSafe({
2881
3515
  operation: 'advance',
@@ -2890,7 +3524,13 @@ export function createTools(config) {
2890
3524
  }
2891
3525
  case 'practice_run_finish': {
2892
3526
  const attemptId = requiredPracticeAttemptId(args);
2893
- 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 });
2894
3534
  if (!response.ok) {
2895
3535
  const teaching = isPracticeRunUnavailable(response.status, response.json)
2896
3536
  ? practiceRunUnavailableTeaching(name, attemptId)
@@ -2906,8 +3546,14 @@ export function createTools(config) {
2906
3546
  }
2907
3547
  case 'practice_run_report': {
2908
3548
  const attemptId = requiredPracticeAttemptId(args);
2909
- const response = await practiceApi('GET', `/practice/playbacks/${encodeURIComponent(attemptId)}/report?v=2`);
2910
- 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
+ }
2911
3557
  return textResult({
2912
3558
  operation: 'report',
2913
3559
  ...receiptProjection(response.json ?? {}),
@@ -2916,17 +3562,24 @@ export function createTools(config) {
2916
3562
  }
2917
3563
  case 'practice_run_impact': {
2918
3564
  const attemptId = requiredPracticeAttemptId(args);
2919
- const response = await practiceApi('GET', `/practice/playbacks/${encodeURIComponent(attemptId)}/impact`);
2920
- 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
+ }
2921
3573
  // T1 owns the shape and the API already ran the public-safe scan; pass it through verbatim
2922
3574
  // (no array truncation) rather than re-projecting.
2923
3575
  return textResult({ operation: 'impact', ...(response.json ?? {}) });
2924
3576
  }
2925
3577
  case 'get_connection_details': {
2926
3578
  const requestedStoreId = requiredPracticeStoreId(args);
3579
+ const workspaceId = optionalWorkspaceId(args);
2927
3580
  let details;
2928
3581
  try {
2929
- details = await api('GET', `/practice/${encodeURIComponent(requestedStoreId)}/connection`, undefined, { auth: true });
3582
+ details = await api('GET', `/practice/${encodeURIComponent(requestedStoreId)}/connection`, undefined, { auth: true, workspaceId });
2930
3583
  } catch (error) {
2931
3584
  if (error instanceof ToolResultError) throw error;
2932
3585
  if (error instanceof Error && error.message.startsWith('404 ')) {
@@ -2934,8 +3587,8 @@ export function createTools(config) {
2934
3587
  }
2935
3588
  throw error;
2936
3589
  }
2937
- const storeId = details.storeId ?? details.worldId ?? requestedStoreId;
2938
- const shopDomain = details.host ?? `${details.worldId ?? requestedStoreId}.meguro.io`;
3590
+ const storeId = details.storeId ?? requestedStoreId;
3591
+ const shopDomain = details.host ?? `${requestedStoreId}.meguro.io`;
2939
3592
  return textResult({
2940
3593
  identity: details.identity,
2941
3594
  // Canonical practice-store id first: pass it straight into practice_run_start({ storeId }).
@@ -2947,6 +3600,7 @@ export function createTools(config) {
2947
3600
  adminApiVersions: details.adminApiVersions,
2948
3601
  adminVersions: details.admin?.versions,
2949
3602
  adminVersionPolicy: details.adminVersionPolicy ?? details.admin?.supportPolicy,
3603
+ adminExecutionGuide: details.adminExecutionGuide,
2950
3604
  env: {
2951
3605
  SHOPIFY_ADMIN_GRAPHQL_URL: details.adminUrl,
2952
3606
  SHOPIFY_ADMIN_ACCESS_TOKEN: details.token,
@@ -2956,6 +3610,7 @@ export function createTools(config) {
2956
3610
  }
2957
3611
  case 'admin_probe': {
2958
3612
  const worldId = requiredWorldId(args);
3613
+ const workspaceId = optionalWorkspaceId(args);
2959
3614
  const body = {
2960
3615
  query: typeof args.query === 'string' ? args.query : '',
2961
3616
  ...(args.apiVersion !== undefined ? { apiVersion: args.apiVersion } : {}),
@@ -2965,7 +3620,7 @@ export function createTools(config) {
2965
3620
  // MEG-34: always ask the server to mint a durable Console reference for a successful probe.
2966
3621
  createConsoleReference: true,
2967
3622
  };
2968
- 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 });
2969
3624
  if (ok) {
2970
3625
  // Replace the internal reference id with a ready-to-open Console URL (opaque ref only).
2971
3626
  const { consoleRef, consoleRefExpiresAt, ...envelope } = json;
@@ -2978,15 +3633,27 @@ export function createTools(config) {
2978
3633
  return { content: [{ type: 'text', text: JSON.stringify(secretSafe(json), null, 2) }], isError: true };
2979
3634
  }
2980
3635
  case 'admin_schema': {
2981
- const lookup = requiredString(args, 'lookup');
2982
- const lookupName = requiredString(args, 'name');
2983
- const body = {
2984
- lookup,
2985
- name: lookupName,
2986
- ...(args.apiVersion !== undefined ? { apiVersion: args.apiVersion } : {}),
2987
- };
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
+ };
2988
3651
  return textResult(await api('POST', '/practice/admin-schema', body, { auth: true }));
2989
3652
  }
3653
+ case 'admin_recipes_list':
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 }));
2990
3657
  default:
2991
3658
  return errorResult(`Unknown tool: ${name}`);
2992
3659
  }
@@ -2999,24 +3666,32 @@ export function createTools(config) {
2999
3666
  meaning: 'The fleet tool arguments were invalid or the request could not be sent.',
3000
3667
  nextStep: 'Correct the arguments shown by the tool schema and retry.',
3001
3668
  }],
3002
- });
3669
+ }, undefined, CONDITIONAL_TOOL_BRANCHES.has(name) ? toolInvocationMutates(name, args) : undefined);
3003
3670
  }
3004
- 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;
3005
3675
  }
3006
3676
  }
3007
3677
 
3008
3678
  async function call(name, args = {}) {
3009
- const mutates = TOOL_PRESENTATION[name]?.readOnlyHint === false;
3679
+ const mutates = toolInvocationMutates(name, args);
3010
3680
  try {
3011
3681
  validateTopLevelArguments(name, args);
3012
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
+ }
3013
3688
  const result = errorResult(error instanceof Error ? error.message : String(error));
3014
3689
  return mutates ? withOperationCommitBoundary(result, 'pre-commit') : result;
3015
3690
  }
3016
- const result = await dispatch(name, args);
3017
- return mutates && result?.isError === true
3018
- ? withOperationCommitBoundary(result, 'unknown')
3019
- : 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);
3020
3695
  }
3021
3696
 
3022
3697
  return { definitions, call };