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/CHANGELOG.md +21 -0
- package/CORE.md +13 -5
- package/README.md +31 -16
- package/SPEC.md +7 -3
- package/bin/scrumrun.js +40 -7
- package/docs/COMMANDS.md +10 -8
- package/docs/ENTITY-MODEL.md +17 -14
- package/docs/QUICKSTART.md +21 -19
- package/docs/SCHEMA.md +10 -10
- package/lib/commands/manifest.js +9 -9
- package/lib/commands/render.js +3 -2
- package/lib/runtime/briefing.js +29 -0
- package/lib/runtime/orchestrator.js +4 -4
- package/lib/runtime/policy-integrity.js +83 -0
- package/lib/v2/artifacts.js +9 -0
- package/lib/v2/conformance.js +18 -16
- package/lib/v2/paths.js +2 -1
- package/lib/v2/schema.js +7 -7
- package/package.json +2 -2
- package/scripts/generate-contract-docs.js +2 -2
- package/templates/project/.scrumrun/method.json +3 -0
- package/templates/project/AGENTS.md +10 -4
- package/templates/project-lean/AGENTS.md +6 -2
- package/templates/shared/skills/scrumrun/SKILL.md +11 -7
- package/types/index.d.ts +1 -1
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,
|
|
24
|
-
| sprint | `SPRINT-NNN` | `sprints/` | `proposed` | timebox or delivery-batch membership;
|
|
25
|
-
| run | `RUN-NNN` | `runs/` | `executing` |
|
|
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`) |
|
|
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
|
|
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
|
|
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
|
|
67
|
-
- **sprint:**
|
|
68
|
-
- **run:**
|
|
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 |
|
package/lib/commands/manifest.js
CHANGED
|
@@ -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"],
|
package/lib/commands/render.js
CHANGED
|
@@ -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.
|
|
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
|
|
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.
|
package/lib/runtime/briefing.js
CHANGED
|
@@ -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##
|
|
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
|
-
"##
|
|
598
|
+
"## Done when",
|
|
599
599
|
"",
|
|
600
|
-
|
|
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 =
|
|
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 };
|
package/lib/v2/artifacts.js
CHANGED
|
@@ -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)) {
|
package/lib/v2/conformance.js
CHANGED
|
@@ -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("
|
|
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("
|
|
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", "
|
|
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
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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: "
|
|
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
|
|
124
|
-
sprint: { question: "
|
|
125
|
-
run: { question: "
|
|
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": "
|
|
4
|
-
"description": "
|
|
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
|
|
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
|
|
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,6 @@
|
|
|
1
1
|
# AGENTS.md - {{PROJECT_NAME}}
|
|
2
2
|
|
|
3
|
-
## ScrumRun
|
|
3
|
+
## ScrumRun 4.0 — execution-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
|
|
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
|
-
-
|
|
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
|
|
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
|
+
## 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.
|
|
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.
|