@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,8 +1,9 @@
1
- import { type AllowlistCounts, type ApprovalRung, GthConfig, type McpAnnotationTrustChange, type McpAnnotationTrustView, type ResolvedApprovals, type ToolAnnotationHint } from '#src/config.js';
1
+ import { type AllowlistCounts, type ApprovalRefusal, type ApprovalRefusalLift, type ApprovalRung, GthConfig, type McpAnnotationTrustChange, type McpAnnotationTrustView, type ResolvedApprovals, type ToolAnnotationHint } from '#src/config.js';
2
2
  import { BaseCheckpointSaver } from '@langchain/langgraph';
3
- import { AgentResolvers, AgentStreamEvent, type AttackHaltCallback, GthAgentFactory, GthAgentInterface, GthCommand, GthRunStats, Message, StatusUpdateCallback, ToolApprovalCallback } from '#src/core/types.js';
3
+ import { AgentResolvers, AgentStreamEvent, type ApprovalOutcomeCallback, type AttackHaltCallback, GthAgentFactory, GthAgentInterface, GthCommand, GthRunStats, Message, StatusUpdateCallback, ToolApprovalCallback } from '#src/core/types.js';
4
4
  import { type ApprovalGrant } from '#src/core/approvals/grants.js';
5
5
  import { type ApprovalDecisionCapture } from '#src/core/shell/approvalCapture.js';
6
+ import { type NegotiationDisplay } from '#src/core/shell/negotiation.js';
6
7
  /**
7
8
  * EXT-114 — the page describing the `subagents` config key, pointed at rather than paraphrased in
8
9
  * the notice itself.
@@ -22,7 +23,15 @@ export interface GthAgentRunnerInitOptions {
22
23
  * the mode prompt (`readModePrompt`), the per-command approvals posture (`resolveApprovals`) and
23
24
  * the command-specific filesystem config, so a helper agent that must run on the chat prompt —
24
25
  * the `gth pr` change-requirements discovery agent — cannot borrow it to say which verb it serves.
25
- * Nothing but the wording of a notice reads this.
26
+ *
27
+ * **It is read for the wording of a notice, and for one safety decision.** [[EXT-106]] §4.6 asks
28
+ * whether this run's human messages are the user's own words, and answers it from
29
+ * `command ?? owningCommand` ({@link import('../config/shell-policy.js').commandCarriesUserProvenance}
30
+ * at the top of {@link GthAgentRunner#init}) — precisely because a command-less helper agent whose
31
+ * human turn is content the product FETCHED must not be classified by the absence of a verb. The
32
+ * key has to be out-of-band metadata like this field: a marker inside the message would be
33
+ * forgeable by the attacker-controlled text it is supposed to classify. Both that predicate and
34
+ * the provenance window itself fail closed, so leaving this unset never widens anything.
26
35
  */
27
36
  owningCommand?: GthCommand;
28
37
  /**
@@ -54,6 +63,12 @@ export declare class GthAgentRunner {
54
63
  * for non-interactive entrypoints (a scripted `exec` run with no TTY to prompt on).
55
64
  */
56
65
  private toolApprovalCallback;
66
+ /**
67
+ * [[EXT-150]] — consumer hook invoked once per human-answered approval, AFTER the answer has been
68
+ * recorded, carrying the lifetime it landed with. Set via {@link setApprovalOutcomeCallback}; when
69
+ * unset the outcome is simply not reported, which is where every surface was before this existed.
70
+ */
71
+ private approvalOutcomeCallback;
57
72
  /**
58
73
  * [[TUI-C68]] §6.1 — consumer hook invoked when the rater rates a command an `attack`, so an
59
74
  * interactive surface can show the red banner before the run ends. Set via
@@ -62,6 +77,22 @@ export declare class GthAgentRunner {
62
77
  * therefore keeps the halt rather than losing it.
63
78
  */
64
79
  private attackHaltCallback;
