@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.
Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/dist/attachments.d.ts +1 -1
  3. package/dist/attachments.js +11 -4
  4. package/dist/builtin.d.ts +12 -0
  5. package/dist/builtin.js +22 -1
  6. package/dist/chat.d.ts +31 -3
  7. package/dist/chat.js +169 -30
  8. package/dist/config.d.ts +11 -1
  9. package/dist/config.js +34 -3
  10. package/dist/dispatch.d.ts +12 -2
  11. package/dist/dispatch.js +165 -24
  12. package/dist/floor.d.ts +80 -0
  13. package/dist/floor.js +652 -0
  14. package/dist/index.d.ts +21 -2
  15. package/dist/index.js +463 -46
  16. package/dist/mode.d.ts +12 -4
  17. package/dist/mode.js +32 -10
  18. package/dist/projects.d.ts +106 -0
  19. package/dist/projects.js +212 -0
  20. package/dist/protected-shell.d.ts +88 -0
  21. package/dist/protected-shell.js +406 -0
  22. package/dist/protected-writes.d.ts +29 -0
  23. package/dist/protected-writes.js +54 -0
  24. package/dist/provider-label.d.ts +32 -0
  25. package/dist/provider-label.js +53 -0
  26. package/dist/readonly-shell.d.ts +58 -0
  27. package/dist/readonly-shell.js +590 -0
  28. package/dist/replay.d.ts +53 -0
  29. package/dist/replay.js +158 -0
  30. package/dist/resume-tail.js +4 -1
  31. package/dist/resume.js +3 -3
  32. package/dist/retries.d.ts +9 -0
  33. package/dist/retries.js +16 -0
  34. package/dist/session-cards.d.ts +33 -4
  35. package/dist/session-cards.js +44 -3
  36. package/dist/session-migration.d.ts +104 -0
  37. package/dist/session-migration.js +319 -0
  38. package/dist/shell-words.d.ts +134 -0
  39. package/dist/shell-words.js +628 -0
  40. package/dist/skill-invoke.d.ts +58 -0
  41. package/dist/skill-invoke.js +139 -0
  42. package/dist/state.d.ts +80 -1
  43. package/dist/state.js +113 -2
  44. package/dist/trust-ui.js +29 -5
  45. package/package.json +15 -15
