create-principles-disciple 1.134.0 → 1.134.2

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 (54) hide show
  1. package/codex-adapter/package.json +3 -2
  2. package/console/dist/server/routes/update.js +143 -27
  3. package/console/dist/server/utils/cli-smoke.d.ts +19 -0
  4. package/console/dist/server/utils/cli-smoke.js +60 -0
  5. package/console/dist/server/utils/installed-layout.d.ts +8 -0
  6. package/console/dist/server/utils/installed-layout.js +4 -0
  7. package/console/dist/ui/i18n/en.json +2 -2
  8. package/console/dist/ui/i18n/zh-CN.json +2 -2
  9. package/console/dist/web/assets/app.js +5 -5
  10. package/core/dist/runtime-v2/adapter/__tests__/pi-ai-runtime-adapter.test.js +157 -0
  11. package/core/dist/runtime-v2/adapter/__tests__/pi-ai-runtime-adapter.test.js.map +1 -1
  12. package/core/dist/runtime-v2/adapter/__tests__/structured-output-repair.test.js +9 -0
  13. package/core/dist/runtime-v2/adapter/__tests__/structured-output-repair.test.js.map +1 -1
  14. package/core/dist/runtime-v2/adapter/output-repair-contract.d.ts +10 -0
  15. package/core/dist/runtime-v2/adapter/output-repair-contract.d.ts.map +1 -1
  16. package/core/dist/runtime-v2/adapter/output-repair-contract.js.map +1 -1
  17. package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.d.ts.map +1 -1
  18. package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.js +53 -1
  19. package/core/dist/runtime-v2/adapter/pi-ai-runtime-adapter.js.map +1 -1
  20. package/core/dist/runtime-v2/adapter/structured-output-repair.d.ts +9 -1
  21. package/core/dist/runtime-v2/adapter/structured-output-repair.d.ts.map +1 -1
  22. package/core/dist/runtime-v2/adapter/structured-output-repair.js +8 -0
  23. package/core/dist/runtime-v2/adapter/structured-output-repair.js.map +1 -1
  24. package/core/dist/runtime-v2/runner/__tests__/base-peer-runner-failure-details.test.js +36 -0
  25. package/core/dist/runtime-v2/runner/__tests__/base-peer-runner-failure-details.test.js.map +1 -1
  26. package/core/package.json +1 -1
  27. package/dist/installer.d.ts +21 -0
  28. package/dist/installer.d.ts.map +1 -1
  29. package/dist/installer.js +85 -1
  30. package/dist/installer.js.map +1 -1
  31. package/dist/uninstaller.d.ts +8 -11
  32. package/dist/uninstaller.d.ts.map +1 -1
  33. package/dist/uninstaller.js +97 -19
  34. package/dist/uninstaller.js.map +1 -1
  35. package/host-runtime/package.json +1 -1
  36. package/install-layout/dist/index.d.ts +8 -0
  37. package/install-layout/dist/index.js +1 -0
  38. package/install-layout/package.json +1 -1
  39. package/package.json +1 -1
  40. package/pd-cli/dist/commands/runtime-internalization-run-once.d.ts.map +1 -1
  41. package/pd-cli/dist/commands/runtime-internalization-run-once.js +16 -2
  42. package/pd-cli/dist/commands/runtime-internalization-run-once.js.map +1 -1
  43. package/pd-cli/package.json +1 -1
  44. package/plugin/dist/bundle.js +186 -186
  45. package/plugin/dist/rulehost-evidence.js +77 -77
  46. package/release-manager/dist/installer.d.ts +21 -0
  47. package/release-manager/dist/installer.d.ts.map +1 -1
  48. package/release-manager/dist/installer.js +85 -1
  49. package/release-manager/dist/installer.js.map +1 -1
  50. package/release-manager/dist/uninstaller.d.ts +8 -11
  51. package/release-manager/dist/uninstaller.d.ts.map +1 -1
  52. package/release-manager/dist/uninstaller.js +97 -19
  53. package/release-manager/dist/uninstaller.js.map +1 -1
  54. package/release-manager/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principles/codex-adapter",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Codex CLI host adapter for Principles Disciple — implements HostAdapter interface for OpenAI Codex CLI's stdin/stdout JSON hook model (ADR-0020).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -32,7 +32,8 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@principles/core": "file:../core",
35
- "@principles/host-runtime": "file:../host-runtime"
35
+ "@principles/host-runtime": "file:../host-runtime",
36
+ "@principles/install-layout": "file:../install-layout"
36
37
  },
37
38
  "devDependencies": {
38
39
  "@types/node": "^26.4.1",
@@ -10,6 +10,7 @@ import { migrateLegacyExtensionBackups, reservePdBackupDestination, resolvePdBac
10
10
  import { resolveExtensionsDir, resolveUpdateLayout, resolvePluginDir, resolveCanonicalRuntimeRoot, readCurrentVersion, } from '../utils/installed-layout.js';
11
11
  import { ActivationCompatibilityReadModel, isFeatureEnabled } from '@principles/core/runtime-v2';
12
12
  import { collectFileDepLinkSpecs } from '../utils/update-links.js';
13
+ import { runPostUpdateCliSmoke } from '../utils/cli-smoke.js';
13
14
  import { updateMutationController, LEGACY_MUTATION_AUTHORITY, RELEASE_MANAGER_AUTHORITY, MUTATION_KINDS, } from '../update/mutation-controller.js';
14
15
  import { runLegacyJournaledMutation } from '../update/legacy-mutation-journal.js';
15
16
  import { loadPdConfig, computeFlagsFromLoadResult } from '../config/pd-config-store.js';
@@ -49,6 +50,10 @@ const WORKSPACE_FILES = ['AGENTS.md', 'SOUL.md', 'USER.md', 'CLAUDE.md'];
49
50
  // .node addons locked by the gateway/console processes (EPERM on copyfile),
50
51
  // npm symlinks/junctions, and thousands of regenerable files.
51
52
  const SKIP_DIRS = new Set(['node_modules']);
53
+ // Full-update backups extend the existing plugin backup with sibling code
54
+ // trees. Dependency directories stay in place (native modules may be loaded).
55
+ const RUNTIME_BACKUP_DIR = '.runtime-components';
56
+ const BACKUP_SKIP_DIRS = new Set(['node_modules', RUNTIME_BACKUP_DIR]);
52
57
  // ---------------------------------------------------------------------------
53
58
  // Helpers
54
59
  // ---------------------------------------------------------------------------
@@ -223,6 +228,58 @@ function copyFileTo(src, dest) {
223
228
  fs.mkdirSync(dir, { recursive: true });
224
229
  fs.copyFileSync(src, dest);
225
230
  }
231
+ function runtimeComponents(layout) {
232
+ return {
233
+ console: layout.consoleDir,
234
+ 'pd-cli': layout.pdCliDir,
235
+ 'host-runtime': layout.hostRuntimeDir,
236
+ 'install-layout': layout.installLayoutDir,
237
+ core: layout.coreDir,
238
+ plugin: layout.pluginDir,
239
+ 'release-manager': layout.releaseManagerDir,
240
+ ...(layout.codexAdapterDir ? { 'codex-adapter': layout.codexAdapterDir } : {}),
241
+ };
242
+ }
243
+ /** Resolve a backup entry under the backup root, refusing escapes — the
244
+ * component names recorded in components.json are untrusted runtime data
245
+ * (rc-1/rc-2), so every join into the backup tree is boundary-checked. */
246
+ function backupEntryPath(root, name) {
247
+ const target = path.resolve(root, name);
248
+ if (target === root || !target.startsWith(root + path.sep)) {
249
+ throw new Error(`Runtime backup entry "${name}" escapes the backup root`);
250
+ }
251
+ return target;
252
+ }
253
+ function backupRuntimeComponents(layout, backupDir) {
254
+ const root = path.join(backupDir, RUNTIME_BACKUP_DIR);
255
+ fs.mkdirSync(root, { recursive: true });
256
+ const present = [];
257
+ for (const [name, dir] of Object.entries(runtimeComponents(layout))) {
258
+ if (name === 'plugin' || !fs.existsSync(dir))
259
+ continue;
260
+ copyDirRecursive(dir, backupEntryPath(root, name), BACKUP_SKIP_DIRS);
261
+ present.push(name);
262
+ }
263
+ // Written last: a partial backup must never be accepted as recoverable.
264
+ fs.writeFileSync(path.join(root, 'components.json'), JSON.stringify(present));
265
+ }
266
+ function restoreRuntimeComponents(layout, backupDir) {
267
+ const root = path.join(backupDir, RUNTIME_BACKUP_DIR);
268
+ if (!fs.existsSync(root))
269
+ return false; // Historical plugin-only backup.
270
+ const present = JSON.parse(fs.readFileSync(path.join(root, 'components.json'), 'utf8'));
271
+ const components = runtimeComponents(layout);
272
+ if (!Array.isArray(present) || present.some((name) => typeof name !== 'string' || name === 'plugin' || !Object.hasOwn(components, name)
273
+ || !fs.existsSync(backupEntryPath(root, name)))) {
274
+ throw new Error('Runtime backup is incomplete or incompatible with this installation');
275
+ }
276
+ for (const [name, dir] of Object.entries(components)) {
277
+ if (name === 'plugin' || !present.includes(name))
278
+ continue;
279
+ copyDirRecursive(backupEntryPath(root, name), dir, BACKUP_SKIP_DIRS);
280
+ }
281
+ return true;
282
+ }
226
283
  // --- Plugin skill-language preservation (PR #1332 companion) -----------------
227
284
  // OpenClaw publishes plugin skills by directory name (first declaration
228
285
  // wins, same-name roots only warn — no locale mechanism), so a manifest must
@@ -749,7 +806,11 @@ async function doRollbackUpdate(options, workspaceDir) {
749
806
  // dependencies. Instead, overwrite from backup: modified files (dist/,
750
807
  // package.json, etc.) get the backup version, while node_modules/console/
751
808
  // core/ (not in the backup) are left untouched.
752
- copyDirRecursive(backupDir, targetDir);
809
+ const layout = resolveUpdateLayout();
810
+ const fullBackup = layout && path.resolve(layout.pluginDir) === path.resolve(targetDir)
811
+ ? restoreRuntimeComponents(layout, backupDir)
812
+ : false;
813
+ copyDirRecursive(backupDir, targetDir, BACKUP_SKIP_DIRS);
753
814
  // Keep the OpenClaw extension copy in sync with the restored canonical
754
815
  // plugin (CP-5 invariant from the 2026-09-05 investigation): OpenClaw
755
816
  // loads ~/.openclaw/extensions/principles-disciple, so a rollback that
@@ -759,9 +820,29 @@ async function doRollbackUpdate(options, workspaceDir) {
759
820
  const openClawPluginCopy = path.join(resolveExtensionsDir(), 'principles-disciple');
760
821
  let extCopySynced = false;
761
822
  if (path.resolve(openClawPluginCopy) !== path.resolve(targetDir) && fs.existsSync(openClawPluginCopy)) {
762
- copyDirRecursive(backupDir, openClawPluginCopy);
823
+ copyDirRecursive(backupDir, openClawPluginCopy, BACKUP_SKIP_DIRS);
763
824
  extCopySynced = true;
764
825
  }
826
+ if (fullBackup && layout) {
827
+ const smoke = runPostUpdateCliSmoke(layout.pdCliDir);
828
+ if (!smoke.ok) {
829
+ // rc-9: a failed rollback must be observable in history, like every
830
+ // other failure path — not just in the HTTP response.
831
+ appendUpdateHistory(workspaceDir, {
832
+ fromVersion: 'rolled-back',
833
+ toVersion: readCurrentVersion(targetDir) ?? 'unknown',
834
+ success: false,
835
+ kind: 'failure',
836
+ backupPath: backupDir,
837
+ });
838
+ return {
839
+ success: false,
840
+ message: `Runtime backup restored but pd CLI still fails to start: ${smoke.error}`,
841
+ reason: 'rollback_cli_smoke_failed',
842
+ nextAction: 'Keep the backup and use the official installer pinned to the previous installer release to repair dependencies before restarting Console.',
843
+ };
844
+ }
845
+ }
765
846
  // Record rollback history
766
847
  appendUpdateHistory(workspaceDir, {
767
848
  fromVersion: 'rolled-back',
@@ -772,9 +853,11 @@ async function doRollbackUpdate(options, workspaceDir) {
772
853
  });
773
854
  return {
774
855
  success: true,
775
- message: extCopySynced
776
- ? 'Rollback completed successfully (OpenClaw extension copy restored too). Note: only the plugin was rolled back; console/core/host-runtime/pd-cli keep the upgraded version. For a full step-back, re-run the official installer pinned to the older release.'
777
- : 'Rollback completed successfully. Note: only the plugin was rolled back; console/core/host-runtime/pd-cli keep the upgraded version. For a full step-back, re-run the official installer pinned to the older release.',
856
+ message: fullBackup
857
+ ? 'Runtime components restored and pd CLI startup verified. Restart Console to load the restored code.'
858
+ : extCopySynced
859
+ ? 'Rollback completed successfully (OpenClaw extension copy restored too). Note: only the plugin was rolled back; console/core/host-runtime/pd-cli keep the upgraded version. For a full step-back, re-run the official installer pinned to the older release.'
860
+ : 'Rollback completed successfully. Note: only the plugin was rolled back; console/core/host-runtime/pd-cli keep the upgraded version. For a full step-back, re-run the official installer pinned to the older release.',
778
861
  };
779
862
  }
780
863
  catch (error) {
@@ -998,19 +1081,9 @@ function ensureRuntimeResolutionLinks(layout, tempDir) {
998
1081
  ];
999
1082
  // Data-driven pass: derive links from the STAGED component manifests (see
1000
1083
  // the comment above — staged, never the deployed pre-update manifests).
1001
- const stagedComponents = [
1002
- { manifestDir: path.join(tempDir, 'console'), deployedDir: layout.consoleDir },
1003
- { manifestDir: path.join(tempDir, 'pd-cli'), deployedDir: layout.pdCliDir },
1004
- { manifestDir: path.join(tempDir, 'host-runtime'), deployedDir: layout.hostRuntimeDir },
1005
- { manifestDir: path.join(tempDir, 'install-layout'), deployedDir: layout.installLayoutDir },
1006
- { manifestDir: path.join(tempDir, 'core'), deployedDir: layout.coreDir },
1007
- { manifestDir: path.join(tempDir, 'plugin'), deployedDir: layout.pluginDir },
1008
- // PRI-672: staged release-manager component (npm name
1009
- // create-principles-disciple). Its staged manifest carries the
1010
- // file:../install-layout ref, and the staged console's manifest carries
1011
- // file:../release-manager — both derive their resolution links here.
1012
- { manifestDir: path.join(tempDir, 'release-manager'), deployedDir: layout.releaseManagerDir },
1013
- ];
1084
+ const stagedComponents = Object.entries(runtimeComponents(layout)).map(([name, deployedDir]) => ({
1085
+ manifestDir: path.join(tempDir, name), deployedDir,
1086
+ }));
1014
1087
  const readStagedDependencies = (manifestDir) => {
1015
1088
  try {
1016
1089
  const pkgPath = path.join(manifestDir, 'package.json');
@@ -1263,9 +1336,8 @@ async function doInlineFullUpdate(workspaceDir) {
1263
1336
  // 2026-09-05 install/upgrade investigation). The backup excludes
1264
1337
  // node_modules (same SKIP_DIRS contract as /apply), and the backup dir
1265
1338
  // is recorded in update-history (success AND failure) so the rollback
1266
- // handler can find it. Console self-update owns the console dir backup;
1267
- // core/host-runtime/pd-cli are plain dist overlays re-fetched on any
1268
- // next update, so only the version authority (plugin) is backed up.
1339
+ // handler can find it. Include every sibling code tree: a plugin-only
1340
+ // rollback cannot repair a CLI/adapter import failure after an update.
1269
1341
  // R5 (review): reservePdBackupDestination creates the directory BEFORE
1270
1342
  // the copy — a mid-copy failure must not leave backupPath pointing at a
1271
1343
  // partial backup (a later /rollback would happily restore the fragment).
@@ -1276,6 +1348,7 @@ async function doInlineFullUpdate(workspaceDir) {
1276
1348
  const backupDest = reservePdBackupDestination(path.basename(extDir));
1277
1349
  try {
1278
1350
  copyDirRecursive(extDir, backupDest, SKIP_DIRS);
1351
+ backupRuntimeComponents(layout, backupDest);
1279
1352
  }
1280
1353
  catch (backupError) {
1281
1354
  try {
@@ -1307,6 +1380,23 @@ async function doInlineFullUpdate(workspaceDir) {
1307
1380
  fs.existsSync(path.join(releaseManagerSrc, 'dist'))) {
1308
1381
  copyDirRecursive(releaseManagerSrc, layout.releaseManagerDir, SKIP_DIRS);
1309
1382
  }
1383
+ // PRI-711: same PRI-561 ordering for codex-adapter — pd-cli's eager
1384
+ // import graph statically resolves it, so an updated pd-cli paired with
1385
+ // a stale deployed adapter is exactly the generation drift that bricked
1386
+ // the 1.231.2 update (health-codex imported symbols the deployed
1387
+ // adapter happened to carry; the next API addition would not). The
1388
+ // copy keeps the deployed node_modules (SKIP_DIRS), so its resolution
1389
+ // links survive; the data-driven pass below re-derives them anyway.
1390
+ // Skipped when the running console resolves a one-generation-old
1391
+ // install-layout without codexAdapterDir — that generation also lacks
1392
+ // the derivation entry, mirroring the old no-op behavior (rc-9).
1393
+ const codexAdapterDest = layout.codexAdapterDir;
1394
+ if (codexAdapterDest !== undefined &&
1395
+ fs.existsSync(path.join(tempDir, 'codex-adapter')) &&
1396
+ fs.existsSync(path.join(tempDir, 'codex-adapter', 'package.json')) &&
1397
+ fs.existsSync(path.join(tempDir, 'codex-adapter', 'dist'))) {
1398
+ copyDirRecursive(path.join(tempDir, 'codex-adapter'), codexAdapterDest, SKIP_DIRS);
1399
+ }
1310
1400
  const { error: linkError, quarantined } = ensureRuntimeResolutionLinks(layout, tempDir);
1311
1401
  reconciledQuarantine = quarantined;
1312
1402
  if (linkError) {
@@ -1383,12 +1473,32 @@ async function doInlineFullUpdate(workspaceDir) {
1383
1473
  if (tempDir && fs.existsSync(tempDir)) {
1384
1474
  fs.rmSync(tempDir, { recursive: true, force: true });
1385
1475
  }
1386
- // 6.5 Discard the quarantined stale dependency copies — the canonical
1387
- // components are in place now (PRI-665). Both the success return and the
1388
- // version-drift failure return flow through here with files already
1389
- // swapped, so restoring the stale copies would re-break resolution.
1390
- if (reconciledQuarantine.length > 0) {
1391
- cleanupQuarantined(reconciledQuarantine);
1476
+ // 6.8 PRI-711: post-apply CLI smoke. The swap and link steps above are
1477
+ // data-driven but not self-proving — run the updated CLI once and refuse
1478
+ // to record a success if the eager import graph is broken (see
1479
+ // runPostUpdateCliSmoke for the three precedents this catches). The
1480
+ // backup recorded below keeps the failure recoverable via /rollback.
1481
+ const smoke = runPostUpdateCliSmoke(layout.pdCliDir);
1482
+ if (!smoke.ok) {
1483
+ restoreQuarantined(reconciledQuarantine);
1484
+ reconciledQuarantine = [];
1485
+ const failedVersion = readCurrentVersion(extDir) ?? toVersion ?? 'unknown';
1486
+ appendUpdateHistory(workspaceDir, {
1487
+ fromVersion,
1488
+ toVersion: failedVersion,
1489
+ success: false,
1490
+ kind: 'failure',
1491
+ ...(pluginBackupDir ? { backupPath: pluginBackupDir } : {}),
1492
+ });
1493
+ return {
1494
+ success: false,
1495
+ message: `Update applied but the updated pd CLI fails to start: ${smoke.error}`,
1496
+ reason: 'post_update_cli_smoke_failed',
1497
+ nextAction: pluginBackupDir
1498
+ ? 'Restore the backup recorded in update history (backupPath), then report the stderr above. The console must be restarted before further use.'
1499
+ : 'Report the stderr above; no backup was reserved for this update. The console must be restarted before further use.',
1500
+ requiresRestart: false,
1501
+ };
1392
1502
  }
1393
1503
  // 7. Version-advance check (drift guard). The full update installs the
1394
1504
  // plugin bundled inside the installer. If the installer is stale (its
@@ -1400,6 +1510,8 @@ async function doInlineFullUpdate(workspaceDir) {
1400
1510
  const newVersion = readCurrentVersion(extDir) ?? toVersion ?? 'unknown';
1401
1511
  const installedExpectedRelease = newVersion === stagedVersion;
1402
1512
  if (!installedExpectedRelease) {
1513
+ restoreQuarantined(reconciledQuarantine);
1514
+ reconciledQuarantine = [];
1403
1515
  // Files were already rewritten to the same (or lower) version. Record a
1404
1516
  // FAILED history entry so the operator sees why, and return a structured
1405
1517
  // error with nextAction.
@@ -1426,6 +1538,10 @@ async function doInlineFullUpdate(workspaceDir) {
1426
1538
  kind: 'update',
1427
1539
  ...(pluginBackupDir ? { backupPath: pluginBackupDir } : {}),
1428
1540
  });
1541
+ // The quarantined pre-update copies are discarded on success; no reset of
1542
+ // the variable is needed here — the return below cannot re-enter the
1543
+ // catch (CodeQL js/useless-assignment-to-local, PR #1580 review).
1544
+ cleanupQuarantined(reconciledQuarantine);
1429
1545
  return {
1430
1546
  success: true,
1431
1547
  message: depsChanged
@@ -0,0 +1,19 @@
1
+ export type CliSmokeResult = {
2
+ ok: true;
3
+ version: string;
4
+ } | {
5
+ ok: false;
6
+ error: string;
7
+ };
8
+ /**
9
+ * Spawn the pd CLI entry and require exit 0.
10
+ *
11
+ * Resolves `node` from PATH — the same resolution the installed bin/pd.cmd
12
+ * shim already depends on, so any environment that can run `pd` at all can
13
+ * run this probe. The entry path is boundary-checked to stay inside the
14
+ * pd-cli component dir (same guard shape as the legacy-rule preflight's
15
+ * subprocess target), and the subprocess is invoked with an argv array —
16
+ * never a shell string (ERR-045). Bounded stderr extract on failure (rc-8);
17
+ * never throws.
18
+ */
19
+ export declare function runPostUpdateCliSmoke(pdCliDir: string): CliSmokeResult;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * PRI-711: post-apply CLI smoke for the full-update pipeline.
3
+ *
4
+ * The full update swaps component dists without running npm install, and
5
+ * resolution gaps of one class have now shipped three times — each discovered
6
+ * only as a bricked CLI at the next console launch (PRI-561 host-runtime
7
+ * links, the 2026-08-29 install-layout component gap, PRI-711 codex-adapter).
8
+ * Spawning the pd CLI once with `--version` loads its entire eager import
9
+ * graph (health-codex → @principles/codex-adapter →
10
+ * @principles/host-runtime → @principles/core) and exits without touching
11
+ * workspace state, so a gap anywhere in that graph surfaces before the update
12
+ * is recorded as a success.
13
+ */
14
+ import { execFileSync } from 'child_process';
15
+ import * as fs from 'fs';
16
+ import * as path from 'path';
17
+ /**
18
+ * Spawn the pd CLI entry and require exit 0.
19
+ *
20
+ * Resolves `node` from PATH — the same resolution the installed bin/pd.cmd
21
+ * shim already depends on, so any environment that can run `pd` at all can
22
+ * run this probe. The entry path is boundary-checked to stay inside the
23
+ * pd-cli component dir (same guard shape as the legacy-rule preflight's
24
+ * subprocess target), and the subprocess is invoked with an argv array —
25
+ * never a shell string (ERR-045). Bounded stderr extract on failure (rc-8);
26
+ * never throws.
27
+ */
28
+ export function runPostUpdateCliSmoke(pdCliDir) {
29
+ const resolvedRoot = path.resolve(pdCliDir);
30
+ const pdCliEntry = path.join(resolvedRoot, 'dist', 'index.js');
31
+ if (!pdCliEntry.startsWith(resolvedRoot + path.sep)) {
32
+ return { ok: false, error: `pd CLI entry ${pdCliEntry} is outside the pd-cli component dir ${resolvedRoot}` };
33
+ }
34
+ if (!fs.existsSync(pdCliEntry)) {
35
+ return { ok: false, error: `pd CLI entry not found at ${pdCliEntry}` };
36
+ }
37
+ try {
38
+ const stdout = execFileSync('node', [pdCliEntry, '--version'], {
39
+ timeout: 60_000,
40
+ encoding: 'utf8',
41
+ windowsHide: true,
42
+ stdio: ['ignore', 'pipe', 'pipe'],
43
+ });
44
+ const version = (stdout.trim().split(/\r?\n/, 1)[0] ?? '').trim();
45
+ return { ok: true, version };
46
+ }
47
+ catch (error) {
48
+ // rc-1/rc-2: the child failure shape is unknown — guard every read.
49
+ // Node prints the error MESSAGE first and stack frames after, so the
50
+ // operator-facing extract is the bounded HEAD of stderr (a tail would
51
+ // carry only stack noise — observed in the PRI-711 test fixture).
52
+ let reason = error instanceof Error ? error.message : String(error);
53
+ if (typeof error === 'object' && error !== null && Object.hasOwn(error, 'stderr')) {
54
+ const { stderr } = error;
55
+ if (typeof stderr === 'string' && stderr.trim().length > 0)
56
+ reason = stderr;
57
+ }
58
+ return { ok: false, error: reason.trim().slice(0, 400) };
59
+ }
60
+ }
@@ -8,6 +8,14 @@ export interface UpdateLayout {
8
8
  pdCliDir: string;
9
9
  installLayoutDir: string;
10
10
  releaseManagerDir: string;
11
+ /**
12
+ * PRI-711: codex-adapter is a runtime-layout component pd-cli resolves
13
+ * eagerly. Undefined when the DEPLOYED @principles/install-layout is one
14
+ * generation old (this update runs inside the currently-running console,
15
+ * which resolves the layout helper installed by the PREVIOUS update) —
16
+ * consumers must degrade to skipping the adapter, never assume it.
17
+ */
18
+ codexAdapterDir: string | undefined;
11
19
  hosts: InstallHost[];
12
20
  }
13
21
  export declare function resolveUpdateLayout(): UpdateLayout | undefined;
@@ -46,6 +46,9 @@ export function resolveUpdateLayout() {
46
46
  pdCliDir: paths.pdCliDir,
47
47
  installLayoutDir: paths.installLayoutDir,
48
48
  releaseManagerDir: paths.releaseManagerDir,
49
+ // rc-1: the deployed install-layout may predate the codexAdapterDir
50
+ // field — guard instead of trusting the shape.
51
+ codexAdapterDir: typeof paths.codexAdapterDir === 'string' ? paths.codexAdapterDir : undefined,
49
52
  hosts: resolution.manifest?.hosts ?? [],
50
53
  };
51
54
  }
@@ -57,6 +60,7 @@ export function resolveUpdateLayout() {
57
60
  pdCliDir: path.join(legacyPluginDir, 'pd-cli'),
58
61
  installLayoutDir: path.join(legacyPluginDir, 'install-layout'),
59
62
  releaseManagerDir: path.join(legacyPluginDir, 'release-manager'),
63
+ codexAdapterDir: path.join(legacyPluginDir, 'codex-adapter'),
60
64
  hosts: ['openclaw'],
61
65
  };
62
66
  }
@@ -1355,8 +1355,8 @@
1355
1355
  "filesUpdated": "Updated {{count}} files",
1356
1356
  "networkErrorHint": "Please check your network connection and try again.",
1357
1357
  "retry": "Retry",
1358
- "codexWarning": "Codex Host detected. This update only covers the OpenClaw plugin code and does not update the Codex adapter (@principles/codex-adapter). For a full update, run the installer.",
1359
- "partialUpdateHint": "Plugin code updated. To update the full runtime (including the Codex adapter and dependencies), run: npx create-principles-disciple",
1358
+ "codexWarning": "Codex Host detected. The full update also refreshes the Codex adapter (@principles/codex-adapter) in the runtime layout. To re-register Codex hooks or repair the Codex host install, run the installer.",
1359
+ "partialUpdateHint": "Plugin code updated. If components ever look out of sync, refresh the full runtime with: npx create-principles-disciple",
1360
1360
  "applyFullUpdate": "Full Update",
1361
1361
  "fullUpdating": "Full update in progress…",
1362
1362
  "fullUpdateSuccess": "Full update completed.",
@@ -1355,8 +1355,8 @@
1355
1355
  "filesUpdated": "更新了 {{count}} 个文件",
1356
1356
  "networkErrorHint": "请检查网络连接后重试。",
1357
1357
  "retry": "重试",
1358
- "codexWarning": "检测到 Codex Host 已安装。此处的更新仅覆盖 OpenClaw 插件代码,不会更新 Codex 适配器(@principles/codex-adapter)。如需完整更新,请运行安装器。",
1359
- "partialUpdateHint": "插件代码已更新。如需更新完整运行时(含 Codex 适配器和依赖),请运行:npx create-principles-disciple",
1358
+ "codexWarning": "检测到 Codex Host 已安装。全量更新现在会同步刷新运行时布局中的 Codex 适配器(@principles/codex-adapter)。如需重新注册 Codex hooks 或修复 Codex 宿主安装,请运行安装器。",
1359
+ "partialUpdateHint": "插件代码已更新。如发现组件版本不同步,请运行以下命令刷新完整运行时:npx create-principles-disciple",
1360
1360
  "applyFullUpdate": "全量更新",
1361
1361
  "fullUpdating": "全量更新中…",
1362
1362
  "fullUpdateSuccess": "全量更新完成。",