scrumrun 4.2.1 → 4.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,14 @@ All notable changes follow Semantic Versioning.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 4.2.2 - 2026-09-22
8
+
9
+ ### Changed
10
+
11
+ - **`scrumrun view` picks a random free port by default.** Running `scrumrun view` without `--port` now asks the OS for any ephemeral port instead of always trying `8080` first (a common conflict with Vite, Django, and other dev servers). Pass `--port <n>` explicitly if you need a stable URL; sequential retry only fires when the port was explicitly requested. The bound URL is always printed on stdout and the browser is auto-launched at the correct port.
12
+ - **Kanban card width capped.** Columns are constrained to `280–320 px` and align to the left of the grid, so a single populated column no longer stretches its cards to full viewport width. The `Compact` view mode was removed — `Kanban` and `List` cover the two real use-cases.
13
+ - **Guardrail rows truncate the title at 150 characters.** Long rules no longer produce a linha extensa e ilegível; the row shows the id, a truncated title with `…`, and clicking the row opens the detail drawer with the **full rule body rendered as Markdown** (fields, prose, YAML enforcement block).
14
+
7
15
  ## 4.2.1 - 2026-09-22
8
16
 
9
17
  ### Changed
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  ScrumRun gives an agent a small command surface and a precise project memory: what should be done, how each attempt happened, which decisions constrain the code, and why the architecture exists in its current form.
6
6
 
7
- **Package:** `4.2.1` · **Method target:** `2.0.0` · **Runtime:** Node.js `>=22.13.0` · **License:** MIT
7
+ **Package:** `4.2.2` · **Method target:** `2.0.0` · **Runtime:** Node.js `>=22.13.0` · **License:** MIT
8
8
 
9
9
  **New here?** Read the [Quickstart](docs/QUICKSTART.md) — first Run in under 10 minutes, no `SPEC.md` reading required. Full docs map in [`docs/INDEX.md`](docs/INDEX.md).
10
10
 
package/bin/scrumrun.js CHANGED
@@ -64,7 +64,7 @@ Usage:
64
64
  scrumrun update [all|codex|opencode|claude] [--project] [--seal-policy] [--migrate] [--repair-legacy] [--verbose]
65
65
  scrumrun init [--local|--shared] [--lean] [--no-agent-hint] [--force]
66
66
  scrumrun status
67
- scrumrun view [--port 8080] [--host 127.0.0.1] [--no-open]
67
+ scrumrun view [--port <n>] [--host 127.0.0.1] [--no-open] # port defaults to random ephemeral
68
68
  scrumrun sync [--repair] [--apply]
69
69
  scrumrun lock <acquire|release|status|reap> [ARTIFACT-ID] [--note "..."] [--ttl <minutes>] [--force]
70
70
  scrumrun core [--path|--prompt]
@@ -2805,7 +2805,7 @@ if (!command || command === "--help" || command === "-h") {
2805
2805
  const { view } = require(path.join(root, "lib", "view", "server"));
2806
2806
  const portArg = args.indexOf("--port");
2807
2807
  const hostArg = args.indexOf("--host");
2808
- const port = portArg !== -1 && args[portArg + 1] ? Number(args[portArg + 1]) : 8080;
2808
+ const port = portArg !== -1 && args[portArg + 1] ? Number(args[portArg + 1]) : 0;
2809
2809
  const host = hostArg !== -1 && args[hostArg + 1] ? args[hostArg + 1] : "127.0.0.1";
2810
2810
  const autoOpen = !args.includes("--no-open");
2811
2811
  const { server } = await view(process.cwd(), { port, host, autoOpen });
@@ -59,13 +59,15 @@ function requestHandler(root) {
59
59
  };
60
60
  }
61
61
 
