thincoder 0.12.50 → 0.12.52

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +64 -3
  2. package/README.md +2 -2
  3. package/package.json +4 -3
  4. package/src/acp/bridge.mjs +5 -0
  5. package/src/agent/dispatch.mjs +19 -7
  6. package/src/agent/helpers.mjs +13 -1
  7. package/src/agent/record-results.mjs +130 -0
  8. package/src/agent/setup.mjs +4 -7
  9. package/src/agent/spawn-child.mjs +159 -0
  10. package/src/agent-tools/consult.mjs +94 -73
  11. package/src/agent-tools/escalate.mjs +53 -62
  12. package/src/agent-tools/skill.mjs +1 -1
  13. package/src/agent-tools/subagent.mjs +39 -38
  14. package/src/agent-tools/task.mjs +0 -2
  15. package/src/agent-tools/verify.mjs +0 -1
  16. package/src/agent.mjs +27 -112
  17. package/src/config.mjs +8 -103
  18. package/src/generate-title.mjs +30 -1
  19. package/src/model-specs.mjs +108 -0
  20. package/src/prompts/advisor-round1.md +5 -6
  21. package/src/prompts/advisor-round2.md +3 -4
  22. package/src/prompts/advisor-round3.md +3 -4
  23. package/src/prompts/eng-coder.md +9 -0
  24. package/src/prompts/engineering.md +61 -9
  25. package/src/prompts/system.md +2 -2
  26. package/src/provider/core.mjs +5 -71
  27. package/src/provider/normalize.mjs +81 -0
  28. package/src/session.mjs +40 -1
  29. package/src/tools/git.mjs +3 -3
  30. package/src/tools/shared.mjs +1 -0
  31. package/src/tools/system.mjs +3 -1
  32. package/src/tui/agent-turn.mjs +37 -364
  33. package/src/tui/clipboard.mjs +3 -1
  34. package/src/tui/dims.mjs +47 -0
  35. package/src/tui/fold-block.mjs +208 -0
  36. package/src/tui/index.mjs +33 -17
  37. package/src/tui/key-handler-search.mjs +1 -1
  38. package/src/tui/key-handler.mjs +10 -6
  39. package/src/tui/layout.mjs +21 -20
  40. package/src/tui/mouse.mjs +9 -6
  41. package/src/tui/pickers.mjs +1 -1
  42. package/src/tui/render-conversation.mjs +367 -113
  43. package/src/tui/render-frame.mjs +16 -90
  44. package/src/tui/render-loop.mjs +12 -8
  45. package/src/tui/render.mjs +16 -0
  46. package/src/tui/startup.mjs +66 -13
  47. package/src/tui/subagent-blocks.mjs +327 -0
  48. package/src/tui/tool-args.mjs +67 -0
  49. package/src/tui/tool-events.mjs +459 -0
package/src/agent.mjs CHANGED
@@ -3,23 +3,23 @@
3
3
  * LLM ↔ tool-call loop, until the task is done.
4
4
  */
5
5
  import { chat } from "./provider/index.mjs"
6
- import { estimateText } from "./provider/rate.mjs"
7
6
  import { compressIfNeeded, compressFallback, COMPRESS_FAILURE_LIMIT, pushReal, summarizeRunExplorations } from "./context.mjs"
8
7
  import { specForModel } from "./config.mjs"
9
8
  import { readFileSync } from "node:fs"
10
9
  import { join, dirname } from "node:path"
11
10
  import { fileURLToPath } from "node:url"
12
11
  import { executeToolCalls } from "./agent/dispatch.mjs"
12
+ import { recordToolResults } from "./agent/record-results.mjs"
13
13
  import { prepareRun } from "./agent/setup.mjs"
14
- import { injectPostTurn, STALL_WINDOW_SIZE, STALL_THRESHOLD, GOAL_BUDGET_WARN_RATIO } from "./agent/post-turn.mjs"
14
+ import { injectPostTurn } from "./agent/post-turn.mjs"
15
15
  import { handleCompletion } from "./agent/completion.mjs"
16
16
  import { cleanupConsultSessions } from "./agent-tools/consult.mjs"
17
17
  import {
18
- escapeXml, tryCanonicalize, repairHistory, listWorkDir,
18
+ escapeXml, repairHistory, listWorkDir, ensureAutoReminder,
19
19
  readonlyToolNames, collectGitContext, loadProjectInstructions,
20
- ContinueError, FILE_MUTATORS,
20
+ ContinueError,
21
21
  DEFAULT_MAX_TURNS, DEFAULT_SUBAGENT_TURNS,
22
- MIN_REPORT_CHARS, REPORT_CONTINUATION, OUTLINE_INJECT_PREFIX,
22
+ MIN_REPORT_CHARS, REPORT_CONTINUATION,
23
23
  } from "./agent/helpers.mjs"
24
24
 
25
25
  // Prompt files (byte-stable, loaded once)
@@ -42,13 +42,11 @@ export const CONSULT_BASE = _CONSULT_BASE
42
42
  // exported for consumption by agent-tools.mjs
43
43
  export {
44
44
  ContinueError,
45
- repairHistory, listWorkDir, loadProjectInstructions,
45
+ listWorkDir, loadProjectInstructions,
46
46
  readonlyToolNames, collectGitContext, escapeXml,
47
47
  MIN_REPORT_CHARS, REPORT_CONTINUATION, DEFAULT_SUBAGENT_TURNS,
48
48
  }
49
49
 
50
- let _reindexFile = null
51
- const AUTO_REMINDER = "[System reminder: AUTO mode is active — all tool calls are automatically approved without asking.]"
52
50
 
53
51
  // Engineering mode reminder — shared with eng.mjs tool
54
52
  export const ENG_ON_REMINDER =
