symbiot 0.9.3 → 0.10.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.
Files changed (3) hide show
  1. package/README.md +13 -0
  2. package/index.mjs +67 -7
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -52,6 +52,19 @@ where no Chromium-family browser is found it opens your default browser instead.
52
52
  npm install -g symbiot # or run without installing: npx symbiot week
53
53
  ```
54
54
 
55
+ ## Hand tasks to your coding agent: `symbiot push`
56
+
57
+ ```bash
58
+ symbiot push # or the "Send to repos" button in the app's Tasks tab
59
+ ```
60
+
61
+ Writes a **`.symbiot/TASKS.md`** into each repo your tasks reference — a checklist
62
+ **plus the context an agent needs to get oriented**: the stack, recent commits,
63
+ open `TODO/FIXME` markers (with `file:line`), and current drift. Then point Claude
64
+ Code / Cursor / any agent at it: *"Read `.symbiot/TASKS.md` and implement the
65
+ unchecked items."* Tasks come from ticking a repo review's ideas, or adding your
66
+ own in the Tasks tab.
67
+
55
68
  ## What's out of sync? `symbiot drift`
56
69
 
57
70
  ```bash
package/index.mjs CHANGED
@@ -103,6 +103,53 @@ 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
+ // Push tasks into the repos as an agent-readable brief (.symbiot/TASKS.md):
107
+ // a checklist plus the deterministic context an agent needs to get oriented.
108
+ function buildTasksMd(name, ctx, list) {
109
+ const L = [];
110
+ L.push(`# Symbiot tasks — ${name}`);
111
+ L.push(`_generated ${new Date().toISOString().slice(0, 10)}${ctx.branch ? ` · branch ${ctx.branch}` : ""} · by symbiot ${VERSION}_`, "");
112
+ L.push("## Context for an AI agent", `Tasks for the \`${name}\` repo. Get oriented from the signals below before actioning them.`);
113
+ if (ctx.stack) L.push(`- **Stack:** ${ctx.stack}`);
114
+ if (ctx.commits && ctx.commits.length) { L.push("- **Recent commits:**"); for (const c of ctx.commits) L.push(` - ${c}`); }
115
+ if (ctx.open && ctx.open.length) { L.push("- **Open markers (TODO/FIXME + uncommitted):**"); for (const o of ctx.open) L.push(` - ${o}`); }
116
+ if (ctx.drift && ctx.drift.length) { L.push("- **Current drift / risk:**"); for (const d of ctx.drift) L.push(` - ${d}`); }
117
+ L.push("", "## Tasks");
118
+ for (const t of list) L.push(`- [ ] ${t.text}`);
119
+ 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
+ return L.join("\n") + "\n";
121
+ }
122
+ function pushTasks() {
123
+ const tasks = loadTasks().filter((t) => !t.done);
124
+ if (!tasks.length) return { empty: true, written: [], unresolved: [] };
125
+ const byName = {}; for (const r of findAllRepos(BASE)) if (!byName[r.name]) byName[r.name] = r.path;
126
+ const groups = {}; for (const t of tasks) { const k = t.repo || ""; (groups[k] = groups[k] || []).push(t); }
127
+ const written = [], unresolved = [];
128
+ for (const name of Object.keys(groups)) {
129
+ const list = groups[name], path = name && byName[name];
130
+ if (!path) { unresolved.push({ name: name || "(no repo)", count: list.length }); continue; }
131
+ try {
132
+ const st = repoState(path);
133
+ const commits = sh(`git -C ${JSON.stringify(path)} log --format='%s' -10 2>/dev/null`).split("\n").filter(Boolean);
134
+ const drift = driftRepo(path, {}).flags.map((f) => (f.level === "warn" ? "⚠ " : "") + f.text);
135
+ const open = openWork([{ path, name }]).slice(0, 12);
136
+ const det = detectRepo({ path, name, recency: 0 });
137
+ const stack = [...det.langs.slice(0, 4), ...det.tools].join(", ");
138
+ const dir = join(path, ".symbiot"); mkdirSync(dir, { recursive: true });
139
+ const file = join(dir, "TASKS.md");
140
+ writeFileSync(file, buildTasksMd(name, { branch: st.branch, commits, open, drift, stack }, list));
141
+ written.push({ name, file, count: list.length });
142
+ } catch (e) { unresolved.push({ name, count: list.length, error: String((e && e.message) || e) }); }
143
+ }
144
+ return { empty: false, written, unresolved };
145
+ }
146
+ function cmdPush() {
147
+ const r = pushTasks();
148
+ 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; }
149
+ 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" : "") + ")")}`); }
150
+ 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})`); }
151
+ console.log("\n" + c.d("Open each repo in your IDE/agent and point it at .symbiot/TASKS.md."));
152
+ }
106
153
  function envKey(provider) {
107
154
  for (const e of (PROVIDERS[provider].env || [])) if (process.env[e]) return process.env[e];
108
155
  return null;
@@ -1021,7 +1068,8 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
1021
1068
  <div class="rfoot" id="outfoot" style="display:none"></div>
1022
1069
  </section>
1023
1070
  <section id="panel-tasks" class="hidden">
1024
- <div class="row"><input id="newtask" placeholder="Add a task..." style="flex:1"><button class="act" id="addtask">Add</button></div>
1071
+ <div class="row"><input id="newtask" placeholder="Add a task..." style="flex:1"><button class="act" id="addtask">Add</button><button class="ghost" id="pushtasks" title="Write .symbiot/TASKS.md into each repo for your coding agent">Send to repos</button></div>
1072
+ <div id="pushout"></div>
1025
1073
  <div id="tasklist"></div>
1026
1074
  </section>
1027
1075
  <section id="panel-drift" class="hidden">
@@ -1114,18 +1162,19 @@ el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='chips'>"+chips+"</div><button
1114
1162
  document.getElementById('suggest').addEventListener('click',function(){var o=document.getElementById('sugout');o.innerHTML="<div class='out2'>Thinking...</div>";api('/api/suggest',{path:d.path}).then(function(r){if(r.error==='not-connected'){o.innerHTML="<div class='out2'>Connect a model in Settings to get suggestions - Ollama is free and runs locally.</div>";return;}o.innerHTML="<div class='out2'>"+esc(r.text||'(no output)')+"</div>"+(r.footer?"<div class='rfoot'>"+esc(r.footer)+"</div>":"");});});return;}
1115
1163
  if(d.type==='lang'||d.type==='tool'){var lis=(d.repos||[]).map(function(r){return "<li>"+esc(r)+"</li>";}).join("");el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='k'>Used in "+((d.repos||[]).length)+" repos</div><ul>"+lis+"</ul>";return;}
1116
1164
  if(d.type==='person'){var st=d.stats||{};el.innerHTML="<h3>"+esc(d.label)+"</h3><div class='k'>"+st.repos+" repos &middot; "+st.commits+" commits &middot; "+st.languages+" languages &middot; "+st.tools+" tools</div>";return;}}
1117
- var reviewCache={};
1165
+ var reviewCache={};var IDEAS=[];var IREPO="";
1118
1166
  function hideReview(){var el=document.getElementById('review');el.classList.add('hidden');el.innerHTML='';}
1119
- function reviewHtml(name,body,ideas,tasks,footer){
1167
+ function reviewHtml(name,body,ideas,tasks,footer){IDEAS=ideas||[];IREPO=name;
1120
1168
  var h="<h4>AI review &middot; <span class='rname'>"+esc(name)+"</span></h4><div class='body'>"+esc(body)+"</div>";
1121
1169
  if(ideas&&ideas.length){h+="<div class='ideas'><h4>Upgrade ideas &middot; tick to add to Tasks</h4>";
1122
- ideas.forEach(function(idea){var t=(tasks||[]).filter(function(x){return x.text===idea&&x.repo===name;})[0];var tid=t?t.id:"";
1123
- h+="<label class='idea'><input type='checkbox' class='ideachk' data-text=\""+esc(idea)+"\" data-repo=\""+esc(name)+"\" data-tid='"+tid+"'"+(t?" checked":"")+"><span>"+esc(idea)+"</span></label>";});
1170
+ ideas.forEach(function(idea,i){var t=(tasks||[]).filter(function(x){return x.text===idea&&x.repo===name;})[0];var tid=t?t.id:"";
1171
+ h+="<label class='idea'><input type='checkbox' class='ideachk' data-idx='"+i+"' data-tid='"+esc(tid)+"'"+(t?" checked":"")+"><span>"+esc(idea)+"</span></label>";});
1124
1172
  h+="</div>";}
1125
1173
  if(footer)h+="<div class='rfoot'>"+esc(footer)+"</div>";
1126
1174
  return h;}
1127
1175
  function wireIdeas(){document.querySelectorAll('.ideachk').forEach(function(cb){cb.addEventListener('change',function(){
1128
- if(cb.checked){api('/api/tasks/add',{text:cb.getAttribute('data-text'),repo:cb.getAttribute('data-repo')}).then(function(it){if(it&&it.id)cb.setAttribute('data-tid',it.id);});}
1176
+ var idea=IDEAS[+cb.getAttribute('data-idx')];if(idea==null)return;
1177
+ if(cb.checked){api('/api/tasks/add',{text:idea,repo:IREPO}).then(function(it){if(it&&it.id)cb.setAttribute('data-tid',it.id);});}
1129
1178
  else{var id=cb.getAttribute('data-tid');if(id){api('/api/tasks/remove',{id:id}).then(function(){cb.setAttribute('data-tid','');});}}});});}
1130
1179
  function loadReview(name,path){var el=document.getElementById('review');el.classList.remove('hidden');
1131
1180
  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;}
@@ -1153,6 +1202,13 @@ if(clean.length)h+="<div class='muted' style='margin-top:10px'>clean: "+esc(clea
1153
1202
  if(!repos.length)h+="<div class='muted'>No repos found under your home folder.</div>";
1154
1203
  out.innerHTML=h;});}
1155
1204
  function addTaskUI(){var i=document.getElementById('newtask');var v=(i.value||'').trim();if(!v)return;api('/api/tasks/add',{text:v,repo:''}).then(function(){i.value='';loadTasks();});}
1205
+ function pushTasksUI(){var o=document.getElementById('pushout');o.innerHTML="<div class='muted' style='margin-top:10px'>Writing .symbiot/TASKS.md into your repos&hellip;</div>";
1206
+ api('/api/tasks/push').then(function(r){if(r.empty){o.innerHTML="<div class='muted' style='margin-top:10px'>No open tasks to send. Tick ideas in a repo review, or add tasks above.</div>";return;}
1207
+ var h="<div class='drift' style='margin-top:10px'>";
1208
+ if(r.written&&r.written.length){h+="<div class='dn'>Sent to "+r.written.length+" repo(s):</div><ul>";r.written.forEach(function(w){h+="<li class='info'>&middot; <b>"+esc(w.name)+"</b> &rarr; "+esc(w.file)+" <span class='ev'>("+w.count+")</span></li>";});h+="</ul>";}
1209
+ if(r.unresolved&&r.unresolved.length){h+="<div class='dd' style='margin-top:6px'>Not written (repo not found): "+esc(r.unresolved.map(function(u){return u.name;}).join(", "))+"</div>";}
1210
+ h+="<div class='dd' style='margin-top:8px'>Point your agent at <b>.symbiot/TASKS.md</b> in each repo.</div></div>";
1211
+ o.innerHTML=h;});}
1156
1212
  function selectNode(id){sel=id;render();updateBar(id);var n=nodeById(id);api('/api/node?id='+encodeURIComponent(id)).then(showDetail);
1157
1213
  if(n&&n.type==='repo'&&n.meta&&n.meta.path){loadReview(n.label,n.meta.path);}else{hideReview();}}
1158
1214
  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};}
@@ -1183,6 +1239,7 @@ document.getElementById('remap').addEventListener('click',loadMap);
1183
1239
  document.getElementById('recbtn').addEventListener('click',loadRec);
1184
1240
  document.getElementById('driftrun').addEventListener('click',function(){driftLoaded=false;loadDrift();});
1185
1241
  document.getElementById('addtask').addEventListener('click',addTaskUI);
1242
+ document.getElementById('pushtasks').addEventListener('click',pushTasksUI);
1186
1243
  document.getElementById('newtask').addEventListener('keydown',function(e){if(e.key==='Enter')addTaskUI();});
1187
1244
  initGraphEvents();syncP();refresh();loadMap();
1188
1245
  </script></body></html>`;
