great-cto 2.76.0 → 2.77.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.
Files changed (34) hide show
  1. package/board/.claude-plugin/plugin.json +256 -0
  2. package/board/packages/board/lib/alerts.mjs +413 -0
  3. package/board/packages/board/lib/beads.mjs +233 -0
  4. package/board/packages/board/lib/config.mjs +45 -0
  5. package/board/packages/board/lib/data-readers.mjs +340 -0
  6. package/board/packages/board/lib/fleet.mjs +363 -0
  7. package/board/packages/board/lib/metrics.mjs +282 -0
  8. package/board/packages/board/lib/notifications.mjs +50 -0
  9. package/board/packages/board/lib/projects.mjs +256 -0
  10. package/board/packages/board/lib/routes.mjs +1036 -0
  11. package/board/packages/board/lib/share.mjs +143 -0
  12. package/board/packages/board/lib/sse.mjs +20 -0
  13. package/board/packages/board/lib/state.mjs +31 -0
  14. package/board/packages/board/lib/util.mjs +36 -0
  15. package/board/packages/board/lib/verdicts.mjs +168 -0
  16. package/board/packages/board/lib/watchers.mjs +87 -0
  17. package/board/packages/board/mcp-server.mjs +310 -0
  18. package/board/packages/board/public/assets/apple-touch-icon.png +0 -0
  19. package/board/packages/board/public/assets/favicon-16.png +0 -0
  20. package/board/packages/board/public/assets/favicon-32.png +0 -0
  21. package/board/packages/board/public/assets/favicon.ico +0 -0
  22. package/board/packages/board/public/assets/favicon.svg +8 -0
  23. package/board/packages/board/public/index.html +5452 -0
  24. package/board/packages/board/public/share.html +1190 -0
  25. package/board/packages/board/public/sw.js +79 -0
  26. package/board/packages/board/push-adapter.mjs +222 -0
  27. package/board/packages/board/server.mjs +133 -0
  28. package/board/packages/cli/dist/archetypes.js +1461 -0
  29. package/board/scripts/lib/change-tier.mjs +119 -0
  30. package/board/scripts/lib/gate-plan.mjs +119 -0
  31. package/board/scripts/lib/judge-model.mjs +42 -0
  32. package/dist/board-path.js +47 -0
  33. package/dist/main.js +3 -31
  34. package/package.json +3 -1
