Skip to content

Why github-sdlc-planning exists and how it's built

github-sdlc-planning is the marketplace’s foundation plugin: Issues, native sub-issues, Projects v2, Milestones, and Discussions. Every other domain plugin in this marketplace either depends on it directly or transitively:

  • github-pull-requests depends on github-sdlc-planning (its manifest declares the edge) — PR classification and PR-to-project coupling reuse planning’s MIF reader and board tools.
  • github-bug-capture depends on github-pull-requests, and therefore transitively on github-sdlc-planning — see ADR-0001 and ADR-0002.

github-repo-config, github-insights, github-packages, and github-org-identity are standalone and do not depend on planning. The dependency graph is one-directional and rooted here: if github-sdlc-planning is broken or its manifest version drifts, every plugin above it in the chain is affected (the “mif-docs staleness incident of 2026-07-05,” referenced in ADR-0002, is a documented real instance of this failure mode for a different dependency edge in the same marketplace).

Concretely, this plugin owns:

  • Issues: create/update, with MIF frontmatter authored automatically.
  • Native sub-issues: GitHub’s real parent/child issue relationship (addSubIssue), not a task-list checkbox convention — bounded at 100 children per parent and 8 nesting levels.
  • Projects v2: adding items, setting field values, reading board state.
  • Milestones: full REST-backed lifecycle (GraphQL exposes milestones read-only).
  • Discussions: create and list.
  • A portable session/capability floor (get_session_context, get_agent_capabilities) for any MCP host that isn’t Claude Code and therefore has no SessionStart hook to lean on.

Architecture: MCP-server core + progressive-enhancement layer

Section titled “Architecture: MCP-server core + progressive-enhancement layer”

The plugin is built on the marketplace’s house pattern, described in its own README and confirmed by ADR-0001 as the pattern the rest of the marketplace follows too:

  1. The MCP-server core (mcp-server/src/) is a portable TypeScript server exposing all 16 tools over stdio (@modelcontextprotocol/sdk). It has zero dependency on Claude Code: any MCP-capable host (Cursor, Gemini CLI, Copilot, Codex, or a bare gh-authenticated script talking MCP) drives the exact same tool surface. Shared infrastructure lives in github-client.ts (token resolution with an env-var-then-gh-fallback chain, deterministic mutation pacing at a hard 1-second minimum interval between content-creating calls, and three-way 403 classification — secondary rate limit vs. primary budget exhaustion vs. plain permission denial) and resolvers.ts (node-ID resolution for issues/repos/projects, since Projects v2 mutations require GraphQL node IDs, never numeric issue/project numbers). mif.ts implements MIF L1 frontmatter formatting/parsing natively in this layer — deliberately not delegated to the mif-docs skill set, because every MCP host needs identical body framing regardless of whether it has any Claude Code skill installed.
  2. The progressive-enhancement layer sits on top, Claude-Code-specific, and is optional:
    • Skills: project-setup, epic-decomposition, sprint-plan, milestone-triage, template-gallery — each a higher-level workflow built from the same 16 tools.
    • An agent: project-setup’s six-stage pipeline (classify intent → resolve template → configure fields → seed draft issues → wire automations → report).
    • Hooks: session-start.mjs (injects open milestones as session context, the Claude Code equivalent of calling get_session_context manually), validate-mif.mjs (MIF frontmatter conformance), confirm-mutation.mjs (a confirmation prompt before certain board mutations), and set-in-progress.mjs (see ADR-0003 below).

A host with no hooks support is not degraded to “broken” — it is degraded to “do the equivalent thing via an explicit tool call,” which is exactly what get_session_context and get_agent_capabilities exist for (getAgentCapabilities() returns hooksSupported: false precisely to make this explicit and discoverable at runtime).

The four hooks are not independent — they attach to different points of the same tool-call lifecycle, and one of them (set-in-progress.mjs) delegates its own configuration resolution to a layered cascade shared with github-bug-capture. For a single create_issue/add_sub_issue/ update_issue call in Claude Code, the tree looks like this:

