create-principles-disciple 1.79.0 → 1.81.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 (196) hide show
  1. package/console/package.json +1 -0
  2. package/core/dist/runtime-v2/__tests__/admission-gate.test.js +6 -6
  3. package/core/dist/runtime-v2/__tests__/admission-gate.test.js.map +1 -1
  4. package/core/dist/runtime-v2/__tests__/architecture-regression.test.js +39 -0
  5. package/core/dist/runtime-v2/__tests__/architecture-regression.test.js.map +1 -1
  6. package/core/dist/runtime-v2/__tests__/artificer-runner-vslice.test.js +52 -0
  7. package/core/dist/runtime-v2/__tests__/artificer-runner-vslice.test.js.map +1 -1
  8. package/core/dist/runtime-v2/__tests__/evaluator-runner-vslice.test.js +245 -1
  9. package/core/dist/runtime-v2/__tests__/evaluator-runner-vslice.test.js.map +1 -1
  10. package/core/dist/runtime-v2/__tests__/feedback/redact-sensitive.test.js +131 -1
  11. package/core/dist/runtime-v2/__tests__/feedback/redact-sensitive.test.js.map +1 -1
  12. package/core/dist/runtime-v2/__tests__/fixtures/llm-e2e-config.d.ts.map +1 -1
  13. package/core/dist/runtime-v2/__tests__/fixtures/llm-e2e-config.js +4 -2
  14. package/core/dist/runtime-v2/__tests__/fixtures/llm-e2e-config.js.map +1 -1
  15. package/core/dist/runtime-v2/__tests__/scribe-runner-vslice.test.js +425 -2
  16. package/core/dist/runtime-v2/__tests__/scribe-runner-vslice.test.js.map +1 -1
  17. package/core/dist/runtime-v2/__tests__/telemetry-event.test.d.ts +2 -0
  18. package/core/dist/runtime-v2/__tests__/telemetry-event.test.d.ts.map +1 -0
  19. package/core/dist/runtime-v2/__tests__/telemetry-event.test.js +69 -0
  20. package/core/dist/runtime-v2/__tests__/telemetry-event.test.js.map +1 -0
  21. package/core/dist/runtime-v2/admission-gate.d.ts.map +1 -1
  22. package/core/dist/runtime-v2/admission-gate.js +0 -8
  23. package/core/dist/runtime-v2/admission-gate.js.map +1 -1
  24. package/core/dist/runtime-v2/config/__tests__/pd-config-contract.test.d.ts +15 -0
  25. package/core/dist/runtime-v2/config/__tests__/pd-config-contract.test.d.ts.map +1 -0
  26. package/core/dist/runtime-v2/config/__tests__/pd-config-contract.test.js +685 -0
  27. package/core/dist/runtime-v2/config/__tests__/pd-config-contract.test.js.map +1 -0
  28. package/core/dist/runtime-v2/config/index.d.ts +14 -0
  29. package/core/dist/runtime-v2/config/index.d.ts.map +1 -0
  30. package/core/dist/runtime-v2/config/index.js +16 -0
  31. package/core/dist/runtime-v2/config/index.js.map +1 -0
  32. package/core/dist/runtime-v2/config/pd-config-defaults.d.ts +14 -0
  33. package/core/dist/runtime-v2/config/pd-config-defaults.d.ts.map +1 -0
  34. package/core/dist/runtime-v2/config/pd-config-defaults.js +74 -0
  35. package/core/dist/runtime-v2/config/pd-config-defaults.js.map +1 -0
  36. package/core/dist/runtime-v2/config/pd-config-effective.d.ts +14 -0
  37. package/core/dist/runtime-v2/config/pd-config-effective.d.ts.map +1 -0
  38. package/core/dist/runtime-v2/config/pd-config-effective.js +118 -0
  39. package/core/dist/runtime-v2/config/pd-config-effective.js.map +1 -0
  40. package/core/dist/runtime-v2/config/pd-config-feature-flags.d.ts +35 -0
  41. package/core/dist/runtime-v2/config/pd-config-feature-flags.d.ts.map +1 -0
  42. package/core/dist/runtime-v2/config/pd-config-feature-flags.js +99 -0
  43. package/core/dist/runtime-v2/config/pd-config-feature-flags.js.map +1 -0
  44. package/core/dist/runtime-v2/config/pd-config-redaction.d.ts +22 -0
  45. package/core/dist/runtime-v2/config/pd-config-redaction.d.ts.map +1 -0
  46. package/core/dist/runtime-v2/config/pd-config-redaction.js +179 -0
  47. package/core/dist/runtime-v2/config/pd-config-redaction.js.map +1 -0
  48. package/core/dist/runtime-v2/config/pd-config-types.d.ts +124 -0
  49. package/core/dist/runtime-v2/config/pd-config-types.d.ts.map +1 -0
  50. package/core/dist/runtime-v2/config/pd-config-types.js +35 -0
  51. package/core/dist/runtime-v2/config/pd-config-types.js.map +1 -0
  52. package/core/dist/runtime-v2/config/pd-config-validate.d.ts +16 -0
  53. package/core/dist/runtime-v2/config/pd-config-validate.d.ts.map +1 -0
  54. package/core/dist/runtime-v2/config/pd-config-validate.js +443 -0
  55. package/core/dist/runtime-v2/config/pd-config-validate.js.map +1 -0
  56. package/core/dist/runtime-v2/feature-flags/plugin-surface-registry.js +10 -10
  57. package/core/dist/runtime-v2/feature-flags/plugin-surface-registry.js.map +1 -1
  58. package/core/dist/runtime-v2/feedback/index.d.ts +1 -1
  59. package/core/dist/runtime-v2/feedback/index.d.ts.map +1 -1
  60. package/core/dist/runtime-v2/feedback/index.js +1 -1
  61. package/core/dist/runtime-v2/feedback/index.js.map +1 -1
  62. package/core/dist/runtime-v2/feedback/redact-sensitive.d.ts +2 -0
  63. package/core/dist/runtime-v2/feedback/redact-sensitive.d.ts.map +1 -1
  64. package/core/dist/runtime-v2/feedback/redact-sensitive.js +32 -3
  65. package/core/dist/runtime-v2/feedback/redact-sensitive.js.map +1 -1
  66. package/core/dist/runtime-v2/index.d.ts +5 -1
  67. package/core/dist/runtime-v2/index.d.ts.map +1 -1
  68. package/core/dist/runtime-v2/index.js +9 -1
  69. package/core/dist/runtime-v2/index.js.map +1 -1
  70. package/core/dist/runtime-v2/internalization/__tests__/peer-runner-lineage-injection.test.d.ts +2 -0
  71. package/core/dist/runtime-v2/internalization/__tests__/peer-runner-lineage-injection.test.d.ts.map +1 -0
  72. package/core/dist/runtime-v2/internalization/__tests__/peer-runner-lineage-injection.test.js +95 -0
  73. package/core/dist/runtime-v2/internalization/__tests__/peer-runner-lineage-injection.test.js.map +1 -0
  74. package/core/dist/runtime-v2/internalization/__tests__/philosopher-runner-trust-boundary.test.d.ts +2 -0
  75. package/core/dist/runtime-v2/internalization/__tests__/philosopher-runner-trust-boundary.test.d.ts.map +1 -0
  76. package/core/dist/runtime-v2/internalization/__tests__/philosopher-runner-trust-boundary.test.js +398 -0
  77. package/core/dist/runtime-v2/internalization/__tests__/philosopher-runner-trust-boundary.test.js.map +1 -0
  78. package/core/dist/runtime-v2/internalization/artificer-output.d.ts +3 -2
  79. package/core/dist/runtime-v2/internalization/artificer-output.d.ts.map +1 -1
  80. package/core/dist/runtime-v2/internalization/artificer-output.js +40 -31
  81. package/core/dist/runtime-v2/internalization/artificer-output.js.map +1 -1
  82. package/core/dist/runtime-v2/internalization/artificer-runner.d.ts +59 -38
  83. package/core/dist/runtime-v2/internalization/artificer-runner.d.ts.map +1 -1
  84. package/core/dist/runtime-v2/internalization/artificer-runner.js +122 -394
  85. package/core/dist/runtime-v2/internalization/artificer-runner.js.map +1 -1
  86. package/core/dist/runtime-v2/internalization/dreamer-output.d.ts +4 -3
  87. package/core/dist/runtime-v2/internalization/dreamer-output.d.ts.map +1 -1
  88. package/core/dist/runtime-v2/internalization/dreamer-output.js +11 -9
  89. package/core/dist/runtime-v2/internalization/dreamer-output.js.map +1 -1
  90. package/core/dist/runtime-v2/internalization/dreamer-runner.d.ts +31 -92
  91. package/core/dist/runtime-v2/internalization/dreamer-runner.d.ts.map +1 -1
  92. package/core/dist/runtime-v2/internalization/dreamer-runner.js +80 -492
  93. package/core/dist/runtime-v2/internalization/dreamer-runner.js.map +1 -1
  94. package/core/dist/runtime-v2/internalization/evaluator-output.d.ts +2 -2
  95. package/core/dist/runtime-v2/internalization/evaluator-output.d.ts.map +1 -1
  96. package/core/dist/runtime-v2/internalization/evaluator-output.js +29 -23
  97. package/core/dist/runtime-v2/internalization/evaluator-output.js.map +1 -1
  98. package/core/dist/runtime-v2/internalization/evaluator-runner.d.ts +74 -48
  99. package/core/dist/runtime-v2/internalization/evaluator-runner.d.ts.map +1 -1
  100. package/core/dist/runtime-v2/internalization/evaluator-runner.js +165 -443
  101. package/core/dist/runtime-v2/internalization/evaluator-runner.js.map +1 -1
  102. package/core/dist/runtime-v2/internalization/peer-runner-contracts.d.ts +14 -0
  103. package/core/dist/runtime-v2/internalization/peer-runner-contracts.d.ts.map +1 -1
  104. package/core/dist/runtime-v2/internalization/peer-runner-contracts.js +21 -0
  105. package/core/dist/runtime-v2/internalization/peer-runner-contracts.js.map +1 -1
  106. package/core/dist/runtime-v2/internalization/philosopher-output.d.ts +8 -2
  107. package/core/dist/runtime-v2/internalization/philosopher-output.d.ts.map +1 -1
  108. package/core/dist/runtime-v2/internalization/philosopher-output.js +16 -11
  109. package/core/dist/runtime-v2/internalization/philosopher-output.js.map +1 -1
  110. package/core/dist/runtime-v2/internalization/philosopher-runner.d.ts +36 -55
  111. package/core/dist/runtime-v2/internalization/philosopher-runner.d.ts.map +1 -1
  112. package/core/dist/runtime-v2/internalization/philosopher-runner.js +118 -400
  113. package/core/dist/runtime-v2/internalization/philosopher-runner.js.map +1 -1
  114. package/core/dist/runtime-v2/internalization/rollout-reviewer-runner.d.ts.map +1 -1
  115. package/core/dist/runtime-v2/internalization/rollout-reviewer-runner.js +3 -1
  116. package/core/dist/runtime-v2/internalization/rollout-reviewer-runner.js.map +1 -1
  117. package/core/dist/runtime-v2/internalization/scribe-output.d.ts +8 -2
  118. package/core/dist/runtime-v2/internalization/scribe-output.d.ts.map +1 -1
  119. package/core/dist/runtime-v2/internalization/scribe-output.js +33 -17
  120. package/core/dist/runtime-v2/internalization/scribe-output.js.map +1 -1
  121. package/core/dist/runtime-v2/internalization/scribe-runner.d.ts +56 -49
  122. package/core/dist/runtime-v2/internalization/scribe-runner.d.ts.map +1 -1
  123. package/core/dist/runtime-v2/internalization/scribe-runner.js +117 -397
  124. package/core/dist/runtime-v2/internalization/scribe-runner.js.map +1 -1
  125. package/core/dist/runtime-v2/internalization/trainer-runner.d.ts.map +1 -1
  126. package/core/dist/runtime-v2/internalization/trainer-runner.js +3 -1
  127. package/core/dist/runtime-v2/internalization/trainer-runner.js.map +1 -1
  128. package/core/dist/runtime-v2/runner/__tests__/base-peer-runner-trust-boundary.test.d.ts +2 -0
  129. package/core/dist/runtime-v2/runner/__tests__/base-peer-runner-trust-boundary.test.d.ts.map +1 -0
  130. package/core/dist/runtime-v2/runner/__tests__/base-peer-runner-trust-boundary.test.js +379 -0
  131. package/core/dist/runtime-v2/runner/__tests__/base-peer-runner-trust-boundary.test.js.map +1 -0
  132. package/core/dist/runtime-v2/runner/base-peer-runner.d.ts +122 -0
  133. package/core/dist/runtime-v2/runner/base-peer-runner.d.ts.map +1 -0
  134. package/core/dist/runtime-v2/runner/base-peer-runner.js +528 -0
  135. package/core/dist/runtime-v2/runner/base-peer-runner.js.map +1 -0
  136. package/core/dist/runtime-v2/runner/peer-runner-types.d.ts +107 -0
  137. package/core/dist/runtime-v2/runner/peer-runner-types.d.ts.map +1 -0
  138. package/core/dist/runtime-v2/runner/peer-runner-types.js +13 -0
  139. package/core/dist/runtime-v2/runner/peer-runner-types.js.map +1 -0
  140. package/core/dist/telemetry-event.d.ts +2 -2
  141. package/core/dist/telemetry-event.d.ts.map +1 -1
  142. package/core/dist/telemetry-event.js +89 -0
  143. package/core/dist/telemetry-event.js.map +1 -1
  144. package/dist/installer.d.ts +1 -1
  145. package/dist/installer.d.ts.map +1 -1
  146. package/dist/installer.js +39 -10
  147. package/dist/installer.js.map +1 -1
  148. package/dist/mvp-config.d.ts +33 -0
  149. package/dist/mvp-config.d.ts.map +1 -1
  150. package/dist/mvp-config.js +174 -0
  151. package/dist/mvp-config.js.map +1 -1
  152. package/package.json +3 -3
  153. package/pd-cli/dist/commands/config-doctor.d.ts +17 -0
  154. package/pd-cli/dist/commands/config-doctor.d.ts.map +1 -0
  155. package/pd-cli/dist/commands/config-doctor.js +143 -0
  156. package/pd-cli/dist/commands/config-doctor.js.map +1 -0
  157. package/pd-cli/dist/commands/console.d.ts +18 -0
  158. package/pd-cli/dist/commands/console.d.ts.map +1 -1
  159. package/pd-cli/dist/commands/console.js +439 -0
  160. package/pd-cli/dist/commands/console.js.map +1 -1
  161. package/pd-cli/dist/commands/runtime-features.d.ts +23 -8
  162. package/pd-cli/dist/commands/runtime-features.d.ts.map +1 -1
  163. package/pd-cli/dist/commands/runtime-features.js +72 -31
  164. package/pd-cli/dist/commands/runtime-features.js.map +1 -1
  165. package/pd-cli/dist/index.d.ts +1 -1
  166. package/pd-cli/dist/index.js +83 -31
  167. package/pd-cli/dist/index.js.map +1 -1
  168. package/pd-cli/dist/services/config-doctor.d.ts +102 -0
  169. package/pd-cli/dist/services/config-doctor.d.ts.map +1 -0
  170. package/pd-cli/dist/services/config-doctor.js +426 -0
  171. package/pd-cli/dist/services/config-doctor.js.map +1 -0
  172. package/pd-cli/dist/services/console-launcher.d.ts +110 -0
  173. package/pd-cli/dist/services/console-launcher.d.ts.map +1 -0
  174. package/pd-cli/dist/services/console-launcher.js +282 -0
  175. package/pd-cli/dist/services/console-launcher.js.map +1 -0
  176. package/pd-cli/dist/services/pd-config-loader.d.ts +64 -0
  177. package/pd-cli/dist/services/pd-config-loader.d.ts.map +1 -0
  178. package/pd-cli/dist/services/pd-config-loader.js +156 -0
  179. package/pd-cli/dist/services/pd-config-loader.js.map +1 -0
  180. package/pd-cli/package.json +3 -1
  181. package/plugin/dist/core/event-log.d.ts +10 -0
  182. package/plugin/dist/core/event-log.js +85 -1
  183. package/plugin/dist/core/pd-config-loader.d.ts +96 -0
  184. package/plugin/dist/core/pd-config-loader.js +307 -0
  185. package/plugin/dist/core/runtime-v2-prompt-activation-reader.d.ts +4 -0
  186. package/plugin/dist/core/runtime-v2-prompt-activation-reader.js +12 -56
  187. package/plugin/dist/core/surface-guard.d.ts +12 -8
  188. package/plugin/dist/core/surface-guard.js +51 -2
  189. package/plugin/dist/hooks/gate.js +3 -0
  190. package/plugin/dist/hooks/prompt.js +4 -5
  191. package/plugin/dist/index.d.ts +4 -0
  192. package/plugin/dist/index.js +70 -94
  193. package/plugin/dist/service/correction-observer-service.d.ts +8 -0
  194. package/plugin/dist/service/correction-observer-service.js +48 -29
  195. package/plugin/dist/utils/workspace-resolver.d.ts +33 -1
  196. package/plugin/dist/utils/workspace-resolver.js +182 -3
@@ -1,7 +1,5 @@
1
1
  import * as path from 'path';
2
- import * as fs from 'fs';
3
- import * as yaml from 'js-yaml';
4
- import { computeEffectiveFlags, DEFAULT_FEATURE_FLAGS } from '@principles/core/runtime-v2';
2
+ import { loadFeatureFlagFromConfig } from './core/pd-config-loader.js';
5
3
  import { classifyTask } from './core/local-worker-routing.js';
6
4
  import { completeShadowObservation, recordShadowRouting } from './core/shadow-observation-registry.js';
7
5
  import { getCommandDescription } from './i18n/commands.js';
@@ -39,72 +37,28 @@ import { migrateDirectoryStructure } from './core/migration.js';
39
37
  import { migrateStaleWorkspaceGuidance } from './core/workspace-guidance-migrator.js';
40
38
  import { SystemLogger } from './core/system-logger.js';
41
39
  import { PathResolver } from './core/path-resolver.js';
42
- import { resolveCommandWorkspaceDir, resolveToolHookWorkspaceDirSafe } from './utils/workspace-resolver.js';
40
+ import { resolveCommandWorkspaceDir, resolveToolHookWorkspaceDirSafe, resolveHookWorkspaceDir } from './utils/workspace-resolver.js';
43
41
  import { computeRuntimeShadowTaskFingerprint, PD_LOCAL_PROFILES } from './utils/shadow-fingerprint.js';
