@vincemakes/kiso-code 0.45.3 → 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/dist/index.js CHANGED
@@ -37,14 +37,16 @@ import { skillMenuItems } from "./skill-invoke.js";
37
37
  import { canonicalPath, hasSession, locateSession, projectLayoutActive, sessionFolders } from "./projects.js";
38
38
  import { reverseMigration } from "./session-migration.js";
39
39
  import { defaultTrashRoot, findEmptySessions, moveToTrash } from "./empty-sessions.js";
40
+ import { browserCommand, launchCommand } from "./launch.js";
40
41
  import { createFauxProvider } from "@vincemakes/kiso-evals";
41
42
  import { isProtectedPath, protectedIdentity, PROTECTED_REFUSAL } from "@vincemakes/kiso-tools-node";
42
- import { MODES, OFFERED_MODES, getMode, modeFromEnv, setMode } from "./mode.js";
43
+ import { OFFERED_MODES, applyModeState, envModeLayer, getDontAsk, modeDisplay, parseMode, resolveModeLayers } from "./mode.js";
43
44
  import { activeStoreDir, setActiveStoreDir, agentModel, atFiles, body, bodyLog, kisoHome, workspaceRoot, projectRoot, ownSessionsDir, setOpenSessionFolder, builtInExtensions, currentFaux, dock, loadedExtensions, mergedConfig, mergedTempPaths, modelChoice, projectExtensions, configModels, configuredWindow, agentBaseUrl, currentModelName, currentAgentExtensions, sessionStoreRef, sessionsDir, setAgentModel, setBody, setConfigModels, setConfiguredWindow, setCurrentAgentExtensions, setCurrentFaux, setCurrentModelName, setCurrentProfileName, setExtensionLists, protectedFiles, setMergedConfig, setModelChoice, setSessionStore, userExtensions, VERSION, lastBinding, acceptDrift, setAcceptDrift, loadedSkillsCatalog, queuedSwitchLines } from "./state.js";
44
45
  import { fauxSkip, readFauxScript } from "./faux-glue.js";
45
46
  import { chat, contextWindowTokens, displayCtxRatio, microcompactThresholdFor, statusModelLabel } from "./chat.js";
46
47
  import { preferences, usePreferences } from "./preferences.js";
47
- import { settingsLayers } from "./state.js";
48
+ import { settingsLayers, setChildTurnBudget, stopAllTasks } from "./state.js";
49
+ import { finishChild } from "./child-result.js";
48
50
  import { loadUserConfig, resolveAutoCompact } from "./config.js";
49
51
  import { checkForUpdate, knownUpdate, updateCardLines } from "./update-check.js";
50
52
  import { tmuxMouseHint } from "./tmux-hint.js";
@@ -74,14 +76,14 @@ export { applyProjectMerges } from "./trust-ui.js";
74
76
  * path's exact semantics (auto-denied asks, no chips, no pops). */
