SLOPSHOPPER

replay-theater

Step through the last turn's edits, one diff at a time.

newpanebandguardcommand
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · replay-theater
│ ┃ Replay Theater ✕ › fix the failing auth test and add an audit log call │ ┃ Replay Theater 1 2 3 │ ┃ /work/app/src/auth.ts (step 1 of 3) ⏺ Read(src/auth.ts) │ ┃ - if (!claims) throw new Error('invalid ⎿ Read 6 lines │ ┃ token') ⏺ Update(src/auth.ts) │ ┃ + if (!claims || claims.exp < Date.now() / ⎿ Added 2 lines, removed 1 line │ ┃ 1000) throw new Error('invalid token') ⏺ Bash(bun test) │ ┃ + await audit('refresh', claims.sub) ⎿ 3 pass, 1 fail │ ┃ [ Prev ] [ Next ] [ Close ] │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /replay │ ⎿ replay-theater: Replaying │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Replay Theater
Replay Theater 1 2 3 /work/app/src/auth.ts (step 1 of 3) - if (!claims) throw new Error('invalid token') + if (!claims || claims.exp < Date.now() / 1000) throw new Error('invalid token') + await audit('refresh', claims.sub) [ Prev ] [ Next ] [ Close ]
README

ADYA Agent Harness

ADYA is a compact governance and product layer built around the existing registry surfaces, with a deterministic local mode and a DSH-native execution mode.

What is implemented

  • A server-enforced governance decision for goals, agents, skills, MCP capabilities, and tool calls.
  • Agent contracts with roles, responsibilities, allowed tools, allowed skills, constraints, and acceptance criteria.
  • Dynamic role selection and temporary-agent lifecycle cleanup with a zero-active-agent invariant.
  • Executable existing MCP, skill, and tool registry entries with input-schema validation.
  • Evidence-based review that returns completed only when evidence exists, otherwise needs_review or a persisted failure state.
  • JSON-backed retrieval memory, feedback adaptation, restart/reload retrieval, and admin correction/deletion.
  • Separate user and admin planes with user-owned runs and fail-closed admin-token authorization.
  • DSH-native ADYA_LLM_MODE=dsh execution through the checked-out DeepSeek Harness Node SDK: DSH owns the Agent loop, workflow, temporary child Agents, model-facing tools, and sessions; ADYA owns the product API, contracts, native capability policy, evidence projection, memory, and the HTTP/user/admin planes.

The existing DSH tool registry, workflow engine, subagent lifecycle, and session persistence are reused. The ADYA native plugin adds one governed capability surface over the existing DSH registry instead of maintaining a second agent executor or an HTTP bridge.

Run locally

$env:ADYA_ADMIN_TOKEN = "replace-with-a-long-random-local-token"
$env:ADYA_DATA_FILE = "data/state.json"
python server.py

Open http://localhost:8080. Send X-User-Id: alice when using the user plane. Send X-Admin-Token for admin operations.

Deterministic demo workflow

The default mode is dependency-free and deterministic. It is explicitly labeled local-demo; its temporary graph agents are simulated ADYA demo agents, not proof of DSH-native execution.

  1. Start the server with the command above.
  2. Submit a multi-step goal:
$body = @{ goal = "Build a governed registry-backed multi-agent memory demo"; requested_agents = 5 } | ConvertTo-Json
Invoke-RestMethod http://localhost:8080/api/runs -Method Post -Headers @{ "X-User-Id" = "alice" } -ContentType "application/json" -Body $body
  1. Inspect the returned graph.nodes, events, governance, artifacts.review, memory_hits, and cleanup event.
  2. Add feedback, then run another goal to retrieve the stored experience:
$feedback = @{ run_id = "<run-id-from-response>"; text = "Prefer registry evidence before synthesis"; score = 5 } | ConvertTo-Json
Invoke-RestMethod http://localhost:8080/api/feedback -Method Post -Headers @{ "X-User-Id" = "alice" } -ContentType "application/json" -Body $feedback
$body = @{ goal = "Use prior registry memory to build another governed demo" } | ConvertTo-Json
Invoke-RestMethod http://localhost:8080/api/runs -Method Post -Headers @{ "X-User-Id" = "alice" } -ContentType "application/json" -Body $body
  1. Set ADYA_ADMIN_TOKEN and use the admin plane to inspect /api/admin/audit, /api/admin/errors, registries, contracts, and memory.

DSH mode

Set ADYA_LLM_MODE=dsh and follow docs/setup.md. This path fails closed if the checked-out DSH runtime is unavailable; it does not fall back to the deterministic local orchestrator.

Check the implementation

python -m unittest discover -s tests -v
python -m pytest -q
python -m py_compile harness.py server.py adya-share/harness.py adya-share/server.py

The architecture and configuration details are in docs/architecture.md and docs/setup.md. The native DSH acceptance test is conditional; run it with ADYA_DSH_ACCEPTANCE=1 when Node and the checked-out DSH source runtime are available. The checked-in fixture is keyless; a live provider run is a separate, unclaimed check.

Deployment status

The service is container-ready through Dockerfile, but no GCP deployment is claimed here. A GCP project, billing/free-tier choice, region, and authenticated gcloud account must be confirmed before an external deployment is performed. JSON state on local disk is suitable for a demo, not a multi-instance Cloud Run deployment without a durable datastore.

