scrumrun 3.0.0 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/CORE.md +14 -12
- package/README.md +53 -31
- package/SPEC.md +4 -4
- package/bin/scrumrun.js +59 -35
- package/docs/COMMANDS.md +51 -35
- package/docs/DEMO.md +6 -6
- package/docs/ERROR-CODES.md +11 -11
- package/docs/QUICKSTART.md +14 -14
- package/docs/RELEASE-SCORECARD.md +1 -1
- package/docs/RELEASE.md +1 -1
- package/docs/SEMANTIC-MEMORY.md +3 -3
- package/docs/TROUBLESHOOTING.md +9 -9
- package/lib/commands/manifest.js +3 -3
- package/lib/commands/normalize-legacy.js +1 -1
- package/lib/commands/pretty-intake.js +2 -2
- package/lib/commands/render.js +8 -5
- package/lib/commands/repair.js +61 -8
- package/lib/errors.js +11 -11
- package/lib/runtime/briefing.js +2 -2
- package/lib/runtime/orchestrator.js +158 -3
- package/lib/v2/conformance.js +13 -3
- package/package.json +1 -1
- package/templates/project/AGENTS.md +5 -4
- package/templates/shared/skills/scrumrun/SKILL.md +28 -26
package/lib/errors.js
CHANGED
|
@@ -31,44 +31,44 @@ const CATALOG = Object.freeze({
|
|
|
31
31
|
"SR-E-052": { summary: "Approval token expired.", remediation: "Re-run intake to obtain a fresh token before approving." },
|
|
32
32
|
|
|
33
33
|
// Runs and ledger
|
|
34
|
-
"SR-E-100": { summary: "Run not found.", remediation: "Check the RUN-NNN id with `
|
|
35
|
-
"SR-E-101": { summary: "Run ledger contains an invalid event.", remediation: "Inspect the reported event id; use `
|
|
34
|
+
"SR-E-100": { summary: "Run not found.", remediation: "Check the RUN-NNN id with `scrumrun plan run --list`." },
|
|
35
|
+
"SR-E-101": { summary: "Run ledger contains an invalid event.", remediation: "Inspect the reported event id; use `scrumrun plan run --render` for a human view or restore from git history." },
|
|
36
36
|
"SR-E-102": { summary: "Run transition rejected: missing validation or learning evidence.", remediation: "Complete `--validate` and `--learn` with typed evidence before `--complete`." },
|
|
37
37
|
"SR-E-103": { summary: "Retry rejected because the previous Run is still active.", remediation: "Complete, fail, or block the current Run before creating a retry." },
|
|
38
38
|
|
|
39
39
|
// Guardrails
|
|
40
40
|
"SR-E-150": { summary: "Guardrail check blocked the operation.", remediation: "Read the reported GR-NNN, satisfy or retire it explicitly; guardrails never bypass silently." },
|
|
41
|
-
"SR-E-151": { summary: "Guardrail obligation is still pending.", remediation: "Resolve each `pending guardrail` via `
|
|
41
|
+
"SR-E-151": { summary: "Guardrail obligation is still pending.", remediation: "Resolve each `pending guardrail` via `scrumrun plan run --satisfy-guardrail` before completing the Run." },
|
|
42
42
|
"SR-E-152": { summary: "Guardrail declaration is malformed.", remediation: "Every active guardrail requires Status, Enforcement, Scope, and Rule fields; check .scrumrun/guardrails.md." },
|
|
43
43
|
|
|
44
44
|
// Edit permits (Mutation Gateway)
|
|
45
|
-
"SR-E-200": { summary: "No edit permit for this path.", remediation: "Request one with `
|
|
45
|
+
"SR-E-200": { summary: "No edit permit for this path.", remediation: "Request one with `scrumrun plan run --authorize-mutation RUN-NNN --path <path>` before editing canonical or source files." },
|
|
46
46
|
"SR-E-201": { summary: "Edit permit expired.", remediation: "Permits last 15 minutes. Authorize a new one and record the change immediately." },
|
|
47
47
|
"SR-E-202": { summary: "Edit permit path scope mismatch.", remediation: "The permit does not cover the modified path. Request a new permit that lists it." },
|
|
48
48
|
"SR-E-203": { summary: "File hash changed unexpectedly since the permit was issued.", remediation: "Someone else modified the file. Re-plan the change and request a fresh permit." },
|
|
49
49
|
|
|
50
50
|
// Transactions and recovery
|
|
51
|
-
"SR-E-250": { summary: "Pending kernel transaction cannot be recovered automatically.", remediation: "Run `
|
|
51
|
+
"SR-E-250": { summary: "Pending kernel transaction cannot be recovered automatically.", remediation: "Run `scrumrun config doctor --recover --dry-run` to preview; if it shows 'would overwrite owner changes', reconcile the file manually before applying." },
|
|
52
52
|
"SR-E-251": { summary: "Journal fails integrity check.", remediation: "Inspect .scrumrun/.transactions/pending. Do not delete; contact support or restore from backup." },
|
|
53
53
|
|
|
54
54
|
// Memory
|
|
55
|
-
"SR-E-300": { summary: "Memory candidate rejected: missing resolvable evidence.", remediation: "Attach at least one --evidence path or `
|
|
55
|
+
"SR-E-300": { summary: "Memory candidate rejected: missing resolvable evidence.", remediation: "Attach at least one --evidence path or `scrumrun knowledge <subject> --propose` before `--confirm`." },
|
|
56
56
|
"SR-E-301": { summary: "Attempt to write into vault via canonical channel.", remediation: "vault.local.md is local-only and never indexed. Edit the file directly." },
|
|
57
57
|
|
|
58
58
|
// Semantic index / code intel
|
|
59
|
-
"SR-E-350": { summary: "Semantic index is stale.", remediation: "Rebuild with `
|
|
60
|
-
"SR-E-351": { summary: "Search backend advertised in the cache does not match this runtime.", remediation: "Delete .scrumrun/.cache/semantic-index.sqlite and re-run any `
|
|
59
|
+
"SR-E-350": { summary: "Semantic index is stale.", remediation: "Rebuild with `scrumrun knowledge map --build`. Cache is disposable; canonical memory is unaffected." },
|
|
60
|
+
"SR-E-351": { summary: "Search backend advertised in the cache does not match this runtime.", remediation: "Delete .scrumrun/.cache/semantic-index.sqlite and re-run any `scrumrun knowledge` query to rebuild against the current runtime." },
|
|
61
61
|
|
|
62
62
|
// Migration
|
|
63
|
-
"SR-E-400": { summary: "Migration preflight failed.", remediation: "Run `
|
|
63
|
+
"SR-E-400": { summary: "Migration preflight failed.", remediation: "Run `scrumrun update` (dry) to see blockers, resolve them, then apply with `--migrate`." },
|
|
64
64
|
"SR-E-401": { summary: "Migration rollback requested but no backup was found.", remediation: "Rollback needs the ignored byte-exact backup created during --migrate. Restore from version control if the backup is gone." },
|
|
65
65
|
|
|
66
66
|
// Conformance / doctor
|
|
67
67
|
"SR-E-450": { summary: "Conformance check failed.", remediation: "The reported invariant identifies the exact violation; the message includes the file and expected shape." },
|
|
68
|
-
"SR-E-451": { summary: "Installed client asset is stale.", remediation: "Re-run `
|
|
68
|
+
"SR-E-451": { summary: "Installed client asset is stale.", remediation: "Re-run `scrumrun update` for the specific client. `doctor --strict` shows which files diverge." },
|
|
69
69
|
|
|
70
70
|
// Configuration / install
|
|
71
|
-
"SR-E-500": { summary: "ScrumRun project not initialized.", remediation: "Run `
|
|
71
|
+
"SR-E-500": { summary: "ScrumRun project not initialized.", remediation: "Run `scrumrun init` in the repository root." },
|
|
72
72
|
"SR-E-501": { summary: "Unsupported Node.js runtime.", remediation: "ScrumRun requires Node.js >=22.13.0 for native SQLite. Upgrade Node and retry." }
|
|
73
73
|
});
|
|
74
74
|
|
package/lib/runtime/briefing.js
CHANGED
|
@@ -97,8 +97,8 @@ ${backlogTasks.length ? backlogTasks.join("\n") : "- No backlog Tasks."}
|
|
|
97
97
|
- Guardrails: .scrumrun/guardrails.md
|
|
98
98
|
- Tasks: .scrumrun/tasks/ (${counts.task || 0} total)
|
|
99
99
|
- Runs: .scrumrun/runs/ (${counts.run || 0} total)
|
|
100
|
-
- Semantic:
|
|
101
|
-
- Full state: rebuild with
|
|
100
|
+
- Semantic: scrumrun knowledge study "<topic>" for deep queries
|
|
101
|
+
- Full state: rebuild with scrumrun config doctor --recover
|
|
102
102
|
`;
|
|
103
103
|
|
|
104
104
|
return { content, sourceFingerprint, watchFingerprint: watch.fingerprint, sourceFiles: watch.files };
|
|
@@ -583,10 +583,10 @@ const ADD_PLAN_INITIAL_STATUS = Object.freeze({
|
|
|
583
583
|
feature: ["backlog", "proposed"],
|
|
584
584
|
sprint: ["proposed"]
|
|
585
585
|
});
|
|
586
|
-
const TASK_TYPES = new Set(["task", "fix", "docs", "discovery"]);
|
|
586
|
+
const TASK_TYPES = new Set(["task", "fix", "feature", "docs", "discovery"]);
|
|
587
587
|
|
|
588
588
|
function planArtifactBody(kind, title, created) {
|
|
589
|
-
const source = `- ${created}: created via CLI (\`
|
|
589
|
+
const source = `- ${created}: created via CLI (\`scrumrun plan ${kind} --add\`).`;
|
|
590
590
|
if (kind === "task") {
|
|
591
591
|
return [
|
|
592
592
|
`# ${title}`,
|
|
@@ -689,4 +689,159 @@ function addPlanArtifact(projectRoot, kind, label, options = {}) {
|
|
|
689
689
|
return withArtifactLock(scrumDir, "create", () => addPlanArtifactUnlocked(projectRoot, kind, label, options));
|
|
690
690
|
}
|
|
691
691
|
|
|
692
|
-
|
|
692
|
+
const AMENDABLE_STATUSES = Object.freeze({
|
|
693
|
+
task: new Set(["backlog", "proposed"]),
|
|
694
|
+
feature: new Set(["backlog", "proposed", "active", "paused"]),
|
|
695
|
+
sprint: new Set(["proposed"])
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
function normalizeAmendText(value, label) {
|
|
699
|
+
const text = String(value || "").trim();
|
|
700
|
+
if (!text) throw new Error(`${label} cannot be empty.`);
|
|
701
|
+
if (containsSecret(text)) throw new Error("Secret-like content is forbidden outside the local vault.");
|
|
702
|
+
return text;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function replaceTitle(body, title) {
|
|
706
|
+
const clean = normalizeAmendText(title, "Title");
|
|
707
|
+
if (/^# [^\r\n]+/m.test(body)) return body.replace(/^# [^\r\n]+/m, `# ${clean}`);
|
|
708
|
+
return `# ${clean}\n\n${body.trimStart()}`;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function sectionExpression(value) {
|
|
712
|
+
const match = String(value || "").match(/^([^=\r\n]{1,120})=([\s\S]*)$/);
|
|
713
|
+
if (!match) throw new Error('--section must use "Heading=content".');
|
|
714
|
+
const heading = match[1].trim();
|
|
715
|
+
const content = normalizeAmendText(match[2], `Section ${heading}`);
|
|
716
|
+
if (!heading || /[#\r\n]/.test(heading)) throw new Error("Section heading must be plain text without # or line breaks.");
|
|
717
|
+
return { heading, content };
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function replaceSection(body, heading, content) {
|
|
721
|
+
const cleanHeading = String(heading).trim();
|
|
722
|
+
const cleanContent = normalizeAmendText(content, `Section ${cleanHeading}`);
|
|
723
|
+
const escaped = cleanHeading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
724
|
+
const expression = new RegExp(`^## ${escaped}[ \\t]*\\r?\\n([\\s\\S]*?)(?=^## [^\\r\\n]+[ \\t]*$|\\s*$)`, "m");
|
|
725
|
+
const replacement = `## ${cleanHeading}\n\n${cleanContent}\n`;
|
|
726
|
+
if (expression.test(body)) return body.replace(expression, replacement);
|
|
727
|
+
return `${body.trimEnd()}\n\n${replacement}`;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function replaceFirstKnownSection(body, headings, content) {
|
|
731
|
+
for (const heading of headings) {
|
|
732
|
+
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
733
|
+
if (new RegExp(`^## ${escaped}[ \\t]*$`, "m").test(body)) return replaceSection(body, heading, content);
|
|
734
|
+
}
|
|
735
|
+
return replaceSection(body, headings[0], content);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function relationValue(repository, kind, value) {
|
|
739
|
+
if (value === undefined) return undefined;
|
|
740
|
+
if (value === null || value === "null" || value === "none") return null;
|
|
741
|
+
const id = String(value).trim();
|
|
742
|
+
const artifact = repository.read(kind, id);
|
|
743
|
+
if (!artifact || artifact.errors.length) throw new Error(`${kind} relation must reference an existing valid ${ARTIFACT_TYPES[kind].prefix}-NNN or null.`);
|
|
744
|
+
return id;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function setReferenceSection(body, heading, id) {
|
|
748
|
+
const exists = new RegExp(`^## ${heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[ \\t]*$`, "m").test(body);
|
|
749
|
+
if (id) return replaceSection(body, heading, `- ${id}`);
|
|
750
|
+
if (!exists) return body;
|
|
751
|
+
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
752
|
+
return body.replace(new RegExp(`^## ${escaped}[ \\t]*\\r?\\n[\\s\\S]*?(?=^## [^\\r\\n]+[ \\t]*$|\\s*$)`, "m"), "").replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function taskListBody(body, taskId, include) {
|
|
756
|
+
const heading = "Tasks";
|
|
757
|
+
const expression = /^## Tasks[ \t]*\r?\n([\s\S]*?)(?=^## [^\r\n]+[ \t]*$|\s*$)/m;
|
|
758
|
+
const current = expression.exec(body);
|
|
759
|
+
const listed = current ? [...current[1].matchAll(/^-\s+(TASK-\d{3,})\b.*$/gm)].map((match) => match[1]) : [];
|
|
760
|
+
const next = include ? [...new Set([...listed, taskId])] : listed.filter((id) => id !== taskId);
|
|
761
|
+
const content = next.length ? next.map((id) => `- ${id}`).join("\n") : "_Pending: add Task ids here._";
|
|
762
|
+
return replaceSection(body, heading, content);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function linkedTaskBody(body, taskId, include) {
|
|
766
|
+
const heading = "Linked Tasks";
|
|
767
|
+
const expression = /^## Linked Tasks[ \t]*\r?\n([\s\S]*?)(?=^## [^\r\n]+[ \t]*$|\s*$)/m;
|
|
768
|
+
const current = expression.exec(body);
|
|
769
|
+
const listed = current ? [...current[1].matchAll(/^-\s+(TASK-\d{3,})\b.*$/gm)].map((match) => match[1]) : [];
|
|
770
|
+
const next = include ? [...new Set([...listed, taskId])] : listed.filter((id) => id !== taskId);
|
|
771
|
+
const content = next.length ? next.map((id) => `- ${id}`).join("\n") : "_Pending: link Task ids here._";
|
|
772
|
+
return replaceSection(body, heading, content);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function amendPlanArtifact(projectRoot, kind, id, options = {}) {
|
|
776
|
+
if (!ADDABLE_PLAN_KINDS.has(kind)) throw new Error(`Only Task, Feature, and Sprint artifacts can be amended; ${kind} is not planning truth.`);
|
|
777
|
+
const scrumDir = path.join(projectRoot, ".scrumrun");
|
|
778
|
+
const methodFile = path.join(scrumDir, "method.json");
|
|
779
|
+
if (!fs.existsSync(methodFile) || !fs.lstatSync(methodFile).isFile()) throw new Error(`ScrumRun project not found in ${projectRoot}.`);
|
|
780
|
+
return withArtifactLock(scrumDir, `amend-${id}`, () => {
|
|
781
|
+
recoverPendingTransactions(scrumDir);
|
|
782
|
+
const repository = new ArtifactRepository(scrumDir);
|
|
783
|
+
const artifact = repository.read(kind, id);
|
|
784
|
+
if (!artifact) throw new Error(`${kind} not found: ${id}`);
|
|
785
|
+
if (artifact.errors.length) throw new Error(`Invalid ${id}: ${artifact.errors.join("; ")}`);
|
|
786
|
+
if (!AMENDABLE_STATUSES[kind].has(artifact.record.status)) {
|
|
787
|
+
throw new Error(`${id} is ${artifact.record.status}; its approved/executed scope is no longer amendable. Create a follow-up Task instead.`);
|
|
788
|
+
}
|
|
789
|
+
const hasChange = ["title", "request", "acceptance", "purpose", "exitCriteria", "timebox", "exitGate", "type", "feature", "sprint"].some((key) => options[key] !== undefined) || (options.sections && options.sections.length);
|
|
790
|
+
if (!hasChange) throw new Error("--amend requires at least one change.");
|
|
791
|
+
let record = { ...artifact.record, updated: date() };
|
|
792
|
+
let body = artifact.body;
|
|
793
|
+
if (options.title !== undefined) body = replaceTitle(body, options.title);
|
|
794
|
+
if (options.request !== undefined) body = replaceSection(body, "Request", options.request);
|
|
795
|
+
if (options.acceptance !== undefined) body = replaceSection(body, "Acceptance Criteria", options.acceptance.map((item) => `- [ ] ${normalizeAmendText(item, "Acceptance criterion")}`).join("\n"));
|
|
796
|
+
if (options.purpose !== undefined) body = replaceFirstKnownSection(body, ["Purpose", "Motivation"], options.purpose);
|
|
797
|
+
if (options.exitCriteria !== undefined) body = replaceFirstKnownSection(body, ["Exit Criteria", "Exit criteria"], options.exitCriteria.map((item) => `- [ ] ${normalizeAmendText(item, "Exit criterion")}`).join("\n"));
|
|
798
|
+
if (options.timebox !== undefined) body = replaceSection(body, "Timebox", options.timebox);
|
|
799
|
+
if (options.exitGate !== undefined) body = replaceSection(body, "Exit Gate", options.exitGate.map((item) => `- [ ] ${normalizeAmendText(item, "Exit gate")}`).join("\n"));
|
|
800
|
+
for (const rawSection of options.sections || []) {
|
|
801
|
+
const section = sectionExpression(rawSection);
|
|
802
|
+
body = replaceSection(body, section.heading, section.content);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const sideEffects = [];
|
|
806
|
+
if (kind === "task") {
|
|
807
|
+
if (options.type !== undefined) {
|
|
808
|
+
if (!TASK_TYPES.has(options.type)) throw new Error(`Invalid task --type: ${options.type}. Valid: ${[...TASK_TYPES].join(", ")}.`);
|
|
809
|
+
record.type = options.type;
|
|
810
|
+
}
|
|
811
|
+
const previousFeature = record.feature || null;
|
|
812
|
+
const previousSprint = record.sprint || null;
|
|
813
|
+
const nextFeature = relationValue(repository, "feature", options.feature);
|
|
814
|
+
const nextSprint = relationValue(repository, "sprint", options.sprint);
|
|
815
|
+
if (nextFeature !== undefined) {
|
|
816
|
+
record.feature = nextFeature;
|
|
817
|
+
body = setReferenceSection(body, "Feature", nextFeature);
|
|
818
|
+
for (const featureId of new Set([previousFeature, nextFeature].filter(Boolean))) {
|
|
819
|
+
const feature = repository.read("feature", featureId);
|
|
820
|
+
if (!feature || feature.errors.length) throw new Error(`Cannot synchronize invalid Feature relation ${featureId}; repair it before amending ${id}.`);
|
|
821
|
+
const include = featureId === nextFeature;
|
|
822
|
+
sideEffects.push({ file: feature.file, previous: fs.readFileSync(feature.file, "utf8"), next: serializeArtifact(feature.record, linkedTaskBody(feature.body, id, include)) });
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
if (nextSprint !== undefined) {
|
|
826
|
+
record.sprint = nextSprint;
|
|
827
|
+
body = setReferenceSection(body, "Sprint", nextSprint);
|
|
828
|
+
for (const sprintId of new Set([previousSprint, nextSprint].filter(Boolean))) {
|
|
829
|
+
const sprint = repository.read("sprint", sprintId);
|
|
830
|
+
if (!sprint || sprint.errors.length) throw new Error(`Cannot synchronize invalid Sprint relation ${sprintId}; repair it before amending ${id}.`);
|
|
831
|
+
const include = sprintId === nextSprint;
|
|
832
|
+
sideEffects.push({ file: sprint.file, previous: fs.readFileSync(sprint.file, "utf8"), next: serializeArtifact(sprint.record, taskListBody(sprint.body, id, include)) });
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
const next = serializeArtifact(record, body);
|
|
837
|
+
assertCanonicalWrite(projectRoot, `amend-${kind}`, [next, ...sideEffects.map((entry) => entry.next)]);
|
|
838
|
+
runKernelTransaction(scrumDir, `amend-${kind}`, [
|
|
839
|
+
{ file: artifact.file, previous: fs.readFileSync(artifact.file, "utf8"), next },
|
|
840
|
+
...sideEffects
|
|
841
|
+
]);
|
|
842
|
+
refreshState(scrumDir);
|
|
843
|
+
return repository.read(kind, id);
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
module.exports = { addPlanArtifact, amendPlanArtifact, approveRequest, finalizeRun, nextBacklogTask, nextId, refreshErrors, refreshState, renderState, retryTask, startBacklogTask, stateFingerprint, stateIsStale, transitionRun };
|
package/lib/v2/conformance.js
CHANGED
|
@@ -79,7 +79,7 @@ function auditProject(projectRoot) {
|
|
|
79
79
|
if (methodMarker.method !== METHOD_VERSION) findings.push(finding("critical", "METHOD_VERSION", `method.json must declare ${METHOD_VERSION}.`, marker));
|
|
80
80
|
const expectedPaths = canonicalPaths();
|
|
81
81
|
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 \`
|
|
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
83
|
} else {
|
|
84
84
|
const drift = diffPathIndex(expectedPaths, methodMarker.paths);
|
|
85
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));
|
|
@@ -152,7 +152,7 @@ function auditProject(projectRoot) {
|
|
|
152
152
|
if (run.record && run.record.ledger === RUN_LEDGER_VERSION) {
|
|
153
153
|
const ledger = validateRunLedger(run.record, run.body);
|
|
154
154
|
if (ledger.errors.length) {
|
|
155
|
-
findings.push(finding("high", "RUN_WRITE_BYPASS", `${run.record.id}: ledger has ${ledger.errors.length} validation error(s); this Run was likely written without going through the CLI. Recover with \`
|
|
155
|
+
findings.push(finding("high", "RUN_WRITE_BYPASS", `${run.record.id}: ledger has ${ledger.errors.length} validation error(s); this Run was likely written without going through the CLI. Recover with \`scrumrun plan run --normalize-legacy --dry-run\` then re-run without --dry-run.`, run.file));
|
|
156
156
|
for (const error of ledger.errors) findings.push(finding("high", "RUN_LEDGER_INVALID", `${run.record.id}: ${error}`, run.file));
|
|
157
157
|
}
|
|
158
158
|
} else if (run.record) {
|
|
@@ -198,7 +198,17 @@ function auditProject(projectRoot) {
|
|
|
198
198
|
const attempts = (records.run || [])
|
|
199
199
|
.filter((run) => run.record && !run.errors.length && run.record.task === task.record.id)
|
|
200
200
|
.sort((left, right) => right.record.attempt - left.record.attempt);
|
|
201
|
-
if (!attempts.length)
|
|
201
|
+
if (!attempts.length) {
|
|
202
|
+
if (["running", "validating", "learning", "partial"].includes(task.record.status)) {
|
|
203
|
+
findings.push(finding(
|
|
204
|
+
"high",
|
|
205
|
+
"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.`,
|
|
207
|
+
task.file
|
|
208
|
+
));
|
|
209
|
+
}
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
202
212
|
const latest = attempts[0].record;
|
|
203
213
|
const taskStatuses = {
|
|
204
214
|
executing: "running",
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## ScrumRun 3.0
|
|
4
4
|
|
|
5
|
-
This project uses ScrumRun. The method is mandatory; `/sc` is
|
|
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
|
|
|
7
7
|
For normal work, read:
|
|
8
8
|
|
|
@@ -16,16 +16,17 @@ Natural-language product requests automatically enter the read-only ScrumRun int
|
|
|
16
16
|
After approval:
|
|
17
17
|
|
|
18
18
|
- Task is the atomic work item; define its `## Acceptance Criteria` before execution;
|
|
19
|
+
- before a Task starts, amend its intended scope through `scrumrun plan task --amend TASK-NNN` (including `--section "Heading=content"`); never hand-edit canonical planning Markdown;
|
|
19
20
|
- Sprint is only a real timebox/batch of Tasks;
|
|
20
21
|
- Run is one execution attempt and follows `executing → validating → learning → completed|failed|blocked`;
|
|
21
22
|
- a retry creates a new Run and preserves the old one;
|
|
22
23
|
- record a `## Technical Summary` at completion so the next agent inherits what was done;
|
|
23
24
|
- work directly in code and the linked Task Markdown after approval; do not call `npx scrumrun@latest` during execution;
|
|
24
|
-
- record one `## Guardrail Evidence` line per non-automatic guardrail in the Task, then run `scrumrun
|
|
25
|
-
- when a Run completes and work remains queued, surface it with `
|
|
25
|
+
- record one `## Guardrail Evidence` line per non-automatic guardrail in the Task, then run `scrumrun plan run --finalize RUN-NNN` once to validate and close the Run;
|
|
26
|
+
- when a Run completes and work remains queued, surface it with `scrumrun plan task --next` and start it with `scrumrun plan task --start` — starting is explicit approval;
|
|
26
27
|
- learning proposes evidence-backed Knowledge, Decisions, or candidate Insights;
|
|
27
28
|
- the final checkpoint verifies all Guardrails, workspace changes, protected paths, and secret boundaries before completion; use the path-scoped Mutation Gateway only when the owner requests strict execution.
|
|
28
29
|
|
|
29
30
|
Never bypass guardrails or edit around the Mutation Gateway, overwrite owner work, treat generated state/cache as truth, auto-confirm AI knowledge, auto-migrate a v1 project, or print vault values.
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
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.
|
|
@@ -5,13 +5,13 @@ description: Use when initializing or migrating ScrumRun, handling product reque
|
|
|
5
5
|
|
|
6
6
|
# ScrumRun 2.0
|
|
7
7
|
|
|
8
|
-
ScrumRun is an evidence-driven Agile runtime for AI agents. Its canonical command is:
|
|
8
|
+
ScrumRun is an evidence-driven Agile runtime for AI agents. Its canonical shell command is:
|
|
9
9
|
|
|
10
10
|
```text
|
|
11
|
-
|
|
11
|
+
scrumrun <noun> <subject> <action> [args]
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
The five nouns are `plan`, `knowledge`, `rules`, `review`, and `config`. If the command is incomplete, show only the valid next tokens and do not guess.
|
|
14
|
+
The five nouns are `plan`, `knowledge`, `rules`, `review`, and `config`. `/sc` is an optional AI-client shortcut; `scrumrun sc ...` is a compatibility alias. If the command is incomplete, show only the valid next tokens and do not guess.
|
|
15
15
|
|
|
16
16
|
When a project contains `.scrumrun/`, use ScrumRun for product work even if the user did not type `/sc`. Natural-language work begins as `plan intake`.
|
|
17
17
|
|
|
@@ -33,10 +33,12 @@ Normal hot path:
|
|
|
33
33
|
2. read `AGENTS.md`;
|
|
34
34
|
3. read `.scrumrun/guardrails.md`;
|
|
35
35
|
4. read `.scrumrun/state.md` — the **briefing**: active work, recent completions with their technical summaries, open decisions, active memory, backlog queue, and pointers;
|
|
36
|
-
5. follow the briefing's pointers to only the relevant canonical artifacts; go deeper only when the briefing lacks what you need (`## Where to look`, `
|
|
36
|
+
5. follow the briefing's pointers to only the relevant canonical artifacts; go deeper only when the briefing lacks what you need (`## Where to look`, `scrumrun knowledge study "<topic>"`);
|
|
37
37
|
6. load `.scrumrun/core.md` when the method contract or an exceptional transition is needed.
|
|
38
38
|
|
|
39
|
-
**Never write Run events by hand.** Work directly in code and the linked Task Markdown after approval, then let the one final CLI checkpoint mutate `runs/RUN-NNN.md`: `scrumrun
|
|
39
|
+
**Never write Run events by hand.** Work directly in code and the linked Task Markdown after approval, then let the one final CLI checkpoint mutate `runs/RUN-NNN.md`: `scrumrun plan run --finalize RUN-NNN`. It creates the validated transition ledger, audits the full workspace delta, and records the Guardrail results together. Do not invoke `npx scrumrun@latest` during normal execution. The older `--validate | --learn | --complete | --satisfy-guardrail | --authorize-mutation | --record-mutation` commands remain only for owner-requested strict mode. Recover hand-written Runs via `scrumrun plan run --normalize-legacy` — originals are preserved byte-exact under `.scrumrun/.migration-backup/runs/`.
|
|
40
|
+
|
|
41
|
+
**Never hand-edit canonical planning Markdown.** Before a Task starts, refine it with `scrumrun plan task --amend TASK-NNN`; use title/request/acceptance/relation options or generic repeated `--section "Heading=content"`. Feature and Sprint support the same `--amend` pattern. Runs, Reviews, confirmed memory, and Guardrails are append-only evidence/policy, so record a new event or supersede them instead of rewriting them.
|
|
40
42
|
|
|
41
43
|
Lean mode is a read policy, not an incomplete store. Generated files and `.scrumrun/.cache/` are never authoritative.
|
|
42
44
|
|
|
@@ -81,7 +83,7 @@ At intake:
|
|
|
81
83
|
|
|
82
84
|
Evaluate every active Guardrail into a structured `passed`, `blocked`, or `deferred` result. Report blocked results with the exact `GR-NNN` id and reason code. Keep deferred results visible and enforce them at the mutation, migration, review, or owner gate they identify; never describe a deferred check as passed.
|
|
83
85
|
|
|
84
|
-
Assert the classification explicitly when the keyword inference is wrong: `
|
|
86
|
+
Assert the classification explicitly when the keyword inference is wrong: `scrumrun plan intake "…" --type fix|task|feature|docs|discovery`. Attach a short technical explanation before approval with `--preview "…"` (rendered in the terminal, bound into the token, stored as `## Preview` on the Task).
|
|
85
87
|
|
|
86
88
|
Do not create canonical artifacts, change status, edit application code, or treat ambiguous acknowledgement as approval. Temporary context may exist only in ignored disposable cache.
|
|
87
89
|
|
|
@@ -104,12 +106,12 @@ During execution:
|
|
|
104
106
|
4. work normally: edit code and update the Task's `## Technical Summary` and, for non-automatic rules, `## Guardrail Evidence` as evidence becomes available;
|
|
105
107
|
5. validate in proportion to risk and against the acceptance criteria;
|
|
106
108
|
6. run configured reviewers when a Guardrail requires one;
|
|
107
|
-
7. finish once with `scrumrun
|
|
109
|
+
7. finish once with `scrumrun plan run --finalize RUN-NNN`; it verifies every changed path, policy, secret boundary, and guardrail evidence before creating the structured Run events and completing the Task;
|
|
108
110
|
8. use path-scoped Mutation Gateway commands only when the owner explicitly requests strict execution.
|
|
109
111
|
|
|
110
112
|
Never overwrite a prior attempt. Never mark work complete because time/token budget ended.
|
|
111
113
|
|
|
112
|
-
When a Run completes and work remains queued, the briefing's `## Next Up` names the next backlog Task. Surface it with `
|
|
114
|
+
When a Run completes and work remains queued, the briefing's `## Next Up` names the next backlog Task. Surface it with `scrumrun plan task --next` and start it with `scrumrun plan task --start [TASK-NNN]` — starting is the explicit approval; the owner can always decline. Each agent declares its identity via `SCRUMRUN_AGENT` (or `Agent Identity` in `config.md`); it is recorded as the Task `assignee` and the Run event `actor`.
|
|
113
115
|
|
|
114
116
|
Every deferred policy result is an append-only Run obligation. The final checkpoint fails closed on policy drift, protected-path changes, unsafe symlinks, unscannable content, newly introduced secret-like content, or missing Guardrail Evidence. In strict mode it additionally requires the permit chain. The ignored permit cache is disposable; deleting it invalidates outstanding strict-mode permits and never creates authority.
|
|
115
117
|
|
|
@@ -155,22 +157,22 @@ scrumrun migrate --to 2 --apply
|
|
|
155
157
|
scrumrun migrate --to 2 --rollback
|
|
156
158
|
```
|
|
157
159
|
|
|
158
|
-
Inside a v1 project, `
|
|
160
|
+
Inside a v1 project, `scrumrun update` runs a read-only preflight. `update --migrate` explicitly approves application of that verified plan and keeps rollback available.
|
|
159
161
|
|
|
160
162
|
Dry-run must not write project data. Apply requires a hashed inventory, byte-exact local backup, staged validation, atomic switch, mapping report, and idempotent replay. Incomplete hybrid trees reuse existing evidenced v2 relations rather than duplicating them; early v2 prose Runs upgrade to ledger schema 1 only through the same explicit apply gate. Legacy-only aggregates leave the active tree but remain byte-exact in the ignored backup. Ambiguous records are preserved as warnings or evidenced snapshots, never guessed. Vault content remains local and is never rendered. Rollback must refuse if it would erase post-migration changes.
|
|
161
163
|
|
|
162
164
|
## Command grammar
|
|
163
165
|
|
|
164
|
-
###
|
|
166
|
+
### `scrumrun plan`
|
|
165
167
|
|
|
166
|
-
- `task`: add/list/show/run/audit/cancel/retry atomic work; use `type: fix` for fixes and `status: backlog` for parked work. `--next` surfaces the oldest backlog Task; `--start [TASK-NNN]` promotes it and creates its first Run.
|
|
167
|
-
- `sprint`: add/list/show/start/complete/block a real Task batch/timebox.
|
|
168
|
-
- `feature`: add/list/show/activate/complete long-lived initiatives.
|
|
168
|
+
- `task`: add/amend/list/show/run/audit/cancel/retry atomic work; use `--amend TASK-NNN` before it starts to refine planning truth without hand-editing Markdown. Use `type: fix` for fixes and `status: backlog` for parked work. `--next` surfaces the oldest backlog Task; `--start [TASK-NNN]` promotes it and creates its first Run.
|
|
169
|
+
- `sprint`: add/amend/list/show/start/complete/block a real Task batch/timebox.
|
|
170
|
+
- `feature`: add/amend/list/show/activate/complete long-lived initiatives.
|
|
169
171
|
- `run`: list/show/render/stats/normalize-legacy/authorize-mutation/record-mutation/satisfy-guardrail/validate/learn/complete/resume/fail/block concrete Task attempts. `--complete` accepts `--summary "…"` to store a technical summary.
|
|
170
172
|
- `intake <request>`: execute the read-only request pipeline; accepts `--type fix|task|feature|docs|discovery` and `--preview "…"`.
|
|
171
173
|
- `challenge <question>`: deep read-only analysis with evidence, risks, options, and recommendation.
|
|
172
174
|
|
|
173
|
-
###
|
|
175
|
+
### `scrumrun knowledge`
|
|
174
176
|
|
|
175
177
|
- `fact`: add/list/show/approve/reject/deprecate/invalidate `K-NNN` records.
|
|
176
178
|
- `decision`: add/list/show/resolve/deprecate/invalidate `DEC-NNN` records.
|
|
@@ -181,12 +183,12 @@ Dry-run must not write project data. Apply requires a hashed inventory, byte-exa
|
|
|
181
183
|
- `study <focus>`: deep read-only discovery with precise evidence.
|
|
182
184
|
- `vault`: add/list/show/remove/path for explicitly requested local development values; list redacts values.
|
|
183
185
|
|
|
184
|
-
###
|
|
186
|
+
### `scrumrun rules`
|
|
185
187
|
|
|
186
188
|
- `guardrail`: add/list/show/retire project rules. Adding/retiring requires explicit approval.
|
|
187
189
|
- `reviewer`: add/list/show/run configured review roles.
|
|
188
190
|
|
|
189
|
-
###
|
|
191
|
+
### `scrumrun review`
|
|
190
192
|
|
|
191
193
|
- `code --run`: review code changes by severity with file/line evidence.
|
|
192
194
|
- `artifact --run`: validate method artifacts and relations.
|
|
@@ -195,7 +197,7 @@ Dry-run must not write project data. Apply requires a hashed inventory, byte-exa
|
|
|
195
197
|
|
|
196
198
|
Review is read-only unless the user separately authorizes fixes.
|
|
197
199
|
|
|
198
|
-
###
|
|
200
|
+
### `scrumrun config`
|
|
199
201
|
|
|
200
202
|
- `project`: show/set language, interaction, approval, and quick-task preferences.
|
|
201
203
|
- `init`: initialize ScrumRun without overwriting existing state silently.
|
|
@@ -207,16 +209,16 @@ Review is read-only unless the user separately authorizes fixes.
|
|
|
207
209
|
|
|
208
210
|
## Compatibility
|
|
209
211
|
|
|
210
|
-
Upgrade installs may provide thin v1 adapters for one release cycle. An adapter must execute the mapped
|
|
212
|
+
Upgrade installs may provide thin v1 adapters for one release cycle. An adapter must execute the mapped direct route in the same turn and emit one concise deprecation note; it must not merely ask the user to invoke another command.
|
|
211
213
|
|
|
212
214
|
Important mappings:
|
|
213
215
|
|
|
214
|
-
- `/sc-sprint` →
|
|
215
|
-
- `/sc-fix` →
|
|
216
|
-
- `/sc-backlog` →
|
|
217
|
-
- `/sc-decisions` →
|
|
218
|
-
- `/sc-know` →
|
|
219
|
-
- `/sc-golden` →
|
|
220
|
-
- `/sc-agent` →
|
|
216
|
+
- `/sc-sprint` → `scrumrun plan sprint`
|
|
217
|
+
- `/sc-fix` → `scrumrun plan task` with `type: fix`
|
|
218
|
+
- `/sc-backlog` → `scrumrun plan task` with `status: backlog`
|
|
219
|
+
- `/sc-decisions` → `scrumrun knowledge decision`
|
|
220
|
+
- `/sc-know` → `scrumrun knowledge fact`
|
|
221
|
+
- `/sc-golden` → `scrumrun rules guardrail`
|
|
222
|
+
- `/sc-agent` → `scrumrun rules reviewer`
|
|
221
223
|
|
|
222
|
-
Always generate and recommend the canonical
|
|
224
|
+
Always generate and recommend the canonical direct CLI grammar.
|