@bramburn/pi-model-council 1.6.3 → 1.6.11
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/CHANGELOG.md +342 -0
- package/CODE_OF_CONDUCT.md +134 -0
- package/CONTRIBUTING.md +128 -0
- package/DISCLAIMER.md +62 -0
- package/LICENSE +21 -0
- package/README.md +460 -0
- package/SECURITY.md +80 -0
- package/SUPPORT.md +72 -0
- package/commandParser.ts +333 -0
- package/councilRunner.ts +499 -0
- package/index.ts +304 -0
- package/markdown.ts +113 -0
- package/openrouterClient.ts +244 -0
- package/package.json +76 -1
- package/prompts.ts +299 -0
- package/retry.ts +92 -0
- package/runnerHelpers.ts +130 -0
- package/schemas.ts +44 -0
- package/searchSelector.ts +313 -0
- package/secondOpinionRunner.ts +203 -0
- package/settings-ui.ts +488 -0
- package/settings.ts +135 -0
- package/structuredOutput.ts +500 -0
- package/types.ts +169 -0
- package/index.js +0 -1
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CouncilDecision,
|
|
3
|
+
CouncilInput,
|
|
4
|
+
CouncilMode,
|
|
5
|
+
ModelOpinion,
|
|
6
|
+
} from "./types.js";
|
|
7
|
+
|
|
8
|
+
// JSON Schema for model opinion response
|
|
9
|
+
export const modelOpinionJsonSchema = {
|
|
10
|
+
type: "object",
|
|
11
|
+
additionalProperties: false,
|
|
12
|
+
required: [
|
|
13
|
+
"stance",
|
|
14
|
+
"recommendedApproach",
|
|
15
|
+
"steps",
|
|
16
|
+
"filesToConsider",
|
|
17
|
+
"risks",
|
|
18
|
+
"verification",
|
|
19
|
+
"confidence"
|
|
20
|
+
],
|
|
21
|
+
properties: {
|
|
22
|
+
stance: { type: "string" },
|
|
23
|
+
recommendedApproach: { type: "string" },
|
|
24
|
+
steps: { type: "array", items: { type: "string" } },
|
|
25
|
+
filesToConsider: {
|
|
26
|
+
type: "array",
|
|
27
|
+
items: {
|
|
28
|
+
type: "object",
|
|
29
|
+
additionalProperties: false,
|
|
30
|
+
required: ["path", "reason", "suggestedAction"],
|
|
31
|
+
properties: {
|
|
32
|
+
path: { type: "string" },
|
|
33
|
+
reason: { type: "string" },
|
|
34
|
+
suggestedAction: { type: "string" }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
risks: { type: "array", items: { type: "string" } },
|
|
39
|
+
verification: { type: "array", items: { type: "string" } },
|
|
40
|
+
confidence: { type: "string", enum: ["low", "medium", "high"] }
|
|
41
|
+
}
|
|
42
|
+
} as const;
|
|
43
|
+
|
|
44
|
+
// JSON Schema for council decision response
|
|
45
|
+
export const councilDecisionJsonSchema = {
|
|
46
|
+
type: "object",
|
|
47
|
+
additionalProperties: false,
|
|
48
|
+
required: [
|
|
49
|
+
"decisionId",
|
|
50
|
+
"mode",
|
|
51
|
+
"confidence",
|
|
52
|
+
"consensus",
|
|
53
|
+
"recommendedPlan",
|
|
54
|
+
"implementationGuidance",
|
|
55
|
+
"modelNotes",
|
|
56
|
+
"handoffPrompt"
|
|
57
|
+
],
|
|
58
|
+
properties: {
|
|
59
|
+
decisionId: { type: "string" },
|
|
60
|
+
mode: { type: "string", enum: ["fix", "ask", "architecture"] },
|
|
61
|
+
confidence: { type: "string", enum: ["low", "medium", "high"] },
|
|
62
|
+
consensus: {
|
|
63
|
+
type: "object",
|
|
64
|
+
additionalProperties: false,
|
|
65
|
+
required: ["agreements", "disagreements", "unknowns"],
|
|
66
|
+
properties: {
|
|
67
|
+
agreements: { type: "array", items: { type: "string" } },
|
|
68
|
+
disagreements: { type: "array", items: { type: "string" } },
|
|
69
|
+
unknowns: { type: "array", items: { type: "string" } }
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
recommendedPlan: {
|
|
73
|
+
type: "object",
|
|
74
|
+
additionalProperties: false,
|
|
75
|
+
required: ["summary", "steps"],
|
|
76
|
+
properties: {
|
|
77
|
+
summary: { type: "string" },
|
|
78
|
+
steps: { type: "array", items: { type: "string" } }
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
implementationGuidance: {
|
|
82
|
+
type: "object",
|
|
83
|
+
additionalProperties: false,
|
|
84
|
+
required: ["filesToEdit", "testsToRun", "guardrails"],
|
|
85
|
+
properties: {
|
|
86
|
+
filesToEdit: {
|
|
87
|
+
type: "array",
|
|
88
|
+
items: {
|
|
89
|
+
type: "object",
|
|
90
|
+
additionalProperties: false,
|
|
91
|
+
required: ["path", "reason", "action"],
|
|
92
|
+
properties: {
|
|
93
|
+
path: { type: "string" },
|
|
94
|
+
reason: { type: "string" },
|
|
95
|
+
action: { type: "string" }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
testsToRun: { type: "array", items: { type: "string" } },
|
|
100
|
+
guardrails: { type: "array", items: { type: "string" } }
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
modelNotes: {
|
|
104
|
+
type: "array",
|
|
105
|
+
items: {
|
|
106
|
+
type: "object",
|
|
107
|
+
additionalProperties: false,
|
|
108
|
+
required: ["model", "stance", "keyRisks"],
|
|
109
|
+
properties: {
|
|
110
|
+
model: { type: "string" },
|
|
111
|
+
stance: { type: "string" },
|
|
112
|
+
keyRisks: { type: "array", items: { type: "string" } }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
handoffPrompt: { type: "string" }
|
|
117
|
+
}
|
|
118
|
+
} as const;
|
|
119
|
+
|
|
120
|
+
type ParseStatus = "valid" | "repaired" | "fallback";
|
|
121
|
+
|
|
122
|
+
interface ValidationResult<T> {
|
|
123
|
+
ok: boolean;
|
|
124
|
+
value?: T;
|
|
125
|
+
warnings: string[];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
interface RepairResult<T> {
|
|
129
|
+
value: T;
|
|
130
|
+
parseStatus: ParseStatus;
|
|
131
|
+
warnings: string[];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Validate a model opinion object against required fields.
|
|
136
|
+
*/
|
|
137
|
+
export function validateModelOpinion(value: unknown): ValidationResult<ModelOpinion> {
|
|
138
|
+
const warnings: string[] = [];
|
|
139
|
+
|
|
140
|
+
if (typeof value !== "object" || value === null) {
|
|
141
|
+
return { ok: false, warnings: ["Model opinion is not an object"] };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const obj = value as Record<string, unknown>;
|
|
145
|
+
|
|
146
|
+
// Check required fields
|
|
147
|
+
const requiredFields: (keyof ModelOpinion)[] = [
|
|
148
|
+
"stance",
|
|
149
|
+
"recommendedApproach",
|
|
150
|
+
"steps",
|
|
151
|
+
"filesToConsider",
|
|
152
|
+
"risks",
|
|
153
|
+
"verification",
|
|
154
|
+
"confidence"
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
for (const field of requiredFields) {
|
|
158
|
+
if (!(field in obj)) {
|
|
159
|
+
warnings.push(`Missing required field: ${field}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Validate confidence
|
|
164
|
+
const validConfidence = ["low", "medium", "high"];
|
|
165
|
+
if (obj.confidence && !validConfidence.includes(obj.confidence as string)) {
|
|
166
|
+
warnings.push(`Invalid confidence value: ${obj.confidence}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Check if all required fields are present and valid
|
|
170
|
+
const hasStance = typeof obj.stance === "string";
|
|
171
|
+
const hasApproach = typeof obj.recommendedApproach === "string";
|
|
172
|
+
const hasSteps = Array.isArray(obj.steps);
|
|
173
|
+
|
|
174
|
+
if (hasStance && hasApproach && hasSteps) {
|
|
175
|
+
return {
|
|
176
|
+
ok: true,
|
|
177
|
+
value: {
|
|
178
|
+
stance: obj.stance as string,
|
|
179
|
+
recommendedApproach: obj.recommendedApproach as string,
|
|
180
|
+
steps: obj.steps as string[],
|
|
181
|
+
filesToConsider: Array.isArray(obj.filesToConsider) ? obj.filesToConsider as ModelOpinion["filesToConsider"] : [],
|
|
182
|
+
risks: Array.isArray(obj.risks) ? obj.risks as string[] : [],
|
|
183
|
+
verification: Array.isArray(obj.verification) ? obj.verification as string[] : [],
|
|
184
|
+
confidence: validConfidence.includes(obj.confidence as string)
|
|
185
|
+
? obj.confidence as ModelOpinion["confidence"]
|
|
186
|
+
: "medium",
|
|
187
|
+
},
|
|
188
|
+
warnings,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return { ok: false, warnings };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Repair a model opinion object, filling in missing fields with defaults.
|
|
197
|
+
*/
|
|
198
|
+
export function repairModelOpinion(value: unknown, rawText: string): RepairResult<ModelOpinion> {
|
|
199
|
+
const warnings: string[] = [];
|
|
200
|
+
|
|
201
|
+
if (typeof value !== "object" || value === null) {
|
|
202
|
+
warnings.push("Model opinion is not an object, creating fallback");
|
|
203
|
+
return {
|
|
204
|
+
value: createFallbackOpinion(rawText),
|
|
205
|
+
parseStatus: "fallback",
|
|
206
|
+
warnings,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const obj = value as Record<string, unknown>;
|
|
211
|
+
|
|
212
|
+
// Repair stance
|
|
213
|
+
let stance = "Unclear stance";
|
|
214
|
+
if (typeof obj.stance === "string" && obj.stance.length > 0) {
|
|
215
|
+
stance = obj.stance;
|
|
216
|
+
} else {
|
|
217
|
+
warnings.push("Missing or invalid stance, using default");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Repair recommendedApproach
|
|
221
|
+
let recommendedApproach = rawText.substring(0, 300);
|
|
222
|
+
if (typeof obj.recommendedApproach === "string" && obj.recommendedApproach.length > 0) {
|
|
223
|
+
recommendedApproach = obj.recommendedApproach;
|
|
224
|
+
} else {
|
|
225
|
+
warnings.push("Missing or invalid recommendedApproach, using raw text");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Repair steps
|
|
229
|
+
let steps: string[] = [];
|
|
230
|
+
if (Array.isArray(obj.steps)) {
|
|
231
|
+
steps = obj.steps.filter((s): s is string => typeof s === "string");
|
|
232
|
+
} else if (typeof obj.steps === "string") {
|
|
233
|
+
steps = [obj.steps];
|
|
234
|
+
warnings.push("steps was a string, converted to array");
|
|
235
|
+
} else {
|
|
236
|
+
warnings.push("Missing or invalid steps, using empty array");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Repair filesToConsider
|
|
240
|
+
let filesToConsider: ModelOpinion["filesToConsider"] = [];
|
|
241
|
+
if (Array.isArray(obj.filesToConsider)) {
|
|
242
|
+
filesToConsider = obj.filesToConsider
|
|
243
|
+
.filter((f): f is Record<string, unknown> => typeof f === "object" && f !== null)
|
|
244
|
+
.filter((f) => typeof f.path === "string")
|
|
245
|
+
.map((f) => ({
|
|
246
|
+
path: f.path as string,
|
|
247
|
+
reason: typeof f.reason === "string" ? f.reason : "",
|
|
248
|
+
suggestedAction: typeof f.suggestedAction === "string" ? f.suggestedAction : "",
|
|
249
|
+
}));
|
|
250
|
+
} else {
|
|
251
|
+
warnings.push("Missing or invalid filesToConsider");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Repair risks
|
|
255
|
+
let risks: string[] = [];
|
|
256
|
+
if (Array.isArray(obj.risks)) {
|
|
257
|
+
risks = obj.risks.filter((r): r is string => typeof r === "string");
|
|
258
|
+
} else if (typeof obj.risks === "string") {
|
|
259
|
+
risks = [obj.risks];
|
|
260
|
+
} else {
|
|
261
|
+
warnings.push("Missing or invalid risks");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Repair verification
|
|
265
|
+
let verification: string[] = [];
|
|
266
|
+
if (Array.isArray(obj.verification)) {
|
|
267
|
+
verification = obj.verification.filter((v): v is string => typeof v === "string");
|
|
268
|
+
} else if (typeof obj.verification === "string") {
|
|
269
|
+
verification = [obj.verification];
|
|
270
|
+
warnings.push("verification was a string, converted to array");
|
|
271
|
+
} else {
|
|
272
|
+
warnings.push("Missing or invalid verification");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Repair confidence
|
|
276
|
+
const validConfidence = ["low", "medium", "high"] as const;
|
|
277
|
+
let confidence: ModelOpinion["confidence"] = "medium";
|
|
278
|
+
if (typeof obj.confidence === "string" && validConfidence.includes(obj.confidence as "low" | "medium" | "high")) {
|
|
279
|
+
confidence = obj.confidence as "low" | "medium" | "high";
|
|
280
|
+
} else {
|
|
281
|
+
warnings.push("Missing or invalid confidence, defaulting to medium");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
value: {
|
|
286
|
+
stance,
|
|
287
|
+
recommendedApproach,
|
|
288
|
+
steps,
|
|
289
|
+
filesToConsider,
|
|
290
|
+
risks,
|
|
291
|
+
verification,
|
|
292
|
+
confidence,
|
|
293
|
+
},
|
|
294
|
+
parseStatus: warnings.length > 0 ? "repaired" : "valid",
|
|
295
|
+
warnings,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Validate a council decision object against required fields.
|
|
301
|
+
*/
|
|
302
|
+
export function validateCouncilDecision(value: unknown): ValidationResult<CouncilDecision> {
|
|
303
|
+
const warnings: string[] = [];
|
|
304
|
+
|
|
305
|
+
if (typeof value !== "object" || value === null) {
|
|
306
|
+
return { ok: false, warnings: ["Council decision is not an object"] };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const obj = value as Record<string, unknown>;
|
|
310
|
+
|
|
311
|
+
// Check required top-level fields
|
|
312
|
+
if (!obj.decisionId) warnings.push("Missing required field: decisionId");
|
|
313
|
+
if (!obj.recommendedPlan) warnings.push("Missing required field: recommendedPlan");
|
|
314
|
+
if (!obj.implementationGuidance) warnings.push("Missing required field: implementationGuidance");
|
|
315
|
+
|
|
316
|
+
// Validate consensus structure if present
|
|
317
|
+
if (obj.consensus && typeof obj.consensus === "object") {
|
|
318
|
+
const consensus = obj.consensus as Record<string, unknown>;
|
|
319
|
+
if (!Array.isArray(consensus.agreements)) warnings.push("consensus.agreements should be an array");
|
|
320
|
+
if (!Array.isArray(consensus.disagreements)) warnings.push("consensus.disagreements should be an array");
|
|
321
|
+
if (!Array.isArray(consensus.unknowns)) warnings.push("consensus.unknowns should be an array");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Check if we have enough to construct a valid decision
|
|
325
|
+
if (obj.decisionId && obj.recommendedPlan && obj.implementationGuidance) {
|
|
326
|
+
return {
|
|
327
|
+
ok: true,
|
|
328
|
+
value: normalizeCouncilDecision(obj, undefined),
|
|
329
|
+
warnings,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return { ok: false, warnings };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Repair a council decision object, filling in missing fields with defaults.
|
|
338
|
+
*/
|
|
339
|
+
export function repairCouncilDecision(value: unknown, input: CouncilInput): RepairResult<CouncilDecision> {
|
|
340
|
+
const warnings: string[] = [];
|
|
341
|
+
|
|
342
|
+
// Handle string input (raw text fallback)
|
|
343
|
+
if (typeof value === "string") {
|
|
344
|
+
warnings.push("Council decision is raw text, creating fallback");
|
|
345
|
+
return {
|
|
346
|
+
value: createFallbackDecisionRepaired(input),
|
|
347
|
+
parseStatus: "fallback",
|
|
348
|
+
warnings,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (typeof value !== "object" || value === null) {
|
|
353
|
+
warnings.push("Council decision is not an object, creating fallback");
|
|
354
|
+
return {
|
|
355
|
+
value: createFallbackDecisionRepaired(input),
|
|
356
|
+
parseStatus: "fallback",
|
|
357
|
+
warnings,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const obj = value as Record<string, unknown>;
|
|
362
|
+
|
|
363
|
+
// Add warnings for missing fields
|
|
364
|
+
if (!obj.decisionId) warnings.push("Missing decisionId, generating new one");
|
|
365
|
+
if (!obj.consensus) warnings.push("Missing consensus, using empty arrays");
|
|
366
|
+
if (!obj.modelNotes) warnings.push("Missing modelNotes");
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
value: normalizeCouncilDecision(obj, input),
|
|
370
|
+
parseStatus: warnings.length > 0 ? "repaired" : "valid",
|
|
371
|
+
warnings,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function createFallbackOpinion(rawText: string): ModelOpinion {
|
|
376
|
+
return {
|
|
377
|
+
stance: "Unstructured response",
|
|
378
|
+
recommendedApproach: rawText.substring(0, 300),
|
|
379
|
+
steps: [],
|
|
380
|
+
filesToConsider: [],
|
|
381
|
+
risks: [],
|
|
382
|
+
verification: [],
|
|
383
|
+
confidence: "medium",
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function createFallbackDecisionRepaired(input: CouncilInput): CouncilDecision {
|
|
388
|
+
return {
|
|
389
|
+
decisionId: `council-${Date.now()}`,
|
|
390
|
+
mode: input.mode,
|
|
391
|
+
confidence: "medium",
|
|
392
|
+
consensus: {
|
|
393
|
+
agreements: [],
|
|
394
|
+
disagreements: [],
|
|
395
|
+
unknowns: ["Council decision could not be parsed"],
|
|
396
|
+
},
|
|
397
|
+
recommendedPlan: {
|
|
398
|
+
summary: "Review the council outputs and implement the safest minimal plan.",
|
|
399
|
+
steps: [
|
|
400
|
+
"Review relevant files.",
|
|
401
|
+
"Implement the smallest safe change.",
|
|
402
|
+
"Run verification.",
|
|
403
|
+
],
|
|
404
|
+
},
|
|
405
|
+
implementationGuidance: {
|
|
406
|
+
filesToEdit: [],
|
|
407
|
+
testsToRun: [],
|
|
408
|
+
guardrails: ["Prefer minimal, reversible changes."],
|
|
409
|
+
},
|
|
410
|
+
modelNotes: [],
|
|
411
|
+
handoffPrompt: "Implement the recommended plan cautiously and verify before reporting success.",
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function normalizeCouncilDecision(
|
|
416
|
+
obj: Record<string, unknown>,
|
|
417
|
+
input?: CouncilInput
|
|
418
|
+
): CouncilDecision {
|
|
419
|
+
const validModes: CouncilMode[] = ["fix", "ask", "architecture"];
|
|
420
|
+
const validConfidence = ["low", "medium", "high"];
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
decisionId: typeof obj.decisionId === "string" ? obj.decisionId : `council-${Date.now()}`,
|
|
424
|
+
mode: validModes.includes(obj.mode as CouncilMode)
|
|
425
|
+
? obj.mode as CouncilMode
|
|
426
|
+
: (input?.mode ?? "ask"),
|
|
427
|
+
confidence: validConfidence.includes(obj.confidence as string)
|
|
428
|
+
? obj.confidence as CouncilDecision["confidence"]
|
|
429
|
+
: "medium",
|
|
430
|
+
consensus: normalizeConsensus(obj.consensus),
|
|
431
|
+
recommendedPlan: normalizeRecommendedPlan(obj.recommendedPlan),
|
|
432
|
+
implementationGuidance: normalizeImplementationGuidance(obj.implementationGuidance),
|
|
433
|
+
modelNotes: normalizeModelNotes(obj.modelNotes),
|
|
434
|
+
handoffPrompt: typeof obj.handoffPrompt === "string"
|
|
435
|
+
? obj.handoffPrompt
|
|
436
|
+
: "Review and implement the recommended plan.",
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function normalizeConsensus(consensus: unknown): CouncilDecision["consensus"] {
|
|
441
|
+
if (typeof consensus !== "object" || consensus === null) {
|
|
442
|
+
return { agreements: [], disagreements: [], unknowns: [] };
|
|
443
|
+
}
|
|
444
|
+
const c = consensus as Record<string, unknown>;
|
|
445
|
+
return {
|
|
446
|
+
agreements: Array.isArray(c.agreements) ? c.agreements.filter((s): s is string => typeof s === "string") : [],
|
|
447
|
+
disagreements: Array.isArray(c.disagreements) ? c.disagreements.filter((s): s is string => typeof s === "string") : [],
|
|
448
|
+
unknowns: Array.isArray(c.unknowns) ? c.unknowns.filter((s): s is string => typeof s === "string") : [],
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function normalizeRecommendedPlan(plan: unknown): CouncilDecision["recommendedPlan"] {
|
|
453
|
+
if (typeof plan !== "object" || plan === null) {
|
|
454
|
+
return {
|
|
455
|
+
summary: "Review the council outputs and implement the safest minimal plan.",
|
|
456
|
+
steps: ["Review relevant files.", "Implement the smallest safe change.", "Run verification."],
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
const p = plan as Record<string, unknown>;
|
|
460
|
+
return {
|
|
461
|
+
summary: typeof p.summary === "string" ? p.summary : "Review the council outputs and implement the safest minimal plan.",
|
|
462
|
+
steps: Array.isArray(p.steps) ? p.steps.filter((s): s is string => typeof s === "string") : [],
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function normalizeImplementationGuidance(guidance: unknown): CouncilDecision["implementationGuidance"] {
|
|
467
|
+
if (typeof guidance !== "object" || guidance === null) {
|
|
468
|
+
return { filesToEdit: [], testsToRun: [], guardrails: ["Prefer minimal, reversible changes."] };
|
|
469
|
+
}
|
|
470
|
+
const g = guidance as Record<string, unknown>;
|
|
471
|
+
|
|
472
|
+
const filesToEdit = Array.isArray(g.filesToEdit)
|
|
473
|
+
? g.filesToEdit
|
|
474
|
+
.filter((f): f is Record<string, unknown> => typeof f === "object" && f !== null)
|
|
475
|
+
.filter((f) => typeof f.path === "string")
|
|
476
|
+
.map((f) => ({
|
|
477
|
+
path: f.path as string,
|
|
478
|
+
reason: typeof f.reason === "string" ? f.reason : "",
|
|
479
|
+
action: typeof f.action === "string" ? f.action : "",
|
|
480
|
+
}))
|
|
481
|
+
: [];
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
filesToEdit,
|
|
485
|
+
testsToRun: Array.isArray(g.testsToRun) ? g.testsToRun.filter((s): s is string => typeof s === "string") : [],
|
|
486
|
+
guardrails: Array.isArray(g.guardrails) ? g.guardrails.filter((s): s is string => typeof s === "string") : ["Prefer minimal, reversible changes."],
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function normalizeModelNotes(notes: unknown): CouncilDecision["modelNotes"] {
|
|
491
|
+
if (!Array.isArray(notes)) return [];
|
|
492
|
+
return notes
|
|
493
|
+
.filter((n): n is Record<string, unknown> => typeof n === "object" && n !== null)
|
|
494
|
+
.filter((n) => typeof n.model === "string")
|
|
495
|
+
.map((n) => ({
|
|
496
|
+
model: n.model as string,
|
|
497
|
+
stance: typeof n.stance === "string" ? n.stance : "",
|
|
498
|
+
keyRisks: Array.isArray(n.keyRisks) ? n.keyRisks.filter((r): r is string => typeof r === "string") : [],
|
|
499
|
+
}));
|
|
500
|
+
}
|
package/types.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
export type CouncilMode = "fix" | "ask" | "architecture";
|
|
2
|
+
|
|
3
|
+
export type SecondOpinionMode = "fix" | "ask" | "architecture" | "general";
|
|
4
|
+
|
|
5
|
+
export type SecondOpinionInput = {
|
|
6
|
+
problem: string;
|
|
7
|
+
mode?: SecondOpinionMode;
|
|
8
|
+
currentUnderstanding?: string;
|
|
9
|
+
relevantFiles?: CouncilRelevantFile[];
|
|
10
|
+
constraints?: string[];
|
|
11
|
+
questions?: string[];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type CouncilRelevantFile = {
|
|
15
|
+
path: string;
|
|
16
|
+
summary: string;
|
|
17
|
+
importantSnippets?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type CouncilInput = {
|
|
21
|
+
mode: CouncilMode;
|
|
22
|
+
problem: string;
|
|
23
|
+
currentUnderstanding?: string;
|
|
24
|
+
relevantFiles?: CouncilRelevantFile[];
|
|
25
|
+
constraints?: string[];
|
|
26
|
+
questionsToCouncil?: string[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type CouncilModelResult = {
|
|
30
|
+
model: string;
|
|
31
|
+
ok: boolean;
|
|
32
|
+
rawText?: string;
|
|
33
|
+
parsed?: ModelOpinion;
|
|
34
|
+
error?: string;
|
|
35
|
+
metadata?: {
|
|
36
|
+
attemptCount?: number;
|
|
37
|
+
durationMs?: number;
|
|
38
|
+
usedStructuredOutput?: boolean;
|
|
39
|
+
parseStatus?: "ok" | "repaired" | "fallback" | "failed";
|
|
40
|
+
warnings?: string[];
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type ModelOpinion = {
|
|
45
|
+
stance: string;
|
|
46
|
+
recommendedApproach: string;
|
|
47
|
+
steps: string[];
|
|
48
|
+
filesToConsider: Array<{
|
|
49
|
+
path: string;
|
|
50
|
+
reason: string;
|
|
51
|
+
suggestedAction: string;
|
|
52
|
+
}>;
|
|
53
|
+
risks: string[];
|
|
54
|
+
verification: string[];
|
|
55
|
+
confidence: "low" | "medium" | "high";
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type CouncilDecision = {
|
|
59
|
+
decisionId: string;
|
|
60
|
+
mode: CouncilMode;
|
|
61
|
+
confidence: "low" | "medium" | "high";
|
|
62
|
+
consensus: {
|
|
63
|
+
agreements: string[];
|
|
64
|
+
disagreements: string[];
|
|
65
|
+
unknowns: string[];
|
|
66
|
+
};
|
|
67
|
+
recommendedPlan: {
|
|
68
|
+
summary: string;
|
|
69
|
+
steps: string[];
|
|
70
|
+
};
|
|
71
|
+
implementationGuidance: {
|
|
72
|
+
filesToEdit: Array<{
|
|
73
|
+
path: string;
|
|
74
|
+
reason: string;
|
|
75
|
+
action: string;
|
|
76
|
+
}>;
|
|
77
|
+
testsToRun: string[];
|
|
78
|
+
guardrails: string[];
|
|
79
|
+
};
|
|
80
|
+
modelNotes: Array<{
|
|
81
|
+
model: string;
|
|
82
|
+
stance: string;
|
|
83
|
+
keyRisks: string[];
|
|
84
|
+
}>;
|
|
85
|
+
handoffPrompt: string;
|
|
86
|
+
metadata?: {
|
|
87
|
+
degraded?: boolean;
|
|
88
|
+
fallbackUsed?: boolean;
|
|
89
|
+
warnings?: string[];
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// --- Settings types ---
|
|
94
|
+
|
|
95
|
+
export interface OpenRouterModel {
|
|
96
|
+
id: string;
|
|
97
|
+
name: string;
|
|
98
|
+
/** True if the model supports extended thinking / reasoning. Optional — only
|
|
99
|
+
* populated when sourced from pi's model registry, since OpenRouter's REST
|
|
100
|
+
* /models endpoint doesn't expose it. */
|
|
101
|
+
reasoning?: boolean;
|
|
102
|
+
/** Context window size in tokens, if known. */
|
|
103
|
+
contextWindow?: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface CouncilSettings {
|
|
107
|
+
version: 1;
|
|
108
|
+
openRouter: {
|
|
109
|
+
/** May be empty when the API key is sourced from pi's auth storage
|
|
110
|
+
* (`ctx.modelRegistry.getApiKeyForProvider("openrouter")`). */
|
|
111
|
+
apiKey: string;
|
|
112
|
+
models: {
|
|
113
|
+
model1: string;
|
|
114
|
+
model2: string;
|
|
115
|
+
model3: string;
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
opinion: {
|
|
119
|
+
provider: string;
|
|
120
|
+
modelId: string;
|
|
121
|
+
};
|
|
122
|
+
/** Optional override for the synthesis model. When omitted, the council
|
|
123
|
+
* runner falls back to `openRouter.models.model1`. */
|
|
124
|
+
synthesis?: {
|
|
125
|
+
modelId: string;
|
|
126
|
+
};
|
|
127
|
+
options: {
|
|
128
|
+
useStructuredOutput: boolean;
|
|
129
|
+
modelTimeoutMs: number;
|
|
130
|
+
synthesisTimeoutMs: number;
|
|
131
|
+
retryAttempts: number;
|
|
132
|
+
retryDelayMs: number;
|
|
133
|
+
};
|
|
134
|
+
lastUpdated: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface ValidationResult {
|
|
138
|
+
valid: boolean;
|
|
139
|
+
errors: string[];
|
|
140
|
+
warnings?: string[];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface PingResult {
|
|
144
|
+
ok: boolean;
|
|
145
|
+
error?: string;
|
|
146
|
+
quota?: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface ModelOption {
|
|
150
|
+
provider: string;
|
|
151
|
+
models: Array<{
|
|
152
|
+
id: string;
|
|
153
|
+
name: string;
|
|
154
|
+
}>;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export class CouncilSetupError extends Error {
|
|
158
|
+
constructor(message: string) {
|
|
159
|
+
super(message);
|
|
160
|
+
this.name = "CouncilSetupError";
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export class OpinionSetupError extends Error {
|
|
165
|
+
constructor(message: string) {
|
|
166
|
+
super(message);
|
|
167
|
+
this.name = "OpinionSetupError";
|
|
168
|
+
}
|
|
169
|
+
}
|
package/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
module.exports = {};
|