OR Key
drop another .md file to compare - side-by-side diff against chat-drive

chat-drive

Lets you kick off a task by simply typing what you want into chat.
personal 2 files

What it does for you

Lets you kick off a task by simply typing what you want into chat.

What it produces

A recent result, so you can see the kind of work it returns.

loading…

How to get it

These run inside the Snappy workspace. Want this working in your business? I set skills like this up with you, in one focused week.

Work with me
For developers how this skill is built, graded, and how it runs

at a glance- the short version

actorDispatchInChatUI(text) - pushes onto the queue.
auditorThe
eval modeshape
stages1

what's inside - the parts that make up a skill 3/4 present

A skill is just a few plain-text files. Only the main one is required. The rest are optional, added as the work needs them. This is what the skill is made of; how it runs is just below.

The skill
state/skills/chat-drive/SKILL.md present
the skill itself, in plain text
The main file. It says what the skill is and lays out the steps in plain English.
Code
state/lib/chat-drive.ts present
code the skill can run
Reusable code this skill can call when it needs to.
Scripts
state/bin/chat-drive/ not present
helper scripts
Optional. Added when a skill has a few commands to run.
Loader
state/skills/chat-drive/AGENTS.md present
what the AI loads on the fly
Loaded automatically the moment this skill is needed. Kept short on purpose.

how it runs - the shared frame every skill uses 4/5 present

Every skill runs the same way. One part does the work, a separate part checks it, and a short loader hands the AI exactly what it needs for the job. Anything this skill doesn't use shows a one-line note saying why, on purpose, not by accident.

makes the work The worker
present
DispatchInChatUI(text) - pushes onto the queue. the worker
Does the actual work. Whatever it produces is what gets checked next.
checks the work The reviewer
present
The the checker
A separate checker grades the work, so the part that made it can't approve its own work.
frame
learns Self-correction
not present

This skill doesn't fix its own gaps yet.

tidies up Background fixes
present
queued for rewrite runs in the background
Bigger fixes that can't be made on the spot get queued and rewritten in the background later.
remembers Run history
present
state/log/evals.ndjson shape runs
Every run is written down here, so the next time this skill is used it already knows how the last runs went.
Critical rules the things this skill must not get wrong
No must-not-break rules called out for this skill. Anything important lives in the writeup below.

what it has learned - fixes written back in over time sample

When a run hits something this skill didn't handle, the fix gets written back into the skill so it doesn't happen again. FIXED means it was corrected on the spot. LOGGED means it's queued for a bigger rewrite. Either way, the skill gets a little better and never makes the same mistake twice.

  1. Loading feedback rows…

how the work flows- who makes it, who checks it

actor DispatchInChatUI(text) - pushes onto the queue.
auditor The
1 stage
npx
npx tsx state/lib/chat-drive.ts "say hello in three words"

SKILL.md- the skill, written out in plain English

chat-drive

Push text into the Snappy OS composer from any agent, anywhere. The text flows through the same OpenUI submit path the human types into: processMessage/dispatch/chat → AG-UI stream → generative-UI cards rendered in the live React tree. Same store, same surface, same eyes.

This is the missing primitive for closed-loop Snappy OS dogfood: an agent can now type intent and watch real cards stream in, then audit by screenshot.

What it's for

  • Dogfood loops. A subagent pushes a stress-test intent, screenshots the

result, grades the rendered card. The actor (push) and the auditor (read the screenshot) are necessarily distinct - the contract holds for free.

  • Automated UX QA. Verify the welcome surface unmounts on first message,

user-pill alignment, dispatch-card variants, etc., end-to-end through the rendered DOM.

  • Recursive subagent dispatch. A long-running agent can re-enter the chat

surface mid-task by pushing a follow-up intent. The chat is the agent's outbox.

When NOT to use it

  • Anything that doesn't need the rendered UI. If you don't care about the

React tree, call the head-screen server's /dispatch/chat directly, or use the dispatch skill. Running through the chat surface adds streaming latency for no reason.

  • As a synthesis transport. The bridge is a queue, not an RPC channel -

there's no callback when streaming finishes. Use /dispatch/chat directly when you need the response programmatically.

