@principles/pd-cli 1.112.0 → 1.114.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-activation.d.ts +37 -0
- package/dist/commands/runtime-activation.d.ts.map +1 -1
- package/dist/commands/runtime-activation.js +416 -2
- package/dist/commands/runtime-activation.js.map +1 -1
- 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 +56 -1
- 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 +132 -0
- package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -0
- package/dist/services/rulehost-pipeline-runner.js +376 -0
- package/dist/services/rulehost-pipeline-runner.js.map +1 -0
- package/package.json +1 -1
- package/scripts/llm-dogfood.ts +419 -0
- package/src/commands/__tests__/run-rulehost-flag-wiring.test.ts +280 -0
- package/src/commands/runtime-activation.ts +459 -1
- package/src/commands/runtime-internalization-run-rulehost.ts +417 -0
- package/src/index.ts +60 -1
- package/src/services/demo-rule-compiler.ts +71 -0
- package/src/services/rulehost-pipeline-runner.ts +638 -0
- package/tests/commands/cli-command-tree.test.ts +14 -0
- package/tests/commands/runtime-activation.test.ts +553 -1
- package/tests/e2e/cross-package-acceptance.test.ts +549 -0
- package/tests/services/rulehost-pipeline-e2e.test.ts +477 -0
- package/tests/services/rulehost-pipeline-runner.test.ts +525 -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';
|
|
@@ -46,6 +47,7 @@ import { handleRuntimeDiagnosticsExport } from './commands/runtime-diagnostics-e
|
|
|
46
47
|
import { handleRuntimeRecoverySweep } from './commands/runtime-recovery.js';
|
|
47
48
|
import { handleRuntimeRecoveryFailedTasks } from './commands/runtime-recovery-failed-tasks.js';
|
|
48
49
|
import { handleRuntimeActivationDispatch } from './commands/runtime-activation.js';
|
|
50
|
+
import { handleRuntimeActivationDeactivate, handleRuntimeActivationList, handleRuntimeActivationEdit } from './commands/runtime-activation.js';
|
|
49
51
|
import { handleProvenChannelBaseline } from './commands/proven-channel-baseline.js';
|
|
50
52
|
import { handleDemoStoryA } from './commands/demo-story-a.js';
|
|
51
53
|
import { handleRuntimeFeaturesStatus } from './commands/runtime-features.js';
|
|
@@ -536,6 +538,8 @@ internalizationCmd
|
|
|
536
538
|
await handleRuntimeInternalizationRunOnce({ workspace: opts.workspace, json: opts.json, runtime: opts.runtime, runner: opts.runner, allowTestDouble: opts.allowTestDouble, enqueueNext: opts.enqueueNext, timeoutMs: opts.timeoutMs });
|
|
537
539
|
});
|
|
538
540
|
|
|
541
|
+
registerRunRuleHostCommand(internalizationCmd);
|
|
542
|
+
|
|
539
543
|
internalizationCmd
|
|
540
544
|
.command('integrity')
|
|
541
545
|
.description('Check internalization chain integrity')
|
|
@@ -574,7 +578,7 @@ const activationCmd = runtimeCmd
|
|
|
574
578
|
activationCmd
|
|
575
579
|
.command('dispatch')
|
|
576
580
|
.description('Dispatch an activation for a rollout-reviewed artifact')
|
|
577
|
-
.
|
|
581
|
+
.option('-a, --artifact-id <id>', 'PIArtifact ID to activate')
|
|
578
582
|
.option('-w, --workspace <path>', 'Workspace directory')
|
|
579
583
|
.option('-c, --channel <channel>', 'Activation channel (prompt|defer_archive)', 'prompt')
|
|
580
584
|
.option('--dry-run', 'Dry-run mode (default, no writes)')
|
|
@@ -591,6 +595,61 @@ activationCmd
|
|
|
591
595
|
});
|
|
592
596
|
});
|
|
593
597
|
|
|
598
|
+
// PRI-408 Contract E: Owner-initiated rollback/deactivate of an activation.
|
|
599
|
+
// Idempotent — calling twice on the same ID is safe and returns ok=false with reason.
|
|
600
|
+
activationCmd
|
|
601
|
+
.command('deactivate')
|
|
602
|
+
.description('Deactivate (rollback) an active activation — idempotent (PRI-408 Contract E)')
|
|
603
|
+
.option('-a, --activation-id <id>', 'Activation ID to deactivate')
|
|
604
|
+
.option('-w, --workspace <path>', 'Workspace directory')
|
|
605
|
+
.option('--json', 'Output raw JSON')
|
|
606
|
+
.action(async (opts) => {
|
|
607
|
+
await handleRuntimeActivationDeactivate({
|
|
608
|
+
workspace: opts.workspace,
|
|
609
|
+
activationId: opts.activationId,
|
|
610
|
+
json: opts.json,
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// PRI-408 Contract D: Owner observability — list current activations.
|
|
615
|
+
activationCmd
|
|
616
|
+
.command('list')
|
|
617
|
+
.description('List activations (default: active only) — PRI-408 Contract D observability')
|
|
618
|
+
.option('-w, --workspace <path>', 'Workspace directory')
|
|
619
|
+
.option('-c, --channel <channel>', 'Filter by channel (prompt|code_tool_hook)')
|
|
620
|
+
.option('--include-deactivated', 'Include deactivated records in output')
|
|
621
|
+
.option('--json', 'Output raw JSON')
|
|
622
|
+
.action(async (opts) => {
|
|
623
|
+
await handleRuntimeActivationList({
|
|
624
|
+
workspace: opts.workspace,
|
|
625
|
+
channel: opts.channel,
|
|
626
|
+
includeDeactivated: opts.includeDeactivated,
|
|
627
|
+
json: opts.json,
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
// P1 #2 fix: Owner edit entry point — swap a pending approval's artifact.
|
|
632
|
+
// Required because ApprovalQueue.edit() was dead code with no CLI/OpenClaw entry.
|
|
633
|
+
// P2 #5: use .option() instead of .requiredOption() so missing-flag errors
|
|
634
|
+
// produce structured JSON output via the handler, not Commander's pre-handler exit.
|
|
635
|
+
activationCmd
|
|
636
|
+
.command('edit')
|
|
637
|
+
.description('Edit a pending approval to swap its artifact — P1 #2 owner edit entry point')
|
|
638
|
+
.option('-a, --approval-id <id>', 'Approval ID to edit (must be pending)')
|
|
639
|
+
.option('-n, --new-artifact-id <id>', 'New PIArtifact ID to swap to')
|
|
640
|
+
.option('-r, --edit-reason <text>', 'Reason for the edit')
|
|
641
|
+
.option('-w, --workspace <path>', 'Workspace directory')
|
|
642
|
+
.option('--json', 'Output raw JSON')
|
|
643
|
+
.action(async (opts) => {
|
|
644
|
+
await handleRuntimeActivationEdit({
|
|
645
|
+
workspace: opts.workspace,
|
|
646
|
+
approvalId: opts.approvalId,
|
|
647
|
+
newArtifactId: opts.newArtifactId,
|
|
648
|
+
editReason: opts.editReason,
|
|
649
|
+
json: opts.json,
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
|
|
594
653
|
const diagnosticsCmd = runtimeCmd
|
|
595
654
|
.command('diagnostics')
|
|
596
655
|
.description('Control plane diagnostic bundle operations');
|
|
@@ -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
|
+
}
|