symbiot 0.28.2 → 0.28.4
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/README.md +45 -40
- package/index.mjs +69 -53
- package/package.json +2 -2
- package/ui.mjs +7 -6
package/README.md
CHANGED
|
@@ -41,49 +41,42 @@ no install — it's the same CLI. Press Ctrl+C (or click Quit) to stop.
|
|
|
41
41
|
|
|
42
42
|
Tabs:
|
|
43
43
|
|
|
44
|
-
- **Map** — a live
|
|
45
|
-
you at the centre, your repos,
|
|
46
|
-
projects cluster),
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
- **Map** (the landing view) — a live node graph of your work from your local git:
|
|
45
|
+
you at the centre, your repos, the languages and tools they share (so related
|
|
46
|
+
projects cluster), the coding **agents and editors** you have installed, and the
|
|
47
|
+
**AI** powering Symbiot. Project folders that **aren't git repos** show up too
|
|
48
|
+
(see [Where it looks](#where-it-looks)). **Scroll to zoom, drag to pan, click a
|
|
49
|
+
node.** A repo's panel shows its branch, commits, uncommitted work and stack, a
|
|
50
50
|
**Suggest next steps** button, and an **AI review**: what the project does, a
|
|
51
|
-
one-line verdict on whether it needs new work at all (it
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
the scan runs, the Map shows live progress (which phase, how many repos, which
|
|
56
|
-
one). This is the landing view.
|
|
51
|
+
one-line verdict on whether it needs new work at all (it prefers stabilising over
|
|
52
|
+
new features), and ideas you can tick straight into **Tasks**. The graph needs
|
|
53
|
+
**no AI key**; reviews and suggestions use your chosen model. While the scan runs
|
|
54
|
+
the Map shows its progress.
|
|
57
55
|
- **Drift** — the [`symbiot drift`](#whats-out-of-sync-symbiot-drift) report, with
|
|
58
56
|
"fetch latest" and "check CI" toggles.
|
|
59
57
|
- **Week / Standup / Todo** — the write-ups (these use your chosen AI).
|
|
60
58
|
- **Tasks** — a checklist, grouped by kind (Fixes, Tests & CI, Docs, …). Filter by
|
|
61
59
|
type or repo, then **Send to repos** to hand just those to your agent (see
|
|
62
60
|
[`symbiot push`](#hand-tasks-to-your-coding-agent-symbiot-push)). What your agent
|
|
63
|
-
finishes lands in **Awaiting your review
|
|
64
|
-
[Review and approve](#review-and-approve-the-agents-work)).
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
- **Settings** —
|
|
70
|
-
|
|
61
|
+
finishes lands in **Awaiting your review** (see
|
|
62
|
+
[Review and approve](#review-and-approve-the-agents-work)). A task you tick
|
|
63
|
+
yourself is done and **auto-archives**; the archived view can restore it.
|
|
64
|
+
- **Agents** — every agent run Symbiot has started (and local-model downloads):
|
|
65
|
+
live status, elapsed time, exit code, the tail of its output, and **what it
|
|
66
|
+
did** — files changed and commits made, read from git, whichever agent it was.
|
|
67
|
+
- **Settings** — your AI, the folders to scan, your agent command, model
|
|
68
|
+
recommendations and a one-click local model.
|
|
71
69
|
|
|
72
70
|
**Stays current by itself.** The app checks npm for a newer Symbiot every couple of
|
|
73
|
-
minutes (and whenever you come back to the window)
|
|
74
|
-
|
|
75
|
-
app on the same address, and the open window reloads itself
|
|
76
|
-
|
|
77
|
-
same mechanism means that if you restart `symbiot app` yourself, the open window
|
|
78
|
-
reconnects and reloads on its own.
|
|
71
|
+
minutes (and whenever you come back to the window); the version you're on is shown
|
|
72
|
+
next to the name. When there's a newer one, **Update & restart** installs that exact
|
|
73
|
+
version, relaunches the app on the same address, and the open window reloads itself
|
|
74
|
+
onto it. Restart `symbiot app` yourself and the open window reconnects the same way.
|
|
79
75
|
|
|
80
76
|
The app listens on port **7391** so its address survives restarts; set
|
|
81
77
|
`SYMBIOT_PORT` to use another (it falls back to a free port if that one is busy).
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
and UI are just a local web page. The chrome-less window is detected per-OS
|
|
85
|
-
(PATH on Linux, the `/Applications` bundle on macOS, `Program Files` on Windows);
|
|
86
|
-
where no Chromium-family browser is found it opens your default browser instead.
|
|
78
|
+
It works on **Linux, macOS and Windows**; with no Chromium-family browser to open
|
|
79
|
+
the chrome-less window, it uses your default browser.
|
|
87
80
|
|
|
88
81
|
## Hand tasks to your coding agent: `symbiot push`
|
|
89
82
|
|
|
@@ -114,7 +107,8 @@ size of the uncommitted change and a **Show diff** button. For each repo:
|
|
|
114
107
|
on a feature branch it commits and opens the PR from there. Each step that can't
|
|
115
108
|
happen stops there and says why: no `origin` remote means a local commit only, and
|
|
116
109
|
without `gh` it pushes and stops. Your commit is never lost. Then the tasks are
|
|
117
|
-
archived with their commit and PR link.
|
|
110
|
+
archived with their commit and PR link. With no changes to commit the button
|
|
111
|
+
reads **Approve → archive** and just archives them.
|
|
118
112
|
- **↩ (send back)** is for one that isn't right: it reopens the task and unticks it in
|
|
119
113
|
`TASKS.md`, so the next **Send to repos** hands it to the agent again.
|
|
120
114
|
|
|
@@ -133,9 +127,10 @@ aider --message "{prompt}" --yes
|
|
|
133
127
|
code {dir}
|
|
134
128
|
```
|
|
135
129
|
|
|
136
|
-
Settings shows **one-click presets** for what's installed on your machine
|
|
130
|
+
Settings shows **one-click presets** for what's installed on your machine. A
|
|
131
|
+
preset only fills in the command box; the saved command is what runs.
|
|
137
132
|
|
|
138
|
-
- **Agents
|
|
133
|
+
- **Agents** (all make changes, ready for your review): Claude Code, Codex
|
|
139
134
|
(OpenAI), Aider, Cursor agent, Gemini CLI.
|
|
140
135
|
- **Orca IDE** (any OS): opens the repo in Orca — either just the repo, to use
|
|
141
136
|
Orca's own agent, or with Claude running in a new tab. It launches Orca if it's
|
|
@@ -146,7 +141,7 @@ Settings shows **one-click presets** for what's installed on your machine:
|
|
|
146
141
|
Each run is logged to `.symbiot/agent.log` in the repo and shown live in the
|
|
147
142
|
**Agents** tab. The command is saved as `agentCmd` in
|
|
148
143
|
`~/.config/symbiot/config.json` — it's your command, Symbiot only fills in
|
|
149
|
-
`{dir}` and `{prompt}`.
|
|
144
|
+
`{dir}` and `{prompt}`. A command saved from an older preset keeps working as-is.
|
|
150
145
|
|
|
151
146
|
## What's out of sync? `symbiot drift`
|
|
152
147
|
|
|
@@ -300,16 +295,26 @@ conventions every repo review must respect, alongside each repo's own
|
|
|
300
295
|
npm test
|
|
301
296
|
```
|
|
302
297
|
|
|
303
|
-
Runs
|
|
298
|
+
Runs four suites, all against throwaway repos and an isolated `HOME`:
|
|
304
299
|
|
|
300
|
+
- `test/load.mjs` — the shipped files parse and load: `node --check` on each
|
|
301
|
+
module, `ui.mjs` imported on its own with its page's JavaScript parsed, every
|
|
302
|
+
local import listed in package.json `"files"`, and the `bin` entry point shipped.
|
|
305
303
|
- `test/fixtures.mjs` — accuracy fixtures: the facts Symbiot collects (identity
|
|
306
304
|
matching, stale checkouts, worktrees, drift) and the `symbiot drift` report as
|
|
307
|
-
printed,
|
|
305
|
+
printed, the scan time limit, the agent handoff, and the review → approve cycle.
|
|
308
306
|
- `test/smoke.mjs` — boots `symbiot app`, runs the page's own JavaScript against
|
|
309
|
-
a fake DOM (every tab, every button),
|
|
307
|
+
a fake DOM (every tab, every button), hits every endpoint the UI calls with the
|
|
308
|
+
method the UI uses, and checks POST-only endpoints refuse GET and `/api` needs
|
|
309
|
+
the token.
|
|
310
310
|
- `test/install.mjs` — runs the CLI through a bin symlink, then `npm pack` +
|
|
311
|
-
global install into a temp prefix
|
|
312
|
-
|
|
311
|
+
global install into a temp prefix: `symbiot help`, every shipped file present,
|
|
312
|
+
and the installed `symbiot app` serving its page. Needs npm registry access for
|
|
313
|
+
dependencies; `SYMBIOT_SKIP_INSTALL_TEST=1` skips that part.
|
|
314
|
+
|
|
315
|
+
`SYMBIOT_NO_OPEN=1` stops `symbiot app` opening a window (the tests set it).
|
|
316
|
+
Releases publish from a version tag via `.github/workflows/publish.yml`, and only
|
|
317
|
+
after the full suite passes in CI.
|
|
313
318
|
|
|
314
319
|
---
|
|
315
320
|
|
package/index.mjs
CHANGED
|
@@ -182,7 +182,12 @@ function shipChanges(path, texts, opts = {}) {
|
|
|
182
182
|
branch = stem; for (let i = 2; git(path, ["rev-parse", "--verify", "-q", "refs/heads/" + branch]).ok; i++) branch = `${stem}-${i}`;
|
|
183
183
|
const sw = git(path, ["switch", "-c", branch]); if (!sw.ok) return { error: "Could not create branch: " + sw.err };
|
|
184
184
|
}
|
|
185
|
-
|
|
185
|
+
// Stage everything, then drop .symbiot. A `. :(exclude).symbiot` pathspec
|
|
186
|
+
// warns+exits-1 once .symbiot is gitignored ("paths are ignored, use -f"),
|
|
187
|
+
// which falsely aborted the ship. `add -A` skips gitignored paths silently;
|
|
188
|
+
// the reset also covers repos where .symbiot isn't ignored.
|
|
189
|
+
const add = git(path, ["add", "-A"]); if (!add.ok) return { error: "git add failed: " + add.err, branch };
|
|
190
|
+
git(path, ["reset", "-q", "--", ".symbiot"]); // never ship Symbiot's own scratch
|
|
186
191
|
const subject = texts.length === 1 ? (texts[0].length > 72 ? texts[0].slice(0, 71).replace(/\s+\S*$/, "") + "…" : texts[0]) : `symbiot: ${texts.length} approved tasks`;
|
|
187
192
|
const body = texts.map((x) => "- " + x).join("\n");
|
|
188
193
|
const cm = git(path, ["commit", "-m", subject, "-m", body]); if (!cm.ok) return { error: "Commit failed: " + (cm.err || cm.out), branch };
|
|
@@ -256,11 +261,7 @@ function pushTasks(filter) {
|
|
|
256
261
|
if (filter && filter.type) tasks = tasks.filter((t) => taskType(t.text) === filter.type);
|
|
257
262
|
if (filter && filter.repo) tasks = tasks.filter((t) => t.repo === filter.repo);
|
|
258
263
|
if (!tasks.length) return { empty: true, written: [], unresolved: [] };
|
|
259
|
-
|
|
260
|
-
// (avoids a fresh full scan); fall back to a scan only if needed.
|
|
261
|
-
const byName = {};
|
|
262
|
-
const src = (LAST_MAP && LAST_MAP.nodes) ? LAST_MAP.nodes.filter((n) => n.type === "repo" || n.type === "folder").map((n) => ({ name: n.label, path: n.meta && n.meta.path })) : findAllRepos();
|
|
263
|
-
for (const r of src) if (r.path && !byName[r.name]) byName[r.name] = r.path;
|
|
264
|
+
const byName = repoPathMap(); // from the already-scanned map when there is one
|
|
264
265
|
const groups = {}; for (const t of tasks) { const k = t.repo || ""; (groups[k] = groups[k] || []).push(t); }
|
|
265
266
|
const written = [], unresolved = [];
|
|
266
267
|
for (const name of Object.keys(groups)) {
|
|
@@ -717,44 +718,47 @@ function recommendModels(hw) {
|
|
|
717
718
|
return { local, paid, best };
|
|
718
719
|
}
|
|
719
720
|
// ---- hand a repo (+ its tasks) to the user's agent — generic, settings-based
|
|
720
|
-
// The handoff is a command TEMPLATE the user
|
|
721
|
-
//
|
|
721
|
+
// ONE code path, whatever the agent. The handoff is a command TEMPLATE the user
|
|
722
|
+
// saves (`agentCmd`), with {dir} (repo path) and {prompt} (the task
|
|
723
|
+
// instruction), and it is the only thing that ever runs:
|
|
724
|
+
// handoffCmd() -> fillHandoff() -> runHandoff() -> track()
|
|
725
|
+
// Everything else in this section only helps pick a template: one-click presets
|
|
726
|
+
// for what's installed (detectHandoffs), the Orca preset (a template too, kept
|
|
727
|
+
// current by migrateOrcaCmd) and the cross-platform lookups behind them.
|
|
722
728
|
// claude -p "{prompt}" · aider --message "{prompt}" · code {dir}
|
|
723
729
|
// gnome-terminal --working-directory={dir} -- claude "{prompt}"
|
|
724
730
|
// Not tied to any one tool — you decide what runs.
|
|
725
|
-
|
|
726
|
-
|
|
731
|
+
const shSingle = (s) => "'" + String(s).replace(/'/g, "'\\''") + "'";
|
|
732
|
+
const escDq = (s) => String(s).replace(/[\\"$`]/g, "\\$&");
|
|
733
|
+
// Plain words only: in the Orca preset this passes through two shells, so no
|
|
734
|
+
// backticks or $ (escDq covers one level).
|
|
735
|
+
const HANDOFF_PROMPT = "Read .symbiot/TASKS.md and implement the unchecked items in this repo. Tick each item [x] in that file as you finish it and leave your changes uncommitted, so they can be reviewed and approved. Confirm before anything destructive.";
|
|
736
|
+
function handoffCmd() { const cfg = loadConfig(); return migrateOrcaCmd(cfg.agentCmd) || (cfg.ide ? `${cfg.ide} {dir}` : ""); } // ide = legacy
|
|
737
|
+
// Save the template ("" clears it). Either way the legacy `ide` key goes.
|
|
738
|
+
function setHandoffCmd(cmd) {
|
|
739
|
+
const cfg = loadConfig(); const v = String(cmd || "").trim();
|
|
740
|
+
if (v) cfg.agentCmd = v; else delete cfg.agentCmd;
|
|
741
|
+
delete cfg.ide; saveConfig(cfg);
|
|
742
|
+
return { ok: true, cmd: cfg.agentCmd || "" };
|
|
743
|
+
}
|
|
744
|
+
const fillHandoff = (tmpl, repoPath) => tmpl.replace(/\{dir\}/g, shSingle(repoPath)).replace(/\{prompt\}/g, escDq(HANDOFF_PROMPT));
|
|
745
|
+
function runHandoff(repoPath) {
|
|
746
|
+
const tmpl = handoffCmd(); if (!tmpl || !repoPath) return null;
|
|
747
|
+
return track(repoPath.split("/").pop(), fillHandoff(tmpl, repoPath), repoPath);
|
|
748
|
+
}
|
|
749
|
+
// Presets. [cmd, label, macAppName] — macApp used to launch GUI editors on macOS
|
|
750
|
+
// where the CLI isn't on PATH (they're .app bundles).
|
|
727
751
|
const IDE_LIST = [["code", "VS Code", "Visual Studio Code"], ["cursor", "Cursor", "Cursor"], ["windsurf", "Windsurf", "Windsurf"], ["zed", "Zed", "Zed"], ["subl", "Sublime Text", "Sublime Text"], ["idea", "IntelliJ IDEA", "IntelliJ IDEA"], ["nvim", "Neovim", ""]];
|
|
752
|
+
// [cmd, label, template]. Only agents that leave changes to review: a handoff
|
|
753
|
+
// runs unattended, so a "plan only" run can't ask anything and leaves nothing to
|
|
754
|
+
// approve — that preset was dropped (a saved one still runs as-is).
|
|
728
755
|
const AGENT_LIST = [
|
|
729
756
|
["claude", "Claude Code — make changes", 'claude -p "{prompt}" --permission-mode acceptEdits'],
|
|
730
|
-
["claude", "Claude Code — plan only (asks first)", 'claude -p "{prompt}"'],
|
|
731
757
|
["codex", "Codex (OpenAI/GPT) — make changes", 'codex exec --full-auto "{prompt}"'],
|
|
732
758
|
["aider", "Aider — make changes", 'aider --message "{prompt}" --yes'],
|
|
733
759
|
["gemini", "Gemini — make changes", 'gemini --yolo -p "{prompt}"'],
|
|
734
760
|
["cursor-agent", "Cursor agent", 'cursor-agent -p "{prompt}"'],
|
|
735
761
|
];
|
|
736
|
-
// Cross-platform "is this command available?" (command -v on posix, where on win).
|
|
737
|
-
function hasCmd(cmd) { try { return !!sh(process.platform === "win32" ? `where ${cmd}` : `command -v ${cmd}`).trim(); } catch { return false; } }
|
|
738
|
-
function macApp(name) { if (process.platform !== "darwin" || !name) return ""; for (const base of ["/Applications", join(homedir(), "Applications")]) { try { if (existsSync(join(base, name + ".app"))) return name; } catch {} } return ""; }
|
|
739
|
-
// Find the Orca IDE CLI across OSes (known locations, then a bounded search).
|
|
740
|
-
let ORCA_CLI; // cached per process: undefined=unchecked, ""=none, string=path
|
|
741
|
-
function findOrcaCli() {
|
|
742
|
-
if (ORCA_CLI !== undefined) return ORCA_CLI;
|
|
743
|
-
ORCA_CLI = _findOrcaCli();
|
|
744
|
-
return ORCA_CLI;
|
|
745
|
-
}
|
|
746
|
-
function _findOrcaCli() {
|
|
747
|
-
const home = homedir(); const cands = [];
|
|
748
|
-
if (process.platform === "linux") cands.push(join(home, ".local/share/orca-ide/app/resources/bin/orca-ide"));
|
|
749
|
-
if (process.platform === "darwin") { cands.push(join(home, "Library/Application Support/orca-ide/app/resources/bin/orca-ide"), "/Applications/Orca.app/Contents/Resources/app/resources/bin/orca-ide", join(home, "Applications/Orca.app/Contents/Resources/app/resources/bin/orca-ide")); }
|
|
750
|
-
if (process.platform === "win32") { const la = process.env.LOCALAPPDATA || ""; cands.push(join(la, "orca-ide", "app", "resources", "bin", "orca-ide"), join(la, "Programs", "orca-ide", "resources", "app", "resources", "bin", "orca-ide")); }
|
|
751
|
-
for (const c of cands) { try { if (existsSync(c)) return c; } catch {} }
|
|
752
|
-
const roots = process.platform === "darwin" ? [join(home, "Library/Application Support"), "/Applications", join(home, "Applications")]
|
|
753
|
-
: process.platform === "win32" ? [process.env.LOCALAPPDATA || "", process.env.PROGRAMFILES || ""]
|
|
754
|
-
: [join(home, ".local/share"), "/opt", join(home, ".config")];
|
|
755
|
-
for (const r of roots) { if (!r) continue; const hit = sh(`find ${JSON.stringify(r)} -maxdepth 6 -name orca-ide -type f 2>/dev/null | head -1`).trim(); if (hit) return hit; }
|
|
756
|
-
return "";
|
|
757
|
-
}
|
|
758
762
|
function detectHandoffs() {
|
|
759
763
|
const editors = [];
|
|
760
764
|
for (const [cmd, label, app] of IDE_LIST) {
|
|
@@ -779,14 +783,8 @@ function detectHandoffs() {
|
|
|
779
783
|
}
|
|
780
784
|
return { agents, editors };
|
|
781
785
|
}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
// Plain words only: in the Orca preset this passes through two shells, so no
|
|
785
|
-
// backticks or $ (escDq covers one level).
|
|
786
|
-
const HANDOFF_PROMPT = "Read .symbiot/TASKS.md and implement the unchecked items in this repo. Tick each item [x] in that file as you finish it and leave your changes uncommitted, so they can be reviewed and approved. Confirm before anything destructive.";
|
|
787
|
-
function handoffCmd() { const cfg = loadConfig(); return migrateOrcaCmd(cfg.agentCmd) || (cfg.ide ? `${cfg.ide} {dir}` : ""); } // ide = legacy
|
|
788
|
-
// Build the full Orca handoff, cold-start safe. `bin` is the quoted orca-ide
|
|
789
|
-
// path; `commandPart` is e.g. ORCA_CLAUDE_CMD or "" (open only).
|
|
786
|
+
// The Orca preset. Build the full Orca handoff, cold-start safe. `bin` is the
|
|
787
|
+
// quoted orca-ide path; `commandPart` is e.g. ORCA_CLAUDE_CMD or "" (open only).
|
|
790
788
|
// open launches Orca & waits for the runtime to be REACHABLE, but on a cold
|
|
791
789
|
// start the workspace graph isn't ready yet (runtime.state=graph_not_ready) and
|
|
792
790
|
// `terminal create` times out — so we poll `status` until state=ready, then add
|
|
@@ -817,6 +815,30 @@ function migrateOrcaCmd(cmd) {
|
|
|
817
815
|
if (rebuilt !== cmd) { try { const cfg = loadConfig(); if (cfg.agentCmd === cmd) { cfg.agentCmd = rebuilt; saveConfig(cfg); } } catch {} }
|
|
818
816
|
return rebuilt;
|
|
819
817
|
}
|
|
818
|
+
// Cross-platform detection for the presets (and gh / the app's browser).
|
|
819
|
+
// "Is this command available?": command -v on posix, where on win.
|
|
820
|
+
function hasCmd(cmd) { try { return !!sh(process.platform === "win32" ? `where ${cmd}` : `command -v ${cmd}`).trim(); } catch { return false; } }
|
|
821
|
+
function macApp(name) { if (process.platform !== "darwin" || !name) return ""; for (const base of ["/Applications", join(homedir(), "Applications")]) { try { if (existsSync(join(base, name + ".app"))) return name; } catch {} } return ""; }
|
|
822
|
+
// Find the Orca IDE CLI across OSes (known locations, then a bounded search).
|
|
823
|
+
let ORCA_CLI; // cached per process: undefined=unchecked, ""=none, string=path
|
|
824
|
+
function findOrcaCli() {
|
|
825
|
+
if (ORCA_CLI !== undefined) return ORCA_CLI;
|
|
826
|
+
ORCA_CLI = _findOrcaCli();
|
|
827
|
+
return ORCA_CLI;
|
|
828
|
+
}
|
|
829
|
+
function _findOrcaCli() {
|
|
830
|
+
const home = homedir(); const cands = [];
|
|
831
|
+
if (process.platform === "linux") cands.push(join(home, ".local/share/orca-ide/app/resources/bin/orca-ide"));
|
|
832
|
+
if (process.platform === "darwin") { cands.push(join(home, "Library/Application Support/orca-ide/app/resources/bin/orca-ide"), "/Applications/Orca.app/Contents/Resources/app/resources/bin/orca-ide", join(home, "Applications/Orca.app/Contents/Resources/app/resources/bin/orca-ide")); }
|
|
833
|
+
if (process.platform === "win32") { const la = process.env.LOCALAPPDATA || ""; cands.push(join(la, "orca-ide", "app", "resources", "bin", "orca-ide"), join(la, "Programs", "orca-ide", "resources", "app", "resources", "bin", "orca-ide")); }
|
|
834
|
+
for (const c of cands) { try { if (existsSync(c)) return c; } catch {} }
|
|
835
|
+
const roots = process.platform === "darwin" ? [join(home, "Library/Application Support"), "/Applications", join(home, "Applications")]
|
|
836
|
+
: process.platform === "win32" ? [process.env.LOCALAPPDATA || "", process.env.PROGRAMFILES || ""]
|
|
837
|
+
: [join(home, ".local/share"), "/opt", join(home, ".config")];
|
|
838
|
+
for (const r of roots) { if (!r) continue; const hit = sh(`find ${JSON.stringify(r)} -maxdepth 6 -name orca-ide -type f 2>/dev/null | head -1`).trim(); if (hit) return hit; }
|
|
839
|
+
return "";
|
|
840
|
+
}
|
|
841
|
+
// ---- background jobs --------------------------------------------------------
|
|
820
842
|
// Run a shell command as a tracked, logged background job that shows up live in
|
|
821
843
|
// the Agents tab. Shared by the agent handoff and the local-model setup.
|
|
822
844
|
function track(name, cmd, cwd, onExit) {
|
|
@@ -851,11 +873,6 @@ function agentChanges(path, startedAt) {
|
|
|
851
873
|
return { dirty, stat, commits };
|
|
852
874
|
} catch { return { dirty: 0, stat: "", commits: [] }; }
|
|
853
875
|
}
|
|
854
|
-
const fillHandoff = (tmpl, repoPath) => tmpl.replace(/\{dir\}/g, shSingle(repoPath)).replace(/\{prompt\}/g, escDq(HANDOFF_PROMPT));
|
|
855
|
-
function runHandoff(repoPath) {
|
|
856
|
-
const tmpl = handoffCmd(); if (!tmpl || !repoPath) return null;
|
|
857
|
-
return track(repoPath.split("/").pop(), fillHandoff(tmpl, repoPath), repoPath);
|
|
858
|
-
}
|
|
859
876
|
// ---- local model one-command setup (Ollama), OS-aware ---------------------
|
|
860
877
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
861
878
|
function hasOllama() { return !!sh("ollama --version").trim(); } // cross-platform (not POSIX command -v)
|
|
@@ -923,7 +940,7 @@ function worktreeCount(repo) { return sh(`git -C ${JSON.stringify(repo)} worktre
|
|
|
923
940
|
// repo file, which could be attacker-controlled): ~/.config/symbiot/deploys.json
|
|
924
941
|
function loadDeploys() { try { return JSON.parse(readFileSync(join(CONFIG_DIR, "deploys.json"), "utf8")); } catch { return {}; } }
|
|
925
942
|
function ciState(repo, def) {
|
|
926
|
-
if (!
|
|
943
|
+
if (!hasCmd("gh")) return null;
|
|
927
944
|
if (!/github\.com/i.test(sh(`git -C ${JSON.stringify(repo)} remote get-url origin 2>/dev/null`))) return null;
|
|
928
945
|
const j = sh(`cd ${JSON.stringify(repo)} && gh run list --branch ${def} --limit 1 --json databaseId,conclusion,status 2>/dev/null`).trim();
|
|
929
946
|
if (!j) return null;
|
|
@@ -1472,7 +1489,6 @@ function readBody(req) {
|
|
|
1472
1489
|
req.on("end", () => { try { resolve(d ? JSON.parse(d) : {}); } catch { resolve({}); } });
|
|
1473
1490
|
});
|
|
1474
1491
|
}
|
|
1475
|
-
function onPath(cands) { for (const cc of cands) { if (sh(`command -v ${cc} 2>/dev/null`).trim()) return cc; } return null; }
|
|
1476
1492
|
// Find a Chromium-family browser for the chrome-less --app window, per OS.
|
|
1477
1493
|
function chromeBinary() {
|
|
1478
1494
|
const p = process.platform;
|
|
@@ -1491,7 +1507,7 @@ function chromeBinary() {
|
|
|
1491
1507
|
const w = sh("where chrome 2>NUL").split(/\r?\n/).map((s) => s.trim()).find(Boolean);
|
|
1492
1508
|
return w || null;
|
|
1493
1509
|
}
|
|
1494
|
-
return
|
|
1510
|
+
return ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "brave-browser", "microsoft-edge"].find(hasCmd) || null;
|
|
1495
1511
|
}
|
|
1496
1512
|
function openApp(url) {
|
|
1497
1513
|
try {
|
|
@@ -1544,7 +1560,7 @@ async function cmdApp() {
|
|
|
1544
1560
|
if (u.pathname === "/api/scanroots/add" && req.method === "POST") { const b = await readBody(req); return json(res, addScanRoot(String(b.path || ""))); }
|
|
1545
1561
|
if (u.pathname === "/api/scanroots/remove" && req.method === "POST") { const b = await readBody(req); return json(res, removeScanRoot(String(b.path || ""))); }
|
|
1546
1562
|
if (u.pathname === "/api/agentcfg") { const d = detectHandoffs(); return json(res, { cmd: handoffCmd(), agents: d.agents, editors: d.editors }); }
|
|
1547
|
-
if (u.pathname === "/api/agentcmd" && req.method === "POST") { const b = await readBody(req);
|
|
1563
|
+
if (u.pathname === "/api/agentcmd" && req.method === "POST") { const b = await readBody(req); return json(res, setHandoffCmd(b.cmd)); }
|
|
1548
1564
|
if (u.pathname === "/api/open" && req.method === "POST") { const b = await readBody(req); const e = runHandoff(String(b.path || "")); return json(res, { opened: !!e, id: e ? e.id : "" }); }
|
|
1549
1565
|
if (u.pathname === "/api/setup-local" && req.method === "POST") {
|
|
1550
1566
|
const b = await readBody(req);
|
|
@@ -1582,7 +1598,7 @@ async function cmdApp() {
|
|
|
1582
1598
|
server.on("listening", () => {
|
|
1583
1599
|
if (announced) return; announced = true;
|
|
1584
1600
|
const url = `http://127.0.0.1:${server.address().port}/?t=${TOKEN}`;
|
|
1585
|
-
const how = opened ? "" : openApp(url); opened = true; // only pop a window the first time
|
|
1601
|
+
const how = opened || process.env.SYMBIOT_NO_OPEN === "1" ? "" : openApp(url); opened = true; // only pop a window the first time (never in tests)
|
|
1586
1602
|
console.log(`\n${c.g("●")} ${c.b("Symbiot")} is running at ${c.b(url)}`);
|
|
1587
1603
|
console.log(how ? c.d(` Opened in a ${how}.`) : c.d(" Open that URL in your browser."));
|
|
1588
1604
|
console.log(c.d(" Leave this running; press Ctrl+C to stop (or click Quit in the window)."));
|
|
@@ -1605,7 +1621,7 @@ ${c.b("Usage")}
|
|
|
1605
1621
|
symbiot todo what's still on your plate
|
|
1606
1622
|
symbiot app open the visual app in your browser
|
|
1607
1623
|
symbiot drift what's out of sync / at risk across repos
|
|
1608
|
-
symbiot push [--open] write tasks into each repo (and
|
|
1624
|
+
symbiot push [--open] write tasks into each repo (and run your agent)
|
|
1609
1625
|
symbiot models recommend AI models for your hardware
|
|
1610
1626
|
symbiot setup-local [--model X] install/run a free local model (Ollama)
|
|
1611
1627
|
symbiot login connect it to an AI (once)
|
|
@@ -1655,4 +1671,4 @@ const isMain = (() => {
|
|
|
1655
1671
|
})();
|
|
1656
1672
|
if (isMain) main();
|
|
1657
1673
|
|
|
1658
|
-
export { authorship, repoState, readmeInfo, repoShape, houseRules, findAllRepos, buildMap, reportFooter, detectHardware, recommendModels, computeDrift, driftRepo, gitDefaultBranch, buildTasksMd, taskType, EMBEDDED_UI, orcaHandoffCmd, migrateOrcaCmd, fillHandoff, ORCA_CLAUDE_CMD, HANDOFF_PROMPT, shipChanges, syncTasks, pendingReview, approveRepo, sendBack, pushTasks, semverGt };
|
|
1674
|
+
export { authorship, repoState, readmeInfo, repoShape, houseRules, findAllRepos, buildMap, reportFooter, detectHardware, recommendModels, computeDrift, driftRepo, gitDefaultBranch, buildTasksMd, taskType, EMBEDDED_UI, orcaHandoffCmd, migrateOrcaCmd, fillHandoff, handoffCmd, setHandoffCmd, ORCA_CLAUDE_CMD, HANDOFF_PROMPT, shipChanges, syncTasks, pendingReview, approveRepo, sendBack, pushTasks, semverGt };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "symbiot",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.4",
|
|
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": {
|
|
@@ -43,6 +43,6 @@
|
|
|
43
43
|
"@anthropic-ai/sdk": "^0.128.0"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
|
-
"test": "node test/fixtures.mjs && node test/smoke.mjs && node test/install.mjs"
|
|
46
|
+
"test": "node test/load.mjs && node test/fixtures.mjs && node test/smoke.mjs && node test/install.mjs"
|
|
47
47
|
}
|
|
48
48
|
}
|
package/ui.mjs
CHANGED
|
@@ -149,7 +149,7 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
|
149
149
|
<div class="rfoot" id="outfoot" style="display:none"></div>
|
|
150
150
|
</section>
|
|
151
151
|
<section id="panel-tasks" class="hidden">
|
|
152
|
-
<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>
|
|
152
|
+
<div class="row"><input id="newtask" placeholder="Add a task..." style="flex:1"><select id="newtaskrepo" title="Which repo this task is for (needed to send it to an agent)" style="flex:0 0 auto;max-width:180px"><option value="">repo…</option></select><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>
|
|
153
153
|
<div class="note muted" style="margin-top:2px">To give tasks to your agent, use <b>Send to repos</b> — filter by tag below to choose which. As the agent finishes each one it lands in <b>Awaiting your review</b>: <b>Approve</b> commits it on a branch and opens a PR, <b>↶</b> sends it back. Ticking a task yourself marks it done (it auto-archives).</div>
|
|
154
154
|
<div id="pushout"></div>
|
|
155
155
|
<div id="reviewout"></div>
|
|
@@ -192,7 +192,7 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
|
|
|
192
192
|
</div>
|
|
193
193
|
<div style="margin-top:20px;border-top:1px solid var(--line);padding-top:16px">
|
|
194
194
|
<label>Hand off to your agent when you "Send to repos"</label>
|
|
195
|
-
<input id="agentcmd" type="text" placeholder="e.g.
|
|
195
|
+
<input id="agentcmd" type="text" placeholder="e.g. aider --message "{prompt}" --yes · code {dir} · leave blank to just write the file">
|
|
196
196
|
<div class="note muted">Runs in each repo after tasks are written. Use <b>{dir}</b> = repo path, <b>{prompt}</b> = the task instruction. Works with any agent or editor — it's your command.</div>
|
|
197
197
|
<div id="agentpresets" style="margin-top:8px"></div>
|
|
198
198
|
</div>
|
|
@@ -224,7 +224,7 @@ $('panel-drift').classList.toggle('hidden',!isDrift);
|
|
|
224
224
|
$('panel-agents').classList.toggle('hidden',!isAgents);
|
|
225
225
|
if(isRun){$('what').textContent=tab;$('out').textContent='Nothing yet - hit the button.';$('out').classList.add('muted');$('copy').classList.add('hidden');}
|
|
226
226
|
if(isMap&&!mapLoaded)loadMap();
|
|
227
|
-
if(isTasks)loadTasks();
|
|
227
|
+
if(isTasks){fillTaskRepos();loadTasks();}
|
|
228
228
|
if(isDrift&&!driftLoaded)loadDrift();
|
|
229
229
|
if(isAgents)loadAgents(); else stopAgentsPoll();}
|
|
230
230
|
tabs().forEach(function(t){t.addEventListener('click',function(){setTab(t.dataset.tab);});});
|
|
@@ -377,14 +377,15 @@ var clean=repos.filter(function(r){return !r.flags.length;}).map(function(r){ret
|
|
|
377
377
|
if(clean.length)h+="<div class='muted' style='margin-top:10px'>clean: "+esc(clean.join(", "))+"</div>";
|
|
378
378
|
if(!repos.length)h+="<div class='muted'>No repos found under your home folder.</div>";
|
|
379
379
|
out.innerHTML=h;});}
|
|
380
|
-
function
|
|
380
|
+
function fillTaskRepos(){var sel=document.getElementById('newtaskrepo');if(!sel||!GRAPH)return;var cur=sel.value;var names=GRAPH.nodes.filter(function(n){return n.type==='repo'||n.type==='folder';}).map(function(n){return n.label;}).sort();sel.innerHTML="<option value=''>repo…</option>"+names.map(function(n){return "<option value='"+esc(n)+"'"+(n===cur?" selected":"")+">"+esc(n)+"</option>";}).join("");}
|
|
381
|
+
function addTaskUI(){var i=document.getElementById('newtask');var v=(i.value||'').trim();if(!v)return;var repo=(document.getElementById('newtaskrepo')||{}).value||'';api('/api/tasks/add',{text:v,repo:repo}).then(function(){i.value='';TFILTER={type:'',repo:''};loadTasks();});}
|
|
381
382
|
function pushTasksUI(){var o=document.getElementById('pushout');var btn=document.getElementById('pushtasks');btn.disabled=true;
|
|
382
383
|
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…</div>";
|
|
383
384
|
api('/api/tasks/push',{type:TFILTER.type||'',repo:TFILTER.repo||''}).then(function(r){btn.disabled=false;
|
|
384
385
|
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;}
|
|
385
386
|
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 — wrote "+n+" file"+(n===1?"":"s")+"</span></div>";
|
|
386
387
|
if(n){h+="<ul>";r.written.forEach(function(w){h+="<li class='info'>✓ <b>"+esc(w.name)+"</b> <span class='ev'>"+esc(w.file)+" ("+w.count+" task"+(w.count===1?"":"s")+")</span></li>";});h+="</ul>";}
|
|
387
|
-
if(r.unresolved&&r.unresolved.length){h+="<div class='dd' style='margin-top:6px'>⚠ not
|
|
388
|
+
if(r.unresolved&&r.unresolved.length){var names=r.unresolved.map(function(u){return u.name;}).join(", ");var hasNoRepo=r.unresolved.some(function(u){return u.name==='(no repo)';});h+="<div class='dd' style='margin-top:6px'>⚠ not sent: "+esc(names)+". "+(hasNoRepo?"Pick a repo in the dropdown next to <b>Add</b> so the task has somewhere to go.":"That repo isn't in the map — add its folder in Settings.")+"</div>";}
|
|
388
389
|
if(r.handoff&&r.written&&r.written.length){r.written.forEach(function(w){api('/api/open',{path:w.path});});h+="<div class='dd' style='margin-top:8px'>🤖 Handed "+n+" repo(s) to your agent — opening the <b>Agents</b> tab to watch it work…</div>";setTimeout(function(){setTab('agents');},500);}
|
|
389
390
|
else{h+="<div class='dd' style='margin-top:8px'>Set an <b>agent command</b> in Settings to auto-run it on send (and watch it in the Agents tab). For now, tell your agent: <b>“Read .symbiot/TASKS.md and implement the unchecked items.”</b></div>";}
|
|
390
391
|
h+="</div>";
|
|
@@ -423,7 +424,7 @@ function scanLine(s){return 'Mapping your work… '+s.phase+(s.total?' '+s.done+
|
|
|
423
424
|
function loadMap(){var p=document.getElementById("profile");p.textContent="Mapping your work...";document.getElementById("graph").innerHTML="";sel=null;hideDetail();hideReview();var done=false;
|
|
424
425
|
function poll(){if(done)return;api('/api/scan').then(function(s){if(done)return;if(s&&s.active&&s.phase)p.textContent=scanLine(s);setTimeout(poll,600);}).catch(function(){});}
|
|
425
426
|
setTimeout(poll,400);
|
|
426
|
-
api("/api/map").then(function(g){done=true;mapLoaded=true;if(!g.nodes||!g.nodes.length){p.textContent="No git repositories found under your home folder.";return;}GRAPH=g;layout(g.nodes,g.edges);view={k:1,x:0,y:0};p.innerHTML=profileLine(g)+(g.stats&&g.stats.partial?" · <span class='err'>partial — the scan hit its time limit</span>":"");render();});}
|
|
427
|
+
api("/api/map").then(function(g){done=true;mapLoaded=true;if(!g.nodes||!g.nodes.length){p.textContent="No git repositories found under your home folder.";return;}GRAPH=g;fillTaskRepos();layout(g.nodes,g.edges);view={k:1,x:0,y:0};p.innerHTML=profileLine(g)+(g.stats&&g.stats.partial?" · <span class='err'>partial — the scan hit its time limit</span>":"");render();});}
|
|
427
428
|
function fitBadge(m){return m.fits?"<span class='tag'>fits your RAM</span>":"<span class='tag' style='background:#3a2a12;border-color:#6b4a1f;color:#F2A541'>needs more RAM</span>";}
|
|
428
429
|
function loadRec(){var out=document.getElementById('recout');out.innerHTML="<div class='muted'>Reading your hardware...</div>";
|
|
429
430
|
api('/api/models').then(function(d){var hw=d.hardware,rec=d.rec;
|