principles-disciple 1.71.0 → 1.73.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 (309) hide show
  1. package/openclaw.plugin.json +10 -5
  2. package/package.json +17 -19
  3. package/scripts/acceptance-test.mjs +16 -73
  4. package/scripts/sync-plugin.mjs +382 -77
  5. package/src/commands/archive-impl.ts +2 -1
  6. package/src/commands/capabilities.ts +2 -2
  7. package/src/commands/context.ts +2 -2
  8. package/src/commands/disable-impl.ts +2 -1
  9. package/src/commands/evolution-status.ts +16 -16
  10. package/src/commands/export.ts +12 -67
  11. package/src/commands/pain.ts +91 -1
  12. package/src/commands/principle-rollback.ts +2 -1
  13. package/src/commands/promote-impl.ts +7 -43
  14. package/src/commands/rollback-impl.ts +2 -1
  15. package/src/commands/rollback.ts +2 -1
  16. package/src/commands/samples.ts +2 -1
  17. package/src/commands/thinking-os.ts +2 -1
  18. package/src/config/errors.ts +18 -2
  19. package/src/constants/diagnostician.ts +2 -2
  20. package/src/constants/tools.ts +2 -1
  21. package/src/core/__tests__/focus-history.test.ts +210 -0
  22. package/src/core/config.ts +1 -1
  23. package/src/core/confirm-first-gate.ts +255 -0
  24. package/src/core/correction-cue-learner.ts +2 -136
  25. package/src/core/correction-types.ts +16 -88
  26. package/src/core/dictionary.ts +19 -20
  27. package/src/core/empathy-keyword-matcher.ts +17 -289
  28. package/src/core/empathy-types.ts +18 -229
  29. package/src/core/event-log.ts +38 -132
  30. package/src/core/evolution-reducer.ts +21 -2
  31. package/src/core/evolution-types.ts +76 -464
  32. package/src/core/file-store.ts +80 -0
  33. package/src/core/focus-history.ts +228 -955
  34. package/src/core/local-worker-routing.ts +34 -314
  35. package/src/core/merge-gate-audit.ts +0 -195
  36. package/src/core/pain-diagnostic-gate.ts +154 -0
  37. package/src/core/pain-signal.ts +21 -138
  38. package/src/core/pain.ts +15 -88
  39. package/src/core/pd-task-reconciler.ts +26 -115
  40. package/src/core/pd-task-service.ts +9 -9
  41. package/src/core/pd-task-types.ts +23 -127
  42. package/src/core/principle-compiler/__tests__/compiler-replay-gate.test.ts +174 -0
  43. package/src/core/principle-compiler/code-validator.ts +15 -42
  44. package/src/core/principle-compiler/compiler.ts +100 -15
  45. package/src/core/principle-compiler/index.ts +5 -2
  46. package/src/core/principle-compiler/template-generator.ts +4 -104
  47. package/src/core/principle-injection.ts +10 -202
  48. package/src/core/principle-internalization/filesystem-lifecycle-datasource.ts +42 -0
  49. package/src/core/principle-internalization/lifecycle-read-model.ts +39 -242
  50. package/src/core/principle-internalization/principle-lifecycle-service.ts +12 -10
  51. package/src/core/principle-tree-ledger-adapter.ts +145 -0
  52. package/src/core/principle-tree-ledger.ts +8 -6
  53. package/src/core/reflection/reflection-context.ts +14 -109
  54. package/src/core/replay-engine.ts +8 -500
  55. package/src/core/rule-host-helpers.ts +5 -35
  56. package/src/core/rule-host-types.ts +10 -82
  57. package/src/core/rule-host.ts +6 -63
  58. package/src/core/runtime-v2-prompt-activation-reader.ts +231 -0
  59. package/src/core/session-tracker.ts +87 -101
  60. package/src/core/shadow-observation-registry.ts +19 -48
  61. package/src/core/trajectory.ts +3 -1
  62. package/src/core/workflow-funnel-loader.ts +62 -68
  63. package/src/core/workspace-context.ts +46 -0
  64. package/src/core/workspace-dir-service.ts +1 -1
  65. package/src/core/workspace-dir-validation.ts +18 -9
  66. package/src/hooks/AGENTS.md +1 -1
  67. package/src/hooks/gate-block-helper.ts +46 -44
  68. package/src/hooks/gate.ts +207 -7
  69. package/src/hooks/lifecycle.ts +30 -32
  70. package/src/hooks/llm.ts +60 -32
  71. package/src/hooks/pain.ts +297 -103
  72. package/src/hooks/prompt.ts +469 -339
  73. package/src/hooks/subagent.ts +2 -29
  74. package/src/i18n/commands.ts +2 -10
  75. package/src/index.ts +95 -85
  76. package/src/openclaw-sdk.ts +311 -0
  77. package/src/service/central-database.ts +8 -4
  78. package/src/service/evolution-queue-migration.ts +2 -1
  79. package/src/service/evolution-worker.ts +163 -1786
  80. package/src/service/internalization-trigger-adapter.ts +302 -0
  81. package/src/service/keyword-optimization-service.ts +4 -4
  82. package/src/service/monitoring-query-service.ts +1 -215
  83. package/src/service/queue-io.ts +60 -331
  84. package/src/service/runtime-summary-service.ts +115 -18
  85. package/src/service/subagent-workflow/index.ts +0 -41
  86. package/src/service/subagent-workflow/types.ts +9 -120
  87. package/src/service/subagent-workflow/workflow-store.ts +2 -119
  88. package/src/service/workflow-watchdog.ts +0 -43
  89. package/src/types/event-payload.ts +16 -74
  90. package/src/types/event-types.ts +39 -547
  91. package/src/types/hygiene-types.ts +7 -30
  92. package/src/types/principle-tree-schema.ts +20 -222
  93. package/src/types/queue.ts +15 -70
  94. package/src/types/runtime-summary.ts +5 -49
  95. package/src/utils/io.ts +10 -0
  96. package/src/utils/retry.ts +1 -1
  97. package/src/utils/shadow-fingerprint.ts +2 -2
  98. package/src/utils/workspace-resolver.ts +50 -0
  99. package/templates/langs/en/core/AGENTS.md +2 -2
  100. package/templates/langs/en/core/BOOT.md +1 -1
  101. package/templates/langs/en/core/HEARTBEAT.md +2 -2
  102. package/templates/langs/en/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  103. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  104. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  105. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  106. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  107. package/templates/langs/en/skills/ai-sprint-orchestration/runtime/.gitignore +2 -2
  108. package/templates/langs/en/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  109. package/templates/langs/en/skills/evolve-task/SKILL.md +1 -1
  110. package/templates/langs/en/skills/pd-cli-operator/SKILL.md +67 -0
  111. package/templates/langs/en/skills/pd-diagnostician/SKILL.md +1 -1
  112. package/templates/langs/en/skills/pd-mentor/SKILL.md +1 -1
  113. package/templates/langs/en/skills/pd-pain-signal/SKILL.md +17 -39
  114. package/templates/langs/en/skills/pd-runtime-v2/SKILL.md +61 -0
  115. package/templates/langs/zh/core/AGENTS.md +2 -2
  116. package/templates/langs/zh/core/BOOT.md +1 -1
  117. package/templates/langs/zh/core/HEARTBEAT.md +2 -2
  118. package/templates/langs/zh/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  119. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  120. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  121. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +8 -8
  122. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  123. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  124. package/templates/langs/zh/skills/ai-sprint-orchestration/runtime/.gitignore +2 -2
  125. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  126. package/templates/langs/zh/skills/ai-sprint-orchestration/test/run.test.mjs +21 -5
  127. package/templates/langs/zh/skills/evolve-task/SKILL.md +2 -2
  128. package/templates/langs/zh/skills/pd-cli-operator/SKILL.md +67 -0
  129. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +1 -1
  130. package/templates/langs/zh/skills/pd-mentor/SKILL.md +1 -1
  131. package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +17 -38
  132. package/templates/langs/zh/skills/pd-runtime-v2/SKILL.md +61 -0
  133. package/tests/build-artifacts.test.ts +1 -3
  134. package/tests/commands/evolution-status.test.ts +0 -118
  135. package/tests/core/bootstrap-rules.test.ts +1 -1
  136. package/tests/core/config.test.ts +1 -1
  137. package/tests/core/event-log.test.ts +35 -0
  138. package/tests/core/evolution-engine.test.ts +610 -0
  139. package/tests/core/file-store.test.ts +102 -0
  140. package/tests/core/focus-history.test.ts +203 -11
  141. package/tests/core/merge-gate-audit.test.ts +2 -169
  142. package/tests/core/model-deployment-registry.test.ts +7 -1
  143. package/tests/core/model-training-registry.test.ts +19 -0
  144. package/tests/core/observability.test.ts +0 -1
  145. package/tests/core/pain-diagnostic-gate.test.ts +498 -0
  146. package/tests/core/pain.test.ts +0 -1
  147. package/tests/core/principle-internalization/deprecated-readiness.test.ts +2 -2
  148. package/tests/core/principle-internalization/lifecycle-metrics.test.ts +2 -2
  149. package/tests/core/principle-internalization/{internalization-routing-policy.test.ts → lifecycle-routing-policy.test.ts} +6 -6
  150. package/tests/core/principle-internalization/lineage-source-retired.test.ts +56 -0
  151. package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +1 -23
  152. package/tests/core/principle-tree-ledger-adapter.test.ts +253 -0
  153. package/tests/core/reflection-context.test.ts +0 -14
  154. package/tests/core/replay-engine.test.ts +127 -215
  155. package/tests/core/rule-host-helpers.test.ts +2 -2
  156. package/tests/core/rule-implementation-runtime.test.ts +0 -27
  157. package/tests/core/workflow-funnel-loader.test.ts +162 -0
  158. package/tests/core/workspace-dir-validation.test.ts +8 -1
  159. package/tests/core-anti-growth.test.ts +192 -0
  160. package/tests/hook-workspace-nextaction-contract.test.ts +42 -0
  161. package/tests/hooks/confirm-first-gate.test.ts +333 -0
  162. package/tests/hooks/gate-auto-correct-shadow.test.ts +310 -0
  163. package/tests/hooks/gate-auto-correct.test.ts +665 -0
  164. package/tests/hooks/gate-rule-host-pipeline.test.ts +2 -1
  165. package/tests/hooks/pain.test.ts +269 -12
  166. package/tests/hooks/prompt-characterization.test.ts +500 -0
  167. package/tests/hooks/prompt-size-guard.test.ts +329 -0
  168. package/tests/hooks/runtime-v2-prompt-activation.test.ts +869 -0
  169. package/tests/index.test.ts +94 -1
  170. package/tests/integration/auto-entry-gate.test.ts +248 -0
  171. package/tests/integration/internalization-trigger-guard.test.ts +69 -0
  172. package/tests/integration/m8-legacy-paths.test.ts +63 -0
  173. package/tests/integration/runtime-v2-pain-guard.test.ts +125 -0
  174. package/tests/plugin-config-resolution-cutover.test.ts +359 -0
  175. package/tests/runtime-v2-discovery-guard.test.ts +154 -0
  176. package/tests/service/central-database.test.ts +457 -0
  177. package/tests/service/evolution-worker.correction-observer.test.ts +173 -0
  178. package/tests/service/evolution-worker.timeout.test.ts +11 -129
  179. package/tests/service/internalization-trigger-adapter.test.ts +251 -0
  180. package/tests/service/monitoring-query-service.test.ts +1 -47
  181. package/tests/service/queue-io.test.ts +1 -62
  182. package/tests/service/runtime-summary-service.test.ts +184 -3
  183. package/tests/service/workflow-watchdog.test.ts +0 -91
  184. package/tests/utils/file-lock.test.ts +5 -3
  185. package/tests/utils/session-key.test.ts +52 -0
  186. package/tests/utils/subagent-probe.test.ts +48 -1
  187. package/vitest.config.ts +4 -11
  188. package/.planning/codebase/ARCHITECTURE.md +0 -157
  189. package/.planning/codebase/CONCERNS.md +0 -145
  190. package/.planning/codebase/CONVENTIONS.md +0 -148
  191. package/.planning/codebase/INTEGRATIONS.md +0 -81
  192. package/.planning/codebase/STACK.md +0 -87
  193. package/.planning/codebase/STRUCTURE.md +0 -193
  194. package/.planning/codebase/TESTING.md +0 -243
  195. package/.planning/phases/01-basic-visualization/01-GAP-CLOSURE-VERIFICATION.md +0 -113
  196. package/docs/COMMAND_REFERENCE.md +0 -76
  197. package/docs/COMMAND_REFERENCE_EN.md +0 -79
  198. package/scripts/build-web.mjs +0 -46
  199. package/scripts/diagnose-nocturnal.mjs +0 -537
  200. package/scripts/seed-nocturnal-scenarios.mjs +0 -384
  201. package/src/commands/nocturnal-review.ts +0 -322
  202. package/src/commands/nocturnal-rollout.ts +0 -790
  203. package/src/commands/nocturnal-train.ts +0 -986
  204. package/src/commands/pd-reflect.ts +0 -88
  205. package/src/core/adaptive-thresholds.ts +0 -478
  206. package/src/core/diagnostician-task-store.ts +0 -192
  207. package/src/core/nocturnal-arbiter.ts +0 -715
  208. package/src/core/nocturnal-artifact-lineage.ts +0 -116
  209. package/src/core/nocturnal-artificer.ts +0 -257
  210. package/src/core/nocturnal-candidate-scoring.ts +0 -530
  211. package/src/core/nocturnal-compliance.ts +0 -1146
  212. package/src/core/nocturnal-dataset.ts +0 -763
  213. package/src/core/nocturnal-executability.ts +0 -428
  214. package/src/core/nocturnal-export.ts +0 -499
  215. package/src/core/nocturnal-paths.ts +0 -240
  216. package/src/core/nocturnal-reasoning-deriver.ts +0 -343
  217. package/src/core/nocturnal-rule-implementation-validator.ts +0 -246
  218. package/src/core/nocturnal-snapshot-contract.ts +0 -99
  219. package/src/core/nocturnal-trajectory-extractor.ts +0 -512
  220. package/src/core/nocturnal-trinity-types.ts +0 -218
  221. package/src/core/nocturnal-trinity.ts +0 -2680
  222. package/src/core/principle-internalization/deprecated-readiness.ts +0 -93
  223. package/src/core/principle-internalization/internalization-routing-policy.ts +0 -208
  224. package/src/core/principle-internalization/lifecycle-metrics.ts +0 -152
  225. package/src/http/principles-console-route.ts +0 -709
  226. package/src/service/central-health-service.ts +0 -49
  227. package/src/service/central-overview-service.ts +0 -138
  228. package/src/service/control-ui-query-service.ts +0 -900
  229. package/src/service/cooldown-strategy.ts +0 -97
  230. package/src/service/evolution-pain-context.ts +0 -79
  231. package/src/service/evolution-query-service.ts +0 -407
  232. package/src/service/health-query-service.ts +0 -1038
  233. package/src/service/nocturnal-config.ts +0 -214
  234. package/src/service/nocturnal-runtime.ts +0 -734
  235. package/src/service/nocturnal-service.ts +0 -1605
  236. package/src/service/nocturnal-target-selector.ts +0 -545
  237. package/src/service/sleep-cycle.ts +0 -157
  238. package/src/service/startup-reconciler.ts +0 -112
  239. package/src/service/subagent-workflow/correction-observer-types.ts +0 -82
  240. package/src/service/subagent-workflow/correction-observer-workflow-manager.ts +0 -250
  241. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +0 -1
  242. package/src/service/subagent-workflow/dynamic-timeout.ts +0 -30
  243. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +0 -268
  244. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +0 -795
  245. package/src/service/subagent-workflow/runtime-direct-driver.ts +0 -268
  246. package/src/service/subagent-workflow/workflow-manager-base.ts +0 -580
  247. package/src/tools/write-pain-flag.ts +0 -215
  248. package/tests/commands/nocturnal-review.test.ts +0 -448
  249. package/tests/commands/nocturnal-train.test.ts +0 -97
  250. package/tests/commands/pd-reflect.test.ts +0 -49
  251. package/tests/core/adaptive-thresholds.test.ts +0 -261
  252. package/tests/core/nocturnal-arbiter.test.ts +0 -559
  253. package/tests/core/nocturnal-artifact-lineage.test.ts +0 -53
  254. package/tests/core/nocturnal-artificer.test.ts +0 -241
  255. package/tests/core/nocturnal-candidate-scoring.test.ts +0 -532
  256. package/tests/core/nocturnal-compliance-p-principles.test.ts +0 -133
  257. package/tests/core/nocturnal-compliance.test.ts +0 -646
  258. package/tests/core/nocturnal-dataset.test.ts +0 -892
  259. package/tests/core/nocturnal-e2e.test.ts +0 -234
  260. package/tests/core/nocturnal-executability.test.ts +0 -357
  261. package/tests/core/nocturnal-export.test.ts +0 -517
  262. package/tests/core/nocturnal-reasoning-deriver.test.ts +0 -372
  263. package/tests/core/nocturnal-reviewed-subset-comparison.test.ts +0 -428
  264. package/tests/core/nocturnal-rule-implementation-validator.test.ts +0 -127
  265. package/tests/core/nocturnal-snapshot-contract.test.ts +0 -121
  266. package/tests/core/nocturnal-trajectory-extractor.test.ts +0 -634
  267. package/tests/core/nocturnal-trinity.test.ts +0 -2053
  268. package/tests/core/pain-auto-repair.test.ts +0 -96
  269. package/tests/core/pain-integration.test.ts +0 -510
  270. package/tests/fixtures/nocturnal-reviewed-subset.json +0 -183
  271. package/tests/http/principles-console-route.test.ts +0 -162
  272. package/tests/integration/chaos-resilience.test.ts +0 -348
  273. package/tests/integration/empathy-workflow-integration.test.ts +0 -626
  274. package/tests/integration/pain-diagnostician-loop.e2e.test.ts +0 -380
  275. package/tests/service/control-ui-query-service.test.ts +0 -121
  276. package/tests/service/cooldown-strategy.test.ts +0 -164
  277. package/tests/service/data-endpoints-regression.test.ts +0 -834
  278. package/tests/service/empathy-observer-workflow-manager.test.ts +0 -175
  279. package/tests/service/evolution-worker.nocturnal.test.ts +0 -601
  280. package/tests/service/nocturnal-runtime-hardening.test.ts +0 -118
  281. package/tests/service/nocturnal-runtime.test.ts +0 -473
  282. package/tests/service/nocturnal-service-code-candidate.test.ts +0 -330
  283. package/tests/service/nocturnal-target-selector.test.ts +0 -615
  284. package/tests/service/startup-reconciler.test.ts +0 -148
  285. package/tests/tools/write-pain-flag.test.ts +0 -358
  286. package/ui/src/App.tsx +0 -45
  287. package/ui/src/api.ts +0 -220
  288. package/ui/src/charts.tsx +0 -955
  289. package/ui/src/components/ErrorState.tsx +0 -6
  290. package/ui/src/components/Loading.tsx +0 -13
  291. package/ui/src/components/ProtectedRoute.tsx +0 -12
  292. package/ui/src/components/Shell.tsx +0 -91
  293. package/ui/src/components/WorkspaceConfig.tsx +0 -178
  294. package/ui/src/components/index.ts +0 -5
  295. package/ui/src/context/auth.tsx +0 -80
  296. package/ui/src/context/theme.tsx +0 -66
  297. package/ui/src/hooks/useAutoRefresh.ts +0 -39
  298. package/ui/src/i18n/ui.ts +0 -473
  299. package/ui/src/main.tsx +0 -16
  300. package/ui/src/pages/EvolutionPage.tsx +0 -333
  301. package/ui/src/pages/FeedbackPage.tsx +0 -138
  302. package/ui/src/pages/GateMonitorPage.tsx +0 -136
  303. package/ui/src/pages/LoginPage.tsx +0 -89
  304. package/ui/src/pages/OverviewPage.tsx +0 -599
  305. package/ui/src/pages/SamplesPage.tsx +0 -174
  306. package/ui/src/pages/ThinkingModelsPage.tsx +0 -702
  307. package/ui/src/styles.css +0 -2020
  308. package/ui/src/types.ts +0 -384
  309. package/ui/src/utils/format.ts +0 -15