@@ -0,0 +1,139 @@
1
+ /**
2
+ * 0.40.0 — a PERSON invokes a skill.
3
+ *
4
+ * `/skill <name> [args…]`, or `/<name> [args…]` when no built-in command
5
+ * has that name — built-ins win, always, so installing a skill can never
6
+ * take a command away. The invocation is a USER TURN: the SKILL.md body,
7
+ * then the person's args after a blank line, submitted through the same
8
+ * path as typed input. The durable record says the turn came from the
9
+ * person (`source` stays "user") and says HOW it was composed in `via`,
10
+ * which carries the line they typed — so the chip, the resume tail and the
11
+ * session's name show what the person wrote, not a body they never saw.
12
+ *
13
+ * Pure: the catalog comes from the skills extension's own scan (one scan,
14
+ * one answer — the model's index and this door are one list), and every
15
+ * outcome is data. The dispatcher does the printing and the submitting.
16
+ */
17
+ /**
18
+ * What a submitted line asks of the skills, if anything. `null` means the
19
+ * line is not a skill request, and the dispatcher goes on as before — which
20
+ * is how a typo stays "unknown command" rather than becoming a skill error.
21
+ */
22
+ export function resolveSkillLine(trimmed, catalog, builtins) {
23
+ if (trimmed.includes("\n") || !trimmed.startsWith("/"))
24
+ return null;
25
+ const [word = "", ...rest] = trimmed.split(/\s+/);
26
+ if (word === "/skills")
27
+ return rest.length === 0 ? { kind: "list" } : { kind: "error", message: "usage: /skills (no arguments) — /skill <name> [args] runs one" };
28
+ if (word === "/skill") {
29
+ const [name = ""] = rest;
30
+ if (name === "")
31
+ return { kind: "error", message: "usage: /skill <name> [args…] — /skills lists them" };
32
+ return invoke(name, argsOf(trimmed, 2), trimmed, catalog);
33
+ }
34
+ // `/<name>`: only a word no built-in claims, and only a skill that
35
+ // exists — anything else is the dispatcher's own "unknown command".
36
+ if (builtins.includes(word))
37
+ return null;
38
+ const name = word.slice(1);
39
+ if (catalog === null || (!catalog.entries.some((e) => e.name === name) && !catalog.broken.some((b) => b.dir === name)))
40
+ return null;
41
+ return invoke(name, argsOf(trimmed, 1), trimmed, catalog);
42
+ }
43
+ /** The args as typed — everything after the first `words` words, with its
44
+ * own spacing kept. A skill that asks for a path wants the path, not a
45
+ * re-joined approximation of it. */
46
+ function argsOf(trimmed, words) {
47
+ let rest = trimmed;
48
+ for (let i = 0; i < words; i += 1)
49
+ rest = rest.replace(/^\S+\s*/, "");
50
+ return rest;
51
+ }
52
+ function invoke(name, args, line, catalog) {
53
+ const entry = catalog?.entries.find((e) => e.name === name);
54
+ if (entry === undefined) {
55
+ const broken = catalog?.broken.find((b) => b.dir === name);
56
+ if (broken !== undefined)
57
+ return { kind: "error", message: `skill "${name}" cannot load: ${broken.reason}` };
58
+ const near = nearest(name, catalog?.entries.map((e) => e.name) ?? []);
59
+ const hint = near.length > 0 ? ` — nearest: ${near.join(", ")}` : "";
60
+ return { kind: "error", message: `no skill named "${name}"${hint} (/skills lists them)` };
61
+ }
62
+ if (!entry.userInvocable)
63
+ return { kind: "error", message: `skill "${name}" is for the model only (user-invocable: false)` };
64
+ const read = catalog.body(name);
65
+ if ("error" in read)
66
+ return { kind: "error", message: `skill "${name}" cannot load: ${read.error}` };
67
+ return {
68
+ kind: "submit",
69
+ content: args === "" ? read.body : `${read.body}\n\n${args}`,
70
+ via: { kind: "skill", name, line },
71
+ };
72
+ }
73
+ /** Up to three installed names close to the one typed: a shared prefix, or
74
+ * within two edits. Closest first; nothing when nothing is close — a
75
+ * suggestion that is not near is noise. */
76
+ export function nearest(name, names) {
77
+ const scored = names
78
+ .map((n) => ({ n, d: n.startsWith(name) || name.startsWith(n) ? 0 : distance(name, n) }))
79
+ .filter((s) => s.d <= 2)
80
+ .sort((a, b) => a.d - b.d || (a.n < b.n ? -1 : 1));
81
+ return scored.slice(0, 3).map((s) => s.n);
82
+ }
83
+ function distance(a, b) {
84
+ const row = Array.from({ length: b.length + 1 }, (_, j) => j);
85
+ for (let i = 1; i <= a.length; i += 1) {
86
+ let prev = row[0];
87
+ row[0] = i;
88
+ for (let j = 1; j <= b.length; j += 1) {
89
+ const cur = row[j];
90
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
91
+ prev = cur;
92
+ }
93
+ }
94
+ return row[b.length];
95
+ }
96
+ /** `/skills` — the skills grouped by the directory they really live in,
97
+ * each directory named ONCE as a header: project and user skills are
98
+ * merged into one scan directory by symlink, so the scan root would name a
99
+ * temp directory nobody can find — `sourceOf` resolves an entry's
100
+ * directory to where it came from. Under each: `/name — description`, a
101
+ * `model only` tag for `user-invocable: false`, then the broken entries
102
+ * with the loader's own reason. */
103
+ export function skillsRows(catalog, sourceOf, userDir) {
104
+ const entries = catalog?.entries ?? [];
105
+ const broken = catalog?.broken ?? [];
106
+ if (entries.length === 0 && broken.length === 0)
107
+ return [`no skills installed — add one as ${userDir}/<name>/SKILL.md`];
108
+ const groups = new Map();
109
+ const add = (dir, row) => {
110
+ const src = sourceOf(dir);
111
+ groups.set(src, [...(groups.get(src) ?? []), row]);
112
+ };
113
+ for (const e of entries)
114
+ add(e.dir, ` /${e.name} — ${e.description}${e.userInvocable ? "" : " (model only)"}`);
115
+ for (const b of broken)
116
+ add(b.dir, ` ${b.dir} — cannot load: ${b.reason}`);
117
+ const rows = [];
118
+ for (const [src, list] of groups)
119
+ rows.push(src, ...list);
120
+ rows.push("/<name> [args] or /skill <name> [args] runs one · a built-in command wins a shared name");
121
+ return rows;
122
+ }
123
+ /**
124
+ * 0.40.1 (owner's ruling, revoking the 0.41.0 deferral) — the installed
125
+ * skills as `/` menu entries. The same rule the dispatcher follows, so the
126
+ * menu never offers what `/<name>` would not run: invocable skills only
127
+ * (`user-invocable: false` stays the model's), a name a person can type
128
+ * after `/`, and never a name a built-in claims — the built-in wins. The
129
+ * description is cut so a long one cannot crowd the band.
130
+ */
131
+ export function skillMenuItems(catalog, builtins) {
132
+ if (catalog === null)
133
+ return [];
134
+ const taken = new Set(builtins);
135
+ const cut = (text, n) => (text.length <= n ? text : `${text.slice(0, n - 1)}…`);
136
+ return catalog.entries
137
+ .filter((e) => e.userInvocable && /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(e.name) && !taken.has(`/${e.name}`))
138
+ .map((e) => ({ name: `/${e.name}`, desc: `${cut(e.description, 56)} · skill` }));
139
+ }
package/dist/state.d.ts CHANGED
@@ -29,7 +29,40 @@ export declare function codingToolOptions(): {
29
29
  readonly workspaceRoot: string;
30
30
  readonly excludeRoots: readonly string[];
31
31
  readonly secretEnvNames: readonly string[];
32
+ readonly protectedFiles: readonly string[];
32
33
  };
