diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc
new file mode 100644
index 0000000..c3502af
--- /dev/null
+++ b/.markdownlint-cli2.jsonc
@@ -0,0 +1,20 @@
+{
+ // markdownlint configuration for Draft Punks
+ // Docs: https://github.com/DavidAnson/markdownlint
+ "config": {
+ "default": true,
+ // Allow long lines (prose-heavy README, CHANGELOG)
+ "MD013": false,
+ // Allow duplicate headings in different sections
+ "MD024": { "siblings_only": true },
+ // Allow trailing punctuation in headings (emoji)
+ "MD026": false,
+ // Allow inline HTML (GitHub callouts, badges)
+ "MD033": false
+ },
+ // Skip archived docs and auto-generated review worksheets
+ "ignores": [
+ "docs/archive/**",
+ "docs/code-reviews/**"
+ ]
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 718b87c..c95d1c1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file.
### Added
+- **GraphQL Pagination**: Review thread fetch now paginates past the 100-thread limit, preventing false "merge ready" on large PRs.
+- **Markdownlint Pre-Commit Hook**: `scripts/hooks/pre-commit` runs `markdownlint-cli2` on staged `.md` files (excludes `docs/archive/` and `docs/code-reviews/`).
- **Doghouse Flight Recorder**: A new agent-native engine for PR state reconstruction.
- **CLI Subcommands**: `snapshot`, `watch`, `playback`, `export`.
- **Blocking Matrix**: Logic to distinguish merge conflicts from secondary blockers.
diff --git a/docs/code-reviews/PR6/2be9725f8f91d2df313e17e22bb1b403f88d6d1a.md b/docs/code-reviews/PR6/2be9725f8f91d2df313e17e22bb1b403f88d6d1a.md
new file mode 100644
index 0000000..6ad4842
--- /dev/null
+++ b/docs/code-reviews/PR6/2be9725f8f91d2df313e17e22bb1b403f88d6d1a.md
@@ -0,0 +1,492 @@
+---
+title: 2be9725f8f91d2df313e17e22bb1b403f88d6d1a.md
+description: Preserved review artifacts and rationale.
+audience: [contributors]
+domain: [quality]
+tags: [review]
+status: archive
+---
+
+# Code Review Feedback
+
+| Date | Agent | SHA | Branch | PR |
+|------|-------|-----|--------|----|
+| 2026-03-29 | CodeRabbit (and reviewers) | `2be9725f8f91d2df313e17e22bb1b403f88d6d1a` | [chore/backlog-cleanup](https://github.com/flyingrobots/draft-punks/tree/chore/backlog-cleanup "flyingrobots/draft-punks:chore/backlog-cleanup") | [PR#6](https://github.com/flyingrobots/draft-punks/pull/6) |
+
+## CODE REVIEW FEEDBACK
+
+### src/doghouse/adapters/github/gh_cli_adapter.py:96 β chatgpt-codex-connector[bot]
+
+```text
+** Pass null cursor for first GraphQL page**
+
+`fetch_blockers` always sends `-F cursor=` on the first GraphQL request, which makes `cursor` an empty string variable rather than a null/omitted cursor. GitHub cursor pagination expects the first page to be queried with `after: null` (or without `after`), so this can trigger invalid-cursor responses and silently drop unresolved-thread blockers, recreating false βmerge readyβ outcomes on affected PRs. Send `-F cursor=null` for the initial call, or only add the cursor variable after `pageInfo.endCursor` is available.
+
+Useful? React with πΒ / π.
+```
+
+_Meta_: https://github.com/flyingrobots/draft-punks/pull/6#discussion_r3006755607
+
+{response}
+
+### scripts/hooks/pre-commit:19 β coderabbitai[bot]
+
+```text
+_β οΈ Potential issue_ | _π Major_
+
+
+π§© Analysis chain
+
+π Script executed:
+
+```shell
+#!/usr/bin/env bash
+set -euo pipefail
+
+echo "Demonstrating current behavior (whitespace split):"
+printf '%s\n' 'docs/My Notes.md' | xargs -n1 printf '<%s>\n'
+
+echo
+echo "Demonstrating safe behavior (NUL-delimited):"
+printf '%s\0' 'docs/My Notes.md' | xargs -0 -n1 printf '<%s>\n'
+```
+
+Repository: flyingrobots/draft-punks
+
+Length of output: 178
+
+---
+
+π Script executed:
+
+```shell
+# Check the actual pre-commit hook file
+cat -n scripts/hooks/pre-commit
+```
+
+Repository: flyingrobots/draft-punks
+
+Length of output: 1169
+
+---
+
+π Script executed:
+
+```shell
+# Run a corrected verification to demonstrate xargs whitespace splitting behavior
+#!/bin/bash
+
+echo "=== Test 1: Default xargs with spaces (splits incorrectly) ==="
+# Simulate what happens when a filename contains spaces
+printf 'docs/My Notes.md\n' | xargs -n1 echo "Processing:"
+
+echo
+echo "=== Test 2: NUL-delimited (safe) ==="
+# Demonstrate safe handling with -0
+printf 'docs/My Notes.md\0' | xargs -0 -n1 echo "Processing:"
+
+echo
+echo "=== Test 3: Multiple files with spaces ==="
+printf 'docs/My Notes.md\ndocs/Another File.md\n' | xargs -n1 echo "File:"
+
+echo
+echo "=== Test 4: NUL-delimited with multiple files ==="
+printf 'docs/My Notes.md\0docs/Another File.md\0' | xargs -0 -n1 echo "File:"
+```
+
+Repository: flyingrobots/draft-punks
+
+Length of output: 445
+
+---
+
+
+
+
+
+**Major: filenames with spaces will catastrophically fail silentlyβthe xargs split is NOT negotiable.**
+
+The current code `echo "$STAGED_MD" | xargs markdownlint-cli2` splits on whitespace. Any staged markdown file containing spaces gets butchered: `docs/My Notes.md` becomes two arguments (`docs/My` and `Notes.md`), causing linting to skip or fail mysteriously.
+
+
+π§ Fix: end-to-end NUL-delimited pipeline
+
+```diff
+- STAGED_MD=$(git diff --cached --name-only --diff-filter=ACM -- '*.md' \
+- | grep -v '^docs/archive/' \
+- | grep -v '^docs/code-reviews/' \
+- || true)
+-
+- if [ -n "$STAGED_MD" ]; then
+- echo "$STAGED_MD" | xargs markdownlint-cli2
++ STAGED_MD=$(
++ git diff --cached --name-only -z --diff-filter=ACM -- '*.md' \
++ | tr '\0' '\n' \
++ | grep -v '^docs/archive/' \
++ | grep -v '^docs/code-reviews/' \
++ | tr '\n' '\0' || true
++ )
++
++ if [ -n "$STAGED_MD" ]; then
++ printf '%s' "$STAGED_MD" | xargs -0 markdownlint-cli2
+ echo "pre-commit: markdown lint passed."
+```
+
+
+
+π€ Prompt for AI Agents
+
+```
+Verify each finding against the current code and only fix it if needed.
+
+In `@scripts/hooks/pre-commit` around lines 12 - 19, The current pipeline splits
+filenames on whitespace because it echoes STAGED_MD into xargs; replace that
+with a NUL-delimited pipeline so filenames with spaces are preserved: produce
+the staged list as NUL-separated entries (use git's NUL output and grep's NUL
+mode or otherwise emit STAGED_MD with NUL separators) and invoke
+markdownlint-cli2 via xargs in NUL-safe mode (so xargs receives the original
+filenames intact) instead of the current echo "$STAGED_MD" | xargs
+markdownlint-cli2.
+```
+
+
+
+
+
+
+```
+
+_Meta_: https://github.com/flyingrobots/draft-punks/pull/6#discussion_r3006765226
+
+{response}
+
+### src/doghouse/adapters/github/gh_cli_adapter.py:97 β coderabbitai[bot]
+
+```text
+_β οΈ Potential issue_ | _π΄ Critical_
+
+
+π§© Analysis chain
+
+π Script executed:
+
+```shell
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Usage: ./verify_cursor.sh
+PR_NUMBER="${1:-1}"
+
+REPO_FULL="$(gh repo view --json nameWithOwner -q .nameWithOwner)"
+OWNER="${REPO_FULL%/*}"
+REPO="${REPO_FULL#*/}"
+
+QUERY='query($owner: String!, $repo: String!, $pr: Int!, $cursor: String) {
+ repository(owner: $owner, name: $repo) {
+ pullRequest(number: $pr) {
+ reviewThreads(first: 1, after: $cursor) {
+ pageInfo { hasNextPage endCursor }
+ }
+ }
+ }
+}'
+
+echo "=== Request with empty cursor ==="
+gh api graphql -F owner="$OWNER" -F repo="$REPO" -F pr="$PR_NUMBER" -f query="$QUERY" -F cursor= 2>&1 | sed -n '1,80p'
+
+echo
+echo "=== Request with omitted cursor ==="
+gh api graphql -F owner="$OWNER" -F repo="$REPO" -F pr="$PR_NUMBER" -f query="$QUERY" 2>&1 | sed -n '1,80p'
+```
+
+Repository: flyingrobots/draft-punks
+
+Length of output: 166
+
+---
+
+π Script executed:
+
+```shell
+cat -n src/doghouse/adapters/github/gh_cli_adapter.py | sed -n '85,105p'
+```
+
+Repository: flyingrobots/draft-punks
+
+Length of output: 1022
+
+---
+
+π Web query:
+
+`GitHub GraphQL after cursor parameter empty string vs omitted behavior`
+
+π‘ Result:
+
+In GitHub's GraphQL API v4, the after parameter for connection-based pagination is of type String (nullable). Omitting the after parameter (or passing null) fetches the first page of results. Passing an empty string "" as the after cursor leads to inconsistent behavior across different connections: it works for some (e.g., repository tags via refs) but fails for others (e.g., search queries) with the error "INVALID_CURSOR_ARGUMENTS": " does not appear to be a valid cursor." The recommended approach for the first page is to omit the after parameter entirely or explicitly pass null (as shown in official docs: pullRequests(first: 100, after: null)). For subsequent pages, use the base64-encoded endCursor from pageInfo. Cursor validation is connection-specific, as implementations may vary slightly, but empty strings are not valid cursors in GitHub's system.
+
+Citations:
+
+- 1: https://github.com/orgs/community/discussions/24443
+- 2: https://docs.github.com/en/graphql/guides/using-pagination-in-the-graphql-api
+- 3: https://docs.github.com/en/graphql/overview/explorer
+- 4: https://graphql.github.com/explorer
+- 5: https://docs.github.com/en/graphql/reference/objects
+- 6: https://facebook.github.io/relay/graphql/connections.htm
+- 7: https://docs.github.com/en/graphql/reference/scalars
+
+---
+
+π Script executed:
+
+```shell
+cat -n src/doghouse/adapters/github/gh_cli_adapter.py | sed -n '98,125p'
+```
+
+Repository: flyingrobots/draft-punks
+
+Length of output: 1592
+
+---
+
+
+
+
+
+**Critical: first-page request must not send empty cursorβGitHub GraphQL rejects it.**
+
+Passing an empty string as the `after` cursor parameter (line 96) triggers `INVALID_CURSOR_ARGUMENTS` errors on many GitHub GraphQL connections. The correct behavior is to omit the `after` parameter entirely on the first page. The current code unconditionally adds `"-F", "cursor="` which forces an empty string into the GraphQL variable.
+
+
+Fix: omit cursor parameter on first page
+
+```diff
+ gql_args = [
+ "api", "graphql",
+ "-F", f"owner={owner}",
+ "-F", f"repo={name}",
+ "-F", f"pr={actual_pr_id}",
+ "-f", f"query={gql_query}",
+ ]
+- if cursor:
++ if cursor is not None:
+ gql_args += ["-F", f"cursor={cursor}"]
+- else:
+- gql_args += ["-F", "cursor="]
+```
+
+
+**Secondary concern**: Line 122 accesses `page_info["endCursor"]` without validating it exists. If GitHub returns `hasNextPage=true` with a null or missing `endCursor`, this will crash. Add a guard: `cursor = page_info.get("endCursor")` and validate before the next iteration.
+
+
+π€ Prompt for AI Agents
+
+```
+Verify each finding against the current code and only fix it if needed.
+
+In `@src/doghouse/adapters/github/gh_cli_adapter.py` around lines 93 - 97, The
+code currently appends ["-F", "cursor="] when cursor is falsy which sends an
+empty after parameter; change the logic around gql_args so that when cursor is
+falsy you do not add any "-F", "cursor=..." entries (only append the "-F",
+f"cursor={cursor}" pair when cursor is truthy), and when reading the next cursor
+use page_info.get("endCursor") into cursor and validate it (e.g., only set
+cursor for the next iteration if page_info.get("endCursor") is not None) so you
+never pass a null/empty endCursor into the next request; update the references
+to cursor, gql_args, page_info and endCursor accordingly.
+```
+
+
+
+
+
+
+```
+
+_Meta_: https://github.com/flyingrobots/draft-punks/pull/6#discussion_r3006765228
+
+{response}
+
+### src/doghouse/adapters/github/gh_cli_adapter.py:124 β coderabbitai[bot]
+
+```text
+_β οΈ Potential issue_ | _π Major_
+
+**Pagination loop needs a cursor-progression guard.**
+
+If `hasNextPage` is true but `endCursor` is null/repeated, this can refetch the same page indefinitely.
+
+
+π‘οΈ Fix: validate next cursor before continuing
+
+```diff
+ page_info = thread_data.get("pageInfo", {})
+ if page_info.get("hasNextPage"):
+- cursor = page_info["endCursor"]
++ next_cursor = page_info.get("endCursor")
++ if not next_cursor or next_cursor == cursor:
++ blockers.append(Blocker(
++ id="warning-thread-pagination-cursor",
++ type=BlockerType.OTHER,
++ message="Warning: review-thread pagination cursor did not advance",
++ severity=BlockerSeverity.WARNING
++ ))
++ break
++ cursor = next_cursor
+ else:
+ break
+```
+
+
+
+π€ Prompt for AI Agents
+
+```
+Verify each finding against the current code and only fix it if needed.
+
+In `@src/doghouse/adapters/github/gh_cli_adapter.py` around lines 120 - 124, The
+pagination loop uses page_info = thread_data.get("pageInfo", {}) and sets cursor
+= page_info["endCursor"] when hasNextPage is true, but it doesn't guard against
+a null or repeated endCursor; update the loop in gh_cli_adapter.py (around the
+cursor/page_info handling) to validate that page_info.get("endCursor") is
+non-null and different from the last cursor before assigning and continuing,
+otherwise break (or raise) to avoid refetching the same page indefinitely; you
+can implement this by tracking previous_cursor and comparing it to
+page_info["endCursor"] and only updating cursor and continuing when it changes.
+```
+
+
+
+
+
+
+```
+
+_Meta_: https://github.com/flyingrobots/draft-punks/pull/6#discussion_r3006765229
+
+{response}
+
+### General comment β coderabbitai[bot]
+
+```text
+
+
+
+
+
+## Summary by CodeRabbit
+
+* **New Features**
+ * GraphQL pagination for review thread fetching ensures correct handling of large pull requests exceeding 100 threads.
+ * Markdownlint pre-commit hook automatically validates staged markdown files.
+
+* **Chores**
+ * Updated markdownlint configuration and git hook setup.
+ * Removed bootstrap script.
+
+
+## Walkthrough
+
+This pull request implements GraphQL pagination for fetching unresolved review threads in the GitHub adapter to handle large pull requests exceeding the previous 100-thread limit, adds a markdownlint pre-commit hook infrastructure with accompanying configuration, updates the changelog with these features, archives a code review document, and removes an obsolete bootstrap script.
+
+## Changes
+
+|Cohort / File(s)|Summary|
+|---|---|
+|**Linting Configuration & Infrastructure**
`.markdownlint-cli2.jsonc`, `scripts/hooks/pre-commit`, `CHANGELOG.md`|Introduces markdownlint configuration with custom rules (allowing long lines, inline HTML), defines a new pre-commit hook script that runs linting on staged Markdown files while excluding `docs/archive/` and `docs/code-reviews/`, and documents both the linting hook and GraphQL pagination feature in the changelog.|
+|**GraphQL Pagination Implementation**
`src/doghouse/adapters/github/gh_cli_adapter.py`|Modifies `fetch_blockers` to implement cursor-based pagination for review threads, replacing single-shot GraphQL queries with iterative calls that accumulate results across pages until `hasNextPage` is false. Preserves existing per-thread processing logic (filtering resolved threads, extracting comments, truncating messages).|
+|**Documentation & Archival**
`docs/code-reviews/PR6/eb263486b01a8eb3844a6159788b646506475cfe.md`|Adds archived code review feedback document with frontmatter metadata and review details table.|
+|**Removed Bootstrap Tooling**
`tools/bootstrap-git-mind.sh`|Deletes the entire bootstrap script that previously created, configured, and initialized a `git-mind` Python package directory structure.|
+
+## Estimated code review effort
+
+π― 3 (Moderate) | β±οΈ ~25 minutes
+
+## Poem
+
+> π Threads now paginate with cursors so fine,
+> Linting enforced before commit time,
+> A bootstrap script bows, its duty complete,
+> Large PRs no longer face defeat! β¨
+> Markdown sanctioned, the review flows clean.
+
+
+
+
+
+
+
+π₯ Pre-merge checks | β
3
+
+
+β
Passed checks (3 passed)
+
+| Check name | Status | Explanation |
+| :----------------: | :------- | :--------------------------------------------------------------------------------------------------------------------------------- |
+| Title check | β
Passed | The title directly and specifically summarizes all three main changes: GraphQL pagination, lint hook setup, and dead code removal. |
+| Description check | β
Passed | The description comprehensively covers all changeset components with clear rationale, test evidence, and technical detail. |
+| Docstring Coverage | β
Passed | No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. |
+
+
+
+βοΈ Tip: You can configure your own custom pre-merge checks in the settings.
+
+
+
+
+
+
+
+
+β¨ Finishing Touches
+
+
+π Generate docstrings
+
+- [ ] Create stacked PR
+- [ ] Commit on current branch
+
+
+
+π§ͺ Generate unit tests (beta)
+
+- [ ] Create PR with unit tests
+- [ ] Commit unit tests in branch `chore/backlog-cleanup`
+
+
+
+
+
+
+
+
+
+---
+
+Thanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=flyingrobots/draft-punks&utm_content=6)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
+
+
+β€οΈ Share
+
+- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
+- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
+- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
+- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)
+
+
+
+Comment `@coderabbitai help` to get the list of available commands and usage tips.
+
+
+
+
+
+
+
+
+
+```
+
+_Meta_: https://github.com/flyingrobots/draft-punks/pull/6#issuecomment-4151060464
+
+{response}
+
diff --git a/docs/code-reviews/PR6/eb263486b01a8eb3844a6159788b646506475cfe.md b/docs/code-reviews/PR6/eb263486b01a8eb3844a6159788b646506475cfe.md
new file mode 100644
index 0000000..2793a48
--- /dev/null
+++ b/docs/code-reviews/PR6/eb263486b01a8eb3844a6159788b646506475cfe.md
@@ -0,0 +1,17 @@
+---
+title: eb263486b01a8eb3844a6159788b646506475cfe.md
+description: Preserved review artifacts and rationale.
+audience: [contributors]
+domain: [quality]
+tags: [review]
+status: archive
+---
+
+# Code Review Feedback
+
+| Date | Agent | SHA | Branch | PR |
+|------|-------|-----|--------|----|
+| 2026-03-29 | CodeRabbit (and reviewers) | `eb263486b01a8eb3844a6159788b646506475cfe` | [chore/backlog-cleanup](https://github.com/flyingrobots/draft-punks/tree/chore/backlog-cleanup "flyingrobots/draft-punks:chore/backlog-cleanup") | [PR#6](https://github.com/flyingrobots/draft-punks/pull/6) |
+
+## CODE REVIEW FEEDBACK
+
diff --git a/scripts/hooks/pre-commit b/scripts/hooks/pre-commit
new file mode 100755
index 0000000..2fd7bd3
--- /dev/null
+++ b/scripts/hooks/pre-commit
@@ -0,0 +1,28 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# Pre-commit hook: lint must pass cleanly.
+# Configured via: git config --local core.hooksPath scripts/hooks
+
+echo "pre-commit: checking markdown lint..."
+
+# Only run markdownlint if it's installed
+if command -v markdownlint-cli2 &>/dev/null; then
+ # Lint staged .md files only (skip archived docs and code-review worksheets)
+ # Use NUL-delimited output to handle filenames with spaces safely.
+ STAGED_MD=$(git diff --cached --name-only -z --diff-filter=ACM -- '*.md' \
+ | tr '\0' '\n' \
+ | grep -v '^docs/archive/' \
+ | grep -v '^docs/code-reviews/' \
+ || true)
+
+ if [ -n "$STAGED_MD" ]; then
+ echo "$STAGED_MD" | tr '\n' '\0' | xargs -0 markdownlint-cli2
+ echo "pre-commit: markdown lint passed."
+ else
+ echo "pre-commit: no staged markdown files to lint (excluding archive)."
+ fi
+else
+ echo "pre-commit: markdownlint-cli2 not installed, skipping markdown lint."
+ echo " Install with: npm install -g markdownlint-cli2"
+fi
diff --git a/src/doghouse/adapters/github/gh_cli_adapter.py b/src/doghouse/adapters/github/gh_cli_adapter.py
index 54acf4a..19aedda 100644
--- a/src/doghouse/adapters/github/gh_cli_adapter.py
+++ b/src/doghouse/adapters/github/gh_cli_adapter.py
@@ -55,13 +55,17 @@ def fetch_blockers(self, pr_id: int | None = None) -> list[Blocker]:
blockers: list[Blocker] = []
- # 2. Fetch Unresolved threads via GraphQL
+ # 2. Fetch Unresolved threads via GraphQL (paginated)
owner, name = self._fetch_repo_info()
gql_query = """
- query($owner: String!, $repo: String!, $pr: Int!) {
+ query($owner: String!, $repo: String!, $pr: Int!, $cursor: String) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $pr) {
- reviewThreads(first: 100) {
+ reviewThreads(first: 100, after: $cursor) {
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
nodes {
isResolved
comments(first: 1) {
@@ -77,28 +81,47 @@ def fetch_blockers(self, pr_id: int | None = None) -> list[Blocker]:
}
"""
try:
- gql_res = self._run_gh_json([
- "api", "graphql",
- "-F", f"owner={owner}",
- "-F", f"repo={name}",
- "-F", f"pr={actual_pr_id}",
- "-f", f"query={gql_query}"
- ], with_repo=False)
- threads = gql_res.get("data", {}).get("repository", {}).get("pullRequest", {}).get("reviewThreads", {}).get("nodes", [])
- for thread in threads:
- if not thread.get("isResolved"):
- comments = thread.get("comments", {}).get("nodes", [])
- if comments:
- first_comment = comments[0]
- msg = first_comment.get("body", "Unresolved thread")
- if len(msg) > 80:
- msg = msg[:77] + "..."
-
- blockers.append(Blocker(
- id=f"thread-{first_comment.get('id', 'unknown')}",
- type=BlockerType.UNRESOLVED_THREAD,
- message=msg
- ))
+ cursor: str | None = None
+ while True:
+ gql_args = [
+ "api", "graphql",
+ "-F", f"owner={owner}",
+ "-F", f"repo={name}",
+ "-F", f"pr={actual_pr_id}",
+ "-f", f"query={gql_query}",
+ ]
+ # Omit cursor on first page; GitHub expects null/absent, not empty string
+ if cursor is not None:
+ gql_args += ["-F", f"cursor={cursor}"]
+
+ gql_res = self._run_gh_json(gql_args, with_repo=False)
+ thread_data = (
+ gql_res.get("data", {})
+ .get("repository", {})
+ .get("pullRequest", {})
+ .get("reviewThreads", {})
+ )
+ for thread in thread_data.get("nodes", []):
+ if not thread.get("isResolved"):
+ comments = thread.get("comments", {}).get("nodes", [])
+ if comments:
+ first_comment = comments[0]
+ msg = first_comment.get("body", "Unresolved thread")
+ if len(msg) > 80:
+ msg = msg[:77] + "..."
+
+ blockers.append(Blocker(
+ id=f"thread-{first_comment.get('id', 'unknown')}",
+ type=BlockerType.UNRESOLVED_THREAD,
+ message=msg
+ ))
+
+ page_info = thread_data.get("pageInfo", {})
+ next_cursor = page_info.get("endCursor")
+ if page_info.get("hasNextPage") and next_cursor and next_cursor != cursor:
+ cursor = next_cursor
+ else:
+ break
except (subprocess.CalledProcessError, subprocess.TimeoutExpired,
json.JSONDecodeError, KeyError) as e:
blockers.append(Blocker(
diff --git a/tools/bootstrap-git-mind.sh b/tools/bootstrap-git-mind.sh
deleted file mode 100644
index e1c8558..0000000
--- a/tools/bootstrap-git-mind.sh
+++ /dev/null
@@ -1,81 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-DEST=${1:-"$HOME/git-mind"}
-SRC_DIR="docs/archive/mind" # Sources were moved here during Doghouse reboot
-
-echo "Bootstrapping git-mind into: $DEST"
-
-if [[ -d "$DEST" ]] && [ "$(ls -A "$DEST")" ]; then
- if [[ -e "$DEST/.git" ]]; then
- echo "Destination already a git repo: $DEST. Refusing to clobber." >&2
- exit 2
- else
- echo "Destination is not empty: $DEST. Refusing to clobber." >&2
- exit 2
- fi
-fi
-
-if [[ ! -d "$SRC_DIR" ]]; then
- echo "Source directory $SRC_DIR not found. Git-mind sources missing." >&2
- exit 3
-fi
-
-mkdir -p "$DEST"
-
-# Create minimal pyproject for git-mind only
-cat >"$DEST/pyproject.toml" <<'PY'
-[project]
-name = "git-mind"
-version = "0.0.1"
-description = "Git-native conversational ops: sessions as refs, commits as speech-acts."
-authors = [{name = "GATOS"}]
-requires-python = ">=3.11"
-dependencies = ["typer>=0.12"]
-
-[project.scripts]
-git-mind = "git_mind.cli:run"
-
-[build-system]
-requires = ["hatchling>=1.21"]
-build-backend = "hatchling.build"
-PY
-
-mkdir -p "$DEST/src/git_mind" "$DEST/tests" "$DEST/docs/mind"
-
-# Copy sources and docs from current repo (using archive location)
-# Note: actual python sources were deleted in reboot, this script might need
-# adjustment if we really want to restore git-mind from history.
-# For now, hardening the script logic as requested.
-
-if [ -d "src/git_mind" ]; then
- cp -R src/git_mind/* "$DEST/src/git_mind/"
-fi
-
-cp -R "$SRC_DIR/"* "$DEST/docs/mind/" 2>/dev/null || true
-
-cat >"$DEST/README.md" <<'MD'
-# git mind (GATOS)
-
-Git-native operating surface. Sessions as refs. Commits as speech-acts. JSONL stdio API.
-
-Quickstart:
-
-```bash
-python -m venv .venv && . .venv/bin/activate && pip install -e .
-git mind session-new main
-git mind repo-detect
-git mind serve --stdio
-```
-MD
-
-cat >"$DEST/.gitignore" <<'GI'
-.venv/
-__pycache__/
-*.pyc
-GI
-
-(cd "$DEST" && git init -b main && git add . && git commit -m "git-mind bootstrap: snapshot engine + JSONL + docs")
-
-echo "Done. Next:"
-echo " cd $DEST && python -m venv .venv && . .venv/bin/activate && pip install -e . && git mind session-new main && git mind repo-detect"