scrumrun 3.1.2 → 4.1.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 (44) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/CORE.md +13 -5
  3. package/README.md +73 -16
  4. package/SPEC.md +38 -3
  5. package/bin/scrumrun.js +102 -13
  6. package/docs/COMMANDS.md +17 -7
  7. package/docs/ENTITY-MODEL.md +17 -14
  8. package/docs/ERROR-CODES.md +4 -0
  9. package/docs/QUICKSTART.md +21 -19
  10. package/docs/SCHEMA.md +14 -10
  11. package/docs/SEMANTIC-MEMORY.md +28 -0
  12. package/lib/actions/index.js +81 -0
  13. package/lib/commands/manifest.js +4 -3
  14. package/lib/commands/render.js +3 -2
  15. package/lib/errors.js +4 -0
  16. package/lib/git/context.js +30 -0
  17. package/lib/guardrails/changeset.js +45 -0
  18. package/lib/guardrails/evaluate.js +175 -0
  19. package/lib/memory/compaction.js +289 -0
  20. package/lib/memory/index.js +62 -2
  21. package/lib/migrate/ops.js +92 -0
  22. package/lib/migrate/run.js +108 -0
  23. package/lib/runtime/briefing.js +29 -0
  24. package/lib/runtime/context.js +3 -1
  25. package/lib/runtime/orchestrator.js +4 -4
  26. package/lib/runtime/policy-engine.js +11 -0
  27. package/lib/runtime/policy-integrity.js +83 -0
  28. package/lib/runtime/watcher.js +185 -0
  29. package/lib/v2/artifacts.js +9 -0
  30. package/lib/v2/conformance.js +45 -18
  31. package/lib/v2/paths.js +2 -1
  32. package/lib/v2/runs-jsonl.js +134 -0
  33. package/lib/v2/schema.js +7 -7
  34. package/lib/v2/task-schema.js +133 -0
  35. package/package.json +2 -2
  36. package/scripts/generate-contract-docs.js +6 -2
  37. package/templates/project/.scrumrun/config.md +9 -0
  38. package/templates/project/.scrumrun/method.json +3 -0
  39. package/templates/project/AGENTS.md +10 -4
  40. package/templates/project-lean/AGENTS.md +6 -2
  41. package/templates/shared/hooks/pre-commit +16 -0
  42. package/templates/shared/skills/scrumrun/SKILL.md +11 -7
  43. package/templates/shared/view.html +281 -0
  44. package/types/index.d.ts +1 -1
@@ -3,7 +3,9 @@ name: scrumrun
3
3
  description: Use when initializing or migrating ScrumRun, handling product requests, planning or executing Tasks/Sprints/Features/Runs, managing guardrails or semantic memory, checking status, and running reviews.
4
4
  ---
5
5
 
6
- # ScrumRun 2.0
6
+ # ScrumRun 4.0 — execution-first Markdown
7
+
8
+ Package 4.0 implements the stable ScrumRun method contract 2.0.0.
7
9
 
8
10
  ScrumRun is an evidence-driven Agile runtime for AI agents. Its canonical shell command is:
9
11
 
@@ -36,9 +38,11 @@ Normal hot path:
36
38
  5. follow the briefing's pointers to only the relevant canonical artifacts; go deeper only when the briefing lacks what you need (`## Where to look`, `scrumrun knowledge study "<topic>"`);
37
39
  6. load `.scrumrun/core.md` when the method contract or an exceptional transition is needed.
38
40
 
