mini-coder 0.0.10 → 0.0.12

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/mc.js CHANGED
@@ -23,7 +23,10 @@ import * as c7 from "yoctocolors";
23
23
  class TerminalIO {
24
24
  cleanupHandlers = new Set;
25
25
  rawModeEnabled = false;
26
- abortController = new AbortController;
26
+ interruptHandler = null;
27
+ setInterruptHandler(handler) {
28
+ this.interruptHandler = handler;
29
+ }
27
30
  stdoutWrite(text) {
28
31
  process.stdout.write(text);
29
32
  }
@@ -58,8 +61,12 @@ class TerminalIO {
58
61
  process.exit(143);
59
62
  });
60
63
  process.on("SIGINT", () => {
61
- cleanup();
62
- process.exit(130);
64
+ if (this.interruptHandler) {
65
+ this.interruptHandler();
66
+ } else {
67
+ cleanup();
68
+ process.exit(130);
69
+ }
63
70
  });
64
71
  process.on("uncaughtException", (err) => {
65
72
  cleanup();
@@ -924,6 +931,8 @@ function fmtTokens(n) {
924
931
  function renderStatusBar(opts) {
925
932
  const cols = process.stdout.columns ?? 80;
926
933
  const left = [c5.cyan(opts.model)];
934
+ if (opts.thinkingEffort)
935
+ left.push(c5.dim(`\u2726 ${opts.thinkingEffort}`));
927
936
  if (opts.provider && opts.provider !== "zen")
928
937
  left.push(c5.dim(opts.provider));
929
938
  left.push(c5.dim(opts.sessionId.slice(0, 8)));
@@ -969,7 +978,7 @@ function renderError(err, context = "render") {
969
978
 
970
979
  // src/cli/output.ts
971
980
  var HOME2 = homedir3();
972
- var PACKAGE_VERSION = "0.0.10";
981
+ var PACKAGE_VERSION = "0.0.12";
973
982
  function tildePath(p) {
974
983
  return p.startsWith(HOME2) ? `~${p.slice(HOME2.length)}` : p;
975
984
  }
@@ -1340,6 +1349,87 @@ var CONTEXT_WINDOW_TABLE = [
1340
1349
  [/^glm-/, 128000],
1341
1350
  [/^qwen3-/, 131000]
1342
1351
  ];
1352
+ var REASONING_MODELS = [
1353
+ /^claude-3-5-sonnet/,
1354
+ /^claude-3-7/,
1355
+ /^claude-sonnet-4/,
1356
+ /^claude-opus-4/,
1357
+ /^o1/,
1358
+ /^o3/,
1359
+ /^o4/,
1360
+ /^gpt-5/,
1361
+ /^gemini-2\.5/,
1362
+ /^gemini-3/
1363
+ ];
1364
+ function supportsThinking(modelString) {
1365
+ const { modelId } = parseModelString(modelString);
1366
+ return REASONING_MODELS.some((p) => p.test(modelId));
1367
+ }
1368
+ var ANTHROPIC_BUDGET = {
1369
+ low: 4096,
1370
+ medium: 8192,
1371
+ high: 16384,
1372
+ xhigh: 32768
1373
+ };
1374
+ function clampEffort(effort, max) {
1375
+ const ORDER = ["low", "medium", "high", "xhigh"];
1376
+ const i = ORDER.indexOf(effort);
1377
+ const m = ORDER.indexOf(max);
1378
+ return ORDER[Math.min(i, m)];
1379
+ }
1380
+ function getThinkingProviderOptions(modelString, effort) {
1381
+ if (!supportsThinking(modelString))
1382
+ return null;
1383
+ const { provider, modelId } = parseModelString(modelString);
1384
+ if (provider === "anthropic" || provider === "zen" && modelId.startsWith("claude-")) {
1385
+ const isAdaptive = /^claude-3-7/.test(modelId) || /^claude-sonnet-4/.test(modelId) || /^claude-opus-4/.test(modelId);
1386
+ if (isAdaptive) {
1387
+ const isOpus = /^claude-opus-4/.test(modelId);
1388
+ const mapped = effort === "xhigh" ? isOpus ? "max" : "high" : effort;
1389
+ return { anthropic: { thinking: { type: "adaptive" }, effort: mapped } };
1390
+ }
1391
+ const budget = ANTHROPIC_BUDGET[effort];
1392
+ return {
1393
+ anthropic: {
1394
+ thinking: { type: "enabled", budgetTokens: budget },
1395
+ betas: ["interleaved-thinking-2025-05-14"]
1396
+ }
1397
+ };
1398
+ }
1399
+ if (provider === "openai" || provider === "zen" && (modelId.startsWith("o") || modelId.startsWith("gpt-5"))) {
1400
+ const supportsXhigh = /^gpt-5\.[2-9]/.test(modelId) || /^o4/.test(modelId);
1401
+ const clamped = supportsXhigh ? effort : clampEffort(effort, "high");
1402
+ return { openai: { reasoningEffort: clamped } };
1403
+ }
1404
+ if (provider === "google" || provider === "zen" && modelId.startsWith("gemini-")) {
1405
+ if (/^gemini-3/.test(modelId)) {
1406
+ const level = clampEffort(effort, "high");
1407
+ return {
1408
+ google: {
1409
+ thinkingConfig: {
1410
+ includeThoughts: true,
1411
+ thinkingLevel: level
1412
+ }
1413
+ }
1414
+ };
1415
+ }
1416
+ const GEMINI_BUDGET = {
1417
+ low: 4096,
1418
+ medium: 8192,
1419
+ high: 16384,
1420
+ xhigh: 24575
1421
+ };
1422
+ return {
1423
+ google: {
1424
+ thinkingConfig: {
1425
+ includeThoughts: true,
1426
+ thinkingBudget: GEMINI_BUDGET[effort]
1427
+ }
1428
+ }
1429
+ };
1430
+ }
1431
+ return null;
1432
+ }
1343
1433
  function getContextWindow(modelString) {
1344
1434
  const { modelId } = parseModelString(modelString);
1345
1435
  for (const [pattern, tokens] of CONTEXT_WINDOW_TABLE) {
@@ -1691,6 +1781,19 @@ function getPreferredModel() {
1691
1781
  function setPreferredModel(model) {
1692
1782
  setSetting("preferred_model", model);
1693
1783
  }
1784
+ function getPreferredThinkingEffort() {
1785
+ const v = getSetting("preferred_thinking_effort");
1786
+ if (v === "low" || v === "medium" || v === "high" || v === "xhigh")
1787
+ return v;
1788
+ return null;
1789
+ }
1790
+ function setPreferredThinkingEffort(effort) {
1791
+ if (effort === null) {
1792
+ getDb().run("DELETE FROM settings WHERE key = 'preferred_thinking_effort'");
1793
+ } else {
1794
+ setSetting("preferred_thinking_effort", effort);
1795
+ }
1796
+ }
1694
1797
  // src/session/db/mcp-repo.ts
1695
1798
  function listMcpServers() {
1696
1799
  return getDb().query("SELECT name, transport, url, command, args, env FROM mcp_servers ORDER BY name").all();
@@ -1779,7 +1882,15 @@ function isOpenAIGPT(modelString) {
1779
1882
  return (provider === "openai" || provider === "zen") && modelId.startsWith("gpt-");
1780
1883
  }
1781
1884
  async function* runTurn(options) {
1782
- const { model, modelString, messages, tools, systemPrompt, signal } = options;
1885
+ const {
1886
+ model,
1887
+ modelString,
1888
+ messages,
1889
+ tools,
1890
+ systemPrompt,
1891
+ signal,
1892
+ thinkingEffort
1893
+ } = options;
1783
1894
  let stepCount = 0;
1784
1895
  let warningClaimed = false;
1785
1896
  function claimWarning() {
@@ -1798,6 +1909,18 @@ async function* runTurn(options) {
1798
1909
  try {
1799
1910
  const useInstructions = systemPrompt !== undefined && isOpenAIGPT(modelString);
1800
1911
  logApiEvent("turn start", { modelString, messageCount: messages.length });
1912
+ const thinkingOpts = thinkingEffort ? getThinkingProviderOptions(modelString, thinkingEffort) : null;
1913
+ const mergedProviderOptions = {
1914
+ ...useInstructions ? { openai: { instructions: systemPrompt, store: false } } : {},
1915
+ ...thinkingOpts ?? {},
1916
+ ...useInstructions && thinkingOpts?.openai ? {
1917
+ openai: {
1918
+ instructions: systemPrompt,
1919
+ store: false,
1920
+ ...thinkingOpts.openai
1921
+ }
1922
+ } : {}
1923
+ };
1801
1924
  const streamOpts = {
1802
1925
  model,
1803
1926
  messages,
@@ -1822,14 +1945,7 @@ async function* runTurn(options) {
1822
1945
  return;
1823
1946
  },
1824
1947
  ...systemPrompt ? { system: systemPrompt } : {},
1825
- ...useInstructions ? {
1826
- providerOptions: {
1827
- openai: {
1828
- instructions: systemPrompt,
1829
- store: false
1830
- }
1831
- }
1832
- } : {},
1948
+ ...Object.keys(mergedProviderOptions).length > 0 ? { providerOptions: mergedProviderOptions } : {},
1833
1949
  ...signal ? { abortSignal: signal } : {}
1834
1950
  };
1835
1951
  const result = streamText(streamOpts);
@@ -2792,7 +2908,7 @@ function createSubagentTool(runSubagent, availableAgents, parentLabel) {
2792
2908
  When the user's message contains @<agent-name>, delegate to that agent by setting agentName to the exact agent name. Available custom agents: ${[...availableAgents.entries()].map(([name, cfg]) => `"${name}" (${cfg.description})`).join(", ")}.` : "";
2793
2909
  return {
2794
2910
  name: "subagent",
2795
- description: `Spawn a sub-agent to handle a focused subtask. Use this for parallel exploration, specialised analysis, or tasks that benefit from a fresh context window. The subagent has access to all the same tools.${agentSection}`,
2911
+ description: `Spawn a sub-agent to handle a focused subtask. Use this for parallel exploration, specialised analysis, or tasks that benefit from a fresh context window. ${agentSection}`,
2796
2912
  schema: SubagentInput,
2797
2913
  execute: async (input) => {
2798
2914
  return runSubagent(input.prompt, input.agentName, parentLabel);
@@ -2878,7 +2994,7 @@ function buildReadOnlyToolSet(opts) {
2878
2994
  }
2879
2995
 
2880
2996
  // src/agent/subagent-runner.ts
2881
- function createSubagentRunner(cwd, reporter, getCurrentModel) {
2997
+ function createSubagentRunner(cwd, reporter, getCurrentModel, getThinkingEffort) {
2882
2998
  let nextLaneId = 1;
2883
2999
  const activeLanes = new Set;
2884
3000
  const runSubagent = async (prompt, depth = 0, agentName, modelOverride, parentLabel) => {
@@ -2901,17 +3017,19 @@ function createSubagentRunner(cwd, reporter, getCurrentModel) {
2901
3017
  onHook: (tool, path, ok) => reporter.renderHook(tool, path, ok),
2902
3018
  availableAgents: allAgents,
2903
3019
  parentLabel: laneLabel
2904
- });
3020
+ }).filter((tool) => tool.name !== "subagent");
2905
3021
  const subLlm = resolveModel(model);
2906
3022
  let result = "";
2907
3023
  let inputTokens = 0;
2908
3024
  let outputTokens = 0;
3025
+ const effort = getThinkingEffort();
2909
3026
  const events = runTurn({
2910
3027
  model: subLlm,
2911
3028
  modelString: model,
2912
3029
  messages: subMessages,
2913
3030
  tools: subTools,
2914
- systemPrompt
3031
+ systemPrompt,
3032
+ ...effort ? { thinkingEffort: effort } : {}
2915
3033
  });
2916
3034
  for await (const event of events) {
2917
3035
  reporter.stopSpinner();
@@ -3368,20 +3486,50 @@ async function expandTemplate(template, args, cwd) {
3368
3486
 
3369
3487
  // src/cli/commands.ts
3370
3488
  async function handleModel(ctx, args) {
3371
- if (args) {
3372
- let modelId = args;
3373
- if (!args.includes("/")) {
3489
+ const parts = args.trim().split(/\s+/).filter(Boolean);
3490
+ if (parts.length > 0) {
3491
+ if (parts[0] === "effort") {
3492
+ const effortArg2 = parts[1] ?? "";
3493
+ if (effortArg2 === "off") {
3494
+ ctx.setThinkingEffort(null);
3495
+ writeln(`${PREFIX.success} thinking effort disabled`);
3496
+ } else if (["low", "medium", "high", "xhigh"].includes(effortArg2)) {
3497
+ ctx.setThinkingEffort(effortArg2);
3498
+ writeln(`${PREFIX.success} thinking effort \u2192 ${c10.cyan(effortArg2)}`);
3499
+ } else {
3500
+ writeln(`${PREFIX.error} usage: /model effort <low|medium|high|xhigh|off>`);
3501
+ }
3502
+ return;
3503
+ }
3504
+ const idArg = parts[0] ?? "";
3505
+ let modelId = idArg;
3506
+ if (!idArg.includes("/")) {
3374
3507
  const models2 = await fetchAvailableModels();
3375
- const match = models2.find((m) => m.id.split("/").slice(1).join("/") === args || m.id === args);
3508
+ const match = models2.find((m) => m.id.split("/").slice(1).join("/") === idArg || m.id === idArg);
3376
3509
  if (match) {
3377
3510
  modelId = match.id;
3378
3511
  } else {
3379
- writeln(`${PREFIX.error} unknown model ${c10.cyan(args)} ${c10.dim("\u2014 run /models for the full list")}`);
3512
+ writeln(`${PREFIX.error} unknown model ${c10.cyan(idArg)} ${c10.dim("\u2014 run /models for the full list")}`);
3380
3513
  return;
3381
3514
  }
3382
3515
  }
3383
3516
  ctx.setModel(modelId);
3384
- writeln(`${PREFIX.success} model \u2192 ${c10.cyan(modelId)}`);
3517
+ const effortArg = parts[1];
3518
+ if (effortArg) {
3519
+ if (effortArg === "off") {
3520
+ ctx.setThinkingEffort(null);
3521
+ writeln(`${PREFIX.success} model \u2192 ${c10.cyan(modelId)} ${c10.dim("(thinking disabled)")}`);
3522
+ } else if (["low", "medium", "high", "xhigh"].includes(effortArg)) {
3523
+ ctx.setThinkingEffort(effortArg);
3524
+ writeln(`${PREFIX.success} model \u2192 ${c10.cyan(modelId)} ${c10.dim(`(\u2726 ${effortArg})`)}`);
3525
+ } else {
3526
+ writeln(`${PREFIX.success} model \u2192 ${c10.cyan(modelId)}`);
3527
+ writeln(`${PREFIX.error} unknown effort level ${c10.cyan(effortArg)} (use low, medium, high, xhigh, off)`);
3528
+ }
3529
+ } else {
3530
+ const e = ctx.thinkingEffort ? c10.dim(` (\u2726 ${ctx.thinkingEffort})`) : "";
3531
+ writeln(`${PREFIX.success} model \u2192 ${c10.cyan(modelId)}${e}`);
3532
+ }
3385
3533
  return;
3386
3534
  }
3387
3535
  writeln(`${c10.dim(" fetching models\u2026")}`);
@@ -3408,13 +3556,15 @@ async function handleModel(ctx, args) {
3408
3556
  const isCurrent = ctx.currentModel === m.id;
3409
3557
  const freeTag = m.free ? c10.green(" free") : "";
3410
3558
  const ctxTag = m.context ? c10.dim(` ${Math.round(m.context / 1000)}k`) : "";
3559
+ const effortTag = isCurrent && ctx.thinkingEffort ? c10.dim(` \u2726 ${ctx.thinkingEffort}`) : "";
3411
3560
  const cur = isCurrent ? c10.cyan(" \u25C0") : "";
3412
- writeln(` ${c10.dim("\xB7")} ${m.displayName}${freeTag}${ctxTag}${cur}`);
3561
+ writeln(` ${c10.dim("\xB7")} ${m.displayName}${freeTag}${ctxTag}${cur}${effortTag}`);
3413
3562
  writeln(` ${c10.dim(m.id)}`);
3414
3563
  }
3415
3564
  }
3416
3565
  writeln();
3417
3566
  writeln(c10.dim(" /model <id> to switch \xB7 e.g. /model zen/claude-sonnet-4-6"));
3567
+ writeln(c10.dim(" /model effort <low|medium|high|xhigh|off> to set thinking effort"));
3418
3568
  }
3419
3569
  function handlePlan(ctx) {
3420
3570
  ctx.setPlanMode(!ctx.planMode);
@@ -3803,20 +3953,25 @@ async function readKey(reader) {
3803
3953
  function watchForInterrupt(abortController) {
3804
3954
  if (!process.stdin.isTTY)
3805
3955
  return () => {};
3956
+ const onInterrupt = () => {
3957
+ cleanup();
3958
+ abortController.abort();
3959
+ };
3806
3960
  const onData = (chunk) => {
3807
3961
  for (const byte of chunk) {
3808
3962
  if (byte === 3) {
3809
- cleanup();
3810
- abortController.abort();
3963
+ onInterrupt();
3811
3964
  return;
3812
3965
  }
3813
3966
  }
3814
3967
  };
3815
3968
  const cleanup = () => {
3816
3969
  process.stdin.removeListener("data", onData);
3970
+ terminal.setInterruptHandler(null);
3817
3971
  process.stdin.setRawMode(false);
3818
3972
  process.stdin.pause();
3819
3973
  };
3974
+ terminal.setInterruptHandler(onInterrupt);
3820
3975
  process.stdin.setRawMode(true);
3821
3976
  process.stdin.resume();
3822
3977
  process.stdin.on("data", onData);
@@ -4276,6 +4431,7 @@ class SessionRunner {
4276
4431
  tools;
4277
4432
  mcpTools;
4278
4433
  currentModel;
4434
+ currentThinkingEffort;
4279
4435
  session;
4280
4436
  coreHistory;
4281
4437
  turnIndex = 1;
@@ -4291,6 +4447,7 @@ class SessionRunner {
4291
4447
  this.tools = opts.tools;
4292
4448
  this.mcpTools = opts.mcpTools;
4293
4449
  this.currentModel = opts.initialModel;
4450
+ this.currentThinkingEffort = opts.initialThinkingEffort;
4294
4451
  this.initSession(opts.sessionId);
4295
4452
  }
4296
4453
  initSession(sessionId) {
@@ -4373,7 +4530,8 @@ class SessionRunner {
4373
4530
  messages: this.coreHistory,
4374
4531
  tools: this.planMode ? [...buildReadOnlyToolSet({ cwd: this.cwd }), ...this.mcpTools] : this.tools,
4375
4532
  systemPrompt,
4376
- signal: abortController.signal
4533
+ signal: abortController.signal,
4534
+ ...this.currentThinkingEffort ? { thinkingEffort: this.currentThinkingEffort } : {}
4377
4535
  });
4378
4536
  const { inputTokens, outputTokens, contextTokens, newMessages } = await this.reporter.renderTurn(events);
4379
4537
  if (newMessages.length > 0) {
@@ -4419,7 +4577,8 @@ class SessionRunner {
4419
4577
  async function runAgent(opts) {
4420
4578
  const cwd = opts.cwd;
4421
4579
  let currentModel = opts.model;
4422
- const runSubagent = createSubagentRunner(cwd, opts.reporter, () => currentModel);
4580
+ let currentThinkingEffort = opts.initialThinkingEffort;
4581
+ const runSubagent = createSubagentRunner(cwd, opts.reporter, () => currentModel, () => currentThinkingEffort);
4423
4582
  const agents = loadAgents(cwd);
4424
4583
  const tools = buildToolSet({
4425
4584
  cwd,
@@ -4460,6 +4619,7 @@ async function runAgent(opts) {
4460
4619
  tools,
4461
4620
  mcpTools,
4462
4621
  initialModel: currentModel,
4622
+ initialThinkingEffort: opts.initialThinkingEffort,
4463
4623
  sessionId: opts.sessionId
4464
4624
  });
4465
4625
  const cmdCtx = {
@@ -4472,6 +4632,14 @@ async function runAgent(opts) {
4472
4632
  setPreferredModel(m);
4473
4633
  currentModel = m;
4474
4634
  },
4635
+ get thinkingEffort() {
4636
+ return runner.currentThinkingEffort;
4637
+ },
4638
+ setThinkingEffort: (e) => {
4639
+ runner.currentThinkingEffort = e;
4640
+ setPreferredThinkingEffort(e);
4641
+ currentThinkingEffort = e;
4642
+ },
4475
4643
  get planMode() {
4476
4644
  return runner.planMode;
4477
4645
  },
@@ -4515,7 +4683,8 @@ async function runAgent(opts) {
4515
4683
  outputTokens: runner.totalOut,
4516
4684
  contextTokens: runner.lastContextTokens,
4517
4685
  contextWindow: getContextWindow(runner.currentModel) ?? 0,
4518
- ralphMode: runner.ralphMode
4686
+ ralphMode: runner.ralphMode,
4687
+ thinkingEffort: runner.currentThinkingEffort
4519
4688
  });
4520
4689
  }
4521
4690
  if (opts.initialPrompt) {
@@ -4691,6 +4860,7 @@ async function main() {
4691
4860
  const agentOpts = {
4692
4861
  model,
4693
4862
  cwd: args.cwd,
4863
+ initialThinkingEffort: getPreferredThinkingEffort(),
4694
4864
  reporter: new CliReporter
4695
4865
  };
4696
4866
  if (sessionId)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mini-coder",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "description": "A small, fast CLI coding agent",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
package/codex-lazy-fix.md DELETED
@@ -1,76 +0,0 @@
1
- # Codex Autonomy Issues & Fix Analysis
2
-
3
- ## Behaviours
4
- When using `zen/gpt-5.3-codex` as the agent, the model consistently exhibits "lazy" or permission-seeking behaviour. Specifically:
5
- 1. **Initial Compliance**: It starts by reading files or globbing the directory.
6
- 2. **Immediate Stall**: Instead of executing edits or implementing the plan, it outputs a multi-paragraph text explaining what it *plans* to do and ends the turn.
7
- 3. **Permission Seeking**: It explicitly asks the user for permission (e.g., "Reply **'proceed'** and I'll start implementing batch 1").
8
- 4. **Ralph Mode Incompatibility**: In `/ralph` mode, the agent loops continuously. Because it restarts with a fresh context on each loop and stalls after gathering context, it never actually writes any files. It just loops through the same read-and-plan phase until it hits the max iteration limit.
9
- 5. **Model Differences**: Both Claude and Gemini models do not exhibit this behaviour. They are not subjected to the same conversational RLHF that pushes the model to ask the user to double check its work.
10
-
11
- ## Root Cause Analysis
12
- An analysis of both OpenAI's open-source `codex-rs` client and `opencode` source code reveals that Codex models (like `gpt-5.3-codex`) are highly RLHF-tuned for safety and collaborative pair-programming. By default, the model prefers to break tasks into chunks and explicitly ask for sign-off.
13
-
14
- To override this, the model requires three things which `mini-coder` was failing to provide correctly:
15
-
16
- ### 1. Dual-Anchored System Prompts (`system` + `instructions`)
17
- `mini-coder` implemented a check `useInstructions` that placed the system prompt into the `instructions` field of the `/v1/responses` API payload. However, doing so stripped the `system` role message from the conversation context (`input` array).
18
-
19
- By looking at `opencode` and `codex-rs`, they both ensure that the context array *also* contains the system prompt:
20
- - `opencode` maps its environment variables and system instructions to `role: "system"` (or `role: "developer"`) inside `input.messages`, **while also** passing behavioral instructions to the `instructions` field in the API payload.
21
- - `codex-rs` directly injects `role: "developer"` into the message list (as seen in `codex-rs/core/src/compact.rs` and their memory tracing implementations).
22
-
23
- Without the `system` / `developer` message anchored at the start of the `input` array, the AI SDK and the model deprioritized the standalone `instructions` field, allowing the model's base permission-seeking behaviors to take over.
24
-
25
- ### 2. Explicit "Do Not Ask" Directives
26
- Both `opencode` and `codex-rs` employ heavy anti-permission prompts.
27
- - **Opencode** (`session/prompt/codex_header.txt`):
28
- > "- Default: do the work without asking questions... Never ask permission questions like 'Should I proceed?' or 'Do you want me to run tests?'; proceed with the most reasonable option and mention what you did."
29
- - **Codex-RS** (`core/templates/model_instructions/gpt-5.2-codex_instructions_template.md`):
30
- > "Persist until the task is fully handled end-to-end within the current turn whenever feasible: do not stop at analysis or partial fixes; carry changes through implementation, verification, and a clear explanation of outcomes unless the user explicitly pauses or redirects you."
31
-
32
- `mini-coder` introduced `CODEX_AUTONOMY` in a previous commit, but because of Issue #1, it was never adequately anchored in the `input` array.
33
-
34
- ## Evidence & Tests
35
- We introduced a fetch wrapper interceptor in `src/llm-api/providers.ts` that logs the full outbound API requests to `~/.config/mini-coder/api.log`.
36
-
37
- A test script `test-turn.ts` running a dummy turn showed the exact payload generated by the AI SDK before our fix:
38
- ```json
39
- "body": {
40
- "model": "gpt-5.3-codex",
41
- "input": [
42
- {
43
- "role": "user",
44
- "content": [
45
- { "type": "input_text", "text": "hello" }
46
- ]
47
- }
48
- ],
49
- "store": false,
50
- "instructions": "You are a test agent.",
51
- ...
52
- ```
53
- ```json
54
- "body": {
55
- "model": "gpt-5.3-codex",
56
- "input": [
57
- {
58
- "role": "developer",
59
- "content": "You are mini-coder, a small and fast CLI coding agent... [CODEX_AUTONOMY directives]"
60
- },
61
- {
62
- "role": "user",
63
- "content": [
64
- { "type": "input_text", "text": "hello" }
65
- ]
66
- }
67
- ],
68
- "instructions": "You are mini-coder, a small and fast CLI coding agent... [CODEX_AUTONOMY directives]"
69
- }
70
- ```
71
- This perfectly mirrors the behavior seen in `opencode` and `codex-rs`.
72
-
73
- ## Actions Taken
74
- 1. Added an `api.log` request interceptor in `providers.ts` to capture and inspect the exact JSON payloads sent to the OpenAI/AI SDK endpoints.
75
- 2. Cloned and analyzed both `opencode` and `codex` repos to observe how they communicate with `gpt-5.*` codex endpoints.
76
- 3. Updated `src/llm-api/turn.ts` so `system: systemPrompt` is *always* passed to the AI SDK, guaranteeing a `developer` message anchors the `input` array, even when `instructions` is also used.
@@ -1,169 +0,0 @@
1
- # Code Health Remediation Plan
2
-
3
- ## Goal
4
- Address maintainability and reliability issues identified in `code-health.md` with low-risk, incremental refactors that keep behavior stable.
5
-
6
- ## Constraints
7
- - Keep `mini-coder-idea.md` and `README.md` unchanged.
8
- - Prefer small PR-sized changes with passing tests after each step.
9
- - Preserve current CLI behavior while improving structure.
10
-
11
- ## Workstreams
12
-
13
- ### 1) Decompose `src/agent/agent.ts` (High)
14
- **Outcome:** `runAgent` remains orchestration entrypoint; responsibilities split into focused modules.
15
-
16
- **Steps:**
17
- 1. Add `src/agent/reporter.ts` interface (narrow surface for output/status/tool events).
18
- 2. Extract session lifecycle + turn loop into `src/agent/session-runner.ts`.
19
- 3. Extract subagent execution into `src/agent/subagent-runner.ts`.
20
- 4. Extract snapshot/undo helpers into `src/agent/undo-snapshot.ts`.
21
- 5. Extract user input processing into `src/agent/input-loop.ts`.
22
- 6. Keep `agent.ts` as composition/wiring file only.
23
-
24
- **Checks:**
25
- - Add/adjust unit tests around orchestration boundaries.
26
- - Ensure no behavior regressions in interrupts, resume, and tool-call flows.
27
-
28
- ---
29
-
30
- ### 2) Decompose `src/cli/output.ts` (High)
31
- **Outcome:** Rendering responsibilities isolated and testable.
32
-
33
- **Target modules:**
34
- - `src/cli/spinner.ts`
35
- - `src/cli/tool-render.ts`
36
- - `src/cli/stream-render.ts`
37
- - `src/cli/status-bar.ts`
38
- - `src/cli/error-render.ts`
39
- - `src/cli/output.ts` as facade
40
-
41
- **Steps:**
42
- 1. Extract pure formatting helpers first (no IO).
43
- 2. Extract spinner lifecycle module.
44
- 3. Extract stream queue/tick/flush behavior.
45
- 4. Keep compatibility exports in `output.ts` to avoid broad callsite churn.
46
-
47
- **Checks:**
48
- - Add focused tests for formatting + stream behavior.
49
- - Verify terminal rendering remains stable manually.
50
-
51
- ---
52
-
53
- ### 3) Introduce `TerminalIO` abstraction (Medium)
54
- **Outcome:** Centralized process/TTY interactions and signal lifecycle.
55
-
56
- **Steps:**
57
- 1. Create `src/cli/terminal-io.ts` with methods for stdout/stderr writes, raw mode, signal subscriptions.
58
- 2. Replace direct `process.*` use in output/input stack with injected `TerminalIO`.
59
- 3. Centralize signal registration/unregistration in one lifecycle owner.
60
-
61
- **Checks:**
62
- - Add unit tests for signal registration cleanup semantics.
63
- - Confirm no stuck raw-mode edge cases.
64
-
65
- ---
66
-
67
- ### 4) Split DB layer by domain (Medium)
68
- **Outcome:** Reduced blast radius and clearer data ownership.
69
-
70
- **Target modules:**
71
- - `src/session/db/connection.ts`
72
- - `src/session/db/session-repo.ts`
73
- - `src/session/db/message-repo.ts`
74
- - `src/session/db/settings-repo.ts`
75
- - `src/session/db/mcp-repo.ts`
76
- - `src/session/db/snapshot-repo.ts`
77
- - `src/session/db/index.ts` (facade exports)
78
-
79
- **Steps:**
80
- 1. Move code without behavior changes.
81
- 2. Keep SQL and schema unchanged initially.
82
- 3. Replace direct `JSON.parse` in message loading with guarded parser:
83
- - skip malformed rows
84
- - emit diagnostic via logger/reporter
85
-
86
- **Checks:**
87
- - Add tests for malformed payload handling.
88
- - Validate existing DB tests still pass.
89
-
90
- ---
91
-
92
- ### 5) Shared markdown config loader (Medium)
93
- **Outcome:** Remove duplication across agents/skills/custom-commands.
94
-
95
- **Steps:**
96
- 1. Create `src/cli/load-markdown-configs.ts` with parameterized layout strategy.
97
- 2. Migrate:
98
- - `src/cli/agents.ts`
99
- - `src/cli/skills.ts`
100
- - `src/cli/custom-commands.ts`
101
- 3. Keep precedence rules identical (built-in/user/project).
102
- 4. Preserve existing frontmatter semantics.
103
-
104
- **Checks:**
105
- - Reuse/expand existing loader tests to cover parity.
106
-
107
- ---
108
-
109
- ### 6) Runtime/UI decoupling via reporter boundary (Medium)
110
- **Outcome:** Core runtime no longer depends directly on terminal rendering.
111
-
112
- **Steps:**
113
- 1. Define domain events or reporter interface in `src/agent/reporter.ts`.
114
- 2. Implement CLI reporter adapter in `src/cli/output-reporter.ts`.
115
- 3. Replace direct output calls in agent runtime with reporter calls.
116
-
117
- **Checks:**
118
- - Add tests using test reporter to assert emitted events.
119
-
120
- ---
121
-
122
- ### 7) Error observability and silent catches (Medium)
123
- **Outcome:** Non-fatal failures become diagnosable without crashing.
124
-
125
- **Steps:**
126
- 1. Find empty/broad catches in agent/output/loaders.
127
- 2. Add debug-level diagnostics with contextual metadata.
128
- 3. Keep user-facing behavior unchanged unless critical.
129
-
130
- **Checks:**
131
- - Validate noisy paths are still quiet at normal verbosity.
132
-
133
- ---
134
-
135
- ### 8) Startup FS sync usage (Low/Deferred)
136
- **Outcome:** Optional responsiveness improvement if startup cost grows.
137
-
138
- **Steps:**
139
- 1. Measure startup and config-loading time first.
140
- 2. If needed, move high-volume file scanning to async or cache results with invalidation.
141
-
142
- ---
143
-
144
- ### 9) Test hygiene cleanup (Low)
145
- **Outcome:** Cleaner CI output.
146
-
147
- **Steps:**
148
- 1. Remove `console.log` skip notices in `src/tools/shell.test.ts`.
149
- 2. Use test-framework-native skip annotations/helpers.
150
-
151
- ---
152
-
153
- ## Execution Order (recommended)
154
- 1. Reporter interface (foundation for later decoupling).
155
- 2. `agent.ts` decomposition.
156
- 3. `output.ts` decomposition.
157
- 4. Shared config loader extraction.
158
- 5. DB module split + safe JSON parsing.
159
- 6. TerminalIO + centralized signals.
160
- 7. Silent catch diagnostics.
161
- 8. Test hygiene and any deferred FS optimization.
162
-
163
- ## Definition of Done
164
- - `bun run typecheck && bun run format && bun run lint && bun test` passes.
165
- - No behavior regressions in interactive CLI flows.
166
- - `agent.ts` and `output.ts` materially reduced in size/responsibility.
167
- - Config loader duplication removed.
168
- - Message loading resilient to malformed JSON rows.
169
- - New abstractions documented in code comments where non-obvious.