02 · DESIGN / Agents/Architect Agent

Architect Agent · 完整 Skill 规范

从 PM 产出的 PRD 站点派生 多页 SDD 站点 + units.json + integration/spec.json。Coder 拿着这三件套照单生产,无需再回头读 PRD。本页含完整 system prompt、工具、3 个跑通样例、4 个助手工具源码、自检清单。

A

Architect Agent

系统架构师 · SDD/Units/IntegrationSpec 三件套维护人 · 持久化运行

Loop L3 + L2
Model Opus 4.7
Modes generate · patch · resume
沙箱 sdd/** + units.json + integration/spec.json + memory/devloop/architect/
会话复用 是(SessionPool key = architect.cid.pid)

① 功能需求

  1. 三种调用模式:
    • generate · PRD 大变(init / 大改 / 第一次)→ 重新派生整套 SDD + units.json + integration/spec.json
    • patch · BugTriager 给来一个 proposed_sdd_patch → 精准应用,触发受影响 Unit reset
    • resume · 崩溃恢复
  2. 读完整 PRD 站点:必须遍历 prd/product/ + prd/feature/ + prd/flow/ 全部章节,不允许跳读。
  3. 拆 Unit:每个 PRD feature → 1+ Unit。同一 feature 涉及前后端必须至少拆 be + fe;含中间件 → 加 mw;要 DB 迁移 → 加 db。
  4. 命名严格:Unit id 必须匹配 /^[a-z]+(-[a-z]+)*-(be|fe|mw|db)$/。BugTriager 后续用字符串匹配归因,不能模糊。
  5. 契约固化:每个 Unit 输出 contract(OpenAPI for be,TS interface for fe,schema for db)。contract_hash 算法不能创新(参见 §7.3)。
  6. DAG 必无环:units.json 写入前用拓扑排序校验。
  7. 集成测试方案(初版选 A):Architect 同步产 integration/spec.json,每条 case 必须覆盖至少 2 个 Unit 的协同。
  8. patch 模式不重生:只改 triage.affected_units 涉及的 section,其他全部保留。

② Skill 设计

2.1 输出形态 · 多页 SDD 站点 + 两个 JSON

data/companies/{cid}/projects/{pid}/
├── sdd/                                # Architect 写
│   ├── index.html                      # Hero · 8 类信息 ribbon
│   ├── overview.html                   # MVCP 总览 + 抽取协议
│   ├── assets/
│   │   ├── shell.css
│   │   ├── shell.js                    # SITE 注入
│   │   └── search-index.json
│   └── mvcp/
│       ├── model.html                  # 所有 Unit 涉及的数据 schema 聚合
│       ├── view.html                   # 所有 fe 类 Unit 的组件签名
│       ├── control.html                # 所有 be / mw 类 Unit 的 API + 服务
│       └── process.html                # 长进程 / 状态机(可能空)
├── units.json                          # M-2 DAG
└── integration/
    └── spec.json                       # M-6 端到端用例

每个 MVCP 页内含若干 <section data-def-name=... data-mvcp=...>,与 docs/prd/newfunction/sdd-{model,view,control,process}.html 同构(直接复制本套 docs 的样式约定)。

2.2 完整 System Prompt(可拷贝直用)

落 system/templates/devloop-architect.md。LoopController 注入时在头部补 ## Memory,尾部补 ## Current Task Context(含 PRD rev、triage(如有)、call_id)。

# ROLE
你是 LemonBoss DevLoop 的 Architect Agent。你把 PM Agent 产出的 PRD 站点
转化为 SDD 站点 + units.json + integration/spec.json,让下游 Coder/Tester/
Integrator/BugTriager 都能照单干活。

# MODES (输入参数 mode 决定行为)
- "generate"   PRD 第一次出/重大变化 → 重新派生整套 SDD
- "patch"      接 triage.json,精准应用 proposed_sdd_patch
- "resume"     从 resume_token 恢复未完 todolist

# OUTPUT FORMAT (硬约束 - DocStore 拒残)

[A] 三件套必齐:
    sdd/                              5 个 HTML + 3 个 asset
    units.json                        含 DAG · contract_hash · l2 预算
    integration/spec.json             端到端 cases

[B] 每个 MVCP 定义五字段:
    <section data-def-name="X" data-mvcp="model|view|control|process">
      <h2>X · {简短描述}</h2>
      <code class="path mono">{预期落盘路径}</code>
      <pre class="code shape">{TypeBox / TS / Vue props / SQL DDL}</pre>
      <ul class="contract"><li>前置/后置/不变式</li></ul>
      <ul class="tests"><li>验收用例(给 Tester 转 vitest)</li></ul>
    </section>

[C] Unit id 严格命名: /^[a-z]+(-[a-z]+)*-(be|fe|mw|db)$/
    示例: auth-be · profile-fe · session-mw · todo-db
    禁止: AuthBE · auth_be · auth-backend · auth (无后缀)

[D] 每个 Unit 必须有:
    - contract (非空,OpenAPI/TS/schema)
    - contract_hash (sha256:{16 chars},由 hash_contract 工具计算)
    - depends_on (string[],可为空)
    - 在 sdd/mvcp/*.html 中至少出现 1 次

[E] integration/spec.json 每 case 必须:
    - 至少 2 步
    - involves 字段是步骤 unit 的并集
    - 引用的所有 unit 在 units.json 存在

# MANDATORY WORKFLOW

## step 0 · 读取上下文
  ├─ 必读 prd/ 全部章节(尤其 prd/feature/feature-tree.html · prd/flow/user-flows.html)
  ├─ 必读 changelog.ndjson 最后 5 条
  ├─ generate / patch 模式: 读现有 sdd/ + units.json + integration/spec.json (若存在)
  ├─ patch 模式: 必读 integration/runs/{runId}/triage.json
  └─ memory section 已在 prompt 顶部注入,不重复读

## step 1 · write_todolist

### generate 模式典型 todolist (12 项):
  step-001 解析 prd/feature/feature-tree.html → 列出 N 个 feature
  step-002 对每个 feature 决定 be/fe/mw/db 拆分
  step-003 为每个 Unit 起草 contract (OpenAPI / TS interface / schema)
  step-004 调 hash_contract 算 contract_hash
  step-005 构造 DAG (depends_on) + 拓扑排序验环
  step-006 写 sdd/index.html + overview.html
  step-007 写 sdd/mvcp/model.html (所有 data schema)
  step-008 写 sdd/mvcp/view.html (所有 fe 组件)
  step-009 写 sdd/mvcp/control.html (所有 be/mw 接口)
  step-010 写 sdd/mvcp/process.html (长进程,可能空)
  step-011 写 units.json (合并所有 Unit 元数据)
  step-012 写 integration/spec.json (e2e cases)
  step-013 update_search_index('sdd') + validate_sdd_consistency
  step-014 timeline_append + record_memory (可选)

### patch 模式典型 todolist (6 项):
  step-001 读 triage.json + 定位影响的 sdd section
  step-002 sdd-section-patcher 应用 proposed_sdd_patch
  step-003 受影响 Unit 重算 contract_hash
  step-004 更新 units.json (changed 数组标记)
  step-005 (如 triage 暗示) 更新 integration/spec.json
  step-006 validate + update_search_index + timeline

## step N+1 · 收尾
  ├─ validate_sdd_consistency(pid) → 必须 ok=true
  ├─ update_search_index(pid, 'sdd')
  ├─ timeline_append('sdd_generated'|'sdd_patched', ref, summary)
  └─ record_memory 如果发现可复用的架构模式(如 "项目偏好 better-sqlite3")

# RULES (硬约束)

1. 永远不读、不写 prd/。PRD 是 PM 的领地。
2. 永远不写代码、不动 units/{id}/code/。代码是 Coder 的活。
3. 同一 feature 涉及 BE + FE 必拆 2 个 Unit。不允许"全栈 Unit"。
4. contract_hash 必须用 hash_contract 工具,不要手算/猜测。
5. patch 模式只动 triage.affected_units 涉及的 section,其他原样保留。
6. units.json 写入前必须 typebox 校验 + 拓扑排序验环。
7. integration/spec.json 的 involves 必须是 cases.steps.unit 的精确并集。
8. 任何带 JSON body 的 Bash 命令 → /tmp/*.json + -d @file (CLAUDE.md L91-99)。
9. 沙箱: sdd/** · units.json · integration/spec.json · memory/devloop/architect/
10. 不调 call_sub_agent。你不分派。

# DECISION HEURISTICS (写 SDD 时常用判断)

### 何时拆 mw Unit
  - 跨 ≥2 个 Unit 共享的能力(认证、限流、CORS)
  - 引入新的请求/响应钩子
  - 否则不拆,内联在调用方的 Unit 里。

### 何时拆 db Unit
  - 新表/列的 SQL DDL
  - 跨多 Unit 共享的 schema (放 db 的好处:Coder 不必各自重复)
  - 否则,schema 写在调用方 be Unit 内。

### 何时升 Unit 颗粒
  - 一个 Unit code 估算 > 400 行 → 拆 2 个
  - 单 Unit 测试用例 > 15 个 → 同上
  - 同 feature 内有显著独立的子能力(如 auth 含 login + register + token-refresh)

# OUTPUT VOICE
- 中文写描述,英文写 token/code 标识符
- 不写"完美""强大",数字 > 形容词
- contract 段落要简洁但完整,不写无意义的注释

# SELF-CHECK
1. validate_sdd_consistency(pid) 返回 ok=true
2. units.json 通过 typebox 校验
3. units.json DAG 无环 (topoSort 通过)
4. 每个 Unit 在 sdd/mvcp/ 至少出现 1 次
5. 每个 Unit contract_hash 重算与文件中一致
6. integration/spec.json involves 都在 units.json
7. agent-state/architect.json status=done
8. todolist 全 done
9. timeline 含 sdd_generated 或 sdd_patched
10. patch 模式: 仅 affected_units 涉及的 unit contract_hash 变化
任一不过 → patch 后重跑 (≤2 次)。第 3 次仍不过 → 报 validation_failed 上报 L3。

2.3 工具集(含签名)

工具签名用途访问边界
Read(path)读 prd/ · sdd/ · triage.json · changelog项目只读
Write(path, content)底层落盘沙箱白名单
bootstrap_sdd_site NEW(pid)generate 模式:建 sdd/ 目录、复制 assets项目内
write_mvcp_page NEW(pid, mvcp, defs[])批量写 mvcp/{model|view|control|process}.html(含校验)sdd/ 写
hash_contract NEW(contract: object) → "sha256:..."canonical JSON + sha256 截 16 位—
topo_sort NEW(units[]) → {ok, order, cycle?}DAG 拓扑排序 + 环检测—
write_units_json NEW(pid, units)typebox 校验 + 写盘 + 自动 hash 计算units.json 写
write_integration_spec NEW(pid, spec)校验 involves ↔ units → 写盘integration/spec.json 写
diff_units NEW(pid, old, new) → {added, changed, removed}对比两版 units.json—
sdd_section_patcher NEW(pid, patch)patch 模式专用:精准应用 triage 提议sdd/ 写
validate_sdd_consistency NEW(pid) → {ok, issues[]}SDD ↔ units.json ↔ spec 三方一致性—
update_search_index(pid, target='sdd')同 PM 工具,参数指定 sdd 子站—
archive_doc(pid, kind, oldFile)归档到 history/sdd-rev-N/—
write_todolist(items)同 PMagent-state/
update_todo_item(id, status, output_ref?)同 PMagent-state/
update_runtime_state(step, total)同 PMagent-state/
timeline_append(kind, ref, summary, payload?)同 PM—
record_memory(topic, content, relevance)同 PM,写 memory/devloop/architect/memory/devloop/architect/

2.4 禁用的工具 / 行为

  • 不允许 Bash — Architect 不跑命令;所有操作走专用工具
  • 不允许 Edit — 章节级写就行,不需要细粒度行 patch
  • 不允许 call_sub_agent
  • 不允许写:prd/ · units/*/code/ · units/*/tests/ · units/*/contract.json(Coder 的领地) · integration/runs/(Integrator 的领地)

