@vincemakes/kiso-code 0.39.2 → 0.40.1
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/README.md +1 -1
- package/dist/attachments.d.ts +1 -1
- package/dist/attachments.js +11 -4
- package/dist/builtin.d.ts +12 -0
- package/dist/builtin.js +22 -1
- package/dist/chat.d.ts +31 -3
- package/dist/chat.js +169 -30
- package/dist/config.d.ts +11 -1
- package/dist/config.js +34 -3
- package/dist/dispatch.d.ts +12 -2
- package/dist/dispatch.js +165 -24
- package/dist/floor.d.ts +80 -0
- package/dist/floor.js +652 -0
- package/dist/index.d.ts +21 -2
- package/dist/index.js +463 -46
- package/dist/mode.d.ts +12 -4
- package/dist/mode.js +32 -10
- package/dist/projects.d.ts +106 -0
- package/dist/projects.js +212 -0
- package/dist/protected-shell.d.ts +88 -0
- package/dist/protected-shell.js +406 -0
- package/dist/protected-writes.d.ts +29 -0
- package/dist/protected-writes.js +54 -0
- package/dist/provider-label.d.ts +32 -0
- package/dist/provider-label.js +53 -0
- package/dist/readonly-shell.d.ts +58 -0
- package/dist/readonly-shell.js +590 -0
- package/dist/replay.d.ts +53 -0
- package/dist/replay.js +158 -0
- package/dist/resume-tail.js +4 -1
- package/dist/resume.js +3 -3
- package/dist/retries.d.ts +9 -0
- package/dist/retries.js +16 -0
- package/dist/session-cards.d.ts +33 -4
- package/dist/session-cards.js +44 -3
- package/dist/session-migration.d.ts +104 -0
- package/dist/session-migration.js +319 -0
- package/dist/shell-words.d.ts +134 -0
- package/dist/shell-words.js +628 -0
- package/dist/skill-invoke.d.ts +58 -0
- package/dist/skill-invoke.js +139 -0
- package/dist/state.d.ts +80 -1
- package/dist/state.js +113 -2
- package/dist/trust-ui.js +29 -5
- package/package.json +15 -15
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ kiso sessions list durable sessions
|
|
|
44
44
|
kiso help usage + configuration reference
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
Sessions live under `~/.kiso/
|
|
47
|
+
Sessions live under `~/.kiso/projects/<project>/`, one folder per project; kill the process — `kill -9`
|
|
48
48
|
included — and `kiso resume` continues exactly where the log ends.
|
|
49
49
|
|
|
50
50
|
**Platforms:** macOS / Linux (Node ≥ 22). Windows is unsupported.
|
package/dist/attachments.d.ts
CHANGED
|
@@ -38,4 +38,4 @@ export declare function sniff(buf: Buffer): "image/png" | "image/jpeg" | "image/
|
|
|
38
38
|
* standing in the text. Silently dropping it would tell the model about
|
|
39
39
|
* a file it cannot see, which is worse than telling it nothing.
|
|
40
40
|
*/
|
|
41
|
-
export declare function attachImages(text: string, files?: ReadonlyMap<number, string
|
|
41
|
+
export declare function attachImages(text: string, files?: ReadonlyMap<number, string>, protectedFiles?: readonly string[]): string | ContentBlock[];
|
package/dist/attachments.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* hand its temp file to when it lands.
|
|
22
22
|
*/
|
|
23
23
|
import { readFileSync, statSync } from "node:fs";
|
|
24
|
+
import { isProtectedPath, protectedIdentity } from "@vincemakes/kiso-tools-node";
|
|
24
25
|
/**
|
|
25
26
|
* The size a single image may reach before it is refused.
|
|
26
27
|
*
|
|
@@ -55,7 +56,12 @@ const CANDIDATES = [
|
|
|
55
56
|
/"([^"]*\.(?:png|jpe?g|gif|webp))"/gi,
|
|
56
57
|
/(\S*\.(?:png|jpe?g|gif|webp))/gi,
|
|
57
58
|
];
|
|
58
|
-
function look(path) {
|
|
59
|
+
function look(path, id) {
|
|
60
|
+
// a protected file is never attached, image or not: a user may list a
|
|
61
|
+
// scan of a passport, and a path in a turn can come from a model's
|
|
62
|
+
// delegate task or a command's output, not only from the person
|
|
63
|
+
if (isProtectedPath(path, id))
|
|
64
|
+
return null;
|
|
59
65
|
try {
|
|
60
66
|
const st = statSync(path);
|
|
61
67
|
if (!st.isFile() || st.size > MAX_IMAGE_BYTES || st.size === 0)
|
|
@@ -83,7 +89,8 @@ function look(path) {
|
|
|
83
89
|
* standing in the text. Silently dropping it would tell the model about
|
|
84
90
|
* a file it cannot see, which is worse than telling it nothing.
|
|
85
91
|
*/
|
|
86
|
-
export function attachImages(text, files) {
|
|
92
|
+
export function attachImages(text, files, protectedFiles = []) {
|
|
93
|
+
const id = protectedIdentity(protectedFiles);
|
|
87
94
|
const found = [];
|
|
88
95
|
const claimed = [];
|
|
89
96
|
// REL-0152-D16: the `[Image #N]` capsules first. The buffer carries a
|
|
@@ -97,7 +104,7 @@ export function attachImages(text, files) {
|
|
|
97
104
|
const path = files.get(Number(m[1]));
|
|
98
105
|
if (path === undefined)
|
|
99
106
|
continue;
|
|
100
|
-
const hit = look(path);
|
|
107
|
+
const hit = look(path, id);
|
|
101
108
|
if (hit === null)
|
|
102
109
|
continue;
|
|
103
110
|
claimed.push({ start: m.index, end: m.index + m[0].length });
|
|
@@ -112,7 +119,7 @@ export function attachImages(text, files) {
|
|
|
112
119
|
const end = start + m[0].length;
|
|
113
120
|
if (claimed.some((c) => start < c.end && end > c.start))
|
|
114
121
|
continue;
|
|
115
|
-
const hit = look(path);
|
|
122
|
+
const hit = look(path, id);
|
|
116
123
|
if (hit === null)
|
|
117
124
|
continue;
|
|
118
125
|
claimed.push({ start, end });
|
package/dist/builtin.d.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { type AskUI } from "@vincemakes/kiso-ask-ext";
|
|
2
2
|
import type { KisoExtension } from "@vincemakes/kiso-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* 0.40.0 (the owner's dogfood): dontAsk never asks, so its tool table never
|
|
5
|
+
* offers ask_user — a model once put four questions to a dontAsk session
|
|
6
|
+
* that could only decline them. A LIVE gate, not a load-time one: the
|
|
7
|
+
* registry reads an extension's tools on every request (registerLive) and
|
|
8
|
+
* the run recomposes the tool table, snippet and guidelines included, so
|
|
9
|
+
* entering dontAsk takes ask_user away from the next run and leaving it
|
|
10
|
+
* brings ask_user back. In dontAsk the table is byte-identical to the pipe
|
|
11
|
+
* path's; each switch in or out costs one prompt-cache break. The decline
|
|
12
|
+
* path (trust-ui) stays for a turn already in flight when the mode changes.
|
|
13
|
+
*/
|
|
14
|
+
export declare function offInDontAsk(ext: KisoExtension): KisoExtension;
|
|
3
15
|
export declare function builtInLayer(user: readonly KisoExtension[], project: readonly KisoExtension[],
|
|
4
16
|
/** KC3.5: built-in #4 — the ask extension, loaded ONLY when a panel
|
|
5
17
|
* bridge exists (an interactive TTY). No bridge, no fourth built-in:
|
package/dist/builtin.js
CHANGED
|
@@ -25,6 +25,27 @@ import createMcp from "@vincemakes/kiso-mcp-ext";
|
|
|
25
25
|
import createSkills from "@vincemakes/kiso-skills-ext";
|
|
26
26
|
import createSubagent from "@vincemakes/kiso-subagent-ext";
|
|
27
27
|
import createAsk, {} from "@vincemakes/kiso-ask-ext";
|
|
28
|
+
import { getMode } from "./mode.js";
|
|
29
|
+
/**
|
|
30
|
+
* 0.40.0 (the owner's dogfood): dontAsk never asks, so its tool table never
|
|
31
|
+
* offers ask_user — a model once put four questions to a dontAsk session
|
|
32
|
+
* that could only decline them. A LIVE gate, not a load-time one: the
|
|
33
|
+
* registry reads an extension's tools on every request (registerLive) and
|
|
34
|
+
* the run recomposes the tool table, snippet and guidelines included, so
|
|
35
|
+
* entering dontAsk takes ask_user away from the next run and leaving it
|
|
36
|
+
* brings ask_user back. In dontAsk the table is byte-identical to the pipe
|
|
37
|
+
* path's; each switch in or out costs one prompt-cache break. The decline
|
|
38
|
+
* path (trust-ui) stays for a turn already in flight when the mode changes.
|
|
39
|
+
*/
|
|
40
|
+
export function offInDontAsk(ext) {
|
|
41
|
+
const tools = ext.tools ?? [];
|
|
42
|
+
return {
|
|
43
|
+
...ext,
|
|
44
|
+
get tools() {
|
|
45
|
+
return getMode() === "dontAsk" ? [] : tools;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
28
49
|
export async function builtInLayer(user, project,
|
|
29
50
|
/** KC3.5: built-in #4 — the ask extension, loaded ONLY when a panel
|
|
30
51
|
* bridge exists (an interactive TTY). No bridge, no fourth built-in:
|
|
@@ -36,7 +57,7 @@ ask,
|
|
|
36
57
|
* constructed, so the names have to arrive here — after construction is
|
|
37
58
|
* too late. Only the config knows them; nothing else can. */
|
|
38
59
|
secretEnvNames = []) {
|
|
39
|
-
const all = await Promise.all([createMcp({ secretEnvNames }), createSkills(), createSubagent(), ...(ask === undefined ? [] : [createAsk(ask)])]);
|
|
60
|
+
const all = await Promise.all([createMcp({ secretEnvNames }), createSkills(), createSubagent(), ...(ask === undefined ? [] : [createAsk(ask).then(offInDontAsk)])]);
|
|
40
61
|
const shadowed = all.filter((b) => user.some((u) => u.name === b.name));
|
|
41
62
|
for (const s of shadowed) {
|
|
42
63
|
console.error(`[extensions] user extension "${s.name}" shadows the built-in — the built-in is not loaded`);
|
package/dist/chat.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* approval-moment mini-diff, the status spinner, and the context
|
|
5
5
|
* estimates. All bodies moved verbatim from index.ts.
|
|
6
6
|
*/
|
|
7
|
+
import type { SessionRoute } from "./projects.js";
|
|
7
8
|
import { type RunUsage } from "@vincemakes/kiso-tui";
|
|
8
9
|
import { type SaferFailure, type SaferOption } from "@vincemakes/kiso-tui";
|
|
9
10
|
import type { AgentSession, Run } from "@vincemakes/kiso-runtime";
|
|
@@ -79,8 +80,10 @@ export declare function microcompactThresholdFor(of?: {
|
|
|
79
80
|
readonly baseUrl?: string;
|
|
80
81
|
}): number;
|
|
81
82
|
/**
|
|
82
|
-
* B area: approximate context ratio
|
|
83
|
-
*
|
|
83
|
+
* B area: approximate context ratio vs the model window. Marked ~
|
|
84
|
+
* everywhere it is shown. 0.40.0: the context as the last bill measured it
|
|
85
|
+
* (session.contextUsed — the estimate only when no bill describes it);
|
|
86
|
+
* chars/4 alone read the owner's 730k Chinese-heavy context as ~470k.
|
|
84
87
|
*/
|
|
85
88
|
export declare function estimateCtxRatio(session: AgentSession): number;
|
|
86
89
|
/** A1a: the ratio the STATUS LINE and the ctx displays show — the request
|
|
@@ -98,6 +101,27 @@ export declare function statusModelLabel(session: {
|
|
|
98
101
|
};
|
|
99
102
|
}): string;
|
|
100
103
|
export declare function displayCtxRatio(session: AgentSession): number;
|
|
104
|
+
/** 0.40.0 item 9: how long a prompt cache is assumed to live. PROVISIONAL:
|
|
105
|
+
* DeepSeek does not publish its TTL (the owner's cache was gone after 27
|
|
106
|
+
* minutes); Anthropic's default is 5 minutes. */
|
|
107
|
+
export declare const COLD_AFTER_MS: number;
|
|
108
|
+
/** 0.40.0 (the owner): the recap's cold-cache wording — only when the turn
|
|
109
|
+
* began at least COLD_AFTER_MS after the last bill AND the cache really
|
|
110
|
+
* missed (a surfaced miss, or under half the prompt from cache). */
|
|
111
|
+
export declare function coldAfter(idleMs: number | undefined, missed: number | null, usage: RunUsage): {
|
|
112
|
+
coldAfterMinutes?: number;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* 0.40.0 item 9 — the cold resume. When the last BILL put the context over
|
|
116
|
+
* the microcompact threshold and that bill is older than COLD_AFTER_MS, the
|
|
117
|
+
* next request re-sends the whole prefix uncached. Compacting first turns
|
|
118
|
+
* that one expensive request into a summary call. Anchored only: a session
|
|
119
|
+
* no bill describes has no known size, and no age to call cold.
|
|
120
|
+
*/
|
|
121
|
+
export declare function coldResumeOffer(session: AgentSession, now?: number): {
|
|
122
|
+
tokens: number;
|
|
123
|
+
minutes: number;
|
|
124
|
+
} | null;
|
|
101
125
|
/** A1a: the number the auto-compact decision reads — the pre-A1a estimate,
|
|
102
126
|
* unchanged this round, named so a gate can pin that it did not move. */
|
|
103
127
|
export declare function autoCompactRatio(session: AgentSession): number;
|
|
@@ -285,6 +309,7 @@ export type ChatEnd = {
|
|
|
285
309
|
} | {
|
|
286
310
|
readonly next: "switch";
|
|
287
311
|
readonly id: string;
|
|
312
|
+
readonly lines?: readonly string[];
|
|
288
313
|
}
|
|
289
314
|
/** §2.5: the caller REBUILDS the agent and re-enters on the same id.
|
|
290
315
|
* Not "switch": switching keeps the agent and only changes the id,
|
|
@@ -293,11 +318,14 @@ export type ChatEnd = {
|
|
|
293
318
|
| {
|
|
294
319
|
readonly next: "reload";
|
|
295
320
|
readonly id: string;
|
|
321
|
+
readonly lines?: readonly string[];
|
|
296
322
|
};
|
|
297
323
|
/** The session-navigation seam main provides: the OTHER sessions'
|
|
298
324
|
* ids, and (when a dock is up) the existing picker. */
|
|
299
325
|
export interface ChatNav {
|
|
300
326
|
readonly sessions: () => readonly string[];
|
|
327
|
+
/** 0.40.0: an id outside this project's folder — refused, or opened where it is */
|
|
328
|
+
readonly route?: (id: string) => SessionRoute | null;
|
|
301
329
|
readonly pick?: () => Promise<string | null>;
|
|
302
330
|
}
|
|
303
|
-
export declare function chat(session: AgentSession, faux: boolean, input: LineInput, autoCompact?: AutoCompact, nav?: ChatNav): Promise<ChatEnd>;
|
|
331
|
+
export declare function chat(session: AgentSession, faux: boolean, input: LineInput, autoCompact?: AutoCompact, nav?: ChatNav, seed?: readonly string[]): Promise<ChatEnd>;
|
package/dist/chat.js
CHANGED
|
@@ -6,19 +6,20 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { readFileSync, statSync } from "node:fs";
|
|
8
8
|
import { escapeTerminal, cacheHitPct, decodeRate, idleStatus, palette, renderEvent, renderRecap, runningStatus, toolTarget, verifyOfferView, STATUS_GLYPHS, } from "@vincemakes/kiso-tui";
|
|
9
|
-
import { askView, deletionRiskHint, editFileDiff, writeFileDiff } from "@vincemakes/kiso-tui";
|
|
10
|
-
import { canonicalTargetPath, shellProgressPath } from "@vincemakes/kiso-tools-node";
|
|
9
|
+
import { askView, coldResumeLine, coldResumeView, deletionRiskHint, editFileDiff, writeFileDiff } from "@vincemakes/kiso-tui";
|
|
10
|
+
import { canonicalTargetPath, isProtectedPath, protectedIdentity, shellProgressPath } from "@vincemakes/kiso-tools-node";
|
|
11
|
+
import { queuedSwitchLines } from "./state.js";
|
|
11
12
|
import { echoText } from "@vincemakes/kiso-tui-cells/render";
|
|
12
13
|
import { canonicalizeUsage } from "@vincemakes/kiso-runtime";
|
|
13
14
|
import { canonicalizeUsageForModel, requestBudget } from "@vincemakes/kiso-runtime/internal";
|
|
14
15
|
import { dispatch, abortBangCommand } from "./dispatch.js";
|
|
15
16
|
import { paintWindowTitle } from "./window-title.js";
|
|
16
|
-
import { agentBaseUrl, agentModel, body, bodyLog, configuredWindow, dock } from "./state.js";
|
|
17
|
+
import { agentBaseUrl, agentModel, body, bodyLog, configuredWindow, dock, retryOnRow, retryShown, setRetryShown, floorOn, protectedFiles } from "./state.js";
|
|
17
18
|
import { attachImages } from "./attachments.js";
|
|
18
19
|
import { lookupModelMetadata } from "@vincemakes/kiso-runtime/internal";
|
|
19
20
|
import { addDontAskAgainRule, askPanel, fixHintFor, pendingAsk, resolveUncertains } from "./trust-ui.js";
|
|
20
21
|
import { FauxExhaustionError, failOnFauxExhaustion } from "./faux-glue.js";
|
|
21
|
-
import {
|
|
22
|
+
import { OFFERED_MODES, getMode, setMode } from "./mode.js";
|
|
22
23
|
/** B area: default context window for the ~ctx estimate (config overridable). */
|
|
23
24
|
const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
24
25
|
/** The in-process fake provider's id, and the window we declare for it.
|
|
@@ -134,13 +135,13 @@ export function microcompactThresholdFor(of) {
|
|
|
134
135
|
return contextWindowTokens(of) / 2;
|
|
135
136
|
}
|
|
136
137
|
/**
|
|
137
|
-
* B area: approximate context ratio
|
|
138
|
-
*
|
|
138
|
+
* B area: approximate context ratio vs the model window. Marked ~
|
|
139
|
+
* everywhere it is shown. 0.40.0: the context as the last bill measured it
|
|
140
|
+
* (session.contextUsed — the estimate only when no bill describes it);
|
|
141
|
+
* chars/4 alone read the owner's 730k Chinese-heavy context as ~470k.
|
|
139
142
|
*/
|
|
140
143
|
export function estimateCtxRatio(session) {
|
|
141
|
-
|
|
142
|
-
const chars = JSON.stringify(projected).length;
|
|
143
|
-
return chars / 4 / contextWindowTokens();
|
|
144
|
+
return session.contextUsed() / contextWindowTokens();
|
|
144
145
|
}
|
|
145
146
|
/** A1a: the ratio the STATUS LINE and the ctx displays show — the request
|
|
146
147
|
* budget counting the parts (system prompt, tool table, messages with
|
|
@@ -153,6 +154,12 @@ export function estimateCtxRatio(session) {
|
|
|
153
154
|
* is the session's own (the durable profile), never the CLI's memory. */
|
|
154
155
|
export function statusModelLabel(session) {
|
|
155
156
|
const effort = session.reasoning?.effort;
|
|
157
|
+
// The owner's dogfood, 2026-09-21 (the second pass): the status row under
|
|
158
|
+
// the composer is the MODEL's, and only the model's — a host belongs where
|
|
159
|
+
// a choice is being made (`/model`'s rows, the switch notice) and where the
|
|
160
|
+
// identity is asked for (`/status`), not in a row that has ~40 columns and
|
|
161
|
+
// cut the host to `deepseek/d…ndcode.ai`. Unnecessary at best, misleading at
|
|
162
|
+
// worst: it read as a path, not as an account.
|
|
156
163
|
return effort !== undefined && effort !== "default" ? `${agentModel} · ${effort}` : agentModel;
|
|
157
164
|
}
|
|
158
165
|
export function displayCtxRatio(session) {
|
|
@@ -164,8 +171,42 @@ export function displayCtxRatio(session) {
|
|
|
164
171
|
const window = knownContextWindow();
|
|
165
172
|
if (window === null)
|
|
166
173
|
return Number.NaN;
|
|
174
|
+
// 0.40.0: the last bill is the truth when one describes the context;
|
|
175
|
+
// the parts estimate is for a session no bill describes yet.
|
|
176
|
+
const anchored = session.contextAnchor();
|
|
177
|
+
if (anchored !== undefined)
|
|
178
|
+
return anchored / window;
|
|
167
179
|
return requestBudget(session.requestParts(), window).ratio;
|
|
168
180
|
}
|
|
181
|
+
/** 0.40.0 item 9: how long a prompt cache is assumed to live. PROVISIONAL:
|
|
182
|
+
* DeepSeek does not publish its TTL (the owner's cache was gone after 27
|
|
183
|
+
* minutes); Anthropic's default is 5 minutes. */
|
|
184
|
+
export const COLD_AFTER_MS = 5 * 60_000;
|
|
185
|
+
/** 0.40.0 (the owner): the recap's cold-cache wording — only when the turn
|
|
186
|
+
* began at least COLD_AFTER_MS after the last bill AND the cache really
|
|
187
|
+
* missed (a surfaced miss, or under half the prompt from cache). */
|
|
188
|
+
export function coldAfter(idleMs, missed, usage) {
|
|
189
|
+
if (idleMs === undefined || idleMs < COLD_AFTER_MS)
|
|
190
|
+
return {};
|
|
191
|
+
const hit = cacheHitPct(usage);
|
|
192
|
+
return missed !== null || (hit !== null && hit < 50) ? { coldAfterMinutes: Math.round(idleMs / 60_000) } : {};
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* 0.40.0 item 9 — the cold resume. When the last BILL put the context over
|
|
196
|
+
* the microcompact threshold and that bill is older than COLD_AFTER_MS, the
|
|
197
|
+
* next request re-sends the whole prefix uncached. Compacting first turns
|
|
198
|
+
* that one expensive request into a summary call. Anchored only: a session
|
|
199
|
+
* no bill describes has no known size, and no age to call cold.
|
|
200
|
+
*/
|
|
201
|
+
export function coldResumeOffer(session, now = Date.now()) {
|
|
202
|
+
const tokens = session.contextAnchor();
|
|
203
|
+
const at = session.lastUsageAt;
|
|
204
|
+
if (tokens === undefined || at === undefined)
|
|
205
|
+
return null;
|
|
206
|
+
if (tokens <= microcompactThresholdFor() || now - at < COLD_AFTER_MS)
|
|
207
|
+
return null;
|
|
208
|
+
return { tokens, minutes: Math.floor((now - at) / 60_000) };
|
|
209
|
+
}
|
|
169
210
|
/** A1a: the number the auto-compact decision reads — the pre-A1a estimate,
|
|
170
211
|
* unchanged this round, named so a gate can pin that it did not move. */
|
|
171
212
|
export function autoCompactRatio(session) {
|
|
@@ -332,6 +373,11 @@ export function turnUsageLedger() {
|
|
|
332
373
|
* is gone) — docked only, 200ms rotation between the request and the
|
|
333
374
|
* first event. */
|
|
334
375
|
export function startStatusSpinner(onTick) {
|
|
376
|
+
// ADR-0005 Amendment 2: a retry belongs to the run that announced it.
|
|
377
|
+
// Cleared where every run starts and stops — the chat's and the
|
|
378
|
+
// recovery flow's alike — so a stale countdown can never survive into
|
|
379
|
+
// the idle row or the next turn.
|
|
380
|
+
setRetryShown(null);
|
|
335
381
|
if (!dock.active)
|
|
336
382
|
return () => { };
|
|
337
383
|
// v3 §03/§05: the working glyph family ▖▘▝▗, 200ms rotation — the
|
|
@@ -340,7 +386,10 @@ export function startStatusSpinner(onTick) {
|
|
|
340
386
|
let i = 0;
|
|
341
387
|
const timer = setInterval(() => onTick(STATUS_GLYPHS[i++ % STATUS_GLYPHS.length]), 200);
|
|
342
388
|
timer.unref();
|
|
343
|
-
return () =>
|
|
389
|
+
return () => {
|
|
390
|
+
clearInterval(timer);
|
|
391
|
+
setRetryShown(null);
|
|
392
|
+
};
|
|
344
393
|
}
|
|
345
394
|
/**
|
|
346
395
|
* TUI2-R1 (C) — the shell tailer: the READER half of the progress
|
|
@@ -391,6 +440,10 @@ function approvalDiff(name, input) {
|
|
|
391
440
|
const path = typeof input.path === "string" ? input.path : "";
|
|
392
441
|
if (path === "")
|
|
393
442
|
return null;
|
|
443
|
+
// the tool refuses a protected file whatever the answer; the panel
|
|
444
|
+
// must not print the store on the way there
|
|
445
|
+
if (isProtectedPath(path, protectedIdentity(protectedFiles())))
|
|
446
|
+
return null;
|
|
394
447
|
let oldContent = null;
|
|
395
448
|
try {
|
|
396
449
|
oldContent = readFileSync(path, "utf8");
|
|
@@ -745,6 +798,9 @@ submitTurn) {
|
|
|
745
798
|
// seconds, usage, ctx left. The turn's WORK is the fold line's, said
|
|
746
799
|
// once, where the work happened.
|
|
747
800
|
const turnStart = Date.now();
|
|
801
|
+
// 0.40.0 (the owner): how long the session had been idle when this turn
|
|
802
|
+
// began — the recap names a cold cache instead of a bare fresh figure.
|
|
803
|
+
const idleMs = session.lastUsageAt === undefined ? undefined : turnStart - session.lastUsageAt;
|
|
748
804
|
// W14: the thinking event carries NO timestamp — the CLI wall-clocks
|
|
749
805
|
// the thinking window: it opens at the first thinking event and closes
|
|
750
806
|
// at the first non-thinking event (the fold needs the seconds).
|
|
@@ -763,11 +819,21 @@ submitTurn) {
|
|
|
763
819
|
try {
|
|
764
820
|
for await (const ev of run) {
|
|
765
821
|
last = ev;
|
|
822
|
+
// ADR-0005 Amendment 2: an event from the run means the retried
|
|
823
|
+
// attempt got through — the row stops saying it is waiting.
|
|
824
|
+
if (retryShown !== null)
|
|
825
|
+
setRetryShown(null);
|
|
766
826
|
// LT2B-F1: a turn count is not evidence of a loop. Keep consuming
|
|
767
827
|
// healthy runs without a periodic presence check; explicit cancellation,
|
|
768
828
|
// the stream watchdog and the repeated-failure breaker remain active.
|
|
769
829
|
// W22-R1: the ledger sees every event; a stop is what ends a call.
|
|
770
830
|
ledger.observe(ev);
|
|
831
|
+
// ADR-0055 Amendment 1 (A1b): a compaction inside the run says so, once.
|
|
832
|
+
if (ev.type === "summarized" || ev.type === "microcompacted") {
|
|
833
|
+
const r = displayCtxRatio(session);
|
|
834
|
+
const ctx = Number.isFinite(r) ? ` · ctx now ~${Math.round(r * 100)}% used` : "";
|
|
835
|
+
body.notice(ev.type === "summarized" ? `✦ compacted mid-run — the conversation before this point is a summary now${ctx}` : `✦ pruned old tool output mid-run${ctx}`);
|
|
836
|
+
}
|
|
771
837
|
if (ev.type !== "thinking") {
|
|
772
838
|
if (thinkingSince !== null) {
|
|
773
839
|
thoughtSeconds += (Date.now() - thinkingSince) / 1000;
|
|
@@ -800,6 +866,12 @@ submitTurn) {
|
|
|
800
866
|
body.notice(` ${typeof ev.content === "string" ? ev.content : ""}`);
|
|
801
867
|
break;
|
|
802
868
|
}
|
|
869
|
+
// 0.40.0: a skill turn's chip is the line the person TYPED — the
|
|
870
|
+
// SKILL.md body is what the model read, not what they said.
|
|
871
|
+
if (ev.via !== undefined) {
|
|
872
|
+
body.userLine(ev.via.line);
|
|
873
|
+
break;
|
|
874
|
+
}
|
|
803
875
|
// DC-60: a turn that carries an image is a content ARRAY — its words
|
|
804
876
|
// still echo, the image as a mark; an empty chip was the bug.
|
|
805
877
|
body.userLine(echoText(ev.content));
|
|
@@ -912,8 +984,17 @@ submitTurn) {
|
|
|
912
984
|
// - Yes+amend → approve(true), the words ride the NEXT turn;
|
|
913
985
|
// - esc → cancel, the conservative denial.
|
|
914
986
|
const name = ev.name;
|
|
915
|
-
body.toolApproval(ev.callId, approvalDiff(name, ev.input ?? {}));
|
|
916
987
|
const decisionId = ev.decisionId;
|
|
988
|
+
// Launch-weekend plan §2 — dontAsk: the chain's final ASK is
|
|
989
|
+
// denied HERE, at the one place kiso asks, so every allow the
|
|
990
|
+
// chain gave still stands (a tier deny would outrank them). The
|
|
991
|
+
// reason is the tool result: the model sees why and goes on.
|
|
992
|
+
if (getMode() === "dontAsk") {
|
|
993
|
+
body.notice(`[dontAsk] ${escapeTerminal(name)} would ask — denied`);
|
|
994
|
+
await session.approve(decisionId, false, `dontAsk: ${name} needs a human's approval, and this session never asks — denied`);
|
|
995
|
+
break;
|
|
996
|
+
}
|
|
997
|
+
body.toolApproval(ev.callId, approvalDiff(name, ev.input ?? {}));
|
|
917
998
|
// TUI2-R3v2 ③: the on-demand alternatives provider. It is built
|
|
918
999
|
// per approval and captured by the panel; it fires ONLY if the
|
|
919
1000
|
// human presses option 3, which is the whole zero-ambient-rent
|
|
@@ -1054,6 +1135,10 @@ submitTurn) {
|
|
|
1054
1135
|
// R-C item 4: only an above-floor miss is surfaced —
|
|
1055
1136
|
// the recap gains "· miss N" on the cache segment.
|
|
1056
1137
|
...(missed !== null ? { missed } : {}),
|
|
1138
|
+
// Cold: idle past the cache's assumed life (the #77
|
|
1139
|
+
// constant, provisional) AND the turn did re-send a
|
|
1140
|
+
// prefix uncached — the time alone is not evidence.
|
|
1141
|
+
...(coldAfter(idleMs, missed, turnUsage() ?? UNKNOWN_USAGE)),
|
|
1057
1142
|
ctxLeftPct: Number.isFinite(ratio) ? (1 - ratio) * 100 : null,
|
|
1058
1143
|
// W19: under plan the recap becomes the way-forward row
|
|
1059
1144
|
// (the /mode hints are the mode's exits).
|
|
@@ -1086,7 +1171,7 @@ submitTurn) {
|
|
|
1086
1171
|
}
|
|
1087
1172
|
return last;
|
|
1088
1173
|
}
|
|
1089
|
-
export async function chat(session, faux, input, autoCompact, nav) {
|
|
1174
|
+
export async function chat(session, faux, input, autoCompact, nav, seed) {
|
|
1090
1175
|
// the switch directive — set once by dispatch's /clear or /resume,
|
|
1091
1176
|
// resolved through the end signal so the final awaits still run
|
|
1092
1177
|
let switchTo = null;
|
|
@@ -1116,7 +1201,7 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
1116
1201
|
console.log("\n[exit requested]");
|
|
1117
1202
|
input.close();
|
|
1118
1203
|
};
|
|
1119
|
-
const turn = (text, seedSource) => new Promise((resolve, reject) => {
|
|
1204
|
+
const turn = (text, seedSource, via) => new Promise((resolve, reject) => {
|
|
1120
1205
|
queued = Math.max(0, queued - 1); // a queued turn starts
|
|
1121
1206
|
// REL-0152-D11: a turn that names an image file carries it. The
|
|
1122
1207
|
// scan returns the STRING unchanged when it finds nothing, so a
|
|
@@ -1126,8 +1211,12 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
1126
1211
|
// REL-0152-D16: the capsules' files come from the editor, which
|
|
1127
1212
|
// is the only thing that knows which number stands for which
|
|
1128
1213
|
// screenshot.
|
|
1129
|
-
|
|
1130
|
-
|
|
1214
|
+
//
|
|
1215
|
+
// 0.40.0: a skill turn is not scanned either — its text is a
|
|
1216
|
+
// SKILL.md body, and a body that mentions `diagram.png` must not
|
|
1217
|
+
// attach a file from the workspace the person never pointed at.
|
|
1218
|
+
const content = seedSource !== undefined || via !== undefined ? text : attachImages(text, input.attachments?.(), protectedFiles());
|
|
1219
|
+
const run = seedSource !== undefined ? session.run(content, { source: seedSource }) : via !== undefined ? session.run(content, { via }) : session.run(content);
|
|
1131
1220
|
currentRun = run;
|
|
1132
1221
|
turnNo += 1;
|
|
1133
1222
|
const myTurn = turnNo;
|
|
@@ -1278,11 +1367,15 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
1278
1367
|
// BEHIND the correction, in their original order. Ephemeral
|
|
1279
1368
|
// reordering of ephemeral state; the durable log still just records
|
|
1280
1369
|
// what ran, in the order it ran.
|
|
1281
|
-
|
|
1370
|
+
// A slot re-enters with its CONTENT and its `via`, never its display
|
|
1371
|
+
// line: a queued skill re-submitted as `/review x` would reach the
|
|
1372
|
+
// model as the literal text of the command.
|
|
1373
|
+
const jumped = pendingTurns.map((s) => ({ content: s.content, via: s.via }));
|
|
1282
1374
|
for (let i = jumped.length; i > 0; i -= 1)
|
|
1283
1375
|
popQueue();
|
|
1284
|
-
|
|
1285
|
-
|
|
1376
|
+
submitTurn(line);
|
|
1377
|
+
for (const j of jumped)
|
|
1378
|
+
submitTurn(j.content, j.via);
|
|
1286
1379
|
});
|
|
1287
1380
|
// round 5 (P1-11): the PERSISTENT line listener is installed BEFORE the
|
|
1288
1381
|
// startup recovery — a cancelled question's re-emitted "line" needs a
|
|
@@ -1293,7 +1386,15 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
1293
1386
|
// REPL is ready (they are never dropped).
|
|
1294
1387
|
const chainRef = { current: Promise.resolve() };
|
|
1295
1388
|
let replReady = false;
|
|
1296
|
-
const queuedLines = [];
|
|
1389
|
+
const queuedLines = [...(seed ?? []), ...queuedSwitchLines.splice(0)];
|
|
1390
|
+
// DC-57 (the owner's ruling, 2026-09-21): once the session is LEAVING —
|
|
1391
|
+
// a switch or a reload has been requested — a line that arrives belongs
|
|
1392
|
+
// to the session the person asked for, not to the one they are leaving.
|
|
1393
|
+
// One read can carry several lines (a paste, a pipe, a scripted
|
|
1394
|
+
// driver), and the departing instance used to dispatch them; they are
|
|
1395
|
+
// queued here and replayed by the NEXT chat() entry, exactly as
|
|
1396
|
+
// pre-ready lines are replayed by this one.
|
|
1397
|
+
let leaving = false;
|
|
1297
1398
|
// B area: user-turn counter for the status line. /last and /think read
|
|
1298
1399
|
// the body (the ToolCell / ThinkingCell final states).
|
|
1299
1400
|
let turnNo = 0;
|
|
@@ -1304,6 +1405,9 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
1304
1405
|
// read (the dock renders the lines, the editor pops the last one).
|
|
1305
1406
|
// A slot leaves the queue when its turn STARTS or when the user
|
|
1306
1407
|
// pops it (cancelled — the chain segment skips it).
|
|
1408
|
+
// 0.40.0: `line` is what the chip shows and what a pop hands back to
|
|
1409
|
+
// the editor — for a skill, the line the person TYPED; `content` is
|
|
1410
|
+
// what the turn submits (the skill's body and args).
|
|
1307
1411
|
const pendingTurns = [];
|
|
1308
1412
|
// TV-1B — the offer memory: session-local BY DESIGN (a dead process's
|
|
1309
1413
|
// "not now" should not silence a live one; resume re-offers once,
|
|
@@ -1344,8 +1448,12 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
1344
1448
|
// KC2 §5: the STATE (the glyph, the run's start, the usage, the dock)
|
|
1345
1449
|
// stays here; the ROW's text is the tui's status formatter.
|
|
1346
1450
|
const paintRunning = () => {
|
|
1451
|
+
// ADR-0005 Amendment 2: the running row gets the idle row's budget
|
|
1452
|
+
// (DF-0330-F1). A pending retry makes it thirty columns longer, and
|
|
1453
|
+
// composed blind it would be cut from the END — the context figure,
|
|
1454
|
+
// a fact, going before the gesture hints.
|
|
1347
1455
|
if (dock.active)
|
|
1348
|
-
dock.setStatus(runningStatus(runGlyph, runStart, runUsage.out, displayCtxRatio(session), lastTokPerSec));
|
|
1456
|
+
dock.setStatus(runningStatus(runGlyph, runStart, runUsage.out, displayCtxRatio(session), lastTokPerSec, rowWidth(), retryOnRow()));
|
|
1349
1457
|
};
|
|
1350
1458
|
// W19: under plan the idle row makes the posture unmistakable — the W4
|
|
1351
1459
|
// parentheses idiom names the read-only constraint. The tier is the
|
|
@@ -1364,7 +1472,7 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
1364
1472
|
// DF-0330-F1: the row's budget. Without it the row is composed
|
|
1365
1473
|
// blind and invariant ① cuts whatever sits last — which is how
|
|
1366
1474
|
// a measured rate went missing at 100 columns.
|
|
1367
|
-
rowWidth()));
|
|
1475
|
+
rowWidth(), !floorOn));
|
|
1368
1476
|
};
|
|
1369
1477
|
// TUI2-R2 ⑥ — the BOOT status line. The row is the product's one
|
|
1370
1478
|
// persistent claim about itself (the tier, how to change it, the model,
|
|
@@ -1408,8 +1516,8 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
1408
1516
|
return;
|
|
1409
1517
|
spentUsd = (spentUsd ?? 0) + usd;
|
|
1410
1518
|
};
|
|
1411
|
-
const submitTurn = (line) => {
|
|
1412
|
-
const slot = { line, cancelled: false };
|
|
1519
|
+
const submitTurn = (line, via) => {
|
|
1520
|
+
const slot = { line: via?.line ?? line, content: line, ...(via !== undefined ? { via } : {}), cancelled: false };
|
|
1413
1521
|
pendingTurns.push(slot);
|
|
1414
1522
|
queued += 1;
|
|
1415
1523
|
chainRef.current = chainRef.current.then(async () => {
|
|
@@ -1433,7 +1541,7 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
1433
1541
|
if (session.uncertainExecutions().length > 0)
|
|
1434
1542
|
await resolveUncertains(session, input, () => cancelled);
|
|
1435
1543
|
if (session.uncertainExecutions().length === 0)
|
|
1436
|
-
return turn(line);
|
|
1544
|
+
return turn(line, undefined, via);
|
|
1437
1545
|
// The human declined (round 10: a cancelled ask records NOTHING —
|
|
1438
1546
|
// the execution stays uncertain and durable), so the turn does not
|
|
1439
1547
|
// start. It is never swallowed in silence: the held text is
|
|
@@ -1472,15 +1580,22 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
1472
1580
|
// the /resume+/clear mini-spec: the switch directive and the
|
|
1473
1581
|
// session-navigation seam (absent nav = the commands degrade to
|
|
1474
1582
|
// an honest refusal in dispatch)
|
|
1583
|
+
// DC-57: the dispatch edge asks this before it answers a line, so the
|
|
1584
|
+
// decision is made when the line's segment runs — not when its bytes
|
|
1585
|
+
// arrived, which can be before the switch was even requested.
|
|
1586
|
+
leaving: () => leaving,
|
|
1475
1587
|
requestSwitch: (id) => {
|
|
1476
1588
|
switchTo = id;
|
|
1589
|
+
leaving = true;
|
|
1477
1590
|
resolveEnd();
|
|
1478
1591
|
},
|
|
1479
1592
|
requestReload: () => {
|
|
1480
1593
|
reloadReq = true;
|
|
1594
|
+
leaving = true;
|
|
1481
1595
|
resolveEnd();
|
|
1482
1596
|
},
|
|
1483
1597
|
sessions: () => nav?.sessions() ?? [],
|
|
1598
|
+
...(nav?.route !== undefined ? { route: nav.route } : {}),
|
|
1484
1599
|
...(nav?.pick !== undefined ? { pickSession: nav.pick } : {}),
|
|
1485
1600
|
};
|
|
1486
1601
|
// the ergonomics batch C8: the auto-compact check — the /compact FULL path via the
|
|
@@ -1509,7 +1624,7 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
1509
1624
|
dispatch(line, dispatchCtx);
|
|
1510
1625
|
};
|
|
1511
1626
|
input.onLine((line) => {
|
|
1512
|
-
if (!replReady) {
|
|
1627
|
+
if (!replReady || leaving) {
|
|
1513
1628
|
queuedLines.push(line);
|
|
1514
1629
|
return;
|
|
1515
1630
|
}
|
|
@@ -1525,10 +1640,11 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
1525
1640
|
// they are the same sentinel: one implementation, one behaviour.
|
|
1526
1641
|
input.onCopy(() => dispatch("\x18copy", dispatchCtx));
|
|
1527
1642
|
// R3a — Shift+Tab: the approval-tier cycle (the /mode ring, in the
|
|
1528
|
-
//
|
|
1529
|
-
//
|
|
1643
|
+
// OFFERED order — a session started in `manual` steps to the first).
|
|
1644
|
+
// The switch is the SAME live-extension flip /mode performs; the status
|
|
1645
|
+
// row repaints at once with a one-line notice.
|
|
1530
1646
|
input.onModeCycle?.(() => {
|
|
1531
|
-
const next =
|
|
1647
|
+
const next = OFFERED_MODES[(OFFERED_MODES.indexOf(getMode()) + 1) % OFFERED_MODES.length];
|
|
1532
1648
|
setMode(next);
|
|
1533
1649
|
paintIdle();
|
|
1534
1650
|
body.notice(`mode → ${next} (shift+tab cycles)`);
|
|
@@ -1539,6 +1655,29 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
1539
1655
|
// round 8: the startup resume is bound to currentRun — Ctrl+C during it
|
|
1540
1656
|
// aborts the recovery, exactly like the interactive turns.
|
|
1541
1657
|
await resolveUncertains(session, input, () => cancelled);
|
|
1658
|
+
// 0.40.0 item 9: a big session whose cache has gone cold compacts BEFORE
|
|
1659
|
+
// its first request, on the person's word — or without asking in
|
|
1660
|
+
// dontAsk, where compacting is not an approval. A piped session is left
|
|
1661
|
+
// to the auto policy, which fires above the hard tier on its own. Lines
|
|
1662
|
+
// typed meanwhile queue behind the compaction.
|
|
1663
|
+
//
|
|
1664
|
+
// The owner's dogfood: asked BEFORE the recovery run. A session whose
|
|
1665
|
+
// last run was cut resumes that run at once, and the resume's first
|
|
1666
|
+
// request is the whole cold prefix — asked afterwards, the question is
|
|
1667
|
+
// about a bill already paid (and the in-run tiers have usually shrunk the
|
|
1668
|
+
// context by then, so it is never asked at all). The compaction settles
|
|
1669
|
+
// before the run resumes: the run continues on the compacted projection.
|
|
1670
|
+
const cold = !cancelled && process.stdin.isTTY ? coldResumeOffer(session) : null;
|
|
1671
|
+
if (cold !== null) {
|
|
1672
|
+
if (getMode() === "dontAsk") {
|
|
1673
|
+
body.notice(`[dontAsk] ${coldResumeLine(cold.tokens, cold.minutes)} — compacting first`);
|
|
1674
|
+
dispatch("/compact", dispatchCtx);
|
|
1675
|
+
}
|
|
1676
|
+
else if ((await askPanel(input, coldResumeView(cold.tokens, cold.minutes))).action === "allow") {
|
|
1677
|
+
dispatch("/compact", dispatchCtx);
|
|
1678
|
+
}
|
|
1679
|
+
await chainRef.current;
|
|
1680
|
+
}
|
|
1542
1681
|
if (!cancelled) {
|
|
1543
1682
|
const recoveryRun = session.resume();
|
|
1544
1683
|
currentRun = recoveryRun;
|
|
@@ -1590,8 +1729,8 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
1590
1729
|
// enough: the /compact segment appends nothing of its own.
|
|
1591
1730
|
await chainRef.current;
|
|
1592
1731
|
if (switchTo !== null)
|
|
1593
|
-
return { next: "switch", id: switchTo };
|
|
1732
|
+
return { next: "switch", id: switchTo, lines: queuedLines.slice() };
|
|
1594
1733
|
// a switch beats a reload: /resume and /clear are going somewhere else,
|
|
1595
1734
|
// and the agent they land on is rebuilt by the caller either way.
|
|
1596
|
-
return reloadReq ? { next: "reload", id: session.id } : { next: "exit" };
|
|
1735
|
+
return reloadReq ? { next: "reload", id: session.id, lines: queuedLines.slice() } : { next: "exit" };
|
|
1597
1736
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* "anthropic" | "openai-responses")
|
|
13
13
|
* models?: { [name]: { kind: "openai-compat"|"anthropic"|"openai-responses",
|
|
14
14
|
* baseUrl?: string, model: string, apiKeyEnv: string } }
|
|
15
|
-
* mode?: "manual"|"default"|"accept-edits"|"plan"|"bypass"
|
|
15
|
+
* mode?: "manual"|"default"|"accept-edits"|"plan"|"bypass"|"dontAsk"
|
|
16
16
|
* contextWindow?: number — tokens
|
|
17
17
|
* autoCompact?: { thresholdRatio: number } — 0<r<1; default off
|
|
18
18
|
* projectTrust?: "ask" | "never" — no "always" (the ruling)
|
|
@@ -82,6 +82,16 @@ export interface KisoConfig {
|
|
|
82
82
|
* project file is a LOUD error rather than a silent win. `KISO_THEME`
|
|
83
83
|
* still outranks it — the environment is the more local answer. */
|
|
84
84
|
readonly theme?: "dark" | "light";
|
|
85
|
+
/** 0.40.0: the catastrophe floor (floor.ts). Default on. USER-LEVEL
|
|
86
|
+
* ONLY, and louder than theme about it: a repository that could lower
|
|
87
|
+
* the floor would be the one thing the floor exists to stop. */
|
|
88
|
+
readonly floor?: "catastrophe" | "off";
|
|
89
|
+
/** kiso never serves its own credential store to a model, and these
|
|
90
|
+
* files join it: no tool reads, writes or searches them, and no shell
|
|
91
|
+
* line may name them (protected-shell.ts). Absolute, or `~/…`. USER-
|
|
92
|
+
* LEVEL ONLY, as loud as floor: a project must never be able to change
|
|
93
|
+
* what kiso guards. */
|
|
94
|
+
readonly protectedPaths?: readonly string[];
|
|
85
95
|
/** DT-1a: named acceptance checks a delegated task may reference —
|
|
86
96
|
* user-authored (or trust-gated project) commands, run by the PARENT
|
|
87
97
|
* in the child's worktree. A model never supplies a command; it names
|