@hybridlabor-api/aos 4.11.0 → 4.12.1
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/README.de.md +296 -725
- package/README.md +294 -831
- package/README.pt.md +316 -677
- package/bin/aos-store.mjs +29 -59
- package/installer.js +39 -2
- package/lib/ecc-store-index.json +1 -1
- package/lib/store-shared.mjs +133 -0
- package/lib/store-ui/index.html +150 -0
- package/lib/store-ui/server.mjs +180 -0
- package/package.json +2 -2
- package/scripts/build-ecc-store-index.mjs +17 -1
- package/scripts/build-plugin-manifest.mjs +3 -3
- package/skills/bdb-aos/SKILL.md +46 -0
- package/skills/global_config/aos-store/SKILL.md +71 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
5
|
+
|
|
6
|
+
export const MAX_FILES = 200;
|
|
7
|
+
export const MAX_BYTES = 5 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
function safeRel(path) {
|
|
10
|
+
if (typeof path !== 'string' || !path || path.startsWith('/') || /[\\\0]/.test(path)
|
|
11
|
+
|| path.split('/').some((seg) => seg === '' || seg === '.' || seg === '..')) {
|
|
12
|
+
throw new Error(`Unsafe file path in store index: ${JSON.stringify(path)}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function validateFiles(files) {
|
|
17
|
+
if (files.length > MAX_FILES) throw new Error(`Too many files (${files.length} > ${MAX_FILES})`);
|
|
18
|
+
let total = 0;
|
|
19
|
+
const seen = new Set();
|
|
20
|
+
for (const f of files) {
|
|
21
|
+
safeRel(f.path);
|
|
22
|
+
if (seen.has(f.path)) throw new Error(`Duplicate file path: ${f.path}`);
|
|
23
|
+
seen.add(f.path);
|
|
24
|
+
if (f.size !== undefined) {
|
|
25
|
+
if (!Number.isInteger(f.size) || f.size < 0) throw new Error(`Invalid size for ${f.path}`);
|
|
26
|
+
total += f.size;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (total > MAX_BYTES) throw new Error(`Skill too large (${total} > ${MAX_BYTES} bytes)`);
|
|
30
|
+
return files;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// True when every file of the item exists (size checked when the index has it); no hashing.
|
|
34
|
+
export function isInstalled(type, name, item, opts) {
|
|
35
|
+
const targets = targetsFor(type, name, opts);
|
|
36
|
+
if (type !== 'skills' || !(item.files?.length > 1)) return targets.every(existsSync);
|
|
37
|
+
return targets.every((t) => fileTargets(t, item.files).every((p, i) => {
|
|
38
|
+
try { const st = statSync(p); return st.isFile() && st.size === item.files[i].size; } catch { return false; }
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const fileTargets = (skillMd, files) => files.map((f) => join(dirname(skillMd), ...f.path.split('/')));
|
|
43
|
+
|
|
44
|
+
// Downloads and verifies everything in memory; nothing touches disk.
|
|
45
|
+
export async function fetchSkillFiles(rawBase, commit, upstreamDir, files) {
|
|
46
|
+
validateFiles(files);
|
|
47
|
+
const out = [];
|
|
48
|
+
let total = 0;
|
|
49
|
+
for (const f of files) {
|
|
50
|
+
const url = `${rawBase}/${commit}/${upstreamDir}/${f.path}`;
|
|
51
|
+
const res = await fetch(url, { headers: { 'user-agent': 'aos-store' } });
|
|
52
|
+
if (!res.ok) throw new Error(`Download failed (${res.status}): ${url}`);
|
|
53
|
+
const data = Buffer.from(await res.arrayBuffer());
|
|
54
|
+
total += data.length;
|
|
55
|
+
if (total > MAX_BYTES) throw new Error(`Skill too large (> ${MAX_BYTES} bytes)`);
|
|
56
|
+
if (f.size !== undefined && data.length !== f.size) throw new Error(`Size mismatch for ${f.path}: expected ${f.size}, got ${data.length}`);
|
|
57
|
+
const digest = createHash('sha256').update(data).digest('hex');
|
|
58
|
+
if (digest !== f.sha256) throw new Error(`SHA-256 mismatch for ${f.path}: expected ${f.sha256}, got ${digest}`);
|
|
59
|
+
out.push({ ...f, data });
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function assertNoSymlink(path) {
|
|
65
|
+
try { if (lstatSync(path).isSymbolicLink()) throw new Error(`Refusing to write through symlink: ${path}`); }
|
|
66
|
+
catch (e) { if (e.code !== 'ENOENT') throw e; }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Checks every destination first, then writes; throws before any write on an unsafe path.
|
|
70
|
+
export function writeSkillFiles(skillMd, files) {
|
|
71
|
+
const dir = resolve(dirname(skillMd));
|
|
72
|
+
const plan = files.map((f) => {
|
|
73
|
+
safeRel(f.path);
|
|
74
|
+
const dest = resolve(dir, ...f.path.split('/'));
|
|
75
|
+
if (!dest.startsWith(dir + sep)) throw new Error(`Path escapes skill dir: ${f.path}`);
|
|
76
|
+
for (let p = dest; p !== dir; p = dirname(p)) assertNoSymlink(p);
|
|
77
|
+
assertNoSymlink(dir);
|
|
78
|
+
return { ...f, dest };
|
|
79
|
+
});
|
|
80
|
+
for (const f of plan) {
|
|
81
|
+
mkdirSync(dirname(f.dest), { recursive: true });
|
|
82
|
+
writeFileSync(f.dest, f.data);
|
|
83
|
+
if (f.exec) chmodSync(f.dest, 0o755);
|
|
84
|
+
}
|
|
85
|
+
return plan.map((f) => f.dest);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function localSkills(root) {
|
|
89
|
+
const found = new Map();
|
|
90
|
+
const visit = (dir) => {
|
|
91
|
+
if (!existsSync(dir)) return;
|
|
92
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
93
|
+
const fullPath = join(dir, entry.name);
|
|
94
|
+
if (entry.isDirectory()) visit(fullPath);
|
|
95
|
+
else if (entry.name === 'SKILL.md') found.set(basename(dirname(fullPath)), fullPath);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
visit(join(root, 'skills'));
|
|
99
|
+
return found;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function localAgents(root) {
|
|
103
|
+
const found = new Map();
|
|
104
|
+
for (const dir of [join(root, '.claude', 'agents'), join(root, '.agents', 'agents'), join(root, 'agents')]) {
|
|
105
|
+
if (!existsSync(dir)) continue;
|
|
106
|
+
for (const entry of readdirSync(dir)) found.set(entry.replace(/\.md$/i, ''), join(dir, entry));
|
|
107
|
+
}
|
|
108
|
+
return found;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function globalRoots(type) {
|
|
112
|
+
const home = homedir();
|
|
113
|
+
const sub = type === 'skills' ? 'skills' : 'agents';
|
|
114
|
+
return [
|
|
115
|
+
join(home, '.agents', sub),
|
|
116
|
+
join(home, '.claude', sub),
|
|
117
|
+
join(home, '.codex', sub),
|
|
118
|
+
join(home, '.cursor', sub),
|
|
119
|
+
join(home, '.roo', sub),
|
|
120
|
+
join(home, '.gemini', 'config', sub),
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function targetsFor(type, name, { project = false, cwd = process.cwd() } = {}) {
|
|
125
|
+
if (project) {
|
|
126
|
+
return type === 'skills'
|
|
127
|
+
? [join(cwd, 'skills', name, 'SKILL.md')]
|
|
128
|
+
: [join(cwd, 'agents', `${name}.md`)];
|
|
129
|
+
}
|
|
130
|
+
return globalRoots(type).map((root) => (type === 'skills'
|
|
131
|
+
? join(root, name, 'SKILL.md')
|
|
132
|
+
: join(root, `${name}.md`)));
|
|
133
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
3
|
+
<title>AOS Store</title>
|
|
4
|
+
<style>
|
|
5
|
+
:root{--paper:oklch(0.97 0.012 95);--paper-2:oklch(0.93 0.018 95);--ink:oklch(0.18 0.025 265);--muted:oklch(0.46 0.025 265);--line:oklch(0.84 0.018 95);--green:oklch(0.72 0.17 145);--green-deep:oklch(0.40 0.12 145);--white:oklch(0.995 0.006 95);--serif:Georgia,"Times New Roman",serif;--sans:"Avenir Next",Avenir,"Segoe UI",sans-serif;--radius:1rem}
|
|
6
|
+
*{box-sizing:border-box}body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--sans);line-height:1.5}
|
|
7
|
+
button,input,select{font:inherit;color:inherit}:focus-visible{outline:3px solid var(--green-deep);outline-offset:3px}
|
|
8
|
+
.container{width:min(calc(100% - 3rem),76rem);margin:0 auto}
|
|
9
|
+
main{padding:2.3rem 0 6rem}
|
|
10
|
+
.kicker{margin:0 0 1rem;color:var(--green-deep);font-size:.7rem;font-weight:850;letter-spacing:.16em;text-transform:uppercase}
|
|
11
|
+
.topbar{display:flex;align-items:center;justify-content:space-between;min-height:4.5rem;border-bottom:1px solid var(--line)}
|
|
12
|
+
.brand{display:flex;align-items:center;gap:.65rem;font-size:.76rem;font-weight:850;letter-spacing:.14em;text-transform:uppercase}
|
|
13
|
+
.mark{display:grid;width:2rem;height:2rem;place-items:center;border:1px solid var(--green-deep);border-radius:.3rem;color:var(--green-deep);font-size:.65rem;letter-spacing:0}
|
|
14
|
+
.nav{color:var(--muted);font-size:.78rem;display:flex;gap:1.2rem;align-items:center}
|
|
15
|
+
.button{display:inline-flex;min-height:2.4rem;align-items:center;justify-content:center;border:1px solid var(--ink);border-radius:999px;padding:.4rem .95rem;background:var(--ink);color:var(--white);font-size:.74rem;font-weight:800;letter-spacing:.04em;cursor:pointer;white-space:nowrap}
|
|
16
|
+
.button.secondary{border-color:var(--line);background:transparent;color:var(--ink)}
|
|
17
|
+
.button[aria-pressed=true],.button.on{background:var(--green);border-color:var(--green);color:var(--ink)}
|
|
18
|
+
.button:hover{background:var(--green);border-color:var(--green);color:var(--ink)}
|
|
19
|
+
.button:disabled{opacity:.5;cursor:not-allowed}
|
|
20
|
+
.state{color:var(--green-deep);font-size:.65rem;font-weight:850;letter-spacing:.1em;text-transform:uppercase;white-space:nowrap}
|
|
21
|
+
.state::before{display:inline-block;width:.4rem;height:.4rem;margin-right:.4rem;border-radius:50%;background:var(--green);content:""}
|
|
22
|
+
.cat{color:var(--muted);font-size:.64rem;font-weight:850;letter-spacing:.12em;text-transform:uppercase}
|
|
23
|
+
.search{width:100%;min-height:2.9rem;border:1px solid var(--line);border-radius:999px;padding:.6rem 1.1rem;background:var(--white)}
|
|
24
|
+
.tabs{display:flex;gap:.4rem;flex-wrap:wrap;align-items:center}
|
|
25
|
+
.chips{display:flex;flex-wrap:wrap;gap:.35rem}
|
|
26
|
+
.chip{border:1px solid var(--line);border-radius:999px;padding:.4rem .7rem;background:transparent;color:var(--muted);cursor:pointer;font-size:.7rem}
|
|
27
|
+
.chip:hover,.chip[aria-pressed=true]{border-color:var(--ink);background:var(--ink);color:var(--white)}
|
|
28
|
+
.chip b{font-weight:850;margin-left:.3rem;opacity:.6}
|
|
29
|
+
.strip{display:flex;flex-wrap:wrap;border:1px solid var(--line);border-radius:var(--radius);background:var(--white);margin:1.5rem 0}
|
|
30
|
+
.strip>div{flex:1 1 7rem;padding:.9rem 1.1rem;border-right:1px solid var(--line)}.strip>div:last-child{border:0}
|
|
31
|
+
.strip strong{display:block;font-family:var(--serif);font-size:1.9rem;font-weight:500;letter-spacing:-.06em;line-height:1}
|
|
32
|
+
.strip small{color:var(--muted);font-size:.64rem;letter-spacing:.12em;text-transform:uppercase;font-weight:850}
|
|
33
|
+
.ledger{display:grid;grid-template-columns:15rem 1fr;gap:2rem;padding-top:2rem}
|
|
34
|
+
.rail{position:sticky;top:1rem;align-self:start;display:grid;gap:1rem}
|
|
35
|
+
.rail .chips{flex-direction:column}.rail .chip{text-align:left;border-radius:.6rem;display:flex;justify-content:space-between}
|
|
36
|
+
.rail h1{font-family:var(--serif);font-weight:500;font-size:2.4rem;letter-spacing:-.07em;line-height:.95;margin:0}
|
|
37
|
+
.row{display:grid;grid-template-columns:1.1rem minmax(0,1.3fr) minmax(0,2fr) 6rem 6rem 8rem;gap:1rem;align-items:center;padding:.6rem .8rem;border-bottom:1px solid var(--line);font-size:.8rem}
|
|
38
|
+
.row.head{position:sticky;top:0;background:var(--paper);z-index:2;color:var(--muted);font-size:.62rem;letter-spacing:.12em;text-transform:uppercase;font-weight:850}
|
|
39
|
+
.row .n{font-family:var(--serif);font-size:1rem;letter-spacing:-.03em;overflow:hidden;text-overflow:ellipsis}
|
|
40
|
+
.dot{width:.5rem;height:.5rem;border-radius:50%;background:var(--line)}.dot.g{background:var(--green)}
|
|
41
|
+
.rowwrap{background:var(--white);border-left:1px solid var(--line);border-right:1px solid var(--line)}
|
|
42
|
+
.rowwrap .cmd{margin:0 .8rem .7rem}.rowwrap .cmdnote{margin:-.4rem .8rem .7rem}
|
|
43
|
+
.cmd{display:flex;gap:.5rem;align-items:center;margin-top:.5rem;padding:.5rem .6rem;border-radius:.6rem;background:var(--ink);color:var(--white)}
|
|
44
|
+
.cmd code{flex:1;font-size:.72rem;overflow-x:auto;white-space:nowrap}.cmd .button{min-height:1.8rem;padding:.2rem .7rem;background:var(--green);border-color:var(--green);color:var(--ink)}
|
|
45
|
+
.cmdnote{font-size:.64rem;color:var(--muted);margin:.3rem 0 0}
|
|
46
|
+
.empty{padding:3rem 1rem;text-align:center;color:var(--muted);border:1px dashed var(--line);border-radius:var(--radius)}
|
|
47
|
+
.src{font-size:.65rem;font-weight:850}
|
|
48
|
+
.detail{padding:.6rem .8rem;background:var(--white);border-left:1px solid var(--line);border-right:1px solid var(--line);border-bottom:1px solid var(--line);font-size:.75rem;line-height:1.6;color:var(--muted)}
|
|
49
|
+
@media(max-width:760px){.ledger{grid-template-columns:1fr}.rail{position:static}.row{grid-template-columns:1rem 1fr 6rem}.row .d,.row .c,.row .s{display:none}.row.head{display:none}}
|
|
50
|
+
|
|
51
|
+
.scope{display:flex;gap:.4rem;align-items:center}
|
|
52
|
+
.badge{display:inline-block;margin-left:.5rem;padding:.1rem .45rem;border:1px solid var(--line);border-radius:999px;background:var(--paper-2);color:var(--muted);font-size:.58rem;font-weight:850;letter-spacing:.06em;text-transform:uppercase;vertical-align:middle;white-space:nowrap}
|
|
53
|
+
dialog{width:min(40rem,calc(100% - 2rem));border:1px solid var(--ink);border-radius:var(--radius);background:var(--white);color:var(--ink);padding:1.4rem}
|
|
54
|
+
dialog::backdrop{background:oklch(0.18 0.025 265/.4)}
|
|
55
|
+
dialog h2{font-family:var(--serif);font-weight:500;margin:0 0 .6rem}
|
|
56
|
+
dialog pre{margin:.6rem 0;padding:.7rem;border-radius:.6rem;background:var(--ink);color:var(--white);font-size:.7rem;overflow-x:auto;white-space:pre-wrap;word-break:break-all}
|
|
57
|
+
dialog .acts{display:flex;gap:.5rem;justify-content:flex-end;margin-top:1rem}
|
|
58
|
+
.err{color:oklch(0.5 0.2 25);font-size:.75rem}
|
|
59
|
+
</style></head><body>
|
|
60
|
+
<div class="container"><main id="app"></main></div>
|
|
61
|
+
<dialog id="dlg"></dialog>
|
|
62
|
+
<script>
|
|
63
|
+
const TOKEN='__TOKEN__';
|
|
64
|
+
const S={tab:'skill',sub:'discover',scope:'global',q:'',cat:'',src:'',expanded:new Set(),data:null,busy:null,detail:{}};
|
|
65
|
+
const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
66
|
+
const key=i=>i.kind+':'+i.name;
|
|
67
|
+
const find=k=>S.data.items.find(i=>key(i)===k);
|
|
68
|
+
const base=()=>S.data.items.filter(i=>i.kind===S.tab);
|
|
69
|
+
const visible=()=>{const q=S.q.toLowerCase();return base().filter(i=>(S.sub==='installed')===i.installed&&(!S.cat||i.category===S.cat)&&(!S.src||i.source===S.src)&&(!q||(i.name+' '+i.description+' '+i.category).toLowerCase().includes(q)))};
|
|
70
|
+
const uniq=f=>[...new Set(S.data.items.map(f))].sort();
|
|
71
|
+
const count=(f,v)=>base().filter(i=>f(i)===v).length;
|
|
72
|
+
const chip=(a,v,label,n)=>`<button class="chip" aria-pressed="${S[a]===v}" data-a="${a}" data-v="${esc(v)}">${esc(label)}<b>${n}</b></button>`;
|
|
73
|
+
function strip(){const all=S.data.items;const n=s=>all.filter(i=>i.source===s).length;return `<div class="strip"><div><strong>${all.length}</strong><small>total</small></div><div><strong>${n('AOS Core')}</strong><small>AOS Core</small></div><div><strong>${n('ECC')}</strong><small>ECC</small></div></div>`}
|
|
74
|
+
function tabs(){const n=base(),ic=n.filter(i=>i.installed).length,k=x=>S.data.items.filter(i=>i.kind===x).length;return `<div class="tabs">${chip('tab','skill','Skills',k('skill'))}${chip('tab','agent','Agents',k('agent'))}<span style="width:1rem"></span>${chip('sub','installed','Installed',ic)}${chip('sub','discover','Discover',n.length-ic)}</div>`}
|
|
75
|
+
const scopeToggle=()=>`<div class="scope"><span class="kicker" style="margin:0">Scope</span>${chip('scope','global','Global','')}${chip('scope','project','Project','')}</div>`;
|
|
76
|
+
function act(i){
|
|
77
|
+
if(i.core) return '<span class="state">AOS Core</span>';
|
|
78
|
+
if(i.installed) return `<span class="state">Installed</span>`;
|
|
79
|
+
if(S.busy===key(i)) return '<button class="button" disabled>Working...</button>';
|
|
80
|
+
return `<button class="button secondary" data-a="add" data-v="${esc(key(i))}">+ Add</button>`;
|
|
81
|
+
}
|
|
82
|
+
function row(i){
|
|
83
|
+
const k=key(i),open=S.expanded.has(k);
|
|
84
|
+
const badge=i.fileCount>1?`<span class="badge">${i.fileCount} files</span>`:'';
|
|
85
|
+
let h=`<div class="rowwrap"><div class="row"><span class="dot ${i.installed?'g':''}"></span><span class="n" data-a="exp" data-v="${esc(k)}" style="cursor:pointer" title="Show description">${esc(i.name)}${badge}</span><span class="d" style="color:var(--muted)">${esc(i.description)}</span><span class="c cat">${esc(i.category)}</span><span class="s src">${esc(i.source)}</span><span style="text-align:right">${act(i)}</span></div>`;
|
|
86
|
+
if(open){const d=i.core?i.description:S.detail[k];h+=`<div class="detail">${d===undefined?'Loading...':esc(d)}</div>`}
|
|
87
|
+
return h+'</div>';
|
|
88
|
+
}
|
|
89
|
+
function render(){
|
|
90
|
+
const a=document.activeElement,fq=a&&a.id==='q',pos=fq?a.selectionStart:0,l=visible();
|
|
91
|
+
document.getElementById('app').innerHTML=`<div class="ledger"><aside class="rail"><h1>AOS<br>Store</h1><p class="kicker" style="margin:0">Active scope: ${S.scope==='global'?'Global (harness roots in ~)':'Project ('+esc(S.data.cwd)+')'}</p>${scopeToggle()}<input class="search" id="q" type="search" placeholder="Search ${S.tab}s" value="${esc(S.q)}" aria-label="Search">${tabs()}<div><p class="kicker">Category</p><div class="chips">${chip('cat','','all',base().length)}${uniq(i=>i.category).map(c=>chip('cat',c,c,count(i=>i.category,c))).join('')}</div></div><div><p class="kicker">Source</p><div class="chips">${chip('src','','all',base().length)}${uniq(i=>i.source).map(s=>chip('src',s,s,count(i=>i.source,s))).join('')}</div></div></aside><section><p class="kicker">${l.length} of ${base().length} ${S.tab}s</p>${strip()}<div class="row head"><span></span><span>name</span><span class="d">description</span><span class="c">category</span><span class="s">source</span><span style="text-align:right">state</span></div>${l.length?l.map(row).join(''):`<div class="empty">${S.sub==='installed'?'Nothing installed yet.':'No matches.'}</div>`}</section></div>`;
|
|
92
|
+
if(fq){const q=document.getElementById('q');q.focus();q.setSelectionRange(pos,pos)}
|
|
93
|
+
}
|
|
94
|
+
async function load(){
|
|
95
|
+
const r=await fetch('/api/catalog?scope='+S.scope);
|
|
96
|
+
if(!r.ok) throw new Error('catalog '+r.status);
|
|
97
|
+
S.data=await r.json();
|
|
98
|
+
}
|
|
99
|
+
async function post(path,item){
|
|
100
|
+
const r=await fetch(path,{method:'POST',headers:{'content-type':'application/json','x-store-token':TOKEN},body:JSON.stringify({name:item.name,kind:item.kind,scope:S.scope})});
|
|
101
|
+
const d=await r.json().catch(()=>({}));
|
|
102
|
+
if(!r.ok) throw new Error(d.error||r.status);
|
|
103
|
+
return d;
|
|
104
|
+
}
|
|
105
|
+
const dlg=document.getElementById('dlg');
|
|
106
|
+
const scopeName=()=>S.scope==='global'?'Global':'Project';
|
|
107
|
+
async function preview(k){
|
|
108
|
+
const i=find(k);S.busy=k;render();
|
|
109
|
+
try{
|
|
110
|
+
const d=await post('/api/preview',i);
|
|
111
|
+
const ok=d.ok&&d.targets.length;
|
|
112
|
+
const tc=i.fileCount>0?Math.round(d.targets.length/i.fileCount):1;const size=i.totalSize!=null?` = ${i.fileCount} file(s) \u00d7 ${tc} target(s), ${(i.totalSize/1024).toFixed(1)} KB each`:'';
|
|
113
|
+
const warn=i.hasHooks?'<p class="err">Includes hooks \u2014 copied only, NOT registered in your harness settings; the skill may need manual wiring.</p>':'';
|
|
114
|
+
dlg.innerHTML=`<h2>Add ${esc(i.name)}</h2><p class="kicker">${scopeName()} scope \u2014 will write ${d.targets.length} file(s)${size}</p>${warn}<pre style="max-height:16rem;overflow:auto">${esc(ok?d.targets.join('\n'):(d.stdout+d.stderr).trim())}</pre><p class="cmdnote">Downloads ${i.fileCount>1?i.fileCount+' files':'the file'} from ECC at ${esc(S.data.pinned_commit.slice(0,10))}, each verified by SHA-256.</p><div id="res"></div><div class="acts"><button class="button secondary" data-a="close">Cancel</button>${ok?`<button class="button" data-a="confirm" data-v="${esc(k)}">Install to ${scopeName()}</button>`:''}</div>`;
|
|
115
|
+
dlg.showModal();
|
|
116
|
+
}catch(e){alert('Preview failed: '+e.message)}
|
|
117
|
+
finally{S.busy=null;render()}
|
|
118
|
+
}
|
|
119
|
+
async function confirmInstall(k,btn){
|
|
120
|
+
const i=find(k);btn.disabled=true;btn.textContent='Installing...';
|
|
121
|
+
const res=dlg.querySelector('#res');
|
|
122
|
+
try{
|
|
123
|
+
const d=await post('/api/install',i);
|
|
124
|
+
if(!d.ok||!d.installed) throw new Error((d.stderr||d.stdout||'install failed').trim());
|
|
125
|
+
dlg.close();await load();
|
|
126
|
+
}catch(e){res.innerHTML=`<p class="err">${esc(e.message)}</p>`;btn.disabled=false;btn.textContent='Retry'}
|
|
127
|
+
render();
|
|
128
|
+
}
|
|
129
|
+
async function expand(k){
|
|
130
|
+
if(S.expanded.has(k)){S.expanded.delete(k);return}
|
|
131
|
+
S.expanded.add(k);
|
|
132
|
+
const i=find(k);
|
|
133
|
+
if(!i.core&&S.detail[k]===undefined){render();
|
|
134
|
+
try{const r=await fetch(`/api/detail?name=${encodeURIComponent(i.name)}&kind=${i.kind}`);const d=await r.json();if(!r.ok)throw new Error(d.error);S.detail[k]=d.description||i.description}
|
|
135
|
+
catch(e){S.detail[k]=i.description+' (full description unavailable: '+e.message+')'}}
|
|
136
|
+
}
|
|
137
|
+
document.addEventListener('click',async e=>{
|
|
138
|
+
const b=e.target.closest('[data-a]');if(!b||b.disabled)return;const a=b.dataset.a,v=b.dataset.v;
|
|
139
|
+
if(a==='close')return dlg.close();
|
|
140
|
+
if(a==='confirm')return confirmInstall(v,b);
|
|
141
|
+
if(a==='add')return preview(v);
|
|
142
|
+
if(a==='tab'){S.tab=v;S.cat='';S.src=''}
|
|
143
|
+
else if(a==='sub'||a==='cat'||a==='src')S[a]=v;
|
|
144
|
+
else if(a==='scope'){S.scope=v;await load()}
|
|
145
|
+
else if(a==='exp')await expand(v);
|
|
146
|
+
render();
|
|
147
|
+
});
|
|
148
|
+
document.addEventListener('input',e=>{if(e.target.id==='q'){S.q=e.target.value;render()}});
|
|
149
|
+
load().then(render).catch(e=>{document.getElementById('app').textContent='Failed to load catalog: '+e.message});
|
|
150
|
+
</script></body></html>
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { execFile, spawn } from 'node:child_process';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { isInstalled, localAgents, localSkills } from '../store-shared.mjs';
|
|
8
|
+
|
|
9
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
10
|
+
const CLI = join(ROOT, 'bin', 'aos-store.mjs');
|
|
11
|
+
const NAME_RE = /^[A-Za-z0-9_-]+$/;
|
|
12
|
+
const DEFAULT_PORT = 4322;
|
|
13
|
+
const readJson = (...p) => JSON.parse(readFileSync(join(ROOT, ...p), 'utf8'));
|
|
14
|
+
const frontDescription = (file) => {
|
|
15
|
+
const fm = readFileSync(file, 'utf8').match(/^---\n([\s\S]*?)\n---/);
|
|
16
|
+
const d = fm?.[1].match(/^description:\s*(.+)$/m);
|
|
17
|
+
return d ? d[1].trim().replace(/^["']|["']$/g, '') : '';
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function catalog(scope, cwd) {
|
|
21
|
+
const idx = readJson('lib', 'ecc-store-index.json');
|
|
22
|
+
const project = scope === 'project';
|
|
23
|
+
const items = [];
|
|
24
|
+
const core = { skill: localSkills(ROOT), agent: localAgents(ROOT) };
|
|
25
|
+
for (const kind of ['skill', 'agent']) {
|
|
26
|
+
for (const [name, file] of core[kind]) {
|
|
27
|
+
items.push({ name, kind, description: frontDescription(file), category: 'core', source: 'AOS Core', installed: true, core: true });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
for (const [kind, key, type] of [['skill', 'skills', 'skills'], ['agent', 'subagents', 'agents']]) {
|
|
31
|
+
for (const [name, it] of Object.entries(idx[key] || {})) {
|
|
32
|
+
if (core[kind].has(name)) continue;
|
|
33
|
+
const installed = isInstalled(type, name, it, { project, cwd });
|
|
34
|
+
const files = kind === 'skill' && it.files?.length > 1 ? it.files : null;
|
|
35
|
+
items.push({
|
|
36
|
+
name, kind, description: it.description || '', category: it.category || '', source: 'ECC', installed, upstreamPath: it.upstream_path,
|
|
37
|
+
fileCount: files ? files.length : 1,
|
|
38
|
+
totalSize: files ? files.reduce((n, f) => n + f.size, 0) : null,
|
|
39
|
+
hasHooks: !!files?.some((f) => f.path.split('/').includes('hooks')),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { scope, cwd, pinned_commit: idx.pinned_commit, items };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const details = new Map();
|
|
47
|
+
async function fetchDetail(name, item, commit) {
|
|
48
|
+
if (details.has(name)) return details.get(name);
|
|
49
|
+
const url = `https://raw.githubusercontent.com/affaan-m/ECC/${commit}/${item.upstreamPath}`;
|
|
50
|
+
const res = await fetch(url, { headers: { 'user-agent': 'aos-store' }, signal: AbortSignal.timeout(10000) });
|
|
51
|
+
if (!res.ok) throw new Error(`upstream ${res.status}`);
|
|
52
|
+
const fm = (await res.text()).match(/^---\n([\s\S]*?)\n---/);
|
|
53
|
+
const d = fm?.[1].match(/^description:\s*(?:>-?|\|-?)?\s*([\s\S]*?)(?=\n[A-Za-z_-]+:|(?![\s\S]))/m);
|
|
54
|
+
const text = (d?.[1] || '').replace(/\s*\n\s*/g, ' ').trim().replace(/^["']|["']$/g, '');
|
|
55
|
+
details.set(name, text);
|
|
56
|
+
return text;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const runCli = (args, cwd, timeout) => new Promise((done) => {
|
|
60
|
+
execFile(process.execPath, [CLI, ...args], { cwd, timeout, maxBuffer: 1 << 20 }, (error, stdout, stderr) => {
|
|
61
|
+
done({ ok: !error, stdout, stderr: stderr || (error && error.message) || '' });
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const readBody = (req) => new Promise((ok, fail) => {
|
|
66
|
+
let data = '';
|
|
67
|
+
req.on('data', (c) => { data += c; if (data.length > 4096) { fail(new Error('too large')); req.destroy(); } });
|
|
68
|
+
req.on('end', () => ok(data));
|
|
69
|
+
req.on('error', fail);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export function createStoreServer({ cwd = process.cwd() } = {}) {
|
|
73
|
+
const token = randomBytes(24).toString('hex');
|
|
74
|
+
const tokenBuf = Buffer.from(token);
|
|
75
|
+
const send = (res, status, body, type = 'application/json') => {
|
|
76
|
+
res.statusCode = status;
|
|
77
|
+
res.setHeader('content-type', type);
|
|
78
|
+
res.setHeader('cache-control', 'no-store');
|
|
79
|
+
res.end(type === 'application/json' ? JSON.stringify(body) : body);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const server = createServer(async (req, res) => {
|
|
83
|
+
try {
|
|
84
|
+
const port = server.address().port;
|
|
85
|
+
const allowed = [`127.0.0.1:${port}`, `localhost:${port}`];
|
|
86
|
+
if (!allowed.includes(req.headers.host || '')) return send(res, 403, { error: 'bad host' });
|
|
87
|
+
if (req.headers.origin && !allowed.some((h) => req.headers.origin === `http://${h}`)) return send(res, 403, { error: 'bad origin' });
|
|
88
|
+
const url = new URL(req.url, 'http://x');
|
|
89
|
+
const p = url.pathname;
|
|
90
|
+
const scopeOf = (v) => (v === 'project' ? 'project' : 'global');
|
|
91
|
+
|
|
92
|
+
if (req.method === 'GET') {
|
|
93
|
+
if (p === '/api/health') return send(res, 200, { ok: true, service: 'aos-store' });
|
|
94
|
+
if (p === '/api/catalog') return send(res, 200, catalog(scopeOf(url.searchParams.get('scope')), cwd));
|
|
95
|
+
if (p === '/api/detail') {
|
|
96
|
+
const name = url.searchParams.get('name') || '';
|
|
97
|
+
if (!NAME_RE.test(name)) return send(res, 400, { error: 'invalid name' });
|
|
98
|
+
const data = catalog('global', cwd);
|
|
99
|
+
const item = data.items.find((i) => i.name === name && i.source === 'ECC' && i.kind === url.searchParams.get('kind'));
|
|
100
|
+
if (!item) return send(res, 404, { error: 'not found' });
|
|
101
|
+
try { return send(res, 200, { description: await fetchDetail(name, item, data.pinned_commit) }); }
|
|
102
|
+
catch (e) { return send(res, 502, { error: e.message }); }
|
|
103
|
+
}
|
|
104
|
+
if (p === '/' || p === '/index.html') {
|
|
105
|
+
return send(res, 200, readFileSync(join(ROOT, 'lib', 'store-ui', 'index.html'), 'utf8').replace('__TOKEN__', token), 'text/html; charset=utf-8');
|
|
106
|
+
}
|
|
107
|
+
return send(res, 404, { error: 'not found' });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (req.method === 'POST' && (p === '/api/preview' || p === '/api/install')) {
|
|
111
|
+
const given = Buffer.from(String(req.headers['x-store-token'] || ''));
|
|
112
|
+
if (given.length !== tokenBuf.length || !timingSafeEqual(given, tokenBuf)) return send(res, 401, { error: 'unauthorized' });
|
|
113
|
+
let body = {};
|
|
114
|
+
try { body = JSON.parse(await readBody(req)); } catch { return send(res, 400, { error: 'invalid body' }); }
|
|
115
|
+
const { name, kind } = body;
|
|
116
|
+
if (typeof name !== 'string' || !NAME_RE.test(name) || !['skill', 'agent'].includes(kind)) return send(res, 400, { error: 'invalid name' });
|
|
117
|
+
const scope = scopeOf(body.scope);
|
|
118
|
+
const item = catalog(scope, cwd).items.find((i) => i.name === name && i.kind === kind);
|
|
119
|
+
if (!item) return send(res, 404, { error: 'not in store index' });
|
|
120
|
+
if (item.core) return send(res, 409, { error: 'provided by AOS Core' });
|
|
121
|
+
const flags = scope === 'project' ? ['--project'] : [];
|
|
122
|
+
if (p === '/api/preview') {
|
|
123
|
+
const r = await runCli(['install', name, '--dry-run', ...flags], cwd, 10000);
|
|
124
|
+
const targets = r.stdout.split('\n').filter((l) => l.startsWith('[dry-run] write ')).map((l) => l.slice(16));
|
|
125
|
+
return send(res, 200, { ...r, scope, targets });
|
|
126
|
+
}
|
|
127
|
+
const r = await runCli(['install', name, '--net', ...flags], cwd, 30000);
|
|
128
|
+
const installed = catalog(scope, cwd).items.find((i) => i.name === name && i.kind === kind).installed;
|
|
129
|
+
return send(res, 200, { ...r, scope, installed });
|
|
130
|
+
}
|
|
131
|
+
return send(res, 405, { error: 'method not allowed' });
|
|
132
|
+
} catch (e) {
|
|
133
|
+
send(res, 500, { error: e.message });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return server;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function openBrowser(url) {
|
|
140
|
+
const [cmd, args] = process.platform === 'darwin' ? ['open', [url]]
|
|
141
|
+
: process.platform === 'win32' ? ['cmd', ['/c', 'start', '', url]] : ['xdg-open', [url]];
|
|
142
|
+
try {
|
|
143
|
+
const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
|
|
144
|
+
child.on('error', () => {});
|
|
145
|
+
child.unref();
|
|
146
|
+
} catch { /* opening is best effort */ }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const isStore = async (port) => {
|
|
150
|
+
try {
|
|
151
|
+
const r = await fetch(`http://127.0.0.1:${port}/api/health`, { signal: AbortSignal.timeout(1500) });
|
|
152
|
+
const j = await r.json();
|
|
153
|
+
return j.ok === true && j.service === 'aos-store';
|
|
154
|
+
} catch { return false; }
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export async function startUi({ argv = [], cwd = process.cwd() } = {}) {
|
|
158
|
+
const i = argv.findIndex((a) => a === '--port' || a.startsWith('--port='));
|
|
159
|
+
const raw = i < 0 ? process.env.AOS_STORE_PORT : (argv[i].includes('=') ? argv[i].split('=')[1] : argv[i + 1]);
|
|
160
|
+
const port = raw === undefined || raw === '' ? DEFAULT_PORT : Number(raw);
|
|
161
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`Invalid port: ${raw}`);
|
|
162
|
+
const url = `http://127.0.0.1:${port}/`;
|
|
163
|
+
const noOpen = argv.includes('--no-open');
|
|
164
|
+
|
|
165
|
+
if (await isStore(port)) {
|
|
166
|
+
console.log(`AOS Store already running: ${url}`);
|
|
167
|
+
if (!noOpen) openBrowser(url);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const server = createStoreServer({ cwd });
|
|
171
|
+
await new Promise((ok, fail) => {
|
|
172
|
+
server.once('error', (e) => fail(e.code === 'EADDRINUSE' ? new Error(`Port ${port} is in use by another service. Use --port or AOS_STORE_PORT.`) : e));
|
|
173
|
+
server.listen(port, '127.0.0.1', ok);
|
|
174
|
+
});
|
|
175
|
+
console.log(`AOS Store: ${url} (project scope: ${cwd})`);
|
|
176
|
+
if (!noOpen) openBrowser(url);
|
|
177
|
+
const stop = () => server.close(() => process.exit(0));
|
|
178
|
+
process.on('SIGINT', stop);
|
|
179
|
+
process.on('SIGTERM', stop);
|
|
180
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hybridlabor-api/aos",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.12.1",
|
|
4
4
|
"description": "AOS — A Curated AI AGENT OS. Optimized agent skills and add-ons like memB, OpenWiki, Heimdall Token Saver, and Godmode architectures.",
|
|
5
5
|
"main": "installer.js",
|
|
6
6
|
"engines": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"plugin:build": "node scripts/build-plugin-manifest.mjs",
|
|
28
28
|
"plugin:check": "node scripts/build-plugin-manifest.mjs --check",
|
|
29
29
|
"doctor": "node bin/aos-doctor.mjs",
|
|
30
|
-
"test": "node scripts/validate-skills.mjs --selftest && node scripts/validate-skills.mjs && node scripts/build-plugin-manifest.mjs --check && node --test tests/opencode-graph-gate.test.js && node --test tests/cross-harness-hooks.test.js && node tests/aos-store.test.mjs && node tests/aos-doctor.test.mjs && node tests/build-plugin-manifest.test.mjs"
|
|
30
|
+
"test": "node scripts/validate-skills.mjs --selftest && node scripts/validate-skills.mjs && node scripts/build-plugin-manifest.mjs --check && node --test tests/opencode-graph-gate.test.js && node --test tests/cross-harness-hooks.test.js && node tests/aos-store.test.mjs && node --test tests/aos-store-ui.test.mjs && node tests/aos-doctor.test.mjs && node tests/build-plugin-manifest.test.mjs"
|
|
31
31
|
},
|
|
32
32
|
"publishConfig": {
|
|
33
33
|
"access": "public"
|
|
@@ -85,6 +85,18 @@ function hash(content) {
|
|
|
85
85
|
return createHash('sha256').update(content).digest('hex');
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// Blobs at the pinned commit (the checkout is that commit); only multi-file skills record a list to keep the index small.
|
|
89
|
+
function skillFiles(dir) {
|
|
90
|
+
const out = runGit(['ls-tree', '-r', '-z', 'HEAD', `${dir}/`]).split('\0').filter(Boolean).map((line) => {
|
|
91
|
+
const [meta, path] = line.split('\t');
|
|
92
|
+
const [mode, kind] = meta.split(' ');
|
|
93
|
+
if (kind !== 'blob' || mode === '120000') throw new Error(`Unsupported entry in ${dir}: ${line}`);
|
|
94
|
+
const data = readFileSync(join(checkout, path));
|
|
95
|
+
return { path: path.slice(dir.length + 1), sha256: hash(data), size: data.length, exec: mode === '100755' };
|
|
96
|
+
});
|
|
97
|
+
return out.sort((a, b) => (a.path < b.path ? -1 : 1));
|
|
98
|
+
}
|
|
99
|
+
|
|
88
100
|
function itemFromFile(file, type) {
|
|
89
101
|
const content = readFileSync(file, 'utf8');
|
|
90
102
|
const name = type === 'skills'
|
|
@@ -102,6 +114,10 @@ function itemFromFile(file, type) {
|
|
|
102
114
|
sha256: hash(content),
|
|
103
115
|
upstream_path: relative(checkout, file).split(sep).join('/'),
|
|
104
116
|
};
|
|
117
|
+
if (type === 'skills') {
|
|
118
|
+
const files = skillFiles(dirname(base.upstream_path));
|
|
119
|
+
if (files.length > 1) base.files = files;
|
|
120
|
+
}
|
|
105
121
|
return { name, item: base, warnings: compatibilityWarnings(content) };
|
|
106
122
|
}
|
|
107
123
|
|
|
@@ -139,7 +155,7 @@ try {
|
|
|
139
155
|
subagents: Object.fromEntries(Object.entries(subagents).sort(([a], [b]) => a.localeCompare(b))),
|
|
140
156
|
};
|
|
141
157
|
const output = JSON.stringify(index) + '\n';
|
|
142
|
-
if (Buffer.byteLength(output) >=
|
|
158
|
+
if (Buffer.byteLength(output) >= 300 * 1024) throw new Error(`Store index exceeds 300 KB: ${Buffer.byteLength(output)} bytes`);
|
|
143
159
|
writeFileSync(OUTPUT_PATH, output);
|
|
144
160
|
console.log(`Wrote ${OUTPUT_PATH} with ${Object.keys(skills).length} skills and ${Object.keys(subagents).length} subagents (${Buffer.byteLength(output)} bytes).`);
|
|
145
161
|
} finally {
|
|
@@ -84,12 +84,12 @@ const manifest = {
|
|
|
84
84
|
name: 'bdb-aos',
|
|
85
85
|
version: pkg.version,
|
|
86
86
|
description:
|
|
87
|
-
'Full BDB Agent OS suite: 200+ curated skills, subagents
|
|
88
|
-
'
|
|
87
|
+
'Full BDB Agent OS suite: 200+ curated skills, 13 subagents and the startcycle pipelines for Claude Code. ' +
|
|
88
|
+
'Gate hooks and MCP servers are installed by the AOS installer.',
|
|
89
89
|
author: { name: 'Tim Rennings / Hybridlabor Global LLC', email: 'hybridlabor@gmail.com' },
|
|
90
90
|
homepage: repoUrl,
|
|
91
91
|
repository: repoUrl,
|
|
92
|
-
license: '
|
|
92
|
+
license: 'Apache-2.0',
|
|
93
93
|
keywords: ['skills', 'agent-os', 'cicd', 'multiagent', 'startcycle', 'bdb'],
|
|
94
94
|
skills,
|
|
95
95
|
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bdb-aos
|
|
3
|
+
category: bdb-core
|
|
4
|
+
description: >-
|
|
5
|
+
Entry point for the BDB Agent OS suite installed as a Claude Code plugin.
|
|
6
|
+
Use to find out which of the 200+ AOS skills, the seven startcycle subagents,
|
|
7
|
+
or the pipeline workflows apply to a task, and where their contracts live.
|
|
8
|
+
Reach for a specific skill directly when you already know its name — this
|
|
9
|
+
skill only routes.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# BDB Agent OS
|
|
13
|
+
|
|
14
|
+
The full AOS suite: 200+ skills, 13 subagents, and the startcycle pipelines.
|
|
15
|
+
|
|
16
|
+
## Where things live
|
|
17
|
+
|
|
18
|
+
| Component | Location |
|
|
19
|
+
|---|---|
|
|
20
|
+
| Skills | `skills/<category>/<name>/SKILL.md` |
|
|
21
|
+
| Subagents | `agents/<name>.md` (mirrors `.claude/agents/`) |
|
|
22
|
+
| Pipeline contract | `.agents/graph.md`, node registry in `.agents/nodes.json` |
|
|
23
|
+
| State schema | `.agents/state.schema.json` |
|
|
24
|
+
|
|
25
|
+
## Pipelines
|
|
26
|
+
|
|
27
|
+
Three variants, differing only in how much machinery the task needs:
|
|
28
|
+
|
|
29
|
+
| Skill | Shape |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `startcycle` | Linear chain: Architect → TechLead → parallel build → Reviewer. |
|
|
32
|
+
| `startcycle-graph` | Dispatcher graph: durable `state.json`, Reviewer repair loop, automated quality gate, human escalation. |
|
|
33
|
+
| `startcycle-graph-user` | Throwaway 2–4 node fan-out, nothing persistent left behind. |
|
|
34
|
+
|
|
35
|
+
**Agents never invoke each other.** A dispatcher — the main session or the
|
|
36
|
+
workflow script — decides every next step. Routing lives in the graph contract,
|
|
37
|
+
never inside an agent's prompt.
|
|
38
|
+
|
|
39
|
+
## Skill categories
|
|
40
|
+
|
|
41
|
+
`design-ui-ux`, `engineering-method`, `media-eventtech`, `bdb-core`, `library`,
|
|
42
|
+
`engineering-hardware`. Pick the narrowest match; a `library` skill should never
|
|
43
|
+
be pulled for a task a domain skill already covers.
|
|
44
|
+
|
|
45
|
+
The harness contract for every skill — required frontmatter, attribution,
|
|
46
|
+
`source:` for derived work — is in `AGENTS.md` at the repo root.
|