@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.
@@ -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
@@ -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 { dirname, isAbsolute, join, relative } from "node:path";
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
- let current = isAbsolute(word) ? "/" : realCase(cwd);
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 { getMode } from "./mode.js";
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 (getMode() === "dontAsk") {
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:bypass")
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 || getMode() === "dontAsk") {
326
- console.error(`${getMode() === "dontAsk" ? "[dontAsk] " : ""}${projectUntrustedNote(artifacts.files.length, artifacts.root)}`);
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 (getMode() === "dontAsk") {
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.45.2",
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.45.2",
22
- "@vincemakes/kiso-core": "0.45.2",
23
- "@vincemakes/kiso-evals": "0.45.2",
24
- "@vincemakes/kiso-mcp-ext": "0.45.2",
25
- "@vincemakes/kiso-provider-anthropic": "0.45.2",
26
- "@vincemakes/kiso-provider-openai": "0.45.2",
27
- "@vincemakes/kiso-provider-openai-responses": "0.45.2",
28
- "@vincemakes/kiso-runtime": "0.45.2",
29
- "@vincemakes/kiso-skills-ext": "0.45.2",
30
- "@vincemakes/kiso-subagent-ext": "0.45.2",
31
- "@vincemakes/kiso-tools-node": "0.45.2",
32
- "@vincemakes/kiso-tui": "0.45.2",
33
- "@vincemakes/kiso-tui-cells": "0.45.2"
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",