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
@@ -1,19 +1,28 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { handleAfterToolCall } from '../../src/hooks/pain';
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { handleAfterToolCall } from '../../src/hooks/pain.js';
3
3
  import * as fs from 'fs';
4
4
  import * as path from 'path';
5
- import * as ioUtils from '../../src/utils/io';
6
- import { WorkspaceContext } from '../../src/core/workspace-context';
7
- import { EventLogService } from '../../src/core/event-log';
8
- import { setInjectedProbationIds, clearSession } from '../../src/core/session-tracker';
5
+ import * as ioUtils from '../../src/utils/io.js';
6
+ import { WorkspaceContext } from '../../src/core/workspace-context.js';
7
+ import { EventLogService } from '../../src/core/event-log.js';
8
+ import { setInjectedProbationIds, clearSession } from '../../src/core/session-tracker.js';
9
9
 
10
10
  vi.mock('fs');
11
11
  vi.mock('../../src/utils/io.js');
12
- vi.mock('../../src/core/workspace-context');
13
- vi.mock('../../src/core/event-log');
12
+ vi.mock('../../src/core/evolution-engine.js', () => ({
13
+ recordEvolutionSuccess: vi.fn(),
14
+ recordEvolutionFailure: vi.fn(),
15
+ }));
16
+ vi.mock('../../src/core/evolution-logger.js', () => ({
17
+ createTraceId: vi.fn(() => 'trace-123'),
18
+ getEvolutionLogger: vi.fn(() => ({
19
+ logPainDetected: vi.fn(),
20
+ })),
21
+ }));
14
22
 
15
23
  const mockEmitSync = vi.fn();
16
24
  const mockRecordProbationFeedback = vi.fn();
25
+ const mockUpdatePrincipleValueMetrics = vi.fn();
17
26
 
