great-cto 2.77.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,119 @@
1
+ // scripts/lib/change-tier.mjs — classify a change into a risk tier (T0/T1/T2).
2
+ //
3
+ // The build-side analog of the runtime volume/scope-aware escalation (great_cto-34g).
4
+ // Pairs with effectiveGates() in packages/cli/src/archetypes.ts: this module decides
5
+ // HOW DANGEROUS a change is; effectiveGates maps that tier to a human-gate set.
6
+ //
7
+ // T2 — irreversible / regulated / deploy-to-prod. A HARD FLOOR: an explicit
8
+ // label can never downgrade a change that trips a T2 trigger.
9
+ // T0 — maintenance: every touched file is non-behavioral (docs/tests/lockfiles).
10
+ // CI + green tests are the gate.
11
+ // T1 — everything else (default): a reversible source change.
12
+ //
13
+ // Plan: docs/plans/PLAN-2026-06-18-gate-tiering-reviewer-consolidation.md
14
+
15
+ export const TIER_RANK = { T0: 0, T1: 1, T2: 2 };
16
+
17
+ // Files that, on their own, never change external behavior. A change touching ONLY
18
+ // these is maintenance (T0). Anything else is "behavioral".
19
+ const NON_BEHAVIORAL = [
20
+ /\.md$/i,
21
+ /(^|\/)docs?\//i,
22
+ /(^|\/)tests?\//i,
23
+ /\.test\.[mc]?[jt]s$/i,
24
+ /(^|\/)__tests__\//,
25
+ /(^|\/)\.beads\//,
26
+ /(^|\/)CHANGELOG/i,
27
+ /\.(png|jpe?g|gif|svg|webp)$/i,
28
+ /(^|\/)package-lock\.json$/,
29
+ /(^|\/)\.gitignore$/,
30
+ ];
31
+
32
+ // Behavioral files whose mere presence makes the change irreversible/regulated (T2).
33
+ // Checked ONLY against behavioral files, so a docs/pricing.md never trips it.
34
+ const T2_PATHS = [
35
+ { re: /(^|\/)migrations?\//i, why: 'migration' },
36
+ { re: /(^|\/)_domains\.json$/i, why: 'domain-config' },
37
+ { re: /(^|\/)(auth|authentication)[\/.]/i, why: 'auth-surface' },
38
+ { re: /(^|\/)pricing[\/.]/i, why: 'pricing-surface' },
39
+ ];
40
+
41
+ const isNonBehavioral = (f) => NON_BEHAVIORAL.some((re) => re.test(f));
42
+
43
+ /** Parse an explicit `tier:tN` beads label → 'T0'|'T1'|'T2'|null (last one wins). */
44
+ function labelTier(labels) {
45
+ let found = null;
46
+ for (const l of labels || []) {
47
+ const m = /^tier:(t[012])$/i.exec(String(l).trim());
48
+ if (m) found = m[1].toUpperCase();
49
+ }
50
+ return found;
51
+ }
52
+
53
+ /**
54
+ * Classify a change.
55
+ * @param {object} signals
56
+ * @param {string[]} [signals.changedFiles] repo-relative paths
57
+ * @param {Array<{id?:string,hasWrite?:boolean,isNew?:boolean}>} [signals.connectors]
58
+ * @param {string|null} [signals.deployTarget] e.g. 'production'
59
+ * @param {string[]} [signals.labels] beads labels (may include `tier:tN`)
60
+ * @returns {{tier:'T0'|'T1'|'T2', reasons:string[], escalatedFromLabel:('T0'|'T1'|null)}}
61
+ */
62
+ export function classify(signals = {}) {
63
+ const changedFiles = Array.isArray(signals.changedFiles) ? signals.changedFiles : [];
64
+ const connectors = Array.isArray(signals.connectors) ? signals.connectors : [];
65
+ const deployTarget = signals.deployTarget ?? null;
66
+ const explicit = labelTier(signals.labels);
67
+
68
+ // ── Hard T2 triggers ────────────────────────────────────────────────────────
69
+ const hard = [];
70
+ const behavioral = changedFiles.filter((f) => !isNonBehavioral(f));
71
+ for (const f of behavioral) {
72
+ const hit = T2_PATHS.find((p) => p.re.test(f));
73
+ if (hit) hard.push(`${hit.why}:${f}`);
74
+ }
75
+ for (const c of connectors) {
76
+ if (c && c.isNew && c.hasWrite) hard.push(`connector:new-write:${c.id ?? '?'}`);
77
+ }
78
+ if (String(deployTarget).toLowerCase() === 'production') hard.push('deploy:production');
79
+
80
+ // Volume/scope escalation — the build-side analog of the runtime AI-firewall
81
+ // (great_cto-34g): a change touching many behavioral files has a large blast radius;
82
+ // even if each file is individually reversible, the bulk gets a human look (T2).
83
+ const bulkThreshold = Number(
84
+ signals.bulkThreshold ?? process.env.GREAT_CTO_BULK_THRESHOLD ?? 50,
85
+ );
86
+ if (Number.isFinite(bulkThreshold) && bulkThreshold > 0 && behavioral.length >= bulkThreshold) {
87
+ hard.push(`bulk:${behavioral.length}files`);
88
+ }
89
+
90
+ if (hard.length) {
91
+ // T2 floor: a lower explicit label is overridden — recorded for the audit log.
92
+ const escalatedFromLabel =
93
+ explicit && TIER_RANK[explicit] < TIER_RANK.T2 ? explicit : null;
94
+ return { tier: 'T2', reasons: hard, escalatedFromLabel };
95
+ }
96
+
97
+ // ── Auto baseline (no hard trigger) ───────────────────────────────────────────
98
+ // T0 only when there is at least one file and EVERY file is non-behavioral.
99
+ const baseAuto =
100
+ changedFiles.length > 0 && behavioral.length === 0 ? 'T0' : 'T1';
101
+
102
+ // Explicit label wins within the non-floored range (can up- or down-grade).
103
+ const tier = explicit ?? baseAuto;
104
+ const reasons = explicit
105
+ ? [`label:${explicit}`]
106
+ : [
107
+ baseAuto === 'T0'
108
+ ? `auto:T0:non-behavioral(${changedFiles.length})`
109
+ : `auto:T1:${behavioral.length ? `behavioral(${behavioral.length})` : 'default'}`,
110
+ ];
111
+ return { tier, reasons, escalatedFromLabel: null };
112
+ }
113
+
114
+ // CLI: node scripts/lib/change-tier.mjs file1 file2 … → prints the tier + reasons
115
+ // (a thin probe; the orchestrator imports classify() directly with richer signals).
116
+ if (import.meta.url === `file://${process.argv[1]}`) {
117
+ const r = classify({ changedFiles: process.argv.slice(2) });
118
+ process.stdout.write(`${r.tier} ${r.reasons.join(', ')}\n`);
119
+ }
@@ -0,0 +1,119 @@
1
+ // scripts/lib/gate-plan.mjs — compute the human gates that should open for THE
2
+ // CURRENT CHANGE, by composing the two pure pieces of the two-axis gate model:
3
+ //
4
+ // classify(diff) → change_tier (T0/T1/T2) [scripts/lib/change-tier.mjs]
5
+ // effectiveGates(arch,size,tier) → the gate set [packages/cli/src/archetypes.ts]
6
+ //
7
+ // This is the runtime entry point the orchestrator (or a human) calls at the start
8
+ // of a change: "given what I'm touching, which gates actually open?". The static
9
+ // FLOW.md gate menu (compileFlow → gatesFor) is unchanged — it lists what a project
10
+ // CAN gate; this decides what THIS change DOES gate.
11
+ //
12
+ // CLI:
13
+ // node scripts/lib/gate-plan.mjs classify the working-tree diff
14
+ // node scripts/lib/gate-plan.mjs --base main diff against a base ref
15
+ // node scripts/lib/gate-plan.mjs --deploy production
16
+ // node scripts/lib/gate-plan.mjs --label tier:t2 --json
17
+ //
18
+ // Plan: docs/plans/PLAN-2026-06-18-gate-tiering-reviewer-consolidation.md
19
+ // ADR: docs/adr/ADR-003-two-axis-gate-model.md
20
+
21
+ import { execFileSync } from 'node:child_process';
22
+ import { readFileSync } from 'node:fs';
23
+ import { fileURLToPath } from 'node:url';
24
+
25
+ import { classify } from './change-tier.mjs';
26
+ import { selectJudgeModel } from './judge-model.mjs';
27
+ // effectiveGates lives in the built CLI package (TS → dist).
28
+ import { effectiveGates } from '../../packages/cli/dist/archetypes.js';
29
+
30
+ /** Parse archetype + project_size out of a .great_cto/PROJECT.md body. */
31
+ export function parseProject(text) {
32
+ const get = (k) => {
33
+ const m = String(text).match(new RegExp(`^${k}:\\s*(\\S+)`, 'm'));
34
+ return m ? m[1] : null;
35
+ };
36
+ return {
37
+ archetype: get('archetype') ?? get('primary') ?? 'greenfield',
38
+ size: get('project_size') ?? get('size') ?? 'medium',
39
+ };
40
+ }
41
+
42
+ // Governance regime per gate (mech-gov R1/R2 vocabulary, borrow-santander TAKE 1):
43
+ // R2 = MECHANICALLY enforced — a script/CI produces the verdict and can hard-block
44
+ // (qa runs tests, security runs secret/CVE scans, ship gates on CI green).
45
+ // R1 = text-only judgment — a human/reviewer reads and approves the direction.
46
+ // R2 gates are the moat: an enforced control, not a vibe. Surfacing the split lets
47
+ // /gov-metrics measure how much of the pipeline is actually enforced vs prose.
48
+ const R2_GATES = new Set(['qa', 'security', 'ship']);
49
+ export function regimeOfGate(gate) { return R2_GATES.has(gate) ? 'R2' : 'R1'; }
50
+
51
+ /**
52
+ * Compose classify → effectiveGates for one change.
53
+ * @returns {{tier:string, reasons:string[], escalatedFromLabel:(string|null), gates:string[], gateRegimes:Object, r2Share:number, judge:Object}}
54
+ */
55
+ export function planGates({ archetype, size, changedFiles, connectors, deployTarget, labels }) {
56
+ const c = classify({ changedFiles, connectors, deployTarget, labels });
57
+ const gates = effectiveGates(archetype, size, c.tier);
58
+ // ADR-004: the per-change plan also says which judge model to use (cheap on T0/T1,
59
+ // frontier + human on T2). Same tier, two consequences — gates and judge.
60
+ const judge = selectJudgeModel(c.tier);
61
+ const gateRegimes = Object.fromEntries(gates.map((g) => [g, regimeOfGate(g)]));
62
+ const r2 = gates.filter((g) => regimeOfGate(g) === 'R2').length;
63
+ const r2Share = gates.length ? Math.round((100 * r2) / gates.length) : 0;
64
+ return { tier: c.tier, reasons: c.reasons, escalatedFromLabel: c.escalatedFromLabel, gates, gateRegimes, r2Share, judge };
65
+ }
66
+
67
+ // ── CLI ───────────────────────────────────────────────────────────────────────
68
+
69
+ function gitChangedFiles(base) {
70
+ try {
71
+ const args = base
72
+ ? ['diff', '--name-only', `${base}...HEAD`]
73
+ : ['diff', '--name-only', 'HEAD'];
74
+ const out = execFileSync('git', args, { encoding: 'utf8' });
75
+ const tracked = out.split('\n').map((s) => s.trim()).filter(Boolean);
76
+ // include untracked files too (a new connector/migration not yet staged)
77
+ const un = execFileSync('git', ['ls-files', '--others', '--exclude-standard'], { encoding: 'utf8' })
78
+ .split('\n').map((s) => s.trim()).filter(Boolean);
79
+ return [...new Set([...tracked, ...un])];
80
+ } catch {
81
+ return [];
82
+ }
83
+ }
84
+
85
+ function readProject() {
86
+ for (const p of ['.great_cto/PROJECT.md', 'PROJECT.md']) {
87
+ try { return parseProject(readFileSync(p, 'utf8')); } catch { /* next */ }
88
+ }
89
+ return { archetype: 'greenfield', size: 'medium' };
90
+ }
91
+
92
+ if (import.meta.url === `file://${process.argv[1]}`) {
93
+ const argv = process.argv.slice(2);
94
+ const opt = (name) => { const i = argv.indexOf(name); return i >= 0 ? argv[i + 1] : null; };
95
+ const json = argv.includes('--json');
96
+ const labels = argv.reduce((a, v, i) => (argv[i - 1] === '--label' ? [...a, v] : a), []);
97
+
98
+ const { archetype, size } = readProject();
99
+ const changedFiles = gitChangedFiles(opt('--base'));
100
+ const result = planGates({
101
+ archetype, size, changedFiles,
102
+ deployTarget: opt('--deploy'),
103
+ labels,
104
+ });
105
+
106
+ if (json) {
107
+ process.stdout.write(`${JSON.stringify({ archetype, size, ...result }, null, 2)}\n`);
108
+ } else {
109
+ const esc = result.escalatedFromLabel ? ` (label ${result.escalatedFromLabel} overridden by T2 floor)` : '';
110
+ process.stdout.write(
111
+ `archetype=${archetype} size=${size}\n` +
112
+ `tier=${result.tier}${esc}\n` +
113
+ `reasons: ${result.reasons.join(', ')}\n` +
114
+ `gates: ${result.gates.length ? result.gates.map((g) => `${g}[${result.gateRegimes[g]}]`).join(', ') : '(none — CI is the gate)'}\n` +
115
+ `regime: R2-mechanical ${result.r2Share}% · R1-textual ${100 - result.r2Share}%\n` +
116
+ `judge: ${result.judge.model}${result.judge.human ? ' + human' : ''}\n`,
117
+ );
118
+ }
119
+ }
@@ -0,0 +1,42 @@
1
+ // scripts/lib/judge-model.mjs — runtime judge-model router (ADR-004).
2
+ //
3
+ // The cost-of-error axis from ADR-004: a judge/eval/scorer grading a T0/T1 (reversible,
4
+ // CI-checked) change runs a CHEAP model — a wrong "looks good" is cheap there. On a T2
5
+ // (irreversible / regulated / prod-deploy) change the judge runs the FRONTIER model AND
6
+ // the human is in the loop, because a false-APPROVED carries liability.
7
+ //
8
+ // The cheap-judge id is env-pluggable (GREAT_CTO_JUDGE_MODEL) so a fine-tuned open judge
9
+ // (Qwen-class — matches frontier at ~100x lower cost per the 2026-06 result) can replace
10
+ // the default without a code change, once qualified by scripts/judge-validate.mjs.
11
+ //
12
+ // Pairs with change-tier.mjs (the tier) + gate-plan.mjs (per-change plan) + ADR-004.
13
+
14
+ export const DEFAULT_FRONTIER_JUDGE = 'claude-opus-4-8';
15
+ export const DEFAULT_CHEAP_JUDGE = 'claude-haiku-4-5';
16
+
17
+ /**
18
+ * Pick the judge model for a change of the given tier.
19
+ * @param {('T0'|'T1'|'T2')} changeTier
20
+ * @param {{cheapModel?:string, frontierModel?:string}} [opts]
21
+ * @returns {{model:string, tier:string, human:boolean, reason:string}}
22
+ */
23
+ export function selectJudgeModel(changeTier, opts = {}) {
24
+ const cheap = opts.cheapModel || process.env.GREAT_CTO_JUDGE_MODEL || DEFAULT_CHEAP_JUDGE;
25
+ const frontier = opts.frontierModel || DEFAULT_FRONTIER_JUDGE;
26
+
27
+ // Fail-safe: an unknown tier is treated as T2 (frontier + human) — never silently cheap.
28
+ const tier = (changeTier === 'T0' || changeTier === 'T1') ? changeTier : 'T2';
29
+
30
+ if (tier === 'T2') {
31
+ return { model: frontier, tier, human: true,
32
+ reason: 'T2 (irreversible/regulated) — frontier judge + human gate; a false-APPROVED carries liability' };
33
+ }
34
+ return { model: cheap, tier, human: false,
35
+ reason: `${tier} (reversible) — cheap judge; CI + tests are the safety net` };
36
+ }
37
+
38
+ // CLI: node scripts/lib/judge-model.mjs T1 → prints the model + reason
39
+ if (import.meta.url === `file://${process.argv[1]}`) {
40
+ const r = selectJudgeModel(process.argv[2]);
41
+ process.stdout.write(`${r.tier} model=${r.model} human=${r.human}\n ${r.reason}\n`);
42
+ }
@@ -0,0 +1,47 @@
1
+ // Board server resolution — extracted from main.ts so it is unit-testable
2
+ // (main.ts self-executes on import). See great_cto-hbu3.
3
+ import { existsSync as fsExistsSync, readdirSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ /**
8
+ * Locate the board server.mjs. Order:
9
+ * 1. dev layouts (running from a repo checkout)
10
+ * 2. installed plugin cache — ANY marketplace under ~/.claude/plugins/cache,
11
+ * newest 5 versions each (not just the "local" marketplace)
12
+ * 3. the copy bundled into the npm package by scripts/bundle-board.mjs —
13
+ * guaranteed fallback so a fresh `npm i -g great-cto` can run the board
14
+ * without the plugin installed
15
+ */
16
+ export function findBoardServerPath(baseDir, home) {
17
+ const here = baseDir ?? dirname(fileURLToPath(import.meta.url));
18
+ const candidates = [
19
+ join(here, "..", "..", "board", "server.mjs"), // packages/cli/dist (dev)
20
+ join(here, "..", "board", "server.mjs"), // alt dev layout
21
+ join(here, "board", "server.mjs"), // flat layout
22
+ ];
23
+ // Numeric semver sort — a plain .sort() is lexicographic and would rank
24
+ // 2.99.0 above 2.100.0 (and once ranked 2.7.0 above 2.69.0).
25
+ const byVer = (a, b) => {
26
+ const pa = a.split(".").map(Number), pb = b.split(".").map(Number);
27
+ return (pb[0] - pa[0]) || (pb[1] - pa[1]) || (pb[2] - pa[2]) || 0;
28
+ };
29
+ const cacheRoot = join(home ?? homedir(), ".claude", "plugins", "cache");
30
+ if (fsExistsSync(cacheRoot)) {
31
+ try {
32
+ for (const marketplace of readdirSync(cacheRoot)) {
33
+ const pluginBase = join(cacheRoot, marketplace, "great_cto");
34
+ if (!fsExistsSync(pluginBase))
35
+ continue;
36
+ const versions = readdirSync(pluginBase).filter(v => /^\d/.test(v)).sort(byVer);
37
+ for (const v of versions.slice(0, 5)) {
38
+ candidates.push(join(pluginBase, v, "packages", "board", "server.mjs"));
39
+ }
40
+ }
41
+ }
42
+ catch { /* ignore */ }
43
+ }
44
+ // bundled copy (dist → ../board/packages/board/server.mjs)
45
+ candidates.push(join(here, "..", "board", "packages", "board", "server.mjs"));
46
+ return candidates.find(fsExistsSync);
47
+ }
package/dist/main.js CHANGED
@@ -21,7 +21,8 @@ import { bootstrap } from "./bootstrap.js";
21
21
  import { compileFlow } from "./flow.js";
22
22
  import { shouldUseLlmFallback, suggestArchetypeFromLlm } from "./llm-fallback.js";
23
23
  import { sendUsagePing, sendInstallPing, telemetrySubcommand, isTelemetryEnabled, computeAnonId } from "./telemetry.js";
24
- import { readFileSync, writeFileSync, copyFileSync, chmodSync, mkdirSync, readdirSync, unlinkSync, existsSync as fsExistsSync } from "node:fs";
24
+ import { findBoardServerPath } from "./board-path.js";
25
+ import { readFileSync, writeFileSync, copyFileSync, chmodSync, mkdirSync, unlinkSync, existsSync as fsExistsSync } from "node:fs";
25
26
  import { dirname, join } from "node:path";
26
27
  import { fileURLToPath } from "node:url";
27
28
  import { homedir } from "node:os";
@@ -215,35 +216,6 @@ function boardPidFilePath(surface) {
215
216
  // start must never kill the builder board (and vice versa).
216
217
  return join(homedir(), ".great_cto", surface === "console" ? "console.pid" : "board.pid");
217
218
  }
218
- /**
219
- * Locate the board server.mjs — checks dev layouts then plugin cache versions
220
- * in descending order, returning the first path that exists.
221
- */
222
- function findBoardServerPath() {
223
- const here = dirname(fileURLToPath(import.meta.url));
224
- const candidates = [
225
- join(here, "..", "..", "board", "server.mjs"), // packages/cli/dist (dev)
226
- join(here, "..", "board", "server.mjs"), // alt dev layout
227
- join(here, "board", "server.mjs"), // flat layout
228
- ];
229
- const pluginBase = join(homedir(), ".claude", "plugins", "cache", "local", "great_cto");
230
- if (fsExistsSync(pluginBase)) {
231
- try {
232
- // Numeric semver sort — a plain .sort() is lexicographic and would rank
233
- // 2.99.0 above 2.100.0 (and once ranked 2.7.0 above 2.69.0).
234
- const byVer = (a, b) => {
235
- const pa = a.split(".").map(Number), pb = b.split(".").map(Number);
236
- return (pb[0] - pa[0]) || (pb[1] - pa[1]) || (pb[2] - pa[2]) || 0;
237
- };
238
- const versions = readdirSync(pluginBase).filter(v => /^\d/.test(v)).sort(byVer);
239
- for (const v of versions.slice(0, 5)) {
240
- candidates.push(join(pluginBase, v, "packages", "board", "server.mjs"));
241
- }
242
- }
243
- catch { /* ignore */ }
244
- }
245
- return candidates.find(fsExistsSync);
246
- }
247
219
  /**
248
220
  * Kill the board server recorded in the PID file.
249
221
  * Returns true if a live process was terminated.
@@ -336,7 +308,7 @@ async function runBoard(args, surface) {
336
308
  }
337
309
  const serverPath = findBoardServerPath();
338
310
  if (!serverPath) {
339
- error("Board server not found. Try reinstalling: npx great-cto@latest");
311
+ error("Board server not found. Reinstall the CLI (npm i -g great-cto@latest) or install the great_cto plugin (npx great-cto install).");
340
312
  return 1;
341
313
  }
342
314
  const nodeArgs = [serverPath];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "2.77.0",
3
+ "version": "2.77.1",
4
4
  "description": "One command install for the great_cto Claude Code plugin. Auto-detects your stack, picks the right archetype, bootstraps PROJECT.md.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -70,6 +70,7 @@
70
70
  "index.mjs",
71
71
  "dist/",
72
72
  "assets/",
73
+ "board/",
73
74
  "postinstall.mjs",
74
75
  "README.md",
75
76
  "scripts/hooks/"
@@ -80,6 +81,7 @@
80
81
  "test": "npm run build && node --test tests/*.test.mjs tests/**/*.test.mjs",
81
82
  "test:e2e": "npm run build && node ../../tests/run-archetype-e2e.mjs",
82
83
  "postinstall": "node postinstall.mjs",
84
+ "prepack": "node scripts/bundle-board.mjs",
83
85
  "prepublishOnly": "npm run build"
84
86
  },
85
87
  "devDependencies": {