issue
SkillFiles & storageReport an issue — captures context and routes it to the right place. Say ''this is broken'', ''bug in'', ''file an issue'', ''report a problem''. Not a personal task (/todo) or team exploration (/quest).
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the issue skill
What this skill tells your AI
The instructions your AI receives, as published by egregore-labs/egregore in .codex/skills/issue/SKILL.md and read by ahel’s review.
Report an issue. Captures context and routes to the right place.
When to invoke
User says: "this is broken", "bug in", "something's wrong with", "file an issue", "report a problem", "[command] isn't working"
Not this: personal task → /todo · team exploration → /quest
Topic: $ARGUMENTS
Auto-saves. No need to run /save after (create mode only).
Mode detection
MODE=$(jq -r '.mode // "connected"' egregore.json 2>/dev/null)
Local mode (mode === "local"): Skip ALL bin/graph.sh and bin/notify.sh calls — do NOT run them. Do NOT show any graph-related messaging ("Graph offline", "will sync", Neo4j, etc.).
Local-mode flow:
- Create mode: Step 0 context capture — run Bash call 1 (git identity + state) normally; skip Bash call 2's
bin/graph.sh testline (keep the memory-symlink and egregore.json checks); skip the Neo4j recent-session query entirely. Steps 1-2 (description, smart routing) work normally. Step 3 — write the markdown file tomemory/knowledge/issues/normally, but skip the Neo4jCREATE (i:Issue)node and the progress message referencing "graph". Step 4 transcript attachment works normally when--transcriptis present. Step 5 — skip graph routing updates (Neo4j node creation, relationship updates), but preservegh issue createif the smart routing targets a GitHub repo (GitHub CLI is independent of the graph). Skip Step 6 notifications entirely. Steps 7-8 (auto-save, confirmation TUI) work normally — in the TUI, show✓ Saved to memory(omit "graphed" and "team notified"). - List mode: Read issues from
memory/knowledge/issues/directory — deriveidfrom filename (e.g.,2026-03-30-memory-bug.md→memory-bug), parse frontmatter fortitle,status,recipient,date(display as created),topics,author(display as reportedBy). Render same TUI. - Close mode: Find issue file in
memory/knowledge/issues/, update frontmatterstatus: closed+ addclosed: {date}. Skip graph update. If frontmatter hasgithub_url, still rungh issue close "{github_url}" 2>/dev/null— GitHub CLI is independent of the graph. - Search mode: Grep through
memory/knowledge/issues/files for matching text. Render same TUI. - Notifications: Skip entirely — do not mention notifications.
Connected mode: Full behavior including graph nodes and notifications as specified below.
Execution rules
Neo4j-first. All queries via bash bin/graph.sh query "...". No MCP. No direct curl to Neo4j.
Notifications via bash bin/notify.sh. No direct curl to Telegram.
Argument routing
Parse $ARGUMENTS to determine mode:
- Empty or
list→ List mode (show open issues) list open→ List mode (open only)list closed→ List mode (closed only)list all→ List mode (all statuses)close [id-or-title]→ Close modesearch [term]→ Search mode--transcript→ Create mode modifier. Strip this flag from the description text, parse any absolute or~-prefixed*.jsonltranscript paths intoselected_transcripts, remove those paths from the description text, then attach scrubbed Claude Code transcripts during Create mode (for example:/issue /activity swallowed the board --transcript ~/.claude/projects/.../session.jsonl).- Anything else → Create mode (existing Steps 0–8 below)
List mode
Query
MATCH (i:Issue)
OPTIONAL MATCH (i)-[:REPORTED_BY]->(p:Person)
RETURN i.id AS id, i.title AS title, i.status AS status,
i.recipient AS recipient, i.created AS created,
i.topics AS topics, p.name AS reportedBy,
i.github_url AS githubUrl
ORDER BY i.created DESC
If list open or list closed was specified, add WHERE i.status = 'open' or WHERE i.status = 'closed' to the query.
Display
TUI box — same boundary rules as all commands (72 chars, no sub-boxes).
┌──────────────────────────────────────────────────────────────────────┐
│ ✱ ISSUES alice · Feb 10 │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ OPEN │
│ memory-symlink-breaks-after-pull │
│ Memory symlink breaks after pull (bob, Feb 09) │
│ │
│ save-fails-silently │
│ /save fails silently when graph offline (alice, Feb 08) · #42 │
│ │
│ CLOSED │
│ im-hungry │
│ Im hungry (bob, Feb 09) │
│ │
├──────────────────────────────────────────────────────────────────────┤
│ /issue [description] to create · /issue close [id] to resolve │
└──────────────────────────────────────────────────────────────────────┘
Format per issue: Two lines per issue:
- Line 1:
{id}(dimmed/secondary — the slug identifier) - Line 2:
{title} ({reportedBy}, {date})+· #{number}if github_url exists
Group by status: OPEN first, then CLOSED. Separate groups with a blank line.
If no issues exist: show No issues found. in the box body.
If only listing one status (e.g., list open), omit the status headers and show a flat list.
Close mode
Step 1: Resolve target
If $ARGUMENTS contains an ID or title after close:
MATCH (i:Issue {status: 'open'})
WHERE i.id CONTAINS toLower($term) OR toLower(i.title) CONTAINS toLower($term)
OPTIONAL MATCH (i)-[:REPORTED_BY]->(p:Person)
RETURN i.id AS id, i.title AS title, p.name AS reportedBy
- 1 match → proceed to close
- Multiple matches → present AskUserQuestion picker with matched issues
- 0 matches → "No open issue matching '{term}'."
If no term provided after close, list all open issues as AskUserQuestion picker.
Step 2: Close the issue
MATCH (i:Issue {id: $id})
SET i.status = 'closed', i.closedAt = datetime()
RETURN i.id, i.title, i.status, i.github_url
Step 3: Close GitHub issue (if linked)
If github_url is set:
gh issue close "{github_url}" 2>/dev/null
Show warning if this fails — don't block the close.
Step 4: Update memory file
If memory/knowledge/issues/{id}.md exists, update the frontmatter status: closed field.
Step 5: Confirmation
✓ Closed: {title}
If GitHub issue was also closed: ✓ Closed: {title} · GitHub #{number} closed
No auto-save for close operations (lightweight).
Search mode
Query
MATCH (i:Issue)
WHERE toLower(i.title) CONTAINS toLower($term)
OR toLower(i.id) CONTAINS toLower($term)
OR ANY(t IN i.topics WHERE toLower(t) CONTAINS toLower($term))
OPTIONAL MATCH (i)-[:REPORTED_BY]->(p:Person)
RETURN i.id AS id, i.title AS title, i.status AS status,
p.name AS reportedBy, i.created AS created,
i.github_url AS githubUrl
ORDER BY i.created DESC LIMIT 10
Display
Same TUI format as list mode, but no grouping by status — results are relevance-ordered. Show status inline: {title} ({reportedBy}, {date}) [open] or [closed].
If no results: No issues matching '{term}'.
Create mode (existing flow)
Step 0: Context Capture (silent, parallel)
Fire all three in parallel before prompting. The user should never describe their environment.
Bash call 1 — identity + git state:
git config user.name && echo "---" && \
git branch --show-current && echo "---" && \
git status --short && echo "---" && \
git log --oneline -5
Map git username → short name: "Alice Smith" → alice, "Bob Jones" → bob, "Bob J" → bob, "Carol" → carol
Bash call 2 — environment health:
[ -L memory ] && echo "memory:linked" || echo "memory:MISSING"
bash bin/graph.sh test 2>&1
jq -r '.org_name,.github_org,.slug,.repos[]' egregore.json 2>/dev/null
Neo4j — recent session context:
MATCH (s:Session)-[:BY]->(p:Person {name: $me})
WHERE date(left(toString(s.date), 10)) >= date() - duration('P3D')
RETURN s.topic, s.date ORDER BY s.date DESC LIMIT 5
Step 1: Description
- If
--transcriptis present → settranscript_attachment=true; parse absolute or~-prefixed paths ending in.jsonlintoselected_transcripts; remove both the flag and those transcript paths from$ARGUMENTS; continue Create mode with the remaining text. This must happen before Step 3 writes the issue file or graph node. - If
$ARGUMENTSis non-empty and doesn't start withegregore:→ use as description - If
$ARGUMENTSstarts withegregore:→ strip prefix, use rest as description, pre-set recipient toegregore - If empty → prompt: "What's the issue?" (plain text, wait for user response)
Step 2: Smart Routing
Infer the most likely destination from the description content, then confirm. Only ask an open "Who's this for?" when the destination is genuinely ambiguous.
Read org config values (needed for matching):
jq -r '.org_name,.github_org,.repos[]' egregore.json
If the user used the egregore: prefix in Step 1, skip this step entirely — recipient is already egregore.
Routing inference
Analyze the description for signals:
| Signal | Inferred destination |
|---|---|
Mentions a slash command (/save, /reflect, /activity, etc.) | {github_org}/egregore-core |
Mentions bin/, egregore.json, .claude/commands/, onboarding, graph.sh | {github_org}/egregore-core |
Mentions a managed repo name from .repos[] (e.g., "frontend", "backend") | {github_org}/{repo} |
| Mentions "memory", "handoff", "knowledge graph", "Neo4j", "sync" | {github_org}/egregore-core |
| General/vague, no code or system references | Just memory |
egregore: prefix (already handled above) | egregore upstream |
Confidence-based flow
High confidence (description clearly matches one destination):
Present a single confirmation via AskUserQuestion:
question: "This looks like an egregore-core issue. File it on {github_org}/egregore-core?"
header: "Route"
multiSelect: false
options:
- label: "Yes, file on {github_org}/egregore-core"
description: "Creates a GitHub issue on the org's fork"
- label: "Just memory"
description: "Track locally only — visible on /activity"
The first option is always the inferred destination. "Just memory" is always the second option (lightweight fallback). If the user picks "Other", trigger a second-round AskUserQuestion with the full destination list (all repos + egregore upstream).
Low confidence (ambiguous — no clear signals, or signals point to multiple destinations):
Fall back to the full destination picker:
question: "Where should this be filed?"
header: "Route"
multiSelect: false
options:
- label: "Just memory"
description: "Tracked in the knowledge graph, visible on /activity"
- label: "egregore"
description: "Sent to Egregore maintainers (sanitized)"
- label: "{github_org}/egregore-core"
description: "Filed on the org's fork"
- (for each repo in .repos[]):
label: "{github_org}/{repo}"
description: "Filed on {repo}"
Step 3: Write to Memory
Every issue, regardless of recipient, gets a markdown file and a graph node.
Generate metadata
- Title: derive from description — short, descriptive (max 60 chars)
- Slug: from title — lowercase, hyphens, no special chars, max 50 chars
- Topics: auto-detect 2-4 topic tags from the description content
- Date: today
YYYY-MM-DD
Write file
Path: memory/knowledge/issues/YYYY-MM-DD-{slug}.md
Write using Bash (memory is outside project):
cat > "memory/knowledge/issues/YYYY-MM-DD-{slug}.md" << 'ISSUEEOF'
---
title: {title}
date: YYYY-MM-DD
author: {short name}
category: issue
status: open
recipient: {selected recipient}
topics: [{topic1}, {topic2}]
github_url:
---
## Description
{user's description}
## Context
- **Branch**: {branch from Step 0}
- **Recent commits**: {last 5 oneline from Step 0}
- **Uncommitted changes**: {git status short from Step 0}
- **Memory**: {linked/missing from Step 0}
- **Graph**: {connected/offline from Step 0; local mode: omit this line}
- **Recent sessions**: {topic list from Neo4j Step 0; local mode: omit this line}
ISSUEEOF
Neo4j node
MATCH (p:Person {name: $author})
CREATE (i:Issue {
id: $id,
title: $title,
status: 'open',
recipient: $recipient,
created: datetime(),
topics: $topics
})
CREATE (i)-[:REPORTED_BY]->(p)
RETURN i.id
Where:
$id=YYYY-MM-DD-{slug}(matches filename without extension)$author= short name (alice, bob, carol)$title= derived title$recipient= selected recipient string$topics= array of topic strings
Show progress:
[1/N] ✓ Issue saved to memory + graph
→ memory/knowledge/issues/YYYY-MM-DD-{slug}.md
Step 4: Transcript attachment (--transcript)
Run this only when transcript_attachment=true. Transcripts are full conversation content and sit outside every telemetry envelope. Scrubbing is mandatory, but publication still requires explicit user consent every time.
Structural consent rule: transcripts must never sit under memory/ before consent. /issue auto-save and any later /save can sweep the memory repo, so pre-consent scrubbed files must live only in a temporary staging directory outside both the repo and memory/.
Pre-consent path privacy rule: no transcript path, filename, or reference may appear in the issue memory file, the graph node, or any GitHub issue body until the user picks Attach — commit to org memory. This includes explicit transcript paths parsed from $ARGUMENTS; they live only in selected_transcripts until the consent decision is known.
Upstream privacy rule (load-bearing): if the recipient is egregore (upstream maintainers / public egregore-labs/egregore issue), transcripts are NEVER linked or attached publicly. They stay in org memory only. The public issue body gets exactly one transcript note:
Session transcripts captured in org memory; available on maintainer request.
Locate
If selected_transcripts is non-empty from Step 1, use those files. Otherwise:
bash bin/transcript-attach.sh locate
The command prints recent Claude Code *.jsonl transcript candidates for the current project. If multiple candidates are returned, choose the line with current:true for the current session. If the issue description clearly refers to a different or past session, offer the other candidates via AskUserQuestion before choosing.
Size guard: before scrubbing, check each selected file size. If any selected transcript is larger than 2MB, warn that the file is large and ask before including it. If the user declines, skip that file and continue with the remaining selected transcripts.
If locate exits 1, say no transcripts were found and continue the issue without attachment.
Scrub
Use the same issue id generated in Step 3 (YYYY-MM-DD-{slug}), but scrub into a temp staging directory outside both the repo and memory/:
staging_dir="$(mktemp -d "/tmp/egregore-issue-transcripts.XXXXXX")"
bash bin/transcript-attach.sh scrub "$staging_dir" <files...>
Show the user the scrub summary before asking for consent:
- files selected
- original sizes
- redaction counts by category (
env_value,token) - temporary staging path (
$staging_dir)
If the scrub summary reports 0 redactions, that is fine. The scrubber is best-effort; the consent gate is the real content-level sensitivity check.
Consent gate (MANDATORY)
AskUserQuestion:
header: "Transcripts"
question: "Attach scrubbed transcripts to this issue?"
options:
- label: "Attach — commit to org memory"
description: "Adds scrubbed transcripts to the private memory repo and references them from the issue"
- label: "Keep local only"
description: "Leaves the scrubbed copies on disk, uncommitted and unreferenced"
- label: "Don't attach"
description: "Deletes the scrubbed copies and files the issue without transcripts"
Never commit, push, or publicly reference transcripts unless the user picks Attach — commit to org memory.
On consent
Move the scrubbed files into org memory only after consent:
mkdir -p "memory/transcripts/issues/{issue-id}/"
for file in "$staging_dir"/*.jsonl; do
[ -e "$file" ] || continue
mv "$file" "memory/transcripts/issues/{issue-id}/"
done
rm -rf "$staging_dir"
Then:
- Add a
transcripts:list field to the issue file frontmatter with memory-repo-relative paths:transcripts: - transcripts/issues/{issue-id}/{basename}.jsonl - Add a body section to the issue file:
## Transcripts - `transcripts/issues/{issue-id}/{basename}.jsonl` - Stage explicit paths in the memory repo only. Never use a broad
git add:git -C memory add "knowledge/issues/{issue-id}.md" "transcripts/issues/{issue-id}" - Commit and push in the memory repo only, following the Step 7 memory-repo convention (
git -C memory ..., push tomainwith pull-rebase-push retry). - If Step 5 files a GitHub issue on an org repo, append this to the GitHub issue body before creation:
## Transcripts - `transcripts/issues/{issue-id}/{basename}.jsonl` (org-private memory repo) - If Step 5 files an
egregoreupstream/public issue, do not list transcript paths. Use only the upstream privacy note above.
If the user chooses Keep local only, tell them the temp staging path, leave the scrubbed copies there, and do not put anything under memory/, do not add transcript references to the issue file, and do not include them in any GitHub issue body. If the user chooses Don't attach, delete the temp staging directory (rm -rf "$staging_dir") and continue without transcript references.
Step 5: Route by Recipient
Simple conditional on the selected recipient value.
"Just memory" → Done
No external action. Issue lives in the graph and memory. Skip to Step 6.
"egregore" → Sanitize + Send Upstream
Sanitize — replace before sending:
| Pattern | Replacement |
|---|---|
Org name (from egregore.json .org_name) | [org] |
GitHub org (from egregore.json .github_org) | [github-org] |
Managed repo names (from egregore.json .repos[]) | [repo] |
ek_*, ghp_*, gho_* token patterns | [redacted] |
Person names (filenames from memory/people/*.md, excluding index.md) | [person-N] |
memory/people/*.md paths | memory/people/[redacted].md |
Show sanitized body to user. They review and confirm or cancel.
If confirmed, construct a report payload and submit via bin/session-report.sh:
echo '{"report_type":"issue","topic":"$TITLE","summary":"$SANITIZED_DESCRIPTION","description":"$USER_DESCRIPTION","system_info":{"mode":"...","platform":"...","shell":"..."}}' \
| bash bin/session-report.sh submit 2>/dev/null
Gate: Check if report_url is configured in egregore.json. If not, show the sanitized body in a code block for manual sharing:
Report URL not configured. Here's the sanitized body you can share manually:
GitHub issue (ask each time):
If gh auth status succeeds, AskUserQuestion:
header: "GitHub"
question: "Also create a GitHub issue on egregore-labs/egregore?"
options:
- label: "Yes, create issue"
description: "Public issue with sanitized content"
- label: "No"
description: "Report submitted to Supabase only"
If yes:
gh issue create --repo egregore-labs/egregore \
--title "$TITLE" \
--body "$SANITIZED_BODY"
Capture the returned URL. Update local Neo4j node:
MATCH (i:Issue {id: $id})
SET i.github_url = $url, i.upstreamRef = $url
RETURN i.id
Any GitHub repo → gh issue create
Compose the issue body from the memory file content (description + context).
gh issue create \
--repo {selected-repo} \
--title "{title}" \
--body "$(cat memory/knowledge/issues/YYYY-MM-DD-{slug}.md)"
Capture the returned URL. Update memory file frontmatter github_url: field and Neo4j node:
MATCH (i:Issue {id: $id})
SET i.github_url = $url
RETURN i.id
Show progress:
[2/N] ✓ Filed on {repo} · #{issue_number}
"Other" → Ask for repo, then same as GitHub repo above
Prompt: "Which repo? (owner/name)" — then use gh issue create with that repo.
Step 6: Offer notification (org issues only)
Only for org-level issues (GitHub repos or "Just memory"). Skip for egregore upstream.
Filing the issue is not notification consent. Follow
.claude/context/notification-consent.md and prepare without sending:
PLAN_JSON=$(bash bin/notify.sh plan group "Issue reported by {author}: {title}")
Show the exact organization, all group channels, and exact message in a dedicated Send / Edit / Cancel checkpoint. If planning or dispatch fails, show warning but don't fail:
Notification failed — team can see this on /activity
Show progress:
[3/N] ✓ Notification sent
Step 7: Auto-save
Run the full /save flow:
- Commit changes in memory repo and push directly to main (pull-rebase-push with retry)
- Commit any egregore changes and push working branch + PR to develop
Show progress:
[N/N] ✓ Auto-saved
Step 8: Confirmation TUI
~72 char width. Sigil: ✱ ISSUE CAPTURED or ✱ ISSUE REPORTED (if filed externally).
Boundary handling (CRITICAL)
No sub-boxes. No inner ┌─┐/└─┘ borders. Sub-boxes break because the model can't count character widths precisely enough.
Only 4 line patterns exist:
- Top:
┌+ 70×─+┐(72 chars) - Separator:
├+ 70×─+┤(72 chars) - Content:
│+ 2 spaces + text + pad spaces to 68 chars +│(72 chars) - Bottom:
└+ 70×─+┘(72 chars)
The separator lines are ALWAYS identical — copy-paste the same 72-char string. Content lines have ONLY the outer frame │ as borders. Pad every content line with trailing spaces so the closing │ is at position 72.
"Just memory" variant:
┌──────────────────────────────────────────────────────────────────────┐
│ ✱ ISSUE CAPTURED {author} · {Mon DD} │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ Title: {title} │
│ For: just memory │
│ │
│ ✓ Saved to memory · graphed · team notified │
│ → memory/knowledge/issues/YYYY-MM-DD-{slug}.md │
└──────────────────────────────────────────────────────────────────────┘
GitHub repo variant:
┌──────────────────────────────────────────────────────────────────────┐
│ ✱ ISSUE REPORTED {author} · {Mon DD} │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ Title: {title} │
│ For: {org}/{repo} · issue #{number} │
│ │
│ ✓ Saved to memory · graphed · team notified │
│ → memory/knowledge/issues/YYYY-MM-DD-{slug}.md │
└──────────────────────────────────────────────────────────────────────┘
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 288
- Forks
- 21
- Last commit
- Sep 2026
- Hacker News mentions
- 20
Advanced
- Catalog kind
- skill
- Gateway key
issue-egregore-labs- Source
- github.com/egregore-labs/egregore