great-cto 3.1.0 → 3.2.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.
@@ -2,7 +2,7 @@
2
2
  "name": "great_cto",
3
3
  "id": "great_cto",
4
4
  "description": "Engineering process for solo founders and teams up to 50 engineers. Agents do architecture, code review, QA, and security. You make two decisions per feature.",
5
- "version": "3.1.0",
5
+ "version": "3.2.0",
6
6
  "author": {
7
7
  "name": "Great CTO",
8
8
  "url": "https://github.com/avelikiy/great_cto"
@@ -1647,8 +1647,20 @@ button { font-family: inherit; cursor: pointer; }
1647
1647
  padding: 0 12px 8px 0; border-bottom: 1px solid var(--border);
1648
1648
  }
1649
1649
  .budgets-table td { padding: 10px 12px 10px 0; border-bottom: 1px solid var(--border); vertical-align: middle; }
1650
- .budgets-table .bt-agent { font-family: var(--mono); }
1651
- .budgets-table .bt-cap { font-family: var(--mono); font-variant-numeric: tabular-nums; }
1650
+ /* A row has to answer three questions: what is this agent, what does it cost,
1651
+ over what period. The slug alone answered none of them. */
1652
+ .budgets-table .bt-slug { font-family: var(--mono); font-size: 12.5px; }
1653
+ .budgets-table .bt-desc {
1654
+ font-size: 12px; color: var(--text2); margin-top: 2px;
1655
+ max-width: 52ch; text-wrap: pretty;
1656
+ }
1657
+ .budgets-table .bt-runs,
1658
+ .budgets-table .bt-cap { font-family: var(--mono); font-variant-numeric: tabular-nums; text-align: right; }
1659
+ .budgets-table .bt-spend { font-family: var(--mono); font-size: 12px; white-space: nowrap; }
1660
+ .budgets-table .bt-usd { font-variant-numeric: tabular-nums; color: var(--text); }
1661
+ .budgets-table th[title] { cursor: help; }
1662
+ .budgets-idle summary { cursor: pointer; font-size: 13px; color: var(--text2); }
1663
+ .budgets-idle summary:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
1652
1664
  .budgets-table .bt-actions { text-align: right; white-space: nowrap; }
1653
1665
  .budgets-table .bt-actions .ab-btn + .ab-btn { margin-left: 6px; }
