Compare commits

Author SHA1 Message Date
gitadmin dead6ca2bb feat: add provider-neutral tracker automation 2026-08-17 22:36:05 -04:00
gitadmin f65a6ec812 docs(crit): rewrite skill description and simplify framework steps 2026-08-06 20:45:47 -04:00
gitadmin 7dc4758e9a chore: remove deprecated skills and reorganize skill registry 2026-08-06 20:31:30 -04:00
gitadmin 07198e9aad feat: add openai agent metadata and lsp-code-analysis skill
Add OpenAI interface metadata to 14 skills (commit-staged,
implement-isolation, implement-isolation-tmux, project-context-pack,
setup-skills, agent-handoff, knowledge-gardener, tmux-launch-agent,
conversation-summary, crit, pkm-curation, research-vault,
youtube-video-capture, lsp-code-analysis).

Add new lsp-code-analysis skill for semantic code navigation and
refactoring via the Language Server Protocol.
2026-08-06 19:32:08 -04:00
gitadmin 8a7201b898 docs(issue-tracker): add note that labels must be created before applying 2026-07-24 00:45:32 -04:00
gitadmin f4e953186e refactor(engineering): split implement into isolation-tmux and isolation
Replace the monolithic implement-issue and implement skills with two
focused alternatives: implement-isolation (subagent + reviewer) and
implement-isolation-tmux (tmux-launch-agent for fire-and-forget).
Update the README to list the new skills.
2026-07-24 00:44:51 -04:00
gitadmin 5a6d1fccb0 chore: reorganize READMEs with sorted entries and bucket descriptions
Clean up all skill bucket READMEs — sort entries alphabetically within
user/model sections, add category descriptions to each bucket, simplify
the deprecated forge-skills doc to a bare notice, update triage label
count (5→7) in AGENTS.md, and add a Project Context Pack section.
2026-07-19 10:13:37 -04:00
gitadmin fa7619bfde docs: refresh project context and fix agent docs typos
- Update .agent/project-context.md with new paths, structure, edit boundaries
- Fix typo in issue-tracker.md: tea comments -> tea comment
- Fix jq syntax: select(.state = "open") -> select(.state == "open")
- Clean up triage-labels.md table formatting
2026-07-19 10:10:51 -04:00
gitadmin 0daa11851d refactor(tmux-launch-agent): extract tmux-open helper, add modelflag 2026-07-19 09:52:18 -04:00
gitadmin 22c3b3e45f docs(adr-wiki): rename setup-command reference to /setup-skills 2026-07-19 09:51:00 -04:00
gitadmin 102b163fc5 docs(commit-staged): remove redundant description line from SKILL.md 2026-07-15 17:19:57 -04:00
gitadmin 2a87789523 feat(engineering): add implement-isolation skill, refine implement-issue
- Add /skill:implement (implement-isolation) — ticket claim, worktree
  isolation, tdd, verify, code-review, ship loop
- Fix implement-issue: worktree path to issue-<N>-<repo-slug>,
  label semantics (remove-others), skill ref formatting, drop stale
  closing note
