Delegate workflow installation to the skills CLI

Replace per-tool distribution generation with a canonical committed skills build so installation and updates share one format.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
2026-08-20 13:42:31 -07:00
parent 4cbf426df2
commit 32d1487dcf
27 changed files with 6126 additions and 984 deletions
@@ -0,0 +1,449 @@
# GitHub Issues Backend
> Part of plan2code-0-pathfinder — loaded at the top of EVERY session whose map lives on GitHub Issues. It re-expresses the local-file model in issue terms: where the map lives, where a question lives, how blocking, claiming, and resolving are done, and what stays on local disk regardless.
>
> **Local-file maps never load this file.** If `## Ground rules` says `**Backend:** local`, close it and use `questions.md`.
Everything the skill says about *judgement* is unchanged by the backend: the fog-vs-question test, the destination grill, one question per session, HITL is never self-answered, the Clearing Gate rubric. This file changes only *where the bytes go*.
---
## Why a second backend exists
Local files are private scratch — `specs/` is gitignored, so the map is yours alone and nobody else can see it, comment on it, or resolve a question in parallel. That is the right default for a solo effort.
A map on GitHub Issues buys three things local files cannot:
| | Local files | GitHub Issues |
|---|---|---|
| Visibility | One machine, one person | Anyone with repo access, in a UI they already have open |
| Blocking | A `Blocked by:` line only an agent reads | Native issue dependencies — GitHub greys out blocked issues in its own UI |
| Concurrency | One session at a time by construction | Several people can work unblocked questions at once; the assignee is a real lock |
It costs three things too, and the human must know all three before they pick it:
1. **Issues on a public repo are public.** The destination, the rejected alternatives, the codebase recon, the technical debt in the blast radius — all of it is world-readable the moment it is written. Never chart to a public repo's tracker anything that would embarrass the project or leak a customer.
2. **It writes to shared state.** A local map costs nothing to abandon. Twelve stale issues labelled `pathfinder:grill-hitl` on a team's tracker is litter someone has to clean.
3. **It needs `gh`, auth, and issues enabled.** More that can break, in a step whose whole job is to remove friction.
---
## Preflight — before the first write
Run these once, at Chart Step 1, **before** offering GitHub as an option. Any failure means GitHub is not offered at all; say why in one line and continue with local files.
| # | Check | Command | On failure |
|---|---|---|---|
| 1 | `gh` is installed | `gh --version` | Not offered — "no `gh` on this machine" |
| 2 | Authenticated | `gh auth status` | Not offered — "`gh` is not logged in" |
| 3 | Inside a repo with a GitHub remote | `gh repo view --json nameWithOwner,visibility,hasIssuesEnabled` | Not offered — "no GitHub remote here" |
| 4 | Issues are enabled | same call, `hasIssuesEnabled` | Not offered — "issues are disabled on this repo" |
| 5 | Write access | `gh api repos/<owner>/<repo> --jq .permissions.push` | Not offered — read-only access cannot chart |
Record `nameWithOwner` and `visibility` from check 3 — **`visibility` is not optional detail.** If it is `PUBLIC`, the offer must say so in the same breath, e.g. *"GitHub Issues — note `jparkerweb/plan2code` is public, so the whole map is world-readable."*
Once a map exists, preflight shrinks to checks 1 and 2. A session that cannot reach `gh` cannot work a GitHub map: say so and stop, rather than silently starting a local one.
### Labels
Create the label set at **Chart Step 5**, with the map issue — never during preflight, which must stay read-only until the human has actually picked `github`. `--force` makes it idempotent, so it is safe to re-run every session:
```bash
gh label create "pathfinder:map" --color 5319E7 --description "Pathfinder map" --force
gh label create "pathfinder:grill-hitl" --color 1D76DB --description "Decision only the human can make" --force
gh label create "pathfinder:research-afk" --color 0E8A16 --description "Fact-finding, agent alone" --force
gh label create "pathfinder:sketch-hitl" --color FBCA04 --description "Human reacts to something concrete" --force
gh label create "pathfinder:legwork-hitl" --color D93F0B --description "Manual work needing a human" --force
gh label create "pathfinder:legwork-afk" --color D93F0B --description "Manual work the agent can do" --force
gh label create "pathfinder:locked" --color B60205 --description "Hard to reverse; consequences recorded" --force
gh label create "pathfinder:out-of-scope" --color CFD3D7 --description "Ruled past the destination" --force
```
**Type and mode share one label**`grill-hitl`, not `grill` plus `hitl` — for the same reason the local `Type:` line is one token: two labels can drift apart, and a `research` question that has quietly become HITL is a question nobody is driving.
---
## The equivalence table
This is the whole mapping. Everything below expands a row.
| Local file model | GitHub Issues model |
|---|---|
| `specs/<idea>/pathfinder/map.md` | One issue, labelled `pathfinder:map`, titled `Map: <idea>` |
| `questions/NN-<slug>.md` | A **sub-issue** of the map, titled with the question name |
| `NN` ordering | The map's sub-issue order — the order they were created, which is dependency order |
| `## Question` in the file | The issue body |
| `## Answer` appended | A comment on the issue, opening `## Answer` |
| `## Evidence` | A comment opening `## Evidence` (a research subagent writes its own) |
| `Type:` line | The `pathfinder:<type>-<mode>` label |
| `State: open` | Issue open, **no assignee** |
| `State: claimed` | Issue open, **assigned** |
| `State: resolved` | Issue **closed as completed**, with an `## Answer` comment |
| `State: out-of-scope` | Issue **closed as not planned**, labelled `pathfinder:out-of-scope`, no `## Answer` |
| `Blocked by: 02, 04` | Native issue dependencies (`dependencies/blocked_by`) |
| `Locked: yes` | The `pathfinder:locked` label |
| `Claimed: <timestamp>` | GitHub's own assignment event in the timeline |
| `## Question Checklist` in `map.md` | **Nothing** — the frontier is a live query, not a written list |
| `## Not yet specified`, `## Out of scope`, `## Ground rules`, `## Destination`, `## Glossary` | The same sections, in the map issue body |
| `sketch-NN/` | Still local disk — see *What stays on local disk* |
| `PLAN-DRAFT-<YYYYMMDD>.md` | Still local disk — see *Handoff* |
### The checklist is deleted, not ported
In local mode `map.md` carries a `## Question Checklist` because a directory of files has no queryable state. GitHub has queryable state, so **the map issue body carries no checklist at all.** Closed questions get one line each under `## Decisions so far`; open questions are not listed anywhere.
This kills the single largest source of drift in the local backend — a checklist that disagrees with the files — and it is why Work Step 2's reconcile pass is much shorter here.
---
## Refer by name
Unchanged, and harder to get right here because GitHub hands you a number for everything. In prose the human reads, write `[Export format](https://github.com/o/r/issues/42)` — never `#42`, never "issue 42", never a bare number. A wall of `#42, #43, #44` is illegible; names read at a glance.
Bare `#<n>` appears in exactly two places: inside a fallback `Blocked by:` body line when native dependencies are unavailable, and inside a `gh` command.
---
## Chart Steps 3-4 — hold the recon, protect the off-ramp
In `local` mode Step 3 writes `questions/00-codebase-context.md` the moment the recon is done, because a file in gitignored scratch costs nothing if the session then takes the Step 4 off-ramp. **On a shared tracker it costs something**: a stray issue nobody asked for, on a repo other people are watching.
So on `github`, Step 3 does the recon and **holds it in the session**. It becomes an issue at Step 6, alongside the other questions.
If the Step 4 breadth-first grill surfaces **no fog**, the off-ramp fires before anything has been created:
1. Write the recon to `specs/<idea>/pathfinder/questions/00-codebase-context.md` — a **local file**, exactly as the local backend would. It is what `/plan2code-1-plan` Phase 2 needs, and it is too valuable to throw away.
2. Create **nothing** on the tracker. No map issue, no question issues, no labels.
3. Tell the user plainly and STOP.
The tracker only ever sees an effort that earned a map.
---
## Creating the map (Chart Step 5)
Title is `Map: <idea>` — the kebab-case idea name, verbatim, so `gh issue list --label pathfinder:map` reads as an index of efforts.
```bash
gh issue create --label "pathfinder:map" --title "Map: audit-log-export" --body-file - <<'EOF'
> Pathfinder planning note - decisions, not implementation work. Archive with the spec; do not delete.
**Status:** Charting
**Updated:** 2026-08-08
**Confidence:** Requirements-clarity 8/25 · Feasibility-technical 6/25 · Integration-points 6/25 · Risk-assessment 5/25
<!-- Status: Charting (Chart Steps 5-6) -> Working (Chart Step 7) -> Cleared at the gate.
A fresh session routes on this line, so it must be correct before the session ends. -->
## Destination
A locked implementation plan for a compliance officer to export a filtered range of audit
events from the admin UI and receive them as a single downloadable file. The map ends at the
plan, not at shipped code. Continuous streaming to external systems is not on the route.
## Ground rules
- **Backend:** github — this issue is the map; questions are its sub-issues.
- `AGENTS.md` exists and governs. Its conventions are not re-litigated by any question here.
- One question _issue_ per session. `research` questions may run as parallel subagents.
- HITL questions are answered by the human in their own words. Never self-answered.
- Sketches are throwaway and live on local disk only, under `specs/audit-log-export/pathfinder/sketch-<issue>/`.
## Glossary
| Term | Meaning here | Avoid |
|---|---|---|
| Audit event | One row in `audit_events`: actor, tenant, action, target, timestamp, payload | log line |
## Decisions so far
<!-- The index — one line per CLOSED question: enough to judge relevance, then open the
issue for the detail. Open questions are NOT listed; they are open sub-issues. -->
## Not yet specified
<!-- The fog: in-scope areas you can see but cannot yet phrase as a question. Graduates into
sub-issues as answers land, and the graduated bullet is deleted from here. -->
## Out of scope
<!-- Work consciously ruled past the destination. Never graduates. One line each: gist plus why. -->
EOF
```
Two things that must be exact:
- **`**Status:**` is still a literal line in the body.** It is how a fresh session routes, exactly as in local mode. `Charting``Working``Cleared`.
- **`**Backend:** github` is the first `## Ground rules` bullet.** It is how a fresh session knows to load this file at all. Without it, a session that opens the map issue has no way to know which playbook it is in.
Confidence keeps the hyphenated `Requirements-clarity 8/25` form for the same reason it does in local mode — the metrics collector scrapes bare dimension words followed by digits, and would ingest a planning confidence nobody scored.
**Say once, at Step 5:** *"The map lives on `<owner>/<repo>`'s issue tracker — `<PUBLIC or PRIVATE>`, so `<world-readable / visible to anyone with repo access>`. Everything charted here is visible there."*
---
## Creating the questions (Chart Step 6) — two passes, not one
The local backend charts in a **single pass** because you choose `NN` yourself and can write `Blocked by: 02` into a file before `02` exists. **On GitHub that is impossible** — an issue has no id until the server assigns one, and a dependency edge needs the blocker's id. So charting here reverts to upstream's shape:
**Pass 1 — create every question issue, in dependency order.** Blockers first. The creation order becomes the sub-issue order, which becomes the reading order for the frontier and the trail, so it is doing the job `NN` does locally. Capture each new issue's number *and* database id as you go.
```bash
# Create, capturing the URL; the number is its last path segment.
gh issue create --label "pathfinder:grill-hitl" --title "Export format" --body-file - <<'EOF'
> Pathfinder planning note - decisions, not implementation work. Archive with the spec; do not delete.
## Question
What file format does an export produce, and what does a recipient need in order to trust
the file has not been altered?
Recommended answer to react to: CSV with a UTF-8 BOM plus a sidecar SHA-256 manifest.
EOF
# The database id — needed for BOTH wiring steps below. Not the #number, not the node_id.
gh api repos/<owner>/<repo>/issues/<number> --jq .id
```
**Pass 2 — wire the structure.** Two edges per question, both keyed on **database ids**:
```bash
# a) Attach as a sub-issue of the map. sub_issue_id is the CHILD's database id.
gh api --method POST repos/<owner>/<repo>/issues/<map-number>/sub_issues \
-F sub_issue_id=<child-db-id>
# b) Add each blocking edge. issue_id is the BLOCKER's database id.
gh api --method POST repos/<owner>/<repo>/issues/<blocked-number>/dependencies/blocked_by \
-F issue_id=<blocker-db-id>
```
**The database id is the single most common failure in this backend.** `gh api repos/o/r/issues/42 --jq .id` returns something like `2716143027`. The `42` is the *number*; `I_kwDO...` is the *node id*. The node id is rejected outright. The *number* is worse: a small integer like `42` is itself a perfectly valid database id — of some unrelated issue created years ago — so the call can succeed and silently attach the wrong thing. Fetch `.id` for every issue you are about to reference, and never hand-assemble one.
Charting still writes `00-codebase-context`'s equivalent — the Step 3 recon you held. Create it **first**, labelled `pathfinder:legwork-afk`, post the recon as an `## Answer` comment, and close it as completed in the same pass. It is resolved on arrival, exactly as in local mode, and it is what the handoff's `## System Context` is built from.
### If the endpoints are unavailable
Sub-issues and dependencies are recent GitHub features. On an instance that rejects either endpoint, fall back in the body — and say plainly, once, that the frontier will not render in GitHub's UI:
| Missing | Fallback |
|---|---|
| Sub-issues | Put `Part of #<map>` on the first line of each question body, and a task list of the questions in the map body |
| Dependencies | Put `Blocked by: #12, #14` on its own line at the top of the question body |
Prefer the native mechanisms every time they work. The whole reason to pay GitHub's costs is that the human sees the frontier in the UI without opening the map.
---
## The frontier query (Work Step 3)
The frontier is every question that is **open, unblocked, and unassigned**. Lowest position in sub-issue order wins — the same traversal `lowest NN first` gives locally.
```bash
# 1. The map's children, in order, with the state you need to filter on.
gh api repos/<owner>/<repo>/issues/<map-number>/sub_issues \
--jq '.[] | {number, title, state, assignee: .assignee.login,
blocked: .issue_dependencies_summary.blocked_by,
labels: [.labels[].name]}'
```
Then, in order:
1. Drop anything `state: closed` — that is resolved or out of scope.
2. Drop anything with an `assignee` — claimed by another session.
3. Drop anything still blocked.
4. The first survivor is the next question.
For step 3, `issue_dependencies_summary.blocked_by` counts **open** blockers, which is exactly the live gate — a blocker that closes drops the count without anyone editing anything. **Treat it as a fast pre-filter, not the authority.** If it comes back `null` or absent from the list response, or you need to *name* the blockers for the trail footer or a fully-blocked report, ask the endpoint that owns the answer:
```bash
gh api repos/<owner>/<repo>/issues/<n>/dependencies/blocked_by --jq '.[] | {number, title, state, reason: .state_reason}'
```
A question is unblocked when every blocker listed there is closed. That call is also the only way to see the next trap:
**A blocker closed as `not planned` is out of scope and will never resolve.** Its dependent is not merely blocked, it is *stranded* — the same trap as locally. Re-frame the dependent's body to drop the dependency, cut the edge, or rule it out too. Never leave it sitting: the summary count cannot tell you the difference, so this check is on you.
```bash
# Cut a dependency edge. The blocker's database id goes in the PATH here, not the body.
gh api --method DELETE \
repos/<owner>/<repo>/issues/<blocked-number>/dependencies/blocked_by/<blocker-db-id>
```
---
## Claim (Work Step 4)
```bash
gh issue edit <n> --add-assignee "@me"
```
**The session's first write, before any work.** The assignee *is* the claim — GitHub timestamps it for you, so there is no `Claimed:` line to maintain. An open, unassigned question is unclaimed; that is the whole protocol.
Unlike the local backend, other people may genuinely be working this map at the same time. Re-read the issue immediately after assigning; if someone else's login is on it, you lost the race — release yours and take the next frontier item.
---
## Resolve (Work Step 7)
Three writes, in this order. The order matters: the answer must exist before the issue closes, or a crash between them leaves a closed question with no decision in it.
```bash
# 1. The answer, as a comment. Same anatomy as a local ## Answer:
# decision, rejected alternatives with reasons, consequences, one-line **Gist:**.
gh issue comment <n> --body-file - <<'EOF'
## Answer
**Decision.** CSV, UTF-8 with a byte-order mark, RFC 4180 quoting, one header row.
Alongside it a sidecar `.sha256` manifest.
**Rejected.**
- *JSONL* — trivially streamable, but every named recipient opens these in Excel.
- *XLSX* — fixes Excel encoding, but adds a library and makes byte-level verification harder.
**Consequences.**
- Do not reuse the billing export's CSV writer; it concatenates strings with no escaping.
- Makes [Delivery channel](https://github.com/o/r/issues/45) sharper — the artifact is
self-verifying, so a short-lived link no longer weakens the integrity story.
**Gist:** CSV with a UTF-8 BOM and RFC 4180 quoting, plus a sidecar SHA-256 manifest;
JSONL rejected because recipients open these in Excel.
EOF
# 2. Close as completed.
gh issue close <n> --reason completed
# 3. Append the gist to the map's Decisions so far (read body, edit, write back).
gh issue view <map-number> --json body --jq .body > /tmp/map.md
# ...append: - [Export format](<issue-url>) — <gist>
gh issue edit <map-number> --body-file /tmp/map.md
```
Then bump `**Updated:**` and re-score `**Confidence:**` in the same map edit.
**Never edit the question body to hold the answer.** The body is the question as asked; the comment is the answer. Editing the body rewrites history and destroys the record of what was actually put to the human — which is half of why the answer is defensible three weeks later.
### Editing the map body safely
Every map mutation is read-modify-write on a body other sessions may be editing concurrently. Read it fresh immediately before the edit, apply your change to *that* text, and write it straight back. Never edit from a copy you read at the top of the session — you will silently revert whatever landed in between.
---
## Ruling a question out of scope (Work Step 8)
```bash
gh issue edit <n> --add-label "pathfinder:out-of-scope"
gh issue close <n> --reason "not planned"
```
Then one line under the map's `## Out of scope`, giving the name as a link plus the reason. **No `## Answer` comment** — there is no decision here, only a scope boundary. Add one comment saying why it is out, so the closed issue explains itself.
`not planned` versus `completed` is the load-bearing distinction: it is how a later session tells a decision that was made from a question that was ruled off the route, and it is what GitHub's UI shows at a glance. Getting it backwards puts a scope boundary into the Provenance table of the plan.
---
## Reconcile (Work Step 2)
Much shorter here — the tracker holds the state, so there is no checklist to rebuild. Three checks:
| Check | Symptom | Repair |
|---|---|---|
| Crashed mid-answer | Open, assigned, and an `## Answer` comment already exists | The comment wins. Close as completed, add the gist to Decisions so far, say so. |
| Stale claim | Open, assigned, no `## Answer`, and the assignee is you from a dead session | Unassign, say so, put it back on the frontier. **If it is someone else's login, leave it** — that is a live session, not a crash. |
| Index drift | A closed, completed question with no line under `## Decisions so far` | Read its `## Answer` comment, append the gist. |
Then re-read `## Not yet specified` in full — that part is identical to local mode, and the bullet left behind after its question exists is just as corrosive here.
---
## What stays on local disk
Three things never move to the tracker, whatever the backend:
| Artifact | Where | Why |
|---|---|---|
| Runnable sketches | `specs/<idea>/pathfinder/sketch-<issue-number>/` | Throwaway code has no business in an issue, and the quarantine rule (never in the project's own source tree) is unchanged. Link the path from the issue and note the reader needs the repo checked out. |
| `PLAN-DRAFT-<YYYYMMDD>.md` | `specs/<idea>/` | `/plan2code-1-plan` reads a **file**. This is a hard downstream contract — see Handoff. |
| Anything secret | Nowhere | Credentials, tokens, customer data. A `legwork` checklist says *where* a credential lives, never what it is — and on a public tracker that rule stops being a convention and starts being an incident. |
Sketch directories are named for the issue number rather than a local `NN`, so `sketch-42` belongs to the question at `#42`. Same rules otherwise: throwaway, one command to run, never merged.
Research subagents work the same way with one substitution: the brief carries the **issue URL** instead of a file path, and the instruction is to post findings as a comment opening `## Evidence` via `gh issue comment` — and to decide nothing. `## Answer` and the close are still written by the session that fired it.
---
## The trail footer
Identical in shape; the inputs come from the query instead of the checklist.
- **Heading** — `🧭 <idea> · <Status> · <closed>/<total> cleared`, where `<total>` excludes anything labelled `pathfinder:out-of-scope`.
- **Glyph order** — sub-issue order, the same order Chart Step 6 created them in.
- **Glyphs** — `●` closed as completed · `◉` open and assigned to you · `○` open, unassigned, unblocked · `⊘` open with `blocked_by > 0` · `⊝` closed as not planned.
- **Named legend** — names, never `#numbers`. `(blocked:<name>)` names the blocker rather than numbering it, since there is no stable `NN` to point at.
- **Confidence** — the plain-English line, from the map body's `**Confidence:**`.
Form A's resume command changes, because there is no local path to resume from:
```
NEXT STEP · start a new conversation and run:
`/plan2code-0-pathfinder https://github.com/<owner>/<repo>/issues/<map-number>`
```
Form B is unchanged — a turn that asks the human something still says `WAITING ON YOU`, still names the outstanding probes, and still emits no resume command.
---
## Handoff (The Clearing Gate)
The gate's four dimensions, the 18/25 bar, the hard caps, and the honesty rules are unchanged. Only the preflight and the plumbing differ.
**Preflight, GitHub form:**
| # | Check |
|---|---|
| 1 | Reconcile pass run (above) |
| 2 | Zero open sub-issues — `gh api .../sub_issues --jq '[.[] \| select(.state=="open")] \| length'` returns `0` |
| 3 | `## Not yet specified` in the map body is empty |
| 4 | Every completed question has an `## Answer` comment carrying a `**Gist:**` |
| 5 | `ls specs/<idea>/` shows no existing `PLAN-DRAFT-*.md` |
| 6 | No `## Answer` defers a choice to "whoever implements this" |
**The draft is written to local disk**, at `specs/<idea>/PLAN-DRAFT-<YYYYMMDD>.md`, with the byte-exact status line `**Status:** Phase 3 Complete - Resume at Phase 4`. This is not a preference. `/plan2code-1-plan` discovers its input with `ls specs/`; it has no notion of an issue tracker, and a draft that exists only as an issue is a draft the rest of plan2code cannot see. Create `specs/<idea>/` if charting never needed it.
Three substitutions inside the template:
| Local | GitHub |
|---|---|
| `**Planning record:** specs/<idea>/pathfinder/map.md` | `**Planning record:** <map issue URL>` |
| `[Export format](./pathfinder/questions/01-export-format.md)` | `[Export format](https://github.com/o/r/issues/42)` |
| `## Out of scope` copied line for line, only the link prefix changing | Copied line for line, links already absolute — **nothing changes at all** |
Everything else — the mapping table, Section 5 left empty, the two load-bearing `**Next:**` bullets, no scrapable confidence numbers — is unchanged.
**Freeze the map:**
1. Set `**Status:** Cleared` in the map issue body.
2. Bump `**Updated:**`.
3. Add `**Plan:** specs/<idea>/PLAN-DRAFT-<YYYYMMDD>.md` under the status line.
4. `gh issue close <map-number> --reason completed`.
5. **Close nothing else, delete nothing, edit no answers.** Every question issue stays exactly as it is — it is the rationale record behind the plan.
Closing the map is the one addition over local mode, and it earns its place: `gh issue list --label pathfinder:map --state open` then reads as *the efforts still being charted*, which is the question a person scanning the tracker actually has.
A later session that opens a map issue reading `Status: Cleared` must not resume it. Point at the PLAN-DRAFT and `/plan2code-1-plan`, and stop. A redrawn destination is a fresh effort with a fresh map issue.
---
## Failure modes
| Symptom | Cause | Fix |
|---|---|---|
| `Not Found` from a `sub_issues` or `dependencies` POST | An issue *number* was passed where a database *id* is required | `gh api repos/o/r/issues/<n> --jq .id`, retry with that |
| The wrong issue got attached | A number from another repo happened to be a valid id | Detach, re-fetch `.id` from the right repo, re-attach |
| Frontier is empty but open questions remain | Every one is blocked, or every one is assigned | Name the chain and stop — or, if the assignees are stale claims from your own dead sessions, reconcile first |
| A blocked question never unblocks | Its blocker was closed as `not planned` | Stranded. Re-frame to drop the dependency and cut the edge, or rule it out too |
| Two sessions resolved the same question | The claim was written after the work, not before | Claim is the *first* write. Merge the two answers into one comment, keep one close |
| A map edit lost someone's line | The body was edited from a stale copy read earlier in the session | Re-read the body immediately before every map write |
| `/plan2code-1-plan` finds nothing to resume | The PLAN-DRAFT was posted as an issue instead of written to `specs/<idea>/` | Write the file. The draft is always local |
| Secrets in the tracker | A `legwork` answer pasted a credential | Rotate the credential first, then delete the comment. Editing it is not enough — GitHub keeps the edit history |