Skip to content
Back to skills

Link Epics

ASecurity

Assign orphaned issues to existing EPICs, or cluster them into new-EPIC proposals, via `ll-issues link-epics`.

  • 6 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added October 6, 2026
ai-agentsgobashgit

Works with

  • cli

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned October 7, 2026

npx -y skills add BrennonTWilliams/little-loops --skill link-epics --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Link Epics?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Link Epics
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/brennontwilliams-link-epics/badge)](https://www.skillsdirectory.com/skills/brennontwilliams-link-epics)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: link-epics
description: Assign orphaned issues to existing EPICs, or cluster them into new-EPIC proposals, via `ll-issues link-epics`.
disable-model-invocation: true

argument-hint: "[--mode assign|synthesize] [--threshold <score>] [--auto] [--deep]"
model: sonnet
allowed-tools:
  - AskUserQuestion
  - Write
  - Edit
  - Read
  - Bash(ll-issues:*, git:*)

arguments:
  - name: flags
    description: "--mode assign|synthesize (default: assign); --threshold 0.5 to set the min score (default: config.issues.link_epics.min_score); --auto to apply/create proposals without prompting; --deep (synthesize mode only) to add an LLM-adjudicated clustering pass"
    required: false
metadata:
  short-description: Assign orphans to EPICs, or cluster them into new-EPIC proposals.
---

# Link Epics

Scoring, tiering, and clustering all live in `ll-issues link-epics` — this skill
only parses arguments, presents the CLI's proposals, and (in `synthesize` mode)
names/creates the resulting EPICs, since EPIC-file creation is not yet part of
the CLI (tracked separately as FEAT-2947).

- **`mode: assign`** (default) — score orphans against **existing** open EPICs;
  apply accepted proposals via the CLI's `--apply`.
- **`mode: synthesize`** — cluster orphans against each other; the CLI returns
  proposal clusters only, this skill names and creates the EPIC files.

`ll-issues link-epics` scores *text similarity* between orphan/EPIC titles —
distinct from `ll-issues clusters`, which visualizes existing *dependency-edge*
relationships between issues that already declare `blocked_by`/`depends_on`.

---

## Step 1: Parse Arguments

- `MODE` from `--mode <value>`. Default: `assign`.
- `THRESHOLD` from `--threshold <value>` if present; otherwise omit the flag and
  let the CLI fall back to `config.issues.link_epics.min_score`.
- `AUTO=true` if `--auto` is present.
- `DEEP=true` if `--deep` is present. Only meaningful with `MODE=synthesize`; the
  CLI itself rejects `--deep` with `assign` (or `MODE` omitted), so no
  argument-parsing check is needed here — just forward it through in S1 below.

---

## Exclusion Report

Both modes' JSON always include `skipped_malformed_metadata`, `skipped_intentional`,
`skipped_children_listed`, `malformed_metadata`, and `children_listed_drift`. Orphans counted
there were removed **before** scoring; never propose or hand-write a parent for them. When any
counter is non-zero or a list is non-empty, show it ahead of everything else:

```
Skipped N orphan(s): X malformed metadata, Y intentionally parentless, Z already listed in an EPIC's Children
  FEAT-1: parenting key(s) after the frontmatter fence (parent) — run `ll-issues format-check FEAT-1 --fix --apply`
  FEAT-2: listed in EPIC-3 (open), EPIC-4 (done) — add the back-reference or fix the listing (`ll-issues epic-consistency`)
```

- `malformed_metadata[].keys` names keys only; point the user at `ll-issues format-check <ID> --fix --apply`.
- In `children_listed_drift[].epics[]`, `blocks_proposal: false` (a `done`/`cancelled` EPIC) is
  informational: that orphan may still appear in `proposals`/`clusters`. `excluded_reason` is
  `null` for those.
- An orphan with a recorded `parentless_reason` is intentional; do not suggest a parent.

---

## Mode: `--mode assign` (default)

### A1: Get Proposals

```bash
ll-issues link-epics --mode assign --json ${THRESHOLD:+--threshold "$THRESHOLD"}
```

Parse `{"proposals": [{orphan_id, epic_id, score, tier}, ...], "applied": []}`. Every
payload also carries the exclusion report — see **Exclusion Report** below — **display it
before** any empty-result early return. If `proposals` is empty, report:
```
No orphan-to-EPIC proposals found above the score threshold.
```
Stop.

### A2: Proposal Flow

**Interactive (no `--auto`)**: present proposals via `AskUserQuestion`:

```yaml
questions:
  - question: "Link these orphaned issues to their proposed epics? Select all you want to apply."
    header: "Proposals"
    multiSelect: true
    options:
      - label: "ENH-123 → EPIC-42 (HIGH 0.82)"
        description: "orphan title — epic title"
```

`ll-issues link-epics --apply` applies the single highest-ranked EPIC for **every** orphan
at or above `THRESHOLD`; it has no single-pair apply, and a score threshold cannot represent
an arbitrary accepted subset or select a non-top EPIC. Run A3 only when the user accepts all
proposals. If they accept only some, or want a different EPIC than the top-ranked one, do
not run `--apply`; apply each such pair individually instead with
`ll-issues link <ORPHAN_ID> --parent <EPIC_ID>` (add `--reparent` only to replace an
existing parent). If nothing is selected, report
`No assignments made.` and stop.

**Auto (`--auto`)**: skip the prompt, go straight to A3.

### A3: Apply Assignments

```bash
ll-issues link-epics --mode assign --apply --json ${THRESHOLD:+--threshold "$THRESHOLD"}
```

This writes `parent:`/`epic:` on each orphan and appends `- **ID** — <title> (open)` to the
target EPIC's `## Children` section (idempotent — reapplying a pair changes nothing). An
EPIC with no exact `## Children` heading is left untouched and its pair carries
`"children_wired": false` in `applied`. Pairs that cannot be applied appear in the
JSON `rejected` list (`reason`: `conflicting_parent`, `ambiguous_children_section`,
`lock_timeout`, `metadata_unsafe`, `write_failed`) and the command exits 1; report them.
A `write_failed` rejection means the orphan was linked but the EPIC bullet was not
written — re-running `link-epics --apply` will **not** repair it (the orphan is no longer
parentless); run `ll-issues epic-consistency --fix <EPIC>` (or re-run
`ll-issues link <ORPHAN_ID> --parent <EPIC>`). For a `conflicting_parent` rejection, the
user can choose to override it explicitly with `ll-issues link <ORPHAN_ID> --parent <EPIC>
--reparent`. Stage the touched files:

```bash
git add -u {{config.issues.base_dir}}/
```

### A4: Report Results

```
Applied N orphan→EPIC assignment(s):

  ✓ ENH-123 → EPIC-42 (HIGH 0.82)
  ✓ BUG-55  → EPIC-42 (MEDIUM 0.51)

Files staged. Run /ll:commit to commit the changes.
```

---

## Mode: `--mode synthesize`

### S1: Get Cluster Proposals

```bash
ll-issues link-epics --mode synthesize --json ${THRESHOLD:+--threshold "$THRESHOLD"} ${DEEP:+--deep}
```

Parse `{"clusters": [{member_ids, placeholder_title, modal_priority,
pairwise_min_score, evidence, source}, ...], "applied": []}` — `evidence`/`source`
are only present on `--deep` runs (ENH-2979). `--apply` is **not** supported for
this mode (EPIC creation is deliberately kept out of the CLI — FEAT-2947).

If a top-level `"deep"` key is present (`--deep` skipped the LLM pass because the
orphan count exceeded the 40-orphan cap), surface it as a warning before
continuing with the score-based `clusters` the CLI still returned:
```
⚠ --deep skipped: {deep.count} orphans exceeds the 40-orphan cap; showing the score-based clusters only.
```

Display the **Exclusion Report** (below) before this empty-result check. If `clusters` is empty, report (mode-aware — `--deep` found no thematic or
vocabulary-based groupings, vs. plain scoring finding no vocabulary overlap):
```
No orphan clusters found above the score threshold — nothing to synthesize.          # without --deep
No orphan clusters found (vocabulary- or LLM-adjudicated) — nothing to synthesize.    # with --deep
```
Stop.

### S2: Name and Validate Clusters

For each cluster, review `placeholder_title` (frequency-derived from member
titles, or LLM-proposed for a `--deep`-sourced cluster) and `member_ids`. Replace
the placeholder with a clearer title when it's awkward or ambiguous; sanity-check
that every member actually belongs (drop odd-fit members from the proposal rather
than forcing the CLI's transitive grouping — clustering can chain unrelated
issues together through a shared intermediate).

When `source` is present (`--deep` runs), display it and the cited `evidence`
alongside each cluster, e.g.:
```
[Duplication Cleanup] ENH-10, ENH-22 (source: deep)
  Evidence: "predicate duplication in autodev", "heuristic duplication in refine-issue"
```
Clusters with `source: deep` have **no lexical corroboration** — they were found
purely by the LLM's thematic read, not by shared vocabulary — so give them a closer
look than `source: jaccard`/`merged` clusters before accepting.

### S3: Proposal Flow

**Interactive (no `--auto`)**: one `AskUserQuestion`, `multiSelect: true`, options
sorted by descending cluster size:

```yaml
questions:
  - question: "Which EPIC proposals should be created? Select all you want to create."
    header: "EPIC Proposals"
    multiSelect: true
    options:
      - label: "Cluster 1 → new EPIC \"CLI Output Format\" (5 issues)"
        description: "FEAT-10, BUG-22, ENH-31, FEAT-45, ENH-67"
```

If nothing is selected, report `No EPICs created.` and stop.

**Auto (`--auto`)**: create an EPIC for every returned cluster.

### S4: Create Accepted EPICs and Write-Back

For each accepted cluster:

1. **Allocate EPIC ID** via `ll-issues next-id`, called **immediately before each
   `Write`** — never batch-allocate upfront. If the PostToolUse hook reports the
   file was deleted (duplicate integer ID), call `ll-issues next-id` again and
   retry.
2. **Determine path**:
   `{{config.issues.base_dir}}/epics/<PRIORITY>-<EPIC_ID>-<slugified-title>.md`
   (slugify: lowercase, non-alphanumeric → `-`, collapse repeats). `<PRIORITY>` is
   the cluster's `modal_priority`.
3. **Write the EPIC file** with `Write`:

```markdown
---
id: EPIC-NNN
title: <validated title>
type: EPIC
priority: <modal_priority>
status: open
captured_at: "<TODAY, date -u +%Y-%m-%dT%H:%M:%SZ>"
discovered_date: <DATE_ONLY, date -u +%Y-%m-%d>
discovered_by: link-epics
relates_to: []
---

# EPIC-NNN: <validated title>

## Summary

Group of <N> related issues: <member titles, comma-separated>.

## Children

- **CHILD_ID_1** — child issue 1 title (open)
- **CHILD_ID_2** — child issue 2 title (open)
```

> **Note (ENH-162)**: `relates_to:` is reserved for peer/see-also cross-references
> between EPICs and sibling issues — never list child IDs there; containment is
> `parent:` on each child plus the `## Children` section above.

4. **Write `parent:`/`epic:` back to each child.** The new EPIC did not exist
   when `ll-issues link-epics --mode assign` last ran, so its `--apply` path
   cannot cover this write — insert `parent: EPIC-NNN` and `epic: EPIC-NNN`
   into each child's frontmatter block directly (same fields `apply_assignment()`
   writes for `assign` mode). If `parent:` already has a non-null value, skip
   and log: `⚠ CHILD_ID already has parent: <existing_value>, skipping.`
