great-cto 3.28.7 → 3.28.9
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.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "great-cto",
|
|
3
3
|
"description": "You already have the agent. This is everything around it. great_cto runs Claude Code as a pipeline of 70 specialist agents \u2014 an independent model checks each stage before the next builds on it, spending caps refuse rather than warn, and three decisions stay yours: what gets built, how, and whether it ships.",
|
|
4
|
-
"version": "3.28.
|
|
4
|
+
"version": "3.28.9",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Alexander Velikiy",
|
|
7
7
|
"url": "https://hashnode.com/@Greatcto"
|
|
@@ -13,15 +13,33 @@ import { readVerdicts, readPlanCosts, readQAStats, readSecStats } from './verdic
|
|
|
13
13
|
// accepted change yet" is the honest reading (same discipline as the null-score
|
|
14
14
|
// oracle). Only two of the five metrics the literature lists are built here on
|
|
15
15
|
// purpose — a solo project's denominator is tiny and the other three would be noise.
|
|
16
|
+
//
|
|
17
|
+
// A halting verdict is split three ways by the `need` its author declared:
|
|
18
|
+
// `implementer` is a pass the implementer can make (rework), `decision` is a
|
|
19
|
+
// question for a human, and anything else is undeclared. Counting every halt as
|
|
20
|
+
// rework called a waiver request "rework"; and REWORK itself, the token the
|
|
21
|
+
// dispatcher sends work back with, matched none of the old patterns and was never
|
|
22
|
+
// counted. Undeclared is its own count because it is not known to be either.
|
|
16
23
|
function acceptanceMetrics(verdicts = [], windowCostUsd = null) {
|
|
17
|
-
const
|
|
18
|
-
const
|
|
24
|
+
const token = (v) => String(v?.verdict || '');
|
|
25
|
+
const isAccepted = (v) => /^APPROVED$/i.test(token(v));
|
|
26
|
+
const isSentBack = (v) => /^(REWORK|CHANGES(_REQUESTED)?)$/i.test(token(v));
|
|
27
|
+
const isHalt = (v) => /^(REJECT(ED)?|FAIL(ED)?|BLOCKED)$/i.test(token(v));
|
|
28
|
+
const needOf = (v) => v?.meta?.need;
|
|
29
|
+
let rework_rounds = 0, decisions = 0, undeclared_blocks = 0;
|
|
30
|
+
for (const v of verdicts) {
|
|
31
|
+
if (isSentBack(v)) rework_rounds++;
|
|
32
|
+
else if (isHalt(v)) {
|
|
33
|
+
if (needOf(v) === 'implementer') rework_rounds++;
|
|
34
|
+
else if (needOf(v) === 'decision') decisions++;
|
|
35
|
+
else undeclared_blocks++;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
19
38
|
const accepted = verdicts.filter(isAccepted).length;
|
|
20
|
-
const rework_rounds = verdicts.filter(isRework).length;
|
|
21
39
|
const cost_per_accepted = (accepted > 0 && windowCostUsd != null)
|
|
22
40
|
? Math.round((windowCostUsd / accepted) * 100) / 100
|
|
23
41
|
: null;
|
|
24
|
-
return { accepted, cost_per_accepted, rework_rounds };
|
|
42
|
+
return { accepted, cost_per_accepted, rework_rounds, decisions, undeclared_blocks };
|
|
25
43
|
}
|
|
26
44
|
|
|
27
45
|
// ── Metrics ────────────────────────────────────────────────────────────────────
|
|
@@ -140,6 +140,9 @@ function readVerdicts(cwd = null, health = null) {
|
|
|
140
140
|
agent,
|
|
141
141
|
verdict,
|
|
142
142
|
cost_usd: parsed.rec.cost_usd ?? null,
|
|
143
|
+
// Kept so a metric can read what the author declared (`need`, `task`).
|
|
144
|
+
// Dropping it made every halt look undeclared.
|
|
145
|
+
meta: parsed.rec.meta ?? null,
|
|
143
146
|
raw: line.replace(/\s*\bcost=\$?\d+\.?\d*\b/i, ''),
|
|
144
147
|
});
|
|
145
148
|
}
|
|
@@ -52,7 +52,7 @@ export const MAX_FILES = 200;
|
|
|
52
52
|
* Returns `null` outside a git repository rather than a fabricated receipt: a
|
|
53
53
|
* receipt that cannot be built must not look like one that matched.
|
|
54
54
|
*/
|
|
55
|
-
export function treeReceipt(cwd = process.cwd(), { base = null, maxFiles = MAX_FILES } = {}) {
|
|
55
|
+
export function treeReceipt(cwd = process.cwd(), { base = null, maxFiles = MAX_FILES, verdictsDir = null } = {}) {
|
|
56
56
|
const head = git(['rev-parse', 'HEAD'], cwd)?.trim();
|
|
57
57
|
if (!head) return null;
|
|
58
58
|
|
|
@@ -73,7 +73,35 @@ export function treeReceipt(cwd = process.cwd(), { base = null, maxFiles = MAX_F
|
|
|
73
73
|
// Which files the change touches. `--diff-filter=d` drops deletions: a file
|
|
74
74
|
// that is gone cannot have a blob sha, and its absence is already visible in
|
|
75
75
|
// the map as a missing key.
|
|
76
|
-
|
|
76
|
+
// What the change is measured against. The merge-base with the default branch
|
|
77
|
+
// is right on a feature branch, and wrong on the branch itself: once the work is
|
|
78
|
+
// on main, or there is no upstream at all, the merge-base IS HEAD, and a stage
|
|
79
|
+
// that committed before recording its verdict gets a change set holding only
|
|
80
|
+
// uncommitted leftovers. A live run on 2026-09-14 did exactly that — senior-dev
|
|
81
|
+
// committed its fix, the receipt named two logs and a report, and the
|
|
82
|
+
// independent judge, shown no code, correctly said nothing was implemented.
|
|
83
|
+
//
|
|
84
|
+
// So when the caller hands in the project's verdicts, the change starts where
|
|
85
|
+
// the previous stage stood (taskBase), unless the merge-base is closer.
|
|
86
|
+
// `base_from` records which, so a reader can tell what the judge was shown.
|
|
87
|
+
let ref, baseFrom;
|
|
88
|
+
if (base) {
|
|
89
|
+
ref = base; baseFrom = 'explicit';
|
|
90
|
+
} else {
|
|
91
|
+
const tb = verdictsDir ? taskBase(cwd, { verdictsDir, head }) : null;
|
|
92
|
+
const mb = mergeBase(cwd);
|
|
93
|
+
const mbBelow = mb && mb !== head ? mb : null;
|
|
94
|
+
if (tb && mbBelow) {
|
|
95
|
+
ref = isAncestor(cwd, mbBelow, tb) ? tb : mbBelow;
|
|
96
|
+
baseFrom = ref === tb ? 'task' : 'merge-base';
|
|
97
|
+
} else if (tb) {
|
|
98
|
+
ref = tb; baseFrom = 'task';
|
|
99
|
+
} else if (mbBelow) {
|
|
100
|
+
ref = mbBelow; baseFrom = 'merge-base';
|
|
101
|
+
} else {
|
|
102
|
+
ref = mb || 'HEAD'; baseFrom = 'head';
|
|
103
|
+
}
|
|
104
|
+
}
|
|
77
105
|
const names = [
|
|
78
106
|
...(git(['diff', '--name-only', '--diff-filter=d', ref], cwd) ?? '')
|
|
79
107
|
.split('\n').map((s) => s.trim()).filter(Boolean),
|
|
@@ -90,7 +118,7 @@ export function treeReceipt(cwd = process.cwd(), { base = null, maxFiles = MAX_F
|
|
|
90
118
|
if (blob) files[p] = blob;
|
|
91
119
|
}
|
|
92
120
|
|
|
93
|
-
return { head, dirty, base: ref, files, ...(truncated ? { truncated: true } : {}) };
|
|
121
|
+
return { head, dirty, base: ref, base_from: baseFrom, files, ...(truncated ? { truncated: true } : {}) };
|
|
94
122
|
}
|
|
95
123
|
|
|
96
124
|
/**
|
|
@@ -105,6 +133,50 @@ export function fileDigest(cwd, path) {
|
|
|
105
133
|
return out ? out.trim() : null;
|
|
106
134
|
}
|
|
107
135
|
|
|
136
|
+
/** Is `ancestor` in the history of `descendant`? */
|
|
137
|
+
function isAncestor(cwd, ancestor, descendant) {
|
|
138
|
+
return git(['merge-base', '--is-ancestor', ancestor, descendant], cwd) !== null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Where the current stage's work began: the commit the previous stage stood on.
|
|
143
|
+
*
|
|
144
|
+
* The newest verdict in `verdictsDir` that carries a receipt names that commit as
|
|
145
|
+
* `receipt.head`. It is a base only if it is still in this history and is not
|
|
146
|
+
* HEAD itself — a previous stage recorded at HEAD means nothing has been
|
|
147
|
+
* committed since, and an earlier verdict would attribute another stage's work
|
|
148
|
+
* to this one. A recorded head that is not an ancestor (rebased away, or from a
|
|
149
|
+
* different clone) is skipped for the next newest.
|
|
150
|
+
*
|
|
151
|
+
* Null when there is no such commit; the caller falls back to the merge-base.
|
|
152
|
+
*/
|
|
153
|
+
export function taskBase(cwd, { verdictsDir, head = null, read = readFileSync, list = null } = {}) {
|
|
154
|
+
if (!verdictsDir) return null;
|
|
155
|
+
const now = head || git(['rev-parse', 'HEAD'], cwd)?.trim();
|
|
156
|
+
if (!now) return null;
|
|
157
|
+
let names;
|
|
158
|
+
try { names = (list || fsModule.readdirSync)(verdictsDir).filter((f) => f.endsWith('.log')); }
|
|
159
|
+
catch { return null; }
|
|
160
|
+
const recorded = [];
|
|
161
|
+
for (const f of names) {
|
|
162
|
+
let text;
|
|
163
|
+
try { text = read(join(verdictsDir, f), 'utf8'); } catch { continue; }
|
|
164
|
+
for (const line of String(text).split('\n')) {
|
|
165
|
+
if (!line.trim()) continue;
|
|
166
|
+
let rec;
|
|
167
|
+
try { rec = JSON.parse(line); } catch { continue; }
|
|
168
|
+
const h = rec?.receipt?.head;
|
|
169
|
+
if (typeof h === 'string' && /^[0-9a-f]{7,40}$/.test(h)) recorded.push({ ts: String(rec.ts || ''), head: h });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
recorded.sort((a, b) => b.ts.localeCompare(a.ts));
|
|
173
|
+
for (const { head: h } of recorded) {
|
|
174
|
+
if (h === now) return null;
|
|
175
|
+
if (isAncestor(cwd, h, now)) return h;
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
108
180
|
/** The fork point from the default branch, or null when there isn't one. */
|
|
109
181
|
export function mergeBase(cwd) {
|
|
110
182
|
for (const branch of ['origin/main', 'main', 'origin/master', 'master']) {
|
|
@@ -301,7 +373,10 @@ function requireFs() {
|
|
|
301
373
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
302
374
|
const argv = process.argv.slice(2);
|
|
303
375
|
if (argv.includes('--emit')) {
|
|
304
|
-
|
|
376
|
+
// `--verdicts <dir>`: measure the change from where the previous stage stood.
|
|
377
|
+
const vi = argv.indexOf('--verdicts');
|
|
378
|
+
const verdictsDir = vi >= 0 && argv[vi + 1] ? argv[vi + 1] : null;
|
|
379
|
+
const r = treeReceipt(process.cwd(), { verdictsDir });
|
|
305
380
|
process.stdout.write(r ? JSON.stringify(r) : '');
|
|
306
381
|
process.exit(r ? 0 : 1);
|
|
307
382
|
}
|
|
@@ -55,6 +55,24 @@ export const KNOWN_VERDICTS = Object.freeze([
|
|
|
55
55
|
* @property {object} [meta] free-form key/value from the caller
|
|
56
56
|
*/
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Who a halting verdict is for, as `meta.need`.
|
|
60
|
+
*
|
|
61
|
+
* implementer — the agent that did the work can fix this; no human choice
|
|
62
|
+
* decision — a waiver, a scope change or a trade-off; a human decides
|
|
63
|
+
*
|
|
64
|
+
* Anything else, including absence, reads as `undeclared`, and undeclared is never
|
|
65
|
+
* routed anywhere: it halts exactly as a BLOCKED always has. qa and security wrote
|
|
66
|
+
* this distinction in prose for months and no verdict line carried it.
|
|
67
|
+
*/
|
|
68
|
+
export const NEED_VALUES = Object.freeze(['implementer', 'decision']);
|
|
69
|
+
|
|
70
|
+
/** 'implementer' | 'decision' | 'undeclared'. Never throws. */
|
|
71
|
+
export function needOf(rec) {
|
|
72
|
+
const n = rec?.meta?.need;
|
|
73
|
+
return NEED_VALUES.includes(n) ? n : 'undeclared';
|
|
74
|
+
}
|
|
75
|
+
|
|
58
76
|
const ISO = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;
|
|
59
77
|
|
|
60
78
|
/**
|
|
@@ -97,9 +115,15 @@ export function makeVerdict({ ts, agent, verdict, project, cost_usd, meta, recei
|
|
|
97
115
|
if (receipt && typeof receipt === 'object' && receipt.head) rec.receipt = receipt;
|
|
98
116
|
|
|
99
117
|
const { valid, errors } = validateVerdict(rec);
|
|
118
|
+
// An unknown `need` is refused HERE, at write time, and not in validateVerdict:
|
|
119
|
+
// that also runs on read, where refusing would discard the verdict itself. On
|
|
120
|
+
// read the value survives and needOf() calls it undeclared.
|
|
121
|
+
if (rec.meta && 'need' in rec.meta && !NEED_VALUES.includes(rec.meta.need)) {
|
|
122
|
+
errors.push(`meta.need must be one of ${NEED_VALUES.join(', ')}, got ${JSON.stringify(rec.meta.need)}`);
|
|
123
|
+
}
|
|
100
124
|
// Refusing here is the point: a malformed record that reaches the log is read
|
|
101
125
|
// by something later, and by then nobody knows which agent wrote it.
|
|
102
|
-
if (
|
|
126
|
+
if (errors.length) throw new Error(`invalid verdict record: ${errors.join('; ')}`);
|
|
103
127
|
return rec;
|
|
104
128
|
}
|
|
105
129
|
|
package/package.json
CHANGED