symbiot 0.9.4 → 0.10.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.
Files changed (3) hide show
  1. package/README.md +13 -0
  2. package/index.mjs +72 -2
  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,62 @@ 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
+ // Resolve repo name -> path from the already-scanned map when we have it
126
+ // (avoids a fresh full scan); fall back to a scan only if needed.
127
+ const byName = {};
128
+ const src = (LAST_MAP && LAST_MAP.nodes) ? LAST_MAP.nodes.filter((n) => n.type === "repo").map((n) => ({ name: n.label, path: n.meta && n.meta.path })) : findAllRepos(BASE);
129
+ for (const r of src) if (r.path && !byName[r.name]) byName[r.name] = r.path;
130
+ const groups = {}; for (const t of tasks) { const k = t.repo || ""; (groups[k] = groups[k] || []).push(t); }
131
+ const written = [], unresolved = [];
132
+ for (const name of Object.keys(groups)) {
133
+ const list = groups[name], path = name && byName[name];
134
+ if (!path) { unresolved.push({ name: name || "(no repo)", count: list.length }); continue; }
135
+ try {
136
+ // Cheap, per-repo signals only — no full drift scan (that can be very slow
137
+ // on big repos and would block the request).
138
+ const st = repoState(path);
139
+ const commits = sh(`git -C ${JSON.stringify(path)} log --format='%s' -10 2>/dev/null`).split("\n").filter(Boolean);
140
+ const open = openWork([{ path, name }]).slice(0, 12);
141
+ const det = detectRepo({ path, name, recency: 0 });
142
+ const stack = [...det.langs.slice(0, 4), ...det.tools].join(", ");
143
+ const risk = [];
144
+ if (st.stale) risk.push("stale checkout — working tree is an old snapshot, not new work");
145
+ if (st.behind) risk.push(`${st.behind} behind upstream on ${st.branch}`);
146
+ if (st.dirty && !st.stale) risk.push(`${st.dirty} uncommitted (${st.mod} mod / ${st.del} del / ${st.add} new)`);
147
+ const dir = join(path, ".symbiot"); mkdirSync(dir, { recursive: true });
148
+ const file = join(dir, "TASKS.md");
149
+ writeFileSync(file, buildTasksMd(name, { branch: st.branch, commits, open, drift: risk, stack }, list));
150
+ written.push({ name, file, count: list.length });
151
+ } catch (e) { unresolved.push({ name, count: list.length, error: String((e && e.message) || e) }); }
152
+ }
153
+ return { empty: false, written, unresolved };
154
+ }
155
+ function cmdPush() {
156
+ const r = pushTasks();
157
+ 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
+ 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
+ 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
+ console.log("\n" + c.d("Open each repo in your IDE/agent and point it at .symbiot/TASKS.md."));
161
+ }
106
162
  function envKey(provider) {
107
163
  for (const e of (PROVIDERS[provider].env || [])) if (process.env[e]) return process.env[e];
108
164
  return null;
@@ -1021,7 +1077,8 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
1021
1077
  <div class="rfoot" id="outfoot" style="display:none"></div>
1022
1078
  </section>
1023
1079
  <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>
1080
+ <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>
1081
+ <div id="pushout"></div>
1025
1082
  <div id="tasklist"></div>
1026
1083
  </section>
1027
1084
  <section id="panel-drift" class="hidden">
@@ -1154,6 +1211,15 @@ if(clean.length)h+="<div class='muted' style='margin-top:10px'>clean: "+esc(clea
1154
1211
  if(!repos.length)h+="<div class='muted'>No repos found under your home folder.</div>";
1155
1212
  out.innerHTML=h;});}
1156
1213
  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();});}
