principles-disciple 1.173.0 → 1.175.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/dist/core/rule-host.d.ts +21 -0
- package/dist/core/rule-host.js +67 -12
- package/dist/hooks/prompt.js +14 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/core/rule-host.d.ts
CHANGED
|
@@ -45,6 +45,16 @@ export declare class RuleHost {
|
|
|
45
45
|
private activationFingerprint;
|
|
46
46
|
private cachedImplementations;
|
|
47
47
|
private sqliteConnection;
|
|
48
|
+
/**
|
|
49
|
+
* R2-RH-002: Guards the "armed but empty" warn so it fires at most once per
|
|
50
|
+
* RuleHost instance. Without this, the 0-rules path (workspaceDir missing OR
|
|
51
|
+
* zero active code_tool_hook activations) returns [] silently on every
|
|
52
|
+
* evaluation — an observability gap (rc-9-no-silent-fallback). The warn is
|
|
53
|
+
* NOT a degradation fallback (RuleHost correctly has no opinion when empty);
|
|
54
|
+
* it makes the empty-armed state visible so operators can investigate why
|
|
55
|
+
* no live rules are loaded.
|
|
56
|
+
*/
|
|
57
|
+
private emptyLoadWarnEmitted;
|
|
48
58
|
constructor(stateDir: string, logger?: RuleHostLogger, options?: RuleHostOptions);
|
|
49
59
|
/**
|
|
50
60
|
* Update the logger sink on a cached RuleHost instance.
|
|
@@ -78,6 +88,17 @@ export declare class RuleHost {
|
|
|
78
88
|
* → JOIN pi_artifacts for content_json → extract implementationCode → compile
|
|
79
89
|
*/
|
|
80
90
|
private _loadActiveCodeImplementations;
|
|
91
|
+
/**
|
|
92
|
+
* R2-RH-002: Emit the "armed but empty" warn at most once per RuleHost
|
|
93
|
+
* instance. The warn is cached via `emptyLoadWarnEmitted` so repeated
|
|
94
|
+
* evaluations (which all hit the empty path) do not spam the log.
|
|
95
|
+
*
|
|
96
|
+
* This is an observability signal, NOT a degradation fallback — RuleHost
|
|
97
|
+
* correctly returns "no opinion" when there are no active rules. The warn
|
|
98
|
+
* makes the empty state visible so operators can distinguish "RuleHost is
|
|
99
|
+
* working but has no rules" from "RuleHost is broken" (rc-9-no-silent-fallback).
|
|
100
|
+
*/
|
|
101
|
+
private _emitEmptyLoadWarn;
|
|
81
102
|
/**
|
|
82
103
|
* Load active code implementations from the activations table (code_tool_hook channel).
|
|
83
104
|
*
|
package/dist/core/rule-host.js
CHANGED
|
@@ -51,6 +51,16 @@ export class RuleHost {
|
|
|
51
51
|
activationFingerprint = null;
|
|
52
52
|
cachedImplementations = [];
|
|
53
53
|
sqliteConnection = null;
|
|
54
|
+
/**
|
|
55
|
+
* R2-RH-002: Guards the "armed but empty" warn so it fires at most once per
|
|
56
|
+
* RuleHost instance. Without this, the 0-rules path (workspaceDir missing OR
|
|
57
|
+
* zero active code_tool_hook activations) returns [] silently on every
|
|
58
|
+
* evaluation — an observability gap (rc-9-no-silent-fallback). The warn is
|
|
59
|
+
* NOT a degradation fallback (RuleHost correctly has no opinion when empty);
|
|
60
|
+
* it makes the empty-armed state visible so operators can investigate why
|
|
61
|
+
* no live rules are loaded.
|
|
62
|
+
*/
|
|
63
|
+
emptyLoadWarnEmitted = false;
|
|
54
64
|
constructor(stateDir, logger = console, options) {
|
|
55
65
|
this.stateDir = stateDir;
|
|
56
66
|
this.logger = logger;
|
|
@@ -85,6 +95,7 @@ export class RuleHost {
|
|
|
85
95
|
this.cachedImplementations = [];
|
|
86
96
|
this.activationFingerprint = null;
|
|
87
97
|
this.implementationSources.clear();
|
|
98
|
+
this.emptyLoadWarnEmitted = false;
|
|
88
99
|
}
|
|
89
100
|
evaluateDetailed(input) {
|
|
90
101
|
try {
|
|
@@ -137,16 +148,38 @@ export class RuleHost {
|
|
|
137
148
|
*/
|
|
138
149
|
_loadActiveCodeImplementations(supportsContextV2) {
|
|
139
150
|
if (!this.workspaceDir) {
|
|
151
|
+
this._emitEmptyLoadWarn('workspaceDir not configured — RuleHost cannot load active code_tool_hook rules', 'Provide workspaceDir when constructing RuleHost (required for code_tool_hook channel)');
|
|
140
152
|
return [];
|
|
141
153
|
}
|
|
142
154
|
try {
|
|
143
|
-
|
|
155
|
+
const loaded = this._loadFromActivationsTable(this.workspaceDir, supportsContextV2);
|
|
156
|
+
if (loaded.length === 0) {
|
|
157
|
+
this._emitEmptyLoadWarn('armed but empty — 0 active code_tool_hook activations loaded (RuleHost will not block or require approval)', 'If this is unexpected, run `pd runtime activation list --channel code_tool_hook` to inspect activations, or `pd runtime activation promote` to enable a live rule');
|
|
158
|
+
}
|
|
159
|
+
return loaded;
|
|
144
160
|
}
|
|
145
161
|
catch (activationError) {
|
|
146
162
|
this.logger.warn?.(`[RuleHost] Failed to load code_tool_hook activations: ${String(activationError)}`);
|
|
147
163
|
return [];
|
|
148
164
|
}
|
|
149
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* R2-RH-002: Emit the "armed but empty" warn at most once per RuleHost
|
|
168
|
+
* instance. The warn is cached via `emptyLoadWarnEmitted` so repeated
|
|
169
|
+
* evaluations (which all hit the empty path) do not spam the log.
|
|
170
|
+
*
|
|
171
|
+
* This is an observability signal, NOT a degradation fallback — RuleHost
|
|
172
|
+
* correctly returns "no opinion" when there are no active rules. The warn
|
|
173
|
+
* makes the empty state visible so operators can distinguish "RuleHost is
|
|
174
|
+
* working but has no rules" from "RuleHost is broken" (rc-9-no-silent-fallback).
|
|
175
|
+
*/
|
|
176
|
+
_emitEmptyLoadWarn(reason, nextAction) {
|
|
177
|
+
if (this.emptyLoadWarnEmitted)
|
|
178
|
+
return;
|
|
179
|
+
this.emptyLoadWarnEmitted = true;
|
|
180
|
+
this.logger.warn?.(`[RuleHost] ${reason}. ` +
|
|
181
|
+
`nextAction=${nextAction}`);
|
|
182
|
+
}
|
|
150
183
|
/**
|
|
151
184
|
* Load active code implementations from the activations table (code_tool_hook channel).
|
|
152
185
|
*
|
|
@@ -169,7 +202,7 @@ export class RuleHost {
|
|
|
169
202
|
const db = sqliteConn.getDb();
|
|
170
203
|
const rows = db.prepare(`
|
|
171
204
|
SELECT a.activation_id, a.artifact_id, a.target_ref, a.action,
|
|
172
|
-
p.content_json, p.source_rule_id
|
|
205
|
+
p.content_json, p.source_rule_id, p.source_principle_id
|
|
173
206
|
FROM activations a
|
|
174
207
|
JOIN pi_artifacts p ON a.artifact_id = p.artifact_id
|
|
175
208
|
WHERE a.channel = 'code_tool_hook' AND a.deactivated_at IS NULL
|
|
@@ -185,7 +218,7 @@ export class RuleHost {
|
|
|
185
218
|
continue;
|
|
186
219
|
const record = row;
|
|
187
220
|
fingerprintParts.push([
|
|
188
|
-
record['activation_id'], record['artifact_id'], record['target_ref'], record['action'], record['content_json'], record['source_rule_id'],
|
|
221
|
+
record['activation_id'], record['artifact_id'], record['target_ref'], record['action'], record['content_json'], record['source_rule_id'], record['source_principle_id'],
|
|
189
222
|
].map((value) => typeof value === 'string' ? value : '').join('\u0001'));
|
|
190
223
|
}
|
|
191
224
|
const fingerprint = fingerprintParts.join('\u0002');
|
|
@@ -255,6 +288,12 @@ export class RuleHost {
|
|
|
255
288
|
const artifactId = typeof r['artifact_id'] === 'string' ? r['artifact_id'] : '';
|
|
256
289
|
const contentJson = typeof r['content_json'] === 'string' ? r['content_json'] : '';
|
|
257
290
|
const sourceRuleId = typeof r['source_rule_id'] === 'string' ? r['source_rule_id'] : null;
|
|
291
|
+
// R2-RH-004: Extract source_principle_id from pi_artifacts (DB lineage field).
|
|
292
|
+
// Previously this column was never SELECTed, so principleId was always
|
|
293
|
+
// derived from meta.ruleId (a rule ID, not a principle ID) — violating
|
|
294
|
+
// rc-6-lineage-consistency. Downstream gate.ts:135/164 recorded the
|
|
295
|
+
// wrong principleId in eventLog.recordRuleEnforced().
|
|
296
|
+
const sourcePrincipleId = typeof r['source_principle_id'] === 'string' ? r['source_principle_id'] : null;
|
|
258
297
|
const action = typeof r['action'] === 'string' ? r['action'] : '';
|
|
259
298
|
if (!activationId || !artifactId || !contentJson) {
|
|
260
299
|
this.logger.warn?.(`[RuleHost] Activation row missing required fields, skipping`);
|
|
@@ -271,17 +310,18 @@ export class RuleHost {
|
|
|
271
310
|
continue;
|
|
272
311
|
}
|
|
273
312
|
if (activationMode === 'shadow') {
|
|
274
|
-
//
|
|
275
|
-
// NOT enter mergeDecisions (no block /
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
313
|
+
// PRI-489 (seed-MVP readiness): shadow activations are
|
|
314
|
+
// observation-only and do NOT enter mergeDecisions (no block /
|
|
315
|
+
// requireApproval). Owner approval creates a shadow activation
|
|
316
|
+
// first; the only shadow -> live transition is `pd activation
|
|
317
|
+
// promote --activation-id ... --confirm` (atomic action rewrite
|
|
318
|
+
// in SqliteActivationStateStore.promoteActivation). Surface a
|
|
279
319
|
// structured reason + nextAction so the degradation is observable
|
|
280
|
-
// (rc-9-no-silent-fallback), not silent. Fires once per
|
|
281
|
-
// change (cached), not per evaluation.
|
|
320
|
+
// (rc-9-no-silent-fallback), not silent. Fires once per
|
|
321
|
+
// fingerprint change (cached), not per evaluation.
|
|
282
322
|
this.logger.warn?.(`[RuleHost] Activation ${activationId}: loaded in shadow (observation-only) mode; ` +
|
|
283
323
|
'it will NOT block or require approval (shadowDecisions only). ' +
|
|
284
|
-
`nextAction=run \`pd
|
|
324
|
+
`nextAction=run \`pd activation promote --activation-id ${activationId} --confirm\` to enable live blocking, ` +
|
|
285
325
|
'or leave as-is for shadow observation.');
|
|
286
326
|
}
|
|
287
327
|
try {
|
|
@@ -311,6 +351,15 @@ export class RuleHost {
|
|
|
311
351
|
const ruleId = typeof contentObj['ruleId'] === 'string'
|
|
312
352
|
? contentObj['ruleId']
|
|
313
353
|
: (sourceRuleId ?? artifactId);
|
|
354
|
+
// R2-RH-004: Extract principleId from artifact content_json.
|
|
355
|
+
// Previously this field was never read, so result.principleId was
|
|
356
|
+
// always set to meta.ruleId (a rule ID) — violating
|
|
357
|
+
// rc-6-lineage-consistency. Precedence: contentJson.principleId
|
|
358
|
+
// (artifact payload) → pi_artifacts.source_principle_id (DB lineage)
|
|
359
|
+
// → meta.ruleId (rule ID fallback) → ruleId (last resort).
|
|
360
|
+
const contentPrincipleId = typeof contentObj['principleId'] === 'string'
|
|
361
|
+
? contentObj['principleId']
|
|
362
|
+
: null;
|
|
314
363
|
const implId = `act-impl-${activationId}`;
|
|
315
364
|
const moduleExports = loadRuleImplementationModule(implementationCode, implId);
|
|
316
365
|
if (!moduleExports || typeof moduleExports.callEvaluate !== 'function') {
|
|
@@ -350,7 +399,13 @@ export class RuleHost {
|
|
|
350
399
|
const result = rawResult;
|
|
351
400
|
if (result.matched && (result.decision === 'block' || result.decision === 'requireApproval')) {
|
|
352
401
|
result.ruleId = ruleId;
|
|
353
|
-
|
|
402
|
+
// R2-RH-004: principleId precedence — contentJson.principleId
|
|
403
|
+
// (artifact payload) → source_principle_id (DB lineage) →
|
|
404
|
+
// meta.ruleId (rule ID fallback) → ruleId (last resort).
|
|
405
|
+
// Previously this was `meta.ruleId ?? ruleId`, which always
|
|
406
|
+
// resolved to a rule ID (never a principle ID) because
|
|
407
|
+
// isRuleHostMeta guarantees meta.ruleId is a non-empty string.
|
|
408
|
+
result.principleId = contentPrincipleId ?? sourcePrincipleId ?? meta.ruleId ?? ruleId;
|
|
354
409
|
}
|
|
355
410
|
return result;
|
|
356
411
|
},
|
package/dist/hooks/prompt.js
CHANGED
|
@@ -984,6 +984,20 @@ function scoreFromSeverityForSpec(severity, wctx) {
|
|
|
984
984
|
return Number(wctx.config.get('empathy_engine.penalties.mild') ?? 10);
|
|
985
985
|
}
|
|
986
986
|
function resolveEmpathyObserver(wctx, logger) {
|
|
987
|
+
// F15 (PRI-442): empathy_observer flag is registered as MVP-Quiet (default
|
|
988
|
+
// off) per ADR-0014 §2.5. Previously this flag was a dead registration —
|
|
989
|
+
// registered in feature-flag-contract.ts but never read anywhere, violating
|
|
990
|
+
// the PRI-239 constraint "Only flags with real consumption paths are
|
|
991
|
+
// registered". Now resolveEmpathyObserver consumes the flag: when disabled
|
|
992
|
+
// (the default), the LLM observer pipeline is short-circuited and the
|
|
993
|
+
// keyword-matcher path (gated by empathy_engine.enabled above) continues
|
|
994
|
+
// to run independently. This mirrors the pattern used by
|
|
995
|
+
// shouldStartEvolutionWorker / shouldStartCorrectionObserver.
|
|
996
|
+
const empathyFlag = loadFeatureFlagFromConfig(wctx.workspaceDir, 'empathy_observer', logger);
|
|
997
|
+
if (!empathyFlag.enabled) {
|
|
998
|
+
logger?.debug?.(`[PD:Empathy] empathy_observer flag disabled (source=${empathyFlag.source}) — LLM observer pipeline skipped`);
|
|
999
|
+
return null;
|
|
1000
|
+
}
|
|
987
1001
|
try {
|
|
988
1002
|
const loader = new WorkflowFunnelLoader(wctx.stateDir);
|
|
989
1003
|
const funnel = loader.getFunnel('pd-empathy-observer');
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
5
|
+
"version": "1.175.0",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onCapabilities": [
|
|
8
8
|
"hook"
|