scrumrun 3.1.1 → 4.0.0

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/docs/SCHEMA.md CHANGED
@@ -20,9 +20,9 @@ The boundaries above are deliberately different: SPEC owns meanings, the executa
20
20
  | Kind | Stable ID | Canonical directory | Allowed initial status | Truth owned by this artifact |
21
21
  |---|---|---|---|---|
22
22
  | feature | `FEAT-NNN` | `features/` | `backlog`, `proposed` | initiative purpose, scope, dependencies, and lifecycle |
23
- | task | `TASK-NNN` | `tasks/` | `backlog`, `proposed`, `running` | scope, acceptance criteria, approval, and intended status |
24
- | sprint | `SPRINT-NNN` | `sprints/` | `proposed` | timebox or delivery-batch membership; never execution history |
25
- | run | `RUN-NNN` | `runs/` | `executing` | append-only execution events, evidence, result, and attempt number |
23
+ | task | `TASK-NNN` | `tasks/` | `backlog`, `proposed`, `in_progress`, `running` | scope, owner-defined sections, links, and status |
24
+ | sprint | `SPRINT-NNN` | `sprints/` | `proposed` | timebox or delivery-batch membership; may be feature, fix, or maintenance |
25
+ | run | `RUN-NNN` | `runs/` | `executing` | optional human-readable execution record and outcome |
26
26
  | review | `REV-NNN` | `reviews/` | `proposed` | scoped findings, checks, evidence, and verdict |
27
27
  | knowledge | `K-NNN` | `memory/knowledge/` | `candidate` | approved evidence-backed fact and validity |
28
28
  | decision | `DEC-NNN` | `memory/decisions/` | `open` | decision, rationale, validity, and lifecycle |
@@ -37,9 +37,9 @@ Every artifact also requires `id`, `kind`, `status`, `created`, `updated`, and `
37
37
  |---|---|---|---|
38
38
  | `feature` | feature (`FEAT-NNN`) | 0..1 | long-lived initiative containing the artifact |
39
39
  | `sprint` | sprint (`SPRINT-NNN`) | 0..1 | optional delivery batch containing a Task or Run |
40
- | `task` | task (`TASK-NNN`) | 1 for Run; otherwise 0..1 | atomic work executed or reviewed by the artifact |
40
+ | `task` | task (`TASK-NNN`) | 0..1 | atomic work executed or reviewed by the artifact |
41
41
 
42
- Task is the atomic unit. A Task may have zero or one Sprint. A Task may have many Runs, but every Run belongs to exactly one Task and has a monotonically increasing attempt number within that Task. Sprint membership is authoritative on `Task.sprint`; a Sprint's `## Tasks` list is a human-readable projection that must agree with it.
42
+ Task is the atomic unit. A Task may have zero or one Sprint. A Run is optional Markdown history and may reference either a Task or a Sprint; it never controls Task status in Markdown-first work. Sprint membership is authoritative on `Task.sprint`; a Sprint's `## Tasks` list is a human-readable projection that must agree with it. Additional owner-defined relations such as `depends_on: [TASK-014, DEC-018]` are preserved as local Markdown graph data.
43
43
 
44
44
  ## Scalar constraints
45
45
 
@@ -58,14 +58,14 @@ Newly authored Runs use `ledger: 1`. Their `## Events` section contains append-o
58
58
 
59
59
  Every event requires `schema`, `id`, contiguous `sequence`, RFC3339 `occurred_at`, `timestamp_precision`, `actor`, `from`, `to`, `reason`, and structured `evidence`. Event types are `transition`, `snapshot`, `guardrail`, `mutation`. Evidence kinds are `approval`, `command`, `test`, `file`, `review`, `decision`, `insight`, `risk`, `note`, `migration`, `legacy`, `guardrail`, `mutation`.
60
60
 
61
- A native ledger begins with `created → executing`; an evidenced migration `snapshot` may establish one historical baseline without inventing missing transitions. Event order, transition legality, final status, updated date, and completion evidence are machine-validated. Run owns the event history; Task stores its intended scope and synchronized current status without copying Run events.
61
+ A native ledger begins with `created → executing`; an evidenced migration `snapshot` may establish one historical baseline without inventing missing transitions. Event order, transition legality, final status, updated date, and completion evidence are machine-validated when a strict audit Run is used. Run owns optional event history; Task Markdown owns the delivered scope and status in normal work.
62
62
 
63
63
  ## Truth questions
64
64
 
65
65
  - **feature:** Why does this initiative exist?
66
- - **task:** What approved atomic outcome is intended?
67
- - **sprint:** When are related Tasks grouped?
68
- - **run:** How did one execution attempt actually happen?
66
+ - **task:** What concrete outcome is intended?
67
+ - **sprint:** Which Tasks are grouped for this delivery?
68
+ - **run:** What happened while executing a Task or Sprint?
69
69
  - **review:** What independent validation was performed?
70
70
  - **knowledge:** What verified project fact is reusable?
71
71
  - **decision:** What normative choice constrains future work?
