principles-disciple 1.174.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.
@@ -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
  *
@@ -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
- return this._loadFromActivationsTable(this.workspaceDir, supportsContextV2);
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`);
@@ -312,6 +351,15 @@ export class RuleHost {
312
351
  const ruleId = typeof contentObj['ruleId'] === 'string'
313
352
  ? contentObj['ruleId']
314
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;
315
363
  const implId = `act-impl-${activationId}`;
316
364
  const moduleExports = loadRuleImplementationModule(implementationCode, implId);
317
365
  if (!moduleExports || typeof moduleExports.callEvaluate !== 'function') {
@@ -351,7 +399,13 @@ export class RuleHost {
351
399
  const result = rawResult;
352
400
  if (result.matched && (result.decision === 'block' || result.decision === 'requireApproval')) {
353
401
  result.ruleId = ruleId;
354
- result.principleId = meta.ruleId ?? ruleId;
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;
355
409
  }
356
410
  return result;
357
411
  },
@@ -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');
@@ -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.174.0",
5
+ "version": "1.175.0",
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.174.0",
3
+ "version": "1.175.0",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",