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.
|
|
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
|
-
|
|
1651
|
-
|
|
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
|
|
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
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
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 =
|
|
3557
|
-
if (!b || b.state === 'no-limit') return '
|
|
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
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
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
|
-
|
|
3571
|
-
? `<div class="ab-note ab-bad">${
|
|
3572
|
-
+
|
|
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
|
-
<
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
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 =
|
|
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