principles-disciple 1.11.0 → 1.13.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 (233) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +66 -0
  3. package/esbuild.config.js +1 -1
  4. package/openclaw.plugin.json +4 -4
  5. package/package.json +2 -3
  6. package/run-nocturnal.mjs +30 -0
  7. package/scripts/db-migrate.mjs +170 -0
  8. package/scripts/sync-plugin.mjs +250 -6
  9. package/src/commands/archive-impl.ts +136 -0
  10. package/src/commands/capabilities.ts +4 -2
  11. package/src/commands/context.ts +5 -1
  12. package/src/commands/disable-impl.ts +151 -0
  13. package/src/commands/evolution-status.ts +64 -19
  14. package/src/commands/export.ts +8 -6
  15. package/src/commands/focus.ts +8 -20
  16. package/src/commands/nocturnal-review.ts +5 -7
  17. package/src/commands/nocturnal-rollout.ts +1 -12
  18. package/src/commands/nocturnal-train.ts +21 -47
  19. package/src/commands/pain.ts +10 -5
  20. package/src/commands/principle-rollback.ts +4 -2
  21. package/src/commands/promote-impl.ts +274 -0
  22. package/src/commands/rollback-impl.ts +234 -0
  23. package/src/commands/rollback.ts +6 -3
  24. package/src/commands/samples.ts +2 -0
  25. package/src/commands/thinking-os.ts +3 -4
  26. package/src/commands/workflow-debug.ts +2 -1
  27. package/src/config/errors.ts +1 -0
  28. package/src/core/AGENTS.md +34 -0
  29. package/src/core/adaptive-thresholds.ts +4 -3
  30. package/src/core/code-implementation-storage.ts +241 -0
  31. package/src/core/config.ts +5 -2
  32. package/src/core/control-ui-db.ts +29 -10
  33. package/src/core/detection-funnel.ts +12 -7
  34. package/src/core/diagnostician-task-store.ts +156 -0
  35. package/src/core/dictionary.ts +4 -4
  36. package/src/core/empathy-keyword-matcher.ts +7 -3
  37. package/src/core/empathy-types.ts +13 -2
  38. package/src/core/event-log.ts +14 -6
  39. package/src/core/evolution-engine.ts +27 -31
  40. package/src/core/evolution-logger.ts +3 -2
  41. package/src/core/evolution-reducer.ts +110 -31
  42. package/src/core/evolution-types.ts +10 -0
  43. package/src/core/external-training-contract.ts +1 -0
  44. package/src/core/focus-history.ts +38 -24
  45. package/src/core/hygiene/tracker.ts +10 -6
  46. package/src/core/init.ts +5 -2
  47. package/src/core/migration.ts +3 -3
  48. package/src/core/model-deployment-registry.ts +6 -4
  49. package/src/core/model-training-registry.ts +5 -3
  50. package/src/core/nocturnal-arbiter.ts +13 -14
  51. package/src/core/nocturnal-artifact-lineage.ts +117 -0
  52. package/src/core/nocturnal-artificer.ts +257 -0
  53. package/src/core/nocturnal-candidate-scoring.ts +4 -2
  54. package/src/core/nocturnal-compliance.ts +67 -19
  55. package/src/core/nocturnal-dataset.ts +95 -2
  56. package/src/core/nocturnal-executability.ts +2 -3
  57. package/src/core/nocturnal-export.ts +6 -3
  58. package/src/core/nocturnal-rule-implementation-validator.ts +245 -0
  59. package/src/core/nocturnal-trajectory-extractor.ts +10 -3
  60. package/src/core/nocturnal-trinity.ts +319 -61
  61. package/src/core/pain-context-extractor.ts +29 -15
  62. package/src/core/pain.ts +7 -5
  63. package/src/core/path-resolver.ts +16 -15
  64. package/src/core/paths.ts +2 -1
  65. package/src/core/pd-task-reconciler.ts +463 -0
  66. package/src/core/pd-task-service.ts +42 -0
  67. package/src/core/pd-task-store.ts +77 -0
  68. package/src/core/pd-task-types.ts +128 -0
  69. package/src/core/principle-internalization/deprecated-readiness.ts +91 -0
  70. package/src/core/principle-internalization/internalization-routing-policy.ts +208 -0
  71. package/src/core/principle-internalization/lifecycle-metrics.ts +149 -0
  72. package/src/core/principle-internalization/lifecycle-read-model.ts +243 -0
  73. package/src/core/principle-internalization/lifecycle-refresh.ts +11 -0
  74. package/src/core/principle-internalization/principle-lifecycle-service.ts +167 -0
  75. package/src/core/principle-training-state.ts +95 -370
  76. package/src/core/principle-tree-ledger.ts +733 -0
  77. package/src/core/principle-tree-migration.ts +195 -0
  78. package/src/core/profile.ts +3 -1
  79. package/src/core/promotion-gate.ts +14 -18
  80. package/src/core/replay-engine.ts +562 -0
  81. package/src/core/risk-calculator.ts +6 -4
  82. package/src/core/rule-host-helpers.ts +39 -0
  83. package/src/core/rule-host-types.ts +82 -0
  84. package/src/core/rule-host.ts +245 -0
  85. package/src/core/rule-implementation-runtime.ts +38 -0
  86. package/src/core/schema/db-types.ts +16 -0
  87. package/src/core/schema/index.ts +26 -0
  88. package/src/core/schema/migration-runner.ts +207 -0
  89. package/src/core/schema/migrations/001-init-trajectory.ts +211 -0
  90. package/src/core/schema/migrations/002-init-central.ts +122 -0
  91. package/src/core/schema/migrations/003-init-workflow.ts +55 -0
  92. package/src/core/schema/migrations/004-add-thinking-and-gfi.ts +74 -0
  93. package/src/core/schema/migrations/index.ts +31 -0
  94. package/src/core/schema/schema-definitions.ts +650 -0
  95. package/src/core/session-tracker.ts +6 -4
  96. package/src/core/shadow-observation-registry.ts +6 -3
  97. package/src/core/system-logger.ts +2 -2
  98. package/src/core/thinking-models.ts +182 -46
  99. package/src/core/thinking-os-parser.ts +156 -0
  100. package/src/core/training-program.ts +7 -7
  101. package/src/core/trajectory.ts +42 -36
  102. package/src/core/workspace-context.ts +77 -11
  103. package/src/core/workspace-dir-validation.ts +152 -0
  104. package/src/hooks/AGENTS.md +31 -0
  105. package/src/hooks/bash-risk.ts +3 -1
  106. package/src/hooks/edit-verification.ts +9 -5
  107. package/src/hooks/gate-block-helper.ts +5 -1
  108. package/src/hooks/gate.ts +152 -5
  109. package/src/hooks/gfi-gate.ts +9 -2
  110. package/src/hooks/lifecycle-routing.ts +124 -0
  111. package/src/hooks/lifecycle.ts +12 -12
  112. package/src/hooks/llm.ts +17 -109
  113. package/src/hooks/message-sanitize.ts +5 -3
  114. package/src/hooks/pain.ts +19 -15
  115. package/src/hooks/progressive-trust-gate.ts +7 -1
  116. package/src/hooks/prompt.ts +169 -60
  117. package/src/hooks/subagent.ts +5 -4
  118. package/src/hooks/thinking-checkpoint.ts +2 -0
  119. package/src/hooks/trajectory-collector.ts +15 -12
  120. package/src/http/principles-console-route.ts +31 -68
  121. package/src/i18n/commands.ts +2 -2
  122. package/src/index.ts +130 -40
  123. package/src/service/central-database.ts +131 -43
  124. package/src/service/central-health-service.ts +47 -0
  125. package/src/service/central-overview-service.ts +135 -0
  126. package/src/service/central-sync-service.ts +87 -0
  127. package/src/service/control-ui-query-service.ts +46 -36
  128. package/src/service/event-log-auditor.ts +261 -0
  129. package/src/service/evolution-query-service.ts +23 -22
  130. package/src/service/evolution-worker.ts +565 -261
  131. package/src/service/health-query-service.ts +213 -36
  132. package/src/service/nocturnal-runtime.ts +8 -4
  133. package/src/service/nocturnal-service.ts +503 -59
  134. package/src/service/nocturnal-target-selector.ts +5 -7
  135. package/src/service/runtime-summary-service.ts +2 -1
  136. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +25 -336
  137. package/src/service/subagent-workflow/dynamic-timeout.ts +30 -0
  138. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +48 -386
  139. package/src/service/subagent-workflow/index.ts +2 -0
  140. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +169 -284
  141. package/src/service/subagent-workflow/runtime-direct-driver.ts +114 -16
  142. package/src/service/subagent-workflow/subagent-error-utils.ts +25 -0
  143. package/src/service/subagent-workflow/types.ts +9 -4
  144. package/src/service/subagent-workflow/workflow-manager-base.ts +573 -0
  145. package/src/service/subagent-workflow/workflow-store.ts +71 -11
  146. package/src/service/trajectory-service.ts +2 -1
  147. package/src/tools/critique-prompt.ts +1 -1
  148. package/src/tools/deep-reflect.ts +175 -209
  149. package/src/tools/model-index.ts +2 -1
  150. package/src/types/event-types.ts +2 -2
  151. package/src/types/principle-tree-schema.ts +29 -23
  152. package/src/utils/file-lock.ts +5 -3
  153. package/src/utils/io.ts +5 -2
  154. package/src/utils/nlp.ts +5 -46
  155. package/src/utils/node-vm-polyfill.ts +11 -0
  156. package/src/utils/plugin-logger.ts +2 -0
  157. package/src/utils/retry.ts +572 -0
  158. package/src/utils/subagent-probe.ts +1 -1
  159. package/templates/langs/en/core/AGENTS.md +0 -13
  160. package/templates/langs/en/core/SOUL.md +1 -31
  161. package/templates/langs/en/core/TOOLS.md +0 -4
  162. package/templates/langs/en/principles/THINKING_OS.md +77 -0
  163. package/templates/langs/en/skills/admin/SKILL.md +0 -1
  164. package/templates/langs/en/skills/evolution-framework-update/SKILL.md +1 -1
  165. package/templates/langs/en/skills/pd-diagnostician/SKILL.md +18 -5
  166. package/templates/langs/zh/core/AGENTS.md +0 -22
  167. package/templates/langs/zh/core/SOUL.md +1 -31
  168. package/templates/langs/zh/core/TOOLS.md +0 -4
  169. package/templates/langs/zh/principles/THINKING_OS.md +77 -0
  170. package/templates/langs/zh/skills/admin/SKILL.md +0 -1
  171. package/templates/langs/zh/skills/evolution-framework-update/SKILL.md +1 -1
  172. package/templates/langs/zh/skills/pd-diagnostician/SKILL.md +25 -4
  173. package/tests/commands/evolution-status.test.ts +119 -0
  174. package/tests/commands/implementation-lifecycle.test.ts +362 -0
  175. package/tests/core/code-implementation-storage.test.ts +398 -0
  176. package/tests/core/evolution-reducer.detector-metadata.test.ts +28 -28
  177. package/tests/core/nocturnal-artifact-lineage.test.ts +53 -0
  178. package/tests/core/nocturnal-artificer.test.ts +241 -0
  179. package/tests/core/nocturnal-compliance-p-principles.test.ts +133 -0
  180. package/tests/core/nocturnal-rule-implementation-validator.test.ts +127 -0
  181. package/tests/core/pd-task-store.test.ts +126 -0
  182. package/tests/core/principle-internalization/deprecated-readiness.test.ts +193 -0
  183. package/tests/core/principle-internalization/internalization-routing-policy.test.ts +212 -0
  184. package/tests/core/principle-internalization/lifecycle-metrics.test.ts +350 -0
  185. package/tests/core/principle-internalization/principle-lifecycle-service.test.ts +211 -0
  186. package/tests/core/principle-training-state.test.ts +228 -1
  187. package/tests/core/principle-tree-ledger.test.ts +423 -0
  188. package/tests/core/regression-v1-9-1.test.ts +265 -0
  189. package/tests/core/replay-engine.test.ts +234 -0
  190. package/tests/core/rule-host-helpers.test.ts +120 -0
  191. package/tests/core/rule-host.test.ts +389 -0
  192. package/tests/core/rule-implementation-runtime.test.ts +64 -0
  193. package/tests/core/workspace-context.test.ts +53 -0
  194. package/tests/core/workspace-dir-validation.test.ts +272 -0
  195. package/tests/hooks/gate-rule-host-pipeline.test.ts +385 -0
  196. package/tests/hooks/pain.test.ts +74 -10
  197. package/tests/hooks/prompt.test.ts +63 -1
  198. package/tests/integration/principle-lifecycle.e2e.test.ts +197 -0
  199. package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +211 -0
  200. package/tests/service/data-endpoints-regression.test.ts +834 -0
  201. package/tests/service/evolution-worker.test.ts +0 -123
  202. package/tests/service/nocturnal-service-code-candidate.test.ts +330 -0
  203. package/tests/utils/nlp.test.ts +1 -19
  204. package/tests/utils/retry.test.ts +327 -0
  205. package/ui/src/App.tsx +1 -1
  206. package/ui/src/api.ts +4 -0
  207. package/ui/src/charts.tsx +366 -0
  208. package/ui/src/components/WorkspaceConfig.tsx +107 -75
  209. package/ui/src/i18n/ui.ts +89 -31
  210. package/ui/src/pages/EvolutionPage.tsx +1 -1
  211. package/ui/src/pages/OverviewPage.tsx +441 -81
  212. package/ui/src/pages/ThinkingModelsPage.tsx +287 -69
  213. package/ui/src/styles.css +43 -0
  214. package/ui/src/types.ts +17 -1
  215. package/src/agents/nocturnal-dreamer.md +0 -152
  216. package/src/agents/nocturnal-philosopher.md +0 -138
  217. package/src/agents/nocturnal-reflector.md +0 -126
  218. package/src/agents/nocturnal-scribe.md +0 -164
  219. package/templates/workspace/.principles/00-kernel.md +0 -51
  220. package/templates/workspace/.principles/DECISION_POLICY.json +0 -44
  221. package/templates/workspace/.principles/PRINCIPLES.md +0 -20
  222. package/templates/workspace/.principles/PROFILE.json +0 -54
  223. package/templates/workspace/.principles/PROFILE.schema.json +0 -56
  224. package/templates/workspace/.principles/THINKING_OS.md +0 -64
  225. package/templates/workspace/.principles/THINKING_OS_ARCHIVE.md +0 -7
  226. package/templates/workspace/.principles/THINKING_OS_CANDIDATES.md +0 -9
  227. package/templates/workspace/.principles/models/_INDEX.md +0 -27
  228. package/templates/workspace/.principles/models/first_principles.md +0 -62
  229. package/templates/workspace/.principles/models/marketing_4p.md +0 -52
  230. package/templates/workspace/.principles/models/porter_five.md +0 -63
  231. package/templates/workspace/.principles/models/swot.md +0 -60
  232. package/templates/workspace/.principles/models/user_story_map.md +0 -63
  233. package/templates/workspace/.state/WORKBOARD.json +0 -4
