Skip to content

feat: auto-tick Plan issue Phases checklist on story merge-to-kahuna #500

@bakeb7j0

Description

@bakeb7j0

Summary

Post-merge automation: when a Story's MR merges to the kahuna branch, locate the Phases section in the parent Plan issue and check off the corresponding story's DoD checkbox.

Context

Filed as explicitly-deferred work from Dev Spec docs/phase-epic-taxonomy-devspec.md §5.1 (Plan Issue Canonical Template). The Plan issue template's Phases section has a per-story DoD checklist with lines like - [ ] #N merged — <description>. The template ships as part of cc-workflow#499.

Template clarification: the shipped Plan template uses ### Phase N — <Name> H3 subsections with **DoD:** checklists. There are no <!-- BEGIN: phases-checklist --> HTML comment markers in the shipped template; the draft reference to those markers is stale. Checkbox parsing targets the ## Phases H2 section, identifies ### Phase subsections within it, and matches lines by story number.

The value: Pair can see at-a-glance Plan progress without opening phases-waves.json or the wave-status panel.

The deferral reason: not load-bearing for the taxonomy rework itself. Plan checkboxes can be toggled by hand until automation ships.

Implementation Steps

Chosen shape: sdlc-server MCP primitive (plan_mark_story_done). Reasons: (a) the mutation logic lives where other Plan/Phase mutations live; (b) other skills (future /plan-status) get a reusable primitive; (c) Prime(post-flight) already calls MCP tools, so one more call is cleaner than shelling to gh. See ## Implementation Sketch below for the rejected alternative.

Step 1 — Add plan_mark_story_done handler in mcp-server-sdlc

Create handlers/plan_mark_story_done.ts in mcp-server-sdlc. Input schema:

z.object({
  plan_ref: z.string(),   // bare number "581" or "owner/repo#581"
  story_id: z.number().int().positive(),
  repo: z.string().optional(),  // override if Plan is in a different repo
})

Handler logic (platform: GitHub first; GitLab via getAdapter):

  1. Fetch Plan body: getAdapter({ repo }).fetchIssue({ number: planNumber, repo }) — reuse existing fetch-issue-{github,gitlab} adapters. If ok: false, return { ok: false, error, warn_only: true }.
  2. Locate ## Phases section: call parseSections(body) from lib/spec_parser.ts. Key is phases. If section is absent or empty, return { ok: false, warn_only: true, error: "Plan body has no ## Phases section" }.
  3. Find and flip the checkbox line: within the phases section content, search line by line for:
    /^- \[([ x])\] #<story_id>\b/
    
    • If the captured group is already x → idempotent return { ok: true, action: "already_ticked" }.
    • If the captured group is → replace with [x], reconstruct the full body (pre-Phases + Phases-section with the one line flipped + post-Phases), write back via gh issue edit <plan_number> --body <tempfile> (GitHub) or glab issue update <plan_number> --description <tempfile> (GitLab).
    • If no matching line found → return { ok: false, warn_only: true, error: "no checkbox for story #<story_id> found in ## Phases section" }.
  4. Write-back strategy: write the modified full body to a temp file (/tmp/plan-tick-<planNumber>.md), then execSync("gh issue edit <planNumber> --body-file /tmp/plan-tick-<planNumber>.md --repo <owner/repo>"). Delete the temp file after. This avoids shell-quoting issues with large bodies.
  5. Return: { ok: true, action: "ticked", plan_ref, story_id, line_replaced: "<old line>" }.

File path: mcp-server-sdlc/handlers/plan_mark_story_done.ts

Step 2 — Register the handler

In mcp-server-sdlc/handlers/_registry.ts, add plan_mark_story_done to the handler import list using the same auto-discovery pattern (import.meta.glob) already present. No further wiring needed if the registry uses glob auto-discovery.

Step 3 — Add corresponding adapter methods (if needed)

If the write-back path differs significantly between GitHub and GitLab, add:

  • mcp-server-sdlc/lib/adapters/plan-mark-story-done-github.ts
  • mcp-server-sdlc/lib/adapters/plan-mark-story-done-gitlab.ts

For v1, the execSync("gh issue edit ...") path is acceptable as a shell-out (consistent with existing handlers like wave_close_issue that shell to wave-status). Defer the full adapter split unless GitLab support is needed immediately.

Step 4 — Update /nextwave Prime(post-flight) prompt

In skills/nextwave/SKILL.md, update the Prime(post-flight) Step 5 prompt block:

Current Step 5:

  1. Merge all flight PRs via pr_merge. On merge, call wave_close_issue(X) and wave_record_mr(issue_number=X, mr_ref=<url>) per issue. Call wave_flight_done(M) after all merges land.