80
+ /**
81
+ * [[TUI-C69]] §5.4/§5.5 — **the surface showing this negotiation while it happens**, when one
82
+ * is. Set via {@link setNegotiationDisplay}.
83
+ *
84
+ * Its presence is the answer to *"is anyone watching?"*, and BOTH halves of the visible
85
+ * negotiation are keyed on it: the rounds are handed over as they are decided, and a negotiated
86
+ * approval is held on screen for {@link NEGOTIATED_APPROVAL_COOLDOWN_MS} before it takes effect.
87
+ *
88
+ * **`null` means neither happens, and that is the point.** An `exec` or CI run has nobody to
89
+ * show an approval to, so an 800 ms hold there would tax every headless run and every gate for a
90
+ * display that does not exist. Deliberately NOT keyed on {@link toolApprovalCallback}: that one
91
+ * answers *"is there a human to ASK"*, a different question with a different answer — a piped
92
+ * readline session can have one wired and no live display, and the §6.2 non-interactive path has
93
+ * a display and no one to ask.
94
+ */
95
+ private negotiationDisplay;
65
96
  /** The command the runner was initialized for; selects which `devTools` config applies. */
66
97
  private command;
67
98
  /**
@@ -90,12 +121,24 @@ export declare class GthAgentRunner {
90
121
  * `undefined` means no profile is configured and the rater uses the session model.
91
122
  */
92
123
  private raterModel;
124
+ /**
125
+ * [[EXT-127]] — the model the ALIGNMENT CHECKER runs on, when `approvals.alignmentChecker` (or, by
126
+ * its read-site default, `approvals.rater`) names an identity profile. Resolved ONCE at
127
+ * {@link init}, exactly as {@link raterModel} is; `undefined` means the session model.
128
+ */
129
+ private alignmentCheckerModel;
93
130
  /**
94
131
  * EXT-66 — how many rating calls this session gave up on. Counted so the notice can say "3 times
95
132
  * this session" rather than repeating an identical line, and so a silent drift toward
96
133
  * escalate-everything has a number attached to it.
97
134
  */
98
135
  private raterTimeouts;
136
+ /**
137
+ * [[EXT-82]] — the consecutive-failure tracker behind the session-level signal, per runner and
138
+ * therefore per session (the ACP surface runs several at once). See
139
+ * {@link import('#src/core/shell/raterHealth.js').RaterHealth}.
140
+ */
141
+ private readonly raterHealth;
99
142
  /**
100
143
  * EXT-71 §3.1/§6 — what the escalation menu granted at run time, for the life of THIS runner
101
144
  * instance: {@link ApprovalEntry} objects, never prefixes, and never anything from config (the
@@ -117,6 +160,19 @@ export declare class GthAgentRunner {
117
160
  */
118
161
  private persistedGrants;
119
162
  private persistedGrantsLoaded;
163
+ /**
164
+ * [[EXT-107]] — the persisted (`always`) **refusal** store, from
165
+ * `.gsloth/.gsloth-settings/shell-denylist.json`. The mirror of {@link persistedGrants}, and the
166
+ * reason the menu's most emphatic answer is no longer its most forgetful one.
167
+ *
168
+ * Null when the file cannot be loaded at all, in which case an `always` refusal degrades to a
169
+ * session one. **Not "a re-prompt next session, never an execution":** nothing in the unreadable
170
+ * file applies to any call, so next session a call it covered is left to the rest of the gate — it
171
+ * may be refused by another rule, it may be prompted for, or it may run without asking, under
172
+ * `bypass` or a matching saved allow. The user is told at load time.
173
+ */
174
+ private persistedDenials;
175
+ private persistedDenialsLoaded;
120
176
  /**
121
177
  * [[EXT-29]] §5 — the state of the agent↔rater negotiation at `auto`: the transcript, §5.3's
122
178
  * consecutive-rejection counter and the reachability bound. Instance-scoped for the same reason
@@ -142,6 +198,21 @@ export declare class GthAgentRunner {
142
198
  * Without a handler the runner rejects pending tool calls (see {@link toolApprovalCallback}).
143
199
  */
144
200
  setToolApprovalCallback(callback: ToolApprovalCallback | null): void;