34
+ export declare function setUserProtectedPaths(paths: readonly string[] | undefined): void;
35
+ /** kiso never serves its own credential store to a model. The store is
36
+ * `auth.json` under KISO_HOME — the path auth/credentials.ts writes (a
37
+ * test holds the two together); its temp file and lock are covered by
38
+ * the match rule (tools-node protected.ts). Then the user's own list.
39
+ * ONE list for every reader: the file tools, the search, the shell
40
+ * check and the `!` gesture. */
41
+ export declare function protectedFiles(): readonly string[];
42
+ /** 0.40.0 — the workspace a session records: the realpath of where kiso
43
+ * runs, so `/a/link` and `/a/target` are one workspace (and one folder). */
44
+ export declare function workspaceRoot(): string;
45
+ /** 0.40.0 — the project a session folder belongs to: the cwd as the disk
46
+ * spells it (projects.ts canonicalPath). Only the folder's identity uses
47
+ * it; the tools keep workspaceRoot(). */
48
+ export declare function projectRoot(): string;
49
+ /**
50
+ * 0.40.0 (the owner's dogfood) — this project's session folder
51
+ * (projects.ts). `KISO_SESSIONS_DIR` pins it (tests, bench runners, a
52
+ * delegated child beside its parent), and a reversed migration restores
53
+ * the single legacy folder.
54
+ *
55
+ * A pure read — the folder is claimed (made, and its workspace recorded)
56
+ * only after the trust gate, where the store is built.
57
+ */
58
+ export declare function ownSessionsDir(): string;
59
+ /** 0.40.0 (the lead's ruling) — a session from `_unknown`, or placed in
60
+ * another project's folder only by inference, resumes WHERE IT IS: a
61
+ * resume never moves a session. The store is then built on that folder,
62
+ * and null returns this process to its own. */
63
+ export declare function setOpenSessionFolder(dir: string | null): void;
64
+ /** The folder of the session this process has open: its own project's,
65
+ * or the one an unknown or inferred session was resumed in. */
33
66
  export declare function sessionsDir(): string;
34
67
  /** E1: the extension scan directory — KISO_EXTENSIONS_DIR overrides. */
35
68
  export declare function extensionsDir(): string;
