scrumrun 4.0.0 → 4.1.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 CHANGED
@@ -4,6 +4,32 @@ All notable changes follow Semantic Versioning.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 4.1.1 - 2026-09-22
8
+
9
+ ### Added
10
+
11
+ - **`scrumrun update --repair-legacy`.** New flag runs `repair --apply` after refreshing project guidance, so a single `scrumrun update --project --repair-legacy` upgrades the CLI, refreshes packaged Markdown, and normalizes the local `.scrumrun/` tree in one shot. The repair pass covers status aliases, `feature:` free-text kebab labels, guardrail scopes, missing frontmatter, orphaned Runs/Tasks, and legacy Sprint projections — never touches secrets (they always require human review).
12
+
13
+ ### Changed
14
+
15
+ - **Broader status aliases in `scrumrun repair`.** `TASK_STATUS_ALIAS` now maps `pending`, `planned`, `skipped`, `canceled`, `wontdo`/`won't do`, `wip`, `doing`, `in-progress`, `review`, `reviewing`, `finished`, `closed` alongside the existing `done`/`todo`/`complete`/`in_progress`/`executing`/`active`. `RUN_STATUS_ALIAS` gains `in-progress`, `wip`, `finished`, `closed`, `canceled`/`cancelled`, `skipped`, `running`. `doctor --strict` remains strict for fresh installs; the aliases only take effect when an owner explicitly runs `repair --apply` (or `update --repair-legacy`).
16
+ - **Guardrail scope parser tolerates parentheses and `+` separators.** `Scope: frontend + backend` and `Scope: frontend (vue templates, docs)` now parse to the intended token(s) instead of splitting on commas inside parens and producing bogus scope names. Unknown scopes still fail conformance in fresh installs — `repair` rewrites them to `all` when normalizing legacy projects.
17
+
18
+ ## 4.1.0 - 2026-09-22
19
+
20
+ ### Added
21
+
22
+ - **Declarative migration runner (`lib/migrate/run.js`).** Migrations become plain Markdown files under `migrations/*.md` with fenced `​```yaml steps`, `​```yaml verify`, and optional `​```yaml rollback` blocks. Whitelisted ops: `create_backup`, `rename_path`, `bump_schema`, `move_frontmatter_field`, `assert_hash`. Runner supports `{ dryRun: true }` (no writes) and returns `{ status, journal }` so rollback can operate on exactly what was applied. Path safety enforced: absolute paths and paths escaping the project root are rejected. See `migrations/README.md` for the format contract. Rewrite of the existing v1→v2 imperative migration into this format is delegated to the release cycle (large diff, coordinated release).
23
+ - **Declarative actions API (`lib/actions/`).** New `executeAction(name, projectRoot, payload)` entry point exposes 7 pure functions (`plan.task.add`, `plan.feature.add`, `plan.sprint.add`, `plan.task.amend`, `knowledge.fact.add`, `knowledge.decision.add`, `knowledge.insight.propose`) that delegate to the existing orchestrator/memory services, sharing validation and atomicity. New CLI command `scrumrun action <name> [--payload <json>|--file <path>|--stdin]` and `scrumrun action --list`. Enables hooks, the static view (TASK-026), and future clients to share one write layer without shelling into subcommands. Existing `plan/knowledge/rules/review/config` subcommands are unchanged; rip-out is a release-cycle task.
24
+ - **Append-only per-Task Run ledger (`runs/TASK-NNN.jsonl`).** New `lib/v2/runs-jsonl.js` records each execution event on one line with SHA-256 chain (`prev`/`hash`) for tamper detection. Supersedes the one-file-per-attempt Markdown layout for new work; historic `RUN-NNN.md` files remain readable. Cross-process append safety via `TASK-NNN.jsonl.lock` (exclusive `open wx` + jittered backoff). Design captured in `DEC-010`. Reader/writer wiring for `scrumrun plan run *` and migration of legacy Markdown Runs will land alongside the declarative migration runner (see task TASK-030).
25
+ - **Static HTML view at `.scrumrun/view.html`.** Zero-dependency single-file dashboard (vanilla JS, inline CSS, self-contained) that reads canonical Markdown via relative `fetch()` and renders a status kanban (Tasks), a recent-Runs list, active Guardrails, and Decisions. `scrumrun init` copies it into every project; `scrumrun update --project` refreshes it. Open with `open .scrumrun/view.html` on macOS, `xdg-open .scrumrun/view.html` on Linux, or `start .scrumrun\view.html` on Windows. If your browser blocks `file://` fetches, serve locally: `cd .scrumrun && python3 -m http.server 8080`.
26
+ - **Task Markdown schema validator (opt-in).** New pure library `lib/v2/task-schema.js` validates that a Task carries the sections and evidence its lifecycle requires: `## Request` + `## Done when` for opt-in Tasks, `## Completion` on `status: completed` (or a linked Run whose body contains `## Technical Summary`), and — inside a git repo — a `branch` field in the frontmatter when the Task is `running`/`in_progress`/`validating`/`learning`. Errors: `SR-E-452` (missing section), `SR-E-453` (completed without summary), `SR-E-454` (running without branch, warning-only). Legacy Tasks without `task_schema: 1` in frontmatter are silently skipped for full backward compatibility. Wired into `scrumrun review artifact --run` and `scrumrun doctor --strict`; both accept `--strict` to promote schema warnings to blocking errors.
27
+ - **`lib/git/context.js`.** Pure shell-out helper that returns `{ isRepo, branch, headSha }` for any project directory. Never throws — returns a non-repo shape when git is missing or the cwd is outside a working tree. Cross-platform: resolves `git` from `PATH` (Git for Windows, macOS, Linux). Consumed by the task-schema validator and available to any future feature (e.g. review/verify diff scoping).
28
+
29
+ ### Changed
30
+
31
+ - **Legacy `sc-*` skill directories are removed on install/update.** Older versions of ScrumRun published one skill per compatibility alias (`sc-sprint`, `sc-decisions`, `sc-vault`, …). Every listed skill costs context in every client conversation, so install/update now sweeps those directories from the target skills folder alongside the pre-existing `ai-scrum` cleanup, while leaving unrelated user skills untouched. The canonical skill remains `scrumrun`; the `scrumrun sc-*` command aliases are unchanged.
32
+
7
33
  ## 4.0.0 - 2026-08-31
