Skip to content

fix(mt#1384): write canonical github-pr ref so reconciler can route Asks#858

Open
minsky-ai[bot] wants to merge 4 commits into
mainfrom
task/mt-1384
Open

fix(mt#1384): write canonical github-pr ref so reconciler can route Asks#858
minsky-ai[bot] wants to merge 4 commits into
mainfrom
task/mt-1384

Conversation

@minsky-ai
Copy link
Copy Markdown
Contributor

@minsky-ai minsky-ai Bot commented Apr 27, 2026

Summary

mt#1180 shipped DONE but had a silent production failure: session_pr_create was writing the raw PR URL (e.g. https://github.com/owner/repo/pull/99) into Ask.contextRefs[0].ref, while the reconciler's parsePrRef in src/domain/ask/reconciler.ts:71 only matches the canonical form github-pr:owner/repo/N. Every quality.review Ask filed in production was silently skipped on every reconciler pass — never transitioned to RESPONDED, never notified.

Surfaced by post-merge audit of mt#1180 (had Claude run an auditor pass).

Changes

  • New helper parseGithubPrUrl(url) returning {owner, repo, prNumber}; parsePrNumber rewritten as a thin wrapper.
  • New helper fileQualityReviewAsk(askRepository, params) that writes contextRef.ref in canonical github-pr:owner/repo/N form and preserves the full URL in contextRef.description for click-through.
  • sessionPrCreate now calls the helper instead of inlining Ask construction.
  • New e2e integration test: file an Ask via the writer, run reconcile() against a fake GithubReviewClient + spy OperatorNotify, assert state transitions to responded and bell + notify fire.
  • New regression-guard test: documents that the old URL-form ref is invisible to the reconciler (skipped) — protects against a regression that re-introduces the bug.

Test plan

  • bun test src/domain/session/commands/pr-create-subcommand.test.ts src/domain/ask -> 74/74 pass (16 new, 58 existing)
  • mcp__minsky__validate_typecheck -> clean
  • mcp__minsky__validate_lint -> clean

Notes for downstream

  • mt#1292 (real GithubReviewClient wiring, IN-PROGRESS) can now land knowing the upstream Ask-filing path actually feeds the reconciler.
  • No backfill of historical rows needed — there is no production deployment of mt#1180 yet that would have accumulated URL-form Asks. If that assumption is wrong, file a follow-up.
  • The skills-lock.json change in this PR is an unrelated lock-file regeneration that surfaced during the session; flagging here for transparency.

Closes mt#1384.

edobry added 2 commits April 27, 2026 19:12
session_pr_create was writing the raw PR URL into Ask.contextRefs[0].ref,
but the reconciler expects canonical 'github-pr:owner/repo/N' form. Every
Ask filed in production was silently skipped on every reconciler pass.

- Add parseGithubPrUrl helper returning {owner, repo, prNumber}
- Add fileQualityReviewAsk helper that writes canonical ref + preserves
  the URL in contextRef.description
- Replace inline Ask block in sessionPrCreate with the helper
- Add e2e integration test: file Ask via writer -> run reconcile -> assert
  state=responded and operator-notify fires
- Add regression-guard test: documents the buggy URL-form path is skipped
@minsky-ai minsky-ai Bot added the authorship/co-authored Co-authored by human and AI agent label Apr 27, 2026
Copy link
Copy Markdown

@minsky-reviewer minsky-reviewer Bot left a comment

Choose a reason for hiding this comment

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

Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: unknown


Findings

[BLOCKING] src/domain/session/commands/pr-create-subcommand.ts: fileQualityReviewAsk drops the PR URL entirely when parse fails or prUrl is omitted

  • Evidence: In fileQualityReviewAsk, contextRefs is set to [] when canonicalRef is undefined (i.e., non-github.com hosts, malformed URLs, or omitted prUrl):
    const contextRefs =
    canonicalRef && params.prUrl
    ? [ { kind: "github-pr", ref: canonicalRef, description: ... } ]
    : [];
  • Failure mode: Previously, the code always included a contextRef for a provided URL (ref: the old path wrote the raw PR URL into contextRefs[0].ref). Now, if the URL is not parseable by parseGithubPrUrl (e.g., GitHub Enterprise hosts like https://github.mycompany.com/owner/repo/pull/99 or other supported providers), the Ask loses the only link to the PR entirely (no context ref, no URL preserved anywhere). This is a silent behavior regression that degrades operator context and blocks the reconciler from ever considering these Asks later if the reconciler gains broader parsing.
  • Impact: Operators and any downstream tooling have no clickable artifact in the Ask; Ask reconciliation will be permanently skipped for non-github.com PRs because contextRefs has no canonical ref and now also no raw URL to potentially recover from later.
  • Requested change: When params.prUrl is provided but not parseable, still attach a contextRef preserving the raw URL (e.g., kind: "github-pr" with ref set to the raw URL or a fallback kind: "url") and keep title/description sensible. This preserves operator click-through and enables future reconcile adapters to parse or migrate.

[BLOCKING] src/domain/session/commands/pr-create-subcommand.ts: parsePrNumber now only recognizes github.com URLs, regressing behavior for enterprise/self-hosted GitHub

  • Evidence: parsePrNumber was rewritten to: return parseGithubPrUrl(url)?.prNumber; and parseGithubPrUrl strictly matches /^https?://github.com/([^/]+)/([^/]+)/pull/(\d+)/. The prior implementation extracted a PR number using //pull/(\d+)/ anywhere in the string.
  • Failure mode: For enterprise/self-hosted GitHub instances (e.g., https://github.mycompany.com/...), the previous parsePrNumber would still extract the number, allowing the Ask title to be “Review PR #N” and contextRef.description to reflect “PR #N”. The new implementation returns undefined, resulting in a degraded title (“Review PR”) and, coupled with the first finding, an empty contextRefs array. This reduces usability and is a silent broadening of the “unsupported” surface area with no migration path.
  • Requested change: Either (a) loosen parseGithubPrUrl to accept arbitrary github-like hosts (configurable or pattern-based), or (b) keep parsePrNumber tolerant (still extract digits after /pull/) while using parseGithubPrUrl only for canonicalRef emission. At minimum, retain prior lenient behavior for parsePrNumber so titles remain informative.

[NON-BLOCKING] src/domain/session/commands/pr-create-subcommand.ts: UI click-through assumption is undocumented and unverified

  • Evidence: The PR states the full PR URL is preserved in contextRefs[0].description “for click-through.” The writer now sets ref to a non-URL canonical value (github-pr:owner/repo/N) and moves the URL into description: pr-create-subcommand.ts, in fileQualityReviewAsk.
  • Risk: If any UI/renderer or notification layer auto-links ref but not description, operator click-through may regress. This is a system-level assumption change that should be documented and verified.
  • Requested action: Confirm that the UI/notification code uses description for link rendering, or alternatively add a contextRef with kind "url" and ref set to the URL to ensure robust clickability.
  • [NON-BLOCKING] NEEDS VERIFICATION: out-of-repo UI/renderer code not visible here; reviewer cannot verify.

[NON-BLOCKING] src/domain/session/commands/pr-create-subcommand.test.ts: lost regression guard that Ask-insert failure is swallowed by sessionPrCreate

  • Evidence: The previous test “Ask insert failure does not throw — PR creation result is still returned” was removed. The swallowing behavior is still implemented in sessionPrCreate via try/catch around fileQualityReviewAsk, but no test covers it after the refactor.
  • Risk: Future changes to fileQualityReviewAsk or the try/catch boundary could reintroduce error propagation that aborts PR creation.
  • Requested action: Reintroduce a test that stubs askRepository.create to throw and asserts sessionPrCreate still resolves successfully with the PR creation result.

[NON-BLOCKING] src/domain/session/commands/pr-create-subcommand.ts: debug log lost useful context

  • Evidence: Replaced log.debug("Filed quality.review Ask for PR", { prUrl, prNumber, sessionId, taskId }) with log.debug("Filed quality.review Ask for PR", { prUrl: result.url, sessionId, taskId }).
  • Impact: Losing prNumber in the log makes triage slightly harder.
  • Requested action: Include the parsed prNumber (if available) in the log metadata.

[NON-BLOCKING] skills-lock.json: unrelated change

  • Evidence: Added "skillPath" for supabase-postgres-best-practices. The PR description calls this out as unrelated lock-file regeneration.
  • Impact: Scope creep against the stated purpose. If necessary, split into a separate housekeeping PR to minimize risk in production changes.

Spec verification

No explicit task spec provided. Using PR description as intent proxy.

  • Write canonical github-pr:owner/repo/N ref into Ask.contextRefs[0].ref: Met
  • Preserve full PR URL for click-through: Partially Met (URL placed in description, but click-through behavior depends on UI; see non-blocking finding)
  • session_pr_create to call helper instead of inlining: Met
  • New parseGithubPrUrl helper and parsePrNumber wrapper: Met (but introduces regression for enterprise/self-hosted; see blocking finding)
  • E2E test verifies reconcile transitions Ask to responded and fires notify: Met
  • Regression guard test documents URL-form ref is skipped by reconciler: Met
  • No backfill needed: N/A (deployment note)
  • Skills lock change is unrelated: Included but out-of-scope

Documentation impact

  • Yes. Update docs/architecture or developer notes to:
    • Document the canonical github-pr:owner/repo/N contextRef format and the reconciler’s dependency on it.
    • Clarify handling for non-github.com PR URLs (enterprise/self-hosted). If not supported, document explicitly; if intended to support, adjust code accordingly.
    • Note the UI/notification behavior regarding clickable links from description vs ref.

Conclusion

REQUEST_CHANGES

The refactor silently regresses behavior for any non-github.com PR URLs by dropping the URL entirely from contextRefs and making parsePrNumber fail to produce titles. Please address the two blocking findings:

  • Preserve raw URL context when parse fails and/or broaden parsing to include enterprise/self-hosted GitHub.
  • Restore tolerant parsePrNumber behavior or otherwise avoid degrading titles and context for legitimate PR URLs outside github.com.

The remaining items are non-blocking but recommended for robustness and observability.

…ail, lenient parsePrNumber

Reviewer findings on pullrequestreview-4184769511:

1. BLOCKING: fileQualityReviewAsk dropped the PR URL when parseGithubPrUrl
   failed (e.g., GitHub Enterprise hosts). contextRefs became empty, losing
   operator click-through entirely. Now: when prUrl is provided but not
   canonically parseable, write contextRef with kind=github-pr and ref=raw URL.
   The reconciler still skips it gracefully because parsePrRef returns null on
   non-canonical refs.

2. BLOCKING: parsePrNumber regressed for non-github.com URLs because it was
   rewritten as a thin wrapper over the strict parseGithubPrUrl. Restored to
   the original lenient regex (matches /pull/N anywhere) so Enterprise URLs
   still get correct titles.

Non-blocking:
- Re-added regression test for Ask-insert failure propagation (the helper
  surfaces errors; sessionPrCreate is the swallow boundary).
- Restored prNumber field in the debug log.

Tests: 77/77 pass (74 -> 77, +3 net).
Copy link
Copy Markdown

@minsky-reviewer minsky-reviewer Bot left a comment

Choose a reason for hiding this comment

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

Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: unknown


⚠️ Reviewer did not emit a conclude_review call. Event derived from severity counts: REQUEST_CHANGES (1 BLOCKING / 2 NON-BLOCKING / 0 PRE-EXISTING findings). Executive summary unavailable.

Findings

  • [BLOCKING] src/domain/session/commands/pr-create-subcommand.test.ts:225 — Test constructs Ask missing required parentTaskId/parentSessionId compared to production code path; weakens e2e fidelity
    In the "regression: Ask with URL-form ref (pre-fix) is silently skipped" test at src/domain/session/commands/pr-create-subcommand.test.ts:225, the Ask is seeded via FakeAskRepository.create without parentTaskId or parentSessionId. In production, session_pr_create supplies these, and other code may assume presence. This discrepancy can hide bugs related to context propagation and state transitions. To keep the e2e test representative, set parentSessionId and parentTaskId consistently (e.g., parentSessionId: SESSION_ID, parentTaskId: TASK_ID).
  • [NON-BLOCKING] src/domain/session/commands/pr-create-subcommand.ts:138 — parseGithubPrUrl regex is not anchored at the end; URLs with extra path segments after the PR number will still match
    At src/domain/session/commands/pr-create-subcommand.ts:138, the regex /^https?://github.com/([^/]+)/([^/]+)/pull/(\d+)/ does not anchor to the end of the string. A URL like https://github.com/owner/repo/pull/42/files will match and return prNumber=42. If the intent of "canonical" is to require exactly the PR page (optionally with only a query string), consider tightening the pattern to anchor at the end or to allow only an optional query string (e.g., /pull/(\d+)(?:?.*)?$/). If lenience is intended, a comment clarifying that trailing segments are accepted would help future readers.
  • [NON-BLOCKING] src/domain/session/commands/pr-create-subcommand.ts:174 — Emitting empty array for contextRefs changes stored shape vs undefined/null and may increase storage churn
    fileQualityReviewAsk sets contextRefs to [] when prUrl is omitted (src/domain/session/commands/pr-create-subcommand.ts:174-186). The Drizzle mapper (src/domain/ask/repository.ts:66) stores undefined as NULL, but an explicit [] will be persisted as an empty JSON array. If downstream code/tests assume contextRefs is undefined when absent, this may be a subtle behavior change. Consider using undefined when there is no context instead of [].

…ed regex doc, undefined vs [])

Reviewer findings on pullrequestreview-4192211461:

1. BLOCKING: regression test seeded an Ask without parentTaskId, weakening
   e2e fidelity vs production. Added parentTaskId: TASK_ID alongside the
   existing parentSessionId.

2. NON-BLOCKING: parseGithubPrUrl regex isn't anchored — URLs with trailing
   segments like /pull/42/files match. Added a docblock note that this is
   intentional (matches how PR URLs appear in the wild from GitHub's UI),
   so future readers don't tighten it inadvertently.

3. NON-BLOCKING: contextRefs was [] when prUrl is omitted, which the Drizzle
   mapper would store as an empty JSON array; undefined maps to NULL and
   matches the schema's intended absence semantics. Switched to undefined and
   updated the test.

Tests: 77/77 still pass. Typecheck + lint clean.
Copy link
Copy Markdown

@minsky-reviewer minsky-reviewer Bot left a comment

Choose a reason for hiding this comment

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

Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: unknown


⚠️ Reviewer did not emit a conclude_review call. Event derived from severity counts: REQUEST_CHANGES (1 BLOCKING / 2 NON-BLOCKING / 0 PRE-EXISTING findings). Executive summary unavailable.

Findings

  • [BLOCKING] src/domain/session/commands/pr-create-subcommand.test.ts:223 — E2E regression test seeds Ask in detected state but reconcile logic expects routed/suspended path; FakeAskRepository/walkToSuspended may hide invalid transitions relative to production
    The test 'regression: Ask with URL-form ref (pre-fix) is silently skipped' (around line 223) seeds an Ask via FakeAskRepository.create and then immediately runs reconcile. reconcile() aggregates detected/classified/routed/suspended states and then walks Asks to suspended using walkToSuspended(), which relies on guardTransition. However, FakeAskRepository and DrizzleAskRepository both enforce guardTransition; if the real state machine diverges (e.g., forbids detected→classified without router), this test could pass while production fails or vice versa. More importantly, the seeded Ask includes parentSessionId/parentTaskId, but it bypasses sessionPrCreate and any routing metadata. Prior review flagged fidelity gaps here. To increase representativeness and avoid false positives/negatives, file the Ask via fileQualityReviewAsk (as done in earlier test) or walk through the same transitions reconcile will perform (detected→classified→routed) explicitly in setup, ensuring parity with production guards.
  • [NON-BLOCKING] src/domain/session/commands/pr-create-subcommand.ts:160 — Behavior change: contextRefs now omitted (undefined) when no prUrl, where previous path wrote an empty array
    In fileQualityReviewAsk, contextRefs is set to undefined when params.prUrl is omitted (lines 160-173). Previously sessionPrCreate always passed an array (empty when no URL). This alters runtime shape from [] (truthy, iterable) to undefined (falsy, non-iterable). While Drizzle mapping (src/domain/ask/repository.ts:66, 102) does normalize null↔undefined, any downstream code that iterates contextRefs without a presence guard (e.g., for (const r of ask.contextRefs)) will now throw. Consider standardizing on [] to preserve iterable semantics or auditing consumers to ensure they use optional chaining or guards.
  • [NON-BLOCKING] src/domain/session/commands/pr-create-subcommand.ts:149 — ContextRef.kind is always "github-pr" even when ref is a raw non-canonical URL (e.g., GitHub Enterprise); may be misleading to downstream consumers
    In fileQualityReviewAsk (lines ~149-170), when parseGithubPrUrl fails (non-github.com hosts), the emitted contextRef keeps kind: "github-pr" but sets ref to the raw URL. The reconciler correctly skips these (parsePrRef returns null), but other consumers might treat kind === "github-pr" as implying the canonical ref format and attempt to parse. Consider either documenting that kind "github-pr" can carry a raw URL ref for non-canonical cases or introducing a distinct kind (e.g., "url") to avoid ambiguity.

@edobry
Copy link
Copy Markdown
Owner

edobry commented Apr 30, 2026

Closing as subsumed by #878 (mt#1414), which fixed the same bug from the reader side: parsePrRef now accepts URL-form refs in addition to the canonical colon form. Per #878's body, that resolved the 72 stuck production Asks discovered during mt#1292 verification.

This PR (mt#1384) fixed it from the writer side — emitting canonical form. With #878 already merged, the production-failure success criterion is met. The remaining value here (canonical-form writer for hygiene/single-source-of-truth) is real but small, and the regression-guard test in this PR (asserting URL-form refs are silently skipped) is now invalidated by #878's permissive parser.

Reviewer iteration was also converging on diminishing returns (R3 BLOCKING claimed FakeAskRepository and DrizzleAskRepository may diverge on guards, but they call the same guardTransition).

Marking mt#1384 CLOSED. If the writer-canonical change is still wanted as defense-in-depth, it should be filed as a fresh task with a clean spec rather than rebasing this branch onto a reconciler that has changed under it.

Had Claude run the audit and recommend this close.

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

Labels

authorship/co-authored Co-authored by human and AI agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant