Model · 数据结构全集
所有持久化数据结构。9 条核心定义,每条带 TypeBox schema(运行时校验)+ TS interface(类型推导)+ 落盘路径。Coder Agent 读这页就能写出所有数据访问代码。
M-1 · ProjectMeta
· data/companies/{cid}/projects/{pid}/project.json
// TypeBox schema
const ProjectMeta = Type.Object({
id: Type.String({ pattern: '^p-[a-z0-9]{6}$' }),
company_id: Type.String(),
name: Type.String(),
// 项目根 (见 arch-project-root)
mode: Type.Union([
Type.Literal('sandbox'), // 默认,根在 data/.../projects/{pid}/
Type.Literal('linked-new'), // 用户指定空目录
Type.Literal('linked-existing'), // v2:复用已有 git repo
]),
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'),
]),
created_at: Type.String({ format: 'date-time' }),
updated_at: Type.String({ format: 'date-time' }),
owner: Type.String(),
token_budget: Type.Number({ default: 5_000_000 }),
token_used: Type.Number({ default: 0 }),
compressOldRuns: Type.Boolean({ default: false }),
});
- id 全局唯一,跨 company 不重复
- status 只能按状态机合法转换(见 sdd-process.html · M-process-project)
- token_used > token_budget 时 LoopController 自动挂起项目
M-2 · units.json
· data/companies/{cid}/projects/{pid}/units.json
const Unit = Type.Object({
id: Type.String({ pattern: '^[a-z]+(-[a-z]+)*-(be|fe|mw|db)$' }),
type: Type.Union([Type.Literal('be'), Type.Literal('fe'), Type.Literal('mw'), Type.Literal('db')]),
status: Type.Union([
Type.Literal('pending'), Type.Literal('coding'),
Type.Literal('testing'), Type.Literal('failed'),
Type.Literal('passed'), Type.Literal('blocked'),
]),
depends_on: Type.Array(Type.String()),
retries: Type.Number({ default: 0 }),
retry_budget: Type.Number({ default: 5 }),
contract_hash: Type.String({ pattern: '^sha256:[a-f0-9]{16}$' }),
last_run: Type.Optional(Type.String()),
});
const UnitsJson = Type.Object({
project_id: Type.String(),
prd_rev: Type.String(),
sdd_rev: Type.String(),
units: Type.Array(Unit),
l2_rounds: Type.Number({ default: 0 }),
l2_budget: Type.Number({ default: 3 }),
});
- DAG 必须无环(DocStore 写入前用拓扑排序校验)
- depends_on 引用的 id 必须存在于本文件 units 中
- contract_hash = sha256(SDD 中对应 section 文本) 截前 16 位 - tests: 循环依赖 → 拒绝写入 - tests: 未知 id → 拒绝写入 - tests: 所有 status 为 passed 且 hash 与当前 SDD 匹配 → l2GateOpen 返回 true
M-3 · prd/ 多页 PRD 站点 · data/companies/{cid}/projects/{pid}/prd/ (目录) ```ts // 目录布局 (PM Agent 产出,见 agents-pm.html §2.1) prd/ ├── index.html // Hero + reading guide entry ├── reading-guide.html ├── assets/ │ ├── shell.css // 复制自模板 │ ├── shell.js // 3 级菜单 + 搜索 │ └── search-index.json ├── product/ │ ├── background.html │ ├── personas.html │ ├── original-req.html │ ├── scope.html │ ├── success-metrics.html │ └── glossary.html ├── feature/ │ └── feature-tree.html ├── flow/ │ └── user-flows.html └── prototype/ └── proto-{viewName}.html // 每个 chapter HTML 内的结构 (cheerio 校验):
...
... // 可选,index.html only
...
- 顶级必须是 `
`,每页都有
- 每个 section 必含 `
### ` + 至少一个 `
` - section data-rev 单调递增(页内),**article data-rev** 取该页内最大 - 每页至少覆盖 4/8 类 HTML 信息(见 agents-pm §2.2) - data-conflict aside 仅出现在 index.html 顶部,存在时整个项目暂停等人 - 11 个必备章节缺一个 → validate_prd_site 失败 - tests: init 模式后 11 章节全在 - tests: incremental 模式后仅 affected 章节被改 - tests: 多 rev 后 history/ 含旧版归档
## M-4 · sdd.html 结构 · `data/companies/{cid}/projects/{pid}/sdd.html` ```ts
<h3>Unit · auth-be <p>...功能描述...
POST /api/auth/login ...
- acceptance criteria 1
- 每个 unit section 必含 contract + acceptance
- data-depends 空格分隔 unit id 列表,可为空
- data-derived-from 指向产生它的 prd 版本
M-5 · contract.json(每 Unit)
· .../units/{unitId}/contract.json
const Contract = Type.Object({
unit_id: Type.String(),
version: Type.String({ pattern: '^sha256:' }),
exports: Type.Object({
http: Type.Optional(Type.Array(HttpEndpoint)),
ts_types: Type.Optional(Type.String()),
events: Type.Optional(Type.Array(EventSpec)),
}),
});
const HttpEndpoint = Type.Object({
method: Type.String(),
path: Type.String(),
request: Type.Optional(Type.Object({}, { additionalProperties: true })),
responses:Type.Object({}, { additionalProperties: true }),
});
完整规格见 api-contracts。
M-6 · integration/spec.json
· .../integration/spec.json
const IntegrationSpec = Type.Object({
version: Type.String(),
cases: Type.Array(Type.Object({
id: Type.String(),
steps: Type.Array(Type.Object({
unit: Type.String(),
call: Type.String(), // "POST /api/x"
body: Type.Optional(Type.Any()),
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
expect: Type.Object({
status: Type.Number(),
json: Type.Optional(Type.Any()),
}),
})),
involves: Type.Array(Type.String()),
})),
});
- involves 列表必须是 cases.steps.unit 的去重并集,BugTriager 用它做归因粗筛
- headers / body 中可用
{{stepN.field}}占位符引用前面步骤结果
M-7 · runs/result.json (L1)
· .../units/{unitId}/runs/{runId}/result.json
const RunResult = Type.Object({
run_id: Type.String({ pattern: '^run-\\d{8}-\\d{4}$' }),
loop: Type.Literal('L1'),
unit_id: Type.String(),
agent: Type.Union([Type.Literal('coder'), Type.Literal('tester')]),
attempt: Type.Number(),
ok: Type.Boolean(),
summary: Type.Object({
total: Type.Number(),
passed: Type.Number(),
failed: Type.Number(),
}),
failures: Type.Array(Type.Object({
test: Type.String(),
expected: Type.Any(),
got: Type.Any(),
stack: Type.String(),
hint: Type.String(),
})),
next_action: Type.Optional(Type.String()),
retry_budget_left: Type.Optional(Type.Number()),
duration_ms: Type.Number(),
});
M-8 · integration/runs/result.json (L2)
· data/companies/{cid}/projects/{pid}/integration/runs/{runId}/result.json
const IntResult = Type.Object({
run_id: Type.String({ pattern: '^run-int-\\d{8}-\\d{4}$' }),
ok: Type.Boolean(),
failures: Type.Array(Type.Object({
case: Type.String(),
failed_step: Type.Number(),
got: Type.Any(),
involves: Type.Array(Type.String()),
stderr_excerpt: Type.String(),
})),
process_log: Type.String(),
duration_ms: Type.Number(),
});
M-9 · triage.json
· .../integration/runs/{runId}/triage.json
const Triage = Type.Object({
run_id: Type.String(),
root_cause: Type.String({ minLength: 20 }),
category: Type.Union([
Type.Literal('contract_mismatch'),
Type.Literal('impl_bug'),
Type.Literal('spec_ambiguous'),
Type.Literal('infra_flake'),
]),
affected_units: Type.Array(Type.String()),
proposed_sdd_patch: Type.Union([Type.Object({
target_section: Type.String(),
op: Type.String(),
text: Type.String(),
}), Type.Null()]),
confidence: Type.Number({ minimum: 0, maximum: 1 }),
evidence: Type.Array(Type.String()),
});
- category = contract_mismatch → proposed_sdd_patch 必填
- category = impl_bug → proposed_sdd_patch 必为 null(不改 SDD,只 reset Unit)
- confidence
- 每条 evidence 必须在原文件中能 grep 到
M-10 · AgentRuntimeState · 持久化运行态
· data/companies/{cid}/projects/{pid}/agent-state/{agent}.json
const AgentRuntimeState = Type.Object({
agent: Type.Union([Type.Literal('pm'), Type.Literal('architect'),
Type.Literal('coder'), Type.Literal('tester'),
Type.Literal('integrator'), Type.Literal('bug-triager')]),
unit_id: Type.Optional(Type.String()), // Coder/Tester 时填
status: Type.Union([
Type.Literal('idle'), Type.Literal('running'),
Type.Literal('paused'), Type.Literal('done'),
Type.Literal('error')
]),
call_id: Type.String(), // 单次调用唯一 id
current_step: Type.Number(), // 0-based, 引用 todolist.items[i]
total_steps: Type.Number(),
last_input_ref: Type.String(), // 输入快照路径
last_output_ref: Type.Optional(Type.String()),
session_id: Type.Optional(Type.String()), // ACP session 引用
resume_token: Type.Optional(Type.String()), // 冷启恢复用
started_at: Type.String({ format: 'date-time' }),
updated_at: Type.String({ format: 'date-time' }),
error: Type.Optional(Type.Object({
code: Type.String(),
message: Type.String(),
stack: Type.Optional(Type.String())
})),
});
- 每次 LoopController.callAgent 前写一次(status=running)
- Agent 自己在跨越主要 checkpoint 时通过
update_runtime_state工具更新 current_step - 调用结束(成功或失败)由 LoopController 写终态 status=done|error
- Server SIGTERM 时所有 status=running 自动改 status=paused 并写 resume_token
Server 启动时扫所有 paused,发 SSE
resume_decision_needed等人决策tests: Agent 崩溃 → state.json status=error,error 字段有 code/message
tests: Server kill -9 → 重启后 state.json status=paused,resume_token 非空
tests: Dashboard 能从 state.json 渲染「Architect at step 3/5」
M-11 · AgentMemory · 跨项目长期记忆
· data/companies/{cid}/memory/devloop/{agent}/{topic}.md
// Markdown + frontmatter,人可读 + 机器可读
---
agent: architect
topic: project-conventions
project_refs: [p-731042, p-845001]
last_updated: 2026-05-13T10:31:00Z
relevance_score: 0.92 # 0-1, agent 自评
hits: 14 # 被注入 prompt 的次数
---
- 用户偏好 RESTful API + 完整 OpenAPI 注解
- Token 续期实测:5s 内人类可接受
- 认证方式:JWT 放 Authorization header,不用 cookie
- 数据库统一用 better-sqlite3,不要 mysql/postgres
- 每个 Agent 启动时扫
memory/devloop/{agent}/*.md - 按 relevance_score × recency 排序,取前 5 条注入 system prompt
- Agent 自己可通过
record_memory(topic, content, relevance)工具新增 - Dedup:同 agent + 同 topic 是 upsert(覆盖 + 累加 hits),不新建文件。文件名 =
{topic-slug}.md,topic-slug 由 record_memory 工具按规则计算(小写 · 空格转 -) - memory 跨项目(同 company 内)共享,不跨 company
- frontmatter 必须含 agent / topic / last_updated 三字段
- 单文件 ≤ 8KB,超出 record_memory 自动 truncate 旧内容(保留新加 + 最相关)
v1 注意:如果初版 token 紧张可以关 memory 注入(top-5 改 top-0),完全跳过。memory 在 ≥ 5 个项目后才开始有积累价值。
tests: 新 Agent 调用 → memory dir 存在则 system prompt 含 "## Memory" section
tests: 不存在该 agent dir → fallback 空 memory,不报错
tests: record_memory 写入后下次启动能读到
M-12 · AgentTodolist · 单次调用的子任务清单
· data/companies/{cid}/projects/{pid}/agent-state/{agent}-todo.json
const AgentTodolist = Type.Object({
agent: Type.String(),
call_id: Type.String(), // 与 AgentRuntimeState.call_id 关联
created_at: Type.String(),
items: Type.Array(Type.Object({
id: Type.String({ pattern: '^step-\\d{3}$' }),
description: Type.String(),
status: Type.Union([Type.Literal('todo'), Type.Literal('doing'),
Type.Literal('done'), Type.Literal('failed')]),
started_at: Type.Optional(Type.String()),
finished_at: Type.Optional(Type.String()),
output_ref: Type.Optional(Type.String()), // 产物路径
depends_on: Type.Optional(Type.Array(Type.String())),
error: Type.Optional(Type.String()),
})),
current: Type.String(), // 当前 item.id
progress_pct: Type.Number({ minimum:0, maximum:100 }),
});
// 典型 Architect generate 模式的 todolist 示例:
items: [
{ id:'step-001', description:'读 prd.html 并解析所有 section', status:'done' },
{ id:'step-002', description:'识别功能列表(共 5 个特性)', status:'done' },
{ id:'step-003', description:'拆分每个特性为 be/fe Unit', status:'doing' },
{ id:'step-004', description:'为每个 Unit 写 OpenAPI contract', status:'todo' },
{ id:'step-005', description:'生成 integration/spec.json', status:'todo' },
]
- Agent 在 system prompt 指引下,开始时必须先写完整 todolist(todo 状态)
- 每完成一项立即调
update_todo_item(id, status, output_ref)工具 - progress_pct = done 数 / total × 100
- Dashboard 从这里渲染「Architect: 3/5 拆分 OAuth 功能为 3 个 Unit」
- 失败 → status=failed,触发 retry 时 todolist 清空重写
M-13 · ProcessTimeline · 全流程统一日志
· data/companies/{cid}/projects/{pid}/timeline.ndjson
// NDJSON,append-only,一行一事件,每行 ≤ 1KB
const TimelineEvent = Type.Object({
ts: Type.Number(), // unix seconds
source: Type.Union([
Type.Literal('system'), // LoopController / DocStore / etc
Type.Literal('human'), // 人类显式动作
Type.Literal('pm-agent'),
Type.Literal('architect'),
Type.Literal('coder'),
Type.Literal('tester'),
Type.Literal('integrator'),
Type.Literal('bug-triager'),
]),
kind: Type.String(), // "project_created" / "prd_drafted" / etc
ref: Type.Optional(Type.String()), // 关联资源,如 "prd@rev3"
summary: Type.String({ maxLength: 280 }), // 人话摘要
payload: Type.Optional(Type.Object({}, { additionalProperties:true })),
});
// 示例 (一行 = 一事件):
{"ts":1747120731,"source":"human","kind":"project_created","summary":"yibo 创建项目'做个 TODO API'"}
{"ts":1747120745,"source":"pm-agent","kind":"prd_drafted","ref":"prd@rev1","summary":"PRD rev 1 完成"}
{"ts":1747120798,"source":"architect","kind":"sdd_generated","ref":"sdd@rev1","summary":"拆 3 个 Unit"}
{"ts":1747120821,"source":"coder","kind":"unit_coded","ref":"unit/todo-be","summary":"todo-be 实现完成"}
{"ts":1747120833,"source":"tester","kind":"unit_failed","ref":"unit/todo-be","summary":"invalid-pwd 期望 401 收 500"}
- Append-only,永不修改已有行
- 所有 state 转移、文档变更、Agent 调用、Human 动作都必须 append 一条
- 是 audit log 也是 Dashboard 时间线视图数据源
- 每条 ≤ 1KB;超长内容(如完整 stderr)放 payload.{x}_ref 引用
- 由
timelineLogger.append(pid, event)写入(C-15 工具)
与 stream.log(SSE 视角)的区别:timeline.ndjson 是事件视角,stream.log 是客户端视角。前者永远存,后者 7 天滚动。
M-14 · bugs.ndjson · 跨 run 跨 unit Bug 聚合
· data/companies/{cid}/projects/{pid}/bugs.ndjson
// NDJSON,append + 状态机更新
const Bug = Type.Object({
id: Type.String({ pattern: '^bug-\\d{8}-\\d{4}-[a-z0-9]{4}$' }),
discovered_at:Type.String({ format: 'date-time' }),
source: Type.Union([Type.Literal('unit_test'),
Type.Literal('integration_test'),
Type.Literal('triage'),
Type.Literal('human')]),
unit_id: Type.Optional(Type.String()),
run_id: Type.Optional(Type.String()), // runs/{runId} 或 integration/runs/{runId}
test_name: Type.Optional(Type.String()),
category: Type.Union([Type.Literal('contract_mismatch'),
Type.Literal('impl_bug'),
Type.Literal('spec_ambiguous'),
Type.Literal('infra_flake'),
Type.Literal('unknown')]),
severity: Type.Union([Type.Literal('critical'), Type.Literal('high'),
Type.Literal('medium'), Type.Literal('low')]),
title: Type.String({ maxLength: 120 }),
detail: Type.String(), // 完整 stack/expected/got
affected_units: Type.Array(Type.String()),
status: Type.Union([Type.Literal('open'),
Type.Literal('fixing'),
Type.Literal('fixed'),
Type.Literal('verified'),
Type.Literal('wontfix'),
Type.Literal('duplicate')]),
status_history: Type.Array(Type.Object({
ts: Type.String(),
from: Type.String(), to: Type.String(),
by: Type.String(), // agent/system/human id
note: Type.Optional(Type.String()),
})),
fix_refs: Type.Array(Type.String()), // 关联的 run_id / triage / commit
duplicate_of: Type.Optional(Type.String()), // 另一个 bug id
});
// 示例行:
{"id":"bug-20260513-0731-7f3a","source":"unit_test","unit_id":"auth-be","run_id":"run-..0731",
"category":"impl_bug","severity":"high","status":"fixed",
"title":"login.invalid-pwd 期望 401 收 500","affected_units":["auth-be"],"fix_refs":["run-..0734"]}
- 每次 Tester / Integrator / BugTriager 失败 → 自动 append 1 条 bug,status=open
- 同一 unit 同一 test 连续两轮失败 → 视同同一 bug(合并),不重复 append
- status 转换走 P-9 状态机,每次转换 append 一条 status_history
- append-only:bug 内容修改通过新写一行 status=... 的更新事件实现(与 timeline 同理)
读取时取每个 bug.id 的最新一行作为当前状态
tests: 单测红 → bugs.ndjson 新增 status=open 行
tests: 下一轮 Coder 修复 → 新增 status=fixed 行 with fix_refs=[新 run_id]
tests: L2 通过 → 该 bug 状态最终 verified
M-15 · changelog.ndjson · 版本变更流
· data/companies/{cid}/projects/{pid}/changelog.ndjson
const ChangelogEntry = Type.Object({
ts: Type.String({ format: 'date-time' }),
kind: Type.Union([
Type.Literal('prd_rev'),
Type.Literal('sdd_rev'),
Type.Literal('units_rev'),
Type.Literal('integration_spec_rev'),
]),
rev: Type.Number(),
by: Type.String(), // agent or human id
trigger: Type.String(), // "human_feedback" | "triage_patch" | "init"
summary: Type.String({ maxLength: 280 }),
diff: Type.Object({ // 影响面快照
sections_added: Type.Array(Type.String()),
sections_changed: Type.Array(Type.String()),
sections_removed: Type.Array(Type.String()),
units_added: Type.Array(Type.String()),
units_changed: Type.Array(Type.String()),
units_removed: Type.Array(Type.String()),
}),
archive_ref: Type.String(), // history/prd-rev-{N}.html 路径
});
// 示例:
{"kind":"prd_rev","rev":3,"by":"pm-agent","trigger":"human_feedback",
"summary":"+ OAuth 登录,影响 auth-mw 新增","diff":{"sections_added":["#oauth"],...}}
{"kind":"sdd_rev","rev":5,"by":"architect","trigger":"prd@rev3",
"summary":"3 个新 Unit: oauth-be / oauth-fe / auth-mw",...}
{"kind":"sdd_rev","rev":6,"by":"architect","trigger":"triage_patch",
"summary":"L2 修复:auth-be 401 明确",...}
- 每次 DocStore.writePRD/writeSDD/writeUnits 成功后必 append 一行
- diff 字段在 PM/Architect Agent 输出后由 DocStore 自动计算(cheerio + 拓扑 diff)
- archive_ref 指向归档文件,可被 Dashboard 直接打开
- 这是 history/ 目录的线性视图:history/ 是物,changelog 是事
归档命名约定(统一目录形式)
| 原产物 | 归档路径 | 形态 |
|---|---|---|
| prd/ 整站 | history/prd-rev-{N}/ |
目录(含整 prd/ 拷贝) |
| sdd/ 整站 | history/sdd-rev-{N}/ |
目录 |
| units.json | history/units-rev-{N}.json |
单文件 |
| integration/spec.json | history/integration-spec-rev-{N}.json |
单文件 |
| removed unit dir | history/removed/{unitId}-{ts}/ |
目录 + 时间戳防重 |
| agent-state snapshots | `agent-state/snapshots/{call_id}-(in | out).json` |
PRD/SDD 现在都是多页站点,归档单元是整个子目录而非单 html。所有 Agent 用 archivedoc 工具,传 kind 即可('prd'/'sdd'/'units'),路径由 DocStore 计算。_
无 SQL · 无新表
整个 DevLoop 系统不引入新 SQLite 表。所有持久化通过文件系统完成。LemonBoss 既有的 data/users.sqlite 等不动。
这是有意为之的简化 — 让外部 Claude Code 复现项目时不用先想 schema migration。