principles-disciple 1.62.0 → 1.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@
2
2
  "id": "principles-disciple",
3
3
  "name": "Principles Disciple",
4
4
  "description": "Evolutionary programming agent framework with strategic guardrails and reflection loops.",
5
- "version": "1.62.0",
5
+ "version": "1.64.0",
6
6
  "skills": [
7
7
  "./skills"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "principles-disciple",
3
- "version": "1.62.0",
3
+ "version": "1.64.0",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/bundle.js",
@@ -1,5 +1,8 @@
1
+ import * as path from 'path';
1
2
  import type { EvolutionReducerImpl } from '../core/evolution-reducer.js';
2
3
  import type { InternalizationRouteRecommendation } from '../core/principle-internalization/internalization-routing-policy.js';
4
+ import { WorkflowFunnelLoader } from '../core/workflow-funnel-loader.js';
5
+ import { resolvePdPath } from '../core/paths.js';
3
6
  import { WorkspaceContext } from '../core/workspace-context.js';
4
7
  import { normalizeLanguage } from '../i18n/commands.js';
5
8
  import type { PluginCommandContext } from '../openclaw-sdk.js';
@@ -175,18 +178,35 @@ export function handleEvolutionStatusCommand(ctx: PluginCommandContext): { text:
175
178
  const wctx = WorkspaceContext.fromHookContext({ workspaceDir });
176
179
  const reducer = wctx.evolutionReducer;
177
180
  const stats = reducer.getStats();
178
- const summary = RuntimeSummaryService.getSummary(workspaceDir, { sessionId });
179
- const recommendations = WorkspaceContext.fromHookContext({ workspaceDir })
180
- .principleLifecycle
181
- .recomputeAll()
182
- .map((assessment) => assessment.routeRecommendation);
183
- const rawLang = (ctx.config?.language as string) || 'en';
184
- const lang = normalizeLanguage(rawLang);
185
- const warnings = summary.metadata.warnings.slice(0, 12);
181
+ // D-12 / YAML-FUNNEL-02: WorkflowFunnelLoader owns funnel lifecycle per workspace
182
+ const stateDir = path.dirname(resolvePdPath(workspaceDir, 'WORKFLOWS_YAML'));
183
+ const loader = new WorkflowFunnelLoader(stateDir);
184
+ loader.watch();
185
+ try {
186
+ const summary = RuntimeSummaryService.getSummary(workspaceDir, { sessionId, loaderWarnings: loader.getWarnings() });
187
+ const recommendations = WorkspaceContext.fromHookContext({ workspaceDir })
188
+ .principleLifecycle
189
+ .recomputeAll()
190
+ .map((assessment) => assessment.routeRecommendation);
191
+ const rawLang = (ctx.config?.language as string) || 'en';
192
+ const lang = normalizeLanguage(rawLang);
193
+ const warnings = summary.metadata.warnings.slice(0, 12);
194
+
195
+ if (lang === 'zh') {
196
+ return {
197
+ text: buildChineseOutput(
198
+ workspaceDir,
199
+ summary.metadata.sessionId,
200
+ warnings,
201
+ stats,
202
+ summary,
203
+ recommendations,
204
+ ),
205
+ };
206
+ }
186
207
 
187
- if (lang === 'zh') {
188
208
  return {
189
- text: buildChineseOutput(
209
+ text: buildEnglishOutput(
190
210
  workspaceDir,
191
211
  summary.metadata.sessionId,
192
212
  warnings,
@@ -195,16 +215,7 @@ export function handleEvolutionStatusCommand(ctx: PluginCommandContext): { text:
195
215
  recommendations,
196
216
  ),
197
217
  };
218
+ } finally {
219
+ loader.dispose(); // YAML-FUNNEL-02: guarantee cleanup on all exit paths
198
220
  }
199
-
200
- return {
201
- text: buildEnglishOutput(
202
- workspaceDir,
203
- summary.metadata.sessionId,
204
- warnings,
205
- stats,
206
- summary,
207
- recommendations,
208
- ),
209
- };
210
221
  }
package/src/core/paths.ts CHANGED
@@ -62,6 +62,7 @@ export const PD_FILES = {
62
62
  SESSION_DIR: PD_DIRS.SESSIONS,
63
63
  DICTIONARY: posixJoin(PD_DIRS.STATE, 'pain_dictionary.json'),
64
64
  PRINCIPLE_BLACKLIST: posixJoin(PD_DIRS.STATE, 'principle_blacklist.json'),
65
+ WORKFLOWS_YAML: posixJoin(PD_DIRS.STATE, 'workflows.yaml'),
65
66
  NOCTURNAL_SAMPLES_DIR: PD_DIRS.NOCTURNAL_SAMPLES,
66
67
  NOCTURNAL_MEMORY_DIR: PD_DIRS.NOCTURNAL_MEMORY,
67
68
  NOCTURNAL_EXPORTS_DIR: PD_DIRS.NOCTURNAL_EXPORTS,
@@ -72,6 +72,9 @@ export class WorkflowFunnelLoader {
72
72
  /** fs.watch() handle for cleanup */
73
73
  private watchHandle?: fs.FSWatcher;
74
74
 
75
+ /** YAML parse warnings from last load() call */
76
+ private readonly warnings: string[] = [];
77
+
75
78
  constructor(stateDir: string) {
76
79
  // D-02: workflows.yaml in .state/ directory
77
80
  this.configPath = path.join(stateDir, 'workflows.yaml');
@@ -84,7 +87,9 @@ export class WorkflowFunnelLoader {
84
87
  * On missing file, clears to empty.
85
88
  */
86
89
  load(): void {
90
+ this.warnings.length = 0; // reset warnings on each load
87
91
  if (!fs.existsSync(this.configPath)) {
92
+ this.warnings.push('workflows.yaml file not found.');
88
93
  this.funnels.clear();
89
94
  return;
90
95
  }
@@ -96,7 +101,9 @@ export class WorkflowFunnelLoader {
96
101
 
97
102
  // Validate top-level structure
98
103
  if (!config || typeof config.version !== 'string' || !Array.isArray(config.funnels)) {
99
- console.warn(`[WorkflowFunnelLoader] workflows.yaml validation failed: missing version or funnels array. Preserving last valid config.`);
104
+ const msg = 'workflows.yaml validation failed: missing version or funnels array. Preserving last valid config.';
105
+ console.warn(`[WorkflowFunnelLoader] ${msg}`);
106
+ this.warnings.push(msg);
100
107
  return;
101
108
  }
102
109
 
@@ -106,7 +113,9 @@ export class WorkflowFunnelLoader {
106
113
  if (funnel?.workflowId && typeof funnel.workflowId === 'string' && Array.isArray(funnel.stages)) {
107
114
  newFunnels.set(funnel.workflowId, funnel.stages);
108
115
  } else {
109
- console.warn(`[WorkflowFunnelLoader] Skipping invalid funnel entry: missing workflowId or stages.`);
116
+ const msg = 'Skipping invalid funnel entry: missing workflowId or stages.';
117
+ console.warn(`[WorkflowFunnelLoader] ${msg}`);
118
+ this.warnings.push(msg);
110
119
  }
111
120
  }
112
121
 
@@ -117,19 +126,27 @@ export class WorkflowFunnelLoader {
117
126
  }
118
127
  } catch (err) {
119
128
  // Best-effort: preserve last known-good config on parse error
120
- console.warn(`[WorkflowFunnelLoader] Failed to parse workflows.yaml: ${String(err)}. Preserving last valid config.`);
129
+ const msg = `Failed to parse workflows.yaml: ${String(err)}. Preserving last valid config.`;
130
+ console.warn(`[WorkflowFunnelLoader] ${msg}`);
131
+ this.warnings.push(msg);
121
132
  }
122
133
  }
123
134
 
124
135
  /**
125
136
  * Start watching workflows.yaml for changes.
126
137
  * Calls load() automatically when the file changes.
138
+ * No-op if the config file does not exist.
127
139
  */
128
140
  watch(): void {
141
+ // WATCHER-01: re-entry guard — prevent FSWatcher leak on double-watch
142
+ if (this.watchHandle) return;
143
+ // Guard: fs.watch fails with ENOENT if the path does not exist
144
+ if (!fs.existsSync(this.configPath)) return;
129
145
  // Debounce: only re-read after file write settles (100ms)
130
146
  let debounceTimer: ReturnType<typeof setTimeout> | undefined;
131
147
  this.watchHandle = fs.watch(this.configPath, (eventType) => {
132
- if (eventType !== 'change') return;
148
+ // PLAT-01: handle both 'change' and 'rename' events for Windows compatibility
149
+ if (eventType !== 'change' && eventType !== 'rename') return;
133
150
  if (debounceTimer) clearTimeout(debounceTimer);
134
151
  debounceTimer = setTimeout(() => {
135
152
  this.load();
@@ -156,9 +173,23 @@ export class WorkflowFunnelLoader {
156
173
 
157
174
  /**
158
175
  * Get the full WORKFLOW_FUNNELS table.
176
+ * Returns a deep clone — consumer mutations do not affect internal state.
159
177
  */
160
178
  getAllFunnels(): Map<string, WorkflowStage[]> {
161
- return new Map(this.funnels);
179
+ const result = new Map<string, WorkflowStage[]>();
180
+ for (const [k, v] of this.funnels) {
181
+ // WATCHER-03: deep-clone arrays and stage objects
182
+ result.set(k, v.map(stage => ({ ...stage })));
183
+ }
184
+ return result;
185
+ }
186
+
187
+ /**
188
+ * Returns warnings from the last load() call.
189
+ * Callers can inspect these and propagate them to metadata.warnings.
190
+ */
191
+ getWarnings(): string[] {
192
+ return [...this.warnings];
162
193
  }
163
194
 
164
195
  /**
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * PURPOSE: Provide ONE authoritative implementation for gate block persistence.
5
5
  *
6
- * All gate modules (progressive-trust-gate, gfi-gate, etc.) must use this
6
+ * All gate sources (rule-host) must use this
7
7
  * helper to ensure consistent block tracking, event logging, and retry behavior.
8
8
  *
9
9
  * This eliminates the "multi-truth source" problem where different modules
package/src/hooks/gate.ts CHANGED
@@ -1,45 +1,27 @@
1
1
  /**
2
- * Security Gate Hook - Orchestration Layer
2
+ * Security Gate Hook - Rule Host Only
3
3
  *
4
- * HOOK CHAIN PRIORITY (short-circuits on first block):
4
+ * This is the SINGLE AUTHORITATIVE orchestration path.
5
+ * All blocking logic is now dynamic via Rule Host — no hardcoded gates remain.
5
6
  *
7
+ * Flow:
6
8
  * 1. Early Return: Skip if not write/bash/agent tool or no workspace
7
- * 2. Thinking OS Checkpoint (P-10): Deep reflection enforcement
8
- * 3. GFI Gate: Fatigue index-based blocking
9
- * 4. Bash Mutation Detection: Heuristic for bash file modifications
10
- * 4.5. Rule Host: Active code implementation evaluation (Phase 12)
11
- * 5. Progressive Gate: EP tier-based access control
12
- * 6. Edit Verification (P-03): Exact/fuzzy match for edit operations
13
- *
14
- * IMPORTANT: This is the SINGLE AUTHORITATIVE orchestration path.
15
- * All policy modules (gfi-gate, progressive-trust-gate, rule-host) use the shared
16
- * `recordGateBlockAndReturn` helper to ensure consistent block persistence.
17
- *
18
- * Zero-width character detection is handled in bash-risk.ts.
9
+ * 2. Rule Host: Dynamic principle-based evaluation (sole gate)
19
10
  */
20
11
 
21
12
  import * as fs from 'fs';
22
13
  import * as path from 'path';
23
- import { isRisky, normalizePath, planStatus as getPlanStatus } from '../utils/io.js';
24
- import { normalizeProfile } from '../core/profile.js';
25
- import { estimateLineChanges } from '../core/risk-calculator.js';
14
+ import { normalizePath, planStatus } from '../utils/io.js';
26
15
  import { WorkspaceContext } from '../core/workspace-context.js';
27
- import { checkThinkingCheckpoint } from './thinking-checkpoint.js';
28
- import { handleEditVerification } from './edit-verification.js';
29
- import { checkGfiGate } from './gfi-gate.js';
30
- import { checkProgressiveTrustGate } from './progressive-trust-gate.js';
31
16
  import { recordGateBlockAndReturn } from './gate-block-helper.js';
32
17
  import { RuleHost } from '../core/rule-host.js';
33
18
  import type { RuleHostInput } from '../core/rule-host-types.js';
34
19
  import type { PluginHookBeforeToolCallEvent, PluginHookToolContext, PluginHookBeforeToolCallResult, PluginLogger } from '../openclaw-sdk.js';
35
- import {
36
- AGENT_TOOLS,
37
- BASH_TOOLS_SET,
38
- WRITE_TOOLS,
39
- } from '../constants/tools.js';
20
+ import { AGENT_TOOLS, BASH_TOOLS_SET, WRITE_TOOLS } from '../constants/tools.js';
40
21
  import { getSession, hasRecentThinking } from '../core/session-tracker.js';
41
22
  import { getEvolutionEngine } from '../core/evolution-engine.js';
42
23
  import { EventLogService } from '../core/event-log.js';
24
+ import { estimateLineChanges } from '../core/risk-calculator.js';
43
25
 
44
26
  export function handleBeforeToolCall(
45
27
  event: PluginHookBeforeToolCallEvent,
@@ -58,154 +40,57 @@ export function handleBeforeToolCall(
58
40
 
59
41
  const wctx = WorkspaceContext.fromHookContext(ctx);
60
42
 
61
- // 2. Load Profile
62
- const profilePath = wctx.resolve('PROFILE');
63
- let profile = {
64
- risk_paths: [] as string[],
65
- gate: { require_plan_for_risk_paths: true },
66
- progressive_gate: {
67
- enabled: true,
68
- plan_approvals: {
69
- enabled: false,
70
- max_lines_override: -1,
71
- allowed_patterns: [] as string[],
72
- allowed_operations: [] as string[],
73
- }
74
- },
75
- edit_verification: {
76
- enabled: true,
77
- max_file_size_bytes: 10 * 1024 * 1024,
78
- fuzzy_match_enabled: true,
79
- fuzzy_match_threshold: 0.8,
80
- skip_large_file_action: 'warn' as 'warn' | 'block',
81
- },
82
- thinking_checkpoint: {
83
- enabled: false, // Default OFF
84
- window_ms: 5 * 60 * 1000,
85
- high_risk_tools: ['run_shell_command', 'delete_file', 'move_file'],
86
- }
87
- };
88
-
89
- if (fs.existsSync(profilePath)) {
90
- try {
91
- const rawProfile = JSON.parse(fs.readFileSync(profilePath, 'utf8'));
92
- profile = normalizeProfile(rawProfile);
93
- } catch (e) {
94
- logger?.error?.(`[PD_GATE] Failed to parse PROFILE.json: ${String(e)}`);
95
- }
96
- }
97
-
98
- // ─────────────────────────────────────────────────────────────────────────────
99
- // POLICY STEP 1: Thinking OS Checkpoint (P-10)
100
- // ─────────────────────────────────────────────────────────────────────────────
101
- // Only enforced when thinking_checkpoint.enabled = true in PROFILE.json
102
- const thinkingResult = checkThinkingCheckpoint(
103
- event,
104
- profile.thinking_checkpoint || {},
105
- ctx.sessionId,
106
- logger
107
- );
108
- if (thinkingResult) {
109
- return thinkingResult;
110
- }
111
-
112
- // ─────────────────────────────────────────────────────────────────────────────
113
- // POLICY STEP 2: GFI Gate - Hard Intercept
114
- // ─────────────────────────────────────────────────────────────────────────────
115
- // 根据 GFI (疲劳指数) 精细化拦截工具调用
116
- // 注意:TIER 0 (只读工具) 已在早期过滤中放行,此处不检查
117
- const gfiGateConfig = wctx.config.get('gfi_gate');
118
- const gfiResult = checkGfiGate(event, wctx, ctx.sessionId, gfiGateConfig, logger);
119
- if (gfiResult) {
120
- return gfiResult;
121
- }
122
-
123
- // Merge pluginConfig (OpenClaw UI settings)
124
- const configRiskPaths = (ctx.pluginConfig?.riskPaths as string[] | undefined) ?? [];
125
- if (configRiskPaths.length > 0) {
126
- profile.risk_paths = [...new Set([...profile.risk_paths, ...configRiskPaths])];
127
- }
128
-
129
- // 3. Resolve the target file path
43
+ // 2. Resolve the target file path
130
44
  let filePath = event.params.file_path || event.params.path || event.params.file || event.params.target;
131
45
 
132
46
  // Heuristic for bash mutation detection
133
47
  if (isBash && !filePath) {
134
- const command = String(event.params.command || event.params.args || "");
48
+ const command = String(event.params.command || event.params.args || '');
135
49
  const mutationMatch = /(?:>|>>|sed\s+-i|rm|mv|mkdir|touch|cp)\s+(?:-[a-zA-Z]+\s+)*([^\s;&|<>]+)/.exec(command);
136
50
 
137
51
  if (mutationMatch) {
138
-
139
-
140
52
  filePath = mutationMatch[1];
141
53
  } else {
142
- const hasRiskPath = profile.risk_paths.some(rp => command.includes(rp));
143
- const isMutation = /(?:>|>>|sed|rm|mv|mkdir|touch|cp|npm|yarn|pnpm|pip|cargo)/.test(command);
144
-
145
- if (hasRiskPath && isMutation) {
146
- filePath = command;
147
- } else {
148
- return;
149
- }
54
+ // Bash command without a clear file target — let it through to Rule Host
55
+ filePath = command;
150
56
  }
151
57
  }
152
58
 
153
59
  if (typeof filePath !== 'string') return;
154
60
 
155
61
  const relPath = normalizePath(filePath, ctx.workspaceDir);
156
- const risky = (isBash && filePath.includes(' '))
157
- ? profile.risk_paths.some(rp => filePath.includes(rp))
158
- : isRisky(relPath, profile.risk_paths);
159
62
 
160
- // ─────────────────────────────────────────────────────────────────────────────
161
- // POLICY STEP 2.5: Rule Host Evaluation (Phase 12, D-01/D-03)
162
- // ─────────────────────────────────────────────────────────────────────────────
163
- // Inserted between GFI gate and Progressive Gate so principle rules can act
164
- // before the capability-boundary fallback. Active code implementations run
165
- // through a constrained vm context with minimal helpers only.
63
+ // 3. Rule Host Evaluation — sole gate
166
64
  try {
167
65
  const ruleHost = new RuleHost(wctx.stateDir, logger);
168
66
  const hostInput: RuleHostInput = {
169
67
  action: {
170
68
  toolName: event.toolName,
171
69
  normalizedPath: relPath,
172
-
173
-
174
70
  paramsSummary: _extractParamsSummary(event.params),
175
71
  },
176
72
  workspace: {
177
- isRiskPath: risky,
178
-
179
-
73
+ isRiskPath: false, // Rule Host determines risk dynamically
180
74
  planStatus: _getPlanStatus(ctx.workspaceDir),
181
-
182
-
183
75
  hasPlanFile: _hasPlanFile(ctx.workspaceDir),
184
76
  },
185
77
  session: {
186
78
  sessionId: ctx.sessionId,
187
-
188
-
189
79
  currentGfi: _getCurrentGfi(ctx.sessionId),
190
-
191
-
192
80
  recentThinking: _hasRecentThinking(ctx.sessionId),
193
81
  },
194
82
  evolution: {
195
-
196
-
197
83
  epTier: _getEpTier(wctx.workspaceDir),
198
84
  },
199
85
  derived: {
200
86
  estimatedLineChanges: estimateLineChanges({ toolName: event.toolName, params: event.params }),
201
-
202
-
203
- bashRisk: _getBashRisk(event, profile),
87
+ bashRisk: _getBashRisk(event),
204
88
  },
205
89
  };
206
90
 
207
91
  const hostResult = ruleHost.evaluate(hostInput);
208
- // PD-FUNNEL-2.4: Always emit rulehost_evaluated on evaluate()
92
+
93
+ // Always emit rulehost_evaluated
209
94
  try {
210
95
  const eventLog = EventLogService.get(wctx.stateDir, logger as PluginLogger | undefined);
211
96
  eventLog.recordRuleHostEvaluated({
@@ -218,8 +103,8 @@ export function handleBeforeToolCall(
218
103
  } catch (evErr) {
219
104
  logger?.warn?.(`[PD_GATE] Failed to record rulehost_evaluated: ${String(evErr)}`);
220
105
  }
106
+
221
107
  if (hostResult?.decision === 'block') {
222
- // C: Record rule_enforced event for matched rules
223
108
  try {
224
109
  const eventLog = EventLogService.get(wctx.stateDir, logger as PluginLogger | undefined);
225
110
  eventLog.recordRuleEnforced({
@@ -236,18 +121,18 @@ export function handleBeforeToolCall(
236
121
  ruleId: hostResult.ruleId,
237
122
  });
238
123
  } catch (evErr) {
239
- logger?.warn?.(`[PD_GATE] Failed to record rule_enforced/rulehost_blocked event: ${String(evErr)}`);
124
+ logger?.warn?.(`[PD_GATE] Failed to record rule_enforced/rulehost_blocked: ${String(evErr)}`);
240
125
  }
241
126
 
242
- const reason = hostResult.reason;
243
127
  return recordGateBlockAndReturn(wctx, {
244
128
  filePath: relPath,
245
- reason,
129
+ reason: hostResult.reason,
246
130
  toolName: event.toolName,
247
131
  sessionId: ctx.sessionId,
248
132
  blockSource: 'rule-host',
249
133
  }, logger);
250
134
  }
135
+
251
136
  if (hostResult?.decision === 'requireApproval') {
252
137
  try {
253
138
  const eventLog = EventLogService.get(wctx.stateDir, logger as PluginLogger | undefined);
@@ -265,68 +150,12 @@ export function handleBeforeToolCall(
265
150
  ruleId: hostResult.ruleId,
266
151
  });
267
152
  } catch (evErr) {
268
- logger?.warn?.(`[PD_GATE] Failed to record rule_enforced/rulehost_requireApproval event: ${String(evErr)}`);
153
+ logger?.warn?.(`[PD_GATE] Failed to record rule_enforced/rulehost_requireApproval: ${String(evErr)}`);
269
154
  }
270
155
  }
271
156
  } catch (hostError: unknown) {
272
- // D-08: Conservative degradation — log and continue to Progressive Gate
273
- logger.warn?.(`[PD_GATE:RULE_HOST] Host evaluation failed, degrading conservatively: ${String(hostError)}`);
274
- }
275
-
276
- // ─────────────────────────────────────────────────────────────────────────────
277
- // POLICY STEP 3: Progressive Trust Gate (Stage 1-4 access control)
278
- // ─────────────────────────────────────────────────────────────────────────────
279
- // IMPORTANT: This step does NOT return early on allow.
280
- // We must continue to edit verification for ALL allowed operations.
281
- if (profile.progressive_gate?.enabled) {
282
- const lineChanges = estimateLineChanges({ toolName: event.toolName, params: event.params });
283
- const progressiveGateResult = checkProgressiveTrustGate(
284
- event,
285
- wctx,
286
- relPath,
287
- risky,
288
- lineChanges,
289
- logger,
290
- ctx,
291
- profile
292
- );
293
- if (progressiveGateResult) {
294
- return progressiveGateResult;
295
- }
296
- // NOTE: Do NOT return here! Continue to edit verification.
297
- // All allowed operations (regardless of EP tier) should still run edit verification.
298
- } else {
299
- // FALLBACK: Legacy Gate Logic (when progressive gate is disabled)
300
- if (risky && profile.gate?.require_plan_for_risk_paths) {
301
- const planStatus = getPlanStatus(ctx.workspaceDir);
302
- if (planStatus !== 'READY') {
303
- return recordGateBlockAndReturn(wctx, {
304
- filePath: relPath,
305
- reason: `No READY plan found in PLAN.md.`,
306
- toolName: event.toolName,
307
- sessionId: ctx.sessionId,
308
- blockSource: 'gate-legacy',
309
- }, logger);
310
- }
311
- }
312
- }
313
-
314
- // ─────────────────────────────────────────────────────────────────────────────
315
- // POLICY STEP 4: Edit Tool Verification (P-03)
316
- // ─────────────────────────────────────────────────────────────────────────────
317
- // This MUST run after all other gate checks for ALL tools.
318
- // Edit verification ensures oldText matches the actual file content.
319
- if (event.toolName === 'edit' && profile.edit_verification?.enabled !== false) {
320
- const verifyResult = handleEditVerification(event, wctx, ctx, {
321
- enabled: profile.edit_verification.enabled,
322
- max_file_size_bytes: profile.edit_verification.max_file_size_bytes,
323
- fuzzy_match_enabled: profile.edit_verification.fuzzy_match_enabled,
324
- fuzzy_match_threshold: profile.edit_verification.fuzzy_match_threshold,
325
- skip_large_file_action: profile.edit_verification.skip_large_file_action as 'warn' | 'block' | undefined,
326
- });
327
- if (verifyResult) {
328
- return verifyResult; // Block or modify params
329
- }
157
+ // D-08: Conservative degradation — log and allow on Rule Host failure
158
+ logger.warn?.(`[PD_GATE:RULE_HOST] Host evaluation failed, allowing conservatively: ${String(hostError)}`);
330
159
  }
331
160
 
332
161
  // All checks passed - allow the operation
@@ -334,9 +163,7 @@ export function handleBeforeToolCall(
334
163
  }
335
164
 
336
165
  // ---------------------------------------------------------------------------
337
- // Private helpers for building RuleHostInput snapshot
338
- // These are NOT passed to hosted implementations — they only populate the
339
- // frozen snapshot that implementations receive.
166
+ // Private helpers
340
167
  // ---------------------------------------------------------------------------
341
168
 
342
169
  function _extractParamsSummary(params: Record<string, unknown>): Record<string, unknown> {
@@ -352,7 +179,7 @@ function _extractParamsSummary(params: Record<string, unknown>): Record<string,
352
179
 
353
180
  function _getPlanStatus(workspaceDir: string): 'NONE' | 'DRAFT' | 'READY' | 'UNKNOWN' {
354
181
  try {
355
- const status = getPlanStatus(workspaceDir);
182
+ const status = planStatus(workspaceDir);
356
183
  if (status === 'READY') return 'READY';
357
184
  if (status === 'DRAFT') return 'DRAFT';
358
185
  if (status === '') return 'NONE';
@@ -397,12 +224,7 @@ function _getEpTier(workspaceDir: string): number {
397
224
  }
398
225
  }
399
226
 
400
-
401
- function _getBashRisk(
402
- event: PluginHookBeforeToolCallEvent,
403
- _profile: { risk_paths: string[] }
404
- ): 'safe' | 'normal' | 'dangerous' | 'unknown' {
405
-
227
+ function _getBashRisk(event: PluginHookBeforeToolCallEvent): 'safe' | 'normal' | 'dangerous' | 'unknown' {
406
228
  if (!BASH_TOOLS_SET.has(event.toolName)) return 'unknown';
407
229
  try {
408
230
  const command = String(event.params.command || event.params.args || '');
@@ -170,10 +170,14 @@ function pushWarning(warnings: string[], message: string): void {
170
170
  export class RuntimeSummaryService {
171
171
  static getSummary(
172
172
  workspaceDir: string,
173
- options?: { sessionId?: string | null }
173
+ options?: { sessionId?: string | null; loaderWarnings?: string[] }
174
174
  ): RuntimeSummary {
175
175
  const generatedAt = new Date().toISOString();
176
176
  const warnings: string[] = [];
177
+ // ERR-01: surface loader warnings (YAML parse failures) into metadata.warnings
178
+ if (options?.loaderWarnings) {
179
+ warnings.push(...options.loaderWarnings);
180
+ }
177
181
  const wctx = WorkspaceContext.fromHookContext({ workspaceDir });
178
182
 
179
183
  const sessions = this.mergeSessionSnapshots(
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: pd-pain-signal
3
- description: Manually inject a pain signal into the evolution system. TRIGGER CONDITIONS: (1) User reports agent stuck/looping/unresponsive (2) User says "record this issue", "force reflection", "trigger pain" (3) Tool failure with no follow-up action (4) User provides human intervention feedback.
4
- disable-model-invocation: true
3
+ description: Manually inject a pain signal into the evolution system. Trigger conditions: (1) User reports agent stuck/looping/unresponsive (2) User says "record this issue", "force reflection", "trigger pain", "create pain signal", "人工注入 pain", "创建一个痛苦信号" (3) Tool failure with no follow-up action (4) User provides human intervention feedback. Mandatory enforcement: any manual pain flag creation scenario MUST use this skill, no bypassing allowed.
4
+ disable-model-invocation: false
5
5
  ---
6
6
 
7
7
  # Pain Signal (Force Pain Signal)
@@ -12,28 +12,21 @@ You are now the "Manual Intervention Pain" component.
12
12
  1. Record the user's feedback `$ARGUMENTS` as a **high-priority** pain signal.
13
13
  2. Inform the user that the signal has been injected, and suggest waiting for the next Hook trigger (e.g., Stop or PreCompact) or manually running `/reflection-log`.
14
14
 
15
- **⚠️ Write Rules (MUST follow)**
15
+ **Mandatory Enforcement**:
16
16
 
17
- **The ONLY correct way**: Use the `write_pain_flag` tool.
18
-
19
- ```
20
- write_pain_flag({
21
- reason: "User feedback or error description",
22
- score: 80,
23
- source: "human_intervention",
24
- is_risky: false
25
- })
26
- ```
17
+ When you need to create, write, or manually create a pain flag, you **MUST** use this skill via the `write_pain_flag` tool. Any bypassing of this skill to directly operate on files violates the mandatory constraint of this skill.
27
18
 
28
19
  **Absolutely forbidden**:
29
20
  - ❌ Writing to `.state/.pain_flag` directly (any method)
30
21
  - ❌ Using bash heredoc (`cat <<EOF > .pain_flag`)
31
22
  - ❌ Using `echo "..." > .pain_flag`
23
+ - ❌ Using `Set-Content` / `Out-File` or other PowerShell file-writing cmdlets
32
24
  - ❌ Using `node -e` to call `writePainFlag` or `buildPainFlag`
33
25
  - ❌ Any method that `toString()` a JavaScript object to the file
26
+ - ❌ Using `exec` tool to invoke shell commands to write the pain_flag file
34
27
 
35
28
  **Why use the tool?**
36
- The `write_pain_flag` tool encapsulates correct KV-format serialization, ensuring `.pain_flag` is never corrupted. Historically, direct file writes caused `[object Object]` corruption multiple times.
29
+ The `write_pain_flag` tool encapsulates correct KV-format serialization, ensuring `.pain_flag` is never corrupted. Historically, direct file writes caused `[object Object]` corruption and field loss (painScore → score mapping failure). Using the tool is the only safe path.
37
30
 
38
31
  **Parameters**:
39
32
  - `reason` (required): The reason for the pain signal — describe what went wrong
@@ -50,3 +43,10 @@ write_pain_flag({
50
43
  is_risky: false
51
44
  })
52
45
  ```
46
+
47
+ **Workflow**:
48
+ 1. Recognize trigger condition → read this skill
49
+ 2. Call `write_pain_flag` tool with `reason` and other parameters
50
+ 3. Confirm tool executed successfully (returns ✅)
51
+ 4. Inform user the pain signal has been injected; evolution system will process it on next heartbeat
52
+ 5. Do NOT perform any direct file write operations after this