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 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.1` · **Method target:** `2.0.0` · **Runtime:** Node.js `>=22.13.0` · **License:** MIT
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
@@ -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
- module.exports = { addPlanArtifact, approveRequest, finalizeRun, nextBacklogTask, nextId, refreshErrors, refreshState, renderState, retryTask, startBacklogTask, stateFingerprint, stateIsStale, transitionRun };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scrumrun",
3
- "version": "3.0.1",
3
+ "version": "3.0.2",
4
4
  "description": "Evidence-driven Agile runtime and semantic project memory for AI coding agents.",
5
5
  "bin": {
6
6
  "scrumrun": "bin/scrumrun.js",
@@ -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.