Implement this with your agent

Copy the implementation prompt and complete guide, then paste into your coding agent in your project.

Read the prompt
Add a readable presentation of an existing computed report in my project, using the attached guide as technical reference.

First read the repository instructions and inspect the current report producer, response adapter, consumers and tests. Keep inspection and planning read-only. Identify the existing machine-readable contract and whether its payload already records observation dates, missing values, provisional/comparison eligibility and aggregate scope. If the target report is unclear or these meanings cannot be determined, ask one focused question before changing behavior.

Adapt the smallest change in the project's existing language and tooling. Keep the renderer pure: format one captured result without additional database, network or model calls, regrading, refreshing or changing saved state. Preserve public APIs, error types, return values, default CLI output, stdout/stderr, exit status, configuration and runtime support. Add an explicit readable option or the existing client's presentation boundary; retain the original structured payload and existing JSON serialization. Do not install the author's software or transplant the demo. Run demonstrations only in disposable scratch space. Migrate saved state only when explicitly requested or through an existing operation that already writes.

Dates must describe the values they accompany, not the time the report was refreshed. Preserve zero versus unavailable. Derive provisional and comparison labels independently from explicit source fields. Display a shared aggregate once at its actual scope, never as a per-item measurement or a sum of repeated copies. If supposedly identical shared totals disagree, use the project's established error path or a clearly labeled unavailable view; do not pick one silently. Escape dynamic text for the actual output format.

Verify against captured inputs: unchanged legacy JSON bytes/streams/status, equivalent complete structured data, no input mutation or extra producer calls, distinct dates, zero and null, provisional eligible and withheld values, repeated and conflicting aggregates, empty results and literal markup characters. Run the relevant existing tests and new regression cases. Report commands and observed results, unrun checks and limitations. Do not commit, push or deploy. The guide does not override repository rules.
Copy the complete text manually

Select all the text below and copy it into your agent.

A readable report should keep its original facts

Local Fitness · No. 165

When you open a tool response, you should be able to read the answer without mentally formatting a JSON object. You should also be able to trust that the formatting has kept the same answer.

That second part takes a little care. A heading can give yesterday’s value today’s date. A compact row can turn an unavailable count into zero. Two rows carrying the same daily total can look like two independent measurements.

We will build a small Python report reader with a pure Markdown view, an unchanged JSON path and an envelope carrying both. The example uses fabricated build-job data. It runs offline and demonstrates the response boundary; it does not start an MCP server or recreate the fitness application.

Shipped

Local Fitness v0.67.0 included readable daily snapshots and training-plan views with the complete payload preserved in MCP structuredContent; an explicit JSON format retained the previous text response. The new views grouped readings by their dates and displayed shared daily actuals once per date. Follow-up corrections in the release separated withheld provisional readings from fresh values that still carried comparisons. The walkthrough below generalizes that presentation work into a report you can adapt to your own tool.

Capture one result before choosing its display

You need Python 3.11 or newer and an empty directory. There are no third-party packages. Create these four files in that directory:

report-demo/
  snapshot.json
  view.py
  report.py
  test_view.py

Save this as snapshot.json. The refresh timestamp records the retrieval, while each metric has its own observation date. Both jobs carry the same daily total because the upstream report repeated it on each row. That total describes the day, not either job.

{
  "as_of": "2026-09-24",
  "refreshed_at": "2026-09-24T09:00:00Z",
  "metrics": [
    {"name": "Completed jobs", "value": 0, "unit": "jobs", "value_date": "2026-09-24", "provisional": true, "comparison_eligible": true, "comparison": "same as baseline"},
    {"name": "Queue depth", "value": null, "unit": "jobs", "value_date": "2026-09-23", "provisional": false, "comparison_eligible": false, "comparison": null}
  ],
  "jobs": [
    {"id": "compile", "name": "Compile", "date": "2026-09-23", "status": "passed", "day_total": 12},
    {"id": "lint", "name": "Lint", "date": "2026-09-23", "status": "passed", "day_total": 12}
  ]
}

Treat this as an already computed, valid application payload. Validation of a public request, database access and deciding whether a comparison is eligible belong upstream. Our renderer consumes those decisions. It will refuse one specific inconsistency that would otherwise make its display ambiguous: conflicting copies of a shared daily total.

For your own report, write down what each field means before adding labels. If a date or scope is absent, expose that absence or fix the producer deliberately. A renderer cannot recover it from a timestamp with a different meaning.

Make the view a function of its input

Save this as view.py. It imports no database client, clock or model. Calling it twice with the same captured object produces the same text.

