@sema-agent/core 7.17.7 → 7.19.0

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 (201) hide show
  1. package/CHANGELOG.md +233 -13
  2. package/dist/agents/send-message-tool.js +1 -0
  3. package/dist/agents/subagent.d.ts +1 -0
  4. package/dist/agents/subagent.js +5 -0
  5. package/dist/agents/teacher.js +1 -0
  6. package/dist/agents/verify.js +3 -0
  7. package/dist/brain/anthropic.js +6 -3
  8. package/dist/brain/open-responses.js +10 -4
  9. package/dist/brain/openai.js +5 -2
  10. package/dist/brain/reasoning.d.ts +13 -13
  11. package/dist/brain/reasoning.js +2 -2
  12. package/dist/brain/route-adjudicator.d.ts +18 -0
  13. package/dist/brain/route-adjudicator.js +16 -0
  14. package/dist/brain/stream-engine.js +2 -1
  15. package/dist/brain/stream-shared.d.ts +3 -0
  16. package/dist/brain/stream-shared.js +1 -0
  17. package/dist/core/agent-definition.d.ts +10 -0
  18. package/dist/core/artifact-host.d.ts +41 -4
  19. package/dist/core/artifact-host.js +18 -2
  20. package/dist/core/ask-origin.d.ts +16 -0
  21. package/dist/core/ask-origin.js +8 -0
  22. package/dist/core/auto-mode-arming.d.ts +6 -0
  23. package/dist/core/auto-mode-arming.js +5 -5
  24. package/dist/core/auto-mode-defaults.d.ts +43 -15
  25. package/dist/core/auto-mode-defaults.js +4 -1
  26. package/dist/core/auto-mode.d.ts +184 -59
  27. package/dist/core/auto-mode.js +188 -13
  28. package/dist/core/checkpoint-store.d.ts +23 -2
  29. package/dist/core/checkpoint-store.js +2 -2
  30. package/dist/core/engine-notice.d.ts +77 -1
  31. package/dist/core/engine-notice.js +7 -0
  32. package/dist/core/gate-fold.js +5 -1
  33. package/dist/core/gate-lanes.js +9 -8
  34. package/dist/core/gate-outcome.d.ts +42 -2
  35. package/dist/core/gate-outcome.js +23 -0
  36. package/dist/core/governance-codes.d.ts +1 -1
  37. package/dist/core/governance-codes.js +14 -0
  38. package/dist/core/hook-seats.d.ts +66 -0
  39. package/dist/core/hook-seats.js +37 -0
  40. package/dist/core/hooks.d.ts +1 -1
  41. package/dist/core/read-root-candidate.d.ts +12 -0
  42. package/dist/core/read-root-candidate.js +24 -0
  43. package/dist/core/roles.d.ts +9 -1
  44. package/dist/core/roles.js +4 -0
  45. package/dist/core/runner/advertised-writable-dirs.d.ts +8 -3
  46. package/dist/core/runner/assemble-result.d.ts +2 -1
  47. package/dist/core/runner/assemble-result.js +1 -1
  48. package/dist/core/runner/classifier-ladder.d.ts +227 -0
  49. package/dist/core/runner/classifier-ladder.js +214 -0
  50. package/dist/core/runner/contracts.d.ts +71 -13
  51. package/dist/core/runner/decide-continuation.d.ts +1 -3
  52. package/dist/core/runner/decide-continuation.js +1 -7
  53. package/dist/core/runner/deferral-census.d.ts +52 -0
  54. package/dist/core/runner/deferral-census.js +44 -0
  55. package/dist/core/runner/denial-limit-arms.js +2 -2
  56. package/dist/core/runner/derived-route-fallback.d.ts +8 -4
  57. package/dist/core/runner/derived-route-fallback.js +8 -3
  58. package/dist/core/runner/gate-exit.d.ts +12 -0
  59. package/dist/core/runner/gate-exit.js +6 -3
  60. package/dist/core/runner/prepare-artifact.d.ts +10 -2
  61. package/dist/core/runner/prepare-artifact.js +5 -4
  62. package/dist/core/runner/prepare-ask-lane.js +1 -0
  63. package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
  64. package/dist/core/runner/prepare-caps-and-workflow.js +43 -55
  65. package/dist/core/runner/prepare-config-doors.d.ts +7 -1
  66. package/dist/core/runner/prepare-config-doors.js +13 -13
  67. package/dist/core/runner/prepare-defer-classify.d.ts +13 -19
  68. package/dist/core/runner/prepare-defer-classify.js +8 -3
  69. package/dist/core/runner/prepare-gate-stations.d.ts +26 -0
  70. package/dist/core/runner/prepare-gate-stations.js +1 -1
  71. package/dist/core/runner/prepare-hands-readface.d.ts +24 -7
  72. package/dist/core/runner/prepare-hands-readface.js +60 -24
  73. package/dist/core/runner/prepare-inherited-gate.d.ts +17 -4
  74. package/dist/core/runner/prepare-inherited-gate.js +10 -2
  75. package/dist/core/runner/prepare-lsp.d.ts +2 -2
  76. package/dist/core/runner/prepare-lsp.js +1 -1
  77. package/dist/core/runner/prepare-policy-chain.js +2 -0
  78. package/dist/core/runner/prepare-project-context.d.ts +5 -1
  79. package/dist/core/runner/prepare-project-context.js +7 -4
  80. package/dist/core/runner/prepare-prompt-inputs.d.ts +4 -2
  81. package/dist/core/runner/prepare-prompt-inputs.js +4 -1
  82. package/dist/core/runner/prepare-run-refs.d.ts +9 -1
  83. package/dist/core/runner/prepare-run-refs.js +3 -2
  84. package/dist/core/runner/prepare-suspend-saga.d.ts +5 -0
  85. package/dist/core/runner/prepare-suspend-saga.js +5 -2
  86. package/dist/core/runner/prepare-task.js +19 -14
  87. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -0
  88. package/dist/core/runner/prepare-tool-disclosure-mount.js +10 -2
  89. package/dist/core/runner/prepare-turn-wiring.d.ts +8 -4
  90. package/dist/core/runner/prepare-turn-wiring.js +3 -2
  91. package/dist/core/runner/prepare-wiring-manifest.d.ts +5 -5
  92. package/dist/core/runner/prepare-wiring-manifest.js +5 -2
  93. package/dist/core/runner/resume-apply.js +3 -3
  94. package/dist/core/runner/resume-checkpoint-screen.js +7 -0
  95. package/dist/core/runner/run-harness-handlers.js +4 -1
  96. package/dist/core/runner/run-identity-wiring.js +6 -2
  97. package/dist/core/runner/run-leg.d.ts +2 -0
  98. package/dist/core/runner/run-leg.js +45 -45
  99. package/dist/core/runner/run-notification-lane.js +55 -27
  100. package/dist/core/runner/run-settle-and-teardown.js +1 -0
  101. package/dist/core/runner/run-terminal-adoption.js +16 -13
  102. package/dist/core/runner/run-turn-boundary.js +15 -5
  103. package/dist/core/runner/runtask.d.ts +6 -5
  104. package/dist/core/runner/runtask.js +26 -14
  105. package/dist/core/runner/skill-base-dir.d.ts +54 -0
  106. package/dist/core/runner/skill-base-dir.js +42 -0
  107. package/dist/core/runner/synthetic-tools.d.ts +83 -48
  108. package/dist/core/runner/synthetic-tools.js +69 -32
  109. package/dist/core/runner/tool-defer-gate.d.ts +22 -9
  110. package/dist/core/runner/tool-defer-gate.js +15 -5
  111. package/dist/core/runner/tool-disclosure.d.ts +5 -0
  112. package/dist/core/runner/tool-disclosure.js +2 -0
  113. package/dist/core/runner/tool-progress-lane.d.ts +42 -0
  114. package/dist/core/runner/tool-progress-lane.js +61 -0
  115. package/dist/core/runner/turn-attachments.d.ts +43 -12
  116. package/dist/core/runner/turn-attachments.js +58 -14
  117. package/dist/core/runner-deps.d.ts +48 -7
  118. package/dist/core/sensitive-path-policy.d.ts +58 -17
  119. package/dist/core/sensitive-path-policy.js +33 -5
  120. package/dist/core/skill-listing-budget.d.ts +112 -0
  121. package/dist/core/skill-listing-budget.js +58 -0
  122. package/dist/core/swappable-deps.d.ts +9 -7
  123. package/dist/core/swappable-deps.js +7 -3
  124. package/dist/core/task-event.d.ts +96 -29
  125. package/dist/core/task-notification.d.ts +142 -16
  126. package/dist/core/task-notification.js +106 -0
  127. package/dist/core/task-result.d.ts +9 -1
  128. package/dist/core/task-spec.d.ts +46 -3
  129. package/dist/core/tighten-task-spec.js +7 -5
  130. package/dist/core/tool-model-gate.d.ts +1 -1
  131. package/dist/core/tool-policy.d.ts +19 -4
  132. package/dist/core/tool-policy.js +6 -7
  133. package/dist/core/tool-search-setting.d.ts +78 -0
  134. package/dist/core/tool-search-setting.js +90 -0
  135. package/dist/core/tool-spec.d.ts +45 -4
  136. package/dist/core/types.d.ts +2 -2
  137. package/dist/core/types.js +1 -1
  138. package/dist/core/wiring-manifest.d.ts +46 -0
  139. package/dist/core/wiring-manifest.js +9 -1
  140. package/dist/core/write-protect.d.ts +8 -3
  141. package/dist/core/write-protect.js +8 -5
  142. package/dist/engine/execution-env/node-execution-env.js +10 -4
  143. package/dist/engine/harness/agent-harness.d.ts +42 -0
  144. package/dist/engine/harness/agent-harness.js +61 -29
  145. package/dist/engine/loop/agent-loop.d.ts +68 -3
  146. package/dist/engine/loop/agent-loop.js +109 -16
  147. package/dist/index.d.ts +13 -8
  148. package/dist/index.js +11 -7
  149. package/dist/internal/harness.d.ts +1 -1
  150. package/dist/orchestration/run-workflow-tool.d.ts +3 -0
  151. package/dist/orchestration/run-workflow-tool.js +6 -1
  152. package/dist/orchestration/workflow-governance.d.ts +7 -1
  153. package/dist/orchestration/workflow-governance.js +9 -5
  154. package/dist/orchestration/workflow-primitives.d.ts +3 -0
  155. package/dist/orchestration/workflow-primitives.js +6 -1
  156. package/dist/orchestration/workflow-types.d.ts +48 -13
  157. package/dist/orchestration/workflow-types.js +1 -1
  158. package/dist/orchestration/workflow.js +45 -17
  159. package/dist/prompt-assembly/epoch.js +2 -0
  160. package/dist/prompt-assembly/packs/sema-default.js +2 -2
  161. package/dist/prompt-assembly/section-usage.d.ts +26 -0
  162. package/dist/prompt-assembly/section-usage.js +10 -0
  163. package/dist/prompt-assembly/types.d.ts +8 -6
  164. package/dist/prompts/default.d.ts +13 -0
  165. package/dist/prompts/default.js +16 -8
  166. package/dist/tools/artifact/artifact-text.d.ts +10 -1
  167. package/dist/tools/artifact/artifact-text.js +6 -3
  168. package/dist/tools/artifact/artifact-tool.d.ts +12 -2
  169. package/dist/tools/artifact/artifact-tool.js +203 -70
  170. package/dist/tools/artifact/conformance.d.ts +56 -0
  171. package/dist/tools/artifact/conformance.js +349 -0
  172. package/dist/tools/artifact/local-stub.js +1 -1
  173. package/dist/tools/artifact/reject-breaker.d.ts +28 -0
  174. package/dist/tools/artifact/reject-breaker.js +42 -0
  175. package/dist/tools/fs/bash-lexer.d.ts +132 -0
  176. package/dist/tools/fs/bash-lexer.js +563 -0
  177. package/dist/tools/fs/bash-program-position.d.ts +142 -0
  178. package/dist/tools/fs/bash-program-position.js +125 -0
  179. package/dist/tools/fs/bash-readonly-classifier.d.ts +81 -16
  180. package/dist/tools/fs/bash-readonly-classifier.js +180 -558
  181. package/dist/tools/fs/containment-face.d.ts +89 -0
  182. package/dist/tools/fs/containment-face.js +81 -0
  183. package/dist/tools/fs/fs-bash.d.ts +46 -1
  184. package/dist/tools/fs/fs-bash.js +165 -25
  185. package/dist/tools/fs/fs-read.d.ts +1 -1
  186. package/dist/tools/fs/fs-search-tools.d.ts +3 -3
  187. package/dist/tools/fs/fs-write.d.ts +4 -3
  188. package/dist/tools/fs/fs-write.js +9 -9
  189. package/dist/tools/fs/index.d.ts +24 -5
  190. package/dist/tools/fs/index.js +18 -6
  191. package/dist/tools/fs/read-deny.d.ts +4 -3
  192. package/dist/tools/fs/repo-map.d.ts +2 -2
  193. package/dist/tools/fs/safety.d.ts +18 -3
  194. package/dist/tools/fs/safety.js +11 -4
  195. package/package.json +1 -1
  196. package/test/export-surface.snapshot.json +99 -13
  197. package/wire-changes/core@7.17.7.json +49 -0
  198. package/wire-changes/core@7.18.0.json +1273 -0
  199. package/wire-changes/core@7.19.0.json +457 -0
  200. package/dist/tools/fs/read-face.d.ts +0 -68
  201. package/dist/tools/fs/read-face.js +0 -47
