Skip to content

🐛 fix(kanban): dedup review cards on PR URL in create_task - #3

Merged
cwest merged 1 commit into
cwest/integrationfrom
topic/fix-dup-review-cards
Jun 24, 2026
Merged

🐛 fix(kanban): dedup review cards on PR URL in create_task#3
cwest merged 1 commit into
cwest/integrationfrom
topic/fix-dup-review-cards

Conversation

@cwest

@cwest cwest commented Jun 19, 2026

Copy link
Copy Markdown
Owner

What

Make a second review card for the same PR impossible to create, regardless of how the card is filed.

create_task already dedups on the idempotency key, but that misses one real case: a manually-filed review card for a PR the webhook also auto-files carries a different (or absent) key, so the key check passes and a duplicate review card lands. The phantom card looks stuck in the pipeline and its bogus gate blocks the downstream merge card.

How

Add a guard in create_task keyed on the PR identity itself, independent of the idempotency key and independent of the creation path (webhook retry OR hand-built chain):

  • A card is a review card when its skills include github-code-review (new REVIEW_SKILL constant) and it names a GitHub PR URL (title first — where the webhook puts it — then body).
  • The PR URL is canonicalised (_canonical_pr_url): host casing / www. normalised, trailing path / query / fragment stripped, so …/pull/43, …/pull/43/files, and …/pull/43?w=1 collapse to one identity.
  • If a non-archived review card already exists for the same PR, no-op and return its id. Archived cards do not block — mirroring the existing idempotency-key semantics — so a deliberate re-review (archive the old card, file a new one) still works.
  • SQL prefilter matches the JSON-quoted skill token ("github-code-review") to avoid substring false-positives (e.g. github-code-review-extra); the in-Python URL compare is authoritative.

Tests (TDD: RED → GREEN)

8 new tests in test_kanban_core_functionality.py:
dedup-without-key, pr-url-in-body-only, ignored-for-archived, only-for-review-skill, distinct-PRs-no-collide, without-PR-URL-not-deduped, canonicalises-URL-variants, substring-skill-not-matched.

Verified on a clean base (70cf64cee) via scripts/run_tests.sh:
tests/hermes_cli/test_kanban_core_functionality.py174/174 passed, zero regressions.

Follow-up (NOT in this PR)

The companion documentation rule — "the webhook owns review-card creation; hand-built chains must not manually file a review card for a PR" — belongs in the homestead-dispatch-golden-path skill, which is on the forge single-copy path (cwest/hermes-skills-forge, active worktree topic/skill-dedup-single-copy). Documented as a handoff rather than edited here to avoid creating a duplicate, diverged skill copy.

@cwest cwest left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

The dedup guard does what it claims. I pulled the head and ran the changed test file in a clean clone: 174 passed / 1 skipped, and the 8 new dedup tests all pass. The two failures I first saw were a missing python-multipart in my venv, not a regression; once installed the file is green.

The design holds up under the cases that matter. The guard sits after the idempotency-key check, so a card with both a key and a PR URL still short-circuits on the key first. The SQL prefilter matches the JSON-quoted token ("github-code-review"), so a skill that only contains the name as a substring doesn't trigger it — the substring test confirms that. Archived cards don't block a re-review, mirroring the existing key semantics. Candidate set is bounded to review cards, so the in-Python canonicalisation stays cheap.

One real gap, not a blocker: owner/repo case isn't normalised. GitHub treats owner/repo case-insensitively, but _canonical_pr_url preserves the captured case, so cwest/hermes-agent and cwest/Hermes-Agent produce different identities and won't dedup. I verified this against the head — host casing and www. collapse, but a repo-case variant slips through. In practice the webhook and a hand-copied URL come from the same place, so they'll match; worth a casefold on owner/repo if you want to close it. Noted inline.

The SELECT-then-INSERT outside a write lock can let two concurrent creators both pass the guard, but that's the same documented race the idempotency-key path already accepts, so it's consistent rather than new.

Leaving this as a comment since the PR is still a draft.