201
+ /**
202
+ * [[EXT-150]] — register the handler that is told **what a human's answer actually landed as**.
203
+ * Pass `null` to clear.
204
+ *
205
+ * Separate from {@link setToolApprovalCallback} for the reason {@link setAttackHaltCallback} is
206
+ * separate: it is a different question asked at a different moment. The approval callback is
207
+ * consulted *before* anything is written and returns the human's REQUEST; the scope that survives
208
+ * is decided here afterwards, because [[EXT-149]] degrades an `always` whose write did not reach
209
+ * disk to the `session` answer it really is. A surface with no way to hear that can only confirm
210
+ * the key that was pressed — and core's own ERROR naming the unwritten file then contradicts it.
211
+ *
212
+ * It reports; it never decides. Nothing downstream reads it, so a surface that ignores it, or
213
+ * never wires it, changes no behaviour of the gate.
214
+ */
215
+ setApprovalOutcomeCallback(callback: ApprovalOutcomeCallback | null): void;
145
216
  /**
146
217
  * [[TUI-C68]] §6.1 — register the handler that shows the **attack banner**, the one way a human
147
218
  * gets past an `attack` verdict. Pass `null` to clear.
@@ -152,6 +223,17 @@ export declare class GthAgentRunner {
152
223
  * other surface keeps the halt (see {@link attackHaltCallback}).
153
224
  */
154
225
  setAttackHaltCallback(callback: AttackHaltCallback | null): void;
226
+ /**
227
+ * [[TUI-C69]] §5.4/§5.5 — **declare that this surface is showing the negotiation as it happens.**
228
+ * Pass `null` to clear.
229
+ *
230
+ * §5.4's requirement is not decoration: *"the spec's own justification for letting the agent
231
+ * argue with the rater at all is that a human can watch it, and an argument conducted in the dark
232
+ * is a different thing from one that can be interrupted."* Wiring this is a surface saying it
233
+ * has somewhere to draw that, which is also what makes §5.5's hold meaningful — see
234
+ * {@link negotiationDisplay} for why one seam carries both.
235
+ */
236
+ setNegotiationDisplay(display: NegotiationDisplay | null): void;
155
237
  /**
156
238
  * §6.1 — **the single seam between an `attack` verdict and the end of the run.** Both rating
157
239
  * paths — the §3.2 allow-match tripwire and the ordinary rater decision — go through here, so the
@@ -202,11 +284,56 @@ export declare class GthAgentRunner {
202
284
  */
203
285
  getAllowlistCounts(): AllowlistCounts;
204
286
  /**
205
- * CFG-27 — the session's deny entries for display: the declared `approvals.deny` entries
206
- * (rendered one line each) followed by whatever the escalation menu's *always reject* added at
207
- * run time. Both refuse a call, so both are shown.
287
+ * CFG-27/[[EXT-107]]**every refusal in force, and which of the three lists holds it**: the
288
+ * declared `approvals.deny` entries, the escalation menu's session-scoped refusals, and the ones
289
+ * saved to the project's deny file.
290
+ *
291
+ * The origins are kept apart rather than concatenated into one list of strings, because they have
292
+ * different lifetimes and different owners and only two of the three can be lifted from here.
293
+ * A merged list makes {@link liftRefusal} impossible to describe honestly.
294
+ *
295
+ * **This is the one list.** {@link liftRefusal} resolves its argument against exactly this
296
+ * sequence, so the number a user reads and the number they type cannot name different entries.
297
+ *
298
+ * Numbering is 1-based, and the order is config → saved → session: the entries a user cannot lift
299
+ * here come first and stay put, so the numbers of the ones they can are not reshuffled by a
300
+ * config edit between two renderings.
301
+ */
302
+ getRefusals(): ApprovalRefusal[];
303
+ /**
304
+ * [[EXT-107]] — the refusals in force with their entries attached, which is what
305
+ * {@link liftRefusal} needs and {@link getRefusals} renders. ONE builder, so the displayed order
306
+ * and the removal order are the same order by construction rather than by two functions agreeing.
307
+ *
308
+ * A saved refusal is held in both runtime stores ({@link recordDenial} writes both), so the
309
+ * session list is filtered against the saved one by entry identity — the same de-duplication
310
+ * {@link getGrants} does. A configured entry is NOT de-duplicated against them: it is a different
311
+ * thing with a different owner, and hiding it would let a lift report success while the config
312
+ * line went on refusing the call.
313
+ */
314
+ private refusalRecords;
315
+ /**
316
+ * [[EXT-107]] — **lift one refusal**, by its number in {@link getRefusals}. The escape hatch, and
317
+ * the reason persisting a refusal is safe to ship: a saved refusal the user cannot find or undo
318
+ * is a trap, and the person who hits it first is whoever pressed `[d]` by reflex and needed the
319
+ * command an hour later. Telling them to delete a file they have not been told exists is not an
320
+ * answer.
321
+ *
322
+ * A saved refusal is removed from the file AND from the in-memory store, because it is in both —
323
+ * dropping only the file would leave the call refused for the rest of the session by a rule the
324
+ * display no longer shows.
325
+ *
326
+ * **A configured entry is reported, never removed.** `approvals.deny` is something the user
327
+ * wrote; rewriting their config file out from under them is not a thing a session command may do,
328
+ * and silently no-oping would be worse. They are told where it lives.
329
+ *
330
+ * **`stillSaved` is the deletion that did not reach disk** ([[EXT-149]]). The file rewrite can
331
+ * fail after the in-memory removal has succeeded — a checkout that is not writable, a settings
332
+ * directory that has gone — and the entry then comes back in the next session. The store now
333
+ * answers that question ({@link PersistedApprovalGrants.remove}) instead of reporting every
334
+ * removal as landed, so the notice can stop promising *it will not come back*.
208
335
  */
