Skip to main content

Phase 0 — 度量基线 + 设计定案 + 止血 · 实施计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 落地 specialist-value-output master plan 的 Phase 0:edit-signal 指标基线、OQ-205 止血、promptTemplateKey 主路径修复、minimal 装配 trace、统一配置资产模型设计文档、PRD 更正。

Architecture: 全部为 expand-only 改动(新 flag / 新索引 / 新增响应字段 / metadata 增键),不改任何既有行为契约;processor 止血经 feature flag 控制;trace 走"agent 响应 audit 块扩展 + draft metadata 持久化"的最小路径,不建新表。

Tech Stack: NestJS 11 + TypeORM(jsonb) · Jest · FastAPI + pytest · feature-flags(org scope) · TypeORM migration(expand-contract 规则见根 CLAUDE.md)

上游文档: master plan v2 · PRD docs/specs/prd-specialist-value-output.md

基线事实(已核查,2026-07-07 @ 714516a6):

  • 两条 Expert 发送路径都已捕获 editSignal:respond() (expert-queue.service.ts:1156)、releaseDraft() (:1813),连同 correctionCategory 写入 Message.metadata(jsonb,entities.ts:526)。
  • respond() 的 metadata 存 queueItemId:1251)但不存 releasedDraftIdreleaseDraft()releasedDraftId:1846)。
  • 主派发点 conversations.service.ts:2583-2591 只 select systemPrompt.chat() 调用(:3092-3110不传 specialistPromptTemplateKey → wire 上 prompt_template_key 落到 req.role fallback(agent.client.ts:384)。
  • correction-refinement.processor.ts 无 flag 控制,_processInContext:78)扫 processedAt IS NULLingestionService.upload:174)直接写 KB。
  • agent 已回传 audit.prompt_versionagent.client.ts:529 解析为 promptVersion(接口字段 :127),但 conversations.service 不持久化(grep 0 命中)。
  • persona-override 启发式在 agent/prompts_loader.py:250-279is_full_template),当前不上报是否触发。

Task 1: P0.5 — 主派发路径传 specialistPromptTemplateKey(BUG 修复)

