principles-disciple 1.16.0 → 1.18.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 (132) hide show
  1. package/README.md +13 -5
  2. package/openclaw.plugin.json +4 -4
  3. package/package.json +1 -1
  4. package/src/commands/archive-impl.ts +3 -3
  5. package/src/commands/capabilities.ts +1 -1
  6. package/src/commands/context.ts +3 -3
  7. package/src/commands/disable-impl.ts +1 -1
  8. package/src/commands/evolution-status.ts +2 -2
  9. package/src/commands/focus.ts +2 -2
  10. package/src/commands/nocturnal-train.ts +6 -6
  11. package/src/commands/pain.ts +4 -4
  12. package/src/commands/pd-reflect.ts +87 -0
  13. package/src/commands/rollback-impl.ts +4 -4
  14. package/src/commands/rollback.ts +2 -2
  15. package/src/commands/samples.ts +2 -2
  16. package/src/commands/workflow-debug.ts +1 -1
  17. package/src/config/errors.ts +1 -1
  18. package/src/core/adaptive-thresholds.ts +1 -1
  19. package/src/core/code-implementation-storage.ts +2 -2
  20. package/src/core/config.ts +1 -1
  21. package/src/core/diagnostician-task-store.ts +2 -2
  22. package/src/core/empathy-keyword-matcher.ts +3 -3
  23. package/src/core/event-log.ts +5 -5
  24. package/src/core/evolution-engine.ts +4 -4
  25. package/src/core/evolution-logger.ts +1 -1
  26. package/src/core/evolution-reducer.ts +3 -3
  27. package/src/core/evolution-types.ts +5 -5
  28. package/src/core/external-training-contract.ts +1 -1
  29. package/src/core/focus-history.ts +14 -14
  30. package/src/core/hygiene/tracker.ts +1 -1
  31. package/src/core/init.ts +2 -2
  32. package/src/core/model-deployment-registry.ts +2 -2
  33. package/src/core/model-training-registry.ts +2 -2
  34. package/src/core/nocturnal-arbiter.ts +1 -1
  35. package/src/core/nocturnal-artificer.ts +2 -2
  36. package/src/core/nocturnal-candidate-scoring.ts +2 -2
  37. package/src/core/nocturnal-compliance.ts +4 -3
  38. package/src/core/nocturnal-dataset.ts +3 -3
  39. package/src/core/nocturnal-export.ts +4 -4
  40. package/src/core/nocturnal-rule-implementation-validator.ts +1 -1
  41. package/src/core/nocturnal-snapshot-contract.ts +112 -0
  42. package/src/core/nocturnal-trajectory-extractor.ts +7 -5
  43. package/src/core/nocturnal-trinity.ts +480 -158
  44. package/src/core/pain-context-extractor.ts +3 -3
  45. package/src/core/pain.ts +124 -11
  46. package/src/core/path-resolver.ts +4 -4
  47. package/src/core/pd-task-reconciler.ts +10 -10
  48. package/src/core/pd-task-service.ts +1 -1
  49. package/src/core/pd-task-store.ts +1 -1
  50. package/src/core/principle-internalization/deprecated-readiness.ts +1 -1
  51. package/src/core/principle-training-state.ts +2 -2
  52. package/src/core/principle-tree-ledger.ts +7 -7
  53. package/src/core/promotion-gate.ts +9 -9
  54. package/src/core/replay-engine.ts +12 -12
  55. package/src/core/risk-calculator.ts +1 -1
  56. package/src/core/rule-host-types.ts +2 -2
  57. package/src/core/rule-host.ts +5 -5
  58. package/src/core/schema/db-types.ts +1 -1
  59. package/src/core/schema/schema-definitions.ts +1 -1
  60. package/src/core/session-tracker.ts +96 -4
  61. package/src/core/shadow-observation-registry.ts +3 -3
  62. package/src/core/system-logger.ts +2 -2
  63. package/src/core/thinking-os-parser.ts +1 -1
  64. package/src/core/training-program.ts +2 -2
  65. package/src/core/trajectory.ts +8 -8
  66. package/src/core/workspace-context.ts +2 -2
  67. package/src/core/workspace-dir-service.ts +85 -0
  68. package/src/core/workspace-dir-validation.ts +30 -107
  69. package/src/hooks/bash-risk.ts +3 -3
  70. package/src/hooks/edit-verification.ts +4 -4
  71. package/src/hooks/gate-block-helper.ts +4 -4
  72. package/src/hooks/gate.ts +10 -10
  73. package/src/hooks/gfi-gate.ts +7 -7
  74. package/src/hooks/lifecycle.ts +2 -2
  75. package/src/hooks/llm.ts +1 -1
  76. package/src/hooks/pain.ts +25 -5
  77. package/src/hooks/progressive-trust-gate.ts +7 -7
  78. package/src/hooks/prompt.ts +24 -5
  79. package/src/hooks/subagent.ts +2 -2
  80. package/src/hooks/thinking-checkpoint.ts +2 -2
  81. package/src/hooks/trajectory-collector.ts +1 -1
  82. package/src/http/principles-console-route.ts +14 -6
  83. package/src/i18n/commands.ts +4 -0
  84. package/src/index.ts +181 -185
  85. package/src/service/central-health-service.ts +1 -1
  86. package/src/service/central-overview-service.ts +3 -3
  87. package/src/service/evolution-query-service.ts +1 -1
  88. package/src/service/evolution-worker.ts +221 -109
  89. package/src/service/health-query-service.ts +27 -17
  90. package/src/service/monitoring-query-service.ts +3 -3
  91. package/src/service/nocturnal-runtime.ts +4 -4
  92. package/src/service/nocturnal-service.ts +40 -23
  93. package/src/service/nocturnal-target-selector.ts +11 -4
  94. package/src/service/runtime-summary-service.ts +1 -1
  95. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +1 -1
  96. package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +3 -3
  97. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +16 -13
  98. package/src/service/subagent-workflow/runtime-direct-driver.ts +10 -6
  99. package/src/service/subagent-workflow/types.ts +4 -4
  100. package/src/service/subagent-workflow/workflow-manager-base.ts +5 -5
  101. package/src/service/subagent-workflow/workflow-store.ts +2 -2
  102. package/src/tools/critique-prompt.ts +2 -3
  103. package/src/tools/deep-reflect.ts +17 -16
  104. package/src/tools/model-index.ts +1 -1
  105. package/src/utils/file-lock.ts +1 -1
  106. package/src/utils/io.ts +7 -2
  107. package/src/utils/nlp.ts +1 -1
  108. package/src/utils/plugin-logger.ts +2 -2
  109. package/src/utils/retry.ts +3 -2
  110. package/src/utils/subagent-probe.ts +20 -33
  111. package/templates/langs/en/skills/pd-pain-signal/SKILL.md +8 -7
  112. package/templates/langs/zh/skills/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +111 -0
  113. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/task-specs.mjs +1 -1
  114. package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +1 -1
  115. package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +8 -7
  116. package/templates/pain_settings.json +1 -1
  117. package/tests/build-artifacts.test.ts +4 -58
  118. package/tests/commands/pd-reflect.test.ts +49 -0
  119. package/tests/core/nocturnal-snapshot-contract.test.ts +70 -0
  120. package/tests/core/pain-auto-repair.test.ts +96 -0
  121. package/tests/core/pain-integration.test.ts +483 -0
  122. package/tests/core/pain.test.ts +5 -4
  123. package/tests/core/workspace-dir-service.test.ts +68 -0
  124. package/tests/core/workspace-dir-validation.test.ts +56 -192
  125. package/tests/hooks/pain.test.ts +20 -0
  126. package/tests/http/principles-console-route.test.ts +42 -20
  127. package/tests/integration/empathy-workflow-integration.test.ts +1 -2
  128. package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +9 -17
  129. package/tests/service/empathy-observer-workflow-manager.test.ts +1 -2
  130. package/tests/service/evolution-worker.nocturnal.test.ts +118 -109
  131. package/tests/service/nocturnal-runtime-hardening.test.ts +33 -0
  132. package/tests/utils/subagent-probe.test.ts +32 -0