@@ -5,11 +5,14 @@ export type WorkflowRunStatus = "running" | "completed" | "failed";
5
5
  * {@link WorkflowAgentRun.parkedCheckpointToken}); a phase or group never parks (a parked agent ends the run). */
6
6
  export type WorkflowItemStatus = "running" | "completed" | "failed" | "parked";
7
7
  /** The arm a `workflow.park_truth_unreadable` resume refusal fired on (`detail.reason`): the prior run's record could not
8
- * be read (`store_threw`), is absent under this scope (`record_missing`), is not terminal (`record_not_terminal`) or
9
- * carries no `parks` array (`parks_unreadable`); a candidate's checkpoint could not be read (`checkpoint_store_threw`);
10
- * or the record/journal name parks while this workflow holds no checkpoint seat to prove them (`capability_not_armed`).
8
+ * be read (`store_threw`), is absent under this scope (`record_missing`), is not terminal (`record_not_terminal`),
9
+ * carries no `parks` array and nothing to derive one from — an empty journal, or an empty derived union
10
+ * (`parks_unreadable`) — or says its OWN resume admission never completed
11
+ * ({@link WorkflowRun.resumeAdmissionIncomplete} — nothing on it describes a settled park set, so it is not a
12
+ * resume base: `admission_incomplete`); a candidate's checkpoint could not be read (`checkpoint_store_threw`); or
13
+ * the record/journal name parks while this workflow holds no checkpoint seat to prove them (`capability_not_armed`).
11
14
  * Registered in docs/CLOSED-SETS.md with a mint obligation: every throw site spells its word. */
