Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion extract-knowhow/commands/extract-knowhow.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,27 @@ Replace specific references with generic descriptions: "our internal dataset"
- Standard textbook knowledge with no novel application
- Any personally identifiable information

### Single-Pass Peer Review

After generating each skill item, perform **exactly one** peer review using a different AI agent family. Do not skip this when a partner agent is available, and do not let the item bounce through multiple reviewers.

- If the current agent is **Claude Code**, ask **Codex CLI** or **Gemini** to review.
- If the current agent is **Codex CLI**, ask **Claude Code** or **Gemini** to review.
- If the current agent is **Gemini**, ask **Codex CLI** or **Claude Code** to review.
- Do not request a second review after the first peer responds.
- Do not ask the reviewer to trigger another reviewer.

The peer review should check:
- scientific correctness
- transferability beyond the original project
- replicability of the reasoning protocol
- de-identification and privacy
- whether the item is genuinely worth reusing

If the reviewer finds problems, revise the skill once using that feedback, but **do not** send the revised version out for another review pass.

If no partner agent is available in the current environment, record that explicitly instead of pretending review happened.

### Output per item
```json
{
Expand All @@ -173,7 +194,12 @@ Replace specific references with generic descriptions: "our internal dataset"
"reasoning_steps": ["Step 1...", "Step 2..."],
"tools": ["tool — what it does"],
"pitfalls": ["Mistake and how to avoid"],
"confidence": "high | medium | low"
"confidence": "high | medium | low",
"peer_review": {
"reviewer_agent": "claude-code | codex-cli | gemini | unavailable",
"review_status": "approved | revise | needs_external_review",
"notes": ["Short review findings"]
}
}
```