@@ -3,11 +3,13 @@ import * as path from 'path';
3
3
  import type { PluginHookBeforePromptBuildEvent, PluginHookAgentContext, PluginHookBeforePromptBuildResult, PluginLogger, OpenClawPluginApi } from '../openclaw-sdk.js';
4
4
  import { clearInjectedProbationIds, getSession, resetFriction, setInjectedProbationIds, trackFriction } from '../core/session-tracker.js';
5
5
  import { WorkspaceContext } from '../core/workspace-context.js';
6
- import { ContextInjectionConfig, defaultContextConfig } from '../types.js';
6
+ import type { ContextInjectionConfig} from '../types.js';
7
+ import { defaultContextConfig } from '../types.js';
7
8
  import { classifyTask, type RoutingInput } from '../core/local-worker-routing.js';
8
9
  import { extractSummary, getHistoryVersions, parseWorkingMemorySection, workingMemoryToInjection, autoCompressFocus, safeReadCurrentFocus } from '../core/focus-history.js';
9
- import { EmpathyObserverWorkflowManager, empathyObserverWorkflowSpec } from '../service/subagent-workflow/index.js';
10
+ import { EmpathyObserverWorkflowManager, empathyObserverWorkflowSpec, isExpectedSubagentError } from '../service/subagent-workflow/index.js';
10
11
  import { PathResolver } from '../core/path-resolver.js';
