specrails-core 6.6.3 → 6.6.5
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/dist/agent-runtime/graph/archive-preflight.d.ts +3 -18
- package/dist/agent-runtime/graph/archive-preflight.js +6 -51
- package/dist/agent-runtime/graph/artifacts.js +1 -1
- package/dist/agent-runtime/openspec.d.ts +1 -1
- package/dist/agent-runtime/openspec.js +11 -5
- package/dist/installer/commands/init.js +1 -1
- package/dist/installer/phases/prereqs.d.ts +1 -1
- package/dist/installer/phases/prereqs.js +2 -2
- package/dist/installer/phases/scaffold.js +3 -6
- package/package.json +2 -2
- package/pinned-versions.json +1 -1
|
@@ -1,29 +1,14 @@
|
|
|
1
|
-
export interface ArchiveRequirementFinding {
|
|
2
|
-
path: string;
|
|
3
|
-
line: number;
|
|
4
|
-
requirement: string;
|
|
5
|
-
}
|
|
6
1
|
export type ArchivePreflight = {
|
|
7
2
|
valid: true;
|
|
8
3
|
} | {
|
|
9
4
|
valid: false;
|
|
10
5
|
reason: string;
|
|
11
|
-
findings: ArchiveRequirementFinding[];
|
|
12
6
|
};
|
|
13
|
-
/**
|
|
14
|
-
* Requirements OpenSpec rejects when it rebuilds a main spec on archive: its spec parser reads only
|
|
15
|
-
* the first non-empty body line before any heading and needs SHALL or MUST there. Delta validation
|
|
16
|
-
* skips `**Key**:` metadata lines, so a delta (or a pre-existing main-spec requirement) can pass
|
|
17
|
-
* `openspec validate` and still abort the archive.
|
|
18
|
-
*/
|
|
19
|
-
export declare function requirementsWithoutKeyword(markdown: string): Array<{
|
|
20
|
-
line: number;
|
|
21
|
-
requirement: string;
|
|
22
|
-
}>;
|
|
23
7
|
/**
|
|
24
8
|
* Dry-runs `openspec archive` on a disposable copy of `openspec/`, so a change that validates but
|
|
25
|
-
* cannot be archived is caught while its author can still edit the plan.
|
|
26
|
-
*
|
|
9
|
+
* cannot be archived is caught while its author can still edit the plan. Delta validation does not
|
|
10
|
+
* check deltas against the main specs; archive rejects a MODIFIED or RENAMED requirement the main
|
|
11
|
+
* spec lacks and an ADDED one it already has. Nothing in the artifact root is written.
|
|
27
12
|
*/
|
|
28
13
|
export declare function previewOpenSpecArchive(artifactRoot: string, change: string, signal?: AbortSignal): Promise<ArchivePreflight>;
|
|
29
14
|
/** Plain-text guidance for the planning role's history. */
|
|
@@ -1,36 +1,8 @@
|
|
|
1
|
-
import { cpSync, existsSync,
|
|
1
|
+
import { cpSync, existsSync, mkdtempSync, readdirSync, rmSync } from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { artifactPath, OpenSpecCommandError, resolveOpenSpecCli, runOpenSpec } from '../openspec.js';
|
|
5
5
|
const ANSI = new RegExp(String.fromCharCode(27) + '\\[[0-9;]*m', 'g');
|
|
6
|
-
/**
|
|
7
|
-
* Requirements OpenSpec rejects when it rebuilds a main spec on archive: its spec parser reads only
|
|
8
|
-
* the first non-empty body line before any heading and needs SHALL or MUST there. Delta validation
|
|
9
|
-
* skips `**Key**:` metadata lines, so a delta (or a pre-existing main-spec requirement) can pass
|
|
10
|
-
* `openspec validate` and still abort the archive.
|
|
11
|
-
*/
|
|
12
|
-
export function requirementsWithoutKeyword(markdown) {
|
|
13
|
-
const lines = markdown.split('\n'), findings = [];
|
|
14
|
-
for (let index = 0; index < lines.length; index++) {
|
|
15
|
-
const header = lines[index].match(/^###\s+Requirement:\s*(.*)$/);
|
|
16
|
-
if (!header)
|
|
17
|
-
continue;
|
|
18
|
-
let body;
|
|
19
|
-
for (let next = index + 1; next < lines.length; next++) {
|
|
20
|
-
const text = lines[next].trim();
|
|
21
|
-
if (text.startsWith('#'))
|
|
22
|
-
break;
|
|
23
|
-
if (text) {
|
|
24
|
-
body = next;
|
|
25
|
-
break;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
const text = body === undefined ? header[1] : lines[body];
|
|
29
|
-
if (!text.includes('SHALL') && !text.includes('MUST'))
|
|
30
|
-
findings.push({ line: (body ?? index) + 1, requirement: header[1].trim() });
|
|
31
|
-
}
|
|
32
|
-
return findings;
|
|
33
|
-
}
|
|
34
6
|
function assertNoSymlinks(root) {
|
|
35
7
|
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
36
8
|
if (entry.isSymbolicLink())
|
|
@@ -39,26 +11,11 @@ function assertNoSymlinks(root) {
|
|
|
39
11
|
assertNoSymlinks(path.join(root, entry.name));
|
|
40
12
|
}
|
|
41
13
|
}
|
|
42
|
-
function capabilityFindings(artifactRoot, change) {
|
|
43
|
-
const deltas = path.join(artifactRoot, 'openspec/changes', change, 'specs');
|
|
44
|
-
if (!existsSync(deltas))
|
|
45
|
-
return [];
|
|
46
|
-
const findings = [];
|
|
47
|
-
for (const capability of readdirSync(deltas).sort()) {
|
|
48
|
-
for (const relative of [`openspec/changes/${change}/specs/${capability}/spec.md`, `openspec/specs/${capability}/spec.md`]) {
|
|
49
|
-
const file = path.join(artifactRoot, relative);
|
|
50
|
-
if (!existsSync(file) || !lstatSync(file).isFile())
|
|
51
|
-
continue;
|
|
52
|
-
for (const finding of requirementsWithoutKeyword(readFileSync(file, 'utf8')))
|
|
53
|
-
findings.push({ path: relative, ...finding });
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return findings;
|
|
57
|
-
}
|
|
58
14
|
/**
|
|
59
15
|
* Dry-runs `openspec archive` on a disposable copy of `openspec/`, so a change that validates but
|
|
60
|
-
* cannot be archived is caught while its author can still edit the plan.
|
|
61
|
-
*
|
|
16
|
+
* cannot be archived is caught while its author can still edit the plan. Delta validation does not
|
|
17
|
+
* check deltas against the main specs; archive rejects a MODIFIED or RENAMED requirement the main
|
|
18
|
+
* spec lacks and an ADDED one it already has. Nothing in the artifact root is written.
|
|
62
19
|
*/
|
|
63
20
|
export async function previewOpenSpecArchive(artifactRoot, change, signal) {
|
|
64
21
|
const source = artifactPath(artifactRoot, 'openspec');
|
|
@@ -80,7 +37,7 @@ export async function previewOpenSpecArchive(artifactRoot, change, signal) {
|
|
|
80
37
|
if (archived && !existsSync(path.join(staging, 'openspec/changes', change)))
|
|
81
38
|
return { valid: true };
|
|
82
39
|
const reason = output.replace(ANSI, '').split('\n').map(line => line.trimEnd()).filter(Boolean).join('\n').slice(-2000);
|
|
83
|
-
return { valid: false, reason
|
|
40
|
+
return { valid: false, reason };
|
|
84
41
|
}
|
|
85
42
|
finally {
|
|
86
43
|
rmSync(staging, { recursive: true, force: true });
|
|
@@ -88,11 +45,9 @@ export async function previewOpenSpecArchive(artifactRoot, change, signal) {
|
|
|
88
45
|
}
|
|
89
46
|
/** Plain-text guidance for the planning role's history. */
|
|
90
47
|
export function describeArchivePreflight(change, preflight) {
|
|
91
|
-
const located = preflight.findings.map(finding => `- ${finding.path}:${finding.line} "${finding.requirement}"`).join('\n');
|
|
92
48
|
return [
|
|
93
49
|
`OpenSpec validates change ${change}, but archiving it would fail. OpenSpec's archive output:`,
|
|
94
50
|
preflight.reason,
|
|
95
|
-
|
|
96
|
-
'Put the SHALL/MUST statement on the line right after "### Requirement: …". For a requirement that only exists in openspec/specs, restate it under "## MODIFIED Requirements" in this change\'s delta with the corrected first line.'] : []),
|
|
51
|
+
'Reconcile each delta with openspec/specs: MODIFIED and RENAMED headers must name an existing requirement exactly, and ADDED requirements must not exist yet.',
|
|
97
52
|
].join('\n');
|
|
98
53
|
}
|
|
@@ -195,7 +195,7 @@ async function archiveWithOpenSpec(context, change, active, signal, adapter) {
|
|
|
195
195
|
// created specification. Preserve that provenance during fork replay;
|
|
196
196
|
// authored text and all byte-for-byte publication guards stay intact.
|
|
197
197
|
if (sourceChange && before === null && after !== null)
|
|
198
|
-
after = after.replace(`\n## Purpose\nTBD - created by archiving change ${change}. Update Purpose after archive.\n
|
|
198
|
+
after = after.replace(`\n## Purpose\nTBD - created by archiving change ${change}. Update Purpose after archive.\n`, `\n## Purpose\nTBD - created by archiving change ${sourceChange}. Update Purpose after archive.\n`);
|
|
199
199
|
return { path: 'openspec/specs/' + name, before, after };
|
|
200
200
|
}).filter(item => item.before !== item.after) };
|
|
201
201
|
write(receipt, JSON.stringify(plan));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type AgentRole, type CliProvider, type RoleDescriptor, type AgentAccess, type ArtifactAccess, type OpenSpecSkill } from './executor-types.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
export declare const OPENSPEC_VERSION = "1.
|
|
3
|
+
export declare const OPENSPEC_VERSION = "1.14.3";
|
|
4
4
|
export declare const ROLE_SKILLS: {
|
|
5
5
|
readonly architect: 'openspec-ff-change';
|
|
6
6
|
readonly developer: 'openspec-apply-change';
|
|
@@ -7,8 +7,11 @@ import path from 'node:path';
|
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { BUILTIN_ROLE_POLICY, isBuiltinRole } from './executor-types.js';
|
|
9
9
|
import { z } from 'zod';
|
|
10
|
-
export const OPENSPEC_VERSION = '1.
|
|
10
|
+
export const OPENSPEC_VERSION = '1.14.3';
|
|
11
11
|
export const ROLE_SKILLS = { architect: 'openspec-ff-change', developer: 'openspec-apply-change', reviewer: 'openspec-verify-change' };
|
|
12
|
+
/** Where `openspec init --tools` writes each provider's skills (Codex and Kimi moved in 1.14). */
|
|
13
|
+
const OPENSPEC_SKILL_DIRECTORIES = { claude: '.claude', codex: '.agents', gemini: '.gemini', kimi: '.kimi-code' };
|
|
14
|
+
const DEFAULT_PROJECT_CONFIG = 'schema: spec-driven\n';
|
|
12
15
|
/** Saved built-in contexts retain their original shape; new roles carry explicit policy. */
|
|
13
16
|
export function openSpecPolicy(context) {
|
|
14
17
|
const legacy = isBuiltinRole(context.role) ? BUILTIN_ROLE_POLICY[context.role] : undefined;
|
|
@@ -144,7 +147,7 @@ export function prepareOpenSpec(root, change, directory) {
|
|
|
144
147
|
});
|
|
145
148
|
for (const provider of ['claude', 'codex', 'gemini', 'kimi']) {
|
|
146
149
|
for (const name of Object.values(ROLE_SKILLS)) {
|
|
147
|
-
const content = readFileSync(path.join(target,
|
|
150
|
+
const content = readFileSync(path.join(target, OPENSPEC_SKILL_DIRECTORIES[provider], 'skills', name, 'SKILL.md'), 'utf8');
|
|
148
151
|
const destination = path.join(skillRoot, provider, name);
|
|
149
152
|
mkdirSync(destination, { recursive: true });
|
|
150
153
|
writeFileSync(path.join(destination, 'SKILL.md'), content, { mode: 0o600 });
|
|
@@ -163,8 +166,11 @@ export function prepareOpenSpec(root, change, directory) {
|
|
|
163
166
|
}
|
|
164
167
|
if (existsSync(path.join(root, 'openspec/schemas/spec-driven')))
|
|
165
168
|
throw new Error('Custom spec-driven schema overrides are not supported by this runtime');
|
|
166
|
-
|
|
167
|
-
|
|
169
|
+
// Same precedence as OpenSpec. `new change` scaffolds DEFAULT_PROJECT_CONFIG when no config exists;
|
|
170
|
+
// it is equivalent to none, so the architect's first write does not change the run's identity.
|
|
171
|
+
const config = ['openspec/config.yaml', 'openspec/config.yml'].map(file => artifactPath(root, file)).find(file => existsSync(file));
|
|
172
|
+
const projectConfig = config ? readFileSync(config, 'utf8') : undefined;
|
|
173
|
+
identity.projectConfig = projectConfig === undefined || projectConfig === DEFAULT_PROJECT_CONFIG ? 'absent' : hash(projectConfig);
|
|
168
174
|
return { cli, skillRoot, identity };
|
|
169
175
|
}
|
|
170
176
|
export function roleOpenSpecContext(prepared, root, change, directory, roleOrDescriptor, provider) {
|
|
@@ -349,7 +355,7 @@ export const OPENSPEC_TOOL_DEFINITION = { type: 'function', function: { name: 'o
|
|
|
349
355
|
export function openSpecPrompt(context) {
|
|
350
356
|
if (!isBuiltinRole(context.role))
|
|
351
357
|
return `\n## Official OpenSpec workflow binding\nExecute ${openSpecSkill(context)} for change ${context.change}. Load action=load_skill using openspec_workflow and follow the pinned official skill. Use scoped tools for status, instructions, validation and permitted artifact writes. Workspace access: ${openSpecPolicy(context).access}; artifacts: ${openSpecPolicy(context).artifacts}. Read required artifact instructions before writing. Never archive or coordinate another role.\n`;
|
|
352
|
-
return `\n## Official OpenSpec workflow binding\nExecute ${openSpecSkill(context)} for change ${context.change}. This headless transport loads the official, version-pinned skill through the openspec_workflow tool (MCP server specrails_openspec, tool workflow on CLI providers). This is explicit skill-document adaptation, not a native slash command.\nFirst call action=load_skill and follow the returned complete official skill. Bind its openspec CLI commands to this tool: new change -> action=new; status -> status; instructions <artifact> -> instructions with artifact; validate -> validate. Author each artifact yourself using write_artifact with its change-relative path and content; read the official instructions first. Do not reproduce templates from memory or return the artifacts in final JSON. The tool fixes the change identity and runs OpenSpec ${OPENSPEC_VERSION}.\nRead source/dependencies with your normal read tools. For this frozen scope, change selection is already answered. Bind AskUserQuestion to a low-confidence final result with question (architect), or a reported blocking issue (other roles); LangGraph asks the requester. Bind TodoWrite to concise progress narration and, for developer, persist the current handoff through action=write_progress with progress {summary, completedTasks, nextTasks, checks: [{command, outcome}], blockers}. Save after each completed task or changed blocker, at most 8 KB. This host journal is separate from frozen OpenSpec artifacts; never write runtime files directly. Developer/reviewer receive savedProgress from load_skill and can refresh with read_progress. Reconcile this advisory history with current files; old test outcomes never replace required host verification. Do not invoke another role, apply/archive from architect, or archive from developer/reviewer. Return the Specrails JSON report after the official workflow. For developer and reviewer, load_skill already executes status and instructions apply and returns their real outputs in planning; read planning.apply.contextFiles and perform the rest of the official skill. You may refresh instructions apply when needed. Verify is a skill, not an instructions artifact.\n`;
|
|
358
|
+
return `\n## Official OpenSpec workflow binding\nExecute ${openSpecSkill(context)} for change ${context.change}. This headless transport loads the official, version-pinned skill through the openspec_workflow tool (MCP server specrails_openspec, tool workflow on CLI providers). This is explicit skill-document adaptation, not a native slash command.\nFirst call action=load_skill and follow the returned complete official skill. Bind its openspec CLI commands to this tool: new change -> action=new; status -> status; instructions <artifact> -> instructions with artifact; validate -> validate. Author each artifact yourself using write_artifact with its change-relative path and content; read the official instructions first. Do not reproduce templates from memory or return the artifacts in final JSON. The tool fixes the change identity and runs OpenSpec ${OPENSPEC_VERSION}.\nRead source/dependencies with your normal read tools. For this frozen scope, change selection is already answered, and so are the skill's store selection and project check: no store is selected (never pass --store or run store commands), and the host has admitted this repository as the OpenSpec project, so action=new may create its repo-local openspec/ root; do not run openspec list to check for one. Bind AskUserQuestion to a low-confidence final result with question (architect), or a reported blocking issue (other roles); LangGraph asks the requester. Bind TodoWrite to concise progress narration and, for developer, persist the current handoff through action=write_progress with progress {summary, completedTasks, nextTasks, checks: [{command, outcome}], blockers}. Save after each completed task or changed blocker, at most 8 KB. This host journal is separate from frozen OpenSpec artifacts; never write runtime files directly. Developer/reviewer receive savedProgress from load_skill and can refresh with read_progress. Reconcile this advisory history with current files; old test outcomes never replace required host verification. Do not invoke another role, apply/archive from architect, or archive from developer/reviewer. Return the Specrails JSON report after the official workflow. For developer and reviewer, load_skill already executes status and instructions apply and returns their real outputs in planning; read planning.apply.contextFiles and perform the rest of the official skill. You may refresh instructions apply when needed. Verify is a skill, not an instructions artifact.\n`;
|
|
353
359
|
}
|
|
354
360
|
export function writeOpenSpecBridge(context, directory) {
|
|
355
361
|
const file = path.join(directory, 'openspec-context.json');
|
|
@@ -340,7 +340,7 @@ function readPinnedVersion(scriptDir, key, fallback) {
|
|
|
340
340
|
* 3. neither set → `runCommand('npx', ['--yes', '-p', '@fission-ai/openspec@<pinned>', '--', 'openspec', 'init', …])`
|
|
341
341
|
* (default online path — users never need a global install).
|
|
342
342
|
*/
|
|
343
|
-
export function buildOpenSpecInvocation(repoRoot, provider, env = process.env, pinnedVersion = readPinnedVersion(resolveScriptDir(), 'openspec', '1.
|
|
343
|
+
export function buildOpenSpecInvocation(repoRoot, provider, env = process.env, pinnedVersion = readPinnedVersion(resolveScriptDir(), 'openspec', '1.14.3')) {
|
|
344
344
|
const bin = env.SPECRAILS_OPENSPEC_BIN;
|
|
345
345
|
const node = env.SPECRAILS_OPENSPEC_NODE;
|
|
346
346
|
const initArgs = ['init', '--tools', provider, '--profile', 'custom', repoRoot];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Provider, type ProviderAvailability } from './provider-detect.js';
|
|
2
2
|
/**
|
|
3
|
-
* OpenSpec 1.
|
|
3
|
+
* OpenSpec 1.14.3, which the installer invokes during the default init/update
|
|
4
4
|
* flow, requires Node >=20.19.0. Keep this floor independent from Kimi's npm
|
|
5
5
|
* package requirement: SpecRails launches an externally installed Kimi CLI and
|
|
6
6
|
* does not require the Kimi npm distribution.
|
|
@@ -6,7 +6,7 @@ import { info, ok, warn } from '../util/logger.js';
|
|
|
6
6
|
import { gitInstalled, initRepo, isGitRepo, repoRoot } from '../util/git.js';
|
|
7
7
|
import { assertClaudeAuthenticated, assertKimiAuthenticated, claudeVersion, detectAvailability, isSupportedKimiVersion, kimiVersion, MIN_KIMI_VERSION, resolveProvider, } from './provider-detect.js';
|
|
8
8
|
/**
|
|
9
|
-
* OpenSpec 1.
|
|
9
|
+
* OpenSpec 1.14.3, which the installer invokes during the default init/update
|
|
10
10
|
* flow, requires Node >=20.19.0. Keep this floor independent from Kimi's npm
|
|
11
11
|
* package requirement: SpecRails launches an externally installed Kimi CLI and
|
|
12
12
|
* does not require the Kimi npm distribution.
|
|
@@ -34,7 +34,7 @@ export async function checkPrerequisites(options) {
|
|
|
34
34
|
const nodeVersion = process.versions.node;
|
|
35
35
|
if (!isSupportedNodeVersion(nodeVersion)) {
|
|
36
36
|
throw new PrerequisiteError(`Node.js ${nodeVersion} is unsupported. SpecRails requires Node.js ` +
|
|
37
|
-
`${MIN_NODE_VERSION} or newer because OpenSpec 1.
|
|
37
|
+
`${MIN_NODE_VERSION} or newer because OpenSpec 1.14.3 uses that runtime floor. ` +
|
|
38
38
|
'Install a supported Node.js release from https://nodejs.org/');
|
|
39
39
|
}
|
|
40
40
|
ok(`Node.js: ${nodeVersion}`);
|
|
@@ -125,9 +125,8 @@ export function scaffoldInstallation(input) {
|
|
|
125
125
|
// --- Directory skeleton ---
|
|
126
126
|
mk(path.join(input.artifactRoot, input.providerDir));
|
|
127
127
|
if (input.provider === 'codex') {
|
|
128
|
-
// Codex skills live under <providerDir>/skills/ (e.g. .codex/skills/).
|
|
129
|
-
//
|
|
130
|
-
// that was a placeholder name from the gated state.
|
|
128
|
+
// Specrails' Codex skills live under <providerDir>/skills/ (e.g. .codex/skills/).
|
|
129
|
+
// OpenSpec's own Codex skills live under `.agents/skills/`.
|
|
131
130
|
mk(path.join(input.artifactRoot, input.providerDir, 'skills'));
|
|
132
131
|
}
|
|
133
132
|
else if (input.provider === 'gemini') {
|
|
@@ -1039,9 +1038,7 @@ function pruneLegacyArtifacts(input) {
|
|
|
1039
1038
|
path.join(input.artifactRoot, '.specrails-version'),
|
|
1040
1039
|
];
|
|
1041
1040
|
if (input.provider === 'codex') {
|
|
1042
|
-
//
|
|
1043
|
-
// legacy install before settling on the canonical `.codex/skills/`.
|
|
1044
|
-
legacyPaths.push(path.join(input.artifactRoot, '.agents'));
|
|
1041
|
+
// `.agents/skills/` is not pruned: OpenSpec 1.14+ writes its Codex skills there.
|
|
1045
1042
|
legacyPaths.push(path.join(input.artifactRoot, input.providerDir, 'skills', 'setup'));
|
|
1046
1043
|
}
|
|
1047
1044
|
else if (input.provider === 'gemini') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specrails-core",
|
|
3
|
-
"version": "6.6.
|
|
3
|
+
"version": "6.6.5",
|
|
4
4
|
"description": "Agent workflow engine for specrails-desktop: provider-independent installer and programmatic agent runtime for Claude Code and Codex",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"ci": "npm run typecheck && npm run test:scripts && npm run test:coverage && npm run check:package"
|
|
89
89
|
},
|
|
90
90
|
"dependencies": {
|
|
91
|
-
"@fission-ai/openspec": "1.
|
|
91
|
+
"@fission-ai/openspec": "1.14.3",
|
|
92
92
|
"@langchain/core": "1.2.14",
|
|
93
93
|
"@langchain/langgraph": "1.4.18",
|
|
94
94
|
"@langchain/langgraph-checkpoint": "1.1.5",
|
package/pinned-versions.json
CHANGED