scrumrun 2.7.3 → 2.7.6

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,28 @@ All notable changes follow Semantic Versioning.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 2.7.6 - 2026-08-24
8
+
9
+ ### Added
10
+
11
+ - **Branch tracking on artifacts.** Task, Feature, Sprint, and Run artifacts now record the active git branch in a `branch` frontmatter field at creation time (`null` outside a git repo). `sc plan task --list` and the generated `map.md` show the branch, giving a human-readable "which branch did this work originate from" reference index to complement the exact commit SHA already captured in each Run's workspace baseline.
12
+
13
+ ## 2.7.5 - 2026-08-22
14
+
15
+ ### Added
16
+
17
+ - **Project-root discovery.** The `sc` CLI now walks up from the current directory to find the nearest `.scrumrun/method.json`, so commands run from any subdirectory (e.g. `app/front`) operate on the project root instead of failing with a cryptic "Mutation Gateway requires a valid method.json" error. `sc config init` and `sc-init` still initialize in the current directory.
18
+
19
+ ### Fixed
20
+
21
+ - **Missing-project error.** `sc plan task --add` now reports a clear "ScrumRun project not found" message (with `scrumrun init` / run-from-root guidance) instead of leaking an internal ENOENT, and no longer leaves a stray `.scrumrun/.cache` lock directory behind when the command is run outside a project.
22
+
23
+ ## 2.7.4 - 2026-08-22
24
+
25
+ ### Fixed
26
+
27
+ - **Approval token truncated in interactive intake.** The pretty terminal renderer was shrinking the approval command to the first 20 characters of the token, so a copy-pasted `sc plan intake --approve …` failed with "Approval token is malformed or was modified." The full token now renders wrapped across lines (copyable, no ellipsis), and `decodeApproval` strips surrounding/interstitial whitespace so pasted tokens with line breaks or spaces still validate.
28
+
7
29
  ## 2.7.3 - 2026-08-22
8
30
 
9
31
  ### Added
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:** `2.7.3` · **Method target:** `2.0.0` · **Runtime:** Node.js `>=22.13.0` · **License:** MIT
7
+ **Package:** `2.7.6` · **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
@@ -1198,6 +1198,25 @@ function v2Project(cwd = process.cwd()) {
1198
1198
  }
1199
1199
  }
1200
1200
 
