principles-disciple 1.94.0 → 1.95.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.
@@ -2,7 +2,7 @@
2
2
  "id": "principles-disciple",
3
3
  "name": "Principles Disciple",
4
4
  "description": "Evolutionary programming agent framework with strategic guardrails and reflection loops.",
5
- "version": "1.94.0",
5
+ "version": "1.95.0",
6
6
  "activation": {
7
7
  "onCapabilities": [
8
8
  "hook"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "principles-disciple",
3
- "version": "1.94.0",
3
+ "version": "1.95.0",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/bundle.js",
@@ -349,10 +349,31 @@ export async function handlePainReportCommand(ctx: PluginCommandContext): Promis
349
349
  };
350
350
  }
351
351
 
352
+ if (result.status === 'retried') {
353
+ const errorInfo = result.failureCategory
354
+ ? (isZh ? `\n⚠️ **错误类别**: ${result.failureCategory}` : `\n⚠️ **Error category**: ${result.failureCategory}`)
355
+ : '';
356
+ const messageInfo = result.message
357
+ ? (isZh ? `\n📝 **详情**: ${result.message}` : `\n📝 **Detail**: ${result.message}`)
358
+ : '';
359
+ return {
360
+ text: isZh
361
+ ? `✅ Pain 已记录,诊断任务已进入重试\n\n📋 **Pain ID**: ${result.painId}\n🔧 **Task ID**: ${result.taskId}${errorInfo}${messageInfo}\n\n诊断任务将在后台自动重试。使用 \`/pd-status\` 查看任务状态。`
362
+ : `✅ Pain recorded, diagnosis task entered retry\n\n📋 **Pain ID**: ${result.painId}\n🔧 **Task ID**: ${result.taskId}${errorInfo}${messageInfo}\n\nThe diagnosis task will retry automatically in the background. Use \`/pd-status\` to check task status.`,
363
+ };
364
+ }
365
+
366
+ // status === 'failed' | 'skipped' | 'degraded' — pain was NOT accepted
367
+ const reasonInfo = result.failureCategory
368
+ ? (isZh ? `\n⚠️ **原因**: ${result.failureCategory}` : `\n⚠️ **Reason**: ${result.failureCategory}`)
369
+ : '';
370
+ const messageInfo = result.message
371
+ ? (isZh ? `\n📝 **详情**: ${result.message}` : `\n📝 **Detail**: ${result.message}`)
372
+ : '';
352
373
  return {
353
374
  text: isZh
354
- ? `⚠️ Pain 记录未成功 (status: ${result.status})。请检查系统日志或使用 \`/pd-status\` 查看状态。`
355
- : `⚠️ Pain recording not accepted (status: ${result.status}). Check system logs or use \`/pd-status\` for status.`,
375
+ ? `❌ Pain 记录未成功 (status: ${result.status})${reasonInfo}${messageInfo}\n\n请检查系统日志或使用 \`/pd-status\` 查看状态。`
376
+ : `❌ Pain recording not accepted (status: ${result.status})${reasonInfo}${messageInfo}\n\nCheck system logs or use \`/pd-status\` for status.`,
356
377
  };
