From b8292edf1035f4615fcaa9e0064be01fd94eaeda Mon Sep 17 00:00:00 2001 From: Self-Managing Codebase Manager <7004983+WillTaylor22@users.noreply.github.com> Date: Tue, 26 May 2026 10:06:49 +0000 Subject: [PATCH 1/3] ENG-25: webhook accepts plain-text session-id marker (MCP-survivable) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The convention previously required `` as an HTML comment in the PR body. GitHub MCP's create_pull_request and update_pull_request both unconditionally strip HTML comments from the body field before persisting (confirmed across PRs #12 and #17), so the convention was mechanically unenforceable by the agent. Fix (ENG-25 option 2): the canonical marker shape is now a plain-text line: session-id: sesn_xxxxxxxxxxxxxxxx which survives MCP. The webhook regex still accepts the legacy HTML form for back-compat with PRs opened before this change. Changes: - app/api/github-webhook/route.ts:18-26 — extractSessionId tries HTML comment first, falls back to plain-text on its own line via ^\s*session-id:\s*((?:sthr_|sesn_)[A-Za-z0-9]+)\s*$ with the /m flag. Anchored to line boundaries so inline prose mentioning "session-id:" doesn't false-match. - .claude/memory/conventions/pr-session-id-marker.md — rewritten to specify the plain-text shape and explain the HTML-comment legacy. Also folds in ENG-27 (drops the duplicate trailing paragraph introduced by the PR #16 merge). - .claude/memory/learnings/2026-05-26-github-mcp-strips-html-comments.md — updated to note that create_pull_request strips too (confirmed on PR #17) and that ENG-25 is resolved by the convention change. - .claude/memory/MEMORY.md — index entries updated. Verification: - 12-case regex unit harness (plain shape, HTML shape, both shapes precedence, leading/trailing whitespace, inline-prose guard, bad prefixes, empty/null body) — all pass. - npm run lint — clean. - npm run build — green; /api/github-webhook still compiles as a dynamic route. - This PR body itself uses the new plain-text marker as the end-to-end test: if create_pull_request preserves the line and the webhook resumes on AGENT_REVIEW: APPROVED, the fix works in production. --- .claude/memory/MEMORY.md | 4 +-- .../conventions/pr-session-id-marker.md | 32 +++++++++++-------- ...6-05-26-github-mcp-strips-html-comments.md | 2 +- app/api/github-webhook/route.ts | 10 ++++-- 4 files changed, 29 insertions(+), 19 deletions(-) diff --git a/.claude/memory/MEMORY.md b/.claude/memory/MEMORY.md index 9f678d0..22f6078 100644 --- a/.claude/memory/MEMORY.md +++ b/.claude/memory/MEMORY.md @@ -7,13 +7,13 @@ Keep this file under 200 lines — anything longer is content bloat, not memory. - [learnings/sentry-mcp-no-comment-tool](learnings/2026-05-26-sentry-mcp-no-comment-tool.md) — Sentry MCP can't comment on issues; back-link from Linear only - [learnings/vercel-blocks-unknown-author-email](learnings/2026-05-26-vercel-blocks-unknown-author-email.md) — Vercel preview deploys block when commit author email has no GitHub account; use the noreply alias - [learnings/sandbox-cant-clone-private-repo](learnings/2026-05-26-sandbox-cant-clone-private-repo.md) — Don't `git clone` from sandbox bash; the `github_repository` resource is auth'd, raw `git clone` is not -- [learnings/github-mcp-strips-html-comments](learnings/2026-05-26-github-mcp-strips-html-comments.md) — `update_pull_request` silently strips `` from PR bodies; session-id marker can't be set by agent (ENG-25) +- [learnings/github-mcp-strips-html-comments](learnings/2026-05-26-github-mcp-strips-html-comments.md) — `create_pull_request` AND `update_pull_request` strip `` from PR bodies; ENG-25 resolved by moving the session-id marker to a plain-text line ## Decisions - [decisions/mcp-for-small-writes-checkout-for-big](decisions/2026-05-26-mcp-for-small-writes-checkout-for-big.md) — Single-file writes go through GitHub MCP; multi-file or test-needing changes use the mounted checkout + `git push` - [decisions/two-agent-builder-reviewer](decisions/2026-05-25-two-agent-builder-reviewer.md) — Separate agents for build vs. review so the reviewer reads diffs cold ## Conventions -- [conventions/pr-session-id-marker](conventions/pr-session-id-marker.md) — PR body MUST end with `` (or legacy `sthr_...`) so webhooks can resume +- [conventions/pr-session-id-marker](conventions/pr-session-id-marker.md) — PR body MUST end with plain-text `session-id: sesn_...` on its own line; legacy `` shape also matched but stripped by MCP (ENG-25) - [conventions/agent-review-marker](conventions/agent-review-marker.md) — Reviewer's verdict goes on the first line as `AGENT_REVIEW: APPROVED|REQUEST_CHANGES|ESCALATE — ` - [conventions/check-open-pr-before-ticket-pickup](conventions/check-open-pr-before-ticket-pickup.md) — Before branching for a Linear ticket, grep open PRs for the ticket ID; abort if one already exists (PR #15 vs #16 ENG-26 race) diff --git a/.claude/memory/conventions/pr-session-id-marker.md b/.claude/memory/conventions/pr-session-id-marker.md index 4bad662..2df86a9 100644 --- a/.claude/memory/conventions/pr-session-id-marker.md +++ b/.claude/memory/conventions/pr-session-id-marker.md @@ -1,10 +1,10 @@ # PR body session-id marker -Every PR opened by the manager MUST end with this HTML comment as the -**last line** of the PR body: +Every PR opened by the manager MUST end with a `session-id:` marker on +its own line as the last non-empty line of the PR body: ``` - +session-id: sesn_xxxxxxxxxxxxxxxx ``` The `/api/github-webhook` route extracts this marker when a webhook @@ -12,17 +12,21 @@ fires for the PR (e.g. `issue_comment.created` with `AGENT_REVIEW: APPROVED`). With it, the webhook resumes the original manager session — full implementation context, no re-explaining. -The webhook regex accepts both the legacy `sthr_` prefix and the -current `sesn_` prefix returned by `client.beta.sessions.create()`. -Use whichever prefix the kickoff `user.message` carries. +## Accepted shapes -Without it, the webhook falls back to creating a fresh session. The -fresh session loses all design rationale and re-derives everything. -Functional but wasteful. +The webhook regex accepts: -The kickoff `user.message` includes the actual session id. Substitute -it verbatim; don't paraphrase or omit. +- Plain-text line: `session-id: sesn_...` — **preferred**. Survives + the GitHub MCP `update_pull_request` and `create_pull_request` body + filters (ENG-25). Always use this on new PRs. +- Legacy HTML comment: `` — still + matched for back-compat with PRs opened before ENG-25 landed, but + the agent cannot write this shape on a new PR because MCP strips it + on body create AND update. Read-only acceptance. -The regex in `app/api/github-webhook/route.ts` accepts either -`sesn_` (current SDK prefix) or `sthr_` (legacy) — both are valid. -Use whatever the kickoff hands you. +Both `sesn_` (current SDK prefix) and `sthr_` (legacy) are accepted. +Use whatever the kickoff `user.message` carries, verbatim. + +Without the marker, the webhook falls back to creating a fresh +session. The fresh session loses all design rationale and re-derives +everything. Functional but wasteful. diff --git a/.claude/memory/learnings/2026-05-26-github-mcp-strips-html-comments.md b/.claude/memory/learnings/2026-05-26-github-mcp-strips-html-comments.md index 5e8fde0..d3015fc 100644 --- a/.claude/memory/learnings/2026-05-26-github-mcp-strips-html-comments.md +++ b/.claude/memory/learnings/2026-05-26-github-mcp-strips-html-comments.md @@ -1 +1 @@ -The GitHub MCP `update_pull_request` tool unconditionally strips `` HTML comments from the `body` field before persisting — the PATCH fires (other prose changes land, `updated_at` advances), but any HTML comment line silently disappears. Confirmed twice on PR #12 review-feedback round 2 with both `sesn_...` and `sthr_...` prefixes; the filter isn't gated on content. This makes the `pr-session-id-marker` convention unenforceable by the agent via the standard transport — there's no `GITHUB_TOKEN` in the sandbox env and `api.github.com` is outside the egress allowlist, so you can't route around MCP with a direct PATCH. Don't waste review rounds on it — see ENG-25 for the fix proposal and the open question of whether `create_pull_request` shares the same filter (worth opportunistic testing whenever you open a new PR). +The GitHub MCP `update_pull_request` AND `create_pull_request` tools both unconditionally strip `` HTML comments from the `body` field before persisting — the PATCH/POST fires (other prose changes land, `updated_at` advances), but any HTML comment line silently disappears. Confirmed across PR #12 review-feedback round 2 (`update_pull_request`, both `sesn_...` and `sthr_...` prefixes) and PR #17 initial open (`create_pull_request`, `sesn_...`). The filter is unconditional, not gated on content. ENG-25 resolved the convention problem by moving the `pr-session-id-marker` to a plain-text line shape that MCP doesn't filter; the webhook regex still accepts the legacy HTML form for back-compat with already-merged PRs. Don't write HTML-comment markers on new PRs — they'll vanish and waste review rounds. The underlying MCP body-filter bug is still upstream of us, but the convention now sidesteps it. diff --git a/app/api/github-webhook/route.ts b/app/api/github-webhook/route.ts index 15fc659..7c94886 100644 --- a/app/api/github-webhook/route.ts +++ b/app/api/github-webhook/route.ts @@ -17,8 +17,14 @@ function verifySignature(rawBody: string, sigHeader: string | null, secret: stri function extractSessionId(text: string | undefined | null): string | null { if (!text) return null; - const m = text.match(//); - return m?.[1] ?? null; + // Accept two shapes: + // 1. Plain-text line: session-id: sesn_xxxx (current — survives GitHub MCP body filter, ENG-25) + // 2. HTML comment: (legacy — MCP strips this on update, kept for back-compat) + // Both `sesn_` (current SDK) and `sthr_` (legacy) prefixes are accepted. + const html = text.match(//); + if (html) return html[1]; + const plain = text.match(/^\s*session-id:\s*((?:sthr_|sesn_)[A-Za-z0-9]+)\s*$/m); + return plain?.[1] ?? null; } async function fetchPrBody(prNumber: number, token: string): Promise { From 3520b8ae33e09bd359217b100edbd0dca1c7c305 Mon Sep 17 00:00:00 2001 From: Self-Managing Codebase Manager <7004983+WillTaylor22@users.noreply.github.com> Date: Tue, 26 May 2026 10:09:07 +0000 Subject: [PATCH 2/3] memory: note greedy-strip behavior surfaced during PR #20 creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MCP doesn't just strip well-formed comments — it strips greedily from any unclosed ` HTML comments from the `body` field before persisting — the PATCH/POST fires (other prose changes land, `updated_at` advances), but any HTML comment line silently disappears. Confirmed across PR #12 review-feedback round 2 (`update_pull_request`, both `sesn_...` and `sthr_...` prefixes) and PR #17 initial open (`create_pull_request`, `sesn_...`). The filter is unconditional, not gated on content. ENG-25 resolved the convention problem by moving the `pr-session-id-marker` to a plain-text line shape that MCP doesn't filter; the webhook regex still accepts the legacy HTML form for back-compat with already-merged PRs. Don't write HTML-comment markers on new PRs — they'll vanish and waste review rounds. The underlying MCP body-filter bug is still upstream of us, but the convention now sidesteps it. +The GitHub MCP `update_pull_request` AND `create_pull_request` tools both unconditionally strip `` HTML comments from the `body` field before persisting — the PATCH/POST fires (other prose changes land, `updated_at` advances), but any HTML comment line silently disappears. Confirmed across PR #12 review-feedback round 2 (`update_pull_request`, both `sesn_...` and `sthr_...` prefixes) and PR #17 initial open (`create_pull_request`, `sesn_...`). The filter is unconditional, not gated on content. Worse: the strip is **greedy** — an unclosed `` in the same paragraph) causes the MCP to drop everything from that point to the end of the body. PR #20's first body version hit this and ate its own trailing session-id marker mid-PR-creation. Workaround in prose: refer to the token by phrase (\"HTML-comment opener\", \"the HTML-comment shape\") rather than as a literal substring. ENG-25 resolved the convention problem by moving the `pr-session-id-marker` to a plain-text line shape that MCP doesn't filter; the webhook regex still accepts the legacy HTML form for back-compat with already-merged PRs. Don't write HTML-comment markers on new PRs — they'll vanish and waste review rounds. From b7d06c492a87f7536dd329cdb4b3876321709a17 Mon Sep 17 00:00:00 2001 From: Self-Managing Codebase Manager <7004983+WillTaylor22@users.noreply.github.com> Date: Tue, 26 May 2026 10:17:07 +0000 Subject: [PATCH 3/3] ENG-25 round 2: last-match regex + unit-test guardrails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewer (PR #20 round 1) caught that String.match(/.../m) returns the first occurrence, so the code-block placeholder in this PR's own body would beat the real session-id trailer — breaking the documented 'last non-empty line' contract and the PR's stated end-to-end verification. Changes: - lib/extract-session-id.ts: extracted from route.ts as a pure module so it can be unit-tested without instantiating the Next route. Plain shape now uses matchAll(...) and takes the last match. HTML legacy shape kept first-wins (distinctive token, read-only-accepted). - app/api/github-webhook/route.ts: import the helper, drop the inline copy. - tests/unit/extract-session-id.spec.ts: 13 cases covering null/empty bodies, both shapes, both prefixes (sesn_ / sthr_), inline-prose guard, indentation tolerance, and the regression itself: 'placeholder in a fenced code block does NOT beat the real trailer'. - playwright.config.ts: testDir widened to ./tests, testMatch covers both e2e/ and unit/. Existing 12 e2e specs still discovered (verified via --list); 25 total tests now. - Memory: new learning at learnings/2026-05-26-regex-last-match-semantics.md on the /m vs /g + matchAll gotcha; MEMORY.md index updated. Verified: npm run lint, npm run build, npm run e2e -- tests/unit (13/13). --- .claude/memory/MEMORY.md | 1 + .../2026-05-26-regex-last-match-semantics.md | 18 ++++ app/api/github-webhook/route.ts | 13 +-- lib/extract-session-id.ts | 21 +++++ playwright.config.ts | 7 +- tests/unit/extract-session-id.spec.ts | 92 +++++++++++++++++++ 6 files changed, 139 insertions(+), 13 deletions(-) create mode 100644 .claude/memory/learnings/2026-05-26-regex-last-match-semantics.md create mode 100644 lib/extract-session-id.ts create mode 100644 tests/unit/extract-session-id.spec.ts diff --git a/.claude/memory/MEMORY.md b/.claude/memory/MEMORY.md index 22f6078..304968f 100644 --- a/.claude/memory/MEMORY.md +++ b/.claude/memory/MEMORY.md @@ -8,6 +8,7 @@ Keep this file under 200 lines — anything longer is content bloat, not memory. - [learnings/vercel-blocks-unknown-author-email](learnings/2026-05-26-vercel-blocks-unknown-author-email.md) — Vercel preview deploys block when commit author email has no GitHub account; use the noreply alias - [learnings/sandbox-cant-clone-private-repo](learnings/2026-05-26-sandbox-cant-clone-private-repo.md) — Don't `git clone` from sandbox bash; the `github_repository` resource is auth'd, raw `git clone` is not - [learnings/github-mcp-strips-html-comments](learnings/2026-05-26-github-mcp-strips-html-comments.md) — `create_pull_request` AND `update_pull_request` strip `` from PR bodies; ENG-25 resolved by moving the session-id marker to a plain-text line +- [learnings/regex-last-match-semantics](learnings/2026-05-26-regex-last-match-semantics.md) — `String.match(/.../m)` returns FIRST match; for "last line of body" use `matchAll(...).at(-1)` (PR #20 bug) ## Decisions - [decisions/mcp-for-small-writes-checkout-for-big](decisions/2026-05-26-mcp-for-small-writes-checkout-for-big.md) — Single-file writes go through GitHub MCP; multi-file or test-needing changes use the mounted checkout + `git push` diff --git a/.claude/memory/learnings/2026-05-26-regex-last-match-semantics.md b/.claude/memory/learnings/2026-05-26-regex-last-match-semantics.md new file mode 100644 index 0000000..5ef561c --- /dev/null +++ b/.claude/memory/learnings/2026-05-26-regex-last-match-semantics.md @@ -0,0 +1,18 @@ +# Regex "last match" needs `/g` + matchAll, not just `/m` + +`String.prototype.match(/.../m)` returns the **first** occurrence; the `m` +flag only line-anchors `^` / `$`. If your convention says "the marker is +the last line of the body" (e.g. `pr-session-id-marker`), you must take +the last match explicitly: + +```ts +const all = [...text.matchAll(/^\s*marker:\s*(\w+)\s*$/gm)]; +const value = all.length ? all[all.length - 1][1] : null; +``` + +PR #20 shipped the buggy form first and a docs-only code-block placeholder +(`marker: xxxxxxxxxxxxxxxx`) won against the real trailer, silently. The +unit test at `tests/unit/extract-session-id.spec.ts` now guards this case. + +General rule: any time the contract is "the LAST occurrence of X in a +multi-line body," reach for `matchAll(...).at(-1)`, not `match(... /m)`. diff --git a/app/api/github-webhook/route.ts b/app/api/github-webhook/route.ts index 7c94886..79df217 100644 --- a/app/api/github-webhook/route.ts +++ b/app/api/github-webhook/route.ts @@ -1,5 +1,6 @@ import Anthropic from '@anthropic-ai/sdk'; import { createHmac, timingSafeEqual } from 'node:crypto'; +import { extractSessionId } from '@/lib/extract-session-id'; export const runtime = 'nodejs'; export const maxDuration = 60; @@ -15,18 +16,6 @@ function verifySignature(rawBody: string, sigHeader: string | null, secret: stri return timingSafeEqual(a, b); } -function extractSessionId(text: string | undefined | null): string | null { - if (!text) return null; - // Accept two shapes: - // 1. Plain-text line: session-id: sesn_xxxx (current — survives GitHub MCP body filter, ENG-25) - // 2. HTML comment: (legacy — MCP strips this on update, kept for back-compat) - // Both `sesn_` (current SDK) and `sthr_` (legacy) prefixes are accepted. - const html = text.match(//); - if (html) return html[1]; - const plain = text.match(/^\s*session-id:\s*((?:sthr_|sesn_)[A-Za-z0-9]+)\s*$/m); - return plain?.[1] ?? null; -} - async function fetchPrBody(prNumber: number, token: string): Promise { const res = await fetch( `https://api.github.com/repos/WillTaylor22/self-managing-codebase/pulls/${prNumber}`, diff --git a/lib/extract-session-id.ts b/lib/extract-session-id.ts new file mode 100644 index 0000000..2df4fbb --- /dev/null +++ b/lib/extract-session-id.ts @@ -0,0 +1,21 @@ +// Pulled out of `app/api/github-webhook/route.ts` so it can be unit-tested +// without instantiating Next's route module. Pure, no I/O. +// +// Accepts two shapes (see .claude/memory/conventions/pr-session-id-marker.md): +// 1. Plain-text line: session-id: sesn_xxxx (preferred — MCP-survivable, ENG-25) +// 2. HTML comment: (legacy, read-only-accepted) +// +// Convention says the marker is the LAST non-empty line of the PR body. We +// honor that for the plain shape so a placeholder inside a fenced code block +// earlier in the body doesn't beat the real trailer (PR #20 review). +// +// The HTML shape is left first-wins: distinctive token, and we don't author +// it anymore so duplicate-in-prose risk is low. +export function extractSessionId(text: string | undefined | null): string | null { + if (!text) return null; + const html = text.match(//); + if (html) return html[1]; + const plain = [...text.matchAll(/^\s*session-id:\s*((?:sthr_|sesn_)[A-Za-z0-9]+)\s*$/gm)]; + if (plain.length) return plain[plain.length - 1][1]; + return null; +} diff --git a/playwright.config.ts b/playwright.config.ts index 8a2f65c..b50210f 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -3,7 +3,12 @@ import { defineConfig, devices } from '@playwright/test'; const baseURL = process.env.BASE_URL ?? 'http://localhost:3000'; export default defineConfig({ - testDir: './tests/e2e', + // Picks up both `tests/e2e/*.spec.ts` and `tests/unit/*.spec.ts`. + // Unit specs (e.g. extract-session-id) are pure-function tests that + // simply don't touch the `page` fixture; they still run under the + // chromium project but never spawn a browser context. + testDir: './tests', + testMatch: ['e2e/**/*.spec.ts', 'unit/**/*.spec.ts'], globalSetup: './tests/global-setup.ts', fullyParallel: true, forbidOnly: !!process.env.CI, diff --git a/tests/unit/extract-session-id.spec.ts b/tests/unit/extract-session-id.spec.ts new file mode 100644 index 0000000..78de6b1 --- /dev/null +++ b/tests/unit/extract-session-id.spec.ts @@ -0,0 +1,92 @@ +import { test, expect } from '@playwright/test'; +import { extractSessionId } from '../../lib/extract-session-id'; + +// Pure-function unit tests for the webhook's session-id marker extractor. +// No browser, no network — Playwright is just our test runner here. +// +// The shape contract is documented in +// .claude/memory/conventions/pr-session-id-marker.md. + +test.describe('extractSessionId', () => { + test('null body → null', () => { + expect(extractSessionId(null)).toBeNull(); + }); + + test('undefined body → null', () => { + expect(extractSessionId(undefined)).toBeNull(); + }); + + test('empty body → null', () => { + expect(extractSessionId('')).toBeNull(); + }); + + test('plain-text marker as the last line', () => { + const body = ['# Some PR', '', 'Body text.', '', 'session-id: sesn_abc123'].join('\n'); + expect(extractSessionId(body)).toBe('sesn_abc123'); + }); + + test('plain-text marker tolerates trailing whitespace / blank lines', () => { + const body = '# Some PR\n\nsession-id: sesn_abc123 \n\n\n'; + expect(extractSessionId(body)).toBe('sesn_abc123'); + }); + + test('sthr_ legacy prefix accepted on the plain shape', () => { + const body = 'Body.\n\nsession-id: sthr_legacy999'; + expect(extractSessionId(body)).toBe('sthr_legacy999'); + }); + + test('HTML-comment marker (legacy shape) still matches', () => { + const body = 'Body.\n\n'; + expect(extractSessionId(body)).toBe('sesn_html42'); + }); + + test('HTML shape wins over plain when both are present (legacy precedence)', () => { + // Documented behavior — legacy PRs may have both during transition. + const body = 'session-id: sesn_plain1\n\n'; + expect(extractSessionId(body)).toBe('sesn_html2'); + }); + + test('placeholder in a fenced code block does NOT beat the real trailer (PR #20)', () => { + // This is the exact regression: an earlier `session-id:` line that's + // documentation (e.g. inside a ```code``` block) used to win because + // .match without /g returns the first occurrence. The real marker is + // the last non-empty line; that must be what we return. + const body = [ + '## Fix', + '', + 'The canonical shape is now a plain-text line on its own:', + '', + '```', + 'session-id: sesn_xxxxxxxxxxxxxxxx', + '```', + '', + 'More prose here.', + '', + 'session-id: sesn_012j21sUvdmnhx3baX6ivYLW', + ].join('\n'); + expect(extractSessionId(body)).toBe('sesn_012j21sUvdmnhx3baX6ivYLW'); + }); + + test('multiple plain markers → last one wins', () => { + const body = 'session-id: sesn_first\nstuff\nsession-id: sesn_second\nmore\nsession-id: sesn_third'; + expect(extractSessionId(body)).toBe('sesn_third'); + }); + + test('inline mention of "session-id:" in prose does NOT match', () => { + // The marker must be on its own line. A sentence like "the session-id: foo line" + // should not produce a false match — note the `^\s* ... \s*$` anchors. + const body = 'In the body we mention the session-id: sesn_inline in prose. End.'; + expect(extractSessionId(body)).toBeNull(); + }); + + test('unknown prefix (not sthr_/sesn_) does not match', () => { + const body = 'session-id: zzzz_notvalid'; + expect(extractSessionId(body)).toBeNull(); + }); + + test('plain marker indented by leading whitespace is still matched', () => { + // Tolerated by `^\s*` — quoted-block / list-indented bodies still work. + const body = 'Body.\n\n session-id: sesn_indented'; + expect(extractSessionId(body)).toBe('sesn_indented'); + }); +});