principles-disciple 1.72.0 → 1.74.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 (319) hide show
  1. package/INSTALL.md +1 -3
  2. package/openclaw.plugin.json +10 -5
  3. package/package.json +17 -19
  4. package/scripts/acceptance-test.mjs +16 -73
  5. package/scripts/sync-plugin.mjs +382 -77
  6. package/src/commands/archive-impl.ts +2 -1
  7. package/src/commands/capabilities.ts +2 -2
  8. package/src/commands/context.ts +2 -2
  9. package/src/commands/disable-impl.ts +2 -1
  10. package/src/commands/evolution-status.ts +16 -16
  11. package/src/commands/export.ts +12 -67
  12. package/src/commands/pain.ts +91 -1
  13. package/src/commands/principle-rollback.ts +2 -1
  14. package/src/commands/promote-impl.ts +7 -43
  15. package/src/commands/rollback-impl.ts +2 -1
  16. package/src/commands/rollback.ts +2 -1
  17. package/src/commands/samples.ts +2 -1
  18. package/src/commands/thinking-os.ts +2 -1
  19. package/src/config/errors.ts +18 -2
  20. package/src/constants/diagnostician.ts +2 -2
  21. package/src/constants/tools.ts +2 -1
  22. package/src/core/__tests__/focus-history.test.ts +210 -0
  23. package/src/core/config.ts +1 -1
  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 +29 -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/migration.ts +0 -1
  37. package/src/core/pain-diagnostic-gate.ts +154 -0
  38. package/src/core/pain-signal.ts +21 -138
  39. package/src/core/pain.ts +15 -88
  40. package/src/core/path-resolver.ts +0 -1
  41. package/src/core/paths.ts +0 -1
  42. package/src/core/pd-task-reconciler.ts +26 -115
  43. package/src/core/pd-task-service.ts +9 -9
  44. package/src/core/pd-task-types.ts +23 -127
  45. package/src/core/principle-compiler/__tests__/compiler-replay-gate.test.ts +174 -0
  46. package/src/core/principle-compiler/code-validator.ts +15 -42
  47. package/src/core/principle-compiler/compiler.ts +100 -15
  48. package/src/core/principle-compiler/index.ts +5 -2
  49. package/src/core/principle-compiler/template-generator.ts +4 -104
  50. package/src/core/principle-injection.ts +10 -202
  51. package/src/core/principle-internalization/filesystem-lifecycle-datasource.ts +42 -0
  52. package/src/core/principle-internalization/lifecycle-read-model.ts +39 -242
  53. package/src/core/principle-internalization/principle-lifecycle-service.ts +12 -10
  54. package/src/core/principle-tree-ledger-adapter.ts +145 -0
  55. package/src/core/principle-tree-ledger.ts +8 -6
  56. package/src/core/reflection/reflection-context.ts +14 -109
  57. package/src/core/replay-engine.ts +8 -500
  58. package/src/core/rule-host-helpers.ts +5 -35
  59. package/src/core/rule-host-types.ts +10 -82
  60. package/src/core/rule-host.ts +6 -63
  61. package/src/core/runtime-v2-prompt-activation-reader.ts +231 -0
  62. package/src/core/session-tracker.ts +87 -101
  63. package/src/core/shadow-observation-registry.ts +19 -48
  64. package/src/core/trajectory.ts +3 -1
  65. package/src/core/workflow-funnel-loader.ts +62 -68
  66. package/src/core/workspace-context.ts +46 -0
  67. package/src/core/workspace-dir-service.ts +1 -1
  68. package/src/core/workspace-dir-validation.ts +18 -9
  69. package/src/hooks/AGENTS.md +1 -1
  70. package/src/hooks/gate-block-helper.ts +71 -64
  71. package/src/hooks/gate.ts +183 -31
  72. package/src/hooks/lifecycle.ts +30 -32
  73. package/src/hooks/llm.ts +60 -32
  74. package/src/hooks/pain.ts +297 -103
  75. package/src/hooks/prompt.ts +400 -440
  76. package/src/hooks/subagent.ts +2 -29
  77. package/src/i18n/commands.ts +2 -10
  78. package/src/index.ts +95 -85
  79. package/src/openclaw-sdk.ts +311 -0
  80. package/src/service/central-database.ts +8 -4
  81. package/src/service/evolution-queue-migration.ts +2 -1
  82. package/src/service/evolution-worker.ts +163 -1786
  83. package/src/service/internalization-trigger-adapter.ts +302 -0
  84. package/src/service/keyword-optimization-service.ts +4 -4
  85. package/src/service/monitoring-query-service.ts +1 -215
  86. package/src/service/queue-io.ts +60 -331
  87. package/src/service/runtime-summary-service.ts +59 -16
  88. package/src/service/subagent-workflow/index.ts +0 -41
  89. package/src/service/subagent-workflow/types.ts +9 -120
  90. package/src/service/subagent-workflow/workflow-store.ts +2 -119
  91. package/src/service/workflow-watchdog.ts +0 -43
  92. package/src/types/event-payload.ts +16 -74
  93. package/src/types/event-types.ts +38 -547
  94. package/src/types/hygiene-types.ts +7 -30
  95. package/src/types/principle-tree-schema.ts +20 -222
  96. package/src/types/queue.ts +15 -70
  97. package/src/types/runtime-summary.ts +5 -49
  98. package/src/utils/io.ts +8 -20
  99. package/src/utils/retry.ts +1 -1
  100. package/src/utils/shadow-fingerprint.ts +2 -2
  101. package/src/utils/workspace-resolver.ts +50 -0
  102. package/templates/langs/en/core/AGENTS.md +7 -7
  103. package/templates/langs/en/core/BOOT.md +1 -1
  104. package/templates/langs/en/core/HEARTBEAT.md +2 -2
  105. package/templates/langs/en/principles/THINKING_OS.md +3 -2
  106. package/templates/langs/en/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  107. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  108. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  109. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  110. package/templates/langs/en/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  111. package/templates/langs/en/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  112. package/templates/langs/en/skills/evolve-task/SKILL.md +3 -3
  113. package/templates/langs/en/skills/pd-cli-operator/SKILL.md +67 -0
  114. package/templates/langs/en/skills/pd-diagnostician/SKILL.md +1 -1
  115. package/templates/langs/en/skills/pd-mentor/SKILL.md +2 -3
  116. package/templates/langs/en/skills/pd-pain-signal/SKILL.md +17 -39
  117. package/templates/langs/en/skills/pd-runtime-v2/SKILL.md +61 -0
  118. package/templates/langs/zh/core/AGENTS.md +7 -7
  119. package/templates/langs/zh/core/BOOT.md +1 -1
  120. package/templates/langs/zh/core/HEARTBEAT.md +2 -2
  121. package/templates/langs/zh/principles/THINKING_OS.md +3 -2
  122. package/templates/langs/zh/skills/ai-sprint-orchestration/references/agent-registry.json +1 -72
  123. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/bugfix-complex-template.json +6 -6
  124. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/feature-complex-template.json +6 -6
  125. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +8 -8
  126. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal-verify.json +2 -12
  127. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/workflow-validation-minimal.json +2 -12
  128. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +51 -15
  129. package/templates/langs/zh/skills/ai-sprint-orchestration/test/run.test.mjs +21 -5
  130. package/templates/langs/zh/skills/evolve-task/SKILL.md +4 -4
  131. package/templates/langs/zh/skills/pd-cli-operator/SKILL.md +67 -0
  132. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +1 -1
  133. package/templates/langs/zh/skills/pd-mentor/SKILL.md +2 -3
  134. package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +17 -38
  135. package/templates/langs/zh/skills/pd-runtime-v2/SKILL.md +61 -0
  136. package/tests/build-artifacts.test.ts +1 -3
  137. package/tests/commands/evolution-status.test.ts +0 -118
  138. package/tests/core/bootstrap-rules.test.ts +1 -1
  139. package/tests/core/config.test.ts +1 -1
  140. package/tests/core/event-log.test.ts +35 -0
  141. package/tests/core/evolution-engine.test.ts +610 -0
  142. package/tests/core/file-store.test.ts +102 -0
  143. package/tests/core/focus-history.test.ts +203 -11
  144. package/tests/core/merge-gate-audit.test.ts +2 -169
  145. package/tests/core/migration.test.ts +7 -7
  146. package/tests/core/model-deployment-registry.test.ts +7 -1
  147. package/tests/core/model-training-registry.test.ts +19 -0
  148. package/tests/core/observability.test.ts +0 -1
  149. package/tests/core/pain-diagnostic-gate.test.ts +498 -0
  150. package/tests/core/pain.test.ts +0 -1
  151. package/tests/core/path-resolver.test.ts +1 -1
  152. package/tests/core/paths-refactor.test.ts +0 -22
  153. package/tests/core/principle-internalization/deprecated-readiness.test.ts +2 -2
  154. package/tests/core/principle-internalization/lifecycle-metrics.test.ts +2 -2
  155. package/tests/core/principle-internalization/{internalization-routing-policy.test.ts → lifecycle-routing-policy.test.ts} +6 -6
  156. package/tests/core/principle-internalization/lineage-source-retired.test.ts +56 -0
  157. package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +1 -23
  158. package/tests/core/principle-tree-ledger-adapter.test.ts +253 -0
  159. package/tests/core/reflection-context.test.ts +0 -14
  160. package/tests/core/replay-engine.test.ts +127 -215
  161. package/tests/core/rule-host-helpers.test.ts +2 -2
  162. package/tests/core/rule-implementation-runtime.test.ts +0 -27
  163. package/tests/core/workflow-funnel-loader.test.ts +162 -0
  164. package/tests/core/workspace-context.test.ts +2 -2
  165. package/tests/core/workspace-dir-validation.test.ts +8 -1
  166. package/tests/core-anti-growth.test.ts +191 -0
  167. package/tests/hook-workspace-nextaction-contract.test.ts +42 -0
  168. package/tests/hooks/confirm-first-removal.test.ts +188 -0
  169. package/tests/hooks/gate-auto-correct-shadow.test.ts +310 -0
  170. package/tests/hooks/gate-auto-correct.test.ts +665 -0
  171. package/tests/hooks/gate-no-path-write-tool.test.ts +172 -0
  172. package/tests/hooks/gate-rule-host-pipeline.test.ts +2 -1
  173. package/tests/hooks/pain.test.ts +269 -12
  174. package/tests/hooks/prompt-characterization.test.ts +500 -0
  175. package/tests/hooks/prompt-size-guard.test.ts +32 -17
  176. package/tests/hooks/runtime-v2-prompt-activation.test.ts +869 -0
  177. package/tests/index.test.ts +94 -1
  178. package/tests/integration/auto-entry-gate.test.ts +248 -0
  179. package/tests/integration/internalization-trigger-guard.test.ts +69 -0
  180. package/tests/integration/m8-legacy-paths.test.ts +63 -0
  181. package/tests/integration/runtime-v2-pain-guard.test.ts +125 -0
  182. package/tests/plugin-config-resolution-cutover.test.ts +359 -0
  183. package/tests/runtime-v2-discovery-guard.test.ts +154 -0
  184. package/tests/service/central-database.test.ts +457 -0
  185. package/tests/service/evolution-worker.correction-observer.test.ts +173 -0
  186. package/tests/service/evolution-worker.timeout.test.ts +11 -129
  187. package/tests/service/internalization-trigger-adapter.test.ts +251 -0
  188. package/tests/service/monitoring-query-service.test.ts +1 -47
  189. package/tests/service/queue-io.test.ts +1 -62
  190. package/tests/service/runtime-summary-service.test.ts +3 -1
  191. package/tests/service/workflow-watchdog.test.ts +0 -91
  192. package/tests/utils/file-lock.test.ts +5 -3
  193. package/tests/utils/session-key.test.ts +52 -0
  194. package/tests/utils/subagent-probe.test.ts +48 -1
  195. package/vitest.config.ts +4 -11
  196. package/.planning/codebase/ARCHITECTURE.md +0 -157
  197. package/.planning/codebase/CONCERNS.md +0 -145
  198. package/.planning/codebase/CONVENTIONS.md +0 -148
  199. package/.planning/codebase/INTEGRATIONS.md +0 -81
  200. package/.planning/codebase/STACK.md +0 -87
  201. package/.planning/codebase/STRUCTURE.md +0 -193
  202. package/.planning/codebase/TESTING.md +0 -243
  203. package/.planning/phases/01-basic-visualization/01-GAP-CLOSURE-VERIFICATION.md +0 -113
  204. package/docs/COMMAND_REFERENCE.md +0 -76
  205. package/docs/COMMAND_REFERENCE_EN.md +0 -79
  206. package/scripts/build-web.mjs +0 -46
  207. package/scripts/diagnose-nocturnal.mjs +0 -537
  208. package/scripts/seed-nocturnal-scenarios.mjs +0 -384
  209. package/src/commands/nocturnal-review.ts +0 -322
  210. package/src/commands/nocturnal-rollout.ts +0 -790
  211. package/src/commands/nocturnal-train.ts +0 -986
  212. package/src/commands/pd-reflect.ts +0 -88
  213. package/src/core/adaptive-thresholds.ts +0 -478
  214. package/src/core/diagnostician-task-store.ts +0 -192
  215. package/src/core/nocturnal-arbiter.ts +0 -715
  216. package/src/core/nocturnal-artifact-lineage.ts +0 -116
  217. package/src/core/nocturnal-artificer.ts +0 -257
  218. package/src/core/nocturnal-candidate-scoring.ts +0 -530
  219. package/src/core/nocturnal-compliance.ts +0 -1146
  220. package/src/core/nocturnal-dataset.ts +0 -763
  221. package/src/core/nocturnal-executability.ts +0 -428
  222. package/src/core/nocturnal-export.ts +0 -499
  223. package/src/core/nocturnal-paths.ts +0 -240
  224. package/src/core/nocturnal-reasoning-deriver.ts +0 -343
  225. package/src/core/nocturnal-rule-implementation-validator.ts +0 -246
  226. package/src/core/nocturnal-snapshot-contract.ts +0 -99
  227. package/src/core/nocturnal-trajectory-extractor.ts +0 -512
  228. package/src/core/nocturnal-trinity-types.ts +0 -218
  229. package/src/core/nocturnal-trinity.ts +0 -2680
  230. package/src/core/principle-internalization/deprecated-readiness.ts +0 -93
  231. package/src/core/principle-internalization/internalization-routing-policy.ts +0 -208
  232. package/src/core/principle-internalization/lifecycle-metrics.ts +0 -152
  233. package/src/http/principles-console-route.ts +0 -709
  234. package/src/service/central-health-service.ts +0 -49
  235. package/src/service/central-overview-service.ts +0 -138
  236. package/src/service/control-ui-query-service.ts +0 -900
  237. package/src/service/cooldown-strategy.ts +0 -97
  238. package/src/service/evolution-pain-context.ts +0 -79
  239. package/src/service/evolution-query-service.ts +0 -407
  240. package/src/service/health-query-service.ts +0 -1038
  241. package/src/service/nocturnal-config.ts +0 -214
  242. package/src/service/nocturnal-runtime.ts +0 -734
  243. package/src/service/nocturnal-service.ts +0 -1605
  244. package/src/service/nocturnal-target-selector.ts +0 -545
  245. package/src/service/sleep-cycle.ts +0 -157
  246. package/src/service/startup-reconciler.ts +0 -112
  247. package/src/service/subagent-workflow/correction-observer-types.ts +0 -82
  248. package/src/service/subagent-workflow/correction-observer-workflow-manager.ts +0 -250
  249. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +0 -1
  250. package/src/service/subagent-workflow/dynamic-timeout.ts +0 -30
  251. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +0 -268
  252. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +0 -795
  253. package/src/service/subagent-workflow/runtime-direct-driver.ts +0 -268
  254. package/src/service/subagent-workflow/workflow-manager-base.ts +0 -580
  255. package/src/tools/write-pain-flag.ts +0 -215
  256. package/templates/langs/en/skills/plan-script/SKILL.md +0 -32
  257. package/templates/langs/zh/skills/plan-script/SKILL.md +0 -32
  258. package/tests/commands/nocturnal-review.test.ts +0 -448
  259. package/tests/commands/nocturnal-train.test.ts +0 -97
  260. package/tests/commands/pd-reflect.test.ts +0 -49
  261. package/tests/core/adaptive-thresholds.test.ts +0 -261
  262. package/tests/core/nocturnal-arbiter.test.ts +0 -559
  263. package/tests/core/nocturnal-artifact-lineage.test.ts +0 -53
  264. package/tests/core/nocturnal-artificer.test.ts +0 -241
  265. package/tests/core/nocturnal-candidate-scoring.test.ts +0 -532
  266. package/tests/core/nocturnal-compliance-p-principles.test.ts +0 -133
  267. package/tests/core/nocturnal-compliance.test.ts +0 -646
  268. package/tests/core/nocturnal-dataset.test.ts +0 -892
  269. package/tests/core/nocturnal-e2e.test.ts +0 -234
  270. package/tests/core/nocturnal-executability.test.ts +0 -357
  271. package/tests/core/nocturnal-export.test.ts +0 -517
  272. package/tests/core/nocturnal-reasoning-deriver.test.ts +0 -372
  273. package/tests/core/nocturnal-reviewed-subset-comparison.test.ts +0 -428
  274. package/tests/core/nocturnal-rule-implementation-validator.test.ts +0 -127
  275. package/tests/core/nocturnal-snapshot-contract.test.ts +0 -121
  276. package/tests/core/nocturnal-trajectory-extractor.test.ts +0 -634
  277. package/tests/core/nocturnal-trinity.test.ts +0 -2053
  278. package/tests/core/pain-auto-repair.test.ts +0 -96
  279. package/tests/core/pain-integration.test.ts +0 -510
  280. package/tests/fixtures/nocturnal-reviewed-subset.json +0 -183
  281. package/tests/http/principles-console-route.test.ts +0 -162
  282. package/tests/integration/chaos-resilience.test.ts +0 -348
  283. package/tests/integration/empathy-workflow-integration.test.ts +0 -626
  284. package/tests/integration/pain-diagnostician-loop.e2e.test.ts +0 -380
  285. package/tests/service/control-ui-query-service.test.ts +0 -121
  286. package/tests/service/cooldown-strategy.test.ts +0 -164
  287. package/tests/service/data-endpoints-regression.test.ts +0 -834
  288. package/tests/service/empathy-observer-workflow-manager.test.ts +0 -175
  289. package/tests/service/evolution-worker.nocturnal.test.ts +0 -601
  290. package/tests/service/nocturnal-runtime-hardening.test.ts +0 -118
  291. package/tests/service/nocturnal-runtime.test.ts +0 -473
  292. package/tests/service/nocturnal-service-code-candidate.test.ts +0 -330
  293. package/tests/service/nocturnal-target-selector.test.ts +0 -615
  294. package/tests/service/startup-reconciler.test.ts +0 -148
  295. package/tests/tools/write-pain-flag.test.ts +0 -358
  296. package/ui/src/App.tsx +0 -45
  297. package/ui/src/api.ts +0 -220
  298. package/ui/src/charts.tsx +0 -955
  299. package/ui/src/components/ErrorState.tsx +0 -6
  300. package/ui/src/components/Loading.tsx +0 -13
  301. package/ui/src/components/ProtectedRoute.tsx +0 -12
  302. package/ui/src/components/Shell.tsx +0 -91
  303. package/ui/src/components/WorkspaceConfig.tsx +0 -178
  304. package/ui/src/components/index.ts +0 -5
  305. package/ui/src/context/auth.tsx +0 -80
  306. package/ui/src/context/theme.tsx +0 -66
  307. package/ui/src/hooks/useAutoRefresh.ts +0 -39
  308. package/ui/src/i18n/ui.ts +0 -473
  309. package/ui/src/main.tsx +0 -16
  310. package/ui/src/pages/EvolutionPage.tsx +0 -333
  311. package/ui/src/pages/FeedbackPage.tsx +0 -138
  312. package/ui/src/pages/GateMonitorPage.tsx +0 -136
  313. package/ui/src/pages/LoginPage.tsx +0 -89
  314. package/ui/src/pages/OverviewPage.tsx +0 -599
  315. package/ui/src/pages/SamplesPage.tsx +0 -174
  316. package/ui/src/pages/ThinkingModelsPage.tsx +0 -702
  317. package/ui/src/styles.css +0 -2020
  318. package/ui/src/types.ts +0 -384
  319. 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';