@@ -77,7 +77,7 @@ A native ledger begins with `created → executing`; an evidenced migration `sna
77
77
  | Kind | Declared transitions |
78
78
  |---|---|
79
79
  | feature | `backlog` → `proposed`, `active`, `cancelled`<br>`proposed` → `active`, `cancelled`<br>`active` → `paused`, `completed`, `cancelled`<br>`completed` → terminal<br>`paused` → `active`, `cancelled`<br>`cancelled` → terminal |
80
- | task | `backlog` → `proposed`, `running`, `cancelled`<br>`proposed` → `running`, `cancelled`<br>`running` → `validating`, `failed`, `blocked`, `cancelled`<br>`validating` → `learning`, `failed`, `blocked`<br>`learning` → `completed`, `failed`, `blocked`<br>`partial` → `running`, `cancelled`<br>`completed` → terminal<br>`failed` → `running`, `cancelled`<br>`blocked` → `running`, `cancelled`<br>`cancelled` → terminal |
80
+ | task | `backlog` → `proposed`, `in_progress`, `running`, `cancelled`<br>`proposed` → `in_progress`, `running`, `cancelled`<br>`in_progress` → `completed`, `failed`, `blocked`, `cancelled`<br>`running` → `validating`, `completed`, `failed`, `blocked`, `cancelled`<br>`validating` → `learning`, `completed`, `failed`, `blocked`<br>`learning` → `completed`, `failed`, `blocked`<br>`partial` → `in_progress`, `running`, `cancelled`<br>`completed` → terminal<br>`failed` → `in_progress`, `running`, `cancelled`<br>`blocked` → `in_progress`, `running`, `cancelled`<br>`cancelled` → terminal |
81
81
  | sprint | `proposed` → `running`, `cancelled`<br>`running` → `partial`, `completed`, `blocked`, `cancelled`<br>`partial` → `running`, `completed`, `cancelled`<br>`completed` → terminal<br>`blocked` → `running`, `cancelled`<br>`cancelled` → terminal |
82
82
  | run | `executing` → `validating`, `failed`, `blocked`<br>`validating` → `learning`, `failed`, `blocked`<br>`learning` → `completed`, `failed`, `blocked`<br>`partial` → `executing`, `failed`, `blocked`<br>`completed` → terminal<br>`failed` → terminal<br>`blocked` → `executing`, `failed` |
83
83
  | review | `proposed` → `running`<br>`running` → `passed`, `failed`<br>`passed` → `archived`<br>`failed` → `running`, `archived`<br>`archived` → terminal |
@@ -6,7 +6,7 @@ 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]", "--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]"],
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] --strict", "--next", "--start [TASK-NNN] --strict"],
10
10
  sprint: ["--add", "--amend <SPRINT-NNN> [--title] [--timebox] [--exit-gate] [--section \"Heading=content\"]", "--list", "--show", "--start", "--complete", "--block"],
11
11
  feature: ["--add", "--amend <FEAT-NNN> [--title] [--purpose] [--exit-criteria] [--section \"Heading=content\"]", "--list", "--show", "--activate", "--complete"],
12
12
  run: [
@@ -15,16 +15,16 @@ const nouns = Object.freeze({
15
15
  "--render <RUN-NNN>",
16
16
  "--stats [--task <TASK-NNN>] [--feature <FEAT-NNN>] [--sprint <SPRINT-NNN>] [--json]",
17
17
  "--normalize-legacy [--dry-run]",
18
- "--finalize <RUN-NNN> [--summary \"technical recap\"] [--note]",
18
+ "--finalize <RUN-NNN> --strict [--summary \"technical recap\"] [--note]",
19
19
  "--authorize-mutation <RUN-NNN> --path <relative-path>",
20
20
  "--record-mutation <RUN-NNN> --permit <MUT-id> [--note] [--actor]",
21
21
  "--satisfy-guardrail <RUN-NNN> --guardrail <GR-NNN> [--note] [--evidence] [--review] [--migration] [--actor]",
22
- "--validate [--note] [--evidence] [--command] [--test] [--file] [--review] [--actor] [--at]",
23
- "--learn [--note] [--evidence] [--decision] [--insight] [--file] [--actor] [--at]",
24
- "--complete [--note] [--evidence] [--review] [--test] [--file] [--actor] [--at] [--summary \"technical recap for future tasks\"]",
25
- "--resume [--note] [--evidence] [--risk] [--actor] [--at]",
26
- "--fail [--note] [--evidence] [--risk] [--test] [--actor] [--at]",
27
- "--block [--note] [--evidence] [--risk] [--actor] [--at]"
22
+ "--validate --strict [--note] [--evidence] [--command] [--test] [--file] [--review] [--actor] [--at]",
23
+ "--learn --strict [--note] [--evidence] [--decision] [--insight] [--file] [--actor] [--at]",
24
+ "--complete --strict [--note] [--evidence] [--review] [--test] [--file] [--actor] [--at] [--summary \"technical recap for future tasks\"]",
25
+ "--resume --strict [--note] [--evidence] [--risk] [--actor] [--at]",
26
+ "--fail --strict [--note] [--evidence] [--risk] [--test] [--actor] [--at]",
27
+ "--block --strict [--note] [--evidence] [--risk] [--actor] [--at]"
28
28
  ],
29
29
  intake: ["<request>", "--request", "--approve", "--plain", "--json", "--type <fix|task|feature|docs|discovery>", "--preview \"technical summary\""],
30
30
  challenge: ["<question>"]
@@ -65,7 +65,7 @@ const nouns = Object.freeze({
65
65
  subjects: {
66
66
  project: ["--show", "--language", "--interaction", "--approval", "--quick-tasks"],
67
67
  init: ["--local", "--shared", "--lean", "--no-agent-hint", "--force"],
68
- update: ["all [--migrate]", "codex [--migrate]", "opencode [--migrate]", "claude [--migrate]"],
68
+ update: ["all [--project] [--seal-policy] [--migrate]", "codex [--project] [--seal-policy] [--migrate]", "opencode [--project] [--seal-policy] [--migrate]", "claude [--project] [--seal-policy] [--migrate]"],
69
69
  migrate: ["--to 2 --dry-run", "--to 2 --apply", "--to 2 --rollback"],
70
70
  doctor: ["all [--strict] [--recover] [--dry-run]", "codex [--strict] [--recover] [--dry-run]", "opencode [--strict] [--recover] [--dry-run]", "claude [--strict] [--recover] [--dry-run]"],
71
71
  uninstall: ["--force"],
@@ -29,15 +29,16 @@ ${grammarLines().join("\n")}
29
29
 
30
30
  - Natural-language product work begins with a read-only understanding pass; do not invoke \`plan intake\` merely to satisfy procedure.
31
31
  - Intake, contextualization, policy, risk, classification, and planning are read-only until explicit approval.
32
- - After approval, work directly in source files and relevant \`.scrumrun/\` Markdown. Create/refine a Task, acceptance criteria, technical summary, and follow-ups directly. Feature/Sprint/Run are useful only when they add context.
32
+ - After approval, work directly in source files and relevant \`.scrumrun/\` Markdown until the approved Task is delivered. Keep executing discover → implement → verify → fix → verify; a progress report is allowed only when the owner asks for it and never ends the workflow. Do not answer with an inventory, partial progress report, decomposition, or remaining-work list; those are internal steps. A gap discovered in scope is work to implement now, not a “next step”. Feature/Sprint/Run are useful only when they add context.
33
33
  - A Run is optional audit/handoff context, never a prerequisite for starting, amending, or completing a Task. Do not block on a missing Run, a legacy status, or stale generated state.
34
34
  - The CLI can inspect/repair/report structured artifacts, but it does not own the daily workflow.
35
35
  - \`guardrails.md\` is canonical project policy; \`golden-rules.md\` is v1 compatibility only.
36
36
  - Evaluate active Guardrails as \`passed\`, \`blocked\`, or \`deferred\`; cite exact \`GR-NNN\` ids and keep deferred execution gates visible.
37
- - Block only for an explicit Guardrail, secret/security risk, destructive action without approval, or an unmet required Acceptance Criterion. Optional unrun E2E/review coverage is a follow-up/risk, not a failed Run.
37
+ - Block only for an explicit Guardrail, secret/security risk, destructive action without approval, or an unmet required delivery criterion. \`Follow-ups\` may only contain work outside the approved \`Done when\` contract; optional unrun E2E/review coverage is a follow-up/risk, not a failed Run.
38
38
  - Use the CLI only for \`init\`, \`update --project\`, \`migrate\`, \`repair\`, \`doctor\`, reports, or release checks. Do not invoke \`npx scrumrun@latest\` during execution.
39
39
  - Never invoke \`plan run --fail|--block|--retry|--finalize|--complete|--validate\` or \`plan task --start\` during normal work. These optional strict-audit commands must not decide a Task outcome.
40
40
  - Strict per-path Mutation Gateway permits and ledger finalization remain available only when the owner explicitly requests strict execution.
41
+ - Never edit \`core.md\` or \`guardrails.md\` during product work. They are sealed policy; owner-requested policy changes are reviewed and sealed at the maintenance edge.
41
42
  - Knowledge/Decision/Insight records require evidence; AI-proposed Insights remain \`candidate\` until confirmed.
42
43
  - Never print vault values or write before approval.
43
44
  - Unknown nouns, subjects, actions, ids, or ambiguous approval must produce a deterministic explanation, never a guessed mutation.
@@ -29,6 +29,31 @@ function generateBriefing(scrumDir, repository) {
29
29
  }))
30
30
  .slice(0, 5);
31
31
 
32
+ // Blocked-by-dependency: Tasks with `depends_on: [ID, ...]` in frontmatter
33
+ // where at least one target artifact is missing or not terminal. This is a
34
+ // visibility signal, not a hard gate — the agent reads it and decides.
35
+ const allById = new Map();
36
+ for (const kind of Object.keys(snapshot.records)) {
37
+ for (const r of snapshot.records[kind] || []) {
38
+ if (r.id) allById.set(r.id, r);
39
+ }
40
+ }
41
+ const blockedLines = [];
42
+ for (const task of snapshot.records.task || []) {
43
+ if (!task.id || TERMINAL.has(task.status)) continue;
44
+ const deps = Array.isArray(task.depends_on) ? task.depends_on : [];
45
+ const blocking = [];
46
+ for (const depId of deps) {
47
+ if (typeof depId !== "string") continue;
48
+ const dep = allById.get(depId);
49
+ if (!dep) blocking.push(`${depId} (missing)`);
50
+ else if (!TERMINAL.has(dep.status)) blocking.push(`${depId} (${dep.status})`);
51
+ }
52
+ if (blocking.length) {
53
+ blockedLines.push(`- ${task.id} waits on ${blocking.join(", ")} — ${task.title || task.id}`);
54
+ }
55
+ }
56
+
32
57
  const completedRuns = (snapshot.records.run || [])
33
58
  .filter((r) => r.status === "completed")
34
59
  .slice(-5)
@@ -76,6 +101,10 @@ Progressive disclosure: read this first; go deeper only if the briefing lacks wh
76
101
 
77
102
  ${activeWork.length ? activeWork.join("\n") : "- No active canonical work."}
78
103
 
104
+ ## Blocked by dependency
105
+
106
+ ${blockedLines.length ? blockedLines.join("\n") : "- None. All active Tasks have their `depends_on` targets terminal (or none declared)."}
107
+
79
108
  ## Recent
80
109
 
81
110
  ${recentLines.length ? recentLines.join("\n") : "- No completed Runs."}
@@ -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\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}`;
60
+ return `# ${titleFor(request)}\n\n## Request\n\n${request}\n\n## Done when\n\n- [ ] _State the smallest observable delivery contract before execution._\n\n## Validation Scope\n\n- Required: validate the stated Done when contract and active Guardrails proportionately to risk.\n- Non-blocking: tests, reviews, or environments not explicitly required by the owner, Done when contract, or an active Guardrail. Record a material coverage gap as a follow-up only when it is outside the approved contract; do not fail the Task solely because that optional check was not run.\n\n## Completion\n\n- _Fill this once, after the delivery contract is satisfied._\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) {
@@ -595,9 +595,9 @@ function planArtifactBody(kind, title, created) {
595
595
  "",
596
596
  title,
597
597
  "",
598
- "## Acceptance Criteria",
598
+ "## Done when",
599
599
  "",
600
- '- [ ] _Define what "done" means before execution._',
600
+ "- [ ] _State the smallest observable delivery contract before execution._",
601
601
  "",
602
602
  "## Source",
603
603
  "",
@@ -792,7 +792,7 @@ function amendPlanArtifact(projectRoot, kind, id, options = {}) {
792
792
  let body = artifact.body;
793
793
  if (options.title !== undefined) body = replaceTitle(body, options.title);
794
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"));
795
+ if (options.acceptance !== undefined) body = replaceFirstKnownSection(body, ["Done when", "Acceptance Criteria"], options.acceptance.map((item) => `- [ ] ${normalizeAmendText(item, "Delivery criterion")}`).join("\n"));
796
796
  if (options.purpose !== undefined) body = replaceFirstKnownSection(body, ["Purpose", "Motivation"], options.purpose);
797
797
  if (options.exitCriteria !== undefined) body = replaceFirstKnownSection(body, ["Exit Criteria", "Exit criteria"], options.exitCriteria.map((item) => `- [ ] ${normalizeAmendText(item, "Exit criterion")}`).join("\n"));
798
798
  if (options.timebox !== undefined) body = replaceSection(body, "Timebox", options.timebox);
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+
3
+ // Daily work is intentionally Markdown-first. This module keeps the
4
+ // enforcement at the boundary: project policy is pinned when ScrumRun is
5
+ // initialized/updated and `doctor --strict` / release audits detect drift.
6
+
7
+ const crypto = require("node:crypto");
8
+ const fs = require("node:fs");
9
+ const path = require("node:path");
10
+
11
+ function sha256(value) {
12
+ return crypto.createHash("sha256").update(value).digest("hex");
13
+ }
14
+
15
+ function readRegular(file) {
16
+ if (!fs.existsSync(file) || !fs.lstatSync(file).isFile() || fs.lstatSync(file).isSymbolicLink()) return null;
17
+ return fs.readFileSync(file, "utf8");
18
+ }
19
+
20
+ function expectedCoreFingerprint(packageRoot = path.resolve(__dirname, "../..")) {
21
+ const core = readRegular(path.join(packageRoot, "CORE.md"));
22
+ return core === null ? null : sha256(core);
23
+ }
24
+
25
+ function readPolicyIntegrity(scrumDir) {
26
+ const marker = path.join(scrumDir, "method.json");
27
+ const raw = readRegular(marker);
28
+ if (raw === null) return { marker: null, integrity: null, error: "method.json is missing or unsafe" };
29
+ try {
30
+ const parsed = JSON.parse(raw);
31
+ return { marker: parsed, integrity: parsed.integrity || null, error: null };
32
+ } catch (error) {
33
+ return { marker: null, integrity: null, error: error.message };
34
+ }
35
+ }
36
+
37
+ function sealPolicyIntegrity(scrumDir, { includeGuardrails = false, coreFingerprint = expectedCoreFingerprint() } = {}) {
38
+ const state = readPolicyIntegrity(scrumDir);
39
+ if (state.error) throw new Error(`Cannot seal ScrumRun policy: ${state.error}`);
40
+ const core = readRegular(path.join(scrumDir, "core.md"));
41
+ if (core === null) throw new Error("Cannot seal ScrumRun policy: core.md is missing or unsafe.");
42
+ const guardrails = readRegular(path.join(scrumDir, "guardrails.md"));
43
+ if (guardrails === null) throw new Error("Cannot seal ScrumRun policy: guardrails.md is missing or unsafe.");
44
+ const next = {
45
+ ...state.marker,
46
+ workflow: { ...(state.marker.workflow || {}), daily: "markdown-first" },
47
+ integrity: {
48
+ ...(state.integrity || {}),
49
+ schema: 1,
50
+ core_sha256: sha256(core),
51
+ core_package_sha256: coreFingerprint || sha256(core),
52
+ ...(includeGuardrails || !state.integrity || !state.integrity.guardrails_sha256
53
+ ? { guardrails_sha256: sha256(guardrails) }
54
+ : {})
55
+ }
56
+ };
57
+ return `${JSON.stringify(next, null, 2)}\n`;
58
+ }
59
+
60
+ function auditPolicyIntegrity(scrumDir, { coreFingerprint = expectedCoreFingerprint() } = {}) {
61
+ const state = readPolicyIntegrity(scrumDir);
62
+ if (state.error) return [{ severity: "critical", code: "POLICY_MARKER", message: state.error }];
63
+ const integrity = state.integrity;
64
+ if (!integrity || integrity.schema !== 1) {
65
+ return [{ severity: "warning", code: "POLICY_INTEGRITY_UNSEALED", message: "Policy fingerprints are not sealed. Run `scrumrun update --project --seal-policy` after owner review." }];
66
+ }
67
+ const findings = [];
68
+ const core = readRegular(path.join(scrumDir, "core.md"));
69
+ const guardrails = readRegular(path.join(scrumDir, "guardrails.md"));
70
+ if (core === null) findings.push({ severity: "critical", code: "CORE_UNSAFE", message: "core.md is missing or unsafe." });
71
+ else {
72
+ const actual = sha256(core);
73
+ if (actual !== integrity.core_sha256) findings.push({ severity: "high", code: "CORE_TAMPERED", message: "core.md differs from the owner-sealed policy. Restore it with `scrumrun update --project`." });
74
+ if (coreFingerprint && actual !== coreFingerprint) findings.push({ severity: "high", code: "CORE_PACKAGE_DRIFT", message: "core.md differs from the installed ScrumRun Core. Review then run `scrumrun update --project`." });
75
+ }
76
+ if (guardrails === null) findings.push({ severity: "critical", code: "GUARDRAILS_UNSAFE", message: "guardrails.md is missing or unsafe." });
77
+ else if (sha256(guardrails) !== integrity.guardrails_sha256) {
78
+ findings.push({ severity: "high", code: "GUARDRAILS_TAMPERED", message: "guardrails.md changed after owner sealing. Review it, then explicitly run `scrumrun update --project --seal-policy`." });
79
+ }
80
+ return findings;
81
+ }
82
+
83
+ module.exports = { auditPolicyIntegrity, expectedCoreFingerprint, sealPolicyIntegrity, sha256 };
@@ -17,6 +17,7 @@ function sha256(content) {
17
17
 
18
18
  function scalar(value) {
19
19
  if (value === null || value === undefined) return "null";
20
+ if (Array.isArray(value)) return `[${value.map((item) => scalar(item)).join(", ")}]`;
20
21
  if (typeof value === "boolean" || typeof value === "number") return String(value);
21
22
  const text = String(value);
22
23
  if (/^[A-Za-z0-9._/-]+$/.test(text)) return text;
@@ -36,6 +37,11 @@ function parseScalar(value) {
36
37
  return text;
37
38
  }
38
39
  }
40
+ if (text.startsWith("[") && text.endsWith("]")) {
41
+ const inner = text.slice(1, -1).trim();
42
+ if (!inner) return [];
43
+ return inner.split(",").map((item) => parseScalar(item));
44
+ }
39
45
  return text;
40
46
  }
41
47
 
@@ -102,6 +108,9 @@ function validateArtifact(record, expectedKind = null) {
102
108
  errors.push(`${record.kind}.${field} must reference ${target.prefix}-NNN`);
103
109
  }
104
110
  }
111
+ if (record.kind === "run" && (record.task === undefined || record.task === null) && (record.sprint === undefined || record.sprint === null)) {
112
+ errors.push("run must reference TASK-NNN or SPRINT-NNN");
113
+ }
105
114
  for (const [field, constraint] of Object.entries(SCALAR_FIELDS)) {
106
115
  if (!constraint.kinds.includes(record.kind)) continue;
107
116
  if (constraint.required && (record[field] === undefined || record[field] === null)) {
@@ -14,6 +14,7 @@ const { pendingTransactionStatus } = require("./transaction");
14
14
  const { configWeakeningAttempts, validateGuardrailDocument } = require("../runtime/policy-engine");
15
15
  const { auditActiveWorkspace } = require("../runtime/mutation-gateway");
16
16
  const { canonicalPaths, PATHS_SCHEMA_VERSION } = require("./paths");
17
+ const { auditPolicyIntegrity } = require("../runtime/policy-integrity");
17
18
 
18
19
  const INVARIANTS = Object.freeze([
19
20
  { id: "I-01", summary: "pre-approval work is read-only", tests: ["intake builds bounded context without writing"] },
@@ -79,10 +80,10 @@ function auditProject(projectRoot) {
79
80
  if (methodMarker.method !== METHOD_VERSION) findings.push(finding("critical", "METHOD_VERSION", `method.json must declare ${METHOD_VERSION}.`, marker));
80
81
  const expectedPaths = canonicalPaths();
81
82
  if (!methodMarker.paths || typeof methodMarker.paths !== "object" || Array.isArray(methodMarker.paths)) {
82
- findings.push(finding("high", "METHOD_PATHS_MISSING", `method.json must declare a canonical "paths" block so agents navigate by declaration, not by search. Run \`scrumrun update --migrate\` to backfill.`, marker));
83
+ findings.push(finding("warning", "METHOD_PATHS_MISSING", `method.json is missing a canonical "paths" block. Non-blocking in Markdown-first daily work; run \`scrumrun update --migrate\` if you want the declared index.`, marker));
83
84
  } else {
84
85
  const drift = diffPathIndex(expectedPaths, methodMarker.paths);
85
- for (const entry of drift) findings.push(finding("high", "METHOD_PATHS_DRIFT", `method.json paths[${entry.label}] is ${entry.actual === undefined ? "missing" : `"${entry.actual}"`}; expected "${entry.expected}". Regenerate via \`update --migrate\`.`, marker));
86
+ for (const entry of drift) findings.push(finding("warning", "METHOD_PATHS_DRIFT", `method.json paths[${entry.label}] is ${entry.actual === undefined ? "missing" : `"${entry.actual}"`}; expected "${entry.expected}". Non-blocking; regenerate via \`update --migrate\` when convenient.`, marker));
86
87
  if (methodMarker.paths_schema !== PATHS_SCHEMA_VERSION) {
87
88
  findings.push(finding("warning", "METHOD_PATHS_SCHEMA", `method.json paths_schema is ${methodMarker.paths_schema || "missing"}; expected ${PATHS_SCHEMA_VERSION}.`, marker));
88
89
  }
@@ -94,6 +95,7 @@ function auditProject(projectRoot) {
94
95
  const file = path.join(scrumDir, relative);
95
96
  if (!fs.existsSync(file) || !fs.lstatSync(file).isFile()) findings.push(finding("high", "CANONICAL_MISSING", `${relative} is missing or unsafe.`, file));
96
97
  }
98
+ for (const item of auditPolicyIntegrity(scrumDir)) findings.push(finding(item.severity, item.code, item.message, marker));
97
99
  const guardrailsFile = path.join(scrumDir, "guardrails.md");
98
100
  const guardrails = fs.existsSync(guardrailsFile) && fs.lstatSync(guardrailsFile).isFile() ? fs.readFileSync(guardrailsFile, "utf8") : "";
99
101
  const guardrailValidation = validateGuardrailDocument(guardrails);
@@ -141,9 +143,9 @@ function auditProject(projectRoot) {
141
143
  task = null;
142
144
  }
143
145
  }
