great-cto 3.27.5 → 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.5",
4
+ "version": "3.28.0",
5
5
  "author": {
6
6
  "name": "Alexander Velikiy",
7
7
  "url": "https://hashnode.com/@Greatcto"
@@ -6957,10 +6957,19 @@ async function init() {
6957
6957
  shareState = share || { enabled: false, url: null };
6958
6958
  allProjects = projs || [];
6959
6959
 
6960
- // Prefer URL param slug; fall back to extracting from first task id
6960
+ // Prefer the URL, then a REGISTERED project, then a guess that is known to be
6961
+ // one. Deriving the slug from a task id's prefix is a heuristic: bd ids are
6962
+ // short, so a project registered as `acme-storefront` whose tasks read
6963
+ // `acme-6f` yielded `acme`, which is not a project, and the board then told
6964
+ // the operator in red that `acme was not found` — on a perfectly healthy
6965
+ // setup. Every README screenshot has carried that banner since.
6966
+ // A prefix is used only when it names something the registry actually has.
6967
+ const fromTaskId = (allTasks[0]?.id || '').split('-')[0];
6968
+ const registered = (allProjects || []).some((p) => p.slug === fromTaskId) ? fromTaskId : '';
6961
6969
  const proj = urlProject
6962
- || (allTasks[0]?.id || '').split('-')[0]
6970
+ || registered
6963
6971
  || (allProjects[0]?.slug)
6972
+ || fromTaskId
6964
6973
  || 'project';
6965
6974
  currentProject = proj;
6966
6975
  document.getElementById('project-name').textContent = proj;
@@ -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.5",
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",