@principles/pd-cli 1.115.0 → 1.117.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.
Files changed (53) hide show
  1. package/dist/commands/candidate.d.ts +23 -0
  2. package/dist/commands/candidate.d.ts.map +1 -1
  3. package/dist/commands/candidate.js +89 -3
  4. package/dist/commands/candidate.js.map +1 -1
  5. package/dist/commands/diagnose.d.ts.map +1 -1
  6. package/dist/commands/diagnose.js +153 -132
  7. package/dist/commands/diagnose.js.map +1 -1
  8. package/dist/commands/runtime-features.d.ts.map +1 -1
  9. package/dist/commands/runtime-features.js +2 -7
  10. package/dist/commands/runtime-features.js.map +1 -1
  11. package/dist/commands/runtime-internalization-integrity-repair.d.ts.map +1 -1
  12. package/dist/commands/runtime-internalization-integrity-repair.js +15 -31
  13. package/dist/commands/runtime-internalization-integrity-repair.js.map +1 -1
  14. package/dist/commands/runtime-internalization-run-once.d.ts.map +1 -1
  15. package/dist/commands/runtime-internalization-run-once.js +246 -326
  16. package/dist/commands/runtime-internalization-run-once.js.map +1 -1
  17. package/dist/commands/runtime-recovery.d.ts.map +1 -1
  18. package/dist/commands/runtime-recovery.js +9 -8
  19. package/dist/commands/runtime-recovery.js.map +1 -1
  20. package/dist/services/__tests__/cli-output.test.d.ts +18 -0
  21. package/dist/services/__tests__/cli-output.test.d.ts.map +1 -0
  22. package/dist/services/__tests__/cli-output.test.js +103 -0
  23. package/dist/services/__tests__/cli-output.test.js.map +1 -0
  24. package/dist/services/__tests__/runtime-adapter-resolver.test.d.ts +18 -0
  25. package/dist/services/__tests__/runtime-adapter-resolver.test.d.ts.map +1 -0
  26. package/dist/services/__tests__/runtime-adapter-resolver.test.js +651 -0
  27. package/dist/services/__tests__/runtime-adapter-resolver.test.js.map +1 -0
  28. package/dist/services/cli-output.d.ts +61 -0
  29. package/dist/services/cli-output.d.ts.map +1 -0
  30. package/dist/services/cli-output.js +72 -0
  31. package/dist/services/cli-output.js.map +1 -0
  32. package/dist/services/runtime-adapter-resolver.d.ts +105 -0
  33. package/dist/services/runtime-adapter-resolver.d.ts.map +1 -0
  34. package/dist/services/runtime-adapter-resolver.js +188 -0
  35. package/dist/services/runtime-adapter-resolver.js.map +1 -0
  36. package/package.json +1 -1
  37. package/src/commands/candidate.ts +92 -3
  38. package/src/commands/diagnose.ts +146 -138
  39. package/src/commands/runtime-features.ts +2 -6
  40. package/src/commands/runtime-internalization-integrity-repair.ts +16 -28
  41. package/src/commands/runtime-internalization-run-once.ts +242 -353
  42. package/src/commands/runtime-recovery.ts +9 -7
  43. package/src/services/__tests__/cli-output.test.ts +130 -0
  44. package/src/services/__tests__/runtime-adapter-resolver.test.ts +772 -0
  45. package/src/services/cli-output.ts +95 -0
  46. package/src/services/runtime-adapter-resolver.ts +339 -0
  47. package/tests/commands/candidate-internalization-backfill.test.ts +43 -3
  48. package/tests/commands/candidate-internalize-lineage.test.ts +521 -0
  49. package/tests/commands/candidate-internalize.test.ts +31 -5
  50. package/tests/commands/diagnose.test.ts +7 -3
  51. package/tests/commands/runtime-internalization-run-once.test.ts +11 -0
  52. package/tests/commands/runtime-recovery.test.ts +27 -4
  53. package/tests/services/rulehost-pipeline-e2e.test.ts +40 -7
@@ -1,17 +1,10 @@
1
1
  import * as path from 'path';
