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 +8 -0
- package/README.md +1 -1
- package/bin/scrumrun.js +2 -2
- package/lib/view/server.js +5 -3
- package/package.json +1 -1
- package/templates/shared/view.html +32 -13
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.
|
|
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
|
|
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]) :
|
|
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 });
|
package/lib/view/server.js
CHANGED
|
@@ -59,13 +59,15 @@ function requestHandler(root) {
|
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
function listen(root, { host = "127.0.0.1", port =
|
|
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
|
-
|
|
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 =
|
|
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
|
@@ -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-
|
|
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
|
-
|
|
283
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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) => ({ "&": "&", "<": "<", ">": ">" }[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")
|
|
447
|
+
const savedMode = localStorage.getItem("scrumrun.view.mode") === "list" ? "list" : "kanban";
|
|
429
448
|
function setMode(mode) {
|
|
430
|
-
|
|
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);
|