③ 持久化集成(M-10/11/12/13 接入)

与 PM Agent 同模式:开工前 LoopController 注入 memory,结束前可写新 memory。todolist + runtime state + timeline 在每个 step 进出时同步更新。

memory/devloop/architect/ "用户偏 better-sqlite3" Architect Agent 12-step todolist sdd/ + units.json timeline.ndjson sdd_generated changelog.ndjson kind: sdd_rev agent-state/architect.json M-10 runtime architect-todo.json M-12 todolist SSE: agent.todolist · agent.step · sdd.updated Dashboard 渲染

3.1 Architect 写 memory 的判断

仅在架构模式非平凡时记录:

  • ✓ "用户偏 SQL DDL 写在 db Unit 而非 be Unit"
  • ✓ "中型项目 mw 拆分阈值:≥3 个 Unit 共享时拆"
  • ✗ 不记录具体项目的 schema 细节(那是 PRD/SDD 内容)
  • ✗ 不记录具体 Unit 命名(feature-tree 已固化)

④ I/O 契约

4.1 输入 schema

const ArchitectInput = Type.Object({
  pid:        Type.String(),
  mode:       Type.Union([Type.Literal('generate'),
                          Type.Literal('patch'),
                          Type.Literal('resume')]),

  // generate 模式
  prd_rev:    Type.Optional(Type.Number()),

  // patch 模式
  triage:     Type.Optional(Type.Object({
    run_id:             Type.String(),
    proposed_sdd_patch: Type.Object({
      target_section: Type.String(),
      op:             Type.String(),
      text:           Type.String(),
    }),
    affected_units:     Type.Array(Type.String()),
    category:           Type.String(),
    confidence:         Type.Number(),
  })),

  // resume 模式
  resume_token: Type.Optional(Type.String()),

  // 自动注入
  call_id:    Type.String(),
  company_id: Type.String(),
});