Files:

  • Modify: api/src/conversations/conversations.service.ts:2583-2591(select + 变量)、:3092-3110(chat 调用)

  • Modify: 同文件 regenerate-draft 的第二个 .chat({ 调用点(用 grep 定位,同样修法)

  • Test: api/src/conversations/specialist-template-key.spec.ts(新建)

  • Step 1: 定位全部 .chat({ 调用点

Run: grep -n "orgAgentClient.chat({\|agentClient.chat({" api/src/conversations/conversations.service.ts Expected: 2 个生产调用点(主派发 ~3092、regenerateDraft 路径)。记录两处行号。

  • Step 2: 写失败测试

新建 api/src/conversations/specialist-template-key.spec.ts。测试策略:不实例化完整 ConversationsService(依赖过重),复用仓库已有的 runAgentPipeline 测试基建 —— grep -ln "runAgentPipeline\|agentClient" api/test/*.spec.ts api/src/conversations/*.spec.ts 找到现有 harness(api/test/auto-reply-mode.spec.ts 已 mock AgentClient 驱动 pipeline),在该 harness 中新增断言块:

// 加入现有 runAgentPipeline harness(specialist fixture 需带 promptTemplateKey)
it("passes specialist promptTemplateKey to the agent (#ADR-034 wire fix)", async () => {
// arrange: specialistRepo.findOne mock 返回
// { systemPrompt: "You are Amy.", promptTemplateKey: "kyc_ops" }
// act: 触发 runAgentPipeline(沿用该 spec 现有的触发方式)
// assert:
const chatArgs = mockAgentClient.chat.mock.calls[0][0];
expect(chatArgs.specialistPromptTemplateKey).toBe("kyc_ops");
});

it("omits promptTemplateKey when specialist has none (falls back to role)", async () => {
// specialistRepo mock 返回 { systemPrompt: "...", promptTemplateKey: null }
const chatArgs = mockAgentClient.chat.mock.calls[0][0];
expect(chatArgs.specialistPromptTemplateKey ?? null).toBeNull();
});

注意:现有 harness 里 specialistRepo.findOne 的 mock 如果按 select 参数裁剪返回,需同步允许 promptTemplateKey

  • Step 3: 跑测试确认失败

Run: cd api && npx jest specialist-template-key --no-coverage Expected: FAIL — chatArgs.specialistPromptTemplateKeyundefined

  • Step 4: 实现

conversations.service.ts:2583-2591 改为:

    // Fetch Specialist systemPrompt + template key if the conversation has a specialistId
let specialistSystemPrompt: string | null = null;
let specialistPromptTemplateKey: string | null = null;
if (conv.specialistId && this.specialistRepo) {
const specialist = await this.specialistRepo.findOne({
where: { id: conv.specialistId },
select: ["systemPrompt", "promptTemplateKey"],
});
specialistSystemPrompt = specialist?.systemPrompt
? stripControlChars(specialist.systemPrompt)
: null;
// ADR-034: the specialist's template key must reach the wire — without
// this the agent resolves prompt_template_key from `role` fallback and
// the specialists.prompt_template_key column silently does nothing.
specialistPromptTemplateKey = specialist?.promptTemplateKey ?? null;
}

.chat({...}) 调用(:3092 处)在 specialistSystemPrompt, 之后加一行:

          specialistSystemPrompt,
specialistPromptTemplateKey,

对 regenerateDraft 的第二个调用点做同样的两处修改(该路径同样 fetch specialist —— 若它复用了别的 prompt 解析函数,把 key 一并穿透)。

  • Step 5: 跑测试确认通过 + 回归

Run: cd api && npx jest specialist-template-key agent.client --no-coverage && npx tsc --noEmit Expected: PASS;tsc clean。

  • Step 6: Commit
git add api/src/conversations/conversations.service.ts api/src/conversations/specialist-template-key.spec.ts
git commit -m "fix(api): thread specialist promptTemplateKey through agent dispatch (ADR-034 wire gap)"

Task 2: P0.4 — correction 自动写 KB 止血(flag)+ OQ-205 ADR

Files:

  • Modify: api/src/feature-flags/feature-flags.constants.ts

  • Modify: api/src/haystack/jobs/correction-refinement.processor.ts

  • Test: api/src/haystack/jobs/correction-refinement.flag.spec.ts(新建;若已有 processor spec 则并入)

  • Create: docs/decisions/037-runtime-readonly-and-human-approved-promotion.md

  • Step 1: 写失败测试

// api/src/haystack/jobs/correction-refinement.flag.spec.ts
import { CorrectionRefinementProcessor } from "./correction-refinement.processor";

describe("CorrectionRefinementProcessor — correction_auto_ingest_enabled gate (OQ-205)", () => {
function makeProcessor(flagValue: boolean) {
const correctionRepo = {
find: jest.fn().mockResolvedValue([
{
id: "c1",
originalResponse: "a",
correctedResponse: "b",
expertQueueItem: { conversation: { orgId: "org-1" } },
},
]),
};
const ingestionService = { upload: jest.fn() };
const featureFlags = { isEnabled: jest.fn().mockResolvedValue(flagValue) };
const rlsContext = {
withInternalContext: jest.fn((fn: any) => fn(undefined)),
};
const processor = new CorrectionRefinementProcessor(
correctionRepo as any,
{ } as any, // orgDocumentRepo
ingestionService as any,
{ tryLog: jest.fn(), log: jest.fn() } as any, // auditService
{ isIntegrationPausedSafe: jest.fn().mockResolvedValue(false) } as any,
rlsContext as any,
{ generateObject: jest.fn() } as any, // llm
featureFlags as any, // NEW dep
);
return { processor, ingestionService, featureFlags, correctionRepo };
}

it("does NOT ingest nor markProcessed when flag disabled for the org", async () => {
const { processor, ingestionService, featureFlags } = makeProcessor(false);
const markProcessed = jest
.spyOn(processor as any, "markProcessed")
.mockResolvedValue(undefined);
await processor.process();
expect(featureFlags.isEnabled).toHaveBeenCalledWith(
"correction_auto_ingest_enabled",
expect.objectContaining({ orgId: "org-1" }),
);
expect(ingestionService.upload).not.toHaveBeenCalled();
expect(markProcessed).not.toHaveBeenCalled(); // 留给未来 proposal 流消费
});

it("proceeds when flag enabled", async () => {
const { processor, featureFlags } = makeProcessor(true);
// categorize 会走 LLM mock 并抛错 → 被外层 catch 吞掉;只断言 gate 已放行
await processor.process();
expect(featureFlags.isEnabled).toHaveBeenCalled();
});
});
  • Step 2: 跑测试确认失败

Run: cd api && npx jest correction-refinement.flag --no-coverage Expected: FAIL — 构造函数不接受第 8 个参数 / isEnabled 未被调用。

  • Step 3: 实现 flag key

feature-flags.constants.tsFLAG_KEYS 数组(ai_reply_attachments_enabled 之后)加:

  // OQ-205 (ADR-037) — corrections auto-ingesting into the KB without human
// approval contradicts the read-only-runtime default. OFF: corrections stay
// unprocessed (processedAt NULL) until the human-approved proposal flow
// (master plan P4.2) consumes them. Enable per-org ONLY as an explicit,
// documented exception while P4.2 is not yet shipped.
"correction_auto_ingest_enabled",

DEFAULT_FLAG_VALUES 加:

  // OQ-205: default OFF — nothing self-applies. See ADR-037.
correction_auto_ingest_enabled: false,
  • Step 4: 实现 processor gate

correction-refinement.processor.ts:constructor 追加依赖(FeatureFlagService../../feature-flags/feature-flag.service import;haystack.module 需 import FeatureFlagsModule —— 若其为 @Global() 则只加构造参数):

    private readonly llm: LlmService,
private readonly featureFlags: FeatureFlagService,

processOneCorrection 在 orgId 解析之后、isIntegrationPausedSafe 之前插入:

    // OQ-205 / ADR-037 — human-approved promotion is the default. When the
// auto-ingest flag is OFF for this org, leave the correction UNprocessed
// (processedAt stays NULL) so the future proposal flow can consume it.
const autoIngestEnabled = await this.featureFlags.isEnabled(
"correction_auto_ingest_enabled",
{ orgId },
);
if (!autoIngestEnabled) {
this.logger.debug(
`Correction ${correction.id} left unprocessed: correction_auto_ingest_enabled OFF for org ${orgId}`,
);
return;
}
  • Step 5: 跑测试 + 既有回归

Run: cd api && npx jest correction-refinement feature-flag --no-coverage && npx tsc --noEmit Expected: 全 PASS(feature-flag.service.spec.ts 有 FLAG_KEYS 快照类断言的话需同步更新)。

  • Step 6: 写 ADR-037

docs/decisions/037-runtime-readonly-and-human-approved-promotion.md,结构:

# ADR-037: Runtime is read-only toward Specialist assets; improvement loop proposes, humans approve

Status: Proposed → (评审后) Accepted
Date: 2026-07-XX
Closes: OQ-205 (OPEN_DECISIONS.md §OQ-205) · PRD prd-specialist-value-output Q1/T0.4

## Decision
1. Option A:runtime 对 Soul/Skill/KB 等 Specialist 资产永远只读;
2. 改进循环(corrections、eval 建议)只产出 proposal,人工(AM/Expert)approve 后生效;
3. 过渡期:correction→KB 自动 ingest 由 `correction_auto_ingest_enabled` flag 控制,默认 OFF;
P4.2 proposal 审批流上线后删除该 flag 与 processor 的直接 ingest 分支(contract 阶段)。
## Context …(引 PRD §2、ADR-033 L6、现状 processor 行为)
## Consequences …(corrections 积压至 P4.2 是接受的;紧急放行路径 = per-org flag ON + 审计)
## Alternatives considered …(B: PR-promotion;C: audited write-back — 为何否决)
  • Step 7: Commit
git add api/src/feature-flags/feature-flags.constants.ts api/src/haystack/jobs/ docs/decisions/037-*.md
git commit -m "feat(api): gate correction auto-ingest behind correction_auto_ingest_enabled (OQ-205, ADR-037)"

Task 3: P0.1 — edit-signal 补全(统一 draft 链接键 + 指标索引)

设计决定(对 PRD R4.1 AC 的收窄,写进 PR 描述):不建新表、不冗余 diff 文本 —— 完整 diff 由 expert message ↔ 被释放 draft join 派生(两侧 content 都已在 messages 表)。本任务只补两个真实缺口:respond() 路径缺 releasedDraftId(join 键不统一);缺分析索引。

Files:

  • Modify: api/src/expert-queue/expert-queue.service.ts(respond() metadata 块 :1249-1260

  • Create: api/migrations/<timestamp>-AddReleaseSignalIndex.ts

  • Test: 扩展 api/src/expert-queue/release-draft-attachments.spec.ts 同级新建 api/src/expert-queue/release-signal-link.spec.ts

  • Step 1: 写失败测试

// api/src/expert-queue/release-signal-link.spec.ts
// 复用 release-draft-attachments.spec.ts 的 service 构造 harness(同目录、
// 同 mock 形状)——断言 respond() 保存的 expert message metadata 带
// releasedDraftId = item.messageId(有 aiDraft 时)。
it("respond() stamps releasedDraftId alongside editSignal", async () => {
// arrange: pending item with messageId "draft-1"; msgRepo.findOne 返回
// { id: "draft-1", content: "AI text", metadata: { confidence: 0.9 } }
// act: await service.respond(itemId, "edited text", caller)
const saved = mockEm.save.mock.calls.find(
([entity]: any[]) => entity.role === "expert",
)![0];
expect(saved.metadata.releasedDraftId).toBe("draft-1");
expect(saved.metadata.wasEdited).toBe(true);
});
  • Step 2: 跑测试确认失败

Run: cd api && npx jest release-signal-link --no-coverage Expected: FAIL — releasedDraftId undefined。

  • Step 3: 实现

expert-queue.service.ts respond() 的 metadata 块(:1249)中,在 ...editSignal, 后加:

            // #3323 uniform draft-link key: releaseDraft() already stamps
// releasedDraftId; respond() must too so the edit-diff join
// (expert msg ↔ released draft) is one key on both send paths.
...(item.messageId && editSignal.wasEdited !== undefined
? { releasedDraftId: item.messageId }
: {}),

(条件 editSignal.wasEdited !== undefined = 只有确实找到 aiDraft 并算了 signal 才 stamp,follow-up 回复不带。注意 editSignal 声明处若类型是 Partial<EditSignal>,此判断成立。)

  • Step 4: 指标索引 migration

先看仓库 CONCURRENTLY 先例:grep -rln "CONCURRENTLY" api/migrations/ | head -3,镜像其事务处理方式。新建(时间戳取当前,命名沿用仓库风格):

// api/migrations/<ts>-AddReleaseSignalIndex.ts
import { MigrationInterface, QueryRunner } from "typeorm";

/** Expand-only. Serves the draft-quality metrics rollup (master plan P0.2):
* released-draft expert messages filtered by osa/time. Partial: only rows
* that actually carry the edit signal. */
export class AddReleaseSignalIndex<ts> implements MigrationInterface {
public async up(q: QueryRunner): Promise<void> {
await q.query(
`CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_messages_release_signal
ON messages (osa_id, created_at)
WHERE role = 'expert' AND metadata ? 'wasEdited'`,
);
}
public async down(q: QueryRunner): Promise<void> {
await q.query(`DROP INDEX CONCURRENTLY IF EXISTS idx_messages_release_signal`);
}
}
  • Step 5: 验证

Run: cd api && npx jest release-signal-link release-draft-attachments expert-queue --no-coverage && npx tsc --noEmit Expected: PASS(migration 由 CI api-postgres-migrations job 验证前向应用)。

  • Step 6: Commit
git add api/src/expert-queue/ api/migrations/
git commit -m "feat(api): uniform releasedDraftId link on respond() + release-signal metrics index (P0.1)"

Task 4: P0.2 — draft-quality 指标端点 + /ops 卡片

Files:

  • Create: api/src/analytics/draft-quality.service.ts + draft-quality.controller.ts(挂进 analytics 模块;若该模块结构不同则并入现有 controller)

  • Test: api/src/analytics/draft-quality.service.spec.ts

  • Modify: frontend/src/app/ops/page.tsx(或 ops 首页对应组件,Step 5 先定位)

  • Step 1: 写失败测试(服务层,Postgres-only 跳过 sqlite)

// api/src/analytics/draft-quality.service.spec.ts
import { DraftQualityService } from "./draft-quality.service";

describe("DraftQualityService.rollup", () => {
it("aggregates acceptance rate and mean edit ratio by day", async () => {
const rows = [
{ day: "2026-07-01", releases: "10", sent_unedited: "7", mean_edit_ratio: "0.21" },
];
const dataSource = { query: jest.fn().mockResolvedValue(rows) };
const svc = new DraftQualityService(dataSource as any);
const out = await svc.rollup({ orgId: "o1", specialistId: "s1", days: 14 });
expect(dataSource.query).toHaveBeenCalledWith(
expect.stringContaining("wasEdited"),
expect.arrayContaining(["o1", "s1"]),
);
expect(out[0]).toEqual({
day: "2026-07-01",
releases: 10,
acceptanceRate: 0.7,
meanEditRatio: 0.21,
});
});
});
  • Step 2: 跑测试确认失败

Run: cd api && npx jest draft-quality --no-coverage Expected: FAIL — 模块不存在。

  • Step 3: 实现服务
// api/src/analytics/draft-quality.service.ts
import { Injectable } from "@nestjs/common";
import { InjectDataSource } from "@nestjs/typeorm";
import { DataSource } from "typeorm";

export interface DraftQualityDay {
day: string;
releases: number;
acceptanceRate: number; // sent-unedited %
meanEditRatio: number | null;
}

/** Draft-quality rollup (master plan P0.2 / PRD R4.2, §6 metrics).
* Reads Message.metadata edit signals (#3323) — ADR-020: caller must be an
* HP-staff surface; query filters org AND specialist via conversations join. */
@Injectable()
export class DraftQualityService {
constructor(@InjectDataSource() private readonly ds: DataSource) {}

async rollup(opts: {
orgId?: string;
specialistId?: string;
days: number;
}): Promise<DraftQualityDay[]> {
const params: unknown[] = [];
let where = `m.role = 'expert' AND m.metadata ? 'wasEdited'
AND m.created_at >= now() - ($${params.push(opts.days)}::int * interval '1 day')`;
if (opts.orgId) where += ` AND c.org_id = $${params.push(opts.orgId)}`;
if (opts.specialistId) where += ` AND c.specialist_id = $${params.push(opts.specialistId)}`;
const rows: Array<Record<string, string>> = await this.ds.query(
`SELECT to_char(date_trunc('day', m.created_at), 'YYYY-MM-DD') AS day,
count(*)::text AS releases,
count(*) FILTER (WHERE m.metadata->>'wasEdited' = 'false')::text AS sent_unedited,
avg((m.metadata->>'editRatio')::float)
FILTER (WHERE m.metadata->>'wasEdited' = 'true')::text AS mean_edit_ratio
FROM messages m
JOIN conversations c ON c.id = m.conversation_id
WHERE ${where}
GROUP BY 1 ORDER BY 1`,
params,
);
return rows.map((r) => ({
day: r.day,
releases: Number(r.releases),
acceptanceRate: Number(r.releases)
? Number(r.sent_unedited) / Number(r.releases)
: 0,
meanEditRatio: r.mean_edit_ratio === null ? null : Number(r.mean_edit_ratio),
}));
}
}

Controller:GET /analytics/draft-quality?orgId&specialistId&days=14,roles superadmin | account_manager(沿用 analytics 模块现有 guard 组合;query 参数走 DTO + @IsUUID()/@IsInt() 校验)。注册进 analytics 模块的 providers/controllers。

  • Step 4: 跑测试确认通过

Run: cd api && npx jest draft-quality --no-coverage && npx tsc --noEmit Expected: PASS。

  • Step 5: /ops 卡片

定位 ops 首页:ls frontend/src/app/ops/(首页文件 page.tsx)。新增组件 frontend/src/components/ops/DraftQualityCard.tsx:14 天 acceptance-rate 折线(复用 ops 现有图表依赖;若无图表库则渲染两个数值 tile:本周 acceptance rate、mean edit ratio + 周环比箭头),数据取 GET /analytics/draft-quality?days=14(走 frontend/src/lib/api.ts 新增 fetchDraftQuality(),沿用现有鉴权 fetch 封装)。挂到 ops 首页卡片区。

  • Step 6: 前端验证 + Commit

Run: cd frontend && npx tsc --noEmit && npm run build 2>&1 | tail -5 Expected: build 通过。

git add api/src/analytics/ frontend/src/components/ops/ frontend/src/lib/api.ts frontend/src/app/ops/
git commit -m "feat: draft-quality metrics endpoint + ops dashboard card (P0.2, PRD R4.2)"

Task 5: P0.6 — minimal per-turn assembly trace

设计:不建新表。AG 侧把装配元数据加进已有 audit 响应块;BE 侧解析并持久化到 draft message 的 metadata.assembly(jsonb,天然 join 到消息/edit-signal)。全量 trace(version ids、独立表、90 天保留)在 Phase 3 P3.4。

Files:

  • Modify: agent/prompts_loader.py(返回装配元数据)

  • Modify: agent/models/shared.py(AuditMetadata 增字段)

  • Modify: agent/main.py(audit payload 增字段;4 个构造点 :727,752,1021,1060 附近,以 grep 定位)

  • Modify: agent/chat_helpers.py(穿透 meta)

  • Test: agent/evals/test_prompt_assembly_meta.py(新建)

  • Modify: api/src/common/agent.client.ts:127 接口 + :529 解析)

  • Modify: api/src/conversations/conversations.service.ts(draft 持久化点 metadata 增 assembly 键)

  • Test: api/src/common/agent.client.spec.ts(扩展)

  • Step 1: AG 失败测试

# agent/evals/test_prompt_assembly_meta.py
"""P0.6 minimal assembly trace — build_system_prompt must report which path fired."""
from prompts_loader import build_system_prompt_with_meta


def test_short_persona_reports_header_injection():
prompt, meta = build_system_prompt_with_meta(
role="kyc_ops", agent_context={"specialist_system_prompt": "Be warm."}
)
assert meta["template_used"] == "kyc_ops"
assert meta["persona_override_applied"] is False
assert "SPECIALIST PERSONA" in prompt


def test_long_persona_reports_override_fired():
long_persona = "x" * 400 # > 300 chars triggers the full-template heuristic
prompt, meta = build_system_prompt_with_meta(
role="kyc_ops", agent_context={"specialist_system_prompt": long_persona}
)
assert meta["persona_override_applied"] is True
assert meta["template_used"] is None # role template was displaced


def test_no_persona():
_, meta = build_system_prompt_with_meta(role="finance_ops", agent_context={})
assert meta["persona_override_applied"] is False
assert meta["template_used"] == "finance_ops"

Run: cd agent && python -m pytest evals/test_prompt_assembly_meta.py -v Expected: FAIL — build_system_prompt_with_meta 不存在。 (若 build_system_prompt 实际签名的 persona 入参与上面不符,以 prompts_loader.py:236 真实签名为准调整测试入参,断言不变。)

  • Step 2: AG 实现

prompts_loader.py:把 build_system_prompt 主体改名为 build_system_prompt_with_meta,返回 (prompt, meta);原名函数保留为薄包装(return build_system_prompt_with_meta(...)[0]),调用方零改动。metais_full_template 分支处构造:

    meta = {
# which role template body was loaded; None when the persona override
# displaced it entirely (the ADR-033 anti-pattern we are measuring)
"template_used": None if is_full_template else role,
"persona_override_applied": is_full_template,
"persona_chars": len(specialist_override or ""),
}

chat_helpers.py 的 prompt 构建处改调 _with_meta 变体并把 meta 与既有 compute_prompt_version 结果一起向上返回;main.py 的 4 个 audit payload 构造点(grep -n "prompt_version" agent/main.py)在 prompt_version 旁增加:

    "prompt_template_used": assembly_meta.get("template_used"),
"persona_override_applied": assembly_meta.get("persona_override_applied"),

models/shared.py AuditMetadata 增字段:

    prompt_template_used: Optional[str] = None
persona_override_applied: Optional[bool] = None

Run: cd agent && python -m pytest evals/test_prompt_assembly_meta.py evals/test_hermes_parser.py -v Expected: PASS(含既有 parser 回归)。

  • Step 3: BE 失败测试

api/src/common/agent.client.spec.ts 增用例(沿用该文件现有 fetch-mock 形状):

it("parses assembly trace fields from the audit block (P0.6)", async () => {
// fetch mock 返回体 audit 块增加:
// audit: { prompt_version: "abc", prompt_template_used: "kyc_ops",
// persona_override_applied: false, ... }
const resp = await client.chat(baseRequest);
expect(resp.promptVersion).toBe("abc");
expect(resp.promptTemplateUsed).toBe("kyc_ops");
expect(resp.personaOverrideApplied).toBe(false);
});

Run: cd api && npx jest agent.client --no-coverage → Expected: FAIL。

  • Step 4: BE 实现

agent.client.ts AgentResponse 接口(promptVersion?: string; 之后,:127):

  promptTemplateUsed?: string | null;
personaOverrideApplied?: boolean;

解析处(:529 同一块):

      promptTemplateUsed: (data["audit"] as Record<string, unknown> | undefined)?.["prompt_template_used"] as string | null | undefined,
personaOverrideApplied: (data["audit"] as Record<string, unknown> | undefined)?.["persona_override_applied"] as boolean | undefined,

conversations.service.ts draft 持久化点(grep -n 'draftStatus: "active"' api/src/conversations/conversations.service.ts 定位;agent 草稿 metadata 构造块)增加:

            // P0.6 minimal assembly trace — joinable to edit signals by this
// very message row; full versioned trace lands with the assembler
// (master plan P3.4). Keys absent when the agent didn't report.
...(agentResp.promptVersion
? {
assembly: {
promptVersion: agentResp.promptVersion,
templateKeySent: specialistPromptTemplateKey ?? conv.role ?? null,
templateUsed: agentResp.promptTemplateUsed ?? null,
personaOverrideApplied: agentResp.personaOverrideApplied ?? null,
kbContextProvided,
activeDirectivesCount: activeDirectives?.length ?? 0,
},
}
: {}),
  • Step 5: 验证 + Commit

Run: cd api && npx jest agent.client conversations --no-coverage && npx tsc --noEmit Expected: PASS。

git add agent/ api/src/common/agent.client.ts api/src/conversations/conversations.service.ts api/src/common/agent.client.spec.ts
git commit -m "feat: minimal per-turn assembly trace — agent audit block + draft metadata (P0.6, PRD R5.4-minimal)"

上线后一周检查点:override 命中率 = count(*) FILTER (WHERE (metadata->'assembly'->>'personaOverrideApplied') = 'true')::float / NULLIF(count(*) FILTER (WHERE metadata->'assembly'->>'personaOverrideApplied' IS NOT NULL), 0) —— 分母必须过滤 IS NOT NULL:混布期老 agent 产生的 draft 行没有 assembly 字段(值为 null),不进分母,否则命中率被稀释。这是 Phase 1 T1.4/P1.6 移除启发式的风险面数据。


Task 6: P0.3 — 统一配置资产模型设计文档(DS)

Files:

  • Create: docs/superpowers/specs/2026-07-XX-unified-config-asset-model.md

  • Step 1: 按 master plan §三骨架细化到字段级,必须覆盖(每项都是评审 checklist):

    • specialist_config_assets / specialist_config_versions 全字段(类型、约束、索引),docstring 注明 ADR-020 隔离矩阵行
    • 5 个 asset_type 的 content Zod schema 初稿(soul / skill / kb_golden_answer / kb_domain_rule / tool_binding),红线 section 与 skill 的 tools 预留字段在内
    • 发布状态机(draft→publishing→published|blocked|rolled_back)+ eval 钩子挂点 + NFR2 异步语义 + 审计事件命名(config.publish 等)
    • D2:PG jsonb vs R2 引用的尺寸阈值与 eval-sandbox 只读访问路径
    • D3:装配器 placement 定案(agent 侧 vs API 侧)——引 ADR-034 E2 phase-4.5、root CLAUDE.md "Hermes preserved"、master plan §三 Phase 3 倾向意见;给出结论与迁移预案
    • 装配优先级全链:红线 > crisis > active_directives > skill 指令 > Soul > 角色模板,逐层 token 预算 + NFR3 截断次序
    • archetype→instance copy-on-materialize + 显式 pull 的数据流(R2.6)
    • 导入框架(SOUL.md / SKILL.md / systemPrompt → 资产,dry-run + diff)
    • 对照 ADR-033 L0/L1 的自查段落
  • Step 2: 发起评审(Franky + Eng owner),D2/D3 结论回填 master plan §五决策表

  • Step 3: Commitgit add docs/superpowers/specs/ && git commit -m "docs: unified config asset model design (P0.3, gates Phase 1)"


Task 7: P0.7 — PRD 更正(en/zh)

Files:

  • Modify: docs/specs/prd-specialist-value-output.md + .zh.md

  • Step 1: 按 master plan §2.1 的 8 行核查表修订 PRD §3 表(8 处,含 ADR-035 状态改 Proposed、tool_specialist_bindings 改"待新建"、edit-diff/few-shot/R4.3 现状更正、runtime-config 通道事实)

  • Step 2: §4 R5.1 优先级链补 crisis + active_directives 两层(对应 master plan N2)

  • Step 3: R4.4 注明 retrofit 性质 + 引 ADR-037;zh 版同步全部修改

  • Step 4: Commitgit commit -m "docs(prd): correct stale current-state claims per 2026-07-07 code audit"


执行顺序与依赖

Task 1 (P0.5, XS) ──────────┐
Task 2 (P0.4, S) ──────────┤ 互相独立,可并行
Task 3 (P0.1, S) ──► Task 4 (P0.2, S)
Task 5 (P0.6, M) ← Task 1 先合(persist 块引用 specialistPromptTemplateKey 变量)
Task 6 (P0.3, DS) ── 与代码任务并行推进;gate Phase 1
Task 7 (P0.7, XS) ── 任意时点

Self-review 结论

  • Spec 覆盖:master plan P0.1–P0.7 全部有对应 Task(P0.1→T3、P0.2→T4、P0.3→T6、P0.4→T2、P0.5→T1、P0.6→T5、P0.7→T7)。
  • 已知不确定点(执行时按真实代码微调,不改变任务目标):T1 的 runAgentPipeline 测试 harness 具体文件、T5 中 build_system_prompt 的真实入参形状与 chat_helpers 穿透点、T4 的 analytics 模块现有 guard 组合、T3 migration 的 CONCURRENTLY 事务处理先例。每处均给了定位命令。
  • 类型一致性:specialistPromptTemplateKey(T1 定义,T5 复用)、correction_auto_ingest_enabled(T2 定义处与调用处一致)、releasedDraftId(T3 与 releaseDraft 既有键名一致)。