@sema-agent/core 7.18.0 → 7.20.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 (137) hide show
  1. package/CHANGELOG.md +276 -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 +8 -3
  6. package/dist/brain/open-responses.js +12 -4
  7. package/dist/brain/openai.js +7 -2
  8. package/dist/brain/reasoning.d.ts +13 -13
  9. package/dist/brain/reasoning.js +2 -2
  10. package/dist/brain/stream-engine.d.ts +4 -0
  11. package/dist/brain/stream-engine.js +6 -1
  12. package/dist/brain/stream-shared.d.ts +3 -0
  13. package/dist/brain/stream-shared.js +1 -0
  14. package/dist/core/ask-origin.d.ts +16 -0
  15. package/dist/core/ask-origin.js +8 -0
  16. package/dist/core/auto-mode-arming.d.ts +6 -0
  17. package/dist/core/auto-mode-arming.js +5 -5
  18. package/dist/core/auto-mode-defaults.d.ts +43 -15
  19. package/dist/core/auto-mode-defaults.js +4 -1
  20. package/dist/core/auto-mode.d.ts +74 -26
  21. package/dist/core/auto-mode.js +80 -11
  22. package/dist/core/checkpoint-store.d.ts +7 -0
  23. package/dist/core/checkpoint-store.js +2 -2
  24. package/dist/core/child-clamp-seats.d.ts +79 -0
  25. package/dist/core/child-clamp-seats.js +29 -0
  26. package/dist/core/engine-notice.d.ts +89 -41
  27. package/dist/core/fs-write-gate-policy.d.ts +17 -9
  28. package/dist/core/fs-write-gate-policy.js +51 -4
  29. package/dist/core/gate-bounded-await.d.ts +38 -0
  30. package/dist/core/gate-bounded-await.js +53 -0
  31. package/dist/core/gate-fold.js +20 -48
  32. package/dist/core/gate-lanes.js +17 -9
  33. package/dist/core/gate-outcome.d.ts +34 -6
  34. package/dist/core/gate-outcome.js +7 -0
  35. package/dist/core/governance-codes.d.ts +16 -1
  36. package/dist/core/governance-codes.js +18 -0
  37. package/dist/core/hooks.d.ts +8 -8
  38. package/dist/core/memory-engine/engine.d.ts +14 -9
  39. package/dist/core/memory-engine/engine.js +21 -8
  40. package/dist/core/memory-engine/layout.d.ts +31 -0
  41. package/dist/core/memory-engine/layout.js +68 -3
  42. package/dist/core/memory-engine/types.d.ts +24 -0
  43. package/dist/core/memory.d.ts +19 -0
  44. package/dist/core/memory.js +12 -0
  45. package/dist/core/read-root-candidate.d.ts +12 -0
  46. package/dist/core/read-root-candidate.js +24 -0
  47. package/dist/core/remote-env.d.ts +32 -0
  48. package/dist/core/remote-env.js +4 -1
  49. package/dist/core/roles.d.ts +9 -1
  50. package/dist/core/roles.js +4 -0
  51. package/dist/core/runner/advertised-writable-dirs.d.ts +59 -1
  52. package/dist/core/runner/advertised-writable-dirs.js +51 -3
  53. package/dist/core/runner/announce-once-ledger.d.ts +15 -0
  54. package/dist/core/runner/announce-once-ledger.js +8 -0
  55. package/dist/core/runner/classifier-ladder.d.ts +154 -11
  56. package/dist/core/runner/classifier-ladder.js +170 -10
  57. package/dist/core/runner/contracts.d.ts +46 -1
  58. package/dist/core/runner/denial-limit-arms.d.ts +14 -1
  59. package/dist/core/runner/denial-limit-arms.js +7 -6
  60. package/dist/core/runner/env-capability-census.d.ts +30 -0
  61. package/dist/core/runner/env-capability-census.js +38 -0
  62. package/dist/core/runner/forward-sink.d.ts +32 -0
  63. package/dist/core/runner/forward-sink.js +41 -0
  64. package/dist/core/runner/gate-exit.d.ts +4 -0
  65. package/dist/core/runner/gate-exit.js +4 -1
  66. package/dist/core/runner/git-status-frame.d.ts +0 -9
  67. package/dist/core/runner/listing-fold.d.ts +57 -0
  68. package/dist/core/runner/listing-fold.js +75 -0
  69. package/dist/core/runner/prepare-ask-lane.js +1 -0
  70. package/dist/core/runner/prepare-caps-and-workflow.js +20 -31
  71. package/dist/core/runner/prepare-config-doors.d.ts +6 -0
  72. package/dist/core/runner/prepare-config-doors.js +8 -10
  73. package/dist/core/runner/prepare-execution-env.d.ts +1 -1
  74. package/dist/core/runner/prepare-execution-env.js +2 -0
  75. package/dist/core/runner/prepare-gate-stations.d.ts +8 -0
  76. package/dist/core/runner/prepare-gate-stations.js +6 -2
  77. package/dist/core/runner/prepare-hands-readface.d.ts +4 -0
  78. package/dist/core/runner/prepare-hands-readface.js +3 -5
  79. package/dist/core/runner/prepare-listings.d.ts +3 -0
  80. package/dist/core/runner/prepare-listings.js +4 -2
  81. package/dist/core/runner/prepare-memory.js +7 -1
  82. package/dist/core/runner/prepare-policy-chain.js +5 -5
  83. package/dist/core/runner/prepare-project-context.d.ts +5 -1
  84. package/dist/core/runner/prepare-project-context.js +3 -2
  85. package/dist/core/runner/prepare-prompt-inputs.d.ts +2 -2
  86. package/dist/core/runner/prepare-run-refs.d.ts +4 -2
  87. package/dist/core/runner/prepare-run-refs.js +5 -4
  88. package/dist/core/runner/prepare-task.js +8 -6
  89. package/dist/core/runner/resume-apply.js +3 -3
  90. package/dist/core/runner/run-clock-and-content.js +3 -7
  91. package/dist/core/runner/run-harness-handlers.js +10 -10
  92. package/dist/core/runner/run-leg.js +31 -16
  93. package/dist/core/runner/run-settle-and-teardown.js +2 -5
  94. package/dist/core/runner/run-turn-boundary.js +44 -25
  95. package/dist/core/runner/runtask.js +2 -1
  96. package/dist/core/runner/synthetic-tools.d.ts +41 -29
  97. package/dist/core/runner/synthetic-tools.js +25 -40
  98. package/dist/core/runner/task-root-canonical.d.ts +45 -0
  99. package/dist/core/runner/task-root-canonical.js +18 -0
  100. package/dist/core/runner/turn-attachments.d.ts +160 -53
  101. package/dist/core/runner/turn-attachments.js +153 -51
  102. package/dist/core/safe-notify.d.ts +5 -0
  103. package/dist/core/safe-notify.js +24 -2
  104. package/dist/core/sensitive-path-policy.d.ts +84 -17
  105. package/dist/core/sensitive-path-policy.js +37 -19
  106. package/dist/core/skill-listing-budget.d.ts +106 -0
  107. package/dist/core/skill-listing-budget.js +57 -0
  108. package/dist/core/skills-directory.js +17 -10
  109. package/dist/core/task-registry.js +2 -1
  110. package/dist/core/task-spec.d.ts +29 -2
  111. package/dist/core/tool-policy.d.ts +31 -5
  112. package/dist/core/tool-policy.js +45 -32
  113. package/dist/core/tool-spec.d.ts +37 -4
  114. package/dist/core/types.d.ts +1 -1
  115. package/dist/core/write-protect.d.ts +105 -20
  116. package/dist/core/write-protect.js +89 -32
  117. package/dist/index.d.ts +8 -7
  118. package/dist/index.js +6 -5
  119. package/dist/orchestration/workflow-primitives.js +10 -20
  120. package/dist/stores/file/memory-store.d.ts +5 -2
  121. package/dist/stores/file/memory-store.js +8 -4
  122. package/dist/tools/fs/bash-readonly-classifier.d.ts +40 -2
  123. package/dist/tools/fs/bash-readonly-classifier.js +376 -41
  124. package/dist/tools/fs/fs-bash.d.ts +1 -1
  125. package/dist/tools/fs/fs-bash.js +19 -28
  126. package/dist/tools/fs/fs-shared.d.ts +22 -0
  127. package/dist/tools/fs/fs-shared.js +30 -0
  128. package/dist/tools/fs/index.d.ts +1 -1
  129. package/dist/tools/fs/index.js +1 -1
  130. package/dist/tools/fs/read-deny.d.ts +4 -3
  131. package/dist/tools/fs/safety.d.ts +15 -0
  132. package/dist/tools/fs/safety.js +7 -0
  133. package/package.json +2 -1
  134. package/test/export-surface.snapshot.json +31 -5
  135. package/wire-changes/core@7.18.0.json +1273 -0
  136. package/wire-changes/core@7.19.0.json +457 -0
  137. package/wire-changes/core@7.20.0.json +568 -0
