Compare commits

...

3 Commits

Author SHA1 Message Date
J
8ded50c7ba docs(project-resources): tighten local_directory wording per review
- Soften "only the bound daemon can take tasks" to "only the bound
  daemon uses this local directory" (zh) — aligns with the
  mixed-resource fallback section where other daemons still run.
- Clarify that local_directory does not create/use a github_repo
  worktree for that task (en + zh); the per-workspace repo cache may
  still sync as a background behaviour.
- Match implementation for the Desktop "Add local directory" button:
  it stays visible but is disabled with a hint when daemon is offline
  or the per-daemon limit is reached; only the web app hides it
  outright (en + zh).

Co-authored-by: multica-agent <github@multica.ai>
2026-05-27 14:26:58 +08:00
J
743713ba60 docs(project-resources): cover write-conflict trade-off + mixed-resource behavior
Expand the local_directory docs in response to review feedback:

- Restate "when to pick local_directory" as two distinct use cases (clone
  cost; fine-grained changes needing frequent local review) instead of a
  one-liner, and make the trade-off explicit: v1 ships no file-level write
  lock, so the per-directory serial gate is the only protection against
  cross-issue agents touching the same files.
- Add a new "Mixing resource types, and multiple local_directory resources"
  section that answers: github_repo + local_directory on the same project
  (local takes precedence on the bound daemon, github_repo falls back
  everywhere else), and two local_directory resources (only possible
  across two daemons, routed by the agent's runtime assignment, no
  load-balancing).

Mirrored into the Chinese translation. typecheck + tests still pass.

Co-authored-by: multica-agent <github@multica.ai>
2026-05-27 14:15:22 +08:00
J
e5684eec09 docs(project-resources): document local_directory resource type
Add user-facing guidance for the local_directory project resource introduced
in #3283 — when to pick it over github_repo, the Desktop / CLI attach flow,
path validation rules, the daemon-scoped one-per-(project, daemon) limit,
serial task execution + waiting_local_directory status, what the daemon will
and won't touch in the user's folder, and the v1 limits to call out
(no auto branch switch / commit / PR; dirty tree carried through).

Also ship the missing Chinese counterpart of project-resources and wire it
into meta.zh.json.

MUL-2618

Co-authored-by: multica-agent <github@multica.ai>
2026-05-27 14:04:58 +08:00
3 changed files with 387 additions and 6 deletions

View File

@@ -11,6 +11,7 @@
"issues",
"projects",
"comments",
"project-resources",
"---智能体---",
"agents",
"agents-create",

View File

