great-cto 2.96.0 → 2.97.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 +5 -1
- package/board/packages/board/lib/data-readers.mjs +53 -8
- package/board/packages/board/lib/docs.mjs +39 -0
- package/board/packages/board/lib/routes.mjs +107 -0
- package/board/packages/board/public/index.html +194 -4
- package/board/scripts/lib/freshness.mjs +175 -0
- package/board/scripts/lib/gate-tier.mjs +279 -0
- package/board/scripts/lib/pipeline-wake.mjs +120 -0
- package/board/scripts/lib/receipt.mjs +386 -0
- package/board/scripts/lib/stand-down.mjs +147 -0
- package/board/scripts/lib/system-map.mjs +206 -0
- package/dist/detect.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* system-map — what this project is made of, derived from the source.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists
|
|
6
|
+
* ---------------
|
|
7
|
+
* `docs/ARCHITECTURE.md` is a hand-drawn ASCII diagram. It was last touched
|
|
8
|
+
* three months ago and says "34 agents"; there are sixty-nine. Around it sit
|
|
9
|
+
* twenty architecture documents, ten ADRs and thirty-one plans, each about one
|
|
10
|
+
* feature — you can read all of them and still not know how the pieces fit.
|
|
11
|
+
*
|
|
12
|
+
* A drawn diagram is stale the week after it is drawn, and nothing tells you.
|
|
13
|
+
* This one is computed when you look at it, so the only way it can be wrong is
|
|
14
|
+
* if the code is.
|
|
15
|
+
*
|
|
16
|
+
* What it does NOT try to be
|
|
17
|
+
* --------------------------
|
|
18
|
+
* Not a file-level import graph. Several hundred modules drawn as a hairball is
|
|
19
|
+
* a picture nobody reads and nobody checks. The useful altitude is the group —
|
|
20
|
+
* agents, hooks, libraries, the board, the CLI, the contracts — and the edges
|
|
21
|
+
* between groups, which is roughly C4's container level.
|
|
22
|
+
*
|
|
23
|
+
* Not prose. It reports what is there and how it connects; what any of it is
|
|
24
|
+
* FOR belongs in a document a human wrote.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { readdirSync, readFileSync, statSync, existsSync } from 'node:fs';
|
|
28
|
+
import { join, relative, sep } from 'node:path';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The groups a great_cto project is built from, and what each one is.
|
|
32
|
+
*
|
|
33
|
+
* Ordered so the map reads top-down the way the system runs: contracts describe
|
|
34
|
+
* it, hooks fire during it, agents do the work, libraries are what they share,
|
|
35
|
+
* and the board and CLI are how a human reaches any of it.
|
|
36
|
+
*/
|
|
37
|
+
export const GROUPS = Object.freeze([
|
|
38
|
+
{ key: 'contracts', label: 'Contracts', dirs: ['shared'], ext: ['.toml'], what: 'the pipeline map, orchestrator rules' },
|
|
39
|
+
{ key: 'agents', label: 'Agents', dirs: ['agents'], ext: ['.md'], what: 'the specialists the pipeline dispatches' },
|
|
40
|
+
{ key: 'commands', label: 'Commands', dirs: ['commands'], ext: ['.md'], what: 'what a human can invoke directly' },
|
|
41
|
+
{ key: 'skills', label: 'Skills', dirs: ['skills'], ext: ['.md'], what: 'knowledge agents load on demand' },
|
|
42
|
+
{ key: 'hooks', label: 'Hooks', dirs: ['scripts/hooks'], ext: ['.mjs', '.sh', '.py'], what: 'what fires on session, tool and stop events' },
|
|
43
|
+
{ key: 'libs', label: 'Libraries', dirs: ['scripts/lib'], ext: ['.mjs'], what: 'the logic hooks and commands share' },
|
|
44
|
+
{ key: 'board', label: 'Board', dirs: ['packages/board'], ext: ['.mjs', '.html'], what: 'the admin view, zero runtime dependencies' },
|
|
45
|
+
{ key: 'cli', label: 'CLI', dirs: ['packages/cli/src'], ext: ['.ts', '.mjs'], what: 'the published npm package' },
|
|
46
|
+
{ key: 'evals', label: 'Evals', dirs: ['tests/eval'], ext: ['.md'], what: 'what each agent is measured against' },
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
// Generated or third-party trees, plus _shared: its contracts are counted by
|
|
50
|
+
// the groups that fetch them, and counting the directory twice inflated Agents.
|
|
51
|
+
const SKIP = new Set(['node_modules', 'dist', 'build', '.git', 'coverage', 'vendor', '_shared']);
|
|
52
|
+
|
|
53
|
+
function filesIn(root, dir, ext, depth = 0, out = []) {
|
|
54
|
+
if (depth > 3) return out;
|
|
55
|
+
let entries;
|
|
56
|
+
try { entries = readdirSync(join(root, dir), { withFileTypes: true }); } catch { return out; }
|
|
57
|
+
for (const e of entries) {
|
|
58
|
+
if (SKIP.has(e.name) || e.name.startsWith('.')) continue;
|
|
59
|
+
const rel = join(dir, e.name);
|
|
60
|
+
if (e.isDirectory()) { filesIn(root, rel, ext, depth + 1, out); continue; }
|
|
61
|
+
if (ext.some((x) => e.name.endsWith(x))) out.push(rel);
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Which group a repo-relative path belongs to, or null. */
|
|
67
|
+
export function groupOf(rel) {
|
|
68
|
+
const p = String(rel).split(sep).join('/');
|
|
69
|
+
for (const g of GROUPS) {
|
|
70
|
+
if (g.dirs.some((d) => p === d || p.startsWith(`${d}/`))) return g.key;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Edges between groups, counted from real `import ... from '...'` statements.
|
|
77
|
+
*
|
|
78
|
+
* Only relative imports: a dependency on `node:fs` says nothing about how this
|
|
79
|
+
* project is arranged, and a dependency on `scripts/lib/gate-state.mjs` says
|
|
80
|
+
* everything.
|
|
81
|
+
*/
|
|
82
|
+
export function importEdges(root, files) {
|
|
83
|
+
const counts = new Map();
|
|
84
|
+
for (const rel of files) {
|
|
85
|
+
const from = groupOf(rel);
|
|
86
|
+
if (!from) continue;
|
|
87
|
+
let src;
|
|
88
|
+
try { src = readFileSync(join(root, rel), 'utf8'); } catch { continue; }
|
|
89
|
+
for (const m of src.matchAll(/(?:^|\n)\s*import\s[^'"]*from\s+['"](\.[^'"]+)['"]/g)) {
|
|
90
|
+
const target = join(rel, '..', m[1]);
|
|
91
|
+
const to = groupOf(relative(root, join(root, target)));
|
|
92
|
+
if (!to || to === from) continue;
|
|
93
|
+
const key = `${from}→${to}`;
|
|
94
|
+
counts.set(key, (counts.get(key) || 0) + 1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return [...counts.entries()]
|
|
98
|
+
.map(([k, count]) => ({ from: k.split('→')[0], to: k.split('→')[1], count }))
|
|
99
|
+
.sort((a, b) => b.count - a.count);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The whole map: what exists, and how the parts reach each other. */
|
|
103
|
+
export function systemMap(root = process.cwd()) {
|
|
104
|
+
const nodes = [];
|
|
105
|
+
const allFiles = [];
|
|
106
|
+
for (const g of GROUPS) {
|
|
107
|
+
const files = g.dirs.flatMap((d) => filesIn(root, d, g.ext));
|
|
108
|
+
allFiles.push(...files);
|
|
109
|
+
if (files.length) nodes.push({ key: g.key, label: g.label, what: g.what, count: files.length });
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
root,
|
|
113
|
+
generatedAt: new Date().toISOString(),
|
|
114
|
+
nodes,
|
|
115
|
+
edges: importEdges(root, allFiles.filter((f) => /\.(mjs|ts)$/.test(f))),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The map as Mermaid.
|
|
121
|
+
*
|
|
122
|
+
* Every count is in the label, so a stale screenshot of this diagram is
|
|
123
|
+
* self-evidently stale — the number is the part that dates it. `ARCHITECTURE.md`
|
|
124
|
+
* said 34 agents for three months because nothing in the picture disagreed with
|
|
125
|
+
* itself.
|
|
126
|
+
*/
|
|
127
|
+
export function toMermaid(map) {
|
|
128
|
+
const lines = ['flowchart TD'];
|
|
129
|
+
for (const n of map.nodes) {
|
|
130
|
+
lines.push(` ${n.key}["${n.label}<br/><small>${n.count} files</small>"]`);
|
|
131
|
+
}
|
|
132
|
+
for (const e of map.edges) {
|
|
133
|
+
lines.push(` ${e.from} -->|${e.count}| ${e.to}`);
|
|
134
|
+
}
|
|
135
|
+
return lines.join('\n');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The pipeline as a diagram — the picture the twenty architecture documents do
|
|
140
|
+
* not add up to.
|
|
141
|
+
*
|
|
142
|
+
* The import graph above says how the code is arranged; this says how the system
|
|
143
|
+
* RUNS, which is the question someone opening the board actually has. It is
|
|
144
|
+
* drawn from `shared/pipeline.toml`, the same file the dispatcher acts on, so a
|
|
145
|
+
* diagram that disagrees with the pipeline is impossible rather than merely
|
|
146
|
+
* unlikely.
|
|
147
|
+
*
|
|
148
|
+
* Gates are drawn because they are where a human stands in the flow, and a map
|
|
149
|
+
* of an automated pipeline that hides its stopping points describes something
|
|
150
|
+
* other than what runs.
|
|
151
|
+
*/
|
|
152
|
+
export function pipelineMermaid(tomlText) {
|
|
153
|
+
const transitions = {};
|
|
154
|
+
let cur = null;
|
|
155
|
+
for (const raw of String(tomlText || '').split('\n')) {
|
|
156
|
+
const line = raw.trim();
|
|
157
|
+
if (line.startsWith('#') || !line) continue;
|
|
158
|
+
const sec = line.match(/^\[transitions\.([\w-]+)\]$/);
|
|
159
|
+
if (sec) { cur = transitions[sec[1]] = {}; continue; }
|
|
160
|
+
if (line.startsWith('[')) { cur = null; continue; }
|
|
161
|
+
if (!cur) continue;
|
|
162
|
+
const kv = line.match(/^([\w-]+)\s*=\s*(.+)$/);
|
|
163
|
+
if (!kv) continue;
|
|
164
|
+
const [, k, v] = kv;
|
|
165
|
+
cur[k] = v.startsWith('[')
|
|
166
|
+
? v.replace(/^\[|\]$/g, '').split(',').map((x) => x.trim().replace(/^"|"$/g, '')).filter(Boolean)
|
|
167
|
+
: v.trim().replace(/^"|"$/g, '');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const id = (a) => a.replace(/[^\w]/g, '_');
|
|
171
|
+
const lines = ['flowchart TD'];
|
|
172
|
+
const seen = new Set();
|
|
173
|
+
for (const [agent, rule] of Object.entries(transitions)) {
|
|
174
|
+
if (!seen.has(agent)) { lines.push(` ${id(agent)}["${agent}"]`); seen.add(agent); }
|
|
175
|
+
for (const next of rule.next || []) {
|
|
176
|
+
if (!seen.has(next)) { lines.push(` ${id(next)}["${next}"]`); seen.add(next); }
|
|
177
|
+
const gates = Array.isArray(rule.gate) ? rule.gate : rule.gate ? [rule.gate] : [];
|
|
178
|
+
lines.push(gates.length
|
|
179
|
+
? ` ${id(agent)} -->|${gates.join(' + ')}| ${id(next)}`
|
|
180
|
+
: ` ${id(agent)} --> ${id(next)}`);
|
|
181
|
+
}
|
|
182
|
+
for (const j of rule.join || []) {
|
|
183
|
+
if (!seen.has(j)) { lines.push(` ${id(j)}["${j}"]`); seen.add(j); }
|
|
184
|
+
lines.push(` ${id(j)} -.->|join| ${id(agent)}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return lines.join('\n');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── CLI ─────────────────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
193
|
+
const map = systemMap(process.cwd());
|
|
194
|
+
if (process.argv.includes('--json')) {
|
|
195
|
+
console.log(JSON.stringify(map, null, 2));
|
|
196
|
+
} else if (process.argv.includes('--pipeline')) {
|
|
197
|
+
console.log(pipelineMermaid(readFileSync(join(process.cwd(), 'shared', 'pipeline.toml'), 'utf8')));
|
|
198
|
+
} else if (process.argv.includes('--mermaid')) {
|
|
199
|
+
console.log(toMermaid(map));
|
|
200
|
+
} else {
|
|
201
|
+
console.log(`${map.nodes.length} groups, ${map.edges.length} edges — generated ${map.generatedAt}\n`);
|
|
202
|
+
for (const n of map.nodes) console.log(` ${n.label.padEnd(12)} ${String(n.count).padStart(4)} ${n.what}`);
|
|
203
|
+
console.log('');
|
|
204
|
+
for (const e of map.edges.slice(0, 12)) console.log(` ${e.from} → ${e.to} (${e.count})`);
|
|
205
|
+
}
|
|
206
|
+
}
|
package/dist/detect.js
CHANGED
|
@@ -1341,6 +1341,7 @@ function mineInfraKeywords(dir, pkg) {
|
|
|
1341
1341
|
function collectTf(d, depth) {
|
|
1342
1342
|
if (depth > 4)
|
|
1343
1343
|
return;
|
|
1344
|
+
// Dependency and build output — scanning them counts other people's code.
|
|
1344
1345
|
const SKIP = new Set(["node_modules", ".git", "dist", ".terraform"]);
|
|
1345
1346
|
try {
|
|
1346
1347
|
for (const e of readdirSync(d)) {
|
package/package.json
CHANGED