44
42
  import { validateWorkspaceDir } from './core/workspace-dir-validation.js';
45
43
  import { resolveWorkspaceDirFromApi } from './core/path-resolver.js';
46
44
  import { checkSurfaceGuard, guardHook, guardService } from './core/surface-guard.js';
47
45
  // Track started workspaces — one-time init + evolution worker per workspace
48
46
  const startedWorkspaces = new Set();
49
- const HOOK_WORKSPACE_RESOLUTION_NEXT_ACTION = 'verify gateway plugin activation and hook workspace binding; ' +
50
- 'migrate live hook workspace resolution to PD-owned canonical configuration before relying on config-based recovery';
51
47
  // Map from childSessionKey → shadowObservationId
52
48
  // Used to complete shadow observations when subagent ends
53
49
  const pendingShadowObservations = new Map();
54
50
  // ── Feature Flag Loader (plugin I/O boundary) ─────────────────────────────
55
51
  // Reads workspace feature-flags.yaml and checks a specific flag.
56
52
  // Returns the flag definition with effective enabled state.
57
- const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
58
53
  function isRecord(value) {
59
54
  return value !== null && typeof value === 'object' && !Array.isArray(value);
60
55
  }
56
+ /**
57
+ * PRI-305/PRI-307: Load feature flag from .pd/config.yaml instead of .pd/feature-flags.yaml.
58
+ * Delegates to the shared plugin config loader for consistency.
59
+ */
61
60
  function loadFeatureFlagFromWorkspace(workspaceDir, flagId, logger) {
62
- const configPath = path.join(workspaceDir, '.pd', 'feature-flags.yaml');
63
- if (!fs.existsSync(configPath)) {
64
- const flags = computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath);
65
- const flag = flags.flags[flagId];
66
- return { enabled: flag?.enabled ?? false, source: 'defaults' };
67
- }
68
- let raw;
69
- try {
70
- raw = fs.readFileSync(configPath, 'utf8');
71
- }
72
- catch (e) {
73
- const msg = e instanceof Error ? e.message : String(e);
74
- logger?.warn?.(`[PD:FeatureFlags] Feature flags unreadable: ${msg} — using defaults`);
75
- const flags = computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath);
76
- const flag = flags.flags[flagId];
77
- return { enabled: flag?.enabled ?? false, source: 'defaults' };
78
- }
79
- let parsed;
80
- try {
81
- parsed = yaml.load(raw, { schema: yaml.JSON_SCHEMA });
82
- }
83
- catch (e) {
84
- const parseMsg = e instanceof Error ? e.message : String(e);
85
- logger?.warn?.(`[PD:FeatureFlags] Feature flags YAML parse error: ${parseMsg} — using defaults`);
86
- const flags = computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath);
87
- const flag = flags.flags[flagId];
88
- return { enabled: flag?.enabled ?? false, source: 'defaults' };
89
- }
90
- if (!isRecord(parsed)) {
91
- logger?.warn?.(`[PD:FeatureFlags] Feature flags not a mapping — using defaults`);
92
- const flags = computeEffectiveFlags({}, DEFAULT_FEATURE_FLAGS, configPath);
93
- const flag = flags.flags[flagId];
94
- return { enabled: flag?.enabled ?? false, source: 'defaults' };
95
- }
96
- // parsed is now narrowed to Record<string, unknown> by isRecord guard
97
- const parsedRecord = Object.create(null);
98
- for (const key of Object.keys(parsed)) {
99
- if (DANGEROUS_KEYS.has(key))
100
- continue;
101
- if (Object.hasOwn(parsed, key)) {
102
- parsedRecord[key] = parsed[key];
103
- }
104
- }
105
- const flags = computeEffectiveFlags(parsedRecord, DEFAULT_FEATURE_FLAGS, configPath);
106
- const flag = flags.flags[flagId];
107
- return { enabled: flag?.enabled ?? false, source: flags.source };
61
+ return loadFeatureFlagFromConfig(workspaceDir, flagId, logger);
108
62
  }
