diff --git a/AGENTS.md b/AGENTS.md
index 20fb44d..15bdfa5 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -39,6 +39,30 @@ Set `GITEA_TOKEN` or write the token to `.gitea-credentials` (gitignored). Never
- Open a PR on Gitea and squash-merge after review
- CI runs `pnpm typecheck && pnpm lint` on every PR automatically
+## Testing
+
+Executor agents should follow this three-step workflow when implementing or verifying any change:
+
+**1. Fetch the test plan before starting**
+```bash
+curl /api/testkit/plan
+```
+Returns `TIMMY_TEST_PLAN.md` — full architecture notes, route descriptions, and expected behaviour for all 24 tests. Read this first so you understand what each endpoint is supposed to do before touching the code.
+
+**2. Run the full test suite after implementing**
+```bash
+curl -s /api/testkit | bash
+```
+The server returns a self-contained bash script with the base URL already baked in. Requirements: `curl`, `bash`, `jq` — nothing else. All 24 tests must pass (FAIL=0) before submitting.
+
+**3. Fill in and submit the report**
+```bash
+curl /api/testkit/report
+```
+Returns just the report template section ready to copy and fill in. Attach the completed report to your PR or task output.
+
+Where `` is the running server URL, e.g. `http://localhost:8080` locally or the Replit dev URL in CI.
+
## Stub mode
The API server starts without Lightning or AI credentials:
diff --git a/artifacts/api-server/src/routes/testkit.ts b/artifacts/api-server/src/routes/testkit.ts
index 6c2a6ee..546c81c 100644
--- a/artifacts/api-server/src/routes/testkit.ts
+++ b/artifacts/api-server/src/routes/testkit.ts
@@ -1,4 +1,7 @@
import { Router, type Request, type Response } from "express";
+import { readFileSync } from "fs";
+import { resolve, dirname } from "path";
+import { fileURLToPath } from "url";
const router = Router();
@@ -757,4 +760,50 @@ if [[ "\$FAIL" -gt 0 ]]; then exit 1; fi
res.send(script);
});
+/**
+ * GET /api/testkit/plan
+ *
+ * Returns TIMMY_TEST_PLAN.md verbatim as text/markdown.
+ * Path resolves from the project root regardless of cwd.
+ */
+const _dirname = dirname(fileURLToPath(import.meta.url));
+const PLAN_PATH = resolve(_dirname, "../../../../TIMMY_TEST_PLAN.md");
+
+router.get("/testkit/plan", (_req: Request, res: Response) => {
+ let content: string;
+ try {
+ content = readFileSync(PLAN_PATH, "utf-8");
+ } catch {
+ res.status(500).json({ error: "TIMMY_TEST_PLAN.md not found on server" });
+ return;
+ }
+ res.setHeader("Content-Type", "text/markdown; charset=utf-8");
+ res.send(content);
+});
+
+/**
+ * GET /api/testkit/report
+ *
+ * Returns only the report template section from TIMMY_TEST_PLAN.md —
+ * everything from the "## Report template" heading to end-of-file.
+ * Returned as text/plain so agents can copy and fill in directly.
+ */
+router.get("/testkit/report", (_req: Request, res: Response) => {
+ let content: string;
+ try {
+ content = readFileSync(PLAN_PATH, "utf-8");
+ } catch {
+ res.status(500).json({ error: "TIMMY_TEST_PLAN.md not found on server" });
+ return;
+ }
+ const marker = "## Report template";
+ const idx = content.indexOf(marker);
+ if (idx === -1) {
+ res.status(500).json({ error: "Report template section not found in plan" });
+ return;
+ }
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
+ res.send(content.slice(idx));
+});
+
export default router;