crewforth 3.1.0-rc.5 → 3.1.0-rc.7
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/CHANGELOG.md +76 -12
- package/README.md +3 -2
- package/kit/AGENT_TEMPLATE.md +41 -21
- package/kit/CLAUDE.md +9 -7
- package/kit/agents/crew-backend-expert.md +4 -1
- package/kit/agents/crew-commit-agent.md +8 -3
- package/kit/agents/crew-database-expert.md +4 -1
- package/kit/agents/crew-devops-expert.md +4 -1
- package/kit/agents/crew-frontend-expert.md +4 -1
- package/kit/agents/crew-performance-expert.md +4 -1
- package/kit/agents/crew-planner.md +10 -1
- package/kit/agents/crew-privacy-agent.md +4 -1
- package/kit/agents/crew-review-agent.md +4 -1
- package/kit/agents/crew-security-expert.md +4 -1
- package/kit/agents/crew-session-manager.md +4 -1
- package/kit/agents/crew-test-expert.md +4 -1
- package/kit/eval/smoke-test.sh +860 -27
- package/kit/hooks/agent-outcome.sh +437 -0
- package/kit/hooks/context-usage.sh +1 -1
- package/kit/hooks/guard-agent-model.sh +494 -60
- package/kit/hooks/guard-bash.sh +69 -8
- package/kit/hooks/guard-schedule.sh +8 -2
- package/kit/hooks/guard-write.sh +31 -0
- package/kit/hooks/prompt-approval.sh +94 -7
- package/kit/settings.json +43 -0
- package/kit/skills/crew-approve/SKILL.md +4 -1
- package/kit/skills/crew-loosen/SKILL.md +28 -0
- package/kit/studio/server/hooks/studio-gate.sh +90 -4
- package/kit/studio/server/hooks/studio-host.mjs +92 -0
- package/kit/studio/server/index.js +85 -6
- package/kit/studio/server/lib/agent-origin.js +44 -0
- package/kit/studio/server/lib/graph.js +45 -7
- package/kit/studio/server/lib/model-outcomes.js +287 -0
- package/kit/studio/server/lib/palette.js +5 -18
- package/kit/studio/server/lib/permissions.js +260 -17
- package/kit/studio/server/lib/session.js +41 -1
- package/kit/studio/server/lib/usage.js +8 -1
- package/kit/studio/web/app.js +203 -32
- package/kit/studio/web/approvals.js +53 -2
- package/kit/studio/web/ask.js +137 -0
- package/kit/studio/web/canvas.js +91 -42
- package/kit/studio/web/chat.js +11 -1
- package/kit/studio/web/convo.js +9 -0
- package/kit/studio/web/dock.js +30 -9
- package/kit/studio/web/graph-plan.js +29 -9
- package/kit/studio/web/icons/builtin.svg +1 -1
- package/kit/studio/web/index.html +11 -4
- package/kit/studio/web/keys.js +29 -0
- package/kit/studio/web/list-plan.js +8 -3
- package/kit/studio/web/list.js +27 -5
- package/kit/studio/web/models-plan.js +167 -0
- package/kit/studio/web/models.js +127 -0
- package/kit/studio/web/names.js +30 -0
- package/kit/studio/web/style.css +85 -9
- package/kit/studio/web/timeline-plan.js +8 -0
- package/kit/studio/web/timeline.js +26 -7
- package/package.json +1 -1
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// What Crewforth recorded about which model did which work, and how it went.
|
|
2
|
+
//
|
|
3
|
+
// Crewforth writes one line per finished crew agent to `.claude/state/model-outcomes.tsv` (hooks/agent-outcome.sh):
|
|
4
|
+
//
|
|
5
|
+
// ts agent_id agent change risk card model verify fixes escalated_from ran_on confidence
|
|
6
|
+
//
|
|
7
|
+
// Twelve columns, tab-separated, a header on the first line, "-" for an empty value, and any column added later
|
|
8
|
+
// is added at the end. This file reads that and adds nothing to it: a class's first-try success is counted from
|
|
9
|
+
// the rows, never estimated, and a row that says nothing about the work says nothing here either.
|
|
10
|
+
//
|
|
11
|
+
// class agent x change x risk
|
|
12
|
+
// verified verify is pass or fail: the command ran and gave a verdict
|
|
13
|
+
// first try verify = pass and fixes = 0
|
|
14
|
+
// not verified verify is none (no command), blocked (the command was not allowed to run) or timeout: no
|
|
15
|
+
// verdict about the work, so these are shown apart and are in no rate
|
|
16
|
+
// escalated escalated_from is not "-": this call repeats a card that failed one model down
|
|
17
|
+
//
|
|
18
|
+
// Crewforth also writes `.claude/state/crew-model-floors.auto`: a class held one model up because its first try
|
|
19
|
+
// failed too often. Lowering such a hold is the user's to approve, with a Crewforth command in their own session:
|
|
20
|
+
// Studio shows which holds could be lowered (`suggestionsOf`) and writes nothing. A panel's button is a request
|
|
21
|
+
// any process of the same user can send, so it cannot stand for the user.
|
|
22
|
+
|
|
23
|
+
import fs from 'node:fs';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
|
|
26
|
+
export const COLUMNS = ['ts', 'agent_id', 'agent', 'change', 'risk', 'card', 'model', 'verify', 'fixes', 'escalated_from', 'ran_on', 'confidence'];
|
|
27
|
+
const VERIFIED = new Set(['pass', 'fail']);
|
|
28
|
+
// What the hooks write, and nothing else is a row. The words are hooks/guard-agent-model.sh's and
|
|
29
|
+
// hooks/agent-outcome.sh's; a word they do not use is a line this file cannot read, not a new kind of work.
|
|
30
|
+
export const VERIFY = ['pass', 'fail', 'none', 'blocked', 'timeout'];
|
|
31
|
+
export const CHANGES = ['text', 'feature', 'fix-known', 'fix-unknown', 'refactor', 'migration', 'security', 'architecture', 'test-run', 'test-write', 'audit'];
|
|
32
|
+
export const RISKS = ['critical', 'normal'];
|
|
33
|
+
const TIERS = ['haiku', 'sonnet', 'opus', 'fable'];
|
|
34
|
+
const MAX_BYTES = 4 * 1024 * 1024; // the record is read whole; past this only its end is, and the answer says so
|
|
35
|
+
// How many of a held class's latest verified calls have to have passed first time before lowering it is offered.
|
|
36
|
+
// A starting point; it has not been measured.
|
|
37
|
+
export const LOOSEN_AFTER = 10;
|
|
38
|
+
const TRAIL_MAX = 6;
|
|
39
|
+
|
|
40
|
+
const stateDir = (cwd) => path.join(cwd, '.claude', 'state');
|
|
41
|
+
const dash = (v) => (v === '-' || v === '' || v == null ? null : v);
|
|
42
|
+
|
|
43
|
+
/** Is this line one the hooks could have written? Every field that is counted or compared has to be what they write. */
|
|
44
|
+
function rowProblem(f) {
|
|
45
|
+
if (f.length < COLUMNS.length) return 'fewer than twelve columns';
|
|
46
|
+
if (!/^[A-Za-z0-9._-]+$/.test(f[1])) return 'no agent id';
|
|
47
|
+
if (!Number.isFinite(Date.parse(f[0])) || !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ$/.test(f[0])) return 'ts is not a UTC time';
|
|
48
|
+
if (!f[2]) return 'no agent';
|
|
49
|
+
if (!CHANGES.includes(f[3])) return 'change is not one of the eleven';
|
|
50
|
+
if (!RISKS.includes(f[4])) return 'risk is not critical or normal';
|
|
51
|
+
if (f[6] !== '-' && !TIERS.includes(f[6])) return 'model is not a tier';
|
|
52
|
+
if (!VERIFY.includes(f[7])) return 'verify is not one of the five';
|
|
53
|
+
if (!/^\d{1,4}$/.test(f[8])) return 'fixes is not a count';
|
|
54
|
+
if (f[9] !== '-' && !TIERS.includes(f[9])) return 'escalated_from is not a tier';
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The rows of a model-outcomes.tsv. A line the hooks could not have written is dropped and counted, with the
|
|
60
|
+
* first reason of each kind kept so the view can say why the count is not zero.
|
|
61
|
+
*/
|
|
62
|
+
export function parseOutcomes(text) {
|
|
63
|
+
const lines = String(text ?? '').split('\n').map((l) => l.replace(/\r$/, '')).filter((l) => l !== '');
|
|
64
|
+
const out = { rows: [], dropped: 0, why: {}, header: false };
|
|
65
|
+
for (const [i, line] of lines.entries()) {
|
|
66
|
+
const f = line.split('\t');
|
|
67
|
+
if (i === 0 && f[0] === 'ts' && f[1] === 'agent_id') { out.header = true; continue; }
|
|
68
|
+
const problem = rowProblem(f);
|
|
69
|
+
if (problem) { out.dropped += 1; out.why[problem] = (out.why[problem] ?? 0) + 1; continue; }
|
|
70
|
+
const at = Date.parse(f[0]);
|
|
71
|
+
out.rows.push({
|
|
72
|
+
line: i, ts: f[0], at, agentId: f[1], agent: f[2], change: f[3], risk: f[4], card: dash(f[5]),
|
|
73
|
+
model: dash(f[6]), verify: f[7], fixes: Number.parseInt(f[8], 10), escalatedFrom: dash(f[9]), ranOn: dash(f[10]),
|
|
74
|
+
confidence: dash(f[11]),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The lines of crew-model-floors.auto: agent, change, risk, model, calls, not-first-try, when. Comments start with #. */
|
|
81
|
+
export function parseFloors(text) {
|
|
82
|
+
const out = [];
|
|
83
|
+
for (const line of String(text ?? '').split('\n')) {
|
|
84
|
+
const l = line.replace(/\r$/, '');
|
|
85
|
+
if (!l || l.startsWith('#')) continue;
|
|
86
|
+
const f = l.split('\t');
|
|
87
|
+
if (f.length < 4 || !TIERS.includes(f[3])) continue;
|
|
88
|
+
out.push({ agent: f[0], change: f[1], risk: f[2], model: f[3], calls: Number.parseInt(f[4], 10) || null, notFirstTry: Number.parseInt(f[5], 10) || 0, when: f[6] ?? null });
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The user's approvals to lower a held class: ts, agent, change, risk, from, to. The newest for a class is the one that counts. */
|
|
94
|
+
export function parseLoosened(text) {
|
|
95
|
+
const out = [];
|
|
96
|
+
for (const [i, line] of String(text ?? '').split('\n').entries()) {
|
|
97
|
+
const f = line.replace(/\r$/, '').split('\t');
|
|
98
|
+
if (i === 0 && f[0] === 'ts') continue;
|
|
99
|
+
if (f.length < 6 || !TIERS.includes(f[4]) || !TIERS.includes(f[5])) continue;
|
|
100
|
+
out.push({ ts: f[0], at: Date.parse(f[0]) || null, agent: f[1], change: f[2], risk: f[3], from: f[4], to: f[5] });
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const keyOf = (r) => `${r.agent}\t${r.change}\t${r.risk}`;
|
|
106
|
+
const firstTry = (r) => r.verify === 'pass' && r.fixes === 0;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The lowering that stands for a raised floor, or null. The rule is hooks/guard-agent-model.sh's (`_am_floor_now`),
|
|
110
|
+
* word for word: a lowering counts when it is not older than the raise it answers (the times are UTC text and are
|
|
111
|
+
* compared as text, as the hook does), is from the model the floor was raised to, and goes one model down. Raised
|
|
112
|
+
* again afterwards, the floor is the raised one. The last line that counts is the one in force.
|
|
113
|
+
*/
|
|
114
|
+
export function loweringOf(floor, loosened = []) {
|
|
115
|
+
let hit = null;
|
|
116
|
+
for (const l of loosened) {
|
|
117
|
+
if (keyOf(l) !== keyOf(floor)) continue;
|
|
118
|
+
if (String(l.ts) < String(floor.when ?? '')) continue;
|
|
119
|
+
if (l.from !== floor.model) continue;
|
|
120
|
+
if (TIERS.indexOf(l.to) < 0 || TIERS.indexOf(l.to) !== TIERS.indexOf(l.from) - 1) continue;
|
|
121
|
+
hit = { to: l.to, ts: l.ts };
|
|
122
|
+
}
|
|
123
|
+
return hit;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** One line per class, the busiest first. What the class cost is asked apart (`classCosts`): it is read from elsewhere. */
|
|
127
|
+
export function classesOf(rows, floors = [], loosened = []) {
|
|
128
|
+
const by = new Map();
|
|
129
|
+
for (const r of rows) {
|
|
130
|
+
const k = keyOf(r);
|
|
131
|
+
if (!by.has(k)) {
|
|
132
|
+
by.set(k, {
|
|
133
|
+
key: k, agent: r.agent, change: r.change, risk: r.risk, runs: 0, verified: 0, firstTry: 0, fixed: 0, failed: 0, escalated: 0,
|
|
134
|
+
notVerified: { none: 0, blocked: 0, timeout: 0 }, models: {}, ranOn: {}, lastAt: null, floor: null,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
const c = by.get(k);
|
|
138
|
+
c.runs += 1;
|
|
139
|
+
const m = (c.models[r.model ?? 'unknown'] ??= { runs: 0, verified: 0, firstTry: 0 });
|
|
140
|
+
m.runs += 1;
|
|
141
|
+
if (r.ranOn) c.ranOn[r.ranOn] = (c.ranOn[r.ranOn] ?? 0) + 1;
|
|
142
|
+
if (VERIFIED.has(r.verify)) {
|
|
143
|
+
c.verified += 1; m.verified += 1;
|
|
144
|
+
if (firstTry(r)) { c.firstTry += 1; m.firstTry += 1; }
|
|
145
|
+
if (r.verify === 'fail') c.failed += 1;
|
|
146
|
+
if (r.fixes > 0) c.fixed += 1;
|
|
147
|
+
} else c.notVerified[r.verify] += 1;
|
|
148
|
+
if (r.escalatedFrom) c.escalated += 1;
|
|
149
|
+
if (r.at !== null && (c.lastAt === null || r.at > c.lastAt)) c.lastAt = r.at;
|
|
150
|
+
}
|
|
151
|
+
// A floor the user lowered is still on record as raised; what holds now is the lowered one, and both are said.
|
|
152
|
+
for (const f of floors) { const c = by.get(keyOf(f)); if (c) c.floor = { ...f, lowered: loweringOf(f, loosened) }; }
|
|
153
|
+
return [...by.values()]
|
|
154
|
+
// A rate with nothing under it is not zero: with no verified call there is none.
|
|
155
|
+
.map((c) => ({ ...c, firstTryRate: c.verified ? c.firstTry / c.verified : null }))
|
|
156
|
+
.sort((a, b) => b.runs - a.runs || a.key.localeCompare(b.key));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* What each class's agents come to, where an agent's tokens could be read: { classKey: { cost, costed, runs } }.
|
|
161
|
+
* `costed` of `runs` were priced; the rest are not in the sum, so the sum is a floor and not the total.
|
|
162
|
+
* @param costOf (agentId) => USD or null
|
|
163
|
+
*/
|
|
164
|
+
export function classCosts(rows, costOf) {
|
|
165
|
+
const out = {};
|
|
166
|
+
for (const r of rows) {
|
|
167
|
+
const c = (out[keyOf(r)] ??= { cost: 0, costed: 0, runs: 0 });
|
|
168
|
+
c.runs += 1;
|
|
169
|
+
const usd = costOf(r.agentId);
|
|
170
|
+
if (typeof usd === 'number') { c.cost += usd; c.costed += 1; }
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The holds that could be lowered: a class the record of outcomes holds one model up (a line in
|
|
177
|
+
* crew-model-floors.auto), whose latest LOOSEN_AFTER verified calls on that model all passed the first time.
|
|
178
|
+
* Calls made before the user last approved a lowering of that class are not counted, and a hold already lowered
|
|
179
|
+
* since it was written is not offered again.
|
|
180
|
+
*/
|
|
181
|
+
export function suggestionsOf(rows, floors = [], loosened = [], need = LOOSEN_AFTER) {
|
|
182
|
+
const out = [];
|
|
183
|
+
for (const f of floors) {
|
|
184
|
+
const tier = TIERS.indexOf(f.model);
|
|
185
|
+
if (tier <= 0) continue;
|
|
186
|
+
const k = keyOf(f);
|
|
187
|
+
// Once for each raise: a floor whose lowering stands is not offered again (the same rule the gate reads it by).
|
|
188
|
+
if (loweringOf(f, loosened)) continue;
|
|
189
|
+
const since = Date.parse(f.when ?? '') || 0;
|
|
190
|
+
const recent = rows.filter((r) => keyOf(r) === k && r.model === f.model && VERIFIED.has(r.verify) && (r.at ?? 0) >= since)
|
|
191
|
+
.sort((a, b) => (b.at ?? 0) - (a.at ?? 0)).slice(0, need);
|
|
192
|
+
if (recent.length < need || !recent.every(firstTry)) continue;
|
|
193
|
+
out.push({ key: k, agent: f.agent, change: f.change, risk: f.risk, from: f.model, to: TIERS[tier - 1], passed: recent.length, heldSince: f.when ?? null });
|
|
194
|
+
}
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function readBounded(file) {
|
|
199
|
+
let st;
|
|
200
|
+
try { st = fs.statSync(file); } catch { return null; }
|
|
201
|
+
if (st.size <= MAX_BYTES) return { text: fs.readFileSync(file, 'utf8'), truncated: false, bytes: st.size };
|
|
202
|
+
const fd = fs.openSync(file, 'r');
|
|
203
|
+
try {
|
|
204
|
+
const buf = Buffer.alloc(MAX_BYTES);
|
|
205
|
+
fs.readSync(fd, buf, 0, MAX_BYTES, st.size - MAX_BYTES);
|
|
206
|
+
const text = buf.toString('utf8');
|
|
207
|
+
return { text: text.slice(text.indexOf('\n') + 1), truncated: true, bytes: st.size };
|
|
208
|
+
} finally { fs.closeSync(fd); }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const sigOf = (file) => { try { const st = fs.statSync(file); return `${st.mtimeMs}:${st.size}`; } catch { return '-'; } };
|
|
212
|
+
const FILES = ['model-outcomes.tsv', 'crew-model-floors.auto', 'crew-model-loosened.tsv'];
|
|
213
|
+
const records = new Map(); // cwd -> { sig, value }
|
|
214
|
+
export const _stats = { parsed: 0 }; // how many times a record was really read: the claims count it
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The record of one project, read once and kept until one of its three files changes (its time or its size).
|
|
218
|
+
* A view that asks every few seconds costs three stats, not a parse.
|
|
219
|
+
*/
|
|
220
|
+
export function readRecord(cwd) {
|
|
221
|
+
const dir = stateDir(cwd);
|
|
222
|
+
const sig = FILES.map((f) => sigOf(path.join(dir, f))).join('|');
|
|
223
|
+
const hit = records.get(cwd);
|
|
224
|
+
if (hit && hit.sig === sig) return hit.value;
|
|
225
|
+
const value = parseRecord(dir, cwd);
|
|
226
|
+
_stats.parsed += 1;
|
|
227
|
+
if (records.size > 50) records.clear();
|
|
228
|
+
records.set(cwd, { sig, value });
|
|
229
|
+
return value;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function parseRecord(dir, cwd) {
|
|
233
|
+
const record = readBounded(path.join(dir, FILES[0]));
|
|
234
|
+
if (!record) return { measured: false, reason: 'no .claude/state/model-outcomes.tsv in this project: no crew agent has finished here since model routing was installed', classes: [], suggestions: [], byAgent: {} };
|
|
235
|
+
const parsed = parseOutcomes(record.text);
|
|
236
|
+
const floors = parseFloors(readBounded(path.join(dir, FILES[1]))?.text ?? '');
|
|
237
|
+
const loosened = parseLoosened(readBounded(path.join(dir, FILES[2]))?.text ?? '');
|
|
238
|
+
const byAgent = {};
|
|
239
|
+
for (const r of parsed.rows) byAgent[r.agentId] = r; // a resumed agent's latest line is the one that stands
|
|
240
|
+
// A card that failed is given again one model up, to another agent: the calls of one card, oldest first, are
|
|
241
|
+
// the trail the inspector shows under the newest of them.
|
|
242
|
+
const byCard = new Map();
|
|
243
|
+
for (const r of parsed.rows) if (r.card) byCard.set(`${r.agent}\t${r.card}`, [...(byCard.get(`${r.agent}\t${r.card}`) ?? []), r]);
|
|
244
|
+
for (const r of Object.values(byAgent)) {
|
|
245
|
+
const same = r.card ? (byCard.get(`${r.agent}\t${r.card}`) ?? []) : [];
|
|
246
|
+
// Earlier in the file, not earlier by the clock: two lines can carry the same second.
|
|
247
|
+
r.before = same.filter((x) => x.agentId !== r.agentId && x.line < r.line).slice(-TRAIL_MAX)
|
|
248
|
+
.map(({ agentId, model, verify, fixes, ranOn }) => ({ agentId, model, verify, fixes, ranOn }));
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
measured: true,
|
|
252
|
+
cwd,
|
|
253
|
+
rows: parsed.rows.length,
|
|
254
|
+
dropped: parsed.dropped,
|
|
255
|
+
droppedWhy: parsed.why,
|
|
256
|
+
truncated: record.truncated,
|
|
257
|
+
firstAt: parsed.rows.reduce((m, r) => (m === null || r.at < m ? r.at : m), null),
|
|
258
|
+
classes: classesOf(parsed.rows, floors, loosened),
|
|
259
|
+
floors,
|
|
260
|
+
suggestions: suggestionsOf(parsed.rows, floors, loosened),
|
|
261
|
+
loosened,
|
|
262
|
+
byAgent,
|
|
263
|
+
all: parsed.rows,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* What the Models view and the inspector are sent for one project.
|
|
269
|
+
* @param agents the ids of the agents of the session on screen: only their lines are sent. Without it, none are:
|
|
270
|
+
* a project's record can hold every agent it ever ran, and a page shows one session's.
|
|
271
|
+
* @returns measured: false with a reason when the project has no record — which is a project where no crew agent
|
|
272
|
+
* has finished since model routing was installed, not a project where every agent failed.
|
|
273
|
+
*/
|
|
274
|
+
export function readModels(cwd, { agents = null } = {}) {
|
|
275
|
+
const rec = readRecord(cwd);
|
|
276
|
+
if (!rec.measured) return rec;
|
|
277
|
+
const byAgent = {};
|
|
278
|
+
for (const id of agents ?? []) if (Object.hasOwn(rec.byAgent, id)) byAgent[id] = rec.byAgent[id];
|
|
279
|
+
const { all, byAgent: _every, ...rest } = rec;
|
|
280
|
+
return { ...rest, byAgent };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** What the classes of one project cost, from the kept record: see `classCosts`. Null when there is no record. */
|
|
284
|
+
export function readCosts(cwd, costOf) {
|
|
285
|
+
const rec = readRecord(cwd);
|
|
286
|
+
return rec.measured ? classCosts(rec.all, costOf) : null;
|
|
287
|
+
}
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
// values below give each of those names one shade, so an agent keeps the same
|
|
5
5
|
// colour in every session the panel draws.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// The map holds Crewforth's own agents and nothing else: the ones in its own
|
|
8
|
+
// agents directory. There is no list of Claude Code's built-in agent types
|
|
9
|
+
// here. There was one, and it went stale the first time Claude Code added a
|
|
10
|
+
// type: its own "Workflow Subagent" was drawn as "not declared by Claude Code".
|
|
11
|
+
// Whatever is not Crewforth's is a Claude Code agent, whoever defined it.
|
|
10
12
|
|
|
11
13
|
import fs from 'node:fs';
|
|
12
14
|
import path from 'node:path';
|
|
@@ -46,18 +48,6 @@ const NAMED = {
|
|
|
46
48
|
yellow: '#db5f1c',
|
|
47
49
|
};
|
|
48
50
|
|
|
49
|
-
// Claude Code's built-in agent types are not kit components and declare
|
|
50
|
-
// nothing, so they take the diagram's "core" tones.
|
|
51
|
-
const BUILTIN = {
|
|
52
|
-
Explore: '#26c6e6',
|
|
53
|
-
Plan: '#a874f5',
|
|
54
|
-
'general-purpose': '#c79bff',
|
|
55
|
-
claude: '#5b8cff',
|
|
56
|
-
'code-simplifier': '#84e6b0',
|
|
57
|
-
'statusline-setup': '#94a3c8',
|
|
58
|
-
'claude-code-guide': '#8659ee',
|
|
59
|
-
};
|
|
60
|
-
|
|
61
51
|
const UNKNOWN = '#94a3c8';
|
|
62
52
|
|
|
63
53
|
let cached = null;
|
|
@@ -92,9 +82,6 @@ export function palette() {
|
|
|
92
82
|
for (const [name, v] of Object.entries(kit ?? {})) {
|
|
93
83
|
map[name] = { hex: v.hex ?? UNKNOWN, declared: v.color, known: Boolean(v.hex), source: 'kit' };
|
|
94
84
|
}
|
|
95
|
-
for (const [name, hex] of Object.entries(BUILTIN)) {
|
|
96
|
-
if (!map[name]) map[name] = { hex, declared: null, known: true, source: 'builtin' };
|
|
97
|
-
}
|
|
98
85
|
cached = {
|
|
99
86
|
map,
|
|
100
87
|
unknown: UNKNOWN,
|