symbiot 0.55.0 → 0.55.2
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 +18 -0
- package/README.md +133 -52
- package/agents.mjs +136 -6
- package/away.mjs +18 -12
- package/guard.mjs +6 -2
- package/handback.mjs +2 -2
- package/handover.mjs +14 -3
- package/home.mjs +183 -33
- package/index.mjs +4 -4
- package/lanes.mjs +6 -6
- package/marketing.mjs +130 -0
- package/mind.mjs +9 -9
- package/package.json +4 -3
- package/reports.mjs +3 -3
- package/scan.mjs +9 -2
- package/server.mjs +28 -13
- package/tasks.mjs +14 -10
- package/ui.mjs +309 -128
- package/watch.mjs +5 -5
- package/writeups.mjs +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
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.55.2 — 2026-10-07
|
|
6
|
+
|
|
7
|
+
- Fixed a feature description being read as something waiting on you: an agent's "lit amber when something needs you: work waiting for your OK" showed as a Marketing lane step only you could do. A "when / if / once / until" before the words now means it describes behaviour.
|
|
8
|
+
- Added a Marketing lane on Home that gathers marketing work for all your products, tags each item by product, and lights up when something needs you.
|
|
9
|
+
- Added drafting of social posts in the Marketing lane, each opening with a hook, explaining the product and its goal, and never posted without your approval.
|
|
10
|
+
- Fixed the Pearl look so the middle orb's words show inside it and stay readable on both its bright and dark sides.
|
|
11
|
+
- Added a Minimize control to the Home chat, also triggered by Esc or clicking outside, which keeps your conversation so you can reopen it.
|
|
12
|
+
- Added a "What could be done next" box on Home when nothing waits on you, suggesting a few useful actions you can start in one tap.
|
|
13
|
+
- Changed finished agent runs so your AI reads their last words, catching new ways of saying something still waits on you.
|
|
14
|
+
- Added a sandbox for project agent runs that limits where they can write, which you can turn off with "agentSandbox": false in settings.
|
|
15
|
+
- Fixed the Seen button and reorganized the Map into tidy lanes, merged Tasks and Agents into a Workdesk, and added Reports and Marketing pages.
|
|
16
|
+
|
|
17
|
+
## 0.55.1
|
|
18
|
+
|
|
19
|
+
- **No more stray Symbiot windows.** 0.55.0's smoke test called Away like every other endpoint, so each full test run (yours, or an agent's in the symbiot repo) opened Away windows on your screen. Away now opens only when asked to by name, and never while windows are turned off (as in tests).
|
|
20
|
+
- **One Symbiot window, not a pile.** Opening Symbiot again (its icon, `symbiot app`) used to add a window each time. Now the newest window wins and older ones close themselves; a tab that can't close itself says Symbiot is open in a newer window, with **Use this one**. An update's restart that can't get its port bows out instead of lingering unseen.
|
|
21
|
+
- **Away is Super+`** (Super and the key above Tab). Super+S stacks windows on COSMIC, so it was the wrong key; `symbiot away --shortcut` sets up Super+` and clears the old one.
|
|
22
|
+
|
|
5
23
|
## 0.55.0
|
|
6
24
|
|
|
7
25
|
- **Away: Super+S.** Symbiot full screen while you're away from the desk: the orb at rest, the time, the agents at work and what each is doing, how many things only you can do, and how much waits on what you watch. Counts only, never anyone's words. Any key or click brings you back. On X11 with more than one screen the orb bounces across all of them; under Wayland (COSMIC, GNOME, KDE) it's one window on your screen, because no app may place windows there. `symbiot away --shortcut` sets up Super+S (on COSMIC it's done for you). Super+Esc still locks.
|
package/README.md
CHANGED
|
@@ -53,32 +53,34 @@ no install — it's the same CLI. Press Ctrl+C (or click the X in the top corner
|
|
|
53
53
|
|
|
54
54
|
It opens on [the liquid](#the-liquid-a-home-that-shapes-itself-to-you); each of these parts opens from it:
|
|
55
55
|
|
|
56
|
-
- **Map** — your repos and project folders as droplets in the liquid,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
weeks"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
56
|
+
- **Map** — your repos and project folders as droplets in the liquid, in tidy
|
|
57
|
+
**lanes**: **Needs you** first (a project with a question for you or work ready
|
|
58
|
+
for your OK, and any agent run waiting on you; click one to answer it), then
|
|
59
|
+
**Your repos** (the ones with your commits), **Other repos**, **Folders without
|
|
60
|
+
git** (as rings), and the act-* folders agent runs work in, gathered into one
|
|
61
|
+
**Agent run folders** group until you click it open. Each lane runs newest first
|
|
62
|
+
on a grid as wide as the window, so names never sit on top of each other however
|
|
63
|
+
many folders you add; click a lane's name to fold it away. **Point at a repo** to
|
|
64
|
+
light up the ones most like it and see why: each repo's fingerprint is what it's
|
|
65
|
+
built with, the weeks you work on it and what it's about (`mapknn.mjs`), as in
|
|
66
|
+
"shares JavaScript, Node; you worked on both the same weeks". **Click** for its
|
|
67
|
+
details: branch, commits, uncommitted work, your last 12 weeks, **Most like it**,
|
|
68
|
+
a **Suggest next steps** button, and an **AI review** (what the project does,
|
|
69
|
+
whether it needs new work at all, and ideas you can tick into the **Workdesk**).
|
|
70
|
+
The page scrolls down the lanes; ctrl+scroll (a pinch) or **+** and **−** zoom.
|
|
71
|
+
Under the map: the languages and tools you build with. The Map needs **no AI
|
|
72
|
+
key**; reviews and suggestions use your chosen model. Project folders that
|
|
73
|
+
**aren't git repos** show up too (see [Where it looks](#where-it-looks)). The
|
|
74
|
+
[experimental](#screens-blueprints-for-screen-automation)
|
|
73
75
|
**Screens** view folds away under the map.
|
|
74
76
|
- **Dashboard** — everything you [watch](#screens-blueprints-for-screen-automation),
|
|
75
77
|
as a stream: time runs left to right, one current per feed (your inbox, GitHub,
|
|
76
78
|
WhatsApp, any other page), "now" on the right where each feed pools. Every message
|
|
77
79
|
is a bead placed when it arrived; the ones waiting for your reply are bigger and
|
|
78
|
-
amber
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
other feeds keep theirs), **Talk** (a chat with your AI about what's new there:
|
|
80
|
+
amber. Point at or tap a bead to read it and **Draft a reply** or **Open** it.
|
|
81
|
+
Under each feed's name: **Seen** (sets its count back to 0 and clears its beads,
|
|
82
|
+
and remembers it: only what comes in after counts; what it found stays under
|
|
83
|
+
Watching, and the other feeds keep theirs), **Talk** (a chat with your AI about what's new there:
|
|
82
84
|
what needs you, and what to say to whom; agree it there, then **Draft a reply**
|
|
83
85
|
on one, and your agent gets that talk with the message, so it writes what you
|
|
84
86
|
agreed) and **Check now**. The headline says what's waiting; switch between 24
|
|
@@ -93,7 +95,10 @@ It opens on [the liquid](#the-liquid-a-home-that-shapes-itself-to-you); each of
|
|
|
93
95
|
a "fetch latest" toggle (and an [experimental](#experimental) "check CI").
|
|
94
96
|
- **Week / Standup / Todo** — the write-ups (these use your chosen AI), set as a
|
|
95
97
|
readable page with its dates; **Copy** copies the text as written.
|
|
96
|
-
- **
|
|
98
|
+
- **Workdesk** — everything handed to an agent, in one place: what needs you, work
|
|
99
|
+
awaiting your review, the tasks queued for later, and below them the agents at
|
|
100
|
+
work, their questions and the handovers between them.
|
|
101
|
+
Tasks are a checklist, grouped by kind (Fixes, Tests & CI, Docs, …). You add
|
|
97
102
|
a task by telling Symbiot in the bar at the bottom: it files it under the right
|
|
98
103
|
project. Filter by type or repo, then **Send to repos** (at the top, beside
|
|
99
104
|
Close) to hand just those to your agent (see
|
|
@@ -101,7 +106,7 @@ It opens on [the liquid](#the-liquid-a-home-that-shapes-itself-to-you); each of
|
|
|
101
106
|
finishes lands in **Awaiting your review** (see
|
|
102
107
|
[Review and approve](#review-and-approve-the-agents-work)). A task you tick
|
|
103
108
|
yourself is done and **auto-archives**; the archived view (the liquid's
|
|
104
|
-
**Archive**) can restore it. Opening
|
|
109
|
+
**Archive**) can restore it. Opening the Workdesk from the liquid shows the
|
|
105
110
|
work first: a sphere per task an agent is on, named in plain words, with small
|
|
106
111
|
spheres orbiting it while it works; what's done and waiting for your OK; and what's
|
|
107
112
|
waiting its turn, gathered round the agent in its repo. One **Go** (or saying
|
|
@@ -110,13 +115,20 @@ It opens on [the liquid](#the-liquid-a-home-that-shapes-itself-to-you); each of
|
|
|
110
115
|
(once it's awaiting review) what the agent changed. Answers use your chosen AI,
|
|
111
116
|
grounded in that repo's commits, README, rules and pending diff; the thread is
|
|
112
117
|
kept with the task.
|
|
113
|
-
|
|
114
|
-
live status, elapsed time, exit code, the tail of its output, and **what it
|
|
118
|
+
**Agents and handovers**, under the tasks: every agent run Symbiot has started
|
|
119
|
+
(and local-model downloads): live status, elapsed time, exit code, the tail of its output, and **what it
|
|
115
120
|
did** — files changed and commits made, read from git, whichever agent it was.
|
|
116
121
|
When an agent leaves **questions, options or ideas** for you, they show up on its
|
|
117
122
|
block (see [Questions from your agent](#questions-from-your-agent)).
|
|
118
|
-
- **Reports** — what your agents wrote up for you (findings, audits, plans),
|
|
119
|
-
|
|
123
|
+
- **Reports** — what your agents wrote up for you (findings, audits, plans), newest
|
|
124
|
+
first, the unread ones marked (**Mark all read** clears them), and a reader for
|
|
125
|
+
each; unread reports always have their own droplet on Home (see [What a run hands back](#what-a-run-hands-back-facts-to-remember-replies-to-wait-for-reports)).
|
|
126
|
+
- **Marketing** — marketing for every product you make, in a lane of its own (see
|
|
127
|
+
[The Marketing lane](#the-marketing-lane)): what needs you there first, the
|
|
128
|
+
lane's tasks and what its agent drafted, each tagged with the product it's for,
|
|
129
|
+
the LinkedIn **Drafts to post** (Shipped, Learned / fixed, Longer post), the
|
|
130
|
+
replies to them and the 4-week test (see
|
|
131
|
+
[`symbiot post`](#post-about-your-week-symbiot-post)).
|
|
120
132
|
- **Settings** — [Link your work](#link-your-work), your AI, the folders to scan, your agent command, the
|
|
121
133
|
[weekly write-up and start at login](#every-week-and-at-login), plus the
|
|
122
134
|
[experimental](#experimental) email, model recommendations and one-click local
|
|
@@ -272,7 +284,7 @@ in the repo's `SYMBIOT_KEYSTORE_B64` and `SYMBIOT_KEYSTORE_PASS` secrets (see
|
|
|
272
284
|
## Hand tasks to your coding agent: `symbiot push`
|
|
273
285
|
|
|
274
286
|
```bash
|
|
275
|
-
symbiot push # or the "Send to repos" button in the app's
|
|
287
|
+
symbiot push # or the "Send to repos" button in the app's Workdesk
|
|
276
288
|
symbiot push --open # ...and run your agent command in each repo
|
|
277
289
|
```
|
|
278
290
|
|
|
@@ -281,7 +293,7 @@ Writes a **`.symbiot/TASKS.md`** into each repo your tasks reference — a check
|
|
|
281
293
|
open `TODO/FIXME` markers (with `file:line`), and current drift. Then point Claude
|
|
282
294
|
Code / Cursor / any agent at it: *"Read `.symbiot/TASKS.md` and implement the
|
|
283
295
|
unchecked items."* Tasks come from ticking a repo review's ideas, or adding your
|
|
284
|
-
own
|
|
296
|
+
own on the Workdesk. Non-git project folders on the Map can receive tasks too.
|
|
285
297
|
|
|
286
298
|
The brief asks the agent to tick each item (`- [x]`) as it finishes it and to leave
|
|
287
299
|
its changes **uncommitted**, ready for your review.
|
|
@@ -289,7 +301,7 @@ its changes **uncommitted**, ready for your review.
|
|
|
289
301
|
### Review and approve the agent's work
|
|
290
302
|
|
|
291
303
|
A tick from the agent doesn't archive anything. It moves the task into **Awaiting
|
|
292
|
-
your review** at the top of the
|
|
304
|
+
your review** at the top of the Workdesk, grouped by repo, with the branch, the
|
|
293
305
|
size of the uncommitted change and a **Show diff** button. For each repo:
|
|
294
306
|
|
|
295
307
|
- **Approve → PR** syncs the work. If you're on the default branch it creates
|
|
@@ -358,7 +370,7 @@ quieter choice.
|
|
|
358
370
|
A project blocked on something only you can give (a token, a sign-in) would
|
|
359
371
|
otherwise start its agent again with every answer, handover or Go, only to say
|
|
360
372
|
the same thing. **Park this project** (at the top of its tasks) or **Park** (on
|
|
361
|
-
its run
|
|
373
|
+
its run on the Workdesk) stops its tasks starting agent runs, even Start it
|
|
362
374
|
anyway, until you unpark it. A run already going finishes; Go skips a parked
|
|
363
375
|
project and says so.
|
|
364
376
|
|
|
@@ -425,8 +437,8 @@ Google Drive, Notion) now gets its claude.ai connector's rule on every Claude ru
|
|
|
425
437
|
even before Claude records it or while it waits to be authorized. A rule for tools
|
|
426
438
|
a run doesn't have does nothing.
|
|
427
439
|
|
|
428
|
-
Each run is logged to `.symbiot/agent.log` in the repo and shown live
|
|
429
|
-
**
|
|
440
|
+
Each run is logged to `.symbiot/agent.log` in the repo and shown live on the
|
|
441
|
+
**Workdesk**. The command is saved as `agentCmd` in
|
|
430
442
|
`~/.config/symbiot/config.json` — it's your command, Symbiot only fills in
|
|
431
443
|
`{dir}` and `{prompt}`. A command saved from an older preset keeps working as-is.
|
|
432
444
|
|
|
@@ -434,8 +446,8 @@ One agent per repo: while a run is still going (tracked in
|
|
|
434
446
|
`.symbiot/agent.pid`, so the app and `symbiot push --open` both see it), sending
|
|
435
447
|
again doesn't start a second agent there, and doesn't rewrite the `TASKS.md` it's
|
|
436
448
|
working from either: the new tasks wait in `.symbiot/TASKS.next.md` and replace
|
|
437
|
-
`TASKS.md` when it finishes, keeping anything it ticked. The agent's block
|
|
438
|
-
**
|
|
449
|
+
`TASKS.md` when it finishes, keeping anything it ticked. The agent's block on the
|
|
450
|
+
**Workdesk** shows "tasks held" meanwhile (hover it for their titles), and
|
|
439
451
|
when the agent finishes, the app starts one on the held tasks by itself. If
|
|
440
452
|
`symbiot push --open` started the running agent, the app can't see it exit, so
|
|
441
453
|
it starts one the next time it checks that repo after it finishes (opening the
|
|
@@ -455,6 +467,19 @@ Talk to Symbiot and its agents just do the work. A Claude run skips permission c
|
|
|
455
467
|
folder (or `/`), sudo, piping a script from the internet into a shell, reading your
|
|
456
468
|
SSH keys or cloud credentials, and changing Symbiot's own settings. A stopped agent
|
|
457
469
|
is told why, and asks.
|
|
470
|
+
- **The sandbox**, for a repo run, on top of the membrane. Its commands run in Claude
|
|
471
|
+
Code's own sandbox (bubblewrap on Linux, Seatbelt on macOS). They write only in the
|
|
472
|
+
repo, the folders you gave Symbiot or allowed it, and the npm and download caches
|
|
473
|
+
(`~/.npm`, `~/.cache`). They can't read your keys, or your AI's key in Symbiot's
|
|
474
|
+
settings, and there's no way to run a command outside the sandbox. Local sockets
|
|
475
|
+
work, so headless Chrome starts, but Docker's and your desktop session's are hidden,
|
|
476
|
+
since either one is a way out. Its Edit and Write tools are held to the same
|
|
477
|
+
folders by the membrane, and `gh` keeps working (the run gets its token).
|
|
478
|
+
Its settings live in `~/.config/symbiot/sandbox/`, not in the repo, so a run can't
|
|
479
|
+
widen its own sandbox. An ops run, whose job is this computer, and a draft reply's
|
|
480
|
+
run keep the membrane alone. On Linux it needs `bwrap` and `socat` (`sudo apt
|
|
481
|
+
install bubblewrap socat`); without them, runs have the membrane only, and Settings
|
|
482
|
+
says what's missing. `"agentSandbox": false` in `config.json` turns it off.
|
|
458
483
|
- **One mind.** When an agent asks you something, your answer goes back into the same
|
|
459
484
|
conversation (`--resume`), so it carries on knowing everything it knew. It starts
|
|
460
485
|
afresh after 8 runs or 2 days.
|
|
@@ -488,7 +513,7 @@ Reading both costs about 40 lines.
|
|
|
488
513
|
- [repo: symbiot] Show the drift report in Standup
|
|
489
514
|
```
|
|
490
515
|
|
|
491
|
-
Up to five questions show on that agent's block
|
|
516
|
+
Up to five questions show on that agent's block on the **Workdesk**, each with
|
|
492
517
|
two options and room for your own answer, alongside its ideas, two at a time.
|
|
493
518
|
Two, because most people pick the recommended option or add every idea without
|
|
494
519
|
weighing them, so the agent is told to be the judge. Both options have to be good
|
|
@@ -509,7 +534,7 @@ that starts with `[repo: <name>]`, to that repo's Tasks: an agent working on one
|
|
|
509
534
|
project can have ideas for another (Symbiot itself, say). The agent is told to
|
|
510
535
|
keep going with everything that doesn't depend on an answer, and to ask instead of
|
|
511
536
|
doing anything destructive. Both files live in `.symbiot/`, so they're never
|
|
512
|
-
committed. The questions stay on the
|
|
537
|
+
committed. The questions stay on the Workdesk after Symbiot restarts (or when
|
|
513
538
|
`symbiot push --open` started the run): Symbiot notes each folder a run starts in
|
|
514
539
|
(`~/.config/symbiot/runs.json`) and lists the ones still waiting on you.
|
|
515
540
|
|
|
@@ -567,7 +592,9 @@ in the app.
|
|
|
567
592
|
name: **Ferrofluid** (glossy black liquid metal, lit like a studio; the default),
|
|
568
593
|
**Glass** (clear droplets that bend the colours behind them) or **Pearl** (silver
|
|
569
594
|
lit like a product photo). Every panel follows the look, and the typeface (Geist)
|
|
570
|
-
ships with Symbiot, so it looks the same offline.
|
|
595
|
+
ships with Symbiot, so it looks the same offline. The core's words sit inside it in
|
|
596
|
+
every look; on Pearl's mirror they sit on a soft pearl sheen, so they read over its
|
|
597
|
+
bright top and its dark underside alike.
|
|
571
598
|
|
|
572
599
|
At the top, in a band of its own, is **what only you can do**: an agent's question
|
|
573
600
|
to you, an Approve waiting, a first step. Each is a lit lane orb with a liquid line
|
|
@@ -580,6 +607,19 @@ and the blob dissolves. More than fit across go on a second page. Clicking the o
|
|
|
580
607
|
questions and its Approve under **Needs you here** at the top of its tasks, with its
|
|
581
608
|
handovers that are waiting; an ops run's question on its block in Agents.
|
|
582
609
|
|
|
610
|
+
When nothing waits on you, the band becomes **What could be done next**: three to
|
|
611
|
+
five things that can be done now, from what Symbiot already knows, each with what
|
|
612
|
+
it gains and how long it takes, and one button. **Triage the 8 unread reports**
|
|
613
|
+
starts an agent that reads them and leaves one page, with their next steps as ideas
|
|
614
|
+
you add in a click. **Retry** runs a handover that errored and was left. **Send
|
|
615
|
+
coral's 2 waiting tasks to its agent** is for a project whose tasks have waited
|
|
616
|
+
days with nothing at work there. **Give whatsapp_module "…"** puts a task with no
|
|
617
|
+
project into the project its words name and starts that project's agent. **Draft
|
|
618
|
+
replies** drafts replies to the newest new mail or chats and leaves them unsent.
|
|
619
|
+
**Make a task** turns a decision you made lately, that no task carries yet, into a
|
|
620
|
+
task. One you take stays away for a week, and **×** puts the box away until its
|
|
621
|
+
suggestions change. It never shows while anything still waits on you.
|
|
622
|
+
|
|
583
623
|
What stopped and can't go on without you is a question there too, on its lane.
|
|
584
624
|
A handover that couldn't start (an ops run, limited to its own folder, handed
|
|
585
625
|
"needs a run that can edit `~/Company`" to ops) asks **Allow this run access to
|
|
@@ -589,11 +629,20 @@ that asked; **Skip** lets it go, and that agent reads that you skipped it. It as
|
|
|
589
629
|
for 3 days, or until the same thing goes through another way. A run that ended in
|
|
590
630
|
an error asks **Run it again?** for a day, until another run starts there.
|
|
591
631
|
|
|
632
|
+
A run that ended saying something waits on you, without asking it in QUESTIONS.md
|
|
633
|
+
("the draft isn't sent", "needs sudo, and only you can do that"), asks too:
|
|
634
|
+
**Go ahead** or **Done it**, with what to check first. Symbiot catches the usual
|
|
635
|
+
ways of saying it by their words. For a new way of saying it ("the invoice is
|
|
636
|
+
ready as a draft; give me the word and it goes out"), your AI reads each finished
|
|
637
|
+
run's last words once, so it isn't missed.
|
|
638
|
+
|
|
592
639
|
Under the band, around the core, are the parts of the app, grouped the
|
|
593
640
|
**P.A.R.A.** way, each group a row with its name at its start, always in the same
|
|
594
641
|
place and order:
|
|
595
642
|
|
|
596
|
-
- **Projects** (work with an end): Tasks, Agents and
|
|
643
|
+
- **Projects** (work with an end): Tasks, Agents, Todo and Marketing, which is lit
|
|
644
|
+
amber (with how many) when something there needs you and has spheres orbiting
|
|
645
|
+
it while its agent works. Agents has spheres
|
|
597
646
|
orbiting it while an agent works. Click the **Projects** name for your projects
|
|
598
647
|
themselves: a sphere per lane with work on it, saying what's going on there,
|
|
599
648
|
amber when something there needs you, in order (**Needs you** first, **Recent**,
|
|
@@ -616,6 +665,10 @@ on the Dashboard. Talk to it in the bar at the bottom: "open tasks" opens Tasks,
|
|
|
616
665
|
as every chat. Paste a screenshot into the bar (Ctrl+V) or drop one on it, and
|
|
617
666
|
Symbiot sees it with what you say (up to 4, with any AI you've connected that reads
|
|
618
667
|
images); if it hands the work to an agent, the agent gets the screenshot too.
|
|
668
|
+
Once you've said something, the chat has its own bar over the input: **Minimize**
|
|
669
|
+
(or Esc, or a click anywhere outside the chat) shrinks it back to the input and
|
|
670
|
+
brings Home back as it was, and **Chat · 4 messages** opens it again with
|
|
671
|
+
everything said so far. Sending a message opens it too.
|
|
619
672
|
Leave it alone and it rests as one orb. There are no settings for any
|
|
620
673
|
of this: it adapts to you.
|
|
621
674
|
|
|
@@ -643,21 +696,21 @@ of this: it adapts to you.
|
|
|
643
696
|
Everything it learns stays on this computer: `adapt.json` and `mind.json` in
|
|
644
697
|
Symbiot's config folder, readable by you only.
|
|
645
698
|
|
|
646
|
-
### Away: Super
|
|
699
|
+
### Away: Super+`
|
|
647
700
|
|
|
648
|
-
Stepping away? **Super
|
|
701
|
+
Stepping away? **Super+`** (Super and the key above Tab), or `symbiot away`, puts Symbiot full screen: the orb at
|
|
649
702
|
rest, the time, and what's going on. That means the agents at work and what each is
|
|
650
703
|
doing, how many things only you can do, and how much waits on what you watch. It shows
|
|
651
704
|
counts only, never anyone's words, so it's fine on a screen across the room. Any key
|
|
652
|
-
or click (or Super
|
|
705
|
+
or click (or Super+` again) brings you back.
|
|
653
706
|
|
|
654
707
|
On one screen the orb rests in the middle. On X11 desktops with more screens it
|
|
655
708
|
bounces across all of them, one window per screen, with the orb crossing from one to
|
|
656
709
|
the next. Under Wayland (COSMIC, GNOME, KDE) no app may place a window on a screen, so
|
|
657
710
|
it's one window on the screen you're on.
|
|
658
711
|
|
|
659
|
-
`symbiot away --shortcut` sets up Super
|
|
660
|
-
|
|
712
|
+
`symbiot away --shortcut` sets up Super+`. On COSMIC that's done for you; elsewhere
|
|
713
|
+
it tells you what to bind.
|
|
661
714
|
Away isn't a lock: **Super+Esc** still locks the computer.
|
|
662
715
|
|
|
663
716
|
## Link your work
|
|
@@ -712,7 +765,8 @@ and nothing else. **It never posts by itself**, and it doesn't schedule.
|
|
|
712
765
|
count the facts don't show is sent back once, then dropped. When every draft is
|
|
713
766
|
dropped, Symbiot tries once more from the start by itself before giving up.
|
|
714
767
|
- **You approve each one.** The drafts wait under **Marketing**, a section of its
|
|
715
|
-
own (its orb shows on Home once posts can be drafted
|
|
768
|
+
own (its orb shows on Home once posts can be drafted, lit while drafts wait on
|
|
769
|
+
you), each tagged with the product it's about, with **Approve**,
|
|
716
770
|
**Edit** and **Skip**. Approve copies the post to your clipboard and opens
|
|
717
771
|
LinkedIn's share box: you paste it and post it yourself.
|
|
718
772
|
- **Pictures and videos.** Each draft says in a line what picture or short video
|
|
@@ -764,7 +818,7 @@ can't do.
|
|
|
764
818
|
showed. The agent has your tools and connectors (MCP, the command line, a
|
|
765
819
|
browser signed in to your linked sites). Something for later becomes a task on
|
|
766
820
|
your list. Anything hard to undo (closing an account, deleting, paying, sending)
|
|
767
|
-
the agent asks you about first,
|
|
821
|
+
the agent asks you about first, on the Workdesk.
|
|
768
822
|
- **It looks at links.** Ask about the links on a card and it looks each one up the
|
|
769
823
|
way a link preview does (not signed in, no scripts run, nothing downloaded):
|
|
770
824
|
where it really goes, what the page is, and whether it's a file such as an .apk.
|
|
@@ -778,8 +832,9 @@ can't do.
|
|
|
778
832
|
|
|
779
833
|
### Lanes: agents hand work to each other
|
|
780
834
|
|
|
781
|
-
Each agent has its own lane: a repo,
|
|
782
|
-
computer, accounts, services, your connectors)
|
|
835
|
+
Each agent has its own lane: a repo, **ops** for everything outside one (this
|
|
836
|
+
computer, accounts, services, your connectors), or **marketing** for marketing
|
|
837
|
+
across your products ([The Marketing lane](#the-marketing-lane)). When an agent needs something
|
|
783
838
|
that's another lane's job, it doesn't stop and it doesn't ask you to do it. It
|
|
784
839
|
writes it to its `.symbiot/HANDOFF.md`, under the lane's name, and Symbiot:
|
|
785
840
|
|
|
@@ -788,7 +843,7 @@ writes it to its `.symbiot/HANDOFF.md`, under the lane's name, and Symbiot:
|
|
|
788
843
|
2. when that agent is done, puts what it did into the asking agent's
|
|
789
844
|
`.symbiot/ANSWERS.md` and starts that agent again, so it carries on.
|
|
790
845
|
|
|
791
|
-
The
|
|
846
|
+
The Workdesk lists the **Handovers**: who handed what to whom, and where it
|
|
792
847
|
stands. A handover to a lane that doesn't exist, or back to the agent's own lane,
|
|
793
848
|
goes back to the agent, not to you. After 4 handovers in a row, the next one isn't
|
|
794
849
|
started, so two lanes can't pass the same job back and forth.
|
|
@@ -799,13 +854,39 @@ hand, a cable, which network you're on), your identity or secrets (signing in, a
|
|
|
799
854
|
account, spending money, sending something in your name). Every brief tells the
|
|
800
855
|
agent so, and such a question says which: `👤 You (only you: your Meta token): …`.
|
|
801
856
|
|
|
857
|
+
### The Marketing lane
|
|
858
|
+
|
|
859
|
+
Marketing is a lane of its own, not one repo's: posts, demo videos and screenshots,
|
|
860
|
+
launches, landing-page copy and campaigns for every product you make (Symbiot,
|
|
861
|
+
Dailify, the agent products…). Give it work on the Marketing page (**Add**, with the
|
|
862
|
+
product it's for), by telling Symbiot ("a marketing task: a launch post for
|
|
863
|
+
Dailify"), or by **Move to Marketing** on another project's task about posting,
|
|
864
|
+
launches or pricing; any agent can hand work to it too, as `marketing`.
|
|
865
|
+
|
|
866
|
+
- **Each item is tagged with its product**: `[Dailify] Write the launch post` as you
|
|
867
|
+
wrote it, else the product its words name (as you write it: "Dailify", from your
|
|
868
|
+
repos' names as you say them, the way Home names them).
|
|
869
|
+
- **Its agent works in its own folder**, `~/.config/symbiot/marketing`, a git repo of
|
|
870
|
+
its own on this computer (no remote). It drafts each piece under
|
|
871
|
+
`drafts/<product>/`, and its brief lists the products in its tasks with where each
|
|
872
|
+
one's code and docs are. Every post opens with a strong hook, says plainly what
|
|
873
|
+
the product is and states its end goal; where the end goal isn't written down, it
|
|
874
|
+
drafts one and asks you to confirm it.
|
|
875
|
+
- **It never posts**, publishes, schedules or sends, and never signs in as you.
|
|
876
|
+
Its work waits for your OK like any project's (**Approve** commits it in its
|
|
877
|
+
folder), and its questions reach you like any agent's.
|
|
878
|
+
- **On Home** it's an orb among your projects, lit amber when something there needs
|
|
879
|
+
you: its questions (which also show as blobs under **Needs you**), its work
|
|
880
|
+
waiting for your OK, and the week's drafts. Click it and its page opens with
|
|
881
|
+
those lit at the top.
|
|
882
|
+
|
|
802
883
|
### What a run hands back: facts to remember, replies to wait for, reports
|
|
803
884
|
|
|
804
885
|
Every brief also tells the agent what else it can leave in `.symbiot/`:
|
|
805
886
|
|
|
806
887
|
- **`REMEMBER.json`**: lasting facts it found (who someone is, which account is
|
|
807
888
|
what, a decision), as `[{ "name", "kind", "fact" }]`. A run can't write
|
|
808
|
-
Symbiot's memory itself. Once it stops, its block
|
|
889
|
+
Symbiot's memory itself. Once it stops, its block on the Workdesk shows them,
|
|
809
890
|
each ticked, with **Remember** and **Skip**. Remember puts the ticked ones into
|
|
810
891
|
memory, marked with the run's name; Skip drops them. Either way it's asked once.
|
|
811
892
|
- **`AWAITING.json`**: an email it sent (or drafted for you to send) that waits on
|
|
@@ -1047,7 +1128,7 @@ med / max, marking which fit your RAM) to run free and private via
|
|
|
1047
1128
|
cheap → top). `setup-local` starts Ollama if it isn't running, downloads the model
|
|
1048
1129
|
with progress, and switches Symbiot over to it. If Ollama isn't installed it prints
|
|
1049
1130
|
the one-line install for your OS and you re-run it after. Both are buttons in
|
|
1050
|
-
Settings, where the download shows
|
|
1131
|
+
Settings, where the download shows on the Workdesk. You can always pick a model
|
|
1051
1132
|
yourself with `symbiot login`.
|
|
1052
1133
|
|
|
1053
1134
|
### Screens: blueprints for screen automation
|
|
@@ -1198,7 +1279,7 @@ runs by itself (Claude Code, Codex, Gemini or Aider, not an editor or Orca's tab
|
|
|
1198
1279
|
the app running (its hidden browser stays open between the agent's steps), and
|
|
1199
1280
|
your mail's site (`mail.google.com`) under Trusted sites. Each email gets a folder
|
|
1200
1281
|
of its own in `~/.config/symbiot/drafts` (yours only) with the agent's brief, and
|
|
1201
|
-
the run shows
|
|
1282
|
+
the run shows on the Workdesk like any other: what it asks (a date only you
|
|
1202
1283
|
know, say) is answered there. The agent sees the email's text, so it reaches
|
|
1203
1284
|
whatever AI your agent uses. From a terminal, `symbiot watch new` marks the
|
|
1204
1285
|
emails `"mail": true`, and `symbiot watch draft <id>` does the same as the button.
|
package/agents.mjs
CHANGED
|
@@ -169,13 +169,68 @@ function guardSettings() {
|
|
|
169
169
|
try { mkdirSync(CONFIG_DIR, { recursive: true }); writeFileSync(GUARD_SETTINGS, JSON.stringify(s, null, 2) + "\n", { mode: 0o600 }); } catch {}
|
|
170
170
|
return GUARD_SETTINGS;
|
|
171
171
|
}
|
|
172
|
-
function withTrust(tmpl) {
|
|
172
|
+
function withTrust(tmpl, box = null) {
|
|
173
173
|
if (!isClaudeCmd(tmpl) || !trustFull()) return tmpl;
|
|
174
174
|
let c = String(tmpl).replace(/\s--permission-mode\s+\S+/g, "");
|
|
175
175
|
if (!/--dangerously-skip-permissions\b/.test(c)) c += " --dangerously-skip-permissions";
|
|
176
|
-
if (!/--settings\b/.test(c)) c += ` --settings ${JSON.stringify(guardSettings())}`;
|
|
176
|
+
if (!/--settings\b/.test(c)) c += ` --settings ${JSON.stringify(box ? box.file : guardSettings())}`;
|
|
177
177
|
return c;
|
|
178
178
|
}
|
|
179
|
+
// ---- a repo run, sandboxed ----------------------------------------------------------
|
|
180
|
+
// On top of the membrane, a repo run's commands run in Claude Code's own sandbox
|
|
181
|
+
// (bubblewrap on Linux, Seatbelt on macOS): they write only in the repo, the folders
|
|
182
|
+
// you gave Symbiot or allowed it (--add-dir, the repo's allow list) and the npm and
|
|
183
|
+
// download caches, can't read your keys, and have no way out of it (no unsandboxed
|
|
184
|
+
// commands). Its Edit and Write tools are held to the same folders by the guard
|
|
185
|
+
// (SYMBIOT_WRITES). An ops run, whose job is this computer, and a draft reply's run
|
|
186
|
+
// keep the membrane alone. Its settings live in Symbiot's folder, not the repo, so a
|
|
187
|
+
// run can't widen its own sandbox for the next one. Without what the sandbox needs
|
|
188
|
+
// (Linux: bwrap and socat) runs aren't sandboxed, and Settings says what to install;
|
|
189
|
+
// config.agentSandbox: false turns it off.
|
|
190
|
+
const SANDBOX_DIR = join(CONFIG_DIR, "sandbox");
|
|
191
|
+
const SANDBOX_CACHES = [".npm", ".cache"]; // npm's cache and logs; pip, uv, node-gyp, Chrome for Testing
|
|
192
|
+
const SANDBOX_SECRETS = [".ssh", ".gnupg", ".aws", ".azure", ".kube", ".docker", ".config/gcloud", ".config/gh", ".netrc", ".npmrc", ".pypirc", ".git-credentials", ".config/symbiot/config.json"]; // your AI's key and the app's token too
|
|
193
|
+
// Local sockets are allowed (headless Chrome needs one to start, so the browser tests
|
|
194
|
+
// ran in no sandboxed run), but the ones that would be a way out are hidden: Docker's,
|
|
195
|
+
// and your session's (its bus starts commands outside, through systemd-run; X11 and
|
|
196
|
+
// Wayland type into your windows; the ssh agent and keyring hold your keys).
|
|
197
|
+
const SANDBOX_SOCKETS = (uid = typeof process.getuid === "function" ? process.getuid() : "") => ["/run/docker.sock", "/var/run/docker.sock", "/run/containerd", "/run/podman", "/run/dbus", ...(uid !== "" ? [`/run/user/${uid}`] : []), "/tmp/.X11-unix"];
|
|
198
|
+
function sandboxNeeds({ platform = process.platform, has = hasCmd } = {}) {
|
|
199
|
+
if (platform === "darwin") return { ready: true, missing: [] };
|
|
200
|
+
if (platform !== "linux") return { ready: false, missing: [], unsupported: true };
|
|
201
|
+
const missing = ["bwrap", "socat"].filter((c) => !has(c));
|
|
202
|
+
return { ready: !missing.length, missing };
|
|
203
|
+
}
|
|
204
|
+
const sandboxed = (repoPath, cfg = loadConfig()) => cfg.agentSandbox !== false && !!repoPath && !resolve(repoPath).startsWith(CONFIG_DIR + "/");
|
|
205
|
+
function sandboxWrites(repoPath, tmpl = "") {
|
|
206
|
+
const out = [resolve(repoPath)];
|
|
207
|
+
try { for (const k of knowledgeFolders()) out.push(k.path); } catch {}
|
|
208
|
+
for (const m of String(tmpl).matchAll(/--add-dir\s+(?:"([^"]+)"|'([^']+)'|(\S+))/g)) out.push(m[1] || m[2] || m[3]);
|
|
209
|
+
for (const f of ["settings.json", "settings.local.json"]) { try { const d = JSON.parse(readFileSync(join(repoPath, ".claude", f), "utf8")); for (const x of (d.permissions && d.permissions.additionalDirectories) || []) out.push(String(x)); } catch {} }
|
|
210
|
+
for (const c of SANDBOX_CACHES) out.push(join(homedir(), c));
|
|
211
|
+
return [...new Set(out.filter(Boolean).map((p) => resolve(String(p).replace(/^~(?=\/|$)/, homedir()))))];
|
|
212
|
+
}
|
|
213
|
+
// { file, writes } for a repo run to be sandboxed, else null.
|
|
214
|
+
function sandboxFor(repoPath, tmpl = "", { needs = sandboxNeeds } = {}) {
|
|
215
|
+
if (!sandboxed(repoPath) || !needs().ready) return null;
|
|
216
|
+
const writes = sandboxWrites(repoPath, tmpl);
|
|
217
|
+
const s = { ...JSON.parse(readFileSync(guardSettings(), "utf8")), sandbox: { enabled: true, failIfUnavailable: true, autoAllowBashIfSandboxed: true, allowUnsandboxedCommands: false,
|
|
218
|
+
network: { allowAllUnixSockets: true }, filesystem: { allowWrite: writes, denyRead: [...SANDBOX_SECRETS.map((p) => join(homedir(), p)), ...SANDBOX_SOCKETS()] } } };
|
|
219
|
+
const file = join(SANDBOX_DIR, digest(resolve(repoPath)) + ".json");
|
|
220
|
+
try { mkdirSync(SANDBOX_DIR, { recursive: true, mode: 0o700 }); writeFileSync(file, JSON.stringify(s, null, 2) + "\n", { mode: 0o600 }); } catch { return null; }
|
|
221
|
+
return { file, writes };
|
|
222
|
+
}
|
|
223
|
+
// What a sandboxed run's environment adds: the folders its Edit and Write tools may
|
|
224
|
+
// write (the guard reads them), and gh's token, since the sandbox can't reach the
|
|
225
|
+
// keyring gh keeps it in (gh then says "Requires authentication").
|
|
226
|
+
function sandboxEnv(box, { token = () => (hasCmd("gh") ? sh("gh auth token").trim() : "") } = {}) {
|
|
227
|
+
if (!box) return null;
|
|
228
|
+
const env = { SYMBIOT_WRITES: JSON.stringify(box.writes) };
|
|
229
|
+
if (!process.env.GH_TOKEN) { let t = ""; try { t = token(); } catch {} if (/^[\w-]{20,}$/.test(t)) env.GH_TOKEN = t; }
|
|
230
|
+
return env;
|
|
231
|
+
}
|
|
232
|
+
// For Settings → Handoff: { on, ready, missing } (on: repo runs get it).
|
|
233
|
+
function sandboxState() { const n = sandboxNeeds(); return { on: loadConfig().agentSandbox !== false && trustFull(), ready: n.ready, missing: n.missing, ...(n.unsupported ? { unsupported: true } : {}) }; }
|
|
179
234
|
const SESSION = "session.json", RESUME_MAX = 8, RESUME_AGE = 2 * 86400000;
|
|
180
235
|
const RESUME_PROMPT = "The user has answered: read the newest entries in .symbiot/ANSWERS.md and carry on with .symbiot/TASKS.md from where you were. Same rules as before.";
|
|
181
236
|
function sessionOf(path) { try { return JSON.parse(readSymbiot(path, SESSION)) || null; } catch { return null; } }
|
|
@@ -214,14 +269,16 @@ function runHandoff(repoPath, { force = false } = {}) {
|
|
|
214
269
|
let opts = {}; try { opts = JSON.parse(readSymbiot(repoPath, "handoff.json")) || {}; } catch {}
|
|
215
270
|
const busy = runningHandoff(repoPath); if (busy) return { busy: true, id: busy.id || "", pid: busy.pid, auto: !!busy.auto };
|
|
216
271
|
releaseHeldTasks(repoPath); // held for an agent another process started, which has since exited
|
|
217
|
-
|
|
272
|
+
const scoped = withScope(withConnectors(tmpl, repoPath), repoPath), box = isClaudeCmd(scoped) && trustFull() ? sandboxFor(repoPath, scoped) : null; // a repo run's sandbox, on top of the membrane
|
|
273
|
+
tmpl = withStream(withTrust(scoped, box));
|
|
218
274
|
const resume = isClaudeCmd(tmpl) ? resumeFor(repoPath) : null;
|
|
219
275
|
const runCmd = resume ? tmpl + ` --resume ${resume.id}` : tmpl;
|
|
220
276
|
if (!force) { const w = waitingFor(repoPath); if (w) { askedFor(repoPath); return { blocked: true, waiting: true, questions: 0, note: waitNote(w) }; } }
|
|
221
277
|
const blocked = !force && blockedAgain(repoPath, tmpl); if (blocked) return blocked;
|
|
222
278
|
try { unlinkSync(join(repoPath, ".symbiot", WAITING)); } catch {} // the step's done (or this is Start it anyway): this is the run it waited for
|
|
223
279
|
const lock = join(repoPath, ".symbiot", LOCK);
|
|
224
|
-
const
|
|
280
|
+
const own = opts.env && typeof opts.env === "object" ? Object.fromEntries(Object.entries(opts.env).map(([k, v]) => [k, String(v)])) : null, boxEnv = sandboxEnv(box);
|
|
281
|
+
const env = own || boxEnv ? { ...(own || {}), ...(boxEnv || {}) } : null;
|
|
225
282
|
const e = track(typeof opts.name === "string" && opts.name ? opts.name.slice(0, 80) : repoPath.split("/").pop(), fillHandoff(runCmd, repoPath, resume ? RESUME_PROMPT : HANDOFF_PROMPT), repoPath, (code) => {
|
|
226
283
|
if (noteSession(repoPath, code, !!resume) === "lost") { setTimeout(() => { try { runHandoff(repoPath, { force: true }); } catch {} }, 300); return; } // its conversation is gone: start afresh
|
|
227
284
|
try { if (JSON.parse(readFileSync(lock, "utf8")).pid === e.pid) unlinkSync(lock); } catch {}
|
|
@@ -323,13 +380,86 @@ function takenUp(path, end) {
|
|
|
323
380
|
}
|
|
324
381
|
function needsOf(path, { final = "", waiting = null, end = Date.now(), now = Date.now() } = {}) {
|
|
325
382
|
if (now - end > NEEDS_FOR) return null;
|
|
326
|
-
const n = waiting ? { kind: "step", what: clipWords(String(waiting.step || "").replace(/^\s*👤\s*(You\s*(\([^)]*\))?:)?\s*/, "").trim(), 240), check: "", label: "", waiting: true } : leftToYou(final);
|
|
383
|
+
const n = waiting ? { kind: "step", what: clipWords(String(waiting.step || "").replace(/^\s*👤\s*(You\s*(\([^)]*\))?:)?\s*/, "").trim(), 240), check: "", label: "", waiting: true } : leftToYou(final) || readNeeds(path, final);
|
|
327
384
|
if (!n || !n.what) return null;
|
|
328
385
|
const key = digest(n.what);
|
|
329
386
|
if (loadSettled()[path] === key || takenUp(path, end)) return null;
|
|
330
387
|
return { ...n, key };
|
|
331
388
|
}
|
|
332
389
|
|
|
390
|
+
// ---- your AI reads the last words too ------------------------------------------
|
|
391
|
+
// leftToYou catches the ways runs have said it so far ("isn't sent", "only you can").
|
|
392
|
+
// A new way of saying it would slip by, so once a run ends your AI reads its last
|
|
393
|
+
// words for anything waiting on you, once per run's words (READ_FILE keeps what it
|
|
394
|
+
// found, by folder and words), and needsOf takes that when the patterns found
|
|
395
|
+
// nothing. Only for a run that asked nothing in QUESTIONS.md and isn't waiting on a
|
|
396
|
+
// step already: those show as they are.
|
|
397
|
+
const READ_FILE = join(CONFIG_DIR, "lastwords.json"), READ_KEEP = 300, READ_RETRY = 15 * 60000, READ_PER_TICK = 3, READ_MAX = 4000;
|
|
398
|
+
const loadRead = (file = READ_FILE) => { try { const o = JSON.parse(readFileSync(file, "utf8")); return o && typeof o === "object" && !Array.isArray(o) ? o : {}; } catch { return {}; } };
|
|
399
|
+
const readKey = (path, final) => digest(path + "\n" + String(final || "").trim());
|
|
400
|
+
function readNeeds(path, final, { file = READ_FILE } = {}) {
|
|
401
|
+
if (!String(final || "").trim()) return null;
|
|
402
|
+
const r = loadRead(file)[readKey(path, final)];
|
|
403
|
+
return r && r.what ? { kind: r.kind === "approve" ? "approve" : "step", what: r.what, check: r.check || "", label: r.label || "", read: true } : null;
|
|
404
|
+
}
|
|
405
|
+
const READ_SYSTEM = `You read the last message a coding agent left when its run ended, and say whether it leaves the user something to do before the work is finished.
|
|
406
|
+
That's either something waiting for their OK before it happens (sending, posting, deleting, paying, closing, merging, publishing) or a step only they can take (signing in, a password or a 2FA code, sudo, a phone or a cable, a token from a provider's console, a choice that's theirs to make).
|
|
407
|
+
Not: work that's done, a summary of what changed, an idea they might like, changes waiting for review or Approve in Symbiot, a connector to sign in to again, or a question already asked in QUESTIONS.md.
|
|
408
|
+
Reply with JSON only, no markdown fence: {"needs": false} or {"needs": true, "kind": "approve" or "step", "what": "the sentence that says so, in its own words, under 240 characters", "check": "what it says to check first, or empty", "label": "what it is in 1 to 3 words, or empty"}.`;
|
|
409
|
+
function parseRead(raw) {
|
|
410
|
+
const t = String(raw || ""), a = t.indexOf("{"), b = t.lastIndexOf("}"); if (a < 0 || b < a) return null;
|
|
411
|
+
let o; try { o = JSON.parse(t.slice(a, b + 1)); } catch { return null; }
|
|
412
|
+
if (!o || typeof o.needs !== "boolean") return null;
|
|
413
|
+
if (!o.needs || !String(o.what || "").trim()) return { needs: false };
|
|
414
|
+
const c = (x, n) => clipWords(String(x || "").replace(/\s+/g, " ").trim(), n);
|
|
415
|
+
return { needs: true, kind: o.kind === "approve" ? "approve" : "step", what: c(o.what, 240), check: c(o.check, 240), label: c(o.label, 60) };
|
|
416
|
+
}
|
|
417
|
+
// One pass (the app runs it every minute): each run that ended in the last
|
|
418
|
+
// NEEDS_FOR, whose words neither the patterns nor your AI has read yet, a few at a
|
|
419
|
+
// time. Without an AI connected it does nothing. { read } (how many it asked about).
|
|
420
|
+
let reading = false; // one pass at a time: a slow model mustn't have the next minute's pass ask again
|
|
421
|
+
async function readLastWords(o = {}) {
|
|
422
|
+
if (reading) return { read: 0, busy: true };
|
|
423
|
+
reading = true; try { return await readPass(o); } finally { reading = false; }
|
|
424
|
+
}
|
|
425
|
+
// Each folder's newest finished run, with its last words as needsOf reads them:
|
|
426
|
+
// those this app started, then those from before it last started (runs.json).
|
|
427
|
+
function finishedRuns() {
|
|
428
|
+
const out = [], seen = new Set();
|
|
429
|
+
for (const e of HANDOFFS) {
|
|
430
|
+
if (seen.has(e.path)) continue; seen.add(e.path); if (e.status === "running") continue;
|
|
431
|
+
let tail = "", work = null; try { ({ tail, work } = workOf(e.path, readRunLog(e.log))); } catch {}
|
|
432
|
+
out.push({ path: e.path, name: e.name, endedAt: e.endedAt, final: work ? tail : "" });
|
|
433
|
+
}
|
|
434
|
+
for (const r of loadRuns()) {
|
|
435
|
+
if (seen.has(r.path)) continue; seen.add(r.path); if (runningHandoff(r.path)) continue;
|
|
436
|
+
let end = 0; try { end = statSync(join(r.path, ".symbiot", "agent.log")).mtimeMs; } catch { continue; }
|
|
437
|
+
out.push({ path: r.path, name: r.name, endedAt: end, final: lastFinal(r.path) });
|
|
438
|
+
}
|
|
439
|
+
return out;
|
|
440
|
+
}
|
|
441
|
+
async function readPass({ list, ask, connected, now = Date.now(), file = READ_FILE } = {}) {
|
|
442
|
+
if (!ask) { const ai = await import("./ai.mjs"); if (!(connected ? connected() : ai.resolveProvider())) return { read: 0 }; ask = ai.write; }
|
|
443
|
+
const store = loadRead(file), due = [];
|
|
444
|
+
for (const a of list || finishedRuns()) {
|
|
445
|
+
const final = String(a.final || "").trim();
|
|
446
|
+
if (!final || now - (a.endedAt || now) > NEEDS_FOR || leftToYou(final)) continue;
|
|
447
|
+
const k = readKey(a.path, a.final), had = store[k];
|
|
448
|
+
if (had && !(had.error && now - had.at > READ_RETRY)) continue;
|
|
449
|
+
if (waitingFor(a.path) || agentQuestions(a.path, a.name).questions.length) continue; // those show as they are
|
|
450
|
+
due.push({ k, final });
|
|
451
|
+
}
|
|
452
|
+
for (const d of due.slice(0, READ_PER_TICK)) {
|
|
453
|
+
let r = null; try { r = parseRead(await ask(READ_SYSTEM, `The run's last message:\n\n${String(d.final).trim().slice(-READ_MAX)}`)); } catch {}
|
|
454
|
+
store[d.k] = r ? { at: now, ...(r.needs ? { kind: r.kind, what: r.what, check: r.check, label: r.label } : {}) } : { at: now, error: true };
|
|
455
|
+
}
|
|
456
|
+
if (due.length) {
|
|
457
|
+
const keys = Object.keys(store).sort((x, y) => store[y].at - store[x].at).slice(0, READ_KEEP), kept = {}; for (const k of keys) kept[k] = store[k];
|
|
458
|
+
try { mkdirSync(CONFIG_DIR, { recursive: true }); writeFileSync(file, JSON.stringify(kept), { mode: 0o600 }); } catch {}
|
|
459
|
+
}
|
|
460
|
+
return { read: Math.min(due.length, READ_PER_TICK) };
|
|
461
|
+
}
|
|
462
|
+
|
|
333
463
|
// ---- a step that's yours: wait for it -----------------------------------------
|
|
334
464
|
// An answer that picks a "👤 You:" option leaves a step for the user to do (put a
|
|
335
465
|
// key in .env, allow a command in Settings). Starting the agent before it's done
|
|
@@ -945,4 +1075,4 @@ function agentsList() {
|
|
|
945
1075
|
}).concat(earlierRuns());
|
|
946
1076
|
}
|
|
947
1077
|
|
|
948
|
-
export { 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, PARKED_NOTE, parkedPaths, isParked, parkLane, agentMissing, blockedAgain, runningHandoff, loadRuns, earlierRuns, namedFiles, waitingFor, startWaiting, writeTasks, droppedTasks, releaseHeldTasks, startHeldTasks, detectHandoffs, pickAgent, findOrcaCli, orcaHandoffCmd, migrateOrcaCmd, migrateClaudeCmd, track, agentChanges, parseQuestions, suggestionTarget, skipIdea, agentQuestions, answerQuestions, agentsList, needsOf, settleNeeds, NEEDS_FOR , autoAllow, autoAllowSweep, allowlistInWork, installAllowlist, inWork, withScope , withTrust, resumeFor, noteSession, trustFull, GUARD_SETTINGS };
|
|
1078
|
+
export { 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, PARKED_NOTE, parkedPaths, isParked, parkLane, agentMissing, blockedAgain, runningHandoff, loadRuns, earlierRuns, namedFiles, waitingFor, startWaiting, 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 };
|