62
- function listen(root, { host = "127.0.0.1", port = 8080 } = {}) {
62
+ function listen(root, { host = "127.0.0.1", port = 0 } = {}) {
63
63
  return new Promise((resolve, reject) => {
64
64
  const server = http.createServer(requestHandler(root));
65
65
  let attempts = 0;
66
66
  const tryPort = (p) => {
67
67
  server.once("error", (err) => {
68
- if (err.code === "EADDRINUSE" && attempts < 20) {
68
+ // Only retry sequentially when the user asked for a specific port; port 0
69
+ // means "let the OS pick", so a collision there is genuinely fatal.
70
+ if (err.code === "EADDRINUSE" && p !== 0 && attempts < 20) {
69
71
  attempts += 1;
70
72
  setImmediate(() => tryPort(p + 1));
71
73
  } else {
@@ -88,7 +90,7 @@ function openInBrowser(url) {
88
90
  return spawn("xdg-open", [url], { stdio: "ignore", detached: true }).unref();
89
91
  }
90
92
 
91
- async function view(projectRoot, { port = 8080, host = "127.0.0.1", autoOpen = true, logger = console } = {}) {
93
+ async function view(projectRoot, { port = 0, host = "127.0.0.1", autoOpen = true, logger = console } = {}) {
92
94
  const root = path.join(projectRoot, ".scrumrun");
93
95
  if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
94
96
  throw new Error(`ScrumRun project not found at ${root}. Run \`scrumrun init\` first.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scrumrun",
3
- "version": "4.2.1",
3
+ "version": "4.2.2",
4
4
  "description": "Markdown-first Agile memory and guardrails for AI coding agents.",
5
5
  "bin": {
6
6
  "scrumrun": "bin/scrumrun.js",
@@ -34,8 +34,7 @@
34
34
  .toolbar button { background: transparent; border: 0; color: var(--muted); padding: 5px 10px; font: inherit; font-size: 12px; cursor: pointer; }
35
35
  .toolbar button.active { background: var(--border); color: var(--fg); }
36
36
  .toolbar button:hover { color: var(--fg); }
37
- .kanban { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 14px; align-items: start; }
38
- .kanban.compact { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; }
37
+ .kanban { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 320px)); gap: 14px; align-items: start; justify-content: start; }
39
38
  .col { min-width: 0; }
40
39
  .col h3 { margin: 0 0 10px; font-size: 11px; text-transform: uppercase; color: var(--muted); letter-spacing: .06em; display: flex; align-items: center; gap: 6px; }
41
40
  .col h3 .count { background: var(--border); color: var(--fg); padding: 1px 6px; border-radius: 10px; font-family: var(--mono); font-size: 10px; }
@@ -47,9 +46,6 @@
47
46
  .card .id { font-family: var(--mono); font-size: 10px; color: var(--muted); letter-spacing: .04em; }
48
47
  .card .title { font-size: 13.5px; font-weight: 500; margin-top: 3px; line-height: 1.35; color: var(--fg); }
49
48
  .card .meta { font-size: 11px; color: var(--muted); margin-top: 8px; display: flex; gap: 5px; flex-wrap: wrap; }
50
- .kanban.compact .card { padding: 7px 9px; }
51
- .kanban.compact .card .title { font-size: 12.5px; }
52
- .kanban.compact .card .meta { display: none; }
53
49
  body.list-mode .kanban { display: block; }
54
50
  body.list-mode .col { margin-bottom: 20px; }
55
51
  body.list-mode .col-body { max-height: none; }
@@ -120,7 +116,6 @@
120
116
  <div class="toolbar">
121
117
  <div class="group" role="tablist" aria-label="View mode">
122
118
  <button data-mode="kanban" class="active">Kanban</button>
123
- <button data-mode="compact">Compact</button>
124
119
  <button data-mode="list">List</button>
125
120
  </div>
126
121
  </div>
@@ -275,16 +270,40 @@
275
270
  }
276
271
  }
277
272
 
273
+ function truncate(text, max = 150) {
274
+ const t = String(text || "").replace(/\s+/g, " ").trim();
275
+ return t.length > max ? t.slice(0, max - 1).trimEnd() + "…" : t;
276
+ }
277
+
278
278
  function renderGuardrailsFromText(text) {
279
279
  const el = document.getElementById("guardrails");
280
280
  if (!text) { el.innerHTML = '<div class="empty">guardrails.md not found</div>'; return; }
281
281
  const rules = [];
282
- for (const match of text.matchAll(/^##\s+(GR-\d+)\s+-\s+(.+)$/gm)) {
283
- rules.push({ id: match[1], title: match[2] });
282
+ // Parse each ## GR-XXX block: capture id, title, and body until next ## or EOF
283
+ const blockRe = /^##\s+(GR-\d+)\s+-\s+(.+?)\r?\n([\s\S]*?)(?=^##\s+GR-|\Z)/gm;
284
+ let match;
285
+ while ((match = blockRe.exec(text)) !== null) {
286
+ rules.push({ id: match[1], title: match[2].trim(), body: match[3].trim() });
287
+ }
288
+ el.innerHTML = "";
289
+ if (!rules.length) { el.innerHTML = '<div class="empty">no guardrails declared</div>'; return; }
290
+ for (const rule of rules) {
291
+ const row = document.createElement("div");
292
+ row.className = "list-item";
293
+ row.innerHTML = `<span class="row-id">${rule.id}</span><span class="row-text">${escape(truncate(rule.title, 150))}</span>`;
294
+ row.addEventListener("click", () => openGuardrailDetail(rule));
295
+ el.appendChild(row);
284
296
  }
285
- el.innerHTML = rules.length
286
- ? rules.map((r) => `<div class="list-item"><span class="row-id">${r.id}</span><span class="row-text">${escape(r.title)}</span></div>`).join("")
287
- : '<div class="empty">no guardrails declared</div>';
297
+ }
298
+
299
+ function openGuardrailDetail(rule) {
300
+ const body = document.getElementById("detail-body");
301
+ body.innerHTML = `
302
+ <h2 style="margin-top:0;font-size:15px">${escape(rule.id)}</h2>
303
+ <div class="sub" style="margin-bottom:12px">${escape(rule.title)}</div>
304
+ <div class="md">${renderMarkdown(rule.body)}</div>`;
305
+ document.getElementById("detail").classList.add("open");
306
+ document.getElementById("detail").setAttribute("aria-hidden", "false");
288
307
  }
289
308
 
290
309
  function escape(s) { return String(s).replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" }[c])); }
@@ -425,9 +444,9 @@
425
444
  document.getElementById("search").addEventListener("input", applyFilters);
426
445
 
427
446
  const kanbanEl = document.getElementById("kanban");
428
- const savedMode = localStorage.getItem("scrumrun.view.mode") || "kanban";
447
+ const savedMode = localStorage.getItem("scrumrun.view.mode") === "list" ? "list" : "kanban";
429
448
  function setMode(mode) {
430
- kanbanEl.classList.toggle("compact", mode === "compact");
449
+ if (mode !== "list") mode = "kanban";
431
450
  document.body.classList.toggle("list-mode", mode === "list");
432
451
  for (const button of document.querySelectorAll(".toolbar button")) {
433
452
  button.classList.toggle("active", button.dataset.mode === mode);