8
34
 
9
35
  ### 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.0.0` · **Method target:** `2.0.0` · **Runtime:** Node.js `>=22.13.0` · **License:** MIT
7
+ **Package:** `4.1.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
 
@@ -63,6 +63,32 @@ scrumrun <noun> <subject> <action> [args]
63
63
 
64
64
  The five nouns are `plan`, `knowledge`, `rules`, `review`, and `config`. `/sc` is an optional AI-client shortcut; `scrumrun sc ...` remains a compatibility alias for existing integrations.
65
65
 
66
+ ## Compatibility
67
+
68
+ ScrumRun is designed to be **portable across AI clients and operating systems**. The runtime is plain Markdown + a small Node.js CLI; nothing is tied to a specific vendor.
69
+
70
+ ### Operating systems
71
+
72
+ | OS | Status | Notes |
73
+ |---|---|---|
74
+ | macOS 12+ | Supported | Primary development platform. |
75
+ | Linux (glibc-based distros) | Supported | Tested on Ubuntu/Debian/Fedora; `fs.watch` uses inotify. |
76
+ | Windows 10/11 | Supported | Requires Git for Windows for the optional pre-commit hook; `.scrumrun/` paths are POSIX-normalized internally. |
77
+ | WSL2 | Supported | Behaves as Linux. |
78
+
79
+ Requirement everywhere: **Node.js ≥ 22.13.0** and (optionally) `git` on `PATH`.
80
+
81
+ ### AI clients
82
+
83
+ | Client | Integration | Notes |
84
+ |---|---|---|
85
+ | Claude Code | `scrumrun install claude` | Installs the `scrumrun` skill under `~/.claude/skills/`. |
86
+ | OpenCode | `scrumrun install opencode` | Installs skill under `~/.config/opencode/skills/`. |
87
+ | Codex | `scrumrun install codex` | Installs skill under `~/.codex/skills/`. |
88
+ | Cursor / Windsurf / Zed / any Markdown-capable agent | Manual | Point the agent at `.scrumrun/` and `CORE.md`; no CLI adapter required. |
89
+
90
+ The method itself (Markdown tree + guardrails + task/run model) is client-agnostic: any agent that can read files can operate a ScrumRun project.
91
+
66
92
  ## Daily flow
67
93
 
68
94
  Natural language is the normal entry point:
@@ -90,6 +116,8 @@ EXECUTING → VALIDATING → LEARNING → COMPLETED | FAILED | BLOCKED
90
116
 
91
117
  Every Task carries a short `## Done when` delivery contract. After approval, the agent works directly in code and Task Markdown until that contract is delivered: it keeps the full discover → implement → verify → fix → verify loop running. A report is allowed only when you ask for it and never ends execution. The normal close is a concise `## Completion`; `## Follow-ups` may only contain work outside the agreed contract. No CLI transition is required.
92
118
 
119
+ **Opt-in Task-schema validator.** Adding `task_schema: 1` to a Task's frontmatter turns on the structural checks in `lib/v2/task-schema.js`: `## Request` + `## Done when` are required, `## Completion` (or an associated Run's `## Technical Summary`) is required once the Task is `completed`, and inside a git repo the Task must record its `branch` while executing. `scrumrun doctor --strict` and `scrumrun review artifact --run --strict` promote these to blocking errors; without `--strict` they are warnings. Legacy Tasks without `task_schema` are unaffected.
120
+
93
121
  Guardrails still apply. An agent stops only for an explicit active Guardrail, a secret/security risk, destructive work without approval, or an unmet required delivery criterion. Tests, reviews, and environments are gates only when the owner, `Done when`, or a Guardrail explicitly requires them. Optional missing E2E coverage is a follow-up/risk, not a failed Task.
94
122
 
95
123
  Use the CLI at the edges, where its safety is valuable:
@@ -151,6 +179,22 @@ The fast graph/search layer is `.scrumrun/.cache/semantic-index.sqlite`. It is i
151
179
 
152
180
  `map.md` is shown only when its source fingerprint matches the current semantic index. A fresh placeholder or stale map is rejected with an explicit rebuild instruction instead of being presented as project truth.
153
181
 
182
+ For local projects that benefit from always-fresh projections, opt into the lightweight watcher in `.scrumrun/config.md` and start it once:
183
+
184
+ ```yaml
185
+ watcher.enabled: true
186
+ watcher.debounce_ms: 250
187
+ watcher.poll_interval_ms: 1500
188
+ ```
189
+
190
+ ```bash
191
+ scrumrun config watch --start
192
+ scrumrun config watch --status
193
+ scrumrun config watch --stop
194
+ ```
195
+
196
+ The watcher uses native `fs.watch` where recursive watching is available and polling otherwise. It is never authority or a work gate: a later on-demand rebuild always remains correct if it stops. The daemon records its PID in `.scrumrun/.cache/watcher.pid`, ignores the vault, `.cache/contexts/`, and its own generated outputs, coalesces rapid writes, and writes only `state.md`, `map.md`, and `.cache/`.
197
+
154
198
  ## Project briefing
155
199
 
156
200
  `state.md` is the **briefing**: a bounded summary an agent reads first, before touching anything else.
package/SPEC.md CHANGED
@@ -234,6 +234,37 @@ Secret-like content detection is canonical-policy-level and applies to every art
234
234
 
235
235
  The executable Policy Engine may infer enforcement for migrated prose, but fresh v2 policy declares it explicitly. Unknown enforcement, duplicate ids, inactive-only policy, configuration that disables approval, and unsafe read-only paths fail conformance. Configuration can tune presentation and workflow preferences; it cannot retire, bypass, or weaken active Guardrails.
236
236
 
