great-cto 3.12.0 → 3.13.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.
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "great_cto",
|
|
3
3
|
"id": "great_cto",
|
|
4
4
|
"description": "Engineering process for solo founders and teams up to 50 engineers. Agents do architecture, code review, QA, and security. You make two decisions per feature.",
|
|
5
|
-
"version": "3.
|
|
5
|
+
"version": "3.13.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Great CTO",
|
|
8
8
|
"url": "https://github.com/avelikiy/great_cto"
|
|
@@ -155,7 +155,18 @@ function getCostHistory(cwd = process.cwd(), days = 30) {
|
|
|
155
155
|
for (let i = 0; i <= days; i++) {
|
|
156
156
|
const d = new Date(now - i * 86400000);
|
|
157
157
|
const key = d.toISOString().slice(0, 10);
|
|
158
|
-
|
|
158
|
+
// `runs` was one field incremented in two places for two different things:
|
|
159
|
+
// once per verdict that carried a cost, and once per closed task on a day that
|
|
160
|
+
// happened to have no cost data. Its meaning therefore changed from day to day
|
|
161
|
+
// depending on whether anything had been measured, and the two sums were
|
|
162
|
+
// indistinguishable once added.
|
|
163
|
+
//
|
|
164
|
+
// Measured against the verdict logs on disk, it was wrong in BOTH directions —
|
|
165
|
+
// +18 on one project over 90 days (closed tasks counted as agent runs) and −3
|
|
166
|
+
// on two others (verdicts with no cost contributing nothing). Split into the
|
|
167
|
+
// two facts, with `runs` kept as their sum so every existing reader keeps
|
|
168
|
+
// working and no chart silently changes shape.
|
|
169
|
+
buckets.set(key, { date: key, llm: 0, human: 0, plans: 0, runs: 0, agent_runs: 0, task_estimates: 0 });
|
|
159
170
|
}
|
|
160
171
|
|
|
161
172
|
// Plans: file mtime as date
|
|
@@ -165,6 +176,20 @@ function getCostHistory(cwd = process.cwd(), days = 30) {
|
|
|
165
176
|
// "LLM" and another near "Human", and FIRE the sanity check below to
|
|
166
177
|
// reject pathological pairs (the 7,638× regression — total_human present
|
|
167
178
|
// but total_llm fell to zero because the LLM regex was too strict).
|
|
179
|
+
// Whether a plan carried a cost at all, kept apart from what that cost was.
|
|
180
|
+
//
|
|
181
|
+
// The pair of regexes below reads a format nothing writes: measured across
|
|
182
|
+
// this repository's 41 plans, ZERO carry an LLM figure at the start of a line
|
|
183
|
+
// and one carries a Human figure — which the guard then subtracts back out. So
|
|
184
|
+
// `total_human` was $0 on every window, beside a plan count in the dozens, and
|
|
185
|
+
// read as "these plans cost nothing" rather than "no plan states a cost".
|
|
186
|
+
//
|
|
187
|
+
// There is no plan template to blame the plans for: the only template shipped
|
|
188
|
+
// is GAP-WAVE-PLAN-template.yaml, so no cost line was ever specified. Counted
|
|
189
|
+
// rather than parsed harder — the dollar figures that DO appear in plans are
|
|
190
|
+
// prose ("10 × ~$0.15", "assert total < $5") and reading them as this plan's
|
|
191
|
+
// cost would be inventing a number.
|
|
192
|
+
let planCostParsed = 0, planCostAbsent = 0, planHumanSuppressed = 0;
|
|
168
193
|
const plansDir = path.join(cwd, 'docs/plans');
|
|
169
194
|
if (fs.existsSync(plansDir)) {
|
|
170
195
|
const planFiles = fs.readdirSync(plansDir).filter(x => x.endsWith('.md')).map(x => path.join(plansDir, x));
|
|
@@ -199,8 +224,10 @@ function getCostHistory(cwd = process.cwd(), days = 30) {
|
|
|
199
224
|
if (humanMatch && !llmMatch && b.human > 0) {
|
|
200
225
|
// Reverse the suppression — drop the bogus single-sided Human entry.
|
|
201
226
|
b.human -= parseFloat(humanMatch[1].replace(/,/g, ''));
|
|
227
|
+
planHumanSuppressed += 1;
|
|
202
228
|
}
|
|
203
229
|
b.plans++;
|
|
230
|
+
if (llmMatch || humanMatch) planCostParsed += 1; else planCostAbsent += 1;
|
|
204
231
|
}
|
|
205
232
|
}
|
|
206
233
|
|
|
@@ -217,18 +244,28 @@ function getCostHistory(cwd = process.cwd(), days = 30) {
|
|
|
217
244
|
// feature=X aggregation — answers "how much did stripe-webhook cost?"
|
|
218
245
|
const featureMap = new Map(); // feature → { llm, runs }
|
|
219
246
|
for (const v of verdicts) {
|
|
220
|
-
if (v.cost_usd == null) continue;
|
|
221
247
|
const dayKey = (v.ts || '').slice(0, 10);
|
|
222
248
|
if (!buckets.has(dayKey)) continue;
|
|
223
249
|
const b = buckets.get(dayKey);
|
|
224
|
-
|
|
250
|
+
// A run that was never priced is still a run.
|
|
251
|
+
//
|
|
252
|
+
// This began `if (v.cost_usd == null) continue`, so a verdict carrying no
|
|
253
|
+
// cost contributed nothing at all — not even its own existence. Measured
|
|
254
|
+
// against the logs: one project had four verdicts and reported ZERO agent
|
|
255
|
+
// runs, because none of the four had been priced. "We do not know what it
|
|
256
|
+
// cost" was being rendered as "it did not happen", which is the confusion
|
|
257
|
+
// this codebase keeps having to unpick.
|
|
258
|
+
//
|
|
259
|
+
// Cost is added only when there is one; the run is counted either way.
|
|
260
|
+
if (v.cost_usd != null) b.llm += v.cost_usd;
|
|
261
|
+
b.agent_runs++;
|
|
225
262
|
b.runs++;
|
|
226
263
|
// Extract feature= tag from raw verdict line
|
|
227
264
|
const featMatch = v.raw && v.raw.match(/\bfeature=([^\s|]+)/);
|
|
228
265
|
if (featMatch) {
|
|
229
266
|
const feat = featMatch[1];
|
|
230
267
|
const f = featureMap.get(feat) || { llm: 0, runs: 0 };
|
|
231
|
-
f.llm += v.cost_usd;
|
|
268
|
+
if (v.cost_usd != null) f.llm += v.cost_usd;
|
|
232
269
|
f.runs++;
|
|
233
270
|
featureMap.set(feat, f);
|
|
234
271
|
}
|
|
@@ -258,6 +295,10 @@ function getCostHistory(cwd = process.cwd(), days = 30) {
|
|
|
258
295
|
if (b.llm === 0) {
|
|
259
296
|
const mins = t.estimated_minutes || DEFAULT_TASK_MIN;
|
|
260
297
|
b.llm += mins / 60 * LLM_RATE_PER_HR;
|
|
298
|
+
// A closed task is not an agent run. It is counted, and counted
|
|
299
|
+
// separately, so a caller asking "how many times did agents run" is not
|
|
300
|
+
// handed a number that is partly something else.
|
|
301
|
+
b.task_estimates++;
|
|
261
302
|
b.runs++;
|
|
262
303
|
}
|
|
263
304
|
}
|
|
@@ -267,6 +308,11 @@ function getCostHistory(cwd = process.cwd(), days = 30) {
|
|
|
267
308
|
let totalLlm = series.reduce((a, b) => a + b.llm, 0);
|
|
268
309
|
let totalHuman = series.reduce((a, b) => a + b.human, 0);
|
|
269
310
|
const totalPlans = series.reduce((a, b) => a + b.plans, 0);
|
|
311
|
+
// Reported alongside the sum rather than instead of it: `total_runs` keeps its
|
|
312
|
+
// meaning for every existing caller, and the two facts it was made of are now
|
|
313
|
+
// separately answerable.
|
|
314
|
+
const totalAgentRuns = series.reduce((a, b) => a + b.agent_runs, 0);
|
|
315
|
+
const totalTaskEstimates = series.reduce((a, b) => a + b.task_estimates, 0);
|
|
270
316
|
|
|
271
317
|
// SANITY GUARD — anti-7,638× regression. If ratio > 1000×, one of the
|
|
272
318
|
// numbers is wrong. Almost always: total_llm collapsed to ~0 because plan
|
|
@@ -291,6 +337,14 @@ function getCostHistory(cwd = process.cwd(), days = 30) {
|
|
|
291
337
|
total_llm: Math.round(totalLlm * 100) / 100,
|
|
292
338
|
total_human: Math.round(totalHuman),
|
|
293
339
|
total_plans: totalPlans,
|
|
340
|
+
total_agent_runs: totalAgentRuns,
|
|
341
|
+
total_task_estimates: totalTaskEstimates,
|
|
342
|
+
// Three states, not a number and a silence: a plan that states a cost, a
|
|
343
|
+
// plan that states none, and a plan whose Human figure was suppressed
|
|
344
|
+
// because its LLM counterpart could not be read.
|
|
345
|
+
plans_with_cost: planCostParsed,
|
|
346
|
+
plans_without_cost: planCostAbsent,
|
|
347
|
+
plans_human_suppressed: planHumanSuppressed,
|
|
294
348
|
daily_avg: Math.round(dayRate * 100) / 100,
|
|
295
349
|
projected_monthly: projectedMonthly,
|
|
296
350
|
monthly_budget: budget,
|
|
@@ -92,6 +92,28 @@ function readVerdicts(cwd = null, health = null) {
|
|
|
92
92
|
continue;
|
|
93
93
|
}
|
|
94
94
|
for (const file of files) {
|
|
95
|
+
// Only per-agent verdict logs. `<agent>-YYYY-MM-DD-HHMMSS.log` is a different
|
|
96
|
+
// artefact that lives in the same directory: a free-text report of one run,
|
|
97
|
+
// written as prose. Reading it as a verdict log made its first words into
|
|
98
|
+
// records — `ts: "DONE:"`, `ts: "artifact:"`, `ts: "next:"`, with the agent
|
|
99
|
+
// name taken from the filename including its timestamp. Six of the ten
|
|
100
|
+
// "verdicts" one project appeared to have were paragraph openings.
|
|
101
|
+
//
|
|
102
|
+
// Skipped rather than parsed leniently: a run report has no verdict token, no
|
|
103
|
+
// cost and no meta, so anything recovered from it would be invented. Counted,
|
|
104
|
+
// though — a directory whose contents this reader ignores is worth saying out
|
|
105
|
+
// loud, because from outside it looks identical to a project that never ran.
|
|
106
|
+
if (!file.endsWith('.log')) continue;
|
|
107
|
+
// Rejected by the shape that IDENTIFIES a run report, not by trying to
|
|
108
|
+
// describe what a valid agent name looks like. The first attempt did the
|
|
109
|
+
// latter — `/^[A-Za-z0-9_:.-]+\.log$/` — and matched
|
|
110
|
+
// `architect-2026-08-26-134109.log` perfectly well, because digits and
|
|
111
|
+
// hyphens are exactly what an agent name may contain. An allowlist that
|
|
112
|
+
// cannot exclude the thing it was written to exclude is not a filter.
|
|
113
|
+
if (RUN_REPORT_FILE.test(file)) {
|
|
114
|
+
health?.notes?.push(`${file} is a run report, not a verdict log — not read`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
95
117
|
const agent = file.replace('.log', '');
|
|
96
118
|
let lines;
|
|
97
119
|
try {
|
|
@@ -192,6 +214,9 @@ function readVerdicts(cwd = null, health = null) {
|
|
|
192
214
|
return results.sort((a, b) => a.ts.localeCompare(b.ts));
|
|
193
215
|
}
|
|
194
216
|
|
|
217
|
+
/** `<agent>-YYYY-MM-DD-HHMMSS.log` — a prose report of one run, not a verdict. */
|
|
218
|
+
const RUN_REPORT_FILE = /-\d{4}-\d{2}-\d{2}-\d{6}\.log$/;
|
|
219
|
+
|
|
195
220
|
function readPlanCosts(cwd = process.cwd(), sinceMsAgo = null) {
|
|
196
221
|
const plansDir = path.join(cwd, 'docs/plans');
|
|
197
222
|
let totalLlmMin = 0, totalLlmUsd = 0, totalHumanUsd = 0, count = 0;
|
|
@@ -164,10 +164,29 @@ export function parseVerdictLine(line) {
|
|
|
164
164
|
agent = fields.length >= 2 ? fields[0] : '';
|
|
165
165
|
verdict = fields.length >= 2 ? fields[1] : fields[0];
|
|
166
166
|
} else {
|
|
167
|
+
// The space form comes in two shapes, and this used to assume only one:
|
|
168
|
+
//
|
|
169
|
+
// <ts> <verdict> <rest> what the comment here claimed
|
|
170
|
+
// <ts> <agent> <verdict> <rest> also written, e.g. by log-verdict.sh
|
|
171
|
+
//
|
|
172
|
+
// Assuming the first turned `2026-08-26T16:59:22Z great_cto:code-reviewer
|
|
173
|
+
// APPROVED …` into a record whose VERDICT was `GREAT_CTO:CODE-REVIEWER`. It
|
|
174
|
+
// parsed, it validated, and the board showed an agent name in the verdict
|
|
175
|
+
// column — a wrong value that looks like a value.
|
|
176
|
+
//
|
|
177
|
+
// Discriminated by the one fact the file already carries: whether the token
|
|
178
|
+
// is a verdict this system knows. Neither position matching leaves the
|
|
179
|
+
// original reading in place rather than guessing a different wrong one.
|
|
167
180
|
const parts = raw.split(/\s+/);
|
|
168
181
|
ts = parts[0] || '';
|
|
169
|
-
|
|
170
|
-
|
|
182
|
+
const known = (t) => KNOWN_VERDICTS.includes(String(t || '').toUpperCase());
|
|
183
|
+
if (!known(parts[1]) && known(parts[2])) {
|
|
184
|
+
agent = parts[1] || '';
|
|
185
|
+
verdict = parts[2] || '';
|
|
186
|
+
} else {
|
|
187
|
+
verdict = parts[1] || '';
|
|
188
|
+
agent = ''; // this shape does not carry one; the filename does
|
|
189
|
+
}
|
|
171
190
|
}
|
|
172
191
|
|
|
173
192
|
const rec = { v: VERDICT_FORMAT_VERSION, ts, agent, verdict: String(verdict || '').toUpperCase() };
|
package/package.json
CHANGED