Steps

  1. Verify the head-screen server is up. The bridge endpoints live on it.
   bash ~/projects/snappy-os/state/bin/head-screen/launch.sh   # idempotent
  1. Verify Snappy OS is running and on screen so the polled push lands somewhere.
  pgrep -af "/Applications/SnappyChat.app/Contents/MacOS/SnappyChat"

If it's not, build + install:

   cd ~/projects/snappy-os-app/apps/snappy-os && bash scripts/build-app.sh --install
  1. Push the intent.
   npx tsx -e "
   import { dispatchInChatUI } from './state/lib/chat-drive.ts';
   await dispatchInChatUI('what did the agents do today', { waitForFirstFrame: 12000 });
   "

The default waitForFirstFrame is 8000ms. Pass a larger value when the target backend is slow (Claude Code: 12-15s; openrouter/gemini: 6-10s).

  1. Audit by screenshot. The bridge has no completion callback - actor ≠ auditor.
   npx tsx -e "
   import { captureScreen } from './state/lib/desktop.ts';
   const path = await captureScreen('/tmp/chat-drive-verify.png');
   console.log(path);
   "

Then Read the PNG. Welcome surface unmounted + user pill on the right + assistant card streaming = bridge working.

Library API

state/lib/chat-drive.ts exports three functions. Importable from any TS agent code; also runnable as a CLI smoke.

export async function dispatchInChatUI(
  text: string,
  opts?: { waitForFirstFrame?: number }   // default 8000ms
): Promise<void>;

export async function resetChatUI(
  opts?: { waitMs?: number }               // default 1500ms
): Promise<void>;

export async function chatDriveAvailable(): Promise<boolean>;

resetChatUI is for multi-scenario dogfood loops: clears the thread and brings the welcome surface back so the next dispatchInChatUI lands in a clean state. Same FIFO as text pushes (single ordering), distinct /chat-inject-control endpoint so wire shape is unambiguous.

CLI:

npx tsx state/lib/chat-drive.ts "say hello in three words"

HEAD_SCREEN_URL env var overrides the default http://127.0.0.1:3147.

Architecture (one paragraph)

dispatchInChatUI POSTs to the head-screen server's POST /chat-inject-push endpoint, which appends the text to a 50-slot in-memory FIFO with FIFO eviction on overflow. The snappy-os React app mounts a polling effect that hits GET /chat-inject-pop every 500ms; on hit, it locates whichever OpenUI composer is currently visible (welcome OR thread variant) and writes through React's native value setter to trigger the textarea's onChange, then clicks the submit button. From there, the real processMessage path takes over - same code as a human typing.

For QA probes and dogfood agents, include "newThread": true in the push body. The app resets to a fresh chat before dispatching that item so probe traffic does not land in Robert's active thread. Omit it only when the intent is deliberately a follow-up in the current conversation.

Eval

Actor: dispatchInChatUI(text) - pushes onto the queue. Auditor: the audit harness re-reads the lib's exported function shape (present, async, two parameters); the user-facing audit is "is the rendered card correct" via screenshot, deliberately outside the lib.

Eval kind: shape. Mechanical: import the lib, assert dispatchInChatUI and chatDriveAvailable exist as functions, type-check passes. Logged as the skill's eval row in state/log/evals.ndjson.

Pitfalls

  • The head-screen server must be alive. The bridge IS the head-screen

server. If /healthz doesn't answer, push will throw. chatDriveAvailable() is the cheap precheck.

  • No completion callback. waitForFirstFrame is the only synchronization

knob. Tune it per backend, then screenshot.

  • Restart drops queued pushes. The queue is in-memory by design - restart

= empty. If a dogfood loop relies on durability across restarts, you're using the wrong primitive.

  • The bridge is loopback only. No external network exposure. The CORS

headers are wide so file:// origins (WKWebView) work; the listener is bound to 127.0.0.1.

Files

  • state/lib/chat-drive.ts - the API (importable + CLI).
  • state/bin/head-screen/server.ts - owns the queue endpoints

(POST /chat-inject-push, POST /chat-inject-control, GET /chat-inject-pop).

  • ~/projects/snappy-os-app/apps/snappy-os/web/src/App.tsx - the React polling effect and

composer-injection helper that drains the queue.

AGENTS.md- what the AI loads when this skill comes up

chat-drive - loader

Push text into the Snappy OS composer. Routes through same OpenUI path as human typing: processMessage -> /dispatch/chat -> AG-UI stream -> live cards. Closed-loop dogfood: agent pushes intent, auditor screenshots result. Actor != auditor by design.

Full reference: state/skills/chat-drive/SKILL.md. Lib: state/lib/chat-drive.ts. Server: state/bin/head-screen/server.ts. Consumer: ~/projects/snappy-os-app/apps/snappy-os/web/src/App.tsx (poll 1000ms).

Critical Rules

  1. Actor (push) ≠ auditor (screenshot). dispatchInChatUI() queues text; lib never reports "did card render." Audit via npx tsx state/lib/desktop.ts capture-screen /tmp/path.png + Read. No completion callback.
  2. waitForFirstFrame is only sync knob. Default 8000ms. Budget: 8s dispatch + 1s React poll lag = 9s minimum. Tune per backend: Claude Code 12-15s, openrouter/gemini 6-10s. After push, screenshot.
  3. Head-screen server MUST be alive. Pre-flight: chatDriveAvailable() or bash state/bin/head-screen/launch.sh (idempotent). Verify: curl http://127.0.0.1:3147/healthz answers 200.
  4. Snappy OS must be running and visible. Push -> in-memory FIFO. No polling = silent eviction. Confirm: pgrep -af "/Applications/SnappyChat.app" shows process. Build if missing: cd ~/projects/snappy-os-app/apps/snappy-os && bash scripts/build-app.sh --install.
  5. Queue is in-memory FIFO: 50-item cap, 30s TTL per item, wiped on server restart. Never durability-dependent. Pre-drain before QA: curl -XPOST http://127.0.0.1:3147/chat-inject-flush (returns {flushed:N}).
  6. React polls /chat-inject-pop every 1000ms (App.tsx:337). Each push incurs up to 1000ms before composer sees it. Factor into waitForFirstFrame budget.
  7. tsx never hot-reloads server.ts. After any server.ts edit, kill + restart the server. Verify: pgrep -af server.ts + git log --oneline -1 match. Without restart, edits invisible.
  8. Don't push faster than dispatcher streams. Wait for RUN_FINISHED before next text push. Use resetChatUI() (not manual reset) between scenarios.
  9. resetChatUI() does NOT flush. It sends control items for nav reset (welcome). Pre-flush with /chat-inject-flush if stale pushes queued.
  10. Activate app before screenshot on secondary Space. Snappy OS off-screen = screencapture captures wallpaper. Run osascript -e 'tell application "Snappy Chat" to activate' with 1-2s wait.
  11. React pre-fetches /chat-inject-pop items. Polling effect calls pop() before dispatching. Manual curl drain competes with React. Use /chat-inject-flush endpoint only; never manual pop drain.
  12. Concurrent claude -p / claude --continue compete for queue. Symptom: pushes queue but never dispatch (other session consumed them). Mitigation: serialize QA sessions or use /dispatch/chat POST directly (isolated, independent thread).
  13. Server crashes wipe in-memory queue. Watch for FATAL evalLeaderboardRegex undefined in state/log/head-screen.log. Verify uptime: ps -p $(pgrep -f server.ts | head -1) -o etime. If elapsed time reset, queue is gone; re-push after restart.
  14. Force fresh crypto.randomUUID() for each messageId. Reusing any id (especially OpenUI's optimistic user-message id) collides in store reducer = duplicate-render bug. App.tsx processMessage must call crypto.randomUUID() per injected message. Do not remove.
  15. Direct /dispatch/chat POST requires intent field. curl -XPOST http://127.0.0.1:3147/dispatch/chat -d '{"intent":"text","threadId":"<id>"}' (NOT flat messages[] array). Returns 400 intent required if missing.
  16. TCC screencapture blocked in subagent context. If capture-screen fails with exit:1, use the bundled Computer/helper path or a live appshot as the audit surface; do not route through external helper daemons.
  17. __snappyNav does NOT navigate to Live Apps. window.__snappyNav('#/chat/live-apps') navigates to the chat view, not the Live Apps surface. Do not use it for Live Apps navigation.
  18. JS textarea injection does NOT submit messages. Injecting text into the OpenUI composer textarea via document.querySelector(...).value = ... or .dispatchEvent(new Event(...)) fills the field visually but does not trigger React's controlled-component submission handler. Use /chat-inject-push instead.
  19. System Events keystrokes do NOT reach WKWebView. AppleScript System Events keystroke commands target native AppKit views; WKWebView content lives in a separate web process and does not receive synthetic key events this way.
  20. Live Apps navigation requires custom events via /test-eval. The Live Apps surface listens for snappy:open-artifacts and snappy:live-apps-tab DOM events. Fire them via the /test-eval endpoint (see Commands). /chat-inject-control view-artifacts navigates to the artifacts sidebar tab, not the Live Apps surface.

Commands

operationcommand
push (TS)import { dispatchInChatUI, resetChatUI, chatDriveAvailable } from "./state/lib/chat-drive.ts"; await dispatchInChatUI("text", { waitForFirstFrame: 8000 });
push (CLI)npx tsx state/lib/chat-drive.ts "intent text"
push (direct)curl -s -XPOST http://127.0.0.1:3147/chat-inject-push -H "Content-Type: application/json" -d '{"text":"your message here"}'
preflightchatDriveAvailable() (async, returns bool)
server startbash state/bin/head-screen/launch.sh (idempotent, :3147)
server verifypgrep -af server.ts (match git log --oneline -1)
server log`tail state/log/head-screen.log \grep FATAL`
chat verifypgrep -af "/Applications/SnappyChat.app"
app activateosascript -e 'tell application "Snappy Chat" to activate' (1-2s before screenshot)
drain queuecurl -XPOST http://127.0.0.1:3147/chat-inject-flush (returns {flushed:N})
reset welcomecurl -XPOST http://127.0.0.1:3147/chat-inject-control -d '{"action":"reset"}' (does NOT flush)
navigate view`curl -XPOST http://127.0.0.1:3147/chat-inject-control -d '{"action":"view-artifacts\view-files\view-chat\view-scheduled\view-customize\view-projects"}'` (view-files = Skills tab)
open Live Appscurl -s -XPOST http://127.0.0.1:3147/test-eval -H "Content-Type: application/json" -d '{"js":"window.dispatchEvent(new CustomEvent(\"snappy:open-artifacts\"))"}'
Live Apps tabcurl -s -XPOST http://127.0.0.1:3147/test-eval -H "Content-Type: application/json" -d '{"js":"window.dispatchEvent(new CustomEvent(\"snappy:live-apps-tab\",{detail:{tab:\"apps\"}}))"}' (tab: "apps"\"components"\"themes")
select threadcurl -XPOST http://127.0.0.1:3147/chat-inject-control -H "Content-Type: application/json" -d '{"action":"select-thread","threadId":"<uuid>"}'
list threadscurl -s http://127.0.0.1:3147/threads (returns array, copy threadId)
direct dispatchcurl -XPOST http://127.0.0.1:3147/dispatch/chat -d '{"intent":"text","threadId":"<id>"}' (bypass queue)
check contention`pgrep -af "claude.*-p\claude.*continue"` (>1 = racing)
screenshotnpx tsx state/lib/desktop.ts capture-screen /tmp/path.png (or use Computer/appshot when TCC blocks the shell)
verify dispatched`tail state/log/dispatch-chat.ndjson \grep intent_chars:<N>`
server uptime`ps -p $(pgrep -f server.ts \head -1) -o etime` (reset = queue wiped)
env overrideHEAD_SCREEN_URL=http://custom:port (default :3147)
referencestate/skills/chat-drive/SKILL.md
eval logstate/log/evals.ndjson (skill: chat-drive)

WKWebView Hard Failures (do not retry these)

approachwhy it fails
window.__snappyNav('#/chat/live-apps')navigates to /chat, not Live Apps surface
JS textarea.value = ... + synthetic eventsfills visually, bypasses React controlled-component submit handler
AppleScript System Events keystrokestargets AppKit layer, WKWebView web process does not receive them
desktop.ts capture-screen in subagentTCC may block shell capture; use Computer/appshot for the audit instead
/chat-inject-control view-artifacts for Live Appsroutes to artifacts sidebar tab, not the Live Apps surface

Self-Test

An agent reading this should correctly:

  1. [ ] Pre-flight chatDriveAvailable() before push?
  2. [ ] Tune waitForFirstFrame for backend + 1s React poll lag?
  3. [ ] Audit by screenshot Read, NOT lib return values?
  4. [ ] Keep both snappy-os and head-screen alive?
  5. [ ] Use resetChatUI() (not manual) between scenarios?
  6. [ ] Restart server after any server.ts edit (tsx no hot-reload)?
  7. [ ] Serialize pushes; wait for RUN_FINISHED before next?
  8. [ ] Drain /chat-inject-flush before QA tests?
  9. [ ] Know React pre-fetches (no completion callback)?
  10. [ ] Activate app via osascript before screenshot on secondary Space?
  11. [ ] Know resetChatUI() doesn't flush (pre-flush if stale)?
  12. [ ] Check pgrep claude.*-p for competing consumers?
  13. [ ] Know server crashes wipe queue; verify uptime via ps?
  14. [ ] Force fresh crypto.randomUUID() per messageId?
  15. [ ] Use bundled Computer/appshot or local desktop capture for screenshots when needed.
  16. [ ] Know TCC screencapture blocked in subagent context (use bridge)?
  17. [ ] Know __snappyNav does NOT navigate to Live Apps?
  18. [ ] Know JS textarea injection does NOT submit (use /chat-inject-push)?
  19. [ ] Know System Events keystrokes do NOT reach WKWebView content?
  20. [ ] Use snappy:open-artifacts event via /test-eval for Live Apps navigation?
  21. [ ] Use snappy:live-apps-tab event via /test-eval to switch Live Apps tabs?

Found a gap? Edit this file. <!-- footer-injection-point -->

api.ts- the code it can call

// snappy-chat-drive/api.ts
//
// Push text into the snappy-chat composer programmatically. The bridge is
// the head-screen server's chat-inject FIFO: this lib POSTs to /chat-inject-push,
// the snappy-chat WKWebView polls /chat-inject-pop on a 500ms interval and
// runs the text through the real OpenUI submit path (processMessage →
// /dispatch/chat). The result: dogfood loops, automated UX QA, and recursive
// subagent dispatch all flow through the actual chat surface — same React
// store, same generative-UI cards — instead of trying to drive WKWebView
// with synthetic clicks (peekaboo's clickAt does not fire React onClick on
// WKWebView).
//
// Sync contract: there is NO callback when the chat finishes streaming. The
// caller is the actor (push); the auditor is whatever reads a screenshot
// afterward. `waitForFirstFrame` is a coarse sleep so the dispatcher has
// time to start streaming before the auditor captures.

const HEAD_SCREEN_BASE = process.env.HEAD_SCREEN_URL ?? "http://127.0.0.1:3147";
const DEFAULT_FIRST_FRAME_MS = 8_000;

export interface DispatchInChatUIOpts {
  /**
   * Sleep duration after the push so the dispatcher has time to start
   * streaming. Default 8000ms. Pass 0 to return immediately.
   */
  waitForFirstFrame?: number;
  /**
   * Per-agent queue isolation key. The server keeps a Map<agentId, queue>
   * so parallel QA subagents don't share a single FIFO. Default "ui" matches
   * the snappy-chat React poll loop — so omitting this routes pushes to the
   * actual cockpit. Pass a stable identifier (e.g. "qa-broad-smoke",
   * "dogfood-loop2") to isolate from the cockpit and from each other.
   */
  agentId?: string;
}

export interface DispatchInChatUIResult {
  injectId: string | null;
  queued: number | null;
  agentId: string | null;
}

/**
 * Push `text` onto the snappy-chat input bridge. Resolves once the queue
 * has accepted the push and (optionally) `waitForFirstFrame` ms have passed.
 *
 * Throws if the head-screen server is unreachable or the push is rejected.
 */
export async function dispatchInChatUI(
  text: string,
  opts: DispatchInChatUIOpts = {},
): Promise<DispatchInChatUIResult> {
  if (typeof text !== "string" || text.length === 0) {
    throw new Error("dispatchInChatUI: text (non-empty string) required");
  }
  const wait = opts.waitForFirstFrame ?? DEFAULT_FIRST_FRAME_MS;
  const body: { text: string; agentId?: string } = { text };
  if (typeof opts.agentId === "string" && opts.agentId.length > 0) {
    body.agentId = opts.agentId;
  }

  const res = await fetch(`${HEAD_SCREEN_BASE}/chat-inject-push`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(body),
  });
  if (!res.ok) {
    let detail = "";
    try { detail = await res.text(); } catch {}
    throw new Error(
      `chat-inject-push ${res.status}: ${detail.slice(0, 240) || res.statusText}`,
    );
  }
  let payload: unknown = null;
  try { payload = await res.json(); } catch {}

  if (wait > 0) {
    await new Promise(r => setTimeout(r, wait));
  }
  const bodyOut = payload && typeof payload === "object" ? payload as Record<string, unknown> : {};
  return {
    injectId: typeof bodyOut.injectId === "string" ? bodyOut.injectId : null,
    queued: typeof bodyOut.queued === "number" ? bodyOut.queued : null,
    agentId: typeof bodyOut.agentId === "string" ? bodyOut.agentId : null,
  };
}

export interface ResetChatUIOpts {
  /**
   * Sleep duration after the control push so the React app has time to
   * pop the control message, unmount FullScreen, and remount the welcome
   * surface. Default 1500ms — enough for the 500ms poll cadence + a remount.
   */
  waitMs?: number;
  /**
   * Per-agent queue isolation key. See `DispatchInChatUIOpts.agentId`.
   * Default "ui". Parallel QA agents pass their own ID so a reset on one
   * thread doesn't drop the queue another agent is filling.
   */
  agentId?: string;
}

/**
 * Push a control message that resets the snappy-chat UI to the welcome
 * surface. Equivalent to the user clicking "+ New chat" in the sidebar.
 * Use between dogfood scenarios so a single subagent can run multiple
 * intents end-to-end without thread state bleeding between them.
 *
 * Throws if the head-screen server is unreachable or the push is rejected.
 */
export async function resetChatUI(opts: ResetChatUIOpts = {}): Promise<void> {
  const wait = opts.waitMs ?? 1500;
  const body: { action: string; agentId?: string } = { action: "reset" };
  if (typeof opts.agentId === "string" && opts.agentId.length > 0) {
    body.agentId = opts.agentId;
  }
  const res = await fetch(`${HEAD_SCREEN_BASE}/chat-inject-control`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(body),
  });
  if (!res.ok) {
    let detail = "";
    try { detail = await res.text(); } catch {}
    throw new Error(
      `chat-inject-control ${res.status}: ${detail.slice(0, 240) || res.statusText}`,
    );
  }
  if (wait > 0) {
    await new Promise(r => setTimeout(r, wait));
  }
}

/**
 * Cheap reachability check for the head-screen server. Returns true iff the
 * server answers any 2xx-ish response on `/healthz`. Use to gate dogfood
 * loops so they fail fast when the bridge is down rather than timing out
 * mid-push.
 */
export async function chatDriveAvailable(): Promise<boolean> {
  try {
    const res = await fetch(`${HEAD_SCREEN_BASE}/healthz`, { method: "GET" });
    return res.ok;
  } catch {
    return false;
  }
}

// CLI smoke: `npx tsx state/lib/chat-drive.ts "say hello in three words"`
// Set CHAT_INJECT_AGENT_ID=<id> to isolate from the cockpit's "ui" queue
// (e.g. parallel QA subagents).
if (import.meta.url === `file://${process.argv[1]}`) {
  const text = process.argv.slice(2).join(" ").trim();
  if (!text) {
    console.error('usage: tsx state/lib/chat-drive.ts "<intent>"');
    process.exit(2);
  }
  const agentId = process.env.CHAT_INJECT_AGENT_ID;
  (async () => {
    const up = await chatDriveAvailable();
    if (!up) {
      console.error("head-screen server unreachable at", HEAD_SCREEN_BASE);
      process.exit(1);
    }
    await dispatchInChatUI(text, { waitForFirstFrame: 0, agentId });
    console.log("OK pushed:", text, agentId ? `(agentId=${agentId})` : "");
  })().catch(e => { console.error("FAIL:", e?.message ?? e); process.exit(1); });
}

scripts- helper scripts it can run

prose-only skill - 2 inline code blocks live in SKILL.md above (no state/bin/ sidecar yet).

how we check it- the checks, plus the last 10 runs

rubric shape schema-shape check (no inline rubric)
recent no runs actor/auditor: unverifiable
deps none declared

no recent runs logged - the eval contract is declared but nothing has been graded yet