Source 1 files
hooks/replay-theater.mjs 134 lines
1// Replay Theater, from "Getting started with Claude Code mods"
2// (https://claude.dev/blog/getting-started-with-claude-code-mods/, Anthropic, 2026-10-01).
3// The post publishes the five hooks that record edits and register /replay
4// unchanged; the rest of the module (stepsFor, the diff, openReplay, the pane
5// and the hint) is completed to the post's description, marked where it starts.
6
7// Replay Theater: step through the last turn's edits, one diff at a time.
8
9// —— completed to the post's description (not in the published excerpt) ——
10
11const EDIT_TOOLS = new Set(["Edit", "Write"]);
12
13// Edits of the running turn, the sealed replay of the last one, and where the pane is.
14const state = { pending: [], replay: [], index: 0, open: false, placed: false };
15
16export function register(on) {
17  on("tool.call", async ($, e, next) => {
18    if (EDIT_TOOLS.has(e.tool)) state.pending.push(...(await stepsFor($, e)));  // old/new text → diff
19    return next(e);                                                              // the edit runs untouched
20  });
21
22  on("turn.start", ($, e, next) => { if (!e.agentId) state.pending = []; return next(e); });
23
24  on("turn.complete", async ($, e, next) => {
25    const r = await next(e);
26    if (!e.agentId && state.pending.length) state.replay = state.pending;       // one replay per turn
27    return r;
28  });
29
30  on("session.start", async ($, e, next) => {
31    const r = await next(e);
32    await $.command.register({ name: "replay", description: "Step through the last turn's file edits" });
33    return r;
34  });
35  on("command.run", { command: "replay" }, async ($, e) => ({ text: (await openReplay($)) ? "Replaying" : "No edits" }));
36
37  // —— completed to the post's description (not in the published excerpt) ——
38
39  on("ui.render", { component: "Pane" }, ($, e, next) => {
40    if (e.requestId !== "replay" || !state.open) return next(e);
41    return theater($, e);
42  });
43
44  on("ui.render", { component: "AbovePrompt" }, ($, e, next) => {
45    if (state.open && !state.placed) return theater($, e);          // no pane could be placed: the same tree, inline
46    if (state.open || state.replay.length === 0) return next(e);
47    const { Box, Text, Button } = $.ui.resolve(e);
48    const n = state.replay.length;
49    return Box({
50      flexDirection: "row",
51      paddingX: 1,
52      gap: 1,
53      children: [
54        Text({ color: "magenta", bold: true, children: "Replay Theater" }),
55        Text({ dimColor: true, children: `${n} edit${n === 1 ? "" : "s"} this turn — press r or type /replay` }),
56        Button({ label: "Replay", hotkey: "r", onPress: () => openReplay($) }),
57      ],
58    });
59  });
60}
61
62// One replay step per edit: the file, and the text before and after.
63async function stepsFor($, e) {
64  const file = String(e.file_path ?? "");
65  if (e.tool === "Edit") {
66    return [{ file, before: String(e.old_string ?? ""), after: String(e.new_string ?? "") }];
67  }
68  // For a Write, the old contents are read just before the write lands, so the diff is real.
69  const before = await $.fs.read(file).catch(() => "");
70  return [{ file, before, after: String(e.content ?? "") }];
71}
72
73async function openReplay($) {
74  if (state.replay.length === 0) return false;
75  state.index = 0;
76  state.open = true;
77  const opened = await $.ui.open({ id: "replay", title: "Replay Theater", focus: true });
78  state.placed = opened.isPlaced;
79  $.ui.invalidate("ui.render");
80  return true;
81}
82
83function step(delta, $) {
84  state.index = Math.min(Math.max(state.index + delta, 0), state.replay.length - 1);
85  $.ui.invalidate("ui.render");
86}
87
88async function close($) {
89  state.open = false;
90  await $.ui.close({ id: "replay" });
91  $.ui.invalidate("ui.render");
92}
93
94// Lines the edit removed and added, as a unified diff shows them.
95function diffLines(before, after) {
96  const old = before.split("\n");
97  const neu = after.split("\n");
98  const common = new Set(neu);
99  const commonOld = new Set(old);
100  return [
101    ...old.filter((line) => !common.has(line)).map((line) => ({ sign: "-", line })),
102    ...neu.filter((line) => !commonOld.has(line)).map((line) => ({ sign: "+", line })),
103  ];
104}
105
106function theater($, e) {
107  const { Box, Text, Button } = $.ui.resolve(e);
108  const current = state.replay[state.index];
109  const strip = state.replay.map((_, i) => Text({ color: i === state.index ? "magenta" : undefined, bold: i === state.index, children: ` ${i + 1} ` }));
110  const diff = diffLines(current.before, current.after).map(({ sign, line }) =>
111    Text({ color: sign === "+" ? "green" : "red", children: `${sign} ${line}` }),
112  );
113  return Box({
114    flexDirection: "column",
115    border: true,
116    borderColor: "magenta",
117    paddingX: 1,
118    children: [
119      Box({ flexDirection: "row", children: [Text({ bold: true, children: "Replay Theater " }), ...strip] }),
120      Text({ dimColor: true, children: `${current.file}  (step ${state.index + 1} of ${state.replay.length})` }),
121      ...diff,
122      Box({
123        flexDirection: "row",
124        gap: 2,
125        children: [
126          Button({ label: "Prev", hotkey: "p", disabled: state.index === 0, onPress: () => step(-1, $) }),
127          Button({ label: "Next", hotkey: "n", disabled: state.index >= state.replay.length - 1, onPress: () => step(1, $) }),
128          Button({ label: "Close", hotkey: "q", onPress: () => close($) }),
129        ],
130      }),
131    ],
132  });
133}
134