runboard 0.1.0 → 1.1.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/AGENTS.md +6 -0
- package/README.md +34 -4
- package/dist/{chunk-U4SVYBXI.js → chunk-3NNMYWCN.js} +172 -55
- package/dist/cli.js +219 -7
- package/dist/mcp.js +4 -130
- package/package.json +3 -2
package/AGENTS.md
CHANGED
|
@@ -25,6 +25,7 @@ runboard pulse # compare the two latest assessments
|
|
|
25
25
|
runboard roadmap # Now/Next/Later from the binding constraint
|
|
26
26
|
runboard report --type board-update|baseline|monthly
|
|
27
27
|
runboard status # one-screen current state
|
|
28
|
+
runboard skills install [--target <dir>] [--force] [--dry-run] # copy these skills to an agent
|
|
28
29
|
```
|
|
29
30
|
|
|
30
31
|
Dimension keys: `build.team`, `build.tools`, `build.techniques`, `run.team`, `run.tools`,
|
|
@@ -49,6 +50,11 @@ their results match the CLI exactly.
|
|
|
49
50
|
`skills/*/SKILL.md` are written to the open SKILL.md standard and run unmodified across
|
|
50
51
|
agent platforms. They own the conversation; the CLI owns the computation.
|
|
51
52
|
|
|
53
|
+
To install them into your agent, run `runboard skills install`. With no `--target` it
|
|
54
|
+
auto-detects a supported agent (v1: Claude Code → `.claude/skills/`); for any other agent
|
|
55
|
+
pass `--target <its skills dir>` (e.g. `--target .cursor/skills`). Existing skills are not
|
|
56
|
+
overwritten without `--force`, and `--dry-run` previews without writing.
|
|
57
|
+
|
|
52
58
|
> Note: in published installs, `CLAUDE.md` is provided as a copy/symlink of this file so
|
|
53
59
|
> Claude Code reads the same guidance. In this source repo, `CLAUDE.md` is the
|
|
54
60
|
> contributor/Spec-Kit guide instead — this AGENTS.md is the end-user agent guide.
|
package/README.md
CHANGED
|
@@ -41,13 +41,43 @@ history becomes your trajectory record.
|
|
|
41
41
|
| `runboard roadmap` | Now/Next/Later plan from your binding constraint. |
|
|
42
42
|
| `runboard report --type <t>` | Render a board-ready report from a template. |
|
|
43
43
|
| `runboard status` | One-screen current state. |
|
|
44
|
+
| `runboard skills install [--target <dir>]` | Copy the bundled SKILL.md skills into an agent's skills directory. |
|
|
44
45
|
|
|
45
46
|
## Use it from your AI assistant
|
|
46
47
|
|
|
47
|
-
Portable `skills/` (SKILL.md) and an MCP server
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
Portable `skills/` (SKILL.md) and an MCP server let Claude, Cursor, Codex, Copilot, Gemini,
|
|
49
|
+
and other agents run the assessment conversation and persist results through the same core
|
|
50
|
+
— the numbers are always computed by the tool, never by the model.
|
|
51
|
+
|
|
52
|
+
The MCP server ships in this same package and runs locally over stdio (no network, no
|
|
53
|
+
account). Add it to your client with one zero-install command:
|
|
54
|
+
|
|
55
|
+
```jsonc
|
|
56
|
+
{
|
|
57
|
+
"mcpServers": {
|
|
58
|
+
"runboard": { "command": "npx", "args": ["-y", "runboard", "mcp"] }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
See [docs/mcp.md](./docs/mcp.md) for Claude Desktop, Cursor, and VS Code setup.
|
|
64
|
+
|
|
65
|
+
### Installing the skills
|
|
66
|
+
|
|
67
|
+
SKILL.md is a cross-agent open format, so "installing" a skill just means copying its
|
|
68
|
+
folder to wherever your agent looks for skills. `skills install` does that from the copy
|
|
69
|
+
already bundled in this package — no download, no network:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npx runboard@latest skills install # auto-detect (Claude Code → .claude/skills/)
|
|
73
|
+
npx runboard@latest skills install --target .cursor/skills # any other agent
|
|
74
|
+
npx runboard@latest skills install --dry-run # preview; writes nothing
|
|
75
|
+
npx runboard@latest skills install --force # overwrite existing copies (e.g. after upgrade)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Without `--target` it auto-detects a supported agent (v1: Claude Code, via a `.claude/`
|
|
79
|
+
directory). For any other agent, point `--target` at its skills directory. Existing skills
|
|
80
|
+
are never overwritten without `--force`. Restart your agent afterward so it discovers them.
|
|
51
81
|
|
|
52
82
|
## Contributing
|
|
53
83
|
|
|
@@ -1,42 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
};
|
|
3
|
+
// mcp/server.ts
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { z } from "zod";
|
|
6
7
|
|
|
7
|
-
// src/
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import path from "path";
|
|
12
|
-
function runboardPaths(root = process.cwd()) {
|
|
13
|
-
const dir = path.join(root, ".runboard");
|
|
14
|
-
return {
|
|
15
|
-
root,
|
|
16
|
-
dir,
|
|
17
|
-
config: path.join(dir, "config.yaml"),
|
|
18
|
-
rubric: path.join(dir, "rubric.yaml"),
|
|
19
|
-
assessmentsDir: path.join(dir, "assessments"),
|
|
20
|
-
reportsDir: path.join(dir, "reports"),
|
|
21
|
-
roadmap: path.join(dir, "roadmap.md"),
|
|
22
|
-
boardHtml: path.join(dir, "board.html")
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
function assessmentFile(root, date) {
|
|
26
|
-
return path.join(runboardPaths(root).assessmentsDir, `${date}.md`);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// src/commands/shared.ts
|
|
30
|
-
function requireInit(root = process.cwd()) {
|
|
31
|
-
if (!existsSync(runboardPaths(root).dir)) {
|
|
32
|
-
throw new UserError("No .runboard/ found here. Run `runboard init` first.");
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
function requireAssessments(dates) {
|
|
36
|
-
if (dates.length === 0) {
|
|
37
|
-
throw new UserError("No assessments yet. Run `runboard assess` to record one.");
|
|
38
|
-
}
|
|
39
|
-
}
|
|
8
|
+
// src/version.ts
|
|
9
|
+
import { readFileSync } from "fs";
|
|
10
|
+
var pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
11
|
+
var VERSION = pkg.version;
|
|
40
12
|
|
|
41
13
|
// src/commands/assess.ts
|
|
42
14
|
import { cancel, intro, isCancel, note, outro, select, text } from "@clack/prompts";
|
|
@@ -79,9 +51,30 @@ function isLevel(value) {
|
|
|
79
51
|
}
|
|
80
52
|
|
|
81
53
|
// src/data/assessments.ts
|
|
82
|
-
import { existsSync
|
|
54
|
+
import { existsSync, mkdirSync, readFileSync as readFileSync2, readdirSync, writeFileSync } from "fs";
|
|
83
55
|
import path2 from "path";
|
|
84
56
|
import { parse, stringify } from "yaml";
|
|
57
|
+
|
|
58
|
+
// src/data/paths.ts
|
|
59
|
+
import path from "path";
|
|
60
|
+
function runboardPaths(root = process.cwd()) {
|
|
61
|
+
const dir = path.join(root, ".runboard");
|
|
62
|
+
return {
|
|
63
|
+
root,
|
|
64
|
+
dir,
|
|
65
|
+
config: path.join(dir, "config.yaml"),
|
|
66
|
+
rubric: path.join(dir, "rubric.yaml"),
|
|
67
|
+
assessmentsDir: path.join(dir, "assessments"),
|
|
68
|
+
reportsDir: path.join(dir, "reports"),
|
|
69
|
+
roadmap: path.join(dir, "roadmap.md"),
|
|
70
|
+
boardHtml: path.join(dir, "board.html")
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function assessmentFile(root, date) {
|
|
74
|
+
return path.join(runboardPaths(root).assessmentsDir, `${date}.md`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/data/assessments.ts
|
|
85
78
|
var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
86
79
|
var AssessmentError = class extends Error {
|
|
87
80
|
};
|
|
@@ -159,14 +152,14 @@ function parseAssessment(text2, fallbackDate) {
|
|
|
159
152
|
}
|
|
160
153
|
function listAssessmentDates(root = process.cwd()) {
|
|
161
154
|
const dir = runboardPaths(root).assessmentsDir;
|
|
162
|
-
if (!
|
|
155
|
+
if (!existsSync(dir)) {
|
|
163
156
|
return [];
|
|
164
157
|
}
|
|
165
158
|
return readdirSync(dir).filter((f) => f.endsWith(".md")).map((f) => path2.basename(f, ".md")).filter((d) => DATE_RE.test(d)).sort();
|
|
166
159
|
}
|
|
167
160
|
function loadAssessment(date, root = process.cwd()) {
|
|
168
161
|
const file = assessmentFile(root, date);
|
|
169
|
-
return parseAssessment(
|
|
162
|
+
return parseAssessment(readFileSync2(file, "utf8"), date);
|
|
170
163
|
}
|
|
171
164
|
function loadAllAssessments(root = process.cwd()) {
|
|
172
165
|
return listAssessmentDates(root).map((date) => loadAssessment(date, root));
|
|
@@ -180,7 +173,7 @@ function saveAssessment(a, root = process.cwd(), options = {}) {
|
|
|
180
173
|
const dir = runboardPaths(root).assessmentsDir;
|
|
181
174
|
mkdirSync(dir, { recursive: true });
|
|
182
175
|
const file = assessmentFile(root, a.date);
|
|
183
|
-
if (
|
|
176
|
+
if (existsSync(file) && !options.force) {
|
|
184
177
|
throw new AssessmentError(
|
|
185
178
|
`An assessment for ${a.date} already exists. Re-run with --force to overwrite.`
|
|
186
179
|
);
|
|
@@ -190,7 +183,7 @@ function saveAssessment(a, root = process.cwd(), options = {}) {
|
|
|
190
183
|
}
|
|
191
184
|
|
|
192
185
|
// src/data/rubric.ts
|
|
193
|
-
import { readFileSync as
|
|
186
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
194
187
|
import path3 from "path";
|
|
195
188
|
import { fileURLToPath } from "url";
|
|
196
189
|
import { parse as parse2 } from "yaml";
|
|
@@ -202,7 +195,7 @@ function shippedRubricPath() {
|
|
|
202
195
|
];
|
|
203
196
|
for (const candidate of candidates) {
|
|
204
197
|
try {
|
|
205
|
-
|
|
198
|
+
readFileSync3(candidate);
|
|
206
199
|
return candidate;
|
|
207
200
|
} catch {
|
|
208
201
|
}
|
|
@@ -262,13 +255,32 @@ function parseDimension(raw) {
|
|
|
262
255
|
function loadRubric(filePath = shippedRubricPath()) {
|
|
263
256
|
let text2;
|
|
264
257
|
try {
|
|
265
|
-
text2 =
|
|
258
|
+
text2 = readFileSync3(filePath, "utf8");
|
|
266
259
|
} catch {
|
|
267
260
|
throw new Error(`Could not read rubric at ${filePath}.`);
|
|
268
261
|
}
|
|
269
262
|
return parseRubric(text2);
|
|
270
263
|
}
|
|
271
264
|
|
|
265
|
+
// src/commands/shared.ts
|
|
266
|
+
import { existsSync as existsSync2 } from "fs";
|
|
267
|
+
|
|
268
|
+
// src/core/errors.ts
|
|
269
|
+
var UserError = class extends Error {
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// src/commands/shared.ts
|
|
273
|
+
function requireInit(root = process.cwd()) {
|
|
274
|
+
if (!existsSync2(runboardPaths(root).dir)) {
|
|
275
|
+
throw new UserError("No .runboard/ found here. Run `runboard init` first.");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function requireAssessments(dates) {
|
|
279
|
+
if (dates.length === 0) {
|
|
280
|
+
throw new UserError("No assessments yet. Run `runboard assess` to record one.");
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
272
284
|
// src/commands/assess.ts
|
|
273
285
|
function today() {
|
|
274
286
|
const d = /* @__PURE__ */ new Date();
|
|
@@ -609,7 +621,7 @@ function detectTriggers(assessmentsChrono) {
|
|
|
609
621
|
}
|
|
610
622
|
|
|
611
623
|
// src/render/reports.ts
|
|
612
|
-
import { readFileSync as
|
|
624
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
613
625
|
import path4 from "path";
|
|
614
626
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
615
627
|
import { Eta } from "eta";
|
|
@@ -638,7 +650,7 @@ function templatesDir() {
|
|
|
638
650
|
const candidates = [path4.resolve(here, "../templates"), path4.resolve(here, "../../templates")];
|
|
639
651
|
for (const candidate of candidates) {
|
|
640
652
|
try {
|
|
641
|
-
|
|
653
|
+
readFileSync4(path4.join(candidate, "roadmap.eta"));
|
|
642
654
|
return candidate;
|
|
643
655
|
} catch {
|
|
644
656
|
}
|
|
@@ -881,24 +893,129 @@ function registerStatus(program) {
|
|
|
881
893
|
});
|
|
882
894
|
}
|
|
883
895
|
|
|
896
|
+
// mcp/handlers.ts
|
|
897
|
+
function handleAssess(args) {
|
|
898
|
+
const sets = Object.entries(args.scores).map(
|
|
899
|
+
([key, s]) => `${key}=${s.level}:${s.trajectory}:${s.evidence ?? ""}`
|
|
900
|
+
);
|
|
901
|
+
const { date, path: path7 } = runAssess({
|
|
902
|
+
root: args.root,
|
|
903
|
+
sets,
|
|
904
|
+
type: args.type,
|
|
905
|
+
force: args.force,
|
|
906
|
+
date: args.date
|
|
907
|
+
});
|
|
908
|
+
return { date, path: path7, written: true };
|
|
909
|
+
}
|
|
910
|
+
function handleBoard(ctx) {
|
|
911
|
+
const { summary, htmlPath } = runBoard({ root: ctx.root, html: ctx.html });
|
|
912
|
+
return {
|
|
913
|
+
cells: summary.cells,
|
|
914
|
+
average: formatAverage(summary.average),
|
|
915
|
+
trajectoryCounts: summary.trajectoryCounts,
|
|
916
|
+
...htmlPath ? { htmlPath } : {}
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
function handlePulse(ctx) {
|
|
920
|
+
const { path: path7, triggers } = runPulse({ root: ctx.root });
|
|
921
|
+
return { path: path7, triggers };
|
|
922
|
+
}
|
|
923
|
+
function handleRoadmap(ctx) {
|
|
924
|
+
const { path: path7 } = runRoadmap({ root: ctx.root });
|
|
925
|
+
const latest = latestAssessment(ctx.root);
|
|
926
|
+
if (!latest) throw new Error("unreachable");
|
|
927
|
+
return { path: path7, bindingConstraint: bindingConstraint(latest) };
|
|
928
|
+
}
|
|
929
|
+
function handleReport(ctx) {
|
|
930
|
+
const { path: path7 } = runReport({ root: ctx.root, type: ctx.type });
|
|
931
|
+
return { path: path7 };
|
|
932
|
+
}
|
|
933
|
+
function handleStatus(ctx) {
|
|
934
|
+
const s = runStatus(ctx.root);
|
|
935
|
+
return {
|
|
936
|
+
latestDate: s.latestDate ?? null,
|
|
937
|
+
average: s.average ?? null,
|
|
938
|
+
trajectoryCounts: s.trajectoryCounts ?? {},
|
|
939
|
+
activeTriggers: s.activeTriggers
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// mcp/server.ts
|
|
944
|
+
var scoreShape = z.object({
|
|
945
|
+
level: z.number().int().min(1).max(5),
|
|
946
|
+
trajectory: z.enum(["up", "flat", "down", "volatile"]),
|
|
947
|
+
evidence: z.string().default("")
|
|
948
|
+
});
|
|
949
|
+
function json(value) {
|
|
950
|
+
return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
|
|
951
|
+
}
|
|
952
|
+
function buildServer() {
|
|
953
|
+
const server = new McpServer({ name: "runboard", version: VERSION });
|
|
954
|
+
server.registerTool(
|
|
955
|
+
"runboard_assess",
|
|
956
|
+
{
|
|
957
|
+
description: "Record a 9-dimension assessment (the model supplies scores).",
|
|
958
|
+
inputSchema: {
|
|
959
|
+
scores: z.record(z.string(), scoreShape),
|
|
960
|
+
type: z.enum(["baseline", "pulse", "quarterly", "event"]).optional(),
|
|
961
|
+
force: z.boolean().optional()
|
|
962
|
+
}
|
|
963
|
+
},
|
|
964
|
+
async (args) => json(handleAssess(args))
|
|
965
|
+
);
|
|
966
|
+
server.registerTool(
|
|
967
|
+
"runboard_board",
|
|
968
|
+
{
|
|
969
|
+
description: "Render the board summary; set html to also write board.html.",
|
|
970
|
+
inputSchema: { html: z.boolean().optional() }
|
|
971
|
+
},
|
|
972
|
+
async (args) => json(handleBoard({ html: args.html }))
|
|
973
|
+
);
|
|
974
|
+
server.registerTool(
|
|
975
|
+
"runboard_pulse",
|
|
976
|
+
{
|
|
977
|
+
description: "Compare the two latest assessments and flag stuck dimensions.",
|
|
978
|
+
inputSchema: {}
|
|
979
|
+
},
|
|
980
|
+
async () => json(handlePulse({}))
|
|
981
|
+
);
|
|
982
|
+
server.registerTool(
|
|
983
|
+
"runboard_roadmap",
|
|
984
|
+
{ description: "Generate a Now/Next/Later plan from the binding constraint.", inputSchema: {} },
|
|
985
|
+
async () => json(handleRoadmap({}))
|
|
986
|
+
);
|
|
987
|
+
server.registerTool(
|
|
988
|
+
"runboard_report",
|
|
989
|
+
{
|
|
990
|
+
description: "Render a report (board-update | baseline | monthly).",
|
|
991
|
+
inputSchema: { type: z.enum(["board-update", "baseline", "monthly"]) }
|
|
992
|
+
},
|
|
993
|
+
async (args) => json(handleReport({ type: args.type }))
|
|
994
|
+
);
|
|
995
|
+
server.registerTool(
|
|
996
|
+
"runboard_status",
|
|
997
|
+
{ description: "One-screen current state.", inputSchema: {} },
|
|
998
|
+
async () => json(handleStatus({}))
|
|
999
|
+
);
|
|
1000
|
+
return server;
|
|
1001
|
+
}
|
|
1002
|
+
async function startMcpServer() {
|
|
1003
|
+
const server = buildServer();
|
|
1004
|
+
const transport = new StdioServerTransport();
|
|
1005
|
+
await server.connect(transport);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
884
1008
|
export {
|
|
1009
|
+
VERSION,
|
|
885
1010
|
runboardPaths,
|
|
886
|
-
latestAssessment,
|
|
887
1011
|
shippedRubricPath,
|
|
888
1012
|
loadRubric,
|
|
889
1013
|
UserError,
|
|
890
|
-
runAssess,
|
|
891
1014
|
registerAssess,
|
|
892
|
-
formatAverage,
|
|
893
|
-
bindingConstraint,
|
|
894
|
-
runBoard,
|
|
895
1015
|
registerBoard,
|
|
896
|
-
runPulse,
|
|
897
1016
|
registerPulse,
|
|
898
|
-
runReport,
|
|
899
1017
|
registerReport,
|
|
900
|
-
runRoadmap,
|
|
901
1018
|
registerRoadmap,
|
|
902
|
-
|
|
903
|
-
|
|
1019
|
+
registerStatus,
|
|
1020
|
+
startMcpServer
|
|
904
1021
|
};
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
UserError,
|
|
4
|
+
VERSION,
|
|
4
5
|
loadRubric,
|
|
5
6
|
registerAssess,
|
|
6
7
|
registerBoard,
|
|
@@ -9,8 +10,9 @@ import {
|
|
|
9
10
|
registerRoadmap,
|
|
10
11
|
registerStatus,
|
|
11
12
|
runboardPaths,
|
|
12
|
-
shippedRubricPath
|
|
13
|
-
|
|
13
|
+
shippedRubricPath,
|
|
14
|
+
startMcpServer
|
|
15
|
+
} from "./chunk-3NNMYWCN.js";
|
|
14
16
|
|
|
15
17
|
// src/cli.ts
|
|
16
18
|
import { Command } from "commander";
|
|
@@ -63,8 +65,216 @@ ${created.map((c) => ` ${c}`).join("\n")}
|
|
|
63
65
|
});
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
// src/commands/skills.ts
|
|
69
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, statSync as statSync2 } from "fs";
|
|
70
|
+
import path2 from "path";
|
|
71
|
+
|
|
72
|
+
// src/core/skills.ts
|
|
73
|
+
function selectTarget(options, detected) {
|
|
74
|
+
if (options.target) {
|
|
75
|
+
return { kind: "explicit", dir: options.target };
|
|
76
|
+
}
|
|
77
|
+
if (detected.length === 0) {
|
|
78
|
+
return { kind: "none" };
|
|
79
|
+
}
|
|
80
|
+
if (detected.length === 1) {
|
|
81
|
+
const only = detected[0];
|
|
82
|
+
return { kind: "detected", agent: only.agent, dir: only.dir };
|
|
83
|
+
}
|
|
84
|
+
return { kind: "ambiguous", dirs: detected };
|
|
85
|
+
}
|
|
86
|
+
function planInstall(bundled, existingNames, options) {
|
|
87
|
+
const existing = new Set(existingNames);
|
|
88
|
+
return bundled.map((skill) => {
|
|
89
|
+
if (!existing.has(skill.name)) {
|
|
90
|
+
return { name: skill.name, kind: "create" };
|
|
91
|
+
}
|
|
92
|
+
if (options.force) {
|
|
93
|
+
return { name: skill.name, kind: "overwrite" };
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
name: skill.name,
|
|
97
|
+
kind: "skip",
|
|
98
|
+
reason: "already present (use --force to overwrite)"
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/data/skills.ts
|
|
104
|
+
import { cpSync, readdirSync, statSync } from "fs";
|
|
105
|
+
import path from "path";
|
|
106
|
+
import { fileURLToPath } from "url";
|
|
107
|
+
function shippedSkillsDir() {
|
|
108
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
109
|
+
const candidates = [path.resolve(here, "../skills"), path.resolve(here, "../../skills")];
|
|
110
|
+
for (const candidate of candidates) {
|
|
111
|
+
try {
|
|
112
|
+
if (statSync(candidate).isDirectory()) {
|
|
113
|
+
return candidate;
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return candidates[candidates.length - 1];
|
|
119
|
+
}
|
|
120
|
+
function listBundledSkills(dir = shippedSkillsDir()) {
|
|
121
|
+
let names;
|
|
122
|
+
try {
|
|
123
|
+
names = readdirSync(dir);
|
|
124
|
+
} catch {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
return names.map((name) => ({ name, sourceDir: path.join(dir, name) })).filter((skill) => isDirectory(skill.sourceDir)).filter((skill) => isFile(path.join(skill.sourceDir, "SKILL.md"))).sort((a, b) => a.name.localeCompare(b.name));
|
|
128
|
+
}
|
|
129
|
+
var AGENT_TABLE = [
|
|
130
|
+
{ agent: "claude-code", marker: ".claude", skillsDir: ".claude/skills" }
|
|
131
|
+
];
|
|
132
|
+
function detectAgentDirs(cwd = process.cwd()) {
|
|
133
|
+
return AGENT_TABLE.filter((row) => isDirectory(path.join(cwd, row.marker))).map((row) => ({
|
|
134
|
+
agent: row.agent,
|
|
135
|
+
dir: path.join(cwd, row.skillsDir)
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
function existingSkillNames(destination) {
|
|
139
|
+
try {
|
|
140
|
+
return readdirSync(destination, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
141
|
+
} catch {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function copySkill(sourceDir, destDir) {
|
|
146
|
+
const target = path.join(destDir, path.basename(sourceDir));
|
|
147
|
+
cpSync(sourceDir, target, { recursive: true, force: true });
|
|
148
|
+
}
|
|
149
|
+
function isDirectory(p) {
|
|
150
|
+
try {
|
|
151
|
+
return statSync(p).isDirectory();
|
|
152
|
+
} catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function isFile(p) {
|
|
157
|
+
try {
|
|
158
|
+
return statSync(p).isFile();
|
|
159
|
+
} catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/commands/skills.ts
|
|
165
|
+
function runSkillsInstall(opts = {}) {
|
|
166
|
+
const root = opts.root ?? process.cwd();
|
|
167
|
+
const force = opts.force ?? false;
|
|
168
|
+
const dryRun = opts.dryRun ?? false;
|
|
169
|
+
const bundled = listBundledSkills();
|
|
170
|
+
if (bundled.length === 0) {
|
|
171
|
+
throw new UserError(
|
|
172
|
+
"No bundled skills found in this runboard install. The package may be corrupt; try reinstalling."
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
const destination = resolveDestination(root, opts.target);
|
|
176
|
+
if (existsSync2(destination) && !statSync2(destination).isDirectory()) {
|
|
177
|
+
throw new UserError(`Target ${destination} is a file, not a directory.`);
|
|
178
|
+
}
|
|
179
|
+
const actions = planInstall(bundled, existingSkillNames(destination), { force });
|
|
180
|
+
const installed = [];
|
|
181
|
+
const skipped = [];
|
|
182
|
+
const failed = [];
|
|
183
|
+
if (dryRun) {
|
|
184
|
+
return { destination, dryRun, installed, skipped, failed, actions };
|
|
185
|
+
}
|
|
186
|
+
mkdirSync2(destination, { recursive: true });
|
|
187
|
+
for (const action of actions) {
|
|
188
|
+
if (action.kind === "skip") {
|
|
189
|
+
skipped.push(action.name);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const skill = bundled.find((b) => b.name === action.name);
|
|
193
|
+
if (!skill) {
|
|
194
|
+
throw new Error(`unreachable: planned skill ${action.name} not in bundle`);
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
copySkill(skill.sourceDir, destination);
|
|
198
|
+
installed.push(action.name);
|
|
199
|
+
} catch (err) {
|
|
200
|
+
failed.push({ name: action.name, error: err instanceof Error ? err.message : String(err) });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return { destination, dryRun, installed, skipped, failed, actions };
|
|
204
|
+
}
|
|
205
|
+
function resolveDestination(root, target) {
|
|
206
|
+
const resolution = selectTarget(
|
|
207
|
+
{ target: target ? path2.resolve(root, target) : void 0 },
|
|
208
|
+
detectAgentDirs(root)
|
|
209
|
+
);
|
|
210
|
+
switch (resolution.kind) {
|
|
211
|
+
case "explicit":
|
|
212
|
+
return resolution.dir;
|
|
213
|
+
case "detected":
|
|
214
|
+
return resolution.dir;
|
|
215
|
+
case "none":
|
|
216
|
+
throw new UserError(
|
|
217
|
+
"No agent skills directory detected. Re-run with --target <dir> (e.g. --target .cursor/skills)."
|
|
218
|
+
);
|
|
219
|
+
case "ambiguous":
|
|
220
|
+
throw new UserError(
|
|
221
|
+
`Multiple agent skills directories detected:
|
|
222
|
+
${resolution.dirs.map((d) => ` ${d.agent}: ${d.dir}`).join("\n")}
|
|
223
|
+
Re-run with --target <dir> to choose one.`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function formatReport(report) {
|
|
228
|
+
const lines = [];
|
|
229
|
+
if (report.dryRun) {
|
|
230
|
+
lines.push("Dry run \u2014 no files will be written.");
|
|
231
|
+
lines.push(`Destination: ${report.destination}`);
|
|
232
|
+
for (const action of report.actions) {
|
|
233
|
+
const label = action.kind === "skip" ? "skip (already present)" : action.kind;
|
|
234
|
+
lines.push(` ${action.name.padEnd(14)}${label}`);
|
|
235
|
+
}
|
|
236
|
+
return `${lines.join("\n")}
|
|
237
|
+
`;
|
|
238
|
+
}
|
|
239
|
+
const failedNames = new Set(report.failed.map((f) => f.name));
|
|
240
|
+
lines.push(`Installing skills into ${report.destination}`);
|
|
241
|
+
for (const action of report.actions) {
|
|
242
|
+
const failure = report.failed.find((f) => f.name === action.name);
|
|
243
|
+
if (failure) {
|
|
244
|
+
lines.push(` \u2717 ${action.name.padEnd(14)}(failed: ${failure.error})`);
|
|
245
|
+
} else if (action.kind === "skip") {
|
|
246
|
+
lines.push(
|
|
247
|
+
` - ${action.name.padEnd(14)}(skipped: already present, use --force to overwrite)`
|
|
248
|
+
);
|
|
249
|
+
} else {
|
|
250
|
+
const verb = action.kind === "overwrite" ? "overwritten" : "created";
|
|
251
|
+
lines.push(` \u2713 ${action.name.padEnd(14)}(${verb})`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
lines.push("");
|
|
255
|
+
const tail = failedNames.size > 0 ? `, ${failedNames.size} failed` : "";
|
|
256
|
+
lines.push(
|
|
257
|
+
`Installed ${report.installed.length} skill(s), skipped ${report.skipped.length}${tail}. Restart your agent to pick them up.`
|
|
258
|
+
);
|
|
259
|
+
return `${lines.join("\n")}
|
|
260
|
+
`;
|
|
261
|
+
}
|
|
262
|
+
function registerSkills(program) {
|
|
263
|
+
const skills = program.command("skills").description("Manage runboard's portable AI skills.");
|
|
264
|
+
skills.command("install").description("Copy the bundled SKILL.md skills into an agent's skills directory.").option("--target <dir>", "destination directory (overrides auto-detection)").option("--force", "overwrite skills already present at the destination").option("--dry-run", "show what would be installed without writing anything").action((options) => {
|
|
265
|
+
const report = runSkillsInstall({
|
|
266
|
+
target: options.target,
|
|
267
|
+
force: options.force,
|
|
268
|
+
dryRun: options.dryRun
|
|
269
|
+
});
|
|
270
|
+
process.stdout.write(formatReport(report));
|
|
271
|
+
if (report.failed.length > 0) {
|
|
272
|
+
process.exitCode = 1;
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
66
277
|
// src/cli.ts
|
|
67
|
-
var VERSION = "0.1.0";
|
|
68
278
|
function buildProgram() {
|
|
69
279
|
const program = new Command();
|
|
70
280
|
program.name("runboard").description("Local-first technical-leadership maturity scorecard.").version(VERSION, "-v, --version");
|
|
@@ -75,6 +285,10 @@ function buildProgram() {
|
|
|
75
285
|
registerRoadmap(program);
|
|
76
286
|
registerReport(program);
|
|
77
287
|
registerStatus(program);
|
|
288
|
+
registerSkills(program);
|
|
289
|
+
program.command("mcp").description("Start the MCP server over stdio (for tool-calling AI clients).").action(async () => {
|
|
290
|
+
await startMcpServer();
|
|
291
|
+
});
|
|
78
292
|
return program;
|
|
79
293
|
}
|
|
80
294
|
async function main(argv) {
|
|
@@ -91,12 +305,10 @@ async function main(argv) {
|
|
|
91
305
|
throw err;
|
|
92
306
|
}
|
|
93
307
|
}
|
|
308
|
+
|
|
309
|
+
// src/main.ts
|
|
94
310
|
main(process.argv).catch((err) => {
|
|
95
311
|
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|
|
96
312
|
`);
|
|
97
313
|
process.exit(1);
|
|
98
314
|
});
|
|
99
|
-
export {
|
|
100
|
-
buildProgram,
|
|
101
|
-
main
|
|
102
|
-
};
|
package/dist/mcp.js
CHANGED
|
@@ -1,137 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
latestAssessment,
|
|
6
|
-
runAssess,
|
|
7
|
-
runBoard,
|
|
8
|
-
runPulse,
|
|
9
|
-
runReport,
|
|
10
|
-
runRoadmap,
|
|
11
|
-
runStatus
|
|
12
|
-
} from "./chunk-U4SVYBXI.js";
|
|
3
|
+
startMcpServer
|
|
4
|
+
} from "./chunk-3NNMYWCN.js";
|
|
13
5
|
|
|
14
|
-
// mcp/
|
|
15
|
-
|
|
16
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
17
|
-
import { z } from "zod";
|
|
18
|
-
|
|
19
|
-
// mcp/handlers.ts
|
|
20
|
-
function handleAssess(args) {
|
|
21
|
-
const sets = Object.entries(args.scores).map(
|
|
22
|
-
([key, s]) => `${key}=${s.level}:${s.trajectory}:${s.evidence ?? ""}`
|
|
23
|
-
);
|
|
24
|
-
const { date, path } = runAssess({
|
|
25
|
-
root: args.root,
|
|
26
|
-
sets,
|
|
27
|
-
type: args.type,
|
|
28
|
-
force: args.force,
|
|
29
|
-
date: args.date
|
|
30
|
-
});
|
|
31
|
-
return { date, path, written: true };
|
|
32
|
-
}
|
|
33
|
-
function handleBoard(ctx) {
|
|
34
|
-
const { summary, htmlPath } = runBoard({ root: ctx.root, html: ctx.html });
|
|
35
|
-
return {
|
|
36
|
-
cells: summary.cells,
|
|
37
|
-
average: formatAverage(summary.average),
|
|
38
|
-
trajectoryCounts: summary.trajectoryCounts,
|
|
39
|
-
...htmlPath ? { htmlPath } : {}
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
function handlePulse(ctx) {
|
|
43
|
-
const { path, triggers } = runPulse({ root: ctx.root });
|
|
44
|
-
return { path, triggers };
|
|
45
|
-
}
|
|
46
|
-
function handleRoadmap(ctx) {
|
|
47
|
-
const { path } = runRoadmap({ root: ctx.root });
|
|
48
|
-
const latest = latestAssessment(ctx.root);
|
|
49
|
-
if (!latest) throw new Error("unreachable");
|
|
50
|
-
return { path, bindingConstraint: bindingConstraint(latest) };
|
|
51
|
-
}
|
|
52
|
-
function handleReport(ctx) {
|
|
53
|
-
const { path } = runReport({ root: ctx.root, type: ctx.type });
|
|
54
|
-
return { path };
|
|
55
|
-
}
|
|
56
|
-
function handleStatus(ctx) {
|
|
57
|
-
const s = runStatus(ctx.root);
|
|
58
|
-
return {
|
|
59
|
-
latestDate: s.latestDate ?? null,
|
|
60
|
-
average: s.average ?? null,
|
|
61
|
-
trajectoryCounts: s.trajectoryCounts ?? {},
|
|
62
|
-
activeTriggers: s.activeTriggers
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// mcp/server.ts
|
|
67
|
-
var scoreShape = z.object({
|
|
68
|
-
level: z.number().int().min(1).max(5),
|
|
69
|
-
trajectory: z.enum(["up", "flat", "down", "volatile"]),
|
|
70
|
-
evidence: z.string().default("")
|
|
71
|
-
});
|
|
72
|
-
function json(value) {
|
|
73
|
-
return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
|
|
74
|
-
}
|
|
75
|
-
function buildServer() {
|
|
76
|
-
const server = new McpServer({ name: "runboard", version: "0.1.0" });
|
|
77
|
-
server.registerTool(
|
|
78
|
-
"runboard_assess",
|
|
79
|
-
{
|
|
80
|
-
description: "Record a 9-dimension assessment (the model supplies scores).",
|
|
81
|
-
inputSchema: {
|
|
82
|
-
scores: z.record(z.string(), scoreShape),
|
|
83
|
-
type: z.enum(["baseline", "pulse", "quarterly", "event"]).optional(),
|
|
84
|
-
force: z.boolean().optional()
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
|
-
async (args) => json(handleAssess(args))
|
|
88
|
-
);
|
|
89
|
-
server.registerTool(
|
|
90
|
-
"runboard_board",
|
|
91
|
-
{
|
|
92
|
-
description: "Render the board summary; set html to also write board.html.",
|
|
93
|
-
inputSchema: { html: z.boolean().optional() }
|
|
94
|
-
},
|
|
95
|
-
async (args) => json(handleBoard({ html: args.html }))
|
|
96
|
-
);
|
|
97
|
-
server.registerTool(
|
|
98
|
-
"runboard_pulse",
|
|
99
|
-
{
|
|
100
|
-
description: "Compare the two latest assessments and flag stuck dimensions.",
|
|
101
|
-
inputSchema: {}
|
|
102
|
-
},
|
|
103
|
-
async () => json(handlePulse({}))
|
|
104
|
-
);
|
|
105
|
-
server.registerTool(
|
|
106
|
-
"runboard_roadmap",
|
|
107
|
-
{ description: "Generate a Now/Next/Later plan from the binding constraint.", inputSchema: {} },
|
|
108
|
-
async () => json(handleRoadmap({}))
|
|
109
|
-
);
|
|
110
|
-
server.registerTool(
|
|
111
|
-
"runboard_report",
|
|
112
|
-
{
|
|
113
|
-
description: "Render a report (board-update | baseline | monthly).",
|
|
114
|
-
inputSchema: { type: z.enum(["board-update", "baseline", "monthly"]) }
|
|
115
|
-
},
|
|
116
|
-
async (args) => json(handleReport({ type: args.type }))
|
|
117
|
-
);
|
|
118
|
-
server.registerTool(
|
|
119
|
-
"runboard_status",
|
|
120
|
-
{ description: "One-screen current state.", inputSchema: {} },
|
|
121
|
-
async () => json(handleStatus({}))
|
|
122
|
-
);
|
|
123
|
-
return server;
|
|
124
|
-
}
|
|
125
|
-
async function main() {
|
|
126
|
-
const server = buildServer();
|
|
127
|
-
const transport = new StdioServerTransport();
|
|
128
|
-
await server.connect(transport);
|
|
129
|
-
}
|
|
130
|
-
main().catch((err) => {
|
|
6
|
+
// mcp/main.ts
|
|
7
|
+
startMcpServer().catch((err) => {
|
|
131
8
|
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|
|
132
9
|
`);
|
|
133
10
|
process.exit(1);
|
|
134
11
|
});
|
|
135
|
-
export {
|
|
136
|
-
buildServer
|
|
137
|
-
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "runboard",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Local-first CLI for the Runboard technical-leadership maturity framework. Deterministic scoring core, portable AI adapters, no phone-home.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"test:watch": "vitest",
|
|
29
29
|
"lint": "biome check .",
|
|
30
30
|
"format": "biome format --write .",
|
|
31
|
-
"
|
|
31
|
+
"mcp:smoke": "node scripts/mcp-smoke.mjs",
|
|
32
|
+
"prepublishOnly": "npm run lint && npm run typecheck && npm run test && npm run build && npm run mcp:smoke"
|
|
32
33
|
},
|
|
33
34
|
"dependencies": {
|
|
34
35
|
"@clack/prompts": "^0.7.0",
|