Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"clawdnd-engine": {
"type": "stdio",
"command": "uv",
"alwaysLoad": true,
"args": ["run", "--directory", "${CLAUDE_PLUGIN_ROOT}/servers/engine", "server.py"]
},
"clawdnd-rules": {
Expand Down
61 changes: 61 additions & 0 deletions gaveupfix-vet.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
[uipt-app] run=gaveupfix-vet world=baldurs-gate persona=veteran beats=40 budget=$8.00 part=AB player_agent=claude
[uipt-app] build_sha=f1c796c version=v1.0.3-138-gf1c796c repo=/private/tmp/wos-gate
[uipt-app] === PART A: native-transition gate (re-verifies #356) ===
[A] pkill WorldOSApp + THIS checkout's stale viewers, rm -rf /private/tmp/wos-gate/dist/WorldOS.app, fresh build…
[A] WOS_APP_NO_GLOBAL_KILL=1 — preserving other WorldOSApp processes.
[A] launcher viewer ready on 8766; can_act(before)={"can_act":false,"is_live_view":false,"live":false}
[A] before.png deferred to orchestrator
[A] raising WorldOS to front + CGEvent-clicking the RESUME → PLAY CTA (with retries)…
[A] window X=97.0 Y=36.0 W=1823.0 H=956.0; front='WorldOS'; click=(1701,240)
[A] click posted.
[A] polling for a minted live session (new run dir + can_act:true on a new port; deadline 420s / 140 polls)…
[A] MINTED: run=play-20260602114235 port=8767 can_act=true (after 7 polls)
[A] after.png deferred to orchestrator
[A] tearing down minted backend (run=play-20260602114235) — cold-open was enough.
[A] RESULT: PASS — #356 banner minted a live DM (can_act:true). surface={"can_act":true,"is_live_view":true,"live":true,"campaignId":null,"enabledActions":[]}
<stdin>:23: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
[A] wrote /private/tmp/wos-gate/qa/ui_playtest_runs/gaveupfix-vet/native/transition.json
[uipt-app] === PART B: persona loop on the .app-faithful backend ===
[uipt-app] [B] Playwright not installed at /private/tmp/wos-gate/qa/playwright. Run: (cd qa/playwright && npm install && npx playwright install chromium)
<stdin>:25: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
[uipt-app] === DONE. dir=/private/tmp/wos-gate/qa/ui_playtest_runs/gaveupfix-vet ===
[uipt-app] part A (#356 gate): PASS part B (persona loop): no_playwright
[uipt-app] spend: DM ~$0 + player ~$0 = ~$0.0000 (budget $8.00)
----- run.json -----
{
"run": "gaveupfix-vet",
"world": "baldurs-gate",
"persona": "veteran",
"beats_cap": 40,
"budget_usd": 8.0,
"build_sha": "f1c796c",
"version": "v1.0.3-138-gf1c796c",
"part": "AB",
"part_a": {
"gate": "native_transition_356",
"result": "PASS",
"original_result": "PASS",
"failure_bucket": null,
"failure_detail": null,
"minted_run_dir": "play-20260602114235",
"minted_port": 8767,
"kept_backend_alive": false,
"first_turn_ready": false
},
"part_b": {
"persona_loop": "no_playwright",
"score_pass": false,
"provider": "claude",
"player_agent": "claude",
"original_result": "no_playwright",
"failure_bucket": "no_provider",
"failure_detail": "Playwright palette dependency is missing"
},
"spend_usd": {
"dm_and_companions": 0.0,
"player_agent": 0.0,
"total": 0.0
},
"surface": "BUILT dist/WorldOS.app (part A) + claude provider/claude player backend (part B)",
"at": "2026-06-02T11:42:56Z"
}PLAYTEST_EXIT=1
200 changes: 200 additions & 0 deletions gaveupfix-vet2.log

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions qa/playwright/node_modules
23 changes: 23 additions & 0 deletions qa/playwright/palette_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,29 @@ server.registerTool(
}
);

server.registerTool(
"finish",
{
description:
"End the playtest because you have PLAYED ENOUGH to fairly judge the experience and you are " +
"NOT blocked (use give_up ONLY when you are genuinely stuck). Give your honest overall " +
"satisfaction (1-10) and a 1-2 sentence closing verdict. This is the normal, satisfied way to end.",
inputSchema: {
satisfaction: z.number().int().min(1).max(10).describe("Your honest overall satisfaction, 1-10."),
verdict: z.string().describe("A 1-2 sentence closing verdict."),
},
},
async ({ satisfaction, verdict }) => {
const pg = await ensurePage().catch(() => null);
const sc = pg ? await snap(pg, "finish") : "";
logAction("finish", { satisfaction, verdict: String(verdict).slice(0, 200), screenshot: sc });
try {
fs.writeFileSync(STATUS, JSON.stringify({ ended: true, reason: "finish", satisfaction, detail: String(verdict).slice(0, 500), at: nowIso() }));
} catch (_e) {}
return textResult({ ok: true, ended: true, note: "Run ended — thanks for playing." });
}
);

// Graceful browser teardown on exit.
async function shutdown() {
try {
Expand Down
7 changes: 6 additions & 1 deletion qa/run_duo.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ mkdir -p "$T" "$STATE_DIR"; rm -rf "$STATE_DIR/campaigns" 2>/dev/null
DM_CFG="$STATE_DIR/dm.mcp.json"; PLAYER_CFG="$STATE_DIR/player.mcp.json"
MOVES="$STATE_DIR/player_moves.jsonl"; : > "$MOVES" # the player's structured moves (It.1)
python3 - "$ROOT/qa/qa.mcp.example.json" "$STATE_DIR" "$DM_CFG" "$ROOT" <<'PY'
import json, sys
import json, sys, os
cfg_path, state, out, root = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
cfg = json.load(open(cfg_path))
# RE-ROOT every MCP server's `--directory` at THIS repo ($ROOT) so the DM engine,
Expand All @@ -81,6 +81,11 @@ for name, srv in cfg.get("mcpServers", {}).items():
args[i + 1] = f"{root}/servers/{pkg}"
if name == "clawdnd-engine":
srv.setdefault("env", {})["CLAWDND_STATE_DIR"] = state
# Parity with scripts/play.sh: pin the engine tools (un-defer) so the DM stops burning
# ~2 ToolSearch round-trips/beat re-discovering them. Set CLAWDND_ENGINE_ALWAYSLOAD=0 for
# the deferred baseline (the latency A/B arm).
if os.environ.get("CLAWDND_ENGINE_ALWAYSLOAD", "1") == "1":
srv["alwaysLoad"] = True
json.dump(cfg, open(out, "w"))
PY
# The player gets ONLY the constrained move facade (clawdnd-player): it acts through
Expand Down
4 changes: 2 additions & 2 deletions qa/ui_playtest_app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,7 @@ PY
cat > "$player_prompt" <<EOF
$persona_brief

You have a budget of about $BEATS actions for this whole session. Spend them trying to start and play the story, reporting friction as you go. When you have either (a) genuinely gotten stuck after reporting it, or (b) played a few real turns and seen enough to judge the experience, you may stop — give a final 1-2 sentence verdict and, if you got stuck, call give_up. Start now.
You have a budget of about $BEATS actions for this whole session. Spend them trying to start and play the story, reporting friction as you go. Waiting for the DM to narrate is FREE and does not use your budget — a rich beat can take a few minutes, and the spinner / streaming text is PROGRESS, not a hang, so keep waiting rather than abandoning a beat. When you have played a few real turns and seen enough to judge the experience, call finish with your honest satisfaction (1-10) and a 1-2 sentence verdict. Call give_up ONLY if you are genuinely BLOCKED — a dead control, an error, or the DM truly stalled with no narration — NOT because a beat is slow. Either way, also end your final message with a line exactly like: Satisfaction: N/10. Start now.
EOF
log "[B] player agent starting (agent=$PLAYER_AGENT persona=$PERSONA, ~$BEATS actions, budget \$$player_budget)…"
case "$PLAYER_AGENT" in
Expand Down Expand Up @@ -705,7 +705,7 @@ EOF
-c "mcp_servers.clawdnd-uiplayer.env_vars=[\"CLAWDND_UIPT_URL\",\"CLAWDND_UIPT_RUNDIR\",\"CLAWDND_UIPT_CHANNEL\",\"CLAWDND_UIPT_PERSONA\"]" \
-c "mcp_servers.clawdnd-uiplayer.required=true" \
-c "mcp_servers.clawdnd-uiplayer.default_tools_approval_mode=\"approve\"" \
-c "mcp_servers.clawdnd-uiplayer.enabled_tools=[\"screenshot\",\"a11y_tree\",\"click\",\"type\",\"key\",\"wait\",\"report_bug\",\"give_up\"]" \
-c "mcp_servers.clawdnd-uiplayer.enabled_tools=[\"screenshot\",\"a11y_tree\",\"click\",\"type\",\"key\",\"wait\",\"report_bug\",\"give_up\",\"finish\"]" \
- < "$player_prompt" > "$player_out" 2>> "$PLAYERDIR/player.err"
;;
esac
Expand Down
40 changes: 24 additions & 16 deletions qa/ui_playtest_score.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,23 +147,31 @@ def is_image_404(n: dict) -> bool:
gave_up = status.get("reason") == "give_up"

# --- satisfaction --------------------------------------------------------
satisfaction = extract_satisfaction(verdict)
if satisfaction is None:
# Derive a rough satisfaction when the player didn't state one: start at 8,
# subtract for the friction we measured. (Informational, clearly derived.)
s = 8
if not completed_intro_flow:
s -= 3
if gave_up:
s -= 2
s -= min(3, by_sev.get("critical", 0) * 2 + by_sev.get("major", 0))
s -= min(2, dead_clicks)
if console_errors:
s -= 1
satisfaction = clamp10(s)
satisfaction_source = "derived"
else:
# A `finish` tool call records a structured 1-10 satisfaction in status.json — the most
# reliable self-report (a validated tool arg, impossible to mis-parse). Prefer it; then a
# "N/10" in the verdict text; then derive from measured friction.
status_sat = status.get("satisfaction")
if isinstance(status_sat, (int, float)) and not isinstance(status_sat, bool):
satisfaction = clamp10(int(status_sat))
satisfaction_source = "self-reported"
else:
satisfaction = extract_satisfaction(verdict)
if satisfaction is None:
# Derive a rough satisfaction when the player didn't state one: start at 8,
# subtract for the friction we measured. (Informational, clearly derived.)
s = 8
if not completed_intro_flow:
s -= 3
if gave_up:
s -= 2
s -= min(3, by_sev.get("critical", 0) * 2 + by_sev.get("major", 0))
s -= min(2, dead_clicks)
if console_errors:
s -= 1
satisfaction = clamp10(s)
satisfaction_source = "derived"
else:
satisfaction_source = "self-reported"

critical = by_sev.get("critical", 0)
major = by_sev.get("major", 0)
Expand Down
2 changes: 1 addition & 1 deletion scripts/play.sh
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ python3 - "$ROOT" "$STATE_DIR" "$DM_CFG" <<'PY'
import json, sys
root, state_dir, out = sys.argv[1], sys.argv[2], sys.argv[3]
cfg = {"mcpServers": {
"clawdnd-engine": {"type": "stdio", "command": "uv",
"clawdnd-engine": {"type": "stdio", "command": "uv", "alwaysLoad": True,
"args": ["run", "--directory", f"{root}/servers/engine", "server.py"],
"env": {"CLAWDND_STATE_DIR": state_dir}},
"clawdnd-rules": {"type": "stdio", "command": "uv",
Expand Down
2 changes: 1 addition & 1 deletion scripts/play_party.sh
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ python3 - "$ROOT" "$STATE_DIR" "$DM_CFG" <<'PY'
import json, sys
root, state_dir, out = sys.argv[1], sys.argv[2], sys.argv[3]
cfg = {"mcpServers": {
"clawdnd-engine": {"type": "stdio", "command": "uv",
"clawdnd-engine": {"type": "stdio", "command": "uv", "alwaysLoad": True,
"args": ["run", "--directory", f"{root}/servers/engine", "server.py"],
"env": {"CLAWDND_STATE_DIR": state_dir}},
"clawdnd-rules": {"type": "stdio", "command": "uv",
Expand Down
Loading