create-principles-disciple 1.105.0 → 1.107.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.
Files changed (60) hide show
  1. package/console/dist/server/index.js +16 -0
  2. package/console/dist/server/routes/update.js +126 -9
  3. package/console/dist/server/utils/pd-backups.d.ts +24 -0
  4. package/console/dist/server/utils/pd-backups.js +125 -0
  5. package/console/dist/web/assets/app.css +3 -0
  6. package/core/dist/runtime-v2/__tests__/architecture-regression.test.js +10 -1
  7. package/core/dist/runtime-v2/__tests__/architecture-regression.test.js.map +1 -1
  8. package/core/dist/runtime-v2/activation/__tests__/prompt-activation-reader-contract.test.js +22 -1
  9. package/core/dist/runtime-v2/activation/__tests__/prompt-activation-reader-contract.test.js.map +1 -1
  10. package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.d.ts +12 -1
  11. package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.d.ts.map +1 -1
  12. package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.js +8 -1
  13. package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.js.map +1 -1
  14. package/core/dist/runtime-v2/feature-flags/__tests__/principle-receipt-block-copy-flag.test.d.ts +2 -0
  15. package/core/dist/runtime-v2/feature-flags/__tests__/principle-receipt-block-copy-flag.test.d.ts.map +1 -0
  16. package/core/dist/runtime-v2/feature-flags/__tests__/principle-receipt-block-copy-flag.test.js +42 -0
  17. package/core/dist/runtime-v2/feature-flags/__tests__/principle-receipt-block-copy-flag.test.js.map +1 -0
  18. package/core/dist/runtime-v2/feature-flags/__tests__/principle-receipt-ledger-flag.test.d.ts +2 -0
  19. package/core/dist/runtime-v2/feature-flags/__tests__/principle-receipt-ledger-flag.test.d.ts.map +1 -0
  20. package/core/dist/runtime-v2/feature-flags/__tests__/principle-receipt-ledger-flag.test.js +42 -0
  21. package/core/dist/runtime-v2/feature-flags/__tests__/principle-receipt-ledger-flag.test.js.map +1 -0
  22. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.d.ts.map +1 -1
  23. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js +18 -0
  24. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js.map +1 -1
  25. package/core/dist/runtime-v2/store/sqlite-connection.d.ts.map +1 -1
  26. package/core/dist/runtime-v2/store/sqlite-connection.js +27 -0
  27. package/core/dist/runtime-v2/store/sqlite-connection.js.map +1 -1
  28. package/dist/installer.d.ts +22 -0
  29. package/dist/installer.d.ts.map +1 -1
  30. package/dist/installer.js +67 -5
  31. package/dist/installer.js.map +1 -1
  32. package/dist/mvp-config.d.ts +8 -0
  33. package/dist/mvp-config.d.ts.map +1 -1
  34. package/dist/mvp-config.js +10 -0
  35. package/dist/mvp-config.js.map +1 -1
  36. package/dist/skill-language.d.ts +32 -0
  37. package/dist/skill-language.d.ts.map +1 -0
  38. package/dist/skill-language.js +107 -0
  39. package/dist/skill-language.js.map +1 -0
  40. package/dist/updater.d.ts.map +1 -1
  41. package/dist/updater.js +29 -4
  42. package/dist/updater.js.map +1 -1
  43. package/host-runtime/dist/active-principle-prompt.js +5 -1
  44. package/package.json +1 -1
  45. package/pd-cli/dist/commands/console.d.ts.map +1 -1
  46. package/pd-cli/dist/commands/console.js +2 -0
  47. package/pd-cli/dist/commands/console.js.map +1 -1
  48. package/pd-cli/dist/services/console-launcher.d.ts +6 -0
  49. package/pd-cli/dist/services/console-launcher.d.ts.map +1 -1
  50. package/pd-cli/dist/services/console-launcher.js.map +1 -1
  51. package/plugin/dist/bundle.js +550 -477
  52. package/plugin/dist/core/principle-application-ledger.d.ts +45 -0
  53. package/plugin/dist/core/principle-receipt-metadata.d.ts +15 -0
  54. package/plugin/dist/hooks/gate-block-helper.d.ts +4 -0
  55. package/plugin/dist/openclaw-sdk.d.ts +23 -1
  56. package/plugin/dist/openclaw.plugin.json +0 -1
  57. package/plugin/openclaw.plugin.json +1 -2
  58. package/plugin/package.json +1 -1
  59. package/plugin/scripts/sync-plugin.mjs +17 -8
  60. package/plugin/scripts/verify-build.mjs +19 -8
@@ -24,6 +24,7 @@ import { handleUpdateRoute } from './routes/update.js';
24
24
  import { handleUpdateHistoryRoute } from './routes/update-history.js';
25
25
  import { handleConfigRoute } from './routes/config.js';
26
26
  import { sendJson, sendNotFound, sendUnauthorized } from './utils/response.js';
27
+ import { migrateLegacyExtensionBackups, resolvePdBackupsRoot } from './utils/pd-backups.js';
27
28
  const __filename = fileURLToPath(import.meta.url);
28
29
  const __dirname = path.dirname(__filename);
29
30
  // WEB_ROOT calculation: server is at PKG_ROOT/dist/server/index.js
@@ -400,6 +401,21 @@ export async function main() {
400
401
  console.warn('[pd-console] No auth token configured. Running without authentication. Use --token or PD_CONSOLE_TOKEN to enable.');
401
402
  }
402
403
  const services = await initServices(workspace, authConfig);
404
+ // One-time hygiene: move legacy PD backups out of ~/.openclaw/extensions so
405
+ // OpenClaw plugin discovery stops reporting a duplicate principles-disciple
406
+ // plugin on every gateway startup. Best-effort, never blocks startup.
407
+ try {
408
+ const legacy = migrateLegacyExtensionBackups();
409
+ if (legacy.movedFrom.length > 0) {
410
+ console.log(`[pd-console] Migrated ${legacy.movedFrom.length} legacy PD backup dir(s) out of the extensions dir to ${resolvePdBackupsRoot()}`);
411
+ }
412
+ for (const failure of legacy.failed) {
413
+ console.warn(`[pd-console] Could not migrate legacy PD backup "${failure.name}" out of the extensions dir: ${failure.reason}. Move it out manually to silence the OpenClaw "duplicate plugin id" warning.`);
414
+ }
415
+ }
416
+ catch (err) {
417
+ console.warn('[pd-console] Legacy backup migration failed:', err instanceof Error ? err.message : err);
418
+ }
403
419
  const server = http.createServer(handleRequest(services));
