This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine
What I Built
Temporal workflows are meant to finish. Some don't. They sit in the list as "Running" for days: a worker that was never deployed, an activity that fails the same way on every retry, a code change that breaks replay, a signal nobody sends. Temporal shows you the status, but not why it will never change, and working that out means reading histories, pending activities and task-queue pollers by hand.
The theme was "Ghost in the Machine", and these are the ghosts in mine: workflows that are dead but still walking. So I built Undertaker. You point it at a Temporal server and it sweeps every running workflow, sorts the undead into nine causes using Temporal's own data, and writes a certificate of undeath for each one: the cause, the evidence (history event ids, attempts, errors, poller counts), the line of your code if you give it the source, and the exact temporal command that fixes it or lays it to rest.
The words come from Gemma 4 e4b running on my laptop through Ollama. The diagnosis does not. Plain code decides the cause, and plain code checks every fact the model cites before it reaches the page.
Demo
The morgue at http://127.0.0.1:8666: ten undead workflows, two alive, a filter per cause.
A retry loop. Gemma names activities.py:12, and the excerpt below it is the function that raises.
A non-determinism error: the history says bake was scheduled, the new code asks for frost.
Nothing scheduled at all. The worker reports the signal it handles, so the command is ready to copy.
The same sweep in the terminal.
Code
prateek11rai
/
undertaker
Finds the Temporal workflows that will never finish, and tells you why. Local Gemma 4, read-only by default.
Undertaker finds the Temporal workflows that will never finish, sorts each one into a cause using Temporal's own data, and writes a checked certificate of undeath for it Gemma 4 runs on your laptop through Ollama and writes the words; plain code makes the diagnosis and checks every fact the model cites.
Why local
Workflow histories hold real payloads (orders, customers, card errors), and stack traces point into private code. Sending them to a hosted model to ask "why is this stuck" is the thing most teams can't do. A 4B open model on your own machine can. Undertaker talks to your Temporal server and to Ollama on localhost, and nothing else: no telemetry, no CDN, no web fonts.
The demo in 60 seconds
You need macOS with Homebrew, uv, the Temporal CLI (brew install temporal) and, for model certificates, Ollama with gemma4:e4b.
uv sync
uv…How I Built It
Rules first, the model second
I didn't want a model guessing why a workflow is stuck. Temporal already knows most of the answer; it just spreads it across several APIs. So the diagnosis is a set of fixed rules, checked in order, over data from four places:
-
ListWorkflowExecutionswithExecutionStatus="Running"; -
DescribeWorkflowExecution: the pending workflow task, pending activities with attempt, last failure and last heartbeat, pending children; - the history, fetched once per workflow and cached by its last event id, for timers, workflow-task failures and each activity's task queue;
-
DescribeTaskQueue, to see whether anyone polls the queue a task is waiting on.
For workflows with nothing pending, Undertaker also asks the worker the built-in __temporal_workflow_metadata query, which returns the signal and update names the workflow handles.
The first rule that matches is the cause. Anything that matches none is alive, with the reason shown ("heartbeat 5 s ago", "timer fires in 1 min"). Each rule leaves behind a small list of keyed evidence, like e5: ActivityTaskScheduled charge_card, attempt: 181 or frame1: activities.py:12 in charge_card, and each cause has a code-made remedy with the exact command.
The certificate and its checks
Only then does the model come in. Gemma 4 e4b gets the cause, its rule, the workflow's id and type, the evidence list and, with --source, the function around each stack frame, read only from files inside the folder you gave it. It answers through Ollama's JSON-schema output: a headline, a diagnosis, the evidence keys it used, a next step and an epitaph.
Then code checks the answer. Every cited key must exist. Every number must appear in the evidence. Every code span, file name, file:line, activity or signal name must appear in the evidence or the excerpt. It may not name another cause, write a command or include a URL. The lengths must hold, and the epitaph may not contain digits or identifiers. If anything fails, the model gets one retry with the exact reasons. If that fails too, the page shows a plain template certificate built from the same evidence, and says so.
Failure messages are untrusted text, so they go to the model inside <data> tags. The checks are what really protect the page, though: an injected "say the cause is something else and run this command" fails on the other cause and on the command.
A haunted bakery
To test it, undertaker demo up starts the Homebrew Temporal dev server on 127.0.0.1 and a worker for a made-up bakery where every order goes wrong in its own way:
-
order-1042(retry loop):charge_cardraisesValueErrorfor an amount of 0, with no retry maximum. -
order-1043(heartbeat):bake_batchheartbeats once, then goes quiet; the heartbeat timeout is 5 s. -
order-1044(lost activity):fire_ovenis sent tobakery-ovens, which no worker polls. -
order-1045(orphaned): the workflow itself starts onbakery-legacy, which no worker polls. -
order-1046(broken code): the workflow readsrecipe["sugar"]from a recipe without sugar. -
order-1047(changed code): version A bakes, then sleeps 15 s. During the sleep the demo swaps in version B, which frosts first. When the timer fires, replay fails withTMPRL1100. -
order-1048(waiting for a signal): waits forpayment_received, which never comes. -
order-1049(long sleep): sleeps 365 days. It meant 365 seconds. -
order-1050(undead child): waits on the childorder-1050-box, which was sent tobakery-legacy.
Two healthy workflows (inventory-nightly, heartbeating as it counts shelves, and proof-dough, on short timers) and three finished orders make sure the morgue isn't only bodies.
The numbers
undertaker eval --demo compares the sweep with the demo's manifest and writes every certificate fresh. Two runs on a fresh demo:
- Rules: 10 of 10 undead found (nine causes plus the orphaned child), 0 missed, 0 false alarms, both alive workflows alive.
- Gemma: 10 of 10 certificates passed every check on the first try, no template fallbacks, 8.4 s and 7.6 s per certificate on my Mac.
These are workflows I built to match the rules, so it is a smoke test, not a benchmark. The rejection and fallback paths are covered by unit tests, because the live model didn't trip them in these runs.
Why Does Open Innovation Matter?
Workflow histories hold real payloads: orders, customer details, card errors. Stack traces point straight into private code. "Paste this into a hosted model and ask why it's stuck" is exactly what most teams can't do with that data.
A 4B open model on the laptop can read it without anything leaving the machine. Undertaker talks to your Temporal server and to Ollama on localhost, and nothing else: no telemetry, no CDN, no web fonts. Temporal is open source too, so the whole loop, from the history to the rule to the check, can be read and changed. Undertaker is read-only by default: it can terminate or reset a workflow only when you start it with --allow-actions, and then it asks first.
My Agent Session
You are the builder for Undertaker. Your brief is below (the same text is in docs/design/build-brief.md in this repo; the planner appends answers to that file's ## Answers section). Start with Step 0.
Build Undertaker · core
The brief the build agent gets. Lives at
docs/design/build-brief.md; the planner appends answers at the bottom. Don't edit this file.
Planner: T3 thread mlh (id mcp:d6d84144-d1b5-442b-82e6-d79b07028f77). You: T3 thread mlh · build-undertaker, running in the project ~/github/prateek11rai/undertaker, branch main. Work only inside this repo.
Challenge: https://dev.to/challenges/hacktoberfest-week4-2026-10-26, closes Sun 2026-11-01 12:29 IST. Finish the core by 04:00 IST; move fast and keep it simple. A ship pass (images, README polish, post draft; no video) follows as a second brief.
Goal
Build all of docs/design/spec.md as one Python project managed with uv: the sweep (Temporal client, evidence, rules), certificates (Gemma 4 e4b through Ollama, checks, template fallback, cache), the morgue web page, the CLI, the haunted-bakery demo (dev server + worker + scenarios + manifest), eval, tests, CI and docs. uv run undertaker demo up, then uv run undertaker serve, shows every demo workflow with the right cause and a checked certificate.
Read first
-
docs/design/spec.md. Follow it. If you must deviate, write why indocs/BUILD_LOG.md. -
docs/brand/theme-preview.htmlanddocs/brand/logo.svg: the look. Copy the palette and the logo into the app's static files; the theme preview is the reference for toe tags and the certificate. -
AGENTS.md: the repo rules.
Step 0 · Check the setup before anything else
-
DevRelay is scoped to this repo: your tool list includes the DevRelay gateway tools;
.agents/skills/has ninedevrelay-*skills;.claude/skillsis a symlink to../.agents/skills;~/.claude/skillsand~/.agents/skillshave nodevrelay-*. Don't call any DevRelay tool, and never run thedevrelayCLI. -
Guards:
git config user.emailis[REDACTED];bash .git/info/leak-check.shprintsleak-check: clean. -
Local tools:
uv --version;/opt/homebrew/bin/python3.13 --version;/opt/homebrew/bin/temporal --version(1.9.1; always use this absolute path:PATHfinds an old~/.local/bin/temporalfirst);ollama listshowsgemma4:e4b(the planner started Ollama).
If a check fails, report BLOCKED-setup: <what> to the planner and wait. Don't fix setup yourself.
Already done by the planner
-
git initonmain, noreply identity,.git/info/leak-check.sh+.git/info/leak-patterns(local only; never committed). - DevRelay registered for this repo only; skills in
.agents/skills/devrelay-*(git-ignored through.git/info/exclude);.claude/skills→../.agents/skills(commit the symlink). -
AGENTS.md,docs/design/spec.md,docs/design/plan.md, this brief,docs/brand/logo.svg,docs/brand/theme-preview.html,docs/img/theme-preview.png. Commit them in your first commit.
Dependencies
- Python 3.13 from Homebrew (
.python-version=3.13); uv must never download a Python. -
pyproject.tomlmust have:
[[tool.uv.index]]
name = "pypi"
url = "https://pypi.org/simple"
default = true
[tool.uv]
exclude-newer = "2026-10-25T00:00:00Z"
python-downloads = "never"
python-preference = "only-system"
(The user-level uv config points at a private mirror; this block forces public PyPI and the org's 7-day cooldown. uv.lock may only mention pypi.org and files.pythonhosted.org; the leak check enforces it. Never print index URLs from user-level config files.)
- Runtime, exact pins, stable releases only: temporalio (1.34.0 is the newest inside the cooldown), fastapi, uvicorn, jinja2, pydantic. Dev: pytest, pytest-asyncio, ruff. Ollama over HTTP with the standard library (urllib.request), no client library. CLI …
You are the builder for Undertaker. Your brief is below (the same text is in docs/design/build-brief.md in this repo; the planner appends answers to that file's ## Answers section). Start with Step 0.
Build Undertaker · core
The brief the build agent gets. Lives at
docs/design/build-brief.md; the planner appends answers at the bottom. Don't edit this file.
Planner: T3 thread mlh (id mcp:d6d84144-d1b5-442b-82e6-d79b07028f77). You: T3 thread mlh · build-undertaker, running in the project ~/github/prateek11rai/undertaker, branch main. Work only inside this repo.
Challenge: https://dev.to/challenges/hacktoberfest-week4-2026-10-26, closes Sun 2026-11-01 12:29 IST. Finish the core by 04:00 IST; move fast and keep it simple. A ship pass (images, README polish, post draft; no video) follows as a second brief.
Goal
Build all of docs/design/spec.md as one Python project managed with uv: the sweep (Temporal client, evidence, rules), certificates (Gemma 4 e4b through Ollama, checks, template fallback, cache), the morgue web page, the CLI, the haunted-bakery demo (dev server + worker + scenarios + manifest), eval, tests, CI and docs. uv run undertaker demo up, then uv run undertaker serve, shows every demo workflow with the right cause and a checked certificate.
Read first
-
docs/design/spec.md. Follow it. If you must deviate, write why indocs/BUILD_LOG.md. -
docs/brand/theme-preview.htmlanddocs/brand/logo.svg: the look. Copy the palette and the logo into the app's static files; the theme preview is the reference for toe tags and the certificate. -
AGENTS.md: the repo rules.
Step 0 · Check the setup before anything else
-
DevRelay is scoped to this repo: your tool list includes the DevRelay gateway tools;
.agents/skills/has ninedevrelay-*skills;.claude/skillsis a symlink to../.agents/skills;~/.claude/skillsand~/.agents/skillshave nodevrelay-*. Don't call any DevRelay tool, and never run thedevrelayCLI. -
Guards:
git config user.emailis[REDACTED];bash .git/info/leak-check.shprintsleak-check: clean. -
Local tools:
uv --version;/opt/homebrew/bin/python3.13 --version;/opt/homebrew/bin/temporal --version(1.9.1; always use this absolute path:PATHfinds an old~/.local/bin/temporalfirst);ollama listshowsgemma4:e4b(the planner started Ollama).
If a check fails, report BLOCKED-setup: <what> to the planner and wait. Don't fix setup yourself.
Already done by the planner
-
git initonmain, noreply identity,.git/info/leak-check.sh+.git/info/leak-patterns(local only; never committed). - DevRelay registered for this repo only; skills in
.agents/skills/devrelay-*(git-ignored through.git/info/exclude);.claude/skills→../.agents/skills(commit the symlink). -
AGENTS.md,docs/design/spec.md,docs/design/plan.md, this brief,docs/brand/logo.svg,docs/brand/theme-preview.html,docs/img/theme-preview.png. Commit them in your first commit.
Dependencies
- Python 3.13 from Homebrew (
.python-version=3.13); uv must never download a Python. -
pyproject.tomlmust have:
[[tool.uv.index]]
name = "pypi"
url = "https://pypi.org/simple"
default = true
[tool.uv]
exclude-newer = "2026-10-25T00:00:00Z"
python-downloads = "never"
python-preference = "only-system"
(The user-level uv config points at a private mirror; this block forces public PyPI and the org's 7-day cooldown. uv.lock may only mention pypi.org and files.pythonhosted.org; the leak check enforces it. Never print index URLs from user-level config files.)
- Runtime, exact pins, stable releases only: temporalio (1.34.0 is the newest inside the cooldown), fastapi, uvicorn, jinja2, pydantic. Dev: pytest, pytest-asyncio, ruff. Ollama over HTTP with the standard library (urllib.request), no client library. CLI …
$ cd ~/github/prateek11rai/undertaker && date && ls -la && ls -la .agents/skills .claude 2>&1 | head -30 && ls ~/.claude/skills ~/.agents/skills 2>/dev/null | grep devrelay; git config user.email; bash .git/info/leak-check.sh; uv --version; /opt/homebrew/bin/python3.13 --version; /opt/homebrew/bin/temporal --version; ollama list
Sun Nov 1 00:30:04 IST 2026
755 .agents/
755 .claude/
755 .git/
755 docs/
644 .gitignore 88B
644 AGENTS.md 1.8K
755 devrelay-challenges/
755 devrelay-community-wisdom/
755 devrelay-knowledge/
755 devrelay-mlh-submissions/
755 devrelay-navigator/
755 devrelay-offers/
755 devrelay-publishing/
755 devrelay-sessions/
755 devrelay-sponsor-skills/
755 skills -> ../.agents/skills 17B
…
Step 0 passes. Reading the spec and brand files.
$ cd ~/github/prateek11rai/undertaker && cat docs/design/spec.md && cat .gitignore && ls -R docs && wc -c docs/brand/* docs/design/*
# Undertaker · spec
Undertaker finds the Temporal workflows that will never finish and tells you why. Point it at a Temporal server. It sweeps every running workflow, sorts the undead into causes using Temporal's own data, and writes a **certificate of undeath** for each one: the cause, the evidence (history event ids, attempts, errors), the line of your code if you give it the source, and the ex …
$ cd ~/github/prateek11rai/undertaker && cat docs/brand/theme-preview.html docs/brand/logo.svg && cat docs/design/plan.md && sed -n '/## Answers/,$p' docs/design/build-brief.md
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Undertaker · theme preview</title>
<style>
:root {
--bg: #0f1211; /* crypt */
--panel: #161b19; /* slab */
--line: #2a312e; /* mortar */
--text: #e9e4d8; /* bone */
--muted: #8f968f; /* ash */
--accent: #8be3a0; /* ectoplasm: the one bright colour */
--orphaned: #a3a7a3;
…
Step 1: skeleton.
$ cd ~/github/prateek11rai/undertaker && mkdir -p src/undertaker/{temporal,diagnose,explain,morgue/templates,morgue/static,demo} tests && echo 3.13 > .python-version && cat > pyproject.toml <<'EOF'
[project]
name = "undertaker"
version = "0.1.0"
description = "Finds the Temporal workflows that will never finish and writes a checked certificate of undeath for each, with a local Gemma."
readme = "README.md"
license = "MIT"
requires-python = ">=3.13"
dependencies = [
"temporalio==1.34.0",
"fa …
Using CPython 3.13.12 interpreter at: /opt/homebrew/opt/python@3.13/bin/python3.13
Resolved 29 packages in 413ms
$ cd ~/github/prateek11rai/undertaker && grep -oE 'https?://[^/"]+' uv.lock | sort | uniq -c; grep -A1 '^name = "\(temporalio\|fastapi\|uvicorn\|jinja2\|pydantic\|pytest\|pytest-asyncio\|ruff\|hatchling\)"' uv.lock | grep -v '^--'
248 https://files.pythonhosted.org
28 https://pypi.org
1:58:name = "fastapi"
2:59-version = "0.162.0"
4:102:name = "jinja2"
5:103-version = "3.1.6"
7:269:name = "pydantic"
8:270-version = "2.14.1"
10:380:name = "pytest"
11:381-version = "9.1.1"
13:396:name = "pytest-asyncio"
14:397-version = "1.4.0"
16:408:name = "ruff"
17:409-version = "0.17.2"
19:445:name = "temporalio"
20:446-version = "1.34 …
Pin exact versions, then sync.
$ cd ~/github/prateek11rai/undertaker && python3 - <<'EOF'
import re,pathlib
p=pathlib.Path("pyproject.toml");s=p.read_text()
s=s.replace('"fastapi",','"fastapi==0.162.0",').replace('"uvicorn",','"uvicorn==0.54.0",').replace('"jinja2",','"jinja2==3.1.6",').replace('"pydantic",','"pydantic==2.14.1",')
s=s.replace('dev = ["pytest", "pytest-asyncio", "ruff"]','dev = ["pytest==9.1.1", "pytest-asyncio==1.4.0", "ruff==0.17.2"]')
p.write_text(s)
EOF
uv lock 2>&1|tail -1 && uv sync 2>&1 | tail -2 && uv ru …
Exit code 1
Resolved 29 packages in 12ms
hint: Build failures usually indicate a problem with the package or the build environment
Building undertaker @ file://~/github/prateek11rai/undertaker
Downloading ruff (9.0MiB)
× Failed to build `undertaker @
│ file://~/github/prateek11rai/undertaker`
├─▶ The build backend returned an error
╰─▶ Call to `hatchling.build.build_editable` failed (e …
$ cd ~/github/prateek11rai/undertaker && printf '# undertaker\n\nFinds the Temporal workflows that will never finish, and tells you why.\n' > README.md && touch src/undertaker/__init__.py src/undertaker/{temporal,diagnose,explain,morgue,demo}/__init__.py && uv sync 2>&1 | tail -2
+ undertaker==0.1.0 (from file://~/github/prateek11rai/undertaker)
+ uvicorn==0.54.0
Locked. Now a quick probe of Ollama's JSON-schema output with gemma4 so I know the call shape before writing the model layer.
$ cd /tmp && time curl -s http://127.0.0.1:11434/api/chat -d '{"model":"gemma4:e4b","stream":false,"think":false,"options":{"temperature":0.2},"format":{"type":"object","properties":{"headline":{"type":"string"},"epitaph":{"type":"string"}},"required":["headline","epitaph"]},"messages":[{"role":"user","content":"A workflow retried a payment 47 times. Reply in JSON."}]}' | python3 -c "import sys,json;d=json.load(sys.stdin);print(d.get('message'),d.get('error'))"; curl -s http://127.0.0.1:11434/api/ …
{'role': 'assistant', 'content': '{\n "headline": "Payment Retry Summary",\n "epitaph": "The workflow demonstrated significant persistence in attempting the payment transaction. A total of 47 retries were executed before the final outcome was reached (or the process terminated). This high number suggests potential intermittent network issues, transient service unavailability, or a complex retry …
Ollama works with think:false + JSON schema. Writing config and the data layer.
~/github/prateek11rai/undertaker/src/undertaker/config.py (60 lines)
File created successfully at: ~/github/prateek11rai/undertaker/src/undertaker/config.py (file state is current in your context — no need to Read it back)
~/github/prateek11rai/undertaker/src/undertaker/temporal/evidence.py (184 lines)
File created successfully at: ~/github/prateek11rai/undertaker/src/undertaker/temporal/evidence.py (file state is current in your context — no need to Read it back)
Now the rules.
~/github/prateek11rai/undertaker/src/undertaker/diagnose/rules.py (406 lines)
File created successfully at: ~/github/prateek11rai/undertaker/src/undertaker/diagnose/rules.py (file state is current in your context — no need to Read it back)
Now the Temporal client layer. Let me check proto field names first.
$ cd ~/github/prateek11rai/undertaker && uv run python - <<'EOF'
from temporalio.api.workflow.v1 import PendingActivityInfo, PendingWorkflowTaskInfo, PendingChildExecutionInfo, WorkflowExecutionInfo
from temporalio.api.workflowservice.v1 import DescribeWorkflowExecutionResponse, DescribeTaskQueueRequest, ResetWorkflowExecutionRequest
from temporalio.api.taskqueue.v1 import PollerInfo
for c in [PendingActivityInfo, PendingWorkflowTaskInfo, PendingChildExecutionInfo, DescribeWorkflowExecutionRespons …
PendingActivityInfo ['activity_id', 'activity_type', 'state', 'heartbeat_details', 'last_heartbeat_time', 'last_started_time', 'attempt', 'maximum_attempts', 'scheduled_time', 'expiration_time', 'last_failure', 'last_worker_identity', 'use_workflow_build_id', 'last_independently_assigned_build_id', 'last_worker_version_stamp', 'current_retry_interval', 'last_attempt_complete_time', 'next_attempt_s …
~/github/prateek11rai/undertaker/src/undertaker/temporal/client.py (274 lines)
File created successfully at: ~/github/prateek11rai/undertaker/src/undertaker/temporal/client.py (file state is current in your context — no need to Read it back)
Now the demo: activities, workflows, worker, seed, server lifecycle, manifest.
$ mkdir -p ~/github/prateek11rai/undertaker/src/undertaker/demo && cat > ~/github/prateek11rai/undertaker/src/undertaker/demo/activities.py <<'EOF'
"""Haunted-bakery activities. Some are broken on purpose."""
import asyncio
from temporalio import activity
@activity.defn
async def charge_card(amount: int) -> str:
"""Charge the customer for an order."""
if amount <= 0:
raise ValueError("amount must be positive")
return f"charged {amount}"
@activity.defn
async def bake_batch …
(Bash completed with no output)
$ cd ~/github/prateek11rai/undertaker/src/undertaker/demo && cat > worker.py <<'EOF'
"""The demo worker: runs the bakery's workflows (version B) and activities."""
import asyncio
import signal
import sys
from temporalio.client import Client
from temporalio.worker import Worker
from undertaker.demo.activities import ACTIVITIES
from undertaker.demo.workflows import ORDERS, WORKFLOWS
async def run(address: str) -> None:
"""Poll the orders queue until SIGTERM or SIGINT."""
client = await …
(Bash completed with no output)
Now sweep and a first CLI to try the demo end to end.
$ cd ~/github/prateek11rai/undertaker && cat > src/undertaker/sweep.py <<'EOF'
"""One sweep: list running workflows, snapshot each, and run the rules."""
from datetime import UTC, datetime
from undertaker.config import Thresholds
from undertaker.diagnose.rules import Diagnosis, diagnose
from undertaker.temporal.client import TemporalReader
from undertaker.temporal.evidence import Snapshot
async def sweep(reader: TemporalReader, th: Thresholds) -> list[Diagnosis]:
"""Diagnose every running …
(Bash completed with no output)
$ python3 - <<'EOF'
import pathlib
p=pathlib.Path("src/undertaker/cli.py");s=p.read_text()
s=s.replace("import json\n","").replace('\n__all__ = ["main", "settings_from", "json"]\n',"")
p.write_text(s)
EOF
cat > src/undertaker/commands.py <<'EOF'
"""What each subcommand does."""
import argparse
import json
import platform
import urllib.request
from datetime import UTC, datetime
from undertaker.config import DATA_DIR, TEMPORAL_BIN, Settings
from undertaker.diagnose.rules import Diagnosis, short_du …
Command did not complete within its 120s timeout and was moved to the background (ID: b7v858b1c). Output is being written to: <tmp> You will be notified when it completes. If it is still running after 30m in the background, it will be stopped and you will be notified. It is running now: do not start a second copy while it runs. If it is no longer needed, stop it with TaskStop. No output had been r …
$ cat .data/*.pid; tail -20 .data/temporal.log; tail -30 .data/worker.log 2>/dev/null; /opt/homebrew/bin/temporal workflow list --address 127.0.0.1:7233 2>&1 | head -20
4235742365Temporal CLI 1.9.1 (Server 1.32.0, UI 2.54.1)
Temporal Server: localhost:7233
Temporal Persistence: ~/github/prateek11rai/undertaker/.data/temporal.db
Temporal UI: http://localhost:8233
Temporal Metrics: http://localhost:63363/metrics
Traceback (most recent call last):
File "<frozen runpy>", line 198, in _run_module_as_main
File "<frozen runpy>", line 88, in _run_c …
$ pkill -f "undertaker demo up"; sleep 1; uv run undertaker demo down; pgrep -fl "temporal|undertaker"; python3 - <<'EOF'
import pathlib
p=pathlib.Path("src/undertaker/demo/worker.py");s=p.read_text()
s=s.replace(""" worker = Worker(client, task_queue=ORDERS, workflows=WORKFLOWS, activities=ACTIVITIES,
graceful_shutdown_timeout=None)""",""" worker = Worker(client, task_queue=ORDERS, workflows=WORKFLOWS, activities=ACTIVITIES)""")
p.write_text(s)
p=pathlib.Path("src/undert …
Exit code 127
Stopped: temporal dev server
30920 /bin/sh -c { trap '' TERM; read -r _ <&3; kill -TERM 0; sleep 5; kill -KILL 0; } </dev/null >/dev/null 2>&1 &
exec "$@" 3<&- t3-claude claude --output-format stream-json --verbose --input-format stream-json --thinking adaptive --thinking-display summarized --effort high --model claude-opus-5-5 --permission-prompt-tool stdio --allowedTools mcp__t3-co …
$ uv run undertaker demo up && sleep 25 && uv run undertaker sweep --demo
Demo up: Temporal 127.0.0.1:7233 (UI http://127.0.0.1:8233), worker running.
Seeded 12 running workflows and 3 completed ones.
Wait about 30 s for the thresholds, then: uv run undertaker sweep --demo
10 undead · 2 alive · 127.0.0.1:7233 default
WORKFLOW ID TYPE CAUSE
order-1046 RecipeWorkflow broken-code undead for 25 s · task attempt 4
order- …
All 12 demo workflows diagnosed correctly on the first sweep, including changed-code. Let me check the JSON evidence, then lint and commit.
$ uv run undertaker sweep --demo --json | python3 -c "
import json,sys
for d in json.load(sys.stdin):
print(d['workflow_id'], d['cause'], [ (e['key'], e['text']) for e in d['evidence']])"; lsof -nP -iTCP -sTCP:LISTEN | grep -E 'temporal|python' | awk '{print $1,$9}'
order-1046 broken-code [('e4', 'WorkflowTaskFailed WORKFLOW_WORKER_UNHANDLED_FAILURE'), ('task_attempt', '4'), ('failure', "KeyError: 'sugar'"), ('frame1', 'workflows.py:58 in run')]
order-1047 changed-code [('e15', 'WorkflowTaskFailed NON_DETERMINISTIC_ERROR'), ('task_attempt', '3'), ('failure', "[TMPRL1100] Nondeterminism error: Activity type of scheduled event 'bake' does not match activity typ …
Evidence looks right; the waiting-signal metadata query returned payment_received. Sort by spec order, lint, commit.
$ python3 - <<'EOF'
import pathlib
p=pathlib.Path("src/undertaker/sweep.py");s=p.read_text()
s=s.replace("from undertaker.diagnose.rules import Diagnosis, diagnose","from undertaker.diagnose.rules import CAUSES, Diagnosis, diagnose")
s=s.replace(" return sorted(results.values(), key=lambda d: (not d.undead, d.cause, d.workflow_id))",
" order = list(CAUSES)\n return sorted(results.values(), key=lambda d: (order.index(d.cause), d.workflow_id))")
p.write_text(s)
EOF
uv run ruff format -q . & …
E501 Line too long (108 > 100)
--> src/undertaker/commands.py:125:101
|
123 | pass
124 | print(
125 | f"temporal {TEMPORAL_BIN} {'found' if __import__('os').path.exists(TEMPORAL_BIN) else 'missing'}"
| ^^^^^^^^
126 | )
127 | try:
|
E501 Line too long (1 …
$ python3 - <<'EOF'
import pathlib
p=pathlib.Path("src/undertaker/commands.py");s=p.read_text()
s=s.replace(""" print(
f"temporal {TEMPORAL_BIN} {'found' if __import__('os').path.exists(TEMPORAL_BIN) else 'missing'}"
)""",""" found = "found" if os.path.exists(TEMPORAL_BIN) else "missing"
print(f"temporal {TEMPORAL_BIN} {found}")""")
s=s.replace("import json\n","import json\nimport os\n",1)
p.write_text(s)
p=pathlib.Path("src/undertaker/diagnose/rules.py");s=p.read_text( …
All checks passed!
?? .claude/
?? .gitignore
?? .python-version
?? AGENTS.md
?? README.md
?? docs/
?? pyproject.toml
?? src/
?? uv.lock
[This command modified 1 file you've previously read: src/undertaker/diagnose/rules.py. Call Read before editing.]
$ cat .git/info/exclude; ls -la .claude; bash .git/info/leak-check.sh && git add .gitignore .python-version AGENTS.md README.md docs pyproject.toml uv.lock .claude/skills src/undertaker/__init__.py src/undertaker/config.py src/undertaker/cli.py src/undertaker/commands.py src/undertaker/*/__init__.py && git status --short
.agents/skills/devrelay-*/
.claude/settings.local.json
.env
.worktrees/
755 skills -> ../.agents/skills 17B
leak-check: clean
ok 23 files changed, 1481 insertions(+)
A .claude/skills
A .gitignore
A .python-version
A AGENTS.md
A README.md
A docs/brand/logo.svg
A docs/brand/theme-preview.html
A docs/design/build-brief.md
A docs/design/plan.md
A docs/design/ship.md
A docs/design/spec.md
…
$ git commit -q -m "Skeleton: uv project, CLI, doctor, planner docs
Python 3.13 from Homebrew, public PyPI with the 7-day cooldown, exact pins.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>" && git add src && bash .git/info/leak-check.sh && git commit -q -m "Demo bakery, Temporal reader, the nine rules and sweep
demo up/down/status runs the Homebrew dev server and a background worker,
seeds one workflow per cause (order-1047 via a version A worker, then B).
Co-Authored-By: Claude Opu …
Exit code 1
ok
ok 10 files changed, 1519 insertions(+)
docs/design/ship.md:17:Make each image as an HTML page in `docs/brand/` in the app's own colours and fonts (system fonts only; pages load nothing from the netwo
t3code@users.noreply.github.com
LEAK-CHECK FAIL: a commit has an author/committer email other than the noreply address
1
LEAK-CHECK FAIL: commit messages or past diffs match the patter …
$ git log --format='%h %an <%ae> | %cn <%ce> | %s'; git config --show-origin --get-all user.email; env | grep -i GIT_ | sed 's/=.*/=…/'
467aa34 prateek11rai <165780365+prateek11rai@users.noreply.github.com> | prat...
file:~/.gitconfig 165780365+prateek11rai@users.noreply.github.com
file:.git/config 165780365+prateek11rai@users.noreply.github.com
GIT_EDITOR=…
$ git log --format='%H%n%an <%ae>%n%cn <%ce>%n%B---' | cat; git var -l 2>/dev/null | grep -i ident
467aa3470e2ba3db1b2f11433b91012df82e8908
prateek11rai <165780365+prateek11rai@users.noreply.github.com>
prateek11rai <165780365+prateek11rai@users.noreply.github.com>
Skeleton: uv project, CLI, doctor, planner docs
Python 3.13 from Homebrew, public PyPI with the 7-day cooldown, exact pins.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---
29:GIT_COMMITTER_IDENT=prateek11rai <165780365+ …
Quick status: first commit landed; the leak check flagged docs/design/ship.md (the planner's new ship brief) in it. Checking what matched.
$ grep -n "t3code" docs/design/ship.md | cut -c1-200; sed -n '17p' docs/design/ship.md | cut -c1-400; ls .git/hooks | grep -v sample
Exit code 1
Make each image as an HTML page in `docs/brand/` in the app's own colours and fonts (system fonts only; pages load nothing from the network). Render with a small `docs/brand/render.cjs` (Playwright + the installed Google Chrome, `channel: "chrome"`, abort every non-`file:` request, exact viewport, device scale 1 for the committed file; check one 2x render for crispness in `/tmp`). Play …
$ sed -n '1,80p' .git/info/leak-check.sh | grep -v -i -E 'token|key=' | head -60
#!/usr/bin/env bash
# Local leak check for this repo (lives in .git/info, never committed).
# Run before every commit (staged changes) and before every push (all history).
# A check fails when it prints anything; output is cut to 160 chars per line.
set -uo pipefail
cd "$(git rev-parse --show-toplevel)"
pat="$(git rev-parse --git-dir)/info/leak-patterns"
want_email="165780365+prateek11rai@users.no …
$ git for-each-ref --format='%(refname) %(objectname:short)' ; git log --all --format='%h %ae %s' | grep -v 165780365 | head; while IFS= read -r p; do [ -n "$p" ] && sed -n '17p' docs/design/ship.md | grep -q -i -E -e "$p" && echo "pattern line matched: $(grep -n -x -F -e "$p" .git/info/leak-patterns | cut -d: -f1)"; done < .git/info/leak-patterns
Exit code 1
refs/heads/main 467aa34
refs/t3/orchestration-v2/checkpoints/NjViOThlMTdkNTI2ZjEzMzQ0ZDE1MmNhZjc3Yjk5ZWM/ordinal/0 4f5c7fd
4f5c7fd t3code@users.noreply.github.com t3 checkpoint ref=refs/t3/orchestration-v2/checkpoints/NjViOThlMTdkNTI2ZjEzMzQ0ZDE1MmNhZjc3Yjk5ZWM/ordinal/0
pattern line matched: 6
$ git reset -q --soft HEAD~1 2>/dev/null || true; git status --short | head;
A src/undertaker/demo/activities.py
A src/undertaker/demo/manifest.py
A src/undertaker/demo/seed.py
A src/undertaker/demo/server.py
A src/undertaker/demo/worker.py
A src/undertaker/demo/workflows.py
A src/undertaker/diagnose/rules.py
A src/undertaker/sweep.py
A src/undertaker/temporal/client.py
A src/undertaker/temporal/evidence.py
$ git log --oneline && git reset -q && git rm -q --cached docs/design/ship.md && git commit -q --amend --no-edit && git show --stat HEAD | grep ship; git add src && bash .git/info/leak-check.sh 2>&1 | grep -v '^t3code' ; git log --format='%B' | grep -c -i -E -f .git/info/leak-patterns
467aa34 Skeleton: uv project, CLI, doctor, planner docs
ok
ok 10 files changed, 1519 insertions(+)
2:LEAK-CHECK FAIL: a commit has an author/committer email other than the noreply address
0
Only the T3 checkpoint ref remains (outside main). Committing.
$ git commit -q -m "Demo bakery, Temporal reader, the nine rules and sweep
demo up/down/status runs the Homebrew dev server and a background worker,
seeds one workflow per cause (order-1047 via a version A worker, then B).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>" && git log main --format='%ae %ce' | sort -u && git log --oneline
ok
165780365+prateek11rai@users.noreply.github.com 165780365+prateek11rai@users....
1b29f1d Demo bakery, Temporal reader, the nine rules and sweep
7bbae1c Skeleton: uv project, CLI, doctor, planner docs
Sending the planner a short non-blocking note about the two leak-check findings, then continuing.
{
"query": "select:mcp__t3-code__t3_thread_send",
"max_results": 1
}
{"threadId": "mcp:d6d84144-d1b5-442b-82e6-d79b07028f77", "mode": "queue", "message": "Builder (undertaker core), progress note + 2 questions, not blocked. 01:05 IST.\n\nProgress: steps 1–2 committed (7bbae1c skeleton, 1b29f1d demo + reader + nine rules + sweep). `demo up` + `sweep --demo` already gi …
{
"threadId": "mcp:d6d84144-d1b5-442b-82e6-d79b07028f77",
"link": "<t3-thread://v2/fc11dac6-e4bb-4bb7-b6a4-eaa02f98b99e/mcp:d6d84144-d1b5-442b-82e6-d79b07028f77>",
"messageId": "message:mcp:1c783c84-5205-4a43-b079-ea112c84f11c:thread-send:bea2c6f9-0064-4f23-ae60-ba4287d94357",
"runId": "run:thread:mcp%3Ad6d84144-d1b5-442b-82e6-d79b07028f77:ordinal:8",
"status": "starting",
"delivery": "started"
}
Now unit tests for the rules (good and near-miss per cause).
~/github/prateek11rai/undertaker/tests/helpers.py (67 lines)
File created successfully at: ~/github/prateek11rai/undertaker/tests/helpers.py (file state is current in your context — no need to Read it back)
~/github/prateek11rai/undertaker/tests/test_rules.py (183 lines)
File created successfully at: ~/github/prateek11rai/undertaker/tests/test_rules.py (file state is current in your context — no need to Read it back)
$ touch tests/__init__.py && rm tests/__init__.py; printf 'import sys\nfrom pathlib import Path\n\nsys.path.insert(0, str(Path(__file__).parent))\n' > tests/conftest.py && uv run pytest -q 2>&1 | tail -15
.......................... [100%]
26 passed in 0.21s
Planner answers (also appended as A1–A3 under ## Answers in docs/design/build-brief.md):
1. ship.md is reworded and passes the leak check. Include it in your next commit; don't act on it yet.
2. I changed the local leak-check to scan only pushable refs (--branches --tags --remotes), so T3's refs/t3/... checkpoints are ignored. Don't touch them. It prints clean now.
3. Don't pin SHAs from memory. Use exactly these (verified, same as my earlier projects whose CI passed): actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 and actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5. Install uv in CI with python -m pip install uv==0.12.3, not a third-party action, never curl | sh.
Good progress, all 12 causes from the demo already. Carry on.
Rules pass. Now the @pytest.mark.temporal integration test against a Homebrew dev server.
Planner answers (also appended as A1–A3 under ## Answers in docs/design/build-brief.md):
1. ship.md is reworded and passes the leak check. Include it in your next commit; don't act on it yet.
2. I changed the local leak-check to scan only pushable refs (--branches --tags --remotes), so T3's refs/t3/... checkpoints are ignored. Don't touch them. It prints clean now.
3. Don't pin SHAs from memory. Use exactly these (verified, same as my earlier projects whose CI passed): actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 and actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5. Install uv in CI with python -m pip install uv==0.12.3, not a third-party action, never curl | sh.
Good progress, all 12 causes from the demo already. Carry on.
Noted all three. Writing the integration test now.
~/github/prateek11rai/undertaker/tests/test_temporal.py (60 lines)
File created successfully at: ~/github/prateek11rai/undertaker/tests/test_temporal.py (file state is current in your context — no need to Read it back)
$ uv run pytest -q -m temporal 2>&1 | tail -15
Pytest: 1 passed
Integration test passes, including terminate and reset. Committing step 3 (with ship.md).
$ uv run ruff format -q . && uv run ruff check -q && git add tests docs/design src && bash .git/info/leak-check.sh && git commit -q -m "Rule unit tests and a Temporal integration test
Good and near-miss cases for every cause; the integration test runs the
Homebrew dev server and checks causes, terminate and reset.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>" && git log --oneline | head -1
[]
ok 6 files changed, 415 insertions(+)
leak-check: clean
ok
61d4217 Rule unit tests and a Temporal integration test
[This command modified 3 files you've previously read: tests/test_rules.py, tests/helpers.py, tests/test_temporal.py. Call Read before editing.]
Step 4: remedies and template certificates.
~/github/prateek11rai/undertaker/src/undertaker/diagnose/remedies.py (90 lines)
File created successfully at: ~/github/prateek11rai/undertaker/src/undertaker/diagnose/remedies.py (file state is current in your context — no need to Read it back)
~/github/prateek11rai/undertaker/src/undertaker/explain/source.py (86 lines)
File created successfully at: ~/github/prateek11rai/undertaker/src/undertaker/explain/source.py (file state is current in your context — no need to Read it back)
Now the certificate core: templates, checks, prompt.
~/github/prateek11rai/undertaker/src/undertaker/explain/certificate.py (241 lines)
File created successfully at: ~/github/prateek11rai/undertaker/src/undertaker/explain/certificate.py (file state is current in your context — no need to Read it back)
Now the Ollama client, the cache, and the writer flow (model → check → one retry → template).
$ cd ~/github/prateek11rai/undertaker/src/undertaker/explain && cat > model.py <<'EOF'
"""Ollama over HTTP with the standard library; JSON-schema output."""
import json
import urllib.request
from undertaker.explain.certificate import SCHEMA
class ModelError(Exception):
"""Ollama could not be reached or did not answer with JSON."""
def chat(base: str, model: str, messages: list[dict], timeout: float = 180) -> dict:
"""One chat call returning the parsed JSON object.
Args:
…
(Bash completed with no output)
$ cd ~/github/prateek11rai/undertaker && python3 - <<'EOF'
import pathlib
p=pathlib.Path("src/undertaker/explain/writer.py");s=p.read_text()
s=s.replace(""" cert = None
for tries in (1, 2):
try:""",""" cert = None
done = 0
for tries in (1, 2):
done = tries
try:""")
s=s.replace("cert.model, cert.tries = s.model, len([r for r in rejected if r]) and 2","cert.model, cert.tries = s.model, done")
p.write_text(s)
EOF
cat > src/undertaker/explain/render.py <<'EOF' …
CERTIFICATE OF UNDEATH
order-1042 · run 01a1935d… · PayOrderWorkflow · bakery-orders
template certificate (model off)
----------------------------------------------------------------------------------------
Born 01 Nov 2026, 00:39:29
Last sign of life event 5, ActivityTaskScheduled, 01 Nov 2026, 00:39:29
Cause Dies and rises, forever (retry-loop)
Undead for …
Template path works with source excerpt. Now try the model on three workflows.
$ for w in order-1042 order-1048 order-1047; do /usr/bin/time -p uv run undertaker inspect $w --demo --source src/undertaker/demo 2>&1 | grep -E "^(written|template|Headline|Diagnosis|Next| |\"|real)" | grep -v -E '^\s+[0-9>]|^ temporal|^ [a-z_0-9]+ {3,}' ; echo ====; done; uv run python -c "
import sqlite3,json
for k,b in sqlite3.connect('.data/certificates.sqlite').execute('select key,body from certs'):
d=json.loads(b); print(k, d['source'], d['tries'], d['seconds'], d['rejected'])"
Exit code 1
1:written by gemma4:e4b, every fact checked
2:Headline Workflow Stuck in Activity Retry Loop Due to Constant Failure
3:Diagnosis The `order-1042` workflow is stuck because the `charge_card`
4: activity repeatedly fails with a `ValueError: amount must be
5: positive`. This failure occurs on every attempt, and the activity's
6: …
$ for w in order-1048 order-1047 order-1050; do /usr/bin/time -p uv run undertaker inspect $w --demo --source src/undertaker/demo 2>&1 | grep -E "^(written|template|Headline|Diagnosis|Next| |\"|real)"; echo "--"; done; uv run python -c "
import sqlite3,json
for k,b in sqlite3.connect('.data/certificates.sqlite').execute('select key,body from certs'):
d=json.loads(b); print(k, d['source'], d['tries'], d['seconds'], d['rejected'])"
3:written by gemma4:e4b, every fact checked
9:Headline Workflow Stuck Waiting for Unreceived Payment Signal
10:Diagnosis The `AwaitPaymentWorkflow` in `order-1048` is stuck because it is
11: waiting for the `payment_received` signal. The evidence shows no
12: activity, timer, child, or workflow task is scheduled to wake it up.
13:Next step …
Model certificates pass checks at ~8 s each (first call 25 s incl. load). Now tests for the checks, source safety, and the writer flow.
~/github/prateek11rai/undertaker/tests/test_certificate.py (208 lines)
File created successfully at: ~/github/prateek11rai/undertaker/tests/test_certificate.py (file state is current in your context — no need to Read it back)
$ uv run pytest -q 2>&1 | tail -25
Pytest: 60 passed, 1 failed
Failures:
1. [FAIL] test_templates_pass_their_own_checks[changed-code]
tests/test_certificate.py:75: in test_templates_pass_their_own_checks
assert check(raw, d, []) == []
E assert ["code 'workf...the evidence"] == []
[full output: ~/Library/Application Support/rtk/tee/1793474172_pytest.log]
Template mentions workflow.patched (Temporal's own API, not in evidence). Allow it for changed-code in the corpus.
$ python3 - <<'EOF'
import pathlib
p=pathlib.Path("src/undertaker/explain/certificate.py");s=p.read_text()
s=s.replace(""" bits = [d.workflow_id, d.run_id, d.workflow_type, d.task_queue, d.cause, d.label, d.rule]
""",""" bits = [d.workflow_id, d.run_id, d.workflow_type, d.task_queue, d.cause, d.label, d.rule]
if d.cause == "changed-code":
bits.append("workflow.patched")
""")
p.write_text(s)
EOF
uv run pytest -q 2>&1 | tail -3
............................................................. [100%]
61 passed, 1 deselected in 0.27s
Now the real-model test.
$ cat > tests/test_model.py <<'EOF'
from datetime import timedelta
from pathlib import Path
import pytest
from helpers import NOW, activity, failure, snap
import undertaker.demo.activities as demo_activities
from undertaker.config import Settings, Thresholds
from undertaker.diagnose.rules import diagnose
from undertaker.explain.certificate import check
from undertaker.explain.writer import write
pytestmark = pytest.mark.model
def test_gemma_writes_a_checked_certificate(tmp_path):
src = Pa …
model 1 8.9 []
.
1 passed, 62 deselected in 9.15s
$ uv run ruff format -q . && uv run ruff check 2>&1 | tail -20
[]
[This command modified 4 files you've previously read: src/undertaker/diagnose/remedies.py, src/undertaker/explain/source.py, src/undertaker/explain/certificate.py, tests/test_certificate.py. Call Read before editing.]
$ git add src tests && bash .git/info/leak-check.sh && git commit -q -m "Remedies, template certificates, inspect
Every cause has a plain sentence and exact temporal commands; inspect prints
the certificate as text, with source excerpts when --source is given.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>" -- src/undertaker/diagnose/remedies.py src/undertaker/explain/render.py src/undertaker/explain/source.py && git commit -q -m "Model certificates: Gemma prompt, checks, one retry, fal …
ok 9 files changed, 1046 insertions(+)
leak-check: clean
ok
ok
8fb1961 Model certificates: Gemma prompt, checks, one retry, fallback, cache
80eddb5 Remedies, template certificates, inspect
61d4217 Rule unit tests and a Temporal integration test
Sun Nov 1 00:46:50 IST 2026
Steps 4–5 committed. Step 6: the morgue. App first.
~/github/prateek11rai/undertaker/src/undertaker/morgue/app.py (205 lines)
File created successfully at: ~/github/prateek11rai/undertaker/src/undertaker/morgue/app.py (file state is current in your context — no need to Read it back)
Tidy two spots: certs keyed by (id, run) so a resurrected run doesn't show the old certificate, and use a lifespan instead of the deprecated startup hook.
$ python3 - <<'EOF'
import pathlib
p=pathlib.Path("src/undertaker/morgue/app.py");s=p.read_text()
s=s.replace(""" w = self.certs.get(d.workflow_id)
if w and w.cert.source and self.certs[d.workflow_id] and w is not None:
return w, False
from undertaker.explain.source import excerpts
return Written""",""" w = self.certs.get((d.workflow_id, d.run_id))
if w:
return w, False
return Written""")
s=s.replace(""" …
(Bash completed with no output)
Now the look: theme.css, templates, and morgue.js.
~/github/prateek11rai/undertaker/src/undertaker/morgue/static/theme.css (111 lines)
File created successfully at: ~/github/prateek11rai/undertaker/src/undertaker/morgue/static/theme.css (file state is current in your context — no need to Read it back)
~/github/prateek11rai/undertaker/src/undertaker/morgue/templates/base.html (25 lines)
File created successfully at: ~/github/prateek11rai/undertaker/src/undertaker/morgue/templates/base.html (file state is current in your context — no need to Read it back)
~/github/prateek11rai/undertaker/src/undertaker/morgue/templates/morgue.html (21 lines)
File created successfully at: ~/github/prateek11rai/undertaker/src/undertaker/morgue/templates/morgue.html (file state is current in your context — no need to Read it back)
~/github/prateek11rai/undertaker/src/undertaker/morgue/templates/certificate.html (63 lines)
File created successfully at: ~/github/prateek11rai/undertaker/src/undertaker/morgue/templates/certificate.html (file state is current in your context — no need to Read it back)
~/github/prateek11rai/undertaker/src/undertaker/morgue/static/morgue.js (69 lines)
File created successfully at: ~/github/prateek11rai/undertaker/src/undertaker/morgue/static/morgue.js (file state is current in your context — no need to Read it back)
Start the morgue against the running demo and check it in Playwright.
$ (uv run undertaker serve --demo --source src/undertaker/demo > .data/serve.log 2>&1 &) ; sleep 4; curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8666/ ; curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8666/w/order-1042; cat .data/serve.log
200
200
{"query": "select:mcp__plugin_playwright_playwright__browser_navigate,mcp__plugin_playwright_playwright__browser_take_screenshot,mcp__plugin_playwright_playwright__browser_snapshot,mcp__plugin_playwright_playwright__browser_click,mcp__plugin_playwright_playwright__browser_handle_dialog,mcp__plugin_p …
{
"width": 1360,
"height": 900
}
### Ran Playwright code
```js
await page.setViewportSize({ width: 1360, height: 900 });
```
### Page
- Page URL: about:blank
{
"url": "http://127.0.0.1:8666/"
}
### Ran Playwright code
```js
await page.goto('http://127.0.0.1:8666/');
```
### Page
- Page URL: http://127.0.0.1:8666/
- Page Title: Morgue · undertaker
### Snapshot
- [Snapshot](.playwright-mcp/page-2026-10-31T19-18-58-359Z.yml)
{
"scale": "css",
"filename": "/tmp/undertaker-shots/morgue-draft.png"
}
### Error
Error: File access denied: /tmp/undertaker-shots/morgue-draft.png is outside allowed roots. Allowed roots: ~/github/prateek11rai/undertaker/.playwright-mcp, ~/github/prateek11rai/undertaker
{
"scale": "css",
"filename": ".playwright-mcp/morgue-draft.png"
}
### Result
- [Screenshot of viewport](.playwright-mcp/morgue-draft.png)
### Ran Playwright code
```js
// Screenshot viewport and save it as .playwright-mcp/morgue-draft.png
await page.screenshot({
path: '.playwright-mcp/morgue-draft.png',
scale: 'css',
type: 'png'
});
```
~/github/prateek11rai/undertaker/.playwright-mcp/morgue-draft.png
The morgue matches the theme. Now a certificate page.
{
"url": "http://127.0.0.1:8666/w/order-1042"
}
### Ran Playwright code
```js
await page.goto('http://127.0.0.1:8666/w/order-1042');
```
### Page
- Page URL: http://127.0.0.1:8666/w/order-1042
- Page Title: order-1042 · undertaker
### Snapshot
- [Snapshot](.playwright-mcp/page-2026-10-31T19-19-06-548Z.yml)
{
"scale": "css",
"filename": ".playwright-mcp/cert-draft.png",
"fullPage": true
}
### Result
- [Screenshot of full page](.playwright-mcp/cert-draft.png)
### Ran Playwright code
```js
// Screenshot full page and save it as .playwright-mcp/cert-draft.png
await page.screenshot({
fullPage: true,
path: '.playwright-mcp/cert-draft.png',
scale: 'css',
type: 'png'
});
```
~/github/prateek11rai/undertaker/.playwright-mcp/cert-draft.png
$ python3 - <<'EOF'
import pathlib
p=pathlib.Path("src/undertaker/morgue/static/theme.css");s=p.read_text()
s=s.replace(""" border-radius: 6px; padding: 9px 12px; font: 12.5px/1.45 var(--mono);
word-break: break-all; }""",""" border-radius: 6px; padding: 9px 12px; font: 12.5px/1.45 var(--mono);
white-space: pre-wrap; overflow-wrap: anywhere; word-break: normal; }""")
p.write_text(s)
p=pathlib.Path("src/undertaker/morgue/templates/certificate.html");s=p.read_text()
s=s.rep …
50: self.cert_state: dict[tuple[str, str], tuple[int, str]] = {}
77: if key in self.certs and self.cert_state.get(key) == state:
80: self.cert_state[key] = state
$ python3 - <<'EOF'
import pathlib
p=pathlib.Path("src/undertaker/morgue/templates/certificate.html");s=p.read_text()
s=s.replace("{% if stale %} · written at event {{ w.cert.last_event_id }}, attempt {{ w.cert.attempt }}; a newer one is on the way{% endif %}","{% if stale %} · written at event {{ w.cert.last_event_id }}, attempt {{ w.cert.attempt }}{% endif %}")
p.write_text(s)
EOF
cat > tests/test_morgue.py <<'EOF'
from datetime import UTC, datetime
from fastapi.testclient import TestClient
fro …
E $ pip install httpx2
=========================== short test summary info ============================
ERROR tests/test_morgue.py - RuntimeError: The starlette.testclient module re...
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
2 deselected, 1 error in 0.70s
TestClient needs httpx, which isn't an approved dependency. I'll use a tiny ASGI caller in the tests instead.
$ python3 - <<'EOF'
import pathlib
p=pathlib.Path("tests/test_morgue.py");s=p.read_text()
s=s.replace("""from fastapi.testclient import TestClient
""","""import asyncio
from types import SimpleNamespace
""")
s=s.replace("""from undertaker.morgue.app import code_spans, create_app
""","""from undertaker.morgue.app import code_spans, create_app
class TestClient:
\"\"\"Minimal ASGI caller (no httpx).\"\"\"
def __init__(self, app):
self.app = app
def _call(self, method, path, h …
~/github/prateek11rai/undertaker/tests/test_morgue.py:11: PytestCollectionWarning: cannot collect test class 'TestClient' because it has a __init__ constructor (from: tests/test_morgue.py)
class TestClient:
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
66 passed, 2 deselected, 1 warning in 1.04s
$ sed -i '' 's/TestClient/AsgiClient/g' tests/test_morgue.py && uv run ruff format -q . && uv run ruff check && uv run pytest -q 2>&1 | tail -1 && pkill -f "undertaker serve"; sleep 1; (uv run undertaker serve --demo --source src/undertaker/demo > .data/serve.log 2>&1 &); sleep 3; curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8666/
I001 [*] Import block is un-sorted or un-formatted
--> tests/test_morgue.py:1:1
|
1 | / from datetime import UTC, datetime
2 | |
3 | | import asyncio
4 | | from types import SimpleNamespace
5 | | from test_certificate import DIAG
6 | |
7 | | from undertaker.config import Settings
8 | | from undertaker.morgue.app import code_spans, create_app
| |________________________________________________ …
$ uv run ruff check --fix -q && uv run ruff format -q . && uv run ruff check && uv run pytest -q 2>&1 | tail -1 && git add src tests && bash .git/info/leak-check.sh && git commit -q -m "Morgue: toe tags, filters, certificate page, guarded actions
Server-rendered on 127.0.0.1:8666 with background sweeps and certificates.
Lay to rest and Resurrect need --allow-actions, a confirm and a custom header.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>" && git log --oneline | head -1
[]
[]
Pytest: 66 passed
ok 8 files changed, 639 insertions(+)
leak-check: clean
ok
ccb1897 Morgue: toe tags, filters, certificate page, guarded actions
Step 7: eval.
$ cat > src/undertaker/evals.py <<'EOF'
"""Score a sweep against the demo manifest, and the model against its checks."""
import asyncio
import json
import statistics
from dataclasses import dataclass, field
from undertaker.config import Settings
from undertaker.demo.manifest import MANIFEST
from undertaker.diagnose.rules import Diagnosis
from undertaker.explain.writer import write
@dataclass
class Score:
"""Rule and model results for one eval run."""
found: list[str] = field(default_f …
Rules: found 10/10 · missed 0 · false alarms 0 · alive ok 2
order-1045 orphaned accepted 6.8 s
order-1050-box orphaned accepted 16.4 s
order-1044 lost-activity accepted 15.9 s
order-1046 broken-code accepted 16.0 s
order-1047 changed-code accepted 15.4 s
order-1043 heartbeat accepted 9.5 s
order-1042 ret …
$ cat > tests/test_evals.py <<'EOF'
from test_certificate import DIAG
from undertaker.evals import score
def test_score_found_missed_false_alarm_and_finished_alive():
results = [DIAG["retry-loop"], DIAG["long-sleep"]]
for d, wid in zip(results, ["a", "b"], strict=True):
d.workflow_id = wid
expected = {"a": "retry-loop", "b": "waiting-signal", "c": "alive", "x": "orphaned"}
sc = score(results, expected)
assert sc.found == ["a"]
assert ("b", "waiting-signal", "long …
68 passed, 2 deselected in 0.55s
Avoid mutating the shared fixtures:
$ python3 - <<'EOF'
import pathlib
p=pathlib.Path("tests/test_evals.py");s=p.read_text()
s=s.replace("from test_certificate import DIAG\n","from dataclasses import replace\n\nfrom test_certificate import DIAG\n")
s=s.replace(""" results = [DIAG["retry-loop"], DIAG["long-sleep"]]
for d, wid in zip(results, ["a", "b"], strict=True):
d.workflow_id = wid
""",""" results = [replace(DIAG["retry-loop"], workflow_id="a"),
replace(DIAG["long-sleep"], workflow_id="b")]
""")
…
68 passed, 2 deselected in 0.46s
leak-check: clean
c71e9fc Eval: found, missed, false alarms, model accepted/retried/fallback
Sun Nov 1 00:52:55 IST 2026
Steps 1–7 done well ahead of 03:30, so step 8 (watch) next.
$ cat > src/undertaker/watch.py <<'EOF'
"""Sweep on a timer; one macOS notification per new undead workflow, never more."""
import asyncio
import json
import platform
import shutil
import subprocess
from pathlib import Path
from undertaker.config import Settings
from undertaker.diagnose.rules import Diagnosis
SCRIPT = ["-e", "on run argv", "-e",
"display notification (item 1 of argv) with title (item 2 of argv)", "-e", "end run"]
def notify(title: str, message: str) -> bool:
""" …
69 passed, 2 deselected in 0.67s
Quick live check of watch with one notification only (pre-seeding the seen-file with all but one id).
$ python3 - <<'EOF'
import pathlib
p=pathlib.Path("src/undertaker/cli.py");s=p.read_text()
s=s.replace(""" w.add_argument("--every", default=_env("every", "60s"))
""",""" w.add_argument("--every", default=_env("every", "60s"))
w.add_argument("--once", action="store_true", help="one sweep, then exit")
""")
p.write_text(s)
p=pathlib.Path("src/undertaker/commands.py");s=p.read_text()
s=s.replace("return await watch(s)","return await watch(s, once=args.once)")
p.write_text(s)
EOF
echo '["ord …
Watching 127.0.0.1:7233 default every 60 s. Ctrl-C to stop.
order-1048: waiting for a visitor who never comes (undead for 13 min · nothing scheduled)
Watching 127.0.0.1:7233 default every 60 s. Ctrl-C to stop.
$ uv run ruff format -q . && uv run ruff check -q && uv run pytest -q 2>&1 | tail -1 && git add src tests && bash .git/info/leak-check.sh && git commit -q -m "Watch: one macOS notification per new undead workflow
Text reaches osascript as argv, never inside the script. Notified ids
persist under .data so a restart doesn't repeat them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>" && git log --oneline|head -1
[]
Pytest: 69 passed
ok 4 files changed, 82 insertions(+), 1 deletion(-)
leak-check: clean
ok
55f5ea8 Watch: one macOS notification per new undead workflow
Step 9: CI with the planner's pinned SHAs.
$ mkdir -p .github/workflows && cat > .github/workflows/test.yml <<'EOF'
name: test
on:
push:
branches: [main]
paths-ignore: ["docs/**", "**/*.md", "**/*.png", "**/*.svg", "**/*.jpg"]
pull_request:
paths-ignore: ["docs/**", "**/*.md", "**/*.png", "**/*.svg", "**/*.jpg"]
workflow_dispatch:
permissions:
contents: read
jobs:
unit:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist- …
9:TEMPORAL_BIN = "/opt/homebrew/bin/temporal"
$ sed -i '' 's|^TEMPORAL_BIN = "/opt/homebrew/bin/temporal"|TEMPORAL_BIN = os.environ.get("UNDERTAKER_TEMPORAL_BIN", "/opt/homebrew/bin/temporal")|' src/undertaker/config.py && uv run ruff check -q && uv run python -c "import yaml" 2>/dev/null; uv run python - <<'EOF'
import re,pathlib
for f in pathlib.Path(".github/workflows").glob("*.yml"):
t=f.read_text(); assert "\t" not in t; print(f, "uses:", re.findall(r"uses: (\S+)", t))
EOF
git add .github src && bash .git/info/leak-check.sh && git co …
.github/workflows/test.yml uses: ['actions/checkout@11d5960a326750d5838078e36cf38b85af677262', 'actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065', 'actions/checkout@11d5960a326750d5838078e36cf38b85af677262', 'actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065']
.github/workflows/model.yml uses: ['actions/checkout@11d5960a326750d5838078e36cf38b85af677262', 'actions/setup-pyt …
Step 9 committed. Now the acceptance click-through: fresh demo, then the morgue in Playwright.
$ pkill -f "undertaker serve"; uv run undertaker demo down && sleep 1; pgrep -fl "temporal server|undertaker.demo|undertaker serve|undertaker demo"; rm -f .data/certificates.sqlite && uv run undertaker demo up && sleep 30 && uv run undertaker sweep --demo && (uv run undertaker serve --demo --source src/undertaker/demo > .data/serve.log 2>&1 &) ; sleep 3; curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8666/
Stopped: worker, temporal dev server
Demo up: Temporal 127.0.0.1:7233 (UI http://127.0.0.1:8233), worker running.
Seeded 12 running workflows and 3 completed ones.
Wait about 30 s for the thresholds, then: uv run undertaker sweep --demo
10 undead · 2 alive · 127.0.0.1:7233 default
WORKFLOW ID TYPE CAUSE
order-1045 LegacyOrderWorkflow orphaned un …
While Gemma writes the certificates in the background, I'll write the README.
~/github/prateek11rai/undertaker/README.md (120 lines)
The file ~/github/prateek11rai/undertaker/README.md has been updated successfully. (file state is current in your context — no need to Read it back)
$ cat > LICENSE <<'EOF'
MIT License
Copyright (c) 2026 prateek11rai
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions: …
4 {'order-1045': ('model', 1, 8.4), 'order-1050-box': ('model', 1, 7.6), 'order-1044': ('model', 1, 8.2), 'order-1046': ('model', 1, 10.0)}
Certificates are coming in. Click-through: morgue and every filter.
{
"url": "http://127.0.0.1:8666/"
}
### Ran Playwright code
```js
await page.goto('http://127.0.0.1:8666/');
```
### Page
- Page URL: http://127.0.0.1:8666/
- Page Title: Morgue · undertaker
### Snapshot
- [Snapshot](.playwright-mcp/page-2026-10-31T19-25-16-223Z.yml)
{"function": "async () => {\n const out = {};\n const chips = [...document.querySelectorAll('#filters .chip')];\n for (const c of chips) {\n c.click();\n await new Promise(r => setTimeout(r, 50));\n const shown = [...document.querySelectorAll('.toetag')].filter(t => !t.classList.contains …
### Result
{
"all": {
"count": "12",
"shown": 12,
"causes": [
"orphaned",
"lost-activity",
"broken-code",
"changed-code",
"heartbeat",
"retry-loop",
"undead-child",
"long-sleep",
"waiting-signal",
"alive"
]
},
"orphaned": {
"count": "2",
"shown": 2,
"causes": [
"orphaned"
]
},
"lost-activity …
All 11 filters correct. Now Sweep now.
{
"function": "async () => { await new Promise(r => setTimeout(r, 3000)); return document.getElementById('swept').textContent + ' | at=' + document.getElementById('swept').dataset.at; }"
}
### Result
"swept 47 s ago | at=2026-10-31T19:24:36.603839+00:00"
### Ran Playwright code
```js
await page.evaluate('async () => { await new Promise(r => setTimeout(r, 3000)); return document.getElementById(\'swept\').textContent + \' | at=\' + document.getElementById(\'swept\').dataset.at; }');
```
{
"target": "#sweep",
"element": "Sweep now button"
}
### Ran Playwright code
```js
await page.locator('#sweep').click();
```
### Page
- Page URL: http://127.0.0.1:8666/
- Page Title: Morgue · undertaker
### Snapshot
- [Snapshot](.playwright-mcp/page-2026-10-31T19-25-25-543Z.yml)
{
"function": "async () => { await new Promise(r => setTimeout(r, 1500)); return document.getElementById('swept').textContent + ' | at=' + document.getElementById('swept').dataset.at; }"
}
### Result
"swept 5 s ago | at=2026-10-31T19:25:25.482855+00:00"
### Ran Playwright code
```js
await page.evaluate('async () => { await new Promise(r => setTimeout(r, 1500)); return document.getElementById(\'swept\').textContent + \' | at=\' + document.getElementById(\'swept\').dataset.at; }');
```
Sweep now works. I noticed background sweeps wait behind certificate writing (47 s gap at --every 30s); I'll decouple that after the click-through. Now the certificates and copy buttons.
{
"url": "http://127.0.0.1:8666/w/order-1042"
}
### Ran Playwright code
```js
await page.goto('http://127.0.0.1:8666/w/order-1042');
```
### Page
- Page URL: http://127.0.0.1:8666/w/order-1042
- Page Title: order-1042 · undertaker
### Snapshot
- [Snapshot](.playwright-mcp/page-2026-10-31T19-25-35-107Z.yml)
{"function": "async () => {\n const sub = document.querySelector('.cert .sub').textContent.trim();\n const diag = [...document.querySelectorAll('.field')].find(f => f.querySelector('b').textContent === 'Diagnosis')?.innerText;\n const btns = [...document.querySelectorAll('.btns button')].map(b => …
### Result
{
"sub": "order-1042 · run 01a1936b… · template certificate · the model certificate is still being written",
"diag": "Diagnosis\nAn activity that fails the same way, retried forever\nActivity charge_card has failed 80 times with the same ValueError: amount must be positive. The error comes from activities.py:12. Its retry policy has no maximum, so it will retry forever.\nTemplate ce …
Copy and disabled actions verified. Waiting for the remaining model certificates.
$ sleep 45; curl -s http://127.0.0.1:8666/api/state | python3 -c "
import json,sys; d=json.load(sys.stdin); print(len(d['certificates']), {k:(v['source'],v['tries'],v['seconds']) for k,v in d['certificates'].items()})"
<tool_use_error>Blocked: sleep 45 followed by: rtk curl -s http://127.0.0.1:8666/api/state python3 -c "
import json,sys; d=json.load(sys.stdin); print(len(d['certificates']), {k:(v['source'],v['tries'],v['seconds']) for k,v in d['certificates'].items()})". To wait for a condition, use Monitor with an until-loop (e.g. `until <check>; do sleep 2; done`). To wait for a command you started, use run_in …
I'll make the loop fix meanwhile.
$ python3 - <<'EOF'
import pathlib
p=pathlib.Path("src/undertaker/morgue/app.py");s=p.read_text()
old=""" while True:
await self.sweep_now()
await self.write_certs()
await asyncio.sleep(self.s.every.total_seconds())"""
assert old in s
s=s.replace(old,""" while True:
await self.sweep_now()
if not self.writing:
self._writer = asyncio.create_task(self.write_certs())
await asyncio.sleep(self.s.every.t …
89: self._writer = asyncio.create_task(self.write_certs())
114: task = asyncio.create_task(m.loop()) if start_loop else None
180: asyncio.create_task(m.write_certs())
8 {'order-1045': ('model', 1, 8.4), 'order-1050-box': ('model', 1, 7.6), 'order-1044': ('model', 1, 8.2), 'order-1046': ('model', 1, 10.0), 'order-1047': ('model', 1, 10.3), 'order-1043': ('model', 1, 9.4 …
$ python3 - <<'EOF'
import pathlib
p=pathlib.Path("src/undertaker/morgue/app.py");s=p.read_text()
s=s.replace(""" asyncio.create_task(m.write_certs())""",""" if not m.writing:
m._writer = asyncio.create_task(m.write_certs())""")
s=s.replace(""" self.cert_state: dict""",""" self._writer: asyncio.Task | None = None
self.cert_state: dict""")
p.write_text(s)
EOF
uv run ruff check -q && echo ok
ok
{
"url": "http://127.0.0.1:8666/w/order-1042"
}
### Ran Playwright code
```js
await page.goto('http://127.0.0.1:8666/w/order-1042');
```
### Page
- Page URL: http://127.0.0.1:8666/w/order-1042
- Page Title: order-1042 · undertaker
### Snapshot
- [Snapshot](.playwright-mcp/page-2026-10-31T19-25-57-221Z.yml)
{
"scale": "css",
"filename": ".playwright-mcp/cert-1042.png",
"fullPage": true
}
### Result
- [Screenshot of full page](.playwright-mcp/cert-1042.png)
### Ran Playwright code
```js
// Screenshot full page and save it as .playwright-mcp/cert-1042.png
await page.screenshot({
fullPage: true,
path: '.playwright-mcp/cert-1042.png',
scale: 'css',
type: 'png'
});
```
~/github/prateek11rai/undertaker/.playwright-mcp/cert-1042.png
Good model certificate. It didn't name the file and line in the prose even though frame1 is in evidence, so I'll tighten the prompt. First, check the waiting-signal and changed-code pages.
$ for w in order-1048 order-1047 order-1046 order-1050; do curl -s http://127.0.0.1:8666/w/$w | python3 -c "
import sys,re,html
t=sys.stdin.read()
sub=re.search(r'<div class=\"sub\">(.*?)</div>',t,re.S).group(1)
hd=re.search(r'<div class=\"headline\">(.*?)</div>(.*?)<div class=\"note\">',t,re.S)
ep=re.search(r'class=\"epitaph\">(.*?)</div>',t,re.S)
strip=lambda s: html.unescape(re.sub(r'<[^>]+>','',s)).strip()
print('$w |', strip(sub)); print(' ', strip(hd.group(1)),'::',strip(hd.group(2))); prin …
order-1048 | order-1048 · run 01a1936b… · written by gemma4:e4b, every fact checked
Workflow Stuck Awaiting Unreceived Payment Signal :: The AwaitPaymentWorkflow in order-1048 is stalled because it is waiting for the payment_received signal. The workflow has no scheduled activities, timers, or child/workflow tasks to proceed without this signal.
“Awaiting a visitor who never arrived to the d …
All four read well and every fact checks out (workflows.py:70 is the real line). Prompt tweak, then restart serve read-only for the screenshots.
$ python3 - <<'EOF'
import pathlib
p=pathlib.Path("src/undertaker/explain/certificate.py");s=p.read_text()
old="- Cite the evidence keys you used in \"evidence\".\n"
assert old in s
s=s.replace(old, old+"- If the evidence has a frame1, say in the diagnosis where the error comes from: its file and line,\n exactly as written there.\n")
p.write_text(s)
EOF
grep -n "frame1, say" -A1 src/undertaker/explain/certificate.py; uv run pytest -q 2>&1|tail -1; pkill -f "undertaker serve"; sleep 1; rm -f .data …
198:- If the evidence has a frame1, say in the diagnosis where the error comes from: its file and line,
199- exactly as written there.
69 passed, 2 deselected in 0.45s
200
{
"command": "until curl -s http://127.0.0.1:8666/api/state | python3 -c \"import json,sys; d=json.load(sys.stdin); sys.exit(0 if len(d['certificates'])>=10 else 1)\"; do sleep 5; done; echo ready",
"description": "Wait for all ten certificates",
"timeout_ms": 300000
}
Monitor started (task bfei54kyq, expires in 5m unless the source ends first; you get one notice at expiry — re-arm if you still need the watch). You will be notified on each event. Keep working — do not poll or sleep. Events may arrive while you are waiting for the user — an event is not their reply.
While that runs, the AGENTS.md update and build log draft.
$ cat AGENTS.md | head -20
# Undertaker · agent guide
Undertaker finds Temporal workflows that will never finish and writes a checked "certificate of undeath" for each, using a local Gemma 4 e4b through Ollama. Spec: `docs/design/spec.md`. Build notes: `docs/BUILD_LOG.md`.
## Run and test
- Python 3.13 (Homebrew) with uv: `uv sync`, `uv run undertaker …`. uv never downloads a Python.
- Checks: `uv run ruff check && uv ru …
$ python3 - <<'EOF'
import pathlib
p=pathlib.Path("AGENTS.md");s=p.read_text()
s=s.replace("""- Demo: `uv run undertaker demo up`, then `uv run undertaker serve` (http://127.0.0.1:8666). Always `uv run undertaker demo down` when finished; leave no processes behind.
""","""- Demo: `uv run undertaker demo up`, wait ~30 s, then `uv run undertaker sweep --demo` or `uv run undertaker serve --demo --source src/undertaker/demo` (http://127.0.0.1:8666). `uv run undertaker eval --demo` scores rules and mod …
done
{
"url": "http://127.0.0.1:8666/"
}
### Ran Playwright code
```js
await page.goto('http://127.0.0.1:8666/');
```
### Page
- Page URL: http://127.0.0.1:8666/
- Page Title: Morgue · undertaker
### Snapshot
- [Snapshot](.playwright-mcp/page-2026-10-31T19-26-41-997Z.yml)
{
"scale": "css",
"filename": "docs/img/morgue.png"
}
### Result
- [Screenshot of viewport](docs/img/morgue.png)
### Ran Playwright code
```js
// Screenshot viewport and save it as docs/img/morgue.png
await page.screenshot({
path: 'docs/img/morgue.png',
scale: 'css',
type: 'png'
});
```
Morgue screenshot saved. Drafting the build log while the certificates finish.
~/github/prateek11rai/undertaker/docs/BUILD_LOG.md (46 lines)
File created successfully at: ~/github/prateek11rai/undertaker/docs/BUILD_LOG.md (file state is current in your context — no need to Read it back)
$ curl -s http://127.0.0.1:8666/api/state | python3 -c "
import json,sys; d=json.load(sys.stdin); print(len(d['certificates']), {k:(v['source'],v['tries']) for k,v in d['certificates'].items()}); c=d['certificates'].get('order-1042'); print(c and c['diagnosis'])"
5 {'order-1045': ('model', 1), 'order-1050-box': ('model', 1), 'order-1044': ('model', 1), 'order-1046': ('model', 1), 'order-1047': ('model', 1)}
None
$ uv run undertaker doctor; echo "exit $?"; uv run undertaker demo status
python 3.13.12
temporalio 1.34.0
temporal /opt/homebrew/bin/temporal found
server 127.0.0.1:7233 reachable, version 1.32.0
ollama http://127.0.0.1:11434 reachable, version 0.35.1
model gemma4:e4b present
exit 0
temporal: running, pid 68368
worker: running, pid 68425
{
"taskId": "bfei54kyq"
}
Invalid parameters for tool 't3_background_wait': Missing key
at ["taskIds"]
{
"taskIds": [
"bfei54kyq"
]
}
{
"matched": [
"bfei54kyq"
],
"unmatched": [
]
}
$ curl -s http://127.0.0.1:8666/api/state | python3 -c "
import json,sys; d=json.load(sys.stdin); print(len(d['certificates']), {k:(v['source'],v['tries']) for k,v in d['certificates'].items()}); c=d['certificates'].get('order-1042'); print(c and c['diagnosis'])"
6 {'order-1045': ('model', 1), 'order-1050-box': ('model', 1), 'order-1044': ('model', 1), 'order-1046': ('model', 1), 'order-1047': ('model', 1), 'order-1043': ('model', 1)}
None
Status: steps 1–9 committed; README and build log drafted; waiting for Gemma to finish the retry-loop certificate so I can take the certificate screenshot.
{
"command": "until curl -s http://127.0.0.1:8666/api/state | python3 -c \"import json,sys; d=json.load(sys.stdin); sys.exit(0 if 'order-1042' in d['certificates'] else 1)\"; do sleep 3; done; echo ready-1042",
"description": "Wait for the retry-loop certificate",
"timeout_ms": 240000
}
Monitor started (task b3p0f4b13, expires in 4m unless the source ends first; you get one notice at expiry — re-arm if you still need the watch). You will be notified on each event. Keep working — do not poll or sleep. Events may arrive while you are waiting for the user — an event is not their reply.
{
"taskIds": [
"b3p0f4b13"
]
}
{
"matched": [
"b3p0f4b13"
],
"unmatched": [
]
}
$ curl -s http://127.0.0.1:8666/api/state | python3 -c "
import json,sys; d=json.load(sys.stdin); c=d['certificates'].get('order-1042'); print(c and (c['source'], c['tries'], c['rejected'], c['diagnosis']))"
('model', 1, [], 'The `charge_card` activity is failing with `ValueError: amount must be positive` on every attempt. This occurs in `activities.py` at line 12. Since the retry policy has no maximum, the workflow `order-1042` is stuck in a retry loop.')
The model now names activities.py line 12. Taking the certificate screenshot.
{
"url": "http://127.0.0.1:8666/w/order-1042"
}
### Ran Playwright code
```js
await page.goto('http://127.0.0.1:8666/w/order-1042');
```
### Page
- Page URL: http://127.0.0.1:8666/w/order-1042
- Page Title: order-1042 · undertaker
### Snapshot
- [Snapshot](.playwright-mcp/page-2026-10-31T19-27-33-601Z.yml)
{
"scale": "css",
"filename": "docs/img/certificate.png",
"fullPage": true
}
### Result
- [Screenshot of full page](docs/img/certificate.png)
### Ran Playwright code
```js
// Screenshot full page and save it as docs/img/certificate.png
await page.screenshot({
fullPage: true,
path: 'docs/img/certificate.png',
scale: 'css',
type: 'png'
});
```
~/github/prateek11rai/undertaker/docs/img/certificate.png
Certificate screenshot saved. Now --allow-actions: lay order-1045 to rest and resurrect order-1046 through the confirm dialogs.
$ pkill -f "undertaker serve"; sleep 1; (uv run undertaker serve --demo --allow-actions --no-model > .data/serve.log 2>&1 &); sleep 3; curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8666/; uv run undertaker sweep --demo --json | python3 -c "import json,sys; print({d['workflow_id']:d['run_id'][:13] for d in json.load(sys.stdin) if d['workflow_id'] in ('order-1045','order-1046')})"
200
{'order-1045': '01a1936b-4e67', 'order-1046': '01a1936b-4e6c'}
{
"url": "http://127.0.0.1:8666/w/order-1045"
}
### Ran Playwright code
```js
await page.goto('http://127.0.0.1:8666/w/order-1045');
```
### Page
- Page URL: http://127.0.0.1:8666/w/order-1045
- Page Title: order-1045 · undertaker
### Snapshot
- [Snapshot](.playwright-mcp/page-2026-10-31T19-27-49-545Z.yml)
{
"function": "() => [...document.querySelectorAll('.btns button')].map(b => ({ text: b.textContent, disabled: b.disabled }))"
}
### Result
[
{
"text": "Copy command",
"disabled": false
},
{
"text": "Lay to rest",
"disabled": false
},
{
"text": "Resurrect",
"disabled": false
}
]
### Ran Playwright code
```js
await page.evaluate('() => [...document.querySelectorAll(\'.btns button\')].map(b => ({ text: b.textContent, disabled: b.disabled }))');
```
{
"target": "button[data-action=\"terminate\"]",
"element": "Lay to rest button"
}
### Ran Playwright code
```js
await page.locator('button[data-action="terminate"]').click();
```
### Page
- Page URL: http://127.0.0.1:8666/w/order-1045
### Modal state
- ["confirm" dialog with message "Lay order-1045 to rest? This terminates the running workflow."]: can be handled by browser_handle_dialog
### Snapshot
- [Snapshot](.playwright-mcp/page-2026-10-31T19-27-51-201Z.yml)
The confirm appeared. Accepting.
{
"accept": true
}
### Page
- Page URL: http://127.0.0.1:8666/w/order-1045
- Page Title: order-1045 · undertaker
$ sleep 2; /opt/homebrew/bin/temporal workflow describe --workflow-id order-1045 --address 127.0.0.1:7233 2>&1 | grep -E "Status|Reason" | head -3
Status
$ /opt/homebrew/bin/temporal workflow describe --workflow-id order-1045 --address 127.0.0.1:7233 -o json 2>&1 | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['workflowExecutionInfo']['status'])"
WORKFLOW_EXECUTION_STATUS_TERMINATED
{
"url": "http://127.0.0.1:8666/w/order-1046"
}
### Ran Playwright code
```js
await page.goto('http://127.0.0.1:8666/w/order-1046');
```
### Page
- Page URL: http://127.0.0.1:8666/w/order-1046
- Page Title: order-1046 · undertaker
### Snapshot
- [Snapshot](.playwright-mcp/page-2026-10-31T19-28-02-580Z.yml)
{
"target": "button[data-action=\"reset\"]",
"element": "Resurrect button"
}
### Ran Playwright code
```js
await page.locator('button[data-action="reset"]').click();
```
### Page
- Page URL: http://127.0.0.1:8666/w/order-1046
### Modal state
- ["confirm" dialog with message "Resurrect order-1046? This resets it to its last workflow task as a new run."]: can be handled by browser_handle_dialog
### Snapshot
- [Snapshot](.playwright-mcp/page-2026-10-31T19-28-03-410Z.yml)
{
"accept": true
}
### Page
- Page URL: http://127.0.0.1:8666/w/order-1046
- Page Title: order-1046 · undertaker
$ sleep 2; /opt/homebrew/bin/temporal workflow list --address 127.0.0.1:7233 --query 'WorkflowId="order-1046"' -o json 2>&1 | python3 -c "
import json,sys
for w in json.load(sys.stdin): print(w['execution']['runId'][:13], w['status'])"
c45afce9-0742 WORKFLOW_EXECUTION_STATUS_RUNNING
01a1936b-4e6c WORKFLOW_EXECUTION_STATUS_TERMINATED
Both actions changed server state: order-1045 terminated; order-1046 reset into a new run c45afce9…. Check the page reflects it.
{
"url": "http://127.0.0.1:8666/"
}
### Ran Playwright code
```js
await page.goto('http://127.0.0.1:8666/');
```
### Page
- Page URL: http://127.0.0.1:8666/
- Page Title: Morgue · undertaker
### Snapshot
- [Snapshot](.playwright-mcp/page-2026-10-31T19-28-13-564Z.yml)
{
"function": "() => ({ tally: document.querySelector('.tally').textContent.replace(/\\s+/g,' '), ids: [...document.querySelectorAll('.toetag .wid')].map(e => e.textContent), has1045: !!document.querySelector('a[href=\"/w/order-1045\"]') })"
}
### Result
{
"tally": "8 undead · 3 alive · swept 8 s ago",
"ids": [
"order-1050-box",
"order-1044",
"order-1047",
"order-1043",
"order-1042",
"order-1050",
"order-1049",
"order-1048",
"inventory-nightly",
"order-1046",
"proof-dough"
],
"has1045": false
}
### Ran Playwright code
```js
await page.evaluate('() => ({ tally: document.querySelector(\' …
{
"url": "http://127.0.0.1:8666/w/order-1046"
}
### Ran Playwright code
```js
await page.goto('http://127.0.0.1:8666/w/order-1046');
```
### Page
- Page URL: http://127.0.0.1:8666/w/order-1046
- Page Title: order-1046 · undertaker
### Snapshot
- [Snapshot](.playwright-mcp/page-2026-10-31T19-28-16-081Z.yml)
{
"function": "() => document.querySelector('.cert .sub').textContent.replace(/\\s+/g,' ').trim()"
}
### Result
"order-1046 · run c45afce9… · template certificate (model off)"
### Ran Playwright code
```js
await page.evaluate('() => document.querySelector(\'.cert .sub\').textContent.replace(/\\s+/g,\' \').trim()');
```
{
}
### Result
No open tabs. Navigate to a URL to create one.
### Ran Playwright code
```js
await page.close()
```
Click-through complete. Now a clean eval on a fresh demo for the build log numbers (serve stopped so timing isn't shared).
$ pkill -f "undertaker serve"; uv run undertaker demo down && uv run undertaker demo up >/dev/null && sleep 35 && uv run undertaker eval --demo --source src/undertaker/demo; echo "exit $?"
Stopped: worker, temporal dev server
Rules: found 10/10 · missed 0 · false alarms 0 · alive ok 2
order-1045 orphaned accepted 7.4 s
order-1050-box orphaned accepted 7.8 s
order-1044 lost-activity accepted 8.0 s
order-1046 broken-code accepted 9.9 s
order-1047 changed-code accepted 8.1 s
order-1043 heartbeat acc …
Clean: 10/10, 0 missed, 0 false alarms; Gemma 10/10 accepted first try, 8.4 s each. One more run to see if that holds, plus acceptance 4 (inspect with and without --source).
$ uv run undertaker eval --demo --source src/undertaker/demo | tail -3; echo "exit $?"; echo "--- no source"; uv run undertaker inspect order-1042 --demo | grep -E "^(written|template|Headline|Diagnosis| )" | head -6; echo "--- with source"; uv run undertaker inspect order-1042 --demo --source src/undertaker/demo | grep -E "^(written|template|Diagnosis| |Source|>)" | head -8
order-1049 long-sleep accepted 6.6 s
order-1048 waiting-signal accepted 6.4 s
Model gemma4:e4b: accepted 10/10 · retried then accepted 0 · fallback 0 · 7.6 s per certificate
exit 0
--- no source
written by gemma4:e4b, every fact checked
Headline Workflow Stuck in Infinite Retry Loop Due to Activity Failure
Diagnosis The `order-1042` workflow is stuc …
Acceptance 4 holds: with --source, the retry-loop certificate names activities.py:12. Tear down and verify nothing's left.
$ uv run undertaker demo down; sleep 1; pgrep -fl "temporal server|undertaker.demo|undertaker serve|undertaker demo|undertaker watch" || echo "no undertaker or temporal processes"; uv run undertaker doctor; echo "doctor exit $?"; uv run undertaker demo status
Stopped: worker, temporal dev server
no undertaker or temporal processes
python 3.13.12
temporalio 1.34.0
temporal /opt/homebrew/bin/temporal found
server 127.0.0.1:7233 not reachable (RuntimeError)
ollama http://127.0.0.1:11434 reachable, version 0.35.1
model gemma4:e4b present
doctor exit 1
temporal: stopped
worker: stopped
Doctor reports the stopped server honestly (exit 1). Filling in the build log.
$ python3 - <<'EOF'
import pathlib
p=pathlib.Path("docs/BUILD_LOG.md");s=p.read_text()
eval_txt='''Fresh `demo up`, 35 s wait, `serve` stopped so nothing else used the model. Two runs back to back:
| | Run 1 | Run 2 |
|---|---|---|
| Rules: found | 10/10 | 10/10 |
| Missed | 0 | 0 |
| False alarms | 0 | 0 |
| Alive and right | 2/2 | 2/2 |
| Gemma accepted on the first try | 10/10 | 10/10 |
| Retried, then accepted | 0 | 0 |
| Fell back to the template | 0 | 0 |
| Seconds per certificate (mean) | …
0
Final checks: all three test suites, then a fresh-clone uv sync --frozen.
$ uv run ruff check && uv run ruff format --check && uv run pytest -q 2>&1 | tail -1 && uv run pytest -q -m temporal 2>&1 | tail -1 && uv run pytest -q -m model 2>&1 | tail -1; pgrep -fl "temporal server|temporal-test" || echo "no temporal left"
[]
45 files already formatted
Pytest: 69 passed
Pytest: 1 passed
Pytest: 1 passed
no temporal left
$ ls docs/img && rm -rf .playwright-mcp && git add -A && git status --short && bash .git/info/leak-check.sh && git commit -q -m "Docs: README, build log with eval and click-through, AGENTS, LICENSE
Also: morgue sweeps no longer wait for certificate writing, rewrites only on
real state change, and the prompt asks for the frame's file and line.
Screenshots come from the Playwright page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>" && git log --oneline
certificate.png 246.6K
morgue.png 111.2K
theme-preview.png 353.3K
ok 8 files changed, 236 insertions(+), 5 deletions(-)
M AGENTS.md
A LICENSE
M README.md
A docs/BUILD_LOG.md
A docs/img/certificate.png
A docs/img/morgue.png
M src/undertaker/explain/certificate.py
M src/undertaker/morgue/app.pyleak-check: clean
ok
65c351f Docs: README, build log with eval and click-through, AGENTS, LICENS …
Fresh-clone check (acceptance 1) and the final identity/status checks.
$ rm -rf /tmp/undertaker-clone && git clone -q ~/github/prateek11rai/undertaker /tmp/undertaker-clone && cd /tmp/undertaker-clone && uv sync --frozen -q 2>&1 | tail -2; uv run ruff check && uv run ruff format --check | tail -1 && uv run pytest -q 2>&1 | tail -1; cd ~/github/prateek11rai/undertaker && rm -rf /tmp/undertaker-clone && bash .git/info/leak-check.sh && git log --format='%ae %ce' | sort -u && git status --short | wc -l
[]
45 files already formatted
Pytest: 69 passed
leak-check: clean
165780365+prateek11rai@users.noreply.github.com 165780365+prateek11rai@users....
0
$ cd ~/github/prateek11rai/undertaker && git log --format='%ae%n%ce' | sort -u | grep -v -x '165780365+prateek11rai@users.noreply.github.com' | wc -l; pgrep -fl "temporal server|undertaker.demo|undertaker serve|undertaker watch|ollama runner" | grep -v "ollama" || echo "nothing of ours running"; date
0
nothing of ours running
Sun Nov 1 01:03:25 IST 2026
$ cd ~/github/prateek11rai/undertaker && python3 - <<'EOF'
import pathlib
p=pathlib.Path("src/undertaker/cli.py");s=p.read_text()
old=''' demo = getattr(args, "demo", False) or _env("demo") == "1"
'''
assert old in s
s=s.replace(old,''' demo = getattr(args, "demo", False) or _env("demo") == "1" or _demo_running(args)
''')
s=s.replace('''def settings_from(args: argparse.Namespace) -> Settings:''','''def _demo_running(args: argparse.Namespace) -> bool:
"""True when our demo is up and the c …
All checks passed!
69 passed, 2 deselected in 0.45s
The demo is up: using demo thresholds (--quiet 20s --attempts 3).
10 undead · 2 alive · 127.0.0.1:7233 default
WORKFLOW ID TYPE CAUSE
order-1045 LegacyOrderWorkflow orphaned undead for 32 s · 0 pollers on bakery-legacy
order-1050-box BoxWorkflow orphaned undead for 32 s · 0 …
$ cd ~/github/prateek11rai/undertaker && python3 - <<'EOF'
import pathlib
p=pathlib.Path("docs/BUILD_LOG.md");s=p.read_text()
s=s.replace("- **`demo up` always starts from an empty db.**","- **Demo thresholds apply by themselves** when the demo is up and the command points at its address and namespace, so plain `undertaker sweep` and `serve` show the demo right away (it says so on stderr). `--quiet`/`--attempts` still win; `--demo` forces them.\n- **`demo up` always starts from an empty db.**")
p. …
leak-check: clean
5eab177 Use demo thresholds by themselves while the demo is up
0
Everything is committed and verified. Sending the done report to the planner.
{"threadId": "mcp:d6d84144-d1b5-442b-82e6-d79b07028f77", "mode": "queue", "message": "Builder report · undertaker core · done 01:05 IST. Nothing pushed, nothing left running.\n\nCOMMITS (main, all noreply, leak check clean)\n- 7bbae1c Skeleton: uv project, CLI, doctor, planner docs\n- 1b29f1d Demo b …
{
"threadId": "mcp:d6d84144-d1b5-442b-82e6-d79b07028f77",
"link": "<t3-thread://v2/fc11dac6-e4bb-4bb7-b6a4-eaa02f98b99e/mcp:d6d84144-d1b5-442b-82e6-d79b07028f77>",
"messageId": "message:mcp:1c783c84-5205-4a43-b079-ea112c84f11c:thread-send:7cb5da94-5968-463c-bd9d-e239a210ab2c",
"runId": "run:thread:mcp%3Ad6d84144-d1b5-442b-82e6-d79b07028f77:ordinal:9",
"status": "starting",
"delivery": "started"
}
The Undertaker core is built, and the full report has gone to the planner thread t3-thread://v2/fc11dac6-e4bb-4bb7-b6a4-eaa02f98b99e/mcp:d6d84144-d1b5-442b-82e6-d79b07028f77. It's finished at 01:05 IST, well before the 04:00 target.
-
Commits: 11 on
main, all from the noreply address, with a clean leak check and a cleangit status. Nothing is pushed:undertakerhas commits ready for you to push. -
Tests: lint passes, 69 unit tests pass, and the one Temporal test and the one real-Gemma test both pass. A fresh clone with
uv sync --frozenpasses too. - Eval on the demo: the rules found all 10 undead workflows, with 0 missed, 0 false alarms and both alive ones correct. Gemma's certificates passed the checks on the first try 10 times out of 10, at about 8 s each, in two runs back to back.
-
Click-through in Playwright: everything on the brief's list passed (the full list is in
docs/BUILD_LOG.md).- The morgue, all 11 filters, Sweep now, the copy buttons and four certificates work.
- The action buttons are disabled when read-only. With
--allow-actions, the confirm appeared and the server changed:order-1045was terminated andorder-1046restarted as a new run. - With
--source, the retry-loop certificate namesactivities.py:12. The screenshots are indocs/img/.
-
Nothing left running:
demo downleaves no processes.doctorcorrectly reports the server as down and exits 1.
The hardest demo case, order-1047 (code changed while it slept), worked first time.
Decisions I made without asking, all written up in docs/BUILD_LOG.md:
- proof-dough: it runs twenty 90-second timers instead of one, so it's still alive when you open the morgue.
- Rewriting certificates: the morgue writes a new one only when the history or the cause changes, not on every retry attempt. The page says which attempt it was written at.
- Demo thresholds: they now apply by themselves while the demo is up, so plain undertaker sweep works as the brief's acceptance step says.
- No new dependency: the page tests call the app directly instead of using FastAPI's test client, which would have needed httpx.
From the earlier exchange with the planner: ship.md is committed in its reworded form, the CI uses the action versions you supplied, and the leak check now ignores the T3 harness refs.
Planner: the core passed my review. Next is the ship pass. Your brief is docs/design/ship.md: read it in full, including A1–A3 under ## Answers (A3 is a small code fix to do first). The target is now 02:30 IST.
The real folder for Playwright is NODE_PATH=~/.npm/_npx/705bc6b22212b352/node_modules. Use it only on the command line; never write it into a file (it matches a leak pattern).
Report to me with t3_thread_send (mode queue) when done, ending with DONE-undertaker-ship. Leave nothing running.
Planner: the core passed my review. Next is the ship pass. Your brief is docs/design/ship.md: read it in full, including A1–A3 under ## Answers (A3 is a small code fix to do first). The target is now 02:30 IST.
The real folder for Playwright is NODE_PATH=~/.npm/_npx/705bc6b22212b352/node_modules. Use it only on the command line; never write it into a file (it matches a leak pattern).
Report to me with t3_thread_send (mode queue) when done, ending with DONE-undertaker-ship. Leave nothing running.
$ cd ~/github/prateek11rai/undertaker && git status --short && git log --oneline | head -3 && cat docs/design/ship.md; date
M docs/design/ship.md5eab177 Use demo thresholds by themselves while the demo is up
65c351f Docs: README, build log with eval and click-through, AGENTS, LICENSE
5e0835d CI: unit and Temporal jobs, optional Gemma job
# Undertaker · ship pass (images, README, release notes, post draft)
> Second brief for the builder thread `mlh · build-undertaker`, sent by the planner after the core review. The pl …
Starting with A3: excerpt = the enclosing function only, capped at 8 lines either side of the frame.
$ cd ~/github/prateek11rai/undertaker && python3 - <<'EOF'
import pathlib
p=pathlib.Path("src/undertaker/explain/source.py");s=p.read_text()
s=s.replace('"""Source excerpts around stack frames, only from files inside the given folder."""\n\nfrom dataclasses','"""Source excerpts around stack frames, only from files inside the given folder."""\n\nimport ast\nfrom dataclasses')
s=s.replace("AROUND = 15\n","AROUND = 8\n")
old=''' start = max(1, f.line - AROUND)
end = min(len(lines), f.l …
All checks passed!
70 passed, 2 deselected in 0.67s
8 @activity.defn
9 async def charge_card(amount: int) -> str:
10 """Charge the customer for an order."""
11 if amount <= 0:
> 12 raise ValueError("amount must be positive")
13 return f"charged {amount}"
66 @workflow.run
67 async def run(self, cake: str) -> str:
68 """Follow …
Excerpts are now the function only. Committing A3 (and the planner's ship.md answers separately).
$ cd ~/github/prateek11rai/undertaker && python3 - <<'EOF'
import pathlib
p=pathlib.Path("docs/BUILD_LOG.md");s=p.read_text()
s=s.replace("- **History cache is in memory**","- **Source excerpts are the enclosing function** (planner review, A3), at most 8 lines either side of the frame line, instead of ~30 lines around it, so the excerpt doesn't drag in unrelated functions. Non-Python files get ±8 lines.\n- **History cache is in memory**")
p.write_text(s)
EOF
git add src tests docs/BUILD_LOG.md && …
leak-check: clean
leak-check: clean
ec081d0 Ship brief answers from the planner
254b75a Source excerpt: the enclosing function, at most 8 lines either side
Bring up the demo and the morgue (with --source) so Gemma writes the certificates while I build the image pages.
$ cd ~/github/prateek11rai/undertaker && rm -f .data/certificates.sqlite && uv run undertaker demo up && sleep 30 && uv run undertaker sweep > .data/sweep.txt 2>/dev/null; cat .data/sweep.txt; (uv run undertaker serve --source src/undertaker/demo > .data/serve.log 2>&1 &); sleep 2; curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8666/
Demo up: Temporal 127.0.0.1:7233 (UI http://127.0.0.1:8233), worker running.
Seeded 12 running workflows and 3 completed ones.
Wait about 30 s for the thresholds, then: uv run undertaker sweep --demo
10 undead · 2 alive · 127.0.0.1:7233 default
WORKFLOW ID TYPE CAUSE
order-1045 LegacyOrderWorkflow orphaned undead for 30 s · 0 pollers on bakery-l …
Real sweep output captured for sweep.png. Now the render scripts; first confirm Playwright resolves from the given folder.
$ cd /tmp && NODE_PATH=~/.npm/_npx/705bc6b22212b352/node_modules node -e "const p=require('playwright'); console.log(Object.keys(p).filter(k=>['chromium'].includes(k)), require('playwright/package.json').version)"; node --version; ls "/Applications/Google Chrome.app" >/dev/null && echo chrome-ok
[ 'chromium' ] 1.62.1
v24.1.0
chrome-ok
Shell cwd was reset to ~/github/prateek11rai/undertaker
$ cd ~/github/prateek11rai/undertaker && cat > docs/brand/render.cjs <<'EOF'
// Renders the brand pages in docs/brand/ to PNGs, offline, with the installed Google Chrome.
// Usage: NODE_PATH=<node_modules> node docs/brand/render.cjs [--scale 2] [--out DIR] [name ...]
const path = require("path");
const fs = require("fs");
const { chromium } = require("playwright");
const ROOT = path.resolve(__dirname, "..", "..");
const JOBS = {
banner: { page: "banner.html", out: "docs/img/banner.png", w: 1280 …
2 ['order-1045', 'order-1050-box']
While Gemma works, the brand pages. A shared stylesheet first, then banner, cover, social preview, sweep.
$ cd ~/github/prateek11rai/undertaker/docs/brand && cat > brand.css <<'EOF'
/* Shared look for the brand pages; values match src/undertaker/morgue/static/theme.css. */
:root {
--bg: #0f1211; --panel: #161b19; --line: #2a312e; --text: #e9e4d8; --muted: #8f968f; --accent: #8be3a0;
--orphaned: #a3a7a3; --lost-activity: #b8b08a; --retry-loop: #d9825f; --heartbeat: #a993e6;
--broken-code: #e06666; --changed-code: #e08fb8; --waiting-signal: #e3c46f; --long-sleep: #7fa7d9;
--undead-child: #5fbfb3 …
~/github/prateek11rai/undertaker/docs/img/banner.png
~/github/prateek11rai/undertaker/docs/img/banner.png
Banner reads well. DEV cover (content inside the middle 80 %) and the sweep terminal page next.
$ cd ~/github/prateek11rai/undertaker/docs/brand && cat > devto-cover.html <<'EOF'
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>DEV cover</title>
<link rel="stylesheet" href="brand.css">
<style>
body { width: 1000px; height: 420px; overflow: hidden; position: relative; }
.row { position: absolute; left: 0; right: 0; bottom: -34px; display: flex; gap: 22px; justify-content: center; opacity: 0.18; }
.stone { width: 92px; height: 112px; border: 2px solid var(--text); border …
~/github/prateek11rai/undertaker/docs/img/devto-cover.png
~/github/prateek11rai/undertaker/docs/img/sweep.png
~/github/prateek11rai/undertaker/docs/img/devto-cover.png
~/github/prateek11rai/undertaker/docs/img/sweep.png
Two fixes: the inventory-nightly row coloured "heartbeat" instead of its cause (colour by column, not by word), and full-page renders padded to 800 px.
$ cd ~/github/prateek11rai/undertaker && sed -i '' 's/viewport: { width: job.w, height: job.h || 800 },/viewport: { width: job.w, height: job.h || 200 },/' docs/brand/render.cjs && python3 - <<'EOF'
import html, pathlib, re
p = pathlib.Path("docs/brand/sweep.html")
txt = pathlib.Path(".data/sweep.txt").read_text().rstrip("\n")
lines = txt.splitlines()
hdr = next(l for l in lines if l.startswith("WORKFLOW ID"))
c0 = hdr.index("CAUSE")
out = []
for line in lines:
if line.startswith("WORKFLOW ID" …
rendered
8 ['order-1042', 'order-1043', 'order-1044', 'order-1045', 'order-1046', 'order-1047', 'order-1050', 'order-1050-box']
~/github/prateek11rai/undertaker/docs/img/sweep.png
Sweep image is right now. The three diagrams next (1600 wide, type sized to read at 800).
$ cd ~/github/prateek11rai/undertaker/docs/brand && cat > diagram.css <<'EOF'
/* Shared diagram styles: 1600 px wide, readable at 800 px. */
body { width: 1600px; padding: 56px 64px 64px; }
h1 { font-family: var(--serif); font-weight: 600; font-size: 48px; margin: 0 0 6px; }
.lede { color: var(--muted); font-size: 24px; margin: 0 0 36px; }
.box { background: var(--panel); border: 1.5px solid var(--line); border-radius: 12px; padding: 20px 22px; }
.box h3 { font-family: var(--serif); font-weight: 6 …
~/github/prateek11rai/undertaker/docs/img/diagrams/how-it-works.png
~/github/prateek11rai/undertaker/docs/img/diagrams/causes.png
~/github/prateek11rai/undertaker/docs/img/diagrams/certificate-checks.png
~/github/prateek11rai/undertaker/docs/img/diagrams/how-it-works.png
~/github/prateek11rai/undertaker/docs/img/diagrams/causes.png
~/github/prateek11rai/undertaker/docs/img/diagrams/certificate-checks.png
Diagrams read well. Two small fixes: the JSON block wraps badly, and the boundary label should cover "your own Temporal" since it can be on your network.
$ cd ~/github/prateek11rai/undertaker && python3 - <<'EOF'
import pathlib
p=pathlib.Path("docs/brand/certificate-checks.html");s=p.read_text()
start=s.index("<pre>"); end=s.index("</pre>")+6
s=s[:start]+'''<pre>{<span class="k">"headline"</span>: "≤ 12 words",
<span class="k">"diagnosis"</span>: "≤ 70 words",
<span class="k">"evidence"</span>: ["e5", "failure"],
<span class="k">"next_step"</span>: "≤ 30 words",
<span class="k">"epitaph"</span>: "≤ 14 words"}</pre>'''+s[end:]
s=s.replace("f …
10 {'order-1045': 'model', 'order-1050-box': 'model', 'order-1044': 'model', 'order-1046': 'model', 'order-1047': 'model', 'order-1043': 'model', 'order-1042': 'model', 'order-1050': 'model', 'order-1049': 'model', 'order-1048': 'model'}
~/github/prateek11rai/undertaker/docs/img/diagrams/certificate-checks.png
All 10 certificates are Gemma's now. Shooting the app at 1440 wide, scale 2.
$ cd ~/github/prateek11rai/undertaker && rm -f docs/img/certificate.png && NODE_PATH=~/.npm/_npx/705bc6b22212b352/node_modules node docs/brand/shoot.cjs && python3 -c "
import struct,glob
for f in sorted(glob.glob('docs/img/*.png')+glob.glob('docs/img/diagrams/*.png')):
b=open(f,'rb').read(24); w,h=struct.unpack('>II',b[16:24]); import os; print(f, w,'x',h, round(os.path.getsize(f)/1024),'KB')"
~/github/prateek11rai/undertaker/docs/img/morgue.png
~/github/prateek11rai/undertaker/docs/img/certificate-retry-loop.png
~/github/prateek11rai/undertaker/docs/img/certificate-changed-code.png
~/github/prateek11rai/undertaker/docs/img/certificate-waiting-signal.png
~/github/prateek11rai/undertaker/docs/img/certificate-undead-child.png
docs/img/banner.png 1280 x 400 90 KB
docs/img/certificate-chang …
Looking at each screenshot.
~/github/prateek11rai/undertaker/docs/img/morgue.png
~/github/prateek11rai/undertaker/docs/img/certificate-retry-loop.png
[Image: original 2880x1800, displayed at 2000x1250. Multiply coordinates by 1.44 to map to original image.]
[Image: original 2880x2990, displayed at 1888x1960. Multiply coordinates by 1.53 to map to original image.]
Morgue and retry-loop look right (Gemma names activities.py:12, short excerpt). The other three:
~/github/prateek11rai/undertaker/docs/img/certificate-changed-code.png
~/github/prateek11rai/undertaker/docs/img/certificate-waiting-signal.png
[Image: original 2880x2682, displayed at 1988x1852. Multiply coordinates by 1.45 to map to original image.]
~/github/prateek11rai/undertaker/docs/img/certificate-undead-child.png
[Image: original 2880x2644, displayed at 2000x1836. Multiply coordinates by 1.44 to map to original image.]
[Image: original 2880x2638, displayed at 2000x1832. Multiply coordinates by 1.44 to map to original image.]
All four certificates are Gemma's and read correctly; no UI bugs. Saving the real epitaphs for the post, then the social preview.
$ cd ~/github/prateek11rai/undertaker && curl -s http://127.0.0.1:8666/api/state | python3 -c "
import json,sys; d=json.load(sys.stdin)
for k,v in sorted(d['certificates'].items()): print(f'{k:16} {v[\"epitaph\"]}')" | tee .data/epitaphs.txt
cat > docs/brand/social-preview.html <<'EOF'
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>social preview</title>
<link rel="stylesheet" href="brand.css">
<style>
body { width: 1280px; height: 640px; overflow: hidden; position: relative; …
order-1042 Sometimes the smallest detail causes the longest wait.
order-1043 The process paused, awaiting a timely signal.
order-1044 The task waited patiently, hoping for a helpful hand.
order-1045 The task waited patiently, hoping for a listener to arrive.
order-1046 Even the sweetest recipes need all their ingredients.
order-1047 A little code hiccup caused t …
~/github/prateek11rai/undertaker/docs/img/social-preview.png
The tilted crop leaves empty space at the bottom. Tightening it onto the toe tags.
$ cd ~/github/prateek11rai/undertaker && sed -i '' 's/ .shot img { width: 1440px; margin: -150px 0 0 -40px; transform: scale(0.62); transform-origin: 0 0; }/ .shot img { width: 1440px; margin: -232px 0 0 -70px; transform: scale(0.72); transform-origin: 0 0; }/' docs/brand/social-preview.html && NODE_PATH=~/.npm/_npx/705bc6b22212b352/node_modules node docs/brand/render.cjs social-preview >/dev/null && echo ok
ok
~/github/prateek11rai/undertaker/docs/img/social-preview.png
$ cd ~/github/prateek11rai/undertaker && python3 - <<'EOF'
import pathlib,re
p=pathlib.Path("docs/brand/social-preview.html");s=p.read_text()
s=re.sub(r"\.shot \{ position: absolute; right: -210px; top: 70px; width: 820px; height: 512px;",".shot { position: absolute; right: -190px; top: 118px; width: 820px; height: 404px;",s)
s=re.sub(r"\.shot img \{[^}]*\}",".shot img { width: 1440px; margin: -186px 0 0 -96px; transform: scale(0.78); transform-origin: 0 0; }",s)
p.write_text(s)
EOF
NODE_PATH=~/.n …
ok
~/github/prateek11rai/undertaker/docs/img/social-preview.png
Good. A 2x crispness check in /tmp, then the brand README and the image commit.
$ cd ~/github/prateek11rai/undertaker && mkdir -p /tmp/ut-2x && NODE_PATH=~/.npm/_npx/705bc6b22212b352/node_modules node docs/brand/render.cjs --scale 2 --out /tmp/ut-2x banner && python3 -c "
import struct; b=open('/tmp/ut-2x/banner.png','rb').read(24); print(struct.unpack('>II',b[16:24]))"
/tmp/ut-2x/banner.png
(2560, 800)
/tmp/ut-2x/banner.png
[Image: original 2560x800, displayed at 2000x625. Multiply coordinates by 1.28 to map to original image.]
Crisp at 2x. Brand README, then commit the images.
$ cd ~/github/prateek11rai/undertaker && rm -rf /tmp/ut-2x && cat > docs/brand/README.md <<'EOF'
# Brand pages
Every image in `docs/img/` (except the app screenshots) is an HTML page here, in the app's own colours and system fonts. The pages load nothing from the network.
| Page | Renders to | Size |
|---|---|---|
| `banner.html` | `docs/img/banner.png` | 1280 × 400 |
| `devto-cover.html` | `docs/img/devto-cover.png` | 1000 × 420 (logo, name and tagline inside the middle 80 %) |
| `social-previe …
A docs/brand/README.md
A docs/brand/banner.html
A docs/brand/brand.css
A docs/brand/causes.html
A docs/brand/certificate-checks.html
A docs/brand/devto-cover.html
A docs/brand/diagram.css
A docs/brand/how-it-works.html
A docs/brand/render.cjs
A docs/brand/shoot.cjs
A docs/brand/social-preview.html
A docs/brand/sweep.html
A docs/img/banner.png
A docs/img/certificate-changed-code.png
A …
Images are committed. Stopping the demo and the morgue now, since the remaining work is all text.
$ cd ~/github/prateek11rai/undertaker && pkill -f "undertaker serve"; uv run undertaker demo down; sleep 1; pgrep -fl "temporal server|undertaker.demo|undertaker serve" || echo "nothing running"; date
Stopped: worker, temporal dev server
nothing running
Sun Nov 1 01:14:20 IST 2026
README polish. I'll rewrite it in the brief's order.
$ cd ~/github/prateek11rai/undertaker && sed -n '/^## Commands/,/^## Limits/p' README.md | head -60
## Commands
```
undertaker sweep [--json] table: id, type, cause, undead for, key fact
undertaker inspect <workflow-id> the certificate as text
undertaker serve [--allow-actions] the morgue on 127.0.0.1:8666
undertaker watch [--once] sweep every minute; one macOS notification per new undead
undertaker demo up | down | status local dev server + demo worker + scenarios …
~/github/prateek11rai/undertaker/README.md (160 lines)
The file ~/github/prateek11rai/undertaker/README.md has been updated successfully. (file state is current in your context — no need to Read it back)
$ cd ~/github/prateek11rai/undertaker && grep -oE '\]\((docs/[^)]+|LICENSE)\)' README.md | sed -E 's/^\]\(//; s/\)$//' | sort -u | while read f; do [ -e "$f" ] && echo "ok $f" || echo "MISSING $f"; done; git add README.md && bash .git/info/leak-check.sh && git commit -q -m "README: banner, screenshots, diagrams, measured numbers, credits
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>" && git log --oneline | head -1
ok LICENSE
ok docs/BUILD_LOG.md
ok docs/brand/README.md
ok docs/design/spec.md
ok docs/img/banner.png
ok docs/img/certificate-changed-code.png
ok docs/img/certificate-retry-loop.png
ok docs/img/certificate-undead-child.png
ok docs/img/certificate-waiting-signal.png
ok docs/img/diagrams/causes.png
ok docs/img/diagrams/certificate-checks.png
ok docs/img/diagrams/how-it-works.png
ok docs/img/morgue.p …
Release notes next.
~/github/prateek11rai/undertaker/docs/release-notes-v0.1.0.md (67 lines)
File created successfully at: ~/github/prateek11rai/undertaker/docs/release-notes-v0.1.0.md (file state is current in your context — no need to Read it back)
Now the post draft.
~/github/prateek11rai/undertaker/docs/devto-post.md (125 lines)
File created successfully at: ~/github/prateek11rai/undertaker/docs/devto-post.md (file state is current in your context — no need to Read it back)
You've hit your 5-hour limit · resets 2:50am (Asia/Calcutta)
Continue where you left off.
Continue where you left off.
The session restarted; checking where things stand.
$ cd ~/github/prateek11rai/undertaker && date && git status --short && git log --oneline | head -4 && ls docs/devto-post.md docs/release-notes-v0.1.0.md && pgrep -fl "temporal server|undertaker.demo|undertaker serve" || echo "nothing running"
Sun Nov 1 02:50:14 IST 2026
?? docs/devto-post.md
?? docs/release-notes-v0.1.0.md477bf97 README: banner, screenshots, diagrams, measured numbers, credits
d19e614 Images: screenshots, banner, DEV cover, social preview, diagrams
ec081d0 Ship brief answers from the planner
254b75a Source excerpt: the enclosing function, at most 8 lines either side
docs/devto-post.md 9.6K
docs/release-notes-v0.1.0.m …
It's 02:50, past the 02:30 target, so I'll finish quickly. Checking the post's length and wording.
$ cd ~/github/prateek11rai/undertaker && python3 - <<'EOF'
import re,pathlib
t=pathlib.Path("docs/devto-post.md").read_text()
body=t.split("---",2)[2]
prose=re.sub(r"!\[[^\]]*\]\([^)]*\)|\{%[^%]*%\}","",body)
print("words", len(prose.split()))
for w in ["delve","seamless","game-changer","leverage","—","!"," -- "]:
for i,l in enumerate(t.splitlines(),1):
if w=="!" and ("![" in l or "<!--" in l): l=l.replace("![","").replace("<!--","")
if w in l: print("found",repr(w),"line",i)
p …
words 1329
first line ok: True
99: """Read ~30 lines around each frame that resolves inside root."""
Post is 1,329 words, no banned words, no em dashes or exclamation marks, and the first line is exact. Fixing one stale docstring left over from A3, then committing.
$ cd ~/github/prateek11rai/undertaker && sed -i '' 's| """Read ~30 lines around each frame that resolves inside root."""| """The enclosing function (at most 8 lines either side) for each frame inside root."""|' src/undertaker/explain/source.py && uv run ruff check -q && uv run ruff format --check -q && uv run pytest -q 2>&1 | tail -1 && git add src/undertaker/explain/source.py && bash .git/info/leak-check.sh && git commit -q -m "Source excerpt docstring matches the enclosing-function behavio …
Exit code 1
[]
Pytest: 70 passed
ok 1 file changed, 1 insertion(+), 1 deletion(-)
leak-check: clean
ok
ok 1 file changed, 67 insertions(+)
leak-check: clean
ok
ok 1 file changed, 125 insertions(+)
leak-check: clean
ok
0934eb1 DEV post draft (unpublished; cover and session id left for the planner)
47d77f8 Release notes for v0.1.0
15a04e2 Source excerpt docstring matches the enclosing-function beh …
Everything is committed and clean (the exit 1 is just grep -c finding zero non-noreply emails). Sending the report.
{"threadId": "mcp:d6d84144-d1b5-442b-82e6-d79b07028f77", "mode": "queue", "message": "Builder report · undertaker ship pass · done 02:55 IST (25 min past the 02:30 target; the session restarted partway, no work was lost). Nothing pushed, nothing running (pgrep empty), git status clean, leak check cl …
{
"threadId": "mcp:d6d84144-d1b5-442b-82e6-d79b07028f77",
"link": "<t3-thread://v2/fc11dac6-e4bb-4bb7-b6a4-eaa02f98b99e/mcp:d6d84144-d1b5-442b-82e6-d79b07028f77>",
"messageId": "message:mcp:a043c0b2-800d-4424-a747-1b3587386b20:thread-send:f4c9f42a-fbbc-42b1-a183-aaeb7adb7afa",
"runId": "run:thread:mcp%3Ad6d84144-d1b5-442b-82e6-d79b07028f77:ordinal:13",
"status": "queued",
"delivery": "queued"
}
The ship pass is done and the report is queued to the planner thread t3-thread://v2/fc11dac6-e4bb-4bb7-b6a4-eaa02f98b99e/mcp:d6d84144-d1b5-442b-82e6-d79b07028f77. It finished at 02:55 IST, 25 minutes past the 02:30 target.
-
A3 fix first: the source excerpt now shows only the function around the error line, at most 8 lines either side. For the retry-loop that's lines 8–13 of
activities.py. All 70 unit tests pass. - Screenshots of the real app: the morgue and four certificates (retry-loop, changed-code, waiting-signal, undead-child), taken from the app's own page at 1440 wide and 2x. Every certificate in them was written by Gemma and passed the checks.
-
Rendered images: the README banner, the DEV cover, the GitHub social preview, the
sweepterminal image (real output) and three diagrams (how it works, the nine causes, the certificate checks). They're built from HTML pages indocs/brand/, offline, and I looked at every one before committing. -
Text: the README is rewritten in the brief's order and includes the measured eval numbers. Release notes are in
docs/release-notes-v0.1.0.md. -
Post draft:
docs/devto-post.md, 1,329 words, with three title candidates (each with a real number). It quotes two real epitaphs from the screenshot run.
Placeholders still to fill: {{POST_URL}} in the README, the empty "After the deadline" section, and the post's cover_image and {{SESSION_ID}}.
Seven new commits sit on main in prateek11rai/undertaker, ready for you to push. Nothing is running and git status is clean.
A planner thread wrote the spec and the briefs and reviewed every result; this builder session wrote the code, the tests, the demo and the images.
The Spirit of It
The first sweep against the demo came back with every workflow in the cause it was built for, including the changed-code one, the hardest of the nine to stage. Seeing the morgue fill up with toe tags, each with a reason and a command, made the idea feel real: these workflows were never going to finish, and now each one says why.
Gemma's epitaphs are the only joke on the page. For order-1046, the recipe without sugar: "Even the sweetest recipes need all their ingredients." For order-1048, still waiting to be paid: "Awaiting a visitor who never arrived to the door."
Prize Categories
-
Best Use of Temporal: the whole diagnosis is built on Temporal's own APIs (list, describe, history,
DescribeTaskQueueand the workflow metadata query), and the demo reproduces nine real ways a workflow gets stuck, including a non-determinism error on replay. - Best Use of Gemma: Gemma 4 e4b runs locally through Ollama with a JSON schema, and every fact it writes is checked in code before anyone sees it.










Top comments (0)