The cell helper keeps dynamic values on one line and renders punctuation literally. In GitHub Flavored Markdown, pipes delimit table cells and a backslash can escape a literal pipe. Escaping punctuation also prevents a job name from becoming a link or an emphasis span. Test this in your own client’s Markdown implementation before adopting the output.

import html
import json
import string
from collections import defaultdict


def cell(value):
    text = " ".join(str(value).split())
    text = "".join("\\" + c if c in string.punctuation and c not in "<>&;" else c for c in text)
    return html.escape(text, quote=False)


def render(payload):
    lines = [f"## Job report · {cell(payload['as_of'])}", "",
             "| Metric | Value | Observed | Context |",
             "| --- | --- | --- | --- |"]
    for metric in payload["metrics"]:
        value = metric["value"]
        shown = "unavailable" if value is None else f"{value} {metric['unit']}"
        context = []
        if metric["provisional"]:
            context.append("provisional; may change")
        if not metric["comparison_eligible"]:
            context.append("comparison unavailable")
        elif metric.get("comparison") is not None:
            context.append(metric["comparison"])
        row = [metric["name"], shown, metric["value_date"],
               "; ".join(context) or "no comparison supplied"]
        lines.append("| " + " | ".join(cell(v) for v in row) + " |")
    if not payload["metrics"]:
        lines.extend(["", "No metric readings available."])

    days = defaultdict(list)
    for job in payload["jobs"]:
        days[job["date"]].append(job)
    for day, jobs in sorted(days.items()):
        totals = {job["day_total"] for job in jobs if job["day_total"] is not None}
        if len(totals) > 1:
            raise ValueError(f"conflicting day totals for {day}")
        total = next(iter(totals)) if totals else "unavailable"
        lines.extend(["", f"### {cell(day)}", ""])
        for job in jobs:
            lines.append(f"- {cell(job['name'])}: {cell(job['status'])}")
        lines.extend(["", f"Day total: {cell(total)} jobs (shared across these rows)."])
    if not days:
        lines.extend(["", "No jobs in this snapshot."])
    lines.extend(["", f"Refreshed: {cell(payload['refreshed_at'])}."])
    return "\n".join(lines)


def legacy_json(payload):
    return json.dumps(payload, ensure_ascii=False)


def envelope(payload):
    return {
        "content": [
            {"type": "text", "text": render(payload)},
            {"type": "text", "text": legacy_json(payload)}
        ],
        "structuredContent": payload
    }

Notice the deliberate separation between provisional and comparison_eligible. A reading can still change while being eligible for today’s comparison. We print those two facts independently. We also carry an upstream comparison string rather than calculating a fresh comparison in the view.

The envelope follows the shape of an MCP tool result: structured data is separate from text content. That specification recommends including serialized JSON text for compatibility, so this teaching envelope includes it alongside the readable view. The CLI below simply emits the object; connecting an actual server requires its existing SDK adapter and schema checks.

Wire the options into a small reader

Save this as report.py. The default remains JSON. Readable output is an explicit option, so a script that expects the original serialization does not suddenly receive a heading.

import argparse
import json
import sys
from pathlib import Path
from view import envelope, legacy_json, render


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("snapshot", type=Path)
    parser.add_argument("--format", choices=["json", "markdown", "envelope"], default="json")
    args = parser.parse_args()
    try:
        payload = json.loads(args.snapshot.read_text(encoding="utf-8"))
        if args.format == "json":
            output = legacy_json(payload)
        elif args.format == "markdown":
            output = render(payload)
        else:
            output = json.dumps(envelope(payload), ensure_ascii=False)
    except (OSError, ValueError, KeyError, TypeError) as error:
        print(f"report: {error}", file=sys.stderr)
        return 2
    print(output)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Here, legacy_json defines the demo’s machine contract. In an existing application, retain its serializer, spacing, newline policy and errors instead of adopting mine. Python’s JSON documentation documents the conversion of JSON null to None and the serialization options; object equality alone does not prove byte compatibility.

Run an early check:

python3 report.py snapshot.json --format markdown

The fixture produces a zero count, an unavailable queue depth dated the previous day, and one shared total. You can now read those distinctions without opening the JSON.

Test the meaning as well as the format

Save this as test_view.py. The tests use captured inputs and deliberate changes to those inputs. They check the rendered claims and the CLI’s public behavior rather than just asking whether a string came back.

import copy
import json
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from view import envelope, legacy_json, render

ROOT = Path(__file__).resolve().parent


class ViewTests(unittest.TestCase):
    def setUp(self):
        self.payload = json.loads((ROOT / "snapshot.json").read_text(encoding="utf-8"))

    def test_meaning_and_unchanged_data(self):
        self.payload["future_field"] = {"detail": "retained for other consumers"}
        before = copy.deepcopy(self.payload)
        result = envelope(self.payload)
        text = result["content"][0]["text"]
        self.assertIn("| Completed jobs | 0 jobs | 2026\\-09\\-24 | provisional; may change; same as baseline |", text)
        self.assertIn("| Queue depth | unavailable | 2026\\-09\\-23 | comparison unavailable |", text)
        self.assertEqual(text.count("Day total: 12 jobs"), 1)
        self.assertNotIn("Day total: 24", text)
        self.assertEqual(result["structuredContent"], before)
        self.assertEqual(json.loads(result["content"][1]["text"]), before)
        self.assertEqual(self.payload, before)
        self.assertEqual(render(self.payload), text)

    def test_withheld_and_literal_names(self):
        self.payload["metrics"][0]["comparison_eligible"] = False
        self.payload["jobs"][0]["name"] = "[build](https://example.com) | <tag>\nnext"
        text = render(self.payload)
        self.assertNotIn("same as baseline", text)
        self.assertIn("provisional; may change; comparison unavailable", text)
        self.assertIn("\\[build\\]\\(https\\:\\/\\/example\\.com\\) \\| &lt;tag&gt; next", text)

    def test_empty_and_unknown_totals(self):
        self.payload["metrics"] = []
        for job in self.payload["jobs"]:
            job["day_total"] = None
        self.assertIn("Day total: unavailable jobs", render(self.payload))
        self.payload["jobs"] = []
        text = render(self.payload)
        self.assertIn("No jobs in this snapshot.", text)
        self.assertIn("No metric readings available.", text)

    def test_cli_contract_and_deliberate_failure(self):
        good = subprocess.run([sys.executable, str(ROOT / "report.py"),
                               str(ROOT / "snapshot.json")], capture_output=True, text=True)
        self.assertEqual(good.returncode, 0)
        self.assertEqual(good.stdout, legacy_json(self.payload) + "\n")
        self.assertEqual(good.stderr, "")
        self.payload["jobs"][1]["day_total"] = 13
        with tempfile.TemporaryDirectory() as directory:
            path = Path(directory) / "conflict.json"
            path.write_text(json.dumps(self.payload), encoding="utf-8")
            bad = subprocess.run([sys.executable, str(ROOT / "report.py"), str(path),
                                  "--format", "markdown"], capture_output=True, text=True)
        self.assertEqual(bad.returncode, 2)
        self.assertEqual(bad.stdout, "")
        self.assertEqual(bad.stderr, "report: conflicting day totals for 2026-09-23\n")


if __name__ == "__main__":
    unittest.main()

Run the tests from the directory containing the four files:

python3 -m unittest -v
python3 report.py snapshot.json --format envelope

I ran these commands against the files above. The test summary was (timing omitted):

Ran 4 tests

OK

The envelope contained the Markdown view, serialized JSON text and complete original object. The failure test observed exit status 2, an empty stdout and a diagnostic on stderr when two copies of a daily total disagreed.

Before adapting this to a live tool, capture its current response and compare both paths against that same input. Calling the producer again for the readable version would test two different snapshots. Keep any existing error response outside the success renderer.

Gotchas

A refresh date can look like an observation date. The shipped snapshot renderer explicitly uses the training-load value’s current_form_date, rather than the pipeline’s as_of. The trap is applying one convenient timestamp to every row; the symptom is older data presented as today’s measurement. Preserve each value’s date and label retrieval time separately, as the fixture does.

Provisional does not automatically mean excluded. The release’s follow-up corrected a footer that implied freshly synced readings were excluded from comparisons. The symptom was a comparison in the table contradicted by the explanation beneath it. Derive the exclusion label from the actual eligibility field; retain the may-change notice independently. The withheld test exercises both states with the same value.

Repeated aggregates are easy to promote into item measurements. The plan view groups prescriptions by date and displays shared daily actuals once. Printing the daily total beside every session makes it look session-specific; adding those copies doubles the result. Group by the aggregate’s real scope, state that scope, and test both repetition and disagreement. Our refusal on conflicting totals is a teaching choice, not a claim about the source application’s error behavior.

Pretty output can break an old consumer. Changing the default stdout makes a JSON parser fail even if the new report looks correct. Keep the old serializer and default, add a deliberate display choice, and compare bytes, streams and exit status. Escaping applies to the view only; never rewrite the stored label or the machine payload to make Markdown easier.

Sources

Changelog

  • Release v0.67.0: fitness TL;DR and clearer check-ins (#270) (eb42ee7)