@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.
- package/dist/commands/candidate.d.ts +23 -0
- package/dist/commands/candidate.d.ts.map +1 -1
- package/dist/commands/candidate.js +89 -3
- package/dist/commands/candidate.js.map +1 -1
- package/dist/commands/diagnose.d.ts.map +1 -1
- package/dist/commands/diagnose.js +153 -132
- package/dist/commands/diagnose.js.map +1 -1
- package/dist/commands/runtime-features.d.ts.map +1 -1
- package/dist/commands/runtime-features.js +2 -7
- package/dist/commands/runtime-features.js.map +1 -1
- package/dist/commands/runtime-internalization-integrity-repair.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-integrity-repair.js +15 -31
- package/dist/commands/runtime-internalization-integrity-repair.js.map +1 -1
- package/dist/commands/runtime-internalization-run-once.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-run-once.js +246 -326
- package/dist/commands/runtime-internalization-run-once.js.map +1 -1
- package/dist/commands/runtime-recovery.d.ts.map +1 -1
- package/dist/commands/runtime-recovery.js +9 -8
- package/dist/commands/runtime-recovery.js.map +1 -1
- package/dist/services/__tests__/cli-output.test.d.ts +18 -0
- package/dist/services/__tests__/cli-output.test.d.ts.map +1 -0
- package/dist/services/__tests__/cli-output.test.js +103 -0
- package/dist/services/__tests__/cli-output.test.js.map +1 -0
- package/dist/services/__tests__/runtime-adapter-resolver.test.d.ts +18 -0
- package/dist/services/__tests__/runtime-adapter-resolver.test.d.ts.map +1 -0
- package/dist/services/__tests__/runtime-adapter-resolver.test.js +651 -0
- package/dist/services/__tests__/runtime-adapter-resolver.test.js.map +1 -0
- package/dist/services/cli-output.d.ts +61 -0
- package/dist/services/cli-output.d.ts.map +1 -0
- package/dist/services/cli-output.js +72 -0
- package/dist/services/cli-output.js.map +1 -0
- package/dist/services/runtime-adapter-resolver.d.ts +105 -0
- package/dist/services/runtime-adapter-resolver.d.ts.map +1 -0
- package/dist/services/runtime-adapter-resolver.js +188 -0
- package/dist/services/runtime-adapter-resolver.js.map +1 -0
- package/package.json +1 -1
- package/src/commands/candidate.ts +92 -3
- package/src/commands/diagnose.ts +146 -138
- package/src/commands/runtime-features.ts +2 -6
- package/src/commands/runtime-internalization-integrity-repair.ts +16 -28
- package/src/commands/runtime-internalization-run-once.ts +242 -353
- package/src/commands/runtime-recovery.ts +9 -7
- package/src/services/__tests__/cli-output.test.ts +130 -0
- package/src/services/__tests__/runtime-adapter-resolver.test.ts +772 -0
- package/src/services/cli-output.ts +95 -0
- package/src/services/runtime-adapter-resolver.ts +339 -0
- package/tests/commands/candidate-internalization-backfill.test.ts +43 -3
- package/tests/commands/candidate-internalize-lineage.test.ts +521 -0
- package/tests/commands/candidate-internalize.test.ts +31 -5
- package/tests/commands/diagnose.test.ts +7 -3
- package/tests/commands/runtime-internalization-run-once.test.ts +11 -0
- package/tests/commands/runtime-recovery.test.ts +27 -4
- package/tests/services/rulehost-pipeline-e2e.test.ts +40 -7
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRI-432: Shared CLI output module.
|
|
3
|
+
*
|
|
4
|
+
* Extracts common output patterns from 3 CLI commands:
|
|
5
|
+
* - runtime-features.ts (JSON/text emit)
|
|
6
|
+
* - runtime-recovery.ts (dry-run/confirm conflict + JSON/text emit)
|
|
7
|
+
* - runtime-internalization-integrity-repair.ts (conflict + error catch + JSON/text emit)
|
|
8
|
+
*
|
|
9
|
+
* Design decisions:
|
|
10
|
+
* - formatText stays call-site-specific (domain layout differs per command).
|
|
11
|
+
* This module handles only cross-cutting output concerns.
|
|
12
|
+
* - Functions return exit codes; callers handle process.exit()/process.exitCode.
|
|
13
|
+
* This follows CLI Operator Gate rule #2 (no process.exit in shared modules).
|
|
14
|
+
* - JSON error shape: { ok: false, reason: string, nextAction: string }
|
|
15
|
+
* (matches runtime-internalization-integrity-repair.ts canonical pattern)
|
|
16
|
+
*
|
|
17
|
+
* ERR refs:
|
|
18
|
+
* - ERR-001 (no any): all types explicit
|
|
19
|
+
* - ERR-005 (no as bypass): no type casts
|
|
20
|
+
* - ERR-009 (fail-loud): emitError/emitFlagConflict return exit code 1
|
|
21
|
+
* - ERR-002 (graceful degradation with reason): all error paths include reason + nextAction
|
|
22
|
+
* - ERR-014 (bounded preview): JSON.stringify on known shapes only
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// ── Types ──────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
export interface EmitResultOptions<T> {
|
|
28
|
+
/** When true, emit JSON to stdout; when false, emit text via formatText. */
|
|
29
|
+
json: boolean;
|
|
30
|
+
/** Call-site-specific text formatter (domain layout differs per command). */
|
|
31
|
+
formatText: (output: T) => string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface EmitErrorOptions {
|
|
35
|
+
/** When true, emit JSON error to stdout; when false, emit text to stderr. */
|
|
36
|
+
json: boolean;
|
|
37
|
+
/** Next action suggestion for the user. */
|
|
38
|
+
nextAction: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface EmitFlagConflictOptions {
|
|
42
|
+
/** When true, emit JSON error to stdout; when false, emit text to stderr. */
|
|
43
|
+
json: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Functions ──────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Emit a result object as JSON (stdout) or text (stdout via formatText).
|
|
50
|
+
* Does NOT set exit code — caller decides based on result status.
|
|
51
|
+
*/
|
|
52
|
+
export function emitResult<T extends object>(output: T, opts: EmitResultOptions<T>): void {
|
|
53
|
+
if (opts.json) {
|
|
54
|
+
console.log(JSON.stringify(output, null, 2));
|
|
55
|
+
} else {
|
|
56
|
+
console.log(opts.formatText(output));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Emit a dry-run/confirm flag conflict error.
|
|
62
|
+
* Returns exit code 1. Caller should process.exit(1) or set process.exitCode = 1.
|
|
63
|
+
*
|
|
64
|
+
* JSON shape: { ok: false, reason: string, nextAction: string }
|
|
65
|
+
* Text shape: console.error with message
|
|
66
|
+
*/
|
|
67
|
+
export function emitFlagConflict(opts: EmitFlagConflictOptions): number {
|
|
68
|
+
const reason = 'Error: --dry-run and --confirm are mutually exclusive';
|
|
69
|
+
const nextAction = 'Specify only one of --dry-run or --confirm';
|
|
70
|
+
|
|
71
|
+
if (opts.json) {
|
|
72
|
+
console.log(JSON.stringify({ ok: false, reason, nextAction }, null, 2));
|
|
73
|
+
} else {
|
|
74
|
+
console.error(`${reason}. Specify one or the other.`);
|
|
75
|
+
}
|
|
76
|
+
return 1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Emit a structured error from a caught exception.
|
|
81
|
+
* Returns exit code 1. Caller should process.exit(1) or set process.exitCode = 1.
|
|
82
|
+
*
|
|
83
|
+
* JSON shape: { ok: false, reason: string, nextAction: string }
|
|
84
|
+
* Text shape: console.error with "Error: <message>"
|
|
85
|
+
*/
|
|
86
|
+
export function emitError(err: unknown, opts: EmitErrorOptions): number {
|
|
87
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
88
|
+
|
|
89
|
+
if (opts.json) {
|
|
90
|
+
console.log(JSON.stringify({ ok: false, reason, nextAction: opts.nextAction }, null, 2));
|
|
91
|
+
} else {
|
|
92
|
+
console.error(`Error: ${reason}`);
|
|
93
|
+
}
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRI-431 Step 3: Shared runtime-adapter resolver.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from duplicated `resolveRuntimeAdapter` in:
|
|
5
|
+
* - packages/pd-cli/src/commands/runtime-internalization-run-once.ts (L222-551)
|
|
6
|
+
* - packages/pd-cli/src/commands/diagnose.ts (L186-334, inline)
|
|
7
|
+
*
|
|
8
|
+
* Design decisions:
|
|
9
|
+
* - Test-double payloads stay call-site-specific via `testDoublePayloadBuilder` callback.
|
|
10
|
+
* The 7 internalization runner payloads (philosopher/scribe/artificer/...) are unique
|
|
11
|
+
* to run-once.ts; the diagnostician payload is unique to diagnose.ts. Unifying them
|
|
12
|
+
* would couple unrelated domains.
|
|
13
|
+
* - CLI-gate concerns (process.exit, telemetry, help text) stay at call sites.
|
|
14
|
+
* This resolver throws structured `ConfigResolutionError` that handlers translate.
|
|
15
|
+
* - Lives in pd-cli/services (NOT core) because it constructs adapters and reads
|
|
16
|
+
* CLI feature flags via `loadEffectiveFeatureFlags`, which is a pd-cli service.
|
|
17
|
+
*
|
|
18
|
+
* ERR refs:
|
|
19
|
+
* - ERR-001 (no any): all types explicit
|
|
20
|
+
* - ERR-005 (no as bypass): no type casts
|
|
21
|
+
* - ERR-009 (fail-loud): ConfigResolutionError thrown with structured fields
|
|
22
|
+
* - ERR-013 (Object.hasOwn): feature flag check uses Object.hasOwn
|
|
23
|
+
* - ERR-002 (graceful degradation with reason): structured error includes reason + nextAction
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
TestDoubleRuntimeAdapter,
|
|
28
|
+
PiAiRuntimeAdapter,
|
|
29
|
+
OpenClawCliRuntimeAdapter,
|
|
30
|
+
L2AgentLoopAdapter,
|
|
31
|
+
buildL2PrincipleReaderFromLedger,
|
|
32
|
+
loadLedger,
|
|
33
|
+
isRuntimeConfigError,
|
|
34
|
+
validateRuntimeConfig,
|
|
35
|
+
} from '@principles/core/runtime-v2';
|
|
36
|
+
import type { PDRuntimeAdapter, PdL2ArtifactReader, RuntimeConfig, RuntimeConfigResult } from '@principles/core/runtime-v2';
|
|
37
|
+
import { loadEffectiveFeatureFlags } from './feature-flag-loader.js';
|
|
38
|
+
import { resolveRuntimeFromPdConfig } from './resolve-runtime-from-pd-config.js';
|
|
39
|
+
import type { ResolvedRuntimeFromPdConfig } from './resolve-runtime-from-pd-config.js';
|
|
40
|
+
|
|
41
|
+
// ── Error class ────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
export type ConfigResolutionErrorKind =
|
|
44
|
+
| 'missing-fields'
|
|
45
|
+
| 'invalid-config'
|
|
46
|
+
| 'unsupported-runtime'
|
|
47
|
+
| 'test-double-refused';
|
|
48
|
+
|
|
49
|
+
export interface ConfigResolutionErrorDetails {
|
|
50
|
+
/** Required fields that are missing (for 'missing-fields' kind). */
|
|
51
|
+
missing?: string[];
|
|
52
|
+
/** Structured next action for the operator (CLI Operator Gate rule #6). */
|
|
53
|
+
nextAction?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class ConfigResolutionError extends Error {
|
|
57
|
+
readonly kind: ConfigResolutionErrorKind;
|
|
58
|
+
readonly missing?: string[];
|
|
59
|
+
readonly nextAction?: string;
|
|
60
|
+
|
|
61
|
+
constructor(
|
|
62
|
+
message: string,
|
|
63
|
+
kind: ConfigResolutionErrorKind,
|
|
64
|
+
details?: ConfigResolutionErrorDetails,
|
|
65
|
+
) {
|
|
66
|
+
super(message);
|
|
67
|
+
this.name = 'ConfigResolutionError';
|
|
68
|
+
this.kind = kind;
|
|
69
|
+
this.missing = details?.missing;
|
|
70
|
+
this.nextAction = details?.nextAction;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Options interface ──────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
export interface ResolveAdapterOptions {
|
|
77
|
+
/** Runtime kind: 'test-double' | 'pi-ai' | 'openclaw-cli' | 'config' */
|
|
78
|
+
runtimeKind: string;
|
|
79
|
+
/** Workspace directory (for config resolution + feature flags) */
|
|
80
|
+
workspaceDir: string;
|
|
81
|
+
/** Runner kind (for L2 dreamer routing). Optional. */
|
|
82
|
+
runnerKind?: string;
|
|
83
|
+
/** CLI timeout override. Takes precedence over config timeoutMs. */
|
|
84
|
+
timeoutMs?: number;
|
|
85
|
+
/**
|
|
86
|
+
* Whether test-double runtime is allowed. Default: false.
|
|
87
|
+
* When false, 'test-double' runtimeKind throws ConfigResolutionError.
|
|
88
|
+
*/
|
|
89
|
+
allowTestDouble?: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Call-site-specific test-double payload builder.
|
|
92
|
+
* Required when runtimeKind === 'test-double' and allowTestDouble === true.
|
|
93
|
+
* Receives the full opts object so the builder can access taskId, runnerKind, etc.
|
|
94
|
+
*/
|
|
95
|
+
testDoublePayloadBuilder?: (opts: ResolveAdapterOptions) => PDRuntimeAdapter;
|
|
96
|
+
/**
|
|
97
|
+
* CLI overrides for pi-ai runtime (from --provider, --model, etc. flags).
|
|
98
|
+
* When provided, these take precedence over config values.
|
|
99
|
+
*/
|
|
100
|
+
piAiOverrides?: {
|
|
101
|
+
provider?: string;
|
|
102
|
+
model?: string;
|
|
103
|
+
apiKeyEnv?: string;
|
|
104
|
+
baseUrl?: string;
|
|
105
|
+
maxRetries?: number;
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* CLI override for openclaw mode (from --openclaw-local / --openclaw-gateway flags).
|
|
109
|
+
* When provided, takes precedence over config openclawMode.
|
|
110
|
+
*/
|
|
111
|
+
openclawMode?: 'local' | 'gateway';
|
|
112
|
+
/** PRI-419: L2 artifact reader (only used when l2_dreamer is on). */
|
|
113
|
+
l2ArtifactReader?: PdL2ArtifactReader;
|
|
114
|
+
/** PRI-419: workspace stateDir for L2 principle reader (only used when l2_dreamer is on). */
|
|
115
|
+
l2StateDir?: string;
|
|
116
|
+
/** PRI-431 Step 1d: CLI agentId override for openclaw-cli (from --agent flag). Default: 'main'. */
|
|
117
|
+
agentId?: string;
|
|
118
|
+
/**
|
|
119
|
+
* PRI-431 Step 1d: Whether config resolution errors are tolerated (proceed with CLI overrides alone).
|
|
120
|
+
* When true, pi-ai branch skips validateRuntimeConfig and uses manual missing-field check.
|
|
121
|
+
* Default: false (config errors throw ConfigResolutionError).
|
|
122
|
+
*/
|
|
123
|
+
configOptional?: boolean;
|
|
124
|
+
/**
|
|
125
|
+
* PRI-431 Step 1d: Whether to validate that process.env[apiKeyEnv] is set before
|
|
126
|
+
* constructing PiAiRuntimeAdapter. Default: false (env var check stays at call site for backward compat).
|
|
127
|
+
*/
|
|
128
|
+
validateApiKeyEnv?: boolean;
|
|
129
|
+
/**
|
|
130
|
+
* PRI-431 Step 1d: Callback invoked with the full resolved config object (including legacyWarnings)
|
|
131
|
+
* after successful config resolution. Useful for telemetry, legacyWarnings printing, etc.
|
|
132
|
+
* NOT called for test-double branch (no config resolution).
|
|
133
|
+
* Called even when config returns error IF configOptional is true.
|
|
134
|
+
*/
|
|
135
|
+
onConfigResolved?: (resolved: ResolvedRuntimeFromPdConfig) => void;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Resolver function ──────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
export function resolveRuntimeAdapterFromConfig(opts: ResolveAdapterOptions): PDRuntimeAdapter {
|
|
141
|
+
// ── test-double branch ──────────────────────────────────────────────────
|
|
142
|
+
if (opts.runtimeKind === 'test-double') {
|
|
143
|
+
if (!opts.allowTestDouble) {
|
|
144
|
+
throw new ConfigResolutionError(
|
|
145
|
+
'runtimeKind "test-double" requires allowTestDouble=true.',
|
|
146
|
+
'test-double-refused',
|
|
147
|
+
{ nextAction: 'Pass --allow-test-double or use a production runtime kind.' },
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
if (!opts.testDoublePayloadBuilder) {
|
|
151
|
+
throw new ConfigResolutionError(
|
|
152
|
+
'runtimeKind "test-double" requires testDoublePayloadBuilder callback.',
|
|
153
|
+
'missing-fields',
|
|
154
|
+
{
|
|
155
|
+
missing: ['testDoublePayloadBuilder'],
|
|
156
|
+
nextAction:
|
|
157
|
+
'Provide a testDoublePayloadBuilder function that constructs the test-double adapter.',
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return opts.testDoublePayloadBuilder(opts);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── Resolve config from .pd/config.yaml (for pi-ai, openclaw-cli, config) ──
|
|
165
|
+
const resolved = resolveRuntimeFromPdConfig(opts.workspaceDir);
|
|
166
|
+
const configResult: RuntimeConfigResult = resolved.result;
|
|
167
|
+
|
|
168
|
+
// PRI-431 Step 1d: invoke onConfigResolved callback (for telemetry, legacyWarnings, etc.)
|
|
169
|
+
opts.onConfigResolved?.(resolved);
|
|
170
|
+
|
|
171
|
+
if (isRuntimeConfigError(configResult)) {
|
|
172
|
+
if (!opts.configOptional) {
|
|
173
|
+
throw new ConfigResolutionError(
|
|
174
|
+
`Config resolution from .pd/config.yaml failed: ${configResult.reason}. ${configResult.message}`,
|
|
175
|
+
'invalid-config',
|
|
176
|
+
{ nextAction: configResult.nextAction },
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
// configOptional=true: proceed with CLI overrides alone (pi-ai branch handles missing fields)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── pi-ai branch ────────────────────────────────────────────────────────
|
|
183
|
+
// When runtimeKind === 'config', configResult must be a valid RuntimeConfig
|
|
184
|
+
// (if it were an error and !configOptional, we threw above; if configOptional,
|
|
185
|
+
// the caller passes an explicit runtimeKind, never 'config'). The type guard
|
|
186
|
+
// also narrows for TypeScript.
|
|
187
|
+
const isPiAi =
|
|
188
|
+
opts.runtimeKind === 'pi-ai' ||
|
|
189
|
+
(opts.runtimeKind === 'config' &&
|
|
190
|
+
!isRuntimeConfigError(configResult) &&
|
|
191
|
+
configResult.runtimeKind === 'pi-ai');
|
|
192
|
+
if (isPiAi) {
|
|
193
|
+
// PRI-431 Step 1d + PR review fix: validateRuntimeConfig is only called when
|
|
194
|
+
// config is valid AND configOptional is false. When configOptional=true (diagnose.ts),
|
|
195
|
+
// skip validateRuntimeConfig to match the original diagnose.ts behavior (which never
|
|
196
|
+
// called it — only did a manual missing-field check on merged values). This avoids
|
|
197
|
+
// a behavior change where validateRuntimeConfig would reject configs missing baseUrl
|
|
198
|
+
// for non-built-in providers, even when the user passes --baseUrl on the CLI.
|
|
199
|
+
if (!opts.configOptional && !isRuntimeConfigError(configResult)) {
|
|
200
|
+
try {
|
|
201
|
+
validateRuntimeConfig(configResult);
|
|
202
|
+
} catch (err) {
|
|
203
|
+
throw new ConfigResolutionError(
|
|
204
|
+
err instanceof Error ? err.message : String(err),
|
|
205
|
+
'invalid-config',
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Merge CLI overrides with config values (overrides take precedence)
|
|
211
|
+
// When configOptional and config failed, configResult fields are unavailable — use empty object
|
|
212
|
+
const configFields: Partial<RuntimeConfig> = isRuntimeConfigError(configResult) ? {} : configResult;
|
|
213
|
+
const provider = opts.piAiOverrides?.provider ?? configFields.provider;
|
|
214
|
+
const model = opts.piAiOverrides?.model ?? configFields.model;
|
|
215
|
+
const apiKeyEnv = opts.piAiOverrides?.apiKeyEnv ?? configFields.apiKeyEnv;
|
|
216
|
+
const baseUrl = opts.piAiOverrides?.baseUrl ?? configFields.baseUrl;
|
|
217
|
+
const maxRetries = opts.piAiOverrides?.maxRetries ?? configFields.maxRetries;
|
|
218
|
+
const adapterTimeoutMs = opts.timeoutMs ?? configFields.timeoutMs;
|
|
219
|
+
|
|
220
|
+
// PRI-431 Step 1d + PR review fix: manual missing-field check on MERGED values
|
|
221
|
+
// when configOptional=true. The original diagnose.ts always did this check on merged
|
|
222
|
+
// values (not just when config failed), so we replicate that here. This covers both
|
|
223
|
+
// the "config failed" case and the "config valid but missing fields not in config" case.
|
|
224
|
+
if (opts.configOptional) {
|
|
225
|
+
const missing: string[] = [];
|
|
226
|
+
if (!provider) missing.push('provider');
|
|
227
|
+
if (!model) missing.push('model');
|
|
228
|
+
if (!apiKeyEnv) missing.push('apiKeyEnv');
|
|
229
|
+
if (missing.length > 0) {
|
|
230
|
+
throw new ConfigResolutionError(
|
|
231
|
+
`Missing required pi-ai config: ${missing.join(', ')}.`,
|
|
232
|
+
'missing-fields',
|
|
233
|
+
{
|
|
234
|
+
missing,
|
|
235
|
+
nextAction:
|
|
236
|
+
'Pass --provider, --model, --apiKeyEnv flags or configure .pd/config.yaml runtime profile.',
|
|
237
|
+
},
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// PRI-431 Step 1d: validate API key env var when requested
|
|
243
|
+
if (opts.validateApiKeyEnv && apiKeyEnv && !process.env[apiKeyEnv]) {
|
|
244
|
+
throw new ConfigResolutionError(
|
|
245
|
+
`Environment variable '${apiKeyEnv}' is not set.`,
|
|
246
|
+
'invalid-config',
|
|
247
|
+
{
|
|
248
|
+
nextAction:
|
|
249
|
+
'Set the env var (e.g., export OPENAI_API_KEY=...) or choose a different apiKeyEnv.',
|
|
250
|
+
},
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// PRI-419: L2 dreamer sub-branch — when l2_dreamer flag is on AND this is the
|
|
255
|
+
// dreamer runner, route through the L2 multi-turn agent loop adapter.
|
|
256
|
+
// Other runners (philosopher/scribe/...) stay on L1.
|
|
257
|
+
if (opts.runnerKind === 'dreamer' && opts.l2ArtifactReader && opts.l2StateDir) {
|
|
258
|
+
const effectiveFlags = loadEffectiveFeatureFlags(opts.workspaceDir);
|
|
259
|
+
const l2Flag = Object.hasOwn(effectiveFlags.flags, 'l2_dreamer')
|
|
260
|
+
? effectiveFlags.flags.l2_dreamer.enabled
|
|
261
|
+
: false;
|
|
262
|
+
if (l2Flag) {
|
|
263
|
+
return new L2AgentLoopAdapter(
|
|
264
|
+
{
|
|
265
|
+
provider: String(provider),
|
|
266
|
+
model: String(model),
|
|
267
|
+
apiKeyEnv: String(apiKeyEnv),
|
|
268
|
+
baseUrl,
|
|
269
|
+
workspace: opts.workspaceDir,
|
|
270
|
+
totalBudgetMs: adapterTimeoutMs,
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
artifactReader: opts.l2ArtifactReader,
|
|
274
|
+
principleReader: buildL2PrincipleReaderFromLedger(loadLedger(opts.l2StateDir)),
|
|
275
|
+
},
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return new PiAiRuntimeAdapter({
|
|
281
|
+
provider: String(provider),
|
|
282
|
+
model: String(model),
|
|
283
|
+
apiKeyEnv: String(apiKeyEnv),
|
|
284
|
+
maxRetries,
|
|
285
|
+
timeoutMs: adapterTimeoutMs,
|
|
286
|
+
baseUrl,
|
|
287
|
+
workspace: opts.workspaceDir,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ── openclaw-cli branch ─────────────────────────────────────────────────
|
|
292
|
+
const isOpenClawCli =
|
|
293
|
+
opts.runtimeKind === 'openclaw-cli' ||
|
|
294
|
+
(opts.runtimeKind === 'config' &&
|
|
295
|
+
!isRuntimeConfigError(configResult) &&
|
|
296
|
+
configResult.runtimeKind === 'openclaw-cli');
|
|
297
|
+
if (isOpenClawCli) {
|
|
298
|
+
// PRI-431 Step 1d: handle configOptional — when config failed, configFields is empty
|
|
299
|
+
const openclawConfigFields: Partial<RuntimeConfig> = isRuntimeConfigError(configResult)
|
|
300
|
+
? {}
|
|
301
|
+
: configResult;
|
|
302
|
+
// CLI override takes precedence over config
|
|
303
|
+
const openclawMode = opts.openclawMode ?? openclawConfigFields.openclawMode;
|
|
304
|
+
if (!openclawMode) {
|
|
305
|
+
throw new ConfigResolutionError(
|
|
306
|
+
"runtimeKind 'openclaw-cli' requires openclawMode.",
|
|
307
|
+
'missing-fields',
|
|
308
|
+
{
|
|
309
|
+
missing: ['openclawMode'],
|
|
310
|
+
nextAction:
|
|
311
|
+
'Add openclawMode: local|gateway to your .pd/config.yaml runtime profile or use --openclaw-local/--openclaw-gateway flags.',
|
|
312
|
+
},
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
// PRI-431 Step 1d: only pass agentId when explicitly provided (backward compat with run-once.ts)
|
|
316
|
+
const openclawAdapterOpts: {
|
|
317
|
+
runtimeMode: 'local' | 'gateway';
|
|
318
|
+
workspaceDir: string;
|
|
319
|
+
agentId?: string;
|
|
320
|
+
} = {
|
|
321
|
+
runtimeMode: openclawMode,
|
|
322
|
+
workspaceDir: opts.workspaceDir,
|
|
323
|
+
};
|
|
324
|
+
if (opts.agentId !== undefined) {
|
|
325
|
+
openclawAdapterOpts.agentId = opts.agentId;
|
|
326
|
+
}
|
|
327
|
+
return new OpenClawCliRuntimeAdapter(openclawAdapterOpts);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ── Unsupported runtime ────────────────────────────────────────────────
|
|
331
|
+
throw new Error(
|
|
332
|
+
`Unsupported runtime kind: ${opts.runtimeKind}. Supported: test-double, pi-ai, openclaw-cli, config`,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ── Re-exports for call-site convenience ───────────────────────────────────
|
|
337
|
+
// These allow call sites to import everything from one module.
|
|
338
|
+
export type { RuntimeConfig, RuntimeConfigResult };
|
|
339
|
+
export { TestDoubleRuntimeAdapter, isRuntimeConfigError, validateRuntimeConfig };
|
|
@@ -134,11 +134,25 @@ function setupDefaultMocks(): void {
|
|
|
134
134
|
mockGetCandidate.mockImplementation((id: string) =>
|
|
135
135
|
Promise.resolve({
|
|
136
136
|
candidateId: id,
|
|
137
|
+
taskId: `diag-task-${id}`,
|
|
137
138
|
description: `candidate ${id}`,
|
|
138
139
|
sourceRecommendationJson: JSON.stringify({ kind: 'principle', description: `candidate ${id}` }),
|
|
139
140
|
}),
|
|
140
141
|
);
|
|
141
|
-
|
|
142
|
+
// PRI-435: getTask must return a diagnostician task with sourcePainId when called
|
|
143
|
+
// with candidate.taskId (diag-task-*), and null for dreamer task lookups.
|
|
144
|
+
mockGetTask.mockImplementation((taskId: string) => {
|
|
145
|
+
if (taskId.startsWith('diag-task-')) {
|
|
146
|
+
return Promise.resolve({
|
|
147
|
+
taskId,
|
|
148
|
+
taskKind: 'diagnostician',
|
|
149
|
+
status: 'completed',
|
|
150
|
+
diagnosticJson: JSON.stringify({ sourcePainId: `pain-${taskId}` }),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
// Dreamer task lookup → null (no existing dreamer task)
|
|
154
|
+
return Promise.resolve(null);
|
|
155
|
+
});
|
|
142
156
|
mockCreateTask.mockImplementation((input: { taskId: string }) =>
|
|
143
157
|
Promise.resolve({ taskId: input.taskId }),
|
|
144
158
|
);
|
|
@@ -260,7 +274,20 @@ describe('pd candidate internalization backfill', () => {
|
|
|
260
274
|
if (sql.includes("'pending'")) return [{ candidate_id: 'cand-pending-1' }];
|
|
261
275
|
return [];
|
|
262
276
|
});
|
|
263
|
-
|
|
277
|
+
// PRI-435: getTask returns diagnostician task for diag-task-* lookups,
|
|
278
|
+
// and dreamer task for dreamer-cand-* lookups (testing idempotency).
|
|
279
|
+
mockGetTask.mockImplementation((taskId: string) => {
|
|
280
|
+
if (taskId.startsWith('diag-task-')) {
|
|
281
|
+
return Promise.resolve({
|
|
282
|
+
taskId,
|
|
283
|
+
taskKind: 'diagnostician',
|
|
284
|
+
status: 'completed',
|
|
285
|
+
diagnosticJson: JSON.stringify({ sourcePainId: `pain-${taskId}` }),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
// Dreamer task already exists → idempotency check
|
|
289
|
+
return Promise.resolve({ taskId: 'dreamer-cand-pending-1-prompt' });
|
|
290
|
+
});
|
|
264
291
|
|
|
265
292
|
await handleCandidateInternalizationBackfill({ workspace: WS, includePending: true, confirm: true, json: true });
|
|
266
293
|
|
|
@@ -396,11 +423,24 @@ describe('Commander wiring for backfill --include-pending', () => {
|
|
|
396
423
|
mockGetCandidate.mockImplementation((id: string) =>
|
|
397
424
|
Promise.resolve({
|
|
398
425
|
candidateId: id,
|
|
426
|
+
taskId: `diag-task-${id}`,
|
|
399
427
|
description: `candidate ${id}`,
|
|
400
428
|
sourceRecommendationJson: JSON.stringify({ kind: 'principle', description: `candidate ${id}` }),
|
|
401
429
|
}),
|
|
402
430
|
);
|
|
403
|
-
|
|
431
|
+
// PRI-435: getTask returns diagnostician task with sourcePainId for diag-task-* lookups,
|
|
432
|
+
// null for dreamer task lookups (no existing dreamer task).
|
|
433
|
+
mockGetTask.mockImplementation((taskId: string) => {
|
|
434
|
+
if (taskId.startsWith('diag-task-')) {
|
|
435
|
+
return Promise.resolve({
|
|
436
|
+
taskId,
|
|
437
|
+
taskKind: 'diagnostician',
|
|
438
|
+
status: 'completed',
|
|
439
|
+
diagnosticJson: JSON.stringify({ sourcePainId: `pain-${taskId}` }),
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
return Promise.resolve(null);
|
|
443
|
+
});
|
|
404
444
|
});
|
|
405
445
|
|
|
406
446
|
afterEach(() => {
|