@sienklogic/plan-build-run 2.37.0 → 2.38.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/package.json +1 -1
- package/plugins/copilot-pbr/agents/audit.agent.md +1 -0
- package/plugins/copilot-pbr/agents/codebase-mapper.agent.md +1 -0
- package/plugins/copilot-pbr/agents/debugger.agent.md +3 -0
- package/plugins/copilot-pbr/agents/dev-sync.agent.md +23 -0
- package/plugins/copilot-pbr/agents/executor.agent.md +1 -0
- package/plugins/copilot-pbr/agents/integration-checker.agent.md +7 -4
- package/plugins/copilot-pbr/agents/planner.agent.md +27 -1
- package/plugins/copilot-pbr/agents/researcher.agent.md +4 -1
- package/plugins/copilot-pbr/agents/verifier.agent.md +29 -12
- package/plugins/copilot-pbr/commands/test.md +5 -0
- package/plugins/copilot-pbr/plugin.json +1 -1
- package/plugins/copilot-pbr/references/plan-authoring.md +28 -0
- package/plugins/copilot-pbr/references/verification-patterns.md +44 -17
- package/plugins/copilot-pbr/skills/config/SKILL.md +12 -2
- package/plugins/copilot-pbr/skills/health/SKILL.md +13 -5
- package/plugins/copilot-pbr/skills/setup/SKILL.md +9 -1
- package/plugins/copilot-pbr/skills/shared/context-budget.md +10 -0
- package/plugins/copilot-pbr/skills/shared/universal-anti-patterns.md +6 -0
- package/plugins/copilot-pbr/skills/test/SKILL.md +210 -0
- package/plugins/cursor-pbr/.cursor-plugin/plugin.json +1 -1
- package/plugins/cursor-pbr/agents/audit.md +1 -0
- package/plugins/cursor-pbr/agents/codebase-mapper.md +1 -0
- package/plugins/cursor-pbr/agents/debugger.md +3 -0
- package/plugins/cursor-pbr/agents/dev-sync.md +23 -0
- package/plugins/cursor-pbr/agents/executor.md +1 -0
- package/plugins/cursor-pbr/agents/integration-checker.md +7 -4
- package/plugins/cursor-pbr/agents/planner.md +27 -1
- package/plugins/cursor-pbr/agents/researcher.md +4 -1
- package/plugins/cursor-pbr/agents/verifier.md +29 -12
- package/plugins/cursor-pbr/commands/test.md +5 -0
- package/plugins/cursor-pbr/references/plan-authoring.md +28 -0
- package/plugins/cursor-pbr/references/verification-patterns.md +44 -17
- package/plugins/cursor-pbr/skills/config/SKILL.md +12 -2
- package/plugins/cursor-pbr/skills/health/SKILL.md +14 -5
- package/plugins/cursor-pbr/skills/setup/SKILL.md +9 -1
- package/plugins/cursor-pbr/skills/shared/context-budget.md +10 -0
- package/plugins/cursor-pbr/skills/shared/universal-anti-patterns.md +6 -0
- package/plugins/cursor-pbr/skills/test/SKILL.md +211 -0
- package/plugins/pbr/.claude-plugin/plugin.json +1 -1
- package/plugins/pbr/agents/audit.md +1 -0
- package/plugins/pbr/agents/codebase-mapper.md +1 -0
- package/plugins/pbr/agents/debugger.md +3 -0
- package/plugins/pbr/agents/dev-sync.md +23 -0
- package/plugins/pbr/agents/executor.md +1 -0
- package/plugins/pbr/agents/integration-checker.md +7 -4
- package/plugins/pbr/agents/planner.md +27 -1
- package/plugins/pbr/agents/researcher.md +4 -1
- package/plugins/pbr/agents/verifier.md +29 -12
- package/plugins/pbr/commands/test.md +5 -0
- package/plugins/pbr/references/plan-authoring.md +28 -0
- package/plugins/pbr/references/verification-patterns.md +44 -17
- package/plugins/pbr/scripts/context-bridge.js +15 -9
- package/plugins/pbr/scripts/lib/config.js +96 -3
- package/plugins/pbr/scripts/lib/core.js +9 -0
- package/plugins/pbr/scripts/lib/migrate.js +169 -0
- package/plugins/pbr/scripts/lib/todo.js +300 -0
- package/plugins/pbr/scripts/pbr-tools.js +82 -3
- package/plugins/pbr/skills/config/SKILL.md +12 -2
- package/plugins/pbr/skills/health/SKILL.md +14 -3
- package/plugins/pbr/skills/help/SKILL.md +2 -0
- package/plugins/pbr/skills/setup/SKILL.md +9 -1
- package/plugins/pbr/skills/shared/context-budget.md +10 -0
- package/plugins/pbr/skills/shared/universal-anti-patterns.md +6 -0
- package/plugins/pbr/skills/test/SKILL.md +212 -0
|
@@ -4,9 +4,9 @@ Reference patterns for deriving verification criteria from goals. Used by the pl
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
## The
|
|
7
|
+
## The Four-Layer Check
|
|
8
8
|
|
|
9
|
-
Every must-have is verified through
|
|
9
|
+
Every must-have is verified through up to four layers, checked in order:
|
|
10
10
|
|
|
11
11
|
### Layer 1: Existence
|
|
12
12
|
|
|
@@ -62,6 +62,28 @@ grep -q "prisma" src/app.ts
|
|
|
62
62
|
grep -q "DISCORD_CLIENT_ID" src/auth/discord.ts
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
### Layer 4: Functional
|
|
66
|
+
|
|
67
|
+
Does the artifact actually work when executed?
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# Tests pass
|
|
71
|
+
npm test -- --testPathPattern auth
|
|
72
|
+
pytest tests/test_auth.py -v
|
|
73
|
+
|
|
74
|
+
# Build succeeds
|
|
75
|
+
npm run build
|
|
76
|
+
npx tsc --noEmit
|
|
77
|
+
|
|
78
|
+
# API returns correct data
|
|
79
|
+
curl -s http://localhost:3000/api/auth/login -X POST -d '{"code":"test"}' | jq '.token'
|
|
80
|
+
|
|
81
|
+
# CLI produces expected output
|
|
82
|
+
node src/cli.js --help | grep -q "Usage:"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**When to apply L4:** Only when automated verification commands exist (test suites, build scripts, API endpoints with test data). Skip for items requiring manual/visual testing. L4 is optional — artifacts passing L1-L3 without available automated tests are reported as `PASSED (L3 only)`.
|
|
86
|
+
|
|
65
87
|
---
|
|
66
88
|
|
|
67
89
|
## Verification by Feature Type
|
|
@@ -69,41 +91,46 @@ grep -q "DISCORD_CLIENT_ID" src/auth/discord.ts
|
|
|
69
91
|
### API Endpoint
|
|
70
92
|
|
|
71
93
|
```
|
|
72
|
-
Existence:
|
|
73
|
-
Substance:
|
|
74
|
-
Wiring:
|
|
94
|
+
Existence: curl returns non-404 status
|
|
95
|
+
Substance: curl returns expected response shape (correct fields)
|
|
96
|
+
Wiring: endpoint calls the right service, middleware is applied
|
|
97
|
+
Functional: POST/GET with test data returns correct response, error cases handled
|
|
75
98
|
```
|
|
76
99
|
|
|
77
100
|
### Database Schema
|
|
78
101
|
|
|
79
102
|
```
|
|
80
|
-
Existence:
|
|
81
|
-
Substance:
|
|
82
|
-
Wiring:
|
|
103
|
+
Existence: table/collection exists, can query without error
|
|
104
|
+
Substance: columns/fields match specification, constraints are applied
|
|
105
|
+
Wiring: application code references the schema, migrations run cleanly
|
|
106
|
+
Functional: CRUD operations work end-to-end, constraints reject invalid data
|
|
83
107
|
```
|
|
84
108
|
|
|
85
109
|
### Authentication
|
|
86
110
|
|
|
87
111
|
```
|
|
88
|
-
Existence:
|
|
89
|
-
Substance:
|
|
90
|
-
Wiring:
|
|
112
|
+
Existence: auth routes exist, auth module exports functions
|
|
113
|
+
Substance: login flow returns token, invalid creds return error
|
|
114
|
+
Wiring: protected routes use auth middleware, tokens are validated
|
|
115
|
+
Functional: auth tests pass (valid token, expired token, missing token, malformed token)
|
|
91
116
|
```
|
|
92
117
|
|
|
93
118
|
### UI Component
|
|
94
119
|
|
|
95
120
|
```
|
|
96
|
-
Existence:
|
|
97
|
-
Substance:
|
|
98
|
-
Wiring:
|
|
121
|
+
Existence: component file exists, exports default component
|
|
122
|
+
Substance: component renders expected elements (test or visual check)
|
|
123
|
+
Wiring: component is imported in parent, receives correct props, routes to it
|
|
124
|
+
Functional: component tests pass, build succeeds with component included
|
|
99
125
|
```
|
|
100
126
|
|
|
101
127
|
### Configuration
|
|
102
128
|
|
|
103
129
|
```
|
|
104
|
-
Existence:
|
|
105
|
-
Substance:
|
|
106
|
-
Wiring:
|
|
130
|
+
Existence: config file exists, environment variables documented
|
|
131
|
+
Substance: config values are used (not dead code), defaults are sensible
|
|
132
|
+
Wiring: application reads config at startup, config changes take effect
|
|
133
|
+
Functional: app starts with config, missing config produces clear error message
|
|
107
134
|
```
|
|
108
135
|
|
|
109
136
|
---
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
* PEAK (0-30%) — no warnings
|
|
12
12
|
* GOOD (30-50%) — no warnings
|
|
13
13
|
* DEGRADING (50-70%) — suggest subagent delegation
|
|
14
|
-
* POOR (70
|
|
14
|
+
* POOR (70-85%) — recommend /pbr:pause
|
|
15
|
+
* CRITICAL (85%+) — urgent stop, context rot imminent
|
|
15
16
|
*
|
|
16
|
-
* Debounce: same-tier warnings suppressed for 5 tool calls.
|
|
17
|
+
* Debounce: same-tier warnings suppressed for 5 tool calls (2 for CRITICAL).
|
|
17
18
|
* Tier escalation always warns immediately.
|
|
18
19
|
*
|
|
19
20
|
* Exit codes:
|
|
@@ -28,15 +29,18 @@ const TIERS = [
|
|
|
28
29
|
{ name: 'PEAK', min: 0, max: 30 },
|
|
29
30
|
{ name: 'GOOD', min: 30, max: 50 },
|
|
30
31
|
{ name: 'DEGRADING', min: 50, max: 70 },
|
|
31
|
-
{ name: 'POOR', min: 70, max:
|
|
32
|
+
{ name: 'POOR', min: 70, max: 85 },
|
|
33
|
+
{ name: 'CRITICAL', min: 85, max: 100 }
|
|
32
34
|
];
|
|
33
35
|
|
|
34
36
|
const TIER_MESSAGES = {
|
|
35
|
-
DEGRADING: 'Context
|
|
36
|
-
POOR: 'Context
|
|
37
|
+
DEGRADING: 'Context at ~50-70%. Delegate heavy reads and analysis to Task() subagents to preserve orchestrator quality.',
|
|
38
|
+
POOR: 'Context at ~70-85%. Run /pbr:pause soon to save state before quality degrades.',
|
|
39
|
+
CRITICAL: 'STOP — Context at 85%+. Run /pbr:pause NOW. Context rot is imminent — further work risks hallucinations and skipped steps.'
|
|
37
40
|
};
|
|
38
41
|
|
|
39
42
|
const DEBOUNCE_INTERVAL = 5; // tool calls between same-tier warnings
|
|
43
|
+
const CRITICAL_DEBOUNCE_INTERVAL = 2; // shorter debounce for CRITICAL tier
|
|
40
44
|
|
|
41
45
|
/**
|
|
42
46
|
* Determine the context tier for a given percentage.
|
|
@@ -117,13 +121,14 @@ function shouldWarn(bridge, tierName) {
|
|
|
117
121
|
const callsSinceWarn = bridge.calls_since_warn || 0;
|
|
118
122
|
|
|
119
123
|
// Tier escalation — always warn
|
|
120
|
-
const tierOrder = { PEAK: 0, GOOD: 1, DEGRADING: 2, POOR: 3 };
|
|
124
|
+
const tierOrder = { PEAK: 0, GOOD: 1, DEGRADING: 2, POOR: 3, CRITICAL: 4 };
|
|
121
125
|
if ((tierOrder[tierName] || 0) > (tierOrder[prevTier] || 0)) {
|
|
122
126
|
return true;
|
|
123
127
|
}
|
|
124
128
|
|
|
125
|
-
// Same tier — debounce
|
|
126
|
-
|
|
129
|
+
// Same tier — debounce (CRITICAL uses shorter interval)
|
|
130
|
+
const interval = tierName === 'CRITICAL' ? CRITICAL_DEBOUNCE_INTERVAL : DEBOUNCE_INTERVAL;
|
|
131
|
+
if (callsSinceWarn >= interval) {
|
|
127
132
|
return true;
|
|
128
133
|
}
|
|
129
134
|
|
|
@@ -253,7 +258,8 @@ module.exports = {
|
|
|
253
258
|
updateBridge,
|
|
254
259
|
TIERS,
|
|
255
260
|
TIER_MESSAGES,
|
|
256
|
-
DEBOUNCE_INTERVAL
|
|
261
|
+
DEBOUNCE_INTERVAL,
|
|
262
|
+
CRITICAL_DEBOUNCE_INTERVAL
|
|
257
263
|
};
|
|
258
264
|
|
|
259
265
|
if (require.main === module || process.argv[1] === __filename) { main(); }
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const { validateObject } = require('./core');
|
|
11
|
+
const { CURRENT_SCHEMA_VERSION } = require('./migrate');
|
|
11
12
|
|
|
12
13
|
// --- Cached config loader ---
|
|
13
14
|
|
|
@@ -84,10 +85,11 @@ function configValidate(preloadedConfig, planningDir) {
|
|
|
84
85
|
|
|
85
86
|
validateObject(config, schema, '', errors, warnings);
|
|
86
87
|
|
|
87
|
-
// Schema version check — detect outdated config format
|
|
88
|
-
const CURRENT_SCHEMA_VERSION = 1;
|
|
88
|
+
// Schema version check — detect outdated or future config format
|
|
89
89
|
if (config.schema_version && config.schema_version > CURRENT_SCHEMA_VERSION) {
|
|
90
90
|
warnings.push(`config.json schema_version (${config.schema_version}) is newer than this PBR version supports (${CURRENT_SCHEMA_VERSION}). Some fields may be ignored. Consider updating PBR.`);
|
|
91
|
+
} else if (!config.schema_version || config.schema_version < CURRENT_SCHEMA_VERSION) {
|
|
92
|
+
warnings.push(`config.json schema is outdated. Run: node pbr-tools.js migrate`);
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
// Semantic conflict detection — logical contradictions that pass schema validation
|
|
@@ -169,10 +171,101 @@ function resolveDepthProfile(config) {
|
|
|
169
171
|
return { depth, profile };
|
|
170
172
|
}
|
|
171
173
|
|
|
174
|
+
// --- User-level defaults ---
|
|
175
|
+
|
|
176
|
+
const USER_DEFAULTS_PATH = path.join(
|
|
177
|
+
process.env.HOME || process.env.USERPROFILE || '',
|
|
178
|
+
'.claude',
|
|
179
|
+
'pbr-defaults.json'
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Load user-level defaults from ~/.claude/pbr-defaults.json.
|
|
184
|
+
* Returns null if file doesn't exist or is invalid.
|
|
185
|
+
*
|
|
186
|
+
* @returns {object|null} User defaults or null
|
|
187
|
+
*/
|
|
188
|
+
function loadUserDefaults() {
|
|
189
|
+
try {
|
|
190
|
+
if (!fs.existsSync(USER_DEFAULTS_PATH)) return null;
|
|
191
|
+
return JSON.parse(fs.readFileSync(USER_DEFAULTS_PATH, 'utf8'));
|
|
192
|
+
} catch (_e) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Save current project config as user-level defaults.
|
|
199
|
+
* Only saves portable keys (excludes project-specific state).
|
|
200
|
+
*
|
|
201
|
+
* @param {object} config - Current project config.json contents
|
|
202
|
+
* @returns {{ saved: boolean, path: string, keys: string[] }}
|
|
203
|
+
*/
|
|
204
|
+
function saveUserDefaults(config) {
|
|
205
|
+
const portableKeys = [
|
|
206
|
+
'mode', 'depth', 'context_strategy',
|
|
207
|
+
'features', 'models', 'parallelization',
|
|
208
|
+
'planning', 'git', 'gates', 'safety',
|
|
209
|
+
'hooks', 'dashboard', 'status_line'
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
const defaults = {};
|
|
213
|
+
for (const key of portableKeys) {
|
|
214
|
+
if (config[key] !== undefined) {
|
|
215
|
+
defaults[key] = config[key];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const dir = path.dirname(USER_DEFAULTS_PATH);
|
|
220
|
+
if (!fs.existsSync(dir)) {
|
|
221
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
222
|
+
}
|
|
223
|
+
fs.writeFileSync(USER_DEFAULTS_PATH, JSON.stringify(defaults, null, 2), 'utf8');
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
saved: true,
|
|
227
|
+
path: USER_DEFAULTS_PATH,
|
|
228
|
+
keys: Object.keys(defaults)
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Deep-merge user defaults into a base config.
|
|
234
|
+
* User defaults provide values only where the base config doesn't already set them.
|
|
235
|
+
* For nested objects, merges recursively. Scalars from base take precedence.
|
|
236
|
+
*
|
|
237
|
+
* @param {object} base - The base config (project defaults from setup)
|
|
238
|
+
* @param {object} userDefaults - User-level defaults from ~/.claude/pbr-defaults.json
|
|
239
|
+
* @returns {object} Merged config
|
|
240
|
+
*/
|
|
241
|
+
function mergeUserDefaults(base, userDefaults) {
|
|
242
|
+
if (!userDefaults) return base;
|
|
243
|
+
|
|
244
|
+
const result = { ...base };
|
|
245
|
+
for (const [key, value] of Object.entries(userDefaults)) {
|
|
246
|
+
if (result[key] === undefined) {
|
|
247
|
+
// Key not in base — use user default
|
|
248
|
+
result[key] = value;
|
|
249
|
+
} else if (
|
|
250
|
+
typeof value === 'object' && value !== null && !Array.isArray(value) &&
|
|
251
|
+
typeof result[key] === 'object' && result[key] !== null && !Array.isArray(result[key])
|
|
252
|
+
) {
|
|
253
|
+
// Both are objects — merge recursively (base values take precedence)
|
|
254
|
+
result[key] = mergeUserDefaults(result[key], value);
|
|
255
|
+
}
|
|
256
|
+
// Scalar in base already set — base wins
|
|
257
|
+
}
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
|
|
172
261
|
module.exports = {
|
|
173
262
|
configLoad,
|
|
174
263
|
configClearCache,
|
|
175
264
|
configValidate,
|
|
176
265
|
resolveDepthProfile,
|
|
177
|
-
DEPTH_PROFILE_DEFAULTS
|
|
266
|
+
DEPTH_PROFILE_DEFAULTS,
|
|
267
|
+
loadUserDefaults,
|
|
268
|
+
saveUserDefaults,
|
|
269
|
+
mergeUserDefaults,
|
|
270
|
+
USER_DEFAULTS_PATH
|
|
178
271
|
};
|
|
@@ -319,6 +319,15 @@ function atomicWrite(filePath, content) {
|
|
|
319
319
|
// 3. Rename temp over original (atomic on most filesystems)
|
|
320
320
|
fs.renameSync(tmpPath, filePath);
|
|
321
321
|
|
|
322
|
+
// 4. Clean up backup file on success
|
|
323
|
+
try {
|
|
324
|
+
if (fs.existsSync(bakPath)) {
|
|
325
|
+
fs.unlinkSync(bakPath);
|
|
326
|
+
}
|
|
327
|
+
} catch (_e) {
|
|
328
|
+
// Cleanup failure is non-fatal
|
|
329
|
+
}
|
|
330
|
+
|
|
322
331
|
return { success: true };
|
|
323
332
|
} catch (e) {
|
|
324
333
|
// Rename failed — try to restore from backup
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/migrate.js — Schema migration for Plan-Build-Run config.json.
|
|
3
|
+
*
|
|
4
|
+
* Tracks config.json schema version and applies sequential migrations
|
|
5
|
+
* to bring outdated configs up to the current version.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const { applyMigrations, CURRENT_SCHEMA_VERSION } = require('./migrate');
|
|
9
|
+
* const result = await applyMigrations(planningDir, { dryRun: false });
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { atomicWrite } = require('./core');
|
|
15
|
+
|
|
16
|
+
/** The current schema version supported by this version of PBR. */
|
|
17
|
+
const CURRENT_SCHEMA_VERSION = 1;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Migration registry. Each entry describes one schema version step.
|
|
21
|
+
* Migrations MUST be listed in ascending `from` order.
|
|
22
|
+
*
|
|
23
|
+
* @type {Array<{ from: number, to: number, description: string, migrate: function }>}
|
|
24
|
+
*/
|
|
25
|
+
const MIGRATIONS = [
|
|
26
|
+
{
|
|
27
|
+
from: 0,
|
|
28
|
+
to: 1,
|
|
29
|
+
description: 'Add schema_version field',
|
|
30
|
+
migrate(config) {
|
|
31
|
+
config.schema_version = 1;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Detect the current schema version from a config object.
|
|
38
|
+
* Returns 0 if schema_version is absent or non-numeric.
|
|
39
|
+
*
|
|
40
|
+
* @param {object} config - Parsed config.json object
|
|
41
|
+
* @returns {number} Detected schema version
|
|
42
|
+
*/
|
|
43
|
+
function detectSchemaVersion(config) {
|
|
44
|
+
const v = config && config.schema_version;
|
|
45
|
+
if (typeof v === 'number' && Number.isFinite(v)) return v;
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Return the ordered list of migrations needed to go from `fromVersion` to `toVersion`.
|
|
51
|
+
* Returns an empty array if versions are equal.
|
|
52
|
+
* Throws if fromVersion > toVersion (downgrade not supported).
|
|
53
|
+
*
|
|
54
|
+
* @param {number} fromVersion - Current schema version
|
|
55
|
+
* @param {number} toVersion - Target schema version
|
|
56
|
+
* @returns {Array} Ordered migrations to apply
|
|
57
|
+
*/
|
|
58
|
+
function getMigrationPath(fromVersion, toVersion) {
|
|
59
|
+
if (fromVersion > toVersion) {
|
|
60
|
+
throw new Error(`Cannot downgrade schema from version ${fromVersion} to ${toVersion}`);
|
|
61
|
+
}
|
|
62
|
+
if (fromVersion === toVersion) return [];
|
|
63
|
+
return MIGRATIONS.filter(m => m.from >= fromVersion && m.to <= toVersion);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Apply pending migrations to config.json in planningDir.
|
|
68
|
+
*
|
|
69
|
+
* Options:
|
|
70
|
+
* dryRun {boolean} — If true, simulate migration without writing files (default: false)
|
|
71
|
+
* force {boolean} — Reserved for future use (default: false)
|
|
72
|
+
*
|
|
73
|
+
* Returns:
|
|
74
|
+
* { migrated: false, version: N } — already current
|
|
75
|
+
* { migrated: false, message: string } — future version, no-op
|
|
76
|
+
* { migrated: true, fromVersion, toVersion, applied, backupPath } — success
|
|
77
|
+
* { error: string } — failure
|
|
78
|
+
*
|
|
79
|
+
* @param {string} planningDir - Path to .planning directory
|
|
80
|
+
* @param {object} [options] - Options { dryRun, force }
|
|
81
|
+
* @returns {Promise<object>} Result object
|
|
82
|
+
*/
|
|
83
|
+
async function applyMigrations(planningDir, options) {
|
|
84
|
+
const opts = options || {};
|
|
85
|
+
const dryRun = opts.dryRun === true;
|
|
86
|
+
|
|
87
|
+
const configPath = path.join(planningDir, 'config.json');
|
|
88
|
+
|
|
89
|
+
// Load config.json
|
|
90
|
+
if (!fs.existsSync(configPath)) {
|
|
91
|
+
return { error: 'config.json not found in ' + planningDir };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let config;
|
|
95
|
+
try {
|
|
96
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
97
|
+
} catch (e) {
|
|
98
|
+
return { error: 'config.json is not valid JSON: ' + e.message };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const currentVersion = detectSchemaVersion(config);
|
|
102
|
+
|
|
103
|
+
// Future version — don't touch it
|
|
104
|
+
if (currentVersion > CURRENT_SCHEMA_VERSION) {
|
|
105
|
+
return {
|
|
106
|
+
migrated: false,
|
|
107
|
+
version: currentVersion,
|
|
108
|
+
message: `config.json schema_version (${currentVersion}) is newer than this PBR version supports (${CURRENT_SCHEMA_VERSION}). No migration applied.`
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Already current
|
|
113
|
+
if (currentVersion === CURRENT_SCHEMA_VERSION) {
|
|
114
|
+
return { migrated: false, version: currentVersion };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Determine migrations to apply
|
|
118
|
+
const migrations = getMigrationPath(currentVersion, CURRENT_SCHEMA_VERSION);
|
|
119
|
+
if (migrations.length === 0) {
|
|
120
|
+
return { migrated: false, version: currentVersion };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Clone config for mutation
|
|
124
|
+
const updatedConfig = JSON.parse(JSON.stringify(config));
|
|
125
|
+
|
|
126
|
+
// Apply each migration in sequence
|
|
127
|
+
const applied = [];
|
|
128
|
+
for (const m of migrations) {
|
|
129
|
+
m.migrate(updatedConfig);
|
|
130
|
+
applied.push(m.description);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (dryRun) {
|
|
134
|
+
return {
|
|
135
|
+
migrated: true,
|
|
136
|
+
fromVersion: currentVersion,
|
|
137
|
+
toVersion: CURRENT_SCHEMA_VERSION,
|
|
138
|
+
applied,
|
|
139
|
+
dryRun: true
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Create backup
|
|
144
|
+
const backupDir = path.join(planningDir, '.migration-backup');
|
|
145
|
+
const backupPath = path.join(backupDir, 'config.json.bak');
|
|
146
|
+
if (!fs.existsSync(backupDir)) {
|
|
147
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
148
|
+
}
|
|
149
|
+
fs.copyFileSync(configPath, backupPath);
|
|
150
|
+
|
|
151
|
+
// Write updated config atomically
|
|
152
|
+
atomicWrite(configPath, JSON.stringify(updatedConfig, null, 2));
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
migrated: true,
|
|
156
|
+
fromVersion: currentVersion,
|
|
157
|
+
toVersion: CURRENT_SCHEMA_VERSION,
|
|
158
|
+
applied,
|
|
159
|
+
backupPath
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = {
|
|
164
|
+
CURRENT_SCHEMA_VERSION,
|
|
165
|
+
MIGRATIONS,
|
|
166
|
+
detectSchemaVersion,
|
|
167
|
+
getMigrationPath,
|
|
168
|
+
applyMigrations
|
|
169
|
+
};
|