great-cto 3.4.0 → 3.6.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.6.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
|
|
|
@@ -161,12 +162,23 @@ function readPlanCosts(cwd = process.cwd(), sinceMsAgo = null) {
|
|
|
161
162
|
let totalLlmMin = 0, totalLlmUsd = 0, totalHumanUsd = 0, count = 0;
|
|
162
163
|
if (!fs.existsSync(plansDir)) return { llm_usd: 0, human_usd: 0, savings_x: 0, count: 0 };
|
|
163
164
|
const cutoff = sinceMsAgo != null ? Date.now() - sinceMsAgo : null;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
165
|
+
const planFiles = fs.readdirSync(plansDir).filter(f => f.endsWith('.md')).map(f => path.join(plansDir, f));
|
|
166
|
+
// Dated from the plan itself — front-matter, then the filename, then the
|
|
167
|
+
// commit that added it — rather than from mtime. mtime is a fact about the
|
|
168
|
+
// filesystem: `git clone` stamps every plan with the clone time, which
|
|
169
|
+
// collapsed thirteen dates into one and made this 30-day window return the
|
|
170
|
+
// project's entire history. See plan-date.mjs for the measurement.
|
|
171
|
+
const dated = datePlans(planFiles, { root: cwd, dir: 'docs/plans' });
|
|
172
|
+
let fromMtime = 0;
|
|
173
|
+
for (const fp of planFiles) {
|
|
174
|
+
const d = dated.dates.get(fp);
|
|
175
|
+
if (cutoff != null) {
|
|
176
|
+
if (!d?.date) continue; // undatable: not in any window
|
|
177
|
+
if (Date.parse(`${d.date}T23:59:59Z`) < cutoff) continue; // outside it
|
|
178
|
+
}
|
|
179
|
+
// Counted after the window test, so the figure describes the plans that
|
|
180
|
+
// actually went into these totals rather than everything in the directory.
|
|
181
|
+
if (d && !d.reliable) fromMtime += 1;
|
|
170
182
|
const content = fs.readFileSync(fp, 'utf8');
|
|
171
183
|
// Parse cost lines from PLAN-*.md.
|
|
172
184
|
// Use the SAME anchored regex as getCostHistory() so both endpoints agree
|
|
@@ -188,6 +200,10 @@ function readPlanCosts(cwd = process.cwd(), sinceMsAgo = null) {
|
|
|
188
200
|
human_usd: Math.round(totalHumanUsd),
|
|
189
201
|
savings_x: totalLlmUsd > 0 ? Math.round(totalHumanUsd / totalLlmUsd) : 0,
|
|
190
202
|
count,
|
|
203
|
+
// How many of these plans could only be dated by their file timestamp. A
|
|
204
|
+
// figure assembled partly from filesystem metadata says so rather than
|
|
205
|
+
// presenting itself as measured.
|
|
206
|
+
dated_by_mtime: fromMtime,
|
|
191
207
|
};
|
|
192
208
|
}
|
|
193
209
|
|
|
@@ -33,6 +33,11 @@ export const VERDICT_FORMAT_VERSION = 1;
|
|
|
33
33
|
/** Verdict values with a defined meaning. Anything else is kept but flagged. */
|
|
34
34
|
export const KNOWN_VERDICTS = Object.freeze([
|
|
35
35
|
'APPROVED', 'BLOCKED', 'DONE', 'FAIL', 'PASS', 'REJECTED', 'SKIPPED', 'ESCALATED',
|
|
36
|
+
// REWORK — independent verification found the stage incomplete and the agent
|
|
37
|
+
// that ran it can fix that itself. Distinct from BLOCKED, which means a human
|
|
38
|
+
// must decide. Without it here the verifier could reach its conclusion and not
|
|
39
|
+
// be able to write it down, which is the same as not reaching it.
|
|
40
|
+
'REWORK',
|
|
36
41
|
]);
|
|
37
42
|
|
|
38
43
|
/**
|
package/package.json
CHANGED