@vincemakes/kiso-code 0.40.6 → 0.41.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
@@ -21,7 +21,7 @@
21
21
  * resume.ts, trust-ui.ts (the question surface + E3 merges), faux-glue.ts
22
22
  * (the scripted-model plumbing), state.ts (the shared process state).
23
23
  * index.ts keeps the entry: banner, input sources, the A area prompt,
24
- * makeAgent, and main.
24
+ * createCodingAgent, and main.
25
25
  */
26
26
  import { appendFileSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
27
27
  import { spawn, spawnSync } from "node:child_process";
@@ -31,33 +31,21 @@ import { Readable } from "node:stream";
31
31
  import { fileURLToPath } from "node:url";
32
32
  import { basename, join } from "node:path";
33
33
  import { Body, Editor, PROMPT, bannerLines, currentGround, resolveGround, setGround, escapeTerminal, extensionsBannerText, idColumn, idleStatus, interactivePrompt, palette, renderSessionLine, sessionListFooter, sessionListHeader, sessionListRow, sessionListUnknownLine, slashCommandNames } from "@vincemakes/kiso-tui";
34
- import { createAgent, disposeExtensions, loadExtensions, loadProjectExtensions, SessionStore, } from "@vincemakes/kiso-runtime";
35
- import { listSessionSidecars, migrateSummaries, readProfile, runsACheck, summaryMigrationPending } from "@vincemakes/kiso-runtime/internal";
34
+ import { disposeExtensions, SessionStore } from "@vincemakes/kiso-runtime";
35
+ import { listSessionSidecars, migrateSummaries, readProfile, summaryMigrationPending } from "@vincemakes/kiso-runtime/internal";
36
36
  import { skillMenuItems } from "./skill-invoke.js";
37
- import { canonicalPath, claimProjectDir, hasSession, locateSession, projectLayoutActive, sessionFolders } from "./projects.js";
38
- import { migrationNotice, pendingLegacyIds, planMigration, reverseMigration, runMigration } from "./session-migration.js";
37
+ import { canonicalPath, hasSession, locateSession, projectLayoutActive, sessionFolders } from "./projects.js";
38
+ import { reverseMigration } from "./session-migration.js";
39
39
  import { defaultTrashRoot, findEmptySessions, moveToTrash } from "./empty-sessions.js";
40
40
  import { createFauxProvider } from "@vincemakes/kiso-evals";
41
- import { createCodingTools, isProtectedPath, protectedIdentity, PROTECTED_REFUSAL } from "@vincemakes/kiso-tools-node";
42
- import { MODES, OFFERED_MODES, getMode, modeExtensions, modeFromEnv, modeSystemPrompt, setMode } from "./mode.js";
43
- import { readOnlyShellExtension } from "./readonly-shell.js";
44
- import { guardSavedAllow, isProtectedWrite } from "./protected-writes.js";
45
- import { floorExtension, isDestructiveCall } from "./floor.js";
46
- import { protectedShellExtension } from "./protected-shell.js";
47
- import { breakerExtension } from "./breaker.js";
48
- import { builtInLayer } from "./builtin.js";
49
- import { agentModel, atFiles, body, bodyLog, codingToolOptions, kisoHome, workspaceRoot, projectRoot, ownSessionsDir, setOpenSessionFolder, builtInExtensions, currentFaux, dock, extensionsDir, loadedExtensions, mergedConfig, mergedTempPaths, modelChoice, projectExtensions, configModels, configuredWindow, agentBaseUrl, currentModelName, currentAgentExtensions, sessionStoreRef, sessionsDir, setAgentModel, setBody, setConfigModels, setConfiguredWindow, setCurrentAgentExtensions, setCurrentFaux, setCurrentModelName, setCurrentProfileName, setExtensionLists, setUserProtectedPaths, protectedFiles, setMergedConfig, setModelChoice, setSessionStore, setRetryShown, setNeverInherited, secretEnvNamesOf, userExtensions, VERSION, lastBinding, acceptDrift, setAcceptDrift, setFloorOn, floorOn, loadedSkillsCatalog, queuedSwitchLines } from "./state.js";
50
- import { maxRetriesFromEnv } from "./retries.js";
51
- import { askUi, resolveProjectTrust } from "./trust-ui.js";
52
- import { isFirstRun, scaffoldFirstRun } from "./first-run.js";
41
+ import { isProtectedPath, protectedIdentity, PROTECTED_REFUSAL } from "@vincemakes/kiso-tools-node";
42
+ import { MODES, OFFERED_MODES, getMode, modeFromEnv, setMode } from "./mode.js";
43
+ 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";
53
44
  import { fauxSkip, readFauxScript } from "./faux-glue.js";
54
- import { chat, compactionDiscardedNotice, contextWindowTokens, displayCtxRatio, knownContextWindow, microcompactThresholdFor, statusModelLabel, unknownWindowNotice, windowLearnedNotice } from "./chat.js";
55
- import { recordLearnedWindow, useLearnedWindows } from "./learned-windows.js";
45
+ import { chat, contextWindowTokens, displayCtxRatio, microcompactThresholdFor, statusModelLabel } from "./chat.js";
56
46
  import { preferences, usePreferences } from "./preferences.js";
57
47
  import { settingsLayers } from "./state.js";
58
- import { providerHost } from "./provider-label.js";
59
- import { adapterOptionsFor } from "./auth/adapter-options.js";
60
- import { loadProjectConfig, loadUserConfig, mergeConfigs, resolveAutoCompact, resolveContextWindow, resolveModel } from "./config.js";
48
+ import { loadUserConfig, resolveAutoCompact } from "./config.js";
61
49
  import { checkForUpdate, knownUpdate, updateCardLines } from "./update-check.js";
62
50
  import { tmuxMouseHint } from "./tmux-hint.js";
63
51
  import { resume } from "./resume.js";
@@ -68,6 +56,9 @@ import { armByteTrace } from "./byte-trace.js";
68
56
  import { tmpdir, homedir } from "node:os";
69
57
  import { clipboardImage } from "./clipboard.js";
70
58
  import { cardsFromListings } from "./session-cards.js";
59
+ import { createCodingAgent } from "./create-coding-agent.js";
60
+ export { contextPolicyFromEnv } from "./create-coding-agent.js";
61
+ export { SYSTEM_PROMPT, composeSystemPrompt, readProjectInstructions } from "./coding-prompt.js";
71
62
  // The moved exports stay reachable from this entry — the test imports
72
63
  // (project-trust, coding-agent) never change (B4: zero assertion changes).
73
64
  export { applyProjectMerges } from "./trust-ui.js";
@@ -96,17 +87,6 @@ function openInBrowser(url) {
96
87
  // the URL is on screen; opening it is the person's fallback
97
88
  }
98
89
  }