@@ -1,4 +1,4 @@
1
- 
1
+
2
2
 
3
3
  import * as fs from 'fs';
4
4
  import * as path from 'path';
@@ -8,33 +8,29 @@ import { WorkspaceContext } from '../core/workspace-context.js';
8
8
  import type { ContextInjectionConfig} from '../types.js';
9
9
  import { defaultContextConfig } from '../types.js';
10
10
  import { classifyTask, type RoutingInput } from '../core/local-worker-routing.js';
11
+ import { detectApprovalMarker, setConfirmFirstApproval, setConfirmFirstDirective, hydrateFromStore, pruneStoreStaleRows, setConfirmFirstStore, resetConfirmFirst } from '../core/confirm-first-gate.js';
11
12
  import { extractSummary, getHistoryVersions, parseWorkingMemorySection, workingMemoryToInjection, autoCompressFocus, safeReadCurrentFocus } from '../core/focus-history.js';
12
- import { EmpathyObserverWorkflowManager, empathyObserverWorkflowSpec, isExpectedSubagentError } from '../service/subagent-workflow/index.js';
13
13
  import { PathResolver } from '../core/path-resolver.js';
14
14
  import { selectPrinciplesForInjection, DEFAULT_PRINCIPLE_BUDGET } from '../core/principle-injection.js';
