@principles/pd-cli 1.145.1 → 1.146.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/README.md +11 -2
- package/dist/commands/build-trajectory-evidence.d.ts +25 -6
- package/dist/commands/build-trajectory-evidence.d.ts.map +1 -1
- package/dist/commands/build-trajectory-evidence.js +174 -114
- package/dist/commands/build-trajectory-evidence.js.map +1 -1
- package/dist/commands/diagnose.d.ts.map +1 -1
- package/dist/commands/diagnose.js +48 -27
- package/dist/commands/diagnose.js.map +1 -1
- package/dist/commands/pain-record.d.ts.map +1 -1
- package/dist/commands/pain-record.js +212 -17
- package/dist/commands/pain-record.js.map +1 -1
- package/dist/commands/pain-retry.d.ts.map +1 -1
- package/dist/commands/pain-retry.js +43 -26
- package/dist/commands/pain-retry.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/build-trajectory-evidence.ts +211 -122
- package/src/commands/diagnose.ts +59 -36
- package/src/commands/pain-record.ts +224 -16
- package/src/commands/pain-retry.ts +56 -36
- package/src/index.ts +1 -1
- package/tests/bdd/cli-contract.steps.ts +3 -0
- package/tests/commands/build-trajectory-evidence.test.ts +226 -161
- package/tests/commands/diagnose.test.ts +82 -0
- package/tests/commands/pain-record-async.test.ts +36 -30
- package/tests/commands/pain-record-session-parser.test.ts +127 -0
- package/tests/commands/pain-record.test.ts +205 -37
- package/tests/commands/pain-retry.test.ts +72 -1
|
@@ -20,123 +20,122 @@ import {
|
|
|
20
20
|
import type { PainEvidenceEntry } from '@principles/core/runtime-v2';
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* @param workspaceDir - Workspace directory for path redaction (sanitizeString)
|
|
28
|
-
* @returns Array of PainEvidenceEntry (never empty; degraded entries on failure)
|
|
23
|
+
* PRI-642 (SPEC §7.3): discriminated acquisition result. `unavailable` carries
|
|
24
|
+
* a distinct reasonCode so `pd pain record --session` can fail/degrade
|
|
25
|
+
* explicitly BEFORE any LLM/task/candidate mutation, without placeholder
|
|
26
|
+
* evidence.
|
|
29
27
|
*/
|
|
30
|
-
export
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
export type TrajectoryEvidenceAcquisition =
|
|
29
|
+
| { status: 'available'; entries: PainEvidenceEntry[] }
|
|
30
|
+
| {
|
|
31
|
+
status: 'unavailable';
|
|
32
|
+
reasonCode: 'trajectory_unavailable' | 'session_not_found' | 'empty_trajectory' | 'evidence_read_failed';
|
|
33
|
+
detail: string;
|
|
34
|
+
};
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
/** SourceRef markers that carry no real behavior trace (placeholder shapes). */
|
|
37
|
+
const PLACEHOLDER_SOURCE_REFS = new Set([
|
|
38
|
+
'owner_reported:cli',
|
|
39
|
+
'owner_message:unavailable',
|
|
40
|
+
'agent_turn:unavailable',
|
|
41
|
+
'tool_call_failure:unavailable',
|
|
42
|
+
'trajectory:empty',
|
|
43
|
+
]);
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
});
|
|
52
|
-
return evidence;
|
|
53
|
-
}
|
|
45
|
+
interface TrajectoryDbCollection {
|
|
46
|
+
entries: PainEvidenceEntry[];
|
|
47
|
+
realEntryCount: number;
|
|
48
|
+
readFailed: boolean;
|
|
49
|
+
}
|
|
54
50
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Collect evidence entries from an open trajectory.db for one session.
|
|
53
|
+
* Shared by the typed acquisition API and the legacy array wrapper so the
|
|
54
|
+
* two can never drift.
|
|
55
|
+
*/
|
|
56
|
+
function collectEvidenceFromDb(
|
|
57
|
+
db: Database.Database,
|
|
58
|
+
sessionId: string,
|
|
59
|
+
workspaceDir?: string,
|
|
60
|
+
): TrajectoryDbCollection {
|
|
61
|
+
const evidence: PainEvidenceEntry[] = [];
|
|
62
|
+
let readFailed = false;
|
|
65
63
|
|
|
64
|
+
// Try to read user turns with correction detection
|
|
66
65
|
try {
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
const userTurns = db.prepare(`
|
|
66
|
+
const userTurns = db.prepare(`
|
|
70
67
|
SELECT id, raw_excerpt, correction_detected, correction_cue, created_at
|
|
71
68
|
FROM user_turns
|
|
72
69
|
WHERE session_id = ?
|
|
73
70
|
ORDER BY id ASC
|
|
74
71
|
`).all(sessionId) as Record<string, unknown>[];
|
|
75
72
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
} catch {
|
|
91
|
-
// user_turns table may not exist — degrade gracefully
|
|
92
|
-
if (evidence.length < MAX_EVIDENCE_ENTRIES) {
|
|
93
|
-
evidence.push({
|
|
94
|
-
sourceRef: 'owner_message:unavailable',
|
|
95
|
-
note: 'trajectory_user_turns_unavailable',
|
|
96
|
-
});
|
|
97
|
-
}
|
|
73
|
+
const lastCorrectionTurn = [...userTurns].reverse().find(t => Boolean(t.correction_detected));
|
|
74
|
+
if (lastCorrectionTurn) {
|
|
75
|
+
const rawExcerpt = typeof lastCorrectionTurn.raw_excerpt === 'string'
|
|
76
|
+
? lastCorrectionTurn.raw_excerpt
|
|
77
|
+
: '';
|
|
78
|
+
const sanitizedNote = sanitizeString(
|
|
79
|
+
rawExcerpt.slice(0, MAX_EVIDENCE_NOTE_CHARS),
|
|
80
|
+
workspaceDir,
|
|
81
|
+
);
|
|
82
|
+
evidence.push({
|
|
83
|
+
sourceRef: `owner_message:${String(lastCorrectionTurn.created_at ?? 'unknown')}`,
|
|
84
|
+
note: sanitizedNote,
|
|
85
|
+
});
|
|
98
86
|
}
|
|
87
|
+
} catch {
|
|
88
|
+
readFailed = true;
|
|
89
|
+
// user_turns table may not exist — degrade gracefully
|
|
90
|
+
if (evidence.length < MAX_EVIDENCE_ENTRIES) {
|
|
91
|
+
evidence.push({
|
|
92
|
+
sourceRef: 'owner_message:unavailable',
|
|
93
|
+
note: 'trajectory_user_turns_unavailable',
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
99
97
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
// Try to read assistant turns (last 3)
|
|
99
|
+
try {
|
|
100
|
+
const assistantTurns = db.prepare(`
|
|
103
101
|
SELECT id, sanitized_text, stop_reason, created_at
|
|
104
102
|
FROM assistant_turns
|
|
105
103
|
WHERE session_id = ?
|
|
106
104
|
ORDER BY id ASC
|
|
107
105
|
`).all(sessionId) as Record<string, unknown>[];
|
|
108
106
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
} catch {
|
|
128
|
-
// assistant_turns table may not exist — degrade gracefully
|
|
129
|
-
if (evidence.length < MAX_EVIDENCE_ENTRIES) {
|
|
130
|
-
evidence.push({
|
|
131
|
-
sourceRef: 'agent_turn:unavailable',
|
|
132
|
-
note: 'trajectory_assistant_turns_unavailable',
|
|
133
|
-
});
|
|
134
|
-
}
|
|
107
|
+
const recentAssistant = assistantTurns.slice(-3);
|
|
108
|
+
for (const turn of recentAssistant) {
|
|
109
|
+
if (evidence.length >= MAX_EVIDENCE_ENTRIES) break;
|
|
110
|
+
const sanitizedText = typeof turn.sanitized_text === 'string'
|
|
111
|
+
? turn.sanitized_text
|
|
112
|
+
: '';
|
|
113
|
+
const sanitizedNote = sanitizeString(
|
|
114
|
+
sanitizedText.slice(0, MAX_EVIDENCE_NOTE_CHARS),
|
|
115
|
+
workspaceDir,
|
|
116
|
+
);
|
|
117
|
+
// Enhanced: append truncation warning when stop_reason=length
|
|
118
|
+
const stopReason = typeof turn.stop_reason === 'string' ? turn.stop_reason : null;
|
|
119
|
+
const truncationWarning = stopReason === 'length' ? ' [TRUNCATED: output cut off by length limit]' : '';
|
|
120
|
+
evidence.push({
|
|
121
|
+
sourceRef: `agent_turn:${String(turn.created_at ?? 'unknown')}`,
|
|
122
|
+
note: sanitizedNote + truncationWarning,
|
|
123
|
+
});
|
|
135
124
|
}
|
|
125
|
+
} catch {
|
|
126
|
+
readFailed = true;
|
|
127
|
+
// assistant_turns table may not exist — degrade gracefully
|
|
128
|
+
if (evidence.length < MAX_EVIDENCE_ENTRIES) {
|
|
129
|
+
evidence.push({
|
|
130
|
+
sourceRef: 'agent_turn:unavailable',
|
|
131
|
+
note: 'trajectory_assistant_turns_unavailable',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
136
135
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
136
|
+
// PRI-358: Try to read failed tool_calls (last 3 failures, chronological order)
|
|
137
|
+
try {
|
|
138
|
+
const failedToolCalls = db.prepare(`
|
|
140
139
|
SELECT tool_name, error_type, exit_code, result_preview, created_at
|
|
141
140
|
FROM (
|
|
142
141
|
SELECT tool_name, error_type, exit_code, result_preview, created_at
|
|
@@ -148,39 +147,129 @@ export function buildTrajectoryEvidenceFromDb(
|
|
|
148
147
|
ORDER BY created_at ASC
|
|
149
148
|
`).all(sessionId) as Record<string, unknown>[];
|
|
150
149
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
} catch {
|
|
166
|
-
// tool_calls table may not exist — degrade gracefully (only when no other evidence)
|
|
167
|
-
if (evidence.length === 0) {
|
|
168
|
-
evidence.push({
|
|
169
|
-
sourceRef: 'tool_call_failure:unavailable',
|
|
170
|
-
note: 'trajectory_tool_calls_unavailable',
|
|
171
|
-
});
|
|
172
|
-
}
|
|
150
|
+
for (const tc of failedToolCalls) {
|
|
151
|
+
if (evidence.length >= MAX_EVIDENCE_ENTRIES) break;
|
|
152
|
+
const toolName = typeof tc.tool_name === 'string' ? tc.tool_name : 'unknown';
|
|
153
|
+
const errorType = typeof tc.error_type === 'string' ? tc.error_type : 'unknown';
|
|
154
|
+
const exitCode = tc.exit_code != null ? String(tc.exit_code) : 'N/A';
|
|
155
|
+
// Enhanced: append resultPreview when available
|
|
156
|
+
const resultPreview = typeof tc.result_preview === 'string' ? tc.result_preview : null;
|
|
157
|
+
const previewSuffix = resultPreview ? ` | ${resultPreview.slice(0, 200)}` : '';
|
|
158
|
+
const note = `Tool ${toolName} failed: ${errorType} (exitCode: ${exitCode})${previewSuffix}`;
|
|
159
|
+
evidence.push({
|
|
160
|
+
sourceRef: `tool_call_failure:${String(tc.created_at ?? 'unknown')}`,
|
|
161
|
+
note: sanitizeString(note.slice(0, MAX_EVIDENCE_NOTE_CHARS), workspaceDir),
|
|
162
|
+
});
|
|
173
163
|
}
|
|
174
|
-
|
|
175
|
-
|
|
164
|
+
} catch {
|
|
165
|
+
readFailed = true;
|
|
166
|
+
// tool_calls table may not exist — degrade gracefully (only when no other evidence)
|
|
176
167
|
if (evidence.length === 0) {
|
|
177
168
|
evidence.push({
|
|
178
|
-
sourceRef: '
|
|
179
|
-
note: '
|
|
169
|
+
sourceRef: 'tool_call_failure:unavailable',
|
|
170
|
+
note: 'trajectory_tool_calls_unavailable',
|
|
180
171
|
});
|
|
181
172
|
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// If no evidence at all from trajectory, provide a meaningful placeholder
|
|
176
|
+
if (evidence.length === 0) {
|
|
177
|
+
evidence.push({
|
|
178
|
+
sourceRef: 'trajectory:empty',
|
|
179
|
+
note: 'trajectory_available_but_empty: no user correction or assistant turns found',
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const bounded = evidence.slice(0, MAX_EVIDENCE_ENTRIES);
|
|
184
|
+
const realEntryCount = bounded.reduce(
|
|
185
|
+
(count, entry) => count + (PLACEHOLDER_SOURCE_REFS.has(entry.sourceRef) ? 0 : 1),
|
|
186
|
+
0,
|
|
187
|
+
);
|
|
188
|
+
return { entries: bounded, realEntryCount, readFailed };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* PRI-642 Scope A typed acquisition from trajectory.db (SPEC §7.3).
|
|
193
|
+
*
|
|
194
|
+
* Validates that the requested session exists in the trajectory `sessions`
|
|
195
|
+
* table (every recorded turn/tool call upserts its session row, so an absent
|
|
196
|
+
* row means the session was never seen) and classifies the outcome:
|
|
197
|
+
* - `available` — session bound, ≥1 real behavior-trace entry;
|
|
198
|
+
* - `session_not_found` — session absent from the sessions table (or a
|
|
199
|
+
* `cli`/`unknown` sentinel — never a real session);
|
|
200
|
+
* - `trajectory_unavailable` — no trajectory.db in this workspace;
|
|
201
|
+
* - `evidence_read_failed` — the DB exists but cannot be opened/read;
|
|
202
|
+
* - `empty_trajectory` — real session, no usable evidence rows.
|
|
203
|
+
*/
|
|
204
|
+
export function acquireTrajectoryEvidenceFromDb(
|
|
205
|
+
stateDir: string,
|
|
206
|
+
sessionId: string | undefined,
|
|
207
|
+
workspaceDir?: string,
|
|
208
|
+
): TrajectoryEvidenceAcquisition {
|
|
209
|
+
if (!sessionId || sessionId === 'cli' || sessionId === 'unknown') {
|
|
210
|
+
return {
|
|
211
|
+
status: 'unavailable',
|
|
212
|
+
reasonCode: 'session_not_found',
|
|
213
|
+
detail: sessionId ? `sentinel_session_id:${sessionId}` : 'missing_session_id',
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const dbPath = path.join(stateDir, 'trajectory.db');
|
|
218
|
+
if (!fs.existsSync(dbPath)) {
|
|
219
|
+
return {
|
|
220
|
+
status: 'unavailable',
|
|
221
|
+
reasonCode: 'trajectory_unavailable',
|
|
222
|
+
detail: 'trajectory_db_missing',
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let db: Database.Database;
|
|
227
|
+
try {
|
|
228
|
+
db = new Database(dbPath, { readonly: true });
|
|
229
|
+
} catch (err) {
|
|
230
|
+
return {
|
|
231
|
+
status: 'unavailable',
|
|
232
|
+
reasonCode: 'evidence_read_failed',
|
|
233
|
+
detail: `trajectory_db_unreadable: ${err instanceof Error ? err.message : String(err)}`,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
// Session existence: rows are upserted by every turn/tool-call write, so
|
|
239
|
+
// an absent row means the trajectory never saw this session.
|
|
240
|
+
let sessionExists = false;
|
|
241
|
+
try {
|
|
242
|
+
const row = db.prepare('SELECT 1 FROM sessions WHERE session_id = ?').get(sessionId);
|
|
243
|
+
sessionExists = row !== undefined;
|
|
244
|
+
} catch {
|
|
245
|
+
// sessions table missing → cannot validate existence; fall through and
|
|
246
|
+
// let the evidence rows decide (legacy DBs may lack the table).
|
|
247
|
+
sessionExists = true;
|
|
248
|
+
}
|
|
249
|
+
if (!sessionExists) {
|
|
250
|
+
return {
|
|
251
|
+
status: 'unavailable',
|
|
252
|
+
reasonCode: 'session_not_found',
|
|
253
|
+
detail: 'session_not_present_in_trajectory',
|
|
254
|
+
};
|
|
255
|
+
}
|
|
182
256
|
|
|
183
|
-
|
|
257
|
+
const collection = collectEvidenceFromDb(db, sessionId, workspaceDir);
|
|
258
|
+
if (collection.realEntryCount > 0) {
|
|
259
|
+
return { status: 'available', entries: collection.entries };
|
|
260
|
+
}
|
|
261
|
+
if (collection.readFailed) {
|
|
262
|
+
return {
|
|
263
|
+
status: 'unavailable',
|
|
264
|
+
reasonCode: 'evidence_read_failed',
|
|
265
|
+
detail: 'trajectory_tables_unreadable',
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
status: 'unavailable',
|
|
270
|
+
reasonCode: 'empty_trajectory',
|
|
271
|
+
detail: 'session_present_but_no_usable_evidence',
|
|
272
|
+
};
|
|
184
273
|
} finally {
|
|
185
274
|
db.close();
|
|
186
275
|
}
|
package/src/commands/diagnose.ts
CHANGED
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
DiagRouterRunner,
|
|
21
21
|
DefaultDiagRootCauseValidator,
|
|
22
22
|
DefaultDiagDistillerValidator,
|
|
23
|
-
DisabledDiagnosticianRunner,
|
|
24
23
|
type DiagnosticianRunnerLike,
|
|
25
24
|
TestDoubleRuntimeAdapter,
|
|
26
25
|
PDRuntimeError,
|
|
@@ -37,8 +36,8 @@ import {
|
|
|
37
36
|
import type { PDRuntimeAdapter, OutputLanguage } from '@principles/core/runtime-v2';
|
|
38
37
|
import { resolveWorkspaceDir } from '../resolve-workspace.js';
|
|
39
38
|
import { readOutputLanguageFromWorkspace } from '../config-reader.js';
|
|
40
|
-
import { loadPdConfig
|
|
41
|
-
import {
|
|
39
|
+
import { loadPdConfig } from '../services/pd-config-loader.js';
|
|
40
|
+
import { SPLIT_PIPELINE_TOTAL_TIMEOUT_MS, resolveDiagnosticianCapability } from '@principles/core/runtime-v2';
|
|
42
41
|
import { createHash } from 'node:crypto';
|
|
43
42
|
/** Layer 0 content-hash (design §6.1); injected so diag writers can attach predecessorSummary hashes. */
|
|
44
43
|
const contentHashFn = (input: string): string => createHash('sha256').update(input).digest('hex');
|
|
@@ -208,6 +207,35 @@ export async function handleDiagnoseRun(opts: DiagnoseRunOptions): Promise<void>
|
|
|
208
207
|
return;
|
|
209
208
|
}
|
|
210
209
|
|
|
210
|
+
// PRI-638: this is the CLI's ONLY capability disable check. It reads the
|
|
211
|
+
// canonical authority (internalAgents.agents.diagnostician.enabled) through
|
|
212
|
+
// the same resolver the runtime factory uses, so `pd diagnose` can never
|
|
213
|
+
// mislabel a deliberate Owner kill switch as a missing runtime, a provider
|
|
214
|
+
// failure or a malformed config. No runtime adapter is constructed and no
|
|
215
|
+
// provider is contacted on this path.
|
|
216
|
+
const configLoadResult = loadPdConfig(workspaceDir);
|
|
217
|
+
const capability = resolveDiagnosticianCapability(
|
|
218
|
+
configLoadResult.ok ? configLoadResult.effective : configLoadResult.defaults,
|
|
219
|
+
);
|
|
220
|
+
if (!capability.available) {
|
|
221
|
+
const disabledResult = {
|
|
222
|
+
ok: false,
|
|
223
|
+
status: 'failed',
|
|
224
|
+
reason: capability.reason,
|
|
225
|
+
message: capability.message,
|
|
226
|
+
nextAction: capability.nextAction,
|
|
227
|
+
};
|
|
228
|
+
if (opts.json) {
|
|
229
|
+
console.log(JSON.stringify(disabledResult, null, 2));
|
|
230
|
+
} else {
|
|
231
|
+
console.error(`error: ${capability.message}`);
|
|
232
|
+
console.error(`reason: ${capability.reason}`);
|
|
233
|
+
console.error(`nextAction: ${capability.nextAction}`);
|
|
234
|
+
}
|
|
235
|
+
process.exit(1);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
211
239
|
// Resolve runtime kind. P1 fix (mirrors pain-retry.ts): pd diagnose run
|
|
212
240
|
// must NOT default to test-double. Without --runtime, the split pipeline
|
|
213
241
|
// would validate the test-double's stale DiagnosticianOutputV1-shaped
|
|
@@ -389,11 +417,11 @@ export async function handleDiagnoseRun(opts: DiagnoseRunOptions): Promise<void>
|
|
|
389
417
|
const outputLangResult = readOutputLanguageFromWorkspace(workspaceDir);
|
|
390
418
|
const outputLanguage: OutputLanguage | undefined = outputLangResult.outputLanguage;
|
|
391
419
|
|
|
392
|
-
//
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const pipelineTimeoutMs =
|
|
420
|
+
// PRI-638: implementation selection is gone. The split pipeline is the only
|
|
421
|
+
// Diagnostician implementation in the tree, so it always runs with its
|
|
422
|
+
// documented 3-stage budget; `diagnostician_split_pipeline` no longer
|
|
423
|
+
// selects a runner nor disables capability.
|
|
424
|
+
const pipelineTimeoutMs = SPLIT_PIPELINE_TOTAL_TIMEOUT_MS;
|
|
397
425
|
// BUG-1 (PRI-442): extract effectiveConfig so ADR-0019 LLM rate-limit
|
|
398
426
|
// degradation (isDegradationEnabled in base-peer-runner) can read the
|
|
399
427
|
// diagnostician_llm_degradation feature flag. Without this, the runners
|
|
@@ -410,34 +438,29 @@ export async function handleDiagnoseRun(opts: DiagnoseRunOptions): Promise<void>
|
|
|
410
438
|
}
|
|
411
439
|
const effectiveConfig = configLoadResult.ok ? configLoadResult.effective : configLoadResult.defaults;
|
|
412
440
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
)
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
perStageTimeoutMs,
|
|
437
|
-
});
|
|
438
|
-
} else {
|
|
439
|
-
runner = new DisabledDiagnosticianRunner();
|
|
440
|
-
}
|
|
441
|
+
const resolvedKind = typeof runtimeAdapter.kind === 'function' ? runtimeAdapter.kind() : runtimeKind;
|
|
442
|
+
const perStageTimeoutMs = pipelineTimeoutMs / 3;
|
|
443
|
+
const rootCauseRunner = new DiagRootCauseRunner(
|
|
444
|
+
{ stateManager, runtimeAdapter, eventEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultDiagRootCauseValidator(), contextAssembler, contentHashFn },
|
|
445
|
+
{ owner: 'pd-cli-diagnose', runtimeKind: resolvedKind, outputLanguage, timeoutMs: perStageTimeoutMs, effectiveConfig },
|
|
446
|
+
);
|
|
447
|
+
const distillerRunner = new DiagDistillerRunner(
|
|
448
|
+
{ stateManager, runtimeAdapter, eventEmitter, artifactStore: stateManager.piArtifactStore, validator: new DefaultDiagDistillerValidator(), contentHashFn },
|
|
449
|
+
{ owner: 'pd-cli-diagnose', runtimeKind: resolvedKind, outputLanguage, timeoutMs: perStageTimeoutMs, effectiveConfig },
|
|
450
|
+
);
|
|
451
|
+
const routerRunner = new DiagRouterRunner(
|
|
452
|
+
{ stateManager, runtimeAdapter, eventEmitter, artifactStore: stateManager.piArtifactStore, committer, contentHashFn },
|
|
453
|
+
{ owner: 'pd-cli-diagnose', runtimeKind: resolvedKind, outputLanguage, timeoutMs: perStageTimeoutMs, effectiveConfig },
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
const runner: DiagnosticianRunnerLike = new SplitDiagnosticianRunner({
|
|
457
|
+
rootCauseRunner,
|
|
458
|
+
distillerRunner,
|
|
459
|
+
routerRunner,
|
|
460
|
+
stateManager,
|
|
461
|
+
committer,
|
|
462
|
+
perStageTimeoutMs,
|
|
463
|
+
});
|
|
441
464
|
|
|
442
465
|
if (!opts.json) {
|
|
443
466
|
console.log(`\nRunning diagnostician for task: ${opts.taskId}`);
|