great-cto 3.26.0 → 3.26.2

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.26.0",
4
+ "version": "3.26.2",
5
5
  "author": {
6
6
  "name": "Alexander Velikiy",
7
7
  "url": "https://hashnode.com/@Greatcto"
@@ -648,9 +648,13 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
648
648
  .tab-btn:hover { background: var(--bg-muted); color: var(--text); }
649
649
  .tab-btn.active { background: var(--bg-strong); color: var(--text); font-weight: 500; }
650
650
 
651
-
652
- }
653
- /* Period chip-bar slim version of tab-btn */
651
+ /* A stray `}` stood here, left behind when `.leash-chip` was removed (34657cf5
652
+ added it, a later cleanup took the rule and not its brace) along with the
653
+ comment that introduced it. At the top level a `}` opens a qualified rule, so
654
+ it swallowed everything up to the next `{` — brace, dead comment and the
655
+ `.icon-btn` selector became one invalid prelude, and `.icon-btn`'s box went
656
+ with it. The icon buttons had been rendering at the browser's default size,
657
+ beside a `:hover` rule that still worked. */
654
658
  .icon-btn {
655
659
  background: transparent; border: 1px solid transparent;
656
660
  width: 30px; height: 30px;
@@ -1433,6 +1437,26 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
1433
1437
  /* Same shape as `.muted`: the `--mono` token exists and is used 115×, the
1434
1438
  CLASS that applies it did not. */
1435
1439
  .mono { font-family: var(--mono); }
1440
+ /* The board has two text inputs styled in place — `#agent-panel input` and
1441
+ `.fleet-search input[type="search"]` — and a third, the Sessions search box,
1442
+ that asked for the house look by name and got the user agent's. Same values
1443
+ as those two, said once, so the name now means what it says. */
1444
+ .input {
1445
+ background: var(--bg-muted); border: 1px solid var(--border);
1446
+ color: var(--text); font-family: var(--mono); font-size: var(--fs-body);
1447
+ padding: 8px 12px; border-radius: 0;
1448
+ }
1449
+ .input:focus { outline: 2px solid var(--focus-ring); outline-offset: 1px; border-color: transparent; }
1450
+ /* Visually hidden, still read aloud. The one use — the verdict modal's
1451
+ `aria-labelledby` target — carried the whole pattern inline, which is how a
1452
+ second use would have got a heading that is merely invisible to everyone, or
1453
+ merely visible to everyone. `clip-path` over the old `left:-10000px`: an
1454
+ off-screen box still counts as layout, and in an RTL context widens the page. */
1455
+ .sr-only {
1456
+ position: absolute; width: 1px; height: 1px;
1457
+ margin: -1px; padding: 0; border: 0;
1458
+ overflow: hidden; clip-path: inset(50%); white-space: nowrap;
1459
+ }
1436
1460
  .tier-badge { font-size: var(--fs-caption); padding: 1px 8px; border-radius: 999px; margin-left: 6px;
1437
1461
  font-family: var(--mono); white-space: nowrap; }
1438
1462
  .tier-badge:empty { display: none; }
@@ -2115,11 +2139,30 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
2115
2139
  background: var(--bg-card); color: var(--text); min-width: 26ch;
2116
2140
  }
2117
2141
  .budgets-table { width: 100%; border-collapse: collapse; font-size: var(--fs-body); }
2118
- .
2142
+ /* Restored from ad779c7a~1. That commit — about subagent batching — replaced
2143
+ this selector with a bare `.` and left `id var(--border);`, the tail of a
2144
+ duplicated border-bottom, before the closing brace.
2145
+
2146
+ `.` is not a selector, and CSS does not stop there: the prelude runs on to the
2147
+ next `{`, which is `td`'s, and takes `td`'s block with it. Confirmed against
2148
+ the released 3.26.0 with the browser's own parser — of this region it keeps
2149
+ `.budgets-table`, `.bt-slug`, `.bt-desc`, `.bt-runs`, `.bt-cap`, `.bt-spend`,
2150
+ `.bt-usd`, `th[title]` and `.budgets-idle summary`, and exactly `th` and `td`
2151
+ are gone. So the table has had no header typography AND no cell padding or
2152
+ borders since that commit. `th[title] { cursor: help }` survived, one rule
2153
+ further down, which is part of why nobody looked: the headers still did
2154
+ something on hover.
2155
+
2156
+ `font-weight: 500` is kept deliberately and is NOT a line that crept in. It
2157
+ was ad779c7a's own edit — one of seven in that commit, and its sibling
2158
+ `.cost-table th` took the identical change on the line above and parses fine.
2159
+ The weight normalisation was the point of the commit; only this rule's
2160
+ selector was destroyed while applying it. Dropping it would leave `th` at the
2161
+ UA's 700, the one weight this design says it does not have. */
2162
+ .budgets-table th {
2119
2163
  text-align: left; font-family: var(--mono); font-weight: 500; font-size: var(--fs-eyebrow);
2120
2164
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--text2);
2121
2165
  padding: 0 12px 8px 0; border-bottom: 1px solid var(--border);
