Claude Skill

work-items-to-issues

Break a work-items.md file (produced by /plan-work-items) into independently-grabbable GitHub issues, one per slice, in each slice's target repo. Use when you want to turn a work-items file into GitHub issues, publish work items as issue tickets, or create implementation tickets

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download testdouble-han-han-github_skills_work-items-to-issues-abba73a.zip · 24 KB
Part of testdouble/han — 47 skills

Install

skills CLI npx skills add https://github.com/testdouble/han/tree/main/han-github/skills/work-items-to-issues
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install testdouble-han@llmmart
Git git clone https://github.com/testdouble/han.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole testdouble/han collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Project Context

  • personal config directory: !bash "${CLAUDE_PLUGIN_ROOT}/scripts/han-config-dir.sh" 2>/dev/null || echo "$HOME/.claude"
  • project .han/config.md: !cat .han/config.md 2>/dev/null || echo ""

As your first action, use the Read tool on .han/config.md inside the personal config directory path above. A read that returns no file is no personal configuration: continue silently. When that file or the project .han/config.md probe supplies content, apply it per config-rule.md, which governs precedence between the two files, relative-path resolution, and what to do with a file that reads but cannot be used.

Work Items to GitHub Issues

Take an already-broken-down work-items.md file (produced by /plan-work-items) and publish each slice as a GitHub issue in its target repo.

The breakdown work — drafting slices, assigning symbolic IDs, specifying dependencies, inventorying references — has already been done upstream. This skill's job is to map each slice to its target repo, validate the format, write a per-repo work-items file alongside the source, and run the publish pipeline.

Rules

  • Each slice lives in exactly one repo. Cross-repo coordination is documented in prose at the top of work-items.md — never as a native blocker link.
  • Native blocked_by relationships are within-repo only. A cross-repo Depends on is a format error to surface for repair.
  • Symbolic-ID prefixes: accept whatever the input uses. Both shapes are valid input — single-prefix across repos (e.g., W-N for every slice) and per-repo prefixes (e.g., V2-N backend, W-N frontend, EV-N events). The publish scripts accept any uppercase prefix.
  • Every slice issue body MUST link the reference artifacts an implementer needs — API/event contracts, design frames, schema docs, runbooks, ADRs, coding standards. Issues that consume an HTTP endpoint or event payload MUST link the contract section that defines it.
  • UI slices, when the plan folder has a ui-designs/ subfolder, MUST embed the relevant screenshots inline using same-target-repo raw URLs. See references/screenshot-embed-rules.md.
  • NEVER include process artifacts in issue bodies or the work-items preamble. Excluded categories — iteration histories, decision logs, review findings, team findings, facilitation summaries, gap analyses, and anything under an artifacts/ subfolder of the plan that is not a contract or design reference. Full include/exclude list in references/reference-artifact-inventory.md.

Process

1. Locate the work-items file

If the path is not provided, ask for it. The input is a single work-items.md produced by /plan-work-items. Read it.

If the user named a target repo (or repos), a label, or an assignee, note them for Steps 2 and 6. By default, issues are created with no label and no assignee — only apply a label or assignee when the user explicitly asked for one.

2. Build the SYM→repo map

Determine which repo each slice belongs to. Use both signals and reconcile them:

  • Primary — cross-repo work order prose. Most work-items.md files include an intro paragraph naming which SYMs ship to which repo (e.g., "W-1 through W-4 ship to acme-api. W-5 through W-9 ship to acme-web."). Parse this for the mapping.
  • Corroborating — file paths inside each slice. Each slice's **Work to be done.** and **References.** blocks reference files in the target repo. Path roots map cleanly: acme-api/... → acme/acme-api, acme-web/... → acme/acme-web, acme-events/... → acme/acme-events. Use this to verify the prose and to assign any slice the prose doesn't cover.

If the prose and the file-path evidence disagree for a slice, surface the conflict to the user before proceeding.

3. Validate the format with evidence-based repair

Check the work-items file against the format invariants in references/issue-template.md and references/work-items-file-format.md:

  • Heading shape. Every slice heading matches ## <SYM-N> — <title> with an em-dash separator (already-published headings annotated as ## <SYM-N> (#NNN) — <title> are valid too).
  • Depends on line. Literal bold marker **Depends on.**, trailing period, None. or comma-separated SYMs.
  • Within-repo blockers. Every SYM named in a Depends on line maps to the same target repo as the dependent slice (under the map from Step 2).
  • Screenshot URLs. When present, match https://github.com/<org>/<target-repo>/raw/<branch>/.github/issue-assets/<feature-slug>/<SYM-N>/<file>.<ext> against the target repo's default branch and a real file under <plan-folder>/ui-designs/, whose extension is one of the accepted set (png, jpg, jpeg, gif, webp, svg, pdf) and is copied into the URL unchanged. <feature-slug> is the kebab-cased basename of the plan folder.
  • References block. Present whenever the slice consumes an HTTP endpoint, event payload, design frame, ADR, coding standard, or other named artifact.
  • No process artifacts. No links to iteration histories, decision logs, review findings, team findings, facilitation summaries, gap analyses, or anything under an artifacts/ subfolder that is not a contract or design reference.

When a check fails, attempt evidence-based repair. Pull evidence from the source work-items.md, the parent plan referenced in its intro, the feature spec in the same folder, sibling files in the plan folder, and the target repo's ADRs / coding standards / docs:

  • Malformed heading — propose the corrected shape based on the surrounding text. Cite the line number.
  • Missing Depends on line — propose None. if no blockers are evident in the slice's prose. Cite the absence.
  • Cross-repo Depends on — propose moving the relationship to the cross-repo work-order prose at the top of the file and replacing the line with None. or remaining within-repo SYMs. Cite the SYM→repo map entries that prove the cross-repo split.
  • Missing References bullet for an HTTP-consuming slice — propose the contract section link by inspecting the parent plan's External Interfaces / API Contracts section. Cite the anchor.
  • Missing References bullet for a UI slice with ui-designs/ present — propose the design frame and screenshot files by inspecting the feature spec's Visual Reference table and the spec's inline screenshot embeds. Cite the spec section.
  • Process-artifact link found — propose removing the link and (if the slice still needs the context the artifact held) restating the decision in plain language, with a Plan decisions References bullet naming the ID plus a one-sentence description. Cite the include/exclude list.

After validation, report findings in plain language. For each finding, name:

  1. What is wrong — slice SYM, line reference, the failing invariant.
  2. What the proposed fill is — the corrected line, new bullet, removed link, etc.
  3. Evidence for the fill — file path with line number, document section, or named source.

Then give the user three actions:

  • Continue with fills — apply the proposed repairs to the source work-items.md (so the per-repo split files inherit them) and proceed to Step 4.
  • Correct the fills — user provides the right values; apply those and proceed.
  • Stop — exit without publishing. User edits the file by hand and re-runs.

If validation passes with no findings, proceed silently to Step 4.

4. Show the SYM→repo map for confirmation

Present a table for user review:

SYM Title Target repo
W-1 Backend per-list validator generalization acme/acme-api
W-2 … acme/acme-api
W-5 Frontend type widening and drift comparator acme/acme-web

Wait for confirmation before writing files or creating issues.

5. Write per-repo work-items files

For each target repo named in the SYM→repo map, write a <repo-name>.work-items.md file in the same folder as the source work-items.md. The per-repo file is a filtered view of the source — it is the file the publish scripts consume:

  • Header section. Copy the source file's title, intro paragraph, and cross-repo work-order prose verbatim. This keeps each per-repo file self-contained for review.
  • Shared reference artifacts. Copy the source file's "Shared reference artifacts" section, filtered to entries that apply to at least one slice in this repo. When in doubt, include the entry.
  • Slices. Include only the slices whose SYM maps to this repo, in their original order from the source file.

