arch-project-root.html
29.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Project Root · LemonBoss PRD</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300;9..144,500;9..144,700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="assets/shell.css" />
</head>
<body data-page="arch-project-root.html">
<div class="shell">
<aside id="sidebar"></aside>
<main class="page">
<header class="page-head">
<div class="crumb"><span class="pill">05 · ARCHITECTURE</span><span class="sep">/</span><span>Project Root</span></div>
<h1>Project Root · 项目根目录是什么 · 在哪建</h1>
<p class="deck">DevLoop 的<b>所有产物</b>(PRD 站点 · SDD 站点 · units/code/ · tests · runs · timeline · bugs · changelog · .git)都围绕一个 <b>project root</b> 组织。本页定义这个根是什么、如何创建、三种模式(sandbox / linked-new / linked-existing)、三种入口(REST / CLI / UI),以及各组件如何引用它而不硬编码路径。</p>
</header>
<article class="body">
<section>
<h2>① 「项目」的定义</h2>
<p>一个 <b>DevLoop 项目</b> = 一个目录 + 一个 <code>project.json</code> + 一棵 git 历史。这个目录就是 <b>project root</b>,是整个 DevLoop 流程的<b>唯一锚点</b>。</p>
<p>所有路径都从 root 派生:</p>
<pre class="tree">
{project_root}/ <span class="com"># 这就是「根」</span>
├── .git/ <span class="com"># GitVault init</span>
├── .gitignore
├── project.json <span class="com"># M-1 元数据 + root_path 自身</span>
├── prd/ <span class="com"># PM 写</span>
├── sdd/ <span class="com"># Architect 写</span>
├── units.json
├── units/{unitId}/ <span class="com"># Coder/Tester 写</span>
├── integration/ <span class="com"># Integrator/Triager 写</span>
├── agent-state/ <span class="com"># P-8 持久化</span>
├── timeline.ndjson · bugs.ndjson · changelog.ndjson
├── stream.log
└── history/ <span class="com"># 归档 (.gitignore)</span>
</pre>
<p>所有 Agent、所有工具、LoopController、GitVault 都按 <code>{project_root}/...</code> 找文件。<b>没有任何代码硬编码 <code>data/companies/...</code></b>。这是关键设计。</p>
</section>
<section>
<h2>② 三种项目模式</h2>
<table>
<thead><tr><th>mode</th><th>root_path</th><th>git repo</th><th>v1 支持</th><th>典型场景</th></tr></thead>
<tbody>
<tr>
<td><b>sandbox</b><br/>(默认)</td>
<td>自动生成:<code>{server_data_dir}/companies/{cid}/projects/{pid}/</code></td>
<td>自动 git init</td>
<td>✅ v1</td>
<td>不确定代码归宿 / 内部 demo / 学习 / 隔离实验</td>
</tr>
<tr>
<td><b>linked-new</b></td>
<td>用户指定空目录或不存在的目录,比如 <code>~/code/auto-todo</code></td>
<td>自动 git init 到该路径</td>
<td>✅ v1</td>
<td>已知代码最终归宿;想生成就直接落盘那里;之后可 git push</td>
</tr>
<tr>
<td><b>linked-existing</b></td>
<td>用户指定已有 git repo 的子目录,比如 <code>~/code/my-app/services/auth/</code></td>
<td>复用上级 git repo</td>
<td>⚠️ v2</td>
<td>给已存在的项目<b>增量</b>加新模块;冲突风险高,需要 git stash 等保护</td>
</tr>
</tbody>
</table>
<p><b>v1 推荐:sandbox 是默认。</b>linked-new 当作 power user 选项 — 它给你「自动 push 到 GitHub」的可能性,但也要承担「DevLoop 误覆盖了你 ~/code/ 下别的东西」的风险(沙箱机制阻止越界,但 user-chosen path 越界相对沙箱更难定义)。</p>
</section>
<section>
<h2>③ project.json 加 root_path 字段</h2>
<p><a href="sdd-model.html">M-1 ProjectMeta</a> 现在含 root_path(绝对路径)和 mode:</p>
<pre class="code shape"><span class="k">const</span> ProjectMeta = Type.<span class="n">Object</span>(<span class="p">{</span>
id: Type.<span class="n">String</span>(<span class="p">{</span> pattern: <span class="s">'^p-[a-z0-9]{6}$'</span> <span class="p">}</span>),
company_id: Type.<span class="n">String</span>(),
name: Type.<span class="n">String</span>(),
<span class="c">// 新增字段</span>
mode: Type.<span class="n">Union</span>(<span class="p">[</span>
Type.<span class="n">Literal</span>(<span class="s">'sandbox'</span>),
Type.<span class="n">Literal</span>(<span class="s">'linked-new'</span>),
Type.<span class="n">Literal</span>(<span class="s">'linked-existing'</span>),
<span class="p">]</span>),
root_path: Type.<span class="n">String</span>(), <span class="c">// 绝对路径,所有派生路径的源</span>
status: Type.<span class="n">Union</span>(<span class="p">[</span>
Type.<span class="n">Literal</span>(<span class="s">'prd_drafting'</span>),
Type.<span class="n">Literal</span>(<span class="s">'running'</span>),
Type.<span class="n">Literal</span>(<span class="s">'blocked'</span>),
Type.<span class="n">Literal</span>(<span class="s">'shipped'</span>),
Type.<span class="n">Literal</span>(<span class="s">'shipped-final'</span>),
Type.<span class="n">Literal</span>(<span class="s">'abandoned'</span>),
<span class="p">]</span>),
...
<span class="p">}</span>);</pre>
<p>所有组件读 path 都走 <code>resolveProjectPath(pid) → project.root_path</code>,而不是拼 <code>data/companies/.../</code>。</p>
</section>
<section>
<h2>④ 路径解析服务(新组件)</h2>
<code class="path mono" style="color:var(--ink-soft)">server/services/path-resolver.js · 新增,~80 行</code>
<pre class="code shape">module.<span class="n">exports</span>.pathResolver = <span class="p">{</span>
<span class="c">// 单例索引: pid → root_path</span>
<span class="c">// 在 LoopController 启动时 walk(data/companies/) 建立</span>
<span class="c">// 加: linked 项目通过 ~/.devloop/linked-projects.json 维护索引</span>
<span class="n">init</span>(): <span class="n">Promise</span><<span class="n">void</span>>,
<span class="n">resolveProjectRoot</span>(pid: <span class="n">string</span>): <span class="n">string</span>, <span class="c">// 绝对路径,unknown pid 抛错</span>
<span class="n">resolveUnitDir</span>(pid, unitId): <span class="n">string</span>,
<span class="n">resolvePrdSite</span>(pid): <span class="n">string</span>,
<span class="n">resolveSddSite</span>(pid): <span class="n">string</span>,
<span class="n">resolveAgentState</span>(pid, agent): <span class="n">string</span>,
<span class="n">resolveMemoryDir</span>(pid, agent): <span class="n">string</span>, <span class="c">// 注意:memory 总在 company 级,不在 root 下</span>
<span class="n">resolveTimelineLog</span>(pid): <span class="n">string</span>,
...
<span class="c">// 反向</span>
<span class="n">findProjectByPath</span>(abs: <span class="n">string</span>): <span class="p">{</span> pid, root <span class="p">}</span> | <span class="v">null</span>,
<span class="c">// linked 项目注册</span>
<span class="n">registerLinkedProject</span>(<span class="p">{</span> pid, root_path, mode <span class="p">}</span>): <span class="n">Promise</span><<span class="n">void</span>>,
<span class="n">unregisterLinkedProject</span>(pid): <span class="n">Promise</span><<span class="n">void</span>>,
<span class="p">}</span>;</pre>
<p><b>关键:</b>所有用 <code>projectsDir(pid)</code> 的地方改成 <code>pathResolver.resolveProjectRoot(pid)</code>。这样切换 mode 是<b>路径透明</b>的。Agent 的 <code>ctx.projectDir</code> 注入也走这个。</p>
<p>linked 项目索引 <code>~/.devloop/linked-projects.json</code> 形如:</p>
<pre class="code"><span class="p">{</span>
<span class="n">"p-731042"</span>: <span class="p">{</span>
<span class="n">"root_path"</span>: <span class="s">"/Users/yibo/code/auto-todo"</span>,
<span class="n">"mode"</span>: <span class="s">"linked-new"</span>,
<span class="n">"company_id"</span>: <span class="s">"c-lemonv"</span>,
<span class="n">"registered_at"</span>: <span class="s">"2026-05-13T11:31:00Z"</span>
<span class="p">}</span>
<span class="p">}</span></pre>
<p>sandbox 项目<b>不需要</b>注册(按惯例从 <code>data/companies/.../projects/</code> 扫描即可)。</p>
</section>
<section>
<h2>⑤ 三个创建入口</h2>
<h3>5.1 REST · 主入口</h3>
<pre class="code shape">POST /api/devloop/projects
<span class="p">{</span>
<span class="n">"company_id"</span>: <span class="s">"c-lemonv"</span>,
<span class="n">"name"</span>: <span class="s">"Auto TODO"</span>,
<span class="n">"requirement"</span>: <span class="s">"做一个 TODO API + Vue 前端"</span>,
<span class="c">// 新增 - 可选,默认 mode=sandbox</span>
<span class="n">"mode"</span>: <span class="s">"sandbox"</span> | <span class="s">"linked-new"</span> | <span class="s">"linked-existing"</span>,
<span class="n">"target_path"</span>: <span class="s">"/Users/yibo/code/auto-todo"</span> <span class="c">// linked-* 时必填</span>
<span class="p">}</span>
<span class="c">// 响应</span>
<span class="p">{</span>
<span class="n">"project_id"</span>: <span class="s">"p-731042"</span>,
<span class="n">"mode"</span>: <span class="s">"linked-new"</span>,
<span class="n">"root_path"</span>: <span class="s">"/Users/yibo/code/auto-todo"</span>,
<span class="n">"status"</span>: <span class="s">"prd_drafting"</span>,
<span class="n">"stream_url"</span>: <span class="s">"/api/devloop/projects/p-731042/stream"</span>
<span class="p">}</span></pre>
<h3>5.2 CLI · 命令行</h3>
<pre class="code"><span class="c"># 文件: scripts/devloop-new.js (NEW, ~80 行)</span>
<span class="c"># sandbox 模式 (默认)</span>
node scripts/devloop-new.js \
--name <span class="s">"Auto TODO"</span> \
--requirement <span class="s">"做一个 TODO API + Vue 前端"</span>
<span class="c"># 等价于 mode=sandbox, root 自动到 data/.../projects/p-XXXXXX/</span>
<span class="c"># 输出: 🍋 Project p-731042 created (sandbox)</span>
<span class="c"># Root: /Users/yibo/Documents/lemonAI/lemonboss/data/companies/c-default/projects/p-731042</span>
<span class="c"># linked-new 模式</span>
node scripts/devloop-new.js \
--path ~/code/auto-todo \
--name <span class="s">"Auto TODO"</span> \
--requirement <span class="s">"做一个 TODO API + Vue 前端"</span>
<span class="c"># 输出: 🍋 Project p-731042 created (linked-new)</span>
<span class="c"># Root: /Users/yibo/code/auto-todo</span>
<span class="c"># git init done</span>
<span class="c"># PM Agent dispatched, follow at http://localhost:3010/devloop/p-731042</span>
<span class="c"># 也支持 stdin 多行需求 (long requirement)</span>
cat my-spec.md | node scripts/devloop-new.js --path ~/code/x --name <span class="s">"X"</span> --requirement -
<span class="c"># 列出现有项目</span>
node scripts/devloop-new.js --list
<span class="c"># 输出:</span>
<span class="c"># p-731042 Auto TODO linked-new ~/code/auto-todo running</span>
<span class="c"># p-845001 Auth System sandbox data/.../p-845001 shipped</span></pre>
<h3>5.3 UI · Dashboard 的「+ NEW PROJECT」</h3>
<div class="mock">
<div class="topbar">
<span class="dot" style="background:#FF5F57"></span>
<span class="dot" style="background:#FEBC2E"></span>
<span class="dot" style="background:#28C840"></span>
<span class="url">localhost:5173/devloop · "+ NEW PROJECT" clicked</span>
</div>
<svg viewBox="0 0 900 440" width="100%" role="img" aria-label="New project modal">
<defs>
<linearGradient id="bg-np" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#000" stop-opacity=".3"/><stop offset="100%" stop-color="#000" stop-opacity=".5"/></linearGradient>
</defs>
<rect width="900" height="440" fill="url(#bg-np)"/>
<rect x="100" y="20" width="700" height="400" rx="10" fill="#FAF7F1" stroke="#0E0E10"/>
<text x="130" y="58" font-family="Fraunces" font-size="22" fill="#0E0E10">新建项目</text>
<text x="130" y="80" font-family="JetBrains Mono" font-size="11" fill="#6E48C3">L3 · HUMAN · INIT</text>
<text x="130" y="110" font-family="JetBrains Mono" font-size="10" fill="#3B3B40">项目名</text>
<rect x="130" y="116" width="640" height="30" rx="4" fill="#fff" stroke="#dcd5c4"/>
<text x="142" y="135" font-family="JetBrains Mono" font-size="11" fill="#0E0E10">Auto TODO</text>
<text x="130" y="166" font-family="JetBrains Mono" font-size="10" fill="#3B3B40">创建模式</text>
<rect x="130" y="172" width="200" height="30" rx="4" fill="#0E0E10"/>
<text x="230" y="191" font-family="JetBrains Mono" font-size="11" text-anchor="middle" fill="#FAF7F1">● sandbox (默认)</text>
<rect x="340" y="172" width="200" height="30" rx="4" fill="#fff" stroke="#0E0E10"/>
<text x="440" y="191" font-family="JetBrains Mono" font-size="11" text-anchor="middle" fill="#0E0E10">○ linked-new</text>
<rect x="550" y="172" width="220" height="30" rx="4" fill="#EAEAEA" stroke="#999"/>
<text x="660" y="191" font-family="JetBrains Mono" font-size="11" text-anchor="middle" fill="#999">○ linked-existing (v2)</text>
<text x="130" y="222" font-family="JetBrains Mono" font-size="10" fill="#6c6c6c">target path: 自动分配到 data/.../projects/p-XXXXXX/</text>
<text x="130" y="256" font-family="JetBrains Mono" font-size="10" fill="#3B3B40">需求描述</text>
<rect x="130" y="262" width="640" height="100" rx="4" fill="#fff" stroke="#dcd5c4"/>
<text x="142" y="282" font-family="JetBrains Mono" font-size="11" fill="#0E0E10">做一个 TODO API + 简单 Vue 前端</text>
<text x="142" y="300" font-family="JetBrains Mono" font-size="11" fill="#0E0E10">能新增、列出。</text>
<text x="130" y="380" font-family="JetBrains Mono" font-size="10" fill="#6c6c6c">25 / 4000 字 · Ctrl+Enter 提交</text>
<rect x="560" y="376" width="100" height="36" rx="18" fill="#fff" stroke="#0E0E10"/>
<text x="610" y="400" text-anchor="middle" font-family="JetBrains Mono" font-size="12">取消</text>
<rect x="670" y="376" width="100" height="36" rx="18" fill="#6E48C3"/>
<text x="720" y="400" text-anchor="middle" font-family="JetBrains Mono" font-size="12" fill="#fff">创建项目</text>
</svg>
</div>
<p>选 linked-new 时弹出 file picker 让用户选目录(或手填路径)。选 sandbox 时不显示 path 输入。</p>
</section>
<section>
<h2>⑥ 创建流程 · 三模式各自 step-by-step</h2>
<div class="tabs" data-tabs>
<div class="tabbar" role="tablist">
<button role="tab" aria-selected="true" aria-controls="m-sb">sandbox</button>
<button role="tab" aria-selected="false" aria-controls="m-ln">linked-new</button>
<button role="tab" aria-selected="false" aria-controls="m-le">linked-existing (v2)</button>
</div>
<div class="tabpanel" id="m-sb">
<pre class="code shape"><span class="c">// LoopController.createProject({ company_id, name, requirement })</span>
step 1 · 生成 pid = <span class="s">"p-"</span> + randomBase36(<span class="v">6</span>)
step 2 · root_path = <span class="s">`</span><span class="v">${dataDir}</span><span class="s">/companies/</span><span class="v">${company_id}</span><span class="s">/projects/</span><span class="v">${pid}</span><span class="s">/`</span>
step 3 · fs.mkdirSync(root_path, <span class="p">{</span> recursive: <span class="v">true</span> <span class="p">}</span>)
step 4 · 写 project.json (含 root_path · mode='sandbox')
step 5 · GitVault.init(pid) → 在 root_path 起 .git/
step 6 · 写 .gitignore 模板 → 首个 commit <span class="s">"init: project </span><span class="v">${pid}</span><span class="s"> · </span><span class="v">${name}</span><span class="s">"</span>
step 7 · pathResolver 不需要 register (按 data/ 路径约定可扫到)
step 8 · LoopController.onHumanFeedback(pid, <span class="p">{</span> text: requirement <span class="p">}</span>)
→ PM Agent 异步启动
<span class="c">// 全程在 server 进程内,无文件系统外部依赖</span></pre>
</div>
<div class="tabpanel" id="m-ln" hidden>
<pre class="code shape"><span class="c">// 同上,但 root_path 来自用户</span>
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, <span class="p">{</span> recursive: <span class="v">false</span> <span class="p">}</span>) <span class="c">// 必须显式空</span>
step 5 · 写 project.json
step 6 · GitVault.init(pid) → git init 在用户路径
step 7 · 写 .gitignore 模板 → 首个 commit
step 8 · pathResolver.registerLinkedProject(<span class="p">{</span> pid, root_path, mode <span class="p">}</span>)
→ 写 ~/.devloop/linked-projects.json
step 9 · onHumanFeedback → PM 启动
<span class="c">// 与 sandbox 区别仅在 root_path 来源和 register 步骤</span></pre>
</div>
<div class="tabpanel" id="m-le" hidden>
<pre class="code shape"><span class="c">// v2 (v1 拒绝接受 linked-existing)</span>
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/<span class="v">${pid}</span>
step 9 · 后续 commit 都在该 branch
step 10 · shipped 时不自动合 main,等人 review + merge
<span class="c">// 风险点:用户其他工具(IDE/lint) 同时改文件 → 冲突</span>
<span class="c"># v2 才做,因为对边界 case 处理复杂</span></pre>
</div>
</div>
</section>
<section>
<h2>⑦ 各组件如何引用 root_path</h2>
<p>关键改造:<b>没有任何地方</b>硬编码 <code>data/companies/...</code>。一切走 pathResolver。</p>
<table class="dense">
<thead><tr><th>原来(硬编码)</th><th>改成(path-resolver)</th></tr></thead>
<tbody>
<tr><td><code>path.join(dataDir, 'companies', cid, 'projects', pid)</code></td><td><code>pathResolver.resolveProjectRoot(pid)</code></td></tr>
<tr><td><code>projectsDir(pid)</code> helper</td><td>delegate 到 pathResolver</td></tr>
<tr><td>Agent ctx.projectDir 注入</td><td>由 callAgentWithPersistence 用 pathResolver 计算</td></tr>
<tr><td>data-layout.html 的目录示意</td><td>明确写 <code>{project_root}/</code> 而非 <code>data/companies/.../projects/{pid}/</code></td></tr>
<tr><td>GitVault 内部 cwd</td><td>每次操作前 <code>pathResolver.resolveProjectRoot(pid)</code></td></tr>
<tr><td>Bash 工具的沙箱 cwd 锁</td><td>同上</td></tr>
</tbody>
</table>
<p class="sig">这是 P0 / P1 阶段必须的<b>抽象化重构</b>,否则 linked 模式没法实现。代价不大(~30 行改动散在 5-6 处)。</p>
</section>
<section>
<h2>⑧ memory · 始终在 company 级</h2>
<p>注意:<b>memory 不跟 project root 移动</b>。memory 是 company 范围长期记忆,所以它永远在:</p>
<pre class="tree">
{server_data_dir}/companies/{cid}/memory/devloop/
├── pm/*.md
├── architect/*.md
└── ...
</pre>
<p>哪怕 project 在 ~/code/auto-todo 下,memory 仍在 server 的 data/ 内。这样 linked 项目也能享受跨项目记忆,且 memory 不会跟着 git push 出去(隐私)。</p>
</section>
<section>
<h2>⑨ 跨主机 / 跨机器场景</h2>
<p>linked-new 模式启用后,<b>项目根可以跨机器</b>:</p>
<ul>
<li>主机 A 上 DevLoop 创建项目,root = <code>~/code/x</code></li>
<li><code>git push origin main</code> 推到 GitHub</li>
<li>主机 B <code>git clone</code> 到任意位置</li>
<li>主机 B 上重启 DevLoop server,<b>不会识别这个项目</b>(pathResolver 索引在 A 上)</li>
<li>主机 B 用 <code>node scripts/devloop-import.js --path ~/work/x</code> 重新注册(NEW,v2 工具)</li>
</ul>
<p class="sig">memory 不跨机器(属 company 数据);timeline / bugs / changelog 跟着 git 走(tracked);agent-state 不跟(.gitignore)— 这意味着<b>对端续跑不能恢复 paused agents</b>,要从干净状态重启。可接受。</p>
</section>
<section>
<h2>⑩ 创建后 LoopController 接管的统一接口</h2>
<p>不管哪种模式,创建后 LoopController 的视角<b>无差异</b>:</p>
<pre class="code shape"><span class="c">// 任何 mode 都走这个接口</span>
loopController.<span class="n">onHumanFeedback</span>(pid, <span class="p">{</span> text, origin <span class="p">}</span>);
<span class="c">// LoopController 内部:</span>
<span class="c">// 1. pathResolver.resolveProjectRoot(pid) → 拿到根路径(不关心是 sandbox 还是 linked)</span>
<span class="c">// 2. 注入 ctx 给 Agent: ctx.projectDir = root</span>
<span class="c">// 3. Agent 通过 Write 工具落盘,沙箱白名单基于 root + 子路径(写 prd/** 等)</span>
<span class="c">// 4. GitVault.commit({ pid, ... }) → 自动在 root 上 git 操作</span></pre>
<p>这就是<b>路径透明</b>的好处:业务代码无 mode 判断分支。</p>
</section>
<section>
<h2>⑪ project.json 完整示例</h2>
<pre class="code"><span class="c">// sandbox 项目</span>
<span class="p">{</span>
<span class="n">"id"</span>: <span class="s">"p-731042"</span>,
<span class="n">"company_id"</span>: <span class="s">"c-lemonv"</span>,
<span class="n">"name"</span>: <span class="s">"Auto TODO"</span>,
<span class="n">"mode"</span>: <span class="s">"sandbox"</span>,
<span class="n">"root_path"</span>: <span class="s">"/Users/yibo/Documents/lemonAI/lemonboss/data/companies/c-lemonv/projects/p-731042"</span>,
<span class="n">"status"</span>: <span class="s">"running"</span>,
<span class="n">"created_at"</span>: <span class="s">"2026-05-13T10:31:00Z"</span>,
<span class="n">"updated_at"</span>: <span class="s">"2026-05-13T11:02:00Z"</span>,
<span class="n">"owner"</span>: <span class="s">"yibo"</span>,
<span class="n">"token_budget"</span>: <span class="v">5000000</span>, <span class="n">"token_used"</span>: <span class="v">42018</span>,
<span class="n">"compressOldRuns"</span>: <span class="v">false</span>
<span class="p">}</span>
<span class="c">// linked-new 项目</span>
<span class="p">{</span>
<span class="n">"id"</span>: <span class="s">"p-845001"</span>,
<span class="n">"company_id"</span>: <span class="s">"c-lemonv"</span>,
<span class="n">"name"</span>: <span class="s">"Auto Auth System"</span>,
<span class="n">"mode"</span>: <span class="s">"linked-new"</span>,
<span class="n">"root_path"</span>: <span class="s">"/Users/yibo/code/auto-auth"</span>, <span class="c">// 用户选的</span>
<span class="n">"status"</span>: <span class="s">"running"</span>,
...
<span class="p">}</span></pre>
</section>
<section>
<h2>⑫ 路径白名单(沙箱安全)</h2>
<p>linked 模式给了用户灵活性,但也带来风险。沙箱必须严格:</p>
<table class="dense">
<thead><tr><th>规则</th><th>实现</th></tr></thead>
<tbody>
<tr><td>拒绝 root_path = 系统敏感路径</td><td>黑名单:/, /etc, /System, /usr, /var, /Library, /Applications, $HOME/.ssh, $HOME/.aws, $HOME/Downloads(防误操作)</td></tr>
<tr><td>拒绝 root_path 不可写</td><td>fs.accessSync(W_OK) 检查</td></tr>
<tr><td>拒绝 root_path 已存在且非空(linked-new)</td><td>fs.readdirSync 检查</td></tr>
<tr><td>Agent 写白名单走 root 相对路径</td><td>writeAllowList: ["prd/**", "sdd/**", "units/**", ...] · resolve 后必须以 root_path 开头</td></tr>
<tr><td>跨项目读写</td><td>禁止:任何 ../{别的项目} 路径直接拒绝</td></tr>
<tr><td>memory 跨项目读</td><td>白名单例外:memory/devloop/ 在 company 级允许只读</td></tr>
</tbody>
</table>
</section>
<section>
<h2>⑬ 实战命令</h2>
<pre class="code"><span class="c"># 启动 server</span>
PORT=3010 node server/index.js
<span class="c"># 方式 A · REST sandbox</span>
echo <span class="s">'{"company_id":"c-lemonv","name":"todo-demo","requirement":"做一个 TODO API"}'</span> > /tmp/req.json
curl -X POST http://localhost:3010/api/devloop/projects -d @/tmp/req.json
<span class="c"># → { "project_id": "p-731042", "mode": "sandbox", "root_path": "/.../data/companies/c-lemonv/projects/p-731042" }</span>
<span class="c"># 方式 B · REST linked-new</span>
echo <span class="s">'{"company_id":"c-lemonv","name":"Auto TODO","requirement":"做一个 TODO API","mode":"linked-new","target_path":"/Users/yibo/code/auto-todo"}'</span> > /tmp/req.json
curl -X POST http://localhost:3010/api/devloop/projects -d @/tmp/req.json
<span class="c"># → { "project_id": "p-845001", "mode": "linked-new", "root_path": "/Users/yibo/code/auto-todo" }</span>
<span class="c"># 在 ~/code/auto-todo 下立即看到 .git · .gitignore · project.json · prd/ 等开始生成</span>
<span class="c"># 方式 C · CLI</span>
node scripts/devloop-new.js \
--path ~/code/auto-todo \
--name <span class="s">"Auto TODO"</span> \
--requirement <span class="s">"做一个 TODO API"</span>
<span class="c"># 方式 D · UI (假设 frontend 启动)</span>
<span class="c"># 浏览器: http://localhost:5173/devloop · 点 "+ NEW PROJECT" · 填表 · 提交</span>
<span class="c"># 列出现有项目 (跨 sandbox + linked)</span>
node scripts/devloop-new.js --list
curl http://localhost:3010/api/devloop/projects
<span class="c"># → [{pid, name, mode, root_path, status}, ...]</span>
<span class="c"># 删除项目 (linked 模式不删用户目录,只 unregister + 移除 sandbox 索引)</span>
curl -X DELETE http://localhost:3010/api/devloop/projects/p-731042
<span class="c"># sandbox: 整 data/.../projects/p-XXXXXX/ 删除</span>
<span class="c"># linked: 只从 ~/.devloop/linked-projects.json unregister,用户目录保留</span></pre>
</section>
<section>
<h2>⑭ 工时增量</h2>
<table class="dense">
<thead><tr><th>任务</th><th>工时</th><th>归属阶段</th></tr></thead>
<tbody>
<tr><td>pathResolver 实现 + 写硬编码迁移</td><td>1.5 天</td><td>P0 (前置,后续 GitVault/工具都依赖)</td></tr>
<tr><td>project.json schema 加 mode + root_path</td><td>0.5 天</td><td>P0</td></tr>
<tr><td>POST /projects 加 mode + target_path 参数</td><td>0.5 天</td><td>P1</td></tr>
<tr><td>scripts/devloop-new.js CLI</td><td>1 天</td><td>P1</td></tr>
<tr><td>scripts/devloop-import.js (v2)</td><td>—</td><td>v2</td></tr>
<tr><td>UI 新建项目 modal</td><td>1 天</td><td>P4</td></tr>
<tr><td><b>合计 v1</b></td><td><b>4.5 天</b></td><td>分摊到 P0/P1/P4</td></tr>
</tbody>
</table>
<p class="sig">P0 时间预算从 13.5 天 → 15.5 天(+2 天 path-resolver + schema),P1 从 5 天 → 6.5 天(+1.5 天 mode 参数 + CLI)。总工时 10 周 → 10.5 周。</p>
</section>
<section>
<h2>⑮ 关联</h2>
<ul>
<li><a href="sdd-model.html">M-1 ProjectMeta</a> · 加 mode + root_path 字段</li>
<li><a href="sdd-control.html">C-1 REST.createProject</a> · 接受 mode + target_path</li>
<li><a href="arch-git-versioning.html">Git Versioning</a> · GitVault 用 root_path 当 cwd</li>
<li><a href="data-layout.html">data-layout</a> · 用 {project_root}/ 而非硬编码</li>
<li><a href="plan-code-lifecycle.html">代码归宿</a> · linked-new 让代码直接落到目标位置</li>
</ul>
</section>
</article>
<nav class="prev-next"></nav>
<footer class="site"><div><b>Section</b>05 · Architecture</div><div><b>Page</b>arch-project-root.html</div></footer>
</main>
</div>
<script src="assets/shell.js"></script>
</body>
</html>