great-cto 3.68.0 → 3.69.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.68.0",
4
+ "version": "3.69.0",
5
5
  "author": {
6
6
  "name": "Alexander Velikiy",
7
7
  "url": "https://hashnode.com/@Greatcto"
@@ -2,6 +2,7 @@
2
2
  name: coordinator
3
3
  description: "Multi-agent coordinator. Use when a CTO request spans 3+ independent work streams, requires parallel research before implementation, or the task graph is complex enough that sequencing matters. Orchestrates agents across the full DECOMPOSE→CLASSIFY→DISPATCH→MONITOR→SYNTHESIZE→VERIFY lifecycle."
4
4
  model: sonnet
5
+ color: purple
5
6
  authority: autonomous
6
7
  tools: Read, Write, Edit, Bash, Glob, Grep, Agent
7
8
  allowed-tools: Read, Write, Edit, Bash, Glob, Grep, Agent
@@ -10,6 +10,7 @@ import { readVerdicts } from './verdicts.mjs';
10
10
  // dispatcher enforces the same verdict — one definition, so the board cannot
11
11
  // show `within` while the dispatcher holds the stage.
12
12
  import { parseAgentBudgets, judgeAgentBudget } from '../../../scripts/lib/agent-budget.mjs';
13
+ import { classifyAgentGroup } from '../../../scripts/lib/agent-groups.mjs';
13
14
 
14
15
  // ── Agent fleet view (DESIGN-agents-fleet-view §3) ─────────────────────────
15
16
  //
@@ -33,20 +34,7 @@ const AGENTS_DIR = process.env.GREAT_CTO_AGENTS_DIR
33
34
  || path.join(os.homedir(), '.claude', 'agents');
34
35
 
35
36
  function deriveDomain(slug) {
36
- const s = slug.toLowerCase();
37
- if (/architect|adr|design|prompt/.test(s)) return 'arch';
38
- if (/security|sec-|threat|pci|gdpr|hipaa/.test(s)) return 'security';
39
- // `review` used to be in this alternation, and it matched every `*-reviewer`
40
- // slug — so 37 of the 40 agents in `qa` were domain reviewers, the `domain`
41
- // bucket below was permanently empty, and the panel grouped 58% of the fleet
42
- // under one heading. The rule that was supposed to separate them sat two lines
43
- // later and could never be reached.
44
- if (/\bqa\b|test|eval|^code-reviewer$/.test(s)) return 'qa';
45
- if (/devops|deploy|infra|l3|support|oncall/.test(s)) return 'ops';
46
- if (/reviewer$/.test(s)) return 'domain';
47
- if (/pm|plan|product/.test(s)) return 'pm';
48
- if (/learn|memory|continuous/.test(s)) return 'memory';
49
- return 'other';
37
+ return classifyAgentGroup(slug);
50
38
  }
51
39
 
52
40
  // Founder Q#5 — picked: baked-in regex set (start small, expand later).
@@ -0,0 +1,41 @@
1
+ /* Canonical role-group glyphs and classification. Pure classic script: loaded
2
+ * by the browser and imported by the Node bridge; no DOM, fetch or dependencies.
3
+ * The site vendors these exact bytes. Glyphs describe roles, never run status. */
4
+ (function (root) {
5
+ 'use strict';
6
+ const groups = Object.freeze([
7
+ { key: 'pm', label: 'Plan', svg: '<rect x="6" y="4" width="12" height="17" rx="2"/><path d="M9 4h6v3H9zM9 12h6M9 16h4"/>' },
8
+ { key: 'arch', label: 'Architecture', svg: '<path d="M12 3l9 5-9 5-9-5z"/><path d="M3 13l9 5 9-5"/>' },
9
+ { key: 'build', label: 'Build', svg: '<path d="M8 7l-5 5 5 5M16 7l5 5-5 5M14 4l-4 16"/>' },
10
+ { key: 'qa', label: 'Test & review', svg: '<circle cx="12" cy="12" r="9"/><path d="M8 12l3 3 5-6"/>' },
11
+ { key: 'security', label: 'Security', svg: '<path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6z"/>' },
12
+ { key: 'domain', label: 'Domain reviewers', svg: '<path d="M12 4v16M8 20h8M5 8h14M5 8l-2 6h4zM19 8l-2 6h4z"/>' },
13
+ { key: 'ops', label: 'Ops', svg: '<rect x="4" y="4" width="16" height="6" rx="1"/><rect x="4" y="14" width="16" height="6" rx="1"/><path d="M8 7h.01M8 17h.01"/>' },
14
+ { key: 'memory', label: 'Memory', svg: '<path d="M6 3h12v18l-6-4-6 4z"/>' },
15
+ { key: 'other', label: 'Other', svg: '<circle cx="12" cy="12" r="3"/>' },
16
+ ].map(Object.freeze));
17
+ // Order matters: test/eval before builders; domain reviewers before generic
18
+ // engineers. These are display hints, not pipeline routing or permissions.
19
+ const rules = [
20
+ [/architect|adr|design|prompt|decision-scorer|project-auditor/, 'arch'],
21
+ [/security|sec-|threat|pci|gdpr|hipaa|smart-contract/, 'security'],
22
+ [/\bqa\b|test|eval|performance|^code-reviewer$/, 'qa'],
23
+ [/devops|deploy|infra|l3|support|oncall/, 'ops'],
24
+ [/reviewer$/, 'domain'],
25
+ [/pm|plan|product|coordinator/, 'pm'],
26
+ [/learn|memory|continuous|knowledge/, 'memory'],
27
+ [/^senior-dev$|-engineer$|-builder$|scaffolder|ci-resolver/, 'build'],
28
+ ];
29
+ function classify(slug) {
30
+ const value = typeof slug === 'string' ? slug.toLowerCase() : '';
31
+ return rules.find(([pattern]) => pattern.test(value))?.[1] || 'other';
32
+ }
33
+ const group = key => groups.find(g => g.key === key) || groups[groups.length - 1];
34
+ const label = key => group(key).label;
35
+ function icon(key) {
36
+ const g = group(key);
37
+ // All markup and attributes below come from authored constants, never key.
38
+ return `<svg class="grp-icon" data-agent-group="${g.key}" aria-hidden="true" focusable="false" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">${g.svg}</svg>`;
39
+ }
40
+ root.GctoAgentGroups = Object.freeze({ groups, classify, label, icon });
41
+ })(globalThis);
@@ -1524,6 +1524,9 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
1524
1524
  .bar > i { display: block; height: 100%; background: var(--accent); }
1525
1525
  /* Fleet (great_cto-ki1x.8): saved views in the sidebar, domain groups, AgentRow cells */
1526
1526
  .fleet-group-head { display: flex; align-items: center; gap: 8px; padding: 8px 12px 4px; font-size: var(--fs-eyebrow); letter-spacing: .08em; text-transform: uppercase; color: var(--text2); font-weight: 500; }
1527
+ .fleet-group-head .grp-icon, .agent-row .grp-icon, #agent-drawer-title .grp-icon { width: 14px; height: 14px; flex-shrink: 0; color: var(--text2); vertical-align: -2px; }
1528
+ .agent-row .grp-icon { margin-right: 6px; color: var(--text3); }
1529
+ #agent-drawer-title .grp-icon { margin-right: 6px; }
1527
1530
  .fleet-group-collapsed { display: flex; flex-wrap: wrap; gap: 20px; padding: 10px 12px; font-size: var(--fs-eyebrow); letter-spacing: .08em; text-transform: uppercase; color: var(--text3); }
1528
1531
  .ar-cells { display: grid; grid-template-columns: minmax(200px, 1.4fr) 130px 48px 56px 80px auto; gap: 12px; align-items: center; margin-top: 4px; }
1529
1532
  .ar-posture { display: flex; gap: 6px; flex-wrap: wrap; }
@@ -3395,6 +3398,7 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
3395
3398
  </style>
3396
3399
  </head>
3397
3400
  <body>
3401
+ <script src="/assets/agent-groups.js"></script>
3398
3402
  <script src="/assets/work-view.js"></script>
3399
3403
  <div class="board">
3400
3404
  <!-- Subtle emerald glow background -->
@@ -9768,7 +9772,7 @@ function renderFleetList() {
9768
9772
  // Groups come from deriveDomain() on the server — one taxonomy, not two. A
9769
9773
  // group with rows in this view is expanded; the rest are one line each with
9770
9774
  // their count, so 70 agents fit a screen without a scroll or a virtualizer.
9771
- const DOMAINS = ['arch', 'security', 'qa', 'ops', 'domain', 'pm', 'memory', 'other'];
9775
+ const DOMAINS = AGENT_GROUPS.map(g => g.key);
9772
9776
  const totals = {}; for (const a of fleetState.agents) totals[a.domain] = (totals[a.domain] || 0) + 1;
9773
9777
  const byDomain = {}; for (const a of filtered) (byDomain[a.domain] = byDomain[a.domain] || []).push(a);
9774
9778
  const order = [...DOMAINS, ...Object.keys(byDomain).filter((d) => !DOMAINS.includes(d))];
@@ -9781,14 +9785,21 @@ function renderFleetList() {
9781
9785
  : `<li class="fleet-group-collapsed"><span class="abs">${esc(uu.state === 'computing' ? 'counting dispatches in the session logs…' : (uu.why || 'dispatches not counted'))}</span></li>`;
9782
9786
  const head = usageLine + `<li class="fleet-head" aria-hidden="true"><span></span><span>Agent</span><span>Posture</span><span>Last verdict</span><span class="tab">Runs</span><span class="tab">Pass</span><span class="tab">30d $</span><span></span></li>`;
9783
9787
  const rows = head + expanded.map((d) => `
9784
- <li class="fleet-group-head"><span>${esc(d)} · ${byDomain[d].length} of ${totals[d] || 0}</span></li>
9788
+ <li class="fleet-group-head">${groupIcon(d)}<span>${esc(groupLabel(d))} · ${byDomain[d].length} of ${totals[d] || 0}</span></li>
9785
9789
  ${byDomain[d].map(agentRowHtml).join('')}`).join('');
9786
9790
  const tail = collapsed.length
9787
- ? `<li class="fleet-group-collapsed">${collapsed.map((d) => `<span title="no ${esc(d)} agent is in this view">${esc(d)} · 0 of ${totals[d]}</span>`).join('')}</li>`
9791
+ ? `<li class="fleet-group-collapsed">${collapsed.map((d) => `<span title="no ${esc(groupLabel(d))} agent is in this view">${esc(groupLabel(d))} · 0 of ${totals[d]}</span>`).join('')}</li>`
9788
9792
  : '';
9789
9793
  el.innerHTML = rows + tail;
9790
9794
  }
9791
9795
 
9796
+ // One glyph per role group, so a reviewer and a builder differ at a glance —
9797
+ // 72 agents share 10 frontmatter colors, and a color says nothing about the role.
9798
+ // Keys are deriveDomain() on the server; order is the pipeline's.
9799
+ const AGENT_GROUPS = window.GctoAgentGroups?.groups || [];
9800
+ function groupLabel(key) { return window.GctoAgentGroups?.label(key) || key; }
9801
+ function groupIcon(key) { return window.GctoAgentGroups?.icon(key) || ''; }
9802
+
9792
9803
  // AgentRow (DESIGN §6.4 / §8.5): slug · posture · last verdict · runs · pass · 30d $.
9793
9804
  // Numbers that can be summed are tabular and right-aligned; identifiers are not.
9794
9805
  // Every absence carries its reason on hover.
@@ -9850,7 +9861,7 @@ function agentRowHtml(a) {
9850
9861
  onclick="openAgentDrawer('${esc(a.slug)}')"
9851
9862
  onkeydown="if(event.key==='Enter'){event.preventDefault();openAgentDrawer('${esc(a.slug)}')}">
9852
9863
  <span class="ar-status" aria-hidden="true"></span>
9853
- <span class="ar-name">${esc(a.slug)} <span class="mono cap muted" style="font-weight:400">${model}</span>${a.retired ? ' <span class="cap" style="color:var(--agent-retired)">retired</span>' : ''}</span>
9864
+ <span class="ar-name">${groupIcon(a.domain)}${esc(a.slug)} <span class="mono cap muted" style="font-weight:400">${model}</span>${a.retired ? ' <span class="cap" style="color:var(--agent-retired)">retired</span>' : ''}</span>
9854
9865
  <span class="ar-posture">${requiredChip(a)}${postureChips(a)}</span>
9855
9866
  <span class="mono cap ar-last">${last}</span>
9856
9867
  <span class="mono cap tab" title="runs in the last 30 days">${runs}</span>
@@ -9979,7 +9990,7 @@ function renderAgentProfile(p, extra = {}) {
9979
9990
  const title = document.getElementById('agent-drawer-title');
9980
9991
  const pill = document.getElementById('agent-drawer-pill');
9981
9992
  if (!body) return;
9982
- title.textContent = p.slug;
9993
+ title.innerHTML = groupIcon(window.GctoAgentGroups?.classify(p.slug)) + esc(p.slug);
9983
9994
  const row = (fleetState.agents || []).find((a) => a.slug === p.slug) || {};
9984
9995
  const tier = extra.tier;
9985
9996
  const tierChip = tier
@@ -0,0 +1,8 @@
1
+ // One implementation for Node readers and the classic-script board. The
2
+ // side-effect import also works in the site's committed CommonJS snapshot.
3
+ import '../../packages/board/public/assets/agent-groups.js';
4
+ const api = globalThis.GctoAgentGroups;
5
+ export const agentGroups = api.groups;
6
+ export const classifyAgentGroup = api.classify;
7
+ export const agentGroupLabel = api.label;
8
+ export const agentGroupIcon = api.icon;
@@ -29,7 +29,8 @@ import { execFileSync } from 'node:child_process';
29
29
  import { createHash } from 'node:crypto';
30
30
  import { readFileSync } from 'node:fs';
31
31
  import * as fsModule from 'node:fs';
32
- import { join } from 'node:path';
32
+ import { dirname, join, resolve } from 'node:path';
33
+ import { fileURLToPath } from 'node:url';
33
34
 
34
35
  const sha = (s) => createHash('sha256').update(String(s)).digest('hex');
35
36
 
@@ -351,12 +352,23 @@ export function describeDrift(cmp, { max = 10 } = {}) {
351
352
  export const APPROVING_AGENTS = Object.freeze(['code-reviewer', 'security-officer', 'qa-engineer']);
352
353
  const APPROVING_VERDICTS = new Set(['APPROVED', 'PASS', 'PASSED']);
353
354
 
354
- export function latestApproval(cwd = process.cwd(), { agents = APPROVING_AGENTS, read = readFileSync } = {}) {
355
+ /** Same state location as log-verdict.sh; the candidate bytes stay in cwd. */
356
+ export function verdictStateDir(cwd = process.cwd(), env = process.env) {
357
+ if (env.GREAT_CTO_DIR) return resolve(cwd, env.GREAT_CTO_DIR);
358
+ const common = git(['rev-parse', '--git-common-dir'], cwd)?.trim();
359
+ const gitdir = git(['rev-parse', '--git-dir'], cwd)?.trim();
360
+ if (common && gitdir && resolve(cwd, common) !== resolve(cwd, gitdir)) {
361
+ return join(dirname(resolve(cwd, common)), '.great_cto');
362
+ }
363
+ return join(cwd, '.great_cto');
364
+ }
365
+
366
+ export function latestApproval(cwd = process.cwd(), { agents = APPROVING_AGENTS, read = readFileSync, stateDir = verdictStateDir(cwd) } = {}) {
355
367
  let best = null;
356
368
  for (const agent of agents) {
357
369
  let text;
358
370
  try {
359
- text = read(join(cwd, '.great_cto', 'verdicts', `${agent}.log`), 'utf8');
371
+ text = read(join(stateDir, 'verdicts', `${agent}.log`), 'utf8');
360
372
  } catch { continue; }
361
373
  for (const line of String(text).split('\n')) {
362
374
  if (!line.trim()) continue;
@@ -454,8 +466,26 @@ function requireFs() {
454
466
  // `--check <file>` compares a recorded receipt held in a file.
455
467
  // `--verify` compares the newest approving verdict's receipt against now.
456
468
 
457
- if (import.meta.url === `file://${process.argv[1]}`) {
469
+ function currentForReceipt(cwd, receipt) {
470
+ // Persisted evidence is data, not Git options or a new revision expression.
471
+ // Missing bases retain legacy behavior; invalid declared bases stay unreadable.
472
+ let base = receipt?.base;
473
+ // The emitter uses HEAD when there is no conventional default branch. Bind
474
+ // that legacy symbolic base to the recorded commit, not today's moving HEAD.
475
+ if (base === 'HEAD') {
476
+ if (!receipt?.head) return null;
477
+ base = receipt.head;
478
+ }
479
+ if (base != null && (typeof base !== 'string' || !/^[0-9a-f]{7,64}$/i.test(base))) return null;
480
+ return treeReceipt(cwd, { base: base || null });
481
+ }
482
+
483
+ if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
458
484
  const argv = process.argv.slice(2);
485
+ if (argv.includes('--state-dir')) {
486
+ console.log(verdictStateDir());
487
+ process.exit(0);
488
+ }
459
489
  if (argv.includes('--emit')) {
460
490
  // `--verdicts <dir>`: measure the change from where the previous stage stood.
461
491
  const vi = argv.indexOf('--verdicts');
@@ -467,7 +497,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
467
497
  if (argv.includes('--verify')) {
468
498
  const cwd = process.cwd();
469
499
  const approval = latestApproval(cwd);
470
- const current = treeReceipt(cwd);
500
+ const current = currentForReceipt(cwd, approval?.receipt);
471
501
  const cmp = compareReceipts(approval?.receipt ?? null, current);
472
502
  if (cmp.state === 'no-receipt') {
473
503
  // Not silence. A push with no approval to compare against and a push whose
@@ -479,6 +509,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
479
509
  console.log(`receipt: against ${who}`);
480
510
  console.log(describeDrift(cmp).split('\n').map((l) => ` ${l}`).join('\n'));
481
511
 
512
+ if (cmp.state === 'unreadable') process.exit(2);
482
513
  if (cmp.state !== 'differs') process.exit(0);
483
514
 
484
515
  // Drift, but a human may already have looked at exactly this state.
@@ -507,8 +538,12 @@ if (import.meta.url === `file://${process.argv[1]}`) {
507
538
  if (argv.includes('--accept')) {
508
539
  const cwd = process.cwd();
509
540
  const approval = latestApproval(cwd);
510
- const current = treeReceipt(cwd);
541
+ const current = currentForReceipt(cwd, approval?.receipt);
511
542
  const cmp = compareReceipts(approval?.receipt ?? null, current);
543
+ if (cmp.state === 'unreadable') {
544
+ console.error(`receipt: cannot accept — ${cmp.why}`);
545
+ process.exit(2);
546
+ }
512
547
  if (cmp.state !== 'differs') {
513
548
  console.log(`receipt: nothing to accept — ${cmp.why}`);
514
549
  process.exit(0);
@@ -537,9 +572,9 @@ if (import.meta.url === `file://${process.argv[1]}`) {
537
572
  const { readFileSync } = await import('node:fs');
538
573
  let recorded = null;
539
574
  try { recorded = JSON.parse(readFileSync(argv[i + 1], 'utf8')); } catch { /* absent */ }
540
- const cmp = compareReceipts(recorded, treeReceipt(process.cwd()));
575
+ const cmp = compareReceipts(recorded, currentForReceipt(process.cwd(), recorded));
541
576
  console.log(describeDrift(cmp));
542
- process.exit(cmp.state === 'differs' ? 1 : 0);
577
+ process.exit(cmp.state === 'unreadable' ? 2 : cmp.state === 'differs' ? 1 : 0);
543
578
  }
544
579
  console.log(JSON.stringify(treeReceipt(process.cwd()), null, 2));
545
580
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "3.68.0",
3
+ "version": "3.69.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",