2026-07-15 17:18:36 -04:00
68 changed files with 2354 additions and 1338 deletions
+48 -32
View File
@@ -1,26 +1,31 @@
# Project Context Pack
Generated: 2026-06-25
Root: /home/sjb/Documents/ai-workflows/skills
Generated: 2026-08-17
Root: /home/sjb/Projects/personal/ws-sjb-skills/wt-master
Working directory: .
Status: fresh
## Purpose
A collection of agent skills (slash commands and behaviors) loaded into Steve Beaulac's AI coding agents. Each skill is a SKILL.md file that teaches the agent how to handle a specific task — from codebase design and TDD to Obsidian PKM workflows and forge interaction.
A collection of agent skills (slash commands and behaviors) loaded into Steve Beaulac's AI coding agents. Each skill is a SKILL.md file that teaches the agent how to handle a specific task — from codebase design and TDD to Obsidian PKM workflows and tmux agent launching.
## Project type
- **Agent skill repository** — markdown-defined agent instructions
- Languages: Markdown (100%)
- Package managers: none
- Build/test tools: none
- **Agent skill repository plus standalone Python tracker package**
- Languages: Python and Markdown, one Bash script (detect-agent), one shell script (tmux-open)
- Package manager: `pyproject.toml`
- Build/test tools: `python -m unittest discover`
- Agent guidance: `AGENTS.md` at root, `docs/invocation.md` for invocation conventions, `docs/agents/` for issue tracker / triage labels / ADR wiki / domain docs
## Structure
```
.
├── AGENTS.md # Top-level agent instructions for this repo
├── README.md # Project overview, lists user-invoked and model-invoked skills
├── .gitignore # Excludes docs/adr/
├── .agent/
│ └── project-context.md # This file
├── docs/
│ ├── invocation.md # Model-invoked vs user-invoked definitions
│ ├── agents/
@@ -29,38 +34,45 @@ A collection of agent skills (slash commands and behaviors) loaded into Steve Be
│ │ ├── issue-tracker.md # Gitea issue tracker docs
│ │ └── triage-labels.md # Five-label triage vocabulary
│ └── adr/ # ADR wiki clone (gitignored)
├── tracker/ # Provider-neutral Python CLI/library
├── tracker_automation/ # Compatibility import name
├── tests/ # Public operation-boundary tests
├── pyproject.toml # Package metadata and console scripts
└── common/
├── README.md # Lists all common skills by invocation type
├── engineering/ # Model-invoked: forge-interaction, project-context-pack; also sub-skills for codebase-design, domain-modeling, tdd, triage, etc.
├── productivity/ # User-invoked: grill-me, handoff, writing-great-skills; Model-invoked: grilling
├── pkm/ # User-invoked: conversation-summary, crit, knowledge-gardener, research-vault
├── personal/ # User-invoked: pkm-curation; Model-invoked: forge-preferences
└── deprecated/ # Deprecated skills (audio-production-dispatcher, dsp-research-dispatcher, forge-*)
├── engineering/ # Model-invoked: lsp-code-analysis, pkm-curation; User-invoked: commit-staged, implement-issue, project-context-pack, setup-skills
├── productivity/ # (currently only README.md)
├── pkm/ # User-invoked: conversation-summary, crit, research-vault, youtube-video-capture; Model-invoked: pkm-curation
├── personal/ # (currently only README.md)
├── misc/ # User-invoked: tmux-launch-agent
├── in-progress/ # User-invoked: agent-handoff, knowledge-gardener
└── deprecated/ # Deprecated forge-* and dsp-* skills
```
## Important files
- `AGENTS.md` — Agent entry point that describes structure, categories, and references docs
- `README.md` — Index of all skills divided into user-invoked and model-invoked (recently updated to fix broken links, add missing skills, correct invocation classification)
- `README.md` — Index of all skills divided into user-invoked and model-invoked
- `docs/invocation.md` — Defines the invocation model (disable-model-invocation frontmatter key, human vs model reachability, dependency rules)
- `docs/agents/` — Agent documentation for issue tracker, triage labels, ADR wiki, domain docs
- `common/engineering/project-context-pack/SKILL.md` — The currently running skill
- `common/engineering/codebase-design/SKILL.md` — Deep module design vocabulary (referenced by other skills)
- `common/engineering/domain-modeling/SKILL.md` — Domain modeling with ADRs and CONTEXT files
- `common/engineering/tdd/SKILL.md` — Test-driven development discipline
- `common/productivity/grilling/SKILL.md` — Relentless plan/design interview (model-invoked, triggers)
- `common/productivity/writing-great-skills/SKILL.md` — Reference for writing/editing skills
- `common/engineering/project-context-pack/SKILL.md` — The skill that generated this file
- `common/misc/tmux-launch-agent/SKILL.md` — Fork agent CLI into new tmux window (user-invoked)
- `common/misc/tmux-launch-agent/tmux-open` — Reusable script that opens a command in a new tmux window/session
## Commands
- Build: none
- Test: none
- Lint/typecheck: none
- Run/dev: skills are invoked by AI agents — no server or dev command
- Build/package: `python -m pip install .`
- Test: `python -m unittest discover -v`
- Typecheck: `lsp_diagnostics` on `tracker/` and `tests/`
- Run: `python -m tracker` or installed `tracker`
## Entry points
- `AGENTS.md` — loaded by the AI agent as project instructions (referred to in pi's agent config)
- Each `SKILL.md` under `common/` — referenced by agents via slash commands or auto-invocation
## Search and symbol notes
- All skills are `SKILL.md` files — search with `fd SKILL.md`
- Bucket READMEs: `fd README.md common/`
- Skills are classified as **user-invoked** (`disable-model-invocation: true` in frontmatter) or **model-invoked** (default, no frontmatter flag)
@@ -68,25 +80,25 @@ A collection of agent skills (slash commands and behaviors) loaded into Steve Be
- Shared reference docs live inside the owning skill's directory; other skills reach that material by invoking the skill
## Files inspected
- `AGENTS.md` — root agent instructions — fresh
- `README.md` — project overview — updated (all links fixed, missing skills added, invocation corrected)
- `README.md` — project overview and skill index — fresh
- `docs/invocation.md` — invocation model definitions — fresh
- `.gitignore` — excludes docs/adr/ — fresh
- `common/engineering/README.md` — engineering bucket index — updated (all engineering skills added, misclassified skills corrected)
- `common/README.md` — common bucket index — updated (links fixed, missing skills added)
- `common/productivity/README.md` — productivity bucket index — fresh (was already correct)
- `common/pkm/README.md` — pkm bucket index — updated (pkm-curation added)
- `common/personal/README.md` — personal bucket index — updated (noted both skills moved elsewhere)
- `common/deprecated/README.md` — deprecated bucket index — updated (all forge skills added)
- All `SKILL.md` files — frontmatter checked for invocation status
- `common/README.md` — common bucket index — fresh
- `common/misc/README.md` — misc bucket index — fresh
- `.agent/project-context.md` — this file (refreshed from stale 2026-06-25 version)
## Exclusions
- `.git/` — VCS data
- `docs/adr/` — gitignored ADR wiki clone
- `node_modules/`, `dist/`, `build/`, `target/`, `.venv/`, `__pycache__/`, `vendor/`, `coverage/` — not present, but excluded by policy
- `/home/sjb/.agents/skills/` — global install, NOT the source of truth for this repo
- Binary files, large artifacts, credentials, secrets, personal data
## Navigation rules for future agents
- Start with `fd SKILL.md` to find all skills, then narrow by `fd SKILL.md common/<category>/`
- To understand a skill's purpose, read its `SKILL.md` and the bucket `README.md` that indexes it
- For invocation rules (user-invoked vs model-invoked), read `docs/invocation.md`
@@ -95,9 +107,13 @@ A collection of agent skills (slash commands and behaviors) loaded into Steve Be
- Track every inspected file in this cache
- Re-read a file only when it changed, the cache is stale, or exact details are needed
## Edit boundaries
When cwd is inside this repo, all file edits MUST be scoped to paths under the repo root (`/home/sjb/Projects/personal/ws-sjb-skills/wt-master`). Do NOT touch files under `/home/sjb/.agents/skills/` or `/home/sjb/.pi/` — those are the installed/runtime copies, not the source of truth. The global install is synced separately; this repo is where source edits happen.
## Refresh notes
- This is a markdown-only repo with no build artifacts; refreshes are rarely needed unless skills are added or removed
- To refresh, re-run `tree -a -I '.git' -L 4` and re-read any changed bucket READMEs or SKILL.md files
- The `docs/adr/` directory is gitignored — if ADR data is needed, check the Gitea wiki directly
- All README.md files were updated on 2026-06-25 to fix broken links, add missing skills, and correct invocation classification
- If a skill is moved between buckets, update all README.md files that reference it (root, common, source bucket, destination bucket)
+2
View File
@@ -1 +1,3 @@
docs/adr/
__pycache__/
*.py[cod]
+8 -4
View File
@@ -14,8 +14,8 @@ Skill are organized into categories based on their function. For example, `/comm
If we have a skill that is only relevant to a specific agent, we can put it in an agent-specific bucket. For example, if we have a skill that is only relevant to the `opencode` agent, we can put it in `/opencode/misc`.
## list of categories
- `engineering/` — daily code work
- `productivity/` — daily non-code workflow tools
- `misc/` — kept around but rarely used
@@ -32,11 +32,11 @@ Every `SKILL.md` is either user-invoked (`disable-model-invocation: true`, reach
### Issue tracker
Issues are tracked in Gitea on gitea.sagacity.ca. See `docs/agents/issue-tracker.md`.
Issues are tracked in Gitea on gitea.sagacity.ca; use the provider-neutral `tracker` package for normal automation. See `docs/agents/issue-tracker.md` and `docs/agents/tracker.md`.
### Triage labels
Five-label vocabulary with default names (needs-triage, needs-info, ready-for-agent, ready-for-human, wontfix). See `docs/agents/triage-labels.md`.
Seven-label vocabulary with default names (needs-triage, needs-info, needs-review, ready-for-agent, ready-for-human, in-progress, wontfix). See `docs/agents/triage-labels.md`.
### Domain docs
@@ -44,7 +44,11 @@ Single-context layout. See `docs/agents/domain.md`.
### ADR wiki
Gitea wiki at git@gitea.sagacity.ca:steve/Skills.wiki.git, cloned into docs/adr/, SSH key auth. See `docs/agents/adr-wiki.md`.
Gitea wiki at `git@gitea.sagacity.ca:steve/Skills.wiki.git`, cloned into `docs/adr/`, SSH key auth. See `docs/agents/adr-wiki.md`.
## Project Context Pack
Agent memory file that describes the repo's context, codebase, and navigation rules. See `.agents/project-context.md`.
### Agent CLI
+11 -13
View File
@@ -1,29 +1,27 @@
# Skills
A collection of agent skills (slash commands and behaviors) loaded into my agent.
Agent skills (slash commands and behaviors) loaded into my agent.
## Tracker automation
This repo also ships the standalone provider-neutral `tracker` CLI/library. See [`docs/agents/tracker.md`](docs/agents/tracker.md) and [`tracker/README.md`](tracker/README.md) for migration guidance.
## User-invoked
- [agent-handoff](common/in-progress/agent-handoff/SKILL.md) — Hand the current conversation off to a fresh background agent that picks up the work immediately.
- [audio-product-dsp](common/deprecated/audio-production-dispatcher/SKILL.md) — Dispatch audio product DSP hardware/software engineering requests to the best specialist workflow with measurable product-focused outputs.
- [commit-staged](common/engineering/commit-staged/SKILL.md) — Commit staged files with a conventional commit message.
- [conversation-summary](common/pkm/conversation-summary/SKILL.md) — Save the current conversation as a comprehensive report note in your Obsidian vault, following OKF v0.1 conventions.
- [crit](common/pkm/crit/SKILL.md) — Brainstorm with AI using the CRIT framework to generate and evaluate ideas.
- [dsp-research-engineering](common/deprecated/dsp-research-dispatcher/SKILL.md) — Route DSP hardware and software research-engineering requests to the best specialist workflow and return a unified, decision-ready output.
- [forge-router](common/deprecated/forge-router/SKILL.md) — High-level guidance for choosing the right forge skill.
- [implement-issue](common/engineering/implement-issue/SKILL.md) — Dispatch a child agent in an isolated git worktree to implement a piece of work based on a PRD or set of issues.
- [crit](common/pkm/crit/SKILL.md) — Run the CRIT framework — give the AI Context, assign it a Role, let it Interview you one question at a time, then issue the Task.
- [implement-isolation](common/engineering/implement-isolation/SKILL.md) — Implement a piece of work based on a spec or set of tickets in isolation.
- [implement-isolation-tmux](common/engineering/implement-isolation-tmux/SKILL.md) — Dispatch a child agent in an isolated git worktree to implement a piece of work based on a PRD or set of issues.
- [knowledge-gardener](common/in-progress/knowledge-gardener/SKILL.md) — Run vault-aware semantic search, synthesis, note creation, linking, and Zettelkasten workflows for this Obsidian vault.
- [project-context-pack](common/engineering/project-context-pack/SKILL.md) — Use when the user wants a bounded repo context pack, project map, codebase index, or cached memory file so later work uses fd/rg/tree-sitter/LSP instead of repeated browsing.
- [project-context-pack](common/engineering/project-context-pack/SKILL.md) — Build a bounded repo context pack (project map, codebase index, cached memory file) so later work uses fd/rg/tree-sitter/LSP instead of repeated browsing.
- [research-vault](common/pkm/research-vault/SKILL.md) — Research a topic through a one-question-at-a-time learning conversation, answer directly, share resources when useful, and save a linked OKF-conformant research packet in the Obsidian vault.
- [setup-skills](common/engineering/setup-skills/SKILL.md) — Configure this repo for the engineering skills, set up its issue tracker, triage label vocabulary, and domain doc layout. Run once before first use of the other engineering skills.
- [tmux-launch-agent](common/misc/tmux-launch-agent/SKILL.md) — Fork a new agent CLI session into a new tmux window, detected from the current agent.
- [youtube-video-capture](common/pkm/youtube-video-capture/SKILL.md) — Fetch subtitles from a YouTube video, summarize the content, and save both the summary and raw subtitles to the Video bundle in the Obsidian vault.
## Model-invoked
- [lsp-code-analysis](common/engineering/lsp-code-analysis/SKILL.md) — Semantic code analysis via LSP. Navigate code (definitions, references, implementations), search symbols, preview refactorings, and get file outlines. Use for exploring unfamiliar codebases or performing safe refactoring.
- [pkm-curation](common/pkm/pkm-curation/SKILL.md) — Curate an Obsidian vault — classify notes, normalize frontmatter, add wikilinks, extract atomic notes. Use when curating, batch-processing, reviewing, or doing a serendipity pick.
- [youtube-video-capture](common/pkm/youtube-video-capture/SKILL.md) — Fetch subtitles from a YouTube video, summarize the content, and save the summary and raw subtitles to the Video bundle.
- [forge-gitea](common/deprecated/forge-gitea/SKILL.md) — Use the Gitea CLI (`tea`) to interact with Gitea issues, pull requests, releases, CI, and repository state.
- [forge-github](common/deprecated/forge-github/SKILL.md) — Use the GitHub CLI (`gh`) to interact with GitHub issues, pull requests, releases, CI, and repository state.
- [forge-interaction](common/deprecated/forge-interaction/SKILL.md) — Use when the user wants forge work such as opening a PR, creating or listing issues, checking CI, looking at the repo, pushing a branch, publishing changes, or making a release on GitHub or Gitea. This skill now delegates to specialized skills for better predictability.
- [forge-preferences](common/deprecated/forge-preferences/SKILL.md) — Use with forge-interaction to apply Steve's personal or project-specific GitHub/Gitea remote, CLI, issue, PR, and release preferences.
- [pkm-curation](common/pkm/pkm-curation/SKILL.md) — Curate an Obsidian vault — classify notes, normalize frontmatter, add links, extract atomic notes. Use when curating, batch-processing, reviewing, or doing a serendipity pick.
+6 -11
View File
@@ -5,24 +5,19 @@ Skills that work in all CLI agents.
## User-invoked
- [agent-handoff](in-progress/agent-handoff/SKILL.md) — Hand the current conversation off to a fresh background agent that picks up the work immediately.
- [audio-product-dsp](deprecated/audio-production-dispatcher/SKILL.md) — Dispatch audio product DSP hardware/software engineering requests to the best specialist workflow with measurable product-focused outputs.
- [commit-staged](engineering/commit-staged/SKILL.md) — Commit staged files with a conventional commit message.
- [conversation-summary](pkm/conversation-summary/SKILL.md) — Save the current conversation as a comprehensive report note in your Obsidian vault, following OKF v0.1 conventions.
- [crit](pkm/crit/SKILL.md) — Brainstorm with AI using the CRIT framework to generate and evaluate ideas.
- [dsp-research-engineering](deprecated/dsp-research-dispatcher/SKILL.md) — Route DSP hardware and software research-engineering requests to the best specialist workflow and return a unified, decision-ready output.
- [forge-router](deprecated/forge-router/SKILL.md) — High-level guidance for choosing the right forge skill.
- [implement-issue](engineering/implement-issue/SKILL.md) — Dispatch a child agent in an isolated git worktree to implement a piece of work based on a PRD or set of issues.
- [crit](pkm/crit/SKILL.md) — Run the CRIT framework — give the AI Context, assign it a Role, let it Interview you one question at a time, then issue the Task.
- [implement-isolation](engineering/implement-isolation/SKILL.md) — Implement a piece of work based on a spec or set of tickets in isolation.
- [implement-isolation-tmux](engineering/implement-isolation-tmux/SKILL.md) — Dispatch a child agent in an isolated git worktree to implement a piece of work based on a PRD or set of issues.
- [knowledge-gardener](in-progress/knowledge-gardener/SKILL.md) — Run vault-aware semantic search, synthesis, note creation, linking, and Zettelkasten workflows for this Obsidian vault.
- [project-context-pack](engineering/project-context-pack/SKILL.md) — Use when the user wants a bounded repo context pack, project map, codebase index, or cached memory file so later work uses fd/rg/tree-sitter/LSP instead of repeated browsing.
- [project-context-pack](engineering/project-context-pack/SKILL.md) — Build a bounded repo context pack (project map, codebase index, cached memory file) so later work uses fd/rg/tree-sitter/LSP instead of repeated browsing.
- [research-vault](pkm/research-vault/SKILL.md) — Research a topic through a one-question-at-a-time learning conversation, answer directly, share resources when useful, and save a linked OKF-conformant research packet in the Obsidian vault.
- [setup-skills](engineering/setup-skills/SKILL.md) — Configure this repo for the engineering skills, set up its issue tracker, triage label vocabulary, and domain doc layout. Run once before first use of the other engineering skills.
- [tmux-launch-agent](misc/tmux-launch-agent/SKILL.md) — Fork a new agent CLI session into a new tmux window, detected from the current agent.
- [youtube-video-capture](pkm/youtube-video-capture/SKILL.md) — Fetch subtitles from a YouTube video, summarize the content, and save both the summary and raw subtitles to the Video bundle in the Obsidian vault.
## Model-invoked
- [lsp-code-analysis](engineering/lsp-code-analysis/SKILL.md) — Semantic code analysis via LSP. Navigate code (definitions, references, implementations), search symbols, preview refactorings, and get file outlines. Use for exploring unfamiliar codebases or performing safe refactoring.
- [pkm-curation](pkm/pkm-curation/SKILL.md) — Curate an Obsidian vault — classify notes, normalize frontmatter, add wikilinks, extract atomic notes. Use when curating, batch-processing, reviewing, or doing a serendipity pick.
- [forge-gitea](deprecated/forge-gitea/SKILL.md) — Work with Gitea repositories, issues, pull requests, releases, and CI.
- [forge-github](deprecated/forge-github/SKILL.md) — Work with GitHub repositories, issues, pull requests, releases, and CI.
- [forge-interaction](deprecated/forge-interaction/SKILL.md) — Use when the user wants forge work such as opening a PR, creating or listing issues, checking CI, looking at the repo, pushing a branch, publishing changes, or making a release on GitHub or Gitea. This skill now delegates to specialized skills for better predictability.
- [forge-preferences](deprecated/forge-preferences/SKILL.md) — Use with forge-interaction to apply Steve's personal or project-specific GitHub/Gitea remote, CLI, issue, PR, and release preferences.
- [pkm-curation](pkm/pkm-curation/SKILL.md) — Curate an Obsidian vault — classify notes, normalize frontmatter, add links, extract atomic notes. Use when curating, batch-processing, reviewing, or doing a serendipity pick.
-15
View File
@@ -1,15 +0,0 @@
# Deprecated Skills
## User-invoked
- [forge-router](forge-router/SKILL.md) — High-level guidance for choosing the right forge skill.
**Deprecated / user-invoked:**
- [audio-product-dsp](audio-production-dispatcher/SKILL.md) — Dispatch audio product DSP hardware/software engineering requests to the best specialist workflow with measurable product-focused outputs.
- [dsp-research-engineering](dsp-research-dispatcher/SKILL.md) — Route DSP hardware and software research-engineering requests to the best specialist workflow and return a unified, decision-ready output.
**Deprecated / model-invoked:**
- [forge-gitea](forge-gitea/SKILL.md) — Work with Gitea repositories, issues, pull requests, releases, and CI.
- [forge-github](forge-github/SKILL.md) — Work with GitHub repositories, issues, pull requests, releases, and CI.
- [forge-interaction](forge-interaction/SKILL.md) — Use when the user wants forge work such as opening a PR, creating or listing issues, checking CI, looking at the repo, pushing a branch, publishing changes, or making a release on GitHub or Gitea. This skill now delegates to specialized skills for better predictability.
- [forge-preferences](forge-preferences/SKILL.md) — Use with forge-interaction to apply Steve's personal or project-specific GitHub/Gitea remote, CLI, issue, PR, and release preferences.
@@ -1,180 +0,0 @@
---
disable-model-invocation: true
name: audio-product-dsp
description: Dispatch audio product DSP hardware/software engineering requests to the best specialist workflow with measurable product-focused outputs
---
Role: You are a dispatcher skill for audio product DSP research and engineering. You route requests to the right specialist path(s), enforce product constraints, and return one decision-ready answer.
Primary objectives:
- Classify audio product requests across algorithm, embedded implementation, hardware integration, tuning, and validation.
- Route to the best specialist workflow(s) using explicit scoring.
- Deliver outputs tied to user-perceived quality, latency, power, and manufacturable constraints.
- Keep recommendations testable and release-oriented.
Scope:
- In scope: speech/audio enhancement, ANC, beamforming, AEC/NS/AGC, codec pipelines, loudness/tuning, fixed-point deployment, RT embedded audio, product validation plans.
- Out of scope: medical diagnosis claims, regulatory/legal sign-off, unsafe hearing-level recommendations, fabricated bench/listening data.
Non-goals:
- Do not claim audible improvements without metric or listening-test basis.
- Do not suggest architecture changes that violate hard latency/power/platform constraints without calling out tradeoffs.
- Do not present lab verification as completed if only conceptual.
Inputs expected:
- User request text
- Conversation context
- Available specialist agents/skills
- Product constraints (if available):
- device type (earbuds, headset, speakerphone, soundbar, hearing-assist, etc.)
- mic/speaker topology
- sample rate/frame size
- end-to-end latency budget
- CPU/MIPS, RAM/flash
- battery/power target
- codec/transport constraints (BT, USB, VoIP, etc.)
- target metrics and UX goals
Required output contract:
- Always provide:
1) Selected route
2) Why route fits product goals
3) Final recommendation
4) Assumptions and open risks
5) Verification plan (objective + subjective)
6) Confidence level
Dispatch taxonomy (audio product specific):
- Voice Quality Path: AEC/NS/AGC, double-talk robustness, far-end preservation, speech intelligibility.
- Playback Quality Path: EQ/DRC/loudness, distortion management, clipping avoidance, tonal balance.
- Spatial/Array Path: beamforming, DOA, mic calibration sensitivity, wind/noise robustness.
- ANC Path: feedforward/feedback/hybrid ANC stability, leakage robustness, fit variance strategy.
- Embedded RT Path: buffering, ISR/DMA, frame deadlines, SIMD acceleration, memory bandwidth.
- Hardware Integration Path: codec clocks, interfaces, mic bias/noise floor, amp/headroom, thermal limits.
- Validation Path: objective metrics, golden references, listening tests, production regression.
- Research Synthesis Path: state-of-the-art comparison, feasibility/risk, phased experiment plan.
Routing policy:
1. Parse request into one or more intents.
2. Extract success criteria and hard product constraints.
3. Score candidate routes:
- Relevance (0-5)
- Product-fit (0-5)
- Feasibility/safety (0-5)
- Evidence readiness (0-5)
- Implementation cost (0-5, lower is better)
4. Select single-route or multi-route orchestration.
5. Dispatch structured task packets.
6. Reconcile into one release-oriented recommendation.
Confidence rules:
- High: clear winner and all critical constraints known.
- Medium: winner exists but one non-critical constraint unknown; proceed with explicit assumptions.
- Low: tied routes or missing critical constraint; ask exactly one targeted question.
Critical constraints checklist:
- Product form factor and acoustic topology
- Sample rate, frame size, channel count
- End-to-end latency budget (capture->process->render)
- CPU/MIPS and memory budgets
- Power target and thermal envelope
- Numeric format (float/fixed word lengths)
- UX priority (call clarity, music fidelity, ANC depth, wake-word reliability, etc.)
- Acceptance metrics and pass/fail thresholds
Audio product metrics catalog:
- Voice/call: PESQ/POLQA, STOI, ERLE, double-talk performance, barge-in robustness.
- Playback: THD+N, frequency response error, max SPL before limiting artifacts, crest-factor handling.
- ANC: attenuation vs frequency, residual noise spectra, stability margin, fit-leak sensitivity.
- System: RTL latency, glitch/dropout rate, CPU load, memory headroom, battery impact.
- Subjective: MUSHRA/AB preference tests, panel notes, artifact taxonomy.
Safety and integrity gates:
- Never fabricate measurements, listening outcomes, or citations.
- If hearing safety could be impacted, require explicit level limits and verification steps.
- If irreversible hardware actions are requested, require explicit confirmation and safe fallback path.
- Protect credentials and proprietary parameters.
Specialist route mapping:
- "Improve call quality" -> Voice Quality + Validation paths
- "Reduce earbud power while keeping ANC" -> ANC + Embedded RT + Hardware Integration
- "Fix audio glitches" -> Embedded RT + Hardware Integration + Validation
- "Compare beamforming methods" -> Spatial/Array + Research Synthesis
- "Ship-ready tuning plan" -> Playback/Voice/ANC (as relevant) + Validation
Task packet format for downstream specialists:
```json
{
"objective": "<single product outcome>",
"constraints": {
"latency_ms": "<value or unknown>",
"cpu_budget": "<value or unknown>",
"power_budget": "<value or unknown>",
"platform": "<SoC/DSP/MCU>",
"sample_rate_hz": "<value>",
"frame_size": "<value>
"
},
"required_output": [
"Recommended approach",
"Why it fits product goals",
"Tradeoffs",
"Top 3 risks",
"Objective metrics to track",
"Subjective listening checks",
"Implementation next steps"
],
"limits": [
"No fabricated data",
"State assumptions explicitly"
]
}
```
Orchestration rules:
- Split only when subproblems are independent and interfaces are clear.
- Normalize units (ms, dB, Hz, mW, MIPS) and definitions across outputs.
- Resolve conflicts by preferring measured evidence > validated simulation > reasoned estimate.
- If conflict remains, present it as a decision fork with verification to break the tie.
Fallback behavior:
- If selected specialist fails, retry once with narrower objective and stricter output schema.
- If retry fails, route to a generalist technical path and lower confidence.
- If critical constraints are missing, provide best-effort baseline + one blocking question.
Response template:
```text
Route Selected:
- <specialist path(s)>
Why This Route:
- <1-3 product-focused bullets>
Recommendation:
<final user-facing answer>
Assumptions and Risks:
- <bullets>
Verification Plan:
- Objective: <3-7 checks with metrics and thresholds>
- Subjective: <2-5 listening test checks>
Confidence:
- <High|Medium|Low> with one-line rationale
```
Clarification template (only when blocked):
```text
I can dispatch this accurately, but I need one detail:
- <single targeted question>
Default I will assume for speed:
- <recommended default>
```
Quality bar:
- Product impact over algorithm novelty.
- Verifiable claims over qualitative promises.
- Fast experiment loops over broad rewrites.
- Explicit uncertainty over false precision.
@@ -1,150 +0,0 @@
---
disable-model-invocation: true
name: research-engineering
description: Route DSP hardware and software research-engineering requests to the best specialist workflow and return a unified, decision-ready output
---
Role: You are a dispatcher skill for DSP hardware and software research engineering. You triage requests, select the right specialist path(s), enforce safety and reproducibility constraints, and return one coherent response.
Primary objectives:
- Identify technical intent across algorithms, embedded implementation, hardware architecture, tooling, and validation.
- Route work to the most appropriate specialist workflow(s) with explicit assumptions.
- Produce practical, testable outputs for research engineering decisions.
- Minimize unnecessary handoffs and avoid over-engineering.
Scope:
- In scope: signal analysis, DSP algorithm design, fixed-point strategy, embedded audio/DSP implementation, architecture tradeoffs, measurement plans, benchmarking, verification strategy, literature-grounded research synthesis.
- Out of scope: legal/compliance claims, medical claims, fabrication process sign-off, irreversible production actions.
Non-goals:
- Do not pretend to run lab measurements that were not run.
- Do not claim numerical performance without source, simulation, or measurement basis.
- Do not bypass hardware safety, power, thermal, EMC, or hearing-safety constraints.
Inputs expected:
- User request text
- Current conversation context
- Available specialist agents/skills
- Environment/tooling constraints
- Optional project constraints (sample rate, latency budget, CPU target, memory budget, power target, BOM constraints)
Required output contract:
- Always provide:
1) Selected route
2) Why this route
3) Final user-facing result
4) Assumptions and unknowns
5) Verification plan (how to confirm correctness/performance)
Dispatch taxonomy:
- Algorithm Design: filters, adaptive processing, beamforming, detection/classification front-ends, denoising, dynamics, time-frequency methods.
- Numerical Implementation: fixed-point, quantization noise, saturation behavior, scaling, coefficient sensitivity, stability under finite precision.
- Embedded Software: RT constraints, DMA/ISR design, buffering, scheduling, memory layout, SIMD/accelerators, portability.
- Hardware/Platform: MCU/DSP/FPGA partitioning, codec/interface constraints, clocking, throughput, latency, power/thermal tradeoffs.
- Validation and Measurement: objective metrics, stimulus design, golden references, regression tests, bench/lab measurement plans.
- Research Synthesis: literature scan, method comparison, risk/novelty assessment, experiment roadmap.
Routing policy:
1. Parse request into one or more intents.
2. Extract hard constraints and success criteria.
3. Score candidate routes on:
- Relevance (0-5)
- Capability fit (0-5)
- Safety/feasibility (0-5)
- Evidence availability (0-5)
- Execution cost (0-5, lower is better)
4. Select route:
- Single-route if one clear winner.
- Multi-route if subproblems are separable and independent.
5. Dispatch with structured task packets.
6. Reconcile outputs into a single final response.
Confidence rules:
- High: top route exceeds second by >= 3 and all hard constraints are known.
- Medium: top route exceeds second by 1-2 or one non-critical constraint missing; proceed with explicit assumptions.
- Low: tie score or missing critical constraint (platform, sample rate, latency, safety limit); ask exactly one targeted question.
Critical constraints checklist:
- Target platform (e.g., Cortex-M4/M7, SHARC, FPGA family)
- Sample rate and channel count
- End-to-end latency budget
- CPU/memory budget
- Power/thermal envelope (if embedded/portable)
- Numeric format (float/fixed, word lengths)
- Required performance metrics (SNR, THD+N, PESQ/STOI, detection F1, etc.)
Safety and integrity gates (must run before dispatch):
- If safety-critical or human-impacting audio claims are requested, include explicit uncertainty and verification requirements.
- If destructive hardware actions are requested, require explicit confirmation and safe fallback.
- Never expose secrets, proprietary keys, or internal credentials.
- Never fabricate measurement data or citations.
Specialist route mapping:
- Signal characterization question -> Signal Analysis specialist
- Embedded DSP implementation/debug -> Embedded DSP specialist
- Hardware/software partitioning -> Embedded hardware architect path
- Literature-heavy "state of the art" request -> Research Assistant or literature path
- Cross-domain request (algorithm + embedded + validation) -> Multi-route orchestration with unified recommendation
Task packet format for downstream specialists:
```json
{
"objective": "<single clear objective>",
"context": ["<key constraints>", "<known assumptions>"],
"required_output": [
"Approach",
"Tradeoffs",
"Risks",
"Verification steps",
"Confidence"
],
"limits": ["No fabricated data", "State unknowns explicitly"]
}
```
Multi-route orchestration rules:
- Split only when interfaces between subproblems are clear.
- Normalize units and terminology across outputs.
- Resolve disagreements by preferring: measured evidence > validated simulation > reasoned estimate.
- If unresolved conflict remains, surface it as a decision risk.
Fallback behavior:
- If selected specialist fails, retry once with narrowed objective and stricter output format.
- If retry fails, route to a generalist technical path and label confidence reduced.
- If key constraints are missing, provide a best-effort scaffold plus one blocking question.
Response template:
```text
Route Selected:
- <specialist path(s)>
Why This Route:
- <1-3 concise bullets>
Result:
<final user-facing answer>
Assumptions and Unknowns:
- <bullet list or "None">
Verification Plan:
- <3-7 concrete checks/tests/measurements>
Confidence:
- <High|Medium|Low> with one-line rationale
```
Clarification template (only when blocked):
```text
I can dispatch this precisely, but I need one detail:
- <single targeted question>
Default I will assume if you prefer speed:
- <recommended default>
```
Quality bar:
- Actionable over theoretical.
- Reproducible over vague.
- Explicit uncertainty over false precision.
- Deliver the smallest valid plan that can be tested quickly.
-146
View File
@@ -1,146 +0,0 @@
---
name: forge-gitea
---
# Gitea Forge Interaction
Use this skill for Gitea forge work when the user wants to interact with Gitea repositories, issues, pull requests, releases, or CI.
## Purpose
Choose the correct Gitea CLI (`tea`) and use it to interact with Gitea issues, pull requests, releases, CI, and repository state.
This skill is intended to perform Gitea forge actions, including mutating actions, when the user asks for them. Do not turn every requested forge action into a confirmation loop; if the user clearly asks to create, edit, comment, publish, or release, do the requested action after selecting the correct CLI.
## Strict Decision Tree
Follow this order every time.
### 1. Determine the target remote
1. If the user explicitly says which remote or forge to use, use that.
2. Else, if user/project memory or repo guidance states where to find the forge, use that.
3. Else, if a remote named `forge` exists, use `forge`.
4. Else, if the user has configured a default remote, use that.
5. Else, use `origin`.
Useful inspection commands:
```bash
git remote -v
git config --get checkout.defaultRemote
git config --get clone.defaultRemoteName
git config --get branch.$(git branch --show-current).remote
```
Interpretation:
- A remote named `forge` is the preferred convention for the canonical forge remote.
- If no `forge` remote exists, `origin` is the fallback.
- If the current branch has an upstream remote and no stronger rule applies, treat that as the user's configured default for the current work.
Only mention this selection if there is ambiguity or a conflict.
### 2. Choose the CLI
Use `tea` for Gitea interactions.
Check whether the chosen CLI is available:
```bash
command -v tea >/dev/null 2>&1 && tea --version
```
If the needed CLI is missing:
- Tell the user which CLI is required: `tea` for Gitea.
- Tell the user to install it.
- If it may already be installed but not discoverable, tell the user to add it to their `PATH`.
- Do not use the wrong CLI as a fallback.
### 3. Check authentication/context
Use read-only checks for the selected tool:
```bash
tea login list
tea repos ls
```
If authentication is missing, tell the user which CLI needs login/configuration. Do not ask the user to paste tokens or secrets.
### 4. Do the requested forge task
Perform the requested action with `tea` commands such as:
```bash
tea issues list
tea issues create
tea issues comment
tea pulls list
tea pulls create
tea pulls view
tea pulls comment
tea pulls merge
tea releases list
tea releases create
```
Use exact command syntax supported by the installed CLI version; inspect help when needed:
```bash
tea help
```
## Mutating Actions
Mutating forge actions are allowed when clearly requested by the user, including:
- create an issue;
- modify an issue;
- comment on an issue;
- create a pull request;
- modify a pull request;
- comment on a pull request;
- push a branch;
- publish changes;
- create a release.
Still be careful with destructive or high-impact actions:
- Ask before deleting branches, tags, releases, issues, or repositories.
- Ask before force-pushing.
- Ask before merging a PR unless the user explicitly asked to merge it.
- Ask before overwriting existing release assets or tags.
## Branch and PR Preparation
Before creating or updating a PR:
1. Select the remote using the strict decision tree.
2. Inspect branch state and upstream tracking.
3. Inspect the diff against the target branch.
4. Read the PR template if one exists.
5. Push the branch if needed and requested by the workflow.
6. Create or update the PR.
Useful local inspection:
```bash
git status --short --branch
git branch -vv
git diff --stat
git diff --check
```
## Output Style
Normally, do not over-explain. If the remote/tool selection is straightforward, just complete the task and summarize the result.
Report detection details only when there is ambiguity, conflict, missing tooling, or failure. In those cases include:
- selected remote;
- detected forge;
- selected CLI;
- reason for the choice;
- what the user needs to fix, if anything.
-149
View File
@@ -1,149 +0,0 @@
---
name: forge-github
---
# GitHub Forge Interaction
Use this skill for GitHub forge work when the user wants to interact with GitHub repositories, issues, pull requests, releases, or CI.
## Purpose
Choose the correct GitHub CLI (`gh`) and use it to interact with GitHub issues, pull requests, releases, CI, and repository state.
This skill is intended to perform GitHub forge actions, including mutating actions, when the user asks for them. Do not turn every requested forge action into a confirmation loop; if the user clearly asks to create, edit, comment, publish, or release, do the requested action after selecting the correct CLI.
## Strict Decision Tree
Follow this order every time.
### 1. Determine the target remote
1. If the user explicitly says which remote or forge to use, use that.
2. Else, if user/project memory or repo guidance states where to find the forge, use that.
3. Else, if a remote named `forge` exists, use `forge`.
4. Else, if the user has configured a default remote, use that.
5. Else, use `origin`.
Useful inspection commands:
```bash
git remote -v
git config --get checkout.defaultRemote
git config --get clone.defaultRemoteName
git config --get branch.$(git branch --show-current).remote
```
Interpretation:
- A remote named `forge` is the preferred convention for the canonical forge remote.
- If no `forge` remote exists, `origin` is the fallback.
- If the current branch has an upstream remote and no stronger rule applies, treat that as the user's configured default for the current work.
Only mention this selection if there is ambiguity or a conflict.
### 2. Choose the CLI
Use `gh` for GitHub interactions.
Check whether the chosen CLI is available:
```bash
command -v gh >/dev/null 2>&1 && gh --version
```
If the needed CLI is missing:
- Tell the user which CLI is required: `gh` for GitHub.
- Tell the user to install it.
- If it may already be installed but not discoverable, tell the user to add it to their `PATH`.
- Do not use the wrong CLI as a fallback.
### 3. Check authentication/context
Use read-only checks for the selected tool:
```bash
gh auth status
gh repo view
```
If authentication is missing, tell the user which CLI needs login/configuration. Do not ask the user to paste tokens or secrets.
### 4. Do the requested forge task
Perform the requested action with `gh` commands such as:
```bash
gh issue list
gh issue create
gh issue comment
gh pr list
gh pr create
gh pr view
gh pr comment
gh pr edit
gh pr merge
gh run list
gh run view
gh release list
gh release create
```
Use exact command syntax supported by the installed CLI version; inspect help when needed:
```bash
gh help
```
## Mutating Actions
Mutating forge actions are allowed when clearly requested by the user, including:
- create an issue;
- modify an issue;
- comment on an issue;
- create a pull request;
- modify a pull request;
- comment on a pull request;
- push a branch;
- publish changes;
- create a release.
Still be careful with destructive or high-impact actions:
- Ask before deleting branches, tags, releases, issues, or repositories.
- Ask before force-pushing.
- Ask before merging a PR unless the user explicitly asked to merge it.
- Ask before overwriting existing release assets or tags.
## Branch and PR Preparation
Before creating or updating a PR:
1. Select the remote using the strict decision tree.
2. Inspect branch state and upstream tracking.
3. Inspect the diff against the target branch.
4. Read the PR template if one exists.
5. Push the branch if needed and requested by the workflow.
6. Create or update the PR.
Useful local inspection:
```bash
git status --short --branch
git branch -vv
git diff --stat
git diff --check
```
## Output Style
Normally, do not over-explain. If the remote/tool selection is straightforward, just complete the task and summarize the result.
Report detection details only when there is ambiguity, conflict, missing tooling, or failure. In those cases include:
- selected remote;
- detected forge;
- selected CLI;
- reason for the choice;
- what the user needs to fix, if anything.
@@ -1,227 +0,0 @@
---
name: forge-interaction
description: Use when the user wants forge work such as opening a PR, creating or listing issues, checking CI, looking at the repo, pushing a branch, publishing changes, or making a release on GitHub or Gitea. This skill now delegates to specialized skills for better predictability.
---
# Forge Interaction (Router)
This skill has been refactored into specialized skills for better predictability and maintainability:
- **GitHub**: Use `/forge-github` for GitHub repositories, issues, pull requests, releases, or CI
- **Gitea**: Use `/forge-gitea` for Gitea repositories, issues, pull requests, releases, or CI
## Purpose
This router skill delegates to the appropriate specialized forge interaction skill based on the target forge. Each specialized skill handles one forge type completely, making them more predictable and easier to maintain.
## When to use:
- If you're unsure which forge you're working with, use this skill and it will guide you
- For general forge work without specifying the forge type
- When you want the system to figure out which specialized skill to use
## Delegation Logic
The router analyzes your request and determines whether to delegate to:
1. **GitHub skill** (`/forge-github`) - for GitHub repositories and workflows
2. **Gitea skill** (`/forge-gitea`) - for Gitea repositories and workflows
Each specialized skill contains the complete decision tree and implementation for its respective forge type.
## Recommendation
For most predictable results, use the specialized skills directly:
- `/forge-github` when working with GitHub
- `/forge-gitea` when working with Gitea
This separation follows the principle of single responsibility and makes each skill more focused and reliable.
## Strict Decision Tree
Follow this order every time.
### 1. Determine the target remote
1. If the user explicitly says which remote or forge to use, use that.
2. Else, if user/project memory or repo guidance states where to find the forge, use that.
3. Else, if a remote named `forge` exists, use `forge`.
4. Else, if the user has configured a default remote, use that.
5. Else, use `origin`.
Useful inspection commands:
```bash
git remote -v
git config --get checkout.defaultRemote
git config --get clone.defaultRemoteName
git config --get branch.$(git branch --show-current).remote
```
Interpretation:
- A remote named `forge` is the preferred convention for the canonical forge remote.
- If no `forge` remote exists, `origin` is the fallback.
- If the current branch has an upstream remote and no stronger rule applies, treat that as the user's configured default for the current work.
Only mention this selection if there is ambiguity or a conflict.
### 2. Determine the forge type from the chosen remote
Inspect the selected remote URL:
```bash
git remote get-url <remote>
```
Classify it:
- URLs containing `github.com` are GitHub.
- URLs containing `gitea`, or a known Gitea host from memory/project guidance, are Gitea.
- If the remote is self-hosted and ambiguous, inspect repo guidance, memory, and web/API/tool configuration before asking.
Do not choose based on which CLI is installed. The remote determines the forge; the forge determines the CLI.
### 3. Choose the CLI
- If the forge is GitHub, use `gh`.
- If the forge is Gitea, use `tea`.
Check whether the chosen CLI is available:
```bash
command -v gh >/dev/null 2>&1 && gh --version
command -v tea >/dev/null 2>&1 && tea --version
```
If the needed CLI is missing:
- Tell the user which CLI is required: `gh` for GitHub, `tea` for Gitea.
- Tell the user to install it.
- If it may already be installed but not discoverable, tell the user to add it to their `PATH`.
- Do not use the wrong CLI as a fallback.
### 4. Check authentication/context
Use read-only checks for the selected tool:
```bash
# GitHub
gh auth status
gh repo view
# Gitea
tea login list
tea repos ls
```
If authentication is missing, tell the user which CLI needs login/configuration. Do not ask the user to paste tokens or secrets.
### 5. Do the requested forge task
Perform the requested action with the selected CLI.
For GitHub, use `gh` commands such as:
```bash
gh issue list
gh issue create
gh issue comment
gh pr list
gh pr create
gh pr view
gh pr comment
gh pr edit
gh pr merge
gh run list
gh run view
gh release list
gh release create
```
For Gitea, use `tea` commands such as:
```bash
tea issues list
tea issues create
tea issues comment
tea pulls list
tea pulls create
tea pulls view
tea pulls comment
tea pulls merge
tea releases list
tea releases create
```
Use exact command syntax supported by the installed CLI version; inspect help when needed:
```bash
gh help
tea help
```
## Mutating Actions
Mutating forge actions are allowed when clearly requested by the user, including:
- create an issue;
- modify an issue;
- comment on an issue;
- create a pull request;
- modify a pull request;
- comment on a pull request;
- push a branch;
- publish changes;
- create a release.
Still be careful with destructive or high-impact actions:
- Ask before deleting branches, tags, releases, issues, or repositories.
- Ask before force-pushing.
- Ask before merging a PR unless the user explicitly asked to merge it.
- Ask before overwriting existing release assets or tags.
## Branch and PR Preparation
Before creating or updating a PR:
1. Select the remote using the strict decision tree.
2. Select the CLI from the remote's forge type.
3. Inspect branch state and upstream tracking.
4. Inspect the diff against the target branch.
5. Read the PR template if one exists.
6. Push the branch if needed and requested by the workflow.
7. Create or update the PR.
Useful local inspection:
```bash
git status --short --branch
git branch -vv
git diff --stat
git diff --check
```
## Output Style
Normally, do not over-explain. If the remote/tool selection is straightforward, just complete the task and summarize the result.
Report detection details only when there is ambiguity, conflict, missing tooling, or failure. In those cases include:
- selected remote;
- detected forge;
- selected CLI;
- reason for the choice;
- what the user needs to fix, if anything.
## Companion Personal Skill
Personal forge preferences should live in a separate skill or memory entry, not in this general skill. That companion skill/memory may specify things like:
- preferred default remote conventions;
- known personal Gitea or GitHub hosts;
- preferred issue/PR/release styles;
- project-specific forge rules.
This general skill should consume that memory/guidance when present, but keep the generic decision tree above as the fallback.
@@ -1,44 +0,0 @@
---
name: forge-preferences
description: Use with forge-interaction to apply Steve's personal or project-specific GitHub/Gitea remote, CLI, issue, PR, and release preferences.
---
# Forge Preferences
Use this companion skill with `/forge-interaction` when personal or project-specific forge preferences are relevant.
## Purpose
Store personal conventions that should not live in the generic forge interaction skill.
## Current Preferences
- If a remote named `forge` exists, treat it as the canonical forge remote.
- If no `forge` remote exists, use the user's configured default remote when available.
- If no default remote is configured, use `origin`.
- Use the forge type of the selected remote to choose the CLI:
- GitHub → `gh`
- Gitea → `tea`
- If the correct CLI is missing, tell the user to install it or add it to `PATH`.
- Do not use the wrong CLI as a fallback.
## Derived Preferences (auto-detected at runtime)
- Canonical forge remote: prefer `forge`, then default remote, then `origin`.
- Forge CLI: determined from remote URL (GitHub → `gh`, Gitea → `tea`).
- Known personal Gitea hosts and GitHub usernames/orgs are collected from the remote URL and git CLI at time of use — no hardcoded list.
## Issue Labels
See `triage-labels.md` in this directory for the canonical-to-actual label mapping.
## PR Conventions
- PR titles follow Conventional Commits: `type(scope): description`.
- Common types: `feat`, `fix`, `refactor`, `docs`, `chore`, `test`, `style`.
- PR body is freeform but should summarise the change and any breaking or noteworthy details.
## Release Conventions
- Tags follow semver: `v{major}.{minor}.{patch}` (e.g. `v1.2.3`).
- Releases are created from the tag with auto-generated or manually curated notes.
@@ -1,15 +0,0 @@
# Triage Labels
The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker.
| Label in mattpocock/skills | Label in our tracker | Meaning |
| -------------------------- | -------------------- | ---------------------------------------- |
| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
| `needs-info` | `needs-info` | Waiting on reporter for more information |
| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
| `ready-for-human` | `ready-for-human` | Requires human implementation |
| `wontfix` | `wontfix` | Will not be actioned |
When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table.
Edit the right-hand column to match whatever vocabulary you actually use.
-50
View File
@@ -1,50 +0,0 @@
---
name: forge-router
disable-model-invocation: true
---
# Forge Router
Choose the right forge interaction skill for your task.
## When to use:
- **GitHub**: Use `/forge-github` when working with GitHub repositories, issues, pull requests, releases, or CI.
- **Gitea**: Use `/forge-gitea` when working with Gitea repositories, issues, pull requests, releases, or CI.
## How it works:
The router delegates to specialized skills that handle each forge type separately. This keeps each skill focused and predictable.
**GitHub skill** (`/forge-github`):
- Handles GitHub-specific interactions using the `gh` CLI
- Follows GitHub's API patterns and conventions
- Optimized for GitHub's feature set
**Gitea skill** (`/forge-gitea`):
- Handles Gitea-specific interactions using the `tea` CLI
- Follows Gitea's API patterns and conventions
- Optimized for Gitea's feature set
## Why separate:
- **Predictability**: Each skill knows exactly one forge type
- **Maintainability**: Changes to GitHub or Gitea logic stay isolated
- **Clarity**: Users can see which forge a skill handles at a glance
- **Testing**: Each skill can be tested independently
## Usage examples:
```
# For GitHub work
/forge-github create an issue in this repo
/forge-github list pull requests
/forge-github check CI status
# For Gitea work
/forge-gitea create an issue in this repo
/forge-gitea list pull requests
/forge-gitea check CI status
```
The router ensures you always use the right tool for the right forge.
-86
View File
@@ -1,86 +0,0 @@
# Forge Skills
A collection of specialized forge interaction skills for GitHub and Gitea.
## Overview
This directory contains focused skills for interacting with different code hosting platforms:
- **GitHub**: Use `/forge-github` for GitHub repositories, issues, pull requests, releases, or CI
- **Gitea**: Use `/forge-gitea` for Gitea repositories, issues, pull requests, releases, or CI
- **Router**: Use `/forge-interaction` to let the system choose the right skill automatically
## Skill Structure
Each forge-specific skill is designed with:
1. **Single Responsibility**: Handles only one forge type (GitHub or Gitea)
2. **Predictable Behavior**: Clear decision tree and completion criteria
3. **Complete Coverage**: All requested forge actions are supported
4. **Error Handling**: Specific guidance for missing tools or authentication
## Choosing the Right Skill
### Use `/forge-github` when:
- Working with GitHub repositories (`github.com` URLs)
- Using the `gh` CLI tool
- Interacting with GitHub-specific features (issues, PRs, releases, CI)
### Use `/forge-gitea` when:
- Working with Gitea repositories (`gitea.com` or self-hosted Gitea)
- Using the `tea` CLI tool
- Interacting with Gitea-specific features (issues, PRs, releases, CI)
### Use `/forge-interaction` when:
- You're unsure which forge you're working with
- You want the system to automatically choose the right skill
- You prefer a unified interface that handles both platforms
## Skill Comparison
| Feature | GitHub Skill | Gitea Skill |
|---------|--------------|-------------|
| CLI Tool | `gh` | `tea` |
| Platform | GitHub | Gitea |
| Focus | GitHub-specific | Gitea-specific |
| Predictability | High | High |
| Maintenance | Isolated | Isolated |
## Usage Examples
```
# For GitHub work
/forge-github create an issue in this repo
/forge-github list pull requests
/forge-github check CI status
/forge-github publish a new release
# For Gitea work
/forge-gitea create an issue in this repo
/forge-gitea list pull requests
/forge-gitea check CI status
/forge-gitea publish a new release
# Let the system decide
/forge-interaction create a PR for this feature
```
## Why Separate Skills?
1. **Predictability**: Each skill knows exactly one forge type
2. **Maintainability**: Changes to GitHub or Gitea logic stay isolated
3. **Clarity**: Users can see which forge a skill handles at a glance
4. **Testing**: Each skill can be tested independently
5. **Cognitive Load**: Users remember fewer skills and their specific purposes
## Related Skills
- `/forge-router` - High-level guidance for choosing the right forge skill
- `/forge-preferences` - Personal or project-specific forge configurations
## Next Steps
1. Choose the appropriate skill based on your forge platform
2. Ensure the required CLI tool (`gh` or `tea`) is installed
3. Configure authentication for your forge platform
4. Start with simple actions and build up to more complex workflows
+3 -2
View File
@@ -5,8 +5,9 @@ Daily code work.
## User-invoked
- [commit-staged](commit-staged/SKILL.md) — Commit staged files with a conventional commit message.
- [implement-issue](implement-issue/SKILL.md) — Dispatch a child agent in an isolated git worktree to implement a piece of work based on a PRD or set of issues.
- [project-context-pack](project-context-pack/SKILL.md) — Use when the user wants a bounded repo context pack, project map, codebase index, or cached memory file so later work uses fd/rg/tree-sitter/LSP instead of repeated browsing.
- [implement-isolation](implement-isolation/SKILL.md) — Implement a piece of work based on a spec or set of tickets in isolation.
- [implement-isolation-tmux](implement-isolation-tmux/SKILL.md) — Dispatch a child agent in an isolated git worktree to implement a piece of work based on a PRD or set of issues.
- [project-context-pack](project-context-pack/SKILL.md) — Build a bounded repo context pack (project map, codebase index, cached memory file) so later work uses fd/rg/tree-sitter/LSP instead of repeated browsing.
- [setup-skills](setup-skills/SKILL.md) — Configure this repo for the engineering skills, set up its issue tracker, triage label vocabulary, and domain doc layout. Run once before first use of the other engineering skills.
## Model-invoked
@@ -4,8 +4,6 @@ description: Commit staged files with a conventional commit message.
disable-model-invocation: true
---
Commit staged files with a conventional commit message.
## Process
1. **Check the staging area** — Run `git diff --cached --stat`. If empty, report "nothing staged" and stop.
@@ -0,0 +1,5 @@
interface:
display_name: "Commit Staged"
short_description: "Commit staged changes to the repository"
policy:
allow_implicit_invocation: false
@@ -0,0 +1,72 @@
---
name: implement-isolation-tmux
description: "Dispatch a child agent in an isolated git worktree to implement a piece of work based on a PRD or set of issues."
disable-model-invocation: true
---
## Invocation
/skill:implement-isolation-tmux <N> [--base <branch>] [--force]
- `<N>` — required, the issue number
- `--base <branch>` — optional, target base branch (default: repo default branch)
- `--force` — optional, remove the existing worktree directory first, then recreate
Triage labels follow the vocabulary in `docs/agents/triage-labels.md`.
## Process
## 1. Claim and isolate
Change the issue triage label to `in-progress`, remove all other labels. Derive `<issue-slug>` from the issue title: lowercase, replace spaces with hyphens, strip non-alphanumeric characters (keep hyphens). Then create a git worktree for the ticket as a sibling directory named `../issue-<N>-<issue-slug>`, checked out from the base branch. Switch to the worktree. The branch name matches the directory: `issue-<N>-<issue-slug>`.
```bash
git worktree add -b issue-<N>-<issue-slug> ../issue-<N>-<issue-slug> <base>
```
Stay in the worktree directory for the rest of the process.
Completion criterion: The issue label is confirmed as `in-progress` and the worktree exists at `../issue-<N>-<issue-slug>` with branch `issue-<N>-<issue-slug>` checked out.
### 2. Compose the child prompt
Assemble a single prompt that the child agent will receive. Include:
- **Issue body** — the full markdown body of the issue.
- **Comments** — all comments, if any
- **Standing instruction**: "Implement the work described by the user in the spec or tickets, using tdd skill where possible. Run typechecking regularly, run single test files regularly, and run the full test suite once at the end. Once done, use code-review skill to review the work. Commit your work and push the new branch and create a PR with a link to the issue, one-sentence summary, and short key-changes list. Comment on the issue with the PR link: `PR opened: <url>`. Change the issue triage label to needs-review when done. Remove all other labels"
- **Failure instruction**: "If any step fails, report where you stopped and what remains for manual recovery. Print the exact commands needed."
Completion criterion: The prompt is composed with all required sections (issue body, comments, standing instruction, failure instruction).
### 3. Launch the child via tmux-launch-agent
Use the `tmux-launch-agent` skill to fork the child agent into a new tmux window. Tmux-launch-agent handles agent detection, config lookup, command building (prompt-file or stdin-pipe), mise/SHELL wrapping, and `tmux new-window` creation.
Pass these parameters:
| Parameter | Value |
|-----------|-------|
| `--name` | `"issue-<N>-<issue-slug>"` — tmux window title |
| Remaining args | The full prompt text composed in step 2 |
Ensure the child agent starts in the worktree. If the current pane's working directory is already inside `<absolute-worktree-path>` (from step 1), tmux-launch-agent's new window inherits it. Otherwise, pass `-c <absolute-worktree-path>` when invoking tmux-launch-agent so the child agent starts in the worktree directory.
Let tmux-launch-agent handle prompt-file creation — it writes a temp file automatically when the target agent uses prompt-file mode. Pass the prompt text inline.
The window stays open after the child completes so the user can review the output.
Completion criterion: `tmux-launch-agent` exits 0 and a new tmux window appears with the child agent session active.
### 4. Print summary
After launching, print:
- Issue number and title
- Worktree path
- Branch name
- Base branch
- Agent used (detected by tmux-launch-agent)
- Tmux window name (so the user can find it)
- Cleanup command: `git worktree remove <worktree-path> && git worktree prune`
@@ -0,0 +1,5 @@
interface:
display_name: "Implement Isolation Tmux"
short_description: "Implement isolation using tmux for the agent"
policy:
allow_implicit_invocation: false
@@ -0,0 +1,59 @@
---
name: implement-isolation
description: "Implement a piece of work based on a spec or set of tickets in isolation."
disable-model-invocation: true
---
## Invocation
/skill:implement-isolation <N> [--base <branch>] [--force]
- `<N>` — required, the ticket/issue number
- `--base <branch>` — optional, target base branch (default: repo default branch)
- `--force` — optional, remove the existing worktree directory first, then recreate
Triage labels follow the vocabulary in `docs/agents/triage-labels.md`.
## Process
### 1. Claim and isolate
Change the issue triage label to `in-progress`, remove all other labels. Derive `<issue-slug>` from the issue title: lowercase, replace spaces with hyphens, strip non-alphanumeric characters (keep hyphens). Then create a git worktree for the ticket as a sibling directory named `../issue-<N>-<issue-slug>`, checked out from the base branch. Switch to the worktree. The branch name matches the directory: `issue-<N>-<issue-slug>`.
```bash
git worktree add -b issue-<N>-<issue-slug> ../issue-<N>-<issue-slug> <base>
```
Stay in the worktree directory for the rest of the process.
Completion criterion: The issue label is confirmed as `in-progress` and the worktree exists at `../issue-<N>-<issue-slug>` with branch `issue-<N>-<issue-slug>` checked out.
### 2. Implement
Start a subagent to implement the work in isolation. The subagent receives a single prompt that includes:
- Issue body: the full markdown body of the issue.
- Comments: all comments, if any
- Implement: the work described by the user in the spec or tickets, using tdd skill where possible. Run typechecking regularly, run single test files regularly, and run the full test suite once at the end.
Completion criterion: The subagent exits with code 0 and all changes are committed.
### 3. Review
Start a reviewer subagent that runs the `code-review` skill against the base branch to identify findings in the diff. Wait for the reviewer to finish.
Review each finding and fix the code until zero findings remain. Commit fixes as you go.
Completion criterion: All code-review findings are resolved (zero open findings) and fixes are committed.
### 4. Ship
Push the branch, open a PR. The PR description must include a link to the ticket and a key-changes list.
Comment on the issue: `PR opened: <url>`. Label the issue `needs-review`, remove all other labels.
Print a cleanup command for the user: `git worktree remove ../issue-<N>-<issue-slug> && git worktree prune`
### Failure recovery
If the implement subagent exits non-zero or the review finds issues that cannot be fully resolved, stop. Report to the user: issue number, what was completed, what failed, and the exact commands needed to resume or clean up (including `git worktree remove ...`). Do not push partial work.
@@ -0,0 +1,5 @@
interface:
display_name: "Implement Isolation"
short_description: "Implement isolation for the agent"
policy:
allow_implicit_invocation: false
@@ -1,76 +0,0 @@
---
name: implement-issue
description: "Dispatch a child agent in an isolated git worktree to implement a piece of work based on a PRD or set of issues."
disable-model-invocation: true
---
## Invocation
/skill:implement-issue <N> [--base <branch>] [--force]
- `<N>` — required, the issue number
- `--base <branch>` — optional, target base branch (default: repo default branch)
- `--force` — optional, allow overwriting an existing worktree
## Process
### 1. Setup
#### a. Label the issue `in-progress`
Change the issue triage label to `in-progress`.
Completion criterion: The issue label is confirmed as `in-progress`.
#### b. Create the worktree
```bash
git worktree add -b <branch-name> ../<repo-name>-issue-<N> <base>
```
Completion criterion: The worktree exists at `../<repo-name>-issue-<N>` and the new branch is checked out.
### 2. Compose the child prompt
Assemble a single prompt that the child agent will receive. Include:
- **Issue body** — the full markdown body of the issue.
- **Comments** — all comments, if any
- **Standing instruction**: "Implement using /tdd where possible, at the pre-agreed seam. Run typechecking regularly, run single test files regularly, and run the full test suite once at the end. Once done, use /code-review to review the work. Commit your work and push the new branch and create a PR with a link to the issue, one-sentence summary, and short key-changes list. Comment on the issue with the PR link: `PR opened: <url>`. Change the issue triage label to needs-review when done."
- **Failure instruction**: "If any step fails, report where you stopped and what remains for manual recovery. Print the exact commands needed."
(The prompt is passed as arguments to tmux-launch-agent in step 3, which handles writing it to a temp file if needed.)
Completion criterion: The prompt is composed with all required sections (issue body, comments, standing instruction, failure instruction).
### 3. Launch the child via tmux-launch-agent
Use the [`tmux-launch-agent`](../../../../.agents/skills/tmux-launch-agent/SKILL.md) skill to fork the child agent into a new tmux window. Tmux-launch-agent handles agent detection, config lookup, command building (prompt-file or stdin-pipe), mise/SHELL wrapping, and `tmux new-window` creation.
Pass these parameters:
| Parameter | Value |
|-----------|-------|
| `--name` | `"issue-<N>-<repo-slug>"` — tmux window title |
| Remaining args | The full prompt text composed in step 2 |
Ensure the child agent starts in the worktree. If the current pane's working directory is already inside `<absolute-worktree-path>` (from step 1b), tmux-launch-agent's new window inherits it. Otherwise, add `-c <absolute-worktree-path>` to the `tmux new-window` command in tmux-launch-agent's step 6.
Do **not** pre-write the prompt to a separate file — tmux-launch-agent writes its own temp file if the target agent uses prompt-file mode.
The window stays open after the child completes so the user can review the output.
Completion criterion: `tmux new-window` exits 0 and a new tmux window appears with the child agent session active.
### 4. Print summary
After launching, print:
- Issue number and title
- Worktree path
- Branch name
- Base branch
- Agent used (detected by tmux-launch-agent)
- Tmux window name (so the user can find it)
- Cleanup command: `git worktree remove <worktree-path> && git worktree prune`
The parent's turn ends here. The user can dispatch another issue immediately.
@@ -0,0 +1,334 @@
---
name: lsp-code-analysis
description: Semantic code analysis via LSP. Navigate code (definitions, references, implementations), search symbols, preview refactorings, and get file outlines. Use for exploring unfamiliar codebases or performing safe refactoring.
license: LICENSE
---
# LSP Code Analysis
## IMPORTANT: PREREQUISITE
To use this skill, you **MUST** follow these steps:
1. **Check for updates**: Run the [update script](scripts/update.sh) to ensure you are using the latest version of the tool.
2. **Verify project support**: Run `lsp server start <project_path>` to start the LSP server and confirm the project is supported.
**IF YOU DO NOT PERFORM THESE STEPS, YOU ARE NOT ALLOWED TO USE THIS SKILL.**
## Abstract
This document specifies the operational requirements and best practices for the `lsp-code-analysis` skill. It provides a semantic interface to codebase navigation, analysis and refactoring via the Language Server Protocol (LSP).
## Overview
You are provided with `lsp` CLI tool for semantic code navigation and analysis. It SHOULD be preferred over `read` or `grep` for most code understanding tasks.
Usages:
- **Semantic navigation**: Jump to definitions, find references, locate implementations - understands code structure, not just text patterns.
- **Language-aware**: Distinguishes between variables, functions, classes, types - eliminates false positives from text search.
- **Cross-file intelligence**: Trace dependencies, refactor safely across entire codebase - knows what imports what.
- **Type-aware**: Get precise type information, signatures, documentation - without reading implementation code.
### Tool Selection
**Guideline**: You SHOULD prioritize LSP commands for code navigation and analysis. Agents MAY use `read` or `rg` ONLY when semantic analysis is not applicable (e.g., searching for comments or literal strings).
| Task | Traditional Tool | Recommended LSP Command |
| ------------------- | ---------------- | ----------------------------------------------- |
| **Find Definition** | `rg`, `read` | [`definition`](#definition-navigate-to-source)|
| **Find Usages** | `rg` | [`reference`](#reference-find-all-usages) |
| **Understand File** | `read` | [`outline`](#outline-file-structure) |
| **View Docs/Types** | `read` | [`doc`](#doc-get-documentation) |
| **Refactor** | `sed` | See [Refactoring Guide](references/refactor.md) |
## Commands
All commands support `-h` or `--help`.
### Locating Symbols
Most commands use a unified locating syntax via the `--scope` and `--find` options.
**Arguments**: `<file_path>`
**Options**:
- `--scope`: Narrow search to a symbol body or line range.
- `--find`: Text pattern to find within the scope.
**Scope Formats**:
- `<line>`: Single line number (e.g., `42`).
- `<start>,<end>`: Line range (e.g., `10,20`). Use `0` for end to mean till EOF (e.g., `10,0`).
- `<symbol_path>`: Symbol path with dots (e.g., `MyClass.my_method`).
**Find Pattern (`--find`)**:
The `--find` option narrows the target to a **text pattern within the selected scope**:
- The scope is determined by `--scope` (line/range/symbol). If no `--scope` is given, the entire file is the scope.
- Pattern matching is **whitespace-insensitive**: differences in spaces, tabs, and newlines are ignored.
- You MAY include the cursor marker `<|>` inside the pattern to specify the **exact position of interest** within the match (for example, on a variable name, keyword, or operator).
- If `--find` is omitted, the command uses the start of the scope (or a tool-specific default) as the navigation target.
**Cursor Marker (`<|>`)**:
The `<|>` marker indicates the exact position for symbol resolution. It represents the character immediately to its right. Use it within the find pattern to point to a specific element (e.g., `user.<|>name` to target the `name` property).
**Examples**:
- `lsp doc foo.py --find "self.<|>"` - Find `self.` in entire file, position at the character after the dot (typically for completion or member access)
- `lsp doc foo.py --scope 42 --find "return <|>result"` - Find `return result` on line 42, position at `r` of `result`
- `lsp doc foo.py --scope 10,20 --find "if <|>condition"` - Find `if condition` in lines 10-20, position at `c` of `condition`
- `lsp doc foo.py --scope MyClass.my_method --find "self.<|>"` - Find `self.` within `MyClass.my_method`, position after the dot
- `lsp doc foo.py --scope MyClass` - Target the `MyClass` symbol directly
**Guideline for Scope vs. Find**:
- Use `--scope <symbol_path>` (e.g., `--scope MyClass`, `--scope MyClass.my_method`) to target **classes, functions, or methods**. This is the most robust and preferred way to target symbol.
- Use `--find` (often combined with `--scope`) to target variables or specific positions. Use this when the target is not a uniquely named symbol or when you need to pinpoint a specific usage within a code block.
Agents MAY use `lsp locate <file_path> --scope <scope> --find <find>` to verify if the target exists in the file and view its context before running other commands.
```bash
# Verify location exists
lsp locate main.py --scope 42 --find "<|>process_data"
```
### Pagination
Use pagination for large result sets like `reference` or `search`.
- `--pagination-id <ID>`: (Required) Unique session ID for consistent paging.
- `--max-items <N>`: Page size.
- `--start-index <N>`: Offset (0-based).
**Example**:
```bash
# Page 1
lsp search "User" --max-items 20 --pagination-id "task_123"
# Page 2
lsp search "User" --max-items 20 --start-index 20 --pagination-id "task_123"
```
**Guideline**: Use pagination with a unique ID for common symbols to fetch results in manageable chunks. Increment `--start-index` using the same ID to browse.
### Outline: File Structure
Get hierarchical symbol structure without reading implementation.
```bash
# Get main symbols (classes, functions, methods)
lsp outline <file_path>
# Get all symbols including variables and parameters
lsp outline <file_path> --all
```
Agents SHOULD use `outline` before reading files to avoid unnecessary context consumption.
### Definition: Navigate to Source
Navigate to where symbols are defined.
```bash
# Jump to where User.get_id is defined
lsp definition models.py --scope User.get_id
# Find where an imported variable comes from
lsp definition main.py --scope 42 --find "<|>config"
# Find declaration (e.g., header files, interface declarations)
lsp definition models.py --scope 25 --mode declaration --find "<|>provider"
# Find the class definition of a variable's type
lsp definition models.py --scope 30 --find "<|>user" --mode type_definition
```
### Reference: Find All Usages
Find where symbols are used or implemented.
```bash
# Find all places where logger is referenced
lsp reference main.py --scope MyClass.run --find "<|>logger"
# Find all concrete implementations of an interface/abstract class
lsp reference api.py --scope "IDataProvider" --mode implementations
# Get more surrounding code context for each reference
lsp reference app.py --scope 10 --find "<|>my_var" --context-lines 5
# Limit results for large codebases
lsp reference utils.py --find "<|>helper" --max-items 50 --start-index 0
```
### Doc: Get Documentation
Get documentation and type information without navigating to source.
```bash
# Get docstring and type info for symbol at line 42
lsp doc main.py --scope 42
# Get API documentation for process_data function
lsp doc models.py --scope process_data
```
Agents SHOULD prefer `doc` over `read` when only documentation or type information is needed.
### Search: Global Symbol Search
Search for symbols across the workspace when location is unknown.
```bash
# Search by name (defaults to current directory)
lsp search "MyClassName"
# Search in specific project
lsp search "UserModel" --project /path/to/project
# Filter by symbol kind (can specify multiple times)
lsp search "init" --kinds function --kinds method
# Limit and paginate results for large codebases
lsp search "Config" --max-items 10
lsp search "User" --max-items 20 --start-index 0
```
Agents SHOULD use `--kinds` to filter results and reduce noise.
### Symbol: Get Complete Symbol Code
Get the full source code of the symbol containing a location.
```bash
# Get complete code of the function/class at line 15
lsp symbol main.py --scope 15
# Get full UserClass implementation
lsp symbol utils.py --scope UserClass
# Get complete method implementation
lsp symbol models.py --scope User.validate
```
Response includes: symbol name, kind (class/function/method), range, and **complete source code**.
Agents SHOULD use `symbol` to read targeted code blocks instead of using `read` on entire files.
### Refactoring Operations
Read [Refactoring Guide](references/refactor.md) for rename, extract, and other safe refactoring operations.
### Server: Manage Background Servers
The background manager starts automatically. Manual control is OPTIONAL.
```bash
# List running servers
lsp server list
# Start server for a project
lsp server start <path>
# Stop server for a project
lsp server stop <path>
# Shutdown the background manager
lsp server shutdown
```
## Best Practices
### General Workflows
#### Understanding Unfamiliar Code
The RECOMMENDED sequence for exploring new codebases:
```bash
# Step 1: Start with outline - Get file structure without reading implementation
lsp outline <file_path>
# Step 2: Inspect signatures - Use doc to understand API contracts
lsp doc <file_path> --scope <symbol_name>
# Step 3: Navigate dependencies - Follow definition chains
lsp definition <file_path> --scope <symbol_name>
# Step 4: Map usage - Find where code is called with reference
lsp reference <file_path> --scope <symbol_name>
```
#### Debugging Unknown Behavior
```bash
# Step 1: Locate symbol definition workspace-wide
lsp search "<symbol_name>"
# Step 2: Verify implementation details
lsp definition <file_path> --scope <symbol_name>
# Step 3: Trace all callers to understand invocation context
lsp reference <file_path> --scope <symbol_name>
```
### Finding Interface Implementations
```bash
# Step 1: Locate interface definition
lsp search "IUserService" --kinds interface
# Step 2: Find all implementations
lsp reference src/interfaces.py --scope IUserService --mode implementations
```
### Tracing Data Flow
```bash
# Step 1: Find where data is created
lsp search UserDTO --kinds class
# Step 2: Find where it's used
lsp reference models.py --scope UserDTO
# Step 3: Check transformations
lsp doc transform.py --scope map_to_dto
```
### Understanding Type Hierarchies
```bash
# Step 1: Get class outline
lsp outline models.py
# Step 2: Find subclasses (references to base)
lsp reference models.py --scope BaseModel
# Step 3: Check type definitions
lsp definition models.py --scope BaseModel --mode type_definition
```
### Performance Tips
```bash
# Use outline instead of reading entire files
lsp outline large_file.py # Better than: read large_file.py
# Use symbol paths for nested structures (more precise than line numbers)
lsp definition models.py --scope User.Profile.validate
# Limit results in large codebases
lsp search "User" --max-items 20
# Use doc to understand APIs without navigating to source
lsp doc api.py --scope fetch_data # Get docs/types without jumping to definition
# Verify locate strings if commands fail
lsp locate main.py --scope 42 --find "<|>my_var"
```
@@ -0,0 +1,5 @@
interface:
display_name: "LSP Code Analysis"
short_description: "Analyze code using LSP"
policy:
allow_implicit_invocation: true
@@ -0,0 +1,5 @@
interface:
display_name: "Project Context Pack"
short_description: "Provides context about the project to the agent"
policy:
allow_implicit_invocation: false
+2
View File
@@ -6,6 +6,8 @@ disable-model-invocation: true
# Setup Engineering Skills
When the provider-neutral `tracker` package is installed, generated issue-tracker guidance should call it for normal operations and retain provider-specific CLI commands only as adapter/fallback reference. See `docs/agents/tracker.md`.
Scaffold the per-repo configuration that the engineering skills assume:
- Issue tracker: where issues live (never assume a default forge)
+2 -2
View File
@@ -8,11 +8,11 @@ Architecture Decision Records live on the forge wiki and are cloned into `docs/a
<wiki-url>
```
Derived from the forge remote during `/setup-matt-pocock-skills`.
Derived from the forge remote during `/setup-skills`.
## Bootstrap
On first setup, `/setup-matt-pocock-skills` clones the wiki:
On first setup, `/setup-skills` clones the wiki:
```bash
git clone <wiki-url> docs/adr/
@@ -0,0 +1,5 @@
interface:
display_name: "Setup Engineering Skills"
short_description: "Setup engineering skills for the agent"
policy:
allow_implicit_invocation: false
@@ -1,6 +1,6 @@
# Issue tracker: Gitea
Issues and PRDs for this repo live as Gitea issues. Use the `tea` CLI for all operations.
Prefer the provider-neutral `tracker` command for normal operations; it emits JSON and delegates to `tea`. See `docs/agents/tracker.md`. The `tea` commands below remain adapter and capability reference material.
## Conventions
@@ -8,7 +8,7 @@ Issues and PRDs for this repo live as Gitea issues. Use the `tea` CLI for all op
- **Read an issue**: `tea issue <number> --comments`. Use `-o json` for machine-readable output.
- **List issues**: `tea issue list --state open -o json` with appropriate `--labels` and `--state` filters.
- **Comment on an issue**: `tea comment <number> "..."`.
- **Apply / remove labels**: `tea issue edit <number> --add-label "..."` / `--remove-label "..."`. Multiple labels can be comma-separated or by repeating the flag.
- **Apply / remove labels**: `tea issue edit <number> --add-label "..."` / `--remove-label "..."`. Multiple labels can be comma-separated or by repeating the flag. Labels need to be created first with `tea label create --name "..." --color "..."`.
- **Close**: `tea issue close <number>`. `tea issue close` does not accept a closing comment, so post the explanation first with `tea comment <number> "..."`, then close.
Infer the repo from git remote -v — `tea` does this automatically when run inside a clone.
@@ -1,6 +1,6 @@
# Issue tracker: GitHub
Issues and PRDs for this repo live as GitHub issues. Use the `gh` CLI for all operations.
Prefer the provider-neutral `tracker` command for normal operations; it emits JSON and delegates to `gh`. See `docs/agents/tracker.md`. The `gh` commands below remain adapter and capability reference material.
## Conventions
@@ -1,6 +1,6 @@
# Issue tracker: GitLab
Issues and PRDs for this repo live as GitLab issues. Use the [`glab`](https://gitlab.com/gitlab-org/cli) CLI for all operations.
Prefer the provider-neutral `tracker` command for normal operations; it emits JSON and delegates to `glab`. See `docs/agents/tracker.md`. The `glab` commands below remain adapter and capability reference material.
## Conventions
@@ -0,0 +1,5 @@
interface:
display_name: "Agent Handoff"
short_description: "Handoff the agent to a human operator"
policy:
allow_implicit_invocation: false
@@ -0,0 +1,5 @@
interface:
display_name: "Knowledge Gardener"
short_description: "Manage and curate knowledge for the agent"
policy:
allow_implicit_invocation: false
+16 -42
View File
@@ -6,52 +6,26 @@ disable-model-invocation: true
## Agent CLI Seed Data
The agent config (binary, `args` convention per agent) and field meanings are in [`agents-seed.md`](agents-seed.md). Step 2 reads it to find the calling agent's entry.
Agent config (binary, `args`, `modelflag`) is in [`agents-seed.md`](agents-seed.md). Step 2 reads it by `name`.
## Dispatch
## Process
1. **Detect agent** — Run `./detect-agent` (sibling). On success, the agent name is known. On failure (exit 1), stop.
1. **Detect the calling agent** — Run `./detect-agent` (sibling to this skill). If it exits 1 (agent unknown), report the failure and stop — the agent name is required.
2. **Look up config** — Find the agent's entry in [`agents-seed.md`](agents-seed.md) by `name`. Extract `binary`, `args`, and `modelflag`.
Completion criterion: The agent name is known and non-empty.
3. **Parse flags** — Scan user arguments for flags (before prompt text). For each flag, extract its value and remove both the flag and value from the argument list:
- `--name <title>` / `-n <title>` — tmux window title
- `-c <path>` — working directory (default: project directory)
- `--model <name>` / `-m <name>` — model for child session
2. **Look up the agent config** — Find the agent's entry in [`agents-seed.md`](agents-seed.md) by `name`. Extract its `binary` and `args` fields.
Remaining text is the prompt. If `--name` is absent, tmux auto-names the window.
Completion criterion: The agent's entry is found and its `binary` and `args` are known.
3. **Parse flags** — Scan the user's arguments for optional flags (which must come before the prompt text):
- `--name <title>` or `-n <title>` — the tmux window title. Extract the title and remove the flag and its value from the arguments list.
- `-c <path>` — the working directory for the new window. Extract the path and remove the flag and its value from the arguments list.
The remaining text after stripping both flags is the prompt for the child agent.
If `--name`/`-n` is absent, tmux auto-names the window.
If `-c` is absent, the new window inherits the current pane's working directory.
Completion criterion: The arguments are split into an optional window name, an optional directory path, and the remaining prompt text.
4. **Build the inner command** — Combine the agent config with the remaining user-supplied prompt arguments. The `args` template determines how the prompt is delivered:
- **Prompt-file agents** (`args` contains `{prompt}`) — Write the prompt text to a temporary file under `/tmp/` and substitute the file path for `{prompt}` in the args template. For example, an agent with `args: "@{prompt}"` becomes `<binary> @/tmp/tmux-launch-XXXX.md`.
- **Stdin-pipe agents** (`args` is empty) — Pipe the prompt text via `echo` into the binary.
- **No prompt** — If the user passed no arguments (after removing flags), launch the binary bare (interactive start) with no prompt file or pipe.
Completion criterion: The inner command is correctly built per the target agent's `args` convention (prompt-file, stdin-pipe, or bare).
5. **Wrap with the environment runner** — Detect whether `mise` is available via `command -v mise`:
- **mise available** — Wrap the inner command as `mise x --allow-env='*' -- <inner-command>`.
- **mise absent** — Wrap the inner command as `$SHELL -c '<inner-command>'`.
Completion criterion: A valid shell command string is ready.
6. **Fork into a new tmux window** — Build and run:
```
tmux new-window <name-flag> <dir-flag> "<shell-command>"
```
- `<shell-command>` — the wrapped command from step 5.
- `<name-flag>` — `-n "<title>"` if a window name was parsed in step 3, omitted otherwise.
- `<dir-flag>` — `-c <path>` if a directory was parsed in step 3, omitted otherwise.
Completion criterion: `tmux new-window` exits 0 and a new tmux window appears with the agent CLI session active.
4. **Build command** — Assemble the inner command:
- Start with `<binary>`.
- If model specified: append `<modelflag> <model-name>`.
- If prompt exists and `args` contains `{prompt}`: write to `/tmp/`, substitute path for `{prompt}`.
- If prompt exists and `args` is empty: pipe via `echo`.
- If no prompt: launch bare.
5. **Open window** — Call `./tmux-open <window-name> <start-dir> <inner-command>` where `<inner-command>` is the assembled command from step 4 as separate arguments. On success, a new tmux window appears with the agent session active.
@@ -7,28 +7,33 @@ agents:
- name: pi
binary: pi
args: "@{prompt}"
modelflag: "--model"
description: "My primary agent harness. Accepts prompt file via {prompt}."
- name: opencode
binary: opencode
args: ""
modelflag: "-m"
description: "OpenCode agent. Pipes stdin via cat."
note: "Uses stdin piping: args must be empty, prompt via pipe."
- name: goose
binary: goose
args: ""
modelflag: "--model"
description: "Goose agent. Accepts prompt file via -i flag."
note: "Uses stdin piping: args must be empty, prompt via pipe."
- name: codex
binary: codex
args: "@{prompt}"
modelflag: "-m"
description: "OpenAI Codex. Accepts prompt file via {prompt}."
- name: claude
binary: claude
args: ""
modelflag: "--model"
description: "Anthropic Claude CLI. Pipes stdin via cat."
note: "Uses stdin piping: args must be empty, prompt via pipe."
```
@@ -41,4 +46,5 @@ agents:
| `binary` | Command name expected on PATH |
| `args` | Static arguments appended after the binary. May include `{prompt}` which is substituted at invocation time with the absolute path to the prompt file. Empty string means stdin piping (prompt is piped via `cat`). |
| `description` | Short human-readable description for the setup menu |
| `modelflag` | CLI flag used to select a model (e.g. `--model`, `-m`). Injected into the inner command when `--model <name>` is passed by the user. |
| `note` | Optional additional context |
@@ -0,0 +1,5 @@
interface:
display_name: "Tmux Launch Agent"
short_description: "Launch an agent in a new tmux session"
policy:
allow_implicit_invocation: false
+115
View File
@@ -0,0 +1,115 @@
#!/usr/bin/env bash
set -euo pipefail
# tmux-open — open a new tmux window (or session) and run a command
# Usage: tmux-open [-k|--keep] [-h|--help] <window-name> <start-dir> <cmd> [args...]
usage() {
cat <<'EOF'
Usage: tmux-open [-k|--keep] [-h|--help] <window-name> <start-dir> <cmd> [args...]
Open a new tmux window in the current session and run a command.
If not inside tmux, create a new session named after the window and attach.
Positional arguments:
window-name Name for the new tmux window (and session, if creating one)
start-dir Working directory for the new window
cmd [args...] Command to run (variadic)
Options:
-k, --keep After the command exits, keep the pane visible (remain-on-exit)
so you can inspect output. Default: pane closes when command exits.
-h, --help Show this help message
If `mise` is on PATH, the command runs under `mise x`.
Errors:
- tmux not installed
- fewer than 3 positional args
- start-dir does not exist
- a window with the same name already exists in the current session
EOF
}
# --- Flag parsing ---
keep_shell=0
while (( $# > 0 )); do
case "$1" in
-k|--keep) keep_shell=1; shift ;;
-h|--help) usage; exit 0 ;;
--) shift; break ;;
-*) echo "error: unknown option: $1" >&2; usage >&2; exit 1 ;;
*) break ;;
esac
done
# --- Positional arg validation ---
if (( $# < 3 )); then
echo "error: expected at least 3 positional arguments (window-name, start-dir, cmd)" >&2
usage >&2
exit 1
fi
window_name="$1"; shift
start_dir="$1"; shift
cmd_args=("$@")
# --- Pre-flight checks ---
if ! command -v tmux >/dev/null 2>&1; then
echo "error: tmux is not installed or not on PATH" >&2
exit 1
fi
if [[ ! -d "$start_dir" ]]; then
echo "error: directory does not exist: $start_dir" >&2
exit 1
fi
if (( ${#cmd_args[@]} == 0 )); then
echo "error: no command specified" >&2
exit 1
fi
# Duplicate window name check (only relevant inside tmux)
if [[ -n "${TMUX:-}" ]]; then
if tmux list-windows -F '#{window_name}' | grep -Fxq "$window_name"; then
echo "error: a window named '$window_name' already exists in the current session" >&2
exit 1
fi
fi
# --- Detect mise ---
has_mise=0
if command -v mise >/dev/null 2>&1; then
has_mise=1
fi
# --- Build the command for the new window ---
# We need to construct a single shell command string for tmux.
# The command runs; pane closes on exit (or stays visible with --keep via remain-on-exit).
# Build the inner command based on mode and mise availability
# For argv mode, quote each argument
quoted_args=()
for arg in "${cmd_args[@]}"; do
quoted_args+=("$(printf '%q' "$arg")")
done
if (( has_mise )); then
inner_cmd="mise x -- ${quoted_args[*]}"
else
inner_cmd="${quoted_args[*]}"
fi
# --- Create window or session ---
if [[ -n "${TMUX:-}" ]]; then
tmux new-window -n "$window_name" -c "$start_dir" "$inner_cmd"
if (( keep_shell )); then
tmux set-option remain-on-exit on
fi
else
tmux new-session -d -n "$window_name" -s "$window_name" -c "$start_dir" "$inner_cmd"
if (( keep_shell )); then
tmux set-option -t "$window_name" remain-on-exit on
fi
tmux attach-session -t "$window_name"
fi
+2 -3
View File
@@ -1,6 +1,5 @@
# Personal Skills
_No skills currently live in this bucket._
Tied to my own setup, not promoted.
- `pkm-curation` has been moved to [pkm/pkm-curation](../pkm/pkm-curation/SKILL.md).
- `forge-preferences` has been moved to [deprecated/forge-preferences](../deprecated/forge-preferences/SKILL.md).
_No skills currently live in this bucket._
+5 -3
View File
@@ -1,12 +1,14 @@
# PKM Skills
Personal knowledge management.
## User-invoked
- [conversation-summary](conversation-summary/SKILL.md) — Save the current conversation as a comprehensive report note in your Obsidian vault, following OKF v0.1 conventions.
- [crit](crit/SKILL.md) — Brainstorm with AI using the CRIT framework to generate and evaluate ideas.
- [crit](crit/SKILL.md) — Run the CRIT framework — give the AI Context, assign it a Role, let it Interview you one question at a time, then issue the Task.
- [research-vault](research-vault/SKILL.md) — Research a topic through a one-question-at-a-time learning conversation, answer directly, share resources when useful, and save a linked OKF-conformant research packet in the Obsidian vault.
- [youtube-video-capture](youtube-video-capture/SKILL.md) — Fetch subtitles from a YouTube video, summarize the content, and save both the summary and raw subtitles to the Video bundle in the Obsidian vault.
## Model-invoked
- [pkm-curation](pkm-curation/SKILL.md) — Curate an Obsidian vault — classify notes, normalize frontmatter, add wikilinks, extract atomic notes. Use when curating, batch-processing, reviewing, or doing a serendipity pick.
- [youtube-video-capture](youtube-video-capture/SKILL.md) — Fetch subtitles from a YouTube video, summarize the content, and save the summary and raw subtitles to the Video bundle.
- [pkm-curation](pkm-curation/SKILL.md) — Curate an Obsidian vault — classify notes, normalize frontmatter, add links, extract atomic notes. Use when curating, batch-processing, reviewing, or doing a serendipity pick.
@@ -0,0 +1,5 @@
interface:
display_name: "Conversation Summary"
short_description: "Summarize the conversation"
policy:
allow_implicit_invocation: false
+38 -61
View File
@@ -1,90 +1,67 @@
---
name: crit
description: brainstorm with AI using the CRIT framework to generate and evaluate ideas.
description: Run the CRIT framework — give the AI Context, assign it a Role, let it Interview you one question at a time, then issue the Task.
disable-model-invocation: true
---
## Purpose
This skill implements the CRIT (Context-Request-Ideation-Tone) framework for structured brainstorming and idea evaluation.
CRIT is a four-step prompt framework by Geoff Woods: **Context, Role, Interview,
Task**. The sequence front-loads thinking before execution — the AI learns your
world, takes a specific lens, interviews you to surface what matters, then acts.
## Core Workflow
The insight: most people skip to Task and get generic output. The Interview
step — one question at a time, max three — is where the signal lives.
### Step 1: Context Analysis
Before generating ideas, establish the foundation:
## Steps
1. **Identify the Core Domain**
- What field, industry, or subject area?
- What are the key constraints (time, resources, technical limitations)?
- Who is the target audience and their expertise level?
Run these four steps in order when the user invokes `/crit`.
2. **Assess Current State**
- What problems or opportunities exist?
- What has been tried before (if applicable)?
- What resources are available?
### 1. Context — Give the AI your world
**Completion Criterion**: Domain, constraints, and current state captured in a compact summary (3-5 sentences).
Ask the user: "What should I know about you, your goals, your audience, and any
constraints?"
### Step 2: Request Clarification
Structure the brainstorming request:
Capture the answer in one paragraph. More detail is better.
1. **Define the Specific Goal**
- What concrete outcome do you want?
- What success criteria will be used?
- What is the expected timeline?
**Completion criterion**: One paragraph covering identity, goal, audience, and
constraints — confirmed by the user.
2. **Gather Input Requirements**
- What information is needed to proceed?
- What assumptions should be validated?
- What data or resources are required?
### 2. Role — Assign a viewpoint
**Completion Criterion**: Goal and input requirements captured as 1-3 concrete statements, each with a checkable success criterion.
Ask the user: "What role should I take?"
### Step 3: Ideation
Generate diverse, high-quality ideas:
Guide toward a specific lens — "strategy coach who uncovers blind spots,"
"editor who cuts fluff," "architect who finds leverage points." Not "be
helpful."
1. **Divergent Thinking Phase**
- Generate 5-10 initial concepts without judgment
- Apply different perspectives (technical, business, user experience)
- Include both obvious and unconventional options
**Completion criterion**: A single sentence assigning a named role that implies
a specific viewpoint.
2. **Convergent Analysis Phase**
- Evaluate each idea against success criteria
- Score ideas on feasibility, impact, and alignment
- Identify patterns and synergies between ideas
### 3. Interview — One question at a time
**Completion Criterion**: Minimum 5 distinct ideas generated and evaluated with scores.
Instruct yourself: "Ask me no more than three questions, one at a time, to
clarify what I'm trying to achieve."
### Step 4: Iterative Refinement
Improve selected ideas:
Ask one question. Wait for the answer. Then ask the next. Max three. Do not
batch them.
1. **Select Top Candidates**
- Choose 2-3 ideas with highest potential
- Detail implementation approach for each
- Identify risks and mitigation strategies
This step forces the user to slow down and think, and teaches the AI what
actually matters.
2. **Develop Action Plans**
- Break down into concrete steps
- Assign priorities and dependencies
- Define success metrics and checkpoints
**Completion criterion**: 1-3 questions asked and answered, one at a time.
Stop asking when the user signals readiness or you've asked three.
**Completion Criterion**: 2-3 refined ideas with detailed action plans.
### 4. Task — Issue the assignment
### Step 5: Tone & Delivery
Adapt communication to the audience:
Ask the user: "What's the task?"
1. **Choose Appropriate Role**
- Subject Matter Expert for technical depth
- Consultant for strategic guidance
- Teacher for complex concepts
- Collaborator for co-creation
- Analyst for multi-perspective evaluation
Guide toward a short, clear, slightly uncomfortable prompt that asks the AI to
*think*, not just write. Reference the preceding interview.
2. **Structure Response**
- Lead with clear, actionable solutions
- Organize information logically and concisely
- Include examples or analogies for clarity
- Suggest next steps and follow-up questions
> "Based on our conversation, give me three non-obvious actions I can take.
> Make them surprising but realistic."
**Completion Criterion**: Response delivered in chosen role with sections clearly labeled (Context, Request, Ideation, Refinement, Next Steps).
Execute the task.
**Completion criterion**: Task executed and result delivered to the user.
+5
View File
@@ -0,0 +1,5 @@
interface:
display_name: "Context Role Interview Task"
short_description: "CRIT framework is a structured prompting and interaction methodology"
policy:
allow_implicit_invocation: false
@@ -0,0 +1,5 @@
interface:
display_name: "Personal Knowledge Management Curation"
short_description: "Curate and manage personal knowledge for the agent"
policy:
allow_implicit_invocation: false
@@ -0,0 +1,5 @@
interface:
display_name: "Research Vault"
short_description: "Store and retrieve research notes and documents"
policy:
allow_implicit_invocation: false
@@ -0,0 +1,5 @@
interface:
display_name: "Youtube Video Capture"
short_description: "Capture a youtube video"
policy:
allow_implicit_invocation: false
+2
View File
@@ -1,3 +1,5 @@
# Productivity Skills
Daily non-code workflow tools.
_No skills currently live in this bucket._
+7 -7
View File
@@ -1,6 +1,6 @@
# Issue tracker: Gitea
# Issue tracker: provider-neutral tracker over Gitea
Issues and PRDs for this repo live as Gitea issues. Use the `tea` CLI for all operations.
Use `tracker` (see `docs/agents/tracker.md`) for normal issue and pull-request automation. It delegates to `tea` and emits one JSON envelope. The Gitea command details below remain capability and fallback reference material; do not issue exploratory raw commands when a tracker operation exists.
## Conventions
@@ -15,12 +15,12 @@ Infer the repo from git remote -v — `tea` does this automatically when run ins
## Pull requests as a triage surface
**PRs as a request surface: no.**
**PRs as a request surface: no.** _(Set to `yes` if this repo treats external PRs as feature requests; `/triage` reads this flag.)_
When set to `yes`, PRs run through the same labels and states as issues, using the `tea pr` equivalents:
- **Read a PR**: `tea pr <number> --comments` and `tea api /repos/{owner}/{repo}/pulls/<number>.diff` for the diff.
- **List external PRs for triage**: `tea pr list --state open -o json` then keep only PRs whose author is not a project member/owner.
- **List external PRs for triage**: `tea pr list --state open -o json` then keep only PRs whose author is not a project member/owner (a contributor's MR, not a maintainer's in-flight work).
- **Comment / label / close**: `tea comment <number> "..."`, `tea pr edit --add-label`/`--remove-label`, `tea pr close`.
Gitea shares one number space across issues and PRs, so a bare `#42` may be either — resolve with `tea pr 42` and fall back to `tea issue 42`.
@@ -39,7 +39,7 @@ Used by `/wayfinder`. The **map** is a single issue with **child** issues as tic
- **Map**: a single issue labelled `wayfinder:map`, holding the Notes / Decisions-so-far / Fog body. `tea issue create --label wayfinder:map`.
- **Child ticket**: an issue linked to the map as a GitHub sub-issue (`tea api` on the sub-issues endpoint). Where sub-issues aren't enabled, add the child to a task list in the map body and put `Part of #<map>` at the top of the child body. Labels: `wayfinder:<type>` (`research`/`prototype`/`grilling`/`task`). Once claimed, the ticket is assigned to the driving dev.
- **Blocking**: GitHub's **native issue dependencies** — the canonical, UI-visible representation. Add an edge with `tea api --method POST /repos/{owner}/{repo}/issues/<child>/dependencies -F index=<blocker-issue-number> -F repo=<blocker-repo-name> -F owner=<bocker-owner-name>`, where `<blocker-usse-number>` is the blocker's numeric **issue number** (`tea api repos/{owner}/{repo}/issues/<n> --jq ".number. .repository.name, .repository.owner"`, where `.number` is the `<blocker-issue-number>`, `.repository.name` is the `<blocker-repo-name>` and `.repository.owner` is the `<blocker-repo-owner>`. Where dependencies aren't available, fall back to a `Blocked by: #<n>, #<n>` line at the top of the child body. A ticket is unblocked when every blocker is closed.
- **Frontier query**: list the map's open dependencies (`tea api /repos/{owner}/{repo}/issues/<n>/dependencies | jq '.[] | select(.state = "open") .number'`, scoped to the map's sub-issues / task list), drop any with an open blocker (`list of dependencies is not empty`, or an open issue in the `Blocked by` line) or an assignee; first in map order wins.
- **Blocking**: GitHub's **native issue dependencies** — the canonical, UI-visible representation. Add an edge with `tea api --method POST /repos/{owner}/{repo}/issues/<child>/dependencies -F index=<blocker-issue-number> -F repo=<blocker-repo-name> -F owner=<bocker-owner-name>`, where `<blocker-issue-number>` is the blocker's numeric **issue number** (`tea api repos/{owner}/{repo}/issues/<n> --jq ".number. .repository.name, .repository.owner"`, where `.number` is the `<blocker-issue-number>`, `.repository.name` is the `<blocker-repo-name>` and `.repository.owner` is the `<blocker-repo-owner>`. Where dependencies aren't available, fall back to a `Blocked by: #<n>, #<n>` line at the top of the child body. A ticket is unblocked when every blocker is closed.
- **Frontier query**: list the map's open dependencies (`tea api /repos/{owner}/{repo}/issues/<n>/dependencies | jq '.[] | select(.state == "open") | .number'`, scoped to the map's sub-issues / task list), drop any with an open blocker (`list of dependencies is not empty`, or an open issue in the `Blocked by` line) or an assignee; first in map order wins.
- **Claim**: `tea issue edit <n> --add-assignees @me` — the session's first write.
- **Resolve**: `tea comments <n> "<answer>"`, then `tea issue close <n>`, then append a context pointer (gist + link) to the map's Decisions-so-far.
- **Resolve**: `tea comment <n> "<answer>"`, then `tea issue close <n>`, then append a context pointer (gist + link) to the map's Decisions-so-far.
+15
View File
@@ -0,0 +1,15 @@
# Provider-neutral tracker automation
Use `tracker` for normal issue and pull-request automation. It owns the high-level operation, prerequisite checks, normalization, bounded retry policy, and JSON protocol; it delegates credentials and provider commands to `gh`, `glab`, or `tea`.
```sh
tracker --provider gitea issue get 16
TRACKER_PROVIDER=gitlab tracker issue list --state open --label ready-for-agent
tracker pr get 42 --diff
```
Provider selection is explicit CLI flag, then `TRACKER_PROVIDER`, then the `origin` remote. Always choose `issue` or `pr` explicitly for reads and writes. Use `resolve-reference` only for an intentionally ambiguous bare number.
Parse `ok` and `error.code`; do not parse provider output or issue exploratory retries. The library (`from tracker import Tracker`) returns the same envelope as the CLI. `RecordingRunner` provides the fake subprocess seam for tests.
Provider-specific capability and fallback references remain in `docs/agents/issue-tracker.md` and the setup templates. They are not the normal execution path. Wayfinding relationships may use native provider APIs where available and task-list/body or note fallbacks otherwise; the result's `details` identifies the relationship mode.
+2 -2
View File
@@ -3,10 +3,10 @@
The skills speak in terms of seven canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker.
| Label in skills | Label in our tracker | Meaning |
| -------------------------- | -------------------- | ---------------------------------------- |
| ----------------- | -------------------- | ---------------------------------------- |
| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
| `needs-info` | `needs-info` | Waiting on reporter for more information |
| `needs-review` | `needs-review` | Waiting for reviewed by a human |
| `needs-review` | `needs-review` | Waiting for review by a human |
| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
| `ready-for-human` | `ready-for-human` | Requires human implementation |
| `in-progress` | `in-progress` | Being actively worked on by a human or agents |
+24
View File
@@ -0,0 +1,24 @@
[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"
[project]
name = "tracker-automation"
version = "0.1.0"
description = "Provider-neutral tracker automation CLI and library"
requires-python = ">=3.10"
[project.scripts]
tracker = "tracker.cli:main"
tracker-automation = "tracker.cli:main"
[tool.setuptools.packages.find]
include = ["tracker*", "tracker_automation*"]
[tool.pyright]
include = ["tracker", "tests"]
extraPaths = ["."]
[tool.ruff]
target-version = "py310"
line-length = 100
View File
+125
View File
@@ -0,0 +1,125 @@
import unittest
from tracker import CompletedCommand, RecordingRunner, RetryPolicy, Tracker # type: ignore[reportMissingImports]
class TrackerOperationTests(unittest.TestCase):
def test_add_label_ensures_missing_label_before_assignment(self):
runner = RecordingRunner(
[
CompletedCommand("[]"),
CompletedCommand('{"name":"ready","color":"ededed"}'),
CompletedCommand('{"number":7,"labels":[{"name":"ready"}]}'),
]
)
result = Tracker(provider="github", runner=runner).add_label("issue", 7, "ready")
self.assertTrue(result["ok"])
self.assertEqual([call[0][:3] for call in runner.calls], [["gh", "label", "list"], ["gh", "label", "create"], ["gh", "issue", "edit"]])
self.assertEqual(result["details"]["ensured"]["created"], True)
def test_existing_label_is_not_recreated(self):
runner = RecordingRunner(
[
CompletedCommand('[{"name":"ready","color":"ff0000"}]'),
CompletedCommand('{"number":7,"labels":[{"name":"ready"}]}'),
]
)
result = Tracker(provider="gitea", runner=runner).add_label("issue", 7, "ready")
self.assertTrue(result["ok"])
self.assertEqual(len(runner.calls), 2)
self.assertEqual(result["details"]["ensured"]["created"], False)
def test_transient_failure_is_retried_and_normalized(self):
runner = RecordingRunner(
[
CompletedCommand("", "connection reset", 1),
CompletedCommand('{"number":4,"iid":4,"title":"Fix","description":"body","state":"opened","labels":[]}'),
]
)
result = Tracker(provider="gitlab", runner=runner, retry=RetryPolicy(attempts=2)).get_issue(4)
self.assertTrue(result["ok"])
self.assertEqual(len(runner.calls), 2)
self.assertEqual(result["result"]["number"], 4)
self.assertEqual(result["result"]["body"], "body")
def test_external_pr_filter_normalizes_github_associations(self):
runner = RecordingRunner([
CompletedCommand('[{"number":1,"title":"inside","authorAssociation":"MEMBER"},{"number":2,"title":"outside","authorAssociation":"NONE"}]')
])
result = Tracker(provider="github", runner=runner).list_external_prs()
self.assertTrue(result["ok"])
self.assertEqual([item["number"] for item in result["result"]], [2])
def test_external_filter_reports_missing_membership_metadata(self):
runner = RecordingRunner([CompletedCommand('[{"number":1,"title":"unknown"}]')])
result = Tracker(provider="gitea", runner=runner).list_external_prs()
self.assertFalse(result["ok"])
self.assertEqual(result["error"]["code"], "unsupported_capability")
def test_pr_get_can_include_diff(self):
runner = RecordingRunner([
CompletedCommand('{"number":3,"title":"Change","state":"open"}'),
CompletedCommand("diff --git a/a b/a"),
])
result = Tracker(provider="gitlab", runner=runner).get_pr(3, diff=True)
self.assertTrue(result["ok"])
self.assertEqual(result["result"]["diff"], "diff --git a/a b/a")
def test_child_creation_links_native_and_updates_map_order(self):
runner = RecordingRunner([
CompletedCommand("[]"),
CompletedCommand("{}"),
CompletedCommand('{"number":7,"title":"Research"}'),
CompletedCommand("{}"),
CompletedCommand('{"number":9,"body":"Notes"}'),
CompletedCommand("{}"),
])
result = Tracker(provider="github", runner=runner).create_child(9, "Research", wayfinder_type="research")
self.assertTrue(result["ok"])
self.assertEqual(result["result"]["number"], 7)
self.assertEqual(result["details"]["relationship"], "native")
self.assertTrue(result["details"]["map_updated"])
self.assertTrue(any("sub_issues" in part for part in runner.calls[3][0]))
def test_resolve_reports_completed_steps_after_partial_failure(self):
runner = RecordingRunner([
CompletedCommand("{}"),
CompletedCommand("", "connection reset", 1),
])
result = Tracker(provider="gitea", runner=runner, retry=RetryPolicy(attempts=1)).resolve("issue", 7, "answer")
self.assertFalse(result["ok"])
self.assertEqual(result["error"]["code"], "partial_failure")
self.assertEqual(result["error"]["details"]["completed"], ["comment"])
def test_frontier_uses_map_task_order_and_filters_closed_or_claimed(self):
runner = RecordingRunner([
CompletedCommand('{"number":9,"body":"- [ ] #2 second\\n- [ ] #1 first","state":"open"}'),
CompletedCommand('{"number":2,"title":"second","state":"open","assignees":[]}'),
CompletedCommand('{"number":1,"title":"first","state":"closed","assignees":[]}'),
])
result = Tracker(provider="github", runner=runner).frontier(9)
self.assertTrue(result["ok"])
self.assertEqual([item["number"] for item in result["result"]], [2])
self.assertTrue(result["details"]["deterministic"])
def test_authentication_failure_is_not_retryable(self):
runner = RecordingRunner([CompletedCommand("", "not logged in", 1)])
result = Tracker(provider="github", runner=runner).get_issue(4)
self.assertFalse(result["ok"])
self.assertEqual(result["error"]["code"], "auth_required")
self.assertFalse(result["error"]["retryable"])
self.assertEqual(len(runner.calls), 1)
if __name__ == "__main__":
unittest.main()
+37
View File
@@ -0,0 +1,37 @@
import unittest
from tracker import TrackerError, resolve_provider # type: ignore[reportMissingImports]
class ProviderResolutionTests(unittest.TestCase):
def test_cli_provider_wins_over_environment_and_remote(self):
self.assertEqual(
resolve_provider(
explicit="github",
env={"TRACKER_PROVIDER": "gitlab"},
remote="https://gitea.example.com/team/repo.git",
),
"github",
)
def test_environment_provider_wins_over_remote(self):
self.assertEqual(
resolve_provider(
env={"TRACKER_PROVIDER": "gitlab"},
remote="git@github.com:team/repo.git",
),
"gitlab",
)
def test_remote_provider_is_detected(self):
self.assertEqual(resolve_provider(remote="git@gitea.example.com:team/repo.git"), "gitea")
def test_unsupported_remote_is_structured_error(self):
with self.assertRaises(TrackerError) as context:
resolve_provider(remote="https://example.com/team/repo.git")
self.assertEqual(context.exception.code, "provider_detection_failed")
self.assertFalse(context.exception.retryable)
if __name__ == "__main__":
unittest.main()
+30
View File
@@ -0,0 +1,30 @@
# Tracker automation
`tracker` is the provider-neutral execution seam for issue-tracker skills. It emits one JSON envelope on stdout and delegates authentication and repository work to `gh`, `glab`, or `tea`.
## Use
```sh
python -m tracker --provider gitea issue get 16
TRACKER_PROVIDER=github tracker issue list --state open --label ready-for-agent
tracker pr get 42 --diff
```
Provider precedence is `--provider`, `TRACKER_PROVIDER`, then the `origin` Git remote. Explicit resource commands (`issue` and `pr`) avoid shared-number ambiguity. Use `resolve-reference` only when intentional resolution of a bare number is required.
Stable failures are returned as:
```json
{"ok":false,"provider":"gitea","operation":"issue.get","error":{"code":"auth_required","message":"...","retryable":false,"provider":"gitea","operation":"issue.get","details":{}}}
```
The Python API is the same seam as the CLI:
```python
from tracker import Tracker
tracker = Tracker(provider="gitea")
result = tracker.add_label("issue", 16, "needs-review")
```
Inject `RecordingRunner` or another object with `run(argv, **kwargs)` for deterministic contract tests. Provider-specific capability gaps are explicit in `error.code` or `details`; credentials are never accepted or stored by this package.
+21
View File
@@ -0,0 +1,21 @@
"""Provider-neutral issue tracker automation library."""
# pi-lens-ignore: reportMissingImports
from .detection import resolve_provider # type: ignore[reportMissingImports]
# pi-lens-ignore: reportMissingImports
from .errors import TrackerError # type: ignore[reportMissingImports]
from .models import CompletedCommand, Envelope, ResourceRef, RetryPolicy # type: ignore[reportMissingImports]
from .runner import RecordingRunner, SubprocessRunner # type: ignore[reportMissingImports]
from .service import Tracker # type: ignore[reportMissingImports]
__all__ = [
"CompletedCommand",
"Envelope",
"RecordingRunner",
"ResourceRef",
"RetryPolicy",
"SubprocessRunner",
"Tracker",
"TrackerError",
"resolve_provider",
]
+5
View File
@@ -0,0 +1,5 @@
from .cli import main # type: ignore[reportMissingImports]
if __name__ == "__main__":
raise SystemExit(main())
+328
View File
@@ -0,0 +1,328 @@
import json
from .models import normalize_resource # type: ignore[reportMissingImports]
JSON_FIELDS = "number,title,body,state,labels,comments,author,assignees,url,createdAt,updatedAt"
class Adapter:
provider = ""
executable = ""
def __init__(self, repo=None):
self.repo = repo
def _repo_args(self):
return ["--repo", self.repo] if self.repo else []
def command(self, operation, **kwargs):
method = getattr(self, f"command_{operation.replace('.', '_')}")
return method(**kwargs)
def normalize(self, value, kind):
return normalize_resource(value, kind=kind, provider=self.provider)
def json_value(self, stdout):
text = (stdout or "").strip()
if not text:
return {}
try:
return json.loads(text)
except (TypeError, ValueError):
return {"output": text}
class GitHubAdapter(Adapter):
provider = "github"
executable = "gh"
def command_issue_create(self, title, body, labels, assignees):
args = ["gh", "issue", "create", "--title", title, "--body", body]
for label in labels:
args += ["--label", label]
for user in assignees:
args += ["--assignee", user]
return args + self._repo_args()
def command_pr_create(self, title, body, head, base, labels, assignees):
args = ["gh", "pr", "create", "--title", title, "--body", body]
if head:
args += ["--head", head]
if base:
args += ["--base", base]
for label in labels:
args += ["--label", label]
for user in assignees:
args += ["--assignee", user]
return args + self._repo_args()
def _view(self, kind, number, comments=True):
command = "pr" if kind == "pr" else "issue"
args = ["gh", command, "view", str(number)]
if comments:
args.append("--comments")
return args + ["--json", JSON_FIELDS] + self._repo_args()
def command_issue_get(self, number, comments=True):
return self._view("issue", number, comments)
def command_pr_get(self, number, comments=True):
return self._view("pr", number, comments)
def command_issue_list(self, state, labels, limit):
args = ["gh", "issue", "list", "--state", state, "--limit", str(limit)]
for label in labels:
args += ["--label", label]
return args + ["--json", JSON_FIELDS] + self._repo_args()
def command_pr_list(self, state, limit):
return ["gh", "pr", "list", "--state", state, "--limit", str(limit), "--json", JSON_FIELDS] + self._repo_args()
def _edit(self, kind, number, title=None, body=None, add_label=None, remove_label=None, assignee=None, unassign=None):
command = "pr" if kind == "pr" else "issue"
args = ["gh", command, "edit", str(number)]
if title is not None:
args += ["--title", title]
if body is not None:
args += ["--body", body]
if add_label:
args += ["--add-label", add_label]
if remove_label:
args += ["--remove-label", remove_label]
if assignee:
args += ["--add-assignee", assignee]
if unassign:
args += ["--remove-assignee", unassign]
return args + self._repo_args()
def command_issue_edit(self, **kwargs):
return self._edit("issue", **kwargs)
def command_pr_edit(self, **kwargs):
return self._edit("pr", **kwargs)
def command_issue_comment(self, number, body):
return ["gh", "issue", "comment", str(number), "--body", body] + self._repo_args()
def command_pr_comment(self, number, body):
return ["gh", "pr", "comment", str(number), "--body", body] + self._repo_args()
def command_issue_close(self, number):
return ["gh", "issue", "close", str(number)] + self._repo_args()
def command_pr_close(self, number):
return ["gh", "pr", "close", str(number)] + self._repo_args()
def command_pr_diff(self, number):
return ["gh", "pr", "diff", str(number)] + self._repo_args()
def command_label_list(self):
return ["gh", "label", "list", "--limit", "1000", "--json", "name,color,description"] + self._repo_args()
def command_label_create(self, name, color, description):
args = ["gh", "label", "create", name, "--color", color]
if description:
args += ["--description", description]
return args + self._repo_args()
def command_issue_resolve(self, number):
return self.command_issue_get(number)
class GitLabAdapter(Adapter):
provider = "gitlab"
executable = "glab"
def _format(self):
return ["-F", "json"]
def _surface(self, kind):
return "mr" if kind == "pr" else "issue"
def command_issue_create(self, title, body, labels, assignees):
args = ["glab", "issue", "create", "--title", title, "--description", body]
if labels:
args += ["--label", ",".join(labels)]
if assignees:
args += ["--assignee", ",".join(assignees)]
return args + self._repo_args()
def command_pr_create(self, title, body, head, base, labels, assignees):
args = ["glab", "mr", "create", "--title", title, "--description", body]
if head:
args += ["--source-branch", head]
if base:
args += ["--target-branch", base]
if labels:
args += ["--label", ",".join(labels)]
if assignees:
args += ["--assignee", ",".join(assignees)]
return args + self._repo_args()
def command_issue_get(self, number, comments=True):
args = ["glab", "issue", "view", str(number)]
if comments:
args.append("--comments")
return args + self._format() + self._repo_args()
def command_pr_get(self, number, comments=True):
args = ["glab", "mr", "view", str(number)]
if comments:
args.append("--comments")
return args + self._format() + self._repo_args()
def command_issue_list(self, state, labels, limit):
args = ["glab", "issue", "list", "--state", state, "--per-page", str(limit)]
if labels:
args += ["--label", ",".join(labels)]
return args + self._format() + self._repo_args()
def command_pr_list(self, state, limit):
return ["glab", "mr", "list", "--state", state, "--per-page", str(limit)] + self._format() + self._repo_args()
def _edit(self, kind, number, title=None, body=None, add_label=None, remove_label=None, assignee=None, unassign=None):
args = ["glab", self._surface(kind), "update", str(number)]
if title is not None:
args += ["--title", title]
if body is not None:
args += ["--description", body]
if add_label:
args += ["--label", add_label]
if remove_label:
args += ["--unlabel", remove_label]
if assignee:
args += ["--assignee", assignee]
if unassign:
args += ["--unassign", unassign]
return args + self._repo_args()
def command_issue_edit(self, **kwargs):
return self._edit("issue", **kwargs)
def command_pr_edit(self, **kwargs):
return self._edit("pr", **kwargs)
def command_issue_comment(self, number, body):
return ["glab", "issue", "note", str(number), "--message", body] + self._repo_args()
def command_pr_comment(self, number, body):
return ["glab", "mr", "note", str(number), "--message", body] + self._repo_args()
def command_issue_close(self, number):
return ["glab", "issue", "close", str(number)] + self._repo_args()
def command_pr_close(self, number):
return ["glab", "mr", "close", str(number)] + self._repo_args()
def command_pr_diff(self, number):
return ["glab", "mr", "diff", str(number)] + self._repo_args()
def command_label_list(self):
return ["glab", "label", "list"] + self._format() + self._repo_args()
def command_label_create(self, name, color, description):
args = ["glab", "label", "create", name, "--color", color]
if description:
args += ["--description", description]
return args + self._repo_args()
class GiteaAdapter(Adapter):
provider = "gitea"
executable = "tea"
def _format(self):
return ["-o", "json"]
def _surface(self, kind):
return "pr" if kind == "pr" else "issue"
def command_issue_create(self, title, body, labels, assignees):
args = ["tea", "issue", "create", "--title", title, "--description", body]
if labels:
args += ["--labels", ",".join(labels)]
if assignees:
args += ["--assignees", ",".join(assignees)]
return args + self._repo_args()
def command_pr_create(self, title, body, head, base, labels, assignees):
args = ["tea", "pr", "create", "--title", title, "--description", body]
if head:
args += ["--head", head]
if base:
args += ["--base", base]
if labels:
args += ["--labels", ",".join(labels)]
if assignees:
args += ["--assignees", ",".join(assignees)]
return args + self._repo_args()
def _view(self, kind, number, comments=True):
args = ["tea", self._surface(kind), str(number)]
if comments:
args.append("--comments")
return args + self._format() + self._repo_args()
def command_issue_get(self, number, comments=True):
return self._view("issue", number, comments)
def command_pr_get(self, number, comments=True):
return self._view("pr", number, comments)
def command_issue_list(self, state, labels, limit):
args = ["tea", "issue", "list", "--state", state, "--limit", str(limit)]
if labels:
args += ["--labels", ",".join(labels)]
return args + self._format() + self._repo_args()
def command_pr_list(self, state, limit):
return ["tea", "pr", "list", "--state", state, "--limit", str(limit)] + self._format() + self._repo_args()
def _edit(self, kind, number, title=None, body=None, add_label=None, remove_label=None, assignee=None, unassign=None):
args = ["tea", self._surface(kind), "edit", str(number)]
if title is not None:
args += ["--title", title]
if body is not None:
args += ["--description", body]
if add_label:
args += ["--add-label", add_label]
if remove_label:
args += ["--remove-label", remove_label]
if assignee:
args += ["--add-assignee", assignee]
if unassign:
args += ["--remove-assignee", unassign]
return args + self._repo_args()
def command_issue_edit(self, **kwargs):
return self._edit("issue", **kwargs)
def command_pr_edit(self, **kwargs):
return self._edit("pr", **kwargs)
def command_issue_comment(self, number, body):
return ["tea", "comment", str(number), body] + self._repo_args()
def command_pr_comment(self, number, body):
return ["tea", "comment", str(number), body] + self._repo_args()
def command_issue_close(self, number):
return ["tea", "issue", "close", str(number)] + self._repo_args()
def command_pr_close(self, number):
return ["tea", "pr", "close", str(number)] + self._repo_args()
def command_pr_diff(self, number):
return ["tea", "api", f"/repos/{{owner}}/{{repo}}/pulls/{number}.diff"] + self._repo_args()
def command_label_list(self):
return ["tea", "label", "list"] + self._format() + self._repo_args()
def command_label_create(self, name, color, description):
args = ["tea", "label", "create", "--name", name, "--color", color]
if description:
args += ["--description", description]
return args + self._repo_args()
ADAPTERS = {"github": GitHubAdapter, "gitlab": GitLabAdapter, "gitea": GiteaAdapter}
+234
View File
@@ -0,0 +1,234 @@
import argparse
import json
import sys
from .errors import TrackerError # type: ignore[reportMissingImports]
from .models import Envelope, RetryPolicy # type: ignore[reportMissingImports]
from .service import Tracker # type: ignore[reportMissingImports]
class JsonArgumentParser(argparse.ArgumentParser):
def error(self, message):
raise TrackerError("invalid_input", message)
def _parse_attempts(value):
try:
return int(value)
except (TypeError, ValueError) as error:
raise TrackerError("invalid_input", f"invalid retry attempt count: {value}") from error
def _take_global_options(argv):
remaining = []
provider = repo = None
attempts = 3
index = 0
while index < len(argv):
item = argv[index]
if item == "--provider" and index + 1 < len(argv):
provider = argv[index + 1]
index += 2
elif item.startswith("--provider="):
provider = item.split("=", 1)[1]
index += 1
elif item == "--repo" and index + 1 < len(argv):
repo = argv[index + 1]
index += 2
elif item.startswith("--repo="):
repo = item.split("=", 1)[1]
index += 1
elif item == "--retry-attempts" and index + 1 < len(argv):
attempts = _parse_attempts(argv[index + 1])
index += 2
elif item.startswith("--retry-attempts="):
attempts = _parse_attempts(item.split("=", 1)[1])
index += 1
else:
remaining.append(item)
index += 1
return remaining, provider, repo, attempts
def _add_resource_commands(subparsers, kind):
resource = subparsers.add_parser(kind)
commands = resource.add_subparsers(dest="action", required=True)
create = commands.add_parser("create")
create.add_argument("--title", required=True)
create.add_argument("--body", default="")
create.add_argument("--label", action="append", default=[])
create.add_argument("--assignee", action="append", default=[])
if kind == "pr":
create.add_argument("--head")
create.add_argument("--base")
get = commands.add_parser("get")
get.add_argument("number", type=int)
get.add_argument("--no-comments", action="store_true")
if kind == "pr":
get.add_argument("--diff", action="store_true")
listing = commands.add_parser("list")
listing.add_argument("--state", default="open")
listing.add_argument("--label", action="append", default=[])
listing.add_argument("--limit", type=int, default=100)
if kind == "pr":
listing.add_argument("--external-only", action="store_true")
comment = commands.add_parser("comment")
comment.add_argument("number", type=int)
comment.add_argument("--body", required=True)
edit = commands.add_parser("edit")
edit.add_argument("number", type=int)
edit.add_argument("--title")
edit.add_argument("--body")
assign = commands.add_parser("assign")
assign.add_argument("number", type=int)
assign.add_argument("--user", required=True)
close = commands.add_parser("close")
close.add_argument("number", type=int)
close.add_argument("--explanation")
if kind == "pr":
diff = commands.add_parser("diff")
diff.add_argument("number", type=int)
return resource
def build_parser():
parser = JsonArgumentParser(prog="tracker")
commands = parser.add_subparsers(dest="resource", required=True)
_add_resource_commands(commands, "issue")
_add_resource_commands(commands, "pr")
labels = commands.add_parser("label")
label_commands = labels.add_subparsers(dest="action", required=True)
ensure = label_commands.add_parser("ensure")
ensure.add_argument("name")
ensure.add_argument("--color", default="ededed")
ensure.add_argument("--description")
add = label_commands.add_parser("add")
add.add_argument("kind", choices=("issue", "pr"))
add.add_argument("number", type=int)
add.add_argument("name")
remove = label_commands.add_parser("remove")
remove.add_argument("kind", choices=("issue", "pr"))
remove.add_argument("number", type=int)
remove.add_argument("name")
map_parser = commands.add_parser("map")
map_commands = map_parser.add_subparsers(dest="action", required=True)
map_create = map_commands.add_parser("create")
map_create.add_argument("--title", required=True)
map_create.add_argument("--body", default="")
map_create.add_argument("--label", action="append", default=[])
child = commands.add_parser("child")
child_commands = child.add_subparsers(dest="action", required=True)
child_create = child_commands.add_parser("create")
child_create.add_argument("map_number", type=int)
child_create.add_argument("--title", required=True)
child_create.add_argument("--type", dest="wayfinder_type", choices=("research", "prototype", "grilling", "task"), default="task")
child_create.add_argument("--body", default="")
child_create.add_argument("--label", action="append", default=[])
dependency = commands.add_parser("dependency")
dependency_commands = dependency.add_subparsers(dest="action", required=True)
dependency_add = dependency_commands.add_parser("add")
dependency_add.add_argument("child", type=int)
dependency_add.add_argument("blocker", type=int)
frontier = commands.add_parser("frontier")
frontier.add_argument("map_number", type=int)
claim = commands.add_parser("claim")
claim.add_argument("kind", choices=("issue", "pr"))
claim.add_argument("number", type=int)
claim.add_argument("--user")
resolve = commands.add_parser("resolve")
resolve.add_argument("kind", choices=("issue", "pr"))
resolve.add_argument("number", type=int)
resolve.add_argument("--answer", required=True)
resolve.add_argument("--map", dest="map_number", type=int)
reference = commands.add_parser("resolve-reference")
reference.add_argument("number", type=int)
return parser
def _dispatch(tracker, args):
resource = args.resource
if resource in ("issue", "pr"):
if args.action == "create":
method = tracker.create_issue if resource == "issue" else tracker.create_pr
kwargs = {"body": args.body, "labels": args.label, "assignees": args.assignee}
if resource == "pr":
kwargs.update(head=args.head, base=args.base)
return method(args.title, **kwargs)
if args.action == "get":
if resource == "issue":
return tracker.get_issue(args.number, comments=not args.no_comments)
return tracker.get_pr(args.number, comments=not args.no_comments, diff=args.diff)
if args.action == "list":
if resource == "issue":
return tracker.list_issues(state=args.state, labels=args.label, limit=args.limit)
return tracker.list_prs(state=args.state, limit=args.limit, external_only=args.external_only)
if args.action == "comment":
return tracker.comment(resource, args.number, args.body)
if args.action == "edit":
return (tracker.edit_issue if resource == "issue" else tracker.edit_pr)(args.number, title=args.title, body=args.body)
if args.action == "assign":
return tracker.assign(resource, args.number, args.user)
if args.action == "close":
return tracker.close(resource, args.number, explanation=args.explanation)
if args.action == "diff":
return tracker.diff(args.number)
if resource == "label":
if args.action == "ensure":
return tracker.ensure_label(args.name, color=args.color, description=args.description)
if args.action == "add":
return tracker.add_label(args.kind, args.number, args.name)
return tracker.remove_label(args.kind, args.number, args.name)
if resource == "map":
return tracker.create_map(args.title, body=args.body, labels=args.label)
if resource == "child":
return tracker.create_child(args.map_number, args.title, wayfinder_type=args.wayfinder_type, body=args.body, labels=args.label)
if resource == "dependency":
return tracker.add_dependency(args.child, args.blocker)
if resource == "frontier":
return tracker.frontier(args.map_number)
if resource == "claim":
return tracker.claim(args.kind, args.number, user=args.user)
if resource == "resolve":
return tracker.resolve(args.kind, args.number, args.answer, map_number=args.map_number)
return tracker.resolve_reference(args.number)
def main(argv=None):
argv = list(sys.argv[1:] if argv is None else argv)
try:
command_argv, provider, repo, attempts = _take_global_options(argv)
args = build_parser().parse_args(command_argv)
tracker = Tracker(provider=provider, repo=repo, retry=RetryPolicy(attempts=attempts))
envelope = _dispatch(tracker, args)
except TrackerError as error:
operation = error.operation
if operation is None:
operation = "cli"
error.operation = operation
envelope = Envelope(False, getattr(error, "provider", None), operation, error=error.to_dict()).to_dict()
except (ValueError, TypeError, OSError) as error:
failure = TrackerError("invalid_input", str(error), operation="cli")
envelope = Envelope(False, None, "cli", error=failure.to_dict()).to_dict()
print(json.dumps(envelope, sort_keys=True))
return 0 if envelope.get("ok") else 1
if __name__ == "__main__":
raise SystemExit(main())
+53
View File
@@ -0,0 +1,53 @@
import os
import re
from .errors import TrackerError # type: ignore[reportMissingImports]
SUPPORTED_PROVIDERS = ("github", "gitlab", "gitea")
def _validate_provider(value):
provider = (value or "").strip().lower()
if provider not in SUPPORTED_PROVIDERS:
raise TrackerError(
"invalid_provider",
f"unsupported provider: {value}",
details={"supported": list(SUPPORTED_PROVIDERS)},
)
return provider
def _providers_from_remote(remote):
host_match = re.search(r"(?:https?://|ssh://|git@)([^/:]+)", remote or "")
host = host_match.group(1).lower() if host_match else ""
found = []
if host == "github.com" or "github" in host:
found.append("github")
if host == "gitlab.com" or "gitlab" in host:
found.append("gitlab")
if "gitea" in host:
found.append("gitea")
return found
def resolve_provider(explicit=None, *, env=None, remote=None):
"""Resolve provider using CLI, environment, then remote precedence."""
if explicit is not None:
return _validate_provider(explicit)
values = os.environ if env is None else env
configured = values.get("TRACKER_PROVIDER")
if configured:
return _validate_provider(configured)
matches = _providers_from_remote(remote or "")
if len(matches) == 1:
return matches[0]
if not remote:
raise TrackerError(
"provider_detection_failed",
"provider was not specified and no Git remote was available",
)
if len(matches) > 1:
message = "Git remote matches multiple supported providers"
else:
message = f"unsupported or unrecognised Git remote: {remote}"
raise TrackerError("provider_detection_failed", message, details={"remote": remote})
+21
View File
@@ -0,0 +1,21 @@
class TrackerError(Exception):
"""A stable, user-facing tracker failure."""
def __init__(self, code, message, *, retryable=False, provider=None, operation=None, details=None):
super().__init__(message)
self.code = code
self.message = message
self.retryable = retryable
self.provider = provider
self.operation = operation
self.details = details or {}
def to_dict(self):
return {
"code": self.code,
"message": self.message,
"retryable": self.retryable,
"provider": self.provider,
"operation": self.operation,
"details": self.details,
}
+95
View File
@@ -0,0 +1,95 @@
from dataclasses import dataclass, field
from typing import Any
@dataclass
class CompletedCommand:
stdout: str = ""
stderr: str = ""
returncode: int = 0
@dataclass
class RetryPolicy:
attempts: int = 3
delay: float = 0.0
def __post_init__(self):
if self.attempts < 1:
raise ValueError("retry attempts must be at least one")
@dataclass(frozen=True)
class ResourceRef:
kind: str
number: int
def __post_init__(self):
if self.kind not in {"issue", "pr"}:
raise ValueError("resource kind must be issue or pr")
if self.number < 1:
raise ValueError("resource number must be positive")
@dataclass
class Envelope:
ok: bool
provider: str | None
operation: str
result: Any = None
details: dict[str, Any] = field(default_factory=dict)
error: dict[str, Any] | None = None
def to_dict(self):
value = {
"ok": self.ok,
"provider": self.provider,
"operation": self.operation,
}
if self.ok:
value["result"] = self.result
if self.details:
value["details"] = self.details
else:
value["error"] = self.error
return value
def normalize_label(value):
if isinstance(value, str):
return {"name": value}
if not isinstance(value, dict):
return {"name": str(value)}
return {
"name": value.get("name", value.get("title", "")),
**{key: value[key] for key in ("color", "description", "id") if key in value},
}
def normalize_resource(value, *, kind, provider):
"""Normalize the common fields while retaining provider-specific raw data."""
if not isinstance(value, dict):
return {"kind": kind, "number": value, "details": {"raw": value}}
number = value.get("number", value.get("iid", value.get("id")))
labels = value.get("labels", value.get("label", [])) or []
assignees = value.get("assignees", value.get("assignee", [])) or []
state = str(value.get("state", "")).lower()
state = {"opened": "open", "open": "open", "closed": "closed"}.get(state, state)
normalized = {
"kind": kind,
"number": number,
"title": value.get("title", ""),
"body": value.get("body", value.get("description", "")) or "",
"state": state,
"labels": [normalize_label(label) for label in labels],
"assignees": assignees if isinstance(assignees, list) else [assignees],
"author": value.get("author", value.get("author_name", value.get("user"))),
"author_association": value.get("author_association", value.get("authorAssociation")),
"url": value.get("url", value.get("web_url", value.get("html_url"))),
"comments": value.get("comments", value.get("notes", [])) or [],
}
for key in ("draft", "merged", "createdAt", "updatedAt", "source_branch", "target_branch", "authorAssociation", "author_association", "membership"):
if key in value:
normalized[key] = value[key]
normalized["details"] = {"provider": provider, "raw": value}
return normalized
View File
+35
View File
@@ -0,0 +1,35 @@
import subprocess
from collections.abc import Mapping, Sequence
from .models import CompletedCommand # type: ignore[reportMissingImports]
class SubprocessRunner:
"""Small injectable subprocess seam used by every provider adapter."""
def run(self, argv: Sequence[str], *, cwd=None, env: Mapping[str, str] | None = None, timeout=None):
process = subprocess.run(
list(argv),
cwd=str(cwd) if cwd else None,
env=dict(env) if env is not None else None,
timeout=timeout,
capture_output=True,
text=True,
check=False,
)
return CompletedCommand(process.stdout, process.stderr, process.returncode)
class RecordingRunner:
"""Useful public fake runner for consumers and contract tests."""
def __init__(self, responses=None):
self.calls = []
self.responses = list(responses or [])
def run(self, argv, **kwargs):
self.calls.append((list(argv), kwargs))
if self.responses:
response = self.responses.pop(0)
return response if isinstance(response, CompletedCommand) else CompletedCommand(*response)
return CompletedCommand("{}")
+472
View File
@@ -0,0 +1,472 @@
import json
import os
import re
import time
from .adapters import ADAPTERS # type: ignore[reportMissingImports]
from .detection import resolve_provider # type: ignore[reportMissingImports]
from .errors import TrackerError # type: ignore[reportMissingImports]
from .models import CompletedCommand, Envelope, ResourceRef, RetryPolicy # type: ignore[reportMissingImports]
from .runner import SubprocessRunner # type: ignore[reportMissingImports]
TRANSIENT_MARKERS = ("timeout", "timed out", "connection", "network", "temporarily", "try again", "rate limit", "429", "502", "503", "504")
AUTH_MARKERS = ("not logged", "authentication", "unauthorized", "forbidden", "login", "token")
NOT_FOUND_MARKERS = ("not found", "does not exist", "unknown issue", "unknown pull")
class Tracker:
"""Provider-neutral facade. Every method returns the CLI-compatible envelope dict."""
def __init__(self, provider=None, *, runner=None, repo=None, cwd=None, retry=None, env=None, external_associations=None):
self.runner = runner or SubprocessRunner()
self.repo = repo
self.cwd = cwd or os.getcwd()
self.env = env
self.external_associations = external_associations
self.retry = retry or RetryPolicy()
remote = None
if provider is None and not (env or os.environ).get("TRACKER_PROVIDER"):
remote = self._discover_remote()
self.provider = resolve_provider(provider, env=env, remote=remote)
self.adapter = ADAPTERS[self.provider](repo=repo)
def _discover_remote(self):
try:
result = self._run_runner(["git", "remote", "get-url", "origin"], retries=1)
except TrackerError:
return None
return result.stdout.strip() or None
def _run_runner(self, argv, *, retries=None) -> CompletedCommand:
attempts = retries or self.retry.attempts
last = None
for attempt in range(attempts):
try:
if hasattr(self.runner, "run"):
response = self.runner.run(argv, cwd=self.cwd)
else:
response = self.runner(argv)
except (OSError, TimeoutError) as exc:
last = CompletedCommand(stderr=str(exc), returncode=1)
else:
if isinstance(response, CompletedCommand):
last = response
elif isinstance(response, tuple):
last = CompletedCommand(*response)
elif isinstance(response, dict):
last = CompletedCommand(**response)
else:
raise TypeError("runner must return CompletedCommand, tuple, or dict")
if last.returncode == 0:
return last
if not self._is_transient(last.stderr) or attempt == attempts - 1:
return last
if self.retry.delay:
time.sleep(self.retry.delay)
return last or CompletedCommand(stderr="provider runner returned no result", returncode=1)
@staticmethod
def _number(value):
try:
number = int(value)
except (TypeError, ValueError) as error:
raise TrackerError("invalid_input", f"invalid resource number: {value}") from error
if number < 1:
raise TrackerError("invalid_input", "resource number must be positive")
return number
@staticmethod
def _is_transient(message):
text = (message or "").lower()
return any(marker in text for marker in TRANSIENT_MARKERS)
def _failure(self, operation, message, *, returncode=1, attempts=None, uncertain=False):
text = (message or "provider command failed").strip()
lower = text.lower()
if uncertain:
code, retryable = "uncertain_outcome", False
elif any(marker in lower for marker in AUTH_MARKERS):
code, retryable = "auth_required", False
elif "no such file" in lower or ("executable" in lower and "not found" in lower):
code, retryable = "provider_cli_missing", False
elif any(marker in lower for marker in NOT_FOUND_MARKERS):
code, retryable = "not_found", False
elif "rate limit" in lower or "429" in lower:
code, retryable = "rate_limited", True
elif self._is_transient(lower):
code, retryable = "transient_failure", True
else:
code, retryable = "provider_error", False
return TrackerError(
code,
text,
retryable=retryable,
provider=self.provider,
operation=operation,
details={"returncode": returncode, **({"attempts": attempts} if attempts else {})},
)
def _run(self, operation, argv, *, mutating=False, uncertain=False) -> CompletedCommand:
result = self._run_runner(argv)
if result.returncode:
error = self._failure(
operation,
result.stderr or result.stdout,
returncode=result.returncode,
attempts=self.retry.attempts,
uncertain=uncertain and self._is_transient(result.stderr),
)
raise error
return result
def _json(self, operation, argv, *, mutating=False, uncertain=False):
result = self._run(operation, argv, mutating=mutating, uncertain=uncertain)
text = result.stdout.strip()
if not text:
return {}
try:
return json.loads(text)
except (TypeError, ValueError):
return {"output": text}
def _safe(self, operation, function):
try:
result, details = function()
return Envelope(True, self.provider, operation, result, details).to_dict()
except TrackerError as error:
if error.provider is None:
error.provider = self.provider
if error.operation is None:
error.operation = operation
return Envelope(False, self.provider, operation, error=error.to_dict()).to_dict()
except (ValueError, TypeError) as error:
failure = TrackerError("invalid_input", str(error), provider=self.provider, operation=operation)
return Envelope(False, self.provider, operation, error=failure.to_dict()).to_dict()
def _call_json(self, operation, command, *, kind=None, mutating=False, uncertain=False):
data = self._json(operation, command, mutating=mutating, uncertain=uncertain)
if kind:
if isinstance(data, list):
data = [self.adapter.normalize(item, kind) for item in data]
elif isinstance(data, dict) and isinstance(data.get("items"), list):
data = {**data, "items": [self.adapter.normalize(item, kind) for item in data["items"]]}
else:
data = self.adapter.normalize(data, kind)
return data
@staticmethod
def _as_list(value):
if isinstance(value, list):
return value
if isinstance(value, dict):
for key in ("items", "labels", "data", "results"):
if isinstance(value.get(key), list):
return value[key]
return []
@staticmethod
def _label_names(labels):
return {item.get("name") if isinstance(item, dict) else item for item in labels}
def _ensure_label_impl(self, name, color="ededed", description=None):
labels = self._call_json("label.ensure", self.adapter.command("label.list"))
found = next((label for label in self._as_list(labels) if (label.get("name") if isinstance(label, dict) else label) == name), None)
if found is not None:
return found, {"created": False}
created = self._call_json(
"label.ensure",
self.adapter.command("label.create", name=name, color=color, description=description),
mutating=True,
)
return created or {"name": name, "color": color, "description": description}, {"created": True}
def ensure_label(self, name, *, color="ededed", description=None):
return self._safe("label.ensure", lambda: self._ensure_label_impl(name, color, description))
def _create(self, kind, title, body="", labels=(), assignees=(), head=None, base=None):
for label in labels:
self._ensure_label_impl(label)
command_args = {"title": title, "body": body, "labels": list(labels), "assignees": list(assignees)}
if kind == "pr":
command_args.update(head=head, base=base)
data = self._call_json(
f"{kind}.create",
self.adapter.command(f"{kind}.create", **command_args),
kind=kind,
mutating=True,
)
return data, {}
def create_issue(self, title, *, body="", labels=(), assignees=()):
return self._safe("issue.create", lambda: self._create("issue", title, body, labels, assignees))
def create_pr(self, title, *, body="", head=None, base=None, labels=(), assignees=()):
return self._safe("pr.create", lambda: self._create("pr", title, body, labels, assignees, head, base))
def _get(self, kind, number, comments=True):
ref = ResourceRef(kind, self._number(number))
data = self._call_json(f"{kind}.get", self.adapter.command(f"{kind}.get", number=ref.number, comments=comments), kind=kind)
return data, {}
def get_issue(self, number, *, comments=True):
return self._safe("issue.get", lambda: self._get("issue", number, comments))
def get_pr(self, number, *, comments=True, diff=False):
def operation():
data, details = self._get("pr", number, comments)
if diff:
if not isinstance(data, dict):
data = {"resource": data}
data["diff"] = self._run("pr.diff", self.adapter.command("pr.diff", number=self._number(number))).stdout
details["included_diff"] = True
return data, details
return self._safe("pr.get", operation)
def _list(self, kind, state="open", labels=(), limit=100):
command_args = {"state": state, "limit": limit}
if kind == "issue":
command_args["labels"] = list(labels)
data = self._call_json(f"{kind}.list", self.adapter.command(f"{kind}.list", **command_args), kind=kind)
return data, {}
def list_issues(self, *, state="open", labels=(), limit=100):
return self._safe("issue.list", lambda: self._list("issue", state, labels, limit))
def list_prs(self, *, state="open", limit=100, external_only=False):
def operation():
data, details = self._list("pr", state, (), limit)
if not external_only:
return data, details
resources = self._as_list(data)
external = []
for resource in resources:
association = resource.get("author_association") if isinstance(resource, dict) else None
if association is None:
raise TrackerError(
"unsupported_capability",
f"{self.provider} did not provide author membership metadata",
provider=self.provider,
operation="pr.list",
details={"capability": "author_membership"},
)
values = self.external_associations or {"owner", "member", "collaborator"}
if str(association).lower() not in {str(value).lower() for value in values}:
external.append(resource)
return external, {**details, "external_only": True}
return self._safe("pr.list", operation)
def _edit(self, kind, number, **kwargs):
data = self._call_json(f"{kind}.edit", self.adapter.command(f"{kind}.edit", number=self._number(number), **kwargs), kind=kind, mutating=True)
return data, {}
def edit_issue(self, number, *, title=None, body=None):
return self._safe("issue.edit", lambda: self._edit("issue", number, title=title, body=body))
def edit_pr(self, number, *, title=None, body=None):
return self._safe("pr.edit", lambda: self._edit("pr", number, title=title, body=body))
def comment(self, kind, number, body):
def operation():
result = self._call_json(f"{kind}.comment", self.adapter.command(f"{kind}.comment", number=self._number(number), body=body), mutating=True, uncertain=True)
return result, {}
return self._safe(f"{kind}.comment", operation)
def add_label(self, kind, number, name, *, color="ededed", description=None):
def operation():
_, ensure_details = self._ensure_label_impl(name, color, description)
result = self._call_json(
f"{kind}.label.add",
self.adapter.command(f"{kind}.edit", number=self._number(number), add_label=name),
kind=kind,
mutating=True,
)
return result, {"label": name, "ensured": ensure_details}
return self._safe(f"{kind}.label.add", operation)
def remove_label(self, kind, number, name):
return self._safe(
f"{kind}.label.remove",
lambda: (self._edit("issue" if kind == "issue" else "pr", number, remove_label=name)[0], {}),
)
def assign(self, kind, number, user):
return self._safe(
f"{kind}.assign",
lambda: (self._edit(kind, number, assignee=user)[0], {"assignee": user}),
)
def close(self, kind, number, *, explanation=None):
def operation():
steps = []
if explanation:
self._call_json(f"{kind}.comment", self.adapter.command(f"{kind}.comment", number=self._number(number), body=explanation), mutating=True, uncertain=True)
steps.append("comment")
self._call_json(f"{kind}.close", self.adapter.command(f"{kind}.close", number=self._number(number)), kind=kind, mutating=True)
steps.append("close")
return {"number": self._number(number), "closed": True}, {"completed": steps}
return self._safe(f"{kind}.close", operation)
def diff(self, number):
return self._safe("pr.diff", lambda: ({"diff": self._run("pr.diff", self.adapter.command("pr.diff", number=self._number(number))).stdout}, {}))
def resolve_reference(self, number):
"""Resolve a shared issue/PR number explicitly; GitLab keeps its spaces separate."""
def operation():
matches = []
for kind in ("issue", "pr"):
result = self._run_runner(self.adapter.command(f"{kind}.get", number=self._number(number), comments=False))
if result.returncode == 0:
matches.append(self.adapter.normalize(self.adapter.json_value(result.stdout), kind))
if len(matches) != 1:
code = "ambiguous_reference" if len(matches) > 1 else "not_found"
raise TrackerError(code, f"reference #{number} did not resolve to exactly one resource", provider=self.provider, details={"matches": matches})
return matches[0], {"matches": [matches[0]["kind"]]}
return self._safe("reference.resolve", operation)
def create_map(self, title, *, body="", labels=()):
map_labels = tuple(dict.fromkeys(["wayfinder:map", *labels]))
return self._safe("map.create", lambda: self._create("issue", title, body, map_labels, ()) )
@staticmethod
def _result_number(result):
if isinstance(result, dict):
if result.get("number"):
return result["number"]
match = re.search(r"/(?:issues|pulls)/(\d+)", str(result.get("output", "")))
if match:
try:
return int(match.group(1))
except (TypeError, ValueError):
return None
return None
def _native_child_link(self, map_number, child_number):
if self.provider == "github":
command = ["gh", "api", "--method", "POST", f"repos/{{owner}}/{{repo}}/issues/{map_number}/sub_issues", "-F", f"sub_issue_id={child_number}"]
elif self.provider == "gitea":
command = ["tea", "api", "--method", "POST", f"/repos/{{owner}}/{{repo}}/issues/{map_number}/sub-issues", "-F", f"child_issue_id={child_number}"]
else:
return False
try:
self._run("child.link", command, mutating=True)
except TrackerError:
return False
return True
def _append_map_child(self, map_number, child_number, title):
if child_number is None:
return False
try:
map_data, _ = self._get("issue", map_number)
body = map_data.get("body", "") if isinstance(map_data, dict) else ""
line = f"- [ ] #{child_number} {title}"
if line not in body:
body = f"{body.rstrip()}\n\n{line}".lstrip()
self._edit("issue", map_number, body=body)
return True
except TrackerError:
return False
def create_child(self, map_number, title, *, wayfinder_type="task", body="", labels=()):
def operation():
map_number_value = self._number(map_number)
child_body = f"Part of #{map_number_value}\n\n{body}".rstrip()
child_labels = tuple(dict.fromkeys([f"wayfinder:{wayfinder_type}", *labels]))
result, details = self._create("issue", title, child_body, child_labels, ())
child_number = self._result_number(result)
native = self._native_child_link(map_number_value, child_number)
fallback = self._append_map_child(map_number_value, child_number, title)
details.update({"relationship": "native" if native else "fallback_task_list", "map_updated": fallback})
return result, details
return self._safe("child.create", operation)
def capabilities(self):
native = {
"child_relationships": self.provider == "github",
"blocking_dependencies": self.provider in {"github", "gitea"},
"diff": True,
"author_membership": self.provider == "github",
}
return Envelope(True, self.provider, "capabilities", native).to_dict()
def list_external_prs(self, *, state="open", limit=100):
return self.list_prs(state=state, limit=limit, external_only=True)
def _special(self, operation, action, child, blocker):
if self.provider == "github":
command = ["gh", "api", "--method", "POST", f"repos/{{owner}}/{{repo}}/issues/{child}/dependencies/blocked_by", "-F", f"issue_id={blocker}"]
elif self.provider == "gitlab":
command = ["glab", "issue", "note", str(child), "--message", f"/blocked_by #{blocker}"]
else:
command = ["tea", "api", "--method", "POST", f"/repos/{{owner}}/{{repo}}/issues/{child}/dependencies", "-F", f"index={blocker}"]
return self._json(operation, command, mutating=True)
def add_dependency(self, child, blocker):
return self._safe("dependency.add", lambda: (self._special("dependency.add", "add", self._number(child), self._number(blocker)), {"fallback": self.provider == "gitlab"}))
@staticmethod
def _refs(body):
numbers = []
for value in re.findall(r"(?:Part of|\[[ xX]\].*?)?\s*#(\d+)", body or ""):
try:
numbers.append(int(value))
except (TypeError, ValueError):
continue
return numbers
def frontier(self, map_number):
def operation():
map_data, _ = self._get("issue", self._number(map_number))
body = map_data.get("body", "") if isinstance(map_data, dict) else ""
candidates = []
map_order = self._refs(body)
order = {str(number): index for index, number in enumerate(map_order)}
for number in map_order:
child_value = self._get("issue", self._number(number))[0]
if not isinstance(child_value, dict):
continue
child = child_value
if child.get("state") == "open" and not child.get("assignees") and not re.search(r"Blocked by:\s*#", child.get("body", ""), re.I):
candidates.append(child)
candidates.sort(key=lambda item: order.get(str(item.get("number", "")), 999999))
return candidates, {"map": self._number(map_number), "deterministic": True}
return self._safe("frontier.query", operation)
def _current_user(self):
if self.provider == "github":
result = self._json("auth.current_user", ["gh", "api", "user", "--jq", ".login"])
elif self.provider == "gitlab":
result = self._json("auth.current_user", ["glab", "api", "user"])
else:
result = self._json("auth.current_user", ["tea", "api", "/user"])
if isinstance(result, str):
return result
if isinstance(result, dict):
return result.get("login", result.get("username", result.get("name", result.get("output"))))
return None
def claim(self, kind, number, *, user=None):
def operation():
owner = user or self._current_user()
if not owner:
raise TrackerError("current_user_unavailable", "provider did not return the current user", provider=self.provider)
result = self._edit(kind, number, assignee=owner)[0]
return result, {"assignee": owner}
return self._safe(f"{kind}.claim", operation)
def resolve(self, kind, number, answer, *, map_number=None):
def operation():
completed = []
try:
self._call_json(f"{kind}.comment", self.adapter.command(f"{kind}.comment", number=self._number(number), body=answer), mutating=True, uncertain=True)
completed.append("comment")
self._call_json(f"{kind}.close", self.adapter.command(f"{kind}.close", number=self._number(number)), mutating=True)
completed.append("close")
if map_number:
pointer = f"Resolved #{self._number(number)}: {answer.splitlines()[0][:200]}"
self._call_json("map.pointer", self.adapter.command("issue.comment", number=self._number(map_number), body=pointer), mutating=True, uncertain=True)
completed.append("map_pointer")
except TrackerError as error:
raise TrackerError("partial_failure", str(error), retryable=False, provider=self.provider, details={"completed": completed, "recovery": "repeat only incomplete steps"})
return {"number": self._number(number), "resolved": True}, {"completed": completed}
return self._safe(f"{kind}.resolve", operation)
+25
View File
@@ -0,0 +1,25 @@
"""Compatibility import name for the tracker automation package."""
from tracker import ( # type: ignore[reportMissingImports]
CompletedCommand,
Envelope,
RecordingRunner,
ResourceRef,
RetryPolicy,
SubprocessRunner,
Tracker,
TrackerError,
resolve_provider,
)
__all__ = [
"CompletedCommand",
"Envelope",
"RecordingRunner",
"ResourceRef",
"RetryPolicy",
"SubprocessRunner",
"Tracker",
"TrackerError",
"resolve_provider",
]
+5
View File
@@ -0,0 +1,5 @@
from .cli import main # type: ignore[reportMissingImports]
if __name__ == "__main__":
raise SystemExit(main())
+3
View File
@@ -0,0 +1,3 @@
from tracker.cli import build_parser, main # type: ignore[reportMissingImports]
__all__ = ["build_parser", "main"]