一、技能重组(动作-对象命名) - 旧目录 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)按"框架与创作分开"未入本提交。
98 lines
4.7 KiB
Python
98 lines
4.7 KiB
Python
#!/usr/bin/env python3
|
||
"""sandbox 探针:走官方 run_claude(sandbox-exec + 最小环境)真调一次 claude。
|
||
|
||
目的:核实 claude_runtime 的 sandbox 路径在本机能否跑通(前会话实测空 envelope /
|
||
EPERM,但未留档原始 stdout/stderr)。本探针用一个只记录不改行为的 runner 包住
|
||
subprocess,把完整 command / env 键 / returncode / stdout / stderr 落 artifacts,
|
||
失败即可定位 claude 在 sandbox 里到底缺什么。
|
||
"""
|
||
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
|
||
SKILLS = SCRIPT_DIR.parents[1] / ".claude" / "skills"
|
||
for sub in (
|
||
"write-next-chapter/scripts",
|
||
"execute-claude-task/scripts",
|
||
"assemble-context/scripts",
|
||
):
|
||
p = str(SKILLS / sub)
|
||
if p not in sys.path:
|
||
sys.path.insert(0, p)
|
||
|
||
from claude_runtime import ClaudeRuntimeError, run_claude # noqa: E402
|
||
from run_writer import build_writer_execution_profile # noqa: E402
|
||
from writer_contract import validate_writer_draft # noqa: E402
|
||
|
||
CLAUDE_BIN = "/Users/qingse/.nvm/versions/node/v24.15.0/bin/claude"
|
||
CLAUDE_SHA256 = "ba790279cab6ef77b713864d4bf5f764fcea87d3a3eb7591a41f741e45212b5c"
|
||
GATE_A = json.loads((SKILLS / "evaluate-frozen-replay" / "configs" /
|
||
"writer-gate-a-deep-space-v1.json").read_text(encoding="utf-8"))
|
||
WRITER_PROFILE_CFG = GATE_A["executionProfiles"]["writer"]
|
||
ARTIFACTS = SCRIPT_DIR / "artifacts"
|
||
ARTIFACTS.mkdir(exist_ok=True)
|
||
|
||
LOG_PATH = ARTIFACTS / f"probe-sandbox-{uuid.uuid4().hex[:8]}.json"
|
||
|
||
|
||
def logging_runner(command, *, input, text, capture_output, timeout, check, cwd, env,
|
||
start_new_session):
|
||
"""透传 subprocess.run,只把现场原样记到留档文件。"""
|
||
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
|
||
|
||
|
||
def main():
|
||
profile = build_writer_execution_profile(
|
||
claude_executable_path=CLAUDE_BIN, claude_executable_sha256=CLAUDE_SHA256,
|
||
claude_cli_version="2.1.211", resolved_model_id="claude-opus-4-8[1m]",
|
||
effort="high", max_budget_usd_per_call=Decimal("1.0"), timeout_seconds=300,
|
||
max_context_chars=10000, system_prompt=WRITER_PROFILE_CFG["systemPrompt"])
|
||
probe_input = {"fineOutline": {"hardConstraints": ["写一句至少二十字的测试正文。"],
|
||
"adjustableBeats": [], "declaredNewFacts": []},
|
||
"narrativeState": {"time": "", "location": "", "characterPositions": {},
|
||
"immediateSituation": "探针"},
|
||
"factConstraints": [], "proseExcerpts": [], "patternReferences": [],
|
||
"lengthContract": {"targetChars": 40, "minChars": 10, "maxChars": 200,
|
||
"frontmatterRequired": False},
|
||
"styleConstraints": []}
|
||
try:
|
||
invocation = run_claude(profile, probe_input, runner=logging_runner,
|
||
business_validator=validate_writer_draft)
|
||
except ClaudeRuntimeError as exc:
|
||
print(f"[探针失败] primary={exc.primary_code}")
|
||
print(f" causes: {sorted(exc.causes)}")
|
||
receipt = exc.details.get("executionReceipt") or {}
|
||
print(f" receipt: exitCode={receipt.get('exitCode')} "
|
||
f"actualModelId={receipt.get('actualModelId')} "
|
||
f"terminalReason={receipt.get('terminalReason')}")
|
||
print(f" 现场留档: {LOG_PATH}")
|
||
raise SystemExit(1)
|
||
print("[探针通过] sandbox 路径可跑通")
|
||
print(f" structured_output: {json.dumps(invocation.structured_output, ensure_ascii=False)[:200]}")
|
||
print(f" actual_model_id: {invocation.receipt.actual_model_id}")
|
||
print(f" total_cost_usd: {invocation.receipt.total_cost_usd}")
|
||
print(f" 现场留档: {LOG_PATH}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|