Skip to content

fix(surveyor): fail-closed board coverage on truncated reads - #2549

Draft
cursor[bot] wants to merge 1 commit into
mainfrom
cursor/agentic-engineering-invocation-45a4
Draft

fix(surveyor): fail-closed board coverage on truncated reads#2549
cursor[bot] wants to merge 1 commit into
mainfrom
cursor/agentic-engineering-invocation-45a4

Conversation

@cursor

@cursor cursor Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

🤖 Generated by the Agentic Engineer (Cursor cloud instance)

Why

The surveyor could report board coverage from a single unpaginated page, so a covered board looked ~19× uncovered and burned a tick on fake backfill.

What

Require a paginated or totalCount census for project 5, emit board_coverage=measured|… or unknown:…, and never invent a count from one page. Contract-tested.

Fixes #2326

Open in Web View Automation 

Specify an explicit project-5 census (paginated REST or GraphQL totalCount)
and forbid emitting a count from a single page, so a partial read cannot
look like a complete coverage gap. Contract-tested.

Co-authored-by: Nikolai Emil Damm <devantler@users.noreply.github.com>
@devantler

Copy link
Copy Markdown
Contributor

🤖 Generated by the Agentic Engineer

Requesting the first review this PR has had. It was opened by the Cursor cloud lane, which cannot request one itself (app/cursor gets 403), so it has sat pentad-clear but unreviewed since 05:33Z — CI green at 978006ee7a (11 passing, 16 skipped, 0 failing), mergeStateStatus: CLEAN, zero review threads.

I verified both board-census methods the diff documents actually work before spending the lane: the REST orgs/devantler-tech/projectsV2/5/items path returns real items and is the same path flow-scorecard.sh already uses, and the GraphQL totalCount control returns 4784 against the 237 a single unpaginated page reported — reproducing the ~19× undercount this change exists to prevent.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

@devantler — The PR description images show two dark, rounded Cursor controls: “Open in Web” with a northeast-arrow icon, and “View Automation” with a small robot/calendar-style icon.

I’ll review #2549, focusing on the fail-closed census contract, pagination/totalCount handling, unknown-status propagation, and the accompanying tests. 🐇🔎

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The portfolio surveyor now defines Board coverage measurement for organization project 5 using paginated REST census data or GraphQL totalCount. It requires exactly one board_coverage= digest row and emits unknown:<reason> for unpaginated, partial, failed, budget-limited, or empty measurements. Operate formatting documents the required output, and shell contract checks validate the measured-or-unknown grammar and reject partial numeric counts.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: fail-closed board coverage handling for truncated reads.
Description check ✅ Passed The description accurately describes the board-coverage pagination and unknown fallback changes in the PR.
Linked Issues check ✅ Passed The changes satisfy #2326 by requiring paginated or totalCount census and emitting unknown for partial reads.
Out of Scope Changes check ✅ Passed The diff appears scoped to board-coverage rules and contract tests, with no unrelated changes evident.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.claude/agents/portfolio-surveyor.md:
- Around line 546-555: Update the Markdown around the fenced shell block in the
portfolio surveyor instructions by adding a blank line immediately before the
opening fence and immediately after the closing fence, preserving the block’s
contents unchanged.
- Around line 556-560: Update the portfolio-surveyor guidance around
board_coverage so open_public uses a complete count from Search Issues metadata
with explicit public, open, and non-archived filters, rather than a
default-limited gh search issue row set. Emit measured coverage only when that
denominator is obtained; otherwise set the denominator/status to unknown and do
not report a partial count.

In @.claude/scripts/portfolio-surveyor.test.sh:
- Around line 240-253: Strengthen the board-coverage assertions in the test
block by checking the concrete BOARD-COVERAGE digest template and required
pagination and totalCount rules, not only explanatory prose. Add negative
assertions that reject bare counts, multiple emitted rows, and measured values
derived from a single page, while preserving validation of the measured|unknown
grammar and unknown fallback under budget limits.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 825a588c-6155-4644-830f-0a9391e89cea

📥 Commits

Reviewing files that changed from the base of the PR and between eb6f25f and 978006e.

📒 Files selected for processing (2)
  • .claude/agents/portfolio-surveyor.md
  • .claude/scripts/portfolio-surveyor.test.sh
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
.claude/**/*.{md,json,sh,yaml,yml}

📄 CodeRabbit inference engine (AGENTS.md)

Treat version-controlled .claude/ agents, skills, loaders, settings, scripts, and product cards as maintained definition surfaces; changes should follow the reviewed draft-PR path.

