@sema-agent/core 7.19.0 → 7.20.1

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 (114) hide show
  1. package/CHANGELOG.md +199 -2
  2. package/dist/agents/subagent.js +12 -20
  3. package/dist/agents/teacher.js +2 -5
  4. package/dist/agents/verify.js +21 -10
  5. package/dist/brain/anthropic.js +3 -1
  6. package/dist/brain/open-responses.js +4 -2
  7. package/dist/brain/openai.js +3 -1
  8. package/dist/brain/stream-engine.d.ts +4 -0
  9. package/dist/brain/stream-engine.js +4 -0
  10. package/dist/core/auto-mode.d.ts +37 -18
  11. package/dist/core/auto-mode.js +1 -1
  12. package/dist/core/child-clamp-seats.d.ts +79 -0
  13. package/dist/core/child-clamp-seats.js +29 -0
  14. package/dist/core/engine-notice.d.ts +76 -49
  15. package/dist/core/fs-write-gate-policy.d.ts +17 -9
  16. package/dist/core/fs-write-gate-policy.js +51 -4
  17. package/dist/core/gate-bounded-await.d.ts +38 -0
  18. package/dist/core/gate-bounded-await.js +53 -0
  19. package/dist/core/gate-fold.js +16 -48
  20. package/dist/core/gate-lanes.js +11 -3
  21. package/dist/core/gate-outcome.d.ts +9 -6
  22. package/dist/core/governance-codes.d.ts +16 -1
  23. package/dist/core/governance-codes.js +14 -0
  24. package/dist/core/hooks.d.ts +8 -8
  25. package/dist/core/memory-engine/engine.d.ts +14 -9
  26. package/dist/core/memory-engine/engine.js +21 -8
  27. package/dist/core/memory-engine/layout.d.ts +31 -0
  28. package/dist/core/memory-engine/layout.js +68 -3
  29. package/dist/core/memory-engine/tools.js +6 -0
  30. package/dist/core/memory-engine/types.d.ts +24 -0
  31. package/dist/core/memory.d.ts +19 -0
  32. package/dist/core/memory.js +12 -0
  33. package/dist/core/remote-env.d.ts +32 -0
  34. package/dist/core/remote-env.js +4 -1
  35. package/dist/core/runner/advertised-writable-dirs.d.ts +59 -1
  36. package/dist/core/runner/advertised-writable-dirs.js +51 -3
  37. package/dist/core/runner/announce-once-ledger.d.ts +15 -0
  38. package/dist/core/runner/announce-once-ledger.js +8 -0
  39. package/dist/core/runner/classifier-ladder.d.ts +5 -0
  40. package/dist/core/runner/classifier-ladder.js +13 -2
  41. package/dist/core/runner/contracts.d.ts +44 -1
  42. package/dist/core/runner/denial-limit-arms.d.ts +14 -1
  43. package/dist/core/runner/denial-limit-arms.js +5 -4
  44. package/dist/core/runner/env-capability-census.d.ts +30 -0
  45. package/dist/core/runner/env-capability-census.js +38 -0
  46. package/dist/core/runner/forward-sink.d.ts +32 -0
  47. package/dist/core/runner/forward-sink.js +41 -0
  48. package/dist/core/runner/gate-exit.d.ts +4 -0
  49. package/dist/core/runner/gate-exit.js +4 -1
  50. package/dist/core/runner/git-status-frame.d.ts +0 -9
  51. package/dist/core/runner/listing-fold.d.ts +57 -0
  52. package/dist/core/runner/listing-fold.js +75 -0
  53. package/dist/core/runner/prepare-execution-env.d.ts +1 -1
  54. package/dist/core/runner/prepare-execution-env.js +2 -0
  55. package/dist/core/runner/prepare-gate-stations.d.ts +6 -0
  56. package/dist/core/runner/prepare-gate-stations.js +1 -3
  57. package/dist/core/runner/prepare-hands-readface.d.ts +4 -0
  58. package/dist/core/runner/prepare-hands-readface.js +3 -5
  59. package/dist/core/runner/prepare-listings.d.ts +3 -0
  60. package/dist/core/runner/prepare-listings.js +4 -2
  61. package/dist/core/runner/prepare-memory.js +7 -1
  62. package/dist/core/runner/prepare-policy-chain.js +5 -5
  63. package/dist/core/runner/prepare-prompt-inputs.d.ts +2 -2
  64. package/dist/core/runner/prepare-run-refs.d.ts +4 -2
  65. package/dist/core/runner/prepare-run-refs.js +5 -4
  66. package/dist/core/runner/prepare-task.js +8 -6
  67. package/dist/core/runner/prepare-wiring-manifest.d.ts +8 -1
  68. package/dist/core/runner/prepare-wiring-manifest.js +5 -2
  69. package/dist/core/runner/run-clock-and-content.js +3 -7
  70. package/dist/core/runner/run-harness-handlers.js +10 -10
  71. package/dist/core/runner/run-leg.js +28 -19
  72. package/dist/core/runner/run-settle-and-teardown.js +2 -5
  73. package/dist/core/runner/run-turn-boundary.js +44 -33
  74. package/dist/core/runner/runtask.js +2 -1
  75. package/dist/core/runner/synthetic-tools.d.ts +13 -39
  76. package/dist/core/runner/synthetic-tools.js +15 -59
  77. package/dist/core/runner/task-root-canonical.d.ts +45 -0
  78. package/dist/core/runner/task-root-canonical.js +18 -0
  79. package/dist/core/runner/turn-attachments.d.ts +125 -49
  80. package/dist/core/runner/turn-attachments.js +124 -66
  81. package/dist/core/runner/write-protection-station.d.ts +59 -0
  82. package/dist/core/runner/write-protection-station.js +32 -0
  83. package/dist/core/safe-notify.d.ts +5 -0
  84. package/dist/core/safe-notify.js +24 -2
  85. package/dist/core/sensitive-path-policy.d.ts +53 -27
  86. package/dist/core/sensitive-path-policy.js +16 -26
  87. package/dist/core/skill-listing-budget.d.ts +34 -40
  88. package/dist/core/skill-listing-budget.js +25 -26
  89. package/dist/core/skills-directory.js +17 -10
  90. package/dist/core/task-registry.js +2 -1
  91. package/dist/core/task-spec.d.ts +8 -7
  92. package/dist/core/tool-policy.d.ts +12 -1
  93. package/dist/core/tool-policy.js +39 -25
  94. package/dist/core/wiring-manifest.d.ts +31 -0
  95. package/dist/core/wiring-manifest.js +1 -0
  96. package/dist/core/write-protect.d.ts +124 -18
  97. package/dist/core/write-protect.js +91 -34
  98. package/dist/index.d.ts +6 -5
  99. package/dist/index.js +6 -5
  100. package/dist/orchestration/workflow-primitives.js +10 -20
  101. package/dist/stores/file/memory-store.d.ts +5 -2
  102. package/dist/stores/file/memory-store.js +8 -4
  103. package/dist/tools/fs/bash-readonly-classifier.d.ts +13 -2
  104. package/dist/tools/fs/bash-readonly-classifier.js +318 -36
  105. package/dist/tools/fs/fs-bash.d.ts +1 -1
  106. package/dist/tools/fs/fs-bash.js +12 -25
  107. package/dist/tools/fs/fs-shared.d.ts +22 -0
  108. package/dist/tools/fs/fs-shared.js +30 -0
  109. package/dist/tools/fs/index.d.ts +1 -1
  110. package/dist/tools/fs/index.js +1 -1
  111. package/package.json +2 -1
  112. package/test/export-surface.snapshot.json +25 -3
  113. package/wire-changes/core@7.20.0.json +568 -0
  114. package/wire-changes/core@7.20.1.json +183 -0
@@ -77,9 +77,90 @@ export declare const CHANGED_FILES_MAX = 20;
77
77
  * formatter storms remain a documented false-positive source (hash-confirm 抗假阳 = v2).
78
78
  */
79
79
  export declare const CHANGED_FILES_MTIME_EPS_MS = 2000;
80
- /** Per-boundary payload byte cap over the whole bundle (weak-model context economics). */
80
+ /** Per-boundary byte cap over the bundle's CLIPPABLE frames (weak-model context economics). Intact-class
81
+ * frames ride outside it — see {@link ATTACHMENT_DELIVERY_CLASS}. */
81
82
  export declare const ATTACHMENT_BYTE_CAP: number;
82
83
  export type AttachmentSource = "todo_reminder" | "task_reminder" | "tool_search_usage_reminder" | "changed_files" | "plan_mode" | "date_change" | "instructions_change" | "workflow_size_guideline_change" | "budget_usd" | "total_tokens_reminder" | "background_tasks" | "tools_delta" | "agent_listing" | "skills_listing" | "mcp_instructions" | "mcp_dropped_tools";
84
+ /** How the bundle cap treats a source — see {@link ATTACHMENT_DELIVERY_CLASS}. */
85
+ export type AttachmentDeliveryClass = {
86
+ readonly class: "clippable";
87
+ } | {
88
+ readonly class: "intact";
89
+ /** The producer's own byte bound for the frame it renders from these inputs — the promise the lane
90
+ * relies on instead of clipping. A closed form of bounded quantities (a resolved budget, a list cap,
91
+ * a per-item cap, a constant), never of unbounded remote text. */
92
+ readonly bound: (inp: AttachmentInputs) => number;
93
+ };
94
+ /**
95
+ * @contract attachments.delivery_class — every attachment source is one of two classes, declared HERE
96
+ * and nowhere else. An INTACT source's consumption is "recorded when delivered" (a listing's announced
97
+ * set, a drained pending list, an append-⇔-commit fixed frame), so a clipped frame could never be
98
+ * recorded and would re-render at every boundary; the lane therefore never clips or drops it, and the
99
+ * producer bounds it (`bound`). A CLIPPABLE source is a window-based reminder that re-fires at the next
100
+ * quiet boundary, so the bundle cap ({@link ATTACHMENT_BYTE_CAP}) may clip or drop it. `satisfies
101
+ * Record<AttachmentSource, …>` is the totality guard: a new source that is not classed here — or an
102
+ * intact one without its bound — does not compile.
103
+ */
104
+ export declare const ATTACHMENT_DELIVERY_CLASS: {
105
+ readonly todo_reminder: {
106
+ readonly class: "clippable";
107
+ };
108
+ readonly task_reminder: {
109
+ readonly class: "clippable";
110
+ };
111
+ readonly tool_search_usage_reminder: {
112
+ readonly class: "clippable";
113
+ };
114
+ readonly changed_files: {
115
+ readonly class: "clippable";
116
+ };
117
+ readonly plan_mode: {
118
+ readonly class: "clippable";
119
+ };
120
+ readonly budget_usd: {
121
+ readonly class: "clippable";
122
+ };
123
+ readonly total_tokens_reminder: {
124
+ readonly class: "clippable";
125
+ };
126
+ readonly background_tasks: {
127
+ readonly class: "clippable";
128
+ };
129
+ readonly tools_delta: {
130
+ readonly class: "intact";
131
+ readonly bound: (inp: AttachmentInputs) => number;
132
+ };
133
+ readonly agent_listing: {
134
+ readonly class: "intact";
135
+ readonly bound: (inp: AttachmentInputs) => number;
136
+ };
137
+ readonly skills_listing: {
138
+ readonly class: "intact";
139
+ readonly bound: (inp: AttachmentInputs) => number;
140
+ };
141
+ readonly mcp_instructions: {
142
+ readonly class: "intact";
143
+ readonly bound: (inp: AttachmentInputs) => number;
144
+ };
145
+ readonly mcp_dropped_tools: {
146
+ readonly class: "intact";
147
+ readonly bound: (inp: AttachmentInputs) => number;
148
+ };
149
+ readonly date_change: {
150
+ readonly class: "intact";
151
+ readonly bound: () => number;
152
+ };
153
+ readonly instructions_change: {
154
+ readonly class: "intact";
155
+ readonly bound: () => number;
156
+ };
157
+ readonly workflow_size_guideline_change: {
158
+ readonly class: "intact";
159
+ readonly bound: () => number;
160
+ };
161
+ };
162
+ /** Whether the bundle cap must leave this source whole ({@link ATTACHMENT_DELIVERY_CLASS}). */
163
+ export declare function isIntactSource(source: AttachmentSource): boolean;
83
164
  /** G1 续批 — one agent type on the delegation tool's roster (ToolSpec.agentListing, filled by
84
165
  * createSubagentTool: defined agents + the built-in fork when offered). */
