@trydreamstate/cli 1.3.3 → 1.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +14 -7
  2. package/agent-plugin/.claude-plugin/plugin.json +1 -1
  3. package/agent-plugin/.codex-plugin/plugin.json +1 -1
  4. package/agent-plugin/hooks/approve-readonly.sh +2 -2
  5. package/agent-plugin/hooks/hooks.json +1 -1
  6. package/agent-plugin/skills/crm/SKILL.md +2 -2
  7. package/agent-plugin/skills/diagnostics/SKILL.md +3 -1
  8. package/agent-plugin/skills/dreamstate/SKILL.md +5 -3
  9. package/agent-plugin/skills/first-win/SKILL.md +2 -2
  10. package/agent-plugin/skills/inbox-replies/SKILL.md +3 -1
  11. package/agent-plugin/skills/knowledge/SKILL.md +3 -5
  12. package/agent-plugin/skills/linkedin-posts/SKILL.md +3 -1
  13. package/agent-plugin/skills/linkedin-warm-signals/SKILL.md +3 -3
  14. package/agent-plugin/skills/list-building/SKILL.md +4 -9
  15. package/agent-plugin/skills/list-building/column-authoring.md +3 -2
  16. package/agent-plugin/skills/list-building/sourcing.md +2 -2
  17. package/agent-plugin/skills/outreach/SKILL.md +3 -3
  18. package/agent-plugin/skills/plays/SKILL.md +3 -3
  19. package/agent-plugin/skills/reddit-posts/SKILL.md +3 -1
  20. package/agent-plugin/skills/skill-authoring/SKILL.md +3 -1
  21. package/agent-plugin/skills/social/SKILL.md +3 -5
  22. package/agent-plugin/skills/visibility/SKILL.md +2 -6
  23. package/agent-plugin/skills/workflows/SKILL.md +2 -2
  24. package/agent-plugin/skills/workflows/lifecycle-and-enrollment.md +1 -1
  25. package/agent-plugin/skills/worksheet-tidy/SKILL.md +3 -1
  26. package/agent-plugin/skills/writing/SKILL.md +3 -5
  27. package/agent-plugin/skills/x-posts/SKILL.md +3 -1
  28. package/dist/agentGuidance.generated.json +4 -4
  29. package/dist/capabilityPosture.d.ts +12 -0
  30. package/dist/capabilityPosture.d.ts.map +1 -0
  31. package/dist/capabilityPosture.js +39 -0
  32. package/dist/client.d.ts +9 -3
  33. package/dist/client.d.ts.map +1 -1
  34. package/dist/client.js +110 -63
  35. package/dist/commands/sequences.d.ts +3 -2
  36. package/dist/commands/sequences.d.ts.map +1 -1
  37. package/dist/commands/sequences.js +19 -37
  38. package/dist/commands/shared.d.ts.map +1 -1
  39. package/dist/commands/shared.js +22 -45
  40. package/dist/commands/workflows.d.ts +3 -2
  41. package/dist/commands/workflows.d.ts.map +1 -1
  42. package/dist/commands/workflows.js +19 -35
  43. package/dist/config.d.ts +5 -1
  44. package/dist/config.d.ts.map +1 -1
  45. package/dist/config.js +12 -2
  46. package/dist/errors.d.ts +2 -6
  47. package/dist/errors.d.ts.map +1 -1
  48. package/dist/errors.js +10 -43
  49. package/dist/gatedRun.d.ts +2 -2
  50. package/dist/gatedRun.d.ts.map +1 -1
  51. package/dist/gatedRun.js +157 -60
  52. package/dist/generated/packVerbs.generated.json +492 -0
  53. package/dist/generated/runContract.generated.d.ts +4 -0
  54. package/dist/generated/runContract.generated.d.ts.map +1 -0
  55. package/dist/generated/runContract.generated.js +7 -0
  56. package/dist/index.d.ts +9 -2
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +164 -59
  59. package/dist/money.d.ts +10 -0
  60. package/dist/money.d.ts.map +1 -0
  61. package/dist/money.js +75 -0
  62. package/dist/registryManifest.d.ts +6 -0
  63. package/dist/registryManifest.d.ts.map +1 -0
  64. package/dist/registryManifest.js +66 -0
  65. package/dist/runContract.d.ts +10 -0
  66. package/dist/runContract.d.ts.map +1 -0
  67. package/dist/runContract.js +13 -0
  68. package/dist/runWait.d.ts +0 -1
  69. package/dist/runWait.d.ts.map +1 -1
  70. package/dist/runWait.js +2 -13
  71. package/dist/turnCache.d.ts +3 -1
  72. package/dist/turnCache.d.ts.map +1 -1
  73. package/dist/turnCache.js +6 -2
  74. package/package.json +4 -2
