symbiot 0.9.4 → 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 +61 -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,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">
@@ -1154,6 +1202,13 @@ if(clean.length)h+="<div class='muted' style='margin-top:10px'>clean: "+esc(clea
1154
1202
  if(!repos.length)h+="<div class='muted'>No repos found under your home folder.</div>";
1155
1203
  out.innerHTML=h;});}
1156
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;});}
1157
1212
  function selectNode(id){sel=id;render();updateBar(id);var n=nodeById(id);api('/api/node?id='+encodeURIComponent(id)).then(showDetail);
1158
1213
  if(n&&n.type==='repo'&&n.meta&&n.meta.path){loadReview(n.label,n.meta.path);}else{hideReview();}}
1159
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};}
@@ -1184,6 +1239,7 @@ document.getElementById('remap').addEventListener('click',loadMap);
1184
1239
  document.getElementById('recbtn').addEventListener('click',loadRec);
1185
1240
  document.getElementById('driftrun').addEventListener('click',function(){driftLoaded=false;loadDrift();});
1186
1241
  document.getElementById('addtask').addEventListener('click',addTaskUI);
1242
+ document.getElementById('pushtasks').addEventListener('click',pushTasksUI);
1187
1243
  document.getElementById('newtask').addEventListener('keydown',function(e){if(e.key==='Enter')addTaskUI();});
1188
1244
  initGraphEvents();syncP();refresh();loadMap();
1189
1245
  </script></body></html>`;
@@ -1248,6 +1304,7 @@ async function cmdApp() {
1248
1304
  if (u.pathname === "/api/tasks/add" && req.method === "POST") { const b = await readBody(req); return json(res, addTask(b.text, b.repo)); }
1249
1305
  if (u.pathname === "/api/tasks/toggle" && req.method === "POST") { const b = await readBody(req); return json(res, toggleTask(String(b.id || ""))); }
1250
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());
1251
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)); }
1252
1309
  if (u.pathname === "/api/connect" && req.method === "POST") { return json(res, await connectProvider(await readBody(req))); }
1253
1310
  if (u.pathname === "/api/quit") { res.writeHead(200); res.end("bye"); setTimeout(() => process.exit(0), 150); return; }
@@ -1271,6 +1328,7 @@ ${c.b("Usage")}
1271
1328
  symbiot todo what's still on your plate
1272
1329
  symbiot app open the visual app in your browser
1273
1330
  symbiot drift what's out of sync / at risk across repos
1331
+ symbiot push write your tasks into each repo for your agent
1274
1332
  symbiot models recommend AI models for your hardware
1275
1333
  symbiot login connect it to an AI (once)
1276
1334
  symbiot whoami show how it's connected
@@ -1302,6 +1360,7 @@ async function main() {
1302
1360
  if (cmd === "app" || cmd === "ui") return cmdApp();
1303
1361
  if (cmd === "models" || cmd === "hardware") return cmdModels();
1304
1362
  if (cmd === "drift") return cmdDrift();
1363
+ if (cmd === "push") return cmdPush();
1305
1364
  if (cmd === "week") return cmdRun("week");
1306
1365
  if (cmd === "standup") return cmdRun("standup");
1307
1366
  if (cmd === "todo") return cmdRun("todo");
@@ -1317,4 +1376,4 @@ const isMain = (() => {
1317
1376
  })();
1318
1377
  if (isMain) main();
1319
1378
 
1320
- export { authorship, repoState, readmeInfo, repoShape, houseRules, findAllRepos, buildMap, reportFooter, detectHardware, recommendModels, computeDrift, driftRepo, gitDefaultBranch, EMBEDDED_UI };
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.4",
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": {