357
378
  } catch (err) {
358
379
  return {
@@ -1,10 +1,19 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { handlePainCommand } from '../../src/commands/pain.js';
2
+ import { handlePainCommand, handlePainReportCommand } from '../../src/commands/pain.js';
3
3
  import * as sessionTracker from '../../src/core/session-tracker.js';
4
4
  import { WorkspaceContext } from '../../src/core/workspace-context.js';
5
5
 
6
6
  vi.mock('../../src/core/session-tracker.js');
7
7
  vi.mock('../../src/core/workspace-context.js');
8
+ vi.mock('../../src/core/pd-config-loader.js', () => ({
9
+ loadPdConfigForPlugin: vi.fn().mockReturnValue({ ok: true, effective: {}, source: 'defaults', warnings: [], errors: [] }),
10
+ }));
11
+ vi.mock('@principles/core/runtime-v2', () => ({
12
+ PainToPrincipleService: vi.fn(),
13
+ PrincipleTreeLedgerAdapter: vi.fn(function(this: any) { this.stateDir = ''; }),
14
+ }));
15
+
16
+ import { PainToPrincipleService } from '@principles/core/runtime-v2';
8
17
 
9
18
  describe('Pain Command', () => {
10
19
  const workspaceDir = '/mock/workspace';
@@ -106,3 +115,173 @@ describe('Pain Command', () => {
106
115
  expect(result.text).toContain('approved samples');
107
116
  });
108
117
  });
118
+
119
+ describe('Pain Report Command (/pd-pain)', () => {
120
+ const workspaceDir = '/mock/workspace';
121
+ const sessionId = 's1';
122
+
123
+ const mockEvolutionReducer = { emitSync: vi.fn() };
124
+ const mockWctx = {
125
+ workspaceDir,
126
+ stateDir: '/mock/workspace/.state',
127
+ evolutionReducer: mockEvolutionReducer,
128
+ };
129
+
130
+ beforeEach(() => {
131
+ vi.clearAllMocks();
132
+ vi.mocked(WorkspaceContext.fromHookContext).mockReturnValue(mockWctx as any);
133
+ });
134
+
135
+ async function runPainReport(args: string, lang = 'en') {
136
+ return handlePainReportCommand({
137
+ args,
138
+ config: { workspaceDir, language: lang },
139
+ sessionId,
140
+ } as any);
141
+ }
142
+
143
+ it('rejects empty args', async () => {
144
+ const result = await runPainReport('');
145
+ expect(result.text).toContain('Please provide a pain reason');
146
+ });
147
+
148
+ it('rejects missing session ID', async () => {
149
+ const result = await handlePainReportCommand({
150
+ args: 'something broke',
151
+ config: { workspaceDir, language: 'en' },
152
+ } as any);
153
+ expect(result.text).toContain('Session ID not available');
154
+ });
155
+
156
+ it('reports success when recordPain returns succeeded', async () => {
157
+ const mockRecordPain = vi.fn().mockResolvedValue({
158
+ status: 'succeeded',
159
+ painId: 'manual_123_abc',
160
+ taskId: 'diagnosis_manual_123_abc',
161
+ candidateIds: [],
162
+ ledgerEntryIds: [],
163
+ observabilityWarnings: [],
164
+ latencyMs: 100,
165
+ });
166
+ vi.mocked(PainToPrincipleService).mockImplementation(function(this: any) { this.recordPain = mockRecordPain; } as any);
167
+
168
+ const result = await runPainReport('something broke');
169
+ expect(result.text).toContain('Pain recorded');
170
+ expect(result.text).toContain('manual_');
171
+ expect(result.text).not.toContain('not accepted');
172
+ });
173
+
174
+ it('reports retried as pain recorded with retry info, NOT as "not accepted"', async () => {
175
+ const mockRecordPain = vi.fn().mockResolvedValue({
176
+ status: 'retried',
177
+ painId: 'manual_456_def',
178
+ taskId: 'diagnosis_manual_456_def',
179
+ failureCategory: 'output_invalid',
180
+ message: 'Diagnostician output failed validation',
181
+ candidateIds: [],
182
+ ledgerEntryIds: [],
183
+ observabilityWarnings: [],
184
+ latencyMs: 200,
185
+ });
186
+ vi.mocked(PainToPrincipleService).mockImplementation(function(this: any) { this.recordPain = mockRecordPain; } as any);
187
+
188
+ const result = await runPainReport('something broke');
189
+ expect(result.text).toContain('Pain recorded');
190
+ expect(result.text).toContain('retry');
191
+ expect(result.text).toContain('diagnosis_manual_456_def');
192
+ expect(result.text).toContain('output_invalid');
193
+ expect(result.text).toContain('/pd-status');
194
+ // Must NOT say "not accepted" or "failed"
195
+ expect(result.text).not.toContain('not accepted');
196
+ expect(result.text).not.toContain('未成功');
197
+ });
198
+
199
+ it('reports retried in Chinese correctly', async () => {
200
+ const mockRecordPain = vi.fn().mockResolvedValue({
201
+ status: 'retried',
202
+ painId: 'manual_789_xyz',
203
+ taskId: 'diagnosis_manual_789_xyz',
204
+ failureCategory: 'output_invalid',
205
+ candidateIds: [],
206
+ ledgerEntryIds: [],
207
+ observabilityWarnings: [],
208
+ latencyMs: 200,
209
+ });
210
+ vi.mocked(PainToPrincipleService).mockImplementation(function(this: any) { this.recordPain = mockRecordPain; } as any);
211
+
212
+ const result = await runPainReport('something broke', 'zh');
213
+ expect(result.text).toContain('Pain 已记录');
214
+ expect(result.text).toContain('重试');
215
+ expect(result.text).not.toContain('未成功');
216
+ expect(result.text).not.toContain('not accepted');
217
+ });
218
+
219
+ it('reports retried without failureCategory or message', async () => {
220
+ const mockRecordPain = vi.fn().mockResolvedValue({
221
+ status: 'retried',
222
+ painId: 'manual_000_nocat',
223
+ taskId: 'diagnosis_manual_000_nocat',
224
+ candidateIds: [],
225
+ ledgerEntryIds: [],
226
+ observabilityWarnings: [],
227
+ latencyMs: 150,
228
+ });
229
+ vi.mocked(PainToPrincipleService).mockImplementation(function(this: any) { this.recordPain = mockRecordPain; } as any);
230
+
231
+ const result = await runPainReport('something broke');
232
+ expect(result.text).toContain('Pain recorded');
233
+ expect(result.text).toContain('retry');
234
+ expect(result.text).toContain('diagnosis_manual_000_nocat');
235
+ // No error category or detail lines when absent
236
+ expect(result.text).not.toContain('Error category');
237
+ expect(result.text).not.toContain('Detail');
238
+ });
239
+
240
+ it('reports failed as "not accepted" with reason', async () => {
241
+ const mockRecordPain = vi.fn().mockResolvedValue({
242
+ status: 'failed',
243
+ painId: 'manual_fail_1',
244
+ taskId: 'diagnosis_manual_fail_1',
245
+ failureCategory: 'runtime_unavailable',
246
+ message: 'No runner available',
247
+ candidateIds: [],
248
+ ledgerEntryIds: [],
249
+ observabilityWarnings: [],
250
+ latencyMs: 50,
251
+ });
252
+ vi.mocked(PainToPrincipleService).mockImplementation(function(this: any) { this.recordPain = mockRecordPain; } as any);
253
+
254
+ const result = await runPainReport('something broke');
255
+ expect(result.text).toContain('not accepted');
256
+ expect(result.text).toContain('failed');
257
+ expect(result.text).toContain('runtime_unavailable');
258
+ expect(result.text).toContain('No runner available');
259
+ });
260
+
261
+ it('reports degraded as "not accepted"', async () => {
262
+ const mockRecordPain = vi.fn().mockResolvedValue({
263
+ status: 'degraded',
264
+ painId: 'manual_deg_1',
265
+ taskId: 'diagnosis_manual_deg_1',
266
+ candidateIds: [],
267
+ ledgerEntryIds: [],
268
+ observabilityWarnings: [],
269
+ latencyMs: 30,
270
+ });
271
+ vi.mocked(PainToPrincipleService).mockImplementation(function(this: any) { this.recordPain = mockRecordPain; } as any);
272
+
273
+ const result = await runPainReport('something broke');
274
+ expect(result.text).toContain('not accepted');
275
+ expect(result.text).toContain('degraded');
276
+ });
277
+
278
+ it('reports error on exception', async () => {
279
+ vi.mocked(PainToPrincipleService).mockImplementation(function(this: any) {
280
+ throw new Error('DB connection failed');
281
+ });
282
+
283
+ const result = await runPainReport('something broke');
284
+ expect(result.text).toContain('Failed to record pain');
285
+ expect(result.text).toContain('DB connection failed');
286
+ });
287
+ });