Skip to content

Cross-project spawns: a thread can start workers in another project's repo #109

Closed

Description

@currentbits

A thread is hard-bound to one project: every spawn uses thread.worktreePath || project.path as cwd (electron/runner.js:1320, 1515, 2238, 2603, 2972), and the sidebar/threads/activity all key off projectId.

Real work does not stay inside that box. Examples:

  • Working in the app repo and needing a matching change in the docs/site repo.
  • A memory-server or core change that needs a consumer repo updated in the same breath.
  • "Go look at how project X does this" — read-only, but currently impossible without switching projects and losing thread context.

Today the workarounds are all bad: start a second thread in the other project and hand-copy context, or let the agent cd out of its project and end up with a thread whose commits, diffs, PR list and worktree state all belong to a project the UI says it is not in.

Rough options (not decided):

  1. Read-only reach — allow other project paths as extra readable roots, keep writes in the owning project. Smallest, covers the "go look at X" case.
  2. Secondary projects on a thread — a thread declares extra project ids; runner passes their paths as additional dirs, and diff/commit/PR views group by project instead of assuming one.
  3. Hand-off — a thread spawns a linked thread in the other project with the relevant context carried over, and the two show as related.

Open questions: what the diff/commit/worktree UI does when a run touches two repos; whether worktrees are created per project; how the sidebar shows a thread that spans projects.

Activity

  1. changed the title [-]Cross-project work: a thread in one project sometimes needs to act in another[/-] [+]Cross-project spawns: a thread can start workers in another project's repo[/+] on Aug 16, 2026
  2. currentbits commented on Aug 16, 2026

    @currentbits
    OwnerAuthor

    Reframed after investigating: the issue asked for cross-project reach, but the workspace already had accidental cross-project reach with no guard rail. Willem's ruling: a run should never write to a second repo. So this closes the hole rather than building the feature.

    What actually happened

    Shipnest-v2 thread, 2026-08-16 12:48–13:10. An agent working in /Users/willem/code/Shipnest-v2 called thread_fork({threadId: "260be2c1"}) — a Solenta thread. Two grok workers spawned on git worktrees of the wrong repo. Told off, it repeated the mistake four minutes later with 9fafc689, guessed from the title "changelog and website". Cleanup cost two deleted branches and a removed worktree.

    Its own diagnosis, from the transcript:

    "I have no way to see which project a thread belongs to — threads_list returns id, title, provider, status, nothing about the workspace — so every fork I make is a guess, and guessing is exactly what caused this."

    Root cause

    thread_fork(threadId) takes its source thread as an argument, and electron/orchServer.js has no caller identity at all — one workspace-wide bearer token over stateless HTTP. So the server cannot know who is calling, and the caller must name a thread. Naming one was necessarily a guess: the agent is never told its own thread id, and threads_list never said which project any thread belonged to.

    thread_send had the same hole — it started a run on any threadId with no check.

    Fixed in a12bd26

    • threads_list reports projectId and projectName per thread.
    • Every dispatched prompt carries a [Thread] note stating the run's own thread id, project and cwd. Gated on coder-threads being registered, mirroring planboardNoteFor's GitHub-origin gate.
    • thread_fork and thread_send require a stated projectId and reject a thread outside it, naming both projects. A same-project fork of a sibling thread still works — only the boundary is blocked.

    Because each thread stays bound to one project, the diff/commit/PR/worktree views are untouched. That answers all three of this issue's original open questions with "nothing changes".

    Known ceiling

    The assert is a self-declared claim, not proof — it catches a confused caller, not a malicious one. Real caller identity needs per-run bearer tokens mapped to thread ids; grok and kimi can't receive those (they register MCP globally via grok mcp add … -s user and ~/.kimi-code/mcp.json), and the incident's workers were grok. Marked with a ponytail: comment naming the upgrade path.

    Design doc: docs/superpowers/specs/2026-08-16-cross-project-spawn-guard-design.md. Tests: 1593 across core/renderer/electron/memory, 0 failures.

    Deferred

    Read-only reach into another project ("go look at how X does this") is left unbuilt until the leak fix has been lived with. Cross-project writes are off the table.

  3. currentbits commented on Aug 16, 2026

    @currentbits
    OwnerAuthor

    PR: #111

  4. added a commit that references this issue on Aug 16, 2026
  5. currentbits commented on Aug 16, 2026

    @currentbits
    OwnerAuthor

    Second half: read-only reach, designed

    The guard shipped in #111. This is the piece that comment deferred — "go look at how project X does this", read-only.

    The finding that shaped it: there is no sandbox to open. Solenta passes no sandbox or approval flags to any provider, and grok, kimi and opencode all run tools unprompted in headless mode (providers.js:275, :393, :475). codex is held only by its own default sandbox. Only claude has a permission channel. So four of five providers can already read and write any repo on this disk — this feature grants no new capability. What it adds is the agent being told which checkouts exist and that they are read-only, plus not being nagged per file on claude.

    --add-dir is not used anywhere: it makes a directory a full workspace dir, which grants writes.

    Shape (design: docs/superpowers/specs/2026-08-16-cross-project-read-reach-design.md):

    • Per-thread opt-in readProjectIds, set from a "Read access" submenu in the thread overflow menu. Forks inherit it.
    • readRootsFor resolves ids → on-disk roots, dropping deleted and remote-host projects at read time.
    • A [Read access] sentence in the dispatched prompt naming each root, ungated (the existing [Thread] sentence keeps its coder-threads gate).
    • On claude, autoAllowRead answers allow to Read/Grep/Glob/NotebookRead under a root. Edit, Write, Bash and anything unrecognised still prompt, so a write into another repo stays a decision the user makes.
    • Nothing in diff/commit/PR/worktree/sidebar changes — writes still only land in the owning project.

    Known ceiling: on grok, kimi and opencode the read-only rule is a sentence in the prompt, nothing more. Marked with a ponytail: comment, same as assertSameProject.

    Plan: docs/superpowers/plans/2026-08-16-cross-project-read-reach.md — 6 tasks, TDD, on branch coder/cross-project-spawns-a-thread-can-start--c056ca.

  6. currentbits commented on Aug 16, 2026

    @currentbits
    OwnerAuthor

    Design + plan PR: #119 (docs only; implementation not started).

  7. added a commit that references this issue on Aug 16, 2026
  8. currentbits commented on Aug 18, 2026

    @currentbits
    OwnerAuthor

    Status check: resolved by product decision rather than by building the feature. After an incident where an agent forked a foreign thread by guessing its id, the ruling was 'a run must never write to a second repo'. What shipped (PR #111): assertSameProject in orchServer.js blocks cross-project fork/send, threads_list rows carry projectId/projectName, runs are told their own thread/project id. The read-only-reach half has a committed design + implementation plan (docs/superpowers/specs/2026-08-16-cross-project-read-reach-design.md, docs/superpowers/plans/2026-08-16-cross-project-read-reach.md) but was explicitly deferred and has no code. Closing this as decided; the read-reach plan can be revived as its own issue when wanted.

  9. added and removed on Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions