principles-disciple 1.77.0 → 1.79.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.
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/commands/context.ts +5 -5
- package/src/core/runtime-v2-prompt-activation-reader.ts +38 -107
- package/src/hooks/prompt.ts +19 -151
- package/src/i18n/commands.ts +2 -2
- package/src/types.ts +3 -3
- package/tests/hooks/prompt-characterization.test.ts +22 -36
- package/tests/hooks/prompt-diet.test.ts +343 -0
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/commands/context.ts
CHANGED
|
@@ -226,7 +226,7 @@ function applyPreset(
|
|
|
226
226
|
break;
|
|
227
227
|
case 'standard':
|
|
228
228
|
config = {
|
|
229
|
-
thinkingOs:
|
|
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
|
|
295
|
-
\`/pd-context full\` - Full mode (
|
|
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();
|
|
@@ -1,23 +1,11 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as yaml from 'js-yaml';
|
|
4
|
-
import { SqliteConnection, SqliteActivationStateStore, computeEffectiveFlags, DEFAULT_FEATURE_FLAGS } from '@principles/core/runtime-v2';
|
|
5
|
-
import type {
|
|
4
|
+
import { SqliteConnection, SqliteActivationStateStore, computeEffectiveFlags, DEFAULT_FEATURE_FLAGS, filterPromptActivations, resolvePrincipleFromArtifact } from '@principles/core/runtime-v2';
|
|
5
|
+
import type { EffectiveFeatureFlags, ActivatedPrinciple, PromptActivationReaderResult } from '@principles/core/runtime-v2';
|
|
6
6
|
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
export interface ActivatedPrinciple {
|
|
10
|
-
principleId: string;
|
|
11
|
-
text: string;
|
|
12
|
-
artifactId: string;
|
|
13
|
-
activationId: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface PromptActivationReaderResult {
|
|
17
|
-
principles: ActivatedPrinciple[];
|
|
18
|
-
warnings: string[];
|
|
19
|
-
source: 'runtime_v2';
|
|
20
|
-
}
|
|
7
|
+
export { RUNTIME_V2_PRINCIPLE_BUDGET } from '@principles/core/runtime-v2';
|
|
8
|
+
export type { ActivatedPrinciple, PromptActivationReaderResult };
|
|
21
9
|
|
|
22
10
|
export interface PromptActivationReaderDeps {
|
|
23
11
|
logger?: { warn?: (msg: string) => void; info?: (msg: string) => void; error?: (msg: string) => void };
|
|
@@ -54,14 +42,29 @@ export class PromptActivationReader {
|
|
|
54
42
|
sqliteConn = new SqliteConnection(this.workspaceDir);
|
|
55
43
|
const store = new SqliteActivationStateStore(sqliteConn);
|
|
56
44
|
|
|
57
|
-
const
|
|
45
|
+
const allActivations = await store.listPromptActivations();
|
|
46
|
+
const promptActivations = filterPromptActivations(allActivations);
|
|
47
|
+
|
|
48
|
+
for (const activation of promptActivations) {
|
|
49
|
+
let artifactRow: unknown | null;
|
|
50
|
+
try {
|
|
51
|
+
artifactRow = this.queryArtifactRow(sqliteConn, activation.artifactId);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
54
|
+
const warning = `artifact_query_failed: artifactId=${activation.artifactId} reason=${msg}; nextAction=check_pi_artifacts_table`;
|
|
55
|
+
warnings.push(warning);
|
|
56
|
+
this.deps.logger?.warn?.(`[PD:RuntimeV2] ${warning}`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
if (artifactRow === null) {
|
|
61
|
+
const warning = `artifact_not_found: artifactId=${activation.artifactId}; nextAction=check_pi_artifacts_table`;
|
|
62
|
+
warnings.push(warning);
|
|
63
|
+
this.deps.logger?.warn?.(`[PD:RuntimeV2] ${warning}`);
|
|
61
64
|
continue;
|
|
62
65
|
}
|
|
63
66
|
|
|
64
|
-
const result =
|
|
67
|
+
const result = resolvePrincipleFromArtifact(artifactRow, activation);
|
|
65
68
|
if (result.ok) {
|
|
66
69
|
principles.push(result.principle);
|
|
67
70
|
} else {
|
|
@@ -85,6 +88,21 @@ export class PromptActivationReader {
|
|
|
85
88
|
return { principles, warnings, source: 'runtime_v2' };
|
|
86
89
|
}
|
|
87
90
|
|
|
91
|
+
private queryArtifactRow(sqliteConn: SqliteConnection, artifactId: string): unknown | null {
|
|
92
|
+
try {
|
|
93
|
+
const db = sqliteConn.getDb();
|
|
94
|
+
const row = db.prepare(`
|
|
95
|
+
SELECT artifact_id, artifact_kind, content_json, validation_status
|
|
96
|
+
FROM pi_artifacts
|
|
97
|
+
WHERE artifact_id = ?
|
|
98
|
+
`).get(artifactId);
|
|
99
|
+
return row ?? null;
|
|
100
|
+
} catch (e) {
|
|
101
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
102
|
+
throw new Error(`artifact_query_failed: artifactId=${artifactId} reason=${msg}; nextAction=check_pi_artifacts_table`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
88
106
|
private loadFeatureFlags(): EffectiveFeatureFlags {
|
|
89
107
|
const configPath = path.join(this.workspaceDir, '.pd', 'feature-flags.yaml');
|
|
90
108
|
|
|
@@ -141,91 +159,4 @@ export class PromptActivationReader {
|
|
|
141
159
|
}
|
|
142
160
|
return result;
|
|
143
161
|
}
|
|
144
|
-
|
|
145
|
-
private resolvePrincipleFromActivation(
|
|
146
|
-
sqliteConn: SqliteConnection,
|
|
147
|
-
activation: ActivationStatusRecord,
|
|
148
|
-
): { ok: true; principle: ActivatedPrinciple } | { ok: false; warning: string } {
|
|
149
|
-
const db = sqliteConn.getDb();
|
|
150
|
-
|
|
151
|
-
let contentJson: string;
|
|
152
|
-
|
|
153
|
-
try {
|
|
154
|
-
const row = db.prepare(`
|
|
155
|
-
SELECT artifact_id, artifact_kind, content_json, validation_status
|
|
156
|
-
FROM pi_artifacts
|
|
157
|
-
WHERE artifact_id = ?
|
|
158
|
-
`).get(activation.artifactId);
|
|
159
|
-
|
|
160
|
-
if (!isRecord(row)) {
|
|
161
|
-
return { ok: false, warning: `artifact_query_unexpected: artifactId=${activation.artifactId}; nextAction=check_pi_artifacts_table` };
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const artifact_id = Object.hasOwn(row, 'artifact_id') && typeof row.artifact_id === 'string' && row.artifact_id.length > 0 ? row.artifact_id : null;
|
|
165
|
-
const artifact_kind = Object.hasOwn(row, 'artifact_kind') && typeof row.artifact_kind === 'string' ? row.artifact_kind : null;
|
|
166
|
-
const raw_content_json = Object.hasOwn(row, 'content_json') && typeof row.content_json === 'string' ? row.content_json : null;
|
|
167
|
-
const validation_status = Object.hasOwn(row, 'validation_status') && typeof row.validation_status === 'string' ? row.validation_status : null;
|
|
168
|
-
|
|
169
|
-
if (!artifact_id) {
|
|
170
|
-
return { ok: false, warning: `artifact_not_found: artifactId=${activation.artifactId} activationId=${activation.activationId}; nextAction=verify_artifact_exists_or_remove_stale_activation` };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (artifact_kind !== 'principle') {
|
|
174
|
-
return { ok: false, warning: `artifact_not_principle: artifactId=${artifact_id} kind=${artifact_kind ?? 'missing'}; nextAction=skip_non_principle_activations` };
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (validation_status !== 'validated') {
|
|
178
|
-
return { ok: false, warning: `artifact_not_validated: artifactId=${artifact_id} status=${validation_status ?? 'missing'}; nextAction=skip_unvalidated_artifacts` };
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (raw_content_json === null) {
|
|
182
|
-
return { ok: false, warning: `artifact_missing_content_json: artifactId=${artifact_id}; nextAction=ensure_artifact_has_content_json` };
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
contentJson = raw_content_json;
|
|
186
|
-
} catch (e) {
|
|
187
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
188
|
-
return { ok: false, warning: `artifact_query_failed: artifactId=${activation.artifactId} reason=${msg}; nextAction=check_pi_artifacts_table` };
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
let parsed: unknown;
|
|
192
|
-
try {
|
|
193
|
-
parsed = JSON.parse(contentJson);
|
|
194
|
-
} catch (e) {
|
|
195
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
196
|
-
return { ok: false, warning: `artifact_content_json_parse_error: artifactId=${activation.artifactId} reason=${msg}; nextAction=fix_artifact_content_json` };
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (!isRecord(parsed)) {
|
|
200
|
-
return { ok: false, warning: `artifact_content_malformed: artifactId=${activation.artifactId} reason=parsed_to_non_object; nextAction=fix_artifact_content_json` };
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const principleId = Object.hasOwn(parsed, 'principleId') && typeof parsed.principleId === 'string' ? parsed.principleId : undefined;
|
|
204
|
-
const text = Object.hasOwn(parsed, 'text') && typeof parsed.text === 'string' ? parsed.text : undefined;
|
|
205
|
-
|
|
206
|
-
const draftObj = Object.hasOwn(parsed, 'principleDraft') && isRecord(parsed.principleDraft) ? parsed.principleDraft : null;
|
|
207
|
-
const draftTitle = draftObj && Object.hasOwn(draftObj, 'title') && typeof draftObj.title === 'string' ? draftObj.title : undefined;
|
|
208
|
-
const draftStatement = draftObj && Object.hasOwn(draftObj, 'statement') && typeof draftObj.statement === 'string' ? draftObj.statement : undefined;
|
|
209
|
-
|
|
210
|
-
const resolvedPrincipleId = principleId && principleId.length > 0 ? principleId : draftTitle;
|
|
211
|
-
const resolvedText = text && text.length > 0 ? text : draftStatement;
|
|
212
|
-
|
|
213
|
-
if (!resolvedPrincipleId || resolvedPrincipleId.length === 0) {
|
|
214
|
-
return { ok: false, warning: `artifact_missing_principle_id: artifactId=${activation.artifactId}; nextAction=ensure_artifact_has_principleId_or_principleDraft_title` };
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (!resolvedText || resolvedText.length === 0) {
|
|
218
|
-
return { ok: false, warning: `artifact_missing_text: artifactId=${activation.artifactId} principleId=${resolvedPrincipleId}; nextAction=ensure_artifact_has_text_or_principleDraft_statement` };
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return {
|
|
222
|
-
ok: true,
|
|
223
|
-
principle: {
|
|
224
|
-
principleId: resolvedPrincipleId,
|
|
225
|
-
text: resolvedText,
|
|
226
|
-
artifactId: activation.artifactId,
|
|
227
|
-
activationId: activation.activationId,
|
|
228
|
-
},
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
162
|
}
|
package/src/hooks/prompt.ts
CHANGED
|
@@ -7,13 +7,14 @@ 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
|
-
|
|
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';
|
|
14
|
-
import { getCachedMaskedPrincipleSet, WorkflowFunnelLoader, PiAiRuntimeAdapter, EmpathyObserver, AgentScheduler } from '@principles/core/runtime-v2';
|
|
15
|
+
import { getCachedMaskedPrincipleSet, WorkflowFunnelLoader, PiAiRuntimeAdapter, EmpathyObserver, AgentScheduler, RUNTIME_V2_PRINCIPLE_BUDGET, trimToBudget, renderPrinciplesToDirectives } from '@principles/core/runtime-v2';
|
|
15
16
|
import { truncateInjectionToBudget } from '@principles/core/prompt-builder';
|
|
16
|
-
import { PromptActivationReader
|
|
17
|
+
import { PromptActivationReader } from '../core/runtime-v2-prompt-activation-reader.js';
|
|
17
18
|
import {
|
|
18
19
|
matchEmpathyKeywords,
|
|
19
20
|
loadKeywordStore,
|
|
@@ -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.
|
|
707
|
-
|
|
708
|
-
|
|
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)
|
|
@@ -899,20 +898,12 @@ ${heartbeatChecklist}
|
|
|
899
898
|
dedupedV2 = v2Result.principles.filter((p) => !legacyActiveIds.has(p.principleId));
|
|
900
899
|
|
|
901
900
|
if (dedupedV2.length > 0) {
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
const entry = `- [${escapeXml(p.principleId)}] ${escapeXml(p.text)}`;
|
|
909
|
-
if (remaining < entry.length + 1) {
|
|
910
|
-
logger?.info?.(`[PD:RuntimeV2] Principle budget reached (${RUNTIME_V2_PRINCIPLE_BUDGET}c) — truncating after ${lines.length - 1} principles`);
|
|
911
|
-
break;
|
|
912
|
-
}
|
|
913
|
-
lines.push(entry);
|
|
914
|
-
remaining -= entry.length + 1;
|
|
915
|
-
runtimeV2PrincipleIds.add(p.principleId);
|
|
901
|
+
const { lines, injectedIds, truncated } = trimToBudget(dedupedV2, RUNTIME_V2_PRINCIPLE_BUDGET, escapeXml);
|
|
902
|
+
if (truncated) {
|
|
903
|
+
logger?.info?.(`[PD:RuntimeV2] Principle budget reached (${RUNTIME_V2_PRINCIPLE_BUDGET}c) — truncating after ${injectedIds.size} principles`);
|
|
904
|
+
}
|
|
905
|
+
for (const id of injectedIds) {
|
|
906
|
+
runtimeV2PrincipleIds.add(id);
|
|
916
907
|
}
|
|
917
908
|
runtimeV2PrinciplesContent = lines.join('\n');
|
|
918
909
|
}
|
|
@@ -985,129 +976,13 @@ ${empathySilenceConstraint}
|
|
|
985
976
|
// PLACED IN prependSystemContext (before gateway system prompt) for highest LLM attention.
|
|
986
977
|
// These are owner-reviewed, validated behavior constraints — not background context.
|
|
987
978
|
if (runtimeV2PrincipleIds.size > 0) {
|
|
988
|
-
const
|
|
989
|
-
|
|
990
|
-
directiveLines.push('## 【OWNER-APPROVED BEHAVIOR DIRECTIVES】');
|
|
991
|
-
directiveLines.push('');
|
|
992
|
-
directiveLines.push('Owner-approved behavior directives are active operating constraints learned from prior owner corrections.');
|
|
993
|
-
directiveLines.push('These directives are mandatory for this session unless they conflict with safety, security, or higher-priority system policy.');
|
|
994
|
-
directiveLines.push('For ambiguous coding or file-changing tasks, follow these directives before using mutating tools.');
|
|
995
|
-
directiveLines.push('');
|
|
996
|
-
for (const p of dedupedV2) {
|
|
997
|
-
if (!runtimeV2PrincipleIds.has(p.principleId)) continue;
|
|
998
|
-
directiveLines.push(`<directive id="${escapeXml(p.principleId)}" source="runtime_v2_activation">`);
|
|
999
|
-
directiveLines.push(`MANDATORY: ${escapeXml(p.text)}`);
|
|
1000
|
-
directiveLines.push('Apply this as an active behavior constraint. Do not treat this as background context.');
|
|
1001
|
-
directiveLines.push('</directive>');
|
|
1002
|
-
directiveLines.push('');
|
|
1003
|
-
}
|
|
1004
|
-
directiveLines.push('Note: These directives do not override safety, security, or core system policy.');
|
|
1005
|
-
prependSystemContext += directiveLines.join('\n');
|
|
979
|
+
const directiveText = renderPrinciplesToDirectives(dedupedV2, runtimeV2PrincipleIds, escapeXml);
|
|
980
|
+
prependSystemContext += directiveText;
|
|
1006
981
|
}
|
|
1007
982
|
|
|
1008
|
-
// Routing
|
|
1009
|
-
//
|
|
1010
|
-
//
|
|
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
|
-
}
|
|
983
|
+
// Routing guidance removed per PRI-291 (MVP diet).
|
|
984
|
+
// Local worker routing is MVP-Quiet per ADR-0014 §2.5.
|
|
985
|
+
// The classifyTask helper and local-worker-routing module are preserved for non-prompt consumers.
|
|
1111
986
|
|
|
1112
987
|
|
|
1113
988
|
// 6. Principles (always on, highest priority, goes last for recency effect)
|
|
@@ -1130,15 +1005,8 @@ The sections below are ordered by priority. When conflicts arise, **later sectio
|
|
|
1130
1005
|
**【EXECUTION RULES】** (Priority: Low → High):
|
|
1131
1006
|
- \`<behavioral_constraints>\` - Output format restrictions (hide diagnostic JSON)
|
|
1132
1007
|
- \`<project_context>\` - Current priorities (can be overridden)
|
|
1133
|
-
- \`<
|
|
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)
|
|
1008
|
+
- \`<evolution_principles>\` - Learned principles (active + probation)
|
|
1137
1009
|
- \`<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
1010
|
`;
|
|
1143
1011
|
}
|
|
1144
1012
|
|
package/src/i18n/commands.ts
CHANGED
|
@@ -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
|
|
40
|
+
* Based on MVP-first strategy (ADR-0014):
|
|
41
41
|
* - principles: always on (not configurable)
|
|
42
|
-
* - thinkingOs:
|
|
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:
|
|
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 (
|
|
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 —
|
|
194
|
-
// Ensure appendParts is non-empty so
|
|
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
|
|
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
|
-
|
|
218
|
-
expect(
|
|
220
|
+
const combined = (result?.prependSystemContext ?? '') + (result?.appendSystemContext ?? '');
|
|
221
|
+
expect(combined).not.toContain('HUMBLE_RECOVERY');
|
|
219
222
|
});
|
|
220
223
|
|
|
221
|
-
it('GFI >=
|
|
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
|
-
|
|
235
|
-
expect(
|
|
229
|
+
const combined = (result?.prependSystemContext ?? '') + (result?.appendSystemContext ?? '');
|
|
230
|
+
expect(combined).not.toContain('CONCILIATORY');
|
|
236
231
|
});
|
|
237
232
|
|
|
238
|
-
it('GFI
|
|
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
|
-
|
|
252
|
-
expect(
|
|
238
|
+
const combined = (result?.prependSystemContext ?? '') + (result?.appendSystemContext ?? '');
|
|
239
|
+
expect(combined).not.toContain('EFFICIENT');
|
|
253
240
|
});
|
|
254
241
|
|
|
255
|
-
it('
|
|
256
|
-
setSessionGfi(
|
|
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({
|
|
245
|
+
const result = await handleBeforePromptBuild(makeMinimalEvent(), makeCtx({ sessionGfi: 20 }));
|
|
263
246
|
|
|
264
|
-
|
|
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
|
+
});
|