principles-disciple 1.76.0 → 1.78.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.
@@ -188,10 +188,13 @@ function makeCtx(overrides: {
188
188
  } as unknown as Parameters<typeof import('../../src/hooks/prompt.js').handleBeforePromptBuild>[1];
189
189
  }
190
190
 
191
- // ─── Tests: Attitude Directive (GFI thresholds) ───────────────────────────
191
+ // ─── Tests: Attitude Directive removed (PRI-291 MVP diet) ───────────────
192
+ // Attitude/personality prompt text was removed per PRI-291.
193
+ // GFI scoring (trackFriction) and empathy pain emission remain active.
194
+ // These tests verify that attitude text no longer appears in prompts.
192
195
 
193
- describe('Attitude directive — GFI thresholds', () => {
194
- // Ensure appendParts is non-empty so attitudeDirective is included in appendSystemContext.
196
+ describe('Attitude/personality directive — removed from prompt (PRI-291)', () => {
197
+ // Ensure appendParts is non-empty so we can verify absence
195
198
  beforeEach(async () => {
196
199
  const { WorkspaceContext } = await import('../../src/core/workspace-context.js');
197
200
  (WorkspaceContext.fromHookContext as ReturnType<typeof vi.fn>).mockReturnValueOnce({
@@ -209,59 +212,42 @@ describe('Attitude directive — GFI thresholds', () => {
209
212
  });
210
213
  });
211
214
 
212
- it('GFI >= 70 injects HUMBLE_RECOVERY mode', async () => {
215
+ it('GFI >= 70 does NOT inject HUMBLE_RECOVERY mode', async () => {
213
216
  setSessionGfi(75);
214
217
  const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
215
218
  const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx({ sessionGfi: 75 }));
216
219
 
217
- expect(result?.appendSystemContext).toContain('HUMBLE_RECOVERY');
218
- expect(result?.appendSystemContext).toContain('GFI: 75');
220
+ const combined = (result?.prependSystemContext ?? '') + (result?.appendSystemContext ?? '');
221
+ expect(combined).not.toContain('HUMBLE_RECOVERY');
219
222
  });
220
223
 
221
- it('GFI >= 70 injects HUMBLE_RECOVERY mode at boundary (70 exactly)', async () => {
222
- setSessionGfi(70);
223
- const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
224
- const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx({ sessionGfi: 70 }));
225
-
226
- expect(result?.appendSystemContext).toContain('HUMBLE_RECOVERY');
227
- });
228
-
229
- it('GFI >= 40 and < 70 injects CONCILIATORY mode', async () => {
224
+ it('GFI >= 40 does NOT inject CONCILIATORY mode', async () => {
230
225
  setSessionGfi(50);
231
226
  const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
232
227
  const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx({ sessionGfi: 50 }));
233
228
 
234
- expect(result?.appendSystemContext).toContain('CONCILIATORY');
235
- expect(result?.appendSystemContext).toContain('GFI: 50');
229
+ const combined = (result?.prependSystemContext ?? '') + (result?.appendSystemContext ?? '');
230
+ expect(combined).not.toContain('CONCILIATORY');
236
231
  });
237
232
 
238
- it('GFI >= 40 and < 70 injects CONCILIATORY mode at boundary (40 exactly)', async () => {
239
- setSessionGfi(40);
240
- const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
241
- const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx({ sessionGfi: 40 }));
242
-
243
- expect(result?.appendSystemContext).toContain('CONCILIATORY');
244
- });
245
-
246
- it('GFI < 40 injects EFFICIENT mode', async () => {
233
+ it('GFI < 40 does NOT inject EFFICIENT mode', async () => {
247
234
  setSessionGfi(10);
248
235
  const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
249
236
  const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx({ sessionGfi: 10 }));
250
237
 
251
- expect(result?.appendSystemContext).toContain('EFFICIENT');
252
- expect(result?.appendSystemContext).toContain('GFI: 10');
238
+ const combined = (result?.prependSystemContext ?? '') + (result?.appendSystemContext ?? '');
239
+ expect(combined).not.toContain('EFFICIENT');
253
240
  });
254
241
 
255
- it('GFI = 0 (no session) falls back to 0 and injects EFFICIENT', async () => {
256
- setSessionGfi(0);
257
- const { getSession } = await import('../../src/core/session-tracker.js');
258
- // Override to return undefined (no session found)
259
- (getSession as ReturnType<typeof vi.fn>).mockReturnValueOnce(undefined);
260
-
242
+ it('no "Spicy Evolver" persona text appears in prompt', async () => {
243
+ setSessionGfi(20);
261
244
  const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
262
- const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx({ sessionId: 'no-such-session' }));
245
+ const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx({ sessionGfi: 20 }));
263
246
 
264
- expect(result?.appendSystemContext).toContain('EFFICIENT');
247
+ const combined = (result?.prependSystemContext ?? '') + (result?.appendSystemContext ?? '');
248
+ expect(combined).not.toContain('Spicy Evolver');
249
+ expect(combined).not.toContain('despise entropy');
250
+ expect(combined).not.toContain('evolve through pain');
265
251
  });
266
252
  });
267
253
 
@@ -0,0 +1,343 @@
1
+ /**
2
+ * Prompt diet tests — PRI-291
3
+ *
4
+ * Verifies that the default prompt contains only MVP-required sections
5
+ * and does NOT contain sections removed/default-disabled by the MVP diet.
6
+ *
7
+ * Acceptance criteria covered:
8
+ * 1. Default prompt does NOT contain Thinking OS text
9
+ * 2. Default prompt does NOT contain <routing_guidance>
10
+ * 3. Default prompt does NOT contain GFI attitude/personality directive text
11
+ * 4. Runtime V2 activation still injects validated directives into prependSystemContext
12
+ * 5. runtime_v2_prompt_activations_injected event still emits
13
+ * 6. GFI scoring/empathy evidence path still records friction and can emit pain
14
+ */
15
+
16
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
17
+
18
+ // ─── Mock dependencies ───────────────────────────────────────────────────────
19
+
20
+ beforeEach(() => {
21
+ vi.clearAllMocks();
22
+ process.env.PD_LEGACY_PROMPT_DIAGNOSTICIAN_ENABLED = 'true';
23
+ });
24
+
25
+ afterEach(() => {
26
+ process.env.PD_LEGACY_PROMPT_DIAGNOSTICIAN_ENABLED = '';
27
+ });
28
+
29
+ const mockGetPendingDiagnosticianTasks = vi.fn<(stateDir: string) => unknown[]>();
30
+
31
+ vi.mock('../../src/core/diagnostician-task-store.js', async () => ({
32
+ getPendingDiagnosticianTasks: (...args: unknown[]) =>
33
+ mockGetPendingDiagnosticianTasks(...args),
34
+ }));
35
+
36
+ vi.mock('../../src/core/event-log.js', () => ({
37
+ EventLogService: {
38
+ get: vi.fn().mockReturnValue({
39
+ recordHeartbeatDiagnosis: vi.fn(),
40
+ recordRuntimeV2ActivationsInjected: vi.fn(),
41
+ }),
42
+ },
43
+ }));
44
+
45
+ vi.mock('../../src/core/workspace-context.js', () => {
46
+ const mockWctx = {
47
+ workspaceDir: '/fake/workspace',
48
+ stateDir: '/fake/state',
49
+ resolve: (key: string) => `/fake/${key}`,
50
+ trajectory: { recordSession: vi.fn(), recordUserTurn: vi.fn() },
51
+ config: { get: vi.fn() },
52
+ eventLog: {
53
+ recordRuntimeV2ActivationsInjected: vi.fn(),
54
+ },
55
+ evolutionReducer: {
56
+ getActivePrinciples: vi.fn().mockReturnValue([]),
57
+ getProbationPrinciples: vi.fn().mockReturnValue([]),
58
+ },
59
+ };
60
+ return {
61
+ WorkspaceContext: {
62
+ fromHookContext: vi.fn().mockReturnValue(mockWctx),
63
+ fromHookContextExplicit: vi.fn().mockReturnValue(mockWctx),
64
+ },
65
+ };
66
+ });
67
+
68
+ let sessionGfiValue = 20;
69
+ vi.mock('../../src/core/session-tracker.js', () => ({
70
+ getSession: vi.fn().mockImplementation(() => ({ currentGfi: sessionGfiValue })),
71
+ resetFriction: vi.fn(),
72
+ trackFriction: vi.fn(),
73
+ setInjectedProbationIds: vi.fn(),
74
+ clearInjectedProbationIds: vi.fn(),
75
+ decayGfi: vi.fn(),
76
+ getGfiDecayElapsed: vi.fn().mockReturnValue(0),
77
+ }));
78
+
79
+ vi.mock('../../src/core/path-resolver.js', () => ({
80
+ PathResolver: { getExtensionRoot: vi.fn().mockReturnValue('/fake/extension') },
81
+ }));
82
+
83
+ vi.mock('../../src/core/principle-injection.js', () => ({
84
+ selectPrinciplesForInjection: vi.fn().mockReturnValue({
85
+ selected: [],
86
+ wasTruncated: false,
87
+ breakdown: { p0: 0, p1: 0, p2: 0 },
88
+ totalChars: 0,
89
+ }),
90
+ DEFAULT_PRINCIPLE_BUDGET: 3000,
91
+ }));
92
+
93
+ vi.mock('../../src/core/empathy-keyword-matcher.js', () => ({
94
+ matchEmpathyKeywords: vi.fn().mockReturnValue({ score: 0, matched: null, severity: 'none', matchedTerms: [] }),
95
+ loadKeywordStore: vi.fn().mockReturnValue({ terms: {}, stats: { totalHits: 0 } }),
96
+ saveKeywordStore: vi.fn(),
97
+ shouldTriggerOptimization: vi.fn().mockReturnValue(false),
98
+ getKeywordStoreSummary: vi.fn().mockReturnValue({ totalTerms: 0, highFalsePositiveTerms: [] }),
99
+ }));
100
+
101
+ vi.mock('../../src/core/empathy-types.js', () => ({
102
+ severityToPenalty: vi.fn().mockReturnValue(5),
103
+ DEFAULT_EMPATHY_KEYWORD_CONFIG: {},
104
+ }));
105
+
106
+ vi.mock('../../src/core/correction-cue-learner.js', () => ({
107
+ CorrectionCueLearner: {
108
+ get: vi.fn().mockReturnValue({
109
+ match: vi.fn().mockReturnValue({ matched: null, matchedTerms: [], confidence: 0 }),
110
+ recordHits: vi.fn(),
111
+ recordTruePositive: vi.fn(),
112
+ flush: vi.fn(),
113
+ }),
114
+ },
115
+ }));
116
+
117
+ vi.mock('../../src/core/focus-history.js', () => ({
118
+ extractSummary: vi.fn().mockReturnValue(''),
119
+ getHistoryVersions: vi.fn().mockResolvedValue([]),
120
+ parseWorkingMemorySection: vi.fn().mockReturnValue(null),
121
+ workingMemoryToInjection: vi.fn().mockReturnValue(''),
122
+ autoCompressFocus: vi.fn().mockReturnValue({ compressed: false, reason: 'not_needed' }),
123
+ safeReadCurrentFocus: vi.fn().mockReturnValue({ content: '', recovered: false, validationErrors: [] }),
124
+ }));
125
+
126
+ vi.mock('../../src/service/subagent-workflow/index.js', () => ({
127
+ EmpathyObserverWorkflowManager: vi.fn(),
128
+ empathyObserverWorkflowSpec: {},
129
+ isExpectedSubagentError: vi.fn().mockReturnValue(false),
130
+ }));
131
+
132
+ vi.mock('../../src/utils/subagent-probe.js', () => ({
133
+ isSubagentRuntimeAvailable: vi.fn().mockReturnValue(false),
134
+ }));
135
+
136
+ vi.mock('../../src/core/local-worker-routing.js', () => ({
137
+ classifyTask: vi.fn().mockReturnValue({
138
+ decision: 'stay_main',
139
+ classification: 'unknown',
140
+ reason: 'mocked',
141
+ blockers: [],
142
+ }),
143
+ }));
144
+
145
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
146
+
147
+ function makeMinimalEvent(overrides: {
148
+ trigger?: string;
149
+ sessionId?: string;
150
+ } = {}) {
151
+ const { trigger = 'user', sessionId = 'test-session-diet' } = overrides;
152
+ return {
153
+ prompt: 'hello world',
154
+ messages: [],
155
+ trigger,
156
+ sessionId,
157
+ } as unknown as Parameters<typeof import('../../src/hooks/prompt.js').handleBeforePromptBuild>[0];
158
+ }
159
+
160
+ function makeCtx(overrides: {
161
+ sessionGfi?: number;
162
+ trigger?: string;
163
+ sessionId?: string;
164
+ } = {}) {
165
+ const { sessionGfi = 20, trigger = 'user', sessionId = 'test-session-diet' } = overrides;
166
+ sessionGfiValue = sessionGfi;
167
+ return {
168
+ workspaceDir: '/fake/workspace',
169
+ trigger,
170
+ sessionId,
171
+ api: {
172
+ logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
173
+ runtime: {},
174
+ config: {},
175
+ },
176
+ } as unknown as Parameters<typeof import('../../src/hooks/prompt.js').handleBeforePromptBuild>[1];
177
+ }
178
+
179
+ async function getPromptOutput(overrides: { sessionGfi?: number } = {}) {
180
+ const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
181
+ const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx(overrides));
182
+ return {
183
+ prepend: result?.prependSystemContext ?? '',
184
+ append: result?.appendSystemContext ?? '',
185
+ context: result?.prependContext ?? '',
186
+ };
187
+ }
188
+
189
+ // ─── Tests: MVP Diet — sections that must NOT appear by default ────────────
190
+
191
+ describe('PRI-291 Prompt Diet: default prompt excludes non-MVP sections', () => {
192
+ it('default prompt does NOT contain Thinking OS text', async () => {
193
+ const { append } = await getPromptOutput();
194
+ expect(append).not.toContain('<thinking_os>');
195
+ });
196
+
197
+ it('default prompt does NOT contain <routing_guidance>', async () => {
198
+ const { append } = await getPromptOutput();
199
+ expect(append).not.toContain('<routing_guidance>');
200
+ expect(append).not.toContain('DELEGATION SUGGESTION');
201
+ expect(append).not.toContain('ROUTING GUIDANCE');
202
+ });
203
+
204
+ it('default prompt does NOT contain GFI attitude/personality text', async () => {
205
+ const { prepend, append } = await getPromptOutput();
206
+ const combined = prepend + append;
207
+ expect(combined).not.toContain('HUMBLE_RECOVERY');
208
+ expect(combined).not.toContain('CONCILIATORY');
209
+ expect(combined).not.toContain('EFFICIENT');
210
+ expect(combined).not.toContain('Spicy Evolver');
211
+ expect(combined).not.toContain('despise entropy');
212
+ expect(combined).not.toContain('evolve through pain');
213
+ });
214
+
215
+ it('default prompt does NOT contain <project_context> (default is off)', async () => {
216
+ const { append } = await getPromptOutput();
217
+ // project_context tag should not appear as a content block.
218
+ // Note: the EXECUTION RULES section may list it as a priority description,
219
+ // but the actual <project_context>...</project_context> content block must be absent.
220
+ const hasProjectContextBlock = append.includes('<project_context>\n');
221
+ expect(hasProjectContextBlock).toBe(false);
222
+ });
223
+
224
+ it('EXECUTION RULES does not list removed sections', async () => {
225
+ const { append } = await getPromptOutput();
226
+ // Only present when appendParts is non-empty (principles exist)
227
+ // But even when present, removed sections should not be listed
228
+ if (append.includes('EXECUTION RULES')) {
229
+ expect(append).not.toContain('<thinking_os>');
230
+ expect(append).not.toContain('<routing_guidance>');
231
+ expect(append).not.toContain('<reflection_log>');
232
+ }
233
+ });
234
+ });
235
+
236
+ // ─── Tests: MVP Diet — sections that MUST still appear ─────────────────────
237
+
238
+ describe('PRI-291 Prompt Diet: MVP sections preserved', () => {
239
+ it('AGENT IDENTITY is still injected in prependSystemContext', async () => {
240
+ const { prepend } = await getPromptOutput();
241
+ expect(prepend).toContain('AGENT IDENTITY');
242
+ });
243
+
244
+ it('evolution principles can still be injected when active', async () => {
245
+ const { WorkspaceContext } = await import('../../src/core/workspace-context.js');
246
+ (WorkspaceContext.fromHookContext as ReturnType<typeof vi.fn>).mockReturnValueOnce({
247
+ workspaceDir: '/fake/workspace',
248
+ stateDir: '/fake/state',
249
+ resolve: (key: string) => `/fake/${key}`,
250
+ trajectory: { recordSession: vi.fn(), recordUserTurn: vi.fn() },
251
+ config: { get: vi.fn() },
252
+ eventLog: { recordRuntimeV2ActivationsInjected: vi.fn() },
253
+ evolutionReducer: {
254
+ getActivePrinciples: vi.fn().mockReturnValue([
255
+ { id: 'P1', text: 'Evolution principle still works' },
256
+ ]),
257
+ getProbationPrinciples: vi.fn().mockReturnValue([]),
258
+ },
259
+ });
260
+
261
+ const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
262
+ const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx());
263
+
264
+ expect(result?.appendSystemContext).toContain('<evolution_principles>');
265
+ expect(result?.appendSystemContext).toContain('Evolution principle still works');
266
+ });
267
+
268
+ it('core principles can still be injected', async () => {
269
+ const { WorkspaceContext } = await import('../../src/core/workspace-context.js');
270
+ (WorkspaceContext.fromHookContext as ReturnType<typeof vi.fn>).mockReturnValueOnce({
271
+ workspaceDir: '/fake/workspace',
272
+ stateDir: '/fake/state',
273
+ resolve: (key: string) => `/fake/${key}`,
274
+ trajectory: { recordSession: vi.fn(), recordUserTurn: vi.fn() },
275
+ config: { get: vi.fn() },
276
+ eventLog: { recordRuntimeV2ActivationsInjected: vi.fn() },
277
+ evolutionReducer: {
278
+ getActivePrinciples: vi.fn().mockReturnValue([
279
+ { id: 'CP1', text: 'Core principle preserved' },
280
+ ]),
281
+ getProbationPrinciples: vi.fn().mockReturnValue([]),
282
+ },
283
+ });
284
+
285
+ const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
286
+ const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx());
287
+
288
+ expect(result?.appendSystemContext).toContain('<core_principles>');
289
+ });
290
+
291
+ it('GFI scoring still runs — trackFriction called on empathy match', async () => {
292
+ const { matchEmpathyKeywords } = await import('../../src/core/empathy-keyword-matcher.js');
293
+ (matchEmpathyKeywords as ReturnType<typeof vi.fn>).mockReturnValueOnce({
294
+ score: 0.8,
295
+ matched: true,
296
+ severity: 'moderate',
297
+ matchedTerms: ['frustrated'],
298
+ });
299
+
300
+ const { WorkspaceContext } = await import('../../src/core/workspace-context.js');
301
+ (WorkspaceContext.fromHookContext as ReturnType<typeof vi.fn>).mockReturnValueOnce({
302
+ workspaceDir: '/fake/workspace',
303
+ stateDir: '/fake/state',
304
+ resolve: (key: string) => `/fake/${key}`,
305
+ trajectory: { recordSession: vi.fn(), recordUserTurn: vi.fn(), recordPainEvent: vi.fn() },
306
+ config: { get: vi.fn().mockImplementation((k: string) => {
307
+ if (k === 'thresholds.pain_trigger') return 40;
308
+ if (k === 'severity_thresholds.high') return 70;
309
+ if (k === 'language') return 'en';
310
+ return undefined;
311
+ }) },
312
+ eventLog: { recordPainSignal: vi.fn() },
313
+ evolutionReducer: {
314
+ getActivePrinciples: vi.fn().mockReturnValue([]),
315
+ getProbationPrinciples: vi.fn().mockReturnValue([]),
316
+ },
317
+ });
318
+
319
+ const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
320
+ const event = {
321
+ prompt: 'I am frustrated with this result',
322
+ messages: [{ role: 'user', content: 'I am frustrated' }],
323
+ trigger: 'user',
324
+ sessionId: 'gfi-test-session',
325
+ } as unknown as Parameters<typeof import('../../src/hooks/prompt.js').handleBeforePromptBuild>[0];
326
+
327
+ await handleBeforePromptBuild(event, makeCtx({ sessionGfi: 80 }));
328
+
329
+ const { trackFriction } = await import('../../src/core/session-tracker.js');
330
+ expect(trackFriction).toHaveBeenCalled();
331
+ });
332
+
333
+ it('size guard still works — total injection under 9000', async () => {
334
+ const { handleBeforePromptBuild } = await import('../../src/hooks/prompt.js');
335
+ const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx());
336
+
337
+ const total =
338
+ (result?.prependSystemContext?.length ?? 0) +
339
+ (result?.prependContext?.length ?? 0) +
340
+ (result?.appendSystemContext?.length ?? 0);
341
+ expect(total).toBeLessThanOrEqual(9000);
342
+ });
343
+ });