Skip to content

feat(team-pulse): include in-flight work in AI summary (GLOOK-7)#44

Merged
msogin merged 10 commits into
mainfrom
spec/glook-7-team-pulse-inflight
May 20, 2026
Merged

feat(team-pulse): include in-flight work in AI summary (GLOOK-7)#44
msogin merged 10 commits into
mainfrom
spec/glook-7-team-pulse-inflight

Conversation

@msogin
Copy link
Copy Markdown
Contributor

@msogin msogin commented May 20, 2026

Summary

Implements GLOOK-7: extend the Team Pulse AI summary to also describe what a team is currently working on — open PRs and unmerged branches — woven into the existing narrative (no new headings, no UI work).

Spec: docs/superpowers/specs/2026-05-20-glook-7-team-pulse-inflight-design.md
Plan: docs/superpowers/plans/2026-05-20-glook-7-team-pulse-inflight.md

What the LLM gets now

A new IN-FLIGHT WORK context block, fed by aggregateInflight() over the existing unmerged_prs and unmerged_commits snapshot tables:

IN-FLIGHT WORK (snapshot at report time):
- Open PRs: 12 (2 draft, 10 ready); oldest 8d; +32145/-10238 lines
- Unmerged branches: 14 branches, 47 commits
- In-flight by repo:   sfcc-cartridge (4), api-sdk-nodejs (3), connectors-mt-service (2)
- In-flight by author: @sduiev-smartling (3), @PavelLoparev (3), @ChernovDmytro (2), @akhodyka-smartling (2), @vsolovei-smartling (2)

Plus one prompt rule instructing the model to weave it into Team Focus and Alerts (not as a new heading). Smoke-tested on real Integrations data; the model picked it up naturally:

"@ChernovDmytro has 2 in-flight PRs but no current commits"
"Oldest PR open 8 days needs attention"
"Large in-flight diff total (+32k/-10k lines) across 12 ready PRs needs review bandwidth"

Architecture

  • Pure client-of-existing-data: no new fetch, no new API endpoint. The aggregations come from snapshot tables (unmerged_prs, unmerged_commits) that the report runner already populates.
  • New pure aggregateInflight(prRows, commitRows, now) function with 6 unit tests covering empty input, draft/ready counts, top-N with alphabetical tiebreak, oldest_days math, and distinct-branch counting (including branch IS NULL rows).
  • Existing prompt builder gains a single INFLIGHT_BLOCK placeholder; renders to either a formatted block or the literal (none) (no {{X}} orphans in the prompt).
  • Cache invalidated via a new prompt_version column on team_pulse_summaries. Old v1 rows are preserved but silently bypassed by the new v2-inflight lookup; first access after deploy regenerates with the new prompt.

Files

  • New src/lib/__tests__/unit/team-pulse-inflight.test.ts — 8 unit tests (aggregator + prompt builder).
  • Modified src/lib/team-pulse/data.tsInflight* types, aggregateInflight() pure helper, fetchInflight() SQL helper, wired into extractTeamPulseData().
  • Modified src/lib/team-pulse/prompt.tsrenderInflightBlock() + INFLIGHT_BLOCK placeholder.
  • Modified src/lib/team-pulse/service.tsPROMPT_VERSION = 'v2-inflight' in cache SELECT and INSERT.
  • Modified prompts/team-pulse-system.txt — placeholder + one rule.
  • Modified src/lib/db/mysql.ts, src/lib/db/sqlite.tsprompt_version VARCHAR(16) NOT NULL DEFAULT 'v1' migration on team_pulse_summaries.

Tests

  • npm test → 63 suites / 656 tests pass.
  • npx tsc --noEmit -p tsconfig.json → clean.
  • Local smoke confirmed on report 129ac0cc… / Integrations team: cache row written with prompt_version = 'v2-inflight', narrative correctly references in-flight signals.

Known caveats (documented in spec)

  • oldest_days derived from pr_updated_at (last activity) rather than pr_created_at (true PR age). Trivial to swap later by changing one column name in the SELECT.
  • In-progress Jira issues intentionally out of scope for v1 (only signals already in the report snapshot are used).
  • Devs in multiple teams contribute to each team's totals (no de-duping in v1). Matches existing team-rollup behavior.

Test plan

  • Open /report/<id>/team, pick a team filter — TeamPulseCard generates a fresh summary.
  • Markdown still uses existing headings (Activity Changes / Silent Members / Team Focus / Alerts) — no IN-FLIGHT heading.
  • Narrative under Team Focus and/or Alerts mentions in-flight signals (open PR counts, oldest PR age, large in-flight diffs).
  • SELECT prompt_version FROM team_pulse_summaries WHERE team_name = '<X>' returns v2-inflight for fresh rows.
  • For a team with zero in-flight work, the prompt block reads IN-FLIGHT WORK (snapshot at report time): (none) and the LLM doesn't invent in-flight content.

