@gaunt-sloth/core 2.0.0-alpha.3 → 2.0.0-alpha.30

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 (271) hide show
  1. package/README.md +52 -20
  2. package/dist/config/colour.d.ts +38 -0
  3. package/dist/config/colour.js +36 -0
  4. package/dist/config/colour.js.map +1 -0
  5. package/dist/config/defaults.d.ts +84 -0
  6. package/dist/config/defaults.js +99 -0
  7. package/dist/config/defaults.js.map +1 -0
  8. package/dist/config/filesystem-tools.d.ts +41 -0
  9. package/dist/config/filesystem-tools.js +56 -0
  10. package/dist/config/filesystem-tools.js.map +1 -0
  11. package/dist/config/jsonc.d.ts +12 -0
  12. package/dist/config/jsonc.js +41 -0
  13. package/dist/config/jsonc.js.map +1 -0
  14. package/dist/config/loader.d.ts +246 -0
  15. package/dist/config/loader.js +1321 -0
  16. package/dist/config/loader.js.map +1 -0
  17. package/dist/config/mouse.d.ts +50 -0
  18. package/dist/config/mouse.js +44 -0
  19. package/dist/config/mouse.js.map +1 -0
  20. package/dist/config/profiles.d.ts +68 -0
  21. package/dist/config/profiles.js +93 -0
  22. package/dist/config/profiles.js.map +1 -0
  23. package/dist/config/providerKeys.d.ts +69 -0
  24. package/dist/config/providerKeys.js +69 -0
  25. package/dist/config/providerKeys.js.map +1 -0
  26. package/dist/config/schema.d.ts +2999 -0
  27. package/dist/config/schema.js +1519 -0
  28. package/dist/config/schema.js.map +1 -0
  29. package/dist/config/shell-policy.d.ts +896 -0
  30. package/dist/config/shell-policy.js +750 -0
  31. package/dist/config/shell-policy.js.map +1 -0
  32. package/dist/config/tool-descriptions.d.ts +211 -0
  33. package/dist/config/tool-descriptions.js +272 -0
  34. package/dist/config/tool-descriptions.js.map +1 -0
  35. package/dist/config/types.d.ts +760 -0
  36. package/dist/config/types.js +13 -0
  37. package/dist/config/types.js.map +1 -0
  38. package/dist/config.d.ts +34 -827
  39. package/dist/config.js +25 -657
  40. package/dist/config.js.map +1 -1
  41. package/dist/constants.d.ts +46 -0
  42. package/dist/constants.js +46 -0
  43. package/dist/constants.js.map +1 -1
  44. package/dist/core/GthAbstractAgent.d.ts +135 -2
  45. package/dist/core/GthAbstractAgent.js +496 -30
  46. package/dist/core/GthAbstractAgent.js.map +1 -1
  47. package/dist/core/GthAgentRunner.d.ts +568 -54
  48. package/dist/core/GthAgentRunner.js +1537 -136
  49. package/dist/core/GthAgentRunner.js.map +1 -1
  50. package/dist/core/GthLangChainAgent.d.ts +115 -0
  51. package/dist/core/GthLangChainAgent.js +644 -17
  52. package/dist/core/GthLangChainAgent.js.map +1 -1
  53. package/dist/core/approvals/annotations.d.ts +122 -0
  54. package/dist/core/approvals/annotations.js +137 -0
  55. package/dist/core/approvals/annotations.js.map +1 -0
  56. package/dist/core/approvals/grants.d.ts +216 -0
  57. package/dist/core/approvals/grants.js +469 -0
  58. package/dist/core/approvals/grants.js.map +1 -0
  59. package/dist/core/approvals/matcher.d.ts +202 -0
  60. package/dist/core/approvals/matcher.js +267 -0
  61. package/dist/core/approvals/matcher.js.map +1 -0
  62. package/dist/core/approvals/mcpSubjects.d.ts +40 -0
  63. package/dist/core/approvals/mcpSubjects.js +99 -0
  64. package/dist/core/approvals/mcpSubjects.js.map +1 -0
  65. package/dist/core/approvals/toolAnnotationSources.d.ts +105 -0
  66. package/dist/core/approvals/toolAnnotationSources.js +277 -0
  67. package/dist/core/approvals/toolAnnotationSources.js.map +1 -0
  68. package/dist/core/approvals/toolHost.d.ts +46 -0
  69. package/dist/core/approvals/toolHost.js +108 -0
  70. package/dist/core/approvals/toolHost.js.map +1 -0
  71. package/dist/core/debugCapture.d.ts +75 -0
  72. package/dist/core/debugCapture.js +101 -0
  73. package/dist/core/debugCapture.js.map +1 -0
  74. package/dist/core/gthLeanAgentFactory.d.ts +9 -0
  75. package/dist/core/gthLeanAgentFactory.js +10 -0
  76. package/dist/core/gthLeanAgentFactory.js.map +1 -0
  77. package/dist/core/launchBanner.d.ts +120 -0
  78. package/dist/core/launchBanner.js +418 -0
  79. package/dist/core/launchBanner.js.map +1 -0
  80. package/dist/core/plainToolIndication.d.ts +15 -0
  81. package/dist/core/plainToolIndication.js +174 -0
  82. package/dist/core/plainToolIndication.js.map +1 -0
  83. package/dist/core/reasoningBlocks.d.ts +60 -0
  84. package/dist/core/reasoningBlocks.js +98 -0
  85. package/dist/core/reasoningBlocks.js.map +1 -0
  86. package/dist/core/refusal.d.ts +53 -0
  87. package/dist/core/refusal.js +133 -0
  88. package/dist/core/refusal.js.map +1 -0
  89. package/dist/core/runStats.d.ts +52 -0
  90. package/dist/core/runStats.js +118 -0
  91. package/dist/core/runStats.js.map +1 -0
  92. package/dist/core/shell/ShellCommandFailedError.d.ts +54 -0
  93. package/dist/core/shell/ShellCommandFailedError.js +68 -0
  94. package/dist/core/shell/ShellCommandFailedError.js.map +1 -0
  95. package/dist/core/shell/abstention.d.ts +88 -0
  96. package/dist/core/shell/abstention.js +184 -0
  97. package/dist/core/shell/abstention.js.map +1 -0
  98. package/dist/core/shell/approvalCapture.d.ts +271 -0
  99. package/dist/core/shell/approvalCapture.js +108 -0
  100. package/dist/core/shell/approvalCapture.js.map +1 -0
  101. package/dist/core/shell/approvalStop.d.ts +84 -0
  102. package/dist/core/shell/approvalStop.js +119 -0
  103. package/dist/core/shell/approvalStop.js.map +1 -0
  104. package/dist/core/shell/arity.d.ts +6 -0
  105. package/dist/core/shell/arity.js +20 -6
  106. package/dist/core/shell/arity.js.map +1 -1
  107. package/dist/core/shell/denylist.d.ts +11 -0
  108. package/dist/core/shell/denylist.js +37 -0
  109. package/dist/core/shell/denylist.js.map +1 -0
  110. package/dist/core/shell/escalationSeverity.d.ts +141 -0
  111. package/dist/core/shell/escalationSeverity.js +89 -0
  112. package/dist/core/shell/escalationSeverity.js.map +1 -0
  113. package/dist/core/shell/framing.d.ts +190 -0
  114. package/dist/core/shell/framing.js +633 -0
  115. package/dist/core/shell/framing.js.map +1 -0
  116. package/dist/core/shell/hardline.d.ts +81 -0
  117. package/dist/core/shell/hardline.js +607 -0
  118. package/dist/core/shell/hardline.js.map +1 -0
  119. package/dist/core/shell/negotiation.d.ts +249 -0
  120. package/dist/core/shell/negotiation.js +355 -0
  121. package/dist/core/shell/negotiation.js.map +1 -0
  122. package/dist/core/shell/normalize.d.ts +44 -4
  123. package/dist/core/shell/normalize.js +61 -7
  124. package/dist/core/shell/normalize.js.map +1 -1
  125. package/dist/core/shell/openWorld.d.ts +263 -0
  126. package/dist/core/shell/openWorld.js +1188 -0
  127. package/dist/core/shell/openWorld.js.map +1 -0
  128. package/dist/core/shell/rater.d.ts +935 -0
  129. package/dist/core/shell/rater.js +1473 -0
  130. package/dist/core/shell/rater.js.map +1 -0
  131. package/dist/core/shell/raterModel.d.ts +41 -0
  132. package/dist/core/shell/raterModel.js +51 -0
  133. package/dist/core/shell/raterModel.js.map +1 -0
  134. package/dist/core/shell/rejection.d.ts +69 -0
  135. package/dist/core/shell/rejection.js +38 -0
  136. package/dist/core/shell/rejection.js.map +1 -0
  137. package/dist/core/toolCallRepair/grammar.d.ts +41 -0
  138. package/dist/core/toolCallRepair/grammar.js +116 -0
  139. package/dist/core/toolCallRepair/grammar.js.map +1 -0
  140. package/dist/core/toolCallRepair/index.d.ts +2 -0
  141. package/dist/core/toolCallRepair/index.js +7 -0
  142. package/dist/core/toolCallRepair/index.js.map +1 -0
  143. package/dist/core/toolCallRepair/payload.d.ts +36 -0
  144. package/dist/core/toolCallRepair/payload.js +341 -0
  145. package/dist/core/toolCallRepair/payload.js.map +1 -0
  146. package/dist/core/toolCallRepair/promote.d.ts +45 -0
  147. package/dist/core/toolCallRepair/promote.js +90 -0
  148. package/dist/core/toolCallRepair/promote.js.map +1 -0
  149. package/dist/core/toolDisplay.d.ts +123 -0
  150. package/dist/core/toolDisplay.js +451 -0
  151. package/dist/core/toolDisplay.js.map +1 -0
  152. package/dist/core/toolOutputChannel.d.ts +95 -0
  153. package/dist/core/toolOutputChannel.js +165 -0
  154. package/dist/core/toolOutputChannel.js.map +1 -0
  155. package/dist/core/types.d.ts +294 -12
  156. package/dist/core/types.js.map +1 -1
  157. package/dist/history/historyFormat.d.ts +28 -0
  158. package/dist/history/historyFormat.js +127 -0
  159. package/dist/history/historyFormat.js.map +1 -0
  160. package/dist/history/historyStore.d.ts +198 -0
  161. package/dist/history/historyStore.js +482 -0
  162. package/dist/history/historyStore.js.map +1 -0
  163. package/dist/history/recordSession.d.ts +37 -0
  164. package/dist/history/recordSession.js +56 -0
  165. package/dist/history/recordSession.js.map +1 -0
  166. package/dist/index.d.ts +4 -0
  167. package/dist/index.js +4 -0
  168. package/dist/index.js.map +1 -1
  169. package/dist/providers/anthropic.d.ts +1 -1
  170. package/dist/providers/anthropic.js +5 -10
  171. package/dist/providers/anthropic.js.map +1 -1
  172. package/dist/providers/deepseek.d.ts +1 -1
  173. package/dist/providers/deepseek.js +5 -10
  174. package/dist/providers/deepseek.js.map +1 -1
  175. package/dist/providers/geminiSchemaSanitizer.d.ts +55 -0
  176. package/dist/providers/geminiSchemaSanitizer.js +347 -0
  177. package/dist/providers/geminiSchemaSanitizer.js.map +1 -0
  178. package/dist/providers/geminiThinking.d.ts +52 -0
  179. package/dist/providers/geminiThinking.js +72 -0
  180. package/dist/providers/geminiThinking.js.map +1 -0
  181. package/dist/providers/google-genai.d.ts +1 -1
  182. package/dist/providers/google-genai.js +12 -11
  183. package/dist/providers/google-genai.js.map +1 -1
  184. package/dist/providers/groq.d.ts +1 -1
  185. package/dist/providers/groq.js +5 -10
  186. package/dist/providers/groq.js.map +1 -1
  187. package/dist/providers/huggingface.d.ts +25 -0
  188. package/dist/providers/huggingface.js +69 -0
  189. package/dist/providers/huggingface.js.map +1 -0
  190. package/dist/providers/modelCatalog.d.ts +109 -0
  191. package/dist/providers/modelCatalog.js +245 -0
  192. package/dist/providers/modelCatalog.js.map +1 -0
  193. package/dist/providers/modelDiscovery.d.ts +153 -6
  194. package/dist/providers/modelDiscovery.js +295 -41
  195. package/dist/providers/modelDiscovery.js.map +1 -1
  196. package/dist/providers/ollama.d.ts +19 -5
  197. package/dist/providers/ollama.js +60 -52
  198. package/dist/providers/ollama.js.map +1 -1
  199. package/dist/providers/openai.d.ts +1 -1
  200. package/dist/providers/openai.js +39 -10
  201. package/dist/providers/openai.js.map +1 -1
  202. package/dist/providers/openrouter.d.ts +4 -5
  203. package/dist/providers/openrouter.js +20 -35
  204. package/dist/providers/openrouter.js.map +1 -1
  205. package/dist/providers/vertexai.d.ts +1 -1
  206. package/dist/providers/vertexai.js +12 -11
  207. package/dist/providers/vertexai.js.map +1 -1
  208. package/dist/providers/xai.d.ts +1 -1
  209. package/dist/providers/xai.js +5 -10
  210. package/dist/providers/xai.js.map +1 -1
  211. package/dist/runtime/askStructured.d.ts +105 -0
  212. package/dist/runtime/askStructured.js +120 -0
  213. package/dist/runtime/askStructured.js.map +1 -0
  214. package/dist/runtime/conversation.d.ts +60 -0
  215. package/dist/runtime/conversation.js +151 -0
  216. package/dist/runtime/conversation.js.map +1 -0
  217. package/dist/runtime/singleShot.d.ts +25 -5
  218. package/dist/runtime/singleShot.js +95 -36
  219. package/dist/runtime/singleShot.js.map +1 -1
  220. package/dist/runtime/structuredOutput.d.ts +104 -0
  221. package/dist/runtime/structuredOutput.js +393 -0
  222. package/dist/runtime/structuredOutput.js.map +1 -0
  223. package/dist/utils/ProgressIndicator.d.ts +21 -0
  224. package/dist/utils/ProgressIndicator.js +30 -3
  225. package/dist/utils/ProgressIndicator.js.map +1 -1
  226. package/dist/utils/aiignoreUtils.js.map +1 -1
  227. package/dist/utils/binaryOutputUtils.js.map +1 -1
  228. package/dist/utils/consoleUtils.d.ts +52 -0
  229. package/dist/utils/consoleUtils.js +72 -2
  230. package/dist/utils/consoleUtils.js.map +1 -1
  231. package/dist/utils/crashHandler.d.ts +87 -0
  232. package/dist/utils/crashHandler.js +128 -0
  233. package/dist/utils/crashHandler.js.map +1 -0
  234. package/dist/utils/debugDump.d.ts +134 -0
  235. package/dist/utils/debugDump.js +381 -0
  236. package/dist/utils/debugDump.js.map +1 -0
  237. package/dist/utils/debugUtils.d.ts +13 -4
  238. package/dist/utils/debugUtils.js +36 -13
  239. package/dist/utils/debugUtils.js.map +1 -1
  240. package/dist/utils/displayWidth.d.ts +53 -0
  241. package/dist/utils/displayWidth.js +195 -0
  242. package/dist/utils/displayWidth.js.map +1 -0
  243. package/dist/utils/fileUtils.d.ts +24 -2
  244. package/dist/utils/fileUtils.js +54 -12
  245. package/dist/utils/fileUtils.js.map +1 -1
  246. package/dist/utils/llmUtils.d.ts +39 -8
  247. package/dist/utils/llmUtils.js +76 -8
  248. package/dist/utils/llmUtils.js.map +1 -1
  249. package/dist/utils/redactSecrets.d.ts +63 -0
  250. package/dist/utils/redactSecrets.js +286 -0
  251. package/dist/utils/redactSecrets.js.map +1 -0
  252. package/dist/utils/systemPromptNotes.d.ts +225 -0
  253. package/dist/utils/systemPromptNotes.js +340 -0
  254. package/dist/utils/systemPromptNotes.js.map +1 -0
  255. package/dist/utils/systemUtils.d.ts +49 -1
  256. package/dist/utils/systemUtils.js +67 -3
  257. package/dist/utils/systemUtils.js.map +1 -1
  258. package/dist/utils/toolMatching.d.ts +30 -0
  259. package/dist/utils/toolMatching.js +44 -0
  260. package/dist/utils/toolMatching.js.map +1 -0
  261. package/dist/utils/untrustedText.d.ts +66 -0
  262. package/dist/utils/untrustedText.js +80 -0
  263. package/dist/utils/untrustedText.js.map +1 -0
  264. package/package.json +22 -6
  265. package/schema/gsloth-config.schema.json +3134 -0
  266. package/dist/core/shell/allowlist.d.ts +0 -75
  267. package/dist/core/shell/allowlist.js +0 -187
  268. package/dist/core/shell/allowlist.js.map +0 -1
  269. package/dist/core/shell/judge.d.ts +0 -161
  270. package/dist/core/shell/judge.js +0 -261
  271. package/dist/core/shell/judge.js.map +0 -1
