scrumrun 3.0.1 → 3.0.2
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 +10 -0
- package/CORE.md +2 -0
- package/README.md +13 -1
- package/bin/scrumrun.js +22 -1
- package/docs/COMMANDS.md +16 -0
- package/lib/commands/manifest.js +3 -3
- package/lib/runtime/orchestrator.js +157 -2
- package/package.json +1 -1
- package/templates/project/AGENTS.md +1 -0
- package/templates/shared/skills/scrumrun/SKILL.md +6 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@ All notable changes follow Semantic Versioning.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 3.0.2 - 2026-08-31
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Planning artifact amendments.** `scrumrun plan task|feature|sprint --amend <ID>` updates planning truth through the CLI rather than hand-editing Markdown. It supports title and structured fields plus generic repeated `--section "Heading=content"`, so new scope context does not require another command. Task Feature/Sprint relations update their projections atomically.
|
|
12
|
+
|
|
13
|
+
### Safety
|
|
14
|
+
|
|
15
|
+
- A Task can be amended only while `backlog` or `proposed`; once started, its approved scope is preserved and new work belongs in a follow-up Task. Runs, Reviews, confirmed memory, and Guardrails remain append-only/evidence-led by design.
|
|
16
|
+
|
|
7
17
|
## 3.0.1 - 2026-08-31
|
|
8
18
|
|
|
9
19
|
### Fixed
|
package/CORE.md
CHANGED
|
@@ -86,6 +86,8 @@ AGENTS.md
|
|
|
86
86
|
|
|
87
87
|
**Never write Run events by hand.** After approval, work directly in code and in the linked Task Markdown. Finish the normal session once with `scrumrun plan run --finalize RUN-NNN`; the CLI then audits the complete workspace delta, verifies Guardrails and evidence, and writes the validated Run event chain. Do not use `npx scrumrun@latest` during execution. The older `--validate | --learn | --complete | --satisfy-guardrail | --authorize-mutation | --record-mutation` operations remain available only for an owner-requested strict path. Existing hand-written Runs can be recovered with `scrumrun plan run --normalize-legacy` (byte-exact original preserved in `.scrumrun/.migration-backup/runs/`).
|
|
88
88
|
|
|
89
|
+
**Amend planning truth through the CLI, never by hand.** Before execution, use `scrumrun plan task|feature|sprint --amend <ID>` to adjust the title, structured fields, or any named body section (`--section "Heading=content"`). A Task may be amended only in `backlog` or `proposed`; changing its `--feature` or `--sprint` also synchronizes the related projections atomically. Runs, Reviews, confirmed memory, and Guardrails are evidence/history and are not amendable: append a transition, record a new Review, or supersede/deprecate the old fact instead.
|
|
90
|
+
|
|
89
91
|
Canonical truth is Markdown. SQLite/cache data stores only rebuildable indexes, symbol projections, relations, and bounded context packages. Deleting `.cache/` must never delete authored truth.
|
|
90
92
|
|
|
91
93
|
`state.md` and the semantic index use two-tier freshness checks. Matching path/stat watch fingerprints avoid rereading unchanged sources; any metadata drift falls back to complete content hashing. A cache schema mismatch rebuilds the disposable index once. Watch metadata is only an optimization and never authority.
|
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:** `3.0.
|
|
7
|
+
**Package:** `3.0.2` · **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
|
|
|
@@ -93,6 +93,18 @@ scrumrun plan run --finalize RUN-001
|
|
|
93
93
|
|
|
94
94
|
The checkpoint validates the complete delta, policy, protected paths, secret boundary, acceptance evidence, and every Guardrail before writing the Run ledger and synchronizing the Task. Failed retries remain available as separate Runs.
|
|
95
95
|
|
|
96
|
+
Before a Task starts, refine it through the CLI rather than hand-editing Markdown. `--amend` updates title, request, acceptance criteria, Feature/Sprint links, task type, or any named section while preserving its ID and status. It synchronizes related Feature/Sprint projections atomically.
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
scrumrun plan task --amend TASK-153 \
|
|
100
|
+
--title "Organization Aside and context selector" \
|
|
101
|
+
--request "Add shared organization navigation, context selection, routes, and authorization boundaries." \
|
|
102
|
+
--acceptance "Organization context is available before protected routes render." \
|
|
103
|
+
--section "Architecture Notes=Navigation stays in the Aside; authorization stays server-side."
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Tasks become immutable in scope once started; create a follow-up Task for a new request. Runs, Reviews, confirmed memory, and Guardrails are deliberately append-only/evidence-led rather than editable. See [the command reference](docs/COMMANDS.md#what-can-be-changed) for the complete artifact matrix.
|
|
107
|
+
|
|
96
108
|
Each Run contains a machine-validated event ledger. Events have stable ids such as `RUN-044-EVT-003`, RFC3339 timestamps, actors, reasons, and typed evidence for commands, tests, files, reviews, decisions, insights, and risks. Run is the only operational history; Task keeps its approved scope and synchronized current status without duplicating those events. Completion is rejected when validation or learning evidence is missing.
|
|
97
109
|
|
|
98
110
|
Linked Task/Run writes use a durable ignored transaction journal. Captured failures roll back immediately; interrupted operations are recovered byte-exactly on retry or through explicit `doctor --recover`. Read-only audit reports pending recovery and never repairs state silently.
|
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 { addPlanArtifact, approveRequest, finalizeRun, nextBacklogTask, refreshErrors, refreshState, retryTask, startBacklogTask, transitionRun } = require(path.join(root, "lib", "runtime", "orchestrator"));
|
|
34
|
+
const { addPlanArtifact, amendPlanArtifact, approveRequest, finalizeRun, nextBacklogTask, refreshErrors, 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"));
|
|
@@ -1475,6 +1475,27 @@ function executeRootRoute(route) {
|
|
|
1475
1475
|
console.log(`Started ${result.task.id} (${result.run.id}) assigned to ${result.task.assignee || "agent"}.`);
|
|
1476
1476
|
return;
|
|
1477
1477
|
}
|
|
1478
|
+
if (noun === "plan" && ["task", "feature", "sprint"].includes(subject) && routeArgs[0] === "--amend") {
|
|
1479
|
+
const id = routeArgs[1];
|
|
1480
|
+
if (!id) throw new Error(`--amend requires a ${subject.toUpperCase()}-NNN id.`);
|
|
1481
|
+
const amendArgs = routeArgs.slice(2);
|
|
1482
|
+
const amendValue = (flag) => amendArgs.includes(flag) ? optionValue(amendArgs, flag) : undefined;
|
|
1483
|
+
const result = amendPlanArtifact(process.cwd(), subject, id, {
|
|
1484
|
+
title: amendValue("--title"),
|
|
1485
|
+
request: amendValue("--request"),
|
|
1486
|
+
acceptance: amendArgs.includes("--acceptance") ? optionValues(amendArgs, "--acceptance") : undefined,
|
|
1487
|
+
purpose: amendValue("--purpose"),
|
|
1488
|
+
exitCriteria: amendArgs.includes("--exit-criteria") ? optionValues(amendArgs, "--exit-criteria") : undefined,
|
|
1489
|
+
timebox: amendValue("--timebox"),
|
|
1490
|
+
exitGate: amendArgs.includes("--exit-gate") ? optionValues(amendArgs, "--exit-gate") : undefined,
|
|
1491
|
+
sections: optionValues(amendArgs, "--section"),
|
|
1492
|
+
type: amendValue("--type"),
|
|
1493
|
+
feature: amendValue("--feature"),
|
|
1494
|
+
sprint: amendValue("--sprint")
|
|
1495
|
+
});
|
|
1496
|
+
console.log(`Amended ${result.record.id}: ${path.relative(process.cwd(), result.file)} (status remains ${result.record.status}).`);
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1478
1499
|
if (noun === "plan" && ["task", "feature", "sprint"].includes(subject) && routeArgs[0] === "--add") {
|
|
1479
1500
|
const label = removeOptionPairs(routeArgs.slice(1), ["--type", "--status"])
|
|
1480
1501
|
.filter((arg) => !arg.startsWith("--"))
|
package/docs/COMMANDS.md
CHANGED
|
@@ -14,8 +14,11 @@ Use the installed `scrumrun` command for all normal project work. `/sc` is an op
|
|
|
14
14
|
scrumrun plan intake <request>
|
|
15
15
|
scrumrun plan intake --approve <token>
|
|
16
16
|
scrumrun plan task --add|--list|--show|--run|--audit|--cancel|--retry
|
|
17
|
+
scrumrun plan task --amend TASK-NNN [--title "..."] [--request "..."] [--acceptance "..."] [--section "Heading=content"] [--type fix|task|feature|docs|discovery] [--feature FEAT-NNN|null] [--sprint SPRINT-NNN|null]
|
|
17
18
|
scrumrun plan sprint --add|--list|--show|--start|--complete|--block
|
|
19
|
+
scrumrun plan sprint --amend SPRINT-NNN [--title "..."] [--timebox "..."] [--exit-gate "..."] [--section "Heading=content"]
|
|
18
20
|
scrumrun plan feature --add|--list|--show|--activate|--complete
|
|
21
|
+
scrumrun plan feature --amend FEAT-NNN [--title "..."] [--purpose "..."] [--exit-criteria "..."] [--section "Heading=content"]
|
|
19
22
|
scrumrun plan run --list|--show|--validate|--learn|--complete|--resume|--fail|--block [--note] [typed evidence flags]
|
|
20
23
|
scrumrun plan run --finalize RUN-NNN [--summary "technical recap"] [--note]
|
|
21
24
|
scrumrun plan run --authorize-mutation RUN-NNN --path <relative-path> [--path ...]
|
|
@@ -26,6 +29,19 @@ scrumrun plan challenge <question>
|
|
|
26
29
|
|
|
27
30
|
Normal execution is Markdown-first: after approval, work in code and the linked Task, then use one `--finalize` checkpoint. It verifies every workspace change and all Guardrail evidence before writing the Run transitions. A retry requires a failed, blocked, or partial Task and creates a new Run. Mutation permits are available only for explicitly requested strict mode.
|
|
28
31
|
|
|
32
|
+
`--amend` is the canonical way to adjust planning truth before execution — never edit an artifact Markdown file directly. Every named `--section "Heading=content"` replaces or adds one `## Heading` section, so new planning context does not need a new CLI release. Repeated `--acceptance`, `--exit-criteria`, and `--exit-gate` values become checklist items. Updating a Task's `--feature` or `--sprint` synchronizes the linked Feature/Sprint projection atomically. A Task is amendable only while `backlog` or `proposed`; after it starts, scope is historical/approved truth and follow-up work needs a new Task.
|
|
33
|
+
|
|
34
|
+
## What can be changed
|
|
35
|
+
|
|
36
|
+
| Artifact | Canonical operation | Why |
|
|
37
|
+
| --- | --- | --- |
|
|
38
|
+
| Task, Feature, Sprint | `--add`, `--amend`, lifecycle commands | Planning truth can be refined before execution. |
|
|
39
|
+
| Run | transitions, `--finalize`, `--retry` | Execution history is append-only; it is never amended. |
|
|
40
|
+
| Review | `--run` / `--record` | A verdict is evidence, not editable prose. Record another review if it changes. |
|
|
41
|
+
| Knowledge, Decision, Insight, Dossier | create + lifecycle commands | Preserve evidence lineage; supersede/deprecate rather than rewrite confirmed truth. |
|
|
42
|
+
| Guardrail | `--add` / `--retire` | Policy history must remain auditable. |
|
|
43
|
+
| `state.md`, `map.md`, cache | rebuild commands | Derived projections, never manually edited. |
|
|
44
|
+
|
|
29
45
|
## Knowledge
|
|
30
46
|
|
|
31
47
|
```text
|
package/lib/commands/manifest.js
CHANGED
|
@@ -6,9 +6,9 @@ const nouns = Object.freeze({
|
|
|
6
6
|
plan: {
|
|
7
7
|
description: "turn intent into Features, Tasks, Sprints, and Runs",
|
|
8
8
|
subjects: {
|
|
9
|
-
task: ["--add [--type fix] [--status backlog]", "--list", "--show", "--run", "--audit", "--cancel", "--retry [--reassign]", "--next", "--start [TASK-NNN]"],
|
|
10
|
-
sprint: ["--add", "--list", "--show", "--start", "--complete", "--block"],
|
|
11
|
-
feature: ["--add", "--list", "--show", "--activate", "--complete"],
|
|
9
|
+
task: ["--add [--type fix] [--status backlog]", "--amend <TASK-NNN> [--title] [--request] [--acceptance] [--section \"Heading=content\"] [--type task|fix|feature|docs|discovery] [--feature] [--sprint]", "--list", "--show", "--run", "--audit", "--cancel", "--retry [--reassign]", "--next", "--start [TASK-NNN]"],
|
|
10
|
+
sprint: ["--add", "--amend <SPRINT-NNN> [--title] [--timebox] [--exit-gate] [--section \"Heading=content\"]", "--list", "--show", "--start", "--complete", "--block"],
|
|
11
|
+
feature: ["--add", "--amend <FEAT-NNN> [--title] [--purpose] [--exit-criteria] [--section \"Heading=content\"]", "--list", "--show", "--activate", "--complete"],
|
|
12
12
|
run: [
|
|
13
13
|
"--list",
|
|
14
14
|
"--show",
|
|
@@ -583,7 +583,7 @@ const ADD_PLAN_INITIAL_STATUS = Object.freeze({
|
|
|
583
583
|
feature: ["backlog", "proposed"],
|
|
584
584
|
sprint: ["proposed"]
|
|
585
585
|
});
|
|
586
|
-
const TASK_TYPES = new Set(["task", "fix", "docs", "discovery"]);
|
|
586
|
+
const TASK_TYPES = new Set(["task", "fix", "feature", "docs", "discovery"]);
|
|
587
587
|
|
|
588
588
|
function planArtifactBody(kind, title, created) {
|
|
589
589
|
const source = `- ${created}: created via CLI (\`scrumrun plan ${kind} --add\`).`;
|
|
@@ -689,4 +689,159 @@ function addPlanArtifact(projectRoot, kind, label, options = {}) {
|
|
|
689
689
|
return withArtifactLock(scrumDir, "create", () => addPlanArtifactUnlocked(projectRoot, kind, label, options));
|
|
690
690
|
}
|
|
691
691
|
|
|
692
|
-
|
|
692
|
+
const AMENDABLE_STATUSES = Object.freeze({
|
|
693
|
+
task: new Set(["backlog", "proposed"]),
|
|
694
|
+
feature: new Set(["backlog", "proposed", "active", "paused"]),
|
|
695
|
+
sprint: new Set(["proposed"])
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
function normalizeAmendText(value, label) {
|
|
699
|
+
const text = String(value || "").trim();
|
|
700
|
+
if (!text) throw new Error(`${label} cannot be empty.`);
|
|
701
|
+
if (containsSecret(text)) throw new Error("Secret-like content is forbidden outside the local vault.");
|
|
702
|
+
return text;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function replaceTitle(body, title) {
|
|
706
|
+
const clean = normalizeAmendText(title, "Title");
|
|
707
|
+
if (/^# [^\r\n]+/m.test(body)) return body.replace(/^# [^\r\n]+/m, `# ${clean}`);
|
|
708
|
+
return `# ${clean}\n\n${body.trimStart()}`;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function sectionExpression(value) {
|
|
712
|
+
const match = String(value || "").match(/^([^=\r\n]{1,120})=([\s\S]*)$/);
|
|
713
|
+
if (!match) throw new Error('--section must use "Heading=content".');
|
|
714
|
+
const heading = match[1].trim();
|
|
715
|
+
const content = normalizeAmendText(match[2], `Section ${heading}`);
|
|
716
|
+
if (!heading || /[#\r\n]/.test(heading)) throw new Error("Section heading must be plain text without # or line breaks.");
|
|
717
|
+
return { heading, content };
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function replaceSection(body, heading, content) {
|
|
721
|
+
const cleanHeading = String(heading).trim();
|
|
722
|
+
const cleanContent = normalizeAmendText(content, `Section ${cleanHeading}`);
|
|
723
|
+
const escaped = cleanHeading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
724
|
+
const expression = new RegExp(`^## ${escaped}[ \\t]*\\r?\\n([\\s\\S]*?)(?=^## [^\\r\\n]+[ \\t]*$|\\s*$)`, "m");
|
|
725
|
+
const replacement = `## ${cleanHeading}\n\n${cleanContent}\n`;
|
|
726
|
+
if (expression.test(body)) return body.replace(expression, replacement);
|
|
727
|
+
return `${body.trimEnd()}\n\n${replacement}`;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function replaceFirstKnownSection(body, headings, content) {
|
|
731
|
+
for (const heading of headings) {
|
|
732
|
+
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
733
|
+
if (new RegExp(`^## ${escaped}[ \\t]*$`, "m").test(body)) return replaceSection(body, heading, content);
|
|
734
|
+
}
|
|
735
|
+
return replaceSection(body, headings[0], content);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function relationValue(repository, kind, value) {
|
|
739
|
+
if (value === undefined) return undefined;
|
|
740
|
+
if (value === null || value === "null" || value === "none") return null;
|
|
741
|
+
const id = String(value).trim();
|
|
742
|
+
const artifact = repository.read(kind, id);
|
|
743
|
+
if (!artifact || artifact.errors.length) throw new Error(`${kind} relation must reference an existing valid ${ARTIFACT_TYPES[kind].prefix}-NNN or null.`);
|
|
744
|
+
return id;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function setReferenceSection(body, heading, id) {
|
|
748
|
+
const exists = new RegExp(`^## ${heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[ \\t]*$`, "m").test(body);
|
|
749
|
+
if (id) return replaceSection(body, heading, `- ${id}`);
|
|
750
|
+
if (!exists) return body;
|
|
751
|
+
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
752
|
+
return body.replace(new RegExp(`^## ${escaped}[ \\t]*\\r?\\n[\\s\\S]*?(?=^## [^\\r\\n]+[ \\t]*$|\\s*$)`, "m"), "").replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function taskListBody(body, taskId, include) {
|
|
756
|
+
const heading = "Tasks";
|
|
757
|
+
const expression = /^## Tasks[ \t]*\r?\n([\s\S]*?)(?=^## [^\r\n]+[ \t]*$|\s*$)/m;
|
|
758
|
+
const current = expression.exec(body);
|
|
759
|
+
const listed = current ? [...current[1].matchAll(/^-\s+(TASK-\d{3,})\b.*$/gm)].map((match) => match[1]) : [];
|
|
760
|
+
const next = include ? [...new Set([...listed, taskId])] : listed.filter((id) => id !== taskId);
|
|
761
|
+
const content = next.length ? next.map((id) => `- ${id}`).join("\n") : "_Pending: add Task ids here._";
|
|
762
|
+
return replaceSection(body, heading, content);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function linkedTaskBody(body, taskId, include) {
|
|
766
|
+
const heading = "Linked Tasks";
|
|
767
|
+
const expression = /^## Linked Tasks[ \t]*\r?\n([\s\S]*?)(?=^## [^\r\n]+[ \t]*$|\s*$)/m;
|
|
768
|
+
const current = expression.exec(body);
|
|
769
|
+
const listed = current ? [...current[1].matchAll(/^-\s+(TASK-\d{3,})\b.*$/gm)].map((match) => match[1]) : [];
|
|
770
|
+
const next = include ? [...new Set([...listed, taskId])] : listed.filter((id) => id !== taskId);
|
|
771
|
+
const content = next.length ? next.map((id) => `- ${id}`).join("\n") : "_Pending: link Task ids here._";
|
|
772
|
+
return replaceSection(body, heading, content);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function amendPlanArtifact(projectRoot, kind, id, options = {}) {
|
|
776
|
+
if (!ADDABLE_PLAN_KINDS.has(kind)) throw new Error(`Only Task, Feature, and Sprint artifacts can be amended; ${kind} is not planning truth.`);
|
|
777
|
+
const scrumDir = path.join(projectRoot, ".scrumrun");
|
|
778
|
+
const methodFile = path.join(scrumDir, "method.json");
|
|
779
|
+
if (!fs.existsSync(methodFile) || !fs.lstatSync(methodFile).isFile()) throw new Error(`ScrumRun project not found in ${projectRoot}.`);
|
|
780
|
+
return withArtifactLock(scrumDir, `amend-${id}`, () => {
|
|
781
|
+
recoverPendingTransactions(scrumDir);
|
|
782
|
+
const repository = new ArtifactRepository(scrumDir);
|
|
783
|
+
const artifact = repository.read(kind, id);
|
|
784
|
+
if (!artifact) throw new Error(`${kind} not found: ${id}`);
|
|
785
|
+
if (artifact.errors.length) throw new Error(`Invalid ${id}: ${artifact.errors.join("; ")}`);
|
|
786
|
+
if (!AMENDABLE_STATUSES[kind].has(artifact.record.status)) {
|
|
787
|
+
throw new Error(`${id} is ${artifact.record.status}; its approved/executed scope is no longer amendable. Create a follow-up Task instead.`);
|
|
788
|
+
}
|
|
789
|
+
const hasChange = ["title", "request", "acceptance", "purpose", "exitCriteria", "timebox", "exitGate", "type", "feature", "sprint"].some((key) => options[key] !== undefined) || (options.sections && options.sections.length);
|
|
790
|
+
if (!hasChange) throw new Error("--amend requires at least one change.");
|
|
791
|
+
let record = { ...artifact.record, updated: date() };
|
|
792
|
+
let body = artifact.body;
|
|
793
|
+
if (options.title !== undefined) body = replaceTitle(body, options.title);
|
|
794
|
+
if (options.request !== undefined) body = replaceSection(body, "Request", options.request);
|
|
795
|
+
if (options.acceptance !== undefined) body = replaceSection(body, "Acceptance Criteria", options.acceptance.map((item) => `- [ ] ${normalizeAmendText(item, "Acceptance criterion")}`).join("\n"));
|
|
796
|
+
if (options.purpose !== undefined) body = replaceFirstKnownSection(body, ["Purpose", "Motivation"], options.purpose);
|
|
797
|
+
if (options.exitCriteria !== undefined) body = replaceFirstKnownSection(body, ["Exit Criteria", "Exit criteria"], options.exitCriteria.map((item) => `- [ ] ${normalizeAmendText(item, "Exit criterion")}`).join("\n"));
|
|
798
|
+
if (options.timebox !== undefined) body = replaceSection(body, "Timebox", options.timebox);
|
|
799
|
+
if (options.exitGate !== undefined) body = replaceSection(body, "Exit Gate", options.exitGate.map((item) => `- [ ] ${normalizeAmendText(item, "Exit gate")}`).join("\n"));
|
|
800
|
+
for (const rawSection of options.sections || []) {
|
|
801
|
+
const section = sectionExpression(rawSection);
|
|
802
|
+
body = replaceSection(body, section.heading, section.content);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const sideEffects = [];
|
|
806
|
+
if (kind === "task") {
|
|
807
|
+
if (options.type !== undefined) {
|
|
808
|
+
if (!TASK_TYPES.has(options.type)) throw new Error(`Invalid task --type: ${options.type}. Valid: ${[...TASK_TYPES].join(", ")}.`);
|
|
809
|
+
record.type = options.type;
|
|
810
|
+
}
|
|
811
|
+
const previousFeature = record.feature || null;
|
|
812
|
+
const previousSprint = record.sprint || null;
|
|
813
|
+
const nextFeature = relationValue(repository, "feature", options.feature);
|
|
814
|
+
const nextSprint = relationValue(repository, "sprint", options.sprint);
|
|
815
|
+
if (nextFeature !== undefined) {
|
|
816
|
+
record.feature = nextFeature;
|
|
817
|
+
body = setReferenceSection(body, "Feature", nextFeature);
|
|
818
|
+
for (const featureId of new Set([previousFeature, nextFeature].filter(Boolean))) {
|
|
819
|
+
const feature = repository.read("feature", featureId);
|
|
820
|
+
if (!feature || feature.errors.length) throw new Error(`Cannot synchronize invalid Feature relation ${featureId}; repair it before amending ${id}.`);
|
|
821
|
+
const include = featureId === nextFeature;
|
|
822
|
+
sideEffects.push({ file: feature.file, previous: fs.readFileSync(feature.file, "utf8"), next: serializeArtifact(feature.record, linkedTaskBody(feature.body, id, include)) });
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
if (nextSprint !== undefined) {
|
|
826
|
+
record.sprint = nextSprint;
|
|
827
|
+
body = setReferenceSection(body, "Sprint", nextSprint);
|
|
828
|
+
for (const sprintId of new Set([previousSprint, nextSprint].filter(Boolean))) {
|
|
829
|
+
const sprint = repository.read("sprint", sprintId);
|
|
830
|
+
if (!sprint || sprint.errors.length) throw new Error(`Cannot synchronize invalid Sprint relation ${sprintId}; repair it before amending ${id}.`);
|
|
831
|
+
const include = sprintId === nextSprint;
|
|
832
|
+
sideEffects.push({ file: sprint.file, previous: fs.readFileSync(sprint.file, "utf8"), next: serializeArtifact(sprint.record, taskListBody(sprint.body, id, include)) });
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
const next = serializeArtifact(record, body);
|
|
837
|
+
assertCanonicalWrite(projectRoot, `amend-${kind}`, [next, ...sideEffects.map((entry) => entry.next)]);
|
|
838
|
+
runKernelTransaction(scrumDir, `amend-${kind}`, [
|
|
839
|
+
{ file: artifact.file, previous: fs.readFileSync(artifact.file, "utf8"), next },
|
|
840
|
+
...sideEffects
|
|
841
|
+
]);
|
|
842
|
+
refreshState(scrumDir);
|
|
843
|
+
return repository.read(kind, id);
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
module.exports = { addPlanArtifact, amendPlanArtifact, approveRequest, finalizeRun, nextBacklogTask, nextId, refreshErrors, refreshState, renderState, retryTask, startBacklogTask, stateFingerprint, stateIsStale, transitionRun };
|
package/package.json
CHANGED
|
@@ -16,6 +16,7 @@ Natural-language product requests automatically enter the read-only ScrumRun int
|
|
|
16
16
|
After approval:
|
|
17
17
|
|
|
18
18
|
- Task is the atomic work item; define its `## Acceptance Criteria` before execution;
|
|
19
|
+
- before a Task starts, amend its intended scope through `scrumrun plan task --amend TASK-NNN` (including `--section "Heading=content"`); never hand-edit canonical planning Markdown;
|
|
19
20
|
- Sprint is only a real timebox/batch of Tasks;
|
|
20
21
|
- Run is one execution attempt and follows `executing → validating → learning → completed|failed|blocked`;
|
|
21
22
|
- a retry creates a new Run and preserves the old one;
|
|
@@ -36,7 +36,9 @@ Normal hot path:
|
|
|
36
36
|
5. follow the briefing's pointers to only the relevant canonical artifacts; go deeper only when the briefing lacks what you need (`## Where to look`, `scrumrun knowledge study "<topic>"`);
|
|
37
37
|
6. load `.scrumrun/core.md` when the method contract or an exceptional transition is needed.
|
|
38
38
|
|
|
39
|
-
**Never write Run events by hand.** Work directly in code and the linked Task Markdown after approval, then let the one final CLI checkpoint mutate `runs/RUN-NNN.md`: `scrumrun plan run --finalize RUN-NNN`. It creates the validated transition ledger, audits the full workspace delta, and records the Guardrail results together. Do not invoke `scrumrun` during normal execution. The older `--validate | --learn | --complete | --satisfy-guardrail | --authorize-mutation | --record-mutation` commands remain only for owner-requested strict mode. Recover hand-written Runs via `scrumrun plan run --normalize-legacy` — originals are preserved byte-exact under `.scrumrun/.migration-backup/runs/`.
|
|
39
|
+
**Never write Run events by hand.** Work directly in code and the linked Task Markdown after approval, then let the one final CLI checkpoint mutate `runs/RUN-NNN.md`: `scrumrun plan run --finalize RUN-NNN`. It creates the validated transition ledger, audits the full workspace delta, and records the Guardrail results together. Do not invoke `npx scrumrun@latest` during normal execution. The older `--validate | --learn | --complete | --satisfy-guardrail | --authorize-mutation | --record-mutation` commands remain only for owner-requested strict mode. Recover hand-written Runs via `scrumrun plan run --normalize-legacy` — originals are preserved byte-exact under `.scrumrun/.migration-backup/runs/`.
|
|
40
|
+
|
|
41
|
+
**Never hand-edit canonical planning Markdown.** Before a Task starts, refine it with `scrumrun plan task --amend TASK-NNN`; use title/request/acceptance/relation options or generic repeated `--section "Heading=content"`. Feature and Sprint support the same `--amend` pattern. Runs, Reviews, confirmed memory, and Guardrails are append-only evidence/policy, so record a new event or supersede them instead of rewriting them.
|
|
40
42
|
|
|
41
43
|
Lean mode is a read policy, not an incomplete store. Generated files and `.scrumrun/.cache/` are never authoritative.
|
|
42
44
|
|
|
@@ -163,9 +165,9 @@ Dry-run must not write project data. Apply requires a hashed inventory, byte-exa
|
|
|
163
165
|
|
|
164
166
|
### `scrumrun plan`
|
|
165
167
|
|
|
166
|
-
- `task`: add/list/show/run/audit/cancel/retry atomic work; use `type: fix` for fixes and `status: backlog` for parked work. `--next` surfaces the oldest backlog Task; `--start [TASK-NNN]` promotes it and creates its first Run.
|
|
167
|
-
- `sprint`: add/list/show/start/complete/block a real Task batch/timebox.
|
|
168
|
-
- `feature`: add/list/show/activate/complete long-lived initiatives.
|
|
168
|
+
- `task`: add/amend/list/show/run/audit/cancel/retry atomic work; use `--amend TASK-NNN` before it starts to refine planning truth without hand-editing Markdown. Use `type: fix` for fixes and `status: backlog` for parked work. `--next` surfaces the oldest backlog Task; `--start [TASK-NNN]` promotes it and creates its first Run.
|
|
169
|
+
- `sprint`: add/amend/list/show/start/complete/block a real Task batch/timebox.
|
|
170
|
+
- `feature`: add/amend/list/show/activate/complete long-lived initiatives.
|
|
169
171
|
- `run`: list/show/render/stats/normalize-legacy/authorize-mutation/record-mutation/satisfy-guardrail/validate/learn/complete/resume/fail/block concrete Task attempts. `--complete` accepts `--summary "…"` to store a technical summary.
|
|
170
172
|
- `intake <request>`: execute the read-only request pipeline; accepts `--type fix|task|feature|docs|discovery` and `--preview "…"`.
|
|
171
173
|
- `challenge <question>`: deep read-only analysis with evidence, risks, options, and recommendation.
|