SessionStart (startup)
└─ session-start.mjs
└─ gh api repos/{owner}/{repo}/milestones → additionalContext
Tool call: mcp__github-sdlc-planning__{create_issue,update_issue,add_sub_issue,
add_item_to_project,set_field_value,create_milestone,
assign_milestone,create_discussion}
├─ PreToolUse
│ └─ confirm-mutation.mjs
│ └─ describes the target from tool_input alone (never resolves
│ config itself); labels an omitted, config-defaulted field
│ "(config default)", or "(invalid ...)" if the caller gave
│ exactly one of an atomic pair (owner/repo, or
│ projectOwnerLogin/projectNumber) and omitted the other
├─ [the MCP server itself may resolve config here — see below]
└─ PostToolUse
├─ validate-mif.mjs (matcher: any mcp__github-sdlc-planning__.* call,
│ but only acts on create_issue/update_issue — MIF frontmatter is
│ an issue-body convention, not a discussion one)
│ └─ isMifConformant(body) → correction instruction if non-conformant
└─ set-in-progress.mjs (matcher: add_sub_issue|update_issue only)
├─ extractAffectedIssue(tool_input) → the issue that started work
└─ readBoardConfig(cwd) — hooks/lib/in-progress.mjs's own
dependency-free cascade (ADR-0004):
1. .config/gdlc/config.yml's board: section (project layer)
2. $XDG_CONFIG_HOME/gdlc/config.yml's board: section (global)
3. .claude/github-sdlc-planning.local.md's board: key
(legacy, deprecated, one release, with a warning)
→ setIssueInProgress(...) via gh api graphql, or a silent
no-op if no layer resolves

The MCP-server-side resolution referenced above is a separate cascade implementation, in a separate language runtime, from the hook’s: the tool call itself (create_issue’s owner/repo, or add_item_to_project/set_field_value/get_project_items/ get_session_context’s projectOwnerLogin/projectNumber) resolves omitted fields via mcp-server/src/config.ts’s loadGdlcConfig + resolveBoardCoordinates/resolveDestinationRepo — a TypeScript module with an npm dependency (yaml), bundled into the server’s own dist/. hooks/lib/in-progress.mjs’s readBoardConfig cannot import that module (hooks have no node_modules at execution time — see hooks/lib/settings.mjs’s doc comment in github-bug-capture) and re-implements the same three-layer resolution and the same board: schema as a dependency-free regex parser instead. The two are deliberately kept behaviorally identical (a real divergence between them was caught and fixed during issue #83’s review — see the layered config schema’s “Verified end-to-end” section) — but they are two independent implementations of one resolution rule, not one shared function, and any future change to the cascade must be made in both places.

github-bug-capture’s four board-coordinate tools (ensure_severity_field/set_severity/get_lifecycle_state/ set_lifecycle_state) go through the first of these two paths directly: that plugin’s mcp-server has its own file: dependency on this one specifically to import config.ts’s ./config subpath (see “New dependency edge, decided here” in the config schema doc) — it has no hooks of its own that read board config, so there is no second, hooks-layer implementation to keep in sync for that plugin.

ADR audit: which decisions govern this plugin

Section titled “ADR audit: which decisions govern this plugin”

Four ADRs exist under docs/decisions/ as of this writing. Two make a decision specifically about github-sdlc-planning’s own tool/hook behavior:

ADR Title Relevance to this plugin
ADR-0003 Rely on Native Projects v2 Workflows for Status Hygiene; Add a Hook Only for the In-Progress Gap Directly governs this plugin. It decided (a) add_item_to_project must query for an existing board item before mutating, returning existed: true instead of creating a duplicate — implemented in mcp-server/src/tools/projects.ts; and (b) the set-in-progress PostToolUse hook in this plugin’s own hooks/ directory, which fires on add_sub_issue/update_issue and calls the equivalent of set_field_value to move a board item to In Progress. Status: accepted, implemented, and audited compliant.
ADR-0004 One XDG-Mirrored Path for Global and Project Config; .claude/<plugin>.local.md Stays Local-Only Directly governs this plugin. It decided the carrier set-in-progress.mjs’s board-mapping resolution now reads first — .config/gdlc/config.yml, mirroring $XDG_CONFIG_HOME/gdlc/config.yml — superseding the board: key this plugin previously shipped in .claude/github-sdlc-planning.local.md (kept working for one release as a deprecated fallback). Also governs mcp-server/src/config.ts, the shared loader github-bug-capture depends on this plugin for. Status: accepted, implemented (issues #80-84), and audited compliant.
ADR-0001 MCP-Server Core for the github-bug-capture Plugin’s Agent-Neutral Layer 1 Not specific to this plugin. It decides github-bug-capture’s own architecture. It references github-sdlc-planning only as prior art for the house pattern this plugin already established — no requirement in the ADR’s Decision changes anything in github-sdlc-planning’s code.
ADR-0002 PR-to-Issue Linkage Stays in github-pull-requests; github-bug-capture Consumes It Not specific to this plugin. It settles a boundary between github-pull-requests and github-bug-capture. github-sdlc-planning is mentioned only as the root of the transitive dependency chain (bug-capture → pull-requests → sdlc-planning) whose fragility the ADR flags as a risk — it imposes no decision on this plugin’s own tools.

If a future ADR is warranted for a github-sdlc-planning-specific decision not yet recorded (for example, formalizing the 100-sub-issue / 8-level nesting limits as a decision rather than an implementation detail), that gap is noted here rather than an ADR being authored as part of this documentation task.