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
@@ -16,6 +16,8 @@ export * from './rule-implementation-runtime.js';
16
16
  export * from './production-pain-evidence.js';
17
17
  export * from './governance-observation-store.js';
18
18
  export * from './governance-signal-admission.js';
19
+ export * from './signal-classifier.js';
20
+ export * from './signal-stage2-cycle.js';
19
21
  export * from './host-liveness-contract.js';
20
22
  export * from './internalization-consumer-governance.js';
21
23
  export * from './internalization-consumer-cycle.js';
@@ -26,6 +26,8 @@ export * from './rule-implementation-runtime.js';
26
26
  export * from './production-pain-evidence.js';
27
27
  export * from './governance-observation-store.js';
28
28
  export * from './governance-signal-admission.js';
29
+ export * from './signal-classifier.js';
30
+ export * from './signal-stage2-cycle.js';
29
31
  export * from './host-liveness-contract.js';
30
32
  // PRI-624 Slice C: shared internalization consumer execution (OpenClaw
31
33
  // auto-consumer + Companion workspace worker call the same cycle).
@@ -0,0 +1,46 @@
1
+ import { type LlmClassificationResult } from '@principles/core/runtime-v2';
2
+ export interface SignalClassifierCallOptions {
3
+ /**
4
+ * Per-call timeout override. The worker passes the cycle's REMAINING
5
+ * aggregate budget so the real adapter request (startRun/poll deadline)
6
+ * uses that budget instead of relying on an outer Promise race.
7
+ */
8
+ readonly timeoutMs?: number;
9
+ }
10
+ export type SignalLlmClassifier = (text: string, options?: SignalClassifierCallOptions) => Promise<LlmClassificationResult | null>;
11
+ export interface SignalClassifierProfile {
12
+ readonly provider: string;
13
+ readonly model: string;
14
+ readonly apiKeyEnv: string;
15
+ readonly timeoutMs: number;
16
+ readonly maxTokens?: number;
17
+ readonly baseUrl?: string;
18
+ }
19
+ export interface SignalClassifierLog {
20
+ (event: string, detail: string): void;
21
+ }
22
+ export interface CreateSignalClassifierOptions {
23
+ readonly log?: SignalClassifierLog;
24
+ readonly timeoutMs?: number;
25
+ }
26
+ /** Build the classifier from an already-resolved profile (no config I/O). */
27
+ export declare function createSignalLlmClassifier(profile: SignalClassifierProfile, workspaceDir: string, options?: CreateSignalClassifierOptions): SignalLlmClassifier;
28
+ export type SignalClassifierResolution = {
29
+ ok: true;
30
+ classifier: SignalLlmClassifier;
31
+ profile: SignalClassifierProfile;
32
+ } | {
33
+ ok: false;
34
+ reason: string;
35
+ nextAction: string;
36
+ readiness: 'disabled' | 'needs_setup' | 'config_malformed';
37
+ };
38
+ /**
39
+ * Resolve the `signalCollector` internal agent's pi-ai profile from
40
+ * `.pd/config.yaml` and build the classifier. This is the ONE resolution path
41
+ * for the Codex/WorkBuddy workers and the OpenClaw plugin factory (which adds
42
+ * its own bounded warning on top).
43
+ */
44
+ export declare function createSignalClassifierForWorkspace(workspaceDir: string, options?: CreateSignalClassifierOptions & {
45
+ readonly flagId?: string;
46
+ }): SignalClassifierResolution;
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Shared Stage-2 signal classifier factory (SPEC v0.3 §3.4 / B4).
3
+ *
4
+ * ONE implementation of the pi-ai classifier call for every host:
5
+ * OpenClaw's plugin factory, the Codex worker and the WorkBuddy worker all
6
+ * consume this instead of re-implementing startRun/pollRun/fetchOutput.
7
+ *
8
+ * The classifier contract is core-owned: `buildLlmPrompt`,
9
+ * `signal-classification-output-v1` and `resolveLlmClassificationPayload`
10
+ * come from `@principles/core/runtime-v2`. This module only owns acquisition:
11
+ * reading the signalCollector runtime profile and calling the adapter.
12
+ *
13
+ * No second model runtime is introduced (ADR-0029: pi-ai is the only backend).
14
+ */
15
+ import path from 'node:path';
16
+ import { PiAiRuntimeAdapter, SIGNAL_CLASSIFIER_SYSTEM_PROMPT, buildLlmPrompt, resolveLlmClassificationPayload, safeStringifyPreview, } from '@principles/core/runtime-v2';
17
+ import { loadPdConfigForPlugin } from './pd-config.js';
18
+ /** Build the classifier from an already-resolved profile (no config I/O). */
19
+ export function createSignalLlmClassifier(profile, workspaceDir, options = {}) {
20
+ const timeoutMs = options.timeoutMs ?? profile.timeoutMs;
21
+ const adapter = new PiAiRuntimeAdapter({
22
+ provider: profile.provider,
23
+ model: profile.model,
24
+ apiKeyEnv: profile.apiKeyEnv,
25
+ timeoutMs,
26
+ maxTokens: profile.maxTokens,
27
+ baseUrl: profile.baseUrl,
28
+ workspace: workspaceDir,
29
+ // Stage-2 emits a 5-field JSON. json_mode_first avoids depending on the
30
+ // provider's tool-call support; one repair attempt bounds latency.
31
+ outputPathStrategy: 'json_mode_first',
32
+ maxRepairAttempts: 1,
33
+ });
34
+ return async (text, callOptions) => {
35
+ const prompt = buildLlmPrompt(text);
36
+ // v0.3.1 §2.2: the effective timeout is the worker's remaining budget,
37
+ // and it is handed to the adapter itself (not only to an outer race).
38
+ const effectiveTimeoutMs = callOptions?.timeoutMs ?? timeoutMs;
39
+ try {
40
+ const handle = await adapter.startRun({
41
+ agentSpec: { agentId: 'signal-collector', schemaVersion: '1' },
42
+ inputPayload: { prompt },
43
+ contextItems: [],
44
+ timeoutMs: effectiveTimeoutMs,
45
+ outputSchemaRef: 'signal-classification-output-v1',
46
+ systemPrompt: SIGNAL_CLASSIFIER_SYSTEM_PROMPT,
47
+ });
48
+ let status = await adapter.pollRun(handle.runId);
49
+ const deadline = performance.now() + effectiveTimeoutMs;
50
+ while (status.status === 'running' && performance.now() < deadline) {
51
+ await new Promise((resolve) => setTimeout(resolve, 500));
52
+ status = await adapter.pollRun(handle.runId);
53
+ }
54
+ if (status.status !== 'succeeded') {
55
+ options.log?.('SIGNAL_LLM_TIMEOUT', JSON.stringify({ status: status.status }));
56
+ return null;
57
+ }
58
+ const output = await adapter.fetchOutput(handle.runId);
59
+ const resolved = resolveLlmClassificationPayload(output?.payload);
60
+ if (resolved.path === 'structured' && resolved.value)
61
+ return resolved.value;
62
+ if ((resolved.path === 'legacy_string' || resolved.path === 'legacy_envelope') && resolved.value) {
63
+ options.log?.('SIGNAL_LLM_LEGACY_STRING_PAYLOAD', `adapter payload path=${resolved.path}`);
64
+ return resolved.value;
65
+ }
66
+ options.log?.('SIGNAL_LLM_PARSE_FAIL', `unusable classifier payload path=${resolved.path}: ${safeStringifyPreview(output?.payload)}`);
67
+ return null;
68
+ }
69
+ catch (error) {
70
+ options.log?.('SIGNAL_LLM_FAILED', String(error).slice(0, 200));
71
+ return null;
72
+ }
73
+ };
74
+ }
75
+ /**
76
+ * Resolve the `signalCollector` internal agent's pi-ai profile from
77
+ * `.pd/config.yaml` and build the classifier. This is the ONE resolution path
78
+ * for the Codex/WorkBuddy workers and the OpenClaw plugin factory (which adds
79
+ * its own bounded warning on top).
80
+ */
81
+ export function createSignalClassifierForWorkspace(workspaceDir, options = {}) {
82
+ const configResult = loadPdConfigForPlugin(workspaceDir);
83
+ if (!configResult.ok) {
84
+ return {
85
+ ok: false,
86
+ readiness: 'config_malformed',
87
+ reason: `Config validation failed: ${configResult.errors.map((error) => error.reason).join('; ')}`,
88
+ nextAction: configResult.errors[0]?.nextAction ?? 'Fix .pd/config.yaml and retry',
89
+ };
90
+ }
91
+ const { config } = configResult.effective;
92
+ const flagId = options.flagId ?? 'signal_collector';
93
+ const agentName = 'signalCollector';
94
+ const feature = config.features[flagId];
95
+ if (!feature?.enabled) {
96
+ return {
97
+ ok: false,
98
+ readiness: 'disabled',
99
+ reason: `${flagId} is disabled in .pd/config.yaml`,
100
+ nextAction: `Enable it in the existing "features:" block as ${flagId}: {enabled: true}`,
101
+ };
102
+ }
103
+ const agent = config.internalAgents.agents[agentName];
104
+ if (!agent?.enabled) {
105
+ return {
106
+ ok: false,
107
+ readiness: 'disabled',
108
+ reason: `internalAgents.agents.${agentName}.enabled is false`,
109
+ nextAction: `Enable internalAgents > agents > ${agentName} > enabled: true`,
110
+ };
111
+ }
112
+ const runtimeProfileId = agent.runtimeProfile ?? config.internalAgents.defaultRuntime;
113
+ const profile = config.runtimeProfiles[runtimeProfileId];
114
+ if (!profile) {
115
+ return {
116
+ ok: false,
117
+ readiness: 'needs_setup',
118
+ reason: `Runtime profile '${runtimeProfileId}' not found`,
119
+ nextAction: `Add runtime profile '${runtimeProfileId}' to runtimeProfiles`,
120
+ };
121
+ }
122
+ if (profile.type !== 'pi-ai') {
123
+ return {
124
+ ok: false,
125
+ readiness: 'needs_setup',
126
+ reason: `Runtime profile '${runtimeProfileId}' is '${profile.type}', not pi-ai`,
127
+ nextAction: 'ADR-0029 requires a pi-ai runtime profile for signal classification',
128
+ };
129
+ }
130
+ if (!profile.apiKeyEnv) {
131
+ return {
132
+ ok: false,
133
+ readiness: 'needs_setup',
134
+ reason: `pi-ai profile '${runtimeProfileId}' missing apiKeyEnv`,
135
+ nextAction: `Add apiKeyEnv to runtime profile '${runtimeProfileId}'`,
136
+ };
137
+ }
138
+ if (!profile.provider || !profile.model) {
139
+ return {
140
+ ok: false,
141
+ readiness: 'needs_setup',
142
+ reason: `pi-ai profile '${runtimeProfileId}' missing provider/model`,
143
+ nextAction: `Set provider and model on runtime profile '${runtimeProfileId}'`,
144
+ };
145
+ }
146
+ if (!Object.hasOwn(process.env, profile.apiKeyEnv) || !process.env[profile.apiKeyEnv]) {
147
+ return {
148
+ ok: false,
149
+ readiness: 'needs_setup',
150
+ reason: `Environment variable '${profile.apiKeyEnv}' is not set or empty`,
151
+ nextAction: `Set the environment variable '${profile.apiKeyEnv}' with a valid API key`,
152
+ };
153
+ }
154
+ const resolved = {
155
+ provider: profile.provider,
156
+ model: profile.model,
157
+ apiKeyEnv: profile.apiKeyEnv,
158
+ timeoutMs: options.timeoutMs ?? profile.timeoutMs ?? 60_000,
159
+ ...(profile.maxTokens !== undefined ? { maxTokens: profile.maxTokens } : {}),
160
+ ...(profile.baseUrl !== undefined ? { baseUrl: profile.baseUrl } : {}),
161
+ };
162
+ return {
163
+ ok: true,
164
+ profile: resolved,
165
+ classifier: createSignalLlmClassifier(resolved, path.resolve(workspaceDir), options),
166
+ };
167
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Bounded Stage-2 correction review step shared by the Codex and WorkBuddy
3
+ * workers (SPEC v0.3 §4.4.7 / v0.3.1 §2).
4
+ *
5
+ * Contract:
6
+ * - runs at the END of the worker cycle (never ahead of the Diagnostician);
7
+ * - at most 2 candidates per cycle;
8
+ * - one aggregate wall-clock budget (default 60s), checked before each
9
+ * candidate and clamped into the single classification timeout;
10
+ * - serial, CAS-claimed candidates; no new worker process, no new flag.
11
+ *
12
+ * The classifier itself comes from `createSignalClassifierForWorkspace`
13
+ * (B4). A missing classifier does NOT fail the cycle: candidates with a
14
+ * durable review result are still settled without any LLM call.
15
+ */
16
+ import type { SignalLlmClassifier } from './signal-classifier.js';
17
+ export interface Stage2CorrectionCycleReport {
18
+ readonly ok: boolean;
19
+ readonly classifier: 'used' | 'unavailable' | 'disabled';
20
+ readonly claimed: number;
21
+ readonly admitted: number;
22
+ readonly resolvedNone: number;
23
+ readonly abandoned: number;
24
+ readonly stillPending: number;
25
+ readonly skipped: number;
26
+ readonly expired: number;
27
+ readonly durationMs: number;
28
+ readonly reason?: string;
29
+ readonly nextAction?: string;
30
+ readonly degradations: readonly string[];
31
+ }
32
+ export interface Stage2CorrectionCycleInput {
33
+ readonly workspaceDir: string;
34
+ readonly classifier: SignalLlmClassifier | null;
35
+ readonly classifierState?: 'used' | 'unavailable' | 'disabled';
36
+ readonly maxCandidates?: number;
37
+ readonly aggregateBudgetMs?: number;
38
+ readonly classificationTimeoutMs?: number;
39
+ readonly now?: Date;
40
+ readonly onEvent?: (event: string, payloadJson: string) => void;
41
+ }
42
+ export declare function runStage2CorrectionCycle(input: Stage2CorrectionCycleInput): Promise<Stage2CorrectionCycleReport>;
@@ -0,0 +1,184 @@
1
+ import { claimPendingCorrectionCandidates, recordPendingCorrectionReview, releasePendingCorrectionClaim, settlePendingCorrection, } from './governance-signal-admission.js';
2
+ const DEFAULT_MAX_CANDIDATES = 2;
3
+ const DEFAULT_AGGREGATE_BUDGET_MS = 60_000;
4
+ const DEFAULT_CLASSIFICATION_TIMEOUT_MS = 60_000;
5
+ function withTimeout(promise, timeoutMs) {
6
+ if (timeoutMs <= 0)
7
+ return Promise.resolve(null);
8
+ return new Promise((resolve) => {
9
+ const timer = setTimeout(() => resolve(null), timeoutMs);
10
+ void promise.then((value) => {
11
+ clearTimeout(timer);
12
+ resolve(value);
13
+ }, () => {
14
+ clearTimeout(timer);
15
+ resolve(null);
16
+ });
17
+ });
18
+ }
19
+ function emptyReport(classifier, reason, nextAction) {
20
+ return {
21
+ ok: reason === undefined,
22
+ classifier,
23
+ claimed: 0,
24
+ admitted: 0,
25
+ resolvedNone: 0,
26
+ abandoned: 0,
27
+ stillPending: 0,
28
+ skipped: 0,
29
+ expired: 0,
30
+ durationMs: 0,
31
+ ...(reason !== undefined ? { reason } : {}),
32
+ ...(nextAction !== undefined ? { nextAction } : {}),
33
+ degradations: [],
34
+ };
35
+ }
36
+ export async function runStage2CorrectionCycle(input) {
37
+ const classifierState = input.classifierState ?? (input.classifier === null ? 'unavailable' : 'used');
38
+ // Monotonic clock for the aggregate budget; wall clock stays for DB/TTL.
39
+ const startedAt = performance.now();
40
+ const now = input.now ?? new Date();
41
+ const maxCandidates = Math.min(Math.max(input.maxCandidates ?? DEFAULT_MAX_CANDIDATES, 1), DEFAULT_MAX_CANDIDATES);
42
+ const budgetMs = Math.max(0, input.aggregateBudgetMs ?? DEFAULT_AGGREGATE_BUDGET_MS);
43
+ const classificationTimeoutMs = Math.max(0, input.classificationTimeoutMs ?? DEFAULT_CLASSIFICATION_TIMEOUT_MS);
44
+ const claimedResult = claimPendingCorrectionCandidates({ workspaceDir: input.workspaceDir, limit: maxCandidates, now });
45
+ if (!claimedResult.ok) {
46
+ return {
47
+ ...emptyReport(classifierState, claimedResult.reason, claimedResult.nextAction),
48
+ durationMs: Math.round(performance.now() - startedAt),
49
+ degradations: [claimedResult.reason],
50
+ };
51
+ }
52
+ let admitted = 0;
53
+ let resolvedNone = 0;
54
+ let abandoned = 0;
55
+ let stillPending = 0;
56
+ let skipped = 0;
57
+ const degradations = [];
58
+ /**
59
+ * Release our claim when classification has NOT started. The release is
60
+ * guarded inside gsa (never clears a durable reviewResult, CAS on the
61
+ * fencing token), so a concurrent worker's state cannot be touched.
62
+ */
63
+ const releaseClaim = (candidate) => {
64
+ if (candidate.claimToken === null)
65
+ return;
66
+ const released = releasePendingCorrectionClaim({
67
+ workspaceDir: input.workspaceDir,
68
+ logicalObservationKey: candidate.logicalObservationKey,
69
+ claimToken: candidate.claimToken,
70
+ });
71
+ if (!released.ok) {
72
+ degradations.push(`${candidate.logicalObservationKey}:claim_release_failed:${released.reason}`);
73
+ }
74
+ };
75
+ const tally = (outcome) => {
76
+ if (outcome.disposition === 'admitted')
77
+ admitted += 1;
78
+ else if (outcome.disposition === 'resolved_none')
79
+ resolvedNone += 1;
80
+ else if (outcome.disposition === 'abandoned')
81
+ abandoned += 1;
82
+ else if (outcome.disposition === 'pending')
83
+ stillPending += 1;
84
+ else
85
+ skipped += 1;
86
+ };
87
+ for (const candidate of claimedResult.candidates) {
88
+ const elapsed = performance.now() - startedAt;
89
+ const remaining = budgetMs - elapsed;
90
+ if (remaining <= 0) {
91
+ releaseClaim(candidate);
92
+ skipped += 1;
93
+ continue;
94
+ }
95
+ try {
96
+ let review = candidate.reviewResult;
97
+ if (review === null) {
98
+ if (input.classifier === null) {
99
+ // No LLM configured/available: release the claim (no attempt
100
+ // consumed) so a later cycle can retry immediately. NOT a verdict.
101
+ releaseClaim(candidate);
102
+ skipped += 1;
103
+ continue;
104
+ }
105
+ const timeoutMs = Math.max(1, Math.min(classificationTimeoutMs, remaining));
106
+ // The remaining budget goes INTO the adapter request as well as the
107
+ // outer race (v0.3.1 §2.2); an outer timeout does not cancel a
108
+ // provider request that the adapter does not support cancelling.
109
+ const llm = await withTimeout(input.classifier(candidate.text, { timeoutMs }), timeoutMs);
110
+ const { claimToken } = candidate;
111
+ if (claimToken === null) {
112
+ // A freshly claimed candidate always carries a token; its absence
113
+ // means the claim was consumed elsewhere — do not write without it.
114
+ degradations.push(`${candidate.logicalObservationKey}:claim_token_missing`);
115
+ skipped += 1;
116
+ continue;
117
+ }
118
+ const recorded = recordPendingCorrectionReview({
119
+ workspaceDir: input.workspaceDir,
120
+ logicalObservationKey: candidate.logicalObservationKey,
121
+ claimToken,
122
+ llmResult: llm,
123
+ now,
124
+ });
125
+ if (!recorded.ok) {
126
+ // Nothing durable was written; release so the next cycle retries
127
+ // instead of waiting out the claim TTL.
128
+ releaseClaim(candidate);
129
+ degradations.push(`${candidate.logicalObservationKey}:${recorded.reason}`);
130
+ skipped += 1;
131
+ continue;
132
+ }
133
+ if (recorded.disposition === 'concurrent_loss') {
134
+ skipped += 1;
135
+ continue;
136
+ }
137
+ // Read the freshly persisted verdict through the settle step; the
138
+ // recorded verdict is durable even if a later step throws.
139
+ }
140
+ const settled = await settlePendingCorrection({
141
+ workspaceDir: input.workspaceDir,
142
+ logicalObservationKey: candidate.logicalObservationKey,
143
+ // Fencing token only when this cycle holds the claim; crash recovery
144
+ // of an already durable reviewResult settles without one.
145
+ ...(candidate.claimToken !== null ? { claimToken: candidate.claimToken } : {}),
146
+ now,
147
+ });
148
+ if (!settled.ok) {
149
+ degradations.push(`${candidate.logicalObservationKey}:${settled.reason}`);
150
+ skipped += 1;
151
+ continue;
152
+ }
153
+ tally(settled);
154
+ input.onEvent?.('SIGNAL_STAGE2_SETTLED', JSON.stringify({
155
+ logicalObservationKey: candidate.logicalObservationKey,
156
+ disposition: settled.disposition,
157
+ reason: settled.reason,
158
+ }));
159
+ }
160
+ catch (error) {
161
+ // Guarded release: a durable reviewResult is never cleared here.
162
+ releaseClaim(candidate);
163
+ const detail = error instanceof Error ? error.message.slice(0, 160) : String(error).slice(0, 160);
164
+ degradations.push(`${candidate.logicalObservationKey}:stage2_failed:${detail}`);
165
+ skipped += 1;
166
+ }
167
+ }
168
+ return {
169
+ ok: degradations.length === 0,
170
+ classifier: classifierState,
171
+ claimed: claimedResult.candidates.length,
172
+ admitted,
173
+ resolvedNone,
174
+ abandoned,
175
+ stillPending,
176
+ skipped,
177
+ expired: claimedResult.expired,
178
+ durationMs: Math.round(performance.now() - startedAt),
179
+ ...(degradations.length > 0
180
+ ? { reason: 'stage2_correction_degradations', nextAction: 'Inspect the per-candidate degradations; pending candidates remain durable and retry next cycle.' }
181
+ : {}),
182
+ degradations: degradations.slice(0, 10),
183
+ };
184
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principles/host-runtime",
3
- "version": "0.14.6",
3
+ "version": "0.15.0",
4
4
  "description": "Shared host-neutral orchestration for Principles Disciple MVP-Core hook paths.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -72,6 +72,14 @@ export interface InstallLayoutPaths {
72
72
  * the same way the Codex hook resolves the canonical runtime.
73
73
  */
74
74
  workbuddyAdapterDir: string;
75
+ /**
76
+ * Gate 4 (PRI-1071): the shared read-only MCP server component (payload dir
77
+ * pd-mcp-server/, npm name @principles/pd-mcp-server). This is the Install
78
+ * Layout SSoT for the canonical server entry OpenClaw/WorkBuddy launchers
79
+ * bind to; Codex keeps its plugin-data runtime layout and records that dir
80
+ * in its own binding.
81
+ */
82
+ mcpServerDir: string;
75
83
  }
76
84
  export interface LayoutResolution {
77
85
  mode: InstallLayoutMode | 'missing';
@@ -111,6 +119,13 @@ export declare function parseInstallManifest(value: unknown): {
111
119
  error?: string;
112
120
  };
113
121
  export declare function getInstallLayoutPaths(homeDir: string): InstallLayoutPaths;
122
+ /**
123
+ * Canonical installed MCP server entry for the shared runtime layout
124
+ * (~/.pd/runtime/pd-mcp-server/dist/index.js). Launchers, binding writers and
125
+ * health diagnostics derive the path from this single owner instead of each
126
+ * repeating the segment list.
127
+ */
128
+ export declare function getMcpServerEntry(paths: InstallLayoutPaths): string;
114
129
  /**
115
130
  * WorkBuddy governance workspace (SPEC workbuddy-host-adapter §4.1 OD-07,
116
131
  * frozen by Phase 0 probe P06 2026-10-12): an explicit user-level directory
@@ -140,8 +140,18 @@ export function getInstallLayoutPaths(homeDir) {
140
140
  releaseManagerDir: path.join(runtimeDir, 'release-manager'),
141
141
  codexAdapterDir: path.join(runtimeDir, 'codex-adapter'),
142
142
  workbuddyAdapterDir: path.join(runtimeDir, 'workbuddy-adapter'),
143
+ mcpServerDir: path.join(runtimeDir, 'pd-mcp-server'),
143
144
  };
144
145
  }
146
+ /**
147
+ * Canonical installed MCP server entry for the shared runtime layout
148
+ * (~/.pd/runtime/pd-mcp-server/dist/index.js). Launchers, binding writers and
149
+ * health diagnostics derive the path from this single owner instead of each
150
+ * repeating the segment list.
151
+ */
152
+ export function getMcpServerEntry(paths) {
153
+ return path.join(paths.mcpServerDir, 'dist', 'index.js');
154
+ }
145
155
  /**
146
156
  * WorkBuddy governance workspace (SPEC workbuddy-host-adapter §4.1 OD-07,
147
157
  * frozen by Phase 0 probe P06 2026-10-12): an explicit user-level directory
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@principles/install-layout",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
package/package.json CHANGED
@@ -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",
@@ -60,7 +62,7 @@
60
62
  "dependencies": {
61
63
  "@earendil-works/pi-agent-core": "1.2.0",
62
64
  "@earendil-works/pi-ai": "1.4.0",
63
- "@principles/install-layout": "^0.4.0",
65
+ "@principles/install-layout": "^0.4.1",
64
66
  "@sinclair/typebox": "^0.34.52",
65
67
  "@tufjs/models": "3.0.1",
66
68
  "@inquirer/prompts": "^8.7.2",
@@ -96,7 +98,7 @@
96
98
  "directory": "packages/create-principles-disciple"
97
99
  },
98
100
  "pd": {
99
- "bundledPluginVersion": "2.2.13"
101
+ "bundledPluginVersion": "2.4.0"
100
102
  },
101
- "gitHead": "8e0cdc607ee5d25e0b66a0655a6e0ce033a1fe99"
103
+ "gitHead": "d3355465b090daf098581cdb4c174634fe1e5279"
102
104
  }
@@ -1 +1 @@
1
- {"version":3,"file":"health-codex.d.ts","sourceRoot":"","sources":["../../src/commands/health-codex.ts"],"names":[],"mappings":"AAiDA,UAAU,kBAAkB;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AA6PD,wBAAsB,iBAAiB,CAAC,IAAI,GAAE,kBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CA0YpF"}
1
+ {"version":3,"file":"health-codex.d.ts","sourceRoot":"","sources":["../../src/commands/health-codex.ts"],"names":[],"mappings":"AAmDA,UAAU,kBAAkB;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AA+PD,wBAAsB,iBAAiB,CAAC,IAAI,GAAE,kBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoZpF"}
@@ -31,6 +31,7 @@ import { createRequire } from 'module';
31
31
  import { resolveWorkspaceDir } from '../resolve-workspace.js';
32
32
  import { loadPdConfigForPlugin, resolveNearestPdWorkspace, readCodexIngestionConsent, deriveCodexIngestionConsentState, listGovernanceCheckpoints, readGovernanceObservationStats, readGovernanceAdmissionCounts, detectLegacyCodexHookRegistration, CODEX_INGESTION_DISCLOSURE_VERSION, } from '@principles/host-runtime';
33
33
  import { assessCodexInstall, CODEX_INSTALL_NEXT_ACTIONS } from './health-codex-install.js';
34
+ import { assessMcpHealth, formatMcpHealthLines } from './health-mcp.js';
34
35
  import { getInstallLayoutPaths, parseInstallManifest } from '@principles/install-layout';
35
36
  import { SqliteConnection, SqliteTaskStore, computeFeatureFlagsFromConfig, isFeatureEnabled } from '@principles/core/runtime-v2';
36
37
  const require = createRequire(import.meta.url);
@@ -441,6 +442,14 @@ export async function handleHealthCodex(opts = {}) {
441
442
  }
442
443
  }
443
444
  // ── §15 ready conjunction ──────────────────────────────────────────────────
445
+ // PRI-1071 Gate 4: the shared MCP recall surface is reported separately.
446
+ // It is NOT part of the §15 ingestion/closure conjunction — a missing MCP
447
+ // binding must be visible and actionable, but it must not reclassify the
448
+ // (independent) Codex ingestion readiness contract.
449
+ const mcp = assessMcpHealth({ host: 'codex', homeDir: os.homedir(), env: process.env });
450
+ if (mcp.status !== 'HEALTHY') {
451
+ warnings.push(`mcp: ${mcp.status} — ${mcp.reason ?? 'unverified'}. ${mcp.nextAction ?? ''}`.trim());
452
+ }
444
453
  if (!hostCodexEnabled)
445
454
  noteReadyBlocker('host.codex', 'disabled');
446
455
  if (workerEvaluation.mode === 'paused')
@@ -492,6 +501,7 @@ export async function handleHealthCodex(opts = {}) {
492
501
  cliStatusSource: install.cliStatusSource,
493
502
  }
494
503
  : { reason: install.reason, nextAction: install.nextAction },
504
+ mcp,
495
505
  ready,
496
506
  readyBlockers: readyBlockers.slice(0, 12),
497
507
  warnings,
@@ -526,6 +536,8 @@ export async function handleHealthCodex(opts = {}) {
526
536
  }
527
537
  line('codexIngestionMinVersion', report.codexIngestionMinVersion);
528
538
  line('codexIngestionVerifiedVersion', report.codexIngestionVerifiedVersion);
539
+ for (const mcpLine of formatMcpHealthLines(report.mcp))
540
+ console.log(mcpLine);
529
541
  line('featureFlag.host.codex', `${String(report.featureFlag.enabled)} (${report.featureFlag.source})`);
530
542
  if ('enabled' in report.ingestionFlag) {
531
543
  line('ingestionFlag.codex_conversation_ingestion', `${String(report.ingestionFlag.enabled)} (${report.ingestionFlag.source})`);