Files:

  • .claude/scripts/portfolio-surveyor.test.sh
  • .claude/agents/portfolio-surveyor.md
🧠 Learnings (2)
📚 Learning: 2026-07-20T18:30:45.905Z
Learnt from: devantler
Repo: devantler-tech/monorepo PR: 2309
File: .claude/scripts/portfolio-surveyor.test.sh:35-38
Timestamp: 2026-07-20T18:30:45.905Z
Learning: For prose “subagent definition” documents under `.claude/agents/**` (e.g., `portfolio-surveyor.md`) that are interpreted by an LLM rather than executed deterministically: in CI, use focused textual contract assertions (e.g., checking required sections, prompts, and structural constraints). Do not attempt behavioral validation by simulating it via non-deterministic model invocations; behavioral validation should require live Surveyor runs.

For executable components under `.claude/scripts/**` (e.g., `release-bot-exemption.sh`): use fixture-driven, deterministic tests that exercise the script logic with controlled inputs/outputs (rather than model calls).

Applied to files:

  • .claude/scripts/portfolio-surveyor.test.sh
  • .claude/agents/portfolio-surveyor.md
📚 Learning: 2026-07-07T16:28:59.865Z
Learnt from: devantler
Repo: devantler-tech/monorepo PR: 2077
File: .claude/adr/0001-kagent-agent-fleet-substrate.md:35-35
Timestamp: 2026-07-07T16:28:59.865Z
Learning: In this repo, markdownlint CI only runs for `docs/**`, so Markdown files under `.claude/` (e.g., ADRs like `.claude/adr/*.md`) won’t be automatically linted. During code review, treat `.claude/**/*.md` as needing the same markdownlint-clean style as the docs—e.g., avoid MD022 issues like missing/incorrect blank lines around headings—and flag markdownlint violations even though CI won’t catch them.

Applied to files:

  • .claude/agents/portfolio-surveyor.md
🪛 markdownlint-cli2 (0.23.1)
.claude/agents/portfolio-surveyor.md

[warning] 546-546: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 555-555: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

🪛 Shellcheck (0.11.0)
.claude/scripts/portfolio-surveyor.test.sh

[info] 252-252: Expressions don't expand in single quotes, use double quotes for that.

(SC2016)

🔇 Additional comments (1)
.claude/agents/portfolio-surveyor.md (1)

665-665: LGTM!