99
- /** LT-1: the stream watchdog's bound from the environment — a non-negative
100
- * number of milliseconds (0 disables it); anything else is ignored. The
101
- * PTY rigs use it to trip the watchdog in seconds against a stub that
102
- * never finishes a stream. */
103
- function streamIdleFromEnv() {
104
- const raw = process.env.KISO_STREAM_IDLE_MS;
105
- if (raw === undefined || raw === "")
106
- return undefined;
107
- const n = Number(raw);
108
- return Number.isFinite(n) && n >= 0 ? n : undefined;
109
- }
110
90
  /** OR-3: the commands that own their own stdin reader (a hidden key prompt,
111
91
  * an OAuth paste prompt) or read nothing at all — never the editor.
112
92
  * OR-9 adds `update`: npm inherits the terminal for the install's own
@@ -593,354 +573,6 @@ function extensionsBanner(resume = []) {
593
573
  homeWorkspace: realOf(cwd) === realOf(home),
594
574
  });
595
575
  }
596
- /**
597
- * A area: the coding-agent system prompt — ONE constant, byte-stable for the
598
- * session's lifetime (D area). Kept under ~80 lines; no template engine.
599
- */
600
- /** The built-in prompt. Exported for scripts/request-surface.mjs — the
601
- * model-side token-rent counter measures the REAL bytes, never a copy. */
602
- export const SYSTEM_PROMPT = `You are kiso, a coding agent. You work in a workspace
603
- directory and change code with tools. Be concise: answer in a few lines
604
- unless the task genuinely needs more. Never claim a file was changed
605
- unless a tool confirmed it.
606
-
607
- What you can reach:
608
- - The workspace: read_file, list_dir, search_text, write_file, edit_file.
609
- - This machine and the network: shell — builds, tests, git, package
610
- managers, curl for HTTP, system queries. A request one command can
611
- answer is answered by running it.
612
- - The human: ask_user, for a decision that is theirs.
613
-
614
- Tool discipline:
615
- - READ BEFORE YOU EDIT. For any file you are about to change, read it
616
- first — never guess its content.
617
- - Use edit_file for targeted changes and write_file for full rewrites.
618
- Prefer many small edits over one large write.
619
- - Be careful — shell has side effects and may take time.
620
- - search_text and list_dir are cheap — locate first, then read ranges
621
- with read_file offset/limit; never read a whole large file in one call.
622
- - Do not re-read a file you already read unchanged, or one you changed
623
- yourself through a confirmed edit — rely on the earlier result and
624
- on the change you just made.
625
- - When a tool fails, read the error and adjust; do not repeat the same
626
- call blindly.
627
-
628
- An authorization covers what it NAMES: when the scope is not named —
629
- which files, which branches, whether to push — ask before acting.
630
- Delivering, sharing or handing over changes means committing locally and
631
- stopping; pushing, publishing or sending happens only when the human
632
- names it.
633
-
634
- Workflow: understand the request, find the relevant code, make the
635
- smallest change that works, then verify with a command (tests/build).
636
- Report what you did in one or two lines per change.`;
637
- /** The project-instructions file names, in priority order (A area). */
638
- const INSTRUCTION_FILES = ["AGENTS.md", "CLAUDE.md"];
639
- /** Hard cap for injected instructions — truncate and say so. */
640
- /**
641
- * A area: read the FIRST present instruction file (AGENTS.md preferred) and
642
- * return it as an injected section, or "" when none exists. Truncated at
643
- * 8KB with an explicit note. Pure — read once per session, so the prompt
644
- * is byte-stable for the session's lifetime.
645
- *
646
- * kiso never serves its own credential store to a model, and this read is
647
- * the one that needs no model at all: a cloned repository whose AGENTS.md
648
- * is a symlink to the store would put it in the SYSTEM PROMPT of every
649
- * request, with no tool call and no trust prompt. A file that resolves to
650
- * a protected one is skipped as though absent.
651
- */
652
- export function readProjectInstructions(cwd, protectedFiles = []) {
653
- const id = protectedIdentity(protectedFiles);
654
- for (const name of INSTRUCTION_FILES) {
655
- if (isProtectedPath(join(cwd, name), id))
656
- continue;
657
- let text;
658
- try {
659
- text = readFileSync(join(cwd, name), "utf8");
660
- }
661
- catch {
662
- continue; // not present — try the next
663
- }
664
- return `\n\n=== Project instructions (${name}) ===\n${text.length > 8 * 1024 ? text.slice(0, 8 * 1024) + `\n\n[truncated at ${8 * 1024} chars]` : text}`;
665
- }
666
- return "";
667
- }
668
- /** A area: the session's system prompt — the constant plus any project
669
- * instructions found in the workspace. Deterministic per cwd. */
670
- export function composeSystemPrompt(cwd, protectedFiles = []) {
671
- const injected = readProjectInstructions(cwd, protectedFiles);
672
- return injected === "" ? SYSTEM_PROMPT : `${SYSTEM_PROMPT}\n${injected}`;
673
- }
674
- /**
675
- * E6: the run-start context policy, OFF unless env-armed (invalid values
676
- * are ABSENT, never a crash — the autoCompactFromEnv convention).
677
- * The product arming is KISO_CONTEXT_WINDOW → window − POLICY_RESERVE
678
- * (the window rides the config as windowTokens; the runtime owns the
679
- * arithmetic — never a fixed low absolute). KISO_POLICY_SUMMARY_TRIGGER
680
- * survives ONLY as the legacy absolute override when no window is set
681
- * (bench back-compat); the window wins when both are set.
682
- * KISO_POLICY_SUMMARY_KEEP (rounds) and KISO_POLICY_SUMMARY_KEEP_TOKENS
683
- * override the runtime defaults (KEEP_RECENT_ROUNDS = 4,
684
- * KEEP_TOKENS_DEFAULT = 20,000) — emitted only when set.
685
- * KISO_POLICY_SUMMARY_MAX_FAILURES overrides the (h) circuit-breaker
686
- * default (MAX_SUMMARY_FAILURES = 3).
687
- * KISO_POLICY_DROP=1 switches the armed mode to the crux C arm
688
- * (mechanical drop — same trigger/keep envs); KISO_POLICY_MICROCOMPACT
689
- * arms the session-aware override (MIN_TURNS = the no-fire guard).
690
- */
691
- export function contextPolicyFromEnv() {
692
- const summaryTrigger = positiveIntEnv("KISO_POLICY_SUMMARY_TRIGGER");
693
- const contextWindow = positiveIntEnv("KISO_CONTEXT_WINDOW");
694
- const microcompactTrigger = positiveIntEnv("KISO_POLICY_MICROCOMPACT");
695
- if (summaryTrigger === undefined && contextWindow === undefined && microcompactTrigger === undefined)
696
- return undefined;
697
- return {
698
- ...(summaryTrigger === undefined && contextWindow === undefined ? {} : {
699
- [(process.env.KISO_POLICY_DROP === "1" ? "drop" : "summary")]: {
700
- ...(contextWindow !== undefined ? { windowTokens: contextWindow } : summaryTrigger !== undefined ? { triggerTokens: summaryTrigger } : {}),
701
- ...kv("keepRounds", "KISO_POLICY_SUMMARY_KEEP"),
702
- ...kv("keepTokens", "KISO_POLICY_SUMMARY_KEEP_TOKENS"),
703
- ...kv("maxFailures", "KISO_POLICY_SUMMARY_MAX_FAILURES"),
704
- },
705
- }),
706
- ...(microcompactTrigger !== undefined ? { microcompact: { thresholdTokens: microcompactTrigger, ...kv("keepResults", "KISO_POLICY_MICROCOMPACT_KEEP"), ...kv("minTurns", "KISO_POLICY_MICROCOMPACT_MIN_TURNS") } } : {}),
707
- };
708
- }
709
- /** { [key]: value } when the env int is set — the spread-friendly optional field. */
710
- function kv(key, env) {
711
- const v = positiveIntEnv(env);
712
- return v !== undefined ? { [key]: v } : {};
713
- }
714
- /** Parse a positive-int env var — absent or invalid is undefined (no crash). */
715
- function positiveIntEnv(name) {
716
- const n = Number.parseInt(process.env[name] ?? "", 10);
717
- return Number.isFinite(n) && n > 0 ? n : undefined;
718
- }
719
- async function makeAgent(sessionId, input, modelFlag) {
720
- // §2.5: the ONE source a reload reads for the model, seeded here from
721
- // the startup flag. Without this a session started with `--model X`
722
- // would silently revert to the env/config default on its first reload.
723
- if (modelFlag !== undefined)
724
- setModelChoice(modelFlag);
725
- // E3: the project-level trust gate runs BEFORE any extension load (the
726
- // mcp/skills merges must be in the env when the user-level extensions
727
- // load). Untrusted project capability is never loaded — never silently.
728
- const project = input !== undefined ? await resolveProjectTrust(input) : await resolveProjectTrust(undefined);
729
- // R-D 0.1.45 (deliverable B): the first-run scaffold lands AFTER the
730
- // verdict — pre-trust zero-read/write/scan is absolute. The sessions
731
- // dir (SessionStore's constructor mkdirs) moved behind the gate too:
732
- // the trust record is the first home write, the scaffold the second.
733
- if (isFirstRun())
734
- scaffoldFirstRun();
735
- // 0.40.0 — one folder per project: the one-time move of the legacy
736
- // folder, then this project's folder claimed (made, its workspace
737
- // recorded). Both write, so both sit behind the verdict, with the store.
738
- prepareSessionFolders();
739
- activeStoreDir = sessionsDir();
740
- const store = new SessionStore(activeStoreDir);
741
- // TUI2-R2 ②/③: the navigation surfaces read through THIS store — one
742
- // store per process, and the picker/listing never write through it.
743
- setSessionStore(store);
744
- // E area: the durable script position — computed AFTER the verdict
745
- // (fauxSkip's session-log read is a home read: pre-trust zero-read).
746
- const fauxSkipTurns = sessionId === undefined ? 0 : fauxSkip(sessionId);
747
- // E1: the startup extension scan — a broken extension fails the process
748
- // LOUDLY here (loadExtensions throws), never silently.
749
- const user = await loadExtensions(extensionsDir());
750
- const proj = project !== null ? await loadProjectExtensions(process.cwd(), user) : [];
751
- // R-D 0.1.45: the built-in layer registers by module import (builtin.ts)
752
- // — a user extension may shadow a built-in, a project one may not.
753
- // KC3.5: built-in #4 (ask) registers ONLY where a human can answer —
754
- // the panel bridge is the argument, and a non-TTY session has none to
755
- // give. A piped run's composed tool table therefore cannot contain
756
- // ask_user (T-Q3: the bench's structural byte-identity proof).
757
- // Astra F7: the config is READ here — a pure read, its setters still run
758
- // below with the rest of merge round B — because the mcp extension spawns
759
- // its stdio children while it is being constructed, and the strip needs
760
- // the configured secret names by then.
761
- const userCfg = loadUserConfig();
762
- const projectCfg = loadProjectConfig(process.cwd(), project !== null);
763
- const merged = mergeConfigs(userCfg, projectCfg);
764
- // 0.40.6: /settings names each value's layer from these
765
- settingsLayers.user = userCfg;
766
- settingsLayers.project = projectCfg;
767
- settingsLayers.modelFlag = modelFlag;
768
- const secretEnvNames = secretEnvNamesOf(merged.models ?? {});
769
- const builtIn = await builtInLayer(user, proj, input !== undefined && process.stdin.isTTY ? askUi(input) : undefined, secretEnvNames);
770
- setExtensionLists(builtIn, user, proj, [...builtIn, ...user, ...proj]);
771
- // merge round B — the config surface: user config + (trusted) project config,
772
- // resolved with flags > env > project > user > default. The CLI never
773
- // imports provider SDKs directly — the runtime's lazy provider
774
- // resolution owns them (a config profile only ever NAMES an env var for
775
- // its key; the key itself never sits in a config file).
776
- setMergedConfig(merged);
777
- // DT-1a: what a delegated task may NAME — the configured checks and the
778
- // model profiles — handed to the (in-process) subagent extension through
779
- // the environment. A model never supplies a command; it names a check.
780
- // 0.40.0: and the folder this process keeps its sessions in — a child
781
- // writes beside its parent, whatever directory it runs in. Through this
782
- // channel and not an exported KISO_SESSIONS_DIR: a variable in
783
- // process.env would reach every shell child, and a kiso started from a
784
- // shell tool would then write into this project's folder.
785
- process.env.KISO_DELEGATION_CONFIG_JSON = JSON.stringify({ checks: merged.checks ?? {}, profiles: Object.keys(merged.models ?? {}), sessionsDir: sessionsDir() });
786
- setConfigModels(merged.models ?? {});
787
- // CW-1 batch 2: the windows endpoints stated by refusing — read before the
788
- // first window is asked for (the unknown-window notice below).
789
- useLearnedWindows();
790
- const resolved = resolveModel(modelFlag, merged);
791
- // AFTER the model resolves: the window a PROFILE states is about that
792
- // profile's model, so it cannot be read before we know which profile is
793
- // selected. Reading it a line too early is how the compaction threshold
794
- // ended up frozen at the wrong model's value (CTX-1).
795
- setConfiguredWindow(resolveContextWindow(merged, resolved?.profile));
796
- const model = resolved === null ? "faux" : resolved.profile.model;
797
- if (resolved === null) {
798
- console.log("[faux mode — set ANTHROPIC_API_KEY or OPENAI_API_KEY, or configure models in ~/.kiso/config.json]\n");
799
- setCurrentFaux(true);
800
- setCurrentModelName("faux");
801
- setCurrentProfileName(null);
802
- }
803
- else {
804
- setCurrentFaux(false);
805
- setCurrentModelName(resolved.name);
806
- setCurrentProfileName(resolved.name);
807
- }
808
- setAgentModel(model, resolved?.profile.baseUrl); // v2b: the status bar shows it; OR-1: the endpoint rides along
809
- // ADR-0055 Amendment 2: the row says `ctx ?` for an unstated window, but
810
- // the tiers still need a number and assume the fallback — say so, once,
811
- // at build.
812
- if (!unknownWindowNoticed && knownContextWindow() === null) {
813
- unknownWindowNoticed = true;
814
- console.error(unknownWindowNotice(model));
815
- }
816
- // W21: the extensions array is built ONCE per agent and shared with
817
- // the runtime by reference — the don't-ask-again writer pushes the
818
- // generated extension into it so a first-time rule joins the chain
819
- // at the NEXT run (the run's policies are fixed at its start; run.ts
820
- // re-reads the config's extensions array per run).
821
- // LT-2: the loop breaker at the chain HEAD — it speaks first when it speaks,
822
- // so `decidedBy` names it; a deny there beats every tier, bypass included.
823
- // 0.40.0: the read-only shell allow sits after the tiers — an allow from
824
- // it outranks a tier's ask and names itself in decidedBy.
825
- // 0.40.0: a saved allow never carries a write into .git/ or .kiso/, nor
826
- // a destructive shell command.
827
- const workspaceRoot = () => codingToolOptions().workspaceRoot;
828
- const neverInherited = (call) => isProtectedWrite(call, workspaceRoot()) || isDestructiveCall(call);
829
- setNeverInherited(neverInherited);
830
- // 0.40.0: the catastrophe floor, at the chain's HEAD — a deny there
831
- // names itself in decidedBy and outranks every tier, bypass included.
832
- // Read per agent, so /reload picks up an edited user config.
833
- const userConfig = loadUserConfig();
834
- setFloorOn(userConfig?.floor !== "off");
835
- // kiso never serves its own credential store to a model: the file tools
836
- // refuse it through codingToolOptions, and this member denies a shell
837
- // line naming it — at the HEAD with the floor, in every mode, and NOT
838
- // switched off with it (`floor: "off"` lowers the catastrophe floor,
839
- // never this).
840
- setUserProtectedPaths(userConfig?.protectedPaths);
841
- const extensions = [
842
- protectedShellExtension({ files: protectedFiles, workspaceRoot, env: () => ({ home: homedir(), kisoHome: kisoHome() }) }),
843
- floorExtension(() => floorOn, workspaceRoot),
844
- breakerExtension(),
845
- ...modeExtensions(workspaceRoot),
846
- readOnlyShellExtension(codingToolOptions),
847
- ...loadedExtensions.map((e) => guardSavedAllow(e, neverInherited)),
848
- ];
849
- setCurrentAgentExtensions(extensions);
850
- // E6: the run-start context policy (captured once — exactOptionalPropertyTypes).
851
- const contextPolicy = contextPolicyFromEnv();
852
- const idleFromEnv = streamIdleFromEnv(); // read once: a narrowed const, not a call per spread
853
- const retriesFromEnv = maxRetriesFromEnv();
854
- const definition = {
855
- model,
856
- store,
857
- // 0.40.0: a NEW session records where it started; the picker scopes by
858
- // it. The profile name is recorded only when the model came from a
859
- // config profile — a direct provider/model or an env key names none.
860
- workspace: workspaceRoot(),
861
- ...(resolved !== null && merged.models?.[resolved.name] === resolved.profile ? { profileName: resolved.name } : {}),
862
- // Area 5: the coding tools are bound to the workspace — every path
863
- // they touch is canonicalized inside cwd, escapes are refused.
864
- tools: [...createCodingTools(codingToolOptions())], // DC-49 — the options live in state.ts, shared with the `!` command's runner
865
- // Modes: the five tiers ride the E1 policy chain (mode:<tier>
866
- // extensions, current tier first) — the old static PERMISSION_POLICY
867
- // is gone, its semantics live in the "default" tier. The banner
868
- // still counts loadedExtensions only — the modes are in-process,
869
- // never a file extension.
870
- systemPrompt: (() => {
871
- const sp = composeSystemPrompt(process.cwd(), protectedFiles());
872
- const extra = modeSystemPrompt();
873
- return extra === undefined ? sp : `${sp}\n\n${extra}`;
874
- })(),
875
- // ADR-0055 Amendment 1 (A1b): compaction is ON by default and runs
876
- // INSIDE a run, by tiers drawn from the model's window (CTX-1: the
877
- // binding step moves the window with /model and /resume). The
878
- // standing microcompact at half the window is gone (A4). Phase rule
879
- // 1 reads the user's configured checks first, then the runner table.
880
- contextPolicy: {
881
- ...(contextPolicy ?? {}),
882
- tiers: {
883
- windowTokens: contextWindowTokens(),
884
- isCheck: (command) => runsACheck(command, Object.values(merged.checks ?? {})),
885
- // ADR-0055 Amendment 2: a discarded checkpoint says so, in sizes only.
886
- onDiscard: (d) => body.notice(compactionDiscardedNotice(d)),
887
- // ADR-0055 Amendment 2 (decision 3): only a window someone stated
888
- // arms the overflow belt — read from the LIVE binding, so it moves
889
- // with /model and /resume; the fallback reads as null.
890
- statedWindow: () => knownContextWindow(),
891
- // CW-1 batch 2: an endpoint's refusal stated its cap. The tiers
892
- // took it already; kept, it is where the next session starts, and
893
- // the status row's denominator moves with it. Said once per new
894
- // figure — a refusal at a cap already kept says nothing.
895
- onWindowLearned: (w) => {
896
- if (recordLearnedWindow(w.model, w.baseUrl, w.tokens))
897
- body.notice(windowLearnedNotice(w.model, providerHost(w.baseUrl) ?? "", w.tokens));
898
- },
899
- },
900
- },
901
- // R3e (owner ruling, 2026-08-28): NO turn limit on an interactive
902
- // session. This was `maxTurns: 20`, hardcoded on 2026-08-03 with no
903
- // stated reason and no way to change it — and it was the thing that
904
- // stopped a real 43-call session dead, mid-task, in silence. The
905
- // field survives for the callers that want a bound (subagents, the
906
- // SDK, `kiso run`); the interactive front door does not set one.
907
- // Modes: the five tiers join at the CHAIN HEAD, before the user/
908
- // project extensions (the deny>allow>ask composition keeps a user
909
- // deny winning over any mode tier — bypass included).
910
- extensions,
911
- ...(resolved !== null
912
- ? {
913
- provider: resolved.profile.kind,
914
- // Astra F1 (P0): the wire config is built in ONE place
915
- // (auth/adapter-options.ts) — credential shape, the ALWAYS
916
- // EXPLICIT endpoint, the session's cache key and the
917
- // profile's caching flag. This site and the `/model` site
918
- // were the same spread written twice.
919
- ...adapterOptionsFor(resolved.profile, resolved.oauthProviderId !== undefined
920
- ? { type: "oauth", providerId: resolved.oauthProviderId }
921
- : { type: "api-key", apiKey: resolved.apiKey ?? "none" }, sessionId),
922
- // LT-1: the profile's stream watchdog bound, if it states one.
923
- // NOT an adapter option — an agent-definition field the
924
- // runtime reads — so it stays here rather than moving into
925
- // adapterOptionsFor.
926
- ...(resolved.profile.streamIdleMs !== undefined ? { streamIdleMs: resolved.profile.streamIdleMs } : {}),
927
- }
928
- : { adapter: createFauxProvider(readFauxScript().slice(fauxSkipTurns)) }),
929
- // LT-1: KISO_STREAM_IDLE_MS (the test rigs' knob) beats the profile —
930
- // the last spread wins, which is why it sits after the profile's.
931
- ...(idleFromEnv !== undefined ? { streamIdleMs: idleFromEnv } : {}),
932
- ...(retriesFromEnv !== undefined ? { maxRetries: retriesFromEnv } : {}),
933
- // ADR-0005 Amendment 2: the kernel announces each retry before its
934
- // wait; the running row shows it. Composed with every extension's
935
- // hooks by the runtime — an extension observing retries too is heard.
936
- hooks: {
937
- onRetry: async (info) => {
938
- setRetryShown({ attempt: info.attempt, maxRetries: info.maxRetries, code: info.code, until: Date.now() + info.delayMs });
939
- },
940
- },
941
- };
942
- return createAgent(definition);
943
- }
944
576
  export { workspaceRoot } from "./state.js";
