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.
@@ -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
- const snapshot = artifactSnapshot(repository.scrumDir);
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 taskBody = `# ${titleFor(payload.request)}\n\n## Request\n\n${payload.request}\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}`;
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: runNext.content },
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
- module.exports = { approveRequest, nextId, refreshState, renderState, retryTask, stateFingerprint, stateIsStale, transitionRun };
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
- function planRequest(projectRoot, request) {
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
- const classification = classifyRequest(normalized);
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,
@@ -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.5.1",
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` — disposable index of active ids;
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
- - learning proposes evidence-backed Knowledge, Decisions, or candidate Insights.
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 active ids;
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 ids/pointers to only the relevant canonical artifacts;
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. 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;
102
- 4. validate in proportion to risk;
103
- 5. record exactly one structured `RUN-NNN-EVT-NNN` event per state transition, with RFC3339 time, actor, reason, and typed evidence;
104
- 6. run configured reviewers;
105
- 7. extract candidate learning only after validation;
106
- 8. complete the Run, then Task, and only then the Sprint when its whole batch is done.
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`