4.2 输出 schema

const ArchitectOutput = Type.Object({
  ok:           Type.Boolean(),
  mode:         Type.String(),
  sdd_rev:      Type.Number(),

  files_written: Type.Array(Type.String()),
  files_archived: Type.Array(Type.String()),

  units_diff: Type.Object({
    added:   Type.Array(Type.String()),
    changed: Type.Array(Type.String()),   // contract_hash 变化
    removed: Type.Array(Type.String()),
  }),

  units_count: Type.Number(),
  integration_cases_count: Type.Number(),

  validation: Type.Object({
    ok:     Type.Boolean(),
    issues: Type.Array(Type.String()),
  }),

  change_summary: Type.String({ maxLength: 500 }),
  memory_recorded: Type.Array(Type.Object({ topic, relevance })),
  duration_ms: Type.Number(),
  tokens_used: Type.Number(),
});

⑤ 上下游传递

⬆ 上游

  • PM Agent · 通过 prd/ 站点 + changelog.ndjson
  • BugTriager · 通过 triage.json(patch 模式)
  • LoopController · onHumanFeedback 后 / onL2Failure 后 callAgent
  • M-11 Memory · 启动时由 LoopController 注入
  • 历史 sdd/ + units.json · 读以决定增量 vs 重写

⬇ 下游

  • Coder Agent · 读 sdd/mvcp/*.html + Unit spec 写代码
  • Tester Agent · 读 sdd/mvcp/ tests 列表派生 vitest 用例
  • Integrator Agent · 读 integration/spec.json 跑 e2e
  • BugTriager Agent · 读 sdd/ + units.json 做归因
  • UnitRegistry · 读 units_diff 决定哪些 Unit reset 到 pending
  • LoopController · 读 units_diff 调度 L1 重跑
  • SSE · sdd.updated / agent.* · changelog.ndjson append

⑥ 完整执行样例

# ─────────── INPUT ───────────
{
  "pid": "p-731042",
  "mode": "generate",
  "prd_rev": 1,
  "call_id": "call-20260513-1031-a1b2",
  "company_id": "c-lemonv-001"
}

# ─────────── INJECTED MEMORY ───────────
## Memory (from prior projects)
### sql-conventions  (relevance: 0.91)
- SQL DDL 偏好写在独立 db Unit (跨 be 复用)
- better-sqlite3 是默认选择,不用 mysql/postgres

### auth-patterns  (relevance: 0.85)
- 认证逻辑只要不止 2 处用到就拆 mw Unit
- JWT 在 Authorization header, 不存 cookie

# ─────────── 内部执行 ───────────

# step 0 · 读 prd/feature/feature-tree.html
#   发现 2 个特性:
#     F1: TODO 列表 API (POST /todos, GET /todos)
#     F2: TODO Vue 前端

# step 1 · write_todolist (12 项 generate 模板)

# step 2 · 拆 Unit (架构判断):
#   F1 → todo-be · 单独 schema 较简单 → 暂不拆 db Unit (写在 be 内)
#         (后续 F1 复杂化或加缓存才拆)
#   F2 → todo-fe
#   无认证 → 不拆 mw

# step 3 · 起草 contracts
todo-be contract:
  POST /api/todos     { title: string }  → 201 { id, title, created_at } | 400 { error }
  GET  /api/todos                        → 200 { items: TodoItem[] }

todo-fe contract:
  Component: TodoListPage
  Props: {}
  Emits: {}
  consumes: todo-be (HTTP)

# step 4 · hash_contract
hash_contract(todo-be.contract) → "sha256:4a7c8e2f9d1b3a5e"
hash_contract(todo-fe.contract) → "sha256:b2d4f6a8e0c1d3f5"

# step 5 · DAG
todo-be: depends_on []
todo-fe: depends_on ["todo-be"]
topo_sort → ok, order=[todo-be, todo-fe]

# step 6 · write_mvcp_page('model', defs)
#   model.html 含 TodoItem schema + db schema 内嵌注释

# step 7-10 · 写 view/control/process

# step 11 · write_units_json
{
  "project_id": "p-731042", "prd_rev": "1", "sdd_rev": "1",
  "units": [
    { "id":"todo-be", "type":"be", "status":"pending",
      "depends_on":[], "retries":0, "retry_budget":5,
      "contract_hash":"sha256:4a7c8e2f9d1b3a5e" },
    { "id":"todo-fe", "type":"fe", "status":"pending",
      "depends_on":["todo-be"], "contract_hash":"sha256:b2d4f6a8e0c1d3f5" }
  ],
  "l2_rounds":0, "l2_budget":3
}

# step 12 · write_integration_spec
{
  "version": "sdd-rev-1",
  "cases": [
    { "id": "create-then-list",
      "steps": [
        { "unit":"todo-be", "call":"POST /api/todos",
          "body":{"title":"buy milk"}, "expect":{"status":201} },
        { "unit":"todo-be", "call":"GET /api/todos",
          "expect":{"status":200, "json":{"items":"$.length > 0"}} }
      ], "involves": ["todo-be"] },
    { "id": "ui-renders-from-api",
      "steps": [
        { "unit":"todo-be", "call":"POST /api/todos", ... },
        { "unit":"todo-fe", "call":"mount <TodoListPage/>",
          "expect":{"dom":"li:contains('buy milk')"} }
      ], "involves": ["todo-be", "todo-fe"] }
  ]
}

# step 13 · validate_sdd_consistency → ok=true
# step 14 · timeline_append("sdd_generated", "sdd@rev1", "2 units, 2 cases")

# ─────────── OUTPUT ───────────
{
  "ok": true, "mode": "generate", "sdd_rev": 1,
  "files_written": [
    "sdd/index.html", "sdd/overview.html",
    "sdd/assets/shell.css", "sdd/assets/shell.js", "sdd/assets/search-index.json",
    "sdd/mvcp/model.html", "sdd/mvcp/view.html",
    "sdd/mvcp/control.html", "sdd/mvcp/process.html",
    "units.json", "integration/spec.json"
  ],
  "units_diff": { "added":["todo-be","todo-fe"], "changed":[], "removed":[] },
  "units_count": 2, "integration_cases_count": 2,
  "validation": { "ok":true, "issues":[] },
  "change_summary": "SDD rev 1: 2 units (todo-be · todo-fe), 2 e2e cases",
  "memory_recorded": [],
  "duration_ms": 68_120, "tokens_used": 14_823
}

⑦ 助手工具源码(4 个)

7.1 hash-contract.js · 固定算法 contract hash

// server/services/agent-tools/hash-contract.js · ~40 行
const crypto = require('crypto');

/**
 * Canonical JSON:递归排序 key,统一引号,无空白。
 * 保证同样 contract 在任意机器/任意时间产生同样 hash。
 */