39
- **Markdown is the daily runtime.** After approval, work directly in source files and the relevant `.scrumrun/` Markdown. Create/refine the Task, its acceptance criteria, technical summary, follow-ups, and optional Run handoff without waiting for a CLI transition. Do not invoke `npx scrumrun@latest` or normal `scrumrun plan/run` commands during execution. The CLI is optional maintenance for `init`, `update --project`, `migrate`, `repair`, `doctor`, reports, and release checks.
41
+ **Markdown is the daily runtime.** After approval, work directly in source files and the relevant `.scrumrun/` Markdown until the approved Task is delivered. Continue the full loop — discover → implement → verify → fix → verify — before responding. A progress report is allowed only when the owner asks for status and must be followed immediately by further execution; it never ends the Task. Do not stop to issue a progress report, inventory, decomposition, or list of remaining work; those are internal implementation steps. A missing implementation discovered in scope remains required work now, never a follow-up or a “next step”. Stop only for an owner decision, external access, an explicit Guardrail, security/secret risk, destructive work without approval, or an unmet required delivery criterion. Do not invoke `npx scrumrun@latest` or normal `scrumrun plan/run` commands during execution. The CLI is optional maintenance for `init`, `update --project`, `migrate`, `repair`, `doctor`, reports, and release checks.
42
+
43
+ **Do not block on administrative state.** A missing/invalid Run, legacy status vocabulary, stale generated view, or optional unrun test is a warning to reconcile in Markdown, not a reason to refuse productive work. Define a concise `## Done when` delivery contract. `## Follow-ups` is only for work outside that contract: never relabel an unfinished requirement as a follow-up without explicit owner approval. Optional E2E/integration/review coverage belongs there or in a risk note, never in a fabricated failed Run.
40
44
 
41
- **Do not block on administrative state.** A missing/invalid Run, legacy status vocabulary, stale generated view, or optional unrun test is a warning to reconcile in Markdown, not a reason to refuse productive work. Block only for an explicit Guardrail, security/secret risk, destructive action without approval, or an unmet required Acceptance Criterion. Optional E2E/integration/review coverage belongs in `## Follow-ups` or a risk note, never in a fabricated failed Run.
45
+ **A check must prove its claim.** A passing narrow checker does not prove a broader Task condition. If the task says no visible hardcoded literals, scan the relevant source, replace every in-scope match, and re-run that scan plus the build. Apply the same coverage rule to all completion claims.
42
46
 
43
47
  **Normal-operation command ban.** Do not invoke `scrumrun plan run --fail`, `--block`, `--retry`, `--finalize`, `--complete`, `--validate`, or `scrumrun plan task --start` during ordinary work. Those are optional strict-audit tools and cannot be used to decide whether a Task is delivered. If an old Run is already failed for an administrative reason, leave it as historical evidence, continue the Task directly, and write the corrected outcome in the Task's Technical Summary and Follow-ups.
44
48
 
@@ -104,11 +108,11 @@ During execution:
104
108
 
105
109
  1. keep the change inside the approved Task scope;
106
110
  2. preserve existing owner work and unrelated dirty files;
107
- 3. define or confirm the Task's `## Acceptance Criteria` before execution and check them off as evidence;
108
- 4. work normally: edit code and update the Task's `## Technical Summary`, `## Follow-ups`, and any Guardrail evidence required by an active rule;
111
+ 3. define or confirm the Task's short `## Done when` contract before execution and use it as the only delivery boundary;
112
+ 4. work normally and continuously. Privately decompose work or create linked child Tasks when useful, but do not return after planning; edit code until the contract is satisfied;
109
113
  5. validate in proportion to risk and against the acceptance criteria. Tests, reviews, and environments are required only when the owner, Acceptance Criteria, or an active Guardrail says so. Do not fail or block an otherwise accepted Task merely because an optional E2E/integration suite does not exist or was not run;
110
114
  6. run a configured reviewer only when a Guardrail requires it;
111
- 7. complete the Task directly in Markdown after the required work is satisfied; use CLI release/doctor/repair commands only when their audit or recovery value is wanted;
115
+ 7. complete the Task directly in Markdown after the required work is satisfied, adding a concise `## Completion`; use CLI release/doctor/repair commands only when their audit or recovery value is wanted;
112
116
  8. use path-scoped Mutation Gateway commands only when the owner explicitly requests strict execution.
113
117
 
114
118
  Never overwrite a prior attempt. Never mark work complete because time/token budget ended.
@@ -117,7 +121,7 @@ When work remains queued, the briefing may name the next backlog Task. The owner
117
121
 
