@stcrft/statecraft 1.1.2
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/README.md +43 -0
- package/dist/index.js +493 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -0
- package/spec.md +172 -0
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Statecraft CLI
|
|
2
|
+
|
|
3
|
+
Validate and summarize Statecraft board files from the terminal.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
- **`statecraft validate [path]`** — Parse and validate a board file. Exits 0 if valid, 1 on parse or validation errors.
|
|
8
|
+
- **`statecraft summarize [path]`** — Summarize a board. Prints a text summary to stdout.
|
|
9
|
+
- **`statecraft render [path]`** — Serve the board UI in the browser. Starts a local server that serves the built renderer, `GET /api/board` (board file from disk), and a WebSocket for live updates when the file changes. Options: `--port 3000` (default), `--open` (open browser).
|
|
10
|
+
|
|
11
|
+
## Path handling
|
|
12
|
+
|
|
13
|
+
- **Single path only.** One board file per run. Passing multiple paths (e.g. `statecraft validate a.yaml b.yaml`) is not supported and will exit with an error.
|
|
14
|
+
- **Default path:** If you omit the path, the CLI uses **`./board.yaml`** (relative to the current working directory).
|
|
15
|
+
- **Relative vs absolute:** The path is relative to the current working directory unless you pass an absolute path. The core library resolves paths internally, so the CLI passes your argument through as-is.
|
|
16
|
+
|
|
17
|
+
### Examples
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# Validate the default board (./board.yaml)
|
|
21
|
+
statecraft validate
|
|
22
|
+
|
|
23
|
+
# Validate a specific file
|
|
24
|
+
statecraft validate examples/board.yaml
|
|
25
|
+
statecraft validate /path/to/board.yaml
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Development
|
|
29
|
+
|
|
30
|
+
From the repo root:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pnpm build # build all packages (required before `render` so renderer dist exists)
|
|
34
|
+
pnpm cli validate # run the CLI (uses workspace script)
|
|
35
|
+
pnpm cli render --open # serve board UI (opens browser)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Or from this package:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pnpm build
|
|
42
|
+
node dist/index.js validate
|
|
43
|
+
```
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { createRequire } from "module";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
|
|
7
|
+
// src/constants.ts
|
|
8
|
+
var DEFAULT_BOARD_PATH = "./board.yaml";
|
|
9
|
+
var DEFAULT_RENDER_PORT = 3e3;
|
|
10
|
+
var RENDER_WATCH_DEBOUNCE_MS = 100;
|
|
11
|
+
var INIT_DEFAULT_BOARD_PATH = "board.yaml";
|
|
12
|
+
var INIT_DEFAULT_TASKS_DIR = "tasks";
|
|
13
|
+
var CANONICAL_COLUMNS = ["Backlog", "Ready", "In Progress", "Done"];
|
|
14
|
+
var SPEC_FILENAME = "spec.md";
|
|
15
|
+
|
|
16
|
+
// src/executors/init.ts
|
|
17
|
+
import fs from "fs";
|
|
18
|
+
import path from "path";
|
|
19
|
+
import readline from "readline";
|
|
20
|
+
import { stringify } from "yaml";
|
|
21
|
+
|
|
22
|
+
// src/executors/rule-content.ts
|
|
23
|
+
function buildStatecraftRuleBody(boardPath, tasksDir) {
|
|
24
|
+
return `# Statecraft
|
|
25
|
+
|
|
26
|
+
This project uses Statecraft for the task board.
|
|
27
|
+
|
|
28
|
+
- **Board file:** \`${boardPath}\`
|
|
29
|
+
- **Task spec files:** \`${tasksDir}/<task-id>.md\` (relative to board directory)
|
|
30
|
+
- **Columns (canonical):** Backlog \u2192 Ready \u2192 In Progress \u2192 Done.
|
|
31
|
+
|
|
32
|
+
## Commands
|
|
33
|
+
|
|
34
|
+
- Get board format spec: \`statecraft spec\`
|
|
35
|
+
- Validate board: \`statecraft validate ${boardPath}\`
|
|
36
|
+
- View board in browser: \`statecraft render ${boardPath}\`
|
|
37
|
+
|
|
38
|
+
## Task lifecycle (edit board and task files directly)
|
|
39
|
+
|
|
40
|
+
- **Prepare for work:** When the task has a clear definition and dependencies are satisfied, set \`status\` to **Ready**.
|
|
41
|
+
- **Start work:** Set the task's \`status\` to **In Progress**. Optionally open/read the task's \`spec\` file.
|
|
42
|
+
- **Finish work:** Set the task's \`status\` to **Done** only when the task's acceptance criteria (in its spec file) are satisfied.
|
|
43
|
+
- **Create task:** Add an entry under \`tasks\` with \`status: Backlog\` (id, title, optional description, spec, owner, priority, depends_on). If needed, create \`${tasksDir}/<task-id>.md\` with description and DoD.
|
|
44
|
+
|
|
45
|
+
## AI guidelines for creating tickets
|
|
46
|
+
|
|
47
|
+
- **Task naming:** kebab-case, verb or noun phrase (e.g. \`fix-auth-timeout\`).
|
|
48
|
+
- **Description:** One line summary; optional markdown for context.
|
|
49
|
+
- **Definition of Done:** Acceptance criteria in task spec; all checked before moving to Done.
|
|
50
|
+
- **Task fields (from spec):** \`title\` (required), \`status\` (required), optional \`description\`, \`spec\` (path to .md), \`owner\`, \`priority\`, \`depends_on\`.
|
|
51
|
+
- **Spec file:** Path relative to board directory, e.g. \`${tasksDir}/<task-id>.md\`.
|
|
52
|
+
`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/executors/init.ts
|
|
56
|
+
function question(rl, prompt, defaultValue) {
|
|
57
|
+
const suffix = defaultValue !== void 0 ? ` (default: ${defaultValue})` : "";
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
rl.question(`${prompt}${suffix}: `, (answer) => {
|
|
60
|
+
const trimmed = answer.trim();
|
|
61
|
+
resolve(trimmed !== "" ? trimmed : defaultValue ?? "");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async function getAnswer(rl, answers, index, prompt, defaultValue) {
|
|
66
|
+
if (answers !== void 0) {
|
|
67
|
+
const raw = answers[index.current++] ?? "";
|
|
68
|
+
const trimmed = raw.trim();
|
|
69
|
+
return trimmed !== "" ? trimmed : defaultValue ?? "";
|
|
70
|
+
}
|
|
71
|
+
return question(rl, prompt, defaultValue);
|
|
72
|
+
}
|
|
73
|
+
function parseWipLimit(wipStr) {
|
|
74
|
+
if (wipStr === "") return void 0;
|
|
75
|
+
const n = parseInt(wipStr, 10);
|
|
76
|
+
return Number.isInteger(n) && n >= 1 ? n : void 0;
|
|
77
|
+
}
|
|
78
|
+
async function collectInitAnswers(rl, answers, index) {
|
|
79
|
+
const boardName = await getAnswer(rl, answers, index, "Board name");
|
|
80
|
+
const boardPath = await getAnswer(rl, answers, index, "Path for board file", INIT_DEFAULT_BOARD_PATH);
|
|
81
|
+
const tasksDir = await getAnswer(
|
|
82
|
+
rl,
|
|
83
|
+
answers,
|
|
84
|
+
index,
|
|
85
|
+
"Directory for task .md files (relative to board)",
|
|
86
|
+
INIT_DEFAULT_TASKS_DIR
|
|
87
|
+
);
|
|
88
|
+
const wipStr = await getAnswer(
|
|
89
|
+
rl,
|
|
90
|
+
answers,
|
|
91
|
+
index,
|
|
92
|
+
"WIP limit for In Progress (optional, press Enter to skip)",
|
|
93
|
+
""
|
|
94
|
+
);
|
|
95
|
+
const genCursor = await getAnswer(rl, answers, index, "Generate Cursor rule? (Y/n)", "Y");
|
|
96
|
+
const genClaude = await getAnswer(rl, answers, index, "Generate Claude Code rule? (y/n)", "n");
|
|
97
|
+
const genCodex = await getAnswer(rl, answers, index, "Generate Codex instructions (AGENTS.md)? (y/n)", "n");
|
|
98
|
+
return {
|
|
99
|
+
boardName,
|
|
100
|
+
boardPath,
|
|
101
|
+
tasksDir,
|
|
102
|
+
wipStr,
|
|
103
|
+
generateCursorRule: /^y(es)?$/i.test(genCursor.trim()),
|
|
104
|
+
generateClaudeRule: /^y(es)?$/i.test(genClaude.trim()),
|
|
105
|
+
generateCodexAgents: /^y(es)?$/i.test(genCodex.trim())
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function buildBoardFromAnswers(boardName, tasksDir, wipStr) {
|
|
109
|
+
const inProgressLimit = parseWipLimit(wipStr);
|
|
110
|
+
const columns = [
|
|
111
|
+
CANONICAL_COLUMNS[0],
|
|
112
|
+
CANONICAL_COLUMNS[1],
|
|
113
|
+
inProgressLimit != null ? { name: CANONICAL_COLUMNS[2], limit: inProgressLimit } : CANONICAL_COLUMNS[2],
|
|
114
|
+
CANONICAL_COLUMNS[3]
|
|
115
|
+
];
|
|
116
|
+
return {
|
|
117
|
+
board: boardName,
|
|
118
|
+
columns,
|
|
119
|
+
tasks: {}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function ensureDirAndWrite(filePath, content) {
|
|
123
|
+
const dir = path.dirname(filePath);
|
|
124
|
+
if (!fs.existsSync(dir)) {
|
|
125
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
126
|
+
}
|
|
127
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
128
|
+
}
|
|
129
|
+
function writeBoardFile(cwd, boardPath, board) {
|
|
130
|
+
const absolutePath = path.resolve(cwd, boardPath);
|
|
131
|
+
const yamlContent = stringify(board, { lineWidth: 0 });
|
|
132
|
+
ensureDirAndWrite(absolutePath, yamlContent);
|
|
133
|
+
}
|
|
134
|
+
function writeRuleFile(filePath, content, log) {
|
|
135
|
+
ensureDirAndWrite(filePath, content);
|
|
136
|
+
log(filePath);
|
|
137
|
+
}
|
|
138
|
+
var CODECX_MARKER = "## Statecraft (generated by statecraft init)";
|
|
139
|
+
function writeCursorRule(cwd, boardPath, specDir, log) {
|
|
140
|
+
const cursorRulesDir = path.resolve(cwd, ".cursor", "rules");
|
|
141
|
+
const cursorRulePath = path.join(cursorRulesDir, "statecraft.mdc");
|
|
142
|
+
const content = buildCursorRuleContent(boardPath, specDir);
|
|
143
|
+
writeRuleFile(cursorRulePath, content, (p) => log(`Wrote Cursor rule to ${p}`));
|
|
144
|
+
}
|
|
145
|
+
function writeClaudeRule(cwd, boardPath, specDir, log) {
|
|
146
|
+
const claudeRulesDir = path.resolve(cwd, ".claude", "rules");
|
|
147
|
+
const claudeRulePath = path.join(claudeRulesDir, "statecraft.md");
|
|
148
|
+
const content = buildClaudeRuleContent(boardPath, specDir);
|
|
149
|
+
writeRuleFile(claudeRulePath, content, (p) => log(`Wrote Claude Code rule to ${p}`));
|
|
150
|
+
}
|
|
151
|
+
function writeCodexRule(cwd, boardPath, specDir, log) {
|
|
152
|
+
const codexAgentsPath = path.resolve(cwd, "AGENTS.md");
|
|
153
|
+
const codexContent = buildCodexAgentsContent(boardPath, specDir);
|
|
154
|
+
if (fs.existsSync(codexAgentsPath)) {
|
|
155
|
+
const existing = fs.readFileSync(codexAgentsPath, "utf-8");
|
|
156
|
+
if (existing.includes(CODECX_MARKER)) {
|
|
157
|
+
log("AGENTS.md already contains Statecraft section; skipped.");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
fs.writeFileSync(codexAgentsPath, existing.trimEnd() + "\n\n" + codexContent + "\n", "utf-8");
|
|
161
|
+
log(`Appended Statecraft section to ${codexAgentsPath}`);
|
|
162
|
+
} else {
|
|
163
|
+
fs.writeFileSync(codexAgentsPath, codexContent + "\n", "utf-8");
|
|
164
|
+
log(`Wrote Codex instructions to ${codexAgentsPath}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function buildCursorRuleContent(boardPath, tasksDir) {
|
|
168
|
+
return `---
|
|
169
|
+
description: Statecraft board and task workflow; when to update the board and how to create tasks
|
|
170
|
+
alwaysApply: false
|
|
171
|
+
---
|
|
172
|
+
${buildStatecraftRuleBody(boardPath, tasksDir)}`;
|
|
173
|
+
}
|
|
174
|
+
function buildClaudeRuleContent(boardPath, tasksDir) {
|
|
175
|
+
return buildStatecraftRuleBody(boardPath, tasksDir);
|
|
176
|
+
}
|
|
177
|
+
function buildCodexAgentsContent(boardPath, tasksDir) {
|
|
178
|
+
return `## Statecraft (generated by statecraft init)
|
|
179
|
+
|
|
180
|
+
${buildStatecraftRuleBody(boardPath, tasksDir)}`;
|
|
181
|
+
}
|
|
182
|
+
async function runInit(options) {
|
|
183
|
+
const answers = options?.answers;
|
|
184
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
185
|
+
const answerIndex = { current: 0 };
|
|
186
|
+
const rl = answers === void 0 ? readline.createInterface({ input: process.stdin, output: process.stdout }) : null;
|
|
187
|
+
const log = (msg) => {
|
|
188
|
+
if (rl) process.stdout.write(msg + "\n");
|
|
189
|
+
};
|
|
190
|
+
try {
|
|
191
|
+
if (rl) {
|
|
192
|
+
process.stdout.write("\nStatecraft init \u2014 create your board and connect it to your workflow.\n\n");
|
|
193
|
+
}
|
|
194
|
+
const collected = await collectInitAnswers(rl, answers, answerIndex);
|
|
195
|
+
if (!collected.boardName) {
|
|
196
|
+
process.stderr.write("Board name is required.\n");
|
|
197
|
+
process.exitCode = 1;
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
rl?.close();
|
|
201
|
+
const board = buildBoardFromAnswers(collected.boardName, collected.tasksDir, collected.wipStr);
|
|
202
|
+
writeBoardFile(cwd, collected.boardPath, board);
|
|
203
|
+
const absolutePath = path.resolve(cwd, collected.boardPath);
|
|
204
|
+
log(`
|
|
205
|
+
Created board at ${absolutePath}`);
|
|
206
|
+
log(`Task spec files: ${path.join(path.dirname(collected.boardPath), collected.tasksDir)}/<task-id>.md`);
|
|
207
|
+
const specDir = path.join(path.dirname(collected.boardPath), collected.tasksDir);
|
|
208
|
+
if (collected.generateCursorRule) writeCursorRule(cwd, collected.boardPath, specDir, log);
|
|
209
|
+
if (collected.generateClaudeRule) writeClaudeRule(cwd, collected.boardPath, specDir, log);
|
|
210
|
+
if (collected.generateCodexAgents) writeCodexRule(cwd, collected.boardPath, specDir, log);
|
|
211
|
+
log("\nRun `statecraft validate " + collected.boardPath + "` to validate, or `statecraft render " + collected.boardPath + "` to view.");
|
|
212
|
+
} catch (err) {
|
|
213
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
214
|
+
process.stderr.write(message + "\n");
|
|
215
|
+
process.exitCode = 1;
|
|
216
|
+
} finally {
|
|
217
|
+
rl?.close();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/render-server.ts
|
|
222
|
+
import express from "express";
|
|
223
|
+
import fs2 from "fs";
|
|
224
|
+
import path2 from "path";
|
|
225
|
+
import { createServer } from "http";
|
|
226
|
+
import { WebSocketServer } from "ws";
|
|
227
|
+
import { fileURLToPath } from "url";
|
|
228
|
+
var __dirname2 = path2.dirname(fileURLToPath(import.meta.url));
|
|
229
|
+
function getRendererDistPath() {
|
|
230
|
+
return path2.resolve(__dirname2, "..", "..", "renderer", "dist");
|
|
231
|
+
}
|
|
232
|
+
function readBoardContent(boardPath) {
|
|
233
|
+
try {
|
|
234
|
+
const resolved = path2.resolve(process.cwd(), boardPath);
|
|
235
|
+
return fs2.readFileSync(resolved, "utf-8");
|
|
236
|
+
} catch {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function startRenderServer(options) {
|
|
241
|
+
const { boardPath, port = DEFAULT_RENDER_PORT, openBrowser = false } = options;
|
|
242
|
+
const rendererDist = getRendererDistPath();
|
|
243
|
+
if (!fs2.existsSync(rendererDist) || !fs2.statSync(rendererDist).isDirectory()) {
|
|
244
|
+
process.stderr.write(
|
|
245
|
+
"Renderer build not found. Run: pnpm build\n"
|
|
246
|
+
);
|
|
247
|
+
process.exitCode = 1;
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const app = express();
|
|
251
|
+
const resolvedBoardPath = path2.resolve(process.cwd(), boardPath);
|
|
252
|
+
const boardDir = path2.dirname(resolvedBoardPath);
|
|
253
|
+
app.get("/api/board", (_req, res) => {
|
|
254
|
+
const content = readBoardContent(boardPath);
|
|
255
|
+
if (content === null) {
|
|
256
|
+
res.status(404).type("text/plain").send("Board file not found or unreadable.");
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
res.type("text/yaml").send(content);
|
|
260
|
+
});
|
|
261
|
+
app.get("/api/spec", (req, res) => {
|
|
262
|
+
const rawPath = typeof req.query.path === "string" ? req.query.path : "";
|
|
263
|
+
if (!rawPath || rawPath.includes("..")) {
|
|
264
|
+
res.status(400).type("text/plain").send("Invalid or missing path.");
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const ext = path2.extname(rawPath).toLowerCase();
|
|
268
|
+
if (ext !== ".md") {
|
|
269
|
+
res.status(400).type("text/plain").send("Only .md spec files are allowed.");
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const resolved = path2.resolve(boardDir, rawPath);
|
|
273
|
+
const relative = path2.relative(boardDir, resolved);
|
|
274
|
+
if (relative.startsWith("..") || path2.isAbsolute(relative)) {
|
|
275
|
+
res.status(400).type("text/plain").send("Path must be under board directory.");
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
const content = fs2.readFileSync(resolved, "utf-8");
|
|
280
|
+
res.type("text/markdown").send(content);
|
|
281
|
+
} catch {
|
|
282
|
+
res.status(404).type("text/plain").send("Spec file not found or unreadable.");
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
app.use(express.static(rendererDist));
|
|
286
|
+
app.use((_req, res, next) => {
|
|
287
|
+
if (_req.method !== "GET" || res.headersSent) return next();
|
|
288
|
+
const indexHtml = path2.join(rendererDist, "index.html");
|
|
289
|
+
if (fs2.existsSync(indexHtml)) {
|
|
290
|
+
res.sendFile(indexHtml);
|
|
291
|
+
} else {
|
|
292
|
+
res.status(404).send("Not found");
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
const server = createServer(app);
|
|
296
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
297
|
+
server.on("upgrade", (request, socket, head) => {
|
|
298
|
+
const url = new URL(request.url ?? "", `http://${request.headers.host}`);
|
|
299
|
+
if (url.pathname === "/api/board/watch") {
|
|
300
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
301
|
+
wss.emit("connection", ws, request);
|
|
302
|
+
});
|
|
303
|
+
} else {
|
|
304
|
+
socket.destroy();
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
let debounceTimer = null;
|
|
308
|
+
function broadcastBoard() {
|
|
309
|
+
const content = readBoardContent(boardPath);
|
|
310
|
+
const payload = content ?? "";
|
|
311
|
+
wss.clients.forEach((client) => {
|
|
312
|
+
if (client.readyState === 1) {
|
|
313
|
+
client.send(payload);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
wss.on("connection", (ws) => {
|
|
318
|
+
const content = readBoardContent(boardPath);
|
|
319
|
+
if (content !== null) {
|
|
320
|
+
ws.send(content);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
try {
|
|
324
|
+
fs2.watch(resolvedBoardPath, { persistent: false }, () => {
|
|
325
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
326
|
+
debounceTimer = setTimeout(() => {
|
|
327
|
+
debounceTimer = null;
|
|
328
|
+
broadcastBoard();
|
|
329
|
+
}, RENDER_WATCH_DEBOUNCE_MS);
|
|
330
|
+
});
|
|
331
|
+
} catch {
|
|
332
|
+
}
|
|
333
|
+
server.on("error", (err) => {
|
|
334
|
+
process.stderr.write(`Render server error: ${err.message}
|
|
335
|
+
`);
|
|
336
|
+
if (err.code === "EADDRINUSE") {
|
|
337
|
+
process.stderr.write(`Port ${port} is in use. Try --port <number>.
|
|
338
|
+
`);
|
|
339
|
+
}
|
|
340
|
+
process.exitCode = 1;
|
|
341
|
+
});
|
|
342
|
+
server.listen(port, () => {
|
|
343
|
+
const url = `http://localhost:${port}`;
|
|
344
|
+
process.stdout.write(`Open ${url}
|
|
345
|
+
`);
|
|
346
|
+
if (openBrowser) {
|
|
347
|
+
import("open").then(({ default: open }) => {
|
|
348
|
+
open(url).catch(() => {
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
const shutdown = () => {
|
|
354
|
+
server.close(() => {
|
|
355
|
+
process.exit(process.exitCode ?? 0);
|
|
356
|
+
});
|
|
357
|
+
};
|
|
358
|
+
process.on("SIGINT", shutdown);
|
|
359
|
+
process.on("SIGTERM", shutdown);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// src/executors/render.ts
|
|
363
|
+
function runRender(path5, options) {
|
|
364
|
+
startRenderServer({
|
|
365
|
+
boardPath: path5,
|
|
366
|
+
port: options.port,
|
|
367
|
+
openBrowser: options.open
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// src/executors/spec.ts
|
|
372
|
+
import fs4 from "fs";
|
|
373
|
+
import path4 from "path";
|
|
374
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
375
|
+
|
|
376
|
+
// src/utils.ts
|
|
377
|
+
import fs3 from "fs";
|
|
378
|
+
import path3 from "path";
|
|
379
|
+
function findPackageRoot(startDir) {
|
|
380
|
+
let dir = path3.resolve(startDir);
|
|
381
|
+
for (; ; ) {
|
|
382
|
+
const pkgPath = path3.join(dir, "package.json");
|
|
383
|
+
if (fs3.existsSync(pkgPath)) return dir;
|
|
384
|
+
const parent = path3.dirname(dir);
|
|
385
|
+
if (parent === dir) return null;
|
|
386
|
+
dir = parent;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// src/executors/spec.ts
|
|
391
|
+
function runSpec() {
|
|
392
|
+
const thisFile = fileURLToPath2(import.meta.url);
|
|
393
|
+
const startDir = path4.dirname(thisFile);
|
|
394
|
+
const packageRoot = findPackageRoot(startDir);
|
|
395
|
+
if (!packageRoot) {
|
|
396
|
+
process.stderr.write("statecraft spec: could not find package root\n");
|
|
397
|
+
process.exitCode = 1;
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const specPath = path4.join(packageRoot, SPEC_FILENAME);
|
|
401
|
+
if (!fs4.existsSync(specPath)) {
|
|
402
|
+
process.stderr.write(`statecraft spec: spec file not found at ${specPath}
|
|
403
|
+
`);
|
|
404
|
+
process.exitCode = 1;
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
try {
|
|
408
|
+
const content = fs4.readFileSync(specPath, "utf-8");
|
|
409
|
+
process.stdout.write(content);
|
|
410
|
+
if (!content.endsWith("\n")) process.stdout.write("\n");
|
|
411
|
+
process.exitCode = 0;
|
|
412
|
+
} catch (err) {
|
|
413
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
414
|
+
process.stderr.write(`statecraft spec: ${message}
|
|
415
|
+
`);
|
|
416
|
+
process.exitCode = 1;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/executors/summarize.ts
|
|
421
|
+
import { parseBoard, summarize } from "@stcrft/statecraft-core";
|
|
422
|
+
function runSummarize(path5) {
|
|
423
|
+
try {
|
|
424
|
+
const board = parseBoard(path5);
|
|
425
|
+
const summary = summarize(board);
|
|
426
|
+
process.stdout.write(summary);
|
|
427
|
+
process.exitCode = 0;
|
|
428
|
+
} catch (err) {
|
|
429
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
430
|
+
process.stderr.write(message + "\n");
|
|
431
|
+
process.exitCode = 1;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// src/executors/validate.ts
|
|
436
|
+
import { parseBoard as parseBoard2, validate } from "@stcrft/statecraft-core";
|
|
437
|
+
function formatValidationError(err) {
|
|
438
|
+
const prefix = err.path != null && err.path !== "" ? `${err.path}: ` : "";
|
|
439
|
+
return `${prefix}${err.message}`;
|
|
440
|
+
}
|
|
441
|
+
function runValidate(path5) {
|
|
442
|
+
try {
|
|
443
|
+
const board = parseBoard2(path5);
|
|
444
|
+
const result = validate(board);
|
|
445
|
+
if (!result.valid) {
|
|
446
|
+
for (const err of result.errors) {
|
|
447
|
+
process.stderr.write(formatValidationError(err) + "\n");
|
|
448
|
+
}
|
|
449
|
+
process.exitCode = 1;
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
process.exitCode = 0;
|
|
453
|
+
} catch (err) {
|
|
454
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
455
|
+
process.stderr.write(message + "\n");
|
|
456
|
+
process.exitCode = 1;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/index.ts
|
|
461
|
+
var require2 = createRequire(import.meta.url);
|
|
462
|
+
var { version } = require2("../package.json");
|
|
463
|
+
function rejectMultiplePaths(extra) {
|
|
464
|
+
if (extra.length > 0) {
|
|
465
|
+
process.stderr.write("Only one board file per run. Multiple paths are not supported.\n");
|
|
466
|
+
process.exitCode = 1;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
var program = new Command();
|
|
470
|
+
program.name("statecraft").description("Validate, summarize, and render Statecraft board files").version(version);
|
|
471
|
+
program.command("init").description("Interactive setup: create board and configure Statecraft for your workflow").action(async () => {
|
|
472
|
+
await runInit().catch((err) => {
|
|
473
|
+
process.stderr.write(err instanceof Error ? err.message : String(err) + "\n");
|
|
474
|
+
process.exitCode = 1;
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
program.command("spec").description("Print the board format spec (for AI agents)").action(() => {
|
|
478
|
+
runSpec();
|
|
479
|
+
});
|
|
480
|
+
program.command("validate").description("Validate a board file (exit 0 if valid, 1 on errors)").argument("[path]", "path to board file", DEFAULT_BOARD_PATH).argument("[extra...]", "ignored (only one path allowed)").action((path5, extra) => {
|
|
481
|
+
rejectMultiplePaths(extra);
|
|
482
|
+
if (process.exitCode !== 1) runValidate(path5);
|
|
483
|
+
});
|
|
484
|
+
program.command("summarize").description("Print a short text summary of the board").argument("[path]", "path to board file", DEFAULT_BOARD_PATH).argument("[extra...]", "ignored (only one path allowed)").action((path5, extra) => {
|
|
485
|
+
rejectMultiplePaths(extra);
|
|
486
|
+
if (process.exitCode !== 1) runSummarize(path5);
|
|
487
|
+
});
|
|
488
|
+
program.command("render").description("Serve the board in the browser (read-only UI)").argument("[path]", "path to board file", DEFAULT_BOARD_PATH).option("-p, --port <number>", "port for the server", String(DEFAULT_RENDER_PORT)).option("--open", "open browser after starting server").action((path5, options) => {
|
|
489
|
+
const port = parseInt(options.port, 10) || DEFAULT_RENDER_PORT;
|
|
490
|
+
runRender(path5, { port, open: options.open ?? false });
|
|
491
|
+
});
|
|
492
|
+
program.parseAsync();
|
|
493
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/constants.ts","../src/executors/init.ts","../src/executors/rule-content.ts","../src/render-server.ts","../src/executors/render.ts","../src/executors/spec.ts","../src/utils.ts","../src/executors/summarize.ts","../src/executors/validate.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { createRequire } from \"node:module\";\nimport { Command } from \"commander\";\nimport { DEFAULT_BOARD_PATH, DEFAULT_RENDER_PORT } from \"./constants.js\";\nimport { runInit, runRender, runSpec, runSummarize, runValidate } from \"./executors/index.js\";\n\nconst require = createRequire(import.meta.url);\nconst { version } = require(\"../package.json\") as { version: string };\n\nfunction rejectMultiplePaths(extra: string[]): void {\n if (extra.length > 0) {\n process.stderr.write(\"Only one board file per run. Multiple paths are not supported.\\n\");\n process.exitCode = 1;\n }\n}\n\nconst program = new Command();\n\nprogram\n .name(\"statecraft\")\n .description(\"Validate, summarize, and render Statecraft board files\")\n .version(version);\n\nprogram\n .command(\"init\")\n .description(\"Interactive setup: create board and configure Statecraft for your workflow\")\n .action(async () => {\n await runInit().catch((err) => {\n process.stderr.write(err instanceof Error ? err.message : String(err) + \"\\n\");\n process.exitCode = 1;\n });\n });\n\nprogram\n .command(\"spec\")\n .description(\"Print the board format spec (for AI agents)\")\n .action(() => {\n runSpec();\n });\n\nprogram\n .command(\"validate\")\n .description(\"Validate a board file (exit 0 if valid, 1 on errors)\")\n .argument(\"[path]\", \"path to board file\", DEFAULT_BOARD_PATH)\n .argument(\"[extra...]\", \"ignored (only one path allowed)\")\n .action((path: string, extra: string[]) => {\n rejectMultiplePaths(extra);\n if (process.exitCode !== 1) runValidate(path);\n });\n\nprogram\n .command(\"summarize\")\n .description(\"Print a short text summary of the board\")\n .argument(\"[path]\", \"path to board file\", DEFAULT_BOARD_PATH)\n .argument(\"[extra...]\", \"ignored (only one path allowed)\")\n .action((path: string, extra: string[]) => {\n rejectMultiplePaths(extra);\n if (process.exitCode !== 1) runSummarize(path);\n });\n\nprogram\n .command(\"render\")\n .description(\"Serve the board in the browser (read-only UI)\")\n .argument(\"[path]\", \"path to board file\", DEFAULT_BOARD_PATH)\n .option(\"-p, --port <number>\", \"port for the server\", String(DEFAULT_RENDER_PORT))\n .option(\"--open\", \"open browser after starting server\")\n .action((path: string, options: { port: string; open: boolean }) => {\n const port = parseInt(options.port, 10) || DEFAULT_RENDER_PORT;\n runRender(path, { port, open: options.open ?? false });\n });\n\nprogram.parseAsync();\n","/** Default path to the board file when none is given (validate, summarize, render). */\nexport const DEFAULT_BOARD_PATH = \"./board.yaml\";\n\n/** Default port for the render server. */\nexport const DEFAULT_RENDER_PORT = 3000;\n\n/** Debounce delay (ms) for file watcher before broadcasting board updates to WebSocket clients. */\nexport const RENDER_WATCH_DEBOUNCE_MS = 100;\n\n/** Default board file path offered by init (cwd). */\nexport const INIT_DEFAULT_BOARD_PATH = \"board.yaml\";\n\n/** Default directory for task .md files (relative to board), offered by init. */\nexport const INIT_DEFAULT_TASKS_DIR = \"tasks\";\n\n/** Canonical column set per spec; init creates boards with these columns. */\nexport const CANONICAL_COLUMNS = [\"Backlog\", \"Ready\", \"In Progress\", \"Done\"] as const;\n\n/** Filename of the board format spec shipped with the CLI package (statecraft spec). */\nexport const SPEC_FILENAME = \"spec.md\";\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport readline from \"node:readline\";\nimport { stringify } from \"yaml\";\nimport {\n CANONICAL_COLUMNS,\n INIT_DEFAULT_BOARD_PATH,\n INIT_DEFAULT_TASKS_DIR,\n} from \"../constants.js\";\nimport { buildStatecraftRuleBody } from \"./rule-content.js\";\n\nexport interface InitAnswers {\n boardName: string;\n columns: Array<{ name: string; limit?: number }>;\n boardPath: string;\n tasksDir: string;\n}\n\n/** Raw answers collected from prompts (before parsing WIP or booleans). */\ninterface CollectedInitAnswers {\n boardName: string;\n boardPath: string;\n tasksDir: string;\n wipStr: string;\n generateCursorRule: boolean;\n generateClaudeRule: boolean;\n generateCodexAgents: boolean;\n}\n\nfunction question(rl: readline.Interface, prompt: string, defaultValue?: string): Promise<string> {\n const suffix = defaultValue !== undefined ? ` (default: ${defaultValue})` : \"\";\n return new Promise((resolve) => {\n rl.question(`${prompt}${suffix}: `, (answer) => {\n const trimmed = answer.trim();\n resolve(trimmed !== \"\" ? trimmed : (defaultValue ?? \"\"));\n });\n });\n}\n\nasync function getAnswer(\n rl: readline.Interface | null,\n answers: string[] | undefined,\n index: { current: number },\n prompt: string,\n defaultValue?: string\n): Promise<string> {\n if (answers !== undefined) {\n const raw = answers[index.current++] ?? \"\";\n const trimmed = raw.trim();\n return trimmed !== \"\" ? trimmed : (defaultValue ?? \"\");\n }\n return question(rl!, prompt, defaultValue);\n}\n\nfunction parseWipLimit(wipStr: string): number | undefined {\n if (wipStr === \"\") return undefined;\n const n = parseInt(wipStr, 10);\n return Number.isInteger(n) && n >= 1 ? n : undefined;\n}\n\n/** Collect all init prompts into a structured object. */\nasync function collectInitAnswers(\n rl: readline.Interface | null,\n answers: string[] | undefined,\n index: { current: number }\n): Promise<CollectedInitAnswers> {\n const boardName = await getAnswer(rl, answers, index, \"Board name\");\n const boardPath = await getAnswer(rl, answers, index, \"Path for board file\", INIT_DEFAULT_BOARD_PATH);\n const tasksDir = await getAnswer(\n rl,\n answers,\n index,\n \"Directory for task .md files (relative to board)\",\n INIT_DEFAULT_TASKS_DIR\n );\n const wipStr = await getAnswer(\n rl,\n answers,\n index,\n \"WIP limit for In Progress (optional, press Enter to skip)\",\n \"\"\n );\n const genCursor = await getAnswer(rl, answers, index, \"Generate Cursor rule? (Y/n)\", \"Y\");\n const genClaude = await getAnswer(rl, answers, index, \"Generate Claude Code rule? (y/n)\", \"n\");\n const genCodex = await getAnswer(rl, answers, index, \"Generate Codex instructions (AGENTS.md)? (y/n)\", \"n\");\n\n return {\n boardName,\n boardPath,\n tasksDir,\n wipStr,\n generateCursorRule: /^y(es)?$/i.test(genCursor.trim()),\n generateClaudeRule: /^y(es)?$/i.test(genClaude.trim()),\n generateCodexAgents: /^y(es)?$/i.test(genCodex.trim()),\n };\n}\n\n/** Build the board object (name, columns, empty tasks) from collected answers. */\nfunction buildBoardFromAnswers(\n boardName: string,\n tasksDir: string,\n wipStr: string\n): { board: string; columns: Array<string | { name: string; limit: number }>; tasks: Record<string, never> } {\n const inProgressLimit = parseWipLimit(wipStr);\n const columns: Array<string | { name: string; limit: number }> = [\n CANONICAL_COLUMNS[0],\n CANONICAL_COLUMNS[1],\n inProgressLimit != null ? { name: CANONICAL_COLUMNS[2], limit: inProgressLimit } : CANONICAL_COLUMNS[2],\n CANONICAL_COLUMNS[3],\n ];\n return {\n board: boardName,\n columns,\n tasks: {},\n };\n}\n\n/** Ensure parent directory exists and write file. */\nfunction ensureDirAndWrite(filePath: string, content: string): void {\n const dir = path.dirname(filePath);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n fs.writeFileSync(filePath, content, \"utf-8\");\n}\n\n/** Write board YAML to cwd-relative path. */\nfunction writeBoardFile(\n cwd: string,\n boardPath: string,\n board: { board: string; columns: unknown[]; tasks: object }\n): void {\n const absolutePath = path.resolve(cwd, boardPath);\n const yamlContent = stringify(board, { lineWidth: 0 });\n ensureDirAndWrite(absolutePath, yamlContent);\n}\n\n/** Write a rule file and optionally log. */\nfunction writeRuleFile(\n filePath: string,\n content: string,\n log: (msg: string) => void\n): void {\n ensureDirAndWrite(filePath, content);\n log(filePath);\n}\n\nconst CODECX_MARKER = \"## Statecraft (generated by statecraft init)\";\n\nfunction writeCursorRule(cwd: string, boardPath: string, specDir: string, log: (msg: string) => void): void {\n const cursorRulesDir = path.resolve(cwd, \".cursor\", \"rules\");\n const cursorRulePath = path.join(cursorRulesDir, \"statecraft.mdc\");\n const content = buildCursorRuleContent(boardPath, specDir);\n writeRuleFile(cursorRulePath, content, (p) => log(`Wrote Cursor rule to ${p}`));\n}\n\nfunction writeClaudeRule(cwd: string, boardPath: string, specDir: string, log: (msg: string) => void): void {\n const claudeRulesDir = path.resolve(cwd, \".claude\", \"rules\");\n const claudeRulePath = path.join(claudeRulesDir, \"statecraft.md\");\n const content = buildClaudeRuleContent(boardPath, specDir);\n writeRuleFile(claudeRulePath, content, (p) => log(`Wrote Claude Code rule to ${p}`));\n}\n\nfunction writeCodexRule(cwd: string, boardPath: string, specDir: string, log: (msg: string) => void): void {\n const codexAgentsPath = path.resolve(cwd, \"AGENTS.md\");\n const codexContent = buildCodexAgentsContent(boardPath, specDir);\n if (fs.existsSync(codexAgentsPath)) {\n const existing = fs.readFileSync(codexAgentsPath, \"utf-8\");\n if (existing.includes(CODECX_MARKER)) {\n log(\"AGENTS.md already contains Statecraft section; skipped.\");\n return;\n }\n fs.writeFileSync(codexAgentsPath, existing.trimEnd() + \"\\n\\n\" + codexContent + \"\\n\", \"utf-8\");\n log(`Appended Statecraft section to ${codexAgentsPath}`);\n } else {\n fs.writeFileSync(codexAgentsPath, codexContent + \"\\n\", \"utf-8\");\n log(`Wrote Codex instructions to ${codexAgentsPath}`);\n }\n}\n\n// --- Public rule content builders (used by init and by tests) ---\n\nexport function buildCursorRuleContent(boardPath: string, tasksDir: string): string {\n return `---\ndescription: Statecraft board and task workflow; when to update the board and how to create tasks\nalwaysApply: false\n---\n${buildStatecraftRuleBody(boardPath, tasksDir)}`;\n}\n\n/** Claude Code: modular rule in .claude/rules/ (markdown, no frontmatter). */\nexport function buildClaudeRuleContent(boardPath: string, tasksDir: string): string {\n return buildStatecraftRuleBody(boardPath, tasksDir);\n}\n\n/** Codex: section for AGENTS.md at project root. */\nexport function buildCodexAgentsContent(boardPath: string, tasksDir: string): string {\n return `## Statecraft (generated by statecraft init)\n\n${buildStatecraftRuleBody(boardPath, tasksDir)}`;\n}\n\n// --- runInit ---\n\nexport interface RunInitOptions {\n /** For tests: pre-filled answers (board name, board path, tasks dir, WIP, Cursor y/n, Claude y/n, Codex y/n). */\n answers?: string[];\n /** For tests: working directory for writing board and rule files (default: process.cwd()). */\n cwd?: string;\n}\n\nexport async function runInit(options?: RunInitOptions): Promise<void> {\n const answers = options?.answers;\n const cwd = options?.cwd ?? process.cwd();\n const answerIndex = { current: 0 };\n const rl = answers === undefined ? readline.createInterface({ input: process.stdin, output: process.stdout }) : null;\n const log = (msg: string) => {\n if (rl) process.stdout.write(msg + \"\\n\");\n };\n\n try {\n if (rl) {\n process.stdout.write(\"\\nStatecraft init — create your board and connect it to your workflow.\\n\\n\");\n }\n\n const collected = await collectInitAnswers(rl, answers, answerIndex);\n if (!collected.boardName) {\n process.stderr.write(\"Board name is required.\\n\");\n process.exitCode = 1;\n return;\n }\n\n rl?.close();\n\n const board = buildBoardFromAnswers(collected.boardName, collected.tasksDir, collected.wipStr);\n writeBoardFile(cwd, collected.boardPath, board);\n\n const absolutePath = path.resolve(cwd, collected.boardPath);\n log(`\\nCreated board at ${absolutePath}`);\n log(`Task spec files: ${path.join(path.dirname(collected.boardPath), collected.tasksDir)}/<task-id>.md`);\n\n const specDir = path.join(path.dirname(collected.boardPath), collected.tasksDir);\n\n if (collected.generateCursorRule) writeCursorRule(cwd, collected.boardPath, specDir, log);\n if (collected.generateClaudeRule) writeClaudeRule(cwd, collected.boardPath, specDir, log);\n if (collected.generateCodexAgents) writeCodexRule(cwd, collected.boardPath, specDir, log);\n\n log(\"\\nRun `statecraft validate \" + collected.boardPath + \"` to validate, or `statecraft render \" + collected.boardPath + \"` to view.\");\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(message + \"\\n\");\n process.exitCode = 1;\n } finally {\n rl?.close();\n }\n}\n","/**\n * Shared content for generated AI rules (Cursor, Claude Code, Codex).\n * Single source for the Statecraft rule body so init only orchestrates.\n */\n\nexport function buildStatecraftRuleBody(boardPath: string, tasksDir: string): string {\n return `# Statecraft\n\nThis project uses Statecraft for the task board.\n\n- **Board file:** \\`${boardPath}\\`\n- **Task spec files:** \\`${tasksDir}/<task-id>.md\\` (relative to board directory)\n- **Columns (canonical):** Backlog → Ready → In Progress → Done.\n\n## Commands\n\n- Get board format spec: \\`statecraft spec\\`\n- Validate board: \\`statecraft validate ${boardPath}\\`\n- View board in browser: \\`statecraft render ${boardPath}\\`\n\n## Task lifecycle (edit board and task files directly)\n\n- **Prepare for work:** When the task has a clear definition and dependencies are satisfied, set \\`status\\` to **Ready**.\n- **Start work:** Set the task's \\`status\\` to **In Progress**. Optionally open/read the task's \\`spec\\` file.\n- **Finish work:** Set the task's \\`status\\` to **Done** only when the task's acceptance criteria (in its spec file) are satisfied.\n- **Create task:** Add an entry under \\`tasks\\` with \\`status: Backlog\\` (id, title, optional description, spec, owner, priority, depends_on). If needed, create \\`${tasksDir}/<task-id>.md\\` with description and DoD.\n\n## AI guidelines for creating tickets\n\n- **Task naming:** kebab-case, verb or noun phrase (e.g. \\`fix-auth-timeout\\`).\n- **Description:** One line summary; optional markdown for context.\n- **Definition of Done:** Acceptance criteria in task spec; all checked before moving to Done.\n- **Task fields (from spec):** \\`title\\` (required), \\`status\\` (required), optional \\`description\\`, \\`spec\\` (path to .md), \\`owner\\`, \\`priority\\`, \\`depends_on\\`.\n- **Spec file:** Path relative to board directory, e.g. \\`${tasksDir}/<task-id>.md\\`.\n`;\n}\n","import express, { type Request, type Response } from \"express\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { createServer } from \"node:http\";\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport { fileURLToPath } from \"node:url\";\nimport { DEFAULT_RENDER_PORT, RENDER_WATCH_DEBOUNCE_MS } from \"./constants.js\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\n/**\n * Resolve path to the renderer's dist folder (sibling package in monorepo).\n * When running from packages/cli/dist, go up to packages and into renderer/dist.\n */\nfunction getRendererDistPath(): string {\n return path.resolve(__dirname, \"..\", \"..\", \"renderer\", \"dist\");\n}\n\n/**\n * Read board file and return content as UTF-8. Returns null on error.\n */\nfunction readBoardContent(boardPath: string): string | null {\n try {\n const resolved = path.resolve(process.cwd(), boardPath);\n return fs.readFileSync(resolved, \"utf-8\");\n } catch {\n return null;\n }\n}\n\nexport interface RenderServerOptions {\n boardPath: string;\n port?: number;\n openBrowser?: boolean;\n}\n\n/**\n * Start the render server: static app + GET /api/board + WebSocket /api/board/watch.\n * Watches the board file and broadcasts content to WS clients on change.\n */\nexport function startRenderServer(options: RenderServerOptions): void {\n const { boardPath, port = DEFAULT_RENDER_PORT, openBrowser = false } = options;\n const rendererDist = getRendererDistPath();\n\n if (!fs.existsSync(rendererDist) || !fs.statSync(rendererDist).isDirectory()) {\n process.stderr.write(\n \"Renderer build not found. Run: pnpm build\\n\"\n );\n process.exitCode = 1;\n return;\n }\n\n const app = express();\n\n // Board file directory (for resolving spec paths relative to board)\n const resolvedBoardPath = path.resolve(process.cwd(), boardPath);\n const boardDir = path.dirname(resolvedBoardPath);\n\n // API: board file content (raw YAML)\n app.get(\"/api/board\", (_req: Request, res: Response) => {\n const content = readBoardContent(boardPath);\n if (content === null) {\n res.status(404).type(\"text/plain\").send(\"Board file not found or unreadable.\");\n return;\n }\n res.type(\"text/yaml\").send(content);\n });\n\n // API: spec file content (path relative to board file directory; only .md files)\n app.get(\"/api/spec\", (req: Request, res: Response) => {\n const rawPath = typeof req.query.path === \"string\" ? req.query.path : \"\";\n if (!rawPath || rawPath.includes(\"..\")) {\n res.status(400).type(\"text/plain\").send(\"Invalid or missing path.\");\n return;\n }\n const ext = path.extname(rawPath).toLowerCase();\n if (ext !== \".md\") {\n res.status(400).type(\"text/plain\").send(\"Only .md spec files are allowed.\");\n return;\n }\n const resolved = path.resolve(boardDir, rawPath);\n const relative = path.relative(boardDir, resolved);\n if (relative.startsWith(\"..\") || path.isAbsolute(relative)) {\n res.status(400).type(\"text/plain\").send(\"Path must be under board directory.\");\n return;\n }\n try {\n const content = fs.readFileSync(resolved, \"utf-8\");\n res.type(\"text/markdown\").send(content);\n } catch {\n res.status(404).type(\"text/plain\").send(\"Spec file not found or unreadable.\");\n }\n });\n\n // Static files (must be after /api routes so they take precedence)\n app.use(express.static(rendererDist));\n\n // SPA fallback: serve index.html for GET requests not handled by static (Express 5 / path-to-regexp v8 reject bare '*')\n app.use((_req: Request, res: Response, next: express.NextFunction) => {\n if (_req.method !== \"GET\" || res.headersSent) return next();\n const indexHtml = path.join(rendererDist, \"index.html\");\n if (fs.existsSync(indexHtml)) {\n res.sendFile(indexHtml);\n } else {\n res.status(404).send(\"Not found\");\n }\n });\n\n const server = createServer(app);\n\n // WebSocket: /api/board/watch — broadcast board content on file change\n const wss = new WebSocketServer({ noServer: true });\n\n server.on(\"upgrade\", (request, socket, head) => {\n const url = new URL(request.url ?? \"\", `http://${request.headers.host}`);\n if (url.pathname === \"/api/board/watch\") {\n wss.handleUpgrade(request, socket, head, (ws: WebSocket) => {\n wss.emit(\"connection\", ws, request);\n });\n } else {\n socket.destroy();\n }\n });\n\n let debounceTimer: ReturnType<typeof setTimeout> | null = null;\n\n function broadcastBoard(): void {\n const content = readBoardContent(boardPath);\n const payload = content ?? \"\";\n wss.clients.forEach((client: WebSocket) => {\n if (client.readyState === 1) {\n client.send(payload);\n }\n });\n }\n\n wss.on(\"connection\", (ws: WebSocket) => {\n // Send current board on connect\n const content = readBoardContent(boardPath);\n if (content !== null) {\n ws.send(content);\n }\n });\n\n try {\n fs.watch(resolvedBoardPath, { persistent: false }, () => {\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n debounceTimer = null;\n broadcastBoard();\n }, RENDER_WATCH_DEBOUNCE_MS);\n });\n } catch {\n // File might not exist yet; watcher will not run\n }\n\n server.on(\"error\", (err: Error & { code?: string }) => {\n process.stderr.write(`Render server error: ${err.message}\\n`);\n if (err.code === \"EADDRINUSE\") {\n process.stderr.write(`Port ${port} is in use. Try --port <number>.\\n`);\n }\n process.exitCode = 1;\n });\n\n server.listen(port, () => {\n const url = `http://localhost:${port}`;\n process.stdout.write(`Open ${url}\\n`);\n if (openBrowser) {\n import(\"open\").then(({ default: open }) => {\n open(url).catch(() => {});\n });\n }\n });\n\n const shutdown = () => {\n server.close(() => {\n process.exit(process.exitCode ?? 0);\n });\n };\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n}\n","import { startRenderServer } from \"../render-server.js\";\n\nexport function runRender(path: string, options: { port: number; open: boolean }): void {\n startRenderServer({\n boardPath: path,\n port: options.port,\n openBrowser: options.open,\n });\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { SPEC_FILENAME } from \"../constants.js\";\nimport { findPackageRoot } from \"../utils.js\";\n\nexport function runSpec(): void {\n const thisFile = fileURLToPath(import.meta.url);\n const startDir = path.dirname(thisFile);\n const packageRoot = findPackageRoot(startDir);\n if (!packageRoot) {\n process.stderr.write(\"statecraft spec: could not find package root\\n\");\n process.exitCode = 1;\n return;\n }\n const specPath = path.join(packageRoot, SPEC_FILENAME);\n if (!fs.existsSync(specPath)) {\n process.stderr.write(`statecraft spec: spec file not found at ${specPath}\\n`);\n process.exitCode = 1;\n return;\n }\n try {\n const content = fs.readFileSync(specPath, \"utf-8\");\n process.stdout.write(content);\n if (!content.endsWith(\"\\n\")) process.stdout.write(\"\\n\");\n process.exitCode = 0;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`statecraft spec: ${message}\\n`);\n process.exitCode = 1;\n }\n}\n","/**\n * Shared CLI utilities (path resolution, etc.).\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\n/**\n * Walk up from startDir until a directory containing package.json is found.\n * @returns Absolute path to package root, or null if not found.\n */\nexport function findPackageRoot(startDir: string): string | null {\n let dir = path.resolve(startDir);\n for (;;) {\n const pkgPath = path.join(dir, \"package.json\");\n if (fs.existsSync(pkgPath)) return dir;\n const parent = path.dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n","import { parseBoard, summarize } from \"@stcrft/statecraft-core\";\n\nexport function runSummarize(path: string): void {\n try {\n const board = parseBoard(path);\n const summary = summarize(board);\n process.stdout.write(summary);\n process.exitCode = 0;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(message + \"\\n\");\n process.exitCode = 1;\n }\n}\n","import { parseBoard, validate } from \"@stcrft/statecraft-core\";\n\nfunction formatValidationError(err: { message: string; path?: string }): string {\n const prefix = err.path != null && err.path !== \"\" ? `${err.path}: ` : \"\";\n return `${prefix}${err.message}`;\n}\n\nexport function runValidate(path: string): void {\n try {\n const board = parseBoard(path);\n const result = validate(board);\n if (!result.valid) {\n for (const err of result.errors) {\n process.stderr.write(formatValidationError(err) + \"\\n\");\n }\n process.exitCode = 1;\n return;\n }\n process.exitCode = 0;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(message + \"\\n\");\n process.exitCode = 1;\n }\n}\n"],"mappings":"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","names":["fs","path","__dirname","path","fs","path","fs","path","fileURLToPath","fs","path","fileURLToPath","path","fs","path","parseBoard","path","require","path"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stcrft/statecraft",
|
|
3
|
+
"version": "1.1.2",
|
|
4
|
+
"description": "CLI for Statecraft: validate, summarize, and render board-as-code",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/KristijanS99/statecraft.git"
|
|
9
|
+
},
|
|
10
|
+
"license": "GPL-3.0-only",
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=20"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"statecraft": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"spec.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "node scripts/copy-spec.mjs && tsup",
|
|
23
|
+
"test": "vitest run"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@stcrft/statecraft-core": "workspace:*",
|
|
27
|
+
"commander": "^12.1.0",
|
|
28
|
+
"express": "^5.2.1",
|
|
29
|
+
"open": "^11.0.0",
|
|
30
|
+
"ws": "^8.18.0",
|
|
31
|
+
"yaml": "^2.8.2"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/express": "^5.0.0",
|
|
35
|
+
"@types/node": "^22.0.0",
|
|
36
|
+
"@types/ws": "^8.5.0",
|
|
37
|
+
"tsup": "^8.3.5",
|
|
38
|
+
"typescript": "^5.7.2",
|
|
39
|
+
"vitest": "^4.0.18"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/spec.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Statecraft board DSL — Spec (v0)
|
|
2
|
+
|
|
3
|
+
This document defines the Statecraft board format. A single file describes one Kanban-style board in YAML. The file is the source of truth for AI agents and developers; tools (CLI, renderer) read and validate it.
|
|
4
|
+
|
|
5
|
+
**One board per file.** Use `.yaml` or `.yml`. Convention: name the file e.g. `board.yaml`, or pass the path explicitly to tools.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
- **Format:** YAML (UTF-8).
|
|
12
|
+
- **Scope:** One board per file. Top-level keys: `board`, `columns`, `tasks`.
|
|
13
|
+
- **Purpose:** Machine-readable task state: columns, tasks, status, dependencies, optional WIP limits. Designed for AI agents to read, write, and update; versioned in Git; developers can work with the same files.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Board
|
|
18
|
+
|
|
19
|
+
**Required.** Identifies the board.
|
|
20
|
+
|
|
21
|
+
| Key | Type | Required | Description |
|
|
22
|
+
|---------|--------|----------|----------------------------|
|
|
23
|
+
| `board` | string | Yes | Board name (human-readable label). |
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Columns
|
|
28
|
+
|
|
29
|
+
**Required.** The board **must** use the following columns in this exact order. This is the only supported workflow; it gives AI agents a single, unambiguous contract.
|
|
30
|
+
|
|
31
|
+
### Canonical column set
|
|
32
|
+
|
|
33
|
+
| Column | Meaning for AI |
|
|
34
|
+
|----------------|----------------|
|
|
35
|
+
| **Backlog** | Not yet selected for immediate work. New tasks are created here. May be unrefined or unprioritized. |
|
|
36
|
+
| **Ready** | Prepared and selected to be worked on next. Definition is clear (e.g. in spec file), dependencies met or N/A; safe to move to In Progress when capacity is free. |
|
|
37
|
+
| **In Progress** | Currently being worked on. Move a task here when starting work; at most one (or a configurable WIP limit) at a time per column if enforced. |
|
|
38
|
+
| **Done** | Completed and accepted. Move here when the task’s acceptance criteria (in its spec file) are satisfied. |
|
|
39
|
+
|
|
40
|
+
- **Order:** `Backlog` → `Ready` → `In Progress` → `Done` (left to right).
|
|
41
|
+
- **Syntax:** Each column is either a string (name only) or an object with `name` and optional `limit`. The `limit` (positive integer) is a WIP limit: max tasks allowed in that column. Typically only **In Progress** has a limit (e.g. `{ name: "In Progress", limit: 3 }`).
|
|
42
|
+
- Validators must reject boards whose `columns` do not match this set (same names, same order). Column names are case-sensitive.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Tasks
|
|
47
|
+
|
|
48
|
+
**Required** (but may be an empty object). Map of task id → task. Task id is a string key (e.g. `AUTH-12`, `task-1`). Each task has the following fields:
|
|
49
|
+
|
|
50
|
+
| Field | Type | Required | Description |
|
|
51
|
+
|--------------|--------|----------|-------------|
|
|
52
|
+
| `title` | string | Yes | Short description of the task. |
|
|
53
|
+
| `status` | string | Yes | Current column; must match one of the column names. |
|
|
54
|
+
| `description` | string | No | Inline summary or context; may be markdown. Use for one-liners. |
|
|
55
|
+
| `spec` | string | No | Path to a markdown file (e.g. `.md`) with full task spec, acceptance criteria, notes. Path is **relative to the board file's directory**. AI agents can read this file for context. |
|
|
56
|
+
| `owner` | string | No | Assignee (e.g. username or agent id). |
|
|
57
|
+
| `priority` | string | No | e.g. `low`, `medium`, `high`. Semantics are tool-specific. |
|
|
58
|
+
| `depends_on` | string or list of strings | No | Task id(s) that must be completed before this task; single id or list. |
|
|
59
|
+
|
|
60
|
+
- Task ids must be unique within the board.
|
|
61
|
+
- `status` must equal one of the column names from `columns`.
|
|
62
|
+
- `depends_on` must reference task ids that exist in `tasks` (validation rule).
|
|
63
|
+
- `spec`: resolved relative to the directory containing the board file. Tools may warn if the file is missing; validation does not require the file to exist.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## CRUS: AI behavior
|
|
68
|
+
|
|
69
|
+
Statecraft is built around **CRUS** (Create, Read, Update, Summarize). AI agents must follow this behavior when working with the board.
|
|
70
|
+
|
|
71
|
+
### Create
|
|
72
|
+
|
|
73
|
+
- **Where:** New tasks are created with `status: Backlog`.
|
|
74
|
+
- **What:** Add an entry under `tasks` with at least `title` and `status`. Use a stable, kebab-case **task id** (e.g. `fix-auth-timeout`). Optionally set `description`, `spec` (path to a `.md` file relative to the board directory), `owner`, `priority`, `depends_on`.
|
|
75
|
+
- **Spec file:** If the task has acceptance criteria or context, create a markdown file (e.g. `tasks/<task-id>.md`) and set `spec: tasks/<task-id>.md`. Path is relative to the board file’s directory.
|
|
76
|
+
|
|
77
|
+
### Read
|
|
78
|
+
|
|
79
|
+
- **Board file:** The single source of truth. Read it to see all tasks, their `status`, and dependencies (`depends_on`).
|
|
80
|
+
- **Spec files:** For a task with a `spec` field, read that file for full description and definition of done. Resolve the path relative to the board file’s directory.
|
|
81
|
+
- **Status:** A task’s `status` is exactly one of the canonical column names (Backlog, Ready, In Progress, Done). Use it to reason about what is not yet selected, ready to start, in progress, or completed.
|
|
82
|
+
|
|
83
|
+
### Update
|
|
84
|
+
|
|
85
|
+
- **How:** Edit the board YAML file (and, if needed, task spec files) directly. There are no separate “move” APIs; changing `status` is the update.
|
|
86
|
+
- **Prepare for work:** When a task has a clear definition and dependencies are satisfied (or N/A), set `status` to `Ready`. Do not move to In Progress until the task is Ready (or skip Ready if the workflow is minimal).
|
|
87
|
+
- **Start work:** Set the task’s `status` to `In Progress`. Optionally read the task’s `spec` file first.
|
|
88
|
+
- **Finish work:** Set the task’s `status` to `Done` only when the task’s acceptance criteria (in its spec file, if present) are satisfied. Do not move to Done without meeting the definition of done.
|
|
89
|
+
- **Create / change fields:** Add or edit `title`, `status`, `description`, `spec`, `owner`, `priority`, `depends_on` in the board file; create or edit the spec file when the task has one.
|
|
90
|
+
|
|
91
|
+
### Summarize
|
|
92
|
+
|
|
93
|
+
- Completion is represented by moving tasks to **Done**, not by deleting them. Use `statecraft summarize` (or equivalent) to produce a short summary of the board (counts, task list, WIP, blocked). Summarize replaces “delete”: tasks are completed and reasoned about, not removed.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Example
|
|
98
|
+
|
|
99
|
+
Complete valid board:
|
|
100
|
+
|
|
101
|
+
```yaml
|
|
102
|
+
board: "Auth Service"
|
|
103
|
+
|
|
104
|
+
columns:
|
|
105
|
+
- Backlog
|
|
106
|
+
- Ready
|
|
107
|
+
- name: In Progress
|
|
108
|
+
limit: 3
|
|
109
|
+
- Done
|
|
110
|
+
|
|
111
|
+
tasks:
|
|
112
|
+
AUTH-7:
|
|
113
|
+
title: "Audit existing JWT usage"
|
|
114
|
+
status: Done
|
|
115
|
+
owner: kristijan
|
|
116
|
+
priority: high
|
|
117
|
+
description: "List all endpoints and call sites using JWT."
|
|
118
|
+
|
|
119
|
+
AUTH-12:
|
|
120
|
+
title: "Replace JWT with PASETO"
|
|
121
|
+
status: In Progress
|
|
122
|
+
owner: kristijan
|
|
123
|
+
priority: high
|
|
124
|
+
depends_on: AUTH-7
|
|
125
|
+
spec: tasks/AUTH-12.md
|
|
126
|
+
|
|
127
|
+
AUTH-13:
|
|
128
|
+
title: "Add rate limiting to auth endpoints"
|
|
129
|
+
status: Ready
|
|
130
|
+
priority: medium
|
|
131
|
+
description: "Per-IP and per-user limits; configurable thresholds."
|
|
132
|
+
|
|
133
|
+
AUTH-14:
|
|
134
|
+
title: "Document auth API"
|
|
135
|
+
status: Backlog
|
|
136
|
+
priority: low
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Constraints (for validation)
|
|
142
|
+
|
|
143
|
+
Tools that validate board files should enforce:
|
|
144
|
+
|
|
145
|
+
1. **Board:** `board` is present and a non-empty string.
|
|
146
|
+
2. **Columns:** `columns` is present and **must equal the canonical set** in order: Backlog, Ready, In Progress, Done. Each entry is either the string (e.g. `"Backlog"`, `"Ready"`, `"In Progress"`, `"Done"`) or an object `{ name: "<exact name>", limit: N }` with optional positive integer `limit` (typically only on In Progress). No extra or missing columns; no reordering.
|
|
147
|
+
3. **Tasks:** `tasks` is present (may be `{}`). Each key is a task id; each value has required `title` and `status`. `status` must be one of the canonical column names. `depends_on` entries must be task ids that exist in `tasks`. Task ids are unique.
|
|
148
|
+
4. **WIP:** If a column has a `limit`, the number of tasks with that column as `status` must not exceed `limit`.
|
|
149
|
+
|
|
150
|
+
These constraints are the basis for parser/validator implementations; this spec does not define error messages or reporting format.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Setup and AI workflow
|
|
155
|
+
|
|
156
|
+
**Creating a board:** Use **`statecraft init`** to create a board file and optionally connect Statecraft to your AI workflow. Init creates a board with the **canonical columns** (Backlog, Ready, In Progress, Done) and prompts for: board name, optional WIP limit for In Progress, board file path, directory for task `.md` files (relative to the board), and whether to generate rules for **Cursor**, **Claude Code**, and/or **Codex**. It writes valid board YAML and, if you choose, tool-specific rule files so your AI assistant knows where the board is and how to follow CRUS.
|
|
157
|
+
|
|
158
|
+
**Generated rule files:**
|
|
159
|
+
|
|
160
|
+
- **Cursor:** `.cursor/rules/statecraft.mdc` — Cursor rules with YAML frontmatter (description, alwaysApply) and Markdown body.
|
|
161
|
+
- **Claude Code:** `.claude/rules/statecraft.md` — Modular rule in Claude Code’s rules directory (Markdown, no frontmatter).
|
|
162
|
+
- **Codex:** `AGENTS.md` at project root — Codex custom instructions; init creates the file or appends a Statecraft section if `AGENTS.md` already exists.
|
|
163
|
+
|
|
164
|
+
**Where AI guidelines live:** When init generates any of these files, it embeds **AI guidelines** for creating and updating tasks: task naming (e.g. kebab-case), description and DoD, task fields from this spec (`title`, `status`, `description`, `spec`, `owner`, `priority`, `depends_on`), and the convention for spec files (e.g. `tasks/<task-id>.md`). Each rule also describes the **task lifecycle**: how to start work, finish work, and create tasks by editing the board file and task spec files directly. To customize guidelines, edit the generated file(s) after init.
|
|
165
|
+
|
|
166
|
+
**Spec and validation:** After init, run `statecraft spec` to print this spec (for AI agents) and `statecraft validate <board-path>` to validate the board.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## See also
|
|
171
|
+
|
|
172
|
+
- **Example boards** in the repo: [`examples/`](../examples/) — `board.yaml` (full), `board-minimal.yaml` (minimal). Use with `statecraft validate examples/board.yaml` and similar.
|