@connorbritain/roadmap 0.3.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.
Files changed (53) hide show
  1. package/README.md +495 -0
  2. package/docs/DEPLOYMENT.md +195 -0
  3. package/package.json +46 -0
  4. package/schema/backlog.schema.json +65 -0
  5. package/schema/roadmap.schema.json +273 -0
  6. package/scripts/assistant.mjs +30 -0
  7. package/scripts/backlog.mjs +81 -0
  8. package/scripts/cleanup.mjs +69 -0
  9. package/scripts/cli.mjs +119 -0
  10. package/scripts/cycle.mjs +89 -0
  11. package/scripts/dispatch.mjs +302 -0
  12. package/scripts/estimate.mjs +201 -0
  13. package/scripts/fanout.mjs +355 -0
  14. package/scripts/grab.mjs +119 -0
  15. package/scripts/init.mjs +60 -0
  16. package/scripts/lib/assistant-core.mjs +64 -0
  17. package/scripts/lib/backlog-core.mjs +318 -0
  18. package/scripts/lib/brief.mjs +123 -0
  19. package/scripts/lib/cli-core.mjs +97 -0
  20. package/scripts/lib/cycle-core.mjs +58 -0
  21. package/scripts/lib/estimate-core.mjs +204 -0
  22. package/scripts/lib/execution.mjs +186 -0
  23. package/scripts/lib/fanout-core.mjs +39 -0
  24. package/scripts/lib/graph.mjs +346 -0
  25. package/scripts/lib/journal-core.mjs +48 -0
  26. package/scripts/lib/linear-core.mjs +812 -0
  27. package/scripts/lib/mcp-core.mjs +313 -0
  28. package/scripts/lib/plan.mjs +68 -0
  29. package/scripts/lib/plate-core.mjs +53 -0
  30. package/scripts/lib/pr-watch-core.mjs +72 -0
  31. package/scripts/lib/priority.mjs +43 -0
  32. package/scripts/lib/recommend.mjs +178 -0
  33. package/scripts/lib/render-core.mjs +215 -0
  34. package/scripts/lib/review-core.mjs +104 -0
  35. package/scripts/lib/store.mjs +113 -0
  36. package/scripts/lib/sync-core.mjs +89 -0
  37. package/scripts/lib/validate-core.mjs +130 -0
  38. package/scripts/lib/wizard-core.mjs +50 -0
  39. package/scripts/linear.mjs +780 -0
  40. package/scripts/mcp.mjs +193 -0
  41. package/scripts/next.mjs +43 -0
  42. package/scripts/plate.mjs +77 -0
  43. package/scripts/promote.mjs +27 -0
  44. package/scripts/prompt.mjs +124 -0
  45. package/scripts/render.mjs +39 -0
  46. package/scripts/review.mjs +79 -0
  47. package/scripts/scheduler.mjs +78 -0
  48. package/scripts/set.mjs +31 -0
  49. package/scripts/show.mjs +53 -0
  50. package/scripts/test/run.mjs +3943 -0
  51. package/scripts/validate.mjs +28 -0
  52. package/scripts/watch-prs.mjs +72 -0
  53. package/scripts/wizard.mjs +97 -0
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ // roadmap — validate a roadmap.yaml.
3
+ // Thin wrapper around lib/validate-core.mjs. Exits non-zero on any error.
4
+ // Usage: node validate.mjs [path-to-roadmap.yaml] (default: docs/roadmap/roadmap.yaml)
5
+
6
+ import { loadGraph } from "./lib/graph.mjs";
7
+ import { validateGraph } from "./lib/validate-core.mjs";
8
+
9
+ const path = process.argv[2] || "docs/roadmap/roadmap.yaml";
10
+
11
+ let graph;
12
+ try {
13
+ graph = loadGraph(path);
14
+ } catch (e) {
15
+ console.error(`✗ could not load ${path}: ${e.message}`);
16
+ process.exit(2);
17
+ }
18
+
19
+ const { errors, warnings, nodeCount } = validateGraph(graph);
20
+
21
+ for (const w of warnings) console.warn(`⚠ ${w}`);
22
+ if (errors.length) {
23
+ for (const e of errors) console.error(`✗ ${e}`);
24
+ console.error(`\n${errors.length} error(s) in ${path}`);
25
+ process.exit(1);
26
+ }
27
+ console.log(`✓ ${path} valid — ${(graph.pis || []).length} PIs, ${nodeCount} sprints, ${warnings.length} warning(s)`);
28
+ process.exit(0);
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ // roadmap — PR-watch monitor. Polls `gh pr list` for this roadmap's fanout branches and
3
+ // prints one line per PR phase transition (draft -> ready, checks -> green, open -> merged, ...).
4
+ // Bundled as a plugin monitor (monitors/monitors.json): each printed line becomes a notification
5
+ // to the lead session. Also runnable as `roadmap watch` in a pane.
6
+ //
7
+ // Quiet by design: the first poll establishes a silent baseline; only changes after that print.
8
+ // Degrades gracefully: if there's no roadmap or `gh` is missing/unauthenticated, it says so once
9
+ // and exits 0 (never a crash loop).
10
+
11
+ import { spawnSync } from "node:child_process";
12
+ import { join } from "node:path";
13
+ import { findRepoRoot, REL } from "./lib/cli-core.mjs";
14
+ import { loadGraph } from "./lib/graph.mjs";
15
+ import { diffPrStates, roadmapBranches, checksOf } from "./lib/pr-watch-core.mjs";
16
+
17
+ const POLL_MS = Number(process.env.ROADMAP_WATCH_INTERVAL_MS || 30000);
18
+ const log = (m) => process.stdout.write(m + "\n");
19
+
20
+ function loadRoadmap() {
21
+ const root = findRepoRoot(process.env.CODEX_PROJECT_DIR || process.env.CLAUDE_PROJECT_DIR || process.cwd());
22
+ if (!root) return null;
23
+ try {
24
+ return { root, graph: loadGraph(join(root, ...REL)) };
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ const ghAvailable = () => {
31
+ try { return spawnSync("gh", ["--version"], { encoding: "utf8" }).status === 0; }
32
+ catch { return false; }
33
+ };
34
+
35
+ function fetchPrs(root) {
36
+ const r = spawnSync("gh", ["pr", "list", "--state", "all", "--limit", "100",
37
+ "--json", "number,title,headRefName,state,isDraft,mergeStateStatus,statusCheckRollup"],
38
+ { cwd: root, encoding: "utf8" });
39
+ if (r.status !== 0) return null;
40
+ try { return JSON.parse(r.stdout); } catch { return null; }
41
+ }
42
+
43
+ function snapshot(prs, graph) {
44
+ const branches = roadmapBranches(graph);
45
+ const map = {};
46
+ for (const pr of prs) {
47
+ if (!branches.has(pr.headRefName)) continue; // only this roadmap's fanout branches
48
+ map[pr.number] = {
49
+ number: pr.number, title: pr.title, headRefName: pr.headRefName,
50
+ state: pr.state, isDraft: pr.isDraft, mergeStateStatus: pr.mergeStateStatus, checks: checksOf(pr),
51
+ };
52
+ }
53
+ return map;
54
+ }
55
+
56
+ const rm = loadRoadmap();
57
+ if (!rm) { log("roadmap-prs: no docs/roadmap/roadmap.yaml found; PR watch idle."); process.exit(0); }
58
+ if (!ghAvailable()) { log("roadmap-prs: `gh` CLI not found or not authenticated; PR watch disabled."); process.exit(0); }
59
+
60
+ let prev = {};
61
+ let primed = false;
62
+ function tick() {
63
+ const prs = fetchPrs(rm.root);
64
+ if (!prs) return; // transient gh failure; try again next interval
65
+ const curr = snapshot(prs, rm.graph);
66
+ if (!primed) { prev = curr; primed = true; return; } // silent baseline on first poll
67
+ for (const ev of diffPrStates(prev, curr)) log(ev.message);
68
+ prev = curr;
69
+ }
70
+
71
+ tick();
72
+ setInterval(tick, POLL_MS); // keep polling until the process is killed (monitor lifecycle / Ctrl-C)
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+ // roadmap — the interactive console (bare `roadmap` in a TTY, or `roadmap go`).
3
+ // Hot-loads THIS repo's roadmap (cwd = repo root, set by cli.mjs), shows what's runnable, then
4
+ // walks through a few prompts — terminal, max concurrency, wave, lead?, action — and hands the
5
+ // choices to fanout.mjs. Worker permission mode is NOT surfaced here: it comes from
6
+ // meta.worker_mode (or the --worker-mode flag). Nothing here merges; fanout enforces that.
7
+
8
+ import os from "node:os";
9
+ import { spawnSync } from "node:child_process";
10
+ import { dirname, join } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { loadGraph, flatten, computeWaves, readyNodes } from "./lib/graph.mjs";
13
+ import { recommendConcurrency } from "./lib/recommend.mjs";
14
+ import { terminalChoices, buildFanArgs, autoOutName } from "./lib/wizard-core.mjs";
15
+ import { select, number, confirm } from "./prompt.mjs";
16
+
17
+ const SCRIPTS = dirname(fileURLToPath(import.meta.url));
18
+ const INPATH = "docs/roadmap/roadmap.yaml";
19
+ const S = { reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m", cyan: "\x1b[36m", green: "\x1b[32m", red: "\x1b[31m" };
20
+
21
+ const TERM_HINTS = {
22
+ wt: "Windows Terminal tabs",
23
+ warp: "Warp tab config + deeplink",
24
+ tmux: "tmux panes (WSL/macOS/Linux)",
25
+ print: "print the commands only",
26
+ background: "detached background sessions",
27
+ };
28
+
29
+ async function main() {
30
+ let graph;
31
+ try { graph = loadGraph(INPATH); }
32
+ catch (e) { console.error(`${S.red}Couldn't load ${INPATH}: ${e.message}${S.reset}`); process.exit(1); }
33
+
34
+ const model = flatten(graph);
35
+ const ready = readyNodes(model);
36
+ const rec = recommendConcurrency(ready, graph);
37
+ const program = (graph.meta && graph.meta.program) || "roadmap";
38
+
39
+ console.log(`\n${S.bold}${S.cyan}${program} — fanout${S.reset} ${S.dim}${process.cwd()}${S.reset}`);
40
+
41
+ if (!ready.length) {
42
+ console.log(`\n${S.dim}No agent-runnable slices right now.${S.reset}`);
43
+ let held = null;
44
+ try { ({ held } = computeWaves(model, 1)); } catch { /* cycle — skip the held view */ }
45
+ if (held && held.onHuman.length) {
46
+ console.log("Held on a human:");
47
+ for (const n of held.onHuman) console.log(` • ${n.invoke} — gated on ${n.gatedOn}`);
48
+ }
49
+ console.log(`\nRun ${S.bold}roadmap plan${S.reset} for the full picture.`);
50
+ process.exit(0);
51
+ }
52
+
53
+ console.log(`${ready.length} ready slice(s) · recommended cap ${S.bold}${rec.recommended}${S.reset} ${S.dim}(${rec.binding.why.split(" — ")[0]})${S.reset}\n`);
54
+
55
+ // 1) terminal — platform default first, or meta.terminal if set
56
+ const termIds = terminalChoices(os.platform());
57
+ const metaTerm = graph.meta && graph.meta.terminal;
58
+ const defTermIdx = Math.max(0, termIds.indexOf(metaTerm || termIds[0]));
59
+ const term = await select(
60
+ "Terminal",
61
+ termIds.map((t) => ({ label: t, value: t, hint: TERM_HINTS[t] })),
62
+ { defaultIdx: defTermIdx },
63
+ );
64
+
65
+ // 2) max concurrency — default = recommended, clamp to the ready count
66
+ const cap = await number("Max concurrent sessions", { def: rec.recommended, min: 1, max: ready.length });
67
+
68
+ // 3) wave — recompute under the chosen cap
69
+ let waves;
70
+ try { ({ waves } = computeWaves(model, cap)); }
71
+ catch (e) { console.error(`${S.red}✗ ${e.message}${S.reset}`); process.exit(1); }
72
+ if (!waves.length) { console.log(`${S.dim}No runnable waves at cap ${cap}.${S.reset}`); process.exit(0); }
73
+ const wave = await select(
74
+ "Which wave",
75
+ waves.map((w, i) => ({ label: `Wave ${i + 1} — ${w.length} concurrent`, value: i + 1, hint: w.map((n) => n.invoke).join(", ") })),
76
+ { defaultIdx: 0 },
77
+ );
78
+
79
+ // 4) lead coordinator session?
80
+ const lead = await confirm("Open a LEAD coordinator session too?", false);
81
+
82
+ // 5) action
83
+ const mode = await select("Action", [
84
+ { label: "Launch now", value: "launch", hint: "open the wave" },
85
+ { label: "Preview only", value: "dry", hint: "print the launch script, spawn nothing" },
86
+ { label: "Save script to a file", value: "save", hint: "write it, don't launch" },
87
+ ], { defaultIdx: 0 });
88
+
89
+ const outName = mode === "save" ? autoOutName(term, wave) : undefined;
90
+ const fanArgs = buildFanArgs({ term, cap, wave, lead, mode, outName });
91
+
92
+ console.log(`\n${S.green}▶${S.reset} roadmap fan ${fanArgs.join(" ")}\n`);
93
+ const r = spawnSync("node", [join(SCRIPTS, "fanout.mjs"), ...fanArgs], { stdio: "inherit", cwd: process.cwd() });
94
+ process.exit(r.status ?? 0);
95
+ }
96
+
97
+ main();