great-cto 3.59.0 → 3.60.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.
- package/board/.claude-plugin/plugin.json +1 -1
- package/board/packages/board/lib/projects.mjs +2 -1
- package/board/packages/board/lib/routes.mjs +81 -13
- package/board/packages/board/lib/skills-inventory.mjs +228 -0
- package/board/packages/board/lib/usage-report-worker.mjs +2 -2
- package/board/packages/board/lib/usage-report.mjs +18 -10
- package/board/packages/board/lib/view-counter.mjs +1 -1
- package/board/packages/board/public/index.html +139 -34
- package/board/packages/board/public/share.html +4 -4
- package/board/scripts/lib/gate-plan.mjs +3 -2
- package/board/scripts/lib/gate-policy.mjs +97 -0
- package/board/scripts/lib/outcomes.mjs +8 -3
- package/board/scripts/lib/session-usage.mjs +72 -4
- package/board/scripts/skill-lint.mjs +182 -0
- package/package.json +1 -1
- package/postinstall.mjs +15 -9
|
@@ -2426,6 +2426,13 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
|
|
|
2426
2426
|
.usage-table th { text-align: left; font-weight: 500; color: var(--text3); font-size: var(--fs-caption); padding: 4px 6px; border-bottom: 1px solid var(--border); white-space: nowrap; }
|
|
2427
2427
|
.usage-table td { padding: 6px; border-bottom: 1px solid var(--border); color: var(--text); vertical-align: top; }
|
|
2428
2428
|
.usage-table td.num, .usage-table th.num { text-align: right; font-family: var(--mono); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
2429
|
+
.skills-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-bottom: 16px; }
|
|
2430
|
+
.skills-controls label { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; font-size: var(--fs-small); color: var(--text2); }
|
|
2431
|
+
.skills-controls input, .skills-controls select { min-height: 32px; max-width: 100%; padding: 5px 8px; border: 1px solid var(--border); color: var(--text); background: var(--bg-muted); font: inherit; }
|
|
2432
|
+
.skills-controls :is(input, select):focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
|
|
2433
|
+
#skills-table td { max-width: 320px; overflow-wrap: anywhere; }
|
|
2434
|
+
#skills-table code { word-break: break-all; font-family: var(--mono); font-size: var(--fs-caption); }
|
|
2435
|
+
@media (max-width: 767px) { .skills-controls :is(input, select) { min-height: 44px; } }
|
|
2429
2436
|
.usage-title { max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2430
2437
|
.usage-tag { display: inline-block; font-family: var(--mono); font-size: var(--fs-eyebrow); letter-spacing: 0.04em; padding: 1px 5px; margin-left: 6px; border: 1px solid var(--border-strong); color: var(--text2); }
|
|
2431
2438
|
.usage-share { display: inline-block; width: 48px; height: 6px; background: var(--bg-muted); margin-right: 6px; vertical-align: 1px; }
|
|
@@ -3461,6 +3468,10 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
|
|
|
3461
3468
|
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 20V10 M10 20V4 M16 20v-7 M22 20H2"/></svg>
|
|
3462
3469
|
<span class="nav-label">Usage</span>
|
|
3463
3470
|
</div>
|
|
3471
|
+
<div class="nav-item" data-tab="skills" role="tab" tabindex="0" aria-label="Skills" title="Skills" onkeydown="navTabKeydown(event, this)" onclick="switchTab('skills', this)">
|
|
3472
|
+
<svg class="icon" aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 3h6a2 2 0 0 1 2 2v16a3 3 0 0 0-3-3H4V3zM20 3h-6a2 2 0 0 0-2 2v16a3 3 0 0 1 3-3h5V3z"/></svg>
|
|
3473
|
+
<span class="nav-label">Skills</span>
|
|
3474
|
+
</div>
|
|
3464
3475
|
</nav>
|
|
3465
3476
|
</details>
|
|
3466
3477
|
<div class="nav-divider"></div>
|
|
@@ -3711,14 +3722,32 @@ h1, h2, h3, h4 { font-weight: 600; font-family: var(--sans); }
|
|
|
3711
3722
|
<div class="panel" id="panel-usage" role="tabpanel">
|
|
3712
3723
|
<div class="metrics-page">
|
|
3713
3724
|
<div class="usage-head">
|
|
3714
|
-
<h1 class="page-title">Usage <span class="muted" style="font-weight:400;font-size:var(--fs-small)">what Claude Code and Codex consumed
|
|
3725
|
+
<h1 class="page-title">Usage <span class="muted" style="font-weight:400;font-size:var(--fs-small)">what Claude Code and Codex consumed in the selected project</span></h1>
|
|
3715
3726
|
<div class="usage-chips" role="group" aria-label="Period">
|
|
3716
3727
|
<button type="button" class="usage-chip" data-days="7" onclick="loadUsage(7)">7D</button>
|
|
3717
3728
|
<button type="button" class="usage-chip active" data-days="30" onclick="loadUsage(30)">30D</button>
|
|
3718
3729
|
<button type="button" class="usage-chip" data-days="90" onclick="loadUsage(90)">90D</button>
|
|
3719
3730
|
</div>
|
|
3720
3731
|
</div>
|
|
3721
|
-
<div id="usage-body" class="usage-note" role="status">Reading
|
|
3732
|
+
<div id="usage-body" class="usage-note" role="status">Reading this project's Claude Code and Codex session logs.</div>
|
|
3733
|
+
</div>
|
|
3734
|
+
</div>
|
|
3735
|
+
|
|
3736
|
+
<div class="panel" id="panel-skills" role="tabpanel" aria-label="Skills">
|
|
3737
|
+
<div class="metrics-page">
|
|
3738
|
+
<div class="usage-head">
|
|
3739
|
+
<h1 class="page-title">Skills <span class="muted" style="font-weight:400;font-size:var(--fs-small)">local inventory, not a quality ranking</span></h1>
|
|
3740
|
+
<button type="button" class="usage-chip" onclick="loadSkillsInventory()">Reload</button>
|
|
3741
|
+
</div>
|
|
3742
|
+
<p class="usage-note">Project scope includes only the selected project's skill directories. Machine scope shows local host directories and plugin caches, not other projects' statistics.</p>
|
|
3743
|
+
<div class="skills-controls">
|
|
3744
|
+
<label>Search <input id="skills-search" type="search" aria-label="Filter skills" placeholder="Filter skills…" oninput="renderSkillsInventory()"></label>
|
|
3745
|
+
<label>Host location <select id="skills-host" aria-label="Skill host location" onchange="renderSkillsInventory()"><option value="all">All locations</option><option>Claude Code</option><option>Codex</option><option>Shared</option><option>Registry</option></select></label>
|
|
3746
|
+
<label>Scope <select id="skills-scope" aria-label="Skill scope" onchange="renderSkillsInventory()"><option value="all">Machine + selected project</option><option value="project">Selected project only</option><option value="machine">Machine only</option></select></label>
|
|
3747
|
+
</div>
|
|
3748
|
+
<p id="skills-status" class="usage-note" role="status">Reading local skill metadata.</p>
|
|
3749
|
+
<div id="skills-body" aria-busy="false"></div>
|
|
3750
|
+
<p class="usage-note">A file in a host location is not proof that the host enabled or loaded it. Shared and registry locations do not prove compatibility with either host. SHA-256 covers only the document, not its scripts or reference tree. Upstream trust and task quality are not measured. No installation, execution or upstream network checks. Read snapshots are cached for up to 30 seconds.</p>
|
|
3722
3751
|
</div>
|
|
3723
3752
|
</div>
|
|
3724
3753
|
|
|
@@ -4107,6 +4136,7 @@ window.GctoWork.bind({
|
|
|
4107
4136
|
let searchQuery = '';
|
|
4108
4137
|
let allProjects = [];
|
|
4109
4138
|
let currentProject = '';
|
|
4139
|
+
let serverDefaultProject = false;
|
|
4110
4140
|
let inboxData = null;
|
|
4111
4141
|
let activeFilters = { agents: new Set(), priorities: new Set(), labels: new Set() };
|
|
4112
4142
|
// Period selector for dashboard (1/7/30/90/365 days). Persists in localStorage.
|
|
@@ -4114,12 +4144,12 @@ let currentPeriod = parseInt(localStorage.getItem('gcto.period') || '30', 10) ||
|
|
|
4114
4144
|
|
|
4115
4145
|
// Returns ?project=slug query string for all API calls after init() sets currentProject
|
|
4116
4146
|
function pqs() {
|
|
4117
|
-
return currentProject ? `?project=${encodeURIComponent(currentProject)}` : '';
|
|
4147
|
+
return currentProject && !serverDefaultProject ? `?project=${encodeURIComponent(currentProject)}` : '';
|
|
4118
4148
|
}
|
|
4119
4149
|
// Combined query string: ?project=X&days=Y (for metrics/cost APIs)
|
|
4120
4150
|
function pqsd() {
|
|
4121
4151
|
const parts = [];
|
|
4122
|
-
if (currentProject) parts.push(`project=${encodeURIComponent(currentProject)}`);
|
|
4152
|
+
if (currentProject && !serverDefaultProject) parts.push(`project=${encodeURIComponent(currentProject)}`);
|
|
4123
4153
|
parts.push(`days=${currentPeriod}`);
|
|
4124
4154
|
return '?' + parts.join('&');
|
|
4125
4155
|
}
|
|
@@ -5130,7 +5160,7 @@ async function loadJudgeScores() {
|
|
|
5130
5160
|
const el = document.getElementById('judge-scores');
|
|
5131
5161
|
if (!el) return;
|
|
5132
5162
|
let d;
|
|
5133
|
-
try { d = await api(`/api/scores${pqs()}
|
|
5163
|
+
try { d = await api(`/api/scores?limit=40${pqs().replace('?', '&')}`); } catch { d = null; }
|
|
5134
5164
|
|
|
5135
5165
|
// Three states, and the empty one earns a sentence rather than a blank space:
|
|
5136
5166
|
// "nothing has been verified yet" and "the panel is broken" look identical
|
|
@@ -5885,7 +5915,7 @@ function renderTurns(snap) {
|
|
|
5885
5915
|
if (snap.state === 'unreadable') return `<div class="aa-empty aa-bad">Turns not measured: ${esc(snap.why || 'git refused')}</div>`;
|
|
5886
5916
|
const sessions = Array.isArray(snap.sessions) ? snap.sessions : [];
|
|
5887
5917
|
if (!sessions.length) return `<div class="aa-empty">No turn snapshots yet — they start with the next agent turn.</div>`;
|
|
5888
|
-
const when = (ms) => { try { return ms ? new Date(ms).toLocaleString(
|
|
5918
|
+
const when = (ms) => { try { return ms ? new Date(ms).toLocaleString('en-US', { hour12: false }) : ''; } catch { return ''; } };
|
|
5889
5919
|
return sessions.map((s) => {
|
|
5890
5920
|
const count = Math.min(Number(s.turns) || 0, 50);
|
|
5891
5921
|
const first = Math.max(0, (Number(s.newestTurn) || 0) - count + 1);
|
|
@@ -5974,7 +6004,7 @@ function renderAgentActivity(snap) {
|
|
|
5974
6004
|
root.innerHTML = `<div class="aa-empty">No agent events recorded yet — they start with the next agent run.</div>`;
|
|
5975
6005
|
return;
|
|
5976
6006
|
}
|
|
5977
|
-
const time = (ts) => { try { return new Date(ts).toLocaleTimeString(
|
|
6007
|
+
const time = (ts) => { try { return new Date(ts).toLocaleTimeString('en-US', { hour12: false }); } catch { return ''; } };
|
|
5978
6008
|
const what = (e) => {
|
|
5979
6009
|
if (e.kind === 'tool') {
|
|
5980
6010
|
const p = Array.isArray(e.paths) && e.paths.length ? ` ${esc(e.paths[0])}${e.paths.length > 1 ? ` +${e.paths.length - 1}` : ''}` : '';
|
|
@@ -6000,16 +6030,70 @@ function renderAgentActivity(snap) {
|
|
|
6000
6030
|
// The Harnesses card. Four states for the second opinion, and the fourth is
|
|
6001
6031
|
// the reason the card exists: `unavailable` — declared codex, no codex here —
|
|
6002
6032
|
// must never look like `none` or like a switch someone turned off.
|
|
6033
|
+
// Read-only Skills: keep controls mounted so filtering never steals focus.
|
|
6034
|
+
let skillsData = null;
|
|
6035
|
+
let _skillsRequest = 0;
|
|
6036
|
+
async function loadSkillsInventory() {
|
|
6037
|
+
const request = ++_skillsRequest, project = currentProject;
|
|
6038
|
+
const status = document.getElementById('skills-status');
|
|
6039
|
+
const body = document.getElementById('skills-body');
|
|
6040
|
+
skillsData = null; body.innerHTML = ''; body.setAttribute('aria-busy', 'true');
|
|
6041
|
+
status.textContent = 'Reading local skill metadata.';
|
|
6042
|
+
try {
|
|
6043
|
+
const data = await api(`/api/skills${pqs()}`, { signal: AbortSignal.timeout(10000) });
|
|
6044
|
+
if (request !== _skillsRequest || project !== currentProject) return;
|
|
6045
|
+
if (!data || !Array.isArray(data.skills)) throw new Error('Invalid inventory response');
|
|
6046
|
+
skillsData = data; renderSkillsInventory();
|
|
6047
|
+
} catch {
|
|
6048
|
+
if (request !== _skillsRequest || project !== currentProject) return;
|
|
6049
|
+
status.textContent = 'Could not read skill inventory. No counts or provenance were measured.';
|
|
6050
|
+
body.innerHTML = '';
|
|
6051
|
+
} finally {
|
|
6052
|
+
if (request === _skillsRequest && project === currentProject) body.setAttribute('aria-busy', 'false');
|
|
6053
|
+
}
|
|
6054
|
+
}
|
|
6055
|
+
function renderSkillsInventory() {
|
|
6056
|
+
if (!skillsData) return;
|
|
6057
|
+
const body = document.getElementById('skills-body');
|
|
6058
|
+
const q = document.getElementById('skills-search').value.trim().toLowerCase();
|
|
6059
|
+
const host = document.getElementById('skills-host').value, scope = document.getElementById('skills-scope').value;
|
|
6060
|
+
const d = skillsData;
|
|
6061
|
+
const rows = d.skills.filter(s => (host === 'all' || s.host === host) && (scope === 'all' || s.scope === scope)
|
|
6062
|
+
&& [s.name, s.location, s.declared_source].some(v => String(v || '').toLowerCase().includes(q)));
|
|
6063
|
+
document.getElementById('skills-status').textContent = `${d.state === 'partial' ? 'Partial inventory' : 'Observed inventory'} · ${rows.length} shown · ${d.skills.length} documents listed · ${fmtDate(d.observed_at)}${d.truncated ? ' · scan limit reached' : ''}`;
|
|
6064
|
+
const labels = { 'missing-frontmatter': 'Missing frontmatter', 'name-mismatch': 'Name differs from directory',
|
|
6065
|
+
'description-length': 'Description outside lint limits', 'invalid-frontmatter': 'Invalid frontmatter',
|
|
6066
|
+
'ambiguous-yaml': 'Ambiguous YAML', 'over-context-budget': 'Over 20 KB context budget',
|
|
6067
|
+
'too-large': 'Document too large', 'changed-during-read': 'Document changed during read',
|
|
6068
|
+
unreadable: 'Unreadable', missing: 'Missing', 'outside-root': 'Outside allowed root' };
|
|
6069
|
+
const table = rows.length ? `<div class="usage-table-wrap"><table id="skills-table" class="usage-table"><caption class="usage-note">Local document observations</caption><thead><tr><th scope="col">Skill</th><th scope="col">Location</th><th scope="col">Scope</th><th scope="col">Document integrity</th><th scope="col">Structure</th></tr></thead><tbody>${rows.map(s => `<tr>
|
|
6070
|
+
<td>${esc(s.name)}<div class="usage-sub">${esc(s.host)} location</div></td>
|
|
6071
|
+
<td>${esc(s.location)}${s.declared_source ? `<div class="usage-sub">Registry declaration: ${esc(s.declared_source)}</div>` : ''}${s.cache_version ? `<div class="usage-sub">Cache version: ${esc(s.cache_version)}</div>` : ''}</td>
|
|
6072
|
+
<td>${s.scope === 'project' ? 'Selected project' : 'Machine'}</td>
|
|
6073
|
+
<td>${s.document_sha256 ? `<code>${esc(s.document_sha256)}</code><div class="usage-sub">${esc(s.bytes)} bytes · document only</div>` : esc(labels[s.read_state] || 'Not measured')}<div class="usage-sub">Upstream revision: unknown</div></td>
|
|
6074
|
+
<td>${s.read_state !== 'observed' ? 'Not checked' : s.warnings.length ? s.warnings.map(v => esc(labels[v] || v)).join('<br>') : 'No structural warnings'}<div class="usage-sub">Enabled / loaded: unknown</div></td>
|
|
6075
|
+
</tr>`).join('')}</tbody></table></div>` : `<p class="usage-note">${d.skills.length ? 'No skills match these filters.' : d.state === 'partial' ? 'No documents were observed. Some sources could not be read; this is not a complete empty inventory.' : 'No skill documents found in the scanned local locations.'}</p>`;
|
|
6076
|
+
body.innerHTML = table + `<details class="usage-note" style="margin-top:16px"><summary>Source coverage and registry</summary><p>Registry: ${esc(d.registry.state)} · declared at: ${esc(d.registry.declared_at || 'unknown')} · declared plugin version: ${esc(d.registry.plugin_version || 'unknown')}</p>
|
|
6077
|
+
<ul>${d.sources.map(s => `<li>${esc(s.label)} · ${esc(s.scope === 'project' ? 'selected project' : 'machine')} · ${esc(s.state)}${s.warnings.length ? ' · ' + s.warnings.map(esc).join(', ') : ''}</li>`).join('')}</ul></details>`;
|
|
6078
|
+
}
|
|
6079
|
+
|
|
6003
6080
|
// ── Usage (great_cto-egkw) ─────────────────────────────────────────────────
|
|
6004
6081
|
// Both hosts, side by side, from /api/usage. Three honesty rules carried from
|
|
6005
6082
|
// the reader: an unpriced model is n/a, never $0; Claude Code's plan use is not
|
|
6006
6083
|
// written anywhere, so it is not drawn — only the refusals are; and a host with
|
|
6007
|
-
// no logs
|
|
6084
|
+
// no logs attributed to this project says so instead of showing zeros.
|
|
6008
6085
|
let usageDays = 30;
|
|
6009
6086
|
let usageHost = 'claude';
|
|
6010
6087
|
let usageData = null;
|
|
6011
6088
|
let _usageTimer = null;
|
|
6012
6089
|
let _usageRequest = 0;
|
|
6090
|
+
function resetProjectUsage() {
|
|
6091
|
+
clearTimeout(_usageTimer); clearTimeout(_outcomesTimer);
|
|
6092
|
+
++_usageRequest; ++_outcomesRequest;
|
|
6093
|
+
usageData = null;
|
|
6094
|
+
const body = document.getElementById('usage-body');
|
|
6095
|
+
if (body) { body.className = 'usage-note'; body.textContent = 'Reading the selected project’s statistics…'; }
|
|
6096
|
+
}
|
|
6013
6097
|
const USAGE_HOSTS = { claude: 'Claude Code', codex: 'Codex' };
|
|
6014
6098
|
const USAGE_LOGS = { claude: '~/.claude/projects', codex: '~/.codex/sessions' };
|
|
6015
6099
|
const USAGE_FN = { chat: 'Conversations', subagent: 'Subagents', automation: 'Automations', headless: 'Headless runs' };
|
|
@@ -6029,16 +6113,17 @@ function fmtUsdEq(n) {
|
|
|
6029
6113
|
const fmtPct = (x) => (x == null ? null : `${Math.round(x * 100)}%`);
|
|
6030
6114
|
|
|
6031
6115
|
async function loadUsage(days) {
|
|
6116
|
+
if (!currentProject) return; // init() loads again after resolving the project.
|
|
6032
6117
|
if (days) usageDays = days;
|
|
6033
6118
|
document.querySelectorAll('.usage-chip[data-days]').forEach((c) => c.classList.toggle('active', Number(c.dataset.days) === usageDays));
|
|
6034
6119
|
clearTimeout(_usageTimer);
|
|
6035
|
-
const request = ++_usageRequest, requestedDays = usageDays;
|
|
6120
|
+
const request = ++_usageRequest, requestedDays = usageDays, requestedProject = currentProject;
|
|
6036
6121
|
const loadingBody = document.getElementById('usage-body');
|
|
6037
6122
|
if (loadingBody && (!usageData || usageData.window?.days !== requestedDays)) {
|
|
6038
6123
|
loadingBody.className = 'usage-note'; loadingBody.textContent = 'Loading session statistics…';
|
|
6039
6124
|
}
|
|
6040
|
-
const d = await api(`/api/usage?days=${
|
|
6041
|
-
if (request !== _usageRequest || requestedDays !== usageDays || currentTab !== 'usage') return;
|
|
6125
|
+
const d = await api(`/api/usage?days=${requestedDays}${pqs().replace('?', '&')}`);
|
|
6126
|
+
if (request !== _usageRequest || requestedDays !== usageDays || requestedProject !== currentProject || currentTab !== 'usage') return;
|
|
6042
6127
|
const body = document.getElementById('usage-body');
|
|
6043
6128
|
if (!body) return;
|
|
6044
6129
|
if (!d || d.error) {
|
|
@@ -6052,9 +6137,9 @@ async function loadUsage(days) {
|
|
|
6052
6137
|
_usageTimer = setTimeout(() => { if (currentTab === 'usage') loadUsage(); }, 2000);
|
|
6053
6138
|
return;
|
|
6054
6139
|
}
|
|
6055
|
-
if (d.state !== 'counted') {
|
|
6140
|
+
if (d.state !== 'counted' || d.scope?.kind !== 'project') {
|
|
6056
6141
|
body.className = 'usage-note';
|
|
6057
|
-
body.textContent = `Usage is not available: ${d.why || d.state}`;
|
|
6142
|
+
body.textContent = `Usage is not available: ${d.why || (d.scope?.kind !== 'project' ? 'the server did not confirm project-scoped statistics; update the board' : d.state)}`;
|
|
6058
6143
|
return;
|
|
6059
6144
|
}
|
|
6060
6145
|
usageData = d;
|
|
@@ -6083,7 +6168,7 @@ function usageLimitWindow(minutes) {
|
|
|
6083
6168
|
function usageHostCard(d, host) {
|
|
6084
6169
|
const name = USAGE_HOSTS[host];
|
|
6085
6170
|
if (!d.seen || !d.seen[host]) {
|
|
6086
|
-
return `<section class="usage-card" data-host="${host}"><h3>${name}</h3><p class="usage-note">No ${name} session logs
|
|
6171
|
+
return `<section class="usage-card" data-host="${host}"><h3>${name}</h3><p class="usage-note">No ${name} session logs attributed to this project — nothing to count, which is not the same as nothing used.</p></section>`;
|
|
6087
6172
|
}
|
|
6088
6173
|
const h = d.hosts[host];
|
|
6089
6174
|
const unpricedHere = (d.models || []).filter((m) => m.host === host && m.usd == null).map((m) => m.model);
|
|
@@ -6124,6 +6209,10 @@ function usageHostCard(d, host) {
|
|
|
6124
6209
|
: `<div class="usage-limit"><div class="usage-limit-head"><span>Plan limit</span>${absent('unloaded', usageRecorderHint(d).short)}</div>
|
|
6125
6210
|
<div class="usage-sub">${hits.plan} refused at the plan limit · ${hits.server} server-side throttles (not your limit) in ${d.window.days} days · plan percentage: ${d.limits && d.limits.claude && d.limits.claude.recorder === 'on' ? 'from <code>claude</code> in a terminal' : '<code>great-cto statusline install</code>'}</div></div>`;
|
|
6126
6211
|
}
|
|
6212
|
+
if (d.scope?.kind === 'project') {
|
|
6213
|
+
const hits = h.limitHits || { plan: 0, server: 0 };
|
|
6214
|
+
limit = `<div class="usage-sub">In this project: ${hits.plan} plan-limit refusals · ${hits.server} server-side throttles in ${d.window.days} days.</div>`;
|
|
6215
|
+
}
|
|
6127
6216
|
return `<section class="usage-card" data-host="${host}">
|
|
6128
6217
|
<h3><span>${name}</span><span class="muted">${esc(d.window.from)} → ${esc(d.window.to)}</span></h3>
|
|
6129
6218
|
<div class="usage-big" title="${esc(`${(h.tokens || 0).toLocaleString('en-US')} tokens, cache reads included`)}">${fmtTok(h.tokens)} <span class="muted" style="font-size:var(--fs-small)">tokens</span></div>
|
|
@@ -6174,7 +6263,7 @@ function usageTopTable(d, host) {
|
|
|
6174
6263
|
const name = USAGE_HOSTS[host];
|
|
6175
6264
|
const rows = (d.top && d.top[host]) || [];
|
|
6176
6265
|
const head = `<h3><span>${name}: heaviest conversations</span><span class="muted">share of ${name} tokens</span></h3>`;
|
|
6177
|
-
if (!rows.length) return `<section class="usage-card">${head}<p class="usage-note">${d.seen && d.seen[host] ? 'Nothing in this window.' : 'No logs
|
|
6266
|
+
if (!rows.length) return `<section class="usage-card">${head}<p class="usage-note">${d.seen && d.seen[host] ? 'Nothing in this window.' : 'No logs attributed to this project.'}</p></section>`;
|
|
6178
6267
|
const money = host === 'claude';
|
|
6179
6268
|
const body = rows.map((r) => {
|
|
6180
6269
|
const tag = r.fn && r.fn !== 'chat' ? `<span class="usage-tag">${esc(USAGE_FN[r.fn] || r.fn)}</span>` : '';
|
|
@@ -6317,10 +6406,11 @@ function usageCodexLimits(d) {
|
|
|
6317
6406
|
}
|
|
6318
6407
|
|
|
6319
6408
|
// ── Agents and reviews (great_cto-egkw stage 3) ────────────────────────────
|
|
6320
|
-
// Outcomes come from /api/outcomes (verdicts of
|
|
6409
|
+
// Outcomes come from /api/outcomes (verdicts of the selected project, Beads
|
|
6321
6410
|
// bugs by priority); hook refusals and failures from /api/usage (Claude Code's
|
|
6322
6411
|
// own logs). Counts only — no bug title reaches the page.
|
|
6323
6412
|
let _outcomesTimer = null;
|
|
6413
|
+
let _outcomesRequest = 0;
|
|
6324
6414
|
function fmtSpan(days) {
|
|
6325
6415
|
if (days == null) return '—';
|
|
6326
6416
|
const h = days * 24;
|
|
@@ -6332,7 +6422,10 @@ function fmtSpan(days) {
|
|
|
6332
6422
|
async function loadOutcomes() {
|
|
6333
6423
|
clearTimeout(_outcomesTimer);
|
|
6334
6424
|
if (!document.getElementById('usage-outcomes')) return;
|
|
6335
|
-
const
|
|
6425
|
+
const request = ++_outcomesRequest;
|
|
6426
|
+
const requestedProject = currentProject, requestedDays = usageDays, requestedHost = usageHost;
|
|
6427
|
+
const d = await api(`/api/outcomes?days=${requestedDays}${pqs().replace('?', '&')}`);
|
|
6428
|
+
if (request !== _outcomesRequest || requestedProject !== currentProject || requestedDays !== usageDays || requestedHost !== usageHost || currentTab !== 'usage') return;
|
|
6336
6429
|
// Looked up AFTER the answer: Usage may have re-rendered while it was on the
|
|
6337
6430
|
// way, and an answer written into the replaced element is an answer nobody sees.
|
|
6338
6431
|
const el = document.getElementById('usage-outcomes');
|
|
@@ -6344,14 +6437,14 @@ async function loadOutcomes() {
|
|
|
6344
6437
|
_outcomesTimer = setTimeout(() => { if (currentTab === 'usage') loadOutcomes(); }, 2000);
|
|
6345
6438
|
return;
|
|
6346
6439
|
}
|
|
6347
|
-
if (d.state !== 'counted') { el.className = 'usage-note'; el.textContent = `
|
|
6440
|
+
if (d.state !== 'counted' || d.scope?.kind !== 'project') { el.className = 'usage-note'; el.textContent = `Project outcomes are not available: ${d.why || d.state}`; return; }
|
|
6348
6441
|
el.className = 'usage-grid';
|
|
6349
6442
|
el.innerHTML = usageAgentsCard(d) + usageFindingsCard(d);
|
|
6350
6443
|
}
|
|
6351
6444
|
|
|
6352
6445
|
function usageAgentsCard(d) {
|
|
6353
6446
|
const A = d.agents || { agents: [] };
|
|
6354
|
-
const head = `<h3><span>Agents: what they concluded</span><span class="muted"
|
|
6447
|
+
const head = `<h3><span>Agents: what they concluded</span><span class="muted">this project · ${esc(d.window.from)} → ${esc(d.window.to)}</span></h3>`;
|
|
6355
6448
|
if (!A.agents.length) return `<section class="usage-card" data-outcomes="agents">${head}<p class="usage-note">No great_cto agent recorded a verdict in this period.</p></section>`;
|
|
6356
6449
|
const dispatched = Object.fromEntries(((usageData && usageData.lists && usageData.lists.claude && usageData.lists.claude.agents) || []).map((a) => [a.name, a.n]));
|
|
6357
6450
|
const rows = A.agents.map((a) => {
|
|
@@ -6391,7 +6484,7 @@ function usageFindingsCard(d) {
|
|
|
6391
6484
|
|
|
6392
6485
|
function usageHooksCards(d) {
|
|
6393
6486
|
const H = d.hooks;
|
|
6394
|
-
if (!d.seen || !d.seen.claude) return '<p class="usage-note">No Claude Code logs
|
|
6487
|
+
if (!d.seen || !d.seen.claude) return '<p class="usage-note">No Claude Code logs attributed to this project — hook outcomes are read from them.</p>';
|
|
6395
6488
|
if (!H) return '';
|
|
6396
6489
|
const total = (rows) => (rows || []).reduce((n, r) => n + r.n, 0);
|
|
6397
6490
|
return `<div class="usage-lists">
|
|
@@ -6411,8 +6504,7 @@ function renderUsage(d) {
|
|
|
6411
6504
|
const hostBtn = (h) => `<button type="button" class="usage-chip${usageHost === h ? ' active' : ''}" aria-pressed="${usageHost === h}" onclick="setUsageHost('${h}')">${USAGE_HOSTS[h]}</button>`;
|
|
6412
6505
|
body.innerHTML = `
|
|
6413
6506
|
<div class="usage-grid">${usageHostCard(d, 'claude')}${usageHostCard(d, 'codex')}</div>
|
|
6414
|
-
<
|
|
6415
|
-
<div class="usage-grid">${usageClaudeLimits(d)}${usageCodexLimits(d)}</div>
|
|
6507
|
+
<p class="usage-note">Statistics attributable to <b>${esc(currentProject || 'the current project')}</b> only, including existing linked Git worktrees. Sessions without a recoverable project identity are excluded; missing history is not proof of no activity. Account-wide subscription quotas are not project statistics and are not shown here.</p>
|
|
6416
6508
|
<h2 class="mp-section-label">By day, by kind of work</h2>
|
|
6417
6509
|
<div class="usage-grid">${usageDayChart(d, 'claude')}${usageDayChart(d, 'codex')}</div>
|
|
6418
6510
|
<h2 class="mp-section-label">Heaviest conversations</h2>
|
|
@@ -6427,7 +6519,7 @@ function renderUsage(d) {
|
|
|
6427
6519
|
${usageList('MCP servers', l.mcp, 'No MCP tools were called in this window.')}
|
|
6428
6520
|
</div>
|
|
6429
6521
|
<h2 class="mp-section-label">Agents and reviews</h2>
|
|
6430
|
-
<div id="usage-outcomes" class="usage-note" role="status">Reading
|
|
6522
|
+
<div id="usage-outcomes" class="usage-note" role="status">Reading this project's verdicts and Beads.</div>
|
|
6431
6523
|
<h2 class="mp-section-label">Guards and hooks</h2>
|
|
6432
6524
|
${usageHooksCards(d)}
|
|
6433
6525
|
<p class="usage-note">Read on this machine from ${USAGE_LOGS.claude} and ${USAGE_LOGS.codex}; nothing is sent anywhere. Tokens include cache reads. <b>$ list</b> is the API list-price equivalent from great_cto's price table, not a subscription bill — Claude Code's own estimate can differ for some models. A model without a price shows n/a, never $0.</p>`;
|
|
@@ -7372,7 +7464,7 @@ async function searchTranscripts() {
|
|
|
7372
7464
|
const body = document.getElementById('sessions-body');
|
|
7373
7465
|
if (q.length < 3) { loadSessions(); return; }
|
|
7374
7466
|
body.innerHTML = '<div class="muted">Searching…</div>';
|
|
7375
|
-
const d = await api(`/api/session-search
|
|
7467
|
+
const d = await api(`/api/session-search?q=${encodeURIComponent(q)}${pqs().replace('?', '&')}`);
|
|
7376
7468
|
if (!d) { body.innerHTML = '<div class="muted">Search failed.</div>'; return; }
|
|
7377
7469
|
if (!d.hits?.length) {
|
|
7378
7470
|
body.innerHTML = `<div class="muted">No match for “${esc(q)}”.${d.note ? ' ' + esc(d.note) : ''}
|
|
@@ -7621,7 +7713,14 @@ async function switchProject(slug) {
|
|
|
7621
7713
|
if (lbl) lbl.textContent = `loading ${slug}…`;
|
|
7622
7714
|
// Close existing SSE so it doesn't push the wrong project's data
|
|
7623
7715
|
if (window._gctoSSE) { try { window._gctoSSE.close(); } catch {} window._gctoSSE = null; }
|
|
7716
|
+
serverDefaultProject = false;
|
|
7624
7717
|
currentProject = slug;
|
|
7718
|
+
document.getElementById('project-name').textContent = slug;
|
|
7719
|
+
document.getElementById('crumb-project').textContent = slug;
|
|
7720
|
+
document.title = `${slug} · great_cto`;
|
|
7721
|
+
resetProjectUsage();
|
|
7722
|
+
if (currentTab === 'usage') loadUsage();
|
|
7723
|
+
if (currentTab === 'skills') loadSkillsInventory();
|
|
7625
7724
|
window.GctoWork.reset();
|
|
7626
7725
|
// Optimistic UI clear
|
|
7627
7726
|
allTasks = [];
|
|
@@ -7634,12 +7733,10 @@ async function switchProject(slug) {
|
|
|
7634
7733
|
api(`/api/share${pqs()}`),
|
|
7635
7734
|
api(`/api/inbox${pqs()}`),
|
|
7636
7735
|
]);
|
|
7736
|
+
if (slug !== currentProject) return;
|
|
7637
7737
|
allTasks = tasks || [];
|
|
7638
7738
|
metrics = met || {};
|
|
7639
7739
|
shareState = share || { enabled: false, url: null };
|
|
7640
|
-
document.getElementById('project-name').textContent = slug;
|
|
7641
|
-
document.getElementById('crumb-project').textContent = slug;
|
|
7642
|
-
document.title = `${slug} · great_cto`;
|
|
7643
7740
|
const me = (allProjects || []).find(p => p.slug === slug);
|
|
7644
7741
|
const arch = me?.archetype || 'default';
|
|
7645
7742
|
const swIcon = document.getElementById('proj-switch-icon');
|
|
@@ -7833,7 +7930,11 @@ async function init() {
|
|
|
7833
7930
|
|| (allProjects[0]?.slug)
|
|
7834
7931
|
|| fromTaskId
|
|
7835
7932
|
|| 'project';
|
|
7933
|
+
serverDefaultProject = !urlProject && !(allProjects || []).some(p => p.slug === proj);
|
|
7836
7934
|
currentProject = proj;
|
|
7935
|
+
resetProjectUsage();
|
|
7936
|
+
if (currentTab === 'usage') loadUsage();
|
|
7937
|
+
if (currentTab === 'skills') loadSkillsInventory();
|
|
7837
7938
|
window.GctoWork.reset();
|
|
7838
7939
|
document.getElementById('project-name').textContent = proj;
|
|
7839
7940
|
document.getElementById('crumb-project').textContent = proj;
|
|
@@ -8498,7 +8599,7 @@ function renderShareState(state) {
|
|
|
8498
8599
|
: ago < 1440 ? `${Math.floor(ago/60)}h ago`
|
|
8499
8600
|
: `${Math.floor(ago/1440)}d ago`;
|
|
8500
8601
|
const stale = ago > 60 * 24 ? ' · ⚠ data is stale — click ↻ Republish' : '';
|
|
8501
|
-
metaEl.textContent = `Published ${agoStr} (${dt.toLocaleString()})${stale}`;
|
|
8602
|
+
metaEl.textContent = `Published ${agoStr} (${dt.toLocaleString('en-US')})${stale}`;
|
|
8502
8603
|
} else if (metaEl) {
|
|
8503
8604
|
metaEl.textContent = '';
|
|
8504
8605
|
}
|
|
@@ -8917,6 +9018,7 @@ function cmdkActions() {
|
|
|
8917
9018
|
{ group: 'Go', label: 'Fleet — the agents and what they may do', run: () => switchTab('fleet') },
|
|
8918
9019
|
{ group: 'Go', label: 'Harness — host and second opinion', run: () => switchTab('harness') },
|
|
8919
9020
|
{ group: 'Go', label: 'Usage — what Claude Code and Codex consumed', run: () => switchTab('usage') },
|
|
9021
|
+
{ group: 'Go', label: 'Skills — local inventory and document integrity', run: () => switchTab('skills') },
|
|
8920
9022
|
{ group: 'Go', label: 'Settings', run: () => switchTab('settings') },
|
|
8921
9023
|
{ group: 'Go', label: 'Tasks board (#/kanban, deep link)', run: () => switchTab('kanban') },
|
|
8922
9024
|
{ group: 'Do', label: 'New issue', run: () => openNewIssueModal() },
|
|
@@ -9109,7 +9211,7 @@ try {
|
|
|
9109
9211
|
// panels, not keys.
|
|
9110
9212
|
const DEST = { decisions: 'inbox', ledger: 'budgets', fleet: 'agents', dashboard: 'budgets', share: 'settings', notifications: 'settings' };
|
|
9111
9213
|
const NAV = { inbox: 'decisions', budgets: 'ledger', agents: 'fleet', dashboard: 'ledger', share: 'settings', notifications: 'settings' };
|
|
9112
|
-
const ROUTES = { work: 1, history: 1, decisions: 1, ledger: 1, fleet: 1, harness: 1, usage: 1, settings: 1, kanban: 1 };
|
|
9214
|
+
const ROUTES = { work: 1, history: 1, decisions: 1, ledger: 1, fleet: 1, harness: 1, usage: 1, skills: 1, settings: 1, kanban: 1 };
|
|
9113
9215
|
function switchTab(id, el) {
|
|
9114
9216
|
const lid = DEST[id] || id; // the panel's own name
|
|
9115
9217
|
const navKey = NAV[id] || id; // the destination to highlight
|
|
@@ -9118,7 +9220,7 @@ function switchTab(id, el) {
|
|
|
9118
9220
|
currentTab = lid;
|
|
9119
9221
|
document.getElementById('nav-views-label').hidden = lid !== 'agents';
|
|
9120
9222
|
document.querySelector('.nav-views').hidden = lid !== 'agents';
|
|
9121
|
-
if (['agents', 'budgets', 'harness', 'usage'].includes(lid)) document.getElementById('tools-nav').open = true;
|
|
9223
|
+
if (['agents', 'budgets', 'harness', 'usage', 'skills'].includes(lid)) document.getElementById('tools-nav').open = true;
|
|
9122
9224
|
if (lid === 'work' || lid === 'history') window.GctoWork.refresh();
|
|
9123
9225
|
if (ROUTES[id]) {
|
|
9124
9226
|
if (location.hash !== `#/${id}`) history.replaceState(null, '', `#/${id}`);
|
|
@@ -9141,7 +9243,7 @@ function switchTab(id, el) {
|
|
|
9141
9243
|
// crumb — `labels` used to be referenced here but is only ever a function-local in
|
|
9142
9244
|
// this file, so this threw ReferenceError on every tab switch and skipped the per-tab
|
|
9143
9245
|
// loaders below (e.g. logs stuck on "Loading…"). Use a local label map.
|
|
9144
|
-
const TAB_LABELS = { work: 'Work', history: 'History', decisions: 'Decisions', ledger: 'Ledger', fleet: 'Fleet', harness: 'Harness', usage: 'Usage', settings: 'Settings', budgets: 'Budgets', docs: 'Docs', dashboard: 'Board', inbox: 'Inbox', tasks: 'Tasks', agents: 'Agents', logs: 'Activity', notifications: 'Notifications', metrics: 'Metrics', share: 'Share' };
|
|
9246
|
+
const TAB_LABELS = { work: 'Work', history: 'History', decisions: 'Decisions', ledger: 'Ledger', fleet: 'Fleet', harness: 'Harness', usage: 'Usage', skills: 'Skills', settings: 'Settings', budgets: 'Budgets', docs: 'Docs', dashboard: 'Board', inbox: 'Inbox', tasks: 'Tasks', agents: 'Agents', logs: 'Activity', notifications: 'Notifications', metrics: 'Metrics', share: 'Share' };
|
|
9145
9247
|
const crumb = document.getElementById('crumb-here'); if (crumb) crumb.textContent = TAB_LABELS[id] || TAB_LABELS[lid] || 'Board';
|
|
9146
9248
|
// Budgets render on Metrics, so the heartbeat that feeds them has to be
|
|
9147
9249
|
// fetched when Metrics opens — not only from the agents drill-in it used to
|
|
@@ -9175,6 +9277,7 @@ function switchTab(id, el) {
|
|
|
9175
9277
|
if (lid === 'settings') loadSettingsPage();
|
|
9176
9278
|
if (lid === 'harness') renderHarnesses();
|
|
9177
9279
|
if (lid === 'usage') loadUsage();
|
|
9280
|
+
if (lid === 'skills') loadSkillsInventory();
|
|
9178
9281
|
}
|
|
9179
9282
|
|
|
9180
9283
|
// Sidebar tabs: Enter/Space activate, arrows move (WAI-ARIA tablist).
|
|
@@ -9983,9 +10086,11 @@ function relTime(iso) {
|
|
|
9983
10086
|
if (s < 86400) return `${Math.round(s/3600)}h`;
|
|
9984
10087
|
return `${Math.round(s/86400)}d`;
|
|
9985
10088
|
}
|
|
9986
|
-
|
|
10089
|
+
// Board-owned copy and formatting stay English; task/document content is rendered
|
|
10090
|
+
// in its original language. Locale is fixed, but the operator's timezone is not.
|
|
10091
|
+
function fmtDate(iso) { return iso ? new Date(iso).toLocaleString('en-US') : '—'; }
|
|
9987
10092
|
// Short date+time stamp for cards (full timestamp goes in the title attribute).
|
|
9988
|
-
function fmtDT(iso) { return iso ? new Date(iso).toLocaleString(
|
|
10093
|
+
function fmtDT(iso) { return iso ? new Date(iso).toLocaleString('en-US', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) : ''; }
|
|
9989
10094
|
// Money formatting — uses non-breaking thin space (U+202F) as thousands separator
|
|
9990
10095
|
// to avoid the comma-as-decimal confusion ($195,960 reads as "195.96" in EU/RU
|
|
9991
10096
|
// locales). Matches SI standard. Returns "1 234" not "1,234".
|
|
@@ -10147,7 +10252,7 @@ function renderNotifHistoryInline() {
|
|
|
10147
10252
|
<span style="font-size:var(--fs-small);color:var(--text3);margin-left:8px">${esc(n.body)}</span>
|
|
10148
10253
|
</div>
|
|
10149
10254
|
<span style="font-size:var(--fs-caption);color:var(--text3);font-family:var(--mono);flex-shrink:0">
|
|
10150
|
-
${new Date(n.ts).toLocaleTimeString()}
|
|
10255
|
+
${new Date(n.ts).toLocaleTimeString('en-US')}
|
|
10151
10256
|
</span>
|
|
10152
10257
|
</div>`).join('');
|
|
10153
10258
|
}
|
|
@@ -783,7 +783,7 @@ if (paused) {
|
|
|
783
783
|
// (b) Cost source explicitly comes from plans/verdicts (independent measurement)
|
|
784
784
|
const savingsTrustworthy = realLlmUsd > 0 || (m.cost?.source && m.cost.source !== 'tasks');
|
|
785
785
|
const effectiveSavings = savingsTrustworthy && effectiveLlmUsd > 0 && effectiveHumanUsd > 0
|
|
786
|
-
? Math.round(effectiveHumanUsd / effectiveLlmUsd).toLocaleString() + '×' : null;
|
|
786
|
+
? Math.round(effectiveHumanUsd / effectiveLlmUsd).toLocaleString('en-US') + '×' : null;
|
|
787
787
|
const effectiveAiH = totalAiMin > 0 ? totalAiMin / 60 : totalTimeMin / 60;
|
|
788
788
|
|
|
789
789
|
// Hero narrative — focus on cost difference, not time
|
|
@@ -800,7 +800,7 @@ if (paused) {
|
|
|
800
800
|
// Human equivalent: standard 4h per feature × $150/hr
|
|
801
801
|
const humanCost = done * 4 * 150;
|
|
802
802
|
if (aiCostDisplay && humanCost > 0) {
|
|
803
|
-
line += `. Human team estimate: $${humanCost.toLocaleString()}.`;
|
|
803
|
+
line += `. Human team estimate: $${humanCost.toLocaleString('en-US')}.`;
|
|
804
804
|
}
|
|
805
805
|
return line;
|
|
806
806
|
})();
|
|
@@ -1037,7 +1037,7 @@ if (paused) {
|
|
|
1037
1037
|
const costRatio = (effectiveHumanUsd > 0 && effectiveLlmUsd > 0)
|
|
1038
1038
|
? effectiveHumanUsd / effectiveLlmUsd : 0;
|
|
1039
1039
|
const costRatioStr = costRatio >= 2
|
|
1040
|
-
? Math.round(costRatio).toLocaleString() + '×'
|
|
1040
|
+
? Math.round(costRatio).toLocaleString('en-US') + '×'
|
|
1041
1041
|
: null;
|
|
1042
1042
|
if (effectiveSavings) {
|
|
1043
1043
|
return `<div class="big">${effectiveSavings} cheaper</div>
|
|
@@ -1058,7 +1058,7 @@ if (paused) {
|
|
|
1058
1058
|
<div class="cost-bar-row human">
|
|
1059
1059
|
<div class="cb-name">Humans</div>
|
|
1060
1060
|
<div class="cb-rail"><div class="cb-fill" style="width:${humanPct.toFixed(1)}%"></div></div>
|
|
1061
|
-
<div class="cb-val">$${Number((effectiveHumanUsd > 0 ? effectiveHumanUsd : humanH * 150).toFixed(0)).toLocaleString()}</div>
|
|
1061
|
+
<div class="cb-val">$${Number((effectiveHumanUsd > 0 ? effectiveHumanUsd : humanH * 150).toFixed(0)).toLocaleString('en-US')}</div>
|
|
1062
1062
|
</div>
|
|
1063
1063
|
<div class="cost-bar-row ai">
|
|
1064
1064
|
<div class="cb-name">AI</div>
|
|
@@ -24,8 +24,9 @@ import { fileURLToPath } from 'node:url';
|
|
|
24
24
|
|
|
25
25
|
import { classify } from './change-tier.mjs';
|
|
26
26
|
import { selectJudgeModel } from './judge-model.mjs';
|
|
27
|
-
//
|
|
28
|
-
|
|
27
|
+
// Generated from the canonical CLI policy, but shipped in Git too: marketplace
|
|
28
|
+
// installs do not contain the gitignored CLI build.
|
|
29
|
+
import { effectiveGates } from './gate-policy.mjs';
|
|
29
30
|
|
|
30
31
|
/** Parse archetype + project_size out of a .great_cto/PROJECT.md body. */
|
|
31
32
|
export function parseProject(text) {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Generated by scripts/build-gate-policy.mjs from packages/cli/src/archetypes.ts.
|
|
2
|
+
// Do not edit; regenerate, then run --check. No CLI build is needed at runtime.
|
|
3
|
+
export const GATES_BY_ARCHETYPE = {
|
|
4
|
+
"web-service": ["plan", "qa", "ship"],
|
|
5
|
+
"mobile-app": ["plan", "qa", "ship"],
|
|
6
|
+
"ai-system": ["plan", "cost", "qa", "security", "ship"],
|
|
7
|
+
"agent-product": ["plan", "cost", "qa", "security", "ship"],
|
|
8
|
+
"mlops": ["plan", "cost", "qa", "security", "ship"],
|
|
9
|
+
"data-platform": ["plan", "qa", "ship"],
|
|
10
|
+
"streaming": ["plan", "qa", "ship"],
|
|
11
|
+
"infra": ["plan", "qa", "ship"],
|
|
12
|
+
"library": ["plan", "qa", "ship"],
|
|
13
|
+
"cli-tool": ["plan", "qa", "ship"],
|
|
14
|
+
"commerce": ["plan", "qa", "security", "ship", "compliance"],
|
|
15
|
+
"marketplace": ["plan", "qa", "security", "ship", "compliance"],
|
|
16
|
+
"fintech": ["plan", "qa", "security", "ship", "compliance"],
|
|
17
|
+
"healthcare": ["plan", "qa", "security", "ship", "compliance"],
|
|
18
|
+
"web3": ["plan", "qa", "oracle-review", "security", "ship"],
|
|
19
|
+
"iot-embedded": ["plan", "qa", "security", "ship"],
|
|
20
|
+
"regulated": ["plan", "qa", "security", "ship", "compliance"],
|
|
21
|
+
"devtools": ["plan", "qa", "ship"],
|
|
22
|
+
"browser-extension": ["plan", "qa", "ship"],
|
|
23
|
+
"game": ["plan", "qa", "ship"],
|
|
24
|
+
"cms": ["plan", "qa", "ship"],
|
|
25
|
+
"enterprise-saas": ["plan", "qa", "security", "ship", "compliance"],
|
|
26
|
+
"edtech": ["plan", "qa", "edtech-review", "security", "ship", "compliance"],
|
|
27
|
+
"gov-public": ["plan", "qa", "gov-review", "security", "ship", "compliance"],
|
|
28
|
+
"insurance": ["plan", "qa", "insurance-review", "security", "ship", "compliance"],
|
|
29
|
+
"legal": ["plan", "qa", "upl-review", "security", "ship", "compliance"],
|
|
30
|
+
"defense-govcon": ["plan", "qa", "cmmc-assessment", "security", "ship", "compliance"],
|
|
31
|
+
// Product Builder archetypes (A1–A6) — non-regulated baseline; the single CTO gate
|
|
32
|
+
// emerges at change_tier T1 (effectiveGates → [plan]).
|
|
33
|
+
"vertical-saas": ["plan", "qa", "ship"],
|
|
34
|
+
"booking": ["plan", "qa", "ship"],
|
|
35
|
+
"crm": ["plan", "qa", "ship"],
|
|
36
|
+
"dashboard": ["plan", "qa", "ship"],
|
|
37
|
+
"content-platform": ["plan", "qa", "ship"],
|
|
38
|
+
"marketplace-lite": ["plan", "qa", "ship"],
|
|
39
|
+
"greenfield": ["plan"],
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Returns the subset of gates the pipeline will actually open for a
|
|
43
|
+
* given archetype + project_size. Used by the orchestrator before
|
|
44
|
+
* opening any gate to skip unnecessary human checkpoints on smaller
|
|
45
|
+
* projects (e.g. nano always skips QA).
|
|
46
|
+
*/
|
|
47
|
+
export function gatesFor(archetype, size) {
|
|
48
|
+
const all = GATES_BY_ARCHETYPE[archetype] ?? [];
|
|
49
|
+
if (size === "nano")
|
|
50
|
+
return all.filter((g) => g === "plan");
|
|
51
|
+
if (size === "small")
|
|
52
|
+
return all.filter((g) => g === "plan" || g === "ship");
|
|
53
|
+
if (size === "medium")
|
|
54
|
+
return all;
|
|
55
|
+
// large + enterprise → ensure compliance is included
|
|
56
|
+
return all.includes("compliance") ? all : [...all, "compliance"];
|
|
57
|
+
}
|
|
58
|
+
/** Gates that form the non-negotiable floor for a *regulated* archetype. */
|
|
59
|
+
const FLOOR_GATES = ["security", "compliance", "ship"];
|
|
60
|
+
/**
|
|
61
|
+
* The human gates the pipeline opens for a change, given its archetype, the
|
|
62
|
+
* project size, AND the per-change risk tier. Composes with `gatesFor()`:
|
|
63
|
+
*
|
|
64
|
+
* - T2 (default): the full size baseline, with `ship` guaranteed even if a
|
|
65
|
+
* small project_size had stripped it. An irreversible change always gets a
|
|
66
|
+
* final go/no-go. Nothing is ever removed → fail-safe for an unknown tier.
|
|
67
|
+
* - T1: only `plan` (you review intent) PLUS the regulated floor. The standalone
|
|
68
|
+
* qa/security-as-ceremony is dropped because CI + tests cover a reversible change.
|
|
69
|
+
* - T0: the regulated floor only — for a plain archetype that is the empty set
|
|
70
|
+
* (CI is the gate); for a regulated one, security+compliance+ship still fire.
|
|
71
|
+
*
|
|
72
|
+
* An archetype is "regulated" iff its baseline contains `security` or `compliance`.
|
|
73
|
+
* That is what keeps the downgrade sound: a fintech repo cannot ship an
|
|
74
|
+
* irreversible change unreviewed even when the change is labelled a fix.
|
|
75
|
+
*
|
|
76
|
+
* NOTE: change-specific T2 triggers (a migration, a new write-connector, a prod
|
|
77
|
+
* deploy) are resolved upstream by the classifier, which escalates such a change
|
|
78
|
+
* to T2 before this function sees it. Here we only map an already-decided tier.
|
|
79
|
+
*/
|
|
80
|
+
export function effectiveGates(archetype, size, tier = "T2") {
|
|
81
|
+
const base = gatesFor(archetype, size);
|
|
82
|
+
// T2 (and any unrecognized tier) → full baseline, never downgraded, ship forced.
|
|
83
|
+
if (tier !== "T0" && tier !== "T1") {
|
|
84
|
+
return base.includes("ship") ? base : [...base, "ship"];
|
|
85
|
+
}
|
|
86
|
+
// Regulated archetypes carry a floor that survives every downgrade.
|
|
87
|
+
const isRegulated = base.includes("security") || base.includes("compliance");
|
|
88
|
+
const floor = isRegulated ? base.filter((g) => FLOOR_GATES.includes(g)) : [];
|
|
89
|
+
if (tier === "T1") {
|
|
90
|
+
const keep = new Set(floor);
|
|
91
|
+
if (base.includes("plan"))
|
|
92
|
+
keep.add("plan");
|
|
93
|
+
return base.filter((g) => keep.has(g)); // preserve base order
|
|
94
|
+
}
|
|
95
|
+
// T0 — maintenance.
|
|
96
|
+
return floor;
|
|
97
|
+
}
|
|
@@ -60,7 +60,7 @@ function verdictFiles(dir) {
|
|
|
60
60
|
* Per-agent outcomes in the window.
|
|
61
61
|
* @param {{projects:{name:string,path:string}[], globalDir?:string, days?:number, now?:number, roster?:string[]}} opts
|
|
62
62
|
*/
|
|
63
|
-
export function agentOutcomes({ projects, globalDir = path.join(os.homedir(), '.great_cto', 'verdicts'), days = 30, now = Date.now(), roster = [] }) {
|
|
63
|
+
export function agentOutcomes({ projects, globalDir = path.join(os.homedir(), '.great_cto', 'verdicts'), days = 30, now = Date.now(), roster = [], projectScope = false }) {
|
|
64
64
|
const from = now - days * DAY;
|
|
65
65
|
const known = new Set(roster);
|
|
66
66
|
const seen = new Set();
|
|
@@ -78,6 +78,11 @@ export function agentOutcomes({ projects, globalDir = path.join(os.homedir(), '.
|
|
|
78
78
|
const p = parseVerdictLine(line);
|
|
79
79
|
if (!p.ok) continue;
|
|
80
80
|
const rec = p.rec;
|
|
81
|
+
// Local placement supplies attribution only when the record has no tag.
|
|
82
|
+
// An explicit foreign tag cannot be overridden by a copied local log;
|
|
83
|
+
// untagged global records remain unattributable.
|
|
84
|
+
if (projectScope && (project === null || rec.project != null)
|
|
85
|
+
&& !projects.some(p => (p.aliases || [p.name, p.path]).includes(rec.project))) continue;
|
|
81
86
|
const t = Date.parse(rec.ts);
|
|
82
87
|
if (!Number.isFinite(t) || t < from || t > now) continue;
|
|
83
88
|
const key = line.trim();
|
|
@@ -207,13 +212,13 @@ export async function bugFindings({ projects, days = 30, now = Date.now(), list
|
|
|
207
212
|
const sum = (o) => Object.values(o).reduce((a, n) => a + n, 0);
|
|
208
213
|
|
|
209
214
|
/** Both, for the board. */
|
|
210
|
-
export async function outcomes({ days = 30, now = Date.now(), roster = [], projects = registeredProjects(), globalDir, list } = {}) {
|
|
215
|
+
export async function outcomes({ days = 30, now = Date.now(), roster = [], projects = registeredProjects(), globalDir, list, projectScope = false } = {}) {
|
|
211
216
|
const findings = await bugFindings({ projects, days, now, list });
|
|
212
217
|
return {
|
|
213
218
|
state: 'counted',
|
|
214
219
|
window: { days, from: dayOf(now - (days - 1) * DAY), to: dayOf(now) },
|
|
215
220
|
projects: projects.length,
|
|
216
|
-
agents: agentOutcomes({ projects, globalDir, days, now, roster }),
|
|
221
|
+
agents: agentOutcomes({ projects, globalDir, days, now, roster, projectScope }),
|
|
217
222
|
findings,
|
|
218
223
|
};
|
|
219
224
|
}
|