diff --git a/apps/docs/content/docs/meta.zh.json b/apps/docs/content/docs/meta.zh.json index 403fc8dbab..5af2eddcf9 100644 --- a/apps/docs/content/docs/meta.zh.json +++ b/apps/docs/content/docs/meta.zh.json @@ -11,6 +11,7 @@ "issues", "projects", "comments", + "project-resources", "---智能体---", "agents", "agents-create", diff --git a/apps/docs/content/docs/project-resources.mdx b/apps/docs/content/docs/project-resources.mdx index bff15d27bf..819efe60fd 100644 --- a/apps/docs/content/docs/project-resources.mdx +++ b/apps/docs/content/docs/project-resources.mdx @@ -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,108 @@ 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 re-cloning is the expensive part. Stay on `github_repo` when you want multiple agents working in parallel. + +### 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. + +The button is also hidden (with a hint) when the daemon on this machine is offline, or when the project already has a `local_directory` bound to this daemon. 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 \ + --type local_directory \ + --local-path /Users/me/code/big-game \ + --daemon-id \ + --ref-label "main checkout" # optional + +multica project resource update \ + --local-path /Users/me/code/big-game-new +``` + +`--daemon-id` comes from `multica daemon list`. The CLI also accepts the generic `--ref ''` 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. + +### 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. diff --git a/apps/docs/content/docs/project-resources.zh.mdx b/apps/docs/content/docs/project-resources.zh.mdx new file mode 100644 index 0000000000..3ac61d83a6 --- /dev/null +++ b/apps/docs/content/docs/project-resources.zh.mdx @@ -0,0 +1,248 @@ +--- +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 —— 用你已有的目录 | + +如果"重新 clone"是最贵的步骤,选 `local_directory`;如果你需要同一仓库上多条任务并行,继续用 `github_repo`。 + +### 添加本地目录 + +文件夹选择器 **只在桌面端** 提供 —— 浏览器没法读 OS 路径,所以网页端不显示"添加本地目录"按钮。桌面端的流程是: + +1. 打开项目 → **Resources(资源)** 面板。 +2. 点击 **Add local directory(添加本地目录)**,系统会弹出原生文件夹选择器。 +3. 选一个文件夹。这个路径会被绑定到 **当前这台桌面端注册的守护进程** —— 资源记录里同时保存路径和该守护进程的 ID。 + +当本机的守护进程离线,或者这个项目已经在本机绑定了一个 `local_directory` 时,按钮会自动隐藏并给出说明。要把另一台机器上的目录绑过来,需要在那台机器装上桌面端、并在那边添加资源。 + +也可以用 CLI(即便环境是纯网页端也行,只要你自己提供守护进程 ID): + +```bash +multica project resource add \ + --type local_directory \ + --local-path /Users/me/code/big-game \ + --daemon-id \ + --ref-label "主开发目录" # 可选 + +multica project resource update \ + --local-path /Users/me/code/big-game-new +``` + +`--daemon-id` 可以通过 `multica daemon list` 拿到。CLI 也接受 `--ref ''` 这种通用形式,直接传 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 匹配的那一行,其它的忽略。 + +### 任务在本地目录里如何运行 + +当一条任务被分发到某个 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 +multica project resource add --type github_repo --url +multica project resource remove + +# 任何服务器认识的 resource_type 都可以用这个通用入口 —— +# 加新类型时不需要改 CLI: +multica project resource add \ + --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 ` 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 ''` 选项接受校验器认识的任何 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 之类的文档文本缓存会随对应类型一起到位。 + +这些是有意的留白 —— 第一版的目标是用最小的活动部件去验证这个抽象。