@@ -9,32 +9,27 @@ 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
11
  import { extractSummary, getHistoryVersions, parseWorkingMemorySection, workingMemoryToInjection, autoCompressFocus, safeReadCurrentFocus } from '../core/focus-history.js';
12
- import { EmpathyObserverWorkflowManager, empathyObserverWorkflowSpec, isExpectedSubagentError } from '../service/subagent-workflow/index.js';
13
12
  import { PathResolver } from '../core/path-resolver.js';
14
13
  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';
14
+ import { getCachedMaskedPrincipleSet, WorkflowFunnelLoader, PiAiRuntimeAdapter, EmpathyObserver, AgentScheduler } from '@principles/core/runtime-v2';
15
+ import { truncateInjectionToBudget } from '@principles/core/prompt-builder';
16
+ import { PromptActivationReader, RUNTIME_V2_PRINCIPLE_BUDGET } from '../core/runtime-v2-prompt-activation-reader.js';
17
17
  import {
18
18
  matchEmpathyKeywords,
19
19
  loadKeywordStore,
20
20
  saveKeywordStore,
21
- shouldTriggerOptimization,
22
21
  getKeywordStoreSummary,
23
22
  } from '../core/empathy-keyword-matcher.js';
24
23
  import { severityToPenalty, DEFAULT_EMPATHY_KEYWORD_CONFIG } from '../core/empathy-types.js';
