scrumrun 2.0.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 +45 -0
- package/CORE.md +17 -3
- package/DECISIONS.md +56 -0
- package/MIGRATION-1-to-2.md +11 -0
- package/README.md +23 -5
- package/SPEC.md +30 -10
- package/bin/scrumrun.js +175 -11
- package/docs/COMMANDS.md +10 -4
- package/docs/ENTITY-MODEL.md +1 -1
- package/docs/RELEASE-SCORECARD.md +43 -0
- package/docs/RELEASE.md +19 -12
- package/docs/SCHEMA.md +11 -0
- package/docs/SEMANTIC-MEMORY.md +1 -1
- package/docs/TROUBLESHOOTING.md +13 -1
- package/lib/commands/manifest.js +15 -3
- package/lib/commands/render.js +4 -0
- package/lib/memory/index.js +201 -41
- package/lib/memory/service.js +3 -0
- package/lib/runtime/budgets.js +4 -0
- package/lib/runtime/canonical-snapshot.js +110 -0
- package/lib/runtime/context.js +5 -45
- package/lib/runtime/mutation-gateway.js +434 -0
- package/lib/runtime/orchestrator.js +130 -65
- package/lib/runtime/policy-engine.js +267 -0
- package/lib/runtime/request-engine.js +32 -24
- package/lib/runtime/review-service.js +92 -0
- package/lib/runtime/run-ledger.js +546 -0
- package/lib/runtime/workspace-state.js +146 -0
- package/lib/security/secrets.js +15 -1
- package/lib/v2/artifacts.js +24 -1
- package/lib/v2/conformance.js +78 -12
- package/lib/v2/migration.js +74 -10
- package/lib/v2/run-ledger-migration.js +268 -0
- package/lib/v2/schema.js +28 -1
- package/lib/v2/transaction.js +254 -0
- package/package.json +1 -1
- package/scripts/generate-contract-docs.js +11 -0
- package/templates/project/.scrumrun/guardrails.md +8 -0
- package/templates/project/.scrumrun/map.md +4 -3
- package/templates/project/.scrumrun/method.json +7 -1
- package/templates/project/.scrumrun/state.md +7 -14
- package/templates/project/AGENTS.md +2 -1
- package/templates/project-lean/AGENTS.md +3 -1
- package/templates/shared/skills/scrumrun/SKILL.md +19 -5
|
@@ -3,51 +3,56 @@
|
|
|
3
3
|
const { sha256 } = require("../v2/artifacts");
|
|
4
4
|
const { buildContextPackage } = require("./context");
|
|
5
5
|
const { containsSecret } = require("../security/secrets");
|
|
6
|
+
const { evaluatePolicy, normalized } = require("./policy-engine");
|
|
7
|
+
const { workspaceState } = require("./workspace-state");
|
|
6
8
|
|
|
7
9
|
function assessRisk(request) {
|
|
8
|
-
const text = request
|
|
9
|
-
const high = [
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
const text = normalized(request);
|
|
11
|
+
const high = [
|
|
12
|
+
["destructive", /\b(delete|drop|truncate|erase|wipe|apagar|excluir|remover tudo)\b/],
|
|
13
|
+
["migration", /\b(migrat\w*|migrac\w*)\b/],
|
|
14
|
+
["external-release", /\b(publish|release|deploy|publicar|lancar|implant|producao)\b/],
|
|
15
|
+
["security-boundary", /\b(auth|authorization|permission|security|secret|credential|autentic|autoriz|permiss|seguranca|segredo|credencial)\b/],
|
|
16
|
+
["money", /\b(payment|pricing|billing|pagamento|preco|faturamento)\b/]
|
|
17
|
+
];
|
|
18
|
+
const medium = [
|
|
19
|
+
["data-or-contract", /\b(database|schema|api|contract|banco|contrato)\b/],
|
|
20
|
+
["runtime-kernel", /\b(runtime|kernel|transaction|journal|recovery|multi-file|transacion|recuperac|multi-arquivo)\b/],
|
|
21
|
+
["performance-or-cache", /\b(cache|performance|latency|index|desempenho|latencia|indice)\b/],
|
|
22
|
+
["dependency-or-upgrade", /\b(dependency|upgrade|package|version|dependencia|atualiz|versao)\b/],
|
|
23
|
+
["cross-cutting-change", /\b(refactor|rename|move|architecture|cli|refator|renome|mover|arquitetura)\b/]
|
|
24
|
+
];
|
|
25
|
+
const highSignals = high.filter(([, matcher]) => matcher.test(text)).map(([signal]) => `high-impact signal: ${signal}`);
|
|
26
|
+
if (highSignals.length) return { level: "high", reasons: highSignals.slice(0, 6) };
|
|
27
|
+
const mediumSignals = medium.filter(([, matcher]) => matcher.test(text)).map(([signal]) => `cross-cutting signal: ${signal}`);
|
|
28
|
+
if (mediumSignals.length) return { level: "medium", reasons: mediumSignals.slice(0, 6) };
|
|
16
29
|
return { level: "low", reasons: ["no high-impact signals detected; validate scope before execution"] };
|
|
17
30
|
}
|
|
18
31
|
|
|
19
32
|
function classifyRequest(request) {
|
|
20
|
-
const text = request
|
|
33
|
+
const text = normalized(request);
|
|
21
34
|
if (/\b(investigat|analy[sz]|study|discover|pesquis|analis|estud|descobr)/.test(text)) {
|
|
22
35
|
return { type: "discovery", taskType: "discovery", reason: "request is primarily knowledge-seeking" };
|
|
23
36
|
}
|
|
24
|
-
if (/\b(bug|fix|broken|regression|erro|falha|corrigir|
|
|
37
|
+
if (/\b(bug|fix|broken|regression|erro|falha|corrigir|consertar)\b/.test(text)) {
|
|
25
38
|
return { type: "fix", taskType: "fix", reason: "request describes corrective work" };
|
|
26
39
|
}
|
|
27
|
-
if (/\b(
|
|
40
|
+
if (/\b(initiative|epic|roadmap|program|iniciativa|programa)\b/.test(text)) {
|
|
28
41
|
return { type: "feature", taskType: "feature", reason: "request describes a long-lived product initiative" };
|
|
29
42
|
}
|
|
30
|
-
if (/\b(sprint|timebox|batch|release train|lote)\b/.test(text)) {
|
|
43
|
+
if (/\b(sprint|timebox|batch|release train|lote)\b/.test(text) && /\b(tasks?|tarefas?|entregas?)\b/.test(text)) {
|
|
31
44
|
return { type: "sprint", taskType: "task", reason: "request explicitly asks for a timebox or delivery batch" };
|
|
32
45
|
}
|
|
33
|
-
|
|
46
|
+
const documentation = /\b(document|docs|readme|typo|texto|documenta)/.test(text);
|
|
47
|
+
const implementation = /\b(implement\w*|build\w*|creat\w*|add\w*|runtime|kernel|schema|migrat\w*|migrac\w*|transaction\w*|code|cli|criar|adicionar|construir|codigo|transacion\w*)\b/.test(text);
|
|
48
|
+
if (documentation && !implementation) {
|
|
34
49
|
return { type: "task", taskType: "docs", reason: "request appears bounded to documentation" };
|
|
35
50
|
}
|
|
36
51
|
return { type: "task", taskType: "task", reason: "request is best represented as one atomic Task" };
|
|
37
52
|
}
|
|
38
53
|
|
|
39
54
|
function applyPolicy(context) {
|
|
40
|
-
|
|
41
|
-
if (context.layout === "v1") violations.push("Project must be explicitly migrated to v2 before approved execution.");
|
|
42
|
-
if (containsSecret(context.request)) violations.push("Request appears to contain a secret; remove/redact it before planning or persistence.");
|
|
43
|
-
if (context.warnings.some((warning) => warning.includes("Secret-like content"))) violations.push("Canonical project context contains secret-like content; remove it before planning.");
|
|
44
|
-
if (!context.guardrails.length) violations.push("No active canonical guardrail could be loaded.");
|
|
45
|
-
if (context.warnings.some((warning) => warning.includes("guardrails.md"))) violations.push("Canonical guardrails are missing.");
|
|
46
|
-
return {
|
|
47
|
-
status: violations.length ? "blocked" : "passed",
|
|
48
|
-
checked: context.guardrails.map((guardrail) => guardrail.id),
|
|
49
|
-
violations
|
|
50
|
-
};
|
|
55
|
+
return evaluatePolicy(context);
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
function stableJson(value) {
|
|
@@ -67,6 +72,7 @@ function encodeApproval(plan) {
|
|
|
67
72
|
classification: plan.classification,
|
|
68
73
|
risk: plan.risk,
|
|
69
74
|
policy: plan.policy,
|
|
75
|
+
workspaceFingerprint: plan.workspaceFingerprint,
|
|
70
76
|
issuedAt: plan.issuedAt
|
|
71
77
|
};
|
|
72
78
|
const encoded = Buffer.from(stableJson(payload)).toString("base64url");
|
|
@@ -94,6 +100,7 @@ function planRequest(projectRoot, request) {
|
|
|
94
100
|
if (!normalized) throw new Error("A non-empty request is required for intake.");
|
|
95
101
|
if (normalized.length > 10000) throw new Error("Request exceeds the 10,000 character intake limit.");
|
|
96
102
|
const context = buildContextPackage(projectRoot, normalized);
|
|
103
|
+
const workspaceFingerprint = workspaceState(projectRoot).fingerprint;
|
|
97
104
|
const policy = applyPolicy(context);
|
|
98
105
|
const risk = assessRisk(normalized);
|
|
99
106
|
const classification = classifyRequest(normalized);
|
|
@@ -102,6 +109,7 @@ function planRequest(projectRoot, request) {
|
|
|
102
109
|
pipeline: ["received", "contextualizing", "policy", "risk", "classification", "planning", policy.status === "passed" ? "awaiting_approval" : "blocked"],
|
|
103
110
|
request: normalized,
|
|
104
111
|
context,
|
|
112
|
+
workspaceFingerprint,
|
|
105
113
|
policy,
|
|
106
114
|
risk,
|
|
107
115
|
classification,
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
const { ArtifactRepository, METHOD_VERSION, withArtifactLock } = require("../v2/artifacts");
|
|
5
|
+
const { auditProject } = require("../v2/conformance");
|
|
6
|
+
const { assertCanonicalWrite } = require("./mutation-gateway");
|
|
7
|
+
|
|
8
|
+
function today() {
|
|
9
|
+
return new Date().toISOString().slice(0, 10);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function oneLine(value, label) {
|
|
13
|
+
const text = String(value || "").replace(/\s+/g, " ").trim();
|
|
14
|
+
if (!text) throw new Error(`${label} is required.`);
|
|
15
|
+
if (text.length > 500) throw new Error(`${label} exceeds 500 characters.`);
|
|
16
|
+
return text;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function nextReviewId(repository) {
|
|
20
|
+
const highest = repository.list("review").reduce((max, artifact) => {
|
|
21
|
+
const match = artifact.record.id.match(/-(\d+)$/);
|
|
22
|
+
return match ? Math.max(max, Number(match[1])) : max;
|
|
23
|
+
}, 0);
|
|
24
|
+
return `REV-${String(highest + 1).padStart(3, "0")}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function recordArtifactReviewUnlocked(projectRoot, options) {
|
|
28
|
+
const scrumDir = path.join(projectRoot, ".scrumrun");
|
|
29
|
+
const repository = new ArtifactRepository(scrumDir);
|
|
30
|
+
const taskId = oneLine(options.task, "task");
|
|
31
|
+
const task = repository.read("task", taskId);
|
|
32
|
+
if (!task || task.errors.length) throw new Error(`Review Task is missing or invalid: ${taskId}`);
|
|
33
|
+
const runId = options.run ? oneLine(options.run, "run") : null;
|
|
34
|
+
if (runId) {
|
|
35
|
+
const run = repository.read("run", runId);
|
|
36
|
+
if (!run || run.errors.length) throw new Error(`Review Run is missing or invalid: ${runId}`);
|
|
37
|
+
if (run.record.task !== taskId) throw new Error(`${runId} belongs to ${run.record.task}, not ${taskId}.`);
|
|
38
|
+
}
|
|
39
|
+
const title = oneLine(options.title || `Artifact conformance review for ${runId || taskId}`, "title");
|
|
40
|
+
const suppliedEvidence = (Array.isArray(options.evidence) ? options.evidence : []).map((item) => oneLine(item, "evidence"));
|
|
41
|
+
const audit = auditProject(projectRoot);
|
|
42
|
+
const created = today();
|
|
43
|
+
const record = {
|
|
44
|
+
id: nextReviewId(repository),
|
|
45
|
+
kind: "review",
|
|
46
|
+
status: audit.passed ? "passed" : "failed",
|
|
47
|
+
created,
|
|
48
|
+
updated: created,
|
|
49
|
+
method: METHOD_VERSION,
|
|
50
|
+
task: taskId
|
|
51
|
+
};
|
|
52
|
+
const findings = audit.findings.length
|
|
53
|
+
? audit.findings.map((finding) => `- ${finding.severity}: ${finding.code} — ${finding.message}`)
|
|
54
|
+
: ["- No conformance findings."];
|
|
55
|
+
const evidence = [
|
|
56
|
+
`- Artifact audit: ${audit.invariants} invariants; ${audit.findings.length} finding(s); passed=${audit.passed}.`,
|
|
57
|
+
...(runId ? [`- Run under review: ${runId}.`] : []),
|
|
58
|
+
...suppliedEvidence.map((item) => `- ${item}`)
|
|
59
|
+
];
|
|
60
|
+
const verdict = audit.passed
|
|
61
|
+
? "Passed. The machine-readable artifact audit has no blocking finding."
|
|
62
|
+
: "Failed. Blocking conformance findings must be resolved in a separately authorized mutation.";
|
|
63
|
+
const body = [
|
|
64
|
+
`# ${title}`,
|
|
65
|
+
"",
|
|
66
|
+
"## Scope",
|
|
67
|
+
"",
|
|
68
|
+
`Review canonical project conformance for ${runId || taskId}.`,
|
|
69
|
+
"",
|
|
70
|
+
"## Evidence",
|
|
71
|
+
"",
|
|
72
|
+
...evidence,
|
|
73
|
+
"",
|
|
74
|
+
"## Findings",
|
|
75
|
+
"",
|
|
76
|
+
...findings,
|
|
77
|
+
"",
|
|
78
|
+
"## Verdict",
|
|
79
|
+
"",
|
|
80
|
+
verdict
|
|
81
|
+
].join("\n");
|
|
82
|
+
assertCanonicalWrite(projectRoot, "record-artifact-review", [title, ...suppliedEvidence, body]);
|
|
83
|
+
repository.write(record, body);
|
|
84
|
+
return { review: repository.read("review", record.id), audit };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function recordArtifactReview(projectRoot, options = {}) {
|
|
88
|
+
const scrumDir = path.join(projectRoot, ".scrumrun");
|
|
89
|
+
return withArtifactLock(scrumDir, "review-artifact", () => recordArtifactReviewUnlocked(projectRoot, options));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { recordArtifactReview };
|