Comment thread hermes_cli/kanban_db.py
if not m:
return None
return (
f"https://github.com/{m.group('owner')}/{m.group('repo')}"

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Owner/repo are interpolated with their captured case. GitHub resolves owner/repo case-insensitively, so two cards naming cwest/hermes-agent and cwest/Hermes-Agent for the same PR won't dedup here. The host already normalises via the IGNORECASE match; a .casefold() (or .lower()) on owner and repo would make the identity match GitHub's own. Low-likelihood in the webhook+manual path, but it's the one input that defeats the guard.

@cwest
cwest force-pushed the cwest/integration branch from 902cded to 650f403 Compare June 21, 2026 23:28
A second review card for the same PR could be created whenever the
idempotency key missed it — specifically a manually-filed review card
carrying a different (or absent) key for a PR the webhook also auto-files.
The phantom card looks stuck in the pipeline and its bogus gate blocks the
downstream merge card.

Guard create_task on the PR identity itself: when a card carries the
github-code-review skill and names a GitHub PR URL (title first, then body),
no-op and return any existing non-archived review card for the same PR.
The PR URL is canonicalised (host casing/www, trailing path/query/fragment
stripped) so URL variants collapse to one identity. Archived cards do not
block, mirroring the idempotency-key semantics, so a deliberate re-review
still works. The SQL prefilter matches the JSON-quoted skill token to avoid
substring false-positives; the in-Python URL compare is authoritative.

Closes the duplicate-review-card path regardless of how the card was filed
(webhook retry or hand-built chain).
@cwest
cwest force-pushed the topic/fix-dup-review-cards branch from 3147b6e to 755eefc Compare June 24, 2026 11:16

@cwest cwest left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

The dedup logic holds up. Membership is checked against the already-cleaned skills list, so github-code-review-extra correctly doesn't match, and the SQL prefilter on the JSON-quoted token plus the in-Python canonical compare gives you both a cheap candidate set and an exact decision. Running the full file against this head: 175 passed, 0 failed, including all eight new dedup cases. Commit is signed and follows the convention.

One thing to confirm, noted inline: the new scan and the existing idempotency-key check are both global across boards/tenants. For a PR-URL identity that's probably what you want, but create_task does take board/tenant, so worth a deliberate decision rather than an accident.

Leaving this as a comment rather than an approval since the PR is still a draft.

Comment thread hermes_cli/kanban_db.py
# authoritative check.
rows = conn.execute(
"SELECT id, title, body FROM tasks "
"WHERE status != 'archived' AND skills LIKE ? "

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

This candidate query (like the idempotency-key lookup just above) isn't scoped by board or tenant, so a review card for the same PR URL on a different board would dedup to this one. Given a PR URL is globally unique that's likely the intended semantic, and it matches the existing key-dedup behavior. Flagging only to confirm it's a choice, not an oversight — elsewhere lookups do filter on tenant.

Comment thread hermes_cli/kanban_db.py
return None
if REVIEW_SKILL not in skills_list:
return None
return _canonical_pr_url(title) or _canonical_pr_url(body)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Title-first then body matches how the webhook files cards. Worth knowing: if a card's title happens to mention a different PR than its body, title wins silently. Not a problem for the webhook path; just the one ambiguity in the precedence.

@cwest
cwest marked this pull request as ready for review June 24, 2026 11:30
@cwest
cwest merged commit 4716e76 into cwest/integration Jun 24, 2026
8 checks passed
@cwest
cwest deleted the topic/fix-dup-review-cards branch June 24, 2026 11:46
cwest added a commit that referenced this pull request Jul 1, 2026
Deduplicate review cards on their PR URL in create_task so a repeated
PR-review request does not open a second review card for the same PR.

upstream-pending: fork PR #3
cwest pushed a commit that referenced this pull request Jul 26, 2026
Blocking #1 — gateway-connecting-overlay.tsx reduced-motion regression:
the top `if (reduce) setPhase('gone')` fired unconditionally on mount
whenever reduce-motion was on, so every OS reduced-motion user lost the
CONNECTING overlay during cold boot entirely (jumped to 'gone' before the
gateway was even open). The intent was to skip the exit *choreography*,
not to skip showing the overlay. Removed the unconditional top block and
the redundant nested preview block; kept only the third branch
(`gatewayState === 'open' && shownRef.current` → `reduce ? 'gone' :
'text-out'`) which correctly gates the short-circuit on connect. Also
fixed `if(reduce)` missing-space, 6-space misindent, and the same 3-line
comment pasted three times.

Nit #1 — tsconfig excludes e2e, so specs were never typechecked in CI.
Added tsconfig.e2e.json (extends base, includes e2e/ + playwright.config.ts,
adds @playwright/test types) and wired it into the typecheck script. This
surfaced three latent type errors that are fixed in the same commit:
  - fix-electron-tracing.ts: `app._context` and `electron._playwright` are
    private APIs — added `as any` on the access before the existing cast.
  - playwright.config.ts: `reducedMotion: 'reduce'` directly under `use:`
    is not a valid UseOptions property in playwright 1.58; it's a
    BrowserContextOption accessed via `contextOptions: { reducedMotion:
    'reduce' }`. The old form was silently ignored at runtime, so
    reduced-motion emulation wasn't actually active — screenshots could
    catch overlays mid-fade (exactly what the comment warned about).

Nit #2 — fix-electron-tracing.ts reaches into Playwright internals
(_playwright, _allContexts, _context) with no public contract. Added a
header comment calling out the `@playwright/test` exact pin (=1.58.2) so a
future bump knows to re-verify the private symbols still exist.

Nit #3 — main.ts TEST_WORKER_INDEX block had stray 6-space indentation.

Verified: tsc -p . && tsconfig.electron && tsconfig.e2e → 0 errors;
vitest boot-failure-overlay (3/3) + boot-failure-reauth (21/21) pass;
npm run build clean; playwright e2e/boot-failure.spec.ts 2/2 pass.
cwest pushed a commit that referenced this pull request Jul 26, 2026
…d curator

The skill-authoring guide and curator prompt both reference
descriptions as the primary discovery mechanism but never mentioned
the 57-char system prompt truncation. Add explicit guidance:

- Authoring guide: frontmatter docs, template comment, size limits,
  pitfall #3 with good/bad examples, verification checklist
- Curator prompt: parenthetical noting the 57-char window when
  writing umbrella skill descriptions
cwest added a commit that referenced this pull request Jul 26, 2026
Deduplicate review cards on their PR URL in create_task so a repeated
PR-review request does not open a second review card for the same PR.

upstream-pending: fork PR #3
(cherry picked from commit b16b103)
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.

1 participant