209
- getDenylist(): string[];
336
+ liftRefusal(index: number): ApprovalRefusalLift;
210
337
  /**
211
338
  * §3/§4.7.4 — **the grants themselves**, for an approvals view that shows *what* was granted,
212
339
  * *when*, and *under which effective annotations*. The counterpart of {@link getAllowlistCounts},
@@ -424,6 +551,88 @@ export declare class GthAgentRunner {
424
551
  * whenever the shell gate is on, so §2.5's rule that the deny list survives `bypass` is untouched.)
425
552
  */
426
553
  private decideToolApproval;
554
+ /**
555
+ * [[EXT-29]] §5.3 / [[TUI-C69]] §5.4 — **a person was reached, so the exchange is over**: the
556
+ * gate's own transcript is spent and the surface showing it is told, on the same event.
557
+ *
558
+ * Every direct `humanReached()` in this class goes through here, which is what keeps the two from
559
+ * drifting: a new site that spent the transcript without telling the display would leave a
560
+ * finished argument standing on screen, and at an escalation it would put the same exchange on an
561
+ * unscrollable dialog twice — once live, once in the prompt about to render all of it.
562
+ */
563
+ private endNegotiation;
564
+ /**
565
+ * [[TUI-C69]] §5.4 — **take the finished argument off the screen WITHOUT spending the gate's
566
+ * transcript.**
567
+ *
568
+ * The two are separate on purpose. `humanReached()` clears the rounds *and* the reachability
569
+ * bound, which is correct only when a person was actually reached; calling it merely to tidy the
570
+ * panel would hand the agent a fresh {@link MAX_REJECTIONS_BEFORE_HUMAN} budget it had already
571
+ * spent, turning a display concern into a way to argue indefinitely. [[EXT-108]] made an approved
572
+ * call stop clearing the transcript for exactly this reason, so the tidy-up cannot be the thing
573
+ * that puts it back.
574
+ *
575
+ * Guarded, like {@link showNegotiationRound}: a surface that throws while clearing must never
576
+ * change what the gate decided, or become the reason a turn ends.
577
+ */
578
+ private clearNegotiationDisplay;
579
+ /**
580
+ * [[TUI-C69]] §5.4 — **tell the agent to forget the tool-call ids the rater bounced**, on every
581
+ * event that ends a turn's display state: a new turn, and `/clear`.
582
+ *
583
+ * The runner decides *when* because it owns the turn boundary; the agent holds the set because it
584
+ * owns the rendering. Fail-soft in the shape {@link resetRunStats} uses — an agent without the
585
+ * method (a test double, a renderer-less agent) is simply skipped, and a tone hint must never be
586
+ * the reason a turn fails to start.
587
+ */
588
+ private clearRaterClarifications;
589
+ /**
590
+ * [[TUI-C69]] §5.4 — **hand one round of the argument to the surface that is showing it**, the
591
+ * moment the gate decided it.
592
+ *
593
+ * The round travels raw. Every surface lays it out from the SAME rows the escalation prompt
594
+ * draws, at its own terminal width, so the rounds a person watches and the rounds they later rule
595
+ * on cannot be two different renderings of one exchange — and the rater's turns are yellow in
596
+ * both because the rows carry the voice rather than a colour.
597
+ *
598
+ * No-ops when no surface is watching, which is the §5.5 seam as well as this one.
599
+ */
600
+ private showNegotiationRound;
601
+ /**
602
+ * [[TUI-C69]] §5.4/§5.5 — **the rater agreeing is the last round of the argument, and it is held
603
+ * on screen before it takes effect.**
604
+ *
605
+ * Two things, in this order, because the order is the requirement: the approving round is drawn,
606
+ * and only then does the minimum visible interval run. A hold before the draw would be a pause
607
+ * over nothing.
608
+ *
609
+ * **It is a visibility pause, not a reading window**, and it must never be relied on as an
610
+ * opportunity to evaluate the command — nobody reads a command in 800 ms. What it buys is that
611
+ * the approving round is on screen as its own event instead of being overwritten by the tool
612
+ * output that follows it immediately.
613
+ *
614
+ * **What it is NOT, on either surface, is a guaranteed abort window — do not restore that claim
615
+ * without building the mechanism.** Stated precisely, because the previous wording asserted a
616
+ * mechanism that is not here and a comment like that stops the next reader checking:
617
+ *
618
+ * - **Event/TUI path.** An abort raised during the hold does end the run with the tool unrun, but
619
+ * *this code is not why*. The runner never re-checks the signal after the hold — it issues the
620
+ * approving resume regardless — and what stops the tool is that LangGraph refuses an
621
+ * already-aborted signal downstream. True today, and true by someone else's invariant.
622
+ * - **Plain/readline path.** Not true at all. `resolveToolInterrupts` threads no signal, and
623
+ * `waitForEscape` is armed inside `streamFromInput` and torn down before the hold begins — the
624
+ * hold happens *between* streams. Esc during it is not handled, so the command runs. The pause
625
+ * still buys the visibility above, which is why it is not conditioned on the surface.
626
+ *
627
+ * Making the affordance real by construction — checking the signal here, and threading one into
628
+ * the readline path so there is something to check — is deliberately left out of scope rather
629
+ * than half-built, since a window honoured on one surface and not the other is the more dangerous
630
+ * shape: it is what teaches the user the gesture that then silently fails.
631
+ *
632
+ * **Both halves are gated on a surface being wired**, so a headless `exec`/CI run neither draws
633
+ * nor sleeps and pays nothing. See {@link negotiationDisplay}.
634
+ */
635
+ private showNegotiatedApproval;
427
636
  /**
428
637
  * [[TUI-C27]] — {@link decideToolApprovalInner} with the record closed off on EVERY exit.
429
638
  *
@@ -435,6 +644,17 @@ export declare class GthAgentRunner {
435
644
  private recordedDecision;
436
645
  /** The decision itself; {@link decideToolApproval} wraps it with §5.3's reset. */