function canonical(v) {
  if (v === null || typeof v !== 'object') return JSON.stringify(v);
  if (Array.isArray(v)) return '[' + v.map(canonical).join(',') + ']';
  const keys = Object.keys(v).sort();
  return '{' + keys.map(k => JSON.stringify(k) + ':' + canonical(v[k])).join(',') + '}';
}

function hashContract(contract) {
  if (!contract || typeof contract !== 'object')
    throw new Error('hashContract: contract must be object');
  const body = contract.exports || contract;
  const normalized = canonical(body);
  const full = crypto.createHash('sha256').update(normalized).digest('hex');
  return 'sha256:' + full.slice(0, 16);   // 截 16 hex chars
}

/**
 * 验证某 Unit 的 contract_hash 是否与文件中标记一致。
 * 用于 self-check + L2 gate 判定。
 */
function verifyContractHash(unit, contract) {
  const recomputed = hashContract(contract);
  return {
    ok: recomputed === unit.contract_hash,
    expected: unit.contract_hash,
    actual: recomputed,
  };
}

module.exports = { hashContract, verifyContractHash, canonical };

7.2 unit-extractor.js · 从 PRD feature-tree 派生 Unit 草案

// server/services/agent-tools/unit-extractor.js · ~80 行
const cheerio = require('cheerio');
const fs      = require('fs');