144
- if (run.record && !run.record.task) {
145
- findings.push(finding("high", "RUN_TASK_MISSING", `${run.record.id} has no task reference (record.task is null).`, run.file));
146
- } else if (run.record && (!task || task.errors.length)) {
146
+ if (run.record && !run.record.task && !run.record.sprint) {
147
+ findings.push(finding("high", "RUN_TARGET_MISSING", `${run.record.id} must reference a Task or Sprint.`, run.file));
148
+ } else if (run.record && run.record.task && (!task || task.errors.length)) {
147
149
  findings.push(finding("high", "RUN_TASK_MISSING", `${run.record.id} references missing or invalid ${run.record.task}.`, run.file));
148
150
  }
149
151
  if (run.record && task && !task.errors.length && (run.record.sprint || null) !== (task.record.sprint || null)) {
@@ -170,6 +172,7 @@ function auditProject(projectRoot) {
170
172
  }
171
173
  }
172
174
  const byId = new Map(Object.values(records).flat().filter((artifact) => artifact.record).map((artifact) => [artifact.record.id, artifact]));
175
+ const markdownFirst = methodMarker && methodMarker.workflow && methodMarker.workflow.daily === "markdown-first";
173
176
  for (const task of records.task || []) {
174
177
  for (const [field, prefix] of [["feature", "FEAT"], ["sprint", "SPRINT"]]) {
175
178
  const target = task.record && task.record[field];
@@ -182,7 +185,7 @@ function auditProject(projectRoot) {
182
185
  }
183
186
  const attemptsByTask = new Map();
184
187
  for (const run of records.run || []) {
185
- if (!run.record || run.errors.length) continue;
188
+ if (!run.record || run.errors.length || !run.record.task) continue;
186
189
  if (!attemptsByTask.has(run.record.task)) attemptsByTask.set(run.record.task, []);
187
190
  attemptsByTask.get(run.record.task).push(run.record.attempt);
188
191
  }
@@ -199,11 +202,14 @@ function auditProject(projectRoot) {
199
202
  .filter((run) => run.record && !run.errors.length && run.record.task === task.record.id)
200
203
  .sort((left, right) => right.record.attempt - left.record.attempt);
201
204
  if (!attempts.length) {
205
+ if (markdownFirst) continue;
202
206
  if (["running", "validating", "learning", "partial"].includes(task.record.status)) {
203
207
  findings.push(finding(
204
- "high",
208
+ markdownFirst ? "warning" : "high",
205
209
  "TASK_ORPHANED",
206
- `${task.record.id}.status is ${task.record.status}, but no canonical Run references it. Review with \`scrumrun repair --recover-orphan-tasks\`, then apply the explicit recovery and start the Task to create its first Run.`,
210
+ markdownFirst
211
+ ? `${task.record.id}.status is ${task.record.status} without a Run. This is valid Markdown-first work; keep the Task handoff current.`
212
+ : `${task.record.id}.status is ${task.record.status}, but no canonical Run references it. Review with \`scrumrun repair --recover-orphan-tasks\`, then apply the explicit recovery and start the Task to create its first Run.`,
207
213
  task.file
208
214
  ));
209
215
  }
@@ -219,17 +225,13 @@ function auditProject(projectRoot) {
219
225
  blocked: "blocked",
220
226
  partial: "partial"
221
227
  };
222
- if (taskStatuses[latest.status] && task.record.status !== taskStatuses[latest.status]) {
228
+ if (!markdownFirst && taskStatuses[latest.status] && task.record.status !== taskStatuses[latest.status]) {
223
229
  findings.push(finding("high", "TASK_RUN_STATUS_MISMATCH", `${task.record.id}.status ${task.record.status} disagrees with latest ${latest.id}.status ${latest.status}.`, task.file));
224
230
  }
225
231
  }
226
- for (const task of records.task || []) {
227
- if (!task.record || task.errors.length) continue;
228
- if (["backlog", "proposed", "cancelled"].includes(task.record.status)) continue;
229
- if (!/^## Acceptance Criteria\b/m.test(task.body || "")) {
230
- findings.push(finding("warning", "ACCEPTANCE_CRITERIA_MISSING", `${task.record.id} has no Acceptance Criteria section; define what "done" means before execution.`, task.file));
231
- }
232
- }
232
+ // Task bodies are intentionally extensible Markdown. Project Guardrails may
233
+ // require sections for selected Tasks; the kernel never requires one global
234
+ // heading or rejects unknown owner-defined structure.
233
235
  for (const sprint of records.sprint || []) {
234
236
  if (!sprint.record || sprint.errors.length) continue;
235
237
  const heading = /^## Tasks[ \t]*$/m.exec(sprint.body);
package/lib/v2/paths.js CHANGED
@@ -56,10 +56,11 @@ function flattenPaths(paths = CANONICAL_PATHS, prefix = "") {
56
56
  return out;
57
57
  }
58
58
 
59
- function renderMethodJson({ methodVersion, layout = "v2", schemas = {}, migratedFrom, migration } = {}) {
59
+ function renderMethodJson({ methodVersion, layout = "v2", schemas = {}, workflow = { daily: "markdown-first" }, migratedFrom, migration } = {}) {
60
60
  const payload = {
61
61
  method: methodVersion,
62
62
  layout,
63
+ workflow,
63
64
  paths_schema: PATHS_SCHEMA_VERSION,
64
65
  paths: canonicalPaths(),
65
66
  schemas
package/lib/v2/schema.js CHANGED
@@ -38,8 +38,8 @@ const ARTIFACT_TYPES = deepFreeze({
38
38
  task: {
39
39
  prefix: "TASK",
40
40
  directory: "tasks",
41
- initial: ["backlog", "proposed", "running"],
42
- statuses: ["backlog", "proposed", "running", "validating", "learning", "partial", "completed", "failed", "blocked", "cancelled"]
41
+ initial: ["backlog", "proposed", "in_progress", "running"],
42
+ statuses: ["backlog", "proposed", "in_progress", "running", "validating", "learning", "partial", "completed", "failed", "blocked", "cancelled"]
43
43
  },
44
44
  sprint: {
45
45
  prefix: "SPRINT",
@@ -90,7 +90,7 @@ const ARTIFACT_TRANSITIONS = deepFreeze({
90
90
  backlog: ["proposed", "active", "cancelled"], proposed: ["active", "cancelled"], active: ["paused", "completed", "cancelled"], paused: ["active", "cancelled"]
91
91
  },
92
92
  task: {
93
- backlog: ["proposed", "running", "cancelled"], proposed: ["running", "cancelled"], running: ["validating", "failed", "blocked", "cancelled"], validating: ["learning", "failed", "blocked"], learning: ["completed", "failed", "blocked"], partial: ["running", "cancelled"], failed: ["running", "cancelled"], blocked: ["running", "cancelled"]
93
+ backlog: ["proposed", "in_progress", "running", "cancelled"], proposed: ["in_progress", "running", "cancelled"], in_progress: ["completed", "failed", "blocked", "cancelled"], running: ["validating", "completed", "failed", "blocked", "cancelled"], validating: ["learning", "completed", "failed", "blocked"], learning: ["completed", "failed", "blocked"], partial: ["in_progress", "running", "cancelled"], failed: ["in_progress", "running", "cancelled"], blocked: ["in_progress", "running", "cancelled"]
94
94
  },
95
95
  sprint: {
96
96
  proposed: ["running", "cancelled"], running: ["partial", "completed", "blocked", "cancelled"], partial: ["running", "completed", "cancelled"], blocked: ["running", "cancelled"]
@@ -108,7 +108,7 @@ const ARTIFACT_TRANSITIONS = deepFreeze({
108
108
  const STRUCTURAL_RELATIONS = deepFreeze({
109
109
  feature: { targetKind: "feature", cardinality: "0..1", meaning: "long-lived initiative containing the artifact" },
110
110
  sprint: { targetKind: "sprint", cardinality: "0..1", meaning: "optional delivery batch containing a Task or Run" },
111
- task: { targetKind: "task", cardinality: "1 for Run; otherwise 0..1", requiredFor: ["run"], meaning: "atomic work executed or reviewed by the artifact" }
111
+ task: { targetKind: "task", cardinality: "0..1", meaning: "atomic work executed or reviewed by the artifact" }
112
112
  });
113
113
 
114
114
  const SCALAR_FIELDS = deepFreeze({
@@ -120,9 +120,9 @@ const SCALAR_FIELDS = deepFreeze({
120
120
 
121
121
  const TRUTH_OWNERSHIP = deepFreeze({
122
122
  feature: { question: "Why does this initiative exist?", truth: "initiative purpose, scope, dependencies, and lifecycle" },
123
- task: { question: "What approved atomic outcome is intended?", truth: "scope, acceptance criteria, approval, and intended status" },
124
- sprint: { question: "When are related Tasks grouped?", truth: "timebox or delivery-batch membership; never execution history" },
125
- run: { question: "How did one execution attempt actually happen?", truth: "append-only execution events, evidence, result, and attempt number" },
123
+ task: { question: "What concrete outcome is intended?", truth: "scope, owner-defined sections, links, and status" },
124
+ sprint: { question: "Which Tasks are grouped for this delivery?", truth: "timebox or delivery-batch membership; may be feature, fix, or maintenance" },
125
+ run: { question: "What happened while executing a Task or Sprint?", truth: "optional human-readable execution record and outcome" },
126
126
  review: { question: "What independent validation was performed?", truth: "scoped findings, checks, evidence, and verdict" },
127
127
  knowledge: { question: "What verified project fact is reusable?", truth: "approved evidence-backed fact and validity" },
128
128
  decision: { question: "What normative choice constrains future work?", truth: "decision, rationale, validity, and lifecycle" },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "scrumrun",
3
- "version": "3.1.1",
4
- "description": "Evidence-driven Agile runtime and semantic project memory for AI coding agents.",
3
+ "version": "4.0.0",
4
+ "description": "Markdown-first Agile memory and guardrails for AI coding agents.",
5
5
  "bin": {
6
6
  "scrumrun": "bin/scrumrun.js",
7
7
  "sr-claude": "bin/claude-install.js"
@@ -69,7 +69,7 @@ Every artifact also requires \`id\`, \`kind\`, \`status\`, \`created\`, \`update
69
69
  |---|---|---|---|
70
70
  ${relationRows.join("\n")}
71
71
 
72
- Task is the atomic unit. A Task may have zero or one Sprint. A Task may have many Runs, but every Run belongs to exactly one Task and has a monotonically increasing attempt number within that Task. Sprint membership is authoritative on \`Task.sprint\`; a Sprint's \`## Tasks\` list is a human-readable projection that must agree with it.
72
+ Task is the atomic unit. A Task may have zero or one Sprint. A Run is optional Markdown history and may reference either a Task or a Sprint; it never controls Task status in Markdown-first work. Sprint membership is authoritative on \`Task.sprint\`; a Sprint's \`## Tasks\` list is a human-readable projection that must agree with it. Additional owner-defined relations such as \`depends_on: [TASK-014, DEC-018]\` are preserved as local Markdown graph data.
73
73
 
74
74
  ## Scalar constraints
75
75
 
@@ -85,7 +85,7 @@ Newly authored Runs use \`ledger: ${RUN_LEDGER_VERSION}\`. Their \`## Events\` s
85
85
 
86
86
  Every event requires \`schema\`, \`id\`, contiguous \`sequence\`, RFC3339 \`occurred_at\`, \`timestamp_precision\`, \`actor\`, \`from\`, \`to\`, \`reason\`, and structured \`evidence\`. Event types are ${RUN_EVENT_TYPES.map((value) => `\`${value}\``).join(", ")}. Evidence kinds are ${RUN_EVIDENCE_KINDS.map((value) => `\`${value}\``).join(", ")}.
87
87
 
88
- A native ledger begins with \`created → executing\`; an evidenced migration \`snapshot\` may establish one historical baseline without inventing missing transitions. Event order, transition legality, final status, updated date, and completion evidence are machine-validated. Run owns the event history; Task stores its intended scope and synchronized current status without copying Run events.
88
+ A native ledger begins with \`created → executing\`; an evidenced migration \`snapshot\` may establish one historical baseline without inventing missing transitions. Event order, transition legality, final status, updated date, and completion evidence are machine-validated when a strict audit Run is used. Run owns optional event history; Task Markdown owns the delivered scope and status in normal work.
89
89
 
90
90
  ## Truth questions
91
91
 
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "method": "2.0.0",
3
3
  "layout": "v2",
4
+ "workflow": {
5
+ "daily": "markdown-first"
6
+ },
4
7
  "paths_schema": 1,
5
8
  "paths": {
6
9
  "guardrails": "guardrails.md",
@@ -1,6 +1,6 @@
1
1
  # AGENTS.md - {{PROJECT_NAME}}
2
2
 
3
- ## ScrumRun 3.1Markdown-first
3
+ ## ScrumRun 4.0execution-first Markdown
4
4
 
5
5
  This project uses ScrumRun. The method is mandatory; the direct CLI is `scrumrun <noun> <subject> <action>`. `/sc` is only an optional client shortcut.
6
6
 
@@ -15,18 +15,24 @@ Natural-language product requests begin with a read-only understanding pass. Bef
15
15
 
16
16
  After approval:
17
17
 
18
- - Task is the atomic work item; create or refine its Markdown directly, define its `## Acceptance Criteria`, and retain a short `## Technical Summary` / `## Follow-ups` handoff;
18
+ - execute continuously until the approved Task is delivered: keep working through discover → implement → verify → fix → verify. A progress report is allowed only when the owner asks for status and must be followed immediately by more execution; it never closes the workflow. Do not stop to send an inventory, a progress report, a decomposition, or a list of work still to do. A missing implementation found during the work remains work to do now, not a follow-up, a “next step”, or a reason to reply. Stop only for an owner decision, external access, an explicit Guardrail, a security/secret risk, destructive work without approval, or an unmet required Acceptance Criterion;
19
+ - Task is the atomic work item; create or refine its Markdown directly, define a short `## Done when` delivery contract, and retain a short `## Completion` / `## Follow-ups` handoff;
20
+ - you may decompose implementation privately or add linked child Tasks when needed, but do not make the owner manage that decomposition and do not stop after planning it;
19
21
  - 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
22
  - Feature and Sprint remain useful organization, but are optional; create them only when they clarify real initiative or timebox context;
21
23
  - Sprint is only a real timebox/batch of Tasks;
22
24
  - Run is an optional audit/handoff record, never an administrative prerequisite to start, amend, or complete a Task; preserve useful prior attempts but do not let missing/invalid Run metadata stop work;
23
- - record a `## Technical Summary` at completion so the next agent inherits what was done; record optional missing coverage in `## Follow-ups`;
25
+ - only claim a condition validated when the check actually covers that condition; a narrow checker never proves a broad delivery claim. Record a `## Completion` at completion so the next agent inherits what was done; `## Follow-ups` may contain only work outside the approved `## Done when`, never unfinished acceptance work;
24
26
  - work directly in code and Task Markdown after approval; do not call `npx scrumrun@latest` or normal `scrumrun plan/run` commands during execution;
25
27
  - use the CLI only for `init`, `update --project`, `migrate`, `repair`, `doctor`, reports, or release checks. It audits/repairs the folder; it does not own the daily workflow;
26
28
  - never invoke `plan run --fail`, `--block`, `--retry`, `--finalize`, `--complete`, `--validate`, or `plan task --start` in normal work. A failed legacy Run due to administrative state remains historical; write the corrected delivery outcome directly in the Task instead;
27
- - learning proposes evidence-backed Knowledge, Decisions, or candidate Insights when the work reveals reusable context;
29
+ - learning proposes evidence-backed Knowledge, Decisions, or candidate Insights after delivery, or only when it materially helps the current implementation; it never interrupts execution;
28
30
  - guardrails remain mandatory: stop only for an explicit Guardrail, security/secret risk, destructive action without approval, or an unmet required Acceptance Criterion. Status vocabulary, missing Runs, unavailable optional tests, and stale generated state are warnings to reconcile, not blockers.
29
31
 
32
+ `core.md` and `guardrails.md` are sealed policy. Never edit either during a product Task. A policy change requires an explicit owner request; after review, seal it at the maintenance edge with `scrumrun update --project --seal-policy`.
33
+
34
+ Artifacts are local Markdown connected by stable IDs and relative links. Preserve any owner-defined frontmatter and sections in a Task. A Guardrail may require sections such as `## Migration Plan`, `## Rollback`, or `## Guardrail Evidence`; add them only to the affected Task.
35
+
30
36
  Never bypass guardrails, overwrite owner work, treat generated state/cache as truth, auto-confirm AI knowledge, auto-migrate a v1 project, or print vault values.
31
37
 
32
38
  Never use `npx scrumrun@latest` in the normal work loop. If the installed CLI is unavailable, stop and report that blocker rather than substituting a network command.
@@ -1,6 +1,6 @@
1
1
  # AGENTS.md - {{PROJECT_NAME}}
2
2
 
3
- ## ScrumRun 3.1 lean read policy — Markdown-first
3
+ ## ScrumRun 4.0 lean read policy — execution-first Markdown
4
4
 
5
5
  This project stores the complete ScrumRun v2 truth but uses a bounded default read path:
6
6
 
@@ -11,12 +11,16 @@ This project stores the complete ScrumRun v2 truth but uses a bounded default re
11
11
 
12
12
  Do not scan every Task, Run, Sprint, Feature, or Memory file by default. Generated `state.md`, `map.md`, and `.cache/` guide retrieval but never override canonical Markdown.
13
13
 
14
- Natural-language product work begins as a read-only understanding pass. Explicit approval authorizes direct work in source files and `.scrumrun/` Markdown. Define the Task's `## Acceptance Criteria` before execution and record a `## Technical Summary` at completion. A Run is optional handoff/audit context, not a state machine that may prevent starting, amending, or completing work. A Sprint exists only for a real batch/timebox.
14
+ Natural-language product work begins as a read-only understanding pass. Explicit approval authorizes direct work in source files and `.scrumrun/` Markdown. Execute continuously until the approved Task is delivered: keep working through discover → implement → verify → fix → verify. A progress report is allowed only when the owner asks for status and must be followed immediately by more execution; it never closes the workflow. Do not stop to provide an inventory, a partial progress report, or a list of remaining work. A missing implementation found during the work remains work to do now, not a follow-up or “next step”. Define a short `## Done when` delivery contract, record `## Completion` only at the end, and never move unfinished contract work to `## Follow-ups` without explicit owner approval. A Run is optional handoff/audit context, not a state machine that may prevent starting, amending, or completing work. A Sprint exists only for a real batch/timebox.
15
15
 
16
16
  Guardrails are mandatory, but administrative state is not: block only for an explicit Guardrail, security/secret risk, destructive action without approval, or an unmet required Acceptance Criterion. Missing Runs, invalid legacy status vocabulary, stale generated views, and optional unrun tests are warnings to reconcile in Markdown. Record meaningful optional coverage gaps under `## Follow-ups`; they do not fail a delivered Task.
17
17
 
18
18
  `.scrumrun/guardrails.md` is canonical policy. Never bypass it, overwrite owner work, auto-confirm AI knowledge, auto-migrate v1 state, or print vault values.
19
19
 
20
+ `core.md` and `guardrails.md` are sealed policy. Never edit them during a product Task. A policy change requires an explicit owner request and `scrumrun update --project --seal-policy` after review.
21
+
22
+ Artifacts are local Markdown connected by stable IDs and relative links. Preserve owner-defined frontmatter and sections in each Task; Guardrails may require a section only on affected Tasks.
23
+
20
24
  Use the installed CLI only for `init`, `update --project`, `migrate`, `repair`, `doctor`, reports, and release checks. For daily product work, follow `.scrumrun/core.md` and edit the relevant Markdown directly.
21
25
 
22
26
  Never invoke `plan run --fail`, `--block`, `--retry`, `--finalize`, `--complete`, `--validate`, or `plan task --start` during normal work. If an old Run says failed for an administrative reason, leave it as history and record the actual delivered outcome in the Task's Technical Summary and Follow-ups.