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
@@ -2,14 +2,10 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
2
  import {
3
3
  EvolutionWorkerService,
4
4
  createEvolutionTaskId,
5
- createPainCandidateFingerprint,
6
5
  extractEvolutionTaskId,
7
6
  hasRecentDuplicateTask,
8
7
  hasEquivalentPromotedRule,
9
- processPromotion,
10
8
  registerEvolutionTaskSession,
11
- shouldTrackPainCandidate,
12
- trackPainCandidate,
13
9
  } from '../../src/service/evolution-worker.js';
14
10
  import { DictionaryService } from '../../src/core/dictionary-service.js';
15
11
  import * as sessionTracker from '../../src/core/session-tracker.js';
@@ -26,15 +22,6 @@ vi.mock('../../src/core/session-tracker', () => ({
26
22
  flushAllSessions: vi.fn(),
27
23
  listSessions: vi.fn(() => []), // Returns empty sessions for idle detection
28
24
  }));
29
- vi.mock('../../src/core/event-log', () => ({
30
- EventLogService: {
31
- get: vi.fn(() => ({
32
- recordEvolutionTask: vi.fn(),
33
- recordRulePromotion: vi.fn(),
34
- flush: vi.fn(),
35
- })),
36
- },
37
- }));
38
25
 
39
26
  describe('EvolutionWorkerService', () => {
40
27
  beforeEach(() => {
@@ -103,43 +90,6 @@ describe('EvolutionWorkerService', () => {
103
90
  expect(idA).not.toBe(idB);
104
91
  });
105
92
 
106
- it('should generate distinct pain candidate fingerprints when only the suffix differs', () => {
107
- const prefix = 'A'.repeat(60);
108
- const textA = `${prefix} root-cause-one`;
109
- const textB = `${prefix} root-cause-two`;
110
-
111
- expect(createPainCandidateFingerprint(textA)).not.toBe(createPainCandidateFingerprint(textB));
112
- });
113
-
114
- it('should skip known noise payloads when tracking pain candidates', () => {
115
- expect(shouldTrackPainCandidate('NO_REPLY')).toBe(false);
116
- expect(shouldTrackPainCandidate(
117
- '{"damageDetected":false,"severity":"mild","confidence":0.95,"reason":"observer"}'
118
- )).toBe(false);
119
- expect(shouldTrackPainCandidate('Tool edit failed on MEMORY.md')).toBe(true);
120
- });
121
-
122
- it('should initialize candidates as pending and keep longer samples', async () => {
123
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-pain-candidate-'));
124
- const candidatePath = path.join(dir, 'pain_candidates.json');
125
- const longText = `Tool edit failed on MEMORY.md: ${'x'.repeat(1200)}`;
126
-
127
- try {
128
- await trackPainCandidate(longText, {
129
- resolve: () => candidatePath,
130
- } as any);
131
-
132
- const data = JSON.parse(fs.readFileSync(candidatePath, 'utf8'));
133
- const candidate = Object.values(data.candidates)[0] as any;
134
-
135
- expect(candidate.status).toBe('pending');
136
- expect(candidate.samples[0].length).toBeGreaterThan(200);
137
- expect(candidate.samples[0].length).toBeLessThanOrEqual(1000);
138
- } finally {
139
- safeRmDir(dir);
140
- }
141
- });
142
-
143
93
  it('should extract evolution task ids from diagnostician payloads', () => {
144
94
  expect(extractEvolutionTaskId('Diagnose systemic pain [ID: ab12cd34]. Source: tool_failure.')).toBe('ab12cd34');
145
95
  expect(extractEvolutionTaskId('plain task without id')).toBeNull();
@@ -171,79 +121,6 @@ describe('EvolutionWorkerService', () => {
171
121
  }
172
122
  });
173
123
 
174
- it('should promote legacy candidates even when status is missing', async () => {
175
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-promotion-'));
176
- const candidatePath = path.join(dir, 'pain_candidates.json');
177
- const addRule = vi.fn();
178
-
179
- fs.writeFileSync(candidatePath, JSON.stringify({
180
- candidates: {
181
- deadbeef: {
182
- count: 3,
183
- firstSeen: '2026-03-20T00:00:00.000Z',
184
- samples: [
185
- 'Tool edit failed on MEMORY.md. Could not find the exact text in MEMORY.md.',
186
- 'Tool edit failed on CURRENT_FOCUS.md. Could not find the exact text in CURRENT_FOCUS.md.',
187
- 'Tool edit failed on TEAM_COMMS.md. Could not find the exact text in TEAM_COMMS.md.',
188
- ],
189
- },
190
- },
191
- }, null, 2), 'utf8');
192
-
193
- try {
194
- await processPromotion({
195
- workspaceDir: dir,
196
- resolve: () => candidatePath,
197
- config: {
198
- get: (key: string) => {
199
- if (key === 'thresholds.promotion_count_threshold') return 3;
200
- if (key === 'scores.default_confusion') return 35;
201
- return undefined;
202
- },
203
- },
204
- dictionary: {
205
- addRule,
206
- getAllRules: () => ({}),
207
- },
208
- } as any, { info: vi.fn() }, null);
209
-
210
- const saved = JSON.parse(fs.readFileSync(candidatePath, 'utf8'));
211
- expect(addRule).toHaveBeenCalled();
212
- expect(saved.candidates.deadbeef.status).toBe('promoted');
213
- } finally {
214
- safeRmDir(dir);
215
- }
216
- });
217
-
218
- it('should flush the dictionary on its interval', async () => {
219
- const mockDict = {
220
- flush: vi.fn()
221
- };
222
- vi.mocked(DictionaryService.get).mockReturnValue(mockDict as any);
223
-
224
- // Use path.resolve for cross-platform compatibility
225
- const workspaceDir = path.resolve('/mock/workspace');
226
- const expectedStateDir = path.join(workspaceDir, '.state');
227
-
228
- const ctx = {
229
- workspaceDir,
230
- stateDir: path.resolve('/mock/state'),
231
- logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }
232
- };
233
-
234
- EvolutionWorkerService.start(ctx as any);
235
-
236
- // Advance by 15 minutes
237
- await vi.advanceTimersByTimeAsync(15 * 60 * 1000);
238
-
239
- expect(mockDict.flush).toHaveBeenCalled();
240
- // Service now uses workspace-specific stateDir, not ctx.stateDir
241
- expect(sessionTracker.initPersistence).toHaveBeenCalledWith(expectedStateDir);
242
- expect(sessionTracker.flushAllSessions).toHaveBeenCalled();
243
-
244
- EvolutionWorkerService.stop(ctx as any);
245
- });
246
-
247
124
  it('should process queue work without persisting a legacy directive file', async () => {
248
125
  const mockDict = {
249
126
  flush: vi.fn()
@@ -0,0 +1,330 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import * as os from 'os';
4
+ import * as path from 'path';
5
+ import { executeNocturnalReflection } from '../../src/service/nocturnal-service.js';
6
+ import { createNocturnalTrajectoryExtractor } from '../../src/core/nocturnal-trajectory-extractor.js';
7
+ import { getDatasetRecordByArtifactId, listDatasetRecords } from '../../src/core/nocturnal-dataset.js';
8
+ import { listArtifactLineageRecords } from '../../src/core/nocturnal-artifact-lineage.js';
9
+ import {
10
+ listImplementationsForRule,
11
+ saveLedger,
12
+ type LedgerPrinciple,
13
+ type LedgerRule,
14
+ } from '../../src/core/principle-tree-ledger.js';
15
+ import {
16
+ getImplementationAssetRoot,
17
+ loadEntrySource,
18
+ loadManifest,
19
+ } from '../../src/core/code-implementation-storage.js';
20
+ import { TrajectoryDatabase, TrajectoryRegistry } from '../../src/core/trajectory.js';
21
+ import { clearSession, listSessions, seedSessionForTest } from '../../src/core/session-tracker.js';
22
+ import { safeRmDir } from '../test-utils.js';
23
+
24
+ function makePrinciple(overrides: Partial<LedgerPrinciple> = {}): LedgerPrinciple {
25
+ return {
26
+ id: 'T-08',
27
+ version: 1,
28
+ text: 'Pain as Signal',
29
+ triggerPattern: 'pain',
30
+ action: 'Diagnose before repeating failures',
31
+ status: 'active',
32
+ priority: 'P1',
33
+ scope: 'general',
34
+ evaluability: 'deterministic',
35
+ valueScore: 0,
36
+ adherenceRate: 80,
37
+ painPreventedCount: 0,
38
+ derivedFromPainIds: [],
39
+ ruleIds: ['R-001'],
40
+ conflictsWithPrincipleIds: [],
41
+ createdAt: '2026-04-08T00:00:00.000Z',
42
+ updatedAt: '2026-04-08T00:00:00.000Z',
43
+ ...overrides,
44
+ };
45
+ }
46
+
47
+ function makeRule(overrides: Partial<LedgerRule> = {}): LedgerRule {
48
+ return {
49
+ id: 'R-001',
50
+ version: 1,
51
+ name: 'Protect risky write',
52
+ description: 'Require approval for risky write operations.',
53
+ type: 'gate',
54
+ triggerCondition: 'risky write',
55
+ enforcement: 'block',
56
+ action: 'require approval for risky write',
57
+ principleId: 'T-08',
58
+ status: 'implemented',
59
+ coverageRate: 0,
60
+ falsePositiveRate: 0,
61
+ implementationIds: [],
62
+ createdAt: '2026-04-08T00:00:00.000Z',
63
+ updatedAt: '2026-04-08T00:00:00.000Z',
64
+ ...overrides,
65
+ };
66
+ }
67
+
68
+ function makeBehavioralArtifact(sessionId: string) {
69
+ return {
70
+ artifactId:
71
+ sessionId === 'session-skip'
72
+ ? '11111111-1111-4111-8111-111111111111'
73
+ : sessionId === 'session-success'
74
+ ? '22222222-2222-4222-8222-222222222222'
75
+ : sessionId === 'session-invalid'
76
+ ? '33333333-3333-4333-8333-333333333333'
77
+ : '44444444-4444-4444-8444-444444444444',
78
+ sessionId,
79
+ principleId: 'T-08',
80
+ sourceSnapshotRef: `snapshot-${sessionId}`,
81
+ badDecision: 'Retried a failing write operation without first checking the approval requirements',
82
+ betterDecision:
83
+ 'Read docs/gateblocks.md to confirm the approval requirements before retrying the write operation',
84
+ rationale: 'Checking the gate guidance first prevents repeated blocked writes and preserves operator review boundaries',
85
+ createdAt: '2026-04-08T00:00:00.000Z',
86
+ };
87
+ }
88
+
89
+ describe('nocturnal-service code candidates', () => {
90
+ let tempDir: string;
91
+ let workspaceDir: string;
92
+ let stateDir: string;
93
+ let trajectory: TrajectoryDatabase;
94
+
95
+ beforeEach(() => {
96
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-nocturnal-code-candidate-'));
97
+ workspaceDir = path.join(tempDir, 'workspace');
98
+ stateDir = path.join(tempDir, 'state');
99
+ fs.mkdirSync(workspaceDir, { recursive: true });
100
+ fs.mkdirSync(stateDir, { recursive: true });
101
+ trajectory = new TrajectoryDatabase({ workspaceDir });
102
+ createNocturnalTrajectoryExtractor(workspaceDir);
103
+
104
+ for (const session of listSessions()) {
105
+ clearSession(session.sessionId);
106
+ }
107
+ });
108
+
109
+ afterEach(() => {
110
+ try {
111
+ trajectory.dispose();
112
+ } catch {
113
+ // Best effort cleanup.
114
+ }
115
+ try {
116
+ TrajectoryRegistry.dispose(workspaceDir);
117
+ } catch {
118
+ // Best effort cleanup.
119
+ }
120
+ safeRmDir(tempDir);
121
+ });
122
+
123
+ function makeIdleResult() {
124
+ return {
125
+ isIdle: true,
126
+ mostRecentActivityAt: Date.now() - 2 * 60 * 60 * 1000,
127
+ idleForMs: 2 * 60 * 60 * 1000,
128
+ userActiveSessions: 0,
129
+ abandonedSessionIds: [],
130
+ trajectoryGuardrailConfirmsIdle: true,
131
+ reason: 'test override',
132
+ };
133
+ }
134
+
135
+ function seedSession(sessionId: string): void {
136
+ const startedAt = new Date().toISOString();
137
+ trajectory.recordSession({ sessionId, startedAt });
138
+ for (let i = 0; i < 2; i += 1) {
139
+ trajectory.recordToolCall({
140
+ sessionId,
141
+ toolName: 'write',
142
+ outcome: 'failure',
143
+ errorMessage: 'risky write blocked',
144
+ });
145
+ }
146
+ trajectory.recordPainEvent({
147
+ sessionId,
148
+ source: 'gate',
149
+ score: 75,
150
+ reason: 'risky write requires approval',
151
+ });
152
+ trajectory.recordGateBlock({
153
+ sessionId,
154
+ toolName: 'write',
155
+ reason: 'risky write requires approval',
156
+ riskLevel: 'medium',
157
+ });
158
+ seedSessionForTest(sessionId, workspaceDir, new Date(startedAt).getTime());
159
+ }
160
+
161
+ function seedLedger(singleRule = true): void {
162
+ saveLedger(stateDir, {
163
+ trainingStore: {
164
+ 'T-08': {
165
+ principleId: 'T-08',
166
+ evaluability: 'deterministic',
167
+ applicableOpportunityCount: 5,
168
+ observedViolationCount: 3,
169
+ complianceRate: 0.2,
170
+ violationTrend: 1,
171
+ generatedSampleCount: 0,
172
+ approvedSampleCount: 0,
173
+ includedTrainRunIds: [],
174
+ deployedCheckpointIds: [],
175
+ internalizationStatus: 'internalized',
176
+ },
177
+ },
178
+ tree: {
179
+ principles: {
180
+ 'T-08': makePrinciple({
181
+ ruleIds: singleRule ? ['R-001'] : ['R-001', 'R-002'],
182
+ }),
183
+ },
184
+ rules: singleRule
185
+ ? {
186
+ 'R-001': makeRule(),
187
+ }
188
+ : {
189
+ 'R-001': makeRule(),
190
+ 'R-002': makeRule({
191
+ id: 'R-002',
192
+ name: 'Protect risky write',
193
+ description: 'Require approval for risky write operations.',
194
+ triggerCondition: 'risky write',
195
+ action: 'require approval for risky write',
196
+ }),
197
+ },
198
+ implementations: {},
199
+ metrics: {},
200
+ lastUpdated: '2026-04-08T00:00:00.000Z',
201
+ },
202
+ });
203
+ }
204
+
205
+ it('persists a behavioral artifact only when Artificer routing is skipped', () => {
206
+ seedLedger(false);
207
+ seedSession('session-skip');
208
+
209
+ const result = executeNocturnalReflection(workspaceDir, stateDir, {
210
+ idleCheckOverride: makeIdleResult(),
211
+ skipReflector: true,
212
+ reflectorOutputOverride: JSON.stringify(makeBehavioralArtifact('session-skip')),
213
+ });
214
+
215
+ expect(result.success).toBe(true);
216
+ expect(result.diagnostics.persisted).toBe(true);
217
+ expect(result.diagnostics.artificer.status).toBe('skipped');
218
+ expect(result.diagnostics.artificer.reason).toBe('no_deterministic_rule');
219
+ expect(listImplementationsForRule(stateDir, 'R-001')).toHaveLength(0);
220
+
221
+ const datasetRecord = getDatasetRecordByArtifactId(
222
+ workspaceDir,
223
+ result.artifact!.artifactId
224
+ );
225
+ expect(datasetRecord?.classification).toBeNull();
226
+ });
227
+
228
+ it('persists a behavioral artifact and a candidate implementation when Artificer succeeds', () => {
229
+ seedLedger(true);
230
+ seedSession('session-success');
231
+
232
+ const result = executeNocturnalReflection(workspaceDir, stateDir, {
233
+ idleCheckOverride: makeIdleResult(),
234
+ skipReflector: true,
235
+ reflectorOutputOverride: JSON.stringify(makeBehavioralArtifact('session-success')),
236
+ });
237
+
238
+ expect(result.success).toBe(true);
239
+ expect(result.diagnostics.artificer.status).toBe('persisted_candidate');
240
+
241
+ const implementations = listImplementationsForRule(stateDir, 'R-001');
242
+ expect(implementations).toHaveLength(1);
243
+ expect(implementations[0].lifecycleState).toBe('candidate');
244
+
245
+ const manifest = loadManifest(stateDir, implementations[0].id);
246
+ expect(manifest?.lineage).toMatchObject({
247
+ principleId: 'T-08',
248
+ ruleId: 'R-001',
249
+ sourceSessionId: 'session-success',
250
+ });
251
+ expect(loadEntrySource(stateDir, implementations[0].id)).toContain(
252
+ 'requireApproval'
253
+ );
254
+ expect(fs.existsSync(getImplementationAssetRoot(stateDir, implementations[0].id))).toBe(
255
+ true
256
+ );
257
+
258
+ const lineage = listArtifactLineageRecords(workspaceDir);
259
+ expect(
260
+ lineage.some((record) => record.artifactKind === 'behavioral-sample')
261
+ ).toBe(true);
262
+ expect(
263
+ lineage.some(
264
+ (record) =>
265
+ record.artifactKind === 'rule-implementation-candidate' &&
266
+ record.implementationId === implementations[0].id
267
+ )
268
+ ).toBe(true);
269
+
270
+ const datasetRecord = getDatasetRecordByArtifactId(
271
+ workspaceDir,
272
+ result.artifact!.artifactId
273
+ );
274
+ expect(datasetRecord?.classification).toBeNull();
275
+ });
276
+
277
+ it('preserves the behavioral artifact when Artificer validation fails', () => {
278
+ seedLedger(true);
279
+ seedSession('session-invalid');
280
+
281
+ const result = executeNocturnalReflection(workspaceDir, stateDir, {
282
+ idleCheckOverride: makeIdleResult(),
283
+ skipReflector: true,
284
+ reflectorOutputOverride: JSON.stringify(makeBehavioralArtifact('session-invalid')),
285
+ artificerOutputOverride: JSON.stringify({
286
+ ruleId: 'R-001',
287
+ candidateSource:
288
+ 'export const meta = { name: "bad", version: "1", ruleId: "R-001", coversCondition: "bad" }; export function evaluate() { eval("1 + 1"); return { decision: "allow", matched: false, reason: "bad" }; }',
289
+ helperUsage: [],
290
+ expectedDecision: 'allow',
291
+ rationale: 'bad candidate',
292
+ lineage: {
293
+ artifactKind: 'rule-implementation-candidate',
294
+ sourceSnapshotRef: 'snapshot-1',
295
+ sourcePainIds: ['pain:1'],
296
+ sourceGateBlockIds: ['gate:1'],
297
+ },
298
+ }),
299
+ });
300
+
301
+ expect(result.success).toBe(true);
302
+ expect(result.diagnostics.persisted).toBe(true);
303
+ expect(result.diagnostics.artificer.status).toBe('validation_failed');
304
+ expect(result.diagnostics.artificer.reason).toBe('validator_rejected');
305
+ expect(listImplementationsForRule(stateDir, 'R-001')).toHaveLength(0);
306
+ });
307
+
308
+ it('cleans up the candidate when storage fails after ledger creation', () => {
309
+ seedLedger(true);
310
+ seedSession('session-storage-failure');
311
+ fs.mkdirSync(stateDir, { recursive: true });
312
+ fs.writeFileSync(path.join(stateDir, 'principles'), 'not-a-directory', 'utf-8');
313
+
314
+ const result = executeNocturnalReflection(workspaceDir, stateDir, {
315
+ idleCheckOverride: makeIdleResult(),
316
+ skipReflector: true,
317
+ reflectorOutputOverride: JSON.stringify(
318
+ makeBehavioralArtifact('session-storage-failure')
319
+ ),
320
+ });
321
+
322
+ expect(result.success).toBe(true);
323
+ expect(result.diagnostics.artificer.status).toBe('validation_failed');
324
+ expect(result.diagnostics.artificer.reason).toBe('persistence_failed');
325
+ expect(listImplementationsForRule(stateDir, 'R-001')).toHaveLength(0);
326
+ expect(
327
+ listArtifactLineageRecords(workspaceDir, 'rule-implementation-candidate')
328
+ ).toHaveLength(0);
329
+ });
330
+ });
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { extractCommonPhrases, extractCommonSubstring } from '../../src/utils/nlp.js';
2
+ import { extractCommonPhrases } from '../../src/utils/nlp.js';
3
3
 
4
4
  describe('NLP Utils', () => {
5
5
  describe('extractCommonPhrases (English)', () => {
@@ -32,22 +32,4 @@ describe('NLP Utils', () => {
32
32
  expect(result).toHaveLength(0);
33
33
  });
34
34
  });
35
-
36
- describe('extractCommonSubstring (Chinese/Mixed)', () => {
37
- it('should extract common substring from Chinese text', () => {
38
- const samples = [
39
- "这个问题让我有点头疼,不知道怎么解决。",
40
- "代码报错了,让我有点头疼。",
41
- "让我有点头疼,这逻辑不对啊。"
42
- ];
43
- const result = extractCommonSubstring(samples, 4);
44
- // "让我有点头疼" is 6 chars, so it should find it or a part of it
45
- expect(result[0]).toContain('让我有点头疼');
46
- });
47
-
48
- it('should handle short or insufficient samples', () => {
49
- expect(extractCommonSubstring(["仅有一个样本"])).toHaveLength(0);
50
- expect(extractCommonSubstring([], 4)).toHaveLength(0);
51
- });
52
- });
53
35
  });