2
- import { RuntimeStateManager, InternalizationOrchestrator, DreamerRunner, PhilosopherRunner, ScribeRunner, ArtificerRunner, EvaluatorRunner, RolloutReviewerRunner, TrainerRunner, StoreEventEmitter, DefaultDreamerValidator, DefaultPhilosopherValidator, DefaultScribeValidator, DefaultArtificerValidator, DefaultEvaluatorValidator, DefaultRolloutReviewerValidator, DefaultTrainerValidator, TestDoubleRuntimeAdapter, PiAiRuntimeAdapter, OpenClawCliRuntimeAdapter, L2AgentLoopAdapter, loadLedger, isRuntimeConfigError, validateRuntimeConfig, } from '@principles/core/runtime-v2';
3
- import { loadEffectiveFeatureFlags } from '../services/feature-flag-loader.js';
2
+ import { RuntimeStateManager, InternalizationOrchestrator, DreamerRunner, PhilosopherRunner, ScribeRunner, ArtificerRunner, EvaluatorRunner, RolloutReviewerRunner, TrainerRunner, StoreEventEmitter, DefaultDreamerValidator, DefaultPhilosopherValidator, DefaultScribeValidator, DefaultArtificerValidator, DefaultEvaluatorValidator, DefaultRolloutReviewerValidator, DefaultTrainerValidator, TestDoubleRuntimeAdapter, } from '@principles/core/runtime-v2';
4
3
  import { resolveWorkspaceDir } from '../resolve-workspace.js';
5
4
  import { readOutputLanguageFromWorkspace } from '../config-reader.js';
6
- import { resolveRuntimeFromPdConfig } from '../services/resolve-runtime-from-pd-config.js';
5
+ import { resolveRuntimeAdapterFromConfig, ConfigResolutionError, } from '../services/runtime-adapter-resolver.js';
7
6
  const OWNER = 'pd-cli-internalization-run-once';
8
7
  const RUNTIME_KIND = 'local-worker';
9
- class ConfigResolutionError extends Error {
10
- constructor(message) {
11
- super(message);
12
- this.name = 'ConfigResolutionError';
13
- }
14
- }
15
8
  const SUPPORTED_RUNNERS = new Set(['dreamer', 'philosopher', 'scribe', 'artificer', 'evaluator', 'rollout_reviewer', 'trainer']);
