great-cto 3.5.0 → 3.7.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.7.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Great CTO",
|
|
8
8
|
"url": "https://github.com/avelikiy/great_cto"
|
|
@@ -5,6 +5,7 @@ import { GREAT_CTO_DIR } from './config.mjs';
|
|
|
5
5
|
import { readFileSafe } from './util.mjs';
|
|
6
6
|
import { getTasks } from './beads.mjs';
|
|
7
7
|
import { readVerdicts, readSecStats } from './verdicts.mjs';
|
|
8
|
+
import { datePlans } from './plan-date.mjs';
|
|
8
9
|
|
|
9
10
|
// ── Memory: 4-layer file contents ─────────────────────────────────────────────
|
|
10
11
|
function getMemory(cwd = process.cwd()) {
|
|
@@ -166,11 +167,15 @@ function getCostHistory(cwd = process.cwd(), days = 30) {
|
|
|
166
167
|
// but total_llm fell to zero because the LLM regex was too strict).
|
|
167
168
|
const plansDir = path.join(cwd, 'docs/plans');
|
|
168
169
|
if (fs.existsSync(plansDir)) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
170
|
+
const planFiles = fs.readdirSync(plansDir).filter(x => x.endsWith('.md')).map(x => path.join(plansDir, x));
|
|
171
|
+
// The day a plan lands on is read from the plan, not from its mtime. The
|
|
172
|
+
// twin of the same defect in readPlanCosts: on a fresh clone every file
|
|
173
|
+
// carries the clone time, so the whole history stacked onto one bar of this
|
|
174
|
+
// chart — today's — and every earlier day read as zero spend.
|
|
175
|
+
const dated = datePlans(planFiles, { root: cwd, dir: 'docs/plans' });
|
|
176
|
+
for (const fp of planFiles) {
|
|
177
|
+
const dayKey = dated.dates.get(fp)?.date;
|
|
178
|
+
if (!dayKey || !buckets.has(dayKey)) continue;
|
|
174
179
|
const content = fs.readFileSync(fp, 'utf8');
|
|
175
180
|
// Anchor LLM/Human at START of line (with optional markdown emphasis)
|
|
176
181
|
// so we never mis-match cases like:
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* plan-date — when a plan was written, as opposed to when its file was touched.
|
|
3
|
+
*
|
|
4
|
+
* The defect
|
|
5
|
+
* ----------
|
|
6
|
+
* Both cost readers dated a plan by `statSync(fp).mtime`. mtime is not a fact
|
|
7
|
+
* about the plan; it is a fact about the filesystem, and everything ordinary
|
|
8
|
+
* changes it — a reformat, a checkout, a copy, a `sed -i` over the directory.
|
|
9
|
+
*
|
|
10
|
+
* `git clone` is the case that turns it from imprecise into wrong. Clone writes
|
|
11
|
+
* every file at clone time, so on a fresh checkout of this repository all 41
|
|
12
|
+
* plans carry today's date: THIRTEEN distinct dates collapse into ONE. The
|
|
13
|
+
* 30-day cost window then selects every plan ever written, and reports the
|
|
14
|
+
* project's entire history as the last month. Measured, both ways:
|
|
15
|
+
*
|
|
16
|
+
* fresh clone 30-day window → 41 plans, $150 human ← identical to
|
|
17
|
+
* all time → 41 plans, $150 human all-time
|
|
18
|
+
* this checkout 30-day window → 13 plans
|
|
19
|
+
* all time → 41 plans
|
|
20
|
+
*
|
|
21
|
+
* Nobody would read "$150 in the last 30 days" as suspicious. That is the shape
|
|
22
|
+
* of the whole defect class: a number that is wrong in a way that looks normal.
|
|
23
|
+
*
|
|
24
|
+
* Where the date actually lives
|
|
25
|
+
* -----------------------------
|
|
26
|
+
* Measured over this repository's 41 plans rather than assumed:
|
|
27
|
+
*
|
|
28
|
+
* 21 the filename — `PLAN-2026-08-17-gate-fail-closed.md`
|
|
29
|
+
* 20 nowhere in the file at all
|
|
30
|
+
* 0 front-matter (supported anyway; it is the one an author can correct)
|
|
31
|
+
*
|
|
32
|
+
* So git is not a nicety, it is the only honest source for half of them: the
|
|
33
|
+
* first commit that added the file is when the plan appeared. One batched
|
|
34
|
+
* `git log` covers the whole directory in 0.27s, against 1.47s for one call per
|
|
35
|
+
* file, so the cheap way is also the correct way.
|
|
36
|
+
*
|
|
37
|
+
* Three states, and the third is the point
|
|
38
|
+
* ----------------------------------------
|
|
39
|
+
* When no source but mtime exists the date is still returned — dropping the plan
|
|
40
|
+
* would silently shrink the window, which is a different wrong answer. It is
|
|
41
|
+
* returned with `reliable: false`, and callers report how many of those went
|
|
42
|
+
* into a figure. A total assembled partly from filesystem timestamps should say
|
|
43
|
+
* so rather than present itself as measured.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { statSync, readFileSync } from 'node:fs';
|
|
47
|
+
import { spawnSync } from 'node:child_process';
|
|
48
|
+
import path from 'node:path';
|
|
49
|
+
|
|
50
|
+
const ISO_DAY = /(\d{4}-\d{2}-\d{2})/;
|
|
51
|
+
|
|
52
|
+
/** `date:` in YAML front-matter, if the file opens with a front-matter block. */
|
|
53
|
+
export function frontMatterDate(text) {
|
|
54
|
+
const m = String(text || '').match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
55
|
+
if (!m) return null;
|
|
56
|
+
const d = m[1].match(/^date:\s*(\d{4}-\d{2}-\d{2})/m);
|
|
57
|
+
return d ? d[1] : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** `YYYY-MM-DD` anywhere in the basename. */
|
|
61
|
+
export function fileNameDate(file) {
|
|
62
|
+
const m = path.basename(file).match(ISO_DAY);
|
|
63
|
+
return m ? m[1] : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* One `git log` for a whole directory: relative path → date the file was ADDED.
|
|
68
|
+
*
|
|
69
|
+
* Returns an empty Map when git is absent, the directory is untracked, or the
|
|
70
|
+
* project is not a repository at all. That is not an error — it is a project
|
|
71
|
+
* without this source, and the caller falls through to the next one.
|
|
72
|
+
*/
|
|
73
|
+
export function gitAddedIndex(root, dir) {
|
|
74
|
+
const out = new Map();
|
|
75
|
+
let res;
|
|
76
|
+
try {
|
|
77
|
+
res = spawnSync('git', ['log', '--diff-filter=A', '--name-only',
|
|
78
|
+
'--format=%x00%ad', '--date=short', '--', dir],
|
|
79
|
+
{ cwd: root, encoding: 'utf8', timeout: 15_000 });
|
|
80
|
+
} catch { return out; }
|
|
81
|
+
if (!res || res.status !== 0 || !res.stdout) return out;
|
|
82
|
+
|
|
83
|
+
let current = null;
|
|
84
|
+
for (const line of res.stdout.split('\n')) {
|
|
85
|
+
if (line.startsWith('\0')) { current = line.slice(1).trim(); continue; }
|
|
86
|
+
const rel = line.trim();
|
|
87
|
+
// git log walks newest-first, so the LAST assignment for a path is its
|
|
88
|
+
// oldest add — a file added, deleted and re-added should date from the
|
|
89
|
+
// first time it appeared, not the most recent.
|
|
90
|
+
if (rel && current) out.set(rel, current);
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @returns {{date: string|null, source: 'front-matter'|'filename'|'git'|'mtime'|'none', reliable: boolean}}
|
|
97
|
+
*/
|
|
98
|
+
export function planDate(absPath, { root = process.cwd(), gitIndex = null, readText = null } = {}) {
|
|
99
|
+
let text = '';
|
|
100
|
+
try { text = readText ? readText(absPath) : readFileSync(absPath, 'utf8').slice(0, 2000); } catch { /* unreadable */ }
|
|
101
|
+
|
|
102
|
+
const fm = frontMatterDate(text);
|
|
103
|
+
if (fm) return { date: fm, source: 'front-matter', reliable: true };
|
|
104
|
+
|
|
105
|
+
const fn = fileNameDate(absPath);
|
|
106
|
+
if (fn) return { date: fn, source: 'filename', reliable: true };
|
|
107
|
+
|
|
108
|
+
if (gitIndex && gitIndex.size) {
|
|
109
|
+
const rel = path.relative(root, absPath);
|
|
110
|
+
const g = gitIndex.get(rel);
|
|
111
|
+
if (g) return { date: g, source: 'git', reliable: true };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
return { date: statSync(absPath).mtime.toISOString().slice(0, 10), source: 'mtime', reliable: false };
|
|
116
|
+
} catch {
|
|
117
|
+
return { date: null, source: 'none', reliable: false };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Date every `.md` in a directory in one pass, with one git call for all of them.
|
|
123
|
+
*
|
|
124
|
+
* @returns {{dates: Map<string,{date,source,reliable}>, unreliable: number, total: number}}
|
|
125
|
+
*/
|
|
126
|
+
export function datePlans(files, { root = process.cwd(), dir = 'docs/plans' } = {}) {
|
|
127
|
+
const gitIndex = gitAddedIndex(root, dir);
|
|
128
|
+
const dates = new Map();
|
|
129
|
+
let unreliable = 0;
|
|
130
|
+
for (const f of files) {
|
|
131
|
+
const d = planDate(f, { root, gitIndex });
|
|
132
|
+
dates.set(f, d);
|
|
133
|
+
if (!d.reliable) unreliable += 1;
|
|
134
|
+
}
|
|
135
|
+
return { dates, unreliable, total: files.length };
|
|
136
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { GREAT_CTO_DIR } from './config.mjs';
|
|
4
|
+
import { datePlans } from './plan-date.mjs';
|
|
4
5
|
import { parseVerdictLine } from '../../../scripts/lib/verdict-record.mjs';
|
|
5
6
|
|
|
6
7
|
|
|
@@ -140,16 +141,42 @@ function readVerdicts(cwd = null, health = null) {
|
|
|
140
141
|
if (!fs.existsSync(histPath)) continue;
|
|
141
142
|
const lines = fs.readFileSync(histPath, 'utf8').split('\n').filter(Boolean);
|
|
142
143
|
for (const line of lines) {
|
|
143
|
-
|
|
144
|
+
// Anchored on a real ISO timestamp rather than "the first run of
|
|
145
|
+
// non-space characters". The writer used to emit an entire compact-JSON
|
|
146
|
+
// verdict in that position, and `\S+` accepted it — producing keys like
|
|
147
|
+
// `{"v":1,"ts":"202|qa-engineer` that could never match a verdict.
|
|
148
|
+
// Lines from that era are skipped here instead of being half-parsed.
|
|
149
|
+
const m = line.match(/^(\d{4}-\d{2}-\d{2}T\S+)\s+(\S+)\s+(\d+\.?\d*)(?:\s|$)/);
|
|
144
150
|
if (!m) continue;
|
|
151
|
+
const usd = parseFloat(m[3]);
|
|
152
|
+
// A recorded zero is `log-verdict.sh` writing through whatever the agent
|
|
153
|
+
// reported, which is nothing. Enriching a zero verdict with a zero from
|
|
154
|
+
// this file changes no number and would label it `measured` — a claim
|
|
155
|
+
// that something was measured when the file says only that a line exists.
|
|
156
|
+
if (!(usd > 0)) continue;
|
|
145
157
|
const key = `${m[1].slice(0, 16)}|${m[2]}`; // minute + agent
|
|
146
|
-
if (!costByKey.has(key)) costByKey.set(key,
|
|
158
|
+
if (!costByKey.has(key)) costByKey.set(key, usd); // project wins
|
|
147
159
|
}
|
|
148
160
|
}
|
|
149
161
|
for (const v of results) {
|
|
150
|
-
|
|
162
|
+
// A self-reported ZERO is not a measurement — it is the absence of one,
|
|
163
|
+
// and it must not shadow a figure that was actually measured.
|
|
164
|
+
//
|
|
165
|
+
// This read `if (v.cost_usd != null) continue`, and every verdict carries
|
|
166
|
+
// `cost_usd: 0` because agents do not measure their own spend. Zero is not
|
|
167
|
+
// null, so enrichment was skipped for all 26 verdicts in the window while
|
|
168
|
+
// the measured costs sat in cost-history.log unused — and the budgets
|
|
169
|
+
// screen reported `unmeasured`, which was true of what it read and false
|
|
170
|
+
// of what existed.
|
|
171
|
+
//
|
|
172
|
+
// A measured value overrides a zero; nothing overrides a non-zero figure
|
|
173
|
+
// an agent actually reported.
|
|
174
|
+
if (v.cost_usd != null && v.cost_usd > 0) continue;
|
|
151
175
|
const key = `${(v.ts || '').slice(0, 16)}|${v.agent}`;
|
|
152
|
-
if (costByKey.has(key))
|
|
176
|
+
if (costByKey.has(key)) {
|
|
177
|
+
v.cost_usd = costByKey.get(key);
|
|
178
|
+
v.cost_source = 'measured'; // read from a transcript, not self-reported
|
|
179
|
+
}
|
|
153
180
|
}
|
|
154
181
|
}
|
|
155
182
|
|
|
@@ -161,12 +188,23 @@ function readPlanCosts(cwd = process.cwd(), sinceMsAgo = null) {
|
|
|
161
188
|
let totalLlmMin = 0, totalLlmUsd = 0, totalHumanUsd = 0, count = 0;
|
|
162
189
|
if (!fs.existsSync(plansDir)) return { llm_usd: 0, human_usd: 0, savings_x: 0, count: 0 };
|
|
163
190
|
const cutoff = sinceMsAgo != null ? Date.now() - sinceMsAgo : null;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
191
|
+
const planFiles = fs.readdirSync(plansDir).filter(f => f.endsWith('.md')).map(f => path.join(plansDir, f));
|
|
192
|
+
// Dated from the plan itself — front-matter, then the filename, then the
|
|
193
|
+
// commit that added it — rather than from mtime. mtime is a fact about the
|
|
194
|
+
// filesystem: `git clone` stamps every plan with the clone time, which
|
|
195
|
+
// collapsed thirteen dates into one and made this 30-day window return the
|
|
196
|
+
// project's entire history. See plan-date.mjs for the measurement.
|
|
197
|
+
const dated = datePlans(planFiles, { root: cwd, dir: 'docs/plans' });
|
|
198
|
+
let fromMtime = 0;
|
|
199
|
+
for (const fp of planFiles) {
|
|
200
|
+
const d = dated.dates.get(fp);
|
|
201
|
+
if (cutoff != null) {
|
|
202
|
+
if (!d?.date) continue; // undatable: not in any window
|
|
203
|
+
if (Date.parse(`${d.date}T23:59:59Z`) < cutoff) continue; // outside it
|
|
204
|
+
}
|
|
205
|
+
// Counted after the window test, so the figure describes the plans that
|
|
206
|
+
// actually went into these totals rather than everything in the directory.
|
|
207
|
+
if (d && !d.reliable) fromMtime += 1;
|
|
170
208
|
const content = fs.readFileSync(fp, 'utf8');
|
|
171
209
|
// Parse cost lines from PLAN-*.md.
|
|
172
210
|
// Use the SAME anchored regex as getCostHistory() so both endpoints agree
|
|
@@ -188,6 +226,10 @@ function readPlanCosts(cwd = process.cwd(), sinceMsAgo = null) {
|
|
|
188
226
|
human_usd: Math.round(totalHumanUsd),
|
|
189
227
|
savings_x: totalLlmUsd > 0 ? Math.round(totalHumanUsd / totalLlmUsd) : 0,
|
|
190
228
|
count,
|
|
229
|
+
// How many of these plans could only be dated by their file timestamp. A
|
|
230
|
+
// figure assembled partly from filesystem metadata says so rather than
|
|
231
|
+
// presenting itself as measured.
|
|
232
|
+
dated_by_mtime: fromMtime,
|
|
191
233
|
};
|
|
192
234
|
}
|
|
193
235
|
|
package/package.json
CHANGED