2122
- id var(--border);
2123
2166
  }
2124
2167
  .budgets-table td { padding: 10px 12px 10px 0; border-bottom: 1px solid var(--border); vertical-align: middle; }
2125
2168
  /* A row has to answer three questions: what is this agent, what does it cost,
@@ -4554,7 +4597,7 @@ async function loadJudgeStatus() {
4554
4597
  <span class="js-where">${esc(s.fingerprint || '')} · from ${esc(String(s.from || '').replace(/^.*\/(?=\.great_cto)/, '~/'))}</span>
4555
4598
  <span class="js-form">
4556
4599
  <input id="judge-key" type="password" autocomplete="off" spellcheck="false" placeholder="replace key (sk-or-…)">
4557
- <button class="btn" onclick="saveJudgeKey()">Replace</button>
4600
+ <button class="btn-black" onclick="saveJudgeKey()">Replace</button>
4558
4601
  </span>`
4559
4602
  : `<span class="js-dot"></span>
4560
4603
  <span><strong>No judge connected.</strong> Every stage will be scored
@@ -4563,7 +4606,7 @@ async function loadJudgeStatus() {
4563
4606
  ? `<span class="js-where">${esc(s.problems.join('; '))}</span>` : ''}
4564
4607
  <span class="js-form">
4565
4608
  <input id="judge-key" type="password" autocomplete="off" spellcheck="false" placeholder="OpenRouter key (sk-or-…)">
4566
- <button class="btn" onclick="saveJudgeKey()">Connect</button>
4609
+ <button class="btn-black" onclick="saveJudgeKey()">Connect</button>
4567
4610
  </span>`;
4568
4611
  }
4569
4612
 
@@ -4685,7 +4728,7 @@ async function loadBudgetsPage() {
4685
4728
  <div class="bf-why">${esc(String(err && err.message || err))}</div>
4686
4729
  <div class="bf-note">Caps already set are unaffected — this failed to
4687
4730
  <em>read</em> them, and the pipeline reads them itself, not from here.</div>
4688
- <button class="btn" onclick="loadBudgetsPage()">Try again</button>
4731
+ <button class="btn-black" onclick="loadBudgetsPage()">Try again</button>
4689
4732
  </div>`;
4690
4733
  return;
4691
4734
  }
@@ -5070,7 +5113,7 @@ function showVerdictDetail(idx) {
5070
5113
  modal.innerHTML = `
5071
5114
  <div class="rdm-backdrop" onclick="document.getElementById('resume-detail-modal').remove()"></div>
5072
5115
  <div class="rdm-card" role="dialog" aria-modal="true" aria-labelledby="rdm-title-verdict">
5073
- <h2 id="rdm-title-verdict" class="sr-only" style="position:absolute;left:-10000px;width:1px;height:1px;overflow:hidden;">Verdict detail: ${esc(v.agent || 'agent')} ${esc(v.verdict || '')}</h2>
5116
+ <h2 id="rdm-title-verdict" class="sr-only">Verdict detail: ${esc(v.agent || 'agent')} ${esc(v.verdict || '')}</h2>
5074
5117
  <div class="rdm-head">
5075
5118
  <span class="ri-tag ${cls}">${esc(v.agent || 'agent')}</span>
5076
5119
  <span class="rdm-verdict ${cls}">${esc(v.verdict || '')}</span>
@@ -5971,8 +6014,8 @@ async function loadSessions() {
5971
6014
  <span class="muted" style="white-space:nowrap">${ago(s.modified)} · ${fmtMB(s.size_bytes)}</span>
5972
6015
  </div>
5973
6016
  <div style="margin-top:6px">
5974
- <button class="btn-sm" onclick="openSession('${esc(s.id)}')">transcript</button>
5975
- <button class="btn-sm" onclick="openEdits('${esc(s.id)}')">edits</button>
6017
+ <button class="btn-ghost-sm" onclick="openSession('${esc(s.id)}')">transcript</button>
6018
+ <button class="btn-ghost-sm" onclick="openEdits('${esc(s.id)}')">edits</button>
5976
6019
  </div>
5977
6020
  <div id="sess-${esc(s.id)}" style="margin-top:8px"></div>
5978
6021
  </div>`).join('');
@@ -6024,12 +6067,12 @@ async function searchTranscripts() {
6024
6067
  if (!d) { body.innerHTML = '<div class="muted">Search failed.</div>'; return; }
6025
6068
  if (!d.hits?.length) {
6026
6069
  body.innerHTML = `<div class="muted">No match for “${esc(q)}”.${d.note ? ' ' + esc(d.note) : ''}
6027
- <button class="btn-sm" onclick="loadSessions()">back</button></div>`;
6070
+ <button class="btn-ghost-sm" onclick="loadSessions()">back</button></div>`;
6028
6071
  return;
6029
6072
  }
6030
6073
  body.innerHTML =
6031
6074
  `<div class="muted" style="margin-bottom:8px">${d.hits.length} match(es)${d.truncated ? ', capped' : ''}
6032
- <button class="btn-sm" onclick="loadSessions()">back</button></div>` +
6075
+ <button class="btn-ghost-sm" onclick="loadSessions()">back</button></div>` +
6033
6076
  d.hits.map(h => `
6034
6077
  <div class="card" style="margin-bottom:6px">
6035
6078
  <div class="muted" style="font-size:.8em">${esc(h.title || h.session)} · ${esc(h.role)} · ${h.ts ? ago(h.ts) : ''}</div>
@@ -32,13 +32,38 @@
32
32
  * authoring work, done deliberately, a few documents at a time.
33
33
  */
