great-cto 3.27.6 → 3.28.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.
@@ -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.27.6",
4
+ "version": "3.28.0",
5
5
  "author": {
6
6
  "name": "Alexander Velikiy",
7
7
  "url": "https://hashnode.com/@Greatcto"
@@ -112,8 +112,21 @@ export function parseFindings(text) {
112
112
  const m = line.match(/^(.+?):(\d+)\s*\|\s*(P[012])\s*\|\s*(.+)$/i);
113
113
  if (m) findings.push({ file: m[1].trim(), line: parseInt(m[2], 10), severity: m[3].toUpperCase(), issue: m[4].trim() });
114
114
  }
115
- // Derive verdict if the model omitted it: any P0 → BLOCK.
116
- if (!verdict) verdict = findings.some(f => f.severity === 'P0') ? 'BLOCK' : 'PASS';
115
+ // Derive a verdict only in the direction that is safe to be wrong about.
116
+ //
117
+ // This used to answer PASS whenever no `VERDICT:` line was found and no P0
118
+ // was parsed. But an empty body, a refusal, a reply in some other format and
119
+ // a model that genuinely said "looks clean" are the SAME input here — all
120
+ // four leave `verdict` null and `findings` without a P0. Reading any of them
121
+ // as PASS reads all of them as PASS, so a review that could not be read
122
+ // became a review that approved, and the Stop hook downstream ended the turn
123
+ // on it.
124
+ //
125
+ // A derivation that BLOCKS costs a second look. A derivation that PASSES
126
+ // spends the guarantee. So: a P0 with no verdict line still blocks; nothing
127
+ // else is turned into a verdict, and `null` means the model never reached
128
+ // one. Callers must treat null as "no verdict", never as a pass.
129
+ if (!verdict && findings.some(f => f.severity === 'P0')) verdict = 'BLOCK';
117
130
  return { findings, verdict };
118
131
  }
119
132
 
@@ -216,6 +229,17 @@ async function main(argv) {
216
229
  const cost = res.usage && priced ? round4(costForUsage({ model: res.model, usage: res.usage })) : null;
217
230
 
218
231
  for (const f of findings) console.log(` ${f.severity} ${f.file}:${f.line} — ${f.issue}`);
232
+ if (verdict == null) {
233
+ // The call succeeded and the answer is unusable. `ok` would put it in the
234
+ // log as a reviewed tree and exit 0 would tell the caller it passed; both
235
+ // are the claim this tool exists to refuse to make.
236
+ console.log(`\ncross-model-review (${decision.provider}:${res.model}): the answer carries no verdict `
237
+ + `(${findings.length} finding(s) parsed). Not a pass — re-run, or say in your answer that the reviewer `
238
+ + `returned nothing readable.`);
239
+ log({ provider: decision.provider, model: res.model, state: 'unreadable', verdict: null, findings, cost,
240
+ error_kind: 'no-verdict' });
241
+ process.exit(EXIT.SKIPPED);
242
+ }
219
243
  console.log(`\ncross-model-review (${decision.provider}:${res.model}): ${findings.length} finding(s), VERDICT: ${verdict} (${cost == null ? 'cost unpriced' : '$' + cost})`);
220
244
  log({ provider: decision.provider, model: res.model, state: 'ok', verdict, findings, cost });
221
245
  process.exit(verdict === 'BLOCK' ? EXIT.BLOCK : EXIT.PASS);
@@ -40,6 +40,17 @@ export function secondOpinionForTree(c) {
40
40
  const r = paired[paired.length - 1];
41
41
  return { ...base, state: 'ok', verdict: r.verdict ?? null, findings: r.findings ?? null, p0: r.p0 ?? null, sha: r.sha, ts: r.ts ?? null, why: '' };
42
42
  }
43
+ // A line that joins this tree but reached no verdict is not "no review line".
44
+ // It is a review that produced nothing readable, and saying so is the whole
45
+ // point — falling through to `unmeasured` would tell the operator to run a
46
+ // review that already ran.
47
+ const joins = (r) => typeof r.sha === 'string' && r.sha !== '' && head && (r.sha === head || head.startsWith(r.sha) || r.sha.startsWith(head));
48
+ const noVerdict = rows.filter((r) => joins(r) && r.state === 'unreadable');
49
+ if (noVerdict.length) {
50
+ const r = noVerdict[noVerdict.length - 1];
51
+ return { ...base, state: 'unreadable', sha: r.sha, ts: r.ts ?? null,
52
+ why: `${tool} answered for this tree and the answer carries no verdict — it is not a pass` };
53
+ }
43
54
  const anyKeyed = rows.some((r) => typeof r.sha === 'string' && r.sha !== '');
44
55
  if (!anyKeyed && rows.length) {
45
56
  return { ...base, state: 'unreadable', why: `${rows.length} review line(s) predate the join key — none can be paired with this tree` };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "3.27.6",
3
+ "version": "3.28.0",
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",