ship-issue-gh — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited ship-issue-gh (Agent Skill) and scored it 87/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 3 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 3 flagged
The text {match} tells the agent to skip the normal "ask the user first" gate. Used adversarially it removes the human-in-the-loop check before destructive or sensitive actions, turning a normally-gated agent into a fire-and-forget executor.
The text {match} tells the agent to skip the normal "ask the user first" gate. Used adversarially it removes the human-in-the-loop check before destructive or sensitive actions, turning a normally-gated agent into a fire-and-forget executor.
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Drive a GitHub issue (or ordered list of issues, or parent issue with a managed ## Sub-issues task-list) from pending to merged through the implement → open-PR → address-review → merge loop. GitHub-only. Stateless across sessions — GitHub Issues + the PR's check runs are the sources of truth.
Usage: /ship-issue-gh <arg> — the skill self-arms its own /loop. Invoke once and walk away.
Where <arg> is one of:
<owner>/<repo>#<n> (or #<n> when invoked inside the target repo's git working copy)#<n> shorthand in a list, since the list can span repos): <owner>/<repo>#<65>,<owner>/<repo>#<66>## Sub-issues task-list — children are resolved from the task-list and walked in body ordermilestone:<owner>/<repo>/<num-or-name> — expands to the milestone's non-terminal issues, ordered by createdAt ascendingAny shape may carry a trailing --no-compact flag to suppress the compact-on-merge prompt — see ../_shared/compact-on-merge.md.
The architecture (state machine, blocked-user triggers, escape hatches, locked decisions) lives inDESIGN.mdalongside this file. The label scheme + marker comments + task-list fence live in../scaffold-sub-issues-gh/github-conventions.md. Read both before changing behavior. This file is the operational procedure.
Normalize $ARGUMENTS into an ordered list of fully-qualified <owner>/<repo>#<n> IDs.
Flag extraction (before shape detection): detect and strip a trailing --no-compact flag from $ARGUMENTS. When present, set no-compact mode for this invocation — the compact-on-merge prompt (Phase 4 § merged) is skipped at every trigger boundary. Contract and rationale live in ../_shared/compact-on-merge.md. Shape detection below runs on the flag-stripped string.
https://github.com/<owner>/<repo>/issues/<n>, optionally with Enterprise host) → extract <owner>/<repo>#<n>, continue as single ID.<owner>/<repo>#<n> after trimming (no #<n> shorthand in lists — ambiguous across repos). Otherwise → blocked-user.<owner>/<repo> from git -C <cwd> remote get-url origin. If cwd is not in a git repo, or origin isn't a GitHub URL → blocked-user with reason bare-issue-num-needs-cwd-in-github-repo. Continue as single ID.gh issue view <n> --repo <owner>/<repo> --json body,labels,state,state_reason,closedByPullRequestsReferences,milestone. If the body contains a <!-- ship-epic:sub-issues:start --> ... <!-- ship-epic:sub-issues:end --> block with one or more - [ ] <ref> lines, expand to the children in body order (preserving [x]-completed entries for state derivation but skipping them for work selection). Otherwise, keep as single ID.For milestone:<owner>/<repo>/<num-or-name>:
gh api /repos/<owner>/<repo>/milestones?state=all --jq '.[] | select(.number == <num> or .title == "<name>")'. If no match → blocked-user with reason milestone-not-found:<spec>. Do not proceed to Phase 0.5 — no cron arming.gh api "/repos/<owner>/<repo>/issues?milestone=<number>&state=all" --paginate. The REST issues endpoint returns both issues and PRs — filter to issues only with .pull_request==null (PRs carry a pull_request object; issues don't). --paginate follows the Link header transparently, so there's no per-page cap to manage. (Avoid gh issue list --milestone <n> --search <cursor>: gh issue list exposes no cursor-pagination knob, so a --search "cursor" silently caps the result set — the gh api --paginate form is the correct unbounded expansion.)state=closed AND stateReason=completed is "Done"; state=closed AND stateReason=not_planned is "Canceled". Both are terminal — skip. Open issues are kept regardless of status:* labels.blocked-user with reason milestone-no-open-issues:<spec>. Do not arm the cron.createdAt ascending. Rewrite each as <owner>/<repo>#<n>. The resulting ordered list becomes the work queue. Phase 1 onward is unchanged.Ordering caveat (document inline on the first wake's output): ordering is createdAt ascending. Pass a comma-separated list in the desired order if you need to override.
Retain the raw arg string (as the user typed it — e.g. <owner>/<repo>#42,<owner>/<repo>#43, milestone:<owner>/<repo>/3, <owner>/<repo>#100, NOT the expanded list) for the self-arming check in Phase 0.5. It's the match key for CronList and CronDelete. For milestone args specifically, one loop polls the same milestone across wakes — new issues added to the milestone mid-flight get picked up on the next wake's re-derivation without spawning a second loop.
A --no-compact flag is part of the raw arg string — it stays in the cron entry's command so the opt-out survives every subsequent wake (the skill persists nothing locally; the cron arg is the only carrier — see ../_shared/compact-on-merge.md § --no-compact).
The user's order is respected. The skill does not re-prioritise.
Identical contract to the Linear sibling's Phase 0.5 — the skill arms its own /loop so the user invokes once and walks away. Runs on every wake, including loop-triggered ones; the idempotent match below makes subsequent wakes a no-op.
Used by both this phase's arm check and Phase 7's self-cancel — these two checks must stay in lockstep. Defined in ../_shared/cron-match.md; this skill is the `ship-issue-gh` consumer (<boundary-class> = alphanumeric, -, ,, /, # — the / and # exclusions are GitHub-ID specific; /loop interval 6m).
CronList to enumerate active scheduled tasks in the current session.<command-name> <raw-arg> directly without a /loop wrapper (the expected first-invocation case). Invoke Skill(skill: "loop", args: "6m <command-name> <raw-arg>") to arm the cron — substituting the captured `<command-name>`, not a hardcoded skill name. This is what makes the next wake's match check succeed against this cron entry. Then proceed to Phase 1.Match key is the full raw arg string. Two separate invocations with different args → two independent loops.
Proceed to Phase 1 in both cases — the first wake also does the work of the first iteration.
For each item in the list, apply the per-item resolution rule:
gh issue view <n> --repo <owner>/<repo> --json number,title,state,stateReason,labels,body,closedAt,closedByPullRequestsReferences,milestone. Capture labels.repo:.Count of repo: labels | Action |
|---|---|
| 0 | Use a workdir derived from the issue's <owner>/<repo> — look for a subdirectory of cwd matching <repo>. If missing, fall back to using cwd itself only if git remote get-url origin matches <owner>/<repo>. Otherwise → blocked-user. |
| 1 | Extract <name> from repo:<name>. Look for a subdirectory of cwd matching <name>. If missing → blocked-user with a note listing available subdirs. |
| 2+ | blocked-user — one item resolves to at most one repo. |
git -C <workdir> remote get-url origin resolves to the same <owner>/<repo> (or compatible host alias). If they disagree → blocked-user with reason repo-label-mismatches-workdir-remote.gh auth status --hostname <derived-from-remote-url>. If not authed → blocked-user with the auth command to run.Cache the resolved {workdir, owner, repo} tuple per issue for this invocation. Re-resolution on the next /loop wake is cheap and handles the case where the user added/removed a label mid-flight.
`gh` capability. This skill relies on --json body support on gh issue view. If a gh issue view ... --json body call fails because the flag is unsupported (very old gh) → blocked-user with reason gh-too-old-for-json-body and the upgrade command. No proactive gh --version probe — treat the first failing --json call as the signal.
Walk the list in order. For each item, derive its current state (Phase 3). The skill works on one issue at a time — serialisation is deliberate.
Skip items already in merged. Work the first non-terminal item. If any item is in failed or blocked-user, halt the whole loop — see Phase 7.
Gather, for the current issue:
gh api /repos/<owner>/<repo>/issues/<n>/comments --paginate --jq '.[] | {id, body, created_at, author: .user.login}'. Filter to bodies containing <!-- ship-issue: markers. Retain the full non-marker comment bodies too — the cancel scan below reads them.gh pr list --search "linked:<n>" is not valid (linked: is not a GitHub search qualifier). Build the linked-PR set by unioning three sources:closedByPullRequestsReferences field (from the Phase 1 fetch).gh pr list --repo <owner>/<repo> --search "<owner>/<repo>#<n> in:body" --state all --json number,state,headRefName,url,mergedAt.<n>- prefix: gh pr list --repo <owner>/<repo> --head <n>- --state all --json number,state,headRefName,url,mergedAt, or fetch all open PRs and client-side filter headRefName that begins with <n>- (the branch-derivation prefix from Phase 4).De-duplicate by PR number across the three sources.
/abc:review-epic-gh and abc:reviewer, which post via the reviews API) may use any of them, so reading only inline comments silently drops feedback and mis-derives pr-open:gh api /repos/<owner>/<repo>/pulls/<pr-num>/comments --paginate (each has body, created_at, user.login, in_reply_to_id).gh api /repos/<owner>/<repo>/pulls/<pr-num>/reviews --paginate (each has body, state, submitted_at, user.login). A review whose state is CHANGES_REQUESTED or COMMENTED with a non-empty `body` is actionable feedback; APPROVED / empty-body reviews (e.g. the wrapper review GitHub creates around inline-thread replies) are not. An empty-body `CHANGES_REQUESTED` isn't lost by this — its substance lives on the inline surface (gathered by surface 1), so dropping only the empty review row loses nothing.gh api /repos/<owner>/<repo>/issues/<pr-num>/comments --paginate (a PR reuses the issue-comments endpoint at the same number).Union the three into the PR's review-comment set, then filter to actionable items by timestamp (created_at / submitted_at) strictly after the last skill commit (the <!-- ship-issue:commit --> marker lookup) and by excluding the skill's own bookkeeping. Identify the skill's own items by marker/structure, not login: do not exclude by PR author (or any login filter) — in the two-session pattern the reviewer (/abc:review-epic-gh) and the PR author share one gh login, so a login-based exclusion would drop the reviewer's own review body and re-bury exactly the feedback ST-12 exists to surface. Exclude: the skill's own <!-- ship-issue:* --> comments (including the marker-tagged Fixed in <sha> replies from the fixing handler — step 5), the reviewer's <!-- review-epic:* --> marker-only comments, and inline replies the skill itself authored (in_reply_to_id set on a skill-authored body) — bookkeeping, not feedback. This filtered union is exactly what rows 3 / 3a / 4 mean by "review comments." Also fetch its check-runs via gh pr checks <pr-num> --repo <owner>/<repo> --json name,state,bucket and its behind-base signal via gh pr view <pr-num> --repo <owner>/<repo> --json mergeStateStatus (a value of BEHIND means the branch is behind its base — Phase 3.5's rebase trigger reads this).
Cancel scan. Scan the latest non-marker comments for a standalone cancel — case-insensitive, where the whole comment body (or its first line) trimmed equals cancel. On a match → terminal halt: derive failed with reason cancel-requested and run Phase 7's stop flow (CronDelete). A cancel buried mid-paragraph does not trigger — only a standalone-comment / first-line cancel. Scan all reviewer-reachable surfaces: the issue's own comments and (when an open PR exists) the three PR surfaces above — inline, review bodies, top-level. The Phase 4 fixing handler's @-mention scan runs over the same unioned set, for the same reason: a reviewer may cancel or @-mention on any surface, not only inline.
If any of these reads fail (non-zero exit, timeout, partial pagination the skill can't reconcile) → transition to blocked-user with reason cannot-read-issue-state:<tool>:<detail>. Do not fall through to the table — an unknown state is not "no match found." This matters most for row 1a: silently treating a failed comments list as "no verify:passed marker" would bypass the validation gate.
For CI checks specifically, classify by the bucket field returned by gh pr checks --json name,state,bucket (bucket values: fail | pass | pending | skipping | cancel): a check is failing when bucket=fail; pending when bucket=pending; pass/skipping/cancel are not failing. Only bucket=fail checks drive rows 3a/3b.
Apply these rules in order — first match wins. Phase 3 is the single point of state decision.
| # | Condition | Derived state |
|---|---|---|
| 0 | The issue is already in a terminal tracker state (state=closed — whether stateReason=completed or not_planned) AND no merged PR is linked | In list/parent context: skip this item (move to the next). In single-issue context: blocked-user (reason: ticket-already-terminal) |
| 1a | A PR linked to this issue is merged AND the issue body has a ## Validation section (matching rule below) AND no <!-- ship-issue:verify:passed --> comment exists | blocked-verify |
| 1 | A PR linked to this issue is merged (and 1a does not apply) | merged |
| 2 | A PR linked to this issue is closed but not merged | failed (surface the closed-PR URL, halt) |
| 3 | An open PR exists AND has review comments (human or code-review-bot) created after the last skill commit | fixing |
| 3a | An open PR exists, no new review comments since the last skill commit, AND one or more CI checks have bucket=fail of the assertion type (unit test, lint, type check, code-review-bot check-run with findings) | fixing |
| 3b | An open PR exists, no new review comments since the last skill commit, AND one or more CI checks have bucket=fail of the infra/env type (secrets missing, dependency resolution failure, runner error, no test output at all) | blocked-user (reason: ci-infra-failure:<check-name>) |
| 4 | An open PR exists, no new review comments, no bucket=fail CI checks | pr-open |
| 5 | No PR has ever existed, issue has status:in-progress or status:in-review label | implementing (resume — do not reset) |
| 6 | No PR, no status:* label | pending |
Row 0 is first-match-wins and precedes row 1: an issue closed-as-not_planned (Canceled) or closed-as-completed with no merged PR is already terminal and re-shipping it would be wrong — skip it in a list/parent walk, or block in single-issue context so the human knows the ID points at a closed ticket.
Heading-match for row 1a's `## Validation`: case-insensitive match on any heading at ## level or deeper whose leading word is Validation — same rule as the Linear sibling. When ambiguous, err on the side of row 1a (transition to blocked-verify).
Edge cases for rows 3a / 3b — same per-check classification then row evaluation as the Linear sibling.
Stale-CI freshness guard (rows 3a/3b + three-strikes). A bucket=fail check counts as failing only if its run targets the current head SHA of the PR (the headRefOid from gh pr view --json headRefOid, cross-referenced with the check-run's head_sha). A failure recorded against an older commit is stale — treat it as pending, not failing. This stops a fix-push from being scored against a not-yet-rerun check: the old failing run lingers until CI re-triggers on the new SHA, and counting it would mis-fire row 3a and burn a three-strikes attempt on a result that no longer reflects the branch.
The "last skill commit" definition + the Skill-commit marker conventions (used by rows 3/3a/3b/4 and referenced by name from Phase 4's commit steps) are defined in ../_shared/skill-commit-marker.md. This skill is the `ship-issue-gh` consumer (<pr-branch> = the <n>-<kebab-title> branch derived in Phase 4).
Never reset an issue that already has a status:in-progress or status:in-review label. Resume.
Run Phase 3.5 escape-hatch checks first, before executing any handler below. Phase 3.5's hard stops dominate state derivation: the three-strikes CI counter, rebase-against-base, and the self-cheating hard stop are all evaluated before any handler here runs.
pending → implementingstatus:in-progress label: gh issue edit <n> --repo <owner>/<repo> --add-label status:in-progress. (Confirm the label exists; create it with gh label create if missing — should already exist if scaffold-sub-issues-gh set up the repo.)gh issue comment <n> --repo <owner>/<repo> --body '<!-- ship-issue:event:started --> 🚢 ship-issue-gh started.'cd to the resolved workdir. Pull latest main/master. Derive the branch name from the issue: <n>-<kebab-title> where <title> is lowercased, non-alphanumerics replaced with -, repeated - collapsed, leading/trailing - trimmed, truncated to 60 chars. ASCII-only — strip diacritics. Example: issue #42 "Add Avatar primitive" → 42-add-avatar-primitive. Create the branch from main.package.json scripts or an existing CLAUDE.md for the correct commands). After local checks pass, run the UI-reachability check (defined below) — ensures the change is reachable from the existing UI, or that the issue carries an explicit note for human validators.<!-- ship-issue:commit --> HTML comment in the commit body; include a Co-Authored-By: Claude <[email protected]> trailer unless a reachable CLAUDE.md forbids it.gh pr create. Trailer choice depends on the validation gate (same ## Validation heading-match rule as Phase 3 row 1a):Closes <owner>/<repo>#<n> (fully-qualified even for same-repo, so cross-repo parent linking is consistent). This triggers GitHub's auto-close on merge.Refs <owner>/<repo>#<n> instead of Closes .... Refs links the PR to the issue without auto-closing it on merge, so the worker — not GitHub's merge — controls when the issue closes (the merged handler closes it only after the blocked-verify gate passes). This eliminates the Closes-trailer-races-the-validation-gate problem documented in DESIGN.md § Known design tensions: a Closes trailer would auto-close the issue on merge before the next wake's row 1a could derive blocked-verify, turning the gate into a post-mortem.status:in-progress, add status:in-review. gh issue edit <n> --remove-label status:in-progress --add-label status:in-review.pr-open. Return — the /loop harness will wake again in 6 minutes.#### UI-reachability check (referenced by pending → implementing step 4 and implementing (resume))
Defined in ../_shared/ui-reachability.md. This skill is the `ship-issue-gh` consumer: option (b)'s <tracker-comment> is a <!-- ship-issue:note:reachability --> comment on the issue; the artifact opened after the check is the PR.
implementing (resume)Same as pending → implementing but:
status:in-progress label if it's already present.pr-openpr-open means: an open PR exists, no new reviewer comments, no failing checks. There's nothing to do on this wake.
This skill NEVER runs `gh pr merge` — a human merges. The skill drives the PR to green-and-reviewed and then waits; the final merge is always a human action.
bucket=pass/skipping, no unresolved review comments) and the last skill commit is older than ~30 minutes — use the %ai timestamp from the Phase 3 "last skill commit" lookup; the skill is stateless and keeps no per-wake counter, so the age of that commit (not a count of pr-open wakes) is the durable, re-run-safe trigger — and no <!-- ship-issue:note:merge-nudge --> marker comment already exists on the issue, post one marker-only comment: <!-- ship-issue:note:merge-nudge --> (body is the marker plus the one-line "PR green & review addressed — ready to merge"). The marker's own presence makes this a no-op on every later wake, so it never re-posts.Do not push new commits, re-classify, re-evaluate CI state, or merge here. All classification lives in Phase 3.
fixingEntered from Phase 3 row 3 (new review comments) or row 3a (failing assertion CI check). Invoked by Phase 3.5 sub-phase B step 4 — Phase 3.5 dispatches into this handler and observes whether execution reaches step 6.
gh run view <run-id> --log-failed --repo <owner>/<repo>). Classify each item:~/.claude/review-bots.md if that file exists (one bot login per line; blank lines and #-comments ignored), otherwise use the built-in default set (dependabot[bot], renovate[bot], github-actions[bot], and common code-review bots). An author matching neither the allowlist nor the default set is treated as a human reviewer comment, not a bot finding — never hardcode a specific employer's bot name here; it belongs in the user's local ~/.claude/review-bots.md. See the README's Review-bot allowlist section for the file format.blocked-user with reason scope-creep.fixing. An ambiguous mention, or a redirect/cancel mention → blocked-user with reason user-mention-ambiguous. (Phase 6 and Phase 7 defer to this rule — they do not flatly halt on every @-mention.)blocked-user; new regression → blocked-user; same failure → back to step 2.pending → implementing step 5. Push.Fixed in abc1234.), on the matching surface: for an inline review thread, reply in-thread via gh api /repos/<owner>/<repo>/pulls/<pr>/comments/<comment-id>/replies -f body=<text>; for a review-body item or a top-level PR comment (neither has an inline thread to reply into), post one top-level gh pr comment <pr> that leads with a `<!-- ship-issue:reply:fixed -->` marker and names what it addresses (the reviewer + the gist). The marker is load-bearing: this reply shares the skill's gh login with the reviewer, so the next wake's Phase 3 can only exclude it by marker (not by author) — without it the reply would re-trigger fixing forever. Don't reply to the skill's own marker comments.Do not write or modify `<!-- ship-issue:failcount:... -->` comments in this handler. Phase 3.5 owns the counter lifecycle.
mergedReaching this handler means Phase 3's row 1a did not apply.
--reason completed: gh issue close <n> --repo <owner>/<repo> --reason completed. (GitHub's auto-close from the merged PR's Closes trailer may have already closed it — that's fine; the call is idempotent and we still write the comment below.)gh issue comment <n> --repo <owner>/<repo> --body '<!-- ship-issue:event:merged --> ✅ Merged: <PR URL>.'🗜 Sub-issue <ref> merged. Run /compact now to free context before picking up <next-ref>. Trigger boundary, safety rationale, and exact rules live in ../_shared/compact-on-merge.md. Never fires when the merged item was the last (the loop is about to self-cancel).<next-ref> in this same wake; the next /loop wake re-derives state (the merged item is now skipped by Phase 2) and picks up <next-ref> fresh (safe per "Notes on persistence"). This is what gives the user a /compact opportunity at the boundary — same-wake continuation would surface the prompt only after <next-ref>'s work has already accumulated. Otherwise (no-compact mode, or no non-terminal items remain), advance to the next item in the list.blocked-verifyPre-Phase C behavior — auto-verify is Planned (Phase C — /verify-ticket):
## Validation text from the issue body into a blocked-user comment so a human can run it manually. The comment MUST include the unlock instruction verbatim: "post a comment containing exactly <!-- ship-issue:verify:passed -->, then re-run /abc:ship-issue-gh <arg>". Without that line the human has no documented way to clear the gate, and the next wake will re-derive blocked-verify forever.blocked-user with reason awaiting-manual-verification. Leave the issue open with status:in-review — closing now would skip the verification gate.blocked-usergh issue comment <n> --repo <owner>/<repo> --body '<!-- ship-issue:event:blocked --> 🛑 Blocked: <reason>. Needs: <ask>.'status:* label at its current value — do not transition. If blocked-user fires during implementing, label is status:in-progress and stays there. If during pr-open / fixing, label is status:in-review and stays there. The work isn't regressing; it's waiting on a human./loop — print the reason clearly.failed<!-- ship-issue:event:failed --> ❌ Failed: <reason>.--reason not_planned: gh issue close <n> --repo <owner>/<repo> --reason not_planned. Remove any status:* label.Run immediately after Phase 3, before any Phase 4 handler — execution order matches reading order (3 → 3.5 → 4). Evaluated against the data already gathered in Phase 3 — do not re-fetch.
Defined in ../_shared/three-strikes-counter.md. This skill is the `ship-issue-gh` consumer: <failing-bucket> = bucket=fail; <passing-bucket> = bucket=pass; <failcount-key> = github:<check-name> (GitHub-only — no platform prefix variation).
Defined in ../_shared/rebase-attempt-and-gate.md. This skill is the `ship-issue-gh` consumer: <behind-base-signal> = mergeStateStatus=BEHIND on the open PR; <command> = /abc:ship-issue-gh. The self-cheating hard stop below applies verbatim inside that flow.
If the skill catches itself about to bypass a failing check rather than fix it — deleting a failing assertion, adding --no-verify to a commit, widening a type to suppress an error, wrapping a line in // @ts-expect-error, .skip()-ing a failing test, commenting out a lint rule that was firing, eslint-disable-ing a violation — hard stop → `failed`. Write the attempted-cheat into the issue comment so the human can see exactly what the skill was about to do.
Soft stops — the loop pauses, the user resumes:
main after the attempted auto-rebase (see Phase 3.5 § Rebase against base — attempt-and-gate). Conflict markers or red gates after a clean rebase both escalate as blocked-user, not failed.blocked-user. Actionable @-mentions are handled in the fixing handler (Phase 4), which is canonical for mentions — do not halt on every @-mention.The loop halts when any of:
merged.blocked-user.failed.fixing handler, not here).cancel comment on the issue is detected by the Phase 3 cancel scan (case-insensitive, whole-comment-body or first line) → terminal halt.In every terminal case — blocked-user, failed, and all-merged alike — before returning, the skill calls CronDelete on its own /loop entry via the cron-entry match rule defined in Phase 0.5. CronDelete fires on all halts; the cron never stays armed past a halt. If CronDelete fails, print a note but continue — the terminal comment + output are the authoritative surface.
Print a concise block:
<command-name> wake <timestamp>
Items: <N>
[merged] <owner>/<repo>#42 <PR URL>
[pr-open] <owner>/<repo>#43 <PR URL> (no new comments since <sha>)
[pending] <owner>/<repo>#44
[blocked] <owner>/<repo>#45 awaiting-manual-verification
Working: <owner>/<repo>#43
Next wake: /loop 6m <command-name> <original-arg><command-name> — in both the title line and the Next wake: line — is the captured slash-command name from Phase 0.5 (e.g. /abc:ship-issue-gh), not a hardcoded literal; same convention as the self-arm string.
Keep the output short on no-op wakes.
Nothing is persisted locally. All skill-authored state lives in GitHub issue comments using the marker format in ../scaffold-sub-issues-gh/github-conventions.md. Closing the terminal mid-loop is safe; re-running /ship-issue-gh <same-arg> derives state fresh.
If a counter comment or event comment needs updating, append a new comment rather than editing the existing one — comment history is the audit trail. GitHub doesn't expose edit history through gh api reliably, so an edited marker is worse than an appended one.
gh issue develop --name <derived> for richer GitHub UI linking.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.