scrumrun 2.3.0 → 2.4.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 +14 -0
- package/CORE.md +2 -0
- package/README.md +1 -1
- package/SPEC.md +2 -1
- package/lib/v2/conformance.js +31 -1
- package/lib/v2/migration.js +5 -5
- package/lib/v2/paths.js +72 -0
- package/package.json +1 -1
- package/templates/project/.scrumrun/method.json +22 -0
- package/templates/shared/skills/scrumrun/SKILL.md +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ All notable changes follow Semantic Versioning.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 2.4.0 - 2026-07-23
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `method.json` now carries a canonical `paths` block plus `paths_schema: 1`. The block declares the exact relative location of every artifact family (`guardrails`, `config`, `project`, `core`, `state_view`, `map_view`, `tasks`, `sprints`, `features`, `runs`, `reviews`, `memory.{knowledge,decisions,insights,dossiers}`, `vault_local`, `cache`) so ScrumRun-aware agents navigate by index instead of grep. Powered by a new `lib/v2/paths` module used by init, migration, and conformance so drift is impossible.
|
|
12
|
+
- Invariant **I-23** — *method.json declares canonical paths so agents navigate by index, not by search*. `doctor --strict` flags `METHOD_PATHS_MISSING`, `METHOD_PATHS_DRIFT`, and `METHOD_PATHS_SCHEMA` findings; a `high` severity blocks conformance until `update --migrate` regenerates the block.
|
|
13
|
+
- CORE and the shared SKILL now open with a hard instruction: read `.scrumrun/method.json` **before** any grep, and never search for legacy paths (`goals/`, `backlog.md`, `sprint.md`, `history.md`). Directory listing is a fallback, never a first step.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Migration output writes `method.json` through the shared `renderMethodJson` helper, so v1→v2 migrations backfill the `paths` block automatically.
|
|
18
|
+
- SPEC.md conformance range extended to `I-01 through I-23`.
|
|
19
|
+
- Test suite grew from 170 to 177 passing.
|
|
20
|
+
|
|
7
21
|
## 2.3.0 - 2026-07-23
|
|
8
22
|
|
|
9
23
|
### Added
|
package/CORE.md
CHANGED
|
@@ -82,6 +82,8 @@ AGENTS.md
|
|
|
82
82
|
contexts/
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
**Before querying project state, read `.scrumrun/method.json`.** Its `paths` block is the authoritative index of every canonical location in this project. Navigate by that index; if a path is not declared there, it is not canonical truth. Directory listing and grep are fallbacks — never the first step. A ScrumRun-aware agent must never search for `goals/`, `backlog.md`, `sprint.md`, or any legacy layout: those are absent by design once migration completes and are surfaced only through `.scrumrun/.migration-backup/`.
|
|
86
|
+
|
|
85
87
|
Canonical truth is Markdown. SQLite/cache data stores only rebuildable indexes, symbol projections, relations, and bounded context packages. Deleting `.cache/` must never delete authored truth.
|
|
86
88
|
|
|
87
89
|
`state.md` and the semantic index use two-tier freshness checks. Matching path/stat watch fingerprints avoid rereading unchanged sources; any metadata drift falls back to complete content hashing. A cache schema mismatch rebuilds the disposable index once. Watch metadata is only an optimization and never authority.
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
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.4.0` · **Method target:** `2.0.0` · **Runtime:** Node.js `>=22.13.0` · **License:** MIT
|
|
8
8
|
|
|
9
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).
|
|
10
10
|
|
package/SPEC.md
CHANGED
|
@@ -309,6 +309,7 @@ Legacy sprint entries become Tasks. History entries become Runs only with an evi
|
|
|
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
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.
|
|
312
|
+
- **I-23** `method.json` declares the canonical path index for every artifact family so agents navigate by declaration, not by search. Missing or drifted paths fail conformance; grep and directory scans are fallbacks, never the first step.
|
|
312
313
|
|
|
313
314
|
## 11. Command grammar
|
|
314
315
|
|
|
@@ -326,7 +327,7 @@ Unknown syntax fails deterministically and never guesses a mutation.
|
|
|
326
327
|
|
|
327
328
|
An implementation may claim ScrumRun method 2.0.0 only when it:
|
|
328
329
|
|
|
329
|
-
1. passes positive and negative tests for I-01 through I-
|
|
330
|
+
1. passes positive and negative tests for I-01 through I-23;
|
|
330
331
|
2. enforces every exposed state machine and schema;
|
|
331
332
|
3. proves read-only intake and dry-run migration through full-tree fingerprints;
|
|
332
333
|
4. proves migration failure recovery, rollback safety, and vault exclusion;
|
package/lib/v2/conformance.js
CHANGED
|
@@ -13,6 +13,7 @@ const { containsSecret, containsSecretWithAllowlist, loadSecretAllowlist } = req
|
|
|
13
13
|
const { pendingTransactionStatus } = require("./transaction");
|
|
14
14
|
const { configWeakeningAttempts, validateGuardrailDocument } = require("../runtime/policy-engine");
|
|
15
15
|
const { auditActiveWorkspace } = require("../runtime/mutation-gateway");
|
|
16
|
+
const { canonicalPaths, PATHS_SCHEMA_VERSION } = require("./paths");
|
|
16
17
|
|
|
17
18
|
const INVARIANTS = Object.freeze([
|
|
18
19
|
{ id: "I-01", summary: "pre-approval work is read-only", tests: ["intake builds bounded context without writing"] },
|
|
@@ -36,13 +37,32 @@ const INVARIANTS = Object.freeze([
|
|
|
36
37
|
{ id: "I-19", summary: "code intelligence is derived and fingerprinted", tests: ["language adapters are replaceable", "moves remap by fingerprint"] },
|
|
37
38
|
{ id: "I-20", summary: "learning candidates never block execution", tests: ["post-validation extraction creates candidate insights"] },
|
|
38
39
|
{ 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"] }
|
|
40
|
+
{ 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"] },
|
|
41
|
+
{ id: "I-23", summary: "method.json declares canonical paths so agents navigate by index, not by search", tests: ["method.json path index is present, well-formed, and matches the canonical layout"] }
|
|
40
42
|
]);
|
|
41
43
|
|
|
42
44
|
function finding(severity, code, message, file = null) {
|
|
43
45
|
return { severity, code, message, ...(file ? { file } : {}) };
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
function diffPathIndex(expected, actual, prefix = "") {
|
|
49
|
+
const drift = [];
|
|
50
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
51
|
+
const label = prefix ? `${prefix}.${key}` : key;
|
|
52
|
+
const actualValue = actual && typeof actual === "object" ? actual[key] : undefined;
|
|
53
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
54
|
+
if (!actualValue || typeof actualValue !== "object" || Array.isArray(actualValue)) {
|
|
55
|
+
drift.push({ label, expected: "<object>", actual: actualValue });
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
drift.push(...diffPathIndex(value, actualValue, label));
|
|
59
|
+
} else if (actualValue !== value) {
|
|
60
|
+
drift.push({ label, expected: value, actual: actualValue });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return drift;
|
|
64
|
+
}
|
|
65
|
+
|
|
46
66
|
function auditProject(projectRoot) {
|
|
47
67
|
const scrumDir = path.join(projectRoot, ".scrumrun");
|
|
48
68
|
const findings = [];
|
|
@@ -56,6 +76,16 @@ function auditProject(projectRoot) {
|
|
|
56
76
|
if (!fs.existsSync(marker) || !fs.lstatSync(marker).isFile()) throw new Error("marker is missing, not a regular file, or is a symbolic link");
|
|
57
77
|
methodMarker = JSON.parse(fs.readFileSync(marker, "utf8"));
|
|
58
78
|
if (methodMarker.method !== METHOD_VERSION) findings.push(finding("critical", "METHOD_VERSION", `method.json must declare ${METHOD_VERSION}.`, marker));
|
|
79
|
+
const expectedPaths = canonicalPaths();
|
|
80
|
+
if (!methodMarker.paths || typeof methodMarker.paths !== "object" || Array.isArray(methodMarker.paths)) {
|
|
81
|
+
findings.push(finding("high", "METHOD_PATHS_MISSING", `method.json must declare a canonical "paths" block so agents navigate by declaration, not by search. Run \`npx scrumrun@latest update --migrate\` to backfill.`, marker));
|
|
82
|
+
} else {
|
|
83
|
+
const drift = diffPathIndex(expectedPaths, methodMarker.paths);
|
|
84
|
+
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));
|
|
85
|
+
if (methodMarker.paths_schema !== PATHS_SCHEMA_VERSION) {
|
|
86
|
+
findings.push(finding("warning", "METHOD_PATHS_SCHEMA", `method.json paths_schema is ${methodMarker.paths_schema || "missing"}; expected ${PATHS_SCHEMA_VERSION}.`, marker));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
59
89
|
} catch (error) {
|
|
60
90
|
findings.push(finding("critical", "METHOD_MARKER", `method.json is missing or malformed: ${error.message}`, marker));
|
|
61
91
|
}
|
package/lib/v2/migration.js
CHANGED
|
@@ -18,6 +18,7 @@ const {
|
|
|
18
18
|
} = require("./artifacts");
|
|
19
19
|
const { inferEnforcement, normalizeGuardrailDocument } = require("../runtime/policy-engine");
|
|
20
20
|
const { RUN_LEDGER_VERSION } = require("./schema");
|
|
21
|
+
const { renderMethodJson } = require("./paths");
|
|
21
22
|
const { migrateLegacyRun } = require("../runtime/run-ledger");
|
|
22
23
|
|
|
23
24
|
const MIGRATION_NAME = "v1-to-v2";
|
|
@@ -927,13 +928,12 @@ function migrationPlan(projectRoot) {
|
|
|
927
928
|
}
|
|
928
929
|
|
|
929
930
|
const sourceLayout = existingIds.size ? "hybrid-v1-v2" : "1.x";
|
|
930
|
-
generated.set("method.json",
|
|
931
|
-
|
|
932
|
-
layout: "v2",
|
|
931
|
+
generated.set("method.json", renderMethodJson({
|
|
932
|
+
methodVersion: METHOD_VERSION,
|
|
933
933
|
schemas: { run_ledger: RUN_LEDGER_VERSION, guardrails: 1, run_obligations: 1, mutation_gateway: 1 },
|
|
934
|
-
|
|
934
|
+
migratedFrom: sourceLayout,
|
|
935
935
|
migration: MIGRATION_NAME
|
|
936
|
-
}
|
|
936
|
+
}));
|
|
937
937
|
|
|
938
938
|
const counts = {};
|
|
939
939
|
for (const artifact of artifacts) counts[artifact.kind] = (counts[artifact.kind] || 0) + 1;
|
package/lib/v2/paths.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Single source of truth for the canonical layout inside `.scrumrun/`.
|
|
4
|
+
//
|
|
5
|
+
// This module powers `method.json.paths` — the machine-readable index a
|
|
6
|
+
// ScrumRun-aware agent must consult BEFORE searching. Any change here
|
|
7
|
+
// must land in the template, the migration generator, and the doctor
|
|
8
|
+
// invariant together so no drift is possible.
|
|
9
|
+
|
|
10
|
+
const CANONICAL_PATHS = Object.freeze({
|
|
11
|
+
guardrails: "guardrails.md",
|
|
12
|
+
config: "config.md",
|
|
13
|
+
project: "project.md",
|
|
14
|
+
core: "core.md",
|
|
15
|
+
state_view: "state.md",
|
|
16
|
+
map_view: "map.md",
|
|
17
|
+
tasks: "tasks/",
|
|
18
|
+
sprints: "sprints/",
|
|
19
|
+
features: "features/",
|
|
20
|
+
runs: "runs/",
|
|
21
|
+
reviews: "reviews/",
|
|
22
|
+
memory: Object.freeze({
|
|
23
|
+
knowledge: "memory/knowledge/",
|
|
24
|
+
decisions: "memory/decisions/",
|
|
25
|
+
insights: "memory/insights/",
|
|
26
|
+
dossiers: "memory/dossiers/"
|
|
27
|
+
}),
|
|
28
|
+
vault_local: "vault.local.md",
|
|
29
|
+
cache: ".cache/"
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const PATHS_SCHEMA_VERSION = 1;
|
|
33
|
+
|
|
34
|
+
function clone(value) {
|
|
35
|
+
if (value === null || typeof value !== "object") return value;
|
|
36
|
+
if (Array.isArray(value)) return value.map(clone);
|
|
37
|
+
const out = {};
|
|
38
|
+
for (const [key, entry] of Object.entries(value)) out[key] = clone(entry);
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function canonicalPaths() {
|
|
43
|
+
return clone(CANONICAL_PATHS);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function flattenPaths(paths = CANONICAL_PATHS, prefix = "") {
|
|
47
|
+
const out = [];
|
|
48
|
+
for (const [key, value] of Object.entries(paths)) {
|
|
49
|
+
const label = prefix ? `${prefix}.${key}` : key;
|
|
50
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
51
|
+
out.push(...flattenPaths(value, label));
|
|
52
|
+
} else if (typeof value === "string") {
|
|
53
|
+
out.push({ label, relative: value });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function renderMethodJson({ methodVersion, layout = "v2", schemas = {}, migratedFrom, migration } = {}) {
|
|
60
|
+
const payload = {
|
|
61
|
+
method: methodVersion,
|
|
62
|
+
layout,
|
|
63
|
+
paths_schema: PATHS_SCHEMA_VERSION,
|
|
64
|
+
paths: canonicalPaths(),
|
|
65
|
+
schemas
|
|
66
|
+
};
|
|
67
|
+
if (migratedFrom) payload.migrated_from = migratedFrom;
|
|
68
|
+
if (migration) payload.migration = migration;
|
|
69
|
+
return `${JSON.stringify(payload, null, 2)}\n`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { CANONICAL_PATHS, PATHS_SCHEMA_VERSION, canonicalPaths, flattenPaths, renderMethodJson };
|
package/package.json
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"method": "2.0.0",
|
|
3
3
|
"layout": "v2",
|
|
4
|
+
"paths_schema": 1,
|
|
5
|
+
"paths": {
|
|
6
|
+
"guardrails": "guardrails.md",
|
|
7
|
+
"config": "config.md",
|
|
8
|
+
"project": "project.md",
|
|
9
|
+
"core": "core.md",
|
|
10
|
+
"state_view": "state.md",
|
|
11
|
+
"map_view": "map.md",
|
|
12
|
+
"tasks": "tasks/",
|
|
13
|
+
"sprints": "sprints/",
|
|
14
|
+
"features": "features/",
|
|
15
|
+
"runs": "runs/",
|
|
16
|
+
"reviews": "reviews/",
|
|
17
|
+
"memory": {
|
|
18
|
+
"knowledge": "memory/knowledge/",
|
|
19
|
+
"decisions": "memory/decisions/",
|
|
20
|
+
"insights": "memory/insights/",
|
|
21
|
+
"dossiers": "memory/dossiers/"
|
|
22
|
+
},
|
|
23
|
+
"vault_local": "vault.local.md",
|
|
24
|
+
"cache": ".cache/"
|
|
25
|
+
},
|
|
4
26
|
"schemas": {
|
|
5
27
|
"run_ledger": 1,
|
|
6
28
|
"guardrails": 1,
|
|
@@ -29,11 +29,12 @@ Apply instructions in this order:
|
|
|
29
29
|
|
|
30
30
|
Normal hot path:
|
|
31
31
|
|
|
32
|
-
1. read `
|
|
33
|
-
2. read
|
|
34
|
-
3. read `.scrumrun/
|
|
35
|
-
4.
|
|
36
|
-
5.
|
|
32
|
+
1. read `.scrumrun/method.json` — its `paths` block is the authoritative index of every canonical location; navigate by that index and never grep for legacy paths (`goals/`, `backlog.md`, `sprint.md`, `history.md`);
|
|
33
|
+
2. read `AGENTS.md`;
|
|
34
|
+
3. read `.scrumrun/guardrails.md`;
|
|
35
|
+
4. read `.scrumrun/state.md`;
|
|
36
|
+
5. follow the ids/pointers to only the relevant canonical artifacts;
|
|
37
|
+
6. load `.scrumrun/core.md` when the method contract or an exceptional transition is needed.
|
|
37
38
|
|
|
38
39
|
Lean mode is a read policy, not an incomplete store. Generated files and `.scrumrun/.cache/` are never authoritative.
|
|
39
40
|
|