principles-disciple 1.6.0 → 1.7.1
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/dist/commands/context.js +7 -3
- package/dist/commands/evolution-status.d.ts +4 -0
- package/dist/commands/evolution-status.js +134 -0
- package/dist/commands/export.d.ts +2 -0
- package/dist/commands/export.js +45 -0
- package/dist/commands/focus.js +9 -6
- package/dist/commands/pain.js +8 -0
- package/dist/commands/principle-rollback.d.ts +4 -0
- package/dist/commands/principle-rollback.js +22 -0
- package/dist/commands/rollback.js +9 -3
- package/dist/commands/samples.d.ts +2 -0
- package/dist/commands/samples.js +55 -0
- package/dist/commands/trust.js +64 -81
- package/dist/core/config.d.ts +5 -0
- package/dist/core/control-ui-db.d.ts +68 -0
- package/dist/core/control-ui-db.js +274 -0
- package/dist/core/detection-funnel.d.ts +1 -1
- package/dist/core/detection-funnel.js +4 -0
- package/dist/core/dictionary.d.ts +2 -0
- package/dist/core/dictionary.js +13 -0
- package/dist/core/event-log.d.ts +7 -1
- package/dist/core/event-log.js +10 -0
- package/dist/core/evolution-engine.d.ts +5 -5
- package/dist/core/evolution-engine.js +18 -18
- package/dist/core/evolution-migration.d.ts +5 -0
- package/dist/core/evolution-migration.js +65 -0
- package/dist/core/evolution-reducer.d.ts +69 -0
- package/dist/core/evolution-reducer.js +369 -0
- package/dist/core/evolution-types.d.ts +103 -0
- package/dist/core/path-resolver.js +75 -36
- package/dist/core/paths.d.ts +7 -8
- package/dist/core/paths.js +48 -40
- package/dist/core/profile.js +1 -1
- package/dist/core/session-tracker.d.ts +14 -2
- package/dist/core/session-tracker.js +75 -9
- package/dist/core/thinking-models.d.ts +38 -0
- package/dist/core/thinking-models.js +170 -0
- package/dist/core/trajectory.d.ts +184 -0
- package/dist/core/trajectory.js +817 -0
- package/dist/core/trust-engine.d.ts +6 -0
- package/dist/core/trust-engine.js +50 -29
- package/dist/core/workspace-context.d.ts +13 -0
- package/dist/core/workspace-context.js +50 -7
- package/dist/hooks/gate.js +171 -87
- package/dist/hooks/llm.js +119 -71
- package/dist/hooks/pain.js +105 -5
- package/dist/hooks/prompt.d.ts +11 -14
- package/dist/hooks/prompt.js +283 -57
- package/dist/hooks/subagent.js +69 -28
- package/dist/hooks/trajectory-collector.d.ts +32 -0
- package/dist/hooks/trajectory-collector.js +256 -0
- package/dist/http/principles-console-route.d.ts +2 -0
- package/dist/http/principles-console-route.js +257 -0
- package/dist/i18n/commands.js +16 -0
- package/dist/index.js +105 -4
- package/dist/service/control-ui-query-service.d.ts +217 -0
- package/dist/service/control-ui-query-service.js +537 -0
- package/dist/service/empathy-observer-manager.d.ts +2 -0
- package/dist/service/empathy-observer-manager.js +43 -1
- package/dist/service/evolution-worker.d.ts +27 -0
- package/dist/service/evolution-worker.js +256 -41
- package/dist/service/runtime-summary-service.d.ts +79 -0
- package/dist/service/runtime-summary-service.js +319 -0
- package/dist/service/trajectory-service.d.ts +2 -0
- package/dist/service/trajectory-service.js +15 -0
- package/dist/tools/agent-spawn.d.ts +27 -6
- package/dist/tools/agent-spawn.js +339 -87
- package/dist/tools/deep-reflect.d.ts +27 -7
- package/dist/tools/deep-reflect.js +210 -121
- package/dist/types/event-types.d.ts +10 -2
- package/dist/types.d.ts +10 -0
- package/dist/types.js +5 -0
- package/openclaw.plugin.json +43 -11
- package/package.json +14 -4
- package/templates/langs/zh/skills/pd-daily/SKILL.md +97 -13
package/dist/hooks/gate.js
CHANGED
|
@@ -7,6 +7,7 @@ import { trackBlock, hasRecentThinking, getSession } from '../core/session-track
|
|
|
7
7
|
import { assessRiskLevel, estimateLineChanges } from '../core/risk-calculator.js';
|
|
8
8
|
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
9
9
|
import { checkEvolutionGate } from '../core/evolution-engine.js';
|
|
10
|
+
import { EventLogService } from '../core/event-log.js';
|
|
10
11
|
// ═══ GFI Gate Tool Tiers ═══
|
|
11
12
|
// TIER 0: 只读工具 - 永不拦截
|
|
12
13
|
const READ_ONLY_TOOLS = new Set([
|
|
@@ -19,7 +20,7 @@ const READ_ONLY_TOOLS = new Set([
|
|
|
19
20
|
'deep_reflect',
|
|
20
21
|
]);
|
|
21
22
|
// TIER 1: 低风险修改 - GFI >= low_risk_block 时拦截
|
|
22
|
-
// 注意:
|
|
23
|
+
// 注意:pd_run_worker、sessions_spawn、task 是 Agent 派生工具,不应被 GFI Gate 拦截
|
|
23
24
|
// 它们属于 AGENT_TOOLS,在早期过滤后直接放行
|
|
24
25
|
const LOW_RISK_WRITE_TOOLS = new Set([
|
|
25
26
|
'write', 'write_file',
|
|
@@ -36,32 +37,69 @@ const BASH_TOOLS_SET = new Set([
|
|
|
36
37
|
/**
|
|
37
38
|
* 分析 Bash 命令风险等级
|
|
38
39
|
*/
|
|
39
|
-
function analyzeBashCommand(command, safePatterns, dangerousPatterns) {
|
|
40
|
-
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
function analyzeBashCommand(command, safePatterns, dangerousPatterns, logger) {
|
|
41
|
+
let normalizedCmd = command.trim().toLowerCase();
|
|
42
|
+
// P2 fix: Unicode de-obfuscation — convert Cyrillic/Unicode lookalikes to ASCII equivalents
|
|
43
|
+
// Common Cyrillic lookalikes that could bypass detection: аеорсух (Cyrillic) → aeopcyx (Latin)
|
|
44
|
+
const CYRILLIC_TO_LATIN = {
|
|
45
|
+
'а': 'a', 'е': 'e', 'о': 'o', 'р': 'p', 'с': 'c', 'у': 'y', 'х': 'x',
|
|
46
|
+
'А': 'a', 'Е': 'e', 'О': 'o', 'Р': 'p', 'С': 'c', 'У': 'y', 'Х': 'x',
|
|
47
|
+
// Additional confusable chars
|
|
48
|
+
'і': 'i', 'ј': 'j', 'ѕ': 's', 'ԁ': 'd', 'ɡ': 'g', 'һ': 'h', 'ⅰ': 'i',
|
|
49
|
+
'ƚ': 'l', 'м': 'm', 'п': 'n', 'ѵ': 'v', 'ѡ': 'w', 'ᴦ': 'r', 'ꜱ': 's',
|
|
50
|
+
};
|
|
51
|
+
normalizedCmd = normalizedCmd.replace(/[а-яА-Яіјѕԁɡһⅰƚмпеꜱѵѡᴦꜱ]/g, m => CYRILLIC_TO_LATIN[m] ?? m);
|
|
52
|
+
// P2 fix: Tokenize command chain before pattern matching to catch `cmd1 && cmd2` bypasses
|
|
53
|
+
// Only split on statement separators (; && ||), NOT on pipe (|) which is part of the command
|
|
54
|
+
const tokens = normalizedCmd
|
|
55
|
+
.split(/\s*(?:;|&&|\|\|)\s*/)
|
|
56
|
+
.map(t => t.trim())
|
|
57
|
+
.filter(t => t.length > 0);
|
|
58
|
+
// If no tokens (e.g., pure pipe-only), use the original
|
|
59
|
+
const segments = tokens.length > 0 ? tokens : [normalizedCmd];
|
|
60
|
+
// P2 fix: Also strip outer $() and backticks from each segment
|
|
61
|
+
const cleanSegments = segments.map(seg => {
|
|
62
|
+
let s = seg;
|
|
63
|
+
// Strip leading $() or ${} or backtick-wrapped commands
|
|
64
|
+
s = s.replace(/^\$\([^)]+\)$/, '').replace(/^\$\{[^}]+\}$/, '').replace(/^`([^`]+)`$/, '$1');
|
|
65
|
+
return s.trim();
|
|
66
|
+
}).filter(s => s.length > 0);
|
|
67
|
+
// 1. Check dangerous patterns against each segment
|
|
68
|
+
for (const seg of cleanSegments) {
|
|
69
|
+
for (const pattern of dangerousPatterns) {
|
|
70
|
+
try {
|
|
71
|
+
if (new RegExp(pattern, 'i').test(seg)) {
|
|
72
|
+
return 'dangerous';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
logger?.warn?.(`[PD_GATE] Invalid dangerous bash regex "${pattern}": ${String(error)}. Failing closed.`);
|
|
45
77
|
return 'dangerous';
|
|
78
|
+
// Fail-closed: 无效的危险模式正则视为匹配危险命令
|
|
46
79
|
}
|
|
47
80
|
}
|
|
48
|
-
catch {
|
|
49
|
-
// 忽略无效正则
|
|
50
|
-
}
|
|
51
81
|
}
|
|
52
|
-
// 2.
|
|
53
|
-
for (const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
82
|
+
// 2. Check safe patterns (only if ALL segments are safe)
|
|
83
|
+
for (const seg of cleanSegments) {
|
|
84
|
+
let isSafe = false;
|
|
85
|
+
for (const pattern of safePatterns) {
|
|
86
|
+
try {
|
|
87
|
+
if (new RegExp(pattern, 'i').test(seg)) {
|
|
88
|
+
isSafe = true;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
logger?.warn?.(`[PD_GATE] Invalid safe bash regex "${pattern}": ${String(error)}. Ignoring safe override.`);
|
|
57
94
|
}
|
|
58
95
|
}
|
|
59
|
-
|
|
60
|
-
//
|
|
96
|
+
if (!isSafe) {
|
|
97
|
+
// Not all segments are safe → treat as normal
|
|
98
|
+
return 'normal';
|
|
61
99
|
}
|
|
62
100
|
}
|
|
63
|
-
//
|
|
64
|
-
return '
|
|
101
|
+
// All segments are safe
|
|
102
|
+
return 'safe';
|
|
65
103
|
}
|
|
66
104
|
/**
|
|
67
105
|
* 计算动态 GFI 阈值
|
|
@@ -82,7 +120,7 @@ export function handleBeforeToolCall(event, ctx) {
|
|
|
82
120
|
// 1. Identify tool type
|
|
83
121
|
const WRITE_TOOLS = ['write', 'edit', 'apply_patch', 'write_file', 'replace', 'insert', 'patch', 'edit_file', 'delete_file', 'move_file'];
|
|
84
122
|
const BASH_TOOLS = ['bash', 'run_shell_command', 'exec', 'execute', 'shell', 'cmd'];
|
|
85
|
-
const AGENT_TOOLS = ['
|
|
123
|
+
const AGENT_TOOLS = ['pd_run_worker', 'sessions_spawn'];
|
|
86
124
|
const isBash = BASH_TOOLS.includes(event.toolName);
|
|
87
125
|
const isWriteTool = WRITE_TOOLS.includes(event.toolName);
|
|
88
126
|
const isAgentTool = AGENT_TOOLS.includes(event.toolName);
|
|
@@ -115,7 +153,7 @@ export function handleBeforeToolCall(event, ctx) {
|
|
|
115
153
|
thinking_checkpoint: {
|
|
116
154
|
enabled: false, // Default OFF
|
|
117
155
|
window_ms: 5 * 60 * 1000,
|
|
118
|
-
high_risk_tools: ['run_shell_command', 'delete_file', 'move_file', '
|
|
156
|
+
high_risk_tools: ['run_shell_command', 'delete_file', 'move_file', 'pd_run_worker'],
|
|
119
157
|
}
|
|
120
158
|
};
|
|
121
159
|
if (fs.existsSync(profilePath)) {
|
|
@@ -151,7 +189,7 @@ export function handleBeforeToolCall(event, ctx) {
|
|
|
151
189
|
// TIER 3: Bash 命令 - 根据内容判断
|
|
152
190
|
if (BASH_TOOLS_SET.has(event.toolName)) {
|
|
153
191
|
const command = String(event.params.command || event.params.args || '');
|
|
154
|
-
const bashRisk = analyzeBashCommand(command, gfiGateConfig?.bash_safe_patterns || [], gfiGateConfig?.bash_dangerous_patterns || []);
|
|
192
|
+
const bashRisk = analyzeBashCommand(command, gfiGateConfig?.bash_safe_patterns || [], gfiGateConfig?.bash_dangerous_patterns || [], logger);
|
|
155
193
|
if (bashRisk === 'dangerous') {
|
|
156
194
|
// 危险命令 - 直接拦截
|
|
157
195
|
logger?.warn?.(`[PD:GFI_GATE] Dangerous bash command blocked: ${command.substring(0, 50)}...`);
|
|
@@ -272,6 +310,26 @@ GFI: ${currentGfi}/100
|
|
|
272
310
|
};
|
|
273
311
|
}
|
|
274
312
|
}
|
|
313
|
+
// AGENT_TOOLS: Block subagent spawn when GFI is critically high (P0 fix: prevent privilege escalation via spawned subagents)
|
|
314
|
+
if (isAgentTool) {
|
|
315
|
+
const AGENT_SPAWN_GFI_THRESHOLD = 90;
|
|
316
|
+
if (currentGfi >= AGENT_SPAWN_GFI_THRESHOLD) {
|
|
317
|
+
logger?.warn?.(`[PD:GFI_GATE] Agent tool "${event.toolName}" blocked by GFI: ${currentGfi} >= ${AGENT_SPAWN_GFI_THRESHOLD}`);
|
|
318
|
+
return {
|
|
319
|
+
block: true,
|
|
320
|
+
blockReason: `[GFI Gate] 疲劳指数过高,禁止派生子智能体。
|
|
321
|
+
|
|
322
|
+
GFI: ${currentGfi}/100
|
|
323
|
+
阈值: ${AGENT_SPAWN_GFI_THRESHOLD} (Stage ${wctx.trust.getStage()})
|
|
324
|
+
|
|
325
|
+
原因: 高疲劳状态下派生子智能体会放大错误风险。
|
|
326
|
+
|
|
327
|
+
解决方案:
|
|
328
|
+
1. 执行 /pd-status reset 清零疲劳值
|
|
329
|
+
2. 简化任务后重试`,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
}
|
|
275
333
|
}
|
|
276
334
|
// Merge pluginConfig (OpenClaw UI settings)
|
|
277
335
|
const configRiskPaths = ctx.pluginConfig?.riskPaths ?? [];
|
|
@@ -314,71 +372,17 @@ GFI: ${currentGfi}/100
|
|
|
314
372
|
};
|
|
315
373
|
const riskLevel = assessRiskLevel(relPath, { toolName: event.toolName, params: event.params }, profile.risk_paths);
|
|
316
374
|
const lineChanges = estimateLineChanges({ toolName: event.toolName, params: event.params });
|
|
375
|
+
const planApprovals = profile.progressive_gate?.plan_approvals;
|
|
376
|
+
const canUsePlanApproval = Boolean(stage === 1 &&
|
|
377
|
+
planApprovals?.enabled &&
|
|
378
|
+
getPlanStatus(ctx.workspaceDir) === 'READY' &&
|
|
379
|
+
planApprovals.allowed_operations?.includes(event.toolName) &&
|
|
380
|
+
matchesAnyPattern(relPath, planApprovals.allowed_patterns || []) &&
|
|
381
|
+
((planApprovals.max_lines_override ?? -1) === -1 || lineChanges <= (planApprovals.max_lines_override ?? -1)));
|
|
317
382
|
logger.info(`[PD_GATE] Trust: ${trustScore} (Stage ${stage}), Risk: ${riskLevel}, Path: ${relPath}`);
|
|
318
|
-
// Stage 1 (Bankruptcy): Block ALL writes to risk paths, and all medium+ writes
|
|
319
|
-
if (stage === 1) {
|
|
320
|
-
if (risky || riskLevel !== 'LOW') {
|
|
321
|
-
// Check if PLAN whitelist is enabled
|
|
322
|
-
if (profile.progressive_gate?.plan_approvals?.enabled) {
|
|
323
|
-
const planApprovals = profile.progressive_gate.plan_approvals;
|
|
324
|
-
const planStatus = getPlanStatus(ctx.workspaceDir);
|
|
325
|
-
// Must have READY plan
|
|
326
|
-
if (planStatus === 'READY') {
|
|
327
|
-
// Check operation type
|
|
328
|
-
if (planApprovals.allowed_operations?.includes(event.toolName)) {
|
|
329
|
-
// Check path pattern
|
|
330
|
-
if (matchesAnyPattern(relPath, planApprovals.allowed_patterns || [])) {
|
|
331
|
-
// Check line limit (if configured)
|
|
332
|
-
const maxLines = planApprovals.max_lines_override ?? -1;
|
|
333
|
-
if (maxLines === -1 || lineChanges <= maxLines) {
|
|
334
|
-
// Record PLAN approval event
|
|
335
|
-
wctx.eventLog.recordPlanApproval(ctx.sessionId, {
|
|
336
|
-
toolName: event.toolName,
|
|
337
|
-
filePath: relPath,
|
|
338
|
-
pattern: relPath,
|
|
339
|
-
planStatus
|
|
340
|
-
});
|
|
341
|
-
logger.info(`[PD_GATE] Stage 1 PLAN approval: ${relPath}`);
|
|
342
|
-
return; // Allow the operation
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
// Block if not approved by whitelist
|
|
349
|
-
return block(relPath, `Trust score too low (${trustScore}). Stage 1 agents cannot modify risk paths or perform non-trivial edits.`, wctx, event.toolName);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
// Stage 2 (Editor): Block writes to risk paths. Block large changes.
|
|
353
|
-
if (stage === 2) {
|
|
354
|
-
if (risky) {
|
|
355
|
-
return block(relPath, `Stage 2 agents are not authorized to modify risk paths.`, wctx, event.toolName);
|
|
356
|
-
}
|
|
357
|
-
const stage2Limit = trustSettings.limits?.stage_2_max_lines ?? 50;
|
|
358
|
-
if (lineChanges > stage2Limit) {
|
|
359
|
-
return block(relPath, `Modification too large (${lineChanges} lines) for Stage 2. Max allowed is ${stage2Limit}.`, wctx, event.toolName);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
// Stage 3 (Developer): Allow normal writes. Require READY plan for risk paths.
|
|
363
|
-
if (stage === 3) {
|
|
364
|
-
if (risky) {
|
|
365
|
-
const planStatus = getPlanStatus(ctx.workspaceDir);
|
|
366
|
-
if (planStatus !== 'READY') {
|
|
367
|
-
return block(relPath, `No READY plan found. Stage 3 requires a plan for risk path modifications.`, wctx, event.toolName);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
const stage3Limit = trustSettings.limits?.stage_3_max_lines ?? 300;
|
|
371
|
-
if (lineChanges > stage3Limit) {
|
|
372
|
-
return block(relPath, `Modification too large (${lineChanges} lines) for Stage 3. Max allowed is ${stage3Limit}.`, wctx, event.toolName);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
// Stage 4 (Architect): Full bypass
|
|
376
|
-
if (stage === 4) {
|
|
377
|
-
logger.info(`[PD_GATE] Trusted Architect bypass for ${relPath}`);
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
383
|
// ── EP SIMULATION MODE (M6验证) ──
|
|
381
384
|
// 记录EP系统的模拟决策,但不生效(仅用于对比分析)
|
|
385
|
+
// BUGFIX #90: 移到所有Stage检查之前,确保所有Stage都触发EP simulation记录
|
|
382
386
|
try {
|
|
383
387
|
const epDecision = checkEvolutionGate(ctx.workspaceDir, {
|
|
384
388
|
toolName: event.toolName,
|
|
@@ -416,13 +420,81 @@ GFI: ${currentGfi}/100
|
|
|
416
420
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
417
421
|
logger.warn(`[PD_EP_SIM] Simulation failed: ${errMsg}, continuing with Trust Engine`);
|
|
418
422
|
}
|
|
423
|
+
// Stage 1 (Bankruptcy): Block ALL writes to risk paths, and all medium+ writes
|
|
424
|
+
if (stage === 1) {
|
|
425
|
+
if (canUsePlanApproval) {
|
|
426
|
+
const planStatus = 'READY';
|
|
427
|
+
wctx.eventLog.recordPlanApproval(ctx.sessionId, {
|
|
428
|
+
toolName: event.toolName,
|
|
429
|
+
filePath: relPath,
|
|
430
|
+
pattern: relPath,
|
|
431
|
+
planStatus
|
|
432
|
+
});
|
|
433
|
+
wctx.trajectory?.recordGateBlock?.({
|
|
434
|
+
sessionId: ctx.sessionId,
|
|
435
|
+
toolName: event.toolName,
|
|
436
|
+
filePath: relPath,
|
|
437
|
+
reason: 'plan_approval',
|
|
438
|
+
planStatus,
|
|
439
|
+
});
|
|
440
|
+
logger.info(`[PD_GATE] Stage 1 PLAN approval: ${relPath}`);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (risky || riskLevel !== 'LOW') {
|
|
444
|
+
// Block if not approved by whitelist
|
|
445
|
+
return block(relPath, `Trust score too low (${trustScore}). Stage 1 agents cannot modify risk paths or perform non-trivial edits.`, wctx, event.toolName, ctx.sessionId);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
// Stage 2 (Editor): Block writes to risk paths. Block large changes.
|
|
449
|
+
if (stage === 2) {
|
|
450
|
+
if (risky) {
|
|
451
|
+
return block(relPath, `Stage 2 agents are not authorized to modify risk paths.`, wctx, event.toolName, ctx.sessionId);
|
|
452
|
+
}
|
|
453
|
+
const stage2Limit = trustSettings.limits?.stage_2_max_lines ?? 50;
|
|
454
|
+
if (lineChanges > stage2Limit) {
|
|
455
|
+
return block(relPath, `Modification too large (${lineChanges} lines) for Stage 2. Max allowed is ${stage2Limit}.`, wctx, event.toolName, ctx.sessionId);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// Stage 3 (Developer): Allow normal writes. Require READY plan for risk paths.
|
|
459
|
+
if (stage === 3) {
|
|
460
|
+
if (risky) {
|
|
461
|
+
const planStatus = getPlanStatus(ctx.workspaceDir);
|
|
462
|
+
if (planStatus !== 'READY') {
|
|
463
|
+
return block(relPath, `No READY plan found. Stage 3 requires a plan for risk path modifications.`, wctx, event.toolName, ctx.sessionId);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const stage3Limit = trustSettings.limits?.stage_3_max_lines ?? 300;
|
|
467
|
+
if (lineChanges > stage3Limit) {
|
|
468
|
+
return block(relPath, `Modification too large (${lineChanges} lines) for Stage 3. Max allowed is ${stage3Limit}.`, wctx, event.toolName, ctx.sessionId);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
// Stage 4 (Architect): Full bypass
|
|
472
|
+
if (stage === 4) {
|
|
473
|
+
logger.info(`[PD_GATE] Trusted Architect bypass for ${relPath}`);
|
|
474
|
+
// Audit log for Stage 4 bypass (security traceability)
|
|
475
|
+
try {
|
|
476
|
+
const stateDir = wctx.resolve('STATE_DIR');
|
|
477
|
+
const eventLog = EventLogService.get(stateDir);
|
|
478
|
+
eventLog.recordGateBypass(ctx.sessionId, {
|
|
479
|
+
toolName: event.toolName,
|
|
480
|
+
filePath: relPath,
|
|
481
|
+
bypassType: 'stage4_architect',
|
|
482
|
+
trustScore,
|
|
483
|
+
trustStage: stage,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
catch (auditErr) {
|
|
487
|
+
logger?.warn?.(`[PD_GATE] Failed to record Stage 4 bypass audit: ${String(auditErr)}`);
|
|
488
|
+
}
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
419
491
|
}
|
|
420
492
|
else {
|
|
421
493
|
// FALLBACK: Legacy Gate Logic
|
|
422
494
|
if (risky && profile.gate?.require_plan_for_risk_paths) {
|
|
423
495
|
const planStatus = getPlanStatus(ctx.workspaceDir);
|
|
424
496
|
if (planStatus !== 'READY') {
|
|
425
|
-
return block(relPath, `No READY plan found in PLAN.md.`, wctx, event.toolName);
|
|
497
|
+
return block(relPath, `No READY plan found in PLAN.md.`, wctx, event.toolName, ctx.sessionId);
|
|
426
498
|
}
|
|
427
499
|
}
|
|
428
500
|
}
|
|
@@ -443,10 +515,22 @@ GFI: ${currentGfi}/100
|
|
|
443
515
|
}
|
|
444
516
|
}
|
|
445
517
|
}
|
|
446
|
-
function block(filePath, reason, wctx, toolName) {
|
|
518
|
+
function block(filePath, reason, wctx, toolName, sessionId) {
|
|
447
519
|
const logger = console;
|
|
448
520
|
logger.error(`[PD_GATE] BLOCKED: ${filePath}. Reason: ${reason}`);
|
|
449
|
-
|
|
521
|
+
if (sessionId) {
|
|
522
|
+
trackBlock(sessionId);
|
|
523
|
+
}
|
|
524
|
+
try {
|
|
525
|
+
wctx.eventLog.recordGateBlock(sessionId, {
|
|
526
|
+
toolName,
|
|
527
|
+
filePath,
|
|
528
|
+
reason,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
catch (error) {
|
|
532
|
+
logger.warn(`[PD_GATE] Failed to record gate block event: ${String(error)}`);
|
|
533
|
+
}
|
|
450
534
|
return {
|
|
451
535
|
block: true,
|
|
452
536
|
blockReason: `[Principles Disciple] Security Gate Blocked this action.\nFile: ${filePath}\nReason: ${reason}\n\nHint: You may need a READY plan or a higher trust score to perform this action.`,
|
package/dist/hooks/llm.js
CHANGED
|
@@ -2,8 +2,11 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { trackFriction, trackLlmOutput, recordThinkingCheckpoint, resetFriction } from '../core/session-tracker.js';
|
|
4
4
|
import { writePainFlag } from '../core/pain.js';
|
|
5
|
+
import { ControlUiDatabase } from '../core/control-ui-db.js';
|
|
5
6
|
import { DetectionService } from '../core/detection-service.js';
|
|
7
|
+
import { detectThinkingModelMatches, deriveThinkingScenarios } from '../core/thinking-models.js';
|
|
6
8
|
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
9
|
+
import { sanitizeAssistantText } from './message-sanitize.js';
|
|
7
10
|
const empathyDedupState = new Map();
|
|
8
11
|
const empathyRateState = new Map();
|
|
9
12
|
function clamp(value, min, max) {
|
|
@@ -184,6 +187,25 @@ export function handleLlmOutput(event, ctx) {
|
|
|
184
187
|
if (!event.assistantTexts || event.assistantTexts.length === 0)
|
|
185
188
|
return;
|
|
186
189
|
const text = event.assistantTexts.join('\n');
|
|
190
|
+
const signal = extractEmpathySignal(text);
|
|
191
|
+
const createdAt = new Date().toISOString();
|
|
192
|
+
let assistantTurnId = null;
|
|
193
|
+
try {
|
|
194
|
+
assistantTurnId = wctx.trajectory?.recordAssistantTurn?.({
|
|
195
|
+
sessionId: ctx.sessionId,
|
|
196
|
+
runId: event.runId,
|
|
197
|
+
provider: event.provider,
|
|
198
|
+
model: event.model,
|
|
199
|
+
rawText: text,
|
|
200
|
+
sanitizedText: sanitizeAssistantText(text),
|
|
201
|
+
usageJson: event.usage || {},
|
|
202
|
+
empathySignalJson: signal,
|
|
203
|
+
createdAt,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
ctx.logger?.warn?.(`[PD:LLM] Failed to persist assistant turn to trajectory: ${String(error)}`);
|
|
208
|
+
}
|
|
187
209
|
// ── Track B: Semantic Pain Detection (V1.3.0 Funnel) ──
|
|
188
210
|
const detectionService = DetectionService.get(wctx.stateDir);
|
|
189
211
|
const detection = detectionService.detect(text);
|
|
@@ -205,7 +227,6 @@ export function handleLlmOutput(event, ctx) {
|
|
|
205
227
|
// empathy sub-pipeline (enabled by default)
|
|
206
228
|
const empathyEnabled = config.get('empathy_engine.enabled');
|
|
207
229
|
if (empathyEnabled !== false) {
|
|
208
|
-
const signal = extractEmpathySignal(text);
|
|
209
230
|
if (signal.detected) {
|
|
210
231
|
const dedupeWindow = Number(config.get('empathy_engine.dedupe_window_ms') ?? 60000);
|
|
211
232
|
const deduped = shouldDedupe(ctx.sessionId, event.runId, signal, dedupeWindow);
|
|
@@ -216,7 +237,21 @@ export function handleLlmOutput(event, ctx) {
|
|
|
216
237
|
const calibratedScore = Math.round(weightedScore * calibrationFactor);
|
|
217
238
|
const boundedScore = applyRateLimit(ctx.sessionId, event.runId, calibratedScore, config);
|
|
218
239
|
if (boundedScore > 0) {
|
|
219
|
-
trackFriction(ctx.sessionId, boundedScore, `user_empathy_${signal.severity}`, ctx.workspaceDir);
|
|
240
|
+
trackFriction(ctx.sessionId, boundedScore, `user_empathy_${signal.severity}`, ctx.workspaceDir, { source: 'user_empathy' });
|
|
241
|
+
try {
|
|
242
|
+
wctx.trajectory?.recordPainEvent?.({
|
|
243
|
+
sessionId: ctx.sessionId,
|
|
244
|
+
source: 'user_empathy',
|
|
245
|
+
score: boundedScore,
|
|
246
|
+
reason: signal.reason || 'Assistant self-reported user emotional distress.',
|
|
247
|
+
severity: signal.severity,
|
|
248
|
+
origin: 'assistant_self_report',
|
|
249
|
+
confidence: signal.confidence,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
ctx.logger?.warn?.(`[PD:LLM] Failed to persist empathy pain event to trajectory: ${String(error)}`);
|
|
254
|
+
}
|
|
220
255
|
// Generate unique event ID for rollback support
|
|
221
256
|
const eventId = `emp_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
|
|
222
257
|
eventLog.recordPainSignal(ctx.sessionId, {
|
|
@@ -263,7 +298,10 @@ export function handleLlmOutput(event, ctx) {
|
|
|
263
298
|
const rolledBackScore = eventLog.rollbackEmpathyEvent(eventId, ctx.sessionId, 'Natural language rollback request detected', 'natural_language');
|
|
264
299
|
if (rolledBackScore > 0) {
|
|
265
300
|
// Reset GFI after successful rollback
|
|
266
|
-
resetFriction(ctx.sessionId
|
|
301
|
+
resetFriction(ctx.sessionId, ctx.workspaceDir, {
|
|
302
|
+
source: 'user_empathy',
|
|
303
|
+
amount: rolledBackScore,
|
|
304
|
+
});
|
|
267
305
|
}
|
|
268
306
|
}
|
|
269
307
|
}
|
|
@@ -297,56 +335,18 @@ export function handleLlmOutput(event, ctx) {
|
|
|
297
335
|
});
|
|
298
336
|
}
|
|
299
337
|
// ═══ Thinking OS: Mental Model Usage Tracking ═══
|
|
300
|
-
trackThinkingModelUsage(
|
|
338
|
+
trackThinkingModelUsage({
|
|
339
|
+
text,
|
|
340
|
+
wctx,
|
|
341
|
+
sessionId: ctx.sessionId,
|
|
342
|
+
runId: event.runId,
|
|
343
|
+
assistantTurnId,
|
|
344
|
+
createdAt,
|
|
345
|
+
logger: ctx.logger,
|
|
346
|
+
});
|
|
301
347
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
/let me (first )?(understand|map|outline|survey|review the (structure|architecture|dependencies))/i,
|
|
305
|
-
/让我先(梳理|了解|画出|理解|查看)(一下)?(结构|架构|依赖|全貌)/,
|
|
306
|
-
],
|
|
307
|
-
'T-02': [
|
|
308
|
-
/(type|test|contract|schema|interface) (constraint|requirement|check|validation)/i,
|
|
309
|
-
/we (must|need to) (respect|follow|adhere to) the/i,
|
|
310
|
-
/(必须|需要).*?(遵守|符合|满足).*?(类型|测试|契约|接口|规范)/,
|
|
311
|
-
],
|
|
312
|
-
'T-03': [
|
|
313
|
-
/based on (the |this )?(evidence|logs?|output|error|stack trace|test result)/i,
|
|
314
|
-
/let me (check|verify|confirm|read|look at) (the |)(actual|source|code|file|log)/i,
|
|
315
|
-
/根据(日志|证据|输出|报错|堆栈|测试结果)/,
|
|
316
|
-
],
|
|
317
|
-
'T-04': [
|
|
318
|
-
/this (is|would be) (irreversible|destructive|permanent|not easily undone)/i,
|
|
319
|
-
/(reversible|can be undone|safely roll back)/i,
|
|
320
|
-
/(不可逆|破坏性|永久的|无法回滚|可以回滚|安全地撤销)/,
|
|
321
|
-
],
|
|
322
|
-
'T-05': [
|
|
323
|
-
/we (must|should) (not|never|avoid|prevent|ensure we don't)/i,
|
|
324
|
-
/(critical|important) (not to|that we don't|to avoid)/i,
|
|
325
|
-
/(绝不能|必须避免|不可以|禁止|确保不会)/,
|
|
326
|
-
],
|
|
327
|
-
'T-06': [
|
|
328
|
-
/(simpl(er|est|ify)|minimal|straightforward|lean) (approach|solution|fix|implementation)/i,
|
|
329
|
-
/(simple is better|keep it simple|no need to over)/i,
|
|
330
|
-
/(最简(单|洁)|精简|没有必要(过度|额外))/,
|
|
331
|
-
],
|
|
332
|
-
'T-07': [
|
|
333
|
-
/(minimal|smallest|narrowest|least) (change|diff|modification|impact)/i,
|
|
334
|
-
/only (change|modify|touch|edit) (the |what)/i,
|
|
335
|
-
/(最小(改动|变更|修改)|只(改|动|修))/,
|
|
336
|
-
],
|
|
337
|
-
'T-08': [
|
|
338
|
-
/this (error|failure|issue) (tells us|indicates|signals|suggests|means)/i,
|
|
339
|
-
/let me (stop|pause|step back|reconsider|rethink)/i,
|
|
340
|
-
/这个(错误|失败|问题)(告诉我们|表明|说明|意味)/,
|
|
341
|
-
/让我(停下|暂停|退一步|重新(考虑|思考|审视))/,
|
|
342
|
-
],
|
|
343
|
-
'T-09': [
|
|
344
|
-
/(break|split|decompose|divide) (this |the task |it )?(into|down)/i,
|
|
345
|
-
/(step 1|first,? (we|i|let's)|phase 1)/i,
|
|
346
|
-
/(拆分|分解|分步|分阶段|第一步)/,
|
|
347
|
-
],
|
|
348
|
-
};
|
|
349
|
-
function trackThinkingModelUsage(text, wctx, sessionId) {
|
|
348
|
+
function trackThinkingModelUsage(args) {
|
|
349
|
+
const { text, wctx, sessionId, runId, assistantTurnId, createdAt, logger } = args;
|
|
350
350
|
const logPath = wctx.resolve('THINKING_OS_USAGE');
|
|
351
351
|
const logDir = path.dirname(logPath);
|
|
352
352
|
if (!fs.existsSync(logDir))
|
|
@@ -360,27 +360,75 @@ function trackThinkingModelUsage(text, wctx, sessionId) {
|
|
|
360
360
|
console.error(`[PD:LLM] Failed to parse thinking OS usage log: ${String(e)}`);
|
|
361
361
|
}
|
|
362
362
|
}
|
|
363
|
-
|
|
364
|
-
for (const
|
|
365
|
-
|
|
366
|
-
if (pattern.test(text)) {
|
|
367
|
-
usageLog[modelId] = (usageLog[modelId] || 0) + 1;
|
|
368
|
-
anyMatch = true;
|
|
369
|
-
break;
|
|
370
|
-
}
|
|
371
|
-
}
|
|
363
|
+
const matches = detectThinkingModelMatches(text);
|
|
364
|
+
for (const match of matches) {
|
|
365
|
+
usageLog[match.modelId] = (usageLog[match.modelId] || 0) + 1;
|
|
372
366
|
}
|
|
373
367
|
usageLog['_total_turns'] = (usageLog['_total_turns'] || 0) + 1;
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
368
|
+
try {
|
|
369
|
+
fs.writeFileSync(logPath, JSON.stringify(usageLog, null, 2), 'utf8');
|
|
370
|
+
}
|
|
371
|
+
catch (e) {
|
|
372
|
+
console.error(`[PD:LLM] Failed to write thinking OS usage log: ${String(e)}`);
|
|
373
|
+
}
|
|
374
|
+
if (matches.length === 0) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (sessionId) {
|
|
378
|
+
recordThinkingCheckpoint(sessionId, wctx.workspaceDir);
|
|
379
|
+
}
|
|
380
|
+
if (!sessionId || !assistantTurnId) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const uiDb = new ControlUiDatabase({ workspaceDir: wctx.workspaceDir });
|
|
384
|
+
try {
|
|
385
|
+
const recentContext = uiDb.getRecentThinkingContext(sessionId, createdAt);
|
|
386
|
+
const toolContext = recentContext.toolCalls.map((call) => ({
|
|
387
|
+
toolName: call.toolName,
|
|
388
|
+
outcome: call.outcome,
|
|
389
|
+
errorType: call.errorType,
|
|
390
|
+
}));
|
|
391
|
+
const painContext = recentContext.painEvents.map((event) => ({
|
|
392
|
+
source: event.source,
|
|
393
|
+
score: event.score,
|
|
394
|
+
}));
|
|
395
|
+
const principleContext = recentContext.principleEvents.map((event) => ({
|
|
396
|
+
principleId: event.principleId,
|
|
397
|
+
eventType: event.eventType,
|
|
398
|
+
}));
|
|
399
|
+
const triggerExcerpt = text.length > 280 ? `${text.slice(0, 277)}...` : text;
|
|
400
|
+
for (const match of matches) {
|
|
401
|
+
const scenarios = deriveThinkingScenarios(match.modelId, {
|
|
402
|
+
recentToolCalls: toolContext,
|
|
403
|
+
recentPainEvents: painContext,
|
|
404
|
+
recentGateBlocks: recentContext.gateBlocks.map((block) => ({
|
|
405
|
+
toolName: block.toolName,
|
|
406
|
+
reason: block.reason,
|
|
407
|
+
})),
|
|
408
|
+
recentUserCorrections: recentContext.userCorrections.map((correction) => ({
|
|
409
|
+
correctionCue: correction.correctionCue,
|
|
410
|
+
})),
|
|
411
|
+
recentPrincipleEvents: principleContext,
|
|
412
|
+
});
|
|
413
|
+
uiDb.recordThinkingModelEvent({
|
|
414
|
+
sessionId,
|
|
415
|
+
runId,
|
|
416
|
+
assistantTurnId,
|
|
417
|
+
modelId: match.modelId,
|
|
418
|
+
matchedPattern: match.matchedPattern,
|
|
419
|
+
scenarioJson: scenarios,
|
|
420
|
+
toolContextJson: toolContext,
|
|
421
|
+
painContextJson: painContext,
|
|
422
|
+
principleContextJson: principleContext,
|
|
423
|
+
triggerExcerpt,
|
|
424
|
+
createdAt,
|
|
425
|
+
});
|
|
384
426
|
}
|
|
385
427
|
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
logger?.warn?.(`[PD:LLM] Failed to persist thinking model events: ${String(error)}`);
|
|
430
|
+
}
|
|
431
|
+
finally {
|
|
432
|
+
uiDb.dispose();
|
|
433
|
+
}
|
|
386
434
|
}
|