@stcrft/statecraft 1.2.0 → 1.4.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/README.md CHANGED
@@ -15,12 +15,16 @@ Then run `statecraft <command>`. Or use `npx @stcrft/statecraft init` (etc.) wit
15
15
 
16
16
  | Command | Description |
17
17
  |--------|-------------|
18
- | **`statecraft init`** | Interactive setup: create a board file and optionally generate rules for Cursor (`.cursor/rules/statecraft.mdc`), Claude Code (`.claude/rules/statecraft.md`), and/or Codex (`AGENTS.md`) so your AI assistant knows the board path, task lifecycle, and AI guidelines. Re-run to create a new board; edit generated files to tweak guidelines. |
18
+ | **`statecraft init`** | Interactive setup: create a board file and optionally generate rules for Cursor (`.cursor/rules/statecraft.mdc`), Claude Code (`.claude/rules/statecraft.md`), and/or Codex (`AGENTS.md`). Prompts for workflow options (defaults: enforce “create task before any work”, require each task to have a spec `.md` file, include task spec format guidelines). Re-run to create a new board; edit generated files to tweak guidelines. |
19
19
  | **`statecraft spec`** | Print the board format spec (for AI agents; no paths). |
20
20
  | **`statecraft validate [path]`** | Parse and validate a board file. Exit 0 if valid, 1 on errors. |
21
21
  | **`statecraft summarize [path]`** | Print a short text summary of the board (name, columns, tasks, WIP/blocked). |
22
22
  | **`statecraft render [path]`** | Serve the board in the browser (read-only UI). Options: `--port 3000` (default), `--open` (open browser). Starts a local server with `GET /api/board` and WebSocket for live updates when the file changes. |
23
23
 
24
+ ## Upgrade notification
25
+
26
+ After each command, the CLI may check npm for a newer version and print a one-line message to stderr if an upgrade is available. To disable: set `STATECRAFT_NO_UPDATE_CHECK=1` or `CI=true`. When running from the repo (e.g. `pnpm cli validate`), the check is skipped. A cache in the system temp directory is used when writable (writes are best-effort and do not fail the command).
27
+
24
28
  ## Path handling
25
29
 
26
30
  - **Single path only.** One board file per run. Passing multiple paths is not supported and will exit with an error.
package/dist/index.js CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { createRequire } from "module";
5
+ import path6 from "path";
6
+ import { fileURLToPath as fileURLToPath3 } from "url";
5
7
  import { Command } from "commander";
6
8
 
7
9
  // src/constants.ts
@@ -12,6 +14,9 @@ var INIT_DEFAULT_BOARD_PATH = "board.yaml";
12
14
  var INIT_DEFAULT_TASKS_DIR = "tasks";
13
15
  var CANONICAL_COLUMNS = ["Backlog", "Ready", "In Progress", "Done"];
14
16
  var SPEC_FILENAME = "spec.md";
17
+ var INIT_STRICT_MODE_DEFAULT = true;
18
+ var INIT_REQUIRE_SPEC_FILE_DEFAULT = true;
19
+ var INIT_INCLUDE_TASK_SPEC_FORMAT_DEFAULT = true;
15
20
 
16
21
  // src/executors/init.ts
17
22
  import fs from "fs";
@@ -20,9 +25,43 @@ import readline from "readline";
20
25
  import { stringify } from "yaml";
21
26
 
22
27
  // src/executors/rule-content.ts