1201
+ function findProjectRoot(startDir = process.cwd()) {
1202
+ let dir = path.resolve(startDir);
1203
+ while (true) {
1204
+ const marker = path.join(dir, ".scrumrun", "method.json");
1205
+ if (fs.existsSync(marker) && !fs.lstatSync(marker).isSymbolicLink() && fs.lstatSync(marker).isFile()) {
1206
+ return dir;
1207
+ }
1208
+ const parent = path.dirname(dir);
1209
+ if (parent === dir) return null;
1210
+ dir = parent;
1211
+ }
1212
+ }
1213
+
1214
+ function chdirToProjectRoot() {
1215
+ const root = findProjectRoot();
1216
+ if (root && root !== process.cwd()) process.chdir(root);
1217
+ return root;
1218
+ }
1219
+
1201
1220
  function optionValues(args, flag) {
1202
1221
  const values = [];
1203
1222
  for (let index = 0; index < args.length; index++) {
@@ -1518,7 +1537,11 @@ function executeRootRoute(route) {
1518
1537
  if (noun === "plan" && ["task", "run", "feature", "sprint"].includes(subject) && ["--list", "--show"].includes(routeArgs[0])) {
1519
1538
  const repository = new ArtifactRepository(projectFile());
1520
1539
  if (routeArgs[0] === "--list") {
1521
- const artifacts = repository.list(subject).map((artifact) => `${artifact.record.id} | ${artifact.record.status} | ${((artifact.body || "").match(/^# ([^\r\n]+)/m) || [])[1] || artifact.record.id}`);
1540
+ const artifacts = repository.list(subject).map((artifact) => {
1541
+ const title = ((artifact.body || "").match(/^# ([^\r\n]+)/m) || [])[1] || artifact.record.id;
1542
+ const branch = artifact.record.branch ? ` [${artifact.record.branch}]` : "";
1543
+ return `${artifact.record.id} | ${artifact.record.status} | ${title}${branch}`;
1544
+ });
1522
1545
  console.log(artifacts.length ? artifacts.join("\n") : `No ${subject} artifacts.`);
1523
1546
  } else {
1524
1547
  const artifact = repository.read(subject, routeArgs[1]);
@@ -1644,6 +1667,7 @@ function runRoot(parts) {
1644
1667
  process.exitCode = 1;
1645
1668
  return;
1646
1669
  }
1670
+ if (!(route.noun === "config" && route.subject === "init")) chdirToProjectRoot();
1647
1671
  return executeRootRoute(route);
1648
1672
  }
1649
1673
 
@@ -1652,6 +1676,7 @@ function runCompatibilityAlias(alias, parts) {
1652
1676
  console.warn(`Deprecated: ${alias} now executes /sc ${route.noun} ${route.subject}.`);
1653
1677
  if (route.note) console.warn(route.note);
1654
1678
  if (alias === "sc-backlog") return runBacklog(parts);
1679
+ if (alias !== "sc-init") chdirToProjectRoot();
1655
1680
  return executeRootRoute({ type: "route", noun: route.noun, subject: route.subject, args: route.args });
1656
1681
  }
1657
1682
 
@@ -89,6 +89,15 @@ function wrap(text, width) {
89
89
  return lines.length ? lines : [""];
90
90
  }
91
91
 
92
+ function hardWrap(text, width) {
93
+ const source = String(text || "");
94
+ const lines = [];
95
+ for (let index = 0; index < source.length; index += width) {
96
+ lines.push(source.slice(index, index + width));
97
+ }
98
+ return lines.length ? lines : [""];
99
+ }
100
+
92
101
  function boxTop(width, title) {
93
102
  const label = ` ${title} `;
94
103
  const remaining = width - visibleLength(label) - 4;
@@ -154,16 +163,6 @@ function fieldLine(label, value, width, valueColor = FG.white, labelColor = FG.g
154
163
  return rendered;
155
164
  }
156
165
 
157
- function commandBox(interiorWidth, command) {
158
- const inside = ` $ ${command} `;
159
- const boxWidth = Math.min(interiorWidth, visibleLength(inside) + 4);
160
- const top = paint(DIM_ACID_FG, `┌${"─".repeat(boxWidth - 2)}┐`);
161
- const bottom = paint(DIM_ACID_FG, `└${"─".repeat(boxWidth - 2)}┘`);
162
- const content = padEnd(inside, boxWidth - 2);
163
- const middle = `${paint(DIM_ACID_FG, "│")}${paint(ACID_FG, content)}${paint(DIM_ACID_FG, "│")}`;
164
- return [top, middle, bottom];
165
- }
166
-
167
166
  function pipelineDetail(stage, plan) {
168
167
  if (stage === "POLICY") {
169
168
  return `${plan.policy.checked.length} checked · ${plan.policy.deferred.length} deferred`;
@@ -240,16 +239,15 @@ function renderIntake(plan) {
240
239
  }
241
240
  }
242
241
  } else if (plan.approvalToken) {
243
- const command = `scrumrun sc plan intake --approve ${plan.approvalToken}`;
244
- const truncated = command.length > width - 8 ? `scrumrun sc plan intake --approve ${plan.approvalToken.slice(0, 20)}…` : command;
245
- for (const boxRow of commandBox(width - 6, truncated)) {
246
- lines.push(boxLine(width, ` ${boxRow}`));
242
+ const command = "scrumrun sc plan intake --approve";
243
+ const tokenWidth = Math.max(16, width - 8);
244
+ lines.push(boxLine(width, ` ${paint(FG.gray, "APPROVE · copy the command below to create Task + Run")}`));
245
+ lines.push(boxLine(width, ` ${paint(ACID_FG, "$")} ${paint(FG.white, command)}`));
246
+ for (const chunk of hardWrap(plan.approvalToken, tokenWidth)) {
247
+ lines.push(boxLine(width, ` ${paint(DIM_ACID_FG, chunk)}`));
247
248
  }
248
249
  lines.push(boxBlank(width));
249
250
  lines.push(boxLine(width, ` ${paint(FG.gray, "[ awaiting owner approval ]")}`));
250
- if (command.length !== truncated.length) {
251
- lines.push(boxLine(width, ` ${paint(FG.gray, "(full token above is truncated for display; copy from --json if needed)")}`));
252
- }
253
251
  }
254
252
  lines.push(boxBlank(width));
255
253
  lines.push(boxBottom(width));
@@ -12,7 +12,7 @@ const { containsSecret } = require("../security/secrets");
12
12
  const { canonicalWatchSnapshot, fileWatchSnapshot } = require("../runtime/canonical-snapshot");
13
13
 
14
14
  const CACHE_RELATIVE = path.join(".cache", "semantic-index.sqlite");
15
- const INDEX_SCHEMA_VERSION = 4;
15
+ const INDEX_SCHEMA_VERSION = 5;
16
16
  const INACTIVE = new Set(["rejected", "invalidated", "deprecated", "archived"]);
17
17
  const SEARCH_BACKENDS = new Set(["auto", "fts5", "like"]);
18
18
  const MAX_SEARCH_TOKENS = 32;
@@ -124,6 +124,7 @@ function createSchema(database, { searchBackend = "auto" } = {}) {
124
124
  path TEXT NOT NULL UNIQUE,
125
125
  hash TEXT NOT NULL,
126
126
  content TEXT NOT NULL,
127
+ branch TEXT,
127
128
  valid_from TEXT,
128
129
  valid_until TEXT,
129
130
  last_verified_commit TEXT,
@@ -380,7 +381,7 @@ function rebuildIndex(projectRoot, { searchBackend = "auto" } = {}) {
380
381
  try {
381
382
  database = new DatabaseSync(temp);
382
383
  selectedSearchBackend = createSchema(database, { searchBackend });
383
- const insertArtifact = database.prepare("INSERT INTO artifacts(id, kind, status, title, path, hash, content, valid_from, valid_until, last_verified_commit, review_trigger, search_text, active) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
384
+ const insertArtifact = database.prepare("INSERT INTO artifacts(id, kind, status, title, path, hash, content, branch, valid_from, valid_until, last_verified_commit, review_trigger, search_text, active) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
384
385
  const insertFts = selectedSearchBackend === "fts5"
385
386
  ? database.prepare("INSERT INTO artifacts_fts(id, title, content) VALUES (?, ?, ?)")
386
387
  : null;
@@ -400,6 +401,7 @@ function rebuildIndex(projectRoot, { searchBackend = "auto" } = {}) {
400
401
  artifact.path,
401
402
  sha256(artifact.content),
402
403
  artifact.content,
404
+ artifact.record.branch || null,
403
405
  artifact.record.valid_from || null,
404
406
  artifact.record.valid_until || null,
405
407
  artifact.record.last_verified_commit || null,
@@ -690,7 +692,7 @@ function graphIndex(projectRoot, { rebuild = true } = {}) {
690
692
  else if (status.stale) throw new Error("Semantic index is missing or stale; rebuild it first.");
691
693
  const database = new DatabaseSync(indexPath(projectRoot), { readOnly: true });
692
694
  try {
693
- const artifacts = database.prepare("SELECT id, kind, status, title, path, 'artifact' AS node_type FROM artifacts ORDER BY id").all();
695
+ const artifacts = database.prepare("SELECT id, kind, status, title, path, branch, 'artifact' AS node_type FROM artifacts ORDER BY id").all();
694
696
  const code = database.prepare("SELECT id, kind, status, name AS title, path, 'code' AS node_type, fingerprint, remapped_from FROM code_nodes ORDER BY id").all();
695
697
  return {
696
698
  nodes: [...artifacts, ...code],
@@ -725,7 +727,10 @@ function writeMap(projectRoot, { nodeLimit = 200, edgeLimit = 400 } = {}) {
725
727
  "",
726
728
  "## Nodes",
727
729
  "",
728
- ...(nodes.length ? nodes.map((node) => `- ${node.id} | ${node.node_type} | ${node.kind} | ${node.status} | ${node.title} | ${node.path}`) : ["- None."]),
730
+ ...(nodes.length ? nodes.map((node) => {
731
+ const branch = node.branch ? ` | branch:${node.branch}` : "";
732
+ return `- ${node.id} | ${node.node_type} | ${node.kind} | ${node.status} | ${node.title} | ${node.path}${branch}`;
733
+ }) : ["- None."]),
729
734
  ...(graph.nodes.length > nodes.length ? [`- … ${graph.nodes.length - nodes.length} more nodes in the semantic index.`] : []),
730
735
  "",
731
736
  "## Relations",
@@ -19,6 +19,7 @@ const { agentIdentity, evaluatePolicy } = require("./policy-engine");
19
19
  const { artifactSnapshot, canonicalFingerprint, canonicalWatchSnapshot } = require("./canonical-snapshot");
20
20
  const { decodeApproval } = require("./request-engine");
21
21
  const { containsSecret } = require("../security/secrets");
22
+ const { currentBranch } = require("./workspace-state");
22
23
  const { extractLearningCandidates } = require("../code-intel/learning");
23
24
  const { appendRunEvent, appendTechnicalSummary, createRunBody, instant } = require("./run-ledger");
24
25
  const { generateBriefing } = require("./briefing");
@@ -103,6 +104,7 @@ function approveRequestUnlocked(projectRoot, token, { failurePoint = null, inter
103
104
  const created = date();
104
105
  const enforceablePolicy = policyState(projectRoot);
105
106
  const baseline = publicWorkspace(workspace);
107
+ const branch = currentBranch(projectRoot);
106
108
  const taskId = nextId(repository, "task");
107
109
  const runId = nextId(repository, "run");
108
110
  const task = {
@@ -115,6 +117,7 @@ function approveRequestUnlocked(projectRoot, token, { failurePoint = null, inter
115
117
  method: METHOD_VERSION,
116
118
  feature: null,
117
119
  sprint: null,
120
+ branch,
118
121
  assignee: agentIdentity(scrumDir) || "agent",
119
122
  approval_id: approvalId,
120
123
  context_fingerprint: payload.fingerprint,
@@ -129,6 +132,7 @@ function approveRequestUnlocked(projectRoot, token, { failurePoint = null, inter
129
132
  method: METHOD_VERSION,
130
133
  task: taskId,
131
134
  sprint: null,
135
+ branch,
132
136
  attempt: 1,
133
137
  ledger: 1,
134
138
  guardrails: 1,
@@ -304,6 +308,7 @@ function retryTaskUnlocked(projectRoot, taskId, { note = "Retry explicitly appro
304
308
  method: METHOD_VERSION,
305
309
  task: taskId,
306
310
  sprint: taskArtifact.record.sprint || null,
311
+ branch: currentBranch(projectRoot),
307
312
  attempt: attempts.length ? Math.max(...attempts) + 1 : 1,
308
313
  ledger: 1,
309
314
  guardrails: 1,
@@ -374,6 +379,7 @@ function startBacklogTaskUnlocked(projectRoot, taskId, { note = "Backlog Task st
374
379
  method: METHOD_VERSION,
375
380
  task: taskId,
376
381
  sprint: taskArtifact.record.sprint || null,
382
+ branch: currentBranch(projectRoot),
377
383
  attempt: 1,
378
384
  ledger: 1,
379
385
  guardrails: 1,
@@ -504,7 +510,8 @@ function addPlanArtifactUnlocked(projectRoot, kind, label, { type = null, status
504
510
  status: chosenStatus,
505
511
  created,
506
512
  updated: created,
507
- method: METHOD_VERSION
513
+ method: METHOD_VERSION,
514
+ branch: currentBranch(projectRoot)
508
515
  };
509
516
  if (kind === "task") {
510
517
  record.type = chosenType;
@@ -521,6 +528,10 @@ function addPlanArtifactUnlocked(projectRoot, kind, label, { type = null, status
521
528
 
522
529
  function addPlanArtifact(projectRoot, kind, label, options = {}) {
523
530
  const scrumDir = path.join(projectRoot, ".scrumrun");
531
+ const methodFile = path.join(scrumDir, "method.json");
532
+ if (!fs.existsSync(methodFile) || !fs.lstatSync(methodFile).isFile()) {
533
+ throw new Error(`ScrumRun project not found in ${projectRoot}. Run \`scrumrun init\` there, or run this command from inside the project.`);
534
+ }
524
535
  return withArtifactLock(scrumDir, "create", () => addPlanArtifactUnlocked(projectRoot, kind, label, options));
525
536
  }
526
537
 
@@ -82,7 +82,8 @@ function encodeApproval(plan) {
82
82
  }
83
83
 
84
84
  function decodeApproval(token) {
85
- const [encoded, checksum, extra] = String(token || "").split(".");
85
+ const cleaned = String(token || "").replace(/\s+/g, "");
86
+ const [encoded, checksum, extra] = cleaned.split(".");
86
87
  if (!encoded || !checksum || extra || sha256(`scrumrun-v2-approval\0${encoded}`) !== checksum) {
87
88
  throw new Error("Approval token is malformed or was modified.");
88
89
  }
@@ -110,6 +110,16 @@ function workspaceState(projectRoot) {
110
110
  return { ...state, fingerprint: sha256(stable(canonical)) };
111
111
  }
112
112
 
113
+ function currentBranch(projectRoot) {
114
+ try {
115
+ const branch = gitOutput(projectRoot, ["rev-parse", "--abbrev-ref", "HEAD"]).toString("utf8").trim();
116
+ if (!branch || branch === "HEAD") return null;
117
+ return branch;
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+
113
123
  function changesBetween(before, after) {
114
124
  if (!before || !after || before.mode !== after.mode || before.head !== after.head) throw new Error("Workspace baseline mode or Git HEAD changed during the Run.");
115
125
  const left = new Map(before.files.map((item) => [item.path, item]));
@@ -139,6 +149,7 @@ function pathAuthorized(relative, allowed) {
139
149
 
140
150
  module.exports = {
141
151
  changesBetween,
152
+ currentBranch,
142
153
  normalizedRelative,
143
154
  pathAuthorized,
144
155
  stable,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scrumrun",
3
- "version": "2.7.3",
3
+ "version": "2.7.6",
4
4
  "description": "Evidence-driven Agile runtime and semantic project memory for AI coding agents.",
5
5
  "bin": {
6
6
  "scrumrun": "bin/scrumrun.js",