scrumrun 2.1.0 → 2.1.1
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 +23 -0
- package/CORE.md +6 -2
- package/MIGRATION-1-to-2.md +2 -2
- package/README.md +12 -4
- package/SPEC.md +5 -2
- package/bin/scrumrun.js +56 -0
- package/docs/COMMANDS.md +6 -2
- package/docs/RELEASE-SCORECARD.md +11 -11
- package/docs/RELEASE.md +7 -7
- package/docs/SCHEMA.md +3 -1
- package/docs/SEMANTIC-MEMORY.md +1 -1
- package/docs/TROUBLESHOOTING.md +1 -1
- package/lib/commands/manifest.js +4 -1
- package/lib/commands/render.js +1 -0
- package/lib/memory/index.js +135 -34
- package/lib/memory/service.js +3 -0
- package/lib/runtime/mutation-gateway.js +434 -0
- package/lib/runtime/orchestrator.js +35 -3
- package/lib/runtime/policy-engine.js +85 -2
- package/lib/runtime/request-engine.js +4 -0
- package/lib/runtime/review-service.js +92 -0
- package/lib/runtime/run-ledger.js +228 -6
- package/lib/runtime/workspace-state.js +146 -0
- package/lib/security/secrets.js +15 -1
- package/lib/v2/artifacts.js +3 -0
- package/lib/v2/conformance.js +20 -1
- package/lib/v2/migration.js +6 -2
- package/lib/v2/run-ledger-migration.js +30 -2
- package/lib/v2/schema.js +10 -3
- package/package.json +1 -1
- package/templates/project/.scrumrun/method.json +4 -1
- package/templates/project/AGENTS.md +2 -1
- package/templates/project-lean/AGENTS.md +3 -1
- package/templates/shared/skills/scrumrun/SKILL.md +6 -2
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { execFileSync } = require("node:child_process");
|
|
6
|
+
const { assertNoSymlinkPath, sha256 } = require("../v2/artifacts");
|
|
7
|
+
const { secretFingerprints } = require("../security/secrets");
|
|
8
|
+
|
|
9
|
+
const SKIP_DIRECTORIES = new Set([".git", ".scrumrun", "node_modules"]);
|
|
10
|
+
|
|
11
|
+
function posix(value) {
|
|
12
|
+
return value.split(path.sep).join("/");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function stable(value) {
|
|
16
|
+
if (Array.isArray(value)) return `[${value.map(stable).join(",")}]`;
|
|
17
|
+
if (value && typeof value === "object") return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stable(value[key])}`).join(",")}}`;
|
|
18
|
+
return JSON.stringify(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizedRelative(projectRoot, value) {
|
|
22
|
+
const root = path.resolve(projectRoot);
|
|
23
|
+
const text = String(value || "").trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "");
|
|
24
|
+
if (!text || text === "." || path.posix.isAbsolute(text) || text.split("/").includes("..")) throw new Error(`Unsafe mutation path: ${value || "missing"}`);
|
|
25
|
+
if (text === ".git" || text.startsWith(".git/") || text === ".scrumrun" || text.startsWith(".scrumrun/")) {
|
|
26
|
+
throw new Error(`Mutation Gateway cannot authorize control path: ${text}`);
|
|
27
|
+
}
|
|
28
|
+
const target = path.resolve(root, text);
|
|
29
|
+
if (!target.startsWith(`${root}${path.sep}`)) throw new Error(`Mutation path escapes project: ${text}`);
|
|
30
|
+
assertNoSymlinkPath(root, target);
|
|
31
|
+
return posix(path.relative(root, target));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function fileValue(projectRoot, relative) {
|
|
35
|
+
const target = path.join(projectRoot, relative);
|
|
36
|
+
let stat;
|
|
37
|
+
try {
|
|
38
|
+
stat = fs.lstatSync(target);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error.code === "ENOENT") return { path: relative, sha256: null, kind: "missing", mode: null, scan: "not-applicable", secrets: [] };
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
if (stat.isSymbolicLink()) {
|
|
44
|
+
const link = fs.readlinkSync(target);
|
|
45
|
+
return { path: relative, sha256: sha256(`symlink\0${link}`), kind: "symlink", mode: stat.mode & 0o777, scan: "not-applicable", secrets: [] };
|
|
46
|
+
}
|
|
47
|
+
if (!stat.isFile()) return { path: relative, sha256: sha256(`non-file\0${stat.mode}`), kind: "other", mode: stat.mode & 0o777, scan: "not-applicable", secrets: [] };
|
|
48
|
+
const content = fs.readFileSync(target);
|
|
49
|
+
const scan = content.length > 10 * 1024 * 1024 ? "too-large" : content.includes(0) ? "binary" : "text";
|
|
50
|
+
const secrets = scan === "text" ? secretFingerprints(content.toString("utf8")) : [];
|
|
51
|
+
return { path: relative, sha256: sha256(content), kind: "file", mode: stat.mode & 0o777, scan, secrets };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function gitOutput(projectRoot, args) {
|
|
55
|
+
return execFileSync("git", args, { cwd: projectRoot, encoding: "buffer", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 64 * 1024 * 1024 });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function gitState(projectRoot) {
|
|
59
|
+
let top;
|
|
60
|
+
try {
|
|
61
|
+
top = gitOutput(projectRoot, ["rev-parse", "--show-toplevel"]).toString("utf8").trim();
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
if (path.resolve(top) !== path.resolve(projectRoot)) return null;
|
|
66
|
+
const head = gitOutput(projectRoot, ["rev-parse", "--verify", "HEAD"]).toString("utf8").trim();
|
|
67
|
+
const flagged = gitOutput(projectRoot, ["ls-files", "-v", "-z", "--", "."]).toString("utf8").split("\0").filter(Boolean)
|
|
68
|
+
.filter((entry) => /^[a-zS] /.test(entry));
|
|
69
|
+
if (flagged.length) throw new Error(`Git workspace contains hidden index flags (assume-unchanged or skip-worktree): ${flagged.slice(0, 10).map((entry) => entry.slice(2)).join(", ")}.`);
|
|
70
|
+
const tracked = gitOutput(projectRoot, ["diff", "--name-only", "-z", "--no-renames", "HEAD", "--", "."]);
|
|
71
|
+
const untracked = gitOutput(projectRoot, ["ls-files", "--others", "--exclude-standard", "-z", "--", "."]);
|
|
72
|
+
const ignored = gitOutput(projectRoot, ["ls-files", "--others", "--ignored", "--exclude-standard", "-z", "--", "."]);
|
|
73
|
+
const paths = new Set(Buffer.concat([tracked, untracked, ignored]).toString("utf8").split("\0").filter(Boolean)
|
|
74
|
+
.map((entry) => entry.replace(/\\/g, "/"))
|
|
75
|
+
.filter((entry) => {
|
|
76
|
+
const segments = entry.split("/");
|
|
77
|
+
return entry !== ".scrumrun" && !entry.startsWith(".scrumrun/") && !segments.includes("node_modules") && !segments.includes(".git");
|
|
78
|
+
}));
|
|
79
|
+
if (paths.size > 20000) throw new Error("Git workspace exceeds the 20,000-file Mutation Gateway limit after ignored-file coverage.");
|
|
80
|
+
const files = [...paths].sort().map((relative) => fileValue(projectRoot, relative));
|
|
81
|
+
return { schema: 1, mode: "git", head, files };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function fileState(projectRoot) {
|
|
85
|
+
const files = [];
|
|
86
|
+
function visit(directory, relative = "") {
|
|
87
|
+
const entries = fs.readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name));
|
|
88
|
+
for (const entry of entries) {
|
|
89
|
+
if (entry.isDirectory() && SKIP_DIRECTORIES.has(entry.name)) continue;
|
|
90
|
+
const next = relative ? `${relative}/${entry.name}` : entry.name;
|
|
91
|
+
const target = path.join(directory, entry.name);
|
|
92
|
+
if (entry.isDirectory()) visit(target, next);
|
|
93
|
+
else files.push(fileValue(projectRoot, next));
|
|
94
|
+
if (files.length > 20000) throw new Error("Non-git workspace exceeds the 20,000-file Mutation Gateway limit.");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
visit(projectRoot);
|
|
98
|
+
return { schema: 1, mode: "files", head: null, files };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function workspaceState(projectRoot) {
|
|
102
|
+
const root = path.resolve(projectRoot);
|
|
103
|
+
const state = gitState(root) || fileState(root);
|
|
104
|
+
const canonical = {
|
|
105
|
+
schema: state.schema,
|
|
106
|
+
mode: state.mode,
|
|
107
|
+
head: state.head,
|
|
108
|
+
files: state.files.map(({ path: file, sha256: hash, kind, mode, scan }) => ({ path: file, sha256: hash, kind, mode, scan }))
|
|
109
|
+
};
|
|
110
|
+
return { ...state, fingerprint: sha256(stable(canonical)) };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function changesBetween(before, after) {
|
|
114
|
+
if (!before || !after || before.mode !== after.mode || before.head !== after.head) throw new Error("Workspace baseline mode or Git HEAD changed during the Run.");
|
|
115
|
+
const left = new Map(before.files.map((item) => [item.path, item]));
|
|
116
|
+
const right = new Map(after.files.map((item) => [item.path, item]));
|
|
117
|
+
return [...new Set([...left.keys(), ...right.keys()])].sort().flatMap((file) => {
|
|
118
|
+
const previous = left.get(file) || { sha256: null, kind: "clean", mode: null, scan: "not-applicable", secrets: [] };
|
|
119
|
+
const next = right.get(file) || { sha256: null, kind: "clean", mode: null, scan: "not-applicable", secrets: [] };
|
|
120
|
+
if (previous.sha256 === next.sha256 && previous.kind === next.kind && previous.mode === next.mode) return [];
|
|
121
|
+
return [{
|
|
122
|
+
path: file,
|
|
123
|
+
before_sha256: previous.sha256,
|
|
124
|
+
after_sha256: next.sha256,
|
|
125
|
+
before_kind: previous.kind,
|
|
126
|
+
after_kind: next.kind,
|
|
127
|
+
before_mode: previous.mode,
|
|
128
|
+
after_mode: next.mode,
|
|
129
|
+
before_scan: previous.scan,
|
|
130
|
+
after_scan: next.scan,
|
|
131
|
+
new_secret_fingerprints: (next.secrets || []).filter((value) => !(previous.secrets || []).includes(value))
|
|
132
|
+
}];
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function pathAuthorized(relative, allowed) {
|
|
137
|
+
return allowed.some((entry) => relative === entry || relative.startsWith(`${entry}/`));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
changesBetween,
|
|
142
|
+
normalizedRelative,
|
|
143
|
+
pathAuthorized,
|
|
144
|
+
stable,
|
|
145
|
+
workspaceState
|
|
146
|
+
};
|
package/lib/security/secrets.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const crypto = require("node:crypto");
|
|
4
|
+
|
|
3
5
|
const SECRET_PATTERNS = Object.freeze([
|
|
4
6
|
/\bsk-[A-Za-z0-9_-]{16,}\b/,
|
|
5
7
|
/\bAKIA[0-9A-Z]{16}\b/,
|
|
@@ -20,4 +22,16 @@ function assertNoSecret(values, message = "Secret-like content is forbidden outs
|
|
|
20
22
|
if (list.some(containsSecret)) throw new Error(message);
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
function secretFingerprints(value) {
|
|
26
|
+
const text = String(value || "");
|
|
27
|
+
const found = new Set();
|
|
28
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
29
|
+
const matcher = new RegExp(pattern.source, `${pattern.flags.replace(/g/g, "")}g`);
|
|
30
|
+
for (const match of text.matchAll(matcher)) {
|
|
31
|
+
found.add(crypto.createHash("sha256").update(match[0]).digest("hex"));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return [...found].sort();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { SECRET_PATTERNS, assertNoSecret, containsSecret, secretFingerprints };
|
package/lib/v2/artifacts.js
CHANGED
|
@@ -114,6 +114,9 @@ function validateArtifact(record, expectedKind = null) {
|
|
|
114
114
|
if (field === "ledger" && record[field] !== undefined && record[field] !== 1) {
|
|
115
115
|
errors.push(`${record.kind}.${field} must be 1`);
|
|
116
116
|
}
|
|
117
|
+
if (["guardrails", "workspace"].includes(field) && record[field] !== undefined && record[field] !== 1) {
|
|
118
|
+
errors.push(`${record.kind}.${field} must be 1`);
|
|
119
|
+
}
|
|
117
120
|
}
|
|
118
121
|
return errors;
|
|
119
122
|
}
|
package/lib/v2/conformance.js
CHANGED
|
@@ -12,6 +12,7 @@ const { resolveEvidence } = require("../memory/service");
|
|
|
12
12
|
const { containsSecret } = require("../security/secrets");
|
|
13
13
|
const { pendingTransactionStatus } = require("./transaction");
|
|
14
14
|
const { configWeakeningAttempts, validateGuardrailDocument } = require("../runtime/policy-engine");
|
|
15
|
+
const { auditActiveWorkspace } = require("../runtime/mutation-gateway");
|
|
15
16
|
|
|
16
17
|
const INVARIANTS = Object.freeze([
|
|
17
18
|
{ id: "I-01", summary: "pre-approval work is read-only", tests: ["intake builds bounded context without writing"] },
|
|
@@ -33,7 +34,8 @@ const INVARIANTS = Object.freeze([
|
|
|
33
34
|
{ id: "I-17", summary: "ambiguous migration is preserved and warned", tests: ["partial v1 layout is preserved"] },
|
|
34
35
|
{ id: "I-18", summary: "unsafe and partial writes fail safely", tests: ["canonical writes reject traversal and symlink paths", "repository refuses conflicting overwrite", "interrupted kernel transaction is recovered"] },
|
|
35
36
|
{ id: "I-19", summary: "code intelligence is derived and fingerprinted", tests: ["language adapters are replaceable", "moves remap by fingerprint"] },
|
|
36
|
-
{ id: "I-20", summary: "learning candidates never block execution", tests: ["post-validation extraction creates candidate insights"] }
|
|
37
|
+
{ id: "I-20", summary: "learning candidates never block execution", tests: ["post-validation extraction creates candidate insights"] },
|
|
38
|
+
{ id: "I-21", summary: "material mutations are scoped, policy-bound, and fail closed", tests: ["Mutation Gateway rejects bypass and out-of-scope writes", "Run completion rejects unresolved Guardrail obligations"] }
|
|
37
39
|
]);
|
|
38
40
|
|
|
39
41
|
function finding(severity, code, message, file = null) {
|
|
@@ -66,6 +68,14 @@ function auditProject(projectRoot) {
|
|
|
66
68
|
if (!guardrailValidation.records.length) findings.push(finding("high", "GUARDRAILS_EMPTY", "No stable-id Guardrail is defined."));
|
|
67
69
|
if (!guardrailValidation.records.some((record) => record.status === "active")) findings.push(finding("high", "GUARDRAILS_INACTIVE", "No active stable-id Guardrail is defined."));
|
|
68
70
|
for (const error of guardrailValidation.errors) findings.push(finding("high", "GUARDRAIL_INVALID", error, guardrailsFile));
|
|
71
|
+
const strictGuardrails = methodMarker && methodMarker.schemas && methodMarker.schemas.guardrails === 1;
|
|
72
|
+
if (strictGuardrails) {
|
|
73
|
+
for (const record of guardrailValidation.records) {
|
|
74
|
+
for (const field of ["status", "enforcement", "scope", "rule"]) {
|
|
75
|
+
if (!record.explicit[field]) findings.push(finding("high", "GUARDRAIL_FIELD_INFERRED", `${record.id} must explicitly declare ${field}.`, guardrailsFile));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
69
79
|
const configFile = path.join(scrumDir, "config.md");
|
|
70
80
|
const config = fs.existsSync(configFile) && fs.lstatSync(configFile).isFile() ? fs.readFileSync(configFile, "utf8") : "";
|
|
71
81
|
for (const error of configWeakeningAttempts(config)) findings.push(finding("high", "CONFIG_WEAKENS_POLICY", error, configFile));
|
|
@@ -102,6 +112,15 @@ function auditProject(projectRoot) {
|
|
|
102
112
|
const declared = methodMarker && methodMarker.schemas && methodMarker.schemas.run_ledger === RUN_LEDGER_VERSION;
|
|
103
113
|
findings.push(finding(declared ? "high" : "warning", "RUN_LEDGER_LEGACY", `${run.record.id} requires the Run ledger ${RUN_LEDGER_VERSION} migration.`, run.file));
|
|
104
114
|
}
|
|
115
|
+
const secureRuns = methodMarker && methodMarker.schemas && methodMarker.schemas.run_obligations === 1 && methodMarker.schemas.mutation_gateway === 1;
|
|
116
|
+
const active = run.record && !["completed", "failed", "blocked"].includes(run.record.status);
|
|
117
|
+
if (secureRuns && active && (run.record.guardrails !== 1 || run.record.workspace !== 1)) {
|
|
118
|
+
findings.push(finding("high", "RUN_SECURITY_SCHEMA", `${run.record.id} must be retried or migrated to Guardrail/workspace schema 1.`, run.file));
|
|
119
|
+
}
|
|
120
|
+
if (run.record && run.record.workspace === 1) {
|
|
121
|
+
const bypass = auditActiveWorkspace(projectRoot, run);
|
|
122
|
+
if (bypass) findings.push(finding("critical", "MUTATION_BYPASS", `${run.record.id}: ${bypass}`, run.file));
|
|
123
|
+
}
|
|
105
124
|
}
|
|
106
125
|
const byId = new Map(Object.values(records).flat().filter((artifact) => artifact.record).map((artifact) => [artifact.record.id, artifact]));
|
|
107
126
|
for (const task of records.task || []) {
|
package/lib/v2/migration.js
CHANGED
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
sha256,
|
|
17
17
|
validateArtifact
|
|
18
18
|
} = require("./artifacts");
|
|
19
|
+
const { inferEnforcement, normalizeGuardrailDocument } = require("../runtime/policy-engine");
|
|
19
20
|
const { RUN_LEDGER_VERSION } = require("./schema");
|
|
20
21
|
const { migrateLegacyRun } = require("../runtime/run-ledger");
|
|
21
22
|
|
|
@@ -817,7 +818,7 @@ function migrationPlan(projectRoot) {
|
|
|
817
818
|
const golden = readText(scrumDir, "golden-rules.md");
|
|
818
819
|
const current = readText(scrumDir, "guardrails.md");
|
|
819
820
|
if (/^# ScrumRun Project Guardrails\b/m.test(current) && /^#{2,6} GR-\d{3,}\b/m.test(current)) {
|
|
820
|
-
const canonical = current.replace(/^#{3,6}(?= GR-\d{3,}\b)/gm, "##");
|
|
821
|
+
const canonical = normalizeGuardrailDocument(current.replace(/^#{3,6}(?= GR-\d{3,}\b)/gm, "##"));
|
|
821
822
|
if (canonical !== current) {
|
|
822
823
|
generated.set("guardrails.md", canonical.endsWith("\n") ? canonical : `${canonical}\n`);
|
|
823
824
|
const mapping = mappings.find((item) => item.source === "guardrails.md" && !item.anchor);
|
|
@@ -848,6 +849,9 @@ function migrationPlan(projectRoot) {
|
|
|
848
849
|
`## GR-${String(index + 1).padStart(3, "0")} - ${rule}`,
|
|
849
850
|
"",
|
|
850
851
|
"Status: active",
|
|
852
|
+
`Enforcement: ${inferEnforcement(rule, rule)}`,
|
|
853
|
+
"Scope: all",
|
|
854
|
+
`Rule: ${rule}`,
|
|
851
855
|
`Source: \`${source}\``,
|
|
852
856
|
""
|
|
853
857
|
]),
|
|
@@ -925,7 +929,7 @@ function migrationPlan(projectRoot) {
|
|
|
925
929
|
generated.set("method.json", `${JSON.stringify({
|
|
926
930
|
method: METHOD_VERSION,
|
|
927
931
|
layout: "v2",
|
|
928
|
-
schemas: { run_ledger: RUN_LEDGER_VERSION },
|
|
932
|
+
schemas: { run_ledger: RUN_LEDGER_VERSION, guardrails: 1, run_obligations: 1, mutation_gateway: 1 },
|
|
929
933
|
migrated_from: sourceLayout,
|
|
930
934
|
migration: MIGRATION_NAME
|
|
931
935
|
}, null, 2)}\n`);
|
|
@@ -15,6 +15,7 @@ const {
|
|
|
15
15
|
} = require("./artifacts");
|
|
16
16
|
const { RUN_LEDGER_VERSION } = require("./schema");
|
|
17
17
|
const { migrateLegacyRun, validateRunLedger } = require("../runtime/run-ledger");
|
|
18
|
+
const { normalizeGuardrailDocument, validateGuardrailDocument } = require("../runtime/policy-engine");
|
|
18
19
|
|
|
19
20
|
const MIGRATION_NAME = "run-ledger-v1";
|
|
20
21
|
const MIGRATION_DIR = path.join(".migration", MIGRATION_NAME);
|
|
@@ -49,6 +50,27 @@ function planRunLedgerMigration(projectRoot) {
|
|
|
49
50
|
const warnings = [];
|
|
50
51
|
const errors = [];
|
|
51
52
|
|
|
53
|
+
const guardrailsFile = assertNoSymlinkPath(scrumDir, path.join(scrumDir, "guardrails.md"));
|
|
54
|
+
if (fs.existsSync(guardrailsFile) && fs.lstatSync(guardrailsFile).isFile()) {
|
|
55
|
+
const before = fs.readFileSync(guardrailsFile, "utf8");
|
|
56
|
+
const after = normalizeGuardrailDocument(before);
|
|
57
|
+
const validation = validateGuardrailDocument(after);
|
|
58
|
+
if (validation.errors.length) errors.push(`guardrails.md: ${validation.errors.join("; ")}`);
|
|
59
|
+
else if (before !== after) {
|
|
60
|
+
changes.push({
|
|
61
|
+
relative: "guardrails.md",
|
|
62
|
+
before,
|
|
63
|
+
after,
|
|
64
|
+
beforeSha256: sha256(before),
|
|
65
|
+
afterSha256: sha256(after),
|
|
66
|
+
mode: "guardrail-schema"
|
|
67
|
+
});
|
|
68
|
+
warnings.push("guardrails.md: inferred legacy policy fields were made explicit without deleting source prose.");
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
errors.push("guardrails.md is missing or unsafe.");
|
|
72
|
+
}
|
|
73
|
+
|
|
52
74
|
for (const artifact of repository.list("run")) {
|
|
53
75
|
if (!artifact.record || artifact.errors.length) {
|
|
54
76
|
errors.push(`${path.relative(scrumDir, artifact.file)}: ${artifact.errors.join("; ")}`);
|
|
@@ -79,7 +101,13 @@ function planRunLedgerMigration(projectRoot) {
|
|
|
79
101
|
|
|
80
102
|
const markerValue = {
|
|
81
103
|
...marker.value,
|
|
82
|
-
schemas: {
|
|
104
|
+
schemas: {
|
|
105
|
+
...(marker.value.schemas || {}),
|
|
106
|
+
run_ledger: RUN_LEDGER_VERSION,
|
|
107
|
+
guardrails: 1,
|
|
108
|
+
run_obligations: 1,
|
|
109
|
+
mutation_gateway: 1
|
|
110
|
+
}
|
|
83
111
|
};
|
|
84
112
|
const markerAfter = `${JSON.stringify(markerValue, null, 2)}\n`;
|
|
85
113
|
if (marker.content !== markerAfter) {
|
|
@@ -112,7 +140,7 @@ function reportRunLedgerMigration(plan, status = plan.status) {
|
|
|
112
140
|
: "- No changes required.";
|
|
113
141
|
const warnings = plan.warnings.length ? plan.warnings.map((warning) => `- ${warning}`).join("\n") : "- None.";
|
|
114
142
|
const errors = plan.errors.length ? plan.errors.map((error) => `- ${error}`).join("\n") : "- None.";
|
|
115
|
-
return `# ScrumRun
|
|
143
|
+
return `# ScrumRun Kernel Schema Migration\n\nStatus: ${status}\nRun ledger schema: ${RUN_LEDGER_VERSION}\nGuardrail schema: 1\nMutation Gateway schema: 1\nSource fingerprint: \`${plan.fingerprint}\`\nChanged files: ${plan.changes.length}\n\n## Mappings\n\n${mappings}\n\n## Warnings\n\n${warnings}\n\n## Blockers\n\n${errors}\n`;
|
|
116
144
|
}
|
|
117
145
|
|
|
118
146
|
function manifestFile(scrumDir) {
|
package/lib/v2/schema.js
CHANGED
|
@@ -9,7 +9,7 @@ function deepFreeze(value) {
|
|
|
9
9
|
const METHOD_VERSION = "2.0.0";
|
|
10
10
|
const RUN_LEDGER_VERSION = 1;
|
|
11
11
|
|
|
12
|
-
const RUN_EVENT_TYPES = deepFreeze(["transition", "snapshot"]);
|
|
12
|
+
const RUN_EVENT_TYPES = deepFreeze(["transition", "snapshot", "guardrail", "mutation"]);
|
|
13
13
|
const RUN_EVIDENCE_KINDS = deepFreeze([
|
|
14
14
|
"approval",
|
|
15
15
|
"command",
|
|
@@ -21,9 +21,13 @@ const RUN_EVIDENCE_KINDS = deepFreeze([
|
|
|
21
21
|
"risk",
|
|
22
22
|
"note",
|
|
23
23
|
"migration",
|
|
24
|
-
"legacy"
|
|
24
|
+
"legacy",
|
|
25
|
+
"guardrail",
|
|
26
|
+
"mutation"
|
|
25
27
|
]);
|
|
26
28
|
|
|
29
|
+
const GUARDRAIL_RESULTS = deepFreeze(["pending", "passed", "blocked"]);
|
|
30
|
+
|
|
27
31
|
const ARTIFACT_TYPES = deepFreeze({
|
|
28
32
|
feature: {
|
|
29
33
|
prefix: "FEAT",
|
|
@@ -109,7 +113,9 @@ const STRUCTURAL_RELATIONS = deepFreeze({
|
|
|
109
113
|
|
|
110
114
|
const SCALAR_FIELDS = deepFreeze({
|
|
111
115
|
attempt: { kinds: ["run"], required: true, type: "positive integer", meaning: "monotonic execution-attempt number within one Task" },
|
|
112
|
-
ledger: { kinds: ["run"], required: false, type: `integer ${RUN_LEDGER_VERSION}`, meaning: "canonical Run event-ledger schema; required for newly authored Runs" }
|
|
116
|
+
ledger: { kinds: ["run"], required: false, type: `integer ${RUN_LEDGER_VERSION}`, meaning: "canonical Run event-ledger schema; required for newly authored Runs" },
|
|
117
|
+
guardrails: { kinds: ["run"], required: false, type: "integer 1", meaning: "append-only Guardrail obligation schema" },
|
|
118
|
+
workspace: { kinds: ["run"], required: false, type: "integer 1", meaning: "workspace mutation-gateway schema" }
|
|
113
119
|
});
|
|
114
120
|
|
|
115
121
|
const TRUTH_OWNERSHIP = deepFreeze({
|
|
@@ -137,6 +143,7 @@ module.exports = {
|
|
|
137
143
|
ARTIFACT_TRANSITIONS,
|
|
138
144
|
ARTIFACT_TYPES,
|
|
139
145
|
AUTHORITY,
|
|
146
|
+
GUARDRAIL_RESULTS,
|
|
140
147
|
METHOD_VERSION,
|
|
141
148
|
RUN_EVENT_TYPES,
|
|
142
149
|
RUN_EVIDENCE_KINDS,
|
package/package.json
CHANGED
|
@@ -20,7 +20,8 @@ After approval:
|
|
|
20
20
|
- Run is one execution attempt and follows `executing → validating → learning → completed|failed|blocked`;
|
|
21
21
|
- a retry creates a new Run and preserves the old one;
|
|
22
22
|
- learning proposes evidence-backed Knowledge, Decisions, or candidate Insights.
|
|
23
|
+
- every application/source edit requires a path-scoped Mutation Gateway permit, immediate hash recording, and resolution of the Run's Guardrail obligations before completion.
|
|
23
24
|
|
|
24
|
-
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.
|
|
25
|
+
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.
|
|
25
26
|
|
|
26
27
|
If `/sc` is unavailable, follow the equivalent workflow in `.scrumrun/core.md` manually.
|
|
@@ -13,6 +13,8 @@ Do not scan every Task, Run, Sprint, Feature, or Memory file by default. Generat
|
|
|
13
13
|
|
|
14
14
|
Natural-language product work begins as read-only intake. Explicit approval creates/updates a Task and creates one Run. A Sprint exists only for a real batch/timebox. Run state is `executing → validating → learning → completed|failed|blocked`; retries preserve prior Runs.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Every application/source edit requires a short-lived path-scoped Mutation Gateway permit and immediate hash recording in the active Run. Resolve all persisted Guardrail obligations before completion; policy/workspace drift fails closed.
|
|
17
|
+
|
|
18
|
+
`.scrumrun/guardrails.md` is canonical policy. Never bypass it or the Mutation Gateway, overwrite owner work, auto-confirm AI knowledge, auto-migrate v1 state, or print vault values.
|
|
17
19
|
|
|
18
20
|
Use `/sc <noun> <subject> <action> [args]`; if `/sc` is unavailable, follow `.scrumrun/core.md` manually.
|
|
@@ -80,6 +80,8 @@ Evaluate every active Guardrail into a structured `passed`, `blocked`, or `defer
|
|
|
80
80
|
|
|
81
81
|
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.
|
|
82
82
|
|
|
83
|
+
The approval token binds both canonical context and a complete workspace fingerprint. Any canonical or source change after planning invalidates it and requires a new intake.
|
|
84
|
+
|
|
83
85
|
## Approved execution
|
|
84
86
|
|
|
85
87
|
Explicit approval creates/updates the Task and creates a Run. The Run follows:
|
|
@@ -93,7 +95,7 @@ During execution:
|
|
|
93
95
|
|
|
94
96
|
1. keep the change inside the approved Task scope;
|
|
95
97
|
2. preserve existing owner work and unrelated dirty files;
|
|
96
|
-
3. enforce guardrails before every material mutation;
|
|
98
|
+
3. enforce guardrails before every material mutation: obtain a short-lived path-scoped Mutation Gateway permit before editing and record its verified before/after hashes immediately afterward;
|
|
97
99
|
4. validate in proportion to risk;
|
|
98
100
|
5. record exactly one structured `RUN-NNN-EVT-NNN` event per state transition, with RFC3339 time, actor, reason, and typed evidence;
|
|
99
101
|
6. run configured reviewers;
|
|
@@ -102,6 +104,8 @@ During execution:
|
|
|
102
104
|
|
|
103
105
|
Never overwrite a prior attempt. Never mark work complete because time/token budget ended.
|
|
104
106
|
|
|
107
|
+
Every deferred policy result is an append-only Run obligation. Unrecorded workspace drift, policy drift, an expired/missing permit, out-of-scope changes, unsafe symlinks, newly introduced secret-like content, or an unresolved obligation blocks validation/completion. The ignored permit cache is disposable; deleting it invalidates outstanding permits and never creates authority.
|
|
108
|
+
|
|
105
109
|
Run is the sole operational-history authority. Task synchronizes current status without copying Run events. Validation, learning, completion, failure, block, and resume require a reason or structured evidence; completion also requires evidenced validation and learning. Early v2 prose Runs are migrated explicitly, with deterministic chains recovered and uncertain history represented as an evidenced snapshot.
|
|
106
110
|
|
|
107
111
|
Linked canonical writes use the ignored durable transaction journal. An interrupted prepared mutation rolls back before the next approved mutation; a committed journal is verified and finalized. Audit remains read-only and reports pending recovery. Use `doctor --recover` only when explicitly requested, and never overwrite bytes changed after interruption.
|
|
@@ -155,7 +159,7 @@ Dry-run must not write project data. Apply requires a hashed inventory, byte-exa
|
|
|
155
159
|
- `task`: add/list/show/run/audit/cancel/retry atomic work; use `type: fix` for fixes and `status: backlog` for parked work.
|
|
156
160
|
- `sprint`: add/list/show/start/complete/block a real Task batch/timebox.
|
|
157
161
|
- `feature`: add/list/show/activate/complete long-lived initiatives.
|
|
158
|
-
- `run`: list/show/validate/learn/complete/resume/fail/block concrete Task attempts.
|
|
162
|
+
- `run`: list/show/authorize-mutation/record-mutation/satisfy-guardrail/validate/learn/complete/resume/fail/block concrete Task attempts.
|
|
159
163
|
- `intake <request>`: execute the read-only request pipeline.
|
|
160
164
|
- `challenge <question>`: deep read-only analysis with evidence, risks, options, and recommendation.
|
|
161
165
|
|