404
420
  const shutdown = async (signal) => {
405
421
  console.log(`[pd-console] Received ${signal}, shutting down...`);
@@ -6,6 +6,7 @@ import semver from 'semver';
6
6
  import { sendSuccess, sendError, sendMethodNotAllowed, sendBadRequest, sendNotFound, } from '../utils/response.js';
7
7
  import { appendUpdateHistory } from './update-history.js';
8
8
  import { checkOpenClawGateway, stopOpenClawGateway, restartOpenClawGateway, } from '../utils/gateway.js';
9
+ import { migrateLegacyExtensionBackups, reservePdBackupDestination, resolvePdBackupsRoot, } from '../utils/pd-backups.js';
9
10
  // ---------------------------------------------------------------------------
10
11
  // Constants
11
12
  // ---------------------------------------------------------------------------
@@ -82,12 +83,27 @@ function isValidMergeStrategy(value) {
82
83
  function validatePathInWorkspace(target, workspaceDir) {
83
84
  const resolved = path.resolve(target);
84
85
  const resolvedWorkspace = path.resolve(workspaceDir);
85
- // Allow paths within workspace or within the OpenClaw extensions directory.
86
- // The extensions dir is under the OpenClaw install home (~/.openclaw/extensions),
87
- // which is NOT derived from the workspace dir (the two may be on different drives).
86
+ // Allow paths within workspace, within the OpenClaw extensions directory,
87
+ // or within the PD backups root. The extensions dir and backups root are
88
+ // under the OpenClaw install home (~/.openclaw), which is NOT derived from
89
+ // the workspace dir (the two may be on different drives).
88
90
  const extensionsDir = path.resolve(resolveExtensionsDir());
89
- return resolved.startsWith(resolvedWorkspace + path.sep) || resolved === resolvedWorkspace
90
- || resolved.startsWith(extensionsDir + path.sep) || resolved === extensionsDir;
91
+ const backupsRoot = path.resolve(resolvePdBackupsRoot());
92
+ const insideRoot = (root) => resolved.startsWith(root + path.sep) || resolved === root;
93
+ return insideRoot(resolvedWorkspace) || insideRoot(extensionsDir) || insideRoot(backupsRoot);
94
+ }
95
+ /**
96
+ * Log the result of a legacy-backup migration (rc-9: failures are surfaced,
97
+ * never silent). Returns nothing; migration itself is best-effort.
98
+ */
99
+ function logLegacyBackupMigration(source) {
100
+ const legacy = migrateLegacyExtensionBackups();
101
+ if (legacy.movedFrom.length > 0) {
102
+ console.log(`[${source}] Migrated ${legacy.movedFrom.length} legacy PD backup dir(s) out of the extensions dir to ${resolvePdBackupsRoot()}`);
103
+ }
104
+ for (const failure of legacy.failed) {
105
+ console.warn(`[${source}] Could not migrate legacy PD backup "${failure.name}" out of the extensions dir: ${failure.reason}. Move it out manually to silence the OpenClaw "duplicate plugin id" warning.`);
106
+ }
91
107
  }
92
108
  // ---------------------------------------------------------------------------
93
109
  // Core update operations (inline to avoid cross-package import)
@@ -128,6 +144,85 @@ function copyFileTo(src, dest) {
128
144
  fs.mkdirSync(dir, { recursive: true });
129
145
  fs.copyFileSync(src, dest);
130
146
  }
147
+ // --- Plugin skill-language preservation (PR #1332 companion) -----------------
148
+ // OpenClaw publishes plugin skills by directory name (first declaration
149
+ // wins, same-name roots only warn — no locale mechanism), so a manifest must
150
+ // declare exactly ONE language root. The shipped manifest declares zh
151
+ // (product default); installs made with --lang en have their installed
152
+ // manifest rewritten to the en root by the installer. Updates ship a fresh
153
+ // zh-default manifest and would silently revert that choice — so capture the
154
+ // installed language BEFORE any mutation and re-apply it after the new
155
+ // manifest lands. Mirrors create-principles-disciple/src/skill-language.ts
156
+ // (keep the two transforms in sync).
157
+ const SKILL_LANGUAGE_ROOTS = {
158
+ zh: 'templates/langs/zh/skills',
159
+ en: 'templates/langs/en/skills',
160
+ };
161
+ function isLanguageSkillRoot(entry) {
162
+ if (typeof entry !== 'string')
163
+ return false;
164
+ const normalized = entry.replaceAll('\\', '/');
165
+ return normalized === SKILL_LANGUAGE_ROOTS.zh || normalized === SKILL_LANGUAGE_ROOTS.en;
166
+ }
167
+ function detectInstalledSkillLanguage(extDir) {
168
+ try {
169
+ const manifestPath = path.join(extDir, 'openclaw.plugin.json');
170
+ if (!fs.existsSync(manifestPath))
171
+ return 'zh';
172
+ const raw = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
173
+ if (!isRecord(raw) || !Array.isArray(raw.skills))
174
+ return 'zh';
175
+ const languages = new Set();
176
+ for (const entry of raw.skills) {
177
+ if (typeof entry !== 'string')
178
+ continue;
179
+ const normalized = entry.replaceAll('\\', '/');
180
+ if (normalized === SKILL_LANGUAGE_ROOTS.zh)
181
+ languages.add('zh');
182
+ else if (normalized === SKILL_LANGUAGE_ROOTS.en)
183
+ languages.add('en');
184
+ }
185
+ // Exactly one language root → that is the user's selection. Zero or both
186
+ // (pre-ERR-097 legacy install) → product default.
187
+ if (languages.size !== 1)
188
+ return 'zh';
189
+ for (const language of languages)
190
+ return language;
191
+ return 'zh';
192
+ }
193
+ catch {
194
+ return 'zh';
195
+ }
196
+ }
197
+ function reapplySkillLanguage(extDir, language) {
198
+ try {
199
+ const manifestPath = path.join(extDir, 'openclaw.plugin.json');
200
+ if (!fs.existsSync(manifestPath))
201
+ return;
202
+ // Defense in depth: only rewrite when the selected language's templates
203
+ // actually exist — otherwise the host would warn "plugin skill path not
204
+ // found" and publish nothing.
205
+ if (!fs.existsSync(path.join(extDir, SKILL_LANGUAGE_ROOTS[language]))) {
206
+ console.error(`[pd-console:update] skill language "${language}" templates missing — keeping shipped manifest skills`);
207
+ return;
208
+ }
209
+ const raw = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
210
+ if (!isRecord(raw) || !Array.isArray(raw.skills))
211
+ return;
212
+ const skills = raw.skills;
213
+ const kept = skills.filter((entry) => typeof entry === 'string' && !isLanguageSkillRoot(entry));
214
+ const next = [...kept, SKILL_LANGUAGE_ROOTS[language]];
215
+ const unchanged = next.length === skills.length && next.every((entry, i) => entry === skills[i]);
216
+ if (unchanged)
217
+ return;
218
+ raw.skills = next;
219
+ fs.writeFileSync(manifestPath, JSON.stringify(raw, null, 2) + '\n', 'utf-8');
220
+ }
221
+ catch (error) {
222
+ // rc-9: degrade observably, never fail the whole update over skill language.
223
+ console.error(`[pd-console:update] failed to re-apply skill language "${language}": ${error instanceof Error ? error.message : String(error)}`);
224
+ }
225
+ }
131
226
  function getAllFilesLocal(dir, skipDirs) {
132
227
  const skip = skipDirs ?? new Set();
133
228
  const result = [];
@@ -293,6 +388,13 @@ async function doApplyUpdate(options, workspaceDir) {
293
388
  try {
294
389
  // 0. Save current version BEFORE any changes
295
390
  const fromVersion = readCurrentVersion(targetDir) ?? 'unknown';
391
+ // Capture the installed skill language before the tarball's manifest
392
+ // overwrites it (PR #1332 companion — see reapplySkillLanguage).
393
+ const skillLanguage = detectInstalledSkillLanguage(targetDir);
394
+ // 0.5 Heal legacy installs: older versions left backups inside the
395
+ // extensions dir, where OpenClaw discovery picks them up as duplicate
396
+ // plugins. Move them out before creating a new backup alongside them.
397
+ logLegacyBackupMigration('pd-update');
296
398
  // 1. Fetch latest package info (with timeout + retry)
297
399
  const response = await fetchWithRetry(NPM_REGISTRY_LATEST, 'Registry check');
298
400
  const rawData = await response.json();
@@ -307,10 +409,13 @@ async function doApplyUpdate(options, workspaceDir) {
307
409
  if (!tarball)
308
410
  return { success: false, message: 'Missing tarball URL in registry response' };
309
411
  // 2. Create backup if requested (Fix 2: skip node_modules to avoid EPERM
310
- // from locked native modules and npm symlinks/junctions)
412
+ // from locked native modules and npm symlinks/junctions).
413
+ // The backup lives in <openclawHome>/pd-backups — OUTSIDE the
414
+ // extensions dir, because OpenClaw plugin discovery scans every
415
+ // extensions/ child and would report the backup as a duplicate
416
+ // "principles-disciple" plugin on every gateway startup.
311
417
  if (createBackup) {
312
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
313
- backupPath = path.join(path.dirname(targetDir), `.pd-backup-${timestamp}`);
418
+ backupPath = reservePdBackupDestination(path.basename(targetDir));
314
419
  copyDirRecursive(targetDir, backupPath, SKIP_DIRS);
315
420
  }
316
421
  // 3. Download and extract new version (with timeout + retry)
@@ -355,6 +460,9 @@ async function doApplyUpdate(options, workspaceDir) {
355
460
  copyFileTo(path.join(tempDir, file), path.join(targetDir, file));
356
461
  updatedFiles.push(file);
357
462
  }
463
+ // 4b. The incoming manifest declares the product-default skill language
464
+ // (zh); restore the language this install was materialized with.
465
+ reapplySkillLanguage(targetDir, skillLanguage);
358
466
  // Fix 3: deletions intentionally skipped — see comment above.
359
467
  // 5. Update package.json version
360
468
  const pkgPath = path.join(targetDir, 'package.json');
@@ -487,7 +595,13 @@ async function doInlineFullUpdate(workspaceDir) {
487
595
  }
488
596
  // Capture version before changes
489
597
  const fromVersion = readCurrentVersion(extDir) ?? 'unknown';
598
+ // Capture the installed skill language before the tarball's manifest
599
+ // overwrites it (PR #1332 companion — see reapplySkillLanguage).
600
+ const skillLanguage = detectInstalledSkillLanguage(extDir);
490
601
  let tempDir;
602
+ // Heal legacy installs: move old backups out of the extensions dir so
603
+ // OpenClaw discovery stops reporting a duplicate plugin (see pd-backups.ts).
604
+ logLegacyBackupMigration('pd-update-full');
491
605
  try {
492
606
  // 2. Fetch installer package info from npm
493
607
  const response = await fetchWithRetry(NPM_REGISTRY_INSTALLER, 'Installer registry check');
@@ -527,6 +641,9 @@ async function doInlineFullUpdate(workspaceDir) {
527
641
  const pluginSrc = path.join(tempDir, 'plugin');
528
642
  if (fs.existsSync(pluginSrc)) {
529
643
  copyDirRecursive(pluginSrc, extDir, SKIP_DIRS);
644
+ // The incoming manifest declares the product-default skill language
645
+ // (zh); restore the language this install was materialized with.
646
+ reapplySkillLanguage(extDir, skillLanguage);
530
647
  }
531
648
  // b. console/ → extDir/console/ (overwrite dist/ files; do NOT rmSync —
532
649
  // console/node_modules/ may contain locked native modules like
@@ -725,7 +842,7 @@ export async function handleUpdateRoute(req, res, workspaceDir, subPath) {
725
842
  return;
726
843
  }
727
844
  if (!validatePathInWorkspace(backupDir, workspaceDir)) {
728
- sendBadRequest(res, 'backupDir must be within workspace or extensions directory');
845
+ sendBadRequest(res, 'backupDir must be within the workspace, extensions directory, or PD backups directory');
729
846
  return;
730
847
  }
731
848
  const result = await doRollbackUpdate({ targetDir, backupDir }, workspaceDir);
@@ -0,0 +1,24 @@
1
+ export interface LegacyBackupMigrationResult {
2
+ movedFrom: string[];
3
+ failed: {
4
+ name: string;
5
+ reason: string;
6
+ }[];
7
+ }
8
+ /** Backups root — outside the extensions dir OpenClaw scans for plugins. */
9
+ export declare function resolvePdBackupsRoot(): string;
10
+ /**
11
+ * Reserve a unique backup destination under the backups root:
12
+ * <root>/<targetName>-<ISO timestamp>, with a numeric suffix on collision.
13
+ * Atomic reservation via plain (non-recursive) mkdirSync — EEXIST means the
14
+ * name is taken, retry with the next suffix. The retry loop is bounded; never
15
+ * poll existsSync in a loop (a stale/mocked always-true predicate makes it
16
+ * unbounded).
17
+ */
18
+ export declare function reservePdBackupDestination(targetName: string): string;
19
+ /**
20
+ * Move legacy PD backup directories out of the extensions dir into the
21
+ * backups root, so OpenClaw stops discovering them as duplicate plugins.
22
+ * Best-effort per entry (rc-9: failures are reported, never silent).
23
+ */
24
+ export declare function migrateLegacyExtensionBackups(): LegacyBackupMigrationResult;
@@ -0,0 +1,125 @@
1
+ /**
2
+ * PD plugin backup location utilities.
3
+ *
4
+ * Backups must NEVER live inside ~/.openclaw/extensions: OpenClaw's plugin
5
+ * discovery scans every child directory of the extensions root, so a backup
6
+ * containing package.json (openclaw.extensions) + dist/bundle.js is discovered
7
+ * as a SECOND "principles-disciple" plugin, producing
8
+ * "duplicate plugin id detected; global plugin will be overridden by global
9
+ * plugin" on every gateway startup.
10
+ *
11
+ * Backups therefore live in <openclawHome>/pd-backups, which is outside every
12
+ * plugin discovery root. Legacy backups that older PD versions left inside
13
+ * extensions/ (".pd-backup-<ts>" from the console updater,
14
+ * "principles-disciple.backup.<ms>" from the installer) are migrated out by
15
+ * migrateLegacyExtensionBackups().
16
+ */
17
+ import * as fs from 'fs';
18
+ import * as path from 'path';
19
+ import * as os from 'os';
20
+ /**
21
+ * Resolve the OpenClaw install home.
22
+ * Priority: OPENCLAW_HOME env var (explicit override) > ~/.openclaw
23
+ */
24
+ function resolveOpenClawHome() {
25
+ const envHome = process.env.OPENCLAW_HOME;
26
+ if (envHome && envHome.trim().length > 0)
27
+ return path.resolve(envHome);
28
+ return path.join(os.homedir(), '.openclaw');
29
+ }
30
+ /** Backups root — outside the extensions dir OpenClaw scans for plugins. */
31
+ export function resolvePdBackupsRoot() {
32
+ return path.join(resolveOpenClawHome(), 'pd-backups');
33
+ }
34
+ /**
35
+ * Extract a POSIX errno code from a thrown fs error (rc-2: type guard, no
36
+ * blind `as` cast on the error object).
37
+ */
38
+ function getErrorCode(err) {
39
+ if (err !== null && typeof err === 'object' && Object.hasOwn(err, 'code')) {
40
+ const { code } = err;
41
+ return typeof code === 'string' ? code : undefined;
42
+ }
43
+ return undefined;
44
+ }
45
+ /**
46
+ * Reserve a unique backup destination under the backups root:
47
+ * <root>/<targetName>-<ISO timestamp>, with a numeric suffix on collision.
48
+ * Atomic reservation via plain (non-recursive) mkdirSync — EEXIST means the
49
+ * name is taken, retry with the next suffix. The retry loop is bounded; never
50
+ * poll existsSync in a loop (a stale/mocked always-true predicate makes it
51
+ * unbounded).
52
+ */
53
+ export function reservePdBackupDestination(targetName) {
54
+ const root = resolvePdBackupsRoot();
55
+ fs.mkdirSync(root, { recursive: true });
56
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
57
+ let lastError;
58
+ for (let attempt = 0; attempt < 100; attempt++) {
59
+ const suffix = attempt === 0 ? '' : `-${attempt}`;
60
+ const candidate = path.join(root, `${targetName}-${timestamp}${suffix}`);
61
+ try {
62
+ fs.mkdirSync(candidate);
63
+ return candidate;
64
+ }
65
+ catch (err) {
66
+ if (getErrorCode(err) === 'EEXIST') {
67
+ lastError = err;
68
+ continue;
69
+ }
70
+ throw err;
71
+ }
72
+ }
73
+ throw new Error(`Could not reserve a unique backup directory under ${root}: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
74
+ }
75
+ /**
76
+ * Backup directory names older PD versions created as siblings of the plugin
77
+ * inside the extensions dir (both are re-discovered as duplicate plugins).
78
+ */
79
+ function isLegacyPdBackupName(name) {
80
+ // Console updater (<= 2026-08): ".pd-backup-<ISO ts>" sibling.
81
+ if (name.startsWith('.pd-backup-'))
82
+ return true;
83
+ // Installer (<= 2026-08): "<extDir>.backup.<epoch ms>" sibling.
84
+ return /^principles-disciple\.backup\.\d+$/.test(name);
85
+ }
86
+ /**
87
+ * Move legacy PD backup directories out of the extensions dir into the
88
+ * backups root, so OpenClaw stops discovering them as duplicate plugins.
89
+ * Best-effort per entry (rc-9: failures are reported, never silent).
90
+ */
91
+ export function migrateLegacyExtensionBackups() {
92
+ const result = { movedFrom: [], failed: [] };
93
+ const extensionsDir = path.join(resolveOpenClawHome(), 'extensions');
94
+ let entries;
95
+ try {
96
+ entries = fs.readdirSync(extensionsDir, { withFileTypes: true });
97
+ }
98
+ catch {
99
+ return result; // no extensions dir — nothing to migrate
100
+ }
101
+ const root = resolvePdBackupsRoot();
102
+ for (const entry of entries) {
103
+ if (!entry.isDirectory() || !isLegacyPdBackupName(entry.name))
104
+ continue;
105
+ const from = path.join(extensionsDir, entry.name);
106
+ let to = path.join(root, entry.name);
107
+ let suffix = 1;
108
+ while (fs.existsSync(to)) {
109
+ to = path.join(root, `${entry.name}-${suffix}`);
110
+ suffix += 1;
111
+ }
112
+ try {
113
+ fs.mkdirSync(root, { recursive: true });
114
+ fs.renameSync(from, to);
115
+ result.movedFrom.push(from);
116
+ }
117
+ catch (err) {
118
+ result.failed.push({
119
+ name: entry.name,
120
+ reason: err instanceof Error ? err.message : String(err),
121
+ });
122
+ }
123
+ }
124
+ return result;
125
+ }
@@ -2234,6 +2234,9 @@
2234
2234
  -moz-user-select: none;
2235
2235
  user-select: none;
2236
2236
  }
2237
+ .\[pd-console\:update\] {
2238
+ pd-console: update;
2239
+ }
2237
2240
  @media (hover: hover) {
2238
2241
  .group-hover\:opacity-100:is(:where(.group):hover *) {
2239
2242
  opacity: 100%;
@@ -328,6 +328,12 @@ const KNOWN_PLUGIN_CORE_FILES = new Set([
328
328
  // I/O shell: reads .pd/config.yaml, constructs PiAiRuntimeAdapter, writes trajectory.
329
329
  // Pure detection logic lives in @principles/core/runtime-v2/signal-collector/.
330
330
  'signal-collector-host.ts',
331
+ // PRI-530: Plugin I/O boundary — sync readonly state.db reads resolving principle
332
+ // receipt attribution (title fallback chain / approval date / source summary).
333
+ 'principle-receipt-metadata.ts',
334
+ // PRI-531: Plugin I/O boundary — durable receipt ledger writes (state.db
335
+ // principle_applications inserts + 90-day retention sweep).
336
+ 'principle-application-ledger.ts',
331
337
  'pain-lifecycle.ts',
332
338
  'session-tracker.ts',
333
339
  // PRI-459: now a thin re-export adapter over @principles/core/principle-tree-ledger.
@@ -429,7 +435,10 @@ describe('PRI-212 plugin core anti-growth guard', () => {
429
435
  // collection I/O host (correction + empathy upstream merge). Reads
430
436
  // .pd/config.yaml, constructs PiAiRuntimeAdapter, writes trajectory. Pure
431
437
  // detection logic lives in @principles/core/runtime-v2/signal-collector/.
432
- expect(KNOWN_PLUGIN_CORE_FILES.size).toBe(94);
438
+ // PRI-530: Added principle-receipt-metadata.ts (94 → 95) — plugin I/O boundary
439
+ // for principle receipt attribution reads (readonly state.db joins).
440
+ // PRI-531: Added principle-application-ledger.ts (95 → 96) — receipt ledger writer.
441
+ expect(KNOWN_PLUGIN_CORE_FILES.size).toBe(96);
433
442
  });
434
443
  });
435
444
  const REQUIRED_TEST_FILES = [