Skip to content

docs(specs): a task remembers every PR it has been associated with - #839

Merged
pat-lewczuk merged 5 commits into
mainfrom
spec/task-pr-reference-list
Sep 14, 2026
Merged

pat-lewczuk merged 5 commits into
mainfrom
spec/task-pr-reference-list

Conversation

@wojciechszyjka

Copy link
Copy Markdown
Contributor

Refs #779

Source doc: .ai/specs/2026-08-10-task-pr-reference-list.md
Status: complete

🎯 Goal

  • A task remembers every PR it has been associated with instead of only the last declared one, so the original PR stays clickable after a follow-up — and a closed-and-replaced PR is de-prioritised rather than the deleted-from-view default of today.

📐 Design in one line

  • RunRecord gains one additive optional prRefs list (append-only, deduped, capped at 8); prNumber / pullRequestUrl / referencedPullRequestUrl become derived projections of that list, so every existing reader, script and hand-edited runs.json keeps working and pre-spec records need no migration — they derive a one-entry view at read time.

What Changed

  • Spec document at .ai/specs/2026-08-10-task-pr-reference-list.md
  • Mockups + current-state screenshots under .ai/specs/assets/task-pr-reference-list/ (also attached below as inline evidence)

💥 Breaking Changes

  • None — design only. The spec's own compatibility stance: additive optional field in both runs.json (§3) and the run DTO, no route change in Phase 1, and a documented (non-destructive) downgrade behaviour when an older cezar rewrites the file.

@wojciechszyjka wojciechszyjka added review Ready for code review documentation Improvements or additions to documentation skip-qa Low risk, QA not required priority-medium Ordinary bug or feature risk-low Isolated, low blast radius labels Aug 10, 2026
@wojciechszyjka

wojciechszyjka commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

🤖 om-auto-write-spec — 🏷️ label rationale

  • 🔍 review — the PR is ready for specification review; nothing here is a work-in-progress placeholder.
  • 📚 documentation — the change is one spec document plus its illustrative assets, design-only.
  • ⏭️ skip-qa — there is no runtime behaviour to exercise: not a line of application code changes on this branch.
  • 🔹 priority-medium — the underlying defect is real and silent (a PR link the user relied on disappears), but it degrades discoverability rather than blocking any run, and this PR only designs the fix.
  • 🟢 risk-low — merging a design document cannot break a consumer; every compatibility decision it proposes is additive and is argued in the spec's Risks section.

