feat(config): per-repo Penpot team mapping (#255) #256
No reviewers
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks!256
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "boss/255"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Adds an optional top-level
penpot:block toconfig/agents.jsonthat pins each watched repo to a specific Penpot team.designertasks no longer have tolist_teams+ guess — the pinnedteam_id/team_nameis threaded through the task prompt as aPenpot team: <name> (<id>)line, whichskills/design-implement.mdstep 2 reads to callmcp__penpot__list_files/create_filewith the pinned id directly.Motivation: 2026-04-21 designer task
ce585c20stalled 10 turns ($0.54) onlist_teams— it received a transit-json-encoded team list it couldn't turn into action. Pinning the team in config removes the guess entirely.PenpotConfig/PenpotTeamMapping/PenpotTeamResolutionin@claude-hooks/shared.parsePenpotConfig+resolvePenpotTeaminwebhook-config.tswith UUID validation (generic 8-4-4-4-12 hex — Penpot ids aren't strict v4) and a non-fatal warn onteam_by_repokeys not inrepos:.main.tsstartup usescfg.penpot?.base_urlinstead of the hardcodedhttps://design.jacquin.app.agent-runner.runAgentTaskresolves the team forpenpot_mcp: trueagents and injects it via the extendedbuildPromptopts; prompt stays byte-identical for everyone else.GET /agents(+penpot_mcpboolean per agent) andGET /foreman/configecho the effectivepenpot:block.skills/design-implement.mdstep 2 rewritten: pinned team first,list_teamsfallback with a visible warning comment.config/agents.jsongets the initial block;CLAUDE.mddocuments the add-a-new-repo runbook.Missing
penpot:block is back-compat: the loader storesnull, legacypenpot_mcp: trueagents keep working with the oldlist_teamspath.Closes #255
Test plan
bun x turbo run typecheckgreen across all three workspaces.bun x biome check .clean.bun test src/webhook-config.test.ts src/agent-runner.test.ts— 132 new/existing pass (2 pre-existingtoken_economybaseline failures unrelated to this PR — see commit0767024).parsePenpotConfigcovers: absent block → null, malformed UUID rejected, missingbase_urlrejected, malformedteam_by_repokey rejected, unknown repo key warns-but-parses.resolvePenpotTeamcovers: explicit entry wins, default fallback withteam_name: null, null when neither is set.buildPromptcovers: team line rendered with<name> (<id>),(unnamed)when only default, no line whenpenpotTeamabsent.🤖 Generated with Claude Code
Review — APPROVED
CI: ✅ green (
fix(ci): update token_economy testsrun #1930, sha1f4d15c, 4m9s).Acceptance criteria — all met
config/agents.jsongetspenpot:block withbase_url,default_team_id,team_by_repobase_urlreplaces hardcodedhttps://design.jacquin.appinmain.tsstartupdefault_team_idfallback when repo has no explicit entrypenpot:block → loader storesnull, legacylist_teamspath unchangedwebhook-config.tsparses + validates (parsePenpotConfig, UUID shape, unknown-repo-key warning)agent-runner.tsinjectsPenpot team: <name> (<id>)line forpenpot_mcp: trueagents/foreman/config+GET /agentsecho the effectivepenpot:blockskills/design-implement.mdstep 2 updated: pinned team first,list_teamsfallback with warningCLAUDE.md§"Penpot MCP auth" gains the per-repo team config runbookwebhook-config.test.ts(10parsePenpotConfigcases + 4resolvePenpotTeamcases + 3 integration cases)agent-runner.test.ts(buildPromptteam-line cases)Code walkthrough — no issues found
packages/shared/src/penpot.ts— Three clean, well-documented interfaces.PenpotTeamResolution.team_name: string | nullcorrectly captures the "default-fallback has no name" case. Exported frompackages/shared/src/index.ts.webhook-config.ts—parsePenpotConfig/resolvePenpotTeamForRepo— Validation is thorough:base_urlrequired, UUID shape checked on bothdefault_team_idand everyteam_by_repo[*].team_id, malformedowner/namekeys rejected, unknown repo keys warn-but-parse (non-fatal, right call).resolvePenpotTeamForReporeads the frozen module-level config singleton, correct pattern for this codebase.agent-runner.ts—buildPrompt—penpotTeam?: PenpotTeamResolution | nullis optional so callers that don't pass it get byte-identical output — confirmed by the "no line whenpenpotTeamabsent" test. Renders(unnamed)whenteam_nameis null (default-fallback path). Team resolution gated onpenpot_mcp: trueat dispatch time.skills/design-implement.md— step 2 — Pinned-team path callsmcp__penpot__list_fileswith the injectedteam_idto reuse an existing file, then falls back tocreate_file. Legacy path logs⚠ no team pinned — used fallback <name>in the handoff comment so the operator sees the signal without the agent silently eating the gap. Clean.config/agents.json— Thecharles/claude-hooksteam_by_repoentry intentionally hasteam_id == default_team_id— this is correct because the explicit entry is what provides the human-readableteam_name: "peon-manager"for the prompt line; the baredefault_team_idalone would only get(unnamed).foreman.ts/main.ts—penpot?: PenpotConfig | nullthreaded throughForemanHttpContextand surfaced in theGET /foreman/configresponse.main.tsusescfg.penpot?.base_urlinstead of the hardcoded URL.Test coverage — All branches exercised: absent block, minimal block, full block, bad UUID (both fields), missing
base_url, malformed key shape, unknown-repo warning, explicit-entry wins, default fallback with null name, null when neither set, module-state read after load, malformed UUID tripsloadWebhookConfig.Nothing to request changes on.