12
- export declare const PARK_TRUTH_UNREADABLE_REASONS: readonly ["store_threw", "record_missing", "record_not_terminal", "parks_unreadable", "checkpoint_store_threw", "capability_not_armed"];
15
+ export declare const PARK_TRUTH_UNREADABLE_REASONS: readonly ["store_threw", "record_missing", "record_not_terminal", "parks_unreadable", "admission_incomplete", "checkpoint_store_threw", "capability_not_armed"];
13
16
  export type ParkTruthUnreadableReason = (typeof PARK_TRUTH_UNREADABLE_REASONS)[number];
14
17
  /** What the checkpoint store said about a parked ordinal's token when a resume carried no acknowledgement for it
15
18
  * (`workflow.park_not_pending`, `detail.status`): the row's own word, or `absent` when the store holds no row. */
@@ -141,6 +144,13 @@ export interface WorkflowAgentRun {
141
144
  * truth read binds on the same two facts — `WorkflowRun.parks`); the checkpoint row itself carries no
142
145
  * workflow key. It is the resume CAPABILITY (never log it or put it in a URL); a durable row is its home
143
146
  * exactly as on the background lane. Absent on every other status.
147
+ *
148
+ * CONTRACT (a resume's second key cache). This token lands in the SAME store write as the run record's
149
+ * {@link WorkflowRun.parks} entry for the same call, so a record carrying one carries both — and one written before
150
+ * `parks` existed carries only this. A resume of a record WITHOUT that list reads a `parked` row as a park CANDIDATE
151
+ * of equal standing with the journal's entry, proven the only way any cache is: one `checkpointStore.get(token)`,
152
+ * whose word is the truth. Deriving adds NO trust; the row is POSITIVE evidence only (an empty `agents` array never
153
+ * reads as "no park"), and one carrying neither this token nor its {@link sessionId} claims its ordinal with NO key.
144
154
  */
145
155
  parkedCheckpointToken?: string;
146
156
  /**
@@ -388,22 +398,47 @@ export interface WorkflowRun {
388
398
  };
389
399
  /**
390
400
  * Every park this run is RESPONSIBLE for: its own (written at the park) and the ones it inherited from the run
391
- * it resumed and did not settle (written at resume admission). KEYS ONLY — the truth of a park is the checkpoint
392
- * store (`get(token).status`), never this list: a listed token may be resolved, expired or reaped, and the next
393
- * resume reads the store for every entry. Present on every record this engine writes (`[]` when there is none)
394
- * EXCEPT a record whose resume admission refused before its inherited set was established (deliberately absent:
395
- * not a resume base). It is an ADMISSION INPUT of the next resume, so a store must project it verbatim and keep
396
- * it through any size degrade; a reader treats absence as a state (an older engine's record, a stripped
397
- * projection), never as `[]`. `token` is the same capability as {@link WorkflowAgentRun.parkedCheckpointToken}
398
- * (never log it, never put it in a URL); `originRunId` is the run whose journal holds the `parked` entry for
399
- * `callKey` (the re-park material).
401
+ * it resumed and did not settle (written when resume admission COMPLETES). KEYS ONLY — the truth of a park is the
402
+ * checkpoint store (`get(token).status`), never this list: a listed token may be resolved, expired or reaped, and
403
+ * the next resume reads the store for every entry. Present on every record this engine writes (`[]` when there is
404
+ * none) EXCEPT a record whose resume admission never completed — that record carries
405
+ * {@link resumeAdmissionIncomplete} instead, which is what says "not a resume base". Absence ALONE is the older
406
+ * engine's record or a stripped projection, and the next resume derives its candidates from the other two key
407
+ * caches ({@link WorkflowAgentRun.parkedCheckpointToken} on this record's `parked` agent rows, and the journal's
408
+ * `parked` entries) — and that union must come back NON-EMPTY: a record with no list beside an empty journal, or
409
+ * beside an empty union, is refused (`parks_unreadable`), zero positive evidence not being evidence of zero (both
410
+ * derived caches only ever hold a park the run MADE, so one it inherited and never reached lives in the list alone).
411
+ * When the list IS present it is the record's word ALONE (the agent rows are not a second opinion, so a size degrade
412
+ * emptying `agents` costs nothing), and it is an ADMISSION INPUT of the next resume: a store must project it
413
+ * verbatim and keep it through any size degrade. `token` is the same capability as
414
+ * {@link WorkflowAgentRun.parkedCheckpointToken} (never log it, never put it in a URL); `originRunId` is the run
415
+ * whose journal holds the `parked` entry for `callKey` (the re-park material).
416
+ *
417
+ * `originUnconfirmed: true` says the entry IS on the record but no journal has confirmed whose it is: the key
418
+ * must be durable before the append is awaited, so it lands first and the confirmation follows. `originRunId`
419
+ * then names the run the park was made in — where the material BELONGS, not where it has been seen — and A
420
+ * CONSUMER MUST NOT READ IT AS CONFIRMED. The member is REMOVED (never written `false`) the moment a journal is
421
+ * read or written carrying this token's `parked` entry, so
422
+ * absence is the confirmed state and an entry written before this member reads as confirmed, exactly as it did; the
423
+ * engine's own material read identity-checks what it finds, so an unconfirmed origin degrades rather than mis-binds.
400
424
  */
401
425
  parks?: Array<{
402
426
  callKey: string;
403
427
  sessionId: string;
404
428
  token: string;
405
429
  originRunId: string;
430
+ originUnconfirmed?: true;
406
431
  }>;
432
+ /**
433
+ * Set (`true`) on a resume run's record from its creation and REMOVED the moment its admission completes — so a record
434
+ * still carrying it is one whose admission never finished (it refused, or the process died inside it). Nothing on such
435
+ * a record describes a settled park set: `parks` is absent, its own journal holds no prefix, and the run it resumed is
436
+ * still the valid base. PRESENCE MEANS NOT A RESUME BASE — the engine refuses (`workflow.park_truth_unreadable` /
437
+ * `admission_incomplete`) and derives no candidate from it — while absence is the ordinary state of every non-resume
438
+ * run and of every completed admission, so a reader treats presence as the signal. Same projection obligation as
439
+ * {@link parks}: dropping it turns a refused record back into an admissible one.
440
+ */
441
+ resumeAdmissionIncomplete?: true;
407
442
  /** RB-243: how many agent results were NOT journaled because they exceeded the per-entry size
408
443
  * cap ({@link MAX_JOURNAL_RESULT_BYTES}) — each skip also logs and journals a tombstone. Additive
409
444
  * observation; absent when 0. */
@@ -1,4 +1,4 @@
1
- export const PARK_TRUTH_UNREADABLE_REASONS = ["store_threw", "record_missing", "record_not_terminal", "parks_unreadable", "checkpoint_store_threw", "capability_not_armed"];
1
+ export const PARK_TRUTH_UNREADABLE_REASONS = ["store_threw", "record_missing", "record_not_terminal", "parks_unreadable", "admission_incomplete", "checkpoint_store_threw", "capability_not_armed"];
2
2
  export const PARK_NOT_PENDING_STATUSES = ["resolved", "expired", "absent"];
3
3
  export class WorkflowBudgetExceededError extends Error {
4
4
  spent;
@@ -564,7 +564,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
564
564
  startedAt: 0,
565
565
  })),
