principles-disciple 1.61.0 → 1.63.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/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,238 +40,122 @@ 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
- if (hostResult?.decision === 'block' || hostResult?.decision === 'requireApproval') {
209
- // C: Record rule_enforced event for matched rules
92
+
93
+ // Always emit rulehost_evaluated
94
+ try {
95
+ const eventLog = EventLogService.get(wctx.stateDir, logger as PluginLogger | undefined);
96
+ eventLog.recordRuleHostEvaluated({
97
+ toolName: event.toolName,
98
+ filePath: relPath,
99
+ matched: hostResult?.matched ?? false,
100
+ decision: hostResult?.decision ?? 'allow',
101
+ ruleId: hostResult?.ruleId,
102
+ });
103
+ } catch (evErr) {
104
+ logger?.warn?.(`[PD_GATE] Failed to record rulehost_evaluated: ${String(evErr)}`);
105
+ }
106
+
107
+ if (hostResult?.decision === 'block') {
210
108
  try {
211
109
  const eventLog = EventLogService.get(wctx.stateDir, logger as PluginLogger | undefined);
212
110
  eventLog.recordRuleEnforced({
213
111
  ruleId: hostResult.ruleId || 'unknown',
214
112
  principleId: hostResult.principleId || 'unknown',
215
- enforcement: hostResult.decision === 'requireApproval' ? 'requireApproval' : 'block',
113
+ enforcement: 'block',
114
+ toolName: event.toolName,
115
+ filePath: relPath,
116
+ });
117
+ eventLog.recordRuleHostBlocked({
216
118
  toolName: event.toolName,
217
119
  filePath: relPath,
120
+ reason: hostResult.reason,
121
+ ruleId: hostResult.ruleId,
218
122
  });
219
123
  } catch (evErr) {
220
- logger?.warn?.(`[PD_GATE] Failed to record rule_enforced event: ${String(evErr)}`);
124
+ logger?.warn?.(`[PD_GATE] Failed to record rule_enforced/rulehost_blocked: ${String(evErr)}`);
221
125
  }
222
126
 
223
- const reason = hostResult.decision === 'requireApproval'
224
- ? `[Rule Host] Approval required: ${hostResult.reason}`
225
- : hostResult.reason;
226
127
  return recordGateBlockAndReturn(wctx, {
227
128
  filePath: relPath,
228
- reason,
129
+ reason: hostResult.reason,
229
130
  toolName: event.toolName,
230
131
  sessionId: ctx.sessionId,
231
132
  blockSource: 'rule-host',
232
133
  }, logger);
233
134
  }
234
- } catch (hostError: unknown) {
235
- // D-08: Conservative degradation — log and continue to Progressive Gate
236
- logger.warn?.(`[PD_GATE:RULE_HOST] Host evaluation failed, degrading conservatively: ${String(hostError)}`);
237
- }
238
135
 
239
- // ─────────────────────────────────────────────────────────────────────────────
240
- // POLICY STEP 3: Progressive Trust Gate (Stage 1-4 access control)
241
- // ─────────────────────────────────────────────────────────────────────────────
242
- // IMPORTANT: This step does NOT return early on allow.
243
- // We must continue to edit verification for ALL allowed operations.
244
- if (profile.progressive_gate?.enabled) {
245
- const lineChanges = estimateLineChanges({ toolName: event.toolName, params: event.params });
246
- const progressiveGateResult = checkProgressiveTrustGate(
247
- event,
248
- wctx,
249
- relPath,
250
- risky,
251
- lineChanges,
252
- logger,
253
- ctx,
254
- profile
255
- );
256
- if (progressiveGateResult) {
257
- return progressiveGateResult;
258
- }
259
- // NOTE: Do NOT return here! Continue to edit verification.
260
- // All allowed operations (regardless of EP tier) should still run edit verification.
261
- } else {
262
- // FALLBACK: Legacy Gate Logic (when progressive gate is disabled)
263
- if (risky && profile.gate?.require_plan_for_risk_paths) {
264
- const planStatus = getPlanStatus(ctx.workspaceDir);
265
- if (planStatus !== 'READY') {
266
- return recordGateBlockAndReturn(wctx, {
136
+ if (hostResult?.decision === 'requireApproval') {
137
+ try {
138
+ const eventLog = EventLogService.get(wctx.stateDir, logger as PluginLogger | undefined);
139
+ eventLog.recordRuleEnforced({
140
+ ruleId: hostResult.ruleId || 'unknown',
141
+ principleId: hostResult.principleId || 'unknown',
142
+ enforcement: 'requireApproval',
143
+ toolName: event.toolName,
267
144
  filePath: relPath,
268
- reason: `No READY plan found in PLAN.md.`,
145
+ });
146
+ eventLog.recordRuleHostRequireApproval({
269
147
  toolName: event.toolName,
270
- sessionId: ctx.sessionId,
271
- blockSource: 'gate-legacy',
272
- }, logger);
148
+ filePath: relPath,
149
+ reason: hostResult.reason,
150
+ ruleId: hostResult.ruleId,
151
+ });
152
+ } catch (evErr) {
153
+ logger?.warn?.(`[PD_GATE] Failed to record rule_enforced/rulehost_requireApproval: ${String(evErr)}`);
273
154
  }
274
155
  }
275
- }
276
-
277
- // ─────────────────────────────────────────────────────────────────────────────
278
- // POLICY STEP 4: Edit Tool Verification (P-03)
279
- // ─────────────────────────────────────────────────────────────────────────────
280
- // This MUST run after all other gate checks for ALL tools.
281
- // Edit verification ensures oldText matches the actual file content.
282
- if (event.toolName === 'edit' && profile.edit_verification?.enabled !== false) {
283
- const verifyResult = handleEditVerification(event, wctx, ctx, {
284
- enabled: profile.edit_verification.enabled,
285
- max_file_size_bytes: profile.edit_verification.max_file_size_bytes,
286
- fuzzy_match_enabled: profile.edit_verification.fuzzy_match_enabled,
287
- fuzzy_match_threshold: profile.edit_verification.fuzzy_match_threshold,
288
- skip_large_file_action: profile.edit_verification.skip_large_file_action as 'warn' | 'block' | undefined,
289
- });
290
- if (verifyResult) {
291
- return verifyResult; // Block or modify params
292
- }
156
+ } catch (hostError: unknown) {
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)}`);
293
159
  }
294
160
 
295
161
  // All checks passed - allow the operation
@@ -297,9 +163,7 @@ export function handleBeforeToolCall(
297
163
  }
298
164
 
299
165
  // ---------------------------------------------------------------------------
300
- // Private helpers for building RuleHostInput snapshot
301
- // These are NOT passed to hosted implementations — they only populate the
302
- // frozen snapshot that implementations receive.
166
+ // Private helpers
303
167
  // ---------------------------------------------------------------------------
304
168
 
305
169
  function _extractParamsSummary(params: Record<string, unknown>): Record<string, unknown> {
@@ -315,7 +179,7 @@ function _extractParamsSummary(params: Record<string, unknown>): Record<string,
315
179
 
316
180
  function _getPlanStatus(workspaceDir: string): 'NONE' | 'DRAFT' | 'READY' | 'UNKNOWN' {
317
181
  try {
318
- const status = getPlanStatus(workspaceDir);
182
+ const status = planStatus(workspaceDir);
319
183
  if (status === 'READY') return 'READY';
320
184
  if (status === 'DRAFT') return 'DRAFT';
321
185
  if (status === '') return 'NONE';
@@ -360,12 +224,7 @@ function _getEpTier(workspaceDir: string): number {
360
224
  }
361
225
  }
362
226
 
363
-
364
- function _getBashRisk(
365
- event: PluginHookBeforeToolCallEvent,
366
- _profile: { risk_paths: string[] }
367
- ): 'safe' | 'normal' | 'dangerous' | 'unknown' {
368
-
227
+ function _getBashRisk(event: PluginHookBeforeToolCallEvent): 'safe' | 'normal' | 'dangerous' | 'unknown' {
369
228
  if (!BASH_TOOLS_SET.has(event.toolName)) return 'unknown';
370
229
  try {
371
230
  const command = String(event.params.command || event.params.args || '');
@@ -957,6 +957,16 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
957
957
  if (presentPhases.length < requiredPhases.length) {
958
958
  const missing = requiredPhases.filter(p => !phases[p]);
959
959
  if (logger) logger.warn(`[PD:EvolutionWorker] Report for task ${task.id} incomplete — missing phases: ${missing.join(', ')} (present: ${presentPhases.length}/${requiredPhases.length})`);
960
+ // PD-FUNNEL-1.1: Record incomplete_fields event BEFORE retry so funnel can see it.
961
+ // The phase-completeness check requeues incomplete reports with continue; without
962
+ // this record the funnel would have no signal for JSON-present-but-incomplete cases.
963
+ if (eventLog) {
964
+ eventLog.recordDiagnosticianReport({
965
+ taskId: task.id,
966
+ reportPath,
967
+ category: 'incomplete_fields',
968
+ });
969
+ }
960
970
  // Treat as retryable failure: don't mark success, let retry logic kick in
961
971
  reportParsed = false;
962
972
  // Also delete the incomplete marker so next heartbeat re-runs the diagnostician
@@ -104,6 +104,7 @@ import { registerSample } from '../core/nocturnal-dataset.js';
104
104
  import { getPrincipleState, setPrincipleState } from '../core/principle-training-state.js';
105
105
  import type { Implementation } from '../types/principle-tree-schema.js';
106
106
  import { validateNocturnalSnapshotIngress } from '../core/nocturnal-snapshot-contract.js';
107
+ import { EventLogService } from '../core/event-log.js';
107
108
 
108
109
 
109
110
  // ---------------------------------------------------------------------------
@@ -522,9 +523,20 @@ function persistCodeCandidate(
522
523
  try {
523
524
  refreshPrincipleLifecycle(workspaceDir, stateDir);
524
525
  } catch (err) {
525
-
526
526
  console.warn('[nocturnal-service] Lifecycle refresh failed after code candidate persistence:', err instanceof Error ? err.stack : err);
527
527
  }
528
+ // PD-FUNNEL-2.3: Emit nocturnal_code_candidate_created event
529
+ try {
530
+ const eventLog = EventLogService.get(stateDir, undefined);
531
+ eventLog.recordNocturnalCodeCandidateCreated({
532
+ implementationId,
533
+ artifactId,
534
+ ruleId: parsedArtificer.ruleId,
535
+ persistedPath: assetRoot,
536
+ });
537
+ } catch (evErr) {
538
+ console.warn(`[nocturnal-service] Failed to record nocturnal_code_candidate_created: ${String(evErr)}`);
539
+ }
528
540
  return {
529
541
  status: 'persisted_candidate',
530
542
  ruleResolution: {
@@ -1039,13 +1051,22 @@ export function executeNocturnalReflection(
1039
1051
  boundedAction: execResult.boundedAction,
1040
1052
  };
1041
1053
 
1042
-
1043
-
1044
1054
  let persistedPath: string;
1045
1055
  try {
1046
1056
  persistedPath = persistArtifact(workspaceDir, artifactWithBoundedAction);
1047
1057
  diagnostics.persisted = true;
1048
1058
  diagnostics.persistedPath = persistedPath;
1059
+ // PD-FUNNEL-2.3: Emit nocturnal_artifact_persisted event
1060
+ try {
1061
+ const eventLog = EventLogService.get(stateDir, undefined);
1062
+ eventLog.recordNocturnalArtifactPersisted({
1063
+ artifactId: artifactWithBoundedAction.artifactId,
1064
+ principleId: artifactWithBoundedAction.principleId,
1065
+ persistedPath,
1066
+ });
1067
+ } catch (evErr) {
1068
+ console.warn(`[nocturnal-service] Failed to record nocturnal_artifact_persisted: ${String(evErr)}`);
1069
+ }
1049
1070
  } catch (err) {
1050
1071
  void recordRunEnd(stateDir, 'failed', { reason: `persistence error: ${String(err)}` }).catch((e) => {
1051
1072
  warn(`[nocturnal-service] Failed to record run end (persistence failed): ${String(e)}`);
@@ -41,6 +41,7 @@ import * as fs from 'fs';
41
41
  import * as path from 'path';
42
42
  import { validateNocturnalSnapshotIngress } from '../../core/nocturnal-snapshot-contract.js';
43
43
  import { isSubagentRuntimeAvailable } from '../../utils/subagent-probe.js';
44
+ import { EventLogService } from '../../core/event-log.js';
44
45
 
45
46
  // ─────────────────────────────────────────────────────────────────────────────
46
47
  // NocturnalResult Type Alias
@@ -290,6 +291,21 @@ export class NocturnalWorkflowManager implements WorkflowManager {
290
291
  artifactId: result.diagnostics?.persistedPath,
291
292
  });
292
293
  this.completedWorkflows.set(workflowId, Date.now());
294
+ // PD-FUNNEL-2.3: Emit nocturnal_dreamer_completed event
295
+ const chainMode: 'trinity' | 'single-reflector' =
296
+ result.trinityTelemetry ? 'trinity' : 'single-reflector';
297
+ try {
298
+ const eventLog = EventLogService.get(this.stateDir, this.logger as unknown as import('../../openclaw-sdk.js').PluginLogger);
299
+ eventLog.recordNocturnalDreamerCompleted({
300
+ workflowId,
301
+ principleId: result.artifact?.principleId ?? 'unknown',
302
+ sessionId: result.snapshot?.sessionId ?? 'unknown',
303
+ candidateCount: result.trinityTelemetry?.candidateCount ?? 1,
304
+ chainMode,
305
+ });
306
+ } catch (evErr) {
307
+ this.logger.warn?.(`[PD:NocturnalWorkflow] Failed to record nocturnal_dreamer_completed: ${String(evErr)}`);
308
+ }
293
309
  } else {
294
310
  const reason = result.noTargetSelected ? 'no_target_selected' : 'validation_failed';
295
311
  const failuresSummary = result.validationFailures?.length > 0
@@ -23,7 +23,15 @@ export type EventType =
23
23
  | 'heartbeat_diagnosis' // Heartbeat injected diagnostician tasks
24
24
  | 'diagnostician_report' // Diagnostician completed and wrote report
25
25
  | 'principle_candidate' // Principle candidate created from report
26
- | 'rule_enforced'; // Rule enforced (matched) during tool call
26
+ | 'rule_enforced' // Rule enforced (matched) during tool call
27
+ // C: Nocturnal funnel stage events (PD-FUNNEL-2.3)
28
+ | 'nocturnal_dreamer_completed'
29
+ | 'nocturnal_artifact_persisted'
30
+ | 'nocturnal_code_candidate_created'
31
+ // C: RuleHost funnel events (PD-FUNNEL-2.4)
32
+ | 'rulehost_evaluated'
33
+ | 'rulehost_blocked'
34
+ | 'rulehost_requireApproval';
27
35
 
28
36
  export type EventCategory =
29
37
  | 'success'
@@ -42,7 +50,11 @@ export type EventCategory =
42
50
  | 'written'
43
51
  | 'injected'
44
52
  | 'created'
45
- | 'matched';
53
+ | 'matched'
54
+ // C: New categories for RuleHost funnel (PD-FUNNEL-2.4) — completed/created already exist
55
+ | 'evaluated' // Used by: rulehost_evaluated
56
+ | 'blocked' // Used by: rulehost_blocked
57
+ | 'requireApproval'; // Used by: rulehost_requireApproval
46
58
 
47
59
  /**
48
60
  * Base event structure for JSONL logging.
@@ -237,6 +249,77 @@ export interface RuleEnforcedEventData {
237
249
  filePath: string;
238
250
  }
239
251
 
252
+ // ============== Nocturnal Funnel Events (PD-FUNNEL-2.3) ==============
253
+
254
+ /**
255
+ * nocturnal_dreamer_completed — Trinity Dreamer stage completed.
256
+ * Emitted from nocturnal-workflow-manager.ts after Trinity chain success.
257
+ */
258
+ export interface NocturnalDreamerCompletedEventData {
259
+ workflowId: string;
260
+ principleId: string;
261
+ sessionId: string;
262
+ candidateCount: number;
263
+ chainMode: 'trinity' | 'single-reflector';
264
+ }
265
+
266
+ /**
267
+ * nocturnal_artifact_persisted — Artifact saved to .state/nocturnal/samples/.
268
+ * Emitted from nocturnal-service.ts persistArtifact() after atomicWriteFileSync.
269
+ */
270
+ export interface NocturnalArtifactPersistedEventData {
271
+ artifactId: string;
272
+ principleId: string;
273
+ persistedPath: string;
274
+ }
275
+
276
+ /**
277
+ * nocturnal_code_candidate_created — Rule implementation candidate persisted.
278
+ * Emitted from nocturnal-service.ts persistCodeCandidate() after successful creation.
279
+ */
280
+ export interface NocturnalCodeCandidateCreatedEventData {
281
+ implementationId: string;
282
+ artifactId: string;
283
+ ruleId: string;
284
+ persistedPath: string;
285
+ }
286
+
287
+ // ============== RuleHost Funnel Events (PD-FUNNEL-2.4) ==============
288
+
289
+ /**
290
+ * rulehost_evaluated — RuleHost.evaluate() was called.
291
+ * Emitted from gate.ts for every evaluate() call (matched or not).
292
+ */
293
+ export interface RuleHostEvaluatedEventData {
294
+ toolName: string;
295
+ filePath: string;
296
+ matched: boolean;
297
+ decision: 'allow' | 'block' | 'requireApproval';
298
+ ruleId?: string;
299
+ }
300
+
301
+ /**
302
+ * rulehost_blocked — Tool call was blocked by RuleHost.
303
+ * Emitted from gate.ts when hostResult.decision === 'block'.
304
+ */
305
+ export interface RuleHostBlockedEventData {
306
+ toolName: string;
307
+ filePath: string;
308
+ reason: string;
309
+ ruleId?: string;
310
+ }
311
+
312
+ /**
313
+ * rulehost_requireApproval — Tool call requires approval by RuleHost.
314
+ * Emitted from gate.ts when hostResult.decision === 'requireApproval'.
315
+ */
316
+ export interface RuleHostRequireApprovalEventData {
317
+ toolName: string;
318
+ filePath: string;
319
+ reason: string;
320
+ ruleId?: string;
321
+ }
322
+
240
323
  // ============== Daily Statistics ==============
241
324
 
242
325
  export interface ToolCallStats {
@@ -335,6 +418,15 @@ export interface EvolutionStats {
335
418
  reportsIncompleteFields: number;
336
419
  principleCandidatesCreated: number;
337
420
  rulesEnforced: number;
421
+ // C: Nocturnal funnel counters (PD-FUNNEL-2.3)
422
+ nocturnalDreamerCompleted: number;
423
+ nocturnalTrinityCompleted: number;
424
+ nocturnalArtifactPersisted: number;
425
+ nocturnalCodeCandidateCreated: number;
426
+ // C: RuleHost funnel counters (PD-FUNNEL-2.4)
427
+ rulehostEvaluated: number;
428
+ rulehostBlocked: number;
429
+ rulehostRequireApproval: number;
338
430
  }
339
431
 
340
432
  export interface HookStats {
@@ -501,6 +593,15 @@ export function createEmptyDailyStats(date: string): DailyStats {
501
593
  reportsIncompleteFields: 0,
502
594
  principleCandidatesCreated: 0,
503
595
  rulesEnforced: 0,
596
+ // C: Nocturnal funnel counters (PD-FUNNEL-2.3)
597
+ nocturnalDreamerCompleted: 0,
598
+ nocturnalTrinityCompleted: 0,
599
+ nocturnalArtifactPersisted: 0,
600
+ nocturnalCodeCandidateCreated: 0,
601
+ // C: RuleHost funnel counters (PD-FUNNEL-2.4)
602
+ rulehostEvaluated: 0,
603
+ rulehostBlocked: 0,
604
+ rulehostRequireApproval: 0,
504
605
  },
505
606
  hooks: {
506
607
  total: 0,