18
27
  describe('Post-Write Checks & Pain Hook', () => {
19
28
  const workspaceDir = '/mock/workspace';
@@ -34,6 +43,9 @@ describe('Post-Write Checks & Pain Hook', () => {
34
43
  recordToolCall: vi.fn(),
35
44
  recordPainEvent: vi.fn(),
36
45
  },
46
+ principleTreeLedger: {
47
+ updatePrincipleValueMetrics: mockUpdatePrincipleValueMetrics,
48
+ },
37
49
  evolutionReducer: {
38
50
  emitSync: mockEmitSync,
39
51
  recordProbationFeedback: mockRecordProbationFeedback,
@@ -49,11 +61,16 @@ describe('Post-Write Checks & Pain Hook', () => {
49
61
  vi.clearAllMocks();
50
62
  mockEmitSync.mockReset();
51
63
  mockRecordProbationFeedback.mockReset();
52
- vi.mocked(WorkspaceContext.fromHookContext).mockReturnValue(mockWctx as any);
53
- vi.mocked(EventLogService.get).mockReturnValue(mockEventLog as any);
64
+ mockUpdatePrincipleValueMetrics.mockReset();
65
+ vi.spyOn(WorkspaceContext, 'fromHookContext').mockReturnValue(mockWctx as any);
66
+ vi.spyOn(EventLogService, 'get').mockReturnValue(mockEventLog as any);
54
67
  clearSession('s-success');
55
68
  });
56
69
 
70
+ afterEach(() => {
71
+ vi.restoreAllMocks();
72
+ });
73
+
57
74
  it('should ignore non-write tools', () => {
58
75
  const mockCtx = { workspaceDir, sessionId: 's1' };
59
76
  const mockEvent = { toolName: 'read', params: {}, result: { exitCode: 0 }, error: undefined };
@@ -138,4 +155,51 @@ describe('Post-Write Checks & Pain Hook', () => {
138
155
  }));
139
156
  });
140
157
 
158
+ it('should persist matched principle valueMetrics through the locked ledger owner without raw training-state writes', () => {
159
+ const mockCtx = { workspaceDir, sessionId: 's-metrics', api: { logger: {} } };
160
+ const mockEvent = {
161
+ toolName: 'write',
162
+ params: { file_path: 'src/main.ts' },
163
+ error: 'Delete failed for src/main.ts',
164
+ result: { exitCode: 1 },
165
+ };
166
+
167
+ vi.mocked(ioUtils.normalizePath).mockReturnValue('src/main.ts');
168
+ vi.mocked(ioUtils.isRisky).mockReturnValue(false);
169
+ vi.mocked(ioUtils.serializeKvLines).mockReturnValue('mocked-pain-flag-content');
170
+ vi.mocked(fs.existsSync).mockImplementation((filePath: fs.PathLike) => {
171
+ const normalizedPath = String(filePath).replace(/\\/g, '/');
172
+ return normalizedPath.includes('.principles/PROFILE.json');
173
+ });
174
+
175
+ mockWctx.evolutionReducer.getActivePrinciples = vi.fn().mockReturnValue([
176
+ {
177
+ id: 'p-match',
178
+ trigger: 'delete src main',
179
+ valueMetrics: undefined,
180
+ },
181
+ ]);
182
+ mockWctx.evolutionReducer.getPrincipleById = vi.fn().mockReturnValue({
183
+ id: 'p-match',
184
+ trigger: 'delete src main',
185
+ contextTags: ['write'],
186
+ });
187
+
188
+ handleAfterToolCall(mockEvent as any, mockCtx as any);
189
+
190
+ expect(mockUpdatePrincipleValueMetrics).toHaveBeenCalledWith(
191
+ 'p-match',
192
+ expect.objectContaining({
193
+ painPreventedCount: 1,
194
+ }),
195
+ );
196
+
197
+ const trainingStateWrites = vi
198
+ .mocked(fs.writeFileSync)
199
+ .mock.calls
200
+ .filter(([targetPath]) => String(targetPath).includes('principle_training_state.json'));
201
+
202
+ expect(trainingStateWrites).toEqual([]);
203
+ });
204
+
141
205
  });
@@ -5,9 +5,34 @@ import { WorkspaceContext } from '../../src/core/workspace-context';
5
5
  import fs from 'fs';
6
6
  import path from 'path';
7
7
 
8
+ const promptHookMocks = vi.hoisted(() => ({
9
+ empathyManagerCtor: vi.fn(),
10
+ startWorkflow: vi.fn(),
11
+ isSubagentRuntimeAvailable: vi.fn(() => false),
12
+ }));
13
+
8
14
  vi.mock('fs');
9
15
  vi.mock('../../src/core/session-tracker.js');
10
16
  vi.mock('../../src/core/workspace-context.js');
17
+ vi.mock('../../src/service/subagent-workflow/index.js', () => {
18
+ class MockEmpathyObserverWorkflowManager {
19
+ constructor(...args: unknown[]) {
20
+ promptHookMocks.empathyManagerCtor(...args);
21
+ }
22
+
23
+ startWorkflow(...args: unknown[]) {
24
+ return promptHookMocks.startWorkflow(...args);
25
+ }
26
+ }
27
+
28
+ return {
29
+ EmpathyObserverWorkflowManager: MockEmpathyObserverWorkflowManager,
30
+ empathyObserverWorkflowSpec: { name: 'mock-empathy-workflow' },
31
+ };
32
+ });
33
+ vi.mock('../../src/utils/subagent-probe.js', () => ({
34
+ isSubagentRuntimeAvailable: promptHookMocks.isSubagentRuntimeAvailable,
35
+ }));
11
36
 
12
37
  // 🎭️Test Group: Model Resolution Functions 🎭️
13
38
  describe('resolveModelFromConfig', () => {
@@ -184,6 +209,8 @@ describe('Prompt Context Injection Hook', () => {
184
209
 
185
210
  beforeEach(() => {
186
211
  vi.clearAllMocks();
212
+ promptHookMocks.startWorkflow.mockResolvedValue(undefined);
213
+ promptHookMocks.isSubagentRuntimeAvailable.mockReturnValue(false);
187
214
  vi.mocked(sessionTracker.getSession).mockReturnValue(undefined);
188
215
  mockWctx.evolutionReducer.getActivePrinciples.mockReturnValue([]);
189
216
  mockWctx.evolutionReducer.getProbationPrinciples.mockReturnValue([]);
@@ -226,6 +253,42 @@ describe('Prompt Context Injection Hook', () => {
226
253
  expect(mockConfig.get).toHaveBeenCalledWith('empathy_engine.enabled');
227
254
  });
228
255
 
256
+ it('does not start empathy workflow when subagent runtime probe fails', async () => {
257
+ const randomSpy = vi.spyOn(Math, 'random').mockReturnValue(0.01);
258
+ vi.mocked(fs.existsSync).mockReturnValue(false);
259
+ mockConfig.get.mockReturnValue(undefined);
260
+ promptHookMocks.isSubagentRuntimeAvailable.mockReturnValue(false);
261
+
262
+ await handleBeforePromptBuild({
263
+ messages: [{ role: 'user', content: 'hello there' }],
264
+ } as any, {
265
+ workspaceDir,
266
+ trigger: 'user',
267
+ sessionId: 'session-empathy-probe',
268
+ api: {
269
+ logger: {
270
+ info: vi.fn(),
271
+ warn: vi.fn(),
272
+ error: vi.fn(),
273
+ debug: vi.fn(),
274
+ },
275
+ runtime: {
276
+ subagent: {
277
+ run: () => {
278
+ throw new Error('Plugin runtime subagent methods are only available during a gateway request');
279
+ },
280
+ },
281
+ },
282
+ },
283
+ } as any);
284
+
285
+ expect(promptHookMocks.isSubagentRuntimeAvailable).toHaveBeenCalled();
286
+ expect(promptHookMocks.empathyManagerCtor).not.toHaveBeenCalled();
287
+ expect(promptHookMocks.startWorkflow).not.toHaveBeenCalled();
288
+
289
+ randomSpy.mockRestore();
290
+ });
291
+
229
292
  it('records latest user turn and flags explicit corrections', async () => {
230
293
  vi.mocked(fs.existsSync).mockReturnValue(false);
231
294
 
@@ -1408,4 +1471,3 @@ describe('Prompt Context Injection Hook', () => {
1408
1471
  });
1409
1472
  });
1410
1473
  });
1411
-
@@ -0,0 +1,197 @@
1
+ /**
2
+ * End-to-End Test: Principle Lifecycle
3
+ *
4
+ * Tests the complete principle lifecycle from creation to evaluation:
5
+ * 1. createPrincipleFromDiagnosis() → training store write
6
+ * 2. listEvaluablePrinciples() → returns the principle
7
+ * 3. executeNocturnalReflectionAsync() → full pipeline (Selector → Trinity → Arbiter → Persist)
8
+ *
9
+ * This test addresses the root causes identified in 5-WHY analysis:
10
+ * - #204: EvolutionReducer must write to training store
11
+ * - #205: NocturnalWorkflowManager must use executeNocturnalReflectionAsync
12
+ */
13
+
14
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
15
+ import * as fs from 'fs';
16
+ import * as path from 'path';
17
+ import * as os from 'os';
18
+ import { EvolutionReducerImpl } from '../../src/core/evolution-reducer.js';
19
+ import { listEvaluablePrinciples, loadStore } from '../../src/core/principle-training-state.js';
20
+ import { updateTrainingStore } from '../../src/core/principle-tree-ledger.js';
21
+ import { PathResolver } from '../../src/core/path-resolver.js';
22
+
23
+ describe('Principle Lifecycle E2E', () => {
24
+ let tempDir: string;
25
+ let workspaceDir: string;
26
+ let stateDir: string;
27
+
28
+ beforeEach(() => {
29
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-lifecycle-test-'));
30
+ workspaceDir = tempDir;
31
+ stateDir = path.join(workspaceDir, '.state');
32
+ fs.mkdirSync(stateDir, { recursive: true });
33
+
34
+ // Create required directories
35
+ const resolver = new PathResolver({ workspaceDir });
36
+ const memoryDir = path.dirname(resolver.resolve('EVOLUTION_STREAM'));
37
+ if (!fs.existsSync(memoryDir)) {
38
+ fs.mkdirSync(memoryDir, { recursive: true });
39
+ }
40
+ });
41
+
42
+ afterEach(() => {
43
+ fs.rmSync(tempDir, { recursive: true, force: true });
44
+ });
45
+
46
+ describe('Training Store Integration (#204)', () => {
47
+ it('should set needs_training for auto-evaluable principles', () => {
48
+ // Arrange: Create EvolutionReducer with stateDir
49
+ const reducer = new EvolutionReducerImpl({ workspaceDir, stateDir });
50
+
51
+ // Act: Create a principle from diagnosis with complete detectorMetadata
52
+ const principleId = reducer.createPrincipleFromDiagnosis({
53
+ painId: 'pain-001',
54
+ painType: 'tool_failure',
55
+ triggerPattern: 'file not found',
56
+ action: 'check file existence before access',
57
+ source: 'test',
58
+ evaluability: 'weak_heuristic',
59
+ detectorMetadata: {
60
+ applicabilityTags: ['file-operations'],
61
+ positiveSignals: ['file-exists-check'],
62
+ negativeSignals: ['file-not-found-error'],
63
+ toolSequenceHints: [['fs.existsSync', 'fs.readFile']],
64
+ confidence: 'medium',
65
+ },
66
+ });
67
+
68
+ // Assert: Principle should exist in training store with needs_training status
69
+ expect(principleId).not.toBeNull();
70
+
71
+ const store = loadStore(stateDir);
72
+ expect(store[principleId!]).toBeDefined();
73
+ expect(store[principleId!].evaluability).toBe('weak_heuristic');
74
+ expect(store[principleId!].internalizationStatus).toBe('needs_training');
75
+ });
76
+
77
+ it('should set prompt_only for manual_only principles', () => {
78
+ // Arrange: Create EvolutionReducer with stateDir
79
+ const reducer = new EvolutionReducerImpl({ workspaceDir, stateDir });
80
+
81
+ // Act: Create a manual_only principle
82
+ const principleId = reducer.createPrincipleFromDiagnosis({
83
+ painId: 'pain-manual',
84
+ painType: 'user_frustration',
85
+ triggerPattern: 'user is confused',
86
+ action: 'ask clarifying question',
87
+ source: 'test',
88
+ evaluability: 'manual_only', // Cannot be auto-evaluated
89
+ });
90
+
91
+ // Assert: Principle should have prompt_only status
92
+ expect(principleId).not.toBeNull();
93
+
94
+ const store = loadStore(stateDir);
95
+ expect(store[principleId!]).toBeDefined();
96
+ expect(store[principleId!].evaluability).toBe('manual_only');
97
+ expect(store[principleId!].internalizationStatus).toBe('prompt_only');
98
+ });
99
+
100
+ it('should list auto-evaluable principles immediately after creation', () => {
101
+ // Arrange: Create reducer and auto-evaluable principle
102
+ const reducer = new EvolutionReducerImpl({ workspaceDir, stateDir });
103
+ const principleId = reducer.createPrincipleFromDiagnosis({
104
+ painId: 'pain-auto',
105
+ painType: 'tool_failure',
106
+ triggerPattern: 'timeout exceeded',
107
+ action: 'implement retry with backoff',
108
+ source: 'test',
109
+ evaluability: 'deterministic',
110
+ detectorMetadata: {
111
+ applicabilityTags: ['network-operations'],
112
+ positiveSignals: ['retry-success'],
113
+ negativeSignals: ['timeout-error'],
114
+ toolSequenceHints: [['fetch', 'retry']],
115
+ confidence: 'high',
116
+ },
117
+ });
118
+
119
+ // Assert: Principle should be immediately evaluable (no status upgrade needed)
120
+ const evaluablePrinciples = listEvaluablePrinciples(stateDir);
121
+ expect(evaluablePrinciples.length).toBeGreaterThan(0);
122
+ expect(evaluablePrinciples.some(p => p.principleId === principleId)).toBe(true);
123
+ });
124
+
125
+ it('should NOT list manual_only principles as evaluable', () => {
126
+ // Arrange: Create reducer and manual_only principle
127
+ const reducer = new EvolutionReducerImpl({ workspaceDir, stateDir });
128
+ reducer.createPrincipleFromDiagnosis({
129
+ painId: 'pain-003',
130
+ painType: 'user_frustration',
131
+ triggerPattern: 'user repeated same request',
132
+ action: 'ask clarifying question',
133
+ source: 'test',
134
+ evaluability: 'manual_only', // Should not be evaluable
135
+ });
136
+
137
+ // Act: List evaluable principles
138
+ const evaluablePrinciples = listEvaluablePrinciples(stateDir);
139
+
140
+ // Assert: No evaluable principles (manual_only is excluded)
141
+ expect(evaluablePrinciples).toHaveLength(0);
142
+ });
143
+ });
144
+
145
+ describe('State Consistency', () => {
146
+ it('should maintain consistency across evolution.jsonl, PRINCIPLES.md, and training store', () => {
147
+ // Arrange
148
+ const reducer = new EvolutionReducerImpl({ workspaceDir, stateDir });
149
+
150
+ // Act: Create multiple principles with complete detectorMetadata
151
+ const ids = [
152
+ reducer.createPrincipleFromDiagnosis({
153
+ painId: 'pain-a',
154
+ painType: 'tool_failure',
155
+ triggerPattern: 'pattern a',
156
+ action: 'action a',
157
+ source: 'test',
158
+ evaluability: 'deterministic',
159
+ detectorMetadata: {
160
+ applicabilityTags: ['test-a'],
161
+ positiveSignals: ['signal-a'],
162
+ negativeSignals: ['neg-a'],
163
+ toolSequenceHints: [['tool-a']],
164
+ confidence: 'high',
165
+ },
166
+ }),
167
+ reducer.createPrincipleFromDiagnosis({
168
+ painId: 'pain-b',
169
+ painType: 'subagent_error',
170
+ triggerPattern: 'pattern b',
171
+ action: 'action b',
172
+ source: 'test',
173
+ evaluability: 'weak_heuristic',
174
+ detectorMetadata: {
175
+ applicabilityTags: ['test-b'],
176
+ positiveSignals: ['signal-b'],
177
+ negativeSignals: ['neg-b'],
178
+ toolSequenceHints: [['tool-b']],
179
+ confidence: 'medium',
180
+ },
181
+ }),
182
+ ];
183
+
184
+ // Assert: All should be in training store with needs_training (auto-evaluable)
185
+ const store = loadStore(stateDir);
186
+ ids.forEach(id => {
187
+ expect(id).not.toBeNull();
188
+ expect(store[id!]).toBeDefined();
189
+ expect(store[id!].internalizationStatus).toBe('needs_training');
190
+ });
191
+
192
+ // Assert: Auto-evaluable principles are immediately available
193
+ const evaluablePrinciples = listEvaluablePrinciples(stateDir);
194
+ expect(evaluablePrinciples).toHaveLength(2);
195
+ });
196
+ });
197
+ });
@@ -0,0 +1,211 @@
1
+ /**
2
+ * E2E tests for tool hooks workspaceDir resolution
3
+ *
4
+ * Verifies that after_tool_call and before_tool_call hooks
5
+ * correctly resolve workspaceDir and write events to the correct location.
6
+ *
7
+ * This test addresses the bug where PluginHookToolContext lacks workspaceDir,
8
+ * causing events to be written to ~/.state/ instead of workspace directory.
9
+ */
10
+
11
+ import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+ import * as os from 'os';
15
+
16
+ // Mock OpenClaw API for testing
17
+ const createMockApi = (workspaceDir: string) => ({
18
+ runtime: {
19
+ agent: {
20
+ resolveAgentWorkspaceDir: vi.fn().mockReturnValue(workspaceDir),
21
+ },
22
+ },
23
+ config: {},
24
+ resolvePath: vi.fn().mockReturnValue(workspaceDir),
25
+ logger: {
26
+ warn: vi.fn(),
27
+ error: vi.fn(),
28
+ info: vi.fn(),
29
+ debug: vi.fn(),
30
+ },
31
+ pluginConfig: {},
32
+ });
33
+
34
+ describe('E2E: Tool Hooks workspaceDir Resolution', () => {
35
+ const testWorkspaceDir = path.join(os.tmpdir(), 'pd-tool-hooks-e2e-test');
36
+ const stateDir = path.join(testWorkspaceDir, '.state');
37
+ const logsDir = path.join(stateDir, 'logs');
38
+ const eventsFile = path.join(logsDir, 'events.jsonl');
39
+
40
+ beforeAll(() => {
41
+ // Create test workspace structure
42
+ fs.mkdirSync(logsDir, { recursive: true });
43
+ });
44
+
45
+ afterAll(() => {
46
+ // Cleanup
47
+ if (fs.existsSync(testWorkspaceDir)) {
48
+ fs.rmSync(testWorkspaceDir, { recursive: true, force: true });
49
+ }
50
+ });
51
+
52
+ describe('Scenario 1: ctx.workspaceDir is provided (future OpenClaw fix)', () => {
53
+ it('should use ctx.workspaceDir directly when valid', async () => {
54
+ const { validateWorkspaceDir } = await import('../../src/core/workspace-dir-validation.js');
55
+
56
+ const ctx = {
57
+ workspaceDir: testWorkspaceDir,
58
+ agentId: 'test-agent'
59
+ };
60
+
61
+ const result = validateWorkspaceDir(ctx.workspaceDir);
62
+ expect(result).toBeNull(); // Valid
63
+ });
64
+ });
65
+
66
+ describe('Scenario 2: ctx.workspaceDir is undefined (current OpenClaw behavior)', () => {
67
+ it('should fallback to agentId resolution', async () => {
68
+ const { resolveValidWorkspaceDir } = await import('../../src/core/workspace-dir-validation.js');
69
+
70
+ const mockApi = createMockApi(testWorkspaceDir);
71
+ const ctx = {
72
+ workspaceDir: undefined, // OpenClaw doesn't provide this
73
+ agentId: 'test-agent'
74
+ };
75
+
76
+ const result = resolveValidWorkspaceDir(ctx, mockApi as any, { source: 'after_tool_call' });
77
+
78
+ expect(result).toBe(testWorkspaceDir);
79
+ expect(mockApi.runtime.agent.resolveAgentWorkspaceDir).toHaveBeenCalledWith(mockApi.config, 'test-agent');
80
+ });
81
+
82
+ it('should fallback to resolvePath when agentId is also undefined', async () => {
83
+ const { resolveValidWorkspaceDir } = await import('../../src/core/workspace-dir-validation.js');
84
+
85
+ const mockApi = createMockApi(testWorkspaceDir);
86
+ const ctx = {
87
+ workspaceDir: undefined,
88
+ agentId: undefined
89
+ };
90
+
91
+ const result = resolveValidWorkspaceDir(ctx, mockApi as any, { source: 'after_tool_call' });
92
+
93
+ expect(result).toBe(testWorkspaceDir);
94
+ expect(mockApi.resolvePath).toHaveBeenCalledWith('.');
95
+ });
96
+ });
97
+
98
+ describe('Scenario 3: Events are written to correct location', () => {
99
+ it('should create events.jsonl in workspace directory with correct content', () => {
100
+ // Simulate event being written
101
+ const testEvent = {
102
+ ts: new Date().toISOString(),
103
+ type: 'hook_execution',
104
+ category: 'success',
105
+ data: { hook: 'after_tool_call' },
106
+ };
107
+
108
+ fs.appendFileSync(eventsFile, JSON.stringify(testEvent) + '\n', 'utf-8');
109
+
110
+ // Verify file exists in workspace directory
111
+ expect(fs.existsSync(eventsFile)).toBe(true);
112
+
113
+ // Verify content contains the expected hook
114
+ const content = fs.readFileSync(eventsFile, 'utf-8');
115
+ const lines = content.trim().split('\n');
116
+ const lastLine = JSON.parse(lines[lines.length - 1]);
117
+ expect(lastLine.data.hook).toBe('after_tool_call');
118
+
119
+ // Verify the path is NOT directly under home directory
120
+ // (it should be under a workspace subdirectory)
121
+ const homeDir = os.homedir();
122
+ const normalizedPath = path.normalize(eventsFile);
123
+ // Events file should not be directly at ~/.state/logs/events.jsonl
124
+ const directHomeEventsFile = path.join(homeDir, '.state', 'logs', 'events.jsonl');
125
+ // If it happens to be the same path, that's a problem
126
+ if (normalizedPath === path.normalize(directHomeEventsFile)) {
127
+ throw new Error('Events file is being written to ~/.state/ instead of workspace directory!');
128
+ }
129
+ });
130
+ });
131
+
132
+ describe('Scenario 4: Warning is logged when workspaceDir resolution fails', () => {
133
+ it('should warn when falling back to resolvePath with invalid result', async () => {
134
+ const { resolveValidWorkspaceDir } = await import('../../src/core/workspace-dir-validation.js');
135
+
136
+ const mockApi = createMockApi(os.homedir()); // resolvePath returns home dir
137
+ mockApi.runtime.agent.resolveAgentWorkspaceDir.mockReturnValue(os.homedir()); // agentId also returns home
138
+
139
+ const warningMessages: string[] = [];
140
+ const onWarning = (msg: string) => warningMessages.push(msg);
141
+
142
+ const ctx = { workspaceDir: undefined, agentId: 'test-agent' };
143
+
144
+ const result = resolveValidWorkspaceDir(ctx, mockApi as any, {
145
+ source: 'test',
146
+ onWarning,
147
+ });
148
+
149
+ // Should have warnings about invalid paths
150
+ expect(warningMessages.length).toBeGreaterThan(0);
151
+ expect(warningMessages.some(m => m.includes('FINAL FALLBACK') || m.includes('invalid'))).toBe(true);
152
+ });
153
+ });
154
+ });
155
+
156
+ describe('E2E: EventLog flushImmediately', () => {
157
+ const testWorkspaceDir = path.join(os.tmpdir(), 'pd-eventlog-flush-test');
158
+ const stateDir = path.join(testWorkspaceDir, '.state');
159
+ const logsDir = path.join(stateDir, 'logs');
160
+ const eventsFile = path.join(logsDir, 'events.jsonl');
161
+
162
+ beforeAll(() => {
163
+ fs.mkdirSync(logsDir, { recursive: true });
164
+ });
165
+
166
+ afterAll(() => {
167
+ if (fs.existsSync(testWorkspaceDir)) {
168
+ fs.rmSync(testWorkspaceDir, { recursive: true, force: true });
169
+ }
170
+ });
171
+
172
+ it('should flush events immediately when flushImmediately is true', async () => {
173
+ const { EventLog } = await import('../../src/core/event-log.js');
174
+
175
+ const eventLog = new EventLog(stateDir);
176
+
177
+ // Write with flushImmediately
178
+ eventLog.recordHookExecution({ hook: 'after_tool_call' }, { flushImmediately: true });
179
+
180
+ // File should exist immediately (not waiting for buffer or timer)
181
+ expect(fs.existsSync(eventsFile)).toBe(true);
182
+
183
+ // Content should be there
184
+ const content = fs.readFileSync(eventsFile, 'utf-8');
185
+ expect(content).toContain('after_tool_call');
186
+
187
+ eventLog.flush();
188
+ });
189
+
190
+ it('should not flush immediately when flushImmediately is false or omitted', async () => {
191
+ const { EventLog } = await import('../../src/core/event-log.js');
192
+
193
+ // Clean up
194
+ if (fs.existsSync(eventsFile)) {
195
+ fs.unlinkSync(eventsFile);
196
+ }
197
+
198
+ const eventLog = new EventLog(stateDir);
199
+
200
+ // Write without flushImmediately
201
+ eventLog.recordHookExecution({ hook: 'before_tool_call' });
202
+
203
+ // File might not exist yet (depends on buffer)
204
+ // But after manual flush, it should be there
205
+ eventLog.flush();
206
+
207
+ expect(fs.existsSync(eventsFile)).toBe(true);
208
+ const content = fs.readFileSync(eventsFile, 'utf-8');
209
+ expect(content).toContain('before_tool_call');
210
+ });
211
+ });