1214
+ function pushTasksUI(){var o=document.getElementById('pushout');var btn=document.getElementById('pushtasks');btn.disabled=true;
1215
+ o.innerHTML="<div class='muted' style='margin-top:10px'><span class='dot-c' style='background:var(--amber)'></span> Writing .symbiot/TASKS.md into your repos&hellip;</div>";
1216
+ api('/api/tasks/push').then(function(r){btn.disabled=false;
1217
+ 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;}
1218
+ 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 &mdash; wrote "+n+" file"+(n===1?"":"s")+"</span></div>";
1219
+ if(n){h+="<ul>";r.written.forEach(function(w){h+="<li class='info'>&#10003; <b>"+esc(w.name)+"</b> <span class='ev'>"+esc(w.file)+" ("+w.count+" task"+(w.count===1?"":"s")+")</span></li>";});h+="</ul>";}
1220
+ if(r.unresolved&&r.unresolved.length){h+="<div class='dd' style='margin-top:6px'>&#9888; 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'>Now tell your coding agent, in each repo: <b>“Read .symbiot/TASKS.md and implement the unchecked items.”</b></div></div>";
1222
+ o.innerHTML=h;}).catch(function(e){btn.disabled=false;o.innerHTML="<div class='err' style='margin-top:10px'>Couldn&#39;t write tasks: "+esc(String((e&&e.message)||e))+"</div>";});}
1157
1223
  function selectNode(id){sel=id;render();updateBar(id);var n=nodeById(id);api('/api/node?id='+encodeURIComponent(id)).then(showDetail);
1158
1224
  if(n&&n.type==='repo'&&n.meta&&n.meta.path){loadReview(n.label,n.meta.path);}else{hideReview();}}
1159
1225
  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};}
@@ -1184,6 +1250,7 @@ document.getElementById('remap').addEventListener('click',loadMap);
1184
1250
  document.getElementById('recbtn').addEventListener('click',loadRec);
1185
1251
  document.getElementById('driftrun').addEventListener('click',function(){driftLoaded=false;loadDrift();});
1186
1252
  document.getElementById('addtask').addEventListener('click',addTaskUI);
1253
+ document.getElementById('pushtasks').addEventListener('click',pushTasksUI);
1187
1254
  document.getElementById('newtask').addEventListener('keydown',function(e){if(e.key==='Enter')addTaskUI();});
1188
1255
  initGraphEvents();syncP();refresh();loadMap();
1189
1256
  </script></body></html>`;
@@ -1248,6 +1315,7 @@ async function cmdApp() {
1248
1315
  if (u.pathname === "/api/tasks/add" && req.method === "POST") { const b = await readBody(req); return json(res, addTask(b.text, b.repo)); }
1249
1316
  if (u.pathname === "/api/tasks/toggle" && req.method === "POST") { const b = await readBody(req); return json(res, toggleTask(String(b.id || ""))); }
1250
1317
  if (u.pathname === "/api/tasks/remove" && req.method === "POST") { const b = await readBody(req); return json(res, removeTask(String(b.id || ""))); }
1318
+ if (u.pathname === "/api/tasks/push" && req.method === "POST") return json(res, pushTasks());
1251
1319
  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)); }
1252
1320
  if (u.pathname === "/api/connect" && req.method === "POST") { return json(res, await connectProvider(await readBody(req))); }
1253
1321
  if (u.pathname === "/api/quit") { res.writeHead(200); res.end("bye"); setTimeout(() => process.exit(0), 150); return; }
@@ -1271,6 +1339,7 @@ ${c.b("Usage")}
1271
1339
  symbiot todo what's still on your plate
1272
1340
  symbiot app open the visual app in your browser
1273
1341
  symbiot drift what's out of sync / at risk across repos
1342
+ symbiot push write your tasks into each repo for your agent
1274
1343
  symbiot models recommend AI models for your hardware
1275
1344
  symbiot login connect it to an AI (once)
1276
1345
  symbiot whoami show how it's connected
@@ -1302,6 +1371,7 @@ async function main() {
1302
1371
  if (cmd === "app" || cmd === "ui") return cmdApp();
1303
1372
  if (cmd === "models" || cmd === "hardware") return cmdModels();
1304
1373
  if (cmd === "drift") return cmdDrift();
1374
+ if (cmd === "push") return cmdPush();
1305
1375
  if (cmd === "week") return cmdRun("week");
1306
1376
  if (cmd === "standup") return cmdRun("standup");
1307
1377
  if (cmd === "todo") return cmdRun("todo");
@@ -1317,4 +1387,4 @@ const isMain = (() => {
1317
1387
  })();
1318
1388
  if (isMain) main();
1319
1389
 
1320
- export { authorship, repoState, readmeInfo, repoShape, houseRules, findAllRepos, buildMap, reportFooter, detectHardware, recommendModels, computeDrift, driftRepo, gitDefaultBranch, EMBEDDED_UI };
1390
+ 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.4",
3
+ "version": "0.10.1",
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": {