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
@@ -34,40 +34,40 @@ export interface ControlUiDatabaseOptions {
34
34
  }
35
35
 
36
36
  export interface RecentThinkingContext {
37
- toolCalls: Array<{
37
+ toolCalls: {
38
38
  id: number;
39
39
  toolName: string;
40
40
  outcome: 'success' | 'failure' | 'blocked';
41
41
  errorType: string | null;
42
42
  errorMessage: string | null;
43
43
  createdAt: string;
44
- }>;
45
- painEvents: Array<{
44
+ }[];
45
+ painEvents: {
46
46
  id: number;
47
47
  source: string;
48
48
  score: number;
49
49
  reason: string | null;
50
50
  createdAt: string;
51
- }>;
52
- gateBlocks: Array<{
51
+ }[];
52
+ gateBlocks: {
53
53
  id: number;
54
54
  toolName: string;
55
55
  reason: string;
56
56
  filePath: string | null;
57
57
  createdAt: string;
58
- }>;
59
- userCorrections: Array<{
58
+ }[];
59
+ userCorrections: {
60
60
  id: number;
61
61
  correctionCue: string | null;
62
62
  rawExcerpt: string | null;
63
63
  createdAt: string;
64
- }>;
65
- principleEvents: Array<{
64
+ }[];
65
+ principleEvents: {
66
66
  id: number;
67
67
  principleId: string | null;
68
68
  eventType: string;
69
69
  createdAt: string;
70
- }>;
70
+ }[];
71
71
  }
72
72
 
73
73
  const DEFAULT_BUSY_TIMEOUT_MS = 5000;
@@ -274,6 +274,25 @@ export class ControlUiDatabase {
274
274
  return this.db.prepare(sql).get(...params) as T | undefined;
275
275
  }
276
276
 