16
9
  function buildOutput(wakeResult, runnerResult, skipReason) {
17
10
  const base = { decision: wakeResult.decision };
@@ -125,266 +118,123 @@ function formatTextOutput(output) {
125
118
  return lines.join('\n');
126
119
  }
127
120
  /**
128
- * PRI-419: build a read-only principle reader from the workspace ledger.
129
- * Returns active internalized principles (id + statement). Degrades to empty on missing ledger.
121
+ * PRI-431: Local test-double payload builder.
122
+ * The 7 runner-specific payloads are unique to run-once.ts; the shared resolver
123
+ * calls this via `testDoublePayloadBuilder` callback.
130
124
  */
131
- function makeDreamerPrincipleReader(stateDir) {
132
- return {
133
- listActivePrinciples: async () => {
134
- try {
135
- const ledger = loadLedger(stateDir);
136
- const principles = ledger.tree.principles ?? {};
137
- const active = Object.values(principles).filter(p => p.status === 'active' && typeof p.id === 'string' && typeof p.text === 'string');
138
- return active.map(p => ({ id: p.id, statement: p.text }));
139
- }
140
- catch (error) {
141
- // Graceful degradation WITH an observable reason (Runtime Contract R9): the L2
142
- // dreamer proceeds with only core axioms; the degradation is logged for debugging.
143
- const reason = error instanceof Error ? error.message : String(error);
144
- console.warn(`[l2_dreamer] listActivePrinciples degraded — no internalized principles loaded: ${reason}`);
145
- return [];
146
- }
147
- },
148
- };
149
- }
150
- function resolveRuntimeAdapter(opts) {
151
- if (opts.runtimeKind === 'test-double') {
152
- if (opts.runnerKind === 'philosopher') {
153
- return new TestDoubleRuntimeAdapter({
154
- onPollRun: (_runId) => ({
155
- runId: _runId,
156
- status: 'succeeded',
157
- startedAt: new Date().toISOString(),
158
- endedAt: new Date().toISOString(),
159
- }),
160
- onFetchOutput: (_runId) => ({
161
- runId: _runId,
162
- payload: {
163
- taskId: opts.taskId,
164
- sourceDreamerArtifactId: 'pi-art-test-dreamer',
165
- thesis: 'Test thesis from test-double',
166
- principleCandidate: {
167
- title: 'Test Principle',
168
- rationale: 'Test rationale',
169
- scope: 'Test scope',
170
- confidence: 0.8,
171
- },
172
- risks: [],
173
- generatedAt: new Date().toISOString(),
174
- },
175
- }),
176
- });
177
- }
178
- if (opts.runnerKind === 'scribe') {
179
- return new TestDoubleRuntimeAdapter({
180
- onPollRun: (_runId) => ({
181
- runId: _runId,
182
- status: 'succeeded',
183
- startedAt: new Date().toISOString(),
184
- endedAt: new Date().toISOString(),
185
- }),
186
- onFetchOutput: (_runId) => ({
187
- runId: _runId,
188
- payload: {
189
- taskId: opts.taskId,
190
- sourcePhilosopherArtifactId: 'pi-art-test-philosopher',
191
- principleDraft: {
192
- title: 'Test Principle Draft',
193
- statement: 'Test principle statement',
194
- rationale: 'Test rationale',
195
- applicability: ['All operations'],
196
- antiPatterns: ['Ignoring validation'],
197
- confidence: 0.8,
198
- },
199
- sourceTrace: {
200
- philosopherArtifactId: 'pi-art-test-philosopher',
201
- },
202
- risks: [],
203
- generatedAt: new Date().toISOString(),
125
+ function buildTestDoubleAdapter(runnerKind, taskId) {
126
+ if (runnerKind === 'philosopher') {
127
+ return new TestDoubleRuntimeAdapter({
128
+ onPollRun: (_runId) => ({
129
+ runId: _runId,
130
+ status: 'succeeded',
131
+ startedAt: new Date().toISOString(),
132
+ endedAt: new Date().toISOString(),
133
+ }),
134
+ onFetchOutput: (_runId) => ({
135
+ runId: _runId,
136
+ payload: {
137
+ taskId,
138
+ sourceDreamerArtifactId: 'pi-art-test-dreamer',
139
+ thesis: 'Test thesis from test-double',
140
+ principleCandidate: {
141
+ title: 'Test Principle',
142
+ rationale: 'Test rationale',
143
+ scope: 'Test scope',
144
+ confidence: 0.8,
204
145
  },
205
- }),
206
- });
207
- }
208
- if (opts.runnerKind === 'artificer') {
209
- let capturedSourceScribeArtifactId = 'pi-art-test-scribe';
210
- return new TestDoubleRuntimeAdapter({
211
- onStartRun: (input) => {
212
- try {
213
- const payloadStr = typeof input.inputPayload === 'string' ? input.inputPayload : JSON.stringify(input.inputPayload);
214
- const parsed = JSON.parse(payloadStr);
215
- if (typeof parsed.sourceScribeArtifactId === 'string' && parsed.sourceScribeArtifactId.trim() !== '') {
216
- capturedSourceScribeArtifactId = parsed.sourceScribeArtifactId;
217
- }
218
- }
219
- catch { /* use default */ }
220
- return { runId: `td-artificer-${Date.now()}`, runtimeKind: 'test-double', startedAt: new Date().toISOString() };
146
+ risks: [],
147
+ generatedAt: new Date().toISOString(),
221
148
  },
222
- onPollRun: (_runId) => ({
223
- runId: _runId,
224
- status: 'succeeded',
225
- startedAt: new Date().toISOString(),
226
- endedAt: new Date().toISOString(),
227
- }),
228
- onFetchOutput: (_runId) => ({
229
- runId: _runId,
230
- payload: {
231
- taskId: opts.taskId,
232
- sourceScribeArtifactId: capturedSourceScribeArtifactId,
233
- implementationPlan: {
234
- summary: 'Test implementation summary',
235
- targetSurface: 'src/test/*.ts',
236
- changes: ['Add validation to test module'],
237
- tests: ['Unit test for validation'],
238
- rolloutNotes: ['Deploy behind feature flag'],
239
- confidence: 0.8,
240
- },
241
- sourceTrace: {
242
- scribeArtifactId: capturedSourceScribeArtifactId,
243
- },
244
- risks: [],
245
- generatedAt: new Date().toISOString(),
149
+ }),
150
+ });
151
+ }
152
+ if (runnerKind === 'scribe') {
153
+ return new TestDoubleRuntimeAdapter({
154
+ onPollRun: (_runId) => ({
155
+ runId: _runId,
156
+ status: 'succeeded',
157
+ startedAt: new Date().toISOString(),
158
+ endedAt: new Date().toISOString(),
159
+ }),
160
+ onFetchOutput: (_runId) => ({
161
+ runId: _runId,
162
+ payload: {
163
+ taskId,
164
+ sourcePhilosopherArtifactId: 'pi-art-test-philosopher',
165
+ principleDraft: {
166
+ title: 'Test Principle Draft',
167
+ statement: 'Test principle statement',
168
+ rationale: 'Test rationale',
169
+ applicability: ['All operations'],
170
+ antiPatterns: ['Ignoring validation'],
171
+ confidence: 0.8,
246
172
  },
247
- }),
248
- });
249
- }
250
- if (opts.runnerKind === 'evaluator') {
251
- let capturedSourceArtificerArtifactId = 'pi-art-test-artificer';
252
- return new TestDoubleRuntimeAdapter({
253
- onStartRun: (input) => {
254
- try {
255
- const payloadStr = typeof input.inputPayload === 'string' ? input.inputPayload : JSON.stringify(input.inputPayload);
256
- const parsed = JSON.parse(payloadStr);
257
- if (typeof parsed.sourceArtificerArtifactId === 'string' && parsed.sourceArtificerArtifactId.trim() !== '') {
258
- capturedSourceArtificerArtifactId = parsed.sourceArtificerArtifactId;
259
- }
260
- }
261
- catch { /* use default */ }
262
- return { runId: `td-evaluator-${Date.now()}`, runtimeKind: 'test-double', startedAt: new Date().toISOString() };
263
- },
264
- onPollRun: (_runId) => ({
265
- runId: _runId,
266
- status: 'succeeded',
267
- startedAt: new Date().toISOString(),
268
- endedAt: new Date().toISOString(),
269
- }),
270
- onFetchOutput: (_runId) => ({
271
- runId: _runId,
272
- payload: {
273
- taskId: opts.taskId,
274
- sourceArtificerArtifactId: capturedSourceArtificerArtifactId,
275
- evaluation: {
276
- decision: 'approved',
277
- summary: 'Test evaluation summary',
278
- score: 0.85,
279
- strengths: ['Well-structured plan'],
280
- concerns: [],
281
- requiredChanges: [],
282
- },
283
- sourceTrace: {
284
- artificerArtifactId: capturedSourceArtificerArtifactId,
285
- },
286
- risks: [],
287
- generatedAt: new Date().toISOString(),
173
+ sourceTrace: {
174
+ philosopherArtifactId: 'pi-art-test-philosopher',
288
175
  },
289
- }),
290
- });
291
- }
292
- if (opts.runnerKind === 'rollout_reviewer') {
293
- let capturedSourceEvaluatorArtifactId = 'pi-art-test-evaluator';
294
- return new TestDoubleRuntimeAdapter({
295
- onStartRun: (input) => {
296
- try {
297
- const payloadStr = typeof input.inputPayload === 'string' ? input.inputPayload : JSON.stringify(input.inputPayload);
298
- const parsed = JSON.parse(payloadStr);
299
- if (typeof parsed.sourceEvaluatorArtifactId === 'string' && parsed.sourceEvaluatorArtifactId.trim() !== '') {
300
- capturedSourceEvaluatorArtifactId = parsed.sourceEvaluatorArtifactId;
301
- }
302
- }
303
- catch { /* use default */ }
304
- return { runId: `td-rollout-reviewer-${Date.now()}`, runtimeKind: 'test-double', startedAt: new Date().toISOString() };
176
+ risks: [],
177
+ generatedAt: new Date().toISOString(),
305
178
  },
306
- onPollRun: (_runId) => ({
307
- runId: _runId,
308
- status: 'succeeded',
309
- startedAt: new Date().toISOString(),
310
- endedAt: new Date().toISOString(),
311
- }),
312
- onFetchOutput: (_runId) => ({
313
- runId: _runId,
314
- payload: {
315
- taskId: opts.taskId,
316
- sourceEvaluatorArtifactId: capturedSourceEvaluatorArtifactId,
317
- review: {
318
- decision: 'approve_rollout',
319
- summary: 'Test rollout review summary',
320
- confidence: 0.9,
321
- requiredChanges: [],
322
- rolloutRisks: [],
323
- safetyChecks: ['Verify feature flag is properly configured'],
324
- },
325
- sourceTrace: {
326
- evaluatorArtifactId: capturedSourceEvaluatorArtifactId,
327
- },
328
- risks: [],
329
- generatedAt: new Date().toISOString(),
330
- },
331
- }),
332
- });
333
- }
334
- if (opts.runnerKind === 'trainer') {
335
- let capturedSourceRolloutReviewerArtifactId = 'pi-art-test-rollout-reviewer';
336
- return new TestDoubleRuntimeAdapter({
337
- onStartRun: (input) => {
338
- try {
339
- const payloadStr = typeof input.inputPayload === 'string' ? input.inputPayload : JSON.stringify(input.inputPayload);
340
- const parsed = JSON.parse(payloadStr);
341
- if (typeof parsed.sourceRolloutReviewerArtifactId === 'string' && parsed.sourceRolloutReviewerArtifactId.trim() !== '') {
342
- capturedSourceRolloutReviewerArtifactId = parsed.sourceRolloutReviewerArtifactId;
343
- }
179
+ }),
180
+ });
181
+ }
182
+ if (runnerKind === 'artificer') {
183
+ let capturedSourceScribeArtifactId = 'pi-art-test-scribe';
184
+ return new TestDoubleRuntimeAdapter({
185
+ onStartRun: (input) => {
186
+ try {
187
+ const payloadStr = typeof input.inputPayload === 'string' ? input.inputPayload : JSON.stringify(input.inputPayload);
188
+ const parsed = JSON.parse(payloadStr);
189
+ if (typeof parsed.sourceScribeArtifactId === 'string' && parsed.sourceScribeArtifactId.trim() !== '') {
190
+ capturedSourceScribeArtifactId = parsed.sourceScribeArtifactId;
344
191
  }
345
- catch { /* use default */ }
346
- return { runId: `td-trainer-${Date.now()}`, runtimeKind: 'test-double', startedAt: new Date().toISOString() };
347
- },
348
- onPollRun: (_runId) => ({
349
- runId: _runId,
350
- status: 'succeeded',
351
- startedAt: new Date().toISOString(),
352
- endedAt: new Date().toISOString(),
353
- }),
354
- onFetchOutput: (_runId) => ({
355
- runId: _runId,
356
- payload: {
357
- taskId: opts.taskId,
358
- sourceRolloutReviewerArtifactId: capturedSourceRolloutReviewerArtifactId,
359
- ruleCandidate: {
360
- toolScope: 'src/**/*.ts',
361
- triggerCondition: 'TypeScript file edit with schema mismatch',
362
- proposedDecision: 'auto_correct',
363
- proposedCorrection: {
364
- description: 'Auto-correct by adding input validation before processing',
365
- proposedParams: {
366
- strategy: 'prepend',
367
- snippet: 'const validated = schema.parse(input); if (!validated.success) throw new ValidationError(validated.error);',
368
- },
369
- },
370
- rationale: 'Auto-correct validates input before processing to prevent downstream errors',
371
- confidence: 0.88,
372
- },
373
- safety: {
374
- limitations: ['Requires schema definition for all input types', 'May not handle complex nested structures'],
375
- falsePositiveRisks: ['Could over-correct on intentional dynamic patterns'],
376
- requiredReplayCases: ['Schema validation edge case', 'Nested object validation'],
377
- },
378
- sourceTrace: {
379
- rolloutReviewerArtifactId: capturedSourceRolloutReviewerArtifactId,
380
- },
381
- risks: [],
382
- generatedAt: new Date().toISOString(),
192
+ }
193
+ catch { /* use default */ }
194
+ return { runId: `td-artificer-${Date.now()}`, runtimeKind: 'test-double', startedAt: new Date().toISOString() };
195
+ },
196
+ onPollRun: (_runId) => ({
197
+ runId: _runId,
198
+ status: 'succeeded',
199
+ startedAt: new Date().toISOString(),
200
+ endedAt: new Date().toISOString(),
201
+ }),
202
+ onFetchOutput: (_runId) => ({
203
+ runId: _runId,
204
+ payload: {
205
+ taskId,
206
+ sourceScribeArtifactId: capturedSourceScribeArtifactId,
207
+ implementationPlan: {
208
+ summary: 'Test implementation summary',
209
+ targetSurface: 'src/test/*.ts',
210
+ changes: ['Add validation to test module'],
211
+ tests: ['Unit test for validation'],
212
+ rolloutNotes: ['Deploy behind feature flag'],
213
+ confidence: 0.8,
383
214
  },
384
- }),
385
- });
386
- }
215
+ sourceTrace: {
216
+ scribeArtifactId: capturedSourceScribeArtifactId,
217
+ },
218
+ risks: [],
219
+ generatedAt: new Date().toISOString(),
220
+ },
221
+ }),
222
+ });
223
+ }
224
+ if (runnerKind === 'evaluator') {
225
+ let capturedSourceArtificerArtifactId = 'pi-art-test-artificer';
387
226
  return new TestDoubleRuntimeAdapter({
227
+ onStartRun: (input) => {
228
+ try {
229
+ const payloadStr = typeof input.inputPayload === 'string' ? input.inputPayload : JSON.stringify(input.inputPayload);
230
+ const parsed = JSON.parse(payloadStr);
231
+ if (typeof parsed.sourceArtificerArtifactId === 'string' && parsed.sourceArtificerArtifactId.trim() !== '') {
232
+ capturedSourceArtificerArtifactId = parsed.sourceArtificerArtifactId;
233
+ }
234
+ }
235
+ catch { /* use default */ }
236
+ return { runId: `td-evaluator-${Date.now()}`, runtimeKind: 'test-double', startedAt: new Date().toISOString() };
237
+ },
388
238
  onPollRun: (_runId) => ({
389
239
  runId: _runId,
390
240
  status: 'succeeded',
@@ -394,78 +244,147 @@ function resolveRuntimeAdapter(opts) {
394
244
  onFetchOutput: (_runId) => ({
395
245
  runId: _runId,
396
246
  payload: {
397
- valid: true,
398
- taskId: opts.taskId,
399
- candidates: [{
400
- candidateIndex: 0,
401
- badDecision: 'Ignored input validation requirement',
402
- betterDecision: 'Validate all inputs against schema before processing',
403
- rationale: 'Input validation prevents downstream errors and data corruption',
404
- confidence: 0.85,
405
- riskLevel: 'low',
406
- strategicPerspective: 'defensive-programming',
407
- }],
408
- contextRefs: [],
247
+ taskId,
248
+ sourceArtificerArtifactId: capturedSourceArtificerArtifactId,
249
+ evaluation: {
250
+ decision: 'approved',
251
+ summary: 'Test evaluation summary',
252
+ score: 0.85,
253
+ strengths: ['Well-structured plan'],
254
+ concerns: [],
255
+ requiredChanges: [],
256
+ },
257
+ sourceTrace: {
258
+ artificerArtifactId: capturedSourceArtificerArtifactId,
259
+ },
260
+ risks: [],
409
261
  generatedAt: new Date().toISOString(),
410
262
  },
411
263
  }),
412
264
  });
413
265
  }
414
- // PRI-393: resolve runtime from .pd/config.yaml (not .state/workflows.yaml)
415
- const resolved = resolveRuntimeFromPdConfig(opts.workspaceDir);
416
- const configResult = resolved.result;
417
- if (isRuntimeConfigError(configResult)) {
418
- throw new ConfigResolutionError(`Config resolution from .pd/config.yaml failed: ${configResult.reason}. ` +
419
- `${configResult.message}. nextAction: ${configResult.nextAction}`);
420
- }
421
- if (opts.runtimeKind === 'pi-ai' || (opts.runtimeKind === 'config' && configResult.runtimeKind === 'pi-ai')) {
422
- validateRuntimeConfig(configResult);
423
- // CLI --timeout-ms overrides config timeoutMs
424
- const adapterTimeoutMs = opts.timeoutMs ?? configResult.timeoutMs;
425
- // PRI-419: when l2_dreamer flag is on AND this is the dreamer runner, route through the
426
- // L2 multi-turn agent loop adapter. Other runners (philosopher/scribe/...) stay on L1.
427
- if (opts.runnerKind === 'dreamer' && opts.l2ArtifactReader && opts.l2StateDir) {
428
- const effectiveFlags = loadEffectiveFeatureFlags(opts.workspaceDir);
429
- const l2Flag = Object.hasOwn(effectiveFlags.flags, 'l2_dreamer')
430
- ? effectiveFlags.flags.l2_dreamer.enabled
431
- : false;
432
- if (l2Flag) {
433
- return new L2AgentLoopAdapter({
434
- provider: String(configResult.provider),
435
- model: String(configResult.model),
436
- apiKeyEnv: String(configResult.apiKeyEnv),
437
- baseUrl: configResult.baseUrl,
438
- workspace: opts.workspaceDir,
439
- totalBudgetMs: adapterTimeoutMs,
440
- }, {
441
- artifactReader: opts.l2ArtifactReader,
442
- principleReader: makeDreamerPrincipleReader(opts.l2StateDir),
443
- });
444
- }
445
- }
446
- return new PiAiRuntimeAdapter({
447
- provider: String(configResult.provider),
448
- model: String(configResult.model),
449
- apiKeyEnv: String(configResult.apiKeyEnv),
450
- maxRetries: configResult.maxRetries,
451
- timeoutMs: adapterTimeoutMs,
452
- baseUrl: configResult.baseUrl,
453
- workspace: opts.workspaceDir,
266
+ if (runnerKind === 'rollout_reviewer') {
267
+ let capturedSourceEvaluatorArtifactId = 'pi-art-test-evaluator';
268
+ return new TestDoubleRuntimeAdapter({
269
+ onStartRun: (input) => {
270
+ try {
271
+ const payloadStr = typeof input.inputPayload === 'string' ? input.inputPayload : JSON.stringify(input.inputPayload);
272
+ const parsed = JSON.parse(payloadStr);
273
+ if (typeof parsed.sourceEvaluatorArtifactId === 'string' && parsed.sourceEvaluatorArtifactId.trim() !== '') {
274
+ capturedSourceEvaluatorArtifactId = parsed.sourceEvaluatorArtifactId;
275
+ }
276
+ }
277
+ catch { /* use default */ }
278
+ return { runId: `td-rollout-reviewer-${Date.now()}`, runtimeKind: 'test-double', startedAt: new Date().toISOString() };
279
+ },
280
+ onPollRun: (_runId) => ({
281
+ runId: _runId,
282
+ status: 'succeeded',
283
+ startedAt: new Date().toISOString(),
284
+ endedAt: new Date().toISOString(),
285
+ }),
286
+ onFetchOutput: (_runId) => ({
287
+ runId: _runId,
288
+ payload: {
289
+ taskId,
290
+ sourceEvaluatorArtifactId: capturedSourceEvaluatorArtifactId,
291
+ review: {
292
+ decision: 'approve_rollout',
293
+ summary: 'Test rollout review summary',
294
+ confidence: 0.9,
295
+ requiredChanges: [],
296
+ rolloutRisks: [],
297
+ safetyChecks: ['Verify feature flag is properly configured'],
298
+ },
299
+ sourceTrace: {
300
+ evaluatorArtifactId: capturedSourceEvaluatorArtifactId,
301
+ },
302
+ risks: [],
303
+ generatedAt: new Date().toISOString(),
304
+ },
305
+ }),
454
306
  });
455
307
  }
456
- if (opts.runtimeKind === 'openclaw-cli' || (opts.runtimeKind === 'config' && configResult.runtimeKind === 'openclaw-cli')) {
457
- const { openclawMode } = configResult;
458
- if (!openclawMode) {
459
- throw new ConfigResolutionError(`runtimeKind 'openclaw-cli' requires openclawMode. ` +
460
- `Provide --openclaw-local or --openclaw-gateway, or set openclawMode in .pd/config.yaml. ` +
461
- `nextAction: Add openclawMode: local|gateway to your .pd/config.yaml runtime profile or use CLI flags.`);
462
- }
463
- return new OpenClawCliRuntimeAdapter({
464
- runtimeMode: openclawMode,
465
- workspaceDir: opts.workspaceDir,
308
+ if (runnerKind === 'trainer') {
309
+ let capturedSourceRolloutReviewerArtifactId = 'pi-art-test-rollout-reviewer';
310
+ return new TestDoubleRuntimeAdapter({
311
+ onStartRun: (input) => {
312
+ try {
313
+ const payloadStr = typeof input.inputPayload === 'string' ? input.inputPayload : JSON.stringify(input.inputPayload);
314
+ const parsed = JSON.parse(payloadStr);
315
+ if (typeof parsed.sourceRolloutReviewerArtifactId === 'string' && parsed.sourceRolloutReviewerArtifactId.trim() !== '') {
316
+ capturedSourceRolloutReviewerArtifactId = parsed.sourceRolloutReviewerArtifactId;
317
+ }
318
+ }
319
+ catch { /* use default */ }
320
+ return { runId: `td-trainer-${Date.now()}`, runtimeKind: 'test-double', startedAt: new Date().toISOString() };
321
+ },
322
+ onPollRun: (_runId) => ({
323
+ runId: _runId,
324
+ status: 'succeeded',
325
+ startedAt: new Date().toISOString(),
326
+ endedAt: new Date().toISOString(),
327
+ }),
328
+ onFetchOutput: (_runId) => ({
329
+ runId: _runId,
330
+ payload: {
331
+ taskId,
332
+ sourceRolloutReviewerArtifactId: capturedSourceRolloutReviewerArtifactId,
333
+ ruleCandidate: {
334
+ toolScope: 'src/**/*.ts',
335
+ triggerCondition: 'TypeScript file edit with schema mismatch',
336
+ proposedDecision: 'auto_correct',
337
+ proposedCorrection: {
338
+ description: 'Auto-correct by adding input validation before processing',
339
+ proposedParams: {
340
+ strategy: 'prepend',
341
+ snippet: 'const validated = schema.parse(input); if (!validated.success) throw new ValidationError(validated.error);',
342
+ },
343
+ },
344
+ rationale: 'Auto-correct validates input before processing to prevent downstream errors',
345
+ confidence: 0.88,
346
+ },
347
+ safety: {
348
+ limitations: ['Requires schema definition for all input types', 'May not handle complex nested structures'],
349
+ falsePositiveRisks: ['Could over-correct on intentional dynamic patterns'],
350
+ requiredReplayCases: ['Schema validation edge case', 'Nested object validation'],
351
+ },
352
+ sourceTrace: {
353
+ rolloutReviewerArtifactId: capturedSourceRolloutReviewerArtifactId,
354
+ },
355
+ risks: [],
356
+ generatedAt: new Date().toISOString(),
357
+ },
358
+ }),
466
359
  });
467
360
  }
468
- throw new Error(`Unsupported runtime kind: ${opts.runtimeKind}. Supported: test-double, pi-ai, openclaw-cli, config`);
361
+ // dreamer / default
362
+ return new TestDoubleRuntimeAdapter({
363
+ onPollRun: (_runId) => ({
364
+ runId: _runId,
365
+ status: 'succeeded',
366
+ startedAt: new Date().toISOString(),
367
+ endedAt: new Date().toISOString(),
368
+ }),
369
+ onFetchOutput: (_runId) => ({
370
+ runId: _runId,
371
+ payload: {
372
+ valid: true,
373
+ taskId,
374
+ candidates: [{
375
+ candidateIndex: 0,
376
+ badDecision: 'Ignored input validation requirement',
377
+ betterDecision: 'Validate all inputs against schema before processing',
378
+ rationale: 'Input validation prevents downstream errors and data corruption',
379
+ confidence: 0.85,
380
+ riskLevel: 'low',
381
+ strategicPerspective: 'defensive-programming',
382
+ }],
383
+ contextRefs: [],
384
+ generatedAt: new Date().toISOString(),
385
+ },
386
+ }),
387
+ });
469
388
  }
470
389
  export async function handleRuntimeInternalizationRunOnce(opts) {
471
390
  const workspaceDir = opts.workspace ? path.resolve(opts.workspace) : resolveWorkspaceDir();
@@ -514,13 +433,14 @@ export async function handleRuntimeInternalizationRunOnce(opts) {
514
433
  }
515
434
  const eventEmitter = new StoreEventEmitter();
516
435
  const artifactStore = stateManager.piArtifactStore;
517
- const runtimeAdapter = resolveRuntimeAdapter({
436
+ const runtimeAdapter = resolveRuntimeAdapterFromConfig({
518
437
  runtimeKind,
519
- taskId: wakeResult.taskId,
520
438
  workspaceDir,
521
439
  runnerKind,
522
440
  timeoutMs: cliTimeoutMs,
523
- // PRI-419: pass the L2 readers so resolveRuntimeAdapter can build an L2AgentLoopAdapter
441
+ allowTestDouble: true,
442
+ testDoublePayloadBuilder: () => buildTestDoubleAdapter(runnerKind, wakeResult.taskId),
443
+ // PRI-419: pass the L2 readers so the resolver can build an L2AgentLoopAdapter
524
444
  // when l2_dreamer is enabled. Only consumed for dreamer; harmless for other runners.
525
445
  l2ArtifactReader: artifactStore,
526
446
  l2StateDir: `${workspaceDir}/.state`,