75
77
  /** OR-4: hand a URL to the platform's opener, detached, and never let a
76
78
  * missing or failing opener touch the sign-in — the printed URL is the
77
- * path that always works. macOS `open`, Linux `xdg-open`; elsewhere the
78
- * URL stays printed (Windows is unsupported by the CLI as a whole). */
79
+ * path that always works. macOS `open`, Linux `xdg-open`, Windows
80
+ * rundll32's URL handler (launch.ts); elsewhere the URL stays printed. */
79
81
  function openInBrowser(url) {
80
- const opener = process.platform === "darwin" ? "open" : process.platform === "linux" ? "xdg-open" : null;
82
+ const opener = browserCommand(url);
81
83
  if (opener === null)
82
84
  return;
83
85
  try {
84
- spawn(opener, [url], { detached: true, stdio: "ignore" }).unref();
86
+ spawn(opener[0], [...opener[1]], { detached: true, stdio: "ignore" }).unref();
85
87
  }
86
88
  catch {
87
89
  // the URL is on screen; opening it is the person's fallback
@@ -180,6 +182,9 @@ function readlineInput(rl) {
180
182
  /* §2.3: same as ctrl+o above — readline has no binding, and a
181
183
  * dock-less session has no committed rows to reprint. */
182
184
  },
185
+ onBackground() {
186
+ /* 3e: readline has no ctrl+b — /tasks and a steer still work. */
187
+ },
183
188
  onEditor() {
184
189
  /* §2.4: readline has no ctrl+g, and a piped session has no
185
190
  * composer to hand over. */
@@ -274,6 +279,9 @@ function editorInput(editor) {
274
279
  onThink(cb) {
275
280
  editor.onThink(cb);
276
281
  },
282
+ onBackground(cb) {
283
+ editor.onBackground(cb);
284
+ },
277
285
  onEditor(cb) {
278
286
  editor.onEditor(cb);
279
287
  },
@@ -488,10 +496,10 @@ function bannerExtensionText() {
488
496
  // terminal layer (extensionsBannerText — a pure function of the three
489
497
  // name lists, including the "(connecting…)" in-flight label). Which
490
498
  // lists exist is the CLI's fact and stays here.
491
- // 0.40.0: in dontAsk the ask extension is loaded but offers no tool
492
- // (builtin.ts offInDontAsk) — the banner says so beside the tier that
493
- // turns it off, rather than listing it as if it could ask.
494
- const builtIn = builtInExtensions.map((e) => (e.name === "ask" && getMode() === "dontAsk" ? { name: e.name, note: "off in dontAsk" } : e));
499
+ // 0.40.0: with the don't-ask switch on the ask extension is loaded but
500
+ // offers no tool (builtin.ts offInDontAsk) — the banner says so, rather
501
+ // than listing it as if it could ask.
502
+ const builtIn = builtInExtensions.map((e) => (e.name === "ask" && getDontAsk() ? { name: e.name, note: "off in dontAsk" } : e));
495
503
  return extensionsBannerText(builtIn, userExtensions, projectExtensions);
496
504
  }
497
505
  /** E1: the startup banner line(s) — TTY: logo + merged extensions + the
@@ -569,7 +577,7 @@ function extensionsBanner(resume = []) {
569
577
  };
570
578
  body.banner(VERSION, text.replace(/^ · /, ""), resume, {
571
579
  model: agentModel,
572
- mode: getMode() === "plan" ? "plan (read-only)" : getMode(),
580
+ mode: modeDisplay(),
573
581
  cwd: cwd.startsWith(home) ? `~${cwd.slice(home.length)}` : cwd,
574
582
  // DC-49 — REALPATH on both sides. A symlinked HOME (or a symlinked
575
583
  // cwd) compares unequal as raw strings while being the same
@@ -808,7 +816,7 @@ function paintBootStatus(session) {
808
816
  return;
809
817
  // DF-0330-F1: the BOOT row gets the budget too — it is the same row, and
810
818
  // the opening screen is where a long model id is first seen.
811
- dock.setStatus(idleStatus(getMode() === "plan" ? "plan (read-only)" : getMode(), statusModelLabel(session), displayCtxRatio(session), undefined, process.stdout.columns > 0 ? process.stdout.columns : 80));
819
+ dock.setStatus(idleStatus(modeDisplay(), statusModelLabel(session), displayCtxRatio(session), undefined, process.stdout.columns > 0 ? process.stdout.columns : 80));
812
820
  }
813
821
  /** PH-1a (finding PH-F12): a usage error raised from inside the TUI —
814
822
  * main's finally still runs (dock teardown, agent close, temp cleanup)
@@ -816,6 +824,9 @@ function paintBootStatus(session) {
816
824
  class CliUsageError extends Error {
817
825
  exitCode = 2;
818
826
  }
827
+ /** ADR-0058 3d: set when this process is a background child (its budget
828
+ * and where its answer goes) — finished once its turn is done. */
829
+ let childEnd = null;
819
830
  /** The /resume+/clear mini-spec — the chat LOOP: chat() ends with a
820
831
  * directive; a switch re-enters it on another session with the SAME
821
832
  * editor. First entry paints the banner; a switch paints one notice
@@ -1103,8 +1114,11 @@ async function chatLoop(first, firstId, input, autoCompact) {
1103
1114
  ...(process.stdin.isTTY ? { pick: () => pickSession(agent, input) } : {}),
1104
1115
  };
1105
1116
  const end = await chat(session, currentFaux, input, autoCompact, nav, seed);
1106
- if (end.next === "exit")
1117
+ if (end.next === "exit") {
1118
+ if (childEnd !== null)
1119
+ await finishChild(session, childEnd);
1107
1120
  return;
1121
+ }
1108
1122
  // DC-57: the lines that arrived with the switch command ride INTO the
1109
1123
  // next entry — they were aimed at the session being asked for.
1110
1124
  seed = end.lines ?? [];
@@ -1250,23 +1264,30 @@ async function main() {
1250
1264
  }
1251
1265
  args.splice(modelArgIdx, 2);
1252
1266
  }
1253
- // Modes: --mode wins over KISO_MODE, which wins over the USER config's
1254
- // mode (the project config's mode applies later — after the trust gate,
1255
- // inside createCodingAgent — unless a higher layer already decided).
1267
+ // Modes: flag > env > project config > user config > default, for the
1268
+ // tier and the don't-ask switch each (mode.ts resolveModeLayers). The
1269
+ // project config exists only once the trust gate has passed, so it joins
1270
+ // later (applyConfigMode); until then the other three decide.
1271
+ const flagLayer = {};
1256
1272
  const modeFlag = args.indexOf("--mode");
1257
1273
  if (modeFlag !== -1) {
1258
- const m = MODES.find((x) => x === args[modeFlag + 1]);
1259
- if (m === undefined) {
1260
- console.error(`unknown mode: ${args[modeFlag + 1]} (tiers: ${OFFERED_MODES.join(", ")})`);
1274
+ const raw = args[modeFlag + 1];
1275
+ if (raw === undefined || parseMode(raw) === undefined) {
1276
+ console.error(`unknown mode: ${raw} (tiers: ${OFFERED_MODES.join(", ")})`);
1261
1277
  process.exit(2);
1262
1278
  }
1263
- setMode(m);
1264
- settingsLayers.modeFlag = m;
1279
+ flagLayer.mode = raw;
1280
+ settingsLayers.modeFlag = raw;
1265
1281
  args.splice(modeFlag, 2);
1266
1282
  }
1267
- else {
1268
- setMode(modeFromEnv() ?? loadUserConfig()?.mode ?? "default");
1283
+ const dontAskFlag = args.indexOf("--dont-ask");
1284
+ if (dontAskFlag !== -1) {
1285
+ flagLayer.dontAsk = true;
1286
+ settingsLayers.dontAskFlag = true;
1287
+ args.splice(dontAskFlag, 1);
1269
1288
  }
1289
+ const modeLayers = (project) => ({ flag: flagLayer, env: envModeLayer(), project, user: loadUserConfig() });
1290
+ applyModeState(resolveModeLayers(modeLayers(null)));
1270
1291
  // CX-1 F5: --task-file <path> — the file is the ONE user turn (the
1271
1292
  // structured child entry). Read BEFORE any session exists: an
1272
1293
  // unreadable file is a usage error with nothing executed.
@@ -1291,6 +1312,35 @@ async function main() {
1291
1312
  args.splice(i, 2);
1292
1313
  }
1293
1314
  }
1315
+ // ADR-0058 3d: a background child's --max-turns N (its budget, D6) and
1316
+ // --result-file <path> (where its answer goes). Child-only: without
1317
+ // --task-file they are refused — the interactive door has no turn
1318
+ // limit (R3e).
1319
+ {
1320
+ const end = {};
1321
+ for (const flag of ["--max-turns", "--result-file"]) {
1322
+ const i = args.indexOf(flag);
1323
+ if (i === -1)
1324
+ continue;
1325
+ const value = args[i + 1];
1326
+ if (taskFile === undefined)
1327
+ throw new CliUsageError(`${flag} needs --task-file — it is for a delegated child only`);
1328
+ if (value === undefined || value.startsWith("--"))
1329
+ throw new CliUsageError(`${flag} needs a value`);
1330
+ if (flag === "--max-turns") {
1331
+ const n = Number(value);
1332
+ if (!Number.isInteger(n) || n < 1)
1333
+ throw new CliUsageError(`--max-turns: expected a whole number of at least 1, got ${value}`);
1334
+ end.maxTurns = n;
1335
+ }
1336
+ else
1337
+ end.resultFile = value;
1338
+ args.splice(i, 2);
1339
+ }
1340
+ if (end.maxTurns !== undefined || end.resultFile !== undefined)
1341
+ childEnd = end;
1342
+ setChildTurnBudget(end.maxTurns);
1343
+ }
1294
1344
  // 0.40.0: `kiso sessions --all | --current` — the one command that takes
1295
1345
  // them. Parsed only there, so neither ever becomes a session id. Absent
1296
1346
  // = each form's default (TTY: current; pipe: all, today's bytes).
@@ -1433,10 +1483,9 @@ async function main() {
1433
1483
  try {
1434
1484
  // merge round B: the project config's mode applies AFTER the trust gate
1435
1485
  // (its verdict decides whether the project config exists at all) —
1436
- // unless a higher layer (--mode flag / KISO_MODE) already decided.
1486
+ // under whatever a higher layer (the flags, the env) already decided.
1437
1487
  const applyConfigMode = () => {
1438
- if (modeFlag === -1 && process.env.KISO_MODE === undefined && mergedConfig.mode !== undefined)
1439
- setMode(mergedConfig.mode);
1488
+ applyModeState(resolveModeLayers(modeLayers(settingsLayers.project)));
1440
1489
  };
1441
1490
  if (printPrompt !== undefined) {
1442
1491
  // the -p flow: recovery-first one-shot, the resume() machinery
@@ -1553,7 +1602,7 @@ async function main() {
1553
1602
  console.log("kiso sessions --prune-empty --yes moves them to the Trash");
1554
1603
  }
1555
1604
  else
1556
- console.log(`moved ${n(empty.length, "empty session")} to ${moveToTrash(empty, defaultTrashRoot())}`);
1605
+ console.log(`moved ${n(empty.length, "empty session")} to ${moveToTrash(empty, defaultTrashRoot(kisoHome()))}`);
1557
1606
  if (inUse > 0)
1558
1607
  console.log(`skipped ${n(inUse, "empty session")} in use — a live process holds the lock`);
1559
1608
  break;
@@ -1719,7 +1768,9 @@ async function main() {
1719
1768
  // npm's to explain; the message names the manual command so
1720
1769
  // the person can run it with whatever their setup needs.
1721
1770
  const [bin, ...installArgs] = INSTALL_COMMAND;
1722
- const r = spawnSync(bin, installArgs, { stdio: "inherit" });
1771
+ // Windows: npm is npm.cmd — through cmd.exe (launch.ts)
1772
+ const l = launchCommand(bin, installArgs);
1773
+ const r = spawnSync(l.file, [...l.args], { stdio: "inherit", ...(l.windowsVerbatimArguments === true ? { windowsVerbatimArguments: true } : {}) });
1723
1774
  if (r.error !== undefined || r.status !== 0) {
1724
1775
  const why = r.error !== undefined ? `could not run npm (${r.error.message})` : `npm exited ${r.status ?? "on a signal"}`;
1725
1776
  process.stderr.write(`kiso update: ${why} — run it yourself: ${INSTALL_COMMAND.join(" ")}\n`);
@@ -1751,7 +1802,8 @@ async function main() {
1751
1802
  " kiso help this help\n\n" +
1752
1803
  "flags (any position):\n" +
1753
1804
  " --model <profile|provider/model> pick the model (also /model in-session)\n" +
1754
- " --mode <tier> approval tier: default|accept-edits|plan|dontAsk|bypass\n" +
1805
+ " --mode <tier> approval tier: default|accept-edits|plan|full-access\n" +
1806
+ " --dont-ask never stop for a person: what would ask is refused\n" +
1755
1807
  " --version print the version\n\n" +
1756
1808
  "configuration:\n" +
1757
1809
  " no key keyless faux demo (a scripted four-round session)\n" +
@@ -1795,11 +1847,19 @@ async function main() {
1795
1847
  // variable now). A signal death skips this and leaves the dead-pid
1796
1848
  // residue — the dead-holder takeover recovers it by design
1797
1849
  // (ADR-0050); the lock never outlives a live writer either way.
1850
+ // ADR-0058: a clean exit stops the session's tasks first — or, when
1851
+ // the person chose to leave them (3e), only the moved commands. What
1852
+ // could not be confirmed is said, never called stopped.
1853
+ const tasksAtExit = await stopAllTasks();
1798
1854
  agent?.close();
1799
1855
  // v2b: the dock tears down on EVERY exit path — CSI r resets the
1800
1856
  // scroll region, the cursor lands at the input line, no broken
1801
1857
  // terminal (kill -9 excepted; `reset` saves it).
1802
1858
  dock.exit();
1859
+ for (const id of tasksAtExit.unconfirmed)
1860
+ console.error(`${id}: stop unconfirmed — outcome unknown`);
1861
+ if (tasksAtExit.left.length > 0)
1862
+ console.error(`left running: ${tasksAtExit.left.join(", ")} — reopen this session to hear how ${tasksAtExit.left.length === 1 ? "it ends" : "they end"}`);
1803
1863
  // finding #8 (P1): extension dispose runs on the same exit path — a
1804
1864
  // dispose failure prints one line and NEVER changes the exit code.
1805
1865
  await disposeExtensions(loadedExtensions);
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Windows P3 — how the CLI's small spawns start: `kiso update` (npm), the
3
+ * external editor, and the sign-in page.
4
+ *
5
+ * npm is npm.cmd and an editor is often code.cmd: a .cmd/.bat shim needs
6
+ * cmd.exe to start, and Node will not start one without a shell. Rather
7
+ * than `shell: true`, the call goes to cmd.exe itself — `/d /s /c "<line>"`
8
+ * — with every word quoted by `viaCmd`, the one function here (the
9
+ * cross-spawn rules: CommandLineToArgvW quoting, then a caret before each
10
+ * cmd.exe metacharacter). The shell tool never comes this way: it only
11
+ * ever starts bash (P1).
12
+ *
13
+ * Pure — no process is started here, so importing it moves no test into
14
+ * the pty pool.
15
+ */
16
+ export interface Launch {
17
+ readonly file: string;
18
+ readonly args: readonly string[];
19
+ /** Windows: the line is cmd.exe's to parse, not Node's to requote */
20
+ readonly windowsVerbatimArguments?: true;
21
+ }
22
+ /** `bin args` as cmd.exe starts it. */
23
+ export declare function viaCmd(bin: string, args: readonly string[], env?: NodeJS.ProcessEnv): Launch;
24
+ /** `bin args` as this platform starts it: directly, or through cmd.exe on Windows. */
25
+ export declare function launchCommand(bin: string, args: readonly string[]): Launch;
26
+ /** The platform's URL opener — `rundll32`'s URL handler on Windows (cmd's
27
+ * `start` would cut a sign-in URL at its first `&`) — or null, and the
28
+ * URL stays printed. */
29
+ export declare function browserCommand(url: string): readonly [string, readonly string[]] | null;
package/dist/launch.js ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Windows P3 — how the CLI's small spawns start: `kiso update` (npm), the
3
+ * external editor, and the sign-in page.
4
+ *
5
+ * npm is npm.cmd and an editor is often code.cmd: a .cmd/.bat shim needs
6
+ * cmd.exe to start, and Node will not start one without a shell. Rather
7
+ * than `shell: true`, the call goes to cmd.exe itself — `/d /s /c "<line>"`
8
+ * — with every word quoted by `viaCmd`, the one function here (the
9
+ * cross-spawn rules: CommandLineToArgvW quoting, then a caret before each
10
+ * cmd.exe metacharacter). The shell tool never comes this way: it only
11
+ * ever starts bash (P1).
12
+ *
13
+ * Pure — no process is started here, so importing it moves no test into
14
+ * the pty pool.
15
+ */
16
+ /** cmd.exe's metacharacters: each is escaped with a caret. */
17
+ const CMD_META = /([()\][%!^"`<>&|;, *?])/g;
18
+ /** One argument, quoted as CommandLineToArgvW reads it, then escaped for cmd.exe. */
19
+ function cmdArgument(arg) {
20
+ const quoted = `"${arg.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, "$1$1")}"`;
21
+ return quoted.replace(CMD_META, "^$1");
22
+ }
23
+ /** `bin args` as cmd.exe starts it. */
24
+ export function viaCmd(bin, args, env = process.env) {
25
+ const line = [bin.replace(CMD_META, "^$1"), ...args.map(cmdArgument)].join(" ");
26
+ return { file: env.ComSpec ?? "cmd.exe", args: ["/d", "/s", "/c", `"${line}"`], windowsVerbatimArguments: true };
27
+ }
28
+ /** `bin args` as this platform starts it: directly, or through cmd.exe on Windows. */
29
+ export function launchCommand(bin, args) {
30
+ return process.platform === "win32" ? viaCmd(bin, args) : { file: bin, args };
31
+ }
32
+ /** The platform's URL opener — `rundll32`'s URL handler on Windows (cmd's
33
+ * `start` would cut a sign-in URL at its first `&`) — or null, and the
34
+ * URL stays printed. */
35
+ export function browserCommand(url) {
36
+ if (process.platform === "darwin")
37
+ return ["open", [url]];
38
+ if (process.platform === "linux")
39
+ return ["xdg-open", [url]];
40
+ if (process.platform === "win32")
41
+ return ["rundll32", ["url.dll,FileProtocolHandler", url]];
42
+ return null;
43
+ }
package/dist/mode.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Modes — the six built-in approval tiers, built ON the E1 policy chain
2
+ * Modes — the built-in approval tiers, built ON the E1 policy chain
3
3
  * (the kernel is untouched). Each tier is an in-process "mode:<name>"
4
4
  * extension whose decide() is live — it only speaks when it is the
5
5
  * CURRENT tier (otherwise abstain = no opinion, ADR-0042), so /mode
@@ -7,19 +7,37 @@
7
7
  * field: an automated denial records decidedBy: "mode:<name>" — the
8
8
  * audit sell. User-level extensions stay on the chain AFTER the mode
9
9
  * tiers; a user deny always wins (the chain's deny>allow>ask
10
- * monotonicity — bypass cannot override an extension deny).
10
+ * monotonicity — full-access cannot override an extension deny).
11
+ *
12
+ * Two questions, two settings (the modes round, 2026-09-30). The TIER
13
+ * answers what the agent may do. The DON'T-ASK SWITCH answers whether
14
+ * kiso may stop for a person, and it composes with every tier: it never
15
+ * grants anything — what would ask is refused, a model's question is not
16
+ * offered, an uncertain execution stays uncertain. The old `dontAsk` tier
17
+ * was that switch welded to default's authority; it is still accepted,
18
+ * and means exactly that.
11
19
  */
12
20
  import type { KisoExtension } from "@vincemakes/kiso-runtime";
13
- export type Mode = "manual" | "default" | "accept-edits" | "plan" | "bypass" | "dontAsk";
14
- /** Every tier kiso ACCEPTS — from a config, KISO_MODE, --mode, or
15
- * `/mode <name>`. */
21
+ export type Mode = "manual" | "default" | "accept-edits" | "plan" | "full-access";
22
+ /** Every tier kiso decides with. */
16
23
  export declare const MODES: readonly Mode[];
17
- /** The tiers kiso OFFERS a person (launch-weekend plan §2): the picker,
18
- * shift+tab, the printed list, the help. `manual` left the offer — a
19
- * saved allow outranks its ask, so "asks for every tool" was never
20
- * quite what it did — and stays accepted, so no config that names it
21
- * breaks. `dontAsk` joined: the unattended tier. */
24
+ /** The tiers kiso OFFERS a person: the picker, shift+tab, the printed
25
+ * list, the help. `manual` left the offer at 0.40.0 — a saved allow
26
+ * outranks its ask, so "asks for every tool" was never quite what it
27
+ * did — and stays accepted, so no config that names it breaks. */
22
28
  export declare const OFFERED_MODES: readonly Mode[];
29
+ /** What a person reads for each tier — the status row, the picker, the
30
+ * notices. The typed value (config, --mode, KISO_MODE) keeps its hyphen. */
31
+ export declare const MODE_LABEL: Readonly<Record<Mode, string>>;
32
+ /** One tier plus, for the one spelling that carried it, the switch. */
33
+ export interface ModeSetting {
34
+ readonly mode: Mode;
35
+ readonly dontAsk?: true;
36
+ }
37
+ /** Every value a config, KISO_MODE, --mode or `/mode <name>` accepts. */
38
+ export declare const MODE_VALUES: readonly string[];
39
+ /** A typed tier: its value, its label ("full access"), or an old name. */
40
+ export declare function parseMode(raw: string): ModeSetting | undefined;
23
41
  /** DC-36 — one line per tier, for the picker, TRANSCRIBED FROM decide()
24
42
  * below rather than written fresh. A description that drifts from the
25
43
  * behaviour is worse than none: this is the row a human reads before
@@ -34,21 +52,76 @@ export declare const OFFERED_MODES: readonly Mode[];
34
52
  * is unchanged; the copy was the thing that was wrong. `plan` needs no
35
53
  * qualification because it DENIES, and a deny is what nothing overrides. */
36
54
  export declare const MODE_NOTE: Readonly<Record<Mode, string>>;
55
+ /** The switch in one line, for a surface that describes it, under the
56
+ * same 60-column rule. The /mode panel does not offer it: don't ask is
57
+ * not a tier, so it never sits beside them (owner, 2026-09-30) — the
58
+ * panel's header names it when it is on. */
59
+ export declare const DONT_ASK_NOTE = "never asks: what would ask is refused and the run goes on";
60
+ /** The switch. "old-name" is a switch that arrived bundled with the old
61
+ * `dontAsk` tier: it leaves with the tier, as leaving dontAsk always
62
+ * gave the asks back. A switch set on its own ("on") outlives every tier
63
+ * change. */
64
+ export type DontAskState = "off" | "on" | "old-name";
37
65
  export declare function getMode(): Mode;
66
+ /** A tier switch. The switch stays as it is — unless it came with the
67
+ * old dontAsk tier, which this switch leaves. */
38
68
  export declare function setMode(m: Mode): void;
39
- /** The startup mode from env: KISO_MODE, or undefined when unset (the
40
- * config layer's mode then applies — merge round B; the --mode flag is
41
- * applied by main before the first createCodingAgent and wins over everything). */
42
- export declare function modeFromEnv(): Mode | undefined;
43
- /** The six built-in mode tiers as chain extensions — named "mode:<tier>"
69
+ /** A typed tier, old names included: `/mode dontAsk` is default with the
70
+ * switch on, exactly the tier it used to be. */
71
+ export declare function applyModeSetting(s: ModeSetting): void;
72
+ /** Whether kiso may stop for a person — every ask endpoint reads this. */
73
+ export declare function getDontAsk(): boolean;
74
+ export declare function setDontAsk(on: boolean): void;
75
+ /** The tier as the status row and the notices name it, with the switch
76
+ * beside it when it is on. */
77
+ export declare function modeDisplay(): string;
78
+ /** One layer's say: what a config file, the environment or the command
79
+ * line wrote. KisoConfig is one of these. */
80
+ export interface ModeLayer {
81
+ readonly mode?: string | undefined;
82
+ readonly dontAsk?: boolean | undefined;
83
+ }
84
+ export type ModeSource = "flag" | "env" | "project" | "user" | "default";
85
+ export interface ModeState {
86
+ readonly mode: Mode;
87
+ readonly dontAsk: DontAskState;
88
+ /** the layer each answer came from — /settings names it */
89
+ readonly from: {
90
+ readonly mode: ModeSource;
91
+ readonly dontAsk: ModeSource;
92
+ };
93
+ }
94
+ /** KISO_MODE and KISO_DONT_ASK. A value kiso cannot read is no value —
95
+ * the next layer decides, as an unknown KISO_MODE always did. */
96
+ export declare function envModeLayer(env?: Readonly<Record<string, string | undefined>>): ModeLayer;
97
+ /**
98
+ * Precedence: flag > env > project config > user config > default, for
99
+ * each answer on its own. The tier comes from the highest layer that
100
+ * names a readable one. The switch comes from the highest layer that
101
+ * either sets it (`--dont-ask`, KISO_DONT_ASK, "dontAsk": true|false) or
102
+ * WON THE TIER with the old name `dontAsk` — an old name's switch counts
103
+ * only where that old name decided the tier. So every combination of old
104
+ * spellings resolves as it did when dontAsk was a tier: a user config
105
+ * saying "dontAsk" under `--mode bypass` still asks, because --mode won.
106
+ */
107
+ export declare function resolveModeLayers(layers: {
108
+ readonly flag?: ModeLayer | null | undefined;
109
+ readonly env?: ModeLayer | null | undefined;
110
+ readonly project?: ModeLayer | null | undefined;
111
+ readonly user?: ModeLayer | null | undefined;
112
+ }): ModeState;
113
+ /** Put a resolved state in force — startup, and again once the project
114
+ * config is known. */
115
+ export declare function applyModeState(s: ModeState): void;
116
+ /** The built-in mode tiers as chain extensions — named "mode:<tier>"
44
117
  * so the runtime's decidedBy records exactly that (the runtime derives
45
118
  * approvalPolicies from extensions[].approvals, tagging each with the
46
119
  * extension name). The CURRENT tier is first: an all-allow chain records
47
120
  * decidedBy = the FIRST SPEAKER, so an auto-allow under the startup mode
48
121
  * names that mode honestly. Order never affects verdicts — the chain is
49
122
  * deny>allow>ask over the SPEAKING verdicts (abstain = no opinion), so a
50
- * user extension's deny wins over any mode tier, bypass included (the
51
- * monotonicity e2e pins it). */
123
+ * user extension's deny wins over any mode tier, full-access included
124
+ * (the monotonicity e2e pins it). */
52
125
  export declare function modeExtensions(workspaceRoot?: () => string): readonly KisoExtension[];
53
126
  /** The plan tier's system prompt addition — injected at startup when the
54
127
  * initial mode is plan (the session prompt is fixed at creation; runtime