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.7",
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 isAccepted = (v) => /^APPROVED$/i.test(v.verdict || '');
18
- const isRework = (v) => /^(CHANGES(_REQUESTED)?|REJECT(ED)?|FAIL(ED)?|BLOCKED)$/i.test(v.verdict || '');
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
- const ref = base || mergeBase(cwd) || 'HEAD';
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
- const r = treeReceipt(process.cwd());
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 (!valid) throw new Error(`invalid verdict record: ${errors.join('; ')}`);
126
+ if (errors.length) throw new Error(`invalid verdict record: ${errors.join('; ')}`);
103
127
  return rec;
104
128
  }
105
129
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "3.28.7",
3
+ "version": "3.28.9",
4
4
  "description": "One command install for the great_cto Claude Code plugin. Auto-detects your stack, picks the right archetype, bootstraps PROJECT.md.",
5
5
  "keywords": [
6
6
  "claude-code",