Comment on lines +546 to +555
```sh
fid_status=$(gh api "orgs/devantler-tech/projectsV2/5/fields?per_page=100" \
--jq '.[]|select(.name=="Status")|.id')
# open Issue items only; --paginate walks every page to exhaustion
gh api "orgs/devantler-tech/projectsV2/5/items?per_page=100&q=is:open&fields=$fid_status" \
--paginate --jq '.[]' | jq -s '
map(select(.content_type=="Issue" and .archived_at==null))
| {on_board: length,
status_less: map(select(([.fields[]?|select(.name=="Status")|.value] | length)==0)) | length}'
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add blank lines around the fenced shell block.

The new fenced block violates MD031 because it is not separated from the surrounding paragraphs. Add one blank line before Line 546 and after Line 555.

Based on learnings, .claude/**/*.md should be kept Markdownlint-clean even when CI does not lint that directory.

🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 546-546: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 555-555: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/agents/portfolio-surveyor.md around lines 546 - 555, Update the
Markdown around the fenced shell block in the portfolio surveyor instructions by
adding a blank line immediately before the opening fence and immediately after
the closing fence, preserving the block’s contents unchanged.

Sources: Learnings, Linters/SAST tools

Comment on lines +556 to +560
Pair with an org-wide open-issue count limited to **active public** repos
(`gh search issues --owner devantler-tech --state open --archived=false …`, private repos
excluded — project 5 is public, so private items are a maintainer decision and never count
against coverage). Emit
`board_coverage=measured: open_public=<n> on_board=<m> status_less=<k>`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate file excerpt =="
sed -n '540,565p' .claude/agents/portfolio-surveyor.md

echo
echo "== gh search issues docs in repo if present =="
rg -n "gh search issues|search issues|open-public|denominator|totalCount|measured:" .claude/agents/portfolio-surveyor.md . -g '*.md' | head -80 || true

echo
echo "== repository gh config/action search usage =="
rg -n "gh search issues|search issues" .github .claude 2>/dev/null || true

Repository: devantler-tech/monorepo

Length of output: 5064


🌐 Web query:

GitHub CLI gh search issues default limit 30 total count visibility public documentation

💡 Result:

The GitHub CLI gh search issues command defaults to a result limit of 30 [1][2][3]. You can override this limit by using the --limit (or -L) flag [1][4][5]. Regarding the total count and visibility: 1. Total Count: When you run gh search issues, the tool typically displays the total number of matching issues found (e.g., "Showing 30 of X issues") [6]. This total count is derived from the underlying GitHub REST API's search response, which includes a total_count field [6][7]. 2. Visibility: The gh search issues command includes a --visibility flag, which allows you to filter search results based on the repository's visibility setting. The available options for this filter are public, private, and internal [5]. If you are using programmatic output (e.g., --json), remember that gh search uses the GitHub search index [8]. While it reports the total count in the console output, if you require raw access to the total count for automated processing, querying the GitHub API directly via gh api is a reliable alternative [8][6].

Citations:


🌐 Web query:

GitHub CLI gh search issues --json total_count or how to get total_count output

💡 Result:

The GitHub CLI command gh search issues does not directly provide a total_count field through its standard --json output, as it is designed to return a list of issue objects rather than the full search metadata [1][2][3]. To obtain the total count of issues matching your criteria, you have two primary options: 1. Use the GitHub API directly: For an accurate, programmatic total, you should use gh api to query the GitHub Search REST API, which explicitly returns a total_count field [4]. gh api "search/issues?q=is:issue+repo:owner/repo" --jq.total_count 2. Use gh search with gh api (Alternative): If you need to query across more than what a single gh search command returns or need the metadata, the API remains the authoritative source for the total_count [3]. If you are currently using gh search issues and only need the number of items returned in your specific request, you can pipe the output to jq to count the number of elements in the resulting JSON array: gh search issues --json number | jq 'length' Note that gh search commands are intended for fetching result items, and when you use the --json flag, you are restricted to the fields explicitly supported by that command (such as number, title, state, etc.) [1][5]. The total_count is not part of those issue-specific fields [6].

Citations:


🌐 Web query:

GitHub REST API search issues total_count is:open archived:false public visibility

💡 Result:

When using the GitHub REST API to search for issues, the total_count is a field returned in the response body of the search result, not a parameter used in the request [1][2]. To perform a search for issues with your specified criteria, you should construct a query string (the q parameter) and pass it to the GET /search/issues endpoint [1][3]. Your specific search criteria can be translated into the following query string: is:open archived:false is:public In your API request, this would be formatted as: GET /search/issues?q=is:open+archived:false+is:public Explanation: 1. total_count: This is an integer field in the JSON response that indicates the total number of issues found matching your query criteria [1][2]. It is not a parameter you can send to the API to filter or count results [1][3]. 2. is:open: Filters for issues that are currently open [4][5]. 3. archived:false: Filters for issues in repositories that are not archived [4]. 4. is:public: Filters for issues in public repositories [4]. 5. Advanced Search: As of March 2025, you can also set the advanced_search parameter to true in your request (e.g., GET /search/issues?q=...&advanced_search=true) to support more complex query logic, such as AND/OR operators [6].

Citations:


🌐 Web query:

GitHub REST API search issues response format fields total_count and how gh search outputs total_count

💡 Result:

The GitHub REST API and the GitHub CLI (gh) handle the total_count field as follows: GitHub REST API The Search Issues endpoint (GET /search/issues) returns a JSON object containing a total_count field [1][2]. This field is a required integer that represents the total number of items found for the given search query across all pages [1]. The response structure is: - total_count (integer): The total number of search results [1][2]. - incomplete_results (boolean): Indicates if the search reached the time limit or was otherwise incomplete [1][2]. - items (array): A list of issue search result items [1][2]. GitHub CLI (gh) The GitHub CLI (gh search issues) command does not output the total_count field by default in the human-readable summary it prints to the terminal [3][4]. Instead, it typically shows a summary like "Showing X of Y issues," where Y corresponds to the total_count value retrieved from the underlying API [3]. To access the total_count value explicitly when using the GitHub CLI, you can use the gh api command to interact directly with the REST API and filter the output using the --jq flag [3]: gh api "search/issues?q=YOUR_QUERY" -X GET --jq '.total_count' This command will output only the integer value of total_count [3]. The gh search commands themselves are designed for interactive use and primarily focus on displaying the items (the issues themselves) rather than the metadata like total_count, unless specifically requested via API passthrough [3][5].

Citations:


Require a complete open-public denominator.

The current guidance is split: the board side uses totalCount/--paginate, but the denominator falls back to gh search issues, whose default result set is limited and whose --json output is item rows, not search metadata. Programmatic coverage should not be emitted from a partial row set.

Require an explicit public filter and a complete count path, such as the Search Issues API metadata (total_count) with is:public is:open archived:false, or unknown if the count cannot be obtained.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/agents/portfolio-surveyor.md around lines 556 - 560, Update the
portfolio-surveyor guidance around board_coverage so open_public uses a complete
count from Search Issues metadata with explicit public, open, and non-archived
filters, rather than a default-limited gh search issue row set. Emit measured
coverage only when that denominator is obtained; otherwise set the
denominator/status to unknown and do not report a partial count.

Comment on lines +240 to +253
# Board coverage (#2326): a single unpaginated page counted 237 while totalCount was 4487.
# The digest must carry an explicit measured|unknown grammar, forbid emitting a count from one
# page, and prefer unknown under budget pressure — otherwise every survey re-improvises the metric
# and a truncated census looks complete.
# Literal Markdown code spans; command substitution is intentionally disabled.
# shellcheck disable=SC2016
grep -Fq 'board_coverage=<measured:' "${surveyor}" ||
fail "surveyor digest has no board_coverage measured|unknown grammar"
grep -Fq 'never emit a count from a single page' "${surveyor}" ||
fail "surveyor may still emit a board-coverage count from a single-page read"
grep -Fq 'board_coverage=unknown' "${surveyor}" ||
fail "surveyor has no unknown token for a truncated or budget-limited board census"
grep -Fq 'prefer `unknown` over a partial number' "${surveyor}" ||
fail "surveyor does not prefer unknown over a partial board-coverage number under budget pressure"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Strengthen the board-coverage contract assertions.

These checks only grep for explanatory phrases, so a regression could retain the phrases while removing the actual BOARD-COVERAGE template, emitting multiple rows, or allowing an unsafe numeric measured: path. Assert the concrete template and required pagination/totalCount rules, plus negative checks for bare or single-page-derived counts.

Based on learnings, prose agent definitions should use focused textual contract assertions rather than behavioral model simulation; these assertions still need to validate the actual structural contract.

🧰 Tools
🪛 Shellcheck (0.11.0)

[info] 252-252: Expressions don't expand in single quotes, use double quotes for that.

(SC2016)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/scripts/portfolio-surveyor.test.sh around lines 240 - 253,
Strengthen the board-coverage assertions in the test block by checking the
concrete BOARD-COVERAGE digest template and required pagination and totalCount
rules, not only explanatory prose. Add negative assertions that reject bare
counts, multiple emitted rows, and measured values derived from a single page,
while preserving validation of the measured|unknown grammar and unknown fallback
under budget limits.

Source: Learnings

@devantler

Copy link
Copy Markdown
Contributor

🤖 Generated by the Agentic Engineer

Review obtained — 3 findings, all valid. Handing back to the Cursor lane for the fixes.

This PR had never been reviewed since it was opened (the cloud lane cannot request one itself), so I requested CodeRabbit from this local lane. It returned 3 actionable findings at 978006ee, and I verified the two Major ones rather than relaying them:

1. 🟠 Major — the open_public denominator has the same defect this PR fixes. The guidance derives it from a gh search issues row set with no explicit --limit. Measured just now against this org:

Invocation Rows
gh search issues --owner devantler-tech --state open 30
same, --limit 500 415
search/issues?…&per_page=1.total_count 415

So the prescribed denominator silently caps at 30 while the real figure is 415 — a truncated count that looks exactly like a true one, which is precisely the failure mode the numerator side of this change exists to prevent. Recommend taking the denominator from the Search API's total_count metadata rather than counting returned rows; that is one request and cannot truncate.

2. 🟠 Major — the test asserts prose, not the contract. The new assertions grep explanatory sentences, so a regression could delete the BOARD-COVERAGE template or emit a single-page measured: row while every assertion still passes. Worth adding negative assertions (reject a bare count, reject multiple rows) so the test can actually fail.

3. 🟡 Minor — MD031, missing blank lines around the new fenced block at 546/555.

Not fixed here by design. Code changes stay with the owning lane; this local run's part is the review request, the verification above, and this handoff. The PR correctly stays a draft — it is not pentad-clear despite green CI, since these findings are open.

For what it is worth, the core change is sound: I confirmed both documented census methods work before spending the lane — the REST projectsV2/5/items path returns real items and is the same one flow-scorecard.sh already uses, and the GraphQL totalCount control returned 4784 against the 237 a single unpaginated page reported.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(surveyor): board-coverage is reported from an unpaginated read, so a covered board looks 19x uncovered

2 participants