5. **Stage all files** by explicit path (`git add "<epic_path>"`,
   `git add "<child_path>"` per child) — never `git add .issues/` (sweeps unrelated
   files; see BUG-1976).

### S5: Report Results

```
Created N EPIC(s) from M orphaned issue(s):

  ✓ EPIC-42 "CLI Output Format" (5 issues)
      • FEAT-10 — issue title
      • BUG-22  — issue title

Files staged. Run /ll:commit to commit the changes.
```

If nothing was created (user declined all proposals), report:
```
No EPICs created. Run /ll:link-epics --mode assign to assign orphans to existing EPICs.
```

---

## Choosing a Mode

Run `--mode synthesize` first when no EPICs exist yet, or orphaned issues don't fit
any existing EPIC — it clusters orphans by thematic similarity and proposes new EPIC
files. Then run `--mode assign` (the default) to link any remaining orphans to the
newly created (or pre-existing) EPICs.

## Usage Examples

```bash
/ll:link-epics                              # assign mode, interactive
/ll:link-epics --auto                       # assign mode, apply without prompting
/ll:link-epics --threshold 0.4              # assign mode, interactive, custom threshold
/ll:link-epics --mode synthesize            # synthesize mode, interactive
/ll:link-epics --mode synthesize --auto     # synthesize mode, create all clusters
/ll:link-epics --mode synthesize --deep     # + LLM-adjudicated clusters for same-theme,
                                             # different-vocabulary orphans (ENH-2979)
```

Files in this skill

  • SKILL.md9.6 KB
  • agents/openai.yaml148 B

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…