agents-architect.md 34 KB

Architect Agent · 完整 Skill 规范

Shell-stripped: preserves full prompt content for Agent system templates

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

X · {简短描述}

{预期落盘路径} {TypeBox / TS / Vue props / SQL DDL}

  • 前置/后置/不变式 - 验收用例(给 Tester 转 vitest) [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_siteNEW | (pid) | generate 模式:建 sdd/ 目录、复制 assets | 项目内 | |write_mvcp_pageNEW | (pid, mvcp, defs[]) | 批量写 mvcp/{model|view|control|process}.html(含校验) | sdd/ 写 | |hash_contractNEW | (contract: object) → "sha256:..." | canonical JSON + sha256 截 16 位 | — | |topo_sortNEW | (units[]) → {ok, order, cycle?} | DAG 拓扑排序 + 环检测 | — | |write_units_jsonNEW | (pid, units) | typebox 校验 + 写盘 + 自动 hash 计算 | units.json 写 | |write_integration_specNEW | (pid, spec) | 校验 involves ↔ units → 写盘 | integration/spec.json 写 | |diff_unitsNEW | (pid, old, new) → {added, changed, removed} | 对比两版 units.json | — | |sdd_section_patcherNEW | (pid, patch) | patch 模式专用:精准应用 triage 提议 | sdd/ 写 | |validate_sdd_consistencyNEW | (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) | 同 PM | agent-state/ | |update_todo_item| (id, status, output_ref?) | 同 PM | agent-state/ | |update_runtime_state| (step, total) | 同 PM | agent-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 进出时同步更新。

    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

    ⑥ 完整执行样例

    Sample A · generate (TODO API) Sample B · patch (contract_mismatch) Sample C · incremental (+ OAuth)

    # ─────────── 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
    }
    
    # ─────────── INPUT (patch 模式) ───────────
    {
      "pid": "p-731042",
      "mode": "patch",
      "triage": {
        "run_id": "run-int-20260513-1031",
        "proposed_sdd_patch": {
          "target_section": "#unit-auth-be",
          "op": "update_contract",
          "text": "明确:登录失败必须 401 with {error:'invalid_credentials'}"
        },
        "affected_units": ["auth-be", "auth-fe"],
        "category": "contract_mismatch",
        "confidence": 0.86
      },
      ...
    }
    
    # step 1 · 读 triage + 定位 sdd/mvcp/control.html 内 data-def-name="auth-be" section
    
    # step 2 · sdd_section_patcher.apply()
    #   在 #unit-auth-be 的 contract 段落更新 401 响应描述
    #   同时同步 #unit-auth-fe 内对 401 期望的描述(因为 contract_mismatch 是双侧)
    
    # step 3 · 重算 contract_hash
    #   auth-be.contract 文本变了 → 新 hash sha256:8e1f...
    #   auth-fe.contract 文本变了 → 新 hash sha256:9c4d...
    
    # step 4 · write_units_json (incremental patch)
    #   找到 units[].id == 'auth-be' → 更新 contract_hash
    #   找到 units[].id == 'auth-fe' → 更新 contract_hash
    #   其他 unit 不动
    
    # step 5 · integration/spec.json 不动 (triage 没要求改)
    
    # step 6 · validate_sdd_consistency → ok=true
    #         timeline_append("sdd_patched", "sdd@rev2",
    #                         "+ 401 invalid_credentials · affected: auth-be, auth-fe")
    
    # ─────────── OUTPUT ───────────
    {
      "ok": true, "mode": "patch", "sdd_rev": 2,
      "files_written": [
        "sdd/mvcp/control.html",                // 仅这一页改
        "units.json",
        "sdd/assets/search-index.json"
      ],
      "files_archived": [
        "history/sdd-rev-1/control.html",
        "history/units-rev-1.json"
      ],
      "units_diff": {
        "added":[],
        "changed":["auth-be","auth-fe"],  // contract_hash 变化 → UnitRegistry reset
        "removed":[]
      },
      ...
    }
    
    # 下游连锁:
    #   UnitRegistry 见 changed=[auth-be, auth-fe]
    #     → 两个 Unit status: passed → pending
    #     → LoopController 回 L1 重跑
    
    # ─────────── INPUT (incremental,PRD 加了 OAuth) ───────────
    {
      "pid": "p-731042",
      "mode": "generate",         // PRD 变化 → 走 generate,不是 patch
      "prd_rev": 2,
      ...
    }
    
    # 注意: PRD 变化触发的 Architect 调用是 mode=generate (重新派生整套 SDD)
    # patch 模式专门给 BugTriager 用
    
    # step 0 · 读 prd/feature/feature-tree.html (rev 2)
    #   原 2 特性 + 1 新特性 (OAuth)
    
    # step 2 · 拆 Unit
    #   原:todo-be / todo-fe (保留)
    #   新 OAuth → 多处复用认证(todo-be 也得保护) → 拆 mw
    #     - auth-be: 处理 GitHub OAuth callback
    #     - auth-fe: 登录按钮 + redirect 处理
    #     - auth-mw: JWT 校验中间件 (todo-be 会用到)
    
    # step 5 · DAG
    #   todo-be    : depends_on []
    #   auth-be    : depends_on []
    #   auth-mw    : depends_on []
    #   todo-be 现在依赖 auth-mw → 加 dependency: depends_on:["auth-mw"]
    #   todo-fe    : depends_on ["todo-be","auth-fe"]
    #   auth-fe    : depends_on ["auth-be"]
    #   topo_sort → ok
    
    # diff_units(old, new):
    #   added:   ["auth-be","auth-fe","auth-mw"]
    #   changed: ["todo-be"]   ← 加了 auth-mw 依赖,contract 变了
    #   removed: []
    
    # OUTPUT
    {
      "ok": true, "mode": "generate", "sdd_rev": 3,
      "units_diff": {
        "added": ["auth-be", "auth-fe", "auth-mw"],
        "changed": ["todo-be"],         // 受 auth-mw 添加影响
        "removed": []
      },
      "units_count": 5,
      "integration_cases_count": 5,    // 新加 3 个 oauth flow + 1 个回归
      "change_summary": "+ OAuth 3 units · todo-be 加 auth-mw 依赖",
      "memory_recorded": [
        { "topic":"oauth-arch", "relevance":0.72 }
      ]
    }
    
    # UnitRegistry:
    #   added [auth-be, auth-fe, auth-mw] → status: pending (入 L1 队列)
    #   changed [todo-be] → status: passed → pending (重新跑)
    #   todo-fe 不在 diff → status 保持 (passed 的不动)
    

    ⑦ 助手工具源码(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]').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 
    `); } // 追加 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=[] | | 2 | units.json schema | typebox 校验 | 合规 | | 3 | DAG 无环 | `topo_sort` | cycle=false | | 4 | 每个 Unit 在 sdd/mvcp/ 有引用 | cheerio grep | 全 hit | | 5 | 每个 Unit contract_hash 重算一致 | `verifyContractHash` | 全 ok=true | | 6 | Unit id 全部符合命名规则 | `validateUnitId` | 全 true | | 7 | integration involves ⊆ units | validate_sdd_consistency 内置 | 无 "unknown unit" | | 8 | SDD 站点搜索索引同步 | `update_search_index(pid, 'sdd')` | pages 含新写文件 | | 9 | 每页 ≥4 类信息 | cheerio 同 PRD 校验 | 无 "X/8 types" | | 10 | todolist 全 done | read agent-state/architect-todo.json | 全 done | | 11 | timeline 含 sdd_generated/sdd_patched | tail timeline.ndjson | 最后 5 条之一命中 | | 12 | agent-state.architect.json 终态 | read M-10 | status=done | | 13 | changelog.ndjson 含 sdd_rev 新条目 | tail changelog.ndjson | kind='sdd_rev' | | 14 | **patch 模式专用**:仅 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 引用未知 unit | validate 内置 | 修 spec 或 SDD,retry | | Unit 命名违规(输入了 AuthBE) | validateUnitId | 直接拒,Agent 必须改名 | | cheerio 解析 sdd/ html 失败 | write_mvcp_page 工具内置 | 不写盘,retry 一次重生这段 | | memory 注入太多挤爆 prompt | LoopController 控制 top 5 | — | | generate 模式遇到现有 units.json | Agent 自识 | 归档现有 → 重生 → 用 diff_units 算 added/changed/removed | 
    
     ## ⑩ 与其他 Agent 的握手摘要 | 对方 | Architect 给的 | Architect 收的 | | --- | --- | --- | | **PM** | (无;PM 不读 SDD) | prd/ 站点(generate 模式) | | **BugTriager** | (无) | triage.json(patch 模式) | | **Coder** | sdd/mvcp/*.html · 每 Unit 的 spec + contract 描述 | (无;Architect 不读 code) | | **Tester** | sdd/mvcp/*.html 的 `<ul class="tests">` 列表 | (无) | | **Integrator** | integration/spec.json | (无) | | **UnitRegistry** | units.json + units_diff | (无) | | **LoopController** | ArchitectOutput JSON | ArchitectInput JSON · triage | | **memory/devloop/architect/** | 新 .md 文件 | 启动时被 LoopController 注入 | 
    
     ## ⑪ 「测试 Architect Agent 是否真能跑」的实战命令 <p>本规范的验证终局:
    
    

    前置: 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。