package/dist/gatedRun.js CHANGED
@@ -1,14 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.code = code;
4
- exports.toolTurnToken = toolTurnToken;
5
- exports.omitsToolTurnForInlineTableRead = omitsToolTurnForInlineTableRead;
6
4
  exports.capabilityRecord = capabilityRecord;
7
5
  exports.capabilityIdempotencyKey = capabilityIdempotencyKey;
8
6
  exports.parameterIdempotencyKey = parameterIdempotencyKey;
9
7
  exports.cliSurfaceRefusal = cliSurfaceRefusal;
10
8
  exports.planFingerprint = planFingerprint;
11
9
  exports.runGated = runGated;
10
+ exports.refusalFromRunResult = refusalFromRunResult;
12
11
  exports.readCapability = readCapability;
13
12
  const node_crypto_1 = require("node:crypto");
14
13
  const client_1 = require("./client");
@@ -17,6 +16,9 @@ const idempotency_1 = require("./idempotency");
17
16
  const schemaCoercion_1 = require("./schemaCoercion");
18
17
  const runWait_1 = require("./runWait");
19
18
  const turnCache_1 = require("./turnCache");
19
+ const capabilityPosture_1 = require("./capabilityPosture");
20
+ const registryManifest_1 = require("./registryManifest");
21
+ const runContract_1 = require("./runContract");
20
22
  /**
21
23
  * The one path every capability run takes: `tools run` and every pack verb
22
24
  * (`sequences ...`, `workflows ...`) call runGated. Gating lives here and only
@@ -32,43 +34,12 @@ function code(error) {
32
34
  ? error.code
33
35
  : undefined;
34
36
  }
35
- function toolTurnToken(value) {
37
+ function optionalToolTurnToken(value) {
36
38
  const root = value && typeof value === 'object' ? value : {};
37
39
  const data = root.data && typeof root.data === 'object' ? root.data : {};
38
- if (typeof data.tool_turn_token === 'string' && data.tool_turn_token.trim())
39
- return data.tool_turn_token;
40
- throw new client_1.CliError('tools get did not return a tool_turn_token.', 'invalid_tool_turn_token', 0);
41
- }
42
- /**
43
- * Free sync reads. The server runs these on the inline read path when
44
- * the body has no tool_turn_token and the request has no Idempotency-Key
45
- * (isInlineReadEligible). A tools get before the run is a second HTTP the
46
- * read does not need. Measured 2026-10-04: tables.workbooks.get full live CLI
47
- * 0.726s against whoami 0.455s, about 0.27s of tools get per harness read-back.
48
- * knowledge.brain.context.get is the same class (catalog: credit_class free,
49
- * async_policy.mode sync, mutates not set). Live 2026-10-05 a standalone
50
- * tools get of that id was 0.400s including node startup, next to whoami
51
- * 0.422s, so the same-process skip is one HTTP, not a second process.
52
- * The set name is historical. Writes are not in this set: they stay fetch-first.
53
- */
54
- const INLINE_READ_CAPABILITIES = new Set([
55
- 'tables.workbooks.get',
56
- 'tables.workbooks.overview',
57
- 'tables.workbooks.list',
58
- 'tables.columns.list',
59
- 'tables.columns.stats',
60
- 'tables.rows.query',
61
- 'tables.rows.get',
62
- 'tables.rows.count',
63
- 'knowledge.brain.context.get',
64
- // Both are stored, free, synchronous inbox reads. Keeping either behind a
65
- // tools_get turns one list or thread open into two HTTP requests, while the
66
- // canonical inline-read gate already validates their read-only contract.
67
- 'unibox.inbox.unified_list',
68
- 'unibox.inbox.conversation_get',
69
- ]);
70
- function omitsToolTurnForInlineTableRead(capabilityId) {
71
- return INLINE_READ_CAPABILITIES.has(capabilityId);
40
+ return typeof data.tool_turn_token === 'string' && data.tool_turn_token.trim()
41
+ ? data.tool_turn_token
42
+ : undefined;
72
43
  }
