great-cto 3.62.2 → 3.63.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 72 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.62.2",
4
+ "version": "3.63.0",
5
5
  "author": {
6
6
  "name": "Alexander Velikiy",
7
7
  "url": "https://hashnode.com/@Greatcto"
@@ -16,6 +16,30 @@ const date = v => typeof v === 'string' && Number.isFinite(Date.parse(v)) ? v :
16
16
  const RUN_PHASE = { ready: 'accepted', 'awaiting-gate': 'needs_decision',
17
17
  'awaiting-release': 'needs_decision', blocked: 'blocked', 'manual-action': 'blocked' };
18
18
 
19
+ /** Recorded observations are not current-tree verification or gate authority. */
20
+ function evidenceSections(entry) {
21
+ const events = entry.inspector?.state === 'recorded' ? entry.inspector.events.length : 0;
22
+ const verdicts = entry.evidence.filter(e => e.kind === 'verdict');
23
+ const reported = verdicts.filter(e => e.verifierReported === true).length;
24
+ const publication = entry.publication;
25
+ const published = ['prepared', 'pushed', 'pr-linked'].includes(publication?.state);
26
+ return [
27
+ { id: 'execution', label: 'Execution', state: events ? 'recorded' : 'unknown',
28
+ detail: events ? `${events} linked activity events in the bounded window. Liveness and task acceptance are not established.${entry.inspector.partial ? ' The activity window is incomplete.' : ''}`
29
+ : entry.inspector?.reason || 'No explicitly linked execution events are available.' },
30
+ { id: 'checks', label: 'Checks', state: verdicts.length ? 'recorded' : 'unknown',
31
+ detail: verdicts.length ? `In this bounded view: ${verdicts.length} role verdict records; ${reported} verifier-success reports. Test counts and current input bindings are not available in this projection.`
32
+ : 'No task-linked check records are available. Completed roles do not establish verification.' },
33
+ { id: 'review', label: 'Review', state: 'unknown',
34
+ detail: 'No task-bound review input digest and reviewer provenance are available. Role verdicts alone do not establish independent review.' },
35
+ { id: 'publication', label: 'Publication', state: published ? 'recorded' : 'unknown',
36
+ detail: published ? `Publication state: ${publication.state}${/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(publication.head || '') ? `; recorded head: ${publication.head}` : ''}. This does not establish merge, release or deployment.`
37
+ : 'No task-linked publication record is available.' },
38
+ { id: 'runtime', label: 'Runtime', state: 'unknown',
39
+ detail: 'No task-linked runtime probe is available. Release records and successful process exits do not establish deployment health.' },
40
+ ];
41
+ }
42
+
19
43
  export function projectWork({ projectId, issues = [], codex = { state: 'absent', runs: [] },
20
44
  tasks = [], sessions = [], sources = [], activity = { state: 'none', events: [] }, observedAt = new Date().toISOString() }) {
21
45
  const entries = [];
@@ -78,7 +102,8 @@ export function projectWork({ projectId, issues = [], codex = { state: 'absent',
78
102
  host: t.host, phase: t.phase, nativeState: run?.nativeState || t.phase,
79
103
  terminal: ['cancelled', 'completed'].includes(t.phase), updatedAt: t.updatedAt, outcome: t.outcome?.state === 'completed' ? `${t.intent || 'delivery'} outcome explicitly completed` : null, reason: t.reason,
80
104
  intent: t.intent || 'delivery', stage: t.stage || null, taskOutcome: t.outcome || null,
81
- decisions: decisionCapabilities(t).map(d => ({ ...d, id: d.decisionId, label: d.label || 'Host decision' })), evidence: (t.evidence || []).map(e => ({ kind: 'verdict', label: `${e.role}: ${e.verdict || 'not recorded'}` })),
105
+ decisions: decisionCapabilities(t).map(d => ({ ...d, id: d.decisionId, label: d.label || 'Host decision' })), evidence: (t.evidence || []).slice(0, 200).filter(e => e && text(e.role)).map(e => ({ kind: 'verdict',
106
+ label: `${e.role.slice(0, 80)}: ${text(e.verdict)?.slice(0, 80) || 'not recorded'}`, verifierReported: typeof e.verified === 'boolean' ? e.verified : null })),
82
107
  release: run?.release || null, revision: t.revision, metrics: t.metrics,
83
108
  inspector: taskInspector([...t.links.runs, ...t.links.sessions], activity),
84
109
  publication: t.publication || null,
@@ -90,6 +115,7 @@ export function projectWork({ projectId, issues = [], codex = { state: 'absent',
90
115
  command: approveCommand || (enabled ? `great-cto resume --task ${t.taskId} --host ${t.host} --revision ${t.revision}` : null),
91
116
  });
92
117
  }
118
+ for (const entry of entries) entry.evidenceSections = evidenceSections(entry);
93
119
  entries.sort((a, b) => (a.terminal - b.terminal) || (b.decisions.length - a.decisions.length)
94
120
  || (Date.parse(b.updatedAt || '') || 0) - (Date.parse(a.updatedAt || '') || 0) || a.key.localeCompare(b.key));
95
121
  const health = sources.some(s => ['degraded', 'unavailable'].includes(s.health)) ? 'degraded' : 'current';
@@ -28,6 +28,7 @@
28
28
  + action + (e.command ? `<pre>${esc(e.command)}</pre>` : '')
29
29
  + (!cap && e.capabilities[0]?.reason ? `<p>${esc(e.capabilities[0].reason)}</p>` : '')
30
30
  + renderInspector(e.inspector)
31
+ + renderEvidence(e.evidenceSections)
31
32
  + (e.publicationPreview ? `<label>Publication paths (comma-separated) <input data-publication-allow="${esc(e.key)}" value="${esc(e.publicationAllow || '')}" placeholder="src,tests"></label><button type="button" class="gate-btn" data-work-key="${esc(e.key)}" data-work-action="preview_publication">Preview draft PR publication</button><div data-publication-preview="${esc(e.key)}"></div>` : '')
32
33
  + (e.publication ? `<p>PR publication: ${esc(e.publication.state)} · ${esc(e.publication.head || '')}</p>`
33
34
  + (typeof e.publication.url === 'string' && /^https:\/\/github\.com\/[A-Za-z0-9_-]+\/[A-Za-z0-9_.-]+\/pull\/[1-9][0-9]*$/.test(e.publication.url) ? `<a href="${esc(e.publication.url)}" target="_blank" rel="noopener noreferrer">Open draft PR result</a>` : '')
@@ -56,6 +57,12 @@
56
57
  + (e.durationMs !== null ? ` · ${esc(e.durationMs)} ms` : '') + '</li>').join('') + '</ol>' : '')
57
58
  + '</details>';
58
59
  }
60
+ function renderEvidence(sections) {
61
+ if (!Array.isArray(sections)) return '';
62
+ return '<details data-work-detail="evidence"><summary>Task evidence · read-only</summary>'
63
+ + '<p>Recorded means an observation exists, not that the current code passed a gate. Unknown means evidence is unavailable, not zero or success.</p>'
64
+ + '<dl>' + sections.map(s => `<dt>${esc(s.label)} · ${s.state === 'recorded' ? 'Recorded' : 'Unknown'}</dt><dd>${esc(s.detail)}</dd>`).join('') + '</dl></details>';
65
+ }
59
66
  function publicationCommand(preview) {
60
67
  const confirmation = preview.confirmation || { expectedRevision: preview.revision, approval: preview.approval };
61
68
  if (!/^[0-9a-f-]{36}$/.test(preview.taskId || '') || !Number.isInteger(confirmation.expectedRevision) || confirmation.expectedRevision < 1
@@ -3378,6 +3378,8 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
3378
3378
  .work-card h2 { font-size: var(--fs-title-s); }
3379
3379
  .work-card p, #work-compose p { margin: 8px 0; font-size: var(--fs-body); }
3380
3380
  .work-meta { color: var(--text2); font-size: var(--fs-caption); }
3381
+ .work-card [data-work-detail="evidence"] dt { margin-top: 12px; font-weight: 600; }
3382
+ .work-card [data-work-detail="evidence"] dd { margin: 4px 0 12px; color: var(--text2); font-size: var(--fs-body); overflow-wrap: anywhere; }
3381
3383
  .work-warning { color: var(--absent-warn-fg); background: var(--bg-card); border: 1px solid var(--absent-warn-fg); padding: 12px; }
3382
3384
  .work-page pre { font-weight: 400; white-space: pre-wrap; overflow-wrap: anywhere; margin: 12px 0; }
3383
3385
  #work-compose textarea, #work-compose input, #work-compose select { display: block; width: 100%; margin: 8px 0 12px; padding: 8px; background: var(--bg-muted); color: var(--text); border: 1px solid var(--border-strong); border-radius: 4px; }
@@ -4224,8 +4226,12 @@ function reviewerCells(t) {
4224
4226
  if (!so || so.state === 'not-run') codex = `<span class="abs" title="${esc(so?.why || 'second opinion not run')}">— Codex</span>`;
4225
4227
  else if (so.state === 'unmeasured') codex = `<span class="abs" title="${esc(so.why)}">· Codex</span>`;
4226
4228
  else if (so.state === 'unreadable') codex = `<span class="abs" title="${esc(so.why)}">! Codex</span>`;
4227
- else if (so.verdict === 'BLOCK') codex = `<span title="Codex BLOCK on ${esc((so.sha || '').slice(0, 8))} — ${esc(String(so.p0 ?? '?'))}×P0"><span class="dot bad"></span> Codex ✗</span>`;
4228
- else codex = `<span title="Codex ${esc(so.verdict || 'PASS')} on ${esc((so.sha || '').slice(0, 8))}"><span class="dot ok"></span> Codex ✓</span>`;
4229
+ else if (so.state === 'unreviewable') codex = `<span class="abs" title="${esc(so.why)}">! Codex</span>`;
4230
+ else if (so.state === 'ok' && so.verdict === 'BLOCK') codex = `<span title="Codex BLOCK on ${esc((so.sha || '').slice(0, 8))} — ${esc(String(so.p0 ?? '?'))}×P0"><span class="dot bad"></span> Codex ✗</span>`;
4231
+ // A tick only for a review that passed — never as the fallback for a state
4232
+ // this renderer does not know (an `unreviewable` diff once fell through to ✓).
4233
+ else if (so.state === 'ok' && so.verdict === 'PASS') codex = `<span title="Codex PASS on ${esc((so.sha || '').slice(0, 8))}"><span class="dot ok"></span> Codex ✓</span>`;
4234
+ else codex = `<span class="abs" title="${esc(so.why || `second opinion: ${so.state || 'unknown'}, verdict ${so.verdict ?? 'none'}`)}">? Codex</span>`;
4229
4235
  return `<span class="gate-review">${claude}${codex}</span>`;
4230
4236
  }
4231
4237
  function gateChip(t) {
@@ -1,9 +1,9 @@
1
1
  // scripts/lib/cross-model-review.mjs — cross-model adversarial review (architect-loop R3).
2
2
  //
3
- // great_cto's reviews are Claude-on-Claude → same-model blind spots. This red-teams
4
- // the diff with a DIFFERENT model via OpenRouter (default openai/gpt-5), flagging
3
+ // Review the supplied diff through the configured second-opinion provider, flagging
5
4
  // ONLY correctness / requirement / invariant gaps with file:line — no style. The
6
5
  // code-reviewer agent merges these with its own findings for high-stakes changes.
6
+ // Provider/model metadata alone does not establish cross-model independence.
7
7
  //
8
8
  // Pure (buildReviewPrompt / parseFindings / pickReviewerModel) is unit-tested with
9
9
  // no network; the CLI does the live OpenRouter call.
@@ -21,6 +21,7 @@ import { resolveSecondOpinion, codexReview } from './second-opinion.mjs';
21
21
  import { principalError } from './provider-exhaustion.mjs';
22
22
  import { existsSync, appendFileSync, mkdirSync } from 'node:fs';
23
23
  import { join } from 'node:path';
24
+ import { reviewInputBinding, parseLegacyReview } from './review-contract.mjs';
24
25
 
25
26
  /**
26
27
  * Exit codes. The first version had two: 0 for PASS and 1 for everything else —
@@ -64,13 +65,14 @@ export function decideProvider({ argv = [], projectMd = '', codex = null, env =
64
65
  * written before this field existed, and any caller that doesn't supply them,
65
66
  * still serializes to the same shape a reader already knows how to parse.
66
67
  */
67
- export function reviewLogLine({ provider, model, state, verdict, findings, cost, source, error_kind, resets_at, sha, dirty }) {
68
+ export function reviewLogLine({ provider, model, state, verdict, findings, cost, source, error_kind, resets_at, sha, dirty, review_input }) {
68
69
  return JSON.stringify({
69
70
  ts: new Date().toISOString(), provider, model: model ?? null, state, verdict: verdict ?? null,
70
71
  error_kind: error_kind ?? null, resets_at: resets_at ?? null,
71
72
  findings: Array.isArray(findings) ? findings.length : null,
72
73
  p0: Array.isArray(findings) ? findings.filter((f) => f.severity === 'P0').length : null,
73
74
  cost: cost ?? null, source, sha: sha ?? null, dirty: dirty ?? null,
75
+ review_input: review_input ?? null,
74
76
  });
75
77
  }
76
78
 
@@ -84,8 +86,8 @@ export function pickReviewerModel(env = process.env) {
84
86
  /** Build the red-team prompt. Calibrated: correctness/invariant only, file:line, no style. */
85
87
  export function buildReviewPrompt({ diff, spec }) {
86
88
  const system =
87
- 'You are an adversarial code reviewer from a DIFFERENT model family than the author. ' +
88
- 'Your job is to catch what a same-model reviewer would miss. Review ONLY for: ' +
89
+ 'You are an adversarial code reviewer. Do not assume your model family differs from the author. ' +
90
+ 'Review ONLY for: ' +
89
91
  'correctness bugs, violated requirements, broken invariants, security holes, data loss. ' +
90
92
  'Do NOT report style, naming, or preferences. Ground every finding in the diff with file:line. ' +
91
93
  'Default to silence over a weak finding. ' +
@@ -102,32 +104,7 @@ export function buildReviewPrompt({ diff, spec }) {
102
104
 
103
105
  /** Parse the model's findings + verdict. */
104
106
  export function parseFindings(text) {
105
- const findings = [];
106
- let verdict = null;
107
- for (const raw of String(text).split('\n')) {
108
- const line = raw.trim();
109
- const v = line.match(/^VERDICT:\s*(BLOCK|PASS)/i);
110
- if (v) { verdict = v[1].toUpperCase(); continue; }
111
- // <file>:<line> | <SEV> | <issue>
112
- const m = line.match(/^(.+?):(\d+)\s*\|\s*(P[012])\s*\|\s*(.+)$/i);
113
- if (m) findings.push({ file: m[1].trim(), line: parseInt(m[2], 10), severity: m[3].toUpperCase(), issue: m[4].trim() });
114
- }
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';
130
- return { findings, verdict };
107
+ return parseLegacyReview(text);
131
108
  }
132
109
 
133
110
  // ── CLI ───────────────────────────────────────────────────────────────────────
@@ -178,8 +155,9 @@ async function main(argv) {
178
155
  const projectMd = existsSync(mdPath) ? readFileSync(mdPath, 'utf8') : '';
179
156
  const decision = decideProvider({ argv, projectMd });
180
157
  const identity = gitIdentity(root);
158
+ let review_input = null;
181
159
  const logPath = join(root, '.great_cto', 'cross-review.log');
182
- const log = (rec) => { try { mkdirSync(join(root, '.great_cto'), { recursive: true }); appendFileSync(logPath, reviewLogLine({ ...rec, source: decision.source, sha: identity.sha, dirty: identity.dirty }) + '\n'); } catch { /* the log is evidence, not a gate */ } };
160
+ const log = (rec) => { try { mkdirSync(join(root, '.great_cto'), { recursive: true }); appendFileSync(logPath, reviewLogLine({ ...rec, source: decision.source, sha: identity.sha, dirty: identity.dirty, review_input }) + '\n'); } catch { /* the log is evidence, not a gate */ } };
183
161
 
184
162
  // Anything that is not a reviewer reviewing exits SKIPPED — not PASS, and not
185
163
  // the BLOCK code either. The line says why, and the log keeps it.
@@ -190,10 +168,17 @@ async function main(argv) {
190
168
  }
191
169
 
192
170
  const diff = diffPath === '-' ? readFileSync(0, 'utf8') : readFileSync(diffPath, 'utf8');
193
- if (!diff.trim()) { console.log('cross-model-review: empty diff, nothing to review.'); process.exit(EXIT.PASS); }
194
171
  const specFile = readArg(argv, '--spec');
195
- const spec = specFile ? readFileSync(specFile, 'utf8').slice(0, 4000) : null;
196
- const prompt = buildReviewPrompt({ diff: diff.slice(0, 24000), spec });
172
+ const spec = specFile ? readFileSync(specFile, 'utf8') : null;
173
+ review_input = reviewInputBinding({ diff, spec, sourceSha: identity.sha });
174
+ // A truncated prefix is not a review of the full input. Never silently approve it.
175
+ if (!diff.trim() || diff.length > 24000 || (spec !== null && spec.length > 4000)) {
176
+ const error_kind = !diff.trim() ? 'empty-diff' : 'input-too-large';
177
+ console.log(`cross-model-review: SKIPPED (${error_kind}) — complete review input required.`);
178
+ log({ provider: decision.provider, state: 'unreadable', verdict: null, findings: null, cost: null, error_kind });
179
+ process.exit(EXIT.SKIPPED);
180
+ }
181
+ const prompt = buildReviewPrompt({ diff, spec });
197
182
 
198
183
  let res;
199
184
  if (decision.provider === 'codex') {
@@ -0,0 +1,66 @@
1
+ /** Host-neutral review evidence. Pure data contract, never gate authority. */
2
+ import { createHash } from 'node:crypto';
3
+
4
+ export const REVIEW_CONTRACT_VERSION = 1;
5
+ const hash = value => createHash('sha256').update(value).digest('hex');
6
+ const digest = value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
7
+
8
+ /** Bind the exact submitted UTF-8 inputs and caller context, without storing bodies. */
9
+ export function reviewInputBinding({ diff, spec = null, taskId = null, sourceSha = null } = {}) {
10
+ if (typeof diff !== 'string' || (spec !== null && typeof spec !== 'string')
11
+ || (taskId !== null && (typeof taskId !== 'string' || taskId.length > 256))
12
+ || (sourceSha !== null && !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(sourceSha))) throw Error('invalid review input');
13
+ const binding = { version: REVIEW_CONTRACT_VERSION, taskId, sourceSha,
14
+ diffSha256: hash(diff), diffBytes: Buffer.byteLength(diff),
15
+ specSha256: spec === null ? null : hash(spec), specBytes: spec === null ? null : Buffer.byteLength(spec) };
16
+ return { ...binding, inputDigest: hash(JSON.stringify(binding)) };
17
+ }
18
+
19
+ /** Normalize trusted adapter metadata plus untrusted reviewer output. */
20
+ export function normalizeReviewResult(options = {}) {
21
+ const { inputDigest, expectedInputDigest, reviewer,
22
+ state = 'ok', verdict = null, findings = null } = options && typeof options === 'object' ? options : {};
23
+ const identity = { host: typeof reviewer?.host === 'string' ? reviewer.host : null,
24
+ model: typeof reviewer?.model === 'string' ? reviewer.model : null };
25
+ const unknown = reason => ({ version: REVIEW_CONTRACT_VERSION, state: 'not-checked',
26
+ verdict: null, reason, inputDigest: digest(inputDigest) ? inputDigest : null,
27
+ reviewer: identity, findings: [] });
28
+ if (!digest(inputDigest) || !digest(expectedInputDigest) || inputDigest !== expectedInputDigest) return unknown('input-mismatch');
29
+ if (state !== 'ok') return unknown('adapter-unavailable');
30
+ if (!['PASS', 'BLOCK'].includes(verdict) || !Array.isArray(findings) || findings.length > 200) return unknown('invalid-result');
31
+ const valid = Array.from(findings).every(f => f && typeof f === 'object'
32
+ && typeof f.file === 'string' && f.file.length > 0 && f.file.length <= 1024
33
+ && !/^[\\/]|^[A-Za-z]:|[\\\x00-\x1f]/.test(f.file)
34
+ && f.file.split('/').every(part => part && part !== '.' && part !== '..')
35
+ && Number.isSafeInteger(f.line) && f.line > 0
36
+ && ['P0', 'P1', 'P2'].includes(f.severity)
37
+ && typeof f.issue === 'string' && f.issue.trim().length > 0 && f.issue.length <= 4000);
38
+ if (!valid) return unknown('invalid-findings');
39
+ const result = findings.map(({ file, line, severity, issue }) => ({ file, line, severity, issue }));
40
+ return { version: REVIEW_CONTRACT_VERSION, state: 'checked',
41
+ verdict: result.some(f => f.severity === 'P0') ? 'BLOCK' : verdict,
42
+ reason: result.some(f => f.severity === 'P0') && verdict === 'PASS' ? 'p0-overrides-pass' : null,
43
+ inputDigest, reviewer: identity, findings: result };
44
+ }
45
+
46
+ /** Legacy line protocol adapter; ambiguity never becomes approval. */
47
+ export function parseLegacyReview(text) {
48
+ if (typeof text !== 'string' || Buffer.byteLength(text) > 1024 * 1024) return { findings: [], verdict: null };
49
+ const findings = [], verdicts = []; let malformed = false;
50
+ for (const raw of text.split('\n')) {
51
+ const line = raw.trim(), v = line.match(/^VERDICT:\s*(BLOCK|PASS)$/i);
52
+ if (v) { verdicts.push(v[1].toUpperCase()); continue; }
53
+ const m = line.match(/^(.+?):(\d+)\s*\|\s*(P[012])\s*\|\s*(.+)$/i);
54
+ // Protocol keywords inside a complete finding are diagnostic text, not verdict lines.
55
+ if (m) { findings.push({ file: m[1].trim(), line: Number(m[2]), severity: m[3].toUpperCase(), issue: m[4].trim() }); continue; }
56
+ if (/\bVERDICT\b/i.test(line) || /\|\s*P\d/i.test(line) || /^.+?:[^|]*\|/.test(line) || /\|[^|]*\|/.test(line)) malformed = true;
57
+ }
58
+ // No verdict may conservatively block a concrete P0, never pass.
59
+ const verdict = malformed || verdicts.length > 1 ? null
60
+ : verdicts[0] || (findings.some(f => f.severity === 'P0') ? 'BLOCK' : null);
61
+ const pin = hash('legacy-adapter');
62
+ const normalized = normalizeReviewResult({ inputDigest: pin, expectedInputDigest: pin, verdict, findings });
63
+ // Preserve valid parsed findings for diagnostics when the verdict is missing.
64
+ const validation = normalizeReviewResult({ inputDigest: pin, expectedInputDigest: pin, verdict: 'BLOCK', findings });
65
+ return { findings: validation.findings, verdict: normalized.verdict };
66
+ }
@@ -12,6 +12,8 @@
12
12
  // not-run — no second opinion is declared (including a deliberate `none`)
13
13
  // unmeasured — declared, but nothing has reviewed THIS tree
14
14
  // unreadable — lines exist and none can be paired (they predate the join key)
15
+ // unreviewable — the review refused this tree's input (empty, or over its size
16
+ // limit) — no reviewer read it, and re-running will not change that
15
17
  import fs from 'node:fs';
16
18
  import path from 'node:path';
17
19
  import { execFileSync } from 'node:child_process';
@@ -48,6 +50,15 @@ export function secondOpinionForTree(c) {
48
50
  const noVerdict = rows.filter((r) => joins(r) && r.state === 'unreadable');
49
51
  if (noVerdict.length) {
50
52
  const r = noVerdict[noVerdict.length - 1];
53
+ // A refusal before any reviewer was asked (ADR-030: no review of a truncated
54
+ // prefix) is not an answer. Saying "answered without a verdict" sent the
55
+ // agent to re-run a review that refuses the same input every time.
56
+ if (r.error_kind === 'input-too-large' || r.error_kind === 'empty-diff') {
57
+ return { ...base, state: 'unreviewable', error_kind: r.error_kind, sha: r.sha, ts: r.ts ?? null,
58
+ why: r.error_kind === 'empty-diff'
59
+ ? 'the diff was empty — there was nothing to review'
60
+ : `the diff is too large for ${tool} to review whole (over 24 000 characters of diff or 4 000 of spec) — no reviewer read it` };
61
+ }
51
62
  return { ...base, state: 'unreadable', sha: r.sha, ts: r.ts ?? null,
52
63
  why: `${tool} answered for this tree and the answer carries no verdict — it is not a pass` };
53
64
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "3.62.2",
3
+ "version": "3.63.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",