437
646
  private decideToolApprovalInner;
647
+ /**
648
+ * [[EXT-150]] — tell the surface what its own answer landed as, and hand the decision straight
649
+ * back.
650
+ *
651
+ * A one-liner returning the decision, exactly as {@link stage} is, and for the identical reason:
652
+ * the report happens ON the return that carries the decision rather than on the line above it, so
653
+ * no early return can later be inserted between the two and leave a surface waiting for an answer
654
+ * that never comes. There is one return out of {@link decideToolApprovalInner} after the callback
655
+ * is awaited, and this is it.
656
+ */
657
+ private reportOutcome;
438
658
  /**
439
659
  * [[TUI-C27]] — attribute the deciding stage and hand the decision straight back.
440
660
  *
@@ -449,6 +669,23 @@ export declare class GthAgentRunner {
449
669
  * only in what they do with the answer.
450
670
  */
451
671
  private rateCommand;
672
+ /**
673
+ * [[EXT-127]] — **one alignment check**, with the `user` role fed from the settled provenance
674
+ * channel and nothing else.
675
+ *
676
+ * **`provenance` is `ShellNegotiationState.retainedUserMessages()`, read ONCE by the caller and
677
+ * handed down**, exactly as §4.6's floor reads it. Three things are true of it and only of it: it
678
+ * is EMPTY until `admitUserProvenance` positively established that this session's human turns are
679
+ * the user's own words (so `review` and `pr`, which fold a fetched diff into a human message,
680
+ * contribute nothing); it is not `noteUserMessages`' raw store, which answers a different question
681
+ * for a different reader; and it is not `humanMessageTexts`, the unfiltered upstream. Reading any
682
+ * of the other three here would make the one place this design says provenance is structural the
683
+ * one place it is not.
684
+ *
685
+ * The prior rounds come from the transcript's own alignment decisions, so the checker meets its
686
+ * earlier turns as its own turns rather than as a quoted summary of them.
687
+ */
688
+ private checkAlignment;
452
689
  /**
453
690
  * EXT-58 (§4.3/§4.4) — the built-in tools already granted at the session's CURRENT rung, as
454
691
  * names plus one-line locally-authored descriptions, for the rater prompt.
@@ -488,6 +725,25 @@ export declare class GthAgentRunner {
488
725
  * knows how to reach the user.
489
726
  */