The source work-items.md is not modified by the publish step. The per-repo files are what carry the (#NNN) issue-number annotations after publishing.

6. Publish each per-repo file to GitHub

For each per-repo file, publish it by running ${CLAUDE_SKILL_DIR}/scripts/publish-work-items.sh <per-repo-work-items-file> <org>/<target-repo> <plan-folder> [--label <name>] [--assignee <user>]. Pass the per-repo work-items file written in Step 5, the target repo as <org>/<target-repo>, and the plan folder that contains the ui-designs/ subfolder.

Created issues are unassigned and carry no label by default. Append --label <name> and/or --assignee <user> only when the user asked for a label or assignee (Step 1). Both flags are optional and may be omitted.

The wrapper runs three idempotent scripts in order:

  1. scripts/upload-screenshots.sh — extracts every .github/issue-assets/<feature-slug>/<SYM-N>/<file>.<ext> URL from the per-repo file and copies the matching file from <plan-folder>/ui-designs/ into the target repo, verifying each upload. The <feature-slug> segment (the kebab-cased plan-folder basename) keeps assets from different features that publish to the same repo from colliding. Upload is adaptive: by default each file is written directly to the default branch via the GitHub Contents API, but if that branch is protected and rejects the direct write (HTTP 409), the script falls back to committing the assets to an assets branch, opening a pull request, and printing the PR URL. The embedded image URLs always name the default branch, so on the PR path the inline designs render once that assets PR merges — the issues are still created immediately. Overwrites existing files cleanly and, on re-run, reuses the assets branch and open PR — but only a branch it created for this feature (one already carrying the feature's issue-assets/<feature-slug>/ tree); a same-named branch it does not own is refused rather than committed onto.
  2. scripts/create-issues.sh — creates one GitHub issue per ## <SYM-N> slice in file order (blocker-first), unassigned and unlabeled by default. When --label <name> is passed, it applies that label to every issue, creating it on the repo only if it does not already exist (an existing label's color and description are left intact); when --assignee <user> is passed, it assigns each issue to that user. Captures each returned issue number and rewrites the heading in place to ## <SYM-N> (#NNN) — <title>. Skips slices already annotated with (#NNN) so partial runs resume cleanly.
  3. scripts/link-blockers.sh — reads the SYM↔#NNN mapping from the rewritten headings, walks each **Depends on.** line, and POSTs repos/<repo>/issues/<N>/dependencies/blocked_by once per blocker. Errors out if a blocker SYM is not present in the same file (cross-repo dependencies are forbidden as native links — they belong in the cross-repo work-order prose).

When upload-screenshots.sh reports that it fell back to PR mode (it prints a NOTE: with an assets-branch pull request URL), surface that PR URL to the user and tell them the issues' inline designs render only once that assets PR merges. This is a required follow-up action; do not summarize it away.

The format invariants the scripts depend on (heading shape, URL scheme, Depends on syntax) are documented in references/issue-template.md. Edits to that template require matching script changes.

Files (han)
  • references
    • issue-template.md 6.6 KB
      # Slice issue format
      
      > Each slice in a `work-items.md` file (and in the per-repo files the skill writes) must follow this format. The publish
      > scripts (`scripts/create-issues.sh`, `scripts/link-blockers.sh`) parse it; the skill's Step 3 validation checks it.
      > Changes here require matching script changes.
      
      The format below is what `/plan-work-items` emits and what the publish pipeline reads. Required fields appear in the
      order shown. The `**References.**` block is required whenever the slice consumes any external artifact (HTTP endpoint,
      event payload, design frame, ADR, coding standard) — omit it only when no external artifact applies. Additional
      `**Bold paragraph.**` context blocks are allowed between required fields when a slice needs them — common ones:
      `**Note on scope boundary with <other effort>.**` for ticket-boundary clarifications,
      `**Note on <subsystem> capability.**` for SDK or platform caveats that affect acceptance.
      
      ```
      ## <SYM-N> — <short descriptive name>
      
      **Summary.** Three to five very short, plain-language sentences stating why the work is needed and what is being done. No technical detail and no ID references — plan references live in the References block, each with a one-sentence description.
      
      **Work to be done.**
      - Plain-language bullets stating the actual work, one to two short sentences each, every bullet supporting an acceptance criterion below.
        - Technical detail, when needed, nests under the plain-language bullet it belongs to: starting points (a file path, a contract, a boundary), never a prescribed edit list or implementation code.
      
      *(Optional `**Bold paragraph.**` blocks here — e.g., `**Note on scope boundary.**`, `**Note on cross-repo gate.**`.)*
      
      **Screenshots.** *(Required for UI-bearing slices when the plan folder contains a `ui-designs/` subfolder. Each item is embedded inline using a same-target-repo raw URL of the form `https://github.com/<org>/<target-repo>/raw/<branch>/.github/issue-assets/<feature-slug>/<SYM-N>/<file>.<ext>`, where `<feature-slug>` is the kebab-cased basename of the plan folder and `<ext>` is the source file's own extension, one of `png`, `jpg`, `jpeg`, `gif`, `webp`, `svg`, or `pdf` — the accepted set in [screenshot-embed-rules.md](./screenshot-embed-rules.md). Copy the source filename including its extension; never rewrite `.jpg` to `.png`, because the upload resolves the source file by the filename in the URL. The file is copied into the target repo by `scripts/upload-screenshots.sh` before the issue is created. Cross-repo URLs into the planning repo are forbidden — the automated implementation tooling cannot resolve them. Each embed is wrapped in a link to the same URL so readers can open the full-size image in a new tab. A `.pdf` is linked rather than embedded as an image, because GitHub does not render it inline. One item per bullet, with a short caption naming the depicted state. Omitted when the slice has no UI surface or no `ui-designs/` folder exists.)*
      
      - *<state-or-scenario name>* — `[![<alt text>](https://github.com/<org>/<target-repo>/raw/<branch>/.github/issue-assets/<feature-slug>/<SYM-N>/<file>.<ext>)](https://github.com/<org>/<target-repo>/raw/<branch>/.github/issue-assets/<feature-slug>/<SYM-N>/<file>.<ext>)`
      
      **References.**
      - **Plan decisions** — every plan decision or work unit this slice satisfies, one bullet each: the ID as a link (e.g., `[D-6](feature-implementation-plan.md#d-6-...)`) followed by one short plain sentence saying what it is. Never a bare ID list. Replaces any inline `See plan: ...` reference and any standalone "Work items addressed" field.
      - **API contract** — `[<file>#<anchor>](<relative-path>)` (e.g., `[feature-implementation-plan.md#external-interfaces](feature-implementation-plan.md#external-interfaces)`). Required when the slice produces or consumes an HTTP endpoint.
      - **Event contract** — `[<file>#<event-section>](<relative-path>)`. Required when the slice produces or consumes an event payload.
      - **Design (Pencil)** — `<pen-file-path>`, frames `<frameId>` (purpose), `<frameId>` (purpose). Required for UI slices.
      - **Spec section** — `[feature-specification.md#<anchor>](feature-specification.md#<anchor>)` for the behavior this slice realizes.
      - **ADR / standard / repo doc** — links to architectural decisions, coding standards, or feature docs the implementer must honor.
      - Omits any bullet that does not apply. Does not link iteration histories, decision logs, review findings, team findings, facilitation summaries, or any other process artifact.
      
      **Acceptance criteria.**
      - [ ] Each criterion is an observable, verifiable outcome of this slice's own behavior. Test expectations live here (e.g., "Automated tests cover the rejection path"). Never standard operating procedure (commit pushed, CI green, PR opened), and never a prohibition without its validated reason stated alongside it.
      
      **Depends on.** `<SYM-N>` (within this repo), comma-separated for multiple, or `None.`
      ```
      
      ## Format invariants the scripts depend on
      
      These are the patterns the publish scripts grep for; violating them breaks the pipeline. The skill's Step 3 validation
      checks each invariant before publishing and proposes evidence-based repairs.
      
      - **Heading line** begins with `## ` followed by `<SYM-N>` (uppercase letters or digits, dash, digits), then `—`
        (em-dash with surrounding spaces), then the title.
      - **Heading rewrite.** After issue creation, `scripts/create-issues.sh` rewrites each heading in place to
        `## <SYM-N> (#NNN) — <title>`. The `(#NNN)` annotation is how `link-blockers.sh` resolves symbolic IDs to GitHub issue
        numbers, and how `create-issues.sh` knows to skip already-created slices on re-run. Both shapes — with and without
        `(#NNN)` — are valid input.
      - **Slice body** ends at the next `## ` heading or end of file.
      - **Screenshot URLs** use the exact path scheme `.github/issue-assets/<feature-slug>/<SYM-N>/<file>.<ext>`, where
        `<feature-slug>` is the kebab-cased basename of the plan folder and `<ext>` is the source file's own extension from the
        accepted set. The upload script extracts this path verbatim from the per-repo file and reads both the slug and the
        filename back out of it, so what is written into the URL is authoritative for both.
      - **`Depends on` line** uses the literal bold marker `**Depends on.**`, comma-separates blockers, and ends with `.` (the
        trailing period is part of the format, not a sentence terminator).
      - **Within-repo blockers only.** Every SYM named in a `Depends on` line must resolve to a slice in the same per-repo
        file. Cross-repo blockers belong in the cross-repo work-order prose at the top of the source `work-items.md`, not as a
        native `blocked_by` link.
      
    • reference-artifact-inventory.md 5.9 KB
      # Reference artifact inventory
      
      The breakdown work upstream (`/plan-work-items`) is responsible for inventorying artifacts and embedding them in each
      slice's `**References.**` block. This skill's job is to **verify** those references are complete and correct before
      publishing, and to propose evidence-based fills when they are not.
      
      This file defines what belongs in slice References blocks (the include list), what never belongs (the exclude list), and
      how the skill's Step 3 validation reasons about both.
      
      ## Include (these belong in slice References blocks and/or the source file's Shared reference artifacts)
      
      - **HTTP API contract files** (e.g., the parent plan's "External Interfaces" or "API Contracts" section) and the
        specific endpoint sections that slices produce or consume.
      - **Event payload contract files** and the specific event sections.
      - **Feature specification** (`feature-specification.md`) — sections that define the behavior a slice realizes.
      - **Design assets** — Pencil document file paths plus specific frame IDs (when the plan or spec maps frames to UI),
        screenshot files, Figma URLs, mockup PDFs.
      - **Screenshots from `ui-designs/`**, when present. Map each PNG to the slices that realize the behavior it depicts. The
        feature spec is the mapping source: its Visual Reference table (or equivalent) lists every screenshot, and its inline
        `![…](ui-designs/…png)` embeds appear next to the prose describing the depicted state — that prose tells you which
        slice owns the screenshot. A single slice may need multiple screenshots when it implements multiple states; a single
        screenshot may apply to multiple slices when distinct slices share a screen — in that case the file is copied once
        into each owning slice's `.github/issue-assets/<feature-slug>/<SYM-N>/` folder by the upload script. The planning-repo
        location is the _source_; it is never the URL embedded in the issue. See
        [screenshot-embed-rules.md](./screenshot-embed-rules.md) for the full URL rule.
      - **Schema / migration references** in the target repo when a slice depends on a not-yet-shipped schema.
      - **ADRs** (`docs/adr/...` in target repos), coding standards, and feature documentation that constrain the slice's
        implementation.
      - **Runbook skeletons or observability notes** when a slice's acceptance criteria require them.
      
      ## Exclude (these never belong in slice References blocks or the Shared reference artifacts section)
      
      - Iteration histories (`*-iteration-history.md`, `.evidence-roundN.md`, `.junior-developer-roundN.md`,
        `.adversarial-roundN.md`, etc.)
      - Decision logs (`decision-log.md`, `implementation-decision-log.md`)
      - Review findings (`review-findings.md`, `implementation-review-findings.md`)
      - Team findings, facilitation summaries, gap analyses, security/UX round notes
      - Anything under an `artifacts/` subfolder of the plan **unless** it is a contract or design reference (e.g., a
        `design-frame-verification.md` may be cited; a `team-findings.md` may not).
      
      These exist to record how the plan was reached, not what the implementer needs to build. Plan-level decisions that
      survive into a slice are restated in plain language in the slice description, and cited in the slice's `**References.**`
      block as the decision ID plus a one-sentence description of what it is — never an inline ID-only breadcrumb, and never a
      link to the decision log itself.
      
      If validation finds a process-artifact link in a slice, the proposed repair is to remove the link and (if the context
      the artifact held is load-bearing) restate the decision in plain language with a **Plan decisions** References bullet.
      
      ## Where each artifact should be cited
      
      - When a single artifact applies to **many slices**, it should appear once in the source file's **Shared reference
        artifacts** section. Slices reference it by anchor instead of duplicating.
      - When an artifact applies to **a single slice**, it appears inline in that slice's `**References.**` block.
      
      When the skill writes per-repo files, it copies the Shared reference artifacts section filtered to entries that apply to
      at least one slice in that repo. When in doubt, the entry is included.
      
      ## What Step 3 validation checks
      
      For each slice:
      
      - If the slice produces or consumes an HTTP endpoint (detected from slice prose mentioning a route, method,
        status code, or DTO shape), an **API contract** bullet must be present in `**References.**`.
      - If the slice produces or consumes an event payload, an **Event contract** bullet must be present.
      - If the slice has a UI surface and the plan folder contains `ui-designs/`, the `**Screenshots.**` block must be present
        with at least one embed, AND a **Design** bullet should be present in `**References.**`.
      - A **Spec section** bullet should be present whenever the slice realizes a named behavior from the feature spec.
      - No process-artifact link is present anywhere in the slice body.
      
      ## Missing-artifact handling (evidence-based repair)
      
      When validation finds a missing or excluded artifact:
      
      - **Missing API contract link** — propose the parent plan's External Interfaces / API Contracts section by file path and
        anchor, evidenced by the section's existence at that location.
      - **Missing event contract link** — propose the parent plan's events section, evidenced by the section's existence.
      - **Missing Design / Screenshots block** — inspect the feature spec's Visual Reference table and inline screenshot
        embeds; propose the design frame ID(s) and screenshot file(s), cited by spec section.
      - **Process-artifact link found** — propose removal, evidenced by the exclude list above. If the link was load-bearing
        for context, propose a plain-language restatement with a **Plan decisions** References bullet (ID plus a one-sentence
        description).
      
      Every proposed fill must cite a concrete source — a file path with line number, a document section, or an ADR ID. Fills
      without evidence are surfaced as gaps for the user to resolve, not silently applied.
      
    • screenshot-embed-rules.md 6.5 KB
      # Screenshot embed rules
      
      ## Contents
      
      - The accepted file types
      - Why cross-repo URLs are forbidden
      - Required URL form
      - Why the path is feature-scoped
      - How assets reach the default branch
      - Embed format inside the issue body
      - Mapping visual material to slices
      - Duplication over sharing
      - What never to do
      
      When the plan folder contains a `ui-designs/` subfolder with visual material, every UI-bearing slice MUST embed the
      relevant material **inline in the issue body** — not as plain links.
      
      ## The accepted file types
      
      The upload chain accepts the same visual-material file set the planning skills persist:
      `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.svg`, and `.pdf`. That set is named canonically in
      `han-planning/references/planning-boundary-rule.md`; this file and the issue template honor it rather than restating an
      extension of their own.
      
      Three places have to agree on the set, and the third is the one a first pass misses:
      
      1. `scripts/upload-screenshots.sh`, in its `ASSET_EXT_PATTERN` variable, which decides what gets selected and uploaded.
      2. The required URL form below, which decides what a valid embed looks like.
      3. `references/issue-template.md`, which is what an implementer copies when writing the embed.
      
      Widening the first two without the third produces an issue body that writes a `.png` extension over a file that is not
      one. The upload succeeds and the image still renders broken.
      
      A `.pdf` does not render as an inline image on GitHub. Embed it as a link rather than an image, with the same caption
      naming the state it shows.
      
      ## Why cross-repo URLs are forbidden
      
      A naive embed would point at the raw URL inside the planning repo. That fails: the automated implementation tooling
      (Ralph) runs against the **target code repo** and cannot resolve URLs that point into a different repository. An issue
      body with cross-repo image URLs renders blank in that environment, and the implementer can't see the design.
      
      Solution: **copy each file into the target repo first**, then embed it via a same-repo URL.
      
      ## Required URL form
      
      ```
      https://github.com/<org>/<target-repo>/raw/<branch>/.github/issue-assets/<feature-slug>/<SYM-N>/<file>.<ext>
      ```
      
      - `<target-repo>` is the code repo the issue is being created in (e.g., `acme-web`).
      - `<branch>` is the target repo's default branch, fetched at upload time via
        `gh repo view <org>/<repo> --json defaultBranchRef --jq .defaultBranchRef.name` (typically `main`).
      - `<feature-slug>` is the kebab-cased basename of the plan folder (e.g., a plan folder `notification-preferences/`
        yields `notification-preferences`). It namespaces this feature's assets so they never collide with another feature's.
        See "Why the path is feature-scoped" below.
      - `<SYM-N>` is the slice's symbolic ID (e.g., `W-3`).
      - `<file>.<ext>` matches the source filename in `<plan-folder>/ui-designs/<file>.<ext>`, extension included. `<ext>` is
        one of the accepted types above. Never rewrite the extension: the upload reads the filename straight out of the URL, so
        an embed naming `card.png` over a source file named `card.jpg` fails to find its source.
      
      The `scripts/upload-screenshots.sh` script extracts this path from the work-items file, copies the file from the plan
      folder into the target repo at the corresponding location, and verifies the resulting URL resolves before issue
      creation.
      
      ## Why the path is feature-scoped
      
      Symbolic IDs restart at `<PREFIX>-1` for every feature, so a flat `.github/issue-assets/<SYM-N>/` namespace commingles
      unrelated features that publish to the same repo: a second feature's `W-4` assets would land in the same folder as the
      first feature's `W-4`, with a real overwrite risk if they share a filename. The `<feature-slug>` segment gives each
      feature its own subtree. Derive the slug from the plan folder name (its kebab-cased basename) so it is stable and
      reproducible — `upload-screenshots.sh` reads the slug straight back out of the embedded URL, so the slug written into
      the URL is the one that must match.
      
      ## How assets reach the default branch
      
      The embedded URL always points at the target repo's **default branch**, because that is where the asset lives once
      published. How the file gets there depends on the repo:
      
      - **Unprotected default branch** — `upload-screenshots.sh` writes each file directly to the default branch via the GitHub
        Contents API. Fully autonomous, no human step.
      - **Protected default branch** — a direct write is rejected (HTTP 409, "changes must be made through a pull request").
        The script falls back automatically: it commits every file to an assets branch (`issue-assets/<feature-slug>`), opens a
        pull request, and prints the PR URL. The issues are still created immediately; their inline images render once that
        assets PR merges. The embedded URL does not change — it still names the default branch.
      
      The fallback never runs local git and never touches your current branch — every branch, commit, and PR operation is a
      server-side GitHub API call against the target code repo. The assets branch is created fresh from the default branch's
      tip, or reused on a re-run **only when it already carries this feature's `issue-assets/<feature-slug>/` tree**. A branch
      of that name that the skill did not create is refused, never committed onto, so no unrelated work is ever modified.
      
      ## Embed format inside the issue body
      
      Wrap each embed in a link to the same URL so readers can open the full-size image in a new tab:
      
      ```
      - *<state-or-scenario name>* — [![<alt text>](<URL>)](<URL>)
      ```
      
      One image per bullet, with a short caption naming the depicted state.
      
      ## Mapping visual material to slices
      
      Map material to slices via the feature spec's own embeds: the section in the spec that describes the behavior a slice
      implements is where the canonical item for that slice is referenced.
      
      ## Duplication over sharing
      
      When an item applies to multiple slices, copy it once **per slice** so each `<SYM-N>` folder is self-contained. The
      upload script handles this automatically — every URL it sees in the work-items file produces an upload, even if the
      source file is shared.
      
      ## What never to do
      
      - Never embed planning-repo raw URLs (or any cross-repo URL) in an issue body.
      - Never use a shared `.github/issue-assets/shared/` path. Each slice owns its own folder.
      - Never link to an image without embedding it — implementers should see the design without clicking out. A `.pdf` is the
        one exception, because GitHub does not render it inline.
      - Never rewrite a source file's extension in the embed. The upload resolves the source file by the filename in the URL.
      
    • work-items-file-format.md 4.4 KB
      # Work-items file format
      
      > This file describes the format the skill **reads** from `/plan-work-items`, and the format the skill **writes** when
      > splitting per-repo. The publish scripts parse the per-repo files. Changes to the slice-body format require updating
      > `scripts/create-issues.sh` and `scripts/link-blockers.sh`.
      
      There are two file shapes to know:
      
      - **Source `work-items.md`** — one single file emitted by `/plan-work-items`, covering every repo touched by the plan.
        The skill reads this.
      - **Per-repo `<repo-name>.work-items.md`** — filtered views the skill writes alongside the source. One file per affected
        repo. The publish scripts read these.
      
      ## Source file shape (input)
      
      The source file lives next to its parent plan (typically in `<feature>/<phase>/work-items.md`) and contains, in order:
      
      ### 1. Title
      
      ```
      # Work Items — <feature or phase name>
      ```
      
      ### 2. Intro paragraph
      
      Links the parent implementation plan and feature spec, and notes that work-item SYMs are for cross-reference only:
      
      > Source: [feature-implementation-plan.md](feature-implementation-plan.md). Spec:
      > [feature-specification.md](feature-specification.md).
      >
      > Work items are numbered `<SYM>-N` for cross-reference only. `Depends on` lines refer to other work items in this file.
      
      ### 3. Cross-repo work order prose _(required when the plan touches more than one repo)_
      
      A single paragraph (not a table) naming which SYMs ship to which repo and noting any cross-repo deploy ordering or merge
      gates. Example:
      
      > **Cross-repo work order.** W-1 through W-4 ship to `acme-api` (backend). W-5 through W-9 ship to `acme-web`
      > (frontend). The frontend page-integration items (W-7, W-8) must not be merged until the backend changes (W-1..W-4) are
      > live in production. Per project rule, no cross-repo issue links — the gate is recorded in prose here, not by a ticket
      > reference.
      
      This paragraph is the primary signal the skill uses to build the SYM→repo map. File paths inside each slice corroborate
      it.
      
      ### 4. Shared reference artifacts _(required when any artifact applies to more than one slice)_
      
      A flat list (not per-repo) of artifacts more than one slice references — API contract sections, spec sections, ent
      schemas, shared stylesheets, ADRs, coding standards. Entries that apply to one repo only stay here too; the section is
      organized by artifact, not by repo. Each entry is a relative link plus the anchor or file path an implementer should
      jump to.
      
      ### 5. Slices
      
      One slice per `## <SYM-N> — <title>` heading. Slice bodies follow [issue-template.md](./issue-template.md). Slices may
      appear in any order — the skill does not reorder them when writing per-repo files; it preserves source order.
      
      ## Symbolic-ID prefixes
      
      Both shapes are valid input:
      
      - **Single prefix across repos.** Every slice uses `W-N` (or any single prefix) regardless of target repo. The SYM→repo
        map carries the repo assignment separately. This is what `/plan-work-items` currently emits.
      - **Per-repo prefixes.** Different prefixes signal target repo at-a-glance — e.g., `V2-N` backend, `W-N` frontend,
        `EV-N` events.
      
      The publish scripts accept any `[A-Z][A-Z0-9]*-[0-9]+` heading; the skill prose just reads what is there.
      
      ## Per-repo file shape (output, written by the skill)
      
      For each repo named in the SYM→repo map, the skill writes `<repo-name>.work-items.md` alongside the source. Each
      per-repo file is a filtered view of the source, retaining enough context to stand alone:
      
      1. **Title** — copy from source.
      2. **Intro paragraph** — copy from source verbatim.
      3. **Cross-repo work order prose** — copy from source verbatim so the reader knows the relationship to other repos.
      4. **Shared reference artifacts** — copy from source, filtered to entries that apply to at least one slice in this file.
         When in doubt, include the entry.
      5. **Slices** — only the slices whose SYM maps to this repo, in source order.
      
      The per-repo file is what the publish scripts consume. The source `work-items.md` is not modified by the publish step.
      After publishing, the per-repo file's slice headings carry `(#NNN)` annotations from `scripts/create-issues.sh`; the
      source file does not.
      
      ## What the publish scripts depend on
      
      The slice-body invariants are documented in [issue-template.md](./issue-template.md). The per-repo file's preamble
      (title, intro, cross-repo prose, shared references) is for the human reviewer — the scripts ignore everything before the
      first `## <SYM-N>` heading.
      
  • scripts
    • create-issues.sh 4 KB
      #!/usr/bin/env bash
      # Usage: create-issues.sh <work-items-file> <target-repo> [--label <name>] [--assignee <user>]
      #
      # Walks every `## <SYM-N> — <title>` heading in file order (blocker-first,
      # since the work-items file is authored that way) and creates one GitHub
      # issue per slice. After each successful creation, rewrites the heading in
      # place to `## <SYM-N> (#NNN) — <title>` so link-blockers.sh can resolve
      # symbolic IDs to issue numbers.
      #
      # By default issues are created with no label and no assignee. Pass
      # `--label <name>` to apply that label to every issue (creating it only if it
      # does not already exist — an existing label's color/description are left
      # intact), and/or `--assignee <user>` to assign every issue to that user. Both
      # are optional.
      #
      # Idempotent: slices whose heading already includes `(#NNN)` are skipped.
      
      set -euo pipefail
      
      WORK_ITEMS="${1:?work-items file required}"
      TARGET_REPO="${2:?target repo (org/name) required}"
      shift 2 || true
      
      LABEL=""
      ASSIGNEE=""
      while [ $# -gt 0 ]; do
        case "$1" in
          --label)    LABEL="${2:?--label requires a value}";    shift 2 ;;
          --assignee) ASSIGNEE="${2:?--assignee requires a value}"; shift 2 ;;
          *) echo "unknown argument: $1" >&2; exit 1 ;;
        esac
      done
      
      [ -f "$WORK_ITEMS" ] || { echo "work-items file not found: $WORK_ITEMS" >&2; exit 1; }
      
      if [ -n "$LABEL" ]; then
        # Create the label without `--force`, which would reset an existing label's
        # color/description. A create that fails on an existing label leaves it
        # untouched, so fall back to confirming it exists (exact name match) and
        # reuse it; anything else is a real error.
        if gh label create "$LABEL" --repo "$TARGET_REPO" >/dev/null 2>&1; then
          echo "label '$LABEL' created on $TARGET_REPO"
        elif gh label list --repo "$TARGET_REPO" --json name --jq '.[].name' | grep -qxF "$LABEL"; then
          echo "label '$LABEL' already exists on $TARGET_REPO — reusing as-is"
        else
          echo "ERROR: could not create or find label '$LABEL' on $TARGET_REPO" >&2
          exit 1
        fi
      fi
      
      tmpdir=$(mktemp -d)
      trap 'rm -rf "$tmpdir"' EXIT
      
      # Slice heading pattern: `^## <PREFIX>-<NUM>` — captured at file scan time.
      # We re-read the file inside the loop (after each rewrite) so the line
      # numbers stay valid even though only one line changes per iteration.
      
      while true; do
        # Find the first un-numbered slice heading.
        match=$(grep -nE '^## [A-Z][A-Z0-9]*-[0-9]+ — ' "$WORK_ITEMS" | head -n 1 || true)
        [ -z "$match" ] && break
      
        start_line=$(echo "$match" | cut -d: -f1)
        heading=$(echo "$match" | cut -d: -f2-)
      
        # Find the next heading line after start_line (the body terminator).
        next_line=$(awk -v s="$start_line" 'NR>s && /^## / {print NR; exit}' "$WORK_ITEMS")
        if [ -z "$next_line" ]; then
          next_line=$(($(wc -l < "$WORK_ITEMS") + 1))
        fi
      
        sym=$(echo "$heading" | sed -E 's/^## ([A-Z][A-Z0-9]*-[0-9]+) — .*/\1/')
        title=$(echo "$heading" | sed -E 's/^## //')
      
        body_file="$tmpdir/$sym.md"
        sed -n "$((start_line + 1)),$((next_line - 1))p" "$WORK_ITEMS" > "$body_file"
      
        # Optional label/assignee. Empty-array expansion is guarded with the
        # `${arr[@]+...}` idiom so it stays safe under `set -u` on bash 3.2 (macOS).
        extra_args=()
        [ -n "$LABEL" ]    && extra_args+=(--label "$LABEL")
        [ -n "$ASSIGNEE" ] && extra_args+=(--assignee "$ASSIGNEE")
      
        url=$(gh issue create \
          --repo "$TARGET_REPO" \
          --title "$title" \
          --body-file "$body_file" \
          ${extra_args[@]+"${extra_args[@]}"})
      
        num=$(echo "$url" | grep -oE '[0-9]+$')
        [ -n "$num" ] || { echo "ERROR: could not parse issue number from: $url" >&2; exit 1; }
      
        # Rewrite that specific line: `## SYM-N — title` -> `## SYM-N (#NNN) — title`.
        # sed -i.bak works on both BSD and GNU sed; we delete the backup after.
        sed -i.bak "${start_line}s|^## ${sym} — |## ${sym} (#${num}) — |" "$WORK_ITEMS"
        rm -f "$WORK_ITEMS.bak"
      
        echo "created: $sym -> $url"
      done
      
      # Report skipped (already-numbered) slices for transparency.
      already=$(grep -cE '^## [A-Z][A-Z0-9]*-[0-9]+ \(#[0-9]+\) — ' "$WORK_ITEMS" || true)
      echo "done — $already slice(s) carried existing issue numbers and were skipped"
      
    • link-blockers.sh 4 KB
      #!/usr/bin/env bash
      # Usage: link-blockers.sh <work-items-file> <target-repo>
      #
      # Reads the SYM -> #NNN mapping from `## <SYM-N> (#NNN) — ...` headings,
      # then walks each slice's `**Depends on.** ...` line and posts a native
      # blocked_by relationship per blocker via the GitHub Issue Dependencies
      # API (within-repo only).
      #
      # API contract (GA since 2025-08-21, no preview header required):
      #   POST /repos/{owner}/{repo}/issues/{issue_number}/dependencies/blocked_by
      #   body: { "issue_id": <blocker's global database id> }
      # The body field is the blocker's global `id` (unique across GitHub), not
      # its repo-local issue number. GitHub links up to 50 issues per relationship
      # type; a slice with more blockers than that fails loudly on the 51st POST
      # (set -e), which is acceptable: a work item with 50+ blockers is a planning
      # smell to fix upstream, not silently truncate here.
      # Docs: https://docs.github.com/en/rest/issues/issue-dependencies
      #
      # Errors if a blocker SYM has no #NNN mapping in this file — that means
      # either the slice wasn't created yet (run create-issues.sh first) or the
      # Depends on line references a cross-repo SYM, which is forbidden by the
      # skill's rules (cross-repo deps belong in the preamble integration table).
      
      set -euo pipefail
      
      WORK_ITEMS="${1:?work-items file required}"
      TARGET_REPO="${2:?target repo (org/name) required}"
      
      [ -f "$WORK_ITEMS" ] || { echo "work-items file not found: $WORK_ITEMS" >&2; exit 1; }
      
      # Portable across bash 3.2 (macOS default) and bash 4+ — no associative arrays.
      # Each sym -> num mapping is stored as a flat shell variable named
      # SYM_TO_NUM__<sym-with-dashes-as-underscores>.
      sym_var() { echo "SYM_TO_NUM__$(echo "$1" | tr '-' '_')"; }
      sym_set() { eval "$(sym_var "$1")=\"$2\""; }
      sym_get() { eval "echo \"\${$(sym_var "$1"):-}\""; }
      
      SYM_COUNT=0
      while IFS= read -r line; do
        sym=$(echo "$line" | sed -E 's/^## ([A-Z][A-Z0-9]*-[0-9]+) \(#[0-9]+\) — .*/\1/')
        num=$(echo "$line" | sed -E 's/^## [A-Z][A-Z0-9]*-[0-9]+ \(#([0-9]+)\) — .*/\1/')
        sym_set "$sym" "$num"
        SYM_COUNT=$((SYM_COUNT + 1))
      done < <(grep -E '^## [A-Z][A-Z0-9]*-[0-9]+ \(#[0-9]+\) — ' "$WORK_ITEMS")
      
      if [ "$SYM_COUNT" -eq 0 ]; then
        echo "no created slices found in $WORK_ITEMS — run create-issues.sh first" >&2
        exit 1
      fi
      
      current=""
      linked=0
      
      while IFS= read -r line; do
        if [[ "$line" =~ ^##[[:space:]]+([A-Z][A-Z0-9]*-[0-9]+) ]]; then
          current="${BASH_REMATCH[1]}"
          continue
        fi
      
        if [[ "$line" =~ ^\*\*Depends\ on\.\*\*[[:space:]]+(.+)$ ]]; then
          deps="${BASH_REMATCH[1]}"
          deps="${deps//\`/}"           # strip backticks (e.g., `None.` → None.)
          deps="${deps%.}"              # strip trailing period
          deps=$(echo "$deps" | xargs)  # trim
      
          if [ "$deps" = "None" ] || [ -z "$deps" ]; then
            continue
          fi
      
          blocked_num=$(sym_get "$current")
          if [ -z "$blocked_num" ]; then
            echo "ERROR: $current has Depends on but no #NNN mapping — was it created?" >&2
            exit 1
          fi
      
          IFS=',' read -ra blockers <<< "$deps"
          for raw in "${blockers[@]}"; do
            b=$(echo "$raw" | xargs)
            b="${b%.}"
            [ -z "$b" ] && continue
      
            blocker_num=$(sym_get "$b")
            if [ -z "$blocker_num" ]; then
              echo "ERROR: $current Depends on $b, but $b has no #NNN mapping in this file." >&2
              echo "  Cross-repo dependencies must live in the preamble integration table, not in Depends on." >&2
              echo "  If $b is in this repo, run create-issues.sh first." >&2
              exit 1
            fi
      
            # `.id` is the blocker's global database id (what the API's issue_id
            # field expects), not `.number`. `-F` sends it as a JSON integer.
            blocker_id=$(gh api "repos/$TARGET_REPO/issues/$blocker_num" --jq .id)
            gh api --method POST \
              "repos/$TARGET_REPO/issues/$blocked_num/dependencies/blocked_by" \
              -F issue_id="$blocker_id" \
              --jq '.issue_dependencies_summary' >/dev/null
            echo "linked: $current(#$blocked_num) blocked_by $b(#$blocker_num)"
            linked=$((linked + 1))
          done
        fi
      done < "$WORK_ITEMS"
      
      echo "done — $linked dependency link(s) created"
      
    • publish-work-items.sh 1.2 KB
      #!/usr/bin/env bash
      # Usage: publish-work-items.sh <work-items-file> <target-repo> <plan-folder> [--label <name>] [--assignee <user>]
      #
      # Runs the three publish steps in order:
      #   1. upload-screenshots.sh — copies UI screenshots into the target repo
      #   2. create-issues.sh      — creates one issue per slice, annotates the
      #                              work-items file with the resulting #NNN
      #   3. link-blockers.sh      — posts native blocked_by links per Depends on
      #
      # Issues are created with no label and no assignee by default. Optional
      # `--label <name>` and `--assignee <user>` flags are forwarded to
      # create-issues.sh, which applies them to every issue.
      #
      # Each step is idempotent. Re-running after a partial failure resumes
      # without duplicating completed work.
      
      set -euo pipefail
      
      DIR="$(cd "$(dirname "$0")" && pwd)"
      
      WORK_ITEMS="${1:?work-items file required}"
      TARGET_REPO="${2:?target repo (org/name) required}"
      PLAN_FOLDER="${3:?plan folder required}"
      shift 3 || true
      # Remaining args (optional --label/--assignee) pass through to create-issues.sh.
      
      "$DIR/upload-screenshots.sh" "$WORK_ITEMS" "$TARGET_REPO" "$PLAN_FOLDER"
      "$DIR/create-issues.sh" "$WORK_ITEMS" "$TARGET_REPO" "$@"
      "$DIR/link-blockers.sh" "$WORK_ITEMS" "$TARGET_REPO"
      
    • upload-screenshots.sh 11.7 KB
      #!/usr/bin/env bash
      # Usage: upload-screenshots.sh <work-items-file> <target-repo> <plan-folder>
      #
      # Copies every visual-material file referenced in the work-items file's embeds
      # from <plan-folder>/ui-designs/<file> into <target-repo> at
      # .github/issue-assets/<feature-slug>/<SYM-N>/<file>. The accepted file types are
      # the set named in han-planning/references/planning-boundary-rule.md, kept in
      # ASSET_EXT_PATTERN below: png, jpg, jpeg, gif, webp, svg, pdf. The <feature-slug>
      # segment keeps assets from different features that publish to the same repo
      # from colliding — every feature restarts its work-item numbering at <PREFIX>-1,
      # so a flat <SYM-N> namespace would commingle (and risk overwriting) a prior
      # feature's screenshots. The slug is read verbatim from the embedded URL, which
      # the work-items file authored as the kebab-cased basename of the plan folder
      # (see references/screenshot-embed-rules.md).
      #
      # The upload mechanism is adaptive:
      #   * By default each file is written directly to the repo's default branch via
      #     the GitHub Contents API. This is fast and fully autonomous — the common
      #     case for an unprotected repo.
      #   * If the default branch is protected and rejects the direct write (HTTP 409,
      #     "changes must be made through a pull request"), the script falls back to
      #     committing every file to an assets branch and opening a pull request, then
      #     prints the PR URL. The embedded image URLs always point at the default
      #     branch, so the inline designs render once that PR merges.
      #
      # Idempotent in both modes: re-running overwrites existing blobs using their
      # current sha, reuses an existing assets branch and its open PR, and never
      # duplicates work.
      #
      # Exits 0 with no work when the work-items file contains no screenshot URLs.
      
      set -euo pipefail
      
      WORK_ITEMS="${1:?work-items file required}"
      TARGET_REPO="${2:?target repo (org/name, e.g. acme/acme-web) required}"
      PLAN_FOLDER="${3:?plan folder required}"
      
      [ -f "$WORK_ITEMS" ] || { echo "work-items file not found: $WORK_ITEMS" >&2; exit 1; }
      
      UI_DESIGNS="$PLAN_FOLDER/ui-designs"
      
      # The accepted visual-material file types, as an extended-regex alternation. This
      # is the set named in han-planning/references/planning-boundary-rule.md, which the
      # planning skills persist into ui-designs/. Widening that set means widening this
      # pattern, references/screenshot-embed-rules.md, and references/issue-template.md
      # together: an upload that accepts a file type the template still writes a .png
      # extension for succeeds and renders a broken image.
      ASSET_EXT_PATTERN='(png|jpg|jpeg|gif|webp|svg|pdf)'
      
      # Extract every embedded same-repo raw URL pointing at
      # .github/issue-assets/<feature-slug>/<SYM>/<file>.<ext>.
      # A single embed `[![alt](URL)](URL)` produces the URL twice; sort -u dedupes.
      # Portable across bash 3.2 (macOS default) and bash 4+ — no mapfile.
      URLS=()
      while IFS= read -r url; do
        [ -n "$url" ] && URLS+=("$url")
      done < <(grep -oE "https://github\.com/${TARGET_REPO//\//\\/}/raw/[^/]+/\.github/issue-assets/[^/]+/[^/]+/[^)]+\.${ASSET_EXT_PATTERN}" "$WORK_ITEMS" | sort -u)
      
      if [ ${#URLS[@]} -eq 0 ]; then
        echo "no screenshot URLs for $TARGET_REPO in $WORK_ITEMS — nothing to upload"
        exit 0
      fi
      
      [ -d "$UI_DESIGNS" ] || { echo "ui-designs folder not found: $UI_DESIGNS" >&2; exit 1; }
      
      DEFAULT_BRANCH=$(gh repo view "$TARGET_REPO" --json defaultBranchRef --jq .defaultBranchRef.name)
      
      base64_encode() {
        if [ "$(uname)" = "Darwin" ]; then
          base64 -i "$1"
        else
          base64 -w 0 "$1"
        fi
      }
      
      # Parse each URL into parallel arrays (bash 3.2 has no associative arrays we can
      # rely on). Path shape: .github/issue-assets/<feature-slug>/<SYM>/<file>.<ext>
      # field positions under awk -F/ are: $1=.github $2=issue-assets $3=slug
      # $4=sym $5=file.
      PATHS=(); SYMS=(); FILES=(); SRCS=()
      FEATURE_SLUG=""
      for url in "${URLS[@]}"; do
        branch=$(echo "$url" | sed -E "s|^https://github\.com/${TARGET_REPO//\//\\/}/raw/([^/]+)/.*|\1|")
        path=$(echo "$url" | sed -E "s|^https://github\.com/${TARGET_REPO//\//\\/}/raw/[^/]+/||")
        slug=$(echo "$path" | awk -F/ '{print $3}')
        sym=$(echo "$path"  | awk -F/ '{print $4}')
        file=$(echo "$path" | awk -F/ '{print $5}')
        src="$UI_DESIGNS/$file"
      
        if [ "$branch" != "$DEFAULT_BRANCH" ]; then
          echo "ERROR: embedded URL references branch '$branch' but $TARGET_REPO default is '$DEFAULT_BRANCH'" >&2
          echo "  url: $url" >&2
          echo "  fix the work-items file to use the default branch, then re-run" >&2
          exit 1
        fi
      
        [ -f "$src" ] || { echo "ERROR: source file not found: $src" >&2; exit 1; }
      
        PATHS+=("$path"); SYMS+=("$sym"); FILES+=("$file"); SRCS+=("$src")
        FEATURE_SLUG="$slug"
      done
      
      # Write one asset file to <branch> at its Contents API path. Creates or overwrites,
      # fetching the current sha on that branch first so the PUT is idempotent.
      # Prints "added"/"updated" on stdout; gh errors flow to stderr. Returns the
      # PUT's exit status so the caller can distinguish a branch-protection rejection
      # from success. The `|| return $?` after each PUT is load-bearing: the trailing
      # echo would otherwise become the function's last command and mask a failed
      # PUT with status 0 — and because the probe calls put_file inside an `if`
      # condition, `set -e` is suppressed in this body and will not catch it either.
      put_file() {
        local branch="$1" api_path="$2" src="$3" sym="$4" file="$5"
        local content_b64 existing_sha
        content_b64=$(base64_encode "$src")
        # Gate on gh's exit status, not on the captured output. On a 404 (file not
        # yet present) gh prints the error body to stdout and does not apply --jq, so
        # capturing with `|| true` would leave the JSON error in existing_sha and
        # wrongly take the update path. The `|| existing_sha=""` resets it to empty
        # when the GET fails, so a missing file is correctly treated as an add.
        existing_sha=$(gh api "$api_path?ref=$branch" --jq .sha 2>/dev/null) || existing_sha=""
        if [ -n "$existing_sha" ]; then
          gh api --method PUT "$api_path" \
            -f message="issue-assets: update $file for $sym" \
            -f content="$content_b64" \
            -f branch="$branch" \
            -f sha="$existing_sha" >/dev/null || return $?
          echo updated
        else
          gh api --method PUT "$api_path" \
            -f message="issue-assets: add $file for $sym" \
            -f content="$content_b64" \
            -f branch="$branch" >/dev/null || return $?
          echo added
        fi
      }
      
      # Verify a path is visible on <branch> via the Contents API. Read-after-write is
      # eventually consistent, so retry briefly. We verify against the branch we wrote
      # to (the default branch in direct mode, the assets branch in PR mode) rather
      # than the public raw URL, because a private repo returns 404 to anonymous raw
      # requests — but GitHub's in-app image proxy still renders the embed for
      # authenticated viewers, so the issue body is correct.
      verify_on() {
        local branch="$1" api_path="$2"
        for _ in 1 2 3 4 5; do
          if gh api "$api_path?ref=$branch" --jq .sha >/dev/null 2>&1; then
            return 0
          fi
          sleep 2
        done
        return 1
      }
      
      upload_all_to() {
        local branch="$1" i action api_path
        for i in "${!PATHS[@]}"; do
          api_path="repos/$TARGET_REPO/contents/${PATHS[$i]}"
          action=$(put_file "$branch" "$api_path" "${SRCS[$i]}" "${SYMS[$i]}" "${FILES[$i]}")
          if ! verify_on "$branch" "$api_path"; then
            echo "ERROR: verification failed — ${PATHS[$i]} not visible on '$branch' after PUT (5 retries)" >&2
            exit 1
          fi
          echo "$action: ${PATHS[$i]}  (branch: $branch)"
        done
      }
      
      # Decide the mode by probing the direct write of the first asset. A success
      # means the default branch accepts direct commits (unprotected) and we continue
      # in direct mode with the rest. A branch-protection rejection switches the whole
      # run to PR mode; nothing was written by a rejected PUT, so there is no partial
      # state to undo. Any other failure is a genuine error and we surface it.
      FIRST_API="repos/$TARGET_REPO/contents/${PATHS[0]}"
      if PROBE_OUT=$(put_file "$DEFAULT_BRANCH" "$FIRST_API" "${SRCS[0]}" "${SYMS[0]}" "${FILES[0]}" 2>&1); then
        # Direct mode: first asset is already uploaded. Verify it, then do the rest.
        if ! verify_on "$DEFAULT_BRANCH" "$FIRST_API"; then
          echo "ERROR: verification failed — ${PATHS[0]} not visible on '$DEFAULT_BRANCH' after PUT (5 retries)" >&2
          exit 1
        fi
        echo "$PROBE_OUT: ${PATHS[0]}  (branch: $DEFAULT_BRANCH)"
        for i in "${!PATHS[@]}"; do
          [ "$i" -eq 0 ] && continue
          api_path="repos/$TARGET_REPO/contents/${PATHS[$i]}"
          action=$(put_file "$DEFAULT_BRANCH" "$api_path" "${SRCS[$i]}" "${SYMS[$i]}" "${FILES[$i]}")
          if ! verify_on "$DEFAULT_BRANCH" "$api_path"; then
            echo "ERROR: verification failed — ${PATHS[$i]} not visible on '$DEFAULT_BRANCH' after PUT (5 retries)" >&2
            exit 1
          fi
          echo "$action: ${PATHS[$i]}  (branch: $DEFAULT_BRANCH)"
        done
        echo "all screenshots uploaded and verified for $TARGET_REPO"
        exit 0
      fi
      
      # The direct probe failed. Only a branch-protection rejection is recoverable;
      # anything else is a real error.
      if ! echo "$PROBE_OUT" | grep -qiE 'pull request|protected branch|required status check|HTTP 409'; then
        echo "ERROR: failed to upload ${PATHS[0]} to $TARGET_REPO" >&2
        echo "$PROBE_OUT" >&2
        exit 1
      fi
      
      # PR mode. Commit every asset to a deterministic assets branch and open — or
      # reuse — a pull request into the default branch.
      #
      # The branch name is derived from the feature slug, so it is possible (if
      # unlikely) for a branch of that exact name to already exist for an unrelated
      # reason. Guard against ever adding commits to a branch this skill did not
      # create: a pre-existing branch is adopted only when it already carries this
      # feature's .github/issue-assets/<slug>/ tree — the signature of our own prior
      # run (including one that was interrupted before opening the PR). A same-named
      # branch without that tree is refused, so no foreign work is ever touched.
      ASSETS_BRANCH="issue-assets/$FEATURE_SLUG"
      echo "default branch '$DEFAULT_BRANCH' is protected — falling back to a pull request on '$ASSETS_BRANCH'"
      
      if gh api "repos/$TARGET_REPO/git/ref/heads/$ASSETS_BRANCH" >/dev/null 2>&1; then
        if gh api "repos/$TARGET_REPO/contents/.github/issue-assets/$FEATURE_SLUG?ref=$ASSETS_BRANCH" >/dev/null 2>&1; then
          echo "reusing existing assets branch: $ASSETS_BRANCH"
        else
          echo "ERROR: branch '$ASSETS_BRANCH' already exists in $TARGET_REPO but carries no" >&2
          echo "  '.github/issue-assets/$FEATURE_SLUG/' tree, so this skill did not create it" >&2
          echo "  for this feature. Refusing to commit onto a branch it does not own." >&2
          echo "  Inspect that branch: if it is safe to discard (e.g. an interrupted prior" >&2
          echo "  run that uploaded nothing), delete it and re-run; otherwise rename the" >&2
          echo "  plan folder so the assets branch gets a distinct name." >&2
          exit 1
        fi
      else
        base_sha=$(gh api "repos/$TARGET_REPO/git/ref/heads/$DEFAULT_BRANCH" --jq .object.sha)
        gh api --method POST "repos/$TARGET_REPO/git/refs" \
          -f ref="refs/heads/$ASSETS_BRANCH" \
          -f sha="$base_sha" >/dev/null
        echo "created assets branch: $ASSETS_BRANCH"
      fi
      
      upload_all_to "$ASSETS_BRANCH"
      
      PR_URL=$(gh pr list --repo "$TARGET_REPO" --head "$ASSETS_BRANCH" --state open --json url --jq '.[0].url // empty')
      if [ -z "$PR_URL" ]; then
        PR_URL=$(gh pr create --repo "$TARGET_REPO" \
          --head "$ASSETS_BRANCH" \
          --base "$DEFAULT_BRANCH" \
          --title "issue-assets: design screenshots for $FEATURE_SLUG" \
          --body "Adds issue design screenshots under \`.github/issue-assets/$FEATURE_SLUG/\`. Inline images in the related issues render once this merges.")
      fi
      
      echo
      echo "NOTE: $TARGET_REPO's default branch is protected, so design assets were"
      echo "      committed to branch '$ASSETS_BRANCH' and a pull request was opened:"
      echo "        $PR_URL"
      echo "      The issues are created now; their inline design images will render"
      echo "      once this assets PR merges."
      
  • SKILL.md 13.1 KB
    ---
    name: work-items-to-issues
    description: >
      Break a work-items.md file (produced by /plan-work-items) into independently-grabbable GitHub issues, one per slice,
      in each slice's target repo. Use when you want to turn a work-items file into GitHub issues, publish work items as
      issue tickets, or create implementation tickets that can be worked on and tracked on GitHub. Does not produce the
      work-items file itself — use plan-work-items to break a plan into work items first. Does not review code or post pull
      request comments — use post-code-review-to-pr for that.
    argument-hint:
      "[path to work-items.md] [target repo(s), e.g. org/repo] [--label name (optional)] [--assignee user (optional)]"
    allowed-tools:
      Read, Write, Edit, Glob, Grep, Bash(gh *), Bash(git *), Bash(find *),
      Bash(bash "${CLAUDE_PLUGIN_ROOT}/scripts/han-config-dir.sh")
    ---
    
    ## Project Context
    
    - personal config directory: !`bash "${CLAUDE_PLUGIN_ROOT}/scripts/han-config-dir.sh" 2>/dev/null || echo "$HOME/.claude"`
    - project .han/config.md: !`cat .han/config.md 2>/dev/null || echo ""`
    
    As your first action, use the Read tool on `.han/config.md` inside the `personal config directory` path above. A read
    that returns no file is no personal configuration: continue silently. When that file or the `project .han/config.md`
    probe supplies content, apply it per [config-rule.md](../../references/config-rule.md), which governs precedence
    between the two files, relative-path resolution, and what to do with a file that reads but cannot be used.
    
    # Work Items to GitHub Issues
    
    Take an already-broken-down `work-items.md` file (produced by `/plan-work-items`) and publish each slice as a GitHub
    issue in its target repo.
    
    The breakdown work — drafting slices, assigning symbolic IDs, specifying dependencies, inventorying references — has
    already been done upstream. This skill's job is to map each slice to its target repo, validate the format, write a
    per-repo work-items file alongside the source, and run the publish pipeline.
    
    ## Rules
    
    - Each slice lives in exactly one repo. Cross-repo coordination is documented in prose at the top of `work-items.md` —
      never as a native blocker link.
    - Native `blocked_by` relationships are **within-repo only**. A cross-repo `Depends on` is a format error to surface for
      repair.
    - Symbolic-ID prefixes: accept whatever the input uses. Both shapes are valid input — single-prefix across repos (e.g.,
      `W-N` for every slice) and per-repo prefixes (e.g., `V2-N` backend, `W-N` frontend, `EV-N` events). The publish
      scripts accept any uppercase prefix.
    - Every slice issue body MUST link the reference artifacts an implementer needs — API/event contracts, design frames,
      schema docs, runbooks, ADRs, coding standards. Issues that consume an HTTP endpoint or event payload MUST link the
      contract section that defines it.
    - UI slices, when the plan folder has a `ui-designs/` subfolder, MUST embed the relevant screenshots inline using
      same-target-repo raw URLs. See [references/screenshot-embed-rules.md](./references/screenshot-embed-rules.md).
    - NEVER include process artifacts in issue bodies or the work-items preamble. Excluded categories — iteration histories,
      decision logs, review findings, team findings, facilitation summaries, gap analyses, and anything under an
      `artifacts/` subfolder of the plan that is not a contract or design reference. Full include/exclude list in
      [references/reference-artifact-inventory.md](./references/reference-artifact-inventory.md).
    
    ## Process
    
    ### 1. Locate the work-items file
    
    If the path is not provided, ask for it. The input is a single `work-items.md` produced by `/plan-work-items`. Read it.
    
    If the user named a target repo (or repos), a label, or an assignee, note them for Steps 2 and 6. By default, issues are
    created with **no label and no assignee** — only apply a label or assignee when the user explicitly asked for one.
    
    ### 2. Build the SYM→repo map
    
    Determine which repo each slice belongs to. Use both signals and reconcile them:
    
    - **Primary — cross-repo work order prose.** Most `work-items.md` files include an intro paragraph naming which SYMs
      ship to which repo (e.g., "W-1 through W-4 ship to `acme-api`. W-5 through W-9 ship to `acme-web`."). Parse this for
      the mapping.
    - **Corroborating — file paths inside each slice.** Each slice's `**Work to be done.**` and `**References.**` blocks
      reference files in the target repo. Path roots map cleanly: `acme-api/...` → `acme/acme-api`, `acme-web/...` →
      `acme/acme-web`, `acme-events/...` → `acme/acme-events`. Use this to verify the prose and to assign any slice the
      prose doesn't cover.
    
    If the prose and the file-path evidence disagree for a slice, surface the conflict to the user before proceeding.
    
    ### 3. Validate the format with evidence-based repair
    
    Check the work-items file against the format invariants in
    [references/issue-template.md](./references/issue-template.md) and
    [references/work-items-file-format.md](./references/work-items-file-format.md):
    
    - **Heading shape.** Every slice heading matches `## <SYM-N> — <title>` with an em-dash separator (already-published
      headings annotated as `## <SYM-N> (#NNN) — <title>` are valid too).
    - **`Depends on` line.** Literal bold marker `**Depends on.**`, trailing period, `None.` or comma-separated SYMs.
    - **Within-repo blockers.** Every SYM named in a `Depends on` line maps to the same target repo as the dependent slice
      (under the map from Step 2).
    - **Screenshot URLs.** When present, match
      `https://github.com/<org>/<target-repo>/raw/<branch>/.github/issue-assets/<feature-slug>/<SYM-N>/<file>.<ext>` against
      the target repo's default branch and a real file under `<plan-folder>/ui-designs/`, whose extension is one of the
      accepted set (`png`, `jpg`, `jpeg`, `gif`, `webp`, `svg`, `pdf`) and is copied into the URL unchanged. `<feature-slug>` is the
      kebab-cased basename of the plan folder.
    - **References block.** Present whenever the slice consumes an HTTP endpoint, event payload, design frame, ADR, coding
      standard, or other named artifact.
    - **No process artifacts.** No links to iteration histories, decision logs, review findings, team findings, facilitation
      summaries, gap analyses, or anything under an `artifacts/` subfolder that is not a contract or design reference.
    
    When a check fails, attempt evidence-based repair. Pull evidence from the source `work-items.md`, the parent plan
    referenced in its intro, the feature spec in the same folder, sibling files in the plan folder, and the target repo's
    ADRs / coding standards / docs:
    
    - **Malformed heading** — propose the corrected shape based on the surrounding text. Cite the line number.
    - **Missing `Depends on` line** — propose `None.` if no blockers are evident in the slice's prose. Cite the absence.
    - **Cross-repo `Depends on`** — propose moving the relationship to the cross-repo work-order prose at the top of the
      file and replacing the line with `None.` or remaining within-repo SYMs. Cite the SYM→repo map entries that prove the
      cross-repo split.
    - **Missing References bullet for an HTTP-consuming slice** — propose the contract section link by inspecting the parent
      plan's External Interfaces / API Contracts section. Cite the anchor.
    - **Missing References bullet for a UI slice with `ui-designs/` present** — propose the design frame and screenshot
      files by inspecting the feature spec's Visual Reference table and the spec's inline screenshot embeds. Cite the spec
      section.
    - **Process-artifact link found** — propose removing the link and (if the slice still needs the context the artifact
      held) restating the decision in plain language, with a **Plan decisions** References bullet naming the ID plus a
      one-sentence description. Cite the include/exclude list.
    
    After validation, report findings in plain language. For each finding, name:
    
    1. **What is wrong** — slice SYM, line reference, the failing invariant.
    2. **What the proposed fill is** — the corrected line, new bullet, removed link, etc.
    3. **Evidence for the fill** — file path with line number, document section, or named source.
    
    Then give the user three actions:
    
    - **Continue with fills** — apply the proposed repairs to the source `work-items.md` (so the per-repo split files
      inherit them) and proceed to Step 4.
    - **Correct the fills** — user provides the right values; apply those and proceed.
    - **Stop** — exit without publishing. User edits the file by hand and re-runs.
    
    If validation passes with no findings, proceed silently to Step 4.
    
    ### 4. Show the SYM→repo map for confirmation
    
    Present a table for user review:
    
    | SYM | Title                                       | Target repo     |
    | --- | ------------------------------------------- | --------------- |
    | W-1 | Backend per-list validator generalization   | `acme/acme-api` |
    | W-2 | …                                           | `acme/acme-api` |
    | W-5 | Frontend type widening and drift comparator | `acme/acme-web` |
    
    Wait for confirmation before writing files or creating issues.
    
    ### 5. Write per-repo work-items files
    
    For each target repo named in the SYM→repo map, write a `<repo-name>.work-items.md` file in the same folder as the
    source `work-items.md`. The per-repo file is a filtered view of the source — it is the file the publish scripts consume:
    
    - **Header section.** Copy the source file's title, intro paragraph, and cross-repo work-order prose verbatim. This
      keeps each per-repo file self-contained for review.
    - **Shared reference artifacts.** Copy the source file's "Shared reference artifacts" section, filtered to entries that
      apply to at least one slice in this repo. When in doubt, include the entry.
    - **Slices.** Include only the slices whose SYM maps to this repo, in their original order from the source file.
    
    The source `work-items.md` is not modified by the publish step. The per-repo files are what carry the `(#NNN)`
    issue-number annotations after publishing.
    
    ### 6. Publish each per-repo file to GitHub
    
    For each per-repo file, publish it by running
    `${CLAUDE_SKILL_DIR}/scripts/publish-work-items.sh <per-repo-work-items-file> <org>/<target-repo> <plan-folder> [--label <name>] [--assignee <user>]`.
    Pass the per-repo work-items file written in Step 5, the target repo as `<org>/<target-repo>`, and the plan folder that
    contains the `ui-designs/` subfolder.
    
    Created issues are unassigned and carry no label by default. Append `--label <name>` and/or `--assignee <user>` only
    when the user asked for a label or assignee (Step 1). Both flags are optional and may be omitted.
    
    The wrapper runs three idempotent scripts in order:
    
    1. **`scripts/upload-screenshots.sh`** — extracts every `.github/issue-assets/<feature-slug>/<SYM-N>/<file>.<ext>` URL
       from the per-repo file and copies the matching file from `<plan-folder>/ui-designs/` into the target repo, verifying
       each upload. The `<feature-slug>` segment (the kebab-cased plan-folder basename) keeps assets from different features
       that publish to the same repo from colliding. Upload is adaptive: by default each file is written directly to the
       default branch via the GitHub Contents API, but if that branch is protected and rejects the direct write (HTTP 409),
       the script falls back to committing the assets to an assets branch, opening a pull request, and printing the PR URL.
       The embedded image URLs always name the default branch, so on the PR path the inline designs render once that assets
       PR merges — the issues are still created immediately. Overwrites existing files cleanly and, on re-run, reuses the
       assets branch and open PR — but only a branch it created for this feature (one already carrying the feature's
       `issue-assets/<feature-slug>/` tree); a same-named branch it does not own is refused rather than committed onto.
    2. **`scripts/create-issues.sh`** — creates one GitHub issue per `## <SYM-N>` slice in file order (blocker-first),
       unassigned and unlabeled by default. When `--label <name>` is passed, it applies that label to every issue, creating
       it on the repo only if it does not already exist (an existing label's color and description are left intact); when
       `--assignee <user>` is passed, it assigns each issue to that user. Captures each returned issue number and rewrites
       the heading in place to `## <SYM-N> (#NNN) — <title>`. Skips slices already annotated with `(#NNN)` so partial runs
       resume cleanly.
    3. **`scripts/link-blockers.sh`** — reads the SYM↔#NNN mapping from the rewritten headings, walks each `**Depends on.**`
       line, and POSTs `repos/<repo>/issues/<N>/dependencies/blocked_by` once per blocker. Errors out if a blocker SYM is
       not present in the same file (cross-repo dependencies are forbidden as native links — they belong in the cross-repo
       work-order prose).
    
    When `upload-screenshots.sh` reports that it fell back to PR mode (it prints a `NOTE:` with an assets-branch pull
    request URL), surface that PR URL to the user and tell them the issues' inline designs render only once that assets PR
    merges. This is a required follow-up action; do not summarize it away.
    
    The format invariants the scripts depend on (heading shape, URL scheme, `Depends on` syntax) are documented in
    [references/issue-template.md](./references/issue-template.md). Edits to that template require matching script changes.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related