scrumrun 4.2.0 → 4.2.1
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 +9 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/templates/shared/view.html +154 -20
package/CHANGELOG.md
CHANGED
|
@@ -4,13 +4,21 @@ All notable changes follow Semantic Versioning.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 4.2.1 - 2026-09-22
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **`scrumrun view` drawer renders Markdown.** The right-side detail panel now parses artifact bodies (headings h1–h4, bullet + ordered lists, inline `code` and fenced code blocks, links, bold/italic, blockquotes, `hr`) and displays them formatted instead of dumping the raw file. A structured metadata card (`id`, `kind`, `status`, `type`, `created`, `updated`, `feature`, `sprint`, `branch`, `assignee`, `method` — plus any extra frontmatter fields) renders above the body. Artifact ids (`TASK-*`, `RUN-*`, `FEAT-*`, `SPRINT-*`, `DEC-*`, `K-*`, `INS-*`, `DOS-*`, `GR-*`, `REV-*`) appearing in prose are auto-linked as `<code>`. A "open raw file" link at the bottom keeps the plain view one click away.
|
|
12
|
+
- **Canonical status columns always visible.** `backlog`, `running`, and `completed` render even when they contain no tasks, so the kanban layout is predictable across projects. Empty columns show a `—` placeholder.
|
|
13
|
+
- **Guardrails / Recent Runs / Decisions list layout.** Id renders in monospace on the left (72 px reserved column) with the title next to it — no more `justify-between` stretch that pushed the title to the far right of a wide viewport. Optional status badge on the right side.
|
|
14
|
+
|
|
7
15
|
## 4.2.0 - 2026-09-22
|
|
8
16
|
|
|
9
17
|
### Added
|
|
10
18
|
|
|
11
19
|
- **`scrumrun sync`.** Team-first git wrapper: fetches the remote, rebases the current branch, refreshes `state.md`, and optionally runs `repair --apply` (with `--repair --apply`). Refuses cleanly when the tree is dirty, when git isn't set up, when there's no remote, or when HEAD is detached — so no half-applied merges. Returns structured JSON so wrappers can automate.
|
|
12
20
|
- **`scrumrun lock <acquire|release|status|reap> [ARTIFACT-ID]`.** Cooperative soft locks under `.scrumrun/.locks/<ID>.lock`. Auto-expire after 15 minutes (configurable via `--ttl <minutes>`). Non-owner release is refused unless `--force`. `status` (without id) lists every active lock; `reap` sweeps expired locks. Advisory only — never gates writes, but surfaces "who's touching what" for teams working on the same repo. Owner detected from `SCRUMRUN_AGENT`/`USER`/`USERNAME`.
|
|
13
|
-
- **`scrumrun view` visual polish.** Kanban / Compact / List mode toggle in the toolbar (persisted in `localStorage`). Per-column scroll containers so a huge Completed column doesn't stretch the whole grid. Card hover elevation, larger titles, rounded corners, count pills next to status headers. Cards now render `@assignee` as a highlighted chip and expose `data-assignee` for `?assignee=<name>` filtering. `?readonly=1` disables interactive affordances for stakeholder viewing.
|
|
21
|
+
- **`scrumrun view` visual polish.** Kanban / Compact / List mode toggle in the toolbar (persisted in `localStorage`). Per-column scroll containers so a huge Completed column doesn't stretch the whole grid. Card hover elevation, larger titles, rounded corners, count pills next to status headers. Cards now render `@assignee` as a highlighted chip and expose `data-assignee` for `?assignee=<name>` filtering. `?readonly=1` disables interactive affordances for stakeholder viewing. Canonical columns (`backlog`, `running`, `completed`) always render even when empty so the layout stays predictable. **Detail drawer now renders Markdown** — headings, lists, code (inline + fenced), links, bold/italic, blockquotes, horizontal rules — plus a structured metadata card and auto-linked artifact ids. Guardrails / Recent Runs / Decisions lists now show the id on the left and the title next to it (no more far-right stretch), with an optional status badge on the right.
|
|
14
22
|
|
|
15
23
|
## 4.1.4 - 2026-09-22
|
|
16
24
|
|
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.1` · **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/package.json
CHANGED
|
@@ -71,11 +71,37 @@
|
|
|
71
71
|
#detail.open { transform: translateX(0); }
|
|
72
72
|
#detail pre { white-space: pre-wrap; word-break: break-word; font-family: var(--mono); font-size: 12px; background: var(--bg); padding: 10px; border-radius: 6px; border: 1px solid var(--border); }
|
|
73
73
|
#detail .close { float: right; background: none; border: 0; color: var(--muted); font-size: 20px; cursor: pointer; }
|
|
74
|
-
.
|
|
74
|
+
.md { line-height: 1.55; font-size: 13.5px; color: var(--fg); }
|
|
75
|
+
.md h1, .md h2, .md h3, .md h4 { margin: 18px 0 8px; font-weight: 600; }
|
|
76
|
+
.md h1 { font-size: 20px; }
|
|
77
|
+
.md h2 { font-size: 16px; padding-bottom: 4px; border-bottom: 1px solid var(--border); }
|
|
78
|
+
.md h3 { font-size: 14px; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
|
|
79
|
+
.md h4 { font-size: 13px; color: var(--muted); }
|
|
80
|
+
.md p { margin: 8px 0; }
|
|
81
|
+
.md ul, .md ol { margin: 8px 0; padding-left: 22px; }
|
|
82
|
+
.md li { margin: 4px 0; }
|
|
83
|
+
.md code { font-family: var(--mono); font-size: 12px; background: var(--bg); padding: 1px 5px; border-radius: 3px; border: 1px solid var(--border); }
|
|
84
|
+
.md pre.md-code { white-space: pre-wrap; word-break: break-word; font-family: var(--mono); font-size: 12px; background: var(--bg); padding: 10px 12px; border-radius: 6px; border: 1px solid var(--border); margin: 10px 0; }
|
|
85
|
+
.md pre.md-code code { background: none; border: 0; padding: 0; font-size: inherit; }
|
|
86
|
+
.md a { color: var(--accent); text-decoration: none; }
|
|
87
|
+
.md a:hover { text-decoration: underline; }
|
|
88
|
+
.md strong { color: var(--fg); font-weight: 600; }
|
|
89
|
+
.md em { font-style: italic; }
|
|
90
|
+
.md hr { border: 0; border-top: 1px solid var(--border); margin: 14px 0; }
|
|
91
|
+
.md blockquote { border-left: 3px solid var(--border); padding: 4px 12px; margin: 10px 0; color: var(--muted); }
|
|
92
|
+
.md table { border-collapse: collapse; margin: 10px 0; font-size: 12.5px; }
|
|
93
|
+
.md th, .md td { border: 1px solid var(--border); padding: 4px 8px; }
|
|
94
|
+
.md th { background: var(--bg); font-weight: 600; }
|
|
95
|
+
.md-meta { display: grid; grid-template-columns: 100px 1fr; gap: 4px 12px; font-size: 12px; padding: 10px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; margin-bottom: 12px; }
|
|
96
|
+
.md-meta .k { font-family: var(--mono); color: var(--muted); font-size: 11px; }
|
|
97
|
+
.md-meta .v { font-family: var(--mono); font-size: 11.5px; word-break: break-all; }
|
|
98
|
+
.list-item { padding: 8px 0; border-bottom: 1px dotted var(--border); cursor: pointer; display: flex; align-items: baseline; gap: 12px; }
|
|
75
99
|
.list-item:last-child { border-bottom: 0; }
|
|
76
|
-
.list-item:hover { color: var(--
|
|
77
|
-
.
|
|
78
|
-
.row
|
|
100
|
+
.list-item:hover .row-text { color: var(--fg); }
|
|
101
|
+
.list-item:hover .row-id { color: var(--accent); }
|
|
102
|
+
.row-id { font-family: var(--mono); font-size: 11px; color: var(--muted); min-width: 72px; flex-shrink: 0; letter-spacing: .04em; }
|
|
103
|
+
.row-text { font-size: 13px; color: var(--fg); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
104
|
+
.row-meta { font-size: 11px; color: var(--muted); flex-shrink: 0; }
|
|
79
105
|
.header-right { margin-left: auto; display: flex; gap: 12px; align-items: center; }
|
|
80
106
|
</style>
|
|
81
107
|
</head>
|
|
@@ -121,6 +147,7 @@
|
|
|
121
147
|
(() => {
|
|
122
148
|
"use strict";
|
|
123
149
|
const STATUS_ORDER = ["backlog", "running", "in_progress", "validating", "learning", "blocked", "failed", "completed"];
|
|
150
|
+
const ALWAYS_SHOW_STATUSES = ["backlog", "running", "completed"];
|
|
124
151
|
const STATUS_LABELS = {
|
|
125
152
|
backlog: "Backlog",
|
|
126
153
|
running: "Running",
|
|
@@ -190,18 +217,26 @@
|
|
|
190
217
|
if (!bucket[status]) bucket[status] = [];
|
|
191
218
|
bucket[status].push(task);
|
|
192
219
|
}
|
|
193
|
-
const ordered = STATUS_ORDER.filter((s) => bucket[s] && bucket[s].length);
|
|
220
|
+
const ordered = STATUS_ORDER.filter((s) => (bucket[s] && bucket[s].length) || ALWAYS_SHOW_STATUSES.includes(s));
|
|
194
221
|
const container = document.getElementById("kanban");
|
|
195
222
|
container.innerHTML = "";
|
|
196
|
-
if (!
|
|
223
|
+
if (!tasks.length) { container.innerHTML = '<div class="empty">no tasks found</div>'; return; }
|
|
197
224
|
for (const status of ordered) {
|
|
225
|
+
const items = bucket[status] || [];
|
|
198
226
|
const col = document.createElement("div");
|
|
199
227
|
col.className = "col";
|
|
200
|
-
col.innerHTML = `<h3>${STATUS_LABELS[status] || status} <span class="count">${
|
|
228
|
+
col.innerHTML = `<h3>${STATUS_LABELS[status] || status} <span class="count">${items.length}</span></h3>`;
|
|
201
229
|
const body = document.createElement("div");
|
|
202
230
|
body.className = "col-body";
|
|
203
231
|
col.appendChild(body);
|
|
204
|
-
|
|
232
|
+
if (!items.length) {
|
|
233
|
+
const empty = document.createElement("div");
|
|
234
|
+
empty.className = "empty";
|
|
235
|
+
empty.style.padding = "6px 0";
|
|
236
|
+
empty.textContent = "—";
|
|
237
|
+
body.appendChild(empty);
|
|
238
|
+
}
|
|
239
|
+
for (const task of items) {
|
|
205
240
|
const title = extractHeading(task.body);
|
|
206
241
|
const type = task.record.type || "task";
|
|
207
242
|
const branch = task.record.branch;
|
|
@@ -235,7 +270,7 @@
|
|
|
235
270
|
const row = document.createElement("div");
|
|
236
271
|
row.className = "list-item";
|
|
237
272
|
row.innerHTML = format(item);
|
|
238
|
-
row.addEventListener("click", () => openDetail(item));
|
|
273
|
+
if (item && item.record) row.addEventListener("click", () => openDetail(item));
|
|
239
274
|
el.appendChild(row);
|
|
240
275
|
}
|
|
241
276
|
}
|
|
@@ -248,26 +283,125 @@
|
|
|
248
283
|
rules.push({ id: match[1], title: match[2] });
|
|
249
284
|
}
|
|
250
285
|
el.innerHTML = rules.length
|
|
251
|
-
? rules.map((r) => `<div class="
|
|
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("")
|
|
252
287
|
: '<div class="empty">no guardrails declared</div>';
|
|
253
288
|
}
|
|
254
289
|
|
|
290
|
+
function escape(s) { return String(s).replace(/[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" }[c])); }
|
|
291
|
+
|
|
292
|
+
function renderInline(text) {
|
|
293
|
+
let s = escape(text);
|
|
294
|
+
// code inline first (so we don't process markdown inside)
|
|
295
|
+
s = s.replace(/`([^`]+)`/g, (_, code) => `<code>${code}</code>`);
|
|
296
|
+
// links [text](url)
|
|
297
|
+
s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, href) => `<a href="${href}" target="_blank" rel="noopener">${label}</a>`);
|
|
298
|
+
// bold and italic
|
|
299
|
+
s = s.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
300
|
+
s = s.replace(/(^|[^*])\*([^*]+)\*/g, "$1<em>$2</em>");
|
|
301
|
+
// auto-link bare TASK-NNN / RUN-NNN / etc — do it last, and skip if already inside a tag
|
|
302
|
+
s = s.replace(/(^|\s)(TASK-\d+|RUN-\d+|FEAT-\d+|SPRINT-\d+|DEC-\d+|K-\d+|INS-\d+|DOS-\d+|GR-\d+|REV-\d+)(?![^<]*>)/g, "$1<code>$2</code>");
|
|
303
|
+
return s;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function renderMarkdown(source) {
|
|
307
|
+
const lines = String(source || "").split(/\r?\n/);
|
|
308
|
+
const out = [];
|
|
309
|
+
let i = 0;
|
|
310
|
+
let inCode = false;
|
|
311
|
+
let codeBuf = [];
|
|
312
|
+
const flushCode = () => {
|
|
313
|
+
out.push(`<pre class="md-code"><code>${escape(codeBuf.join("\n"))}</code></pre>`);
|
|
314
|
+
codeBuf = [];
|
|
315
|
+
};
|
|
316
|
+
while (i < lines.length) {
|
|
317
|
+
const line = lines[i];
|
|
318
|
+
if (/^```/.test(line)) {
|
|
319
|
+
if (inCode) { flushCode(); inCode = false; }
|
|
320
|
+
else { inCode = true; }
|
|
321
|
+
i += 1; continue;
|
|
322
|
+
}
|
|
323
|
+
if (inCode) { codeBuf.push(line); i += 1; continue; }
|
|
324
|
+
if (/^#{1,4}\s/.test(line)) {
|
|
325
|
+
const level = line.match(/^(#{1,4})/)[1].length;
|
|
326
|
+
const text = line.replace(/^#{1,4}\s+/, "");
|
|
327
|
+
out.push(`<h${level}>${renderInline(text)}</h${level}>`);
|
|
328
|
+
i += 1; continue;
|
|
329
|
+
}
|
|
330
|
+
if (/^\s*(-{3,}|\*{3,})\s*$/.test(line)) {
|
|
331
|
+
out.push("<hr />");
|
|
332
|
+
i += 1; continue;
|
|
333
|
+
}
|
|
334
|
+
if (/^\s*>\s?/.test(line)) {
|
|
335
|
+
const chunk = [];
|
|
336
|
+
while (i < lines.length && /^\s*>\s?/.test(lines[i])) {
|
|
337
|
+
chunk.push(lines[i].replace(/^\s*>\s?/, ""));
|
|
338
|
+
i += 1;
|
|
339
|
+
}
|
|
340
|
+
out.push(`<blockquote>${renderInline(chunk.join(" "))}</blockquote>`);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (/^\s*[-*]\s+/.test(line)) {
|
|
344
|
+
const items = [];
|
|
345
|
+
while (i < lines.length && /^\s*[-*]\s+/.test(lines[i])) {
|
|
346
|
+
items.push(`<li>${renderInline(lines[i].replace(/^\s*[-*]\s+/, ""))}</li>`);
|
|
347
|
+
i += 1;
|
|
348
|
+
}
|
|
349
|
+
out.push(`<ul>${items.join("")}</ul>`);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (/^\s*\d+\.\s+/.test(line)) {
|
|
353
|
+
const items = [];
|
|
354
|
+
while (i < lines.length && /^\s*\d+\.\s+/.test(lines[i])) {
|
|
355
|
+
items.push(`<li>${renderInline(lines[i].replace(/^\s*\d+\.\s+/, ""))}</li>`);
|
|
356
|
+
i += 1;
|
|
357
|
+
}
|
|
358
|
+
out.push(`<ol>${items.join("")}</ol>`);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (!line.trim()) { i += 1; continue; }
|
|
362
|
+
// paragraph — accumulate until blank line
|
|
363
|
+
const para = [line];
|
|
364
|
+
i += 1;
|
|
365
|
+
while (i < lines.length && lines[i].trim() && !/^(#{1,4}\s|```|\s*[-*]\s|\s*\d+\.\s|\s*>|-{3,}\s*$)/.test(lines[i])) {
|
|
366
|
+
para.push(lines[i]);
|
|
367
|
+
i += 1;
|
|
368
|
+
}
|
|
369
|
+
out.push(`<p>${renderInline(para.join(" "))}</p>`);
|
|
370
|
+
}
|
|
371
|
+
if (inCode && codeBuf.length) flushCode();
|
|
372
|
+
return out.join("\n");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function renderMeta(record) {
|
|
376
|
+
const order = ["id", "kind", "status", "type", "created", "updated", "feature", "sprint", "branch", "assignee", "completed_via", "method"];
|
|
377
|
+
const seen = new Set();
|
|
378
|
+
const rows = [];
|
|
379
|
+
for (const key of order) {
|
|
380
|
+
if (record[key] === undefined || record[key] === null || record[key] === "") continue;
|
|
381
|
+
seen.add(key);
|
|
382
|
+
rows.push(`<span class="k">${escape(key)}</span><span class="v">${escape(String(record[key]))}</span>`);
|
|
383
|
+
}
|
|
384
|
+
for (const [key, value] of Object.entries(record)) {
|
|
385
|
+
if (seen.has(key) || value === undefined || value === null || value === "") continue;
|
|
386
|
+
rows.push(`<span class="k">${escape(key)}</span><span class="v">${escape(String(value))}</span>`);
|
|
387
|
+
}
|
|
388
|
+
return `<div class="md-meta">${rows.join("")}</div>`;
|
|
389
|
+
}
|
|
390
|
+
|
|
255
391
|
function openDetail(artifact) {
|
|
256
392
|
const body = document.getElementById("detail-body");
|
|
257
393
|
const record = artifact.record || {};
|
|
258
|
-
const
|
|
394
|
+
const title = extractHeading(artifact.body || "");
|
|
259
395
|
body.innerHTML = `
|
|
260
|
-
<h2 style="margin-top:0;font-size:
|
|
261
|
-
<div class="sub" style="margin-bottom:
|
|
262
|
-
|
|
263
|
-
<
|
|
264
|
-
<div class="sub"><a href="${artifact.file || "#"}" target="_blank" rel="noopener">open raw file</a></div>`;
|
|
396
|
+
<h2 style="margin-top:0;font-size:15px">${escape(record.id || "(unknown)")}</h2>
|
|
397
|
+
<div class="sub" style="margin-bottom:12px">${escape(title)}</div>
|
|
398
|
+
${renderMeta(record)}
|
|
399
|
+
<div class="md">${renderMarkdown(artifact.body || "")}</div>
|
|
400
|
+
<div class="sub" style="margin-top:14px"><a href="${artifact.file || "#"}" target="_blank" rel="noopener">open raw file</a></div>`;
|
|
265
401
|
document.getElementById("detail").classList.add("open");
|
|
266
402
|
document.getElementById("detail").setAttribute("aria-hidden", "false");
|
|
267
403
|
}
|
|
268
404
|
|
|
269
|
-
function escape(s) { return String(s).replace(/[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" }[c])); }
|
|
270
|
-
|
|
271
405
|
document.getElementById("detail-close").addEventListener("click", () => {
|
|
272
406
|
document.getElementById("detail").classList.remove("open");
|
|
273
407
|
document.getElementById("detail").setAttribute("aria-hidden", "true");
|
|
@@ -333,8 +467,8 @@
|
|
|
333
467
|
}
|
|
334
468
|
renderKanban(tasks);
|
|
335
469
|
applyFilters();
|
|
336
|
-
renderList("runs", runs.slice().reverse().slice(0, 20), (r) => `<
|
|
337
|
-
renderList("decisions", decisions, (d) => `<
|
|
470
|
+
renderList("runs", runs.slice().reverse().slice(0, 20), (r) => `<span class="row-id">${r.record.id}</span><span class="row-text">${escape(extractHeading(r.body))}</span>${r.record.status ? `<span class="row-meta">${r.record.status}</span>` : ""}`);
|
|
471
|
+
renderList("decisions", decisions, (d) => `<span class="row-id">${d.record.id}</span><span class="row-text">${escape(extractHeading(d.body))}</span>${d.record.status ? `<span class="row-meta">${d.record.status}</span>` : ""}`);
|
|
338
472
|
renderGuardrailsFromText(guardrailsText);
|
|
339
473
|
}
|
|
340
474
|
|