@optima-chat/dev-skills 0.16.2 → 0.16.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/.claude/commands/logs.md +60 -3
  2. package/.claude/commands/trace-user.md +18 -6
  3. package/.claude/skills/cn-deploy/SKILL.md +5 -3
  4. package/.claude/skills/gateway-admin/SKILL.md +15 -0
  5. package/.claude/skills/logs/SKILL.md +10 -2
  6. package/.claude/skills/reset-onboarding/SKILL.md +116 -0
  7. package/.claude/skills/yzsgo-e2e/SKILL.md +51 -0
  8. package/.claude/skills/yzsgo-e2e/SYNC.md +10 -0
  9. package/.claude/skills/yzsgo-e2e/chat_driver.py +659 -0
  10. package/.claude/skills/yzsgo-e2e/judge_outcome.js +11 -0
  11. package/.claude/skills/yzsgo-e2e/judge_workflow.js +100 -0
  12. package/.claude/skills/yzsgo-e2e/preflight.py +44 -0
  13. package/.claude/skills/yzsgo-e2e/prep_conversation.py +21 -0
  14. package/.claude/skills/yzsgo-e2e/pull_wire.py +312 -0
  15. package/.claude/skills/yzsgo-e2e/run_e2e.py +122 -0
  16. package/.claude/skills/yzsgo-e2e/verify_drift.py +42 -0
  17. package/.codex/skills/cn-deploy/SKILL.md +5 -3
  18. package/.codex/skills/reset-onboarding/SKILL.md +116 -0
  19. package/.codex/skills/yzsgo-e2e/SKILL.md +51 -0
  20. package/.codex/skills/yzsgo-e2e/SYNC.md +10 -0
  21. package/.codex/skills/yzsgo-e2e/chat_driver.py +659 -0
  22. package/.codex/skills/yzsgo-e2e/judge_outcome.js +11 -0
  23. package/.codex/skills/yzsgo-e2e/judge_workflow.js +100 -0
  24. package/.codex/skills/yzsgo-e2e/preflight.py +44 -0
  25. package/.codex/skills/yzsgo-e2e/prep_conversation.py +21 -0
  26. package/.codex/skills/yzsgo-e2e/pull_wire.py +312 -0
  27. package/.codex/skills/yzsgo-e2e/run_e2e.py +122 -0
  28. package/.codex/skills/yzsgo-e2e/verify_drift.py +42 -0
  29. package/AGENTS.md +12 -6
  30. package/README.md +74 -61
  31. package/bin/helpers/cn-deploy.ts +44 -37
  32. package/bin/helpers/logs.ts +464 -31
  33. package/dist/bin/helpers/cn-deploy.js +43 -37
  34. package/dist/bin/helpers/logs.js +410 -31
  35. package/docs/superpowers/plans/2026-08-31-yzsgo-e2e.md +969 -0
  36. package/docs/superpowers/specs/2026-08-31-yzsgo-e2e-design.md +154 -0
  37. package/package.json +1 -1