@@ -141,7 +174,7 @@ export interface LineInput {
141
174
  * picked id, or null when the human leaves without picking.
142
175
  * OPTIONAL: only the raw-mode editor has it, and the picker only
143
176
  * ever opens on a TTY (a pipe has nobody to pick). */
144
- pick?(cards: () => readonly SessionCardView[], onPick: (id: string | null) => void): void;
177
+ pick?(cards: () => readonly SessionCardView[], onPick: (id: string | null) => void, here?: string): void;
145
178
  /** W22: bind the pending-turn queue — the ↑ pop walks the CLI's
146
179
  * live slots (each pop cancels the turn), esc ends the walk after
147
180
  * one more pop. The chips are the compositor's own bindQueue. */
@@ -195,6 +228,23 @@ export declare function setCurrentFaux(value: boolean): void;
195
228
  * LAST makeAgent — /model and autoCompact resolve against it. */
196
229
  export declare let mergedConfig: import("./config.js").KisoConfig;
197
230
  export declare function setMergedConfig(value: import("./config.js").KisoConfig): void;
231
+ /** ADR-0005 Amendment 2: the retry the kernel announced and is waiting
232
+ * on, for the running and compacting rows — `until` is the wall-clock
233
+ * end of the wait. Null when no retry is pending. Cleared by the next
234
+ * event from the run, which means the attempt got through, and wherever
235
+ * a run or a compaction starts and ends. */
236
+ export interface RetryShown {
237
+ readonly attempt: number;
238
+ readonly maxRetries: number;
239
+ readonly code: string;
240
+ readonly until: number;
241
+ }
242
+ export declare let retryShown: RetryShown | null;
243
+ export declare function setRetryShown(value: RetryShown | null): void;
244
+ /** The pending retry as a row shows it, with the time left computed NOW:
245
+ * the rows repaint on their own timers, so the countdown moves between
246
+ * the kernel's announcements. */
247
+ export declare function retryOnRow(): import("@vincemakes/kiso-tui").RetryOnRow | null;
198
248
  /** merge round B: the resolved context window (env > config.contextWindow) —
199
249
  * chat.ts's contextWindowTokens() consults it before the env. */
200
250
  export declare let configuredWindow: number | undefined;
@@ -225,7 +275,28 @@ export declare let modelChoice: string | undefined;
225
275
  export declare function setModelChoice(value: string | undefined): void;
226
276
  export declare let currentModelName: string;
227
277
  export declare function setCurrentModelName(value: string): void;
278
+ /** REVIEW (2026-09-21): WHO the binding came from is a different fact from
279
+ * WHICH model it names, and `currentModelName` carried both — a profile alias
280
+ * after `/model`, a model id after a switch, a resume or a reload. Surfaces
281
+ * that want to name the PROFILE ask this one; an unknown profile is null, and
282
+ * nothing guesses a credential identity out of a model name and a host. */
283
+ export declare let currentProfileName: string | null;
284
+ export declare function setCurrentProfileName(value: string | null): void;
285
+ /** DC-57 (the owner's ruling, 2026-09-21): lines that arrived with a switch
286
+ * command — a paste, a pipe, a scripted driver — belong to the session the
287
+ * person asked for. The departing entry queues them here and the next
288
+ * `chat()` drains them into its own replay, so no line is answered by the
289
+ * session being left and none is dropped. */
290
+ export declare const queuedSwitchLines: string[];
228
291
  /** E1: the extensions loaded by makeAgent — their names feed the banner. */
292
+ /** 0.40.0: whether the catastrophe floor is on (floor.ts) — the user
293
+ * config's `floor`, read where the chain is assembled. */
294
+ export declare let floorOn: boolean;
295
+ export declare function setFloorOn(value: boolean): void;
296
+ /** 0.40.0: the calls a saved allow never carries (protected-writes.ts) —
297
+ * set where the chain is assembled, read where a first grant joins it. */
298
+ export declare let neverInherited: (call: import("@vincemakes/kiso-core").PolicyCall) => boolean;
299
+ export declare function setNeverInherited(value: (call: import("@vincemakes/kiso-core").PolicyCall) => boolean): void;
229
300
  export declare let loadedExtensions: readonly KisoExtension[];
230
301
  /** R-D 0.1.45: the BUILT-IN layer — the three default official extensions,
231
302
  * shipped with the cli (module imports, never a disk scan; builtin.ts).
@@ -239,6 +310,11 @@ export declare let userExtensions: readonly KisoExtension[];
239
310
  * banner distinguishes them from the user-level ones. */
240
311
  export declare let projectExtensions: readonly KisoExtension[];
241
312
  export declare function setExtensionLists(builtIn: readonly KisoExtension[], user: readonly KisoExtension[], project: readonly KisoExtension[], loaded: readonly KisoExtension[]): void;
313
+ /** 0.40.0: the skills catalog of the loaded skills extension — the scan the
314
+ * model's index came from, so `/skill` and `/skills` cannot list a
315
+ * different set. Null when no skills extension is loaded (or a user
316
+ * extension named "skills" shadows it and reports none). */
317
+ export declare function loadedSkillsCatalog(): import("@vincemakes/kiso-skills-ext").SkillsCatalog | null;
242
318
  /** W21: the CURRENT agent's extensions array — set by makeAgent, the
243
319
  * don't-ask-again writer pushes the generated extension into it so a
244
320
  * first-time rule joins the chain at the NEXT run (the run's policies
@@ -266,6 +342,9 @@ export interface LastBinding {
266
342
  readonly baseUrl?: string;
267
343
  readonly scope?: import("@vincemakes/kiso-core").ContinuationScope;
268
344
  readonly reasoning?: import("@vincemakes/kiso-runtime/internal").ReasoningSetting;
345
+ /** 0.40.0: the config profile that named the binding (null for a direct
346
+ * provider/model) — a /clear-fresh session records it too. */
347
+ readonly profileName?: string | null;
269
348
  }
