@gaunt-sloth/core 2.0.0-beta.0 → 2.0.0-beta.2

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 (90) hide show
  1. package/LICENSE +6 -6
  2. package/dist/config/loader.d.ts +13 -0
  3. package/dist/config/loader.js +55 -11
  4. package/dist/config/loader.js.map +1 -1
  5. package/dist/config/schema.d.ts +10 -0
  6. package/dist/config/schema.js +46 -24
  7. package/dist/config/schema.js.map +1 -1
  8. package/dist/config/shell-policy.d.ts +142 -1
  9. package/dist/config/shell-policy.js +114 -7
  10. package/dist/config/shell-policy.js.map +1 -1
  11. package/dist/config/types.d.ts +19 -0
  12. package/dist/config/types.js.map +1 -1
  13. package/dist/constants.d.ts +10 -0
  14. package/dist/constants.js +10 -0
  15. package/dist/constants.js.map +1 -1
  16. package/dist/core/GthAbstractAgent.d.ts +31 -0
  17. package/dist/core/GthAbstractAgent.js +159 -4
  18. package/dist/core/GthAbstractAgent.js.map +1 -1
  19. package/dist/core/GthAgentRunner.d.ts +314 -18
  20. package/dist/core/GthAgentRunner.js +1032 -101
  21. package/dist/core/GthAgentRunner.js.map +1 -1
  22. package/dist/core/approvals/approvalRequest.d.ts +247 -0
  23. package/dist/core/approvals/approvalRequest.js +315 -0
  24. package/dist/core/approvals/approvalRequest.js.map +1 -0
  25. package/dist/core/approvals/grants.d.ts +165 -9
  26. package/dist/core/approvals/grants.js +702 -57
  27. package/dist/core/approvals/grants.js.map +1 -1
  28. package/dist/core/plainToolIndication.d.ts +11 -1
  29. package/dist/core/plainToolIndication.js +25 -10
  30. package/dist/core/plainToolIndication.js.map +1 -1
  31. package/dist/core/shell/abstention.d.ts +1 -1
  32. package/dist/core/shell/abstention.js +65 -11
  33. package/dist/core/shell/abstention.js.map +1 -1
  34. package/dist/core/shell/alignment.d.ts +491 -0
  35. package/dist/core/shell/alignment.js +687 -0
  36. package/dist/core/shell/alignment.js.map +1 -0
  37. package/dist/core/shell/approvalCapture.d.ts +53 -41
  38. package/dist/core/shell/approvalCapture.js +0 -42
  39. package/dist/core/shell/approvalCapture.js.map +1 -1
  40. package/dist/core/shell/approvalStop.d.ts +121 -3
  41. package/dist/core/shell/approvalStop.js +153 -90
  42. package/dist/core/shell/approvalStop.js.map +1 -1
  43. package/dist/core/shell/hardline.d.ts +26 -4
  44. package/dist/core/shell/hardline.js +321 -16
  45. package/dist/core/shell/hardline.js.map +1 -1
  46. package/dist/core/shell/negotiation.d.ts +352 -63
  47. package/dist/core/shell/negotiation.js +295 -123
  48. package/dist/core/shell/negotiation.js.map +1 -1
  49. package/dist/core/shell/openWorld.d.ts +135 -0
  50. package/dist/core/shell/openWorld.js +572 -35
  51. package/dist/core/shell/openWorld.js.map +1 -1
  52. package/dist/core/shell/provenance.d.ts +91 -0
  53. package/dist/core/shell/provenance.js +136 -0
  54. package/dist/core/shell/provenance.js.map +1 -0
  55. package/dist/core/shell/rater.d.ts +394 -163
  56. package/dist/core/shell/rater.js +536 -238
  57. package/dist/core/shell/rater.js.map +1 -1
  58. package/dist/core/shell/raterHealth.d.ts +101 -0
  59. package/dist/core/shell/raterHealth.js +121 -0
  60. package/dist/core/shell/raterHealth.js.map +1 -0
  61. package/dist/core/shell/raterModel.d.ts +17 -8
  62. package/dist/core/shell/raterModel.js +11 -8
  63. package/dist/core/shell/raterModel.js.map +1 -1
  64. package/dist/core/shell/rejection.d.ts +20 -5
  65. package/dist/core/shell/rejection.js +18 -4
  66. package/dist/core/shell/rejection.js.map +1 -1
  67. package/dist/core/toolDisplay.d.ts +42 -0
  68. package/dist/core/toolDisplay.js +31 -0
  69. package/dist/core/toolDisplay.js.map +1 -1
  70. package/dist/core/types.d.ts +134 -16
  71. package/dist/core/types.js.map +1 -1
  72. package/dist/providers/configurationPassthrough.d.ts +36 -6
  73. package/dist/providers/configurationPassthrough.js +40 -28
  74. package/dist/providers/configurationPassthrough.js.map +1 -1
  75. package/dist/providers/openrouter.js +36 -6
  76. package/dist/providers/openrouter.js.map +1 -1
  77. package/dist/providers/vertexai.js +21 -0
  78. package/dist/providers/vertexai.js.map +1 -1
  79. package/dist/utils/binaryOutputUtils.js +103 -21
  80. package/dist/utils/binaryOutputUtils.js.map +1 -1
  81. package/dist/utils/fileUtils.d.ts +7 -1
  82. package/dist/utils/fileUtils.js +17 -3
  83. package/dist/utils/fileUtils.js.map +1 -1
  84. package/dist/utils/globalConfigUtils.d.ts +5 -2
  85. package/dist/utils/globalConfigUtils.js +14 -3
  86. package/dist/utils/globalConfigUtils.js.map +1 -1
  87. package/dist/utils/vertexaiUtils.js +61 -3
  88. package/dist/utils/vertexaiUtils.js.map +1 -1
  89. package/package.json +4 -4
  90. package/schema/gsloth-config.schema.json +34 -0
