diff --git a/ventures/agentlore/README.md b/ventures/agentlore/README.md new file mode 100644 index 0000000..28b9d03 --- /dev/null +++ b/ventures/agentlore/README.md @@ -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//` 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.” diff --git a/ventures/agentlore/site/app.js b/ventures/agentlore/site/app.js new file mode 100644 index 0000000..de8ba47 --- /dev/null +++ b/ventures/agentlore/site/app.js @@ -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(); diff --git a/ventures/agentlore/site/core.js b/ventures/agentlore/site/core.js new file mode 100644 index 0000000..1e7dc53 --- /dev/null +++ b/ventures/agentlore/site/core.js @@ -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/\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\n\n'+rows.map(r=>`\n${/^[-*+]\s/.test(r.text)?r.text:'- '+r.text}\n`).join('\n');} diff --git a/ventures/agentlore/site/index.html b/ventures/agentlore/site/index.html new file mode 100644 index 0000000..5d46b7a --- /dev/null +++ b/ventures/agentlore/site/index.html @@ -0,0 +1,9 @@ + +AgentLore — instructions worth carrying +
AgentLoreAL / 01Early access ↗
+

NOTPLANNED / A WORKING TASTE TEST

Good instructions.
Worth carrying.

Find the rules hiding in public repositories. Keep the useful ones, trace every line, and take a reviewed AGENTS.md into your next project.

Read-only · No tokens · No automatic repository changes

+
01 / FIND THE LORE

Your scan runs in this browser using GitHub’s public API.

+ +
03 / HELP SHAPE THE NEXT CHAPTER

Bring a real use case.

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.

Request early access on GitHub ↗

Requires a GitHub account. Review the issue before submitting. Never include private code, tokens, or credentials.

+ +
diff --git a/ventures/agentlore/site/style.css b/ventures/agentlore/site/style.css new file mode 100644 index 0000000..1b04bd7 --- /dev/null +++ b/ventures/agentlore/site/style.css @@ -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}} diff --git a/ventures/agentlore/test.mjs b/ventures/agentlore/test.mjs new file mode 100644 index 0000000..341e319 --- /dev/null +++ b/ventures/agentlore/test.mjs @@ -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.');