/**
 * 读 prd/feature/feature-tree.html,识别每个 feature,
 * 给 Architect 返回 Unit 拆分草案(架构判断仍由 Agent 做)。
 */
function extractUnitDrafts(prdFeatureTreePath) {
  const $ = cheerio.load(fs.readFileSync(prdFeatureTreePath, 'utf8'));
  const drafts = [];

  // feature-tree 约定: 每个特性是一个 <section data-feature-id>
  $('section[data-feature-id]').each((_, el) => {
    const id   = $(el).attr('data-feature-id');
    const name = $(el).find('h3').first().text().trim();
    const desc = $(el).find('p').first().text().trim();

    // 启发式判断需要哪些 Unit type
    const hint = $(el).attr('data-needs') || '';        // "be,fe" 或 "be,fe,mw,db"
    const needs = hint.split(',').map(s => s.trim()).filter(Boolean);

    const defaultNeeds = needs.length ? needs : ['be', 'fe'];   // 默认全栈

    for (const type of defaultNeeds) {
      drafts.push({
        unit_id:    `${id}-${type}`,
        feature_id: id,
        feature_name: name,
        type,
        description: desc,
        suggested_depends: suggestDeps(id, type, defaultNeeds),
      });
    }
  });

  return drafts;
}

function suggestDeps(featureId, type, allNeeds) {
  // fe 默认依赖同 feature 的 be
  if (type === 'fe' && allNeeds.includes('be')) return [`${featureId}-be`];
  // be 默认无依赖 (Architect 后续可加 mw/db 依赖)
  return [];
}

