principles-disciple 1.72.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 +459 -439
  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 +59 -16
  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 +32 -17
  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 +3 -1
  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
 
@@ -535,7 +483,9 @@ The empathy observer subagent handles pain detection independently.
535
483
 
536
484
  const isUserInteraction = trigger === 'user' || trigger === 'api' || !trigger;
537
485
 
486
+ // Empathy Observer: keyword fast-path + optional LLM deep analysis (zero latency async dispatch)
538
487
  const empathyEnabled = wctx.config.get('empathy_engine.enabled') !== false;
488
+
539
489
  logger?.info?.(`[PD:Empathy] Conditions: enabled=${empathyEnabled}, isUser=${isUserInteraction}, sessionId=${!!sessionId}, api=${!!api}, !agentToAgent=${!isAgentToAgent}, workspaceDir=${!!workspaceDir}, hasMessage=${!!latestUserMessage}`);
540
490
 
541
491
  // Track if we should inject behavioral constraints (will be added to appendSystemContext later)
@@ -564,23 +514,6 @@ The empathy observer subagent handles pain detection independently.
564
514
  _empathyTurnCounter++;
565
515
  const turnCount = _empathyTurnCounter;
566
516
 
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
517
  if (matchResult.matched) {
585
518
  const penalty = severityToPenalty(matchResult.severity, DEFAULT_EMPATHY_KEYWORD_CONFIG);
586
519
  // trackFriction signature: (sessionId, deltaF: number, hash: string, workspaceDir?, options?)
@@ -588,93 +521,194 @@ The empathy observer subagent handles pain detection independently.
588
521
  source: 'user_empathy',
589
522
  });
590
523
 
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
- }
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
+ });
627
548
 
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
- };
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
+ });
634
555
 
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()}`);
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
+ }
639
584
 
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),
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,
652
593
  });
594
+
595
+ logger?.info?.(`[PD:Empathy] Triggering background Empathy Observer deep analysis for message: "${msgPreview}"`);
653
596
 
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
- }
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})`);
667
701
  }
668
702
  }
669
703
 
670
704
  // Periodic summary (every 100 turns)
671
705
  if (turnCount > 0 && turnCount % 100 === 0) {
672
- 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}]`);
673
709
  }
674
710
 
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.
711
+ // Save keyword store on every match
678
712
  if (matchResult.matched) {
679
713
  saveKeywordStore(wctx.stateDir, keywordStore);
680
714
  const {totalHits} = keywordStore.stats;
@@ -685,21 +719,6 @@ The empathy observer subagent handles pain detection independently.
685
719
  }
686
720
  }
687
721
 
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
722
  }
704
723
 
705
724
  // ──── 4. Heartbeat-specific checklist (also fires for cron-triggered sessions) ────
@@ -728,121 +747,12 @@ ${heartbeatChecklist}
728
747
  }
729
748
  }
730
749
 
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
750
  }
810
751
 
811
752
  // ──── 6. Dynamic Attitude Matrix (based on GFI) ────
812
-
813
-
814
- let attitudeDirective: string;
753
+
815
754
  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
- }
755
+ const attitudeDirective = buildAttitudeDirective(currentGfi);
846
756
 
847
757
  // ──── 7. appendSystemContext: Principles + Thinking OS + reflection_log + project_context ────
848
758
  // NOTE: Principles is ALWAYS injected (not configurable)
@@ -918,7 +828,7 @@ ${taskBlocks}${processingNote}
918
828
  projectContextContent = extractSummary(finalContent, 30);
919
829
  } else {
920
830
  // Full mode: current version + recent history (3 versions)
921
- const historyVersions = getHistoryVersions(focusPath, 3);
831
+ const historyVersions = await getHistoryVersions(focusPath, 3);
922
832
  if (historyVersions.length > 0) {
923
833
  const historySections = historyVersions.map((v, i) =>
924
834
  `\n---\n\n**历史版本 v${historyVersions.length - i}**\n\n${v}`
@@ -943,13 +853,33 @@ ${taskBlocks}${processingNote}
943
853
  const allActive = reducer.getActivePrinciples();
944
854
  const allProbation = reducer.getProbationPrinciples();
945
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
+
946
870
  // Budget-aware selection: prioritize P0>P1>P2 and recency
947
- const activeSelection = selectPrinciplesForInjection(allActive, DEFAULT_PRINCIPLE_BUDGET);
871
+ const activeSelection = selectPrinciplesForInjection(
872
+ allActive.filter(p => !maskedIds.has(p.id)),
873
+ DEFAULT_PRINCIPLE_BUDGET,
874
+ );
948
875
  const active = activeSelection.selected;
949
876
 
950
877
  // Probation principles get a smaller sub-budget (1000 chars)
951
878
  const probationBudget = 1000;
952
- const probationSelection = selectPrinciplesForInjection(allProbation, probationBudget);
879
+ const probationSelection = selectPrinciplesForInjection(
880
+ allProbation.filter(p => !maskedIds.has(p.id)),
881
+ probationBudget,
882
+ );
953
883
  const probation = probationSelection.selected;
954
884
 
955
885
  if (activeSelection.wasTruncated || probationSelection.wasTruncated) {
@@ -986,13 +916,105 @@ ${taskBlocks}${processingNote}
986
916
  logger?.warn?.(`[PD:Prompt] Failed to load evolution principles: ${String(e)}`);
987
917
  }
988
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
+
989
1011
  // Build appendSystemContext with recency effect
990
1012
  // Content order (most important last): behavioral_constraints -> project_context -> working_memory -> reflection_log -> thinking_os -> principles
991
1013
  const appendParts: string[] = [];
992
1014
 
993
1015
  // 0. Behavioral Constraints (empathy observer coordination)
994
1016
  // Injected here (appendSystemContext) instead of prependContext to hide from WebUI users.
995
- // See: https://github.com/csuzngjh/principles/issues/XXX
1017
+ // Behavioral constraints: empathy observer coordination
996
1018
  if (shouldInjectBehavioralConstraints) {
997
1019
  appendParts.push(`<behavioral_constraints>
