@principles/pd-cli 1.111.0 → 1.113.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/commands/__tests__/run-rulehost-flag-wiring.test.d.ts +24 -0
- package/dist/commands/__tests__/run-rulehost-flag-wiring.test.d.ts.map +1 -0
- package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js +223 -0
- package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js.map +1 -0
- package/dist/commands/runtime-internalization-run-rulehost.d.ts +23 -0
- package/dist/commands/runtime-internalization-run-rulehost.d.ts.map +1 -0
- package/dist/commands/runtime-internalization-run-rulehost.js +364 -0
- package/dist/commands/runtime-internalization-run-rulehost.js.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/services/demo-rule-compiler.d.ts +24 -0
- package/dist/services/demo-rule-compiler.d.ts.map +1 -0
- package/dist/services/demo-rule-compiler.js +53 -0
- package/dist/services/demo-rule-compiler.js.map +1 -0
- package/dist/services/rulehost-pipeline-runner.d.ts +124 -0
- package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -0
- package/dist/services/rulehost-pipeline-runner.js +334 -0
- package/dist/services/rulehost-pipeline-runner.js.map +1 -0
- package/package.json +1 -1
- package/src/commands/__tests__/run-rulehost-flag-wiring.test.ts +280 -0
- package/src/commands/runtime-internalization-run-rulehost.ts +417 -0
- package/src/index.ts +3 -0
- package/src/services/demo-rule-compiler.ts +71 -0
- package/src/services/rulehost-pipeline-runner.ts +585 -0
- package/tests/commands/diagnose.test.ts +178 -1
- package/tests/services/resolve-runtime-from-pd-config.test.ts +59 -0
- package/tests/services/rulehost-pipeline-e2e.test.ts +477 -0
- package/tests/services/rulehost-pipeline-runner.test.ts +519 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command handler: `pd runtime internalization run-rulehost`
|
|
3
|
+
*
|
|
4
|
+
* Drives a pain signal all the way to a validated rule artifact in one call:
|
|
5
|
+
* pain → dreamer → philosopher → scribe → artificer↔evaluator adversarial loop
|
|
6
|
+
*
|
|
7
|
+
* This is the production entry point for the code_tool_hook channel. It wraps
|
|
8
|
+
* runRuleHostPipeline (the service) and constructs the PiAi runtime adapter
|
|
9
|
+
* from .pd/config.yaml (same resolution path as run-once).
|
|
10
|
+
*
|
|
11
|
+
* Atomic capability (per user correction 2026-06-18):
|
|
12
|
+
* ArtificerL2 + Evaluator are atomic — both must run or neither runs.
|
|
13
|
+
* The CLI resolves per-agent config (enabled/runtimeProfile) for artificer
|
|
14
|
+
* and evaluator. When both are enabled, it constructs the ArtificerL2Adapter
|
|
15
|
+
* via buildArtificerL2GenerateCode and passes CodeRuleCapability.enabled=true.
|
|
16
|
+
* When either is disabled, it passes CodeRuleCapability.enabled=false with a
|
|
17
|
+
* structured reason, and the pipeline degrades to text-principle-only.
|
|
18
|
+
*
|
|
19
|
+
* CLI gate compliance:
|
|
20
|
+
* - --json mode emits exactly one JSON object on stdout
|
|
21
|
+
* - exit paths return after setting process.exitCode (no fall-through)
|
|
22
|
+
* - failure paths include a structured reason + next action
|
|
23
|
+
* - --dry-run is default; --confirm required for mutation
|
|
24
|
+
* - --dry-run and --confirm are mutually exclusive
|
|
25
|
+
*/
|
|
26
|
+
import * as path from 'path';
|
|
27
|
+
import type { Command } from 'commander';
|
|
28
|
+
import { runRuleHostPipeline } from '../services/rulehost-pipeline-runner.js';
|
|
29
|
+
import type { RuleHostPipelineResult, CodeRuleCapability, RuleHostAgentAdapters } from '../services/rulehost-pipeline-runner.js';
|
|
30
|
+
import { createSandboxGateDeps } from '../services/rulehost-pipeline-runner.js';
|
|
31
|
+
import {
|
|
32
|
+
PiAiRuntimeAdapter,
|
|
33
|
+
ArtificerL2Adapter,
|
|
34
|
+
buildArtificerL2GenerateCode,
|
|
35
|
+
DefaultArtificerValidator,
|
|
36
|
+
resolveAgentRuntimeBinding,
|
|
37
|
+
computeFeatureFlagsFromConfig,
|
|
38
|
+
isFeatureEnabled,
|
|
39
|
+
} from '@principles/core/runtime-v2';
|
|
40
|
+
import type { EffectivePdConfig, InternalAgentName, PDRuntimeAdapter } from '@principles/core/runtime-v2';
|
|
41
|
+
import { resolveRuntimeFromPdConfig } from '../services/resolve-runtime-from-pd-config.js';
|
|
42
|
+
|
|
43
|
+
export interface RunRuleHostOptions {
|
|
44
|
+
workspace?: string;
|
|
45
|
+
painId: string;
|
|
46
|
+
channel?: string;
|
|
47
|
+
maxRounds?: number;
|
|
48
|
+
timeoutMs?: number;
|
|
49
|
+
json?: boolean;
|
|
50
|
+
/** Dry-run mode (default). Validates inputs + config, reports capability status, does NOT run the pipeline. */
|
|
51
|
+
dryRun?: boolean;
|
|
52
|
+
/** Confirm mutation — actually run the pipeline. Mutually exclusive with --dry-run. */
|
|
53
|
+
confirm?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const DEFAULT_WORKSPACE = process.cwd();
|
|
57
|
+
const SUPPORTED_CHANNELS = new Set(['prompt', 'code_tool_hook', 'defer_archive']);
|
|
58
|
+
|
|
59
|
+
type RuleHostChannel = 'prompt' | 'code_tool_hook' | 'defer_archive';
|
|
60
|
+
|
|
61
|
+
function resolveWorkspace(opts: RunRuleHostOptions): string {
|
|
62
|
+
return opts.workspace ? path.resolve(opts.workspace) : DEFAULT_WORKSPACE;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface ResolvedRunRuleHostRuntime {
|
|
66
|
+
readonly agentAdapters: RuleHostAgentAdapters;
|
|
67
|
+
readonly agentRuntimeProfiles: Partial<Record<InternalAgentName, string>>;
|
|
68
|
+
readonly capability: CodeRuleCapability;
|
|
69
|
+
readonly capabilityStatus: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function resolvePiAiAgentAdapter(
|
|
73
|
+
effective: EffectivePdConfig,
|
|
74
|
+
agentName: InternalAgentName,
|
|
75
|
+
options: { readonly workspaceDir: string; readonly timeoutMs: number | undefined },
|
|
76
|
+
): { adapter: PDRuntimeAdapter; profileId: string } {
|
|
77
|
+
const binding = resolveAgentRuntimeBinding(effective, agentName);
|
|
78
|
+
if (!binding.ok) {
|
|
79
|
+
throw new Error(`${agentName}: ${binding.reason}. nextAction: ${binding.nextAction}`);
|
|
80
|
+
}
|
|
81
|
+
if (binding.profile.type !== 'pi-ai') {
|
|
82
|
+
throw new Error(`${agentName}: runtime profile '${binding.profileId}' must be pi-ai (got '${binding.profile.type}')`);
|
|
83
|
+
}
|
|
84
|
+
if (!process.env[binding.profile.apiKeyEnv]) {
|
|
85
|
+
throw new Error(`${agentName}: apiKeyEnv '${binding.profile.apiKeyEnv}' is not set`);
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
profileId: binding.profileId,
|
|
89
|
+
adapter: new PiAiRuntimeAdapter({
|
|
90
|
+
provider: binding.profile.provider,
|
|
91
|
+
model: binding.profile.model,
|
|
92
|
+
apiKeyEnv: binding.profile.apiKeyEnv,
|
|
93
|
+
maxRetries: binding.profile.maxRetries,
|
|
94
|
+
timeoutMs: options.timeoutMs ?? binding.profile.timeoutMs,
|
|
95
|
+
baseUrl: binding.profile.baseUrl,
|
|
96
|
+
workspace: options.workspaceDir,
|
|
97
|
+
}),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Resolve the code-rule capability (atomic: ArtificerL2 + Evaluator).
|
|
103
|
+
*
|
|
104
|
+
* Checks per-agent config for artificer and evaluator. When both are enabled,
|
|
105
|
+
* constructs the ArtificerL2Adapter via buildArtificerL2GenerateCode. When
|
|
106
|
+
* either is disabled, returns a disabled capability with a structured reason.
|
|
107
|
+
*
|
|
108
|
+
* Returns the capability AND a human-readable status summary for dry-run output.
|
|
109
|
+
*/
|
|
110
|
+
function resolveRunRuleHostRuntime(
|
|
111
|
+
workspaceDir: string,
|
|
112
|
+
timeoutMs: number | undefined,
|
|
113
|
+
): ResolvedRunRuleHostRuntime {
|
|
114
|
+
const { configLoadResult } = resolveRuntimeFromPdConfig(workspaceDir);
|
|
115
|
+
|
|
116
|
+
if (!configLoadResult.ok) {
|
|
117
|
+
throw new Error('config_malformed — cannot resolve agent bindings');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const { effective } = configLoadResult;
|
|
121
|
+
const adapterOptions = { workspaceDir, timeoutMs };
|
|
122
|
+
const dreamer = resolvePiAiAgentAdapter(effective, 'dreamer', adapterOptions);
|
|
123
|
+
const philosopher = resolvePiAiAgentAdapter(effective, 'philosopher', adapterOptions);
|
|
124
|
+
const scribe = resolvePiAiAgentAdapter(effective, 'scribe', adapterOptions);
|
|
125
|
+
const agentRuntimeProfiles: Partial<Record<InternalAgentName, string>> = {
|
|
126
|
+
dreamer: dreamer.profileId,
|
|
127
|
+
philosopher: philosopher.profileId,
|
|
128
|
+
scribe: scribe.profileId,
|
|
129
|
+
};
|
|
130
|
+
const featureFlags = computeFeatureFlagsFromConfig(effective);
|
|
131
|
+
if (!isFeatureEnabled(featureFlags, 'code_rule_capability')) {
|
|
132
|
+
return {
|
|
133
|
+
agentAdapters: { dreamer: dreamer.adapter, philosopher: philosopher.adapter, scribe: scribe.adapter, evaluator: scribe.adapter },
|
|
134
|
+
agentRuntimeProfiles,
|
|
135
|
+
capability: { enabled: false, disabledReason: 'code_rule_capability feature flag is disabled' },
|
|
136
|
+
capabilityStatus: 'code_rule_capability: OFF (feature flag disabled)',
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const artificerBinding = resolveAgentRuntimeBinding(effective, 'artificer');
|
|
141
|
+
const evaluatorBinding = resolveAgentRuntimeBinding(effective, 'evaluator');
|
|
142
|
+
|
|
143
|
+
// Both must be enabled (atomic capability).
|
|
144
|
+
if (!artificerBinding.ok) {
|
|
145
|
+
return {
|
|
146
|
+
agentAdapters: { dreamer: dreamer.adapter, philosopher: philosopher.adapter, scribe: scribe.adapter, evaluator: scribe.adapter },
|
|
147
|
+
agentRuntimeProfiles,
|
|
148
|
+
capability: { enabled: false, disabledReason: `artificer agent disabled: ${artificerBinding.reason}` },
|
|
149
|
+
capabilityStatus: `code_rule_capability: OFF (artificer disabled — ${artificerBinding.reason})`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
if (!evaluatorBinding.ok) {
|
|
153
|
+
return {
|
|
154
|
+
agentAdapters: { dreamer: dreamer.adapter, philosopher: philosopher.adapter, scribe: scribe.adapter, evaluator: scribe.adapter },
|
|
155
|
+
agentRuntimeProfiles,
|
|
156
|
+
capability: { enabled: false, disabledReason: `evaluator agent disabled: ${evaluatorBinding.reason}` },
|
|
157
|
+
capabilityStatus: `code_rule_capability: OFF (evaluator disabled — ${evaluatorBinding.reason})`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Both enabled — construct the ArtificerL2Adapter.
|
|
162
|
+
// Resolve the artificer's runtime profile to get provider/model/apiKeyEnv.
|
|
163
|
+
const { profile: artificerProfile } = artificerBinding;
|
|
164
|
+
if (artificerProfile.type !== 'pi-ai') {
|
|
165
|
+
return {
|
|
166
|
+
agentAdapters: { dreamer: dreamer.adapter, philosopher: philosopher.adapter, scribe: scribe.adapter, evaluator: scribe.adapter },
|
|
167
|
+
agentRuntimeProfiles,
|
|
168
|
+
capability: { enabled: false, disabledReason: `artificer runtime profile is not pi-ai (got '${artificerProfile.type}')` },
|
|
169
|
+
capabilityStatus: `code_rule_capability: OFF (artificer profile type='${artificerProfile.type}', expected pi-ai)`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const apiKey = process.env[artificerProfile.apiKeyEnv];
|
|
174
|
+
if (!apiKey) {
|
|
175
|
+
return {
|
|
176
|
+
agentAdapters: { dreamer: dreamer.adapter, philosopher: philosopher.adapter, scribe: scribe.adapter, evaluator: scribe.adapter },
|
|
177
|
+
agentRuntimeProfiles,
|
|
178
|
+
capability: { enabled: false, disabledReason: `artificer apiKeyEnv '${artificerProfile.apiKeyEnv}' is not set in environment` },
|
|
179
|
+
capabilityStatus: `code_rule_capability: OFF (artificer apiKeyEnv '${artificerProfile.apiKeyEnv}' not set)`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const evaluator = resolvePiAiAgentAdapter(effective, 'evaluator', adapterOptions);
|
|
184
|
+
agentRuntimeProfiles.artificer = artificerBinding.profileId;
|
|
185
|
+
agentRuntimeProfiles.evaluator = evaluator.profileId;
|
|
186
|
+
|
|
187
|
+
const generateCode = buildArtificerL2GenerateCode({
|
|
188
|
+
provider: artificerProfile.provider,
|
|
189
|
+
model: artificerProfile.model,
|
|
190
|
+
apiKey,
|
|
191
|
+
baseUrl: artificerProfile.baseUrl,
|
|
192
|
+
timeoutMs,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const artificerAdapter = new ArtificerL2Adapter({
|
|
196
|
+
generateCode,
|
|
197
|
+
gateDeps: createSandboxGateDeps(),
|
|
198
|
+
validator: new DefaultArtificerValidator(),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
agentAdapters: { dreamer: dreamer.adapter, philosopher: philosopher.adapter, scribe: scribe.adapter, evaluator: evaluator.adapter },
|
|
203
|
+
agentRuntimeProfiles,
|
|
204
|
+
capability: { enabled: true, artificerAdapter },
|
|
205
|
+
capabilityStatus: `code_rule_capability: ON (artificer profile='${artificerBinding.profileId}', evaluator profile='${evaluatorBinding.profileId}')`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function formatTextOutput(result: RuleHostPipelineResult): string {
|
|
210
|
+
const lines: string[] = [];
|
|
211
|
+
const isReady = result.decision === 'candidate_ready_for_owner_review';
|
|
212
|
+
const icon = isReady ? '✓' : result.decision === 'text_principle_only' ? '⚠' : '✗';
|
|
213
|
+
lines.push('RuleHost Pipeline (PRI-429)');
|
|
214
|
+
lines.push(`pain: ${result.painId}`);
|
|
215
|
+
lines.push(`OVERALL: ${icon} ${result.decision.toUpperCase()}`);
|
|
216
|
+
lines.push('');
|
|
217
|
+
|
|
218
|
+
for (const stage of result.stages) {
|
|
219
|
+
const sIcon = stage.status === 'succeeded' ? '✓' : stage.status === 'degraded' ? '⚠' : stage.status === 'skipped' ? '○' : '✗';
|
|
220
|
+
lines.push(` ${sIcon} ${stage.name}: ${stage.status}`);
|
|
221
|
+
if (stage.reason) lines.push(` reason: ${stage.reason}`);
|
|
222
|
+
}
|
|
223
|
+
lines.push('');
|
|
224
|
+
lines.push(`ruleArtifactId: ${result.ruleArtifactId ?? '(none)'}`);
|
|
225
|
+
lines.push(`principleArtifactId: ${result.principleArtifactId ?? '(none)'}`);
|
|
226
|
+
if (result.degradationReason) {
|
|
227
|
+
lines.push(`degradationReason: ${result.degradationReason}`);
|
|
228
|
+
}
|
|
229
|
+
if (result.decision === 'candidate_ready_for_owner_review') {
|
|
230
|
+
lines.push('');
|
|
231
|
+
lines.push('Next: the rule artifact is validated and WAITING for owner review. This is NOT owner approval.');
|
|
232
|
+
} else if (result.decision === 'text_principle_only') {
|
|
233
|
+
lines.push('');
|
|
234
|
+
lines.push('Next: code-rule capability is OFF. Principle artifact remains for prompt-channel fallback.');
|
|
235
|
+
} else {
|
|
236
|
+
lines.push('');
|
|
237
|
+
lines.push('Next: generation rejected. Check degradationReason. Principle artifact may still exist for prompt-channel fallback.');
|
|
238
|
+
}
|
|
239
|
+
return lines.join('\n');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function formatDryRunOutput(opts: RunRuleHostOptions, capabilityStatus: string, workspaceDir: string): string {
|
|
243
|
+
const lines: string[] = [];
|
|
244
|
+
lines.push('RuleHost Pipeline (PRI-429) — DRY RUN');
|
|
245
|
+
lines.push(`pain: ${opts.painId}`);
|
|
246
|
+
lines.push(`workspace: ${workspaceDir}`);
|
|
247
|
+
lines.push(`channel: ${opts.channel ?? 'code_tool_hook'}`);
|
|
248
|
+
lines.push(capabilityStatus);
|
|
249
|
+
lines.push('');
|
|
250
|
+
lines.push('No tasks created, no LLM calls made, no artifacts written.');
|
|
251
|
+
lines.push('Next: pass --confirm to actually run the pipeline.');
|
|
252
|
+
return lines.join('\n');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export async function handleRunRuleHost(opts: RunRuleHostOptions): Promise<void> {
|
|
256
|
+
// ── Validate dry-run/confirm mutual exclusivity (CLI gate rule 4) ──
|
|
257
|
+
if (opts.dryRun && opts.confirm) {
|
|
258
|
+
if (opts.json) {
|
|
259
|
+
process.stdout.write(JSON.stringify({ status: 'failed', reason: '--dry-run and --confirm are mutually exclusive', nextAction: 'pass either --dry-run or --confirm, not both' }) + '\n');
|
|
260
|
+
} else {
|
|
261
|
+
console.error('Error: --dry-run and --confirm are mutually exclusive. Pass either one, not both.');
|
|
262
|
+
}
|
|
263
|
+
process.exitCode = 1;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── Validate inputs ──
|
|
268
|
+
if (!opts.painId || opts.painId.trim() === '') {
|
|
269
|
+
if (opts.json) {
|
|
270
|
+
process.stdout.write(JSON.stringify({ status: 'failed', reason: 'painId is required', nextAction: 'pass --pain-id <id> (run pd pain record first)' }) + '\n');
|
|
271
|
+
} else {
|
|
272
|
+
console.error('Error: --pain-id is required. Run `pd pain record` first to seed a pain signal.');
|
|
273
|
+
}
|
|
274
|
+
process.exitCode = 1;
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const channel = opts.channel ?? 'code_tool_hook';
|
|
279
|
+
if (!SUPPORTED_CHANNELS.has(channel)) {
|
|
280
|
+
if (opts.json) {
|
|
281
|
+
process.stdout.write(JSON.stringify({ status: 'failed', reason: `unsupported channel: ${channel}`, nextAction: 'use one of prompt|code_tool_hook|defer_archive' }) + '\n');
|
|
282
|
+
} else {
|
|
283
|
+
console.error(`Error: unsupported channel '${channel}'. Use one of: prompt, code_tool_hook, defer_archive.`);
|
|
284
|
+
}
|
|
285
|
+
process.exitCode = 1;
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Validate numeric opts (Commander parseInt can yield NaN for non-numeric input).
|
|
290
|
+
if (opts.maxRounds !== undefined && (!Number.isInteger(opts.maxRounds) || opts.maxRounds <= 0)) {
|
|
291
|
+
if (opts.json) {
|
|
292
|
+
process.stdout.write(JSON.stringify({ status: 'failed', reason: `invalid --max-rounds: ${opts.maxRounds}`, nextAction: 'pass a positive integer (PRD cap = 2)' }) + '\n');
|
|
293
|
+
} else {
|
|
294
|
+
console.error(`Error: --max-rounds must be a positive integer (got ${opts.maxRounds}).`);
|
|
295
|
+
}
|
|
296
|
+
process.exitCode = 1;
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (opts.timeoutMs !== undefined && (!Number.isInteger(opts.timeoutMs) || opts.timeoutMs <= 0)) {
|
|
300
|
+
if (opts.json) {
|
|
301
|
+
process.stdout.write(JSON.stringify({ status: 'failed', reason: `invalid --timeout-ms: ${opts.timeoutMs}`, nextAction: 'pass a positive integer (default 300000)' }) + '\n');
|
|
302
|
+
} else {
|
|
303
|
+
console.error(`Error: --timeout-ms must be a positive integer (got ${opts.timeoutMs}).`);
|
|
304
|
+
}
|
|
305
|
+
process.exitCode = 1;
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const workspaceDir = resolveWorkspace(opts);
|
|
310
|
+
|
|
311
|
+
// ── Resolve each executed agent from canonical config ──
|
|
312
|
+
let resolvedRuntime: ResolvedRunRuleHostRuntime;
|
|
313
|
+
try {
|
|
314
|
+
resolvedRuntime = resolveRunRuleHostRuntime(workspaceDir, opts.timeoutMs);
|
|
315
|
+
} catch (err) {
|
|
316
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
317
|
+
if (opts.json) {
|
|
318
|
+
process.stdout.write(JSON.stringify({ status: 'failed', reason: 'agent_runtime_resolution_failed', message, nextAction: 'check internalAgents and runtimeProfiles in .pd/config.yaml; run pd config doctor' }) + '\n');
|
|
319
|
+
} else {
|
|
320
|
+
console.error(`Error: ${message}`);
|
|
321
|
+
}
|
|
322
|
+
process.exitCode = 1;
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ── Dry-run mode: report what would happen, don't run the pipeline ──
|
|
327
|
+
// Default is dry-run (CLI gate rule 4: mutating commands default to dry-run).
|
|
328
|
+
const isDryRun = opts.dryRun || !opts.confirm;
|
|
329
|
+
if (isDryRun) {
|
|
330
|
+
if (opts.json) {
|
|
331
|
+
process.stdout.write(JSON.stringify({
|
|
332
|
+
status: 'dry_run',
|
|
333
|
+
painId: opts.painId,
|
|
334
|
+
workspace: workspaceDir,
|
|
335
|
+
channel,
|
|
336
|
+
capabilityStatus: resolvedRuntime.capabilityStatus,
|
|
337
|
+
agentRuntimeProfiles: resolvedRuntime.agentRuntimeProfiles,
|
|
338
|
+
codeRuleCapability: { enabled: resolvedRuntime.capability.enabled, disabledReason: resolvedRuntime.capability.disabledReason },
|
|
339
|
+
nextAction: 'pass --confirm to actually run the pipeline',
|
|
340
|
+
}) + '\n');
|
|
341
|
+
} else {
|
|
342
|
+
process.stdout.write(formatDryRunOutput(opts, resolvedRuntime.capabilityStatus, workspaceDir) + '\n');
|
|
343
|
+
}
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ── Run the pipeline (--confirm mode) ──
|
|
348
|
+
let result: RuleHostPipelineResult;
|
|
349
|
+
try {
|
|
350
|
+
result = await runRuleHostPipeline({
|
|
351
|
+
workspaceDir,
|
|
352
|
+
painId: opts.painId,
|
|
353
|
+
runtimeAdapter: resolvedRuntime.agentAdapters.dreamer,
|
|
354
|
+
agentAdapters: resolvedRuntime.agentAdapters,
|
|
355
|
+
codeRuleCapability: resolvedRuntime.capability,
|
|
356
|
+
channel: channel as RuleHostChannel,
|
|
357
|
+
maxRounds: opts.maxRounds,
|
|
358
|
+
timeoutMs: opts.timeoutMs,
|
|
359
|
+
});
|
|
360
|
+
} catch (err) {
|
|
361
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
362
|
+
if (opts.json) {
|
|
363
|
+
process.stdout.write(JSON.stringify({ status: 'failed', reason: 'pipeline_threw', message, nextAction: 'check workspace state and retry' }) + '\n');
|
|
364
|
+
} else {
|
|
365
|
+
console.error(`Error: pipeline failed: ${message}`);
|
|
366
|
+
}
|
|
367
|
+
process.exitCode = 1;
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ── Output ──
|
|
372
|
+
if (opts.json) {
|
|
373
|
+
// Exactly one parseable JSON object on stdout (CLI gate rule 1).
|
|
374
|
+
// CLI gate rule 6: degraded/refused results include structured reason + nextAction.
|
|
375
|
+
const output = result.decision === 'candidate_ready_for_owner_review'
|
|
376
|
+
? { status: 'candidate_ready_for_owner_review', ...result }
|
|
377
|
+
: {
|
|
378
|
+
status: result.decision,
|
|
379
|
+
...result,
|
|
380
|
+
nextAction: result.decision === 'text_principle_only'
|
|
381
|
+
? 'code-rule capability OFF; principle artifact available for prompt-channel fallback.'
|
|
382
|
+
: 'Check degradationReason; principle artifact may still exist for prompt-channel fallback.',
|
|
383
|
+
};
|
|
384
|
+
process.stdout.write(JSON.stringify(output) + '\n');
|
|
385
|
+
} else {
|
|
386
|
+
process.stdout.write(formatTextOutput(result) + '\n');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// exit 1 when not candidate_ready_for_owner_review (CLI gate: operator knows it didn't fully succeed)
|
|
390
|
+
if (result.decision !== 'candidate_ready_for_owner_review') {
|
|
391
|
+
process.exitCode = 1;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Register the `pd runtime internalization run-rulehost` subcommand.
|
|
397
|
+
*
|
|
398
|
+
* Single source of truth for both production (`index.ts`) and parser tests.
|
|
399
|
+
* Extracted so parser-level tests can exercise the real flag wiring without
|
|
400
|
+
* triggering `program.parse()` at module load (CLI gate rule 7).
|
|
401
|
+
*/
|
|
402
|
+
export function registerRunRuleHostCommand(internalizationCmd: Command): Command {
|
|
403
|
+
return internalizationCmd
|
|
404
|
+
.command('run-rulehost')
|
|
405
|
+
.description('Full-chain: drive a pain signal to a validated rule artifact (pain → dreamer → philosopher → scribe → adversarial loop)')
|
|
406
|
+
.option('-w, --workspace <path>', 'Workspace directory')
|
|
407
|
+
.requiredOption('--pain-id <id>', 'Pain ID whose internalization chain to drive (run `pd pain record` first)')
|
|
408
|
+
.option('--channel <channel>', 'Activation channel: code_tool_hook, prompt, defer_archive (default: code_tool_hook)', 'code_tool_hook')
|
|
409
|
+
.option('--max-rounds <n>', 'Max adversarial rounds (PRD cap = 2)', parseInt)
|
|
410
|
+
.option('--timeout-ms <ms>', 'Per-LLM-call timeout in milliseconds (default: 300000)', parseInt)
|
|
411
|
+
.option('--dry-run', 'Validate inputs + config, report capability status, do NOT run the pipeline (default)')
|
|
412
|
+
.option('--confirm', 'Actually run the pipeline (mutually exclusive with --dry-run)')
|
|
413
|
+
.option('--json', 'Output raw JSON')
|
|
414
|
+
.action(async (opts) => {
|
|
415
|
+
await handleRunRuleHost({ workspace: opts.workspace, painId: opts.painId, channel: opts.channel, maxRounds: opts.maxRounds, timeoutMs: opts.timeoutMs, dryRun: opts.dryRun, confirm: opts.confirm, json: opts.json });
|
|
416
|
+
});
|
|
417
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -34,6 +34,7 @@ import { handleRuntimeUat } from './commands/runtime-uat.js';
|
|
|
34
34
|
import { handleRuntimeInternalizationQueue } from './commands/runtime-internalization-queue.js';
|
|
35
35
|
import { handleRuntimeInternalizationWakeOnce } from './commands/runtime-internalization-wake-once.js';
|
|
36
36
|
import { handleRuntimeInternalizationRunOnce } from './commands/runtime-internalization-run-once.js';
|
|
37
|
+
import { registerRunRuleHostCommand } from './commands/runtime-internalization-run-rulehost.js';
|
|
37
38
|
import { handleCandidateList, handleCandidateShow, handleCandidateIntake, handleCandidateAudit, handleCandidateRepair, handleCandidateRoute, handleCandidateInternalize, handleCandidateInternalizationBackfill } from './commands/candidate.js';
|
|
38
39
|
import { handleArtifactShow } from './commands/artifact.js';
|
|
39
40
|
import { handleRuntimeCanary } from './commands/runtime-canary.js';
|
|
@@ -536,6 +537,8 @@ internalizationCmd
|
|
|
536
537
|
await handleRuntimeInternalizationRunOnce({ workspace: opts.workspace, json: opts.json, runtime: opts.runtime, runner: opts.runner, allowTestDouble: opts.allowTestDouble, enqueueNext: opts.enqueueNext, timeoutMs: opts.timeoutMs });
|
|
537
538
|
});
|
|
538
539
|
|
|
540
|
+
registerRunRuleHostCommand(internalizationCmd);
|
|
541
|
+
|
|
539
542
|
internalizationCmd
|
|
540
543
|
.command('integrity')
|
|
541
544
|
.description('Check internalization chain integrity')
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Demo Rule Compiler — Minimal bridge for Story A demo runner
|
|
3
|
+
*
|
|
4
|
+
* PURPOSE: Compile rule implementation code strings into typed evaluate
|
|
5
|
+
* functions for use in `evaluateInRefinerSandbox`. This allows the demo
|
|
6
|
+
* to perform REAL canActivate validation instead of always returning success.
|
|
7
|
+
*
|
|
8
|
+
* ARCHITECTURE: This intentionally duplicates the compilation logic from
|
|
9
|
+
* `openclaw-plugin/src/core/rule-implementation-runtime.ts` because pd-cli
|
|
10
|
+
* cannot depend on the bundled openclaw-plugin package. The compilation
|
|
11
|
+
* logic is identical: normalize exports → vm compile → extract evaluate.
|
|
12
|
+
*
|
|
13
|
+
* NOT for production use — the openclaw-plugin's RuleHost uses its own
|
|
14
|
+
* `loadRuleImplementationModule` for production code_tool_hook evaluation.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as vm from 'node:vm';
|
|
18
|
+
import type { RuleHostInput, RuleHostResult } from '@principles/core/runtime-v2';
|
|
19
|
+
import type { RuleHostHelpers } from '@principles/core/runtime-v2';
|
|
20
|
+
import type { ReplayEvaluateFn } from '@principles/core/runtime-v2';
|
|
21
|
+
import { safeStringifyPreview } from '@principles/core/runtime-v2';
|
|
22
|
+
|
|
23
|
+
function normalizeSource(sourceCode: string): string {
|
|
24
|
+
const withoutExports = sourceCode
|
|
25
|
+
.replace(/export\s+const\s+meta\s*=/, 'const meta =')
|
|
26
|
+
.replace(/export\s+function\s+evaluate\s*\(/, 'function evaluate(');
|
|
27
|
+
|
|
28
|
+
return `${withoutExports}
|
|
29
|
+
globalThis.__pdRuleModule = {
|
|
30
|
+
meta: typeof meta === 'undefined' ? undefined : meta,
|
|
31
|
+
evaluate: typeof evaluate === 'undefined' ? undefined : evaluate,
|
|
32
|
+
};`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Compile rule implementation code and return a typed evaluate function.
|
|
37
|
+
* Mirrors `createReplayEvaluateFromCode` in openclaw-plugin.
|
|
38
|
+
*
|
|
39
|
+
* @throws if the code fails to compile or does not define a function evaluate
|
|
40
|
+
*/
|
|
41
|
+
export function compileDemoRule(code: string, sourceLabel: string): ReplayEvaluateFn {
|
|
42
|
+
const context = vm.createContext(Object.create(null));
|
|
43
|
+
const script = new vm.Script(normalizeSource(code), { filename: sourceLabel });
|
|
44
|
+
|
|
45
|
+
script.runInContext(context, { timeout: 1000, displayErrors: true });
|
|
46
|
+
|
|
47
|
+
const moduleExports = (context as { __pdRuleModule?: { meta?: unknown; evaluate?: unknown } })
|
|
48
|
+
.__pdRuleModule;
|
|
49
|
+
delete (context as { __pdRuleModule?: unknown }).__pdRuleModule;
|
|
50
|
+
|
|
51
|
+
if (!moduleExports || typeof moduleExports.evaluate !== 'function') {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`[compileDemoRule] ${sourceLabel}: compiled module has no evaluate function`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const evaluateFn = moduleExports.evaluate as (
|
|
58
|
+
input: RuleHostInput,
|
|
59
|
+
helpers: RuleHostHelpers,
|
|
60
|
+
) => RuleHostResult;
|
|
61
|
+
|
|
62
|
+
return (input: RuleHostInput, helpers: RuleHostHelpers): RuleHostResult => {
|
|
63
|
+
const result = evaluateFn(input, helpers);
|
|
64
|
+
if (typeof result !== 'object' || result === null || !Object.hasOwn(result, 'decision')) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`[${sourceLabel}]: evaluate returned invalid RuleHostResult (got ${typeof result === 'object' && result !== null ? safeStringifyPreview(result) : String(result)})`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
};
|
|
71
|
+
}
|