1654
1666
  .budgets-empty {
@@ -3544,66 +3556,120 @@ async function loadBudgetsPage() {
3544
3556
 
3545
3557
  const [h] = await Promise.all([
3546
3558
  api(`/api/heartbeat${pqs()}`),
3547
- (typeof loadFleet === 'function' && !(fleetState?.agents || []).length) ? loadFleet() : Promise.resolve(),
3559
+ (typeof refreshAgentsInstalled === 'function' && !(fleetState?.agents || []).length)
3560
+ ? refreshAgentsInstalled() : Promise.resolve(),
3548
3561
  ]);
3549
3562
  const caps = h?.budgets || {};
3550
- const agents = fleetState?.agents || [];
3551
- const judged = new Map(agents.map((a) => [a.slug, a.budget]));
3552
- const capped = Object.entries(caps);
3553
- const uncapped = agents.filter((a) => !(a.slug in caps));
3563
+ const agents = (fleetState?.agents || []).filter((a) => !a.retired);
3564
+
3565
+ // 69 identical chips in one blob answered none of the three questions a person
3566
+ // arrives with: what does this agent do, what does it usually cost, and over
3567
+ // what period. Worse, 62 of the 69 had not run in thirty days — a cap on an
3568
+ // agent that never runs is a cap on nothing, and they were the bulk of the
3569
+ // wall.
3570
+ //
3571
+ // Ordered by what it costs to leave uncapped: agents that ran, most spend
3572
+ // first. The ones that never ran are still reachable, behind a count.
3573
+ const spendOf = (a) => (a.llm_usd_30d_real ?? a.llm_usd_30d_est ?? 0);
3574
+ const ran = agents.filter((a) => (a.runs_30d || 0) > 0)
3575
+ .sort((x, y) => spendOf(y) - spendOf(x) || (y.runs_30d || 0) - (x.runs_30d || 0));
3576
+ const idle = agents.filter((a) => !(a.runs_30d > 0));
3577
+
3578
+ const firstSentence = (t) => {
3579
+ const s1 = String(t || '').split(/(?<=[.!?])\s/)[0] || '';
3580
+ return s1.length > 150 ? s1.slice(0, 147) + '…' : s1;
3581
+ };
3582
+
3583
+ // Spend is stated with its window and its provenance. `est` is a time-based
3584
+ // guess and says so; a real figure only exists where a verdict carried a cost.
3585
+ const spendCell = (a) => {
3586
+ if (a.llm_usd_30d_real != null && a.llm_usd_30d_real > 0) {
3587
+ return `<span class="bt-usd">$${a.llm_usd_30d_real.toFixed(2)}</span> <span class="muted">measured</span>`;
3588
+ }
3589
+ if (a.llm_usd_30d_est != null && a.llm_usd_30d_est > 0) {
3590
+ return `<span class="bt-usd">$${a.llm_usd_30d_est.toFixed(2)}</span> <span class="muted">estimated</span>`;
3591
+ }
3592
+ return '<span class="muted">not measured</span>';
3593
+ };
3554
3594
 
3555
3595
  const stateCell = (slug) => {
3556
- const b = judged.get(slug);
3557
- if (!b || b.state === 'no-limit') return '<span class="muted">state not loaded</span>';
3596
+ const b = (fleetState?.agents || []).find((a) => a.slug === slug)?.budget;
3597
+ if (!b || b.state === 'no-limit') return '';
3558
3598
  const spent = (b.state === 'within' || b.state === 'exceeded')
3559
- ? `$${(b.measuredUsd ?? 0).toFixed(2)} measured`
3560
- : (b.state === 'unmeasured' ? 'nothing measured yet' : '');
3599
+ ? `$${(b.measuredUsd ?? 0).toFixed(2)} measured` : (b.state === 'unmeasured' ? 'nothing measured yet' : '');
3561
3600
  return `${budgetChip(b)} <span class="ab-spent">${esc(spent)}</span>`;
3562
3601
  };
3563
3602
 
3564
- // Three states, as everywhere: capped, uncapped, and unreadable — never a
3565
- // silent "all fine".
3566
- const bad = (h?.budgets_malformed || []);
3603
+ const row = (a, cap) => `
3604
+ <tr>
3605
+ <td class="bt-agent">
3606
+ <div class="bt-slug">${esc(a.slug)}</div>
3607
+ <div class="bt-desc">${esc(firstSentence(a.description))}</div>
3608
+ </td>
3609
+ <td class="bt-runs">${a.runs_30d || 0}</td>
3610
+ <td class="bt-spend">${spendCell(a)}</td>
3611
+ <td class="bt-cap">${cap != null ? `$${esc(String(cap))}` : '<span class="muted">—</span>'}</td>
3612
+ <td class="bt-state">${cap != null ? stateCell(a.slug) : ''}</td>
3613
+ <td class="bt-actions">${cap != null
3614
+ ? `<button type="button" class="ab-btn" onclick="editAgentBudget('${esc(a.slug)}', ${Number(cap)})">Change</button>`
3615
+ + `<button type="button" class="ab-btn ab-rm" onclick="clearAgentBudget('${esc(a.slug)}', ${Number(cap)})">Remove</button>`
3616
+ : `<button type="button" class="ab-btn" onclick="addBudgetFor('${esc(a.slug)}')">Set a cap</button>`}</td>
3617
+ </tr>`;
3618
+
3619
+ const table = (rows) => `
3620
+ <table class="budgets-table">
3621
+ <thead><tr>
3622
+ <th scope="col">Agent</th>
3623
+ <th scope="col" title="Dispatches in the last 30 days">Runs 30d</th>
3624
+ <th scope="col" title="LLM spend over the same 30 days">Spend 30d</th>
3625
+ <th scope="col">Cap</th><th scope="col">State</th><th scope="col"></th>
3626
+ </tr></thead>
3627
+ <tbody>${rows}</tbody>
3628
+ </table>`;
3629
+
3630
+ // A cap declared for an agent the fleet does not list still has to appear —
3631
+ // otherwise a limit that is holding a stage would be invisible here.
3632
+ const known = new Set(agents.map((a) => a.slug));
3633
+ const orphanCaps = Object.entries(caps).filter(([slug]) => !known.has(slug));
3634
+
3635
+ const capped = ran.filter((a) => a.slug in caps);
3636
+ const uncappedRan = ran.filter((a) => !(a.slug in caps));
3637
+
3567
3638
  const notes = [
3568
3639
  h?.budgets_deprecated_key
3569
3640
  ? `<div class="ab-note">Read from the deprecated <code>${esc(h.budgets_deprecated_key)}:</code> key — rename it to <code>agent-budgets:</code>.</div>` : '',
3570
- bad.length
3571
- ? `<div class="ab-note ab-bad">${bad.length} line(s) in PROJECT.md were not read: `
3572
- + bad.map((m) => `<code>${esc(m.line)}</code> — ${esc(m.why)}`).join('; ') + '</div>' : '',
3641
+ (h?.budgets_malformed || []).length
3642
+ ? `<div class="ab-note ab-bad">${h.budgets_malformed.length} line(s) in PROJECT.md were not read: `
3643
+ + h.budgets_malformed.map((m) => `<code>${esc(m.line)}</code> — ${esc(m.why)}`).join('; ') + '</div>' : '',
3573
3644
  ].join('');
3574
3645
 
3575
- const capsBlock = capped.length ? `
3576
- <table class="budgets-table">
3577
- <thead><tr><th scope="col">Agent</th><th scope="col">Cap</th><th scope="col">State</th><th scope="col"></th></tr></thead>
3578
- <tbody>${capped.map(([slug, cap]) => `
3579
- <tr>
3580
- <td class="bt-agent">${esc(slug)}</td>
3581
- <td class="bt-cap">$${esc(String(cap))}</td>
3582
- <td>${stateCell(slug)}</td>
3583
- <td class="bt-actions">
3584
- <button type="button" class="ab-btn" onclick="editAgentBudget('${esc(slug)}', ${Number(cap)})">Change</button>
3585
- <button type="button" class="ab-btn ab-rm" onclick="clearAgentBudget('${esc(slug)}', ${Number(cap)})">Remove</button>
3586
- </td>
3587
- </tr>`).join('')}</tbody>
3588
- </table>`
3589
- : `<div class="budgets-empty">
3590
- No agent has a spending cap in this project. Nothing is being held, and nothing will be.
3591
- </div>`;
3592
-
3593
- const uncappedBlock = uncapped.length ? `
3594
- <div class="mp-section-label" style="margin-top:24px">No cap (${uncapped.length})</div>
3595
- <div class="budgets-uncapped">${uncapped.slice(0, 60).map((a) => `
3596
- <button type="button" class="ab-btn" onclick="addBudgetFor('${esc(a.slug)}')"
3597
- title="Set a cap for ${esc(a.slug)}">${esc(a.slug)} +</button>`).join('')}
3598
- ${uncapped.length > 60 ? `<span class="muted">and ${uncapped.length - 60} more</span>` : ''}
3599
- </div>` : '';
3646
+ const capsBlock = (capped.length || orphanCaps.length) ? `
3647
+ <div class="mp-section-label">Capped (${capped.length + orphanCaps.length})</div>
3648
+ ${table(capped.map((a) => row(a, caps[a.slug])).join('')
3649
+ + orphanCaps.map(([slug, cap]) => row({ slug, description: 'not among the installed agents', runs_30d: 0 }, cap)).join(''))}`
3650
+ : `<div class="budgets-empty">No agent has a spending cap in this project. Nothing is being held, and nothing will be.</div>`;
3651
+
3652
+ const activeBlock = uncappedRan.length ? `
3653
+ <div class="mp-section-label" style="margin-top:28px">Ran in the last 30 days, no cap (${uncappedRan.length})</div>
3654
+ <p class="muted" style="margin:4px 0 10px">These are the ones a cap would actually apply to.</p>
3655
+ ${table(uncappedRan.map((a) => row(a, null)).join(''))}` : '';
3656
+
3657
+ // Capping an agent that has not run is capping nothing. Reachable, not shown.
3658
+ const idleBlock = idle.length ? `
3659
+ <details class="budgets-idle" style="margin-top:28px">
3660
+ <summary>${idle.length} installed agents have not run in 30 days</summary>
3661
+ <div class="budgets-uncapped">${idle.slice(0, 80).map((a) => `
3662
+ <button type="button" class="ab-btn" onclick="addBudgetFor('${esc(a.slug)}')"
3663
+ title="${esc(firstSentence(a.description))}">${esc(a.slug)} +</button>`).join('')}
3664
+ ${idle.length > 80 ? `<span class="muted">and ${idle.length - 80} more</span>` : ''}
3665
+ </div>
3666
+ </details>` : '';
3600
3667
 
3601
- body.innerHTML = `${notes}${capsBlock}
3602
- <button type="button" class="ab-btn ab-add" onclick="addAgentBudget()">+ Add a budget</button>
3603
- ${uncappedBlock}`;
3668
+ body.innerHTML = `${notes}${capsBlock}${activeBlock}${idleBlock}
3669
+ <button type="button" class="ab-btn ab-add" onclick="addAgentBudget()">+ Add a budget for any agent</button>`;
3604
3670
 
3605
3671
  const badge = document.getElementById('nav-budget-count');
3606
- if (badge) badge.textContent = capped.length ? String(capped.length) : '';
3672
+ if (badge) badge.textContent = Object.keys(caps).length ? String(Object.keys(caps).length) : '';
3607
3673
  }
3608
3674
 
3609
3675
  /** Set a cap for a named agent, skipping the "which agent?" question. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "3.1.0",
3
+ "version": "3.2.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",