85
166
  export interface AgentListingEntry {
@@ -196,14 +277,6 @@ export interface AttachmentState {
196
277
  * `announcedListings.models` so a catalog change across a suspend IS re-announced (the drift
197
278
  * line rides the first boundary's agent_listing frame). */
198
279
  announcedModels?: readonly string[];
199
- /** [c209-C] R2 C4② — same-boundary anti-starvation latch: set by {@link collectDueAttachments}
200
- * when a rendered skills_listing block did NOT survive its own bundle intact (clipped/starved —
201
- * typically because the higher-priority agent_listing spent the byte budget first), cleared when
202
- * one survives. While set, the NEXT boundary orders skills_listing BEFORE agent_listing, so two
203
- * listing frames that cannot share one 8KB bundle alternate to completion instead of the
204
- * agent frame permanently starving the skills frame (each frame individually fits the cap by
205
- * construction — agent roster ≤ cap by deployment size, skills block ≤ SKILLS_BLOCK_MAX_BYTES). */
206
- skillsListingStarved?: boolean;
207
280
  /**
208
281
  * The `padded-countdown` arm's SMOOTHING FLOOR: the highest usage figure this run has
209
282
  * already published against. CC's `cumulativeUsed` (bundle: `Math.max(storedFloor, rolledOver +
@@ -546,9 +619,10 @@ export interface AttachmentInputs {
546
619
  * the delta path serves dynamic-installation deployments and tests, same posture as
547
620
  * `agentListing`). Only passed when `config.skillsListing` is on. */
548
621
  skillsListing?: ReadonlyArray<SkillListingEntry>;
549
- /** The lowered skills-listing budget seats, resolved once at the config door. Absent ⇒ the module
550
- * defaults, i.e. byte-identical to the pre-seat render. */
551
- skillsListingBudget?: ResolvedSkillListingBudget;
622
+ /** The listing family's budget, resolved once at the config door: `agent_listing` and `skills_listing`
623
+ * are each folded into it (each frame measured against the whole number, not a share). Absent ⇒ the
624
+ * seat reader's no-knob answer. */
625
+ listingBudget?: ResolvedSkillListingBudget;
552
626
  /** G1 续批 mcp_instructions: pending deltas snapshotted from `MaterializedMcp.instructionsDelta`.
553
627
  * CALLER-owned consumption on intact cap survival (same `newTools` discipline). */
554
628
  mcpInstructions?: {
@@ -823,11 +897,24 @@ export declare function agentListingDeltaHeader(toolName: string): string;
823
897
  * no re-render byte-identity exposure of its own.
824
898
  *
825
899
  * PURE — never mutates `state`. The announced-set advance is CALLER-owned via
826
- * {@link commitAgentListing}, committed ONLY when the rendered block survived the byte cap INTACT
827
- * (tools_delta/mcp_instructions discipline — a cap-clipped/dropped block previously advanced the
828
- * announced map anyway, losing the roster announcement forever).
900
+ * {@link commitAgentListing}, committed on the carrying message's receipt (`FrameReceipt`).
901
+ *
902
+ * The roster is folded into the listing budget by the one folding engine ({@link foldListingWithin}),
903
+ * the skills block's sibling: every type stays listed, the last-declared ones lose their detail first,
904
+ * and the frame's size is this producer's own promise ({@link ATTACHMENT_DELIVERY_CLASS}). The report
905
+ * (`nameOnly` / `dropped`) covers the roster on an initial frame and the added set on a drift frame.
829
906
  */
830
- export declare function renderAgentListingDelta(state: AttachmentState, entries: ReadonlyArray<AgentListingEntry>, toolName: string, models?: readonly string[]): string | undefined;
907
+ export declare function renderAgentListingFrame(state: AttachmentState, entries: ReadonlyArray<AgentListingEntry>, toolName: string, models?: readonly string[], budget?: ResolvedSkillListingBudget): AgentListingFrameRender | undefined;
908
+ /** The text-only face of {@link renderAgentListingFrame} — what the delivery lanes render. */
909
+ export declare function renderAgentListingDelta(state: AttachmentState, entries: ReadonlyArray<AgentListingEntry>, toolName: string, models?: readonly string[], budget?: ResolvedSkillListingBudget): string | undefined;
910
+ /** What one {@link renderAgentListingFrame} call produced: the frame, the report, and how many types it set out to list. */
911
+ export interface AgentListingFrameRender {
912
+ text: string;
913
+ nameOnly: readonly string[];
914
+ dropped: readonly string[];
915
+ /** The denominator for the disclosure: the whole roster on an initial frame, the added set on a drift frame. */
916
+ listed: number;
917
+ }
831
918
  /** [c209-C] R2 C7 — the Q4 catalog line, single-sourced for the initial frame's tail AND the
832
919
  * drift re-announce arm so the two can never diverge byte-wise. [c209-C] 收口 C7 补口: an
833
920
  * EMPTY catalog renders an honest "(none)" — reachable only from the DRIFT arm (a known announced
@@ -841,9 +928,8 @@ export declare const MODELS_AVAILABLE_PREFIX = "Models available for the 'model'
841
928
  export declare function replayAnnouncedModels(texts: readonly string[]): readonly string[] | undefined;
842
929
  /**
843
930
  * MED-3① commit half of {@link renderAgentListingDelta}: mark the CURRENT roster as announced. The run
844
- * loop calls this only when the `agent_listing` block survived the capped bundle INTACT (body equality
845
- * against a re-render — same predicate as the tools_delta drain); a clipped/dropped block leaves the
846
- * announced map untouched so the next quiet boundary re-announces. Snapshotting `entries` wholesale is
931
+ * loop calls this on the receipt of the message that carried the frame; an undelivered frame leaves the
932
+ * announced map untouched so the next boundary re-announces. Snapshotting `entries` wholesale is
847
933
  * equivalent to add/remove bookkeeping (the diff is name-keyed; descriptions are never re-diffed).
848
934
  */
849
935
  export declare function commitAgentListing(state: AttachmentState, entries: ReadonlyArray<AgentListingEntry>, models?: readonly string[]): void;
@@ -874,10 +960,8 @@ export interface SkillsListingFrameRender extends SkillsBlockRender {
874
960
  *
875
961
  * ONE RULER FOR EVERY FRAME: the drift frame is measured against the SAME resolved budget as the
876
962
  * initial block, arms and joiners included, and its ADD arm degrades through the same three tiers
877
- * ({@link renderSkillLinesWithin}). An unmeasured drift frame is the block's own failure mode one lane
878
- * over: a roster's worth of new skills renders past `ATTACHMENT_BYTE_CAP`, `capAttachments` clips the
879
- * body, a clipped body never equals the intact-survival re-render, and the frame is re-rendered and
880
- * re-clipped at every boundary for the life of the task. The split: the ambient tail and the removal
963
+ * ({@link renderSkillLinesWithin}). The frame is an intact-class attachment — the lane never clips
964
+ * it — so this measurement is its whole bound. The split: the ambient tail and the removal
881
965
  * arm's one-line floor are reserved first (both constants), the ADD arm degrades into what is left,
882
966
  * and the removal arm spends whatever the add arm did not — so the frame's size is the resolved budget
883
967
  * or, when even the two floors do not fit it, a small constant, never a function of the roster.
@@ -886,7 +970,7 @@ export interface SkillsListingFrameRender extends SkillsBlockRender {
886
970
  * description or could not list at all. A departed name that did not fit is not a listed skill.
887
971
  *
888
972
  * MED-3① discipline: PURE — the announced-set advance is CALLER-owned via {@link commitSkillsListing},
889
- * committed only on first-frame delivery or intact cap survival at a boundary. The commit records
973
+ * committed on the carrying message's receipt. The commit records
890
974
  * EVERY entry, including one this frame dropped: the alternative is a frame re-rendering the same
891
975
  * un-listable tail at every boundary, which is the replay this budget exists to end. The `Skill` tool
892
976
  * serves a dropped skill, and `skills.listing_truncated` tells the deployment it happened.
@@ -896,19 +980,16 @@ export declare function renderSkillsListingFrame(state: AttachmentState, entries
896
980
  * the intact-survival predicate re-renders to compare. */
897
981
  export declare function renderSkillsListingDelta(state: AttachmentState, entries: ReadonlyArray<SkillListingEntry>, budget?: ResolvedSkillListingBudget): string | undefined;
898
982
  /** [c209-C] commit half of {@link renderSkillsListingDelta} — same MED-3① contract as
899
- * {@link commitAgentListing}: called by the run loop only on first-frame delivery or when the
900
- * `skills_listing` block survived the capped bundle INTACT; a clipped/dropped block leaves the
901
- * announced map untouched so the next quiet boundary re-announces. */
983
+ * {@link commitAgentListing}: called by the run loop on the receipt of the message that carried the
984
+ * frame; an undelivered frame leaves the announced map untouched so the next boundary re-announces. */
902
985
  export declare function commitSkillsListing(state: AttachmentState, entries: ReadonlyArray<SkillListingEntry>): void;
903
986
  /**
904
987
  * Per-server bound (in CODE POINTS, matching every other clipper in `untrusted-text.ts`) on
905
- * ONE MCP server's `instructions` text. The text is SERVER-controlled and UNBOUNDED on the wire: the
906
- * bundle byte cap ({@link ATTACHMENT_BYTE_CAP}) is a per-frame structural backstop on the delta lane
907
- * and does not exist AT ALL on the stable system-prompt lane, so without this a single server could
908
- * pin an arbitrarily large blob into the cached prompt prefix for the whole task (and, on the delta
909
- * lane, wedge the frame permanently against the 8KB cap — the failure shape).
910
- * Sized at the delta lane's own order of magnitude (the 8KB frame budget), leaving fence + header
911
- * room under the bundle cap once the "…" truncation marker lands.
988
+ * ONE MCP server's `instructions` text. The text is SERVER-controlled and UNBOUNDED on the wire, and
989
+ * neither lane it rides has a cap above this one: the stable system-prompt lane has none, and the
990
+ * `mcp_instructions` attachment is intact-class (the bundle cap never clips it), so this bound IS the
991
+ * producer's promise — without it a single server could pin an arbitrarily large blob into the cached
992
+ * prompt prefix or into one boundary frame.
912
993
  */
913
994
  export declare const MCP_INSTRUCTIONS_MAX_CHARS: number;
914
995
  /**
@@ -942,8 +1023,8 @@ export declare function fenceMcpServerInstructions(server: string, text: string)
942
1023
  * the text is additionally already `sanitizeUntrustedText`-neutralized at the mcp.ts producer).
943
1024
  * The fence is now applied through {@link fenceMcpServerInstructions}, shared with the
944
1025
  * STABLE system-prompt lane, so the two lanes can never again carry the same text with different
945
- * trust postures — and it carries a per-server {@link MCP_INSTRUCTIONS_MAX_CHARS} bound (a
946
- * server text above the bundle cap would otherwise clip → never drain → re-render forever).
1026
+ * trust postures — and it carries a per-server {@link MCP_INSTRUCTIONS_MAX_CHARS} bound (the
1027
+ * frame is intact-class: its size is this producer's promise, servers × that cap).
947
1028
  * The disconnect list is followed by the CC-verbatim {@link AMBIENT_CONTEXT_NOTE} (CC `Emr`,
948
1029
  * pretty.js:599240/599992 — resolved by the 2026-07-10 parity spot audit).
949
1030
  * Removal names are `\n`-joined WITHOUT a `- ` prefix (CC-exact; the agent-listing removal list DOES
@@ -974,21 +1055,16 @@ export declare function renderMcpInstructionsDelta(added: ReadonlyArray<{
974
1055
  * loop's intact-survival consumption check (tools_delta discipline).
975
1056
  */
976
1057
  export declare function renderMcpDroppedTools(entries: ReadonlyArray<McpDroppedTool>): string;
977
- /** per-boundary byte bound for one mcp_dropped_tools frame — half the bundle cap, so
978
- * the batch can never monopolize the boundary against the listing family/changed_files. */
1058
+ /** Per-boundary byte bound for one mcp_dropped_tools frame — the producer's own promise for an
1059
+ * intact-class frame ({@link ATTACHMENT_DELIVERY_CLASS}), so one batch reads as one bounded frame. */
979
1060
  export declare const MCP_DROPPED_BATCH_MAX_BYTES = 4096;
980
1061
  /**
981
- * (medium) — bounded-batch selection for the dropped-tools announce. An UNBOUNDED
982
- * render larger than the 8KB bundle cap gets prefix-CLIPPED by capAttachments; the intact-survival
983
- * drain predicate then never matches, the full array is retained, and every later boundary re-renders
984
- * and re-clips the identical prefix forever (tail entries never disclosed, permanent cap churn).
985
- * Selecting the longest prefix of COMPLETE entries whose render fits {@link MCP_DROPPED_BATCH_MAX_BYTES}
986
- * (always ≥1) restores monotonic drainage: the run loop splices exactly the delivered batch, the
987
- * tail rides later boundaries. Pure/deterministic so the drain predicate can re-derive the exact
988
- * snapshot. the ≥1 floor is SAFE only because entries are bounded at the mcp.ts
989
- * producer (tool ≤160 cps, reason ≤240 cps, both single-lined) — one rendered entry is always a
990
- * few hundred bytes, far under both this bound and the 8KB bundle cap, so the single-entry frame
991
- * can never be cap-clipped into a permanent re-render wedge.
1062
+ * Bounded-batch selection for the dropped-tools announce: the longest prefix of COMPLETE entries whose
1063
+ * render fits {@link MCP_DROPPED_BATCH_MAX_BYTES} (always ≥1), so an oversized list drains
1064
+ * monotonically across boundaries — the run loop splices exactly the delivered batch, the tail rides
1065
+ * later boundaries. Pure/deterministic so the drain predicate can re-derive the exact snapshot. The ≥1
1066
+ * floor is SAFE because entries are bounded at the mcp.ts producer (tool ≤160 cps, reason ≤240 cps,
1067
+ * both single-lined) — one rendered entry is always a few hundred bytes.
992
1068
  */
993
1069
  export declare function selectMcpDroppedBatch<T extends McpDroppedTool>(entries: ReadonlyArray<T>): T[];
994
1070
  /**
@@ -2,7 +2,8 @@ import { PRESENT_PLAN_TOOL_NAME } from "../present-plan-tool.js";
2
2
  import { delimitUntrustedWithClip, sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
3
3
  import { protocolOf } from "../protocol-table.js";
4
4
  import { renderSkillLinesWithin, renderSkillsBlock } from "./synthetic-tools.js";
5
- import { SKILL_LISTING_DESC_MAX_CHARS_DEFAULT, SKILLS_BLOCK_MAX_BYTES } from "../skill-listing-budget.js";
5
+ import { SKILL_LISTING_DEFAULT_BUDGET } from "../skill-listing-budget.js";
6
+ import { foldListingWithin, foldRemovalArmWithin, removalArmFloor } from "./listing-fold.js";
6
7
  import { TOOL_SEARCH_NAME as TOOL_SEARCH_TOOL_NAME } from "./tool-disclosure.js";
7
8
  export const TODO_REMINDER_CONFIG = {
8
9
  TURNS_SINCE_WRITE: 10,
@@ -24,6 +25,33 @@ export const CHANGED_FILES_MTIME_EPS_MS = 2000;
24
25
  export const ATTACHMENT_BYTE_CAP = 8 * 1024;
25
26
  const PROJECTION_ITEMS_MAX = 50;
26
27
  const PROJECTION_CONTENT_MAX = 80;
28
+ const LISTING_FRAME_FLOOR_BYTES = 768;
29
+ const DATE_CHANGE_MAX_BYTES = 160;
30
+ const WORKFLOW_SIZE_GUIDELINE_CHANGE_MAX_BYTES = 256;
31
+ export const ATTACHMENT_DELIVERY_CLASS = {
32
+ todo_reminder: { class: "clippable" },
33
+ task_reminder: { class: "clippable" },
34
+ tool_search_usage_reminder: { class: "clippable" },
35
+ changed_files: { class: "clippable" },
36
+ plan_mode: { class: "clippable" },
37
+ budget_usd: { class: "clippable" },
38
+ total_tokens_reminder: { class: "clippable" },
39
+ background_tasks: { class: "clippable" },
40
+ tools_delta: { class: "intact", bound: (inp) => toolsDeltaBound(inp) },
41
+ agent_listing: { class: "intact", bound: (inp) => listingFrameBound(inp.listingBudget) + (inp.agentModels !== undefined ? Buffer.byteLength(modelsAvailableLine(inp.agentModels), "utf8") : 0) },
42
+ skills_listing: { class: "intact", bound: (inp) => listingFrameBound(inp.listingBudget) },
43
+ mcp_instructions: { class: "intact", bound: (inp) => mcpInstructionsBound(inp) },
44
+ mcp_dropped_tools: { class: "intact", bound: (inp) => mcpDroppedToolsBound(inp) },
45
+ date_change: { class: "intact", bound: () => DATE_CHANGE_MAX_BYTES },
46
+ instructions_change: { class: "intact", bound: () => INSTRUCTIONS_CHANGE_BYTE_CAP },
47
+ workflow_size_guideline_change: { class: "intact", bound: () => WORKFLOW_SIZE_GUIDELINE_CHANGE_MAX_BYTES },
48
+ };
49
+ export function isIntactSource(source) {
50
+ return ATTACHMENT_DELIVERY_CLASS[source].class === "intact";
51
+ }
52
+ function listingFrameBound(budget) {
53
+ return Math.max((budget ?? SKILL_LISTING_DEFAULT_BUDGET).blockMaxBytes, LISTING_FRAME_FLOOR_BYTES);
54
+ }
27
55
  const ATTACHMENT_TAGS_DEFAULT = [...SHELLED_BODY_ENVELOPE_TAGS];
28
56
  const ATTACHMENT_TAGS_OWNED = {
29
57
  skills_listing: SHELLED_BODY_ENVELOPE_TAGS.filter((t) => t !== "skills"),
@@ -208,23 +236,15 @@ export function collectDueAttachments(state, inp) {
208
236
  (out ??= []).push({ source: "tools_delta", body });
209
237
  }
210
238
  const agentListingBody = inp.config.agentListing && inp.agentListing !== undefined
211
- ? renderAgentListingDelta(state, inp.agentListing, inp.agentToolName ?? "Agent", inp.agentModels)
239
+ ? renderAgentListingDelta(state, inp.agentListing, inp.agentToolName ?? "Agent", inp.agentModels, inp.listingBudget)
212
240
  : undefined;
241
+ if (agentListingBody !== undefined)
242
+ (out ??= []).push({ source: "agent_listing", body: agentListingBody });
213
243
  const skillsListingBody = inp.config.skillsListing && inp.skillsListing !== undefined
214
- ? renderSkillsListingDelta(state, inp.skillsListing, inp.skillsListingBudget)
244
+ ? renderSkillsListingDelta(state, inp.skillsListing, inp.listingBudget)
215
245
  : undefined;
216
- const listingPair = [];
217
- if (agentListingBody !== undefined)
218
- listingPair.push({ source: "agent_listing", body: agentListingBody });
219
- if (skillsListingBody !== undefined) {
220
- const att = { source: "skills_listing", body: skillsListingBody };
221
- if (state.skillsListingStarved === true)
222
- listingPair.unshift(att);
223
- else
224
- listingPair.push(att);
225
- }
226
- for (const att of listingPair)
227
- (out ??= []).push(att);
246
+ if (skillsListingBody !== undefined)
247
+ (out ??= []).push({ source: "skills_listing", body: skillsListingBody });
228
248
  if (inp.config.mcpInstructions && inp.mcpInstructions !== undefined) {
229
249
  const body = renderMcpInstructionsDelta(inp.mcpInstructions.added, inp.mcpInstructions.removed);
230
250
  if (body !== undefined)
@@ -244,9 +264,6 @@ export function collectDueAttachments(state, inp) {
244
264
  if (out === undefined)
245
265
  return EMPTY;
246
266
  const capped = capAttachments(out);
247
- if (skillsListingBody !== undefined) {
248
- state.skillsListingStarved = !capped.some((a) => a.source === "skills_listing" && a.body === skillsListingBody);
249
- }
250
267
  if (pendingSurfaced !== undefined && capped.some((a) => a.source === "changed_files")) {
251
268
  for (const c of pendingSurfaced)
252
269
  state.surfacedMtime.set(c.path, c.mtimeMs);
@@ -434,15 +451,19 @@ export function renderToolsDelta(input) {
434
451
  const added = input.added ?? [];
435
452
  if (added.length > 0) {
436
453
  const swapped = input.staticFace === true ? (input.swappedUnderStatic ?? []).filter((n) => added.includes(n)) : [];
437
- blocks.push((input.staticFace === true
438
- ? "The following deferred tools are now active — call them directly. The tools list keeps compact " +
439
- 'placeholder entries, so re-run ToolSearch ("select:<name>") if you need a parameter schema:\n'
440
- : "The following deferred tools are now available. Their full schemas are loaded — call them " +
441
- "directly like any other tool:\n") +
442
- added.map((n) => `- ${n}`).join("\n") +
443
- (swapped.length > 0
444
- ? `\nException: ${swapped.join(", ")} — too large to inline, so the full declaration is in the tools list instead.`
445
- : ""));
454
+ const grouped = added.length > TOOLS_DELTA_LIST_MAX;
455
+ const exception = swapped.length > 0
456
+ ? grouped
457
+ ? `\nException: ${swapped.length} of them were too large to inline, so their full declarations are in the tools list instead.`
458
+ : `\nException: ${swapped.join(", ")} — too large to inline, so the full declaration is in the tools list instead.`
459
+ : "";
460
+ blocks.push((grouped ? `${added.length} deferred tools are now ${input.staticFace === true ? "active" : "available"}: ${groupByMcpServer(added)}. ` : "") +
461
+ (input.staticFace === true
462
+ ? (grouped ? "Call them directly. " : "The following deferred tools are now active — call them directly. ") +
463
+ `The tools list keeps compact placeholder entries, so re-run ToolSearch ("select:<name>") if you need a parameter schema${grouped ? "." : ":\n"}`
464
+ : (grouped ? "" : "The following deferred tools are now available. ") + `Their full schemas are loaded — call them directly like any other tool${grouped ? "." : ":\n"}`) +
465
+ (grouped ? "" : added.map((n) => `- ${n}`).join("\n")) +
466
+ exception);
446
467
  }
447
468
  const readded = input.readded ?? [];
448
469
  if (readded.length > 0) {
@@ -530,22 +551,35 @@ function listingIdentity(name) {
530
551
  export function agentListingDeltaHeader(toolName) {
531
552
  return `New agent types are now available for the ${sanitizeUntrustedText(toolName, SHELLED_BODY_ENVELOPE_TAGS)} tool:`;
532
553
  }
533
- export function renderAgentListingDelta(state, entries, toolName, models) {
554
+ export function renderAgentListingFrame(state, entries, toolName, models, budget = SKILL_LISTING_DEFAULT_BUDGET) {
534
555
  const safe = (t) => sanitizeUntrustedText(t, SHELLED_BODY_ENVELOPE_TAGS);
535
556
  const line = (e) => {
536
557
  const base = e.description ? `- ${safe(e.name)}: ${safe(e.description)}` : `- ${safe(e.name)}`;
537
558
  return `${base} (Tools: ${safe(e.tools ?? AGENT_TOOLS_NOTE_DEFAULT)})`;
538
559
  };
560
+ const bytes = (s) => Buffer.byteLength(s, "utf8");
561
+ const joiner = bytes(AGENT_DELTA_BLOCK_JOINER);
562
+ const fold = (items, header, maxBytes) => foldListingWithin(items, {
563
+ blockMaxBytes: maxBytes,
564
+ full: line,
565
+ nameOnly: (e) => `- ${safe(e.name)}`,
566
+ nameOf: (e) => e.name,
567
+ capMarker: AGENT_LISTING_CAP_MARKER,
568
+ overflowLine: (n) => `(… +${n} more agent types not listed)`,
569
+ frame: (lines) => [header, ...lines].join("\n"),
570
+ });
539
571
  const announced = state.announcedAgentTypes;
540
572
  if (announced === undefined) {
541
573
  if (entries.length === 0)
542
574
  return undefined;
543
- const blocks = [`${agentListingInitialHeader(toolName)}\n${entries.map(line).join("\n")}`];
575
+ const tail = [];
544
576
  if (entries.length > 1)
545
- blocks.push(AGENT_CONCURRENCY_NOTE);
577
+ tail.push(AGENT_CONCURRENCY_NOTE);
546
578
  if (models !== undefined && models.length > 0)
547
- blocks.push(modelsAvailableLine(models));
548
- return blocks.join("\n\n");
579
+ tail.push(modelsAvailableLine(models));
580
+ const tailCost = tail.reduce((acc, t) => acc + joiner + bytes(t), 0);
581
+ const roster = fold(entries, agentListingInitialHeader(toolName), budget.blockMaxBytes - tailCost);
582
+ return { text: [roster.text, ...tail].join(AGENT_DELTA_BLOCK_JOINER), nameOnly: roster.nameOnly, dropped: roster.dropped, listed: entries.length };
549
583
  }
550
584
  const announcedIds = new Set([...announced.keys()].map(listingIdentity));
551
585
  const entryIds = new Set(entries.map((e) => listingIdentity(e.name)));
@@ -557,17 +591,26 @@ export function renderAgentListingDelta(state, entries, toolName, models) {
557
591
  state.announcedModels.some((m, i) => listingIdentity(m) !== listingIdentity(models[i])));
558
592
  if (added.length === 0 && removed.length === 0 && !modelsDrifted)
559
593
  return undefined;
560
- const blocks = [];
561
- if (added.length > 0)
562
- blocks.push(`${agentListingDeltaHeader(toolName)}\n${added.map(line).join("\n")}`);
594
+ const modelsLine = modelsDrifted ? modelsAvailableLine(models) : undefined;
595
+ const modelsCost = modelsLine !== undefined ? joiner + bytes(modelsLine) : 0;
596
+ const ambientCost = removed.length > 0 ? joiner + bytes(AMBIENT_CONTEXT_NOTE) : 0;
597
+ const removalReserve = removed.length > 0 ? joiner + bytes(removalArmFloor(AGENT_LISTING_REMOVED_HEADER, removed.length)) : 0;
598
+ const addArm = added.length > 0 ? fold(added, agentListingDeltaHeader(toolName), budget.blockMaxBytes - modelsCost - ambientCost - removalReserve) : undefined;
599
+ const addCost = addArm === undefined ? 0 : bytes(addArm.text) + joiner;
600
+ const blocks = addArm === undefined ? [] : [addArm.text];
563
601
  if (removed.length > 0) {
564
- blocks.push(`${AGENT_LISTING_REMOVED_HEADER}\n${removed.map((n) => `- ${safe(n)}`).join("\n")}`);
602
+ blocks.push(foldRemovalArmWithin(removed, AGENT_LISTING_REMOVED_HEADER, budget.blockMaxBytes - modelsCost - ambientCost - addCost));
565
603
  blocks.push(AMBIENT_CONTEXT_NOTE);
566
604
  }
567
- if (modelsDrifted)
568
- blocks.push(modelsAvailableLine(models));
569
- return blocks.join("\n\n");
605
+ if (modelsLine !== undefined)
606
+ blocks.push(modelsLine);
607
+ return { text: blocks.join(AGENT_DELTA_BLOCK_JOINER), nameOnly: addArm?.nameOnly ?? [], dropped: addArm?.dropped ?? [], listed: added.length };
608
+ }
609
+ export function renderAgentListingDelta(state, entries, toolName, models, budget) {
610
+ return renderAgentListingFrame(state, entries, toolName, models, budget)?.text;
570
611
  }
612
+ const AGENT_LISTING_CAP_MARKER = "(listing budget reached — lower-priority agent types are listed name-only)";
613
+ const AGENT_DELTA_BLOCK_JOINER = "\n\n";
571
614
  export const MODELS_AVAILABLE_PREFIX = "Models available for the 'model' parameter: ";
572
615
  function modelsAvailableLine(models) {
573
616
  const named = models.map((m) => sanitizeUntrustedText(m, SHELLED_BODY_ENVELOPE_TAGS));
@@ -597,28 +640,7 @@ export function commitAgentListing(state, entries, models) {
597
640
  export const SKILLS_LISTING_DELTA_HEADER = "New skills are now available:";
598
641
  export const SKILLS_LISTING_REMOVED_HEADER = "The following skills are no longer available:";
599
642
  const SKILLS_DELTA_BLOCK_JOINER = "\n\n";
600
- function skillsRemovalArmFloor(count) {
601
- return [SKILLS_LISTING_REMOVED_HEADER, `(… +${count} more are no longer available)`].join("\n");
602
- }
603
- function renderSkillsRemovalArm(removed, maxBytes) {
604
- const bytes = (s) => Buffer.byteLength(s, "utf8");
605
- const lines = removed.map((n) => `- ${sanitizeUntrustedText(n, SHELLED_BODY_ENVELOPE_TAGS)}`);
606
- const head = bytes(SKILLS_LISTING_REMOVED_HEADER);
607
- const lineCost = (l) => bytes(l) + 1;
608
- const prefix = new Array(lines.length + 1);
609
- prefix[0] = 0;
610
- for (let i = 0; i < lines.length; i++)
611
- prefix[i + 1] = prefix[i] + lineCost(lines[i]);
612
- if (head + prefix[lines.length] <= maxBytes)
613
- return [SKILLS_LISTING_REMOVED_HEADER, ...lines].join("\n");
614
- for (let m = lines.length - 1; m >= 0; m--) {
615
- const tail = `(… +${lines.length - m} more are no longer available)`;
616
- if (head + prefix[m] + lineCost(tail) <= maxBytes)
617
- return [SKILLS_LISTING_REMOVED_HEADER, ...lines.slice(0, m), tail].join("\n");
618
- }
619
- return skillsRemovalArmFloor(lines.length);
620
- }
621
- export function renderSkillsListingFrame(state, entries, budget) {
643
+ export function renderSkillsListingFrame(state, entries, budget = SKILL_LISTING_DEFAULT_BUDGET) {
622
644
  const announced = state.announcedSkills;
623
645
  if (announced === undefined) {
624
646
  if (entries.length === 0)
@@ -634,20 +656,20 @@ export function renderSkillsListingFrame(state, entries, budget) {
634
656
  return undefined;
635
657
  const bytes = (s) => Buffer.byteLength(s, "utf8");
636
658
  const joiner = bytes(SKILLS_DELTA_BLOCK_JOINER);
637
- const frameMaxBytes = Math.min(budget?.blockMaxBytes ?? SKILLS_BLOCK_MAX_BYTES, SKILLS_BLOCK_MAX_BYTES);
659
+ const frameMaxBytes = budget.blockMaxBytes;
638
660
  const ambientCost = removed.length > 0 ? joiner + bytes(AMBIENT_CONTEXT_NOTE) : 0;
639
- const removalReserve = removed.length > 0 ? joiner + bytes(skillsRemovalArmFloor(removed.length)) : 0;
661
+ const removalReserve = removed.length > 0 ? joiner + bytes(removalArmFloor(SKILLS_LISTING_REMOVED_HEADER, removed.length)) : 0;
640
662
  const addArm = added.length > 0
641
663
  ? renderSkillLinesWithin(added, {
642
664
  blockMaxBytes: frameMaxBytes - ambientCost - removalReserve,
643
- descMaxChars: budget?.descMaxChars ?? SKILL_LISTING_DESC_MAX_CHARS_DEFAULT,
665
+ descMaxChars: budget.descMaxChars,
644
666
  frame: (lines) => [SKILLS_LISTING_DELTA_HEADER, ...lines].join("\n"),
645
667
  })
646
668
  : undefined;
647
669
  const addCost = addArm === undefined ? 0 : bytes(addArm.text) + (removed.length > 0 ? joiner : 0);
648
670
  const blocks = addArm === undefined ? [] : [addArm.text];
649
671
  if (removed.length > 0) {
650
- blocks.push(renderSkillsRemovalArm(removed, frameMaxBytes - ambientCost - addCost), AMBIENT_CONTEXT_NOTE);
672
+ blocks.push(foldRemovalArmWithin(removed, SKILLS_LISTING_REMOVED_HEADER, frameMaxBytes - ambientCost - addCost), AMBIENT_CONTEXT_NOTE);
651
673
  }
652
674
  return {
653
675
  text: blocks.join(SKILLS_DELTA_BLOCK_JOINER),
@@ -703,6 +725,37 @@ export function selectMcpDroppedBatch(entries) {
703
725
  }
704
726
  return out;
705
727
  }
728
+ const TOOLS_DELTA_FIXED_MAX_BYTES = 2048;
729
+ function toolsDeltaBound(inp) {
730
+ const bytes = (s) => Buffer.byteLength(s, "utf8");
731
+ const list = (names) => (names.length > TOOLS_DELTA_LIST_MAX ? bytes(groupByMcpServer(names)) : names.reduce((acc, n) => acc + bytes(n) + 3, 0));
732
+ const swapped = (names) => names.reduce((acc, n) => acc + bytes(n) + 2, 0);
733
+ const mcp = inp.mcpToolsDelta;
734
+ return (TOOLS_DELTA_FIXED_MAX_BYTES +
735
+ list(inp.newTools ?? []) +
736
+ swapped(inp.newToolsSwappedUnderStatic ?? []) +
737
+ bytes(groupByMcpServer(mcp?.readded ?? [])) +
738
+ swapped(inp.readdedToolsSwappedUnderStatic ?? []) +
739
+ list(mcp?.removed ?? []) +
740
+ (mcp?.failedServers ?? []).slice(0, TOOLS_DELTA_LIST_MAX).reduce((acc, f) => acc + bytes(f.name) + bytes(f.error ?? "") + 8, 0));
741
+ }
742
+ const MCP_INSTRUCTIONS_FENCE_MAX_BYTES = 512;
743
+ function mcpInstructionsBound(inp) {
744
+ const bytes = (s) => Buffer.byteLength(s, "utf8");
745
+ const m = inp.mcpInstructions;
746
+ if (m === undefined)
747
+ return 0;
748
+ return (512 +
749
+ m.added.reduce((acc, a) => acc + MCP_INSTRUCTIONS_MAX_CHARS * 4 + bytes(a.server) + MCP_INSTRUCTIONS_FENCE_MAX_BYTES, 0) +
750
+ m.removed.reduce((acc, n) => acc + bytes(n) + 1, 0));
751
+ }
752
+ const MCP_DROPPED_FIXED_MAX_BYTES = 1024;
753
+ function mcpDroppedToolsBound(inp) {
754
+ const bytes = (s) => Buffer.byteLength(s, "utf8");
755
+ const first = inp.mcpDroppedTools?.[0];
756
+ const single = first === undefined ? 0 : MCP_DROPPED_FIXED_MAX_BYTES + bytes(first.tool) + bytes(first.server) + bytes(first.reason) + 32;
757
+ return Math.max(MCP_DROPPED_BATCH_MAX_BYTES, single);
758
+ }
706
759
  const PLAN_MODE_FULL_BODY = `Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supercedes any other instructions you have received. Instead, you should: research and analyze with read-only tools, and when you have formed a concrete plan, present it with the ${PRESENT_PLAN_TOOL_NAME} tool for approval. That tool inherently requests plan approval — never ask for approval via plain text.`;
707
760
  const PLAN_MODE_SPARSE_BODY = `Plan mode still active (see full instructions earlier in conversation). Read-only: no edits, commits, or config changes. When your plan is ready, present it with the ${PRESENT_PLAN_TOOL_NAME} tool for approval. Never ask about plan approval via plain text.`;
708
761
  const TRUNCATION_MARKER = "\n…[truncated]";
@@ -710,12 +763,17 @@ const MIN_KEEP_BYTES = 160;
710
763
  function capAttachments(atts) {
711
764
  let total = 0;
712
765
  for (const a of atts)
713
- total += Buffer.byteLength(a.body, "utf8");
766
+ if (!isIntactSource(a.source))
767
+ total += Buffer.byteLength(a.body, "utf8");
714
768
  if (total <= ATTACHMENT_BYTE_CAP)
715
769
  return atts;
716
770
  let budget = ATTACHMENT_BYTE_CAP;
717
771
  const out = [];
718
772
  for (const a of atts) {
773
+ if (isIntactSource(a.source)) {
774
+ out.push(a);
775
+ continue;
776
+ }
719
777
  const len = Buffer.byteLength(a.body, "utf8");
720
778
  if (len <= budget) {
721
779
  out.push(a);
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The leg's write-protection MOUNT — one compile, and the loud outlet for the reading it could not get.
3
+ *
4
+ * The always-on write-protection judge re-judges a table-missing spelling on its RESOLVED real target,
5
+ * which needs a filesystem to resolve in. A deployment that wires no execution environment (a tool-only
6
+ * or MCP-only host) hands it none, so the spelling becomes the whole judgment and a covered write that
7
+ * reaches a protected row only through an alias is cleared. That verdict is the judge's documented
8
+ * absence law and is deliberately unchanged here: the announcement dialect applies (announce, do not
9
+ * refuse) — refusing every covered write instead would break exactly the deployments this shape exists
10
+ * for, and the reference implementation falls back to the same spelling-only set when resolution is
11
+ * unavailable. What this seat removes is the SILENCE.
12
+ *
13
+ * Two readers, one compile: the gate station mounts `check`, and the leg's wiring manifest reports
14
+ * `targetView`. Compiling twice would be two judges free to disagree about the seats they were handed.
15
+ */
16
+ import type { ExecutionEnv } from "../../internal/harness.js";
17
+ import type { RunnerDeps } from "../runner-deps.js";
18
+ import type { ToolRoster } from "../tool-roster.js";
19
+ import { type WriteProtectionMount } from "../write-protect.js";
20
+ /**
21
+ * Compile the leg's write-protection judge and announce a missing target view.
22
+ *
23
+ * Returns the mount, or `undefined` when the deployment emptied the table (`[]`): no judge, no tighten
24
+ * mounted at all, and nothing announced — an explicit opt-out is not a degrade. A garbage table refuses
25
+ * prepare loudly from here, as it did from the gate station before, so an unusable table cannot lie
26
+ * dormant on a write-tool-less task.
27
+ *
28
+ * The notice fires exactly once per prepared leg and only when a judge with NO target view sits beside at
29
+ * least one tool whose writes it covers. Coverage runs the judge's OWN per-call test ({@link
30
+ * protectivePathTargetOf}) rather than a copy of it, over every name a row DISPATCHES under — its own and
31
+ * its aliases, because the gate hands the judge the name the call was made with. A row that declares a
32
+ * path target is covered by that declaration under every name; a row that declares none is covered under
33
+ * whichever of its names the catalog gives a non-read slot to. Both halves are load-bearing: a tool named
34
+ * `Write` that declares nothing, and a tool aliased to `Write` that declares nothing, are each judged by
35
+ * spelling alone on a hands-less leg, and each would otherwise go unnamed by the very line that exists to
36
+ * name them. Reported by the ROW's own name — what the deployment mounted.
37
+ *
38
+ * SNAPSHOT BOUNDARY, deliberate and disclosed: the roster read here is the LEG-START one, the same one
39
+ * the wiring manifest reports and for the same reason — this is one announcement per prepared leg, not a
40
+ * subscription. A run that gains its FIRST covered write tool after prepare (a mid-run roster delta) is
41
+ * judged by the same spelling-only judge and is not announced; the leg's `writeProtection.targetView`
42
+ * still reports the judge's reading correctly, because the judge never gains an environment mid-run —
43
+ * only the covered SET can widen.
44
+ */
45
+ export declare function mountWriteProtection(input: {
46
+ /** borrowed-readonly — the deployment seats: the table (`writeProtectedPaths`; absent ⇒ the default
47
+ * table, `[]` ⇒ opted out), the data root the memory library is materialized under
48
+ * (`memoryEngineDir`; absent ⇒ the resolver's env/default chain) and the operator sink. */
49
+ deps: Pick<RunnerDeps, "writeProtectedPaths" | "memoryEngineDir" | "onNotice">;
50
+ /** The HANDS' environment, or `undefined` with no hands — the driver's own predicate, not a second
51
+ * reading of the env seats, so the judge sees exactly the environment the write tools do. */
52
+ env: ExecutionEnv | undefined;
53
+ /** The base a `base: "root"` write tool's relative spelling resolves against. */
54
+ rootPath: string;
55
+ /** The leg's minted roster — what actually MOUNTED. */
56
+ roster: ToolRoster;
57
+ sessionId: string;
58
+ runId: string;
59
+ }): WriteProtectionMount | undefined;