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 +22 -0
- package/README.md +1 -1
- package/bin/scrumrun.js +26 -1
- package/lib/commands/pretty-intake.js +15 -17
- package/lib/memory/index.js +9 -4
- package/lib/runtime/orchestrator.js +12 -1
- package/lib/runtime/request-engine.js +2 -1
- package/lib/runtime/workspace-state.js +11 -0
- package/package.json +1 -1
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.
|
|
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) =>
|
|
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 =
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
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));
|
package/lib/memory/index.js
CHANGED
|
@@ -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 =
|
|
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) =>
|
|
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
|
|
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,
|