@@ -33,8 +33,9 @@ vi.mock('../../src/service/subagent-workflow/nocturnal-workflow-manager.js', ()
33
33
  },
34
34
  }));
35
35
 
36
- const { mockGetNocturnalSessionSnapshot } = vi.hoisted(() => ({
36
+ const { mockGetNocturnalSessionSnapshot, mockListRecentNocturnalCandidateSessions } = vi.hoisted(() => ({
37
37
  mockGetNocturnalSessionSnapshot: vi.fn(),
38
+ mockListRecentNocturnalCandidateSessions: vi.fn(() => []),
38
39
  }));
39
40
  vi.mock('../../src/core/nocturnal-trajectory-extractor.js', async () => {
40
41
  const actual = await vi.importActual<typeof import('../../src/core/nocturnal-trajectory-extractor.js')>(
@@ -44,11 +45,14 @@ vi.mock('../../src/core/nocturnal-trajectory-extractor.js', async () => {
44
45
  ...actual,
45
46
  createNocturnalTrajectoryExtractor: vi.fn(() => ({
46
47
  getNocturnalSessionSnapshot: mockGetNocturnalSessionSnapshot,
48
+ listRecentNocturnalCandidateSessions: mockListRecentNocturnalCandidateSessions,
47
49
  })),
48
50
  };
49
51
  });
50
52
 
51
- import { EvolutionWorkerService } from '../../src/service/evolution-worker.js';
53
+ import { EvolutionWorkerService, readRecentPainContext } from '../../src/service/evolution-worker.js';
54
+ import { WorkspaceContext } from '../../src/core/workspace-context.js';
55
+ import { handlePdReflect } from '../../src/commands/pd-reflect.js';
52
56
  import { safeRmDir } from '../test-utils.js';
53
57
 
54
58
  function readQueue(stateDir: string) {
@@ -67,141 +71,146 @@ describe('EvolutionWorkerService nocturnal hardening', () => {
67
71
  EvolutionWorkerService.api = null;
68
72
  });
69
73
 
70
- it('does not start a nocturnal workflow when only an empty fallback snapshot is available', async () => {
71
- const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-nocturnal-empty-'));
74
+ it('extracts session_id from .pain_flag file correctly', async () => {
75
+ const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-pain-session-'));
72
76
  const stateDir = path.join(workspaceDir, '.state');
73
- fs.mkdirSync(path.join(stateDir, 'sessions'), { recursive: true });
74
- fs.mkdirSync(path.join(stateDir, 'logs'), { recursive: true });
75
-
76
- mockGetNocturnalSessionSnapshot.mockReturnValue(null);
77
+ fs.mkdirSync(stateDir, { recursive: true });
77
78
 
79
+ // Write a pain flag WITH session_id
78
80
  fs.writeFileSync(
79
- path.join(stateDir, 'evolution_queue.json'),
80
- JSON.stringify([
81
- {
82
- id: 'sleep-empty',
83
- taskKind: 'sleep_reflection',
84
- priority: 'medium',
85
- score: 50,
86
- source: 'nocturnal',
87
- reason: 'Sleep reflection',
88
- timestamp: '2026-04-10T00:00:00.000Z',
89
- enqueued_at: '2026-04-10T00:00:00.000Z',
90
- status: 'pending',
91
- retryCount: 0,
92
- maxRetries: 1,
93
- recentPainContext: {
94
- mostRecent: null,
95
- recentPainCount: 0,
96
- recentMaxPainScore: 0,
97
- },
98
- },
99
- ], null, 2),
81
+ path.join(stateDir, '.pain_flag'),
82
+ `source: test_pain
83
+ score: 80
84
+ reason: test reason
85
+ time: 2026-04-10T00:00:00.000Z
86
+ session_id: explicit-session-from-pain
87
+ `,
100
88
  'utf8'
101
89
  );
102
90
 
91
+ // Create a WorkspaceContext to test the function
92
+ const wctx = WorkspaceContext.fromHookContext({ workspaceDir, stateDir, logger: console } as any);
93
+
103
94
  try {
104
- EvolutionWorkerService.start({
105
- workspaceDir,
106
- stateDir,
107
- logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
108
- config: {},
109
- } as any);
95
+ const context = readRecentPainContext(wctx);
96
+
97
+ // Verify the session_id was extracted from the pain flag file
98
+ expect(context.mostRecent).toBeDefined();
99
+ expect(context.mostRecent.sessionId).toBe('explicit-session-from-pain');
100
+ expect(context.mostRecent.score).toBe(80);
101
+ expect(context.recentPainCount).toBe(1);
102
+ } finally {
103
+ safeRmDir(workspaceDir);
104
+ }
105
+ });
110
106
 
111
- await vi.advanceTimersByTimeAsync(6000);
107
+ it('treats malformed pain flag data as unusable context', async () => {
108
+ const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-pain-invalid-'));
109
+ const stateDir = path.join(workspaceDir, '.state');
110
+ fs.mkdirSync(stateDir, { recursive: true });
112
111
 
113
- const queue = readQueue(stateDir);
114
- expect(queue[0].status).toBe('failed');
115
- expect(queue[0].lastError).toContain('missing_usable_snapshot');
116
- expect(queue[0].resultRef).toBeFalsy();
117
- expect(mockStartWorkflow).not.toHaveBeenCalled();
112
+ fs.writeFileSync(
113
+ path.join(stateDir, '.pain_flag'),
114
+ `source: test_pain
115
+ score: 80`,
116
+ 'utf8'
117
+ );
118
+
119
+ const wctx = WorkspaceContext.fromHookContext({ workspaceDir, stateDir, logger: console } as any);
120
+
121
+ try {
122
+ const context = readRecentPainContext(wctx);
123
+ expect(context.mostRecent).toBeNull();
124
+ expect(context.recentPainCount).toBe(0);
118
125
  } finally {
119
- EvolutionWorkerService.stop({ workspaceDir, stateDir, logger: console } as any);
120
126
  safeRmDir(workspaceDir);
121
127
  }
122
128
  });
123
129
 
124
- it('keeps gateway-only background failures as failed instead of completed stub fallback', async () => {
125
- const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-nocturnal-gateway-'));
130
+ // === End-to-End Contract Tests ===
131
+
132
+ it('e2e: pain flag → worker enqueue → session_id is correctly attached to queued task', async () => {
133
+ // This test verifies the contract: when a pain flag with session_id exists,
134
+ // any sleep_reflection task created by the worker MUST carry that session_id
135
+ // in its recentPainContext.mostRecent.sessionId field.
136
+ const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-e2e-pain-enqueue-'));
126
137
  const stateDir = path.join(workspaceDir, '.state');
127
- fs.mkdirSync(path.join(stateDir, 'sessions'), { recursive: true });
128
- fs.mkdirSync(path.join(stateDir, 'logs'), { recursive: true });
129
-
130
- mockGetNocturnalSessionSnapshot.mockReturnValue({
131
- sessionId: 'sleep-gateway',
132
- startedAt: '2026-04-10T00:00:00.000Z',
133
- updatedAt: '2026-04-10T00:01:00.000Z',
134
- assistantTurns: [],
135
- userTurns: [],
136
- toolCalls: [],
137
- painEvents: [],
138
- gateBlocks: [],
139
- stats: {
140
- totalAssistantTurns: 1,
141
- totalToolCalls: 1,
142
- totalPainEvents: 0,
143
- totalGateBlocks: 0,
144
- failureCount: 0,
145
- },
146
- });
147
- mockStartWorkflow.mockResolvedValue({ workflowId: 'wf-1', childSessionKey: 'child-1', state: 'active' });
148
- mockGetWorkflowDebugSummary.mockResolvedValue({
149
- state: 'terminal_error',
150
- metadata: {},
151
- recentEvents: [
152
- {
153
- reason: 'Error: Plugin runtime subagent methods are only available during a gateway request.',
154
- payload: {},
155
- },
156
- ],
157
- });
158
-
159
- EvolutionWorkerService.api = {
160
- logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
161
- runtime: {},
162
- } as any;
138
+ fs.mkdirSync(stateDir, { recursive: true });
163
139
 
140
+ // Write a pain flag WITH session_id
164
141
  fs.writeFileSync(
165
- path.join(stateDir, 'evolution_queue.json'),
166
- JSON.stringify([
167
- {
168
- id: 'sleep-gateway',
169
- taskKind: 'sleep_reflection',
170
- priority: 'medium',
171
- score: 50,
172
- source: 'nocturnal',
173
- reason: 'Sleep reflection',
174
- timestamp: '2026-04-10T00:00:00.000Z',
175
- enqueued_at: '2026-04-10T00:00:00.000Z',
176
- status: 'pending',
177
- retryCount: 0,
178
- maxRetries: 1,
179
- recentPainContext: {
180
- mostRecent: { score: 0.5, source: 'pain', reason: 'x', timestamp: '2026-04-10T00:00:00.000Z' },
181
- recentPainCount: 1,
182
- recentMaxPainScore: 0.5,
183
- },
184
- },
185
- ], null, 2),
142
+ path.join(stateDir, '.pain_flag'),
143
+ `source: tool_failure
144
+ score: 70
145
+ reason: Test pain with session
146
+ time: 2026-04-10T00:00:00.000Z
147
+ session_id: pain-session-abc
148
+ `,
186
149
  'utf8'
187
150
  );
188
151
 
152
+ // Verify the worker's readRecentPainContext extracts the session_id correctly
153
+ const wctx = WorkspaceContext.fromHookContext({ workspaceDir, stateDir, logger: console } as any);
154
+ const painContext = readRecentPainContext(wctx);
155
+
156
+ // Contract: session_id must be extracted from the pain flag
157
+ expect(painContext.mostRecent).toBeDefined();
158
+ expect(painContext.mostRecent.sessionId).toBe('pain-session-abc');
159
+ expect(painContext.mostRecent.score).toBe(70);
160
+ expect(painContext.mostRecent.source).toBe('tool_failure');
161
+
162
+ // Now simulate what the worker does: attach this context to a queued task
163
+ const simulatedTask = {
164
+ id: 'simulated-task',
165
+ taskKind: 'sleep_reflection',
166
+ recentPainContext: painContext,
167
+ };
168
+
169
+ // Verify the contract holds end-to-end
170
+ expect(simulatedTask.recentPainContext.mostRecent.sessionId).toBe('pain-session-abc');
171
+ });
172
+
173
+ it('e2e: /pd-reflect command writes to workspace/.state, never to HOME/.state', async () => {
174
+ const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-e2e-command-writes-'));
175
+ const stateDir = path.join(workspaceDir, '.state');
176
+ fs.mkdirSync(stateDir, { recursive: true });
177
+ fs.mkdirSync(path.join(stateDir, 'sessions'), { recursive: true });
178
+
179
+ // Ensure HOME/.state does NOT have the queue file
180
+ const homeState = path.join(os.homedir(), '.state');
181
+ const homeQueue = path.join(homeState, 'evolution_queue.json');
182
+ const homeExistedBefore = fs.existsSync(homeQueue);
183
+
189
184
  try {
190
- EvolutionWorkerService.start({
185
+ // Execute the command with explicit workspaceDir
186
+ const result = await handlePdReflect.handler({
191
187
  workspaceDir,
192
- stateDir,
193
- logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
188
+ channel: 'test',
189
+ isAuthorizedSender: true,
190
+ commandBody: '',
194
191
  config: {},
192
+ api: { logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() } } as any,
195
193
  } as any);
196
194
 
197
- await vi.advanceTimersByTimeAsync(6000);
195
+ // Command should succeed
196
+ expect(result.isError).toBeFalsy();
197
+ expect(result.text).toContain('enqueued');
198
+
199
+ // Queue file should exist in workspace
200
+ const workspaceQueue = path.join(stateDir, 'evolution_queue.json');
201
+ expect(fs.existsSync(workspaceQueue)).toBe(true);
198
202
 
203
+ // Verify the task is in the workspace queue
199
204
  const queue = readQueue(stateDir);
200
- expect(queue[0].status).toBe('failed');
201
- expect(queue[0].resolution).toBe('failed_max_retries');
202
- expect(queue[0].lastError).toContain('gateway request');
205
+ const manualTasks = queue.filter((t: any) => t.id.startsWith('manual_'));
206
+ expect(manualTasks.length).toBe(1);
207
+ expect(manualTasks[0].taskKind).toBe('sleep_reflection');
208
+
209
+ // HOME/.state/evolution_queue.json should NOT have been created/modified by this command
210
+ if (!homeExistedBefore) {
211
+ expect(fs.existsSync(homeQueue)).toBe(false);
212
+ }
203
213
  } finally {
204
- EvolutionWorkerService.stop({ workspaceDir, stateDir, logger: console } as any);
205
214
  safeRmDir(workspaceDir);
206
215
  }
207
216
  });
@@ -82,4 +82,37 @@ describe('NocturnalWorkflowManager runtime hardening', () => {
82
82
 
83
83
  manager.dispose();
84
84
  });
85
+
86
+ it('rejects malformed snapshot ingress before starting the async pipeline', async () => {
87
+ const manager = new NocturnalWorkflowManager({
88
+ workspaceDir,
89
+ stateDir,
90
+ logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() } as any,
91
+ runtimeAdapter: {} as any,
92
+ });
93
+
94
+ const handle = await manager.startWorkflow(nocturnalWorkflowSpec, {
95
+ parentSessionId: 'sleep_reflection:test',
96
+ taskInput: {},
97
+ metadata: {
98
+ snapshot: {
99
+ sessionId: 'session-1',
100
+ stats: {
101
+ totalAssistantTurns: 1,
102
+ totalToolCalls: 1,
103
+ totalPainEvents: 0,
104
+ totalGateBlocks: 0,
105
+ failureCount: 0,
106
+ },
107
+ },
108
+ },
109
+ });
110
+
111
+ const summary = await manager.getWorkflowDebugSummary(handle.workflowId);
112
+ expect(summary?.state).toBe('terminal_error');
113
+ expect(summary?.recentEvents.some((event) => event.eventType === 'nocturnal_failed')).toBe(true);
114
+ expect(mockExecuteNocturnalReflectionAsync).not.toHaveBeenCalled();
115
+
116
+ manager.dispose();
117
+ });
85
118
  });
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ getSubagentRuntimeAvailability,
4
+ isSubagentRuntimeAvailable,
5
+ } from '../../src/utils/subagent-probe.js';
6
+
7
+ describe('subagent-probe', () => {
8
+ it('treats any callable run entrypoint as available', () => {
9
+ const runtime = {
10
+ run() {
11
+ return Promise.resolve({ runId: 'run-1' });
12
+ },
13
+ };
14
+
15
+ expect(getSubagentRuntimeAvailability(runtime)).toEqual({
16
+ available: true,
17
+ reason: 'callable',
18
+ });
19
+ expect(isSubagentRuntimeAvailable(runtime)).toBe(true);
20
+ });
21
+
22
+ it('reports missing runtime and missing run distinctly', () => {
23
+ expect(getSubagentRuntimeAvailability(undefined)).toEqual({
24
+ available: false,
25
+ reason: 'missing_runtime',
26
+ });
27
+ expect(getSubagentRuntimeAvailability({})).toEqual({
28
+ available: false,
29
+ reason: 'missing_run',
30
+ });
31
+ });
32
+ });