@@ -103,7 +101,7 @@ export function createAgent({
103
101
  _emptyRetries: 0, // empty-response retry budget (per-run; reset on a fresh user turn)
104
102
  _runStartHistoryLen: 0, // machine-line length at the start of the current run — end-of-run exploration distillation slices from here
105
103
  _pendingDistill: null, // in-flight end-of-run exploration distillation (SEND-STALL-DISTILL §2.1) — awaited at next run start / TUI exit flush
106
- _currentTurn: 0, _maxTurns: 100, // turn counter for status bar display
104
+ _currentTurn: 0, _maxTurns: DEFAULT_MAX_TURNS, // turn counter for status bar display
107
105
  }
108
106
  }
109
107
 
@@ -170,6 +168,13 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
170
168
  // Update turn counter for status bar display
171
169
  agent._currentTurn = turn + 1
172
170
  agent._maxTurns = maxTurns
171
+ // D2 (AGENT-LOOP.md §7.2): depth>0 children emit a ⟦ev⟧turn progress token on every
172
+ // turn — a single emit point covering all three spawn tools (natural heartbeat for the
173
+ // TUI subagent block header: "turn N/max"). phase=llm (tool/done progress rides the
174
+ // existing onToolCall/onToolResult prefix relay — no token for those).
175
+ if (depth > 0 && callbacks.onToken) {
176
+ callbacks.onToken(`⟦ev⟧turn\x1e${turn + 1}\x1e${maxTurns}\x1ellm\x1e`)
177
+ }
173
178
 
174
179
  const lastRole = agent.history.at(-1)?.role
175
180
  if (lastRole === "user" || lastRole === "tool") {
@@ -179,9 +184,7 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
179
184
  agent._planReminderAtLen = 0 // After compression history shrinks, reset cadence so reminders resume
180
185
  recentCallSigs.length = 0 // After compression history is rebuilt, reset stall detection counter
181
186
  callbacks.onCompress?.()
182
- if (agent.autoApprove && !agent.history.some((m) => m.content === AUTO_REMINDER)) {
183
- agent.history.push({ role: "user", content: AUTO_REMINDER })
184
- }
187
+ ensureAutoReminder(agent)
185
188
  }