🤖 Generated with Claude Code

msogin and others added 9 commits May 20, 2026 13:13
Captures the design for extending the Team Pulse AI summary to include
in-flight work signals (open PRs, unmerged branches) per team. Key
decisions:

- Narrative integration only (no new headings, no UI badges). LLM
  weaves in-flight context into the existing Team Focus and Alerts
  sections.
- Data sourced from existing snapshot tables (unmerged_prs,
  unmerged_commits) populated by the report runner. No new fetch.
- In-progress Jira intentionally out of scope for v1 (only signals
  already in the report snapshot are used).
- Cache invalidation via a new prompt_version column on
  team_pulse_summaries; old cached summaries are silently bypassed.

Co-Authored-By: Claude <noreply@anthropic.com>
8 bite-sized tasks: schema migration, type stub, pure aggregator with
tests, SQL fetcher, prompt template edit, prompt builder with tests,
cache invalidation via PROMPT_VERSION, local smoke.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Defaults to an empty struct so the existing pipeline keeps compiling.
Real aggregation lands in the next commit.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…OOK-7)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Old cached summaries (prompt_version='v1') are silently bypassed by the
new SELECT and regenerate with the in-flight-aware prompt.

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor Author

@msogin msogin left a comment

Choose a reason for hiding this comment

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

9 inline comments covering correctness, type safety, test coverage, and prompt rendering. All findings are non-blocking except where noted.

Comment thread src/lib/team-pulse/data.ts Outdated
commitRows: InflightCommitRow[],
now: Date,
): Inflight {
if (prRows.length === 0 && commitRows.length === 0) return EMPTY_INFLIGHT;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🟡 warning

aggregateInflight returns EMPTY_INFLIGHT directly by reference when both inputs are empty. The nested arrays (by_author: [], by_repo: []) are the same instances on the module constant — any caller mutating the returned struct would corrupt all future zero-input calls.

Fix — return a spread clone:

if (prRows.length === 0 && commitRows.length === 0) return {
  open_prs: { ...EMPTY_INFLIGHT.open_prs, by_author: [], by_repo: [] },
  unmerged_branches: { ...EMPTY_INFLIGHT.unmerged_branches },
};

Or Object.freeze the constant (deep-freeze needed for nested arrays).

];
const out = aggregateInflight([], commits, NOW);
expect(out.unmerged_branches.total_commits).toBe(5);
expect(out.unmerged_branches.total_branches).toBe(4);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🟡 warning

The test passes but doesn't cover the key deduplication behaviour for null-branch commits. The implementation uses branchKeys.add(`${c.repo} ${c.branch ?? ''}`), which means two null-branch commits from the same repo both map to "api " and are deduplicated into one branch slot — not "one per row." The fixture only has one null-branch commit per repo so this edge case goes untested.

Suggested additions:

  1. Add a comment on the null-branch row: // branchless commits in the same repo deduplicate to one branch slot
  2. Add a second { github_login: 'bob', repo: 'api', branch: null } commit row and assert total_branches stays at 4 (not 5), confirming deduplication.

Comment thread src/lib/team-pulse/prompt.ts Outdated
});
}

