create-principles-disciple 1.133.13 → 1.134.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.
Files changed (71) hide show
  1. package/codex-adapter/package.json +3 -2
  2. package/console/dist/server/routes/update.js +181 -34
  3. package/console/dist/server/update/legacy-mutation-journal.d.ts +118 -0
  4. package/console/dist/server/update/legacy-mutation-journal.js +220 -0
  5. package/console/dist/server/utils/cli-smoke.d.ts +19 -0
  6. package/console/dist/server/utils/cli-smoke.js +60 -0
  7. package/console/dist/server/utils/installed-layout.d.ts +8 -0
  8. package/console/dist/server/utils/installed-layout.js +4 -0
  9. package/console/dist/ui/i18n/en.json +2 -2
  10. package/console/dist/ui/i18n/zh-CN.json +2 -2
  11. package/console/dist/web/assets/app.js +85 -15
  12. package/console/package.json +2 -2
  13. package/core/dist/runtime-v2/__tests__/recovery-sweep-service.test.js +130 -0
  14. package/core/dist/runtime-v2/__tests__/recovery-sweep-service.test.js.map +1 -1
  15. package/core/dist/runtime-v2/recovery-sweep-service.d.ts +9 -0
  16. package/core/dist/runtime-v2/recovery-sweep-service.d.ts.map +1 -1
  17. package/core/dist/runtime-v2/recovery-sweep-service.js +7 -2
  18. package/core/dist/runtime-v2/recovery-sweep-service.js.map +1 -1
  19. package/core/package.json +1 -1
  20. package/dist/installer.d.ts +69 -0
  21. package/dist/installer.d.ts.map +1 -1
  22. package/dist/installer.js +200 -14
  23. package/dist/installer.js.map +1 -1
  24. package/dist/uninstaller.d.ts.map +1 -1
  25. package/dist/uninstaller.js +7 -3
  26. package/dist/uninstaller.js.map +1 -1
  27. package/dist/update/install-layout.d.ts +39 -2
  28. package/dist/update/install-layout.d.ts.map +1 -1
  29. package/dist/update/install-layout.js +69 -3
  30. package/dist/update/install-layout.js.map +1 -1
  31. package/dist/update/release-manager-authority.d.ts +8 -0
  32. package/dist/update/release-manager-authority.d.ts.map +1 -1
  33. package/dist/update/release-manager-authority.js +26 -4
  34. package/dist/update/release-manager-authority.js.map +1 -1
  35. package/dist/update/release-metadata-source.d.ts +85 -0
  36. package/dist/update/release-metadata-source.d.ts.map +1 -0
  37. package/dist/update/release-metadata-source.js +102 -0
  38. package/dist/update/release-metadata-source.js.map +1 -0
  39. package/install-layout/dist/index.d.ts +8 -0
  40. package/install-layout/dist/index.js +1 -0
  41. package/install-layout/package.json +1 -1
  42. package/package.json +2 -2
  43. package/pd-cli/dist/commands/runtime-recovery-failed-tasks.d.ts.map +1 -1
  44. package/pd-cli/dist/commands/runtime-recovery-failed-tasks.js +82 -6
  45. package/pd-cli/dist/commands/runtime-recovery-failed-tasks.js.map +1 -1
  46. package/pd-cli/dist/index.js +6 -3
  47. package/pd-cli/dist/index.js.map +1 -1
  48. package/pd-cli/package.json +1 -1
  49. package/plugin/dist/bundle.js +44 -44
  50. package/plugin/dist/governance-audit.js +139 -139
  51. package/plugin/dist/rulehost-evidence.js +144 -144
  52. package/release-manager/dist/installer.d.ts +69 -0
  53. package/release-manager/dist/installer.d.ts.map +1 -1
  54. package/release-manager/dist/installer.js +200 -14
  55. package/release-manager/dist/installer.js.map +1 -1
  56. package/release-manager/dist/uninstaller.d.ts.map +1 -1
  57. package/release-manager/dist/uninstaller.js +7 -3
  58. package/release-manager/dist/uninstaller.js.map +1 -1
  59. package/release-manager/dist/update/install-layout.d.ts +39 -2
  60. package/release-manager/dist/update/install-layout.d.ts.map +1 -1
  61. package/release-manager/dist/update/install-layout.js +69 -3
  62. package/release-manager/dist/update/install-layout.js.map +1 -1
  63. package/release-manager/dist/update/release-manager-authority.d.ts +8 -0
  64. package/release-manager/dist/update/release-manager-authority.d.ts.map +1 -1
  65. package/release-manager/dist/update/release-manager-authority.js +26 -4
  66. package/release-manager/dist/update/release-manager-authority.js.map +1 -1
  67. package/release-manager/dist/update/release-metadata-source.d.ts +85 -0
  68. package/release-manager/dist/update/release-metadata-source.d.ts.map +1 -0
  69. package/release-manager/dist/update/release-metadata-source.js +102 -0
  70. package/release-manager/dist/update/release-metadata-source.js.map +1 -0
  71. package/release-manager/package.json +2 -2
@@ -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,7 +10,9 @@ 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';
15
+ import { runLegacyJournaledMutation } from '../update/legacy-mutation-journal.js';
14
16
  import { loadPdConfig, computeFlagsFromLoadResult } from '../config/pd-config-store.js';
15
17
  /**
16
18
  * Legacy rule contract preflight (2026-08-19): refuse to swap the runtime
@@ -48,6 +50,10 @@ const WORKSPACE_FILES = ['AGENTS.md', 'SOUL.md', 'USER.md', 'CLAUDE.md'];
48
50
  // .node addons locked by the gateway/console processes (EPERM on copyfile),
49
51
  // npm symlinks/junctions, and thousands of regenerable files.
50
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]);
51
57
  // ---------------------------------------------------------------------------
52
58
  // Helpers
53
59
  // ---------------------------------------------------------------------------
@@ -222,6 +228,58 @@ function copyFileTo(src, dest) {
222
228
  fs.mkdirSync(dir, { recursive: true });
223
229
  fs.copyFileSync(src, dest);
224
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
+ }
225
283
  // --- Plugin skill-language preservation (PR #1332 companion) -----------------
226
284
  // OpenClaw publishes plugin skills by directory name (first declaration
227
285
  // wins, same-name roots only warn — no locale mechanism), so a manifest must
@@ -748,7 +806,11 @@ async function doRollbackUpdate(options, workspaceDir) {
748
806
  // dependencies. Instead, overwrite from backup: modified files (dist/,
749
807
  // package.json, etc.) get the backup version, while node_modules/console/
750
808
  // core/ (not in the backup) are left untouched.
751
- 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);
752
814
  // Keep the OpenClaw extension copy in sync with the restored canonical
753
815
  // plugin (CP-5 invariant from the 2026-09-05 investigation): OpenClaw
754
816
  // loads ~/.openclaw/extensions/principles-disciple, so a rollback that
@@ -758,9 +820,29 @@ async function doRollbackUpdate(options, workspaceDir) {
758
820
  const openClawPluginCopy = path.join(resolveExtensionsDir(), 'principles-disciple');
759
821
  let extCopySynced = false;
760
822
  if (path.resolve(openClawPluginCopy) !== path.resolve(targetDir) && fs.existsSync(openClawPluginCopy)) {
761
- copyDirRecursive(backupDir, openClawPluginCopy);
823
+ copyDirRecursive(backupDir, openClawPluginCopy, BACKUP_SKIP_DIRS);
762
824
  extCopySynced = true;
763
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
+ }
764
846
  // Record rollback history
765
847
  appendUpdateHistory(workspaceDir, {
766
848
  fromVersion: 'rolled-back',
@@ -771,9 +853,11 @@ async function doRollbackUpdate(options, workspaceDir) {
771
853
  });
772
854
  return {
773
855
  success: true,
774
- message: extCopySynced
775
- ? '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.'
776
- : '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.',
777
861
  };
778
862
  }
779
863
  catch (error) {
@@ -997,19 +1081,9 @@ function ensureRuntimeResolutionLinks(layout, tempDir) {
997
1081
  ];
998
1082
  // Data-driven pass: derive links from the STAGED component manifests (see
999
1083
  // the comment above — staged, never the deployed pre-update manifests).
1000
- const stagedComponents = [
1001
- { manifestDir: path.join(tempDir, 'console'), deployedDir: layout.consoleDir },
1002
- { manifestDir: path.join(tempDir, 'pd-cli'), deployedDir: layout.pdCliDir },
1003
- { manifestDir: path.join(tempDir, 'host-runtime'), deployedDir: layout.hostRuntimeDir },
1004
- { manifestDir: path.join(tempDir, 'install-layout'), deployedDir: layout.installLayoutDir },
1005
- { manifestDir: path.join(tempDir, 'core'), deployedDir: layout.coreDir },
1006
- { manifestDir: path.join(tempDir, 'plugin'), deployedDir: layout.pluginDir },
1007
- // PRI-672: staged release-manager component (npm name
1008
- // create-principles-disciple). Its staged manifest carries the
1009
- // file:../install-layout ref, and the staged console's manifest carries
1010
- // file:../release-manager — both derive their resolution links here.
1011
- { manifestDir: path.join(tempDir, 'release-manager'), deployedDir: layout.releaseManagerDir },
1012
- ];
1084
+ const stagedComponents = Object.entries(runtimeComponents(layout)).map(([name, deployedDir]) => ({
1085
+ manifestDir: path.join(tempDir, name), deployedDir,
1086
+ }));
1013
1087
  const readStagedDependencies = (manifestDir) => {
1014
1088
  try {
1015
1089
  const pkgPath = path.join(manifestDir, 'package.json');
@@ -1262,9 +1336,8 @@ async function doInlineFullUpdate(workspaceDir) {
1262
1336
  // 2026-09-05 install/upgrade investigation). The backup excludes
1263
1337
  // node_modules (same SKIP_DIRS contract as /apply), and the backup dir
1264
1338
  // is recorded in update-history (success AND failure) so the rollback
1265
- // handler can find it. Console self-update owns the console dir backup;
1266
- // core/host-runtime/pd-cli are plain dist overlays re-fetched on any
1267
- // 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.
1268
1341
  // R5 (review): reservePdBackupDestination creates the directory BEFORE
1269
1342
  // the copy — a mid-copy failure must not leave backupPath pointing at a
1270
1343
  // partial backup (a later /rollback would happily restore the fragment).
@@ -1275,6 +1348,7 @@ async function doInlineFullUpdate(workspaceDir) {
1275
1348
  const backupDest = reservePdBackupDestination(path.basename(extDir));
1276
1349
  try {
1277
1350
  copyDirRecursive(extDir, backupDest, SKIP_DIRS);
1351
+ backupRuntimeComponents(layout, backupDest);
1278
1352
  }
1279
1353
  catch (backupError) {
1280
1354
  try {
@@ -1306,6 +1380,23 @@ async function doInlineFullUpdate(workspaceDir) {
1306
1380
  fs.existsSync(path.join(releaseManagerSrc, 'dist'))) {
1307
1381
  copyDirRecursive(releaseManagerSrc, layout.releaseManagerDir, SKIP_DIRS);
1308
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
+ }
1309
1400
  const { error: linkError, quarantined } = ensureRuntimeResolutionLinks(layout, tempDir);
1310
1401
  reconciledQuarantine = quarantined;
1311
1402
  if (linkError) {
@@ -1382,12 +1473,32 @@ async function doInlineFullUpdate(workspaceDir) {
1382
1473
  if (tempDir && fs.existsSync(tempDir)) {
1383
1474
  fs.rmSync(tempDir, { recursive: true, force: true });
1384
1475
  }
1385
- // 6.5 Discard the quarantined stale dependency copies — the canonical
1386
- // components are in place now (PRI-665). Both the success return and the
1387
- // version-drift failure return flow through here with files already
1388
- // swapped, so restoring the stale copies would re-break resolution.
1389
- if (reconciledQuarantine.length > 0) {
1390
- 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
+ };
1391
1502
  }
1392
1503
  // 7. Version-advance check (drift guard). The full update installs the
1393
1504
  // plugin bundled inside the installer. If the installer is stale (its
@@ -1399,6 +1510,8 @@ async function doInlineFullUpdate(workspaceDir) {
1399
1510
  const newVersion = readCurrentVersion(extDir) ?? toVersion ?? 'unknown';
1400
1511
  const installedExpectedRelease = newVersion === stagedVersion;
1401
1512
  if (!installedExpectedRelease) {
1513
+ restoreQuarantined(reconciledQuarantine);
1514
+ reconciledQuarantine = [];
1402
1515
  // Files were already rewritten to the same (or lower) version. Record a
1403
1516
  // FAILED history entry so the operator sees why, and return a structured
1404
1517
  // error with nextAction.
@@ -1425,6 +1538,10 @@ async function doInlineFullUpdate(workspaceDir) {
1425
1538
  kind: 'update',
1426
1539
  ...(pluginBackupDir ? { backupPath: pluginBackupDir } : {}),
1427
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);
1428
1545
  return {
1429
1546
  success: true,
1430
1547
  message: depsChanged
@@ -1540,7 +1657,26 @@ function legacyCheckMutation(req, res, ctx) {
1540
1657
  }
1541
1658
  })();
1542
1659
  }
1660
+ /**
1661
+ * PRI-709 P0-3 (ADR-0024 D-2): the shared installation root that owns the
1662
+ * transaction journal. Legacy console mutations journal to the SAME place as
1663
+ * the installer and the ReleaseManager — never a console-private ledger.
1664
+ */
1665
+ function resolvePdHome() {
1666
+ return path.join(os.homedir(), '.pd');
1667
+ }
1668
+ /**
1669
+ * A runtime mutation that ran without journal evidence is a governance gap,
1670
+ * not a mutation failure: it is reported loud (rc-9) and the mutation result
1671
+ * is still served. Blocking the Owner's update here would be worse than an
1672
+ * unaudited one — but the gap must never be silent.
1673
+ */
1674
+ function logLegacyJournalGap(kind, reason) {
1675
+ console.warn(`[update] Runtime mutation "${kind}" completed WITHOUT journal evidence (${reason}). `
1676
+ + 'ADR-0024 D-2 requires every runtime mutation to be auditable; this is usually a missing create-principles-disciple install surface.');
1677
+ }
1543
1678
  function legacyApplyMutation(req, res, ctx) {
1679
+ const pdHome = resolvePdHome();
1544
1680
  return (async () => {
1545
1681
  const pluginDir = resolvePluginDir(ctx.workspaceDir);
1546
1682
  if (req.method !== 'POST') {
@@ -1578,11 +1714,12 @@ function legacyApplyMutation(req, res, ctx) {
1578
1714
  sendBadRequest(res, 'targetDir must be within workspace or extensions directory');
1579
1715
  return;
1580
1716
  }
1581
- const result = await doApplyUpdate({
1582
- targetDir,
1583
- mergeStrategy,
1584
- createBackup,
1585
- }, ctx.workspaceDir);
1717
+ // ADR-0024 D-2 (PRI-709 P0-3): every runtime mutation is journaled.
1718
+ // `planned` lands after all request validation, so a rejected request
1719
+ // leaves no transaction behind.
1720
+ const { result, journal } = await runLegacyJournaledMutation({ kind: 'apply', pdHome, pluginDir }, () => doApplyUpdate({ targetDir, mergeStrategy, createBackup }, ctx.workspaceDir));
1721
+ if (!journal.journaled)
1722
+ logLegacyJournalGap('apply', journal.reason);
1586
1723
  sendSuccess(res, result);
1587
1724
  }
1588
1725
  catch (err) {
@@ -1595,6 +1732,7 @@ function legacyApplyMutation(req, res, ctx) {
1595
1732
  })();
1596
1733
  }
1597
1734
  function legacyRollbackMutation(req, res, ctx) {
1735
+ const pdHome = resolvePdHome();
1598
1736
  return (async () => {
1599
1737
  const pluginDir = resolvePluginDir(ctx.workspaceDir);
1600
1738
  if (req.method !== 'POST') {
@@ -1626,7 +1764,12 @@ function legacyRollbackMutation(req, res, ctx) {
1626
1764
  sendBadRequest(res, 'backupDir must be within the workspace, extensions directory, or PD backups directory');
1627
1765
  return;
1628
1766
  }
1629
- const result = await doRollbackUpdate({ targetDir, backupDir }, ctx.workspaceDir);
1767
+ // ADR-0024 D-2 (PRI-709 P0-3): a rollback is a forward transition to a
1768
+ // known-good deployment, not an undo of this transaction — that is why
1769
+ // the terminal state is `confirmed`, not `rolled_back`.
1770
+ const { result, journal } = await runLegacyJournaledMutation({ kind: 'rollback', pdHome, pluginDir }, () => doRollbackUpdate({ targetDir, backupDir }, ctx.workspaceDir));
1771
+ if (!journal.journaled)
1772
+ logLegacyJournalGap('rollback', journal.reason);
1630
1773
  sendSuccess(res, result);
1631
1774
  }
1632
1775
  catch (err) {
@@ -1639,13 +1782,17 @@ function legacyRollbackMutation(req, res, ctx) {
1639
1782
  })();
1640
1783
  }
1641
1784
  function legacyApplyFullMutation(req, res, ctx) {
1785
+ const pdHome = resolvePdHome();
1642
1786
  return (async () => {
1643
1787
  if (req.method !== 'POST') {
1644
1788
  sendMethodNotAllowed(res);
1645
1789
  return;
1646
1790
  }
1791
+ const pluginDir = resolvePluginDir(ctx.workspaceDir);
1647
1792
  try {
1648
- const result = await doInlineFullUpdate(ctx.workspaceDir);
1793
+ const { result, journal } = await runLegacyJournaledMutation({ kind: 'apply-full', pdHome, pluginDir }, () => doInlineFullUpdate(ctx.workspaceDir));
1794
+ if (!journal.journaled)
1795
+ logLegacyJournalGap('apply-full', journal.reason);
1649
1796
  sendSuccess(res, result);
1650
1797
  }
1651
1798
  catch (err) {
@@ -0,0 +1,118 @@
1
+ /**
2
+ * PRI-709 P0-3 — journal coverage for the legacy console updater (ADR-0024 D-2).
3
+ *
4
+ * PRI-698 Phase 0 Audit finding F-3: the console updater performed runtime
5
+ * mutations with **zero** journal writes. Under ADR-0024 D-2 an unjournaled
6
+ * runtime mutation is the one thing that must never happen, so the legacy
7
+ * updater had to be brought under the SAME journal as the installer and the
8
+ * ReleaseManager before the legacy path can be retired.
9
+ *
10
+ * Design rules (all load-bearing):
11
+ *
12
+ * - **No new journal implementation.** This module reuses
13
+ * `create-principles-disciple`'s `transaction-journal` — one JSONL file per
14
+ * transaction under `~/.pd/transactions/`, append + fsync, strict reader.
15
+ * Nothing here parses, formats or rotates journals.
16
+ * - **The ReleaseManager path is never double-journaled.** ReleaseManager
17
+ * `apply-full` orchestrates the installer, which journals the whole
18
+ * lifecycle; the console dispatch for that path never reaches the legacy
19
+ * handlers this module wraps. When the ReleaseManager explicitly falls back
20
+ * to legacy, the legacy handler is the ONLY writer — one transaction per
21
+ * mutation, whichever authority served it.
22
+ * - **Explicit degradation, never a blocked mutation.** The journal module is
23
+ * loaded dynamically: the console runs in installations where the
24
+ * create-principles-disciple dist may be absent (the same delivery-surface
25
+ * gap the ReleaseManager authority loader already handles). A missing module
26
+ * is reported and the mutation proceeds unjournaled — refusing the Owner's
27
+ * update would be worse than an unaudited one, and the gap is observable.
28
+ * - **No fabricated digests.** The legacy updater does not verify signed
29
+ * release metadata, so its digest provenance is `fallback`: a synthetic
30
+ * sha256 over a literal marker, readable but explicitly NOT verifiable. It
31
+ * never claims `manifest` / `signed_channel` it cannot back.
32
+ *
33
+ * Transition strategy for legacy kinds:
34
+ *
35
+ * apply / apply-full / rollback: `planned` → `confirmed` | `failed`
36
+ *
37
+ * `planned` is appended immediately before the mutation (after all request
38
+ * validation, so rejected requests leave no transaction); `confirmed` or
39
+ * `failed` after it. `rolled_back` is NOT used for the rollback kind — a
40
+ * rollback restores a previous deployment and is itself a forward transition
41
+ * to a known-good state, whereas `rolled_back` means "this transaction was
42
+ * undone".
43
+ *
44
+ * Two documented trade-offs (PRI-709 review, deliberate):
45
+ *
46
+ * - **`generation` stays at the standalone default `1`.** Legacy console
47
+ * transactions do not participate in the dual-slot generation lineage — they
48
+ * never move the active record — so recording a real `active.json.generation
49
+ * + 1` would CLAIM a lineage the console does not actually advance. Recovery
50
+ * is unaffected either way: `recoverUnfinishedTransaction` keys on
51
+ * `activeRecord.transactionId`, which a console transaction never matches, so
52
+ * it resolves to "the previously confirmed release stands".
53
+ * - **The legacy path leaves `active.json` untouched.** The legacy updater
54
+ * replaces the runtime without the installer's dual-slot swap, so after a
55
+ * legacy `apply-full` the active record describes the PREVIOUS deployment.
56
+ * Making the console a second writer of the deployment identity would be a
57
+ * worse violation (one source of truth); the honest fix is retiring the
58
+ * legacy path (ADR-0024 D-1), which is exactly what this journal coverage
59
+ * unblocks.
60
+ */
61
+ import type { ReleaseMetadataDigestSource, TransactionState } from 'create-principles-disciple/dist/update/transaction-journal.js';
62
+ /** Journal kinds the legacy console updater performs. */
63
+ export type LegacyMutationKind = 'apply' | 'apply-full' | 'rollback';
64
+ export interface LegacyMutationJournal {
65
+ readonly transactionId: string;
66
+ readonly journalPath: string;
67
+ readonly releaseId: string;
68
+ readonly productVersion: string;
69
+ readonly releaseMetadataDigest: string;
70
+ }
71
+ export type LegacyJournalStatus = {
72
+ readonly journaled: true;
73
+ readonly journal: LegacyMutationJournal;
74
+ } | {
75
+ readonly journaled: false;
76
+ readonly reason: string;
77
+ };
78
+ /** Journal port — the real implementation is the shared transaction journal. */
79
+ export interface LegacyJournalPort {
80
+ appendJournalTransition(journalPath: string, transition: {
81
+ readonly at: string;
82
+ readonly from: TransactionState | null;
83
+ readonly to: TransactionState;
84
+ readonly transactionId: string;
85
+ readonly releaseId: string;
86
+ readonly productVersion: string;
87
+ readonly releaseMetadataDigest: string;
88
+ readonly releaseMetadataDigestSource: ReleaseMetadataDigestSource;
89
+ readonly generation: number;
90
+ readonly detail?: string;
91
+ }): void;
92
+ }
93
+ export interface LegacyJournalOptions {
94
+ /** `~/.pd` — the shared installation root (ADR-0023). */
95
+ readonly pdHome: string;
96
+ /** Installed runtime plugin dir; identity source of what is deployed. */
97
+ readonly pluginDir: string;
98
+ readonly kind: LegacyMutationKind;
99
+ readonly now?: () => Date;
100
+ /** Test seam; defaults to the dynamically imported shared journal. */
101
+ readonly journal?: LegacyJournalPort;
102
+ }
103
+ /**
104
+ * Open one legacy transaction and append `planned` before the mutation runs.
105
+ */
106
+ export declare function openLegacyMutationJournal(options: LegacyJournalOptions): Promise<LegacyJournalStatus>;
107
+ /**
108
+ * Runs a legacy mutation under one journal transaction.
109
+ *
110
+ * `planned` is written before `run()`; `confirmed` / `failed` after — decided
111
+ * by BOTH the thrown/not-thrown boundary and the updater's `success` result
112
+ * shape (see `toLegacyMutationOutcome`). A journal failure never changes the
113
+ * mutation's outcome — it is reported alongside it.
114
+ */
115
+ export declare function runLegacyJournaledMutation<T>(options: LegacyJournalOptions, run: () => Promise<T>): Promise<{
116
+ readonly result: T;
117
+ readonly journal: LegacyJournalStatus;
118
+ }>;