SLOPSHOPPER

blast-radius

See what a risky command would change before it runs.

newpanebandguardprocess
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · blast-radius
│ ┃ Blast Radius ✕ › fix the failing auth test and add an audit log call │ ┃ Blast Radius: rm -rf build && git push │ ┃ --force origin main ⏺ Read(src/auth.ts) │ ┃ Would delete 3 file(s) () under build. ⎿ Read 6 lines │ ┃ package.json ⏺ Update(src/auth.ts) │ ┃ README.md ⎿ Added 2 lines, removed 1 line │ ┃ src ⏺ Bash(bun test) │ ┃ [ Proceed ] [ Cancel ] ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Blast Radius
Blast Radius: rm -rf build && git push --force origin main Would delete 3 file(s) () under build. package.json README.md src [ Proceed ] [ Cancel ]
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/blast-radius.mjs 128 lines
1// Blast Radius, from "Getting started with Claude Code mods"
2// (https://claude.dev/blog/getting-started-with-claude-code-mods/, Anthropic, 2026-10-01).
3// The tool.call hook adds a waitArgv option for DSH's portable timer; the rest of the module
4// (classify, measure, the pane and band trees) is completed to the post's
5// description of the mod, which is marked where it starts.
6
7// Blast Radius: see what a risky command would change before it runs.
8
9// —— completed to the post's description (not in the published excerpt) ——
10
11const RISKS = [
12  { pattern: /\brm\s+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r)\b/, kind: "delete", what: "delete files recursively" },
13  { pattern: /\bgit\s+reset\s+--hard\b/, kind: "reset", what: "discard uncommitted changes" },
14  { pattern: /\bgit\s+clean\b/, kind: "clean", what: "delete untracked files" },
15  { pattern: /\bgit\s+push\b[^\n]*\s(--force|-f)\b/, kind: "force-push", what: "rewrite a remote branch" },
16  { pattern: /\b(migrate|manage\.py\s+migrate|prisma\s+migrate|rails\s+db:migrate)\b/, kind: "migrate", what: "change the database schema" },
17];
18
19// What the hook is holding: one command at a time, until a button decides.
20let held = null;
21
22export function register(on, { waitArgv = ["sleep", "0.25"] } = {}) {
23  on("tool.call", { tool: "Bash" }, async ($, e, next) => {
24    const risk = classify(String(e.command ?? ""));
25    if (risk === null) return next(e);                 // everything else runs as normal
26
27    const report = await measure($, risk, await $.session.cwd());  // git status, git clean -n, du, ...
28    held = { command: e.command, risk, report, decision: null };
29    const opened = await $.ui.open({ id: "blast-radius", title: "Blast Radius", focus: true });
30    if (!opened.isPlaced) held.where = "band";         // too narrow for a pane: draw above the prompt
31
32    while (held.decision === null && !next.signal.aborted) {
33      await $.process.run(waitArgv);                   // time inside $ calls doesn't count against the hook's time limit
34    }
35    if (held.decision === "proceed") return next(e);   // let it run
36    return { deny: `Blast Radius held this command: the user pressed Cancel. It would have: ${report.summary}.` };
37  });
38
39  // —— completed to the post's description (not in the published excerpt) ——
40
41  on("ui.render", { component: "Pane" }, ($, e, next) => {
42    if (e.requestId !== "blast-radius" || held === null || held.decision !== null) return next(e);
43    return report($, e);
44  });
45
46  on("ui.render", { component: "AbovePrompt" }, ($, e, next) => {
47    if (held === null || held.decision !== null || held.where !== "band") return next(e);
48    return report($, e);
49  });
50}
51
52function classify(command) {
53  const risk = RISKS.find((r) => r.pattern.test(command));
54  return risk ? { kind: risk.kind, what: risk.what, command } : null;
55}
56
57async function measure($, risk, cwd) {
58  const lines = [];
59  let summary = `${risk.what} in ${cwd}`;
60  if (risk.kind === "delete") {
61    const target = risk.command.match(/\brm\s+-\S+\s+(\S+)/)?.[1];
62    const du = await run($, ["du", "-sh", target ?? "."]);
63    const size = du.stdout.trim().split(/\s+/)[0] ?? "?";
64    const count = await run($, ["find", target ?? ".", "-type", "f"]);
65    const files = count.stdout.split("\n").filter(Boolean).length;
66    summary = `delete ${files} file(s) (${size}) under ${target ?? cwd}`;
67    lines.push(...count.stdout.split("\n").filter(Boolean).slice(0, 9));
68  } else if (risk.kind === "reset") {
69    const status = await run($, ["git", "status", "--porcelain"]);
70    const changed = status.stdout.split("\n").filter(Boolean);
71    summary = `discard uncommitted changes to ${changed.length} file(s)`;
72    lines.push(...changed.slice(0, 9));
73  } else if (risk.kind === "clean") {
74    const dry = await run($, ["git", "clean", "-n"]);
75    const removed = dry.stdout.split("\n").filter(Boolean);
76    summary = `delete ${removed.length} untracked path(s)`;
77    lines.push(...removed.slice(0, 9));
78  } else if (risk.kind === "force-push") {
79    const ahead = await run($, ["git", "log", "--oneline", "HEAD..origin/main"]);
80    const commits = ahead.stdout.split("\n").filter(Boolean);
81    summary = `rewrite the remote branch, dropping ${commits.length} commit(s) not on this branch`;
82    lines.push(...commits.slice(0, 9));
83  } else if (risk.kind === "migrate") {
84    const pending = await run($, ["sh", "-c", "python manage.py showmigrations --plan 2>/dev/null | grep '\\[ \\]' || true"]);
85    const migrations = pending.stdout.split("\n").filter(Boolean);
86    summary = `apply ${migrations.length} pending migration(s)`;
87    lines.push(...migrations.slice(0, 9));
88  }
89  return { summary, lines };
90}
91
92// A dry run that fails (no git, no python, no such path) still lets the user decide.
93async function run($, argv) {
94  try {
95    return await $.process.run(argv);
96  } catch {
97    return { exitCode: 1, stdout: "", stderr: "" };
98  }
99}
100
101function decide(decision) {
102  if (held !== null) held.decision = decision;
103}
104
105function report($, e) {
106  const { Box, Text, Button } = $.ui.resolve(e);
107  const detail = held.report.lines.map((line) => Text({ dimColor: true, children: `  ${line}` }));
108  return Box({
109    flexDirection: "column",
110    border: true,
111    borderColor: "yellow",
112    paddingX: 1,
113    children: [
114      Text({ color: "yellow", bold: true, children: `Blast Radius: ${held.command}` }),
115      Text({ children: `Would ${held.report.summary}.` }),
116      ...detail,
117      Box({
118        flexDirection: "row",
119        gap: 2,
120        children: [
121          Button({ label: "Proceed", hotkey: "1", onPress: () => decide("proceed") }),
122          Button({ label: "Cancel", hotkey: "2", onPress: () => decide("cancel") }),
123        ],
124      }),
125    ],
126  });
127}
128