一、技能重组(动作-对象命名) - 旧目录 clean/confirm/continuation/db/detect/embed/… 重组为 clean-book-text/decide-candidate/write-next-chapter/access-database/ check-content-consistency/embed-knowledge/…(git 识别为 rename,内容保持) - agents/*.md、AGENTS.md/CLAUDE.md 收编、example_skill 登记表同步新名 二、先审后入创作闭环(本次核心) 正文接受从"机械门一过就写正典"改为"机械门+语义审查双通过+用户批准+单事务原子提交", DB 级兜底,编排层跳步即被硬拒。 - candidate_cas.py + example_candidate_cas(109):持久化 CAS 状态链 - fact_delta.py + example_fact_delta/example_fact_ledger(106):结构化事实增量, 模型只提六型闭集增量+正文证据引文,仅用户批准的增量随正文同事务入账本 - projection_registry.py + example_projection_run(107):投影登记与恢复 - acceptance_state.py:接受前置实时状态重读 - lesson_registry.py + example_lesson(108):经验升格链,禁止自动升格 - DDL 105:example_candidate 增 semantic_status/semantic_report_sha256 - write_canonical.accept:语义兜底+同事务合并增量+登记投影; run_writer_pipeline/persist_writer_run/run_writer_semantic_detector/step2 接入全链 - claude_runtime:兼容新 CLI modelUsage 信息字段 三、审查修复(独立子代理四维审查后) - 事实增量 propose→approve 翻态正道,不撞唯一键 - 冻结配置探针重刷(CLI 2.1.211→2.1.231 漂移),profileSha256/adapterVersion 再登记 - 可视化合同悬空路径/五六空间矛盾、 SoT 旧技能名漂移、行尾空白清理 测试:离线 65 套 + 真实库集成 5 套(CAS/接受故障注入/事实增量/投影/经验升格)+ 回放 79 项全绿。 创作内容(docs/design、生成正文 artifacts)按"框架与创作分开"未入本提交。
265 lines
14 KiB
Python
265 lines
14 KiB
Python
#!/usr/bin/env python3
|
||
"""真写一章 · 阶段二a:writer 真子进程写第 1 章正文(基线章)。
|
||
|
||
为什么第 1 章单独走这条路(而不是生产组装):
|
||
- read-context 合同要求 asOf>=1 且 asOf<targetChapter,第 1 章没有合法 asOf;
|
||
- validate_writer_context / check_writer_candidate 都以完整 WriterContext v1 为前提。
|
||
所以第 1 章是基线章:writer 真调(claude_runtime.run_claude:sandbox 隔离 + 冻结 profile
|
||
+ 联合回执校验),但不走 assemble_context 与机械/语义门(门不适用于无前文的章),落库后
|
||
经 confirm 的 accept 唯一通道转正式正文,为第 2 章的生产流程(asOf=1)提供基线。
|
||
|
||
⚠ 先审后入兜底说明(2026-08-14):`write_canonical.accept` 现要求候选 `semantic_status='passed'`
|
||
才放行。本脚本造的第 1 章基线候选不跑语义门(semantic_status 为空),**重跑会在 accept 处被
|
||
DB 级兜底拒绝**。第 1 章基线已在本脚本首跑时落库;如未来确需重写基线章,须先就"无前文基线章
|
||
的接受豁免"做显式治理决策,不得绕过兜底直接写块。
|
||
|
||
本脚本修复的两处真 bug(上一会话遗留):
|
||
1. system prompt:上一版用 .claude/agents/writer.md(元定义,声称"合同由派发指令加载"),
|
||
writer 拿不到功能合同只回 3 字占位 "..."。本版用 replay-eval config 里冻结且跑出过
|
||
完整章节的 writer 提示词(systemPromptId=writer-gate-a-system-v2),从配置文件读取,
|
||
不复制粘贴(sha256 断言核对)。
|
||
2. 落库:上一版 example_run insert 缺 finished_at,撞 check 约束
|
||
chk_example_run_terminal_needs_finish 整段回滚。本版补 started_at/finished_at,
|
||
并把 ExecutionReceipt 的 usage/stop_reason/terminal_reason/is_error 落回执。
|
||
|
||
另:上一会话"绕过 sandbox"的临时手段已撤销——探针(probe_sandbox.py)证明 sandbox 本身
|
||
完好,当时失败的真因是 build_writer_execution_profile 把 normal_terminal_reasons 冻结成
|
||
("success",),而 CLI 2.1.211 正常结束给 "completed"(已在 run_writer.py 修复并补测试)。
|
||
本版恢复 sandbox 隔离,走 run_claude 官方路径。
|
||
|
||
创作输入严格按 build_writer_creative_input 的生产投影形状构造(见 run_writer.py);
|
||
文风约束从设定包(planning section id=3)的 style 字段投影——第 1 章无历史原文摘录,
|
||
文风画像是唯一的文风锚点。
|
||
"""
|
||
import hashlib
|
||
import json
|
||
import subprocess
|
||
import sys
|
||
import uuid
|
||
from datetime import datetime
|
||
from decimal import Decimal
|
||
from pathlib import Path
|
||
|
||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||
AGENT_ROOT = SCRIPT_DIR.parents[1]
|
||
SKILLS = AGENT_ROOT / ".claude" / "skills"
|
||
for sub in (
|
||
"write-next-chapter/scripts",
|
||
"execute-claude-task/scripts",
|
||
"access-database/scripts",
|
||
"decide-candidate/scripts",
|
||
"assemble-context/scripts",
|
||
):
|
||
p = str(SKILLS / sub)
|
||
if p not in sys.path:
|
||
sys.path.insert(0, p)
|
||
|
||
from db import connect # noqa: E402
|
||
from claude_runtime import run_claude # noqa: E402
|
||
from run_writer import build_writer_execution_profile # noqa: E402
|
||
from writer_contract import han_count, normalize_text, validate_writer_draft # noqa: E402
|
||
from write_canonical import accept # noqa: E402
|
||
|
||
WORK_ID = 12
|
||
TARGET_CHAPTER = 1
|
||
CLAUDE_BIN = "/Users/qingse/.nvm/versions/node/v24.15.0/bin/claude"
|
||
CLAUDE_SHA256 = "ba790279cab6ef77b713864d4bf5f764fcea87d3a3eb7591a41f741e45212b5c"
|
||
CLAUDE_VERSION = "2.1.231"
|
||
RESOLVED_MODEL_ID = "claude-opus-4-8[1m]"
|
||
|
||
# 冻结 writer 提示词:从 replay-eval config 读(不复制粘贴,sha256 可与配置核对)
|
||
GATE_A_CONFIG_PATH = (
|
||
SKILLS
|
||
/ "evaluate-frozen-replay"
|
||
/ "configs"
|
||
/ "writer-gate-a-deep-space-v1.json"
|
||
)
|
||
GATE_A_WRITER_PROFILE = json.loads(GATE_A_CONFIG_PATH.read_text(encoding="utf-8"))[
|
||
"executionProfiles"]["writer"]
|
||
SYSTEM_PROMPT = GATE_A_WRITER_PROFILE["systemPrompt"]
|
||
SYSTEM_PROMPT_ID = GATE_A_WRITER_PROFILE["systemPromptId"]
|
||
SYSTEM_PROMPT_SHA256 = "sha256:" + hashlib.sha256(SYSTEM_PROMPT.encode("utf-8")).hexdigest()
|
||
assert SYSTEM_PROMPT_SHA256 == GATE_A_WRITER_PROFILE["systemPromptSha256"], \
|
||
"config 里的 systemPrompt 与其冻结 sha256 不一致,停止(事实来源已漂移)"
|
||
|
||
ARTIFACTS_DIR = SCRIPT_DIR / "artifacts"
|
||
|
||
# 第 1 章细纲硬骨架(与库内 planning section id=5 confirmed payload 同源)
|
||
HARD_CONSTRAINTS = [
|
||
"主角是天才机师,初次驾驶生物机甲。",
|
||
"深渊异族(克苏鲁式)首次出现,营造微恐/精神压迫。",
|
||
"建立黑暗森林/舰队/巨舰的世界观基调。",
|
||
"章末钩子:主角觉醒生物机甲的代价(精神污染的预兆)。",
|
||
]
|
||
ADJUSTABLE_BEATS = [
|
||
"开场节奏(侦察船日常 → 异常信号 → 异族现身)可调整。",
|
||
"精神压迫的描写强度可调整(微恐基调)。",
|
||
]
|
||
DECLARED_NEW_FACTS = [
|
||
{"factId": "fact-ch1-1", "text": "主角林深:天才机师。"},
|
||
{"factId": "fact-ch1-2", "text": "生物机甲:以异种核心驱动,机师与机甲精神链接。"},
|
||
{"factId": "fact-ch1-3", "text": "深渊异族:克苏鲁式,造成精神污染。"},
|
||
]
|
||
|
||
# 篇幅合同:细纲显式 targetChars=4000,区间按生产算法 = 目标值 ±30%(硬边界 2000-10000)
|
||
LENGTH_CONTRACT = {"targetChars": 4000, "minChars": 2800, "maxChars": 5200,
|
||
"frontmatterRequired": False}
|
||
|
||
# 文风约束:投影自设定包 style 字段(planning section id=3)。第 1 章无历史原文摘录,
|
||
# 文风画像是唯一文风锚点;生产投影从第 2 章起有 proseEvidence,本字段可为空。
|
||
STYLE_TEXT = ("冷峻、克制、微恐。战斗场面宏大(舰队/巨舰/机甲),心理描写细腻"
|
||
"(克苏鲁式精神压迫)。句式偏短,节奏紧。")
|
||
|
||
# 创作输入——严格按 build_writer_creative_input 的生产投影形状
|
||
CREATIVE_INPUT = {
|
||
"fineOutline": {
|
||
"hardConstraints": HARD_CONSTRAINTS,
|
||
"adjustableBeats": ADJUSTABLE_BEATS,
|
||
"declaredNewFacts": DECLARED_NEW_FACTS,
|
||
},
|
||
"narrativeState": {
|
||
"time": "故事开篇",
|
||
"location": "深渊星域,人类舰队侦察船",
|
||
"characterPositions": {"林深": "侦察船"},
|
||
"immediateSituation": "侦察船在深渊星域捕获异常信号,未知存在逼近。",
|
||
},
|
||
"factConstraints": [],
|
||
"proseExcerpts": [],
|
||
"patternReferences": [],
|
||
"lengthContract": LENGTH_CONTRACT,
|
||
"styleConstraints": [{"styleId": "style-baseline-v1", "text": STYLE_TEXT}],
|
||
}
|
||
|
||
|
||
def make_logging_runner(log_path: Path):
|
||
"""透传 subprocess.run,只把 sandbox 调用现场原样记到留档文件(审计用)。"""
|
||
|
||
def runner(command, *, input, text, capture_output, timeout, check, cwd, env,
|
||
start_new_session):
|
||
record = {"command": list(command), "envKeys": sorted(env.keys()), "cwd": cwd,
|
||
"inputChars": len(input), "startedAt": str(datetime.now())}
|
||
try:
|
||
completed = subprocess.run(list(command), input=input, text=text,
|
||
capture_output=capture_output, timeout=timeout,
|
||
check=check, cwd=cwd, env=dict(env),
|
||
start_new_session=start_new_session)
|
||
except subprocess.TimeoutExpired as exc:
|
||
record["exception"] = f"TimeoutExpired: {exc}"
|
||
log_path.write_text(json.dumps(record, ensure_ascii=False, indent=1),
|
||
encoding="utf-8")
|
||
raise
|
||
record.update({"returncode": completed.returncode,
|
||
"stdout": completed.stdout or "", "stderr": completed.stderr or ""})
|
||
log_path.write_text(json.dumps(record, ensure_ascii=False, indent=1), encoding="utf-8")
|
||
return completed
|
||
|
||
return runner
|
||
|
||
|
||
def main():
|
||
run_id = f"run-prod-work12-ch1-{uuid.uuid4().hex[:8]}"
|
||
ARTIFACTS_DIR.mkdir(exist_ok=True)
|
||
(ARTIFACTS_DIR / f"{run_id}-creative-input.json").write_text(
|
||
json.dumps(CREATIVE_INPUT, ensure_ascii=False, indent=1), encoding="utf-8")
|
||
envelope_log = ARTIFACTS_DIR / f"{run_id}-sandbox-call.json"
|
||
|
||
profile = build_writer_execution_profile(
|
||
claude_executable_path=CLAUDE_BIN, claude_executable_sha256=CLAUDE_SHA256,
|
||
claude_cli_version=CLAUDE_VERSION, resolved_model_id=RESOLVED_MODEL_ID,
|
||
effort="high", max_budget_usd_per_call=Decimal("5.0"), timeout_seconds=1200,
|
||
max_context_chars=200000, system_prompt=SYSTEM_PROMPT)
|
||
|
||
print(f"writer 真调开始: run_id={run_id}, 模型={RESOLVED_MODEL_ID}, "
|
||
f"提示词={SYSTEM_PROMPT_ID}, sandbox=on")
|
||
started = datetime.now()
|
||
invocation = run_claude(profile, CREATIVE_INPUT,
|
||
runner=make_logging_runner(envelope_log),
|
||
business_validator=validate_writer_draft)
|
||
finished = datetime.now()
|
||
|
||
receipt = invocation.receipt
|
||
body = normalize_text(invocation.structured_output["candidateBody"])
|
||
actual_han = han_count(body)
|
||
candidate_sha256 = hashlib.sha256(body.encode("utf-8")).hexdigest()
|
||
|
||
print("── 回执 ──")
|
||
print(f" actual_model_id: {receipt.actual_model_id} model_match: {receipt.model_match}")
|
||
print(f" terminal_reason: {receipt.terminal_reason} stop_reason: {receipt.stop_reason} "
|
||
f"is_error: {receipt.is_error} exit_code: {receipt.exit_code}")
|
||
print(f" total_cost_usd: {receipt.total_cost_usd} usage: "
|
||
f"{json.dumps(dict(receipt.usage or {}), ensure_ascii=False)[:160]}")
|
||
print(f"── 候选 ──\n 汉字数: {actual_han}(合同 {LENGTH_CONTRACT['minChars']}"
|
||
f"-{LENGTH_CONTRACT['maxChars']},目标 {LENGTH_CONTRACT['targetChars']})")
|
||
print(f" sha256: {candidate_sha256[:16]}...")
|
||
print(f" 正文前 120 字: {body[:120]}")
|
||
|
||
# 落库前硬门:篇幅不出界才落库(3 字级垃圾不许入库污染看板)
|
||
if not LENGTH_CONTRACT["minChars"] <= actual_han <= LENGTH_CONTRACT["maxChars"]:
|
||
print(f"[拒绝落库] 汉字数 {actual_han} 超出篇幅合同,候选只留档 artifacts 不入库")
|
||
raise SystemExit(1)
|
||
if not receipt.model_match:
|
||
print(f"[拒绝落库] 实际模型与冻结模型 {RESOLVED_MODEL_ID} 不一致")
|
||
raise SystemExit(1)
|
||
|
||
# ── 落库:run + 候选 + 回执 + 实跑长度检测(同事务)──
|
||
with connect() as conn:
|
||
conn.execute(
|
||
"INSERT INTO example_run(run_id, work_id, target_chapter, trigger_source, "
|
||
"trigger_detail, started_at, finished_at, terminal_state, creator) "
|
||
"VALUES (%s,%s,%s,'user',%s,%s,%s,'completed','writer')",
|
||
(run_id, WORK_ID, TARGET_CHAPTER,
|
||
json.dumps({"note": "第1章基线:writer 真子进程(sandbox);asOf 合同不适用,"
|
||
"未走生产组装与门",
|
||
"systemPromptId": SYSTEM_PROMPT_ID,
|
||
"systemPromptSha256": SYSTEM_PROMPT_SHA256,
|
||
"executionProfileSha256": receipt.execution_profile_sha256,
|
||
"artifact": envelope_log.name}, ensure_ascii=False),
|
||
started, finished))
|
||
cand_id = conn.execute(
|
||
"INSERT INTO example_candidate(work_id, target_chapter, run_id, attempt, run_type, "
|
||
"candidate_version, candidate_sha256, candidate_body, state, mode, source_role, "
|
||
"acceptance_eligible, creator) "
|
||
"VALUES (%s,%s,%s,1,'production','1',%s,%s,'passed','continuation','writer',true,"
|
||
"'writer') RETURNING id",
|
||
(WORK_ID, TARGET_CHAPTER, run_id, candidate_sha256, body)).fetchone()[0]
|
||
receipt_id = conn.execute(
|
||
"INSERT INTO example_run_receipt(run_id, sample_id, revision, adapter_role, "
|
||
"stage_kind, attempt, candidate_version, candidate_sha256, requested_model_id, "
|
||
"actual_model_id, model_match, effort, total_cost_usd, usage, stop_reason, "
|
||
"terminal_reason, is_error, creator) "
|
||
"VALUES (%s,'writer-ch1',1,'writer','generation',1,'1',%s,%s,%s,%s,'high',%s,"
|
||
"%s::jsonb,%s,%s,%s,'writer') RETURNING id",
|
||
(run_id, candidate_sha256, receipt.requested_model_id, receipt.actual_model_id,
|
||
receipt.model_match, str(receipt.total_cost_usd),
|
||
json.dumps(dict(receipt.usage or {}), ensure_ascii=False),
|
||
receipt.stop_reason, receipt.terminal_reason,
|
||
bool(receipt.is_error))).fetchone()[0]
|
||
# 长度检测是脚本真跑过的机械检查,如实落账;语义门对基线章不适用,不伪造通过。
|
||
conn.execute(
|
||
"INSERT INTO example_quality_result(run_id, receipt_id, candidate_sha256, "
|
||
"judge_kind, dimension, scale_version, conclusion, detail, creator) "
|
||
"VALUES (%s,%s,%s,'detection','length','writer-production-v1','pass',%s::jsonb,"
|
||
"'writer')",
|
||
(run_id, receipt_id, candidate_sha256,
|
||
json.dumps({"actualHanChars": actual_han, **LENGTH_CONTRACT,
|
||
"note": "基线章:仅篇幅机械检查;无 WriterContext,语义门不适用"},
|
||
ensure_ascii=False)))
|
||
conn.commit()
|
||
print(f"候选落库: candidate_id={cand_id}, run_id={run_id}")
|
||
|
||
# 接受为正式正文(confirm 唯一通道)——第 1 章无正文块,expected_revision=0
|
||
accept_result = accept(
|
||
cand_id, decided_by="1",
|
||
rationale=("开篇基线章:writer 真子进程产出(sandbox 隔离),联合回执校验与篇幅"
|
||
"机械检查通过。第 1 章无前文,asOf 合同无解,生产组装与机械/语义门不适用;"
|
||
"文风锚定设定包 style。"),
|
||
basis_ref="细纲@v1 (planning_section id=5)",
|
||
expected_revision=0,
|
||
command_id=f"accept-work12-ch1-{run_id}")
|
||
print(f"接受结果: {json.dumps(accept_result, ensure_ascii=False)}")
|
||
print(f"\nCANDIDATE_ID={cand_id}\nRUN_ID={run_id}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|