277
+ /**
278
+ * Execute SQL statement that does not return rows (DDL, CREATE TABLE, etc.).
279
+ *
280
+ * Returns: void (executes directly)
281
+ * Not for: SELECT queries (use all() or get() instead)
282
+ */
283
+ execute(sql: string): void {
284
+ this.db.exec(sql);
285
+ }
286
+
287
+ /**
288
+ * Execute a parameterized write statement (INSERT, UPDATE, DELETE).
289
+ */
290
+ run(sql: string, ...params: unknown[]): void {
291
+ this.withWrite(() => {
292
+ this.db.prepare(sql).run(...params);
293
+ });
294
+ }
295
+
277
296
  restoreRawText(inlineText?: string | null, blobRef?: string | null): string {
278
297
  if (inlineText) return inlineText;
279
298
  if (!blobRef) return '';
@@ -12,9 +12,11 @@ export interface DetectionResult {
12
12
  * A simple LRU Cache implementation using Map.
13
13
  */
14
14
  class SimpleLRU<K, V> {
15
- private cache: Map<K, V>;
16
- constructor(private maxSize: number = 100) {
15
+ private readonly cache: Map<K, V>;
16
+ private readonly maxSize: number;
17
+ constructor(maxSize = 100) {
17
18
  this.cache = new Map();
19
+ this.maxSize = maxSize;
18
20
  }
19
21
 
20
22
  get(key: K): V | undefined {
@@ -45,10 +47,13 @@ class SimpleLRU<K, V> {
45
47
  * Orchestrates the three-layer detection funnel for pain signals.
46
48
  */
47
49
  export class DetectionFunnel {
48
- private cache = new SimpleLRU<string, { detected: boolean; severity?: number }>(100);
50
+ private readonly cache = new SimpleLRU<string, { detected: boolean; severity?: number }>(100);
49
51
  private asyncQueue: string[] = [];
52
+ private readonly dictionary: PainDictionary;
50
53
 
51
- constructor(private dictionary: PainDictionary) {}
54
+ constructor(dictionary: PainDictionary) {
55
+ this.dictionary = dictionary;
56
+ }
52
57
 
53
58
  /**
54
59
  * Detects pain in the given text using L1 (Exact), L2 (Cache), and L3 (Async).
@@ -70,7 +75,7 @@ export class DetectionFunnel {
70
75
  }
71
76
 
72
77
  // --- Layer 2: LRU Cache (Sync) ---
73
- const hash = this.computeHash(text);
78
+ const hash = DetectionFunnel.computeHash(text);
74
79
  const cached = this.cache.get(hash);
75
80
  if (cached) {
76
81
  return {
@@ -89,7 +94,7 @@ export class DetectionFunnel {
89
94
  };
90
95
  }
91
96
 
92
- private computeHash(text: string): string {
97
+ private static computeHash(text: string): string {
93
98
  return createHash('sha256').update(text).digest('hex');
94
99
  }
95
100
 
@@ -104,7 +109,7 @@ export class DetectionFunnel {
104
109
  * Internal method for the worker to update the cache after a semantic hit.
105
110
  */
106
111
  updateCache(text: string, result: { detected: boolean; severity?: number }): void {
107
- const hash = this.computeHash(text);
112
+ const hash = DetectionFunnel.computeHash(text);
108
113
  this.cache.set(hash, result);
109
114
  }
110
115
 
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Diagnostician Task Store
3
+ *
4
+ * Manages diagnostician task prompts stored in `.state/diagnostician_tasks.json`.
5
+ * This replaces the previous HEARTBEAT.md approach which had a race condition:
6
+ * the main session heartbeat would overwrite HEARTBEAT.md while the diagnostician
7
+ * was still running, causing the task prompt to be lost.
8
+ *
9
+ * This store uses file-level locking via withLockAsync to prevent concurrent writes.
10
+ *
11
+ * Design:
12
+ * - Each task is written once by the evolution worker (when pain is detected)
13
+ * - The prompt hook reads tasks during heartbeat triggers and injects them
14
+ * - The diagnostician agent completes the task and writes marker files
15
+ * - The worker detects marker files and extracts principles
16
+ *
17
+ * File format:
18
+ * {
19
+ * "tasks": {
20
+ * "<task-id>": {
21
+ * "prompt": "...full diagnostician prompt...",
22
+ * "createdAt": "ISO timestamp",
23
+ * "status": "pending" | "completed"
24
+ * }
25
+ * }
26
+ * }
27
+ */
28
+
29
+ import * as fs from 'fs';
30
+ import * as path from 'path';
31
+ import { withLockAsync } from '../utils/file-lock.js';
32
+
33
+ const DIAGNOSTICIAN_TASKS_FILE = 'diagnostician_tasks.json';
34
+
35
+ export interface DiagnosticianTask {
36
+ prompt: string;
37
+ createdAt: string;
38
+ status: 'pending' | 'completed';
39
+ }
40
+
41
+ export interface DiagnosticianTaskStore {
42
+ tasks: Record<string, DiagnosticianTask>;
43
+ }
44
+
45
+ /**
46
+ * Resolve the diagnostician tasks file path.
47
+ */
48
+ function resolveTasksPath(stateDir: string): string {
49
+ return path.join(stateDir, DIAGNOSTICIAN_TASKS_FILE);
50
+ }
51
+
52
+ /**
53
+ * Read the diagnostician task store from disk.
54
+ * Returns an empty store if the file doesn't exist or is malformed.
55
+ */
56
+ function readTaskStore(stateDir: string): DiagnosticianTaskStore {
57
+ const filePath = resolveTasksPath(stateDir);
58
+ if (!fs.existsSync(filePath)) {
59
+ return { tasks: {} };
60
+ }
61
+ try {
62
+ const raw = fs.readFileSync(filePath, 'utf8');
63
+ const parsed = JSON.parse(raw);
64
+ if (parsed && typeof parsed.tasks === 'object') {
65
+ return parsed as DiagnosticianTaskStore;
66
+ }
67
+ return { tasks: {} };
68
+ } catch {
69
+ return { tasks: {} };
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Add a new diagnostician task to the store.
75
+ * Overwrites if a task with the same ID already exists.
76
+ * Read-modify-write is performed atomically inside the file lock.
77
+ */
78
+ export async function addDiagnosticianTask(
79
+ stateDir: string,
80
+ taskId: string,
81
+ prompt: string,
82
+ ): Promise<void> {
83
+ const filePath = resolveTasksPath(stateDir);
84
+ await withLockAsync(filePath, async () => {
85
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: function defined later in file, hoisting pattern
86
+ const store = readTaskStoreSync(filePath);
87
+ store.tasks[taskId] = {
88
+ prompt,
89
+ createdAt: new Date().toISOString(),
90
+ status: 'pending',
91
+ };
92
+ const tmpPath = filePath + '.tmp';
93
+ fs.writeFileSync(tmpPath, JSON.stringify(store, null, 2), 'utf8');
94
+ fs.renameSync(tmpPath, filePath);
95
+ });
96
+ }
97
+
98
+ /**
99
+ * Mark a task as completed and remove it from the store.
100
+ * Read-modify-write is performed atomically inside the file lock.
101
+ */
102
+ export async function completeDiagnosticianTask(
103
+ stateDir: string,
104
+ taskId: string,
105
+ ): Promise<void> {
106
+ const filePath = resolveTasksPath(stateDir);
107
+ await withLockAsync(filePath, async () => {
108
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: function defined later in file, hoisting pattern
109
+ const store = readTaskStoreSync(filePath);
110
+ delete store.tasks[taskId];
111
+ const tmpPath = filePath + '.tmp';
112
+ fs.writeFileSync(tmpPath, JSON.stringify(store, null, 2), 'utf8');
113
+ fs.renameSync(tmpPath, filePath);
114
+ });
115
+ }
116
+
117
+ /**
118
+ * Synchronous read without lock — for use INSIDE a lock context.
119
+ */
120
+
121
+ function readTaskStoreSync(filePath: string): DiagnosticianTaskStore {
122
+ if (!fs.existsSync(filePath)) {
123
+ return { tasks: {} };
124
+ }
125
+ try {
126
+ const raw = fs.readFileSync(filePath, 'utf8');
127
+ const parsed = JSON.parse(raw);
128
+ if (parsed && typeof parsed.tasks === 'object') {
129
+ return parsed as DiagnosticianTaskStore;
130
+ }
131
+ return { tasks: {} };
132
+ } catch {
133
+ return { tasks: {} };
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Get all pending diagnostician tasks.
139
+ * Returns an array of [taskId, task] pairs.
140
+ */
141
+ export function getPendingDiagnosticianTasks(
142
+ stateDir: string,
143
+ ): { id: string; task: DiagnosticianTask }[] {
144
+ const store = readTaskStore(stateDir);
145
+ return Object.entries(store.tasks)
146
+ .filter(([, task]) => task.status === 'pending')
147
+ .map(([id, task]) => ({ id, task }));
148
+ }
149
+
150
+ /**
151
+ * Check if there are any pending diagnostician tasks.
152
+ */
153
+ export function hasPendingDiagnosticianTasks(stateDir: string): boolean {
154
+ const store = readTaskStore(stateDir);
155
+ return Object.values(store.tasks).some(t => t.status === 'pending');
156
+ }
@@ -62,10 +62,10 @@ const DEFAULT_RULES: Record<string, PainRule> = {
62
62
 
63
63
  export class PainDictionary {
64
64
  private data: PainDictionaryData = { rules: {} };
65
- private filePath: string;
66
- private compiledRegex: Map<string, RegExp> = new Map();
65
+ private readonly filePath: string;
66
+ private readonly compiledRegex: Map<string, RegExp> = new Map();
67
67
 
68
- constructor(private stateDir: string) {
68
+ constructor(private readonly stateDir: string) {
69
69
  this.filePath = path.join(stateDir, 'pain_dictionary.json');
70
70
  }
71
71
 
@@ -73,7 +73,7 @@ export class PainDictionary {
73
73
  if (fs.existsSync(this.filePath)) {
74
74
  try {
75
75
  this.data = JSON.parse(fs.readFileSync(this.filePath, 'utf8'));
76
- } catch (e) {
76
+ } catch {
77
77
  console.error('[PD] Failed to parse pain_dictionary.json, using defaults.');
78
78
  this.data = { rules: { ...DEFAULT_RULES } };
79
79
  }
@@ -14,13 +14,13 @@
14
14
 
15
15
  import * as fs from 'fs';
16
16
  import * as path from 'path';
17
- import {
17
+ import type {
18
18
  EmpathyKeywordStore,
19
19
  EmpathyKeywordEntry,
20
20
  EmpathyKeywordStats,
21
21
  EmpathyMatchResult,
22
- EmpathyKeywordConfig,
23
- SeedKeywordEntry,
22
+ EmpathyKeywordConfig} from './empathy-types.js';
23
+ import {
24
24
  EMPATHY_SEED_KEYWORDS,
25
25
  DEFAULT_EMPATHY_KEYWORD_CONFIG,
26
26
  scoreToSeverity,
@@ -80,6 +80,7 @@ export function loadKeywordStore(stateDir: string, language?: 'zh' | 'en'): Empa
80
80
  try {
81
81
  if (!fs.existsSync(filePath)) {
82
82
  const store = createDefaultKeywordStore(language);
83
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: function defined later in file, hoisting pattern
83
84
  saveKeywordStore(stateDir, store);
84
85
  return store;
85
86
  }
@@ -91,6 +92,7 @@ export function loadKeywordStore(stateDir: string, language?: 'zh' | 'en'): Empa
91
92
  if (!parsed.terms || !parsed.stats || !parsed.version) {
92
93
  console.warn('[PD:Empathy] Invalid keyword store format, creating default');
93
94
  const store = createDefaultKeywordStore(language);
95
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: function defined later in file, hoisting pattern
94
96
  saveKeywordStore(stateDir, store);
95
97
  return store;
96
98
  }
@@ -99,6 +101,7 @@ export function loadKeywordStore(stateDir: string, language?: 'zh' | 'en'): Empa
99
101
  } catch (e) {
100
102
  console.warn(`[PD:Empathy] Failed to load keyword store: ${e}`);
101
103
  const store = createDefaultKeywordStore(language);
104
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: function defined later in file, hoisting pattern
102
105
  saveKeywordStore(stateDir, store);
103
106
  return store;
104
107
  }
@@ -107,6 +110,7 @@ export function loadKeywordStore(stateDir: string, language?: 'zh' | 'en'): Empa
107
110
  /**
108
111
  * Saves the keyword store to disk.
109
112
  */
113
+
110
114
  export function saveKeywordStore(stateDir: string, store: EmpathyKeywordStore): void {
111
115
  const filePath = path.join(stateDir, KEYWORD_STORE_FILE);
112
116
  const dir = path.dirname(filePath);
@@ -178,8 +178,8 @@ export interface EmpathyKeywordConfig {
178
178
  export const DEFAULT_EMPATHY_KEYWORD_CONFIG: EmpathyKeywordConfig = {
179
179
  matchThreshold: 0.3,
180
180
  maxTermsPerMessage: 5,
181
- optimizationIntervalTurns: 100,
182
- optimizationIntervalMs: 24 * 60 * 60 * 1000, // 24 hours
181
+ optimizationIntervalTurns: 50,
182
+ optimizationIntervalMs: 6 * 60 * 60 * 1000, // 6 hours
183
183
  penaltyMild: 10,
184
184
  penaltyModerate: 25,
185
185
  penaltySevere: 40,
@@ -216,3 +216,14 @@ export function severityToPenalty(
216
216
  case 'severe': return config.penaltySevere;
217
217
  }
218
218
  }
219
+
220
+ /**
221
+ * Normalizes various severity string inputs to the canonical empathy severity type.
222
+ * Handles common aliases: 'high' → 'severe', 'medium' → 'moderate'.
223
+ */
224
+ export function normalizeSeverity(input?: string): 'mild' | 'moderate' | 'severe' {
225
+ const normalized = (input || '').toLowerCase();
226
+ if (normalized === 'severe' || normalized === 'high') return 'severe';
227
+ if (normalized === 'moderate' || normalized === 'medium') return 'moderate';
228
+ return 'mild';
229
+ }
@@ -29,7 +29,7 @@ export class EventLog {
29
29
  private readonly statsFile: string;
30
30
  private readonly logger?: PluginLogger;
31
31
 
32
- private statsCache: Map<string, DailyStats> = new Map();
32
+ private readonly statsCache: Map<string, DailyStats> = new Map();
33
33
  private eventBuffer: EventLogEntry[] = [];
34
34
  private readonly maxBufferSize = 20;
35
35
  private readonly flushIntervalMs = 30000;
@@ -66,9 +66,12 @@ export class EventLog {
66
66
  this.record('rule_promotion', 'promoted', undefined, data);
67
67
  }
68
68
 
69
- recordHookExecution(data: HookExecutionEventData): void {
69
+ recordHookExecution(data: HookExecutionEventData, opts?: { flushImmediately?: boolean }): void {
70
70
  const category = data.error ? 'failure' : 'success';
71
71
  this.record('hook_execution', category, undefined, data);
72
+ if (opts?.flushImmediately) {
73
+ this.flushEvents();
74
+ }
72
75
  }
73
76
 
74
77
  recordGateBlock(sessionId: string | undefined, data: GateBlockEventData): void {
@@ -104,6 +107,7 @@ export class EventLog {
104
107
  this.record('warn', 'failure', sessionId, { message, ...context });
105
108
  }
106
109
 
110
+ // eslint-disable-next-line @typescript-eslint/max-params -- Reason: event record requires type + category + session + data - refactoring would break internal API
107
111
  private record(
108
112
  type: EventType,
109
113
  category: EventCategory,
@@ -130,6 +134,7 @@ export class EventLog {
130
134
  }
131
135
  }
132
136
 
137
+ // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: utility method doesn't require this - pure function
133
138
  private formatDate(date: Date): string {
134
139
  return date.toISOString().split('T')[0];
135
140
  }
@@ -155,7 +160,8 @@ export class EventLog {
155
160
  }
156
161
 
157
162
  if (entry.type === 'tool_call') {
158
- const data = entry.data as unknown as ToolCallEventData;
163
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars -- Reason: data used for type narrowing only, actual fields accessed via stats
164
+ const _data = entry.data as unknown as ToolCallEventData;
159
165
  stats.tools.total++;
160
166
  if (entry.category === 'success') stats.tools.success++;
161
167
  else stats.tools.failure++;
@@ -237,6 +243,7 @@ export class EventLog {
237
243
  return this.eventBuffer.map((entry) => ({ ...entry, data: { ...entry.data } }));
238
244
  }
239
245
 
246
+ // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: getEventDedupKey is a pure key-generation function, no this reference needed
240
247
  private getEventDedupKey(entry: EventLogEntry): string {
241
248
  const eventId = typeof (entry.data as { eventId?: unknown } | undefined)?.eventId === 'string'
242
249
  ? String((entry.data as { eventId?: string }).eventId)
@@ -452,6 +459,7 @@ export class EventLog {
452
459
  * Rollback an empathy event by ID.
453
460
  * Returns the rolled back score, or 0 if event not found.
454
461
  */
462
+ // eslint-disable-next-line @typescript-eslint/max-params -- Reason: rollback requires eventId + sessionId + reason + triggeredBy - refactoring would break API
455
463
  rollbackEmpathyEvent(eventId: string, sessionId: string | undefined, reason: string, triggeredBy: 'user_command' | 'natural_language' | 'system'): number {
456
464
  const allEvents = this.getMergedEvents();
457
465
  let foundEvent: { entry: EventLogEntry; data: PainSignalEventData } | null = null;
@@ -459,7 +467,7 @@ export class EventLog {
459
467
  for (const entry of allEvents) {
460
468
  if (entry.type === 'pain_signal') {
461
469
  const data = entry.data as unknown as PainSignalEventData;
462
- if ((entry.data as any).eventId === eventId && data.source === 'user_empathy') {
470
+ if (data.eventId === eventId && data.source === 'user_empathy') {
463
471
  foundEvent = { entry, data };
464
472
  break;
465
473
  }
@@ -494,7 +502,7 @@ export class EventLog {
494
502
  if (entry.sessionId === sessionId && entry.type === 'pain_signal') {
495
503
  const data = entry.data as unknown as PainSignalEventData;
496
504
  if (data.source === 'user_empathy' && !data.deduped) {
497
- return (entry.data as any).eventId || null;
505
+ return data.eventId || null;
498
506
  }
499
507
  }
500
508
  }
@@ -518,7 +526,7 @@ export class EventLog {
518
526
  * Service to manage multiple EventLog instances by stateDir.
519
527
  */
520
528
  export class EventLogService {
521
- private static instances: Map<string, EventLog> = new Map();
529
+ private static readonly instances: Map<string, EventLog> = new Map();
522
530
 
523
531
  static get(stateDir: string, logger?: PluginLogger): EventLog {
524
532
  let instance = this.instances.get(stateDir);
@@ -21,19 +21,17 @@ import {
21
21
  HIGH_RISK_TOOLS,
22
22
  } from '../constants/tools.js';
23
23
 
24
- import {
25
- EvolutionTier,
24
+ import type {
26
25
  EvolutionEvent,
27
26
  EvolutionScorecard,
28
27
  EvolutionStats,
29
28
  EvolutionConfig,
30
- EvolutionStorage,
31
29
  TaskDifficulty,
32
30
  TierDefinition,
33
- TierPermissions,
34
31
  GateDecision,
35
- ToolCallContext,
36
- TierPromotionEvent,
32
+ ToolCallContext} from './evolution-types.js';
33
+ import {
34
+ EvolutionTier,
37
35
  DEFAULT_EVOLUTION_CONFIG,
38
36
  TIER_DEFINITIONS,
39
37
  TASK_DIFFICULTY_CONFIG,
@@ -44,11 +42,11 @@ import {
44
42
  // ===== 主引擎 =====
45
43
 
46
44
  export class EvolutionEngine {
47
- private scorecard: EvolutionScorecard;
48
- private workspaceDir: string;
49
- private stateDir: string;
50
- private config: EvolutionConfig;
51
- private storagePath: string;
45
+ private readonly scorecard: EvolutionScorecard;
46
+ private readonly workspaceDir: string;
47
+ private readonly stateDir: string;
48
+ private readonly config: EvolutionConfig;
49
+ private readonly storagePath: string;
52
50
 
53
51
  constructor(workspaceDir: string, config?: Partial<EvolutionConfig>) {
54
52
  this.workspaceDir = workspaceDir;
@@ -123,14 +121,14 @@ export class EvolutionEngine {
123
121
  ): { pointsAwarded: number; isDoubleReward: boolean; newTier?: EvolutionTier } {
124
122
  // 探索性工具成功不给积分,只重置失败记录
125
123
  if (EXPLORATORY_TOOLS.has(toolName)) {
126
- const taskHash = this.computeTaskHash(toolName, options?.filePath);
124
+ const taskHash = EvolutionEngine.computeTaskHash(toolName, options?.filePath);
127
125
  this.scorecard.recentFailureHashes.delete(taskHash);
128
126
  this.saveScorecard();
129
127
  return { pointsAwarded: 0, isDoubleReward: false };
130
128
  }
131
129
 
132
- const difficulty = options?.difficulty || this.inferDifficulty(toolName);
133
- const taskHash = this.computeTaskHash(toolName, options?.filePath);
130
+ const difficulty = options?.difficulty || EvolutionEngine.inferDifficulty(toolName);
131
+ const taskHash = EvolutionEngine.computeTaskHash(toolName, options?.filePath);
134
132
 
135
133
  // 计算积分
136
134
  let points = this.calculatePoints(difficulty, taskHash);
@@ -178,14 +176,14 @@ export class EvolutionEngine {
178
176
  ): { pointsAwarded: number; lessonRecorded: boolean } {
179
177
  // 探索性工具失败:记录但几乎不影响
180
178
  if (EXPLORATORY_TOOLS.has(toolName)) {
181
- const event = this.createEvent('failure', this.computeTaskHash(toolName, options?.filePath), 'trivial', toolName, options?.filePath, options?.reason, 0, false, options?.sessionId);
179
+ const event = this.createEvent('failure', EvolutionEngine.computeTaskHash(toolName, options?.filePath), 'trivial', toolName, options?.filePath, options?.reason, 0, false, options?.sessionId);
182
180
  this.addEvent(event);
183
181
  this.saveScorecard();
184
182
  return { pointsAwarded: 0, lessonRecorded: true };
185
183
  }
186
184
 
187
- const difficulty = options?.difficulty || this.inferDifficulty(toolName);
188
- const taskHash = this.computeTaskHash(toolName, options?.filePath);
185
+ const difficulty = options?.difficulty || EvolutionEngine.inferDifficulty(toolName);
186
+ const taskHash = EvolutionEngine.computeTaskHash(toolName, options?.filePath);
189
187
 
190
188
  // 失败不扣分,但记录教训(用于后续双倍奖励)
191
189
  this.scorecard.recentFailureHashes.set(taskHash, new Date().toISOString());
@@ -244,7 +242,7 @@ export class EvolutionEngine {
244
242
  // ===== 积分计算 =====
245
243
 
246
244
  private calculatePoints(difficulty: TaskDifficulty, taskHash: string): number {
247
- const basePoints = TASK_DIFFICULTY_CONFIG[difficulty].basePoints;
245
+ const {basePoints} = TASK_DIFFICULTY_CONFIG[difficulty];
248
246
 
249
247
  // 难度衰减
250
248
  const penalty = this.getDifficultyPenalty(difficulty);
@@ -300,15 +298,6 @@ export class EvolutionEngine {
300
298
  this.scorecard.currentTier = newTier;
301
299
  this.scorecard.stats.tierPromotions++;
302
300
 
303
- // 记录升级事件
304
- const promotionEvent: TierPromotionEvent = {
305
- previousTier,
306
- newTier,
307
- totalPoints: this.scorecard.totalPoints,
308
- timestamp: new Date().toISOString(),
309
- newPermissions: getTierDefinition(newTier).permissions,
310
- };
311
-
312
301
  console.log(`[Evolution] 🎉 Tier promotion: ${previousTier} → ${newTier} (${getTierDefinition(newTier).name})`);
313
302
 
314
303
  return newTier;
@@ -319,7 +308,7 @@ export class EvolutionEngine {
319
308
 
320
309
  // ===== 任务难度推断 =====
321
310
 
322
- private inferDifficulty(toolName: string): TaskDifficulty {
311
+ private static inferDifficulty(toolName: string): TaskDifficulty {
323
312
  if (HIGH_RISK_TOOLS.has(toolName)) return 'hard';
324
313
  if (CONSTRUCTIVE_TOOLS.has(toolName)) return 'normal';
325
314
  return 'trivial';
@@ -327,13 +316,14 @@ export class EvolutionEngine {
327
316
 
328
317
  // ===== 任务哈希 =====
329
318
 
330
- private computeTaskHash(toolName: string, filePath?: string): string {
319
+ private static computeTaskHash(toolName: string, filePath?: string): string {
331
320
  const normalizedPath = filePath ? path.normalize(filePath) : '_nofile';
332
321
  return `${toolName}:${normalizedPath}`;
333
322
  }
334
323
 
335
324
  // ===== 事件管理 =====
336
325
 
326
+ // eslint-disable-next-line @typescript-eslint/max-params -- Reason: event creation requires all event fields - refactoring would break internal structure
337
327
  private createEvent(
338
328
  type: 'success' | 'failure',
339
329
  taskHash: string,
@@ -395,6 +385,7 @@ export class EvolutionEngine {
395
385
  return this.createNewScorecard();
396
386
  }
397
387
 
388
+ // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: createNewScorecard is a factory function, no this reference needed
398
389
  private createNewScorecard(): EvolutionScorecard {
399
390
  const now = new Date().toISOString();
400
391
  return {
@@ -453,12 +444,15 @@ export class EvolutionEngine {
453
444
  } catch (e) {
454
445
  console.error(`[Evolution] Failed to save scorecard: ${String(e)}`);
455
446
  this.scheduleRetrySave();
456
- try { fs.unlinkSync(tempPath); } catch {}
447
+ /* eslint-disable @typescript-eslint/no-unused-vars, no-unused-vars -- Reason: intentionally ignored - cleanup failure should not mask original error */
448
+ try { fs.unlinkSync(tempPath); } catch (_e) {
449
+ // Cleanup failure intentionally ignored - should not mask original error
450
+ }
457
451
  }
458
452
  }
459
453
 
460
454
  /** Per-instance retry queue (P0 fix: was static, causing cross-instance race) */
461
- private retryQueue: Array<{ engine: EvolutionEngine; data: Partial<EvolutionScorecard> }> = [];
455
+ private retryQueue: { engine: EvolutionEngine; data: Partial<EvolutionScorecard> }[] = [];
462
456
  private retryTimer: ReturnType<typeof setTimeout> | null = null;
463
457
 
464
458
  /** 调度重试保存 */
@@ -533,6 +527,7 @@ export class EvolutionEngine {
533
527
 
534
528
  // ===== 工具方法 =====
535
529
 
530
+ // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- Reason: generateId is a pure utility function, no this reference needed
536
531
  private generateId(): string {
537
532
  return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
538
533
  }
@@ -557,6 +552,7 @@ export function getEvolutionEngine(workspaceDir: string): EvolutionEngine {
557
552
  if (!_instances.has(resolved)) {
558
553
  _instances.set(resolved, new EvolutionEngine(resolved));
559
554
  }
555
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Reason: set() above guarantees get() returns non-null
560
556
  return _instances.get(resolved)!;
561
557
  }
562
558
 
@@ -10,7 +10,7 @@
10
10
  * 2. evolution_events 表 (SQLite) - 结构化查询
11
11
  */
12
12
 
13
- import { createHash, randomBytes } from 'crypto';
13
+ import { randomBytes } from 'crypto';
14
14
  import type { TrajectoryDatabase } from './trajectory.js';
15
15
  import { SystemLogger } from './system-logger.js';
16
16
 
@@ -131,7 +131,7 @@ export class EvolutionLogger {
131
131
  });
132
132
  } catch (err) {
133
133
  // Database write failure doesn't affect main flow, but log for diagnostics
134
- // eslint-disable-next-line no-console
134
+
135
135
  console.error(`[EvolutionLogger] Failed to write to trajectory: ${String(err)}`);
136
136
  }
137
137
  }
@@ -264,6 +264,7 @@ export class EvolutionLogger {
264
264
  durationMs?: number;
265
265
  principlesGenerated?: number;
266
266
  }): void {
267
+ // eslint-disable-next-line @typescript-eslint/init-declarations -- assigned in all if/else branches
267
268
  let summary: string;
268
269
  if (params.resolution === 'marker_detected' || params.resolution === 'late_marker_principle_created') {
269
270
  summary = `任务 ${params.taskId} 完成,已生成 ${params.principlesGenerated || 0} 条原则`;