490
727
  private getPersistedGrants;
728
+ /**
729
+ * [[EXT-107]] — lazily load (once per instance) the persisted **refusal** store, the mirror of
730
+ * {@link getPersistedGrants}. Returns null when it cannot be loaded at all, in which case an
731
+ * `always` refusal degrades to `session`.
732
+ *
733
+ * **No `bypass` guard, and no v1 migration**, and the two are the same decision. The allow side
734
+ * skips the file at `bypass` so a session with the gate switched off neither reads nor *rewrites*
735
+ * the project's grants; the only thing that rewrites on load is the v1 `prefixes` migration, and
736
+ * turning it off here makes this store's load a pure read. That is what lets it be consulted at
737
+ * every rung — which it must be, because a refusal is resolved before the `bypass` return and a
738
+ * saved refusal that lapsed when the gate was relaxed would be worthless.
739
+ *
740
+ * It is also loaded by the `/approvals` DISPLAY, unlike its allow-side twin. The rule there — a
741
+ * display must not create the store in order to show it — is a rule about writing, and this load
742
+ * cannot write. The reason to break the symmetry is that the display is the escape hatch: a saved
743
+ * refusal a user cannot see is one they cannot lift, and a fresh session has made no gated call
744
+ * yet.
745
+ */
746
+ private getPersistedDenials;
491
747
  /**
492
748
  * §3.1/§4.7.4/§6 — **the grant a sticky choice would write for this call**, or `undefined` when
493
749
  * none is on offer. The one place that question is answered, so the menu's *this is what will be
@@ -560,7 +816,7 @@ export declare class GthAgentRunner {
560
816
  * - **`run_shell_command` arriving as a TOOL subject gets a tool entry** — a shell call whose
561
817
  * `command` argument cannot even be read. On the allow side that entry would auto-approve every
562
818
  * future unreadable shell call, which is why it is excluded there; as a refusal it stops the
563
- * shell tool for the session, and the dialog says so in the words the entry is written in.
819
+ * shell tool outright, and the dialog says so in the words the entry is written in.
564
820
  *
565
821
  * The one genuine exclusion is an **MCP call whose server could not be attributed**
566
822
  * ({@link toolGrantEntry} returns `null`): the grammar's `server` cannot be the empty string, so
@@ -570,16 +826,38 @@ export declare class GthAgentRunner {
570
826
  */
571
827
  private denyEntryFor;