566
566
  agents: [],
567
- ...(opts.resumeFromRunId === undefined ? { parks: [] } : {}),
567
+ ...(opts.resumeFromRunId === undefined ? { parks: [] } : { resumeAdmissionIncomplete: true }),
568
568
  groups: [],
569
569
  stats: { tokens: 0, turns: 0, costMicroUsd: 0, nested: { tokens: 0, turns: 0, tasks: 0, costMicroUsd: 0 } },
570
570
  startedAt: t0,
@@ -999,10 +999,11 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
999
999
  };
1000
1000
  const journalAppendParked = async (callKey, paused) => {
1001
1001
  if (!journalStore)
1002
- return;
1002
+ return false;
1003
1003
  const p = journalStore.append(runId, scope, { callKey, parked: paused });
1004
1004
  journalTail = journalTail.then(() => p).catch(() => undefined);
1005
1005
  await p;
1006
+ return true;
1006
1007
  };
1007
1008
  const parkAgentLeg = async (rec, paused, activityTail, chargeLive) => {
1008
1009
  const s = paused.stats;
@@ -1027,15 +1028,20 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1027
1028
  const ordinal = callKeyOrdinal(rec.callKey);
1028
1029
  const parks = run.parks ?? (run.parks = []);
1029
1030
  const at = parks.findIndex((e) => e.callKey === rec.callKey);
1030
- const key = { callKey: rec.callKey, sessionId: paused.sessionId, token: paused.terminal.token, originRunId: at === -1 ? runId : parks[at].originRunId };
1031
+ const held = at === -1 ? undefined : parks[at];
1032
+ const key = held !== undefined && held.token === paused.terminal.token && held.originUnconfirmed !== true
1033
+ ? { callKey: rec.callKey, sessionId: paused.sessionId, token: paused.terminal.token, originRunId: held.originRunId }
1034
+ : { callKey: rec.callKey, sessionId: paused.sessionId, token: paused.terminal.token, originRunId: runId, originUnconfirmed: true };
1031
1035
  if (at === -1)
1032
1036
  parks.push(key);
1033
1037
  else
1034
1038
  parks[at] = key;
1035
1039
  void persist("update");
1036
1040
  try {
1037
- await journalAppendParked(rec.callKey, paused);
1038
- key.originRunId = runId;
1041
+ if (await journalAppendParked(rec.callKey, paused)) {
1042
+ key.originRunId = runId;
1043
+ delete key.originUnconfirmed;
1044
+ }
1039
1045
  }
1040
1046
  catch (err) {
1041
1047
  emitRunLog(`resume-journal: the PARKED leg at ordinal ${ordinal} ("${rec.label.slice(0, 80)}") could not be recorded in this run's journal (${withoutToken(errText(err), key.token)}) — ` +
@@ -2214,34 +2220,52 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
2214
2220
  throw new WorkflowParkRefusal("workflow.park_truth_unreadable", `startWorkflow: resume from "${fromRunId}" was REFUSED — the prior run's record is not in the run store under this scope, so its parks cannot be proven.`, { reason: "record_missing" });
2215
2221
  if (parkSeat !== undefined && prior !== null && prior.status === "running")
2216
2222
  throw new WorkflowParkRefusal("workflow.park_truth_unreadable", `startWorkflow: resume from "${fromRunId}" was REFUSED — the prior run is still recorded as running; a run that could park must have finished before it is resumed (a crashed park-capable run is decided through the deployment's approval channel, not resumed).`, { reason: "record_not_terminal" });
2217
- if (prior !== null && !Array.isArray(prior.parks))
2218
- throw new WorkflowParkRefusal("workflow.park_truth_unreadable", `startWorkflow: resume from "${fromRunId}" was REFUSED — the prior run's record carries no parks list (written by an older engine, or stripped by the store's projection), so its parks cannot be proven.`, { reason: "parks_unreadable" });
2223
+ if (prior?.resumeAdmissionIncomplete === true)
2224
+ throw new WorkflowParkRefusal("workflow.park_truth_unreadable", `startWorkflow: resume from "${fromRunId}" was REFUSED — that record's own resume admission never completed, so it settled no park set and its journal holds no replayable prefix; the run IT resumed is still the base. Resume that one instead.`, { reason: "admission_incomplete" });
2225
+ const recordParks = Array.isArray(prior?.parks) ? prior.parks : undefined;
2226
+ const noParksList = prior !== null && recordParks === undefined;
2227
+ const parksUnreadable = (why) => new WorkflowParkRefusal("workflow.park_truth_unreadable", `startWorkflow: resume from "${fromRunId}" was REFUSED — the prior run's record carries no parks list (written by an older engine, or stripped by the store's projection) and ${why}, so its parks cannot be proven.`, { reason: "parks_unreadable" });
2228
+ if (noParksList && entries.length === 0)
2229
+ throw parksUnreadable("its journal yielded no entry to derive one from");
2219
2230
  const candidates = new Map();
2220
- const recordParks = prior !== null && Array.isArray(prior.parks) ? prior.parks : [];
2221
- for (const raw of recordParks) {
2231
+ const keyed = (k) => k.token !== "" && k.sessionId !== "";
2232
+ const offerCandidate = (ord, key) => {
2233
+ const recorded = candidates.get(ord);
2234
+ if (recorded !== undefined && (recorded.callKey !== key.callKey || (keyed(recorded) && keyed(key) && (recorded.token !== key.token || recorded.sessionId !== key.sessionId))))
2235
+ throw parkBindingBroken(ord, "the prior run's key caches name different tokens, sessions or calls for it");
2236
+ if (recorded === undefined || (!keyed(recorded) && keyed(key)))
2237
+ candidates.set(ord, key);
2238
+ };
2239
+ for (const raw of recordParks ?? []) {
2222
2240
  const e = raw;
2223
2241
  if (e === null || typeof e !== "object" || typeof e.callKey !== "string" || typeof e.sessionId !== "string" || typeof e.token !== "string" || typeof e.originRunId !== "string" || !/^\d+:/.test(e.callKey)) {
2224
2242
  throw parkBindingBroken(-1, "a recorded park entry is malformed or its call key has no ordinal");
2225
2243
  }
2226
- candidates.set(callKeyOrdinal(e.callKey), { callKey: e.callKey, sessionId: e.sessionId, token: e.token, originRunId: e.originRunId });
2244
+ offerCandidate(callKeyOrdinal(e.callKey), { callKey: e.callKey, sessionId: e.sessionId, token: e.token, originRunId: e.originRunId, ...(e.originUnconfirmed === true ? { originUnconfirmed: true } : {}) });
2227
2245
  }
2228
2246
  for (const [ord, e] of replayByOrdinal.entries()) {
2229
2247
  if (e?.parked === undefined)
2230
2248
  continue;
2231
- const fromJournal = { callKey: e.callKey, sessionId: e.parked.sessionId, token: e.parked.terminal.token, originRunId: opts.resumeFromRunId };
2232
- const recorded = candidates.get(ord);
2233
- if (recorded === undefined)
2234
- candidates.set(ord, fromJournal);
2235
- else if (recorded.token !== fromJournal.token || recorded.sessionId !== fromJournal.sessionId || recorded.callKey !== fromJournal.callKey)
2236
- throw parkBindingBroken(ord, "the run record and the journal name different tokens, sessions or calls for it");
2249
+ offerCandidate(ord, { callKey: e.callKey, sessionId: e.parked.sessionId, token: e.parked.terminal.token, originRunId: opts.resumeFromRunId });
2250
+ const settled = candidates.get(ord);
2251
+ settled.originRunId = opts.resumeFromRunId;
2252
+ delete settled.originUnconfirmed;
2237
2253
  }
2254
+ for (const rec of recordParks === undefined && Array.isArray(prior?.agents) ? prior.agents : []) {
2255
+ if (rec === null || typeof rec !== "object" || rec.status !== "parked")
2256
+ continue;
2257
+ if (typeof rec.callKey !== "string" || !/^\d+:/.test(rec.callKey))
2258
+ throw parkBindingBroken(-1, "a parked agent row is malformed or its call key has no ordinal");
2259
+ offerCandidate(callKeyOrdinal(rec.callKey), { callKey: rec.callKey, sessionId: rec.sessionId ?? "", token: rec.parkedCheckpointToken ?? "", originRunId: opts.resumeFromRunId, originUnconfirmed: true });
2260
+ }
2261
+ if (noParksList && candidates.size === 0)
2262
+ throw parksUnreadable("neither its journal nor its own parked agent rows names a park to derive (a park it inherited and never reached is recorded in that list alone)");
2238
2263
  const inherited = [];
2239
2264
  for (const [ord, key] of [...candidates.entries()].sort((a, b) => a[0] - b[0])) {
2240
2265
  if (replayByOrdinal[ord]?.result !== undefined)
2241
2266
  continue;
2242
2267
  inherited.push(key);
2243
2268
  }
2244
- run.parks = inherited;
2245
2269
  if (parkSeat === undefined) {
2246
2270
  if (inherited.length > 0) {
2247
2271
  throw new WorkflowParkRefusal("workflow.park_truth_unreadable", `startWorkflow: resume from "${fromRunId}" was REFUSED — it is responsible for ${inherited.length} recorded park(s) and this workflow holds no checkpoint store to prove them against (RunWorkflowOptions.checkpointStore); without the seat a parked call would run live beside its pinned child.`, { reason: "capability_not_armed" });
@@ -2261,6 +2285,10 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
2261
2285
  parkedByOrdinal[ord] = truth;
2262
2286
  }
2263
2287
  }
2288
+ if (!finalized) {
2289
+ run.parks = inherited;
2290
+ delete run.resumeAdmissionIncomplete;
2291
+ }
2264
2292
  run.resume = {
2265
2293
  fromRunId: clampRunIdText(opts.resumeFromRunId),
2266
2294
  journalEntries: entries.length,
@@ -23,6 +23,7 @@ const PROBE_FACTS_OFF = {
23
23
  promptProfile: "simple",
24
24
  fableMitigations: false,
25
25
  readFaceOpen: false,
26
+ writeFaceOpen: false,
26
27
  reminderMark: undefined,
27
28
  };
28
29
  const PROBE_FACTS_ON = {
@@ -41,6 +42,7 @@ const PROBE_FACTS_ON = {
41
42
  promptProfile: "classic",
42
43
  fableMitigations: true,
43
44
  readFaceOpen: true,
45
+ writeFaceOpen: true,
44
46
  reminderMark: "PROBE-FIXED-MARK-AXIS0",
45
47
  };
46
48
  const PROBE_VECTORS = [
@@ -1,4 +1,4 @@
1
- import { CYBER_RISK, EXECUTION_ENVIRONMENT, EXECUTION_ENVIRONMENT_OPEN_READS, URL_SAFETY, SUMMARIZE_TOOL_RESULTS, WORKTREE_NOTICE, SUBAGENT_CONSENT_NOTICE, SUBAGENT_DELIVERY_NOTES, harnessHeadLines, } from "../../prompts/default.js";
1
+ import { CYBER_RISK, executionEnvironmentBlockFor, URL_SAFETY, SUMMARIZE_TOOL_RESULTS, WORKTREE_NOTICE, SUBAGENT_CONSENT_NOTICE, SUBAGENT_DELIVERY_NOTES, harnessHeadLines, } from "../../prompts/default.js";
2
2
  import { GOAL_COMPLETION_GUIDANCE, ORCHESTRATION_AWARENESS, ORCHESTRATION_GUIDANCE, ORCHESTRATION_GUIDANCE_DEFERRED, SUPERVISOR_PROMPT, } from "../../prompts/supervisor.js";
3
3
  import { TEAMMATE_COMMUNICATION_ADDENDUM } from "../../prompts/coordinator.js";
4
4
  import { SIMPLE_ACTION_CAUTION, SIMPLE_ACT_DONT_REDERIVE, SIMPLE_AUTONOMY_FABLE, SIMPLE_COMMUNICATING_FABLE, SIMPLE_CORRECTIONS_FABLE, SIMPLE_DELIVERING_WORK_FABLE, SIMPLE_COMMUNICATING_LEAN, SIMPLE_CONTEXT_MANAGEMENT, SIMPLE_PRONOUNS, SIMPLE_TOOL_PARAM_JSON, SEMA_VERIFY_FRESH, SEMA_EVIDENCE_AUDIT, } from "../../prompts/simple-sections.js";
@@ -41,7 +41,7 @@ export const SEMA_DEFAULT_PACK = {
41
41
  rank: 240,
42
42
  ...CORE,
43
43
  admit: (i) => i.facts.policyEnabled || i.facts.isolationEnabled,
44
- content: (i) => (i.facts.readFaceOpen === true ? EXECUTION_ENVIRONMENT_OPEN_READS : EXECUTION_ENVIRONMENT),
44
+ content: (i) => executionEnvironmentBlockFor({ readOpen: i.facts.readFaceOpen === true, writeOpen: i.facts.writeFaceOpen === true }),
45
45
  blockId: "harness.context",
46
46
  },
47
47
  { id: "core/simple.communicating", slot: "harness", rank: 250, ...CORE, admit: (i) => i.facts.promptProfile !== "classic", content: (i) => (i.facts.fableMitigations === true ? SIMPLE_COMMUNICATING_FABLE : SIMPLE_COMMUNICATING_LEAN), blockId: "harness.context" },
@@ -0,0 +1,26 @@
1
+ /** One section's share of the leg's system prompt, as the `context_usage` frame reports it. */
2
+ export interface ContextUsageSection {
3
+ /** The pack section id — the same wordlist `prompt.assembled`'s `sections[].id` uses. */
4
+ id: string;
5
+ /** The section's ESTIMATED token weight (see {@link contextUsageSections} for what that promises). */
6
+ tokens: number;
7
+ /** The section's semantic slot (`identity`, `security`, …), so a view can group without knowing every
8
+ * id. Absent only if the manifest entry carried no slot. */
9
+ kind?: string;
10
+ }
11
+ /**
12
+ * Project the manifest's compiled sections onto the frame's occupancy view.
13
+ * @contract context_usage.sections — `sections[]` is the leg's PREPARE-TIME system-prompt composition,
14
+ * one entry per rendered pack section, `tokens` ESTIMATED from rendered characters at the engine's
15
+ * chars→tokens coefficient. A different measurement from `usedTokens`, the engine's trigger input over the
16
+ * whole conversation. The two coordinates MUST NOT be mixed: do not subtract `Σ sections[].tokens` from
17
+ * `usedTokens`, do not compare a section to `compactAtTokens`, and do not read a change across frames as
18
+ * growth — the value is frozen at prepare and re-reported verbatim at every boundary of the leg. It
19
+ * answers "what is my system prompt made of, and which part is big". Absent when the leg assembled no
20
+ * section IR (a provider that replaced the prompt wholesale ships one opaque block).
21
+ */
22
+ export declare function contextUsageSections(sections: ReadonlyArray<{
23
+ id: string;
24
+ slot?: string;
25
+ chars: number;
26
+ }> | undefined): ContextUsageSection[] | undefined;
@@ -0,0 +1,10 @@
1
+ import { DEFAULT_CHARS_PER_TOKEN } from "../internal/harness.js";
2
+ export function contextUsageSections(sections) {
3
+ if (sections === undefined || sections.length === 0)
4
+ return undefined;
5
+ return sections.map((s) => ({
6
+ id: s.id,
7
+ tokens: Math.ceil(s.chars / DEFAULT_CHARS_PER_TOKEN),
8
+ ...(s.slot !== undefined ? { kind: s.slot } : {}),
9
+ }));
10
+ }
@@ -45,6 +45,10 @@ export interface PromptRuntimeFacts {
45
45
  * open-reads first bullet). OPTIONAL: absence reads as false (roots wording, byte-identical),
46
46
  * so existing fact constructors keep compiling. */
47
47
  readFaceOpen?: boolean;
48
+ /** The resolved WRITE face is OPEN (renders the execution-environment block's open-writes first
49
+ * bullet column). OPTIONAL on the same law as {@link readFaceOpen}: absence reads as false (roots
50
+ * wording, byte-identical), so existing fact constructors keep compiling. */
51
+ writeFaceOpen?: boolean;
48
52
  /** design/319 (A ticket) — the session's reminder provenance mark: threads into the
49
53
  * `core/harness.head` section (single-sourced from `harnessHeadLines`), which extends the
50
54
  * reminder sentence into the strict mark declaration when present. OPTIONAL (public export —
@@ -190,12 +194,10 @@ export interface ComposedPrompt {
190
194
  * routes sections into groups WITHOUT reordering them (emission stays rank order — slot never
191
195
  * drives physical order).
192
196
  *
193
- * Accuracy note (hardening 2026-07-31): the layout is a LOWERING argument, not a pack field —
194
- * `PromptPack` declares none, and every production `layoutSystemBlocks` call site passes no second
195
- * argument, i.e. always `SINGLE_BLOCK_LAYOUT` (one group, one cache_control breakpoint at the end).
196
- * Multi-block layouts (CC209's billing/persona/body) are therefore a lowering CAPABILITY exercised
197
- * only by fixtures today; no bundled pack can select one. The earlier wording ("the pack's layout")
198
- * described an association that does not exist in the type system.
197
+ * The layout is a LOWERING argument, not a pack field — `PromptPack` declares none, and every production
198
+ * `layoutSystemBlocks` call site passes no second argument, i.e. always `SINGLE_BLOCK_LAYOUT` (one group,
199
+ * one cache_control breakpoint at the end). Multi-block layouts are a lowering CAPABILITY exercised only
200
+ * by fixtures today; no bundled pack can select one.
199
201
  */
200
202
  export interface CompiledSystemBlock {
201
203
  groupId: string;
@@ -167,6 +167,10 @@ export declare const URL_SAFETY = "IMPORTANT: You must NEVER generate or guess U
167
167
  * per-message aggregate tool-result budget, which sheds the largest results at any size.
168
168
  */
169
169
  export declare const SUMMARIZE_TOOL_RESULTS = "When working with tool results, write down any important information you might need later in your own response, as the original tool result may be cleared or summarized from the context later.";
170
+ export declare function executionEnvironmentBlockFor(faces?: {
171
+ readOpen?: boolean;
172
+ writeOpen?: boolean;
173
+ }): string;
170
174
  export declare const EXECUTION_ENVIRONMENT: string;
171
175
  /** design/199 F8① — the open-read-face variant (first bullet tells the truth about reads). */
172
176
  export declare const EXECUTION_ENVIRONMENT_OPEN_READS: string;
@@ -319,6 +323,9 @@ export interface EnvironmentFacts {
319
323
  * structured key). Rendered only when "open" (the roots posture is the historical default and
320
324
  * renders nothing new — byte-compat). */
321
325
  readFace?: "open" | "roots";
326
+ /** The RESOLVED write-face containment state ("open" | "roots"; additive structured key). Rendered
327
+ * only when "open" (the roots posture is the historical default and renders nothing new). */
328
+ writeFace?: "open" | "roots";
322
329
  /** OS name (uname -s) — remote = the container's, not the host's. */
323
330
  platform?: string;
324
331
  /** OS version (uname -r). */
@@ -496,6 +503,12 @@ export interface StablePromptContext {
496
503
  * byte-identical.
497
504
  */
498
505
  readFaceOpen?: boolean;
506
+ /**
507
+ * The resolved WRITE face is OPEN: the `# Execution environment` block's first bullet renders its
508
+ * open-writes column so the prompt never claims a write fence that is not there. Absent/false ⇒
509
+ * the historical (roots) wording, byte-identical.
510
+ */
511
+ writeFaceOpen?: boolean;
499
512
  /**
500
513
  * design/319 (A ticket) — the session's reminder provenance mark
501
514
  * ({@link import("../core/reminder-mint.js").mintReminderMark}): when present, the `# Harness`
@@ -109,10 +109,14 @@ You have no persistent memory store: what is said in this conversation is not au
109
109
  export const CYBER_RISK = `IMPORTANT: Assist with authorized security testing, defensive security, CTF challenges, and educational contexts. Refuse requests for destructive techniques, DoS attacks, mass targeting, supply chain compromise, or detection evasion for malicious purposes. Dual-use security tools (C2 frameworks, credential testing, exploit development) require clear authorization context: pentesting engagements, CTF competitions, security research, or defensive use cases.`;
110
110
  export const URL_SAFETY = `IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.`;
111
111
  export const SUMMARIZE_TOOL_RESULTS = `When working with tool results, write down any important information you might need later in your own response, as the original tool result may be cleared or summarized from the context later.`;
112
- function executionEnvironmentBlock(openReads) {
113
- const readWriteLine = openReads
114
- ? "- You can read files anywhere this environment exposes (a small sensitive-path deny list applies), and write within the project working directory. Writes outside it, or to system paths, may be denied by the environment or the permission policy."
115
- : "- You can read and write within the project working directory. Writes outside it, or to system paths, may be denied by the environment or the permission policy.";
112
+ const READ_WRITE_BULLET = {
113
+ "roots/roots": "- You can read and write within the project working directory. Writes outside it, or to system paths, may be denied by the environment or the permission policy.",
114
+ "open/roots": "- You can read files anywhere this environment exposes (a small sensitive-path deny list applies), and write within the project working directory. Writes outside it, or to system paths, may be denied by the environment or the permission policy.",
115
+ "roots/open": "- You can read within the project working directory, and write files anywhere this environment exposes (a small protected-path list still applies). Writes to system paths may still be denied by the environment or the permission policy.",
116
+ "open/open": "- You can read and write files anywhere this environment exposes (small sensitive-path and protected-path lists still apply). Writes to system paths may still be denied by the environment or the permission policy.",
117
+ };
118
+ export function executionEnvironmentBlockFor(faces = {}) {
119
+ const readWriteLine = READ_WRITE_BULLET[`${faces.readOpen === true ? "open" : "roots"}/${faces.writeOpen === true ? "open" : "roots"}`];
116
120
  return `# Execution environment
117
121
  Commands run inside an isolated execution environment (a managed container or remote host), not on the operator's machine. Within it:
118
122
  ${readWriteLine}
@@ -125,8 +129,8 @@ When a command fails, identify the cause before retrying:
125
129
 
126
130
  If a restriction genuinely blocks a necessary action, do NOT attempt to circumvent it (no privilege escalation, no disabling of guards, no destructive workarounds). Adjust your approach, or surface the limitation to the user with the specific evidence you saw.`;
127
131
  }
128
- export const EXECUTION_ENVIRONMENT = executionEnvironmentBlock(false);
129
- export const EXECUTION_ENVIRONMENT_OPEN_READS = executionEnvironmentBlock(true);
132
+ export const EXECUTION_ENVIRONMENT = executionEnvironmentBlockFor();
133
+ export const EXECUTION_ENVIRONMENT_OPEN_READS = executionEnvironmentBlockFor({ readOpen: true });
130
134
  export const WORKTREE_NOTICE = `# Isolated worktree
131
135
  This task runs in its own isolated git worktree — a separate working copy whose root is the working directory shown in # Environment, NOT the repository's main checkout. Any absolute path you were given that points at the main checkout (or another worktree) refers to a DIFFERENT copy; translate it to the same relative path under this worktree's root before reading or writing, and operate only within this worktree. A file's content here may differ from the main checkout, so re-read a file in this worktree before editing it rather than assuming an earlier or external view is current.`;
132
136
  export const WORKTREE_STASH_WARNING = "The git stash stack is shared with the main checkout and all other worktrees, and other agent sessions may push or pop it concurrently. " +
@@ -162,8 +166,9 @@ export function harnessHeadLines(ctx) {
162
166
  }
163
167
  export function harnessContext(ctx) {
164
168
  const blocks = [harnessHeadLines(ctx), CYBER_RISK, URL_SAFETY, SUMMARIZE_TOOL_RESULTS];
165
- if (ctx.policyEnabled || ctx.isolationEnabled)
166
- blocks.push(ctx.readFaceOpen === true ? EXECUTION_ENVIRONMENT_OPEN_READS : EXECUTION_ENVIRONMENT);
169
+ if (ctx.policyEnabled || ctx.isolationEnabled) {
170
+ blocks.push(executionEnvironmentBlockFor({ readOpen: ctx.readFaceOpen === true, writeOpen: ctx.writeFaceOpen === true }));
171
+ }
167
172
  if (ctx.promptProfile !== "classic") {
168
173
  blocks.push(ctx.fableMitigations === true ? SIMPLE_COMMUNICATING_FABLE : SIMPLE_COMMUNICATING_LEAN);
169
174
  blocks.push(SIMPLE_PRONOUNS);
@@ -259,6 +264,9 @@ export function buildEnvironmentContext(facts) {
259
264
  if (facts.readFace === "open") {
260
265
  lines.push("File reads are not confined to the workspace root. A small sensitive-path deny list applies (see refusals for the exact pattern).");
261
266
  }
267
+ if (facts.writeFace === "open") {
268
+ lines.push("File writes are not confined to the workspace root. A small protected-path list still applies, and every write still passes the deployment's approval policy (see refusals for the exact reason).");
269
+ }
262
270
  if (facts.isGitRepo !== undefined)
263
271
  lines.push(`Is a git repository: ${facts.isGitRepo ? "yes" : "no"}`);
264
272
  if (facts.gitWorktreeRoot)
@@ -6,6 +6,7 @@
6
6
  * neutral phrase. The replacement list is registered in the divergence register beside this package.
7
7
  */
8
8
  import type { ArtifactAction, ArtifactPlane } from "../../core/artifact-host.js";
9
+ import type { AssertAllKeysHandled } from "../../core/ask-origin.js";
9
10
  /** Opening sentence of the hosted face (`U$t`, line 51145; "hosted on claude.ai" replaced). */
10
11
  export declare const HOSTED_OPENING = "Render an HTML file to an Artifact \u2014 a default-private web page hosted by this deployment's artifact host. Use this when communicating visually would be clearer than terminal text. Publishing proactively is fine for your own work-product \u2014 artifacts start private. The exception is content that could mislead or cause harm if shared onward: anything imitating a real organization, person, or record, or content the user framed as sensitive. Build those as files, and let the user decide whether they get a URL.";
11
12
  /** The title rule (`G$t`, line 51145). */
@@ -54,7 +55,15 @@ export declare const PARAM_TEXT: {
54
55
  readonly scope: "list only: 'mine' (default) lists artifacts the user owns — the only ones the update flow can target; 'shared' lists artifacts other people shared with the user (read-only); 'all' lists both. Rows are labeled (mine)/(shared) whenever scope is not 'mine'.";
55
56
  readonly path: "read_file only: the published path of the file to return (as listed by 'list_files').";
56
57
  };
57
- /** The tool description for a face: the plane picks the opening, the declared actions pick the paragraphs. */
58
+ /** The opening paragraphs of each declared plane: the hosted opening with its title rule, or the two stub sentences
59
+ * (the stub names no gallery, so no title rule). Every paragraph after the opening is keyed on a declared action. */
60
+ export declare const ARTIFACT_PLANE_OPENING: {
61
+ readonly hosted: readonly ["Render an HTML file to an Artifact — a default-private web page hosted by this deployment's artifact host. Use this when communicating visually would be clearer than terminal text. Publishing proactively is fine for your own work-product — artifacts start private. The exception is content that could mislead or cause harm if shared onward: anything imitating a real organization, person, or record, or content the user framed as sensitive. Build those as files, and let the user decide whether they get a URL.", "**Title**: Set a `<title>` at the top of the HTML — only the first 8KB of the file is scanned for it. It names the artifact in the browser tab and gallery, so make it a name, not a summary: a short noun phrase, typically two to four words, distinctive to this page's subject so the reader can pick it out of a gallery of many — the way an app or a document gets named, never a generic category label, and never a name plus an appended explainer after a dash or colon. When a natural title pairs the name with a generic word, the name is the half that survives the trim — keeping the generic half and dropping the identity makes the title worse, not shorter. And trim only actual explainers: a multi-word title that already reads as one specific name is finished as it is. The explanation belongs in the `description` parameter instead: pass a one-sentence `description` — it becomes the gallery card's subtitle. For HTML publishes, a `title` parameter fills in when the file has no tag (Markdown pages always keep their filename identity). Keep the title stable across redeploys."];
62
+ readonly "local-stub": readonly ["This eval run stubs artifact publishing locally and has no artifact control plane: only publishing (the default action) is available here — list, delete, comments, replies, watches, database writes, asset uploads, and live docs are not.", "This eval run stubs artifact publishing locally: `url` may only name an artifact this run published (an eval-stub:// link), never an artifact hosted anywhere else. Omit `url` to publish fresh."];
63
+ };
64
+ export type OpeningTableCoversEveryPlane = AssertAllKeysHandled<Exclude<ArtifactPlane, keyof typeof ARTIFACT_PLANE_OPENING>>;
65
+ /** The tool description for a face: the plane's opening, the update rule, one paragraph per declared action, and the
66
+ * comments disclosure — no host declares a comments action in this version, so its absence is a fact of every face. */
58
67
  export declare function artifactDescription(plane: ArtifactPlane, actions: ReadonlyArray<ArtifactAction>): string;
59
68
  /** The `url` parameter's description for a face: the list hint and the read clause ride only with their actions. */
60
69
  export declare function urlDescription(actions: ReadonlyArray<ArtifactAction>): string;
@@ -33,11 +33,13 @@ export const PARAM_TEXT = {
33
33
  scope: "list only: 'mine' (default) lists artifacts the user owns — the only ones the update flow can target; 'shared' lists artifacts other people shared with the user (read-only); 'all' lists both. Rows are labeled (mine)/(shared) whenever scope is not 'mine'.",
34
34
  path: "read_file only: the published path of the file to return (as listed by 'list_files').",
35
35
  };
36
+ export const ARTIFACT_PLANE_OPENING = {
37
+ hosted: [HOSTED_OPENING, TITLE_RULE],
38
+ "local-stub": [STUB_PUBLISH_ONLY, STUB_OWN_MINT_ONLY],
39
+ };
36
40
  export function artifactDescription(plane, actions) {
37
- if (plane === "local-stub")
38
- return [STUB_PUBLISH_ONLY, STUB_OWN_MINT_ONLY, UPDATE_RULE, COMMENTS_NOT_ENABLED].join("\n\n");
39
41
  const has = (a) => actions.includes(a);
40
- const parts = [HOSTED_OPENING, TITLE_RULE, UPDATE_RULE];
42
+ const parts = [...ARTIFACT_PLANE_OPENING[plane], UPDATE_RULE];
41
43
  if (has("list") && has("read"))
42
44
  parts.push(UPDATE_FROM_EARLIER_RULE);
43
45
  if (has("read"))
@@ -48,6 +50,7 @@ export function artifactDescription(plane, actions) {
48
50
  parts.push(LIST_FILES_RULE);
49
51
  if (has("read_file"))
50
52
  parts.push(READ_FILE_RULE);
53
+ parts.push(COMMENTS_NOT_ENABLED);
51
54
  return parts.join("\n\n");
52
55
  }
53
56
  export function urlDescription(actions) {
@@ -1,4 +1,5 @@
1
1
  import { type ArtifactHost, type ArtifactHostCapabilities } from "../../core/artifact-host.js";
2
+ import { type OnAsk } from "../../core/tool-policy.js";
2
3
  import type { ToolSpec } from "../../core/types.js";
3
4
  /** What the engine's local-file reader hands back: the canonical key (the path-binding key) and the bytes, or one of
4
5
  * three refusals — the fence's own (`refused`, with its text and card), a file over the byte budget the caller
@@ -35,11 +36,20 @@ export interface ArtifactToolOptions {
35
36
  /** The engine's fs-gated single read of a model-named local file (one read per call; the bytes read are the bytes
36
37
  * published); `maxBytes` is the budget left for this file, judged on metadata before the read. */
37
38
  readLocalFile: (path: string, maxBytes: number, signal?: AbortSignal) => Promise<ArtifactLocalRead>;
39
+ /** The run's effective approver — the same frozen seat the tool gate resolves asks at (`frozenOnAsk` =
40
+ * `spec.onAsk ?? deps.onAsk`), riding the MOUNT dep under the same rule as the workflow mount's `parentOnAsk`: a core
41
+ * mount's execute ctx is minimal, so the seat never rides `ctx`. The shared-read consent gate resolves at it. Absent
42
+ * = the run has no ask face, and a shared read is then REFUSED fail-closed (`artifact.refused`, content withheld) —
43
+ * never silently delivered. */
44
+ onAsk?: OnAsk;
38
45
  }
46
+ /** Supporting files per publish (upstream cap); enforced in the schema AND in the body, for callers that bypass the schema. */
47
+ export declare const MAX_FILES_PER_PUBLISH = 255;
39
48
  export declare function mediaTypeOf(path: string): string;
40
49
  export declare function isTextMedia(mediaType: string | undefined): boolean;
41
- /** One opaque key per tool-call OCCURRENCE (a gateway may reuse a call id across turns; `occurrence` counts prior
42
- * executions of that id in the run), stable for any host retry inside that execution: the host's idempotency key. */
50
+ /** One opaque key per HOST CALL (`occurrence` counts prior host calls made under that tool-call id in this run — so a
51
+ * gateway that reuses a call id across turns never replays, and neither do two host calls of ONE execution), stable
52
+ * for any retry the host itself makes of that call: the host's idempotency key. */
43
53
  export declare function artifactAttemptId(runId: string, toolCallId: string, occurrence?: number): string;
44
54
  /** A published path: relative, forward-slash, no dot segments, no NUL, no backslash; returns the NFC form or the refusal. */
45
55
  export declare function normalizePublishedPath(raw: string): {