① 「项目」的定义
一个 DevLoop 项目 = 一个目录 + 一个 project.json + 一棵 git 历史。这个目录就是 project root,是整个 DevLoop 流程的唯一锚点。
所有路径都从 root 派生:
{project_root}/ # 这就是「根」
├── .git/ # GitVault init
├── .gitignore
├── project.json # M-1 元数据 + root_path 自身
├── prd/ # PM 写
├── sdd/ # Architect 写
├── units.json
├── units/{unitId}/ # Coder/Tester 写
├── integration/ # Integrator/Triager 写
├── agent-state/ # P-8 持久化
├── timeline.ndjson · bugs.ndjson · changelog.ndjson
├── stream.log
└── history/ # 归档 (.gitignore)
所有 Agent、所有工具、LoopController、GitVault 都按 {project_root}/... 找文件。没有任何代码硬编码 data/companies/...。这是关键设计。
② 三种项目模式
| mode | root_path | git repo | v1 支持 | 典型场景 |
|---|---|---|---|---|
| sandbox (默认) |
自动生成:{server_data_dir}/companies/{cid}/projects/{pid}/ |
自动 git init | ✅ v1 | 不确定代码归宿 / 内部 demo / 学习 / 隔离实验 |
| linked-new | 用户指定空目录或不存在的目录,比如 ~/code/auto-todo |
自动 git init 到该路径 | ✅ v1 | 已知代码最终归宿;想生成就直接落盘那里;之后可 git push |
| linked-existing | 用户指定已有 git repo 的子目录,比如 ~/code/my-app/services/auth/ |
复用上级 git repo | ⚠️ v2 | 给已存在的项目增量加新模块;冲突风险高,需要 git stash 等保护 |
v1 推荐:sandbox 是默认。linked-new 当作 power user 选项 — 它给你「自动 push 到 GitHub」的可能性,但也要承担「DevLoop 误覆盖了你 ~/code/ 下别的东西」的风险(沙箱机制阻止越界,但 user-chosen path 越界相对沙箱更难定义)。
③ project.json 加 root_path 字段
M-1 ProjectMeta 现在含 root_path(绝对路径)和 mode:
const ProjectMeta = Type.Object({ id: Type.String({ pattern: '^p-[a-z0-9]{6}$' }), company_id: Type.String(), name: Type.String(), // 新增字段 mode: Type.Union([ Type.Literal('sandbox'), Type.Literal('linked-new'), Type.Literal('linked-existing'), ]), root_path: Type.String(), // 绝对路径,所有派生路径的源 status: Type.Union([ Type.Literal('prd_drafting'), Type.Literal('running'), Type.Literal('blocked'), Type.Literal('shipped'), Type.Literal('shipped-final'), Type.Literal('abandoned'), ]), ... });
所有组件读 path 都走 resolveProjectPath(pid) → project.root_path,而不是拼 data/companies/.../。
④ 路径解析服务(新组件)
server/services/path-resolver.js · 新增,~80 行
module.exports.pathResolver = { // 单例索引: pid → root_path // 在 LoopController 启动时 walk(data/companies/) 建立 // 加: linked 项目通过 ~/.devloop/linked-projects.json 维护索引 init(): Promise<void>, resolveProjectRoot(pid: string): string, // 绝对路径,unknown pid 抛错 resolveUnitDir(pid, unitId): string, resolvePrdSite(pid): string, resolveSddSite(pid): string, resolveAgentState(pid, agent): string, resolveMemoryDir(pid, agent): string, // 注意:memory 总在 company 级,不在 root 下 resolveTimelineLog(pid): string, ... // 反向 findProjectByPath(abs: string): { pid, root } | null, // linked 项目注册 registerLinkedProject({ pid, root_path, mode }): Promise<void>, unregisterLinkedProject(pid): Promise<void>, };
关键:所有用 projectsDir(pid) 的地方改成 pathResolver.resolveProjectRoot(pid)。这样切换 mode 是路径透明的。Agent 的 ctx.projectDir 注入也走这个。
linked 项目索引 ~/.devloop/linked-projects.json 形如:
{ "p-731042": { "root_path": "/Users/yibo/code/auto-todo", "mode": "linked-new", "company_id": "c-lemonv", "registered_at": "2026-05-13T11:31:00Z" } }
sandbox 项目不需要注册(按惯例从 data/companies/.../projects/ 扫描即可)。
⑤ 三个创建入口
5.1 REST · 主入口
POST /api/devloop/projects { "company_id": "c-lemonv", "name": "Auto TODO", "requirement": "做一个 TODO API + Vue 前端", // 新增 - 可选,默认 mode=sandbox "mode": "sandbox" | "linked-new" | "linked-existing", "target_path": "/Users/yibo/code/auto-todo" // linked-* 时必填 } // 响应 { "project_id": "p-731042", "mode": "linked-new", "root_path": "/Users/yibo/code/auto-todo", "status": "prd_drafting", "stream_url": "/api/devloop/projects/p-731042/stream" }
5.2 CLI · 命令行
# 文件: scripts/devloop-new.js (NEW, ~80 行) # sandbox 模式 (默认) node scripts/devloop-new.js \ --name "Auto TODO" \ --requirement "做一个 TODO API + Vue 前端" # 等价于 mode=sandbox, root 自动到 data/.../projects/p-XXXXXX/ # 输出: 🍋 Project p-731042 created (sandbox) # Root: /Users/yibo/Documents/lemonAI/lemonboss/data/companies/c-default/projects/p-731042 # linked-new 模式 node scripts/devloop-new.js \ --path ~/code/auto-todo \ --name "Auto TODO" \ --requirement "做一个 TODO API + Vue 前端" # 输出: 🍋 Project p-731042 created (linked-new) # Root: /Users/yibo/code/auto-todo # git init done # PM Agent dispatched, follow at http://localhost:3010/devloop/p-731042 # 也支持 stdin 多行需求 (long requirement) cat my-spec.md | node scripts/devloop-new.js --path ~/code/x --name "X" --requirement - # 列出现有项目 node scripts/devloop-new.js --list # 输出: # p-731042 Auto TODO linked-new ~/code/auto-todo running # p-845001 Auth System sandbox data/.../p-845001 shipped
5.3 UI · Dashboard 的「+ NEW PROJECT」
选 linked-new 时弹出 file picker 让用户选目录(或手填路径)。选 sandbox 时不显示 path 输入。
⑥ 创建流程 · 三模式各自 step-by-step
// LoopController.createProject({ company_id, name, requirement }) step 1 · 生成 pid = "p-" + randomBase36(6) step 2 · root_path = `${dataDir}/companies/${company_id}/projects/${pid}/` step 3 · fs.mkdirSync(root_path, { recursive: true }) step 4 · 写 project.json (含 root_path · mode='sandbox') step 5 · GitVault.init(pid) → 在 root_path 起 .git/ step 6 · 写 .gitignore 模板 → 首个 commit "init: project ${pid} · ${name}" step 7 · pathResolver 不需要 register (按 data/ 路径约定可扫到) step 8 · LoopController.onHumanFeedback(pid, { text: requirement }) → PM Agent 异步启动 // 全程在 server 进程内,无文件系统外部依赖
// 同上,但 root_path 来自用户 step 1 · 校验 target_path: - 必须是绝对路径 - 必须不存在,或者存在但是空目录 - 必须不在系统敏感目录 (e.g. /, /etc, /System) - 必须可写 (fs.accessSync check) - 如果父目录不存在 → 拒绝 (不递归创建,避免误操作) step 2 · 生成 pid (同上) step 3 · root_path = target_path (绝对路径,resolve('~') if any) step 4 · fs.mkdirSync(root_path, { recursive: false }) // 必须显式空 step 5 · 写 project.json step 6 · GitVault.init(pid) → git init 在用户路径 step 7 · 写 .gitignore 模板 → 首个 commit step 8 · pathResolver.registerLinkedProject({ pid, root_path, mode }) → 写 ~/.devloop/linked-projects.json step 9 · onHumanFeedback → PM 启动 // 与 sandbox 区别仅在 root_path 来源和 register 步骤
// v2 (v1 拒绝接受 linked-existing) step 1 · 校验 target_path 已存在 + 是 git repo (含 .git) step 2 · git status 必须 clean (否则要求用户先 commit/stash) step 3 · 在 target_path 下创建子目录 (如 services/{pid}/ 或用户指定) step 4 · 写 project.json 到该子目录 step 5 · GitVault 不 init (复用上级 repo) step 6 · 追加 .gitignore 条目 (sandbox-specific exclusions 到上级 .gitignore) step 7 · pathResolver.registerLinkedProject(mode='linked-existing') step 8 · 创建 feature branch: git checkout -b devloop/${pid} step 9 · 后续 commit 都在该 branch step 10 · shipped 时不自动合 main,等人 review + merge // 风险点:用户其他工具(IDE/lint) 同时改文件 → 冲突 # v2 才做,因为对边界 case 处理复杂
⑦ 各组件如何引用 root_path
关键改造:没有任何地方硬编码 data/companies/...。一切走 pathResolver。
| 原来(硬编码) | 改成(path-resolver) |
|---|---|
path.join(dataDir, 'companies', cid, 'projects', pid) | pathResolver.resolveProjectRoot(pid) |
projectsDir(pid) helper | delegate 到 pathResolver |
| Agent ctx.projectDir 注入 | 由 callAgentWithPersistence 用 pathResolver 计算 |
| data-layout.html 的目录示意 | 明确写 {project_root}/ 而非 data/companies/.../projects/{pid}/ |
| GitVault 内部 cwd | 每次操作前 pathResolver.resolveProjectRoot(pid) |
| Bash 工具的沙箱 cwd 锁 | 同上 |
这是 P0 / P1 阶段必须的抽象化重构,否则 linked 模式没法实现。代价不大(~30 行改动散在 5-6 处)。
⑧ memory · 始终在 company 级
注意:memory 不跟 project root 移动。memory 是 company 范围长期记忆,所以它永远在:
{server_data_dir}/companies/{cid}/memory/devloop/
├── pm/*.md
├── architect/*.md
└── ...
哪怕 project 在 ~/code/auto-todo 下,memory 仍在 server 的 data/ 内。这样 linked 项目也能享受跨项目记忆,且 memory 不会跟着 git push 出去(隐私)。
⑨ 跨主机 / 跨机器场景
linked-new 模式启用后,项目根可以跨机器:
- 主机 A 上 DevLoop 创建项目,root =
~/code/x git push origin main推到 GitHub- 主机 B
git clone到任意位置 - 主机 B 上重启 DevLoop server,不会识别这个项目(pathResolver 索引在 A 上)
- 主机 B 用
node scripts/devloop-import.js --path ~/work/x重新注册(NEW,v2 工具)
memory 不跨机器(属 company 数据);timeline / bugs / changelog 跟着 git 走(tracked);agent-state 不跟(.gitignore)— 这意味着对端续跑不能恢复 paused agents,要从干净状态重启。可接受。
⑩ 创建后 LoopController 接管的统一接口
不管哪种模式,创建后 LoopController 的视角无差异:
// 任何 mode 都走这个接口 loopController.onHumanFeedback(pid, { text, origin }); // LoopController 内部: // 1. pathResolver.resolveProjectRoot(pid) → 拿到根路径(不关心是 sandbox 还是 linked) // 2. 注入 ctx 给 Agent: ctx.projectDir = root // 3. Agent 通过 Write 工具落盘,沙箱白名单基于 root + 子路径(写 prd/** 等) // 4. GitVault.commit({ pid, ... }) → 自动在 root 上 git 操作
这就是路径透明的好处:业务代码无 mode 判断分支。
⑪ project.json 完整示例
// sandbox 项目 { "id": "p-731042", "company_id": "c-lemonv", "name": "Auto TODO", "mode": "sandbox", "root_path": "/Users/yibo/Documents/lemonAI/lemonboss/data/companies/c-lemonv/projects/p-731042", "status": "running", "created_at": "2026-05-13T10:31:00Z", "updated_at": "2026-05-13T11:02:00Z", "owner": "yibo", "token_budget": 5000000, "token_used": 42018, "compressOldRuns": false } // linked-new 项目 { "id": "p-845001", "company_id": "c-lemonv", "name": "Auto Auth System", "mode": "linked-new", "root_path": "/Users/yibo/code/auto-auth", // 用户选的 "status": "running", ... }
⑫ 路径白名单(沙箱安全)
linked 模式给了用户灵活性,但也带来风险。沙箱必须严格:
| 规则 | 实现 |
|---|---|
| 拒绝 root_path = 系统敏感路径 | 黑名单:/, /etc, /System, /usr, /var, /Library, /Applications, $HOME/.ssh, $HOME/.aws, $HOME/Downloads(防误操作) |
| 拒绝 root_path 不可写 | fs.accessSync(W_OK) 检查 |
| 拒绝 root_path 已存在且非空(linked-new) | fs.readdirSync 检查 |
| Agent 写白名单走 root 相对路径 | writeAllowList: ["prd/**", "sdd/**", "units/**", ...] · resolve 后必须以 root_path 开头 |
| 跨项目读写 | 禁止:任何 ../{别的项目} 路径直接拒绝 |
| memory 跨项目读 | 白名单例外:memory/devloop/ 在 company 级允许只读 |
⑬ 实战命令
# 启动 server PORT=3010 node server/index.js # 方式 A · REST sandbox echo '{"company_id":"c-lemonv","name":"todo-demo","requirement":"做一个 TODO API"}' > /tmp/req.json curl -X POST http://localhost:3010/api/devloop/projects -d @/tmp/req.json # → { "project_id": "p-731042", "mode": "sandbox", "root_path": "/.../data/companies/c-lemonv/projects/p-731042" } # 方式 B · REST linked-new echo '{"company_id":"c-lemonv","name":"Auto TODO","requirement":"做一个 TODO API","mode":"linked-new","target_path":"/Users/yibo/code/auto-todo"}' > /tmp/req.json curl -X POST http://localhost:3010/api/devloop/projects -d @/tmp/req.json # → { "project_id": "p-845001", "mode": "linked-new", "root_path": "/Users/yibo/code/auto-todo" } # 在 ~/code/auto-todo 下立即看到 .git · .gitignore · project.json · prd/ 等开始生成 # 方式 C · CLI node scripts/devloop-new.js \ --path ~/code/auto-todo \ --name "Auto TODO" \ --requirement "做一个 TODO API" # 方式 D · UI (假设 frontend 启动) # 浏览器: http://localhost:5173/devloop · 点 "+ NEW PROJECT" · 填表 · 提交 # 列出现有项目 (跨 sandbox + linked) node scripts/devloop-new.js --list curl http://localhost:3010/api/devloop/projects # → [{pid, name, mode, root_path, status}, ...] # 删除项目 (linked 模式不删用户目录,只 unregister + 移除 sandbox 索引) curl -X DELETE http://localhost:3010/api/devloop/projects/p-731042 # sandbox: 整 data/.../projects/p-XXXXXX/ 删除 # linked: 只从 ~/.devloop/linked-projects.json unregister,用户目录保留
⑭ 工时增量
| 任务 | 工时 | 归属阶段 |
|---|---|---|
| pathResolver 实现 + 写硬编码迁移 | 1.5 天 | P0 (前置,后续 GitVault/工具都依赖) |
| project.json schema 加 mode + root_path | 0.5 天 | P0 |
| POST /projects 加 mode + target_path 参数 | 0.5 天 | P1 |
| scripts/devloop-new.js CLI | 1 天 | P1 |
| scripts/devloop-import.js (v2) | — | v2 |
| UI 新建项目 modal | 1 天 | P4 |
| 合计 v1 | 4.5 天 | 分摊到 P0/P1/P4 |
P0 时间预算从 13.5 天 → 15.5 天(+2 天 path-resolver + schema),P1 从 5 天 → 6.5 天(+1.5 天 mode 参数 + CLI)。总工时 10 周 → 10.5 周。
⑮ 关联
- M-1 ProjectMeta · 加 mode + root_path 字段
- C-1 REST.createProject · 接受 mode + target_path
- Git Versioning · GitVault 用 root_path 当 cwd
- data-layout · 用 {project_root}/ 而非硬编码
- 代码归宿 · linked-new 让代码直接落到目标位置