@@ -0,0 +1,50 @@
1
+ import fs from 'fs';
2
+ import crypto from 'node:crypto';
3
+ import { GREAT_CTO_DIR, NOTIF_HISTORY_FILE } from './config.mjs';
4
+ import { notifHistory, MAX_NOTIF_HISTORY } from './state.mjs';
5
+ import { eventSurface } from './util.mjs';
6
+ import { broadcast } from './sse.mjs';
7
+
8
+ function loadNotifHistory() {
9
+ try {
10
+ if (fs.existsSync(NOTIF_HISTORY_FILE)) {
11
+ const parsed = JSON.parse(fs.readFileSync(NOTIF_HISTORY_FILE, 'utf8'));
12
+ if (Array.isArray(parsed)) { notifHistory.length = 0; notifHistory.push(...parsed); }
13
+ }
14
+ } catch { /* start fresh on corrupt file */ }
15
+ }
16
+
17
+ function saveNotifHistory() {
18
+ try {
19
+ fs.mkdirSync(GREAT_CTO_DIR, { recursive: true });
20
+ fs.writeFileSync(NOTIF_HISTORY_FILE, JSON.stringify(notifHistory, null, 2));
21
+ } catch { /* best-effort */ }
22
+ }
23
+
24
+ /**
25
+ * Record a notification, broadcast via SSE, and persist.
26
+ * Called alongside fireEmailAlert / firePushAlert at every trigger point.
27
+ */
28
+ function addNotification(event, payload) {
29
+ const notif = {
30
+ id: crypto.randomUUID(),
31
+ event,
32
+ surface: eventSurface(event),
33
+ title: payload.title,
34
+ body: payload.body,
35
+ level: payload.level || 'info',
36
+ project: payload.project || '',
37
+ ts: new Date().toISOString(),
38
+ read: false,
39
+ };
40
+ notifHistory.unshift(notif);
41
+ if (notifHistory.length > MAX_NOTIF_HISTORY) notifHistory.length = MAX_NOTIF_HISTORY;
42
+ broadcast('notification', notif);
43
+ saveNotifHistory();
44
+ return notif;
45
+ }
46
+
47
+ // Load history at server start
48
+ loadNotifHistory();
49
+
50
+ export { loadNotifHistory, saveNotifHistory, addNotification };
@@ -0,0 +1,256 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { spawnSync } from 'child_process';
5
+ import { planGates } from '../../../scripts/lib/gate-plan.mjs';
6
+ import { GREAT_CTO_DIR, PROJECTS_FILE } from './config.mjs';
7
+
8
+ // ── Project registry ───────────────────────────────────────────────────────────
9
+ function readProjectsRegistry() {
10
+ try { if (fs.existsSync(PROJECTS_FILE)) return JSON.parse(fs.readFileSync(PROJECTS_FILE, 'utf8')); }
11
+ catch {}
12
+ return { projects: [] };
13
+ }
14
+ function writeProjectsRegistry(reg) {
15
+ fs.mkdirSync(GREAT_CTO_DIR, { recursive: true });
16
+ fs.writeFileSync(PROJECTS_FILE, JSON.stringify(reg, null, 2));
17
+ }
18
+ // Map legacy / informal archetype names to canonical archetype slugs.
19
+ // This lets the board correctly badge old PROJECT.md files (mobile, saas-platform,
20
+ // ai-agent, etc.) without forcing users to rewrite them.
21
+ const ARCHETYPE_ALIASES = {
22
+ 'mobile': 'mobile-app',
23
+ 'mobile-android': 'mobile-app',
24
+ 'mobile-ios': 'mobile-app',
25
+ 'saas-platform': 'enterprise-saas',
26
+ 'saas': 'enterprise-saas',
27
+ 'ai-agent': 'agent-product',
28
+ 'agent': 'agent-product',
29
+ 'rag-system': 'ai-system',
30
+ 'llm-app': 'ai-system',
31
+ 'trading-system': 'fintech',
32
+ 'financial-platform': 'fintech',
33
+ 'neobroker': 'fintech',
34
+ 'neobank': 'fintech',
35
+ 'ml-platform': 'mlops',
36
+ 'ml-training': 'mlops',
37
+ 'ml-serving': 'mlops',
38
+ 'web-fullstack': 'web-service',
39
+ 'web-app': 'web-service',
40
+ 'api': 'web-service',
41
+ 'backend': 'web-service',
42
+ 'cli': 'cli-tool',
43
+ 'sdk': 'library',
44
+ 'npm-library': 'library',
45
+ };
46
+
47
+ function normalizeArchetype(raw) {
48
+ if (!raw) return '';
49
+ // Strip backticks, quotes, surrounding whitespace
50
+ const clean = raw.replace(/[`'"]/g, '').trim().toLowerCase();
51
+ // Take first token if comma/plus-separated (e.g. "trading-system + financial-platform")
52
+ const first = clean.split(/[,+]/)[0].trim();
53
+ return ARCHETYPE_ALIASES[first] || first;
54
+ }
55
+
56
+ function extractArchetype(text) {
57
+ // 1. Canonical: `archetype: foo`
58
+ let m = text.match(/^archetype:\s*(.+)$/m);
59
+ if (m) return normalizeArchetype(m[1]);
60
+ // 2. Legacy yaml-like: `primary: foo`
61
+ m = text.match(/^primary:\s*(.+)$/m);
62
+ if (m) return normalizeArchetype(m[1]);
63
+ // 3. Markdown list (bullet): `- Primary: \`foo\`` or `- primary: foo + bar`
64
+ m = text.match(/^[-*]\s*Primary:\s*(.+)$/im);
65
+ if (m) return normalizeArchetype(m[1]);
66
+ return '';
67
+ }
68
+
69
+ function readProjectMd(dir) {
70
+ const p = path.join(dir, '.great_cto', 'PROJECT.md');
71
+ if (!fs.existsSync(p)) return null;
72
+ const text = fs.readFileSync(p, 'utf8');
73
+ const get = k => (text.match(new RegExp(`^${k}:\\s*(.+)$`, 'm')) || [])[1]?.trim() || '';
74
+ return {
75
+ slug: get('project') || get('name') || path.basename(dir),
76
+ archetype: extractArchetype(text) || 'unknown',
77
+ description: get('description') || '',
78
+ path: dir,
79
+ added_at: new Date().toISOString(),
80
+ };
81
+ }
82
+ // change_tier for a project's working-tree diff (gate-tiering, ADR-003/004): which
83
+ // human gates open for the current change + which judge model runs. Pure planGates()
84
+ // over the project's archetype/size + `git diff --name-only`.
85
+ function getChangeTier(dir) {
86
+ try {
87
+ const meta = readProjectMd(dir);
88
+ if (!meta) return { tier: null, error: 'no PROJECT.md' };
89
+ const pm = fs.readFileSync(path.join(dir, '.great_cto', 'PROJECT.md'), 'utf8');
90
+ const size = (pm.match(/^project_size:\s*(\S+)/m) || [])[1] || 'medium';
91
+ const out = spawnSync('git', ['diff', '--name-only', 'HEAD'], { cwd: dir, encoding: 'utf8', timeout: 5000 });
92
+ const changedFiles = String(out.stdout || '').split('\n').map(s => s.trim()).filter(Boolean);
93
+ return planGates({ archetype: meta.archetype, size, changedFiles });
94
+ } catch (e) {
95
+ return { tier: 'T2', error: e.message }; // fail-safe: unknown → full gates
96
+ }
97
+ }
98
+ function autoRegisterProject(dir) {
99
+ const meta = readProjectMd(dir);
100
+ if (!meta) return null;
101
+ const reg = readProjectsRegistry();
102
+ if (!reg.projects.find(p => p.path === meta.path)) {
103
+ reg.projects.push(meta);
104
+ writeProjectsRegistry(reg);
105
+ }
106
+ return meta;
107
+ }
108
+
109
+ // Discover great_cto projects across common dev folders + Claude Code's known
110
+ // project list. Runs at server startup AND every /api/projects request, so any
111
+ // project that ran /audit or /start (which writes .great_cto/PROJECT.md) gets
112
+ // auto-registered without the user having to do anything.
113
+ // Fully async — never blocks the event loop.
114
+ async function discoverProjects() {
115
+ const fsAsync = fs.promises;
116
+ const HOME = os.homedir();
117
+ const seen = new Set();
118
+ const found = [];
119
+
120
+ async function scanDir(dir, depth) {
121
+ if (depth < 0 || seen.has(dir)) return;
122
+ seen.add(dir);
123
+ try {
124
+ // Check the dir itself first
125
+ try {
126
+ await fsAsync.access(path.join(dir, '.great_cto', 'PROJECT.md'));
127
+ found.push(dir);
128
+ return; // don't descend into a registered project
129
+ } catch {}
130
+ if (depth === 0) return;
131
+ // Scan children (skip dotfiles + heavyweight dirs)
132
+ const entries = await fsAsync.readdir(dir, { withFileTypes: true });
133
+ for (const e of entries) {
134
+ if (!e.isDirectory()) continue;
135
+ const name = e.name;
136
+ if (name.startsWith('.') || name === 'node_modules' || name === 'dist' ||
137
+ name === 'build' || name === 'target' || name === 'venv' || name === '__pycache__') continue;
138
+ await scanDir(path.join(dir, name), depth - 1);
139
+ }
140
+ } catch {} // permission denied — skip silently
141
+ }
142
+
143
+ // 1) Common dev folders — top-level scan, 1-level deep
144
+ const roots = [
145
+ path.join(HOME, 'work'),
146
+ path.join(HOME, 'dev'),
147
+ path.join(HOME, 'development'),
148
+ path.join(HOME, 'code'),
149
+ path.join(HOME, 'projects'),
150
+ path.join(HOME, 'src'),
151
+ path.join(HOME, 'Documents', 'projects'),
152
+ HOME,
153
+ ];
154
+ for (const root of roots) {
155
+ try { await fsAsync.access(root); await scanDir(root, 1); } catch {}
156
+ }
157
+
158
+ // 2) Claude Code's known project list (~/.claude/projects/<encoded-path>/)
159
+ try {
160
+ const ccProj = path.join(HOME, '.claude', 'projects');
161
+ await fsAsync.access(ccProj);
162
+ const entries = await fsAsync.readdir(ccProj);
163
+ for (const dir of entries) {
164
+ // Claude encodes paths as -Users-foo-projects-bar — decode to /Users/foo/projects/bar
165
+ const decoded = '/' + dir.replace(/^-+/, '').replace(/-/g, '/');
166
+ try {
167
+ await fsAsync.access(path.join(decoded, '.great_cto', 'PROJECT.md'));
168
+ found.push(decoded);
169
+ } catch {}
170
+ }
171
+ } catch {}
172
+
173
+ // Auto-register everything found
174
+ for (const dir of found) autoRegisterProject(dir);
175
+ return found.length;
176
+ }
177
+
178
+ function listProjects() {
179
+ // Auto-register cwd if it has PROJECT.md (cheap)
180
+ autoRegisterProject(process.cwd());
181
+ const reg = readProjectsRegistry();
182
+ // Filter out projects whose paths no longer exist
183
+ reg.projects = reg.projects.filter(p => fs.existsSync(p.path));
184
+ // Re-read metadata in case archetype/description changed
185
+ // Enrich with last_activity (mtime of .beads/interactions.jsonl) so the UI
186
+ // can sort projects by recent activity instead of slug-alpha.
187
+ return reg.projects.map(p => {
188
+ const fresh = readProjectMd(p.path);
189
+ let lastActivity = null;
190
+ try {
191
+ const interactionsFile = path.join(p.path, '.beads', 'interactions.jsonl');
192
+ if (fs.existsSync(interactionsFile)) {
193
+ lastActivity = fs.statSync(interactionsFile).mtime.toISOString();
194
+ }
195
+ } catch {}
196
+ return { ...(fresh ? { ...p, ...fresh } : p), last_activity: lastActivity };
197
+ }).sort((a, b) => {
198
+ // Recent activity first; projects with no activity sink to the bottom (alphabetic)
199
+ const ax = a.last_activity || '';
200
+ const bx = b.last_activity || '';
201
+ if (ax && bx) return bx.localeCompare(ax);
202
+ if (ax) return -1;
203
+ if (bx) return 1;
204
+ return (a.slug || '').localeCompare(b.slug || '');
205
+ });
206
+ }
207
+ function resolveProjectCwd(slugOrPath) {
208
+ if (!slugOrPath) return process.cwd();
209
+ // If absolute path or starts with ~, use directly
210
+ if (slugOrPath.startsWith('/')) return slugOrPath;
211
+ if (slugOrPath.startsWith('~')) return slugOrPath.replace(/^~/, os.homedir());
212
+ // Else look up in registry by slug
213
+ const reg = readProjectsRegistry();
214
+ const found = reg.projects.find(p => p.slug === slugOrPath);
215
+ return found ? found.path : process.cwd();
216
+ }
217
+
218
+ /**
219
+ * Same as resolveProjectCwd but returns { cwd, resolved, fallback? } so
220
+ * callers know whether resolution was authoritative or fell back to the
221
+ * server's working directory. Used by HTTP handlers to set an explicit
222
+ * `X-Project-Fallback` response header (BH-5 fix, 2026-05-15).
223
+ *
224
+ * resolved values:
225
+ * 'cwd' — no project param passed; using server cwd as documented
226
+ * 'path' — absolute / tilde path passed and used directly
227
+ * 'slug' — slug found in registry
228
+ * 'fallback' — slug requested but NOT in registry; using cwd as fallback.
229
+ * Caller should warn the user (header + log).
230
+ */
231
+ function resolveProjectInfo(slugOrPath) {
232
+ if (!slugOrPath) return { cwd: process.cwd(), resolved: 'cwd' };
233
+ if (slugOrPath.startsWith('/')) return { cwd: slugOrPath, resolved: 'path' };
234
+ if (slugOrPath.startsWith('~')) {
235
+ return { cwd: slugOrPath.replace(/^~/, os.homedir()), resolved: 'path' };
236
+ }
237
+ const reg = readProjectsRegistry();
238
+ const found = reg.projects.find(p => p.slug === slugOrPath);
239
+ if (found) return { cwd: found.path, resolved: 'slug' };
240
+ return { cwd: process.cwd(), resolved: 'fallback', requested: slugOrPath };
241
+ }
242
+
243
+ export {
244
+ readProjectsRegistry,
245
+ writeProjectsRegistry,
246
+ ARCHETYPE_ALIASES,
247
+ normalizeArchetype,
248
+ extractArchetype,
249
+ readProjectMd,
250
+ getChangeTier,
251
+ autoRegisterProject,
252
+ discoverProjects,
253
+ listProjects,
254
+ resolveProjectCwd,
255
+ resolveProjectInfo,
256
+ };