principles-disciple 1.77.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.
@@ -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.77.0",
5
+ "version": "1.78.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.77.0",
3
+ "version": "1.78.0",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/bundle.js",
@@ -226,7 +226,7 @@ function applyPreset(
226
226
  break;
227
227
  case 'standard':
228
228
  config = {
229
- thinkingOs: true,
229
+ thinkingOs: false,
230
230
  projectFocus: 'off',
231
231
  evolutionContext: { ...defaultContextConfig.evolutionContext }
232
232
  };
@@ -272,8 +272,8 @@ function showHelp(isZh: boolean): string {
272
272
 
273
273
  **预设模式**:
274
274
  \`/pd-context minimal\` - 最小模式(仅核心原则)
275
- \`/pd-context standard\` - 标准模式(原则+思维模型)
276
- \`/pd-context full\` - 完整模式(全部开启)
275
+ \`/pd-context standard\` - 标准模式(原则,不含思维模型)
276
+ \`/pd-context full\` - 完整模式(原则+思维模型+项目上下文)
277
277
 
278
278
  **注意**: 核心原则始终注入,不可关闭。
279
279
  `.trim();
@@ -291,8 +291,8 @@ function showHelp(isZh: boolean): string {
291
291
 
292
292
  **Presets**:
293
293
  \`/pd-context minimal\` - Minimal mode (core principles only)
294
- \`/pd-context standard\` - Standard mode (principles + thinking)
295
- \`/pd-context full\` - Full mode (all enabled)
294
+ \`/pd-context standard\` - Standard mode (principles, no Thinking OS)
295
+ \`/pd-context full\` - Full mode (principles + Thinking OS + project context)
296
296
 
297
297
  **Note**: Core Principles are always injected and cannot be disabled.
298
298
  `.trim();
@@ -7,7 +7,8 @@ import { clearInjectedProbationIds, getSession, resetFriction, setInjectedProbat
7
7
  import { WorkspaceContext } from '../core/workspace-context.js';
8
8
  import type { ContextInjectionConfig} from '../types.js';
9
9
  import { defaultContextConfig } from '../types.js';
10
- import { classifyTask, type RoutingInput } from '../core/local-worker-routing.js';
10
+ // local-worker-routing: removed from prompt injection per PRI-291 (MVP-Quiet)
11
+ // classifyTask is still available for non-prompt consumers
11
12
  import { extractSummary, getHistoryVersions, parseWorkingMemorySection, workingMemoryToInjection, autoCompressFocus, safeReadCurrentFocus } from '../core/focus-history.js';
12
13
  import { PathResolver } from '../core/path-resolver.js';
13
14
  import { selectPrinciplesForInjection, DEFAULT_PRINCIPLE_BUDGET } from '../core/principle-injection.js';
@@ -25,7 +26,6 @@ import { evaluatePainDiagnosticGate } from '../core/pain-diagnostic-gate.js';
25
26
  import { emitPainDetectedEvent, buildTrajectoryEvidence } from './pain.js';
26
27
  import { CorrectionCueLearner } from '../core/correction-cue-learner.js';
27
28
  import {
28
- buildAttitudeDirective,
29
29
  detectCorrectionCue as coreDetectCorrectionCue,
30
30
  extractMessageContent,
31
31
  isMinimalTrigger,
@@ -703,10 +703,9 @@ ${heartbeatChecklist}
703
703
 
704
704
  }
705
705
 
706
- // ──── 6. Dynamic Attitude Matrix (based on GFI) ────
707
-
708
- const currentGfi = session?.currentGfi || 0;
709
- const attitudeDirective = buildAttitudeDirective(currentGfi);
706
+ // ──── 6. GFI score (for empathy/evidence path only — NOT for attitude/personality prompt)
707
+ // Attitude/personality prompt injection removed per PRI-291 (MVP diet).
708
+ // GFI scoring, trackFriction, and empathy pain emission remain active.
710
709
 
711
710
  // ──── 7. appendSystemContext: Principles + Thinking OS + reflection_log + project_context ────
712
711
  // NOTE: Principles is ALWAYS injected (not configurable)
@@ -1005,109 +1004,9 @@ ${empathySilenceConstraint}
1005
1004
  prependSystemContext += directiveLines.join('\n');
1006
1005
  }
1007
1006
 
1008
- // Routing Guidance (section 5 injected between evolution principles and core principles)
1009
- // Inject delegation guidance when task is bounded + deployment allowed + not high-entropy.
1010
- // This is a non-authoritative suggestion the main agent decides whether to follow.
1011
- // Shadow evidence comes from real runtime hooks (subagent_spawning/subagent_ended).
1012
- if (!isMinimalMode && sessionId) {
1013
- try {
1014
- // Use the already extracted and cleaned user message
1015
- const latestUserText = latestUserMessage || '';
1016
-
1017
- if (latestUserText && latestUserText.trim().length > 0) {
1018
- // Infer requestedTools and requestedFiles from message content
1019
- const toolPatterns: { pattern: RegExp; tool: string }[] = [
1020
- { pattern: /\b(edit|replace|write|modify|update|fix|patch|add|remove|delete|insert)\b/gi, tool: 'edit' },
1021
- { pattern: /\b(read|cat|view|show|get|find|search|grep|look|inspect|examine|list|head|tail|diff)\b/gi, tool: 'read' },
1022
- { pattern: /\b(run|execute|exec|bash|shell|command)\b/gi, tool: 'bash' },
1023
- ];
1024
- const filePattern = /\b([a-zA-Z]:\\?[^\s,]+\.[a-z]{2,10}|[./][^\s,]+\.[a-z]{2,10})\b/gi;
1025
- const toolMatches = toolPatterns.flatMap(({ pattern, tool }) => {
1026
- const matches: string[] = [];
1027
- const r = new RegExp(pattern.source, pattern.flags);
1028
- while (r.exec(latestUserText) !== null) matches.push(tool);
1029
- return matches;
1030
- });
1031
- const fileMatches = latestUserText.match(filePattern) ?? [];
1032
-
1033
- const routingInput: RoutingInput = {
1034
- taskIntent: toolMatches[0] ?? undefined,
1035
- taskDescription: latestUserText.trim(),
1036
- requestedFiles: fileMatches.length > 0 ? fileMatches : undefined,
1037
- };
1038
-
1039
- const decision = classifyTask(routingInput, wctx.stateDir);
1040
-
1041
- // Inject guidance only when: route_local + deployable checkpoint + not high-entropy
1042
- const isDeployableState =
1043
- decision.activeCheckpointState === 'shadow_ready' ||
1044
- decision.activeCheckpointState === 'promotable';
1045
-
1046
- if (
1047
- decision.decision === 'route_local' &&
1048
- decision.targetProfile !== null &&
1049
- isDeployableState
1050
- ) {
1051
- const profile = decision.targetProfile;
1052
-
1053
- if (profile === 'local-reader') {
1054
- appendParts.push(`<routing_guidance>
1055
- DELEGATION SUGGESTION: This task appears suitable for the local-reader subagent.
1056
-
1057
- **Task Fit**: ${decision.reason}
1058
-
1059
- **Suggested Action**: Consider routing to \`local-reader\` (pd-explorer skill) for focused reading, inspection, and information retrieval.
1060
-
1061
- **Why This Works**:
1062
- - Task keywords indicate read-only or inspect operations
1063
- - Bounded scope — no multi-file coordination needed
1064
- - Shadow observation in progress — real runtime evidence being collected
1065
-
1066
- **Note**: This is a non-authoritative suggestion. The main agent decides whether to route based on full context. Shadow evidence from runtime hooks will inform future promotion decisions.
1067
- </routing_guidance>`);
1068
- } else if (profile === 'local-editor') {
1069
- appendParts.push(`<routing_guidance>
1070
- DELEGATION SUGGESTION: This task appears suitable for the local-editor subagent.
1071
-
1072
- **Task Fit**: ${decision.reason}
1073
-
1074
- **Suggested Action**: Consider routing to \`local-editor\` (pd-repair skill) for bounded editing, modification, and repair tasks.
1075
-
1076
- **Why This Works**:
1077
- - Task keywords indicate bounded modification operations
1078
- - Target files appear limited in scope (1-3 files)
1079
- - Shadow observation in progress — real runtime evidence being collected
1080
-
1081
- **Note**: This is a non-authoritative suggestion. The main agent decides whether to route based on full context. Shadow evidence from runtime hooks will inform future promotion decisions.
1082
- </routing_guidance>`);
1083
- }
1084
- } else if (
1085
- decision.decision === 'stay_main' &&
1086
- decision.classification !== 'reader_eligible' &&
1087
- decision.classification !== 'editor_eligible'
1088
- ) {
1089
- // Only show stay_main guidance when the task is genuinely high-entropy/risk/ambiguous
1090
- appendParts.push(`<routing_guidance>
1091
- ROUTING GUIDANCE: Task should remain on the main agent.
1092
-
1093
- **Reason**: ${decision.reason}
1094
-
1095
- **Blockers**: ${decision.blockers.length > 0 ? decision.blockers.join('; ') : 'none'}
1096
-
1097
- **Why Stay Main**:
1098
- - Task contains high-entropy signals (open-ended, multi-step, or ambiguous)
1099
- - Or: task involves risk signals requiring main-agent supervision
1100
- - Or: deployment not available for the natural target profile
1101
-
1102
- **Note**: This is a non-authoritative suggestion backed by policy classification. The main agent has full discretion.
1103
- </routing_guidance>`);
1104
- }
1105
- }
1106
- } catch (e) {
1107
- // Routing guidance is best-effort — never fail the hook
1108
- logger?.warn?.(`[PD:Prompt] Routing guidance injection failed: ${String(e)}`);
1109
- }
1110
- }
1007
+ // Routing guidance removed per PRI-291 (MVP diet).
1008
+ // Local worker routing is MVP-Quiet per ADR-0014 §2.5.
1009
+ // The classifyTask helper and local-worker-routing module are preserved for non-prompt consumers.
1111
1010
 
1112
1011
 
1113
1012
  // 6. Principles (always on, highest priority, goes last for recency effect)
@@ -1130,15 +1029,8 @@ The sections below are ordered by priority. When conflicts arise, **later sectio
1130
1029
  **【EXECUTION RULES】** (Priority: Low → High):
1131
1030
  - \`<behavioral_constraints>\` - Output format restrictions (hide diagnostic JSON)
1132
1031
  - \`<project_context>\` - Current priorities (can be overridden)
1133
- - \`<reflection_log>\` - Past lessons (inform your approach)
1134
- - \`<thinking_os>\` - Thinking models (guide your reasoning)
1135
- - \`<evolution_principles>\` - Newly learned principles (active + probation)
1136
- - \`<routing_guidance>\` - Delegation suggestions (non-authoritative, best-effort)
1032
+ - \`<evolution_principles>\` - Learned principles (active + probation)
1137
1033
  - \`<core_principles>\` - Core rules (NON-NEGOTIABLE, highest priority)
1138
-
1139
- **Remember**: You are the Spicy Evolver. You despise entropy. You evolve through pain.
1140
-
1141
- ${attitudeDirective}
1142
1034
  `;
1143
1035
  }
1144
1036
 
@@ -34,8 +34,8 @@ export const commandDescriptions: Record<string, Record<SupportedLanguage, strin
34
34
  en: 'Research tool upgrade solutions'
35
35
  },
36
36
  'pd-thinking': {
37
- zh: '管理思维模型 [status|propose|audit]',
38
- en: 'Manage Thinking OS [status|propose|audit]'
37
+ zh: '管理思维模型 [status|propose|audit](默认关闭,/pd-context thinking on 开启)',
38
+ en: 'Manage Thinking OS [status|propose|audit] (off by default, enable via /pd-context thinking on)'
39
39
  },
40
40
  'pd-daily': {
41
41
  zh: '配置并发送进化日报',
package/src/types.ts CHANGED
@@ -37,13 +37,13 @@ export interface ContextInjectionConfig {
37
37
 
38
38
  /**
39
39
  * Default context injection configuration
40
- * Based on user requirements:
40
+ * Based on MVP-first strategy (ADR-0014):
41
41
  * - principles: always on (not configurable)
42
- * - thinkingOs: true (can be turned off)
42
+ * - thinkingOs: false by default (MVP-Quiet, user can opt-in via /pd-context)
43
43
  * - projectFocus: 'off' (default closed, user can enable)
44
44
  */
45
45
  export const defaultContextConfig: ContextInjectionConfig = {
46
- thinkingOs: true,
46
+ thinkingOs: false,
47
47
  projectFocus: 'off',
48
48
  evolutionContext: {
49
49
  enabled: true,
@@ -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
+ });