109
63
  export function shouldStartEvolutionWorker(workspaceDir, logger) {
110
64
  const flag = loadFeatureFlagFromWorkspace(workspaceDir, 'evolution_worker', logger);
@@ -113,7 +67,7 @@ export function shouldStartEvolutionWorker(workspaceDir, logger) {
113
67
  }
114
68
  const disabledInfo = JSON.stringify({
115
69
  reason: 'mvp_quiet_per_adr0014',
116
- nextAction: 'set evolution_worker.enabled=true in .pd/feature-flags.yaml to enable',
70
+ nextAction: 'set features.evolution_worker.enabled=true in .pd/config.yaml to enable',
117
71
  featureFlag: 'evolution_worker',
118
72
  boundedContext: 'legacy_evolution_worker',
119
73
  flagSource: flag.source,
@@ -127,7 +81,7 @@ export function shouldStartCorrectionObserver(workspaceDir, logger) {
127
81
  }
128
82
  const disabledInfo = JSON.stringify({
129
83
  reason: 'correction_observer_disabled',
130
- nextAction: 'set correction_observer.enabled=true in .pd/feature-flags.yaml to enable',
84
+ nextAction: 'set features.correction_observer.enabled=true in .pd/config.yaml to enable',
131
85
  featureFlag: 'correction_observer',
132
86
  boundedContext: 'correction_observer_service',
133
87
  flagSource: flag.source,
@@ -170,15 +124,18 @@ const plugin = {
170
124
  const language = api.pluginConfig?.language || 'en';
171
125
  // ── Hook: Prompt Building ──
172
126
  api.on('before_prompt_build', guardHook('hook:before_prompt_build', api.logger, async (event, ctx) => {
173
- const workspaceDir = resolveToolHookWorkspaceDirSafe(ctx, api, 'before_prompt_build');
174
- if (!workspaceDir) {
127
+ const wsResult = resolveHookWorkspaceDir(ctx, api, 'before_prompt_build');
128
+ if (!wsResult.ok) {
175
129
  api.logger.error(`[PD:before_prompt_build] workspaceDir resolution failed. ` +
176
- `agentId=${ctx.agentId ?? '(missing)'} sessionId=${ctx.sessionId ?? '(missing)'} ` +
177
- `sessionKey=${ctx.sessionKey ?? '(missing)'}. ` +
130
+ `reason=${wsResult.reason} agentId=${ctx.agentId ?? '(missing)'} sessionId=${ctx.sessionId ?? '(missing)'}. ` +
178
131
  `Hook skipped — no mutation will occur. ` +
179
- `NextAction: ${HOOK_WORKSPACE_RESOLUTION_NEXT_ACTION}`);
132
+ `NextAction: ${wsResult.nextAction}`);
180
133
  return;
181
134
  }
135
+ const workspaceDir = wsResult.workspaceDir;
136
+ if (wsResult.consistencyWarning) {
137
+ api.logger.warn(`[PD:before_prompt_build] ${wsResult.consistencyWarning}`);
138
+ }
182
139
  try {
183
140
  if (!startedWorkspaces.has(workspaceDir)) {
184
141
  startedWorkspaces.add(workspaceDir);
@@ -240,15 +197,18 @@ const plugin = {
240
197
  }));
241
198
  // ── Hook: Security Gate ──
242
199
  api.on('before_tool_call', guardHook('hook:before_tool_call', api.logger, (event, ctx) => {
243
- const workspaceDir = resolveToolHookWorkspaceDirSafe(ctx, api, 'before_tool_call');
244
- if (!workspaceDir) {
200
+ const wsResult = resolveHookWorkspaceDir(ctx, api, 'before_tool_call');
201
+ if (!wsResult.ok) {
245
202
  api.logger.error(`[PD:before_tool_call] workspaceDir resolution failed. ` +
246
- `agentId=${ctx.agentId ?? '(missing)'} sessionId=${ctx.sessionId ?? '(missing)'} ` +
247
- `sessionKey=${ctx.sessionKey ?? '(missing)'}. ` +
203
+ `reason=${wsResult.reason} agentId=${ctx.agentId ?? '(missing)'} sessionId=${ctx.sessionId ?? '(missing)'}. ` +
248
204
  `Hook skipped — security gate bypassed. ` +
249
- `NextAction: ${HOOK_WORKSPACE_RESOLUTION_NEXT_ACTION}`);
205
+ `NextAction: ${wsResult.nextAction}`);
250
206
  return;
251
207
  }
208
+ const workspaceDir = wsResult.workspaceDir;
209
+ if (wsResult.consistencyWarning) {
210
+ api.logger.warn(`[PD:before_tool_call] ${wsResult.consistencyWarning}`);
211
+ }
252
212
  try {
253
213
  const pluginConfig = api.pluginConfig ?? {};
254
214
  const { logger } = api;
@@ -268,15 +228,18 @@ const plugin = {
268
228
  }));
269
229
  // ── Hook: Pain & Trust ──
270
230
  api.on('after_tool_call', guardHook('hook:after_tool_call', api.logger, (event, ctx) => {
271
- const workspaceDir = resolveToolHookWorkspaceDirSafe(ctx, api, 'after_tool_call');
272
- if (!workspaceDir) {
231
+ const wsResult = resolveHookWorkspaceDir(ctx, api, 'after_tool_call');
232
+ if (!wsResult.ok) {
273
233
  api.logger.error(`[PD:after_tool_call] workspaceDir resolution failed. ` +
274
- `agentId=${ctx.agentId ?? '(missing)'} sessionId=${ctx.sessionId ?? '(missing)'} ` +
275
- `sessionKey=${ctx.sessionKey ?? '(missing)'}. ` +
234
+ `reason=${wsResult.reason} agentId=${ctx.agentId ?? '(missing)'} sessionId=${ctx.sessionId ?? '(missing)'}. ` +
276
235
  `Hook skipped — pain detection bypassed. ` +
277
- `NextAction: ${HOOK_WORKSPACE_RESOLUTION_NEXT_ACTION}`);
236
+ `NextAction: ${wsResult.nextAction}`);
278
237
  return;
279
238
  }
239
+ const workspaceDir = wsResult.workspaceDir;
240
+ if (wsResult.consistencyWarning) {
241
+ api.logger.warn(`[PD:after_tool_call] ${wsResult.consistencyWarning}`);
242
+ }
280
243
  try {
281
244
  const pluginConfig = api.pluginConfig ?? {};
282
245
  // Pass api separately to handleAfterToolCall to maintain type safety
@@ -295,15 +258,19 @@ const plugin = {
295
258
  }));
296
259
  // ── Hook: LLM Analysis ──
297
260
  api.on('llm_output', guardHook('hook:llm_output', api.logger, (event, ctx) => {
298
- const workspaceDir = resolveToolHookWorkspaceDirSafe(ctx, api, 'llm_output');
299
- if (!workspaceDir) {
261
+ const wsResult = resolveHookWorkspaceDir(ctx, api, 'llm_output');
262
+ if (!wsResult.ok) {
300
263
  api.logger.error(`[PD:llm_output] workspaceDir resolution failed. ` +
301
- `agentId=${ctx.agentId ?? '(missing)'} ` +
264
+ `reason=${wsResult.reason} agentId=${ctx.agentId ?? '(missing)'} ` +
302
265
  `sessionId=${ctx.sessionId ?? '(missing)'}. ` +
303
266
  `Hook skipped — LLM analysis bypassed. ` +
304
- `NextAction: ${HOOK_WORKSPACE_RESOLUTION_NEXT_ACTION}`);
267
+ `NextAction: ${wsResult.nextAction}`);
305
268
  return;
306
269
  }
270
+ const workspaceDir = wsResult.workspaceDir;
271
+ if (wsResult.consistencyWarning) {
272
+ api.logger.warn(`[PD:llm_output] ${wsResult.consistencyWarning}`);
273
+ }
307
274
  try {
308
275
  handleLlmOutput(event, { ...ctx, workspaceDir });
309
276
  WorkspaceContext.fromHookContext({ workspaceDir }).eventLog.recordHookExecution({
@@ -423,41 +390,50 @@ const plugin = {
423
390
  }));
424
391
  // ── Hook: Lifecycle ──
425
392
  api.on('before_reset', guardHook('hook:before_reset', api.logger, (event, ctx) => {
426
- const workspaceDir = resolveToolHookWorkspaceDirSafe(ctx, api, 'before_reset');
427
- if (!workspaceDir) {
393
+ const wsResult = resolveHookWorkspaceDir(ctx, api, 'before_reset');
394
+ if (!wsResult.ok) {
428
395
  api.logger.error(`[PD:before_reset] workspaceDir resolution failed. ` +
429
- `agentId=${ctx.agentId ?? '(missing)'} sessionId=${ctx.sessionId ?? '(missing)'}. ` +
430
- `Hook skipped. NextAction: ${HOOK_WORKSPACE_RESOLUTION_NEXT_ACTION}`);
396
+ `reason=${wsResult.reason} agentId=${ctx.agentId ?? '(missing)'} sessionId=${ctx.sessionId ?? '(missing)'}. ` +
397
+ `Hook skipped. NextAction: ${wsResult.nextAction}`);
431
398
  return;
432
399
  }
433
- return handleBeforeReset(event, { ...ctx, workspaceDir });
400
+ if (wsResult.consistencyWarning) {
401
+ api.logger.warn(`[PD:before_reset] ${wsResult.consistencyWarning}`);
402
+ }
403
+ return handleBeforeReset(event, { ...ctx, workspaceDir: wsResult.workspaceDir });
434
404
  }));
435
405
  api.on('before_compaction', guardHook('hook:before_compaction', api.logger, (event, ctx) => {
436
- const workspaceDir = resolveToolHookWorkspaceDirSafe(ctx, api, 'before_compaction');
437
- if (!workspaceDir) {
406
+ const wsResult = resolveHookWorkspaceDir(ctx, api, 'before_compaction');
407
+ if (!wsResult.ok) {
438
408
  api.logger.error(`[PD:before_compaction] workspaceDir resolution failed. ` +
439
- `agentId=${ctx.agentId ?? '(missing)'} sessionId=${ctx.sessionId ?? '(missing)'}. ` +
440
- `Hook skipped. NextAction: ${HOOK_WORKSPACE_RESOLUTION_NEXT_ACTION}`);
409
+ `reason=${wsResult.reason} agentId=${ctx.agentId ?? '(missing)'} sessionId=${ctx.sessionId ?? '(missing)'}. ` +
410
+ `Hook skipped. NextAction: ${wsResult.nextAction}`);
441
411
  return;
442
412
  }
443
- return handleBeforeCompaction(event, { ...ctx, workspaceDir });
413
+ if (wsResult.consistencyWarning) {
414
+ api.logger.warn(`[PD:before_compaction] ${wsResult.consistencyWarning}`);
415
+ }
416
+ return handleBeforeCompaction(event, { ...ctx, workspaceDir: wsResult.workspaceDir });
444
417
  }));
445
418
  api.on('after_compaction', guardHook('hook:after_compaction', api.logger, (event, ctx) => {
446
- const workspaceDir = resolveToolHookWorkspaceDirSafe(ctx, api, 'after_compaction');
447
- if (!workspaceDir) {
419
+ const wsResult = resolveHookWorkspaceDir(ctx, api, 'after_compaction');
420
+ if (!wsResult.ok) {
448
421
  api.logger.error(`[PD:after_compaction] workspaceDir resolution failed. ` +
449
- `agentId=${ctx.agentId ?? '(missing)'} sessionId=${ctx.sessionId ?? '(missing)'}. ` +
450
- `Hook skipped. NextAction: ${HOOK_WORKSPACE_RESOLUTION_NEXT_ACTION}`);
422
+ `reason=${wsResult.reason} agentId=${ctx.agentId ?? '(missing)'} sessionId=${ctx.sessionId ?? '(missing)'}. ` +
423
+ `Hook skipped. NextAction: ${wsResult.nextAction}`);
451
424
  return;
452
425
  }
453
- return handleAfterCompaction(event, { ...ctx, workspaceDir });
426
+ if (wsResult.consistencyWarning) {
427
+ api.logger.warn(`[PD:after_compaction] ${wsResult.consistencyWarning}`);
428
+ }
429
+ return handleAfterCompaction(event, { ...ctx, workspaceDir: wsResult.workspaceDir });
454
430
  }));
455
- // ── Service: Background Evolution Worker ──
431
+ // ── Service Registration (surface-guarded) ──
432
+ // PRI-294: EvolutionWorker service registration removed — it starts via
433
+ // before_prompt_build hook gate, not via api.registerService. The surface
434
+ // guard already prevents registration when disabled (enabledByDefault=false).
435
+ // Dead pre-assignment of EvolutionWorkerService.api removed.
456
436
  try {
457
- EvolutionWorkerService.api = api;
458
- const guardedEvolutionWorker = guardService('service:evolution-worker', EvolutionWorkerService, api.logger);
459
- if (guardedEvolutionWorker)
460
- api.registerService(guardedEvolutionWorker);
461
437
  const guardedCorrectionObserver = guardService('service:correction-observer', CorrectionObserverService, api.logger);
462
438
  if (guardedCorrectionObserver)
463
439
  api.registerService(guardedCorrectionObserver);
@@ -6,6 +6,14 @@ export interface CorrectionObserverServiceShape {
6
6
  start: (ctx: OpenClawPluginServiceContext) => void;
7
7
  stop?: (ctx: OpenClawPluginServiceContext) => void;
8
8
  }
9
+ /**
10
+ * PRI-307: Resolve CorrectionObserver from .pd/config.yaml.
11
+ *
12
+ * States:
13
+ * - disabled: feature flag off → return null, no noisy logs
14
+ * - needs_setup: enabled but missing API key or profile → return null with structured reason
15
+ * - ready/not_ready: enabled and configured → return observer instance
16
+ */
9
17
  export declare function resolveCorrectionObserver(wctx: WorkspaceContext, logger?: Pick<PluginLogger, 'info' | 'warn' | 'error' | 'debug'>): CorrectionObserver | null;
10
18
  export declare function runCorrectionObserverCycle(wctx: WorkspaceContext, logger: PluginLogger): Promise<void>;
11
19
  export declare const CorrectionObserverService: CorrectionObserverServiceShape;
@@ -1,9 +1,10 @@
1
1
  import { WorkspaceContext } from '../core/workspace-context.js';
2
2
  import { TrajectoryRegistry } from '../core/trajectory.js';
3
3
  import { CorrectionCueLearner } from '../core/correction-cue-learner.js';
4
- import { WorkflowFunnelLoader, PiAiRuntimeAdapter, CorrectionObserver, AgentScheduler, } from '@principles/core/runtime-v2';
4
+ import { PiAiRuntimeAdapter, CorrectionObserver, AgentScheduler, } from '@principles/core/runtime-v2';
5
5
  import { KeywordOptimizationService } from './keyword-optimization-service.js';
6
6
  import { SystemLogger } from '../core/system-logger.js';
7
+ import { resolveObserverConfig } from '../core/pd-config-loader.js';
7
8
  let correctionObserverTimeoutId = null;
8
9
  let correctionObserverStopped = false;
9
10
  const startedWorkspaces = new Set();
@@ -11,40 +12,45 @@ const CORRECTION_OBSERVER_INTERVAL_MS = 15 * 60 * 1000;
11
12
  const CORRECTION_OBSERVER_INITIAL_DELAY_MS = 10_000;
12
13
  const CORRECTION_OBSERVER_MAX_RECENT_SESSIONS = 20;
13
14
  const CORRECTION_OBSERVER_MAX_PAYLOAD_SESSIONS = 5;
15
+ /**
16
+ * PRI-307: Resolve CorrectionObserver from .pd/config.yaml.
17
+ *
18
+ * States:
19
+ * - disabled: feature flag off → return null, no noisy logs
20
+ * - needs_setup: enabled but missing API key or profile → return null with structured reason
21
+ * - ready/not_ready: enabled and configured → return observer instance
22
+ */
14
23
  export function resolveCorrectionObserver(wctx, logger) {
15
24
  try {
16
- const loader = new WorkflowFunnelLoader(wctx.stateDir);
17
- const funnel = loader.getFunnel('pd-correction-observer');
18
- const policy = funnel?.policy;
19
- if (!policy || policy.runtimeKind !== 'pi-ai') {
20
- logger?.debug?.('[PD:CorrectionObserver] workflows.yaml pd-correction-observer policy not found. Falling back to environment variables.');
21
- const provider = process.env.PD_CORRECTION_PROVIDER || 'anthropic';
22
- const model = process.env.PD_CORRECTION_MODEL || 'anthropic/claude-3-5-sonnet';
23
- const apiKeyEnv = process.env.PD_CORRECTION_API_KEY_ENV || 'ANTHROPIC_API_KEY';
24
- const baseUrl = process.env.PD_CORRECTION_BASE_URL;
25
- if (!process.env[apiKeyEnv]) {
26
- logger?.debug?.(`[PD:CorrectionObserver] API key env ${apiKeyEnv} is not set. Periodic optimization disabled.`);
27
- return null;
25
+ const observerConfig = resolveObserverConfig(wctx.workspaceDir, 'correction_observer', 'correctionObserver', logger);
26
+ if (!observerConfig.enabled) {
27
+ if (observerConfig.readiness === 'config_malformed') {
28
+ logger?.warn?.(`[PD:CorrectionObserver] Config malformed: ${observerConfig.reason}. ${observerConfig.nextAction}`);
28
29
  }
30
+ else {
31
+ logger?.debug?.(`[PD:CorrectionObserver] ${observerConfig.reason}`);
32
+ }
33
+ return null;
34
+ }
35
+ if (observerConfig.readiness === 'needs_setup') {
36
+ logger?.info?.(`[PD:CorrectionObserver] ${observerConfig.reason}. ${observerConfig.nextAction}`);
37
+ return null;
38
+ }
39
+ // ready or not_ready — create the observer
40
+ if (observerConfig.runtimeProfileType === 'pi-ai') {
29
41
  const adapter = new PiAiRuntimeAdapter({
30
- provider,
31
- model,
32
- apiKeyEnv,
33
- baseUrl,
42
+ provider: observerConfig.provider ?? 'anthropic',
43
+ model: observerConfig.model ?? 'anthropic/claude-3-5-sonnet',
44
+ apiKeyEnv: observerConfig.apiKeyEnv ?? 'ANTHROPIC_API_KEY',
45
+ timeoutMs: observerConfig.timeoutMs ?? undefined,
46
+ baseUrl: observerConfig.baseUrl ?? undefined,
34
47
  workspace: wctx.workspaceDir,
35
48
  });
36
- return new CorrectionObserver({ runtimeAdapter: adapter });
49
+ return new CorrectionObserver({ runtimeAdapter: adapter }, { timeoutMs: observerConfig.timeoutMs ?? undefined });
37
50
  }
38
- const adapter = new PiAiRuntimeAdapter({
39
- provider: String(policy.provider),
40
- model: String(policy.model),
41
- apiKeyEnv: String(policy.apiKeyEnv),
42
- maxRetries: policy.maxRetries,
43
- timeoutMs: policy.timeoutMs ?? 30_000,
44
- baseUrl: policy.baseUrl,
45
- workspace: wctx.workspaceDir,
46
- });
47
- return new CorrectionObserver({ runtimeAdapter: adapter }, { timeoutMs: policy.timeoutMs });
51
+ // OpenClaw profile — not yet supported for observer runtime
52
+ logger?.info?.(`[PD:CorrectionObserver] OpenClaw runtime profile not yet supported for correction observer. Skipping.`);
53
+ return null;
48
54
  }
49
55
  catch (err) {
50
56
  logger?.warn?.(`[PD:CorrectionObserver] Failed to resolve CorrectionObserver: ${String(err)}`);
@@ -55,7 +61,8 @@ export async function runCorrectionObserverCycle(wctx, logger) {
55
61
  try {
56
62
  const observer = resolveCorrectionObserver(wctx, logger);
57
63
  if (!observer) {
58
- logger?.info?.('[PD:CorrectionObserver] Observer not resolved (no API key or config). Skipping cycle.');
64
+ // PRI-307: No noisy "no API key" cycling. Only log at debug level.
65
+ logger?.debug?.(`[PD:CorrectionObserver] Observer not resolved. Skipping cycle.`);
59
66
  return;
60
67
  }
61
68
  logger?.info?.('[PD:CorrectionObserver] Observer resolved. Initiating periodic optimization...');
@@ -135,6 +142,18 @@ export const CorrectionObserverService = {
135
142
  logger.info(`[PD:CorrectionObserver] Already started for workspace: ${workspaceDir}. Skipping duplicate start.`);
136
143
  return;
137
144
  }
145
+ // PRI-307: Check observer config before starting
146
+ const observerConfig = resolveObserverConfig(workspaceDir, 'correction_observer', 'correctionObserver', logger);
147
+ if (!observerConfig.enabled) {
148
+ // Disabled → no start, no noisy cycling. Single structured log.
149
+ logger?.info?.(`[PD:CorrectionObserver] ${observerConfig.reason}. ${observerConfig.nextAction}`);
150
+ return;
151
+ }
152
+ if (observerConfig.readiness === 'needs_setup') {
153
+ // Enabled but missing setup → structured needs_setup, no noisy cycling
154
+ logger?.info?.(`[PD:CorrectionObserver] ${observerConfig.reason}. ${observerConfig.nextAction}`);
155
+ return;
156
+ }
138
157
  startedWorkspaces.add(workspaceDir);
139
158
  correctionObserverStopped = false;
140
159
  const wctx = WorkspaceContext.fromHookContext({ workspaceDir, ...ctx.config });
@@ -2,6 +2,11 @@
2
2
  * Workspace Directory Resolution Utilities
3
3
  *
4
4
  * Shared helpers for resolving workspace directories across commands and hooks.
5
+ *
6
+ * Hook resolution priority (PRI-259): PD canonical config → OpenClaw fallback.
7
+ * PD canonical sources: PD_WORKSPACE_DIR env → OPENCLAW_WORKSPACE env →
8
+ * principles-disciple.json → ~/.openclaw/workspace default.
9
+ * OpenClaw fallback: ctx.workspaceDir → api.runtime.agent.resolveAgentWorkspaceDir().
5
10
  */
6
11
  import type { OpenClawPluginApi, PluginCommandContext } from '../openclaw-sdk.js';
7
12
  import { type WorkspaceResolutionContext } from '../core/workspace-dir-validation.js';
@@ -29,11 +34,38 @@ export declare function resolveCommandWorkspaceDir(api: OpenClawPluginApi, ctx:
29
34
  * @param source - Source label for error messages (e.g. 'evolution-status', 'pain')
30
35
  */
31
36
  export declare function resolvePluginCommandWorkspaceDir(ctx: PluginCommandContext, source: string): string;
37
+ export type CanonicalWorkspaceSource = 'pd_env' | 'openclaw_env' | 'pd_config' | 'pd_default';
38
+ export interface CanonicalWorkspaceResult {
39
+ workspaceDir: string;
40
+ source: CanonicalWorkspaceSource;
41
+ }
42
+ export declare function resolveCanonicalWorkspaceDir(): CanonicalWorkspaceResult | null;
43
+ export type HookWorkspaceSource = CanonicalWorkspaceSource | 'openclaw_context' | 'openclaw_api';
44
+ export interface HookWorkspaceResolutionSuccess {
45
+ ok: true;
46
+ workspaceDir: string;
47
+ source: HookWorkspaceSource;
48
+ consistencyWarning?: string;
49
+ }
50
+ export interface HookWorkspaceResolutionFailure {
51
+ ok: false;
52
+ reason: string;
53
+ nextAction: string;
54
+ message: string;
55
+ }
56
+ export type HookWorkspaceResolutionResult = HookWorkspaceResolutionSuccess | HookWorkspaceResolutionFailure;
57
+ export interface HookWorkspaceResolutionOptions {
58
+ canonicalResolver?: () => CanonicalWorkspaceResult | null;
59
+ explicitPdResolver?: () => CanonicalWorkspaceResult | null;
60
+ }
61
+ export declare function resolveHookWorkspaceDir(ctx: WorkspaceResolutionContext, api: OpenClawPluginApi, source: string, options?: HookWorkspaceResolutionOptions): HookWorkspaceResolutionResult;
32
62
  /**
33
63
  * Resolve workspace directory for tool hook execution (safe version).
34
64
  * Returns undefined instead of throwing if resolution fails.
65
+ *
66
+ * PRI-259: Uses PD canonical config as primary source, OpenClaw as fallback.
35
67
  */
36
- export declare function resolveToolHookWorkspaceDirSafe(ctx: WorkspaceResolutionContext, api: OpenClawPluginApi, source: string): string | undefined;
68
+ export declare function resolveToolHookWorkspaceDirSafe(ctx: WorkspaceResolutionContext, api: OpenClawPluginApi, source: string, options?: HookWorkspaceResolutionOptions): string | undefined;
37
69
  export declare class WorkspaceResolutionError extends Error {
38
70
  readonly reason: string;
39
71
  readonly nextAction: string;
@@ -2,11 +2,17 @@
2
2
  * Workspace Directory Resolution Utilities
3
3
  *
4
4
  * Shared helpers for resolving workspace directories across commands and hooks.
5
+ *
6
+ * Hook resolution priority (PRI-259): PD canonical config → OpenClaw fallback.
7
+ * PD canonical sources: PD_WORKSPACE_DIR env → OPENCLAW_WORKSPACE env →
8
+ * principles-disciple.json → ~/.openclaw/workspace default.
9
+ * OpenClaw fallback: ctx.workspaceDir → api.runtime.agent.resolveAgentWorkspaceDir().
5
10
  */
6
11
  import { validateWorkspaceDir } from '../core/workspace-dir-validation.js';
7
- import { resolveWorkspaceDir } from '../core/workspace-dir-service.js';
8
12
  import { resolveWorkspaceDirFromApi } from '../core/path-resolver.js';
9
13
  import * as path from 'path';
14
+ import * as os from 'os';
15
+ import * as fs from 'fs';
10
16
  /**
11
17
  * Resolve workspace directory for command execution.
12
18
  *
@@ -69,12 +75,185 @@ export function resolvePluginCommandWorkspaceDir(ctx, source) {
69
75
  throw new Error(`[PD:Command:${source}] CRITICAL: workspaceDir is not set in ctx.workspaceDir or ctx.config.workspaceDir. ` +
70
76
  `Commands cannot execute without a valid workspace. Set OPENCLAW_WORKSPACE_DIR env var or ensure the workspace is properly initialized.`);
71
77
  }
78
+ const PD_CONFIG_FILENAME = 'principles-disciple.json';
79
+ function loadWorkspaceFromPdConfigFile() {
80
+ const candidates = [
81
+ path.join(os.homedir(), '.openclaw', PD_CONFIG_FILENAME),
82
+ path.join(os.homedir(), '.principles', PD_CONFIG_FILENAME),
83
+ path.join(process.cwd(), PD_CONFIG_FILENAME),
84
+ ];
85
+ for (const configPath of candidates) {
86
+ if (!fs.existsSync(configPath))
87
+ continue;
88
+ try {
89
+ const raw = fs.readFileSync(configPath, 'utf8');
90
+ const parsed = JSON.parse(raw);
91
+ if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
92
+ if (Object.hasOwn(parsed, 'workspace')) {
93
+ const workspaceValue = parsed['workspace'];
94
+ if (typeof workspaceValue === 'string' && workspaceValue.trim()) {
95
+ return workspaceValue.trim();
96
+ }
97
+ }
98
+ }
99
+ }
100
+ catch {
101
+ continue;
102
+ }
103
+ }
104
+ return null;
105
+ }
106
+ export function resolveCanonicalWorkspaceDir() {
107
+ const pdEnv = process.env.PD_WORKSPACE_DIR;
108
+ if (pdEnv && pdEnv.trim()) {
109
+ const dir = path.resolve(pdEnv.trim());
110
+ if (!validateWorkspaceDir(dir)) {
111
+ return { workspaceDir: dir, source: 'pd_env' };
112
+ }
113
+ }
114
+ const ocEnv = process.env.OPENCLAW_WORKSPACE;
115
+ if (ocEnv && ocEnv.trim()) {
116
+ const dir = path.resolve(ocEnv.trim());
117
+ if (!validateWorkspaceDir(dir)) {
118
+ return { workspaceDir: dir, source: 'openclaw_env' };
119
+ }
120
+ }
121
+ const configWorkspace = loadWorkspaceFromPdConfigFile();
122
+ if (configWorkspace) {
123
+ const dir = path.resolve(configWorkspace);
124
+ if (!validateWorkspaceDir(dir)) {
125
+ return { workspaceDir: dir, source: 'pd_config' };
126
+ }
127
+ }
128
+ const defaultDir = path.join(os.homedir(), '.openclaw', 'workspace');
129
+ if (!validateWorkspaceDir(defaultDir)) {
130
+ return { workspaceDir: defaultDir, source: 'pd_default' };
131
+ }
132
+ return null;
133
+ }
134
+ /**
135
+ * Resolve only PD explicit sources (env vars + config file), excluding pd_default.
136
+ * Used by hook resolution to ensure ctx.workspaceDir takes priority over the
137
+ * hardcoded default fallback.
138
+ */
139
+ function resolveExplicitPdSources() {
140
+ const pdEnv = process.env.PD_WORKSPACE_DIR;
141
+ if (pdEnv && pdEnv.trim()) {
142
+ const dir = path.resolve(pdEnv.trim());
143
+ if (!validateWorkspaceDir(dir)) {
144
+ return { workspaceDir: dir, source: 'pd_env' };
145
+ }
146
+ }
147
+ const ocEnv = process.env.OPENCLAW_WORKSPACE;
148
+ if (ocEnv && ocEnv.trim()) {
149
+ const dir = path.resolve(ocEnv.trim());
150
+ if (!validateWorkspaceDir(dir)) {
151
+ return { workspaceDir: dir, source: 'openclaw_env' };
152
+ }
153
+ }
154
+ const configWorkspace = loadWorkspaceFromPdConfigFile();
155
+ if (configWorkspace) {
156
+ const dir = path.resolve(configWorkspace);
157
+ if (!validateWorkspaceDir(dir)) {
158
+ return { workspaceDir: dir, source: 'pd_config' };
159
+ }
160
+ }
161
+ return null;
162
+ }
163
+ function tryResolveFromOpenClawApi(api, agentId) {
164
+ try {
165
+ const resolved = api.runtime?.agent?.resolveAgentWorkspaceDir?.(api.config, agentId ?? 'main');
166
+ if (resolved && !validateWorkspaceDir(resolved)) {
167
+ return resolved;
168
+ }
169
+ }
170
+ catch {
171
+ // Fall through
172
+ }
173
+ return undefined;
174
+ }
175
+ export function resolveHookWorkspaceDir(ctx, api, source, options) {
176
+ // Priority 1: PD explicit sources (env vars + config file) — these are
177
+ // owner-declared and intentionally override the live session context.
178
+ const resolveExplicit = options?.explicitPdResolver ?? resolveExplicitPdSources;
179
+ const explicit = resolveExplicit();
180
+ if (explicit) {
181
+ let consistencyWarning;
182
+ if (ctx.workspaceDir) {
183
+ const normalizedCtx = path.resolve(ctx.workspaceDir);
184
+ const normalizedExplicit = path.resolve(explicit.workspaceDir);
185
+ if (normalizedCtx !== normalizedExplicit) {
186
+ consistencyWarning =
187
+ `PD explicit workspace (${explicit.source}: ${explicit.workspaceDir}) ` +
188
+ `differs from OpenClaw context (${ctx.workspaceDir}). Using PD explicit.`;
189
+ }
190
+ }
191
+ return {
192
+ ok: true,
193
+ workspaceDir: explicit.workspaceDir,
194
+ source: explicit.source,
195
+ consistencyWarning,
196
+ };
197
+ }
198
+ // Priority 2: OpenClaw live context — the real session workspace.
199
+ // This MUST take priority over pd_default (the hardcoded fallback).
200
+ if (ctx.workspaceDir) {
201
+ const issue = validateWorkspaceDir(ctx.workspaceDir);
202
+ if (!issue) {
203
+ return {
204
+ ok: true,
205
+ workspaceDir: ctx.workspaceDir,
206
+ source: 'openclaw_context',
207
+ };
208
+ }
209
+ }
210
+ // Priority 3: OpenClaw API resolution
211
+ const apiResolved = tryResolveFromOpenClawApi(api, ctx.agentId);
212
+ if (apiResolved) {
213
+ return {
214
+ ok: true,
215
+ workspaceDir: apiResolved,
216
+ source: 'openclaw_api',
217
+ };
218
+ }
219
+ // Priority 4: pd_default (hardcoded fallback) — only when nothing else works
220
+ const resolveCanonical = options?.canonicalResolver ?? resolveCanonicalWorkspaceDir;
221
+ const canonical = resolveCanonical();
222
+ if (canonical && canonical.source === 'pd_default') {
223
+ return {
224
+ ok: true,
225
+ workspaceDir: canonical.workspaceDir,
226
+ source: 'pd_default',
227
+ consistencyWarning: 'Using hardcoded default workspace (~/.openclaw/workspace). ' +
228
+ 'Set PD_WORKSPACE_DIR or create ~/.openclaw/principles-disciple.json for stable resolution.',
229
+ };
230
+ }
231
+ return {
232
+ ok: false,
233
+ reason: 'workspace_dir_unresolvable',
234
+ nextAction: 'Set PD_WORKSPACE_DIR environment variable, create ~/.openclaw/principles-disciple.json ' +
235
+ 'with a "workspace" field, or ensure OpenClaw provides workspaceDir in hook context.',
236
+ message: `[PD:${source}] Cannot resolve workspace directory from any source. ` +
237
+ `PD explicit config (PD_WORKSPACE_DIR, principles-disciple.json) ` +
238
+ `and OpenClaw fallback (ctx.workspaceDir, api.resolveAgentWorkspaceDir, ~/.openclaw/workspace) all failed.`,
239
+ };
240
+ }
72
241
  /**
73
242
  * Resolve workspace directory for tool hook execution (safe version).
74
243
  * Returns undefined instead of throwing if resolution fails.
244
+ *
245
+ * PRI-259: Uses PD canonical config as primary source, OpenClaw as fallback.
75
246
  */
76
- export function resolveToolHookWorkspaceDirSafe(ctx, api, source) {
77
- return resolveWorkspaceDir(api, ctx, { source });
247
+ export function resolveToolHookWorkspaceDirSafe(ctx, api, source, options) {
248
+ const result = resolveHookWorkspaceDir(ctx, api, source, options);
249
+ if (!result.ok) {
250
+ api.logger.warn(result.message);
251
+ return undefined;
252
+ }
253
+ if (result.consistencyWarning) {
254
+ api.logger.warn(`[PD:${source}] ${result.consistencyWarning}`);
255
+ }
256
+ return result.workspaceDir;
78
257
  }
79
258
  export class WorkspaceResolutionError extends Error {
80
259
  reason;