scrumrun 2.2.0 → 2.3.0
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 +25 -0
- package/README.md +59 -1
- package/SPEC.md +2 -1
- package/bin/scrumrun.js +62 -4
- package/docs/DEMO.md +67 -0
- package/docs/ERROR-CODES.md +141 -0
- package/docs/INDEX.md +53 -0
- package/docs/QUICKSTART.md +156 -0
- package/lib/commands/manifest.js +3 -1
- package/lib/commands/run-render.js +161 -0
- package/lib/commands/run-stats.js +206 -0
- package/lib/errors.js +98 -0
- package/lib/memory/index.js +1 -1
- package/lib/v2/conformance.js +7 -2
- package/lib/v2/transaction.js +59 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,31 @@ All notable changes follow Semantic Versioning.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 2.3.0 - 2026-07-23
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Stable error catalog in [`lib/errors.js`](lib/errors.js) exposing a `ScrumRunError` class plus `describe`/`codes` accessors, and a human view in [`docs/ERROR-CODES.md`](docs/ERROR-CODES.md). Codes follow `SR-E-NNN` with reserved ranges per subsystem and are guaranteed stable once assigned.
|
|
12
|
+
- `sc config doctor --recover --dry-run` previews every pending kernel transaction, describes each planned rollback or commit-verification, warns about unsafe paths, journal integrity failures, or owner-touched files, and exits non-zero when at least one transaction cannot be recovered safely — never writes.
|
|
13
|
+
- Invariant `I-22`: the semantic index's declared search backend must match the runtime's actual capabilities. Conformance now emits a `high` `SEARCH_BACKEND_MISMATCH` finding when a cache advertises `fts5` on a runtime that cannot execute it, replacing the previous lazy-rebuild-only behaviour that hid the drift.
|
|
14
|
+
- `previewPendingRecovery` exported from `lib/v2/transaction` for library consumers.
|
|
15
|
+
- Property-based intake tests: 200 adversarial inputs per run (NUL bytes, ANSI escapes, homoglyphs, injection shapes, oversize strings) proving `planRequest` is total, never throws unclassified errors, and does not mutate `.scrumrun/`.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- `sc plan run --render RUN-NNN` prints the ledger as a chronological, human-readable timeline with actors, transitions, reasons, evidence, and total span. Complements the existing `--show` (raw Markdown) so auditing no longer requires reading JSON blocks by hand.
|
|
20
|
+
- `sc plan run --stats [--task TASK-NNN] [--feature FEAT-NNN] [--sprint SPRINT-NNN] [--json]` aggregates every Run in the project into status mix, p50/p95/max time in `VALIDATING` and `EXECUTING`, retry counts per Task, completions after prior failure, and guardrail/mutation event totals — computed straight over the canonical ledger, no separate index.
|
|
21
|
+
- New modules `lib/commands/run-render` and `lib/commands/run-stats` expose `renderRun`, `renderRunFromDisk`, `computeStats`, and `renderStats` for library consumers.
|
|
22
|
+
|
|
23
|
+
### Documentation
|
|
24
|
+
|
|
25
|
+
- Added [`docs/QUICKSTART.md`](docs/QUICKSTART.md): first Run in under 10 minutes, no `SPEC.md` reading required. Now includes the new `sc plan run --render` and `sc plan run --stats` commands.
|
|
26
|
+
- Added [`docs/DEMO.md`](docs/DEMO.md) with the exact asciinema script for a 40-second intake → approve → render → stats demo the owner can record and embed in the README and LP.
|
|
27
|
+
- Added [`docs/INDEX.md`](docs/INDEX.md) with reading-order recommendations by intent (fresh install, integrating a client, upgrading from v1, browsing rationale, troubleshooting).
|
|
28
|
+
- Added a **Terminology quick reference** in the README that maps internal names (`Mutation Gateway`, `Policy Engine`, `Guardrail obligation`, `Canonical projection`, `Executable schema`) to friendlier user-facing forms; both are treated as synonyms.
|
|
29
|
+
- Bumped README package badge to `2.2.0` and linked the Quickstart from the top.
|
|
30
|
+
- Created `.scrumrun/features/FEAT-001.md` to record the ongoing "Method 2.x — Onboarding, Visible Auditability, and Stable Diagnostics" initiative and its four-wave scope.
|
|
31
|
+
|
|
7
32
|
## 2.2.0 - 2026-07-23
|
|
8
33
|
|
|
9
34
|
### Security
|
package/README.md
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
ScrumRun gives an agent a small command surface and a precise project memory: what should be done, how each attempt happened, which decisions constrain the code, and why the architecture exists in its current form.
|
|
6
6
|
|
|
7
|
-
**Package:** `2.
|
|
7
|
+
**Package:** `2.3.0` · **Method target:** `2.0.0` · **Runtime:** Node.js `>=22.13.0` · **License:** MIT
|
|
8
|
+
|
|
9
|
+
**New here?** Read the [Quickstart](docs/QUICKSTART.md) — first Run in under 10 minutes, no `SPEC.md` reading required. Full docs map in [`docs/INDEX.md`](docs/INDEX.md).
|
|
8
10
|
|
|
9
11
|
## The model
|
|
10
12
|
|
|
@@ -75,6 +77,20 @@ Each Run contains a machine-validated event ledger. Events have stable ids such
|
|
|
75
77
|
|
|
76
78
|
Linked Task/Run writes use a durable ignored transaction journal. Captured failures roll back immediately; interrupted operations are recovered byte-exactly on retry or through explicit `doctor --recover`. Read-only audit reports pending recovery and never repairs state silently.
|
|
77
79
|
|
|
80
|
+
## Terminology quick reference
|
|
81
|
+
|
|
82
|
+
Internal names in the code and spec are precise but occasionally heavy. When they surface in CLI output or docs, the shorter form works too.
|
|
83
|
+
|
|
84
|
+
| Internal (SPEC/CORE) | User-facing (docs, output) | What it means |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| Mutation Gateway | edit permit | The 15-minute, path-scoped permission an approved Run needs to edit source files. |
|
|
87
|
+
| Policy Engine | guardrail check | The executable evaluation of every active `GR-NNN` rule before planning and before execution. |
|
|
88
|
+
| Guardrail obligation | pending guardrail | A deferred guardrail check persisted on a Run; must resolve before completion. |
|
|
89
|
+
| Canonical projection | derived view | Generated navigation files such as `state.md` and `map.md` — always regenerable, never authority. |
|
|
90
|
+
| Executable schema | frozen contract | The single machine-checked shape for ids, transitions, and cardinalities. |
|
|
91
|
+
|
|
92
|
+
The short forms are synonyms — safe to use in issues, PRs, and everyday conversation.
|
|
93
|
+
|
|
78
94
|
## Semantic project memory
|
|
79
95
|
|
|
80
96
|
Canonical memory is human-readable Markdown:
|
|
@@ -159,6 +175,32 @@ npx scrumrun@latest doctor codex --strict
|
|
|
159
175
|
|
|
160
176
|
Approved Runs persist every deferred result as an append-only obligation and bind the exact policy plus workspace baseline. Material source edits use a 15-minute, path-scoped Mutation Gateway permit. Recording verifies before/after hashes, owner/read-only scope, symlinks, new secret-like content, and workspace drift. Validation and completion fail closed when edits bypass that chain or obligations remain unresolved.
|
|
161
177
|
|
|
178
|
+
### Owner-controlled allowlist for descriptive files
|
|
179
|
+
|
|
180
|
+
Secret-like content detection is canonical-policy-level and applies to every file outside `vault.local.md`. Owners can exempt specific **non-canonical descriptive paths** from the keyword heuristic by adding frontmatter to `.scrumrun/config.md`:
|
|
181
|
+
|
|
182
|
+
```yaml
|
|
183
|
+
---
|
|
184
|
+
allow_secrets_in:
|
|
185
|
+
- goals/main/history.md
|
|
186
|
+
- goals/main/sprint.md
|
|
187
|
+
- docs/migration-notes/
|
|
188
|
+
- "*.legacy.md"
|
|
189
|
+
---
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Supported matchers per entry:
|
|
193
|
+
|
|
194
|
+
- exact path (`goals/main/history.md`)
|
|
195
|
+
- directory prefix ending in `/` (`docs/legacy/`)
|
|
196
|
+
- glob pattern (`*.legacy.md`)
|
|
197
|
+
|
|
198
|
+
Rules:
|
|
199
|
+
|
|
200
|
+
- Canonical artifacts (Task, Sprint, Run, Feature, Review, Memory) are **never** eligible — a secret in a canonical file always fails conformance.
|
|
201
|
+
- High-confidence shapes (`sk-…`, `AKIA…`, JWT three-part tokens, PEM private keys, long bearer tokens) are never exempted regardless of path.
|
|
202
|
+
- The allowlist is recorded as evidence of an explicit owner decision and is reviewed by `doctor`.
|
|
203
|
+
|
|
162
204
|
## Useful commands
|
|
163
205
|
|
|
164
206
|
```bash
|
|
@@ -186,19 +228,35 @@ npx scrumrun@latest sc knowledge study calculateFinalPrice
|
|
|
186
228
|
# rebuild or inspect the derived graph
|
|
187
229
|
npx scrumrun@latest sc knowledge map --build
|
|
188
230
|
npx scrumrun@latest sc knowledge map --show
|
|
231
|
+
|
|
232
|
+
# read a Run as a human timeline instead of raw ledger JSON
|
|
233
|
+
npx scrumrun@latest sc plan run --render RUN-001
|
|
234
|
+
|
|
235
|
+
# aggregate every Run in the project (p50/p95 durations, retries, guardrail counts)
|
|
236
|
+
npx scrumrun@latest sc plan run --stats
|
|
237
|
+
npx scrumrun@latest sc plan run --stats --task TASK-001 --json
|
|
238
|
+
|
|
239
|
+
# preview a canonical-transaction recovery before touching disk
|
|
240
|
+
npx scrumrun@latest sc config doctor --recover --dry-run
|
|
189
241
|
```
|
|
190
242
|
|
|
191
243
|
The command manifest in `lib/commands/manifest.js` generates help and compatibility adapters, preventing client grammar drift. A recorded artifact Review cannot self-declare success: ScrumRun reruns the audit and derives the `REV-NNN` verdict from the result.
|
|
192
244
|
|
|
245
|
+
Every user-facing failure carries a stable [`SR-E-NNN` code](docs/ERROR-CODES.md) with a permanent meaning, a short summary, and an exact remediation.
|
|
246
|
+
|
|
193
247
|
## Documentation
|
|
194
248
|
|
|
195
249
|
| File | Purpose |
|
|
196
250
|
|---|---|
|
|
251
|
+
| [`docs/QUICKSTART.md`](docs/QUICKSTART.md) | First Run in under 10 minutes, no `SPEC.md` reading required. |
|
|
252
|
+
| [`docs/INDEX.md`](docs/INDEX.md) | Reading-order recommendations across every doc, by intent. |
|
|
197
253
|
| [`CORE.md`](./CORE.md) | Operational runtime guide for agents. |
|
|
198
254
|
| [`SPEC.md`](./SPEC.md) | Normative 2.0 state machines, invariants, and conformance rules. |
|
|
199
255
|
| [`DECISIONS.md`](./DECISIONS.md) | Architectural decisions and trade-offs. |
|
|
256
|
+
| [`docs/ERROR-CODES.md`](docs/ERROR-CODES.md) | Stable `SR-E-NNN` catalog with remediation for every failure. |
|
|
200
257
|
| `MIGRATION-1-to-2.md` | Upgrade, verification, rollback, and recovery guide. |
|
|
201
258
|
| [`docs/RELEASE-SCORECARD.md`](docs/RELEASE-SCORECARD.md) | Evidence-backed local readiness scores and residual release risks. |
|
|
259
|
+
| [`docs/DEMO.md`](docs/DEMO.md) | Script for a 40-second asciinema demo of the full intake → approve → render → stats loop. |
|
|
202
260
|
|
|
203
261
|
ScrumRun remains client-independent: any agent that reads Markdown can follow `CORE.md`; Codex, Claude Code, and OpenCode integrations are accelerators.
|
|
204
262
|
|
package/SPEC.md
CHANGED
|
@@ -308,6 +308,7 @@ Legacy sprint entries become Tasks. History entries become Runs only with an evi
|
|
|
308
308
|
- **I-19** Code intelligence is derived, adapter-based, fingerprinted, and cannot silently confirm memory.
|
|
309
309
|
- **I-20** Post-validation learning proposes candidates and never blocks Task/Run completion.
|
|
310
310
|
- **I-21** Material mutations are policy-bound, path-scoped, hash-verified, append-only, and fail closed on bypass; unresolved Guardrail obligations block completion.
|
|
311
|
+
- **I-22** The semantic index's declared search backend must match the runtime capabilities of the current Node.js SQLite build; conformance flags a mismatch instead of relying on lazy runtime fallback.
|
|
311
312
|
|
|
312
313
|
## 11. Command grammar
|
|
313
314
|
|
|
@@ -325,7 +326,7 @@ Unknown syntax fails deterministically and never guesses a mutation.
|
|
|
325
326
|
|
|
326
327
|
An implementation may claim ScrumRun method 2.0.0 only when it:
|
|
327
328
|
|
|
328
|
-
1. passes positive and negative tests for I-01 through I-
|
|
329
|
+
1. passes positive and negative tests for I-01 through I-22;
|
|
329
330
|
2. enforces every exposed state machine and schema;
|
|
330
331
|
3. proves read-only intake and dry-run migration through full-tree fingerprints;
|
|
331
332
|
4. proves migration failure recovery, rollback safety, and vault exclusion;
|
package/bin/scrumrun.js
CHANGED
|
@@ -25,7 +25,7 @@ const { recordArtifactReview } = require(path.join(root, "lib", "runtime", "revi
|
|
|
25
25
|
const { createMemory, listMemory, showMemory, transitionMemory } = require(path.join(root, "lib", "memory", "service"));
|
|
26
26
|
const { indexPath, indexStatus, mapStatus, queryIndex, rebuildIndex, writeMap } = require(path.join(root, "lib", "memory", "index"));
|
|
27
27
|
const { auditProject } = require(path.join(root, "lib", "v2", "conformance"));
|
|
28
|
-
const { recoverPendingTransactions } = require(path.join(root, "lib", "v2", "transaction"));
|
|
28
|
+
const { recoverPendingTransactions, previewPendingRecovery } = require(path.join(root, "lib", "v2", "transaction"));
|
|
29
29
|
const { containsSecret } = require(path.join(root, "lib", "security", "secrets"));
|
|
30
30
|
|
|
31
31
|
const COMMANDS = ["sc"];
|
|
@@ -1371,6 +1371,37 @@ function executeRootRoute(route) {
|
|
|
1371
1371
|
console.log(`Created retry ${result.run.id} for ${result.task.id} (attempt ${result.run.attempt}).`);
|
|
1372
1372
|
return;
|
|
1373
1373
|
}
|
|
1374
|
+
if (noun === "plan" && subject === "run" && routeArgs[0] === "--render") {
|
|
1375
|
+
const { renderRunFromDisk } = require(path.join(root, "lib", "commands", "run-render"));
|
|
1376
|
+
const runId = routeArgs[1];
|
|
1377
|
+
if (!runId) {
|
|
1378
|
+
console.error("--render requires a RUN-NNN id.");
|
|
1379
|
+
process.exitCode = 1;
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
try {
|
|
1383
|
+
console.log(renderRunFromDisk(process.cwd(), runId));
|
|
1384
|
+
} catch (error) {
|
|
1385
|
+
console.error(error.message);
|
|
1386
|
+
process.exitCode = 1;
|
|
1387
|
+
}
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
if (noun === "plan" && subject === "run" && routeArgs[0] === "--stats") {
|
|
1391
|
+
const { computeStats, renderStats } = require(path.join(root, "lib", "commands", "run-stats"));
|
|
1392
|
+
const filters = {
|
|
1393
|
+
task: optionValue(routeArgs.slice(1), "--task"),
|
|
1394
|
+
feature: optionValue(routeArgs.slice(1), "--feature"),
|
|
1395
|
+
sprint: optionValue(routeArgs.slice(1), "--sprint")
|
|
1396
|
+
};
|
|
1397
|
+
const summary = computeStats(process.cwd(), filters);
|
|
1398
|
+
if (routeArgs.includes("--json")) {
|
|
1399
|
+
console.log(JSON.stringify({ filters, summary }, null, 2));
|
|
1400
|
+
} else {
|
|
1401
|
+
console.log(renderStats(summary, filters));
|
|
1402
|
+
}
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1374
1405
|
if (noun === "plan" && ["task", "run"].includes(subject) && ["--list", "--show"].includes(routeArgs[0])) {
|
|
1375
1406
|
const repository = new ArtifactRepository(projectFile());
|
|
1376
1407
|
if (routeArgs[0] === "--list") {
|
|
@@ -1462,7 +1493,7 @@ function executeRootRoute(route) {
|
|
|
1462
1493
|
if (noun === "config" && subject === "migrate") return runMigration(routeArgs);
|
|
1463
1494
|
if (noun === "config" && subject === "doctor") {
|
|
1464
1495
|
const target = ["all", "codex", "opencode", "claude"].includes(routeArgs[0]) ? routeArgs[0] : "all";
|
|
1465
|
-
return doctor(target, { strict: routeArgs.includes("--strict"), recover: routeArgs.includes("--recover") });
|
|
1496
|
+
return doctor(target, { strict: routeArgs.includes("--strict"), recover: routeArgs.includes("--recover"), dryRun: routeArgs.includes("--dry-run") });
|
|
1466
1497
|
}
|
|
1467
1498
|
if (noun === "config" && subject === "update") {
|
|
1468
1499
|
const target = ["all", "codex", "opencode", "claude"].includes(routeArgs[0]) ? routeArgs[0] : "all";
|
|
@@ -2279,7 +2310,7 @@ function runMigration(parts) {
|
|
|
2279
2310
|
}
|
|
2280
2311
|
}
|
|
2281
2312
|
|
|
2282
|
-
function doctor(target = "all", { compatibility = false, strict = false, recover = false } = {}) {
|
|
2313
|
+
function doctor(target = "all", { compatibility = false, strict = false, recover = false, dryRun = false } = {}) {
|
|
2283
2314
|
const home = os.homedir();
|
|
2284
2315
|
const checks = [];
|
|
2285
2316
|
const commands = compatibility ? [...COMMANDS, ...COMPATIBILITY_COMMANDS] : COMMANDS;
|
|
@@ -2288,6 +2319,33 @@ function doctor(target = "all", { compatibility = false, strict = false, recover
|
|
|
2288
2319
|
if (recover) {
|
|
2289
2320
|
const scrumDir = path.join(process.cwd(), ".scrumrun");
|
|
2290
2321
|
if (!fs.existsSync(scrumDir)) throw new Error("Cannot recover transactions outside a ScrumRun project.");
|
|
2322
|
+
if (dryRun) {
|
|
2323
|
+
const preview = previewPendingRecovery(scrumDir);
|
|
2324
|
+
if (preview.errors.length) {
|
|
2325
|
+
for (const err of preview.errors) console.error(`dry-run: ${err}`);
|
|
2326
|
+
process.exitCode = 1;
|
|
2327
|
+
return;
|
|
2328
|
+
}
|
|
2329
|
+
if (!preview.plans.length) {
|
|
2330
|
+
console.log("dry-run: no pending kernel transactions to recover");
|
|
2331
|
+
} else {
|
|
2332
|
+
console.log(`dry-run: ${preview.plans.length} pending transaction(s) would be handled as follows:`);
|
|
2333
|
+
for (const plan of preview.plans) {
|
|
2334
|
+
const header = ` ${plan.id} (${plan.status}) → ${plan.action}${plan.name ? ` [${plan.name}]` : ""}`;
|
|
2335
|
+
console.log(header);
|
|
2336
|
+
for (const warning of plan.warnings) console.log(` ! ${warning}`);
|
|
2337
|
+
for (const file of plan.files) console.log(` · ${file.relative} (${file.from} → ${file.to})`);
|
|
2338
|
+
}
|
|
2339
|
+
const blocked = preview.plans.filter((plan) => plan.action === "blocked").length;
|
|
2340
|
+
if (blocked) {
|
|
2341
|
+
console.log(`dry-run: ${blocked} transaction(s) cannot be recovered automatically.`);
|
|
2342
|
+
process.exitCode = 1;
|
|
2343
|
+
} else {
|
|
2344
|
+
console.log("dry-run: safe to re-run without --dry-run to apply.");
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2291
2349
|
const recovered = recoverPendingTransactions(scrumDir);
|
|
2292
2350
|
console.log(recovered.length
|
|
2293
2351
|
? `recovered ${recovered.map((item) => `${item.id}:${item.action}`).join(", ")}`
|
|
@@ -2409,7 +2467,7 @@ if (!command || command === "--help" || command === "-h") {
|
|
|
2409
2467
|
runMigration(args.slice(1));
|
|
2410
2468
|
} else if (command === "doctor") {
|
|
2411
2469
|
const target = ["all", "codex", "opencode", "claude"].includes(args[1]) ? args[1] : "all";
|
|
2412
|
-
doctor(target, { compatibility: args.includes("--compat"), strict: args.includes("--strict"), recover: args.includes("--recover") });
|
|
2470
|
+
doctor(target, { compatibility: args.includes("--compat"), strict: args.includes("--strict"), recover: args.includes("--recover"), dryRun: args.includes("--dry-run") });
|
|
2413
2471
|
} else if (command === "claude") {
|
|
2414
2472
|
const sub = args[1];
|
|
2415
2473
|
if (sub === "install" || sub === "update") {
|
package/docs/DEMO.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Demo recording script
|
|
2
|
+
|
|
3
|
+
40-second asciicast that shows a real intake → approve → run → memory
|
|
4
|
+
loop. Aim for a public URL you can embed in the README under the pitch.
|
|
5
|
+
|
|
6
|
+
## Setup
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
brew install asciinema # or: sudo apt install asciinema
|
|
10
|
+
asciinema --version # confirm >= 2.4
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Optional but recommended: `agg` to convert the `.cast` file to an SVG or
|
|
14
|
+
animated GIF for wider viewer support.
|
|
15
|
+
|
|
16
|
+
## Recording
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# 1. Start a clean throwaway project
|
|
20
|
+
mkdir /tmp/scrumrun-demo && cd /tmp/scrumrun-demo
|
|
21
|
+
git init -q
|
|
22
|
+
|
|
23
|
+
# 2. Start recording
|
|
24
|
+
asciinema rec -c "zsh" --idle-time-limit 2 --title "ScrumRun in 40 seconds" demo.cast
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Inside the recording, run these commands slowly (about 5 seconds of
|
|
28
|
+
reading between each), typing them yourself so viewers see the cadence:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx scrumrun@latest install --client claude
|
|
32
|
+
npx scrumrun@latest init
|
|
33
|
+
npx scrumrun@latest sc plan intake "Fix duplicate charges after refresh"
|
|
34
|
+
# copy the approval token printed above
|
|
35
|
+
npx scrumrun@latest sc plan intake --approve <token>
|
|
36
|
+
npx scrumrun@latest sc plan run --render RUN-001
|
|
37
|
+
npx scrumrun@latest sc plan run --stats
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Exit the shell (`exit`) to stop the recording.
|
|
41
|
+
|
|
42
|
+
## Publishing
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
asciinema upload demo.cast # returns a URL like https://asciinema.org/a/<id>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or, for a self-hosted, browser-friendly SVG:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
agg demo.cast demo.svg
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Where to link it
|
|
55
|
+
|
|
56
|
+
- `README.md`, immediately after the tagline and before "The model".
|
|
57
|
+
- LP hero on `scrumrun.dev`, as a small "Watch (40s)" link near the
|
|
58
|
+
`Start with ScrumRun` CTA.
|
|
59
|
+
|
|
60
|
+
## Script notes
|
|
61
|
+
|
|
62
|
+
- Keep it under 45 seconds. Cut any dead air on export with
|
|
63
|
+
`asciinema play --idle-time-limit 1`.
|
|
64
|
+
- Use a light-on-dark terminal theme that matches the LP palette
|
|
65
|
+
(`#08090a` background, `#c9ff5c` cursor if possible).
|
|
66
|
+
- Do not record real secrets — the throwaway project keeps
|
|
67
|
+
`.scrumrun/vault.local.md` empty.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Error codes
|
|
2
|
+
|
|
3
|
+
Every failure that ScrumRun surfaces to the user carries a stable
|
|
4
|
+
`SR-E-NNN` code. Codes never change meaning once assigned; retired
|
|
5
|
+
entries stay in the catalog with `retired: true` so historical logs
|
|
6
|
+
remain resolvable.
|
|
7
|
+
|
|
8
|
+
The canonical source is [`lib/errors.js`](../lib/errors.js). This document
|
|
9
|
+
is a human view of the same catalog, grouped by area.
|
|
10
|
+
|
|
11
|
+
Roll-out is incremental: high-visibility error sites (intake, approval,
|
|
12
|
+
run transitions, doctor, edit permits, transactions) adopt the codes
|
|
13
|
+
first. Uncoded errors are still valid — they simply do not carry an
|
|
14
|
+
`SR-E-NNN` prefix yet.
|
|
15
|
+
|
|
16
|
+
## How to use them
|
|
17
|
+
|
|
18
|
+
When a CLI output looks like:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
SR-E-102 Run transition rejected: missing validation or learning evidence.
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- The **code** is a stable anchor for logs, dashboards, and support
|
|
25
|
+
conversations. Grep it. Link to it.
|
|
26
|
+
- The **summary** describes what went wrong.
|
|
27
|
+
- The **remediation** describes what to try next; the CLI copy is
|
|
28
|
+
identical to the entry in this document.
|
|
29
|
+
|
|
30
|
+
## Ranges
|
|
31
|
+
|
|
32
|
+
| Range | Area |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `SR-E-001..049` | Request and intake |
|
|
35
|
+
| `SR-E-050..099` | Approvals, tokens |
|
|
36
|
+
| `SR-E-100..149` | Runs and ledger |
|
|
37
|
+
| `SR-E-150..199` | Guardrails, policy, obligations |
|
|
38
|
+
| `SR-E-200..249` | Edit permits (Mutation Gateway) |
|
|
39
|
+
| `SR-E-250..299` | Transactions and recovery |
|
|
40
|
+
| `SR-E-300..349` | Memory (facts, decisions, insights, dossiers, vault) |
|
|
41
|
+
| `SR-E-350..399` | Semantic index and code intelligence |
|
|
42
|
+
| `SR-E-400..449` | Migration and update |
|
|
43
|
+
| `SR-E-450..499` | Conformance and doctor |
|
|
44
|
+
| `SR-E-500..549` | Configuration and installation |
|
|
45
|
+
|
|
46
|
+
## Catalog
|
|
47
|
+
|
|
48
|
+
### Request and intake
|
|
49
|
+
|
|
50
|
+
| Code | Summary | Remediation |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| `SR-E-001` | Intake request is missing. | Provide a natural-language request or use `--request "..."`. |
|
|
53
|
+
| `SR-E-002` | Intake payload contains secret-like content. | Remove the secret from the request or reference a vault entry by path. |
|
|
54
|
+
| `SR-E-003` | Intake classification could not be produced. | Rephrase the request in one sentence describing the observable problem. |
|
|
55
|
+
|
|
56
|
+
### Approvals
|
|
57
|
+
|
|
58
|
+
| Code | Summary | Remediation |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `SR-E-050` | Approval token is malformed. | Re-run the intake and copy the token exactly as printed after `Approval:`. |
|
|
61
|
+
| `SR-E-051` | Approval token signature does not match this repository. | Tokens are bound to the project fingerprint. Re-run intake from the repo the change targets. |
|
|
62
|
+
| `SR-E-052` | Approval token expired. | Re-run intake to obtain a fresh token before approving. |
|
|
63
|
+
|
|
64
|
+
### Runs and ledger
|
|
65
|
+
|
|
66
|
+
| Code | Summary | Remediation |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `SR-E-100` | Run not found. | Check the RUN-NNN id with `sc plan run --list`. |
|
|
69
|
+
| `SR-E-101` | Run ledger contains an invalid event. | Inspect the reported event id; use `sc plan run --render` for a human view or restore from git history. |
|
|
70
|
+
| `SR-E-102` | Run transition rejected: missing validation or learning evidence. | Complete `--validate` and `--learn` with typed evidence before `--complete`. |
|
|
71
|
+
| `SR-E-103` | Retry rejected because the previous Run is still active. | Complete, fail, or block the current Run before creating a retry. |
|
|
72
|
+
|
|
73
|
+
### Guardrails
|
|
74
|
+
|
|
75
|
+
| Code | Summary | Remediation |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `SR-E-150` | Guardrail check blocked the operation. | Read the reported GR-NNN, satisfy or retire it explicitly; guardrails never bypass silently. |
|
|
78
|
+
| `SR-E-151` | Guardrail obligation is still pending. | Resolve each pending guardrail via `sc plan run --satisfy-guardrail` before completing the Run. |
|
|
79
|
+
| `SR-E-152` | Guardrail declaration is malformed. | Every active guardrail requires Status, Enforcement, Scope, and Rule fields; check `.scrumrun/guardrails.md`. |
|
|
80
|
+
|
|
81
|
+
### Edit permits (Mutation Gateway)
|
|
82
|
+
|
|
83
|
+
| Code | Summary | Remediation |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `SR-E-200` | No edit permit for this path. | Request one with `sc plan run --authorize-mutation RUN-NNN --path <path>` before editing canonical or source files. |
|
|
86
|
+
| `SR-E-201` | Edit permit expired. | Permits last 15 minutes. Authorize a new one and record the change immediately. |
|
|
87
|
+
| `SR-E-202` | Edit permit path scope mismatch. | The permit does not cover the modified path. Request a new permit that lists it. |
|
|
88
|
+
| `SR-E-203` | File hash changed unexpectedly since the permit was issued. | Someone else modified the file. Re-plan the change and request a fresh permit. |
|
|
89
|
+
|
|
90
|
+
### Transactions and recovery
|
|
91
|
+
|
|
92
|
+
| Code | Summary | Remediation |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| `SR-E-250` | Pending kernel transaction cannot be recovered automatically. | Run `sc config doctor --recover --dry-run` to preview; if it shows "would overwrite owner changes", reconcile the file manually before applying. |
|
|
95
|
+
| `SR-E-251` | Journal fails integrity check. | Inspect `.scrumrun/.transactions/pending`. Do not delete; contact support or restore from backup. |
|
|
96
|
+
|
|
97
|
+
### Memory
|
|
98
|
+
|
|
99
|
+
| Code | Summary | Remediation |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| `SR-E-300` | Memory candidate rejected: missing resolvable evidence. | Attach at least one `--evidence` path or `sc knowledge <subject> --propose` before `--confirm`. |
|
|
102
|
+
| `SR-E-301` | Attempt to write into vault via canonical channel. | `vault.local.md` is local-only and never indexed. Edit the file directly. |
|
|
103
|
+
|
|
104
|
+
### Semantic index and code intelligence
|
|
105
|
+
|
|
106
|
+
| Code | Summary | Remediation |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `SR-E-350` | Semantic index is stale. | Rebuild with `sc knowledge map --build`. Cache is disposable; canonical memory is unaffected. |
|
|
109
|
+
| `SR-E-351` | Search backend advertised in the cache does not match this runtime. | Delete `.scrumrun/.cache/semantic-index.sqlite` and re-run any `sc knowledge` query to rebuild against the current runtime. |
|
|
110
|
+
|
|
111
|
+
### Migration
|
|
112
|
+
|
|
113
|
+
| Code | Summary | Remediation |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| `SR-E-400` | Migration preflight failed. | Run `npx scrumrun@latest update` (dry) to see blockers, resolve them, then apply with `--migrate`. |
|
|
116
|
+
| `SR-E-401` | Migration rollback requested but no backup was found. | Rollback needs the ignored byte-exact backup created during `--migrate`. Restore from version control if the backup is gone. |
|
|
117
|
+
|
|
118
|
+
### Conformance and doctor
|
|
119
|
+
|
|
120
|
+
| Code | Summary | Remediation |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `SR-E-450` | Conformance check failed. | The reported invariant identifies the exact violation; the message includes the file and expected shape. |
|
|
123
|
+
| `SR-E-451` | Installed client asset is stale. | Re-run `npx scrumrun@latest update` for the specific client. `doctor --strict` shows which files diverge. |
|
|
124
|
+
|
|
125
|
+
### Configuration and installation
|
|
126
|
+
|
|
127
|
+
| Code | Summary | Remediation |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| `SR-E-500` | ScrumRun project not initialized. | Run `npx scrumrun@latest init` in the repository root. |
|
|
130
|
+
| `SR-E-501` | Unsupported Node.js runtime. | ScrumRun requires Node.js >=22.13.0 for native SQLite. Upgrade Node and retry. |
|
|
131
|
+
|
|
132
|
+
## Adding a new code
|
|
133
|
+
|
|
134
|
+
1. Reserve the next number in the appropriate range.
|
|
135
|
+
2. Add an entry to `CATALOG` in `lib/errors.js` with `summary` and
|
|
136
|
+
`remediation`.
|
|
137
|
+
3. Throw `new ScrumRunError("SR-E-XYZ", "...")` from the site that
|
|
138
|
+
raises it.
|
|
139
|
+
4. Add or update the row in this document.
|
|
140
|
+
5. Never rename or reassign an existing code. If a code is no longer
|
|
141
|
+
thrown, mark it `retired: true` and keep the entry.
|
package/docs/INDEX.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Documentation index
|
|
2
|
+
|
|
3
|
+
Read in the order that matches how deep you want to go.
|
|
4
|
+
|
|
5
|
+
## Start here
|
|
6
|
+
|
|
7
|
+
| Doc | For | Time |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| [`QUICKSTART.md`](QUICKSTART.md) | Your first Run in under 10 minutes. Zero jargon. | 5 min |
|
|
10
|
+
| [`../README.md`](../README.md) | Pitch, install, useful commands. | 8 min |
|
|
11
|
+
|
|
12
|
+
## Understand the method
|
|
13
|
+
|
|
14
|
+
| Doc | For | Time |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| [`../CORE.md`](../CORE.md) | The operational guide agents follow at runtime. Portable across clients. | 15 min |
|
|
17
|
+
| [`SEMANTIC-MEMORY.md`](SEMANTIC-MEMORY.md) | How facts, decisions, insights, and dossiers fit together. | 10 min |
|
|
18
|
+
| [`SCHEMA.md`](SCHEMA.md) | The generated executable schema: ids, cardinalities, initial states, transitions. | 10 min |
|
|
19
|
+
|
|
20
|
+
## Verify and troubleshoot
|
|
21
|
+
|
|
22
|
+
| Doc | For | Time |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| [`TROUBLESHOOTING.md`](TROUBLESHOOTING.md) | Install, migration, and daily-use issues with fixes. | as needed |
|
|
25
|
+
| [`ERROR-CODES.md`](ERROR-CODES.md) | Catalog of stable `SR-E-NNN` codes with summary + remediation for every user-facing failure. | as needed |
|
|
26
|
+
| [`RELEASE-SCORECARD.md`](RELEASE-SCORECARD.md) | Evidence-backed local readiness signals per release. | 5 min |
|
|
27
|
+
|
|
28
|
+
## Go deep
|
|
29
|
+
|
|
30
|
+
| Doc | For | Time |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| [`../SPEC.md`](../SPEC.md) | The normative specification: state machines, invariants, composition, precedence, conformance. | 30 min |
|
|
33
|
+
| [`../DECISIONS.md`](../DECISIONS.md) | Architecture Decision Records: why the method is the way it is. Every ADR carries context, decision, alternatives considered, consequences. | browse |
|
|
34
|
+
| [`../CHANGELOG.md`](../CHANGELOG.md) | What shipped, what broke, what got safer. | as needed |
|
|
35
|
+
| [`../MIGRATION-1-to-2.md`](../MIGRATION-1-to-2.md) | Upgrade, verify, rollback, and recover for existing v1 projects. | as needed |
|
|
36
|
+
|
|
37
|
+
## Reading order recommendations
|
|
38
|
+
|
|
39
|
+
**You just installed ScrumRun.**
|
|
40
|
+
`QUICKSTART.md` → `README.md` → `CORE.md` when you want more.
|
|
41
|
+
|
|
42
|
+
**You are integrating ScrumRun with a new AI client.**
|
|
43
|
+
`CORE.md` → `SCHEMA.md` → `SPEC.md`.
|
|
44
|
+
|
|
45
|
+
**You are upgrading from v1.**
|
|
46
|
+
`../MIGRATION-1-to-2.md` → `../CHANGELOG.md` → `TROUBLESHOOTING.md`.
|
|
47
|
+
|
|
48
|
+
**You want to know why a design choice exists.**
|
|
49
|
+
`../DECISIONS.md`. Search for the topic; every ADR is self-contained.
|
|
50
|
+
|
|
51
|
+
**Something broke.**
|
|
52
|
+
`TROUBLESHOOTING.md` first. If the code shown starts with `SR-E-`, the
|
|
53
|
+
message points to the exact remediation step.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Quickstart
|
|
2
|
+
|
|
3
|
+
Get from "never heard of it" to your first approved Run in under 10 minutes.
|
|
4
|
+
No `SPEC.md` reading required.
|
|
5
|
+
|
|
6
|
+
## The three-minute mental model
|
|
7
|
+
|
|
8
|
+
ScrumRun gives an AI coding agent a small vocabulary and a project memory.
|
|
9
|
+
|
|
10
|
+
- **Task** — one atomic piece of work.
|
|
11
|
+
- **Sprint** — a batch of Tasks grouped by time or theme. Optional.
|
|
12
|
+
- **Run** — one attempt at executing a Task. Retries create new Runs; the
|
|
13
|
+
previous one is never overwritten.
|
|
14
|
+
- **Feature** — a bigger initiative that groups Tasks and its own decisions.
|
|
15
|
+
- **Memory** — what the project learned: facts, decisions, insights, dossiers.
|
|
16
|
+
|
|
17
|
+
Everything lives as Markdown under `.scrumrun/`. Any Markdown-capable agent
|
|
18
|
+
can follow it.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
You need Node.js 22.13 or newer.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx scrumrun@latest install # installs the agent integration for your client
|
|
26
|
+
npx scrumrun@latest init # creates the .scrumrun/ tree in the current repo
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
By default `.scrumrun/` is added to `.git/info/exclude` — the methodology
|
|
30
|
+
stays local to your machine. Add `--shared` to `init` if your team wants
|
|
31
|
+
to commit it.
|
|
32
|
+
|
|
33
|
+
## Your first intake
|
|
34
|
+
|
|
35
|
+
You do not have to remember any commands. Just describe the problem in
|
|
36
|
+
natural language to your AI client. ScrumRun classifies it before writing
|
|
37
|
+
anything.
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
Owner: The checkout charges twice when the page is refreshed.
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The agent responds with an intake summary:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
Classification: Task (fix)
|
|
47
|
+
Risk: high — financial path
|
|
48
|
+
Why: Payment behavior changed after completed work; corrective Task
|
|
49
|
+
linked to the original Run history.
|
|
50
|
+
Next: /sc plan intake "double charge on refresh"
|
|
51
|
+
Awaiting owner approval.
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Nothing has been written yet. The classification, risk, and plan are
|
|
55
|
+
proposals. Approving is one command:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npx scrumrun@latest sc plan intake --approve <token>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Only then does a Task and a Run get created.
|
|
62
|
+
|
|
63
|
+
## Watching the Run
|
|
64
|
+
|
|
65
|
+
The agent executes inside the approved scope. Every step lands in an
|
|
66
|
+
append-only ledger under `.scrumrun/runs/RUN-NNN.md` with a stable event
|
|
67
|
+
id, timestamp, actor, reason, and typed evidence.
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
RUN-001-EVT-001 planned by owner reason: "checkout double-charge fix"
|
|
71
|
+
RUN-001-EVT-002 executed command: npm test → 132 passed
|
|
72
|
+
RUN-001-EVT-003 validated (REV-001) guardrail checks: passed
|
|
73
|
+
RUN-001-EVT-004 learned (INS-001) "refresh triggers duplicate submit"
|
|
74
|
+
RUN-001-EVT-005 completed
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
A retry does not overwrite `RUN-001`. It creates `RUN-002` beside it. The
|
|
78
|
+
old attempt stays as evidence.
|
|
79
|
+
|
|
80
|
+
You can render a Run's ledger as a human timeline instead of reading the
|
|
81
|
+
raw JSON:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npx scrumrun@latest sc plan run --render RUN-001
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
For aggregate signal across every Run in the project — status mix, p50
|
|
88
|
+
and p95 time in `VALIDATING`, retries per Task, guardrail check counts —
|
|
89
|
+
use `--stats`:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npx scrumrun@latest sc plan run --stats
|
|
93
|
+
npx scrumrun@latest sc plan run --stats --task TASK-001
|
|
94
|
+
npx scrumrun@latest sc plan run --stats --json
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Reading the memory
|
|
98
|
+
|
|
99
|
+
As you work, the project accumulates:
|
|
100
|
+
|
|
101
|
+
- **Facts** (`K-NNN`) — reviewed truths about the code.
|
|
102
|
+
- **Decisions** (`DEC-NNN`) — normative choices that constrain future work.
|
|
103
|
+
- **Insights** (`INS-NNN`) — context, rationale, warnings, trade-offs.
|
|
104
|
+
- **Dossiers** (`DOS-NNN`) — curated topic or module deep-dives.
|
|
105
|
+
|
|
106
|
+
Nothing becomes canonical without confirmation. AI extraction creates
|
|
107
|
+
candidates; you promote them with evidence.
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npx scrumrun@latest sc knowledge insight --propose "..." --evidence src/foo.ts
|
|
111
|
+
npx scrumrun@latest sc knowledge insight --confirm INS-001
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Ask the agent things like *"why is calculateFinalPrice in checkout?"* or
|
|
115
|
+
*"which decision constrains the pricing module?"* — it answers by reading
|
|
116
|
+
the memory index, not by guessing.
|
|
117
|
+
|
|
118
|
+
## What to read next
|
|
119
|
+
|
|
120
|
+
- [`docs/INDEX.md`](INDEX.md) — the full documentation map.
|
|
121
|
+
- [`CORE.md`](../CORE.md) — the operational guide agents follow at runtime.
|
|
122
|
+
- [`docs/SEMANTIC-MEMORY.md`](SEMANTIC-MEMORY.md) — how facts, decisions,
|
|
123
|
+
insights, and dossiers fit together.
|
|
124
|
+
- [`docs/TROUBLESHOOTING.md`](TROUBLESHOOTING.md) — for common install and
|
|
125
|
+
migration issues.
|
|
126
|
+
- [`SPEC.md`](../SPEC.md) — the formal specification, when you want the
|
|
127
|
+
invariants and state machines.
|
|
128
|
+
|
|
129
|
+
## Common questions
|
|
130
|
+
|
|
131
|
+
**Do I have to type `/sc` commands?** No. Natural language is the primary
|
|
132
|
+
entry point. The `/sc` grammar exists for scripting and reproducibility.
|
|
133
|
+
|
|
134
|
+
**What if I already use v1?** Run `npx scrumrun@latest update` for a
|
|
135
|
+
read-only migration preflight, then `update --migrate` when you are
|
|
136
|
+
satisfied with the plan.
|
|
137
|
+
|
|
138
|
+
**Can I hide the `.scrumrun/` tree from Git?** It is hidden by default
|
|
139
|
+
(local mode). Use `--shared` if you want to commit it.
|
|
140
|
+
|
|
141
|
+
**Where do secrets live?** Never in canonical artifacts. Use
|
|
142
|
+
`.scrumrun/vault.local.md` for local-only plaintext that is never indexed
|
|
143
|
+
and never migrated.
|
|
144
|
+
|
|
145
|
+
**How do I add my own project rules?** Edit `.scrumrun/guardrails.md`.
|
|
146
|
+
Each rule has `Status`, `Enforcement`, `Scope`, and `Rule` fields and gets
|
|
147
|
+
a stable `GR-NNN` id.
|
|
148
|
+
|
|
149
|
+
**What if something goes wrong?** Every user-facing failure carries a
|
|
150
|
+
stable `SR-E-NNN` code. Grep it in [`docs/ERROR-CODES.md`](ERROR-CODES.md)
|
|
151
|
+
for the exact remediation. If a canonical transaction was interrupted,
|
|
152
|
+
preview the repair before running it:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
npx scrumrun@latest sc config doctor --recover --dry-run
|
|
156
|
+
```
|
package/lib/commands/manifest.js
CHANGED
|
@@ -12,6 +12,8 @@ const nouns = Object.freeze({
|
|
|
12
12
|
run: [
|
|
13
13
|
"--list",
|
|
14
14
|
"--show",
|
|
15
|
+
"--render <RUN-NNN>",
|
|
16
|
+
"--stats [--task <TASK-NNN>] [--feature <FEAT-NNN>] [--sprint <SPRINT-NNN>] [--json]",
|
|
15
17
|
"--authorize-mutation <RUN-NNN> --path <relative-path>",
|
|
16
18
|
"--record-mutation <RUN-NNN> --permit <MUT-id> [--note] [--actor]",
|
|
17
19
|
"--satisfy-guardrail <RUN-NNN> --guardrail <GR-NNN> [--note] [--evidence] [--review] [--migration] [--actor]",
|
|
@@ -62,7 +64,7 @@ const nouns = Object.freeze({
|
|
|
62
64
|
init: ["--local", "--shared", "--lean", "--no-agent-hint", "--force"],
|
|
63
65
|
update: ["all [--migrate]", "codex [--migrate]", "opencode [--migrate]", "claude [--migrate]"],
|
|
64
66
|
migrate: ["--to 2 --dry-run", "--to 2 --apply", "--to 2 --rollback"],
|
|
65
|
-
doctor: ["all [--strict] [--recover]", "codex [--strict] [--recover]", "opencode [--strict] [--recover]", "claude [--strict] [--recover]"],
|
|
67
|
+
doctor: ["all [--strict] [--recover] [--dry-run]", "codex [--strict] [--recover] [--dry-run]", "opencode [--strict] [--recover] [--dry-run]", "claude [--strict] [--recover] [--dry-run]"],
|
|
66
68
|
uninstall: ["--force"],
|
|
67
69
|
help: ["<topic>"]
|
|
68
70
|
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
|
|
6
|
+
const { parseRunLedger } = require("../runtime/run-ledger");
|
|
7
|
+
|
|
8
|
+
const RULE = "─".repeat(72);
|
|
9
|
+
|
|
10
|
+
function formatInstant(iso) {
|
|
11
|
+
if (!iso) return " ";
|
|
12
|
+
const date = new Date(iso);
|
|
13
|
+
if (Number.isNaN(date.valueOf())) return String(iso);
|
|
14
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
15
|
+
return `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())} ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())} UTC`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function humanDuration(startIso, endIso) {
|
|
19
|
+
if (!startIso || !endIso) return null;
|
|
20
|
+
const delta = new Date(endIso).valueOf() - new Date(startIso).valueOf();
|
|
21
|
+
if (!Number.isFinite(delta) || delta < 0) return null;
|
|
22
|
+
const seconds = Math.round(delta / 1000);
|
|
23
|
+
if (seconds < 60) return `${seconds}s`;
|
|
24
|
+
const minutes = Math.floor(seconds / 60);
|
|
25
|
+
const rem = seconds % 60;
|
|
26
|
+
if (minutes < 60) return rem ? `${minutes}m ${rem}s` : `${minutes}m`;
|
|
27
|
+
const hours = Math.floor(minutes / 60);
|
|
28
|
+
const mrem = minutes % 60;
|
|
29
|
+
return mrem ? `${hours}h ${mrem}m` : `${hours}h`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readFrontmatter(body) {
|
|
33
|
+
const match = String(body || "").match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
34
|
+
if (!match) return {};
|
|
35
|
+
const result = {};
|
|
36
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
37
|
+
const kv = line.match(/^([a-z_][a-z0-9_]*):\s*(.*)$/i);
|
|
38
|
+
if (!kv) continue;
|
|
39
|
+
let value = kv[2].trim();
|
|
40
|
+
if (value === "null") value = null;
|
|
41
|
+
else if (/^-?\d+$/.test(value)) value = Number(value);
|
|
42
|
+
result[kv[1]] = value;
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readTitle(body) {
|
|
48
|
+
const match = String(body || "").match(/^#\s+([^\r\n]+)/m);
|
|
49
|
+
return match ? match[1].trim() : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function summarizeEvidence(evidence) {
|
|
53
|
+
if (!Array.isArray(evidence) || !evidence.length) return [];
|
|
54
|
+
return evidence.map((item) => {
|
|
55
|
+
if (!item || typeof item !== "object") return String(item);
|
|
56
|
+
const kind = item.kind || "evidence";
|
|
57
|
+
if (item.ref && item.summary) return `${kind}: ${item.ref} — ${item.summary}`;
|
|
58
|
+
if (item.ref) return `${kind}: ${item.ref}`;
|
|
59
|
+
if (item.command) return `${kind}: ${item.command}${item.result ? ` → ${item.result}` : ""}`;
|
|
60
|
+
if (item.test) return `${kind}: ${item.test}${item.result ? ` → ${item.result}` : ""}`;
|
|
61
|
+
if (item.summary) return `${kind}: ${item.summary}`;
|
|
62
|
+
return kind;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function headline(event) {
|
|
67
|
+
const type = event.type || "event";
|
|
68
|
+
if (type === "transition") {
|
|
69
|
+
const from = event.from || "?";
|
|
70
|
+
const to = event.to || "?";
|
|
71
|
+
return `${from} → ${to}`;
|
|
72
|
+
}
|
|
73
|
+
if (type === "snapshot") return "snapshot (baseline)";
|
|
74
|
+
if (type === "guardrail") {
|
|
75
|
+
const status = event.status || event.result || "recorded";
|
|
76
|
+
return `guardrail ${event.guardrail || event.code || ""} ${status}`.trim();
|
|
77
|
+
}
|
|
78
|
+
if (type === "mutation") {
|
|
79
|
+
const permit = event.mutation || event.permit || "";
|
|
80
|
+
return `mutation ${permit}`.trim();
|
|
81
|
+
}
|
|
82
|
+
return type;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function renderEvent(event, indent = " ") {
|
|
86
|
+
const lines = [];
|
|
87
|
+
const time = formatInstant(event.occurred_at);
|
|
88
|
+
const actor = String(event.actor || "?").padEnd(8, " ");
|
|
89
|
+
lines.push(`${time} ${actor}${event.id || ""} ${headline(event)}`);
|
|
90
|
+
if (event.reason) {
|
|
91
|
+
lines.push(`${indent}reason ${event.reason}`);
|
|
92
|
+
}
|
|
93
|
+
const evidence = summarizeEvidence(event.evidence);
|
|
94
|
+
if (evidence.length) {
|
|
95
|
+
lines.push(`${indent}evidence ${evidence[0]}`);
|
|
96
|
+
for (let i = 1; i < evidence.length; i++) {
|
|
97
|
+
lines.push(`${indent} ${evidence[i]}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (event.type === "mutation" && Array.isArray(event.paths) && event.paths.length) {
|
|
101
|
+
lines.push(`${indent}paths ${event.paths.join(", ")}`);
|
|
102
|
+
}
|
|
103
|
+
if (event.type === "guardrail" && event.gate) {
|
|
104
|
+
lines.push(`${indent}gate ${event.gate}`);
|
|
105
|
+
}
|
|
106
|
+
return lines.join("\n");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function renderRun({ id, file, body }) {
|
|
110
|
+
const frontmatter = readFrontmatter(body);
|
|
111
|
+
const title = readTitle(body) || id;
|
|
112
|
+
const { events, errors } = parseRunLedger(body);
|
|
113
|
+
|
|
114
|
+
const header = [
|
|
115
|
+
`${id} ${title}`,
|
|
116
|
+
`status ${frontmatter.status || "unknown"} task ${frontmatter.task || "-"} attempt ${frontmatter.attempt ?? "-"} sprint ${frontmatter.sprint || "-"}`,
|
|
117
|
+
file ? `file ${path.relative(process.cwd(), file)}` : null,
|
|
118
|
+
RULE
|
|
119
|
+
].filter(Boolean);
|
|
120
|
+
|
|
121
|
+
const body_lines = [];
|
|
122
|
+
if (!events.length) {
|
|
123
|
+
body_lines.push("(no ledger events found)");
|
|
124
|
+
} else {
|
|
125
|
+
for (let i = 0; i < events.length; i++) {
|
|
126
|
+
body_lines.push(renderEvent(events[i]));
|
|
127
|
+
if (i < events.length - 1) body_lines.push("");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const first = events[0]?.occurred_at;
|
|
132
|
+
const last = events[events.length - 1]?.occurred_at;
|
|
133
|
+
const duration = humanDuration(first, last);
|
|
134
|
+
|
|
135
|
+
const footer = [
|
|
136
|
+
RULE,
|
|
137
|
+
`${events.length} event${events.length === 1 ? "" : "s"}${duration ? ` span ${duration}` : ""}`
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
if (errors.length) {
|
|
141
|
+
footer.push("");
|
|
142
|
+
footer.push("Ledger validation warnings:");
|
|
143
|
+
for (const err of errors) footer.push(` - ${err}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return [...header, ...body_lines, ...footer].join("\n");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function locateRun(cwd, runId) {
|
|
150
|
+
const file = path.join(cwd, ".scrumrun", "runs", `${runId}.md`);
|
|
151
|
+
if (!fs.existsSync(file)) return null;
|
|
152
|
+
return { id: runId, file, body: fs.readFileSync(file, "utf8") };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function renderRunFromDisk(cwd, runId) {
|
|
156
|
+
const run = locateRun(cwd, runId);
|
|
157
|
+
if (!run) throw new Error(`Run not found: ${runId}`);
|
|
158
|
+
return renderRun(run);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = { renderRun, renderRunFromDisk, formatInstant, humanDuration, summarizeEvidence };
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
|
|
6
|
+
const { parseRunLedger } = require("../runtime/run-ledger");
|
|
7
|
+
|
|
8
|
+
function readFrontmatter(body) {
|
|
9
|
+
const match = String(body || "").match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
10
|
+
if (!match) return {};
|
|
11
|
+
const result = {};
|
|
12
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
13
|
+
const kv = line.match(/^([a-z_][a-z0-9_]*):\s*(.*)$/i);
|
|
14
|
+
if (!kv) continue;
|
|
15
|
+
let value = kv[2].trim();
|
|
16
|
+
if (value === "null" || value === "") value = null;
|
|
17
|
+
else if (/^-?\d+$/.test(value)) value = Number(value);
|
|
18
|
+
result[kv[1]] = value;
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function loadRuns(cwd) {
|
|
24
|
+
const dir = path.join(cwd, ".scrumrun", "runs");
|
|
25
|
+
if (!fs.existsSync(dir)) return [];
|
|
26
|
+
return fs
|
|
27
|
+
.readdirSync(dir)
|
|
28
|
+
.filter((name) => /^RUN-\d{3,}\.md$/.test(name))
|
|
29
|
+
.sort()
|
|
30
|
+
.map((name) => {
|
|
31
|
+
const file = path.join(dir, name);
|
|
32
|
+
const body = fs.readFileSync(file, "utf8");
|
|
33
|
+
return { id: name.replace(/\.md$/, ""), file, body, frontmatter: readFrontmatter(body) };
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function transitions(events) {
|
|
38
|
+
return events.filter((event) => event && event.type === "transition");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function firstEntryInto(events, status) {
|
|
42
|
+
for (const event of transitions(events)) {
|
|
43
|
+
if (event.to === status) return event.occurred_at;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function lastExitFrom(events, status) {
|
|
49
|
+
let last = null;
|
|
50
|
+
for (const event of transitions(events)) {
|
|
51
|
+
if (event.from === status) last = event.occurred_at;
|
|
52
|
+
}
|
|
53
|
+
return last;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function durationMs(startIso, endIso) {
|
|
57
|
+
if (!startIso || !endIso) return null;
|
|
58
|
+
const delta = new Date(endIso).valueOf() - new Date(startIso).valueOf();
|
|
59
|
+
return Number.isFinite(delta) && delta >= 0 ? delta : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function percentile(sortedMs, p) {
|
|
63
|
+
if (!sortedMs.length) return null;
|
|
64
|
+
const rank = Math.min(sortedMs.length - 1, Math.max(0, Math.floor((p / 100) * sortedMs.length)));
|
|
65
|
+
return sortedMs[rank];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function humanMs(ms) {
|
|
69
|
+
if (ms === null || ms === undefined || !Number.isFinite(ms)) return "-";
|
|
70
|
+
const seconds = Math.round(ms / 1000);
|
|
71
|
+
if (seconds < 60) return `${seconds}s`;
|
|
72
|
+
const minutes = Math.floor(seconds / 60);
|
|
73
|
+
const rem = seconds % 60;
|
|
74
|
+
if (minutes < 60) return rem ? `${minutes}m ${rem}s` : `${minutes}m`;
|
|
75
|
+
const hours = Math.floor(minutes / 60);
|
|
76
|
+
const mrem = minutes % 60;
|
|
77
|
+
return mrem ? `${hours}h ${mrem}m` : `${hours}h`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function computeStats(cwd, filters = {}) {
|
|
81
|
+
const runs = loadRuns(cwd).filter((run) => {
|
|
82
|
+
if (filters.task && run.frontmatter.task !== filters.task) return false;
|
|
83
|
+
if (filters.feature && run.frontmatter.feature !== filters.feature) return false;
|
|
84
|
+
if (filters.sprint && run.frontmatter.sprint !== filters.sprint) return false;
|
|
85
|
+
return true;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const summary = {
|
|
89
|
+
total: runs.length,
|
|
90
|
+
byStatus: {},
|
|
91
|
+
validating: { count: 0, samples: [], p50: null, p95: null, max: null },
|
|
92
|
+
executing: { count: 0, samples: [], p50: null, p95: null, max: null },
|
|
93
|
+
attempts: { max: 0, byTask: {} },
|
|
94
|
+
guardrails: { passed: 0, deferred: 0, blocked: 0 },
|
|
95
|
+
mutations: 0,
|
|
96
|
+
completionsWithFailurePredecessor: 0
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const failedTaskIds = new Set();
|
|
100
|
+
|
|
101
|
+
for (const run of runs) {
|
|
102
|
+
const status = run.frontmatter.status || "unknown";
|
|
103
|
+
summary.byStatus[status] = (summary.byStatus[status] || 0) + 1;
|
|
104
|
+
|
|
105
|
+
const attempt = Number(run.frontmatter.attempt) || 1;
|
|
106
|
+
if (attempt > summary.attempts.max) summary.attempts.max = attempt;
|
|
107
|
+
if (run.frontmatter.task) {
|
|
108
|
+
summary.attempts.byTask[run.frontmatter.task] = Math.max(summary.attempts.byTask[run.frontmatter.task] || 0, attempt);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (status === "failed" && run.frontmatter.task) failedTaskIds.add(run.frontmatter.task);
|
|
112
|
+
if (status === "completed" && run.frontmatter.task && failedTaskIds.has(run.frontmatter.task)) {
|
|
113
|
+
summary.completionsWithFailurePredecessor += 1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const { events } = parseRunLedger(run.body);
|
|
117
|
+
const enterValidating = firstEntryInto(events, "validating");
|
|
118
|
+
const exitValidating = lastExitFrom(events, "validating");
|
|
119
|
+
const validatingMs = durationMs(enterValidating, exitValidating);
|
|
120
|
+
if (validatingMs !== null) {
|
|
121
|
+
summary.validating.count += 1;
|
|
122
|
+
summary.validating.samples.push(validatingMs);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const enterExecuting = firstEntryInto(events, "executing");
|
|
126
|
+
const exitExecuting = lastExitFrom(events, "executing");
|
|
127
|
+
const executingMs = durationMs(enterExecuting, exitExecuting);
|
|
128
|
+
if (executingMs !== null) {
|
|
129
|
+
summary.executing.count += 1;
|
|
130
|
+
summary.executing.samples.push(executingMs);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const event of events) {
|
|
134
|
+
if (event.type === "guardrail") {
|
|
135
|
+
const state = String(event.status || event.result || "").toLowerCase();
|
|
136
|
+
if (state === "passed") summary.guardrails.passed += 1;
|
|
137
|
+
else if (state === "deferred") summary.guardrails.deferred += 1;
|
|
138
|
+
else if (state === "blocked") summary.guardrails.blocked += 1;
|
|
139
|
+
} else if (event.type === "mutation") {
|
|
140
|
+
summary.mutations += 1;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const key of ["validating", "executing"]) {
|
|
146
|
+
const samples = summary[key].samples.slice().sort((a, b) => a - b);
|
|
147
|
+
summary[key].p50 = percentile(samples, 50);
|
|
148
|
+
summary[key].p95 = percentile(samples, 95);
|
|
149
|
+
summary[key].max = samples[samples.length - 1] ?? null;
|
|
150
|
+
delete summary[key].samples;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return summary;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function renderStats(summary, filters = {}) {
|
|
157
|
+
const lines = [];
|
|
158
|
+
const scope = [];
|
|
159
|
+
if (filters.task) scope.push(`task=${filters.task}`);
|
|
160
|
+
if (filters.feature) scope.push(`feature=${filters.feature}`);
|
|
161
|
+
if (filters.sprint) scope.push(`sprint=${filters.sprint}`);
|
|
162
|
+
lines.push(`Run statistics${scope.length ? ` (${scope.join(", ")})` : ""}`);
|
|
163
|
+
lines.push("─".repeat(72));
|
|
164
|
+
lines.push(`total runs ${summary.total}`);
|
|
165
|
+
|
|
166
|
+
const statusEntries = Object.entries(summary.byStatus).sort((a, b) => b[1] - a[1]);
|
|
167
|
+
if (statusEntries.length) {
|
|
168
|
+
lines.push("");
|
|
169
|
+
lines.push("by status");
|
|
170
|
+
for (const [status, count] of statusEntries) {
|
|
171
|
+
lines.push(` ${status.padEnd(30, " ")} ${count}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
lines.push("");
|
|
176
|
+
lines.push("time in VALIDATING");
|
|
177
|
+
lines.push(` runs sampled ${summary.validating.count}`);
|
|
178
|
+
lines.push(` p50 ${humanMs(summary.validating.p50)}`);
|
|
179
|
+
lines.push(` p95 ${humanMs(summary.validating.p95)}`);
|
|
180
|
+
lines.push(` max ${humanMs(summary.validating.max)}`);
|
|
181
|
+
|
|
182
|
+
lines.push("");
|
|
183
|
+
lines.push("time in EXECUTING");
|
|
184
|
+
lines.push(` runs sampled ${summary.executing.count}`);
|
|
185
|
+
lines.push(` p50 ${humanMs(summary.executing.p50)}`);
|
|
186
|
+
lines.push(` p95 ${humanMs(summary.executing.p95)}`);
|
|
187
|
+
lines.push(` max ${humanMs(summary.executing.max)}`);
|
|
188
|
+
|
|
189
|
+
lines.push("");
|
|
190
|
+
lines.push("retries and recovery");
|
|
191
|
+
lines.push(` highest attempt observed ${summary.attempts.max}`);
|
|
192
|
+
const retriedTasks = Object.entries(summary.attempts.byTask).filter(([, n]) => n > 1);
|
|
193
|
+
lines.push(` tasks that retried ${retriedTasks.length}`);
|
|
194
|
+
lines.push(` completions after prior fail ${summary.completionsWithFailurePredecessor}`);
|
|
195
|
+
|
|
196
|
+
lines.push("");
|
|
197
|
+
lines.push("guardrails and edit permits");
|
|
198
|
+
lines.push(` guardrail events passed ${summary.guardrails.passed}`);
|
|
199
|
+
lines.push(` guardrail events deferred ${summary.guardrails.deferred}`);
|
|
200
|
+
lines.push(` guardrail events blocked ${summary.guardrails.blocked}`);
|
|
201
|
+
lines.push(` recorded mutation events ${summary.mutations}`);
|
|
202
|
+
|
|
203
|
+
return lines.join("\n");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
module.exports = { computeStats, renderStats, loadRuns };
|
package/lib/errors.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Stable ScrumRun error catalog.
|
|
4
|
+
//
|
|
5
|
+
// Codes are permanent once assigned. Never reuse a retired code. When
|
|
6
|
+
// removing an error site, keep its entry with { retired: true } instead
|
|
7
|
+
// of deleting it so external logs stay resolvable.
|
|
8
|
+
//
|
|
9
|
+
// Ranges (soft, for readability):
|
|
10
|
+
// SR-E-001..049 request/intake and approval
|
|
11
|
+
// SR-E-050..099 approvals, tokens, and secrets
|
|
12
|
+
// SR-E-100..149 runs, task/run transitions, ledger
|
|
13
|
+
// SR-E-150..199 guardrails, policy, obligations
|
|
14
|
+
// SR-E-200..249 edit permits (Mutation Gateway)
|
|
15
|
+
// SR-E-250..299 transactions and recovery
|
|
16
|
+
// SR-E-300..349 memory (facts, decisions, insights, dossiers, vault)
|
|
17
|
+
// SR-E-350..399 semantic index, code intelligence
|
|
18
|
+
// SR-E-400..449 migration and update
|
|
19
|
+
// SR-E-450..499 conformance and doctor
|
|
20
|
+
// SR-E-500..549 configuration and installation
|
|
21
|
+
|
|
22
|
+
const CATALOG = Object.freeze({
|
|
23
|
+
// Request / intake
|
|
24
|
+
"SR-E-001": { summary: "Intake request is missing.", remediation: "Provide a natural-language request or use --request \"...\"." },
|
|
25
|
+
"SR-E-002": { summary: "Intake payload contains secret-like content.", remediation: "Remove the secret from the request or reference a vault entry by path." },
|
|
26
|
+
"SR-E-003": { summary: "Intake classification could not be produced.", remediation: "Rephrase the request in one sentence describing the observable problem." },
|
|
27
|
+
|
|
28
|
+
// Approvals
|
|
29
|
+
"SR-E-050": { summary: "Approval token is malformed.", remediation: "Re-run the intake and copy the token exactly as printed after 'Approval:'." },
|
|
30
|
+
"SR-E-051": { summary: "Approval token signature does not match this repository.", remediation: "Tokens are bound to the project fingerprint. Re-run intake from the repo the change targets." },
|
|
31
|
+
"SR-E-052": { summary: "Approval token expired.", remediation: "Re-run intake to obtain a fresh token before approving." },
|
|
32
|
+
|
|
33
|
+
// Runs and ledger
|
|
34
|
+
"SR-E-100": { summary: "Run not found.", remediation: "Check the RUN-NNN id with `sc plan run --list`." },
|
|
35
|
+
"SR-E-101": { summary: "Run ledger contains an invalid event.", remediation: "Inspect the reported event id; use `sc plan run --render` for a human view or restore from git history." },
|
|
36
|
+
"SR-E-102": { summary: "Run transition rejected: missing validation or learning evidence.", remediation: "Complete `--validate` and `--learn` with typed evidence before `--complete`." },
|
|
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
|
+
|
|
39
|
+
// Guardrails
|
|
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 `sc plan run --satisfy-guardrail` before completing the Run." },
|
|
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
|
+
|
|
44
|
+
// Edit permits (Mutation Gateway)
|
|
45
|
+
"SR-E-200": { summary: "No edit permit for this path.", remediation: "Request one with `sc plan run --authorize-mutation RUN-NNN --path <path>` before editing canonical or source files." },
|
|
46
|
+
"SR-E-201": { summary: "Edit permit expired.", remediation: "Permits last 15 minutes. Authorize a new one and record the change immediately." },
|
|
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
|
+
"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
|
+
|
|
50
|
+
// Transactions and recovery
|
|
51
|
+
"SR-E-250": { summary: "Pending kernel transaction cannot be recovered automatically.", remediation: "Run `sc config doctor --recover --dry-run` to preview; if it shows 'would overwrite owner changes', reconcile the file manually before applying." },
|
|
52
|
+
"SR-E-251": { summary: "Journal fails integrity check.", remediation: "Inspect .scrumrun/.transactions/pending. Do not delete; contact support or restore from backup." },
|
|
53
|
+
|
|
54
|
+
// Memory
|
|
55
|
+
"SR-E-300": { summary: "Memory candidate rejected: missing resolvable evidence.", remediation: "Attach at least one --evidence path or `sc knowledge <subject> --propose` before `--confirm`." },
|
|
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
|
+
|
|
58
|
+
// Semantic index / code intel
|
|
59
|
+
"SR-E-350": { summary: "Semantic index is stale.", remediation: "Rebuild with `sc 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 `sc knowledge` query to rebuild against the current runtime." },
|
|
61
|
+
|
|
62
|
+
// Migration
|
|
63
|
+
"SR-E-400": { summary: "Migration preflight failed.", remediation: "Run `npx scrumrun@latest update` (dry) to see blockers, resolve them, then apply with `--migrate`." },
|
|
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
|
+
|
|
66
|
+
// Conformance / doctor
|
|
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 `npx scrumrun@latest update` for the specific client. `doctor --strict` shows which files diverge." },
|
|
69
|
+
|
|
70
|
+
// Configuration / install
|
|
71
|
+
"SR-E-500": { summary: "ScrumRun project not initialized.", remediation: "Run `npx scrumrun@latest init` in the repository root." },
|
|
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
|
+
});
|
|
74
|
+
|
|
75
|
+
class ScrumRunError extends Error {
|
|
76
|
+
constructor(code, message, options = {}) {
|
|
77
|
+
const entry = CATALOG[code];
|
|
78
|
+
if (!entry) throw new Error(`Unknown ScrumRun error code: ${code}`);
|
|
79
|
+
const composed = message || entry.summary;
|
|
80
|
+
super(`${code} ${composed}`);
|
|
81
|
+
this.name = "ScrumRunError";
|
|
82
|
+
this.code = code;
|
|
83
|
+
this.summary = entry.summary;
|
|
84
|
+
this.remediation = entry.remediation;
|
|
85
|
+
if (options.cause) this.cause = options.cause;
|
|
86
|
+
if (options.details) this.details = options.details;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function describe(code) {
|
|
91
|
+
return CATALOG[code] || null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function codes() {
|
|
95
|
+
return Object.keys(CATALOG).sort();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { ScrumRunError, describe, codes, CATALOG };
|
package/lib/memory/index.js
CHANGED
|
@@ -499,7 +499,7 @@ function indexStatus(projectRoot) {
|
|
|
499
499
|
return { exists: true, stale: true, file, stored, check: "schema", reason: `semantic index schema ${INDEX_SCHEMA_VERSION} rebuild required` };
|
|
500
500
|
}
|
|
501
501
|
if (searchBackend === "fts5" && !runtimeSupportsFts5(database)) {
|
|
502
|
-
return { exists: true, stale: true, file, stored, searchBackend, check: "
|
|
502
|
+
return { exists: true, stale: true, file, stored, searchBackend, check: "backend", backendMismatch: true, reason: "declared search backend fts5 is not available in this Node.js runtime; disposable rebuild required" };
|
|
503
503
|
}
|
|
504
504
|
const watch = sourceWatchSnapshot(projectRoot);
|
|
505
505
|
if (metadata.get("source_watch_fingerprint") === watch.fingerprint) {
|
package/lib/v2/conformance.js
CHANGED
|
@@ -35,7 +35,8 @@ const INVARIANTS = Object.freeze([
|
|
|
35
35
|
{ id: "I-18", summary: "unsafe and partial writes fail safely", tests: ["canonical writes reject traversal and symlink paths", "repository refuses conflicting overwrite", "interrupted kernel transaction is recovered"] },
|
|
36
36
|
{ id: "I-19", summary: "code intelligence is derived and fingerprinted", tests: ["language adapters are replaceable", "moves remap by fingerprint"] },
|
|
37
37
|
{ id: "I-20", summary: "learning candidates never block execution", tests: ["post-validation extraction creates candidate insights"] },
|
|
38
|
-
{ id: "I-21", summary: "material mutations are scoped, policy-bound, and fail closed", tests: ["Mutation Gateway rejects bypass and out-of-scope writes", "Run completion rejects unresolved Guardrail obligations"] }
|
|
38
|
+
{ id: "I-21", summary: "material mutations are scoped, policy-bound, and fail closed", tests: ["Mutation Gateway rejects bypass and out-of-scope writes", "Run completion rejects unresolved Guardrail obligations"] },
|
|
39
|
+
{ id: "I-22", summary: "declared search backend matches observed runtime capabilities", tests: ["conformance detects a semantic index that declares fts5 when the runtime does not provide it"] }
|
|
39
40
|
]);
|
|
40
41
|
|
|
41
42
|
function finding(severity, code, message, file = null) {
|
|
@@ -213,7 +214,11 @@ function auditProject(projectRoot) {
|
|
|
213
214
|
}
|
|
214
215
|
try {
|
|
215
216
|
const semantic = indexStatus(projectRoot);
|
|
216
|
-
if (semantic.exists && semantic.
|
|
217
|
+
if (semantic.exists && semantic.backendMismatch) {
|
|
218
|
+
findings.push(finding("high", "SEARCH_BACKEND_MISMATCH", `semantic index declares search_backend=${semantic.searchBackend} but the current Node.js runtime does not provide it; delete .scrumrun/.cache/semantic-index.sqlite to rebuild against the observed capabilities.`));
|
|
219
|
+
} else if (semantic.exists && semantic.stale) {
|
|
220
|
+
findings.push(finding("warning", "INDEX_STALE", "semantic-index.sqlite is stale and will be rebuilt on query."));
|
|
221
|
+
}
|
|
217
222
|
const map = mapStatus(projectRoot, { semanticStatus: semantic });
|
|
218
223
|
if (semantic.exists && map.stale) findings.push(finding("warning", "MAP_STALE", `map.md is stale: ${map.reason || map.error || "unknown reason"}.`));
|
|
219
224
|
} catch (error) {
|
package/lib/v2/transaction.js
CHANGED
|
@@ -236,6 +236,64 @@ function runKernelTransaction(scrumDir, name, changes, options = {}) {
|
|
|
236
236
|
});
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
function previewPendingRecovery(scrumDir) {
|
|
240
|
+
const preview = { plans: [], errors: [] };
|
|
241
|
+
let entries;
|
|
242
|
+
try {
|
|
243
|
+
entries = pendingTransactions(scrumDir);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
preview.errors.push(error.message);
|
|
246
|
+
return preview;
|
|
247
|
+
}
|
|
248
|
+
for (const { file, transaction } of entries) {
|
|
249
|
+
const plan = { id: transaction.id, name: transaction.name || null, journal: path.basename(file), status: transaction.status, action: null, files: [], warnings: [] };
|
|
250
|
+
const journalErrors = validateJournal(transaction);
|
|
251
|
+
if (journalErrors.length) {
|
|
252
|
+
plan.action = "blocked";
|
|
253
|
+
plan.warnings.push(`journal validation failed: ${journalErrors.join("; ")}`);
|
|
254
|
+
preview.plans.push(plan);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
let unsafePath = null;
|
|
258
|
+
for (const change of transaction.changes) {
|
|
259
|
+
const target = relativeTarget(scrumDir, path.join(scrumDir, change.relative));
|
|
260
|
+
if (target.relative !== change.relative) {
|
|
261
|
+
unsafePath = change.relative;
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (unsafePath) {
|
|
266
|
+
plan.action = "blocked";
|
|
267
|
+
plan.warnings.push(`unsafe transaction target: ${unsafePath}`);
|
|
268
|
+
preview.plans.push(plan);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const current = transaction.changes.map((change) => ({ change, value: fileSnapshot(scrumDir, change.relative) }));
|
|
272
|
+
const unexpected = current.filter(({ change, value }) => !sameSnapshot(value, change.before) && !sameSnapshot(value, change.after));
|
|
273
|
+
if (unexpected.length) {
|
|
274
|
+
plan.action = "blocked";
|
|
275
|
+
plan.warnings.push(`would overwrite owner changes at: ${unexpected.map(({ change }) => change.relative).join(", ")}`);
|
|
276
|
+
preview.plans.push(plan);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (transaction.status === "prepared") {
|
|
280
|
+
plan.action = "rollback";
|
|
281
|
+
plan.files = transaction.changes.map((change) => ({ relative: change.relative, from: "current", to: "pre-transaction" }));
|
|
282
|
+
} else {
|
|
283
|
+
const incomplete = current.filter(({ change, value }) => !sameSnapshot(value, change.after));
|
|
284
|
+
if (incomplete.length) {
|
|
285
|
+
plan.action = "blocked";
|
|
286
|
+
plan.warnings.push(`committed transaction is incomplete: ${incomplete.map(({ change }) => change.relative).join(", ")}`);
|
|
287
|
+
} else {
|
|
288
|
+
plan.action = "verify-commit";
|
|
289
|
+
plan.files = transaction.changes.map((change) => ({ relative: change.relative, from: "current", to: "verified-commit" }));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
preview.plans.push(plan);
|
|
293
|
+
}
|
|
294
|
+
return preview;
|
|
295
|
+
}
|
|
296
|
+
|
|
239
297
|
function pendingTransactionStatus(scrumDir) {
|
|
240
298
|
try {
|
|
241
299
|
return { pending: pendingTransactions(scrumDir).map(({ transaction }) => ({ id: transaction.id, name: transaction.name, status: transaction.status })) };
|
|
@@ -247,6 +305,7 @@ function pendingTransactionStatus(scrumDir) {
|
|
|
247
305
|
module.exports = {
|
|
248
306
|
TRANSACTION_SCHEMA,
|
|
249
307
|
pendingTransactionStatus,
|
|
308
|
+
previewPendingRecovery,
|
|
250
309
|
recoverPendingTransactions,
|
|
251
310
|
recoverPendingTransactionsUnlocked,
|
|
252
311
|
runKernelTransaction,
|