237
+ ### 6.1 Declarative Guardrail enforcement
238
+
239
+ A Guardrail may add one optional fenced `yaml enforcement` block. It is parsed without an LLM and evaluated as a pure function over a ChangeSet (`paths`, `diff`, and optional `symbols`); ordinary prose-only Guardrails remain valid. The supported YAML subset is intentionally limited to mappings and string lists so every installed client evaluates the same rule without a network dependency.
240
+
241
+ ````markdown
242
+ ## GR-010 - No debug logs in production code
243
+
244
+ Status: active
245
+ Enforcement: manual
246
+ Scope: validation, commit
247
+ Rule: Production changes must not add debug logging.
248
+
249
+ ```yaml enforcement
250
+ match:
251
+ paths:
252
+ - "src/**/*.js"
253
+ diff:
254
+ - "/\\bconsole\\.log\\s*\\(/"
255
+ symbols:
256
+ - "debug*"
257
+ on_violation: block
258
+ severity: high
259
+ evidence:
260
+ - "Remove debug output or justify it in the reviewed policy."
261
+ ```
262
+ ````
263
+
264
+ `match.paths` accepts globs (`src/**`), extensions (`.ts` or `ext:.ts`), and regexes (`regex:^src/` or `/^src\\//`). `match.diff` accepts regexes (recommended) or literal fragments; `match.symbols` uses the same glob/regex form. At least one matcher is required. `on_violation` is `block` or `warn`; `severity` is `low`, `medium`, `high`, or `critical`; `evidence` is an optional scalar or string list surfaced with a match. Invalid blocks are a conformance error with `SR-E-153`.
265
+
266
+ The declared action is authoritative. `config.md` may promote a warning to a block through `Guardrail On Violation: block`; it may never downgrade an explicit block. `scrumrun review artifact --run` evaluates the worktree, `--staged` evaluates the index, and `doctor --strict` consumes the same evaluator. The optional offline pre-commit template is `templates/shared/hooks/pre-commit`.
267
+
237
268
  ## 7. Semantic memory and code intelligence
238
269
 
239
270
  Memory records include subject, source, evidence, validity window, confidence where useful, review trigger, and last-verified commit. Insight types may include placement rationale, design constraint, known trade-off, failure history, usage warning, compatibility reason, business rule, performance reason, security reason, and testing note.
package/bin/scrumrun.js CHANGED
@@ -35,11 +35,13 @@ const { addPlanArtifact, amendPlanArtifact, approveRequest, finalizeRun, nextBac
35
35
  const { authorizeMutation, recordMutation, satisfyGuardrail } = require(path.join(root, "lib", "runtime", "mutation-gateway"));
36
36
  const { recordArtifactReview } = require(path.join(root, "lib", "runtime", "review-service"));
37
37
  const { createMemory, listMemory, showMemory, transitionMemory } = require(path.join(root, "lib", "memory", "service"));
38
+ const { applyCompaction, proposedClusters, rollbackCompaction } = require(path.join(root, "lib", "memory", "compaction"));
38
39
  const { indexPath, indexStatus, mapStatus, queryIndex, rebuildIndex, writeMap } = require(path.join(root, "lib", "memory", "index"));
39
40
  const { auditProject } = require(path.join(root, "lib", "v2", "conformance"));
40
41
  const { recoverPendingTransactions, previewPendingRecovery } = require(path.join(root, "lib", "v2", "transaction"));
41
42
  const { containsSecret } = require(path.join(root, "lib", "security", "secrets"));
42
43
  const { sealPolicyIntegrity } = require(path.join(root, "lib", "runtime", "policy-integrity"));
44
+ const { readStatus: watcherStatus, startWatcher, stopWatcher } = require(path.join(root, "lib", "runtime", "watcher"));
43
45
 
44
46
  const COMMANDS = ["sc"];
45
47
  const COMPATIBILITY_COMMANDS = Object.keys(COMMAND_ALIASES);
@@ -59,7 +61,7 @@ Usage:
59
61
  scrumrun <noun> <subject> <action> [args]
60
62
  scrumrun sc <noun> <subject> <action> [args] # compatibility alias
61
63
  scrumrun install [all|codex|opencode|claude] [--force]
62
- scrumrun update [all|codex|opencode|claude] [--project] [--seal-policy] [--migrate] [--verbose]
64
+ scrumrun update [all|codex|opencode|claude] [--project] [--seal-policy] [--migrate] [--repair-legacy] [--verbose]
63
65
  scrumrun init [--local|--shared] [--lean] [--no-agent-hint] [--force]
64
66
  scrumrun status
65
67
  scrumrun core [--path|--prompt]
@@ -68,6 +70,7 @@ Usage:
68
70
  scrumrun migrate --to 2 --apply
69
71
  scrumrun migrate --to 2 --rollback
70
72
  scrumrun doctor [all|codex|opencode|claude] [--strict] [--recover]
73
+ scrumrun config watch --start|--stop|--status # optional generated-projection daemon; never a gate
71
74
  scrumrun repair [--recover-orphan-tasks] [--apply]
72
75
  scrumrun uninstall [--force]
73
76
 
@@ -219,11 +222,16 @@ function cleanupLegacy(commandsDir, skillsDir) {
219
222
  }
220
223
  }
221
224
  }
222
- const oldSkill = path.join(skillsDir, "ai-scrum");
223
- if (fs.existsSync(oldSkill)) {
224
- fs.rmSync(oldSkill, { recursive: true, force: true });
225
- installSummary.cleaned += 1;
226
- installLog(` rm legacy ${oldSkill}`);
225
+ if (fs.existsSync(skillsDir)) {
226
+ const legacySkillNames = new Set(["ai-scrum", ...COMPATIBILITY_COMMANDS]);
227
+ for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
228
+ if (!entry.isDirectory()) continue;
229
+ if (!legacySkillNames.has(entry.name)) continue;
230
+ const target = path.join(skillsDir, entry.name);
231
+ fs.rmSync(target, { recursive: true, force: true });
232
+ installSummary.cleaned += 1;
233
+ installLog(` rm legacy ${target}`);
234
+ }
227
235
  }
228
236
  }
229
237
 