270
349
  export declare function setLastBinding(b: LastBinding): void;
271
350
  export declare function lastBinding(): LastBinding | null;
package/dist/state.js CHANGED
@@ -11,6 +11,7 @@ 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 { canonicalPath, LEGACY_SESSIONS_DIR, projectDirFor, projectLayoutActive } from "./projects.js";
14
15
  /** finding #11: KISO_HOME is the ONE root — every default path derives from
15
16
  * it (sessions, trust, extensions, mcp config, skills). The dedicated
16
17
  * env vars (KISO_EXTENSIONS_DIR / KISO_MCP_CONFIG / KISO_SKILLS_DIR)
@@ -38,10 +39,74 @@ export function secretEnvNamesOf(models) {
38
39
  ];
39
40
  }
40
41
  export function codingToolOptions() {
41
- return { workspaceRoot: process.cwd(), excludeRoots: [kisoHome()], secretEnvNames: secretEnvNamesOf(configModels) };
42
+ return { workspaceRoot: process.cwd(), excludeRoots: [kisoHome()], secretEnvNames: secretEnvNamesOf(configModels), protectedFiles: protectedFiles() };
42
43
  }
44
+ /** The user config's `protectedPaths`, as read at the agent's build (and
45
+ * again at /reload) — `~/` taken against the home directory. */
46
+ let userProtectedPaths = [];
47
+ export function setUserProtectedPaths(paths) {
48
+ userProtectedPaths = (paths ?? []).map((p) => (p.startsWith("~/") ? join(homedir(), p.slice(2)) : p));
49
+ }
50
+ /** kiso never serves its own credential store to a model. The store is
51
+ * `auth.json` under KISO_HOME — the path auth/credentials.ts writes (a
52
+ * test holds the two together); its temp file and lock are covered by
53
+ * the match rule (tools-node protected.ts). Then the user's own list.
54
+ * ONE list for every reader: the file tools, the search, the shell
55
+ * check and the `!` gesture. */
56
+ export function protectedFiles() {
57
+ return [join(kisoHome(), "auth.json"), ...userProtectedPaths];
58
+ }
59
+ /** 0.40.0 — the workspace a session records: the realpath of where kiso
60
+ * runs, so `/a/link` and `/a/target` are one workspace (and one folder). */
61
+ export function workspaceRoot() {
62
+ try {
63
+ return realpathSync(process.cwd());
64
+ }
65
+ catch {
66
+ return process.cwd();
67
+ }
68
+ }
69
+ /** 0.40.0 — the project a session folder belongs to: the cwd as the disk
70
+ * spells it (projects.ts canonicalPath). Only the folder's identity uses
71
+ * it; the tools keep workspaceRoot(). */
72
+ export function projectRoot() {
73
+ return canonicalPath(process.cwd());
74
+ }
75
+ let projectSessions = null;
76
+ /**
77
+ * 0.40.0 (the owner's dogfood) — this project's session folder
78
+ * (projects.ts). `KISO_SESSIONS_DIR` pins it (tests, bench runners, a
79
+ * delegated child beside its parent), and a reversed migration restores
80
+ * the single legacy folder.
81
+ *
82
+ * A pure read — the folder is claimed (made, and its workspace recorded)
83
+ * only after the trust gate, where the store is built.
84
+ */
85
+ export function ownSessionsDir() {
86
+ const pinned = process.env.KISO_SESSIONS_DIR;
87
+ if (pinned !== undefined && pinned !== "")
88
+ return pinned;
89
+ const home = kisoHome();
90
+ if (!projectLayoutActive(home))
91
+ return join(home, LEGACY_SESSIONS_DIR);
92
+ const workspace = projectRoot();
93
+ const key = `${home}\0${workspace}`;
94
+ if (projectSessions?.key !== key)
95
+ projectSessions = { key, dir: projectDirFor(home, workspace) };
96
+ return projectSessions.dir;
97
+ }
98
+ let openFolder = null;
99
+ /** 0.40.0 (the lead's ruling) — a session from `_unknown`, or placed in
100
+ * another project's folder only by inference, resumes WHERE IT IS: a
101
+ * resume never moves a session. The store is then built on that folder,
102
+ * and null returns this process to its own. */
103
+ export function setOpenSessionFolder(dir) {
104
+ openFolder = dir;
105
+ }
106
+ /** The folder of the session this process has open: its own project's,
107
+ * or the one an unknown or inferred session was resumed in. */
43
108
  export function sessionsDir() {
44
- return join(kisoHome(), "sessions");
109
+ return openFolder ?? ownSessionsDir();
45
110
  }