945
577
  /** TUI2-R2 ② — the picker's affordance row: the keys, said where the
946
578
  * keys are useful. */
@@ -953,7 +585,7 @@ const PICKER_HINT = "↑↓ pick · ⏎ resumes · type filters · esc";
953
585
  async function sessionCards(_agent, announce = (l) => bodyLog(l)) {
954
586
  const store = sessionStoreRef;
955
587
  if (store === null)
956
- return []; // unreachable: makeAgent builds the store first
588
+ return []; // unreachable: createCodingAgent builds the store first
957
589
  const folders = listingFolders();
958
590
  // 0.40.0 dogfood (item 2, lead's ruling A): the ONE-TIME migration — the
959
591
  // first list after the upgrade reads each legacy log exactly once and
@@ -987,28 +619,6 @@ function listingFolders() {
987
619
  const mine = all.find((f) => f.dir === here) ?? { dir: here, workspace: projectRoot(), kind: "project" };
988
620
  return [mine, ...all.filter((f) => f.dir !== here)];
989
621
  }
990
- /**
991
- * 0.40.0 — before any session opens: the one-time move of the legacy
992
- * folder into project folders (announced once, with the undo), then this
993
- * project's folder claimed. Nothing happens under a pinned folder or after
994
- * a reversed migration.
995
- */
996
- function prepareSessionFolders() {
997
- const home = kisoHome();
998
- if (!projectLayoutActive(home))
999
- return;
1000
- if (pendingLegacyIds(home).length > 0) {
1001
- const result = runMigration(home, planMigration(home));
1002
- if (result !== null && result.moved > 0) {
1003
- const line = migrationNotice(result);
1004
- if (process.stdout.isTTY)
1005
- bodyLog(line);
1006
- else
1007
- process.stderr.write(`${line}\n`);
1008
- }
1009
- }
1010
- claimProjectDir(ownSessionsDir(), projectRoot());
1011
- }
1012
622
  /**
1013
623
  * 0.40.0 (the owner's dogfood: a uooki session resumed from flowpix2) —
1014
624
  * whether a session id may open in this project. Its own folder: yes. A
@@ -1052,8 +662,6 @@ function sessionFolderOf(id) {
1052
662
  const route = routeSession(id);
1053
663
  return route?.kind === "elsewhere" ? route.dir : ownSessionsDir();
1054
664
  }
1055
- /** The folder makeAgent built the store on. */
1056
- let activeStoreDir = "";
1057
665
  /** The workspace a listing is scoped to: a project folder's identity under
1058
666
  * the per-project layout, the recorded realpath in one pinned folder. */
1059
667
  function scopeRoot() {
@@ -1064,7 +672,7 @@ function tildeOf(path) {
1064
672
  return path === home || path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
1065
673
  }
1066
674
  /** The explicit-id doors (`chat <id>`, `resume <id>`, `-p … <id>`), BEFORE
1067
- * makeAgent builds the store: a refusal is the entry's error (the line on
675
+ * createCodingAgent builds the store: a refusal is the entry's error (the line on
1068
676
  * stderr, exit 2); a session in another folder opens there. Returns the
1069
677
  * line to say once the session is up. */
1070
678
  function enterRouted(id) {
@@ -1126,7 +734,7 @@ async function pickSession(agent, input) {
1126
734
  * tier, the /mode hint, the model and the remaining context belong.
1127
735
  *
1128
736
  * It is painted HERE, at the first moment every field is TRUE: after
1129
- * makeAgent, because that is where the model is resolved. Painting it at
737
+ * createCodingAgent, because that is where the model is resolved. Painting it at
1130
738
  * dock.enter() — the literally-first frame — would have to name a model
1131
739
  * nobody had chosen yet, and a status row that guesses is worse than a
1132
740
  * status row that waits two hundred milliseconds.
@@ -1145,7 +753,6 @@ async function pickSession(agent, input) {
1145
753
  * once; 0.41.0 removes it.
1146
754
  */
1147
755
  let autoCompactNoticed = false;
1148
- let unknownWindowNoticed = false;
1149
756
  function retiredAutoCompact(merged) {
1150
757
  if (!autoCompactNoticed && (process.env.KISO_AUTO_COMPACT !== undefined || resolveAutoCompact(merged) !== undefined)) {
1151
758
  autoCompactNoticed = true;
@@ -1244,21 +851,21 @@ function skillCount() {
1244
851
  * invisible in it, because nothing the model said or did changes.
1245
852
  */
1246
853
  async function reloadAgent(old, id, input, announce = true) {
1247
- // Snapshot BEFORE makeAgent: it overwrites every one of these, so the
854
+ // Snapshot BEFORE createCodingAgent: it overwrites every one of these, so the
1248
855
  // old set would be unreachable by the time we needed to dispose it.
1249
856
  const oldLoaded = loadedExtensions;
1250
857
  const oldBuiltIn = builtInExtensions;
1251
858
  const oldUser = userExtensions;
1252
859
  const oldProject = projectExtensions;
1253
860
  const oldTemps = mergedTempPaths.splice(0);
1254
- // makeAgent publishes the config side BEFORE it can fail on the model,
861
+ // createCodingAgent publishes the config side BEFORE it can fail on the model,
1255
862
  // so a failure past the extension load would leave a NEW window, model
1256
863
  // name and profile table beside the OLD agent — and the failure line
1257
864
  // says nothing changed. Rather than soften the line, the snapshot makes
1258
865
  // it true: a failed reload is atomic, which is what load-then-swap
1259
866
  // promises. (Lead's review of 5bee644, observation 1.)
1260
867
  const oldCfg = {
1261
- // RL-F6: makeAgent publishes this BEFORE createAgent, and createAgent
868
+ // RL-F6: createCodingAgent publishes this BEFORE createAgent, and createAgent
1262
869
  // throws on a tool-name collision — a user extension exposing
1263
870
  // `read_file` is enough — so a failure CAN land after it. Left
1264
871
  // unrestored, the new array stands beside the old agent, whose config
@@ -1280,19 +887,19 @@ async function reloadAgent(old, id, input, announce = true) {
1280
887
  };
1281
888
  let next;
1282
889
  try {
1283
- next = await makeAgent(id, input, modelChoice);
890
+ next = await createCodingAgent(id, input, modelChoice);
1284
891
  }
1285
892
  catch (err) {
1286
893
  // Nothing is swapped. Clean up whatever the failed attempt managed
1287
894
  // to create, put the old set's own temp paths back so exit still
1288
- // removes them, and restore the lists in case makeAgent got far
895
+ // removes them, and restore the lists in case createCodingAgent got far
1289
896
  // enough to publish new ones.
1290
897
  for (const p of mergedTempPaths.splice(0)) {
1291
898
  try {
1292
899
  rmSync(p, { recursive: true, force: true });
1293
900
  }
1294
901
  catch {
1295
- // best-effort — the temp dir would be reaped by the OS
902
+ // best-effort — a survivor is removed by a later startup's sweep (temp-sweep.ts) once this process is gone
1296
903
  }
1297
904
  }
1298
905
  mergedTempPaths.push(...oldTemps);
@@ -1321,7 +928,7 @@ async function reloadAgent(old, id, input, announce = true) {
1321
928
  rmSync(p, { recursive: true, force: true });
1322
929
  }
1323
930
  catch {
1324
- // best-effort — the temp dir would be reaped by the OS
931
+ // best-effort — a survivor is removed by a later startup's sweep (temp-sweep.ts) once this process is gone
1325
932
  }
1326
933
  }
1327
934
  if (announce)
@@ -1357,8 +964,8 @@ async function chatLoop(first, firstId, input, autoCompact) {
1357
964
  setOpenSessionFolder(folder === ownSessionsDir() ? null : folder);
1358
965
  const next = await reloadAgent(agent, id, input, false);
1359
966
  if (next === agent) {
1360
- // makeAgent may have built the new store before it failed
1361
- activeStoreDir = was.dir;
967
+ // createCodingAgent may have built the new store before it failed
968
+ setActiveStoreDir(was.dir);
1362
969
  if (was.store !== null)
1363
970
  setSessionStore(was.store);
1364
971
  setOpenSessionFolder(was.dir === ownSessionsDir() ? null : was.dir);
@@ -1473,7 +1080,7 @@ async function chatLoop(first, firstId, input, autoCompact) {
1473
1080
  setCurrentModelName(session.model);
1474
1081
  // A SWITCH knows only a model id here — nothing that names the profile —
1475
1082
  // so the mark is cleared rather than guessed. A FRESH start keeps what
1476
- // makeAgent resolved from `--model <profile>`, which is the one place
1083
+ // createCodingAgent resolved from `--model <profile>`, which is the one place
1477
1084
  // the name is known (prev is null only on the first entry).
1478
1085
  if (prev !== null)
1479
1086
  setCurrentProfileName(null);
@@ -1583,7 +1190,7 @@ async function main() {
1583
1190
  // banner and the first frame are in the record. Off by default.
1584
1191
  armByteTrace();
1585
1192
  // Modes: --mode <name> wins over KISO_MODE — both applied before the
1586
- // first makeAgent (the tier extensions read `current` live). The flag
1193
+ // first createCodingAgent (the tier extensions read `current` live). The flag
1587
1194
  // is stripped from the positional args, so it works in any position.
1588
1195
  const args = process.argv.slice(2);
1589
1196
  // XP-1: --accept-drift (a flag, never an env var) authorizes opening a
@@ -1627,7 +1234,7 @@ async function main() {
1627
1234
  args.splice(printIdx, 2);
1628
1235
  }
1629
1236
  // merge round B: --model <profile|provider/model> — the top of the model
1630
- // precedence chain; the value flows into makeAgent's config resolution.
1237
+ // precedence chain; the value flows into createCodingAgent's config resolution.
1631
1238
  let modelFlag;
1632
1239
  const modelArgIdx = args.indexOf("--model");
1633
1240
  if (modelArgIdx !== -1) {
@@ -1640,7 +1247,7 @@ async function main() {
1640
1247
  }
1641
1248
  // Modes: --mode wins over KISO_MODE, which wins over the USER config's
1642
1249
  // mode (the project config's mode applies later — after the trust gate,
1643
- // inside makeAgent — unless a higher layer already decided).
1250
+ // inside createCodingAgent — unless a higher layer already decided).
1644
1251
  const modeFlag = args.indexOf("--mode");
1645
1252
  if (modeFlag !== -1) {
1646
1253
  const m = MODES.find((x) => x === args[modeFlag + 1]);
@@ -1717,7 +1324,7 @@ async function main() {
1717
1324
  const [command, arg] = args;
1718
1325
  // round 8: faux mode is the keyless demo script — an exhausted script must
1719
1326
  // exit non-zero, never masquerade as a successful provider run. The
1720
- // verdict comes from makeAgent's config resolution now (a config
1327
+ // verdict comes from createCodingAgent's config resolution now (a config
1721
1328
  // profile can provide a real model with no OPENAI_* env).
1722
1329
  let faux = true;
1723
1330
  let agent;
@@ -1831,7 +1438,7 @@ async function main() {
1831
1438
  // verbatim (a fresh id makes the recovery a no-op)
1832
1439
  const id = command ?? newSessionId(sessionsDir());
1833
1440
  const routeLine = command !== undefined ? enterRouted(id) : null;
1834
- agent = await makeAgent(id, input, modelFlag);
1441
+ agent = await createCodingAgent(id, input, modelFlag);
1835
1442
  applyConfigMode();
1836
1443
  sayRouteLine(routeLine);
1837
1444
  const session = await agent.session({ id, ...(acceptDrift() ? { acceptDrift: true } : {}) });
@@ -1851,7 +1458,7 @@ async function main() {
1851
1458
  // E area: a resumed session continues the script at its durable
1852
1459
  // position — never restarts it (fauxSkip).
1853
1460
  const routeLine = arg !== undefined ? enterRouted(id) : null;
1854
- agent = await makeAgent(id, input, modelFlag);
1461
+ agent = await createCodingAgent(id, input, modelFlag);
1855
1462
  applyConfigMode();
1856
1463
  sayRouteLine(routeLine);
1857
1464
  faux = currentFaux;
@@ -1873,7 +1480,7 @@ async function main() {
1873
1480
  const prompt = process.argv[4];
1874
1481
  dock.enter();
1875
1482
  const routeLine = arg !== undefined ? enterRouted(arg) : null;
1876
- agent = await makeAgent(arg, input, modelFlag);
1483
+ agent = await createCodingAgent(arg, input, modelFlag);
1877
1484
  applyConfigMode();
1878
1485
  let id = arg;
1879
1486
  if (id === undefined) {
@@ -1907,7 +1514,7 @@ async function main() {
1907
1514
  faux = currentFaux;
1908
1515
  // E area: the durable script position is computed from the
1909
1516
  // session id, and on the picker path the id did not exist when
1910
- // makeAgent ran. Re-arm the scripted adapter at the PICKED
1517
+ // createCodingAgent ran. Re-arm the scripted adapter at the PICKED
1911
1518
  // session's position so a picked resume continues its script
1912
1519
  // exactly where `kiso resume <id>` would have.
1913
1520
  if (faux && arg === undefined)
@@ -1921,14 +1528,14 @@ async function main() {
1921
1528
  case "sessions": {
1922
1529
  // R-I-p2 audit (the argument-consistency mandate): the
1923
1530
  // read-only listing NEVER writes through the input, but the
1924
- // trust gate lives inside makeAgent and ASKS through it — on
1531
+ // trust gate lives inside createCodingAgent and ASKS through it — on
1925
1532
  // a TTY with a first-discovery .kiso, the undefined input
1926
1533
  // crashed identically to the bare command (finding R-I-p-2,
1927
1534
  // "reading 'question'" on the dock-less branch). The input
1928
1535
  // exists so the gate's ask can be answered; the listing
1929
1536
  // itself never touches it.
1930
1537
  if (pruneEmpty !== undefined) {
1931
- // DC-60: before makeAgent — the scan only reads, and nothing
1538
+ // DC-60: before createCodingAgent — the scan only reads, and nothing
1932
1539
  // needs an agent to move a file to the Trash
1933
1540
  const { empty, inUse } = findEmptySessions(listingFolders().map((f) => f.dir));
1934
1541
  const n = (k, w) => `${k} ${w}${k === 1 ? "" : "s"}`;
@@ -1947,13 +1554,13 @@ async function main() {
1947
1554
  break;
1948
1555
  }
1949
1556
  if (reverseManifest !== undefined) {
1950
- // before makeAgent: its folder preparation would otherwise run
1557
+ // before createCodingAgent: its folder preparation would otherwise run
1951
1558
  // the very move this undoes
1952
1559
  const { restored, left } = reverseMigration(kisoHome(), reverseManifest);
1953
1560
  console.log(`moved ${restored} session${restored === 1 ? "" : "s"} back to ${join(kisoHome(), "sessions")}; one folder per project is off until ${join(kisoHome(), "projects", ".migration-reversed")} is removed${left > 0 ? ` — ${left} created since the move stay in their project folders` : ""}`);
1954
1561
  break;
1955
1562
  }
1956
- agent = await makeAgent(undefined, input, modelFlag);
1563
+ agent = await createCodingAgent(undefined, input, modelFlag);
1957
1564
  // TUI2-R2 ③ — the same projection the picker renders, printed.
1958
1565
  // The PIPE keeps today's bytes exactly: `kiso sessions` is
1959
1566
  // something scripts read, and a badge column is a TTY-render
@@ -2047,7 +1654,7 @@ async function main() {
2047
1654
  if (cred.type === "api-key")
2048
1655
  lines.push(` ${id.padEnd(10)} api-key ${maskSecret(cred.key)} saved ${new Date(cred.savedAt).toISOString().slice(0, 10)}`);
2049
1656
  else
2050
- lines.push(` ${id.padEnd(10)} oauth ${cred.accountId ?? ""} expires ${new Date(cred.expires).toISOString()}${cred.expires < Date.now() ? " (expired)" : ""}`);
1657
+ lines.push(` ${id.padEnd(10)} oauth ${cred.accountId ?? ""} expires ${new Date(cred.expires).toISOString()}${cred.refreshRejectedAt !== undefined ? ` (renewal refused — run kiso login ${id})` : cred.expires < Date.now() ? " (expired — renews on use)" : ""}`);
2051
1658
  }
2052
1659
  const envs = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"].filter((k) => process.env[k] !== undefined);
2053
1660
  lines.push(`env vars set: ${envs.length ? envs.join(", ") : "none"} (a stored credential owns its provider; env applies only when nothing is stored)`);
@@ -2161,7 +1768,7 @@ async function main() {
2161
1768
  // through the undefined input: "Cannot read properties of
2162
1769
  // undefined" at the ask (panelAsk with the dock, question on
2163
1770
  // the dock-less fallback).
2164
- agent = await makeAgent(id, input, modelFlag);
1771
+ agent = await createCodingAgent(id, input, modelFlag);
2165
1772
  // finding E4-1's faux resolution rides chatLoop (currentFaux).
2166
1773
  faux = currentFaux;
2167
1774
  // CX-1 F9 (audit F9): the bare entry resolves autoCompact from
@@ -2198,7 +1805,7 @@ async function main() {
2198
1805
  rmSync(p, { recursive: true, force: true });
2199
1806
  }
2200
1807
  catch {
2201
- // best-effort — the temp dir would be reaped by the OS
1808
+ // best-effort — a survivor is removed by a later startup's sweep (temp-sweep.ts) once this process is gone
2202
1809
  }
2203
1810
  }
2204
1811
  }