15
- import { isSubagentRuntimeAvailable } from '../utils/subagent-probe.js';
16
- import { getPendingDiagnosticianTasks } from '../core/diagnostician-task-store.js';
15
+ import { getCachedMaskedPrincipleSet, WorkflowFunnelLoader, PiAiRuntimeAdapter, EmpathyObserver, AgentScheduler, SqliteConfirmFirstStateStore, SqliteConnection } from '@principles/core/runtime-v2';
16
+ import { truncateInjectionToBudget } from '@principles/core/prompt-builder';
17
+ import { PromptActivationReader, RUNTIME_V2_PRINCIPLE_BUDGET } from '../core/runtime-v2-prompt-activation-reader.js';
17
18
  import {
18
19
  matchEmpathyKeywords,
19
20
  loadKeywordStore,
20
21
  saveKeywordStore,
21
- shouldTriggerOptimization,
22
22
  getKeywordStoreSummary,
23
23
  } from '../core/empathy-keyword-matcher.js';
24
24
  import { severityToPenalty, DEFAULT_EMPATHY_KEYWORD_CONFIG } from '../core/empathy-types.js';
25
+ import { evaluatePainDiagnosticGate } from '../core/pain-diagnostic-gate.js';
26
+ import { emitPainDetectedEvent, buildTrajectoryEvidence } from './pain.js';
25
27
  import { CorrectionCueLearner } from '../core/correction-cue-learner.js';
26
- import { EventLogService } from '../core/event-log.js';
27
- import type { PluginRuntimeSubagent } from '../service/subagent-workflow/runtime-direct-driver.js';
28
-
29
- /**
30
- * Type assertion: OpenClaw SDK subagent -> workflow manager subagent type.
31
- * Both types are structurally identical but come from different import paths.
32
- */
33
- function toWorkflowSubagent(
34
- subagent: NonNullable<OpenClawPluginApi['runtime']>['subagent']
35
- ): PluginRuntimeSubagent {
36
- return subagent as unknown as PluginRuntimeSubagent;
37
- }
28
+ import {
29
+ buildAttitudeDirective,
30
+ detectCorrectionCue as coreDetectCorrectionCue,
31
+ extractMessageContent,
32
+ isMinimalTrigger,
33
+ } from '@principles/core/prompt-builder';
38
34
 
39
35
  // ---------------------------------------------------------------------------
40
36
  // Static file cache — avoids re-reading rarely-changing files every message
