Skip to content

aggregator(projects): isProjectPoolItem never checks labels — a string or non-string-element labels entry passes the feed gate and crashes executionStrip on every board/item render #2418

Closed

Description

@dezren39

Problem

The fleet feed's Project-pool gate never checks labels (or several other fields its consumers treat as typed), so one malformed pool entry passes validation, is converted verbatim into a GhItem, and crashes the board/item renders that call .some/.toLowerCase on its labels.

  • ProjectPoolItem (pm-stack/aggregator/src/model.ts:55-82) declares labels?: string[], required url/projectUrl/contentKind, and status/area/horizon/kind as string | null.
  • isProjectPoolItem (pm-stack/aggregator/src/projects.ts:182-195) — the only filter applied to the feed's projects array, at src/sources/live.ts:703 (items()) and :742 (projects()) — checks only id, ref, repo, number, issueUrl, title, and state. It never checks labels, url, projectUrl, contentKind, status/area/horizon/kind, author, or updatedAt.
  • poolItemToGhItem (projects.ts:197-215) passes the unchecked values through verbatim: labels: p.labels ?? [] (:205) only replaces null/undefined, so a string, number, or non-string-element array survives as a GhItem.labels that is not a string[].
  • Consumers dereference labels as a string array: executionStrip does item.labels.some((l) => IMPLEMENTING_LABELS.has(l.toLowerCase())) (src/derive.ts:48) and derive.ts:776 does i.labels.some((l) => /blocked/i.test(l)); executionStrip is called on the board and item-render paths (src/render.ts:1334 stripHtml, :1568 renderItem, which also does i.labels.join(", ") at :1573).

Evidence (verified this run on main 5a34fa70)

Laptop Bun probe at ~/git/fleet/probe-2026-10-29-laneB-poolgate/ importing the real src/projects.ts, src/program.ts, and src/derive.ts (projects.ts md5 55c76253fa2fced0858e115f97cc8155, identical to main):

  • Control: a well-formed pool item passes isProjectPoolItem and executionStrip returns it as queued (1).
  • labels: "blocked" (string) → isProjectPoolItem returns true; poolItemToGhItem keeps the string; real executionStrip throws TypeError: item.labels.some is not a function.
  • labels: [42] → gate returns true; real executionStrip throws TypeError: l.toLowerCase is not a function.
  • An entry with url, projectUrl, and contentKind deleted entirely → gate still returns true; poolItemToGhItem(...).kind is undefined (broken badge/link rendering, silently).
  • updatedAt: 12345 (number) → gate returns true and the resulting GhItem.updatedAt is a number, despite the string contract every date consumer assumes.

Why it matters

The pool is carried in the fleet feed — a file written by a separate collector on another host — and isProjectPoolItem is the single line of defence between that file and the dashboard's item resolution (items() feeds the board, roadmap, program, and item pages). The collector's own parser (projects.ts:130) carefully filters labels to strings, which shows the intended contract; the feed-side gate is the one that must enforce it when the feed is older, hand-edited, partially written, or produced by a different version. Today one bad entry does not degrade that entry — it throws inside shared render helpers and takes down every view that resolves that ref.

Acceptance criteria

  • isProjectPoolItem rejects an entry whose labels is present but not an array of strings (string/number/object labels, or an array containing non-string elements), and rejects entries missing the required string fields it currently skips (url, projectUrl, contentKind checked against its GhKind values).
  • status/area/horizon/kind are checked as string-or-null and updatedAt/author, when present, as strings — or the triage explicitly pins a narrower set with the consumer that is safe for each unchecked field.
  • A rejected entry is dropped by the live source's existing filter (other pool entries still resolve and render); it never reaches poolItemToGhItem/executionStrip.
  • Regression tests (aggregator projects/live coverage) pin: labels:"blocked", labels:[42], and missing-url entries fail the gate; a well-formed entry (including one with labels absent) still passes unchanged.

Dedup

Activity

  1. developing-today commented on Oct 29, 2026

    @developing-today
    Contributor
  2. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    app/aggregatorPackage/app: aggregator (pm-stack in this repo)
    effort/xsTrivial: expected under 30 minutes of work
    on Oct 29, 2026
  3. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (new-issue-creator-code-b)
    • Session / ID: cron new-issue-creator-code-b run 2026-10-29T20:20:00Z (15:20 CDT)
    • Found via: filed this run
    • Task: Triage the isProjectPoolItem gate gap: pin which unchecked ProjectPoolItem fields must be validated (labels as string[] first) and route a small_fix packet.
    • Expectation: effort/xs triage — done looks like the gate rejecting malformed labels/required-field entries with regression tests, well-formed pool entries unchanged.
    • Links: queue packet packet-2418-triage.json (repo-health-code-pilot)
  4. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  5. 21 remaining items

  6. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — picked up
    Agent: repo-worker-code (OpenChamber scheduled lane)
    Task: repo-health-code-pilot task 5327 (small_fix)
    At: 2026-11-02T05:43:37Z
    State: Reclaimed from the awaiting_ci checkpoint with origin=openchamber-scheduled-lane. Fresh metadata confirms #2418 OPEN and task-bound PR #4181 OPEN at 6995c97. The required test is QUEUED; Codex review is COMMENTED. No PR/worktree changes.
    Next: park 20 minutes; resume by checking current CI/review state fresh.

  7. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — parked
    Agent: repo-worker-code (OpenChamber scheduled lane)
    Task: repo-health-code-pilot task 5327 (small_fix)
    At: 2026-11-02T05:44:02Z
    Reason: Required test on PR #4181 remains QUEUED. Parked 20 minutes; per protocol, ending this launch rather than waiting on CI. No PR/worktree changes.
    Resume: re-read PR/check/review state fresh; merge only when required checks and targeted verification are complete. Labels: queue/parked, status/parked, work/assigned.

  8. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 2, 2026
  9. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  10. added 2 commits that reference this issue on Nov 2, 2026
  11. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

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

Metadata

Metadata

Assignees

Labels

app/aggregatorPackage/app: aggregator (pm-stack in this repo)app/pm-stackpm-stack (Pad/Grist) in developing-today/codeeffort/xsTrivial: expected under 30 minutes of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions