Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsCommunityBlog
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

Back to skills

View Run

ASecurity

Generate a visual, LangSmith-style HTML view of an sdlc-graph run from its state file (docs/graph-runs/<run-id>/state.json) — the graph with the current node highlighted, next-step guards, the transition trace, milestones, retry counters vs bounds, and the skipped-gate ledger. Produces a self-contained snapshot HTML (openable and shareable anywhere). One invocation, no arguments: it works on the current directory, and asks which subproject when more than one has runs. Use this skill WHENEVER ...

10 stars
0 votes
0 copies
0 views
Added 9/22/2026
ai-agentspythongobashnoderailstestinggitapi

Works with

terminalcliapi

Security Analysis

A100/100

Scanned 9/22/2026

Install to Claude Code

$npx -y skills add RonMizrahi/ai-sdlc-graph --skill view-run --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of View Run?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for View Run
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/ronmizrahi-view-run/badge)](https://www.skillsdirectory.com/skills/ronmizrahi-view-run)

More formats (shields.io, HTML) on the badges page.

Download Zip
Files
SKILL.md
---
name: view-run
description: >-
  Generate a visual, LangSmith-style HTML view of an sdlc-graph run from its state file
  (docs/graph-runs/<run-id>/state.json) — the graph with the current node highlighted, next-step guards,
  the transition trace, milestones, retry counters vs bounds, and the skipped-gate ledger. Produces
  a self-contained snapshot HTML (openable and shareable anywhere). One invocation, no arguments:
  it works on the current directory, and asks which subproject when more than one has runs. Use this
  skill WHENEVER the user wants to SEE a graph run — "show me the run", "where is the graph now",
  "visualize the state", "open the run viewer", "what's the current state of the sdlc run",
  "generate the run report page" — or asks to watch a run live.
allowed-tools: Read, Write, Bash, Glob
disable-model-invocation: false
---

# View Run

## Goal

Turn `docs/graph-runs/<run-id>/state.json` into something a human can *look at*: the run's position on
the graph, what can fire next and why, the full trace, and the ledger — without reading JSON.

## Use When

- The user wants to **see** a run rather than read its JSON — *"show me the run"*, *"where is the
  graph now"*, *"visualize the state"*, *"open the run viewer"*, *"what's the current state"*.
- They ask to **watch** a run as it happens, or for a page they can leave open.
- **`sdlc-graph` triggers this at run start** when the plugin is installed — that is the common case,
  and it expects the printed summary relayed verbatim.
- They want a **shareable snapshot** of a finished run to attach to a review or a report.

## Do Not Use When

- **Nothing has run yet.** No `docs/graph-runs/*/state.json` means there is nothing to render; say so
  rather than producing an empty page.
- **The question is "is this run sound?"** — that is the graph's own eval suite and the trail in
  `history[]`. This skill renders what the file says; it does not judge it.
- **Something needs changing.** This skill is read-only with respect to the run, always.

## Inputs

**None.** One invocation, always scoped to **the current working directory**. Everything else is
discovered — or asked.

## Workflow

### 1. Discover runs under the current directory

```bash
find . -maxdepth 6 -path '*/docs/graph-runs/*/state.json' \
  -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/.worktrees/*' 2>/dev/null
```

Group the hits by **project directory** — the path prefix before `/docs/graph-runs/`.

- **No hits** → say so and stop. **Do not invent a state file** — a run that never started has
  nothing to view.
- **One project** → proceed with it.
- **More than one project** (a monorepo, nested test repos) → **ask the user which directory**,
  listing each project dir with its newest run id and that run's `status` so the choice is
  informed. Never pick a subproject silently — the newest file across projects is exactly how the
  wrong project's run gets rendered.

Within the chosen project, use the **newest** `docs/graph-runs/*/state.json` — and *say which one* so a stale pick
is visible. If the user's message named a specific run, prefer that match over newest.

### 2. Sanity-check it, honestly

Read the JSON. If it fails to parse, report the parse error and stop. If `schema_version` is absent
or not `5`, still generate — the viewer shows what it can, and an older file degrades to blank
fields rather than wrong ones — but **say which version the file was written against**, and say it
first. The graph itself is stricter: a mismatch there is an unconditional halt with no migration
path, so a viewer quietly rendering a stale file is showing a run the graph would refuse to resume.

### 3. Generate the snapshot

The template is `${CLAUDE_PLUGIN_ROOT}/skills/view-run/viewer/run-viewer.html`. It contains an injection slot:

```js
const INLINE=/*__STATE__*/null/*__END__*/
```

Replace the `null` between the markers with the state JSON, and write the result **next to the
state file** as `docs/graph-runs/<run-id>/view.html`:

All paths below are **relative to the chosen project directory** from step 1 — in a monorepo the
state file lives under the subproject, and the snapshot belongs beside it:

```bash
python3 - "${CLAUDE_PLUGIN_ROOT}/skills/view-run/viewer/run-viewer.html" \
  "<project>/docs/graph-runs/<run-id>/state.json" "<project>/docs/graph-runs/<run-id>/view.html" <<'PY'
import sys, json
tpl, state, out = sys.argv[1], sys.argv[2], sys.argv[3]
s = open(tpl).read()
data = json.dumps(json.load(open(state)))          # parse first: invalid JSON must fail HERE
# The slot is inside a <script> block, and HTML ends that block at the first literal `</script>`
# ANYWHERE — inside a JS string included. json.dumps escapes neither `<` nor `/`, so an observation
# reading `fixed the parser </script><img src=x onerror=…>` used to truncate the script, kill boot(),
# and execute the rest as markup. These three escapes are legal inside a JS string literal and
# JSON.parse back to the identical characters, so the snapshot is unchanged and can no longer close
# its own tag. Run data is data: it reaches the DOM through esc(), never as markup.
data = data.replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026")
marker_a, marker_b = "/*__STATE__*/", "/*__END__*/"
i, j = s.index(marker_a) + len(marker_a), s.index(marker_b)
open(out, "w").write(s[:i] + data + s[j:])
print("wrote", out)
PY
```

The snapshot is **fully self-contained** — no server, no polling, works from `file://`, safe to
attach or archive. Its stall banner is disabled (a snapshot is a moment, not a feed), and its
sidebar shows exactly one run, the one baked in. Both are expected, not defects.

### 4. Deliver it

- Send the generated file to the user (rendered, not as a download card, when the client supports it).
- On macOS, also offer: `open docs/graph-runs/<run-id>/view.html`.

### 5. Live mode — the default when a server can run

**Live is preferred; the snapshot is the fallback and the archive format.** Check whether this
project's viewer server is already up (sdlc-graph starts one per project at run start when this
plugin is installed). If not, start the **bundled server — ~120 lines, zero dependencies, shipped
in this plugin**:

**First, ensure the env file — the configuration must be explicit on disk, never implicit.**

1. **No `sdlc-graph-viewer.env` in the project root** → create it from the example, **filled in**:

   ```bash
   # <project>/sdlc-graph-viewer.env — written by view-run, resolved values, no placeholders
   PROJECT_DIR=<absolute project path>
   RUNS_DIR=<absolute path to the run directories>   # usually $PROJECT_DIR/docs/graph-runs
   POLL_MS=1000
   HOST=127.0.0.1
   # PORT unset on purpose -> stable per-project derived port
   ```

   Defaults are fine — the point is that **every value the server will use is written down**, so
   any agent (or human) can read one file and know exactly what is configured. Nothing secret goes
   in it; committing it is safe.

2. **File already exists** → read it and **verify `PROJECT_DIR` points at this project**. It does →
   use it untouched (the user's `PORT`/`POLL_MS` choices stand). It points elsewhere → say so, and
   start with an inline `PROJECT_DIR=<this project>` override (env beats file) rather than silently
   rewriting the user's file.

Then start the server from the project root — it auto-loads `./sdlc-graph-viewer.env`:

```bash
cd "<project>" && node "${CLAUDE_PLUGIN_ROOT}/skills/view-run/server/server.mjs" &
```

**Configuration is a native `.env` file — nothing is ever edited in code.** The server loads
`$SDLC_GRAPH_VIEWER_ENV`, else `./sdlc-graph-viewer.env` if present, via Node's own `process.loadEnvFile` —
so real environment variables always beat the file (env > file > default):

```bash
# sdlc-graph-viewer.env
PROJECT_DIR=/abs/path/to/project     # where the project lives (default: cwd)
RUNS_DIR=/abs/path/to/docs/graph-runs      # where the run DIRECTORIES live (default: $PROJECT_DIR/docs/graph-runs)
PORT=8477                            # fixed port (default: stable derived per-project port)
POLL_MS=500                          # page poll interval (default: 1000)
HOST=127.0.0.1                       # bind address (default: localhost only)
VIEWER_HTML=/abs/path.html           # viewer page (default: the bundled viewer/run-viewer.html)
PORT_BASE=8400  PORT_RANGE=400  PORT_RETRIES=20   # derived-port parameters
```

**Every setting the server uses comes from the environment** — the code holds only the defaults
above; no path, suffix, or address is assumed anywhere else.

The pages read `POLL_MS` from `GET /api/config` at load — with a 1000ms fallback, so snapshot and
drag-drop modes stay fully serverless. The printed summary names which env file (or `defaults +
env`) is in force.

**The env file is the contract between agent and server.** The skill fills it at spawn (step
above) precisely so nothing about the running server is implicit: what an agent must provide to
have the HTML + server running is exactly that file's contents, and reading it back is how anyone
verifies the configuration. Later user requests ("set the port to 9000") edit the same file —
copy-and-fill from `server/sdlc-graph-viewer.env.example` for any variable not yet present, never
hand-written from memory.

It prints the summary — project, **its own per-project port**, home, live-view and API URLs, and
which config is in force. **Relay it as a complete block, never partially** — every one of these
lines, every time a server is started or found already running:

```
● sdlc-graph-viewer — <project>
  port        <port>
  home        http://localhost:<port>/            (the run list is the app's own sidebar)
  live view   http://localhost:<port>/view?state=/api/runs/<run-id>/state
  runs api    http://localhost:<port>/api/runs
  config      <path to sdlc-graph-viewer.env in force> · or: defaults (no env file — example at
              ${CLAUDE_PLUGIN_ROOT}/skills/view-run/server/sdlc-graph-viewer.env.example)
  viewer      <path to the run-viewer.html being served>
  stop        kill <pid>
```

A summary missing the port, a URL, or the config location is an incomplete delivery of this skill.

> **If the server prints `cannot read the viewer page` and exits, that is the whole diagnosis** —
> `VIEWER_HTML` (or the bundled default) does not resolve to a readable file. It fails at spawn on
> purpose: a server whose only page is missing used to bind its port anyway and answer `404 not
> found` on `/` and `/view` while every `/api/*` route worked, which reads as a bad URL rather than
> a broken install and sends people hunting the wrong thing. The page polls the state JSON every **1s, forever — even after `DONE`**.
**One server per project, never shared**; a second project gets a second server on its own port.
**Tell the user what is running and how to stop it** whenever you started a server.

## Output Contract

- `docs/graph-runs/<run-id>/view.html` — a self-contained snapshot of the named run, delivered to the user.
- Live mode: the **complete summary block** — project, port, all three URLs, the config file in force (or "defaults" plus where the example lives), and how to stop the server. All of it, every time.
- Any anomaly seen while generating (parse error, missing `schema_version`, `RUNNING` with a quiet
  file) **stated, not smoothed over**.

## Validation

Before telling the user it is done:

- **The page opens standalone, and that is now a command, not a squint:**
  `python3 ${CLAUDE_PLUGIN_ROOT}/skills/view-run/evals/sync/graph_sync.py` — its
  `page-loads-nothing-external` check looks for markup or code that FETCHES at load time
  (`<script src>`, `<link href>`, `@import`, an absolute-URL `fetch`). **Do not "simplify" it to a
  grep for `http`** — a run's own `qa_env.api_base_url` is legitimately `http://localhost:3000`, and
  that grep fails on real state data while catching nothing a snapshot actually loads.
- **The rendered node matches the file — with one deliberate exception.** `state.node` (or `status`
  at a terminal) is what the header and the **run facts** panel show, always. The **board** may
  differ: with several milestones in flight it highlights the **least advanced** one, because a run is
  only as far along as its slowest milestone agent, and the now-card says so in as many words ("least advanced
  of N milestones"). Board ≠ `state.node` **without** that line is a real defect; with it, it is the
  point. If header and file disagree, the injection went wrong and every other panel is suspect.
- **The counters show budget, not just count** — `attempts["TEST:2"] 1/3`, never a bare `1`. A number
  with no bound beside it is the decorative-bound failure one layer out.
- **Display names are everywhere or nowhere, and never in a key** —
  `python3 …/evals/viewer/node_labels.py`. A node's ID is schema (`state.node`, both endpoints of
  every `history[]` entry, the `attempts["GATE_B:<id>"]` keys); `NODE_LABEL` is the name a human
  reads, and `GATE_B` renders as **`ms-final-review`** — the milestone's final review, as opposed to
  `PR_FINAL_REVIEW` on the open PR. Two failure modes, opposite directions: a surface that misses the
  label reads as a *different node* to anyone comparing panels, and a label that reaches a lookup
  (`data-node`, `A.selNode`, any `GRAPH.*[id]`) matches nothing and **throws nothing** — the board
  just highlights empty. The checker counts label sites per renderer rather than testing presence,
  because a five-site panel that keeps one is exactly the half-fixed surface this repo keeps
  producing. **`run facts` stays raw on purpose**: it is the panel that says what the file says.
- **The dimmed next-step buttons still mean what the guard beside them says** —
  `python3 …/evals/viewer/guard_predicates.py`. `APPLIES` is the other thing the viewer owns
  outside the `__GRAPH__` markers, and it is the one the generator therefore never corrected: it
  kept the edge ids of the graph this viewer was forked from, so seven keys named edges that no
  longer existed and `'2'` and `'13'` dimmed the exact **inverse** of their own guard. A reading
  aid that lies is worse than none — it says "this run cannot go there" about a run that can — and
  it stays invisible, because a wrong predicate and a missing one both just draw a button a
  slightly different grey. The checker reads each guard cell out of `GRAPH.EDGES`, runs the real
  predicate under node, and fails when they disagree; a guard whose cell it cannot parse must be
  declared in `UNCHECKED` with a reason.
- **A liveness claim is only ever made in live mode, and only from a real clock.** The milestone agent line may
  say a milestone agent has gone silent **only** when the page is polling — not in a snapshot, not on a
  dropped file, not on a `paused` run, and not on one that is no longer `RUNNING`. `Date.now()` in a
  snapshot is the *viewing* time, so an unguarded marker reports every milestone agent in a shared snapshot dead
  minutes after capture; the stall banner has always been guarded this way and the milestone agent line must
  match it. Two more rules learned the same way: `seen_at` advances at **node boundaries only** — the
  orchestrator's monitor filters heartbeats out — so judge against the journal's own last timestamp
  where the server can supply it, and use a window measured in *tens of minutes* where it cannot, or
  a healthy seven-minute `GATE_A` reads as a corpse. And an absent or unparseable timestamp is
  **unknown, never fresh**: `NaN > threshold` is false, so the input that most suggests a milestone agent died
  mid-write is exactly the one that renders healthiest.
- **The ledger is visible without scrolling past the happy path.** A run with a non-empty
  `skipped_gates[]` is not a clean run, and the page must not read as though it were.
- **Live mode: fetch `/api/runs` and the state endpoint once** and confirm both answer. A summary
  naming a port nothing is listening on is worse than no summary.
- **The guard list still matches the graph** — `python3 ${CLAUDE_PLUGIN_ROOT}/skills/view-run/evals/sync/graph_sync.py`.
  **A non-zero exit is a stale viewer, not a warning.** The graph's own eval suite cannot reach this
  plugin (a plugin may not read above its own root), so this checker reads in the other direction —
  edge ids and endpoints, the node set, the bounds, the six stops, and whether each guard still
  tests the same fields. It prints `skip` and exits 0 when `sdlc-graph` is not installed.
- **A snapshot survives its own run data** —
  `python3 ${CLAUDE_PLUGIN_ROOT}/skills/view-run/evals/safety/snapshot_safety.py`. **Run this after any
  edit to the injector above or to the slot's surroundings.** Snapshot mode is the one place run data
  is inlined into the page's *source* rather than rendered through `esc()`, so a closing script tag
  in an observation ends the block, `boot()` never runs, and the rest of the run is handed to the
  HTML parser as markup — a blank page that also executes. The checker builds a snapshot using **the
  injector extracted from this file**, not a copy of it, so the documented command and the checked
  one cannot drift apart.
- **Both checkers can still fail** — `python3 …/evals/sync/graph_sync_selftest.py` breaks the viewer and
  the spec one way at a time and asserts the drift checker goes red for each. It exists because
  `graph_sync.py` shipped with a guard extractor that read the guard cell *after* its backticks had
  been stripped: it matched nothing at all, left 26 of the 42 guards compared against an empty set,
  and printed `ok` the whole time. A checker nobody checks is the recurring shape here.

## Guardrails

- **Read-only with respect to the run.** Never write to `docs/graph-runs/<run-id>/state.json` — the orchestrator is
  the single writer, and this skill racing it would corrupt the one durable record.
- Never regenerate the template's graph data by hand — copy `run-viewer.html` as-is and inject only
  the state. The embedded graph lives between `/*__GRAPH_BEGIN__*/` and
  `/*__GRAPH_END__*/` and is **data only**, which is what makes `graph_sync.py` able to parse it at
  all. Any hand-edit inside those markers — and any function put in there — must be followed by
  running the checker.
- The snapshot may contain the run's file paths and branch names — fine for the repo, but do not
  publish it outside the repo (artifact, gist) unless the user asks.

## References

- `${CLAUDE_PLUGIN_ROOT}/skills/view-run/viewer/run-viewer.html` — the template (demo mode when opened
  directly). **The one viewer copy in this plugin** — the bundled site serves this same file.
- `${CLAUDE_PLUGIN_ROOT}/skills/view-run/server/sdlc-graph-viewer.env.example` — the annotated config
  template; copy to `sdlc-graph-viewer.env` beside wherever the server is started.
- `${CLAUDE_PLUGIN_ROOT}/skills/view-run/server/server.mjs` — the server: plain `node:http`, zero
  dependencies, no build. Serves `/` **and** `/view` (both the viewer — its sidebar is the run
  list), `/api/runs`, `/api/runs/<id>/state`, `/api/runs/<id>/progress` — everything `no-store`.
  Per-project port derived from the project path. **`/progress`** returns the tail of each milestone's
  journal: a torn last line is the normal case there (append-only, read while being written), so it
  is counted as `unreadable` rather than allowed to blank a live milestone agent.
- `${CLAUDE_PLUGIN_ROOT}/skills/view-run/fixtures/` — ten state files for checking this
  page by hand: three parallel inflight, a `BLOCKED` halt, a 40-transition run paused at `MERGE`, a
  strategy-D park, a pre-plan empty run, one deliberately corrupt file, `parallel-live-*` (three milestones
  mid-flight, one of them deliberately silent for hours), and
  `hostile-run-data-*` — every field markup- or instruction-shaped, because run data is DATA and
  the tags must render as visible text. **Not shipped inside a snapshot** — they are a test
  corpus, and drag-and-drop is how you open one.
- `${CLAUDE_PLUGIN_ROOT}/skills/view-run/evals/` — `run_all.py` runs the seven deterministic
  suites: `graph_sync.py` (the drift checker), `graph_sync_selftest.py` (proves it can go red),
  `snapshot_safety.py` (a snapshot of hostile run data is inert), `server_boot.py`,
  `viewer/agent_panel.py`, `viewer/node_labels.py` (display names), and
  `viewer/guard_predicates.py`. Described under *Validation*.
  The one WRITER, `sync/sync_graph.py --write`, regenerates the `GRAPH` block from the spec —
  which is why anything the viewer owns rather than copies (`NODE_LABEL`, the `APPLIES` guard
  predicates) is deliberately outside `/*__GRAPH_BEGIN__*/…/*__GRAPH_END__*/`. **That is also the
  hole `guard_predicates.py` closes:** the generator does not reach `APPLIES`, so it kept the edge
  ids of the graph this viewer was forked from — seven keys naming edges that no longer existed,
  and two predicates that dimmed the exact inverse of their own guard.
- `graph/state.md` in the **sdlc-graph plugin** — the schema this viewer renders
  (`schema_version 5`: `in_flight` names the milestones with a live agent, `milestones[].progress`
  is what each one claims, and `stopped.kind` is why a run is not moving).

Attribution

RonMizrahiRonMizrahi
View sourceMore from RonMizrahi →
SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Related Skills

Caveman

Ultra-compressed communication mode that cuts output tokens while keeping technical accuracy. Levels: lite, full, ultra and the wenyan variants. Use for /caveman, "caveman mode", "talk like caveman", "be brief" or "less tokens".

1066601 votes

Hyperplan

Adversarial multi-agent planning skill. Self-orchestrates 5 hostile category members (unspecified-low, unspecified-high, deep, ultrabrain, artistry) via team-mode for ruthless cross-critique debate, distills only the defensible insights, then MANDATORILY hands the distilled insight bundle to the `plan` agent for executable plan formalization. Use when planning needs maximum rigor and surfacing of weak assumptions, blind spots, and over-engineering. Triggers: 'hyperplan', 'hpp', '/hyperplan', ...

686011 votes

Mcp Code Execution

Routes multi-tool workflows through MCP servers for large datasets and pipelines. Use when Bash tool overhead is limiting throughput on data-heavy tasks.

3351 votes

catchup

Recovers the conversation and failed tool calls of a previous Codex, Claude Code, Antigravity, Cline, Copilot CLI, Cursor, DeepSeek Harness, Kimi, OpenCode, Pi Agent, or ZCode session. Use when the user says "catch up", "what did the last session do", "get me up to speed", "I switched agents", asks to recover/summarize a previous session before continuing, or asks to diagnose or report a catchup failure. Do NOT use for the current conversation, git history, or any non-agent log.

651 votes

math-skill

A comprehensive mathematical reasoning skill for AI assistants — handles arithmetic to research-level problems with rigorous step-by-step reasoning, systematic verification, and transparent uncertainty handling

381 votes
View all in ai-agents →