@kjerneverk/riotplan-ai 1.0.0-dev.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/index.d.ts +731 -0
- package/dist/index.js +1963 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1963 @@
|
|
|
1
|
+
import { ToolRegistry, ConversationManager, AgentLoop } from "@kjerneverk/agentic";
|
|
2
|
+
import { createRequest } from "@kjerneverk/execution";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { readdir, stat, readFile } from "node:fs/promises";
|
|
5
|
+
import { createSqliteProvider } from "@kjerneverk/riotplan-format";
|
|
6
|
+
const DEFAULT_TOKEN_BUDGET = {
|
|
7
|
+
maxTokens: 4e4,
|
|
8
|
+
// Conservative default for most models
|
|
9
|
+
evidenceFullThreshold: 2048,
|
|
10
|
+
// 2KB
|
|
11
|
+
evidenceSummaryThreshold: 20480,
|
|
12
|
+
// 20KB
|
|
13
|
+
historyFullCount: 15,
|
|
14
|
+
historyAbbreviatedCount: 5
|
|
15
|
+
};
|
|
16
|
+
function estimateTokens(text) {
|
|
17
|
+
return Math.ceil(text.length / 4);
|
|
18
|
+
}
|
|
19
|
+
function truncateToLines(text, lineCount) {
|
|
20
|
+
const lines = text.split("\n");
|
|
21
|
+
if (lines.length <= lineCount) {
|
|
22
|
+
return text;
|
|
23
|
+
}
|
|
24
|
+
return lines.slice(0, lineCount).join("\n") + "\n\n... [truncated, see full file]";
|
|
25
|
+
}
|
|
26
|
+
function calculateTiering(constraints, selectedApproach, evidence, historyEventCount, budget = DEFAULT_TOKEN_BUDGET) {
|
|
27
|
+
const decision = {
|
|
28
|
+
totalEstimatedTokens: 0,
|
|
29
|
+
budgetExceeded: false,
|
|
30
|
+
evidenceTiered: {
|
|
31
|
+
full: [],
|
|
32
|
+
summarized: [],
|
|
33
|
+
listOnly: []
|
|
34
|
+
},
|
|
35
|
+
historyAbbreviated: false,
|
|
36
|
+
warnings: []
|
|
37
|
+
};
|
|
38
|
+
let baseTokens = 0;
|
|
39
|
+
for (const constraint of constraints) {
|
|
40
|
+
baseTokens += estimateTokens(constraint);
|
|
41
|
+
}
|
|
42
|
+
if (selectedApproach) {
|
|
43
|
+
baseTokens += estimateTokens(selectedApproach.name);
|
|
44
|
+
baseTokens += estimateTokens(selectedApproach.description);
|
|
45
|
+
baseTokens += estimateTokens(selectedApproach.reasoning);
|
|
46
|
+
}
|
|
47
|
+
baseTokens += 2e3;
|
|
48
|
+
decision.totalEstimatedTokens = baseTokens;
|
|
49
|
+
let evidenceTokens = 0;
|
|
50
|
+
for (const ev of evidence) {
|
|
51
|
+
const fullTokens = estimateTokens(ev.content);
|
|
52
|
+
if (ev.size < budget.evidenceFullThreshold) {
|
|
53
|
+
decision.evidenceTiered.full.push(ev.name);
|
|
54
|
+
evidenceTokens += fullTokens;
|
|
55
|
+
} else if (ev.size < budget.evidenceSummaryThreshold) {
|
|
56
|
+
const summarized = truncateToLines(ev.content, 50);
|
|
57
|
+
decision.evidenceTiered.summarized.push(ev.name);
|
|
58
|
+
evidenceTokens += estimateTokens(summarized);
|
|
59
|
+
} else {
|
|
60
|
+
const listOnly = truncateToLines(ev.content, 5);
|
|
61
|
+
decision.evidenceTiered.listOnly.push(ev.name);
|
|
62
|
+
evidenceTokens += estimateTokens(listOnly);
|
|
63
|
+
decision.warnings.push(`Evidence file "${ev.name}" is very large (${Math.round(ev.size / 1024)}KB), showing first 5 lines only`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
decision.totalEstimatedTokens += evidenceTokens;
|
|
67
|
+
const avgEventTokens = 50;
|
|
68
|
+
const historyTokens = historyEventCount * avgEventTokens;
|
|
69
|
+
decision.totalEstimatedTokens += historyTokens;
|
|
70
|
+
if (decision.totalEstimatedTokens > budget.maxTokens) {
|
|
71
|
+
decision.budgetExceeded = true;
|
|
72
|
+
if (historyEventCount > budget.historyAbbreviatedCount) {
|
|
73
|
+
decision.historyAbbreviated = true;
|
|
74
|
+
const savedTokens = (historyEventCount - budget.historyAbbreviatedCount) * avgEventTokens;
|
|
75
|
+
decision.totalEstimatedTokens -= savedTokens;
|
|
76
|
+
decision.warnings.push(`History abbreviated to ${budget.historyAbbreviatedCount} most recent events (was ${historyEventCount})`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (decision.evidenceTiered.summarized.length > 0) {
|
|
80
|
+
decision.warnings.push(`${decision.evidenceTiered.summarized.length} evidence file(s) summarized to manage token budget`);
|
|
81
|
+
}
|
|
82
|
+
return decision;
|
|
83
|
+
}
|
|
84
|
+
function applyEvidenceTiering(evidence, decision) {
|
|
85
|
+
return evidence.map((ev) => {
|
|
86
|
+
if (decision.evidenceTiered.summarized.includes(ev.name)) {
|
|
87
|
+
return {
|
|
88
|
+
...ev,
|
|
89
|
+
content: truncateToLines(ev.content, 50),
|
|
90
|
+
tiered: "summarized"
|
|
91
|
+
};
|
|
92
|
+
} else if (decision.evidenceTiered.listOnly.includes(ev.name)) {
|
|
93
|
+
return {
|
|
94
|
+
...ev,
|
|
95
|
+
content: truncateToLines(ev.content, 5),
|
|
96
|
+
tiered: "listOnly"
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return ev;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const PLAN_GENERATION_RESPONSE_SCHEMA = {
|
|
103
|
+
type: "object",
|
|
104
|
+
properties: {
|
|
105
|
+
analysis: {
|
|
106
|
+
type: "object",
|
|
107
|
+
properties: {
|
|
108
|
+
constraintAnalysis: {
|
|
109
|
+
type: "array",
|
|
110
|
+
items: {
|
|
111
|
+
type: "object",
|
|
112
|
+
properties: {
|
|
113
|
+
constraint: { type: "string" },
|
|
114
|
+
understanding: { type: "string" },
|
|
115
|
+
plannedApproach: { type: "string" }
|
|
116
|
+
},
|
|
117
|
+
required: ["constraint", "understanding", "plannedApproach"]
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
evidenceAnalysis: {
|
|
121
|
+
type: "array",
|
|
122
|
+
items: {
|
|
123
|
+
type: "object",
|
|
124
|
+
properties: {
|
|
125
|
+
evidenceFile: { type: "string" },
|
|
126
|
+
keyFindings: { type: "string" },
|
|
127
|
+
impactOnPlan: { type: "string" }
|
|
128
|
+
},
|
|
129
|
+
required: ["evidenceFile", "keyFindings", "impactOnPlan"]
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
approachAnalysis: {
|
|
133
|
+
type: "object",
|
|
134
|
+
properties: {
|
|
135
|
+
selectedApproach: { type: "string" },
|
|
136
|
+
commitments: { type: "string" },
|
|
137
|
+
implementationStrategy: { type: "string" }
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
risks: {
|
|
141
|
+
type: "array",
|
|
142
|
+
items: { type: "string" }
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
required: ["constraintAnalysis"]
|
|
146
|
+
},
|
|
147
|
+
summary: { type: "string" },
|
|
148
|
+
approach: { type: "string" },
|
|
149
|
+
successCriteria: { type: "string" },
|
|
150
|
+
steps: {
|
|
151
|
+
type: "array",
|
|
152
|
+
items: {
|
|
153
|
+
type: "object",
|
|
154
|
+
properties: {
|
|
155
|
+
number: { type: "number" },
|
|
156
|
+
title: { type: "string" },
|
|
157
|
+
objective: { type: "string" },
|
|
158
|
+
background: { type: "string" },
|
|
159
|
+
tasks: {
|
|
160
|
+
type: "array",
|
|
161
|
+
items: {
|
|
162
|
+
type: "object",
|
|
163
|
+
properties: {
|
|
164
|
+
id: { type: "string" },
|
|
165
|
+
description: { type: "string" }
|
|
166
|
+
},
|
|
167
|
+
required: ["id", "description"]
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
acceptanceCriteria: {
|
|
171
|
+
type: "array",
|
|
172
|
+
items: { type: "string" }
|
|
173
|
+
},
|
|
174
|
+
testing: { type: "string" },
|
|
175
|
+
filesChanged: {
|
|
176
|
+
type: "array",
|
|
177
|
+
items: { type: "string" }
|
|
178
|
+
},
|
|
179
|
+
notes: { type: "string" },
|
|
180
|
+
provenance: {
|
|
181
|
+
type: "object",
|
|
182
|
+
properties: {
|
|
183
|
+
constraintsAddressed: {
|
|
184
|
+
type: "array",
|
|
185
|
+
items: { type: "string" }
|
|
186
|
+
},
|
|
187
|
+
evidenceUsed: {
|
|
188
|
+
type: "array",
|
|
189
|
+
items: { type: "string" }
|
|
190
|
+
},
|
|
191
|
+
rationale: { type: "string" }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
required: ["number", "title", "objective", "background", "tasks", "acceptanceCriteria", "testing", "filesChanged", "notes"]
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
required: ["analysis", "summary", "approach", "successCriteria", "steps"]
|
|
200
|
+
};
|
|
201
|
+
function getPlanGenerationSystemPrompt() {
|
|
202
|
+
return SYSTEM_PROMPT;
|
|
203
|
+
}
|
|
204
|
+
async function generatePlan(context, provider, options = {}) {
|
|
205
|
+
let tieringDecision;
|
|
206
|
+
let tieredContext = context;
|
|
207
|
+
if (context.constraints || context.evidence || context.historyContext) {
|
|
208
|
+
const budget = {
|
|
209
|
+
...DEFAULT_TOKEN_BUDGET,
|
|
210
|
+
...context.tokenBudget
|
|
211
|
+
};
|
|
212
|
+
tieringDecision = calculateTiering(
|
|
213
|
+
context.constraints || [],
|
|
214
|
+
context.selectedApproach || null,
|
|
215
|
+
context.evidence || [],
|
|
216
|
+
context.historyContext?.recentEvents.length || 0,
|
|
217
|
+
budget
|
|
218
|
+
);
|
|
219
|
+
if (context.evidence && context.evidence.length > 0) {
|
|
220
|
+
const tieredEvidence = applyEvidenceTiering(context.evidence, tieringDecision);
|
|
221
|
+
tieredContext = {
|
|
222
|
+
...context,
|
|
223
|
+
evidence: tieredEvidence
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (tieringDecision.historyAbbreviated && context.historyContext) {
|
|
227
|
+
const abbreviatedCount = budget.historyAbbreviatedCount || 5;
|
|
228
|
+
tieredContext = {
|
|
229
|
+
...tieredContext,
|
|
230
|
+
historyContext: {
|
|
231
|
+
...context.historyContext,
|
|
232
|
+
recentEvents: context.historyContext.recentEvents.slice(0, abbreviatedCount)
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const prompt = buildPlanPrompt(tieredContext);
|
|
238
|
+
const request = {
|
|
239
|
+
model: options.model || "claude-sonnet-4-5",
|
|
240
|
+
messages: [
|
|
241
|
+
{
|
|
242
|
+
role: "system",
|
|
243
|
+
content: SYSTEM_PROMPT
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
role: "user",
|
|
247
|
+
content: prompt
|
|
248
|
+
}
|
|
249
|
+
],
|
|
250
|
+
responseFormat: {
|
|
251
|
+
type: "json_schema",
|
|
252
|
+
json_schema: {
|
|
253
|
+
name: "plan_generation",
|
|
254
|
+
description: "Generate a detailed execution plan",
|
|
255
|
+
schema: PLAN_GENERATION_RESPONSE_SCHEMA
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
addMessage: function(message) {
|
|
259
|
+
this.messages.push(message);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
let responseContent = "";
|
|
263
|
+
const { onProgress } = options;
|
|
264
|
+
onProgress?.({ type: "started", message: "Generating plan..." });
|
|
265
|
+
if (provider.executeStream) {
|
|
266
|
+
let lastProgressUpdate = Date.now();
|
|
267
|
+
for await (const chunk of provider.executeStream(request, options)) {
|
|
268
|
+
if (chunk.type === "text" && chunk.text) {
|
|
269
|
+
responseContent += chunk.text;
|
|
270
|
+
const now = Date.now();
|
|
271
|
+
if (now - lastProgressUpdate > 500) {
|
|
272
|
+
onProgress?.({
|
|
273
|
+
type: "streaming",
|
|
274
|
+
charsReceived: responseContent.length,
|
|
275
|
+
message: `Generating... (${responseContent.length} chars)`
|
|
276
|
+
});
|
|
277
|
+
lastProgressUpdate = now;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
const response = await provider.execute(request, options);
|
|
283
|
+
responseContent = response.content;
|
|
284
|
+
}
|
|
285
|
+
onProgress?.({ type: "parsing", message: "Parsing generated plan..." });
|
|
286
|
+
const plan = parsePlanResponse(responseContent, context.stepCount || 5);
|
|
287
|
+
onProgress?.({ type: "complete", message: "Plan generation complete" });
|
|
288
|
+
return {
|
|
289
|
+
plan,
|
|
290
|
+
tiering: tieringDecision
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
const SYSTEM_PROMPT = `You are an expert project planner generating an execution plan that MUST be grounded in the provided artifacts.
|
|
294
|
+
|
|
295
|
+
## Dual Audience
|
|
296
|
+
|
|
297
|
+
Your plan serves two audiences: (1) a human reviewer who will read the plan and judge your design choices BEFORE implementation, and (2) an LLM that will execute each step. Both need you to SHOW your thinking with code samples, not just describe it in prose.
|
|
298
|
+
|
|
299
|
+
## Core Principles
|
|
300
|
+
|
|
301
|
+
1. **Constraints are non-negotiable**: Every constraint from IDEA.md MUST be addressed by at least one step. Do not ignore or work around constraints.
|
|
302
|
+
|
|
303
|
+
2. **Evidence informs design**: Evidence files contain research, analysis, code snippets, and findings gathered during idea exploration. Use this evidence to inform step design - reference specific findings, not general knowledge.
|
|
304
|
+
|
|
305
|
+
3. **Selected approach is the strategy**: If a selected approach is provided from SHAPING.md, your plan MUST implement that approach, not an alternative. The approach was chosen deliberately with tradeoffs considered.
|
|
306
|
+
|
|
307
|
+
4. **History shows evolution**: The history context shows how thinking evolved during exploration. Respect decisions that were made and questions that were answered.
|
|
308
|
+
|
|
309
|
+
5. **Be concrete, not generic**: Reference specific files, functions, code patterns from the evidence. Don't generate generic steps that could apply to any project.
|
|
310
|
+
|
|
311
|
+
6. **Use portable file paths**: In \`filesChanged\`, always use repository/project-root-relative paths (e.g., \`src/module/file.ts\`), never absolute machine-specific paths.
|
|
312
|
+
|
|
313
|
+
7. **Show, don't tell**: Every task that introduces an interface, schema, config, or significant function MUST include a code sketch in its description — key method signatures, table schemas, type definitions. Not the full implementation, just enough for a human to evaluate the design and say "yes" or "change X."
|
|
314
|
+
|
|
315
|
+
## Output Requirements
|
|
316
|
+
|
|
317
|
+
- Generate steps that are directly traceable to the artifacts
|
|
318
|
+
- Each step should cite which constraints it addresses
|
|
319
|
+
- Reference evidence when it informs a step's design
|
|
320
|
+
- Be specific about files, functions, and code changes
|
|
321
|
+
- Include sample code in task descriptions (markdown code blocks with language tags)
|
|
322
|
+
|
|
323
|
+
CRITICAL: You must output ONLY valid JSON. Do not include any text before or after the JSON object. Ensure all strings are properly escaped. Code blocks in task descriptions should use escaped backticks.`;
|
|
324
|
+
function buildPlanPrompt(context) {
|
|
325
|
+
const hasStructuredArtifacts = context.constraints && context.constraints.length > 0 || context.evidence && context.evidence.length > 0 || context.selectedApproach || context.historyContext && context.historyContext.recentEvents.length > 0;
|
|
326
|
+
if (hasStructuredArtifacts) {
|
|
327
|
+
return buildArtifactAwarePrompt(context);
|
|
328
|
+
} else {
|
|
329
|
+
return buildLegacyPrompt(context);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function buildArtifactAwarePrompt(context) {
|
|
333
|
+
const sections = [];
|
|
334
|
+
sections.push(`== PLAN NAME ==
|
|
335
|
+
${context.planName}`);
|
|
336
|
+
let coreConcept = context.description;
|
|
337
|
+
const contextMarker = coreConcept.indexOf("\n\n--- IDEA CONTEXT ---");
|
|
338
|
+
if (contextMarker !== -1) {
|
|
339
|
+
coreConcept = coreConcept.substring(0, contextMarker).trim();
|
|
340
|
+
}
|
|
341
|
+
sections.push(`== CORE CONCEPT ==
|
|
342
|
+
${coreConcept}`);
|
|
343
|
+
if (context.constraints && context.constraints.length > 0) {
|
|
344
|
+
const constraintsList = context.constraints.map((c, i) => `${i + 1}. ${c}`).join("\n");
|
|
345
|
+
sections.push(`== CONSTRAINTS (MUST HONOR ALL) ==
|
|
346
|
+
The following constraints are NON-NEGOTIABLE. Every constraint must be addressed by at least one step in the plan.
|
|
347
|
+
|
|
348
|
+
${constraintsList}`);
|
|
349
|
+
}
|
|
350
|
+
if (context.catalystContent?.constraints) {
|
|
351
|
+
sections.push(`== CATALYST CONSTRAINTS ==
|
|
352
|
+
The following constraints come from applied catalysts and must also be honored:
|
|
353
|
+
|
|
354
|
+
${context.catalystContent.constraints}`);
|
|
355
|
+
}
|
|
356
|
+
if (context.selectedApproach) {
|
|
357
|
+
sections.push(`== SELECTED APPROACH ==
|
|
358
|
+
This approach was selected during shaping. Your plan MUST implement this approach, not an alternative.
|
|
359
|
+
|
|
360
|
+
**Name**: ${context.selectedApproach.name}
|
|
361
|
+
|
|
362
|
+
**Description**: ${context.selectedApproach.description}
|
|
363
|
+
|
|
364
|
+
**Reasoning**: ${context.selectedApproach.reasoning}`);
|
|
365
|
+
}
|
|
366
|
+
if (context.evidence && context.evidence.length > 0) {
|
|
367
|
+
const evidenceContent = context.evidence.map((e) => {
|
|
368
|
+
const content = e.content.trim();
|
|
369
|
+
const tiered = e.tiered;
|
|
370
|
+
let header = `### ${e.name}`;
|
|
371
|
+
if (tiered === "summarized") {
|
|
372
|
+
header += " (summarized for token budget)";
|
|
373
|
+
} else if (tiered === "listOnly") {
|
|
374
|
+
header += " (preview only - full content available)";
|
|
375
|
+
}
|
|
376
|
+
return `${header}
|
|
377
|
+
${content}`;
|
|
378
|
+
}).join("\n\n");
|
|
379
|
+
sections.push(`== EVIDENCE ==
|
|
380
|
+
The following evidence was gathered during idea exploration. Use these findings to inform step design.
|
|
381
|
+
|
|
382
|
+
${evidenceContent}`);
|
|
383
|
+
}
|
|
384
|
+
if (context.historyContext && context.historyContext.recentEvents.length > 0) {
|
|
385
|
+
const historyList = context.historyContext.recentEvents.map((e) => `- [${e.type}] ${e.summary}`).join("\n");
|
|
386
|
+
sections.push(`== HISTORY CONTEXT ==
|
|
387
|
+
Recent events from idea exploration (${context.historyContext.totalEvents} total events):
|
|
388
|
+
|
|
389
|
+
${historyList}`);
|
|
390
|
+
}
|
|
391
|
+
if (context.questions && context.questions.length > 0) {
|
|
392
|
+
const questionsList = context.questions.map((q, i) => `${i + 1}. ${q}`).join("\n");
|
|
393
|
+
sections.push(`== OPEN QUESTIONS ==
|
|
394
|
+
These questions were raised during exploration. Consider them when designing steps.
|
|
395
|
+
|
|
396
|
+
${questionsList}`);
|
|
397
|
+
}
|
|
398
|
+
if (context.catalystContent?.domainKnowledge) {
|
|
399
|
+
sections.push(`== CATALYST DOMAIN KNOWLEDGE ==
|
|
400
|
+
The following domain knowledge comes from applied catalysts and provides context about the domain, organization, or technology:
|
|
401
|
+
|
|
402
|
+
${context.catalystContent.domainKnowledge}`);
|
|
403
|
+
}
|
|
404
|
+
if (context.catalystContent?.outputTemplates) {
|
|
405
|
+
sections.push(`== CATALYST OUTPUT TEMPLATES ==
|
|
406
|
+
The following output templates define expected deliverables that the plan should produce:
|
|
407
|
+
|
|
408
|
+
${context.catalystContent.outputTemplates}`);
|
|
409
|
+
}
|
|
410
|
+
if (context.catalystContent?.processGuidance) {
|
|
411
|
+
sections.push(`== CATALYST PROCESS GUIDANCE ==
|
|
412
|
+
The following process guidance from applied catalysts should inform how the plan is structured:
|
|
413
|
+
|
|
414
|
+
${context.catalystContent.processGuidance}`);
|
|
415
|
+
}
|
|
416
|
+
if (context.catalystContent?.questions) {
|
|
417
|
+
sections.push(`== CATALYST QUESTIONS ==
|
|
418
|
+
The following questions from applied catalysts should be considered during planning:
|
|
419
|
+
|
|
420
|
+
${context.catalystContent.questions}`);
|
|
421
|
+
}
|
|
422
|
+
if (context.catalystContent?.validationRules) {
|
|
423
|
+
sections.push(`== CATALYST VALIDATION RULES ==
|
|
424
|
+
The following validation rules from applied catalysts define post-creation checks:
|
|
425
|
+
|
|
426
|
+
${context.catalystContent.validationRules}`);
|
|
427
|
+
}
|
|
428
|
+
if (context.catalystContent?.appliedCatalysts && context.catalystContent.appliedCatalysts.length > 0) {
|
|
429
|
+
const catalystList = context.catalystContent.appliedCatalysts.map((id, i) => `${i + 1}. ${id}`).join("\n");
|
|
430
|
+
sections.push(`== APPLIED CATALYSTS ==
|
|
431
|
+
This plan was generated with the following catalysts applied (in order):
|
|
432
|
+
|
|
433
|
+
${catalystList}
|
|
434
|
+
|
|
435
|
+
The catalyst content above has influenced the constraints, domain knowledge, output expectations, and process guidance for this plan.`);
|
|
436
|
+
}
|
|
437
|
+
sections.push(`== GENERATION INSTRUCTIONS ==
|
|
438
|
+
|
|
439
|
+
**CRITICAL: You must complete the 'analysis' section FIRST before generating steps.**
|
|
440
|
+
|
|
441
|
+
The analysis section demonstrates your understanding of the artifacts and serves as your reasoning gate. Fill it out completely:
|
|
442
|
+
|
|
443
|
+
1. **constraintAnalysis**: For EACH constraint listed above, provide:
|
|
444
|
+
- constraint: The exact constraint text
|
|
445
|
+
- understanding: What this constraint means and why it exists
|
|
446
|
+
- plannedApproach: How you will address this constraint in the plan
|
|
447
|
+
|
|
448
|
+
2. **evidenceAnalysis** (if evidence provided): For each evidence file that informs the plan:
|
|
449
|
+
- evidenceFile: The evidence file name
|
|
450
|
+
- keyFindings: The most important findings from this evidence
|
|
451
|
+
- impactOnPlan: How these findings will shape the implementation steps
|
|
452
|
+
|
|
453
|
+
3. **approachAnalysis** (if selected approach provided):
|
|
454
|
+
- selectedApproach: The approach name
|
|
455
|
+
- commitments: What this approach commits us to doing
|
|
456
|
+
- implementationStrategy: How the steps will implement this approach
|
|
457
|
+
|
|
458
|
+
4. **risks**: Any risks or challenges you foresee in implementing this plan
|
|
459
|
+
|
|
460
|
+
**After completing the analysis, generate ${context.stepCount || 5} implementation steps following these requirements:**
|
|
461
|
+
|
|
462
|
+
1. **Address every constraint**: Each constraint from your analysis MUST be addressed by at least one step
|
|
463
|
+
2. **Implement the selected approach**: Follow the implementation strategy from your analysis
|
|
464
|
+
3. **Reference evidence**: Use the key findings from your evidence analysis to inform step design
|
|
465
|
+
4. **Be concrete**: Reference specific files, functions, and code patterns from the evidence
|
|
466
|
+
5. **Include provenance**: For each step, fill the provenance field with:
|
|
467
|
+
- constraintsAddressed: List which constraints this step addresses
|
|
468
|
+
- evidenceUsed: List which evidence files informed this step
|
|
469
|
+
- rationale: Explain why this step exists and how it connects to the idea
|
|
470
|
+
|
|
471
|
+
For each step, provide:
|
|
472
|
+
- A clear title
|
|
473
|
+
- Objective (what this step accomplishes)
|
|
474
|
+
- Background (context and prerequisites, referencing evidence where relevant)
|
|
475
|
+
- Specific tasks with **sample code** — every task that introduces an interface, schema, config, or significant function MUST include a code sketch in the description using markdown code blocks. Don't write the full implementation, just enough for a human to judge the design: key method signatures, table schemas, type definitions, config formats. This is a plan that a human will review before execution.
|
|
476
|
+
- Acceptance criteria (measurable verification)
|
|
477
|
+
- Testing approach
|
|
478
|
+
- Files that will be changed
|
|
479
|
+
- Notes (including which constraints are addressed)
|
|
480
|
+
|
|
481
|
+
IMPORTANT: Output ONLY the JSON object, with no markdown formatting, no code blocks, no additional text.
|
|
482
|
+
|
|
483
|
+
JSON structure:
|
|
484
|
+
{
|
|
485
|
+
"analysis": {
|
|
486
|
+
"constraintAnalysis": [
|
|
487
|
+
{
|
|
488
|
+
"constraint": "exact constraint text",
|
|
489
|
+
"understanding": "what this constraint means",
|
|
490
|
+
"plannedApproach": "how the plan will address this"
|
|
491
|
+
}
|
|
492
|
+
],
|
|
493
|
+
"evidenceAnalysis": [
|
|
494
|
+
{
|
|
495
|
+
"evidenceFile": "evidence file name",
|
|
496
|
+
"keyFindings": "important findings from this evidence",
|
|
497
|
+
"impactOnPlan": "how these findings shape the plan"
|
|
498
|
+
}
|
|
499
|
+
],
|
|
500
|
+
"approachAnalysis": {
|
|
501
|
+
"selectedApproach": "approach name",
|
|
502
|
+
"commitments": "what this approach commits to",
|
|
503
|
+
"implementationStrategy": "how steps will implement this"
|
|
504
|
+
},
|
|
505
|
+
"risks": ["risk 1", "risk 2"]
|
|
506
|
+
},
|
|
507
|
+
"summary": "executive summary explaining what this plan accomplishes and how it addresses the constraints",
|
|
508
|
+
"approach": "how the plan implements the selected approach (or your chosen approach if none selected)",
|
|
509
|
+
"successCriteria": "how we'll know all constraints are satisfied and the project is complete",
|
|
510
|
+
"steps": [
|
|
511
|
+
{
|
|
512
|
+
"number": 1,
|
|
513
|
+
"title": "Step Title",
|
|
514
|
+
"objective": "what this step accomplishes",
|
|
515
|
+
"background": "context, prerequisites, and relevant evidence",
|
|
516
|
+
"tasks": [
|
|
517
|
+
{"id": "01.1", "description": "specific task"},
|
|
518
|
+
{"id": "01.2", "description": "another task"}
|
|
519
|
+
],
|
|
520
|
+
"acceptanceCriteria": ["criterion 1", "criterion 2"],
|
|
521
|
+
"testing": "how to verify this step",
|
|
522
|
+
"filesChanged": ["file1.ts", "file2.ts"],
|
|
523
|
+
"notes": "additional notes",
|
|
524
|
+
"provenance": {
|
|
525
|
+
"constraintsAddressed": ["constraint 1", "constraint 2"],
|
|
526
|
+
"evidenceUsed": ["evidence file 1", "evidence file 2"],
|
|
527
|
+
"rationale": "why this step exists and how it connects to the idea"
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
]
|
|
531
|
+
}`);
|
|
532
|
+
return sections.join("\n\n");
|
|
533
|
+
}
|
|
534
|
+
function buildLegacyPrompt(context) {
|
|
535
|
+
let prompt = `Create a detailed execution plan for the following project:
|
|
536
|
+
|
|
537
|
+
**Project Name**: ${context.planName}
|
|
538
|
+
|
|
539
|
+
**Description**:
|
|
540
|
+
${context.description}
|
|
541
|
+
`;
|
|
542
|
+
if (context.elaborations && context.elaborations.length > 0) {
|
|
543
|
+
prompt += `
|
|
544
|
+
**Additional Context**:
|
|
545
|
+
`;
|
|
546
|
+
context.elaborations.forEach((elab, i) => {
|
|
547
|
+
prompt += `
|
|
548
|
+
${i + 1}. ${elab}
|
|
549
|
+
`;
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
prompt += `
|
|
553
|
+
**Requirements**:
|
|
554
|
+
- Generate ${context.stepCount || 5} steps
|
|
555
|
+
- Each step should be focused and achievable
|
|
556
|
+
- Provide specific tasks, not generic placeholders
|
|
557
|
+
- Include concrete acceptance criteria
|
|
558
|
+
- Consider what files or components will be affected
|
|
559
|
+
|
|
560
|
+
Please provide:
|
|
561
|
+
1. Executive Summary (2-3 paragraphs explaining what this plan accomplishes)
|
|
562
|
+
2. Approach (how you'll tackle this work, key decisions)
|
|
563
|
+
3. Success Criteria (how we'll know the project is complete)
|
|
564
|
+
4. Detailed steps with:
|
|
565
|
+
- Step title
|
|
566
|
+
- Objective (what this step accomplishes)
|
|
567
|
+
- Background (context needed)
|
|
568
|
+
- Tasks (specific actions to take)
|
|
569
|
+
- Acceptance criteria (how to verify completion)
|
|
570
|
+
- Testing approach
|
|
571
|
+
- Files that will be changed
|
|
572
|
+
- Any notes or considerations
|
|
573
|
+
|
|
574
|
+
IMPORTANT: Output ONLY the JSON object below, with no markdown formatting, no code blocks, no additional text. Ensure all strings use proper JSON escaping for quotes and newlines.
|
|
575
|
+
|
|
576
|
+
JSON structure:
|
|
577
|
+
{
|
|
578
|
+
"summary": "executive summary text",
|
|
579
|
+
"approach": "approach description",
|
|
580
|
+
"successCriteria": "success criteria description",
|
|
581
|
+
"steps": [
|
|
582
|
+
{
|
|
583
|
+
"number": 1,
|
|
584
|
+
"title": "Step Title",
|
|
585
|
+
"objective": "what this step accomplishes",
|
|
586
|
+
"background": "context and prerequisites",
|
|
587
|
+
"tasks": [
|
|
588
|
+
{"id": "01.1", "description": "specific task"},
|
|
589
|
+
{"id": "01.2", "description": "another task"}
|
|
590
|
+
],
|
|
591
|
+
"acceptanceCriteria": ["criterion 1", "criterion 2"],
|
|
592
|
+
"testing": "how to verify this step",
|
|
593
|
+
"filesChanged": ["file1.ts", "file2.ts"],
|
|
594
|
+
"notes": "additional notes"
|
|
595
|
+
}
|
|
596
|
+
]
|
|
597
|
+
}`;
|
|
598
|
+
return prompt;
|
|
599
|
+
}
|
|
600
|
+
function stripMarkdownFences(content) {
|
|
601
|
+
const trimmed = content.trim();
|
|
602
|
+
if (!trimmed.startsWith("```")) return trimmed;
|
|
603
|
+
const firstNewline = trimmed.indexOf("\n");
|
|
604
|
+
if (firstNewline === -1) return trimmed;
|
|
605
|
+
const closingFence = trimmed.lastIndexOf("```");
|
|
606
|
+
if (closingFence <= firstNewline) return trimmed;
|
|
607
|
+
return trimmed.slice(firstNewline + 1, closingFence).trim();
|
|
608
|
+
}
|
|
609
|
+
function extractFirstJsonObject(content) {
|
|
610
|
+
let inString = false;
|
|
611
|
+
let escape = false;
|
|
612
|
+
let depth = 0;
|
|
613
|
+
let start = -1;
|
|
614
|
+
for (let i = 0; i < content.length; i++) {
|
|
615
|
+
const ch = content[i];
|
|
616
|
+
if (escape) {
|
|
617
|
+
escape = false;
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
if (ch === "\\") {
|
|
621
|
+
escape = true;
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
if (ch === '"') {
|
|
625
|
+
inString = !inString;
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
if (inString) continue;
|
|
629
|
+
if (ch === "{") {
|
|
630
|
+
if (depth === 0) start = i;
|
|
631
|
+
depth += 1;
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
if (ch === "}") {
|
|
635
|
+
if (depth === 0) continue;
|
|
636
|
+
depth -= 1;
|
|
637
|
+
if (depth === 0 && start !== -1) {
|
|
638
|
+
return content.slice(start, i + 1);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
function parsePlanResponse(content, _stepCount) {
|
|
645
|
+
try {
|
|
646
|
+
const cleanedContent = stripMarkdownFences(content);
|
|
647
|
+
const jsonCandidate = extractFirstJsonObject(cleanedContent) || cleanedContent;
|
|
648
|
+
const parsed = JSON.parse(jsonCandidate);
|
|
649
|
+
if (!parsed.summary || !parsed.approach || !parsed.successCriteria || !parsed.steps) {
|
|
650
|
+
throw new Error("Invalid plan structure: missing required fields");
|
|
651
|
+
}
|
|
652
|
+
if (parsed.analysis && !parsed.analysis.constraintAnalysis) {
|
|
653
|
+
throw new Error("Invalid analysis structure: missing constraintAnalysis");
|
|
654
|
+
}
|
|
655
|
+
return parsed;
|
|
656
|
+
} catch (error) {
|
|
657
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
658
|
+
throw new Error(
|
|
659
|
+
`Failed to parse plan response: ${message}. Expected a valid JSON object (markdown fences are allowed).`
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
function formatSummary(plan, planName) {
|
|
664
|
+
const title = planName.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
665
|
+
return `# ${title} - Summary
|
|
666
|
+
|
|
667
|
+
## Executive Summary
|
|
668
|
+
|
|
669
|
+
${plan.summary}
|
|
670
|
+
|
|
671
|
+
## Approach
|
|
672
|
+
|
|
673
|
+
${plan.approach}
|
|
674
|
+
|
|
675
|
+
## Success Criteria
|
|
676
|
+
|
|
677
|
+
${plan.successCriteria}
|
|
678
|
+
`;
|
|
679
|
+
}
|
|
680
|
+
function formatStep(step) {
|
|
681
|
+
const num = String(step.number).padStart(2, "0");
|
|
682
|
+
let content = `# Step ${num}: ${step.title}
|
|
683
|
+
|
|
684
|
+
## Objective
|
|
685
|
+
|
|
686
|
+
${step.objective}
|
|
687
|
+
|
|
688
|
+
## Background
|
|
689
|
+
|
|
690
|
+
${step.background}
|
|
691
|
+
|
|
692
|
+
## Tasks
|
|
693
|
+
|
|
694
|
+
`;
|
|
695
|
+
step.tasks.forEach((task) => {
|
|
696
|
+
content += `### ${task.id} ${task.description}
|
|
697
|
+
|
|
698
|
+
`;
|
|
699
|
+
});
|
|
700
|
+
content += `## Acceptance Criteria
|
|
701
|
+
|
|
702
|
+
`;
|
|
703
|
+
step.acceptanceCriteria.forEach((criterion) => {
|
|
704
|
+
content += `- [ ] ${criterion}
|
|
705
|
+
`;
|
|
706
|
+
});
|
|
707
|
+
content += `
|
|
708
|
+
## Testing
|
|
709
|
+
|
|
710
|
+
${step.testing}
|
|
711
|
+
|
|
712
|
+
## Files Changed
|
|
713
|
+
|
|
714
|
+
`;
|
|
715
|
+
step.filesChanged.forEach((file) => {
|
|
716
|
+
content += `- ${file}
|
|
717
|
+
`;
|
|
718
|
+
});
|
|
719
|
+
if (step.notes) {
|
|
720
|
+
content += `
|
|
721
|
+
## Notes
|
|
722
|
+
|
|
723
|
+
${step.notes}
|
|
724
|
+
`;
|
|
725
|
+
}
|
|
726
|
+
return content;
|
|
727
|
+
}
|
|
728
|
+
function createWritePlanTool() {
|
|
729
|
+
let resolvePlan;
|
|
730
|
+
let rejectPlan;
|
|
731
|
+
const planPromise = new Promise((resolve, reject) => {
|
|
732
|
+
resolvePlan = resolve;
|
|
733
|
+
rejectPlan = reject;
|
|
734
|
+
});
|
|
735
|
+
const tool = {
|
|
736
|
+
name: "write_plan",
|
|
737
|
+
description: `Submit the completed execution plan. Call this ONCE when you have finished exploring the codebase and are ready to submit your plan.
|
|
738
|
+
|
|
739
|
+
The plan_json parameter must be a valid JSON string with this structure:
|
|
740
|
+
{
|
|
741
|
+
"analysis": {
|
|
742
|
+
"constraintAnalysis": [{ "constraint": "...", "understanding": "...", "plannedApproach": "..." }],
|
|
743
|
+
"evidenceAnalysis": [{ "evidenceFile": "...", "keyFindings": "...", "impactOnPlan": "..." }],
|
|
744
|
+
"approachAnalysis": { "selectedApproach": "...", "commitments": "...", "implementationStrategy": "..." },
|
|
745
|
+
"risks": ["..."]
|
|
746
|
+
},
|
|
747
|
+
"summary": "Executive summary of what this plan accomplishes",
|
|
748
|
+
"approach": "How the plan implements the selected approach",
|
|
749
|
+
"successCriteria": "How we'll know the project is complete",
|
|
750
|
+
"steps": [{
|
|
751
|
+
"number": 1,
|
|
752
|
+
"title": "Step Title",
|
|
753
|
+
"objective": "What this step accomplishes",
|
|
754
|
+
"background": "Context, prerequisites, and relevant evidence",
|
|
755
|
+
"tasks": [{ "id": "01.1", "description": "Specific task with file paths and code references" }],
|
|
756
|
+
"acceptanceCriteria": ["Measurable criterion 1"],
|
|
757
|
+
"testing": "How to verify this step",
|
|
758
|
+
"filesChanged": ["src/actual/file.ts"],
|
|
759
|
+
"notes": "Additional notes",
|
|
760
|
+
"provenance": {
|
|
761
|
+
"constraintsAddressed": ["constraint text"],
|
|
762
|
+
"evidenceUsed": ["evidence file"],
|
|
763
|
+
"rationale": "Why this step exists"
|
|
764
|
+
}
|
|
765
|
+
}]
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
IMPORTANT: Every step MUST include real file paths in filesChanged, reference actual interfaces/classes by name, and describe concrete code changes based on your codebase exploration.`,
|
|
769
|
+
parameters: {
|
|
770
|
+
type: "object",
|
|
771
|
+
properties: {
|
|
772
|
+
plan_json: {
|
|
773
|
+
type: "string",
|
|
774
|
+
description: "The complete plan as a JSON string matching the GeneratedPlan schema described above."
|
|
775
|
+
}
|
|
776
|
+
},
|
|
777
|
+
required: ["plan_json"]
|
|
778
|
+
},
|
|
779
|
+
category: "planning",
|
|
780
|
+
cost: "cheap",
|
|
781
|
+
execute: async (params) => {
|
|
782
|
+
try {
|
|
783
|
+
const parsed = JSON.parse(params.plan_json);
|
|
784
|
+
if (!parsed.summary || !parsed.approach || !parsed.successCriteria || !parsed.steps) {
|
|
785
|
+
const missing = [];
|
|
786
|
+
if (!parsed.summary) missing.push("summary");
|
|
787
|
+
if (!parsed.approach) missing.push("approach");
|
|
788
|
+
if (!parsed.successCriteria) missing.push("successCriteria");
|
|
789
|
+
if (!parsed.steps) missing.push("steps");
|
|
790
|
+
return `Error: Missing required fields: ${missing.join(", ")}. Please include all required fields and call write_plan again.`;
|
|
791
|
+
}
|
|
792
|
+
if (!Array.isArray(parsed.steps) || parsed.steps.length === 0) {
|
|
793
|
+
return "Error: steps must be a non-empty array. Please generate at least one step and call write_plan again.";
|
|
794
|
+
}
|
|
795
|
+
for (const step of parsed.steps) {
|
|
796
|
+
if (!step.number || !step.title || !step.objective) {
|
|
797
|
+
return `Error: Step ${step.number || "?"} is missing required fields (number, title, objective). Please fix and call write_plan again.`;
|
|
798
|
+
}
|
|
799
|
+
if (!step.tasks || !Array.isArray(step.tasks) || step.tasks.length === 0) {
|
|
800
|
+
return `Error: Step ${step.number} has no tasks. Each step must have at least one concrete task. Please fix and call write_plan again.`;
|
|
801
|
+
}
|
|
802
|
+
if (!step.filesChanged || !Array.isArray(step.filesChanged) || step.filesChanged.length === 0) {
|
|
803
|
+
return `Error: Step ${step.number} has no filesChanged. Each step must list specific files that will be modified. Use your codebase exploration to identify real file paths. Please fix and call write_plan again.`;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
resolvePlan(parsed);
|
|
807
|
+
return `Plan accepted with ${parsed.steps.length} steps. Plan generation complete.`;
|
|
808
|
+
} catch (parseError) {
|
|
809
|
+
if (parseError instanceof SyntaxError) {
|
|
810
|
+
return `Error: Invalid JSON in plan_json. ${parseError.message}. Please fix the JSON and call write_plan again.`;
|
|
811
|
+
}
|
|
812
|
+
return `Error: ${parseError instanceof Error ? parseError.message : "Unknown error"}. Please fix and call write_plan again.`;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
tool._rejectPlan = rejectPlan;
|
|
817
|
+
return { tool, planPromise };
|
|
818
|
+
}
|
|
819
|
+
const DEFAULT_MAX_ITERATIONS = 30;
|
|
820
|
+
const AGENT_SYSTEM_PROMPT = `You are an expert project planner generating a detailed execution plan. You have access to codebase tools that let you explore the actual project.
|
|
821
|
+
|
|
822
|
+
## Dual Audience
|
|
823
|
+
|
|
824
|
+
Your plan serves TWO audiences:
|
|
825
|
+
|
|
826
|
+
1. **A human reviewer** who wants to see your design thinking, judge your approach, and give feedback BEFORE implementation begins. They need sample code, interface sketches, and schema examples — enough to form an opinion and say "yes, that's the right approach" or "no, change X."
|
|
827
|
+
|
|
828
|
+
2. **An LLM executor** who will implement each step. They need precise file paths, interface names, and concrete task descriptions.
|
|
829
|
+
|
|
830
|
+
Both audiences need you to SHOW, not just TELL. Don't say "create a storage interface" — show a draft of the interface with key method signatures. Don't say "design a schema" — show the schema. You don't have to write every line, but include enough sample code that a reader can evaluate your design choices.
|
|
831
|
+
|
|
832
|
+
## Your Workflow
|
|
833
|
+
|
|
834
|
+
1. **EXPLORE the codebase** — Use your tools to understand the project:
|
|
835
|
+
- **START with \`query_index\`** — it returns the full project structure, all packages, and exported symbols in a single call (the index is pre-built and cached, so this is instant). Use queries like "packages", "find file generator", or "export AgentLoop".
|
|
836
|
+
- Then use \`read_file\` to examine specific interfaces, types, and implementations
|
|
837
|
+
- Use \`grep\` to find usage patterns and references
|
|
838
|
+
- Use \`file_outline\` to understand a file's structure without reading every line
|
|
839
|
+
- Use \`list_files\` only if you need directory contents not covered by the index
|
|
840
|
+
|
|
841
|
+
2. **ANALYZE the artifacts** — Study the plan artifacts in the user message:
|
|
842
|
+
- Constraints are NON-NEGOTIABLE — every one must be addressed
|
|
843
|
+
- Evidence contains research and findings — use them to inform design
|
|
844
|
+
- The selected approach defines the strategy — implement it, not an alternative
|
|
845
|
+
- History shows how thinking evolved — respect past decisions
|
|
846
|
+
|
|
847
|
+
3. **DESIGN concrete steps with code examples** — Each step must:
|
|
848
|
+
- Reference REAL file paths you verified with tools
|
|
849
|
+
- Name ACTUAL interfaces, classes, and functions from the codebase
|
|
850
|
+
- Include **sample code** in task descriptions: interface drafts, schema sketches, function signatures, config examples — enough for a human to review the design
|
|
851
|
+
- Include provenance linking back to constraints and evidence
|
|
852
|
+
|
|
853
|
+
4. **SUBMIT the plan** — Call \`write_plan\` with the complete JSON
|
|
854
|
+
|
|
855
|
+
## What "Show, Don't Tell" Means in Practice
|
|
856
|
+
|
|
857
|
+
**BAD task description:**
|
|
858
|
+
"Create a StorageProvider interface with methods for reading and writing plan data"
|
|
859
|
+
|
|
860
|
+
**GOOD task description:**
|
|
861
|
+
"Create a StorageProvider interface in src/storage/types.ts:
|
|
862
|
+
|
|
863
|
+
\\\`\\\`\\\`typescript
|
|
864
|
+
export interface StorageProvider {
|
|
865
|
+
readPlan(id: string): Promise<Plan>;
|
|
866
|
+
writePlan(id: string, plan: Plan): Promise<void>;
|
|
867
|
+
listPlans(): Promise<PlanMetadata[]>;
|
|
868
|
+
readStep(planId: string, stepNumber: number): Promise<PlanStep>;
|
|
869
|
+
writeStep(planId: string, step: PlanStep): Promise<void>;
|
|
870
|
+
}
|
|
871
|
+
\\\`\\\`\\\`
|
|
872
|
+
|
|
873
|
+
This interface mirrors the existing file-based operations in plan/loader.ts but abstracts the storage backend. The key design choice is passing PlanStep objects rather than raw markdown — the provider handles serialization."
|
|
874
|
+
|
|
875
|
+
The good version lets a human reviewer evaluate the interface design, method signatures, and data flow before any code is written.
|
|
876
|
+
|
|
877
|
+
## Critical Rules
|
|
878
|
+
|
|
879
|
+
- EXPLORE FIRST. Do NOT generate the plan without reading relevant code.
|
|
880
|
+
- Every file path in filesChanged MUST be a real file you verified exists.
|
|
881
|
+
- Every interface or type name MUST be verified in the codebase.
|
|
882
|
+
- Include CODE SAMPLES in task descriptions: interface sketches, schema drafts, function signatures, example configs. Use markdown code blocks.
|
|
883
|
+
- Be concrete: "Add a \`generatePlanWithAgent()\` function to \`src/ai/generator.ts\`" not "Add a new function to the generator"
|
|
884
|
+
- If write_plan returns an error, fix the issue and call it again.
|
|
885
|
+
- You MUST call write_plan exactly once with the complete plan when done.`;
|
|
886
|
+
function createAgentProvider(provider, model) {
|
|
887
|
+
return {
|
|
888
|
+
name: provider.name,
|
|
889
|
+
execute: async (request) => {
|
|
890
|
+
const execRequest = createRequest(model);
|
|
891
|
+
for (const msg of request.messages) {
|
|
892
|
+
execRequest.addMessage(msg);
|
|
893
|
+
}
|
|
894
|
+
if (request.tools && request.tools.length > 0) {
|
|
895
|
+
execRequest.tools = request.tools;
|
|
896
|
+
}
|
|
897
|
+
const response = await provider.execute(execRequest);
|
|
898
|
+
return {
|
|
899
|
+
content: response.content,
|
|
900
|
+
model: response.model,
|
|
901
|
+
usage: response.usage ? {
|
|
902
|
+
inputTokens: response.usage.inputTokens,
|
|
903
|
+
outputTokens: response.usage.outputTokens
|
|
904
|
+
} : void 0,
|
|
905
|
+
toolCalls: response.toolCalls
|
|
906
|
+
};
|
|
907
|
+
},
|
|
908
|
+
executeStream: async function* (request) {
|
|
909
|
+
const execRequest = createRequest(model);
|
|
910
|
+
for (const msg of request.messages) {
|
|
911
|
+
execRequest.addMessage(msg);
|
|
912
|
+
}
|
|
913
|
+
if (request.tools && request.tools.length > 0) {
|
|
914
|
+
execRequest.tools = request.tools;
|
|
915
|
+
}
|
|
916
|
+
if (provider.executeStream) {
|
|
917
|
+
for await (const chunk of provider.executeStream(execRequest)) {
|
|
918
|
+
if (chunk.type === "text") {
|
|
919
|
+
yield { type: "text", text: chunk.text };
|
|
920
|
+
} else if (chunk.type === "tool_call_start") {
|
|
921
|
+
yield {
|
|
922
|
+
type: "tool_call_start",
|
|
923
|
+
toolCall: {
|
|
924
|
+
id: chunk.toolCall?.id,
|
|
925
|
+
index: chunk.toolCall?.index,
|
|
926
|
+
name: chunk.toolCall?.name
|
|
927
|
+
}
|
|
928
|
+
};
|
|
929
|
+
} else if (chunk.type === "tool_call_delta") {
|
|
930
|
+
yield {
|
|
931
|
+
type: "tool_call_delta",
|
|
932
|
+
toolCall: {
|
|
933
|
+
id: chunk.toolCall?.id,
|
|
934
|
+
index: chunk.toolCall?.index,
|
|
935
|
+
argumentsDelta: chunk.toolCall?.argumentsDelta
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
} else if (chunk.type === "tool_call_end") {
|
|
939
|
+
yield {
|
|
940
|
+
type: "tool_call_end",
|
|
941
|
+
toolCall: {
|
|
942
|
+
id: chunk.toolCall?.id,
|
|
943
|
+
index: chunk.toolCall?.index
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
} else if (chunk.type === "usage") {
|
|
947
|
+
yield {
|
|
948
|
+
type: "usage",
|
|
949
|
+
usage: chunk.usage ? {
|
|
950
|
+
inputTokens: chunk.usage.inputTokens,
|
|
951
|
+
outputTokens: chunk.usage.outputTokens
|
|
952
|
+
} : void 0
|
|
953
|
+
};
|
|
954
|
+
} else if (chunk.type === "done") {
|
|
955
|
+
yield { type: "done" };
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
} else {
|
|
959
|
+
const response = await provider.execute(execRequest);
|
|
960
|
+
yield { type: "text", text: response.content };
|
|
961
|
+
yield { type: "done" };
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
function buildAgentUserPrompt(context) {
|
|
967
|
+
const sections = [];
|
|
968
|
+
sections.push(`== PLAN NAME ==
|
|
969
|
+
${context.planName}`);
|
|
970
|
+
let coreConcept = context.description;
|
|
971
|
+
const contextMarker = coreConcept.indexOf("\n\n--- IDEA CONTEXT ---");
|
|
972
|
+
if (contextMarker !== -1) {
|
|
973
|
+
coreConcept = coreConcept.substring(0, contextMarker).trim();
|
|
974
|
+
}
|
|
975
|
+
sections.push(`== CORE CONCEPT ==
|
|
976
|
+
${coreConcept}`);
|
|
977
|
+
if (context.constraints && context.constraints.length > 0) {
|
|
978
|
+
const constraintsList = context.constraints.map((c, i) => `${i + 1}. ${c}`).join("\n");
|
|
979
|
+
sections.push(`== CONSTRAINTS (MUST HONOR ALL) ==
|
|
980
|
+
The following constraints are NON-NEGOTIABLE. Every constraint must be addressed by at least one step.
|
|
981
|
+
|
|
982
|
+
${constraintsList}`);
|
|
983
|
+
}
|
|
984
|
+
if (context.catalystContent?.constraints) {
|
|
985
|
+
sections.push(`== CATALYST CONSTRAINTS ==
|
|
986
|
+
${context.catalystContent.constraints}`);
|
|
987
|
+
}
|
|
988
|
+
if (context.selectedApproach) {
|
|
989
|
+
sections.push(`== SELECTED APPROACH ==
|
|
990
|
+
This approach was selected during shaping. Your plan MUST implement this approach.
|
|
991
|
+
|
|
992
|
+
**Name**: ${context.selectedApproach.name}
|
|
993
|
+
**Description**: ${context.selectedApproach.description}
|
|
994
|
+
**Reasoning**: ${context.selectedApproach.reasoning}`);
|
|
995
|
+
}
|
|
996
|
+
if (context.evidence && context.evidence.length > 0) {
|
|
997
|
+
const evidenceContent = context.evidence.map((e) => `### ${e.name}
|
|
998
|
+
${e.content.trim()}`).join("\n\n");
|
|
999
|
+
sections.push(`== EVIDENCE ==
|
|
1000
|
+
Evidence gathered during exploration. Use these findings to inform step design.
|
|
1001
|
+
|
|
1002
|
+
${evidenceContent}`);
|
|
1003
|
+
}
|
|
1004
|
+
if (context.historyContext && context.historyContext.recentEvents.length > 0) {
|
|
1005
|
+
const historyList = context.historyContext.recentEvents.map((e) => `- [${e.type}] ${e.summary}`).join("\n");
|
|
1006
|
+
sections.push(`== HISTORY CONTEXT ==
|
|
1007
|
+
Recent events (${context.historyContext.totalEvents} total):
|
|
1008
|
+
|
|
1009
|
+
${historyList}`);
|
|
1010
|
+
}
|
|
1011
|
+
if (context.questions && context.questions.length > 0) {
|
|
1012
|
+
const questionsList = context.questions.map((q, i) => `${i + 1}. ${q}`).join("\n");
|
|
1013
|
+
sections.push(`== OPEN QUESTIONS ==
|
|
1014
|
+
Consider these when designing steps.
|
|
1015
|
+
|
|
1016
|
+
${questionsList}`);
|
|
1017
|
+
}
|
|
1018
|
+
if (context.catalystContent?.domainKnowledge) {
|
|
1019
|
+
sections.push(`== CATALYST DOMAIN KNOWLEDGE ==
|
|
1020
|
+
${context.catalystContent.domainKnowledge}`);
|
|
1021
|
+
}
|
|
1022
|
+
if (context.catalystContent?.processGuidance) {
|
|
1023
|
+
sections.push(`== CATALYST PROCESS GUIDANCE ==
|
|
1024
|
+
${context.catalystContent.processGuidance}`);
|
|
1025
|
+
}
|
|
1026
|
+
if (context.catalystContent?.outputTemplates) {
|
|
1027
|
+
sections.push(`== CATALYST OUTPUT TEMPLATES ==
|
|
1028
|
+
${context.catalystContent.outputTemplates}`);
|
|
1029
|
+
}
|
|
1030
|
+
if (context.codebaseContext) {
|
|
1031
|
+
sections.push(`== CODEBASE CONTEXT ==
|
|
1032
|
+
${context.codebaseContext}`);
|
|
1033
|
+
}
|
|
1034
|
+
const stepCount = context.stepCount || 5;
|
|
1035
|
+
sections.push(`== YOUR TASK ==
|
|
1036
|
+
|
|
1037
|
+
Generate a ${stepCount}-step execution plan for this project.
|
|
1038
|
+
|
|
1039
|
+
**Step 1: Explore the codebase.** Start by calling \`query_index\` with query "packages" to see the full project structure (this is instant — the index is pre-built). Then use \`query_index\` to find specific files and exports, \`read_file\` to examine key interfaces, and \`grep\` to find usage patterns.
|
|
1040
|
+
|
|
1041
|
+
**Step 2: Call write_plan.** Once you understand the codebase well enough, call \`write_plan\` with the complete plan JSON. Include:
|
|
1042
|
+
- analysis (constraintAnalysis, evidenceAnalysis, approachAnalysis, risks)
|
|
1043
|
+
- summary, approach, successCriteria
|
|
1044
|
+
- ${stepCount} steps, each with concrete tasks referencing real files
|
|
1045
|
+
|
|
1046
|
+
**IMPORTANT — Task descriptions must include sample code.** A human will review this plan before execution. Every task that introduces an interface, schema, config, or significant function should include a code sketch in its description using markdown code blocks. Don't write the full implementation — just enough for a person to judge the design: key method signatures, table schemas, type definitions, config formats. This is the plan's most important quality signal.
|
|
1047
|
+
|
|
1048
|
+
Every step's \`filesChanged\` must list actual files from the codebase. Every task must reference real interfaces and functions. No placeholders.`);
|
|
1049
|
+
return sections.join("\n\n");
|
|
1050
|
+
}
|
|
1051
|
+
async function generatePlanWithAgent(context, provider, codebaseTools, options = {}, projectRoot) {
|
|
1052
|
+
const { onProgress } = options;
|
|
1053
|
+
let tieringDecision;
|
|
1054
|
+
let tieredContext = context;
|
|
1055
|
+
if (context.constraints || context.evidence || context.historyContext) {
|
|
1056
|
+
const budget = {
|
|
1057
|
+
...DEFAULT_TOKEN_BUDGET,
|
|
1058
|
+
...context.tokenBudget
|
|
1059
|
+
};
|
|
1060
|
+
tieringDecision = calculateTiering(
|
|
1061
|
+
context.constraints || [],
|
|
1062
|
+
context.selectedApproach || null,
|
|
1063
|
+
context.evidence || [],
|
|
1064
|
+
context.historyContext?.recentEvents.length || 0,
|
|
1065
|
+
budget
|
|
1066
|
+
);
|
|
1067
|
+
if (context.evidence && context.evidence.length > 0) {
|
|
1068
|
+
const tieredEvidence = applyEvidenceTiering(context.evidence, tieringDecision);
|
|
1069
|
+
tieredContext = { ...context, evidence: tieredEvidence };
|
|
1070
|
+
}
|
|
1071
|
+
if (tieringDecision.historyAbbreviated && context.historyContext) {
|
|
1072
|
+
const abbreviatedCount = budget.historyAbbreviatedCount || 5;
|
|
1073
|
+
tieredContext = {
|
|
1074
|
+
...tieredContext,
|
|
1075
|
+
historyContext: {
|
|
1076
|
+
...context.historyContext,
|
|
1077
|
+
recentEvents: context.historyContext.recentEvents.slice(0, abbreviatedCount)
|
|
1078
|
+
}
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
onProgress?.({ type: "started", message: "Starting agent-powered plan generation..." });
|
|
1083
|
+
const { tool: writePlanTool, planPromise } = createWritePlanTool();
|
|
1084
|
+
const workingDir = projectRoot || process.cwd();
|
|
1085
|
+
const toolRegistry = ToolRegistry.create({
|
|
1086
|
+
workingDirectory: workingDir
|
|
1087
|
+
});
|
|
1088
|
+
for (const tool of codebaseTools) {
|
|
1089
|
+
toolRegistry.register(tool);
|
|
1090
|
+
}
|
|
1091
|
+
toolRegistry.register(writePlanTool);
|
|
1092
|
+
const conversation = ConversationManager.create();
|
|
1093
|
+
conversation.addSystemMessage(AGENT_SYSTEM_PROMPT);
|
|
1094
|
+
const model = options.model || "claude-sonnet-4-20250514";
|
|
1095
|
+
const agentProvider = createAgentProvider(provider, model);
|
|
1096
|
+
const agentLoop = AgentLoop.create({
|
|
1097
|
+
provider: agentProvider,
|
|
1098
|
+
toolRegistry,
|
|
1099
|
+
conversation,
|
|
1100
|
+
model,
|
|
1101
|
+
maxIterations: DEFAULT_MAX_ITERATIONS
|
|
1102
|
+
});
|
|
1103
|
+
const userPrompt = buildAgentUserPrompt(tieredContext);
|
|
1104
|
+
onProgress?.({ type: "streaming", message: "Agent exploring codebase...", charsReceived: 0 });
|
|
1105
|
+
let textContent = "";
|
|
1106
|
+
let toolCallCount = 0;
|
|
1107
|
+
let lastProgressUpdate = Date.now();
|
|
1108
|
+
const agentDone = (async () => {
|
|
1109
|
+
for await (const chunk of agentLoop.runStream(userPrompt)) {
|
|
1110
|
+
if (chunk.type === "text" && chunk.text) {
|
|
1111
|
+
textContent += chunk.text;
|
|
1112
|
+
}
|
|
1113
|
+
if (chunk.type === "tool_start" && chunk.tool) {
|
|
1114
|
+
toolCallCount++;
|
|
1115
|
+
const now = Date.now();
|
|
1116
|
+
if (now - lastProgressUpdate > 300) {
|
|
1117
|
+
const toolName = chunk.tool.name;
|
|
1118
|
+
let detail = "";
|
|
1119
|
+
if (toolName === "read_file" && chunk.tool.arguments?.path) {
|
|
1120
|
+
detail = `: ${chunk.tool.arguments.path}`;
|
|
1121
|
+
} else if (toolName === "grep" && chunk.tool.arguments?.pattern) {
|
|
1122
|
+
detail = `: "${chunk.tool.arguments.pattern}"`;
|
|
1123
|
+
} else if (toolName === "list_files" && chunk.tool.arguments?.path) {
|
|
1124
|
+
detail = `: ${chunk.tool.arguments.path}`;
|
|
1125
|
+
} else if (toolName === "write_plan") {
|
|
1126
|
+
detail = " (submitting plan)";
|
|
1127
|
+
}
|
|
1128
|
+
onProgress?.({
|
|
1129
|
+
type: "streaming",
|
|
1130
|
+
message: `Agent: ${toolName}${detail} (${toolCallCount} tool calls)`,
|
|
1131
|
+
charsReceived: textContent.length
|
|
1132
|
+
});
|
|
1133
|
+
lastProgressUpdate = now;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
if (chunk.type === "tool_result" && chunk.tool) {
|
|
1137
|
+
const now = Date.now();
|
|
1138
|
+
if (now - lastProgressUpdate > 300) {
|
|
1139
|
+
onProgress?.({
|
|
1140
|
+
type: "streaming",
|
|
1141
|
+
message: `Agent: ${chunk.tool.name} complete (${toolCallCount} tool calls)`,
|
|
1142
|
+
charsReceived: textContent.length
|
|
1143
|
+
});
|
|
1144
|
+
lastProgressUpdate = now;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
})();
|
|
1149
|
+
let plan;
|
|
1150
|
+
try {
|
|
1151
|
+
const result = await Promise.race([
|
|
1152
|
+
planPromise.then((p) => ({ type: "plan", plan: p })),
|
|
1153
|
+
agentDone.then(() => ({ type: "done", plan: null }))
|
|
1154
|
+
]);
|
|
1155
|
+
if (result.type === "plan") {
|
|
1156
|
+
plan = result.plan;
|
|
1157
|
+
} else {
|
|
1158
|
+
try {
|
|
1159
|
+
plan = await Promise.race([
|
|
1160
|
+
planPromise,
|
|
1161
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 1e3))
|
|
1162
|
+
]);
|
|
1163
|
+
} catch {
|
|
1164
|
+
throw new Error(
|
|
1165
|
+
`Agent completed ${toolCallCount} tool calls over ${DEFAULT_MAX_ITERATIONS} iterations but did not call write_plan to submit the plan. The agent may have run out of iterations. Try increasing maxIterations or simplifying the plan scope.`
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
} catch (error) {
|
|
1170
|
+
writePlanTool._rejectPlan?.(error instanceof Error ? error : new Error(String(error)));
|
|
1171
|
+
throw error;
|
|
1172
|
+
}
|
|
1173
|
+
onProgress?.({ type: "parsing", message: `Plan received with ${plan.steps.length} steps from ${toolCallCount} tool calls` });
|
|
1174
|
+
onProgress?.({ type: "complete", message: "Agent-powered plan generation complete" });
|
|
1175
|
+
return {
|
|
1176
|
+
plan,
|
|
1177
|
+
tiering: tieringDecision
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
async function loadProvider(config) {
|
|
1181
|
+
const { name, apiKey, session, mcpServer } = config;
|
|
1182
|
+
if (session && session.providerMode === "sampling") {
|
|
1183
|
+
return await loadSamplingProvider(session, mcpServer);
|
|
1184
|
+
}
|
|
1185
|
+
if (name) {
|
|
1186
|
+
return await loadDirectProvider(name, apiKey);
|
|
1187
|
+
}
|
|
1188
|
+
const defaultProvider = getDefaultProvider();
|
|
1189
|
+
if (defaultProvider) {
|
|
1190
|
+
return await loadDirectProvider(defaultProvider, apiKey);
|
|
1191
|
+
}
|
|
1192
|
+
const clientName = session?.clientInfo?.name ?? "your client";
|
|
1193
|
+
const errorMessage = [
|
|
1194
|
+
"❌ No AI provider available for RiotPlan.",
|
|
1195
|
+
"",
|
|
1196
|
+
`Your client (${clientName}) does not support MCP sampling, and no API keys are configured.`,
|
|
1197
|
+
"",
|
|
1198
|
+
"To use RiotPlan's AI generation features, either:",
|
|
1199
|
+
"",
|
|
1200
|
+
"1. Use a client that supports MCP sampling:",
|
|
1201
|
+
" - GitHub Copilot (supports sampling)",
|
|
1202
|
+
" - FastMCP (Python framework for testing)",
|
|
1203
|
+
"",
|
|
1204
|
+
"2. Set up an API key:",
|
|
1205
|
+
" - ANTHROPIC_API_KEY for Claude models (recommended)",
|
|
1206
|
+
" - OPENAI_API_KEY for GPT models",
|
|
1207
|
+
" - GOOGLE_API_KEY for Gemini models",
|
|
1208
|
+
"",
|
|
1209
|
+
"3. Create plan steps manually:",
|
|
1210
|
+
" - Use riotplan_step with action=add to add steps without AI",
|
|
1211
|
+
"",
|
|
1212
|
+
"For more information: https://github.com/kjerneverk/riotplan#ai-providers"
|
|
1213
|
+
].join("\n");
|
|
1214
|
+
throw new Error(errorMessage);
|
|
1215
|
+
}
|
|
1216
|
+
async function loadSamplingProvider(session, mcpServer) {
|
|
1217
|
+
try {
|
|
1218
|
+
const { createSamplingProvider } = await import("@kjerneverk/execution-sampling");
|
|
1219
|
+
const provider = createSamplingProvider({
|
|
1220
|
+
sessionId: session.sessionId,
|
|
1221
|
+
clientName: session.clientInfo?.name,
|
|
1222
|
+
supportsTools: false,
|
|
1223
|
+
// RiotPlan doesn't use tools in generation
|
|
1224
|
+
debug: false
|
|
1225
|
+
});
|
|
1226
|
+
if (mcpServer) {
|
|
1227
|
+
provider.setSamplingClient(mcpServer);
|
|
1228
|
+
}
|
|
1229
|
+
return provider;
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
if (error instanceof Error && (error.message.includes("Cannot find package") || error.message.includes("Cannot find module") || error.message.includes("Could not resolve"))) {
|
|
1232
|
+
throw new Error(
|
|
1233
|
+
"Sampling provider (@kjerneverk/execution-sampling) is not installed.\nThis is a development error - the package should be installed as a dependency."
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
throw error;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
async function loadDirectProvider(name, apiKey) {
|
|
1240
|
+
try {
|
|
1241
|
+
switch (name.toLowerCase()) {
|
|
1242
|
+
case "anthropic":
|
|
1243
|
+
case "claude":
|
|
1244
|
+
return await loadAnthropicProvider(apiKey);
|
|
1245
|
+
case "openai":
|
|
1246
|
+
case "gpt":
|
|
1247
|
+
return await loadOpenAIProvider(apiKey);
|
|
1248
|
+
case "gemini":
|
|
1249
|
+
case "google":
|
|
1250
|
+
return await loadGeminiProvider(apiKey);
|
|
1251
|
+
default:
|
|
1252
|
+
throw new Error(`Unknown provider: ${name}`);
|
|
1253
|
+
}
|
|
1254
|
+
} catch (error) {
|
|
1255
|
+
if (error instanceof Error && (error.message.includes("Cannot find package") || error.message.includes("Cannot find module") || error.message.includes("Could not resolve"))) {
|
|
1256
|
+
const errorMessage = [
|
|
1257
|
+
`❌ Provider '${name}' is not installed.`,
|
|
1258
|
+
"",
|
|
1259
|
+
"To use this provider, install it:",
|
|
1260
|
+
` npm install @kjerneverk/execution-${name}`,
|
|
1261
|
+
"",
|
|
1262
|
+
"Then set the appropriate API key:",
|
|
1263
|
+
` export ${name.toUpperCase()}_API_KEY="your-key-here"`,
|
|
1264
|
+
"",
|
|
1265
|
+
"Alternative options:",
|
|
1266
|
+
" 1. Use a different provider (anthropic, openai, gemini)",
|
|
1267
|
+
" 2. Use RiotPlan via MCP with a sampling-enabled client",
|
|
1268
|
+
" 3. Create plan steps manually with riotplan_step(action=add)"
|
|
1269
|
+
].join("\n");
|
|
1270
|
+
throw new Error(errorMessage);
|
|
1271
|
+
}
|
|
1272
|
+
throw error;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
async function loadAnthropicProvider(_apiKey) {
|
|
1276
|
+
const { createAnthropicProvider } = await import("@kjerneverk/execution-anthropic");
|
|
1277
|
+
return createAnthropicProvider();
|
|
1278
|
+
}
|
|
1279
|
+
async function loadOpenAIProvider(_apiKey) {
|
|
1280
|
+
const { createOpenAIProvider } = await import("@kjerneverk/execution-openai");
|
|
1281
|
+
return createOpenAIProvider();
|
|
1282
|
+
}
|
|
1283
|
+
async function loadGeminiProvider(_apiKey) {
|
|
1284
|
+
const { createGeminiProvider } = await import("@kjerneverk/execution-gemini");
|
|
1285
|
+
return createGeminiProvider();
|
|
1286
|
+
}
|
|
1287
|
+
async function detectAvailableProviders() {
|
|
1288
|
+
const providers = [];
|
|
1289
|
+
const candidates = [
|
|
1290
|
+
{ name: "anthropic", pkg: "@kjerneverk/execution-anthropic" },
|
|
1291
|
+
{ name: "openai", pkg: "@kjerneverk/execution-openai" },
|
|
1292
|
+
{ name: "gemini", pkg: "@kjerneverk/execution-gemini" }
|
|
1293
|
+
];
|
|
1294
|
+
for (const candidate of candidates) {
|
|
1295
|
+
try {
|
|
1296
|
+
await import(candidate.pkg);
|
|
1297
|
+
providers.push(candidate.name);
|
|
1298
|
+
} catch {
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
return providers;
|
|
1302
|
+
}
|
|
1303
|
+
function getDefaultProvider() {
|
|
1304
|
+
if (process.env.ANTHROPIC_API_KEY) return "anthropic";
|
|
1305
|
+
if (process.env.OPENAI_API_KEY) return "openai";
|
|
1306
|
+
if (process.env.GOOGLE_API_KEY) return "gemini";
|
|
1307
|
+
return null;
|
|
1308
|
+
}
|
|
1309
|
+
function getProviderApiKey(provider) {
|
|
1310
|
+
switch (provider.toLowerCase()) {
|
|
1311
|
+
case "anthropic":
|
|
1312
|
+
case "claude":
|
|
1313
|
+
return process.env.ANTHROPIC_API_KEY;
|
|
1314
|
+
case "openai":
|
|
1315
|
+
case "gpt":
|
|
1316
|
+
return process.env.OPENAI_API_KEY;
|
|
1317
|
+
case "gemini":
|
|
1318
|
+
case "google":
|
|
1319
|
+
return process.env.GOOGLE_API_KEY;
|
|
1320
|
+
default:
|
|
1321
|
+
return void 0;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
class ConstraintCoverageCheck {
|
|
1325
|
+
name = "constraint-coverage";
|
|
1326
|
+
validate(plan, context) {
|
|
1327
|
+
const warnings = [];
|
|
1328
|
+
const coverage = {};
|
|
1329
|
+
const constraints = context.constraints || [];
|
|
1330
|
+
if (constraints.length === 0) {
|
|
1331
|
+
return {
|
|
1332
|
+
passed: true,
|
|
1333
|
+
warnings: [],
|
|
1334
|
+
details: { coverage, message: "No constraints to validate" }
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
for (const step of plan.steps) {
|
|
1338
|
+
if (step.provenance?.constraintsAddressed) {
|
|
1339
|
+
for (const constraint of step.provenance.constraintsAddressed) {
|
|
1340
|
+
if (!coverage[constraint]) {
|
|
1341
|
+
coverage[constraint] = [];
|
|
1342
|
+
}
|
|
1343
|
+
coverage[constraint].push(`Step ${step.number}`);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
for (const constraint of constraints) {
|
|
1348
|
+
const addressed = Object.keys(coverage).some(
|
|
1349
|
+
(key) => constraint.includes(key) || key.includes(constraint.substring(0, 50))
|
|
1350
|
+
);
|
|
1351
|
+
if (!addressed) {
|
|
1352
|
+
warnings.push(`Constraint not addressed: "${constraint.substring(0, 80)}${constraint.length > 80 ? "..." : ""}"`);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
return {
|
|
1356
|
+
passed: warnings.length === 0,
|
|
1357
|
+
warnings,
|
|
1358
|
+
details: {
|
|
1359
|
+
coverage,
|
|
1360
|
+
totalConstraints: constraints.length,
|
|
1361
|
+
coveredConstraints: constraints.length - warnings.length
|
|
1362
|
+
}
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
class EvidenceReferenceCheck {
|
|
1367
|
+
name = "evidence-reference";
|
|
1368
|
+
validate(plan, context) {
|
|
1369
|
+
const warnings = [];
|
|
1370
|
+
const references = {};
|
|
1371
|
+
const evidence = context.evidence || [];
|
|
1372
|
+
if (evidence.length === 0) {
|
|
1373
|
+
return {
|
|
1374
|
+
passed: true,
|
|
1375
|
+
warnings: [],
|
|
1376
|
+
details: { references, message: "No evidence to validate" }
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
const analysisFiles = /* @__PURE__ */ new Set();
|
|
1380
|
+
if (plan.analysis?.evidenceAnalysis) {
|
|
1381
|
+
for (const ea of plan.analysis.evidenceAnalysis) {
|
|
1382
|
+
analysisFiles.add(ea.evidenceFile);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
for (const step of plan.steps) {
|
|
1386
|
+
if (step.provenance?.evidenceUsed) {
|
|
1387
|
+
for (const evidenceFile of step.provenance.evidenceUsed) {
|
|
1388
|
+
if (!references[evidenceFile]) {
|
|
1389
|
+
references[evidenceFile] = [];
|
|
1390
|
+
}
|
|
1391
|
+
references[evidenceFile].push(`Step ${step.number}`);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
for (const ev of evidence) {
|
|
1396
|
+
const inAnalysis = analysisFiles.has(ev.name);
|
|
1397
|
+
const inSteps = references[ev.name] && references[ev.name].length > 0;
|
|
1398
|
+
if (!inAnalysis && !inSteps) {
|
|
1399
|
+
warnings.push(`Evidence file not referenced: "${ev.name}"`);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
return {
|
|
1403
|
+
passed: warnings.length === 0,
|
|
1404
|
+
warnings,
|
|
1405
|
+
details: {
|
|
1406
|
+
references,
|
|
1407
|
+
analysisFiles: Array.from(analysisFiles),
|
|
1408
|
+
totalEvidence: evidence.length,
|
|
1409
|
+
referencedEvidence: evidence.length - warnings.length
|
|
1410
|
+
}
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
class SelectedApproachCheck {
|
|
1415
|
+
name = "selected-approach";
|
|
1416
|
+
validate(plan, context) {
|
|
1417
|
+
const warnings = [];
|
|
1418
|
+
if (!context.selectedApproach) {
|
|
1419
|
+
return {
|
|
1420
|
+
passed: true,
|
|
1421
|
+
warnings: [],
|
|
1422
|
+
details: { message: "No selected approach to validate" }
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
const approachName = context.selectedApproach.name;
|
|
1426
|
+
if (!plan.analysis?.approachAnalysis) {
|
|
1427
|
+
warnings.push(`Selected approach "${approachName}" not analyzed in the analysis section`);
|
|
1428
|
+
} else {
|
|
1429
|
+
const analysisText = JSON.stringify(plan.analysis.approachAnalysis).toLowerCase();
|
|
1430
|
+
if (!analysisText.includes(approachName.toLowerCase())) {
|
|
1431
|
+
warnings.push(`Selected approach "${approachName}" not mentioned in approach analysis`);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
const planText = (plan.summary + " " + plan.approach).toLowerCase();
|
|
1435
|
+
if (!planText.includes(approachName.toLowerCase())) {
|
|
1436
|
+
warnings.push(`Selected approach "${approachName}" not mentioned in plan summary or approach`);
|
|
1437
|
+
}
|
|
1438
|
+
return {
|
|
1439
|
+
passed: warnings.length === 0,
|
|
1440
|
+
warnings,
|
|
1441
|
+
details: {
|
|
1442
|
+
approachName,
|
|
1443
|
+
hasApproachAnalysis: !!plan.analysis?.approachAnalysis
|
|
1444
|
+
}
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
class ValidationPipeline {
|
|
1449
|
+
checks = [];
|
|
1450
|
+
constructor() {
|
|
1451
|
+
this.addCheck(new ConstraintCoverageCheck());
|
|
1452
|
+
this.addCheck(new EvidenceReferenceCheck());
|
|
1453
|
+
this.addCheck(new SelectedApproachCheck());
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Add a custom validation check
|
|
1457
|
+
*/
|
|
1458
|
+
addCheck(check) {
|
|
1459
|
+
this.checks.push(check);
|
|
1460
|
+
}
|
|
1461
|
+
/**
|
|
1462
|
+
* Run all validation checks
|
|
1463
|
+
*/
|
|
1464
|
+
validate(plan, context) {
|
|
1465
|
+
const results = [];
|
|
1466
|
+
const allWarnings = [];
|
|
1467
|
+
for (const check of this.checks) {
|
|
1468
|
+
const result = check.validate(plan, context);
|
|
1469
|
+
results.push({ name: check.name, result });
|
|
1470
|
+
allWarnings.push(...result.warnings);
|
|
1471
|
+
}
|
|
1472
|
+
return {
|
|
1473
|
+
overallPassed: allWarnings.length === 0,
|
|
1474
|
+
checks: results,
|
|
1475
|
+
allWarnings
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
function validatePlan(plan, context) {
|
|
1480
|
+
const pipeline = new ValidationPipeline();
|
|
1481
|
+
return pipeline.validate(plan, context);
|
|
1482
|
+
}
|
|
1483
|
+
function generateProvenanceMarkdown(data) {
|
|
1484
|
+
const { plan, context, validation, tiering, generatedAt } = data;
|
|
1485
|
+
const sections = [];
|
|
1486
|
+
sections.push(`# Plan Provenance
|
|
1487
|
+
|
|
1488
|
+
This document traces how the generated plan connects to the idea artifacts.`);
|
|
1489
|
+
sections.push(`## How This Plan Was Generated
|
|
1490
|
+
|
|
1491
|
+
- **Generated**: ${generatedAt.toISOString()}
|
|
1492
|
+
- **Constraints**: ${context.constraints?.length || 0}
|
|
1493
|
+
- **Evidence files**: ${context.evidence?.length || 0}
|
|
1494
|
+
- **Selected approach**: ${context.selectedApproach?.name || "None"}
|
|
1495
|
+
- **History events**: ${context.historyContext?.totalEvents || 0}`);
|
|
1496
|
+
sections.push(`## Artifact Analysis`);
|
|
1497
|
+
if (plan.analysis?.constraintAnalysis && plan.analysis.constraintAnalysis.length > 0) {
|
|
1498
|
+
sections.push(`### Constraints
|
|
1499
|
+
|
|
1500
|
+
The model analyzed ${plan.analysis.constraintAnalysis.length} constraint(s):
|
|
1501
|
+
|
|
1502
|
+
| # | Constraint | Addressed By | Model's Understanding |
|
|
1503
|
+
|---|-----------|-------------|----------------------|`);
|
|
1504
|
+
for (let i = 0; i < plan.analysis.constraintAnalysis.length; i++) {
|
|
1505
|
+
const ca = plan.analysis.constraintAnalysis[i];
|
|
1506
|
+
const addressedBy = [];
|
|
1507
|
+
for (const step of plan.steps) {
|
|
1508
|
+
if (step.provenance?.constraintsAddressed) {
|
|
1509
|
+
for (const constraintRef of step.provenance.constraintsAddressed) {
|
|
1510
|
+
if (ca.constraint.includes(constraintRef) || constraintRef.includes(ca.constraint.substring(0, 30))) {
|
|
1511
|
+
addressedBy.push(`Step ${step.number}`);
|
|
1512
|
+
break;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
const constraintText = ca.constraint.length > 60 ? ca.constraint.substring(0, 60) + "..." : ca.constraint;
|
|
1518
|
+
const understanding = ca.understanding.length > 80 ? ca.understanding.substring(0, 80) + "..." : ca.understanding;
|
|
1519
|
+
const steps = addressedBy.length > 0 ? addressedBy.join(", ") : "*(none)*";
|
|
1520
|
+
sections.push(`| ${i + 1} | ${constraintText} | ${steps} | ${understanding} |`);
|
|
1521
|
+
}
|
|
1522
|
+
} else if (context.constraints && context.constraints.length > 0) {
|
|
1523
|
+
sections.push(`### Constraints
|
|
1524
|
+
|
|
1525
|
+
${context.constraints.length} constraint(s) were provided but not analyzed in the response.`);
|
|
1526
|
+
}
|
|
1527
|
+
if (plan.analysis?.evidenceAnalysis && plan.analysis.evidenceAnalysis.length > 0) {
|
|
1528
|
+
sections.push(`
|
|
1529
|
+
### Evidence Used
|
|
1530
|
+
|
|
1531
|
+
The model analyzed ${plan.analysis.evidenceAnalysis.length} evidence file(s):
|
|
1532
|
+
|
|
1533
|
+
| File | Key Findings | Referenced In |
|
|
1534
|
+
|------|-------------|--------------|`);
|
|
1535
|
+
for (const ea of plan.analysis.evidenceAnalysis) {
|
|
1536
|
+
const referencedIn = [];
|
|
1537
|
+
for (const step of plan.steps) {
|
|
1538
|
+
if (step.provenance?.evidenceUsed?.includes(ea.evidenceFile)) {
|
|
1539
|
+
referencedIn.push(`Step ${step.number}`);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
const findings = ea.keyFindings.length > 100 ? ea.keyFindings.substring(0, 100) + "..." : ea.keyFindings;
|
|
1543
|
+
const refs = referencedIn.length > 0 ? referencedIn.join(", ") : "*(none)*";
|
|
1544
|
+
sections.push(`| ${ea.evidenceFile} | ${findings} | ${refs} |`);
|
|
1545
|
+
}
|
|
1546
|
+
} else if (context.evidence && context.evidence.length > 0) {
|
|
1547
|
+
sections.push(`
|
|
1548
|
+
### Evidence Used
|
|
1549
|
+
|
|
1550
|
+
${context.evidence.length} evidence file(s) were provided but not analyzed in the response.`);
|
|
1551
|
+
}
|
|
1552
|
+
if (plan.analysis?.approachAnalysis) {
|
|
1553
|
+
const aa = plan.analysis.approachAnalysis;
|
|
1554
|
+
sections.push(`
|
|
1555
|
+
### Selected Approach
|
|
1556
|
+
|
|
1557
|
+
- **Approach**: ${aa.selectedApproach}
|
|
1558
|
+
- **Commitments**: ${aa.commitments}
|
|
1559
|
+
- **Implementation Strategy**: ${aa.implementationStrategy}`);
|
|
1560
|
+
} else if (context.selectedApproach) {
|
|
1561
|
+
sections.push(`
|
|
1562
|
+
### Selected Approach
|
|
1563
|
+
|
|
1564
|
+
A selected approach ("${context.selectedApproach.name}") was provided but not analyzed in the response.`);
|
|
1565
|
+
}
|
|
1566
|
+
if (plan.analysis?.risks && plan.analysis.risks.length > 0) {
|
|
1567
|
+
sections.push(`
|
|
1568
|
+
### Risks Identified
|
|
1569
|
+
|
|
1570
|
+
${plan.analysis.risks.map((r, i) => `${i + 1}. ${r}`).join("\n")}`);
|
|
1571
|
+
}
|
|
1572
|
+
if (validation.allWarnings.length > 0) {
|
|
1573
|
+
sections.push(`## Coverage Warnings
|
|
1574
|
+
|
|
1575
|
+
The following gaps were detected during validation:
|
|
1576
|
+
|
|
1577
|
+
${validation.allWarnings.map((w) => `- ${w}`).join("\n")}`);
|
|
1578
|
+
} else {
|
|
1579
|
+
sections.push(`## Coverage Validation
|
|
1580
|
+
|
|
1581
|
+
✅ All constraints addressed, all evidence referenced, selected approach implemented.`);
|
|
1582
|
+
}
|
|
1583
|
+
if (tiering) {
|
|
1584
|
+
sections.push(`## Token Budget
|
|
1585
|
+
|
|
1586
|
+
- **Estimated tokens**: ${tiering.totalEstimatedTokens}
|
|
1587
|
+
- **Budget exceeded**: ${tiering.budgetExceeded ? "Yes" : "No"}`);
|
|
1588
|
+
if (tiering.evidenceTiered.full.length > 0) {
|
|
1589
|
+
sections.push(`- **Evidence (full)**: ${tiering.evidenceTiered.full.join(", ")}`);
|
|
1590
|
+
}
|
|
1591
|
+
if (tiering.evidenceTiered.summarized.length > 0) {
|
|
1592
|
+
sections.push(`- **Evidence (summarized)**: ${tiering.evidenceTiered.summarized.join(", ")}`);
|
|
1593
|
+
}
|
|
1594
|
+
if (tiering.evidenceTiered.listOnly.length > 0) {
|
|
1595
|
+
sections.push(`- **Evidence (preview only)**: ${tiering.evidenceTiered.listOnly.join(", ")}`);
|
|
1596
|
+
}
|
|
1597
|
+
if (tiering.historyAbbreviated) {
|
|
1598
|
+
sections.push(`- **History**: abbreviated to most recent events`);
|
|
1599
|
+
}
|
|
1600
|
+
if (tiering.warnings.length > 0) {
|
|
1601
|
+
sections.push(`
|
|
1602
|
+
### Tiering Warnings
|
|
1603
|
+
|
|
1604
|
+
${tiering.warnings.map((w) => `- ${w}`).join("\n")}`);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
sections.push(`## Step Provenance
|
|
1608
|
+
|
|
1609
|
+
How each step connects to the idea artifacts:
|
|
1610
|
+
`);
|
|
1611
|
+
for (const step of plan.steps) {
|
|
1612
|
+
sections.push(`### Step ${step.number}: ${step.title}`);
|
|
1613
|
+
if (step.provenance) {
|
|
1614
|
+
if (step.provenance.rationale) {
|
|
1615
|
+
sections.push(`
|
|
1616
|
+
**Rationale**: ${step.provenance.rationale}`);
|
|
1617
|
+
}
|
|
1618
|
+
if (step.provenance.constraintsAddressed && step.provenance.constraintsAddressed.length > 0) {
|
|
1619
|
+
sections.push(`
|
|
1620
|
+
**Addresses constraints**: ${step.provenance.constraintsAddressed.join(", ")}`);
|
|
1621
|
+
}
|
|
1622
|
+
if (step.provenance.evidenceUsed && step.provenance.evidenceUsed.length > 0) {
|
|
1623
|
+
sections.push(`
|
|
1624
|
+
**Uses evidence**: ${step.provenance.evidenceUsed.join(", ")}`);
|
|
1625
|
+
}
|
|
1626
|
+
} else {
|
|
1627
|
+
sections.push(`
|
|
1628
|
+
*(No provenance data)*`);
|
|
1629
|
+
}
|
|
1630
|
+
sections.push("");
|
|
1631
|
+
}
|
|
1632
|
+
return sections.join("\n\n");
|
|
1633
|
+
}
|
|
1634
|
+
async function readFileSafe(path) {
|
|
1635
|
+
try {
|
|
1636
|
+
return await readFile(path, "utf-8");
|
|
1637
|
+
} catch {
|
|
1638
|
+
return null;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
function getMostRecentFileByType(files, fileType) {
|
|
1642
|
+
const matches = files.filter((file) => file.type === fileType).sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt));
|
|
1643
|
+
return matches[0]?.content ?? null;
|
|
1644
|
+
}
|
|
1645
|
+
function normalizeArtifactKey(value) {
|
|
1646
|
+
return (value || "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
1647
|
+
}
|
|
1648
|
+
function isArtifactMatch(file, aliases, filenameCandidates) {
|
|
1649
|
+
const normalizedType = normalizeArtifactKey(file.type);
|
|
1650
|
+
const normalizedFilename = normalizeArtifactKey(file.filename);
|
|
1651
|
+
const normalizedAliases = aliases.map(normalizeArtifactKey);
|
|
1652
|
+
const normalizedCandidates = filenameCandidates.map(normalizeArtifactKey);
|
|
1653
|
+
return normalizedAliases.includes(normalizedType) || normalizedCandidates.includes(normalizedFilename);
|
|
1654
|
+
}
|
|
1655
|
+
function getMostRecentArtifactContent(files, aliases, filenameCandidates) {
|
|
1656
|
+
const matches = files.filter((file) => isArtifactMatch(file, aliases, filenameCandidates)).sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt));
|
|
1657
|
+
return matches[0]?.content ?? null;
|
|
1658
|
+
}
|
|
1659
|
+
function extractConstraints(ideaContent) {
|
|
1660
|
+
const constraints = [];
|
|
1661
|
+
const constraintsMatch = ideaContent.match(/## Constraints\n\n([\s\S]*?)(?=\n## |$)/);
|
|
1662
|
+
if (constraintsMatch) {
|
|
1663
|
+
const lines = constraintsMatch[1].split("\n");
|
|
1664
|
+
for (const line of lines) {
|
|
1665
|
+
const trimmed = line.trim();
|
|
1666
|
+
if (trimmed.startsWith("- ") && !trimmed.includes("_Add constraints")) {
|
|
1667
|
+
constraints.push(trimmed.substring(2));
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
return constraints;
|
|
1672
|
+
}
|
|
1673
|
+
function extractQuestions(ideaContent) {
|
|
1674
|
+
const questions = [];
|
|
1675
|
+
const questionsMatch = ideaContent.match(/## Questions\n\n([\s\S]*?)(?=\n## |$)/);
|
|
1676
|
+
if (questionsMatch) {
|
|
1677
|
+
const lines = questionsMatch[1].split("\n");
|
|
1678
|
+
for (const line of lines) {
|
|
1679
|
+
const trimmed = line.trim();
|
|
1680
|
+
if (trimmed.startsWith("- ") && !trimmed.includes("_Add questions")) {
|
|
1681
|
+
questions.push(trimmed.substring(2));
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
return questions;
|
|
1686
|
+
}
|
|
1687
|
+
function extractSelectedApproach(shapingContent) {
|
|
1688
|
+
const nameMatch = shapingContent.match(/\*\*Selected Approach\*\*: (.+)/);
|
|
1689
|
+
if (!nameMatch) return null;
|
|
1690
|
+
const approachName = nameMatch[1].trim();
|
|
1691
|
+
const approachPattern = new RegExp(`### Approach: ${approachName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s+\\*\\*Description\\*\\*: ([\\s\\S]*?)(?:\\*\\*Tradeoffs\\*\\*:|### Approach:|## |$)`);
|
|
1692
|
+
const approachMatch = shapingContent.match(approachPattern);
|
|
1693
|
+
const reasoningMatch = shapingContent.match(/\*\*Reasoning\*\*: ([^\n]+)/);
|
|
1694
|
+
return {
|
|
1695
|
+
name: approachName,
|
|
1696
|
+
description: approachMatch ? approachMatch[1].trim() : "",
|
|
1697
|
+
reasoning: reasoningMatch ? reasoningMatch[1].trim() : ""
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
async function readEvidenceFiles(planPath, includeContent = true) {
|
|
1701
|
+
const evidenceDir = join(planPath, "evidence");
|
|
1702
|
+
const dotEvidenceDir = join(planPath, ".evidence");
|
|
1703
|
+
let files = [];
|
|
1704
|
+
let actualDir = evidenceDir;
|
|
1705
|
+
try {
|
|
1706
|
+
files = await readdir(evidenceDir);
|
|
1707
|
+
} catch {
|
|
1708
|
+
try {
|
|
1709
|
+
files = await readdir(dotEvidenceDir);
|
|
1710
|
+
actualDir = dotEvidenceDir;
|
|
1711
|
+
} catch {
|
|
1712
|
+
return [];
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
const evidenceFiles = [];
|
|
1716
|
+
for (const file of files) {
|
|
1717
|
+
if (!file.endsWith(".md")) continue;
|
|
1718
|
+
const filePath = join(actualDir, file);
|
|
1719
|
+
try {
|
|
1720
|
+
const stats = await stat(filePath);
|
|
1721
|
+
let content = "";
|
|
1722
|
+
if (includeContent) {
|
|
1723
|
+
const fileContent = await readFile(filePath, "utf-8");
|
|
1724
|
+
content = fileContent;
|
|
1725
|
+
}
|
|
1726
|
+
evidenceFiles.push({
|
|
1727
|
+
name: file,
|
|
1728
|
+
content,
|
|
1729
|
+
size: stats.size
|
|
1730
|
+
});
|
|
1731
|
+
} catch {
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
return evidenceFiles;
|
|
1735
|
+
}
|
|
1736
|
+
async function readRecentHistory(planPath, limit = 15) {
|
|
1737
|
+
const timelinePath = join(planPath, ".history", "timeline.jsonl");
|
|
1738
|
+
try {
|
|
1739
|
+
const content = await readFile(timelinePath, "utf-8");
|
|
1740
|
+
const lines = content.trim().split("\n").filter((line) => line.trim());
|
|
1741
|
+
const totalEvents = lines.length;
|
|
1742
|
+
const recentLines = lines.slice(-limit);
|
|
1743
|
+
const recentEvents = [];
|
|
1744
|
+
for (const line of recentLines) {
|
|
1745
|
+
try {
|
|
1746
|
+
const event = JSON.parse(line);
|
|
1747
|
+
recentEvents.push({
|
|
1748
|
+
type: event.type || "unknown",
|
|
1749
|
+
timestamp: event.timestamp || "",
|
|
1750
|
+
summary: summarizeEvent(event)
|
|
1751
|
+
});
|
|
1752
|
+
} catch {
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
return { recentEvents, totalEvents };
|
|
1756
|
+
} catch {
|
|
1757
|
+
return { recentEvents: [], totalEvents: 0 };
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
function summarizeEvent(event) {
|
|
1761
|
+
const type = event.type || "unknown";
|
|
1762
|
+
const data = event.data || {};
|
|
1763
|
+
switch (type) {
|
|
1764
|
+
case "note_added":
|
|
1765
|
+
return `Note: ${truncate(data.note, 60)}`;
|
|
1766
|
+
case "constraint_added":
|
|
1767
|
+
return `Constraint: ${truncate(data.constraint, 60)}`;
|
|
1768
|
+
case "question_added":
|
|
1769
|
+
return `Question: ${truncate(data.question, 60)}`;
|
|
1770
|
+
case "evidence_added":
|
|
1771
|
+
return `Evidence: ${truncate(data.description, 60)}`;
|
|
1772
|
+
case "narrative_chunk":
|
|
1773
|
+
return `Narrative: ${truncate(data.content, 60)}`;
|
|
1774
|
+
case "approach_added":
|
|
1775
|
+
return `Approach: ${data.name || "unnamed"}`;
|
|
1776
|
+
case "approach_selected":
|
|
1777
|
+
return `Selected: ${data.approach || "unknown"}`;
|
|
1778
|
+
case "shaping_started":
|
|
1779
|
+
return "Shaping started";
|
|
1780
|
+
case "checkpoint_created":
|
|
1781
|
+
return `Checkpoint: ${data.name || "unnamed"}`;
|
|
1782
|
+
case "step_reflected":
|
|
1783
|
+
return `Step ${data.step || "?"} reflection: ${truncate(data.reflection, 60)}`;
|
|
1784
|
+
default:
|
|
1785
|
+
return type;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
function truncate(str, maxLength) {
|
|
1789
|
+
if (!str) return "";
|
|
1790
|
+
if (str.length <= maxLength) return str;
|
|
1791
|
+
return str.substring(0, maxLength - 3) + "...";
|
|
1792
|
+
}
|
|
1793
|
+
function loadCatalystContent(mergedCatalyst) {
|
|
1794
|
+
if (!mergedCatalyst) {
|
|
1795
|
+
return void 0;
|
|
1796
|
+
}
|
|
1797
|
+
const renderFacet = (facetContent) => {
|
|
1798
|
+
if (!facetContent || facetContent.length === 0) {
|
|
1799
|
+
return "";
|
|
1800
|
+
}
|
|
1801
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
1802
|
+
for (const item of facetContent) {
|
|
1803
|
+
const existing = bySource.get(item.sourceId) || [];
|
|
1804
|
+
existing.push(item.content);
|
|
1805
|
+
bySource.set(item.sourceId, existing);
|
|
1806
|
+
}
|
|
1807
|
+
const parts = [];
|
|
1808
|
+
for (const [sourceId, contents] of bySource) {
|
|
1809
|
+
parts.push(`### From ${sourceId}
|
|
1810
|
+
|
|
1811
|
+
${contents.join("\n\n")}`);
|
|
1812
|
+
}
|
|
1813
|
+
return parts.join("\n\n");
|
|
1814
|
+
};
|
|
1815
|
+
return {
|
|
1816
|
+
constraints: renderFacet(mergedCatalyst.facets.constraints),
|
|
1817
|
+
domainKnowledge: renderFacet(mergedCatalyst.facets.domainKnowledge),
|
|
1818
|
+
outputTemplates: renderFacet(mergedCatalyst.facets.outputTemplates),
|
|
1819
|
+
processGuidance: renderFacet(mergedCatalyst.facets.processGuidance),
|
|
1820
|
+
questions: renderFacet(mergedCatalyst.facets.questions),
|
|
1821
|
+
validationRules: renderFacet(mergedCatalyst.facets.validationRules),
|
|
1822
|
+
appliedCatalysts: mergedCatalyst.catalystIds
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
async function loadArtifacts(planPath) {
|
|
1826
|
+
if (planPath.endsWith(".plan")) {
|
|
1827
|
+
const provider = createSqliteProvider(planPath);
|
|
1828
|
+
try {
|
|
1829
|
+
const [filesResult, metadataResult, evidenceResult, allEventsResult] = await Promise.all([
|
|
1830
|
+
provider.getFiles(),
|
|
1831
|
+
provider.getMetadata(),
|
|
1832
|
+
provider.getEvidence(),
|
|
1833
|
+
provider.getTimelineEvents()
|
|
1834
|
+
]);
|
|
1835
|
+
if (!filesResult.success) {
|
|
1836
|
+
throw new Error(filesResult.error || "Failed to load plan files from SQLite");
|
|
1837
|
+
}
|
|
1838
|
+
if (!metadataResult.success || !metadataResult.data) {
|
|
1839
|
+
throw new Error(metadataResult.error || "Failed to load plan metadata from SQLite");
|
|
1840
|
+
}
|
|
1841
|
+
if (!evidenceResult.success) {
|
|
1842
|
+
throw new Error(evidenceResult.error || "Failed to load evidence from SQLite");
|
|
1843
|
+
}
|
|
1844
|
+
if (!allEventsResult.success) {
|
|
1845
|
+
throw new Error(allEventsResult.error || "Failed to load timeline events from SQLite");
|
|
1846
|
+
}
|
|
1847
|
+
const files = filesResult.data || [];
|
|
1848
|
+
const ideaContent2 = getMostRecentArtifactContent(files, ["idea"], ["IDEA.md", "idea"]) || getMostRecentFileByType(files, "idea");
|
|
1849
|
+
const shapingContent2 = getMostRecentArtifactContent(files, ["shaping"], ["SHAPING.md", "shaping"]) || getMostRecentFileByType(files, "shaping");
|
|
1850
|
+
const lifecycleFile = getMostRecentArtifactContent(files, ["lifecycle"], ["LIFECYCLE.md", "lifecycle"]) || getMostRecentFileByType(files, "lifecycle");
|
|
1851
|
+
const lifecycleContent2 = lifecycleFile || `# Lifecycle
|
|
1852
|
+
|
|
1853
|
+
## Current Stage
|
|
1854
|
+
|
|
1855
|
+
**Stage**: \`${metadataResult.data.stage}\`
|
|
1856
|
+
**Since**: ${metadataResult.data.updatedAt}
|
|
1857
|
+
`;
|
|
1858
|
+
const evidence2 = (evidenceResult.data || []).map((entry) => {
|
|
1859
|
+
const body = entry.content || entry.summary || entry.description;
|
|
1860
|
+
return {
|
|
1861
|
+
name: entry.filePath || `${entry.id}.md`,
|
|
1862
|
+
content: body,
|
|
1863
|
+
size: Buffer.byteLength(body, "utf-8")
|
|
1864
|
+
};
|
|
1865
|
+
});
|
|
1866
|
+
const events = (allEventsResult.data || []).slice().sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
|
|
1867
|
+
const recentEvents = events.slice(-15).map((event) => ({
|
|
1868
|
+
type: event.type || "unknown",
|
|
1869
|
+
timestamp: event.timestamp || "",
|
|
1870
|
+
summary: summarizeEvent(event)
|
|
1871
|
+
}));
|
|
1872
|
+
const constraints2 = ideaContent2 ? extractConstraints(ideaContent2) : [];
|
|
1873
|
+
const questions2 = ideaContent2 ? extractQuestions(ideaContent2) : [];
|
|
1874
|
+
const selectedApproach2 = shapingContent2 ? extractSelectedApproach(shapingContent2) : null;
|
|
1875
|
+
return {
|
|
1876
|
+
ideaContent: ideaContent2,
|
|
1877
|
+
shapingContent: shapingContent2,
|
|
1878
|
+
lifecycleContent: lifecycleContent2,
|
|
1879
|
+
artifactDiagnostics: {
|
|
1880
|
+
planId: planPath,
|
|
1881
|
+
hasIdeaArtifact: Boolean(ideaContent2),
|
|
1882
|
+
detectedArtifacts: files.map((file) => ({
|
|
1883
|
+
type: file.type,
|
|
1884
|
+
filename: file.filename
|
|
1885
|
+
}))
|
|
1886
|
+
},
|
|
1887
|
+
constraints: constraints2,
|
|
1888
|
+
questions: questions2,
|
|
1889
|
+
selectedApproach: selectedApproach2,
|
|
1890
|
+
evidence: evidence2,
|
|
1891
|
+
historyContext: {
|
|
1892
|
+
recentEvents,
|
|
1893
|
+
totalEvents: events.length
|
|
1894
|
+
}
|
|
1895
|
+
};
|
|
1896
|
+
} finally {
|
|
1897
|
+
await provider.close();
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
const [ideaContent, shapingContent, lifecycleContent, evidence, history] = await Promise.all([
|
|
1901
|
+
readFileSafe(join(planPath, "IDEA.md")),
|
|
1902
|
+
readFileSafe(join(planPath, "SHAPING.md")),
|
|
1903
|
+
readFileSafe(join(planPath, "LIFECYCLE.md")),
|
|
1904
|
+
readEvidenceFiles(planPath, true),
|
|
1905
|
+
readRecentHistory(planPath)
|
|
1906
|
+
]);
|
|
1907
|
+
const constraints = ideaContent ? extractConstraints(ideaContent) : [];
|
|
1908
|
+
const questions = ideaContent ? extractQuestions(ideaContent) : [];
|
|
1909
|
+
const selectedApproach = shapingContent ? extractSelectedApproach(shapingContent) : null;
|
|
1910
|
+
return {
|
|
1911
|
+
ideaContent,
|
|
1912
|
+
shapingContent,
|
|
1913
|
+
lifecycleContent,
|
|
1914
|
+
artifactDiagnostics: {
|
|
1915
|
+
planId: planPath,
|
|
1916
|
+
hasIdeaArtifact: Boolean(ideaContent),
|
|
1917
|
+
detectedArtifacts: [
|
|
1918
|
+
...ideaContent ? [{ type: "idea", filename: "IDEA.md" }] : [],
|
|
1919
|
+
...shapingContent ? [{ type: "shaping", filename: "SHAPING.md" }] : [],
|
|
1920
|
+
...lifecycleContent ? [{ type: "lifecycle", filename: "LIFECYCLE.md" }] : []
|
|
1921
|
+
]
|
|
1922
|
+
},
|
|
1923
|
+
constraints,
|
|
1924
|
+
questions,
|
|
1925
|
+
selectedApproach,
|
|
1926
|
+
evidence,
|
|
1927
|
+
historyContext: history
|
|
1928
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
export {
|
|
1931
|
+
ConstraintCoverageCheck,
|
|
1932
|
+
DEFAULT_TOKEN_BUDGET,
|
|
1933
|
+
EvidenceReferenceCheck,
|
|
1934
|
+
PLAN_GENERATION_RESPONSE_SCHEMA,
|
|
1935
|
+
SelectedApproachCheck,
|
|
1936
|
+
ValidationPipeline,
|
|
1937
|
+
applyEvidenceTiering,
|
|
1938
|
+
buildPlanPrompt,
|
|
1939
|
+
calculateTiering,
|
|
1940
|
+
createWritePlanTool,
|
|
1941
|
+
detectAvailableProviders,
|
|
1942
|
+
estimateTokens,
|
|
1943
|
+
extractConstraints,
|
|
1944
|
+
extractQuestions,
|
|
1945
|
+
extractSelectedApproach,
|
|
1946
|
+
formatStep,
|
|
1947
|
+
formatSummary,
|
|
1948
|
+
generatePlan,
|
|
1949
|
+
generatePlanWithAgent,
|
|
1950
|
+
generateProvenanceMarkdown,
|
|
1951
|
+
getDefaultProvider,
|
|
1952
|
+
getPlanGenerationSystemPrompt,
|
|
1953
|
+
getProviderApiKey,
|
|
1954
|
+
loadArtifacts,
|
|
1955
|
+
loadCatalystContent,
|
|
1956
|
+
loadProvider,
|
|
1957
|
+
parsePlanResponse,
|
|
1958
|
+
readEvidenceFiles,
|
|
1959
|
+
readRecentHistory,
|
|
1960
|
+
truncateToLines,
|
|
1961
|
+
validatePlan as validateGeneratedPlan
|
|
1962
|
+
};
|
|
1963
|
+
//# sourceMappingURL=index.js.map
|