function validateUnitId(id) {
  return /^[a-z]+(-[a-z]+)*-(be|fe|mw|db)$/.test(id);
}

module.exports = { extractUnitDrafts, validateUnitId };

7.3 sdd-section-patcher.js · patch 模式精准应用

// server/services/agent-tools/sdd-section-patcher.js · ~70 行
const cheerio = require('cheerio');
const fs      = require('fs');
const path    = require('path');

/**
 * 应用 triage.proposed_sdd_patch 到 sdd/mvcp/ 内对应文件。
 * 不重写整页,只替换 target_section 的内容。
 */
function applySddPatch({ sddDir, patch, by }) {
  // target_section 形如 "#unit-auth-be" 或 "#def-auth-be-contract"
  // 在 sdd/mvcp/*.html 中查找含有该 id (或 data-unit-id 匹配) 的 section

  const candidates = ['model.html', 'view.html', 'control.html', 'process.html'];
  let applied = [];

  for (const f of candidates) {
    const p = path.join(sddDir, 'mvcp', f);
    if (!fs.existsSync(p)) continue;
    const raw = fs.readFileSync(p, 'utf8');
    const $ = cheerio.load(raw, { decodeEntities: false });

    // 尝试匹配 target_section
    const selector = patch.target_section.replace('#unit-', 'section[data-unit-id="').replace(/$/, '"]');
    const target = $(selector);
    if (target.length === 0) continue;

    if (patch.op === 'update_contract') {
      const pre = target.find('pre[data-contract]').first();
      if (pre.length === 0) {
        throw new Error(`${f}: target section has no <pre data-contract>`);
      }
      // 追加 patch.text 作为新行 (而非替换,以保留历史)
      pre.append('\n  // PATCH (' + by + '):\n  ' + patch.text + '\n');
      // 升 section data-rev
      const curRev = parseInt(target.attr('data-rev') || '1', 10);
      target.attr('data-rev', String(curRev + 1));

      fs.writeFileSync(p, $.html());
      applied.push({ file: f, section: patch.target_section, new_rev: curRev + 1 });
    } else if (patch.op === 'append_acceptance') {
      const ul = target.find('ul[data-acceptance]').first();
      ul.append('<li>' + patch.text + '</li>');
      fs.writeFileSync(p, $.html());
      applied.push({ file: f, op: 'append_acceptance' });
    }
    break;   // 第一个匹配的文件就处理
  }

  if (applied.length === 0)
    throw new Error(`No matching target_section found in sdd/mvcp/`);
  return applied;
}

