great-cto 3.77.0 → 3.78.1
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/config.mjs +27 -6
- package/board/packages/board/lib/util.mjs +19 -1
- package/board/packages/board/public/assets/admin-shell.js +15 -8
- package/board/packages/board/public/index.html +693 -364
- package/board/packages/board/server.mjs +29 -8
- package/dist/main.js +5 -3
- package/package.json +1 -1
|
@@ -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 73 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.
|
|
4
|
+
"version": "3.78.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Alexander Velikiy",
|
|
7
7
|
"url": "https://hashnode.com/@Greatcto"
|
|
@@ -22,12 +22,33 @@ const BUILD_VERSION = (() => {
|
|
|
22
22
|
}
|
|
23
23
|
return 'unknown';
|
|
24
24
|
})();
|
|
25
|
-
// Bind host. Default loopback.
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
// Bind host. Default loopback. `--host`, then GREAT_CTO_BOARD_HOST, then the legacy
|
|
26
|
+
// GREAT_CTO_HOST — but only when that one holds an address. The same variable name
|
|
27
|
+
// carries the AGENT host (`codex`, `claude-code`) for hook children
|
|
28
|
+
// (scripts/hooks/codex-adapter.mjs), and a board started from such a child used to
|
|
29
|
+
// try to bind the name "codex".
|
|
30
|
+
const LOOPBACK_BINDS = new Set(['127.0.0.1', 'localhost', '::1', '[::1]']);
|
|
31
|
+
const looksLikeAddress = (v) => v === 'localhost' || /^[0-9.]+$/.test(v) || v.includes(':');
|
|
32
|
+
export function bindHostFrom(argv = process.argv, env = process.env) {
|
|
33
|
+
const i = argv.indexOf('--host');
|
|
34
|
+
if (i > -1 && argv[i + 1]) return String(argv[i + 1]);
|
|
35
|
+
if (env.GREAT_CTO_BOARD_HOST) return String(env.GREAT_CTO_BOARD_HOST);
|
|
36
|
+
const legacy = String(env.GREAT_CTO_HOST || '');
|
|
37
|
+
return looksLikeAddress(legacy) ? legacy : '127.0.0.1';
|
|
38
|
+
}
|
|
39
|
+
// The board has no login. Beyond loopback, anyone who can reach the port can read
|
|
40
|
+
// every project and approve gates, so the bind is refused unless the operator states
|
|
41
|
+
// that authentication sits in front of it (a reverse proxy, a tunnel with access
|
|
42
|
+
// control). Returns the reason to refuse, or null.
|
|
43
|
+
export function bindRefusal(host = HOST, env = process.env, argv = process.argv) {
|
|
44
|
+
if (LOOPBACK_BINDS.has(String(host).toLowerCase())) return null;
|
|
45
|
+
if (env.GREAT_CTO_BOARD_BEHIND_AUTH === '1' || argv.includes('--behind-auth')) return null;
|
|
46
|
+
return `refusing to bind ${host}: the board has no authentication of its own, and beyond loopback ` +
|
|
47
|
+
`anyone who reaches this port can read every project and approve gates. ` +
|
|
48
|
+
`Put authentication in front of it (a reverse proxy or a tunnel with access control), ` +
|
|
49
|
+
`then start it with GREAT_CTO_BOARD_BEHIND_AUTH=1 (or --behind-auth).`;
|
|
50
|
+
}
|
|
51
|
+
const HOST = bindHostFrom();
|
|
31
52
|
// Shared dedicated namespace with the CLI; unset/empty keeps the operator default.
|
|
32
53
|
const GREAT_CTO_DIR = stateHome();
|
|
33
54
|
const SHARE_STATE_FILE = path.join(GREAT_CTO_DIR, 'board-share.json');
|
|
@@ -51,6 +51,24 @@ function originAllowed(req) {
|
|
|
51
51
|
return allowedHostList().includes(u.host.toLowerCase());
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// The value for Access-Control-Allow-Origin: the request's Origin when it is one of
|
|
55
|
+
// the board's own, otherwise none. It was `*` for every response, which let any page
|
|
56
|
+
// in the same browser read the API.
|
|
57
|
+
function corsOriginFor(req) {
|
|
58
|
+
const o = req.headers.origin;
|
|
59
|
+
if (!o) return null;
|
|
60
|
+
return originAllowed({ headers: { origin: o } }) ? o : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Is this a read coming from another site? Reads carry tasks, verdicts and costs, and
|
|
64
|
+
// a GET reaches the server whether or not the page may read the answer. A foreign
|
|
65
|
+
// Origin or Referer says so; Sec-Fetch-Site says so even when both are stripped
|
|
66
|
+
// (an <img>, a no-referrer link). No header at all is a tool, not a browser.
|
|
67
|
+
function crossSiteRead(req) {
|
|
68
|
+
if (String(req.headers['sec-fetch-site'] || '').toLowerCase() === 'cross-site') return true;
|
|
69
|
+
return !originAllowed(req);
|
|
70
|
+
}
|
|
71
|
+
|
|
54
72
|
// Which surface an alert belongs to. Operate-side events (autopilot runtime: dead-letters,
|
|
55
73
|
// connector health, case SLA, gate/safe-mode pushes) are the operator console's concern and
|
|
56
74
|
// must not clutter the builder board's notifications — the two surfaces are separated.
|
|
@@ -121,4 +139,4 @@ function isInsideDir(base, target) {
|
|
|
121
139
|
return resolvedTarget === resolvedBase || resolvedTarget.startsWith(resolvedBase + path.sep);
|
|
122
140
|
}
|
|
123
141
|
|
|
124
|
-
export { csvCell, originAllowed, hostAllowed, allowedHostList, parseAllowedHosts, eventSurface, readFileSafe, readSafe, parseSafe, isInsideDir };
|
|
142
|
+
export { csvCell, originAllowed, corsOriginFor, crossSiteRead, hostAllowed, allowedHostList, parseAllowedHosts, eventSurface, readFileSafe, readSafe, parseSafe, isInsideDir };
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/* Task-first projection. Existing controller commands and authorization remain authoritative. */
|
|
2
2
|
(() => {
|
|
3
3
|
const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
4
|
-
const aliases = { docs:'knowledge', usage:'insights/consumption', ledger:'insights/ledger', budgets:'insights/ledger', dashboard:'insights/ledger', fleet:'settings/executors', agents:'settings/executors', harness:'settings/runtime', skills:'settings/capabilities', history:'work/completed', inbox:'decisions', notifications:'settings/general', share:'settings/general' };
|
|
4
|
+
const aliases = { docs:'knowledge', usage:'insights/consumption', ledger:'insights/ledger', budgets:'insights/ledger', dashboard:'insights/ledger', fleet:'settings/executors', agents:'settings/executors', harness:'settings/runtime', skills:'settings/capabilities', history:'work/completed', inbox:'home', decisions:'home', cost:'insights/ledger', insights:'insights/ledger', notifications:'settings/general', share:'settings/general' };
|
|
5
5
|
function route(input) {
|
|
6
6
|
const taskRoute = window.GctoWork?.parseRoute?.(input);
|
|
7
7
|
if (taskRoute) return { canonical:taskRoute.canonical, panel:'work', primary:'work', workRoute:true };
|
|
8
8
|
if (/^(?:agents|fleet)\/[a-z0-9-]+$/i.test(input)) return {canonical:input,panel:'agents',primary:'settings'};
|
|
9
9
|
const canonical = aliases[input] || input;
|
|
10
|
-
const panels = { work:'work','work/completed':'history',
|
|
10
|
+
const panels = { home:'inbox', work:'work','work/completed':'history',knowledge:'knowledge',insights:'admin-outcomes','insights/outcomes':'admin-outcomes','insights/consumption':'usage','insights/ledger':'budgets',settings:'agents','settings/executors':'agents','settings/runtime':'harness','settings/capabilities':'skills','settings/general':'settings' };
|
|
11
11
|
if (!panels[canonical]) return null;
|
|
12
12
|
return { canonical, panel:panels[canonical], primary:canonical.split('/')[0] };
|
|
13
13
|
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
const completed = tasks.filter(e => e.taskOutcome?.state === 'completed').length;
|
|
17
17
|
return {tasks:tasks.length,completed,unknown:tasks.length-completed,acceptanceRate:null,costPerAcceptedTask:null};
|
|
18
18
|
}
|
|
19
|
-
let generation=0, selectedDoc=null, selectedRoute='
|
|
19
|
+
let generation=0, selectedDoc=null, selectedRoute='home', request=0, indexRequest=0;
|
|
20
20
|
const scope = () => new URLSearchParams(location.search).get('project') || '';
|
|
21
21
|
const scoped = path => path+(path.includes('?')?'&':'?')+'project='+encodeURIComponent(scope());
|
|
22
22
|
const panel = id => document.getElementById(id);
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
el.innerHTML=`<p class="work-meta">Observed ${esc(data.observedAt)} · ${data.health==='current'?'Sources read':'Sources incomplete'} · Recorded task metadata, not a benchmark</p>
|
|
39
39
|
<div class="admin-metrics"><section><h2>${d.tasks}</h2><p>Tracked tasks</p><small>Legacy issues and standalone runs excluded</small></section><section><h2>${d.completed}</h2><p>Explicitly completed outcomes</p><small>Recorded completion, not an acceptance-rate estimate</small></section><section><h2>${d.unknown}</h2><p>No completed outcome recorded</p><small>Includes in-progress tasks; not a failure count</small></section><section><h2>Unavailable</h2><p>Cost per accepted task</p><small>Complete task-bound actual spend is not available</small></section></div>
|
|
40
40
|
<div class="admin-notice">Acceptance rate, time to acceptance and severe regressions are not measured by this projection. Role verdicts, closed issues and successful processes cannot supply those numbers.</div>
|
|
41
|
-
<h2 class="admin-section-title">Recorded work</h2><div class="admin-table-scroll"><table class="admin-table"><thead><tr><th>Task</th><th>Host</th><th>State</th><th>Outcome</th></tr></thead><tbody>${(data.entries||[]).filter(e=>e.kind==='task').slice(0,100).map(e=>`<tr><td>${esc(e.title)}</td><td>${esc(e.host||'Not linked')}</td><td>${esc(e.phase||'Unknown')}</td><td>${esc(e.taskOutcome?.state||'Not recorded')}</td></tr>`).join('')||'<tr><td colspan="4">No tracked task outcomes in the readable records.</td></tr>'}</tbody></table></div><p class="work-meta">All available tracked tasks, not a time-window cohort. Host quality comparisons require matched tasks, budgets and acceptance criteria.</p>`;
|
|
41
|
+
<h2 class="admin-section-title" id="admin-outcomes-work">Recorded work</h2><div class="admin-table-scroll" tabindex="0" role="region" aria-labelledby="admin-outcomes-work"><table class="admin-table"><thead><tr><th>Task</th><th>Host</th><th>State</th><th>Outcome</th></tr></thead><tbody>${(data.entries||[]).filter(e=>e.kind==='task').slice(0,100).map(e=>`<tr><td>${esc(e.title)}</td><td>${esc(e.host||'Not linked')}</td><td>${esc(e.phase||'Unknown')}</td><td>${esc(e.taskOutcome?.state||'Not recorded')}</td></tr>`).join('')||'<tr><td colspan="4">No tracked task outcomes in the readable records.</td></tr>'}</tbody></table></div><p class="work-meta">All available tracked tasks, not a time-window cohort. Host quality comparisons require matched tasks, budgets and acceptance criteria.</p>`;
|
|
42
42
|
} catch(error) { if(current(seq,project)&&ticket===request) el.textContent='Outcomes could not be read: '+error.message; }
|
|
43
43
|
}
|
|
44
44
|
async function loadKnowledge() {
|
|
@@ -102,7 +102,14 @@
|
|
|
102
102
|
function setup() {
|
|
103
103
|
const nav=panel('nav-tablist'), existing={};
|
|
104
104
|
document.querySelectorAll('.nav-item[data-tab]').forEach(n=>existing[n.dataset.tab]=n.querySelector('svg')?.outerHTML||'');
|
|
105
|
-
|
|
105
|
+
// Home · Work · Cost · Knowledge · Settings (UX audit 2026-11, approved mockup).
|
|
106
|
+
// Route keys are unchanged where a screen is unchanged: Cost is still the
|
|
107
|
+
// `insights` route, Home is the panel Decisions used; `#/decisions` lands on Home.
|
|
108
|
+
const NAV_LABEL={home:'Home',work:'Work',insights:'Cost',knowledge:'Knowledge',settings:'Settings'};
|
|
109
|
+
// Cost is one screen (F4): `insights` opens it; Token detail (`insights/consumption`)
|
|
110
|
+
// and Outcomes (`insights/outcomes`, hidden until it has data) stay reachable by route.
|
|
111
|
+
const NAV_ICON={home:'decisions',work:'work',insights:'ledger',knowledge:'skills',settings:'harness'};
|
|
112
|
+
nav.innerHTML=['home','work','insights','knowledge','settings'].map(key=>`<button type="button" class="nav-item${key==='home'?' active':''}" role="tab" aria-selected="${key==='home'}" data-tab="${key}" aria-label="${NAV_LABEL[key]}" data-nav-label="${NAV_LABEL[key]}" tabindex="0" onkeydown="navTabKeydown(event,this)">${existing[NAV_ICON[key]]||''}<span class="nav-label">${NAV_LABEL[key]}</span>${key==='home'?'<span class="count" id="nav-inbox-count" title="items that need you in this project">0</span>':''}</button>`).join('');
|
|
106
113
|
nav.querySelectorAll('[data-tab]').forEach(n=>n.addEventListener('click',()=>window.switchTab(n.dataset.tab)));
|
|
107
114
|
const tools=panel('tools-nav');tools.hidden=true;
|
|
108
115
|
const workspace=document.querySelector('.workspace');
|
|
@@ -137,8 +144,8 @@
|
|
|
137
144
|
if (r.workRoute && typeof currentTab === 'string' && currentTab === 'work') { closeMenu(false); window.GctoWork.refresh(); }
|
|
138
145
|
else original(old); // Keep established loaders and mutation guards; task inspection adds no view POST.
|
|
139
146
|
document.querySelectorAll('.nav-item[data-tab]').forEach(n=>{const active=n.closest('#nav-tablist')&&n.dataset.tab===r.primary;n.classList.toggle('active',!!active);n.setAttribute('aria-selected',String(!!active));});
|
|
140
|
-
panel('crumb-here').textContent=r.primary[0].toUpperCase()+r.primary.slice(1);
|
|
141
|
-
const items=r.primary==='settings'?[['settings/executors','Executors'],['settings/runtime','Runtime'],['settings/capabilities','Capabilities'],['settings/general','General']]:
|
|
147
|
+
panel('crumb-here').textContent=({home:'Home',insights:'Cost'})[r.primary]||r.primary[0].toUpperCase()+r.primary.slice(1);
|
|
148
|
+
const items=r.primary==='settings'?[['settings/executors','Executors'],['settings/runtime','Runtime'],['settings/capabilities','Capabilities'],['settings/general','General']]:[];
|
|
142
149
|
bar.hidden=!items.length;bar.innerHTML=items.map(([key,label])=>`<button type="button" class="admin-subtab" data-route="${key}" aria-pressed="${r.canonical===key||(r.canonical===r.primary&&key===items[0][0])}">${label}</button>`).join('');
|
|
143
150
|
bar.querySelectorAll('[data-route]').forEach(b=>b.onclick=()=>window.switchTab(b.dataset.route));
|
|
144
151
|
history.replaceState(history.state,'','#/'+r.canonical);
|
|
@@ -149,7 +156,7 @@
|
|
|
149
156
|
const composer=panel('work-compose');
|
|
150
157
|
const disclosure=document.createElement('details');disclosure.className='admin-compose';const summary=document.createElement('summary');summary.textContent='New task · prepare launch command';disclosure.append(summary);composer.before(disclosure);disclosure.append(composer);
|
|
151
158
|
composer.querySelector('button[type="submit"]').textContent='Copy launch command';
|
|
152
|
-
window.switchTab((location.hash||'#/
|
|
159
|
+
window.switchTab((location.hash||'#/home').replace(/^#\/?/,''));
|
|
153
160
|
}
|
|
154
161
|
window.GctoAdmin={route,outcomes,reset,openDocument,setWorkView,navigateWork};
|
|
155
162
|
if(typeof document!=='undefined') setup();
|