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 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, placed by
57
- their **nearest neighbours**: each repo's fingerprint is what it's built with
58
- (languages and tools), the weeks you work on it (your commits per week over the
59
- last 12), and what it's about (the words of its name, README and package
60
- description, TF-IDF weighted), and it sits by the 3 repos most like it
61
- (`mapknn.mjs`). The closest merge into one shape, the next are joined by a thin
62
- thread of liquid, the rest by a faint line; each cluster is named over its region
63
- ("JavaScript · Node"); what you haven't touched in a while is smaller and sinks;
64
- **you are here** marks where your recent work is. **Point at a repo** to light up
65
- its neighbours and see why ("shares JavaScript, Node; you worked on both the same
66
- weeks"). **Click** for its details: branch, commits, uncommitted work, your last
67
- 12 weeks, **Most like it**, a **Suggest next steps** button, and an **AI review**
68
- (what the project does, whether it needs new work at all, and ideas you can tick
69
- into **Tasks**). Scroll to zoom, drag to pan. Under the map: the languages and
70
- tools you build with. The Map needs **no AI key**; reviews and suggestions use
71
- your chosen model. Project folders that **aren't git repos** show up too (see
72
- [Where it looks](#where-it-looks)), as rings. The [experimental](#screens-blueprints-for-screen-automation)
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, and what you've already seen stays as small faint beads. Point at or tap a
79
- bead to read it and **Draft a reply** or **Open** it. Under each feed's name:
80
- **Seen** (sets its count back to 0; what it found stays under Watching, and the
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
- - **Tasks** — a checklist, grouped by kind (Fixes, Tests & CI, Docs, …). You add
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 Tasks or Agents from the liquid shows the
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
- - **Agents** — every agent run Symbiot has started (and local-model downloads):
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), to
119
- read in the app (see [What a run hands back](#what-a-run-hands-back-facts-to-remember-replies-to-wait-for-reports)).
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 Tasks tab
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 in the Tasks tab. Non-git project folders on the Map can receive tasks too.
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 Tasks tab, grouped by repo, with the branch, 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 in the Agents tab) stops its tasks starting agent runs, even Start it
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 in the
429
- **Agents** tab. The command is saved as `agentCmd` in
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 in the
438
- **Agents** tab shows "tasks held" meanwhile (hover it for their titles), and
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 in the **Agents** tab, each with
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 Agents tab after Symbiot restarts (or when
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 Todo. Agents has spheres
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+S
699
+ ### Away: Super+`
647
700
 
648
- Stepping away? **Super+S** (or `symbiot away`) puts Symbiot full screen: the orb at
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+S again) brings you back.
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+S. On COSMIC that's done for you (it replaces
660
- COSMIC's own Super+S, which stacks windows); elsewhere it tells you what to bind.
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), with **Approve**,
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, in the Agents tab.
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, or **ops** for everything outside one (this
782
- computer, accounts, services, your connectors). When an agent needs something
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 Agents tab lists the **Handovers**: who handed what to whom, and where it
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 in the Agents tab shows them,
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 in the Agents tab. You can always pick a model
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 in the Agents tab like any other: what it asks (a date only you
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
- tmpl = withStream(withTrust(withScope(withConnectors(tmpl, repoPath), repoPath)));
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 env = opts.env && typeof opts.env === "object" ? Object.fromEntries(Object.entries(opts.env).map(([k, v]) => [k, String(v)])) : null;
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 };