572
828
  /**
573
- * §6 — record the menu's *always reject* choice, for the life of this runner instance.
574
- *
575
- * It lands in the same store `approvals.deny` entries are matched from ({@link approvalRuleLists}
576
- * concatenates the two), so a refusal the human made at the prompt and one they wrote in their
577
- * config are one list to the matcher and one list to `/approvals`.
578
- *
579
- * **Session-lifetime, and there is nothing else to choose.** There is no persisted deny file;
580
- * whether there should be is a question about a file users live with, not about this prompt. What
581
- * the surfaces must not do is say otherwise a confirmation promising a persistence that did not
582
- * happen is §6's *offered and then refused* with the evidence hidden.
829
+ * §6/[[EXT-107]] — record the menu's *always reject* choice at the given scope. `once` remembers
830
+ * nothing; `session` holds the refusal for the life of this runner instance; `always` additionally
831
+ * writes it to the project's deny file, so it is still in force after a restart.
832
+ *
833
+ * It lands in the same lists `approvals.deny` entries are matched from ({@link approvalRuleLists}
834
+ * concatenates the three), so a refusal the human made at the prompt and one they wrote in their
835
+ * config are one list to the matcher.
836
+ *
837
+ * **The recorded scope is derived from whether the entry REACHED THE FILE** ([[EXT-149]]) what
838
+ * {@link PersistedApprovalGrants.add} returns and not from whether the store was allowed to try.
839
+ * Those differ on a read-only checkout, where the file is simply absent, so the load did not fail,
840
+ * `canPersist()` is true, and every write throws: asking permission stamped `always` on an answer
841
+ * that reached no disk, and nothing told the user. **Do not describe that gap's cost as "one
842
+ * re-prompt next session":** the entry is not on disk, so next session it applies to nothing and
843
+ * the call is left to the rest of the gate — another rule may refuse it, it may be prompted for,
844
+ * or it may run without asking under `bypass` or a matching saved allow.
845
+ *
846
+ * **Two records, not one patched afterwards.** {@link ApprovalGrantStore.add} holds the very
847
+ * object it is handed whenever the grant carries no annotation snapshot — which every refusal
848
+ * does, since {@link denyEntryFor} builds an entry and never a snapshot — so stamping one object
849
+ * `always` and correcting it after the write would reach inside whatever {@link denyGrants}
850
+ * already holds and whatever the display renders from it. The persisted store is handed the
851
+ * `always` record it will hold if the write lands (and takes back if it does not); the session
852
+ * store is handed its own record, stamped from the answer.
853
+ *
854
+ * The persisted store is still told even when it cannot write, because telling it is what reports
855
+ * the refused or failed write to the user, and it declines to hold what it did not write.
856
+ *
857
+ * **It RETURNS the lifetime it recorded** ([[EXT-150]]) — the same value it stamps the record with,
858
+ * handed back rather than left for a caller to re-derive. The surface that asked has to say what
859
+ * happened, and re-deriving it there would put the "did the write land" question in two places
860
+ * that could come to disagree, which is the whole failure this and [[EXT-149]] are about.
583
861
  */
584
862
  private recordDenial;
585
863
  /**
@@ -589,6 +867,24 @@ export declare class GthAgentRunner {
589
867
  *
590
868
  * What is recorded was decided by {@link stickyGrantFor} and shown to the human before they
591
869
  * answered; this only stamps it with when and at what scope.
870
+ *
871
+ * **The stamped scope says whether this grant REACHED THE FILE**, the mirror of
872
+ * {@link recordDenial} and derived the same way ([[EXT-149]]): an `always` whose store is absent,
873
+ * whose file could not be read and so must not be rewritten ([[EXT-144]]), or whose write threw on
874
+ * a checkout nothing can write, is recorded as the `session` grant it actually is — and the store
875
+ * reports the last of those rather than swallowing it.
876
+ *
877
+ * Two records rather than one patched afterwards, for the reason argued in {@link recordDenial}:
878
+ * {@link ApprovalGrantStore.add} holds the object it is handed for a grant with no annotation
879
+ * snapshot — every shell grant — so a scope corrected after the write would be corrected inside
880
+ * {@link sessionGrants}.
881
+ *
882
+ * The store is still loaded only for `always`, unchanged: a display must not create the file in
883
+ * order to show it, and a `session` grant has no business opening it.
884
+ *
885
+ * **It RETURNS the lifetime it recorded** ([[EXT-150]]), the mirror of {@link recordDenial} and for
886
+ * the same reason: the surface's confirmation is written from this value rather than from the key
887
+ * the human pressed.
592
888
  */
593
889
  private recordApproval;
594
890
  /**