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
Install
npx skills add https://github.com/testdouble/han/tree/main/han-github/skills/work-items-to-issues
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install testdouble-han@llmmart
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_byrelationships are within-repo only. A cross-repoDepends onis 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-Nfor every slice) and per-repo prefixes (e.g.,V2-Nbackend,W-Nfrontend,EV-Nevents). 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.mdfiles include an intro paragraph naming which SYMs ship to which repo (e.g., "W-1 through W-4 ship toacme-api. W-5 through W-9 ship toacme-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 online. Literal bold marker**Depends on.**, trailing period,None.or comma-separated SYMs.- Within-repo blockers. Every SYM named in a
Depends online 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 online — proposeNone.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 withNone.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:
- What is wrong — slice SYM, line reference, the failing invariant.
- What the proposed fill is — the corrected line, new bullet, removed link, etc.
- 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:
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'sissue-assets/<feature-slug>/tree); a same-named branch it does not own is refused rather than committed onto.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.scripts/link-blockers.sh— reads the SYM↔#NNN mapping from the rewritten headings, walks each**Depends on.**line, and POSTsrepos/<repo>/issues/<N>/dependencies/blocked_byonce 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>* — `[](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 `` 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>* — [](<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 `[](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.
Reviews (0)
No reviews yet.
No comments yet.