@vincemakes/kiso-code 0.52.0 → 0.54.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/chat.d.ts CHANGED
@@ -411,5 +411,7 @@ export interface ChatNav {
411
411
  /** 0.40.0: an id outside this project's folder — refused, or opened where it is */
412
412
  readonly route?: (id: string) => SessionRoute | null;
413
413
  readonly pick?: () => Promise<string | null>;
414
+ /** the session's first message was sent: the caller may title the session from it (Session.title) */
415
+ readonly titled?: (firstMessage: string) => void;
414
416
  }
415
417
  export declare function chat(session: AgentSession, faux: boolean, input: LineInput, autoCompact?: AutoCompact, nav?: ChatNav, seed?: readonly string[]): Promise<ChatEnd>;
package/dist/chat.js CHANGED
@@ -1386,7 +1386,11 @@ export async function chat(session, faux, input, autoCompact, nav, seed) {
1386
1386
  // 0.40.0: a skill turn is not scanned either — its text is a
1387
1387
  // SKILL.md body, and a body that mentions `diagram.png` must not
1388
1388
  // attach a file from the workspace the person never pointed at.
1389
+ input.setGhost?.(null); // a new turn: the last run's follow-up no longer applies
1389
1390
  const content = via !== undefined ? text : attachImages(text, input.attachments?.(), protectedFiles());
1391
+ // the person's first message of the session: a title is made from it beside the run, never in its way
1392
+ if (via === undefined && !session.log.all.some((e) => e.type === "user_input"))
1393
+ nav?.titled?.(text);
1390
1394
  // ADR-0058: a task notice is the runtime's input — source "system"
1391
1395
  const run = via !== undefined ? session.run(content, { via, ...(via.kind === "tasks" ? { source: "system" } : {}) }) : session.run(content);
1392
1396
  currentRun = run;
@@ -1435,6 +1439,15 @@ export async function chat(session, faux, input, autoCompact, nav, seed) {
1435
1439
  // round 8: after EVERY turn the prompt is re-armed — the human
1436
1440
  // never types blind after the first turn.
1437
1441
  input.prompt();
1442
+ // the follow-up (owner 2026-11-03): after a run that ended well, the line the person may send next,
1443
+ // asked of the session's own model with the run's own prefix (Session.followUp); shown dim in the
1444
+ // empty box only while no other turn has started or waits; off with the faux model and KISO_FOLLOWUP=off
1445
+ if (last?.type === "terminal" && last.outcome.kind === "completed" && !faux && input.setGhost !== undefined && process.env["KISO_FOLLOWUP"] !== "off" && queued === 0) {
1446
+ void session.followUp({ maxTokens: 200, signal: AbortSignal.timeout(15_000) }).then((line) => {
1447
+ if (turnNo === myTurn && currentRun === null && queued === 0 && !leaving)
1448
+ input.setGhost?.(line === "" ? null : line);
1449
+ }, () => undefined);
1450
+ }
1438
1451
  // the ergonomics batch C8: the opt-in auto-compact — checked AFTER the
1439
1452
  // turn ended (the run's terminal is in the log, the ratio
1440
1453
  // is post-run).
@@ -1889,11 +1902,13 @@ export async function chat(session, faux, input, autoCompact, nav, seed) {
1889
1902
  requestSwitch: (id) => {
1890
1903
  switchTo = id;
1891
1904
  leaving = true;
1905
+ input.setGhost?.(null); // REL-0530-F1: the follow-up belongs to the session being left
1892
1906
  resolveEnd();
1893
1907
  },
1894
1908
  requestReload: () => {
1895
1909
  reloadReq = true;
1896
1910
  leaving = true;
1911
+ input.setGhost?.(null);
1897
1912
  resolveEnd();
1898
1913
  },
1899
1914
  sessions: () => nav?.sessions() ?? [],
@@ -34,7 +34,7 @@ import { adapterOptionsFor } from "./auth/adapter-options.js";
34
34
  import { createFauxProvider } from "@vincemakes/kiso-evals";
35
35
  import { sweepStaleMergeDirs } from "./temp-sweep.js";
36
36
  import { CODING_TOOL_RULES, composeSystemPrompt } from "./coding-prompt.js";
37
- import { createAgent, loadExtensions, loadProjectExtensions, SessionStore } from "@vincemakes/kiso-runtime";
37
+ import { createAgent, loadExtensions, loadProjectExtensions, readEnvironment, SessionStore } from "@vincemakes/kiso-runtime";
38
38
  import { preferences, startingEffort } from "./preferences.js";
39
39
  /** LT-1: the stream watchdog's bound from the environment — a non-negative
40
40
  * number of milliseconds (0 disables it); anything else is ignored. The
@@ -269,7 +269,7 @@ export async function createCodingAgent(sessionId, input, modelFlag) {
269
269
  setUserProtectedPaths(userConfig?.protectedPaths);
270
270
  const extensions = [
271
271
  protectedShellExtension({ files: protectedFiles, workspaceRoot, env: () => ({ home: homedir(), kisoHome: kisoHome() }) }),
272
- floorExtension(() => floorOn, workspaceRoot),
272
+ floorExtension(() => floorOn, workspaceRoot, undefined, () => (sessionId === undefined ? [] : readEnvironment(activeStoreDir, sessionId).added)),
273
273
  breakerExtension(),
274
274
  ...modeExtensions(workspaceRoot),
275
275
  readOnlyShellExtension(codingToolOptions),
@@ -4,7 +4,9 @@
4
4
  * the context (the chain, the run state, the prompt arming).
5
5
  */
6
6
  import type { SessionRoute } from "./projects.js";
7
- import type { AgentSession } from "@vincemakes/kiso-runtime";
7
+ import { type StoreRecord } from "@vincemakes/kiso-runtime/internal";
8
+ import { type AgentSession } from "@vincemakes/kiso-runtime";
9
+ import type { SessionCardView } from "@vincemakes/kiso-tui";
8
10
  import { type LineInput } from "./state.js";
9
11
  import type { UserInputVia } from "@vincemakes/kiso-core";
10
12
  /** Everything dispatch touches that chat() owns. */
@@ -72,3 +74,8 @@ export declare function parseBang(trimmed: string): {
72
74
  export declare function markCompactWhy(why: string): void;
73
75
  export declare function markAutoCompact(): void;
74
76
  export declare function dispatch(line: string, ctx: DispatchCtx): void;
77
+ /** /fork's rows: each finished run, newest first, by the person's message it answered and when it ended; the opened
78
+ * row shows the reply's first line under it (owner 2026-11-04: the person knows a turn by what they asked, and an
79
+ * "answered:" label over their own line read backwards). The card's id is the seq of the run's terminal, where
80
+ * SessionStore.fork cuts. */
81
+ export declare function replyCards(records: readonly StoreRecord[]): SessionCardView[];
package/dist/dispatch.js CHANGED
@@ -5,11 +5,13 @@
5
5
  */
6
6
  import { STATUS_GLYPHS, contextRows, contextSheetRows, contextUnavailableRows, contextUnavailableSheetRows, displayVerb, escapeTerminal, helpRows, toolTarget, infoSheetRows, kUnit, modePickView, modelPickView, namedPickView, settingsPickView, compactingStatus, liveRow, palette, renderEvent, settledLabel, slashCommandNames } from "@vincemakes/kiso-tui";
7
7
  import { newSessionId } from "./session-id.js";
8
- import { buildAdapter, lookupModelMetadata, readSessionName, resolveContinuationScope, resolveReasoning, sessionTitle, tiersFor, writeSessionName } from "@vincemakes/kiso-runtime/internal";
8
+ import { buildAdapter, isForkNote, lookupModelMetadata, readSessionName, resolveContinuationScope, resolveReasoning, sessionTitle, tiersFor, writeSessionName } from "@vincemakes/kiso-runtime/internal";
9
+ import { isEnvironmentNote } from "@vincemakes/kiso-runtime";
10
+ import { ForkRefusedError, SessionStore } from "@vincemakes/kiso-runtime";
9
11
  import { MODE_LABEL, MODE_NOTE, OFFERED_MODES, applyModeSetting, getDontAsk, getMode, modeDisplay, parseMode, setDontAsk } from "./mode.js";
10
12
  import { clipboardWrite, lastAnswer } from "./clipboard.js";
11
13
  import { protectedBangReason, protectedShellVerdict } from "./protected-shell.js";
12
- import { activeStoreDir, agentBaseUrl, currentProfileName, setCurrentProfileName, currentModelName, agentModel, body, bodyLog, codingToolOptions, protectedFiles, kisoHome, configModels, dock, lastBinding, loadedSkillsCatalog, mergedConfig, readContextLedger, retryOnRow, sessionsDir, setAgentModel, setConfiguredWindow, setCurrentModelName, setModelChoice, setRetryShown, upstreamOf, VERSION, setLastBinding } from "./state.js";
14
+ import { activeStoreDir, sessionStoreRef, agentBaseUrl, currentProfileName, setCurrentProfileName, currentModelName, agentModel, body, bodyLog, codingToolOptions, protectedFiles, kisoHome, configModels, dock, lastBinding, loadedSkillsCatalog, mergedConfig, readContextLedger, retryOnRow, sessionsDir, setAgentModel, setConfiguredWindow, setCurrentModelName, setModelChoice, setRetryShown, upstreamOf, VERSION, setLastBinding } from "./state.js";
13
15
  import { adapterOptionsFor } from "./auth/adapter-options.js";
14
16
  import { profileProviderLabel, providerLabel } from "./provider-label.js";
15
17
  import { installedVersion, versionStatusLine } from "./stale-version.js";
@@ -1384,6 +1386,44 @@ export function dispatch(line, ctx) {
1384
1386
  ctx.requestReload();
1385
1387
  return;
1386
1388
  }
1389
+ if (trimmed === "/fork") {
1390
+ // owner 2026-11-03: the web app's fork, one rule — a new session from after one of this session's replies
1391
+ // (SessionStore.fork), then the terminal switches to it as /resume does; the replies are picked in /resume's band
1392
+ if (ctx.isRunning()) {
1393
+ body.notice("[/fork] a run is in flight — let it finish (esc stops it), then fork");
1394
+ ctx.input.prompt();
1395
+ return;
1396
+ }
1397
+ ctx.chainRef.current = ctx.chainRef.current.then(async () => {
1398
+ const cards = replyCards(sessionStoreRef?.load(ctx.session.id) ?? []);
1399
+ if (cards.length === 0) {
1400
+ body.notice("no finished reply to fork after yet");
1401
+ ctx.input.prompt();
1402
+ return;
1403
+ }
1404
+ const fork = (afterSeq) => {
1405
+ try {
1406
+ const made = new SessionStore(activeStoreDir).fork(ctx.session.id, { afterSeq });
1407
+ body.notice(`forked "${escapeTerminal(readSessionName(activeStoreDir, made.id) ?? made.id)}" — switching to it`);
1408
+ ctx.requestSwitch(made.id);
1409
+ }
1410
+ catch (error) {
1411
+ body.notice(error instanceof ForkRefusedError ? `[/fork] ${error.message}` : `[/fork] ${error instanceof Error ? error.message : String(error)}`);
1412
+ ctx.input.prompt();
1413
+ }
1414
+ };
1415
+ // no band (a pipe, a dock-less terminal): after the last reply, the sidebar's Fork
1416
+ if (ctx.input.pick === undefined || !dock.active)
1417
+ return fork(Number(cards[0].id));
1418
+ ctx.input.pick(() => cards, (picked) => {
1419
+ if (picked === null)
1420
+ ctx.input.prompt(); // esc: nothing forked, nothing said
1421
+ else
1422
+ fork(Number(picked));
1423
+ }, undefined, "fork");
1424
+ });
1425
+ return;
1426
+ }
1387
1427
  if (trimmed === "/resume" || trimmed.startsWith("/resume ")) {
1388
1428
  // the mini-spec: the in-session door to the durable sessions —
1389
1429
  // /resume <id> switches directly; bare /resume opens the SAME
@@ -1589,3 +1629,30 @@ async function tasksCommand(arg, ctx) {
1589
1629
  else if (action === 1)
1590
1630
  stop(task);
1591
1631
  }
1632
+ /** /fork's rows: each finished run, newest first, by the person's message it answered and when it ended; the opened
1633
+ * row shows the reply's first line under it (owner 2026-11-04: the person knows a turn by what they asked, and an
1634
+ * "answered:" label over their own line read backwards). The card's id is the seq of the run's terminal, where
1635
+ * SessionStore.fork cuts. */
1636
+ export function replyCards(records) {
1637
+ const cards = [];
1638
+ let asked = "";
1639
+ let reply = "";
1640
+ for (const { event: e, ts } of records) {
1641
+ if (e.type === "user_input" && e.source !== "system") {
1642
+ const content = Array.isArray(e.content) ? e.content.filter((b) => !(b.type === "text" && (isForkNote(b.text ?? "") || isEnvironmentNote(b.text ?? "")))) : e.content;
1643
+ asked = (typeof content === "string" ? content : content.map((b) => (b.type === "text" ? (b.text ?? "") : "")).join(" ")).trim();
1644
+ reply = "";
1645
+ }
1646
+ else if (e.type === "tool_result")
1647
+ reply = "";
1648
+ else if (e.type === "text_delta")
1649
+ reply += e.text;
1650
+ else if (e.type === "terminal" && e.outcome.kind === "completed") {
1651
+ // the row is the person's own line; the reply's first line, inline code read as plain text, opens under it
1652
+ const first = (reply.split("\n").map((l) => l.trim()).find((l) => l !== "") ?? "(no reply text)").replace(/`([^`]*)`/g, "$1");
1653
+ const line = asked.split("\n").map((l) => l.trim()).find((l) => l !== "") ?? "(no message text)";
1654
+ cards.unshift({ id: String(e.seq), title: line, badge: "completed", turns: null, updatedAt: ts, uncertain: 0, asks: 0, outcome: null, detail: `└ ${first}` });
1655
+ }
1656
+ }
1657
+ return cards;
1658
+ }
@@ -65,6 +65,8 @@ export function findEmptySessions(dirs, alive = pidAlive) {
65
65
  const files = [name];
66
66
  if (names.includes(lock))
67
67
  files.push(lock);
68
+ if (names.includes(`${id}.environment.json`))
69
+ files.push(`${id}.environment.json`);
68
70
  if (existsSync(join(dir, "traces", `${id}.jsonl`)))
69
71
  files.push(join("traces", `${id}.jsonl`));
70
72
  empty.push({ dir, id, files });
package/dist/floor.d.ts CHANGED
@@ -88,12 +88,12 @@ export declare function gitInvocation(args: readonly LooseWord[]): GitInvocation
88
88
  * Would the floor refuse this command line? `workspaceRoot` is where the
89
89
  * shell runs it.
90
90
  */
91
- export declare function floorCheck(commandLine: string, workspaceRoot: string, home?: string): FloorVerdict;
91
+ export declare function floorCheck(commandLine: string, workspaceRoot: string, home?: string, added?: readonly string[]): FloorVerdict;
92
92
  /** Is there a destructive command anywhere in the line, whatever its
93
93
  * target? A saved allow is never inherited by one (plan §4). */
94
94
  export declare function isDestructive(commandLine: string): boolean;
95
95
  /** The chain member, at the chain's head: DENY or ABSTAIN. */
96
- export declare function floorExtension(on: () => boolean, workspaceRoot: () => string, check?: typeof floorCheck): KisoExtension;
96
+ export declare function floorExtension(on: () => boolean, workspaceRoot: () => string, check?: typeof floorCheck, added?: () => readonly string[]): KisoExtension;
97
97
  /** Plan §4: "destructive commands never inherit a saved allow in the
98
98
  * asking modes" — the predicate the saved-allow wrap
99
99
  * (protected-writes.ts) is given, keyed on destructiveness, not on the
package/dist/floor.js CHANGED
@@ -77,9 +77,9 @@ function windowsRoots(home) {
77
77
  /** Memoized per (workspace, home): the roots do not move within a session,
78
78
  * and resolving forty of them cost every shell call ~4 ms. */
79
79
  const WHERE = new Map();
80
- function where(root, home) {
80
+ function where(root, home, added = []) {
81
81
  const dialect = hostDialect();
82
- const key = `${dialect.msys ? "msys" : "posix"}\0${root}\0${home}`;
82
+ const key = `${dialect.msys ? "msys" : "posix"}\0${root}\0${home}\0${added.join("\0")}`;
83
83
  const hit = WHERE.get(key);
84
84
  if (hit !== undefined)
85
85
  return hit;
@@ -94,6 +94,7 @@ function where(root, home) {
94
94
  rootReal,
95
95
  homeReal: real(home),
96
96
  gitReal: join(rootReal, ".git"),
97
+ added: added.map(real),
97
98
  sys: named(roots.sys),
98
99
  mounts: named(roots.mounts),
99
100
  temps: [...new Set(roots.temps.map(real))],
@@ -137,6 +138,12 @@ function unrecoverable(w, p, over) {
137
138
  return what("the workspace root");
138
139
  if (within(p, w.rootReal))
139
140
  return what(`a directory above the workspace (${p})`);
141
+ for (const a of w.added) {
142
+ if (same(p, a) || within(p, a))
143
+ return what(`an added folder, or a directory above one (${p})`);
144
+ if (within(hostDialect().path.join(a, ".git"), p))
145
+ return what("an added folder's .git");
146
+ }
140
147
  // R7: the workspace's history — without a remote, gone
141
148
  if (within(w.gitReal, p))
142
149
  return what("the workspace's .git");
@@ -519,8 +526,8 @@ const child = (s) => ({ cwds: [...s.cwds], lastCd: null, prev: s.prev });
519
526
  * Would the floor refuse this command line? `workspaceRoot` is where the
520
527
  * shell runs it.
521
528
  */
522
- export function floorCheck(commandLine, workspaceRoot, home = homedir()) {
523
- const w = where(workspaceRoot, home);
529
+ export function floorCheck(commandLine, workspaceRoot, home = homedir(), added = []) {
530
+ const w = where(workspaceRoot, home, added);
524
531
  const seen = new Set();
525
532
  const tilde = (word) => (word.tilde && (word.text === "~" || word.text.startsWith("~/")) ? home + word.text.slice(1) : word.text);
526
533
  const walk = (nodes, state, depth) => {
@@ -652,7 +659,7 @@ export function isDestructive(commandLine) {
652
659
  }
653
660
  const ABSTAIN = { action: "abstain" };
654
661
  /** The chain member, at the chain's head: DENY or ABSTAIN. */
655
- export function floorExtension(on, workspaceRoot, check = floorCheck) {
662
+ export function floorExtension(on, workspaceRoot, check = floorCheck, added = () => []) {
656
663
  return {
657
664
  name: "floor",
658
665
  approvals: [
@@ -669,7 +676,7 @@ export function floorExtension(on, workspaceRoot, check = floorCheck) {
669
676
  // allow. The floor denies what it could not read.
670
677
  let v;
671
678
  try {
672
- v = check(command, workspaceRoot());
679
+ v = check(command, workspaceRoot(), undefined, added());
673
680
  }
674
681
  catch (err) {
675
682
  return { action: "deny", reason: `the floor could not read this line (${err instanceof Error ? err.name : "error"}) — kiso does not run it.` };
package/dist/index.js CHANGED
@@ -33,7 +33,8 @@ import { createRequire } from "node:module";
33
33
  import { basename, dirname, join } from "node:path";
34
34
  import { Body, Editor, PROMPT, bannerLines, currentGround, currentGroundRgb, parseOscColor, resolveGround, setGround, escapeTerminal, extensionsBannerText, extensionsFact, idColumn, interactivePrompt, palette, renderSessionLine, sessionListFooter, sessionListHeader, sessionListRow, sessionListUnknownLine, slashCommandNames } from "@vincemakes/kiso-tui";
35
35
  import { disposeExtensions, SessionStore } from "@vincemakes/kiso-runtime";
36
- import { listSessionSidecars, migrateSummaries, readProfile, readSessionName, summaryMigrationPending } from "@vincemakes/kiso-runtime/internal";
36
+ import { readEnvironment } from "@vincemakes/kiso-runtime";
37
+ import { listSessionSidecars, migrateSummaries, readProfile, readSessionName, summaryMigrationPending, writeSessionName } from "@vincemakes/kiso-runtime/internal";
37
38
  import { skillMenuItems } from "./skill-invoke.js";
38
39
  import { canonicalPath, hasSession, locateSession, projectLayoutActive, sessionFolders } from "./projects.js";
39
40
  import { reverseMigration } from "./session-migration.js";
@@ -332,8 +333,8 @@ function editorInput(editor) {
332
333
  // TUI2-R2 ②: the session picker — the editor owns the keys (the
333
334
  // selection walk, the filter, enter/esc), the compositor draws the
334
335
  // band, and the id comes back here.
335
- pick(cards, onPick, here) {
336
- editor.beginPick(cards, onPick, here);
336
+ pick(cards, onPick, here, kind) {
337
+ editor.beginPick(cards, onPick, here, kind);
337
338
  },
338
339
  // W22: the pending-turn queue — the ↑ pop walk (the keys); the
339
340
  // chips are the compositor's bindQueue (the dock side).
@@ -341,6 +342,9 @@ function editorInput(editor) {
341
342
  editor.bindQueue(state, pop);
342
343
  },
343
344
  // ADR-0057: the steers a stop handed back return to the buffer.
345
+ setGhost(text) {
346
+ editor.setGhost(text);
347
+ },
344
348
  restore(text) {
345
349
  editor.restore(text);
346
350
  },
@@ -1070,6 +1074,10 @@ async function chatLoop(first, firstId, input, autoCompact) {
1070
1074
  refused = true;
1071
1075
  continue;
1072
1076
  }
1077
+ // ADR-0058 Amendment 19: the folders a web Work added; the tools here honour them too
1078
+ const addedFolders = readEnvironment(activeStoreDir, id).added;
1079
+ if (addedFolders.length > 0)
1080
+ bodyLog(`also working in: ${addedFolders.map(tildeOf).join(", ")}`);
1073
1081
  opened = id;
1074
1082
  if (rebuilt) {
1075
1083
  rebuilt = false;
@@ -1158,6 +1166,30 @@ async function chatLoop(first, firstId, input, autoCompact) {
1158
1166
  sessions: () => agent.sessionIds(),
1159
1167
  route: (sessionId) => routeSession(sessionId),
1160
1168
  ...(process.stdin.isTTY ? { pick: () => pickSession(agent, input) } : {}),
1169
+ // owner 2026-11-03: a session is titled by a summary of its first message, in the background; the person's own
1170
+ // name wins, before or after; off with KISO_AUTO_TITLE=off and with the faux model, and never without a TTY:
1171
+ // a piped chat is a script or a bench leg, where one more request per session is not the person's
1172
+ ...(currentFaux || process.env["KISO_AUTO_TITLE"] === "off" || process.stdin.isTTY !== true
1173
+ ? {}
1174
+ : {
1175
+ titled: (firstMessage) => {
1176
+ // the session and its folder as they are NOW: a /resume or a switch before the title lands must not
1177
+ // send it to another session, nor repaint another session's window title
1178
+ const [titling, mine, dir] = [session, session.id, activeStoreDir];
1179
+ if (readSessionName(dir, mine) !== null)
1180
+ return;
1181
+ void titling.title(firstMessage, { signal: AbortSignal.timeout(15_000) }).then((title) => {
1182
+ if (title === "" || readSessionName(dir, mine) !== null)
1183
+ return;
1184
+ writeSessionName(dir, mine, title);
1185
+ // `id` is the loop's: the session open now (a switch moves it on)
1186
+ if (id !== mine)
1187
+ return;
1188
+ setTitleName(title);
1189
+ paintWindowTitle();
1190
+ }, () => undefined);
1191
+ },
1192
+ }),
1161
1193
  };
1162
1194
  const end = await chat(session, currentFaux, input, autoCompact, nav, seed);
1163
1195
  if (end.next === "exit") {
package/dist/policy.d.ts CHANGED
@@ -10,7 +10,9 @@
10
10
  export { CODING_TOOL_RULES, SYSTEM_PROMPT, composeSystemPrompt } from "./coding-prompt.js";
11
11
  export { breakerExtension } from "./breaker.js";
12
12
  export { floorExtension } from "./floor.js";
13
+ export { fileStopRoots } from "./shell-words.js";
13
14
  export { MODE_LABEL, OFFERED_MODES, getMode, modeExtensions, modeSystemPrompt, setMode } from "./mode.js";
14
15
  export { protectedShellExtension } from "./protected-shell.js";
15
16
  export { guardSavedAllow, neverInheritedCall } from "./protected-writes.js";
16
17
  export { readOnlyShellExtension } from "./readonly-shell.js";
18
+ export { cdTargets, isGhPrCreate } from "./shell-facts.js";
package/dist/policy.js CHANGED
@@ -10,7 +10,9 @@
10
10
  export { CODING_TOOL_RULES, SYSTEM_PROMPT, composeSystemPrompt } from "./coding-prompt.js";
11
11
  export { breakerExtension } from "./breaker.js";
12
12
  export { floorExtension } from "./floor.js";
13
+ export { fileStopRoots } from "./shell-words.js";
13
14
  export { MODE_LABEL, OFFERED_MODES, getMode, modeExtensions, modeSystemPrompt, setMode } from "./mode.js";
14
15
  export { protectedShellExtension } from "./protected-shell.js";
15
16
  export { guardSavedAllow, neverInheritedCall } from "./protected-writes.js";
16
17
  export { readOnlyShellExtension } from "./readonly-shell.js";
18
+ export { cdTargets, isGhPrCreate } from "./shell-facts.js";
package/dist/projects.js CHANGED
@@ -153,7 +153,7 @@ export function locateSession(home, id, except) {
153
153
  * repeated move is a no-op.
154
154
  */
155
155
  export function sessionFiles(id) {
156
- return [join("traces", `${id}.jsonl`), `${id}.meta.json`, `${id}.lock`, `${id}.jsonl`];
156
+ return [join("traces", `${id}.jsonl`), `${id}.meta.json`, `${id}.environment.json`, `${id}.environment.json.lock`, `${id}.lock`, `${id}.jsonl`];
157
157
  }
158
158
  export function moveSessionFiles(fromDir, toDir, id, files = sessionFiles(id)) {
159
159
  let moved = 0;
package/dist/replay.d.ts CHANGED
@@ -19,7 +19,7 @@
19
19
  * summary (what the model sees next) and the turns it covers, read in
20
20
  * the viewer — then any uncovered earlier turns folded as above.
21
21
  */
22
- import type { TaskInfo } from "@vincemakes/kiso-runtime/internal";
22
+ import { type TaskInfo } from "@vincemakes/kiso-runtime/internal";
23
23
  import type { NoticeMark } from "@vincemakes/kiso-tui";
24
24
  /** The Body surface the replay drives — the live run's own mutations. */
25
25
  export interface ReplayBody {
package/dist/replay.js CHANGED
@@ -22,6 +22,8 @@
22
22
  import { echoText } from "@vincemakes/kiso-tui-cells/render";
23
23
  import { bandHeader } from "@vincemakes/kiso-tui-cells/strings";
24
24
  import { cappedWaitLine, taskNoticeLines, taskNoticeRows, taskWhat } from "./task-notice.js";
25
+ import { isForkNote } from "@vincemakes/kiso-runtime/internal";
26
+ import { isEnvironmentNote } from "@vincemakes/kiso-runtime";
25
27
  /** How many turns stay on screen in full. */
26
28
  const FULL_TURNS = 2;
27
29
  /** A user turn's opening line, or null when this input does not open one
@@ -32,7 +34,9 @@ function askOf(e) {
32
34
  if (e.type !== "user_input" || e.source === "system")
33
35
  return null;
34
36
  const via = e.via;
35
- const text = typeof via?.line === "string" ? via.line : echoText(e.content);
37
+ // a fork's first input carries the fork note before the person's words: the model's, not the line they typed
38
+ const content = Array.isArray(e.content) ? e.content.filter((b) => !(b.type === "text" && (isForkNote(b.text ?? "") || isEnvironmentNote(b.text ?? "")))) : e.content;
39
+ const text = typeof via?.line === "string" ? via.line : echoText(content);
36
40
  return text.trim() === "" ? null : text;
37
41
  }
38
42
  /** Split events into turns: each starts at a user turn's input; anything
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The directories a line moves the shell to, in order, when the call that ran it did not fail: the literal `cd <path>` commands of a line
3
+ * that is one `&&` chain, so every one of them ran (`cd /nonexistent; echo hello` exits 0 without having moved, and so does
4
+ * `false && cd /x && ls || true`). Relative paths resolve against `base`, then against the previous `cd`; `~` is `home`. An option, a
5
+ * variable or a word the host's shell cannot spell stops the reading there.
6
+ */
7
+ export declare function cdTargets(line: string, base: string, home: string): readonly string[];
8
+ /** Whether the line is one `&&` chain that ENDS in `gh pr create`: what its output ends with is the pull request it made (a mention in an echo, a `||` branch, or a command after it that prints another URL, is not one). */
9
+ export declare function isGhPrCreate(line: string): boolean;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Two facts a UI may read from a shell line, conservatively: literal words only, and what the reader cannot read yields nothing.
3
+ * They are DISPLAY facts (the web page's Recent folders and Repository rows): nothing here grants or refuses anything.
4
+ */
5
+ import { hostDialect, msysPath, parseShellLooseChecked } from "./shell-words.js";
6
+ const literal = (w) => (w === undefined || w.unknownAt !== -1 || w.variableOnly ? null : w.text);
7
+ /** The commands of a line that is ONE `&&` chain (no `||`, `;`, pipe or group anywhere), or null: only then did every command run when the call did not fail. */
8
+ function chainOf(line) {
9
+ const { nodes, truncated } = parseShellLooseChecked(line);
10
+ if (truncated || nodes.some((n, i) => n.kind !== "cmd" || (i > 0 && n.joinedBy !== "&&")))
11
+ return null;
12
+ return nodes;
13
+ }
14
+ /**
15
+ * The directories a line moves the shell to, in order, when the call that ran it did not fail: the literal `cd <path>` commands of a line
16
+ * that is one `&&` chain, so every one of them ran (`cd /nonexistent; echo hello` exits 0 without having moved, and so does
17
+ * `false && cd /x && ls || true`). Relative paths resolve against `base`, then against the previous `cd`; `~` is `home`. An option, a
18
+ * variable or a word the host's shell cannot spell stops the reading there.
19
+ */
20
+ export function cdTargets(line, base, home) {
21
+ const chain = chainOf(line);
22
+ if (chain === null)
23
+ return [];
24
+ const out = [];
25
+ const d = hostDialect(); // the shell's own spelling: Git Bash on Windows (`/c/x`, `C:/x`), POSIX elsewhere
26
+ let at = base;
27
+ for (const cmd of chain) {
28
+ if (literal(cmd.argv[0]) !== "cd")
29
+ continue;
30
+ const raw = cmd.argv.length === 2 ? literal(cmd.argv[1]) : null;
31
+ if (raw === null || raw === "-" || raw.startsWith("-"))
32
+ break;
33
+ const word = raw === "~" || raw.startsWith("~/") ? d.path.join(home, raw.slice(2)) : raw;
34
+ const read = d.msys ? msysPath(word) : { native: word };
35
+ if ("opaque" in read)
36
+ break;
37
+ at = d.path.resolve(at, read.native);
38
+ out.push(at);
39
+ }
40
+ return out;
41
+ }
42
+ /** Whether the line is one `&&` chain that ENDS in `gh pr create`: what its output ends with is the pull request it made (a mention in an echo, a `||` branch, or a command after it that prints another URL, is not one). */
43
+ export function isGhPrCreate(line) {
44
+ const c = chainOf(line)?.at(-1);
45
+ return c !== undefined && literal(c.argv[0]) === "gh" && literal(c.argv[1]) === "pr" && literal(c.argv[2]) === "create";
46
+ }
@@ -93,6 +93,8 @@ export declare const HOME_SUBTREES: readonly string[];
93
93
  /** The home subtrees for this host: on Windows, AppData too — credentials
94
94
  * and configuration that runs live there (Windows P2). */
95
95
  export declare function homeSubtrees(): readonly string[];
96
+ /** Folders the file tools' new reach never enters (ADR-0058 Amendment 19): kiso's own home and the credential homes. */
97
+ export declare function fileStopRoots(home: string, kisoHome: string): readonly string[];
96
98
  /** A real path in the case the DISK holds it. The JS realpath keeps the
97
99
  * case it was given, so on a case-insensitive disk `.ENV` stayed `.ENV`
98
100
  * and walked past every name-based predicate (the lead's review, B2). */
@@ -334,6 +334,10 @@ export const HOME_SUBTREES = [".ssh", ".config", ".kiso", ".gnupg", ".aws"];
334
334
  export function homeSubtrees() {
335
335
  return hostDialect().msys ? [...HOME_SUBTREES, "AppData"] : HOME_SUBTREES;
336
336
  }
337
+ /** Folders the file tools' new reach never enters (ADR-0058 Amendment 19): kiso's own home and the credential homes. */
338
+ export function fileStopRoots(home, kisoHome) {
339
+ return [kisoHome, ...homeSubtrees().map((s) => hostDialect().path.join(home, s))];
340
+ }
337
341
  /** A real path in the case the DISK holds it. The JS realpath keeps the
338
342
  * case it was given, so on a case-insensitive disk `.ENV` stayed `.ENV`
339
343
  * and walked past every name-based predicate (the lead's review, B2). */
package/dist/state.d.ts CHANGED
@@ -10,7 +10,7 @@ import type { TaskDeliveryItem } from "@vincemakes/kiso-core";
10
10
  import { kisoHome, type KisoExtension, type StoreRecord } from "@vincemakes/kiso-runtime";
11
11
  import type { ReasoningSetting } from "@vincemakes/kiso-runtime/internal";
12
12
  import { TaskManager } from "@vincemakes/kiso-runtime/internal";
13
- import { type ShellTasks } from "@vincemakes/kiso-tools-node";
13
+ import { type FileAccess, type ShellTasks } from "@vincemakes/kiso-tools-node";
14
14
  /** finding #11: KISO_HOME is the ONE root — every default path derives from
15
15
  * it (sessions, trust, extensions, mcp config, skills). The dedicated
16
16
  * env vars (KISO_EXTENSIONS_DIR / KISO_MCP_CONFIG / KISO_SKILLS_DIR)
@@ -35,6 +35,8 @@ export declare function codingToolOptions(): {
35
35
  readonly excludeRoots: readonly string[];
36
36
  readonly secretEnvNames: readonly string[];
37
37
  readonly protectedFiles: readonly string[];
38
+ readonly access: (sessionId: string | undefined) => FileAccess;
39
+ readonly stopRoots: readonly string[];
38
40
  readonly tasks: (sessionId: string | undefined) => ShellTasks | undefined;
39
41
  readonly observedRevision: (sessionId: string | undefined, full: string, kind?: "edit" | "write") => string | undefined;
40
42
  readonly limits?: {
@@ -241,7 +243,7 @@ export interface LineInput {
241
243
  * picked id, or null when the human leaves without picking.
242
244
  * OPTIONAL: only the raw-mode editor has it, and the picker only
243
245
  * ever opens on a TTY (a pipe has nobody to pick). */
244
- pick?(cards: () => readonly SessionCardView[], onPick: (id: string | null) => void, here?: string): void;
246
+ pick?(cards: () => readonly SessionCardView[], onPick: (id: string | null) => void, here?: string, kind?: "resume" | "fork"): void;
245
247
  /** W22: bind the pending-turn queue — the ↑ pop walks the CLI's
246
248
  * live slots (each pop cancels the turn), esc ends the walk after
247
249
  * one more pop. The chips are the compositor's own bindQueue. */
@@ -249,6 +251,9 @@ export interface LineInput {
249
251
  /** ADR-0057: put text back into the editor — the steers a stop handed
250
252
  * back before they landed. The pipe path has no editor; optional. */
251
253
  restore?(text: string): void;
254
+ /** The follow-up: a line the person may send next, dim in the empty box, Tab takes it; null clears it. Optional: the
255
+ * readline input has no box to draw it in. */
256
+ setGhost?(text: string | null): void;
252
257
  /** R3a: cross-session history — seed the recall buffer, register the
253
258
  * append sink. The pipe path has no recall keys; optional. */
254
259
  bindHistory?(seed: readonly string[], persist: (line: string) => void): void;
package/dist/state.js CHANGED
@@ -12,12 +12,14 @@ import { dirname, join, resolve } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
13
  import { AT_CAP, AT_SKIP, Dock } from "@vincemakes/kiso-tui";
14
14
  import { projectMessages } from "@vincemakes/kiso-core";
15
- import { kisoHome } from "@vincemakes/kiso-runtime";
15
+ import { kisoHome, readEnvironment } from "@vincemakes/kiso-runtime";
16
16
  import { TaskManager } from "@vincemakes/kiso-runtime/internal";
17
17
  import { processTaskBackend, readShowsWholeFile } from "@vincemakes/kiso-tools-node";
18
18
  import { collectWriter } from "@vincemakes/kiso-subagent-ext";
19
19
  import { canonicalPath, LEGACY_SESSIONS_DIR, projectDirFor, projectLayoutActive } from "./projects.js";
20
20
  import { taskNoticeLines, taskWhat } from "./task-notice.js";
21
+ import { getMode } from "./mode.js";
22
+ import { fileStopRoots } from "./shell-words.js";
21
23
  /** finding #11: KISO_HOME is the ONE root — every default path derives from
22
24
  * it (sessions, trust, extensions, mcp config, skills). The dedicated
23
25
  * env vars (KISO_EXTENSIONS_DIR / KISO_MCP_CONFIG / KISO_SKILLS_DIR)
@@ -49,6 +51,12 @@ let printRun = false;
49
51
  export function setPrintRun() {
50
52
  printRun = true;
51
53
  }
54
+ /** ADR-0058 Amendment 19: the same list the web reads, per call; a delegate child gets none of it. */
55
+ function fileAccess(sessionId) {
56
+ if ((Number.parseInt(process.env.KISO_SUBAGENT_DEPTH ?? "0", 10) || 0) >= 1)
57
+ return { added: [], outside: "deny" };
58
+ return { added: sessionId === undefined || activeStoreDir === "" ? [] : readEnvironment(activeStoreDir, sessionId).added, outside: getMode() === "full-access" ? "allow" : "deny" };
59
+ }
52
60
  export function codingToolOptions() {
53
61
  // KISO_MIN_FOREGROUND_MS — the test rigs' knob (like KISO_AUTO_DETACH_MS):
54
62
  // the least foreground wait (ADR-0058 Amendment 10, default 60 s). The
@@ -66,6 +74,8 @@ export function codingToolOptions() {
66
74
  excludeRoots: [kisoHome()],
67
75
  secretEnvNames: secretEnvNamesOf(configModels),
68
76
  protectedFiles: protectedFiles(),
77
+ access: fileAccess,
78
+ stopRoots: fileStopRoots(homedir(), kisoHome()),
69
79
  tasks: tasksFor,
70
80
  observedRevision: observedRevisionFromLog,
71
81
  ...(Object.keys(limits).length > 0 ? { limits } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-code",
3
- "version": "0.52.0",
3
+ "version": "0.54.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",
@@ -29,19 +29,19 @@
29
29
  "test": "vitest run"
30
30
  },
31
31
  "dependencies": {
32
- "@vincemakes/kiso-ask-ext": "0.52.0",
33
- "@vincemakes/kiso-core": "0.52.0",
34
- "@vincemakes/kiso-evals": "0.52.0",
35
- "@vincemakes/kiso-mcp-ext": "0.52.0",
36
- "@vincemakes/kiso-provider-anthropic": "0.52.0",
37
- "@vincemakes/kiso-provider-openai": "0.52.0",
38
- "@vincemakes/kiso-provider-openai-responses": "0.52.0",
39
- "@vincemakes/kiso-runtime": "0.52.0",
40
- "@vincemakes/kiso-skills-ext": "0.52.0",
41
- "@vincemakes/kiso-subagent-ext": "0.52.0",
42
- "@vincemakes/kiso-tools-node": "0.52.0",
43
- "@vincemakes/kiso-tui": "0.52.0",
44
- "@vincemakes/kiso-tui-cells": "0.52.0"
32
+ "@vincemakes/kiso-ask-ext": "0.54.0",
33
+ "@vincemakes/kiso-core": "0.54.0",
34
+ "@vincemakes/kiso-evals": "0.54.0",
35
+ "@vincemakes/kiso-mcp-ext": "0.54.0",
36
+ "@vincemakes/kiso-provider-anthropic": "0.54.0",
37
+ "@vincemakes/kiso-provider-openai": "0.54.0",
38
+ "@vincemakes/kiso-provider-openai-responses": "0.54.0",
39
+ "@vincemakes/kiso-runtime": "0.54.0",
40
+ "@vincemakes/kiso-skills-ext": "0.54.0",
41
+ "@vincemakes/kiso-subagent-ext": "0.54.0",
42
+ "@vincemakes/kiso-tools-node": "0.54.0",
43
+ "@vincemakes/kiso-tui": "0.54.0",
44
+ "@vincemakes/kiso-tui-cells": "0.54.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/node": "^26.1.2",