principles-disciple 1.6.0 → 1.7.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.
Files changed (67) hide show
  1. package/dist/commands/context.js +7 -3
  2. package/dist/commands/evolution-status.d.ts +4 -0
  3. package/dist/commands/evolution-status.js +138 -0
  4. package/dist/commands/export.d.ts +2 -0
  5. package/dist/commands/export.js +45 -0
  6. package/dist/commands/focus.js +9 -6
  7. package/dist/commands/pain.js +8 -0
  8. package/dist/commands/principle-rollback.d.ts +4 -0
  9. package/dist/commands/principle-rollback.js +22 -0
  10. package/dist/commands/samples.d.ts +2 -0
  11. package/dist/commands/samples.js +55 -0
  12. package/dist/core/config.d.ts +5 -0
  13. package/dist/core/control-ui-db.d.ts +68 -0
  14. package/dist/core/control-ui-db.js +274 -0
  15. package/dist/core/detection-funnel.d.ts +1 -1
  16. package/dist/core/detection-funnel.js +4 -0
  17. package/dist/core/dictionary.d.ts +2 -0
  18. package/dist/core/dictionary.js +13 -0
  19. package/dist/core/event-log.d.ts +2 -1
  20. package/dist/core/event-log.js +3 -0
  21. package/dist/core/evolution-engine.d.ts +5 -5
  22. package/dist/core/evolution-engine.js +18 -18
  23. package/dist/core/evolution-migration.d.ts +5 -0
  24. package/dist/core/evolution-migration.js +65 -0
  25. package/dist/core/evolution-reducer.d.ts +69 -0
  26. package/dist/core/evolution-reducer.js +369 -0
  27. package/dist/core/evolution-types.d.ts +103 -0
  28. package/dist/core/path-resolver.js +75 -36
  29. package/dist/core/paths.d.ts +7 -8
  30. package/dist/core/paths.js +48 -40
  31. package/dist/core/profile.js +1 -1
  32. package/dist/core/session-tracker.d.ts +4 -0
  33. package/dist/core/session-tracker.js +15 -0
  34. package/dist/core/thinking-models.d.ts +38 -0
  35. package/dist/core/thinking-models.js +170 -0
  36. package/dist/core/trajectory.d.ts +184 -0
  37. package/dist/core/trajectory.js +817 -0
  38. package/dist/core/trust-engine.d.ts +2 -0
  39. package/dist/core/trust-engine.js +30 -4
  40. package/dist/core/workspace-context.d.ts +13 -0
  41. package/dist/core/workspace-context.js +50 -7
  42. package/dist/hooks/gate.js +117 -48
  43. package/dist/hooks/llm.js +114 -69
  44. package/dist/hooks/pain.js +105 -5
  45. package/dist/hooks/prompt.d.ts +11 -14
  46. package/dist/hooks/prompt.js +283 -57
  47. package/dist/hooks/subagent.js +27 -1
  48. package/dist/http/principles-console-route.d.ts +2 -0
  49. package/dist/http/principles-console-route.js +257 -0
  50. package/dist/i18n/commands.js +16 -0
  51. package/dist/index.js +83 -4
  52. package/dist/service/control-ui-query-service.d.ts +217 -0
  53. package/dist/service/control-ui-query-service.js +537 -0
  54. package/dist/service/evolution-worker.d.ts +9 -0
  55. package/dist/service/evolution-worker.js +152 -22
  56. package/dist/service/trajectory-service.d.ts +2 -0
  57. package/dist/service/trajectory-service.js +15 -0
  58. package/dist/tools/agent-spawn.d.ts +27 -6
  59. package/dist/tools/agent-spawn.js +339 -87
  60. package/dist/tools/deep-reflect.d.ts +27 -7
  61. package/dist/tools/deep-reflect.js +210 -121
  62. package/dist/types/event-types.d.ts +9 -2
  63. package/dist/types.d.ts +10 -0
  64. package/dist/types.js +5 -0
  65. package/openclaw.plugin.json +43 -11
  66. package/package.json +14 -4
  67. package/templates/langs/zh/skills/pd-daily/SKILL.md +97 -13
@@ -43,6 +43,7 @@ export declare class TrustEngine {
43
43
  sessionId?: string;
44
44
  api?: any;
45
45
  toolName?: string;
46
+ error?: string;
46
47
  }): void;
47
48
  private updateScore;
48
49
  resetTrust(newScore?: number): void;
@@ -67,6 +68,7 @@ export declare function recordFailure(type: 'tool' | 'risky' | 'bypass', workspa
67
68
  sessionId?: string;
68
69
  api?: any;
69
70
  toolName?: string;
71
+ error?: string;
70
72
  }): void;
71
73
  export declare function getAgentScorecard(workspaceDir: string): TrustScorecard;
72
74
  export declare function getTrustStats(scorecard: TrustScorecard): {
@@ -7,6 +7,7 @@ import * as path from 'path';
7
7
  import { EventLogService } from './event-log.js';
8
8
  import { resolvePdPath } from './paths.js';
9
9
  import { ConfigService } from './config-service.js';
10
+ import { TrajectoryRegistry } from './trajectory.js';
10
11
  export const EXPLORATORY_TOOLS = [
11
12
  // 文件读取
12
13
  'read', 'read_file', 'read_many_files', 'image_read',
@@ -26,7 +27,7 @@ export const EXPLORATORY_TOOLS = [
26
27
  export const CONSTRUCTIVE_TOOLS = [
27
28
  'write', 'write_file', 'edit', 'edit_file', 'replace', 'apply_patch',
28
29
  'insert', 'patch', 'delete_file', 'move_file', 'run_shell_command',
29
- 'pd_spawn_agent', 'sessions_spawn', 'evolve-task', 'init-strategy'
30
+ 'pd_run_worker', 'sessions_spawn', 'evolve-task', 'init-strategy'
30
31
  ];
31
32
  export class TrustEngine {
32
33
  scorecard;
@@ -49,7 +50,8 @@ export class TrustEngine {
49
50
  return settings || {
50
51
  stages: { stage_1_observer: 30, stage_2_editor: 60, stage_3_developer: 80 },
51
52
  cold_start: { initial_trust: 85, grace_failures: 5, cold_start_period_ms: 86400000 },
52
- penalties: { tool_failure_base: -2, risky_failure_base: -10, gate_bypass_attempt: -5, failure_streak_multiplier: -2, max_penalty: -20 },
53
+ // BUGFIX #84: Reduced penalties to prevent Trust collapse
54
+ penalties: { tool_failure_base: -1, risky_failure_base: -5, gate_bypass_attempt: -3, failure_streak_multiplier: -1, max_penalty: -10 },
53
55
  rewards: { success_base: 2, subagent_success: 5, tool_success_reward: 0.2, streak_bonus_threshold: 3, streak_bonus: 5, recovery_boost: 5, max_reward: 15 },
54
56
  limits: { stage_2_max_lines: 50, stage_3_max_lines: 300 }
55
57
  };
@@ -170,6 +172,13 @@ export class TrustEngine {
170
172
  this.updateScore(-1, `Exploratory Failure: ${toolName}`, 'failure', context);
171
173
  return;
172
174
  }
175
+ // BUGFIX #84: sessions_send timeout should not be penalized
176
+ // Communication timeouts are not agent failures - the message may have been delivered
177
+ const errorStr = String(context?.error || '');
178
+ if (toolName === 'sessions_send' && (errorStr.includes('timeout') || errorStr === 'timeout')) {
179
+ this.updateScore(0, `Communication timeout (sessions_send): ignored`, 'info', context);
180
+ return;
181
+ }
173
182
  // 3. Constructive Failure (Risky or failed writes)
174
183
  let delta = 0;
175
184
  switch (type) {
@@ -198,8 +207,9 @@ export class TrustEngine {
198
207
  updateScore(delta, reason, type, context) {
199
208
  const oldScore = this.scorecard.trust_score;
200
209
  this.scorecard.trust_score += delta;
201
- if (this.scorecard.trust_score < 0)
202
- this.scorecard.trust_score = 0;
210
+ // Floor score: never drop below 30 (prevents Trust collapse from cascades)
211
+ if (this.scorecard.trust_score < 30)
212
+ this.scorecard.trust_score = 30;
203
213
  if (this.scorecard.trust_score > 100)
204
214
  this.scorecard.trust_score = 100;
205
215
  this.scorecard.last_updated = new Date().toISOString();
@@ -210,6 +220,22 @@ export class TrustEngine {
210
220
  const eventLog = EventLogService.get(this.stateDir);
211
221
  eventLog.recordTrustChange(context.sessionId, { previousScore: oldScore, newScore: this.scorecard.trust_score, delta, reason });
212
222
  }
223
+ if (context?.sessionId) {
224
+ try {
225
+ TrajectoryRegistry.use(this.workspaceDir, (trajectory) => {
226
+ trajectory.recordTrustChange({
227
+ sessionId: context.sessionId,
228
+ previousScore: oldScore,
229
+ newScore: this.scorecard.trust_score,
230
+ delta,
231
+ reason,
232
+ });
233
+ });
234
+ }
235
+ catch {
236
+ // Do not block trust updates if trajectory storage is unavailable.
237
+ }
238
+ }
213
239
  const limit = this.trustSettings.history_limit || 50;
214
240
  if (this.scorecard.history.length > limit) {
215
241
  this.scorecard.history.shift();
@@ -4,6 +4,8 @@ import { EventLog } from './event-log.js';
4
4
  import { PainDictionary } from './dictionary.js';
5
5
  import { TrustEngine } from './trust-engine.js';
6
6
  import { HygieneTracker } from './hygiene/tracker.js';
7
+ import { EvolutionReducerImpl } from './evolution-reducer.js';
8
+ import { TrajectoryDatabase } from './trajectory.js';
7
9
  /**
8
10
  * WorkspaceContext - Centralized management of workspace-specific paths and services.
9
11
  * Implements a cached singleton pattern per workspace directory.
@@ -18,6 +20,8 @@ export declare class WorkspaceContext {
18
20
  private _dictionary?;
19
21
  private _trust?;
20
22
  private _hygiene?;
23
+ private _evolutionReducer?;
24
+ private _trajectory?;
21
25
  private constructor();
22
26
  /**
23
27
  * Governance configuration for this workspace.
@@ -39,6 +43,15 @@ export declare class WorkspaceContext {
39
43
  * Hygiene tracking service for this workspace.
40
44
  */
41
45
  get hygiene(): HygieneTracker;
46
+ /**
47
+ * Evolution reducer singleton for this workspace.
48
+ */
49
+ get evolutionReducer(): EvolutionReducerImpl;
50
+ /**
51
+ * Trajectory database for analytics and sample curation.
52
+ */
53
+ get trajectory(): TrajectoryDatabase;
54
+ private getTrajectoryOptions;
42
55
  /**
43
56
  * Creates or retrieves a WorkspaceContext instance from an OpenClaw hook context.
44
57
  * Uses PathResolver to handle path normalization and fallback logic.
@@ -5,6 +5,8 @@ import { EventLogService } from './event-log.js';
5
5
  import { DictionaryService } from './dictionary-service.js';
6
6
  import { TrustEngine } from './trust-engine.js';
7
7
  import { HygieneTracker } from './hygiene/tracker.js';
8
+ import { EvolutionReducerImpl } from './evolution-reducer.js';
9
+ import { TrajectoryRegistry } from './trajectory.js';
8
10
  /**
9
11
  * WorkspaceContext - Centralized management of workspace-specific paths and services.
10
12
  * Implements a cached singleton pattern per workspace directory.
@@ -19,6 +21,8 @@ export class WorkspaceContext {
19
21
  _dictionary;
20
22
  _trust;
21
23
  _hygiene;
24
+ _evolutionReducer;
25
+ _trajectory;
22
26
  constructor(workspaceDir, stateDir) {
23
27
  this.workspaceDir = workspaceDir;
24
28
  this.stateDir = stateDir;
@@ -68,22 +72,53 @@ export class WorkspaceContext {
68
72
  }
69
73
  return this._hygiene;
70
74
  }
75
+ /**
76
+ * Evolution reducer singleton for this workspace.
77
+ */
78
+ get evolutionReducer() {
79
+ if (!this._evolutionReducer) {
80
+ this._evolutionReducer = new EvolutionReducerImpl({ workspaceDir: this.workspaceDir });
81
+ }
82
+ return this._evolutionReducer;
83
+ }
84
+ /**
85
+ * Trajectory database for analytics and sample curation.
86
+ */
87
+ get trajectory() {
88
+ if (!this._trajectory) {
89
+ this._trajectory = TrajectoryRegistry.get(this.workspaceDir, this.getTrajectoryOptions());
90
+ }
91
+ return this._trajectory;
92
+ }
93
+ getTrajectoryOptions() {
94
+ const inlineThreshold = Number(this.config.get('trajectory.blob_inline_threshold_bytes'));
95
+ const busyTimeoutMs = Number(this.config.get('trajectory.busy_timeout_ms'));
96
+ const orphanBlobGraceDays = Number(this.config.get('trajectory.orphan_blob_grace_days'));
97
+ return {
98
+ blobInlineThresholdBytes: Number.isFinite(inlineThreshold) && inlineThreshold > 0 ? inlineThreshold : undefined,
99
+ busyTimeoutMs: Number.isFinite(busyTimeoutMs) && busyTimeoutMs >= 0 ? busyTimeoutMs : undefined,
100
+ orphanBlobGraceDays: Number.isFinite(orphanBlobGraceDays) && orphanBlobGraceDays >= 0 ? orphanBlobGraceDays : undefined,
101
+ };
102
+ }
71
103
  /**
72
104
  * Creates or retrieves a WorkspaceContext instance from an OpenClaw hook context.
73
105
  * Uses PathResolver to handle path normalization and fallback logic.
74
106
  * @throws Error if workspaceDir is missing and no fallback available.
75
107
  */
76
108
  static fromHookContext(ctx) {
109
+ const logger = ctx.logger;
110
+ const log = (msg) => logger?.info?.(msg) ?? console.log(msg);
111
+ const logWarn = (msg) => logger?.warn?.(msg) ?? console.warn(msg);
77
112
  let workspaceDir = ctx.workspaceDir;
78
113
  if (!workspaceDir) {
79
- console.warn('[PD:WorkspaceContext] workspaceDir not provided in context, using PathResolver fallback');
114
+ logWarn('[PD:WorkspaceContext] workspaceDir not provided in context, using PathResolver fallback');
80
115
  workspaceDir = this.pathResolver.getWorkspaceDir();
81
- console.log(`[PD:WorkspaceContext] Resolved workspaceDir to: ${workspaceDir}`);
116
+ log(`[PD:WorkspaceContext] Resolved workspaceDir to: ${workspaceDir}`);
82
117
  }
83
118
  else {
84
119
  const normalized = this.pathResolver.normalizeWorkspacePath(workspaceDir);
85
120
  if (normalized !== workspaceDir) {
86
- console.log(`[PD:WorkspaceContext] Normalized workspaceDir: ${workspaceDir} -> ${normalized}`);
121
+ log(`[PD:WorkspaceContext] Normalized workspaceDir: ${workspaceDir} -> ${normalized}`);
87
122
  workspaceDir = normalized;
88
123
  }
89
124
  }
@@ -93,11 +128,11 @@ export class WorkspaceContext {
93
128
  let stateDir = ctx.stateDir;
94
129
  if (!stateDir) {
95
130
  stateDir = resolvePdPath(workspaceDir, 'STATE_DIR');
96
- console.log(`[PD:WorkspaceContext] Computed stateDir: ${stateDir}`);
131
+ log(`[PD:WorkspaceContext] Computed stateDir: ${stateDir}`);
97
132
  }
98
133
  const instance = new WorkspaceContext(workspaceDir, stateDir);
99
134
  this.instances.set(workspaceDir, instance);
100
- console.log(`[PD:WorkspaceContext] Created new context for workspace: ${workspaceDir}`);
135
+ log(`[PD:WorkspaceContext] Created new context for workspace: ${workspaceDir}`);
101
136
  return instance;
102
137
  }
103
138
  /**
@@ -114,21 +149,29 @@ export class WorkspaceContext {
114
149
  this._eventLog = undefined;
115
150
  this._dictionary = undefined;
116
151
  this._trust = undefined;
152
+ this._evolutionReducer = undefined;
153
+ this._trajectory = undefined;
117
154
  }
118
155
  /**
119
156
  * Removes a workspace from the cache.
120
157
  */
121
158
  static dispose(workspaceDir) {
122
- const instance = this.instances.get(workspaceDir);
159
+ const normalized = this.pathResolver.normalizeWorkspacePath(workspaceDir);
160
+ const instance = this.instances.get(normalized);
123
161
  if (instance) {
124
162
  instance.invalidate();
125
- this.instances.delete(workspaceDir);
163
+ this.instances.delete(normalized);
126
164
  }
165
+ TrajectoryRegistry.dispose(normalized);
127
166
  }
128
167
  /**
129
168
  * Clears the instance cache (primarily for testing).
130
169
  */
131
170
  static clearCache() {
171
+ for (const instance of this.instances.values()) {
172
+ instance.invalidate();
173
+ }
132
174
  this.instances.clear();
175
+ TrajectoryRegistry.clear();
133
176
  }
134
177
  }
@@ -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
- // 注意:pd_spawn_agent、sessions_spawn、task 是 Agent 派生工具,不应被 GFI Gate 拦截
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',
@@ -37,31 +38,66 @@ const BASH_TOOLS_SET = new Set([
37
38
  * 分析 Bash 命令风险等级
38
39
  */
39
40
  function analyzeBashCommand(command, safePatterns, dangerousPatterns) {
40
- const normalizedCmd = command.trim().toLowerCase();
41
- // 1. 优先检查危险命令
42
- for (const pattern of dangerousPatterns) {
43
- try {
44
- if (new RegExp(pattern, 'i').test(normalizedCmd)) {
45
- return 'dangerous';
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 {
76
+ // 忽略无效正则
46
77
  }
47
- }
48
- catch {
49
- // 忽略无效正则
50
78
  }
51
79
  }
52
- // 2. 检查安全命令
53
- for (const pattern of safePatterns) {
54
- try {
55
- if (new RegExp(pattern, 'i').test(normalizedCmd)) {
56
- return 'safe';
80
+ // 2. Check safe patterns (only if ALL segments are safe)
81
+ for (const seg of cleanSegments) {
82
+ let isSafe = false;
83
+ for (const pattern of safePatterns) {
84
+ try {
85
+ if (new RegExp(pattern, 'i').test(seg)) {
86
+ isSafe = true;
87
+ break;
88
+ }
89
+ }
90
+ catch {
91
+ // ignore
57
92
  }
58
93
  }
59
- catch {
60
- // 忽略无效正则
94
+ if (!isSafe) {
95
+ // Not all segments are safe → treat as normal
96
+ return 'normal';
61
97
  }
62
98
  }
63
- // 3. 默认为普通命令
64
- return 'normal';
99
+ // All segments are safe
100
+ return 'safe';
65
101
  }
66
102
  /**
67
103
  * 计算动态 GFI 阈值
@@ -82,7 +118,7 @@ export function handleBeforeToolCall(event, ctx) {
82
118
  // 1. Identify tool type
83
119
  const WRITE_TOOLS = ['write', 'edit', 'apply_patch', 'write_file', 'replace', 'insert', 'patch', 'edit_file', 'delete_file', 'move_file'];
84
120
  const BASH_TOOLS = ['bash', 'run_shell_command', 'exec', 'execute', 'shell', 'cmd'];
85
- const AGENT_TOOLS = ['pd_spawn_agent', 'sessions_spawn'];
121
+ const AGENT_TOOLS = ['pd_run_worker', 'sessions_spawn'];
86
122
  const isBash = BASH_TOOLS.includes(event.toolName);
87
123
  const isWriteTool = WRITE_TOOLS.includes(event.toolName);
88
124
  const isAgentTool = AGENT_TOOLS.includes(event.toolName);
@@ -115,7 +151,7 @@ export function handleBeforeToolCall(event, ctx) {
115
151
  thinking_checkpoint: {
116
152
  enabled: false, // Default OFF
117
153
  window_ms: 5 * 60 * 1000,
118
- high_risk_tools: ['run_shell_command', 'delete_file', 'move_file', 'pd_spawn_agent'],
154
+ high_risk_tools: ['run_shell_command', 'delete_file', 'move_file', 'pd_run_worker'],
119
155
  }
120
156
  };
121
157
  if (fs.existsSync(profilePath)) {
@@ -272,6 +308,26 @@ GFI: ${currentGfi}/100
272
308
  };
273
309
  }
274
310
  }
311
+ // AGENT_TOOLS: Block subagent spawn when GFI is critically high (P0 fix: prevent privilege escalation via spawned subagents)
312
+ if (isAgentTool) {
313
+ const AGENT_SPAWN_GFI_THRESHOLD = 90;
314
+ if (currentGfi >= AGENT_SPAWN_GFI_THRESHOLD) {
315
+ logger?.warn?.(`[PD:GFI_GATE] Agent tool "${event.toolName}" blocked by GFI: ${currentGfi} >= ${AGENT_SPAWN_GFI_THRESHOLD}`);
316
+ return {
317
+ block: true,
318
+ blockReason: `[GFI Gate] 疲劳指数过高,禁止派生子智能体。
319
+
320
+ GFI: ${currentGfi}/100
321
+ 阈值: ${AGENT_SPAWN_GFI_THRESHOLD} (Stage ${wctx.trust.getStage()})
322
+
323
+ 原因: 高疲劳状态下派生子智能体会放大错误风险。
324
+
325
+ 解决方案:
326
+ 1. 执行 /pd-status reset 清零疲劳值
327
+ 2. 简化任务后重试`,
328
+ };
329
+ }
330
+ }
275
331
  }
276
332
  // Merge pluginConfig (OpenClaw UI settings)
277
333
  const configRiskPaths = ctx.pluginConfig?.riskPaths ?? [];
@@ -314,37 +370,35 @@ GFI: ${currentGfi}/100
314
370
  };
315
371
  const riskLevel = assessRiskLevel(relPath, { toolName: event.toolName, params: event.params }, profile.risk_paths);
316
372
  const lineChanges = estimateLineChanges({ toolName: event.toolName, params: event.params });
373
+ const planApprovals = profile.progressive_gate?.plan_approvals;
374
+ const canUsePlanApproval = Boolean(stage === 1 &&
375
+ planApprovals?.enabled &&
376
+ getPlanStatus(ctx.workspaceDir) === 'READY' &&
377
+ planApprovals.allowed_operations?.includes(event.toolName) &&
378
+ matchesAnyPattern(relPath, planApprovals.allowed_patterns || []) &&
379
+ ((planApprovals.max_lines_override ?? -1) === -1 || lineChanges <= (planApprovals.max_lines_override ?? -1)));
317
380
  logger.info(`[PD_GATE] Trust: ${trustScore} (Stage ${stage}), Risk: ${riskLevel}, Path: ${relPath}`);
318
381
  // Stage 1 (Bankruptcy): Block ALL writes to risk paths, and all medium+ writes
319
382
  if (stage === 1) {
383
+ if (canUsePlanApproval) {
384
+ const planStatus = 'READY';
385
+ wctx.eventLog.recordPlanApproval(ctx.sessionId, {
386
+ toolName: event.toolName,
387
+ filePath: relPath,
388
+ pattern: relPath,
389
+ planStatus
390
+ });
391
+ wctx.trajectory?.recordGateBlock?.({
392
+ sessionId: ctx.sessionId,
393
+ toolName: event.toolName,
394
+ filePath: relPath,
395
+ reason: 'plan_approval',
396
+ planStatus,
397
+ });
398
+ logger.info(`[PD_GATE] Stage 1 PLAN approval: ${relPath}`);
399
+ return;
400
+ }
320
401
  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
402
  // Block if not approved by whitelist
349
403
  return block(relPath, `Trust score too low (${trustScore}). Stage 1 agents cannot modify risk paths or perform non-trivial edits.`, wctx, event.toolName);
350
404
  }
@@ -375,6 +429,21 @@ GFI: ${currentGfi}/100
375
429
  // Stage 4 (Architect): Full bypass
376
430
  if (stage === 4) {
377
431
  logger.info(`[PD_GATE] Trusted Architect bypass for ${relPath}`);
432
+ // Audit log for Stage 4 bypass (security traceability)
433
+ try {
434
+ const stateDir = wctx.resolve('STATE_DIR');
435
+ const eventLog = EventLogService.get(stateDir);
436
+ eventLog.recordGateBypass(ctx.sessionId, {
437
+ toolName: event.toolName,
438
+ filePath: relPath,
439
+ bypassType: 'stage4_architect',
440
+ trustScore,
441
+ trustStage: stage,
442
+ });
443
+ }
444
+ catch (auditErr) {
445
+ logger?.warn?.(`[PD_GATE] Failed to record Stage 4 bypass audit: ${String(auditErr)}`);
446
+ }
378
447
  return;
379
448
  }
380
449
  // ── EP SIMULATION MODE (M6验证) ──