118
122
  Every explicit Guardrail remains mandatory. In strict mode, the CLI final checkpoint fails closed on policy drift, protected-path changes, unsafe symlinks, unscannable content, newly introduced secret-like content, or missing Guardrail Evidence. The ignored permit cache is disposable; deleting it invalidates outstanding strict-mode permits and never creates authority.
119
123
 
120
- Task Markdown is the daily operational handoff authority. A structured Run is optional strict audit history only. Early v2 prose Runs may be repaired/migrated explicitly, but their state never overrides the Task's direct handoff or blocks approved work.
124
+ Task Markdown is the daily operational handoff authority. A structured Run is optional strict audit history only. `core.md` and `guardrails.md` are sealed policy: never edit either during product work. A policy change requires an explicit owner request and the owner seals reviewed changes with `scrumrun update --project --seal-policy`. Early v2 prose Runs may be repaired/migrated explicitly, but their state never overrides the Task's direct handoff or blocks approved work.
121
125
 
122
126
  Linked canonical writes use the ignored durable transaction journal. An interrupted prepared mutation rolls back before the next approved mutation; a committed journal is verified and finalized. Audit remains read-only and reports pending recovery. Use `doctor --recover` only when explicitly requested, and never overwrite bytes changed after interruption.
123
127
 
@@ -0,0 +1,281 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>ScrumRun View</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: light dark;
10
+ --bg: #0f1115;
11
+ --panel: #171a21;
12
+ --border: #262a34;
13
+ --muted: #8b93a1;
14
+ --fg: #e6e9ef;
15
+ --accent: #5aa1ff;
16
+ --ok: #4ac68a;
17
+ --warn: #e9b949;
18
+ --err: #ef6f6f;
19
+ --mono: ui-monospace, "SF Mono", "Menlo", "Consolas", monospace;
20
+ }
21
+ @media (prefers-color-scheme: light) {
22
+ :root { --bg:#f7f8fa; --panel:#fff; --border:#e2e5ec; --muted:#5f6773; --fg:#0f1115; --accent:#1e6feb; }
23
+ }
24
+ * { box-sizing: border-box; }
25
+ html, body { margin: 0; padding: 0; background: var(--bg); color: var(--fg); font: 14px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
26
+ header { padding: 16px 24px; border-bottom: 1px solid var(--border); display: flex; align-items: baseline; gap: 16px; }
27
+ h1 { font-size: 18px; margin: 0; font-weight: 600; }
28
+ .sub { color: var(--muted); font-size: 12px; }
29
+ main { padding: 16px 24px; display: grid; gap: 16px; }
30
+ section { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 12px 16px; }
31
+ section h2 { margin: 0 0 8px; font-size: 13px; text-transform: uppercase; color: var(--muted); letter-spacing: .04em; font-weight: 600; }
32
+ .kanban { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }
33
+ .col h3 { margin: 0 0 8px; font-size: 12px; text-transform: uppercase; color: var(--muted); letter-spacing: .04em; }
34
+ .card { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 8px 10px; margin-bottom: 8px; cursor: pointer; transition: border-color .15s; }
35
+ .card:hover { border-color: var(--accent); }
36
+ .card .id { font-family: var(--mono); font-size: 11px; color: var(--muted); }
37
+ .card .title { font-size: 13px; margin-top: 2px; }
38
+ .card .meta { font-size: 11px; color: var(--muted); margin-top: 4px; display: flex; gap: 8px; flex-wrap: wrap; }
39
+ .badge { display: inline-block; padding: 1px 6px; border-radius: 3px; background: var(--border); font-size: 10px; font-family: var(--mono); }
40
+ .badge.type-fix { color: var(--err); }
41
+ .badge.type-feature { color: var(--accent); }
42
+ .badge.type-docs { color: var(--muted); }
43
+ .search { padding: 6px 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--fg); font: inherit; width: 240px; }
44
+ .empty { color: var(--muted); font-style: italic; padding: 8px 0; }
45
+ .err { color: var(--err); font-family: var(--mono); font-size: 12px; }
46
+ #detail { position: fixed; top: 0; right: 0; height: 100vh; width: min(560px, 60vw); background: var(--panel); border-left: 1px solid var(--border); padding: 16px 20px; overflow: auto; box-shadow: -6px 0 24px rgba(0,0,0,.15); transform: translateX(100%); transition: transform .18s; z-index: 10; }
47
+ #detail.open { transform: translateX(0); }
48
+ #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); }
49
+ #detail .close { float: right; background: none; border: 0; color: var(--muted); font-size: 20px; cursor: pointer; }
50
+ .list-item { padding: 6px 0; border-bottom: 1px dotted var(--border); cursor: pointer; }
51
+ .list-item:last-child { border-bottom: 0; }
52
+ .list-item:hover { color: var(--accent); }
53
+ .row { display: flex; justify-content: space-between; gap: 12px; font-size: 12px; }
54
+ .row .muted { color: var(--muted); }
55
+ .header-right { margin-left: auto; display: flex; gap: 12px; align-items: center; }
56
+ </style>
57
+ </head>
58
+ <body>
59
+ <header>
60
+ <h1>ScrumRun</h1>
61
+ <span class="sub" id="project-label">loading…</span>
62
+ <div class="header-right">
63
+ <input type="search" class="search" id="search" placeholder="filter by id or title" />
64
+ </div>
65
+ </header>
66
+ <main>
67
+ <section id="err-panel" style="display:none"><h2>Load error</h2><div class="err" id="err-msg"></div></section>
68
+ <section>
69
+ <h2>Tasks</h2>
70
+ <div class="kanban" id="kanban"></div>
71
+ </section>
72
+ <section>
73
+ <h2>Guardrails</h2>
74
+ <div id="guardrails"><div class="empty">loading…</div></div>
75
+ </section>
76
+ <section>
77
+ <h2>Recent Runs</h2>
78
+ <div id="runs"><div class="empty">loading…</div></div>
79
+ </section>
80
+ <section>
81
+ <h2>Decisions</h2>
82
+ <div id="decisions"><div class="empty">loading…</div></div>
83
+ </section>
84
+ </main>
85
+ <aside id="detail" aria-hidden="true">
86
+ <button class="close" id="detail-close" aria-label="close">×</button>
87
+ <div id="detail-body"></div>
88
+ </aside>
89
+ <script>
90
+ (() => {
91
+ "use strict";
92
+ const STATUS_ORDER = ["backlog", "running", "in_progress", "validating", "learning", "blocked", "failed", "completed"];
93
+ const STATUS_LABELS = {
94
+ backlog: "Backlog",
95
+ running: "Running",
96
+ in_progress: "In progress",
97
+ validating: "Validating",
98
+ learning: "Learning",
99
+ blocked: "Blocked",
100
+ failed: "Failed",
101
+ completed: "Completed"
102
+ };
103
+
104
+ function parseFrontmatter(text) {
105
+ const match = /^---\r?\n([\s\S]*?)\r?\n---([\s\S]*)$/.exec(text);
106
+ if (!match) return { record: {}, body: text };
107
+ const record = {};
108
+ for (const raw of match[1].split(/\r?\n/)) {
109
+ const line = raw.trim();
110
+ if (!line || line.startsWith("#")) continue;
111
+ const colon = line.indexOf(":");
112
+ if (colon === -1) continue;
113
+ const key = line.slice(0, colon).trim();
114
+ const value = line.slice(colon + 1).trim();
115
+ record[key] = value.replace(/^"(.*)"$/, "$1");
116
+ }
117
+ return { record, body: match[2].trimStart() };
118
+ }
119
+
120
+ async function tryFetch(url) {
121
+ try {
122
+ const res = await fetch(url);
123
+ if (!res.ok) return null;
124
+ return await res.text();
125
+ } catch {
126
+ return null;
127
+ }
128
+ }
129
+
130
+ async function fetchArtifactsByPrefix(dir, prefix, cap = 999) {
131
+ const found = [];
132
+ // Try increasing IDs; stop after 3 consecutive misses beyond the last hit.
133
+ let consecutiveMisses = 0;
134
+ for (let i = 1; i <= cap && consecutiveMisses < 5; i += 1) {
135
+ const id = `${prefix}-${String(i).padStart(3, "0")}`;
136
+ const text = await tryFetch(`${dir}/${id}.md`);
137
+ if (text === null) {
138
+ consecutiveMisses += 1;
139
+ continue;
140
+ }
141
+ consecutiveMisses = 0;
142
+ const parsed = parseFrontmatter(text);
143
+ parsed.record.id = parsed.record.id || id;
144
+ parsed.file = `${dir}/${id}.md`;
145
+ found.push(parsed);
146
+ }
147
+ return found;
148
+ }
149
+
150
+ function extractHeading(body) {
151
+ const line = body.split(/\r?\n/).find((l) => l.trim().startsWith("# "));
152
+ return line ? line.replace(/^# /, "").trim() : "(untitled)";
153
+ }
154
+
155
+ function renderKanban(tasks) {
156
+ const bucket = {};
157
+ for (const task of tasks) {
158
+ const status = task.record.status || "backlog";
159
+ if (!bucket[status]) bucket[status] = [];
160
+ bucket[status].push(task);
161
+ }
162
+ const ordered = STATUS_ORDER.filter((s) => bucket[s] && bucket[s].length);
163
+ const container = document.getElementById("kanban");
164
+ container.innerHTML = "";
165
+ if (!ordered.length) { container.innerHTML = '<div class="empty">no tasks found</div>'; return; }
166
+ for (const status of ordered) {
167
+ const col = document.createElement("div");
168
+ col.className = "col";
169
+ col.innerHTML = `<h3>${STATUS_LABELS[status] || status} (${bucket[status].length})</h3>`;
170
+ for (const task of bucket[status]) {
171
+ const title = extractHeading(task.body);
172
+ const type = task.record.type || "task";
173
+ const branch = task.record.branch;
174
+ const card = document.createElement("div");
175
+ card.className = "card";
176
+ card.dataset.id = task.record.id;
177
+ card.innerHTML = `
178
+ <div class="id">${task.record.id}</div>
179
+ <div class="title"></div>
180
+ <div class="meta">
181
+ <span class="badge type-${type}">${type}</span>
182
+ ${branch ? `<span class="badge">${branch}</span>` : ""}
183
+ ${task.record.sprint ? `<span class="badge">${task.record.sprint}</span>` : ""}
184
+ </div>`;
185
+ card.querySelector(".title").textContent = title;
186
+ card.addEventListener("click", () => openDetail(task));
187
+ col.appendChild(card);
188
+ }
189
+ container.appendChild(col);
190
+ }
191
+ }
192
+
193
+ function renderList(id, items, format) {
194
+ const el = document.getElementById(id);
195
+ el.innerHTML = "";
196
+ if (!items.length) { el.innerHTML = '<div class="empty">none</div>'; return; }
197
+ for (const item of items) {
198
+ const row = document.createElement("div");
199
+ row.className = "list-item";
200
+ row.innerHTML = format(item);
201
+ row.addEventListener("click", () => openDetail(item));
202
+ el.appendChild(row);
203
+ }
204
+ }
205
+
206
+ function renderGuardrailsFromText(text) {
207
+ const el = document.getElementById("guardrails");
208
+ if (!text) { el.innerHTML = '<div class="empty">guardrails.md not found</div>'; return; }
209
+ const rules = [];
210
+ for (const match of text.matchAll(/^##\s+(GR-\d+)\s+-\s+(.+)$/gm)) {
211
+ rules.push({ id: match[1], title: match[2] });
212
+ }
213
+ el.innerHTML = rules.length
214
+ ? rules.map((r) => `<div class="row"><span>${r.id}</span><span class="muted">${r.title}</span></div>`).join("")
215
+ : '<div class="empty">no guardrails declared</div>';
216
+ }
217
+
218
+ function openDetail(artifact) {
219
+ const body = document.getElementById("detail-body");
220
+ const record = artifact.record || {};
221
+ const meta = Object.entries(record).map(([k, v]) => `${k}: ${v}`).join("\n");
222
+ body.innerHTML = `
223
+ <h2 style="margin-top:0;font-size:14px">${record.id || "(unknown)"}</h2>
224
+ <div class="sub" style="margin-bottom:10px">${extractHeading(artifact.body || "")}</div>
225
+ <pre>${escape(meta)}</pre>
226
+ <pre>${escape(artifact.body || "")}</pre>
227
+ <div class="sub"><a href="${artifact.file || "#"}" target="_blank" rel="noopener">open raw file</a></div>`;
228
+ document.getElementById("detail").classList.add("open");
229
+ document.getElementById("detail").setAttribute("aria-hidden", "false");
230
+ }
231
+
232
+ function escape(s) { return String(s).replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" }[c])); }
233
+
234
+ document.getElementById("detail-close").addEventListener("click", () => {
235
+ document.getElementById("detail").classList.remove("open");
236
+ document.getElementById("detail").setAttribute("aria-hidden", "true");
237
+ });
238
+
239
+ document.getElementById("search").addEventListener("input", (event) => {
240
+ const q = event.target.value.trim().toLowerCase();
241
+ for (const card of document.querySelectorAll(".card")) {
242
+ const text = card.textContent.toLowerCase();
243
+ card.style.display = !q || text.includes(q) ? "" : "none";
244
+ }
245
+ });
246
+
247
+ async function boot() {
248
+ const label = document.getElementById("project-label");
249
+ const [tasks, runs, decisions, guardrailsText, methodText, stateText] = await Promise.all([
250
+ fetchArtifactsByPrefix("tasks", "TASK", 999),
251
+ fetchArtifactsByPrefix("runs", "RUN", 999),
252
+ fetchArtifactsByPrefix("memory/decisions", "DEC", 999),
253
+ tryFetch("guardrails.md"),
254
+ tryFetch("method.json"),
255
+ tryFetch("state.md")
256
+ ]);
257
+ if (methodText) {
258
+ try { const m = JSON.parse(methodText); label.textContent = `method ${m.method || "?"} · ${tasks.length} tasks · ${runs.length} runs`; }
259
+ catch { label.textContent = `${tasks.length} tasks · ${runs.length} runs`; }
260
+ } else {
261
+ label.textContent = `${tasks.length} tasks · ${runs.length} runs`;
262
+ }
263
+ if (!tasks.length && !runs.length && !guardrailsText && !stateText) {
264
+ document.getElementById("err-panel").style.display = "";
265
+ document.getElementById("err-msg").textContent = "No .scrumrun/ artifacts loaded. If you are viewing via file:// and your browser blocks local fetch, serve the folder: cd .scrumrun && python3 -m http.server 8080, then open http://localhost:8080/view.html";
266
+ return;
267
+ }
268
+ renderKanban(tasks);
269
+ renderList("runs", runs.slice().reverse().slice(0, 20), (r) => `<div class="row"><span>${r.record.id}</span><span class="muted">${extractHeading(r.body)}</span></div>`);
270
+ renderList("decisions", decisions, (d) => `<div class="row"><span>${d.record.id}</span><span class="muted">${extractHeading(d.body)}</span></div>`);
271
+ renderGuardrailsFromText(guardrailsText);
272
+ }
273
+
274
+ boot().catch((error) => {
275
+ document.getElementById("err-panel").style.display = "";
276
+ document.getElementById("err-msg").textContent = String(error && error.message || error);
277
+ });
278
+ })();
279
+ </script>
280
+ </body>
281
+ </html>
package/types/index.d.ts CHANGED
@@ -21,7 +21,7 @@ export type ArtifactKind =
21
21
 
22
22
  export type ArtifactStatus =
23
23
  | "backlog" | "proposed" | "active" | "completed" | "paused" | "cancelled"
24
- | "running" | "validating" | "learning" | "partial" | "failed" | "blocked"
24
+ | "running" | "in_progress" | "validating" | "learning" | "partial" | "failed" | "blocked"
25
25
  | "candidate" | "approved" | "rejected" | "deprecated" | "invalidated"
26
26
  | "open" | "resolved" | "confirmed" | "stale" | "archived" | "executing" | "passed";
27
27