scrumrun 2.7.11 → 3.0.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 +17 -0
- package/CORE.md +12 -12
- package/README.md +50 -35
- package/SPEC.md +6 -6
- package/bin/scrumrun.js +47 -35
- package/docs/COMMANDS.md +38 -37
- package/docs/DEMO.md +6 -6
- package/docs/ERROR-CODES.md +11 -11
- package/docs/QUICKSTART.md +14 -14
- package/docs/RELEASE-SCORECARD.md +1 -1
- package/docs/RELEASE.md +1 -1
- package/docs/SEMANTIC-MEMORY.md +3 -3
- package/docs/TROUBLESHOOTING.md +9 -9
- package/lib/commands/manifest.js +1 -0
- package/lib/commands/normalize-legacy.js +1 -1
- package/lib/commands/pretty-intake.js +2 -2
- package/lib/commands/render.js +9 -5
- package/lib/commands/repair.js +61 -8
- package/lib/errors.js +11 -11
- package/lib/runtime/briefing.js +2 -2
- package/lib/runtime/mutation-gateway.js +74 -0
- package/lib/runtime/orchestrator.js +94 -3
- package/lib/v2/conformance.js +13 -3
- package/package.json +1 -1
- package/templates/project/AGENTS.md +7 -5
- package/templates/shared/skills/scrumrun/SKILL.md +27 -29
package/lib/errors.js
CHANGED
|
@@ -31,44 +31,44 @@ const CATALOG = Object.freeze({
|
|
|
31
31
|
"SR-E-052": { summary: "Approval token expired.", remediation: "Re-run intake to obtain a fresh token before approving." },
|
|
32
32
|
|
|
33
33
|
// Runs and ledger
|
|
34
|
-
"SR-E-100": { summary: "Run not found.", remediation: "Check the RUN-NNN id with `
|
|
35
|
-
"SR-E-101": { summary: "Run ledger contains an invalid event.", remediation: "Inspect the reported event id; use `
|
|
34
|
+
"SR-E-100": { summary: "Run not found.", remediation: "Check the RUN-NNN id with `scrumrun plan run --list`." },
|
|
35
|
+
"SR-E-101": { summary: "Run ledger contains an invalid event.", remediation: "Inspect the reported event id; use `scrumrun plan run --render` for a human view or restore from git history." },
|
|
36
36
|
"SR-E-102": { summary: "Run transition rejected: missing validation or learning evidence.", remediation: "Complete `--validate` and `--learn` with typed evidence before `--complete`." },
|
|
37
37
|
"SR-E-103": { summary: "Retry rejected because the previous Run is still active.", remediation: "Complete, fail, or block the current Run before creating a retry." },
|
|
38
38
|
|
|
39
39
|
// Guardrails
|
|
40
40
|
"SR-E-150": { summary: "Guardrail check blocked the operation.", remediation: "Read the reported GR-NNN, satisfy or retire it explicitly; guardrails never bypass silently." },
|
|
41
|
-
"SR-E-151": { summary: "Guardrail obligation is still pending.", remediation: "Resolve each `pending guardrail` via `
|
|
41
|
+
"SR-E-151": { summary: "Guardrail obligation is still pending.", remediation: "Resolve each `pending guardrail` via `scrumrun plan run --satisfy-guardrail` before completing the Run." },
|
|
42
42
|
"SR-E-152": { summary: "Guardrail declaration is malformed.", remediation: "Every active guardrail requires Status, Enforcement, Scope, and Rule fields; check .scrumrun/guardrails.md." },
|
|
43
43
|
|
|
44
44
|
// Edit permits (Mutation Gateway)
|
|
45
|
-
"SR-E-200": { summary: "No edit permit for this path.", remediation: "Request one with `
|
|
45
|
+
"SR-E-200": { summary: "No edit permit for this path.", remediation: "Request one with `scrumrun plan run --authorize-mutation RUN-NNN --path <path>` before editing canonical or source files." },
|
|
46
46
|
"SR-E-201": { summary: "Edit permit expired.", remediation: "Permits last 15 minutes. Authorize a new one and record the change immediately." },
|
|
47
47
|
"SR-E-202": { summary: "Edit permit path scope mismatch.", remediation: "The permit does not cover the modified path. Request a new permit that lists it." },
|
|
48
48
|
"SR-E-203": { summary: "File hash changed unexpectedly since the permit was issued.", remediation: "Someone else modified the file. Re-plan the change and request a fresh permit." },
|
|
49
49
|
|
|
50
50
|
// Transactions and recovery
|
|
51
|
-
"SR-E-250": { summary: "Pending kernel transaction cannot be recovered automatically.", remediation: "Run `
|
|
51
|
+
"SR-E-250": { summary: "Pending kernel transaction cannot be recovered automatically.", remediation: "Run `scrumrun config doctor --recover --dry-run` to preview; if it shows 'would overwrite owner changes', reconcile the file manually before applying." },
|
|
52
52
|
"SR-E-251": { summary: "Journal fails integrity check.", remediation: "Inspect .scrumrun/.transactions/pending. Do not delete; contact support or restore from backup." },
|
|
53
53
|
|
|
54
54
|
// Memory
|
|
55
|
-
"SR-E-300": { summary: "Memory candidate rejected: missing resolvable evidence.", remediation: "Attach at least one --evidence path or `
|
|
55
|
+
"SR-E-300": { summary: "Memory candidate rejected: missing resolvable evidence.", remediation: "Attach at least one --evidence path or `scrumrun knowledge <subject> --propose` before `--confirm`." },
|
|
56
56
|
"SR-E-301": { summary: "Attempt to write into vault via canonical channel.", remediation: "vault.local.md is local-only and never indexed. Edit the file directly." },
|
|
57
57
|
|
|
58
58
|
// Semantic index / code intel
|
|
59
|
-
"SR-E-350": { summary: "Semantic index is stale.", remediation: "Rebuild with `
|
|
60
|
-
"SR-E-351": { summary: "Search backend advertised in the cache does not match this runtime.", remediation: "Delete .scrumrun/.cache/semantic-index.sqlite and re-run any `
|
|
59
|
+
"SR-E-350": { summary: "Semantic index is stale.", remediation: "Rebuild with `scrumrun knowledge map --build`. Cache is disposable; canonical memory is unaffected." },
|
|
60
|
+
"SR-E-351": { summary: "Search backend advertised in the cache does not match this runtime.", remediation: "Delete .scrumrun/.cache/semantic-index.sqlite and re-run any `scrumrun knowledge` query to rebuild against the current runtime." },
|
|
61
61
|
|
|
62
62
|
// Migration
|
|
63
|
-
"SR-E-400": { summary: "Migration preflight failed.", remediation: "Run `
|
|
63
|
+
"SR-E-400": { summary: "Migration preflight failed.", remediation: "Run `scrumrun update` (dry) to see blockers, resolve them, then apply with `--migrate`." },
|
|
64
64
|
"SR-E-401": { summary: "Migration rollback requested but no backup was found.", remediation: "Rollback needs the ignored byte-exact backup created during --migrate. Restore from version control if the backup is gone." },
|
|
65
65
|
|
|
66
66
|
// Conformance / doctor
|
|
67
67
|
"SR-E-450": { summary: "Conformance check failed.", remediation: "The reported invariant identifies the exact violation; the message includes the file and expected shape." },
|
|
68
|
-
"SR-E-451": { summary: "Installed client asset is stale.", remediation: "Re-run `
|
|
68
|
+
"SR-E-451": { summary: "Installed client asset is stale.", remediation: "Re-run `scrumrun update` for the specific client. `doctor --strict` shows which files diverge." },
|
|
69
69
|
|
|
70
70
|
// Configuration / install
|
|
71
|
-
"SR-E-500": { summary: "ScrumRun project not initialized.", remediation: "Run `
|
|
71
|
+
"SR-E-500": { summary: "ScrumRun project not initialized.", remediation: "Run `scrumrun init` in the repository root." },
|
|
72
72
|
"SR-E-501": { summary: "Unsupported Node.js runtime.", remediation: "ScrumRun requires Node.js >=22.13.0 for native SQLite. Upgrade Node and retry." }
|
|
73
73
|
});
|
|
74
74
|
|
package/lib/runtime/briefing.js
CHANGED
|
@@ -97,8 +97,8 @@ ${backlogTasks.length ? backlogTasks.join("\n") : "- No backlog Tasks."}
|
|
|
97
97
|
- Guardrails: .scrumrun/guardrails.md
|
|
98
98
|
- Tasks: .scrumrun/tasks/ (${counts.task || 0} total)
|
|
99
99
|
- Runs: .scrumrun/runs/ (${counts.run || 0} total)
|
|
100
|
-
- Semantic:
|
|
101
|
-
- Full state: rebuild with
|
|
100
|
+
- Semantic: scrumrun knowledge study "<topic>" for deep queries
|
|
101
|
+
- Full state: rebuild with scrumrun config doctor --recover
|
|
102
102
|
`;
|
|
103
103
|
|
|
104
104
|
return { content, sourceFingerprint, watchFingerprint: watch.fingerprint, sourceFiles: watch.files };
|
|
@@ -419,9 +419,82 @@ function prepareCompletion(projectRoot, runArtifact, options = {}) {
|
|
|
419
419
|
return { record, body, resolved, status: "passed" };
|
|
420
420
|
}
|
|
421
421
|
|
|
422
|
+
// The normal v3 path is session based: an agent works freely after approval
|
|
423
|
+
// and the kernel verifies the complete workspace delta once, at completion.
|
|
424
|
+
// The older permit chain remains available for strict teams, but is no longer
|
|
425
|
+
// required to produce trustworthy evidence for ordinary work.
|
|
426
|
+
function prepareSessionCompletion(projectRoot, runArtifact, options = {}) {
|
|
427
|
+
if (runArtifact.record.guardrails !== 1 || runArtifact.record.workspace !== 1) {
|
|
428
|
+
return { record: runArtifact.record, body: runArtifact.body, changes: [], resolved: [], status: "legacy" };
|
|
429
|
+
}
|
|
430
|
+
const policy = policyState(projectRoot);
|
|
431
|
+
assertPolicyBound(runArtifact, policy);
|
|
432
|
+
const baseline = expectedWorkspace(runArtifact);
|
|
433
|
+
if (!baseline) throw new Error(`${runArtifact.record.id} has no workspace baseline; retry the Task before finalizing.`);
|
|
434
|
+
const actual = workspaceState(projectRoot);
|
|
435
|
+
const changes = changesBetween(baseline, actual);
|
|
436
|
+
const readOnly = readOnlyPaths(policy.config).map((value) => normalizedRelative(projectRoot, value));
|
|
437
|
+
const protectedChanges = changes.filter((change) => readOnly.some((item) => change.path === item || change.path.startsWith(`${item}/`)));
|
|
438
|
+
if (protectedChanges.length) throw new Error(`READ_ONLY_PATH: final workspace check found changes in ${protectedChanges.map((item) => item.path).join(", ")}.`);
|
|
439
|
+
const unsafeKinds = changes.filter((change) => !["file", "missing", "clean"].includes(change.after_kind));
|
|
440
|
+
if (unsafeKinds.length) throw new Error(`UNSAFE_MUTATION_TYPE: final workspace check found ${unsafeKinds.map((item) => item.path).join(", ")}.`);
|
|
441
|
+
const unscannable = changes.filter((change) => change.after_kind === "file" && change.after_scan !== "text");
|
|
442
|
+
if (unscannable.length) throw new Error(`UNSCANNABLE_MUTATION: final workspace check cannot inspect ${unscannable.map((item) => item.path).join(", ")}.`);
|
|
443
|
+
const leaked = changes.filter((change) => change.new_secret_fingerprints.length);
|
|
444
|
+
if (leaked.length) throw new Error(`SECRET_BOUNDARY: final workspace check found secret-like content in ${leaked.map((item) => item.path).join(", ")}.`);
|
|
445
|
+
|
|
446
|
+
let record = runArtifact.record;
|
|
447
|
+
let body = runArtifact.body;
|
|
448
|
+
if (changes.length) {
|
|
449
|
+
const appended = appendMutationEvent(record, body, {
|
|
450
|
+
mutation_id: `MUT-${sha256(`${runArtifact.record.id}:${baseline.fingerprint}:${actual.fingerprint}`).slice(0, 16)}`,
|
|
451
|
+
paths: changes.map((change) => change.path).sort(),
|
|
452
|
+
changes: changes.map(({ new_secret_fingerprints, ...change }) => change),
|
|
453
|
+
workspace_before: baseline.fingerprint,
|
|
454
|
+
workspace_after: publicWorkspace(actual),
|
|
455
|
+
policy_fingerprint: policy.fingerprint
|
|
456
|
+
}, {
|
|
457
|
+
note: options.note || `Final session audit verified ${changes.length} workspace change(s).`,
|
|
458
|
+
actor: options.actor || "agent",
|
|
459
|
+
evidence: [{ kind: "mutation", summary: `${changes.length} changed path(s) passed the final workspace audit.` }]
|
|
460
|
+
});
|
|
461
|
+
record = appended.record;
|
|
462
|
+
body = appended.body;
|
|
463
|
+
}
|
|
464
|
+
const resolved = [];
|
|
465
|
+
const repository = new ArtifactRepository(path.join(projectRoot, ".scrumrun"));
|
|
466
|
+
const supplied = options.guardrailEvidence || {};
|
|
467
|
+
const automatic = new Set(["builtin:owner-work", "builtin:read-only-path", "builtin:secret-boundary"]);
|
|
468
|
+
for (const obligation of guardrailState(record, body).filter((item) => item.status !== "passed")) {
|
|
469
|
+
const evidence = automatic.has(obligation.enforcement)
|
|
470
|
+
? [{ kind: "mutation", summary: "Verified by the final workspace audit." }]
|
|
471
|
+
: Array.isArray(supplied[obligation.guardrail]) ? supplied[obligation.guardrail] : [];
|
|
472
|
+
if (!evidence.length) {
|
|
473
|
+
throw new Error(`GUARDRAILS_PENDING: ${obligation.guardrail} needs final evidence in the Task's ## Guardrail Evidence section.`);
|
|
474
|
+
}
|
|
475
|
+
assertGateEvidence(projectRoot, repository, requiredEvidenceKind(obligation.enforcement), evidence, obligation.guardrail);
|
|
476
|
+
const appended = appendGuardrailEvent(record, body, obligation, "passed", {
|
|
477
|
+
note: `${obligation.guardrail} passed at the final session checkpoint.`,
|
|
478
|
+
actor: "finalize",
|
|
479
|
+
occurredAt: options.occurredAt,
|
|
480
|
+
evidence
|
|
481
|
+
});
|
|
482
|
+
record = appended.record;
|
|
483
|
+
body = appended.body;
|
|
484
|
+
resolved.push(obligation.guardrail);
|
|
485
|
+
}
|
|
486
|
+
const transactions = pendingTransactionStatus(path.join(projectRoot, ".scrumrun"));
|
|
487
|
+
if (transactions.error || transactions.pending.length) throw new Error(`TRANSACTION_GATE: ${transactions.error || "pending canonical transaction"}.`);
|
|
488
|
+
return { record, body, changes, resolved, status: "passed", policy: policy.fingerprint };
|
|
489
|
+
}
|
|
490
|
+
|
|
422
491
|
function auditActiveWorkspace(projectRoot, runArtifact) {
|
|
423
492
|
try {
|
|
424
493
|
if (!runArtifact.record || runArtifact.record.workspace !== 1 || ["completed", "failed", "blocked"].includes(runArtifact.record.status)) return null;
|
|
494
|
+
// Session Runs intentionally accumulate unrecorded workspace changes until
|
|
495
|
+
// their single final checkpoint. Those changes are audited by --finalize,
|
|
496
|
+
// not reported as a strict-mode bypass while work is still in progress.
|
|
497
|
+
if (runArtifact.record.execution === "session") return null;
|
|
425
498
|
verifyWorkspaceIntegrity(projectRoot, runArtifact);
|
|
426
499
|
return null;
|
|
427
500
|
} catch (error) {
|
|
@@ -440,6 +513,7 @@ module.exports = {
|
|
|
440
513
|
permitsOverlap,
|
|
441
514
|
policyState,
|
|
442
515
|
prepareCompletion,
|
|
516
|
+
prepareSessionCompletion,
|
|
443
517
|
publicWorkspace,
|
|
444
518
|
recordMutation,
|
|
445
519
|
satisfyGuardrail,
|
|
@@ -23,7 +23,7 @@ const { currentBranch } = require("./workspace-state");
|
|
|
23
23
|
const { extractLearningCandidates } = require("../code-intel/learning");
|
|
24
24
|
const { appendRunEvent, appendTechnicalSummary, createRunBody, instant } = require("./run-ledger");
|
|
25
25
|
const { generateBriefing, generateErrorsReport } = require("./briefing");
|
|
26
|
-
const { assertCanonicalWrite, policyState, prepareCompletion, publicWorkspace, verifyWorkspaceIntegrity, workspaceState } = require("./mutation-gateway");
|
|
26
|
+
const { assertCanonicalWrite, policyState, prepareCompletion, prepareSessionCompletion, publicWorkspace, verifyWorkspaceIntegrity, workspaceState } = require("./mutation-gateway");
|
|
27
27
|
const { recoverPendingTransactions, runKernelTransaction } = require("../v2/transaction");
|
|
28
28
|
|
|
29
29
|
const TERMINAL = new Set(["completed", "failed", "cancelled", "resolved", "rejected", "deprecated", "invalidated", "archived", "passed"]);
|
|
@@ -164,6 +164,7 @@ function approveRequestUnlocked(projectRoot, token, { failurePoint = null, inter
|
|
|
164
164
|
ledger: 1,
|
|
165
165
|
guardrails: 1,
|
|
166
166
|
workspace: 1,
|
|
167
|
+
execution: "session",
|
|
167
168
|
approval_id: approvalId
|
|
168
169
|
};
|
|
169
170
|
const taskLinks = { feature: featureId, sprint: sprintId };
|
|
@@ -267,6 +268,94 @@ function transitionedRunContent(content, nextStatus, options = {}) {
|
|
|
267
268
|
};
|
|
268
269
|
}
|
|
269
270
|
|
|
271
|
+
function sectionBody(markdown, heading) {
|
|
272
|
+
const match = new RegExp(`^## ${heading}[ \\t]*\\r?\\n\\r?\\n([\\s\\S]*?)(?=^## |(?![\\s\\S]))`, "m").exec(markdown || "");
|
|
273
|
+
return match ? match[1].trim() : null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Human-authored evidence stays beside the work in the Task, rather than
|
|
277
|
+
// forcing a separate CLI invocation for every guardrail. The single final
|
|
278
|
+
// checkpoint parses it deterministically and rejects malformed entries.
|
|
279
|
+
//
|
|
280
|
+
// - GR-005 | review | REV-001 | Scoped architecture review passed.
|
|
281
|
+
// - GR-006 | migration | db/migrations/20260831.sql | Migration verified.
|
|
282
|
+
// - GR-007 | note | Request authorization confirmed.
|
|
283
|
+
function finalGuardrailEvidence(taskBody) {
|
|
284
|
+
const content = sectionBody(taskBody, "Guardrail Evidence");
|
|
285
|
+
if (!content) return {};
|
|
286
|
+
const result = {};
|
|
287
|
+
for (const line of content.split(/\r?\n/)) {
|
|
288
|
+
if (!line.trim()) continue;
|
|
289
|
+
const raw = line.match(/^\s*-\s*(GR-\d{3,})\s*\|\s*([a-z-]+)\s*\|\s*(.*?)\s*$/i);
|
|
290
|
+
if (!raw) throw new Error(`Invalid Guardrail Evidence entry: ${line.trim()}`);
|
|
291
|
+
const [, guardrail, kind, remainder] = raw;
|
|
292
|
+
const parts = remainder.split("|").map((part) => part.trim()).filter(Boolean);
|
|
293
|
+
if (!parts.length) throw new Error(`${guardrail} needs evidence content.`);
|
|
294
|
+
const allowed = new Set(["approval", "command", "test", "file", "review", "decision", "insight", "risk", "note", "migration", "legacy", "guardrail", "mutation"]);
|
|
295
|
+
if (!allowed.has(kind)) throw new Error(`${guardrail} has unsupported evidence kind: ${kind}.`);
|
|
296
|
+
const evidence = { kind };
|
|
297
|
+
if (parts.length > 1) evidence.ref = parts.shift();
|
|
298
|
+
evidence.summary = parts.join(" | ") || (evidence.ref ? `${kind} evidence: ${evidence.ref}` : null);
|
|
299
|
+
if (!evidence.summary) throw new Error(`${guardrail} needs an evidence summary.`);
|
|
300
|
+
if (!result[guardrail]) result[guardrail] = [];
|
|
301
|
+
result[guardrail].push(evidence);
|
|
302
|
+
}
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function finalizeRunUnlocked(projectRoot, runId, { note = null, summary = null, actor = "agent", occurredAt = null } = {}) {
|
|
307
|
+
const scrumDir = path.join(projectRoot, ".scrumrun");
|
|
308
|
+
recoverPendingTransactions(scrumDir);
|
|
309
|
+
const repository = new ArtifactRepository(scrumDir);
|
|
310
|
+
const runArtifact = repository.read("run", runId);
|
|
311
|
+
if (!runArtifact || runArtifact.errors.length) throw new Error(`Run not found or invalid: ${runId}`);
|
|
312
|
+
if (runArtifact.record.status !== "executing") throw new Error(`Lightweight finalization requires an executing Run; ${runId} is ${runArtifact.record.status}.`);
|
|
313
|
+
const taskArtifact = repository.read("task", runArtifact.record.task);
|
|
314
|
+
if (!taskArtifact || taskArtifact.errors.length) throw new Error(`Task not found or invalid for ${runId}.`);
|
|
315
|
+
const technicalSummary = summary || sectionBody(taskArtifact.body, "Technical Summary");
|
|
316
|
+
if (!technicalSummary) throw new Error(`TASK_SUMMARY_REQUIRED: add ## Technical Summary to ${taskArtifact.record.id}, or pass --summary.`);
|
|
317
|
+
const effectiveActor = actor === "agent" ? (agentIdentity(scrumDir) || "agent") : actor;
|
|
318
|
+
const prepared = prepareSessionCompletion(projectRoot, runArtifact, {
|
|
319
|
+
note,
|
|
320
|
+
actor: effectiveActor,
|
|
321
|
+
occurredAt,
|
|
322
|
+
guardrailEvidence: finalGuardrailEvidence(taskArtifact.body)
|
|
323
|
+
});
|
|
324
|
+
let runContent = serializeArtifact(prepared.record, prepared.body);
|
|
325
|
+
for (const [status, evidence] of [
|
|
326
|
+
["validating", [{ kind: "test", summary: "Final session integrity audit passed." }]],
|
|
327
|
+
["learning", [{ kind: "insight", summary: "Learning extraction scheduled from the completed session." }]],
|
|
328
|
+
["completed", [{ kind: "note", summary: note || "Completed through the final session checkpoint." }]]
|
|
329
|
+
]) {
|
|
330
|
+
runContent = transitionedRunContent(runContent, status, { actor: effectiveActor, occurredAt, note, evidence }).content;
|
|
331
|
+
}
|
|
332
|
+
runContent = appendTechnicalSummary(parseArtifact(runContent).record, runContent, technicalSummary);
|
|
333
|
+
const taskPrevious = fs.readFileSync(taskArtifact.file, "utf8");
|
|
334
|
+
let taskContent = taskPrevious;
|
|
335
|
+
let taskNext = null;
|
|
336
|
+
for (const status of ["validating", "learning", "completed"]) {
|
|
337
|
+
taskNext = transitionedArtifactContent(taskContent, "task", status, new Date().toISOString().slice(0, 10));
|
|
338
|
+
taskContent = taskNext.content;
|
|
339
|
+
}
|
|
340
|
+
runKernelTransaction(scrumDir, "finalize-lightweight-session", [
|
|
341
|
+
{ file: runArtifact.file, previous: fs.readFileSync(runArtifact.file, "utf8"), next: runContent },
|
|
342
|
+
{ file: taskArtifact.file, previous: taskPrevious, next: taskNext.content }
|
|
343
|
+
]);
|
|
344
|
+
refreshState(scrumDir);
|
|
345
|
+
let learning = null;
|
|
346
|
+
try {
|
|
347
|
+
learning = extractLearningCandidates(projectRoot, runId);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
learning = { created: [], warnings: [`Learning extraction did not block finalization: ${error.message}`] };
|
|
350
|
+
}
|
|
351
|
+
refreshState(scrumDir);
|
|
352
|
+
return { run: parseArtifact(runContent).record, task: taskNext.record, changes: prepared.changes.length, resolved: prepared.resolved, learning };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function finalizeRun(projectRoot, runId, options = {}) {
|
|
356
|
+
return withArtifactLock(path.join(projectRoot, ".scrumrun"), `run-${String(runId).toLowerCase()}`, () => finalizeRunUnlocked(projectRoot, runId, options));
|
|
357
|
+
}
|
|
358
|
+
|
|
270
359
|
function transitionRunUnlocked(projectRoot, runId, nextStatus, { note = null, evidence = [], actor = "agent", occurredAt = null, summary = null, failurePoint = null, interruptPoint = null } = {}) {
|
|
271
360
|
const scrumDir = path.join(projectRoot, ".scrumrun");
|
|
272
361
|
const effectiveActor = actor === "agent" ? (agentIdentity(scrumDir) || "agent") : actor;
|
|
@@ -376,6 +465,7 @@ function retryTaskUnlocked(projectRoot, taskId, { note = "Retry explicitly appro
|
|
|
376
465
|
ledger: 1,
|
|
377
466
|
guardrails: 1,
|
|
378
467
|
workspace: 1,
|
|
468
|
+
execution: "session",
|
|
379
469
|
approval_id: taskArtifact.record.approval_id || null
|
|
380
470
|
};
|
|
381
471
|
const runBody = createRunBody(run, {
|
|
@@ -447,6 +537,7 @@ function startBacklogTaskUnlocked(projectRoot, taskId, { note = "Backlog Task st
|
|
|
447
537
|
ledger: 1,
|
|
448
538
|
guardrails: 1,
|
|
449
539
|
workspace: 1,
|
|
540
|
+
execution: "session",
|
|
450
541
|
approval_id: taskArtifact.record.approval_id || null
|
|
451
542
|
};
|
|
452
543
|
const runBody = createRunBody(run, {
|
|
@@ -495,7 +586,7 @@ const ADD_PLAN_INITIAL_STATUS = Object.freeze({
|
|
|
495
586
|
const TASK_TYPES = new Set(["task", "fix", "docs", "discovery"]);
|
|
496
587
|
|
|
497
588
|
function planArtifactBody(kind, title, created) {
|
|
498
|
-
const source = `- ${created}: created via CLI (\`
|
|
589
|
+
const source = `- ${created}: created via CLI (\`scrumrun plan ${kind} --add\`).`;
|
|
499
590
|
if (kind === "task") {
|
|
500
591
|
return [
|
|
501
592
|
`# ${title}`,
|
|
@@ -598,4 +689,4 @@ function addPlanArtifact(projectRoot, kind, label, options = {}) {
|
|
|
598
689
|
return withArtifactLock(scrumDir, "create", () => addPlanArtifactUnlocked(projectRoot, kind, label, options));
|
|
599
690
|
}
|
|
600
691
|
|
|
601
|
-
module.exports = { addPlanArtifact, approveRequest, nextBacklogTask, nextId, refreshErrors, refreshState, renderState, retryTask, startBacklogTask, stateFingerprint, stateIsStale, transitionRun };
|
|
692
|
+
module.exports = { addPlanArtifact, approveRequest, finalizeRun, nextBacklogTask, nextId, refreshErrors, refreshState, renderState, retryTask, startBacklogTask, stateFingerprint, stateIsStale, transitionRun };
|
package/lib/v2/conformance.js
CHANGED
|
@@ -79,7 +79,7 @@ function auditProject(projectRoot) {
|
|
|
79
79
|
if (methodMarker.method !== METHOD_VERSION) findings.push(finding("critical", "METHOD_VERSION", `method.json must declare ${METHOD_VERSION}.`, marker));
|
|
80
80
|
const expectedPaths = canonicalPaths();
|
|
81
81
|
if (!methodMarker.paths || typeof methodMarker.paths !== "object" || Array.isArray(methodMarker.paths)) {
|
|
82
|
-
findings.push(finding("high", "METHOD_PATHS_MISSING", `method.json must declare a canonical "paths" block so agents navigate by declaration, not by search. Run \`
|
|
82
|
+
findings.push(finding("high", "METHOD_PATHS_MISSING", `method.json must declare a canonical "paths" block so agents navigate by declaration, not by search. Run \`scrumrun update --migrate\` to backfill.`, marker));
|
|
83
83
|
} else {
|
|
84
84
|
const drift = diffPathIndex(expectedPaths, methodMarker.paths);
|
|
85
85
|
for (const entry of drift) findings.push(finding("high", "METHOD_PATHS_DRIFT", `method.json paths[${entry.label}] is ${entry.actual === undefined ? "missing" : `"${entry.actual}"`}; expected "${entry.expected}". Regenerate via \`update --migrate\`.`, marker));
|
|
@@ -152,7 +152,7 @@ function auditProject(projectRoot) {
|
|
|
152
152
|
if (run.record && run.record.ledger === RUN_LEDGER_VERSION) {
|
|
153
153
|
const ledger = validateRunLedger(run.record, run.body);
|
|
154
154
|
if (ledger.errors.length) {
|
|
155
|
-
findings.push(finding("high", "RUN_WRITE_BYPASS", `${run.record.id}: ledger has ${ledger.errors.length} validation error(s); this Run was likely written without going through the CLI. Recover with \`
|
|
155
|
+
findings.push(finding("high", "RUN_WRITE_BYPASS", `${run.record.id}: ledger has ${ledger.errors.length} validation error(s); this Run was likely written without going through the CLI. Recover with \`scrumrun plan run --normalize-legacy --dry-run\` then re-run without --dry-run.`, run.file));
|
|
156
156
|
for (const error of ledger.errors) findings.push(finding("high", "RUN_LEDGER_INVALID", `${run.record.id}: ${error}`, run.file));
|
|
157
157
|
}
|
|
158
158
|
} else if (run.record) {
|
|
@@ -198,7 +198,17 @@ function auditProject(projectRoot) {
|
|
|
198
198
|
const attempts = (records.run || [])
|
|
199
199
|
.filter((run) => run.record && !run.errors.length && run.record.task === task.record.id)
|
|
200
200
|
.sort((left, right) => right.record.attempt - left.record.attempt);
|
|
201
|
-
if (!attempts.length)
|
|
201
|
+
if (!attempts.length) {
|
|
202
|
+
if (["running", "validating", "learning", "partial"].includes(task.record.status)) {
|
|
203
|
+
findings.push(finding(
|
|
204
|
+
"high",
|
|
205
|
+
"TASK_ORPHANED",
|
|
206
|
+
`${task.record.id}.status is ${task.record.status}, but no canonical Run references it. Review with \`scrumrun repair --recover-orphan-tasks\`, then apply the explicit recovery and start the Task to create its first Run.`,
|
|
207
|
+
task.file
|
|
208
|
+
));
|
|
209
|
+
}
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
202
212
|
const latest = attempts[0].record;
|
|
203
213
|
const taskStatuses = {
|
|
204
214
|
executing: "running",
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# AGENTS.md - {{PROJECT_NAME}}
|
|
2
2
|
|
|
3
|
-
## ScrumRun
|
|
3
|
+
## ScrumRun 3.0
|
|
4
4
|
|
|
5
|
-
This project uses ScrumRun. The method is mandatory; `/sc` is
|
|
5
|
+
This project uses ScrumRun. The method is mandatory; the direct CLI is `scrumrun <noun> <subject> <action>`. `/sc` is only an optional client shortcut.
|
|
6
6
|
|
|
7
7
|
For normal work, read:
|
|
8
8
|
|
|
@@ -20,10 +20,12 @@ 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
|
- record a `## Technical Summary` at completion so the next agent inherits what was done;
|
|
23
|
-
-
|
|
23
|
+
- work directly in code and the linked Task Markdown after approval; do not call `npx scrumrun@latest` during execution;
|
|
24
|
+
- record one `## Guardrail Evidence` line per non-automatic guardrail in the Task, then run `scrumrun plan run --finalize RUN-NNN` once to validate and close the Run;
|
|
25
|
+
- when a Run completes and work remains queued, surface it with `scrumrun plan task --next` and start it with `scrumrun plan task --start` — starting is explicit approval;
|
|
24
26
|
- learning proposes evidence-backed Knowledge, Decisions, or candidate Insights;
|
|
25
|
-
-
|
|
27
|
+
- the final checkpoint verifies all Guardrails, workspace changes, protected paths, and secret boundaries before completion; use the path-scoped Mutation Gateway only when the owner requests strict execution.
|
|
26
28
|
|
|
27
29
|
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.
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
Never use `npx scrumrun@latest` in the normal work loop. If the installed CLI is unavailable, stop and report that blocker rather than substituting a network command.
|
|
@@ -5,13 +5,13 @@ description: Use when initializing or migrating ScrumRun, handling product reque
|
|
|
5
5
|
|
|
6
6
|
# ScrumRun 2.0
|
|
7
7
|
|
|
8
|
-
ScrumRun is an evidence-driven Agile runtime for AI agents. Its canonical command is:
|
|
8
|
+
ScrumRun is an evidence-driven Agile runtime for AI agents. Its canonical shell command is:
|
|
9
9
|
|
|
10
10
|
```text
|
|
11
|
-
|
|
11
|
+
scrumrun <noun> <subject> <action> [args]
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
The five nouns are `plan`, `knowledge`, `rules`, `review`, and `config`. If the command is incomplete, show only the valid next tokens and do not guess.
|
|
14
|
+
The five nouns are `plan`, `knowledge`, `rules`, `review`, and `config`. `/sc` is an optional AI-client shortcut; `scrumrun sc ...` is a compatibility alias. If the command is incomplete, show only the valid next tokens and do not guess.
|
|
15
15
|
|
|
16
16
|
When a project contains `.scrumrun/`, use ScrumRun for product work even if the user did not type `/sc`. Natural-language work begins as `plan intake`.
|
|
17
17
|
|
|
@@ -33,10 +33,10 @@ Normal hot path:
|
|
|
33
33
|
2. read `AGENTS.md`;
|
|
34
34
|
3. read `.scrumrun/guardrails.md`;
|
|
35
35
|
4. read `.scrumrun/state.md` — the **briefing**: active work, recent completions with their technical summaries, open decisions, active memory, backlog queue, and pointers;
|
|
36
|
-
5. follow the briefing's pointers to only the relevant canonical artifacts; go deeper only when the briefing lacks what you need (`## Where to look`, `
|
|
36
|
+
5. follow the briefing's pointers to only the relevant canonical artifacts; go deeper only when the briefing lacks what you need (`## Where to look`, `scrumrun knowledge study "<topic>"`);
|
|
37
37
|
6. load `.scrumrun/core.md` when the method contract or an exceptional transition is needed.
|
|
38
38
|
|
|
39
|
-
**Never write Run events by hand.**
|
|
39
|
+
**Never write Run events by hand.** Work directly in code and the linked Task Markdown after approval, then let the one final CLI checkpoint mutate `runs/RUN-NNN.md`: `scrumrun plan run --finalize RUN-NNN`. It creates the validated transition ledger, audits the full workspace delta, and records the Guardrail results together. Do not invoke `scrumrun` during normal execution. The older `--validate | --learn | --complete | --satisfy-guardrail | --authorize-mutation | --record-mutation` commands remain only for owner-requested strict mode. Recover hand-written Runs via `scrumrun plan run --normalize-legacy` — originals are preserved byte-exact under `.scrumrun/.migration-backup/runs/`.
|
|
40
40
|
|
|
41
41
|
Lean mode is a read policy, not an incomplete store. Generated files and `.scrumrun/.cache/` are never authoritative.
|
|
42
42
|
|
|
@@ -81,7 +81,7 @@ 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: `
|
|
84
|
+
Assert the classification explicitly when the keyword inference is wrong: `scrumrun plan intake "…" --type fix|task|feature|docs|discovery`. Attach a short technical explanation before approval with `--preview "…"` (rendered in the terminal, bound into the token, stored as `## Preview` on the Task).
|
|
85
85
|
|
|
86
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.
|
|
87
87
|
|
|
@@ -101,19 +101,17 @@ During execution:
|
|
|
101
101
|
1. keep the change inside the approved Task scope;
|
|
102
102
|
2. preserve existing owner work and unrelated dirty files;
|
|
103
103
|
3. define or confirm the Task's `## Acceptance Criteria` before execution and check them off as evidence;
|
|
104
|
-
4.
|
|
104
|
+
4. work normally: edit code and update the Task's `## Technical Summary` and, for non-automatic rules, `## Guardrail Evidence` as evidence becomes available;
|
|
105
105
|
5. validate in proportion to risk and against the acceptance criteria;
|
|
106
|
-
6.
|
|
107
|
-
7. run
|
|
108
|
-
8.
|
|
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.
|
|
106
|
+
6. run configured reviewers when a Guardrail requires one;
|
|
107
|
+
7. finish once with `scrumrun plan run --finalize RUN-NNN`; it verifies every changed path, policy, secret boundary, and guardrail evidence before creating the structured Run events and completing the Task;
|
|
108
|
+
8. use path-scoped Mutation Gateway commands only when the owner explicitly requests strict execution.
|
|
111
109
|
|
|
112
110
|
Never overwrite a prior attempt. Never mark work complete because time/token budget ended.
|
|
113
111
|
|
|
114
|
-
When a Run completes and work remains queued, the briefing's `## Next Up` names the next backlog Task. Surface it with `
|
|
112
|
+
When a Run completes and work remains queued, the briefing's `## Next Up` names the next backlog Task. Surface it with `scrumrun plan task --next` and start it with `scrumrun plan task --start [TASK-NNN]` — starting is the explicit approval; the owner can always decline. Each agent declares its identity via `SCRUMRUN_AGENT` (or `Agent Identity` in `config.md`); it is recorded as the Task `assignee` and the Run event `actor`.
|
|
115
113
|
|
|
116
|
-
Every deferred policy result is an append-only Run obligation.
|
|
114
|
+
Every deferred policy result is an append-only Run obligation. The final checkpoint fails closed on policy drift, protected-path changes, unsafe symlinks, unscannable content, newly introduced secret-like content, or missing Guardrail Evidence. In strict mode it additionally requires the permit chain. The ignored permit cache is disposable; deleting it invalidates outstanding strict-mode permits and never creates authority.
|
|
117
115
|
|
|
118
116
|
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.
|
|
119
117
|
|
|
@@ -157,13 +155,13 @@ scrumrun migrate --to 2 --apply
|
|
|
157
155
|
scrumrun migrate --to 2 --rollback
|
|
158
156
|
```
|
|
159
157
|
|
|
160
|
-
Inside a v1 project, `
|
|
158
|
+
Inside a v1 project, `scrumrun update` runs a read-only preflight. `update --migrate` explicitly approves application of that verified plan and keeps rollback available.
|
|
161
159
|
|
|
162
160
|
Dry-run must not write project data. Apply requires a hashed inventory, byte-exact local backup, staged validation, atomic switch, mapping report, and idempotent replay. Incomplete hybrid trees reuse existing evidenced v2 relations rather than duplicating them; early v2 prose Runs upgrade to ledger schema 1 only through the same explicit apply gate. Legacy-only aggregates leave the active tree but remain byte-exact in the ignored backup. Ambiguous records are preserved as warnings or evidenced snapshots, never guessed. Vault content remains local and is never rendered. Rollback must refuse if it would erase post-migration changes.
|
|
163
161
|
|
|
164
162
|
## Command grammar
|
|
165
163
|
|
|
166
|
-
###
|
|
164
|
+
### `scrumrun plan`
|
|
167
165
|
|
|
168
166
|
- `task`: add/list/show/run/audit/cancel/retry atomic work; use `type: fix` for fixes and `status: backlog` for parked work. `--next` surfaces the oldest backlog Task; `--start [TASK-NNN]` promotes it and creates its first Run.
|
|
169
167
|
- `sprint`: add/list/show/start/complete/block a real Task batch/timebox.
|
|
@@ -172,7 +170,7 @@ Dry-run must not write project data. Apply requires a hashed inventory, byte-exa
|
|
|
172
170
|
- `intake <request>`: execute the read-only request pipeline; accepts `--type fix|task|feature|docs|discovery` and `--preview "…"`.
|
|
173
171
|
- `challenge <question>`: deep read-only analysis with evidence, risks, options, and recommendation.
|
|
174
172
|
|
|
175
|
-
###
|
|
173
|
+
### `scrumrun knowledge`
|
|
176
174
|
|
|
177
175
|
- `fact`: add/list/show/approve/reject/deprecate/invalidate `K-NNN` records.
|
|
178
176
|
- `decision`: add/list/show/resolve/deprecate/invalidate `DEC-NNN` records.
|
|
@@ -183,12 +181,12 @@ Dry-run must not write project data. Apply requires a hashed inventory, byte-exa
|
|
|
183
181
|
- `study <focus>`: deep read-only discovery with precise evidence.
|
|
184
182
|
- `vault`: add/list/show/remove/path for explicitly requested local development values; list redacts values.
|
|
185
183
|
|
|
186
|
-
###
|
|
184
|
+
### `scrumrun rules`
|
|
187
185
|
|
|
188
186
|
- `guardrail`: add/list/show/retire project rules. Adding/retiring requires explicit approval.
|
|
189
187
|
- `reviewer`: add/list/show/run configured review roles.
|
|
190
188
|
|
|
191
|
-
###
|
|
189
|
+
### `scrumrun review`
|
|
192
190
|
|
|
193
191
|
- `code --run`: review code changes by severity with file/line evidence.
|
|
194
192
|
- `artifact --run`: validate method artifacts and relations.
|
|
@@ -197,7 +195,7 @@ Dry-run must not write project data. Apply requires a hashed inventory, byte-exa
|
|
|
197
195
|
|
|
198
196
|
Review is read-only unless the user separately authorizes fixes.
|
|
199
197
|
|
|
200
|
-
###
|
|
198
|
+
### `scrumrun config`
|
|
201
199
|
|
|
202
200
|
- `project`: show/set language, interaction, approval, and quick-task preferences.
|
|
203
201
|
- `init`: initialize ScrumRun without overwriting existing state silently.
|
|
@@ -209,16 +207,16 @@ Review is read-only unless the user separately authorizes fixes.
|
|
|
209
207
|
|
|
210
208
|
## Compatibility
|
|
211
209
|
|
|
212
|
-
Upgrade installs may provide thin v1 adapters for one release cycle. An adapter must execute the mapped
|
|
210
|
+
Upgrade installs may provide thin v1 adapters for one release cycle. An adapter must execute the mapped direct route in the same turn and emit one concise deprecation note; it must not merely ask the user to invoke another command.
|
|
213
211
|
|
|
214
212
|
Important mappings:
|
|
215
213
|
|
|
216
|
-
- `/sc-sprint` →
|
|
217
|
-
- `/sc-fix` →
|
|
218
|
-
- `/sc-backlog` →
|
|
219
|
-
- `/sc-decisions` →
|
|
220
|
-
- `/sc-know` →
|
|
221
|
-
- `/sc-golden` →
|
|
222
|
-
- `/sc-agent` →
|
|
214
|
+
- `/sc-sprint` → `scrumrun plan sprint`
|
|
215
|
+
- `/sc-fix` → `scrumrun plan task` with `type: fix`
|
|
216
|
+
- `/sc-backlog` → `scrumrun plan task` with `status: backlog`
|
|
217
|
+
- `/sc-decisions` → `scrumrun knowledge decision`
|
|
218
|
+
- `/sc-know` → `scrumrun knowledge fact`
|
|
219
|
+
- `/sc-golden` → `scrumrun rules guardrail`
|
|
220
|
+
- `/sc-agent` → `scrumrun rules reviewer`
|
|
223
221
|
|
|
224
|
-
Always generate and recommend the canonical
|
|
222
|
+
Always generate and recommend the canonical direct CLI grammar.
|