copperhead 0.6.0 → 0.8.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.
Files changed (118) hide show
  1. package/README.md +36 -4
  2. package/dist/agent/animate.js +76 -0
  3. package/dist/agent/animate.js.map +1 -0
  4. package/dist/agent/box.js +89 -0
  5. package/dist/agent/box.js.map +1 -0
  6. package/dist/agent/dock-renderer.js +173 -0
  7. package/dist/agent/dock-renderer.js.map +1 -0
  8. package/dist/agent/logo.js +21 -0
  9. package/dist/agent/logo.js.map +1 -0
  10. package/dist/agent/loop.js +130 -18
  11. package/dist/agent/loop.js.map +1 -1
  12. package/dist/agent/prompts.js +2 -1
  13. package/dist/agent/prompts.js.map +1 -1
  14. package/dist/agent/providers/claude-code.js +85 -116
  15. package/dist/agent/providers/claude-code.js.map +1 -1
  16. package/dist/agent/providers/cursor.js +317 -0
  17. package/dist/agent/providers/cursor.js.map +1 -0
  18. package/dist/agent/providers/tool-protocol.js +205 -0
  19. package/dist/agent/providers/tool-protocol.js.map +1 -0
  20. package/dist/agent/recovery.js +148 -0
  21. package/dist/agent/recovery.js.map +1 -0
  22. package/dist/agent/render.js +44 -12
  23. package/dist/agent/render.js.map +1 -1
  24. package/dist/agent/response-cache.js +81 -0
  25. package/dist/agent/response-cache.js.map +1 -0
  26. package/dist/agent/runmeta.js +4 -5
  27. package/dist/agent/runmeta.js.map +1 -1
  28. package/dist/agent/theme.js +84 -0
  29. package/dist/agent/theme.js.map +1 -0
  30. package/dist/agent/tools.js +61 -4
  31. package/dist/agent/tools.js.map +1 -1
  32. package/dist/agent/transcript.js.map +1 -1
  33. package/dist/cli.js +134 -13
  34. package/dist/cli.js.map +1 -1
  35. package/dist/commands/create.js +482 -38
  36. package/dist/commands/create.js.map +1 -1
  37. package/dist/commands/demo.js +146 -0
  38. package/dist/commands/demo.js.map +1 -0
  39. package/dist/commands/doctor.js +240 -0
  40. package/dist/commands/doctor.js.map +1 -0
  41. package/dist/commands/repl-inspect.js +342 -0
  42. package/dist/commands/repl-inspect.js.map +1 -0
  43. package/dist/commands/repl.js +618 -0
  44. package/dist/commands/repl.js.map +1 -0
  45. package/dist/config.js +24 -2
  46. package/dist/config.js.map +1 -1
  47. package/dist/kicad/bootstrap.js +166 -0
  48. package/dist/kicad/bootstrap.js.map +1 -0
  49. package/dist/kicad/cli.js +126 -6
  50. package/dist/kicad/cli.js.map +1 -1
  51. package/dist/kicad/spice.js +306 -0
  52. package/dist/kicad/spice.js.map +1 -0
  53. package/dist/kicad/symlib.js +228 -0
  54. package/dist/kicad/symlib.js.map +1 -0
  55. package/dist/memory/bom-table.js +193 -22
  56. package/dist/memory/bom-table.js.map +1 -1
  57. package/dist/memory/drift.js +33 -11
  58. package/dist/memory/drift.js.map +1 -1
  59. package/dist/util/cli-args.js +35 -0
  60. package/dist/util/cli-args.js.map +1 -0
  61. package/dist/util/dock.js +155 -0
  62. package/dist/util/dock.js.map +1 -0
  63. package/dist/util/git.js +165 -4
  64. package/dist/util/git.js.map +1 -1
  65. package/dist/util/live-prompt.js +542 -0
  66. package/dist/util/live-prompt.js.map +1 -0
  67. package/dist/util/paths.js +9 -0
  68. package/dist/util/paths.js.map +1 -1
  69. package/dist/util/preflight.js +37 -0
  70. package/dist/util/preflight.js.map +1 -1
  71. package/dist/util/retry.js +23 -0
  72. package/dist/util/retry.js.map +1 -1
  73. package/dist/util/select.js +172 -0
  74. package/dist/util/select.js.map +1 -0
  75. package/dist/util/tmp.js +119 -0
  76. package/dist/util/tmp.js.map +1 -0
  77. package/package.json +3 -2
  78. package/src/agent/animate.ts +90 -0
  79. package/src/agent/box.ts +99 -0
  80. package/src/agent/dock-renderer.ts +181 -0
  81. package/src/agent/logo.ts +23 -0
  82. package/src/agent/loop.ts +148 -18
  83. package/src/agent/prompts.ts +2 -1
  84. package/src/agent/providers/claude-code.ts +91 -122
  85. package/src/agent/providers/cursor.ts +364 -0
  86. package/src/agent/providers/tool-protocol.ts +212 -0
  87. package/src/agent/recovery.ts +162 -0
  88. package/src/agent/render.ts +56 -12
  89. package/src/agent/response-cache.ts +80 -0
  90. package/src/agent/runmeta.ts +6 -7
  91. package/src/agent/theme.ts +91 -0
  92. package/src/agent/tools.ts +62 -4
  93. package/src/agent/transcript.ts +1 -0
  94. package/src/agent/types.ts +17 -0
  95. package/src/cli.ts +139 -15
  96. package/src/commands/create.ts +581 -40
  97. package/src/commands/demo.ts +184 -0
  98. package/src/commands/doctor.ts +289 -0
  99. package/src/commands/repl-inspect.ts +353 -0
  100. package/src/commands/repl.ts +685 -0
  101. package/src/config.ts +40 -3
  102. package/src/kicad/bootstrap.ts +181 -0
  103. package/src/kicad/cli.ts +132 -7
  104. package/src/kicad/spice.ts +399 -0
  105. package/src/kicad/symlib.ts +248 -0
  106. package/src/layout/claude-ui-layout.md +72 -0
  107. package/src/layout/repl-ui-layout.md +139 -0
  108. package/src/memory/bom-table.ts +191 -20
  109. package/src/memory/drift.ts +42 -11
  110. package/src/util/cli-args.ts +42 -0
  111. package/src/util/dock.ts +161 -0
  112. package/src/util/git.ts +176 -4
  113. package/src/util/live-prompt.ts +595 -0
  114. package/src/util/paths.ts +10 -0
  115. package/src/util/preflight.ts +44 -0
  116. package/src/util/retry.ts +29 -0
  117. package/src/util/select.ts +192 -0
  118. package/src/util/tmp.ts +113 -0