23
- function buildStatecraftRuleBody(boardPath, tasksDir) {
24
- return `# Statecraft
28
+ var DEFAULT_RULE_OPTIONS = {
29
+ strictMode: INIT_STRICT_MODE_DEFAULT,
30
+ requireSpecFile: INIT_REQUIRE_SPEC_FILE_DEFAULT,
31
+ includeTaskSpecFormat: INIT_INCLUDE_TASK_SPEC_FORMAT_DEFAULT
32
+ };
33
+ function buildStatecraftRuleBody(boardPath, tasksDir, options = {}) {
34
+ const opts = { ...DEFAULT_RULE_OPTIONS, ...options };
35
+ const strictBlock = opts.strictMode && `## \u26A0\uFE0F REQUIRED BEFORE ANY OTHER ACTION (100% enforced)
36
+
37
+ **Before** doing any substantive work (coding, fixing, refactoring, or answering the user's request with implementation), you **MUST**:
38
+
39
+ 1. **Ensure a task exists:** In \`${boardPath}\`, either **create** a new task under \`tasks:\` with \`status: Backlog\` (use a kebab-case id, e.g. \`fix-header-import\`), or use an existing task. New tasks start in **Backlog** (vague is OK).
40
+ 2. ${opts.requireSpecFile ? `**Spec file:** Create or update \`${tasksDir}/<task-id>.md\` (required for each task).` : `**Spec file:** Optionally create or update \`${tasksDir}/<task-id>.md\` with description and acceptance criteria.`}
41
+ 3. **Flow:** Do **not** put the task in In Progress until it has been refined. After discovery/planning, update the task's spec (description, acceptance criteria); when you know what to do and the spec is clear, set \`status\` to **Ready**. Only when you are **actually starting the work** (e.g. writing code), set \`status\` to **In Progress**. So: Backlog \u2192 Ready (after refinement) \u2192 In Progress (when working) \u2192 Done.
42
+
43
+ You **must not** start implementation (no code, no concrete changes) until the task is at least in **Ready** and you are about to work on it; then move to **In Progress**. If \`${boardPath}\` does not exist yet, create a minimal valid board file (board name, columns: Backlog, Ready, In Progress, Done, tasks: {}) and the task in Backlog before proceeding.
44
+
45
+ ---
46
+ `;
47
+ const taskSpecFormatSection = opts.includeTaskSpecFormat ? `
48
+ ## Task spec file format (\`${tasksDir}/<task-id>.md\`)
25
49
 
50
+ Follow this structure so task specs are consistent and machine-friendly:
51
+
52
+ - **Title:** First line or \`# <Task title>\` (match the board \`title\` or expand it).
53
+ - **Description:** Short context or problem statement (optional \`## Description\`).
54
+ - **Acceptance criteria / Definition of Done:** \`## Acceptance criteria\` or \`## Definition of Done\` with a checklist (e.g. \`- [ ] item\`) so completion is unambiguous. All items must be checked before moving the task to Done.
55
+ - **Notes / Dependencies:** Optional \`## Notes\`, \`## Dependencies\` (for human context; \`depends_on\` in the board is the source of truth for task ordering).
56
+
57
+ Keep each file focused on one task; use the task id in the filename (e.g. \`fix-auth-timeout.md\`).
58
+
59
+ ---
60
+ ` : "";
61
+ const requireSpecBullet = opts.requireSpecFile ? `- **Spec required:** Every task must have a \`spec\` field pointing to \`${tasksDir}/<task-id>.md\`. Create the .md file when creating the task.
62
+ ` : "";
63
+ return `# Statecraft
64
+ ${strictBlock || ""}
26
65
  This project uses Statecraft for the task board.
27
66
 
28
67
  - **Board file:** \`${boardPath}\`
@@ -39,15 +78,17 @@ Use \`statecraft\` if installed globally. If not in PATH, use \`npx statecraft\`
39
78
 
40
79
  ## Task lifecycle (edit board and task files directly)
41
80
 
42
- - **Prepare for work:** When the task has a clear definition and dependencies are satisfied, set \`status\` to **Ready**.
43
- - **Start work:** Set the task's \`status\` to **In Progress**. Optionally open/read the task's \`spec\` file.
44
- - **Finish work:** Set the task's \`status\` to **Done** only when the task's acceptance criteria (in its spec file) are satisfied.
45
- - **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.
81
+ **Flow:** Backlog \u2192 Ready \u2192 In Progress \u2192 Done. Do not skip Ready.
46
82
 
83
+ - **Create task:** Add an entry under \`tasks\` with \`status: Backlog\` (id, title, optional description, spec, owner, priority, depends_on). ${opts.requireSpecFile ? `Create \`${tasksDir}/<task-id>.md\` for each task (required).` : `If needed, create \`${tasksDir}/<task-id>.md\` with description and DoD.`} Backlog = initial or vague; refinement comes next.
84
+ - **Refine and move to Ready:** After discovery or planning, update the task's spec file (description, acceptance criteria). When the definition is clear and you know how to solve it, set \`status\` to **Ready**. Ready = "ready to be worked on."
85
+ - **Start work:** Only when you are about to do the actual work (code, fix, etc.), set the task's \`status\` to **In Progress**. Read the task's \`spec\` file if needed.
86
+ - **Finish work:** Set the task's \`status\` to **Done** only when the task's acceptance criteria (in its spec file) are satisfied.
87
+ ${taskSpecFormatSection}
47
88
  ## AI guidelines for creating tickets
48
89
 
49
- - **Keep the board in sync:** When doing substantive work (e.g. a new feature, fix, or refactor), create a new task in Backlog or move an existing task to In Progress/Done as appropriate. Update the board file and optionally add or update a task spec in \`${tasksDir}/\`.
50
- - **Task naming:** kebab-case, verb or noun phrase (e.g. \`fix-auth-timeout\`).
90
+ - **Keep the board in sync:** When doing substantive work, create a new task in **Backlog** (or use an existing one). After discovery/refinement, update the task spec and move to **Ready**; only when actually working move to **In Progress**, then **Done** when criteria are met. Do not skip Ready.
91
+ ${requireSpecBullet}- **Task naming:** kebab-case, verb or noun phrase (e.g. \`fix-auth-timeout\`).
51
92
  - **Description:** One line summary; optional markdown for context.
52
93
  - **Definition of Done:** Acceptance criteria in task spec; all checked before moving to Done.
53
94
  - **Task fields (from spec):** \`title\` (required), \`status\` (required), optional \`description\`, \`spec\` (path to .md), \`owner\`, \`priority\`, \`depends_on\`.
@@ -95,6 +136,27 @@ async function collectInitAnswers(rl, answers, index) {
95
136
  "WIP limit for In Progress (optional, press Enter to skip)",
96
137
  ""
97
138
  );
139
+ const strictModeStr = await getAnswer(
140
+ rl,
141
+ answers,
142
+ index,
143
+ "Enforce Statecraft workflow (create task before any work)? (Y/n)",
144
+ INIT_STRICT_MODE_DEFAULT ? "Y" : "n"
145
+ );
146
+ const requireSpecStr = await getAnswer(
147
+ rl,
148
+ answers,
149
+ index,
150
+ "Require each task to have a spec .md file? (Y/n)",
151
+ INIT_REQUIRE_SPEC_FILE_DEFAULT ? "Y" : "n"
152
+ );
153
+ const includeFormatStr = await getAnswer(
154
+ rl,
155
+ answers,
156
+ index,
157
+ "Include task spec .md format guidelines in rules? (Y/n)",
158
+ INIT_INCLUDE_TASK_SPEC_FORMAT_DEFAULT ? "Y" : "n"
159
+ );
98
160
  const genCursor = await getAnswer(rl, answers, index, "Generate Cursor rule? (Y/n)", "Y");
99
161
  const genClaude = await getAnswer(rl, answers, index, "Generate Claude Code rule? (y/n)", "n");
100
162
  const genCodex = await getAnswer(rl, answers, index, "Generate Codex instructions (AGENTS.md)? (y/n)", "n");
@@ -103,6 +165,9 @@ async function collectInitAnswers(rl, answers, index) {
103
165
  boardPath,
104
166
  tasksDir,
105
167
  wipStr,
168
+ strictMode: /^y(es)?$/i.test(strictModeStr.trim()),
169
+ requireSpecFile: /^y(es)?$/i.test(requireSpecStr.trim()),
170
+ includeTaskSpecFormat: /^y(es)?$/i.test(includeFormatStr.trim()),
106
171
  generateCursorRule: /^y(es)?$/i.test(genCursor.trim()),
107
172
  generateClaudeRule: /^y(es)?$/i.test(genClaude.trim()),
108
173
  generateCodexAgents: /^y(es)?$/i.test(genCodex.trim())
@@ -139,21 +204,21 @@ function writeRuleFile(filePath, content, log) {
139
204
  log(filePath);
140
205
  }
141
206
  var CODECX_MARKER = "## Statecraft (generated by statecraft init)";
142
- function writeCursorRule(cwd, boardPath, specDir, log) {
207
+ function writeCursorRule(cwd, boardPath, specDir, options, log) {
143
208
  const cursorRulesDir = path.resolve(cwd, ".cursor", "rules");
144
209
  const cursorRulePath = path.join(cursorRulesDir, "statecraft.mdc");
145
- const content = buildCursorRuleContent(boardPath, specDir);
210
+ const content = buildCursorRuleContent(boardPath, specDir, options);
146
211
  writeRuleFile(cursorRulePath, content, (p) => log(`Wrote Cursor rule to ${p}`));
147
212
  }
148
- function writeClaudeRule(cwd, boardPath, specDir, log) {
213
+ function writeClaudeRule(cwd, boardPath, specDir, options, log) {
149
214
  const claudeRulesDir = path.resolve(cwd, ".claude", "rules");
150
215
  const claudeRulePath = path.join(claudeRulesDir, "statecraft.md");
151
- const content = buildClaudeRuleContent(boardPath, specDir);
216
+ const content = buildClaudeRuleContent(boardPath, specDir, options);
152
217
  writeRuleFile(claudeRulePath, content, (p) => log(`Wrote Claude Code rule to ${p}`));
153
218
  }
154
- function writeCodexRule(cwd, boardPath, specDir, log) {
219
+ function writeCodexRule(cwd, boardPath, specDir, options, log) {
155
220
  const codexAgentsPath = path.resolve(cwd, "AGENTS.md");
156
- const codexContent = buildCodexAgentsContent(boardPath, specDir);
221
+ const codexContent = buildCodexAgentsContent(boardPath, specDir, options);
157
222
  if (fs.existsSync(codexAgentsPath)) {
158
223
  const existing = fs.readFileSync(codexAgentsPath, "utf-8");
159
224
  if (existing.includes(CODECX_MARKER)) {
@@ -167,20 +232,20 @@ function writeCodexRule(cwd, boardPath, specDir, log) {
167
232
  log(`Wrote Codex instructions to ${codexAgentsPath}`);
168
233
  }
169
234
  }
170
- function buildCursorRuleContent(boardPath, tasksDir) {
235
+ function buildCursorRuleContent(boardPath, tasksDir, options) {
171
236
  return `---
