symbiot 0.10.2 → 0.11.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 (2) hide show
  1. package/index.mjs +34 -15
  2. package/package.json +1 -1
package/index.mjs CHANGED
@@ -766,25 +766,41 @@ async function repoReview(path) {
766
766
  const commits = sh(`git -C ${JSON.stringify(path)} log ${authorArgs(auth.emails)} --format='%ad %s' --date=short -50 2>/dev/null`).trim()
767
767
  || sh(`git -C ${JSON.stringify(path)} log --format='%ad %s' --date=short -50 2>/dev/null`).trim();
768
768
  const footer = reportFooter(path, auth, state, rd);
769
+ // Cheap maturity gauge, so the model can be told to show restraint instead of
770
+ // always inventing new features (over-engineering is a failure, not a win).
771
+ const testFiles = Number(sh(`git -C ${JSON.stringify(path)} ls-files 2>/dev/null | grep -icE '(^|/)(tests?|spec|__tests__)/|\\.(test|spec)\\.'`).trim()) || 0;
772
+ const churn = Number(sh(`git -C ${JSON.stringify(path)} log --since='14 days ago' --oneline 2>/dev/null | wc -l`).trim()) || 0;
773
+ const posture = state.stale ? "the checkout is a stale snapshot — the priority is getting current, not new work"
774
+ : (testFiles === 0 && shape.total > 40) ? "a real codebase with NO test files — favour tests/stabilising over new features"
775
+ : (churn >= 25) ? "very rapid churn lately — likely over-iterating; be conservative, prefer consolidation/finishing over new features"
776
+ : (state.dirty > 20) ? "a lot of unfinished/uncommitted work — finish what's open before starting new things"
777
+ : "no red flags — but still only propose what clearly pays off";
769
778
  const system =
770
- `You are reviewing one software project for its owner. Respond with ONLY a JSON object (no fences, no prose outside it): {"review": string, "ideas": string[]}. ` +
771
- `"review": 3-6 sentences on what it does and who it's for. "ideas": 3-6 short, concrete upgrade items, each a single actionable phrase. ` +
772
- `Ground every claim in the evidence below and tie it to real files/commits. Recent commits are MORE current than the README — when they disagree, trust the commits. ` +
773
- `Never propose building something that already appears in the structure. Never advise against anything the conventions call intentional. ` +
774
- (state.stale ? `CRITICAL: the working tree is a STALE/old checkout, not new work — never suggest committing it (it would revert history). ` : ``);
779
+ `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[]}. ` +
780
+ `"review": 3-6 sentences on what it does and who it's for. ` +
781
+ `"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. ` +
782
+ `"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. ` +
783
+ `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. ` +
784
+ (state.stale ? `CRITICAL: the working tree is a STALE checkout — never suggest committing it. ` : ``);
775
785
  const prompt =
776
- `Project: ${name}\nBranch ${state.branch} · ${auth.total} commits total (${auth.mineCount} yours${auth.filterDropped ? ", filter dropped so counting everyone" : ""})\n\n` +
786
+ `Project: ${name}\nBranch ${state.branch} · ${auth.total} commits total (${auth.mineCount} yours${auth.filterDropped ? ", filter dropped so counting everyone" : ""})\n` +
787
+ `Maturity signal: ${posture}. (${testFiles} test files, ${churn} commits in the last 14 days.)\n\n` +
777
788
  (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
789
  `Structure (top folders · file counts): ${shape.dirs.join(", ")}\nManifests: ${shape.manifests.join(", ") || "none"}\nDocs present: ${shape.docs.join(", ") || "none"}\n\n` +
779
790
  `Recent commits (newest first):\n${commits || "(none)"}\n\n` +
780
791
  (rules ? `Conventions this team has chosen — do NOT advise against these:\n${rules}\n\n` : "") +
781
792
  (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.`;
793
+ `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
794
  const raw = await write(system, prompt);
784
- if (!raw) return { text: "(couldn't reach the model)", ideas: [], footer };
795
+ if (!raw) return { text: "(couldn't reach the model)", ideas: [], verdict: "", footer };
785
796
  const j = extractJson(raw);
786
- if (j && (j.review || j.ideas)) return { text: String(j.review || "").trim() || raw, ideas: Array.isArray(j.ideas) ? j.ideas.map((x) => String(x).trim()).filter(Boolean).slice(0, 8) : [], footer };
787
- return { text: raw, ideas: [], footer };
797
+ if (j && (j.review || j.ideas || j.verdict)) return {
798
+ text: String(j.review || "").trim() || raw,
799
+ verdict: String(j.verdict || "").trim(),
800
+ ideas: Array.isArray(j.ideas) ? j.ideas.map((x) => String(x).trim()).filter(Boolean).slice(0, 5) : [],
801
+ footer,
802
+ };
803
+ return { text: raw, ideas: [], verdict: "", footer };
788
804
  }
789
805
  function extractJson(s) {
790
806
  if (!s) return null;
@@ -1015,6 +1031,7 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
1015
1031
  .review .body{color:var(--text);line-height:1.65;font-size:14px;white-space:pre-wrap}
1016
1032
  .review .rname{color:var(--bone);font-weight:600}
1017
1033
  .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}
1034
+ .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
1035
  .ideas{margin-top:14px}
1019
1036
  .ideas h4{margin:0 0 6px;color:var(--amber);font-size:11px;letter-spacing:.14em;text-transform:uppercase;font-weight:700}
1020
1037
  .idea{display:flex;align-items:flex-start;gap:9px;padding:5px 0;font-size:13.5px;color:var(--text)}
@@ -1173,12 +1190,14 @@ if(d.type==='lang'||d.type==='tool'){var lis=(d.repos||[]).map(function(r){retur
1173
1190
  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;}}
1174
1191
  var reviewCache={};var IDEAS=[];var IREPO="";
1175
1192
  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;
1193
+ function reviewHtml(name,body,verdict,ideas,tasks,footer){IDEAS=ideas||[];IREPO=name;
1177
1194
  var h="<h4>AI review &middot; <span class='rname'>"+esc(name)+"</span></h4><div class='body'>"+esc(body)+"</div>";
1178
- if(ideas&&ideas.length){h+="<div class='ideas'><h4>Upgrade ideas &middot; tick to add to Tasks</h4>";
1195
+ if(verdict)h+="<div class='verdict'>&#9878;&#65039; "+esc(verdict)+"</div>";
1196
+ if(ideas&&ideas.length){h+="<div class='ideas'><h4>Ideas worth doing &middot; tick to add to Tasks</h4>";
1179
1197
  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
1198
  h+="<label class='idea'><input type='checkbox' class='ideachk' data-idx='"+i+"' data-tid='"+esc(tid)+"'"+(t?" checked":"")+"><span>"+esc(idea)+"</span></label>";});
1181
1199
  h+="</div>";}
1200
+ else h+="<div class='muted' style='margin-top:10px'>No new features suggested &mdash; the verdict above is the call.</div>";
1182
1201
  if(footer)h+="<div class='rfoot'>"+esc(footer)+"</div>";
1183
1202
  return h;}
1184
1203
  function wireIdeas(){document.querySelectorAll('.ideachk').forEach(function(cb){cb.addEventListener('change',function(){
@@ -1186,11 +1205,11 @@ var idea=IDEAS[+cb.getAttribute('data-idx')];if(idea==null)return;
1186
1205
  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
1206
  else{var id=cb.getAttribute('data-tid');if(id){api('/api/tasks/remove',{id:id}).then(function(){cb.setAttribute('data-tid','');});}}});});}
1188
1207
  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;}
1208
+ 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
1209
  el.innerHTML="<h4>AI review &middot; <span class='rname'>"+esc(name)+"</span></h4><div class='body'>Reading the project&hellip;</div>";
1191
1210
  Promise.all([api('/api/review',{path:path}),api('/api/tasks')]).then(function(res){var r=res[0]||{},tasks=res[1]||[];
1192
1211
  if(r.error==='not-connected'){el.innerHTML="<h4>AI review &middot; <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();});}
1212
+ 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();});}
1194
1213
  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;}
1195
1214
  var h="";list.forEach(function(t){h+="<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'>&times;</button></div>";});
1196
1215
  el.innerHTML=h;
@@ -1285,7 +1304,7 @@ function chromeBinary() {
1285
1304
  function openApp(url) {
1286
1305
  try {
1287
1306
  const chrome = chromeBinary();
1288
- if (chrome) { spawn(chrome, [`--app=${url}`, "--new-window"], { detached: true, stdio: "ignore" }).unref(); return "app window"; }
1307
+ if (chrome) { spawn(chrome, [`--app=${url}`, "--new-window", "--no-first-run", "--no-default-browser-check"], { detached: true, stdio: "ignore" }).unref(); return "app window"; }
1289
1308
  // fall back to the OS default browser (a normal tab) — still fully functional
1290
1309
  if (process.platform === "win32") { spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref(); return "browser tab"; }
1291
1310
  spawn(process.platform === "darwin" ? "open" : "xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "symbiot",
3
- "version": "0.10.2",
3
+ "version": "0.11.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": {