73
44
  /** The capability record inside a tools get response, or null when the shape is unknown. */
74
45
  function capabilityRecord(fetched) {
@@ -138,7 +109,6 @@ function cliSurfaceRefusal(capabilityId, fetched) {
138
109
  return null;
139
110
  return new client_1.CliError(`${capabilityId} is not available on the CLI surface. Use the web app, MCP or the Architect for it.`, 'unsupported_capability', 0, { details: { capability_id: capabilityId, surface: 'cli' }, retryable: false });
140
111
  }
141
- const QUOTE_HASH_PATTERN = /^sha256:[a-f0-9]{64}$/;
142
112
  /**
143
113
  * What a quote prices: the params plus the ceiling. Keys that say how a call is
144
114
  * made, not what it does, are left out, like the server's own plan digest.
@@ -164,7 +134,7 @@ function quoteFromResult(value, depth = 0) {
164
134
  return null;
165
135
  }
166
136
  const entry = value;
167
- if (typeof entry.quote_hash === 'string' && QUOTE_HASH_PATTERN.test(entry.quote_hash)) {
137
+ if (typeof entry.quote_hash === 'string' && runContract_1.QUOTE_HASH_PATTERN.test(entry.quote_hash)) {
168
138
  const estimate = entry.estimate && typeof entry.estimate === 'object' ? entry.estimate : null;
169
139
  return {
170
140
  quote_hash: entry.quote_hash,
@@ -178,11 +148,103 @@ function quoteFromResult(value, depth = 0) {
178
148
  }
179
149
  return null;
180
150
  }
181
- function quoteSummary(capabilityId, body, quote) {
151
+ function quotePriced(capabilityId, body, quote) {
182
152
  const params = body.params && typeof body.params === 'object' ? body.params : {};
183
153
  const rows = typeof params.max_rows === 'number' ? `${params.max_rows} rows` : 'rows not stated';
184
154
  const credits = quote.maximum_credits === null ? 'credits not stated' : `up to ${quote.maximum_credits} credits`;
185
- return `Dry run of ${capabilityId} priced: ${rows}, ${credits}. Running live on that quote.`;
155
+ return `Dry run of ${capabilityId} priced: ${rows}, ${credits}.`;
156
+ }
157
+ /** A known price of 0 spends nothing. An unknown price or a positive price is sent only after the caller passes the approval flag. */
158
+ function spendNeedsApproval(maximum, approved) {
159
+ if (approved)
160
+ return false;
161
+ if (maximum === 0)
162
+ return false;
163
+ return true;
164
+ }
165
+ const CREDENTIALS_REQUIRED_SENTENCE = 'A connected credential is required before this run can continue.';
166
+ function readinessRecord(value) {
167
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
168
+ }
169
+ function blockerSentence(blocker) {
170
+ if (typeof blocker === 'string' && blocker.trim())
171
+ return blocker.trim();
172
+ const record = readinessRecord(blocker);
173
+ const message = record?.message;
174
+ return typeof message === 'string' && message.trim() ? message.trim() : null;
175
+ }
176
+ function blockerCodeOf(blocker) {
177
+ const record = readinessRecord(blocker);
178
+ const code = record?.code;
179
+ return typeof code === 'string' && /^[a-z][a-z0-9_]*$/.test(code) ? code : null;
180
+ }
181
+ /**
182
+ * HTTP 200 can still be a refusal: smoke and dry_run succeed without spending
183
+ * and attach run_blockers or readiness.blockers. Only those top-level buckets
184
+ * count. A row skip_reason is not a refusal of the command.
185
+ */
186
+ function readinessRefusal(value) {
187
+ const root = readinessRecord(value);
188
+ const data = readinessRecord(root?.data) ?? root;
189
+ if (!data)
190
+ return null;
191
+ const run = readinessRecord(data.run);
192
+ const outputs = readinessRecord(run?.outputs);
193
+ const buckets = [];
194
+ const states = [];
195
+ const take = (readiness) => {
196
+ if (!readiness)
197
+ return;
198
+ if (Array.isArray(readiness.blockers))
199
+ buckets.push(...readiness.blockers);
200
+ if (typeof readiness.state === 'string')
201
+ states.push(readiness.state);
202
+ };
203
+ // Dry runs put readiness on run.outputs. Durable and polled runs put the
204
+ // same object on run.steps[].outputs. A blocked rehearsal is an array of
205
+ // { status:'blocked', blocked_by, next_action }, not { blockers, state }.
206
+ const absorbSurface = (surface) => {
207
+ if (!surface)
208
+ return;
209
+ if (Array.isArray(surface.run_blockers))
210
+ buckets.push(...surface.run_blockers);
211
+ absorbReadiness(surface.readiness);
212
+ };
213
+ const absorbReadiness = (readiness) => {
214
+ if (Array.isArray(readiness)) {
215
+ for (const entry of readiness) {
216
+ const row = readinessRecord(entry);
217
+ if (!row || row.status !== 'blocked')
218
+ continue;
219
+ const blockedBy = Array.isArray(row.blocked_by)
220
+ ? row.blocked_by.filter((item) => typeof item === 'string' && /^[a-z][a-z0-9_]*$/.test(item))
221
+ : [];
222
+ const nextAction = typeof row.next_action === 'string' && row.next_action.trim() ? row.next_action.trim() : null;
223
+ const message = typeof row.message === 'string' && row.message.trim() ? row.message.trim() : null;
224
+ buckets.push({
225
+ ...(blockedBy[0] ? { code: blockedBy[0] } : {}),
226
+ message: nextAction ?? message ?? 'This run is blocked until the workspace is ready.',
227
+ });
228
+ }
229
+ return;
230
+ }
231
+ take(readinessRecord(readiness));
232
+ };
233
+ absorbSurface(outputs);
234
+ if (Array.isArray(run?.steps)) {
235
+ for (const step of run.steps)
236
+ absorbSurface(readinessRecord(readinessRecord(step)?.outputs));
237
+ }
238
+ absorbReadiness(run?.readiness);
239
+ absorbReadiness(data.readiness);
240
+ const codes = buckets.map(blockerCodeOf).filter((code) => Boolean(code));
241
+ const sentences = buckets.map(blockerSentence).filter((sentence) => Boolean(sentence));
242
+ const credentials = codes.includes('credentials_required') || states.includes('credentials_required');
243
+ if (!credentials && codes.length === 0 && sentences.length === 0)
244
+ return null;
245
+ const sentence = sentences[0] ?? (credentials ? CREDENTIALS_REQUIRED_SENTENCE : 'This run is blocked until the workspace is ready.');
246
+ const code = credentials ? 'credentials_required' : codes[0] ?? 'not_ready';
247
+ return new client_1.CliError(sentence, code, 409, { retryable: false });
186
248
  }
187
249
  /** Adds the idempotency key to the printed meta, so a user can see which key to re-run with. */
188
250
  function withIdempotencyMeta(value, key) {
@@ -230,28 +292,38 @@ async function runGated(context, input) {
230
292
  // and a quote is bound to the turn that asked for it. So a turn is reused
231
293
  // only while a quote is pending, together with the quote its dry run
232
294
  // returned, and forgotten once the paid run is admitted.
295
+ const explicitKey = parameterIdempotencyKey(body) ?? input.idempotencyKey;
233
296
  const fingerprint = planFingerprint(body);
234
297
  const cached = input.turnToken
235
298
  ? null
236
299
  : await (0, turnCache_1.reusableTurn)(context.turnsPath, context.baseUrl, context.turnKey, capabilityId, fingerprint, Date.now(), warnTurnCache);
237
300
  const pending = cached?.token ?? null;
238
301
  const live = body.mode === 'live';
239
- const inlineRead = !input.turnToken && !pending && !input.idempotencyKey && omitsToolTurnForInlineTableRead(capabilityId);
240
- if (!input.turnToken && !pending && !inlineRead)
302
+ // --turn-token is the contract. Do not fetch tools get to invent one.
303
+ // A cached inline read also skips the fetch until registry_hash changes.
304
+ const cachedPosture = input.turnToken ? null : (0, registryManifest_1.lookupPosture)(capabilityId);
305
+ const inlineFromCache = !input.turnToken && !pending && !explicitKey && cachedPosture?.inlineRead === true;
306
+ if (!input.turnToken && !pending && !inlineFromCache)
241
307
  await contract();
308
+ const posture = fetched ? (0, capabilityPosture_1.postureFromContract)(fetched) : cachedPosture;
309
+ if (fetched && posture)
310
+ (0, registryManifest_1.rememberPosture)(capabilityId, posture);
242
311
  const refusal = fetched ? cliSurfaceRefusal(capabilityId, fetched) : null;
243
312
  if (refusal)
244
313
  throw refusal;
245
- let turnToken = input.turnToken ?? pending ?? (inlineRead ? undefined : toolTurnToken(fetched));
314
+ const inlineRead = !input.turnToken && !pending && !explicitKey && posture?.inlineRead === true;
315
+ let turnToken = input.turnToken ?? pending ?? (inlineRead ? undefined : optionalToolTurnToken(fetched));
246
316
  // Only a quote the caller named or a dry run of this exact input returned.
247
317
  // The server's refusal also shows a price, but that one is information, not consent.
248
- let quoteHash = typeof body.quote_hash === 'string'
318
+ const explicitQuote = typeof body.quote_hash === 'string';
319
+ let quoteHash = explicitQuote
249
320
  ? body.quote_hash
250
321
  : live ? cached?.quote_hash ?? undefined : undefined;
251
- const explicitKey = parameterIdempotencyKey(body) ?? input.idempotencyKey;
252
- let recoveredInlineRead = false;
322
+ // null means the price was not stated (or the flag named a hash with no ceiling).
323
+ let quotedMaximum = !explicitQuote && cached?.quote_hash ? cached.maximum_credits : undefined;
253
324
  let ranDryRun = false;
254
325
  let sentKey = null;
326
+ const approvalRequired = () => new client_1.CliError('This run spends credits. Rerun the same command with --approved.', 'approval_required', 409, { retryable: false, repair: { kind: 'ask_user', hint: 'Rerun the same command with --approved.' } });
255
327
  const requestBody = () => ({
256
328
  ...body,
257
329
  ...(turnToken ? { tool_turn_token: turnToken } : {}),
@@ -284,16 +356,27 @@ async function runGated(context, input) {
284
356
  let result;
285
357
  try {
286
358
  try {
359
+ if (live && quoteHash) {
360
+ const maximum = explicitQuote ? null : quotedMaximum ?? null;
361
+ if (spendNeedsApproval(maximum, body.approved === true)) {
362
+ stderr.write(`${quotePriced(capabilityId, body, { quote_hash: quoteHash, maximum_credits: maximum })}\n`);
363
+ throw approvalRequired();
364
+ }
365
+ if (body.approved === true || maximum === 0) {
366
+ stderr.write(`${quotePriced(capabilityId, body, { quote_hash: quoteHash, maximum_credits: maximum })}\nRunning live on that quote.\n`);
367
+ }
368
+ }
287
369
  result = await run();
288
370
  }
289
371
  catch (error) {
290
- // The inline skip is only valid while the server still treats this
291
- // capability as a free sync read. capability_not_fetched means the
292
- // fetch-first fence is on, so retry once on the old get-then-run path.
293
- if (inlineRead && !recoveredInlineRead && code(error) === 'capability_not_fetched') {
294
- recoveredInlineRead = true;
372
+ if (!input.turnToken && code(error) === 'capability_not_fetched') {
373
+ // Capability posture can change between tools get and run. Refresh it
374
+ // once rather than maintaining a client-side list of token-free reads.
295
375
  fetched = await client.getTool(capabilityId);
296
- turnToken = toolTurnToken(fetched);
376
+ const refreshed = (0, capabilityPosture_1.postureFromContract)(fetched);
377
+ // The server just required a turn token, so this id is not an inline read.
378
+ (0, registryManifest_1.rememberPosture)(capabilityId, { ...refreshed, inlineRead: false });
379
+ turnToken = optionalToolTurnToken(fetched);
297
380
  result = await run();
298
381
  }
299
382
  else if (live && code(error) === 'quote_required' && !quoteHash && turnToken) {
@@ -318,8 +401,12 @@ async function runGated(context, input) {
318
401
  }
319
402
  ranDryRun = true;
320
403
  quoteHash = quote.quote_hash;
321
- stderr.write(`${quoteSummary(capabilityId, body, quote)}\n`);
322
- await (0, turnCache_1.rememberTurnToken)(context.turnsPath, context.baseUrl, context.turnKey, capabilityId, turnToken, Date.now(), warnTurnCache, { quote_hash: quote.quote_hash, fingerprint });
404
+ quotedMaximum = quote.maximum_credits;
405
+ stderr.write(`${quotePriced(capabilityId, body, quote)}\n`);
406
+ await (0, turnCache_1.rememberTurnToken)(context.turnsPath, context.baseUrl, context.turnKey, capabilityId, turnToken, Date.now(), warnTurnCache, { quote_hash: quote.quote_hash, fingerprint, maximum_credits: quote.maximum_credits });
407
+ if (spendNeedsApproval(quote.maximum_credits, body.approved === true))
408
+ throw approvalRequired();
409
+ stderr.write('Running live on that quote.\n');
323
410
  result = await run();
324
411
  }
325
412
  else {
@@ -334,7 +421,7 @@ async function runGated(context, input) {
334
421
  if (code(error) === 'approval_required') {
335
422
  stderr.write('This run needs a person\'s approval. Show them the rows and cost above; when they agree, rerun the same command with --approved.\n');
336
423
  }
337
- await (0, turnCache_1.rememberTurnToken)(context.turnsPath, context.baseUrl, context.turnKey, capabilityId, turnToken, Date.now(), warnTurnCache, quoteHash ? { quote_hash: quoteHash, fingerprint } : undefined);
424
+ await (0, turnCache_1.rememberTurnToken)(context.turnsPath, context.baseUrl, context.turnKey, capabilityId, turnToken, Date.now(), warnTurnCache, quoteHash ? { quote_hash: quoteHash, fingerprint, ...(quotedMaximum !== undefined ? { maximum_credits: quotedMaximum } : {}) } : undefined);
338
425
  }
339
426
  // Only an unknown or transient result may resume an attempt. A server
340
427
  // refusal with retryable:false is terminal and must receive a new key.
@@ -346,23 +433,33 @@ async function runGated(context, input) {
346
433
  if (!input.turnToken) {
347
434
  if (body.mode === 'dry_run' && turnToken) {
348
435
  const quote = quoteFromResult(result);
349
- await (0, turnCache_1.rememberTurnToken)(context.turnsPath, context.baseUrl, context.turnKey, capabilityId, turnToken, Date.now(), warnTurnCache, quote ? { quote_hash: quote.quote_hash, fingerprint } : undefined);
436
+ await (0, turnCache_1.rememberTurnToken)(context.turnsPath, context.baseUrl, context.turnKey, capabilityId, turnToken, Date.now(), warnTurnCache, quote ? { quote_hash: quote.quote_hash, fingerprint, maximum_credits: quote.maximum_credits } : undefined);
350
437
  }
351
- else if (pending || ranDryRun)
352
- await (0, turnCache_1.forgetTurnToken)(context.turnsPath, context.baseUrl, context.turnKey, capabilityId, warnTurnCache);
353
438
  }
354
439
  let timedOut = false;
355
- if (!input.noWait && !(0, runWait_1.columnRunReturnsRunId)(capabilityId)) {
440
+ if (!input.noWait && !posture?.returnsRunId) {
356
441
  const waited = await (0, runWait_1.waitForRun)(client, result, input.waitTimeout, context.sleep);
357
442
  result = waited.value;
358
443
  timedOut = waited.timedOut;
359
444
  }
445
+ // A timeout still owns the quote and the idempotency key. Forgetting the
446
+ // turn here made the rerun hash a body with no quote_hash and start a second run.
447
+ if (!input.turnToken && body.mode !== 'dry_run' && (pending || ranDryRun) && !timedOut) {
448
+ await (0, turnCache_1.forgetTurnToken)(context.turnsPath, context.baseUrl, context.turnKey, capabilityId, warnTurnCache);
449
+ }
360
450
  // A run still active after waiting has unknown outcome; retain its key for
361
451
  // the read-back/retry rule. A terminal response completes this attempt.
362
452
  if (attemptIdentity && !timedOut)
363
453
  await (0, idempotency_1.finishIdempotencyAttempt)(idempotencyPath, attemptIdentity);
454
+ const blocked = readinessRefusal(result);
455
+ if (blocked)
456
+ throw blocked;
364
457
  return { value: withIdempotencyMeta(result, explicitKey ? null : sentKey), timedOut, idempotencyKey: sentKey, turnToken: turnToken ?? null };
365
458
  }
459
+ /** Blocker refusal for a run envelope that did not go through runGated (runs wait). */
460
+ function refusalFromRunResult(value) {
461
+ return readinessRefusal(value);
462
+ }
366
463
  /** A read the CLI needs before it can form a write, run through the same gate as any other run. */
367
464
  async function readCapability(context, capabilityId, params, waitTimeout) {
368
465
  const outcome = await runGated(context, { capabilityId, body: { params }, waitTimeout });