@@ -0,0 +1,11 @@
1
+ // yzsgo-e2e 三态裁决纯函数(node --test 覆盖)。逻辑与 judge_workflow.js 内联副本保持一致。
2
+ function decideOutcome(votes) {
3
+ const good = (votes || []).filter(Boolean);
4
+ const n = good.length;
5
+ const refuted = good.filter((v) => v.refuted).length;
6
+ if (n === 0) return "needs_review";
7
+ if (refuted === n) return "rejected";
8
+ if (refuted > n / 2) return "needs_review";
9
+ return "confirmed";
10
+ }
11
+ module.exports = { decideOutcome };
@@ -0,0 +1,100 @@
1
+ // .claude/skills/yzsgo-e2e/judge_workflow.js
2
+ // 改编自 optima-gateway conversation-iq/workflow.js(见 SYNC.md)。
3
+ // 输入 args: { base, sids, knownIssues }(备料稿目录 / 待判对话文件名列表 / gh 实时拉的开着 issue 文本)。
4
+
5
+ export const meta = {
6
+ name: "yzsgo-e2e-judge",
7
+ description: "读 e2e 备料稿(wire + 前端证据)逐对话判缺陷,对抗验证出三态",
8
+ phases: [
9
+ { title: "Judge", detail: "每对话一个 agent 读备料稿判缺陷(含前后端一致性)" },
10
+ { title: "Verify", detail: "每条 novel finding 3 个 skeptic 读 runtime 源码反驳,三态裁决" },
11
+ ],
12
+ };
13
+
14
+ // decideOutcome 与 judge_outcome.js 同步(后者有 node 单测)
15
+ function decideOutcome(votes) {
16
+ const good = (votes || []).filter(Boolean);
17
+ const n = good.length;
18
+ const refuted = good.filter((v) => v.refuted).length;
19
+ if (n === 0) return "needs_review";
20
+ if (refuted === n) return "rejected";
21
+ if (refuted > n / 2) return "needs_review";
22
+ return "confirmed";
23
+ }
24
+
25
+ const A = typeof args === "string" ? JSON.parse(args) : args || {};
26
+
27
+ const STABLE_NORMAL = `## 正常现象(不是缺陷,别报):
28
+ - compaction 摘要调用、abort、max_tokens 截断本身;
29
+ - 前端 new_conversation 后主区只有本轮(预期)。`;
30
+
31
+ const JUDGE_SCHEMA = {
32
+ type: "object",
33
+ properties: {
34
+ impression: { type: "string" },
35
+ findings: {
36
+ type: "array",
37
+ items: {
38
+ type: "object",
39
+ properties: {
40
+ what: { type: "string" },
41
+ evidence: { type: "string" },
42
+ knownIssue: { type: "string" },
43
+ },
44
+ required: ["what", "evidence"],
45
+ },
46
+ },
47
+ },
48
+ required: ["impression", "findings"],
49
+ };
50
+
51
+ const VERDICT_SCHEMA = {
52
+ type: "object",
53
+ properties: { refuted: { type: "boolean" }, reason: { type: "string" } },
54
+ required: ["refuted", "reason"],
55
+ };
56
+
57
+ function judgePrompt(path) {
58
+ return `读备料稿 ${path}(含 wire transcript + 「前端所见」证据)。判这次端到端对话有没有网关/agent 缺陷。
59
+ 重点核对:① wire 里 agent 真实产出 vs 前端渲染是否一致(丢内容/半截/前端报错但 wire 成功、或反之);
60
+ ② 悬空 tool_use / max_tokens 截断 / error / abort 是否造成用户可感问题;③ 回答是否编数据/答非所问/活没干完。
61
+ ${STABLE_NORMAL}
62
+ 已知开着的 issue(命中就在 knownIssue 里标 #号,仅打标签、不要因此不报):
63
+ ${A.knownIssues || "(无)"}
64
+ 默认健康:证据不足别硬报。输出 impression + findings。`;
65
+ }
66
+
67
+ function verifyPrompt(f, path) {
68
+ return `有人在 ${path} 报了缺陷:「${f.what}」,证据:${f.evidence}。
69
+ 你是 skeptic:读**真实 runtime 源码**(optima-gateway / agent-runtime)核对机制,尽力反驳。默认判假(refuted=true),
70
+ 只有确凿证明该缺陷真实存在才 refuted=false。输出 refuted + reason。`;
71
+ }
72
+
73
+ const results = await pipeline(
74
+ A.sids,
75
+ (sid) =>
76
+ agent(judgePrompt(`${A.base}/${sid}.md`), {
77
+ label: `judge:${sid}`,
78
+ phase: "Judge",
79
+ schema: JUDGE_SCHEMA,
80
+ }).then((judge) => ({ sid, judge })),
81
+ async (prev) => {
82
+ if (!prev || !prev.judge) return { sid: prev?.sid, confirmed: [], needsReview: [], rejected: [] };
83
+ const path = `${A.base}/${prev.sid}.md`;
84
+ const novel = prev.judge.findings || [];
85
+ if (novel.length === 0)
86
+ return { sid: prev.sid, impression: prev.judge.impression, confirmed: [], needsReview: [], rejected: [] };
87
+ const verified = await parallel(
88
+ novel.map((f) => async () => {
89
+ const votes = await parallel(
90
+ [0, 1, 2].map(() => () => agent(verifyPrompt(f, path), { label: `verify:${prev.sid}`, phase: "Verify", schema: VERDICT_SCHEMA }))
91
+ );
92
+ return { finding: f, outcome: decideOutcome(votes) };
93
+ })
94
+ );
95
+ const by = (o) => verified.filter(Boolean).filter((v) => v.outcome === o);
96
+ return { sid: prev.sid, impression: prev.judge.impression, confirmed: by("confirmed"), needsReview: by("needs_review"), rejected: by("rejected") };
97
+ }
98
+ );
99
+
100
+ return { results: results.filter(Boolean) };
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env python3
2
+ """yzsgo-e2e 环境自检。探测函数有副作用(读端口/文件),汇总用纯函数 summarize_preflight。
3
+ 缺项只指路(不替用户做一次性登录/充值),准备步骤见 store-skills 的 setting-up-yzsgo-test-env。"""
4
+ import os, shutil, socket, sys
5
+
6
+ def _port_open(port: int) -> bool:
7
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
8
+ s.settimeout(1.0)
9
+ return s.connect_ex(("127.0.0.1", port)) == 0
10
+
11
+ def probe(env: str = "cn-prod") -> list[dict]:
12
+ return [
13
+ {"name": "chrome-9222", "ok": _port_open(9222),
14
+ "hint": '起调试端口 Chrome:open -na "Google Chrome" --args --remote-debugging-port=9222 '
15
+ '--user-data-dir=/tmp/yzsgo-chrome https://www.yzsgo.com(手动登测试账号)'},
16
+ {"name": "buildbox-pw", "ok": os.path.exists(os.path.expanduser("~/.buildbox_pw")),
17
+ "hint": "拉 wire 需 buildbox 口令文件 ~/.buildbox_pw(见 setting-up-yzsgo-test-env)"},
18
+ {"name": "sshpass", "ok": shutil.which("sshpass") is not None,
19
+ "hint": "brew install hudochenkov/sshpass/sshpass"},
20
+ {"name": "playwright", "ok": _has_playwright(),
21
+ "hint": "pip install playwright && playwright install chromium"},
22
+ ]
23
+
24
+ def _has_playwright() -> bool:
25
+ try:
26
+ import playwright # noqa: F401
27
+ return True
28
+ except Exception:
29
+ return False
30
+
31
+ def summarize_preflight(checks: list) -> dict:
32
+ ok = all(c["ok"] for c in checks)
33
+ missing = [c["name"] for c in checks if not c["ok"]]
34
+ rows = []
35
+ for c in checks:
36
+ mark = "✅" if c["ok"] else "❌"
37
+ rows.append(f"{mark} {c['name']}" + ("" if c["ok"] else f"\n → {c['hint']}"))
38
+ return {"ok": ok, "missing": missing, "report": "\n".join(rows)}
39
+
40
+ if __name__ == "__main__":
41
+ env = sys.argv[1] if len(sys.argv) > 1 else "cn-prod"
42
+ r = summarize_preflight(probe(env))
43
+ print(r["report"])
44
+ sys.exit(0 if r["ok"] else 1)
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env python3
2
+ """yzsgo-e2e 备料层:wire 侧备料稿(复用 pull_wire.render_conversation)+ 合并浏览器侧证据。
3
+ 改编自 optima-gateway conversation-iq/prep_session.py(备料思路)与 pull_wire(渲染)。见 SYNC.md。"""
4
+ import json
5
+
6
+ def merge_browser_evidence(wire_md: str, browser_turns: list) -> str:
7
+ lines = [wire_md.rstrip(), "",
8
+ "## 前端所见(浏览器侧证据;与上面 wire 对照)",
9
+ "> 判定要求:逐轮核对**前端渲染的**与**wire 里 agent 真实产出的**是否一致——"
10
+ "前端丢内容/半截/报错但 wire 成功(或反之)即为缺陷。", ""]
11
+ for i, t in enumerate(browser_turns):
12
+ lines.append(f"### 轮 #{i} 发送: {t.get('sent','')!r}")
13
+ lines.append(f"- state: {t.get('state','')} timed_out: {t.get('timed_out', False)}")
14
+ tt = t.get("tool_trace")
15
+ if tt:
16
+ lines.append(f"- tool_trace: {json.dumps(tt, ensure_ascii=False)[:1500]}")
17
+ lines.append("- 前端整轮渲染:")
18
+ lines.append("```")
19
+ lines.append((t.get("transcript") or "(空)")[:6000])
20
+ lines.append("```")
21
+ return "\n".join(lines)
@@ -0,0 +1,312 @@
1
+ #!/usr/bin/env python3
2
+ """拉取 + 渲染 yzsgo 会话的 Wire(Optima Gateway #2261 落盘的完整 LLM 上下文)。
3
+
4
+ Wire = 每个 runtime session 的完整 LLM 调用记录,存境内 buildbox 的 NAS:
5
+ /mnt/nas-cn-prod/workspaces/cn-prod/<userId>/.agent/llm-wire/<sessionId>/
6
+ records.jsonl —— 每行一条 {kind:request/response/error, callId, seq, ts, body{system,tools,messages}, finalMessage...}
7
+ blobs/ —— 大内容按 {"$blob": "<hash>"} 引用,deref 时读这里
8
+ TTL 14 天。经 buildbox(47.94.105.163,口令 ~/.buildbox_pw)拉。
9
+
10
+ ⚠️ warm-pool 会把**同一账号顺序跑的多个 web 对话聚在一个 runtime session** 里 —— 一个
11
+ records.jsonl 常含几十个对话。切分靠 seq 复位:agent 每轮 req 的 messages 递增(2,4,6…)、
12
+ seq 递增(1,3,5…),**seq==1(msgs==2)= 新对话开始**。本脚本据此切分,每个对话取「末 req 的
13
+ 全历史 + 末 response」= 该对话的完整 transcript(含推理/工具调用/工具结果/最终回复)。
14
+
15
+ 用法:
16
+ # 拉 + 渲染(默认测试账号,近 1 天)
17
+ pull_wire.py --user <userId> --since 1 --out e2e/wire
18
+ # 已拉过、只重渲染本地数据(迭代用,不再 SSH)
19
+ pull_wire.py --local e2e/wire/raw/llm-wire --out e2e/wire
20
+
21
+ 参考 optima-gateway 的 conversation-iq/prep_session.py(同一 Wire 数据源)。
22
+ """
23
+ import argparse
24
+ import json
25
+ import os
26
+ import subprocess
27
+ import sys
28
+
29
+ BUILDBOX = os.environ.get("OPTIMA_BUILDBOX_HOST", "root@47.94.105.163")
30
+ PW_FILE = os.path.expanduser(os.environ.get("OPTIMA_BUILDBOX_PW", "~/.buildbox_pw"))
31
+ NAS = os.environ.get("OPTIMA_NAS_CN", "/mnt/nas-cn-prod/workspaces/cn-prod")
32
+
33
+
34
+ def ssh(cmd):
35
+ return subprocess.run(
36
+ ["sshpass", "-f", PW_FILE, "ssh", "-o", "StrictHostKeyChecking=no",
37
+ "-o", "ConnectTimeout=25", BUILDBOX, cmd],
38
+ capture_output=True, text=True)
39
+
40
+
41
+ def pull(user, since_days, out):
42
+ """SSH buildbox:打包该 user 近 since_days 天有更新的 wire session,scp 回 out/raw/。"""
43
+ raw = os.path.join(out, "raw")
44
+ os.makedirs(raw, exist_ok=True)
45
+ base = f"{NAS}/{user}/.agent/llm-wire"
46
+ # 只打包近 since_days 天有更新的 session(records.jsonl mtime)
47
+ remote_tar = "/tmp/optima_wire_pull.tgz"
48
+ pack = (f"cd {NAS}/{user}/.agent 2>/dev/null && "
49
+ f"sids=$(find {base}/*/records.jsonl -mtime -{since_days} 2>/dev/null "
50
+ f"| sed 's#/records.jsonl##' | sed 's#.*/##') && "
51
+ f"[ -z \"$sids\" ] && echo NO_SESSIONS && exit 0; "
52
+ f"tar czf {remote_tar} $(for s in $sids; do echo llm-wire/$s; done) && "
53
+ f"echo PACKED $(echo \"$sids\" | wc -w)")
54
+ r = ssh(pack)
55
+ if "NO_SESSIONS" in r.stdout:
56
+ print(f"[pull] {user} 近 {since_days} 天无 wire session(TTL 14 天,或该账号没跑过)")
57
+ return None
58
+ if "PACKED" not in r.stdout:
59
+ print("[pull] 打包失败:", r.stdout, r.stderr, file=sys.stderr)
60
+ return None
61
+ print(f"[pull] 远端打包 {r.stdout.strip().splitlines()[-1]} session")
62
+ local_tar = os.path.join(raw, "wire.tgz")
63
+ subprocess.run(["sshpass", "-f", PW_FILE, "scp", "-o", "StrictHostKeyChecking=no",
64
+ f"{BUILDBOX}:{remote_tar}", local_tar], check=True)
65
+ subprocess.run(["tar", "xzf", local_tar, "-C", raw], check=True)
66
+ ssh(f"rm -f {remote_tar}")
67
+ return os.path.join(raw, "llm-wire")
68
+
69
+
70
+ # ── 渲染(源自 conversation-iq/prep_session.py,扩展为「一个 session 切多个对话」)──
71
+
72
+ def make_deref(session_dir):
73
+ def deref(v):
74
+ if isinstance(v, dict) and "$blob" in v:
75
+ try:
76
+ return json.load(open(os.path.join(session_dir, "blobs", v["$blob"])))
77
+ except Exception:
78
+ return v
79
+ return v
80
+ return deref
81
+
82
+
83
+ def first_user_text(req, deref):
84
+ for m in (deref(req.get("body", {}).get("messages")) or []):
85
+ m = deref(m)
86
+ if m.get("role") == "user":
87
+ c = m.get("content")
88
+ if isinstance(c, str):
89
+ return c
90
+ if isinstance(c, list):
91
+ for b in c:
92
+ b = deref(b)
93
+ if isinstance(b, dict) and b.get("type") == "text":
94
+ return b.get("text", "")
95
+ return "(无 user 消息)"
96
+
97
+
98
+ # 渲染截断上限(审查要逐格核数字 → tool_result 尽量全;截断处标真实长度,别让审查以为"就这些")
99
+ CAP_TEXT = 6000
100
+ CAP_THINK = 3000
101
+ CAP_TOOL_IN = 6000
102
+ CAP_TOOL_OUT = 16000
103
+
104
+
105
+ def _clip(s, cap):
106
+ s = str(s)
107
+ return s if len(s) <= cap else s[:cap] + f" …[截断,共 {len(s)} 字]"
108
+
109
+
110
+ def render_block(b, deref):
111
+ b = deref(b)
112
+ if not isinstance(b, dict):
113
+ return " " + _clip(b, CAP_TOOL_OUT)
114
+ t = b.get("type")
115
+ if t == "text":
116
+ return " [text] " + _clip(b.get("text") or "", CAP_TEXT)
117
+ if t == "thinking":
118
+ return " [thinking] " + _clip(b.get("thinking") or "", CAP_THINK)
119
+ if t == "tool_use":
120
+ return f" [tool_use {b.get('name')}] " + _clip(json.dumps(deref(b.get("input")), ensure_ascii=False), CAP_TOOL_IN)
121
+ if t == "tool_result":
122
+ c = deref(b.get("content"))
123
+ if isinstance(c, list):
124
+ c = " ".join(json.dumps(deref(x), ensure_ascii=False) for x in c)
125
+ return f" [tool_result err={b.get('is_error')}] " + _clip(c, CAP_TOOL_OUT)
126
+ if t == "image":
127
+ return " [image]"
128
+ return " [?] " + _clip(json.dumps(b, ensure_ascii=False), CAP_TOOL_OUT)
129
+
130
+
131
+ def segment(reqs, deref):
132
+ """按 seq==1(新对话起点)把 requests 切成多段对话。返回 [[req,...], ...](各段按 ts 有序)。"""
133
+ reqs = sorted(reqs, key=lambda r: r.get("ts", ""))
134
+ convs, cur = [], []
135
+ for r in reqs:
136
+ if r.get("seq", 1) == 1 and cur:
137
+ convs.append(cur)
138
+ cur = []
139
+ cur.append(r)
140
+ if cur:
141
+ convs.append(cur)
142
+ return convs
143
+
144
+
145
+ def render_conversation(conv, resps, deref, idx):
146
+ """一个对话 = 一串 req(末 req 含全历史)。渲染事实卡 + 完整 transcript。"""
147
+ last = conv[-1]
148
+ prompt = first_user_text(conv[0], deref)
149
+ ts0 = conv[0].get("ts", "")
150
+ # 事实卡
151
+ n_turns = len(conv)
152
+ errs = [resps.get(r["callId"]) for r in conv
153
+ if resps.get(r["callId"]) and resps[r["callId"]].get("kind") == "error"]
154
+ maxtok = sum(1 for r in conv
155
+ if (resps.get(r["callId"]) or {}).get("kind") == "response"
156
+ and ((resps[r["callId"]].get("finalMessage") or {}).get("stopReason") == "max_tokens"))
157
+ # 悬空 tool_use(末 req 全历史)
158
+ msgs = [deref(m) for m in (deref(last["body"].get("messages")) or [])]
159
+ use_ids, res_ids = {}, set()
160
+ for m in msgs:
161
+ c = m.get("content")
162
+ if isinstance(c, list):
163
+ for b in c:
164
+ b = deref(b)
165
+ if isinstance(b, dict):
166
+ if b.get("type") == "tool_use":
167
+ use_ids[b.get("id")] = b.get("name")
168
+ if b.get("type") == "tool_result":
169
+ res_ids.add(b.get("tool_use_id"))
170
+ dangling = [(u, n) for u, n in use_ids.items() if u not in res_ids]
171
+
172
+ L = [f"# 对话 #{idx} {ts0}",
173
+ "", f"**prompt**: {prompt[:200]}", "",
174
+ "## 事实卡(代码算的确定信息)",
175
+ f"- LLM 轮数(req) {n_turns}",
176
+ f"- error 响应 {len(errs)}" + (f":{[json.dumps(deref(e.get('error') or {}),ensure_ascii=False)[:120] for e in errs]}" if errs else ""),
177
+ f"- stop_reason=max_tokens 的响应 {maxtok}",
178
+ f"- 悬空 tool_use(无匹配 result){len(dangling)}: {dangling[:5]}",
179
+ "", "## 完整 transcript(末 req 全历史 + 末 response)", ""]
180
+ # 末 response 拼到历史尾
181
+ lastresp = resps.get(last["callId"])
182
+ if lastresp and lastresp.get("kind") == "response":
183
+ fm = lastresp.get("finalMessage") or {}
184
+ msgs.append({"role": "assistant", "content": fm.get("content", [])})
185
+ for i, m in enumerate(msgs):
186
+ role = m.get("role")
187
+ c = m.get("content")
188
+ L.append(f"\n--- #{i} [{role}] ---")
189
+ if isinstance(c, str):
190
+ L.append(" " + _clip(c, CAP_TEXT))
191
+ elif isinstance(c, list):
192
+ for b in c:
193
+ L.append(render_block(b, deref))
194
+ return prompt, ts0, len(errs), len(dangling), "\n".join(L)
195
+
196
+
197
+ def render_all(wire_root, out):
198
+ conv_dir = os.path.join(out, "conversations")
199
+ os.makedirs(conv_dir, exist_ok=True)
200
+ index = ["# Wire 会话索引", ""]
201
+ sessions = sorted(d for d in os.listdir(wire_root)
202
+ if os.path.isdir(os.path.join(wire_root, d)))
203
+ gidx = 0
204
+ for sid in sessions:
205
+ sdir = os.path.join(wire_root, sid)
206
+ recf = os.path.join(sdir, "records.jsonl")
207
+ if not os.path.exists(recf):
208
+ continue
209
+ deref = make_deref(sdir)
210
+ recs = [json.loads(l) for l in open(recf, encoding="utf-8") if l.strip()]
211
+ reqs = [r for r in recs if r.get("kind") == "request"]
212
+ resps = {r.get("callId"): r for r in recs if r.get("kind") in ("response", "error")}
213
+ convs = segment(reqs, deref)
214
+ index.append(f"\n## session `{sid}` — {len(convs)} 对话 / {len(reqs)} req\n")
215
+ for conv in convs:
216
+ gidx += 1
217
+ prompt, ts0, n_err, n_dang, md = render_conversation(conv, resps, deref, gidx)
218
+ fn = f"{gidx:03d}.md"
219
+ open(os.path.join(conv_dir, fn), "w", encoding="utf-8").write(md)
220
+ flag = (" ⚠️err" if n_err else "") + (" ⚠️dangling" if n_dang else "")
221
+ index.append(f"- [{fn}](conversations/{fn}) [{len(conv):>2} req] {prompt[:64]}{flag}")
222
+ idxpath = os.path.join(out, "index.md")
223
+ open(idxpath, "w", encoding="utf-8").write("\n".join(index))
224
+ print(f"[render] {gidx} 个对话 → {conv_dir}/ 索引 → {idxpath}")
225
+ return idxpath
226
+
227
+
228
+ def main():
229
+ ap = argparse.ArgumentParser()
230
+ ap.add_argument("--user", help="userId(拉该账号的 wire)")
231
+ ap.add_argument("--since", type=int, default=1, help="拉近 N 天有更新的 session(默认 1)")
232
+ ap.add_argument("--out", default="e2e/wire", help="输出目录(默认 e2e/wire)")
233
+ ap.add_argument("--local", help="跳过 SSH,直接渲染这个本地 llm-wire 目录(迭代用)")
234
+ a = ap.parse_args()
235
+
236
+ if a.local:
237
+ render_all(a.local, a.out)
238
+ return
239
+ if not a.user:
240
+ ap.error("需 --user <userId>(或 --local <dir> 只渲染已拉数据)")
241
+ wire_root = pull(a.user, a.since, a.out)
242
+ if wire_root:
243
+ render_all(wire_root, a.out)
244
+
245
+
246
+ # ── yzsgo-e2e 增补:结构化对话索引 + 本次对话定位 ──
247
+
248
+ def emit_conversation_index(wire_root):
249
+ """遍历 wire_root 下各 session,切分对话,产出与 render_all 同序的结构化索引。"""
250
+ out = []
251
+ gidx = 0
252
+ sessions = sorted(d for d in os.listdir(wire_root)
253
+ if os.path.isdir(os.path.join(wire_root, d)))
254
+ for sid in sessions:
255
+ sdir = os.path.join(wire_root, sid)
256
+ recf = os.path.join(sdir, "records.jsonl")
257
+ if not os.path.exists(recf):
258
+ continue
259
+ deref = make_deref(sdir)
260
+ recs = [json.loads(l) for l in open(recf, encoding="utf-8") if l.strip()]
261
+ reqs = [r for r in recs if r.get("kind") == "request"]
262
+ for conv in segment(reqs, deref):
263
+ gidx += 1
264
+ out.append({"gidx": gidx, "sid": sid,
265
+ "ts": conv[0].get("ts", ""),
266
+ "prompt": first_user_text(conv[0], deref)})
267
+ return out
268
+
269
+
270
+ def _ts_key(ts):
271
+ """把 wire 的 ...Z 与 datetime.isoformat 的 ...+00:00 归一成可比较的 aware datetime;解析不了回 None。"""
272
+ from datetime import datetime
273
+ if not ts:
274
+ return None
275
+ try:
276
+ return datetime.fromisoformat(ts.replace("Z", "+00:00"))
277
+ except Exception:
278
+ return None
279
+
280
+
281
+ def locate_conversation(index, started_ts, first_message):
282
+ """按 (started_ts, first_message) 定位本次对话;禁用 ls -t。规则见 plan Task 3 Interfaces。
283
+ 时间比较先把两侧 ISO(Z / +00:00、小数位宽不同)归一成 datetime,避免依赖字典序=数值序的脆弱假设。"""
284
+ key = (first_message or "").strip()[:40]
285
+ cands = []
286
+ for it in index:
287
+ p = (it.get("prompt") or "").strip()
288
+ if not p or not key:
289
+ continue
290
+ if p[:40].startswith(key) or key.startswith(p[:40]):
291
+ cands.append(it)
292
+ if not cands:
293
+ return None
294
+ from datetime import datetime, timezone
295
+ FAR = datetime.max.replace(tzinfo=timezone.utc)
296
+ cands.sort(key=lambda x: _ts_key(x.get("ts", "")) or FAR)
297
+ st = _ts_key(started_ts)
298
+ after = []
299
+ for c in cands:
300
+ ck = _ts_key(c.get("ts", ""))
301
+ if st is not None and ck is not None:
302
+ if ck >= st:
303
+ after.append(c)
304
+ elif c.get("ts", "") >= (started_ts or ""): # 任一解析不了 → 退字符串比较保底
305
+ after.append(c)
306
+ if after:
307
+ return after[0]
308
+ return cands[-1]
309
+
310
+
311
+ if __name__ == "__main__":
312
+ main()
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env python3
2
+ """yzsgo-e2e 编排:驱动对话 → 拉 wire → 定位本次对话 → 备料(含浏览器证据)→ 出备料稿+元数据。
3
+ judge 由 Claude 用 judge_workflow.js 跑;提 issue 由 Claude 依 SKILL.md 用 gh 做。用户不敲本脚本。"""
4
+ import argparse, json, os, sys
5
+ from datetime import datetime, timezone
6
+
7
+ def render_report(result: dict) -> str:
8
+ L = [f"# yzsgo-e2e 报告 · {result['env']} · {result['started_ts']}", ""]
9
+ if result.get("blocked"):
10
+ L.append(f"> ⚠️ blocked: {result['blocked']}(环境问题,非缺陷,未提 issue)")
11
+ L.append("")
12
+ L += ["## 本次端到端问题(按严重度)", "", "| 状态 | 问题 | 证据 |", "|---|---|---|"]
13
+ for f in result.get("confirmed", []):
14
+ L.append(f"| confirmed | {f['what']} | {f['evidence']} |")
15
+ for f in result.get("needs_review", []):
16
+ L.append(f"| needs_review | {f['what']} | {f['evidence']} |")
17
+ if not result.get("confirmed") and not result.get("needs_review"):
18
+ L.append("| — | 无 confirmed/needs_review | — |")
19
+ L += ["", "## 覆盖边界", result.get("coverage", "-"),
20
+ "", "## 判断修正", "(如判定过程中修正过结论,如实记此;无则写'无')"]
21
+ return "\n".join(L)
22
+
23
+ def _utc_now() -> str:
24
+ return datetime.now(timezone.utc).isoformat()
25
+
26
+ def _parse_answers(pairs):
27
+ out = []
28
+ for p in pairs or []:
29
+ if "=" in p:
30
+ k, v = p.split("=", 1)
31
+ out.append({"match": k, "answer": v})
32
+ else:
33
+ print(f"[warn] 忽略格式不对的 --answer {p!r}(应为 关键词=答案)", file=sys.stderr)
34
+ return out
35
+
36
+ def select_conversation_in_session(convs, deref, hit):
37
+ """在同一 session 的 convs 里按 (ts, prompt) 重新定位本次对话;找不到回退末个。
38
+ 不能用跨 session 的全局 gidx 去索引 session-local 列表。"""
39
+ import pull_wire
40
+ for c in convs:
41
+ if not c:
42
+ continue
43
+ if c[0].get("ts", "") == hit.get("ts") and pull_wire.first_user_text(c[0], deref) == hit.get("prompt"):
44
+ return c
45
+ return convs[-1] if convs else None
46
+
47
+ def main():
48
+ ap = argparse.ArgumentParser()
49
+ ap.add_argument("--env", default="cn-prod", choices=["cn-prod", "cn-stage"])
50
+ ap.add_argument("--message", action="append", required=True, help="逐轮发送的消息(可多次)")
51
+ ap.add_argument("--answer", action="append", help="反问预设答案 关键词=答案(可多次)")
52
+ ap.add_argument("--expect", default="", help="关注点,喂给判定层")
53
+ ap.add_argument("--user", required=True, help="测试账号 userId(拉 wire 用)")
54
+ ap.add_argument("--out", default="e2e-out")
55
+ ap.add_argument("--timeout", type=int, default=180,
56
+ help="每轮等回复超时秒;长任务(简报/多工具)调大到 500+")
57
+ ap.add_argument("--since", type=int, default=1, help="拉 wire 的天数窗口(默认 1)")
58
+ ap.add_argument("--issue-repo", default="Optima-Chat/optima-gateway")
59
+ args = ap.parse_args()
60
+
61
+ if args.env == "cn-stage":
62
+ print("[warn] cn-stage 的 wire 取法未验证(spec §8 待核实);仅 cn-prod 全链路已打通。", file=sys.stderr)
63
+
64
+ os.makedirs(args.out, exist_ok=True)
65
+ import preflight, chat_driver, pull_wire, prep_conversation
66
+ pf = preflight.summarize_preflight(preflight.probe(args.env))
67
+ if not pf["ok"]:
68
+ print(pf["report"]); print("\n[preflight 未通过,先按提示准备环境]"); sys.exit(2)
69
+
70
+ started_ts = _utc_now()
71
+ answers = _parse_answers(args.answer)
72
+ d = chat_driver.ChatDriver().attach()
73
+ try:
74
+ d.new_conversation()
75
+ turns = []
76
+ for msg in args.message:
77
+ r = d.send_and_wait(msg, answers=answers, timeout=args.timeout)
78
+ turns.append({"sent": msg, "state": r.get("state"), "transcript": r.get("transcript", ""),
79
+ "tool_trace": r.get("tool_trace"), "timed_out": r.get("timed_out", False)})
80
+ finally:
81
+ d.close()
82
+
83
+ wire_root = pull_wire.pull(args.user, since_days=args.since, out=args.out)
84
+ meta = {"env": args.env, "started_ts": started_ts, "first_message": args.message[0],
85
+ "expect": args.expect, "issue_repo": args.issue_repo, "turns": turns,
86
+ "located": None, "located_reason": None}
87
+ wrote_prepped = False
88
+ if not wire_root:
89
+ meta["located_reason"] = "no_wire_session"
90
+ if wire_root:
91
+ index = pull_wire.emit_conversation_index(wire_root)
92
+ hit = pull_wire.locate_conversation(index, started_ts, args.message[0])
93
+ meta["located"] = hit
94
+ if hit:
95
+ sdir = os.path.join(wire_root, hit["sid"])
96
+ deref = pull_wire.make_deref(sdir)
97
+ with open(os.path.join(sdir, "records.jsonl"), encoding="utf-8") as f:
98
+ recs = [json.loads(l) for l in f if l.strip()]
99
+ reqs = [x for x in recs if x.get("kind") == "request"]
100
+ resps = {x.get("callId"): x for x in recs if x.get("kind") in ("response", "error")}
101
+ convs = pull_wire.segment(reqs, deref)
102
+ conv = select_conversation_in_session(convs, deref, hit)
103
+ if conv is not None:
104
+ _, _, _, _, wire_md = pull_wire.render_conversation(conv, resps, deref, hit["gidx"])
105
+ prepped = prep_conversation.merge_browser_evidence(wire_md, turns)
106
+ with open(os.path.join(args.out, "prepped.md"), "w", encoding="utf-8") as f:
107
+ f.write(prepped)
108
+ wrote_prepped = True
109
+ else:
110
+ meta["located_reason"] = "no_match"
111
+ with open(os.path.join(args.out, "meta.json"), "w", encoding="utf-8") as f:
112
+ f.write(json.dumps(meta, ensure_ascii=False, indent=2))
113
+ if wrote_prepped:
114
+ print(f"[done] 备料稿 → {args.out}/prepped.md 元数据 → {args.out}/meta.json")
115
+ print("下一步:Claude 用 judge_workflow.js 判定,confirmed 提 issue 到", args.issue_repo)
116
+ else:
117
+ reason = "未拉到本账号 wire session(TTL/该窗口没跑过)" if not wire_root else "未能在 wire 里定位到本次对话(ts/prompt 未匹配)"
118
+ print(f"[warn] 只写了 {args.out}/meta.json,未生成 prepped.md:{reason}")
119
+ print(" 检查测试账号 userId / --since 窗口 / 前端是否真落 wire。")
120
+
121
+ if __name__ == "__main__":
122
+ main()
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env python3
2
+ """比对 vendored 文件与上游是否漂移。上游 repo 不在本机则跳过(不破坏自包含)。"""
3
+ import os, subprocess
4
+
5
+ # adapted=True 的文件在上游基础上有本地改动/追加,diff 必然 ≠0 → ⚠️ 是「去看上游有无新变更」而非缺陷;
6
+ # adapted=False 是逐字 vendored,应与上游一致,⚠️ 才是真漂移。
7
+ VENDORED = [
8
+ {"file": "chat_driver.py", "repo": "store-skills", "adapted": False,
9
+ "upstream": "~/optima-store-skills/.claude/skills/operating-yzsgo-chat/chat_driver.py"},
10
+ {"file": "pull_wire.py", "repo": "store-skills", "adapted": True, # 追加了 emit_conversation_index/locate_conversation
11
+ "upstream": "~/optima-store-skills/.claude/skills/pulling-yzsgo-session-wire/pull_wire.py"},
12
+ {"file": "prep_conversation.py", "repo": "gateway", "adapted": True,
13
+ "upstream": "~/optima-gateway/.claude/skills/conversation-iq/prep_session.py"},
14
+ {"file": "judge_workflow.js", "repo": "gateway", "adapted": True,
15
+ "upstream": "~/optima-gateway/.claude/skills/conversation-iq/workflow.js"},
16
+ ]
17
+
18
+ def plan_drift_checks(present: dict) -> list:
19
+ out = []
20
+ for v in VENDORED:
21
+ out.append({"file": v["file"], "upstream": v["upstream"],
22
+ "adapted": v.get("adapted", False),
23
+ "checkable": bool(present.get(v["repo"], False))})
24
+ return out
25
+
26
+ def _present() -> dict:
27
+ return {"store-skills": os.path.isdir(os.path.expanduser("~/optima-store-skills")),
28
+ "gateway": os.path.isdir(os.path.expanduser("~/optima-gateway"))}
29
+
30
+ if __name__ == "__main__":
31
+ here = os.path.dirname(os.path.abspath(__file__))
32
+ for c in plan_drift_checks(_present()):
33
+ if not c["checkable"]:
34
+ print(f"⏭ {c['file']}:上游不在本机,跳过"); continue
35
+ up = os.path.expanduser(c["upstream"])
36
+ r = subprocess.run(["diff", "-q", os.path.join(here, c["file"]), up], capture_output=True, text=True)
37
+ if r.returncode == 0:
38
+ print(f"✅ {c['file']} 与上游一致")
39
+ elif c.get("adapted"):
40
+ print(f"⚠️(改编·预期) {c['file']}:与上游有差异属正常,去看上游有无值得同步的新变更")
41
+ else:
42
+ print(f"⚠️ 漂移 {c['file']} vs {c['upstream']}(逐字 vendored,应一致 → 去同步)")