@@ -384,10 +392,13 @@ function refreshProjectGuidance(cwd = process.cwd()) {
384
392
  const marker = path.join(cwd, ".scrumrun", "method.json");
385
393
  const sealed = writeFile(marker, sealPolicyIntegrity(path.join(cwd, ".scrumrun")), { backup: true });
386
394
  results.push({ status: sealed.changed ? "updated" : "skipped", dest: marker, backup: sealed.backup });
395
+ const viewFile = path.join(cwd, ".scrumrun", "view.html");
396
+ const viewResult = writeFile(viewFile, fs.readFileSync(path.join(templates, "shared", "view.html"), "utf8"), { backup: true });
397
+ results.push({ status: viewResult.changed ? "updated" : "skipped", dest: viewFile, backup: viewResult.backup });
387
398
  return results;
388
399
  }
389
400
 
390
- function updateInstallation(target, { migrate = false, project = false, sealPolicy = false, verbose = false } = {}) {
401
+ function updateInstallation(target, { migrate = false, project = false, sealPolicy = false, verbose = false, repairLegacy = false } = {}) {
391
402
  installVerbose = verbose;
392
403
  installSummary.cleaned = 0;
393
404
  installSummary.written = 0;
@@ -403,6 +414,18 @@ function updateInstallation(target, { migrate = false, project = false, sealPoli
403
414
  const sealed = writeFile(marker, sealPolicyIntegrity(scrumDir, { includeGuardrails: true }), { backup: true });
404
415
  projectResults.push({ status: sealed.changed ? "updated" : "skipped", dest: marker, backup: sealed.backup });
405
416
  }
417
+ let repairSummary = "";
418
+ if (repairLegacy && v2Project()) {
419
+ try {
420
+ const { repair } = require(path.join(root, "lib", "commands", "repair"));
421
+ const scrumDir = path.join(process.cwd(), ".scrumrun");
422
+ const result = repair(scrumDir, { apply: true, recoverOrphanTasks: true });
423
+ const applied = (result.plan && result.plan.entries && result.plan.entries.length) || 0;
424
+ repairSummary = ` Legacy repair: ${applied} entry(ies) normalized.`;
425
+ } catch (error) {
426
+ repairSummary = ` Legacy repair skipped: ${error.message}.`;
427
+ }
428
+ }
406
429
  if (migrate && v2Project()) {
407
430
  try {
408
431
  refreshState(path.join(process.cwd(), ".scrumrun"));
@@ -414,7 +437,7 @@ function updateInstallation(target, { migrate = false, project = false, sealPoli
414
437
  if (!verbose) {
415
438
  const targetSummary = installSummary.targets.join(", ") || "no clients";
416
439
  const projectSummary = projectResults.length ? ` Project guidance: ${projectResults.filter((item) => item.status === "updated").length} file(s) refreshed.` : "";
417
- console.log(`Updated ${targetSummary} — ${installSummary.written} files written, ${installSummary.cleaned} legacy removed.${projectSummary} Run with --verbose to see file paths.`);
440
+ console.log(`Updated ${targetSummary} — ${installSummary.written} files written, ${installSummary.cleaned} legacy removed.${projectSummary}${repairSummary} Run with --verbose to see file paths.`);
418
441
  }
419
442
  return migration;
420
443
  }
@@ -1372,6 +1395,25 @@ function printMemoryArtifact(artifact) {
1372
1395
  function runV2Memory(subject, args) {
1373
1396
  const kind = subject === "fact" ? "knowledge" : subject;
1374
1397
  const action = args[0];
1398
+ if (kind === "dossier" && action === "--compact") {
1399
+ if (args.includes("--dry-run")) {
1400
+ const preview = proposedClusters(process.cwd());
1401
+ console.log(JSON.stringify({ mode: "dry-run", ...preview }, null, 2));
1402
+ return;
1403
+ }
1404
+ if (args.includes("--apply")) {
1405
+ const result = applyCompaction(process.cwd(), { approved: args.includes("--approve") });
1406
+ console.log(JSON.stringify({ mode: "apply", ...result }, null, 2));
1407
+ return;
1408
+ }
1409
+ if (args.includes("--rollback")) {
1410
+ const dossier = args[args.indexOf("--rollback") + 1];
1411
+ if (!dossier || dossier.startsWith("--")) throw new Error("--rollback requires a DOS-NNN id.");
1412
+ console.log(JSON.stringify(rollbackCompaction(process.cwd(), dossier), null, 2));
1413
+ return;
1414
+ }
1415
+ throw new Error("Usage: scrumrun knowledge dossier --compact --dry-run|--apply --approve|--rollback DOS-NNN");
1416
+ }
1375
1417
  const createAction = kind === "insight" ? "--propose" : "--add";
1376
1418
  if (action === createAction) {
1377
1419
  const artifact = createMemory(process.cwd(), kind, memoryOptions(args));
@@ -1691,7 +1733,7 @@ function executeRootRoute(route) {
1691
1733
  if (noun === "knowledge" && subject === "vault") return runVault(routeArgs);
1692
1734
  if (noun === "knowledge" && subject === "context") return runContext(routeArgs);
1693
1735
  if (noun === "review" && subject === "artifact" && routeArgs[0] === "--run") {
1694
- const audit = auditProject(process.cwd());
1736
+ const audit = auditProject(process.cwd(), { staged: routeArgs.includes("--staged"), strict: routeArgs.includes("--strict") });
1695
1737
  console.log(JSON.stringify(audit, null, 2));
1696
1738
  if (!audit.passed) process.exitCode = 1;
1697
1739
  return;
@@ -1715,6 +1757,15 @@ function executeRootRoute(route) {
1715
1757
  const target = ["all", "codex", "opencode", "claude"].includes(routeArgs[0]) ? routeArgs[0] : "all";
1716
1758
  return doctor(target, { strict: routeArgs.includes("--strict"), recover: routeArgs.includes("--recover"), dryRun: routeArgs.includes("--dry-run") });
1717
1759
  }
1760
+ if (noun === "config" && subject === "watch") {
1761
+ let result;
1762
+ if (routeArgs[0] === "--start") result = startWatcher(process.cwd());
1763
+ else if (routeArgs[0] === "--stop") result = stopWatcher(process.cwd());
1764
+ else if (routeArgs[0] === "--status") result = watcherStatus(process.cwd());
1765
+ else throw new Error("Usage: scrumrun config watch --start|--stop|--status");
1766
+ console.log(JSON.stringify(result, null, 2));
1767
+ return;
1768
+ }
1718
1769
  if (noun === "config" && subject === "update") {
1719
1770
  const target = ["all", "codex", "opencode", "claude"].includes(routeArgs[0]) ? routeArgs[0] : "all";
1720
1771
  return updateInstallation(target, {
@@ -2350,6 +2401,7 @@ function initProject({ force, mode, agentHint, lean }) {
2350
2401
 
2351
2402
  results.push(...copyDir(path.join(projectTemplate, ".scrumrun"), path.join(cwd, ".scrumrun"), { force, vars }));
2352
2403
  results.push(copyFile(path.join(root, "CORE.md"), path.join(cwd, ".scrumrun", "core.md"), { force, vars }));
2404
+ results.push(copyFile(path.join(templates, "shared", "view.html"), path.join(cwd, ".scrumrun", "view.html"), { force }));
2353
2405
  if (force || !markerExisted) {
2354
2406
  const sealed = writeFile(marker, sealPolicyIntegrity(path.join(cwd, ".scrumrun"), { includeGuardrails: true }), { backup: false });
2355
2407
  results.push({ status: sealed.changed ? "written" : "skipped", dest: marker, backup: sealed.backup });
@@ -2632,7 +2684,7 @@ function doctor(target = "all", { compatibility = false, strict = false, recover
2632
2684
  ok = false;
2633
2685
  console.log(`miss ScrumRun project audit: ${scrumDir}`);
2634
2686
  } else {
2635
- const audit = auditProject(process.cwd());
2687
+ const audit = auditProject(process.cwd(), { strict: true });
2636
2688
  const blocking = audit.findings.filter((item) => ["critical", "high"].includes(item.severity));
2637
2689
  ok = ok && audit.passed && blocking.length === 0;
2638
2690
  console.log(`${audit.passed && blocking.length === 0 ? "ok " : "fail"} ScrumRun project audit: ${audit.findings.length} finding(s)`);
@@ -2656,12 +2708,38 @@ if (!command || command === "--help" || command === "-h") {
2656
2708
  console.log(`ScrumRun ${version}`);
2657
2709
  } else if (command === "install" || command === "update") {
2658
2710
  const target = ["all", "codex", "opencode", "claude"].includes(args[1]) ? args[1] : "all";
2659
- if (command === "update") updateInstallation(target, { migrate: args.includes("--migrate"), project: args.includes("--project"), sealPolicy: args.includes("--seal-policy"), verbose: args.includes("--verbose") });
2711
+ if (command === "update") updateInstallation(target, { migrate: args.includes("--migrate"), project: args.includes("--project"), sealPolicy: args.includes("--seal-policy"), verbose: args.includes("--verbose"), repairLegacy: args.includes("--repair-legacy") });
2660
2712
  else install(target, true, { compatibility: false });
2661
2713
  } else if (command === "sc") {
2662
2714
  runRoot(args.slice(1));
2663
2715
  } else if (["plan", "knowledge", "rules", "review", "config"].includes(command)) {
2664
2716
  runRoot(args);
2717
+ } else if (command === "action") {
2718
+ const { executeAction, listActions } = require(path.join(root, "lib", "actions"));
2719
+ try {
2720
+ const name = args[1];
2721
+ if (!name || name === "--list") {
2722
+ console.log("Available actions:");
2723
+ for (const item of listActions()) console.log(` ${item.name.padEnd(30)} ${item.describe}`);
2724
+ process.exit(0);
2725
+ }
2726
+ const rest = args.slice(2);
2727
+ let payload = {};
2728
+ const payloadFlag = rest.indexOf("--payload");
2729
+ const fileFlag = rest.indexOf("--file");
2730
+ if (payloadFlag !== -1) {
2731
+ payload = JSON.parse(rest[payloadFlag + 1] || "{}");
2732
+ } else if (fileFlag !== -1) {
2733
+ payload = JSON.parse(fs.readFileSync(rest[fileFlag + 1], "utf8"));
2734
+ } else if (rest.includes("--stdin")) {
2735
+ payload = JSON.parse(fs.readFileSync(0, "utf8"));
2736
+ }
2737
+ const result = executeAction(name, process.cwd(), payload);
2738
+ console.log(JSON.stringify(result, null, 2));
2739
+ } catch (error) {
2740
+ console.error(`action failed: ${error.message}`);
2741
+ process.exitCode = 1;
2742
+ }
2665
2743
  } else if (COMMAND_ALIASES[command]) {
2666
2744
  runCompatibilityAlias(command, args.slice(1));
2667
2745
  } else if (command === "init") {
package/docs/COMMANDS.md CHANGED
@@ -69,13 +69,20 @@ Creation options include `--title`, `--content`, repeated `--evidence`, repeated
69
69
  scrumrun rules guardrail --add|--list|--show|--retire
70
70
  scrumrun rules reviewer --add|--list|--show|--run
71
71
  scrumrun review code --run
72
- scrumrun review artifact --run
72
+ scrumrun review artifact --run [--staged]
73
73
  scrumrun review artifact --record --task TASK-NNN [--run RUN-NNN] [--title "..."] [--evidence "..."]
74
74
  scrumrun review migration --run
75
75
  scrumrun review release --run
76
76
  ```
77
77
 
78
- `review artifact --run` is read-only and returns a machine-readable 21-invariant project audit. `--record` reruns that audit and persists its exact pass/fail result as a canonical `REV-NNN`; supplied evidence is additive and cannot turn a failed audit into a pass. Other review routes require repository reasoning and remain read-only unless fixes receive separate approval.
78
+ `review artifact --run` is read-only and returns a machine-readable project audit, including declarative Guardrail matches against the current Git worktree. `--staged` evaluates the Git index, for the optional pre-commit hook. `--record` reruns that audit and persists its exact pass/fail result as a canonical `REV-NNN`; supplied evidence is additive and cannot turn a failed audit into a pass. Other review routes require repository reasoning and remain read-only unless fixes receive separate approval.
79
+
80
+ To install the optional offline hook locally:
81
+
82
+ ```bash
83
+ cp node_modules/scrumrun/templates/shared/hooks/pre-commit .git/hooks/pre-commit
84
+ chmod +x .git/hooks/pre-commit
85
+ ```
79
86
 
80
87
  ## Config and lifecycle
81
88
 
@@ -85,6 +92,7 @@ scrumrun config init --local|--shared|--lean|--no-agent-hint|--force
85
92
  scrumrun config update [all|codex|opencode|claude] [--project] [--seal-policy] [--migrate]
86
93
  scrumrun config migrate --to 2 --dry-run|--apply|--rollback
87
94
  scrumrun config doctor [all|codex|opencode|claude] [--strict] [--recover]
95
+ scrumrun config watch --start|--stop|--status
88
96
  scrumrun config uninstall --force
89
97
  scrumrun config help <topic>
90
98
  ```
@@ -94,3 +102,5 @@ Top-level CLI aliases (`init`, `update`, `migrate`, `doctor`, `uninstall`, `stat
94
102
  Run transitions accept typed evidence through `--command`, `--test`, `--file`, `--review`, `--decision`, `--insight`, `--risk`, or generic `--evidence kind:value`. `doctor --recover` is an explicit write that resolves only safe pending kernel transactions; doctor without it remains read-only.
95
103
 
96
104
  Run `scrumrun commands` for grammar rendered directly from the current manifest.
105
+
106
+ `watch` is an optional convenience, never a gate or authority. Set `watcher.enabled: true` in `.scrumrun/config.md`, then use `--start`. It records its liveness PID at `.scrumrun/.cache/watcher.pid`; `--stop` shuts that daemon down. Native recursive `fs.watch` is used where supported; otherwise the daemon falls back to portable polling. It ignores `vault.local.md`, `.cache/contexts/`, its own generated outputs, and writes only generated `state.md`, `map.md`, and `.cache/semantic-index.sqlite`.
@@ -77,6 +77,7 @@ SR-E-102 Run transition rejected: missing validation or learning evidence.
77
77
  | `SR-E-150` | Guardrail check blocked the operation. | Read the reported GR-NNN, satisfy or retire it explicitly; guardrails never bypass silently. |
78
78
  | `SR-E-151` | Guardrail obligation is still pending. | Resolve each pending guardrail via `scrumrun plan run --satisfy-guardrail` before completing the Run. |
79
79
  | `SR-E-152` | Guardrail declaration is malformed. | Every active guardrail requires Status, Enforcement, Scope, and Rule fields; check `.scrumrun/guardrails.md`. |
80
+ | `SR-E-153` | Declarative Guardrail YAML is malformed. | Use the documented yaml enforcement block with match, on_violation, severity, and optional evidence. |
80
81
 
81
82
  ### Edit permits (Mutation Gateway)
82
83
 
@@ -121,6 +122,9 @@ SR-E-102 Run transition rejected: missing validation or learning evidence.
121
122
  |---|---|---|
122
123
  | `SR-E-450` | Conformance check failed. | The reported invariant identifies the exact violation; the message includes the file and expected shape. |
123
124
  | `SR-E-451` | Installed client asset is stale. | Re-run `scrumrun update` for the specific client. `doctor --strict` shows which files diverge. |
125
+ | `SR-E-452` | Task Markdown is missing a required section. | Add the reported section (e.g. `## Request`, `## Done when`) to the Task file. The validator lives in `lib/v2/task-schema.js`. |
126
+ | `SR-E-453` | Completed Task has no `## Completion` or associated Run `## Technical Summary`. | Append a short `## Completion` bullet list to the Task, or complete the Run with `plan run --complete --summary "..."`. |
127
+ | `SR-E-454` | Task is running inside a git repository but no `git.branch` was captured. | Warning-only. Reconcile with `scrumrun repair --apply`, or add `git: { branch: <name>, base_sha: <sha> }` to the Task frontmatter. |
124
128
 
125
129
  ### Configuration and installation
126
130
 
package/docs/SCHEMA.md CHANGED
@@ -86,6 +86,10 @@ A native ledger begins with `created → executing`; an evidenced migration `sna
86
86
  | insight | `candidate` → `confirmed`, `invalidated`<br>`confirmed` → `stale`, `deprecated`, `invalidated`<br>`stale` → `confirmed`, `deprecated`, `invalidated`<br>`deprecated` → `confirmed`<br>`invalidated` → terminal |
87
87
  | dossier | `active` → `stale`, `deprecated`, `archived`<br>`stale` → `active`, `deprecated`, `archived`<br>`deprecated` → `archived`<br>`archived` → terminal |
88
88
 
89
+ ## Declarative Guardrail enforcement
90
+
91
+ Project Guardrails may include an optional fenced `yaml enforcement` block. Its restricted, dependency-free YAML schema is defined normatively in `SPEC.md §6.1`: `match.paths[]`, `match.diff[]`, `match.symbols[]`, `on_violation` (`block` or `warn`), `severity`, and optional `evidence`. The pure evaluator consumes only that normalized rule data and a supplied ChangeSet; it has no network or LLM dependency. Prose-only Guardrails remain valid.
92
+
89
93
  ## Projections
90
94
 
91
95
  `state.md`, `map.md`, context packages, and `.cache/` are disposable. They may summarize or index canonical artifacts, but they cannot introduce status, policy, relations, decisions, or knowledge.
@@ -66,3 +66,31 @@ scrumrun knowledge context --clear
66
66
  ```
67
67
 
68
68
  SQLite is ignored and disposable. The derived index records its search backend: FTS5/BM25 is selected when the current Node.js SQLite build supports it; otherwise ScrumRun uses deterministic parameterized token matching over the same artifact, code, and relation tables. Queries default to 10 records/40 relations and hard-cap at 100/100. Match type, truth state, warnings, relation counts, and evidence are returned so recommendations remain explainable.
69
+
70
+ ## Deterministic Dossier compaction
71
+
72
+ Compaction is an opt-in maintenance operation for a project with many related,
73
+ already reviewed memory records. It never calls an LLM and never runs in the
74
+ background:
75
+
76
+ ```bash
77
+ scrumrun knowledge dossier --compact --dry-run
78
+ scrumrun knowledge dossier --compact --apply --approve
79
+ scrumrun knowledge dossier --compact --rollback DOS-001
80
+ ```
81
+
82
+ Only approved Knowledge and confirmed Insights without `superseded_by` are
83
+ eligible. Each record is represented by its subject, declared relations
84
+ (including `used_by`), and explicit `#tags` or `## Tags`/`## Topics` bullets.
85
+ Two records are connected when the Jaccard overlap of those features is at
86
+ least `memory.compaction.threshold` (default `0.6`). Connected components with
87
+ at least `memory.compaction.min_members` records (default `3`) become proposed
88
+ clusters. IDs, feature sorting, links, and labels are all deterministic.
89
+
90
+ Dry-run is read-only and reports member IDs, pair evidence, score and effective
91
+ configuration. Apply additionally requires the explicit `--approve` flag: it
92
+ creates a Dossier with original artifact IDs as evidence and adds only
93
+ `superseded_by: DOS-NNN` to the source records. Sources are never deleted. The
94
+ Dossier holds a reversible snapshot; rollback restores byte-exact sources only
95
+ when they have not changed since compaction, then archives the Dossier. This
96
+ refusal protects later owner edits rather than overwriting them.
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+
3
+ const { addPlanArtifact, amendPlanArtifact } = require("../runtime/orchestrator");
4
+ const { createMemory } = require("../memory/service");
5
+
6
+ const REGISTRY = Object.freeze({
7
+ "plan.task.add": {
8
+ describe: "Create a Task (optionally as backlog or fix).",
9
+ handler: (projectRoot, payload) => {
10
+ requireString(payload, "title");
11
+ return addPlanArtifact(projectRoot, "task", payload.title, {
12
+ type: payload.type || null,
13
+ status: payload.status || null
14
+ });
15
+ }
16
+ },
17
+ "plan.feature.add": {
18
+ describe: "Create a Feature.",
19
+ handler: (projectRoot, payload) => {
20
+ requireString(payload, "title");
21
+ return addPlanArtifact(projectRoot, "feature", payload.title, {});
22
+ }
23
+ },
24
+ "plan.sprint.add": {
25
+ describe: "Create a Sprint.",
26
+ handler: (projectRoot, payload) => {
27
+ requireString(payload, "title");
28
+ return addPlanArtifact(projectRoot, "sprint", payload.title, {});
29
+ }
30
+ },
31
+ "plan.task.amend": {
32
+ describe: "Amend an existing Task's title/sections/type/feature/sprint.",
33
+ handler: (projectRoot, payload) => {
34
+ requireString(payload, "id");
35
+ return amendPlanArtifact(projectRoot, "task", payload.id, payload);
36
+ }
37
+ },
38
+ "knowledge.fact.add": {
39
+ describe: "Create a Knowledge (K-NNN) candidate.",
40
+ handler: (projectRoot, payload) => {
41
+ requireString(payload, "title");
42
+ return createMemory(projectRoot, "knowledge", payload);
43
+ }
44
+ },
45
+ "knowledge.decision.add": {
46
+ describe: "Create a Decision (DEC-NNN) candidate.",
47
+ handler: (projectRoot, payload) => {
48
+ requireString(payload, "title");
49
+ return createMemory(projectRoot, "decision", payload);
50
+ }
51
+ },
52
+ "knowledge.insight.propose": {
53
+ describe: "Propose an Insight (INS-NNN) candidate.",
54
+ handler: (projectRoot, payload) => {
55
+ requireString(payload, "title");
56
+ return createMemory(projectRoot, "insight", payload);
57
+ }
58
+ }
59
+ });
60
+
61
+ function requireString(payload, field) {
62
+ if (!payload || typeof payload[field] !== "string" || !payload[field].trim()) {
63
+ throw new Error(`payload.${field} must be a non-empty string`);
64
+ }
65
+ }
66
+
67
+ function listActions() {
68
+ return Object.entries(REGISTRY).map(([name, { describe }]) => ({ name, describe }));
69
+ }
70
+
71
+ function executeAction(name, projectRoot, payload) {
72
+ const entry = REGISTRY[name];
73
+ if (!entry) {
74
+ const known = Object.keys(REGISTRY).sort().join(", ");
75
+ throw new Error(`Unknown action "${name}". Known actions: ${known}`);
76
+ }
77
+ const normalizedPayload = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
78
+ return entry.handler(projectRoot, normalizedPayload);
79
+ }
80
+
81
+ module.exports = { REGISTRY, executeAction, listActions };
@@ -36,7 +36,7 @@ const nouns = Object.freeze({
36
36
  fact: ["--add [--title] [--content] [--evidence] [--relation] [--subject] [--source] [--source-id] [--valid-from] [--valid-until] [--review-trigger]", "--list [--include-inactive]", "--show", "--approve [--evidence] [--note]", "--reject [--note]", "--deprecate [--note]", "--invalidate [--note]"],
37
37
  decision: ["--add [--title] [--content] [--evidence] [--relation] [--subject] [--source] [--source-id] [--valid-from] [--valid-until] [--review-trigger]", "--list [--include-inactive]", "--show", "--resolve [--evidence] [--note]", "--deprecate [--note]", "--invalidate [--note]"],
38
38
  insight: ["--propose [--title] [--content] [--evidence] [--relation] [--subject] [--source] [--source-id] [--confidence] [--valid-from] [--valid-until] [--review-trigger]", "--list [--include-inactive]", "--show", "--confirm [--evidence] [--note]", "--stale [--note]", "--reject [--note]", "--deprecate [--note]", "--invalidate [--note]"],
39
- dossier: ["--add [--title] [--content] [--evidence] [--relation] [--subject] [--source] [--source-id] [--valid-from] [--valid-until] [--review-trigger]", "--list [--include-inactive]", "--show", "--refresh [--evidence] [--note]", "--stale [--note]", "--deprecate [--note]", "--archive [--note]"],
39
+ dossier: ["--add [--title] [--content] [--evidence] [--relation] [--subject] [--source] [--source-id] [--valid-from] [--valid-until] [--review-trigger]", "--list [--include-inactive]", "--show", "--refresh [--evidence] [--note]", "--stale [--note]", "--deprecate [--note]", "--archive [--note]", "--compact --dry-run", "--compact --apply --approve", "--compact --rollback <DOS-NNN>"],
40
40
  context: ["--build", "--update", "--show", "--clear"],
41
41
  map: ["--build", "--show"],
42
42
  errors: ["--show"],
@@ -55,7 +55,7 @@ const nouns = Object.freeze({
55
55
  description: "run scoped evidence-based quality gates",
56
56
  subjects: {
57
57
  code: ["--run"],
58
- artifact: ["--run", "--record --task <TASK-NNN> [--run <RUN-NNN>] [--title] [--evidence]"],
58
+ artifact: ["--run [--staged]", "--record --task <TASK-NNN> [--run <RUN-NNN>] [--title] [--evidence]"],
59
59
  migration: ["--run"],
60
60
  release: ["--run"]
61
61
  }
@@ -68,6 +68,7 @@ const nouns = Object.freeze({
68
68
  update: ["all [--project] [--seal-policy] [--migrate]", "codex [--project] [--seal-policy] [--migrate]", "opencode [--project] [--seal-policy] [--migrate]", "claude [--project] [--seal-policy] [--migrate]"],
69
69
  migrate: ["--to 2 --dry-run", "--to 2 --apply", "--to 2 --rollback"],
70
70
  doctor: ["all [--strict] [--recover] [--dry-run]", "codex [--strict] [--recover] [--dry-run]", "opencode [--strict] [--recover] [--dry-run]", "claude [--strict] [--recover] [--dry-run]"],
71
+ watch: ["--start", "--stop", "--status"],
71
72
  uninstall: ["--force"],
72
73
  help: ["<topic>"]
73
74
  }
@@ -51,17 +51,38 @@ const VALID_RUN_STATUS = new Set(["executing", "validating", "learning", "partia
51
51
  const TASK_STATUS_ALIAS = {
52
52
  done: "completed",
53
53
  todo: "backlog",
54
+ pending: "backlog",
55
+ planned: "backlog",
54
56
  complete: "completed",
57
+ finished: "completed",
58
+ closed: "completed",
59
+ skipped: "cancelled",
60
+ canceled: "cancelled",
61
+ wontdo: "cancelled",
62
+ "won't do": "cancelled",
55
63
  in_progress: "running",
56
- executing: "running", // Task uses "running", Run uses "executing"
57
- active: "running"
64
+ "in-progress": "running",
65
+ wip: "running",
66
+ doing: "running",
67
+ executing: "running",
68
+ active: "running",
69
+ review: "validating",
70
+ reviewing: "validating"
58
71
  };
59
72
 
60
73
  const RUN_STATUS_ALIAS = {
61
74
  complete: "completed",
62
75
  in_progress: "executing",
76
+ "in-progress": "executing",
77
+ running: "executing",
78
+ wip: "executing",
63
79
  done: "completed",
64
- partial: "failed"
80
+ finished: "completed",
81
+ closed: "completed",
82
+ partial: "failed",
83
+ canceled: "blocked",
84
+ cancelled: "blocked",
85
+ skipped: "blocked"
65
86
  };
66
87
 
67
88
  // When syncing Task.status from latest Run.status, translate Run vocabulary
package/lib/errors.js CHANGED
@@ -40,6 +40,7 @@ const CATALOG = Object.freeze({
40
40
  "SR-E-150": { summary: "Guardrail check blocked the operation.", remediation: "Read the reported GR-NNN, satisfy or retire it explicitly; guardrails never bypass silently." },
41
41
  "SR-E-151": { summary: "Guardrail obligation is still pending.", remediation: "Resolve each `pending guardrail` via `scrumrun plan run --satisfy-guardrail` before completing the Run." },
42
42
  "SR-E-152": { summary: "Guardrail declaration is malformed.", remediation: "Every active guardrail requires Status, Enforcement, Scope, and Rule fields; check .scrumrun/guardrails.md." },
43
+ "SR-E-153": { summary: "Declarative Guardrail YAML is malformed.", remediation: "Use the documented yaml enforcement block with match, on_violation, severity, and optional evidence." },
43
44
 
44
45
  // Edit permits (Mutation Gateway)
45
46
  "SR-E-200": { summary: "No edit permit for this path.", remediation: "Request one with `scrumrun plan run --authorize-mutation RUN-NNN --path <path>` before editing canonical or source files." },
@@ -66,6 +67,9 @@ const CATALOG = Object.freeze({
66
67
  // Conformance / doctor
67
68
  "SR-E-450": { summary: "Conformance check failed.", remediation: "The reported invariant identifies the exact violation; the message includes the file and expected shape." },
68
69
  "SR-E-451": { summary: "Installed client asset is stale.", remediation: "Re-run `scrumrun update` for the specific client. `doctor --strict` shows which files diverge." },
70
+ "SR-E-452": { summary: "Task Markdown is missing a required section.", remediation: "Add the reported section (e.g. `## Request`, `## Done when`) to the Task file. The validator lives in `lib/v2/task-schema.js`." },
71
+ "SR-E-453": { summary: "Completed Task has no `## Completion` or associated Run `## Technical Summary`.", remediation: "Append a short `## Completion` bullet list to the Task, or complete the Run with `plan run --complete --summary \"...\"`." },
72
+ "SR-E-454": { summary: "Task is running inside a git repository but no `git.branch` was captured.", remediation: "Warning-only. Reconcile with `scrumrun repair --apply`, or add `git: { branch: <name>, base_sha: <sha> }` to the Task frontmatter." },
69
73
 
70
74
  // Configuration / install
71
75
  "SR-E-500": { summary: "ScrumRun project not initialized.", remediation: "Run `scrumrun init` in the repository root." },