@@ -0,0 +1,205 @@
1
+ export function renderToolProtocol(tools) {
2
+ if (!tools.length)
3
+ return '';
4
+ const lines = [
5
+ '# Tool protocol',
6
+ '',
7
+ 'You are the reasoning half of a tool-driven workflow; you cannot run anything yourself.',
8
+ 'To take an action, reply with EXACTLY ONE JSON object and nothing else, wrapped in a',
9
+ '```json fenced code block:',
10
+ '',
11
+ '```json',
12
+ '{"tool": "<tool_name>", "args": { ... }}',
13
+ '```',
14
+ '',
15
+ 'Use only the tools listed below, with `args` matching the tool\'s JSON Schema. If you have',
16
+ 'no tool to call and only want to say something, reply with plain prose and no JSON block.',
17
+ '',
18
+ '## Available tools',
19
+ ];
20
+ for (const t of tools) {
21
+ lines.push('', `### ${t.name}`, t.description, `Parameters (JSON Schema): ${JSON.stringify(t.parameters)}`);
22
+ }
23
+ return lines.join('\n');
24
+ }
25
+ /** Delta prompt for a resumed CLI session: new user lines and tool results only. */
26
+ export function renderDelta(messages, from) {
27
+ const idToName = new Map();
28
+ for (const m of messages) {
29
+ if (m.role === 'assistant')
30
+ for (const call of m.toolCalls ?? [])
31
+ idToName.set(call.id, call.name);
32
+ }
33
+ const parts = [];
34
+ for (const m of messages.slice(Math.max(0, from))) {
35
+ if (m.role === 'user') {
36
+ parts.push(`[user]\n${m.content}`);
37
+ }
38
+ else if (m.role === 'tool') {
39
+ const name = idToName.get(m.toolCallId) ?? m.toolCallId;
40
+ parts.push(`[result of ${name}]\n${m.content}`);
41
+ }
42
+ }
43
+ return parts.join('\n\n');
44
+ }
45
+ export function renderConversation(messages) {
46
+ const idToName = new Map();
47
+ const parts = [];
48
+ for (const m of messages) {
49
+ if (m.role === 'system')
50
+ continue;
51
+ if (m.role === 'user') {
52
+ parts.push(`[user]\n${m.content}`);
53
+ }
54
+ else if (m.role === 'assistant') {
55
+ if (m.content)
56
+ parts.push(`[assistant]\n${m.content}`);
57
+ for (const call of m.toolCalls ?? []) {
58
+ idToName.set(call.id, call.name);
59
+ parts.push(`[assistant tool call]\n\`\`\`json\n${JSON.stringify({ tool: call.name, args: call.args })}\n\`\`\``);
60
+ }
61
+ }
62
+ else {
63
+ const name = idToName.get(m.toolCallId) ?? m.toolCallId;
64
+ parts.push(`[result of ${name}]\n${m.content}`);
65
+ }
66
+ }
67
+ return parts.join('\n\n');
68
+ }
69
+ /**
70
+ * Detect a malformed-but-intended tool call in a turn that dispatched none
71
+ * (#I10). The signature is machine-recognizable: the text contains
72
+ * `"tool":"<name>"` naming a tool in the current catalog, yet nothing parsed.
73
+ * That is the exact case where the tolerant extractor's silence misleads the
74
+ * model — the JSON was near-miss malformed (a brace short, or the outer object
75
+ * split so only an inner `{args}` with no `tool` key balanced), not the tool
76
+ * being broken. Returns a one-line steer to re-emit it, or undefined when the
77
+ * absence of a call is genuine (plain prose, no tool named).
78
+ */
79
+ function detectMalformedCall(text, catalog) {
80
+ const re = /"tool"\s*:\s*"([^"]+)"/g;
81
+ let m;
82
+ while ((m = re.exec(text)) !== null) {
83
+ const name = m[1];
84
+ if (catalog.has(name)) {
85
+ return (`A tool call for "${name}" looks malformed — it named the tool but did not parse as ` +
86
+ 'valid JSON (likely unbalanced braces or a missing closing brace), so no call ran. ' +
87
+ 'Re-emit it as exactly one complete JSON object: {"tool": "...", "args": { ... }}.');
88
+ }
89
+ }
90
+ return undefined;
91
+ }
92
+ /**
93
+ * Extract tool-call JSON from the model's reply. Tolerant by design (D1):
94
+ * unparseable output is returned as plain text with no tool calls rather than
95
+ * throwing, so a non-conforming turn degrades to the loop's stall/nudge path.
96
+ * A parsed block only counts as a tool call when its name is in the current
97
+ * turn's catalog (`availableTools(ctx)`): a hallucinated or locked tool name is
98
+ * left as prose so the loop nudges, rather than dispatching a bogus call.
99
+ */
100
+ export function parseToolCalls(text, nextId, catalog) {
101
+ if (!text)
102
+ return { text: null, toolCalls: [] };
103
+ const toolCalls = [];
104
+ const matched = [];
105
+ // Extract tool calls by scanning for complete JSON objects, NOT by matching
106
+ // ``` fences. A tool call's `content`/`args` can hold a full markdown doc that
107
+ // itself contains ``` code fences; a fence regex truncates the JSON at the
108
+ // first inner fence, JSON.parse fails, and the call is silently dropped (the
109
+ // model then assumes it wrote a file it never did). The brace scan is
110
+ // string-aware, so braces and backticks inside JSON string values are ignored.
111
+ let searchFrom = 0;
112
+ while (searchFrom < text.length) {
113
+ const braceAt = text.indexOf('{', searchFrom);
114
+ if (braceAt < 0)
115
+ break;
116
+ const span = scanJsonObject(text, braceAt);
117
+ if (!span) {
118
+ // Unbalanced '{' (stray brace in prose): retry from the next candidate so
119
+ // one bad brace can't hide a well-formed call later in the reply.
120
+ searchFrom = braceAt + 1;
121
+ continue;
122
+ }
123
+ const call = toToolCall(text.slice(span.start, span.end), nextId, catalog);
124
+ if (call) {
125
+ toolCalls.push(call);
126
+ matched.push([span.start, span.end]);
127
+ }
128
+ searchFrom = span.end;
129
+ }
130
+ if (!toolCalls.length) {
131
+ // No call dispatched — but did the model clearly *intend* one? A fenced
132
+ // ```json block that names a catalog tool yet produced zero calls is a
133
+ // malformed near-miss (unbalanced braces, a missing `}`, or an inner object
134
+ // with no `tool` key). Silently dropping it gives the model no signal, so it
135
+ // misreads "no result" as "this tool is broken" and can bake that false
136
+ // conclusion into a committed summary (#I10). Surface a nudge instead.
137
+ return { text: text.trim() ? text : null, toolCalls, nudge: detectMalformedCall(text, catalog) };
138
+ }
139
+ // Prose is whatever survives once the tool-call objects (and any now-empty
140
+ // ```json fences around them) are removed.
141
+ let prose = '';
142
+ let cursor = 0;
143
+ for (const [start, end] of matched) {
144
+ prose += text.slice(cursor, start);
145
+ cursor = end;
146
+ }
147
+ prose += text.slice(cursor);
148
+ prose = prose.replace(/```(?:json)?\s*```/gi, '').replace(/```(?:json)?\s*$/gi, '').trim();
149
+ return { text: prose.length ? prose : null, toolCalls };
150
+ }
151
+ /**
152
+ * Find the first complete, brace-balanced JSON object at or after `from`,
153
+ * respecting JSON string quoting/escaping so braces or backticks inside string
154
+ * values do not end the scan. Returns its `[start, end)` bounds or null.
155
+ */
156
+ function scanJsonObject(text, from) {
157
+ const start = text.indexOf('{', from);
158
+ if (start < 0)
159
+ return null;
160
+ let depth = 0;
161
+ let inStr = false;
162
+ let esc = false;
163
+ for (let i = start; i < text.length; i++) {
164
+ const ch = text[i];
165
+ if (inStr) {
166
+ if (esc)
167
+ esc = false;
168
+ else if (ch === '\\')
169
+ esc = true;
170
+ else if (ch === '"')
171
+ inStr = false;
172
+ continue;
173
+ }
174
+ if (ch === '"')
175
+ inStr = true;
176
+ else if (ch === '{')
177
+ depth++;
178
+ else if (ch === '}' && --depth === 0)
179
+ return { start, end: i + 1 };
180
+ }
181
+ return null;
182
+ }
183
+ function toToolCall(raw, nextId, catalog) {
184
+ if (!raw)
185
+ return null;
186
+ let obj;
187
+ try {
188
+ obj = JSON.parse(raw.trim());
189
+ }
190
+ catch {
191
+ return null;
192
+ }
193
+ if (!obj || typeof obj !== 'object')
194
+ return null;
195
+ const rec = obj;
196
+ if (typeof rec.tool !== 'string')
197
+ return null;
198
+ // Only accept names the turn actually advertised. An empty catalog means the
199
+ // turn offered no tools, so nothing parses as a call.
200
+ if (!catalog.has(rec.tool))
201
+ return null;
202
+ const args = rec.args && typeof rec.args === 'object' ? rec.args : {};
203
+ return { id: nextId(), name: rec.tool, args };
204
+ }
205
+ //# sourceMappingURL=tool-protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-protocol.js","sourceRoot":"","sources":["../../../src/agent/providers/tool-protocol.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,148 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ /** Thrown when a single provider turn blows past its watchdog deadline. */
5
+ export class TurnTimeoutError extends Error {
6
+ ms;
7
+ constructor(ms) {
8
+ super(`turn exceeded ${ms}ms without responding`);
9
+ this.ms = ms;
10
+ this.name = 'TurnTimeoutError';
11
+ }
12
+ }
13
+ /**
14
+ * Race `fn()` against a deadline so a hung provider call cannot stall the run
15
+ * forever. On timeout, `onTimeout` runs (tear down the in-flight call, e.g.
16
+ * provider.close()) and the returned promise rejects with TurnTimeoutError; the
17
+ * caller decides whether to retry or fail. `ms <= 0` (or non-finite) disables the
18
+ * watchdog and just awaits `fn()`.
19
+ */
20
+ export async function withTimeout(fn, ms, onTimeout) {
21
+ if (!Number.isFinite(ms) || ms <= 0)
22
+ return fn();
23
+ let timer;
24
+ const timeout = new Promise((_, reject) => {
25
+ timer = setTimeout(() => {
26
+ void Promise.resolve(onTimeout?.()).catch(() => { });
27
+ reject(new TurnTimeoutError(ms));
28
+ }, ms);
29
+ });
30
+ try {
31
+ return await Promise.race([fn(), timeout]);
32
+ }
33
+ finally {
34
+ if (timer)
35
+ clearTimeout(timer);
36
+ }
37
+ }
38
+ /** Extract the first brace-balanced JSON object from text, tolerating quoting and
39
+ * escaping, and interpret it as a StageDiagnosis. Anything unparseable is treated
40
+ * as "abort" so an ambiguous diagnosis never loops the pipeline forever. */
41
+ export function parseDiagnosis(text) {
42
+ if (!text)
43
+ return { verdict: 'abort', reason: 'no diagnosis produced' };
44
+ const start = text.indexOf('{');
45
+ if (start >= 0) {
46
+ let depth = 0;
47
+ let inStr = false;
48
+ let esc = false;
49
+ for (let i = start; i < text.length; i++) {
50
+ const ch = text[i];
51
+ if (inStr) {
52
+ if (esc)
53
+ esc = false;
54
+ else if (ch === '\\')
55
+ esc = true;
56
+ else if (ch === '"')
57
+ inStr = false;
58
+ continue;
59
+ }
60
+ if (ch === '"')
61
+ inStr = true;
62
+ else if (ch === '{')
63
+ depth++;
64
+ else if (ch === '}' && --depth === 0) {
65
+ try {
66
+ const o = JSON.parse(text.slice(start, i + 1));
67
+ const verdict = o.verdict === 'retry' ? 'retry' : 'abort';
68
+ return {
69
+ verdict,
70
+ reason: typeof o.reason === 'string' ? o.reason : 'no reason given',
71
+ ...(verdict === 'retry' && typeof o.guidance === 'string' && o.guidance.trim()
72
+ ? { guidance: o.guidance.trim() }
73
+ : {}),
74
+ };
75
+ }
76
+ catch {
77
+ break;
78
+ }
79
+ }
80
+ }
81
+ }
82
+ return { verdict: 'abort', reason: 'diagnosis was not valid JSON' };
83
+ }
84
+ /** Compact, most-recent-last excerpt of a run's transcript for the diagnostician:
85
+ * the last assistant message and the last few tool results, truncated. */
86
+ export async function transcriptExcerpt(transcriptDir, maxChars = 4000) {
87
+ const p = path.join(transcriptDir, 'transcript.jsonl');
88
+ if (!existsSync(p))
89
+ return '(no transcript)';
90
+ let lines;
91
+ try {
92
+ lines = (await readFile(p, 'utf8')).trim().split('\n');
93
+ }
94
+ catch {
95
+ return '(transcript unreadable)';
96
+ }
97
+ const parts = [];
98
+ for (const line of lines.slice(-12)) {
99
+ try {
100
+ const e = JSON.parse(line);
101
+ if (e.type === 'assistant' && typeof e.data?.text === 'string' && e.data.text) {
102
+ parts.push(`[assistant] ${e.data.text}`);
103
+ }
104
+ else if (e.type === 'tool') {
105
+ parts.push(`[${String(e.data?.name)}] ${String(e.data?.result ?? '').split('\n')[0]}`);
106
+ }
107
+ }
108
+ catch {
109
+ /* skip */
110
+ }
111
+ }
112
+ const joined = parts.join('\n');
113
+ return joined.length > maxChars ? joined.slice(joined.length - maxChars) : joined;
114
+ }
115
+ /**
116
+ * Ask the model whether a failed/incomplete stage is worth retrying, and if so
117
+ * how. Uses a fresh, tool-less provider turn (the same saved-login backend the
118
+ * pipeline runs on), so no extra credentials or config are needed. Any error or
119
+ * ambiguity resolves to "abort" — recovery must fail safe toward reporting to the
120
+ * human rather than looping.
121
+ */
122
+ export async function diagnoseStageFailure(provider, input) {
123
+ const system = 'You are the recovery supervisor for an automated KiCad PCB-design pipeline. ' +
124
+ 'A stage just failed or ended without meeting its completion contract. Judge whether ' +
125
+ 'another automated attempt is likely to succeed, or whether a human should intervene. ' +
126
+ 'Be decisive and terse.';
127
+ const user = `Stage: ${input.stageName}\n` +
128
+ `Stage goal: ${input.stageGoal}\n` +
129
+ `Failure: ${input.failure}\n` +
130
+ `This was attempt ${input.attempt} of ${input.maxAttempts}.\n\n` +
131
+ `Recent transcript (most recent last):\n${input.excerpt}\n\n` +
132
+ 'Reply with ONLY a JSON object, no prose:\n' +
133
+ '{"verdict":"retry"|"abort","reason":"<one sentence>","guidance":"<if retry: concrete, specific instructions to prepend to the next attempt so it avoids this failure; otherwise empty>"}\n' +
134
+ '- "retry" if the failure looks transient or fixable with clearer instructions (a dropped or locked tool call, an empty/no-op edit, a skipped step, a timeout, a formatting slip).\n' +
135
+ '- "abort" if repeating the same attempt will not help and a human should look (missing inputs, a genuine dead-end, or the same failure already seen on a prior attempt).';
136
+ const messages = [
137
+ { role: 'system', content: system },
138
+ { role: 'user', content: user },
139
+ ];
140
+ try {
141
+ const turn = await provider.chat(messages, []);
142
+ return { ...parseDiagnosis(turn.text), usage: turn.usage };
143
+ }
144
+ catch (e) {
145
+ return { verdict: 'abort', reason: `diagnosis call failed: ${e.message}` };
146
+ }
147
+ }
148
+ //# sourceMappingURL=recovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recovery.js","sourceRoot":"","sources":["../../src/agent/recovery.ts"],"names":[],"mappings":"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"}
@@ -3,7 +3,11 @@
3
3
  * once at startup: interactive (TTY, no --json/--plain) pins a status line to
4
4
  * the bottom of the terminal and redraws it in place; plain emits line-oriented
5
5
  * output with zero ANSI escapes — the mode CI, pipes, and tests see.
6
+ *
7
+ * Interactive mode adds subtle SGR chrome (copper accent, dim secondary text).
8
+ * Plain mode never emits color (AC-8.9).
6
9
  */
10
+ import { copper, dim, setColorEnabled, styleOutcome, toolLine, warn } from './theme.js';
7
11
  /** Compact token count: 850 -> "850", 12300 -> "12.3k". */
8
12
  export function fmtTokens(n) {
9
13
  return n < 1000 ? String(n) : `${(n / 1000).toFixed(1)}k`;
@@ -28,6 +32,8 @@ export function plainRenderer(log) {
28
32
  turnStart: (turn, maxTurns, tokensIn, tokensOut) => log(turnMarker(turn, maxTurns, tokensIn, tokensOut)),
29
33
  toolResult: (name, firstLine) => log(` [${name}] ${firstLine}`),
30
34
  status: () => { },
35
+ heartbeat: ({ elapsedMs, streamedChars }) => log(` … still working — ${fmtDuration(elapsedMs)} elapsed` +
36
+ (streamedChars ? `, ~${fmtTokens(streamedChars)} chars streamed` : ' (no output yet)')),
31
37
  finish: (line) => log(line),
32
38
  };
33
39
  }
@@ -47,6 +53,7 @@ export class InteractiveRenderer {
47
53
  maxTurns = 0;
48
54
  tokensIn = 0;
49
55
  tokensOut = 0;
56
+ streamedChars = 0;
50
57
  busy = null;
51
58
  frame = 0;
52
59
  timer = null;
@@ -69,17 +76,30 @@ export class InteractiveRenderer {
69
76
  process.on('SIGINT', this.onSigint);
70
77
  }
71
78
  statusText() {
72
- const parts = [
73
- `turn ${this.turn}/${this.maxTurns}`,
74
- `${fmtTokens(this.tokensIn)} in / ${fmtTokens(this.tokensOut)} out`,
75
- fmtDuration(Date.now() - this.startMs),
76
- ];
77
- if (this.busy)
78
- parts.push(this.busy);
79
+ // Raw (uncolored) segments once; both the colored line and the
80
+ // narrow-terminal fallback are assembled from these.
79
81
  const spinner = this.busy ? FRAMES[this.frame % FRAMES.length] : '·';
80
- const line = `${spinner} ${parts.join(' · ')}`;
82
+ const turn = `turn ${this.turn}/${this.maxTurns}`;
83
+ const tokens = `${fmtTokens(this.tokensIn)} in / ${fmtTokens(this.tokensOut)} out`;
84
+ const elapsed = fmtDuration(Date.now() - this.startMs);
85
+ // Fold streamed-output volume into the busy segment so a large turn's
86
+ // status line visibly grows — a hung one stays frozen (5.1).
87
+ const busy = this.busy
88
+ ? this.streamedChars
89
+ ? `${this.busy} ~${fmtTokens(this.streamedChars)} ch`
90
+ : this.busy
91
+ : undefined;
92
+ const parts = [turn, dim(tokens), dim(elapsed), ...(busy ? [warn(busy)] : [])];
93
+ const line = `${this.busy ? copper(spinner) : dim(spinner)} ${parts.join(dim(' · '))}`;
94
+ // Truncate by visible length roughly: strip SGR when measuring so color
95
+ // codes don't eat the column budget and clip the readable text early.
81
96
  const width = this.out.columns ?? 80;
82
- return line.length > width ? line.slice(0, width - 1) : line;
97
+ const visible = line.replace(/\x1b\[[0-9;]*m/g, '');
98
+ if (visible.length <= width)
99
+ return line;
100
+ // Fall back to an uncolored truncated line when the terminal is too narrow.
101
+ const plain = [spinner, turn, tokens, elapsed, ...(busy ? [busy] : [])].join(' · ');
102
+ return plain.length > width ? plain.slice(0, width - 1) : plain;
83
103
  }
84
104
  redraw() {
85
105
  if (this.idle)
@@ -115,22 +135,31 @@ export class InteractiveRenderer {
115
135
  this.maxTurns = maxTurns;
116
136
  this.tokensIn = tokensIn;
117
137
  this.tokensOut = tokensOut;
138
+ this.streamedChars = 0; // per-turn: reset so last turn's volume doesn't linger
118
139
  this.ensureTimer();
119
140
  this.redraw();
120
141
  }
121
142
  toolResult(name, firstLine) {
122
- this.log(` [${name}] ${firstLine}`);
143
+ this.log(toolLine(name, firstLine));
123
144
  }
124
145
  status(text) {
125
146
  this.busy = text;
147
+ if (!text)
148
+ this.streamedChars = 0; // turn's provider call ended
126
149
  if (text && !this.idle)
127
150
  this.ensureTimer();
128
151
  this.redraw();
129
152
  }
153
+ heartbeat({ streamedChars }) {
154
+ // The spinner timer already advances elapsed time in place; the heartbeat's
155
+ // job here is to fold in the latest streamed-output volume and redraw.
156
+ this.streamedChars = streamedChars;
157
+ this.redraw();
158
+ }
130
159
  finish(line) {
131
160
  if (this.statusShown)
132
161
  this.out.write(CLEAR_LINE);
133
- this.out.write(line + '\n');
162
+ this.out.write(styleOutcome(line) + '\n');
134
163
  this.suspend();
135
164
  }
136
165
  /** Release the status line (stop the spinner, restore the cursor) but stay usable. */
@@ -161,9 +190,12 @@ export class InteractiveRenderer {
161
190
  * (AC-2.4): the only thing a --json invocation writes to stdout is its JSON.
162
191
  */
163
192
  export function makeRenderer(opts) {
193
+ const interactive = !opts.json && !opts.plain && Boolean(process.stdout.isTTY);
194
+ // Color tracks the interactive path so --plain / pipes stay zero-ANSI (AC-8.9).
195
+ setColorEnabled(interactive && !process.env.NO_COLOR);
164
196
  if (opts.json)
165
197
  return plainRenderer((line) => console.error(line));
166
- if (!opts.plain && process.stdout.isTTY)
198
+ if (interactive)
167
199
  return new InteractiveRenderer();
168
200
  return plainRenderer((line) => console.log(line));
169
201
  }
@@ -1 +1 @@
1
- {"version":3,"file":"render.js","sourceRoot":"","sources":["../../src/agent/render.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../../src/agent/render.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,81 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync } from 'node:fs';
3
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ /**
6
+ * Wraps a provider so each turn's `(messages, tools) -> Turn` is written to disk
7
+ * and replayed on an identical later call. This makes the pipeline cheap and
8
+ * fast to recover: a stage that is retried after a transient failure (a timed-out
9
+ * turn, a crash, an auto-retry) replays the responses it already paid for, from
10
+ * turn 1 up to the point where the inputs first diverge, instead of re-calling
11
+ * the model. When a retry deliberately changes the prompt (e.g. diagnosis
12
+ * guidance is appended), the input hash changes and the model is called fresh —
13
+ * so caching never pins a run to a stale, failing response.
14
+ *
15
+ * Best-effort by construction: a cache miss or any I/O error falls through to the
16
+ * live provider, and a hit reports zero token usage (the real spend was zero).
17
+ * The key is a content hash of the full message history and the advertised tool
18
+ * names, so any change to the conversation or available tools is a fresh call.
19
+ */
20
+ export class CachingProvider {
21
+ inner;
22
+ dir;
23
+ log;
24
+ modelId;
25
+ name;
26
+ hits = 0;
27
+ /** Turns served from the on-disk cache so far (5.2: per-stage cache-hit%). */
28
+ get cacheHits() {
29
+ return this.hits;
30
+ }
31
+ constructor(inner, dir, log,
32
+ /** The concrete model id this run resolved to (e.g. `claude-code:opus`), used
33
+ * in the cache key so switching model on the same repo does not replay the
34
+ * other model's cached turns (F6). Falls back to the provider family name. */
35
+ modelId) {
36
+ this.inner = inner;
37
+ this.dir = dir;
38
+ this.log = log;
39
+ this.modelId = modelId;
40
+ this.name = inner.name;
41
+ }
42
+ keyFor(messages, tools) {
43
+ return createHash('sha256')
44
+ .update(JSON.stringify({ model: this.modelId ?? this.name, messages, tools: tools.map((t) => t.name) }))
45
+ .digest('hex');
46
+ }
47
+ async chat(messages, tools, opts) {
48
+ const file = path.join(this.dir, `${this.keyFor(messages, tools)}.json`);
49
+ if (existsSync(file)) {
50
+ try {
51
+ const cached = JSON.parse(await readFile(file, 'utf8'));
52
+ this.hits++;
53
+ this.log?.(`llm-cache: replayed a cached response (hit #${this.hits}, no tokens spent)`);
54
+ // Report zero usage: replaying a cached turn costs nothing.
55
+ return { ...cached, usage: { inputTokens: 0, outputTokens: 0 } };
56
+ }
57
+ catch {
58
+ // corrupt/partial cache file — fall through and regenerate
59
+ }
60
+ }
61
+ const turn = await this.inner.chat(messages, tools, opts);
62
+ try {
63
+ await mkdir(this.dir, { recursive: true });
64
+ // Keep the cache out of git entirely (and out of failed-run stashes): a
65
+ // `*` .gitignore in the cache dir hides every entry, so the cache persists
66
+ // across runs without ever dirtying the tree.
67
+ const ignore = path.join(this.dir, '.gitignore');
68
+ if (!existsSync(ignore))
69
+ await writeFile(ignore, '*\n', 'utf8');
70
+ await writeFile(file, JSON.stringify(turn), 'utf8');
71
+ }
72
+ catch {
73
+ // best-effort: caching must never break a run
74
+ }
75
+ return turn;
76
+ }
77
+ async close() {
78
+ await this.inner.close?.();
79
+ }
80
+ }
81
+ //# sourceMappingURL=response-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"response-cache.js","sourceRoot":"","sources":["../../src/agent/response-cache.ts"],"names":[],"mappings":"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"}
@@ -78,14 +78,14 @@ export async function collectRunMeta(opts) {
78
78
  };
