principles-disciple 1.104.0 → 1.104.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.
@@ -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.104.0",
5
+ "version": "1.104.1",
6
6
  "activation": {
7
7
  "onCapabilities": [
8
8
  "hook"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "principles-disciple",
3
- "version": "1.104.0",
3
+ "version": "1.104.1",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/bundle.js",
@@ -23,9 +23,8 @@ import { WorkspaceContext } from '../core/workspace-context.js';
23
23
  import { getEvolutionLogger, createTraceId } from '../core/evolution-logger.js';
24
24
  import { recordEvolutionSuccess, recordEvolutionFailure } from '../core/evolution-engine.js';
25
25
  import type { PluginHookAfterToolCallEvent } from '../openclaw-sdk.js';
26
- import { evaluatePainDiagnosticGate, isCooldownActiveForEpisode } from '../core/pain-diagnostic-gate.js';
26
+ import { isCooldownActive as isTriggerCooldownActive, markEpisodeAsDiagnosed, clearCooldownState } from './trigger-cooldown-tracker.js';
27
27
  import { sanitizeForEvidence, sanitizeToolParamsForEvidence } from './message-sanitize.js';
28
- import { loadFeatureFlagFromConfig } from '../core/pd-config-loader.js';
29
28
  import { resolveSourceKindFromToolFailure, evaluateEvidenceTriage } from './triage-adapter.js';
30
29
  import { evaluateTriggerController } from '@principles/core/runtime-v2';
31
30
  import { buildTrajectoryEvidence } from './trajectory-evidence.js';
@@ -40,8 +39,13 @@ import type { ToolCallOutcome, ToolCallObservation, PainAdmissionDecision } from
40
39
  * Extracts exitCode logic, determines failure/success, classifies failure source.
41
40
  */
42
41
  export function classifyToolCallOutcome(event: PluginHookAfterToolCallEvent): ToolCallOutcome {
43
- const resultObj = (event.result && typeof event.result === 'object') ? event.result as Record<string, unknown> : null;
44
- const details = resultObj?.details && typeof resultObj.details === 'object' ? resultObj.details as Record<string, unknown> : null;
42
+ // EP-01: Validate event.result at runtime instead of `as` cast
43
+ const resultObj = (event.result && typeof event.result === 'object' && !Array.isArray(event.result))
44
+ ? event.result as Record<string, unknown> // safe: guarded by typeof + Array.isArray checks above
45
+ : null;
46
+ const details = (resultObj && resultObj.details && typeof resultObj.details === 'object' && !Array.isArray(resultObj.details))
47
+ ? resultObj.details as Record<string, unknown> // safe: guarded by typeof + Array.isArray checks above
48
+ : null;
45
49
  const topExitCode = resultObj?.exitCode;
46
50
  const detailExitCode = details?.exitCode;
47
51
 
@@ -315,13 +319,26 @@ export function handleProbationFeedback(
315
319
 
316
320
  const WRITE_TOOLS = ['write', 'edit', 'apply_patch', 'write_file', 'edit_file', 'replace'];
317
321
 
322
+ /**
323
+ * Cooldown map for trigger controller decisions.
324
+ *
325
+ * PRI-363: This replaces the hidden map in PainDiagnosticGate.
326
+ * Core trigger-controller is stateless; plugin layer owns cooldown state.
327
+ *
328
+ * EP-05: Loop state freshness — each check reads fresh state from this map.
329
+ */
330
+ const TRIGGER_COOLDOWN_MAP = new Map<string, number>();
331
+
318
332
  /**
319
333
  * Evaluate whether a tool failure should trigger pain diagnosis.
320
334
  *
335
+ * PRI-363: Single-gate architecture — only TriggerController decides.
336
+ *
321
337
  * Combines:
322
338
  * 1. Write-tool check — only write tools on failures enter this path
323
- * 2. PEAT-B1 triage — if feature flag is on, check evidence triage
324
- * 3. PainDiagnosticGatecooldown + threshold check
339
+ * 2. PEAT-B1 triage — evidence triage (always enabled now)
340
+ * 3. PEAT-B2 trigger controller single source of truth for task creation
341
+ * 4. Cooldown tracking — plugin layer owns this state
325
342
  *
326
343
  * Returns a structured decision with reason and stage.
327
344
  */
@@ -333,7 +350,7 @@ export function evaluatePainAdmissionForToolCall(
333
350
  sessionState: SessionState | undefined,
334
351
  sessionId: string,
335
352
  workspaceDir: string,
336
- config: { get: (key: string) => unknown },
353
+ _config: { get: (key: string) => unknown },
337
354
  ): PainAdmissionDecision {
338
355
  // Only write-tool failures enter the pain path
339
356
  if (!WRITE_TOOLS.includes(event.toolName) || !outcome.isFailure) {
@@ -347,98 +364,64 @@ export function evaluatePainAdmissionForToolCall(
347
364
 
348
365
  const failureSource = outcome.failureSource ?? 'tool_failure';
349
366
 
350
- // PEAT-B1: Evidence triage (feature-flagged)
351
- // PEAT-B2: Trigger controller adds structured outcome + cooldown awareness
352
- const painTriageFlag = loadFeatureFlagFromConfig(workspaceDir, 'painEvidenceAdmission');
353
- if (painTriageFlag.enabled) {
354
- const sourceKind = resolveSourceKindFromToolFailure(event.toolName, failureSource);
355
- const triage = evaluateEvidenceTriage(sourceKind, observation.painScore);
356
-
357
- // PEAT-B2: Evaluate trigger controller for structured decision
358
- // Compute real cooldown state from PainDiagnosticGate's episode map
359
- // so trigger decision aligns with the gate's cooldown logic (EP-07).
360
- const cooldownActive = isCooldownActiveForEpisode(
361
- failureSource,
362
- sessionId,
363
- latestFailureState?.lastErrorHash,
364
- );
365
- const triggerDecision = evaluateTriggerController({
366
- triageResult: triage,
367
- isOwnerManual: false, // tool failures are never owner manual
368
- isCooldownActive: cooldownActive,
369
- isValid: true,
370
- score: observation.painScore,
371
- sessionId,
372
- });
373
-
374
- if (!triggerDecision.shouldCreateDiagnosticTask) {
375
- SystemLogger.log(workspaceDir, 'TRIGGER_DECISION', JSON.stringify({
376
- outcome: triggerDecision.outcome,
377
- sourceKind: triggerDecision.sourceKind,
378
- reason: triggerDecision.reason,
379
- nextAction: triggerDecision.nextAction,
380
- triageDecision: triggerDecision.triageDecision,
381
- tool: event.toolName,
382
- path: observation.relPath,
383
- }));
384
- return {
385
- admitted: false,
386
- stage: 'triage_evidence_only',
387
- reason: triggerDecision.reason,
388
- detail: `outcome=${triggerDecision.outcome}, sourceKind=${triggerDecision.sourceKind}, nextAction=${triggerDecision.nextAction}`,
389
- };
390
- }
391
- }
367
+ // Check cooldown before calling trigger controller
368
+ const cooldownActive = isTriggerCooldownActive(
369
+ failureSource,
370
+ sessionId,
371
+ latestFailureState?.lastErrorHash,
372
+ TRIGGER_COOLDOWN_MAP,
373
+ );
374
+
375
+ // PEAT-B1: Evidence triage (with consecutiveErrors and isRisky for upgrade logic)
376
+ const sourceKind = resolveSourceKindFromToolFailure(event.toolName, failureSource);
377
+ const triage = evaluateEvidenceTriage(sourceKind, observation.painScore, {
378
+ consecutiveErrors: (latestFailureState ?? sessionState)?.consecutiveErrors,
379
+ isRisky: observation.isRisk,
380
+ });
392
381
 
393
- // PainDiagnosticGate evaluation
394
- const diagnosticGate = evaluatePainDiagnosticGate({
395
- source: failureSource,
382
+ // PEAT-B2: Trigger controller — single source of truth for task creation
383
+ const triggerDecision = evaluateTriggerController({
384
+ triageResult: triage,
385
+ isOwnerManual: false, // tool failures are never owner manual
386
+ isCooldownActive: cooldownActive,
387
+ isValid: true,
396
388
  score: observation.painScore,
397
- currentGfi: (latestFailureState ?? sessionState)?.currentGfi ?? 0,
398
- consecutiveErrors: (latestFailureState ?? sessionState)?.consecutiveErrors ?? 0,
399
- isRisky: observation.isRisk,
400
- errorHash: latestFailureState?.lastErrorHash,
401
389
  sessionId,
402
- thresholds: {
403
- painTrigger: (config.get('thresholds.pain_trigger') as number) || 40,
404
- highSeverity: (config.get('severity_thresholds.high') as number) || 70,
405
- repeatedFailure: (config.get('thresholds.stuck_loops_trigger') as number) || 4,
406
- },
407
390
  });
408
391
 
409
- if (!diagnosticGate.shouldDiagnose) {
410
- SystemLogger.log(workspaceDir, 'PAIN_DIAGNOSE_SKIPPED', `Tool failure recorded as friction only: ${diagnosticGate.detail}; tool=${event.toolName}; path=${observation.relPath}`);
411
- let rejectPayload: string;
412
- try {
413
- rejectPayload = JSON.stringify({
414
- reason: diagnosticGate.reason,
415
- detail: diagnosticGate.detail,
416
- source: failureSource,
417
- sessionId,
418
- gfi: (latestFailureState ?? sessionState)?.currentGfi ?? 0,
419
- score: observation.painScore,
420
- });
421
- } catch (e) {
422
- SystemLogger.log(workspaceDir, 'PAYLOAD_SERIALIZE_FAILED', String(e));
423
- rejectPayload = JSON.stringify({ reason: diagnosticGate.reason, detail: '(log serialization failed)' });
424
- }
425
- SystemLogger.log(workspaceDir, 'PAIN_GATE_REJECTED', rejectPayload);
426
-
392
+ // Log the decision
393
+ SystemLogger.log(workspaceDir, 'TRIGGER_DECISION', JSON.stringify({
394
+ outcome: triggerDecision.outcome,
395
+ sourceKind: triggerDecision.sourceKind,
396
+ reason: triggerDecision.reason,
397
+ nextAction: triggerDecision.nextAction,
398
+ triageDecision: triggerDecision.triageDecision,
399
+ tool: event.toolName,
400
+ path: observation.relPath,
401
+ }));
402
+
403
+ // If trigger controller says yes, mark cooldown and admit
404
+ if (triggerDecision.shouldCreateDiagnosticTask) {
405
+ markEpisodeAsDiagnosed(
406
+ failureSource,
407
+ sessionId,
408
+ latestFailureState?.lastErrorHash,
409
+ TRIGGER_COOLDOWN_MAP,
410
+ );
427
411
  return {
428
- admitted: false,
429
- stage: 'gate_rejected',
430
- reason: diagnosticGate.reason,
431
- detail: diagnosticGate.detail,
432
- gateResult: { shouldDiagnose: false, reason: diagnosticGate.reason, detail: diagnosticGate.detail },
412
+ admitted: true,
413
+ stage: 'trigger_admitted',
414
+ reason: triggerDecision.reason,
415
+ detail: `outcome=${triggerDecision.outcome}, sourceKind=${triggerDecision.sourceKind}, nextAction=${triggerDecision.nextAction}`,
433
416
  };
434
417
  }
435
418
 
419
+ // Otherwise, reject with trigger controller's reason
436
420
  return {
437
- admitted: true,
438
- stage: 'gate_admitted',
439
- reason: diagnosticGate.reason,
440
- detail: diagnosticGate.detail,
441
- gateResult: { shouldDiagnose: true, reason: diagnosticGate.reason, detail: diagnosticGate.detail },
421
+ admitted: false,
422
+ stage: 'trigger_rejected',
423
+ reason: triggerDecision.reason,
424
+ detail: `outcome=${triggerDecision.outcome}, sourceKind=${triggerDecision.sourceKind}, nextAction=${triggerDecision.nextAction}`,
442
425
  };
443
426
  }
444
427
 
@@ -562,6 +545,13 @@ export function emitPainIfAdmitted(
562
545
 
563
546
  export { buildTrajectoryEvidence } from './trajectory-evidence.js';
564
547
 
548
+ /**
549
+ * Reset trigger cooldown state (for tests).
550
+ */
551
+ export function resetTriggerCooldownForTest(): void {
552
+ clearCooldownState(TRIGGER_COOLDOWN_MAP);
553
+ }
554
+
565
555
  // ── Source Classification ────────────────────────────────────────────────────
566
556
 
567
557
  /**
@@ -69,23 +69,17 @@ export interface ToolCallObservation {
69
69
  /**
70
70
  * Result of evaluating whether a tool failure should trigger pain diagnosis.
71
71
  *
72
- * Encapsulates the combined triage + PainDiagnosticGate decision.
72
+ * Encapsulates the single-gate trigger controller decision.
73
73
  */
74
74
  export interface PainAdmissionDecision {
75
75
  /** Whether the tool failure should proceed to pain emission */
76
76
  readonly admitted: boolean;
77
77
  /** The admission stage that made the decision */
78
- readonly stage: 'triage_evidence_only' | 'gate_rejected' | 'gate_admitted' | 'not_applicable';
78
+ readonly stage: 'not_applicable' | 'trigger_admitted' | 'trigger_rejected';
79
79
  /** Human-readable reason for the decision */
80
80
  readonly reason: string;
81
81
  /** Detail about the decision */
82
82
  readonly detail: string;
83
- /** The diagnostic gate result (if gate was evaluated) */
84
- readonly gateResult?: {
85
- readonly shouldDiagnose: boolean;
86
- readonly reason: string;
87
- readonly detail: string;
88
- };
89
83
  }
90
84
 
91
85
  // ── Friction Update Result ──────────────────────────────────────────────────
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Raw Observation Adapter — PRI-362
3
+ *
4
+ * Unified source-kind resolution from RawObservation.
5
+ *
6
+ * Replaces scattered resolveSourceKindFrom* functions with a single
7
+ * field-driven adapter that maps observation fields to SourceKind.
8
+ *
9
+ * Field precedence (highest to lowest):
10
+ * 1. isManualEntry → owner_reported
11
+ * 2. isGateBlock → rulehost_block
12
+ * 3. isSubagentError → subagent_error
13
+ * 4. isRateLimit → rate_limit (if true)
14
+ * 5. toolName === 'pain' / 'skill:pain' → agent_on_owner_request (with openclaw_context_bound) / owner_reported
15
+ * 6. failureSource → tool_failure / dispatch_error
16
+ * 7. isGfiTriggered → gfi_threshold
17
+ * 8. detectionSource → llm_paralysis / semantic / empathy_inferred / unknown
18
+ * 9. Fallback → unknown
19
+ *
20
+ * ERR checklist:
21
+ * - ERR-001: Source kind resolved from runtime values, no `as` casts.
22
+ * - ERR-002: Every path returns a valid SourceKind (fallback to 'unknown').
23
+ * - EP-01: Runtime values validated before use.
24
+ */
25
+
26
+ import type { SourceKind } from '@principles/core/runtime-v2';
27
+ import type { RawObservation } from './raw-observation-types.js';
28
+
29
+ /**
30
+ * Resolve SourceKind from a unified RawObservation.
31
+ *
32
+ * This function replaces the scattered resolveSourceKindFrom* functions
33
+ * and provides a single entry point for source-kind classification.
34
+ *
35
+ * Field precedence is explicitly defined in the function body to ensure
36
+ * deterministic behavior and make the logic easy to understand and test.
37
+ */
38
+ export function resolveSourceKind(observation: RawObservation): SourceKind {
39
+ const {
40
+ isManualEntry,
41
+ isGateBlock,
42
+ isSubagentError,
43
+ isRateLimit,
44
+ toolName,
45
+ failureSource,
46
+ isGfiTriggered,
47
+ detectionSource,
48
+ nonZeroExit,
49
+ timedOut,
50
+ toolNotFound,
51
+ } = observation;
52
+
53
+ // Priority 1: Manual entry (CLI, owner-reported)
54
+ if (isManualEntry) {
55
+ return 'owner_reported';
56
+ }
57
+
58
+ // Priority 2: Gate block
59
+ if (isGateBlock) {
60
+ return 'rulehost_block';
61
+ }
62
+
63
+ // Priority 3: Subagent error
64
+ if (isSubagentError) {
65
+ return 'subagent_error';
66
+ }
67
+
68
+ // Priority 4: Provider rate limit (explicit true/false)
69
+ if (isRateLimit === true) {
70
+ return 'rate_limit';
71
+ }
72
+ if (isRateLimit === false) {
73
+ return 'provider_failure';
74
+ }
75
+
76
+ // Priority 5: Manual pain tool
77
+ if (toolName === 'pain' || toolName === 'skill:pain') {
78
+ // Match resolveSourceKindFromToolFailure behavior:
79
+ // openclaw_context_bound → agent_on_owner_request
80
+ // other provenance or undefined → owner_reported
81
+ if (observation.provenance === 'openclaw_context_bound') {
82
+ return 'agent_on_owner_request';
83
+ }
84
+ return 'owner_reported';
85
+ }
86
+
87
+ // Priority 6: GFI threshold (must check before failure source for LLM detection path)
88
+ if (isGfiTriggered) {
89
+ return 'gfi_threshold';
90
+ }
91
+
92
+ // Priority 7: Tool failure / dispatch error
93
+ if (failureSource) {
94
+ // Match resolveSourceKindFromToolFailure behavior:
95
+ // dispatch_error → dispatch_error, anything else → tool_failure
96
+ if (failureSource === 'dispatch_error') {
97
+ return 'dispatch_error';
98
+ }
99
+ return 'tool_failure';
100
+ }
101
+
102
+ // Infer failureSource from tool failure indicators if not explicitly set
103
+ if (toolNotFound) {
104
+ return 'dispatch_error';
105
+ }
106
+
107
+ // Match classifyToolFailureSource behavior: unknown tool name → dispatch_error
108
+ // BUT only if this looks like a tool failure context (has other tool fields)
109
+ // Otherwise, this is likely a non-tool observation (e.g., LLM detection)
110
+ const hasToolContext = toolName !== undefined || nonZeroExit || timedOut || toolNotFound;
111
+ if (hasToolContext && (!toolName || toolName.trim() === '')) {
112
+ return 'dispatch_error';
113
+ }
114
+
115
+ // Exit code-based detection: non-zero exit or timeout → tool_failure
116
+ if (nonZeroExit || timedOut) {
117
+ return 'tool_failure';
118
+ }
119
+
120
+ // Priority 8: LLM detection source
121
+ if (detectionSource) {
122
+ // Match resolveSourceKindFromLlmDetection behavior:
123
+ if (detectionSource === 'llm_paralysis') {
124
+ return 'llm_paralysis';
125
+ }
126
+ if (detectionSource.startsWith('llm_')) {
127
+ return 'semantic';
128
+ }
129
+ if (detectionSource === 'user_empathy') {
130
+ return 'empathy_inferred';
131
+ }
132
+ }
133
+
134
+ // Fallback: unknown
135
+ return 'unknown';
136
+ }
137
+
138
+ /**
139
+ * Resolve SourceKind from tool failure context (legacy wrapper).
140
+ *
141
+ * This is a thin wrapper around resolveSourceKind for compatibility.
142
+ * It constructs a RawObservation from the old function signature.
143
+ *
144
+ * @deprecated Use resolveSourceKind directly with RawObservation.
145
+ */
146
+ export function resolveSourceKindFromToolFailure(
147
+ toolName: string | undefined,
148
+ failureSource: 'tool_failure' | 'dispatch_error',
149
+ provenance?: 'openclaw_context_bound' | 'owner_reported_no_host_trace' | 'automatic_hook',
150
+ ): SourceKind {
151
+ const observation: RawObservation = {
152
+ observedAt: new Date().toISOString(),
153
+ toolName,
154
+ failureSource,
155
+ provenance,
156
+ };
157
+ return resolveSourceKind(observation);
158
+ }
159
+
160
+ /**
161
+ * Resolve SourceKind from LLM detection context (legacy wrapper).
162
+ *
163
+ * This is a thin wrapper around resolveSourceKind for compatibility.
164
+ *
165
+ * @deprecated Use resolveSourceKind directly with RawObservation.
166
+ */
167
+ export function resolveSourceKindFromLlmDetection(
168
+ detectionSource: string,
169
+ isGfiTriggered: boolean,
170
+ ): SourceKind {
171
+ const observation: RawObservation = {
172
+ observedAt: new Date().toISOString(),
173
+ detectionSource,
174
+ isGfiTriggered,
175
+ };
176
+ return resolveSourceKind(observation);
177
+ }
178
+
179
+ /**
180
+ * Resolve SourceKind from gate block context (legacy wrapper).
181
+ *
182
+ * @deprecated Use resolveSourceKind directly with RawObservation.
183
+ */
184
+ export function resolveSourceKindFromGateBlock(): SourceKind {
185
+ const observation: RawObservation = {
186
+ observedAt: new Date().toISOString(),
187
+ isGateBlock: true,
188
+ };
189
+ return resolveSourceKind(observation);
190
+ }
191
+
192
+ /**
193
+ * Resolve SourceKind from manual command context (legacy wrapper).
194
+ *
195
+ * @deprecated Use resolveSourceKind directly with RawObservation.
196
+ */
197
+ export function resolveSourceKindFromCommand(): SourceKind {
198
+ const observation: RawObservation = {
199
+ observedAt: new Date().toISOString(),
200
+ isManualEntry: true,
201
+ };
202
+ return resolveSourceKind(observation);
203
+ }
204
+
205
+ /**
206
+ * Resolve SourceKind from provider context (legacy wrapper).
207
+ *
208
+ * @deprecated Use resolveSourceKind directly with RawObservation.
209
+ */
210
+ export function resolveSourceKindFromProvider(
211
+ isRateLimit: boolean,
212
+ ): SourceKind {
213
+ const observation: RawObservation = {
214
+ observedAt: new Date().toISOString(),
215
+ isRateLimit,
216
+ };
217
+ return resolveSourceKind(observation);
218
+ }
219
+
220
+ /**
221
+ * Resolve SourceKind from subagent context (legacy wrapper).
222
+ *
223
+ * @deprecated Use resolveSourceKind directly with RawObservation.
224
+ */
225
+ export function resolveSourceKindFromSubagent(): SourceKind {
226
+ const observation: RawObservation = {
227
+ observedAt: new Date().toISOString(),
228
+ isSubagentError: true,
229
+ };
230
+ return resolveSourceKind(observation);
231
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Raw Observation Types — PRI-362
3
+ *
4
+ * Source adapter layer that normalizes diverse hook contexts into a unified
5
+ * observation model before mapping to SourceKind.
6
+ *
7
+ * This replaces scattered resolveSourceKindFrom* functions with a single
8
+ * field-driven adapter.
9
+ *
10
+ * ERR checklist:
11
+ * - ERR-001: No `as` casts; validate unknown payload field-by-field.
12
+ * - ERR-002: Every decision carries reason + nextAction.
13
+ * - EP-01: Source adapter validates before use.
14
+ */
15
+
16
+ /**
17
+ * Raw observation from a source adapter.
18
+ *
19
+ * This is the input to resolveSourceKind. It contains all possible
20
+ * context fields that different sources may provide. The adapter
21
+ * reads only the fields it needs based on the observation source.
22
+ */
23
+ export interface RawObservation {
24
+ /** When the observation was made (ISO timestamp) */
25
+ readonly observedAt: string;
26
+ /** Workspace identifier */
27
+ readonly workspaceId?: string;
28
+ /** Session identifier */
29
+ readonly sessionId?: string;
30
+ /** Trace identifier for correlation */
31
+ readonly traceId?: string;
32
+
33
+ // ── Tool Failure Fields ────────────────────────────────────────────────
34
+ /** Tool name (for after_tool_call hook) */
35
+ readonly toolName?: string;
36
+ /** Failure source classification */
37
+ readonly failureSource?: 'tool_failure' | 'dispatch_error';
38
+ /** Whether the tool call exited with non-zero code */
39
+ readonly nonZeroExit?: boolean;
40
+ /** Whether the tool call timed out */
41
+ readonly timedOut?: boolean;
42
+ /** Whether the tool does not exist */
43
+ readonly toolNotFound?: boolean;
44
+
45
+ // ── LLM Detection Fields ───────────────────────────────────────────────
46
+ /** Detection source identifier */
47
+ readonly detectionSource?: string;
48
+ /** Whether GFI threshold was crossed */
49
+ readonly isGfiTriggered?: boolean;
50
+
51
+ // ── Provider Fields ───────────────────────────────────────────────────
52
+ /** Whether the failure was a rate limit (429) */
53
+ readonly isRateLimit?: boolean;
54
+
55
+ // ── Gate Block Fields ────────────────────────────────────────────────
56
+ /** Whether this observation came from a gate block */
57
+ readonly isGateBlock?: boolean;
58
+
59
+ // ── Manual Entry Fields ───────────────────────────────────────────────
60
+ /** Whether this was a manual CLI entry */
61
+ readonly isManualEntry?: boolean;
62
+ /** Provenance: how trustworthy and context-bound is the observation */
63
+ readonly provenance?: 'openclaw_context_bound' | 'owner_reported_no_host_trace' | 'automatic_hook';
64
+
65
+ // ── Subagent Fields ───────────────────────────────────────────────────
66
+ /** Whether this observation came from a subagent error */
67
+ readonly isSubagentError?: boolean;
68
+
69
+ // ── Raw Payload ──────────────────────────────────────────────────────
70
+ /**
71
+ * Raw payload from the source.
72
+ *
73
+ * This is always `unknown` (ERR-005). Source adapters validate only
74
+ * enough to identify the source and capture bounded context.
75
+ */
76
+ readonly payload?: unknown;
77
+ }