Implementation plan for reply and reply-with-quote context
This commit is contained in:
516
docs/superpowers/plans/2026-09-22-reply-context.md
Normal file
516
docs/superpowers/plans/2026-09-22-reply-context.md
Normal file
@@ -0,0 +1,516 @@
|
||||
# Reply and quoted-reply context implementation plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** When the owner replies to a Telegram message (or "reply with quote"s a substring), inject a `[Reply to your message: "…"]`-style header into the agent's next turn so it knows what's being answered.
|
||||
|
||||
**Architecture:** Extend the `InboundMessage` and `InboundDeps` shapes in `src/telegram/inbound.ts` to carry `reply_to_message`, `quote`, and `botUserId`. Add a pure `buildReplyHeader(reply, quote, botUserId)` helper in the same file that returns the header line (or `undefined`). Compose the header ahead of the existing forward header when both are present. Thread `bot.botInfo?.id` from `bridge.ts` into the deps. No changes to the debouncer, outbound, or agent host.
|
||||
|
||||
**Tech Stack:** TypeScript, Node 24, `grammy` (Telegram), `vitest`.
|
||||
|
||||
**Design spec:** `docs/superpowers/specs/2026-09-22-reply-context-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- **Modify** `src/telegram/inbound.ts` — extend `InboundMessage` and `InboundDeps`, add `RepliedMessage` and `Quote` types, add `buildReplyHeader()` helper, prepend the header before the forward header when composing the final text.
|
||||
- **Modify** `src/telegram/inbound.test.ts` — new tests for every branch of the header helper and the composition ordering.
|
||||
- **Modify** `src/telegram/bridge.ts` — pass `botUserId: bot.botInfo?.id` into `normalizeMessage` deps.
|
||||
- **Modify** `README.md` — one bullet under "Talking to it", one new manual E2E item #19.
|
||||
|
||||
The header helper is small enough that a separate file would be premature — it lives alongside `describeForwardOrigin`, which is exactly the same shape and function.
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Add types and a truncation helper
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/telegram/inbound.ts`
|
||||
|
||||
- [ ] **Step 1: Add `RepliedMessage`, `Quote`, and `MAX_REPLY_CONTEXT_CHARS` at the top of the file, next to `ForwardOrigin`**
|
||||
|
||||
Insert after the existing `ForwardOrigin` union (around `inbound.ts:10`):
|
||||
|
||||
```ts
|
||||
export interface RepliedMessage {
|
||||
from?: { id: number; is_bot?: boolean };
|
||||
text?: string;
|
||||
caption?: string;
|
||||
photo?: Array<unknown>;
|
||||
voice?: unknown;
|
||||
audio?: { file_name?: string };
|
||||
document?: { file_name?: string };
|
||||
}
|
||||
|
||||
export interface Quote {
|
||||
text: string;
|
||||
is_manual?: boolean;
|
||||
}
|
||||
|
||||
/** Longest replied-to text (or quote) we inline into the header before truncating with an ellipsis. */
|
||||
export const MAX_REPLY_CONTEXT_CHARS = 500;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Extend `InboundMessage` with `reply_to_message` and `quote`**
|
||||
|
||||
Change `inbound.ts`'s existing `InboundMessage` interface to add two optional fields at the end:
|
||||
|
||||
```ts
|
||||
export interface InboundMessage {
|
||||
text?: string;
|
||||
caption?: string;
|
||||
photo?: Array<{ file_id: string; width: number; height: number; file_size?: number }>;
|
||||
voice?: { file_id: string; mime_type?: string; duration?: number };
|
||||
audio?: { file_id: string; mime_type?: string; file_name?: string };
|
||||
document?: { file_id: string; file_name?: string; mime_type?: string };
|
||||
forward_origin?: ForwardOrigin;
|
||||
reply_to_message?: RepliedMessage;
|
||||
quote?: Quote;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Extend `InboundDeps` with `botUserId`**
|
||||
|
||||
Change the existing `InboundDeps` interface to add one optional field:
|
||||
|
||||
```ts
|
||||
export interface InboundDeps {
|
||||
download(fileId: string): Promise<{ bytes: Buffer; filePath: string }>;
|
||||
transcriber?: Transcriber;
|
||||
workspaceDir: string;
|
||||
now?: () => Date;
|
||||
/** The bot's own user id, for labelling replies to bot vs owner messages. */
|
||||
botUserId?: number;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Typecheck**
|
||||
|
||||
Run: `npm run typecheck`
|
||||
Expected: exits 0 (no `noEmit` errors). If it fails, the shape of an existing test's `InboundMessage`/`InboundDeps` object is now under-typed — but every new field is optional so this should pass.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/telegram/inbound.ts
|
||||
git commit -m "Add reply, quote, and botUserId types to inbound"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Write and pass tests for `buildReplyHeader`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/telegram/inbound.ts`
|
||||
- Modify: `src/telegram/inbound.test.ts`
|
||||
|
||||
- [ ] **Step 1: Add the failing tests for the header helper**
|
||||
|
||||
Append to `src/telegram/inbound.test.ts` after the last `describe(...)` block (i.e. at the end of the file). This starts a new `describe` block so it can co-exist with the existing `saveToInbox` block:
|
||||
|
||||
```ts
|
||||
describe("buildReplyHeader", () => {
|
||||
const BOT_ID = 42;
|
||||
|
||||
it("returns undefined when there is no reply and no quote", () => {
|
||||
expect(buildReplyHeader(undefined, undefined, BOT_ID)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("labels a plain reply to a bot message with 'your message'", () => {
|
||||
const header = buildReplyHeader({ from: { id: BOT_ID, is_bot: true }, text: "Which file?" }, undefined, BOT_ID);
|
||||
expect(header).toBe('[Reply to your message: "Which file?"]');
|
||||
});
|
||||
|
||||
it("labels a plain reply to the owner's own earlier message", () => {
|
||||
const header = buildReplyHeader({ from: { id: 99, is_bot: false }, text: "Buy milk" }, undefined, BOT_ID);
|
||||
expect(header).toBe('[Reply to your own earlier message: "Buy milk"]');
|
||||
});
|
||||
|
||||
it("falls back to 'the earlier message' when `from` is absent", () => {
|
||||
const header = buildReplyHeader({ text: "hello" }, undefined, BOT_ID);
|
||||
expect(header).toBe('[Reply to the earlier message: "hello"]');
|
||||
});
|
||||
|
||||
it("uses the quote text and 'quoting your message' wording for a reply-with-quote to the bot", () => {
|
||||
const header = buildReplyHeader(
|
||||
{ from: { id: BOT_ID, is_bot: true }, text: "The full assistant answer with lots of context." },
|
||||
{ text: "assistant answer" },
|
||||
BOT_ID,
|
||||
);
|
||||
expect(header).toBe('[Reply quoting your message: "assistant answer"]');
|
||||
});
|
||||
|
||||
it("uses 'quoting your own earlier message' when the source is the owner's own", () => {
|
||||
const header = buildReplyHeader(
|
||||
{ from: { id: 99, is_bot: false }, text: "shopping list" },
|
||||
{ text: "shopping" },
|
||||
BOT_ID,
|
||||
);
|
||||
expect(header).toBe('[Reply quoting your own earlier message: "shopping"]');
|
||||
});
|
||||
|
||||
it("treats an empty quote.text as if there were no quote (falls through to plain reply)", () => {
|
||||
const header = buildReplyHeader(
|
||||
{ from: { id: BOT_ID, is_bot: true }, text: "The answer" },
|
||||
{ text: "" },
|
||||
BOT_ID,
|
||||
);
|
||||
expect(header).toBe('[Reply to your message: "The answer"]');
|
||||
});
|
||||
|
||||
it("describes a photo-only replied-to message", () => {
|
||||
const header = buildReplyHeader({ from: { id: BOT_ID, is_bot: true }, photo: [{}] }, undefined, BOT_ID);
|
||||
expect(header).toBe('[Reply to your message: "(photo)"]');
|
||||
});
|
||||
|
||||
it("describes a voice-only replied-to message", () => {
|
||||
const header = buildReplyHeader({ from: { id: BOT_ID, is_bot: true }, voice: {} }, undefined, BOT_ID);
|
||||
expect(header).toBe('[Reply to your message: "(voice message)"]');
|
||||
});
|
||||
|
||||
it("describes a document with a filename", () => {
|
||||
const header = buildReplyHeader(
|
||||
{ from: { id: BOT_ID, is_bot: true }, document: { file_name: "notes.txt" } },
|
||||
undefined,
|
||||
BOT_ID,
|
||||
);
|
||||
expect(header).toBe('[Reply to your message: "(document: notes.txt)"]');
|
||||
});
|
||||
|
||||
it("describes an unnamed document", () => {
|
||||
const header = buildReplyHeader({ from: { id: BOT_ID, is_bot: true }, document: {} }, undefined, BOT_ID);
|
||||
expect(header).toBe('[Reply to your message: "(document)"]');
|
||||
});
|
||||
|
||||
it("uses the caption instead of the media descriptor when both are present", () => {
|
||||
const header = buildReplyHeader(
|
||||
{ from: { id: BOT_ID, is_bot: true }, photo: [{}], caption: "the sunset" },
|
||||
undefined,
|
||||
BOT_ID,
|
||||
);
|
||||
expect(header).toBe('[Reply to your message: "the sunset"]');
|
||||
});
|
||||
|
||||
it("truncates replied-to text longer than 500 characters with an ellipsis", () => {
|
||||
const long = "a".repeat(600);
|
||||
const header = buildReplyHeader({ from: { id: BOT_ID, is_bot: true }, text: long }, undefined, BOT_ID);
|
||||
expect(header).toBe(`[Reply to your message: "${"a".repeat(499)}…"]`);
|
||||
});
|
||||
|
||||
it("truncates a very long quote in the same way", () => {
|
||||
const long = "b".repeat(600);
|
||||
const header = buildReplyHeader(
|
||||
{ from: { id: BOT_ID, is_bot: true }, text: "irrelevant" },
|
||||
{ text: long },
|
||||
BOT_ID,
|
||||
);
|
||||
expect(header).toBe(`[Reply quoting your message: "${"b".repeat(499)}…"]`);
|
||||
});
|
||||
|
||||
it("labels as 'your message' when botUserId is missing but is_bot is true", () => {
|
||||
const header = buildReplyHeader({ from: { id: 0, is_bot: true }, text: "hi" }, undefined, undefined);
|
||||
expect(header).toBe('[Reply to your message: "hi"]');
|
||||
});
|
||||
|
||||
it("falls back to '(message)' when the replied-to message has no usable fields", () => {
|
||||
const header = buildReplyHeader({ from: { id: BOT_ID, is_bot: true } }, undefined, BOT_ID);
|
||||
expect(header).toBe('[Reply to your message: "(message)"]');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Also update the top-of-file import to pull in `buildReplyHeader` and `MAX_REPLY_CONTEXT_CHARS` (the constant is exported so tests can reference the boundary if needed, though this batch only uses the function):
|
||||
|
||||
```ts
|
||||
import { buildReplyHeader, type InboundDeps, normalizeMessage, saveToInbox } from "./inbound.js";
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the tests to confirm they fail**
|
||||
|
||||
Run: `npx vitest run src/telegram/inbound.test.ts`
|
||||
Expected: FAIL — the tests reference `buildReplyHeader` which doesn't exist yet. Error like "buildReplyHeader is not a function" or a TS compilation error.
|
||||
|
||||
- [ ] **Step 3: Implement `buildReplyHeader` in `src/telegram/inbound.ts`**
|
||||
|
||||
Add at the end of `src/telegram/inbound.ts`, after the existing `errorText` helper:
|
||||
|
||||
```ts
|
||||
/** Builds the `[Reply ...]` header line for the agent, or returns undefined when there is nothing to reply to. */
|
||||
export function buildReplyHeader(
|
||||
reply: RepliedMessage | undefined,
|
||||
quote: Quote | undefined,
|
||||
botUserId: number | undefined,
|
||||
): string | undefined {
|
||||
if (!reply) return undefined;
|
||||
const isFromBot =
|
||||
reply.from !== undefined &&
|
||||
(botUserId !== undefined ? reply.from.id === botUserId : reply.from.is_bot === true);
|
||||
const isFromOwner = reply.from !== undefined && !isFromBot;
|
||||
const quoted = quote?.text ?? "";
|
||||
if (quoted.length > 0) {
|
||||
const who = isFromBot
|
||||
? "your message"
|
||||
: isFromOwner
|
||||
? "your own earlier message"
|
||||
: "the earlier message";
|
||||
return `[Reply quoting ${who}: "${truncateReplyContext(quoted)}"]`;
|
||||
}
|
||||
const who = isFromBot
|
||||
? "your message"
|
||||
: isFromOwner
|
||||
? "your own earlier message"
|
||||
: "the earlier message";
|
||||
const content = truncateReplyContext(replyContent(reply));
|
||||
return `[Reply to ${who}: "${content}"]`;
|
||||
}
|
||||
|
||||
function replyContent(reply: RepliedMessage): string {
|
||||
if (reply.text && reply.text.length > 0) return reply.text;
|
||||
if (reply.caption && reply.caption.length > 0) return reply.caption;
|
||||
if (reply.photo && reply.photo.length > 0) return "(photo)";
|
||||
if (reply.voice) return "(voice message)";
|
||||
if (reply.audio) return reply.audio.file_name ? `(audio: ${reply.audio.file_name})` : "(audio)";
|
||||
if (reply.document) return reply.document.file_name ? `(document: ${reply.document.file_name})` : "(document)";
|
||||
return "(message)";
|
||||
}
|
||||
|
||||
function truncateReplyContext(text: string): string {
|
||||
const chars = Array.from(text);
|
||||
if (chars.length <= MAX_REPLY_CONTEXT_CHARS) return text;
|
||||
return `${chars.slice(0, MAX_REPLY_CONTEXT_CHARS - 1).join("")}…`;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the tests to confirm they pass**
|
||||
|
||||
Run: `npx vitest run src/telegram/inbound.test.ts`
|
||||
Expected: PASS — all new `buildReplyHeader` cases green, existing `normalizeMessage` and `saveToInbox` cases still green.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/telegram/inbound.ts src/telegram/inbound.test.ts
|
||||
git commit -m "Add buildReplyHeader helper with truncation and media descriptors"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Compose the reply header into `normalizeMessage` output
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/telegram/inbound.ts`
|
||||
- Modify: `src/telegram/inbound.test.ts`
|
||||
|
||||
- [ ] **Step 1: Add failing integration tests to `normalizeMessage`**
|
||||
|
||||
Insert new test cases inside the existing `describe("normalizeMessage", ...)` block in `src/telegram/inbound.test.ts`, after the last existing case (the "labels forwarded photos even when there is no caption" test) but before the block closes:
|
||||
|
||||
```ts
|
||||
it("prepends a reply header when the user replies to a bot message", async () => {
|
||||
const r = await normalizeMessage(
|
||||
{
|
||||
text: "workspace/foo.csv",
|
||||
reply_to_message: { from: { id: 42, is_bot: true }, text: "Which file did you mean?" },
|
||||
},
|
||||
deps({ botUserId: 42 }),
|
||||
);
|
||||
expect(r.input?.text).toBe('[Reply to your message: "Which file did you mean?"]\nworkspace/foo.csv');
|
||||
});
|
||||
|
||||
it("uses 'your own earlier message' when replying to the owner's own message", async () => {
|
||||
const r = await normalizeMessage(
|
||||
{
|
||||
text: "still true",
|
||||
reply_to_message: { from: { id: 99, is_bot: false }, text: "the dog's name is Rex" },
|
||||
},
|
||||
deps({ botUserId: 42 }),
|
||||
);
|
||||
expect(r.input?.text).toBe('[Reply to your own earlier message: "the dog\'s name is Rex"]\nstill true');
|
||||
});
|
||||
|
||||
it("uses the quote wording and text on reply-with-quote", async () => {
|
||||
const r = await normalizeMessage(
|
||||
{
|
||||
text: "what does this mean?",
|
||||
reply_to_message: { from: { id: 42, is_bot: true }, text: "long assistant explanation" },
|
||||
quote: { text: "geothermal power plant" },
|
||||
},
|
||||
deps({ botUserId: 42 }),
|
||||
);
|
||||
expect(r.input?.text).toBe('[Reply quoting your message: "geothermal power plant"]\nwhat does this mean?');
|
||||
});
|
||||
|
||||
it("puts the reply header before the forward header when both are present", async () => {
|
||||
const r = await normalizeMessage(
|
||||
{
|
||||
text: "this one",
|
||||
reply_to_message: { from: { id: 42, is_bot: true }, text: "which article?" },
|
||||
forward_origin: { type: "channel", chat: { title: "Daily News" } },
|
||||
},
|
||||
deps({ botUserId: 42 }),
|
||||
);
|
||||
expect(r.input?.text).toBe('[Reply to your message: "which article?"]\n[Forwarded from Daily News]\nthis one');
|
||||
});
|
||||
|
||||
it("keeps the reply header on a photo-only forwarded reply", async () => {
|
||||
const r = await normalizeMessage(
|
||||
{
|
||||
photo: [{ file_id: "p2", width: 1, height: 1 }],
|
||||
reply_to_message: { from: { id: 42, is_bot: true }, text: "which of these?" },
|
||||
forward_origin: { type: "user", sender_user: { first_name: "Alice" } },
|
||||
},
|
||||
deps({ botUserId: 42 }),
|
||||
);
|
||||
expect(r.input?.text).toBe(
|
||||
'[Reply to your message: "which of these?"]\n[Forwarded from Alice]\n(photo attached)',
|
||||
);
|
||||
});
|
||||
|
||||
it("adds a reply header on a photo-only reply with no body text", async () => {
|
||||
const r = await normalizeMessage(
|
||||
{
|
||||
photo: [{ file_id: "p2", width: 1, height: 1 }],
|
||||
reply_to_message: { from: { id: 42, is_bot: true }, text: "which of these?" },
|
||||
},
|
||||
deps({ botUserId: 42 }),
|
||||
);
|
||||
expect(r.input?.text).toBe('[Reply to your message: "which of these?"]\n(photo attached)');
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the tests to confirm they fail**
|
||||
|
||||
Run: `npx vitest run src/telegram/inbound.test.ts`
|
||||
Expected: FAIL on the new `normalizeMessage` cases — output still equals the body without the header, e.g. actual `"workspace/foo.csv"` vs expected `"[Reply to your message: \"Which file did you mean?\"]\nworkspace/foo.csv"`.
|
||||
|
||||
- [ ] **Step 3: Wire the header into `normalizeMessage`**
|
||||
|
||||
Modify the tail of `normalizeMessage` in `src/telegram/inbound.ts`. Locate the existing forward-header block:
|
||||
|
||||
```ts
|
||||
if (parts.length === 0 && images.length > 0) parts.push("(photo attached)");
|
||||
let text = parts.join("\n\n");
|
||||
if (msg.forward_origin && (text || images.length > 0)) {
|
||||
const label = describeForwardOrigin(msg.forward_origin);
|
||||
text = text ? `[Forwarded from ${label}]\n${text}` : `[Forwarded from ${label}]`;
|
||||
}
|
||||
return {
|
||||
input: text ? { text, images: images.length > 0 ? images : undefined } : null,
|
||||
transcript,
|
||||
notes,
|
||||
};
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```ts
|
||||
if (parts.length === 0 && images.length > 0) parts.push("(photo attached)");
|
||||
let text = parts.join("\n\n");
|
||||
if (msg.forward_origin && (text || images.length > 0)) {
|
||||
const label = describeForwardOrigin(msg.forward_origin);
|
||||
text = text ? `[Forwarded from ${label}]\n${text}` : `[Forwarded from ${label}]`;
|
||||
}
|
||||
const replyHeader = buildReplyHeader(msg.reply_to_message, msg.quote, deps.botUserId);
|
||||
if (replyHeader && (text || images.length > 0)) {
|
||||
text = text ? `${replyHeader}\n${text}` : replyHeader;
|
||||
}
|
||||
return {
|
||||
input: text ? { text, images: images.length > 0 ? images : undefined } : null,
|
||||
transcript,
|
||||
notes,
|
||||
};
|
||||
```
|
||||
|
||||
The reply header is prepended after the forward header is prepended — so when both are set, the string ends up `[Reply ...]\n[Forwarded ...]\n<body>`, which matches the ordering the tests expect.
|
||||
|
||||
- [ ] **Step 4: Run the tests to confirm they pass**
|
||||
|
||||
Run: `npx vitest run src/telegram/inbound.test.ts`
|
||||
Expected: PASS — all new `normalizeMessage` cases green, plus every existing case still green.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/telegram/inbound.ts src/telegram/inbound.test.ts
|
||||
git commit -m "Prepend reply header ahead of forward header in normalizeMessage"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Thread `botUserId` from `bridge.ts`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/telegram/bridge.ts`
|
||||
|
||||
- [ ] **Step 1: Pass `bot.botInfo?.id` into the deps**
|
||||
|
||||
Locate the `bot.on("message", ...)` handler in `src/telegram/bridge.ts` (around `bridge.ts:110-129`). Modify the `normalizeMessage` call:
|
||||
|
||||
```ts
|
||||
const result = await normalizeMessage(ctx.message, {
|
||||
download: (fileId) => downloadFile(bot, config.telegramBotToken, fileId, deps.fetchFn),
|
||||
transcriber: deps.transcriber,
|
||||
workspaceDir: config.workspaceDir,
|
||||
botUserId: bot.botInfo?.id,
|
||||
});
|
||||
```
|
||||
|
||||
`bot.botInfo` is populated by grammy once `bot.init()` (or `bot.start()`) has run. By the time this handler fires, both are done. The optional chaining plus the `is_bot` fallback in `buildReplyHeader` keep things safe if somehow it isn't.
|
||||
|
||||
- [ ] **Step 2: Typecheck**
|
||||
|
||||
Run: `npm run typecheck`
|
||||
Expected: exits 0.
|
||||
|
||||
- [ ] **Step 3: Run the full test suite**
|
||||
|
||||
Run: `npm test`
|
||||
Expected: all tests pass, including the ones added in Tasks 2 and 3.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add src/telegram/bridge.ts
|
||||
git commit -m "Pass bot user id into normalizeMessage for reply labelling"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: README updates
|
||||
|
||||
**Files:**
|
||||
- Modify: `README.md`
|
||||
|
||||
- [ ] **Step 1: Add the "Talking to it" bullet**
|
||||
|
||||
In `README.md`, find the "Talking to it" section (around README.md:17-25). After the paragraph starting "It can also send files back", add:
|
||||
|
||||
```markdown
|
||||
Reply to any of my messages (or "reply with quote" a specific part) and I'll see what you're answering to — useful for coming back to something from earlier in the day.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add a manual E2E item**
|
||||
|
||||
At the end of the "Manual end-to-end checklist" list, after item 18, add:
|
||||
|
||||
```markdown
|
||||
19. Reply to one of the bot's earlier messages with a follow-up like "what did you mean by that?" → the answer references that specific message, not the most recent one. Then "reply with quote" a specific phrase from a bot message → the answer references the quoted phrase.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add README.md
|
||||
git commit -m "Document reply / reply-with-quote support in the README"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Self-review notes (for the executor)
|
||||
|
||||
- Every task above ends in a green test run or a typecheck plus a commit — no batching.
|
||||
- The types added in Task 1 (`RepliedMessage`, `Quote`) are the exact same shapes the tests in Task 2 rely on. The `botUserId` field type on `InboundDeps` matches the value we thread through in Task 4 (`bot.botInfo?.id` returns `number | undefined`).
|
||||
- The header helper name is stable across all tasks (`buildReplyHeader`). No aliasing.
|
||||
- Truncation constant is a single exported symbol (`MAX_REPLY_CONTEXT_CHARS`) referenced from one place; tests use the numeric value directly, so a future change to the constant only requires updating the corresponding assertions.
|
||||
- Ordering: forward header is prepended first, reply header second, resulting in the `[Reply ...]\n[Forwarded ...]\n<body>` order the tests expect.
|
||||
Reference in New Issue
Block a user