scrumrun 3.0.1 → 3.0.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/CORE.md +3 -0
- package/README.md +14 -2
- package/SPEC.md +1 -1
- package/bin/scrumrun.js +22 -1
- package/docs/COMMANDS.md +18 -0
- package/lib/commands/manifest.js +3 -3
- package/lib/runtime/orchestrator.js +158 -3
- package/package.json +1 -1
- package/templates/project/AGENTS.md +2 -0
- package/templates/shared/skills/scrumrun/SKILL.md +7 -5
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ All notable changes follow Semantic Versioning.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 3.0.3 - 2026-08-31
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Scoped validation, not test bureaucracy.** New Tasks now state that only checks explicitly required by the owner, Acceptance Criteria, or an active Guardrail can block completion. Missing optional E2E/integration/review coverage is recorded as a follow-up or risk rather than turning a validated delivery into a failed Run. Generated agent instructions and the method contract enforce the same rule.
|
|
12
|
+
|
|
13
|
+
## 3.0.2 - 2026-08-31
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **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.
|
|
18
|
+
|
|
19
|
+
### Safety
|
|
20
|
+
|
|
21
|
+
- 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.
|
|
22
|
+
|
|
7
23
|
## 3.0.1 - 2026-08-31
|
|
8
24
|
|
|
9
25
|
### 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.
|
|
@@ -181,6 +183,7 @@ Rules:
|
|
|
181
183
|
- the Run ledger is operational history; Task synchronizes current status without copying the Run event;
|
|
182
184
|
- validation, learning, completion, failure, block, and resume require a reason or evidence;
|
|
183
185
|
- validation must match the risk and acceptance criteria;
|
|
186
|
+
- tests, reviews, and environments are completion gates only when explicitly required by the owner, the Task's Acceptance Criteria, or an active Guardrail; missing optional coverage is a follow-up/risk, never a reason by itself to fail or block an otherwise accepted Task;
|
|
184
187
|
- each Task declares its `## Acceptance Criteria` before execution; check them off as evidence, never mark done on vibes;
|
|
185
188
|
- configured reviews run before completion;
|
|
186
189
|
- every deferred policy result is persisted as a Run Guardrail obligation;
|
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.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
|
|
|
@@ -91,7 +91,19 @@ Every approved Task carries an `## Acceptance Criteria` section so "done" is def
|
|
|
91
91
|
scrumrun plan run --finalize RUN-001
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
The checkpoint validates the complete delta, policy, protected paths, secret boundary,
|
|
94
|
+
The checkpoint validates the complete delta, policy, protected paths, secret boundary, Task summary, and every applicable Guardrail before writing the Run ledger and synchronizing the Task. Failed retries remain available as separate Runs. Tests, reviews, and environments are a completion gate only when the owner, Acceptance Criteria, or an active Guardrail explicitly requires them; an optional missing E2E suite is a follow-up/risk, not a failed Task.
|
|
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.
|
|
95
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
|
|
package/SPEC.md
CHANGED
|
@@ -200,7 +200,7 @@ RECEIVED
|
|
|
200
200
|
|
|
201
201
|
Everything through `AWAITING_APPROVAL` is read-only. It may exist in process memory or ignored cache only. A valid approval token binds the normalized request, policy result, classification, risk, issuance time, canonical context fingerprint, and complete workspace fingerprint. Canonical or source drift after planning invalidates approval.
|
|
202
202
|
|
|
203
|
-
Approval atomically creates one Task and its first Run. If either write fails, neither may remain. Project changes after planning invalidate the token. Reusing a successfully consumed token is idempotent.
|
|
203
|
+
Approval atomically creates one Task and its first Run. If either write fails, neither may remain. Project changes after planning invalidate the token. Reusing a successfully consumed token is idempotent. Tests, reviews, and environments are completion gates only when explicitly required by the owner, the Task's Acceptance Criteria, or an active Guardrail; missing optional coverage is a documented follow-up/risk, not a failure by itself.
|
|
204
204
|
|
|
205
205
|
The approved Run binds the exact Guardrail-policy fingerprint and a canonical workspace baseline. Every post-approval `deferred` check becomes an append-only Guardrail obligation. In the normal session path, agents work directly and one final checkpoint verifies the complete baseline-to-final delta: policy freshness, read-only boundaries, symlink safety, scannability, newly introduced secret-like content, and evidence for every Guardrail. Missing or unverifiable evidence fails closed. Strict teams may opt into short-lived, path-scoped permits and per-edit recording; those commands add stronger intermediate control but are not required for normal execution.
|
|
206
206
|
|
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,21 @@ 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
|
+
Every new Task starts with a `## Validation Scope`: only checks explicitly required by the owner, Acceptance Criteria, or an active Guardrail block completion. Missing optional E2E, integration, or review coverage belongs in a follow-up/risk note; it must not be used to mark the Run failed.
|
|
35
|
+
|
|
36
|
+
## What can be changed
|
|
37
|
+
|
|
38
|
+
| Artifact | Canonical operation | Why |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| Task, Feature, Sprint | `--add`, `--amend`, lifecycle commands | Planning truth can be refined before execution. |
|
|
41
|
+
| Run | transitions, `--finalize`, `--retry` | Execution history is append-only; it is never amended. |
|
|
42
|
+
| Review | `--run` / `--record` | A verdict is evidence, not editable prose. Record another review if it changes. |
|
|
43
|
+
| Knowledge, Decision, Insight, Dossier | create + lifecycle commands | Preserve evidence lineage; supersede/deprecate rather than rewrite confirmed truth. |
|
|
44
|
+
| Guardrail | `--add` / `--retire` | Policy history must remain auditable. |
|
|
45
|
+
| `state.md`, `map.md`, cache | rebuild commands | Derived projections, never manually edited. |
|
|
46
|
+
|
|
29
47
|
## Knowledge
|
|
30
48
|
|
|
31
49
|
```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",
|
|
@@ -57,7 +57,7 @@ function sprintBody(title, request, created, taskId) {
|
|
|
57
57
|
function buildTaskBody(request, classification, approvalId, fingerprint, risk, previewSection, links = {}) {
|
|
58
58
|
const featureSection = links.feature ? `\n## Feature\n\n- ${links.feature}\n` : "";
|
|
59
59
|
const sprintSection = links.sprint ? `\n## Sprint\n\n- ${links.sprint}\n` : "";
|
|
60
|
-
return `# ${titleFor(request)}\n\n## Request\n\n${request}\n\n## Acceptance Criteria\n\n- [ ] _Define what "done" means before execution._\n${previewSection}${featureSection}${sprintSection}\n## Classification\n\n- Type: ${classification.type}\n- Reason: ${classification.reason}\n- Risk: ${risk.level}\n\n## Approval\n\n- Explicit approval token: ${approvalId}\n- Context fingerprint: ${fingerprint}`;
|
|
60
|
+
return `# ${titleFor(request)}\n\n## Request\n\n${request}\n\n## Acceptance Criteria\n\n- [ ] _Define what "done" means before execution._\n\n## Validation Scope\n\n- Required: validate the stated Acceptance Criteria and active Guardrails proportionately to risk.\n- Non-blocking: tests, reviews, or environments not explicitly required by the owner, Acceptance Criteria, or an active Guardrail. Record a material coverage gap as a follow-up; do not fail the Task solely because that optional check was not run.\n${previewSection}${featureSection}${sprintSection}\n## Classification\n\n- Type: ${classification.type}\n- Reason: ${classification.reason}\n- Risk: ${risk.level}\n\n## Approval\n\n- Explicit approval token: ${approvalId}\n- Context fingerprint: ${fingerprint}`;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
function stateFingerprint(repository) {
|
|
@@ -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,8 @@ 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
|
+
- validation is scoped: only a test/review/environment explicitly required by the owner, the Task's Acceptance Criteria, or an active Guardrail can block completion; a missing optional E2E suite is a documented follow-up/risk, never a reason to fail an otherwise accepted Task;
|
|
20
|
+
- 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
21
|
- Sprint is only a real timebox/batch of Tasks;
|
|
20
22
|
- Run is one execution attempt and follows `executing → validating → learning → completed|failed|blocked`;
|
|
21
23
|
- 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
|
|
|
@@ -102,7 +104,7 @@ During execution:
|
|
|
102
104
|
2. preserve existing owner work and unrelated dirty files;
|
|
103
105
|
3. define or confirm the Task's `## Acceptance Criteria` before execution and check them off as evidence;
|
|
104
106
|
4. work normally: edit code and update the Task's `## Technical Summary` and, for non-automatic rules, `## Guardrail Evidence` as evidence becomes available;
|
|
105
|
-
5. validate in proportion to risk and against the acceptance criteria;
|
|
107
|
+
5. validate in proportion to risk and against the acceptance criteria. Tests, reviews, and environments are required only when the owner, Acceptance Criteria, or an active Guardrail says so. Do not fail or block an otherwise accepted Task merely because an optional E2E/integration suite does not exist or was not run; record a material coverage gap as a follow-up/risk instead;
|
|
106
108
|
6. run configured reviewers when a Guardrail requires one;
|
|
107
109
|
7. finish once with `scrumrun plan run --finalize RUN-NNN`; it verifies every changed path, policy, secret boundary, and guardrail evidence before creating the structured Run events and completing the Task;
|
|
108
110
|
8. use path-scoped Mutation Gateway commands only when the owner explicitly requests strict execution.
|
|
@@ -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.
|