@@ -0,0 +1,249 @@
1
+ import type { RaterNegotiationContext, RaterNegotiationRound } from '#src/core/shell/rater.js';
2
+ /**
3
+ * §5.3 — **three CONSECUTIVE rejections end the negotiation** and escalate to the human: the agent
4
+ * and the rater cannot agree, and that is a human's call, not a machine's.
5
+ *
6
+ * Consecutive, not total: a successful intervening tool call — the agent going away to gather
7
+ * information and returning better informed — resets it, because that is progress, not ping-pong.
8
+ */
9
+ export declare const MAX_CONSECUTIVE_REJECTIONS = 3;
10
+ /**
11
+ * [[EXT-29]] — **the reachability bound: total rejections since a human was last involved.** A
12
+ * reset of {@link MAX_CONSECUTIVE_REJECTIONS} does not refill it; only actually reaching a person
13
+ * clears it ({@link ShellNegotiationState.humanReached}).
14
+ *
15
+ * ## Why this exists, and why §5.3 does not answer it
16
+ *
17
+ * §5.3 says *"no total ceiling is added here, and none is needed"*, and tells the next reader not to
18
+ * add one without a fresh decision. **This is that decision (Andrew, EXT-29), and it answers a
19
+ * different question than the one §5.3 asked.**
20
+ *
21
+ * §5.3's argument is about **safety**, and it is sound and untouched: every attempt is rated
22
+ * independently, the rater does not weaken with repetition, and — because a reset also clears the
23
+ * transcript — a gamed reset approves nothing that would not have been approved on the first try.
24
+ * Nothing here contradicts that, and this bound must never be described as a security control.
25
+ *
26
+ * The question §5.3 never asked is **reachability**: does the human terminus fire at all? Under its
27
+ * own reset predicate it does not. An agent that alternates one approved `ls` with one rejection
28
+ * never accumulates three *consecutive* rejections, so *"three rejections escalate to the human"* —
29
+ * §7's promise that the model is never left to silently give up on a legitimate task — is
30
+ * unreachable for that agent, for any number of rejections. The same shape was measured live on the
31
+ * shipped EXT-65 retry budget, where a human's "No" and a deny-list match each refilled the budget.
32
+ *
33
+ * **`recursionLimit` is not the answer either.** It bounds the *run* at 1000 steps; it does not
34
+ * escalate to a *person*. §5.3's *"that is a human's call"* names an event, and a dying run does not
35
+ * produce it: the user gets a step-limit failure rather than the question they were promised.
36
+ *
37
+ * Nine — three times the consecutive cap — so an agent making genuine progress between rejections
38
+ * still gets several full negotiations before a person is asked, while one making none is in front
39
+ * of a human within a bounded number of rounds.
40
+ *
41
+ * **The shape and the number are Andrew's to change** (a progress-only reset predicate is the
42
+ * standing alternative). What is not negotiable is that some bound ends at a person, so this stays
43
+ * ONE constant and ONE predicate — {@link ShellNegotiationState.recordRejection} — and swapping it
44
+ * is local.
45
+ */
46
+ export declare const MAX_REJECTIONS_BEFORE_HUMAN = 9;
47
+ /** What {@link ShellNegotiationState.recordRejection} decides: serve another round, or ask a human. */
48
+ export type NegotiationVerdict = 'reject' | 'escalate';
49
+ /**
50
+ * Where a negotiation stands against its two bounds, as a plain readable projection.
51
+ *
52
+ * The caps travel WITH the counts on purpose: "2 consecutive rejections" says nothing on its own,
53
+ * and a reader of a `/debug-dump` archive has no way to look {@link MAX_CONSECUTIVE_REJECTIONS} up.
54
+ * "2 of 3" is the same fact and is actionable.
55
+ */
56
+ export interface NegotiationCounters {
57
+ /** §5.3's consecutive-rejection count. Cleared by an approved call and by reaching a human. */
58
+ consecutiveRejections: number;
59
+ /** The reachability bound's count: total rejections since a person was last involved. */
60
+ rejectionsSinceHuman: number;
61
+ /** {@link MAX_CONSECUTIVE_REJECTIONS}. */
62
+ maxConsecutive: number;
63
+ /** {@link MAX_REJECTIONS_BEFORE_HUMAN}. */
64
+ maxBeforeHuman: number;
65
+ }
66
+ /**
67
+ * §5.1 — how many of the user's most recent messages the runner keeps for the rater. The prompt
68
+ * builder takes the last 5 of whatever it is handed and truncates each; this is the runner's own
69
+ * retention bound, so an unbounded conversation does not accumulate here.
70
+ *
71
+ * Deliberately larger than the builder's window: the builder drops blank messages *before* taking
72
+ * its five, and it can only drop what it was given, so keeping a little slack is what stops a run of
73
+ * empty turns from spending the budget that carries the mandate.
74
+ */
75
+ export declare const NEGOTIATION_USER_MESSAGE_RETENTION = 10;
76
+ /**
77
+ * The per-session state of §5's negotiation: one instance per {@link
78
+ * import('../GthAgentRunner.js').GthAgentRunner}.
79
+ *
80
+ * Every mutation is one of four events, and naming them is what keeps the two bounds from being
81
+ * confused with each other:
82
+ *
83
+ * | event | transcript | consecutive | since-human |
84
+ * |---|---|---|---|
85
+ * | a rejection ({@link recordRejection}) | append | +1 | +1 |
86
+ * | the gate approved a call ({@link noteProgress}) | **cleared** | 0 | unchanged |
87
+ * | a human was reached ({@link humanReached}) | cleared | 0 | **0** |
88
+ * | the run halted ({@link humanReached}) | cleared | 0 | 0 |
89
+ */
90
+ export declare class ShellNegotiationState {
91
+ /** §5.1's transcript: every round of the CURRENT negotiation, oldest first. */
92
+ private rounds;
93
+ /** §5.3's consecutive-rejection count. Shares the transcript's lifetime, by construction. */
94
+ private consecutive;
95
+ /** The reachability bound's count. Cleared ONLY by {@link humanReached}. */
96
+ private sinceHuman;
97
+ /** §5.1's last user messages, oldest first, capped at {@link NEGOTIATION_USER_MESSAGE_RETENTION}. */
98
+ private userMessages;
99
+ /**
100
+ * §5.1 — the context for the rating about to be made: the justification the agent supplied for
101
+ * *this* command, the user messages, and the rounds so far.
102
+ *
103
+ * Handed over raw. The prompt builder owns every bound and every transform (last 5, truncation,
104
+ * home-folding, one-lining, tag neutralisation), so a caller that pre-processes anything here is
105
+ * doing the work twice and differently.
106
+ *
107
+ * An empty negotiation has no privileged spelling on the builder's side, so this always returns an
108
+ * object and never `undefined`: a cleared transcript IS the round-1 case, with no `if` at the call
109
+ * site — which is exactly what makes §5.6's *"a cleared transcript means a round-1 context"* fall
110
+ * out of the reset rather than out of a second branch that could disagree with it.
111
+ *
112
+ * **Everything except the command is admitted from round 2, never at round 1**, and the transcript
113
+ * is what decides which round this is. §5.1 is unambiguous — *"Round 1 sees the command alone —
114
+ * nothing else"* — and §5.6 spells out the consequence for the round right after a reset: *"the
115
+ * command and nothing else — no transcript, no user messages, because that is what round 1 means"*.
116
+ *
117
+ * Keying them on the transcript rather than on a flag is what makes those two sentences the same
118
+ * fact, and it is why the justification is withheld by the same test rather than passed straight
119
+ * through. §5.1 lists the justification under what *"from round 2 the rater additionally sees"*,
120
+ * and it is the one channel the design allows to LOWER an outcome — so a justification volunteered
121
+ * before any rejection has happened would open that channel on the first attempt, pre-emptively,
122
+ * for the agent or for anything that has injected into the agent's context. Withholding it until
123
+ * round 2 is what the spec's ordering buys, and a round-1 context is byte-identical to a plain
124
+ * rating because of it.
125
+ *
126
+ * The messages and the volunteered justification are NOT destroyed by a reset (they are the
127
+ * conversation and the pending call's own argument, not the exchange), so §5.6's convergence still
128
+ * works: the reply *"just the last two"* is out of view for the round-1 rating after the reset and
129
+ * in view for the round-2 rating that follows it — which is exactly the row the spec's table shows
130
+ * it arriving on. A justification the agent supplies again with its next attempt is admitted then,
131
+ * because by then there is a rejection for it to answer.
132
+ */
133
+ contextFor(justification?: string): RaterNegotiationContext;
134
+ /**
135
+ * Record a rejected round and decide whether another may be served.
136
+ *
137
+ * **The round is appended BEFORE either bound is tested**, so the rating that escalates is itself
138
+ * on the transcript the human sees. §5.6's escalation example turns on this: what matters on the
139
+ * screen is that the agent proposed the same command three times, and the third proposal is the
140
+ * one being escalated.
141
+ */
142
+ recordRejection(round: RaterNegotiationRound): NegotiationVerdict;
143
+ /**
144
+ * §5.3 — a tool call the gate let through. Resets the consecutive counter **and clears the
145
+ * transcript with it**; the reachability bound is deliberately untouched.
146
+ *
147
+ * **"Approved" is what the gate can observe, and it is not quite §5.3's "successful".** The
148
+ * decision site sees whether a call was allowed to run, never whether it then exited zero — and
149
+ * the honest alternatives were worse: a tool-result stream carries the *rejected* call's own
150
+ * result too, so counting results would let a rejection reset the counter that exists to bound it.
151
+ * Erring here is permissive on this bound alone, which is precisely what
152
+ * {@link MAX_REJECTIONS_BEFORE_HUMAN} is monotonic for.
153
+ */
154
+ noteProgress(): void;
155
+ /**
156
+ * A human was actually reached — an escalation presented to them, a new user turn, or a run that
157
+ * ended. Clears everything, including the reachability bound: §5.3's *"three rejections end the
158
+ * negotiation"* means the exchange is over, not merely paused.
159
+ */
160
+ humanReached(): void;
161
+ /**
162
+ * §5.1 — record what the user said, oldest first, for the ratings of this and later turns.
163
+ *
164
+ * Blank messages are dropped here as well as by the builder: an empty turn carries nothing a rater
165
+ * can weigh, and keeping it would spend a retention slot that the message carrying the mandate
166
+ * needs.
167
+ *
168
+ * **A message already retained moves to the newest position rather than being stored twice**, and
169
+ * that is not tidiness. The runner cannot tell "this turn's new message" from "the whole
170
+ * conversation replayed", because both arrive as the same argument — `runtime/conversation.ts`
171
+ * replays the accumulated array on every turn, while the TUI and the readline session pass the one
172
+ * new message. Appending blindly would fill §5.1's five-message window with repeats of the same
173
+ * sentence for the replaying caller, starving the rater of the one context it is allowed. Keeping
174
+ * the newest position is also the right answer when a user genuinely repeats themselves: it is one
175
+ * thing they said, and it was said most recently.
176
+ */
177
+ noteUserMessages(messages: readonly string[]): void;
178
+ /**
179
+ * §6 — the rounds to show the human, oldest first. A snapshot: the caller holds it across the
180
+ * {@link humanReached} that immediately follows, and nothing it holds may change underneath it.
181
+ */
182
+ transcript(): readonly RaterNegotiationRound[];
183
+ /**
184
+ * [[TUI-C27]] — where this negotiation stands against both bounds, for the `/debug-dump` archive.
185
+ *
186
+ * **There is a real reader for this now, which is what changed.** `sinceHuman` had no accessor
187
+ * because the only thing that wanted it was a spec, and a getter in production for a spec-only
188
+ * reader is a widened class with nothing behind it (`shellNegotiation.spec.ts` reaches it through
189
+ * a cast for exactly that reason, and still does). The archive is a production reader: *"the call
190
+ * was rejected"* and *"the call was the third consecutive rejection, so the next one goes to a
191
+ * person"* are different facts, and a bug report carrying only the first invites the wrong
192
+ * conclusion about why a session started interrupting.
193
+ */
194
+ counters(): NegotiationCounters;
195
+ /** Drop everything, including the user messages — the TUI's `/clear` rotates the thread. */
196
+ clear(): void;
197
+ }
198
+ /**
199
+ * §5.4 — **who is speaking on one row of the transcript**, so a surface can colour the two voices
200
+ * apart. §5.4 requires the rater's turns to be yellow *precisely so the two are never confused*,
201
+ * and a renderer that hands back one joined string cannot express that: both surfaces painted the
202
+ * whole exchange in a single colour, which is the thing the spec forbids.
203
+ *
204
+ * - `chrome` — this renderer's own words (the heading).
205
+ * - `agent` — what the agent proposed, and the justification it gave for it.
206
+ * - `rater` — the rating that answered.
207
+ */
208
+ export type NegotiationVoice = 'chrome' | 'agent' | 'rater';
209
+ /** One terminal row of a rendered negotiation, tagged with whose turn it is. */
210
+ export interface NegotiationRow {
211
+ voice: NegotiationVoice;
212
+ text: string;
213
+ }
214
+ /**
215
+ * §6/§5.4 — render a negotiation for the human being asked to rule on it, one terminal row per
216
+ * element, each tagged with the voice speaking it.
217
+ *
218
+ * **The whole exchange is shown, never only the last attempt.** *That the agent proposed
219
+ * `git reset --hard origin/main` three times unchanged, against two rejections that each told it
220
+ * what to fix, is itself the most important thing on the screen, and it is invisible if only the
221
+ * last attempt is shown.* A prompt that shows the final command alone asks the user to rule on a
222
+ * command; this asks them to rule on an argument, which is the decision they actually have.
223
+ *
224
+ * Empty when there are no rounds, so a surface renders no heading over an argument that never
225
+ * happened (`catastrophic`, a declared escalate entry, an unrated rung).
226
+ *
227
+ * **`width` is optional and means "bind the rows to this terminal".** Given one, every row returned
228
+ * fits it under either ambiguous-width policy — measured with the same conservative ruler
229
+ * `core/shell/framing` budgets with, never `.length`, because a row measured as fitting that does
230
+ * not fit is a row the terminal wraps back to column 0. Omitted, rows are returned unwrapped, which
231
+ * is what the §6.2 non-interactive message wants: it is prose in an exception, not a screen.
232
+ *
233
+ * A wrapped row keeps the voice of the row it continues. A continuation painted as chrome would put
234
+ * the rater's words in the agent's colour at exactly the width where a long argument is hardest to
235
+ * read, which is the confusion §5.4 exists to remove.
236
+ */
237
+ export declare function renderNegotiationRows(rounds: readonly RaterNegotiationRound[], options?: {
238
+ width?: number;
239
+ }): NegotiationRow[];
240
+ /**
241
+ * The same transcript as one string, for a consumer with no screen to lay it out on: §6.2's
242
+ * non-interactive escalation message, where the exchange goes into an exception because that
243
+ * message is the only thing anyone sees on that path.
244
+ *
245
+ * Defined as {@link renderNegotiationRows} joined, so the string and the rows can never come to
246
+ * describe one exchange two ways — the whole reason there is one renderer and two surfaces.
247
+ * `null` when there is nothing to show.
248
+ */
249
+ export declare function renderNegotiationTranscript(rounds: readonly RaterNegotiationRound[]): string | null;
@@ -0,0 +1,355 @@
1
+ /**
2
+ * @module core/shell/negotiation
3
+ *
4
+ * [[EXT-29]] (spec §5) — **the bounded, visible argument between the agent and the rater** that
5
+ * `auto` conducts where `assisted` interrupts a person.
6
+ *
7
+ * A `destructive` rating at `auto` does not go to the human. It goes back to the *agent* as the
8
+ * refused call's tool result (§7), naming what would make the command acceptable (§5.2); the agent
9
+ * may narrow the command or justify the one it chose; the next call is rated again with the
10
+ * exchange in view (§5.1). This module holds the state that makes that a bounded exchange rather
11
+ * than a loop: the transcript, and the two counters that end it at a person.
12
+ *
13
+ * **It decides nothing about safety.** Every round is a full, independent rating by
14
+ * {@link import('./rater.js').mapVerdictToAction}; this only answers *"may another round be
15
+ * served, or is it a human's turn?"*.
16
+ *
17
+ * ## The lifetimes, which are the whole design
18
+ *
19
+ * - **The transcript and the consecutive counter have ONE lifetime, by construction** (§5.3). A
20
+ * successful intervening tool call resets the counter *and clears the transcript with it*, so the
21
+ * rating right after a reset is a round-1 context — the command alone, no transcript and no user
22
+ * messages. They are one field's worth of state precisely because the spec warns that an
23
+ * implementation clearing the counter alone *"looks correct and passes any obvious test"* while
24
+ * letting justification text accumulate across unbounded attempts.
25
+ * - **The reachability bound is monotonic and a reset does not refill it.** See
26
+ * {@link MAX_REJECTIONS_BEFORE_HUMAN}.
27
+ */
28
+ import { MIN_CONTENT_WIDTH, neutralizeToOneLine, wrapToWidth } from '#src/core/shell/framing.js';
29
+ import { maxDisplayWidth } from '#src/utils/displayWidth.js';
30
+ /**
31
+ * §5.3 — **three CONSECUTIVE rejections end the negotiation** and escalate to the human: the agent
32
+ * and the rater cannot agree, and that is a human's call, not a machine's.
33
+ *
34
+ * Consecutive, not total: a successful intervening tool call — the agent going away to gather
35
+ * information and returning better informed — resets it, because that is progress, not ping-pong.
36
+ */
37
+ export const MAX_CONSECUTIVE_REJECTIONS = 3;
38
+ /**
39
+ * [[EXT-29]] — **the reachability bound: total rejections since a human was last involved.** A
40
+ * reset of {@link MAX_CONSECUTIVE_REJECTIONS} does not refill it; only actually reaching a person
41
+ * clears it ({@link ShellNegotiationState.humanReached}).
42
+ *
43
+ * ## Why this exists, and why §5.3 does not answer it
44
+ *
45
+ * §5.3 says *"no total ceiling is added here, and none is needed"*, and tells the next reader not to
46
+ * add one without a fresh decision. **This is that decision (Andrew, EXT-29), and it answers a
47
+ * different question than the one §5.3 asked.**
48
+ *
49
+ * §5.3's argument is about **safety**, and it is sound and untouched: every attempt is rated
50
+ * independently, the rater does not weaken with repetition, and — because a reset also clears the
51
+ * transcript — a gamed reset approves nothing that would not have been approved on the first try.
52
+ * Nothing here contradicts that, and this bound must never be described as a security control.
53
+ *
54
+ * The question §5.3 never asked is **reachability**: does the human terminus fire at all? Under its
55
+ * own reset predicate it does not. An agent that alternates one approved `ls` with one rejection
56
+ * never accumulates three *consecutive* rejections, so *"three rejections escalate to the human"* —
57
+ * §7's promise that the model is never left to silently give up on a legitimate task — is
58
+ * unreachable for that agent, for any number of rejections. The same shape was measured live on the
59
+ * shipped EXT-65 retry budget, where a human's "No" and a deny-list match each refilled the budget.
60
+ *
61
+ * **`recursionLimit` is not the answer either.** It bounds the *run* at 1000 steps; it does not
62
+ * escalate to a *person*. §5.3's *"that is a human's call"* names an event, and a dying run does not
63
+ * produce it: the user gets a step-limit failure rather than the question they were promised.
64
+ *
65
+ * Nine — three times the consecutive cap — so an agent making genuine progress between rejections
66
+ * still gets several full negotiations before a person is asked, while one making none is in front
67
+ * of a human within a bounded number of rounds.
68
+ *
69
+ * **The shape and the number are Andrew's to change** (a progress-only reset predicate is the
70
+ * standing alternative). What is not negotiable is that some bound ends at a person, so this stays
71
+ * ONE constant and ONE predicate — {@link ShellNegotiationState.recordRejection} — and swapping it
72
+ * is local.
73
+ */
74
+ export const MAX_REJECTIONS_BEFORE_HUMAN = 9;
75
+ /**
76
+ * §5.1 — how many of the user's most recent messages the runner keeps for the rater. The prompt
77
+ * builder takes the last 5 of whatever it is handed and truncates each; this is the runner's own
78
+ * retention bound, so an unbounded conversation does not accumulate here.
79
+ *
80
+ * Deliberately larger than the builder's window: the builder drops blank messages *before* taking
81
+ * its five, and it can only drop what it was given, so keeping a little slack is what stops a run of
82
+ * empty turns from spending the budget that carries the mandate.
83
+ */
84
+ export const NEGOTIATION_USER_MESSAGE_RETENTION = 10;
85
+ /**
86
+ * The per-session state of §5's negotiation: one instance per {@link
87
+ * import('../GthAgentRunner.js').GthAgentRunner}.
88
+ *
89
+ * Every mutation is one of four events, and naming them is what keeps the two bounds from being
90
+ * confused with each other:
91
+ *
92
+ * | event | transcript | consecutive | since-human |
93
+ * |---|---|---|---|
94
+ * | a rejection ({@link recordRejection}) | append | +1 | +1 |
95
+ * | the gate approved a call ({@link noteProgress}) | **cleared** | 0 | unchanged |
96
+ * | a human was reached ({@link humanReached}) | cleared | 0 | **0** |
97
+ * | the run halted ({@link humanReached}) | cleared | 0 | 0 |
98
+ */
99
+ export class ShellNegotiationState {
100
+ /** §5.1's transcript: every round of the CURRENT negotiation, oldest first. */
101
+ rounds = [];
102
+ /** §5.3's consecutive-rejection count. Shares the transcript's lifetime, by construction. */
103
+ consecutive = 0;
104
+ /** The reachability bound's count. Cleared ONLY by {@link humanReached}. */
105
+ sinceHuman = 0;
106
+ /** §5.1's last user messages, oldest first, capped at {@link NEGOTIATION_USER_MESSAGE_RETENTION}. */
107
+ userMessages = [];
108
+ /**
109
+ * §5.1 — the context for the rating about to be made: the justification the agent supplied for
110
+ * *this* command, the user messages, and the rounds so far.
111
+ *
112
+ * Handed over raw. The prompt builder owns every bound and every transform (last 5, truncation,
113
+ * home-folding, one-lining, tag neutralisation), so a caller that pre-processes anything here is
114
+ * doing the work twice and differently.
115
+ *
116
+ * An empty negotiation has no privileged spelling on the builder's side, so this always returns an
117
+ * object and never `undefined`: a cleared transcript IS the round-1 case, with no `if` at the call
118
+ * site — which is exactly what makes §5.6's *"a cleared transcript means a round-1 context"* fall
119
+ * out of the reset rather than out of a second branch that could disagree with it.
120
+ *
121
+ * **Everything except the command is admitted from round 2, never at round 1**, and the transcript
122
+ * is what decides which round this is. §5.1 is unambiguous — *"Round 1 sees the command alone —
123
+ * nothing else"* — and §5.6 spells out the consequence for the round right after a reset: *"the
124
+ * command and nothing else — no transcript, no user messages, because that is what round 1 means"*.
125
+ *
126
+ * Keying them on the transcript rather than on a flag is what makes those two sentences the same
127
+ * fact, and it is why the justification is withheld by the same test rather than passed straight
128
+ * through. §5.1 lists the justification under what *"from round 2 the rater additionally sees"*,
129
+ * and it is the one channel the design allows to LOWER an outcome — so a justification volunteered
130
+ * before any rejection has happened would open that channel on the first attempt, pre-emptively,
131
+ * for the agent or for anything that has injected into the agent's context. Withholding it until
132
+ * round 2 is what the spec's ordering buys, and a round-1 context is byte-identical to a plain
133
+ * rating because of it.
134
+ *
135
+ * The messages and the volunteered justification are NOT destroyed by a reset (they are the
136
+ * conversation and the pending call's own argument, not the exchange), so §5.6's convergence still
137
+ * works: the reply *"just the last two"* is out of view for the round-1 rating after the reset and
138
+ * in view for the round-2 rating that follows it — which is exactly the row the spec's table shows
139
+ * it arriving on. A justification the agent supplies again with its next attempt is admitted then,
140
+ * because by then there is a rejection for it to answer.
141
+ */
142
+ contextFor(justification) {
143
+ const roundOne = this.rounds.length === 0;
144
+ return {
145
+ justification: roundOne ? undefined : justification,
146
+ userMessages: roundOne ? [] : [...this.userMessages],
147
+ priorRounds: [...this.rounds],
148
+ };
149
+ }
150
+ /**
151
+ * Record a rejected round and decide whether another may be served.
152
+ *
153
+ * **The round is appended BEFORE either bound is tested**, so the rating that escalates is itself
154
+ * on the transcript the human sees. §5.6's escalation example turns on this: what matters on the
155
+ * screen is that the agent proposed the same command three times, and the third proposal is the
156
+ * one being escalated.
157
+ */
158
+ recordRejection(round) {
159
+ this.rounds.push(round);
160
+ this.consecutive += 1;
161
+ this.sinceHuman += 1;
162
+ if (this.consecutive >= MAX_CONSECUTIVE_REJECTIONS)
163
+ return 'escalate';
164
+ if (this.sinceHuman >= MAX_REJECTIONS_BEFORE_HUMAN)
165
+ return 'escalate';
166
+ return 'reject';
167
+ }
168
+ /**
169
+ * §5.3 — a tool call the gate let through. Resets the consecutive counter **and clears the
170
+ * transcript with it**; the reachability bound is deliberately untouched.
171
+ *
172
+ * **"Approved" is what the gate can observe, and it is not quite §5.3's "successful".** The
173
+ * decision site sees whether a call was allowed to run, never whether it then exited zero — and
174
+ * the honest alternatives were worse: a tool-result stream carries the *rejected* call's own
175
+ * result too, so counting results would let a rejection reset the counter that exists to bound it.
176
+ * Erring here is permissive on this bound alone, which is precisely what
177
+ * {@link MAX_REJECTIONS_BEFORE_HUMAN} is monotonic for.
178
+ */
179
+ noteProgress() {
180
+ this.rounds = [];
181
+ this.consecutive = 0;
182
+ }
183
+ /**
184
+ * A human was actually reached — an escalation presented to them, a new user turn, or a run that
185
+ * ended. Clears everything, including the reachability bound: §5.3's *"three rejections end the
186
+ * negotiation"* means the exchange is over, not merely paused.
187
+ */
188
+ humanReached() {
189
+ this.rounds = [];
190
+ this.consecutive = 0;
191
+ this.sinceHuman = 0;
192
+ }
193
+ /**
194
+ * §5.1 — record what the user said, oldest first, for the ratings of this and later turns.
195
+ *
196
+ * Blank messages are dropped here as well as by the builder: an empty turn carries nothing a rater
197
+ * can weigh, and keeping it would spend a retention slot that the message carrying the mandate
198
+ * needs.
199
+ *
200
+ * **A message already retained moves to the newest position rather than being stored twice**, and
201
+ * that is not tidiness. The runner cannot tell "this turn's new message" from "the whole
202
+ * conversation replayed", because both arrive as the same argument — `runtime/conversation.ts`
203
+ * replays the accumulated array on every turn, while the TUI and the readline session pass the one
204
+ * new message. Appending blindly would fill §5.1's five-message window with repeats of the same
205
+ * sentence for the replaying caller, starving the rater of the one context it is allowed. Keeping
206
+ * the newest position is also the right answer when a user genuinely repeats themselves: it is one
207
+ * thing they said, and it was said most recently.
208
+ */
209
+ noteUserMessages(messages) {
210
+ for (const message of messages) {
211
+ if (message.trim().length === 0)
212
+ continue;
213
+ const seen = this.userMessages.indexOf(message);
214
+ if (seen !== -1)
215
+ this.userMessages.splice(seen, 1);
216
+ this.userMessages.push(message);
217
+ }
218
+ if (this.userMessages.length > NEGOTIATION_USER_MESSAGE_RETENTION) {
219
+ this.userMessages = this.userMessages.slice(-NEGOTIATION_USER_MESSAGE_RETENTION);
220
+ }
221
+ }
222
+ /**
223
+ * §6 — the rounds to show the human, oldest first. A snapshot: the caller holds it across the
224
+ * {@link humanReached} that immediately follows, and nothing it holds may change underneath it.
225
+ */
226
+ transcript() {
227
+ return [...this.rounds];
228
+ }
229
+ /**
230
+ * [[TUI-C27]] — where this negotiation stands against both bounds, for the `/debug-dump` archive.
231
+ *
232
+ * **There is a real reader for this now, which is what changed.** `sinceHuman` had no accessor
233
+ * because the only thing that wanted it was a spec, and a getter in production for a spec-only
234
+ * reader is a widened class with nothing behind it (`shellNegotiation.spec.ts` reaches it through
235
+ * a cast for exactly that reason, and still does). The archive is a production reader: *"the call
236
+ * was rejected"* and *"the call was the third consecutive rejection, so the next one goes to a
237
+ * person"* are different facts, and a bug report carrying only the first invites the wrong
238
+ * conclusion about why a session started interrupting.
239
+ */
240
+ counters() {
241
+ return {
242
+ consecutiveRejections: this.consecutive,
243
+ rejectionsSinceHuman: this.sinceHuman,
244
+ maxConsecutive: MAX_CONSECUTIVE_REJECTIONS,
245
+ maxBeforeHuman: MAX_REJECTIONS_BEFORE_HUMAN,
246
+ };
247
+ }
248
+ /** Drop everything, including the user messages — the TUI's `/clear` rotates the thread. */
249
+ clear() {
250
+ this.humanReached();
251
+ this.userMessages = [];
252
+ }
253
+ }
254
+ /**
255
+ * The prefix a row too wide for the terminal is continued with.
256
+ *
257
+ * **A continuation is where this block could be forged, and an indent alone does not stop it.** The
258
+ * rows carry agent-authored text after this renderer's own `Round N:` / `agent justified:` /
259
+ * `rater answered:` labels, so a long command left to the terminal's own wrap continues at column 0
260
+ * — the flush-left forgery `core/shell/framing` exists to prevent, reached through the one block
261
+ * that was not framed. Wrapping here fixes the column, and a marker no label starts with fixes the
262
+ * rest: a continuation cannot be read as a turn that was never taken, whatever it contains.
263
+ */
264
+ const CONTINUATION_PREFIX = ' ┊ ';
265
+ /**
266
+ * §6/§5.4 — render a negotiation for the human being asked to rule on it, one terminal row per
267
+ * element, each tagged with the voice speaking it.
268
+ *
269
+ * **The whole exchange is shown, never only the last attempt.** *That the agent proposed
270
+ * `git reset --hard origin/main` three times unchanged, against two rejections that each told it
271
+ * what to fix, is itself the most important thing on the screen, and it is invisible if only the
272
+ * last attempt is shown.* A prompt that shows the final command alone asks the user to rule on a
273
+ * command; this asks them to rule on an argument, which is the decision they actually have.
274
+ *
275
+ * Empty when there are no rounds, so a surface renders no heading over an argument that never
276
+ * happened (`catastrophic`, a declared escalate entry, an unrated rung).
277
+ *
278
+ * **`width` is optional and means "bind the rows to this terminal".** Given one, every row returned
279
+ * fits it under either ambiguous-width policy — measured with the same conservative ruler
280
+ * `core/shell/framing` budgets with, never `.length`, because a row measured as fitting that does
281
+ * not fit is a row the terminal wraps back to column 0. Omitted, rows are returned unwrapped, which
282
+ * is what the §6.2 non-interactive message wants: it is prose in an exception, not a screen.
283
+ *
284
+ * A wrapped row keeps the voice of the row it continues. A continuation painted as chrome would put
285
+ * the rater's words in the agent's colour at exactly the width where a long argument is hardest to
286
+ * read, which is the confusion §5.4 exists to remove.
287
+ */
288
+ export function renderNegotiationRows(rounds, options) {
289
+ if (rounds.length === 0)
290
+ return [];
291
+ const rows = [
292
+ {
293
+ voice: 'chrome',
294
+ text: `The agent argued with the auto-rater ${rounds.length} ${rounds.length === 1 ? 'time' : 'times'} before this:`,
295
+ },
296
+ ];
297
+ rounds.forEach((round, index) => {
298
+ rows.push({ voice: 'agent', text: ` Round ${index + 1}: ${oneLine(round.command)}` });
299
+ const justification = round.justification?.trim();
300
+ if (justification) {
301
+ rows.push({ voice: 'agent', text: ` agent justified: ${oneLine(justification)}` });
302
+ }
303
+ const reason = round.reason.trim();
304
+ rows.push({
305
+ voice: 'rater',
306
+ text: ` rater answered: ${round.outcome}${reason ? ` — ${oneLine(reason)}` : ''}`,
307
+ });
308
+ });
309
+ const width = options?.width;
310
+ if (width === undefined)
311
+ return rows;
312
+ return rows.flatMap((row) => wrapRow(row, width));
313
+ }
314
+ /** One logical row as the terminal rows it needs, continuations marked and voice preserved. */
315
+ function wrapRow(row, width) {
316
+ const budget = Math.max(MIN_CONTENT_WIDTH, width - maxDisplayWidth(CONTINUATION_PREFIX));
317
+ const [head, ...rest] = wrapToWidth(row.text, budget);
318
+ return [
319
+ { voice: row.voice, text: head },
320
+ ...rest.map((text) => ({ voice: row.voice, text: `${CONTINUATION_PREFIX}${text}` })),
321
+ ];
322
+ }
323
+ /**
324
+ * The same transcript as one string, for a consumer with no screen to lay it out on: §6.2's
325
+ * non-interactive escalation message, where the exchange goes into an exception because that
326
+ * message is the only thing anyone sees on that path.
327
+ *
328
+ * Defined as {@link renderNegotiationRows} joined, so the string and the rows can never come to
329
+ * describe one exchange two ways — the whole reason there is one renderer and two surfaces.
330
+ * `null` when there is nothing to show.
331
+ */
332
+ export function renderNegotiationTranscript(rounds) {
333
+ const rows = renderNegotiationRows(rounds);
334
+ if (rows.length === 0)
335
+ return null;
336
+ return rows.map((row) => row.text).join('\n');
337
+ }
338
+ /**
339
+ * Collapse a value onto one line before it is rendered into a line-structured block.
340
+ *
341
+ * Every value here is agent-authored or agent-influenced, and this block's meaning is carried by its
342
+ * line structure: a newline inside a command or a justification would otherwise let it forge a
343
+ * `Round N` heading and an answer that was never given. The rating prompt's own transcript builder
344
+ * has the identical rule for the identical reason.
345
+ *
346
+ * [[TUI-C26]] — collapsing whitespace is **not** on its own enough for a value bound for a terminal.
347
+ * JavaScript's `\s` covers LF, CR and TAB and covers neither ESC nor the C1 range, so a rater
348
+ * `reason` carrying a screen-clear sequence used to reach the approval dialog intact on a line that
349
+ * merely looked tidy. Neutralisation runs first, and the collapse then only has ordinary spaces left
350
+ * to fold.
351
+ */
352
+ function oneLine(text) {
353
+ return neutralizeToOneLine(text);
354
+ }
355
+ //# sourceMappingURL=negotiation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"negotiation.js","sourceRoot":"","sources":["../../../src/core/shell/negotiation.ts"],"names":[],"mappings":"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"}