great-cto 3.27.1 → 3.27.3

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.1",
4
+ "version": "3.27.3",
5
5
  "author": {
6
6
  "name": "Alexander Velikiy",
7
7
  "url": "https://hashnode.com/@Greatcto"
@@ -284,6 +284,10 @@
284
284
  {
285
285
  "type": "command",
286
286
  "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/pipeline-stall-guard.mjs\" 2>/dev/null || true"
287
+ },
288
+ {
289
+ "type": "command",
290
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/cross-review-gate.mjs\" 2>/dev/null || true"
287
291
  }
288
292
  ]
289
293
  }
@@ -25,6 +25,9 @@ import { parseAgentBudgets, upsertAgentBudget, removeAgentBudget } from '../../.
25
25
  import { resolveSecondOpinion, SECOND_OPINION_PROVIDERS } from '../../../scripts/lib/second-opinion.mjs';
26
26
  import { detectCodex } from '../../../scripts/lib/codex-exec.mjs';
27
27
  import { upsertCapability, capabilitiesFromProjectMd } from '../../../scripts/lib/stack-capabilities.mjs';
28
+ // Moved to scripts/lib so the cross-review Stop hook can ask the same question
29
+ // without importing a server. Re-exported here: this module's callers are unchanged.
30
+ import { secondOpinionForTree } from '../../../scripts/lib/second-opinion-for-tree.mjs';
28
31
  import { getAgentsFleet, getAgentProfile, retireAgent, restoreAgent, appendDecisionLog, readDecisionsLog } from './fleet.mjs';
29
32
  import { getResume, getShareState, toggleShare } from './share.mjs';
30
33
  import { listSessions, readSession, editedFiles, searchSessions } from './transcripts.mjs';
@@ -1952,34 +1955,5 @@ async function dispatch(req, res, url, cwd) {
1952
1955
  * a throw, because "could not tell" is data for the row, not a reason to lose
1953
1956
  * the inbox.
1954
1957
  */
1955
- export function secondOpinionForTree(c) {
1956
- let projectMd = null;
1957
- try { projectMd = fs.readFileSync(path.join(c, '.great_cto', 'PROJECT.md'), 'utf8'); } catch { projectMd = null; }
1958
- let declared = null;
1959
- try { declared = projectMd == null ? null : capabilitiesFromProjectMd(projectMd).map.second_opinion; } catch { declared = null; }
1960
- const tool = declared?.tool ?? null;
1961
- const declaredState = declared?.state ?? (projectMd == null ? 'no-project-md' : 'undeclared');
1962
- let head = null;
1963
- try { head = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: c, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || null; } catch { head = null; }
1964
- const base = { declared: declaredState, tool, head, verdict: null, findings: null, p0: null, sha: null, ts: null };
1965
- if (declaredState !== 'declared' || !tool) {
1966
- return { ...base, state: 'not-run', why: declaredState === 'none' ? 'second_opinion: none — deliberately off' : 'no second opinion declared in PROJECT.md' };
1967
- }
1968
- let lines = [];
1969
- try { lines = fs.readFileSync(path.join(c, '.great_cto', 'cross-review.log'), 'utf8').trim().split('\n').filter(Boolean); }
1970
- catch { return { ...base, state: 'unmeasured', why: `declared (${tool}), no review has been written yet` }; }
1971
- const rows = [];
1972
- for (const line of lines) { try { rows.push(JSON.parse(line)); } catch { /* counted by /api/harnesses; not a verdict either way */ } }
1973
- const paired = rows.filter((r) => typeof r.sha === 'string' && r.sha !== '' && head && (r.sha === head || head.startsWith(r.sha) || r.sha.startsWith(head)) && r.state === 'ok');
1974
- if (paired.length) {
1975
- const r = paired[paired.length - 1];
1976
- return { ...base, state: 'ok', verdict: r.verdict ?? null, findings: r.findings ?? null, p0: r.p0 ?? null, sha: r.sha, ts: r.ts ?? null, why: '' };
1977
- }
1978
- const anyKeyed = rows.some((r) => typeof r.sha === 'string' && r.sha !== '');
1979
- if (!anyKeyed && rows.length) {
1980
- return { ...base, state: 'unreadable', why: `${rows.length} review line(s) predate the join key — none can be paired with this tree` };
1981
- }
1982
- return { ...base, state: 'unmeasured', why: `declared (${tool}), no review line for ${head ? head.slice(0, 8) : 'this tree'}` };
1983
- }
1984
1958
 
1985
- export { dispatch };
1959
+ export { dispatch, secondOpinionForTree };
@@ -3046,7 +3046,10 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
3046
3046
  .inbox-row .ttl { font-size: var(--fs-small); }
3047
3047
  /* A stalled task is one line: id · reason · title · agent, and the re-run button
3048
3048
  sits at the row's right instead of on a second grid row under it. */
3049
- .inbox-row { grid-template-columns: 110px 1fr auto auto; align-items: center; }
3049
+ .inbox-row { grid-template-columns: 110px 1fr auto auto auto; align-items: center; }
3050
+ .inbox-row .actions:empty { display: none; }
3051
+ .ledger-title-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; margin-bottom: 12px; }
3052
+ .side-task-actions .gate-btn { flex: 0 0 auto; min-width: 96px; }
3050
3053
  .inbox-row .ttl { flex-direction: row; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; }