@@ -1247,6 +1304,7 @@ async function cmdApp() {
1247
1304
  if (u.pathname === "/api/tasks/add" && req.method === "POST") { const b = await readBody(req); return json(res, addTask(b.text, b.repo)); }
1248
1305
  if (u.pathname === "/api/tasks/toggle" && req.method === "POST") { const b = await readBody(req); return json(res, toggleTask(String(b.id || ""))); }
1249
1306
  if (u.pathname === "/api/tasks/remove" && req.method === "POST") { const b = await readBody(req); return json(res, removeTask(String(b.id || ""))); }
1307
+ if (u.pathname === "/api/tasks/push" && req.method === "POST") return json(res, pushTasks());
1250
1308
  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)); }
1251
1309
  if (u.pathname === "/api/connect" && req.method === "POST") { return json(res, await connectProvider(await readBody(req))); }
1252
1310
  if (u.pathname === "/api/quit") { res.writeHead(200); res.end("bye"); setTimeout(() => process.exit(0), 150); return; }
@@ -1270,6 +1328,7 @@ ${c.b("Usage")}
1270
1328
  symbiot todo what's still on your plate
1271
1329
  symbiot app open the visual app in your browser
1272
1330
  symbiot drift what's out of sync / at risk across repos
1331
+ symbiot push write your tasks into each repo for your agent
1273
1332
  symbiot models recommend AI models for your hardware