Expand Down
23 changes: 22 additions & 1 deletion extract-knowhow/templates/report.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
.skill-title{font-weight:600;flex:1;min-width:0}
.skill-conf{font-size:.75rem;color:#8b949e;white-space:nowrap}
.skill-desc{color:#8b949e;font-size:.9rem;margin:.5rem 0 0 2.25rem}
.skill-review{color:#8b949e;font-size:.82rem;margin:.35rem 0 0 2.25rem}
.skill-review strong{color:#e6edf3}
.expand-btn{font-size:.8rem;color:#58a6ff;cursor:pointer;margin-left:2.25rem;margin-top:.25rem;background:none;border:none;padding:0}
.expand-btn:hover{text-decoration:underline}
.gh-btn{font-size:.75rem;background:#238636;color:#fff;border:none;padding:3px 10px;border-radius:4px;cursor:pointer;margin-left:.5rem;text-decoration:none;display:inline-block}
Expand Down Expand Up @@ -144,6 +146,13 @@ <h1>🏛️ OpenScientist</h1>
if(!arr||!arr.length) return '<ul><li><span class="editable" contenteditable="true">N/A</span></li></ul>';
return '<ul>'+arr.map(item=>`<li><span class="editable ${cls||''}" contenteditable="true">${md(item)}</span></li>`).join('')+'</ul>';
}
function normReview(pr){
return {
reviewer_agent: pr && pr.reviewer_agent ? pr.reviewer_agent : 'unavailable',
review_status: pr && pr.review_status ? pr.review_status : 'needs_external_review',
notes: pr && Array.isArray(pr.notes) && pr.notes.length ? pr.notes : ['Peer review was not recorded in this run.']
};
}

projects.forEach((proj, pi) => {
let h = `<div class="project"><div class="project-header">
Expand All @@ -154,6 +163,7 @@ <h1>🏛️ OpenScientist</h1>
proj.skills.forEach((sk, si) => {
const id = `sk-${pi}-${si}`;
const cn = sk.category.split('-')[0];
const pr = normReview(sk.peer_review);
h += `<div class="skill" id="${id}-w">
<div class="skill-top">
<input type="checkbox" class="skill-cb" id="${id}" checked onchange="tog('${id}')">
Expand All @@ -162,6 +172,7 @@ <h1>🏛️ OpenScientist</h1>
<span class="skill-conf">${esc(sk.confidence)}</span>
</div>
<div class="skill-desc">${mkEditable(sk.description, 'ed-desc')}</div>
<div class="skill-review"><strong>Peer review:</strong> ${mkEditable(pr.review_status, 'ed-pr-status')} by ${mkEditable(pr.reviewer_agent, 'ed-pr-agent')}</div>
<div class="skill-actions">
<button class="expand-btn" onclick="exp('${id}')" style="margin:0">▶ Show details</button>
<a class="gh-btn" onclick="openIssue(${pi},${si}); return false;" href="#">Submit to GitHub</a>
Expand All @@ -171,6 +182,7 @@ <h4>Domain Knowledge</h4>${mkEditable(sk.domain_knowledge, 'ed-dk')}
<h4>Reasoning Protocol</h4>${mkEditableList(sk.reasoning_steps, 'ed-rs')}
<h4>Tools</h4>${mkEditableList(sk.tools, 'ed-tools')}
<h4>Common Pitfalls</h4>${mkEditableList(sk.pitfalls, 'ed-pit')}
<h4>Peer Review Notes</h4>${mkEditableList(pr.notes, 'ed-pr-note')}
</div>
</div>`;
});
Expand Down Expand Up @@ -213,6 +225,9 @@ <h4>Common Pitfalls</h4>${mkEditableList(sk.pitfalls, 'ed-pit')}
const rs = readEditableList(detail, 'ed-rs');
const tools = readEditableList(detail, 'ed-tools');
const pit = readEditableList(detail, 'ed-pit');
const prAgent = readEditable(wrap, 'ed-pr-agent') || normReview(sk.peer_review).reviewer_agent;
const prStatus = readEditable(wrap, 'ed-pr-status') || normReview(sk.peer_review).review_status;
const prNotes = readEditableList(detail, 'ed-pr-note');

const slug = title.toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'');
const subLabel = [proj.domain, proj.subdomain].map(s=>s.split('-').map(w=>w.charAt(0).toUpperCase()+w.slice(1)).join(' ')).join(' / ');
Expand Down Expand Up @@ -257,6 +272,12 @@ <h4>Common Pitfalls</h4>${mkEditableList(sk.pitfalls, 'ed-pit')}

${pit.length ? pit.map(p=>'- '+p).join('\n') : '- N/A'}

## Peer Review

- Reviewer agent: ${prAgent}
- Review status: ${prStatus}
${prNotes.length ? prNotes.map(n=>'- '+n).join('\n') : '- Peer review notes not recorded.'}

## References

- Extracted by /extract-knowhow on ${date}
Expand All @@ -272,7 +293,7 @@ <h4>Common Pitfalls</h4>${mkEditableList(sk.pitfalls, 'ed-pit')}
category: sk.category + ' — ' + catLabel,
skill_content: skillMd,
generation_method: 'Generated by /extract-knowhow, then reviewed by me',
notes: 'Confidence: ' + sk.confidence
notes: 'Confidence: ' + sk.confidence + '; Peer review: ' + prStatus + ' by ' + prAgent
});

return 'https://github.com/OpenScientists/OpenScientist/issues/new?' + params.toString();
Expand Down
49 changes: 49 additions & 0 deletions extract-knowhow/tests/test-postinstall.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const { execFileSync } = require("child_process");
const COMMANDS_DIR = path.join(os.homedir(), ".claude", "commands");
const TARGET = path.join(COMMANDS_DIR, "extract-knowhow.md");
const SCRIPT_DIR = path.join(__dirname, "..", "scripts");
const TEMPLATE = path.join(__dirname, "..", "templates", "report.html");

let passed = 0;
let failed = 0;
Expand All @@ -35,6 +36,54 @@ assert(fs.existsSync(TARGET), "Command file exists after install");
const content = fs.readFileSync(TARGET, "utf-8");
assert(content.includes("extract-knowhow"), "Command file contains expected content");
assert(content.startsWith("#"), "Command file starts with markdown header");
assert(content.includes("perform **exactly one** peer review using a different AI agent family"), "Command requires exactly one peer review");
assert(content.includes("If the current agent is **Codex CLI**, ask **Claude Code** or **Gemini** to review."), "Command defines Codex peer review routing");
assert(content.includes("\"peer_review\":"), "Command output schema includes peer review metadata");

console.log("\nTest: report template peer review rendering");
const template = fs.readFileSync(TEMPLATE, "utf-8");
assert(template.includes("Peer Review"), "Template includes peer review section");
assert(template.includes("review_status"), "Template includes peer review status handling");
const sampleHtml = template.replace(
"const DATA = __REPORT_DATA__;",
"const DATA = " + JSON.stringify({
author: "Test User",
email: "test@example.com",
total_sessions: 1,
date: "2026-04-03",
projects: [{
name: "Test Project",
domain: "computer-science",
subdomain: "machine-learning",
session_count: 1,
skills: [{
title: "Test skill",
category: "06-coding-and-execution",
description: "Test description",
domain_knowledge: "Test knowledge",
reasoning_steps: ["Step 1"],
tools: ["tool — role"],
pitfalls: ["pitfall"],
confidence: "high",
peer_review: {
reviewer_agent: "claude-code",
review_status: "approved",
notes: ["Looks reusable."]
}
}]
}]
}) + ";"
);
const scriptMatch = sampleHtml.match(/<script>([\s\S]*)<\/script>/);
assert(Boolean(scriptMatch), "Template contains a script block");
if (scriptMatch) {
try {
new Function(scriptMatch[1]);
assert(true, "Template script parses after peer review data injection");
} catch (err) {
assert(false, "Template script parses after peer review data injection");
}
}

console.log("\nTest: postuninstall.js");
execFileSync(process.execPath, [path.join(SCRIPT_DIR, "postuninstall.js")], { stdio: "pipe" });
Expand Down