46
111
  /** E1: the extension scan directory — KISO_EXTENSIONS_DIR overrides. */
47
112
  export function extensionsDir() {
@@ -254,6 +319,17 @@ export let mergedConfig = {};
254
319
  export function setMergedConfig(value) {
255
320
  mergedConfig = value;
256
321
  }
322
+ export let retryShown = null;
323
+ export function setRetryShown(value) {
324
+ retryShown = value;
325
+ }
326
+ /** The pending retry as a row shows it, with the time left computed NOW:
327
+ * the rows repaint on their own timers, so the countdown moves between
328
+ * the kernel's announcements. */
329
+ export function retryOnRow() {
330
+ const r = retryShown;
331
+ return r === null ? null : { attempt: r.attempt, maxRetries: r.maxRetries, code: r.code, remainingMs: r.until - Date.now() };
332
+ }
257
333
  /** merge round B: the resolved context window (env > config.contextWindow) —
258
334
  * chat.ts's contextWindowTokens() consults it before the env. */
259
335
  export let configuredWindow;
@@ -292,7 +368,34 @@ export let currentModelName = "faux";
292
368
  export function setCurrentModelName(value) {
293
369
  currentModelName = value;
294
370
  }
371
+ /** REVIEW (2026-09-21): WHO the binding came from is a different fact from
372
+ * WHICH model it names, and `currentModelName` carried both — a profile alias
373
+ * after `/model`, a model id after a switch, a resume or a reload. Surfaces
374
+ * that want to name the PROFILE ask this one; an unknown profile is null, and
375
+ * nothing guesses a credential identity out of a model name and a host. */
376
+ export let currentProfileName = null;
377
+ export function setCurrentProfileName(value) {
378
+ currentProfileName = value;
379
+ }
380
+ /** DC-57 (the owner's ruling, 2026-09-21): lines that arrived with a switch
381
+ * command — a paste, a pipe, a scripted driver — belong to the session the
382
+ * person asked for. The departing entry queues them here and the next
383
+ * `chat()` drains them into its own replay, so no line is answered by the
384
+ * session being left and none is dropped. */
385
+ export const queuedSwitchLines = [];
295
386
  /** E1: the extensions loaded by makeAgent — their names feed the banner. */
387
+ /** 0.40.0: whether the catastrophe floor is on (floor.ts) — the user
388
+ * config's `floor`, read where the chain is assembled. */
389
+ export let floorOn = true;
390
+ export function setFloorOn(value) {
391
+ floorOn = value;
392
+ }
393
+ /** 0.40.0: the calls a saved allow never carries (protected-writes.ts) —
394
+ * set where the chain is assembled, read where a first grant joins it. */
395
+ export let neverInherited = () => false;
396
+ export function setNeverInherited(value) {
397
+ neverInherited = value;
398
+ }
296
399
  export let loadedExtensions = [];
297
400
  /** R-D 0.1.45: the BUILT-IN layer — the three default official extensions,
298
401
  * shipped with the cli (module imports, never a disk scan; builtin.ts).
@@ -311,6 +414,14 @@ export function setExtensionLists(builtIn, user, project, loaded) {
311
414
  projectExtensions = project;
312
415
  loadedExtensions = loaded;
313
416
  }
417
+ /** 0.40.0: the skills catalog of the loaded skills extension — the scan the
418
+ * model's index came from, so `/skill` and `/skills` cannot list a
419
+ * different set. Null when no skills extension is loaded (or a user
420
+ * extension named "skills" shadows it and reports none). */
421
+ export function loadedSkillsCatalog() {
422
+ const ext = loadedExtensions.find((e) => e.name === "skills");
423
+ return ext?.catalog ?? null;
424
+ }
314
425
  /** W21: the CURRENT agent's extensions array — set by makeAgent, the
315
426
  * don't-ask-again writer pushes the generated extension into it so a
316
427
  * first-time rule joins the chain at the NEXT run (the run's policies
package/dist/trust-ui.js CHANGED
@@ -11,7 +11,8 @@ import { join } from "node:path";
11
11
  import { pathToFileURL } from "node:url";
12
12
  import { askDeclineAll, askView, projectTrustRows, projectTrustView, projectUntrustedNote, uncertainView, unansweredAskView } from "@vincemakes/kiso-tui";
13
13
  import { projectArtifacts, recordTrust, trustFor } from "@vincemakes/kiso-runtime";
14
- import { bodyLog, currentAgentExtensions, dock, extensionsDir, kisoHome, mergedTempPaths } from "./state.js";
14
+ import { bodyLog, currentAgentExtensions, dock, extensionsDir, kisoHome, mergedTempPaths, neverInherited } from "./state.js";
15
+ import { guardSavedAllow } from "./protected-writes.js";
15
16
  import { loadUserConfig, resolveProjectTrustPolicy } from "./config.js";
16
17
  import { getMode } from "./mode.js";
17
18
  /**
@@ -145,6 +146,13 @@ export function parseFallbackAnswer(answer) {
145
146
  export function askUi(input) {
146
147
  return {
147
148
  ask: async (spec) => {
149
+ // Launch-weekend plan §2: dontAsk asks nobody anything — a
150
+ // question the model puts is declined, recorded as unanswered,
151
+ // exactly as a non-interactive session declines it.
152
+ if (getMode() === "dontAsk") {
153
+ bodyLog("[dontAsk] the model's question was declined — nothing asks in dontAsk");
154
+ return askDeclineAll(spec);
155
+ }
148
156
  const verdict = await askPanel(input, askView(spec));
149
157
  return verdict.action === "answers" ? verdict.result : askDeclineAll(spec);
150
158
  },
@@ -236,10 +244,13 @@ export async function addDontAskAgainRule(rule) {
236
244
  const mod = (await import(`${pathToFileURL(file).href}?grant=${(grantSerial += 1)}`));
237
245
  if (mod.default === undefined)
238
246
  return;
247
+ // 0.40.0: wrapped as at startup — a first grant must not carry what a
248
+ // saved allow never carries.
249
+ const joined = guardSavedAllow(mod.default, neverInherited);
239
250
  if (at >= 0)
240
- currentAgentExtensions[at] = mod.default;
251
+ currentAgentExtensions[at] = joined;
241
252
  else
242
- currentAgentExtensions.push(mod.default);
253
+ currentAgentExtensions.push(joined);
243
254
  }
244
255
  /** RL-F5 — the rules a generated file already holds. The `new Set([...])`
245
256
  * literal is JSON, which is what makes reading it back honest rather than
@@ -307,8 +318,11 @@ export async function resolveProjectTrust(input) {
307
318
  return null; // refused is sticky — no re-ask
308
319
  // First discovery — list every artifact (file name + digest short
309
320
  // prefix) and ask the human ONCE.
310
- if (!process.stdin.isTTY) {
311
- console.error(projectUntrustedNote(artifacts.files.length, artifacts.root));
321
+ // The lead's review of #63, B5: dontAsk takes this same path — nothing
322
+ // asks, so the project is not loaded, the note says so, and no refusal
323
+ // is recorded (a later asking session can still decide).
324
+ if (!process.stdin.isTTY || getMode() === "dontAsk") {
325
+ console.error(`${getMode() === "dontAsk" ? "[dontAsk] " : ""}${projectUntrustedNote(artifacts.files.length, artifacts.root)}`);
312
326
  return null;
313
327
  }
314
328
  // v2c: the shared input (the editor on a TTY) reads the answer.
@@ -457,6 +471,16 @@ function readdirSyncSafe(dir) {
457
471
  * fabricated, round 10). The resolution feedback lands in the body —
458
472
  * the body is the single stdout writer, never a stray console.log. */
459
473
  export async function resolveUncertains(session, input, isCancelled) {
474
+ // The lead's review of #63, B5: under dontAsk nothing asks, so the
475
+ // uncertain executions are left exactly as a cancel leaves them —
476
+ // uncertain and durable, no rerun or abandon fabricated — and the one
477
+ // line says what to do. An unattended session must not sit on a panel.
478
+ if (getMode() === "dontAsk") {
479
+ const n = session.uncertainExecutions().length;
480
+ if (n > 0)
481
+ bodyLog(`[dontAsk] ${n} uncertain execution${n === 1 ? "" : "s"} left unresolved — resolve them in an asking mode`);
482
+ return;
483
+ }
460
484
  for (const uncertain of session.uncertainExecutions()) {
461
485
  // KC3.5 §4: an interrupted ask_user is not a side effect that may
462
486
  // have applied — it is a question nobody answered. The COPY says
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-code",
3
- "version": "0.39.2",
3
+ "version": "0.40.1",
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,20 +18,20 @@
18
18
  "test": "vitest run"
19
19
  },
20
20
  "dependencies": {
21
- "@vincemakes/kiso-ask-ext": "0.39.2",
22
- "@vincemakes/kiso-core": "0.39.2",
23
- "@vincemakes/kiso-evals": "0.39.2",
24
- "@vincemakes/kiso-mcp-ext": "0.39.2",
25
- "@vincemakes/kiso-provider-anthropic": "0.39.2",
26
- "@vincemakes/kiso-provider-openai": "0.39.2",
27
- "@vincemakes/kiso-provider-openai-responses": "0.39.2",
28
- "@vincemakes/kiso-runtime": "0.39.2",
29
- "@vincemakes/kiso-skills-ext": "0.39.2",
30
- "@vincemakes/kiso-subagent-ext": "0.39.2",
31
- "@vincemakes/kiso-task-ext": "0.39.2",
32
- "@vincemakes/kiso-tools-node": "0.39.2",
33
- "@vincemakes/kiso-tui": "0.39.2",
34
- "@vincemakes/kiso-tui-cells": "0.39.2"
21
+ "@vincemakes/kiso-ask-ext": "0.40.1",
22
+ "@vincemakes/kiso-core": "0.40.1",
23
+ "@vincemakes/kiso-evals": "0.40.1",
24
+ "@vincemakes/kiso-mcp-ext": "0.40.1",
25
+ "@vincemakes/kiso-provider-anthropic": "0.40.1",
26
+ "@vincemakes/kiso-provider-openai": "0.40.1",
27
+ "@vincemakes/kiso-provider-openai-responses": "0.40.1",
28
+ "@vincemakes/kiso-runtime": "0.40.1",
29
+ "@vincemakes/kiso-skills-ext": "0.40.1",
30
+ "@vincemakes/kiso-subagent-ext": "0.40.1",
31
+ "@vincemakes/kiso-task-ext": "0.40.1",
32
+ "@vincemakes/kiso-tools-node": "0.40.1",
33
+ "@vincemakes/kiso-tui": "0.40.1",
34
+ "@vincemakes/kiso-tui-cells": "0.40.1"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^26.1.2",