1274
1333
  symbiot login connect it to an AI (once)
1275
1334
  symbiot whoami show how it's connected
@@ -1301,6 +1360,7 @@ async function main() {
1301
1360
  if (cmd === "app" || cmd === "ui") return cmdApp();
1302
1361
  if (cmd === "models" || cmd === "hardware") return cmdModels();
1303
1362
  if (cmd === "drift") return cmdDrift();
1363
+ if (cmd === "push") return cmdPush();
1304
1364
  if (cmd === "week") return cmdRun("week");
1305
1365
  if (cmd === "standup") return cmdRun("standup");
1306
1366
  if (cmd === "todo") return cmdRun("todo");
@@ -1316,4 +1376,4 @@ const isMain = (() => {
1316
1376
  })();
1317
1377
  if (isMain) main();
1318
1378
 
1319
- export { authorship, repoState, readmeInfo, repoShape, houseRules, findAllRepos, buildMap, reportFooter, detectHardware, recommendModels, computeDrift, driftRepo, gitDefaultBranch };
1379
+ export { authorship, repoState, readmeInfo, repoShape, houseRules, findAllRepos, buildMap, reportFooter, detectHardware, recommendModels, computeDrift, driftRepo, gitDefaultBranch, buildTasksMd, EMBEDDED_UI };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "symbiot",
3
- "version": "0.9.3",
3
+ "version": "0.10.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": {