186
189
  } catch (compressError) {
187
190
  // AbortError must not be swallowed: user cancellation must propagate
@@ -354,6 +357,14 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
354
357
  // Ctrl+I interrupt during tool execution: skip committing partial results —
355
358
  // the tool failure messages would mislead the model. Inject the interrupt and retry.
356
359
  if (signal?.reason?.interrupt) {
360
+ // The assistant tool_calls were already committed above (L347) — a strict
361
+ // provider 400s on dangling tool_calls, so synthesize placeholder tool
362
+ // results BEFORE the interrupt message (tool result must immediately
363
+ // follow its assistant tool_calls). The retry turn then sees a clean,
364
+ // pairable history (consult P1, 2026-08-30).
365
+ for (const tc of response.toolCalls) {
366
+ agent.history.push({ role: "tool", tool_call_id: tc.id, content: "[Tool execution interrupted — results discarded]" })
367
+ }
357
368
  agent.history.push({
358
369
  role: "user",
359
370
  content: `[User interrupt: ${signal.reason.message}]`,
@@ -366,106 +377,10 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
366
377
  guardPushbacks = 0
367
378
  advisorPushbacks = 0
368
379
 
369
- // Multimodal user messages (injected images / not-injected reminders) must NOT be pushed
370
- // between tool results of parallel calls strict providers (DeepSeek) 400 when a tool
371
- // message does not immediately follow its assistant tool_calls. Defer to after the loop.
372
- // real: image injections are real messages (pushReal _fullHistory); reminders stay machine-only.
373
- const deferredUserMsgs = []
374
-
375
- for (const { toolCall, result, ok } of results) {
376
- const tool = toolByName.get(toolCall.name)
377
- // Multimodal tools return JSON { text, images } — inject as multimodal user message
378
- if (tool?.multimodal && ok) {
379
- try {
380
- const parsed = JSON.parse(result)
381
- if (parsed.images?.length) {
382
- // tool message first — closes the tool_call pairing (OpenAI API requires tool result immediately after assistant with tool_calls)
383
- pushReal(agent, { role: "tool", tool_call_id: toolCall.id, name: toolCall.name, content: parsed.text })
384
- if (specForModel(agent.provider.model).multimodal) {
385
- // then inject multimodal user message with base64 images for the model to actually "see" them on the next turn
386
- deferredUserMsgs.push({
387
- real: true,
388
- msg: {
389
- role: "user",
390
- content: [{ type: "text", text: parsed.text }, ...parsed.images],
391
- },
392
- })
393
- } else {
394
- // Non-vision model: image parts must never enter history — text-only APIs 400 on them on EVERY
395
- // subsequent request, poisoning the conversation. (read_image itself already refuses; this is defense-in-depth.)
396
- deferredUserMsgs.push({
397
- real: false,
398
- msg: {
399
- role: "user",
400
- content: `[System reminder: the image returned by ${toolCall.name} was NOT injected — model ${agent.provider.model} does not support image input. Do not call ${toolCall.name} again under this provider; verify visual output programmatically instead.]`,
401
- },
402
- })
403
- }
404
- continue
405
- }
406
- } catch { /* Parse failure doesn't affect normal tool messages */ }
407
- }
408
- pushReal(agent, { role: "tool", tool_call_id: toolCall.id, name: toolCall.name, content: result })
409
- if (tool && ok) {
410
- if (FILE_MUTATORS.has(toolCall.name)) {
411
- // Direct file edit — code was changed. The prior advisor review and
412
- // verify are stale: a review that ran before the edit no longer
413
- // covers the current file state.
414
- agent._mutatedThisRun = true
415
- agent._calledAdvisorThisRun = false
416
- agent._verifiedThisRun = false
417
- agent._verifyPassed = undefined
418
- } else if (!tool.readonly && !tool.sideEffectExempt) {
419
- // Non-mutating side-effect tools (bash, git): do NOT invalidate the
420
- // advisor review — a review is triggered by CODE MUTATIONS only
421
- // (user decision 2026-08-08: the guard rule is "review after code
422
- // changes", not "review after any environment change"; bash is
423
- // barred from writing files, so it cannot change the reviewed code).
424
- // Verify IS invalidated: its state snapshot (git diff, file list)
425
- // may be stale after git/shell operations.
426
- if (agent._verifiedThisRun) {
427
- agent._verifiedThisRun = false
428
- agent._verifyPassed = undefined
429
- }
430
- }
431
- if (toolCall.name === "verify") agent._verifiedThisRun = true
432
- if (toolCall.name === "advisor") {
433
- agent._calledAdvisorThisRun = true
434
- // All advisor calls (code and design) share the 5-round convergence
435
- // budget — each advances _advisorRound toward MAX_ADVISOR_ROUNDS.
436
- // Always advance the round — the convergence protocol cares about
437
- // how many reviews have run (round 1→2→3→4→5), not how many succeeded.
438
- // A failed/interrupted review is still a review attempt and should use
439
- // the next round's prompt on retry.
440
- agent._advisorRound++
441
- }
442
- if (FILE_MUTATORS.has(toolCall.name)) {
443
- const args = JSON.parse(toolCall.arguments)
444
- const paths = tool.touchedPaths ? tool.touchedPaths(args) : [args.path]
445
- for (const p of paths) {
446
- const abs = join(agent.cwd, p)
447
- if (!agent._touchedFiles.includes(abs)) agent._touchedFiles.push(abs)
448
- if (agent.memory) {
449
- // Fire-and-forget: don't block the agent loop on indexing.
450
- // Reuses a single cached import; errors surface as pending reminders on next turn.
451
- if (!_reindexFile) {
452
- const mod = await import("./memory.mjs")
453
- _reindexFile = mod.reindexFile
454
- }
455
- _reindexFile(agent.memory, agent.cwd, abs).catch((e) => {
456
- agent._pendingReminders.push(`[System reminder: background indexing failed for ${toolCall.name} on ${abs}: ${e.message}. This does not affect your work — the code index will catch up on next reindex.]`)
457
- })
458
- }
459
- }
460
- }
461
- }
462
- }
463
-
464
- // All tool results committed — now safe to inject deferred multimodal user messages
465
- for (const { real, msg } of deferredUserMsgs) {
466
- if (real) pushReal(agent, msg)
467
- else agent.history.push(msg)
468
- }
380
+ // Commit tool results (pairing, multimodal deferral, mutation accounting,
381
+ // touched files, reindex) split into record-results.mjs (consult P2,
382
+ // 2026-08-30).
383
+ await recordToolResults(agent, toolByName, results)
469
384
 
470
385
  injectPostTurn(agent, results, recentCallSigs, callbacks, turn)
471
386
  }
package/src/config.mjs CHANGED
@@ -75,110 +75,18 @@ export const DEFAULTS = {
75
75
  },
76
76
  }
77
77
 
78
- /**
79
- * Known model capability spec table (prefix match, longer first).
80
- * Used for compaction threshold derivation, continuation protocol selection, and capability-aware optimization.
81
- *
82
- * context: context window (tokens)
83
- * maxOutput: max output tokens (defaults to context)
84
- * thinking: whether thinking/reasoning mode is supported
85
- * partialMode: Kimi/Qwen Partial Mode truncation continuation (assistant message with partial:true)
86
- * prefixMode: DeepSeek Prefix Completion truncation continuation (uses /beta endpoint, with prefix:true)
87
- * multimodal: whether multimodal (image/vision input supported)
88
- * cacheMode: context caching mode: "auto"=automatic / "prompt"=needs explicit / "none"=unsupported
89
- * thinkApi: thinking API type: "type"=thinking.type field / "effort"=reasoning_effort field
90
- * thinkEnabledValue: when thinkApi is "type", the value used to enable thinking (default "enabled"; MiniMax uses "adaptive")
91
- * reasoningEcho: reasoning_content cross-turn echo strategy: "required"=must echo (error if missing) / "optional"=echo optional (default: don't echo)
92
- * reasoningEffortEnum: valid reasoning_effort enum values (if undeclared, no validation — passed through as-is)
93
- * tempRange: valid temperature range [min, max] (if undeclared, no clamping)
94
- */
95
- const MODEL_SPECS = [
96
- // DeepSeek V4 series
97
- ["deepseek-v4-pro", { context: 1_000_000, maxOutput: 384_000, thinking: true, prefixMode: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 2] }],
98
- ["deepseek-v4-flash", { context: 1_000_000, maxOutput: 384_000, thinking: true, prefixMode: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 2] }],
99
- // DeepSeek V4 Flash Vision (experimental) — image input on top of the full V4-Flash stack
100
- ["deepseek-v4-flash-vision-exp", { context: 1_000_000, maxOutput: 384_000, thinking: true, prefixMode: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 2], multimodal: true }],
101
- // Kimi series
102
- ["kimi-k3", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, cacheMode: "auto", thinkApi: "effort", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"] }],
103
- // Qwen router prefixes model IDs with provider namespace: kimi/kimi-k3 → kimi-k3 (IK7K4V)
104
- ["kimi/kimi-k3", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, cacheMode: "auto", thinkApi: "effort", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"] }],
105
- // Kimi For Coding endpoint uses the short model ID "k3" (same specs as kimi-k3) — IK5VGJ
106
- ["k3", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, cacheMode: "auto", thinkApi: "effort", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"] }],
107
- // GLM series
108
- // GLM-5.3: thinking always-on (no "disabled"); effort converges to low/high/max — NOT the
109
- // 7-level glm-5.2 enum (verified vs docs.bigmodel.cn GLM-5.3 page, 2026-08)
110
- ["glm-5.3", { context: 1_000_000, maxOutput: 128_000, thinking: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 1], noUsageStream: true }],
111
- ["glm-5.3-flash", { context: 1_000_000, maxOutput: 128_000, thinking: true, multimodal: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 1], noUsageStream: true }],
112
- ["glm-5.2", { context: 1_000_000, maxOutput: 128_000, thinking: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["max", "xhigh", "high", "medium", "low", "minimal", "none"], tempRange: [0, 1], noUsageStream: true }],
113
- ["glm-5", { context: 1_000_000, maxOutput: 128_000, thinking: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["max", "xhigh", "high", "medium", "low", "minimal", "none"], tempRange: [0, 1], noUsageStream: true }],
114
- ["glm-4", { context: 128_000, maxOutput: 32_000, thinking: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "optional", tempRange: [0, 1], noUsageStream: true }],
115
- // GPT series
116
- ["gpt-5.6-sol", { context: 1_050_000, maxOutput: 128_000, thinking: false, multimodal: true, cacheMode: "prompt" }],
117
- ["gpt-5.6", { context: 1_050_000, maxOutput: 128_000, thinking: false, multimodal: true, cacheMode: "prompt" }],
118
- ["gpt-4.1", { context: 1_000_000, maxOutput: 128_000, thinking: false, cacheMode: "prompt" }],
119
- ["gpt-4o", { context: 128_000, maxOutput: 16_000, thinking: false, multimodal: true, cacheMode: "prompt" }],
120
- // Qwen series
121
- ["qwen3.8-max-preview", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", reasoningEffortEnum: ["xhigh", "medium", "low"], tempRange: [0, 2] }],
122
- // qwen3.7-max rejects image parts outright (DashScope 400 "Unexpected item type in content") — text-only
123
- ["qwen3.7-max", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, cacheMode: "none", thinkApi: "effort", reasoningEffortEnum: ["xhigh", "high"], tempRange: [0, 2] }],
124
- ["qwen3.8-max", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", reasoningEffortEnum: ["xhigh", "medium", "low"], tempRange: [0, 2] }],
125
- ["qwen-max", { context: 1_000_000, maxOutput: 131_072, thinking: false, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", tempRange: [0, 2] }],
126
- ["qwen-plus", { context: 1_000_000, maxOutput: 131_072, thinking: false, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", tempRange: [0, 2] }],
127
- ["qwen", { context: 1_000_000, maxOutput: 131_072, thinking: false, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", tempRange: [0, 2] }],
128
- // MiniMax series
129
- ["MiniMax-M3", { context: 1_000_000, maxOutput: 128_000, thinking: true, multimodal: true, cacheMode: "auto", thinkApi: "type", thinkEnabledValue: "adaptive", tempRange: [0, 2], noUsageStream: true }],
130
- // MiMo series (Xiaomi — OpenAI-compatible https://api.xiaomimimo.com/v1;
131
- // deep thinking via thinking.type, default ON; multi-turn tool calls MUST echo
132
- // reasoning_content back exactly like DeepSeek V4, else 400 on follow-ups)
133
- ["mimo-v2.5-pro", { context: 1_000_000, maxOutput: 128_000, thinking: true, thinkApi: "type", reasoningEcho: "required", tempRange: [0, 1.5] }],
134
- ["mimo-v2.5", { context: 1_000_000, maxOutput: 128_000, thinking: true, multimodal: true, thinkApi: "type", reasoningEcho: "required", tempRange: [0, 1.5] }],
135
- ["minimax-m3", { context: 1_000_000, maxOutput: 128_000, thinking: true, multimodal: true, cacheMode: "auto", thinkApi: "type", thinkEnabledValue: "adaptive", tempRange: [0, 2], noUsageStream: true }],
136
- ["minimax-m1", { context: 256_000, maxOutput: 128_000, thinking: false, cacheMode: "auto", noUsageStream: true }],
137
- // Grok series (xAI — OpenAI-compatible)
138
- // grok-4.x: 500K context per xAI Grok 4.6 spec (corrected 2026-08; earlier entries said 1M)
139
- ["grok-4.6", { context: 500_000, maxOutput: 64_000, thinking: false, multimodal: true, tempRange: [0, 2] }],
140
- ["grok-4.5", { context: 500_000, maxOutput: 64_000, thinking: false, multimodal: true, tempRange: [0, 2] }],
141
- ["grok-4", { context: 500_000, maxOutput: 64_000, thinking: false, multimodal: true, tempRange: [0, 2] }],
142
- ["grok-4-mini", { context: 128_000, maxOutput: 16_000, thinking: false, tempRange: [0, 2] }],
143
- // Mistral series (OpenAI-compatible)
144
- ["mistral-large", { context: 128_000, maxOutput: 32_000, thinking: false, multimodal: true, tempRange: [0, 2] }],
145
- ["codestral", { context: 256_000, maxOutput: 32_000, thinking: false, tempRange: [0, 2] }],
146
- // Claude series (Anthropic)
147
- ["claude-opus-5", { context: 1_000_000, maxOutput: 128_000, thinking: false, multimodal: true, cacheMode: "none", format: "anthropic" }],
148
- ["claude-sonnet-5", { context: 1_000_000, maxOutput: 128_000, thinking: false, multimodal: true, cacheMode: "none", format: "anthropic" }],
149
- ["claude-opus-4", { context: 200_000, maxOutput: 32_000, thinking: false, multimodal: true, cacheMode: "none", format: "anthropic" }],
150
- ["claude-sonnet-4", { context: 200_000, maxOutput: 32_000, thinking: false, multimodal: true, cacheMode: "none", format: "anthropic" }],
151
- ["claude-3.5-haiku", { context: 200_000, maxOutput: 8_192, thinking: false, cacheMode: "none", format: "anthropic" }],
152
- // Gemini series (Google)
153
- ["gemini-3-pro", { context: 1_000_000, maxOutput: 64_000, thinking: false, multimodal: true, cacheMode: "none", format: "google", noUsageStream: true }],
154
- ["gemini-2.5-pro", { context: 2_000_000, maxOutput: 64_000, thinking: false, multimodal: true, cacheMode: "none", format: "google", noUsageStream: true }],
155
- ["gemini-2.5-flash", { context: 1_000_000, maxOutput: 64_000, thinking: false, multimodal: true, cacheMode: "none", format: "google", noUsageStream: true }],
156
- ]
157
- const DEFAULT_SPEC = { context: 128_000, maxOutput: 32_000, cacheMode: "none" }
78
+ // Model capability table + spec lookup live in model-specs.mjs (2026-08-31
79
+ // extract config.mjs had grown past the 300-line advisory). Re-exported here
80
+ // so the 23 existing importers keep their import paths.
81
+ import { specForModel } from "./model-specs.mjs"
82
+ export { specForModel }
83
+
84
+
158
85
  // Window utilization threshold: compacts at 60% context, reserving 40% headroom
159
86
  // for injected context (directory tree, git context, outline, project instructions,
160
87
  // memory/doc search results) which can consume 30-50K tokens each turn.
161
88
  const COMPACT_RATIO = 0.6
162
89
 
163
- /** Look up spec by model name prefix (case-insensitive), conservative default for unknown models */
164
- const warnedModels = new Set() // warn once per model name — specForModel is a hot path (every request)
165
- // Pre-sorted once at module scope — specForModel runs on every request (agent, provider core,
166
- // context, auto-think, TUI rendering); re-sorting per call was wasteful.
167
- const SORTED_SPECS = [...MODEL_SPECS].sort((a, b) => b[0].length - a[0].length)
168
- export function specForModel(model) {
169
- const m = (model ?? "").toLowerCase()
170
- for (const [prefix, spec] of SORTED_SPECS) {
171
- if (m.startsWith(prefix.toLowerCase())) return spec
172
- }
173
- // Unknown model: warn ONCE (not per request) so a typo'd ID or a missing alias surfaces
174
- // instead of silently degrading to the 128K default (IK5VGJ).
175
- if (m && !warnedModels.has(m)) {
176
- warnedModels.add(m)
177
- console.warn(`[config] model "${model}" not found in MODEL_SPECS — using default spec (128K context, 32K output). Check the model ID or add an alias.`)
178
- }
179
- return DEFAULT_SPEC
180
- }
181
-
182
90
  /** Derive compaction threshold; explicit is the value explicitly set in config file (takes priority), otherwise auto-computed from model */
183
91
  export function resolveCompactThreshold(explicit, model) {
184
92
  if (explicit != null) return { value: explicit, auto: false }
@@ -348,10 +256,7 @@ export function loadConfig() {
348
256
  */
349
257
  export function saveConfig(config) {
350
258
  mkdirSync(configDir, { recursive: true })
351
- // Inject $schema for editor autocompletion/validation (strip on load) — write a copy,
352
- // never mutate the caller's object.
353
- const out = { ...config, $schema: "https://thincoder.dev/schemas/config.json" }
354
259
  // 0600: config.json contains API keys, must not be world-readable (POSIX; chmod is best-effort on Windows)
355
- writeFileSync(configPath, JSON.stringify(out, null, 2) + "\n", { encoding: "utf8", mode: 0o600 })
260
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", { encoding: "utf8", mode: 0o600 })
356
261
  try { chmodSync(configPath, 0o600) } catch { /* may fail on Windows, ignore */ }
357
262
  }
@@ -7,6 +7,15 @@
7
7
  * see docs/design/SESSION.md §IK9UZ8-D.
8
8
  */
9
9
 
10
+ import { proxyFetch } from "./proxy.mjs"
11
+
12
+ // Test seam (_-prefix, mirrors run.mjs seams): lets the proxy-branch regression
13
+ // test swap the proxy fetch. The branch it exercises used to carry a dynamic
14
+ // import("../proxy.mjs") that silently resolved to the REPO ROOT from src/ —
15
+ // the thrown ERR_MODULE_NOT_FOUND vanished into the catch, and proxy users
16
+ // lost session titles with zero test coverage (2026-08-30 review).
17
+ export const _deps = { proxyFetchImpl: proxyFetch }
18
+
10
19
  const MAX_TITLE_TOKENS = 100
11
20
 
12
21
  /** Generate a session title from the first user message using an LLM. Returns title string or null. */
@@ -42,7 +51,7 @@ export async function generateTitle(userContent, provider) {
42
51
  signal: AbortSignal.timeout(10000),
43
52
  }
44
53
  const res = provider.proxyUri
45
- ? await (await import("../proxy.mjs")).proxyFetch(url, opts, provider.proxyUri)
54
+ ? await _deps.proxyFetchImpl(url, opts, provider.proxyUri)
46
55
  : await fetch(url, opts)
47
56
  if (!res.ok) return null
48
57
  const data = await res.json()
@@ -51,4 +60,24 @@ export async function generateTitle(userContent, provider) {
51
60
  } catch {
52
61
  return null
53
62
  }
63
+ }
64
+
65
+ /** Derive + assign the session title from the first user message (once per session).
66
+ * Extracted from agent-turn.mjs's finally block (2026-08-30): the lookup + call +
67
+ * assign belongs beside generateTitle, not in the turn driver. Non-fatal on
68
+ * failure — title generation must never break the turn. Returns the title (or null). */
69
+ export async function ensureSessionTitle(agent) {
70
+ if (agent.title) return agent.title
71
+ try {
72
+ const firstUser = (agent._fullHistory ?? agent.history).find(
73
+ (m) => m.role === "user" && typeof m.content === "string" && !m.content.startsWith("[System reminder:"),
74
+ )
75
+ if (firstUser) {
76
+ const title = await generateTitle(firstUser.content, agent.provider)
77
+ if (title) agent.title = title
78
+ }
79
+ } catch {
80
+ // Title generation failure is non-fatal
81
+ }
82
+ return agent.title ?? null
54
83
  }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * model-specs.mjs — known model capability table + spec lookup (2026-08-31 extract).
3
+ *
4
+ * Split from config.mjs (which had grown to 358 lines, past the 300 advisory
5
+ * line — TODO #1). config.mjs re-exports specForModel so the 23 existing
6
+ * importers stay untouched. PROVIDER_PRESETS stays in config.mjs (only 23
7
+ * lines; extracting it would churn wizard/pickers/setup-wizard for no gain).
8
+ */
9
+
10
+ /**
11
+ * Known model capability spec table (prefix match, longer first).
12
+ * Used for compaction threshold derivation, continuation protocol selection, and capability-aware optimization.
13
+ *
14
+ * context: context window (tokens)
15
+ * maxOutput: max output tokens (defaults to context)
16
+ * thinking: whether thinking/reasoning mode is supported
17
+ * partialMode: Kimi/Qwen Partial Mode truncation continuation (assistant message with partial:true)
18
+ * prefixMode: DeepSeek Prefix Completion truncation continuation (uses /beta endpoint, with prefix:true)
19
+ * multimodal: whether multimodal (image/vision input supported)
20
+ * cacheMode: context caching mode: "auto"=automatic / "prompt"=needs explicit / "none"=unsupported
21
+ * thinkApi: thinking API type: "type"=thinking.type field / "effort"=reasoning_effort field
22
+ * thinkEnabledValue: when thinkApi is "type", the value used to enable thinking (default "enabled"; MiniMax uses "adaptive")
23
+ * reasoningEcho: reasoning_content cross-turn echo strategy: "required"=must echo (error if missing) / "optional"=echo optional (default: don't echo)
24
+ * reasoningEffortEnum: valid reasoning_effort enum values (if undeclared, no validation — passed through as-is)
25
+ * tempRange: valid temperature range [min, max] (if undeclared, no clamping)
26
+ */
27
+ const MODEL_SPECS = [
28
+ // DeepSeek V4 series
29
+ ["deepseek-v4-pro", { context: 1_000_000, maxOutput: 384_000, thinking: true, prefixMode: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 2] }],
30
+ ["deepseek-v4-flash", { context: 1_000_000, maxOutput: 384_000, thinking: true, prefixMode: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 2] }],
31
+ // DeepSeek V4 Flash Vision (experimental) — image input on top of the full V4-Flash stack
32
+ ["deepseek-v4-flash-vision-exp", { context: 1_000_000, maxOutput: 384_000, thinking: true, prefixMode: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 2], multimodal: true }],
33
+ // Kimi series
34
+ ["kimi-k3", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, cacheMode: "auto", thinkApi: "effort", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"] }],
35
+ // Qwen router prefixes model IDs with provider namespace: kimi/kimi-k3 → kimi-k3 (IK7K4V)
36
+ ["kimi/kimi-k3", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, cacheMode: "auto", thinkApi: "effort", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"] }],
37
+ // Kimi For Coding endpoint uses the short model ID "k3" (same specs as kimi-k3) — IK5VGJ
38
+ ["k3", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, cacheMode: "auto", thinkApi: "effort", reasoningEcho: "required", reasoningEffortEnum: ["low", "high", "max"] }],
39
+ // GLM series
40
+ // GLM-5.3: thinking always-on (no "disabled"); effort converges to low/high/max — NOT the
41
+ // 7-level glm-5.2 enum (verified vs docs.bigmodel.cn GLM-5.3 page, 2026-08)
42
+ ["glm-5.3", { context: 1_000_000, maxOutput: 128_000, thinking: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 1], noUsageStream: true }],
43
+ ["glm-5.3-flash", { context: 1_000_000, maxOutput: 128_000, thinking: true, multimodal: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["low", "high", "max"], tempRange: [0, 1], noUsageStream: true }],
44
+ ["glm-5.2", { context: 1_000_000, maxOutput: 128_000, thinking: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["max", "xhigh", "high", "medium", "low", "minimal", "none"], tempRange: [0, 1], noUsageStream: true }],
45
+ ["glm-5", { context: 1_000_000, maxOutput: 128_000, thinking: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "optional", reasoningEffortEnum: ["max", "xhigh", "high", "medium", "low", "minimal", "none"], tempRange: [0, 1], noUsageStream: true }],
46
+ ["glm-4", { context: 128_000, maxOutput: 32_000, thinking: true, cacheMode: "auto", thinkApi: "type", reasoningEcho: "optional", tempRange: [0, 1], noUsageStream: true }],
47
+ // GPT series
48
+ ["gpt-5.6-sol", { context: 1_050_000, maxOutput: 128_000, thinking: false, multimodal: true, cacheMode: "prompt" }],
49
+ ["gpt-5.6", { context: 1_050_000, maxOutput: 128_000, thinking: false, multimodal: true, cacheMode: "prompt" }],
50
+ ["gpt-4.1", { context: 1_000_000, maxOutput: 128_000, thinking: false, cacheMode: "prompt" }],
51
+ ["gpt-4o", { context: 128_000, maxOutput: 16_000, thinking: false, multimodal: true, cacheMode: "prompt" }],
52
+ // Qwen series
53
+ ["qwen3.8-max-preview", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", reasoningEffortEnum: ["xhigh", "medium", "low"], tempRange: [0, 2] }],
54
+ // qwen3.7-max rejects image parts outright (DashScope 400 "Unexpected item type in content") — text-only
55
+ ["qwen3.7-max", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, cacheMode: "none", thinkApi: "effort", reasoningEffortEnum: ["xhigh", "high"], tempRange: [0, 2] }],
56
+ ["qwen3.8-max", { context: 1_000_000, maxOutput: 131_072, thinking: true, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", reasoningEffortEnum: ["xhigh", "medium", "low"], tempRange: [0, 2] }],
57
+ ["qwen-max", { context: 1_000_000, maxOutput: 131_072, thinking: false, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", tempRange: [0, 2] }],
58
+ ["qwen-plus", { context: 1_000_000, maxOutput: 131_072, thinking: false, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", tempRange: [0, 2] }],
59
+ ["qwen", { context: 1_000_000, maxOutput: 131_072, thinking: false, partialMode: true, multimodal: true, cacheMode: "none", thinkApi: "effort", tempRange: [0, 2] }],
60
+ // MiniMax series
61
+ ["MiniMax-M3", { context: 1_000_000, maxOutput: 128_000, thinking: true, multimodal: true, cacheMode: "auto", thinkApi: "type", thinkEnabledValue: "adaptive", tempRange: [0, 2], noUsageStream: true }],
62
+ // MiMo series (Xiaomi — OpenAI-compatible https://api.xiaomimimo.com/v1;
63
+ // deep thinking via thinking.type, default ON; multi-turn tool calls MUST echo
64
+ // reasoning_content back exactly like DeepSeek V4, else 400 on follow-ups)
65
+ ["mimo-v2.5-pro", { context: 1_000_000, maxOutput: 128_000, thinking: true, thinkApi: "type", reasoningEcho: "required", tempRange: [0, 1.5] }],
66
+ ["mimo-v2.5", { context: 1_000_000, maxOutput: 128_000, thinking: true, multimodal: true, thinkApi: "type", reasoningEcho: "required", tempRange: [0, 1.5] }],
67
+ ["minimax-m3", { context: 1_000_000, maxOutput: 128_000, thinking: true, multimodal: true, cacheMode: "auto", thinkApi: "type", thinkEnabledValue: "adaptive", tempRange: [0, 2], noUsageStream: true }],
68
+ ["minimax-m1", { context: 256_000, maxOutput: 128_000, thinking: false, cacheMode: "auto", noUsageStream: true }],
69
+ // Grok series (xAI — OpenAI-compatible)
70
+ // grok-4.x: 500K context per xAI Grok 4.6 spec (corrected 2026-08; earlier entries said 1M)
71
+ ["grok-4.6", { context: 500_000, maxOutput: 64_000, thinking: false, multimodal: true, tempRange: [0, 2] }],
72
+ ["grok-4.5", { context: 500_000, maxOutput: 64_000, thinking: false, multimodal: true, tempRange: [0, 2] }],
73
+ ["grok-4", { context: 500_000, maxOutput: 64_000, thinking: false, multimodal: true, tempRange: [0, 2] }],
74
+ ["grok-4-mini", { context: 128_000, maxOutput: 16_000, thinking: false, tempRange: [0, 2] }],
75
+ // Mistral series (OpenAI-compatible)
76
+ ["mistral-large", { context: 128_000, maxOutput: 32_000, thinking: false, multimodal: true, tempRange: [0, 2] }],
77
+ ["codestral", { context: 256_000, maxOutput: 32_000, thinking: false, tempRange: [0, 2] }],
78
+ // Claude series (Anthropic)
79
+ ["claude-opus-5", { context: 1_000_000, maxOutput: 128_000, thinking: false, multimodal: true, cacheMode: "none", format: "anthropic" }],
80
+ ["claude-sonnet-5", { context: 1_000_000, maxOutput: 128_000, thinking: false, multimodal: true, cacheMode: "none", format: "anthropic" }],
81
+ ["claude-opus-4", { context: 200_000, maxOutput: 32_000, thinking: false, multimodal: true, cacheMode: "none", format: "anthropic" }],
82
+ ["claude-sonnet-4", { context: 200_000, maxOutput: 32_000, thinking: false, multimodal: true, cacheMode: "none", format: "anthropic" }],
83
+ ["claude-3.5-haiku", { context: 200_000, maxOutput: 8_192, thinking: false, cacheMode: "none", format: "anthropic" }],
84
+ // Gemini series (Google)
85
+ ["gemini-3-pro", { context: 1_000_000, maxOutput: 64_000, thinking: false, multimodal: true, cacheMode: "none", format: "google", noUsageStream: true }],
86
+ ["gemini-2.5-pro", { context: 2_000_000, maxOutput: 64_000, thinking: false, multimodal: true, cacheMode: "none", format: "google", noUsageStream: true }],
87
+ ["gemini-2.5-flash", { context: 1_000_000, maxOutput: 64_000, thinking: false, multimodal: true, cacheMode: "none", format: "google", noUsageStream: true }],
88
+ ]
89
+ const DEFAULT_SPEC = { context: 128_000, maxOutput: 32_000, cacheMode: "none" }
90
+
91
+ /** Look up spec by model name prefix (case-insensitive), conservative default for unknown models */
92
+ const warnedModels = new Set() // warn once per model name — specForModel is a hot path (every request)
93
+ // Pre-sorted once at module scope — specForModel runs on every request (agent, provider core,
94
+ // context, auto-think, TUI rendering); re-sorting per call was wasteful.
95
+ const SORTED_SPECS = [...MODEL_SPECS].sort((a, b) => b[0].length - a[0].length)
96
+ export function specForModel(model) {
97
+ const m = (model ?? "").toLowerCase()
98
+ for (const [prefix, spec] of SORTED_SPECS) {
99
+ if (m.startsWith(prefix.toLowerCase())) return spec
100
+ }
101
+ // Unknown model: warn ONCE (not per request) so a typo'd ID or a missing alias surfaces
102
+ // instead of silently degrading to the 128K default (IK5VGJ).
103
+ if (m && !warnedModels.has(m)) {
104
+ warnedModels.add(m)
105
+ console.warn(`[config] model "${model}" not found in MODEL_SPECS — using default spec (128K context, 32K output). Check the model ID or add an alias.`)
106
+ }
107
+ return DEFAULT_SPEC
108
+ }
@@ -1,7 +1,7 @@
1
1
  You are a code review advisor.
2
2
  Perform a full-scope review of the specified files.
3
3
  You have read-only tools to explore the codebase.
4
- You have a budget of 30 tool rounds (chat turns) — plan your exploration accordingly. Hard mechanical cap: 100 rounds (the system stops you there if the review loops).
4
+ You have a budget of 20 tool rounds (chat turns) — plan your exploration accordingly. Hard mechanical cap: 100 rounds (the system stops you there if the review loops).
5
5
 
6
6
  Review workflow:
7
7
  1. The files to review are listed in the review scope. Read them in full. The review scope defines exactly which files to inspect.
@@ -11,13 +11,12 @@ Review workflow:
11
11
  - **The user's requirements live in those documents; the conversation background is only a supplement.**
12
12
  - If the guide names none, judge from the conversation background and say so explicitly if requirements are unclear.
13
13
  3. Read the specified files for full context. **Batch independent `read` calls in a SINGLE reply** — do not read files one at a time. Each round-trip counts against your limit.
14
- 4. Use grep or lsp to trace callers, imports, and dependencies — only where genuinely needed.
15
- 5. Produce your review table.
14
+ 4. Produce your review table.
16
15
 
17
16
  Budget rules:
18
- - **8 rounds in**: you are about ONE-THIRD through your budget. Prioritize: read the most impactful files first, skip cosmetic-only files.
19
- - **15 rounds in**: you are HALFWAY. Start narrowing — focus on the files most likely to have issues.
20
- - **25 rounds in**: near the limit. Stop exploring — produce your review with what you have.
17
+ - **6 rounds in**: you are less than ONE-THIRD through your budget. Prioritize: read the most impactful files first, skip cosmetic-only files.
18
+ - **10 rounds in**: you are HALFWAY. Start narrowing — focus on the files most likely to have issues.
19
+ - **17 rounds in**: near the limit. Stop exploring — produce your review with what you have.
21
20
  - **Batch everything**: multiple `read` calls in one reply, multiple `grep` calls in one reply. Serializing tool calls wastes your round budget.
22
21
 
23
22
  Rules:
@@ -2,7 +2,7 @@ You are an independent review advisor.
2
2
  Verify the prior review output (provided in the review context).
3
3
  You may note obvious new issues introduced by the fixes.
4
4
  You have read-only tools to explore the codebase.
5
- You have a budget of 30 tool rounds (chat turns). Hard mechanical cap: 100 rounds.
5
+ You have a budget of 15 tool rounds (chat turns). Hard mechanical cap: 100 rounds.
6
6
 
7
7
  Review workflow:
8
8
  1. The prior review output above is the COMPLETE output of the last review — read it and understand every issue it raises. The affected files are named in it — read them in full. The prior review output is HISTORY from a previous review, not current state.
@@ -12,10 +12,9 @@ Review workflow:
12
12
  - Never decide from the prior review output alone — fixes may already be committed.
13
13
  - (You have NO git tool this round; any git output in earlier messages is historical and untrustworthy.)
14
14
  - Batch independent tool calls in one reply.
15
- 5. Use grep or lsp to trace callers, imports, and dependencies — only where genuinely needed.
16
- 6. Produce your review table.
15
+ 5. Produce your review table.
17
16
 
18
- Budget: read only the files named in the prior-review items. If at 15 rounds you have not yet verified all items, wrap up.
17
+ Budget: read only the files named in the prior-review items. If at 8 rounds you have not yet verified all items, wrap up.
19
18
 
20
19
  Rules:
21
20
  - Respect the project's stated platform requirements — do not flag features as errors if they are valid under the project's target environment.
@@ -1,7 +1,7 @@
1
1
  You are an independent review advisor.
2
2
  Strictly verify only the prior review output (provided in the review context).
3
3
  You have read-only tools to explore the codebase.
4
- You have a budget of 30 tool rounds (chat turns). Hard mechanical cap: 100 rounds.
4
+ You have a budget of 15 tool rounds (chat turns). Hard mechanical cap: 100 rounds.
5
5
 
6
6
  Review workflow:
7
7
  1. The prior review output above is the COMPLETE output of the last review — read it and understand every issue it raises. The affected files are named in it — read them in full. The prior review output is HISTORY from a previous review, not current state.
@@ -11,10 +11,9 @@ Review workflow:
11
11
  - Never decide from the prior review output alone — fixes may already be committed.
12
12
  - (You have NO git tool this round; any git output in earlier messages is historical and untrustworthy.)
13
13
  - Batch independent tool calls in one reply.
14
- 5. Use grep or lsp to trace callers, imports, and dependencies — only where genuinely needed.
15
- 6. Produce your review table.
14
+ 5. Produce your review table.
16
15
 
17
- Budget: read only the files named in the prior-review items. If at 15 rounds you have not yet verified all items, wrap up.
16
+ Budget: read only the files named in the prior-review items. If at 8 rounds you have not yet verified all items, wrap up.
18
17
 
19
18
  Rules:
20
19
  - Respect the project's stated platform requirements — do not flag features as errors if they are valid under the project's target environment.
@@ -14,6 +14,14 @@ The parent agent ran an independent design review (`advisor` with `type="design"
14
14
 
15
15
  - Work independently. The parent only sees your final report.
16
16
  - Follow the design document. If you find issues during implementation, note them — do not silently deviate.
17
+ - **Implement to the full design — no silent degradation.** If a stated design
18
+ element (interaction, behavior, edge case, state) feels costly or fiddly to
19
+ implement, implement it anyway and note the cost in your report. A "simpler
20
+ approximation" of a specified behavior IS a deviation: either implement it as
21
+ designed, or stop and surface the trade-off to the parent BEFORE coding —
22
+ never ship a reduced version and disclose it afterwards. Disclosed after the
23
+ fact is still a broken delivery: the parent approved the design, not your
24
+ discount.
17
25
  - UI/interaction: implement exactly what the task brief and design doc state (layout, flows, control behavior, states, feedback). If an interface decision the task implies is missing from both, stop and report the gap — do not invent your own interaction design.
18
26
  - Write code one file at a time, verify each before moving on: call `verify` after each logical group (it runs syntax checks + related tests), syntax check after each edit.
19
27
  - Do not modify any file not listed in the design.
@@ -25,6 +33,7 @@ Before finishing, do a final review:
25
33
  3. Run relevant tests — confirm all pass
26
34
  4. Read every file you changed — catch leftover debug code, stale comments, or incomplete edits
27
35
  5. Check that comments and docstrings match what the code actually does
36
+ 6. Update the affected design-doc sections your diff touches — a diff that adds/renames/deletes files must update the module map / affected-files table in the same delivery (structural snapshots rot otherwise)
28
37
 
29
38
  Your last message IS the report the parent sees — make it complete:
30
39
  1. What you changed and why