12
+ import { isSubagentRuntimeAvailable } from '../utils/subagent-probe.js';
11
13
  import {
12
14
  matchEmpathyKeywords,
13
15
  loadKeywordStore,
@@ -29,16 +31,6 @@ interface ModelConfigObject {
29
31
  fallbacks?: string[];
30
32
  }
31
33
 
32
- /**
33
- * OpenClaw agents model configuration with subagent model override support
34
- */
35
- interface AgentsModelConfig {
36
- model?: unknown;
37
- subagents?: {
38
- model?: unknown;
39
- };
40
- }
41
-
42
34
  /**
43
35
  * Default model configuration for OpenClaw agents
44
36
  */
@@ -64,21 +56,6 @@ function escapeXml(input: string): string {
64
56
  .replace(/'/g, ''');
65
57
  }
66
58
 
67
- function extractContextSignals(context: { toolName?: string; filePath?: string; userMessage?: string; }): string[] {
68
- const signals: string[] = [];
69
- if (context.filePath?.endsWith('.ts')) signals.push('typescript');
70
- if (context.filePath?.endsWith('.md')) signals.push('markdown');
71
- if (context.toolName && ['edit', 'replace', 'write', 'write_file', 'apply_patch'].includes(context.toolName)) signals.push('edit');
72
- if (context.toolName && ['run_shell_command', 'bash'].includes(context.toolName)) signals.push('shell');
73
- if (context.toolName) signals.push(context.toolName);
74
- const msg = (context.userMessage || '').toLowerCase();
75
- if (msg.includes('.ts') || msg.includes('typescript')) signals.push('typescript');
76
- if (msg.includes('.md') || msg.includes('markdown')) signals.push('markdown');
77
- if (msg.includes('edit') || msg.includes('write') || msg.includes('patch')) signals.push('edit');
78
- if (msg.includes('shell') || msg.includes('bash')) signals.push('shell');
79
- return signals;
80
- }
81
-
82
59
  interface PromptHookApi {
83
60
  config?: {
84
61
  agents?: {
@@ -211,7 +188,7 @@ export function loadContextInjectionConfig(workspaceDir: string): ContextInjecti
211
188
  }
212
189
  } catch (e) {
213
190
  // Failed to load config — continue with defaults, but log for diagnostics
214
- // eslint-disable-next-line no-console
191
+
215
192
  console.warn(`[PD:Prompt] Failed to load contextInjection config: ${String(e)}`);
216
193
  }
217
194
 
@@ -257,37 +234,82 @@ export function getDiagnosticianModel(api: PromptHookApi | null, logger?: Plugin
257
234
  throw new Error(errorMsg);
258
235
  }
259
236
 
260
- function extractLatestUserMessage(messages: unknown[] | undefined): string {
261
- if (!Array.isArray(messages)) return '';
262
-
263
- for (let i = messages.length - 1; i >= 0; i--) {
237
+ /**
238
+ * Extract recent user messages for keyword optimization context.
239
+ */
240
+ function extractRecentMessages(messages: unknown[] | undefined, limit: number): string[] {
241
+ if (!Array.isArray(messages)) return [];
242
+ const userMessages: string[] = [];
243
+
244
+ for (let i = messages.length - 1; i >= 0 && userMessages.length < limit; i--) {
264
245
  const msg = messages[i] as { role?: string; content?: unknown };
265
246
  if (msg?.role !== 'user') continue;
266
-
267
- if (typeof msg.content === 'string') return msg.content;
268
- if (Array.isArray(msg.content)) {
269
- const text = msg.content
270
- .filter((part: any) => part && part.type === 'text' && typeof part.text === 'string')
271
- .map((part: any) => part.text)
247
+
248
+ let text = '';
249
+ if (typeof msg.content === 'string') {
250
+ text = msg.content;
251
+ } else if (Array.isArray(msg.content)) {
252
+ text = msg.content
253
+ .filter((part: unknown) => part && typeof part === 'object' && (part as { type?: string }).type === 'text' && typeof (part as { text?: unknown }).text === 'string')
254
+ .map((part: unknown) => (part as { text: string }).text)
272
255
  .join('\n')
273
256
  .trim();
274
- if (text) return text;
275
257
  }
258
+ if (text) userMessages.unshift(text.substring(0, 500));
276
259
  }
260
+
261
+ return userMessages;
262
+ }
277
263
 
278
- return '';
264
+ /**
265
+ * Build prompt for keyword optimization subagent.
266
+ */
267
+ function buildOptimizationPrompt(
268
+ keywordStore: ReturnType<typeof loadKeywordStore>,
269
+ recentMessages: string[],
270
+ ): string {
271
+ const currentTerms = Object.entries(keywordStore.terms)
272
+ .map(([term, entry]) => ` - "${term}": weight=${entry.weight}, hits=${entry.hitCount || 0}, fp_rate=${entry.falsePositiveRate?.toFixed(2) || '0.10'}`)
273
+ .join('\n');
274
+
275
+ return `You are an empathy keyword optimizer.
276
+
277
+ ## TASK
278
+ Analyze recent user messages and the current empathy keyword store.
279
+ Return STRICT JSON (no markdown):
280
+
281
+ {"updates": {"TERM": {"action": "add|update|remove", "weight": number, "falsePositiveRate": number, "reasoning": "string"}}}
282
+
283
+ ## Current Keyword Store (${Object.keys(keywordStore.terms).length} terms):
284
+ ${currentTerms}
285
+
286
+ ## Recent User Messages (${recentMessages.length} messages):
287
+ ${recentMessages.map((m, i) => `${i + 1}. "${m}"`).join('\n')}
288
+
289
+ ## Rules:
290
+ - ADD: If a message contains frustration/empathy signals not in current terms
291
+ - UPDATE: If a term's weight should change (high hits → increase weight, low hits → decrease)
292
+ - REMOVE: If a term has 0 hits after many turns AND high false positive rate (>0.3)
293
+ - Keep reasoning concise (max 100 chars)
294
+ - Weight range: 0.1-0.9
295
+ - falsePositiveRate range: 0.05-0.5
296
+ `;
279
297
  }
280
298
 
281
299
  export async function handleBeforePromptBuild(
282
300
  event: PluginHookBeforePromptBuildEvent,
283
301
  ctx: PluginHookAgentContext & { api?: PromptHookApi }
284
302
  ): Promise<PluginHookBeforePromptBuildResult | void> {
285
- const workspaceDir = ctx.workspaceDir;
286
- if (!workspaceDir) return;
303
+ const {workspaceDir} = ctx;
304
+ const logger = ctx.api?.logger;
305
+ logger?.info?.(`[PD:Prompt] handleBeforePromptBuild called: workspaceDir=${!!workspaceDir}, trigger=${ctx.trigger}, sessionId=${ctx.sessionId?.substring(0, 20)}`);
306
+ if (!workspaceDir) {
307
+ logger?.warn?.(`[PD:Prompt] workspaceDir is missing — skipping empathy processing`);
308
+ return;
309
+ }
287
310
 
288
311
  const wctx = WorkspaceContext.fromHookContext(ctx);
289
312
  const { trigger, sessionId, api } = ctx;
290
- const logger = api?.logger;
291
313
  if (sessionId) {
292
314
  wctx.trajectory?.recordSession?.({ sessionId });
293
315
  }
@@ -336,6 +358,7 @@ export async function handleBeforePromptBuild(
336
358
  // appendSystemContext: Principles + Thinking OS + reflection_log + project_context (cacheable, WebUI-hidden)
337
359
  // prependContext: Only short dynamic directives: evolutionDirective + heartbeat
338
360
 
361
+ // eslint-disable-next-line no-useless-assignment -- Reason: initial value unused due to immediate reassignment
339
362
  let prependSystemContext = '';
340
363
  let prependContext = '';
341
364
  let appendSystemContext = '';
@@ -378,13 +401,53 @@ The empathy observer subagent handles pain detection independently.
378
401
  `.trim();
379
402
 
380
403
  // ─────────────────────────────────────────────────3. Empathy Observer Spawn
381
- // Skip if this is a subagent session or if the message indicates agent-to-agent communication
382
- const latestUserMessage = extractLatestUserMessage(event.messages);
383
- const isAgentToAgent = latestUserMessage.includes('sourceSession=agent:') || sessionId?.includes(':subagent:') === true;
404
+ // event.prompt contains the full prompt text, which may include system/boot instructions
405
+ // The actual user message from Feishu is embedded in the prompt with various formats:
406
+ // Format 1: "Sender (untrusted metadata): ```json {...}``` user_message_text"
407
+ // Format 2: "You are running a boot check. Follow BOOT.md..." (boot check, skip empathy)
408
+ // Format 3: Clean user message text
409
+ let latestUserMessage = event.prompt || '';
410
+
411
+ // Skip boot check messages — these are system-generated, not real user messages.
412
+ // buildBootPrompt() in OpenClaw src/gateway/boot.ts always starts with:
413
+ // "You are running a boot check. Follow BOOT.md instructions exactly."
414
+ // This exact phrase will never appear in a real user message.
415
+ if (latestUserMessage.startsWith('You are running a boot check.') ||
416
+ latestUserMessage.includes('You are running a boot check. Follow BOOT.md')) {
417
+ latestUserMessage = '';
418
+ }
419
+
420
+ // Try to extract actual user message from Feishu wrapper formats
421
+ if (latestUserMessage.length > 50) {
422
+ // Format 1: "Sender (untrusted metadata): ```json {...}``` user_message_text"
423
+ const senderMatch = /Sender \(untrusted metadata\):[\s\S]*?```json[\s\S]*?```\s*/.exec(latestUserMessage);
424
+ if (senderMatch) {
425
+ const afterSender = latestUserMessage.slice(senderMatch.index + senderMatch[0].length).trim();
426
+ if (afterSender.length > 3) latestUserMessage = afterSender;
427
+ }
428
+
429
+ // Format 2: "Conversation info (untrusted metadata): ```json {...}``` user_message_text"
430
+ if (latestUserMessage.length > 200 && latestUserMessage.includes('Conversation info')) {
431
+ const convInfoMatch = /Conversation info[\s\S]*?```json[\s\S]*?```\s*/.exec(latestUserMessage);
432
+ if (convInfoMatch) {
433
+ const afterConvInfo = latestUserMessage.slice(convInfoMatch.index + convInfoMatch[0].length).trim();
434
+ if (afterConvInfo.length > 3) latestUserMessage = afterConvInfo;
435
+ }
436
+ }
437
+ }
438
+
439
+ // #189: Detect empathy observer output to prevent recursive spawn.
440
+ // The empathy observer runs with parentSessionId (not :subagent:), so its output
441
+ // would be treated as a user message and re-trigger empathy evaluation.
442
+ // Match distinctive patterns from the empathy observer prompt/output.
443
+ const isEmpathyPrompt = /empathy\s*observer/i.test(latestUserMessage) &&
444
+ /damageDetected|severity|confidence/i.test(latestUserMessage);
445
+ const isAgentToAgent = latestUserMessage.includes('sourceSession=agent:') || sessionId?.includes(':subagent:') === true || isEmpathyPrompt;
384
446
 
385
447
  const isUserInteraction = trigger === 'user' || trigger === 'api' || !trigger;
386
448
 
387
449
  const empathyEnabled = wctx.config.get('empathy_engine.enabled') !== false;
450
+ logger?.info?.(`[PD:Empathy] Conditions: enabled=${empathyEnabled}, isUser=${isUserInteraction}, sessionId=${!!sessionId}, api=${!!api}, !agentToAgent=${!isAgentToAgent}, workspaceDir=${!!workspaceDir}, hasMessage=${!!latestUserMessage}`);
388
451
  if (empathyEnabled && isUserInteraction && sessionId && api && !isAgentToAgent) {
389
452
  prependContext = '### BEHAVIORAL_CONSTRAINTS\n' + empathySilenceConstraint + '\n\n' + prependContext;
390
453
 
@@ -393,6 +456,8 @@ The empathy observer subagent handles pain detection independently.
393
456
  // for boundary cases and random discovery of new expressions.
394
457
  if (workspaceDir && latestUserMessage) {
395
458
  try {
459
+ const msgPreview = latestUserMessage.substring(0, 200).replace(/\n/g, ' ');
460
+ logger?.info?.(`[PD:Empathy] Processing user message: "${msgPreview}" (trigger=${trigger}, promptLen=${latestUserMessage.length})`);
396
461
  const lang = (wctx.config.get('language') as 'zh' | 'en') || 'zh';
397
462
 
398
463
  // Load keyword store once, cache in memory (Finding #7: avoid per-turn I/O)
@@ -431,7 +496,7 @@ The empathy observer subagent handles pain detection independently.
431
496
  source: 'user_empathy',
432
497
  });
433
498
 
434
- logger?.info?.(`[PD:Empathy] MATCH: "${matchResult.matchedTerms.join(', ')}" → severity=${matchResult.severity}, score=${matchResult.score.toFixed(2)}, penalty=${penalty}, subagent=${shouldCallSubagent ? samplingReason : 'skipped(high_confidence)'}`);
499
+ 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') + ')'}`);
435
500
  } else {
436
501
  // Log unmatched messages periodically for coverage analysis
437
502
  if (turnCount > 0 && turnCount % 50 === 0) {
@@ -441,7 +506,12 @@ The empathy observer subagent handles pain detection independently.
441
506
  }
442
507
 
443
508
  // Trigger subagent for sampling cases (Finding #1: use shared manager to avoid leaks)
444
- if (shouldCallSubagent && api?.runtime?.subagent) {
509
+ const runtimeSubagent =
510
+ isSubagentRuntimeAvailable(api?.runtime?.subagent)
511
+ ? api.runtime.subagent
512
+ : undefined;
513
+
514
+ if (shouldCallSubagent && runtimeSubagent) {
445
515
  logger?.info?.(`[PD:Empathy] SUBAGENT_SAMPLE: reason=${samplingReason}, score=${matchResult.score.toFixed(2)}, matched=[${matchResult.matchedTerms.join(',')}]`);
446
516
 
447
517
  // EmpathyObserverWorkflowManager auto-finalizes via wait poll mechanism.
@@ -449,13 +519,18 @@ The empathy observer subagent handles pain detection independently.
449
519
  const empathyManager = new EmpathyObserverWorkflowManager({
450
520
  workspaceDir,
451
521
  logger: api.logger ?? console,
452
- subagent: api.runtime.subagent as any,
522
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: runtimeSubagent has structurally compatible shape but differs from workflow manager's subagent type
523
+ subagent: runtimeSubagent as any,
453
524
  });
454
525
  empathyManager.startWorkflow(empathyObserverWorkflowSpec, {
455
526
  parentSessionId: sessionId,
456
527
  workspaceDir,
457
528
  taskInput: latestUserMessage,
458
- }).catch((err) => api.logger?.warn?.(`[PD:Empathy] subagent sample failed: ${String(err)}`));
529
+ }).catch((err) => {
530
+ if (!isExpectedSubagentError(err)) {
531
+ api.logger?.warn?.(`[PD:Empathy] subagent sample failed: ${String(err)}`);
532
+ }
533
+ });
459
534
  }
460
535
 
461
536
  // Helper: build summary string (Finding #2: avoid duplication)
@@ -469,7 +544,35 @@ The empathy observer subagent handles pain detection independently.
469
544
  if (shouldTriggerOptimization(keywordStore, turnCount)) {
470
545
  logger?.info?.(`[PD:Empathy] OPTIMIZATION_TRIGGER: turns=${turnCount}, last_optimized=${keywordStore.lastOptimizedAt}`);
471
546
  logger?.info?.(`[PD:Empathy] STATS: ${buildSummary()}`);
472
- // TODO: Start keyword optimization subagent to update weights and discover new terms
547
+
548
+ // Start keyword optimization subagent to update weights and discover new terms
549
+ try {
550
+ const recentMessages = extractRecentMessages(event.messages, 10);
551
+ const optimizationPrompt = buildOptimizationPrompt(keywordStore, recentMessages);
552
+
553
+ logger?.info?.(`[PD:Empathy] Starting optimization subagent with ${recentMessages.length} recent messages`);
554
+
555
+ const empathyManager = new EmpathyObserverWorkflowManager({
556
+ workspaceDir,
557
+ logger: api.logger ?? console,
558
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: api.runtime.subagent has structurally compatible shape but differs from workflow manager's subagent type
559
+ subagent: api.runtime.subagent as any,
560
+ });
561
+
562
+ empathyManager.startWorkflow(empathyObserverWorkflowSpec, {
563
+ parentSessionId: sessionId,
564
+ workspaceDir,
565
+ taskInput: { prompt: optimizationPrompt },
566
+ }).catch((err) => {
567
+ if (!isExpectedSubagentError(err)) {
568
+ api.logger?.warn?.(`[PD:Empathy] optimization subagent failed: ${String(err)}`);
569
+ }
570
+ });
571
+ } catch (optErr) {
572
+ if (!isExpectedSubagentError(optErr)) {
573
+ logger?.warn?.(`[PD:Empathy] Failed to start optimization subagent: ${String(optErr)}`);
574
+ }
575
+ }
473
576
  }
474
577
 
475
578
  // Periodic summary (every 100 turns)
@@ -477,9 +580,13 @@ The empathy observer subagent handles pain detection independently.
477
580
  logger?.info?.(`[PD:Empathy] ${buildSummary()}`);
478
581
  }
479
582
 
480
- // Save keyword store periodically (Finding #7: not every turn)
481
- if (turnCount % 50 === 0) {
583
+ // Save keyword store on every match to prevent data loss on restart.
584
+ // Previously used turnCount % 50 gate which caused hitCount loss because
585
+ // module-level state resets on plugin reload before reaching turn 50.
586
+ if (matchResult.matched) {
482
587
  saveKeywordStore(wctx.stateDir, keywordStore);
588
+ const {totalHits} = keywordStore.stats;
589
+ logger?.info?.(`[PD:Empathy] Keyword store saved after match: terms=${matchResult.matchedTerms.join(',')}, totalHits=${totalHits}`);
483
590
  }
484
591
  } catch (e) {
485
592
  logger?.warn?.(`[PD:Empathy] ERROR: ${String(e)}`);
@@ -521,6 +628,7 @@ ACTION: Run self-audit. If stable, reply ONLY with "HEARTBEAT_OK".
521
628
  }
522
629
 
523
630
  // ──── 6. Dynamic Attitude Matrix (based on GFI) ────
631
+ // eslint-disable-next-line no-useless-assignment -- Reason: initial value unused due to immediate reassignment
524
632
  let attitudeDirective = '';
525
633
  const currentGfi = session?.currentGfi || 0;
526
634
 
@@ -621,7 +729,7 @@ ACTION: Run self-audit. If stable, reply ONLY with "HEARTBEAT_OK".
621
729
  if (currentFocus.trim()) {
622
730
  try {
623
731
  // 🚀 自动压缩门禁:检查文件大小,超过阈值自动压缩
624
- const stateDir = wctx.stateDir;
732
+ const {stateDir} = wctx;
625
733
  const compressResult = autoCompressFocus(focusPath, workspaceDir, stateDir);
626
734
  if (compressResult.compressed) {
627
735
  logger?.info?.(`[PD:Prompt] Auto-compressed CURRENT_FOCUS: ${compressResult.oldLines} → ${compressResult.newLines} lines. Milestones archived: ${compressResult.milestonesArchived}`);
@@ -732,12 +840,12 @@ ACTION: Run self-audit. If stable, reply ONLY with "HEARTBEAT_OK".
732
840
  // Shadow evidence comes from real runtime hooks (subagent_spawning/subagent_ended).
733
841
  if (!isMinimalMode && sessionId) {
734
842
  try {
735
- // Extract RoutingInput from the latest user message
736
- const latestUserText = extractLatestUserMessage(event.messages);
843
+ // Use the already extracted and cleaned user message
844
+ const latestUserText = latestUserMessage || '';
737
845
 
738
846
  if (latestUserText && latestUserText.trim().length > 0) {
739
847
  // Infer requestedTools and requestedFiles from message content
740
- const toolPatterns: Array<{ pattern: RegExp; tool: string }> = [
848
+ const toolPatterns: { pattern: RegExp; tool: string }[] = [
741
849
  { pattern: /\b(edit|replace|write|modify|update|fix|patch|add|remove|delete|insert)\b/gi, tool: 'edit' },
742
850
  { pattern: /\b(read|cat|view|show|get|find|search|grep|look|inspect|examine|list|head|tail|diff)\b/gi, tool: 'read' },
743
851
  { pattern: /\b(run|execute|exec|bash|shell|command)\b/gi, tool: 'bash' },
@@ -745,9 +853,11 @@ ACTION: Run self-audit. If stable, reply ONLY with "HEARTBEAT_OK".
745
853
  const filePattern = /\b([a-zA-Z]:\\?[^\s,]+\.[a-z]{2,10}|[./][^\s,]+\.[a-z]{2,10})\b/gi;
746
854
  const toolMatches = toolPatterns.flatMap(({ pattern, tool }) => {
747
855
  const matches: string[] = [];
748
- let m;
856
+ // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in while loop condition
857
+ let _m;
749
858
  const r = new RegExp(pattern.source, pattern.flags);
750
- while ((m = r.exec(latestUserText)) !== null) matches.push(tool);
859
+ /* eslint-disable @typescript-eslint/no-unused-vars, no-unused-vars -- Reason: regex exec side effect used, match variable intentionally unused */
860
+ while ((_m = r.exec(latestUserText)) !== null) matches.push(tool);
751
861
  return matches;
752
862
  });
753
863
  const fileMatches = latestUserText.match(filePattern) ?? [];
@@ -917,4 +1027,3 @@ ${attitudeDirective}
917
1027
  appendSystemContext
918
1028
  };
919
1029
  }
920
-
@@ -1,5 +1,4 @@
1
1
  import type { PluginHookSubagentEndedEvent, PluginHookSubagentContext, PluginLogger, OpenClawPluginApi } from '../openclaw-sdk.js';
2
- import * as fs from 'fs';
3
2
  import { buildPainFlag, writePainFlag } from '../core/pain.js';
4
3
  import { WorkspaceContext } from '../core/workspace-context.js';
5
4
  // No longer needed — diagnostician runs via HEARTBEAT, not subagent
@@ -13,6 +12,7 @@ import type { WorkflowManager } from '../service/subagent-workflow/types.js';
13
12
  * Factory to create the appropriate WorkflowManager by workflow_type string.
14
13
  * Used by the subagent_ended hook to dispatch lifecycle recovery to the right manager.
15
14
  */
15
+ /* eslint-disable @typescript-eslint/max-params -- Reason: Factory function requires workflowType, workspaceDir, logger, and subagent */
16
16
  function createWorkflowManagerForType(
17
17
  workflowType: string,
18
18
  workspaceDir: string,
@@ -23,6 +23,7 @@ function createWorkflowManagerForType(
23
23
  info: (m: string) => logger.info(String(m)),
24
24
  warn: (m: string) => logger.warn(String(m)),
25
25
  error: (m: string) => logger.error(String(m)),
26
+ // eslint-disable-next-line @typescript-eslint/no-empty-function -- debug logger no-op
26
27
  debug: () => {},
27
28
  } as unknown as PluginLogger;
28
29
 
@@ -84,7 +85,7 @@ function emitSubagentPainEvent(
84
85
  function extractAgentIdFromSessionKey(sessionKey: string | undefined): string | undefined {
85
86
  // sessionKey format: "agent:{agentId}:{type}:{uuid}" or "agent:{agentId}:{uuid}"
86
87
  if (!sessionKey) return undefined;
87
- const match = sessionKey.match(/^agent:([^:]+):/);
88
+ const match = /^agent:([^:]+):/.exec(sessionKey);
88
89
  return match ? match[1] : undefined;
89
90
  }
90
91
 
@@ -104,7 +105,7 @@ export async function handleSubagentEnded(
104
105
  ctx: SubagentEndedHookContext
105
106
  ): Promise<void> {
106
107
  const { outcome, targetSessionKey } = event;
107
- const workspaceDir = ctx.workspaceDir;
108
+ const {workspaceDir} = ctx;
108
109
 
109
110
  if (!workspaceDir) return;
110
111
 
@@ -151,7 +152,7 @@ export async function handleSubagentEnded(
151
152
  }
152
153
  }
153
154
 
154
- const config = wctx.config;
155
+ const {config} = wctx;
155
156
 
156
157
  // ── Outcome-based EP and Pain Signal handling ──
157
158
  // OpenClaw v2026.3.23 fixes: timeout may be false positive (fast-finishing workers)
@@ -40,10 +40,12 @@ export interface ThinkingCheckpointConfig {
40
40
  * @param logger - Optional logger for info messages
41
41
  * @returns Block result if thinking required, undefined otherwise
42
42
  */
43
+ /* eslint-disable @typescript-eslint/max-params -- Reason: Checkpoint function requires event, config, sessionId, and logger */
43
44
  export function checkThinkingCheckpoint(
44
45
  event: PluginHookBeforeToolCallEvent,
45
46
  config: ThinkingCheckpointConfig,
46
47
  sessionId: string | undefined,
48
+ /* eslint-disable no-unused-vars -- Reason: callback parameter in type annotation */
47
49
  logger?: { info?: (message: string) => void }
48
50
  ): PluginHookBeforeToolCallResult | undefined {
49
51
  const enabled = config.enabled ?? false;
@@ -63,7 +63,7 @@ function scrubSensitive(obj: unknown, depth = 0): unknown {
63
63
  * 异步写入队列 - 确保有序、非阻塞写入
64
64
  */
65
65
  class AsyncWriteQueue {
66
- private queue: Array<() => Promise<void>> = [];
66
+ private readonly queue: (() => Promise<void>)[] = [];
67
67
  private processing = false;
68
68
 
69
69
  async enqueue(task: () => Promise<void>): Promise<void> {
@@ -81,9 +81,14 @@ class AsyncWriteQueue {
81
81
 
82
82
  this.processing = true;
83
83
  const task = this.queue.shift();
84
-
84
+
85
+ if (!task) {
86
+ this.processing = false;
87
+ return;
88
+ }
89
+
85
90
  try {
86
- await task!();
91
+ await task();
87
92
  } catch {
88
93
  // Silently fail - trajectory collection should not block main functionality
89
94
  }
@@ -151,7 +156,7 @@ export function handleAfterToolCall(
151
156
  event: PluginHookAfterToolCallEvent,
152
157
  ctx: PluginHookToolContext & { workspaceDir?: string }
153
158
  ): void {
154
- const workspaceDir = ctx.workspaceDir;
159
+ const {workspaceDir} = ctx;
155
160
  if (!workspaceDir) return;
156
161
 
157
162
  // 递归脱敏处理所有字段
@@ -186,7 +191,7 @@ export function handleLlmOutput(
186
191
  event: PluginHookLlmOutputEvent,
187
192
  ctx: PluginHookAgentContext & { workspaceDir?: string }
188
193
  ): void {
189
- const workspaceDir = ctx.workspaceDir;
194
+ const {workspaceDir} = ctx;
190
195
  if (!workspaceDir) return;
191
196
 
192
197
  const totalTextLength = event.assistantTexts?.reduce((sum, text) => sum + (text?.length || 0), 0) || 0;
@@ -211,7 +216,7 @@ export function handleBeforeMessageWrite(
211
216
  event: PluginHookBeforeMessageWriteEvent,
212
217
  ctx: PluginHookAgentContext & { workspaceDir?: string }
213
218
  ): void {
214
- const workspaceDir = ctx.workspaceDir;
219
+ const {workspaceDir} = ctx;
215
220
  if (!workspaceDir) return;
216
221
 
217
222
  const msg = event.message;
@@ -223,11 +228,13 @@ export function handleBeforeMessageWrite(
223
228
  // 提取文本内容
224
229
  let content = '';
225
230
  if (typeof msg.content === 'string') {
231
+ /* eslint-disable @typescript-eslint/prefer-destructuring */
232
+ // Reason: msg.content is string | ContentPart[]; destructuring would require renaming in the else branch
226
233
  content = msg.content;
227
234
  } else if (Array.isArray(msg.content)) {
228
235
  content = msg.content
229
- .filter((part: any) => part?.type === 'text')
230
- .map((part: any) => part.text)
236
+ .filter((part: unknown) => part && typeof part === 'object' && (part as { type?: string }).type === 'text')
237
+ .map((part: unknown) => (part as { text: string }).text)
231
238
  .join('\n');
232
239
  }
233
240
 
@@ -249,10 +256,6 @@ export function handleBeforeMessageWrite(
249
256
  * 脱敏处理:移除敏感参数(保留旧函数签名以兼容)
250
257
  * @deprecated 使用 scrubSensitive 替代
251
258
  */
252
- function sanitizeParams(params: Record<string, any>): Record<string, any> {
253
- return scrubSensitive(params) as Record<string, any>;
254
- }
255
-
256
259
  /**
257
260
  * 轨迹汇总统计(供 cron 任务调用)
258
261
  */