998
1020
  ${empathySilenceConstraint}
@@ -1014,11 +1036,35 @@ ${empathySilenceConstraint}
1014
1036
  appendParts.push(`<thinking_os>\n${thinkingOsContent}\n</thinking_os>`);
1015
1037
  }
1016
1038
 
1017
- // 3. Evolution Loop principles (active/probation)
1039
+ // 3. Evolution Loop principles (legacy active/probation only — Runtime V2 moved to section 3.5)
1018
1040
  if (evolutionPrinciplesContent) {
1019
1041
  appendParts.push(`<evolution_principles>\n${evolutionPrinciplesContent}\n</evolution_principles>`);
1020
1042
  }
1021
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
+
1022
1068
  // Routing Guidance (section 5 — injected between evolution principles and core principles)
1023
1069
  // Inject delegation guidance when task is bounded + deployment allowed + not high-entropy.
1024
1070
  // This is a non-authoritative suggestion — the main agent decides whether to follow.
@@ -1047,7 +1093,6 @@ ${empathySilenceConstraint}
1047
1093
  const routingInput: RoutingInput = {
1048
1094
  taskIntent: toolMatches[0] ?? undefined,
1049
1095
  taskDescription: latestUserText.trim(),
1050
- requestedTools: toolMatches.length > 0 ? toolMatches : undefined,
1051
1096
  requestedFiles: fileMatches.length > 0 ? fileMatches : undefined,
1052
1097
  };
1053
1098
 
@@ -1158,110 +1203,34 @@ ${attitudeDirective}
1158
1203
  }
1159
1204
 
1160
1205
  // ──── 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');
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 },
1236
1216
  }
1217
+ );
1237
1218
 
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;
1219
+ prependSystemContext = result.prependSystemContext;
1220
+ prependContext = result.prependContext;
1221
+ appendSystemContext = result.appendSystemContext;
1254
1222
 
1223
+ if (result.truncated) {
1224
+ const logEntry = result.truncationLog.join(', ');
1225
+ if (result.appendSystemContext.includes('[WARNING: Context sections stripped')) {
1255
1226
  logger?.error(
1256
1227
  `[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}.`
1228
+ `Diagnostician mode: ${pendingDiagTaskCount > 0}. Stripped: ${logEntry}.`
1259
1229
  );
1260
1230
  } else {
1261
1231
  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}`
1232
+ `[PD:Prompt] Injection size exceeded budget, truncated: ${logEntry || 'none'}, ` +
1233
+ `diagnostician mode: ${pendingDiagTaskCount > 0}`
1265
1234
  );
1266
1235
  }
1267
1236
  }
@@ -1272,3 +1241,54 @@ ${attitudeDirective}
1272
1241
  appendSystemContext
1273
1242
  };
1274
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
+ }