symbiot 0.63.4 → 0.63.5
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/CHANGELOG.md +4 -0
- package/agents.mjs +35 -12
- package/home.mjs +2 -2
- package/package.json +1 -1
- package/tasks.mjs +19 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
What each Symbiot release brought, newest first. When Approve bumps the version, it writes that release here from the tasks it approved, worded as release notes by your connected AI (or in the tasks' own words without one). The app shows the releases newer than yours under "What's new" when it offers an update, and once more after you update.
|
|
4
4
|
|
|
5
|
+
## 0.63.5
|
|
6
|
+
|
|
7
|
+
- **Home stops stalling on git.** Every few seconds Home asked git, one project after another, what each of your last 60 agent runs changed, and what's waiting for review. The app couldn't do anything else while it waited: 20 seconds in every 45 on a busy day. It now reuses each answer and refreshes it in the background, so Home answers straight away. Approve still checks git fresh, and Home forgets what it kept as soon as you approve.
|
|
8
|
+
|
|
5
9
|
## 0.63.4
|
|
6
10
|
|
|
7
11
|
- **Symbiot no longer freezes as your task list grows.** Every 20 seconds it checks the ideas your agents left against all your tasks, and with a few hundred tasks that took up to 10 seconds a time, until the app stopped answering altogether. The update to 0.63.3 then couldn't restart it. The same check now takes a fraction of a second.
|
package/agents.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// tab, and the questions an unattended agent leaves for the user.
|
|
4
4
|
import { MARKETING_DIR } from "./marketing.mjs";
|
|
5
5
|
import { licenceState, isPro, can, canWorkIn } from "./licence.mjs";
|
|
6
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
6
|
+
import { spawn, spawnSync, execFile } from "node:child_process";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { join, dirname, resolve, relative } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
@@ -957,18 +957,41 @@ function track(name, cmd, cwd, onExit, env) {
|
|
|
957
957
|
// Agent-agnostic "what did it do": read it straight from git, so it works the
|
|
958
958
|
// same whoever the agent was (Claude, Codex/GPT, Aider, Gemini, Cursor…). Shows
|
|
959
959
|
// current working-tree changes + any commits the agent made since it started.
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
960
|
+
// The answer is kept a while (CHANGES): Home and the Agents tab ask every few seconds
|
|
961
|
+
// for every earlier run (up to 60), and three or four git commands each, one after
|
|
962
|
+
// another, held the whole app up 20s in every 45 (2026-10-12). One older than
|
|
963
|
+
// CHANGES_FRESH is still given while git is asked again in the background, so a
|
|
964
|
+
// screen never waits on git for a run it has seen before.
|
|
965
|
+
const CHANGES = new Map(), CHANGES_FRESH = 15000, CHANGES_MAX = 300;
|
|
966
|
+
const NO_CHANGES = { dirty: 0, stat: "", commits: [] };
|
|
967
|
+
function changesOf(path, startedAt, git) {
|
|
968
|
+
// Don't count Symbiot's own .symbiot/ dir (agent.log, TASKS.md) as the agent's work.
|
|
969
|
+
const since = new Date(startedAt || Date.now()).toISOString();
|
|
970
|
+
const read = ([porcelain, stat, cached, raw]) => {
|
|
971
|
+
porcelain = porcelain.trim(); raw = raw.trim();
|
|
965
972
|
const dirty = porcelain ? porcelain.split("\n").filter((l) => { const p = l.slice(3); return p !== ".symbiot" && p !== ".symbiot/" && p.indexOf(".symbiot/") !== 0; }).length : 0;
|
|
966
|
-
const stat = (sh(`git -C ${q} diff --shortstat`).trim() || sh(`git -C ${q} diff --cached --shortstat`).trim()).replace(/^\s+/, "");
|
|
967
|
-
const since = new Date(startedAt || Date.now()).toISOString();
|
|
968
|
-
const raw = sh(`git -C ${q} log --since=${JSON.stringify(since)} --pretty=%h\u0001%s`).trim();
|
|
969
973
|
const commits = raw ? raw.split("\n").slice(0, 8).map((l) => { const i = l.indexOf("\u0001"); return { hash: l.slice(0, i), msg: l.slice(i + 1) }; }) : [];
|
|
970
|
-
return { dirty, stat, commits };
|
|
971
|
-
}
|
|
974
|
+
return { dirty, stat: (stat.trim() || cached.trim()).replace(/^\s+/, ""), commits };
|
|
975
|
+
};
|
|
976
|
+
const asks = [["status", "--porcelain"], ["diff", "--shortstat"], ["diff", "--cached", "--shortstat"], ["log", `--since=${since}`, "--pretty=%h\u0001%s"]];
|
|
977
|
+
const outs = asks.map((a) => git(["-C", path, ...a]));
|
|
978
|
+
return outs.some((o) => o && typeof o.then === "function") ? Promise.all(outs).then(read) : read(outs);
|
|
979
|
+
}
|
|
980
|
+
const gitNow = (args) => { const r = spawnSync("git", args, { encoding: "utf8", timeout: 6000, killSignal: "SIGKILL", maxBuffer: 32 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"] }); return r.status === 0 ? String(r.stdout || "") : ""; };
|
|
981
|
+
const gitLater = (args) => new Promise((res) => execFile("git", args, { encoding: "utf8", timeout: 6000, killSignal: "SIGKILL", maxBuffer: 32 * 1024 * 1024 }, (e, out) => res(e ? "" : String(out || ""))));
|
|
982
|
+
function agentChanges(path, startedAt) {
|
|
983
|
+
const had = CHANGES.get(path), now = Date.now();
|
|
984
|
+
// the same run: a run's start is only known roughly when it began before a restart
|
|
985
|
+
if (had && Math.abs(had.startedAt - (startedAt || now)) < 60000) {
|
|
986
|
+
if (now - had.at > CHANGES_FRESH && !had.busy) {
|
|
987
|
+
had.busy = true;
|
|
988
|
+
Promise.resolve(changesOf(path, startedAt, gitLater)).then((v) => { CHANGES.set(path, { at: Date.now(), startedAt: had.startedAt, v }); }, () => { had.busy = false; });
|
|
989
|
+
}
|
|
990
|
+
return had.v;
|
|
991
|
+
}
|
|
992
|
+
let v = NO_CHANGES; try { v = changesOf(path, startedAt, gitNow); } catch {}
|
|
993
|
+
if (CHANGES.size >= CHANGES_MAX) CHANGES.clear();
|
|
994
|
+
CHANGES.set(path, { at: now, startedAt: startedAt || now, v }); return v;
|
|
972
995
|
}
|
|
973
996
|
// ---- agent questions: decisions, options and ideas, from ANY agent ---------
|
|
974
997
|
// A handoff runs unattended (claude -p, codex exec, aider --message, …), so the
|
|
@@ -1327,4 +1350,4 @@ function agentsList() {
|
|
|
1327
1350
|
}).concat(earlierRuns());
|
|
1328
1351
|
}
|
|
1329
1352
|
|
|
1330
|
-
export { toKnownHost, continueLanes, autoPick, autoDecide, FACTS, factsOf, knownRun, withStream, workOf, HANDOFFS, HANDOFF_PROMPT, QUESTIONS_MAX, OPTIONS_SHOWN, IDEAS_SHOWN, shSingle, CLAUDE_CMD, ORCA_CLAUDE_CMD, handoffCmd, setHandoffCmd, grantAgent, grantRule, allowTool, claudeConnectors, withConnectors, linkedConnectors, connectorsLine, connectorsInfo, linkReach, fillHandoff, runHandoff, nothingToDo, PARKED_NOTE, parkedPaths, isParked, parkLane, agentMissing, blockedAgain, runningHandoff, loadRuns, earlierRuns, namedFiles, waitingFor, startWaiting, noteUntracked, untrackedBefore, forgetUntracked, writeTasks, droppedTasks, releaseHeldTasks, startHeldTasks, detectHandoffs, pickAgent, findOrcaCli, orcaHandoffCmd, migrateOrcaCmd, migrateClaudeCmd, track, agentChanges, parseQuestions, suggestionTarget, skipIdea, agentQuestions, answerQuestions, agentsList, needsOf, settleNeeds, NEEDS_FOR, readLastWords, readNeeds, parseRead, READ_FILE, autoAllow, autoAllowSweep, allowlistInWork, installAllowlist, inWork, withScope, withTrust, sandboxFor, sandboxEnv, sandboxNeeds, sandboxState, sandboxWrites, SANDBOX_DIR, resumeFor, noteSession, trustFull, GUARD_SETTINGS, isUrgent, urgentFirst, urgentUndo, urgentDone, urgentSweep, urgentState , licenceGate };
|
|
1353
|
+
export { CHANGES, toKnownHost, continueLanes, autoPick, autoDecide, FACTS, factsOf, knownRun, withStream, workOf, HANDOFFS, HANDOFF_PROMPT, QUESTIONS_MAX, OPTIONS_SHOWN, IDEAS_SHOWN, shSingle, CLAUDE_CMD, ORCA_CLAUDE_CMD, handoffCmd, setHandoffCmd, grantAgent, grantRule, allowTool, claudeConnectors, withConnectors, linkedConnectors, connectorsLine, connectorsInfo, linkReach, fillHandoff, runHandoff, nothingToDo, PARKED_NOTE, parkedPaths, isParked, parkLane, agentMissing, blockedAgain, runningHandoff, loadRuns, earlierRuns, namedFiles, waitingFor, startWaiting, noteUntracked, untrackedBefore, forgetUntracked, writeTasks, droppedTasks, releaseHeldTasks, startHeldTasks, detectHandoffs, pickAgent, findOrcaCli, orcaHandoffCmd, migrateOrcaCmd, migrateClaudeCmd, track, agentChanges, parseQuestions, suggestionTarget, skipIdea, agentQuestions, answerQuestions, agentsList, needsOf, settleNeeds, NEEDS_FOR, readLastWords, readNeeds, parseRead, READ_FILE, autoAllow, autoAllowSweep, allowlistInWork, installAllowlist, inWork, withScope, withTrust, sandboxFor, sandboxEnv, sandboxNeeds, sandboxState, sandboxWrites, SANDBOX_DIR, resumeFor, noteSession, trustFull, GUARD_SETTINGS, isUrgent, urgentFirst, urgentUndo, urgentDone, urgentSweep, urgentState , licenceGate };
|
package/home.mjs
CHANGED
|
@@ -302,7 +302,7 @@ function marketingGo(deps = {}) {
|
|
|
302
302
|
function homeState({ now = Date.now(), fresh = false, deps = {} } = {}) {
|
|
303
303
|
if (!fresh && !deps.board && cached && now - cached.at < KEEP) return cached;
|
|
304
304
|
const board = deps.board || (() => { try { return watchBoard(24, now); } catch { return { cards: [] }; } });
|
|
305
|
-
const pending = deps.pending || (() => { try { return pendingReview(); } catch { return []; } });
|
|
305
|
+
const pending = deps.pending || (() => { try { return pendingReview({ kept: true }); } catch { return []; } });
|
|
306
306
|
const agents = deps.agents || (() => { try { return agentsList(); } catch { return []; } });
|
|
307
307
|
const lanes = deps.lanes || (() => { try { return lanesState(); } catch { return { handoffs: [] }; } });
|
|
308
308
|
const reports = deps.reports || (() => { try { return reportsNews(); } catch { return { count: 0 }; } });
|
|
@@ -626,7 +626,7 @@ function runNow(a, { now = Date.now(), est = estimate } = {}) {
|
|
|
626
626
|
// not a sphere each with an id for a name.
|
|
627
627
|
function workScene({ deps = {}, full = false } = {}) {
|
|
628
628
|
const agents = deps.agents || (() => { try { return agentsList(); } catch { return []; } });
|
|
629
|
-
const pending = deps.pending || (() => { try { return pendingReview(); } catch { return []; } });
|
|
629
|
+
const pending = deps.pending || (() => { try { return pendingReview({ kept: true }); } catch { return []; } });
|
|
630
630
|
const tasks = deps.tasks || (() => { try { return loadTasks(); } catch { return []; } });
|
|
631
631
|
const map = (deps.repos || (() => { try { return laneMap(); } catch { return {}; } }))() || {};
|
|
632
632
|
const named = deps.name || displayName;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "symbiot",
|
|
3
|
-
"version": "0.63.
|
|
3
|
+
"version": "0.63.5",
|
|
4
4
|
"description": "Your week, written from your real work. Reads your local git and writes it up with the AI of your choice \u2014 Claude, OpenAI, Gemini, or a local model.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/tasks.mjs
CHANGED
|
@@ -439,17 +439,29 @@ function finishedOffer(path, open) {
|
|
|
439
439
|
// uncommitted changes no ticked task covers (untasked: approve them as-is, with
|
|
440
440
|
// `open`, the repo's open tasks, to tick the ones the run finished).
|
|
441
441
|
// running: an agent is still editing there, so its changes may be half done.
|
|
442
|
-
|
|
442
|
+
// A repo's git side of Review (what changed, what's unreleased), kept REVIEW_FRESH for
|
|
443
|
+
// the screens that only show it (pendingReview({ kept: true }): Home and its 20s
|
|
444
|
+
// tick), which asked git for every repo every few seconds and held the app up
|
|
445
|
+
// (2026-10-12). Approve asks git itself, and forgets what was kept (forgetReview).
|
|
446
|
+
const REVIEW_GIT = new Map(), REVIEW_FRESH = 15000;
|
|
447
|
+
function reviewGit(path, kept) {
|
|
448
|
+
const had = kept && REVIEW_GIT.get(path);
|
|
449
|
+
if (had && Date.now() - had.at < REVIEW_FRESH) return had.v;
|
|
450
|
+
const v = { ...workingChanges(path), unreleased: unreleased(path), bumpOffer: bumpOffer(path), publishesOnMerge: publishesOnMerge(path) };
|
|
451
|
+
REVIEW_GIT.set(path, { at: Date.now(), v }); return v;
|
|
452
|
+
}
|
|
453
|
+
const forgetReview = () => REVIEW_GIT.clear();
|
|
454
|
+
function pendingReview({ kept = false } = {}) {
|
|
443
455
|
const t = loadTasks(), by = {}; for (const x of t) if (x.review && !x.done && !x.archived) (by[x.repo] = by[x.repo] || []).push(x);
|
|
444
456
|
const sent = [...new Set(t.filter((x) => x.repo && !x.archived && !by[x.repo]).map((x) => x.repo))];
|
|
445
457
|
const map = Object.keys(by).length || sent.length ? laneMap() : {};
|
|
446
458
|
const am = autoMergeRepos();
|
|
447
|
-
const out = Object.keys(by).sort().map((repo) => { const path = map[repo] || ""; return { repo, path, tasks: by[repo], autoMerge: am.includes(repo), running: !!(path && runningHandoff(path)), ...(path ?
|
|
459
|
+
const out = Object.keys(by).sort().map((repo) => { const path = map[repo] || ""; return { repo, path, tasks: by[repo], autoMerge: am.includes(repo), running: !!(path && runningHandoff(path)), ...(path ? reviewGit(path, kept) : { branch: "", files: [], stat: "" }) }; });
|
|
448
460
|
for (const repo of sent.sort()) {
|
|
449
461
|
const path = map[repo]; if (!path || !existsSync(join(path, ".symbiot", "TASKS.md"))) continue;
|
|
450
|
-
const wc =
|
|
462
|
+
const wc = reviewGit(path, kept); if (!wc.files.length) continue;
|
|
451
463
|
const open = finishedOffer(path, t.filter((x) => x.repo === repo && !x.done && !x.archived && !x.review));
|
|
452
|
-
out.push({ repo, path, tasks: [], untasked: true, open, autoMerge: am.includes(repo), running: !!runningHandoff(path), ...wc
|
|
464
|
+
out.push({ repo, path, tasks: [], untasked: true, open, autoMerge: am.includes(repo), running: !!runningHandoff(path), ...wc });
|
|
453
465
|
}
|
|
454
466
|
return out;
|
|
455
467
|
}
|
|
@@ -569,6 +581,7 @@ async function shipChanges(path, texts, opts = {}) {
|
|
|
569
581
|
function autoMergeRepos() { const a = loadConfig().autoMerge; return Array.isArray(a) ? a : []; }
|
|
570
582
|
function setAutoMerge(repo, on) { const cfg = loadConfig(); let a = (Array.isArray(cfg.autoMerge) ? cfg.autoMerge : []).filter((x) => x !== repo); if (on && repo) a.push(repo); if (a.length) cfg.autoMerge = a; else delete cfg.autoMerge; saveConfig(cfg); return { ok: true, repos: cfg.autoMerge || [] }; }
|
|
571
583
|
async function approveRepo(repo, opts = {}) {
|
|
584
|
+
forgetReview();
|
|
572
585
|
const pro = can("ship"); if (!pro.ok) return { error: pro.why, pro: true }; // Pro (licence.mjs); the work stays on its branch
|
|
573
586
|
const waiting = (t) => t.filter((x) => x.repo === repo && x.review && !x.done && !x.archived);
|
|
574
587
|
const shipped = waiting(loadTasks());
|
|
@@ -590,6 +603,7 @@ async function approveRepo(repo, opts = {}) {
|
|
|
590
603
|
// repo's open tasks the changes finished (the run didn't tick them): they're
|
|
591
604
|
// approved with the changes, as Approve does, so they don't go out again.
|
|
592
605
|
async function approveChanges(repo, opts = {}) {
|
|
606
|
+
forgetReview();
|
|
593
607
|
const pro = can("ship"); if (!pro.ok) return { error: pro.why, pro: true };
|
|
594
608
|
if (loadTasks().some((x) => x.repo === repo && x.review && !x.done && !x.archived)) return await approveRepo(repo, opts);
|
|
595
609
|
const path = laneMap()[repo]; if (!path) return { error: "Repo not found: " + (repo || "(no repo)") };
|
|
@@ -710,4 +724,4 @@ function pushTasks(filter) {
|
|
|
710
724
|
return { empty: false, written, unresolved, handoff: handoffCmd() };
|
|
711
725
|
}
|
|
712
726
|
|
|
713
|
-
export { gitFailed, taskPaths, addTask, toggleTask, setTaskDone, removeTask, restoreTask, completedInRepo, removalOf, applyRemovals, applyDrops, syncTasks, workingChanges, workingDiff, publishesOnMerge, unreleased, bumpOffer, learnNpm, releaseNeeded, withReleases, setVersion, changelogEntry, changelogSection, noteChangelog, releaseInput, shipWithBump, runSummary, saidFinished, pendingReview, commitSubject, shipChanges, autoMergeRepos, setAutoMerge, approveRepo, approveChanges, sendBack, TASK_ORDER, taskType, buildTasksMd, pushTasks };
|
|
727
|
+
export { forgetReview, gitFailed, taskPaths, addTask, toggleTask, setTaskDone, removeTask, restoreTask, completedInRepo, removalOf, applyRemovals, applyDrops, syncTasks, workingChanges, workingDiff, publishesOnMerge, unreleased, bumpOffer, learnNpm, releaseNeeded, withReleases, setVersion, changelogEntry, changelogSection, noteChangelog, releaseInput, shipWithBump, runSummary, saidFinished, pendingReview, commitSubject, shipChanges, autoMergeRepos, setAutoMerge, approveRepo, approveChanges, sendBack, TASK_ORDER, taskType, buildTasksMd, pushTasks };
|