Monivaiheinen projektipipeline: kpn project -komento

Uusi kpn project -komento rakentaa ohjelmistoprojektin tiedosto kerrallaan:

1. Manageri pilkkoo projektin tiedostoiksi (max 5)
   → parsii "FILENAME: description" -rivit
2. Koodari generoi jokaisen tiedoston erikseen
   → saa kontekstina aiemmin generoidut tiedostot
3. Testaaja arvioi koko projektin
   → etsii bugeja ja puutteita
4. Korjausluuppi: jos testaaja löytää ongelmia
   → koodari saa review-palautteen ja korjaa
   → testaaja arvioi uudelleen

Fallback: jos manageri ei tuota tiedostolistaa, generoidaan yhtenä kokonaisuutena.

kpn pipeline säilyy yksinkertaisena 3-vaiheisena (manageri → koodari → testaaja).

Esimerkkejä:
  kpn project "FastAPI + SQLite REST API for users"
  kpn project "Flask todo app with database"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaakko Vanhala
2026-04-05 19:30:38 +03:00
parent d68882249e
commit a6e49870d6

View File

@@ -1819,27 +1819,111 @@
} }
} }
// Pipeline: manageri → koodari → testaaja // Pipeline: manageri → koodari (per tiedosto) → testaaja → korjausluuppi
async function kpnPipeline(task) { async function kpnPipeline(task) {
termLog(`<span style="color:#a371f7;font-weight:bold">━━━ Pipeline käynnistyy ━━━</span>`); termLog(`<span style="color:#a371f7;font-weight:bold">━━━ Pipeline käynnistyy ━━━</span>`);
// Vaihe 1: Manageri analysoi // Vaihe 1: Manageri pilkkoo projektin tiedostoiksi
termLog(`\n<span style="color:#d29922;font-weight:bold">[1/3] Manageri</span> — tehtävän analyysi`); termLog(`\n<span style="color:#d29922;font-weight:bold">[1] Manageri</span> — projektin suunnittelu`);
const managerPrompt = `Analysoi seuraava ohjelmistokehitystehtävä ja kirjoita koodarille selkeä tekninen ohje mitä koodata. Vastaa lyhyesti.\n\nTehtävä: ${task}`; const managerPrompt = `You are a project manager. Break this task into individual source files.
For each file, write one line: FILENAME: description
List only the essential files (max 5). No explanations.
Task: ${task}`;
const plan = await kpnRun(agentPrompts.manager.model, managerPrompt); const plan = await kpnRun(agentPrompts.manager.model, managerPrompt);
if (!plan) { termLog(' ✗ Pipeline keskeytyi (manageri)', '#f85149'); return; } if (!plan) { termLog(' ✗ Pipeline keskeytyi (manageri)', '#f85149'); return; }
// Vaihe 2: Koodari toteuttaa // Parsitaan tiedostolista: "FILENAME: description" -rivit
termLog(`\n<span style="color:#3fb950;font-weight:bold">[2/3] Koodari</span> — toteutus`); const fileList = plan.split('\n')
const coderPrompt = `${plan}\n\nKirjoita koodi yllä olevan ohjeen mukaisesti.`; .map(line => line.trim())
.filter(line => line.includes(':') && (line.includes('.') || line.includes('/')))
.map(line => {
const [name, ...desc] = line.replace(/^[\d\.\-\*]+\s*/, '').split(':');
return { name: name.trim().replace(/\*+/g, ''), desc: desc.join(':').trim() };
})
.filter(f => f.name.length > 0 && f.name.length < 50);
if (fileList.length === 0) {
// Fallback: manageri ei tuottanut tiedostolistaa, käytetään koko vastausta ohjeena
termLog(' <span style="color:#8b949e">Ei tiedostojakoa — generoidaan yhtenä kokonaisuutena</span>');
termLog(`\n<span style="color:#3fb950;font-weight:bold">[2] Koodari</span> — toteutus`);
const code = await kpnRun(agentPrompts.coder.model, `${plan}\n\nWrite the code.`);
if (code) {
termLog(`\n<span style="color:#a371f7;font-weight:bold">━━━ Pipeline valmis ━━━</span>`);
}
return;
}
termLog(` <span style="color:#8b949e">${fileList.length} tiedostoa: ${fileList.map(f => f.name).join(', ')}</span>`);
// Vaihe 2: Koodari generoi tiedosto kerrallaan, konteksti ketjutetaan
const generatedFiles = {};
for (let i = 0; i < fileList.length; i++) {
const file = fileList[i];
termLog(`\n<span style="color:#3fb950;font-weight:bold">[${i + 2}] Koodari</span> — ${esc(file.name)}`);
// Rakennetaan konteksti: aiemmin generoidut tiedostot
let context = '';
const prevFiles = Object.entries(generatedFiles);
if (prevFiles.length > 0) {
context = 'Already written files:\n' + prevFiles.map(([name, code]) =>
`--- ${name} ---\n${code}`
).join('\n\n') + '\n\n';
}
const coderPrompt = `${context}Write ONLY the file "${file.name}": ${file.desc}
Project: ${task}`;
const code = await kpnRun(agentPrompts.coder.model, coderPrompt); const code = await kpnRun(agentPrompts.coder.model, coderPrompt);
if (!code) { termLog(' ✗ Pipeline keskeytyi (koodari)', '#f85149'); return; } if (!code) {
termLog(` ✗ Pipeline keskeytyi (${file.name})`, '#f85149');
return;
}
generatedFiles[file.name] = code;
}
// Vaihe 3: Testaaja arvioi // Vaihe 3: Testaaja arvioi koko projektin
termLog(`\n<span style="color:#58a6ff;font-weight:bold">[3/3] Testaaja</span> — arviointi`); const allCode = Object.entries(generatedFiles)
const testerPrompt = `Arvioi seuraava koodi lyhyesti. Onko siinä bugeja? Puuttuuko testejä? Anna arvosana 1-5.\n\nTehtävä: ${task}\n\nKoodi:\n${code}`; .map(([name, code]) => `--- ${name} ---\n${code}`)
await kpnRun(agentPrompts.tester.model, testerPrompt); .join('\n\n');
termLog(`\n<span style="color:#58a6ff;font-weight:bold">[${fileList.length + 2}] Testaaja</span> — arviointi`);
const reviewPrompt = `Review this project. List bugs or issues. Be brief.
If the code is correct, say "LGTM".
${allCode}`;
const review = await kpnRun(agentPrompts.tester.model, reviewPrompt);
// Vaihe 4: Korjausluuppi — jos testaaja löysi ongelmia
if (review && !review.toLowerCase().includes('lgtm') && !review.toLowerCase().includes('looks good')) {
termLog(`\n<span style="color:#d29922;font-weight:bold">[${fileList.length + 3}] Koodari</span> — korjaukset`);
const fixPrompt = `Fix the issues found in the review.
Review feedback: ${review}
Current code:
${allCode}
Write the corrected code.`;
const fixedCode = await kpnRun(agentPrompts.coder.model, fixPrompt);
if (fixedCode) {
termLog(`\n<span style="color:#58a6ff;font-weight:bold">[${fileList.length + 4}] Testaaja</span> — uudelleenarviointi`);
await kpnRun(agentPrompts.tester.model, `Review the corrected code briefly:\n${fixedCode}`);
}
}
termLog(`\n<span style="color:#a371f7;font-weight:bold">━━━ Pipeline valmis (${Object.keys(generatedFiles).length} tiedostoa) ━━━</span>`);
}
// Yksinkertainen pipeline (vanha: manageri → koodari → testaaja)
async function kpnPipelineSimple(task) {
termLog(`<span style="color:#a371f7;font-weight:bold">━━━ Pipeline käynnistyy ━━━</span>`);
termLog(`\n<span style="color:#d29922;font-weight:bold">[1/3] Manageri</span>`);
const plan = await kpnRun(agentPrompts.manager.model, `Analyse this task briefly and write a technical spec for a coder:\n${task}`);
if (!plan) return;
termLog(`\n<span style="color:#3fb950;font-weight:bold">[2/3] Koodari</span>`);
const code = await kpnRun(agentPrompts.coder.model, `${plan}\n\nWrite the code.`);
if (!code) return;
termLog(`\n<span style="color:#58a6ff;font-weight:bold">[3/3] Testaaja</span>`);
await kpnRun(agentPrompts.tester.model, `Review briefly:\n${code}`);
termLog(`\n<span style="color:#a371f7;font-weight:bold">━━━ Pipeline valmis ━━━</span>`); termLog(`\n<span style="color:#a371f7;font-weight:bold">━━━ Pipeline valmis ━━━</span>`);
} }
@@ -1858,7 +1942,8 @@
if (sub === 'help' || !sub) { if (sub === 'help' || !sub) {
termLog(' kpn hello — iloinen tervehdys verkosta', '#a5d6ff'); termLog(' kpn hello — iloinen tervehdys verkosta', '#a5d6ff');
termLog(' kpn run &lt;malli&gt; "&lt;prompti&gt;" — aja tehtävä verkossa', '#a5d6ff'); termLog(' kpn run &lt;malli&gt; "&lt;prompti&gt;" — aja tehtävä verkossa', '#a5d6ff');
termLog(' kpn pipeline "&lt;tehtävä&gt;" — manageri → koodari → testaaja', '#a5d6ff'); termLog(' kpn pipeline "&lt;tehtävä&gt;" — nopea: manageri → koodari → testaaja', '#a5d6ff');
termLog(' kpn project "&lt;kuvaus&gt;" — projekti: tiedostojako + generointi + review', '#a5d6ff');
termLog(' kpn load — lataa kielimalli omalle koneelle', '#a5d6ff'); termLog(' kpn load — lataa kielimalli omalle koneelle', '#a5d6ff');
termLog(' kpn status — verkon tila', '#a5d6ff'); termLog(' kpn status — verkon tila', '#a5d6ff');
termLog(' kpn models — käytettävissä olevat mallit', '#a5d6ff'); termLog(' kpn models — käytettävissä olevat mallit', '#a5d6ff');
@@ -1926,13 +2011,26 @@
} }
if (sub === 'pipeline') { if (sub === 'pipeline') {
const afterPipeline = cmd.replace(/^kpn\s+pipeline\s*/, ''); const afterCmd = cmd.replace(/^kpn\s+pipeline\s*/, '');
const pMatch = afterPipeline.match(/^"(.+)"$|^'(.+)'$|^(.+)$/); const pMatch = afterCmd.match(/^"(.+)"$|^'(.+)'$|^(.+)$/);
const pTask = (pMatch && (pMatch[1] || pMatch[2] || pMatch[3] || '')).trim(); const pTask = (pMatch && (pMatch[1] || pMatch[2] || pMatch[3] || '')).trim();
if (!pTask) { if (!pTask) {
termLog(' Käyttö: kpn pipeline "&lt;tehtävä&gt;"', '#f85149'); termLog(' Käyttö: kpn pipeline "&lt;tehtävä&gt;"', '#f85149');
return; return;
} }
kpnPipelineSimple(pTask);
return;
}
if (sub === 'project') {
const afterCmd = cmd.replace(/^kpn\s+project\s*/, '');
const pMatch = afterCmd.match(/^"(.+)"$|^'(.+)'$|^(.+)$/);
const pTask = (pMatch && (pMatch[1] || pMatch[2] || pMatch[3] || '')).trim();
if (!pTask) {
termLog(' Käyttö: kpn project "&lt;projektin kuvaus&gt;"', '#f85149');
termLog(' Esim: kpn project "FastAPI + SQLite REST API for users"', '#8b949e');
return;
}
kpnPipeline(pTask); kpnPipeline(pTask);
return; return;
} }
@@ -1969,7 +2067,7 @@
// Tab-completion: ennustava komennonsyöttö sana kerrallaan // Tab-completion: ennustava komennonsyöttö sana kerrallaan
const kpnCommands = { const kpnCommands = {
'kpn': ['help', 'run', 'pipeline', 'load', 'status', 'models', 'hello', 'clear'], 'kpn': ['help', 'run', 'project', 'pipeline', 'load', 'status', 'models', 'hello', 'clear'],
'kpn run': ['coder', 'coder-3b', 'manager', 'tester', 'qa', 'data', 'observer', 'qwen-coder', 'qwen-coder-3b', 'smollm-135m', 'qwen-05b', 'phi3-mini'], 'kpn run': ['coder', 'coder-3b', 'manager', 'tester', 'qa', 'data', 'observer', 'qwen-coder', 'qwen-coder-3b', 'smollm-135m', 'qwen-05b', 'phi3-mini'],
'kpn load': ['1', '2'], 'kpn load': ['1', '2'],
'kpn pipeline': ['"'], 'kpn pipeline': ['"'],
@@ -1980,6 +2078,7 @@
'kpn run coder-3b': ['"binary search tree in rust"', '"REST API with Flask"', '"async web scraper in python"'], 'kpn run coder-3b': ['"binary search tree in rust"', '"REST API with Flask"', '"async web scraper in python"'],
'kpn run manager': ['"suunnittele REST API"', '"priorisoi tiimin tehtävät"'], 'kpn run manager': ['"suunnittele REST API"', '"priorisoi tiimin tehtävät"'],
'kpn run tester': ['"testaa login-toiminto"'], 'kpn run tester': ['"testaa login-toiminto"'],
'kpn project': ['"FastAPI + SQLite REST API for users"', '"Flask todo app with database"', '"CLI tool for CSV processing in Python"'],
'kpn pipeline': ['"rakenna todo-sovellus"', '"tee laskin pythonilla"'], 'kpn pipeline': ['"rakenna todo-sovellus"', '"tee laskin pythonilla"'],
}; };