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