Update — 🤖 om-auto-review-pr, 2026-08-11

  • 🔍 review → 🔁 changes-requested — the specification review found 2 blockers and 2 majors (prNumber cannot be a projection while three unlisted writers assign it directly; the legacy seed lets a regex/namer guess outrank a CEZ:PR= declaration; a §8-protected marker semantics change declared as "unchanged"; Phase 2's demotion diverging from the persisted prNumber and the task title). Verdict details in the review comment below.
  • ⏭️ skip-qa, 🔹 priority-medium, 🟢 risk-low, 📚 documentation — all four re-confirmed and kept. The diff is still one markdown file plus six assets: no runtime behaviour to QA, no blast radius on merge, and the priority of the design is unchanged by the findings (which are about the design's precision, not its urgency). Note that the subject issue Task keeps only the last PR link — original PR becomes unreachable after a follow-up #779 carries risk-high for the implementation — the state-file surface per SDLC.md — which is correct there and correctly not inherited here.
  • 🔒 in-progress — the review lock; released when the run reports.
  • 🔁 changes-requested → 🚦 merge-queue — every blocker and major was fixed in the spec document (5d333e4a, 6015a950) and re-reviewed; the PR carries no unresolved actionable finding. merge-queue records the review verdict only: GitHub blocks self-approval, so a human approving review is still required to merge.
  • 🔒 in-progress — removed; the review run is finished and the PR is free for anyone to act on.

@wojciechszyjka

Copy link
Copy Markdown
Contributor Author

📸 om-auto-write-spec — visual evidence for the spec

Captured against this repository's own shared test environment (.ai/scripts/test-env-up.sh, CEZ_DRY_RUN=1, agent-browser 0.33.2), seeded with a task that declared PRs #812, #824 and #831 in that order — the reproduction from #779, rendered.

Image Screen Role
current-01-tasks-overview.png Tasks overview (Ref column) + sidebar Current — one chip, #831, the last declaration. #812 and #824 from the same task are unreachable from anywhere in the cockpit.
current-02-task-thread-header.png Task-thread header meta line Current — the same single #831 chip beside the branch and issue chips.
mockup-01-task-thread-header.png Task-thread header Proposed — primary chip + inline siblings + +N, with the popover listing every PR, its state and how it was associated.
mockup-02-sidebar-and-overview.png Sidebar row + overview Ref column Proposed — the 264 px sidebar keeps one chip with a count suffix; the table gains a two-character +N that opens the same popover.

The mockups are illustrative statics (.html sources committed beside the spec under .ai/specs/assets/task-pr-reference-list/), not app code — they exist to communicate layout and priority, not pixels.

current-01-tasks-overview.png

current-02-task-thread-header.png

mockup-01-task-thread-header.png

mockup-02-sidebar-and-overview.png

@wojciechszyjka

Copy link
Copy Markdown
Contributor Author

🤖 om-auto-write-spec — Open Questions answered with autonomous defaults

This spec had open questions and the run is autonomous, so I applied conservative
defaults to keep moving. Please review and override before merge if any is wrong.

# Question Applied default Why Confirm?
Q1 Is the primary PR decided by declaration order or by PR state? Both, phased: declaration order (created PR wins) is the persisted offline rule in Phase 1; closed-PR demotion is a Phase 2 display rule fed by lazily hydrated state. Matches the issue's own "state first, then declaration order" while keeping Phase 1 shippable with no network dependency — and a GitHub outage can never reshuffle the chips. ok
Q2 Does the CEZ:PR= marker protocol change? No. It keeps meaning "the PR this task is currently about"; only the store's reaction changes from replace to append. A published protocol every skill emits is the least reversible surface in reach, and the store can infer what it needs without touching it. ok
Q3 Do issues (CEZ:ISSUE) get the same list now? No — PRs only. The schema and selector are shaped so issueRefs is a mechanical repeat, listed as a demand-gated Phase 3. You noted the issue side "bites much less in practice"; the smallest scope that fixes the reported defect wins, with the door left open. ok
Q4 Is PR state persisted in runs.json? No — hydrated at display time into the query cache only. A local file cannot be invalidated when a PR merges; a persisted state field would be confidently wrong. The record stays a pure association log. ok
Q5 How is state hydrated in Phase 2? A new windowed GET /api/v1/github/pr-states, mirroring the lazy checks endpoint (#664), with an explicit checkpoint to share the state helper with spec PR #816 if that lands first. The existing list fetches states: OPEN only, so it cannot tell "merged" from "closed" — exactly the distinction the requirement rests on. ok
Q6 Do transcript-scraped PR URLs (referencedPrCandidates) join the list? No. Only created and declared associations; the scraping set keeps feeding today's single-valued fallback. Merging noisy scrapings into a rendered list would rebuild defect #526 (links naming other repositories) across three surfaces. ok
Q7 What happens at the cap? Cap 8 (mirroring MAX_PR_CANDIDATES); evict the oldest non-primary entry. The primary is what the user clicks and the newest is what they just made; a middle entry is the cheapest loss, and 8 reuses an existing budget instead of inventing one. ok

None carries a ⚠ NEEDS HUMAN CONFIRMATION marker: every default is additive, display-tier only, and revertible without touching persisted state — which is why the PR is open ready for review rather than held as a draft.

To change any answer: reply here (or edit the spec's "Resolved assumptions" section) and re-run om-auto-write-spec — or implement with the corrected spec via om-auto-implement-spec .ai/specs/2026-08-10-task-pr-reference-list.md.

@wojciechszyjka

Copy link
Copy Markdown
Contributor Author

🤖 om-auto-write-spec — run summary

Spec: .ai/specs/2026-08-10-task-pr-reference-list.md
Branch: spec/task-pr-reference-list
Final status: complete — opened ready for review, no assumption is gated on a human answer

📝 What the spec decides

The single-valued PR slot becomes an ordered, append-only prRefs list on RunRecord, and the three fields the cockpit reads today (prNumber, pullRequestUrl, referencedPullRequestUrl) become projections of it. That inversion is what buys backward compatibility for free: every existing reader, script and hand-edited runs.json keeps seeing exactly what it sees now, pre-spec records derive a one-entry list at read time (no migration, no rewrite), and a rollback leaves records written by the new code perfectly readable by the old. Phase 1 fixes the reported data loss offline; Phase 2 adds lazily hydrated PR state so a closed-and-replaced PR is dimmed and can hand the primary slot to its successor; Phase 3 (issues) is deliberately demand-gated.

Two decisions are worth a reviewer's attention because they are judgement calls, not derivations: transcript-scraped PR URLs deliberately do not join the list (that path is what defect #526 exists to contain), and PR state is deliberately not persisted (a local file has no way to learn that a PR merged).

📝 Assumptions applied

Seven Open Questions were resolved with autonomous defaults and posted in the assumptions comment above (and on #779) for override. None carries a ⚠ NEEDS HUMAN CONFIRMATION marker, because every default is additive, confined to the display tier, and revertible without touching persisted state.

📸 Visual evidence

Four images attached in the evidence comment: two current-state captures from the repository's own shared test environment (the reproduction from #779 rendered — one #831 chip, with #812 and #824 unreachable), and two static mockups of the proposed chip rows on the task-thread header, the sidebar and the Tasks overview Ref column. The mockup sources are committed beside the spec.

🔁 Hand-off

Implement with: om-auto-implement-spec .ai/specs/2026-08-10-task-pr-reference-list.md — the implementation ships on its own PR referencing this one, which stays design-only. Phase 1 is a complete, independently shippable slice on its own.

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown

📦 npm preview published — 0.10.1-pr839.1474

Try this PR build (exact pinned version — copy-paste as-is):

npx cezar-cli@0.10.1-pr839.1474                                # cockpit at http://localhost:4321
npx cezar-cli@0.10.1-pr839.1474 run "…"                        # headless run
npx cezar-cli@0.10.1-pr839.1474 server-deploy --platform <id>  # roll a server to this exact build

Also tagged: npm install -g cezar-cli@pr-839 (moving tag for this PR).
Packages: cezar-cli@0.10.1-pr839.1474@open-mercato/cezar@0.10.1-pr839.1474@open-mercato/cezar-api-client@0.10.1-pr839.1474 (provenance attested).

@wojciechszyjka wojciechszyjka added the in-progress Cezar agent is actively working this issue label Aug 11, 2026
@wojciechszyjka wojciechszyjka self-assigned this Aug 11, 2026
@wojciechszyjka

Copy link
Copy Markdown
Contributor Author

🤖 om-auto-review-pr started by @wojciechszyjka at 2026-08-11T06:43:03Z. Other auto-skills will skip this PR until the lock is released.

@wojciechszyjka

Copy link
Copy Markdown
Contributor Author

ℹ️ Posted as a comment, not a formal review: GitHub refuses request changes on one's own pull request (addPullRequestReview: "Can not request changes on your own pull request"). The verdict below is the review; the changes-requested pipeline label carries it in the tracker.

🔍 Specification review: a task remembers every PR it has been associated with

Verdict: 🔴 Changes requested — 2 blockers, 2 majors, 4 minors, 2 nits.

This is a strong, unusually well-grounded spec: the tier separation (created vs display) is respected, prRefs is genuinely additive, the "no migration, derive at read time" stance is right for runs.json, and almost every line reference in it checks out against the code (store.ts:862, :758, :296, tasks-table.ts:135/:171, contract/runs.ts:206, task-quick-list.tsx:364, run-header.tsx:504, tasks-overview.tsx:632/:884, run.ts:3226 — all correct). The blockers are not about the idea; they are about one claim the codebase does not support — that prNumber can be the projection of the list — and about what the list is seeded from.


💥 What can go wrong

🔴 Blocker — prNumber cannot be "a projection" while three writers still assign it directly

.ai/specs/2026-08-10-task-pr-reference-list.md:102 states: "Every write path that touches the list recomputes prNumber from the primary. It is not an independent field any more." The Architecture table (:49:56) names three writers to change: the janitor, applyMarkerRefs, and the draft-PR path. But prNumber has three further writers that never touch prRefs, none of them mentioned anywhere in the spec:

  • packages/cezar/src/workflows/run.ts:767-773 — step-0 regex extraction at run creation (refineTaskRefs(extractTaskRefs(input.task))).
  • packages/cezar/src/workflows/run.ts:1835 — the same extraction on the resume path.
  • packages/cezar/src/workflows/run.ts:3179-3181 — the fire-and-forget LLM namer, guarded only by run.markerRefs?.pr === undefined.

Failure scenario. Task text: "port the fix from pr 441 onto the new branch". Step 0 writes prNumber = 441. The run then opens its own PR #900; the janitor sets pullRequestUrl and (per this spec) appends a created entry, so the primary is #900 and prNumber is recomputed to 900. Seconds later the namer answers — no CEZ:PR= was ever emitted, so the guard at :3179 passes — and updateRun({ prNumber: 441 }) lands. Persisted state is now prNumber: 441 with prRefs holding no 441 entry and a primary of #900. The task title prefix (run.ts:3233, auto-name.ts:198) and every runs.json/API reader now name a PR that is not in the list, and nothing ever repairs it, because none of these paths calls appendPrRef.

What the spec needs: name these three writers in the Architecture table and decide their fate — route them through appendPrRef (with a provenance that ranks below created/marker), or state explicitly that prNumber is a projection only from the first append onward and that these paths must be gated on prRefs being empty. As written, an implementer following the table ships the inconsistency.

🔴 Blocker — the legacy seed imports a regex/namer guess into a list the data model says holds authoritative associations only

The schema comment at :70:73 is emphatic: "Authoritative associations only — the PR the run CREATED and the PRs the agent DECLARED via CEZ:PR=." But the seeding rule (:96, :156, :185) derives the first entry from pullRequestUrl ?? referencedPullRequestUrl ?? prNumber — and on the majority of runs prNumber is neither created nor declared: it is a regex over the user's task text (extractTaskRefs, run.ts:769) or an LLM namer's answer (run.ts:3180). Seeded first, that guess takes the primary slot under the Phase 1 rule ("the created entry if the run has one … otherwise the first entry", :99), and Phase 2 can never displace it — only a closed entry loses the slot (:100).

Failure scenario. Task: "review pr 839, then implement it" → step 0 writes prNumber = 839. The agent later opens the implementation PR #845 and declares CEZ:PR=845; the janitor does not catch it as created (its detector needs the created-phrasing in the transcript, store.ts:748-758, and a skill that prints only the PR: #845 (link: …) reference line does not match). Under this spec the primary chip, prNumber and the task title stay #839 permanently. Today they follow the declaration — and that is a documented invariant, not an accident: store.ts:848-852 states "Marker values are authoritative for the display tier: they overwrite the regex/namer numbers".

The spec's own origin enum already carries the distinction ('legacy' vs 'marker'/'created') but the primary rule ignores it. Suggested resolution: seed a legacy entry only from evidence-backed fields (pullRequestUrl / referencedPullRequestUrl), and rank legacy below created and marker in primary selection, so a declaration still beats a guess while a genuine first PR still beats a follow-up. That preserves both #779's "first/primary wins" requirement and today's marker authority.


🔁 Backward compatibility

🟠 Major — an undeclared change to a §8-protected marker surface, asserted as "unchanged"

:170 claims "Agent protocol: unchanged. CEZ:PR= keeps meaning 'the PR this task is currently about'; only the store's reaction to it changes." BACKWARD_COMPATIBILITY.md:145-147 defines the break precisely: "Breaking: … changing what an emitted marker does (e.g. making CEZ:PR gate an action instead of steering display)." After this change, an emitted CEZ:PR=B no longer changes which PR the cockpit presents as the task's PR — while packages/cezar/src/handoff.ts:152 still instructs every agent in the collection: "Re-emit with the new number if the subject changes (e.g. you open a PR later in the task)." That instruction becomes false the day Phase 1 ships: re-emitting adds a chip and changes nothing the user sees first.

This is defensible — #779 explicitly asks for first-wins — but it must be declared, not denied. Implementation step 9 (:193) updates §3, the CHANGELOG and AGENTS.md; it touches neither §8 nor the handoff text. Add: a §8 entry recording the semantic narrowing with the issue's argument, plus either an amendment to the handoff instruction (say what re-emission now does) or an explicit statement of why it stays literally true.

🟠 Major — Phase 2 demotes in the display tier only, so prNumber and the task title keep naming the demoted PR

:100 gives a closed PR's primary slot to the earliest non-closed entry; :200 puts that rule "inside taskPrRefs from the hydrated map (never from the record)". But prNumber is persisted and computed from the Phase 1 offline rule (:102). So once Phase 2 lands, a task whose first PR was closed and replaced shows #B as the primary chip while prNumber — and therefore the task title prefix (run.ts:3233, auto-name.ts:198), the /api/runs DTO and every script reading runs.json — still says #A. The spec never states which one is authoritative when they disagree. Decide it in the Data Model: either prNumber deliberately stays the offline primary (say so, and accept the visible divergence in the title), or the title/DTO read through the same selector.

Correct on this axis, verified: additive optional field (§3's rule at BACKWARD_COMPATIBILITY.md:89"New fields MUST be optional or defaulted … the loader safeParses the whole array" — is satisfied); .catch on the field follows the existing idiom (store.ts:154, :164, :267); the documented lossy downgrade is honest and non-destructive; Phase 1 adds no route, so §2 is genuinely untouched.


🧩 What's missing

🔵 Minor — packages/cezar/src/server/pr.ts is a 7-line re-export barrel, not the draft-PR path

The Architecture table (:52) and plan step 3 (:187) send the implementer to packages/cezar/src/server/pr.ts to record the created entry. That file is, in full, a delegate: export { buildPrBody, createDraftPr } from './forge/github.ts'. createDraftPr lives at packages/cezar/src/server/forge/github.ts:1401, and the place where the created URL is actually persisted is packages/cezar/src/server/server.ts:4118-4127 (pullRequestUrl: outcome.url). Point both references there.

🔵 Minor — v1-parity.test.ts does not exist

:169 and :198 name v1-parity.test.ts as a gate for the Phase 2 route. There is no such file; the /api/v1 surface is covered by packages/cezar/src/server/versioned-surface.test.ts. (route-parity.test.ts, bc-route-inventory.test.ts and the contract-parity.*.test.ts family are all named correctly.)

🔵 Minor — the accessibility claim leans on a contract that produces N identical accessible names

:134 says every chip "stays a real link with a descriptive aria-label (the existing ReferenceChip contract)". That contract is aria-label={Open the ${kind === 'PR' ? 'pull request' : 'issue'} for ${taskTitle}} (packages/web/src/components/reference-chip.tsx:50) — it contains no number. Render a primary plus two siblings and a screen-reader user hears "Open the pull request for <task>" three times with nothing to tell them apart. The +N popover's own semantics are specified well; the chips' are not. Require the number and the primary/origin in each chip's accessible name (e.g. "Open pull request #845 (declared) for <task>"), which is a real change to ReferenceChip's props and belongs in the table beside state/muted.

🔵 Minor — inherited feedback on #779 is not accounted for

@patzick commented on the issue at 2026-08-10T11:08:43Z — after this spec was written (09:00) — that the feature is also needed for GitHub stacked pull requests, and that "easy browsing through PRs in stack is also important". The spec lists "stacked / split PR" as one of the three shapes (:18), so the association side is covered, but the ordering it specifies is first-seen declaration order with no notion of stack position or parent, and the popover is a flat list. Account for the ask explicitly — covered as-is, deferred with a reason, or a small addition to the popover's ordering — rather than leaving it silent.


📈 How can this specification be improved

⚪ Nit — skeleton drift from the repo's recent specs

Resolved assumptions sits at the end (:207); the two most recent specs (2026-08-03-auto-resume-after-usage-limit.md, 2026-07-30-session-usage-metrics.md) both place it directly after the TLDR, where a reviewer meets the open decisions before the design that rests on them. Both also carry a Research section, and the 08-03 one a Testability section; this spec has neither (its testing is distributed through the plan steps, which is good but harder to audit for coverage).

⚪ Nit — one stale line reference

:51 cites trackReferencedPrs at store.ts:779; it is at :783. Every other line reference in the document is exact.


✂️ Is this the simplest possible solution?

Yes — and pleasingly so. The list-with-projections inversion is what makes the change free at the boundary, and the three rejected alternatives (previousPrNumbers, rendering from referencedPrCandidates, a CEZ:PR+= protocol form) are each rejected for the right reason, with the referencedPrCandidates rejection correctly grounded in defect #526 — which tasks-table.ts:139-141 shows is a live guard, not history. Cap 8 reusing MAX_PR_CANDIDATES (store.ts:296) instead of inventing a budget is the right instinct, and Phase 3 being demand-gated rather than shipped unasked is exactly the discipline this repo asks for.

Two smaller simplicity observations, neither a finding:


✅ Validation

This PR changes one markdown document and six asset files (four PNGs, two illustrative HTML statics); the repository's validation gate is npm run typecheck, npm test, npm run test:unit, npm run build, npm run test:package, and none of them applies to a docs-only diff — there is no markdown or link linter configured, and no test reads .ai/specs. Per the spec-review contract, all five are listed as skipped rather than pretended.

Evidence in their place: CI ran the full suite against this exact head and is green — Unit, build, E2E, and package ✅ (4m41s), Publish npm snapshot ✅, license/cla ✅. No check is pending, and the head is MERGEABLE against main with no conflicts. The mergeStateStatus: BLOCKED is the review requirement itself, not a failure.

The spec filename follows the repo's YYYY-MM-DD-slug convention, and the committed .html mockups are correctly framed as illustrative statics rather than app code.


📋 Summary

Severity Count Findings
🔴 Blocker 2 prNumber projection contradicted by three unlisted writers; legacy seed promotes a regex/namer guess above a declaration
🟠 Major 2 §8 marker-semantics change declared as "unchanged"; Phase 2 demotion diverges from the persisted prNumber and the task title
🔵 Minor 4 wrong file for the draft-PR path; v1-parity.test.ts does not exist; chip aria-label has no number; @patzick's stacked-PR ask unaccounted for
⚪ Nit 2 skeleton drift (assumptions placement, no Research/Testability); store.ts:779:783

The two blockers share one root: the spec treats prNumber as if the list owned it, while the codebase has three other writers and one documented precedence rule (marker beats regex/namer) that the primary rule silently inverts. Resolving them is a Data Model edit plus three rows in the Architecture table — no rethink of the design.

@wojciechszyjka wojciechszyjka added changes-requested Reviewer requested changes and removed review Ready for code review labels Aug 11, 2026
- rank the primary by provenance (created > marker > legacy > derived) so a
  CEZ:PR= declaration still outranks a number the regex or the namer guessed,
  preserving the rule at store.ts:848-852
- enumerate all six prNumber writers, including workflows/run.ts:769, :1835 and
  :3179, and route them through the store helper — without them 'prNumber is a
  projection' is not true and the field silently drifts out of the list
- declare the CEZ:PR= display narrowing under BACKWARD_COMPATIBILITY.md §8 and
  schedule the matching handoff.ts instruction amendment, instead of claiming
  the agent protocol is unchanged
- state the deliberate Phase 2 divergence: closed-PR demotion is display-only,
  prNumber and the task title stay on the offline primary
- point the created-tier work at server.ts:4118-4127 (server/pr.ts is a
  re-export barrel), name versioned-surface.test.ts instead of a non-existent
  v1-parity.test.ts, fix store.ts:779 -> :783
- require per-chip accessible names (today's aria-label carries no number, so
  sibling chips read identically), and account for the stacked-PR ask from #779
- move Resolved assumptions ahead of the design, add a Testability section
@wojciechszyjka

Copy link
Copy Markdown
Contributor Author

🔍 Re-review: a task remembers every PR it has been associated with

Verdict: 🟢 Approvable — all 2 blockers, 2 majors and 4 minors from the review above are resolved in 5d333e4a + 6015a950; the 2 nits are one applied, one consciously declined. Posted as a comment rather than a formal approval: GitHub refuses approve on one's own pull request, so this PR still needs a human's approving review before it can merge — the merge-queue label records the review state, not merge permission.

Findings — how each one ended

# Finding Outcome
🔴 1 prNumber cannot be a projection while three writers assign it directly Fixed. The Proposed Solution now names six writers, not three, and a new Architecture row makes workflows/run.ts:769, :1835 and :3179 first-class Phase 1 work: they stop calling updateRun({ prNumber }) and go through a new public recordPrRef(runId, ref) with origin: 'derived'. Plan step 4 exists solely for this and carries the regression test that reproduces the drift (task text "port the fix from pr 441" + created PR #900 + a late namer answer ⇒ prNumber === 900, 441 present as non-primary).
🔴 2 The legacy seed lets a regex/namer guess outrank a declaration Fixed. origin gains a fourth value, derived, and becomes the first key of the primary ordering: created > marker > legacy > derived, earliest at within the winning tier. Pre-spec seeding takes its provenance from the field it came from (pullRequestUrlcreated, referencedPullRequestUrllegacy, a bare prNumberderived). Today's rule at store.ts:848-852 is now enforced structurally rather than contradicted, and #779's "first PR keeps priority" still holds among equals. derived entries are never rendered as extra chips and are evicted first at the cap.
🟠 3 A §8-protected marker change declared as "unchanged" Fixed. The Risks bullet is rewritten as a declared narrowing: grammar and parser untouched, display effect narrowed, with the §8 entry and the amendment to the handoff instruction at handoff.ts:152 both moved into Phase 1 step 10. Q2 in the assumptions table says the same thing in the reviewer's first screenful.
🟠 4 Phase 2 demotion diverging from prNumber and the task title Fixed by deciding it, not by hiding it. New invariant "The one deliberate divergence" plus a new Q8: prNumber stays the offline primary, Phase 2 reorders and tints chips only, and the reason is the same one Q4 gives — a title must not rewrite itself because someone closed a PR on GitHub. Plan step 14 now asserts prNumber and the title are unchanged when a closed primary is demoted.
🔵 5 Wrong file for the draft-PR path Fixed. Architecture row and plan step 3 now point at server.ts:4118-4127, noting that server/pr.ts is a re-export barrel and createDraftPr (forge/github.ts:1401) persists nothing.
🔵 6 v1-parity.test.ts does not exist Fixed. Replaced with versioned-surface.test.ts in both places (Risks and plan step 12).
🔵 7 Sibling chips would share one accessible name Fixed. New accessibleSuffix? prop on ReferenceChip, an accessibility bullet that quotes the current aria-label and its defect, and a component test in step 7 asserting three distinct accessible names while a lone chip keeps today's markup and label byte-for-byte.
🔵 8 @patzick's stacked-PR ask unaccounted for Fixed. Called out in the Problem Statement with the #779 quote and the GitHub docs link, served as navigation (every PR listed and clickable from one popover), with modelling a stack's parent/base topology written down as an explicit non-goal — cezar has no base-branch data for it and should not imply otherwise.
⚪ 9 Skeleton drift Partly applied. Resolved assumptions moved to directly after the TLDR, matching the two most recent specs, and a Testability section added. Declined: a Research section — there is no external prior art this design rests on (it is an internal record-shape change), and writing a section to satisfy a template would be filler.
⚪ 10 store.ts:779:783 Fixed.

Verification

Re-checked every claim the review had verified, against the pushed head: the six writer references (store.ts:758/:862, server.ts:4127, run.ts:769/:1835/:3179), store.ts:848-852, :296, :783, forge/github.ts:1401, reference-chip.tsx:50, versioned-surface.test.ts, and BACKWARD_COMPATIBILITY.md §8 (:137-149) — all correct as written. No implementation code was touched: this PR remains one markdown document plus its six assets, per the design-only contract.

Validation: unchanged from the review — the repository's five gate commands (typecheck, test, test:unit, build, test:package) have no bearing on a docs-only diff and no markdown or link linter is configured, so all five are recorded as skipped. CI re-runs on the new head; the previous head was green on all three checks.

The spec is ready to implement. om-auto-implement-spec should start from Phase 1 step 1 and treat step 4 (retiring the remaining prNumber writers) as non-optional — it is the step that makes the rest of the design true.

@wojciechszyjka wojciechszyjka added merge-queue Approved, ready to merge and removed changes-requested Reviewer requested changes in-progress Cezar agent is actively working this issue labels Aug 11, 2026
@wojciechszyjka

Copy link
Copy Markdown
Contributor Author

🤖 om-auto-review-pr completed: CHANGES REQUESTED → fixed → APPROVABLE. Lock released.

Specification review found 2 blockers, 2 majors, 4 minors and 2 nits; the autofix loop (this PR is the automation's own) resolved every actionable one in the spec document across two commits — 5d333e4a and 6015a950 — and the re-review above records the outcome of each. One nit (a Research section) is deliberately declined with a reason. No implementation code was added: the PR is still design-only.

Labels: reviewchanges-requestedmerge-queue. The ci-monitoring meta label does not exist in this repository, so the lock is simply released; CI on the new head is watched below and its result posted here.

⚠️ This PR cannot be formally approved by its own author — GitHub blocks self-review — so it still needs a human approving review before merge. merge-queue records the review verdict, not merge permission.

@wojciechszyjka

Copy link
Copy Markdown
Contributor Author

🤖 om-auto-review-pr — CI result

All checks green on head 6015a950, so the review verdict stands unchanged and the pipeline label stays merge-queue:

Check Result Duration
Unit, build, E2E, and package ✅ pass 4m52s
Publish npm snapshot ✅ pass 43s
license/cla ✅ pass

Head is MERGEABLE against main; the remaining BLOCKED merge state is the required approving review, which this PR's author cannot supply for themselves. Nothing further is pending from this run.

@pat-lewczuk pat-lewczuk added the in-progress Cezar agent is actively working this issue label Sep 13, 2026
@pat-lewczuk

Copy link
Copy Markdown
Collaborator

🤖 om-auto-review-pr started by @pat-lewczuk at 2026-09-13T15:55:28Z. Other auto-skills will skip this PR until the lock is released.

Note on the claim: the previous om-auto-review-pr run (@wojciechszyjka, 2026-08-11) explicitly released its lock, and no in-progress label was left behind — the only residual signal was the author's own assignee on their own PR, last touched over a month ago. Treating that as a stale/released lock, not a live one. @wojciechszyjka stays assigned; I am added alongside for this review pass.

This run exists to supply the one thing that PR could not: a formal approving-or-rejecting review from a different account, since GitHub refuses self-review. The prior verdict is treated as inherited input to be verified independently, not as a conclusion to rubber-stamp.

@pat-lewczuk pat-lewczuk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 Specification review: a task remembers every PR it has been associated with

Verdict: 🔴 Changes requested — 2 majors, 1 minor, 1 nit. No blockers.

First formal review on this PR: the two earlier passes were posted as comments by the PR's own author account, which GitHub bars from reviewing itself. I re-derived everything from the code rather than trusting those conclusions.

The grounding here is genuinely excellent, and I checked it rather than assuming it. Of ~20 code references, 19 are exact: store.ts:862 (run.prNumber = refs.pr), :296 (MAX_PR_CANDIDATES = 8), the :848-852 precedence comment, run.ts:769 / :1835 / :3179 / :3226 / :3233, server.ts:4118-4127, pr.ts (a 7-line barrel, exactly as claimed), forge/github.ts:1401, contract/runs.ts:206, tasks-table.ts:135/:171, task-quick-list.tsx:364, run-header.tsx:504, tasks-overview.tsx:632/:884, handoff.ts:152, and auto-name.ts:198. Every Phase 2 primitive it leans on is real (/github/checks at server.ts:4702, fetchGithubChecks, resolveRepoHandle, and all three named suites route-parity / versioned-surface / bc-route-inventory). The §8 reading — that the section protects what a marker does, not just its spelling — is correct and well handled.

Both majors are the same root cause, and it is the one thing the previous pass's fix introduced rather than removed.


💥 What can go wrong

🟠 Major 1 — the created tier does not write prNumber today, so making it the top-ranked provenance silently changes a persisted value

.ai/specs/2026-08-10-task-pr-reference-list.md:47 says "Every writer that today assigns run.prNumber … is not three call sites but six", and the Data Model invariant at :130 concludes prNumber's "meaning to an external reader [is] unchanged".

Two of those six do not assign prNumber at all today:

  • the janitor writes this.updateRun(runId, { pullRequestUrl: created }) (store.ts:759)
  • the draft-PR route writes pullRequestUrl: outcome.url (server.ts:4127)

Because created is also the highest provenance rank, routing them through recordPrRef gives the created PR the primary slot — and therefore prNumber — where today it has no claim on that field at all.

Concretely, using the spec's own regression scenario (:222): task text "port the fix from pr 441" → today prNumber stays 441 for the life of the run even after the task opens PR #900. After Phase 1 it becomes 900. The spec asserts exactly this outcome as the expected test result, so the behavior is intended — it is the declaration that is missing.

That matters because prNumber is a §3-protected runs.json field, and §3's own precedent requires value changes to be written down: the diffStat.repointed bullet spells out "What did change for those runs is the values". Plan step 10 (:228) commits only to a §3 bullet for prRefs plus the downgrade note. Downstream readers that move: the CEZ:TITLE= prefix (run.ts:3233 reads current?.prNumber), the run DTO, and any script reading runs.json.

Not a blocker, because no rendered surface regresses — taskReference already prefers the URL-derived number (tasks-table.ts:174-175), so the chip shows #900 today too. The divergence is confined to the persisted field and the title prefix.

To resolve: keep the behavior, and declare it — a §3 bullet stating that prNumber now tracks the created PR where it previously kept a number scraped from the task text, plus a line in the Data Model replacing the "meaning unchanged" claim. Worth a CHANGELOG line for the same reason the diffStat change got one. Alternatively, if the title prefix must stay stable, rank created below marker/derived for the projection while keeping it primary for display — but that reintroduces exactly the drift Phase 1 exists to kill, so declaring is the better trade.

🟠 Major 2 — referencedPullRequestUrl is called a projection of the list, but the plan leaves it on last-declaration-wins

The TLDR (:5) and the Proposed Solution (:44) both say prNumber / pullRequestUrl / referencedPullRequestUrl become "derived projections" of prRefs. The plan does not make two of them projections:

  • pullRequestUrl — explicitly and correctly not one (:65, first-wins, action gates read it directly).
  • referencedPullRequestUrl — plan step 2 (:220) says its "re-resolution stay[s] exactly as [it is]", i.e. still driven by markerRefs.pr, the latest declaration (store.ts:866-871).

So after CEZ:PR=A then CEZ:PR=B on a task with no created PR: the primary prRef is #A (earliest marker) and prNumber is A, while referencedPullRequestUrl still resolves to B's URL. Two persisted fields describing the same association disagree, and referencedPullRequestUrl is the one taskPrUrl falls back to (tasks-table.ts:139) — so unless taskPrRefs overrides it, the chip shows #B while prNumber says A, contradicting the spec's stated primary rule.

To resolve: pick one and say so — either recompute referencedPullRequestUrl from the primary too (and note the §3 value change alongside Major 1), or keep it last-declaration and stop calling it a projection, documenting the divergence the way :131 already does for the Phase 2 case. The second is likely right, since the field's name means "the PR most recently declared".

📋 What's missing

🔵 Minor — the six-writer enumeration is the reason Major 1 went unnoticed

:47 and the Risks bullet at :201 ("the five other places that also write that field") both assert a fact the code contradicts, and the spec leans on that enumeration as its proof that the projection holds. The Architecture table (:69-:71) describes each writer's actual job correctly, so an implementer following the table will build the right thing — but the argument above it should say "every writer that must now record an association" rather than "every writer that assigns prNumber", and distinguish the four that write the field today from the two that write pullRequestUrl.

📝 Improvements

⚪ Nit — one line reference is off by two

:74 and :164 cite reference-chip.tsx:50 for the accessible name; the aria-label is at :48. The observation itself — that today's accessible name carries no number, so sibling chips would read identically — is correct and is one of the sharpest points in the spec.


✅ Checked and found sound

  • 🔁 Backward compatibility: the §8 narrowing of CEZ:PR='s effect is declared rather than asserted away, with the matching handoff.ts:152 amendment as Phase 1 work — that is the right call, and §8's "changing what an emitted marker does" clause confirms it was required. runs.json additivity, .catch([]) degradation and the lossy-but-safe downgrade round-trip are all correct; .catch([]).optional() is the right operator order to keep absent records absent.
  • 🎯 Simplicity: the five rejected alternatives are argued on evidence, not taste — in particular the refusal to merge referencedPrCandidates into a rendered list, which would rebuild #526.
  • Inherited feedback: the four earlier findings (blockers on the projection and the legacy seed, the §8 semantics, the Phase 2 title divergence) are genuinely resolved in 5d333e4a + 6015a950 — I verified each against the spec text, not just the claim. The one declined nit (a Research section) I concur with: the Alternatives analysis covers that ground.

🧪 Validation gate

Docs-only diff (one markdown file + 4 assets + 2 HTML mockups), so every configured command is non-applicable — the repo defines no markdown linter or link checker:

Command Result
npm run typecheck ⏭️ skipped — no TypeScript in the diff
npm test / npm run test:unit ⏭️ skipped — no code under test
npm run build ⏭️ skipped — nothing to compile
npm run test:package ⏭️ skipped — package surface untouched

Evidence instead: CI ran the full gate green on head 6015a950 (Unit/build/E2E/package, npm snapshot, license/cla all ✅), and no check is pending.


🤖 This PR is autofix-eligible on this run, so I am fixing these findings in the spec document myself rather than handing them back — no action needed from @wojciechszyjka. A re-review follows on the updated head.

@pat-lewczuk pat-lewczuk added changes-requested Reviewer requested changes and removed merge-queue Approved, ready to merge labels Sep 13, 2026
…RequestUrl split

Review of #839 found two claims the codebase does not support, both stemming
from the same imprecision in the writer enumeration.

The created tier (janitor `store.ts:758`, draft-PR route `server.ts:4127`)
writes `pullRequestUrl`, never `prNumber`. Enrolling it as the top-ranked
`created` provenance therefore makes `prNumber` start naming a created PR
where it previously kept a number scraped from the task text — a real value
change to a BC §3-protected field that the spec asserted was "unchanged".
Declared now under Data Model, Compatibility with runs.json, Risks, the
Edge Cases table and plan steps 3 and 10, following the precedent §3 set for
the `diffStat.repointed` value shift.

The TLDR and Proposed Solution also called all three scalars projections of
the list, while plan step 2 leaves `referencedPullRequestUrl` on
last-declaration-wins. Only `prNumber` becomes a projection; the other two
are preserved deliberately so a rollback still finds today's answer. The
resulting divergence is now stated, with tests pinning it in steps 2 and 6.

Also corrects `reference-chip.tsx:50` to `:48`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@pat-lewczuk pat-lewczuk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 Re-review: a task remembers every PR it has been associated with

Verdict: 🟢 Approved — all 4 findings from the review above are resolved in 2f657000.

This is the formal approving review the PR has been waiting for since 2026-08-11. The two earlier passes were posted as comments because GitHub bars an author from reviewing their own PR; reviewing from a different account is the only thing that was actually missing, and the merge block should now clear.

Findings — how each one ended

# Finding Outcome
🟠 1 prNumber's persisted value changes for created-PR runs, undeclared under §3 Fixed. The behaviour is kept — it is the more truthful answer — and now declared in five places: a new Data Model invariant with the 441 → 900 worked example, a Compatibility with runs.json bullet citing the diffStat.repointed precedent and stating that historical records are not backfilled, a Risks bullet tying it to the drift fix it comes from, an Edge Cases row, and plan steps 3 and 10 (§3 bullet + a CHANGELOG line naming the task-title prefix as the user-visible consequence).
🟠 2 referencedPullRequestUrl called a projection while step 2 leaves it last-declaration-wins Fixed, resolving toward not a projection — the right call, since the field's name means "most recently declared" and keeping its rule is what makes rollback free. The TLDR and Proposed Solution no longer claim all three scalars are projections; a new bullet states which read moves (the chip's fallback, now a list read) and which do not. The divergence is pinned by tests in steps 2 and 6 rather than left to be discovered.
🔵 3 The six-writer enumeration asserted all six write prNumber Fixed. Split into "four already assign run.prNumber" and "two assign pullRequestUrl and never touch prNumber", with the note that the second group is the behavioural change. The Risks bullet is reworded to "record an association". This was the root cause of finding 1, so correcting it is what made the omission visible.
⚪ 4 reference-chip.tsx:50:48 Fixed in both occurrences.

🔁 Correction to my own review

I wrote the janitor's write as store.ts:759. It is at :758 — the spec had it right, and a fix I briefly applied has been reverted. The substance is unaffected: that line is this.updateRun(runId, { pullRequestUrl: created }), which writes pullRequestUrl and not prNumber, which is what finding 1 rests on.

🧪 Validation

Docs-only diff, so the same four commands remain non-applicable (no markdown linter or link checker is configured) — unchanged from the review above. Line references in the amended text were re-verified against the worktree: store.ts:758 / :848-852 / :854 / :862 / :296, server.ts:4118-4127, run.ts:769 / :1835 / :3179 / :3233, tasks-table.ts:139 / :174-175, reference-chip.tsx:48. The new tasks-table.ts:139 and :174-175 citations I added are correct.

CI on the previous head was fully green; the new head carries a markdown-only change, and its run is reported below.


Merge readiness: approved and in merge-queue. skip-qa is correct — there is no runtime behaviour to exercise. Note this branch is ~52 commits behind main, though MERGEABLE with no conflicts; a squash merge will be clean.

🤖 om-auto-review-pr — review + autofix, 1 fix iteration.

@pat-lewczuk pat-lewczuk added merge-queue Approved, ready to merge and removed changes-requested Reviewer requested changes labels Sep 13, 2026
@pat-lewczuk

Copy link
Copy Markdown
Collaborator

🏷️ om-auto-review-pr — label rationale

  • 🚀 merge-queue — the specification review passed on the updated head; every finding from this pass is resolved and the PR now carries a formal approving review from an account other than the author's, which is what it was blocked on.
  • 📚 documentation — the diff is one spec document plus its mockups and screenshots; no application code changes on this branch.
  • ⏭️ skip-qa — kept: a design document has no runtime behaviour to click through, so manual QA would have nothing to exercise.
  • 🔹 priority-medium — kept: the underlying defect (Task keeps only the last PR link — original PR becomes unreachable after a follow-up #779) silently drops a PR link the user relied on, which degrades discoverability rather than blocking any run, and this PR only designs the fix.
  • 🟢 risk-low — kept: merging a design document cannot break a consumer, and the one behavioural change the spec now declares (prNumber tracking a created PR) is argued and bounded in the document rather than shipped here.

in-progress is released separately as this run's lock. ci-monitoring is not defined in this repository, so the CI follow-up is reported in a comment instead.

@pat-lewczuk pat-lewczuk removed the in-progress Cezar agent is actively working this issue label Sep 13, 2026
@pat-lewczuk pat-lewczuk removed their assignment Sep 13, 2026
@pat-lewczuk

Copy link
Copy Markdown
Collaborator

🤖 om-auto-review-pr completed: APPROVED. Lock released.

Specification review found 2 majors, 1 minor and 1 nit; the autofix loop resolved all four in the spec document in one iteration (2f657000), and the re-review above records the outcome of each. No implementation code was added — the PR is still design-only.

What this run added that the earlier passes could not: a formal approving review. The two reviews on 2026-08-11 were posted as comments because GitHub refuses to let an author review their own PR, so reviewDecision sat at REVIEW_REQUIRED for a month. It now reads APPROVED.

The findings were not a re-run of the earlier ones. Enrolling the created tier as the top-ranked provenance — the fix for the earlier pass's first blocker — is what makes prNumber start naming a created PR where today it keeps a number scraped from the task text. That is a value change to a BACKWARD_COMPATIBILITY.md §3-protected field, and the spec asserted it was "unchanged"; it is now declared, with no backfill of historical records. The companion finding was that the TLDR called all three scalars projections of the list while the plan leaves referencedPullRequestUrl on last-declaration-wins.

Labels: changes-requestedmerge-queue. ci-monitoring is not defined in this repository, so the lock is simply released; CI on the new head 2f657000 is being watched and its result is posted below. Assignee returned to @wojciechszyjka alone.

@pat-lewczuk

Copy link
Copy Markdown
Collaborator

🤖 om-auto-review-pr — CI result

All checks green on head 2f657000 (the autofix commit), so the approving review above stands unchanged and the pipeline label stays merge-queue:

Check Result
Unit, build, E2E, and package ✅ pass
Publish npm snapshot ✅ pass
license/cla ✅ pass

⚠️ The PR is still BLOCKED, and it is no longer the missing review

reviewDecision is now APPROVED, the head is MERGEABLE, there are no unresolved review threads, and every check passes — yet mergeStateStatus stays BLOCKED. I re-queried after CI finished, so this is not a stale computation.

The main ruleset (16805903, active, last updated 2026-07-24) requires one approving review — satisfied — and also sets:

"require_extra_approval_for_unattributed_changes": true

The explanation most consistent with the evidence: the newest commit is mine (2f657000), I am also the only approver, and that commit carries a Co-Authored-By: trailer for an address with no GitHub account. Under that clause an extra approving review is required. In other words, the autofix that resolved the findings is itself what now needs a second signature — by pushing a fix I became a contributor, so my own approval no longer covers the PR alone.

I am flagging this rather than working around it: amending the commit to drop the trailer would need a force-push, which I will not do unattended.

Three ways forward, in order of least disruption:

  1. A maintainer on the bypass team merges it. The ruleset has a Team bypass actor with bypass_mode: always, so this needs nothing else.
  2. Any second approving review from an account other than @wojciechszyjka (the author, who cannot self-approve) and @pat-lewczuk (me) clears it without touching history.
  3. Drop the co-author trailer by amending 2f657000 and force-pushing — only with an explicit go-ahead.

Nothing further is pending from this run: the review is submitted, the findings are fixed and pushed, and CI is green.

@pat-lewczuk
pat-lewczuk merged commit d37b21c into main Sep 14, 2026
3 checks passed
@pat-lewczuk
pat-lewczuk deleted the spec/task-pr-reference-list branch September 14, 2026 07:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation merge-queue Approved, ready to merge priority-medium Ordinary bug or feature risk-low Isolated, low blast radius skip-qa Low risk, QA not required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants