principles-disciple 1.116.0 → 1.118.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.116.0",
5
+ "version": "1.118.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.116.0",
3
+ "version": "1.118.0",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/bundle.js",
@@ -923,6 +923,48 @@ function installTargetDependencies() {
923
923
  }
924
924
  }
925
925
 
926
+ /**
927
+ * Verify injected workspace packages have their transitive dependencies installed.
928
+ * npm install --omit=dev in the target doesn't resolve deps of manually-copied packages,
929
+ * so transitive deps like @earendil-works/pi-agent-core may be missing after production install.
930
+ */
931
+ function verifyInjectedWorkspaceDeps() {
932
+ const corePkgDir = join(INSTALL_DIR, 'node_modules', '@principles', 'core');
933
+ if (!existsSync(corePkgDir)) return;
934
+
935
+ const corePkgPath = join(corePkgDir, 'package.json');
936
+ if (!existsSync(corePkgPath)) return;
937
+
938
+ let corePkg;
939
+ try {
940
+ corePkg = JSON.parse(readFileSync(corePkgPath, 'utf-8'));
941
+ } catch (error) {
942
+ console.error(` ❌ Failed to parse @principles/core/package.json: ${error.message}`);
943
+ process.exit(1);
944
+ }
945
+
946
+ const deps = corePkg.dependencies || {};
947
+ const missing = [];
948
+ for (const [dep, version] of Object.entries(deps)) {
949
+ if (!existsSync(join(INSTALL_DIR, 'node_modules', dep))) {
950
+ missing.push(`${dep}@${version}`);
951
+ }
952
+ }
953
+
954
+ if (missing.length === 0) return;
955
+
956
+ console.log(` ⚠️ ${missing.length} transitive dependenc${missing.length === 1 ? 'y' : 'ies'} of @principles/core missing, installing...`);
957
+ try {
958
+ execSync(`npm install ${missing.join(' ')} --no-audit --no-fund --prefer-offline`, {
959
+ cwd: INSTALL_DIR,
960
+ stdio: 'pipe'
961
+ });
962
+ console.log(' ✅ Transitive dependencies installed');
963
+ } catch (error) {
964
+ console.warn(` ⚠️ Failed to install transitive dependencies: ${error.message}`);
965
+ }
966
+ }
967
+
926
968
  /**
927
969
  * Clean stale backups.
928
970
  */
@@ -1269,6 +1311,7 @@ function main() {
1269
1311
 
1270
1312
  injectLocalWorkspacePackages();
1271
1313
  installTargetDependencies();
1314
+ verifyInjectedWorkspaceDeps();
1272
1315
  verifyPdCliShim();
1273
1316
 
1274
1317
  console.log('\n🔍 Verifying installed plugin can load native dependencies...');
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * Rule Host — Constrained execution layer for active code implementations
3
3
  *
4
- * PURPOSE: Load active code implementations from the principle-tree ledger,
5
- * execute them in a constrained node:vm context, and merge their decisions.
4
+ * PURPOSE: Load active code implementations from the principle-tree ledger
5
+ * AND from the activations table (code_tool_hook channel), execute them in a
6
+ * constrained node:vm context, and merge their decisions.
6
7
  *
7
8
  * ARCHITECTURE:
8
9
  * - Constructor takes stateDir to access the principle-tree ledger
10
+ * - Optional workspaceDir enables reading code_tool_hook activations from SQLite
9
11
  * - evaluate(input) loads active code implementations and runs them
10
12
  * - Each implementation executes in an isolated vm context with minimal helpers
11
13
  * - Decision merge: block short-circuits, requireApproval collects, allow is implicit
@@ -27,6 +29,7 @@ import {
27
29
  import { loadEntrySource } from './code-implementation-storage.js';
28
30
  import { createRuleHostHelpers } from '@principles/core/runtime-v2';
29
31
  import { mergeDecisions } from '@principles/core/runtime-v2';
32
+ import { SqliteConnection } from '@principles/core/runtime-v2';
30
33
  import { loadRuleImplementationModule } from './rule-implementation-runtime.js';
31
34
  import type {
32
35
  RuleHostInput,
@@ -39,13 +42,37 @@ import type { Implementation } from '../types/principle-tree-schema.js';
39
42
  import type { RuleHostLogger } from '@principles/core/runtime-v2';
40
43
  export type { RuleHostLogger } from '@principles/core/runtime-v2';
41
44
 
45
+ export interface RuleHostOptions {
46
+ /** Workspace directory for SQLite access. When provided, RuleHost also loads code_tool_hook activations from the activations table. */
47
+ workspaceDir?: string;
48
+ }
49
+
50
+ /**
51
+ * Type guard for RuleHostMeta from untrusted module exports.
52
+ * Validates all four required string fields (EP-01: no `as` bypass at trust boundary).
53
+ */
54
+ function isRuleHostMeta(value: unknown): value is RuleHostMeta {
55
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
56
+ return false;
57
+ }
58
+ const v = value as Record<string, unknown>;
59
+ return (
60
+ typeof v['name'] === 'string' &&
61
+ typeof v['version'] === 'string' &&
62
+ typeof v['ruleId'] === 'string' &&
63
+ typeof v['coversCondition'] === 'string'
64
+ );
65
+ }
66
+
42
67
  export class RuleHost {
43
68
  private readonly stateDir: string;
44
69
  private readonly logger: RuleHostLogger;
70
+ private readonly workspaceDir: string | null;
45
71
 
46
- constructor(stateDir: string, logger: RuleHostLogger = console) {
72
+ constructor(stateDir: string, logger: RuleHostLogger = console, options?: RuleHostOptions) {
47
73
  this.stateDir = stateDir;
48
74
  this.logger = logger;
75
+ this.workspaceDir = options?.workspaceDir ?? null;
49
76
  }
50
77
 
51
78
  /**
@@ -72,11 +99,21 @@ export class RuleHost {
72
99
  }
73
100
 
74
101
  /**
75
- * Load active code implementations from the ledger.
76
- * Filters by type=code and lifecycleState=active, then attempts to
77
- * compile each implementation's code asset via node:vm.
102
+ * Load active code implementations from the ledger AND the activations table.
103
+ *
104
+ * Sources (merged, deduplicated by implId):
105
+ * 1. principle-tree-ledger.json: implementations with lifecycleState='active' and type='code'
106
+ * 2. activations table: code_tool_hook channel activations (when workspaceDir is provided)
107
+ *
108
+ * This bridges the gap between RuleHostWriter (which records activation metadata in SQLite)
109
+ * and RuleHost enforcement (which previously only read from the ledger JSON file).
110
+ * See BUG-001 / ERR-011 / ERR-035.
78
111
  */
79
112
  private _loadActiveCodeImplementations(): LoadedImplementation[] {
113
+ const loaded: LoadedImplementation[] = [];
114
+ const seenImplIds = new Set<string>();
115
+
116
+ // Source 1: principle-tree-ledger.json (existing path)
80
117
  try {
81
118
  const activeAllTypes = listImplementationsByLifecycleState(
82
119
  this.stateDir,
@@ -86,17 +123,12 @@ export class RuleHost {
86
123
  // Filter to code-type implementations only
87
124
  const codeImpls = activeAllTypes.filter((impl) => impl.type === 'code');
88
125
 
89
- if (codeImpls.length === 0) {
90
- return [];
91
- }
92
-
93
- const loaded: LoadedImplementation[] = [];
94
-
95
126
  for (const impl of codeImpls) {
96
127
  try {
97
128
  const loadedImpl = this._loadSingleImplementation(impl);
98
- if (loadedImpl) {
129
+ if (loadedImpl && !seenImplIds.has(loadedImpl.implId)) {
99
130
  loaded.push(loadedImpl);
131
+ seenImplIds.add(loadedImpl.implId);
100
132
  }
101
133
  } catch (loadError: unknown) {
102
134
  // Individual load failure: log and skip
@@ -105,14 +137,157 @@ export class RuleHost {
105
137
  );
106
138
  }
107
139
  }
108
-
109
- return loaded;
110
140
  } catch (ledgerError: unknown) {
111
- // Ledger access failure: log and return empty
141
+ // Ledger access failure: log and continue to activations table
112
142
  this.logger.warn?.(
113
143
  `[RuleHost] Failed to access ledger: ${String(ledgerError)}`
114
144
  );
115
- return [];
145
+ }
146
+
147
+ // Source 2: activations table (code_tool_hook channel) — bridges BUG-001
148
+ if (this.workspaceDir) {
149
+ try {
150
+ const activationImpls = this._loadFromActivationsTable(this.workspaceDir);
151
+ for (const impl of activationImpls) {
152
+ if (!seenImplIds.has(impl.implId)) {
153
+ loaded.push(impl);
154
+ seenImplIds.add(impl.implId);
155
+ }
156
+ }
157
+ } catch (activationError: unknown) {
158
+ // Activations table access failure: log and continue with ledger-only results
159
+ this.logger.warn?.(
160
+ `[RuleHost] Failed to load code_tool_hook activations: ${String(activationError)}`
161
+ );
162
+ }
163
+ }
164
+
165
+ return loaded;
166
+ }
167
+
168
+ /**
169
+ * Load active code implementations from the activations table (code_tool_hook channel).
170
+ *
171
+ * For each activation record:
172
+ * 1. Query the pi_artifacts table for the artifact content
173
+ * 2. Parse content_json to extract implementationCode
174
+ * 3. Compile via loadRuleImplementationModule (same vm isolation as ledger path)
175
+ *
176
+ * This mirrors the PromptActivationReader pattern for SQLite access.
177
+ */
178
+ private _loadFromActivationsTable(workspaceDir: string): LoadedImplementation[] {
179
+ const sqliteConn = new SqliteConnection(workspaceDir);
180
+ try {
181
+ const db = sqliteConn.getDb();
182
+ const rows = db.prepare(`
183
+ SELECT a.activation_id, a.artifact_id, a.target_ref,
184
+ p.content_json, p.source_rule_id
185
+ FROM activations a
186
+ JOIN pi_artifacts p ON a.artifact_id = p.artifact_id
187
+ WHERE a.channel = 'code_tool_hook' AND a.deactivated_at IS NULL
188
+ ORDER BY a.activated_at ASC
189
+ `).all() as unknown;
190
+
191
+ if (!Array.isArray(rows)) {
192
+ this.logger.warn?.(
193
+ '[RuleHost] Activations table query returned non-array, skipping'
194
+ );
195
+ return [];
196
+ }
197
+
198
+ const loaded: LoadedImplementation[] = [];
199
+
200
+ for (const row of rows) {
201
+ if (!row || typeof row !== 'object') {
202
+ continue;
203
+ }
204
+ const r = row as Record<string, unknown>;
205
+ const activationId = typeof r['activation_id'] === 'string' ? r['activation_id'] : '';
206
+ const artifactId = typeof r['artifact_id'] === 'string' ? r['artifact_id'] : '';
207
+ const contentJson = typeof r['content_json'] === 'string' ? r['content_json'] : '';
208
+ const sourceRuleId = typeof r['source_rule_id'] === 'string' ? r['source_rule_id'] : null;
209
+
210
+ if (!activationId || !artifactId || !contentJson) {
211
+ this.logger.warn?.(
212
+ `[RuleHost] Activation row missing required fields, skipping`
213
+ );
214
+ continue;
215
+ }
216
+ try {
217
+ const content = JSON.parse(contentJson) as unknown;
218
+ if (!content || typeof content !== 'object' || Array.isArray(content)) {
219
+ this.logger.warn?.(
220
+ `[RuleHost] Activation ${activationId}: content_json is not an object, skipping`
221
+ );
222
+ continue;
223
+ }
224
+
225
+ const contentObj = content as Record<string, unknown>;
226
+ const implementationCode = contentObj['implementationCode'];
227
+ if (typeof implementationCode !== 'string' || implementationCode.length === 0) {
228
+ this.logger.warn?.(
229
+ `[RuleHost] Activation ${activationId}: no implementationCode in artifact, skipping`
230
+ );
231
+ continue;
232
+ }
233
+
234
+ const ruleId = typeof contentObj['ruleId'] === 'string'
235
+ ? contentObj['ruleId']
236
+ : (sourceRuleId ?? artifactId);
237
+
238
+ const implId = `act-impl-${activationId}`;
239
+ const moduleExports = loadRuleImplementationModule(implementationCode, implId);
240
+
241
+ if (!moduleExports || typeof moduleExports.evaluate !== 'function') {
242
+ this.logger.warn?.(
243
+ `[RuleHost] Activation ${activationId}: compiled module has no evaluate function, skipping`
244
+ );
245
+ continue;
246
+ }
247
+
248
+ const fallbackMeta: RuleHostMeta = {
249
+ name: implId,
250
+ version: '1',
251
+ ruleId,
252
+ coversCondition: 'all',
253
+ };
254
+ const meta: RuleHostMeta = isRuleHostMeta(moduleExports.meta)
255
+ ? moduleExports.meta
256
+ : fallbackMeta;
257
+
258
+ const rawEvaluate = moduleExports.evaluate as (
259
+ _input: RuleHostInput,
260
+ _helpers: ReturnType<typeof createRuleHostHelpers>
261
+ ) => RuleHostResult;
262
+
263
+ loaded.push({
264
+ implId,
265
+ ruleId,
266
+ meta,
267
+ evaluate: (input: RuleHostInput): RuleHostResult => {
268
+ const frozenHelpers = createRuleHostHelpers(input);
269
+ const result = rawEvaluate(input, frozenHelpers);
270
+ if (result.matched && (result.decision === 'block' || result.decision === 'requireApproval')) {
271
+ result.ruleId = ruleId;
272
+ result.principleId = meta.ruleId ?? ruleId;
273
+ }
274
+ return result;
275
+ },
276
+ });
277
+ } catch (loadError: unknown) {
278
+ this.logger.warn?.(
279
+ `[RuleHost] Failed to load activation ${activationId}: ${String(loadError)}`
280
+ );
281
+ }
282
+ }
283
+
284
+ return loaded;
285
+ } finally {
286
+ try {
287
+ sqliteConn.close();
288
+ } catch {
289
+ // best-effort cleanup
290
+ }
116
291
  }
117
292
  }
118
293
 
package/src/hooks/gate.ts CHANGED
@@ -67,7 +67,7 @@ export function handleBeforeToolCall(
67
67
 
68
68
  // 3. Rule Host Evaluation — sole gate
69
69
  try {
70
- const ruleHost = new RuleHost(wctx.stateDir, logger);
70
+ const ruleHost = new RuleHost(wctx.stateDir, logger, { workspaceDir: ctx.workspaceDir });
71
71
  const hostInput: RuleHostInput = {
72
72
  action: {
73
73
  toolName: event.toolName,
@@ -5,6 +5,8 @@ import {
5
5
  DreamerRunner,
6
6
  DefaultDreamerValidator,
7
7
  PiAiRuntimeAdapter,
8
+ L2AgentLoopAdapter,
9
+ loadLedger,
8
10
  OpenClawCliRuntimeAdapter,
9
11
  storeEmitter,
10
12
  resolveRuntimeConfigFromPdConfig,
@@ -13,6 +15,7 @@ import {
13
15
  InternalizationQueueReadModel,
14
16
  MVP_CORE_TASK_KINDS,
15
17
  type PDRuntimeAdapter,
18
+ type PdL2PrincipleReader,
16
19
  } from '@principles/core/runtime-v2';
17
20
  import { loadPdConfigForPlugin, loadFeatureFlagFromConfig } from '../core/pd-config-loader.js';
18
21
  import { SystemLogger } from '../core/system-logger.js';
@@ -169,15 +172,61 @@ export async function runConsumerCycle(
169
172
 
170
173
  let adapter: PDRuntimeAdapter;
171
174
  if (runtimeKind === 'pi-ai') {
172
- adapter = new PiAiRuntimeAdapter({
173
- provider: runtimeConfigResult.provider ?? 'openai',
174
- model: runtimeConfigResult.model ?? 'gpt-4o',
175
- apiKeyEnv: runtimeConfigResult.apiKeyEnv ?? 'OPENAI_API_KEY',
176
- maxRetries: runtimeConfigResult.maxRetries,
177
- timeoutMs: runtimeConfigResult.timeoutMs,
178
- baseUrl: runtimeConfigResult.baseUrl,
179
- workspace: workspaceDir,
180
- });
175
+ // PRI-419: when l2_dreamer flag is on, route through the L2 multi-turn agent loop.
176
+ const l2Flag = loadFeatureFlagFromConfig(workspaceDir, 'l2_dreamer');
177
+ if (l2Flag.enabled) {
178
+ const stateDir = `${workspaceDir}/.state`;
179
+ const principleReader: PdL2PrincipleReader = {
180
+ listActivePrinciples: async () => {
181
+ try {
182
+ const ledger = loadLedger(stateDir);
183
+ const principles = ledger.tree.principles ?? {};
184
+ return Object.values(principles)
185
+ .filter(p => p.status === 'active' && typeof p.id === 'string' && typeof p.text === 'string')
186
+ .map(p => ({ id: p.id, statement: p.text }));
187
+ } catch (error) {
188
+ const reason = error instanceof Error ? error.message : String(error);
189
+ logger.warn(`[PD:AutoConsumer] L2 dreamer principle reader degraded: ${reason}`);
190
+ return [];
191
+ }
192
+ },
193
+ };
194
+ adapter = new L2AgentLoopAdapter(
195
+ {
196
+ provider: runtimeConfigResult.provider ?? 'openai',
197
+ model: runtimeConfigResult.model ?? 'gpt-4o',
198
+ apiKeyEnv: runtimeConfigResult.apiKeyEnv ?? 'OPENAI_API_KEY',
199
+ baseUrl: runtimeConfigResult.baseUrl,
200
+ workspace: workspaceDir,
201
+ totalBudgetMs: runtimeConfigResult.timeoutMs,
202
+ },
203
+ {
204
+ artifactReader: {
205
+ // Explicit adapter: PIArtifactRecord → PdL2ArtifactReader. The store returns
206
+ // PIArtifactRecord (with PIArtifactKind enum); map to the ArtifactSummary shape.
207
+ getArtifactById: async (id: string) => {
208
+ const r = await stateManager.piArtifactStore.getArtifactById(id);
209
+ return r ? { artifactId: r.artifactId, artifactKind: String(r.artifactKind), sourceTaskId: r.sourceTaskId, contentJson: r.contentJson, createdAt: r.createdAt } : null;
210
+ },
211
+ listBySourceTaskId: async (taskId: string) => {
212
+ const records = await stateManager.piArtifactStore.listBySourceTaskId(taskId);
213
+ return records.map(r => ({ artifactId: r.artifactId, artifactKind: String(r.artifactKind), sourceTaskId: r.sourceTaskId, contentJson: r.contentJson, createdAt: r.createdAt }));
214
+ },
215
+ },
216
+ principleReader,
217
+ },
218
+ );
219
+ } else {
220
+ adapter = new PiAiRuntimeAdapter({
221
+ provider: runtimeConfigResult.provider ?? 'openai',
222
+ model: runtimeConfigResult.model ?? 'gpt-4o',
223
+ apiKeyEnv: runtimeConfigResult.apiKeyEnv ?? 'OPENAI_API_KEY',
224
+ maxRetries: runtimeConfigResult.maxRetries,
225
+ timeoutMs: runtimeConfigResult.timeoutMs,
226
+ baseUrl: runtimeConfigResult.baseUrl,
227
+ workspace: workspaceDir,
228
+ });
229
+ }
181
230
  } else if (runtimeKind === 'openclaw-cli') {
182
231
  adapter = new OpenClawCliRuntimeAdapter({
183
232
  runtimeMode: runtimeConfigResult.openclawMode ?? 'default',