codehere 1.0.3 → 2.0.0-beta.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.
- package/LICENSE +1 -2
- package/README.md +90 -45
- package/bin/codehere.js +90 -0
- package/package.json +20 -64
- package/src/brain.js +186 -0
- package/src/commands.js +445 -0
- package/src/gemma.js +48 -0
- package/src/hooks.js +442 -0
- package/src/signals.js +186 -0
- package/dist/application/use-cases/check-claimed-tests.js +0 -81
- package/dist/application/use-cases/detect-test-weakening.js +0 -237
- package/dist/application/use-cases/rerun-tests.js +0 -329
- package/dist/index.js +0 -111
- package/dist/infrastructure/orchestration/artifacts-extractor.js +0 -66
- package/dist/presentation/cli/commands/check-command.js +0 -251
- package/dist/presentation/cli/commands/help-command.js +0 -77
- package/dist/ui.js +0 -53
- package/dist/utils/typo-guard.js +0 -133
- package/dist/utils/version.js +0 -40
package/src/commands.js
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import {
|
|
5
|
+
findRoot, paths, ensureDirs, readConfig, writeConfig, readState, writeState,
|
|
6
|
+
readRules, listDocs, parseDoc, renderDoc, slug, stamp, today, git, isGitRepo, me,
|
|
7
|
+
} from './brain.js';
|
|
8
|
+
import { redact } from './signals.js';
|
|
9
|
+
import { summarize } from './gemma.js';
|
|
10
|
+
import { transcriptTokens } from './hooks.js';
|
|
11
|
+
|
|
12
|
+
const HOOK_EVENTS = {
|
|
13
|
+
SessionStart: { cmd: 'codehere hook session-start' },
|
|
14
|
+
UserPromptSubmit: { cmd: 'codehere hook prompt', timeout: 10 },
|
|
15
|
+
PostToolUse: { cmd: 'codehere hook post-tool', matcher: 'Bash' },
|
|
16
|
+
PostToolUseFailure: { cmd: 'codehere hook post-tool', matcher: 'Bash' },
|
|
17
|
+
Stop: { cmd: 'codehere hook stop', timeout: 180 },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function requireRoot() {
|
|
21
|
+
const root = findRoot();
|
|
22
|
+
if (!root) {
|
|
23
|
+
console.error('No .codehere folder here. Run `codehere init` in your project first.');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
ensureDirs(root); // git does not keep empty folders, so a fresh clone may lack some
|
|
27
|
+
return root;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function detectTestCommand(root) {
|
|
31
|
+
try {
|
|
32
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
33
|
+
const t = pkg.scripts && pkg.scripts.test;
|
|
34
|
+
if (t && !/no test specified/.test(t)) return 'npm test --silent';
|
|
35
|
+
} catch {}
|
|
36
|
+
if (fs.existsSync(path.join(root, 'pyproject.toml')) || fs.existsSync(path.join(root, 'pytest.ini'))) return 'pytest -q';
|
|
37
|
+
if (fs.existsSync(path.join(root, 'go.mod'))) return 'go test ./...';
|
|
38
|
+
if (fs.existsSync(path.join(root, 'Cargo.toml'))) return 'cargo test --quiet';
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function mergeClaudeSettings(root) {
|
|
43
|
+
const dir = path.join(root, '.claude');
|
|
44
|
+
const file = path.join(dir, 'settings.json');
|
|
45
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
46
|
+
let settings = {};
|
|
47
|
+
try {
|
|
48
|
+
settings = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
49
|
+
} catch {}
|
|
50
|
+
settings.hooks = settings.hooks || {};
|
|
51
|
+
for (const [event, spec] of Object.entries(HOOK_EVENTS)) {
|
|
52
|
+
const list = (settings.hooks[event] = settings.hooks[event] || []);
|
|
53
|
+
const already = list.some((g) => (g.hooks || []).some((h) => h.command === spec.cmd));
|
|
54
|
+
if (already) continue;
|
|
55
|
+
const hook = { type: 'command', command: spec.cmd };
|
|
56
|
+
if (spec.timeout) hook.timeout = spec.timeout;
|
|
57
|
+
const group = { hooks: [hook] };
|
|
58
|
+
if (spec.matcher) group.matcher = spec.matcher;
|
|
59
|
+
list.push(group);
|
|
60
|
+
}
|
|
61
|
+
fs.writeFileSync(file, JSON.stringify(settings, null, 2) + '\n');
|
|
62
|
+
return file;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function addGitignore(root) {
|
|
66
|
+
const file = path.join(root, '.gitignore');
|
|
67
|
+
const line = '.codehere/local/';
|
|
68
|
+
const cur = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
69
|
+
if (!cur.split('\n').includes(line)) {
|
|
70
|
+
fs.writeFileSync(file, cur + (cur && !cur.endsWith('\n') ? '\n' : '') + line + '\n');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------- init ----------
|
|
75
|
+
|
|
76
|
+
export function init(flags) {
|
|
77
|
+
const root = process.cwd();
|
|
78
|
+
const p = ensureDirs(root);
|
|
79
|
+
const cfg = readConfig(root);
|
|
80
|
+
if (flags.test) cfg.testCommand = flags.test;
|
|
81
|
+
else if (!cfg.testCommand) cfg.testCommand = detectTestCommand(root);
|
|
82
|
+
writeConfig(root, cfg);
|
|
83
|
+
|
|
84
|
+
if (!fs.existsSync(p.rules)) {
|
|
85
|
+
fs.writeFileSync(
|
|
86
|
+
p.rules,
|
|
87
|
+
'# Team rules\n\nOne rule per line, starting with "- ". Written by people, not by the agent.\nEvery agent session on this repo loads these.\n\n'
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
fs.writeFileSync(
|
|
91
|
+
path.join(p.base, 'README.md'),
|
|
92
|
+
[
|
|
93
|
+
'# .codehere — this team\'s brain',
|
|
94
|
+
'',
|
|
95
|
+
'Plain markdown, shared through git. Commit and push it like code.',
|
|
96
|
+
'',
|
|
97
|
+
'- `rules.md`: team rules every agent session loads',
|
|
98
|
+
'- `lessons/`: errors someone already solved; shown to the agent only when the same error appears',
|
|
99
|
+
'- `help/`: help cards; teammates see them with `codehere inbox`',
|
|
100
|
+
'- `briefs/`: restart briefs for fresh sessions',
|
|
101
|
+
'- `local/`: this machine only (git-ignored)',
|
|
102
|
+
'',
|
|
103
|
+
].join('\n')
|
|
104
|
+
);
|
|
105
|
+
for (const d of [p.lessons, p.help, p.briefs]) {
|
|
106
|
+
const keep = path.join(d, '.gitkeep');
|
|
107
|
+
if (!fs.existsSync(keep)) fs.writeFileSync(keep, '');
|
|
108
|
+
}
|
|
109
|
+
addGitignore(root);
|
|
110
|
+
const settingsFile = mergeClaudeSettings(root);
|
|
111
|
+
|
|
112
|
+
console.log('Codehere is set up.');
|
|
113
|
+
console.log(`- Team brain: ${path.relative(root, p.base)}/`);
|
|
114
|
+
console.log(`- Claude Code hooks: ${path.relative(root, settingsFile)}`);
|
|
115
|
+
console.log(`- Test check on "done": ${cfg.testCommand ? cfg.testCommand : 'none found (set one with: codehere init --test "npm test")'}`);
|
|
116
|
+
console.log('');
|
|
117
|
+
console.log('Next: commit .codehere/ and .claude/settings.json so your team gets it too.');
|
|
118
|
+
console.log('Then just use Claude Code as usual. Codehere stays quiet unless it helps.');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------- rules ----------
|
|
122
|
+
|
|
123
|
+
export function rule(text) {
|
|
124
|
+
const root = requireRoot();
|
|
125
|
+
if (!text) {
|
|
126
|
+
const rules = readRules(root);
|
|
127
|
+
console.log(rules.length ? rules.map((r) => `- ${r}`).join('\n') : 'No team rules yet. Add one: codehere rule "Never edit migrations by hand"');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
fs.appendFileSync(paths(root).rules, `- ${text} (${me(root)}, ${today()})\n`);
|
|
131
|
+
console.log('Rule added. Every new agent session will follow it. Commit .codehere/rules.md to share it.');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---------- lessons ----------
|
|
135
|
+
|
|
136
|
+
export function lesson(title, flags) {
|
|
137
|
+
const root = requireRoot();
|
|
138
|
+
const dir = paths(root).lessons;
|
|
139
|
+
if (!title) {
|
|
140
|
+
const docs = listDocs(dir);
|
|
141
|
+
if (!docs.length) return console.log('No lessons yet. They are created when someone solves a repeated error.');
|
|
142
|
+
for (const d of docs) console.log(`- ${d.meta.title || d.id} (${d.meta.author || '?'}, ${d.meta.date || ''})`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (!flags.fix) {
|
|
146
|
+
console.error('Add the fix: codehere lesson "<problem>" --fix "<what fixed it>" [--sig "<error text>"]');
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
const file = path.join(dir, `${today()}-${slug(title)}.md`);
|
|
150
|
+
fs.writeFileSync(
|
|
151
|
+
file,
|
|
152
|
+
renderDoc(
|
|
153
|
+
{ title, signature: flags.sig || title, author: me(root), date: today() },
|
|
154
|
+
`## Fix\n- ${flags.fix}\n`
|
|
155
|
+
)
|
|
156
|
+
);
|
|
157
|
+
console.log(`Lesson saved: ${path.relative(root, file)}. Commit it to share with your team.`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---------- transcript reading ----------
|
|
161
|
+
|
|
162
|
+
function userPrompts(transcript) {
|
|
163
|
+
if (!transcript || !fs.existsSync(transcript)) return [];
|
|
164
|
+
const prompts = [];
|
|
165
|
+
for (const line of fs.readFileSync(transcript, 'utf8').split('\n')) {
|
|
166
|
+
if (!line.trim()) continue;
|
|
167
|
+
let rec;
|
|
168
|
+
try {
|
|
169
|
+
rec = JSON.parse(line);
|
|
170
|
+
} catch {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (rec.type !== 'user' || !rec.message) continue;
|
|
174
|
+
const c = rec.message.content;
|
|
175
|
+
let text = '';
|
|
176
|
+
if (typeof c === 'string') text = c;
|
|
177
|
+
else if (Array.isArray(c)) text = c.filter((b) => b && b.type === 'text').map((b) => b.text).join('\n');
|
|
178
|
+
text = text.trim();
|
|
179
|
+
if (!text || text.startsWith('<') || text.startsWith('Caveat:')) continue;
|
|
180
|
+
prompts.push(text);
|
|
181
|
+
}
|
|
182
|
+
return prompts;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// The most recent session that actually has something in it
|
|
186
|
+
// (a transcript or errors), not just a stray hook call.
|
|
187
|
+
function lastSession(root) {
|
|
188
|
+
const state = readState(root);
|
|
189
|
+
const meaningful = (x) => x && (x.transcript || Object.keys(x.errors || {}).length);
|
|
190
|
+
let s = state.sessions[state.lastSession];
|
|
191
|
+
if (!meaningful(s)) {
|
|
192
|
+
s =
|
|
193
|
+
Object.values(state.sessions)
|
|
194
|
+
.filter(meaningful)
|
|
195
|
+
.sort((a, b) => (b.lastActive || b.startedAt || '').localeCompare(a.lastActive || a.startedAt || ''))[0] || null;
|
|
196
|
+
}
|
|
197
|
+
return { state, s };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const times = (n) => (n === 1 ? 'once' : `${n} times`);
|
|
201
|
+
|
|
202
|
+
function repeatedErrors(s) {
|
|
203
|
+
if (!s) return [];
|
|
204
|
+
return Object.entries(s.errors || {})
|
|
205
|
+
.sort((a, b) => b[1] - a[1])
|
|
206
|
+
.slice(0, 3)
|
|
207
|
+
.map(([sig, n]) => ({ sig, n, raw: (s.rawErrors || {})[sig] || [] }));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function gitSummary(root, base) {
|
|
211
|
+
if (!isGitRepo(root)) return { branch: '', stat: '', head: '' };
|
|
212
|
+
return {
|
|
213
|
+
branch: git(root, 'rev-parse --abbrev-ref HEAD'),
|
|
214
|
+
head: git(root, 'log -1 --format="%h %s"'),
|
|
215
|
+
stat: git(root, `diff ${base || 'HEAD'} --stat`).split('\n').slice(-12).join('\n'),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ---------- brief ----------
|
|
220
|
+
|
|
221
|
+
export async function brief(flags) {
|
|
222
|
+
const root = requireRoot();
|
|
223
|
+
const cfg = readConfig(root);
|
|
224
|
+
const { state, s } = lastSession(root);
|
|
225
|
+
const prompts = userPrompts(s && s.transcript);
|
|
226
|
+
const goal = flags.goal || prompts[0] || '(write the goal here)';
|
|
227
|
+
const corrections = prompts.slice(1).slice(-5);
|
|
228
|
+
const errs = repeatedErrors(s);
|
|
229
|
+
const g = gitSummary(root, s && s.startHead);
|
|
230
|
+
const rules = readRules(root);
|
|
231
|
+
|
|
232
|
+
const lines = [`# Restart brief (${today()})`, '', '## Goal', goal, ''];
|
|
233
|
+
if (corrections.length) {
|
|
234
|
+
lines.push('## What the user clarified or corrected along the way');
|
|
235
|
+
corrections.forEach((c) => lines.push(`- ${c.replace(/\s+/g, ' ').slice(0, 300)}`));
|
|
236
|
+
lines.push('');
|
|
237
|
+
}
|
|
238
|
+
if (rules.length) {
|
|
239
|
+
lines.push('## Team rules');
|
|
240
|
+
rules.forEach((r) => lines.push(`- ${r}`));
|
|
241
|
+
lines.push('');
|
|
242
|
+
}
|
|
243
|
+
if (errs.length) {
|
|
244
|
+
lines.push('## What kept failing (do not repeat these attempts)');
|
|
245
|
+
errs.forEach((e) => lines.push(`- ${e.sig} (seen ${times(e.n)})`));
|
|
246
|
+
lines.push('');
|
|
247
|
+
}
|
|
248
|
+
lines.push('## Current state');
|
|
249
|
+
if (g.branch) lines.push(`- Branch: ${g.branch}, last commit: ${g.head}`);
|
|
250
|
+
if (state.lastGreen) lines.push(`- Last time tests passed: commit ${state.lastGreen.commit.slice(0, 8)} (${state.lastGreen.at.slice(0, 16)})`);
|
|
251
|
+
if (g.stat) lines.push('- Changed files:', '```', g.stat, '```');
|
|
252
|
+
lines.push('', '## How to work', '- Start by stating your root-cause hypothesis before editing.', '- Make one small change, then run the tests.');
|
|
253
|
+
|
|
254
|
+
let text = redact(lines.join('\n'));
|
|
255
|
+
if (flags.gemma) {
|
|
256
|
+
const summary = await summarize(
|
|
257
|
+
`Rewrite this into one clear, complete instruction for a coding agent. Keep every requirement and constraint. Plain English, max 120 words.\n\n${text}`,
|
|
258
|
+
cfg.gemmaModel
|
|
259
|
+
);
|
|
260
|
+
if (summary) text = text.replace('## Goal', `## In one paragraph (written by ${summary.model} on this machine; an opinion, check it)\n${summary.text}\n\n## Goal`);
|
|
261
|
+
else console.error('(Gemma not reachable through Ollama; using the plain brief.)');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const file = path.join(paths(root).briefs, `${stamp()}.md`);
|
|
265
|
+
fs.writeFileSync(file, text + '\n');
|
|
266
|
+
state.pendingBrief = file;
|
|
267
|
+
writeState(root, state);
|
|
268
|
+
console.log(text);
|
|
269
|
+
console.log('');
|
|
270
|
+
console.log(`Saved to ${path.relative(root, file)}.`);
|
|
271
|
+
console.log('Edit it if needed, then start a fresh session (/clear or a new terminal). Codehere loads it automatically.');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ---------- help cards ----------
|
|
275
|
+
|
|
276
|
+
export async function help(question, flags) {
|
|
277
|
+
const root = requireRoot();
|
|
278
|
+
const cfg = readConfig(root);
|
|
279
|
+
const { s } = lastSession(root);
|
|
280
|
+
const prompts = userPrompts(s && s.transcript);
|
|
281
|
+
const errs = repeatedErrors(s);
|
|
282
|
+
const g = gitSummary(root, s && s.startHead);
|
|
283
|
+
const ask = question || (errs[0] ? `Stuck on: ${errs[0].sig}` : 'I am stuck and could use a second pair of eyes');
|
|
284
|
+
|
|
285
|
+
const body = ['## Question', ask, ''];
|
|
286
|
+
if (prompts[0]) body.push('## What I am trying to build', prompts[0].slice(0, 600), '');
|
|
287
|
+
if (errs.length) {
|
|
288
|
+
body.push('## The error');
|
|
289
|
+
errs.slice(0, 2).forEach((e) => {
|
|
290
|
+
body.push(`Seen ${times(e.n)}: \`${e.sig}\``);
|
|
291
|
+
if (e.raw.length) body.push('```', ...e.raw.slice(0, 6), '```');
|
|
292
|
+
});
|
|
293
|
+
body.push('');
|
|
294
|
+
}
|
|
295
|
+
const tried = prompts.slice(1).slice(-4);
|
|
296
|
+
if (tried.length) {
|
|
297
|
+
body.push('## What I already tried (my prompts to the agent)');
|
|
298
|
+
tried.forEach((t) => body.push(`- ${t.replace(/\s+/g, ' ').slice(0, 200)}`));
|
|
299
|
+
body.push('');
|
|
300
|
+
}
|
|
301
|
+
if (g.stat) body.push('## Files changed', '```', g.stat, '```', '');
|
|
302
|
+
body.push('## Setup', `- ${os.platform()} ${os.arch()}, Node ${process.version}${g.branch ? `, branch ${g.branch}` : ''}`, '');
|
|
303
|
+
|
|
304
|
+
let text = redact(body.join('\n'));
|
|
305
|
+
if (flags.gemma) {
|
|
306
|
+
const summary = await summarize(
|
|
307
|
+
`Summarise this help request for a busy teammate in 2 sentences: what is wrong and what you need from them.\n\n${text}`,
|
|
308
|
+
cfg.gemmaModel
|
|
309
|
+
);
|
|
310
|
+
if (summary) text = `## In short (written by ${summary.model} on this machine; an opinion, check it)\n${summary.text}\n\n${text}`;
|
|
311
|
+
else console.error('(Gemma not reachable through Ollama; using the plain help card.)');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const id = `${stamp()}-${slug(ask, 30)}`;
|
|
315
|
+
const file = path.join(paths(root).help, `${id}.md`);
|
|
316
|
+
fs.writeFileSync(
|
|
317
|
+
file,
|
|
318
|
+
renderDoc(
|
|
319
|
+
{ id, author: me(root), date: today(), status: 'open', signature: errs[0] ? errs[0].sig : '' },
|
|
320
|
+
text
|
|
321
|
+
)
|
|
322
|
+
);
|
|
323
|
+
console.log(text);
|
|
324
|
+
console.log('---');
|
|
325
|
+
console.log(`Help card saved: ${path.relative(root, file)} (secrets and emails redacted, please still check).`);
|
|
326
|
+
console.log('Share it one of two ways:');
|
|
327
|
+
console.log(' 1. Commit and push. Teammates see it with `codehere inbox` and answer with `codehere reply`.');
|
|
328
|
+
console.log(' 2. Copy the text above into WhatsApp, Discord or Slack.');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function inbox(flags) {
|
|
332
|
+
const root = requireRoot();
|
|
333
|
+
const mine = me(root);
|
|
334
|
+
const docs = listDocs(paths(root).help).filter(
|
|
335
|
+
(d) => (flags.all || d.meta.author !== mine) && (flags.all || d.meta.status === 'open')
|
|
336
|
+
);
|
|
337
|
+
if (!docs.length) return console.log('No open help requests from teammates. (Did you `git pull`?)');
|
|
338
|
+
for (const d of docs) {
|
|
339
|
+
const q = (d.body.match(/## Question\n(.+)/) || [null, ''])[1];
|
|
340
|
+
const replies = (d.body.match(/^- .+ \(\d{4}-\d\d-\d\d\): /gm) || []).length;
|
|
341
|
+
console.log(`${d.id}\n ${d.meta.author} · ${d.meta.date} · ${d.meta.status} · ${replies} repl${replies === 1 ? 'y' : 'ies'}\n ${q}\n`);
|
|
342
|
+
}
|
|
343
|
+
console.log('Read one: codehere show <id> Answer: codehere reply <id> "your answer" [--solved]');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function findHelp(root, id) {
|
|
347
|
+
const docs = listDocs(paths(root).help);
|
|
348
|
+
const d = docs.find((x) => x.id === id) || docs.find((x) => x.id.startsWith(id));
|
|
349
|
+
if (!d) {
|
|
350
|
+
console.error(`No help card matching "${id}". Run codehere inbox --all to see them.`);
|
|
351
|
+
process.exit(1);
|
|
352
|
+
}
|
|
353
|
+
return d;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function show(id) {
|
|
357
|
+
const root = requireRoot();
|
|
358
|
+
const d = findHelp(root, id);
|
|
359
|
+
console.log(fs.readFileSync(d.file, 'utf8'));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function reply(id, text, flags) {
|
|
363
|
+
const root = requireRoot();
|
|
364
|
+
if (!id || !text) {
|
|
365
|
+
console.error('Usage: codehere reply <id> "your answer" [--solved]');
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
const d = findHelp(root, id);
|
|
369
|
+
const { meta, body } = parseDoc(fs.readFileSync(d.file, 'utf8'));
|
|
370
|
+
const who = me(root);
|
|
371
|
+
let newBody = body.includes('## Replies') ? body.trimEnd() : `${body.trimEnd()}\n\n## Replies`;
|
|
372
|
+
newBody += `\n- ${who} (${today()}): ${text}\n`;
|
|
373
|
+
if (flags.solved) {
|
|
374
|
+
meta.status = 'resolved';
|
|
375
|
+
meta.resolvedBy = who;
|
|
376
|
+
const q = (body.match(/## Question\n(.+)/) || [null, meta.id])[1];
|
|
377
|
+
const lessonFile = path.join(paths(root).lessons, `${today()}-${slug(q)}.md`);
|
|
378
|
+
fs.writeFileSync(
|
|
379
|
+
lessonFile,
|
|
380
|
+
renderDoc(
|
|
381
|
+
{ title: q, signature: meta.signature || q, author: who, date: today(), from: `help/${d.id}` },
|
|
382
|
+
`## Fix\n- ${text}\n`
|
|
383
|
+
)
|
|
384
|
+
);
|
|
385
|
+
console.log(`Marked solved and saved as a team lesson: ${path.relative(root, lessonFile)}`);
|
|
386
|
+
console.log('From now on, any teammate\'s agent that hits this error gets your fix automatically.');
|
|
387
|
+
}
|
|
388
|
+
fs.writeFileSync(d.file, renderDoc(meta, newBody));
|
|
389
|
+
console.log(`Reply added to ${path.relative(root, d.file)}. Commit and push so ${meta.author} sees it.`);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ---------- status ----------
|
|
393
|
+
|
|
394
|
+
export function status() {
|
|
395
|
+
const root = requireRoot();
|
|
396
|
+
const cfg = readConfig(root);
|
|
397
|
+
const { state, s } = lastSession(root);
|
|
398
|
+
const p = paths(root);
|
|
399
|
+
console.log('Codehere status');
|
|
400
|
+
console.log(`- Team rules: ${readRules(root).length}`);
|
|
401
|
+
console.log(`- Team lessons: ${listDocs(p.lessons).length}`);
|
|
402
|
+
console.log(`- Open help requests: ${listDocs(p.help).filter((d) => d.meta.status === 'open').length}`);
|
|
403
|
+
console.log(`- Test check on "done": ${cfg.testCommand || 'off'}`);
|
|
404
|
+
if (state.lastGreen) console.log(`- Last green: ${state.lastGreen.commit.slice(0, 8)} at ${state.lastGreen.at.slice(0, 16)}`);
|
|
405
|
+
if (s) {
|
|
406
|
+
const errs = repeatedErrors(s);
|
|
407
|
+
console.log(`- This session: ${errs.length ? errs.map((e) => `${e.n}x "${e.sig}"`).join('; ') : 'no repeated errors'}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ---------- stats ----------
|
|
412
|
+
|
|
413
|
+
const median = (xs) => {
|
|
414
|
+
const a = [...xs].sort((x, y) => x - y);
|
|
415
|
+
const m = Math.floor(a.length / 2);
|
|
416
|
+
return a.length % 2 ? a[m] : (a[m - 1] + a[m]) / 2;
|
|
417
|
+
};
|
|
418
|
+
const k = (n) => `${Math.round(n / 1000)}k`;
|
|
419
|
+
const fixes = (n) => (n === 1 ? '1 fix' : `${n} fixes`);
|
|
420
|
+
|
|
421
|
+
export function stats() {
|
|
422
|
+
const root = requireRoot();
|
|
423
|
+
const state = readState(root);
|
|
424
|
+
const sessions = Object.values(state.sessions || {}).filter(
|
|
425
|
+
(x) => x.prompts || x.transcript || Object.keys(x.errors || {}).length
|
|
426
|
+
);
|
|
427
|
+
const greens = sessions.map((x) => x.firstGreenAt).filter((n) => n > 0);
|
|
428
|
+
const loops = sessions.reduce((n, x) => n + (x.loopsStopped || 0), 0);
|
|
429
|
+
const sizes = sessions.map(transcriptTokens).filter(Boolean);
|
|
430
|
+
const turns = (kind) => (state.episodes || []).filter((e) => e.kind === kind).map((e) => e.turns);
|
|
431
|
+
const detail = turns('with-detail');
|
|
432
|
+
const short = turns('still-broken');
|
|
433
|
+
|
|
434
|
+
console.log(`Codehere stats · ${sessions.length} recent sessions on this machine`);
|
|
435
|
+
console.log(
|
|
436
|
+
`- Turns to green: ${greens.length ? `median ${median(greens)} prompts · ${greens.length} of ${sessions.length} sessions reached green` : 'no session has reached green yet'}`
|
|
437
|
+
);
|
|
438
|
+
console.log(`- Loops stopped: ${loops}`);
|
|
439
|
+
console.log(
|
|
440
|
+
`- Context size: ${sizes.length ? `largest ${k(Math.max(...sizes))} tokens · median ${k(median(sizes))} (last request of each session)` : 'no transcripts yet'}`
|
|
441
|
+
);
|
|
442
|
+
console.log(
|
|
443
|
+
`- Turns to fix a failure: ${detail.length ? `${median(detail)} when the prompt gave details (${fixes(detail.length)})` : 'no fix after a detailed prompt yet'} · ${short.length ? `${median(short)} after a short "still broken" (${fixes(short.length)})` : 'no fix after a short "still broken" yet'}`
|
|
444
|
+
);
|
|
445
|
+
}
|
package/src/gemma.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Optional: a small local model (Gemma 4 via Ollama) for writing summaries.
|
|
2
|
+
// It is never used to make decisions. If Ollama is not running, we fall back
|
|
3
|
+
// to the plain template, and the user always reviews the text before sharing.
|
|
4
|
+
|
|
5
|
+
const url = () => {
|
|
6
|
+
const host = process.env.OLLAMA_HOST || '127.0.0.1:11434';
|
|
7
|
+
return /^https?:\/\//.test(host) ? host : `http://${host}`; // Ollama allows OLLAMA_HOST without a scheme
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
async function post(path, body, ms) {
|
|
11
|
+
const ctrl = new AbortController();
|
|
12
|
+
const timer = setTimeout(() => ctrl.abort(), ms);
|
|
13
|
+
try {
|
|
14
|
+
const res = await fetch(`${url()}${path}`, {
|
|
15
|
+
method: body ? 'POST' : 'GET',
|
|
16
|
+
headers: { 'content-type': 'application/json' },
|
|
17
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
18
|
+
signal: ctrl.signal,
|
|
19
|
+
});
|
|
20
|
+
return res.ok ? await res.json() : null;
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
} finally {
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The configured model if it is installed, else the best small Gemma that is.
|
|
29
|
+
// Ollama tags vary (gemma4:e4b, gemma4:e4b-it-qat, ...).
|
|
30
|
+
export function pickModel(installed, wanted) {
|
|
31
|
+
if (installed.includes(wanted)) return wanted;
|
|
32
|
+
for (const re of [/^gemma4:e4b/, /^gemma4:e2b/, /^gemma4:12b/, /^gemma4:/, /^gemma/]) {
|
|
33
|
+
const hit = installed.find((n) => re.test(n) && !/embed/.test(n));
|
|
34
|
+
if (hit) return hit;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Returns { text, model } or null.
|
|
40
|
+
export async function summarize(prompt, wanted = 'gemma4:e4b') {
|
|
41
|
+
const tags = await post('/api/tags', null, 3000);
|
|
42
|
+
const model = tags ? pickModel((tags.models || []).map((m) => m.name), wanted) : null;
|
|
43
|
+
if (!model) return null;
|
|
44
|
+
// Thinking off: a summary does not need it, and it is several times slower.
|
|
45
|
+
const data = await post('/api/generate', { model, prompt, stream: false, think: false, options: { temperature: 0.2 } }, 60000);
|
|
46
|
+
const text = ((data && data.response) || '').trim();
|
|
47
|
+
return text ? { text, model } : null;
|
|
48
|
+
}
|