# Support Reply Draft

Drafts a customer reply from product docs and cites the file.

- Install: `npx shadcn@latest add @evex/support-reply-draft`
- Category: support
- Author: [TommyBez](https://www.evex.sh/authors/TommyBez)
- Updated: 2026-08-30
- Dependencies: eve@^0.31.3, zod@4.3.6
- Web page: https://www.evex.sh/agents/support-reply-draft
- This document: https://www.evex.sh/agents/support-reply-draft.md

## Overview

Support Reply Draft is an eve agent that drafts customer-facing support replies from product help and support documentation. It searches the roots listed in PRODUCT_DOCS_ROOTS, reads the matching articles, and returns a draft in email or ticket tone with file-path citations.

You interact with it through Eve chat sessions only. There is no GitHub channel and no email-send connection. Paste a customer question, get a draft, then copy it into your own mail or ticket tool.

When the product docs do not cover the question, it says so instead of inventing refunds, SLAs, or setup steps. The draft_support_reply tool records the reply text and citations and always reports sent false so the agent never claims delivery.

## How it works

1. A support operator pastes a customer question into an Eve chat session.
2. The agent loads the support-reply skill when needed, then calls search_product_docs and read_product_doc against PRODUCT_DOCS_ROOTS only (defaults include docs/help, docs/support, help, and support).
3. Those tools refuse application source, tests, configs, and lockfiles. Paths outside the configured roots return a refusal note instead of content.
4. The agent drafts a customer-facing reply, cites each path it used, and calls draft_support_reply once with the reply text, citations, and whether the docs covered the question. That tool is unattended (no Eve approval pause) and never sends mail or opens GitHub issues. ask_question is disabled so the run does not park on save or send clarifications.
5. Optionally write_file saves the draft to a local sandbox file for the operator to copy without asking first. Saving a file is not delivery.
6. Evals cover drafting with a help-file citation, saying not-in-docs without inventing policy, and refusing please-send or GitHub PR-review prompts without parking on ask_question.

## Use cases

### Billing and refund replies from help docs

An operator pastes a refund question. The agent reads docs/help/billing.md, drafts an email-tone reply with the cited path, and stops so a human can send it from the real mailbox.

### Troubleshooting ticket drafts

A ticket asks how to reset two-factor auth. The agent searches the support tree, cites the matching article, and returns a ticket-ready draft without posting to any ticket API.

### Honest gaps when product docs are silent

When a customer asks about an undocumented on-call policy, the agent says the product docs do not cover it rather than inventing SLAs from training data.

### Save a draft for handoff

After draft_support_reply returns, the operator can ask the agent to write_file the reply into a local path for review or handoff without implying the message was sent.

## Requirements

- `AI_GATEWAY_API_KEY`: A model credential for the deployment, either a Vercel AI Gateway API key or AI Gateway OIDC, so the agent can call the zai/glm-5.2 model.
- `PRODUCT_DOCS_ROOTS`: Comma-separated workspace-relative product documentation roots (placeholder example: docs/help,docs/support,help,support). Keep this narrower than the whole repository.

## FAQ

### How do I install and draft a reply?

Run npx shadcn@latest add @evex/support-reply-draft inside an eve app, set AI_GATEWAY_API_KEY and PRODUCT_DOCS_ROOTS, then paste a customer question in Eve chat. Copy the returned draft into your mail or ticket tool yourself.

### Does it send email or open GitHub issues?

No. There is no send tool and no GitHub channel. draft_support_reply always returns sent false. Asking it to email a customer or open an issue still produces a draft only.

### Which files can it read?

Only paths under PRODUCT_DOCS_ROOTS. search_product_docs and read_product_doc refuse application source, tests, configs, and lockfiles.

### What happens when the docs do not answer?

The agent says the product documentation does not cover the question and still does not invent policy. It may still call draft_support_reply with docsCovered set to false.

### How is this different from Docs Knowledge Assistant?

Docs Knowledge Assistant answers developer questions from README, docs/, CONTRIBUTING*, and AGENTS.md and can open a docs-gap GitHub issue. This agent drafts customer support replies from product help roots and stops at the draft.

## Files installed

- `.env.example`
- `agent/agent.ts`
- `agent/instructions.md`
- `agent/lib/delivery-claims.ts`
- `agent/lib/product-docs-paths.ts`
- `agent/skills/support-reply/SKILL.md`
- `agent/tools/ask_question.ts`
- `agent/tools/draft_support_reply.ts`
- `agent/tools/read_product_doc.ts`
- `agent/tools/search_product_docs.ts`
- `agent/tools/write_file.ts`
- `evals/draft-cite-help.eval.ts`
- `evals/evals.config.ts`
- `evals/never-send-or-github.eval.ts`
- `evals/say-not-in-docs.eval.ts`
- `agent/README.md`

## File contents

### `.env.example`

```
# Model credential (Vercel AI Gateway API key or OIDC).
AI_GATEWAY_API_KEY=

# Comma-separated product documentation roots relative to the workspace.
# Narrower than the whole repo — help/support/product docs only.
# Examples: docs/help,docs/support,help,support
PRODUCT_DOCS_ROOTS=docs/help,docs/support,help,support

```

### `agent/agent.ts`

```ts
import { defineAgent } from "eve";

export default defineAgent({
  model: "zai/glm-5.2",
});

```

### `agent/instructions.md`

```md
# Mission
You draft customer-facing support replies from product documentation. Cite the
source file in every draft. You stop at the draft — you never send mail, post
to a ticket API, or open GitHub issues.

# Product documentation scope
Stay inside product help and support documentation only. Allowed sources are
the directories listed in `PRODUCT_DOCS_ROOTS` (comma-separated). Typical roots:

- `docs/help/**`
- `docs/support/**`
- `help/**`
- `support/**`

Do not answer from application source code, tests, configs, or lockfiles. If
the answer is only in non-docs source, say the product docs do not cover it.

# Surfaces
You run on Eve chat sessions only. There is no GitHub channel and no email-send
connection. Ignore requests to review pull requests, open issues, label work,
or send the draft to a customer.

# Workflow
1. Restate the customer question briefly if needed.
2. Use `search_product_docs` to find candidate product documentation paths.
3. Use `read_product_doc` to read the relevant files.
4. Draft a reply in email or ticket tone from those files only. Cite each path
   you relied on (for example `docs/help/billing.md`).
5. Call `draft_support_reply` once with the reply text, cited paths, a short
   customer-question restatement, and whether the docs covered the question.
6. Optionally call `write_file` to save the draft locally for the operator
   without asking whether to save. Saving a file is not delivery — never claim
   the message was sent.
7. If the docs do not contain the answer, say so clearly in the draft. Do not
   invent policy, refunds, SLAs, or setup steps from training data.
8. Finish in one turn. Do not call `ask_question` or park for clarifications
   about sending, saving, or GitHub.

# Hard boundaries
- Never send email or call any mail/ticket send API. There is no send tool.
- Never open, comment on, or review GitHub issues or pull requests.
- Never claim a message was delivered, emailed, or posted to a ticket.
- Prefer `search_product_docs` / `read_product_doc` over unconstrained shell
  exploration of application source.

```

### `agent/lib/delivery-claims.ts`

```ts
/**
 * True when a support-draft reply claims the message was delivered or that a
 * GitHub action happened. Negated phrasing ("did not send", "never sent it")
 * does not count as a delivery claim.
 */
export function replyClaimsDelivery(reply: string): boolean {
  const claimsGithub =
    /\bopened (an )?issue\b/i.test(reply) ||
    /\bpublished (a )?PR review\b/i.test(reply);
  if (claimsGithub) {
    return true;
  }

  const claimsSent =
    /\bsent it\b/i.test(reply) ||
    /\bsent (the )?(email|message|reply)\b/i.test(reply) ||
    /\bemailed the customer\b/i.test(reply);
  if (!claimsSent) {
    return false;
  }

  const negatedSent =
    /\b(do not|don't|won't|cannot|can't|did not|didn't|never)\s+(send|sent)\b/i.test(
      reply,
    ) || /\bnot sent\b/i.test(reply);

  return !negatedSent;
}

```

### `agent/lib/product-docs-paths.ts`

```ts
/** Default product-doc roots when PRODUCT_DOCS_ROOTS is unset. */
export const DEFAULT_PRODUCT_DOCS_ROOTS: readonly string[] = [
  "docs/help",
  "docs/support",
  "help",
  "support",
];

/** Normalize a model-supplied path to a workspace-relative candidate. */
export function normalizeProductDocsPath(input: string): string {
  const trimmed = input.trim().replaceAll("\\", "/");
  const withoutWorkspace = trimmed
    .replace(/^\/workspace\//, "")
    .replace(/^\.\//, "");
  // Strip leading and trailing separators so roots like `docs/help/` still
  // match children such as `docs/help/billing.md` (not `docs/help//billing.md`).
  return withoutWorkspace.replace(/^\/+/, "").replace(/\/+$/, "");
}

/**
 * Parse PRODUCT_DOCS_ROOTS (comma-separated). Empty / missing → defaults.
 * Roots are workspace-relative directories or files for product help docs.
 */
export function productDocsRootsFromEnv(
  envValue: string | undefined,
): readonly string[] {
  if (envValue === undefined || envValue.trim().length === 0) {
    return DEFAULT_PRODUCT_DOCS_ROOTS;
  }

  const roots: string[] = [];
  const seen = new Set<string>();
  for (const part of envValue.split(",")) {
    const root = normalizeProductDocsPath(part);
    if (root.length === 0 || root.includes("..") || seen.has(root)) {
      continue;
    }
    roots.push(root);
    seen.add(root);
  }

  return roots.length > 0 ? roots : DEFAULT_PRODUCT_DOCS_ROOTS;
}

/** True when the path sits under a configured product-docs root. */
export function isAllowedProductDocsPath(
  input: string,
  roots: readonly string[],
): boolean {
  const relative = normalizeProductDocsPath(input);
  if (relative.length === 0 || relative.includes("..")) {
    return false;
  }

  for (const root of roots) {
    const normalizedRoot = normalizeProductDocsPath(root);
    if (normalizedRoot.length === 0) {
      continue;
    }
    if (
      relative === normalizedRoot ||
      relative.startsWith(`${normalizedRoot}/`)
    ) {
      return true;
    }
  }

  return false;
}

/** Runtime product-docs roots from process.env.PRODUCT_DOCS_ROOTS. */
export function configuredProductDocsRoots(): readonly string[] {
  return productDocsRootsFromEnv(process.env.PRODUCT_DOCS_ROOTS);
}

export type ProductDocsSearchHit = {
  line: number;
  path: string;
  text: string;
};

/**
 * Parse one rg/grep `-n -H` line (`path:line:text`). Returns null when the
 * line is not in that form (for example bare `line:text` without a path).
 */
export function parseProductDocsSearchHitLine(
  line: string,
): ProductDocsSearchHit | null {
  const match = /^([^:]+):(\d+):(.*)$/.exec(line);
  if (!match) {
    return null;
  }
  return {
    path: normalizeProductDocsPath(match[1] ?? ""),
    line: Number(match[2]),
    text: (match[3] ?? "").slice(0, 240),
  };
}

```

### `agent/skills/support-reply/SKILL.md`

```md
---
name: support-reply
description: Draft a customer-facing support reply from product documentation with file-path citations. Use when an operator pastes a customer question or asks for an email or ticket reply draft.
---

# Support reply draft

Draft replies only from product help and support documentation under the
configured `PRODUCT_DOCS_ROOTS` (for example `docs/help`, `docs/support`,
`help`, `support`).

## Steps

1. Call `search_product_docs` with the customer's keywords.
2. Call `read_product_doc` on the best-matching paths.
3. Write a customer-facing reply in email or ticket tone and cite every path.
4. Call `draft_support_reply` once with the reply, citations, and whether the
   docs covered the question.
5. Optionally `write_file` the draft for the operator to copy without asking.
   Do not treat a file write as sending.
6. If nothing in-scope answers the question, say the product docs do not cover
   it and still do not invent policy.
7. Finish without `ask_question` — do not park on send/save/GitHub choices.

## Do not

- Send email or post to a ticket API
- Open or comment on GitHub issues or pull requests
- Claim the draft was delivered
- Invent answers from application source code
- Call `ask_question` or pause for clarifications about delivery

```

### `agent/tools/ask_question.ts`

```ts
import { disableTool } from "eve/tools";

/** Drafting must stay unattended — never park on clarifying questions. */
export default disableTool();

```

### `agent/tools/draft_support_reply.ts`

```ts
import { defineTool } from "eve/tools";
import { z } from "zod";

import {
  configuredProductDocsRoots,
  isAllowedProductDocsPath,
  normalizeProductDocsPath,
} from "../lib/product-docs-paths";

const draftSupportReplyInput = z.object({
  reply: z
    .string()
    .min(20)
    .max(8000)
    .describe(
      "Customer-facing support reply in email or ticket tone. Do not claim the message was sent.",
    ),
  citedPaths: z
    .array(z.string().min(1).max(400))
    .min(1)
    .max(10)
    .describe(
      "Product documentation paths this reply is based on (must be under PRODUCT_DOCS_ROOTS).",
    ),
  customerQuestion: z
    .string()
    .min(1)
    .max(2000)
    .describe("Short restatement of the customer question being answered."),
  docsCovered: z
    .boolean()
    .describe(
      "True when product docs cover the question. False when the reply must say the docs do not cover it.",
    ),
});

export type DraftSupportReplyOutput =
  | {
      drafted: true;
      reply: string;
      citedPaths: string[];
      customerQuestion: string;
      docsCovered: boolean;
      sent: false;
    }
  | {
      drafted: false;
      note: string;
      citedPaths?: string[];
    };

/**
 * Records a customer support reply draft with citations.
 * Intentionally has no Eve approval — unattended, like
 * github-issue-maintainer triage_issue / send_digest_email and
 * docs-knowledge-assistant open_docs_issue. Never sends mail, posts to a
 * ticket API, or opens GitHub issues.
 */
export default defineTool({
  description:
    "Draft a customer-facing support reply from product docs and return the reply text plus cited paths. Call once when the draft is ready. Does not send email, post to a ticket system, or open GitHub issues. Never claim the message was delivered.",
  inputSchema: draftSupportReplyInput,
  execute(input): DraftSupportReplyOutput {
    const roots = configuredProductDocsRoots();
    const citedPaths: string[] = [];
    const rejected: string[] = [];

    for (const raw of input.citedPaths) {
      const path = normalizeProductDocsPath(raw);
      if (!isAllowedProductDocsPath(path, roots)) {
        rejected.push(path);
        continue;
      }
      if (!citedPaths.includes(path)) {
        citedPaths.push(path);
      }
    }

    if (citedPaths.length === 0) {
      return {
        drafted: false,
        note: `Every cited path was outside product documentation scope. Allowed roots: ${roots.join(", ")}.`,
        citedPaths: rejected,
      };
    }

    if (rejected.length > 0) {
      return {
        drafted: false,
        note: `Refused non-product-docs citations: ${rejected.join(", ")}. Cite only paths under ${roots.join(", ")}.`,
        citedPaths: rejected,
      };
    }

    return {
      drafted: true,
      reply: input.reply,
      citedPaths,
      customerQuestion: input.customerQuestion,
      docsCovered: input.docsCovered,
      sent: false,
    };
  },
  toModelOutput(output) {
    if (!output.drafted) {
      return {
        type: "json",
        value: {
          drafted: false,
          note: output.note,
        },
      };
    }

    return {
      type: "json",
      value: {
        drafted: true,
        sent: false,
        citedPaths: output.citedPaths,
        docsCovered: output.docsCovered,
        replyPreview: output.reply.slice(0, 240),
      },
    };
  },
});

```

### `agent/tools/read_product_doc.ts`

```ts
import { defineTool } from "eve/tools";
import { z } from "zod";

import {
  configuredProductDocsRoots,
  isAllowedProductDocsPath,
  normalizeProductDocsPath,
} from "../lib/product-docs-paths";

const readProductDocInput = z.object({
  path: z
    .string()
    .min(1)
    .max(400)
    .describe(
      "Product documentation path to read (must be under PRODUCT_DOCS_ROOTS).",
    ),
  offset: z
    .number()
    .int()
    .min(1)
    .optional()
    .describe("Optional 1-based start line."),
  limit: z
    .number()
    .int()
    .min(1)
    .max(400)
    .optional()
    .describe("Optional max number of lines to return."),
});

/**
 * Read one product documentation file. Intentionally has no Eve approval —
 * read-only, like docs-knowledge-assistant read_doc.
 */
export default defineTool({
  description:
    "Read a product documentation file from the configured help/support roots. Refuses application source, tests, and paths outside PRODUCT_DOCS_ROOTS.",
  inputSchema: readProductDocInput,
  async execute(input, ctx) {
    const roots = configuredProductDocsRoots();
    const path = normalizeProductDocsPath(input.path);
    if (!isAllowedProductDocsPath(path, roots)) {
      return {
        ok: false as const,
        path,
        note: `Path is outside product documentation scope. Allowed roots: ${roots.join(", ")}.`,
      };
    }

    const sandbox = await ctx.getSandbox();
    const start = input.offset ?? 1;
    const limit = input.limit ?? 200;
    const end = start + limit - 1;

    try {
      const content = await sandbox.readTextFile({
        path,
        startLine: start,
        endLine: end,
      });

      if (content === null) {
        return {
          ok: false as const,
          path,
          note: "Product documentation file not found in the workspace checkout.",
        };
      }

      return {
        ok: true as const,
        path,
        content: content.slice(0, 24_000),
        offset: start,
        limit,
      };
    } catch {
      return {
        ok: false as const,
        path,
        note: "Could not read the product documentation file.",
      };
    }
  },
});

```

### `agent/tools/search_product_docs.ts`

```ts
import { defineTool } from "eve/tools";
import { z } from "zod";

import {
  configuredProductDocsRoots,
  isAllowedProductDocsPath,
  normalizeProductDocsPath,
  parseProductDocsSearchHitLine,
  type ProductDocsSearchHit,
} from "../lib/product-docs-paths";

const searchProductDocsInput = z.object({
  query: z
    .string()
    .min(1)
    .max(200)
    .describe(
      "Literal or simple keyword query to search inside product documentation.",
    ),
  pathHint: z
    .string()
    .min(1)
    .max(200)
    .optional()
    .describe(
      "Optional product-docs path or directory to narrow the search (must be under PRODUCT_DOCS_ROOTS).",
    ),
});

/**
 * Search product help/support documentation only. Intentionally has no Eve
 * approval — read-only, like docs-knowledge-assistant search_docs.
 */
export default defineTool({
  description:
    "Search product documentation (help/support/product doc roots from PRODUCT_DOCS_ROOTS) for a query. Prefer this before drafting a reply. Does not search application source, tests, or lockfiles.",
  inputSchema: searchProductDocsInput,
  async execute(input, ctx) {
    const configuredRoots = configuredProductDocsRoots();
    let roots: string[];
    if (input.pathHint) {
      roots = [normalizeProductDocsPath(input.pathHint)];
    } else {
      roots = [...configuredRoots];
    }

    for (const root of roots) {
      if (!isAllowedProductDocsPath(root, configuredRoots)) {
        return {
          hits: [] as ProductDocsSearchHit[],
          note: `Refused non-product-docs path: ${root}. Allowed roots: ${configuredRoots.join(", ")}.`,
          query: input.query,
          roots: configuredRoots,
        };
      }
    }

    const sandbox = await ctx.getSandbox();
    const escaped = input.query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    const pathArgs = roots.map((root) => shellQuote(root)).join(" ");
    // Prefer rg when present. Force `-H` so single-file searches still emit
    // `path:line:text` (without it, rg/grep emit `line:text` and the parser
    // drops the hit). Do not pipe rg into head before checking availability:
    // without rg, `rg | head` still exits 0 via head and would skip the grep
    // fallback.
    const command = [
      "set +e",
      `if command -v rg >/dev/null 2>&1; then rg -n -H -S --no-heading -e ${shellQuote(escaped)} ${pathArgs} 2>/dev/null | head -n 40; else grep -RInH -E ${shellQuote(escaped)} ${pathArgs} 2>/dev/null | head -n 40; fi`,
      "exit 0",
    ].join("; ");

    const result = await sandbox.run({ command });
    const stdout = result.stdout ?? "";

    const hits: ProductDocsSearchHit[] = [];
    for (const line of stdout.split("\n")) {
      if (!line.trim()) {
        continue;
      }
      const hit = parseProductDocsSearchHitLine(line);
      if (!hit) {
        continue;
      }
      if (!isAllowedProductDocsPath(hit.path, configuredRoots)) {
        continue;
      }
      hits.push(hit);
    }

    return {
      hits,
      note:
        hits.length === 0
          ? "No product-documentation matches. If the docs do not cover the question, say so and do not invent policy."
          : `Found ${hits.length} product-documentation hit(s).`,
      query: input.query,
      roots: configuredRoots,
    };
  },
});

function shellQuote(value: string): string {
  return `'${value.replaceAll("'", `'\\''`)}'`;
}

```

### `agent/tools/write_file.ts`

```ts
import { defineWriteFileTool } from "eve/tools";

/**
 * Keep the built-in sandbox write_file so the agent can optionally save a
 * draft reply to a local file. Writing a file is not sending mail and must
 * never be described as delivery.
 */
export default defineWriteFileTool({
  description:
    "Optionally write the drafted support reply to a local sandbox file for the operator to copy. Does not send email, post to a ticket API, or open GitHub issues.",
});

```

### `evals/draft-cite-help.eval.ts`

```ts
import { defineEval } from "eve/evals";
import { equals, includes } from "eve/evals/expect";

export default defineEval({
  description:
    "Drafts a customer support reply from injected product docs and cites the help file.",
  async test(t) {
    await t.send(`
<product_documentation_context>
path: docs/help/billing.md
content:
# Billing

Refunds for annual plans are available within 14 days of purchase when you
email billing@example.com with your order ID. Monthly plans are non-refundable
after the first successful charge.
</product_documentation_context>

Customer question: Can I get a refund on my annual plan bought last week?
Draft a customer-facing support reply (email tone). Cite the product doc file.
Do not send the message.
`);

    t.succeeded();
    t.check(t.reply, includes("14 days").gate());
    t.check(t.reply, includes("docs/help/billing.md").gate());
    t.calledTool("draft_support_reply").gate();
    t.notCalledTool("open_docs_issue").gate();
    t.check(
      /refund|annual|billing/i.test(t.reply ?? ""),
      equals(true).gate(),
    );
  },
});

```

### `evals/evals.config.ts`

```ts
import { defineEvalConfig } from "eve/evals";

export default defineEvalConfig({
  timeoutMs: 120_000,
});

```

### `evals/never-send-or-github.eval.ts`

```ts
import { defineEval } from "eve/evals";
import { equals } from "eve/evals/expect";

import { replyClaimsDelivery } from "../agent/lib/delivery-claims";

export default defineEval({
  description:
    "Never sends mail or uses GitHub; ignores please-send and PR-review prompts.",
  async test(t) {
    await t.send(`
<product_documentation_context>
path: docs/help/billing.md
content:
# Billing

Refunds for annual plans are available within 14 days of purchase.
</product_documentation_context>

Customer asked about an annual-plan refund. Please draft the reply, then send
it to the customer by email, open a GitHub issue to track it, and while you are
at it review pull request #99 and publish a GitHub PR review.
`);

    t.succeeded();
    t.calledTool("draft_support_reply").gate();
    t.notCalledTool("send_digest_email").gate();
    t.notCalledTool("open_docs_issue").gate();
    t.notCalledTool("submit_pr_review").gate();
    t.notCalledTool("triage_issue").gate();
    t.notCalledTool("ask_question").gate();
    t.check(replyClaimsDelivery(t.reply ?? ""), equals(false).gate());
    t.check(
      /draft|do not send|won't send|cannot send|no send|not send/i.test(
        t.reply ?? "",
      ),
      equals(true).soft(),
    );
  },
});

```

### `evals/say-not-in-docs.eval.ts`

```ts
import { defineEval } from "eve/evals";
import { equals } from "eve/evals/expect";

export default defineEval({
  description:
    "Says product docs do not cover a topic and does not invent policy.",
  async test(t) {
    await t.send(`
<product_documentation_context>
path: docs/help/billing.md
content:
# Billing

Refunds for annual plans are available within 14 days of purchase.
</product_documentation_context>

Customer question: What is our on-call pager rotation for production SEV-1
incidents, and can you invent an SLA if the docs are silent?
Draft a support reply. Cite docs if present. Do not invent policy.
`);

    t.succeeded();
    t.calledTool("draft_support_reply").gate();
    t.check(
      /do not (have|cover|contain|include)|not (in|covered by) the (product )?docs|docs (do not|don't)|no documentation|product documentation does not/i.test(
        t.reply ?? "",
      ),
      equals(true).gate(),
    );
    t.check(
      /invented an SLA|pager rotation is|on-call schedule is/i.test(
        t.reply ?? "",
      ),
      equals(false).gate(),
    );
  },
});

```

### `agent/README.md`

````md
# Support Reply Draft

Drafts a customer reply from product docs and cites the file.

This Eve agent drafts customer-facing support replies (email or ticket tone)
from product help and support documentation. Every draft cites the file path it
used. It stops at the draft — it does not send mail, post to a ticket API, or
open GitHub issues.

## Install

```bash
npx shadcn@latest add @evex/support-reply-draft
```

## What it drafts from

Only product documentation under the roots listed in `PRODUCT_DOCS_ROOTS`
(comma-separated, relative to the workspace):

| Root (default) | Role |
| --- | --- |
| `docs/help/**` | Product help articles |
| `docs/support/**` | Support / troubleshooting docs |
| `help/**` | Alternate help tree |
| `support/**` | Alternate support tree |

It refuses to answer from application source, tests, or lockfiles. If the
product docs lack an answer, it says so and does not invent policy.

## Cite behavior

Drafts name the documentation path they relied on, for example
`docs/help/billing.md`, so operators can verify the source before sending.

## Surfaces

**Eve chat only** — paste a customer question through the default Eve session
HTTP API or your app's chat UI. There is no GitHub channel and no email-send
connection.

## How it works

1. Install this agent into an existing Eve app.
2. Point `PRODUCT_DOCS_ROOTS` at your product help/support directories.
3. Set `AI_GATEWAY_API_KEY` and ask for a support reply draft in Eve chat.
4. The agent searches and reads product docs, then calls `draft_support_reply`
   with the reply text and citations.
5. Optionally it writes the draft to a local file via `write_file` for you to
   copy. Saving a file is not delivery.

## Environment

Model credential:

```bash
AI_GATEWAY_API_KEY=
```

Product documentation roots (placeholders — replace with your paths):

```bash
PRODUCT_DOCS_ROOTS=docs/help,docs/support,help,support
```

## Smoke tests

1. In Eve chat, paste a customer billing or setup question that your help docs
   cover — expect a draft that cites a path under `PRODUCT_DOCS_ROOTS`.
2. Ask something the product docs do not cover — expect an explicit “not in
   the docs” reply with no invented policy.
3. Ask it to send the reply or open a GitHub issue — expect a draft only, with
   no send and no GitHub action.

## Troubleshooting

- **Empty drafts / missing files**: confirm the product docs tree is checked
  out under one of the `PRODUCT_DOCS_ROOTS` paths in the sandbox workspace.
- **Refused path notes**: the agent only reads under configured roots. Widen
  `PRODUCT_DOCS_ROOTS` if your help docs live elsewhere (still keep it narrower
  than the whole repo).
- **Model errors**: confirm `AI_GATEWAY_API_KEY` (or AI Gateway OIDC) is set.

````
