@vincemakes/kiso-code 0.1.39 → 0.1.41
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.
- package/dist/chat.d.ts +11 -7
- package/dist/chat.js +170 -39
- package/dist/index.js +36 -2
- package/dist/resume.js +1 -1
- package/dist/state.d.ts +18 -1
- package/dist/state.js +9 -0
- package/dist/trust-ui.d.ts +49 -14
- package/dist/trust-ui.js +168 -54
- package/package.json +9 -8
package/dist/chat.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* estimates. All bodies moved verbatim from index.ts.
|
|
6
6
|
*/
|
|
7
7
|
import { type RunUsage } from "@vincemakes/kiso-tui";
|
|
8
|
-
import type { AgentSession } from "@vincemakes/kiso-runtime";
|
|
8
|
+
import type { AgentSession, Run } from "@vincemakes/kiso-runtime";
|
|
9
9
|
import { type LineInput } from "./state.js";
|
|
10
10
|
/**
|
|
11
11
|
* The ergonomics batch C8 — the /compact auto-trigger, OPT-IN (default off: only an
|
|
@@ -38,12 +38,16 @@ export declare function estimateCtxRatio(session: AgentSession): number;
|
|
|
38
38
|
export declare function startStatusSpinner(onTick: (glyph: string) => void): () => void;
|
|
39
39
|
/**
|
|
40
40
|
* Consume a run, answering approval pauses as they arrive. `resumeMode`
|
|
41
|
-
* marks a session.resume() continuation.
|
|
42
|
-
*
|
|
43
|
-
*
|
|
41
|
+
* marks a session.resume() continuation. `faux` picks the status line's
|
|
42
|
+
* form. W22: EVERY user_input event renders its UserMessage chip in the
|
|
43
|
+
* body — the v2a double-echo filter is retired (the transient input-row
|
|
44
|
+
* echo is UI, the chip is the record; the momentary double-render is
|
|
45
|
+
* the design's explicit point).
|
|
44
46
|
*/
|
|
45
|
-
export declare function consumeRun(session: AgentSession, run:
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
export declare function consumeRun(session: AgentSession, run: Run, input: LineInput, turnNo: number, faux: boolean, statusCb: ((usage: RunUsage, ctxRatio: number) => void) | null,
|
|
48
|
+
/** W21: the amend words ("Yes + feedback") ride the NEXT user turn —
|
|
49
|
+
* threaded from chat's submitTurn; absent in the recovery flow
|
|
50
|
+
* (resume) where a dropped amend is noticed instead. */
|
|
51
|
+
submitTurn?: (line: string) => void): Promise<import("@vincemakes/kiso-core").Event | undefined>;
|
|
48
52
|
/** Interactive REPL: stream events, pause for approvals, Ctrl+C aborts. */
|
|
49
53
|
export declare function chat(session: AgentSession, faux: boolean, input: LineInput, autoCompact?: AutoCompact): Promise<void>;
|
package/dist/chat.js
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
* estimates. All bodies moved verbatim from index.ts.
|
|
6
6
|
*/
|
|
7
7
|
import { readFileSync } from "node:fs";
|
|
8
|
-
import { escapeTerminal, kUnit, palette, renderEvent, renderRecap } from "@vincemakes/kiso-tui";
|
|
8
|
+
import { escapeTerminal, kUnit, palette, renderEvent, renderRecap, toolTarget, } from "@vincemakes/kiso-tui";
|
|
9
9
|
import { editFileDiff, writeFileDiff } from "@vincemakes/kiso-tui";
|
|
10
10
|
import { canonicalTargetPath } from "@vincemakes/kiso-tools-node";
|
|
11
11
|
import { dispatch } from "./dispatch.js";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
12
|
+
import { agentModel, body, bodyLog, configuredWindow, dock } from "./state.js";
|
|
13
|
+
import { addDontAskAgainRule, askPanel, fixHintFor, pendingAsk, resolveUncertains } from "./trust-ui.js";
|
|
14
14
|
import { FauxExhaustionError, failOnFauxExhaustion } from "./faux-glue.js";
|
|
15
15
|
import { MODES, getMode, setMode } from "./mode.js";
|
|
16
16
|
/** B area: default context window for the ~ctx estimate (config overridable). */
|
|
@@ -93,6 +93,40 @@ function approvalDiff(name, input) {
|
|
|
93
93
|
return null; // never let the diff break the approval
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
+
/** W21 — the panel view for a permission_requested: the rule line (the
|
|
97
|
+
* why-asked speaker + the §3.5 fix hint), the toolTarget title, the
|
|
98
|
+
* "▸ run paused" status, and the ALWAYS-verbose args. */
|
|
99
|
+
function approvalView(name, ev) {
|
|
100
|
+
const speaker = ev.speaker ?? "kiso";
|
|
101
|
+
const input = ev.input ?? {};
|
|
102
|
+
// exactOptionalPropertyTypes: the hint is OMITTED when the speaker has
|
|
103
|
+
// no fix (mode:accept-edits, shell in default) — never `hint: undefined`.
|
|
104
|
+
const hint = fixHintFor(speaker, name);
|
|
105
|
+
return {
|
|
106
|
+
flavor: "approval",
|
|
107
|
+
name,
|
|
108
|
+
title: toolTarget(name, input),
|
|
109
|
+
speaker,
|
|
110
|
+
...(hint !== undefined ? { hint } : {}),
|
|
111
|
+
statusText: "▸ run paused",
|
|
112
|
+
args: approvalArgs(name, input),
|
|
113
|
+
fallbackQuestion: `approve ${escapeTerminal(name)}? (y/n) `,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** The panel's ALWAYS-verbose args: edit_file/write_file → the full ±
|
|
117
|
+
* diff (the tool cell's capped copy never reaches the panel — the
|
|
118
|
+
* human approves the WHOLE change), shell → the full command line,
|
|
119
|
+
* anything else → the pretty-printed JSON. Nothing that is asked for
|
|
120
|
+
* approval is ever truncated. */
|
|
121
|
+
function approvalArgs(name, input) {
|
|
122
|
+
if (name === "edit_file" || name === "write_file") {
|
|
123
|
+
return { kind: "diff", diff: approvalDiff(name, input)?.lines ?? null };
|
|
124
|
+
}
|
|
125
|
+
if (name === "shell") {
|
|
126
|
+
return { kind: "text", lines: [String(input.command ?? "")] };
|
|
127
|
+
}
|
|
128
|
+
return { kind: "text", lines: JSON.stringify(input, null, 2).split("\n") };
|
|
129
|
+
}
|
|
96
130
|
/**
|
|
97
131
|
* The ergonomics batch C5 — the translation layer: the tui renders its OWN data shape
|
|
98
132
|
* (RenderInput, zero kiso-core imports); the CLI translates its Event
|
|
@@ -165,13 +199,26 @@ function toRenderInput(ev) {
|
|
|
165
199
|
}
|
|
166
200
|
/**
|
|
167
201
|
* Consume a run, answering approval pauses as they arrive. `resumeMode`
|
|
168
|
-
* marks a session.resume() continuation.
|
|
169
|
-
*
|
|
170
|
-
*
|
|
202
|
+
* marks a session.resume() continuation. `faux` picks the status line's
|
|
203
|
+
* form. W22: EVERY user_input event renders its UserMessage chip in the
|
|
204
|
+
* body — the v2a double-echo filter is retired (the transient input-row
|
|
205
|
+
* echo is UI, the chip is the record; the momentary double-render is
|
|
206
|
+
* the design's explicit point).
|
|
171
207
|
*/
|
|
172
|
-
export async function consumeRun(session, run, input, turnNo, faux,
|
|
208
|
+
export async function consumeRun(session, run, input, turnNo, faux, statusCb,
|
|
209
|
+
/** W21: the amend words ("Yes + feedback") ride the NEXT user turn —
|
|
210
|
+
* threaded from chat's submitTurn; absent in the recovery flow
|
|
211
|
+
* (resume) where a dropped amend is noticed instead. */
|
|
212
|
+
submitTurn) {
|
|
173
213
|
let last;
|
|
174
214
|
let usage = { in: null, out: null, cache: null, known: false };
|
|
215
|
+
// R-C item 4: the per-turn cache miss — the overlap with the previous
|
|
216
|
+
// prompt that SHOULD have been cached but was re-sent uncached:
|
|
217
|
+
// missed = min(prevIn, in) − cacheRead. Below the 1024-token floor
|
|
218
|
+
// (Anthropic's minimum cacheable block) it is noise — not surfaced.
|
|
219
|
+
const CACHE_MISS_FLOOR = 1024;
|
|
220
|
+
let prevIn = null;
|
|
221
|
+
let missed = null;
|
|
175
222
|
// v3 §02: the recap line derives ENTIRELY from the local event stream
|
|
176
223
|
// (zero tokens) — wall seconds, tool/edit counts, usage, ctx left.
|
|
177
224
|
const turnStart = Date.now();
|
|
@@ -194,17 +241,6 @@ export async function consumeRun(session, run, input, turnNo, faux, liveInput, s
|
|
|
194
241
|
else if (thinkingSince === null) {
|
|
195
242
|
thinkingSince = Date.now();
|
|
196
243
|
}
|
|
197
|
-
// v2a (the double echo): the interactive echo was already rendered by the
|
|
198
|
-
// input source — rendering the event again is the double echo.
|
|
199
|
-
// v2b: DOCKED — the echo lives in the input row (H), NOT the body;
|
|
200
|
-
// the body render is the ONLY visible copy of the sent line.
|
|
201
|
-
if (ev.type === "user_input" &&
|
|
202
|
-
liveInput !== null &&
|
|
203
|
-
liveInput.current === (typeof ev.content === "string" ? ev.content : "") &&
|
|
204
|
-
process.stdin.isTTY &&
|
|
205
|
-
!dock.active) {
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
244
|
// v2d: EVERY event only mutates a cell — the Body is the single
|
|
209
245
|
// writer of the scroll region, so interleaving is impossible by
|
|
210
246
|
// construction (ADR-0040).
|
|
@@ -260,10 +296,18 @@ export async function consumeRun(session, run, input, turnNo, faux, liveInput, s
|
|
|
260
296
|
case "text_end":
|
|
261
297
|
body.textEnd();
|
|
262
298
|
break;
|
|
263
|
-
case "usage":
|
|
299
|
+
case "usage": {
|
|
264
300
|
usage = { in: ev.inputTokens, out: ev.outputTokens, cache: ev.cacheRead, known: ev.known };
|
|
301
|
+
// R-C item 4: min(prevIn, in) is the part that could have been
|
|
302
|
+
// cached; what cacheRead did NOT cover is the miss.
|
|
303
|
+
if (usage.in !== null && usage.cache !== null && prevIn !== null) {
|
|
304
|
+
const m = Math.min(prevIn, usage.in) - usage.cache;
|
|
305
|
+
missed = m > CACHE_MISS_FLOOR ? m : null;
|
|
306
|
+
}
|
|
307
|
+
prevIn = usage.in;
|
|
265
308
|
statusCb?.(usage, estimateCtxRatio(session));
|
|
266
309
|
break;
|
|
310
|
+
}
|
|
267
311
|
case "uncertain_pending":
|
|
268
312
|
// ruling #12 (ADR-0038): the ⚠ line is pure INFORMATION now — the
|
|
269
313
|
// approval chain guards retries, and the human question belongs
|
|
@@ -276,18 +320,71 @@ export async function consumeRun(session, run, input, turnNo, faux, liveInput, s
|
|
|
276
320
|
// v2e: the mini-diff for edit/write at the approval moment —
|
|
277
321
|
// the human sees the change BEFORE deciding (auto-allowed tools
|
|
278
322
|
// skip the diff: nobody is looking).
|
|
323
|
+
// W21: the PANEL replaces the line question — the bounded block
|
|
324
|
+
// with the ALWAYS-verbose args (the full diff / command / JSON,
|
|
325
|
+
// nothing the human approves is ever cut) and the numbered
|
|
326
|
+
// options. The verdict maps to the session approvals:
|
|
327
|
+
// - bare No → approve(false) FIRST (the denial settles the
|
|
328
|
+
// request), THEN run.abort() — the run's aborted terminal
|
|
329
|
+
// closes the cell;
|
|
330
|
+
// - No+words → approve(false, words) — the words become the
|
|
331
|
+
// tool_result, the run continues;
|
|
332
|
+
// - Yes+amend → approve(true), the words ride the NEXT turn;
|
|
333
|
+
// - esc → cancel, the conservative denial.
|
|
279
334
|
const name = ev.name;
|
|
280
335
|
body.toolApproval(ev.callId, approvalDiff(name, ev.input ?? {}));
|
|
281
336
|
const decisionId = ev.decisionId;
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
337
|
+
const verdict = await askPanel(input, approvalView(name, ev));
|
|
338
|
+
switch (verdict.action) {
|
|
339
|
+
case "cancel": {
|
|
340
|
+
// round 10: a cancellation is a CONSERVATIVE denial,
|
|
341
|
+
// explicitly distinguished from the user typing "n".
|
|
342
|
+
body.notice("[approval cancelled — treated as a denial]");
|
|
343
|
+
await session.approve(decisionId, false);
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
case "allow": {
|
|
347
|
+
await session.approve(decisionId, true);
|
|
348
|
+
if (verdict.reason.trim() !== "") {
|
|
349
|
+
if (submitTurn !== undefined)
|
|
350
|
+
submitTurn(verdict.reason);
|
|
351
|
+
else
|
|
352
|
+
body.notice("[amend words dropped — the recovery flow has no live prompt]");
|
|
353
|
+
}
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
case "allow-rule": {
|
|
357
|
+
// R3: the don't-ask-again extension is ALLOW-ONLY (never
|
|
358
|
+
// emits deny or ask — the mode and safe-defaults moats
|
|
359
|
+
// keep their teeth); the generated file is human-editable
|
|
360
|
+
// and human-deletable — that IS the revocation path.
|
|
361
|
+
await session.approve(decisionId, true);
|
|
362
|
+
await addDontAskAgainRule(verdict.rule);
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
case "deny": {
|
|
366
|
+
if (verdict.reason.trim() !== "") {
|
|
367
|
+
// No+words — the words become the tool_result; the
|
|
368
|
+
// run continues with the model seeing the denial.
|
|
369
|
+
await session.approve(decisionId, false, verdict.reason);
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
// bare No — the denial settles the pause FIRST, then
|
|
373
|
+
// the run aborts.
|
|
374
|
+
await session.approve(decisionId, false);
|
|
375
|
+
run.abort();
|
|
376
|
+
}
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
289
379
|
}
|
|
290
|
-
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
case "permission_decided": {
|
|
383
|
+
// A5: the verdict binds INTO the tool cell — the aggregated
|
|
384
|
+
// head row (name + status + decidedBy in ONE row), never a
|
|
385
|
+
// free-standing ` approved` orphan. The render.ts case stays
|
|
386
|
+
// for the PIPE path (the transcript is the raw event stream).
|
|
387
|
+
body.toolVerdict(ev.callId ?? "", ev.decision, ev.decidedBy, ev.reason);
|
|
291
388
|
break;
|
|
292
389
|
}
|
|
293
390
|
case "terminal": {
|
|
@@ -300,11 +397,21 @@ export async function consumeRun(session, run, input, turnNo, faux, liveInput, s
|
|
|
300
397
|
// the commit loop folds the quiet turn's held cells first (the
|
|
301
398
|
// fold line lands above the recap, natural cell order).
|
|
302
399
|
body.endTurn(Math.round(thoughtSeconds));
|
|
400
|
+
// D4: the max_tokens truncation is named, never silent — the
|
|
401
|
+
// honest notice rides after the partial answer, before the
|
|
402
|
+
// recap (the truncation-guard philosophy: the cut is visible
|
|
403
|
+
// in the scrollback, the model's own text intact).
|
|
404
|
+
if (ev.outcome.kind === "max_tokens") {
|
|
405
|
+
body.notice('┌ answer truncated at max_tokens — say "continue" to finish');
|
|
406
|
+
}
|
|
303
407
|
bodyLog(renderRecap({
|
|
304
408
|
seconds: Math.round((Date.now() - turnStart) / 1000),
|
|
305
409
|
tools: toolCount,
|
|
306
410
|
edits: editCount,
|
|
307
411
|
usage,
|
|
412
|
+
// R-C item 4: only an above-floor miss is surfaced —
|
|
413
|
+
// the recap gains "· miss N" on the cache segment.
|
|
414
|
+
...(missed !== null ? { missed } : {}),
|
|
308
415
|
ctxLeftPct: Number.isFinite(ratio) ? (1 - ratio) * 100 : null,
|
|
309
416
|
// W19: under plan the recap becomes the way-forward row
|
|
310
417
|
// (the /mode hints are the mode's exits).
|
|
@@ -338,10 +445,6 @@ export async function chat(session, faux, input, autoCompact) {
|
|
|
338
445
|
let cancelled = false;
|
|
339
446
|
const turn = (text) => new Promise((resolve, reject) => {
|
|
340
447
|
queued = Math.max(0, queued - 1); // a queued turn starts
|
|
341
|
-
// v2a: the echo filter compares the user_input event against THIS
|
|
342
|
-
// turn's own input — lines that arrive ahead of their turn (piped
|
|
343
|
-
// bursts, queued replays) must not overwrite the reference.
|
|
344
|
-
liveInput.current = text;
|
|
345
448
|
const run = session.run(text);
|
|
346
449
|
currentRun = run;
|
|
347
450
|
turnNo += 1;
|
|
@@ -357,7 +460,7 @@ export async function chat(session, faux, input, autoCompact) {
|
|
|
357
460
|
(async () => {
|
|
358
461
|
let last;
|
|
359
462
|
try {
|
|
360
|
-
last = await consumeRun(session, run, input, myTurn, faux,
|
|
463
|
+
last = await consumeRun(session, run, input, myTurn, faux, statusCb, submitTurn);
|
|
361
464
|
stopSpinner();
|
|
362
465
|
paintIdle();
|
|
363
466
|
currentRun = null;
|
|
@@ -440,12 +543,14 @@ export async function chat(session, faux, input, autoCompact) {
|
|
|
440
543
|
// B area: user-turn counter for the status line. /last and /think read
|
|
441
544
|
// the body (the ToolCell / ThinkingCell final states).
|
|
442
545
|
let turnNo = 0;
|
|
443
|
-
//
|
|
444
|
-
//
|
|
445
|
-
const liveInput = { current: null };
|
|
446
|
-
// v2c: turns submitted while another runs are QUEUED on the chain — the
|
|
447
|
-
// live count rides the status bar (+N queued).
|
|
546
|
+
// v2c: turns submitted while another runs are QUEUED on the chain —
|
|
547
|
+
// the live count rides the status bar (+N queued).
|
|
448
548
|
let queued = 0;
|
|
549
|
+
// W22: the pending turns — the LIVE slots the chips + the ↑/esc pop
|
|
550
|
+
// read (the dock renders the lines, the editor pops the last one).
|
|
551
|
+
// A slot leaves the queue when its turn STARTS or when the user
|
|
552
|
+
// pops it (cancelled — the chain segment skips it).
|
|
553
|
+
const pendingTurns = [];
|
|
449
554
|
// v2b: the live status bar (docked only). Modes: /mode switches repaint
|
|
450
555
|
// it immediately through paintStatus (the last turn stats are kept).
|
|
451
556
|
// v3 §03: the status bar has TWO states. Idle: the mode is ALWAYS
|
|
@@ -478,9 +583,35 @@ export async function chat(session, faux, input, autoCompact) {
|
|
|
478
583
|
paintRunning();
|
|
479
584
|
};
|
|
480
585
|
const submitTurn = (line) => {
|
|
586
|
+
const slot = { line, cancelled: false };
|
|
587
|
+
pendingTurns.push(slot);
|
|
481
588
|
queued += 1;
|
|
482
|
-
chainRef.current = chainRef.current.then(() =>
|
|
589
|
+
chainRef.current = chainRef.current.then(() => {
|
|
590
|
+
if (slot.cancelled)
|
|
591
|
+
return; // the pop already dropped it — no double count
|
|
592
|
+
const idx = pendingTurns.indexOf(slot);
|
|
593
|
+
if (idx >= 0)
|
|
594
|
+
pendingTurns.splice(idx, 1); // the chip leaves when the turn STARTS
|
|
595
|
+
return turn(line);
|
|
596
|
+
});
|
|
597
|
+
};
|
|
598
|
+
// W22: the ↑/esc pop — the LAST queued slot leaves the queue
|
|
599
|
+
// (cancelled + spliced + counted down); null when the queue is
|
|
600
|
+
// empty. The chain segment skips the cancelled slot, so the popped
|
|
601
|
+
// message NEVER runs — it returns to the editor instead.
|
|
602
|
+
const popQueue = () => {
|
|
603
|
+
const slot = pendingTurns[pendingTurns.length - 1];
|
|
604
|
+
if (slot === undefined)
|
|
605
|
+
return null;
|
|
606
|
+
slot.cancelled = true;
|
|
607
|
+
pendingTurns.pop();
|
|
608
|
+
queued = Math.max(0, queued - 1);
|
|
609
|
+
return slot.line;
|
|
483
610
|
};
|
|
611
|
+
// W22: the visibility invariant's binds — the dock renders the
|
|
612
|
+
// pending chips (+N queued), the editor routes the pop keys.
|
|
613
|
+
dock.bindQueue(() => pendingTurns.map((s) => s.line));
|
|
614
|
+
input.bindQueue(() => pendingTurns.map((s) => s.line), popQueue);
|
|
484
615
|
const dispatchCtx = {
|
|
485
616
|
session,
|
|
486
617
|
input,
|
|
@@ -528,7 +659,7 @@ export async function chat(session, faux, input, autoCompact) {
|
|
|
528
659
|
const recoveryRun = session.resume();
|
|
529
660
|
currentRun = recoveryRun;
|
|
530
661
|
turnNo += 1;
|
|
531
|
-
const last = await consumeRun(session, recoveryRun, input, turnNo, faux,
|
|
662
|
+
const last = await consumeRun(session, recoveryRun, input, turnNo, faux, statusCb, submitTurn);
|
|
532
663
|
currentRun = null;
|
|
533
664
|
failOnFauxExhaustion(last, faux, input);
|
|
534
665
|
maybeAutoCompact(); // the ergonomics batch C8: the recovery run ended too — same check (awaited by the exit re-await)
|
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ import { createAgent, disposeExtensions, loadExtensions, loadProjectExtensions,
|
|
|
31
31
|
import { createFauxProvider } from "@vincemakes/kiso-evals";
|
|
32
32
|
import { createCodingTools } from "@vincemakes/kiso-tools-node";
|
|
33
33
|
import { MODES, modeExtensions, modeFromEnv, modeSystemPrompt, setMode } from "./mode.js";
|
|
34
|
-
import { body, bodyLog, currentFaux, dock, extensionsDir, loadedExtensions, mergedConfig, mergedTempPaths, projectExtensions, sessionsDir, setAgentModel, setBody, setConfigModels, setConfiguredWindow, setCurrentFaux, setCurrentModelName, setExtensionLists, setMergedConfig, userExtensions, VERSION } from "./state.js";
|
|
34
|
+
import { body, bodyLog, currentFaux, dock, extensionsDir, loadedExtensions, mergedConfig, mergedTempPaths, projectExtensions, sessionsDir, setAgentModel, setBody, setConfigModels, setConfiguredWindow, setCurrentAgentExtensions, setCurrentFaux, setCurrentModelName, setExtensionLists, setMergedConfig, userExtensions, VERSION } from "./state.js";
|
|
35
35
|
import { interactivePrompt, resolveProjectTrust } from "./trust-ui.js";
|
|
36
36
|
import { fauxSkip, readFauxScript } from "./faux-glue.js";
|
|
37
37
|
import { autoCompactFromEnv, chat, contextWindowTokens } from "./chat.js";
|
|
@@ -81,6 +81,19 @@ function readlineInput(rl) {
|
|
|
81
81
|
/* the rl.question stays pending; the settled branch re-emits
|
|
82
82
|
* the answer as a new line. */
|
|
83
83
|
},
|
|
84
|
+
// W21: readline is never asked — askPanel's non-TTY branch
|
|
85
|
+
// auto-denies before any panel opens (the pipe path).
|
|
86
|
+
panelAsk() {
|
|
87
|
+
/* unreachable — non-TTY asks auto-deny in askPanel */
|
|
88
|
+
},
|
|
89
|
+
panelCancel() {
|
|
90
|
+
/* unreachable */
|
|
91
|
+
},
|
|
92
|
+
// W22: readline has no ↑/esc pop — the pipe path shows no chips
|
|
93
|
+
// and pops nothing (the queue drains on its own).
|
|
94
|
+
bindQueue() {
|
|
95
|
+
/* unreachable — no raw keys in the pipe path */
|
|
96
|
+
},
|
|
84
97
|
emitLine(line) {
|
|
85
98
|
rl.emit("line", line);
|
|
86
99
|
},
|
|
@@ -125,6 +138,19 @@ function editorInput(editor) {
|
|
|
125
138
|
cancelQuestion() {
|
|
126
139
|
editor.cancelQuestion();
|
|
127
140
|
},
|
|
141
|
+
// W21: the panel — the editor's own state machine takes the
|
|
142
|
+
// keys; the compositor renders it via the bound state.
|
|
143
|
+
panelAsk(view, onCommit) {
|
|
144
|
+
editor.beginPanel(view, onCommit);
|
|
145
|
+
},
|
|
146
|
+
panelCancel() {
|
|
147
|
+
editor.cancelPanel();
|
|
148
|
+
},
|
|
149
|
+
// W22: the pending-turn queue — the ↑ pop walk (the keys); the
|
|
150
|
+
// chips are the compositor's bindQueue (the dock side).
|
|
151
|
+
bindQueue(state, pop) {
|
|
152
|
+
editor.bindQueue(state, pop);
|
|
153
|
+
},
|
|
128
154
|
emitLine() {
|
|
129
155
|
/* the editor's buffer survives a cancelled question — its text
|
|
130
156
|
* becomes the next turn on Enter (the readline re-emit
|
|
@@ -157,6 +183,7 @@ function makeLineInput() {
|
|
|
157
183
|
// pipe bytes do not change)
|
|
158
184
|
dock.bindInput(() => editor.dockState(), "› ");
|
|
159
185
|
dock.bindMenu(() => editor.menuState()); // v3 §04: the slash-command menu
|
|
186
|
+
dock.bindApproval(() => editor.panelState()); // W21: the panel's bound state
|
|
160
187
|
return editorInput(editor);
|
|
161
188
|
}
|
|
162
189
|
return readlineInput(createInterface({ input: process.stdin, output: process.stdout }));
|
|
@@ -302,6 +329,13 @@ async function makeAgent(fauxSkipTurns = 0, input, modelFlag) {
|
|
|
302
329
|
setCurrentModelName(resolved.name);
|
|
303
330
|
}
|
|
304
331
|
setAgentModel(model); // v2b: the status bar shows it
|
|
332
|
+
// W21: the extensions array is built ONCE per agent and shared with
|
|
333
|
+
// the runtime by reference — the don't-ask-again writer pushes the
|
|
334
|
+
// generated extension into it so a first-time rule joins the chain
|
|
335
|
+
// at the NEXT run (the run's policies are fixed at its start; run.ts
|
|
336
|
+
// re-reads the config's extensions array per run).
|
|
337
|
+
const extensions = [...modeExtensions(), ...loadedExtensions];
|
|
338
|
+
setCurrentAgentExtensions(extensions);
|
|
305
339
|
const definition = {
|
|
306
340
|
model,
|
|
307
341
|
store,
|
|
@@ -327,7 +361,7 @@ async function makeAgent(fauxSkipTurns = 0, input, modelFlag) {
|
|
|
327
361
|
// Modes: the five tiers join at the CHAIN HEAD, before the user/
|
|
328
362
|
// project extensions (the deny>ask>allow composition keeps a user
|
|
329
363
|
// deny winning over any mode tier — bypass included).
|
|
330
|
-
extensions
|
|
364
|
+
extensions,
|
|
331
365
|
...(resolved !== null
|
|
332
366
|
? {
|
|
333
367
|
provider: resolved.profile.kind,
|
package/dist/resume.js
CHANGED
|
@@ -48,7 +48,7 @@ export async function resume(session, prompt, faux, input) {
|
|
|
48
48
|
});
|
|
49
49
|
try {
|
|
50
50
|
turnNo += 1;
|
|
51
|
-
const last = await consumeRun(session, run, input, turnNo, faux,
|
|
51
|
+
const last = await consumeRun(session, run, input, turnNo, faux, statusCb);
|
|
52
52
|
failOnFauxExhaustion(last, faux, input);
|
|
53
53
|
}
|
|
54
54
|
finally {
|
package/dist/state.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* creates the mutable ones (setBody / setAgentModel / setExtensionLists);
|
|
6
6
|
* the moved modules read and mutate at call time.
|
|
7
7
|
*/
|
|
8
|
-
import { Dock, type Body } from "@vincemakes/kiso-tui";
|
|
8
|
+
import { Dock, type Body, type PanelVerdict, type PanelView } from "@vincemakes/kiso-tui";
|
|
9
9
|
import type { KisoExtension } from "@vincemakes/kiso-runtime";
|
|
10
10
|
/** finding #11: KISO_HOME is the ONE root — every default path derives from
|
|
11
11
|
* it (sessions, trust, extensions, mcp config, skills). The dedicated
|
|
@@ -32,6 +32,16 @@ export interface LineInput {
|
|
|
32
32
|
onExpand(cb: () => void): void;
|
|
33
33
|
question(query: string, cb: (answer: string) => void): void;
|
|
34
34
|
cancelQuestion(): void;
|
|
35
|
+
/** W21: open the approval panel — the editor's state machine takes
|
|
36
|
+
* the keys (the digits/y/n/tab/esc/enter routing, the rule input,
|
|
37
|
+
* the tab-amend), the compositor renders the block + the leads. */
|
|
38
|
+
panelAsk(view: PanelView, onCommit: (v: PanelVerdict) => void): void;
|
|
39
|
+
/** W21: cancel the panel — the SIGINT pair to panelAsk. */
|
|
40
|
+
panelCancel(): void;
|
|
41
|
+
/** W22: bind the pending-turn queue — the ↑ pop walks the CLI's
|
|
42
|
+
* live slots (each pop cancels the turn), esc ends the walk after
|
|
43
|
+
* one more pop. The chips are the compositor's own bindQueue. */
|
|
44
|
+
bindQueue(state: () => readonly string[], pop: () => string | null): void;
|
|
35
45
|
emitLine(line: string): void;
|
|
36
46
|
line(): string;
|
|
37
47
|
clearLine(): void;
|
|
@@ -84,6 +94,13 @@ export declare let userExtensions: readonly KisoExtension[];
|
|
|
84
94
|
* banner distinguishes them from the user-level ones. */
|
|
85
95
|
export declare let projectExtensions: readonly KisoExtension[];
|
|
86
96
|
export declare function setExtensionLists(user: readonly KisoExtension[], project: readonly KisoExtension[], loaded: readonly KisoExtension[]): void;
|
|
97
|
+
/** W21: the CURRENT agent's extensions array — set by makeAgent, the
|
|
98
|
+
* don't-ask-again writer pushes the generated extension into it so a
|
|
99
|
+
* first-time rule joins the chain at the NEXT run (the run's policies
|
|
100
|
+
* are fixed at its start; the array is shared by reference with the
|
|
101
|
+
* runtime's session config — run.ts re-reads it per run). */
|
|
102
|
+
export declare let currentAgentExtensions: KisoExtension[];
|
|
103
|
+
export declare function setCurrentAgentExtensions(value: KisoExtension[]): void;
|
|
87
104
|
/** E3: temp artifacts of the mcp/skills merge — removed on exit. */
|
|
88
105
|
export declare const mergedTempPaths: string[];
|
|
89
106
|
/** The CLI's own version — read from the package.json next to the build. */
|
package/dist/state.js
CHANGED
|
@@ -89,6 +89,15 @@ export function setExtensionLists(user, project, loaded) {
|
|
|
89
89
|
projectExtensions = project;
|
|
90
90
|
loadedExtensions = loaded;
|
|
91
91
|
}
|
|
92
|
+
/** W21: the CURRENT agent's extensions array — set by makeAgent, the
|
|
93
|
+
* don't-ask-again writer pushes the generated extension into it so a
|
|
94
|
+
* first-time rule joins the chain at the NEXT run (the run's policies
|
|
95
|
+
* are fixed at its start; the array is shared by reference with the
|
|
96
|
+
* runtime's session config — run.ts re-reads it per run). */
|
|
97
|
+
export let currentAgentExtensions = [];
|
|
98
|
+
export function setCurrentAgentExtensions(value) {
|
|
99
|
+
currentAgentExtensions = value;
|
|
100
|
+
}
|
|
92
101
|
/** E3: temp artifacts of the mcp/skills merge — removed on exit. */
|
|
93
102
|
export const mergedTempPaths = [];
|
|
94
103
|
/** The CLI's own version — read from the package.json next to the build. */
|
package/dist/trust-ui.d.ts
CHANGED
|
@@ -1,31 +1,62 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The ergonomics batch B4 (pure move) — the human-facing question UI: the E3 project
|
|
3
|
-
* trust gate (ADR-0037), the mcp/skills env merges, the
|
|
4
|
-
* (
|
|
3
|
+
* trust gate (ADR-0037), the mcp/skills env merges, the W21 approval
|
|
4
|
+
* panel (askPanel — the bounded block, the verdict mapping lives in
|
|
5
|
+
* chat.ts), the don't-ask-again rule writer, and the uncertain-execution
|
|
5
6
|
* decisions. All bodies moved verbatim from index.ts.
|
|
6
7
|
*/
|
|
8
|
+
import { type PanelVerdict, type PanelView } from "@vincemakes/kiso-tui";
|
|
7
9
|
import { type ProjectArtifacts } from "@vincemakes/kiso-runtime";
|
|
8
10
|
import type { AgentSession } from "@vincemakes/kiso-runtime";
|
|
9
|
-
import {
|
|
11
|
+
import { type LineInput } from "./state.js";
|
|
10
12
|
/** v2a: the interactive prompt — blue, the identity accent. readline owns
|
|
11
13
|
* the echo of what the user types; we own the prompt's color. (v2c: the
|
|
12
14
|
* readline prompt keeps "you> " — the brick ▌ is the dock's row only;
|
|
13
15
|
* pipe bytes must not change.) */
|
|
14
16
|
export declare function interactivePrompt(): string;
|
|
15
17
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
18
|
+
* W21 — ask the human with the approval panel: the bounded block that
|
|
19
|
+
* replaces the running tool's live window while the approval is pending.
|
|
20
|
+
* The editor owns the keys (the digits/tab/esc/enter routing, the rule
|
|
21
|
+
* input, the tab-amend); the compositor renders the block + the leads;
|
|
22
|
+
* the CLI maps the verdict (the bare-No abort, the No+words tool_result,
|
|
23
|
+
* the allow-amend words riding the next turn — all live in chat.ts,
|
|
24
|
+
* never here).
|
|
19
25
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
+
* Non-interactive stdin (piped, CI) cannot wait forever: the approval
|
|
27
|
+
* AUTO-DENIES, printed loudly — never silently ignored, never hung
|
|
28
|
+
* (Area 7). A TTY without a dock (rows < 4) falls back to the v2a line
|
|
29
|
+
* question (y/n → allow/deny) — the panel cannot render without the
|
|
30
|
+
* dock's live region.
|
|
31
|
+
*
|
|
32
|
+
* rounds 8/10: the ask is ABORTABLE — registered in `pendingAsk`, the
|
|
33
|
+
* SIGINT handler resolves it with { action: "cancel" } (the panel closes
|
|
34
|
+
* through the editor's panelCancel, the dock-less question through
|
|
35
|
+
* cancelQuestion). The dock-less question callback is NOT left dangling:
|
|
36
|
+
* an input that arrives after the cancellation is re-emitted as a fresh
|
|
37
|
+
* "line" — it becomes the next user turn instead of being swallowed by
|
|
38
|
+
* the dead question.
|
|
26
39
|
*/
|
|
27
40
|
export declare let pendingAsk: (() => void) | null;
|
|
28
|
-
export declare function
|
|
41
|
+
export declare function askPanel(input: LineInput, view: PanelView): Promise<PanelVerdict>;
|
|
42
|
+
/**
|
|
43
|
+
* W21/R3 — the "2 Yes, don't ask again for <tool>" rule: a GENERATED
|
|
44
|
+
* extension, ALLOW-ONLY by construction (it never emits deny or ask —
|
|
45
|
+
* the mode moat and the safe-defaults moat keep their teeth). The file
|
|
46
|
+
* is HUMAN-EDITABLE and HUMAN-DELETABLE — that IS the revocation path.
|
|
47
|
+
* The loader sorts *.mjs alphabetically, so dont-ask-again.mjs binds
|
|
48
|
+
* BEFORE safe-defaults.mjs: the allow is the chain's first word, the
|
|
49
|
+
* safe-defaults deny moat still wins (deny > allow — the R3 order).
|
|
50
|
+
*/
|
|
51
|
+
export declare function addDontAskAgainRule(rule: string): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* W21 — the §3.5 fix-hint table: per speaker, the ONE-line path to stop
|
|
54
|
+
* being asked. The hint names the FIX, never a bypass ("switch to
|
|
55
|
+
* bypass" would be an approval-hole nudge — the R3 ruling). The
|
|
56
|
+
* extension hint collapses the home prefix to ~ (the hint must stay
|
|
57
|
+
* short enough for the one-row rule line at narrow widths).
|
|
58
|
+
*/
|
|
59
|
+
export declare function fixHintFor(speaker: string, tool: string): string | undefined;
|
|
29
60
|
/**
|
|
30
61
|
* E3 — the project-level trust gate (ADR-0037): capability is trusted by
|
|
31
62
|
* content digest, not by directory. Runs BEFORE any extension loads — the
|
|
@@ -46,5 +77,9 @@ export declare function resolveProjectTrust(input: LineInput): Promise<ProjectAr
|
|
|
46
77
|
* with project-wins and a stderr note. Exported for tests.
|
|
47
78
|
*/
|
|
48
79
|
export declare function applyProjectMerges(artifacts: ProjectArtifacts): void;
|
|
49
|
-
/** Decide every uncertain execution with the human
|
|
80
|
+
/** Decide every uncertain execution with the human — the panel's simple
|
|
81
|
+
* flavor: 1 Yes = rerun, 3 No = abandon; a cancel records NOTHING (the
|
|
82
|
+
* execution stays uncertain and durable — no rerun/abandoned is
|
|
83
|
+
* fabricated, round 10). The resolution feedback lands in the body —
|
|
84
|
+
* the body is the single stdout writer, never a stray console.log. */
|
|
50
85
|
export declare function resolveUncertains(session: AgentSession, input: LineInput, isCancelled: () => boolean): Promise<void>;
|
package/dist/trust-ui.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The ergonomics batch B4 (pure move) — the human-facing question UI: the E3 project
|
|
3
|
-
* trust gate (ADR-0037), the mcp/skills env merges, the
|
|
4
|
-
* (
|
|
3
|
+
* trust gate (ADR-0037), the mcp/skills env merges, the W21 approval
|
|
4
|
+
* panel (askPanel — the bounded block, the verdict mapping lives in
|
|
5
|
+
* chat.ts), the don't-ask-again rule writer, and the uncertain-execution
|
|
5
6
|
* decisions. All bodies moved verbatim from index.ts.
|
|
6
7
|
*/
|
|
7
|
-
import { existsSync, mkdtempSync, readFileSync, readdirSync, symlinkSync, writeFileSync } from "node:fs";
|
|
8
|
-
import { tmpdir } from "node:os";
|
|
8
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, symlinkSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { homedir, tmpdir } from "node:os";
|
|
9
10
|
import { join } from "node:path";
|
|
11
|
+
import { pathToFileURL } from "node:url";
|
|
10
12
|
import { escapeTerminal, palette } from "@vincemakes/kiso-tui";
|
|
11
13
|
import { projectArtifacts, recordTrust, trustFor } from "@vincemakes/kiso-runtime";
|
|
12
|
-
import {
|
|
14
|
+
import { bodyLog, currentAgentExtensions, dock, extensionsDir, kisoHome, mergedTempPaths } from "./state.js";
|
|
13
15
|
import { loadUserConfig, resolveProjectTrustPolicy } from "./config.js";
|
|
16
|
+
import { getMode } from "./mode.js";
|
|
14
17
|
/** v2a: the interactive prompt — blue, the identity accent. readline owns
|
|
15
18
|
* the echo of what the user types; we own the prompt's color. (v2c: the
|
|
16
19
|
* readline prompt keeps "you> " — the brick ▌ is the dock's row only;
|
|
@@ -20,31 +23,33 @@ export function interactivePrompt() {
|
|
|
20
23
|
return `${p.bold}you> ${p.reset}`;
|
|
21
24
|
}
|
|
22
25
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
+
* W21 — ask the human with the approval panel: the bounded block that
|
|
27
|
+
* replaces the running tool's live window while the approval is pending.
|
|
28
|
+
* The editor owns the keys (the digits/tab/esc/enter routing, the rule
|
|
29
|
+
* input, the tab-amend); the compositor renders the block + the leads;
|
|
30
|
+
* the CLI maps the verdict (the bare-No abort, the No+words tool_result,
|
|
31
|
+
* the allow-amend words riding the next turn — all live in chat.ts,
|
|
32
|
+
* never here).
|
|
26
33
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
34
|
+
* Non-interactive stdin (piped, CI) cannot wait forever: the approval
|
|
35
|
+
* AUTO-DENIES, printed loudly — never silently ignored, never hung
|
|
36
|
+
* (Area 7). A TTY without a dock (rows < 4) falls back to the v2a line
|
|
37
|
+
* question (y/n → allow/deny) — the panel cannot render without the
|
|
38
|
+
* dock's live region.
|
|
39
|
+
*
|
|
40
|
+
* rounds 8/10: the ask is ABORTABLE — registered in `pendingAsk`, the
|
|
41
|
+
* SIGINT handler resolves it with { action: "cancel" } (the panel closes
|
|
42
|
+
* through the editor's panelCancel, the dock-less question through
|
|
43
|
+
* cancelQuestion). The dock-less question callback is NOT left dangling:
|
|
44
|
+
* an input that arrives after the cancellation is re-emitted as a fresh
|
|
45
|
+
* "line" — it becomes the next user turn instead of being swallowed by
|
|
46
|
+
* the dead question.
|
|
33
47
|
*/
|
|
34
48
|
export let pendingAsk = null;
|
|
35
|
-
export function
|
|
49
|
+
export function askPanel(input, view) {
|
|
36
50
|
if (!process.stdin.isTTY) {
|
|
37
|
-
console.log(`[non-interactive — no human to ask: ${
|
|
38
|
-
return Promise.resolve("");
|
|
39
|
-
}
|
|
40
|
-
// v2b: docked — the question takes over the status position, the
|
|
41
|
-
// answer lands at the input line. v2c: a TTY without a dock (rows < 4)
|
|
42
|
-
// prints the question into the body — the editor cannot show it.
|
|
43
|
-
if (dock.active) {
|
|
44
|
-
dock.showQuestion(question);
|
|
45
|
-
}
|
|
46
|
-
else {
|
|
47
|
-
bodyLog(question);
|
|
51
|
+
console.log(`[non-interactive — no human to ask: ${view.fallbackQuestion}]`);
|
|
52
|
+
return Promise.resolve({ action: "deny", reason: "no human to ask" });
|
|
48
53
|
}
|
|
49
54
|
return new Promise((resolve) => {
|
|
50
55
|
let settled = false;
|
|
@@ -53,28 +58,108 @@ export function ask(input, question) {
|
|
|
53
58
|
return;
|
|
54
59
|
settled = true;
|
|
55
60
|
pendingAsk = null;
|
|
56
|
-
input.cancelQuestion();
|
|
57
|
-
resolve(CANCELLED); // the run is aborting — the question is dead
|
|
58
|
-
};
|
|
59
|
-
// v2b: docked — the question reads at the input line, whose prompt
|
|
60
|
-
// is the same blue you> (the editor's brick row; the readline path
|
|
61
|
-
// passes the plain question). An empty prompt would start readline
|
|
62
|
-
// at column 1 while the dock renders "you> " — the typed answer
|
|
63
|
-
// would land on the prompt and drift (probe-confirmed).
|
|
64
|
-
input.question(dock.active ? interactivePrompt() : question, (answer) => {
|
|
65
|
-
if (settled) {
|
|
66
|
-
// The question was cancelled; this line is a NEW user turn.
|
|
67
|
-
input.emitLine(answer);
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
settled = true;
|
|
71
|
-
pendingAsk = null;
|
|
72
61
|
if (dock.active)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
62
|
+
input.panelCancel();
|
|
63
|
+
else
|
|
64
|
+
input.cancelQuestion();
|
|
65
|
+
resolve({ action: "cancel" });
|
|
66
|
+
};
|
|
67
|
+
if (dock.active) {
|
|
68
|
+
input.panelAsk(view, (verdict) => {
|
|
69
|
+
if (settled)
|
|
70
|
+
return; // a cancelled panel's late commit is dead
|
|
71
|
+
settled = true;
|
|
72
|
+
pendingAsk = null;
|
|
73
|
+
resolve(verdict);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
// v2c: a TTY without a dock (rows < 4) — the fallback question
|
|
78
|
+
// in the body; the y/n line answer maps to the verdicts.
|
|
79
|
+
bodyLog(view.fallbackQuestion);
|
|
80
|
+
input.question(view.fallbackQuestion, (answer) => {
|
|
81
|
+
if (settled) {
|
|
82
|
+
// The question was cancelled; this line is a NEW user turn.
|
|
83
|
+
input.emitLine(answer);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
settled = true;
|
|
87
|
+
pendingAsk = null;
|
|
88
|
+
const yes = answer.trim().toLowerCase().startsWith("y");
|
|
89
|
+
resolve(yes ? { action: "allow", reason: "" } : { action: "deny", reason: "" });
|
|
90
|
+
});
|
|
91
|
+
}
|
|
76
92
|
});
|
|
77
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* W21/R3 — the "2 Yes, don't ask again for <tool>" rule: a GENERATED
|
|
96
|
+
* extension, ALLOW-ONLY by construction (it never emits deny or ask —
|
|
97
|
+
* the mode moat and the safe-defaults moat keep their teeth). The file
|
|
98
|
+
* is HUMAN-EDITABLE and HUMAN-DELETABLE — that IS the revocation path.
|
|
99
|
+
* The loader sorts *.mjs alphabetically, so dont-ask-again.mjs binds
|
|
100
|
+
* BEFORE safe-defaults.mjs: the allow is the chain's first word, the
|
|
101
|
+
* safe-defaults deny moat still wins (deny > allow — the R3 order).
|
|
102
|
+
*/
|
|
103
|
+
export async function addDontAskAgainRule(rule) {
|
|
104
|
+
const dir = extensionsDir();
|
|
105
|
+
const file = join(dir, "dont-ask-again.mjs");
|
|
106
|
+
const existed = existsSync(file);
|
|
107
|
+
const module = [
|
|
108
|
+
"// generated by kiso — the \"2 Yes, don't ask again for <tool>\" rule.",
|
|
109
|
+
"// HUMAN-EDITABLE and HUMAN-DELETABLE — this file IS the revocation path:",
|
|
110
|
+
"// delete a rule from the set (or the whole file) to be asked again.",
|
|
111
|
+
"// ALLOW-ONLY by design (R3): never emits deny or ask — the mode moat",
|
|
112
|
+
"// and the safe-defaults moat keep their teeth.",
|
|
113
|
+
`export const RULES = new Set(${JSON.stringify([rule])});`,
|
|
114
|
+
"",
|
|
115
|
+
"export default {",
|
|
116
|
+
' name: "dont-ask-again",',
|
|
117
|
+
" approvals: [",
|
|
118
|
+
" {",
|
|
119
|
+
" decide(call) {",
|
|
120
|
+
' return RULES.has(call.name) ? { action: "allow" } : { action: "abstain" };',
|
|
121
|
+
" },",
|
|
122
|
+
" },",
|
|
123
|
+
" ],",
|
|
124
|
+
"};",
|
|
125
|
+
"",
|
|
126
|
+
].join("\n");
|
|
127
|
+
mkdirSync(dir, { recursive: true });
|
|
128
|
+
writeFileSync(file, module, "utf8");
|
|
129
|
+
// The live in-session effect: a file that existed at startup was
|
|
130
|
+
// imported by the loader — ONE namespace — so mutating the exported
|
|
131
|
+
// RULES Set is live NOW (the current run's chain reads the same Set).
|
|
132
|
+
const mod = (await import(pathToFileURL(file).href));
|
|
133
|
+
if (mod.RULES !== undefined && !mod.RULES.has(rule))
|
|
134
|
+
mod.RULES.add(rule);
|
|
135
|
+
// A FIRST-time rule (a file that did not exist at startup) is not yet
|
|
136
|
+
// in the current run's chain — the chain is fixed at a run's start —
|
|
137
|
+
// so the generated extension joins the NEXT run's policies (run.ts
|
|
138
|
+
// re-reads the session config's extensions array, shared by
|
|
139
|
+
// reference). The honest story: a new rule applies from the next run.
|
|
140
|
+
if (!existed && mod.default !== undefined) {
|
|
141
|
+
currentAgentExtensions.push(mod.default);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* W21 — the §3.5 fix-hint table: per speaker, the ONE-line path to stop
|
|
146
|
+
* being asked. The hint names the FIX, never a bypass ("switch to
|
|
147
|
+
* bypass" would be an approval-hole nudge — the R3 ruling). The
|
|
148
|
+
* extension hint collapses the home prefix to ~ (the hint must stay
|
|
149
|
+
* short enough for the one-row rule line at narrow widths).
|
|
150
|
+
*/
|
|
151
|
+
export function fixHintFor(speaker, tool) {
|
|
152
|
+
if (speaker === "mode:default") {
|
|
153
|
+
if (tool === "shell")
|
|
154
|
+
return undefined;
|
|
155
|
+
return "/mode accept-edits auto-approves edits";
|
|
156
|
+
}
|
|
157
|
+
if (speaker === "mode:manual" || speaker === "mode:plan")
|
|
158
|
+
return "/mode default";
|
|
159
|
+
if (speaker === "mode:accept-edits" || speaker === "mode:bypass")
|
|
160
|
+
return undefined;
|
|
161
|
+
return `edit ${join(extensionsDir(), `${speaker}.mjs`).replace(homedir(), "~")}`;
|
|
162
|
+
}
|
|
78
163
|
/**
|
|
79
164
|
* E3 — the project-level trust gate (ADR-0037): capability is trusted by
|
|
80
165
|
* content digest, not by directory. Runs BEFORE any extension loads — the
|
|
@@ -110,14 +195,30 @@ export async function resolveProjectTrust(input) {
|
|
|
110
195
|
console.error(`[project .kiso] found ${artifacts.files.length} artifact(s) in ${artifacts.root} — not trusted, not loaded (run kiso interactively once to decide)`);
|
|
111
196
|
return null;
|
|
112
197
|
}
|
|
113
|
-
// v2c: the shared input (the editor on a TTY) reads the answer
|
|
114
|
-
//
|
|
198
|
+
// v2c: the shared input (the editor on a TTY) reads the answer.
|
|
199
|
+
// W21: the trust gate is the panel's SIMPLE flavor — the ruleOverride
|
|
200
|
+
// carries the question, the args the artifact listing (the same rows
|
|
201
|
+
// the bodyLog below records, verbatim — the listing still lands in
|
|
202
|
+
// the scrollback; the panel is a bounded block, the record is not).
|
|
115
203
|
bodyLog(`[project .kiso] ${artifacts.root}`);
|
|
116
204
|
for (const f of artifacts.files) {
|
|
117
205
|
bodyLog(` ${f.path} (${f.digest.slice(0, 6)})`);
|
|
118
206
|
}
|
|
119
|
-
const
|
|
120
|
-
|
|
207
|
+
const verdict = await askPanel(input, {
|
|
208
|
+
flavor: "simple",
|
|
209
|
+
name: "project trust",
|
|
210
|
+
title: artifacts.root,
|
|
211
|
+
speaker: "kiso",
|
|
212
|
+
statusText: "▸ project trust",
|
|
213
|
+
args: { kind: "text", lines: artifacts.files.map((f) => `${f.path} (${f.digest.slice(0, 6)})`) },
|
|
214
|
+
ruleOverride: "trust this project's .kiso?",
|
|
215
|
+
fallbackQuestion: `trust this project's .kiso? (y/n) `,
|
|
216
|
+
});
|
|
217
|
+
// A cancel is a "no" HERE — refused is sticky, the project does not
|
|
218
|
+
// load (re-evaluate by deleting the trust line or changing a file).
|
|
219
|
+
// The non-TTY branch above returned WITHOUT a record so an interactive
|
|
220
|
+
// run can still decide later.
|
|
221
|
+
const granted = verdict.action === "allow";
|
|
121
222
|
recordTrust({ root: artifacts.root, digest: artifacts.digest, decision: granted ? "granted" : "refused" });
|
|
122
223
|
if (!granted)
|
|
123
224
|
return null;
|
|
@@ -209,17 +310,30 @@ function readdirSyncSafe(dir) {
|
|
|
209
310
|
return []; // no skills dir on either level = nothing to merge
|
|
210
311
|
}
|
|
211
312
|
}
|
|
212
|
-
/** Decide every uncertain execution with the human
|
|
313
|
+
/** Decide every uncertain execution with the human — the panel's simple
|
|
314
|
+
* flavor: 1 Yes = rerun, 3 No = abandon; a cancel records NOTHING (the
|
|
315
|
+
* execution stays uncertain and durable — no rerun/abandoned is
|
|
316
|
+
* fabricated, round 10). The resolution feedback lands in the body —
|
|
317
|
+
* the body is the single stdout writer, never a stray console.log. */
|
|
213
318
|
export async function resolveUncertains(session, input, isCancelled) {
|
|
214
319
|
for (const uncertain of session.uncertainExecutions()) {
|
|
215
|
-
const
|
|
216
|
-
|
|
320
|
+
const verdict = await askPanel(input, {
|
|
321
|
+
flavor: "simple",
|
|
322
|
+
name: "uncertain execution",
|
|
323
|
+
title: `${uncertain.name} (${uncertain.executionId})`,
|
|
324
|
+
speaker: "kiso",
|
|
325
|
+
statusText: "▸ uncertain execution",
|
|
326
|
+
args: { kind: "text", lines: [uncertain.executionId] },
|
|
327
|
+
ruleOverride: "did the interrupted execution apply? — 1 rerun · 3 abandon",
|
|
328
|
+
fallbackQuestion: `⚠ interrupted execution: ${escapeTerminal(uncertain.name)} (${uncertain.executionId}) — did it apply? (y)es / (n)o `,
|
|
329
|
+
});
|
|
330
|
+
if (isCancelled() || verdict.action === "cancel") {
|
|
217
331
|
// round 10: a cancellation NEVER records a verdict — the execution
|
|
218
332
|
// stays uncertain and durable; no rerun/abandoned is fabricated.
|
|
219
333
|
return;
|
|
220
334
|
}
|
|
221
|
-
const resolution =
|
|
335
|
+
const resolution = verdict.action === "allow" ? "rerun" : "abandoned";
|
|
222
336
|
await session.resolveUncertain(uncertain.executionId, resolution);
|
|
223
|
-
|
|
337
|
+
bodyLog(` ${resolution}`);
|
|
224
338
|
}
|
|
225
339
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-code",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.41",
|
|
4
4
|
"description": "kiso CLI — the coding-agent reference product: kiso chat / kiso resume / kiso sessions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,13 +18,14 @@
|
|
|
18
18
|
"test": "vitest run"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@vincemakes/kiso-core": "0.1.
|
|
22
|
-
"@vincemakes/kiso-evals": "0.1.
|
|
23
|
-
"@vincemakes/kiso-provider-anthropic": "0.1.
|
|
24
|
-
"@vincemakes/kiso-provider-openai": "0.1.
|
|
25
|
-
"@vincemakes/kiso-runtime": "0.1.
|
|
26
|
-
"@vincemakes/kiso-tools-node": "0.1.
|
|
27
|
-
"@vincemakes/kiso-tui": "0.1.
|
|
21
|
+
"@vincemakes/kiso-core": "0.1.32",
|
|
22
|
+
"@vincemakes/kiso-evals": "0.1.33",
|
|
23
|
+
"@vincemakes/kiso-provider-anthropic": "0.1.33",
|
|
24
|
+
"@vincemakes/kiso-provider-openai": "0.1.33",
|
|
25
|
+
"@vincemakes/kiso-runtime": "0.1.33",
|
|
26
|
+
"@vincemakes/kiso-tools-node": "0.1.33",
|
|
27
|
+
"@vincemakes/kiso-tui": "0.1.41",
|
|
28
|
+
"@vincemakes/kiso-tui-cells": "0.1.41"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
31
|
"@types/node": "^26.1.2",
|