24
+ import { evaluatePainDiagnosticGate } from '../core/pain-diagnostic-gate.js';
25
+ import { emitPainDetectedEvent, buildTrajectoryEvidence } from './pain.js';
25
26
  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
- }
27
+ import {
28
+ buildAttitudeDirective,
29
+ detectCorrectionCue as coreDetectCorrectionCue,
30
+ extractMessageContent,
31
+ isMinimalTrigger,
32
+ } from '@principles/core/prompt-builder';
38
33
 
39
34
  // ---------------------------------------------------------------------------
40
35
  // Static file cache — avoids re-reading rarely-changing files every message
@@ -129,43 +124,7 @@ interface PromptHookApi {
129
124
  }
130
125
 
131
126
  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;
127
+ return extractMessageContent(message);
169
128
  }
170
129
 
171
130
  /**
@@ -297,64 +256,12 @@ export function getDiagnosticianModel(api: PromptHookApi | null, logger?: Plugin
297
256
  /**
298
257
  * Extract recent user messages for keyword optimization context.
299
258
  */
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
- }
259
+
323
260
 
324
261
  /**
325
262
  * Build prompt for keyword optimization subagent.
326
263
  */
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
- `;
357
- }
264
+
358
265
 
359
266
  export async function handleBeforePromptBuild(
360
267
  event: PluginHookBeforePromptBuildEvent,
@@ -364,19 +271,13 @@ export async function handleBeforePromptBuild(
364
271
  const logger = ctx.api?.logger;
365
272
  logger?.info?.(`[PD:Prompt] handleBeforePromptBuild called: workspaceDir=${!!workspaceDir}, trigger=${ctx.trigger}, sessionId=${ctx.sessionId?.substring(0, 20)}`);
366
273
  if (!workspaceDir) {
367
- logger?.warn?.(`[PD:Prompt] workspaceDir is missing — skipping empathy processing`);
274
+ logger?.warn?.(`[PD:Prompt] workspaceDir is missing — skipping PD context injection`);
368
275
  return;
369
276
  }
370
277
 
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
278
  const wctx = WorkspaceContext.fromHookContext(ctx);
379
- const { trigger, sessionId, api } = ctx;
279
+ const { trigger, sessionId } = ctx as { trigger: string | undefined; sessionId: string | undefined };
280
+ const api = ctx.api;
380
281
  if (sessionId) {
381
282
  wctx.trajectory?.recordSession?.({ sessionId });
382
283
  }
@@ -389,6 +290,7 @@ export async function handleBeforePromptBuild(
389
290
 
390
291
  if (latestUserIndex) {
391
292
  const userText = getTextContent(latestUserIndex.message);
293
+
392
294
  // Use CorrectionCueLearner for detection — supports learned keywords, not just hardcoded list
393
295
  let correctionCue: string | null = null;
394
296
  try {
@@ -406,7 +308,7 @@ export async function handleBeforePromptBuild(
406
308
  }
407
309
  } catch (learnerErr) {
408
310
  // Fallback to hardcoded detection if learner fails — log for observability
409
- correctionCue = detectCorrectionCue(userText);
311
+ correctionCue = coreDetectCorrectionCue(userText);
410
312
  logger?.warn?.(`[PD:Prompt] CorrectionCueLearner.match() failed (${String(learnerErr)}), fallback=${correctionCue ? `matched="${correctionCue}"` : 'no-match'}`);
411
313
  }
412
314
  let referencesAssistantTurnId: number | null = null;
@@ -432,10 +334,10 @@ export async function handleBeforePromptBuild(
432
334
  }
433
335
 
434
336
  // Load context injection configuration
435
- const contextConfig = loadContextInjectionConfig(workspaceDir);
337
+ const contextConfig = loadContextInjectionConfig(wctx.workspaceDir);
436
338
 
437
339
  // Minimal mode: heartbeat and subagents skip most context to reduce tokens
438
- const isMinimalMode = trigger === "heartbeat" || trigger === "cron" || sessionId?.includes(":subagent:") === true;
340
+ const isMinimalMode = isMinimalTrigger(trigger as string | undefined, sessionId as string | undefined);
439
341
 
440
342
  const session = sessionId ? getSession(sessionId) : undefined;
441
343
 
@@ -535,7 +437,9 @@ The empathy observer subagent handles pain detection independently.
535
437
 
536
438
  const isUserInteraction = trigger === 'user' || trigger === 'api' || !trigger;
537
439
 
440
+ // Empathy Observer: keyword fast-path + optional LLM deep analysis (zero latency async dispatch)
538
441
  const empathyEnabled = wctx.config.get('empathy_engine.enabled') !== false;
442
+
539
443
  logger?.info?.(`[PD:Empathy] Conditions: enabled=${empathyEnabled}, isUser=${isUserInteraction}, sessionId=${!!sessionId}, api=${!!api}, !agentToAgent=${!isAgentToAgent}, workspaceDir=${!!workspaceDir}, hasMessage=${!!latestUserMessage}`);
540
444
 
541
445
  // Track if we should inject behavioral constraints (will be added to appendSystemContext later)
@@ -564,23 +468,6 @@ The empathy observer subagent handles pain detection independently.
564
468
  _empathyTurnCounter++;
565
469
  const turnCount = _empathyTurnCounter;
566
470
 
567
- // Decision: should we call subagent?
568
- let shouldCallSubagent = false;
569
- let samplingReason = '';
570
-
571
- if (matchResult.score >= 0.8) {
572
- // High confidence — keyword match is reliable, no subagent needed
573
- shouldCallSubagent = false;
574
- } else if (matchResult.score >= 0.3) {
575
- // Boundary case — 30% sampling for subagent verification
576
- shouldCallSubagent = Math.random() < 0.3;
577
- samplingReason = 'boundary_verification';
578
- } else {
579
- // No keyword hit — 5% random sampling to discover new expressions
580
- shouldCallSubagent = Math.random() < 0.05;
581
- samplingReason = 'random_discovery';
582
- }
583
-
584
471
  if (matchResult.matched) {
585
472
  const penalty = severityToPenalty(matchResult.severity, DEFAULT_EMPATHY_KEYWORD_CONFIG);
586
473
  // trackFriction signature: (sessionId, deltaF: number, hash: string, workspaceDir?, options?)
@@ -588,93 +475,194 @@ The empathy observer subagent handles pain detection independently.
588
475
  source: 'user_empathy',
589
476
  });
590
477
 
591
- 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') + ')'}`);
592
- } else {
593
- // Log unmatched messages periodically for coverage analysis
594
- if (turnCount > 0 && turnCount % 50 === 0) {
595
- const sampleMsg = latestUserMessage.substring(0, 80).replace(/\n/g, ' ');
596
- logger?.debug?.(`[PD:Empathy] NO_MATCH: "${sampleMsg}" (turn ${turnCount}, keywords_in_store=${Object.keys(keywordStore.terms).length})`);
597
- }
598
- }
599
-
600
- // Trigger subagent for sampling cases (Finding #1: use shared manager to avoid leaks)
601
- const runtimeSubagent =
602
- isSubagentRuntimeAvailable(api?.runtime?.subagent)
603
- ? api.runtime.subagent
604
- : undefined;
605
-
606
- if (shouldCallSubagent && runtimeSubagent) {
607
- logger?.info?.(`[PD:Empathy] SUBAGENT_SAMPLE: reason=${samplingReason}, score=${matchResult.score.toFixed(2)}, matched=[${matchResult.matchedTerms.join(',')}]`);
608
-
609
- // EmpathyObserverWorkflowManager auto-finalizes via wait poll mechanism.
610
- // Create a fresh manager per invocation to ensure clean state.
611
- const empathyManager = new EmpathyObserverWorkflowManager({
612
- workspaceDir,
613
- logger: api.logger ?? console,
614
-
615
- subagent: toWorkflowSubagent(runtimeSubagent),
616
- });
617
- empathyManager.startWorkflow(empathyObserverWorkflowSpec, {
618
- parentSessionId: sessionId,
619
- workspaceDir,
620
- taskInput: latestUserMessage,
621
- }).catch((err) => {
622
- if (!isExpectedSubagentError(err)) {
623
- api.logger?.warn?.(`[PD:Empathy] subagent sample failed: ${String(err)}`);
624
- }
625
- });
626
- }
478
+ logger?.info?.(`[PD:Empathy] MATCH: "${matchResult.matchedTerms.join(', ')}" → severity=${matchResult.severity}, score=${matchResult.score.toFixed(2)}, penalty=${penalty}`);
479
+
480
+ const currentSession = getSession(sessionId);
481
+ const currentGfi = currentSession?.currentGfi ?? 0;
482
+ const painTrigger = wctx.config.get('thresholds.pain_trigger') || 40;
483
+ const highGfiThreshold = Math.max(wctx.config.get('severity_thresholds.high') || 70, painTrigger + 30);
484
+
485
+ if (currentGfi >= highGfiThreshold) {
486
+ const gfiPainScore = Math.min(Math.round(currentGfi), 60);
487
+ logger?.info?.(`[PD:Empathy] GFI-TRIGGERED: currentGfi=${currentGfi.toFixed(1)} >= highGfi=${highGfiThreshold}, emitting pain signal (score=${gfiPainScore})`);
488
+
489
+ const gate = evaluatePainDiagnosticGate({
490
+ source: 'user_empathy',
491
+ score: gfiPainScore,
492
+ currentGfi,
493
+ consecutiveErrors: currentSession?.consecutiveErrors ?? 0,
494
+ sessionId,
495
+ errorHash: 'empathy_gfi_threshold',
496
+ thresholds: {
497
+ painTrigger,
498
+ highSeverity: wctx.config.get('severity_thresholds.high') || 70,
499
+ semanticPain: Math.max(painTrigger, 60),
500
+ },
501
+ });
627
502
 
628
- // Helper: build summary string (Finding #2: avoid duplication)
629
- const buildSummary = (): string => {
630
- const s = getKeywordStoreSummary(keywordStore);
631
- const highFP = s.highFalsePositiveTerms.slice(0, 5).map(t => `${t.term}(${t.falsePositiveRate.toFixed(2)})`).join(', ');
632
- return `SUMMARY(turn=${turnCount}): terms=${s.totalTerms}, hits=${keywordStore.stats.totalHits}, zero_hit=${s.totalTerms - (s.seedTerms + s.discoveredTerms)}, high_fp=[${highFP}]`;
633
- };
503
+ wctx.eventLog.recordPainSignal(sessionId, {
504
+ score: gfiPainScore,
505
+ source: 'user_empathy',
506
+ reason: `Accumulated GFI (${currentGfi.toFixed(1)}) crossed highGfi threshold (${highGfiThreshold}). Matched: ${matchResult.matchedTerms.join(', ')}`,
507
+ isRisky: false,
508
+ });
634
509
 
635
- // Check if keyword optimization should be triggered
636
- if (shouldTriggerOptimization(keywordStore, turnCount)) {
637
- logger?.info?.(`[PD:Empathy] OPTIMIZATION_TRIGGER: turns=${turnCount}, last_optimized=${keywordStore.lastOptimizedAt}`);
638
- logger?.info?.(`[PD:Empathy] STATS: ${buildSummary()}`);
510
+ if (gate.shouldDiagnose) {
511
+ logger?.info?.(`[PD:Empathy] Gate approved, calling emitPainDetectedEvent...`);
512
+ try {
513
+ const evidence = buildTrajectoryEvidence(wctx, sessionId);
514
+ await emitPainDetectedEvent(wctx, {
515
+ ts: new Date().toISOString(),
516
+ type: 'pain_detected',
517
+ data: {
518
+ painId: `empathy_gfi_${Date.now()}`,
519
+ painType: 'user_frustration',
520
+ source: 'user_empathy',
521
+ reason: `Accumulated GFI (${currentGfi.toFixed(1)}) crossed highGfi threshold (${highGfiThreshold}). Matched: ${matchResult.matchedTerms.join(', ')}`,
522
+ score: gfiPainScore,
523
+ sessionId,
524
+ agentId: 'main',
525
+ provenance: 'openclaw_context_bound',
526
+ evidence,
527
+ },
528
+ });
529
+ logger?.info?.(`[PD:Empathy] emitPainDetectedEvent completed (GFI-triggered)`);
530
+ } catch (emitErr) {
531
+ console.error(`[PD:Empathy] FAILED to emit GFI-triggered pain event: ${String(emitErr)}`);
532
+ logger?.warn?.(`[PD:Empathy] Failed to emit GFI-triggered pain event: ${String(emitErr)}`);
533
+ }
534
+ } else {
535
+ logger?.info?.(`[PD:Empathy] GFI-triggered gate rejected: ${gate.detail}`);
536
+ }
537
+ }
639
538
 
640
- // Start keyword optimization subagent to update weights and discover new terms
641
- try {
642
- const recentMessages = extractRecentMessages(event.messages, 10);
643
- const optimizationPrompt = buildOptimizationPrompt(keywordStore, recentMessages);
644
-
645
- logger?.info?.(`[PD:Empathy] Starting optimization subagent with ${recentMessages.length} recent messages`);
646
-
647
- const empathyManager = new EmpathyObserverWorkflowManager({
648
- workspaceDir,
649
- logger: api.logger ?? console,
650
-
651
- subagent: toWorkflowSubagent(api.runtime.subagent),
539
+ // Trigger asynchronous background Empathy Observer deep analysis (Zero Latency)
540
+ const observer = resolveEmpathyObserver(wctx, logger);
541
+ if (observer) {
542
+ const scheduler = new AgentScheduler();
543
+ scheduler.register({
544
+ agentId: 'empathy-observer',
545
+ mode: 'realtime',
546
+ runner: observer,
652
547
  });
548
+
549
+ logger?.info?.(`[PD:Empathy] Triggering background Empathy Observer deep analysis for message: "${msgPreview}"`);
653
550
 
654
- empathyManager.startWorkflow(empathyObserverWorkflowSpec, {
655
- parentSessionId: sessionId,
656
- workspaceDir,
657
- taskInput: { prompt: optimizationPrompt },
658
- }).catch((err) => {
659
- if (!isExpectedSubagentError(err)) {
660
- api.logger?.warn?.(`[PD:Empathy] optimization subagent failed: ${String(err)}`);
661
- }
662
- });
663
- } catch (optErr) {
664
- if (!isExpectedSubagentError(optErr)) {
665
- logger?.warn?.(`[PD:Empathy] Failed to start optimization subagent: ${String(optErr)}`);
666
- }
551
+ void scheduler.dispatch('empathy-observer', { userMessage: latestUserMessage })
552
+ .then(async (result) => {
553
+ if (result.damageDetected) {
554
+ logger?.info?.(`[PD:Empathy] Background Empathy Observer detected damage. Severity: ${result.severity}, Reason: ${result.reason}`);
555
+
556
+ // ── Persistence Contract ──
557
+ const painScore = scoreFromSeverityForSpec(result.severity, wctx);
558
+
559
+ trackFriction(
560
+ sessionId,
561
+ painScore,
562
+ `observer_empathy_${result.severity}`,
563
+ workspaceDir,
564
+ { source: 'user_empathy' }
565
+ );
566
+
567
+ const eventId = `emp_obs_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
568
+ wctx.eventLog.recordPainSignal(sessionId, {
569
+ score: painScore,
570
+ source: 'user_empathy',
571
+ reason: result.reason || 'Empathy observer detected likely user frustration.',
572
+ isRisky: false,
573
+ origin: 'system_infer',
574
+ severity: result.severity,
575
+ confidence: result.confidence,
576
+ detection_mode: 'structured',
577
+ deduped: false,
578
+ trigger_text_excerpt: latestUserMessage.substring(0, 120),
579
+ raw_score: painScore,
580
+ calibrated_score: painScore,
581
+ eventId,
582
+ });
583
+
584
+ try {
585
+ wctx.trajectory?.recordPainEvent?.({
586
+ sessionId,
587
+ source: 'user_empathy',
588
+ score: painScore,
589
+ reason: result.reason || 'Empathy observer detected likely user frustration.',
590
+ severity: result.severity,
591
+ origin: 'system_infer',
592
+ confidence: result.confidence,
593
+ text: latestUserMessage,
594
+ });
595
+ } catch (error) {
596
+ logger?.warn?.(`[PD:Empathy] Failed to persist trajectory: ${String(error)}`);
597
+ }
598
+
599
+ // Check if GFI triggers a pain event post-LLM validation
600
+ const freshSession = getSession(sessionId);
601
+ const freshGfi = freshSession?.currentGfi ?? 0;
602
+ if (freshGfi >= highGfiThreshold) {
603
+ const freshGfiPainScore = Math.min(Math.round(freshGfi), 60);
604
+ const gate = evaluatePainDiagnosticGate({
605
+ source: 'user_empathy',
606
+ score: freshGfiPainScore,
607
+ currentGfi: freshGfi,
608
+ consecutiveErrors: freshSession?.consecutiveErrors ?? 0,
609
+ sessionId,
610
+ errorHash: 'empathy_gfi_threshold',
611
+ thresholds: {
612
+ painTrigger,
613
+ highSeverity: wctx.config.get('severity_thresholds.high') || 70,
614
+ semanticPain: Math.max(painTrigger, 60),
615
+ },
616
+ });
617
+
618
+ if (gate.shouldDiagnose) {
619
+ logger?.info?.(`[PD:Empathy] GFI threshold crossed after background observer. Emitting pain signal...`);
620
+ try {
621
+ const evidence = buildTrajectoryEvidence(wctx, sessionId);
622
+ await emitPainDetectedEvent(wctx, {
623
+ ts: new Date().toISOString(),
624
+ type: 'pain_detected',
625
+ data: {
626
+ painId: `empathy_gfi_${Date.now()}`,
627
+ painType: 'user_frustration',
628
+ source: 'user_empathy',
629
+ reason: `Accumulated GFI (${freshGfi.toFixed(1)}) crossed highGfi threshold (${highGfiThreshold}). Verified by Empathy Observer.`,
630
+ score: freshGfiPainScore,
631
+ sessionId,
632
+ agentId: 'main',
633
+ provenance: 'openclaw_context_bound',
634
+ evidence,
635
+ },
636
+ });
637
+ } catch (emitErr) {
638
+ logger?.error?.(`[PD:Empathy] FAILED to emit observer-triggered pain event: ${String(emitErr)}`);
639
+ }
640
+ }
641
+ }
642
+ } else {
643
+ logger?.info?.(`[PD:Empathy] Background Empathy Observer did not detect any damage.`);
644
+ }
645
+ })
646
+ .catch((err) => {
647
+ logger?.warn?.(`[PD:Empathy] Background analysis failed or rejected: ${String(err)}`);
648
+ });
649
+ }
650
+ } else {
651
+ // Log unmatched messages periodically for coverage analysis
652
+ if (turnCount > 0 && turnCount % 50 === 0) {
653
+ const sampleMsg = latestUserMessage.substring(0, 80).replace(/\n/g, ' ');
654
+ logger?.debug?.(`[PD:Empathy] NO_MATCH: "${sampleMsg}" (turn ${turnCount}, keywords_in_store=${Object.keys(keywordStore.terms).length})`);
667
655
  }
668
656
  }
669
657
 
670
658
  // Periodic summary (every 100 turns)
671
659
  if (turnCount > 0 && turnCount % 100 === 0) {
672
- logger?.info?.(`[PD:Empathy] ${buildSummary()}`);
660
+ const s = getKeywordStoreSummary(keywordStore);
661
+ const highFP = s.highFalsePositiveTerms.slice(0, 5).map(t => `${t.term}(${t.falsePositiveRate.toFixed(2)})`).join(', ');
662
+ 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}]`);
673
663
  }
674
664
 
675
- // Save keyword store on every match to prevent data loss on restart.
676
- // Previously used turnCount % 50 gate which caused hitCount loss because
677
- // module-level state resets on plugin reload before reaching turn 50.
665
+ // Save keyword store on every match
678
666
  if (matchResult.matched) {
679
667
  saveKeywordStore(wctx.stateDir, keywordStore);
680
668
  const {totalHits} = keywordStore.stats;
@@ -685,21 +673,6 @@ The empathy observer subagent handles pain detection independently.
685
673
  }
686
674
  }
687
675
 
688
- // Empathy Observer: analyze user message for frustration signals (legacy, disabled)
689
- // The keyword matching approach above is now the primary empathy detection method.
690
- // The subagent-based observer is kept for periodic keyword optimization only.
691
- // if (workspaceDir) {
692
- // const empathyManager = new EmpathyObserverWorkflowManager({
693
- // workspaceDir,
694
- // logger: api.logger,
695
- // subagent: api.runtime.subagent as any,
696
- // });
697
- // empathyManager.startWorkflow(empathyObserverWorkflowSpec, {
698
- // parentSessionId: sessionId,
699
- // workspaceDir,
700
- // taskInput: latestUserMessage,
701
- // }).catch((err) => api.logger.warn(`[PD:Empathy] workflow failed: ${String(err)}`));
702
- // }
703
676
  }
704
677
 
705
678
  // ──── 4. Heartbeat-specific checklist (also fires for cron-triggered sessions) ────
@@ -728,121 +701,12 @@ ${heartbeatChecklist}
728
701
  }
729
702
  }
730
703
 
731
- // ──── 4b. Inject pending diagnostician tasks (compact summary) ────
732
- // FIX (#283/#380): 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
- //
736
- // INJECTION FORMAT: Compact summary (not full prompt) to stay well within
737
- // OpenClaw's ~10 000 char platform limit. Full task.prompt can be 2–4 KB;
738
- // the compact block is < 400 chars. The agent is instructed to read the
739
- // original from diagnostician_tasks.json if it needs the full context.
740
- try {
741
- const pendingTasks = getPendingDiagnosticianTasks(wctx.stateDir);
742
- if (pendingTasks.length > 0) {
743
- pendingDiagTaskCount = pendingTasks.length;
744
-
745
- // Build compact summary blocks — one per task (only first is processed per heartbeat)
746
- const taskBlocks = pendingTasks
747
- .slice(0, 1)
748
- .map(({ id, task }) => {
749
- // Extract summary fields; reason is truncated to 200 chars to keep
750
- // the injected block small and stable.
751
- const reason = (task.prompt
752
- .match(/reason["\s:]+([^\n]{0,240})/i)?.[1]
753
- ?? task.prompt.slice(0, 200)
754
- ).slice(0, 200);
755
-
756
- const safeId = escapeXml(id);
757
- const safeReason = escapeXml(reason);
758
- const safeCreatedAt = escapeXml(task.createdAt);
759
- const markerFile = `.evolution_complete_${safeId}`;
760
- const reportFile = `.diagnostician_report_${safeId}.json`;
761
-
762
- return `<diagnostician_task id="${safeId}">
763
- task_id: ${safeId}
764
- reason: ${safeReason}
765
- marker: ${markerFile}
766
- report: ${reportFile}
767
- queued_at: ${safeCreatedAt}
768
- action: Analyze pain signal → identify violated principles → write ${markerFile} + ${reportFile}
769
- </diagnostician_task>`;
770
- })
771
- .join('\n\n');
772
-
773
- const processingNote = pendingDiagTaskCount > 1
774
- ? `\n\nNOTE: ${pendingDiagTaskCount - 1} more task(s) are queued. ` +
775
- `Process one at a time; remaining tasks are handled on subsequent heartbeats.`
776
- : '';
777
-
778
- prependContext += `<diagnostician_tasks pending="${pendingDiagTaskCount}">
779
- You are acting as a **Pain Diagnostician**. For each task:
780
- 1. Read the full prompt from: ${escapeXml(wctx.stateDir)}/diagnostician_tasks.json [task_id=${escapeXml(pendingTasks[0]?.id ?? '')}]
781
- 2. Analyze the pain signal and its context
782
- 3. Identify the root cause and violated principles
783
- 4. Write a completion marker: .evolution_complete_<TASK_ID>
784
- 5. Write a diagnostic report: .diagnostician_report_<TASK_ID>.json
785
-
786
- ${taskBlocks}${processingNote}
787
- </diagnostician_tasks>\n`;
788
-
789
- logger?.info?.(
790
- `[PD:Prompt] Injected compact diagnostician task block ` +
791
- `(task=${pendingTasks[0]?.id}, total_pending=${pendingDiagTaskCount})`
792
- );
793
-
794
- // C: Record heartbeat_diagnosis event for observability
795
- try {
796
- const eventLog = EventLogService.get(wctx.stateDir, logger);
797
- eventLog.recordHeartbeatDiagnosis({
798
- taskCount: pendingDiagTaskCount,
799
- taskIds: pendingTasks.slice(0, 1).map(t => t.id),
800
- trigger: 'heartbeat',
801
- });
802
- } catch (evErr) {
803
- logger?.warn?.(`[PD:Prompt] Failed to record heartbeat_diagnosis event: ${String(evErr)}`);
804
- }
805
- }
806
- } catch (e) {
807
- logger?.warn?.(`[PD:Prompt] Failed to read diagnostician tasks: ${String(e)}`);
808
- }
809
704
  }
810
705
 
811
706
  // ──── 6. Dynamic Attitude Matrix (based on GFI) ────
812
-
813
-
814
- let attitudeDirective: string;
707
+
815
708
  const currentGfi = session?.currentGfi || 0;
816
-
817
- if (currentGfi >= 70) {
818
- attitudeDirective = `
819
- ### 【SYSTEM_MODE: HUMBLE_RECOVERY】
820
- **CURRENT STATUS**: Severe system friction / User frustration detected (GFI: ${currentGfi.toFixed(0)}).
821
- **BEHAVIORAL OVERRIDE**:
822
- - You have failed to meet expectations. Humility is your primary directive.
823
- - **STOP** aggressive file modifications.
824
- - **START** every response with a sincere, non-defensive apology.
825
- - **ACTION**: Explain why you failed, and propose a highly cautious recovery plan.
826
- `;
827
- } else if (currentGfi >= 40) {
828
- attitudeDirective = `
829
- ### 【SYSTEM_MODE: CONCILIATORY】
830
- **CURRENT STATUS**: Moderate friction detected (GFI: ${currentGfi.toFixed(0)}).
831
- **BEHAVIORAL OVERRIDE**:
832
- - User is frustrated. Be more explanatory and cautious.
833
- - Before executing any tool, clearly state what you intend to do and **WAIT** for implicit or explicit user consent.
834
- - Avoid technical jargon; focus on the business/project value of your changes.
835
- `;
836
- } else {
837
- attitudeDirective = `
838
- ### 【SYSTEM_MODE: EFFICIENT】
839
- **CURRENT STATUS**: System healthy (GFI: ${currentGfi.toFixed(0)}).
840
- **BEHAVIORAL OVERRIDE**:
841
- - Maintain peak efficiency.
842
- - Be concise. Prefer action over long explanations.
843
- - Follow the "Principles > Directives" rule strictly.
844
- `;
845
- }
709
+ const attitudeDirective = buildAttitudeDirective(currentGfi);
846
710
 
847
711
  // ──── 7. appendSystemContext: Principles + Thinking OS + reflection_log + project_context ────
848
712
  // NOTE: Principles is ALWAYS injected (not configurable)
@@ -918,7 +782,7 @@ ${taskBlocks}${processingNote}
918
782
  projectContextContent = extractSummary(finalContent, 30);
919
783
  } else {
920
784
  // Full mode: current version + recent history (3 versions)
921
- const historyVersions = getHistoryVersions(focusPath, 3);
785
+ const historyVersions = await getHistoryVersions(focusPath, 3);
922
786
  if (historyVersions.length > 0) {
923
787
  const historySections = historyVersions.map((v, i) =>
924
788
  `\n---\n\n**历史版本 v${historyVersions.length - i}**\n\n${v}`
@@ -943,13 +807,33 @@ ${taskBlocks}${processingNote}
943
807
  const allActive = reducer.getActivePrinciples();
944
808
  const allProbation = reducer.getProbationPrinciples();
945
809
 
810
+ // Pruning mask: exclude principles whose latest review is archive-candidate
811
+ let maskedIds = new Set<string>();
812
+ try {
813
+ maskedIds = getCachedMaskedPrincipleSet(wctx.workspaceDir);
814
+ } catch (err) {
815
+ // Safe degradation: if review log unreadable, inject all principles
816
+ const msg = err instanceof Error ? err.message : String(err);
817
+ if (logger?.info) {
818
+ logger.info(`[PD:Pruning] Failed to read review log — all principles injected: ${msg}`);
819
+ } else {
820
+ console.error(`[PD:Pruning] Failed to read review log — all principles injected: ${msg}`);
821
+ }
822
+ }
823
+
946
824
  // Budget-aware selection: prioritize P0>P1>P2 and recency
947
- const activeSelection = selectPrinciplesForInjection(allActive, DEFAULT_PRINCIPLE_BUDGET);
825
+ const activeSelection = selectPrinciplesForInjection(
826
+ allActive.filter(p => !maskedIds.has(p.id)),
827
+ DEFAULT_PRINCIPLE_BUDGET,
828
+ );
948
829
  const active = activeSelection.selected;
949
830
 
950
831
  // Probation principles get a smaller sub-budget (1000 chars)
951
832
  const probationBudget = 1000;
952
- const probationSelection = selectPrinciplesForInjection(allProbation, probationBudget);
833
+ const probationSelection = selectPrinciplesForInjection(
834
+ allProbation.filter(p => !maskedIds.has(p.id)),
835
+ probationBudget,
836
+ );
953
837
  const probation = probationSelection.selected;
954
838
 
955
839
  if (activeSelection.wasTruncated || probationSelection.wasTruncated) {
@@ -986,13 +870,91 @@ ${taskBlocks}${processingNote}
986
870
  logger?.warn?.(`[PD:Prompt] Failed to load evolution principles: ${String(e)}`);
987
871
  }
988
872
 
873
+ let runtimeV2PrinciplesContent = '';
874
+ const runtimeV2PrincipleIds = new Set<string>();
875
+ // Hoisted so the owner_approved_behavior_directives section can access them
876
+ let dedupedV2: Array<{ principleId: string; text: string; artifactId: string; activationId: string }> = [];
877
+ try {
878
+ const reader = new PromptActivationReader(wctx.workspaceDir, { logger });
879
+ const v2Result = await reader.readActivatedPrinciples();
880
+
881
+ if (v2Result.warnings.length > 0) {
882
+ logger?.info?.(`[PD:RuntimeV2] Activation read warnings: ${v2Result.warnings.join('; ')}`);
883
+ }
884
+
885
+ const legacyActiveIds = new Set<string>();
886
+ try {
887
+ const legacyActive = wctx.evolutionReducer.getActivePrinciples();
888
+ for (const p of legacyActive) {
889
+ legacyActiveIds.add(p.id);
890
+ }
891
+ const legacyProbation = wctx.evolutionReducer.getProbationPrinciples();
892
+ for (const p of legacyProbation) {
893
+ legacyActiveIds.add(p.id);
894
+ }
895
+ } catch {
896
+ // best-effort dedup
897
+ }
898
+
899
+ dedupedV2 = v2Result.principles.filter((p) => !legacyActiveIds.has(p.principleId));
900
+
901
+ if (dedupedV2.length > 0) {
902
+ let remaining = RUNTIME_V2_PRINCIPLE_BUDGET;
903
+ const lines: string[] = [];
904
+ lines.push('Runtime V2 activated principles (owner-approved):');
905
+ remaining -= 'Runtime V2 activated principles (owner-approved):'.length;
906
+
907
+ for (const p of dedupedV2) {
908
+ const entry = `- [${escapeXml(p.principleId)}] ${escapeXml(p.text)}`;
909
+ if (remaining < entry.length + 1) {
910
+ logger?.info?.(`[PD:RuntimeV2] Principle budget reached (${RUNTIME_V2_PRINCIPLE_BUDGET}c) — truncating after ${lines.length - 1} principles`);
911
+ break;
912
+ }
913
+ lines.push(entry);
914
+ remaining -= entry.length + 1;
915
+ runtimeV2PrincipleIds.add(p.principleId);
916
+ }
917
+ runtimeV2PrinciplesContent = lines.join('\n');
918
+ }
919
+
920
+ // ── Emit structured observability event ──
921
+ try {
922
+ const eventLog = wctx.eventLog;
923
+ eventLog.recordRuntimeV2ActivationsInjected({
924
+ sessionId: sessionId ?? 'unknown',
925
+ workspaceDir: wctx.workspaceDir,
926
+ principleIds: [...runtimeV2PrincipleIds],
927
+ activationIds: dedupedV2.map((p) => p.activationId),
928
+ artifactIds: dedupedV2.map((p) => p.artifactId),
929
+ injectedCount: runtimeV2PrincipleIds.size,
930
+ skippedWarnings: v2Result.warnings,
931
+ injectedCharCount: runtimeV2PrinciplesContent.length,
932
+ budget: RUNTIME_V2_PRINCIPLE_BUDGET,
933
+ ...(runtimeV2PrincipleIds.size === 0
934
+ ? {
935
+ skipReason: v2Result.principles.length === 0
936
+ ? 'no_validated_activations'
937
+ : 'all_deduped_against_legacy',
938
+ nextAction: v2Result.principles.length === 0
939
+ ? 'check activations table for prompt channel rows with validated artifacts'
940
+ : 'legacy evolution reducer already contains these principle IDs',
941
+ }
942
+ : {}),
943
+ });
944
+ } catch (logErr) {
945
+ logger?.warn?.(`[PD:RuntimeV2] Failed to emit activation observability event: ${String(logErr)}`);
946
+ }
947
+ } catch (e) {
948
+ logger?.warn?.(`[PD:RuntimeV2] Failed to read Runtime V2 prompt activations: ${String(e)}`);
949
+ }
950
+
989
951
  // Build appendSystemContext with recency effect
990
952
  // Content order (most important last): behavioral_constraints -> project_context -> working_memory -> reflection_log -> thinking_os -> principles
991
953
  const appendParts: string[] = [];
992
954
 
993
955
  // 0. Behavioral Constraints (empathy observer coordination)
994
956
  // Injected here (appendSystemContext) instead of prependContext to hide from WebUI users.
995
- // See: https://github.com/csuzngjh/principles/issues/XXX
957
+ // Behavioral constraints: empathy observer coordination
996
958
  if (shouldInjectBehavioralConstraints) {
997
959
  appendParts.push(`<behavioral_constraints>
998
960
  ${empathySilenceConstraint}
@@ -1014,11 +976,35 @@ ${empathySilenceConstraint}
1014
976
  appendParts.push(`<thinking_os>\n${thinkingOsContent}\n</thinking_os>`);
1015
977
  }
1016
978
 
1017
- // 3. Evolution Loop principles (active/probation)
979
+ // 3. Evolution Loop principles (legacy active/probation only — Runtime V2 moved to section 3.5)
1018
980
  if (evolutionPrinciplesContent) {
1019
981
  appendParts.push(`<evolution_principles>\n${evolutionPrinciplesContent}\n</evolution_principles>`);
1020
982
  }
1021
983
 
984
+ // 3.5. Owner-Approved Behavior Directives (Runtime V2 activated principles)
985
+ // PLACED IN prependSystemContext (before gateway system prompt) for highest LLM attention.
986
+ // These are owner-reviewed, validated behavior constraints — not background context.
987
+ if (runtimeV2PrincipleIds.size > 0) {
988
+ const directiveLines: string[] = [];
989
+ directiveLines.push('');
990
+ directiveLines.push('## 【OWNER-APPROVED BEHAVIOR DIRECTIVES】');
991
+ directiveLines.push('');
992
+ directiveLines.push('Owner-approved behavior directives are active operating constraints learned from prior owner corrections.');
993
+ directiveLines.push('These directives are mandatory for this session unless they conflict with safety, security, or higher-priority system policy.');
994
+ directiveLines.push('For ambiguous coding or file-changing tasks, follow these directives before using mutating tools.');
995
+ directiveLines.push('');
996
+ for (const p of dedupedV2) {
997
+ if (!runtimeV2PrincipleIds.has(p.principleId)) continue;
998
+ directiveLines.push(`<directive id="${escapeXml(p.principleId)}" source="runtime_v2_activation">`);
999
+ directiveLines.push(`MANDATORY: ${escapeXml(p.text)}`);
1000
+ directiveLines.push('Apply this as an active behavior constraint. Do not treat this as background context.');
1001
+ directiveLines.push('</directive>');
1002
+ directiveLines.push('');
1003
+ }
1004
+ directiveLines.push('Note: These directives do not override safety, security, or core system policy.');
1005
+ prependSystemContext += directiveLines.join('\n');
1006
+ }
1007
+
1022
1008
  // Routing Guidance (section 5 — injected between evolution principles and core principles)
1023
1009
  // Inject delegation guidance when task is bounded + deployment allowed + not high-entropy.
1024
1010
  // This is a non-authoritative suggestion — the main agent decides whether to follow.
@@ -1047,7 +1033,6 @@ ${empathySilenceConstraint}
1047
1033
  const routingInput: RoutingInput = {
1048
1034
  taskIntent: toolMatches[0] ?? undefined,
1049
1035
  taskDescription: latestUserText.trim(),
1050
- requestedTools: toolMatches.length > 0 ? toolMatches : undefined,
1051
1036
  requestedFiles: fileMatches.length > 0 ? fileMatches : undefined,
1052
1037
  };
1053
1038
 
@@ -1158,110 +1143,34 @@ ${attitudeDirective}
1158
1143
  }
1159
1144
 
1160
1145
  // ──── 8. SIZE GUARD ────
1161
- // Hard cap for OpenClaw prompt injection. OpenClaw's actual platform limit is
1162
- // approximately 10 000 characters. We use 9 000 here to leave ~1 000 chars of
1163
- // headroom for the user's message, tool call delimiters, and encoding overhead.
1164
- // IMPORTANT: PD must never treat the platform's upper bound as its own safe
1165
- // working limit. Always keep a margin.
1166
- const totalSize = prependSystemContext.length + prependContext.length + appendSystemContext.length;
1167
- const MAX_INJECTION_SIZE = 9000;
1168
-
1169
- if (totalSize > MAX_INJECTION_SIZE) {
1170
- const originalSize = totalSize;
1171
- const truncationLog: string[] = [];
1172
-
1173
- // Deterministically remove low-priority context blocks in priority order.
1174
- // In diagnostician-priority mode we aggressively strip everything except
1175
- // the task block and minimum behavioral constraints.
1176
- const inDiagMode = pendingDiagTaskCount > 0;
1177
-
1178
- // Step 1 — strip project_context (largest, lowest priority) — always in diag mode,
1179
- // only strip in normal mode if we are already over limit
1180
- if (projectContextContent && appendSystemContext.includes('<project_context>')) {
1181
- appendSystemContext = appendSystemContext.replace(
1182
- `<project_context>\n${projectContextContent}\n</project_context>`,
1183
- '<project_context>\n[stripped: project_context]\n</project_context>'
1184
- );
1185
- truncationLog.push('project_context');
1186
- }
1187
-
1188
- // Steps 2-4: only strip in diagnostician priority mode (inDiagMode)
1189
- // In normal mode we stop after project_context to preserve context quality
1190
- if (inDiagMode) {
1191
- // Step 2 — strip thinking_os
1192
- if (thinkingOsContent && appendSystemContext.includes('<thinking_os>')) {
1193
- appendSystemContext = appendSystemContext.replace(
1194
- `<thinking_os>\n${thinkingOsContent}\n</thinking_os>`,
1195
- '<thinking_os>\n[stripped: thinking_os]\n</thinking_os>'
1196
- );
1197
- truncationLog.push('thinking_os');
1198
- }
1199
-
1200
- // Step 3 — strip evolution_principles (keep core_principles only)
1201
- if (evolutionPrinciplesContent && appendSystemContext.includes('<evolution_principles>')) {
1202
- appendSystemContext = appendSystemContext.replace(
1203
- `<evolution_principles>\n${evolutionPrinciplesContent}\n</evolution_principles>`,
1204
- '<evolution_principles>\n[stripped: evolution_principles]\n</evolution_principles>'
1205
- );
1206
- truncationLog.push('evolution_principles');
1207
- }
1208
-
1209
- // Step 4 — strip reflection_log if present
1210
- if (appendSystemContext.includes('<reflection_log>')) {
1211
- appendSystemContext = appendSystemContext.replace(
1212
- /<reflection_log>[\s\S]*?<\/reflection_log>/,
1213
- '<reflection_log>\n[stripped: reflection_log]\n</reflection_log>'
1214
- );
1215
- truncationLog.push('reflection_log');
1216
- }
1217
- }
1218
-
1219
- // Step 5 — re-evaluate: check if still over limit
1220
- let newSize = prependSystemContext.length + prependContext.length + appendSystemContext.length;
1221
- if (newSize > MAX_INJECTION_SIZE) {
1222
- // Truncate the injected reason field by finding the "reason:" line prefix
1223
- // and cutting to 120 chars. This is safe because the full prompt is
1224
- // still available in diagnostician_tasks.json for the agent to read.
1225
- prependContext = prependContext
1226
- .split('\n')
1227
- .map((line) => {
1228
- if (line.startsWith('reason: ') && line.length > 129) {
1229
- return line.slice(0, 129) + '...[truncated]';
1230
- }
1231
- return line;
1232
- })
1233
- .join('\n');
1234
- newSize = prependSystemContext.length + prependContext.length + appendSystemContext.length;
1235
- truncationLog.push('diagnostician_reason');
1146
+ // Delegates to @principles/core/prompt-builder/truncateInjectionToBudget
1147
+ // which handles priority stripping: project_context thinking_os
1148
+ // evolution_principles reflection_log reason: truncation fallback.
1149
+ const result = truncateInjectionToBudget(
1150
+ prependSystemContext,
1151
+ prependContext,
1152
+ appendSystemContext,
1153
+ {
1154
+ diagnosticianMode: pendingDiagTaskCount > 0,
1155
+ blocks: { projectContextContent, thinkingOsContent, evolutionPrinciplesContent },
1236
1156
  }
1157
+ );
1237
1158
 
1238
- // FAIL-CLOSED: if we are still over the limit after all deterministic
1239
- // removals, do NOT return a prompt that exceeds MAX_INJECTION_SIZE.
1240
- // Drop the entire appendSystemContext (keep only prependContext +
1241
- // prependSystemContext) and log a hard error.
1242
- if (newSize > MAX_INJECTION_SIZE) {
1243
- const fallbackContext = `
1244
- ## 【CONTEXT SECTIONS】
1245
-
1246
- [WARNING: Context sections stripped due to prompt size constraints.
1247
- This is a diagnostician-priority session — see diagnostician_tasks.json for full task context.]
1248
-
1249
- ${attitudeDirective}
1250
- `.trim();
1251
-
1252
- appendSystemContext = fallbackContext;
1253
- newSize = prependSystemContext.length + prependContext.length + appendSystemContext.length;
1159
+ prependSystemContext = result.prependSystemContext;
1160
+ prependContext = result.prependContext;
1161
+ appendSystemContext = result.appendSystemContext;
1254
1162
 
1163
+ if (result.truncated) {
1164
+ const logEntry = result.truncationLog.join(', ');
1165
+ if (result.appendSystemContext.includes('[WARNING: Context sections stripped')) {
1255
1166
  logger?.error(
1256
1167
  `[PD:Prompt] PROMPT OVER LIMIT AFTER ALL REDUCTIONS — using fallback. ` +
1257
- `Original: ${originalSize}, Current: ${newSize}, Limit: ${MAX_INJECTION_SIZE}. ` +
1258
- `Stripped: ${truncationLog.join(', ')}. Diagnostician mode: ${inDiagMode}.`
1168
+ `Diagnostician mode: ${pendingDiagTaskCount > 0}. Stripped: ${logEntry}.`
1259
1169
  );
1260
1170
  } else {
1261
1171
  logger?.warn(
1262
- `[PD:Prompt] Injection size exceeded: ${originalSize} chars (limit: ${MAX_INJECTION_SIZE}), ` +
1263
- `truncated: ${truncationLog.join(', ') || 'none'}, new size: ${newSize} chars, ` +
1264
- `diagnostician mode: ${inDiagMode}`
1172
+ `[PD:Prompt] Injection size exceeded budget, truncated: ${logEntry || 'none'}, ` +
1173
+ `diagnostician mode: ${pendingDiagTaskCount > 0}`
1265
1174
  );
1266
1175
  }
1267
1176
  }
@@ -1272,3 +1181,54 @@ ${attitudeDirective}
1272
1181
  appendSystemContext
1273
1182
  };
1274
1183
  }
1184
+
1185
+ // ── Empathy Observer Hybrid Deep Analysis Helpers (Unified SDK Migration) ──
1186
+
1187
+ function scoreFromSeverityForSpec(severity: string | undefined, wctx: WorkspaceContext): number {
1188
+ if (severity === 'severe') return Number(wctx.config.get('empathy_engine.penalties.severe') ?? 40);
1189
+ if (severity === 'moderate') return Number(wctx.config.get('empathy_engine.penalties.moderate') ?? 25);
1190
+ return Number(wctx.config.get('empathy_engine.penalties.mild') ?? 10);
1191
+ }
1192
+
1193
+ function resolveEmpathyObserver(wctx: WorkspaceContext, logger?: Pick<PluginLogger, 'info' | 'warn' | 'error' | 'debug'>): EmpathyObserver | null {
1194
+ try {
1195
+ const loader = new WorkflowFunnelLoader(wctx.stateDir);
1196
+ const funnel = loader.getFunnel('pd-empathy-observer');
1197
+ const policy = funnel?.policy;
1198
+ if (!policy || policy.runtimeKind !== 'pi-ai') {
1199
+ logger?.debug?.('[PD:Empathy] workflows.yaml pd-empathy-observer policy not found. Falling back to environment variables.');
1200
+ const provider = process.env.PD_EMPATHY_PROVIDER || 'anthropic';
1201
+ const model = process.env.PD_EMPATHY_MODEL || 'anthropic/claude-3-5-sonnet';
1202
+ const apiKeyEnv = process.env.PD_EMPATHY_API_KEY_ENV || 'ANTHROPIC_API_KEY';
1203
+ const baseUrl = process.env.PD_EMPATHY_BASE_URL;
1204
+
1205
+ if (!process.env[apiKeyEnv]) {
1206
+ logger?.debug?.(`[PD:Empathy] Empathy observer API key env ${apiKeyEnv} is not set. Background analysis disabled.`);
1207
+ return null;
1208
+ }
1209
+
1210
+ const adapter = new PiAiRuntimeAdapter({
1211
+ provider,
1212
+ model,
1213
+ apiKeyEnv,
1214
+ baseUrl,
1215
+ workspace: wctx.workspaceDir,
1216
+ });
1217
+ return new EmpathyObserver({ runtimeAdapter: adapter });
1218
+ }
1219
+
1220
+ const adapter = new PiAiRuntimeAdapter({
1221
+ provider: String(policy.provider),
1222
+ model: String(policy.model),
1223
+ apiKeyEnv: String(policy.apiKeyEnv),
1224
+ maxRetries: policy.maxRetries,
1225
+ timeoutMs: policy.timeoutMs ?? 30_000,
1226
+ baseUrl: policy.baseUrl,
1227
+ workspace: wctx.workspaceDir,
1228
+ });
1229
+ return new EmpathyObserver({ runtimeAdapter: adapter }, { timeoutMs: policy.timeoutMs });
1230
+ } catch (err) {
1231
+ logger?.warn?.(`[PD:Empathy] Failed to resolve EmpathyObserver: ${String(err)}`);
1232
+ return null;
1233
+ }
1234
+ }