@@ -1,4 +1,37 @@
1
- import type { RaterNegotiationContext, RaterNegotiationRound } from '#src/core/shell/rater.js';
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 outlives the consecutive counter** ([[EXT-108]]). A successful intervening
20
+ * tool call resets §5.3's counter and leaves the rounds standing, so the retry after it is rated
21
+ * as round 2 with the argument so far in view. §5.3's reset bounds a *stalled* negotiation, and a
22
+ * command run *because the rater asked for it* is that argument continuing rather than a new one:
23
+ * clearing the rounds there made the agent's compliance erase the record of the advice it was
24
+ * complying with, and demoted the retry to a round-1 rating whose explanation §5.1 withholds.
25
+ * The spec's warning about justification text accumulating across unbounded attempts is answered
26
+ * by the reachability bound rather than by the reset — the attempts are not unbounded.
27
+ * - **Only reaching a person clears the transcript** ({@link ShellNegotiationState.humanReached}),
28
+ * which is also the only thing that clears the reachability bound. The rounds a rating can see
29
+ * and the rejections counted against that bound are therefore the same set, always.
30
+ * - **The reachability bound is monotonic and a reset does not refill it.** See
31
+ * {@link MAX_REJECTIONS_BEFORE_HUMAN}.
32
+ */
33
+ import type { AlignmentRound } from '#src/core/shell/alignment.js';
34
+ import type { RaterNegotiationRound } from '#src/core/shell/rater.js';
2
35
  /**
3
36
  * §5.3 — **three CONSECUTIVE rejections end the negotiation** and escalate to the human: the agent
4
37
  * and the rater cannot agree, and that is a human's call, not a machine's.
@@ -18,10 +51,15 @@ export declare const MAX_CONSECUTIVE_REJECTIONS = 3;
18
51
  * add one without a fresh decision. **This is that decision (Andrew, EXT-29), and it answers a
19
52
  * different question than the one §5.3 asked.**
20
53
  *
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.
54
+ * §5.3's argument is about **safety**, and its core is untouched: every attempt is rated
55
+ * independently, the rater does not weaken with repetition, and every deterministic floor is
56
+ * recomputed from the RAW command each round, so no amount of argument unlocks one. What
57
+ * [[EXT-108]] changed is that a reset no longer clears the rounds with the counter, so the
58
+ * justification channel §5.1 opens at round 2 stays open across an approved call — deliberately,
59
+ * because the round after a call the rater itself asked for is the same argument continuing. That
60
+ * is the one channel able to LOWER an outcome, which is why what bounds it matters: this count,
61
+ * which no reset refills, is what ends an argument nobody is winning. Nothing here contradicts
62
+ * §5.3, and this bound must never be described as a security control.
25
63
  *
26
64
  * The question §5.3 never asked is **reachability**: does the human terminus fire at all? Under its
27
65
  * own reset predicate it does not. An agent that alternates one approved `ls` with one rejection
@@ -83,54 +121,43 @@ export declare const NEGOTIATION_USER_MESSAGE_RETENTION = 10;
83
121
  * | event | transcript | consecutive | since-human |
84
122
  * |---|---|---|---|
85
123
  * | a rejection ({@link recordRejection}) | append | +1 | +1 |
86
- * | the gate approved a call ({@link noteProgress}) | **cleared** | 0 | unchanged |
124
+ * | the gate approved a call ({@link noteProgress}) | **kept** | 0 | unchanged |
87
125
  * | a human was reached ({@link humanReached}) | cleared | 0 | **0** |
88
126
  * | the run halted ({@link humanReached}) | cleared | 0 | 0 |
89
127
  */