3051
3054
  .inbox-row .ttl > :first-child { display: inline; }
3052
3055
  .inbox-row .ttl .why { display: inline-block; font-size: var(--fs-eyebrow); padding: 0 6px; line-height: 18px; white-space: nowrap; }
@@ -3629,7 +3632,21 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
3629
3632
 
3630
3633
  <div class="panel" id="panel-budgets" role="tabpanel">
3631
3634
  <div class="metrics-page">
3632
- <h1 class="page-title">Ledger</h1>
3635
+ <div class="ledger-title-row">
3636
+ <h1 class="page-title" style="margin:0">Ledger</h1>
3637
+ <!-- The period governs every figure below it, so it sits where the
3638
+ figures start — beside the name, not under the stuck list. -->
3639
+ <div class="period-toolbar" style="margin:0">
3640
+ <span class="period-label">PERIOD</span>
3641
+ <div class="period-chips" id="period-chips">
3642
+ <button class="period-chip" data-period="1">1D</button>
3643
+ <button class="period-chip" data-period="7">7D</button>
3644
+ <button class="period-chip active" data-period="30">30D</button>
3645
+ <button class="period-chip" data-period="90">90D</button>
3646
+ <button class="period-chip" data-period="365">1Y</button>
3647
+ </div>
3648
+ </div>
3649
+ </div>
3633
3650
  <!-- Ledger (great_cto-ki1x.7): the one decision this screen supports is
3634
3651
  "is something running that I should stop". Two figures, the stage
3635
3652
  strip and the stuck list answer it; the caps table below is where
@@ -3638,16 +3655,6 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
3638
3655
  <!-- From the old Metrics tab (great_cto-ki1x.16): the period chips, the caps
3639
3656
  card and the cost panel keep their ids — the code that fills them did
3640
3657
  not move, only the screen that shows them. -->
3641
- <div class="period-toolbar">
3642
- <span class="period-label">PERIOD</span>
3643
- <div class="period-chips" id="period-chips">
3644
- <button class="period-chip" data-period="1">1D</button>
3645
- <button class="period-chip" data-period="7">7D</button>
3646
- <button class="period-chip active" data-period="30">30D</button>
3647
- <button class="period-chip" data-period="90">90D</button>
3648
- <button class="period-chip" data-period="365">1Y</button>
3649
- </div>
3650
- </div>
3651
3658
  <!-- Budgets live HERE, on Metrics, not two clicks deep in the Agents
3652
3659
  drill-in. A control the operator could not find is a control they do
3653
3660
  not have — and it was invisible on top of that, hidden whenever a
@@ -4553,6 +4560,11 @@ function renderInboxList(rootId, items, countId, opts = {}) {
4553
4560
  + `<button type="button" class="gate-btn" title="Copy a git diff limited to the reviewed files"`
4554
4561
  + ` onclick="event.stopPropagation(); copyDiff(${esc(JSON.stringify(JSON.stringify(t.__files)))})">Copy diff</button></div>`
4555
4562
  : '';
4563
+ const unblock = (!canDecide && !t.__synthetic && t.id && (t.raw_status || t.status) === 'blocked')
4564
+ ? `<div class="actions" onclick="event.stopPropagation()">`
4565
+ + `<button type="button" class="gate-btn gate-approve" title="Set the task back to open"`
4566
+ + ` onclick="event.stopPropagation(); taskDecision('${esc(t.id)}', 'open')">Unblock</button></div>`
4567
+ : '';
4556
4568
  const rerun = (!canDecide && !t.__synthetic && t.agent && t.id)
4557
4569
  ? `<div class="actions" onclick="event.stopPropagation()">`
4558
4570
  + `<button type="button" class="gate-btn" title="Copy the line that re-runs this agent"`
@@ -4607,7 +4619,7 @@ function renderInboxList(rootId, items, countId, opts = {}) {
4607
4619
  ${canDecide ? gateChip(t) : `<span class="id">${esc(t.id || '')}</span>`}
4608
4620
  <span class="ttl">${canDecide ? '' : reasonChip}${esc(canDecide ? String(t.title || '').replace(/^gate:[a-z-]+\s*[—–-]?\s*/, '') : t.title)}${desc}<span class="meta">${meta}</span>${tierNote}</span>
4609
4621
  ${canDecide ? reviewerCells(t) : ''}
4610
- ${actions}${rerun}${diffBtn}
4622
+ ${actions}${unblock}${rerun}${diffBtn}
4611
4623
  </div>`;
4612
4624
  }).join('');
4613
4625
  }
@@ -7877,11 +7889,22 @@ function openSide(task) {
7877
7889
  Reject
7878
7890
  </button>
7879
7891
  </div>` : '';
7892
+ // A task that is not a gate still needs a decision the operator can take
7893
+ // here: a blocked one is unblocked, an open one is started, either is closed.
7894
+ // Before this the drawer showed the status and told the operator to go run
7895
+ // `bd update` — a board that could read the state but not change it.
7896
+ const taskActions = (!task.is_gate && rawStatus !== 'closed') ? `
7897
+ <div class="side-gate-actions side-task-actions">
7898
+ ${rawStatus === 'blocked' ? `<button class="gate-btn gate-approve" onclick="taskDecision('${esc(task.id)}', 'open')">Unblock</button>` : ''}
7899
+ ${rawStatus === 'open' ? `<button class="gate-btn gate-approve" onclick="taskDecision('${esc(task.id)}', 'in_progress')">Start</button>` : ''}
7900
+ ${rawStatus === 'in_progress' || rawStatus === 'open' ? `<button class="gate-btn" onclick="taskDecision('${esc(task.id)}', 'blocked')">Block</button>` : ''}
7901
+ <button class="gate-btn gate-reject" onclick="taskDecision('${esc(task.id)}', 'closed')">Close</button>
7902
+ </div>` : '';
7880
7903
  _sideTask = task; // so the doc viewer's "← back" can restore this task
7881
7904
  document.getElementById('side-body').innerHTML = `
7882
7905
  <div class="side-title">${esc(task.title)}</div>
7883
7906
  ${desc}
7884
- ${gateActions}
7907
+ ${gateActions}${taskActions}
7885
7908
  <div class="props">
7886
7909
  ${rows.map(([k, v]) => `<div class="prop-key">${k}</div><div class="prop-val">${v}</div>`).join('')}
7887
7910
  </div>
@@ -7914,6 +7937,23 @@ function openSide(task) {
7914
7937
  </div>`).join('')}`;
7915
7938
  });
7916
7939
  }
7940
+ // One status change, through the endpoint the board has had since the kanban:
7941
+ // POST /api/tasks/:id/status. Closing asks once — it is the only transition the
7942
+ // list stops showing; the rest are visible and reversible from the same drawer.
7943
+ async function taskDecision(id, status) {
7944
+ if (status === 'closed' && !confirm(`Close ${id}? It leaves every list on this board.`)) return;
7945
+ const r = await api(`/api/tasks/${encodeURIComponent(id)}/status${pqs()}`, {
7946
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }),
7947
+ }).catch(() => null);
7948
+ if (!r || r.error) { showToast('✗ ' + esc(r?.error || 'status not changed'), 'error', 4000); return; }
7949
+ const said = { open: 'unblocked', in_progress: 'started', blocked: 'blocked', closed: 'closed' }[status] || status;
7950
+ showToast(`<strong>${esc(id)}</strong> ${said}`, status === 'closed' ? 'error' : 'success', 2500);
7951
+ if (_sideTask && _sideTask.id === id) {
7952
+ if (status === 'closed') closeSide();
7953
+ else openSide({ ..._sideTask, status, raw_status: status });
7954
+ }
7955
+ refreshInbox();
7956
+ }
7917
7957
  // ── Documents referenced by a task: extraction + the inline side-panel viewer ──
7918
7958
  let _sideTask = null;
7919
7959
  function extractDocRefs(task) {
@@ -0,0 +1,48 @@
1
+ // One reader for "has another model seen THIS tree", used by the board and by
2
+ // the Stop gate.
3
+ //
4
+ // It lived inside the board's route module, which was fine while the board was
5
+ // the only thing asking. The cross-review Stop gate asks the same question, and
6
+ // a hook importing the board's server routes to get it would drag a server into
7
+ // a hook — so the reader moved here and routes.mjs re-exports it. One reader,
8
+ // because two would drift and then disagree about whether a diff was reviewed.
9
+ //
10
+ // The states are the point, and none of them is a pass:
11
+ // ok — a review line joins this HEAD; carries its verdict
12
+ // not-run — no second opinion is declared (including a deliberate `none`)
13
+ // unmeasured — declared, but nothing has reviewed THIS tree
14
+ // unreadable — lines exist and none can be paired (they predate the join key)
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ import { execFileSync } from 'node:child_process';
18
+ import { capabilitiesFromProjectMd } from './stack-capabilities.mjs';
19
+
20
+ export function secondOpinionForTree(c) {
21
+ let projectMd = null;
22
+ try { projectMd = fs.readFileSync(path.join(c, '.great_cto', 'PROJECT.md'), 'utf8'); } catch { projectMd = null; }
23
+ let declared = null;
24
+ try { declared = projectMd == null ? null : capabilitiesFromProjectMd(projectMd).map.second_opinion; } catch { declared = null; }
25
+ const tool = declared?.tool ?? null;
26
+ const declaredState = declared?.state ?? (projectMd == null ? 'no-project-md' : 'undeclared');
27
+ let head = null;
28
+ try { head = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: c, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || null; } catch { head = null; }
29
+ const base = { declared: declaredState, tool, head, verdict: null, findings: null, p0: null, sha: null, ts: null };
30
+ if (declaredState !== 'declared' || !tool) {
31
+ return { ...base, state: 'not-run', why: declaredState === 'none' ? 'second_opinion: none — deliberately off' : 'no second opinion declared in PROJECT.md' };
32
+ }
33
+ let lines = [];
34
+ try { lines = fs.readFileSync(path.join(c, '.great_cto', 'cross-review.log'), 'utf8').trim().split('\n').filter(Boolean); }
35
+ catch { return { ...base, state: 'unmeasured', why: `declared (${tool}), no review has been written yet` }; }
36
+ const rows = [];
37
+ for (const line of lines) { try { rows.push(JSON.parse(line)); } catch { /* counted by /api/harnesses; not a verdict either way */ } }
38
+ const paired = rows.filter((r) => typeof r.sha === 'string' && r.sha !== '' && head && (r.sha === head || head.startsWith(r.sha) || r.sha.startsWith(head)) && r.state === 'ok');
39
+ if (paired.length) {
40
+ const r = paired[paired.length - 1];
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
+ }
43
+ const anyKeyed = rows.some((r) => typeof r.sha === 'string' && r.sha !== '');
44
+ if (!anyKeyed && rows.length) {
45
+ return { ...base, state: 'unreadable', why: `${rows.length} review line(s) predate the join key — none can be paired with this tree` };
46
+ }
47
+ return { ...base, state: 'unmeasured', why: `declared (${tool}), no review line for ${head ? head.slice(0, 8) : 'this tree'}` };
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "3.27.1",
3
+ "version": "3.27.3",
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",