function renderInflightBlock(i: { open_prs: { total: number; draft: number; ready: number; oldest_days: number; lines_added: number; lines_removed: number; by_author: { login: string; count: number }[]; by_repo: { repo: string; count: number }[] }; unmerged_branches: { total_branches: number; total_commits: number } }): string {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🔵 suggestion

renderInflightBlock uses a 130-character inline structural type instead of the exported Inflight interface from ./data. This bypasses TypeScript's structural guard: if Inflight gains a new field, this function won't be required to handle it.

Fix:

import type { Inflight } from './data';
// ...
function renderInflightBlock(i: Inflight): string {

[reportId, teamName, org, summary, JSON.stringify(health)],
`INSERT INTO team_pulse_summaries (report_id, team_name, org, summary_text, health_json, prompt_version)
VALUES (?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE summary_text = VALUES(summary_text), health_json = VALUES(health_json), prompt_version = VALUES(prompt_version), generated_at = NOW()`,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🟣 question

The spec and commit message say "Old v1 rows survive as historical records but never satisfy the new SELECT." However, ON DUPLICATE KEY UPDATE fires on a (report_id, team_name) conflict and overwrites the v1 row — updating prompt_version from v1 to v2-inflight in place. The cache invalidation behaviour is correct (first SELECT misses → regenerates → writes v2), but the historical preservation claim is false.

Two options:

  • Accept overwrite semantics (current code, simpler) → update the spec/commit message comment.
  • Preserve old rows → change the UNIQUE constraint to (report_id, team_name, prompt_version) so v1 and v2 rows coexist.

Which was intended?

ON DUPLICATE KEY UPDATE summary_text = VALUES(summary_text), health_json = VALUES(health_json), generated_at = NOW()`,
[reportId, teamName, org, summary, JSON.stringify(health)],
`INSERT INTO team_pulse_summaries (report_id, team_name, org, summary_text, health_json, prompt_version)
VALUES (?, ?, ?, ?, ?, ?)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🟣 question

ON DUPLICATE KEY UPDATE is MySQL-specific syntax. SQLite uses INSERT OR REPLACE or ON CONFLICT DO UPDATE SET. Does the db wrapper (or the SQLite adapter in sqlite.ts) translate this automatically? If not, the cache INSERT will fail silently on SQLite dev environments. The pre-PR INSERT had the same pattern — just confirming this is already handled rather than a new gap.

const memberPlaceholders = teamMembers.map(() => '?').join(',');

const [prRows] = await db.execute(
`SELECT github_login, repo, is_draft, pr_additions, pr_deletions, pr_updated_at
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🟣 question

The spec's data-shape section references unmerged_prs.author_login and unmerged_commits.author_login, but the SQL here uses github_login. One of these is wrong. Since there are no integration tests covering this path, a column name mismatch would only surface at runtime.

Can you confirm github_login is the actual column name in both unmerged_prs and unmerged_commits? (If so, the spec should be updated to match.)


PER-MEMBER DATA:
{{MEMBER_DATA}}
{{INFLIGHT_BLOCK}}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🔵 suggestion

{{MEMBER_DATA}} and {{INFLIGHT_BLOCK}} are on adjacent lines with no blank line between them. When both render with content, the IN-FLIGHT header will immediately follow the last member-data line with no visual separator in the rendered prompt.

Suggested:

{{MEMBER_DATA}}

{{INFLIGHT_BLOCK}}

Comment thread src/lib/team-pulse/data.ts Outdated
const oldest_days = oldest_ms > 0 ? Math.floor(oldest_ms / 86_400_000) : 0;

const sortDesc = <T extends { count: number }>(items: T[], tieKey: (x: T) => string) =>
items.sort((a, b) => b.count - a.count || tieKey(a).localeCompare(tieKey(b)));
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🔵 suggestion

Array.prototype.sort mutates the array in place. Safe here because callers always spread before passing ([...prByAuthor], [...prByRepo]), but the helper gives no indication it mutates. A future caller passing a non-copy could be surprised.

Consider sorting inside the helper: [...items].sort(...) — or rename to sortedDesc to signal it returns a value.

});
});

function makeData(inflight: Inflight): TeamPulseData {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🔵 suggestion

makeData is declared between the two describe blocks rather than at the top of the file. While valid (function declarations are hoisted), it reads as if it belongs only to the second describe. Moving it above both describes (just below const NOW) would improve readability.

- 🟡 EMPTY_INFLIGHT shared-reference risk → replace the module-level
  constant with a `emptyInflight()` factory that returns a fresh object
  on every call. Removes the silent mutation hazard.

- 🟡 Branch-dedup test gap → previously claimed "one branch slot per
  row" while the implementation deduplicates `(repo, branch)` keys. Add
  a second null-branch commit in the same repo, assert total_branches
  stays at 4 (not 5), and clarify the comment.

- 🔵 renderInflightBlock now takes the exported `Inflight` interface
  instead of a 130-char inline structural type. Future fields on
  Inflight will be caught at type-check time.

- 🔵 Blank line added between {{MEMBER_DATA}} and {{INFLIGHT_BLOCK}}
  in the prompt template so the IN-FLIGHT header isn't visually butted
  against the last member-data line.

- 🔵 `sortDesc` renamed to `sortedDesc` and explicitly copies before
  sorting; no longer mutates input. Callers' spreads still work, but
  the helper no longer pretends to be pure-by-convention.

- 🔵 makeData moved to the top of the test file (just below NOW),
  TeamPulseData / Inflight imports consolidated into a single line.

- 🟣 Spec corrected: the SQL uses `github_login` (not `author_login`)
  in both `unmerged_prs` and `unmerged_commits`. The spec text now
  matches the actual schema.

- 🟣 Spec corrected: `ON DUPLICATE KEY UPDATE` overwrites the old row
  in place (the UNIQUE KEY is still `(report_id, team_name)`), so we
  do NOT preserve an audit trail of old summaries. The previous claim
  was wrong; accept overwrite semantics for simplicity.

- 🟣 Confirmed (no code change): the SQLite wrapper translates
  `ON DUPLICATE KEY UPDATE` automatically per CLAUDE.md gotchas
  section. Pre-existing pattern, unchanged here.

Co-Authored-By: Claude <noreply@anthropic.com>
@msogin msogin merged commit 4e12e09 into main May 20, 2026
1 check passed
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