create-principles-disciple 1.146.9 → 1.147.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 (112) hide show
  1. package/_release/product-identity.json +1 -1
  2. package/codex-adapter/dist/ingestion/explicit-owner-statement.d.ts +6 -0
  3. package/codex-adapter/dist/ingestion/explicit-owner-statement.js +6 -0
  4. package/codex-adapter/dist/pd-hook.js +5 -2
  5. package/codex-adapter/dist/worker/workspace-worker.d.ts +13 -1
  6. package/codex-adapter/dist/worker/workspace-worker.js +26 -3
  7. package/codex-adapter/package.json +1 -1
  8. package/core/dist/runtime-v2/index.d.ts +2 -2
  9. package/core/dist/runtime-v2/index.d.ts.map +1 -1
  10. package/core/dist/runtime-v2/index.js +1 -1
  11. package/core/dist/runtime-v2/index.js.map +1 -1
  12. package/core/dist/runtime-v2/pain-ingress-payload.d.ts +13 -1
  13. package/core/dist/runtime-v2/pain-ingress-payload.d.ts.map +1 -1
  14. package/core/dist/runtime-v2/pain-ingress-payload.js +21 -2
  15. package/core/dist/runtime-v2/pain-ingress-payload.js.map +1 -1
  16. package/core/dist/runtime-v2/pain-ingress.d.ts.map +1 -1
  17. package/core/dist/runtime-v2/pain-ingress.js +3 -0
  18. package/core/dist/runtime-v2/pain-ingress.js.map +1 -1
  19. package/core/dist/runtime-v2/signal-collector/confidence-gate.d.ts +35 -0
  20. package/core/dist/runtime-v2/signal-collector/confidence-gate.d.ts.map +1 -0
  21. package/core/dist/runtime-v2/signal-collector/confidence-gate.js +48 -0
  22. package/core/dist/runtime-v2/signal-collector/confidence-gate.js.map +1 -0
  23. package/core/dist/runtime-v2/signal-collector/index.d.ts +1 -0
  24. package/core/dist/runtime-v2/signal-collector/index.d.ts.map +1 -1
  25. package/core/dist/runtime-v2/signal-collector/index.js +1 -0
  26. package/core/dist/runtime-v2/signal-collector/index.js.map +1 -1
  27. package/core/dist/runtime-v2/signal-collector/keyword-stage.d.ts +4 -6
  28. package/core/dist/runtime-v2/signal-collector/keyword-stage.d.ts.map +1 -1
  29. package/core/dist/runtime-v2/signal-collector/keyword-stage.js +100 -9
  30. package/core/dist/runtime-v2/signal-collector/keyword-stage.js.map +1 -1
  31. package/core/dist/runtime-v2/store/context/sqlite-context-assembler.d.ts.map +1 -1
  32. package/core/dist/runtime-v2/store/context/sqlite-context-assembler.js +8 -4
  33. package/core/dist/runtime-v2/store/context/sqlite-context-assembler.js.map +1 -1
  34. package/core/package.json +1 -1
  35. package/dist/installer.d.ts.map +1 -1
  36. package/dist/installer.js +52 -1
  37. package/dist/installer.js.map +1 -1
  38. package/dist/installers/mcp-registration-conflict.d.ts +35 -0
  39. package/dist/installers/mcp-registration-conflict.d.ts.map +1 -0
  40. package/dist/installers/mcp-registration-conflict.js +559 -0
  41. package/dist/installers/mcp-registration-conflict.js.map +1 -0
  42. package/dist/installers/openclaw-host-installer.d.ts.map +1 -1
  43. package/dist/installers/openclaw-host-installer.js +20 -0
  44. package/dist/installers/openclaw-host-installer.js.map +1 -1
  45. package/dist/installers/workbuddy-host-installer.d.ts.map +1 -1
  46. package/dist/installers/workbuddy-host-installer.js +124 -2
  47. package/dist/installers/workbuddy-host-installer.js.map +1 -1
  48. package/dist/mcp-server-layout-compat.d.ts +18 -0
  49. package/dist/mcp-server-layout-compat.d.ts.map +1 -0
  50. package/dist/mcp-server-layout-compat.js +22 -0
  51. package/dist/mcp-server-layout-compat.js.map +1 -0
  52. package/host-runtime/dist/governance-signal-admission.d.ts +147 -4
  53. package/host-runtime/dist/governance-signal-admission.js +1065 -60
  54. package/host-runtime/dist/index.d.ts +2 -0
  55. package/host-runtime/dist/index.js +2 -0
  56. package/host-runtime/dist/signal-classifier.d.ts +46 -0
  57. package/host-runtime/dist/signal-classifier.js +167 -0
  58. package/host-runtime/dist/signal-stage2-cycle.d.ts +42 -0
  59. package/host-runtime/dist/signal-stage2-cycle.js +184 -0
  60. package/host-runtime/package.json +1 -1
  61. package/install-layout/dist/index.d.ts +15 -0
  62. package/install-layout/dist/index.js +10 -0
  63. package/install-layout/package.json +1 -1
  64. package/package.json +9 -7
  65. package/pd-cli/dist/commands/health-codex.d.ts.map +1 -1
  66. package/pd-cli/dist/commands/health-codex.js +12 -0
  67. package/pd-cli/dist/commands/health-codex.js.map +1 -1
  68. package/pd-cli/dist/commands/health-mcp.d.ts +74 -0
  69. package/pd-cli/dist/commands/health-mcp.d.ts.map +1 -0
  70. package/pd-cli/dist/commands/health-mcp.js +723 -0
  71. package/pd-cli/dist/commands/health-mcp.js.map +1 -0
  72. package/pd-cli/dist/commands/health.d.ts +2 -0
  73. package/pd-cli/dist/commands/health.d.ts.map +1 -1
  74. package/pd-cli/dist/commands/health.js +11 -6
  75. package/pd-cli/dist/commands/health.js.map +1 -1
  76. package/pd-mcp-server/dist/budget.d.ts +15 -0
  77. package/pd-mcp-server/dist/budget.js +19 -0
  78. package/pd-mcp-server/dist/deadline.d.ts +75 -0
  79. package/pd-mcp-server/dist/deadline.js +136 -0
  80. package/pd-mcp-server/dist/envelope.d.ts +52 -0
  81. package/pd-mcp-server/dist/envelope.js +63 -0
  82. package/pd-mcp-server/dist/index.d.ts +10 -0
  83. package/pd-mcp-server/dist/index.js +76 -0
  84. package/pd-mcp-server/dist/logging.d.ts +12 -0
  85. package/pd-mcp-server/dist/logging.js +42 -0
  86. package/pd-mcp-server/dist/server.d.ts +90 -0
  87. package/pd-mcp-server/dist/server.js +178 -0
  88. package/pd-mcp-server/dist/version.d.ts +13 -0
  89. package/pd-mcp-server/dist/version.js +13 -0
  90. package/pd-mcp-server/dist/workspace.d.ts +23 -0
  91. package/pd-mcp-server/dist/workspace.js +77 -0
  92. package/pd-mcp-server/package.json +49 -0
  93. package/plugin/dist/bundle.js +414 -412
  94. package/plugin/dist/core/mcp-binding.d.ts +41 -0
  95. package/plugin/dist/index.d.ts +33 -0
  96. package/plugin/dist/openclaw.plugin.json +10 -1
  97. package/plugin/dist/utils/workspace-resolver.d.ts +17 -0
  98. package/plugin/openclaw.plugin.json +10 -1
  99. package/plugin/package.json +2 -1
  100. package/release-manager/dist/installer.js +52 -1
  101. package/release-manager/dist/installers/mcp-registration-conflict.js +559 -0
  102. package/release-manager/dist/installers/openclaw-host-installer.js +20 -0
  103. package/release-manager/dist/installers/workbuddy-host-installer.js +124 -2
  104. package/release-manager/dist/mcp-server-layout-compat.js +22 -0
  105. package/release-manager/package.json +7 -5
  106. package/workbuddy-adapter/dist/pd-hook.js +28 -5
  107. package/workbuddy-adapter/dist/worker/workspace-worker.d.ts +6 -1
  108. package/workbuddy-adapter/dist/worker/workspace-worker.js +24 -3
  109. package/workbuddy-adapter/package.json +1 -1
  110. package/workbuddy-adapter/plugin/.codebuddy-plugin/plugin.json +2 -2
  111. package/workbuddy-adapter/plugin/.mcp.json +10 -0
  112. package/workbuddy-adapter/plugin/mcp/launcher.cjs +347 -0
@@ -48,6 +48,8 @@ import * as yaml from 'js-yaml';
48
48
  import semver from 'semver';
49
49
  import { getInstallLayoutPaths, getWorkbuddyWorkspaceDir, installManifestHostsForPersist, mergeInstallManifestWorkspaces, parseInstallManifest, INSTALL_LAYOUT_VERSION, } from '@principles/install-layout';
50
50
  import { atomicReplaceTextFile, errnoCode } from '../utils/config-file-io.js';
51
+ import { checkMcpRegistrationConflict } from './mcp-registration-conflict.js';
52
+ import { resolveMcpServerDir, resolveMcpServerEntry } from '../mcp-server-layout-compat.js';
51
53
  import { getHomeDir, generateConfigYamlContent, getConfigYamlPath, resolveDefaultModelForProvider, isValidHttpBaseUrl } from '../mvp-config.js';
52
54
  // Type-only import from @principles/core — erased at compile time; this
53
55
  // package ships self-contained without @principles/core (PRI-625/PRI-669).
@@ -67,6 +69,7 @@ const WORKBUDDY_REQUIRED_SKILLS = ['pd-cli-operator', 'pd-pain-signal', 'pd-revi
67
69
  const WORKBUDDY_PAYLOAD_FILES = [
68
70
  path.join('.codebuddy-plugin', 'plugin.json'),
69
71
  path.join('hooks', 'hooks.json'),
72
+ path.join('mcp', 'launcher.cjs'),
70
73
  path.join('scripts', 'wrapper.cjs'),
71
74
  path.join('scripts', 'pd-control.cjs'),
72
75
  ...WORKBUDDY_REQUIRED_SKILLS.map((skill) => path.join('skills', skill, 'SKILL.md')),
@@ -84,6 +87,59 @@ const GOVERNANCE_INIT_TIMEOUT_MS = 180_000;
84
87
  function isRecord(value) {
85
88
  return typeof value === 'object' && value !== null && !Array.isArray(value);
86
89
  }
90
+ /**
91
+ * Atomic replace for the binding file (same discipline as the OpenClaw plugin
92
+ * writer and `$pd-control setup`): a reader must never observe a truncated
93
+ * binding. No generic config subsystem — one helper next to its only use.
94
+ */
95
+ function writeFileAtomicSync(file, contents) {
96
+ const temp = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.tmp`);
97
+ try {
98
+ writeFileSync(temp, contents, 'utf-8');
99
+ renameSync(temp, file);
100
+ }
101
+ catch (error) {
102
+ try {
103
+ rmSync(temp, { force: true });
104
+ }
105
+ catch {
106
+ /* best-effort cleanup of our own temp file */
107
+ }
108
+ throw error;
109
+ }
110
+ }
111
+ /**
112
+ * Cache-binding comparison for the idempotent path. `writtenAt` is a
113
+ * per-install timestamp, so comparing raw bytes would force a cache refresh
114
+ * on every run; comparing the rest proves the launcher will read the same
115
+ * workspace/entry/versions the marketplace copy declares. Absent on both
116
+ * sides is a match (no MCP payload); absent on one side is drift.
117
+ */
118
+ function mcpBindingMatches(sourceDir, cacheDir) {
119
+ const bindingFile = 'mcp-binding.json';
120
+ const read = (dir) => {
121
+ const file = path.join(dir, bindingFile);
122
+ if (!existsSync(file))
123
+ return undefined;
124
+ try {
125
+ const parsed = JSON.parse(readFileSync(file, 'utf-8'));
126
+ if (!isRecord(parsed))
127
+ return null;
128
+ const { writtenAt: _writtenAt, ...rest } = parsed;
129
+ return rest;
130
+ }
131
+ catch {
132
+ return null;
133
+ }
134
+ };
135
+ const source = read(sourceDir);
136
+ const cached = read(cacheDir);
137
+ if (source === undefined && cached === undefined)
138
+ return true;
139
+ if (source === undefined || cached === undefined || source === null || cached === null)
140
+ return false;
141
+ return JSON.stringify(source) === JSON.stringify(cached);
142
+ }
87
143
  /** Native v2 stores scope records as an array; old PD installs used one object. */
88
144
  function userInstalledPluginRecord(value) {
89
145
  const records = Array.isArray(value) ? value : [value];
@@ -1087,6 +1143,47 @@ function materializeMarketplace(marketplaceRoot, pluginDir) {
1087
1143
  // sources that escape it ("Path escapes marketplace root", frozen facts).
1088
1144
  cpSync(pluginDir, path.join(marketplaceRoot, WORKBUDDY_PLUGIN_NAME), { recursive: true });
1089
1145
  }
1146
+ /**
1147
+ * Gate 4 (PRI-1071): write the launcher binding into the marketplace payload
1148
+ * copy — the frozen governance workspace (getWorkbuddyWorkspaceDir SSoT) and
1149
+ * the canonical installed MCP server entry. Written only when the server
1150
+ * component exists (an older runtime without it stays honestly absent; the
1151
+ * launcher then fails closed with binding_missing — equally recoverable by
1152
+ * re-running the installer). A failure degrades to a structured note (G4-07:
1153
+ * MCP unavailability must not break the WorkBuddy host install).
1154
+ */
1155
+ function writeWorkbuddyMcpBinding(marketplaceRoot, homeDir) {
1156
+ try {
1157
+ const layout = getInstallLayoutPaths(homeDir);
1158
+ // Gate 4 P2: the launcher validates serverEntry against this recorded
1159
+ // runtime root + the Install Layout SSoT component dir, so the binding
1160
+ // cannot point the host at arbitrary JavaScript.
1161
+ const serverEntry = resolveMcpServerEntry(layout);
1162
+ if (!existsSync(serverEntry))
1163
+ return { ok: false, reason: 'mcp_server_component_missing' };
1164
+ let mcpServerVersion = 'unknown';
1165
+ try {
1166
+ const manifest = JSON.parse(readFileSync(path.join(resolveMcpServerDir(layout), 'package.json'), 'utf-8'));
1167
+ if (typeof manifest.version === 'string')
1168
+ mcpServerVersion = manifest.version;
1169
+ }
1170
+ catch { /* version stays unknown (rc-3: never coerce) */ }
1171
+ const binding = {
1172
+ schemaVersion: 'pd.mcp-binding.v1',
1173
+ host: 'workbuddy',
1174
+ workspaceDir: getWorkbuddyWorkspaceDir(homeDir),
1175
+ runtimeDir: layout.runtimeDir,
1176
+ serverEntry,
1177
+ versions: { mcpServer: mcpServerVersion },
1178
+ writtenAt: new Date().toISOString(),
1179
+ };
1180
+ writeFileAtomicSync(path.join(marketplaceRoot, WORKBUDDY_PLUGIN_NAME, 'mcp-binding.json'), JSON.stringify(binding, null, 2) + '\n');
1181
+ return { ok: true };
1182
+ }
1183
+ catch (error) {
1184
+ return { ok: false, reason: `write_failed:${error instanceof Error ? error.message.slice(0, 120) : String(error).slice(0, 120)}` };
1185
+ }
1186
+ }
1090
1187
  export class WorkBuddyHostInstaller {
1091
1188
  hostId = 'workbuddy';
1092
1189
  deps;
@@ -1209,6 +1306,26 @@ export class WorkBuddyHostInstaller {
1209
1306
  };
1210
1307
  }
1211
1308
  const { payload } = payloadFind;
1309
+ // 3b. SPEC §7.5/§11.3: a same-name user-level MCP server must stop the
1310
+ // install BEFORE any marketplace/cache/registration write. The check is
1311
+ // read-only and leaves the offending file untouched.
1312
+ const mcpConflict = checkMcpRegistrationConflict('workbuddy', {
1313
+ homeDir: this.deps.homeDir(),
1314
+ workbuddyConfigDir: p.configDir,
1315
+ // Ownership is proven by the payload PD ships OR by launcher content
1316
+ // stamps — never by the `mcp/launcher.cjs` file NAME alone.
1317
+ pdLauncherRoots: [payload.pluginDir],
1318
+ });
1319
+ if (!mcpConflict.ok) {
1320
+ return {
1321
+ success: false,
1322
+ hostId: this.hostId,
1323
+ configPath: p.settingsPath,
1324
+ configAction: 'skipped',
1325
+ reason: mcpConflict.reason,
1326
+ nextAction: mcpConflict.nextAction,
1327
+ };
1328
+ }
1212
1329
  // 4. Materialize the PD-owned marketplace (source stays inside its root).
1213
1330
  try {
1214
1331
  materializeMarketplace(p.marketplaceRoot, payload.pluginDir);
@@ -1223,6 +1340,10 @@ export class WorkBuddyHostInstaller {
1223
1340
  nextAction: `Check permissions under ${path.dirname(p.marketplaceRoot)} and re-run the installer.`,
1224
1341
  };
1225
1342
  }
1343
+ // 4b. Gate 4 (PRI-1071): bind the pd-recall MCP launcher to the frozen
1344
+ // governance workspace. Degrades to a note — the host install itself is
1345
+ // unaffected when the MCP component is missing or the write fails.
1346
+ const mcpBinding = writeWorkbuddyMcpBinding(p.marketplaceRoot, this.deps.homeDir());
1226
1347
  // 5. Register: engine commands first; observable direct-write fallback.
1227
1348
  const registration = this.registerPlugin(details, payload.version);
1228
1349
  if (!registration.ok) {
@@ -1328,7 +1449,7 @@ export class WorkBuddyHostInstaller {
1328
1449
  keywordPainEnabled: !governanceDefaults.keywordExplicitOff,
1329
1450
  configPath: governanceDefaults.configPath,
1330
1451
  },
1331
- reason: `Registered ${WORKBUDDY_PLUGIN_KEY} via ${registration.mode}${registration.mode === 'engine' ? '' : ` (engine unreachable: ${registration.engineNote})`}. Governance workspace initialized at ${p.governanceDir}${governance.dbInit === 'skipped' ? ` (${governance.note})` : ''}.${profileNote}${governanceDefaultsNote}`,
1452
+ reason: `Registered ${WORKBUDDY_PLUGIN_KEY} via ${registration.mode}${registration.mode === 'engine' ? '' : ` (engine unreachable: ${registration.engineNote})`}. Governance workspace initialized at ${p.governanceDir}${governance.dbInit === 'skipped' ? ` (${governance.note})` : ''}.${profileNote}${governanceDefaultsNote}${mcpBinding.ok ? ' pd-recall MCP binding written.' : ` pd-recall MCP binding skipped (${mcpBinding.reason ?? 'unknown'}).`}`,
1332
1453
  // PRI-1063 copy (single-line flat-key flow mapping) carried over from
1333
1454
  // main; the "Existing workspaces retain their prior settings" sentence is
1334
1455
  // deliberately dropped — PRI-1064 turns governance ON for those
@@ -1497,7 +1618,8 @@ export class WorkBuddyHostInstaller {
1497
1618
  return false;
1498
1619
  const sourceDir = path.join(p.marketplaceRoot, WORKBUDDY_PLUGIN_NAME);
1499
1620
  try {
1500
- return WORKBUDDY_PAYLOAD_FILES.every((file) => readFileSync(path.join(sourceDir, file)).equals(readFileSync(path.join(cacheDir, file))));
1621
+ const filesMatch = WORKBUDDY_PAYLOAD_FILES.every((file) => readFileSync(path.join(sourceDir, file)).equals(readFileSync(path.join(cacheDir, file))));
1622
+ return filesMatch && mcpBindingMatches(sourceDir, cacheDir);
1501
1623
  }
1502
1624
  catch {
1503
1625
  return false;
@@ -0,0 +1,22 @@
1
+ import * as path from 'node:path';
2
+ /**
3
+ * Temporary twin of install-layout's MCP component path API.
4
+ *
5
+ * The registry only publishes install-layout 0.4.0, which predates
6
+ * `mcpServerDir` / `getMcpServerEntry`. The npx-parity gate installs the
7
+ * packed installer ALONE and resolves that published dependency, so a static
8
+ * import of the new exports crashes at module load (PRI-669 defect class:
9
+ * the installer dist must never import an interface the registry lacks).
10
+ *
11
+ * Retirement condition: the registry serves the install-layout version whose
12
+ * d.ts declares `mcpServerDir` (this PR adds the changeset that publishes
13
+ * it). Then switch both call sites back to the shared exports and delete
14
+ * this file — the same two-step as the #2047 → #2058 manifest-host twin.
15
+ */
16
+ export function resolveMcpServerDir(layout) {
17
+ return path.join(layout.runtimeDir, 'pd-mcp-server');
18
+ }
19
+ export function resolveMcpServerEntry(layout) {
20
+ return path.join(resolveMcpServerDir(layout), 'dist', 'index.js');
21
+ }
22
+ //# sourceMappingURL=mcp-server-layout-compat.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-principles-disciple",
3
- "version": "1.146.9",
3
+ "version": "1.147.0",
4
4
  "description": "Interactive CLI installer for Principles Disciple OpenClaw plugin",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,6 +26,7 @@
26
26
  "host-runtime",
27
27
  "codex-adapter",
28
28
  "workbuddy-adapter",
29
+ "pd-mcp-server",
29
30
  "install-layout",
30
31
  "release-manager",
31
32
  "trust/root.json",
@@ -36,13 +37,14 @@
36
37
  ],
37
38
  "scripts": {
38
39
  "build": "tsc",
40
+ "build:deps": "npm run build --workspace=@principles/core && npm run build --workspace=@principles/install-layout && node scripts/mcp-server-build.mjs",
39
41
  "dev": "tsc --watch",
40
42
  "bundle": "node scripts/bundle-plugin.mjs",
41
- "build:release-asset": "node scripts/build-self-contained-release.mjs",
43
+ "build:release-asset": "npm run build:deps && node scripts/build-self-contained-release.mjs",
42
44
  "generate:release-locks": "node scripts/generate-release-locks.mjs",
43
45
  "check:release-locks": "node scripts/check-release-locks.mjs",
44
- "prepack": "npm run build && npm run bundle",
45
- "prepublishOnly": "npm run build && npm run bundle",
46
+ "prepack": "npm run build:deps && npm run build && npm run bundle",
47
+ "prepublishOnly": "npm run build:deps && npm run build && npm run bundle",
46
48
  "test": "vitest run",
47
49
  "test:fast": "vitest run --exclude tests/smoke-packaged-install.test.ts --exclude tests/release-asset-smoke.test.ts --exclude tests/release-upgrade-gate.test.ts --exclude tests/release-junction-lifecycle.test.ts",
48
50
  "test:smoke:packaged": "vitest run tests/smoke-packaged-install.test.ts",
@@ -96,6 +98,6 @@
96
98
  "directory": "packages/create-principles-disciple"
97
99
  },
98
100
  "pd": {
99
- "bundledPluginVersion": "2.2.8"
101
+ "bundledPluginVersion": "2.2.13"
100
102
  }
101
103
  }
@@ -37,7 +37,7 @@ import path from 'node:path';
37
37
  import process from 'node:process';
38
38
  import { pathToFileURL, fileURLToPath } from 'node:url';
39
39
  import { appendEventLogLine, redactTelemetryString, isRuleHostEvaluatedEventData } from '@principles/core/runtime-v2';
40
- import { createProductionHostRuntime, createWorkbuddyRuleContextProvider, loadPdConfigForPlugin, parseOwnerStatementPrefix, assessWorkbuddyLiveKeyword, resolveSharedCorrectionKeywordStoreForDetection, admitTrustedWorkbuddyLiveCorrection, ensureGovernanceDiagnosticianTask } from '@principles/host-runtime';
40
+ import { createProductionHostRuntime, createWorkbuddyRuleContextProvider, loadPdConfigForPlugin, parseOwnerStatementPrefix, assessWorkbuddyCorrectionCandidate, resolveSharedCorrectionKeywordStoreForDetection, admitTrustedWorkbuddyLiveCorrection, ensureGovernanceDiagnosticianTask } from '@principles/host-runtime';
41
41
  import { computeFeatureFlagsFromConfig } from '@principles/core/runtime-v2';
42
42
  import { WorkBuddyHooksHostAdapter } from './host-adapter.js';
43
43
  import { WORKBUDDY_TOOL_SEMANTICS } from './tool-semantics.js';
@@ -280,9 +280,10 @@ export async function processWorkbuddyHookInvocation(rawStdin, channelEventName,
280
280
  typeof prompt === 'string' && !explicitReserved) {
281
281
  // AUDIT-P3-01: 预检与 admitTrustedWorkbuddyLiveCorrection 内部使用同一份
282
282
  // 共享 live 词库(seed ∪ learned)——预检不会漏掉学习词,也不会比准入
283
- // 更宽。startsWith 预过滤与 matched 判据不变(无 LLM、无 transcript)。
284
- const matched = assessWorkbuddyLiveKeyword(prompt, event.context.sessionId, resolveSharedCorrectionKeywordStoreForDetection(resolution.workspaceDir));
285
- if (matched.matched) {
283
+ // 更宽。startsWith 预过滤保持(SPEC P-5);歧义候选只落 pending,
284
+ // 同步 hook 内零 LLM。
285
+ const matched = assessWorkbuddyCorrectionCandidate(prompt, event.context.sessionId, resolveSharedCorrectionKeywordStoreForDetection(resolution.workspaceDir));
286
+ if (matched.kind !== 'none') {
286
287
  const admitted = admitTrustedWorkbuddyLiveCorrection({
287
288
  workspaceDir: resolution.workspaceDir,
288
289
  rawEvent: parsed,
@@ -321,13 +322,35 @@ export async function processWorkbuddyHookInvocation(rawStdin, channelEventName,
321
322
  catch { /* Evidence-log failure must not block the prompt. */ }
322
323
  }
323
324
  }
325
+ else if (outcome.disposition === 'pending_llm_review') {
326
+ // Ambiguous Stage-1 candidate persisted for the worker's
327
+ // bounded Stage-2 step. No Pain, no task, no quota consumed.
328
+ try {
329
+ appendEventLogLine(path.join(resolution.workspaceDir, '.state'), {
330
+ ts: new Date().toISOString(),
331
+ type: 'workbuddy_keyword_pending_review',
332
+ category: 'detected',
333
+ sessionId: event.context.sessionId,
334
+ data: { hostKind: 'workbuddy', outcome: outcome.disposition, duplicate: outcome.duplicate },
335
+ });
336
+ }
337
+ catch { /* Evidence-log failure must not block the prompt. */ }
338
+ }
339
+ else if (outcome.disposition === 'abandoned') {
340
+ // P1 review round 3: an automatic correction needs REAL
341
+ // persisted behavior evidence. WorkBuddy has no observation
342
+ // store, so the shared ingress correctly settles as
343
+ // observation_only — no Pain, no task, no quota, and no
344
+ // fabricated `pain_event:` trace.
345
+ autoDiagnostics.push(diagnostic('skipped', 'workbuddy_keyword_observation_only:' + outcome.reason, 'No persisted behavior evidence exists for this automatic correction; no Pain was created.'));
346
+ }
324
347
  else if (outcome.disposition === 'rate_limited') {
325
348
  autoDiagnostics.push(diagnostic('degraded', 'workbuddy_keyword_rate_limited', 'Per-session strong signal limit reached; no new Pain was created.'));
326
349
  }
327
350
  }
328
351
  }
329
352
  }
330
- else if (matched.reason === 'prompt_too_large') {
353
+ else if (matched.reason === 'prompt_too_large' && matched.kind === 'none') {
331
354
  autoDiagnostics.push(diagnostic('degraded', 'workbuddy_keyword_prompt_too_large', 'Oversized prompt was not scanned or persisted as a keyword signal.'));
332
355
  }
333
356
  }
@@ -1,4 +1,4 @@
1
- import { type AdmitBareHostPainsResult, type InternalizationConsumerCycleOutcome, type ConsumerCycleLogger, type ReconcileGovernanceContinuationResult } from '@principles/host-runtime';
1
+ import { type AdmitBareHostPainsResult, type InternalizationConsumerCycleOutcome, type ConsumerCycleLogger, type ReconcileGovernanceContinuationResult, type SignalLlmClassifier, type Stage2CorrectionCycleReport } from '@principles/host-runtime';
2
2
  export type WorkbuddyWorkerMode = 'ready' | 'manual_action_required' | 'paused' | 'degraded';
3
3
  export interface WorkbuddyWorkerCycleStepReport {
4
4
  readonly reconcile: ReconcileGovernanceContinuationResult;
@@ -17,6 +17,8 @@ export interface WorkbuddyWorkerCycleStepReport {
17
17
  readonly nextAction?: string;
18
18
  } | null;
19
19
  readonly downstream: InternalizationConsumerCycleOutcome | null;
20
+ /** Phase B Stage-2 review step (always last, ≤2 candidates / ≤60s aggregate). */
21
+ readonly stage2?: Stage2CorrectionCycleReport;
20
22
  }
21
23
  export interface WorkbuddyWorkerCycleResult {
22
24
  readonly workspaceDir: string;
@@ -32,6 +34,9 @@ export interface WorkbuddyWorkerCycleOptions {
32
34
  readonly emitEvent?: (event: string, payloadJson: string) => void;
33
35
  /** Cap on diagnostician candidates inspected per cycle (default 5). */
34
36
  readonly diagnosticianCandidateLimit?: number;
37
+ /** Stage-2 classifier injection (tests); undefined = resolve from config. */
38
+ readonly stage2Classifier?: SignalLlmClassifier | null;
39
+ readonly stage2AggregateBudgetMs?: number;
35
40
  }
36
41
  /**
37
42
  * Run ONE bounded worker cycle for a canonical governance workspace. Never
@@ -33,7 +33,7 @@
33
33
  import fs from 'node:fs';
34
34
  import path from 'node:path';
35
35
  import { computeFeatureFlagsFromConfig, createRuntimeStateHandle, createPainSignalBridge, isRetryWaitBackoffElapsed, PrincipleTreeLedgerAdapter, resolveDiagnosticianCapability, } from '@principles/core/runtime-v2';
36
- import { admitBareHostPains, loadPdConfigForPlugin, loadFeatureFlagFromConfig, reconcileGovernanceContinuation, runInternalizationConsumerCycle, saveHostToolDeclaration, } from '@principles/host-runtime';
36
+ import { admitBareHostPains, loadPdConfigForPlugin, loadFeatureFlagFromConfig, reconcileGovernanceContinuation, runInternalizationConsumerCycle, runStage2CorrectionCycle, createSignalClassifierForWorkspace, saveHostToolDeclaration, } from '@principles/host-runtime';
37
37
  import { WORKBUDDY_TOOL_SEMANTICS, WORKBUDDY_TOOL_SEMANTIC_MAPPINGS } from '../tool-semantics.js';
38
38
  const WORKER_OWNER = 'companion-worker';
39
39
  const DEFAULT_DIAG_CANDIDATE_LIMIT = 5;
@@ -205,16 +205,37 @@ export async function runWorkbuddyWorkspaceWorkerCycle(options) {
205
205
  toolSemantics: WORKBUDDY_TOOL_SEMANTICS,
206
206
  hostKinds: ['workbuddy'],
207
207
  });
208
+ // Step 7 (SPEC v0.3.1 §2) — Stage-2 correction review, ALWAYS last.
209
+ const stage2Resolution = options.stage2Classifier === undefined
210
+ ? createSignalClassifierForWorkspace(workspaceDir)
211
+ : null;
212
+ const stage2Classifier = options.stage2Classifier !== undefined
213
+ ? options.stage2Classifier
214
+ : (stage2Resolution?.ok === true ? stage2Resolution.classifier : null);
215
+ const stage2ClassifierState = options.stage2Classifier === null
216
+ ? 'disabled'
217
+ : stage2Classifier === null
218
+ ? (stage2Resolution !== null && !stage2Resolution.ok && stage2Resolution.readiness === 'disabled' ? 'disabled' : 'unavailable')
219
+ : 'used';
220
+ const stage2 = await runStage2CorrectionCycle({
221
+ workspaceDir,
222
+ classifier: stage2Classifier,
223
+ classifierState: stage2ClassifierState,
224
+ ...(options.stage2AggregateBudgetMs !== undefined ? { aggregateBudgetMs: options.stage2AggregateBudgetMs } : {}),
225
+ ...(options.now !== undefined ? { now: options.now } : {}),
226
+ onEvent: emitEvent,
227
+ });
208
228
  const degradedReason = !reconcile.ok ? `reconcile:${reconcile.reason ?? 'failed'}`
209
229
  : !admission.ok ? `admission:${admission.reason ?? 'failed'}`
210
230
  : diagnostician?.status === 'failed' && diagnostician.errorCategory !== 'lease_conflict' ? `diagnostician_failed:${diagnostician.message ?? 'max_attempts_exceeded'}`
211
231
  : diagnostician?.status === 'degraded' ? `diagnostician_degraded:${diagnostician.message ?? 'unknown'}`
212
232
  : downstream.skipReason === 'runtime_config_error' || downstream.skipReason === 'cycle_error' || downstream.skipReason === 'config_malformed' ? `downstream:${downstream.skipReason}`
213
- : undefined;
233
+ : !stage2.ok ? `stage2:${stage2.reason ?? 'failed'}`
234
+ : undefined;
214
235
  return {
215
236
  ...base,
216
237
  mode: degradedReason !== undefined ? 'degraded' : 'ready',
217
238
  ...(degradedReason !== undefined ? { reason: degradedReason, nextAction: 'Inspect the governance workspace .pd/config.yaml runtime profile and the per-step report above; the worker retries automatically on the next cycle.' } : {}),
218
- report: { reconcile, admission, diagnostician, downstream },
239
+ report: { reconcile, admission, diagnostician, downstream, stage2 },
219
240
  };
220
241
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principles/workbuddy-adapter",
3
- "version": "0.2.17",
3
+ "version": "0.4.0",
4
4
  "description": "WorkBuddy host adapter for Principles Disciple — implements the HostAdapter interface for WorkBuddy's Git Bash command-hook model (ADR-0020 third host, SPEC workbuddy-host-adapter v0.3.0).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "principles-disciple",
3
- "version": "0.2.1",
4
- "description": "Principles Disciple — Owner-governed behavior internalization for AI agents (WorkBuddy host adapter, ADR-0020 third host). Requires explicit setup and host.workbuddy feature enablement; inactive until then.",
3
+ "version": "0.2.2",
4
+ "description": "Principles Disciple — Owner-governed behavior internalization for AI agents (WorkBuddy host adapter, ADR-0020 third host). Requires explicit setup and host.workbuddy feature enablement; inactive until then. Ships the read-only pd-recall MCP server (Gate 4).",
5
5
  "defaultEnabled": false
6
6
  }
@@ -0,0 +1,10 @@
1
+ {
2
+ "mcpServers": {
3
+ "pd-recall": {
4
+ "type": "stdio",
5
+ "command": "node",
6
+ "args": ["${CODEBUDDY_PLUGIN_ROOT}/mcp/launcher.cjs"],
7
+ "cwd": "${CODEBUDDY_PLUGIN_ROOT}"
8
+ }
9
+ }
10
+ }