@@ -81,6 +77,7 @@ function cachedReadFile(filePath: string): string {
81
77
  // Module-level empathy state — shared across calls to avoid per-turn I/O
82
78
  let _empathyTurnCounter = 0;
83
79
  let _empathyKeywordCache: { store: ReturnType<typeof loadKeywordStore>; lang: string } | null = null;
80
+ let _confirmFirstHydrationCounter = 0;
84
81
 
85
82
  /**
86
83
  * Model configuration with primary model and optional fallback models
@@ -129,43 +126,7 @@ interface PromptHookApi {
129
126
  }
130
127
 
131
128
  function getTextContent(message: unknown): string {
132
- if (!message || typeof message !== 'object') return '';
133
- const record = message as { content?: unknown };
134
- if (typeof record.content === 'string') return record.content;
135
- if (Array.isArray(record.content)) {
136
- return record.content
137
- .filter((part: unknown) => part && typeof part === 'object' && (part as { type?: unknown }).type === 'text')
138
- .map((part) => String((part as { text?: unknown }).text ?? ''))
139
- .join('\n')
140
- .trim();
141
- }
142
- return '';
143
- }
144
-
145
- function detectCorrectionCue(text: string): string | null {
146
- const normalized = text
147
- .trim()
148
- .toLowerCase()
149
- .replace(/[.,!?;:,。!?;:]/g, '');
150
- const cues = [
151
- '不是这个',
152
- '不对',
153
- '错了',
154
- '搞错了',
155
- '理解错了',
156
- '你理解错了',
157
- '重新来',
158
- '再试一次',
159
- 'you are wrong',
160
- 'wrong file',
161
- 'not this',
162
- 'redo',
163
- 'try again',
164
- 'again',
165
- 'please redo',
166
- 'please try again',
167
- ];
168
- return cues.find((cue) => normalized.includes(cue)) ?? null;
129
+ return extractMessageContent(message);
169
130
  }
170
131
 
171
132
  /**
@@ -297,64 +258,25 @@ export function getDiagnosticianModel(api: PromptHookApi | null, logger?: Plugin
297
258
  /**
298
259
  * Extract recent user messages for keyword optimization context.
299
260
  */
300
- function extractRecentMessages(messages: unknown[] | undefined, limit: number): string[] {
301
- if (!Array.isArray(messages)) return [];
302
- const userMessages: string[] = [];
303
-
304
- for (let i = messages.length - 1; i >= 0 && userMessages.length < limit; i--) {
305
- const msg = messages[i] as { role?: string; content?: unknown };
306
- if (msg?.role !== 'user') continue;
307
-
308
- let text = '';
309
- if (typeof msg.content === 'string') {
310
- text = msg.content;
311
- } else if (Array.isArray(msg.content)) {
312
- text = msg.content
313
- .filter((part: unknown) => part && typeof part === 'object' && (part as { type?: string }).type === 'text' && typeof (part as { text?: unknown }).text === 'string')
314
- .map((part: unknown) => (part as { text: string }).text)
315
- .join('\n')
316
- .trim();
317
- }
318
- if (text) userMessages.unshift(text.substring(0, 500));
319
- }
320
-
321
- return userMessages;
322
- }
261
+
323
262
 
324
263
  /**
325
264
  * Build prompt for keyword optimization subagent.
326
265
  */
327
- function buildOptimizationPrompt(
328
- keywordStore: ReturnType<typeof loadKeywordStore>,
329
- recentMessages: string[],
330
- ): string {
331
- const currentTerms = Object.entries(keywordStore.terms)
332
- .map(([term, entry]) => ` - "${term}": weight=${entry.weight}, hits=${entry.hitCount || 0}, fp_rate=${entry.falsePositiveRate?.toFixed(2) || '0.10'}`)
333
- .join('\n');
334
-
335
- return `You are an empathy keyword optimizer.
336
-
337
- ## TASK
338
- Analyze recent user messages and the current empathy keyword store.
339
- Return STRICT JSON (no markdown):
340
-
341
- {"updates": {"TERM": {"action": "add|update|remove", "weight": number, "falsePositiveRate": number, "reasoning": "string"}}}
342
-
343
- ## Current Keyword Store (${Object.keys(keywordStore.terms).length} terms):
344
- ${currentTerms}
345
-
346
- ## Recent User Messages (${recentMessages.length} messages):
347
- ${recentMessages.map((m, i) => `${i + 1}. "${m}"`).join('\n')}
348
-
349
- ## Rules:
350
- - ADD: If a message contains frustration/empathy signals not in current terms
351
- - UPDATE: If a term's weight should change (high hits → increase weight, low hits → decrease)
352
- - REMOVE: If a term has 0 hits after many turns AND high false positive rate (>0.3)
353
- - Keep reasoning concise (max 100 chars)
354
- - Weight range: 0.1-0.9
355
- - falsePositiveRate range: 0.05-0.5
356
- `;
266
+
267
+
268
+ function ensureConfirmFirstStore(workspaceDir: string): void {
269
+ if (!_confirmFirstStoreInitialized) {
270
+ try {
271
+ const connection = new SqliteConnection({ workspaceDir, readonly: false });
272
+ setConfirmFirstStore(new SqliteConfirmFirstStateStore(connection));
273
+ _confirmFirstStoreInitialized = true;
274
+ } catch (err) {
275
+ console.warn(`[PD:ConfirmFirst] Failed to initialize store: ${String(err)}`);
276
+ }
277
+ }
357
278
  }
279
+ let _confirmFirstStoreInitialized = false;
358
280
 
359
281
  export async function handleBeforePromptBuild(
360
282
  event: PluginHookBeforePromptBuildEvent,
@@ -364,23 +286,29 @@ export async function handleBeforePromptBuild(
364
286
  const logger = ctx.api?.logger;
365
287
  logger?.info?.(`[PD:Prompt] handleBeforePromptBuild called: workspaceDir=${!!workspaceDir}, trigger=${ctx.trigger}, sessionId=${ctx.sessionId?.substring(0, 20)}`);
366
288
  if (!workspaceDir) {
367
- logger?.warn?.(`[PD:Prompt] workspaceDir is missing — skipping empathy processing`);
289
+ logger?.warn?.(`[PD:Prompt] workspaceDir is missing — skipping PD context injection`);
368
290
  return;
369
291
  }
370
292
 
371
- // ──── DEBUG: Verify subagent availability in this context ────
372
- const subagent = ctx.api?.runtime?.subagent;
373
- logger?.info?.(`[PD:DEBUG:SubagentCheck] trigger=${ctx.trigger}, subagent_exists=${!!subagent}, subagent.run_exists=${!!subagent?.run}`);
374
- if (subagent?.run) {
375
- logger?.info?.('[PD:DEBUG:SubagentCheck] run entrypoint is callable');
376
- }
377
-
378
293
  const wctx = WorkspaceContext.fromHookContext(ctx);
379
- const { trigger, sessionId, api } = ctx;
294
+ const { trigger, sessionId } = ctx as { trigger: string | undefined; sessionId: string | undefined };
295
+ const api = ctx.api;
380
296
  if (sessionId) {
381
297
  wctx.trajectory?.recordSession?.({ sessionId });
382
298
  }
383
299
 
300
+ if (sessionId) {
301
+ ensureConfirmFirstStore(workspaceDir);
302
+ hydrateFromStore(sessionId);
303
+ _confirmFirstHydrationCounter++;
304
+ if (_confirmFirstHydrationCounter % 100 === 0) {
305
+ const pruned = pruneStoreStaleRows();
306
+ if (pruned > 0) {
307
+ logger?.info?.(`[PD:ConfirmFirst] Pruned ${pruned} stale rows from confirm_first_state`);
308
+ }
309
+ }
310
+ }
311
+
384
312
  if (sessionId && trigger === 'user' && Array.isArray(event.messages) && event.messages.length > 0) {
385
313
  const latestUserIndex = [...event.messages]
386
314
  .map((message, index) => ({ message, index }))
@@ -389,6 +317,26 @@ export async function handleBeforePromptBuild(
389
317
 
390
318
  if (latestUserIndex) {
391
319
  const userText = getTextContent(latestUserIndex.message);
320
+
321
+ // ── Confirm-first approval detection ──
322
+ // If user sends approval language, mark session as approved for confirm-first gate
323
+ if (sessionId && detectApprovalMarker(userText)) {
324
+ setConfirmFirstApproval(sessionId);
325
+ // P2: Emit approval telemetry for observability (ERR-002)
326
+ try {
327
+ wctx.eventLog.recordConfirmFirstGateApproved({
328
+ sessionId,
329
+ workspaceDir: wctx.workspaceDir,
330
+ toolName: '(approval)',
331
+ reason: 'user_approval_detected',
332
+ principleId: 'confirm-first',
333
+ nextAction: 'mutating tools now permitted',
334
+ });
335
+ } catch (logErr) {
336
+ logger?.warn?.(`[PD:ConfirmFirst] Failed to emit approval event: ${String(logErr)}`);
337
+ }
338
+ }
339
+
392
340
  // Use CorrectionCueLearner for detection — supports learned keywords, not just hardcoded list
393
341
  let correctionCue: string | null = null;
394
342
  try {
@@ -406,7 +354,7 @@ export async function handleBeforePromptBuild(
406
354
  }
407
355
  } catch (learnerErr) {
408
356
  // Fallback to hardcoded detection if learner fails — log for observability
409
- correctionCue = detectCorrectionCue(userText);
357
+ correctionCue = coreDetectCorrectionCue(userText);
410
358
  logger?.warn?.(`[PD:Prompt] CorrectionCueLearner.match() failed (${String(learnerErr)}), fallback=${correctionCue ? `matched="${correctionCue}"` : 'no-match'}`);
411
359
  }
412
360
  let referencesAssistantTurnId: number | null = null;
@@ -432,10 +380,10 @@ export async function handleBeforePromptBuild(
432
380
  }
433
381
 
434
382
  // Load context injection configuration
435
- const contextConfig = loadContextInjectionConfig(workspaceDir);
383
+ const contextConfig = loadContextInjectionConfig(wctx.workspaceDir);
436
384
 
437
385
  // Minimal mode: heartbeat and subagents skip most context to reduce tokens
438
- const isMinimalMode = trigger === "heartbeat" || trigger === "cron" || sessionId?.includes(":subagent:") === true;
386
+ const isMinimalMode = isMinimalTrigger(trigger as string | undefined, sessionId as string | undefined);
439
387
 
440
388
  const session = sessionId ? getSession(sessionId) : undefined;
441
389
 
@@ -449,6 +397,8 @@ export async function handleBeforePromptBuild(
449
397
  let prependSystemContext: string;
450
398
  let prependContext = '';
451
399
  let appendSystemContext = '';
400
+ // Tracks pending diagnostician task count for diagnostician-priority mode in size guard
401
+ let pendingDiagTaskCount = 0;
452
402
 
453
403
  // ──── 0. Manual Pain Clearance ────
454
404
  if (trigger === 'user' && sessionId && session && session.currentGfi >= 100) {
@@ -533,7 +483,9 @@ The empathy observer subagent handles pain detection independently.
533
483
 
534
484
  const isUserInteraction = trigger === 'user' || trigger === 'api' || !trigger;
535
485
 
486
+ // Empathy Observer: keyword fast-path + optional LLM deep analysis (zero latency async dispatch)
536
487
  const empathyEnabled = wctx.config.get('empathy_engine.enabled') !== false;
488
+
537
489
  logger?.info?.(`[PD:Empathy] Conditions: enabled=${empathyEnabled}, isUser=${isUserInteraction}, sessionId=${!!sessionId}, api=${!!api}, !agentToAgent=${!isAgentToAgent}, workspaceDir=${!!workspaceDir}, hasMessage=${!!latestUserMessage}`);
538
490
 
539
491
  // Track if we should inject behavioral constraints (will be added to appendSystemContext later)
@@ -562,23 +514,6 @@ The empathy observer subagent handles pain detection independently.
562
514
  _empathyTurnCounter++;
563
515
  const turnCount = _empathyTurnCounter;
564
516
 
565
- // Decision: should we call subagent?
566
- let shouldCallSubagent = false;
567
- let samplingReason = '';
568
-
569
- if (matchResult.score >= 0.8) {
570
- // High confidence — keyword match is reliable, no subagent needed
571
- shouldCallSubagent = false;
572
- } else if (matchResult.score >= 0.3) {
573
- // Boundary case — 30% sampling for subagent verification
574
- shouldCallSubagent = Math.random() < 0.3;
575
- samplingReason = 'boundary_verification';
576
- } else {
577
- // No keyword hit — 5% random sampling to discover new expressions
578
- shouldCallSubagent = Math.random() < 0.05;
579
- samplingReason = 'random_discovery';
580
- }
581
-
582
517
  if (matchResult.matched) {
583
518
  const penalty = severityToPenalty(matchResult.severity, DEFAULT_EMPATHY_KEYWORD_CONFIG);
584
519
  // trackFriction signature: (sessionId, deltaF: number, hash: string, workspaceDir?, options?)
@@ -586,93 +521,194 @@ The empathy observer subagent handles pain detection independently.
586
521
  source: 'user_empathy',
587
522
  });
588
523
 
589
- logger?.info?.(`[PD:Empathy] MATCH: "${matchResult.matchedTerms.join(', ')}" → severity=${matchResult.severity}, score=${matchResult.score.toFixed(2)}, penalty=${penalty}, subagent=${shouldCallSubagent ? samplingReason : 'skipped(' + (matchResult.score >= 0.3 ? 'boundary_sampling' : 'random_discovery') + ')'}`);
590
- } else {
591
- // Log unmatched messages periodically for coverage analysis
592
- if (turnCount > 0 && turnCount % 50 === 0) {
593
- const sampleMsg = latestUserMessage.substring(0, 80).replace(/\n/g, ' ');
594
- logger?.debug?.(`[PD:Empathy] NO_MATCH: "${sampleMsg}" (turn ${turnCount}, keywords_in_store=${Object.keys(keywordStore.terms).length})`);
595
- }
596
- }
597
-
598
- // Trigger subagent for sampling cases (Finding #1: use shared manager to avoid leaks)
599
- const runtimeSubagent =
600
- isSubagentRuntimeAvailable(api?.runtime?.subagent)
601
- ? api.runtime.subagent
602
- : undefined;
603
-
604
- if (shouldCallSubagent && runtimeSubagent) {
605
- logger?.info?.(`[PD:Empathy] SUBAGENT_SAMPLE: reason=${samplingReason}, score=${matchResult.score.toFixed(2)}, matched=[${matchResult.matchedTerms.join(',')}]`);
606
-
607
- // EmpathyObserverWorkflowManager auto-finalizes via wait poll mechanism.
608
- // Create a fresh manager per invocation to ensure clean state.
609
- const empathyManager = new EmpathyObserverWorkflowManager({
610
- workspaceDir,
611
- logger: api.logger ?? console,
612
-
613
- subagent: toWorkflowSubagent(runtimeSubagent),
614
- });
615
- empathyManager.startWorkflow(empathyObserverWorkflowSpec, {
616
- parentSessionId: sessionId,
617
- workspaceDir,
618
- taskInput: latestUserMessage,
619
- }).catch((err) => {
620
- if (!isExpectedSubagentError(err)) {
621
- api.logger?.warn?.(`[PD:Empathy] subagent sample failed: ${String(err)}`);
622
- }
623
- });
624
- }
524
+ logger?.info?.(`[PD:Empathy] MATCH: "${matchResult.matchedTerms.join(', ')}" → severity=${matchResult.severity}, score=${matchResult.score.toFixed(2)}, penalty=${penalty}`);
525
+
526
+ const currentSession = getSession(sessionId);
527
+ const currentGfi = currentSession?.currentGfi ?? 0;
528
+ const painTrigger = wctx.config.get('thresholds.pain_trigger') || 40;
529
+ const highGfiThreshold = Math.max(wctx.config.get('severity_thresholds.high') || 70, painTrigger + 30);
530
+
531
+ if (currentGfi >= highGfiThreshold) {
532
+ const gfiPainScore = Math.min(Math.round(currentGfi), 60);
533
+ logger?.info?.(`[PD:Empathy] GFI-TRIGGERED: currentGfi=${currentGfi.toFixed(1)} >= highGfi=${highGfiThreshold}, emitting pain signal (score=${gfiPainScore})`);
534
+
535
+ const gate = evaluatePainDiagnosticGate({
536
+ source: 'user_empathy',
537
+ score: gfiPainScore,
538
+ currentGfi,
539
+ consecutiveErrors: currentSession?.consecutiveErrors ?? 0,
540
+ sessionId,
541
+ errorHash: 'empathy_gfi_threshold',
542
+ thresholds: {
543
+ painTrigger,
544
+ highSeverity: wctx.config.get('severity_thresholds.high') || 70,
545
+ semanticPain: Math.max(painTrigger, 60),
546
+ },
547
+ });
625
548
 
626
- // Helper: build summary string (Finding #2: avoid duplication)
627
- const buildSummary = (): string => {
628
- const s = getKeywordStoreSummary(keywordStore);
629
- const highFP = s.highFalsePositiveTerms.slice(0, 5).map(t => `${t.term}(${t.falsePositiveRate.toFixed(2)})`).join(', ');
630
- return `SUMMARY(turn=${turnCount}): terms=${s.totalTerms}, hits=${keywordStore.stats.totalHits}, zero_hit=${s.totalTerms - (s.seedTerms + s.discoveredTerms)}, high_fp=[${highFP}]`;
631
- };
549
+ wctx.eventLog.recordPainSignal(sessionId, {
550
+ score: gfiPainScore,
551
+ source: 'user_empathy',
552
+ reason: `Accumulated GFI (${currentGfi.toFixed(1)}) crossed highGfi threshold (${highGfiThreshold}). Matched: ${matchResult.matchedTerms.join(', ')}`,
553
+ isRisky: false,
554
+ });
632
555
 
633
- // Check if keyword optimization should be triggered
634
- if (shouldTriggerOptimization(keywordStore, turnCount)) {
635
- logger?.info?.(`[PD:Empathy] OPTIMIZATION_TRIGGER: turns=${turnCount}, last_optimized=${keywordStore.lastOptimizedAt}`);
636
- logger?.info?.(`[PD:Empathy] STATS: ${buildSummary()}`);
556
+ if (gate.shouldDiagnose) {
557
+ logger?.info?.(`[PD:Empathy] Gate approved, calling emitPainDetectedEvent...`);
558
+ try {
559
+ const evidence = buildTrajectoryEvidence(wctx, sessionId);
560
+ await emitPainDetectedEvent(wctx, {
561
+ ts: new Date().toISOString(),
562
+ type: 'pain_detected',
563
+ data: {
564
+ painId: `empathy_gfi_${Date.now()}`,
565
+ painType: 'user_frustration',
566
+ source: 'user_empathy',
567
+ reason: `Accumulated GFI (${currentGfi.toFixed(1)}) crossed highGfi threshold (${highGfiThreshold}). Matched: ${matchResult.matchedTerms.join(', ')}`,
568
+ score: gfiPainScore,
569
+ sessionId,
570
+ agentId: 'main',
571
+ provenance: 'openclaw_context_bound',
572
+ evidence,
573
+ },
574
+ });
575
+ logger?.info?.(`[PD:Empathy] emitPainDetectedEvent completed (GFI-triggered)`);
576
+ } catch (emitErr) {
577
+ console.error(`[PD:Empathy] FAILED to emit GFI-triggered pain event: ${String(emitErr)}`);
578
+ logger?.warn?.(`[PD:Empathy] Failed to emit GFI-triggered pain event: ${String(emitErr)}`);
579
+ }
580
+ } else {
581
+ logger?.info?.(`[PD:Empathy] GFI-triggered gate rejected: ${gate.detail}`);
582
+ }
583
+ }
637
584
 
638
- // Start keyword optimization subagent to update weights and discover new terms
639
- try {
640
- const recentMessages = extractRecentMessages(event.messages, 10);
641
- const optimizationPrompt = buildOptimizationPrompt(keywordStore, recentMessages);
642
-
643
- logger?.info?.(`[PD:Empathy] Starting optimization subagent with ${recentMessages.length} recent messages`);
644
-
645
- const empathyManager = new EmpathyObserverWorkflowManager({
646
- workspaceDir,
647
- logger: api.logger ?? console,
648
-
649
- subagent: toWorkflowSubagent(api.runtime.subagent),
585
+ // Trigger asynchronous background Empathy Observer deep analysis (Zero Latency)
586
+ const observer = resolveEmpathyObserver(wctx, logger);
587
+ if (observer) {
588
+ const scheduler = new AgentScheduler();
589
+ scheduler.register({
590
+ agentId: 'empathy-observer',
591
+ mode: 'realtime',
592
+ runner: observer,
650
593
  });
594
+
595
+ logger?.info?.(`[PD:Empathy] Triggering background Empathy Observer deep analysis for message: "${msgPreview}"`);
651
596
 
652
- empathyManager.startWorkflow(empathyObserverWorkflowSpec, {
653
- parentSessionId: sessionId,
654
- workspaceDir,
655
- taskInput: { prompt: optimizationPrompt },
656
- }).catch((err) => {
657
- if (!isExpectedSubagentError(err)) {
658
- api.logger?.warn?.(`[PD:Empathy] optimization subagent failed: ${String(err)}`);
659
- }
660
- });
661
- } catch (optErr) {
662
- if (!isExpectedSubagentError(optErr)) {
663
- logger?.warn?.(`[PD:Empathy] Failed to start optimization subagent: ${String(optErr)}`);
664
- }
597
+ void scheduler.dispatch('empathy-observer', { userMessage: latestUserMessage })
598
+ .then(async (result) => {
599
+ if (result.damageDetected) {
600
+ logger?.info?.(`[PD:Empathy] Background Empathy Observer detected damage. Severity: ${result.severity}, Reason: ${result.reason}`);
601
+
602
+ // ── Persistence Contract ──
603
+ const painScore = scoreFromSeverityForSpec(result.severity, wctx);
604
+
605
+ trackFriction(
606
+ sessionId,
607
+ painScore,
608
+ `observer_empathy_${result.severity}`,
609
+ workspaceDir,
610
+ { source: 'user_empathy' }
611
+ );
612
+
613
+ const eventId = `emp_obs_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
614
+ wctx.eventLog.recordPainSignal(sessionId, {
615
+ score: painScore,
616
+ source: 'user_empathy',
617
+ reason: result.reason || 'Empathy observer detected likely user frustration.',
618
+ isRisky: false,
619
+ origin: 'system_infer',
620
+ severity: result.severity,
621
+ confidence: result.confidence,
622
+ detection_mode: 'structured',
623
+ deduped: false,
624
+ trigger_text_excerpt: latestUserMessage.substring(0, 120),
625
+ raw_score: painScore,
626
+ calibrated_score: painScore,
627
+ eventId,
628
+ });
629
+
630
+ try {
631
+ wctx.trajectory?.recordPainEvent?.({
632
+ sessionId,
633
+ source: 'user_empathy',
634
+ score: painScore,
635
+ reason: result.reason || 'Empathy observer detected likely user frustration.',
636
+ severity: result.severity,
637
+ origin: 'system_infer',
638
+ confidence: result.confidence,
639
+ text: latestUserMessage,
640
+ });
641
+ } catch (error) {
642
+ logger?.warn?.(`[PD:Empathy] Failed to persist trajectory: ${String(error)}`);
643
+ }
644
+
645
+ // Check if GFI triggers a pain event post-LLM validation
646
+ const freshSession = getSession(sessionId);
647
+ const freshGfi = freshSession?.currentGfi ?? 0;
648
+ if (freshGfi >= highGfiThreshold) {
649
+ const freshGfiPainScore = Math.min(Math.round(freshGfi), 60);
650
+ const gate = evaluatePainDiagnosticGate({
651
+ source: 'user_empathy',
652
+ score: freshGfiPainScore,
653
+ currentGfi: freshGfi,
654
+ consecutiveErrors: freshSession?.consecutiveErrors ?? 0,
655
+ sessionId,
656
+ errorHash: 'empathy_gfi_threshold',
657
+ thresholds: {
658
+ painTrigger,
659
+ highSeverity: wctx.config.get('severity_thresholds.high') || 70,
660
+ semanticPain: Math.max(painTrigger, 60),
661
+ },
662
+ });
663
+
664
+ if (gate.shouldDiagnose) {
665
+ logger?.info?.(`[PD:Empathy] GFI threshold crossed after background observer. Emitting pain signal...`);
666
+ try {
667
+ const evidence = buildTrajectoryEvidence(wctx, sessionId);
668
+ await emitPainDetectedEvent(wctx, {
669
+ ts: new Date().toISOString(),
670
+ type: 'pain_detected',
671
+ data: {
672
+ painId: `empathy_gfi_${Date.now()}`,
673
+ painType: 'user_frustration',
674
+ source: 'user_empathy',
675
+ reason: `Accumulated GFI (${freshGfi.toFixed(1)}) crossed highGfi threshold (${highGfiThreshold}). Verified by Empathy Observer.`,
676
+ score: freshGfiPainScore,
677
+ sessionId,
678
+ agentId: 'main',
679
+ provenance: 'openclaw_context_bound',
680
+ evidence,
681
+ },
682
+ });
683
+ } catch (emitErr) {
684
+ logger?.error?.(`[PD:Empathy] FAILED to emit observer-triggered pain event: ${String(emitErr)}`);
685
+ }
686
+ }
687
+ }
688
+ } else {
689
+ logger?.info?.(`[PD:Empathy] Background Empathy Observer did not detect any damage.`);
690
+ }
691
+ })
692
+ .catch((err) => {
693
+ logger?.warn?.(`[PD:Empathy] Background analysis failed or rejected: ${String(err)}`);
694
+ });
695
+ }
696
+ } else {
697
+ // Log unmatched messages periodically for coverage analysis
698
+ if (turnCount > 0 && turnCount % 50 === 0) {
699
+ const sampleMsg = latestUserMessage.substring(0, 80).replace(/\n/g, ' ');
700
+ logger?.debug?.(`[PD:Empathy] NO_MATCH: "${sampleMsg}" (turn ${turnCount}, keywords_in_store=${Object.keys(keywordStore.terms).length})`);
665
701
  }
666
702
  }
667
703
 
668
704
  // Periodic summary (every 100 turns)
669
705
  if (turnCount > 0 && turnCount % 100 === 0) {
670
- logger?.info?.(`[PD:Empathy] ${buildSummary()}`);
706
+ const s = getKeywordStoreSummary(keywordStore);
707
+ const highFP = s.highFalsePositiveTerms.slice(0, 5).map(t => `${t.term}(${t.falsePositiveRate.toFixed(2)})`).join(', ');
708
+ logger?.info?.(`[PD:Empathy] SUMMARY(turn=${turnCount}): terms=${s.totalTerms}, hits=${keywordStore.stats.totalHits}, zero_hit=${s.totalTerms - (s.seedTerms + s.discoveredTerms)}, high_fp=[${highFP}]`);
671
709
  }
672
710
 
673
- // Save keyword store on every match to prevent data loss on restart.
674
- // Previously used turnCount % 50 gate which caused hitCount loss because
675
- // module-level state resets on plugin reload before reaching turn 50.
711
+ // Save keyword store on every match
676
712
  if (matchResult.matched) {
677
713
  saveKeywordStore(wctx.stateDir, keywordStore);
678
714
  const {totalHits} = keywordStore.stats;
@@ -683,21 +719,6 @@ The empathy observer subagent handles pain detection independently.
683
719
  }
684
720
  }
685
721
 
686
- // Empathy Observer: analyze user message for frustration signals (legacy, disabled)
687
- // The keyword matching approach above is now the primary empathy detection method.
688
- // The subagent-based observer is kept for periodic keyword optimization only.
689
- // if (workspaceDir) {
690
- // const empathyManager = new EmpathyObserverWorkflowManager({
691
- // workspaceDir,
692
- // logger: api.logger,
693
- // subagent: api.runtime.subagent as any,
694
- // });
695
- // empathyManager.startWorkflow(empathyObserverWorkflowSpec, {
696
- // parentSessionId: sessionId,
697
- // workspaceDir,
698
- // taskInput: latestUserMessage,
699
- // }).catch((err) => api.logger.warn(`[PD:Empathy] workflow failed: ${String(err)}`));
700
- // }
701
722
  }
702
723
 
703
724
  // ──── 4. Heartbeat-specific checklist (also fires for cron-triggered sessions) ────
@@ -720,95 +741,18 @@ The empathy observer subagent handles pain detection independently.
720
741
  const heartbeatChecklist = fs.readFileSync(heartbeatPath, 'utf8');
721
742
  prependContext += `<heartbeat_checklist>
722
743
  ${heartbeatChecklist}
723
-
724
- // HEARTBEAT_OK removed - tasks must always be processed
725
744
  </heartbeat_checklist>\n`;
726
745
  } catch (e) {
727
746
  logger?.error(`[PD:Prompt] Failed to read HEARTBEAT: ${String(e)}`);
728
747
  }
729
748
  }
730
749
 
731
- // ──── 4b. Inject pending diagnostician tasks ────
732
- // FIX (#283): The evolution worker writes pain diagnosis tasks to
733
- // diagnostician_tasks.json. The heartbeat prompt hook must read and inject
734
- // them so the LLM (acting as diagnostician) can process them.
735
- try {
736
- const pendingTasks = getPendingDiagnosticianTasks(wctx.stateDir);
737
- if (pendingTasks.length > 0) {
738
- const taskBlocks = pendingTasks
739
- .slice(0, 3)
740
- .map(({ id, task }) => `<diagnostician_task id="${id}">\n${task.prompt}\n</diagnostician_task>`)
741
- .join('\n\n');
742
-
743
- const pendingCount = pendingTasks.length;
744
- const processingNote = pendingCount > 3
745
- ? `\n\nNOTE: ${pendingCount - 3} more tasks are queued. Process these 3 first; remaining tasks will be handled on subsequent heartbeats.`
746
- : '';
747
-
748
- prependContext += `<diagnostician_tasks pending="${pendingCount}">
749
- You are acting as a **Pain Diagnostician**. Process the following task(s) by:
750
- 1. Analyzing the pain signal and its context
751
- 2. Identifying the root cause and violated principles
752
- 3. Writing a completion marker file: .evolution_complete_<TASK_ID>
753
- 4. Writing a diagnostic report: .diagnostician_report_<TASK_ID>.json
754
-
755
- ${taskBlocks}${processingNote}
756
- </diagnostician_tasks>\n`;
757
-
758
- logger?.info?.(`[PD:Prompt] Injected ${Math.min(pendingCount, 3)}/${pendingCount} pending diagnostician task(s) into heartbeat prompt`);
759
-
760
- // C: Record heartbeat_diagnosis event for observability
761
- try {
762
- const eventLog = EventLogService.get(wctx.stateDir, logger);
763
- eventLog.recordHeartbeatDiagnosis({
764
- taskCount: pendingCount,
765
- taskIds: pendingTasks.slice(0, 3).map(t => t.id),
766
- trigger: 'heartbeat',
767
- });
768
- } catch (evErr) {
769
- logger?.warn?.(`[PD:Prompt] Failed to record heartbeat_diagnosis event: ${String(evErr)}`);
770
- }
771
- }
772
- } catch (e) {
773
- logger?.warn?.(`[PD:Prompt] Failed to read diagnostician tasks: ${String(e)}`);
774
- }
775
750
  }
776
751
 
777
752
  // ──── 6. Dynamic Attitude Matrix (based on GFI) ────
778
-
779
-
780
- let attitudeDirective: string;
753
+
781
754
  const currentGfi = session?.currentGfi || 0;
782
-
783
- if (currentGfi >= 70) {
784
- attitudeDirective = `
785
- ### 【SYSTEM_MODE: HUMBLE_RECOVERY】
786
- **CURRENT STATUS**: Severe system friction / User frustration detected (GFI: ${currentGfi.toFixed(0)}).
787
- **BEHAVIORAL OVERRIDE**:
788
- - You have failed to meet expectations. Humility is your primary directive.
789
- - **STOP** aggressive file modifications.
790
- - **START** every response with a sincere, non-defensive apology.
791
- - **ACTION**: Explain why you failed, and propose a highly cautious recovery plan.
792
- `;
793
- } else if (currentGfi >= 40) {
794
- attitudeDirective = `
795
- ### 【SYSTEM_MODE: CONCILIATORY】
796
- **CURRENT STATUS**: Moderate friction detected (GFI: ${currentGfi.toFixed(0)}).
797
- **BEHAVIORAL OVERRIDE**:
798
- - User is frustrated. Be more explanatory and cautious.
799
- - Before executing any tool, clearly state what you intend to do and **WAIT** for implicit or explicit user consent.
800
- - Avoid technical jargon; focus on the business/project value of your changes.
801
- `;
802
- } else {
803
- attitudeDirective = `
804
- ### 【SYSTEM_MODE: EFFICIENT】
805
- **CURRENT STATUS**: System healthy (GFI: ${currentGfi.toFixed(0)}).
806
- **BEHAVIORAL OVERRIDE**:
807
- - Maintain peak efficiency.
808
- - Be concise. Prefer action over long explanations.
809
- - Follow the "Principles > Directives" rule strictly.
810
- `;
811
- }
755
+ const attitudeDirective = buildAttitudeDirective(currentGfi);
812
756
 
813
757
  // ──── 7. appendSystemContext: Principles + Thinking OS + reflection_log + project_context ────
814
758
  // NOTE: Principles is ALWAYS injected (not configurable)
@@ -884,7 +828,7 @@ ${taskBlocks}${processingNote}
884
828
  projectContextContent = extractSummary(finalContent, 30);
885
829
  } else {
886
830
  // Full mode: current version + recent history (3 versions)
887
- const historyVersions = getHistoryVersions(focusPath, 3);
831
+ const historyVersions = await getHistoryVersions(focusPath, 3);
888
832
  if (historyVersions.length > 0) {
889
833
  const historySections = historyVersions.map((v, i) =>
890
834
  `\n---\n\n**历史版本 v${historyVersions.length - i}**\n\n${v}`
@@ -909,13 +853,33 @@ ${taskBlocks}${processingNote}
909
853
  const allActive = reducer.getActivePrinciples();
910
854
  const allProbation = reducer.getProbationPrinciples();
911
855
 
856
+ // Pruning mask: exclude principles whose latest review is archive-candidate
857
+ let maskedIds = new Set<string>();
858
+ try {
859
+ maskedIds = getCachedMaskedPrincipleSet(wctx.workspaceDir);
860
+ } catch (err) {
861
+ // Safe degradation: if review log unreadable, inject all principles
862
+ const msg = err instanceof Error ? err.message : String(err);
863
+ if (logger?.info) {
864
+ logger.info(`[PD:Pruning] Failed to read review log — all principles injected: ${msg}`);
865
+ } else {
866
+ console.error(`[PD:Pruning] Failed to read review log — all principles injected: ${msg}`);
867
+ }
868
+ }
869
+
912
870
  // Budget-aware selection: prioritize P0>P1>P2 and recency
913
- const activeSelection = selectPrinciplesForInjection(allActive, DEFAULT_PRINCIPLE_BUDGET);
871
+ const activeSelection = selectPrinciplesForInjection(
872
+ allActive.filter(p => !maskedIds.has(p.id)),
873
+ DEFAULT_PRINCIPLE_BUDGET,
874
+ );
914
875
  const active = activeSelection.selected;
915
876
 
916
877
  // Probation principles get a smaller sub-budget (1000 chars)
917
878
  const probationBudget = 1000;
918
- const probationSelection = selectPrinciplesForInjection(allProbation, probationBudget);
879
+ const probationSelection = selectPrinciplesForInjection(
880
+ allProbation.filter(p => !maskedIds.has(p.id)),
881
+ probationBudget,
882
+ );
919
883
  const probation = probationSelection.selected;
920
884
 
921
885
  if (activeSelection.wasTruncated || probationSelection.wasTruncated) {
@@ -952,13 +916,105 @@ ${taskBlocks}${processingNote}
952
916
  logger?.warn?.(`[PD:Prompt] Failed to load evolution principles: ${String(e)}`);
953
917
  }
954
918
 
919
+ let runtimeV2PrinciplesContent = '';
920
+ const runtimeV2PrincipleIds = new Set<string>();
921
+ // Hoisted so the owner_approved_behavior_directives section can access them
922
+ let dedupedV2: Array<{ principleId: string; text: string; artifactId: string; activationId: string }> = [];
923
+ try {
924
+ const reader = new PromptActivationReader(wctx.workspaceDir, { logger });
925
+ const v2Result = await reader.readActivatedPrinciples();
926
+
927
+ if (v2Result.warnings.length > 0) {
928
+ logger?.info?.(`[PD:RuntimeV2] Activation read warnings: ${v2Result.warnings.join('; ')}`);
929
+ }
930
+
931
+ const legacyActiveIds = new Set<string>();
932
+ try {
933
+ const legacyActive = wctx.evolutionReducer.getActivePrinciples();
934
+ for (const p of legacyActive) {
935
+ legacyActiveIds.add(p.id);
936
+ }
937
+ const legacyProbation = wctx.evolutionReducer.getProbationPrinciples();
938
+ for (const p of legacyProbation) {
939
+ legacyActiveIds.add(p.id);
940
+ }
941
+ } catch {
942
+ // best-effort dedup
943
+ }
944
+
945
+ dedupedV2 = v2Result.principles.filter((p) => !legacyActiveIds.has(p.principleId));
946
+
947
+ if (dedupedV2.length > 0) {
948
+ let remaining = RUNTIME_V2_PRINCIPLE_BUDGET;
949
+ const lines: string[] = [];
950
+ lines.push('Runtime V2 activated principles (owner-approved):');
951
+ remaining -= 'Runtime V2 activated principles (owner-approved):'.length;
952
+
953
+ for (const p of dedupedV2) {
954
+ const entry = `- [${escapeXml(p.principleId)}] ${escapeXml(p.text)}`;
955
+ if (remaining < entry.length + 1) {
956
+ logger?.info?.(`[PD:RuntimeV2] Principle budget reached (${RUNTIME_V2_PRINCIPLE_BUDGET}c) — truncating after ${lines.length - 1} principles`);
957
+ break;
958
+ }
959
+ lines.push(entry);
960
+ remaining -= entry.length + 1;
961
+ runtimeV2PrincipleIds.add(p.principleId);
962
+ }
963
+ runtimeV2PrinciplesContent = lines.join('\n');
964
+ }
965
+
966
+ // ── Emit structured observability event ──
967
+ try {
968
+ const eventLog = wctx.eventLog;
969
+ eventLog.recordRuntimeV2ActivationsInjected({
970
+ sessionId: sessionId ?? 'unknown',
971
+ workspaceDir: wctx.workspaceDir,
972
+ principleIds: [...runtimeV2PrincipleIds],
973
+ activationIds: dedupedV2.map((p) => p.activationId),
974
+ artifactIds: dedupedV2.map((p) => p.artifactId),
975
+ injectedCount: runtimeV2PrincipleIds.size,
976
+ skippedWarnings: v2Result.warnings,
977
+ injectedCharCount: runtimeV2PrinciplesContent.length,
978
+ budget: RUNTIME_V2_PRINCIPLE_BUDGET,
979
+ ...(runtimeV2PrincipleIds.size === 0
980
+ ? {
981
+ skipReason: v2Result.principles.length === 0
982
+ ? 'no_validated_activations'
983
+ : 'all_deduped_against_legacy',
984
+ nextAction: v2Result.principles.length === 0
985
+ ? 'check activations table for prompt channel rows with validated artifacts'
986
+ : 'legacy evolution reducer already contains these principle IDs',
987
+ }
988
+ : {}),
989
+ });
990
+ } catch (logErr) {
991
+ logger?.warn?.(`[PD:RuntimeV2] Failed to emit activation observability event: ${String(logErr)}`);
992
+ }
993
+
994
+ // ── Set confirm-first directive state for gate enforcement ──
995
+ if (sessionId) {
996
+ const cfPrinciple = dedupedV2.find(
997
+ (p) =>
998
+ p.principleId === 'princ-mvp-acceptance-confirm-first' ||
999
+ (p.text.toLowerCase().includes('confirm requirements') &&
1000
+ p.text.toLowerCase().includes('owner approval')),
1001
+ );
1002
+ setConfirmFirstDirective(sessionId, !!cfPrinciple, cfPrinciple?.principleId);
1003
+ }
1004
+ } catch (e) {
1005
+ logger?.warn?.(`[PD:RuntimeV2] Failed to read Runtime V2 prompt activations: ${String(e)}`);
1006
+ if (sessionId) {
1007
+ resetConfirmFirst(sessionId);
1008
+ }
1009
+ }
1010
+
955
1011
  // Build appendSystemContext with recency effect
956
1012
  // Content order (most important last): behavioral_constraints -> project_context -> working_memory -> reflection_log -> thinking_os -> principles
957
1013
  const appendParts: string[] = [];
958
1014
 
959
1015
  // 0. Behavioral Constraints (empathy observer coordination)
960
1016
  // Injected here (appendSystemContext) instead of prependContext to hide from WebUI users.
961
- // See: https://github.com/csuzngjh/principles/issues/XXX
1017
+ // Behavioral constraints: empathy observer coordination
962
1018
  if (shouldInjectBehavioralConstraints) {
963
1019
  appendParts.push(`<behavioral_constraints>
964
1020
  ${empathySilenceConstraint}
@@ -980,11 +1036,35 @@ ${empathySilenceConstraint}
980
1036
  appendParts.push(`<thinking_os>\n${thinkingOsContent}\n</thinking_os>`);
981
1037
  }
982
1038
 
983
- // 3. Evolution Loop principles (active/probation)
1039
+ // 3. Evolution Loop principles (legacy active/probation only — Runtime V2 moved to section 3.5)
984
1040
  if (evolutionPrinciplesContent) {
985
1041
  appendParts.push(`<evolution_principles>\n${evolutionPrinciplesContent}\n</evolution_principles>`);
986
1042
  }
987
1043
 
1044
+ // 3.5. Owner-Approved Behavior Directives (Runtime V2 activated principles)
1045
+ // PLACED IN prependSystemContext (before gateway system prompt) for highest LLM attention.
1046
+ // These are owner-reviewed, validated behavior constraints — not background context.
1047
+ if (runtimeV2PrincipleIds.size > 0) {
1048
+ const directiveLines: string[] = [];
1049
+ directiveLines.push('');
1050
+ directiveLines.push('## 【OWNER-APPROVED BEHAVIOR DIRECTIVES】');
1051
+ directiveLines.push('');
1052
+ directiveLines.push('Owner-approved behavior directives are active operating constraints learned from prior owner corrections.');
1053
+ directiveLines.push('These directives are mandatory for this session unless they conflict with safety, security, or higher-priority system policy.');
1054
+ directiveLines.push('For ambiguous coding or file-changing tasks, follow these directives before using mutating tools.');
1055
+ directiveLines.push('');
1056
+ for (const p of dedupedV2) {
1057
+ if (!runtimeV2PrincipleIds.has(p.principleId)) continue;
1058
+ directiveLines.push(`<directive id="${escapeXml(p.principleId)}" source="runtime_v2_activation">`);
1059
+ directiveLines.push(`MANDATORY: ${escapeXml(p.text)}`);
1060
+ directiveLines.push('Apply this as an active behavior constraint. Do not treat this as background context.');
1061
+ directiveLines.push('</directive>');
1062
+ directiveLines.push('');
1063
+ }
1064
+ directiveLines.push('Note: These directives do not override safety, security, or core system policy.');
1065
+ prependSystemContext += directiveLines.join('\n');
1066
+ }
1067
+
988
1068
  // Routing Guidance (section 5 — injected between evolution principles and core principles)
989
1069
  // Inject delegation guidance when task is bounded + deployment allowed + not high-entropy.
990
1070
  // This is a non-authoritative suggestion — the main agent decides whether to follow.
@@ -1013,7 +1093,6 @@ ${empathySilenceConstraint}
1013
1093
  const routingInput: RoutingInput = {
1014
1094
  taskIntent: toolMatches[0] ?? undefined,
1015
1095
  taskDescription: latestUserText.trim(),
1016
- requestedTools: toolMatches.length > 0 ? toolMatches : undefined,
1017
1096
  requestedFiles: fileMatches.length > 0 ? fileMatches : undefined,
1018
1097
  };
1019
1098
 
@@ -1124,36 +1203,36 @@ ${attitudeDirective}
1124
1203
  }
1125
1204
 
1126
1205
  // ──── 8. SIZE GUARD ────
1127
- // Truncation happens within appendSystemContext (not prependContext)
1128
- const totalSize = prependSystemContext.length + prependContext.length + appendSystemContext.length;
1129
- const MAX_SIZE = 10000;
1130
-
1131
- if (totalSize > MAX_SIZE) {
1132
- const originalSize = totalSize;
1133
- const truncationLog: string[] = [];
1134
-
1135
- // 1. Truncate project_context in appendSystemContext
1136
- if (projectContextContent && appendSystemContext.includes('<project_context>')) {
1137
- const lines = projectContextContent.split('\n');
1138
- if (lines.length > 20) {
1139
- const truncated = lines.slice(0, 20).join('\n') + '\n...[truncated]';
1140
- appendSystemContext = appendSystemContext.replace(
1141
- `<project_context>\n${projectContextContent}\n</project_context>`,
1142
- `<project_context>\n${truncated}\n</project_context>`
1143
- );
1144
- truncationLog.push('project_context');
1145
- }
1206
+ // Delegates to @principles/core/prompt-builder/truncateInjectionToBudget
1207
+ // which handles priority stripping: project_context thinking_os →
1208
+ // evolution_principles reflection_log → reason: truncation → fallback.
1209
+ const result = truncateInjectionToBudget(
1210
+ prependSystemContext,
1211
+ prependContext,
1212
+ appendSystemContext,
1213
+ {
1214
+ diagnosticianMode: pendingDiagTaskCount > 0,
1215
+ blocks: { projectContextContent, thinkingOsContent, evolutionPrinciplesContent },
1146
1216
  }
1147
-
1148
- // 2. Final check
1149
- let newSize = prependSystemContext.length + prependContext.length + appendSystemContext.length;
1150
- if (newSize > MAX_SIZE) {
1151
- // NOTE: We still return the content even if over limit, as truncating more
1152
- // could lose critical context like principles or evolution directives.
1153
- logger?.error(`[PD:Prompt] Cannot reduce injection size below limit. Current: ${newSize}, Limit: ${MAX_SIZE}`);
1217
+ );
1218
+
1219
+ prependSystemContext = result.prependSystemContext;
1220
+ prependContext = result.prependContext;
1221
+ appendSystemContext = result.appendSystemContext;
1222
+
1223
+ if (result.truncated) {
1224
+ const logEntry = result.truncationLog.join(', ');
1225
+ if (result.appendSystemContext.includes('[WARNING: Context sections stripped')) {
1226
+ logger?.error(
1227
+ `[PD:Prompt] PROMPT OVER LIMIT AFTER ALL REDUCTIONS — using fallback. ` +
1228
+ `Diagnostician mode: ${pendingDiagTaskCount > 0}. Stripped: ${logEntry}.`
1229
+ );
1230
+ } else {
1231
+ logger?.warn(
1232
+ `[PD:Prompt] Injection size exceeded budget, truncated: ${logEntry || 'none'}, ` +
1233
+ `diagnostician mode: ${pendingDiagTaskCount > 0}`
1234
+ );
1154
1235
  }
1155
-
1156
- logger?.warn(`[PD:Prompt] Injection size exceeded: ${originalSize} chars (limit: ${MAX_SIZE}), truncated: ${truncationLog.join(', ') || 'none'}, new size: ${newSize} chars`);
1157
1236
  }
1158
1237
 
1159
1238
  return {
@@ -1162,3 +1241,54 @@ ${attitudeDirective}
1162
1241
  appendSystemContext
1163
1242
  };
1164
1243
  }
1244
+
1245
+ // ── Empathy Observer Hybrid Deep Analysis Helpers (Unified SDK Migration) ──
1246
+
1247
+ function scoreFromSeverityForSpec(severity: string | undefined, wctx: WorkspaceContext): number {
1248
+ if (severity === 'severe') return Number(wctx.config.get('empathy_engine.penalties.severe') ?? 40);
1249
+ if (severity === 'moderate') return Number(wctx.config.get('empathy_engine.penalties.moderate') ?? 25);
1250
+ return Number(wctx.config.get('empathy_engine.penalties.mild') ?? 10);
1251
+ }
1252
+
1253
+ function resolveEmpathyObserver(wctx: WorkspaceContext, logger?: Pick<PluginLogger, 'info' | 'warn' | 'error' | 'debug'>): EmpathyObserver | null {
1254
+ try {
1255
+ const loader = new WorkflowFunnelLoader(wctx.stateDir);
1256
+ const funnel = loader.getFunnel('pd-empathy-observer');
1257
+ const policy = funnel?.policy;
1258
+ if (!policy || policy.runtimeKind !== 'pi-ai') {
1259
+ logger?.debug?.('[PD:Empathy] workflows.yaml pd-empathy-observer policy not found. Falling back to environment variables.');
1260
+ const provider = process.env.PD_EMPATHY_PROVIDER || 'anthropic';
1261
+ const model = process.env.PD_EMPATHY_MODEL || 'anthropic/claude-3-5-sonnet';
1262
+ const apiKeyEnv = process.env.PD_EMPATHY_API_KEY_ENV || 'ANTHROPIC_API_KEY';
1263
+ const baseUrl = process.env.PD_EMPATHY_BASE_URL;
1264
+
1265
+ if (!process.env[apiKeyEnv]) {
1266
+ logger?.debug?.(`[PD:Empathy] Empathy observer API key env ${apiKeyEnv} is not set. Background analysis disabled.`);
1267
+ return null;
1268
+ }
1269
+
1270
+ const adapter = new PiAiRuntimeAdapter({
1271
+ provider,
1272
+ model,
1273
+ apiKeyEnv,
1274
+ baseUrl,
1275
+ workspace: wctx.workspaceDir,
1276
+ });
1277
+ return new EmpathyObserver({ runtimeAdapter: adapter });
1278
+ }
1279
+
1280
+ const adapter = new PiAiRuntimeAdapter({
1281
+ provider: String(policy.provider),
1282
+ model: String(policy.model),
1283
+ apiKeyEnv: String(policy.apiKeyEnv),
1284
+ maxRetries: policy.maxRetries,
1285
+ timeoutMs: policy.timeoutMs ?? 30_000,
1286
+ baseUrl: policy.baseUrl,
1287
+ workspace: wctx.workspaceDir,
1288
+ });
1289
+ return new EmpathyObserver({ runtimeAdapter: adapter }, { timeoutMs: policy.timeoutMs });
1290
+ } catch (err) {
1291
+ logger?.warn?.(`[PD:Empathy] Failed to resolve EmpathyObserver: ${String(err)}`);
1292
+ return null;
1293
+ }
1294
+ }