scrumrun 2.5.1 → 2.6.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 +21 -0
- package/CORE.md +18 -4
- package/DECISIONS.md +20 -0
- package/README.md +27 -3
- package/SPEC.md +9 -1
- package/bin/scrumrun.js +64 -11
- package/lib/commands/manifest.js +3 -3
- package/lib/commands/pretty-intake.js +4 -0
- package/lib/runtime/briefing.js +104 -0
- package/lib/runtime/canonical-snapshot.js +2 -1
- package/lib/runtime/context.js +18 -1
- package/lib/runtime/orchestrator.js +92 -18
- package/lib/runtime/policy-engine.js +22 -0
- package/lib/runtime/request-engine.js +22 -2
- package/lib/runtime/run-ledger.js +16 -0
- package/lib/v2/conformance.js +7 -0
- package/package.json +13 -3
- package/scripts/sync-readme-version.js +20 -0
- package/templates/project/.scrumrun/config.md +3 -0
- package/templates/project/AGENTS.md +6 -4
- package/templates/project-lean/AGENTS.md +3 -3
- package/templates/shared/skills/scrumrun/SKILL.md +17 -11
- package/types/index.d.ts +730 -0
|
@@ -15,11 +15,12 @@ const {
|
|
|
15
15
|
withArtifactLock
|
|
16
16
|
} = require("../v2/artifacts");
|
|
17
17
|
const { buildContextPackage } = require("./context");
|
|
18
|
-
const { evaluatePolicy } = require("./policy-engine");
|
|
18
|
+
const { agentIdentity, evaluatePolicy } = require("./policy-engine");
|
|
19
19
|
const { artifactSnapshot, canonicalFingerprint, canonicalWatchSnapshot } = require("./canonical-snapshot");
|
|
20
20
|
const { decodeApproval } = require("./request-engine");
|
|
21
21
|
const { extractLearningCandidates } = require("../code-intel/learning");
|
|
22
|
-
const { appendRunEvent, createRunBody, instant } = require("./run-ledger");
|
|
22
|
+
const { appendRunEvent, appendTechnicalSummary, createRunBody, instant } = require("./run-ledger");
|
|
23
|
+
const { generateBriefing } = require("./briefing");
|
|
23
24
|
const { policyState, prepareCompletion, publicWorkspace, verifyWorkspaceIntegrity, workspaceState } = require("./mutation-gateway");
|
|
24
25
|
const { recoverPendingTransactions, runKernelTransaction } = require("../v2/transaction");
|
|
25
26
|
|
|
@@ -48,16 +49,7 @@ function stateFingerprint(repository) {
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
function stateProjection(repository) {
|
|
51
|
-
|
|
52
|
-
const linesFor = (kinds) => kinds.flatMap((kind) => (snapshot.records[kind] || [])
|
|
53
|
-
.filter((record) => record.id && !TERMINAL.has(record.status))
|
|
54
|
-
.map((record) => `- ${record.id} | ${kind} | ${record.status} | ${record.title || record.id}`));
|
|
55
|
-
const work = linesFor(["feature", "task", "sprint", "run", "review"]);
|
|
56
|
-
const memory = linesFor(["decision", "knowledge", "insight", "dossier"]);
|
|
57
|
-
const sourceFingerprint = canonicalFingerprint(repository.scrumDir, snapshot.hashes);
|
|
58
|
-
const watch = canonicalWatchSnapshot(repository.scrumDir);
|
|
59
|
-
const content = `# ScrumRun State\n\nProjection schema: 1\nGenerated: ${new Date().toISOString()}\nSource fingerprint: ${sourceFingerprint}\nWatch fingerprint: ${watch.fingerprint}\nAuthority: none; rebuild from canonical artifacts.\n\n## Active Work\n\n${work.length ? work.join("\n") : "- No active canonical work."}\n\n## Relevant Memory\n\n${memory.length ? memory.join("\n") : "- No active canonical memory."}\n`;
|
|
60
|
-
return { content, sourceFingerprint, watchFingerprint: watch.fingerprint, sourceFiles: watch.files };
|
|
52
|
+
return generateBriefing(repository.scrumDir, repository);
|
|
61
53
|
}
|
|
62
54
|
|
|
63
55
|
function renderState(repository) {
|
|
@@ -122,6 +114,7 @@ function approveRequestUnlocked(projectRoot, token, { failurePoint = null, inter
|
|
|
122
114
|
method: METHOD_VERSION,
|
|
123
115
|
feature: null,
|
|
124
116
|
sprint: null,
|
|
117
|
+
assignee: agentIdentity(scrumDir) || "agent",
|
|
125
118
|
approval_id: approvalId,
|
|
126
119
|
context_fingerprint: payload.fingerprint,
|
|
127
120
|
risk: payload.risk.level
|
|
@@ -141,7 +134,8 @@ function approveRequestUnlocked(projectRoot, token, { failurePoint = null, inter
|
|
|
141
134
|
workspace: 1,
|
|
142
135
|
approval_id: approvalId
|
|
143
136
|
};
|
|
144
|
-
const
|
|
137
|
+
const previewSection = payload.preview ? `\n\n## Preview\n\n${payload.preview}\n` : "";
|
|
138
|
+
const taskBody = `# ${titleFor(payload.request)}\n\n## Request\n\n${payload.request}\n\n## Acceptance Criteria\n\n- [ ] _Define what "done" means before execution._\n${previewSection}\n\n## Classification\n\n- Type: ${payload.classification.type}\n- Reason: ${payload.classification.reason}\n- Risk: ${payload.risk.level}\n\n## Approval\n\n- Explicit approval token: ${approvalId}\n- Context fingerprint: ${payload.fingerprint}`;
|
|
145
139
|
const runBody = createRunBody(run, {
|
|
146
140
|
approvalId,
|
|
147
141
|
obligations: payload.policy.obligations || [],
|
|
@@ -169,7 +163,7 @@ function approveRequest(projectRoot, token, options = {}) {
|
|
|
169
163
|
return withArtifactLock(path.join(projectRoot, ".scrumrun"), "approval", () => approveRequestUnlocked(projectRoot, token, options));
|
|
170
164
|
}
|
|
171
165
|
|
|
172
|
-
function transitionedArtifactContent(content, kind, nextStatus, updated) {
|
|
166
|
+
function transitionedArtifactContent(content, kind, nextStatus, updated, assignee = null) {
|
|
173
167
|
const parsed = parseArtifact(content);
|
|
174
168
|
const errors = [...parsed.errors, ...validateArtifact(parsed.record, kind)];
|
|
175
169
|
if (errors.length) throw new Error(`Invalid ${kind} artifact: ${errors.join("; ")}`);
|
|
@@ -178,6 +172,11 @@ function transitionedArtifactContent(content, kind, nextStatus, updated) {
|
|
|
178
172
|
let frontmatter = frontmatterMatch[1]
|
|
179
173
|
.replace(/^status:\s*.*$/m, `status: ${nextStatus}`)
|
|
180
174
|
.replace(/^updated:\s*.*$/m, `updated: ${nextRecord.updated}`);
|
|
175
|
+
if (assignee) {
|
|
176
|
+
frontmatter = /^assignee:/m.test(frontmatter)
|
|
177
|
+
? frontmatter.replace(/^assignee:\s*.*$/m, `assignee: ${assignee}`)
|
|
178
|
+
: `${frontmatter}\nassignee: ${assignee}`;
|
|
179
|
+
}
|
|
181
180
|
const next = `---\n${frontmatter}\n---${frontmatterMatch[2]}`;
|
|
182
181
|
const validated = parseArtifact(next);
|
|
183
182
|
const nextErrors = [...validated.errors, ...validateArtifact(validated.record, kind)];
|
|
@@ -200,8 +199,9 @@ function transitionedRunContent(content, nextStatus, options = {}) {
|
|
|
200
199
|
};
|
|
201
200
|
}
|
|
202
201
|
|
|
203
|
-
function transitionRunUnlocked(projectRoot, runId, nextStatus, { note = null, evidence = [], actor = "agent", occurredAt = null, failurePoint = null, interruptPoint = null } = {}) {
|
|
202
|
+
function transitionRunUnlocked(projectRoot, runId, nextStatus, { note = null, evidence = [], actor = "agent", occurredAt = null, summary = null, failurePoint = null, interruptPoint = null } = {}) {
|
|
204
203
|
const scrumDir = path.join(projectRoot, ".scrumrun");
|
|
204
|
+
const effectiveActor = actor === "agent" ? (agentIdentity(scrumDir) || "agent") : actor;
|
|
205
205
|
const recovered = recoverPendingTransactions(scrumDir);
|
|
206
206
|
const repository = new ArtifactRepository(scrumDir);
|
|
207
207
|
const runArtifact = repository.read("run", runId);
|
|
@@ -229,11 +229,15 @@ function transitionRunUnlocked(projectRoot, runId, nextStatus, { note = null, ev
|
|
|
229
229
|
const taskPrevious = fs.readFileSync(taskArtifact.file, "utf8");
|
|
230
230
|
const prepared = nextStatus === "completed" ? prepareCompletion(projectRoot, runArtifact, { occurredAt }) : null;
|
|
231
231
|
const transitionSource = prepared ? serializeArtifact(prepared.record, prepared.body) : runPrevious;
|
|
232
|
-
const runNext = transitionedRunContent(transitionSource, nextStatus, { note, evidence, actor, occurredAt });
|
|
232
|
+
const runNext = transitionedRunContent(transitionSource, nextStatus, { note, evidence, actor: effectiveActor, occurredAt });
|
|
233
|
+
let runContent = runNext.content;
|
|
234
|
+
if (nextStatus === "completed" && summary) {
|
|
235
|
+
runContent = appendTechnicalSummary(runNext.record, runContent, summary);
|
|
236
|
+
}
|
|
233
237
|
const taskNext = transitionedArtifactContent(taskPrevious, "task", nextTaskStatus, runNext.record.updated);
|
|
234
238
|
try {
|
|
235
239
|
runKernelTransaction(scrumDir, "transition-run-task", [
|
|
236
|
-
{ file: runArtifact.file, previous: runPrevious, next:
|
|
240
|
+
{ file: runArtifact.file, previous: runPrevious, next: runContent },
|
|
237
241
|
{ file: taskArtifact.file, previous: taskPrevious, next: taskNext.content }
|
|
238
242
|
], { failurePoint, interruptPoint });
|
|
239
243
|
} catch (error) {
|
|
@@ -332,4 +336,74 @@ function retryTask(projectRoot, taskId, options = {}) {
|
|
|
332
336
|
return withArtifactLock(path.join(projectRoot, ".scrumrun"), `task-${String(taskId).toLowerCase()}`, () => retryTaskUnlocked(projectRoot, taskId, options));
|
|
333
337
|
}
|
|
334
338
|
|
|
335
|
-
|
|
339
|
+
function nextBacklogTask(repository) {
|
|
340
|
+
const artifact = repository.list("task")
|
|
341
|
+
.find((entry) => entry.record && !entry.errors.length && entry.record.status === "backlog");
|
|
342
|
+
return artifact ? artifact.record : null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function startBacklogTaskUnlocked(projectRoot, taskId, { note = "Backlog Task started.", failurePoint = null, interruptPoint = null } = {}) {
|
|
346
|
+
const scrumDir = path.join(projectRoot, ".scrumrun");
|
|
347
|
+
const recovered = recoverPendingTransactions(scrumDir);
|
|
348
|
+
const repository = new ArtifactRepository(scrumDir);
|
|
349
|
+
const taskArtifact = repository.read("task", taskId);
|
|
350
|
+
if (!taskArtifact) throw new Error(`Task not found: ${taskId}`);
|
|
351
|
+
if (taskArtifact.record.status !== "backlog") {
|
|
352
|
+
throw new Error(`Task ${taskId} is ${taskArtifact.record.status}; start requires backlog.`);
|
|
353
|
+
}
|
|
354
|
+
const identity = agentIdentity(scrumDir) || "agent";
|
|
355
|
+
const requestMatch = taskArtifact.body.match(/^## Request[ \t]*\r?\n\r?\n([\s\S]*?)(?=^## |(?![\s\S]))/m);
|
|
356
|
+
const request = requestMatch ? requestMatch[1].trim() : `Start ${taskId}`;
|
|
357
|
+
const context = buildContextPackage(projectRoot, request);
|
|
358
|
+
const policy = evaluatePolicy(context);
|
|
359
|
+
if (policy.status !== "passed") throw new Error(`Start is blocked by current policy: ${policy.violations.join("; ")}`);
|
|
360
|
+
const enforceablePolicy = policyState(projectRoot);
|
|
361
|
+
const baseline = publicWorkspace(workspaceState(projectRoot));
|
|
362
|
+
const run = {
|
|
363
|
+
id: nextId(repository, "run"),
|
|
364
|
+
kind: "run",
|
|
365
|
+
status: "executing",
|
|
366
|
+
created: date(),
|
|
367
|
+
updated: date(),
|
|
368
|
+
method: METHOD_VERSION,
|
|
369
|
+
task: taskId,
|
|
370
|
+
sprint: taskArtifact.record.sprint || null,
|
|
371
|
+
attempt: 1,
|
|
372
|
+
ledger: 1,
|
|
373
|
+
guardrails: 1,
|
|
374
|
+
workspace: 1,
|
|
375
|
+
approval_id: taskArtifact.record.approval_id || null
|
|
376
|
+
};
|
|
377
|
+
const runBody = createRunBody(run, {
|
|
378
|
+
title: `Run for ${taskId}`,
|
|
379
|
+
actor: identity,
|
|
380
|
+
reason: note,
|
|
381
|
+
evidence: [{ kind: "approval", summary: note }],
|
|
382
|
+
obligations: policy.obligations || [],
|
|
383
|
+
policyFingerprint: enforceablePolicy.fingerprint,
|
|
384
|
+
workspaceBaseline: baseline
|
|
385
|
+
}).body;
|
|
386
|
+
const runContent = serializeArtifact(run, runBody);
|
|
387
|
+
const taskPrevious = fs.readFileSync(taskArtifact.file, "utf8");
|
|
388
|
+
const taskNext = transitionedArtifactContent(taskPrevious, "task", "running", date(), identity);
|
|
389
|
+
try {
|
|
390
|
+
runKernelTransaction(scrumDir, "start-backlog-task", [
|
|
391
|
+
{ file: repository.pathFor(run), previous: null, next: runContent },
|
|
392
|
+
{ file: taskArtifact.file, previous: taskPrevious, next: taskNext.content }
|
|
393
|
+
], {
|
|
394
|
+
failurePoint: failurePoint === "after-run" ? "after-1" : failurePoint,
|
|
395
|
+
interruptPoint
|
|
396
|
+
});
|
|
397
|
+
} catch (error) {
|
|
398
|
+
if (failurePoint === "after-run") throw new Error(`Injected start failure after Run creation: ${error.message}`);
|
|
399
|
+
throw error;
|
|
400
|
+
}
|
|
401
|
+
refreshState(scrumDir);
|
|
402
|
+
return { run, task: taskNext.record, content: runContent, recovered };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function startBacklogTask(projectRoot, taskId, options = {}) {
|
|
406
|
+
return withArtifactLock(path.join(projectRoot, ".scrumrun"), `task-${String(taskId).toLowerCase()}`, () => startBacklogTaskUnlocked(projectRoot, taskId, options));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
module.exports = { approveRequest, nextBacklogTask, nextId, refreshState, renderState, retryTask, startBacklogTask, stateFingerprint, stateIsStale, transitionRun };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
3
5
|
const { containsSecret } = require("../security/secrets");
|
|
4
6
|
|
|
5
7
|
const GUARDRAIL_STATUSES = new Set(["active", "retired", "superseded"]);
|
|
@@ -92,6 +94,25 @@ function configFields(content) {
|
|
|
92
94
|
return fieldMap(String(content || ""));
|
|
93
95
|
}
|
|
94
96
|
|
|
97
|
+
function agentIdentity(scrumDir) {
|
|
98
|
+
if (process.env.SCRUMRUN_AGENT) {
|
|
99
|
+
const env = String(process.env.SCRUMRUN_AGENT).trim();
|
|
100
|
+
if (/^[A-Za-z0-9._-]{1,64}$/.test(env)) return env;
|
|
101
|
+
}
|
|
102
|
+
const configFile = path.join(scrumDir, "config.md");
|
|
103
|
+
if (!fs.existsSync(configFile) || !fs.lstatSync(configFile).isFile()) return null;
|
|
104
|
+
let content;
|
|
105
|
+
try {
|
|
106
|
+
content = fs.readFileSync(configFile, "utf8");
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const value = configFields(content).agent_identity;
|
|
111
|
+
if (!value) return null;
|
|
112
|
+
const clean = String(value).trim();
|
|
113
|
+
return /^[A-Za-z0-9._-]{1,64}$/.test(clean) ? clean : null;
|
|
114
|
+
}
|
|
115
|
+
|
|
95
116
|
function readOnlyPaths(content) {
|
|
96
117
|
const value = configFields(content).read_only_paths;
|
|
97
118
|
if (!value) return [];
|
|
@@ -255,6 +276,7 @@ function normalizeGuardrailDocument(content) {
|
|
|
255
276
|
}
|
|
256
277
|
|
|
257
278
|
module.exports = {
|
|
279
|
+
agentIdentity,
|
|
258
280
|
configWeakeningAttempts,
|
|
259
281
|
evaluatePolicy,
|
|
260
282
|
inferEnforcement,
|
|
@@ -73,6 +73,7 @@ function encodeApproval(plan) {
|
|
|
73
73
|
risk: plan.risk,
|
|
74
74
|
policy: plan.policy,
|
|
75
75
|
workspaceFingerprint: plan.workspaceFingerprint,
|
|
76
|
+
preview: plan.preview || null,
|
|
76
77
|
issuedAt: plan.issuedAt
|
|
77
78
|
};
|
|
78
79
|
const encoded = Buffer.from(stableJson(payload)).toString("base64url");
|
|
@@ -95,7 +96,15 @@ function decodeApproval(token) {
|
|
|
95
96
|
return payload;
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
|
|
99
|
+
const TYPE_MAP = {
|
|
100
|
+
fix: { type: "fix", taskType: "fix" },
|
|
101
|
+
task: { type: "task", taskType: "task" },
|
|
102
|
+
feature: { type: "feature", taskType: "feature" },
|
|
103
|
+
docs: { type: "task", taskType: "docs" },
|
|
104
|
+
discovery: { type: "discovery", taskType: "discovery" }
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
function planRequest(projectRoot, request, { typeOverride = null, preview = null } = {}) {
|
|
99
108
|
const normalized = String(request || "").trim();
|
|
100
109
|
if (!normalized) throw new Error("A non-empty request is required for intake.");
|
|
101
110
|
if (normalized.length > 10000) throw new Error("Request exceeds the 10,000 character intake limit.");
|
|
@@ -103,7 +112,17 @@ function planRequest(projectRoot, request) {
|
|
|
103
112
|
const workspaceFingerprint = workspaceState(projectRoot).fingerprint;
|
|
104
113
|
const policy = applyPolicy(context);
|
|
105
114
|
const risk = assessRisk(normalized);
|
|
106
|
-
|
|
115
|
+
let classification = classifyRequest(normalized);
|
|
116
|
+
if (typeOverride) {
|
|
117
|
+
const override = TYPE_MAP[typeOverride];
|
|
118
|
+
if (!override) throw new Error(`Invalid --type: ${typeOverride}. Valid: ${Object.keys(TYPE_MAP).join(", ")}.`);
|
|
119
|
+
classification = { type: override.type, taskType: override.taskType, reason: "explicitly specified by agent" };
|
|
120
|
+
}
|
|
121
|
+
let safePreview = null;
|
|
122
|
+
if (preview) {
|
|
123
|
+
if (containsSecret(preview)) throw new Error("Preview contains secret-like content.");
|
|
124
|
+
safePreview = String(preview).trim().slice(0, 2000);
|
|
125
|
+
}
|
|
107
126
|
const plan = {
|
|
108
127
|
state: policy.status === "passed" ? "awaiting_approval" : "blocked",
|
|
109
128
|
pipeline: ["received", "contextualizing", "policy", "risk", "classification", "planning", policy.status === "passed" ? "awaiting_approval" : "blocked"],
|
|
@@ -113,6 +132,7 @@ function planRequest(projectRoot, request) {
|
|
|
113
132
|
policy,
|
|
114
133
|
risk,
|
|
115
134
|
classification,
|
|
135
|
+
preview: safePreview,
|
|
116
136
|
proposal: {
|
|
117
137
|
create: ["Task", "Run"],
|
|
118
138
|
sprint: classification.type === "sprint" ? "propose only after confirming real batch/timebox membership" : null,
|
|
@@ -461,6 +461,20 @@ function appendRunEvent(record, body, nextRecord, {
|
|
|
461
461
|
return { body: nextBody, event, events: validated.events };
|
|
462
462
|
}
|
|
463
463
|
|
|
464
|
+
function appendTechnicalSummary(record, body, summary) {
|
|
465
|
+
const summaryText = String(summary || "").trim();
|
|
466
|
+
if (!summaryText) return body;
|
|
467
|
+
if (containsSecret(summaryText)) throw new Error("Technical summary contains secret-like content.");
|
|
468
|
+
const validated = validateRunLedger(record, body);
|
|
469
|
+
if (validated.errors.length) throw new Error(`Cannot append technical summary to invalid Run ledger: ${validated.errors.join("; ")}`);
|
|
470
|
+
return `${String(body).trimEnd()}\n\n## Technical Summary\n\n${summaryText}\n`;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function extractTechnicalSummary(body) {
|
|
474
|
+
const match = String(body || "").match(/^## Technical Summary[ \t]*\r?\n\r?\n([\s\S]*?)(?=^## |(?![\s\S]))/m);
|
|
475
|
+
return match ? match[1].trim() : null;
|
|
476
|
+
}
|
|
477
|
+
|
|
464
478
|
function legacyTransitionEvents(record, body, legacyHash) {
|
|
465
479
|
const raw = [];
|
|
466
480
|
const log = String(body || "").match(/^## Transition Log[ \t]*\r?\n([\s\S]*?)(?=^## |(?![\s\S]))/m);
|
|
@@ -531,11 +545,13 @@ module.exports = {
|
|
|
531
545
|
appendGuardrailEvent,
|
|
532
546
|
appendMutationEvent,
|
|
533
547
|
appendRunEvent,
|
|
548
|
+
appendTechnicalSummary,
|
|
534
549
|
createRunBody,
|
|
535
550
|
dateInstant,
|
|
536
551
|
eventId,
|
|
537
552
|
eventsSection,
|
|
538
553
|
evidenceForTransition,
|
|
554
|
+
extractTechnicalSummary,
|
|
539
555
|
guardrailState,
|
|
540
556
|
instant,
|
|
541
557
|
makeRunEvent,
|
package/lib/v2/conformance.js
CHANGED
|
@@ -213,6 +213,13 @@ function auditProject(projectRoot) {
|
|
|
213
213
|
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));
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
|
+
for (const task of records.task || []) {
|
|
217
|
+
if (!task.record || task.errors.length) continue;
|
|
218
|
+
if (["backlog", "proposed", "cancelled"].includes(task.record.status)) continue;
|
|
219
|
+
if (!/^## Acceptance Criteria\b/m.test(task.body || "")) {
|
|
220
|
+
findings.push(finding("warning", "ACCEPTANCE_CRITERIA_MISSING", `${task.record.id} has no Acceptance Criteria section; define what "done" means before execution.`, task.file));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
216
223
|
for (const sprint of records.sprint || []) {
|
|
217
224
|
if (!sprint.record || sprint.errors.length) continue;
|
|
218
225
|
const heading = /^## Tasks[ \t]*$/m.exec(sprint.body);
|
package/package.json
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scrumrun",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.1",
|
|
4
4
|
"description": "Evidence-driven Agile runtime and semantic project memory for AI coding agents.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"scrumrun": "bin/scrumrun.js",
|
|
7
7
|
"sr-claude": "bin/claude-install.js"
|
|
8
8
|
},
|
|
9
|
+
"types": "types/index.d.ts",
|
|
9
10
|
"files": [
|
|
10
11
|
"bin",
|
|
11
12
|
"lib",
|
|
13
|
+
"types",
|
|
12
14
|
"scripts",
|
|
13
15
|
"templates",
|
|
14
16
|
"CORE.md",
|
|
@@ -24,7 +26,10 @@
|
|
|
24
26
|
"docs:contract": "node scripts/generate-contract-docs.js",
|
|
25
27
|
"check:contract": "node scripts/generate-contract-docs.js --check",
|
|
26
28
|
"test": "node scripts/generate-contract-docs.js --check && node --test tests/*.test.js",
|
|
27
|
-
"benchmark": "node --test tests/performance.test.js"
|
|
29
|
+
"benchmark": "node --test tests/performance.test.js",
|
|
30
|
+
"coverage": "c8 --reporter=text --reporter=lcov node --test tests/*.test.js",
|
|
31
|
+
"lint": "eslint .",
|
|
32
|
+
"prepublishOnly": "node scripts/sync-readme-version.js && npm test"
|
|
28
33
|
},
|
|
29
34
|
"keywords": [
|
|
30
35
|
"ai",
|
|
@@ -54,5 +59,10 @@
|
|
|
54
59
|
"license": "MIT",
|
|
55
60
|
"engines": {
|
|
56
61
|
"node": ">=22.13.0"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@eslint/js": "^10.0.1",
|
|
65
|
+
"c8": "^12.0.0",
|
|
66
|
+
"eslint": "^10.8.1"
|
|
57
67
|
}
|
|
58
|
-
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const fs = require("node:fs");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
|
|
8
|
+
const root = path.resolve(__dirname, "..");
|
|
9
|
+
const { version } = require(path.join(root, "package.json"));
|
|
10
|
+
const readmeFile = path.join(root, "README.md");
|
|
11
|
+
|
|
12
|
+
const readme = fs.readFileSync(readmeFile, "utf8");
|
|
13
|
+
const updated = readme.replace(/(\*\*Package:\*\* `)(\d+\.\d+\.\d+)(`)/, `$1${version}$3`);
|
|
14
|
+
|
|
15
|
+
if (updated === readme) {
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
fs.writeFileSync(readmeFile, updated);
|
|
20
|
+
console.log(`Synced README badge to ${version}`);
|
|
@@ -6,5 +6,8 @@ Language: English
|
|
|
6
6
|
Interaction Mode: guided
|
|
7
7
|
Execution Approval: always
|
|
8
8
|
Quick Tasks: ask
|
|
9
|
+
Agent Identity: agent
|
|
9
10
|
|
|
10
11
|
These are operating preferences. They can never weaken `.scrumrun/guardrails.md`.
|
|
12
|
+
|
|
13
|
+
`Agent Identity` is the default agent name recorded as the Task `assignee` and Run event `actor`. In shared teams, prefer the per-agent `SCRUMRUN_AGENT` environment variable over this project-wide default.
|
|
@@ -7,19 +7,21 @@ This project uses ScrumRun. The method is mandatory; `/sc` is its single optiona
|
|
|
7
7
|
For normal work, read:
|
|
8
8
|
|
|
9
9
|
1. `.scrumrun/guardrails.md` — canonical project policy;
|
|
10
|
-
2. `.scrumrun/state.md` —
|
|
11
|
-
3. the referenced Task, Sprint, Feature, Run, Memory, and Review artifacts relevant to the request;
|
|
10
|
+
2. `.scrumrun/state.md` — the briefing: active work, recent completions, open decisions, active memory, backlog queue, and pointers;
|
|
11
|
+
3. the referenced Task, Sprint, Feature, Run, Memory, and Review artifacts relevant to the request (go deeper only when the briefing lacks what you need);
|
|
12
12
|
4. `.scrumrun/core.md` when the method contract or an exceptional transition is needed.
|
|
13
13
|
|
|
14
14
|
Natural-language product requests automatically enter the read-only ScrumRun intake pipeline. Before explicit approval, do not create canonical records or modify application code.
|
|
15
15
|
|
|
16
16
|
After approval:
|
|
17
17
|
|
|
18
|
-
- Task is the atomic work item;
|
|
18
|
+
- Task is the atomic work item; define its `## Acceptance Criteria` before execution;
|
|
19
19
|
- Sprint is only a real timebox/batch of Tasks;
|
|
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
|
+
- record a `## Technical Summary` at completion so the next agent inherits what was done;
|
|
23
|
+
- when a Run completes and work remains queued, surface it with `sc plan task --next` and start it with `sc plan task --start` — starting is explicit approval;
|
|
24
|
+
- learning proposes evidence-backed Knowledge, Decisions, or candidate Insights;
|
|
23
25
|
- every application/source edit requires a path-scoped Mutation Gateway permit, immediate hash recording, and resolution of the Run's Guardrail obligations before completion.
|
|
24
26
|
|
|
25
27
|
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.
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
This project stores the complete ScrumRun v2 truth but uses a bounded default read path:
|
|
6
6
|
|
|
7
7
|
1. `.scrumrun/guardrails.md`;
|
|
8
|
-
2. `.scrumrun/state.md
|
|
9
|
-
3. only canonical artifacts referenced by
|
|
8
|
+
2. `.scrumrun/state.md` — the briefing: active work, recent completions, open decisions, active memory, backlog queue, and pointers;
|
|
9
|
+
3. only canonical artifacts referenced by the briefing's pointers (go deeper only when the briefing lacks what you need);
|
|
10
10
|
4. `.scrumrun/core.md` only when method details are needed.
|
|
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 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.
|
|
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. Define the Task's `## Acceptance Criteria` before execution and record a `## Technical Summary` at completion. When work remains queued, surface it with `sc plan task --next` and start it with `sc plan task --start`.
|
|
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
17
|
|
|
@@ -32,8 +32,8 @@ Normal hot path:
|
|
|
32
32
|
1. read `.scrumrun/method.json` — its `paths` block is the authoritative index of every canonical location; navigate by that index and never grep for legacy paths (`goals/`, `backlog.md`, `sprint.md`, `history.md`);
|
|
33
33
|
2. read `AGENTS.md`;
|
|
34
34
|
3. read `.scrumrun/guardrails.md`;
|
|
35
|
-
4. read `.scrumrun/state.md
|
|
36
|
-
5. follow the
|
|
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`, `sc knowledge study "<topic>"`);
|
|
37
37
|
6. load `.scrumrun/core.md` when the method contract or an exceptional transition is needed.
|
|
38
38
|
|
|
39
39
|
**Never write Run events by hand.** Only the CLI mutates `runs/RUN-NNN.md`: `sc plan run --validate | --learn | --complete | --resume | --fail | --block | --satisfy-guardrail | --authorize-mutation | --record-mutation`. Direct edits produce invalid ledger events (unknown `type` like `execution`/`validation`/`learning`, unknown evidence `kind` like `guardrail-check`/`build`/`task-status`, missing snapshot invariant) and break project conformance. If the CLI does not expose the shape you need, propose a spec change instead of inventing vocabulary. Recover hand-written Runs via `sc plan run --normalize-legacy` — originals are preserved byte-exact under `.scrumrun/.migration-backup/runs/`.
|
|
@@ -81,6 +81,8 @@ At intake:
|
|
|
81
81
|
|
|
82
82
|
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
83
|
|
|
84
|
+
Assert the classification explicitly when the keyword inference is wrong: `sc 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
|
+
|
|
84
86
|
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.
|
|
85
87
|
|
|
86
88
|
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.
|
|
@@ -98,15 +100,19 @@ During execution:
|
|
|
98
100
|
|
|
99
101
|
1. keep the change inside the approved Task scope;
|
|
100
102
|
2. preserve existing owner work and unrelated dirty files;
|
|
101
|
-
3.
|
|
102
|
-
4.
|
|
103
|
-
5.
|
|
104
|
-
6.
|
|
105
|
-
7.
|
|
106
|
-
8.
|
|
103
|
+
3. define or confirm the Task's `## Acceptance Criteria` before execution and check them off as evidence;
|
|
104
|
+
4. 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;
|
|
105
|
+
5. validate in proportion to risk and against the acceptance criteria;
|
|
106
|
+
6. record exactly one structured `RUN-NNN-EVT-NNN` event per state transition, with RFC3339 time, actor, reason, and typed evidence;
|
|
107
|
+
7. run configured reviewers;
|
|
108
|
+
8. extract candidate learning only after validation;
|
|
109
|
+
9. record a `## Technical Summary` at completion (`sc plan run --complete --summary "…"`) so the next agent inherits what was actually done;
|
|
110
|
+
10. complete the Run, then Task, and only then the Sprint when its whole batch is done.
|
|
107
111
|
|
|
108
112
|
Never overwrite a prior attempt. Never mark work complete because time/token budget ended.
|
|
109
113
|
|
|
114
|
+
When a Run completes and work remains queued, the briefing's `## Next Up` names the next backlog Task. Surface it with `sc plan task --next` and start it with `sc 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`.
|
|
115
|
+
|
|
110
116
|
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.
|
|
111
117
|
|
|
112
118
|
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.
|
|
@@ -159,11 +165,11 @@ Dry-run must not write project data. Apply requires a hashed inventory, byte-exa
|
|
|
159
165
|
|
|
160
166
|
### `/sc plan`
|
|
161
167
|
|
|
162
|
-
- `task`: add/list/show/run/audit/cancel/retry atomic work; use `type: fix` for fixes and `status: backlog` for parked work.
|
|
168
|
+
- `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.
|
|
163
169
|
- `sprint`: add/list/show/start/complete/block a real Task batch/timebox.
|
|
164
170
|
- `feature`: add/list/show/activate/complete long-lived initiatives.
|
|
165
|
-
- `run`: list/show/authorize-mutation/record-mutation/satisfy-guardrail/validate/learn/complete/resume/fail/block concrete Task attempts.
|
|
166
|
-
- `intake <request>`: execute the read-only request pipeline
|
|
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.
|
|
172
|
+
- `intake <request>`: execute the read-only request pipeline; accepts `--type fix|task|feature|docs|discovery` and `--preview "…"`.
|
|
167
173
|
- `challenge <question>`: deep read-only analysis with evidence, risks, options, and recommendation.
|
|
168
174
|
|
|
169
175
|
### `/sc knowledge`
|