symbiot 0.10.3 → 0.13.0
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/index.mjs +118 -28
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -103,6 +103,20 @@ function addTask(text, repo) {
|
|
|
103
103
|
}
|
|
104
104
|
function toggleTask(id) { const t = loadTasks(); const it = t.find((x) => x.id === id); if (it) { it.done = !it.done; saveTasks(t); } return it || { error: "not found" }; }
|
|
105
105
|
function removeTask(id) { saveTasks(loadTasks().filter((x) => x.id !== id)); return { ok: true }; }
|
|
106
|
+
// Deterministic task classification (no model) so tasks batch by kind instead
|
|
107
|
+
// of arriving as a flat, mixed pile. Order matters (most specific first).
|
|
108
|
+
const TASK_ORDER = ["Fixes", "Tests & CI", "Security", "Performance", "Refactor", "UI/UX", "Docs", "Features & other"];
|
|
109
|
+
function taskType(text) {
|
|
110
|
+
const s = String(text || "").toLowerCase();
|
|
111
|
+
if (/\b(test|tests|spec|coverage|smoke|fixture|ci\b)/.test(s)) return "Tests & CI";
|
|
112
|
+
if (/\b(secur|auth|token|secret|vulnerab|permission|sanitiz|escap|injection)/.test(s)) return "Security";
|
|
113
|
+
if (/\b(readme|docs?|document|changelog|comment|guide)/.test(s)) return "Docs";
|
|
114
|
+
if (/\b(perf|performance|slow|optimi|latency|throughput|speed up|memory leak)/.test(s)) return "Performance";
|
|
115
|
+
if (/\b(refactor|simplify|split|extract|clean ?up|consolidat|dedupe|modular|rename)/.test(s)) return "Refactor";
|
|
116
|
+
if (/\b(bug|fix|broken|error|crash|regress|hang|race|leak)/.test(s)) return "Fixes";
|
|
117
|
+
if (/\b(ui|ux|design|layout|style|button|screen|view\b|page\b|responsive)/.test(s)) return "UI/UX";
|
|
118
|
+
return "Features & other";
|
|
119
|
+
}
|
|
106
120
|
// Push tasks into the repos as an agent-readable brief (.symbiot/TASKS.md):
|
|
107
121
|
// a checklist plus the deterministic context an agent needs to get oriented.
|
|
108
122
|
function buildTasksMd(name, ctx, list) {
|
|
@@ -115,8 +129,10 @@ function buildTasksMd(name, ctx, list) {
|
|
|
115
129
|
if (ctx.open && ctx.open.length) { L.push("- **Open markers (TODO/FIXME + uncommitted):**"); for (const o of ctx.open) L.push(` - ${o}`); }
|
|
116
130
|
if (ctx.drift && ctx.drift.length) { L.push("- **Current drift / risk:**"); for (const d of ctx.drift) L.push(` - ${d}`); }
|
|
117
131
|
L.push("", "## Tasks");
|
|
118
|
-
for (const t of list)
|
|
119
|
-
|
|
132
|
+
const byType = {}; for (const t of list) { const ty = taskType(t.text); (byType[ty] = byType[ty] || []).push(t); }
|
|
133
|
+
const keys = Object.keys(byType).sort((a, b) => TASK_ORDER.indexOf(a) - TASK_ORDER.indexOf(b));
|
|
134
|
+
for (const ty of keys) { L.push(`### ${ty}`); for (const t of byType[ty]) L.push(`- [ ] ${t.text}`); L.push(""); }
|
|
135
|
+
L.push("---", 'To action these, tell your coding agent: "Read `.symbiot/TASKS.md` and implement the unchecked items in this repo, using the context above. Confirm with me before anything destructive."');
|
|
120
136
|
return L.join("\n") + "\n";
|
|
121
137
|
}
|
|
122
138
|
function pushTasks() {
|
|
@@ -147,17 +163,22 @@ function pushTasks() {
|
|
|
147
163
|
const dir = join(path, ".symbiot"); mkdirSync(dir, { recursive: true });
|
|
148
164
|
const file = join(dir, "TASKS.md");
|
|
149
165
|
writeFileSync(file, buildTasksMd(name, { branch: st.branch, commits, open, drift: risk, stack }, list));
|
|
150
|
-
written.push({ name, file, count: list.length });
|
|
166
|
+
written.push({ name, file, path, count: list.length });
|
|
151
167
|
} catch (e) { unresolved.push({ name, count: list.length, error: String((e && e.message) || e) }); }
|
|
152
168
|
}
|
|
153
|
-
return { empty: false, written, unresolved };
|
|
169
|
+
return { empty: false, written, unresolved, handoff: handoffCmd() };
|
|
154
170
|
}
|
|
155
171
|
function cmdPush() {
|
|
156
172
|
const r = pushTasks();
|
|
157
173
|
if (r.empty) { console.log(c.y("No open tasks to push.") + c.d(" Add some in `symbiot app` — a repo review's ideas, or the Tasks tab.")); return; }
|
|
158
174
|
if (r.written.length) { console.log("\n" + c.g("●") + " " + c.b("Pushed tasks into repos:")); for (const w of r.written) console.log(` ${c.g("✓")} ${w.name} ${c.d(w.file + " (" + w.count + " task" + (w.count > 1 ? "s" : "") + ")")}`); }
|
|
159
175
|
if (r.unresolved.length) { console.log("\n" + c.y(`Not written (repo not found under ${BASE}):`)); for (const u of r.unresolved) console.log(` · ${u.name} (${u.count})`); }
|
|
160
|
-
|
|
176
|
+
if (has("open")) {
|
|
177
|
+
if (!r.handoff) console.log("\n" + c.y("No agent command set.") + c.d(" Set one in `symbiot app` Settings, or `agentCmd` in ~/.config/symbiot/config.json (use {dir} and {prompt})."));
|
|
178
|
+
else { for (const w of r.written) runHandoff(w.path); console.log("\n" + c.g("→ ") + `Handed ${r.written.length} repo(s) to your agent (${r.handoff}).`); }
|
|
179
|
+
} else {
|
|
180
|
+
console.log("\n" + c.d("Point your agent at .symbiot/TASKS.md in each repo. (add --open to run your configured agent command)"));
|
|
181
|
+
}
|
|
161
182
|
}
|
|
162
183
|
function envKey(provider) {
|
|
163
184
|
for (const e of (PROVIDERS[provider].env || [])) if (process.env[e]) return process.env[e];
|
|
@@ -515,6 +536,31 @@ function recommendModels(hw) {
|
|
|
515
536
|
const best = (local.slice().reverse().find((m) => m.fits) || local[0]).model;
|
|
516
537
|
return { local, paid, best };
|
|
517
538
|
}
|
|
539
|
+
// ---- hand a repo (+ its tasks) to the user's agent — generic, settings-based
|
|
540
|
+
// The handoff is a command TEMPLATE the user configures, with {dir} (repo path)
|
|
541
|
+
// and {prompt} (the task instruction). Works for any agent/editor:
|
|
542
|
+
// claude -p "{prompt}" · aider --message "{prompt}" · code {dir}
|
|
543
|
+
// gnome-terminal --working-directory={dir} -- claude "{prompt}"
|
|
544
|
+
// Not tied to any one tool — you decide what runs.
|
|
545
|
+
const IDE_LIST = [["code", "VS Code"], ["cursor", "Cursor"], ["windsurf", "Windsurf"], ["zed", "Zed"], ["subl", "Sublime Text"], ["idea", "IntelliJ IDEA"], ["nvim", "Neovim"]];
|
|
546
|
+
const AGENT_LIST = [["claude", "Claude Code", 'claude -p "{prompt}"'], ["aider", "Aider", 'aider --message "{prompt}"'], ["cursor-agent", "Cursor agent", 'cursor-agent -p "{prompt}"']];
|
|
547
|
+
function detectHandoffs() {
|
|
548
|
+
const editors = IDE_LIST.filter(([cmd]) => sh(`command -v ${cmd} 2>/dev/null`).trim()).map(([cmd, label]) => ({ label, tmpl: `${cmd} {dir}`, kind: "editor" }));
|
|
549
|
+
const agents = AGENT_LIST.filter(([cmd]) => sh(`command -v ${cmd} 2>/dev/null`).trim()).map(([cmd, label, tmpl]) => ({ label, tmpl, kind: "agent" }));
|
|
550
|
+
return { agents, editors };
|
|
551
|
+
}
|
|
552
|
+
const shSingle = (s) => "'" + String(s).replace(/'/g, "'\\''") + "'";
|
|
553
|
+
const escDq = (s) => String(s).replace(/[\\"$`]/g, "\\$&");
|
|
554
|
+
const HANDOFF_PROMPT = "Read .symbiot/TASKS.md and implement the unchecked items in this repo. Confirm before anything destructive.";
|
|
555
|
+
function handoffCmd() { const cfg = loadConfig(); return cfg.agentCmd || (cfg.ide ? `${cfg.ide} {dir}` : ""); } // ide = legacy
|
|
556
|
+
function runHandoff(repoPath) {
|
|
557
|
+
try {
|
|
558
|
+
const tmpl = handoffCmd(); if (!tmpl || !repoPath) return false;
|
|
559
|
+
const cmd = tmpl.replace(/\{dir\}/g, shSingle(repoPath)).replace(/\{prompt\}/g, escDq(HANDOFF_PROMPT));
|
|
560
|
+
spawn(cmd, { shell: true, cwd: repoPath, detached: true, stdio: "ignore" }).unref();
|
|
561
|
+
return true;
|
|
562
|
+
} catch { return false; }
|
|
563
|
+
}
|
|
518
564
|
function cmdModels() {
|
|
519
565
|
const hw = detectHardware(); const rec = recommendModels(hw);
|
|
520
566
|
const pad = (s, n) => String(s).padEnd(n);
|
|
@@ -766,25 +812,41 @@ async function repoReview(path) {
|
|
|
766
812
|
const commits = sh(`git -C ${JSON.stringify(path)} log ${authorArgs(auth.emails)} --format='%ad %s' --date=short -50 2>/dev/null`).trim()
|
|
767
813
|
|| sh(`git -C ${JSON.stringify(path)} log --format='%ad %s' --date=short -50 2>/dev/null`).trim();
|
|
768
814
|
const footer = reportFooter(path, auth, state, rd);
|
|
815
|
+
// Cheap maturity gauge, so the model can be told to show restraint instead of
|
|
816
|
+
// always inventing new features (over-engineering is a failure, not a win).
|
|
817
|
+
const testFiles = Number(sh(`git -C ${JSON.stringify(path)} ls-files 2>/dev/null | grep -icE '(^|/)(tests?|spec|__tests__)/|\\.(test|spec)\\.'`).trim()) || 0;
|
|
818
|
+
const churn = Number(sh(`git -C ${JSON.stringify(path)} log --since='14 days ago' --oneline 2>/dev/null | wc -l`).trim()) || 0;
|
|
819
|
+
const posture = state.stale ? "the checkout is a stale snapshot — the priority is getting current, not new work"
|
|
820
|
+
: (testFiles === 0 && shape.total > 40) ? "a real codebase with NO test files — favour tests/stabilising over new features"
|
|
821
|
+
: (churn >= 25) ? "very rapid churn lately — likely over-iterating; be conservative, prefer consolidation/finishing over new features"
|
|
822
|
+
: (state.dirty > 20) ? "a lot of unfinished/uncommitted work — finish what's open before starting new things"
|
|
823
|
+
: "no red flags — but still only propose what clearly pays off";
|
|
769
824
|
const system =
|
|
770
|
-
`You are
|
|
771
|
-
`"review": 3-6 sentences on what it does and who it's for.
|
|
772
|
-
`
|
|
773
|
-
`
|
|
774
|
-
|
|
825
|
+
`You are a pragmatic, restraint-minded reviewer of ONE software project. Respond with ONLY a JSON object (no fences): {"review": string, "verdict": string, "ideas": string[]}. ` +
|
|
826
|
+
`"review": 3-6 sentences on what it does and who it's for. ` +
|
|
827
|
+
`"verdict": ONE honest sentence on whether this project needs new work now, and of what KIND. Over-engineering is a failure: if it's already capable, or shows churn / missing tests / unfinished work, say so and steer toward STABILISING (tests, docs, finishing, removing) or simply SHIPPING — not more features. ` +
|
|
828
|
+
`"ideas": AT MOST 5, ranked by value, ONLY items that clearly pay off. Do NOT pad the list — returning 0–2 ideas with a "stabilise, don't add" verdict is a good, correct answer. Prefer fixes / tests / simplification / finishing over new features unless a feature is clearly warranted by the evidence. ` +
|
|
829
|
+
`Ground every claim in the evidence; recent commits beat the README; never propose building something already in the structure; never advise against the stated conventions. ` +
|
|
830
|
+
(state.stale ? `CRITICAL: the working tree is a STALE checkout — never suggest committing it. ` : ``);
|
|
775
831
|
const prompt =
|
|
776
|
-
`Project: ${name}\nBranch ${state.branch} · ${auth.total} commits total (${auth.mineCount} yours${auth.filterDropped ? ", filter dropped so counting everyone" : ""})\n
|
|
832
|
+
`Project: ${name}\nBranch ${state.branch} · ${auth.total} commits total (${auth.mineCount} yours${auth.filterDropped ? ", filter dropped so counting everyone" : ""})\n` +
|
|
833
|
+
`Maturity signal: ${posture}. (${testFiles} test files, ${churn} commits in the last 14 days.)\n\n` +
|
|
777
834
|
(rd.file ? `README (last changed ${rd.lastDate || "?"}, ${rd.commitsAgo} commits ago — may be out of date):\n${rd.excerpt}\n\n` : "(no README)\n\n") +
|
|
778
835
|
`Structure (top folders · file counts): ${shape.dirs.join(", ")}\nManifests: ${shape.manifests.join(", ") || "none"}\nDocs present: ${shape.docs.join(", ") || "none"}\n\n` +
|
|
779
836
|
`Recent commits (newest first):\n${commits || "(none)"}\n\n` +
|
|
780
837
|
(rules ? `Conventions this team has chosen — do NOT advise against these:\n${rules}\n\n` : "") +
|
|
781
838
|
(state.stale ? `Working tree: STALE (≈ ${state.staleBy ? "HEAD~" + state.staleBy : "older"}); its "changes" are the gap to an old snapshot, not new work.\n\n` : "") +
|
|
782
|
-
`Return the JSON.`;
|
|
839
|
+
`Return the JSON. Remember: fewer, higher-value ideas beat a long list; a "don't add — stabilise" verdict with 0–2 ideas is a valid answer.`;
|
|
783
840
|
const raw = await write(system, prompt);
|
|
784
|
-
if (!raw) return { text: "(couldn't reach the model)", ideas: [], footer };
|
|
841
|
+
if (!raw) return { text: "(couldn't reach the model)", ideas: [], verdict: "", footer };
|
|
785
842
|
const j = extractJson(raw);
|
|
786
|
-
if (j && (j.review || j.ideas
|
|
787
|
-
|
|
843
|
+
if (j && (j.review || j.ideas || j.verdict)) return {
|
|
844
|
+
text: String(j.review || "").trim() || raw,
|
|
845
|
+
verdict: String(j.verdict || "").trim(),
|
|
846
|
+
ideas: Array.isArray(j.ideas) ? j.ideas.map((x) => String(x).trim()).filter(Boolean).slice(0, 5) : [],
|
|
847
|
+
footer,
|
|
848
|
+
};
|
|
849
|
+
return { text: raw, ideas: [], verdict: "", footer };
|
|
788
850
|
}
|
|
789
851
|
function extractJson(s) {
|
|
790
852
|
if (!s) return null;
|
|
@@ -1015,6 +1077,7 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
|
1015
1077
|
.review .body{color:var(--text);line-height:1.65;font-size:14px;white-space:pre-wrap}
|
|
1016
1078
|
.review .rname{color:var(--bone);font-weight:600}
|
|
1017
1079
|
.rfoot{margin-top:12px;font-size:11px;color:var(--faint);font-family:ui-monospace,Menlo,Consolas,monospace;border-top:1px solid var(--line);padding-top:8px;line-height:1.5}
|
|
1080
|
+
.verdict{margin-top:12px;padding:10px 12px;border-left:3px solid var(--amber);background:var(--ink3);border-radius:0 8px 8px 0;color:var(--bone);font-size:13.5px;line-height:1.5}
|
|
1018
1081
|
.ideas{margin-top:14px}
|
|
1019
1082
|
.ideas h4{margin:0 0 6px;color:var(--amber);font-size:11px;letter-spacing:.14em;text-transform:uppercase;font-weight:700}
|
|
1020
1083
|
.idea{display:flex;align-items:flex-start;gap:9px;padding:5px 0;font-size:13.5px;color:var(--text)}
|
|
@@ -1025,6 +1088,8 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
|
1025
1088
|
.task .rp{font-size:11px;color:var(--faint);background:var(--ink3);border:1px solid var(--line);border-radius:999px;padding:2px 8px}
|
|
1026
1089
|
.task .rm{background:none;border:0;color:var(--faint);cursor:pointer;font-size:18px;line-height:1}
|
|
1027
1090
|
.task .rm:hover{color:var(--amber)}
|
|
1091
|
+
.tgroup{margin:14px 0 4px;font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--green);font-weight:700}
|
|
1092
|
+
.tcount{color:var(--faint);font-weight:400}
|
|
1028
1093
|
.drift{border:1px solid var(--line);border-radius:10px;margin-top:10px;padding:12px 14px;background:var(--ink2)}
|
|
1029
1094
|
.drift .dh{display:flex;gap:8px;align-items:center}
|
|
1030
1095
|
.drift .dn{color:var(--bone);font-weight:600}
|
|
@@ -1105,6 +1170,12 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
|
1105
1170
|
<div class="row" style="margin-top:16px"><button class="act" id="save">Save & connect</button>
|
|
1106
1171
|
<span class="note" id="saveMsg"></span></div>
|
|
1107
1172
|
<div style="margin-top:20px;border-top:1px solid var(--line);padding-top:16px">
|
|
1173
|
+
<label>Hand off to your agent when you "Send to repos"</label>
|
|
1174
|
+
<input id="agentcmd" type="text" placeholder="e.g. claude -p "{prompt}" · code {dir} · leave blank to just write the file">
|
|
1175
|
+
<div class="note muted">Runs in each repo after tasks are written. Use <b>{dir}</b> = repo path, <b>{prompt}</b> = the task instruction. Works with any agent or editor — it's your command.</div>
|
|
1176
|
+
<div id="agentpresets" style="margin-top:8px"></div>
|
|
1177
|
+
</div>
|
|
1178
|
+
<div style="margin-top:20px;border-top:1px solid var(--line);padding-top:16px">
|
|
1108
1179
|
<button class="ghost" id="recbtn">Recommend models for my machine</button>
|
|
1109
1180
|
<div id="recout" style="margin-top:12px"></div>
|
|
1110
1181
|
</div>
|
|
@@ -1173,12 +1244,14 @@ if(d.type==='lang'||d.type==='tool'){var lis=(d.repos||[]).map(function(r){retur
|
|
|
1173
1244
|
if(d.type==='person'){var st=d.stats||{};el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='k'>"+st.repos+" repos · "+st.commits+" commits · "+st.languages+" languages · "+st.tools+" tools</div>";return;}}
|
|
1174
1245
|
var reviewCache={};var IDEAS=[];var IREPO="";
|
|
1175
1246
|
function hideReview(){var el=document.getElementById('review');el.classList.add('hidden');el.innerHTML='';}
|
|
1176
|
-
function reviewHtml(name,body,ideas,tasks,footer){IDEAS=ideas||[];IREPO=name;
|
|
1247
|
+
function reviewHtml(name,body,verdict,ideas,tasks,footer){IDEAS=ideas||[];IREPO=name;
|
|
1177
1248
|
var h="<h4>AI review · <span class='rname'>"+esc(name)+"</span></h4><div class='body'>"+esc(body)+"</div>";
|
|
1178
|
-
if(
|
|
1249
|
+
if(verdict)h+="<div class='verdict'>⚖️ "+esc(verdict)+"</div>";
|
|
1250
|
+
if(ideas&&ideas.length){h+="<div class='ideas'><h4>Ideas worth doing · tick to add to Tasks</h4>";
|
|
1179
1251
|
ideas.forEach(function(idea,i){var t=(tasks||[]).filter(function(x){return x.text===idea&&x.repo===name;})[0];var tid=t?t.id:"";
|
|
1180
1252
|
h+="<label class='idea'><input type='checkbox' class='ideachk' data-idx='"+i+"' data-tid='"+esc(tid)+"'"+(t?" checked":"")+"><span>"+esc(idea)+"</span></label>";});
|
|
1181
1253
|
h+="</div>";}
|
|
1254
|
+
else h+="<div class='muted' style='margin-top:10px'>No new features suggested — the verdict above is the call.</div>";
|
|
1182
1255
|
if(footer)h+="<div class='rfoot'>"+esc(footer)+"</div>";
|
|
1183
1256
|
return h;}
|
|
1184
1257
|
function wireIdeas(){document.querySelectorAll('.ideachk').forEach(function(cb){cb.addEventListener('change',function(){
|
|
@@ -1186,17 +1259,21 @@ var idea=IDEAS[+cb.getAttribute('data-idx')];if(idea==null)return;
|
|
|
1186
1259
|
if(cb.checked){api('/api/tasks/add',{text:idea,repo:IREPO}).then(function(it){if(it&&it.id)cb.setAttribute('data-tid',it.id);});}
|
|
1187
1260
|
else{var id=cb.getAttribute('data-tid');if(id){api('/api/tasks/remove',{id:id}).then(function(){cb.setAttribute('data-tid','');});}}});});}
|
|
1188
1261
|
function loadReview(name,path){var el=document.getElementById('review');el.classList.remove('hidden');
|
|
1189
|
-
if(reviewCache[path]){var c=reviewCache[path];api('/api/tasks').then(function(tasks){el.innerHTML=reviewHtml(name,c.text,c.ideas,tasks,c.footer);wireIdeas();});return;}
|
|
1262
|
+
if(reviewCache[path]){var c=reviewCache[path];api('/api/tasks').then(function(tasks){el.innerHTML=reviewHtml(name,c.text,c.verdict,c.ideas,tasks,c.footer);wireIdeas();});return;}
|
|
1190
1263
|
el.innerHTML="<h4>AI review · <span class='rname'>"+esc(name)+"</span></h4><div class='body'>Reading the project…</div>";
|
|
1191
1264
|
Promise.all([api('/api/review',{path:path}),api('/api/tasks')]).then(function(res){var r=res[0]||{},tasks=res[1]||[];
|
|
1192
1265
|
if(r.error==='not-connected'){el.innerHTML="<h4>AI review · <span class='rname'>"+esc(name)+"</span></h4><div class='body'>Connect a model in Settings to get a review - Ollama is free and runs locally.</div>"+(r.footer?"<div class='rfoot'>"+esc(r.footer)+"</div>":"");return;}
|
|
1193
|
-
var body=r.text||'(no output)';reviewCache[path]={text:body,ideas:r.ideas||[],footer:r.footer};el.innerHTML=reviewHtml(name,body,r.ideas||[],tasks,r.footer);wireIdeas();});}
|
|
1194
|
-
function
|
|
1195
|
-
|
|
1196
|
-
el.innerHTML=h;
|
|
1197
|
-
el.querySelectorAll('.task').forEach(function(row){var id=row.getAttribute('data-id');
|
|
1266
|
+
var body=r.text||'(no output)';reviewCache[path]={text:body,verdict:r.verdict||"",ideas:r.ideas||[],footer:r.footer};el.innerHTML=reviewHtml(name,body,r.verdict||"",r.ideas||[],tasks,r.footer);wireIdeas();});}
|
|
1267
|
+
function taskRow(t){return "<div class='task"+(t.done?" done":"")+"' data-id='"+esc(t.id)+"'><input type='checkbox' class='taskchk'"+(t.done?" checked":"")+"><span class='t'>"+esc(t.text)+"</span>"+(t.repo?"<span class='rp'>"+esc(t.repo)+"</span>":"")+"<button class='rm' title='remove'>×</button></div>";}
|
|
1268
|
+
function wireTaskRows(el){el.querySelectorAll('.task').forEach(function(row){var id=row.getAttribute('data-id');
|
|
1198
1269
|
row.querySelector('.taskchk').addEventListener('change',function(){api('/api/tasks/toggle',{id:id}).then(function(){row.classList.toggle('done');});});
|
|
1199
1270
|
row.querySelector('.rm').addEventListener('click',function(){api('/api/tasks/remove',{id:id}).then(function(){row.remove();if(!el.querySelector('.task'))renderTasks([]);});});});}
|
|
1271
|
+
function renderTasks(list){var el=document.getElementById('tasklist');if(!list||!list.length){el.innerHTML="<div class='muted' style='margin-top:12px'>No tasks yet. Tick an idea in a repo's review, or add one above.</div>";return;}
|
|
1272
|
+
var order=['Fixes','Tests & CI','Security','Performance','Refactor','UI/UX','Docs','Features & other'];
|
|
1273
|
+
var groups={};list.forEach(function(t){var ty=t.type||'Features & other';(groups[ty]=groups[ty]||[]).push(t);});
|
|
1274
|
+
var keys=Object.keys(groups).sort(function(a,b){var ia=order.indexOf(a),ib=order.indexOf(b);return (ia<0?99:ia)-(ib<0?99:ib);});
|
|
1275
|
+
var h="";keys.forEach(function(ty){h+="<div class='tgroup'>"+esc(ty)+" <span class='tcount'>"+groups[ty].length+"</span></div>";groups[ty].forEach(function(t){h+=taskRow(t);});});
|
|
1276
|
+
el.innerHTML=h;wireTaskRows(el);}
|
|
1200
1277
|
function loadTasks(){api('/api/tasks').then(renderTasks);}
|
|
1201
1278
|
function loadDrift(){var out=document.getElementById('driftout');out.innerHTML="<div class='muted' style='margin-top:12px'>Reading your repos…</div>";
|
|
1202
1279
|
var ci=document.getElementById('driftci').checked?'1':'0';var ft=document.getElementById('driftfetch').checked?'1':'0';
|
|
@@ -1218,8 +1295,17 @@ if(r.empty){o.innerHTML="<div class='muted' style='margin-top:10px'>No open task
|
|
|
1218
1295
|
var n=(r.written||[]).length;var h="<div class='drift' style='margin-top:10px'><div class='dh'><span class='dot-c'></span><span class='dn'>Done — wrote "+n+" file"+(n===1?"":"s")+"</span></div>";
|
|
1219
1296
|
if(n){h+="<ul>";r.written.forEach(function(w){h+="<li class='info'>✓ <b>"+esc(w.name)+"</b> <span class='ev'>"+esc(w.file)+" ("+w.count+" task"+(w.count===1?"":"s")+")</span></li>";});h+="</ul>";}
|
|
1220
1297
|
if(r.unresolved&&r.unresolved.length){h+="<div class='dd' style='margin-top:6px'>⚠ not written (repo not found under your home folder): "+esc(r.unresolved.map(function(u){return u.name;}).join(", "))+"</div>";}
|
|
1221
|
-
h+="<div class='dd' style='margin-top:8px'
|
|
1298
|
+
if(r.handoff&&r.written&&r.written.length){r.written.forEach(function(w){api('/api/open',{path:w.path});});h+="<div class='dd' style='margin-top:8px'>🤖 Handing "+n+" repo(s) to your agent (<b>"+esc(r.handoff)+"</b>)…</div>";}
|
|
1299
|
+
else{h+="<div class='dd' style='margin-top:8px'>Set an <b>agent command</b> in Settings to auto-run it on send. For now, tell your agent: <b>“Read .symbiot/TASKS.md and implement the unchecked items.”</b></div>";}
|
|
1300
|
+
h+="</div>";
|
|
1222
1301
|
o.innerHTML=h;}).catch(function(e){btn.disabled=false;o.innerHTML="<div class='err' style='margin-top:10px'>Couldn't write tasks: "+esc(String((e&&e.message)||e))+"</div>";});}
|
|
1302
|
+
function loadAgentCfg(){api('/api/agentcfg').then(function(d){document.getElementById('agentcmd').value=d.cmd||'';
|
|
1303
|
+
var chips=[];(d.agents||[]).forEach(function(a){chips.push(a);});(d.editors||[]).forEach(function(e){chips.push(e);});
|
|
1304
|
+
var box=document.getElementById('agentpresets');
|
|
1305
|
+
if(!chips.length){box.innerHTML="<span class='muted' style='font-size:12px'>Nothing detected on PATH — type your own command above.</span>";return;}
|
|
1306
|
+
box.innerHTML="<span class='muted' style='font-size:12px'>Detected — click to use:</span><br>"+chips.map(function(c,i){return "<button class='ghost preset' data-i='"+i+"' style='padding:4px 10px;font-size:12px;margin:5px 5px 0 0'>"+esc(c.label)+"</button>";}).join("");
|
|
1307
|
+
box.querySelectorAll('.preset').forEach(function(btn){btn.addEventListener('click',function(){document.getElementById('agentcmd').value=chips[+btn.getAttribute('data-i')].tmpl;saveAgent();});});});}
|
|
1308
|
+
function saveAgent(){api('/api/agentcmd',{cmd:document.getElementById('agentcmd').value});}
|
|
1223
1309
|
function selectNode(id){sel=id;render();updateBar(id);var n=nodeById(id);api('/api/node?id='+encodeURIComponent(id)).then(showDetail);
|
|
1224
1310
|
if(n&&n.type==='repo'&&n.meta&&n.meta.path){loadReview(n.label,n.meta.path);}else{hideReview();}}
|
|
1225
1311
|
function screenToGraph(el,ev){var rc=el.getBoundingClientRect();var mx=(ev.clientX-rc.left)/rc.width*GW;var my=(ev.clientY-rc.top)/rc.height*GH;return {x:(mx-view.x)/view.k,y:(my-view.y)/view.k};}
|
|
@@ -1251,8 +1337,9 @@ document.getElementById('recbtn').addEventListener('click',loadRec);
|
|
|
1251
1337
|
document.getElementById('driftrun').addEventListener('click',function(){driftLoaded=false;loadDrift();});
|
|
1252
1338
|
document.getElementById('addtask').addEventListener('click',addTaskUI);
|
|
1253
1339
|
document.getElementById('pushtasks').addEventListener('click',pushTasksUI);
|
|
1340
|
+
document.getElementById('agentcmd').addEventListener('change',saveAgent);
|
|
1254
1341
|
document.getElementById('newtask').addEventListener('keydown',function(e){if(e.key==='Enter')addTaskUI();});
|
|
1255
|
-
initGraphEvents();syncP();refresh();loadMap();
|
|
1342
|
+
initGraphEvents();syncP();refresh();loadMap();loadAgentCfg();
|
|
1256
1343
|
</script></body></html>`;
|
|
1257
1344
|
|
|
1258
1345
|
function readBody(req) {
|
|
@@ -1311,11 +1398,14 @@ async function cmdApp() {
|
|
|
1311
1398
|
if (u.pathname === "/api/node") return json(res, nodeDetail(u.searchParams.get("id") || "")); // local
|
|
1312
1399
|
if (u.pathname === "/api/suggest" && req.method === "POST") { const b = await readBody(req); return json(res, await repoSuggest(String(b.path || ""))); }
|
|
1313
1400
|
if (u.pathname === "/api/review" && req.method === "POST") { const b = await readBody(req); return json(res, await repoReview(String(b.path || ""))); }
|
|
1314
|
-
if (u.pathname === "/api/tasks" && req.method !== "POST") return json(res, loadTasks());
|
|
1401
|
+
if (u.pathname === "/api/tasks" && req.method !== "POST") return json(res, loadTasks().map((t) => ({ ...t, type: taskType(t.text) })));
|
|
1315
1402
|
if (u.pathname === "/api/tasks/add" && req.method === "POST") { const b = await readBody(req); return json(res, addTask(b.text, b.repo)); }
|
|
1316
1403
|
if (u.pathname === "/api/tasks/toggle" && req.method === "POST") { const b = await readBody(req); return json(res, toggleTask(String(b.id || ""))); }
|
|
1317
1404
|
if (u.pathname === "/api/tasks/remove" && req.method === "POST") { const b = await readBody(req); return json(res, removeTask(String(b.id || ""))); }
|
|
1318
1405
|
if (u.pathname === "/api/tasks/push" && req.method === "POST") return json(res, pushTasks());
|
|
1406
|
+
if (u.pathname === "/api/agentcfg") { const d = detectHandoffs(); return json(res, { cmd: handoffCmd(), agents: d.agents, editors: d.editors }); }
|
|
1407
|
+
if (u.pathname === "/api/agentcmd" && req.method === "POST") { const b = await readBody(req); const cfg = loadConfig(); const v = String(b.cmd || "").trim(); if (v) cfg.agentCmd = v; else delete cfg.agentCmd; delete cfg.ide; saveConfig(cfg); return json(res, { ok: true, cmd: cfg.agentCmd || "" }); }
|
|
1408
|
+
if (u.pathname === "/api/open" && req.method === "POST") { const b = await readBody(req); return json(res, { opened: runHandoff(String(b.path || "")) }); }
|
|
1319
1409
|
if (u.pathname === "/api/run" && req.method === "POST") { const b = await readBody(req); const cmd = ["week", "standup", "todo"].includes(b.cmd) ? b.cmd : "week"; return json(res, await produce(cmd)); }
|
|
1320
1410
|
if (u.pathname === "/api/connect" && req.method === "POST") { return json(res, await connectProvider(await readBody(req))); }
|
|
1321
1411
|
if (u.pathname === "/api/quit") { res.writeHead(200); res.end("bye"); setTimeout(() => process.exit(0), 150); return; }
|
|
@@ -1339,7 +1429,7 @@ ${c.b("Usage")}
|
|
|
1339
1429
|
symbiot todo what's still on your plate
|
|
1340
1430
|
symbiot app open the visual app in your browser
|
|
1341
1431
|
symbiot drift what's out of sync / at risk across repos
|
|
1342
|
-
symbiot push
|
|
1432
|
+
symbiot push [--open] write tasks into each repo (and open your IDE)
|
|
1343
1433
|
symbiot models recommend AI models for your hardware
|
|
1344
1434
|
symbiot login connect it to an AI (once)
|
|
1345
1435
|
symbiot whoami show how it's connected
|
|
@@ -1387,4 +1477,4 @@ const isMain = (() => {
|
|
|
1387
1477
|
})();
|
|
1388
1478
|
if (isMain) main();
|
|
1389
1479
|
|
|
1390
|
-
export { authorship, repoState, readmeInfo, repoShape, houseRules, findAllRepos, buildMap, reportFooter, detectHardware, recommendModels, computeDrift, driftRepo, gitDefaultBranch, buildTasksMd, EMBEDDED_UI };
|
|
1480
|
+
export { authorship, repoState, readmeInfo, repoShape, houseRules, findAllRepos, buildMap, reportFooter, detectHardware, recommendModels, computeDrift, driftRepo, gitDefaultBranch, buildTasksMd, taskType, EMBEDDED_UI };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "symbiot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Your week, written from your real work. Reads your local git and writes it up with the AI of your choice — Claude, OpenAI, Gemini, or a local model.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|