172
237
  description: Statecraft board and task workflow; when to update the board and how to create tasks
173
238
  alwaysApply: true
174
239
  ---
175
- ${buildStatecraftRuleBody(boardPath, tasksDir)}`;
240
+ ${buildStatecraftRuleBody(boardPath, tasksDir, options)}`;
176
241
  }
177
- function buildClaudeRuleContent(boardPath, tasksDir) {
178
- return buildStatecraftRuleBody(boardPath, tasksDir);
242
+ function buildClaudeRuleContent(boardPath, tasksDir, options) {
243
+ return buildStatecraftRuleBody(boardPath, tasksDir, options);
179
244
  }
180
- function buildCodexAgentsContent(boardPath, tasksDir) {
245
+ function buildCodexAgentsContent(boardPath, tasksDir, options) {
181
246
  return `## Statecraft (generated by statecraft init)
182
247
 
183
- ${buildStatecraftRuleBody(boardPath, tasksDir)}`;
248
+ ${buildStatecraftRuleBody(boardPath, tasksDir, options)}`;
184
249
  }
185
250
  async function runInit(options) {
186
251
  const answers = options?.answers;
@@ -208,9 +273,14 @@ async function runInit(options) {
208
273
  Created board at ${absolutePath}`);
209
274
  log(`Task spec files: ${path.join(path.dirname(collected.boardPath), collected.tasksDir)}/<task-id>.md`);
210
275
  const specDir = path.join(path.dirname(collected.boardPath), collected.tasksDir);
211
- if (collected.generateCursorRule) writeCursorRule(cwd, collected.boardPath, specDir, log);
212
- if (collected.generateClaudeRule) writeClaudeRule(cwd, collected.boardPath, specDir, log);
213
- if (collected.generateCodexAgents) writeCodexRule(cwd, collected.boardPath, specDir, log);
276
+ const ruleOptions = {
277
+ strictMode: collected.strictMode,
278
+ requireSpecFile: collected.requireSpecFile,
279
+ includeTaskSpecFormat: collected.includeTaskSpecFormat
280
+ };
281
+ if (collected.generateCursorRule) writeCursorRule(cwd, collected.boardPath, specDir, ruleOptions, log);
282
+ if (collected.generateClaudeRule) writeClaudeRule(cwd, collected.boardPath, specDir, ruleOptions, log);
283
+ if (collected.generateCodexAgents) writeCodexRule(cwd, collected.boardPath, specDir, ruleOptions, log);
214
284
  log("\nRun `statecraft validate " + collected.boardPath + "` to validate, or `statecraft render " + collected.boardPath + "` to view.");
215
285
  } catch (err) {
216
286
  const message = err instanceof Error ? err.message : String(err);
@@ -367,9 +437,9 @@ function startRenderServer(options) {
367
437
  }
368
438
 
369
439
  // src/executors/render.ts
370
- function runRender(path5, options) {
440
+ function runRender(path7, options) {
371
441
  startRenderServer({
372
- boardPath: path5,
442
+ boardPath: path7,
373
443
  port: options.port,
374
444
  openBrowser: options.open
375
445
  });
@@ -426,9 +496,9 @@ function runSpec() {
426
496
 
427
497
  // src/executors/summarize.ts
428
498
  import { parseBoard, summarize } from "@stcrft/statecraft-core";
429
- function runSummarize(path5) {
499
+ function runSummarize(path7) {
430
500
  try {
431
- const board = parseBoard(path5);
501
+ const board = parseBoard(path7);
432
502
  const summary = summarize(board);
433
503
  process.stdout.write(summary);
434
504
  process.exitCode = 0;
@@ -445,9 +515,9 @@ function formatValidationError(err) {
445
515
  const prefix = err.path != null && err.path !== "" ? `${err.path}: ` : "";
446
516
  return `${prefix}${err.message}`;
447
517
  }
448
- function runValidate(path5) {
518
+ function runValidate(path7) {
449
519
  try {
450
- const board = parseBoard2(path5);
520
+ const board = parseBoard2(path7);
451
521
  const result = validate(board);
452
522
  if (!result.valid) {
453
523
  for (const err of result.errors) {
@@ -464,9 +534,95 @@ function runValidate(path5) {
464
534
  }
465
535
  }
466
536
 
537
+ // src/update-check.ts
538
+ import fs5 from "fs";
539
+ import os from "os";
540
+ import path5 from "path";
541
+ import semver from "semver";
542
+ var REGISTRY_URL = "https://registry.npmjs.org/@stcrft/statecraft/latest";
543
+ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
544
+ var FETCH_TIMEOUT_MS = 3e3;
545
+ var CACHE_FILENAME = "statecraft-update-check.json";
546
+ function getCachePath() {
547
+ const tmp = os.tmpdir();
548
+ return path5.join(tmp, CACHE_FILENAME);
549
+ }
550
+ function readCache() {
551
+ try {
552
+ const p = getCachePath();
553
+ const raw = fs5.readFileSync(p, "utf-8");
554
+ const data = JSON.parse(raw);
555
+ if (typeof data.lastCheck === "number" && typeof data.latestVersion === "string") {
556
+ return data;
557
+ }
558
+ } catch {
559
+ }
560
+ return null;
561
+ }
562
+ function writeCache(latestVersion) {
563
+ try {
564
+ const p = getCachePath();
565
+ const data = { lastCheck: Date.now(), latestVersion };
566
+ fs5.writeFileSync(p, JSON.stringify(data), "utf-8");
567
+ } catch {
568
+ }
569
+ }
570
+ async function fetchLatestVersion() {
571
+ const controller = new AbortController();
572
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
573
+ try {
574
+ const res = await fetch(REGISTRY_URL, { signal: controller.signal });
575
+ clearTimeout(timeout);
576
+ if (!res.ok) return null;
577
+ const json = await res.json();
578
+ return typeof json.version === "string" ? json.version : null;
579
+ } catch {
580
+ clearTimeout(timeout);
581
+ return null;
582
+ }
583
+ }
584
+ function shouldSkip(options) {
585
+ if (process.env.CI === "true" || process.env.CI === "1") return true;
586
+ if (process.env.STATECRAFT_NO_UPDATE_CHECK === "1" || process.env.STATECRAFT_NO_UPDATE_CHECK === "true") return true;
587
+ if (options.isLocalDev) return true;
588
+ return false;
589
+ }
590
+ function runUpdateCheck(currentVersion, options) {
591
+ if (shouldSkip(options)) return;
592
+ void (async () => {
593
+ let latest = null;
594
+ const cached = readCache();
595
+ if (cached && Date.now() - cached.lastCheck < CACHE_TTL_MS) {
596
+ latest = cached.latestVersion;
597
+ }
598
+ if (latest === null) {
599
+ latest = await fetchLatestVersion();
600
+ if (latest !== null) writeCache(latest);
601
+ }
602
+ if (latest === null) return;
603
+ try {
604
+ if (semver.gt(latest, currentVersion)) {
605
+ process.stderr.write(
606
+ `A new version of Statecraft is available: ${latest} (you have ${currentVersion}). Upgrade: npm update -g @stcrft/statecraft
607
+ `
608
+ );
609
+ }
610
+ } catch {
611
+ }
612
+ })();
613
+ }
614
+
467
615
  // src/index.ts
468
616
  var require2 = createRequire(import.meta.url);
469
617
  var { version } = require2("../package.json");
618
+ function isLocalDev() {
619
+ try {
620
+ const dir = path6.dirname(fileURLToPath3(import.meta.url));
621
+ return /packages[/\\]cli[/\\]/.test(dir) || dir.includes("statecraft" + path6.sep + "packages" + path6.sep + "cli");
622
+ } catch {
623
+ return false;
624
+ }
625
+ }
470
626
  function rejectMultiplePaths(extra) {
471
627
  if (extra.length > 0) {
472
628
  process.stderr.write("Only one board file per run. Multiple paths are not supported.\n");
@@ -484,17 +640,19 @@ program.command("init").description("Interactive setup: create board and configu
484
640
  program.command("spec").description("Print the board format spec (for AI agents)").action(() => {
485
641
  runSpec();
486
642
  });
487
- 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) => {
643
+ 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((path7, extra) => {
488
644
  rejectMultiplePaths(extra);
489
- if (process.exitCode !== 1) runValidate(path5);
645
+ if (process.exitCode !== 1) runValidate(path7);
490
646
  });
491
- 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) => {
647
+ 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((path7, extra) => {
492
648
  rejectMultiplePaths(extra);
493
- if (process.exitCode !== 1) runSummarize(path5);
649
+ if (process.exitCode !== 1) runSummarize(path7);
494
650
  });
495
- 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) => {
651
+ 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((path7, options) => {
496
652
  const port = parseInt(options.port, 10) || DEFAULT_RENDER_PORT;
497
- runRender(path5, { port, open: options.open ?? false });
653
+ runRender(path7, { port, open: options.open ?? false });
654
+ });
655
+ program.parseAsync().then(() => {
656
+ runUpdateCheck(version, { isLocalDev: isLocalDev() });
498
657
  });
499
- program.parseAsync();
500
658
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +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: true\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\nUse \\`statecraft\\` if installed globally. If not in PATH, use \\`npx statecraft\\` (npm), \\`pnpm dlx statecraft\\` (pnpm), or \\`yarn dlx statecraft\\` (yarn)—e.g. \\`npx statecraft validate ${boardPath}\\`.\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- **Keep the board in sync:** When doing substantive work (e.g. a new feature, fix, or refactor), create a new task in Backlog or move an existing task to In Progress/Done as appropriate. Update the board file and optionally add or update a task spec in \\`${tasksDir}/\\`.\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 static files.\n * - When installed from npm: bundled at package-root/renderer-dist.\n * - When running in monorepo: sibling package at packages/renderer/dist.\n */\nfunction getRendererDistPath(): string {\n const bundled = path.resolve(__dirname, \"..\", \"renderer-dist\");\n if (fs.existsSync(bundled) && fs.statSync(bundled).isDirectory()) {\n return bundled;\n }\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"]}
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","../src/update-check.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { createRequire } from \"node:module\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { Command } from \"commander\";\nimport { DEFAULT_BOARD_PATH, DEFAULT_RENDER_PORT } from \"./constants.js\";\nimport { runInit, runRender, runSpec, runSummarize, runValidate } from \"./executors/index.js\";\nimport { runUpdateCheck } from \"./update-check.js\";\n\nconst require = createRequire(import.meta.url);\nconst { version } = require(\"../package.json\") as { version: string };\n\n/** True when running from monorepo (e.g. pnpm cli validate). */\nfunction isLocalDev(): boolean {\n try {\n const dir = path.dirname(fileURLToPath(import.meta.url));\n return /packages[/\\\\]cli[/\\\\]/.test(dir) || dir.includes(\"statecraft\" + path.sep + \"packages\" + path.sep + \"cli\");\n } catch {\n return false;\n }\n}\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().then(() => {\n runUpdateCheck(version, { isLocalDev: isLocalDev() });\n});\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\n/** Init default: enforce \"create task before work\" workflow for all agents. */\nexport const INIT_STRICT_MODE_DEFAULT = true;\n\n/** Init default: require each task to have a spec .md file in the tasks directory. */\nexport const INIT_REQUIRE_SPEC_FILE_DEFAULT = true;\n\n/** Init default: include task spec .md format guidelines in generated rules. */\nexport const INIT_INCLUDE_TASK_SPEC_FORMAT_DEFAULT = true;\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 INIT_INCLUDE_TASK_SPEC_FORMAT_DEFAULT,\n INIT_REQUIRE_SPEC_FILE_DEFAULT,\n INIT_STRICT_MODE_DEFAULT,\n} from \"../constants.js\";\nimport { buildStatecraftRuleBody, type RuleOptions } 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 strictMode: boolean;\n requireSpecFile: boolean;\n includeTaskSpecFormat: boolean;\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 strictModeStr = await getAnswer(\n rl,\n answers,\n index,\n \"Enforce Statecraft workflow (create task before any work)? (Y/n)\",\n INIT_STRICT_MODE_DEFAULT ? \"Y\" : \"n\"\n );\n const requireSpecStr = await getAnswer(\n rl,\n answers,\n index,\n \"Require each task to have a spec .md file? (Y/n)\",\n INIT_REQUIRE_SPEC_FILE_DEFAULT ? \"Y\" : \"n\"\n );\n const includeFormatStr = await getAnswer(\n rl,\n answers,\n index,\n \"Include task spec .md format guidelines in rules? (Y/n)\",\n INIT_INCLUDE_TASK_SPEC_FORMAT_DEFAULT ? \"Y\" : \"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 strictMode: /^y(es)?$/i.test(strictModeStr.trim()),\n requireSpecFile: /^y(es)?$/i.test(requireSpecStr.trim()),\n includeTaskSpecFormat: /^y(es)?$/i.test(includeFormatStr.trim()),\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(\n cwd: string,\n boardPath: string,\n specDir: string,\n options: Partial<RuleOptions>,\n log: (msg: string) => void\n): void {\n const cursorRulesDir = path.resolve(cwd, \".cursor\", \"rules\");\n const cursorRulePath = path.join(cursorRulesDir, \"statecraft.mdc\");\n const content = buildCursorRuleContent(boardPath, specDir, options);\n writeRuleFile(cursorRulePath, content, (p) => log(`Wrote Cursor rule to ${p}`));\n}\n\nfunction writeClaudeRule(\n cwd: string,\n boardPath: string,\n specDir: string,\n options: Partial<RuleOptions>,\n log: (msg: string) => void\n): void {\n const claudeRulesDir = path.resolve(cwd, \".claude\", \"rules\");\n const claudeRulePath = path.join(claudeRulesDir, \"statecraft.md\");\n const content = buildClaudeRuleContent(boardPath, specDir, options);\n writeRuleFile(claudeRulePath, content, (p) => log(`Wrote Claude Code rule to ${p}`));\n}\n\nfunction writeCodexRule(\n cwd: string,\n boardPath: string,\n specDir: string,\n options: Partial<RuleOptions>,\n log: (msg: string) => void\n): void {\n const codexAgentsPath = path.resolve(cwd, \"AGENTS.md\");\n const codexContent = buildCodexAgentsContent(boardPath, specDir, options);\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(\n boardPath: string,\n tasksDir: string,\n options?: Partial<RuleOptions>\n): string {\n return `---\ndescription: Statecraft board and task workflow; when to update the board and how to create tasks\nalwaysApply: true\n---\n${buildStatecraftRuleBody(boardPath, tasksDir, options)}`;\n}\n\n/** Claude Code: modular rule in .claude/rules/ (markdown, no frontmatter). */\nexport function buildClaudeRuleContent(\n boardPath: string,\n tasksDir: string,\n options?: Partial<RuleOptions>\n): string {\n return buildStatecraftRuleBody(boardPath, tasksDir, options);\n}\n\n/** Codex: section for AGENTS.md at project root. */\nexport function buildCodexAgentsContent(\n boardPath: string,\n tasksDir: string,\n options?: Partial<RuleOptions>\n): string {\n return `## Statecraft (generated by statecraft init)\n\n${buildStatecraftRuleBody(boardPath, tasksDir, options)}`;\n}\n\n// --- runInit ---\n\nexport interface RunInitOptions {\n /** For tests: pre-filled answers (board name, board path, tasks dir, WIP, strict mode Y/n, require spec Y/n, task spec format Y/n, 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 const ruleOptions: Partial<RuleOptions> = {\n strictMode: collected.strictMode,\n requireSpecFile: collected.requireSpecFile,\n includeTaskSpecFormat: collected.includeTaskSpecFormat,\n };\n\n if (collected.generateCursorRule) writeCursorRule(cwd, collected.boardPath, specDir, ruleOptions, log);\n if (collected.generateClaudeRule) writeClaudeRule(cwd, collected.boardPath, specDir, ruleOptions, log);\n if (collected.generateCodexAgents) writeCodexRule(cwd, collected.boardPath, specDir, ruleOptions, 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","import {\n INIT_INCLUDE_TASK_SPEC_FORMAT_DEFAULT,\n INIT_REQUIRE_SPEC_FILE_DEFAULT,\n INIT_STRICT_MODE_DEFAULT,\n} from \"../constants.js\";\n\n/**\n * Options for generated rule content. Used by init; defaults match init prompts (Y/n).\n */\nexport interface RuleOptions {\n /** Enforce \"create task before any work\" workflow (strict mode). Default true. */\n strictMode: boolean;\n /** Require each task to have a spec .md file. Default true. */\n requireSpecFile: boolean;\n /** Include task spec .md format guidelines in the rule. Default true. */\n includeTaskSpecFormat: boolean;\n}\n\nconst DEFAULT_RULE_OPTIONS: RuleOptions = {\n strictMode: INIT_STRICT_MODE_DEFAULT,\n requireSpecFile: INIT_REQUIRE_SPEC_FILE_DEFAULT,\n includeTaskSpecFormat: INIT_INCLUDE_TASK_SPEC_FORMAT_DEFAULT,\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 */\nexport function buildStatecraftRuleBody(\n boardPath: string,\n tasksDir: string,\n options: Partial<RuleOptions> = {}\n): string {\n const opts = { ...DEFAULT_RULE_OPTIONS, ...options };\n\n const strictBlock =\n opts.strictMode &&\n `## ⚠️ REQUIRED BEFORE ANY OTHER ACTION (100% enforced)\n\n**Before** doing any substantive work (coding, fixing, refactoring, or answering the user's request with implementation), you **MUST**:\n\n1. **Ensure a task exists:** In \\`${boardPath}\\`, either **create** a new task under \\`tasks:\\` with \\`status: Backlog\\` (use a kebab-case id, e.g. \\`fix-header-import\\`), or use an existing task. New tasks start in **Backlog** (vague is OK).\n2. ${opts.requireSpecFile ? `**Spec file:** Create or update \\`${tasksDir}/<task-id>.md\\` (required for each task).` : `**Spec file:** Optionally create or update \\`${tasksDir}/<task-id>.md\\` with description and acceptance criteria.`}\n3. **Flow:** Do **not** put the task in In Progress until it has been refined. After discovery/planning, update the task's spec (description, acceptance criteria); when you know what to do and the spec is clear, set \\`status\\` to **Ready**. Only when you are **actually starting the work** (e.g. writing code), set \\`status\\` to **In Progress**. So: Backlog → Ready (after refinement) → In Progress (when working) → Done.\n\nYou **must not** start implementation (no code, no concrete changes) until the task is at least in **Ready** and you are about to work on it; then move to **In Progress**. If \\`${boardPath}\\` does not exist yet, create a minimal valid board file (board name, columns: Backlog, Ready, In Progress, Done, tasks: {}) and the task in Backlog before proceeding.\n\n---\n`;\n\n const taskSpecFormatSection = opts.includeTaskSpecFormat\n ? `\n## Task spec file format (\\`${tasksDir}/<task-id>.md\\`)\n\nFollow this structure so task specs are consistent and machine-friendly:\n\n- **Title:** First line or \\`# <Task title>\\` (match the board \\`title\\` or expand it).\n- **Description:** Short context or problem statement (optional \\`## Description\\`).\n- **Acceptance criteria / Definition of Done:** \\`## Acceptance criteria\\` or \\`## Definition of Done\\` with a checklist (e.g. \\`- [ ] item\\`) so completion is unambiguous. All items must be checked before moving the task to Done.\n- **Notes / Dependencies:** Optional \\`## Notes\\`, \\`## Dependencies\\` (for human context; \\`depends_on\\` in the board is the source of truth for task ordering).\n\nKeep each file focused on one task; use the task id in the filename (e.g. \\`fix-auth-timeout.md\\`).\n\n---\n`\n : \"\";\n\n const requireSpecBullet = opts.requireSpecFile\n ? `- **Spec required:** Every task must have a \\`spec\\` field pointing to \\`${tasksDir}/<task-id>.md\\`. Create the .md file when creating the task.\n`\n : \"\";\n\n return `# Statecraft\n${strictBlock || \"\"}\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\nUse \\`statecraft\\` if installed globally. If not in PATH, use \\`npx statecraft\\` (npm), \\`pnpm dlx statecraft\\` (pnpm), or \\`yarn dlx statecraft\\` (yarn)—e.g. \\`npx statecraft validate ${boardPath}\\`.\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**Flow:** Backlog → Ready → In Progress → Done. Do not skip Ready.\n\n- **Create task:** Add an entry under \\`tasks\\` with \\`status: Backlog\\` (id, title, optional description, spec, owner, priority, depends_on). ${opts.requireSpecFile ? `Create \\`${tasksDir}/<task-id>.md\\` for each task (required).` : `If needed, create \\`${tasksDir}/<task-id>.md\\` with description and DoD.`} Backlog = initial or vague; refinement comes next.\n- **Refine and move to Ready:** After discovery or planning, update the task's spec file (description, acceptance criteria). When the definition is clear and you know how to solve it, set \\`status\\` to **Ready**. Ready = \"ready to be worked on.\"\n- **Start work:** Only when you are about to do the actual work (code, fix, etc.), set the task's \\`status\\` to **In Progress**. Read the task's \\`spec\\` file if needed.\n- **Finish work:** Set the task's \\`status\\` to **Done** only when the task's acceptance criteria (in its spec file) are satisfied.\n${taskSpecFormatSection}\n## AI guidelines for creating tickets\n\n- **Keep the board in sync:** When doing substantive work, create a new task in **Backlog** (or use an existing one). After discovery/refinement, update the task spec and move to **Ready**; only when actually working move to **In Progress**, then **Done** when criteria are met. Do not skip Ready.\n${requireSpecBullet}- **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 static files.\n * - When installed from npm: bundled at package-root/renderer-dist.\n * - When running in monorepo: sibling package at packages/renderer/dist.\n */\nfunction getRendererDistPath(): string {\n const bundled = path.resolve(__dirname, \"..\", \"renderer-dist\");\n if (fs.existsSync(bundled) && fs.statSync(bundled).isDirectory()) {\n return bundled;\n }\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","/**\n * Optional update check: notify users when a newer version of Statecraft is available.\n * Runs fire-and-forget after commands; uses a cache in the temp directory when writable.\n */\n\nimport fs from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport semver from \"semver\";\n\nconst REGISTRY_URL = \"https://registry.npmjs.org/@stcrft/statecraft/latest\";\nconst CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours\nconst FETCH_TIMEOUT_MS = 3000;\nconst CACHE_FILENAME = \"statecraft-update-check.json\";\n\ninterface CacheShape {\n lastCheck: number;\n latestVersion: string;\n}\n\nfunction getCachePath(): string {\n const tmp = os.tmpdir();\n return path.join(tmp, CACHE_FILENAME);\n}\n\nfunction readCache(): CacheShape | null {\n try {\n const p = getCachePath();\n const raw = fs.readFileSync(p, \"utf-8\");\n const data = JSON.parse(raw) as CacheShape;\n if (typeof data.lastCheck === \"number\" && typeof data.latestVersion === \"string\") {\n return data;\n }\n } catch {\n // ignore\n }\n return null;\n}\n\nfunction writeCache(latestVersion: string): void {\n try {\n const p = getCachePath();\n const data: CacheShape = { lastCheck: Date.now(), latestVersion };\n fs.writeFileSync(p, JSON.stringify(data), \"utf-8\");\n } catch {\n // best-effort: do not fail if temp dir is read-only or other error\n }\n}\n\nasync function fetchLatestVersion(): Promise<string | null> {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);\n try {\n const res = await fetch(REGISTRY_URL, { signal: controller.signal });\n clearTimeout(timeout);\n if (!res.ok) return null;\n const json = (await res.json()) as { version?: string };\n return typeof json.version === \"string\" ? json.version : null;\n } catch {\n clearTimeout(timeout);\n return null;\n }\n}\n\nfunction shouldSkip(options: { isLocalDev: boolean }): boolean {\n if (process.env.CI === \"true\" || process.env.CI === \"1\") return true;\n if (process.env.STATECRAFT_NO_UPDATE_CHECK === \"1\" || process.env.STATECRAFT_NO_UPDATE_CHECK === \"true\") return true;\n if (options.isLocalDev) return true;\n return false;\n}\n\n/**\n * Run the update check (fire-and-forget). Call after the command has finished.\n * Skips when CI, STATECRAFT_NO_UPDATE_CHECK, or running from local dev.\n * Uses cache in temp dir when writable; does not fail if cache write fails.\n */\nexport function runUpdateCheck(currentVersion: string, options: { isLocalDev: boolean }): void {\n if (shouldSkip(options)) return;\n\n void (async () => {\n let latest: string | null = null;\n const cached = readCache();\n if (cached && Date.now() - cached.lastCheck < CACHE_TTL_MS) {\n latest = cached.latestVersion;\n }\n if (latest === null) {\n latest = await fetchLatestVersion();\n if (latest !== null) writeCache(latest);\n }\n if (latest === null) return;\n try {\n if (semver.gt(latest, currentVersion)) {\n process.stderr.write(\n `A new version of Statecraft is available: ${latest} (you have ${currentVersion}). Upgrade: npm update -g @stcrft/statecraft\\n`\n );\n }\n } catch {\n // invalid semver or other compare error: do nothing\n }\n })();\n}\n"],"mappings":"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","names":["path","fileURLToPath","fs","path","__dirname","path","fs","path","fs","path","fileURLToPath","fs","path","fileURLToPath","path","fs","path","parseBoard","path","fs","path","require","path","fileURLToPath"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stcrft/statecraft",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "CLI for Statecraft: validate, summarize, and render board-as-code",
5
5
  "type": "module",
6
6
  "repository": {
@@ -25,7 +25,8 @@
25
25
  "open": "^11.0.0",
26
26
  "ws": "^8.18.0",
27
27
  "yaml": "^2.8.2",
28
- "@stcrft/statecraft-core": "1.2.0"
28
+ "semver": "^7.6.3",
29
+ "@stcrft/statecraft-core": "1.4.0"
29
30
  },
30
31
  "devDependencies": {
31
32
  "@types/express": "^5.0.0",
package/spec.md CHANGED
@@ -32,9 +32,9 @@ This document defines the Statecraft board format. A single file describes one K
32
32
 
33
33
  | Column | Meaning for AI |
34
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. |
35
+ | **Backlog** | Initial or vague task. New tasks are created here. May be unrefined—title and minimal spec are enough. After discovery/planning, the agent updates the task spec (description, acceptance criteria) and moves the task to Ready. |
36
+ | **Ready** | Task is refined: definition is clear, spec file has description and acceptance criteria, and the agent (or human) knows how to solve it. Ready to be worked on. Move here **after** discovery; move to In Progress **only when** starting the actual work. |
37
+ | **In Progress** | Currently being worked on (e.g. code, fix, implementation). Move a task here only when actively doing the work—not when still planning. At most one (or a configurable WIP limit) at a time per column if enforced. |
38
38
  | **Done** | Completed and accepted. Move here when the task’s acceptance criteria (in its spec file) are satisfied. |
39
39
 
40
40
  - **Order:** `Backlog` → `Ready` → `In Progress` → `Done` (left to right).
@@ -72,7 +72,7 @@ Statecraft is built around **CRUS** (Create, Read, Update, Summarize). AI agents
72
72
 
73
73
  - **Where:** New tasks are created with `status: Backlog`.
74
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.
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. See **Task spec file format** below for recommended structure.
76
76
 
77
77
  ### Read
78
78
 
@@ -82,9 +82,11 @@ Statecraft is built around **CRUS** (Create, Read, Update, Summarize). AI agents
82
82
 
83
83
  ### Update
84
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.
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
+ - **Flow:** Backlog Ready In Progress Done. Do not skip Ready.
87
+ - **Create (Backlog):** New tasks start in Backlog. They may be vague; add at least `title` and `status`, and create the spec file if required.
88
+ - **Refine and move to Ready:** After discovery or planning, update the task’s spec file (description, acceptance criteria). When the definition is clear and you know how to solve it, set `status` to **Ready**. Ready means "ready to be worked on."
89
+ - **Start work:** Only when you are about to do the actual work (code, fix, implementation), set the task’s `status` to **In Progress**. Do not move to In Progress while still in discovery or planning. Read the task’s `spec` file if needed.
88
90
  - **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
91
  - **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
92
 
@@ -92,6 +94,25 @@ Statecraft is built around **CRUS** (Create, Read, Update, Summarize). AI agents
92
94
 
93
95
  - 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
96
 
97
+
98
+ ## Task spec file format (best practices)
99
+
100
+ Task spec files (e.g. `tasks/<task-id>.md`) are markdown files referenced by a task's `spec` field. Following a consistent structure keeps specs machine-friendly and clear for both AI and humans.
101
+
102
+ **Recommended structure:**
103
+
104
+ | Section | Purpose |
105
+ |--------|---------|
106
+ | **Title** | First line or `# <Task title>` — match or expand the board `title`. |
107
+ | **Description** | Optional `## Description` with short context or problem statement. |
108
+ | **Acceptance criteria / Definition of Done** | `## Acceptance criteria` or `## Definition of Done` with a checklist (e.g. `- [ ] item`). All items must be checked before moving the task to Done. |
109
+ | **Notes / Dependencies** | Optional `## Notes`, `## Dependencies` for human context; `depends_on` in the board is the source of truth for ordering. |
110
+
111
+ - Use one file per task; put the task id in the filename (e.g. `fix-auth-timeout.md`).
112
+ - Path is relative to the board file's directory.
113
+
114
+ Init can optionally add these guidelines to generated AI rules (Cursor, Claude Code, Codex) so agents follow the same format.
115
+
95
116
  ---
96
117
 
97
118
  ## Example
@@ -153,7 +174,7 @@ These constraints are the basis for parser/validator implementations; this spec
153
174
 
154
175
  ## Setup and AI workflow
155
176
 
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.
177
+ **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), **workflow options** (enforce “create task before any work”, require each task to have a spec `.md` file, include task spec format guidelines in rules — all default to yes), 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
178
 
158
179
  **Generated rule files:**
159
180