@@ -1,25 +1,27 @@
---
title: Project Resources
description: Attach typed pointers (Git repos today, more later) to a project so agents can pick them up as scoped context.
description: Attach typed pointers (Git repos, local directories, more later) to a project so agents can pick them up as scoped context.
---
A **Project Resource** is a typed pointer — a Git repo URL today, a Notion page or document link tomorrow — attached to a [project](/workspaces). When an [agent](/agents) runs against an issue inside that project, the daemon automatically writes the project's resource list into the agent's working directory and into its [meta-skill](/skills) prompt.
A **Project Resource** is a typed pointer — a Git repo URL, a path on your own machine, a Notion page tomorrow — attached to a [project](/workspaces). When an [agent](/agents) runs against an issue inside that project, the daemon automatically writes the project's resource list into the agent's working directory and into its [meta-skill](/skills) prompt.
The result: the agent knows which repo to check out, which docs are the "primary references" for this project, without anyone copy-pasting context into the issue body.
The result: the agent knows which repo to check out (or which local directory to work in), and which docs are the "primary references" for this project, without anyone copy-pasting context into the issue body.
## Mental model
A project is no longer just a label. It is a small **resource container**:
- A project has 0..N **resources**.
- A resource has a `resource_type` (e.g. `github_repo`) and a `resource_ref` (a JSON payload typed by `resource_type`).
- A resource has a `resource_type` (e.g. `github_repo`, `local_directory`) and a `resource_ref` (a JSON payload typed by `resource_type`).
- New resource types add a string + a handler. **No schema migration. No frontend rewrite.**
This shape is intentional — it's the same pattern Multica already uses for agent providers: a `type` discriminator and a typed payload. It keeps the schema stable so adding "Notion page", "Google Doc", "uploaded file", or "external URL" later is a small, additive change.
## Today: `github_repo`
Today two resource types ship: [`github_repo`](#resource-type-github_repo) (clone-per-task into an isolated worktree) and [`local_directory`](#resource-type-local_directory) (run directly inside a folder on a specific daemon's machine).
The first resource type ships ready to use:
## Resource type: `github_repo`
The default resource type — checked out per task into an isolated worktree:
```json
{
@@ -33,6 +35,122 @@ The first resource type ships ready to use:
`default_branch_hint` is optional — if present, the daemon surfaces it in the meta-skill so the agent knows which branch to base its work on.
## Resource type: `local_directory`
For repos that can't reasonably be re-cloned per task — multi-gigabyte game checkouts, large monorepos, or any project where the worktree-per-task model is painful — a project can instead point at an **existing directory on a specific [daemon](/daemon-runtimes)'s machine**. The agent runs **directly inside that folder**, with no clone, no copy, and no worktree.
```json
{
"resource_type": "local_directory",
"resource_ref": {
"local_path": "/Users/me/code/big-game",
"daemon_id": "0001234e-…",
"label": "main checkout"
}
}
```
The trade-off vs. `github_repo` is intentional: only the bound daemon can pick up tasks against the directory, and tasks on the same directory run **serially** instead of in parallel. In exchange you keep your existing checkout, your existing branch, your existing dirty state — Multica never re-clones it.
### When to pick `local_directory` over `github_repo`
| Concern | `github_repo` (worktree) | `local_directory` |
| --- | --- | --- |
| Checkout cost per task | Fresh clone + worktree | None — agent runs in place |
| Concurrency on the same repo | Many tasks in parallel | One at a time per directory |
| Branch / dirty state | Each task gets a fresh branch from the default | Whatever the directory currently has |
| Where it can run | Any daemon | Exactly one daemon (the one bound) |
| Disk footprint | One worktree per task | Zero overhead — your existing folder |
Pick `local_directory` when **either** of these matches:
1. **Re-cloning is prohibitively expensive** — a multi-gigabyte game checkout, a monorepo with heavy LFS assets, or anything where the per-task `git clone` would dominate the actual work. You trade concurrency for a clone-free run.
2. **Your changes are fine-grained and you want to review them locally as they happen** — you're iterating on a single component, you want to flip between the agent's edits and your editor every few minutes, and you'd rather have your existing checkout be the source of truth than a per-task worktree you have to dig out of `~/multica_workspaces/`.
The trade-off you accept in both cases is the same: **this version ships no file-level write lock.** The per-directory serial gate (one task at a time on the same folder) is the only protection against agents in two different issues touching the same files at the same time. If you point two issues' agents at the same `local_directory`, their tasks queue rather than parallelise — that's by design. If you need real parallelism on the same codebase, stay on `github_repo`.
### Attaching a local directory
The folder picker lives in the **Desktop app** only — the web app has no way to read OS paths, so the "Add local directory" button is hidden there. On Desktop:
1. Open the project → **Resources** panel.
2. Click **Add local directory**. A native folder picker opens.
3. Pick the folder. The path is bound to **the daemon currently registered by this Desktop install** — the resource record stores both the path and that daemon's ID.
On Desktop the button stays visible but is **disabled with a hint** when the daemon on this machine is offline, or when the project already has a `local_directory` bound to this daemon — so you can see *why* it's unavailable. (On the web app the button is hidden outright, since there's no folder picker available there at all.) To bind another machine's directory, install Desktop on that machine and add the resource from there.
From the CLI (works on web-only environments too, as long as you supply the daemon ID yourself):
```bash
multica project resource add <project-id> \
--type local_directory \
--local-path /Users/me/code/big-game \
--daemon-id <daemon-uuid> \
--ref-label "main checkout" # optional
multica project resource update <project-id> <resource-id> \
--local-path /Users/me/code/big-game-new
```
`--daemon-id` comes from `multica daemon list`. The CLI also accepts the generic `--ref '<json>'` escape hatch if you'd rather pass the payload directly.
### Path rules
The path you attach must clear both an attach-time and a per-task validation. Both are enforced by the daemon that owns the resource — the server only stores the JSON. A path that breaks any rule fails the task with a typed error and leaves your directory untouched:
- Must be **absolute**.
- Must **exist** and be a **directory** (not a file, symlink to a file, or device node).
- Must be **readable and writable** by the daemon process.
- Cannot be a system root or an entire user profile — `/`, `/Users`, `/home`, `/root`, `/etc`, `/tmp`, `/var`, `/usr`, `/opt`, `/Users/Shared`, your own `$HOME`, any Windows drive root (`C:\`, `D:\`, …), or `C:\Users` / `C:\ProgramData` / `C:\Program Files` / `C:\Program Files (x86)` / `C:\Windows`.
- A symlink that resolves to any of the above is rejected, and so is the canonical form of an OS-aliased path (e.g. on macOS, typing `/private/tmp` is rejected the same way as `/tmp`).
The blacklist is intentionally aggressive — picking your home directory would put Multica's runtime files at the root of your account, which is never what you want. Pick a sub-folder (typically your actual project checkout) instead.
### One per (project, daemon)
A project may hold **at most one `local_directory` per daemon**. Attempting to add a second one on the same daemon returns a `409` from the API; the Desktop button hides itself when the limit is already reached and surfaces a tooltip explaining why.
Different daemons are independent — a shared project can have one `local_directory` per teammate's machine, each binding the same project to a different folder on a different host. When the daemon claims a task, it picks the row that matches its own ID and ignores the rest.
### Mixing resource types, and multiple `local_directory` resources
Two cross-resource configurations show up in practice:
- **`github_repo` + `local_directory` on the same project.** On the daemon that has a matching `local_directory` binding, the local directory **takes precedence**: the agent runs in your folder, and the daemon does not create or use a `github_repo` worktree for that task. (The per-workspace repo cache may still sync as usual — that's a background behaviour unrelated to this task's working tree.) The `github_repo` URL still appears in `.multica/project/resources.json` and in the agent's `## Repositories` section for reference — but the working tree the agent edits is your local one, not a worktree. On a daemon that has **no** `local_directory` row for this project (different machine, or before that teammate attached one), the task falls back to the usual `github_repo` worktree flow. Effectively the local directory is a per-daemon override of the worktree path.
- **Two `local_directory` resources on the same project.** Because each `local_directory` is bound to exactly one daemon, this only happens across two different machines (the API rejects two on the same daemon at attach time, see above). Tasks are routed by the agent's runtime assignment, not by which daemon has a local directory: a task lands on the daemon that owns the receiving agent's runtime, that daemon picks the `local_directory` row matching its own ID, and ignores the rest. There is no load-balancing — if you want a specific machine to run a task, dispatch the agent that's bound to that machine's runtime.
A daemon that has no `local_directory` row for a project that has one bound elsewhere is **not** blocked — its tasks simply proceed via the project's other resources (typically the `github_repo` fallback). The `local_directory` only matters for the daemon it's bound to.
### Running tasks against a local directory
When a task is dispatched on an issue whose project has a `local_directory` bound to the receiving daemon, the daemon:
1. Re-validates the path (rules above).
2. Acquires a per-directory lock keyed on the symlink-resolved real path — so two routes to the same folder (one via a symlink, one direct) still serialise.
3. Writes the agent's `CLAUDE.md` / `AGENTS.md` (and `.multica/project/resources.json`) **into the user's directory**. The agent works there, just as if you'd opened the folder yourself.
4. Keeps Multica's runtime artefacts (`output/`, `logs/`, `.gc_meta.json`) in a separate envRoot **outside** the user's directory.
If a second task for the same directory arrives while the first is running, it parks with status **Waiting for local directory** (等待本地目录释放). The status is visible everywhere the task is — the chat task pill, the agent banner, the execution log, and the activity indicator — and the parked task counts toward the agent's "queued" presence. Cancelling the parked task releases its slot immediately; cancelling the running task lets the next one promote.
The wait is not a timeout — a parked task stays parked until either the lock releases or the user / agent cancels it.
### What Multica will and won't touch in your directory
- **Will write** `CLAUDE.md` / `AGENTS.md` (or the equivalent for your agent's provider) and `.multica/project/resources.json` at the directory root, so the agent has its meta-skill and resource list. Add these to your `.gitignore` if you don't want them committed.
- **Will write** whatever code edits the agent decides to make — exactly the same way as if you'd run the agent locally yourself.
- **Will never physically delete** the directory or anything inside it. Garbage collection is path-aware: for `local_directory` envRoots it cleans only its own `output/` and `logs/` under `workspacesRoot`, and treats the user's directory as off-limits.
### v1 limits (will tighten in follow-ups)
The first release deliberately ships with sharper edges than `github_repo`. Expect this list to shrink over time — what's documented here is what's true today:
- **No automatic branch switching.** The agent runs in whatever branch you have checked out. Switch branches before dispatching if it matters.
- **No dirty-tree protection or auto-commit.** Uncommitted changes are visible to the agent, may be modified in place, and won't be stashed. Treat the directory as a real working tree and commit before risky runs.
- **No automatic PR.** When the task ends, the changes sit on whatever branch they were made on — nothing is pushed and no PR is opened. Push and open the PR yourself when you're ready.
- **`waiting_local_directory` shows status, not the holder.** The badge tells you the task is parked; it doesn't surface which task or which file path is currently holding the directory.
These are tracked as the agent-task-lifecycle follow-up to the local-directory work; until that ships, treat `local_directory` as "the agent runs in your folder, the same way you would."
## Attaching repos at project creation
In the **Web** or **Desktop** app, opening *New project* now shows a **Repos** pill alongside Status / Priority / Lead. Selecting workspace-bound repos (or pasting an ad-hoc URL) attaches them as `github_repo` resources the moment the project is created.

View File

@@ -0,0 +1,262 @@
---
title: 项目资源
description: 给项目挂上有类型的指针GitHub 仓库、本地目录,未来更多),让智能体执行任务时自动拿到对应上下文。
---
**项目资源Project Resource** 是挂在 [项目](/workspaces) 上的有类型的指针 —— 今天可以挂 GitHub 仓库 URL 或者本机的一个目录,未来还会有 Notion 页面、文档链接等等。当 [智能体](/agents) 在这个项目的 issue 上跑任务时,守护进程会自动把项目的资源列表写进智能体的工作目录,并塞进它的 [元 skill](/skills) prompt 里。
带来的结果:智能体知道应该 checkout 哪个仓库(或者在哪个本地目录里工作),知道项目"主要参考资料"是哪些 —— 没人需要把上下文复制粘贴进 issue 描述里。
## 心智模型
项目不再只是一个标签,而是一个小的 **资源容器**
- 一个项目可以挂 0..N 个 **资源**。
- 每个资源有一个 `resource_type`(比如 `github_repo`、`local_directory`),以及一个 `resource_ref`(按 `resource_type` 定型的 JSON 负载)。
- 加新资源类型只需要加一个字符串 + 一个 handler。**不需要 schema 迁移,不需要重写前端。**
这个形状是有意的 —— 跟 Multica 里 agent provider 的设计完全一致:一个 `type` 判别字段 + 一个有类型的 payload。schema 保持稳定,未来加"Notion 页面"、"Google Doc"、"上传文件"、"外部 URL"都是小型的、增量的改动。
目前内置两种资源类型:[`github_repo`](#resource-type-github_repo)(每次任务克隆出独立 worktree和 [`local_directory`](#resource-type-local_directory)(直接在某台守护进程所在机器的目录里运行)。
## 资源类型:`github_repo`
默认的资源类型 —— 每次任务都在隔离的 worktree 里 checkout
```json
{
"resource_type": "github_repo",
"resource_ref": {
"url": "https://github.com/owner/repo",
"default_branch_hint": "main"
}
}
```
`default_branch_hint` 可选 —— 填了之后,守护进程会把它写进元 skill告诉智能体该基于哪个分支干活。
## 资源类型:`local_directory`
对于那些不适合每次任务重新 clone 的仓库 —— 几十 GB 的游戏项目、超大 monorepo、或者本身就不想被多次复制的目录 —— 项目可以改为指向 **某台 [守护进程](/daemon-runtimes) 机器上的一个已有目录**。智能体会 **直接在这个目录里运行**,不 clone、不复制、不创建 worktree。
```json
{
"resource_type": "local_directory",
"resource_ref": {
"local_path": "/Users/me/code/big-game",
"daemon_id": "0001234e-…",
"label": "主开发目录"
}
}
```
跟 `github_repo` 相比的取舍是有意的:只有绑定的那台守护进程会使用这个本地目录执行任务,而且 **同一个目录上的任务串行执行**,不再并行。换来的是:你的现有 checkout、当前分支、未提交的脏改动一切照旧 —— Multica 不会重新 clone。
### 什么时候选 `local_directory`、什么时候继续用 `github_repo`
| 关注点 | `github_repo`worktree 模式) | `local_directory` |
| --- | --- | --- |
| 每次任务的 checkout 成本 | 重新 clone + worktree | 0 —— 智能体原地干活 |
| 同一仓库的并发 | 任意条任务并行 | 同一目录每次一条 |
| 分支 / 脏改动 | 每条任务从默认分支拉一个干净分支 | 用目录当前的状态 |
| 可在哪台机器跑 | 任意守护进程 | 仅绑定的那一台 |
| 磁盘占用 | 每条任务一个 worktree | 0 —— 用你已有的目录 |
下面两种场景下推荐选 `local_directory`
1. **重新 clone 的成本过高** —— 几十 GB 的游戏 checkout、带大量 LFS 资源的 monorepo、或者任何场景下 `git clone` 的耗时会盖过实际工作量。你拿并发能力换"零 clone"的运行。
2. **改动很细碎,需要频繁在本地 review** —— 你在反复打磨某个组件,几分钟就要切回编辑器看一眼智能体改了什么;与其每次去 `~/multica_workspaces/` 翻一个新 worktree不如让它就在你已有的 checkout 里干活。
两种情况下你接受的取舍是同一个:**当前版本没有任何文件级的写入锁**。"同一目录上的任务串行执行"这一道闸是唯一防止两个 issue 的智能体同时改同一份文件的保护。如果你把两个 issue 的智能体都指向同一个 `local_directory`,它们的任务会排队、而不是并行 —— 这是有意的。如果需要在同一份代码上真正的并行,请继续用 `github_repo`。
### 添加本地目录
文件夹选择器 **只在桌面端** 提供 —— 浏览器没法读 OS 路径,所以网页端不显示"添加本地目录"按钮。桌面端的流程是:
1. 打开项目 → **Resources资源** 面板。
2. 点击 **Add local directory添加本地目录**,系统会弹出原生文件夹选择器。
3. 选一个文件夹。这个路径会被绑定到 **当前这台桌面端注册的守护进程** —— 资源记录里同时保存路径和该守护进程的 ID。
在桌面端,当本机的守护进程离线、或者这个项目已经在本机绑定了一个 `local_directory` 时,按钮 **会保留显示但置灰**,并在下方给出一行说明 —— 让你看得到为什么暂时不能用。(网页端则是直接隐藏整个按钮,因为网页端本来就没有文件夹选择器。)要把另一台机器上的目录绑过来,需要在那台机器装上桌面端、并在那边添加资源。
也可以用 CLI即便环境是纯网页端也行只要你自己提供守护进程 ID
```bash
multica project resource add <project-id> \
--type local_directory \
--local-path /Users/me/code/big-game \
--daemon-id <daemon-uuid> \
--ref-label "主开发目录" # 可选
multica project resource update <project-id> <resource-id> \
--local-path /Users/me/code/big-game-new
```
`--daemon-id` 可以通过 `multica daemon list` 拿到。CLI 也接受 `--ref '<json>'` 这种通用形式,直接传 payload。
### 路径规则
挂资源时和每次任务启动时,守护进程都会校验一次路径(服务器只负责存 JSON不做校验。任何一条不满足的任务会以一个有类型的错误失败**不会动你的目录**
- 必须是 **绝对路径**。
- 必须 **存在**,并且是 **目录**(不能是普通文件、设备节点、或者指向文件的 symlink
- 守护进程进程必须能 **读 + 写**。
- 不能是系统根目录或整个用户配置区 —— `/`、`/Users`、`/home`、`/root`、`/etc`、`/tmp`、`/var`、`/usr`、`/opt`、`/Users/Shared`、你自己的 `$HOME`、任意 Windows 盘根(`C:\`、`D:\`…),以及 `C:\Users` / `C:\ProgramData` / `C:\Program Files` / `C:\Program Files (x86)` / `C:\Windows`。
- 指向上述任何路径的 symlink 也会被拒macOS 的 canonical 别名(比如手动输入 `/private/tmp`)和 `/tmp` 受同样的限制。
黑名单故意做得激进 —— 选你自己的 home 目录就意味着 Multica 的运行时文件会出现在你账号的根下,这不会是任何人想要的结果。请选一个子目录(一般就是你具体的项目 checkout
### 每台守护进程每个项目最多一条
同一项目上 **每台守护进程最多只能绑一个 `local_directory`**。在同一台机器上想再加一条会被 API 用 `409` 拒绝;桌面端的按钮在达到上限时会自动隐藏,并在 tooltip 里说明原因。
不同的守护进程互不影响 —— 一个共享的项目可以为每个队友的机器各绑一个 `local_directory`,把同一个项目挂到不同主机上的不同目录。守护进程领任务时会挑跟自己 ID 匹配的那一行,其它的忽略。
### 混用资源类型,以及多个 `local_directory` 资源
实际会碰到两种跨资源的组合:
- **同一个项目同时挂 `github_repo` 和 `local_directory`。** 在拥有匹配 `local_directory` 绑定的那台守护进程上,本地目录 **优先**:智能体跑在你的目录里,这次任务不会为项目的 `github_repo` 创建或使用 worktree。每工作区的仓库缓存可能仍会照常 sync这层是和具体任务无关的后台行为。`github_repo` 的 URL 仍然会出现在 `.multica/project/resources.json` 和智能体的 `## Repositories` 段里供参考,但智能体真正在编辑的工作树是你的本地目录,不是 worktree。在 **没有** 匹配 `local_directory` 行的其他守护进程上(不同机器、或者那位队友还没挂本地目录),任务按原本的 `github_repo` worktree 流程走。本质上 `local_directory` 是某台守护进程对 worktree 路径的一个覆盖。
- **同一个项目挂两个 `local_directory`。** 每个 `local_directory` 只能绑一台守护进程,所以这只会发生在两台不同机器之间(同一台机器上想加第二条会被 API 当场拒掉,见上一节)。任务的去向由智能体 runtime 绑定决定,不是由"哪台守护进程有本地目录"决定 —— 任务会落到承载这条智能体 runtime 的那台守护进程上,由它挑出匹配自己 ID 的那一行、忽略其他行。这里没有负载均衡:如果你想让某台机器跑某条任务,请把任务派给绑在那台机器 runtime 上的智能体。
如果某台守护进程没有匹配这个项目的 `local_directory` 行,**不会**因为别处有人绑了就被拦下 —— 它的任务会照常走项目里的其他资源(一般就是 `github_repo` 那条 fallback 路径)。`local_directory` 只对它绑定的那台守护进程生效。
### 任务在本地目录里如何运行
当一条任务被分发到某个 issue、并且该项目在领任务的守护进程上绑了 `local_directory` 时,守护进程会:
1. 再次校验路径(按上面那套规则)。
2. 以 symlink 解析后的真实路径为 key 取一把"目录锁" —— 即便两条路径走不同路由(一条经过 symlink一条直接指向同一个文件夹也会被串行化。
3. 把智能体的 `CLAUDE.md` / `AGENTS.md`(以及 `.multica/project/resources.json`**写进你的目录**。智能体就在那里工作,跟你自己打开这个文件夹后启动它是一样的体验。
4. Multica 自己的运行时产物(`output/`、`logs/`、`.gc_meta.json`)放在 **你目录之外** 的一个独立 envRoot 里。
如果第一条任务还在跑、第二条针对同一目录的任务又来了,第二条会停在 **Waiting for local directory等待本地目录释放** 状态。这个状态在所有相关 UI 都看得见 —— 聊天里的任务状态条、智能体横幅、执行记录、活动指示器;等待中的任务会被计入该智能体的"排队"占用。取消等待中的任务会立即释放它的槽位;取消正在跑的那条会让下一条立即顶上。
等待没有超时 —— 一条等待中的任务会一直等到锁释放、或者被用户/智能体取消为止。
### Multica 会写、不会动什么
- **会写入** `CLAUDE.md` / `AGENTS.md`(或对应 provider 的等价文件)以及 `.multica/project/resources.json` 到目录根 —— 这样智能体才有自己的元 skill 和资源清单。如果不想把这些提交到 git请加进 `.gitignore`。
- **会写入** 智能体决定做出的所有代码改动 —— 跟你自己在本地跑这个智能体没有区别。
- **永远不会物理删除** 你的目录或里面任何内容。垃圾回收对路径是有判断的:对 `local_directory` 的 envRoot它只会清自己挂在 `workspacesRoot` 下的 `output/` 与 `logs/`**完全不碰** 你的目录。
### v1 阶段的限制(后续会逐步收敛)
第一版有意比 `github_repo` 留了更多锋利的边。下面这份清单会随着后续工作逐步缩短;这里列出的是 **今天**的真实行为:
- **不会自动切分支。** 智能体跑在你当前 checkout 的分支上。如果分支选择重要,请在分发任务前自己切换好。
- **不会保护脏工作区,也不会自动 commit。** 未提交的改动智能体看得到、可能就地修改,不会被 stash。请把这个目录当作真实的工作区来对待重要的运行前自己先 commit。
- **不会自动开 PR。** 任务结束后,改动就停在它实际做出来的分支上 —— Multica 不会 push、不会开 PR。需要 push、需要 PR 的话请自己来。
- **`waiting_local_directory` 只显示状态,不显示是谁占着。** 这个徽标只告诉你"任务在等",不会告诉你具体是哪条任务、哪个路径正在持有目录。
这些都列在 local-directory 工作的"智能体任务生命周期"后续项里;在那之前,请把 `local_directory` 当作"智能体在你的目录里跑,跟你自己跑没区别"来用。
## 在创建项目时挂仓库
在 **网页端** 或 **桌面端**,打开 *新建项目* 时Status / Priority / Lead 旁边多了一个 **Repos** 标签。选已绑到工作区的仓库(或者粘贴一个临时 URL项目创建的瞬间它们就以 `github_repo` 资源的形式挂上去。
通过 **CLI**
```bash
# 创建 + 挂资源一步到位。资源是在同一个事务里挂上的 ——
# 任何一个资源不合法都会让整个 create 回滚,绝不会出现
# "项目建好了但资源只挂了一半"的状态。
multica project create \
--title "Agent UX 2026" \
--repo https://github.com/multica-ai/multica
# 后续管理资源
multica project resource list <project-id>
multica project resource add <project-id> --type github_repo --url <url>
multica project resource remove <project-id> <resource-id>
# 任何服务器认识的 resource_type 都可以用这个通用入口 ——
# 加新类型时不需要改 CLI
multica project resource add <project-id> \
--type notion_page \
--ref '{"page_id":"…","title":"…"}'
```
`--repo` 可以重复指定;每个值会被当作一条单独的 `github_repo` 资源挂上去。
## 运行时智能体看到什么
当守护进程为一个项目内的 issue 启动智能体时,会发生两件事:
### 1. `.multica/project/resources.json`
API 响应的结构化透传,写进智能体的工作目录里:
```json
{
"project_id": "…",
"project_title": "Agent UX 2026",
"resources": [
{
"id": "…",
"resource_type": "github_repo",
"resource_ref": {
"url": "https://github.com/multica-ai/multica",
"default_branch_hint": "main"
}
}
]
}
```
Skill、辅助脚本、或者智能体自己想拿到本次运行**精确的**资源列表时,解析这个文件即可。
### 2. 元 skill prompt 里的 "Project Context" 段
智能体的 `CLAUDE.md` / `AGENTS.md`(按 provider 不同)里多出一段人类可读的摘要:
```
## Project Context
This issue belongs to **Agent UX 2026**.
Project resources (also written to `.multica/project/resources.json`):
- **GitHub repo**: https://github.com/multica-ai/multica (default branch: `main`)
Resources are pointers — open them only when relevant to the task. For
`github_repo` resources, use `multica repo checkout <url>` to fetch the code.
```
这段文字故意做得很精简:完整 payload 已经写到磁盘上了prompt 只是给智能体一个定位 —— 让它知道项目存在、知道挂了什么。
### 失败时
资源拉取是 **best-effort**。如果 API 调失败了prompt 里的项目段会省掉、文件也不写,但任务照常启动 —— 智能体永远不会因为缺少项目上下文而卡住。
## 加一种新资源类型
抽象的全部目的就是让加新类型变便宜。完整流程:
1. **服务端校验器**`server/internal/handler/project_resource.go`)—— 在 `validateAndNormalizeResourceRef` 里加一个 case解析并标准化新的 payload。
2. **守护进程元 skill 格式化器**`server/internal/daemon/execenv/runtime_config.go`)—— 在 `formatProjectResource` 里加一个 case让智能体 prompt 把新类型渲染成一条可读的列表项。
3. **TypeScript 类型**`packages/core/types/project.ts`)—— 给 `ProjectResourceType` 加值,并加上对应的 payload 接口。
4. **UI 渲染**`packages/views/projects/components/project-resources-section.tsx`)—— 在 `ResourceRow` 里加一个 case 渲染新类型。
**不需要 schema 迁移、不需要新的 sqlc query、不需要新的 endpoint、也不需要改 CLI** —— CLI 的通用 `--ref '<json>'` 选项接受校验器认识的任何 payload第 0 天就能用。(之后如果想加按类型的 CLI 快捷参数,可以加,但不是必须。)
同一张 `project_resource` 表、同一套 CRUD 调用,处理所有类型。
## 工作区仓库 vs. 项目仓库
智能体看到的仓库列表(`CLAUDE.md` / `AGENTS.md` 里的 `## Repositories` 段)由守护进程的领任务逻辑按以下优先级决定:
- **项目挂了至少一条 `github_repo` 资源** → 仅显示项目挂的仓库。工作区绑定的仓库会被隐藏,避免智能体猜哪一个属于这个 issue。
- **项目没挂 `github_repo` 资源(或 issue 根本不在项目里)** → 回落到工作区的仓库列表,跟以前一样。
这样可以把智能体的工作集压紧:项目把仓库说清楚了,那就是权威答案。而 `.multica/project/resources.json` 里始终带着完整列表,需要查看全部资源的 skill 仍然能拿到。
守护进程在 checkout 这一侧也是配套的:当任务带着项目级别的 `github_repo` URL 进来时,这些 URL 会被合并进每工作区的白名单,并在智能体启动前同步进本地仓库缓存。所以即便某个项目仓库 URL 没被绑到工作区,`multica repo checkout` 也能正常处理它,不会以"未配置"为由拒绝。白名单的划分只是内部实现:工作区绑定的 URL 和任务级别的 URL 分开记录,工作区仓库列表的刷新不会顺手吊销某条项目 URL。
## 当前**不**做的事
- **跨项目共享**。每条资源现在只能挂在一个项目上。
- **按 skill 划定资源可见性**。所有资源对智能体本次运行里的每个 skill 都可见;按类型过滤是后续工作。
- **缓存 / 同步**。`github_repo` 目前只是元数据 —— checkout 仍然按需走 `multica repo checkout`。Notion / Google Docs 之类的文档文本缓存会随对应类型一起到位。
这些是有意的留白 —— 第一版的目标是用最小的活动部件去验证这个抽象。