@@ -1,8 +1,10 @@
1
1
  import { Type } from "typebox";
2
2
  import { defineTool } from "../tools.js";
3
3
  import { sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
4
+ import { SKILL_LISTING_DEFAULT_BUDGET, SKILL_LISTING_DESC_MAX_CHARS_DEFAULT } from "../skill-listing-budget.js";
4
5
  import { toolFace } from "../tool-catalog-entries.js";
5
6
  import { substituteSkillRoot, substituteSkillRootBounded } from "./skill-base-dir.js";
7
+ import { foldListingWithin } from "./listing-fold.js";
6
8
  export const OUTPUT_TOOL_NAME = "StructuredOutput";
7
9
  export const REPORT_BLOCKED_TOOL_NAME = "ReportBlocked";
8
10
  export const REPORT_FINDINGS_TOOL_NAME = "ReportFindings";
@@ -85,8 +87,6 @@ export function createReportFindingsTool() {
85
87
  }
86
88
  export const SKILL_TOOL_NAME = "Skill";
87
89
  export const SKILL_CONTENT_MAX_CHARS = 1024 * 1024;
88
- const SKILL_DESC_MAX_CHARS = 1_536;
89
- export const SKILLS_BLOCK_MAX_BYTES = 8_000;
90
90
  export const SKILL_ATTACHMENTS_MAX_CHARS = 50_000;
91
91
  export const SKILL_ATTACHMENT_PATH_MAX_CHARS = 512;
92
92
  const SKILL_FILES_LIST_MAX_CHARS = 200;
@@ -121,18 +121,18 @@ export function normalizeSkills(skills) {
121
121
  return out.toSorted((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
122
122
  }
123
123
  export const SKILLS_LISTING_PROBE_HEADER = "The following skills are available for this task.";
124
- export function skillListingLine(s) {
125
- const desc = s.description.length > SKILL_DESC_MAX_CHARS
126
- ? `${s.description.slice(0, SKILL_DESC_MAX_CHARS - 1)}…`
127
- : s.description;
124
+ export function skillListingLine(s, descMaxChars = SKILL_LISTING_DESC_MAX_CHARS_DEFAULT) {
125
+ const desc = s.description.length > descMaxChars ? `${s.description.slice(0, descMaxChars - 1)}…` : s.description;
128
126
  const safe = (t) => sanitizeUntrustedText(t, SHELLED_BODY_ENVELOPE_TAGS);
129
127
  return `- ${safe(s.name)}: ${safe(desc)}${safe(skillFilesSuffix(s.files))}`;
130
128
  }
131
129
  const SKILLS_BLOCK_CAP_MARKER = "(block size cap reached — lower-priority skills are listed name-only; descriptions via the tool)";
132
- export function buildSkillsBlock(skills) {
130
+ export function buildSkillsBlock(skills, budget) {
131
+ return renderSkillsBlock(skills, budget)?.text;
132
+ }
133
+ export function renderSkillsBlock(skills, budget = SKILL_LISTING_DEFAULT_BUDGET) {
133
134
  if (skills.length === 0)
134
135
  return undefined;
135
- const bytes = (s) => Buffer.byteLength(s, "utf8");
136
136
  const headerLines = [
137
137
  "<skills>",
138
138
  `${SKILLS_LISTING_PROBE_HEADER} A skill is a named package of instructions;`,
@@ -140,38 +140,23 @@ export function buildSkillsBlock(skills) {
140
140
  `instructions by calling the \`${SKILL_TOOL_NAME}\` tool with \`skill: "<name>"\` (plus optional \`args\`).`,
141
141
  "",
142
142
  ];
143
- const render = (lines) => [...headerLines, ...lines, "</skills>"].join("\n");
144
- const lineCost = (l) => bytes(l) + 1;
145
- const budget = SKILLS_BLOCK_MAX_BYTES - bytes(render([]));
146
- const fullLines = skills.map((s) => skillListingLine(s));
147
- const nameLines = skills.map((s) => `- ${sanitizeUntrustedText(s.name, SHELLED_BODY_ENVELOPE_TAGS)}`);
148
- const fullTotal = fullLines.reduce((acc, l) => acc + lineCost(l), 0);
149
- if (fullTotal <= budget)
150
- return render(fullLines);
151
- const markerCost = lineCost(SKILLS_BLOCK_CAP_MARKER);
152
- const floorCost = markerCost + nameLines.reduce((acc, l) => acc + lineCost(l), 0);
153
- if (floorCost <= budget) {
154
- let slack = budget - floorCost;
155
- const byPriority = skills
156
- .map((s, i) => ({ i, rank: s.declaredRank ?? i }))
157
- .toSorted((a, b) => a.rank - b.rank || a.i - b.i);
158
- const withDescription = new Set();
159
- for (const { i } of byPriority) {
160
- const delta = lineCost(fullLines[i]) - lineCost(nameLines[i]);
161
- if (delta <= slack) {
162
- withDescription.add(i);
163
- slack -= delta;
164
- }
165
- }
166
- return render([SKILLS_BLOCK_CAP_MARKER, ...skills.map((_s, i) => (withDescription.has(i) ? fullLines[i] : nameLines[i]))]);
167
- }
168
- for (let m = skills.length - 1; m >= 0; m--) {
169
- const overflowLine = `(… +${skills.length - m} more skills not listed — the ${SKILL_TOOL_NAME} tool serves them all)`;
170
- const cost = markerCost + nameLines.slice(0, m).reduce((acc, l) => acc + lineCost(l), 0) + lineCost(overflowLine);
171
- if (cost <= budget)
172
- return render([SKILLS_BLOCK_CAP_MARKER, ...nameLines.slice(0, m), overflowLine]);
173
- }
174
- return render([SKILLS_BLOCK_CAP_MARKER, `(… +${skills.length} more skills not listed — the ${SKILL_TOOL_NAME} tool serves them all)`]);
143
+ return renderSkillLinesWithin(skills, {
144
+ blockMaxBytes: budget.blockMaxBytes,
145
+ descMaxChars: budget.descMaxChars,
146
+ frame: (lines) => [...headerLines, ...lines, "</skills>"].join("\n"),
147
+ });
148
+ }
149
+ export function renderSkillLinesWithin(skills, opts) {
150
+ return foldListingWithin(skills, {
151
+ blockMaxBytes: opts.blockMaxBytes,
152
+ full: (s) => skillListingLine(s, opts.descMaxChars),
153
+ nameOnly: (s) => `- ${sanitizeUntrustedText(s.name, SHELLED_BODY_ENVELOPE_TAGS)}`,
154
+ nameOf: (s) => s.name,
155
+ rank: (s, i) => s.declaredRank ?? i,
156
+ capMarker: SKILLS_BLOCK_CAP_MARKER,
157
+ overflowLine: (n) => `(… +${n} more skills not listed — the ${SKILL_TOOL_NAME} tool serves them all)`,
158
+ frame: opts.frame,
159
+ });
175
160
  }
176
161
  export function createSkillTool(skills, scope) {
177
162
  const byName = new Map(skills.map((s) => [s.name, s]));
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The task root's CANONICAL spelling — and the one loud outlet for a host that hands in another one.
3
+ *
4
+ * Containment is judged on the canonical form: the roots fence resolves every target through the same
5
+ * `ExecutionEnv` and compares canonical keys, the shell read boundary compares a command's words
6
+ * against the declared roots, and every refusal message quotes them. A deployment that hands the
7
+ * runner a root which is NOT its own realpath (a symlinked temp directory; `/var` → `/private/var` on
8
+ * macOS) therefore reasons in one spelling while the engine judges in another, and a target the
9
+ * deployment considers inside the root can be judged outside it. Before this seat that difference was
10
+ * SILENT — the engine simply canonicalized and carried on.
11
+ *
12
+ * The bad-value discipline's announcement dialect applies (announce, do not refuse): an existing
13
+ * deployment may well be running correctly on the non-canonical spelling, so the judgment is unchanged
14
+ * and only the difference is disclosed. Nothing here repairs the host's spelling either — the engine
15
+ * cannot know which of the two the deployment's own gates key on, and rewriting one into the other
16
+ * would move a fence nobody asked to move.
17
+ */
18
+ import type { ExecutionEnv } from "../../internal/harness.js";
19
+ import type { RunnerDeps } from "../runner-deps.js";
20
+ /** The canonical root plus whether the difference was announced (the pin's observable). */
21
+ export interface TaskRootCanonical {
22
+ /** The spelling every fence judges with — the env's canonical form, or the given spelling when the
23
+ * env cannot resolve it (an unresolvable root narrows nothing here; the fence's own skip speaks). */
24
+ canonical: string;
25
+ /** True exactly when the given spelling differed and the notice was delivered. */
26
+ announced: boolean;
27
+ }
28
+ /**
29
+ * Resolve the run's task root to the spelling its fences judge with, announcing a host spelling that is
30
+ * not that spelling.
31
+ *
32
+ * @contract task_root.canonical_is_the_judge — the engine judges containment on the task root's
33
+ * CANONICAL spelling; when the root the host handed in is not its own canonical form the run announces
34
+ * `config.task_root_not_canonical` exactly once, carrying both spellings, and then judges by the
35
+ * canonical form exactly as before. The notice discloses the difference — it never repairs the host's
36
+ * spelling, never narrows the fence, and never fires for a root that already is its own canonical form.
37
+ */
38
+ export declare function resolveTaskRootCanonical(input: {
39
+ env: Pick<ExecutionEnv, "canonicalPath">;
40
+ /** The root as the host spelled it (`deps.rootPath` / the restored workspace root). */
41
+ given: string;
42
+ onNotice: RunnerDeps["onNotice"];
43
+ sessionId: string;
44
+ runId: string;
45
+ }): Promise<TaskRootCanonical>;
@@ -0,0 +1,18 @@
1
+ import { deliverEngineNotice } from "../engine-notice.js";
2
+ export async function resolveTaskRootCanonical(input) {
3
+ const c = await input.env.canonicalPath(input.given);
4
+ if (!c.ok)
5
+ return { canonical: input.given, announced: false };
6
+ if (c.value === input.given)
7
+ return { canonical: c.value, announced: false };
8
+ deliverEngineNotice(input.onNotice, {
9
+ code: "config.task_root_not_canonical",
10
+ message: `The task root handed to this run is "${input.given}", which is not its own canonical path ("${c.value}"). ` +
11
+ `Containment is judged on the canonical spelling — the roots fence, the shell read boundary and every ` +
12
+ `refusal message read it — so a target this deployment considers inside the root can be judged outside ` +
13
+ `it whenever the two spellings differ (a symlinked directory; /var → /private/var on macOS). The run ` +
14
+ `proceeds on the canonical spelling exactly as before: hand the canonical path in to make the two agree.`,
15
+ detail: { given: input.given, canonical: c.value, sessionId: input.sessionId, runId: input.runId },
16
+ });
17
+ return { canonical: c.value, announced: true };
18
+ }
@@ -1,6 +1,7 @@
1
1
  import type { McpDroppedTool } from "../mcp.js";
2
2
  import type { TaskSpec } from "../types.js";
3
- import { type SkillListingEntry } from "./synthetic-tools.js";
3
+ import { type SkillListingEntry, type SkillsBlockRender } from "./synthetic-tools.js";
4
+ import { type ResolvedSkillListingBudget } from "../skill-listing-budget.js";
4
5
  /**
5
6
  * Turn-boundary attachment seam: the PURE producer layer (CC `getContextAttachments`
6
7
  * parity). This module is the single home of the CC-parity cadence judgment + reminder copy (提示词主权批:
@@ -76,9 +77,90 @@ export declare const CHANGED_FILES_MAX = 20;
76
77
  * formatter storms remain a documented false-positive source (hash-confirm 抗假阳 = v2).
77
78
  */
78
79
  export declare const CHANGED_FILES_MTIME_EPS_MS = 2000;
79
- /** 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}. */
80
82
  export declare const ATTACHMENT_BYTE_CAP: number;
81
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;
82
164
  /** G1 续批 — one agent type on the delegation tool's roster (ToolSpec.agentListing, filled by
83
165
  * createSubagentTool: defined agents + the built-in fork when offered). */
84
166
  export interface AgentListingEntry {
@@ -195,14 +277,6 @@ export interface AttachmentState {
195
277
  * `announcedListings.models` so a catalog change across a suspend IS re-announced (the drift
196
278
  * line rides the first boundary's agent_listing frame). */
197
279
  announcedModels?: readonly string[];
198
- /** [c209-C] R2 C4② — same-boundary anti-starvation latch: set by {@link collectDueAttachments}
199
- * when a rendered skills_listing block did NOT survive its own bundle intact (clipped/starved —
200
- * typically because the higher-priority agent_listing spent the byte budget first), cleared when
201
- * one survives. While set, the NEXT boundary orders skills_listing BEFORE agent_listing, so two
202
- * listing frames that cannot share one 8KB bundle alternate to completion instead of the
203
- * agent frame permanently starving the skills frame (each frame individually fits the cap by
204
- * construction — agent roster ≤ cap by deployment size, skills block ≤ SKILLS_BLOCK_MAX_BYTES). */
205
- skillsListingStarved?: boolean;
206
280
  /**
207
281
  * The `padded-countdown` arm's SMOOTHING FLOOR: the highest usage figure this run has
208
282
  * already published against. CC's `cumulativeUsed` (bundle: `Math.max(storedFloor, rolledOver +
@@ -545,6 +619,10 @@ export interface AttachmentInputs {
545
619
  * the delta path serves dynamic-installation deployments and tests, same posture as
546
620
  * `agentListing`). Only passed when `config.skillsListing` is on. */
547
621
  skillsListing?: ReadonlyArray<SkillListingEntry>;
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;
548
626
  /** G1 续批 mcp_instructions: pending deltas snapshotted from `MaterializedMcp.instructionsDelta`.
549
627
  * CALLER-owned consumption on intact cap survival (same `newTools` discipline). */
550
628
  mcpInstructions?: {
@@ -819,11 +897,24 @@ export declare function agentListingDeltaHeader(toolName: string): string;
819
897
  * no re-render byte-identity exposure of its own.
820
898
  *
821
899
  * PURE — never mutates `state`. The announced-set advance is CALLER-owned via
822
- * {@link commitAgentListing}, committed ONLY when the rendered block survived the byte cap INTACT
823
- * (tools_delta/mcp_instructions discipline — a cap-clipped/dropped block previously advanced the
824
- * 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.
825
906
  */
826
- 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
+ }
827
918
  /** [c209-C] R2 C7 — the Q4 catalog line, single-sourced for the initial frame's tail AND the
828
919
  * drift re-announce arm so the two can never diverge byte-wise. [c209-C] 收口 C7 补口: an
829
920
  * EMPTY catalog renders an honest "(none)" — reachable only from the DRIFT arm (a known announced
@@ -837,9 +928,8 @@ export declare const MODELS_AVAILABLE_PREFIX = "Models available for the 'model'
837
928
  export declare function replayAnnouncedModels(texts: readonly string[]): readonly string[] | undefined;
838
929
  /**
839
930
  * MED-3① commit half of {@link renderAgentListingDelta}: mark the CURRENT roster as announced. The run
840
- * loop calls this only when the `agent_listing` block survived the capped bundle INTACT (body equality
841
- * against a re-render — same predicate as the tools_delta drain); a clipped/dropped block leaves the
842
- * 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
843
933
  * equivalent to add/remove bookkeeping (the diff is name-keyed; descriptions are never re-diffed).
844
934
  */
845
935
  export declare function commitAgentListing(state: AttachmentState, entries: ReadonlyArray<AgentListingEntry>, models?: readonly string[]): void;
@@ -849,35 +939,57 @@ export declare function commitAgentListing(state: AttachmentState, entries: Read
849
939
  export declare const SKILLS_LISTING_DELTA_HEADER = "New skills are now available:";
850
940
  /** [c209-C] skills removal-arm header — mirrors the agent-listing removal arm's phrasing. */
851
941
  export declare const SKILLS_LISTING_REMOVED_HEADER = "The following skills are no longer available:";
942
+ /** What one {@link renderSkillsListingFrame} call produced: the render's own report, plus the
943
+ * denominator it is against. */
944
+ export interface SkillsListingFrameRender extends SkillsBlockRender {
945
+ /** How many skills this frame set out to list — the whole roster on an initial frame, the ADDED set
946
+ * on a drift frame. The disclosure's denominator, read off the frame that was actually delivered
947
+ * rather than re-derived from the roster. */
948
+ readonly listed: number;
949
+ }
852
950
  /**
853
- * [c209-C] skills_listing producer — the skills counterpart of {@link renderAgentListingDelta}.
854
- * First call (no announced map) = the full `<skills>` disclosure block VERBATIM (buildSkillsBlock —
855
- * the exact bytes that used to sit in the stable system prompt now ride the reminder lane; CC 209
856
- * classic ships its skills listing the same way, as a first-user-turn reminder ~5.9KB, report §5.1).
857
- * Later calls announce only drift, name-keyed like the agent arm: adds under
858
- * {@link SKILLS_LISTING_DELTA_HEADER} with the SAME `- name: description` line shape the block uses,
859
- * removals under {@link SKILLS_LISTING_REMOVED_HEADER} + `- <name>` lines, followed by the
860
- * CC-verbatim {@link AMBIENT_CONTEXT_NOTE} (agent-removal-arm parity). A description change on an
861
- * existing name is not re-announced (name-keyed, agent-arm parity).
951
+ * [c209-C] skills_listing producer — the skills counterpart of {@link renderAgentListingDelta}, with
952
+ * the degradation report the disclosure lane announces. First call (no announced map) = the full
953
+ * `<skills>` disclosure block VERBATIM (the exact bytes that used to sit in the stable system prompt
954
+ * now ride the reminder lane; CC 209 classic ships its skills listing the same way, as a
955
+ * first-user-turn reminder ~5.9KB, report §5.1). Later calls announce only drift, name-keyed like the
956
+ * agent arm: adds under {@link SKILLS_LISTING_DELTA_HEADER} with the SAME `- name: description` line
957
+ * shape the block uses, removals under {@link SKILLS_LISTING_REMOVED_HEADER} + `- <name>` lines,
958
+ * followed by the CC-verbatim {@link AMBIENT_CONTEXT_NOTE} (agent-removal-arm parity). A description
959
+ * change on an existing name is not re-announced (name-keyed, agent-arm parity).
960
+ *
961
+ * ONE RULER FOR EVERY FRAME: the drift frame is measured against the SAME resolved budget as the
962
+ * initial block, arms and joiners included, and its ADD arm degrades through the same three tiers
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
965
+ * arm's one-line floor are reserved first (both constants), the ADD arm degrades into what is left,
966
+ * and the removal arm spends whatever the add arm did not — so the frame's size is the resolved budget
967
+ * or, when even the two floors do not fit it, a small constant, never a function of the roster.
968
+ *
969
+ * The report covers the ADD arm only: `nameOnly` / `dropped` name skills this frame listed without a
970
+ * description or could not list at all. A departed name that did not fit is not a listed skill.
862
971
  *
863
972
  * MED-3① discipline: PURE — the announced-set advance is CALLER-owned via {@link commitSkillsListing},
864
- * committed only on first-frame delivery or intact cap survival at a boundary.
973
+ * committed on the carrying message's receipt. The commit records
974
+ * EVERY entry, including one this frame dropped: the alternative is a frame re-rendering the same
975
+ * un-listable tail at every boundary, which is the replay this budget exists to end. The `Skill` tool
976
+ * serves a dropped skill, and `skills.listing_truncated` tells the deployment it happened.
865
977
  */
866
- export declare function renderSkillsListingDelta(state: AttachmentState, entries: ReadonlyArray<SkillListingEntry>): string | undefined;
978
+ export declare function renderSkillsListingFrame(state: AttachmentState, entries: ReadonlyArray<SkillListingEntry>, budget?: ResolvedSkillListingBudget): SkillsListingFrameRender | undefined;
979
+ /** The text-only face of {@link renderSkillsListingFrame} — what the delivery lanes render, and what
980
+ * the intact-survival predicate re-renders to compare. */
981
+ export declare function renderSkillsListingDelta(state: AttachmentState, entries: ReadonlyArray<SkillListingEntry>, budget?: ResolvedSkillListingBudget): string | undefined;
867
982
  /** [c209-C] commit half of {@link renderSkillsListingDelta} — same MED-3① contract as
868
- * {@link commitAgentListing}: called by the run loop only on first-frame delivery or when the
869
- * `skills_listing` block survived the capped bundle INTACT; a clipped/dropped block leaves the
870
- * 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. */
871
985
  export declare function commitSkillsListing(state: AttachmentState, entries: ReadonlyArray<SkillListingEntry>): void;
872
986
  /**
873
987
  * Per-server bound (in CODE POINTS, matching every other clipper in `untrusted-text.ts`) on
874
- * ONE MCP server's `instructions` text. The text is SERVER-controlled and UNBOUNDED on the wire: the
875
- * bundle byte cap ({@link ATTACHMENT_BYTE_CAP}) is a per-frame structural backstop on the delta lane
876
- * and does not exist AT ALL on the stable system-prompt lane, so without this a single server could
877
- * pin an arbitrarily large blob into the cached prompt prefix for the whole task (and, on the delta
878
- * lane, wedge the frame permanently against the 8KB cap — the failure shape).
879
- * Sized at the delta lane's own order of magnitude (the 8KB frame budget), leaving fence + header
880
- * 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.
881
993
  */
882
994
  export declare const MCP_INSTRUCTIONS_MAX_CHARS: number;
883
995
  /**
@@ -911,8 +1023,8 @@ export declare function fenceMcpServerInstructions(server: string, text: string)
911
1023
  * the text is additionally already `sanitizeUntrustedText`-neutralized at the mcp.ts producer).
912
1024
  * The fence is now applied through {@link fenceMcpServerInstructions}, shared with the
913
1025
  * STABLE system-prompt lane, so the two lanes can never again carry the same text with different
914
- * trust postures — and it carries a per-server {@link MCP_INSTRUCTIONS_MAX_CHARS} bound (a
915
- * 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).
916
1028
  * The disconnect list is followed by the CC-verbatim {@link AMBIENT_CONTEXT_NOTE} (CC `Emr`,
917
1029
  * pretty.js:599240/599992 — resolved by the 2026-07-10 parity spot audit).
918
1030
  * Removal names are `\n`-joined WITHOUT a `- ` prefix (CC-exact; the agent-listing removal list DOES
@@ -943,21 +1055,16 @@ export declare function renderMcpInstructionsDelta(added: ReadonlyArray<{
943
1055
  * loop's intact-survival consumption check (tools_delta discipline).
944
1056
  */
945
1057
  export declare function renderMcpDroppedTools(entries: ReadonlyArray<McpDroppedTool>): string;
946
- /** per-boundary byte bound for one mcp_dropped_tools frame — half the bundle cap, so
947
- * 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. */
948
1060
  export declare const MCP_DROPPED_BATCH_MAX_BYTES = 4096;
949
1061
  /**
950
- * (medium) — bounded-batch selection for the dropped-tools announce. An UNBOUNDED
951
- * render larger than the 8KB bundle cap gets prefix-CLIPPED by capAttachments; the intact-survival
952
- * drain predicate then never matches, the full array is retained, and every later boundary re-renders
953
- * and re-clips the identical prefix forever (tail entries never disclosed, permanent cap churn).
954
- * Selecting the longest prefix of COMPLETE entries whose render fits {@link MCP_DROPPED_BATCH_MAX_BYTES}
955
- * (always ≥1) restores monotonic drainage: the run loop splices exactly the delivered batch, the
956
- * tail rides later boundaries. Pure/deterministic so the drain predicate can re-derive the exact
957
- * snapshot. the ≥1 floor is SAFE only because entries are bounded at the mcp.ts
958
- * producer (tool ≤160 cps, reason ≤240 cps, both single-lined) — one rendered entry is always a
959
- * few hundred bytes, far under both this bound and the 8KB bundle cap, so the single-entry frame
960
- * 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.
961
1068
  */
962
1069
  export declare function selectMcpDroppedBatch<T extends McpDroppedTool>(entries: ReadonlyArray<T>): T[];
963
1070
  /**