scrumrun 2.7.1 → 2.7.3
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 +16 -0
- package/README.md +1 -1
- package/bin/scrumrun.js +14 -2
- package/lib/memory/service.js +1 -1
- package/lib/runtime/orchestrator.js +118 -5
- package/lib/runtime/review-service.js +1 -1
- package/package.json +1 -1
- package/types/index.d.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ All notable changes follow Semantic Versioning.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 2.7.3 - 2026-08-22
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Manual Task/Feature/Sprint creation via CLI.** `sc plan task --add`, `sc plan feature --add`, and `sc plan sprint --add` now create canonical artifacts directly (previously these documented actions fell through to the "Agent workflow" hint without writing anything). `--add` parks intent only — a Task lands in `backlog` with no Run, and `sc plan task --start TASK-NNN` remains the explicit approval that creates the first Run. `sc plan task --add` accepts `--type fix|task|docs|discovery` and `--status backlog|proposed`; Feature and Sprint default to their initial statuses. `--list`/`--show` now also cover Feature and Sprint artifacts.
|
|
12
|
+
|
|
13
|
+
## 2.7.2 - 2026-08-21
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **Duplicate artifact ids across concurrent agents.** All create operations now serialize on a single global `create` lock (`approveRequest`, `retryTask`, `startBacklogTask`, `createMemory`, `recordArtifactReview`) instead of independent per-operation locks (`approval`, `task-…`, `memory-…`, `review-artifact`). Previously two agents could read the same `max` id and both write `TASK-NNN`/`RUN-NNN`; the race is now closed at the source. Conformance's `ID_DUPLICATE` finding remains as a detection backstop for hand-written files.
|
|
18
|
+
|
|
19
|
+
### Validation
|
|
20
|
+
|
|
21
|
+
- Added `tests/id-concurrency.test.js` — concurrent backlog starts and memory proposals across child processes allocate unique ids.
|
|
22
|
+
|
|
7
23
|
## 2.7.1 - 2026-08-21
|
|
8
24
|
|
|
9
25
|
### 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.3` · **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
|
@@ -31,7 +31,7 @@ const { ARTIFACT_TYPES, ArtifactRepository } = require(path.join(root, "lib", "v
|
|
|
31
31
|
const { aliases: COMMAND_ALIASES, resolveAlias, resolveRoute } = require(path.join(root, "lib", "commands", "manifest"));
|
|
32
32
|
const { renderCommandHelp, renderCompatibilityPrompt, renderRootPrompt } = require(path.join(root, "lib", "commands", "render"));
|
|
33
33
|
const { planRequest } = require(path.join(root, "lib", "runtime", "request-engine"));
|
|
34
|
-
const { approveRequest, nextBacklogTask, refreshState, retryTask, startBacklogTask, transitionRun } = require(path.join(root, "lib", "runtime", "orchestrator"));
|
|
34
|
+
const { addPlanArtifact, approveRequest, nextBacklogTask, refreshState, retryTask, startBacklogTask, transitionRun } = require(path.join(root, "lib", "runtime", "orchestrator"));
|
|
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"));
|
|
@@ -1451,6 +1451,18 @@ function executeRootRoute(route) {
|
|
|
1451
1451
|
console.log(`Started ${result.task.id} (${result.run.id}) assigned to ${result.task.assignee || "agent"}.`);
|
|
1452
1452
|
return;
|
|
1453
1453
|
}
|
|
1454
|
+
if (noun === "plan" && ["task", "feature", "sprint"].includes(subject) && routeArgs[0] === "--add") {
|
|
1455
|
+
const label = removeOptionPairs(routeArgs.slice(1), ["--type", "--status"])
|
|
1456
|
+
.filter((arg) => !arg.startsWith("--"))
|
|
1457
|
+
.join(" ")
|
|
1458
|
+
.trim();
|
|
1459
|
+
const result = addPlanArtifact(process.cwd(), subject, label, {
|
|
1460
|
+
type: optionValue(routeArgs, "--type"),
|
|
1461
|
+
status: optionValue(routeArgs, "--status")
|
|
1462
|
+
});
|
|
1463
|
+
console.log(`Created ${result.record.status} ${result.record.id}: ${path.relative(process.cwd(), result.file)}`);
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1454
1466
|
if (noun === "plan" && subject === "run" && routeArgs[0] === "--render") {
|
|
1455
1467
|
const { renderRunFromDisk } = require(path.join(root, "lib", "commands", "run-render"));
|
|
1456
1468
|
const runId = routeArgs[1];
|
|
@@ -1503,7 +1515,7 @@ function executeRootRoute(route) {
|
|
|
1503
1515
|
}
|
|
1504
1516
|
return;
|
|
1505
1517
|
}
|
|
1506
|
-
if (noun === "plan" && ["task", "run"].includes(subject) && ["--list", "--show"].includes(routeArgs[0])) {
|
|
1518
|
+
if (noun === "plan" && ["task", "run", "feature", "sprint"].includes(subject) && ["--list", "--show"].includes(routeArgs[0])) {
|
|
1507
1519
|
const repository = new ArtifactRepository(projectFile());
|
|
1508
1520
|
if (routeArgs[0] === "--list") {
|
|
1509
1521
|
const artifacts = repository.list(subject).map((artifact) => `${artifact.record.id} | ${artifact.record.status} | ${((artifact.body || "").match(/^# ([^\r\n]+)/m) || [])[1] || artifact.record.id}`);
|
package/lib/memory/service.js
CHANGED
|
@@ -213,7 +213,7 @@ function createMemory(projectRoot, kind, options = {}) {
|
|
|
213
213
|
if (!MEMORY_KINDS.has(kind)) throw new Error(`Unsupported memory kind: ${kind}`);
|
|
214
214
|
assertV2Project(projectRoot);
|
|
215
215
|
const scrumDir = path.join(projectRoot, ".scrumrun");
|
|
216
|
-
return withArtifactLock(scrumDir,
|
|
216
|
+
return withArtifactLock(scrumDir, "create", () => createMemoryUnlocked(projectRoot, kind, options));
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
function setFrontmatter(content, updates) {
|
|
@@ -18,10 +18,11 @@ const { buildContextPackage } = require("./context");
|
|
|
18
18
|
const { agentIdentity, evaluatePolicy } = require("./policy-engine");
|
|
19
19
|
const { artifactSnapshot, canonicalFingerprint, canonicalWatchSnapshot } = require("./canonical-snapshot");
|
|
20
20
|
const { decodeApproval } = require("./request-engine");
|
|
21
|
+
const { containsSecret } = require("../security/secrets");
|
|
21
22
|
const { extractLearningCandidates } = require("../code-intel/learning");
|
|
22
23
|
const { appendRunEvent, appendTechnicalSummary, createRunBody, instant } = require("./run-ledger");
|
|
23
24
|
const { generateBriefing } = require("./briefing");
|
|
24
|
-
const { policyState, prepareCompletion, publicWorkspace, verifyWorkspaceIntegrity, workspaceState } = require("./mutation-gateway");
|
|
25
|
+
const { assertCanonicalWrite, policyState, prepareCompletion, publicWorkspace, verifyWorkspaceIntegrity, workspaceState } = require("./mutation-gateway");
|
|
25
26
|
const { recoverPendingTransactions, runKernelTransaction } = require("../v2/transaction");
|
|
26
27
|
|
|
27
28
|
const TERMINAL = new Set(["completed", "failed", "cancelled", "resolved", "rejected", "deprecated", "invalidated", "archived", "passed"]);
|
|
@@ -160,7 +161,7 @@ function approveRequestUnlocked(projectRoot, token, { failurePoint = null, inter
|
|
|
160
161
|
}
|
|
161
162
|
|
|
162
163
|
function approveRequest(projectRoot, token, options = {}) {
|
|
163
|
-
return withArtifactLock(path.join(projectRoot, ".scrumrun"), "
|
|
164
|
+
return withArtifactLock(path.join(projectRoot, ".scrumrun"), "create", () => approveRequestUnlocked(projectRoot, token, options));
|
|
164
165
|
}
|
|
165
166
|
|
|
166
167
|
function transitionedArtifactContent(content, kind, nextStatus, updated, assignee = null) {
|
|
@@ -338,7 +339,7 @@ function retryTaskUnlocked(projectRoot, taskId, { note = "Retry explicitly appro
|
|
|
338
339
|
}
|
|
339
340
|
|
|
340
341
|
function retryTask(projectRoot, taskId, options = {}) {
|
|
341
|
-
return withArtifactLock(path.join(projectRoot, ".scrumrun"),
|
|
342
|
+
return withArtifactLock(path.join(projectRoot, ".scrumrun"), "create", () => retryTaskUnlocked(projectRoot, taskId, options));
|
|
342
343
|
}
|
|
343
344
|
|
|
344
345
|
function nextBacklogTask(repository) {
|
|
@@ -408,7 +409,119 @@ function startBacklogTaskUnlocked(projectRoot, taskId, { note = "Backlog Task st
|
|
|
408
409
|
}
|
|
409
410
|
|
|
410
411
|
function startBacklogTask(projectRoot, taskId, options = {}) {
|
|
411
|
-
return withArtifactLock(path.join(projectRoot, ".scrumrun"),
|
|
412
|
+
return withArtifactLock(path.join(projectRoot, ".scrumrun"), "create", () => startBacklogTaskUnlocked(projectRoot, taskId, options));
|
|
412
413
|
}
|
|
413
414
|
|
|
414
|
-
|
|
415
|
+
const ADDABLE_PLAN_KINDS = new Set(["task", "feature", "sprint"]);
|
|
416
|
+
const ADD_PLAN_DEFAULTS = Object.freeze({
|
|
417
|
+
task: { status: "backlog", type: "task" },
|
|
418
|
+
feature: { status: "backlog", type: "initiative" },
|
|
419
|
+
sprint: { status: "proposed", type: null }
|
|
420
|
+
});
|
|
421
|
+
const ADD_PLAN_INITIAL_STATUS = Object.freeze({
|
|
422
|
+
task: ["backlog", "proposed"],
|
|
423
|
+
feature: ["backlog", "proposed"],
|
|
424
|
+
sprint: ["proposed"]
|
|
425
|
+
});
|
|
426
|
+
const TASK_TYPES = new Set(["task", "fix", "docs", "discovery"]);
|
|
427
|
+
|
|
428
|
+
function planArtifactBody(kind, title, created) {
|
|
429
|
+
const source = `- ${created}: created via CLI (\`sc plan ${kind} --add\`).`;
|
|
430
|
+
if (kind === "task") {
|
|
431
|
+
return [
|
|
432
|
+
`# ${title}`,
|
|
433
|
+
"",
|
|
434
|
+
"## Request",
|
|
435
|
+
"",
|
|
436
|
+
title,
|
|
437
|
+
"",
|
|
438
|
+
"## Acceptance Criteria",
|
|
439
|
+
"",
|
|
440
|
+
'- [ ] _Define what "done" means before execution._',
|
|
441
|
+
"",
|
|
442
|
+
"## Source",
|
|
443
|
+
"",
|
|
444
|
+
source
|
|
445
|
+
].join("\n");
|
|
446
|
+
}
|
|
447
|
+
if (kind === "feature") {
|
|
448
|
+
return [
|
|
449
|
+
`# ${title}`,
|
|
450
|
+
"",
|
|
451
|
+
"## Motivation",
|
|
452
|
+
"",
|
|
453
|
+
title,
|
|
454
|
+
"",
|
|
455
|
+
"## Exit criteria",
|
|
456
|
+
"",
|
|
457
|
+
'- [ ] _Define what success looks like._',
|
|
458
|
+
"",
|
|
459
|
+
"## Source",
|
|
460
|
+
"",
|
|
461
|
+
source
|
|
462
|
+
].join("\n");
|
|
463
|
+
}
|
|
464
|
+
return [
|
|
465
|
+
`# ${title}`,
|
|
466
|
+
"",
|
|
467
|
+
"## Tasks",
|
|
468
|
+
"",
|
|
469
|
+
"_Pending: add Task ids here._",
|
|
470
|
+
"",
|
|
471
|
+
"## Exit Gate",
|
|
472
|
+
"",
|
|
473
|
+
'- [ ] _Define the batch completion condition._',
|
|
474
|
+
"",
|
|
475
|
+
"## Source",
|
|
476
|
+
"",
|
|
477
|
+
source
|
|
478
|
+
].join("\n");
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function addPlanArtifactUnlocked(projectRoot, kind, label, { type = null, status = null } = {}) {
|
|
482
|
+
if (!ADDABLE_PLAN_KINDS.has(kind)) throw new Error(`Unsupported plan kind: ${kind}`);
|
|
483
|
+
const scrumDir = path.join(projectRoot, ".scrumrun");
|
|
484
|
+
recoverPendingTransactions(scrumDir);
|
|
485
|
+
const repository = new ArtifactRepository(scrumDir);
|
|
486
|
+
const title = String(label || "").trim();
|
|
487
|
+
if (!title) throw new Error(`A non-empty title is required for ${kind} --add.`);
|
|
488
|
+
if (/\r|\n/.test(title)) throw new Error(`${kind} title must be one line.`);
|
|
489
|
+
if (title.length > 200) throw new Error(`${kind} title exceeds the 200 character limit.`);
|
|
490
|
+
if (containsSecret(title)) throw new Error("Secret-like content is forbidden outside the local vault.");
|
|
491
|
+
const chosenStatus = status || ADD_PLAN_DEFAULTS[kind].status;
|
|
492
|
+
if (!ADD_PLAN_INITIAL_STATUS[kind].includes(chosenStatus)) {
|
|
493
|
+
throw new Error(`Invalid ${kind} status: ${chosenStatus}. Allowed for --add: ${ADD_PLAN_INITIAL_STATUS[kind].join(", ")}.`);
|
|
494
|
+
}
|
|
495
|
+
let chosenType = ADD_PLAN_DEFAULTS[kind].type;
|
|
496
|
+
if (kind === "task" && type) {
|
|
497
|
+
if (!TASK_TYPES.has(type)) throw new Error(`Invalid task --type: ${type}. Valid: ${[...TASK_TYPES].join(", ")}.`);
|
|
498
|
+
chosenType = type;
|
|
499
|
+
}
|
|
500
|
+
const created = date();
|
|
501
|
+
const record = {
|
|
502
|
+
id: nextId(repository, kind),
|
|
503
|
+
kind,
|
|
504
|
+
status: chosenStatus,
|
|
505
|
+
created,
|
|
506
|
+
updated: created,
|
|
507
|
+
method: METHOD_VERSION
|
|
508
|
+
};
|
|
509
|
+
if (kind === "task") {
|
|
510
|
+
record.type = chosenType;
|
|
511
|
+
record.feature = null;
|
|
512
|
+
record.sprint = null;
|
|
513
|
+
} else if (kind === "feature") {
|
|
514
|
+
record.type = chosenType;
|
|
515
|
+
}
|
|
516
|
+
const body = planArtifactBody(kind, title, created);
|
|
517
|
+
assertCanonicalWrite(projectRoot, `add-${kind}`, [title, body]);
|
|
518
|
+
repository.write(record, body);
|
|
519
|
+
return repository.read(kind, record.id);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function addPlanArtifact(projectRoot, kind, label, options = {}) {
|
|
523
|
+
const scrumDir = path.join(projectRoot, ".scrumrun");
|
|
524
|
+
return withArtifactLock(scrumDir, "create", () => addPlanArtifactUnlocked(projectRoot, kind, label, options));
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
module.exports = { addPlanArtifact, approveRequest, nextBacklogTask, nextId, refreshState, renderState, retryTask, startBacklogTask, stateFingerprint, stateIsStale, transitionRun };
|
|
@@ -86,7 +86,7 @@ function recordArtifactReviewUnlocked(projectRoot, options) {
|
|
|
86
86
|
|
|
87
87
|
function recordArtifactReview(projectRoot, options = {}) {
|
|
88
88
|
const scrumDir = path.join(projectRoot, ".scrumrun");
|
|
89
|
-
return withArtifactLock(scrumDir, "
|
|
89
|
+
return withArtifactLock(scrumDir, "create", () => recordArtifactReviewUnlocked(projectRoot, options));
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
module.exports = { recordArtifactReview };
|
package/package.json
CHANGED
package/types/index.d.ts
CHANGED
|
@@ -396,6 +396,7 @@ export function renderState(repository: ArtifactRepository): string;
|
|
|
396
396
|
export function stateFingerprint(repository: ArtifactRepository): string;
|
|
397
397
|
export function stateIsStale(scrumDir: string): boolean;
|
|
398
398
|
export function nextId(repository: ArtifactRepository, kind: ArtifactKind): string;
|
|
399
|
+
export function addPlanArtifact(projectRoot: string, kind: ArtifactKind, label: string, options?: { type?: string | null; status?: string | null }): ParsedArtifact & { file: string };
|
|
399
400
|
|
|
400
401
|
// ---------------------------------------------------------------------------
|
|
401
402
|
// lib/runtime/mutation-gateway
|