feat(agentlore): read-only public instruction taste test (#87)

This commit is contained in:
matcha committed 2026-10-12 19:21:58 +00:00
1 parent 8b4f44c701
commit d6d199df1b
6 files changed
+61

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
# AgentLore
Working NotPlanned taste test for issue #87. Public URL: https://notplanned.teahub.xyz/v/agentlore/
Buildless HTML/CSS/ES modules. Serve `site/` with any static HTTP server; run `node test.mjs` for core checks. Reuses hb-5 mac-sites and the existing `/v/<slug>/` venture pipeline. No backend, tokens, paid APIs, outbound notifications, or repository write path. GitHub requests explicitly omit credentials.
Scan up to three public GitHub repository URLs. Resolve the default branch commit, discover named instruction documents in its Git tree, read bounded blobs, extract exact instruction-like lines outside fenced code. Review candidate scope and duplicate/possible-conflict flags, select rules, preview and download AGENTS.md with pinned source links and original scope. Issue-based early access uses the existing teahubxyz/time intake; the user reviews and submits the issue themselves.
Limits: unauthenticated GitHub rate limit; recursive trees may truncate; 12 files/repo, 64 KiB/file, 240 candidates total. No semantic rewriting or language model. Heuristics can miss prose and subtle conflicts, and scope classification needs human judgment. Referenced docs are not traversed. License/context review remains with the user. Coverage and failed reads are shown. The app uses textContent for untrusted source text and never evaluates it.
Ready to send: “AgentLore finds useful agent instructions in public GitHub repositories and lets you carry a reviewed set into your next project. Every candidate links to its original commit and line. Try it, select what fits, and download AGENTS.md. It never writes to your repositories. Share a real use case through the GitHub early-access issue link.”
+11
View File
@@ -0,0 +1,11 @@
import {parseRepos,eligible,extract,annotate,exportMarkdown} from './core.js';
const $=id=>document.getElementById(id);let rows=[],selected=new Set(),coverage=[];
function el(tag,text,cls){const n=document.createElement(tag);if(text)n.textContent=text;if(cls)n.className=cls;return n;}
async function api(path){const r=await fetch('https://api.github.com/'+path,{credentials:'omit',headers:{Accept:'application/vnd.github+json'}});if(!r.ok)throw Error(r.status===403||r.status===429?'GitHub public API rate limit reached. Try again later.':`GitHub ${r.status}: repository or file unavailable.`);return r.json();}
function onboarding(){const body='AgentLore early-access request\n\nPublic repositories: '+$('repos').value+'\n\nUse case / desired reusable instructions:\n\nMissing coverage or confusing results:\n\nI understand this is a request, not an activated service. No credentials or private code included.';$('onboard').href='https://github.com/teahubxyz/time/issues/new?'+new URLSearchParams({template:'early-access.yml',title:'Early access: AgentLore',venture:'agentlore',use:body,body});}
function update(){const chosen=rows.filter((r,i)=>selected.has(i));$('output').value=exportMarkdown(chosen);$('count').textContent=chosen.length+' selected · source provenance included';$('download').disabled=!chosen.length;onboarding();}
function render(){const target=$('rules');target.replaceChildren();rows.forEach((r,i)=>{if($('filter').value!=='all'&&r.kind!==$('filter').value)return;const card=el('article',null,'rule'),check=el('input');check.type='checkbox';check.id='rule-'+i;check.checked=selected.has(i);check.addEventListener('change',()=>{check.checked?selected.add(i):selected.delete(i);update();});const content=el('div'),label=el('label',r.text);label.htmlFor=check.id;content.append(label);const meta=el('div',null,'meta'),source=el('a',`${r.repo} / ${r.path}:${r.line}`);source.href=r.url;source.target='_blank';source.rel='noopener noreferrer';meta.append(el('span',r.kind==='reusable'?'Likely reusable':'Project-specific / context needed','flag'),source);for(const f of r.flags)meta.append(el('span',f,'flag warning'));content.append(meta);card.append(check,content);target.append(card);});update();}
async function scan(){let repos;try{repos=parseRepos($('repos').value);}catch(e){$('status').textContent=e.message;return;}$('scan').disabled=true;$('example').disabled=true;rows=[];selected.clear();coverage=[];$('review').hidden=true;$('coverage').replaceChildren();
try{for(const repo of repos){$('status').textContent='Scanning '+repo+'…';const report={repo,files:0,messages:[]};coverage.push(report);try{const info=await api('repos/'+repo);if(info.private)throw Error('Only public repositories are supported.');const commit=await api(`repos/${repo}/commits/${encodeURIComponent(info.default_branch)}`);const sha=commit.sha;report.sha=sha;const tree=await api(`repos/${repo}/git/trees/${sha}?recursive=1`);if(tree.truncated)report.messages.push('Tree truncated: coverage is incomplete.');const found=tree.tree.filter(f=>f.type==='blob'&&eligible(f.path)).sort((a,b)=>a.path.split('/').length-b.path.split('/').length||a.path.localeCompare(b.path));if(!found.length)report.messages.push('No supported instruction files found. This does not prove no instructions exist.');if(found.length>12)report.messages.push(`${found.length-12} files beyond the 12-file limit were not read.`);for(const file of found.slice(0,12)){if(file.size>65536){report.messages.push(file.path+': skipped (over 64 KiB).');continue;}try{const blob=await api(`repos/${repo}/git/blobs/${file.sha}`);if(blob.encoding!=='base64'||blob.size>65536)throw Error('Unsupported encoding or size.');const bytes=Uint8Array.from(atob(blob.content.replace(/\s/g,'')),c=>c.charCodeAt(0));const text=new TextDecoder('utf-8',{fatal:true}).decode(bytes);const extracted=extract(text,repo,file.path,sha);report.files++;report.messages.push(`${file.path}: ${extracted.length} candidate lines.`);rows.push(...extracted.slice(0,Math.max(0,240-rows.length)));if(rows.length>=240){report.messages.push('240-candidate limit reached; remaining candidates omitted.');break;}}catch(e){report.messages.push(file.path+': '+e.message);}}}catch(e){report.messages.push(e.message);}}
annotate(rows);for(const r of coverage){const block=el('div',null,'coverage');block.append(el('strong',`${r.repo} · ${r.files} files read${r.sha?' · commit '+r.sha.slice(0,12):''}`));for(const m of r.messages)block.append(el('div',m));$('coverage').append(block);}$('status').textContent=`Scan complete: ${rows.length} candidate lines. Review coverage and select rules below.`;$('review').hidden=false;render();}finally{$('scan').disabled=false;$('example').disabled=false;}}
$('scan-form').addEventListener('submit',e=>{e.preventDefault();scan();});$('example').onclick=()=>{$('repos').value='https://github.com/openai/codex';scan();};$('filter').onchange=render;$('select-reusable').onclick=()=>{rows.forEach((r,i)=>{if(r.kind==='reusable'&&!r.flags.length)selected.add(i);});render();};$('clear').onclick=()=>{selected.clear();render();};$('repos').oninput=onboarding;$('download').onclick=()=>{const url=URL.createObjectURL(new Blob([$('output').value],{type:'text/markdown;charset=utf-8'}));const a=el('a');a.href=url;a.download='AGENTS.md';a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);};onboarding();
+20
View File
@@ -0,0 +1,20 @@
export function parseRepos(input) {
const urls=input.trim().split(/\s+/).filter(Boolean);
if(!urls.length||urls.length>3) throw Error('Enter one to three public GitHub repository URLs.');
return [...new Set(urls.map(s=>{const u=new URL(s);const m=u.pathname.match(/^\/([\w.-]+)\/([\w.-]+)\/?$/);if(u.protocol!=='https:'||u.hostname!=='github.com'||u.username||u.password||u.search||u.hash||!m)throw Error('Use https://github.com/owner/repository URLs without branches or query strings.');return m[1]+'/'+m[2].replace(/\.git$/,'');}))];
}
export function eligible(path) {return /(^|\/)(AGENTS|CLAUDE|CONTRIBUTING|copilot-instructions|instructions|rules)\.md$/i.test(path)||/(^|\/)\.cursor\/rules\/[^/]+\.mdc$/i.test(path);}
export function extract(text,repo,path,sha) {
let fenced=false;const rows=[];
text.split(/\r?\n/).forEach((line,i)=>{if(/^\s*(```|~~~)/.test(line)){fenced=!fenced;return;}if(fenced)return;
const plain=line.replace(/^\s*(?:[-*+]\s+|\d+[.)]\s+)/,'').trim();
if(plain.length<15||plain.length>800||/^\s*[#>|]/.test(line)||!(/\b(must|should|always|never|avoid|prefer|ensure|do not|don't|required)\b/i.test(plain)||/^(use|run|keep|write|test|check|add|follow|make|include|read|verify)\b/i.test(plain)))return;
const project=path.includes('/')||/`|\b(npm|pnpm|yarn|cargo|pytest|docker|this repo|this project|directory|package|component)\b|\.[a-z]{1,4}\b|https?:\/\//i.test(plain);
rows.push({text:line.trim(),repo,path,sha,line:i+1,kind:project?'project':'reusable',url:`https://github.com/${repo}/blob/${sha}/${path.split('/').map(encodeURIComponent).join('/')}#L${i+1}`,flags:[]});});return rows;
}
export function annotate(rows) {
const groups=new Map();for(const r of rows){const key=r.text.toLowerCase().replace(/[^a-z0-9 ]/g,'').replace(/\s+/g,' ').trim();if(groups.has(key)){r.flags.push('Duplicate');for(const other of groups.get(key))if(!other.flags.includes('Duplicate'))other.flags.push('Duplicate');groups.get(key).push(r);}else groups.set(key,[r]);}
for(let i=0;i<rows.length;i++)for(let j=i+1;j<rows.length;j++) {const a=rows[i],b=rows[j];const neg=s=>/\b(never|not|avoid|don't)\b/i.test(s);const words=s=>new Set(s.toLowerCase().match(/[a-z]{4,}/g)?.filter(w=>!['never','always','should','must','avoid','prefer'].includes(w))||[]);const wa=words(a.text),wb=words(b.text);const common=[...wa].filter(w=>wb.has(w)).length;if(neg(a.text)!==neg(b.text)&&common>=2&&common/Math.max(wa.size,wb.size)>.5){for(const r of [a,b])if(!r.flags.includes('Possible conflict'))r.flags.push('Possible conflict');}}
return rows;
}
export function exportMarkdown(rows) {return '# AGENTS.md\n\n<!-- Human-reviewed exact excerpts. Recheck source license, context and scope before adopting. -->\n\n'+rows.map(r=>`<!-- Source: ${r.url}; original scope: ${r.path.includes('/')?r.path.slice(0,r.path.lastIndexOf('/')):'repository root'} -->\n${/^[-*+]\s/.test(r.text)?r.text:'- '+r.text}\n`).join('\n');}
+9
View File
@@ -0,0 +1,9 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>AgentLore — instructions worth carrying</title><link rel="stylesheet" href="style.css"><script type="module" src="app.js"></script></head>
<body><header><a href="./" class="brand">AgentLore<span>AL / 01</span></a><a href="#access">Early access ↗</a></header><main>
<section class="intro"><p class="eyebrow">NOTPLANNED / A WORKING TASTE TEST</p><h1>Good instructions.<br>Worth carrying.</h1><p class="lede">Find the rules hiding in public repositories. Keep the useful ones, trace every line, and take a reviewed AGENTS.md into your next project.</p><p class="quiet">Read-only · No tokens · No automatic repository changes</p></section>
<section class="workspace"><div class="step">01 / FIND THE LORE</div><form id="scan-form"><label for="repos">Public GitHub repositories <span>one per line, up to three</span></label><textarea id="repos" rows="3" placeholder="https://github.com/openai/codex" required></textarea><div class="actions"><button id="scan" type="submit">Scan repositories ↗</button><button id="example" type="button" class="secondary">Try openai/codex</button></div></form><p id="status" role="status" aria-live="polite">Your scan runs in this browser using GitHub’s public API.</p><div id="coverage"></div></section>
<section id="review" hidden><div class="step">02 / REVIEW, THEN CARRY</div><h2>Every rule has a source.</h2><p>Nothing is preselected. Reusability and conflicts are heuristic suggestions; read the original context before adopting a rule.</p><div class="actions"><label for="filter">Show</label><select id="filter"><option value="all">All candidates</option><option value="reusable">Likely reusable</option><option value="project">Project-specific / context needed</option></select><button id="select-reusable" class="secondary">Select likely reusable</button><button id="clear" class="secondary">Clear selection</button></div><div id="rules"></div><div class="export"><h2>Your reviewed AGENTS.md</h2><p id="count">0 selected</p><textarea id="output" rows="10" readonly aria-label="Export preview"></textarea><button id="download" disabled>Download AGENTS.md ↓</button></div></section>
<section id="access"><div class="step">03 / HELP SHAPE THE NEXT CHAPTER</div><h2>Bring a real use case.</h2><p>This taste test is free. For early access, open a GitHub issue with your public repositories, the rules you want to carry, and the gaps you found. A maintainer confirms next steps on the issue; submitting does not activate a service.</p><a id="onboard" class="button" target="_blank" rel="noopener noreferrer">Request early access on GitHub ↗</a><p class="quiet">Requires a GitHub account. Review the issue before submitting. Never include private code, tokens, or credentials.</p></section>
<footer><h3>Honest limits</h3><p>We inspect AGENTS.md, CLAUDE.md, CONTRIBUTING.md, copilot-instructions.md and Markdown files named instructions or rules, plus .cursor/rules/*.mdc. Up to 12 files per repository, 64 KiB per file, 240 candidate lines overall. Truncated trees, skipped files and missing coverage are shown. Exact instruction-like lines are extracted without a language model; prose, code blocks, subtle conflicts and linked documents can be missed. GitHub’s unauthenticated API rate limit applies. Public source may still contain sensitive text: review before exporting. Nested instructions retain their original scope.</p><p>Made in NotPlanned. Source text is evidence, never an instruction to this app.</p></footer>
</main></body></html>
+1
View File
@@ -0,0 +1 @@
:root{font-family:Inter,ui-sans-serif,system-ui,sans-serif;color:#252b25;background:#f4f3eb;font-size:16px}*{box-sizing:border-box}body{margin:0}header,main{max-width:1120px;margin:auto}header{display:flex;justify-content:space-between;align-items:center;padding:28px 32px;border-bottom:1px solid #cdd0c3}a{color:inherit;text-underline-offset:4px}.brand{text-decoration:none;font-weight:850;font-size:24px}.brand span{font:12px monospace;margin-left:16px;color:#68745f}main{padding:0 32px}.intro{padding:64px 0 46px;max-width:850px}.eyebrow,.step{font:12px ui-monospace,monospace;letter-spacing:2px;color:#54704b}h1{font-size:clamp(44px,7vw,86px);line-height:1.02;letter-spacing:-4px;margin:22px 0 25px}h2{font-size:30px;letter-spacing:-1px}p{line-height:1.6}.lede{font-size:21px;max-width:650px}.quiet{font-size:13px;color:#596455}.workspace{background:#e5eadc;border:1px solid #b6c3a7;padding:30px}.step{margin-bottom:20px}label{display:block;font-weight:650;margin-bottom:10px}label span{font-size:13px;font-weight:400}textarea{width:100%;resize:vertical;border:1px solid #859278;padding:15px;font:14px/1.6 ui-monospace,monospace;background:#fcfcf7;color:#252b25;border-radius:2px}.actions{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin:16px 0}button,.button{display:inline-block;background:#304c2d;color:white;padding:13px 20px;border:1px solid #304c2d;font:600 14px system-ui;cursor:pointer;text-decoration:none;border-radius:3px}button:disabled{opacity:.45;cursor:default}.secondary{background:transparent;color:#304c2d}select{padding:10px;max-width:100%;background:#fcfcf7}#status{font-size:14px}.coverage{padding:12px 0;border-top:1px solid #b6c3a7;font-size:13px;overflow-wrap:anywhere}#review,#access{padding:46px 0;border-bottom:1px solid #cdd0c3}.rule{display:grid;grid-template-columns:24px 1fr;gap:12px;padding:20px 0;border-bottom:1px solid #d1d5c8}.rule input{width:19px;height:19px;accent-color:#304c2d}.rule label{white-space:pre-wrap;overflow-wrap:anywhere;font-weight:500;line-height:1.55}.meta{font-size:12px;color:#56634e;display:flex;gap:10px;flex-wrap:wrap;overflow-wrap:anywhere}.flag{background:#e4e8dc;padding:3px 8px}.warning{background:#ffe1bd;color:#694715}.export{margin-top:32px;padding:25px;background:#e5eadc}footer{font-size:13px;color:#596455;padding:35px 0 55px;max-width:800px}a:focus-visible,button:focus-visible,input:focus-visible{outline:3px solid #a26e29;outline-offset:3px}@media(max-width:600px){header{padding:22px 18px}.brand span{display:none}main{padding:0 18px}.intro{padding-top:42px}h1{letter-spacing:-2px}.workspace{padding:20px}.lede{font-size:18px}.actions button{width:100%}.export{padding:18px}}
+9
View File
@@ -0,0 +1,9 @@
import assert from 'node:assert/strict';
import {parseRepos,eligible,extract,annotate,exportMarkdown} from './site/core.js';
assert.deepEqual(parseRepos('https://github.com/openai/codex'),['openai/codex']);
for(const s of ['http://github.com/a/b','https://evil.example/a/b','https://github.com/a/b?token=x'])assert.throws(()=>parseRepos(s));
assert(eligible('docs/AGENTS.md'));assert(eligible('.cursor/rules/testing.mdc'));assert(!eligible('.env'));assert(!eligible('secrets.md'));
const text='# Rules\n- Always test changes before publishing.\n```\nNever invent this rule.\n```\n- Run `npm test` in this repo.\n';
const rows=extract(text,'example/repo','AGENTS.md','abcdef');assert.equal(rows.length,2);assert.equal(rows[0].line,2);assert.equal(rows[0].kind,'reusable');assert.equal(rows[1].kind,'project');assert(rows[0].url.endsWith('#L2'));assert(exportMarkdown(rows).includes(rows[0].url));
const flags=annotate(extract('- Always use tabs for indentation.\n- Never use tabs for indentation.\n- Always use tabs for indentation.','a/b','AGENTS.md','abc'));assert(flags[0].flags.includes('Duplicate'));assert(flags[0].flags.includes('Possible conflict'));
console.log('PASS: URL allowlist, file allowlist, fenced-code exclusion, line provenance, scope, duplicates/conflicts, export.');