79
79
  }
80
80
  const unk = (v) => v ?? 'unknown';
81
- /** ≤ 2 lines, printed before the first turn (AC-8.4). */
81
+ /** ≤ 2 lines, printed before the first turn (AC-8.4).
82
+ * Live header stays compact: install path / platform live in summary.md only. */
82
83
  export function renderCliHeader(meta) {
83
84
  const v = meta.versions;
84
85
  const line1 = [
85
- `copperhead v${unk(v.copperhead)}${v.installPath ? ` (${v.installPath})` : ''}`,
86
+ `copperhead v${unk(v.copperhead)}`,
86
87
  `kicad-cli ${unk(v.kicadCli)}`,
87
88
  `node ${v.node}`,
88
- v.platform,
89
89
  ].join(' · ');
90
90
  const repoState = meta.git.dirty === null
91
91
  ? 'unknown'
@@ -93,12 +93,11 @@ export function renderCliHeader(meta) {
93
93
  ? `dirty(${meta.git.uncommittedFiles})`
94
94
  : 'clean';
95
95
  const line2 = [
96
- `run ${meta.runId}`,
97
96
  unk(meta.command),
98
97
  ...(meta.stage ? [`stage ${meta.stage.name} (${meta.stage.index}/${meta.stage.total})`] : []),
99
98
  `model ${meta.model} (${meta.provider}, via ${unk(meta.modelSource)})`,
100
99
  `turns ≤${meta.config.maxTurns}`,
101
- `repo ${unk(meta.git.branch)}@${meta.git.commit?.slice(0, 7) ?? 'unknown'} ${repoState}`,
100
+ `${unk(meta.git.branch)}@${meta.git.commit?.slice(0, 7) ?? 'unknown'} ${repoState}`,
102
101
  ].join(' · ');
103
102
  return [line1, line2];
104
103
  }