New Step 5:

  1. Merge all flight PRs via pr_merge. On merge, per issue X:
    a. wave_close_issue(X)
    b. wave_record_mr(issue_number=X, mr_ref=<url>)
    c. Read **Plan:** #M from the story issue's ## Metadata section (via spec_get(issue_ref=X)). If M is present and not N/A, call plan_mark_story_done({plan_ref: M, story_id: X}). A warn_only: true failure is logged to the merge report but does NOT abort the merge sequence.
    Call wave_flight_done(M) after all merges land.

File: skills/nextwave/SKILL.md — find the exact Step 5 block and apply the replacement above.

Step 5 — Build and deploy

cd mcp-server-sdlc
bun run build          # compiles dist/
# Verify handler appears in registry output:
bun run dist/index.js list 2>/dev/null | grep plan_mark_story_done

Install updated binary per the project's install procedure (typically ./install.sh or bun link). Restart the Claude Code session to reload the MCP binary.

Test Procedures

Unit tests — file: mcp-server-sdlc/tests/plan_mark_story_done.test.ts

Test Name Purpose
test_tick_unchecked_story Given a Plan body with - [ ] #42 merged — description in ## Phases, calling tickStory(body, 42) returns the body with [x] on that line and no other lines changed.
test_idempotent_already_ticked Given - [x] #42 merged — description, tickStory(body, 42) returns { action: "already_ticked" } and does not modify the body.
test_no_phases_section Given a Plan body without ## Phases, the handler returns { ok: false, warn_only: true } and does not throw.
test_story_not_in_phases Given a Plan body with a Phases section that has no line for #99, the handler returns { ok: false, warn_only: true } — does not throw, does not corrupt the body.
test_multi_phase_correct_line_flipped Plan body has Phase 1 (#10, #11) and Phase 2 (#12, #13). Calling tickStory(body, 12) flips only #12; the other three lines remain [ ].
test_write_back_called_once Stub the gh issue edit shell-out. Verify it is called exactly once per invocation, with the correct --body-file and --repo arguments.
test_write_back_not_called_when_idempotent Already-ticked case: verify the stub is never called.

Integration verification (manual, run once before merge):

  1. File a scratch Plan issue in a test repo with a Phases section containing three story DoD checkboxes (- [ ] #X, - [ ] #Y, - [ ] #Z).
  2. Call plan_mark_story_done({ plan_ref: "<issue_number>", story_id: X }) via the MCP tool (through Claude Code's tool panel).
  3. gh issue view <issue_number> --json body --jq .body — confirm #X line shows [x], #Y and #Z still show [ ].
  4. Repeat for #Y, then #Z. Confirm each flip is isolated and the prior ticks are preserved.
  5. Re-run for #X (already ticked) — confirm the tool returns already_ticked and the body is unchanged.
  6. Run a minimal kahuna wave end-to-end (single story, existing Plan with checklist): confirm the Plan DoD checkbox ticks automatically after the story MR merges to kahuna.

Acceptance Criteria

  • A primitive exists (in either shape above) that ticks the matching checkbox in the Plan issue phases-checklist section
  • /nextwave invokes it after a Story's MR merges to kahuna
  • Idempotent — re-invocation on an already-ticked box is a no-op, not an error
  • Failure to locate the checklist section (e.g. Plan issue malformed or v1 markers missing) logs a warning and continues; does not abort the wave
  • Regression test: a test Plan with 3 stories, stories merged sequentially, checkbox state inspected after each merge

Not In Scope

  • Two-way sync (editing the Plan issue checklist by hand → updating wave state). One-way only: wave state → Plan issue.
  • Retroactive fill of checkboxes for already-merged stories (e.g. during migration of in-flight legacy Plans per cc-workflow#499 Phase 4). If useful, that's a separate story.

Implementation Sketch

Two viable shapes (evaluated at filing time):

  1. sdlc-server primitive: plan_mark_story_done({plan_id, story_id}) — locates the phases section by H2 heading, finds the line matching #<story_id>, flips [ ][x]. Invoked by /nextwave after pr_merge returns success for a story. Selected — see ## Implementation Steps.
  2. Skill-body logic in /nextwave: inline the mutation via gh issue edit --body or glab issue update --description. Rejected: logic duplication, harder to test, no reuse path.

Dependencies

  • Depends on cc-workflow#499 Phase 3 (Plan template ships; ### Phase N — <Name> H3 structure is canonical).

Metadata

Wave: 2
Plan: #581
Wave Master: N/A

rules-lawyer 📜 (cc-workflow)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions