# Draft a LinkedIn post and visual, then send both to ClickUp for review

> Check approved facts, draft a LinkedIn post with Claude Code, generate a Nano Banana 2.1 visual, and attach the actual PNG to a ClickUp task for human review.

By BIGCHANGE.AI Editorial

Published: 2026-10-26T08:08:38.238Z
Updated: 2026-10-26T08:08:38.238Z
Canonical: https://bigchange.ai/blog/linkedin-post-image-clickup-review-guide

![Reviewer holds tablet showing unreadable draft lines beside abstract orange image.](https://bigchange.ai/api/media/file/linkedin-clickup-human-review-hero-v3.png)
AI-generated conceptual illustration by BIG CHANGE.

An approved topic can become a LinkedIn draft and visual before anyone publishes it. This guide gives a social editor a reproducible handoff: check the owner’s facts, have Claude Code draft one post, generate a PNG with Google’s current Nano Banana 2.1 image model, then put both into a ClickUp task for a named reviewer. The task is the endpoint. No step posts to LinkedIn. The procedure follows official documentation checked on 26 October 2026; BIG CHANGE ran local syntax checks on the reference files, but did not run either model or use a ClickUp account.

## The big change

- **What changed:** A single reviewed fact list can drive a post draft and a visual, with their exact file hashes carried into a ClickUp review handoff.
- **Why it matters:** A social editor can give a named reviewer the actual PNG, post text and source links in one task. The reviewer still has to inspect every claim and image detail before any publishing decision.
- **What to watch:** The handoff is complete only after the task’s List, status, assignee, visibility, description and attachment have been checked in ClickUp. An API filename readback alone cannot establish that the reviewer saw the approved image bytes.

## 1. Check the facts, rights and accounts

Start with a real, owner-approved topic. For each fact you might use, record its exact wording, primary-source URL, UTC check time and permission to reuse it in both a LinkedIn post and generated visual. Confirm that the owner permits sending those facts to Anthropic and Google under the accounts’ applicable data terms. Keep confidential or unlicensed material out of the input. This article supplies templates with placeholders, not a sample campaign that should be published.

You need Python **3.10 or newer**, [Claude Code’s current CLI](https://code.claude.com/docs/en/headless) with API authentication for `--bare`, and the current `google-genai` and `requests` Python packages. As checked on 26 October 2026, [`google-genai` 2.33.0](https://pypi.org/project/google-genai/2.33.0/) and [`requests` 2.34.2](https://pypi.org/project/requests/2.34.2/) both require Python 3.10 or newer; check the metadata again when installing a later release. Install the packages in an owned virtual environment, using your organization’s approved installation process:

```powershell
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade google-genai requests
```

Confirm that `claude --version` and `python --version` run in that environment. Set `ANTHROPIC_API_KEY`, `GEMINI_API_KEY` and, before handoff, `CLICKUP_TOKEN` in the private process environment according to your organization’s secret-handling procedure. Never paste keys into the three reference files or a shared task. The ClickUp token must belong to a user with access to the intended List. ClickUp documents [personal tokens for personal use and OAuth for shared integrations](https://developer.clickup.com/docs/authentication). Get the exact numeric List ID, reviewer user ID, reviewer name and the List’s review-status spelling from that account. Confirm that the reviewer can see the List and that its visibility is appropriate for the source links and image. If its task creation requires custom fields the script cannot provide, stop and have the List owner define an authorized creation procedure.

Check the account’s Claude model, usage and spend controls and [Google’s live image price and quota](https://ai.google.dev/gemini-api/docs/pricing) before `prepare`. Google lists a standard Nano Banana 2.1 1K image equivalent at **$0.0336 per image**, plus input and text-output charges. A request can produce costs and responses that differ by account and settings. Claude Code’s `total_cost_usd` in JSON is an estimate; this guide makes no timed-run or savings claim. Check ClickUp plan, API and storage limits before handoff. The reference code has no explicit retry loop for ClickUp writes after a 429 or uncertain result; a failed model preparation can still incur usage, including any provider or CLI internal retries.

## 2. Save the three reference files

Save the next three complete blocks as `input-template.json`, `human-checked.template.json` and `workflow.py` in one local folder. Keep the filled input, run directory, model response and API token private. The templates are intentionally incomplete. A SHA-256 is a fingerprint of exact file bytes; matching hashes establish that the files did not change between the recorded check and handoff, but they do not establish factual accuracy or reuse rights.

`input-template.json`:

```json
{
  "topic": "REPLACE_WITH_ACTUALLY_APPROVED_TOPIC",
  "reader": "REPLACE_WITH_INTENDED_LINKEDIN_READER",
  "approved_claims": [
    {
      "id": "REPLACE_WITH_UNIQUE_FACT_ID",
      "text": "REPLACE_WITH_EXACT_CHECKED_FACT",
      "source_url": "https://REPLACE_WITH_PRIMARY_SOURCE",
      "checked_at_utc": "REPLACE_WITH_ACTUAL_UTC_CHECK_TIME",
      "reuse_rights": "REPLACE_WITH_APPROVED_LINKEDIN_AND_IMAGE_USE"
    }
  ],
  "visual_style": "REPLACE_WITH_APPROVED_BRAND_PALETTE_AND_STYLE",
  "reviewer": "REPLACE_WITH_NAMED_HUMAN_REVIEWER",
  "clickup_list_id": "REPLACE_WITH_AUTHORIZED_NUMERIC_LIST_ID",
  "clickup_reviewer_user_id": "REPLACE_WITH_REVIEWER_NUMERIC_USER_ID",
  "review_status": "REPLACE_WITH_EXACT_LIST_REVIEW_STATUS",
  "owner_approved_for_linkedin": false
}
```

`human-checked.template.json`:

```json
{
  "reviewer": "REPLACE_WITH_NAMED_PERSON",
  "reviewed_at_utc": "REPLACE_WITH_ACTUAL_TIME",
  "post_sha256": "REPLACE_WITH_SHA256_OF_POST_MD",
  "image_sha256": "REPLACE_WITH_SHA256_OF_VISUAL_PNG",
  "fact_source_rights_visual_review": "REPLACE_WITH_DOCUMENTED_DECISION",
  "approved_for_clickup_review": false
}
```

`workflow.py`:

```python
"""Reader reference: approved facts -> Claude draft + Gemini image -> ClickUp review.

No calls run when imported. `check` is local only; `prepare` incurs model charges;
`handoff` writes to ClickUp. Use a private working directory outside this packet.
"""
import argparse
import base64
import hashlib
import json
import os
from pathlib import Path
import subprocess
import sys


IMAGE_MODEL = "gemini-nano-banana-2.1"
CLAUDE_SCHEMA = {
    "type": "object",
    "properties": {
        "post": {"type": "string"},
        "claim_ids": {"type": "array", "items": {"type": "string"}},
        "review_notes": {"type": "string"},
    },
    "required": ["post", "claim_ids", "review_notes"],
    "additionalProperties": False,
}


def sha(data):
    return hashlib.sha256(data).hexdigest()


def save_new(path, data):
    with path.open("xb") as out:
        out.write(data)


def load_input(path):
    data = json.loads(path.read_text(encoding="utf-8"))
    if "REPLACE_WITH" in json.dumps(data):
        raise ValueError("Replace every template placeholder with checked owned input")
    required = ("topic", "reader", "approved_claims", "visual_style", "reviewer",
                "clickup_list_id", "clickup_reviewer_user_id", "review_status", "owner_approved_for_linkedin")
    if any(not data.get(k) for k in required):
        raise ValueError("Fill every required field in the owned input")
    if data["owner_approved_for_linkedin"] is not True:
        raise ValueError("Owner must approve use of these facts for LinkedIn")
    claims = data["approved_claims"]
    if not isinstance(claims, list) or not claims:
        raise ValueError("approved_claims must contain real checked facts")
    ids = set()
    for claim in claims:
        if not all(claim.get(k) for k in ("id", "text", "source_url", "checked_at_utc", "reuse_rights")):
            raise ValueError("Every claim needs ID, exact fact, source, check time and rights")
        if not claim["source_url"].startswith("https://") or claim["id"] in ids:
            raise ValueError("Claim source URL or unique ID invalid")
        ids.add(claim["id"])
    if not str(data["clickup_list_id"]).isdigit():
        raise ValueError("Use an authorized numeric ClickUp List ID")
    if not str(data["clickup_reviewer_user_id"]).isdigit():
        raise ValueError("Use the intended reviewer's numeric ClickUp user ID")
    return data


def prepare(source, work):
    data = load_input(source)
    if work.exists() and any(work.iterdir()):
        raise ValueError("Use a new empty private work directory for each preparation")
    work.mkdir(parents=True, exist_ok=True)
    source_bytes = source.read_bytes()
    save_new(work / "input.sha256", (sha(source_bytes) + "\n").encode())
    facts = [{k: c[k] for k in ("id", "text", "source_url")} for c in data["approved_claims"]]
    prompt = (
        "Draft ONE LinkedIn post for the named reader from ONLY the approved facts below. "
        "Make the prose useful and natural. Never invent a scene, quote, number, "
        "result, date or capability. If a necessary fact is missing, say so in "
        "review_notes. Do not post, call tools, or claim approval. Return every "
        "material fact's ID in claim_ids. Topic and reader: "
        + json.dumps({"topic": data["topic"], "reader": data["reader"], "facts": facts}, ensure_ascii=False)
    )
    if not os.environ.get("ANTHROPIC_API_KEY"):
        raise ValueError("Claude bare mode needs ANTHROPIC_API_KEY in the reader's environment")
    result = subprocess.run(
        ["claude", "--bare", "--tools", "", "--disallowedTools", "mcp__*", "-p", prompt, "--output-format", "json",
         "--json-schema", json.dumps(CLAUDE_SCHEMA, separators=(",", ":"))],
        text=True, capture_output=True, check=False, timeout=180,
    )
    save_new(work / "claude-response.json", result.stdout.encode("utf-8"))
    save_new(work / "claude-stderr.txt", result.stderr.encode("utf-8"))
    if result.returncode:
        raise RuntimeError("Claude failed; inspect authentication and stderr locally; no ClickUp write occurred")
    envelope = json.loads(result.stdout)
    draft = envelope.get("structured_output")
    if not draft or not draft.get("post") or not set(draft["claim_ids"]).issubset({x["id"] for x in facts}):
        raise ValueError("Claude returned no valid draft or an unknown claim ID")
    save_new(work / "post.md", (draft["post"].strip() + "\n").encode("utf-8"))
    save_new(work / "draft-result.json", json.dumps({
        "claim_ids": draft["claim_ids"], "review_notes": draft["review_notes"],
        "claude_estimated_cost_usd": envelope.get("total_cost_usd"),
        "source_sha256": sha(source_bytes),
    }, indent=2).encode("utf-8"))
    # Google is for the graphic only. It is not a factual source for the post.
    from google import genai
    if not os.environ.get("GEMINI_API_KEY"):
        raise ValueError("Set GEMINI_API_KEY before image generation")
    visual_facts = [c for c in facts if c["id"] in draft["claim_ids"]]
    visual_prompt = (
        "Create a single clear LinkedIn infographic or visual summary. Use only "
        "these verified facts; omit any detail you cannot render accurately. "
        "Do not depict real people, logos, charts, metrics or quotations unless "
        "explicitly present in the facts and permitted. Style: " + data["visual_style"]
        + "\nFacts: " + json.dumps(visual_facts, ensure_ascii=False)
    )
    interaction = genai.Client().interactions.create(
        model=IMAGE_MODEL, input=visual_prompt,
        response_format={"type": "image", "mime_type": "image/png", "aspect_ratio": "4:5", "image_size": "1K"},
    )
    if not interaction.output_image or not interaction.output_image.data:
        raise ValueError("Image generation returned no image; keep draft local")
    image = base64.b64decode(interaction.output_image.data, validate=True)
    if not image.startswith(b"\x89PNG\r\n\x1a\n"):
        raise ValueError("Expected PNG bytes; inspect model response")
    save_new(work / "visual.png", image)
    save_new(work / "image-result.json", json.dumps({"model": IMAGE_MODEL,
        "prompt": visual_prompt, "image_sha256": sha(image), "source_sha256": sha(source_bytes)},
        indent=2).encode("utf-8"))
    print("Prepared post.md and visual.png. Human must inspect every fact and image detail before handoff.")


def handoff(source, work):
    import requests
    data = load_input(source)
    digest = sha(source.read_bytes())
    if (work / "input.sha256").read_text().strip() != digest:
        raise ValueError("Owned facts changed; prepare a new run")
    draft = json.loads((work / "draft-result.json").read_text())
    visual = json.loads((work / "image-result.json").read_text())
    image = (work / "visual.png").read_bytes()
    if len(image) > 1_000_000_000:
        raise ValueError("Image exceeds ClickUp's documented 1 GB attachment limit")
    if draft["source_sha256"] != digest or visual["source_sha256"] != digest or sha(image) != visual["image_sha256"]:
        raise ValueError("Draft/image/source hashes differ")
    if not (work / "human-checked.json").exists():
        raise ValueError("Human reviewer must save human-checked.json before handoff")
    checked = json.loads((work / "human-checked.json").read_text())
    if checked.get("post_sha256") != sha((work / "post.md").read_bytes()) or checked.get("image_sha256") != sha(image) or checked.get("approved_for_clickup_review") is not True:
        raise ValueError("Human checked hashes or handoff decision do not match")
    if any(not checked.get(k) or "REPLACE_WITH" in str(checked[k]) for k in ("reviewer", "reviewed_at_utc", "fact_source_rights_visual_review")):
        raise ValueError("Record the actual human, check time and decision")
    token = os.environ.get("CLICKUP_TOKEN")
    if not token:
        raise ValueError("Set CLICKUP_TOKEN in the reader's environment")
    headers = {"Authorization": token}
    api = "https://api.clickup.com/api/v2"
    receipt_path = work / "clickup-receipt.json"
    if (work / "create-uncertain.txt").exists() and not receipt_path.exists():
        raise RuntimeError("Task creation uncertain; inspect the ClickUp List and reconcile before any retry")
    if receipt_path.exists():
        receipt = json.loads(receipt_path.read_text())
        if receipt.get("source_sha256") != digest:
            raise ValueError("Existing task receipt belongs to different input; reconcile manually")
        task_id = receipt["task_id"]
    else:
        # A timeout after POST is uncertain. Stop and reconcile the List before another create.
        claim_map = "\n".join(f"{c['id']}: {c['source_url']}" for c in data["approved_claims"]
                            if c["id"] in draft["claim_ids"])
        description = ("Human review required. Check the approved fact sources, the exact post, "
                       "and every word/pixel of the attached image. No LinkedIn posting is authorized.\n\n"
                       + (work / "post.md").read_text(encoding="utf-8")
                       + "\nClaim source URLs:\n" + claim_map + "\nSource SHA-256: " + digest)
        save_new(work / "create-uncertain.txt", b"Inspect ClickUp List before retrying creation.\n")
        try:
            response = requests.post(f"{api}/list/{data['clickup_list_id']}/task",
                headers={**headers, "Content-Type": "application/json"},
                json={"name": "Review LinkedIn draft: " + data["topic"], "description": description,
                      "status": data["review_status"], "assignees": [int(data["clickup_reviewer_user_id"])],
                      "check_required_custom_fields": True}, timeout=30)
            response.raise_for_status()
            task_id = response.json()["id"]
        except Exception:
            # The pre-write marker already exists; preserve the original failure.
            raise
        receipt = {"task_id": task_id, "source_sha256": digest, "attachment_verified": False}
        save_new(receipt_path, json.dumps(receipt, indent=2).encode())
        (work / "create-uncertain.txt").unlink()
    task = requests.get(f"{api}/task/{task_id}", headers=headers, timeout=30)
    task.raise_for_status()
    attachments = task.json().get("attachments") or []
    if not any(a.get("title") == "visual.png" for a in attachments):
        if (work / "attachment-uncertain.txt").exists():
            raise RuntimeError("Attachment result uncertain; inspect task and reconcile manually")
        save_new(work / "attachment-uncertain.txt", b"Inspect task attachments before retrying.\n")
        try:
            with (work / "visual.png").open("rb") as handle:
                upload = requests.post(f"{api}/task/{task_id}/attachment", headers=headers,
                    files={"attachment[0]": ("visual.png", handle, "image/png")}, timeout=120)
                upload.raise_for_status()
        except Exception:
            # Preserve the marker even if the process is interrupted during POST.
            raise
    task = requests.get(f"{api}/task/{task_id}", headers=headers, timeout=30)
    task.raise_for_status()
    body = task.json()
    if not any(a.get("title") == "visual.png" for a in (body.get("attachments") or [])):
        raise RuntimeError("ClickUp did not read back the visual attachment")
    if not any(str(a.get("id")) == str(data["clickup_reviewer_user_id"]) for a in (body.get("assignees") or [])):
        raise RuntimeError("ClickUp did not read back the intended reviewer assignment")
    receipt["attachment_name_read_back"] = True
    receipt["attachment_verified"] = False
    receipt["human_task_and_file_inspection"] = "pending"
    receipt["task_url"] = body.get("url")
    receipt_path.write_text(json.dumps(receipt, indent=2), encoding="utf-8")
    print(json.dumps(receipt))


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("action", choices=["check", "prepare", "handoff"])
    parser.add_argument("input", type=Path)
    parser.add_argument("work", type=Path)
    args = parser.parse_args()
    if args.action == "check":
        load_input(args.input)
        print("Input fields present; facts and rights still require human verification.")
    elif args.action == "prepare":
        prepare(args.input, args.work)
    else:
        handoff(args.input, args.work)
```

## 3. Prepare a post and visual for inspection

Copy `input-template.json` to a private `linkedin-input.json` and replace every placeholder. Use one unique ID per approved claim. The `owner_approved_for_linkedin` field must be the literal JSON value `true` only after the owner has approved this use. Enter a named human reviewer, that person’s numeric ClickUp user ID, the authorized numeric List ID and its exact review status. Describe the intended LinkedIn reader and an approved visual style; the input is still subject to human fact and rights review.

Use a new, empty private run directory for each preparation. On Windows PowerShell, for example:

```powershell
python workflow.py check C:\private\linkedin-input.json C:\private\linkedin-run
python workflow.py prepare C:\private\linkedin-input.json C:\private\linkedin-run
```

`check` validates required fields, IDs and placeholders locally. It does not verify the claims or permissions. `prepare` sends the approved fact list to Claude Code in noninteractive bare mode, with built-in and MCP tools disabled by [documented CLI flags](https://code.claude.com/docs/en/cli-reference). It rejects claim IDs outside the input, saves `post.md` and the private Claude response, and uses `gemini-nano-banana-2.1` to save `visual.png`. Google’s [image manual](https://ai.google.dev/gemini-api/docs/image-generation) shows this model and the `interactions.create` image response used in the reference code. Its [model lifecycle](https://ai.google.dev/gemini-api/docs/deprecations) lists Imagen 3’s November 2025 shutdown; the discovered older Imagen 3 instruction is no longer a runnable step.

Open `post.md` and compare every factual sentence with the approved source, including context and wording. Then open `visual.png` at its actual resolution. Check every word, number, depiction, logo and crop against the approved facts and rights. The image is an output to review, not evidence for a claim. If either file needs a change, retain the run for diagnosis, fix the input or approved prompt, and prepare in a fresh empty directory. Repeating a preparation can incur model charges. A failed or timed-out model call is not a reason to create a ClickUp task without both files.

## 4. Approve the exact files for ClickUp review

When a human has inspected both files, copy `human-checked.template.json` into the run directory as `human-checked.json`. Enter the actual reviewer, UTC time and a brief fact, source, rights and visual decision. Use the exact SHA-256 values printed by:

```powershell
(Get-FileHash C:\private\linkedin-run\post.md -Algorithm SHA256).Hash.ToLowerInvariant()
(Get-FileHash C:\private\linkedin-run\visual.png -Algorithm SHA256).Hash.ToLowerInvariant()
```

Set `approved_for_clickup_review` to the literal JSON value `true` only for this review handoff. It does not approve a LinkedIn publication. If private or disallowed text remains in either file, remove it through a fresh preparation and approval with new hashes.

## 5. Create the review task and inspect it

With the intended ClickUp token in the private environment, run:

```powershell
python workflow.py handoff C:\private\linkedin-input.json C:\private\linkedin-run
```

The script creates a task in the specified List and review status, assigns the numeric reviewer, includes the exact post and claim source URLs in the description, and uploads the actual PNG bytes as multipart `attachment[0]`. [ClickUp’s attachment guide](https://developer.clickup.com/docs/attachments) specifies that field and a 1 GB file limit, subject to account storage. The script GETs the task and records its ID and URL, an assignee readback and an attachment **name** readback. Its receipt leaves `attachment_verified` false and `human_task_and_file_inspection` pending for a reason: a matching filename cannot prove byte identity or that the correct reviewer saw the file.

Open the saved task in ClickUp. Check the home List, exact review status, named assignee and actual visibility. Compare its description with `post.md` and the source URLs. Open the attachment and compare it with the locally approved `visual.png`; then record accept, revise or reject in that task and in your private receipt. A human reviewer may request a revision without approving any LinkedIn post.

If task creation times out or returns an uncertain result, keep `create-uncertain.txt`. Search the exact List, including closed tasks and additional pages, for the intended title and source hash; [ClickUp’s List endpoint](https://developer.clickup.com/reference/gettasks) pages results. Reconcile a found task ID before further action. If the task exists but `clickup-receipt.json` was never saved, stop using the script for this run and complete the handoff manually in that task. Check its text, List, status, assignee and visibility; inspect existing attachments before deciding whether the approved PNG still needs uploading. Record the task ID, URL and human decision privately, and retain the uncertainty marker. Do not manufacture a script receipt or blindly create a second task. An uncertain upload similarly leaves `attachment-uncertain.txt`; GET the task, inspect its actual attachment and reconcile before another POST. The reference code deliberately does not clear that marker for you. A ClickUp 429 calls for waiting for its [rate-limit reset window](https://developer.clickup.com/docs/rate-limits); a storage 4xx calls for resolving capacity or reducing an owned asset. Neither failure is fixed by repeated submissions.

This is a documentation-led reference workflow. The local Python syntax and template structure were checked; no Claude Code draft, Gemini image, ClickUp attachment or account readback was produced for this article. For related tasks, our [three-channel Claude Code guide](https://bigchange.ai/blog/claude-code-three-channel-skill-pipeline-guide) covers draft coordination, while the [social visual calendar guide](https://bigchange.ai/blog/ai-social-carousels-quote-cards-30-day-calendar-guide) covers Adobe Express assets. This procedure’s completion point is the reviewed ClickUp task containing a post and actual PNG attachment.

## Sources

- [Google Gen AI Python SDK package metadata](https://pypi.org/project/google-genai/2.33.0/) — Maintainer project metadata requires Python >=3.10 for current google-genai; checked 26 October 2026.
- [Requests Python package metadata](https://pypi.org/project/requests/2.34.2/) — Maintainer project metadata requires Python >=3.10 for current requests; checked 26 October 2026.
- [Google Gemini image generation](https://ai.google.dev/gemini-api/docs/image-generation) — Current Nano Banana 2.1 model and Python interactions image response; checked 26 October 2026. No generation run.
- [Google model lifecycle](https://ai.google.dev/gemini-api/docs/deprecations) — Imagen 3 shutdown and current replacement listing; checked 26 October 2026.
- [Google API pricing](https://ai.google.dev/gemini-api/docs/pricing) — Standard Nano Banana 2.1 1K image equivalent and other charge components; checked 26 October 2026. Account bill unobserved.
- [Claude Code programmatic mode](https://code.claude.com/docs/en/headless) — Noninteractive output, bare mode, structured JSON and estimated cost; checked 26 October 2026. No model run.
- [Claude Code CLI reference](https://code.claude.com/docs/en/cli-reference) — Tool-disabling and JSON-schema flags in the public reference file; checked 26 October 2026.
- [ClickUp Create Task](https://developer.clickup.com/reference/createtask) — Task creation parameters, status, assignee and required custom field gate; checked 26 October 2026. No account write.
- [ClickUp attachments](https://developer.clickup.com/docs/attachments) — Multipart attachment[0], size and storage limits; checked 26 October 2026.
- [ClickUp Get Task](https://developer.clickup.com/reference/gettask) — Task readback can include attachments; checked 26 October 2026. No account readback.
- [ClickUp authentication](https://developer.clickup.com/docs/authentication) — Personal token and OAuth routes, and Authorization header; checked 26 October 2026.
- [ClickUp rate limits](https://developer.clickup.com/docs/rate-limits) — Plan-dependent per-token rate and 429 reset headers; checked 26 October 2026.
- [ClickUp Get Tasks in List](https://developer.clickup.com/reference/gettasks) — Closed-task inclusion and pagination for uncertain-create reconciliation; checked 26 October 2026.
