@vincemakes/kiso-code 0.45.2 → 0.46.0
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 +2 -1
- package/dist/builtin.d.ts +5 -4
- package/dist/builtin.js +12 -7
- package/dist/chat.js +121 -19
- package/dist/child-result.d.ts +23 -0
- package/dist/child-result.js +59 -0
- package/dist/config.d.ts +13 -6
- package/dist/config.js +30 -16
- package/dist/create-coding-agent.js +3 -1
- package/dist/dispatch.d.ts +2 -0
- package/dist/dispatch.js +163 -31
- package/dist/empty-sessions.d.ts +4 -2
- package/dist/empty-sessions.js +6 -2
- package/dist/external-editor.js +4 -1
- package/dist/floor.js +71 -26
- package/dist/index.js +89 -29
- package/dist/launch.d.ts +29 -0
- package/dist/launch.js +43 -0
- package/dist/mode.d.ts +90 -17
- package/dist/mode.js +135 -32
- package/dist/protected-shell.js +60 -19
- package/dist/protected-writes.d.ts +2 -2
- package/dist/protected-writes.js +2 -2
- package/dist/readonly-shell.js +16 -14
- package/dist/replay.js +3 -0
- package/dist/resume.js +2 -2
- package/dist/settings.d.ts +3 -0
- package/dist/settings.js +41 -11
- package/dist/shell-words.d.ts +34 -1
- package/dist/shell-words.js +84 -4
- package/dist/state.d.ts +31 -0
- package/dist/state.js +88 -2
- package/dist/task-notice.d.ts +22 -0
- package/dist/task-notice.js +86 -0
- package/dist/trust-ui.js +6 -6
- package/package.json +14 -14
package/dist/shell-words.d.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* newline counts as `;`), each pipeline a series of simple commands joined
|
|
16
16
|
* by `|`, each command an argv plus its redirections.
|
|
17
17
|
*/
|
|
18
|
+
import { posix } from "node:path";
|
|
18
19
|
export interface Redirect {
|
|
19
20
|
/** The descriptor written before the operator (`2>`), null when none
|
|
20
21
|
* was written, or "&" for `&>` (stdout and stderr together). */
|
|
@@ -55,16 +56,48 @@ export declare function parseShell(src: string): ParseResult;
|
|
|
55
56
|
export interface ResolvedPath {
|
|
56
57
|
readonly canonical: string;
|
|
57
58
|
readonly inside: boolean;
|
|
59
|
+
/** Windows P2: why the word cannot be read as a path here — never
|
|
60
|
+
* inside, and refused under a destructive verb */
|
|
61
|
+
readonly opaque?: string;
|
|
58
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Windows P2 — how a path word reads. `posix` is every host but Windows,
|
|
65
|
+
* unchanged. `msys` is Git Bash on win32 (P1 runs commands through it):
|
|
66
|
+
* `C:\x`, `C:/x` and `/c/x` are `C:\x`, either separator separates, the
|
|
67
|
+
* disk is case-insensitive, and every form that cannot be read exactly is
|
|
68
|
+
* opaque (kiso-doc plan-windows-p2-design-2026-10-01.md).
|
|
69
|
+
*/
|
|
70
|
+
export interface PathDialect {
|
|
71
|
+
readonly msys: boolean;
|
|
72
|
+
readonly path: typeof posix;
|
|
73
|
+
}
|
|
74
|
+
/** The dialect of the shell that runs commands here — read per call. */
|
|
75
|
+
export declare function hostDialect(): PathDialect;
|
|
76
|
+
/**
|
|
77
|
+
* A Git Bash word as a Windows path, `/`-separated (`C:/x`, or relative),
|
|
78
|
+
* each component as Win32 reads it (trailing dots and spaces dropped) —
|
|
79
|
+
* or why it cannot be read: another machine, a device or namespace path,
|
|
80
|
+
* a drive-relative or drive-rooted path, an MSYS mount inside Git's own
|
|
81
|
+
* tree, a data stream, a device name. (An 8.3 short name needs no rule:
|
|
82
|
+
* Windows resolves an existing one, and the real path expands it.)
|
|
83
|
+
*/
|
|
84
|
+
export declare function msysPath(word: string): {
|
|
85
|
+
readonly native: string;
|
|
86
|
+
} | {
|
|
87
|
+
readonly opaque: string;
|
|
88
|
+
};
|
|
59
89
|
/** The well-known home subtrees that hold credentials or configuration
|
|
60
90
|
* that runs — one list for both chain members: the read-only allow
|
|
61
91
|
* never reads or lists under them, the floor never deletes them. */
|
|
62
92
|
export declare const HOME_SUBTREES: readonly string[];
|
|
93
|
+
/** The home subtrees for this host: on Windows, AppData too — credentials
|
|
94
|
+
* and configuration that runs live there (Windows P2). */
|
|
95
|
+
export declare function homeSubtrees(): readonly string[];
|
|
63
96
|
/** A real path in the case the DISK holds it. The JS realpath keeps the
|
|
64
97
|
* case it was given, so on a case-insensitive disk `.ENV` stayed `.ENV`
|
|
65
98
|
* and walked past every name-based predicate (the lead's review, B2). */
|
|
66
99
|
export declare function realCase(p: string): string;
|
|
67
|
-
export declare function resolveShellPath(workspaceRoot: string, cwd: string, word: string): ResolvedPath;
|
|
100
|
+
export declare function resolveShellPath(workspaceRoot: string, cwd: string, word: string, dialect?: PathDialect): ResolvedPath;
|
|
68
101
|
/**
|
|
69
102
|
* The FLOOR's reading of a command line (0.40.0 item 4) — the opposite
|
|
70
103
|
* contract to `parseShell`. The read-only allow must refuse whatever it
|
package/dist/shell-words.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* by `|`, each command an argv plus its redirections.
|
|
17
17
|
*/
|
|
18
18
|
import { existsSync, realpathSync } from "node:fs";
|
|
19
|
-
import {
|
|
19
|
+
import { posix, win32 } from "node:path";
|
|
20
20
|
const GLOB = new Set(["*", "?", "["]);
|
|
21
21
|
/**
|
|
22
22
|
* Parse a command line, or say why not. Pure: no filesystem, no
|
|
@@ -268,10 +268,72 @@ export function parseShell(src) {
|
|
|
268
268
|
return fail(f);
|
|
269
269
|
return { ok: true, list };
|
|
270
270
|
}
|
|
271
|
+
const POSIX_DIALECT = { msys: false, path: posix };
|
|
272
|
+
const MSYS_DIALECT = { msys: true, path: win32 };
|
|
273
|
+
/** The dialect of the shell that runs commands here — read per call. */
|
|
274
|
+
export function hostDialect() {
|
|
275
|
+
return process.platform === "win32" ? MSYS_DIALECT : POSIX_DIALECT;
|
|
276
|
+
}
|
|
277
|
+
const DEVICE_NAME = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i;
|
|
278
|
+
/**
|
|
279
|
+
* A Git Bash word as a Windows path, `/`-separated (`C:/x`, or relative),
|
|
280
|
+
* each component as Win32 reads it (trailing dots and spaces dropped) —
|
|
281
|
+
* or why it cannot be read: another machine, a device or namespace path,
|
|
282
|
+
* a drive-relative or drive-rooted path, an MSYS mount inside Git's own
|
|
283
|
+
* tree, a data stream, a device name. (An 8.3 short name needs no rule:
|
|
284
|
+
* Windows resolves an existing one, and the real path expands it.)
|
|
285
|
+
*/
|
|
286
|
+
export function msysPath(word) {
|
|
287
|
+
const w = word.replace(/\\/g, "/");
|
|
288
|
+
if (/^\/\/[?.]\//.test(w))
|
|
289
|
+
return { opaque: w[2] === "?" ? "a namespaced path" : "a device path" };
|
|
290
|
+
if (w.startsWith("//"))
|
|
291
|
+
return { opaque: "a UNC path (another machine)" };
|
|
292
|
+
let rest;
|
|
293
|
+
let drive = "";
|
|
294
|
+
const lettered = /^([A-Za-z]):(.*)$/s.exec(w);
|
|
295
|
+
const mounted = /^\/([A-Za-z])(\/.*)?$/s.exec(w);
|
|
296
|
+
if (lettered !== null) {
|
|
297
|
+
if (!lettered[2].startsWith("/"))
|
|
298
|
+
return { opaque: "a drive-relative path" };
|
|
299
|
+
drive = `${lettered[1].toUpperCase()}:`;
|
|
300
|
+
rest = lettered[2];
|
|
301
|
+
}
|
|
302
|
+
else if (mounted !== null) {
|
|
303
|
+
drive = `${mounted[1].toUpperCase()}:`;
|
|
304
|
+
rest = mounted[2] ?? "/";
|
|
305
|
+
}
|
|
306
|
+
else if (w.startsWith("/")) {
|
|
307
|
+
return { opaque: word.startsWith("\\") ? "a drive-rooted path" : "an MSYS mount (Git's own tree)" };
|
|
308
|
+
}
|
|
309
|
+
else
|
|
310
|
+
rest = w;
|
|
311
|
+
const parts = [];
|
|
312
|
+
for (const part of rest.split("/")) {
|
|
313
|
+
if (part === "" || part === "." || part === "..") {
|
|
314
|
+
parts.push(part);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (part.includes(":"))
|
|
318
|
+
return { opaque: "an alternate data stream" };
|
|
319
|
+
const name = part.replace(/[. ]+$/, "");
|
|
320
|
+
if (name === "")
|
|
321
|
+
return { opaque: "a name Win32 rewrites" };
|
|
322
|
+
if (DEVICE_NAME.test(name))
|
|
323
|
+
return { opaque: "a device name" };
|
|
324
|
+
parts.push(name);
|
|
325
|
+
}
|
|
326
|
+
return { native: drive + parts.join("/") };
|
|
327
|
+
}
|
|
271
328
|
/** The well-known home subtrees that hold credentials or configuration
|
|
272
329
|
* that runs — one list for both chain members: the read-only allow
|
|
273
330
|
* never reads or lists under them, the floor never deletes them. */
|
|
274
331
|
export const HOME_SUBTREES = [".ssh", ".config", ".kiso", ".gnupg", ".aws"];
|
|
332
|
+
/** The home subtrees for this host: on Windows, AppData too — credentials
|
|
333
|
+
* and configuration that runs live there (Windows P2). */
|
|
334
|
+
export function homeSubtrees() {
|
|
335
|
+
return hostDialect().msys ? [...HOME_SUBTREES, "AppData"] : HOME_SUBTREES;
|
|
336
|
+
}
|
|
275
337
|
/** A real path in the case the DISK holds it. The JS realpath keeps the
|
|
276
338
|
* case it was given, so on a case-insensitive disk `.ENV` stayed `.ENV`
|
|
277
339
|
* and walked past every name-based predicate (the lead's review, B2). */
|
|
@@ -283,16 +345,26 @@ export function realCase(p) {
|
|
|
283
345
|
return p;
|
|
284
346
|
}
|
|
285
347
|
}
|
|
286
|
-
export function resolveShellPath(workspaceRoot, cwd, word) {
|
|
348
|
+
export function resolveShellPath(workspaceRoot, cwd, word, dialect = hostDialect()) {
|
|
349
|
+
const { dirname, isAbsolute, join, relative } = dialect.path;
|
|
350
|
+
if (dialect.msys) {
|
|
351
|
+
const read = msysPath(word);
|
|
352
|
+
if ("opaque" in read)
|
|
353
|
+
return { canonical: word, inside: false, opaque: read.opaque };
|
|
354
|
+
if (!isAbsolute(read.native) && "opaque" in msysPath(cwd))
|
|
355
|
+
return { canonical: word, inside: false, opaque: "relative to a directory that cannot be read" };
|
|
356
|
+
word = read.native;
|
|
357
|
+
}
|
|
287
358
|
// B1 (the lead's review): `..` is taken on the REAL path, one component
|
|
288
359
|
// at a time — never collapsed as text first. `link-out/..` is the parent
|
|
289
360
|
// of where link-out POINTS, which is where the shell goes; collapsed as
|
|
290
361
|
// text it was the workspace, and `cat link-out/../etc/hosts` printed.
|
|
291
|
-
|
|
362
|
+
const top = isAbsolute(word) ? dialect.path.parse(word).root : "";
|
|
363
|
+
let current = top === "" ? realCase(cwd) : dialect.msys ? dialect.path.normalize(top) : top;
|
|
292
364
|
// past the last component that exists, the rest is text: nothing on
|
|
293
365
|
// disk can redirect a path that does not exist
|
|
294
366
|
const missing = [];
|
|
295
|
-
for (const part of word.split("/")) {
|
|
367
|
+
for (const part of word.slice(top.length).split("/")) {
|
|
296
368
|
if (part === "" || part === ".")
|
|
297
369
|
continue;
|
|
298
370
|
if (missing.length > 0) {
|
|
@@ -313,6 +385,14 @@ export function resolveShellPath(workspaceRoot, cwd, word) {
|
|
|
313
385
|
missing.push(part);
|
|
314
386
|
}
|
|
315
387
|
const canonical = missing.length > 0 ? join(current, ...missing) : current;
|
|
388
|
+
// Windows: a `..` after a link — Win32 takes `..` as text, the POSIX
|
|
389
|
+
// walk above takes it on the real path. Where the two land apart, the
|
|
390
|
+
// word is not read at all.
|
|
391
|
+
if (dialect.msys && word.split("/").includes("..")) {
|
|
392
|
+
const lexical = resolveShellPath(workspaceRoot, cwd, dialect.path.resolve(realCase(cwd), word), dialect);
|
|
393
|
+
if (lexical.canonical.toLowerCase() !== canonical.toLowerCase())
|
|
394
|
+
return { canonical: word, inside: false, opaque: "a `..` after a link (Windows and Git Bash may read it apart)" };
|
|
395
|
+
}
|
|
316
396
|
const rel = relative(realCase(workspaceRoot), canonical);
|
|
317
397
|
return { canonical, inside: rel === "" || (!rel.startsWith("..") && !isAbsolute(rel)) };
|
|
318
398
|
}
|
package/dist/state.d.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { Dock, type AtItem, type Body, type PanelVerdict, type PanelView, type SaferAnswer, type SessionCardView } from "@vincemakes/kiso-tui";
|
|
9
9
|
import type { KisoExtension, StoreRecord } from "@vincemakes/kiso-runtime";
|
|
10
|
+
import { TaskManager } from "@vincemakes/kiso-runtime/internal";
|
|
11
|
+
import { type ShellTasks } from "@vincemakes/kiso-tools-node";
|
|
10
12
|
/** finding #11: KISO_HOME is the ONE root — every default path derives from
|
|
11
13
|
* it (sessions, trust, extensions, mcp config, skills). The dedicated
|
|
12
14
|
* env vars (KISO_EXTENSIONS_DIR / KISO_MCP_CONFIG / KISO_SKILLS_DIR)
|
|
@@ -30,7 +32,27 @@ export declare function codingToolOptions(): {
|
|
|
30
32
|
readonly excludeRoots: readonly string[];
|
|
31
33
|
readonly secretEnvNames: readonly string[];
|
|
32
34
|
readonly protectedFiles: readonly string[];
|
|
35
|
+
readonly tasks: (sessionId: string | undefined) => ShellTasks | undefined;
|
|
33
36
|
};
|
|
37
|
+
export declare function tasksFor(sessionId: string | undefined): TaskManager | undefined;
|
|
38
|
+
export declare function setExitTasks(choice: "stop" | "leave"): void;
|
|
39
|
+
/** 3e: the unknown tasks the person has looked at in `/tasks` in this
|
|
40
|
+
* process — the status row stops counting them. */
|
|
41
|
+
export declare const seenUnknownTasks: Set<string>;
|
|
42
|
+
/** 3e: the live tasks of every session this process opened — a runner's
|
|
43
|
+
* (`durable`, it survives this process) and moved commands (`moved`). */
|
|
44
|
+
export declare function liveTasks(): {
|
|
45
|
+
readonly durable: number;
|
|
46
|
+
readonly moved: number;
|
|
47
|
+
};
|
|
48
|
+
/** A clean exit stops the session's tasks — all of them, or (the exit
|
|
49
|
+
* question's "leave") only the moved commands — and waits for their
|
|
50
|
+
* terminals. It returns what it could NOT confirm stopped, and what it
|
|
51
|
+
* left running: a stop requested is never reported as a stop (3e). */
|
|
52
|
+
export declare function stopAllTasks(): Promise<{
|
|
53
|
+
readonly unconfirmed: readonly string[];
|
|
54
|
+
readonly left: readonly string[];
|
|
55
|
+
}>;
|
|
34
56
|
export declare function setUserProtectedPaths(paths: readonly string[] | undefined): void;
|
|
35
57
|
/** kiso never serves its own credential store to a model. The store is
|
|
36
58
|
* `auth.json` under KISO_HOME — the path auth/credentials.ts writes (a
|
|
@@ -134,6 +156,9 @@ export interface LineInput {
|
|
|
134
156
|
onThink?(cb: () => void): void;
|
|
135
157
|
/** §2.4: the external-editor key (ctrl+g). */
|
|
136
158
|
onEditor?(cb: () => void): void;
|
|
159
|
+
/** ADR-0058 (3e): the background key (ctrl+b). Optional: the readline
|
|
160
|
+
* input has no such key — `/tasks` and a steer still reach the tasks. */
|
|
161
|
+
onBackground?(cb: () => void): void;
|
|
137
162
|
/** §2.4: hand the terminal over, run `edit`, take it back. The editor
|
|
138
163
|
* owns the handover; the CLI owns the spawn. */
|
|
139
164
|
externalEdit?(edit: (text: string) => string | null): void;
|
|
@@ -220,6 +245,11 @@ export declare function setSessionStore(value: {
|
|
|
220
245
|
/** The folder createCodingAgent built the store on — written there and by
|
|
221
246
|
* the resume picker's folder switch, read by the listings. */
|
|
222
247
|
export declare let activeStoreDir: string;
|
|
248
|
+
/** ADR-0058 3d (D6): a background child's turn budget (`--max-turns`, with
|
|
249
|
+
* `--task-file` only) — the agent's maxTurns. Unset everywhere else: the
|
|
250
|
+
* interactive door has no turn limit (R3e). */
|
|
251
|
+
export declare let childTurnBudget: number | undefined;
|
|
252
|
+
export declare function setChildTurnBudget(n: number | undefined): void;
|
|
223
253
|
export declare function setActiveStoreDir(value: string): void;
|
|
224
254
|
/** The model name for the status bar — set by createCodingAgent. */
|
|
225
255
|
export declare let agentModel: string;
|
|
@@ -309,6 +339,7 @@ export declare const settingsLayers: {
|
|
|
309
339
|
user: import("./config.js").KisoConfig | null;
|
|
310
340
|
project: import("./config.js").KisoConfig | null;
|
|
311
341
|
modeFlag: string | undefined;
|
|
342
|
+
dontAskFlag: boolean;
|
|
312
343
|
modelFlag: string | undefined;
|
|
313
344
|
modelSwitched: boolean;
|
|
314
345
|
};
|
package/dist/state.js
CHANGED
|
@@ -11,6 +11,8 @@ import { homedir } from "node:os";
|
|
|
11
11
|
import { dirname, join } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import { AT_CAP, AT_SKIP, Dock } from "@vincemakes/kiso-tui";
|
|
14
|
+
import { TaskManager } from "@vincemakes/kiso-runtime/internal";
|
|
15
|
+
import { processTaskBackend } from "@vincemakes/kiso-tools-node";
|
|
14
16
|
import { canonicalPath, LEGACY_SESSIONS_DIR, projectDirFor, projectLayoutActive } from "./projects.js";
|
|
15
17
|
/** finding #11: KISO_HOME is the ONE root — every default path derives from
|
|
16
18
|
* it (sessions, trust, extensions, mcp config, skills). The dedicated
|
|
@@ -39,7 +41,84 @@ export function secretEnvNamesOf(models) {
|
|
|
39
41
|
];
|
|
40
42
|
}
|
|
41
43
|
export function codingToolOptions() {
|
|
42
|
-
return { workspaceRoot: process.cwd(), excludeRoots: [kisoHome()], secretEnvNames: secretEnvNamesOf(configModels), protectedFiles: protectedFiles() };
|
|
44
|
+
return { workspaceRoot: process.cwd(), excludeRoots: [kisoHome()], secretEnvNames: secretEnvNamesOf(configModels), protectedFiles: protectedFiles(), tasks: tasksFor };
|
|
45
|
+
}
|
|
46
|
+
/** ADR-0058 (3b): one TaskManager per session, its directory beside the
|
|
47
|
+
* session's log (`<store dir>/<session>.tasks/`). A call without a session
|
|
48
|
+
* — the `!` gesture — has none, and its shell is today's. */
|
|
49
|
+
const taskManagers = new Map();
|
|
50
|
+
let taskBackend;
|
|
51
|
+
export function tasksFor(sessionId) {
|
|
52
|
+
if (sessionId === undefined || activeStoreDir === "")
|
|
53
|
+
return undefined;
|
|
54
|
+
let manager = taskManagers.get(sessionId);
|
|
55
|
+
if (manager === undefined) {
|
|
56
|
+
taskBackend ??= processTaskBackend();
|
|
57
|
+
manager = new TaskManager({ root: join(activeStoreDir, `${sessionId}.tasks`), backend: taskBackend });
|
|
58
|
+
manager.observe();
|
|
59
|
+
taskManagers.set(sessionId, manager);
|
|
60
|
+
}
|
|
61
|
+
return manager;
|
|
62
|
+
}
|
|
63
|
+
/** 3e: what the exit does with the tasks — the exit question's answer.
|
|
64
|
+
* "leave": a runner's task keeps running (the reopened session hears how
|
|
65
|
+
* it ends); a moved command is owned by this process and is stopped. */
|
|
66
|
+
let exitTasks = "stop";
|
|
67
|
+
export function setExitTasks(choice) {
|
|
68
|
+
exitTasks = choice;
|
|
69
|
+
}
|
|
70
|
+
/** 3e: the unknown tasks the person has looked at in `/tasks` in this
|
|
71
|
+
* process — the status row stops counting them. */
|
|
72
|
+
export const seenUnknownTasks = new Set();
|
|
73
|
+
/** 3e: the live tasks of every session this process opened — a runner's
|
|
74
|
+
* (`durable`, it survives this process) and moved commands (`moved`). */
|
|
75
|
+
export function liveTasks() {
|
|
76
|
+
let durable = 0;
|
|
77
|
+
let moved = 0;
|
|
78
|
+
for (const manager of taskManagers.values()) {
|
|
79
|
+
let list = [];
|
|
80
|
+
try {
|
|
81
|
+
list = manager.list();
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
continue; // a corrupt journal is reported where it is read on purpose
|
|
85
|
+
}
|
|
86
|
+
for (const t of list) {
|
|
87
|
+
if (t.state.kind !== "running" && t.state.kind !== "starting")
|
|
88
|
+
continue;
|
|
89
|
+
if (t.backend === "foreground")
|
|
90
|
+
moved += 1;
|
|
91
|
+
else
|
|
92
|
+
durable += 1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { durable, moved };
|
|
96
|
+
}
|
|
97
|
+
/** A clean exit stops the session's tasks — all of them, or (the exit
|
|
98
|
+
* question's "leave") only the moved commands — and waits for their
|
|
99
|
+
* terminals. It returns what it could NOT confirm stopped, and what it
|
|
100
|
+
* left running: a stop requested is never reported as a stop (3e). */
|
|
101
|
+
export async function stopAllTasks() {
|
|
102
|
+
const unconfirmed = [];
|
|
103
|
+
const left = [];
|
|
104
|
+
const many = taskManagers.size > 1;
|
|
105
|
+
await Promise.all([...taskManagers].map(async ([sessionId, manager]) => {
|
|
106
|
+
const name = (id) => (many ? `${sessionId}/${id}` : id);
|
|
107
|
+
if (exitTasks === "leave") {
|
|
108
|
+
try {
|
|
109
|
+
for (const t of manager.list())
|
|
110
|
+
if (t.backend === "process" && (t.state.kind === "running" || t.state.kind === "starting"))
|
|
111
|
+
left.push(name(t.id));
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// a corrupt journal: nothing to report as left
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const id of await manager.stopAll("exit", 8_000, exitTasks === "leave" ? "moved" : "all"))
|
|
118
|
+
unconfirmed.push(name(id));
|
|
119
|
+
manager.close();
|
|
120
|
+
}));
|
|
121
|
+
return { unconfirmed, left };
|
|
43
122
|
}
|
|
44
123
|
/** The user config's `protectedPaths`, as read at the agent's build (and
|
|
45
124
|
* again at /reload) — `~/` taken against the home directory. */
|
|
@@ -301,6 +380,13 @@ export function setSessionStore(value) {
|
|
|
301
380
|
/** The folder createCodingAgent built the store on — written there and by
|
|
302
381
|
* the resume picker's folder switch, read by the listings. */
|
|
303
382
|
export let activeStoreDir = "";
|
|
383
|
+
/** ADR-0058 3d (D6): a background child's turn budget (`--max-turns`, with
|
|
384
|
+
* `--task-file` only) — the agent's maxTurns. Unset everywhere else: the
|
|
385
|
+
* interactive door has no turn limit (R3e). */
|
|
386
|
+
export let childTurnBudget;
|
|
387
|
+
export function setChildTurnBudget(n) {
|
|
388
|
+
childTurnBudget = n;
|
|
389
|
+
}
|
|
304
390
|
export function setActiveStoreDir(value) {
|
|
305
391
|
activeStoreDir = value;
|
|
306
392
|
}
|
|
@@ -406,7 +492,7 @@ export const queuedSwitchLines = [];
|
|
|
406
492
|
/** 0.40.6: what /settings needs to name each value's layer — the two config
|
|
407
493
|
* files as read, the command-line flags as given, and whether /model
|
|
408
494
|
* changed the binding in this session. */
|
|
409
|
-
export const settingsLayers = { user: null, project: null, modeFlag: undefined, modelFlag: undefined, modelSwitched: false };
|
|
495
|
+
export const settingsLayers = { user: null, project: null, modeFlag: undefined, dontAskFlag: false, modelFlag: undefined, modelSwitched: false };
|
|
410
496
|
export let floorOn = true;
|
|
411
497
|
export function setFloorOn(value) {
|
|
412
498
|
floorOn = value;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { TaskDeliveryItem } from "@vincemakes/kiso-core";
|
|
2
|
+
import type { TaskInfo } from "@vincemakes/kiso-runtime/internal";
|
|
3
|
+
export declare function taskNoticeRow(items: readonly TaskDeliveryItem[]): string;
|
|
4
|
+
/** A task's state in the person's words. A stop that was asked for and is
|
|
5
|
+
* not yet confirmed reads `stopping` — never `stopped` before the journal
|
|
6
|
+
* says so; an outcome nobody can know reads `◌ outcome unknown`. */
|
|
7
|
+
export declare function taskStateLabel(task: TaskInfo): string;
|
|
8
|
+
/** What the task is: the command, or the child's role and task. */
|
|
9
|
+
export declare function taskWhat(task: TaskInfo): string;
|
|
10
|
+
/** One `/tasks` row: `t3 running 1m 20s`, with the command beside it. */
|
|
11
|
+
export declare function taskRow(task: TaskInfo, now?: number): {
|
|
12
|
+
readonly label: string;
|
|
13
|
+
readonly note: string;
|
|
14
|
+
};
|
|
15
|
+
/** A task's last `lines` lines of output — or, for a child, its answer. */
|
|
16
|
+
export declare function taskOutput(task: TaskInfo, lines?: number): string[];
|
|
17
|
+
/** The status row's counts: live tasks, and the unknown ones the person
|
|
18
|
+
* has not looked at in `/tasks` in this process. */
|
|
19
|
+
export declare function taskCounts(tasks: readonly TaskInfo[], seen: ReadonlySet<string>): {
|
|
20
|
+
readonly running: number;
|
|
21
|
+
readonly unknown: number;
|
|
22
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-0058 (3c): the row a task delivery shows in the transcript — live and
|
|
3
|
+
* on resume. The model reads the notice's own lines; a person reads this.
|
|
4
|
+
*
|
|
5
|
+
* 3e: and what the person sees of the tasks themselves — the `/tasks` rows,
|
|
6
|
+
* a task's output on request, and the counts on the status row. All of it
|
|
7
|
+
* is drawn on the screen only: no `user_input`, nothing in the session log,
|
|
8
|
+
* nothing the model sees (the model has its notices and `read_file`).
|
|
9
|
+
*/
|
|
10
|
+
import { closeSync, fstatSync, openSync, readFileSync, readSync } from "node:fs";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { elapsedLabel } from "@vincemakes/kiso-tui-cells";
|
|
13
|
+
export function taskNoticeRow(items) {
|
|
14
|
+
return `✦ task ${items.map((i) => `${i.taskId} ${i.transition}`).join(" · ")}`;
|
|
15
|
+
}
|
|
16
|
+
/** A task's state in the person's words. A stop that was asked for and is
|
|
17
|
+
* not yet confirmed reads `stopping` — never `stopped` before the journal
|
|
18
|
+
* says so; an outcome nobody can know reads `◌ outcome unknown`. */
|
|
19
|
+
export function taskStateLabel(task) {
|
|
20
|
+
const s = task.state;
|
|
21
|
+
switch (s.kind) {
|
|
22
|
+
case "starting":
|
|
23
|
+
return "starting";
|
|
24
|
+
case "not_run":
|
|
25
|
+
return "never ran";
|
|
26
|
+
case "running":
|
|
27
|
+
return task.stoppedBy !== undefined ? "stopping" : s.ready ? "ready" : "running";
|
|
28
|
+
case "unknown":
|
|
29
|
+
return "◌ outcome unknown";
|
|
30
|
+
case "ended":
|
|
31
|
+
if (s.stopped)
|
|
32
|
+
return "stopped";
|
|
33
|
+
if (s.error !== undefined)
|
|
34
|
+
return "failed to start";
|
|
35
|
+
return s.exitCode !== null ? (s.exitCode === 0 ? "exited 0" : `failed ${s.exitCode}`) : `ended ${s.signal ?? ""}`.trim();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** What the task is: the command, or the child's role and task. */
|
|
39
|
+
export function taskWhat(task) {
|
|
40
|
+
return task.agent !== undefined ? task.command : task.command.split("\n")[0];
|
|
41
|
+
}
|
|
42
|
+
/** One `/tasks` row: `t3 running 1m 20s`, with the command beside it. */
|
|
43
|
+
export function taskRow(task, now = Date.now()) {
|
|
44
|
+
const seconds = ((task.endedAt ?? now) - task.startedAt) / 1000;
|
|
45
|
+
return { label: `${task.id} ${taskStateLabel(task)} ${elapsedLabel(seconds)}`, note: taskWhat(task) };
|
|
46
|
+
}
|
|
47
|
+
/** A task's last `lines` lines of output — or, for a child, its answer. */
|
|
48
|
+
export function taskOutput(task, lines = 20) {
|
|
49
|
+
if (task.agent !== undefined) {
|
|
50
|
+
try {
|
|
51
|
+
return readFileSync(join(dirname(task.outputPath), "result.md"), "utf8").trimEnd().split("\n").slice(-lines);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// no answer written (yet): what it printed instead
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const fd = openSync(task.outputPath, "r");
|
|
59
|
+
try {
|
|
60
|
+
const size = fstatSync(fd).size;
|
|
61
|
+
const n = Math.min(size, 64 * 1024);
|
|
62
|
+
const buf = Buffer.alloc(n);
|
|
63
|
+
readSync(fd, buf, 0, n, size - n);
|
|
64
|
+
return buf.toString("utf8").trimEnd().split("\n").slice(-lines);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
closeSync(fd);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** The status row's counts: live tasks, and the unknown ones the person
|
|
75
|
+
* has not looked at in `/tasks` in this process. */
|
|
76
|
+
export function taskCounts(tasks, seen) {
|
|
77
|
+
let running = 0;
|
|
78
|
+
let unknown = 0;
|
|
79
|
+
for (const t of tasks) {
|
|
80
|
+
if (t.state.kind === "running" || t.state.kind === "starting")
|
|
81
|
+
running += 1;
|
|
82
|
+
else if (t.state.kind === "unknown" && !seen.has(t.id))
|
|
83
|
+
unknown += 1;
|
|
84
|
+
}
|
|
85
|
+
return { running, unknown };
|
|
86
|
+
}
|
package/dist/trust-ui.js
CHANGED
|
@@ -15,7 +15,7 @@ import { bodyLog, currentAgentExtensions, dock, extensionsDir, kisoHome, mergedT
|
|
|
15
15
|
import { guardSavedAllow } from "./protected-writes.js";
|
|
16
16
|
import { mergeDirPrefix } from "./temp-sweep.js";
|
|
17
17
|
import { loadUserConfig, resolveProjectTrustPolicy } from "./config.js";
|
|
18
|
-
import {
|
|
18
|
+
import { getDontAsk } from "./mode.js";
|
|
19
19
|
/**
|
|
20
20
|
* W21 — ask the human with the approval panel: the bounded block that
|
|
21
21
|
* replaces the running tool's live window while the approval is pending.
|
|
@@ -150,7 +150,7 @@ export function askUi(input) {
|
|
|
150
150
|
// Launch-weekend plan §2: dontAsk asks nobody anything — a
|
|
151
151
|
// question the model puts is declined, recorded as unanswered,
|
|
152
152
|
// exactly as a non-interactive session declines it.
|
|
153
|
-
if (
|
|
153
|
+
if (getDontAsk()) {
|
|
154
154
|
bodyLog("[dontAsk] the model's question was declined — nothing asks in dontAsk");
|
|
155
155
|
return askDeclineAll(spec);
|
|
156
156
|
}
|
|
@@ -284,7 +284,7 @@ export function fixHintFor(speaker, tool) {
|
|
|
284
284
|
}
|
|
285
285
|
if (speaker === "mode:manual" || speaker === "mode:plan")
|
|
286
286
|
return "/mode default";
|
|
287
|
-
if (speaker === "mode:accept-edits" || speaker === "mode:
|
|
287
|
+
if (speaker === "mode:accept-edits" || speaker === "mode:full-access")
|
|
288
288
|
return undefined;
|
|
289
289
|
return `edit ${join(extensionsDir(), `${speaker}.mjs`).replace(homedir(), "~")}`;
|
|
290
290
|
}
|
|
@@ -322,8 +322,8 @@ export async function resolveProjectTrust(input) {
|
|
|
322
322
|
// The lead's review of #63, B5: dontAsk takes this same path — nothing
|
|
323
323
|
// asks, so the project is not loaded, the note says so, and no refusal
|
|
324
324
|
// is recorded (a later asking session can still decide).
|
|
325
|
-
if (!process.stdin.isTTY ||
|
|
326
|
-
console.error(`${
|
|
325
|
+
if (!process.stdin.isTTY || getDontAsk()) {
|
|
326
|
+
console.error(`${getDontAsk() ? "[dontAsk] " : ""}${projectUntrustedNote(artifacts.files.length, artifacts.root)}`);
|
|
327
327
|
return null;
|
|
328
328
|
}
|
|
329
329
|
// v2c: the shared input (the editor on a TTY) reads the answer.
|
|
@@ -476,7 +476,7 @@ export async function resolveUncertains(session, input, isCancelled) {
|
|
|
476
476
|
// uncertain executions are left exactly as a cancel leaves them —
|
|
477
477
|
// uncertain and durable, no rerun or abandon fabricated — and the one
|
|
478
478
|
// line says what to do. An unattended session must not sit on a panel.
|
|
479
|
-
if (
|
|
479
|
+
if (getDontAsk()) {
|
|
480
480
|
const n = session.uncertainExecutions().length;
|
|
481
481
|
if (n > 0)
|
|
482
482
|
bodyLog(`[dontAsk] ${n} uncertain execution${n === 1 ? "" : "s"} left unresolved — resolve them in an asking mode`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.0",
|
|
4
4
|
"description": "kiso-code \u2014 the coding agent on kiso, the durable runtime for AI agents: kiso chat / kiso resume / kiso sessions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,19 +18,19 @@
|
|
|
18
18
|
"test": "vitest run"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@vincemakes/kiso-ask-ext": "0.
|
|
22
|
-
"@vincemakes/kiso-core": "0.
|
|
23
|
-
"@vincemakes/kiso-evals": "0.
|
|
24
|
-
"@vincemakes/kiso-mcp-ext": "0.
|
|
25
|
-
"@vincemakes/kiso-provider-anthropic": "0.
|
|
26
|
-
"@vincemakes/kiso-provider-openai": "0.
|
|
27
|
-
"@vincemakes/kiso-provider-openai-responses": "0.
|
|
28
|
-
"@vincemakes/kiso-runtime": "0.
|
|
29
|
-
"@vincemakes/kiso-skills-ext": "0.
|
|
30
|
-
"@vincemakes/kiso-subagent-ext": "0.
|
|
31
|
-
"@vincemakes/kiso-tools-node": "0.
|
|
32
|
-
"@vincemakes/kiso-tui": "0.
|
|
33
|
-
"@vincemakes/kiso-tui-cells": "0.
|
|
21
|
+
"@vincemakes/kiso-ask-ext": "0.46.0",
|
|
22
|
+
"@vincemakes/kiso-core": "0.46.0",
|
|
23
|
+
"@vincemakes/kiso-evals": "0.46.0",
|
|
24
|
+
"@vincemakes/kiso-mcp-ext": "0.46.0",
|
|
25
|
+
"@vincemakes/kiso-provider-anthropic": "0.46.0",
|
|
26
|
+
"@vincemakes/kiso-provider-openai": "0.46.0",
|
|
27
|
+
"@vincemakes/kiso-provider-openai-responses": "0.46.0",
|
|
28
|
+
"@vincemakes/kiso-runtime": "0.46.0",
|
|
29
|
+
"@vincemakes/kiso-skills-ext": "0.46.0",
|
|
30
|
+
"@vincemakes/kiso-subagent-ext": "0.46.0",
|
|
31
|
+
"@vincemakes/kiso-tools-node": "0.46.0",
|
|
32
|
+
"@vincemakes/kiso-tui": "0.46.0",
|
|
33
|
+
"@vincemakes/kiso-tui-cells": "0.46.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^26.1.2",
|