① 「项目」的定义
+一个 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 让代码直接落到目标位置 +