34
34
 
35
- import { readFileSync, readdirSync, statSync } from 'node:fs';
35
+ import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
36
36
  import path from 'node:path';
37
37
 
38
38
  /** Generated summaries and translations are copies, not documents. */
39
39
  const IS_SUMMARY = /\.summary\.md$/;
40
- const IS_TRANSLATION = /^docs\/[a-z]{2}(-[A-Z]{2})?\//;
41
40
 
41
+ /**
42
+ * A two-letter directory is not evidence of a language.
43
+ *
44
+ * This used to be `/^docs\/[a-z]{2}(-[A-Z]{2})?\//`, which reads `docs/qa/`,
45
+ * `docs/ai/`, `docs/ci/`, `docs/ux/` and `docs/db/` as language codes and drops
46
+ * every document under them — silently, from the board's docs tab and from the
47
+ * link graph both, because a dropped document declares nothing that could go
48
+ * missing. `docs/qa/` was already being eaten; the rest were waiting.
49
+ *
50
+ * A whitelist of language codes would rot the first time someone adds one, so
51
+ * the test is STRUCTURAL instead: a translation MIRRORS a document at the docs
52
+ * root. `docs/ru/index.md` is a translation because `docs/index.md` exists;
53
+ * `docs/qa/QA-judge-provenance.md` is not, because `docs/QA-judge-provenance.md`
54
+ * does not. Measured across all ten two-letter directories here — de, es, fr,
55
+ * ja, ko, pt-BR, ru, zh-CN, zh-TW each mirror 1 of 1; qa mirrors 0 of 2 — and it
56
+ * self-corrects when a language is added or removed.
57
+ *
58
+ * Consequence worth stating, because it is a visible surface change hiding
59
+ * inside a classifier fix: `docs/qa/` drafts now APPEAR on the board's docs tab.
60
+ * That is the intended direction — the board is local and should show drafts —
61
+ * but it is not what a reader expects from a regex change.
62
+ */
63
+ const LANG_DIR = /^docs\/[a-z]{2}(?:-[A-Z]{2})?\//;
64
+
65
+ /** The basename a translation would be a translation OF. */
66
+ const baseName = (rel) => rel.slice(rel.lastIndexOf('/') + 1);
42
67
  export function listDocs(root = 'docs') {
43
68
  // The translation rule is written against a path that starts at the docs
44
69
  // directory. `root` may be relative ('docs') or absolute (the board serves
@@ -47,7 +72,7 @@ export function listDocs(root = 'docs') {
47
72
  // matches nothing on an absolute walk and translations count as documents,
48
73
  // which is a wrong number that looks like a right one.
49
74
  const base = path.dirname(root);
50
- const out = [];
75
+ const found = [];
51
76
  const walk = (dir) => {
52
77
  let entries;
53
78
  try { entries = readdirSync(dir); } catch { return; }
@@ -56,11 +81,24 @@ export function listDocs(root = 'docs') {
56
81
  let st;
57
82
  try { st = statSync(full); } catch { continue; }
58
83
  if (st.isDirectory()) walk(full);
59
- else if (e.endsWith('.md') && !IS_SUMMARY.test(e) && !IS_TRANSLATION.test(path.relative(base, full))) out.push(full);
84
+ else if (e.endsWith('.md') && !IS_SUMMARY.test(e)) found.push({ full, rel: path.relative(base, full) });
60
85
  }
61
86
  };
62
87
  walk(root);
63
- return out.sort();
88
+
89
+ // Two passes, because whether a file under a two-letter directory is a
90
+ // TRANSLATION depends on the rest of the tree: it is one when a document of
91
+ // the same name lives outside every language directory. One pass cannot know
92
+ // that, and the single-pass version of this rule only looked at the docs
93
+ // root — which called `docs/ru/ADR-001-a.md` a document because its source
94
+ // sits in `docs/adr/`.
95
+ const sources = new Set(
96
+ found.filter((f) => !LANG_DIR.test(f.rel)).map((f) => baseName(f.rel)),
97
+ );
98
+ return found
99
+ .filter((f) => !(LANG_DIR.test(f.rel) && sources.has(baseName(f.rel))))
100
+ .map((f) => f.full)
101
+ .sort();
64
102
  }
65
103
 
66
104
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "3.26.0",
3
+ "version": "3.26.2",
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",