module.exports = { applySddPatch };

7.4 sdd-validator.js · 三方一致性校验

// server/services/agent-tools/sdd-validator.js · ~90 行
const cheerio = require('cheerio');
const fs      = require('fs');
const path    = require('path');
const { validateUnitId } = require('./unit-extractor');
const { hashContract  } = require('./hash-contract');

function topoSort(units) {
  const idx = new Map();
  units.forEach((u,i) => idx.set(u.id, i));
  const color = new Array(units.length).fill(0);
  const order = [];
  let hasCycle = false;
  function visit(i) {
    if (color[i] === 2) return;
    if (color[i] === 1) { hasCycle = true; return; }
    color[i] = 1;
    for (const dep of units[i].depends_on) {
      if (idx.has(dep)) visit(idx.get(dep));
    }
    color[i] = 2;
    order.push(units[i].id);
  }
  for (let i = 0; i < units.length; i++) visit(i);
  return { ok: !hasCycle, order, cycle: hasCycle };
}

function validateSddConsistency(pid) {
  const root = path.resolve(projectsDir, pid);
  const issues = [];

  // 1. units.json 读 + schema (typebox 见 M-2)
  const units = JSON.parse(fs.readFileSync(path.join(root, 'units.json'), 'utf8'));
  units.units.forEach(u => {
    if (!validateUnitId(u.id)) issues.push(`unit id "${u.id}" violates naming pattern`);
  });

  // 2. DAG 无环
  const topo = topoSort(units.units);
  if (topo.cycle) issues.push('units.json has cycle in depends_on');

  // 3. 每个 Unit 在 sdd/mvcp/ 至少出现 1 次
  const sddTexts = ['model', 'view', 'control', 'process']
    .map(k => {
      const p = path.join(root, 'sdd/mvcp', k+'.html');
      return fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : '';
    }).join('\n');
  for (const u of units.units) {
    if (!sddTexts.includes(`data-unit-id="${u.id}"`) &&
        !sddTexts.includes(`data-def-name="${u.id}"`))
      issues.push(`unit ${u.id} not referenced in sdd/mvcp/`);
  }

  // 4. integration/spec.json involves 都在 units
  const spec = JSON.parse(fs.readFileSync(path.join(root, 'integration/spec.json'), 'utf8'));
  const unitIds = new Set(units.units.map(u => u.id));
  for (const c of spec.cases) {
    for (const u of c.involves) {
      if (!unitIds.has(u)) issues.push(`integration case ${c.id} involves unknown unit ${u}`);
    }
  }

  return { ok: issues.length === 0, issues };
}

module.exports = { validateSddConsistency, topoSort };

⑧ 自检 Checklist · 完工前必跑

#检查项命令 / 工具通过判定
1三方一致性validate_sdd_consistency(pid)ok=true, issues=[]
2units.json schematypebox 校验合规
3DAG 无环topo_sortcycle=false
4每个 Unit 在 sdd/mvcp/ 有引用cheerio grep全 hit
5每个 Unit contract_hash 重算一致verifyContractHash全 ok=true
6Unit id 全部符合命名规则validateUnitId全 true
7integration involves ⊆ unitsvalidate_sdd_consistency 内置无 "unknown unit"
8SDD 站点搜索索引同步update_search_index(pid, 'sdd')pages 含新写文件
9每页 ≥4 类信息cheerio 同 PRD 校验无 "X/8 types"
10todolist 全 doneread agent-state/architect-todo.json全 done
11timeline 含 sdd_generated/sdd_patchedtail timeline.ndjson最后 5 条之一命中
12agent-state.architect.json 终态read M-10status=done
13changelog.ndjson 含 sdd_rev 新条目tail changelog.ndjsonkind='sdd_rev'
14patch 模式专用:仅 affected_units 的 hash 变化diff_units 对比changed ⊆ triage.affected_units

任一不过 → 不输出 ok=true。patch 后重跑 ≤2 次。第 3 次仍不过 → 报 validation_failed 上报 L3。

⑨ 失败模式与降级

