@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.
@@ -10,6 +10,10 @@ export type Credential = {
10
10
  readonly expires: number;
11
11
  readonly accountId?: string;
12
12
  readonly savedAt: number;
13
+ /** 0.40.7: when the token endpoint REFUSED this credential's
14
+ * refresh token. Set only by that refusal (never by a network
15
+ * failure); a new login replaces the whole entry and so clears it. */
16
+ readonly refreshRejectedAt?: number;
13
17
  };
14
18
  export interface AuthFile {
15
19
  readonly version: 1;
@@ -1,4 +1,4 @@
1
- import type { OAuthCredential, OAuthFlow } from "./index.js";
1
+ import { type OAuthCredential, type OAuthFlow } from "./index.js";
2
2
  export declare const CHATGPT: {
3
3
  readonly providerId: "chatgpt";
4
4
  readonly clientId: "app_EMoamEEZ73f0CkXaXp7hrann";
@@ -17,6 +17,7 @@
17
17
  * endpoint — the Responses adapter is the second half of this step.
18
18
  */
19
19
  import { createServer } from "node:http";
20
+ import { RefreshRejectedError } from "./index.js";
20
21
  import { generatePkce, randomState } from "./pkce.js";
21
22
  export const CHATGPT = {
22
23
  providerId: "chatgpt",
@@ -86,7 +87,11 @@ export function accountIdOf(accessToken) {
86
87
  async function readTokenResponse(response, operation) {
87
88
  if (!response.ok) {
88
89
  const text = await response.text().catch(() => "");
89
- throw new Error(`ChatGPT sign-in: token ${operation} failed (${response.status})${text ? `: ${text.slice(0, 200)}` : ""}`);
90
+ const message = `ChatGPT sign-in: token ${operation} failed (${response.status})${text ? `: ${text.slice(0, 200)}` : ""}`;
91
+ // the endpoint's own refusal of the refresh token — the sign-in is over
92
+ if (operation === "refresh" && (response.status === 400 || response.status === 401))
93
+ throw new RefreshRejectedError(message);
94
+ throw new Error(message);
90
95
  }
91
96
  const json = (await response.json());
92
97
  if (typeof json.access_token !== "string" || typeof json.refresh_token !== "string" || typeof json.expires_in !== "number") {
@@ -8,6 +8,14 @@ import type { Credential } from "../credentials.js";
8
8
  export type OAuthCredential = Extract<Credential, {
9
9
  type: "oauth";
10
10
  }>;
11
+ /**
12
+ * 0.40.7 — a refresh the token endpoint REFUSED (400 / 401: the refresh
13
+ * token was revoked or has expired), as distinct from one that never got an
14
+ * answer. Only this marks a sign-in dead (`refreshRejectedAt`); a network
15
+ * failure leaves the credential as it was, to be tried again.
16
+ */
17
+ export declare class RefreshRejectedError extends Error {
18
+ }
11
19
  export interface LoginInteraction {
12
20
  /** Print a line for the person signing in (the URL to open, the paste prompt). */
13
21
  readonly notify: (line: string) => void;
@@ -1 +1,8 @@
1
- export {};
1
+ /**
2
+ * 0.40.7 — a refresh the token endpoint REFUSED (400 / 401: the refresh
3
+ * token was revoked or has expired), as distinct from one that never got an
4
+ * answer. Only this marks a sign-in dead (`refreshRejectedAt`); a network
5
+ * failure leaves the credential as it was, to be tried again.
6
+ */
7
+ export class RefreshRejectedError extends Error {
8
+ }
@@ -7,6 +7,6 @@
7
7
  * never a silent fall back to an API key or an env var.
8
8
  */
9
9
  import { type Credential } from "./credentials.js";
10
- import type { OAuthFlow } from "./oauth/index.js";
10
+ import { type OAuthFlow } from "./oauth/index.js";
11
11
  export declare const FRESH_WINDOW_MS: number;
12
12
  export declare function ensureFresh(providerId: string, flow: OAuthFlow, path?: string, now?: number, tokenUrl?: string): Promise<Credential>;
@@ -7,6 +7,7 @@
7
7
  * never a silent fall back to an API key or an env var.
8
8
  */
9
9
  import { AuthError, modifyAuthFile, readAuthFile } from "./credentials.js";
10
+ import { RefreshRejectedError } from "./oauth/index.js";
10
11
  export const FRESH_WINDOW_MS = 5 * 60_000;
11
12
  export async function ensureFresh(providerId, flow, path, now = Date.now(), tokenUrl) {
12
13
  const current = readAuthFile(path).credentials[providerId];
@@ -14,6 +15,10 @@ export async function ensureFresh(providerId, flow, path, now = Date.now(), toke
14
15
  throw new AuthError(`not signed in to ${providerId}: run \`kiso login ${providerId}\``);
15
16
  if (current.type !== "oauth" || current.expires - now > FRESH_WINDOW_MS)
16
17
  return current;
18
+ // 0.40.7: a refresh token the endpoint already refused is not sent again —
19
+ // the answer would be the same refusal, one round trip later
20
+ if (current.refreshRejectedAt !== undefined)
21
+ throw new AuthError(`signed out of ${providerId} (the sign-in was refused when kiso tried to renew it) — run \`kiso login ${providerId}\``);
17
22
  let refreshed;
18
23
  let error;
19
24
  // the double check: another process may have refreshed while we waited for the lock
@@ -26,8 +31,22 @@ export async function ensureFresh(providerId, flow, path, now = Date.now(), toke
26
31
  catch (err) {
27
32
  error = err instanceof Error ? err : new Error(String(err));
28
33
  }
29
- if (refreshed === undefined)
34
+ if (refreshed === undefined) {
35
+ // 0.40.7: the endpoint's REFUSAL is remembered on the credential, so
36
+ // /model can say the sign-in is over before a turn finds out. The
37
+ // same generation rule as the write-back below (R2a): a logout or a
38
+ // new login while the refresh was on the wire is left alone.
39
+ if (error instanceof RefreshRejectedError) {
40
+ const began = current;
41
+ modifyAuthFile((file) => {
42
+ const latest = file.credentials[providerId];
43
+ if (latest === undefined || latest.type !== "oauth" || latest.refresh !== began.refresh || latest.expires !== began.expires)
44
+ return file;
45
+ return { version: 1, credentials: { ...file.credentials, [providerId]: { ...latest, refreshRejectedAt: now } } };
46
+ }, path);
47
+ }
30
48
  throw new AuthError(`signed out of ${providerId} (the token could not be refreshed: ${error?.message ?? "unknown"}) — run \`kiso login ${providerId}\``);
49
+ }
31
50
  const written = refreshed;
32
51
  // R2a — THE WRITE-BACK MAY NOT RESURRECT WHAT THE HUMAN REMOVED.
33
52
  //
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The coding product's prompt policy — the base system prompt and the
3
+ * project-instruction injection. Moved verbatim from index.ts (R1,
4
+ * 2026-09-23): the entry module keeps no prompt text of its own.
5
+ */
6
+ /**
7
+ * A area: the coding-agent system prompt — ONE constant, byte-stable for the
8
+ * session's lifetime (D area). Kept under ~80 lines; no template engine.
9
+ */
10
+ /** The built-in prompt. Exported for scripts/request-surface.mjs — the
11
+ * model-side token-rent counter measures the REAL bytes, never a copy. */
12
+ export declare const SYSTEM_PROMPT = "You are kiso, a coding agent. You work in a workspace\ndirectory and change code with tools. Be concise: answer in a few lines\nunless the task genuinely needs more. Never claim a file was changed\nunless a tool confirmed it.\n\nWhat you can reach:\n- The workspace: read_file, list_dir, search_text, write_file, edit_file.\n- This machine and the network: shell \u2014 builds, tests, git, package\n managers, curl for HTTP, system queries. A request one command can\n answer is answered by running it.\n- The human: ask_user, for a decision that is theirs.\n\nTool discipline:\n- READ BEFORE YOU EDIT. For any file you are about to change, read it\n first \u2014 never guess its content.\n- Use edit_file for targeted changes and write_file for full rewrites.\n Prefer many small edits over one large write.\n- Be careful \u2014 shell has side effects and may take time.\n- search_text and list_dir are cheap \u2014 locate first, then read ranges\n with read_file offset/limit; never read a whole large file in one call.\n- Do not re-read a file you already read unchanged, or one you changed\n yourself through a confirmed edit \u2014 rely on the earlier result and\n on the change you just made.\n- When a tool fails, read the error and adjust; do not repeat the same\n call blindly.\n\nAn authorization covers what it NAMES: when the scope is not named \u2014\nwhich files, which branches, whether to push \u2014 ask before acting.\nDelivering, sharing or handing over changes means committing locally and\nstopping; pushing, publishing or sending happens only when the human\nnames it.\n\nWorkflow: understand the request, find the relevant code, make the\nsmallest change that works, then verify with a command (tests/build).\nReport what you did in one or two lines per change.";
13
+ /** Hard cap for injected instructions — truncate and say so. */
14
+ /**
15
+ * A area: read the FIRST present instruction file (AGENTS.md preferred) and
16
+ * return it as an injected section, or "" when none exists. Truncated at
17
+ * 8KB with an explicit note. Pure — read once per session, so the prompt
18
+ * is byte-stable for the session's lifetime.
19
+ *
20
+ * kiso never serves its own credential store to a model, and this read is
21
+ * the one that needs no model at all: a cloned repository whose AGENTS.md
22
+ * is a symlink to the store would put it in the SYSTEM PROMPT of every
23
+ * request, with no tool call and no trust prompt. A file that resolves to
24
+ * a protected one is skipped as though absent.
25
+ */
26
+ export declare function readProjectInstructions(cwd: string, protectedFiles?: readonly string[]): string;
27
+ /** A area: the session's system prompt — the constant plus any project
28
+ * instructions found in the workspace. Deterministic per cwd. */
29
+ export declare function composeSystemPrompt(cwd: string, protectedFiles?: readonly string[]): string;
30
+ /**
31
+ * The coding agent's tool-table vocabulary — one routing line per tool, in
32
+ * the table only while that tool is active (0.1.40, R-C item 1: the
33
+ * reference implementation's content in kiso's voice). ACI-6 made this the
34
+ * ONE canonical statement of the routing policy; the base prompt above no
35
+ * longer restates it.
36
+ *
37
+ * R1 (2026-09-23): moved verbatim from the runtime's compose.ts, where it
38
+ * sat as a constant and reached every host whose tools shared these names.
39
+ * The rows, their order and their position in the table are unchanged —
40
+ * the byte fixture in tests/fixtures pins that.
41
+ */
42
+ export declare const CODING_TOOL_RULES: ReadonlyArray<{
43
+ readonly tool: string;
44
+ readonly line: string;
45
+ }>;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * The coding product's prompt policy — the base system prompt and the
3
+ * project-instruction injection. Moved verbatim from index.ts (R1,
4
+ * 2026-09-23): the entry module keeps no prompt text of its own.
5
+ */
6
+ import { protectedFiles } from "./state.js";
7
+ import { isProtectedPath, protectedIdentity } from "@vincemakes/kiso-tools-node";
8
+ import { join } from "node:path";
9
+ import { readFileSync } from "node:fs";
10
+ /**
11
+ * A area: the coding-agent system prompt — ONE constant, byte-stable for the
12
+ * session's lifetime (D area). Kept under ~80 lines; no template engine.
13
+ */
14
+ /** The built-in prompt. Exported for scripts/request-surface.mjs — the
15
+ * model-side token-rent counter measures the REAL bytes, never a copy. */
16
+ export const SYSTEM_PROMPT = `You are kiso, a coding agent. You work in a workspace
17
+ directory and change code with tools. Be concise: answer in a few lines
18
+ unless the task genuinely needs more. Never claim a file was changed
19
+ unless a tool confirmed it.
20
+
21
+ What you can reach:
22
+ - The workspace: read_file, list_dir, search_text, write_file, edit_file.
23
+ - This machine and the network: shell — builds, tests, git, package
24
+ managers, curl for HTTP, system queries. A request one command can
25
+ answer is answered by running it.
26
+ - The human: ask_user, for a decision that is theirs.
27
+
28
+ Tool discipline:
29
+ - READ BEFORE YOU EDIT. For any file you are about to change, read it
30
+ first — never guess its content.
31
+ - Use edit_file for targeted changes and write_file for full rewrites.
32
+ Prefer many small edits over one large write.
33
+ - Be careful — shell has side effects and may take time.
34
+ - search_text and list_dir are cheap — locate first, then read ranges
35
+ with read_file offset/limit; never read a whole large file in one call.
36
+ - Do not re-read a file you already read unchanged, or one you changed
37
+ yourself through a confirmed edit — rely on the earlier result and
38
+ on the change you just made.
39
+ - When a tool fails, read the error and adjust; do not repeat the same
40
+ call blindly.
41
+
42
+ An authorization covers what it NAMES: when the scope is not named —
43
+ which files, which branches, whether to push — ask before acting.
44
+ Delivering, sharing or handing over changes means committing locally and
45
+ stopping; pushing, publishing or sending happens only when the human
46
+ names it.
47
+
48
+ Workflow: understand the request, find the relevant code, make the
49
+ smallest change that works, then verify with a command (tests/build).
50
+ Report what you did in one or two lines per change.`;
51
+ /** The project-instructions file names, in priority order (A area). */
52
+ const INSTRUCTION_FILES = ["AGENTS.md", "CLAUDE.md"];
53
+ /** Hard cap for injected instructions — truncate and say so. */
54
+ /**
55
+ * A area: read the FIRST present instruction file (AGENTS.md preferred) and
56
+ * return it as an injected section, or "" when none exists. Truncated at
57
+ * 8KB with an explicit note. Pure — read once per session, so the prompt
58
+ * is byte-stable for the session's lifetime.
59
+ *
60
+ * kiso never serves its own credential store to a model, and this read is
61
+ * the one that needs no model at all: a cloned repository whose AGENTS.md
62
+ * is a symlink to the store would put it in the SYSTEM PROMPT of every
63
+ * request, with no tool call and no trust prompt. A file that resolves to
64
+ * a protected one is skipped as though absent.
65
+ */
66
+ export function readProjectInstructions(cwd, protectedFiles = []) {
67
+ const id = protectedIdentity(protectedFiles);
68
+ for (const name of INSTRUCTION_FILES) {
69
+ if (isProtectedPath(join(cwd, name), id))
70
+ continue;
71
+ let text;
72
+ try {
73
+ text = readFileSync(join(cwd, name), "utf8");
74
+ }
75
+ catch {
76
+ continue; // not present — try the next
77
+ }
78
+ return `\n\n=== Project instructions (${name}) ===\n${text.length > 8 * 1024 ? text.slice(0, 8 * 1024) + `\n\n[truncated at ${8 * 1024} chars]` : text}`;
79
+ }
80
+ return "";
81
+ }
82
+ /** A area: the session's system prompt — the constant plus any project
83
+ * instructions found in the workspace. Deterministic per cwd. */
84
+ export function composeSystemPrompt(cwd, protectedFiles = []) {
85
+ const injected = readProjectInstructions(cwd, protectedFiles);
86
+ return injected === "" ? SYSTEM_PROMPT : `${SYSTEM_PROMPT}\n${injected}`;
87
+ }
88
+ /**
89
+ * The coding agent's tool-table vocabulary — one routing line per tool, in
90
+ * the table only while that tool is active (0.1.40, R-C item 1: the
91
+ * reference implementation's content in kiso's voice). ACI-6 made this the
92
+ * ONE canonical statement of the routing policy; the base prompt above no
93
+ * longer restates it.
94
+ *
95
+ * R1 (2026-09-23): moved verbatim from the runtime's compose.ts, where it
96
+ * sat as a constant and reached every host whose tools shared these names.
97
+ * The rows, their order and their position in the table are unchanged —
98
+ * the byte fixture in tests/fixtures pins that.
99
+ */
100
+ export const CODING_TOOL_RULES = [
101
+ { tool: "read_file", line: "read files with read_file, never shell cat/head/tail" },
102
+ { tool: "search_text", line: "search with search_text, never shell grep/rg" },
103
+ { tool: "list_dir", line: "list with list_dir, never ls" },
104
+ { tool: "shell", line: "shell for what the file tools cannot do: commands, git, the network, the system" },
105
+ ];
package/dist/config.d.ts CHANGED
@@ -118,6 +118,13 @@ export interface KisoConfig {
118
118
  * in the child's worktree. A model never supplies a command; it names
119
119
  * one of these. `{ "test": "npm test", "lint": "npm run lint" }`. */
120
120
  readonly checks?: Readonly<Record<string, string>>;
121
+ /** CS-1 (0.40.7): the evaluator scripts a delegated task may name —
122
+ * absolute paths, `["/abs/path/to/evaluate.sh"]`. The PARENT runs the
123
+ * one a task names with the child's worktree as its argument. A path
124
+ * the user did not list is refused: the model chooses the task, so an
125
+ * unlisted path could be an interpreter running code the child wrote.
126
+ * Same layers as `checks`. */
127
+ readonly evaluators?: readonly string[];
121
128
  }
122
129
  /** The resolved, merged config — project wins over user, both validated. */
123
130
  export interface ResolvedConfig {
@@ -145,7 +152,8 @@ export type ProfileAuth = {
145
152
  readonly source: "store" | "env" | "none";
146
153
  } | {
147
154
  readonly type: "oauth";
148
- readonly providerId: string;
155
+ readonly providerId: string; /** the access token's expiry (epoch ms) — past it, the next use renews */
156
+ readonly expires?: number;
149
157
  };
150
158
  /** Where a profile's sign-in comes from, or why it has none. The sign-in
151
159
  * plan's resolve rule: a stored credential OWNS the provider (an
package/dist/config.js CHANGED
@@ -130,6 +130,15 @@ export function parseConfig(text, source) {
130
130
  }
131
131
  out.checks = obj.checks;
132
132
  }
133
+ if (obj.evaluators !== undefined) {
134
+ if (!Array.isArray(obj.evaluators))
135
+ fail("evaluators", "expected a list of absolute paths to evaluator scripts");
136
+ for (const path of obj.evaluators) {
137
+ if (typeof path !== "string" || !path.startsWith("/"))
138
+ fail("evaluators", `expected an absolute path, got ${JSON.stringify(path)}`);
139
+ }
140
+ out.evaluators = obj.evaluators;
141
+ }
133
142
  if (obj.model !== undefined) {
134
143
  if (typeof obj.model !== "string" || obj.model === "")
135
144
  fail("model", "expected a profile name or provider/model string");
@@ -253,6 +262,8 @@ export function mergeConfigs(user, project) {
253
262
  ...(p.projectTrust !== undefined ? { projectTrust: p.projectTrust } : {}),
254
263
  // DT-1a: checks merge per name — a (trusted) project's check wins over the user's
255
264
  ...(u.checks !== undefined || p.checks !== undefined ? { checks: { ...(u.checks ?? {}), ...(p.checks ?? {}) } } : {}),
265
+ // CS-1: the evaluator lists join — either layer's script may be named
266
+ ...(u.evaluators !== undefined || p.evaluators !== undefined ? { evaluators: [...(u.evaluators ?? []), ...(p.evaluators ?? [])] } : {}),
256
267
  };
257
268
  }
258
269
  /** Where a profile's sign-in comes from, or why it has none. The sign-in
@@ -280,8 +291,14 @@ export function authForProfile(name, p) {
280
291
  if (stored !== undefined) {
281
292
  if (stored.type === "api-key")
282
293
  return { type: "api-key", apiKey: stored.key, source: "store" };
283
- if (p.kind === "openai-responses")
284
- return { type: "oauth", providerId };
294
+ if (p.kind === "openai-responses") {
295
+ // 0.40.7: a sign-in whose renewal the endpoint REFUSED is over —
296
+ // unavailable here, where /model reads it, not first at a turn
297
+ if (stored.refreshRejectedAt !== undefined) {
298
+ throw new ConfigError(`model ${name}: unavailable — the ${providerId} sign-in was refused when kiso tried to renew it: run \`kiso login ${providerId}\``);
299
+ }
300
+ return { type: "oauth", providerId, expires: stored.expires };
301
+ }
285
302
  throw new ConfigError(`model ${name}: signed in to ${providerId} with OAuth, but this profile's adapter needs an API key — run \`kiso login ${providerId}\` with a key, or \`kiso logout ${providerId}\` to use the env var ${p.apiKeyEnv ?? "(none configured)"}`);
286
303
  }
287
304
  }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * kiso-code's composition root — the product's choices (trust, config,
3
+ * credentials, the coding tools, the built-in and loaded extensions, the
4
+ * coding prompt, the mode tiers) bound into ONE AgentDefinition and handed
5
+ * to runtime.createAgent(): the one door every agent goes through.
6
+ *
7
+ * Moved verbatim from index.ts's makeAgent (R1, 2026-09-23) and renamed;
8
+ * the body is not edited beyond the rename and activeStoreDir's setter.
9
+ */
10
+ import { type LineInput } from "./state.js";
11
+ import { type ContextPolicy } from "@vincemakes/kiso-runtime";
12
+ /**
13
+ * E6: the run-start context policy, OFF unless env-armed (invalid values
14
+ * are ABSENT, never a crash — the autoCompactFromEnv convention).
15
+ * The product arming is KISO_CONTEXT_WINDOW → window − POLICY_RESERVE
16
+ * (the window rides the config as windowTokens; the runtime owns the
17
+ * arithmetic — never a fixed low absolute). KISO_POLICY_SUMMARY_TRIGGER
18
+ * survives ONLY as the legacy absolute override when no window is set
19
+ * (bench back-compat); the window wins when both are set.
20
+ * KISO_POLICY_SUMMARY_KEEP (rounds) and KISO_POLICY_SUMMARY_KEEP_TOKENS
21
+ * override the runtime defaults (KEEP_RECENT_ROUNDS = 4,
22
+ * KEEP_TOKENS_DEFAULT = 20,000) — emitted only when set.
23
+ * KISO_POLICY_SUMMARY_MAX_FAILURES overrides the (h) circuit-breaker
24
+ * default (MAX_SUMMARY_FAILURES = 3).
25
+ * KISO_POLICY_DROP=1 switches the armed mode to the crux C arm
26
+ * (mechanical drop — same trigger/keep envs); KISO_POLICY_MICROCOMPACT
27
+ * arms the session-aware override (MIN_TURNS = the no-fire guard).
28
+ */
29
+ export declare function contextPolicyFromEnv(): ContextPolicy | undefined;
30
+ export declare function createCodingAgent(sessionId: string | undefined, input?: LineInput, modelFlag?: string): Promise<import("@vincemakes/kiso-runtime").AgentRuntime>;