90
128
  export declare class ShellNegotiationState {
91
- /** §5.1's transcript: every round of the CURRENT negotiation, oldest first. */
129
+ /** §5.1's transcript: every round since a person was last involved, oldest first. */
92
130
  private rounds;
93
- /** §5.3's consecutive-rejection count. Shares the transcript's lifetime, by construction. */
131
+ /**
132
+ * §5.3's consecutive-rejection count. Reset by an approved call, which [[EXT-108]] stopped taking
133
+ * the rounds with it — so this is the shorter of the two lifetimes, not a shared one.
134
+ */
94
135
  private consecutive;
95
136
  /** The reachability bound's count. Cleared ONLY by {@link humanReached}. */
96
137
  private sinceHuman;
97
138
  /** §5.1's last user messages, oldest first, capped at {@link NEGOTIATION_USER_MESSAGE_RETENTION}. */
98
139
  private userMessages;
99
140
  /**
100
- * §5.1the 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"*.
141
+ * [[EXT-106]] §4.6whether {@link retainedUserMessages} may hand this session's human turns out
142
+ * as **the user's own words**. See {@link admitUserProvenance}; `false` until something positively
143
+ * establishes otherwise, which is the whole of its safety.
144
+ */
145
+ private userProvenanceAdmitted;
146
+ /**
147
+ * [[EXT-127]] — **the checker's own earlier rounds, for the alignment check about to be made.**
116
148
  *
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.
149
+ * The rounds it returns are the transcript's, with each round's alignment decision attached where
150
+ * the checker made one. What §5.1's withholding rules used to govern here is gone with the flat
151
+ * context they governed: the classifier now sees the command alone at every round, and the
152
+ * checker's context is assembled across message roles by
153
+ * {@link import('./alignment.js').buildAlignmentMessages} rather than handed over as a block.
125
154
  *
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.
155
+ * **The user's words are deliberately NOT here.** They come from
156
+ * {@link retainedUserMessages} the gated provenance channel and never from this method or
157
+ * from {@link noteUserMessages}' store. Routing them through a "context for the next call"
158
+ * accessor is exactly how the gate would come to be bypassed by a caller doing the obvious thing.
132
159
  */
133
- contextFor(justification?: string): RaterNegotiationContext;
160
+ alignmentRounds(): readonly AlignmentRound[];
134
161
  /**
135
162
  * Record a rejected round and decide whether another may be served.
136
163
  *
@@ -142,16 +169,24 @@ export declare class ShellNegotiationState {
142
169
  */
143
170
  recordRejection(round: RaterNegotiationRound): NegotiationVerdict;
144
171
  /**
145
- * §5.3 — a tool call the gate let through. Resets the consecutive counter **and clears the
146
- * transcript with it**; the reachability bound is deliberately untouched.
172
+ * §5.3 — a tool call the gate let through. Resets the consecutive counter and **nothing else**:
173
+ * the rounds stand, and so does the reachability bound.
147
174
  *
148
- * **What it must NOT truncate is the human's record of how hard the agent pushed.** The counter's
149
- * reset is §5.3's and is sound; the reader at an escalation is asking a different question, and
150
- * the answer to theirs is {@link NegotiationCounters.rejectionsSinceHuman}, which this leaves
151
- * standing. {@link renderNegotiationRows} reports that count rather than the surviving rounds, so
152
- * an argument this erased is still declared even though its rounds are gone; every erased round
153
- * itself survives whole in [[TUI-C27]]'s archive, which captures per RATING CALL and so is not
154
- * truncated by anything here.
175
+ * **The counter reset is §5.3's and is sound; taking the rounds with it was not** ([[EXT-108]]).
176
+ * The reset exists to bound a *stalled* negotiation an agent that went away, gathered
177
+ * information and came back better informed is making progress, not ping-pong. But the approved
178
+ * call is very often the agent doing exactly what the rejection told it to do, and clearing the
179
+ * rounds there makes that compliance erase the record of the advice: the retry is then rated with
180
+ * an empty transcript, which {@link contextFor} correctly reads as round 1 and so withholds the
181
+ * justification and the user messages from. **Measured**: the rater refused `git reset --hard`
182
+ * advising a stash first, the agent stashed, and the rater then advised a stash twice more —
183
+ * recommending a thing already done, because on the evidence it was given it could not know.
184
+ *
185
+ * **What replaces the clearing as a bound is the count this deliberately leaves standing.** Every
186
+ * appended round increments {@link NegotiationCounters.rejectionsSinceHuman}, which no reset
187
+ * refills, so a transcript still cannot grow past {@link MAX_REJECTIONS_BEFORE_HUMAN} rounds
188
+ * before {@link humanReached} clears the lot. A cap on the retained rounds would bound something
189
+ * that is already bounded, and would re-introduce the erasure on a longer fuse.
155
190
  *
156
191
  * **"Approved" is what the gate can observe, and it is not quite §5.3's "successful".** The
157
192
  * decision site sees whether a call was allowed to run, never whether it then exited zero — and
@@ -184,6 +219,48 @@ export declare class ShellNegotiationState {
184
219
  * thing they said, and it was said most recently.
185
220
  */
186
221
  noteUserMessages(messages: readonly string[]): void;
222
+ /**
223
+ * [[EXT-106]] §4.6 — **the user's own words, for the provenance carve-out that lifts the
224
+ * open-world floor.** A snapshot, oldest first, of the whole retained window.
225
+ *
226
+ * **This is deliberately NOT {@link contextFor}, and the difference is the point.** That function
227
+ * returns `userMessages: []` at round 1 by design — §5.1's *"round 1 sees the command alone"* —
228
+ * and round 1 is exactly the round the carve-out exists to act on: the user asks for a fetch, the
229
+ * agent proposes it, and there has been no rejection for a round 2 to exist. §5.1 bounds what the
230
+ * **rater** may see, and the floor is not the rater. Reading `contextFor()` here would make the
231
+ * carve-out fire only after the command had already been refused once, i.e. never in the case it
232
+ * was built for.
233
+ *
234
+ * The window is cumulative across the turns of a thread (capped and de-duplicated by
235
+ * {@link noteUserMessages}), so a host named in an earlier turn still carves a command proposed in
236
+ * a later one. {@link clear} drops it with the thread.
237
+ *
238
+ * **Empty until {@link admitUserProvenance} says otherwise**, which is why this is the provenance
239
+ * window rather than {@link noteUserMessages}'s store: the store also feeds §5.1's negotiation
240
+ * context, a different question with a different reader, and one that a command's fetched content
241
+ * is legitimately part of. What must never be answered *"yes, the human said this"* is this
242
+ * accessor, so the gate lives on it and defaults to refusing.
243
+ */
244
+ retainedUserMessages(): readonly string[];
245
+ /**
246
+ * [[EXT-106]] §4.6 — **declare whether this session's human turns are the user's own words**, and
247
+ * therefore whether {@link retainedUserMessages} may return them at all.
248
+ *
249
+ * A human message the product SYNTHESISED from content it fetched must never enter the provenance
250
+ * window: `review` and `pr` hand the agent a diff and a PR description that gaunt-sloth went and
251
+ * got, and the review prompt tells the agent to examine them. Reading the same bytes as the user's
252
+ * verbatim words would make the product contradict itself about identical input — and the words
253
+ * are written by whoever opened the pull request.
254
+ *
255
+ * **It defaults to `false` and the caller must positively establish otherwise**, so a surface that
256
+ * never calls this — or a future driver nobody has classified — floors exactly as it did before
257
+ * the carve-out existed. The one caller is
258
+ * {@link import('../GthAgentRunner.js').GthAgentRunner.init}, which decides it from the CLI verb
259
+ * ({@link import('../../config/shell-policy.js').commandCarriesUserProvenance}) and never from
260
+ * anything inside a message: those bytes are attacker-controlled, so a marker in them can be
261
+ * forged by the text it is supposed to classify.
262
+ */
263
+ admitUserProvenance(admitted: boolean): void;
187
264
  /**
188
265
  * §6 — the rounds to show the human, oldest first. A snapshot: the caller holds it across the
189
266
  * {@link humanReached} that immediately follows, and nothing it holds may change underneath it.
@@ -224,6 +301,132 @@ export interface NegotiationRow {
224
301
  voice: NegotiationVoice;
225
302
  text: string;
226
303
  }
304
+ /**
305
+ * §5.4 — **one round of the argument, handed to a surface the moment it happens.**
306
+ *
307
+ * The round travels raw rather than rendered, so each surface lays it out with
308
+ * {@link renderNegotiationRows} at its own terminal width — the same renderer, and the same
309
+ * voice-tagged rows, that the escalation prompt draws. A pre-rendered string would be the second
310
+ * renderer §5.4 exists to prevent, and it could not be bound to a width the runner cannot know.
311
+ */
312
+ export interface LiveNegotiationRound {
313
+ /** The round: the command proposed, the agent's justification, the rater's outcome and reason. */
314
+ round: RaterNegotiationRound;
315
+ /**
316
+ * **How many rounds of the transcript precede this one, counting from zero.**
317
+ *
318
+ * It is passed rather than derived because the two facts {@link renderNegotiationRows} marks —
319
+ * *this was rated on the command alone* (§5.1's round 1) and *this is round N* — are properties
320
+ * of a POSITION IN THE TRANSCRIPT, and a caller handing over one round at a time has taken the
321
+ * array's length away as a source for them. A live block that derived them would call every
322
+ * round the first one.
323
+ *
324
+ * For a rejection this is its own index: the k-th rejection is at `k - 1`, which numbers it
325
+ * `Round k`. For {@link agreed} it is the whole rejection count, because the approving round
326
+ * follows all of them — but it is NOT numbered from it; see {@link agreed}.
327
+ */
328
+ position: number;
329
+ /**
330
+ * [[TUI-C69]] §5.4 — **the rater agreed, so this round ends the argument rather than continuing
331
+ * it, and it takes no round number.**
332
+ *
333
+ * **A number here would be a number two different commands answer to.** The transcript holds
334
+ * rejections and nothing else — {@link ShellNegotiationState.recordRejection} is its only writer
335
+ * — so an approved call never joins it, and the next rejection takes the position this round was
336
+ * displayed at. Numbered, the live view would show two `Round 2` rows, and the escalation prompt
337
+ * (which renders that same transcript) would give `Round 2` to a third command again. The two
338
+ * views exist to be one account of one argument, so the number belongs to whatever the transcript
339
+ * says it belongs to, and the round that is not in the transcript is labelled instead of counted.
340
+ *
341
+ * Left unset for a rejection.
342
+ */
343
+ agreed?: boolean;
344
+ /**
345
+ * [[TUI-C69]] §5.4 — **the command that ended the argument is not the command that started it**,
346
+ * so the row says the rater ACCEPTED a revision rather than AGREED to what it refused.
347
+ *
348
+ * Both are ways an argument ends and both are held, because §5.3 counts either as progress. They
349
+ * are not the same claim, and only one of them can be made about a given command:
350
+ *
351
+ * - unset — the rater refused this exact command and has now passed it. *Agreed.*
352
+ * - set — the approved command is not one the transcript holds. The agent narrowed
353
+ * `git reset --hard origin/main` to `git reset --soft HEAD~2` and the rater passed the new one;
354
+ * or it went off to run something else entirely first. *Accepted*, which says what happened
355
+ * without claiming the rater ever argued about this command.
356
+ *
357
+ * The distinction is the whole of the wrong-output half of the defect this fixes: the label used
358
+ * to read `Agreed: ls src` over a command nobody had refused, which is a false statement about
359
+ * the auto-rater printed in the chrome of the thing asking the user to trust it.
360
+ *
361
+ * Meaningful only with {@link agreed}; ignored on a rejection, which is numbered.
362
+ */
363
+ revised?: boolean;
364
+ }
365
+ /**
366
+ * §5.4/§5.5 — **the surface that is showing this negotiation while it happens.**
367
+ *
368
+ * Wiring one is a surface saying *"I have a live display, and a person is looking at it"*, and it
369
+ * is what both halves of the visible negotiation are keyed on:
370
+ *
371
+ * - each round is handed to {@link round} as it is decided, rather than only at an escalation;
372
+ * - a negotiated approval is held for {@link NEGOTIATED_APPROVAL_COOLDOWN_MS} before it takes
373
+ * effect.
374
+ *
375
+ * **A surface that wires nothing neither renders nor sleeps**, which is the whole reason the two
376
+ * are one seam. An `exec` or CI run has nobody to show an approval to, so an 800 ms hold there
377
+ * would tax every headless run and every gate to display something to no one.
378
+ */
379
+ export interface NegotiationDisplay {
380
+ /** One round of the exchange, the moment the gate decided it. */
381
+ round(event: LiveNegotiationRound): void;
382
+ /**
383
+ * **The exchange is over** — a person was reached, the run halted, the turn ended, or a new one
384
+ * began. Called wherever {@link ShellNegotiationState.humanReached} is spent, and also at the end
385
+ * of a turn, so a surface holding the rounds drops them whenever there is no longer a live
386
+ * argument for them to be the state of.
387
+ *
388
+ * **The turn-end call is the one a converging negotiation depends on.** An escalation clears
389
+ * itself, because a person is reached; a negotiation that SUCCEEDS reaches nobody, so without it
390
+ * the rounds stay pinned through the idle period after the turn — the period in which the user is
391
+ * reading the result and typing the next thing.
392
+ *
393
+ * It matters most at an ESCALATION, where the prompt is about to render the whole argument
394
+ * itself: a live copy left standing above it puts the same exchange on an unscrollable screen
395
+ * twice, and the rows it spends are rows the dialog needs.
396
+ *
397
+ * **Optional, because an append-only surface cannot honour it.** The readline session prints each
398
+ * round as a line of scrollback; there is nothing to clear, and a surface that implements nothing
399
+ * simply keeps what it printed.
400
+ *
401
+ * *That is a limitation rather than a justification*, and the difference is visible at an
402
+ * escalation: the readline prompt re-renders the whole transcript, so a user who has just watched
403
+ * rounds 1-3 scroll past is shown rounds 1-3 again inside the prompt. Nothing here can prevent
404
+ * that — you cannot unprint scrollback — and the duplicate is the cheaper half of the trade,
405
+ * since the prompt must show what it is asking about. Recorded so the next reader meets the cost
406
+ * rather than the excuse.
407
+ */
408
+ end?(): void;
409
+ }
410
+ /**
411
+ * §5.5 — **the minimum interval a negotiated approval is visible before it takes effect.**
412
+ *
413
+ * It must never be described or relied upon as an opportunity to evaluate the command: nobody
414
+ * reads a command in 800 ms, and a design that assumed they did would be resting a safety property
415
+ * on a glance. What it buys is that the approving round exists on screen as its own event, rather
416
+ * than being drawn and overwritten by the tool's own output in the same frame.
417
+ *
418
+ * **It is NOT a guaranteed abort window on either surface**, and the claim must not be restored
419
+ * without the mechanism: on the event/TUI path an abort during the interval does stop the tool,
420
+ * but because LangGraph refuses an already-aborted signal rather than because this code re-checks
421
+ * one; on the plain/readline path no signal is threaded and the Esc handler is torn down before
422
+ * the interval begins, so the command runs. See `GthAgentRunner.showNegotiatedApproval`, which
423
+ * carries the full statement of what is true where.
424
+ *
425
+ * **A minimum visible interval, not a delay added to a finished decision.** The real window is
426
+ * longer at both ends — the rater call's own latency precedes it, and the tool takes time to start
427
+ * after it — so this bounds the short end of a window that mostly already exists.
428
+ */
429
+ export declare const NEGOTIATED_APPROVAL_COOLDOWN_MS = 800;
227
430
  /**
228
431
  * Terminal rows one element of a round — the command, the justification, the rater's answer — may
229
432
  * occupy on a screen before the rest of it is elided.
@@ -231,10 +434,10 @@ export interface NegotiationRow {
231
434
  * **MEASURED, and it is the height bound this block needs rather than a cap on rounds.** At 80
232
435
  * columns three rounds of paragraph-length argument cost 37 rows, of which one round was 12; the
233
436
  * whole prompt was 64 rows against a 20-row budget, so the human saw the command and the verdict
234
- * and neither the later rounds nor the menu line. A cap on the NUMBER of rounds would have changed
235
- * none of that — §5.3 already bounds the transcript at
236
- * {@link MAX_CONSECUTIVE_REJECTIONS} — because the cost is per row, not per round. Bounding each
237
- * element keeps every round structurally on the screen, which is what §5.6 calls the most important
437
+ * and neither the later rounds nor the menu line. A cap on the NUMBER of rounds does not answer
438
+ * that — the measured case was three rounds, exactly what {@link NEGOTIATION_MAX_ROUNDS_SHOWN}
439
+ * allows a screen, and it still cost 37 rows — because the cost is per row, not per round. Bounding
440
+ * each element keeps every round structurally on the screen, which is what §5.6 calls the most important
238
441
  * thing on it, and pays for it out of the tail of a paragraph the reader was never going to need in
239
442
  * full: the archive keeps every round whole ([[TUI-C27]]'s capture is per rating call).
240
443
  *
@@ -243,10 +446,12 @@ export interface NegotiationRow {
243
446
  */
244
447
  export declare const NEGOTIATION_MAX_ROWS_PER_ELEMENT = 2;
245
448
  /**
246
- * Rounds a screen shows, newest last. A backstop rather than today's binding constraint: §5.3
247
- * escalates at {@link MAX_CONSECUTIVE_REJECTIONS}, so a transcript reaching a human is never longer
248
- * than this and an unscrollable prompt must not acquire an unbounded section the moment that
249
- * number is raised. What is dropped is said out loud in the heading, which carries the true count.
449
+ * Rounds a screen shows, newest last. **Binding rather than a backstop, since [[EXT-108]]**: an
450
+ * approved call no longer clears the transcript, so an agent that makes progress between refusals
451
+ * reaches a person carrying every round since the last one up to
452
+ * {@link MAX_REJECTIONS_BEFORE_HUMAN} of them and an unscrollable prompt cannot grow a section
453
+ * that long. What is dropped is said out loud in the heading, which carries the true count, and the
454
+ * markers keyed on a position in the transcript stay keyed on it across the slice.
250
455
  */
251
456
  export declare const NEGOTIATION_MAX_ROUNDS_SHOWN = 3;
252
457
  /**
@@ -278,18 +483,21 @@ export declare const NEGOTIATION_MAX_ROUNDS_SHOWN = 3;
278
483
  * and each of these lied toward approving, at the moment a human was deciding whether to overrule
279
484
  * a refusal.
280
485
  *
281
- * - **`attempts` is the count, and it is not `rounds.length`.** §5.3 clears the transcript on an
282
- * approved call, so the rounds handed over are the attempts since the last *approval*, while the
283
- * fact the reader is weighing is how hard the agent pushed since the last *human* the caller's
284
- * `rejectionsSinceHuman`. A measured escalation attempted the same command five times and
285
- * rendered three. Omit it and this falls back to `rounds.length`, which is the honest reading of
286
- * a caller that has no better number rather than a claim that none were erased.
486
+ * - **`attempts` counts the argument; the rounds below it are a slice of that argument.** A screen
487
+ * shows at most {@link NEGOTIATION_MAX_ROUNDS_SHOWN} of them, so the heading has to report what
488
+ * happened rather than what fits, and the rounds have to be numbered from the true attempt
489
+ * number. The caller's `rejectionsSinceHuman` is that count. [[EXT-108]] brought it into
490
+ * agreement with `rounds.length` for a caller holding the whole transcript an approved call no
491
+ * longer erases rounds, and one event clears both so what this parameter still guards is the
492
+ * slice, plus the `Math.max` below, which stops a stale or smaller number making the block claim
493
+ * less argument than it is about to print. Omit it and this falls back to `rounds.length`, the
494
+ * honest reading of a caller that has no better number.
287
495
  * - **The last round IS the pending rating, not a prior one.** {@link
288
496
  * ShellNegotiationState.recordRejection} appends before either bound is tested, deliberately, so
289
497
  * that the rating being escalated is on the transcript the human sees. The old heading called
290
498
  * them all prior rounds, which both under-reported the argument and put the pending command on
291
499
  * the screen twice with nothing saying they were the same call.
292
- * - **The first round was rated on the command alone.** A cleared transcript IS the round-1 case
500
+ * - **The first round was rated on the command alone.** An EMPTY transcript IS the round-1 case
293
501
  * (see {@link ShellNegotiationState.contextFor}), so §5.1 withheld the justification and the user
294
502
  * messages from that rating — while {@link ShellNegotiationState.recordRejection} stores the
295
503
  * justification the agent supplied whatever the round. Printed unmarked, the round reads as a
@@ -311,6 +519,87 @@ export declare function renderNegotiationRows(rounds: readonly RaterNegotiationR
311
519
  * ShellNegotiationState.counters}, read BEFORE the escalation spends it.
312
520
  */
313
521
  attempts?: number;
522
+ /**
523
+ * [[TUI-C69]] §5.4 — **which block this is, and it changes three things.**
524
+ *
525
+ * - `escalation` (the default, and every caller that predates §5.4's live render) — the whole
526
+ * transcript, put in front of a person who is about to rule on it. It opens with the heading
527
+ * that counts the argument, and its last round IS the request being ruled on.
528
+ * - `live` — one round, drawn the moment it happened. **Nobody has been asked anything**, so
529
+ * the `(this request)` marker would be a lie, and a heading counting the argument would be
530
+ * re-printed on every round. The context sentence is drawn once, over the round that opens
531
+ * the exchange.
532
+ *
533
+ * What does NOT change is everything the rows say: the same labels, the same voices, the same
534
+ * width binding and the same row bound. The live view and the escalation prompt are one
535
+ * renderer precisely so the exchange a person watched and the exchange they are shown cannot
536
+ * be two different accounts of it.
537
+ */
538
+ mode?: 'escalation' | 'live';
539
+ /**
540
+ * [[TUI-C69]] §5.4 — the transcript position of `rounds[0]`, counting from zero, when the
541
+ * CALLER took the slice. Omitted, this block takes the slice itself and knows the answer.
542
+ *
543
+ * It exists because the markers below are properties of a position in the transcript rather
544
+ * than of a rating, and `rounds.length` stops being able to supply one the moment a caller
545
+ * hands over a single round.
546
+ */
547
+ from?: number;
548
+ /**
549
+ * [[TUI-C69]] §5.4 — **this live round is the rater AGREEING**, which ends the argument
550
+ * instead of being another attempt in it, so it is labelled rather than numbered.
551
+ *
552
+ * See {@link LiveNegotiationRound.agreed} for why it takes no number: the approved call never
553
+ * joins the transcript, so any number it were given is the one the next rejection then takes —
554
+ * and the escalation prompt, which renders the transcript, would hand that number to a
555
+ * different command. `live` only; the escalation block renders the transcript, which contains
556
+ * no approved rounds to label.
557
+ */
558
+ agreed?: boolean;
559
+ /**
560
+ * [[TUI-C69]] §5.4 — with {@link agreed}, whether the approved command is one the transcript
561
+ * never held, which changes the label from *Agreed* to *Accepted*. See
562
+ * {@link LiveNegotiationRound.revised}. `live` + `agreed` only.
563
+ */
564
+ revised?: boolean;
565
+ }): NegotiationRow[];
566
+ /**
567
+ * [[TUI-C69]] §5.4 — **the whole live exchange as a bounded screen**, for a surface that redraws a
568
+ * pinned region rather than appending to scrollback.
569
+ *
570
+ * ## Why this exists rather than the caller looping over {@link renderNegotiationRows}
571
+ *
572
+ * **A cap applied one round at a time is not a cap.** The escalation prompt hands the renderer its
573
+ * whole transcript, so `slice(-NEGOTIATION_MAX_ROUNDS_SHOWN)` bounds it; a live surface that
574
+ * rendered each round as it arrived and kept the output handed the renderer a ONE-element array
575
+ * every time, where that slice is an identity operation, and then accumulated the results. The
576
+ * bound was still in the code and applied to nothing. **Measured**: a nine-round argument cost 46
577
+ * rows at 80 columns that way, against 16 for the same argument in the prompt.
578
+ *
579
+ * That is not a tidiness problem. The Ink surface pins this inside a `flexShrink={0}` dock —
580
+ * [[TUI-C75]]'s governing constraint is that the dialog does not scroll, so rows spent here are
581
+ * taken from the conversation, and past a couple of dozen they push the input prompt off the
582
+ * bottom of an 80×24 terminal. Every other occupant of that dock is bounded; this was the one that
583
+ * was not.
584
+ *
585
+ * ## What it shows
586
+ *
587
+ * The newest {@link NEGOTIATION_MAX_ROUNDS_SHOWN} rounds, **unconditionally** — a screen is what
588
+ * this is for, so unlike {@link renderNegotiationRows} it does not let an absent `width` mean "show
589
+ * everything". A caller that wants the whole exchange unbounded wants the escalation renderer.
590
+ *
591
+ * Each round keeps the number of its own position in the transcript, so the numbers a watcher sees
592
+ * are the numbers the escalation prompt will use for the same rounds even after the earlier ones
593
+ * scroll out of this window — and an `agreed` round is labelled rather than numbered, for the
594
+ * reason {@link LiveNegotiationRound.agreed} gives.
595
+ *
596
+ * The context sentence is drawn **every time** rather than only over round one. Once the window
597
+ * slides, the round that carried it is gone, and the alternative is a bare `Round 7:` heading
598
+ * nothing — an unattributed command sitting in the chrome of a tool that is asking the user to
599
+ * trust it. One row is the right price for saying whose argument this is.
600
+ */
601
+ export declare function renderLiveNegotiationRows(rounds: readonly LiveNegotiationRound[], options?: {
602
+ width?: number;
314
603
  }): NegotiationRow[];
315
604
  /**
316
605
  * The same transcript as one string, for a consumer with no screen to lay it out on: §6.2's