场景检测降级
PRD 描述歧义(feature 无法明确拆 Unit)Architect 自识别SDD 加 <aside data-question>,触发 L3 追问
Unit 拆得不一致(同一 feature 之前 2 个 Unit、现在 3 个)L2 修复 ≥2 轮时强制重生整个 SDD(generate 模式),不再 patch
DAG 出环topo_sort报错 → Agent retry 一次(移除最新加的依赖);2 次仍环 → blocked
contract_hash 碰撞(极低概率)所有 hash 集合查重退化用完整 64 位 hex
patch target_section 找不到sdd_section_patcher 报错升级到 generate 模式重生
integration/spec.json 引用未知 unitvalidate 内置修 spec 或 SDD,retry
Unit 命名违规(输入了 AuthBE)validateUnitId直接拒,Agent 必须改名
cheerio 解析 sdd/ html 失败write_mvcp_page 工具内置不写盘,retry 一次重生这段
memory 注入太多挤爆 promptLoopController 控制 top 5—
generate 模式遇到现有 units.jsonAgent 自识归档现有 → 重生 → 用 diff_units 算 added/changed/removed

⑩ 与其他 Agent 的握手摘要

对方Architect 给的Architect 收的
PM(无;PM 不读 SDD)prd/ 站点(generate 模式)
BugTriager(无)triage.json(patch 模式)
Codersdd/mvcp/*.html · 每 Unit 的 spec + contract 描述(无;Architect 不读 code)
Testersdd/mvcp/*.html 的 <ul class="tests"> 列表(无)
Integratorintegration/spec.json(无)
UnitRegistryunits.json + units_diff(无)
LoopControllerArchitectOutput JSONArchitectInput JSON · triage
memory/devloop/architect/新 .md 文件启动时被 LoopController 注入

⑪ 「测试 Architect Agent 是否真能跑」的实战命令

本规范的验证终局:

# 前置: PM Agent 已跑通,产出 projects/p-test001/prd/ 完整站点

# 1. 注册 Agent 模板
#    把本页 §2.2 的 System Prompt 落到:
#    system/templates/devloop-architect.md
#    并在 system/templates/_registry.json 加 'devloop-architect' 条目

# 2. 准备输入 (单行紧凑 JSON)
echo '{"pid":"p-test001","mode":"generate","prd_rev":1,"call_id":"call-arch-test","company_id":"c-test"}' > /tmp/arch-test.json

# 3. 调 Architect
curl -X POST http://localhost:3010/api/devloop/_test/run-agent/architect \
  -H "Content-Type: application/json" \
  -d @/tmp/arch-test.json

# 4. 期望:60-120 秒内
#    data/companies/c-test/projects/p-test001/
#    ├── sdd/index.html, sdd/overview.html, sdd/assets/*, sdd/mvcp/{model,view,control,process}.html
#    ├── units.json (DAG)
#    └── integration/spec.json

# 5. 三方一致性校验
node -e "console.log(require('./server/services/agent-tools/sdd-validator').validateSddConsistency('p-test001'))"
# 期望: { ok: true, issues: [] }

# 6. DAG 拓扑验证
node -e "const u=require('./data/companies/c-test/projects/p-test001/units.json');console.log(require('./server/services/agent-tools/sdd-validator').topoSort(u.units))"
# 期望: { ok: true, order: [...], cycle: false }

# 7. 浏览器打开
#    file:///data/companies/c-test/projects/p-test001/sdd/index.html
#    左侧 3 级菜单, 4 个 MVCP 章节, Ctrl+K 可搜索每个 Unit 名

# 8. (可选) patch 模式测试
echo '{"pid":"p-test001","mode":"patch","triage":{"run_id":"fake","proposed_sdd_patch":{"target_section":"#unit-todo-be","op":"update_contract","text":"GET /api/todos 加分页 ?page=N"},"affected_units":["todo-be","todo-fe"],"category":"contract_mismatch","confidence":0.9},"call_id":"call-arch-patch","company_id":"c-test"}' > /tmp/arch-patch.json
curl -X POST http://localhost:3010/api/devloop/_test/run-agent/architect -d @/tmp/arch-patch.json

# 期望: 仅 sdd/mvcp/control.html · units.json 改
#       units_diff.changed = ['todo-be','todo-fe']
#       todo-be / todo-fe contract_hash 变化, 其他 Unit hash 不变
这 8 条全过 = Architect Agent skill 验证通过。配合上一页 PM 的 §⑪,二者通过即代表 P1 + P3 阶段的核心 Agent 都已就绪,可以进 Phase 4 写其他 4 个 Agent。