Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dead6ca2bb | ||
|
|
f65a6ec812 | ||
|
|
7dc4758e9a | ||
|
|
07198e9aad | ||
|
|
8a7201b898 | ||
|
|
f4e953186e | ||
|
|
5a6d1fccb0 | ||
|
|
fa7619bfde | ||
|
|
0daa11851d | ||
|
|
22c3b3e45f | ||
|
|
102b163fc5 | ||
|
|
2a87789523 | ||
|
|
9b4c6952c5 | ||
|
|
d8be029afb | ||
|
|
cc10b66365 | ||
|
|
36536e0e11 | ||
|
|
e16ca1114a | ||
|
|
8829e27004 | ||
|
|
1b5398afdc | ||
|
|
c68f6b4267 | ||
|
|
a0c5280aac |
+48
-32
@@ -1,26 +1,31 @@
|
|||||||
# Project Context Pack
|
# Project Context Pack
|
||||||
|
|
||||||
Generated: 2026-06-25
|
Generated: 2026-08-17
|
||||||
Root: /home/sjb/Documents/ai-workflows/skills
|
Root: /home/sjb/Projects/personal/ws-sjb-skills/wt-master
|
||||||
Working directory: .
|
Working directory: .
|
||||||
Status: fresh
|
Status: fresh
|
||||||
|
|
||||||
## Purpose
|
## 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
|
## Project type
|
||||||
- **Agent skill repository** — markdown-defined agent instructions
|
|
||||||
- Languages: Markdown (100%)
|
- **Agent skill repository plus standalone Python tracker package**
|
||||||
- Package managers: none
|
- Languages: Python and Markdown, one Bash script (detect-agent), one shell script (tmux-open)
|
||||||
- Build/test tools: none
|
- 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
|
- Agent guidance: `AGENTS.md` at root, `docs/invocation.md` for invocation conventions, `docs/agents/` for issue tracker / triage labels / ADR wiki / domain docs
|
||||||
|
|
||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
.
|
.
|
||||||
├── AGENTS.md # Top-level agent instructions for this repo
|
├── AGENTS.md # Top-level agent instructions for this repo
|
||||||
├── README.md # Project overview, lists user-invoked and model-invoked skills
|
├── README.md # Project overview, lists user-invoked and model-invoked skills
|
||||||
├── .gitignore # Excludes docs/adr/
|
├── .gitignore # Excludes docs/adr/
|
||||||
|
├── .agent/
|
||||||
|
│ └── project-context.md # This file
|
||||||
├── docs/
|
├── docs/
|
||||||
│ ├── invocation.md # Model-invoked vs user-invoked definitions
|
│ ├── invocation.md # Model-invoked vs user-invoked definitions
|
||||||
│ ├── agents/
|
│ ├── 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
|
│ │ ├── issue-tracker.md # Gitea issue tracker docs
|
||||||
│ │ └── triage-labels.md # Five-label triage vocabulary
|
│ │ └── triage-labels.md # Five-label triage vocabulary
|
||||||
│ └── adr/ # ADR wiki clone (gitignored)
|
│ └── 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/
|
└── common/
|
||||||
├── README.md # Lists all common skills by invocation type
|
├── 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.
|
├── engineering/ # Model-invoked: lsp-code-analysis, pkm-curation; User-invoked: commit-staged, implement-issue, project-context-pack, setup-skills
|
||||||
├── productivity/ # User-invoked: grill-me, handoff, writing-great-skills; Model-invoked: grilling
|
├── productivity/ # (currently only README.md)
|
||||||
├── pkm/ # User-invoked: conversation-summary, crit, knowledge-gardener, research-vault
|
├── pkm/ # User-invoked: conversation-summary, crit, research-vault, youtube-video-capture; Model-invoked: pkm-curation
|
||||||
├── personal/ # User-invoked: pkm-curation; Model-invoked: forge-preferences
|
├── personal/ # (currently only README.md)
|
||||||
└── deprecated/ # Deprecated skills (audio-production-dispatcher, dsp-research-dispatcher, forge-*)
|
├── misc/ # User-invoked: tmux-launch-agent
|
||||||
|
├── in-progress/ # User-invoked: agent-handoff, knowledge-gardener
|
||||||
|
└── deprecated/ # Deprecated forge-* and dsp-* skills
|
||||||
```
|
```
|
||||||
|
|
||||||
## Important files
|
## Important files
|
||||||
|
|
||||||
- `AGENTS.md` — Agent entry point that describes structure, categories, and references docs
|
- `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/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
|
- `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/project-context-pack/SKILL.md` — The skill that generated this file
|
||||||
- `common/engineering/codebase-design/SKILL.md` — Deep module design vocabulary (referenced by other skills)
|
- `common/misc/tmux-launch-agent/SKILL.md` — Fork agent CLI into new tmux window (user-invoked)
|
||||||
- `common/engineering/domain-modeling/SKILL.md` — Domain modeling with ADRs and CONTEXT files
|
- `common/misc/tmux-launch-agent/tmux-open` — Reusable script that opens a command in a new tmux window/session
|
||||||
- `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
|
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
- Build: none
|
|
||||||
- Test: none
|
- Build/package: `python -m pip install .`
|
||||||
- Lint/typecheck: none
|
- Test: `python -m unittest discover -v`
|
||||||
- Run/dev: skills are invoked by AI agents — no server or dev command
|
- Typecheck: `lsp_diagnostics` on `tracker/` and `tests/`
|
||||||
|
- Run: `python -m tracker` or installed `tracker`
|
||||||
|
|
||||||
## Entry points
|
## Entry points
|
||||||
|
|
||||||
- `AGENTS.md` — loaded by the AI agent as project instructions (referred to in pi's agent config)
|
- `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
|
- Each `SKILL.md` under `common/` — referenced by agents via slash commands or auto-invocation
|
||||||
|
|
||||||
## Search and symbol notes
|
## Search and symbol notes
|
||||||
|
|
||||||
- All skills are `SKILL.md` files — search with `fd SKILL.md`
|
- All skills are `SKILL.md` files — search with `fd SKILL.md`
|
||||||
- Bucket READMEs: `fd README.md common/`
|
- 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)
|
- 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
|
- Shared reference docs live inside the owning skill's directory; other skills reach that material by invoking the skill
|
||||||
|
|
||||||
## Files inspected
|
## Files inspected
|
||||||
|
|
||||||
- `AGENTS.md` — root agent instructions — fresh
|
- `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
|
- `docs/invocation.md` — invocation model definitions — fresh
|
||||||
- `.gitignore` — excludes docs/adr/ — 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 — fresh
|
||||||
- `common/README.md` — common bucket index — updated (links fixed, missing skills added)
|
- `common/misc/README.md` — misc bucket index — fresh
|
||||||
- `common/productivity/README.md` — productivity bucket index — fresh (was already correct)
|
- `.agent/project-context.md` — this file (refreshed from stale 2026-06-25 version)
|
||||||
- `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
|
|
||||||
|
|
||||||
## Exclusions
|
## Exclusions
|
||||||
|
|
||||||
- `.git/` — VCS data
|
- `.git/` — VCS data
|
||||||
- `docs/adr/` — gitignored ADR wiki clone
|
- `docs/adr/` — gitignored ADR wiki clone
|
||||||
- `node_modules/`, `dist/`, `build/`, `target/`, `.venv/`, `__pycache__/`, `vendor/`, `coverage/` — not present, but excluded by policy
|
- `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
|
- Binary files, large artifacts, credentials, secrets, personal data
|
||||||
|
|
||||||
## Navigation rules for future agents
|
## Navigation rules for future agents
|
||||||
|
|
||||||
- Start with `fd SKILL.md` to find all skills, then narrow by `fd SKILL.md common/<category>/`
|
- 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
|
- 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`
|
- 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
|
- Track every inspected file in this cache
|
||||||
- Re-read a file only when it changed, the cache is stale, or exact details are needed
|
- 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
|
## Refresh notes
|
||||||
|
|
||||||
- This is a markdown-only repo with no build artifacts; refreshes are rarely needed unless skills are added or removed
|
- 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
|
- 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
|
- 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)
|
- If a skill is moved between buckets, update all README.md files that reference it (root, common, source bucket, destination bucket)
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
docs/adr/
|
docs/adr/
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
|||||||
@@ -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`.
|
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
|
## list of categories
|
||||||
|
|
||||||
- `engineering/` — daily code work
|
- `engineering/` — daily code work
|
||||||
- `productivity/` — daily non-code workflow tools
|
- `productivity/` — daily non-code workflow tools
|
||||||
- `misc/` — kept around but rarely used
|
- `misc/` — kept around but rarely used
|
||||||
@@ -32,11 +32,11 @@ Every `SKILL.md` is either user-invoked (`disable-model-invocation: true`, reach
|
|||||||
|
|
||||||
### Issue tracker
|
### 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
|
### 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
|
### Domain docs
|
||||||
|
|
||||||
@@ -44,7 +44,11 @@ Single-context layout. See `docs/agents/domain.md`.
|
|||||||
|
|
||||||
### ADR wiki
|
### 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
|
### Agent CLI
|
||||||
|
|
||||||
|
|||||||
@@ -1,41 +1,27 @@
|
|||||||
# Skills
|
# Skills
|
||||||
|
|
||||||
A collection of agent skills (slash commands and behaviors) loaded into Steve Beaulac's agents.
|
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
|
## User-invoked
|
||||||
|
|
||||||
- [conversation-summary](common/pkm/conversation-summary/SKILL.md) — Summarize the current AI conversation into a new Obsidian markdown note and matching transcript file.
|
- [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.
|
||||||
- [crit](common/pkm/crit/SKILL.md) — Brainstorm with AI using the CRIT framework to generate and evaluate ideas.
|
- [commit-staged](common/engineering/commit-staged/SKILL.md) — Commit staged files with a conventional commit message.
|
||||||
- [grill-me](common/productivity/grill-me/SKILL.md) — A relentless interview to sharpen a plan or design.
|
- [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.
|
||||||
- [grill-with-docs](common/engineering/grill-with-docs/SKILL.md) — A relentless interview to sharpen a plan or design, which also creates docs (ADRs and glossary) as we go.
|
- [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.
|
||||||
- [handoff](common/productivity/handoff/SKILL.md) — Compact the current conversation into a handoff document for another agent to pick up.
|
- [implement-isolation](common/engineering/implement-isolation/SKILL.md) — Implement a piece of work based on a spec or set of tickets in isolation.
|
||||||
- [implement-issue](common/engineering/implement-issue/SKILL.md) — Dispatch a child agent in an isolated git worktree to implement a ready-for-agent issue end-to-end.
|
- [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.
|
||||||
- [improve-codebase-architecture](common/engineering/improve-codebase-architecture/SKILL.md) — Scan a codebase for deepening opportunities, present them as a visual HTML report, then grill through whichever one you pick.
|
- [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.
|
||||||
- [knowledge-gardener](common/pkm/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) — 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.
|
||||||
- [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.
|
- [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.
|
||||||
- [project-context-pack](common/engineering/project-context-pack/SKILL.md) — Build and refresh a bounded repo context memory file so agents use disciplined search instead of repeated browsing.
|
- [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.
|
||||||
- [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 Obsidian research packet.
|
- [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.
|
||||||
- [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.
|
- [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.
|
||||||
- [to-issues](common/engineering/to-issues/SKILL.md) — Break a plan, spec, or PRD into independently-grabbable issues on the project issue tracker using tracer-bullet vertical slices.
|
|
||||||
- [to-prd](common/engineering/to-prd/SKILL.md) — Turn the current conversation into a PRD and publish it to the project issue tracker — no interview, just synthesis of what you've already discussed.
|
|
||||||
- [triage](common/engineering/triage/SKILL.md) — Move issues and external PRs through a state machine of triage roles — categorise, verify, grill if needed, and write agent-ready briefs.
|
|
||||||
- [writing-great-skills](common/productivity/writing-great-skills/SKILL.md) — Reference for writing and editing skills well — the vocabulary and principles that make a skill predictable.
|
|
||||||
|
|
||||||
**Deprecated / user-invoked:**
|
|
||||||
- [forge-router](common/deprecated/forge-router/SKILL.md) — High-level guidance for choosing the right forge skill.
|
|
||||||
|
|
||||||
## Model-invoked
|
## Model-invoked
|
||||||
|
|
||||||
- [codebase-design](common/engineering/codebase-design/SKILL.md) — Shared vocabulary for designing deep modules.
|
- [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.
|
||||||
- [domain-modeling](common/engineering/domain-modeling/SKILL.md) — Build and sharpen a project's domain model.
|
- [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.
|
||||||
- [grilling](common/productivity/grilling/SKILL.md) — Interview the user relentlessly about a plan or design.
|
|
||||||
- [resolving-merge-conflicts](common/engineering/resolving-merge-conflicts/SKILL.md) — Resolve an in-progress git merge/rebase conflict.
|
|
||||||
- [tdd](common/engineering/tdd/SKILL.md) — Test-driven development.
|
|
||||||
|
|
||||||
**Deprecated / user-invoked:**
|
|
||||||
- [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.
|
|
||||||
- [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-gitea](common/deprecated/forge-gitea/SKILL.md) — Work with Gitea repositories, issues, pull requests, releases, and CI.
|
|
||||||
- [forge-github](common/deprecated/forge-github/SKILL.md) — Work with GitHub repositories, issues, pull requests, releases, and CI.
|
|
||||||
- [forge-interaction](common/deprecated/forge-interaction/SKILL.md) — Work safely with GitHub and Gitea repositories, issues, pull requests, releases, and remote forge state.
|
|
||||||
- [forge-preferences](common/deprecated/forge-preferences/SKILL.md) — Apply Steve's personal or project-specific GitHub/Gitea forge preferences.
|
|
||||||
|
|||||||
+14
-31
@@ -4,37 +4,20 @@ Skills that work in all CLI agents.
|
|||||||
|
|
||||||
## User-invoked
|
## User-invoked
|
||||||
|
|
||||||
- [conversation-summary](pkm/conversation-summary/SKILL.md) — Summarize the current AI conversation into a new Obsidian markdown note and matching transcript file.
|
- [agent-handoff](in-progress/agent-handoff/SKILL.md) — Hand the current conversation off to a fresh background agent that picks up the work immediately.
|
||||||
- [crit](pkm/crit/SKILL.md) — Brainstorm with AI using the CRIT framework to generate and evaluate ideas.
|
- [commit-staged](engineering/commit-staged/SKILL.md) — Commit staged files with a conventional commit message.
|
||||||
- [grill-me](productivity/grill-me/SKILL.md) — A relentless interview to sharpen a plan or design.
|
- [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.
|
||||||
- [grill-with-docs](engineering/grill-with-docs/SKILL.md) — A relentless interview to sharpen a plan or design, which also creates docs (ADRs and glossary) as we go.
|
- [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.
|
||||||
- [handoff](productivity/handoff/SKILL.md) — Compact the current conversation into a handoff document for another agent to pick up.
|
- [implement-isolation](engineering/implement-isolation/SKILL.md) — Implement a piece of work based on a spec or set of tickets in isolation.
|
||||||
- [improve-codebase-architecture](engineering/improve-codebase-architecture/SKILL.md) — Scan a codebase for deepening opportunities, present them as a visual HTML report, then grill through whichever one you pick.
|
- [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](pkm/knowledge-gardener/SKILL.md) — Run vault-aware semantic search, synthesis, note creation, linking, and Zettelkasten workflows for this Obsidian vault.
|
- [knowledge-gardener](in-progress/knowledge-gardener/SKILL.md) — Run vault-aware semantic search, synthesis, note creation, linking, and Zettelkasten workflows for this Obsidian vault.
|
||||||
- [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.
|
- [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.
|
||||||
- [project-context-pack](engineering/project-context-pack/SKILL.md) — Build and refresh a bounded repo context memory file so agents use disciplined search 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.
|
||||||
- [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 Obsidian research packet.
|
- [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.
|
||||||
- [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.
|
- [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.
|
||||||
- [to-issues](engineering/to-issues/SKILL.md) — Break a plan, spec, or PRD into independently-grabbable issues on the project issue tracker using tracer-bullet vertical slices.
|
- [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.
|
||||||
- [to-prd](engineering/to-prd/SKILL.md) — Turn the current conversation into a PRD and publish it to the project issue tracker — no interview, just synthesis of what you've already discussed.
|
|
||||||
- [triage](engineering/triage/SKILL.md) — Move issues and external PRs through a state machine of triage roles — categorise, verify, grill if needed, and write agent-ready briefs.
|
|
||||||
- [writing-great-skills](productivity/writing-great-skills/SKILL.md) — Reference for writing and editing skills well — the vocabulary and principles that make a skill predictable.
|
|
||||||
|
|
||||||
**Deprecated / user-invoked:**
|
|
||||||
- [forge-router](deprecated/forge-router/SKILL.md) — High-level guidance for choosing the right forge skill.
|
|
||||||
|
|
||||||
## Model-invoked
|
## Model-invoked
|
||||||
|
|
||||||
- [codebase-design](engineering/codebase-design/SKILL.md) — Shared vocabulary for designing deep modules.
|
- [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.
|
||||||
- [domain-modeling](engineering/domain-modeling/SKILL.md) — Build and sharpen a project's domain model.
|
- [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.
|
||||||
- [grilling](productivity/grilling/SKILL.md) — Interview the user relentlessly about a plan or design.
|
|
||||||
- [resolving-merge-conflicts](engineering/resolving-merge-conflicts/SKILL.md) — Resolve an in-progress git merge/rebase conflict.
|
|
||||||
- [tdd](engineering/tdd/SKILL.md) — Test-driven development.
|
|
||||||
|
|
||||||
**Deprecated / user-invoked:**
|
|
||||||
- [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.
|
|
||||||
- [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-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) — Work safely with GitHub and Gitea repositories, issues, pull requests, releases, and remote forge state.
|
|
||||||
- [forge-preferences](deprecated/forge-preferences/SKILL.md) — Apply Steve's personal or project-specific GitHub/Gitea forge preferences.
|
|
||||||
|
|||||||
@@ -1,13 +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.
|
|
||||||
- [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) — Work safely with GitHub and Gitea repositories, issues, pull requests, releases, and remote forge state.
|
|
||||||
- [forge-preferences](forge-preferences/SKILL.md) — Apply Steve's personal or project-specific GitHub/Gitea forge 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.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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
|
|
||||||
@@ -1,19 +1,15 @@
|
|||||||
# Engineering Skills
|
# Engineering Skills
|
||||||
|
|
||||||
|
Daily code work.
|
||||||
|
|
||||||
## User-invoked
|
## User-invoked
|
||||||
|
|
||||||
- [implement-issue](implement-issue/SKILL.md) — Dispatch a child agent in an isolated git worktree to implement a ready-for-agent issue end-to-end.
|
- [commit-staged](commit-staged/SKILL.md) — Commit staged files with a conventional commit message.
|
||||||
- [grill-with-docs](grill-with-docs/SKILL.md) — A relentless interview to sharpen a plan or design, which also creates docs (ADRs and glossary) as we go.
|
- [implement-isolation](implement-isolation/SKILL.md) — Implement a piece of work based on a spec or set of tickets in isolation.
|
||||||
- [improve-codebase-architecture](improve-codebase-architecture/SKILL.md) — Scan a codebase for deepening opportunities, present them as a visual HTML report, then grill through whichever one you pick.
|
- [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 and refresh a bounded repo context memory file so agents use disciplined search instead of repeated browsing.
|
- [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.
|
- [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.
|
||||||
- [to-issues](to-issues/SKILL.md) — Break a plan, spec, or PRD into independently-grabbable issues on the project issue tracker using tracer-bullet vertical slices.
|
|
||||||
- [to-prd](to-prd/SKILL.md) — Turn the current conversation into a PRD and publish it to the project issue tracker — no interview, just synthesis of what you've already discussed.
|
|
||||||
- [triage](triage/SKILL.md) — Move issues and external PRs through a state machine of triage roles — categorise, verify, grill if needed, and write agent-ready briefs.
|
|
||||||
|
|
||||||
## Model-invoked
|
## Model-invoked
|
||||||
|
|
||||||
- [codebase-design](codebase-design/SKILL.md) — Shared vocabulary for designing deep modules.
|
- [lsp-code-analysis](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.
|
||||||
- [domain-modeling](domain-modeling/SKILL.md) — Build and sharpen a project's domain model.
|
|
||||||
- [resolving-merge-conflicts](resolving-merge-conflicts/SKILL.md) — Resolve an in-progress git merge/rebase conflict.
|
|
||||||
- [tdd](tdd/SKILL.md) — Test-driven development.
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
name: commit-staged
|
||||||
|
description: Commit staged files with a conventional commit message.
|
||||||
|
disable-model-invocation: true
|
||||||
|
---
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
1. **Check the staging area** — Run `git diff --cached --stat`. If empty, report "nothing staged" and stop.
|
||||||
|
|
||||||
|
Completion criterion: Staged files are listed, or the skill terminates.
|
||||||
|
|
||||||
|
2. **Draft the message** — Inspect `git diff --cached` to understand what changed. Write a [conventional commit](https://www.conventionalcommits.org/) message:
|
||||||
|
|
||||||
|
- Format: `type(scope): summary` — scope is optional.
|
||||||
|
- Common types: `feat`, `fix`, `refactor`, `docs`, `chore`, `test`, `style`, `perf`, `ci`, `build`.
|
||||||
|
- Summary: imperative mood, lowercase, no period, ≤72 characters.
|
||||||
|
- Body (if needed): wrap at 72 characters, explain _what_ and _why_, not _how_.
|
||||||
|
|
||||||
|
Completion criterion: A conventional commit message is composed.
|
||||||
|
|
||||||
|
3. **Commit** — Run `git commit -m "<message>"`. If the commit fails, report the error and stop.
|
||||||
|
|
||||||
|
Completion criterion: `git commit` exits 0.
|
||||||
|
|
||||||
|
4. **Report** — Print the short commit hash and the first line of the message.
|
||||||
|
|
||||||
|
Completion criterion: The hash and message are printed.
|
||||||
@@ -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
|
||||||
@@ -6,6 +6,8 @@ disable-model-invocation: true
|
|||||||
|
|
||||||
# Setup Engineering Skills
|
# 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:
|
Scaffold the per-repo configuration that the engineering skills assume:
|
||||||
|
|
||||||
- Issue tracker: where issues live (never assume a default forge)
|
- Issue tracker: where issues live (never assume a default forge)
|
||||||
@@ -21,51 +23,44 @@ This is a prompt-driven skill, not a deterministic script. Explore, present what
|
|||||||
|
|
||||||
Look at the current repo to understand its starting state. Read whatever exists; don't assume:
|
Look at the current repo to understand its starting state. Read whatever exists; don't assume:
|
||||||
|
|
||||||
- find out what force is being used for the issue tracker (GitHub, GitLab, or Gitea)
|
- `git remote -v` and `.git/config` — is this a GitHub repo? Which one?
|
||||||
- `AGENTS.md` and `CLAUDE.md` at the repo root — does either exist? Is there already an ## Agent skills section in either?
|
- `AGENTS.md` and `CLAUDE.md` at the repo root — does either exist? Is there already an `## Agent skills` section in either?
|
||||||
* `CONTEXT.md` and `CONTEXT-MAP.md` at the repo root.
|
- `CONTEXT.md` and `CONTEXT-MAP.md` at the repo root
|
||||||
- `docs/adr/` (check whether it's a wiki clone via `docs/adr/.git/config`) and any `src/*/docs/adr/` directories
|
- `docs/adr/` and any `src/*/docs/adr/` directories
|
||||||
- `docs/agents/` — does this skill's prior output already exist?
|
- `docs/agents/` — does this skill's prior output already exist?
|
||||||
|
- Is the `triage` skill installed? (a `triage` skill folder alongside this one, or `triage` in your available skills.) This decides whether Section B runs at all.
|
||||||
|
- Monorepo signals — a `pnpm-workspace.yaml`, a `workspaces` field in `package.json`, or a populated `packages/*` with its own `src/`. Present only in a genuinely large multi-package repo; their absence means single-context, which is almost every repo.
|
||||||
|
|
||||||
### 2. Present findings and ask
|
### 2. Present findings and ask
|
||||||
|
|
||||||
Summarise what's present and what's missing. Then walk the user through the five decisions **one at a time** - present a section, get the user's answer, then move to the next. Don't dump all at once.
|
Summarise what's present and what's missing. Then take the sections in order — one section, one answer, then the next.
|
||||||
|
|
||||||
Assume the user does not know what these terms mean. Each section starts with a short explainer (what it is, why these skills need it, what changes if they pick differently). Then show the choices and the default.
|
Lead each section with the recommended answer so the user can accept it in a word. Give a one-line explainer only when the choice genuinely branches; skip the section entirely when exploration already settled it (Section B when `triage` isn't installed, Section C when there's no monorepo).
|
||||||
|
|
||||||
Section A - Issue tracker:
|
Section A - Issue tracker:
|
||||||
|
|
||||||
> Explainer: The "issue tracker" is where issues live for this repo. Skills like `to-issues`, `triage`, `to-prd`, and `qa` read from and write to it — they need to know whether to call `gh issue create`, write a markdown file under `.scratch/`, or follow some other workflow you describe. Pick the place you actually track work for this repo.
|
> Explainer: The "issue tracker" is where issues live for this repo. Skills like `to-tickets`, `triage`, `to-spec`, and `qa` read from and write to it — they need to know whether to call `gh issue create`, or follow some other workflow you describe. Pick the place you actually track work for this repo.
|
||||||
|
|
||||||
Default posture: these skills were designed for GitHub. If a `git remote` points at GitHub, propose that. If a `git remote` points at GitLab (`gitlab.com` or a self-hosted host), propose GitLab. If a `git remote` point at a Gitea (a self-hosted host with a `gitea` in the url). Otherwise ask the user, offer:
|
Default posture: these skills were designed for GitHub. If a `git remote` points at GitHub, propose that. If a `git remote` points at GitLab (`gitlab.com` or a self-hosted host), propose GitLab. If a `git remote` point at a Gitea (a self-hosted host with a `gitea` in the url). Otherwise ask the user, offer:
|
||||||
|
|
||||||
- **GitHub** — issues live in the repo's GitHub Issues (uses the `gh` CLI)
|
- **GitHub** — issues live in the repo's GitHub Issues (uses the `gh` CLI)
|
||||||
- **GitLab** — issues live in the repo's GitLab Issues (uses the [`glab`](https://gitlab.com/gitlab-org/cli) CLI)
|
- **GitLab** — issues live in the repo's GitLab Issues (uses the [`glab`](https://gitlab.com/gitlab-org/cli) CLI)
|
||||||
* Gitea — issues live in the repo's Gitea Issues (uses the `tea` CLI)
|
- Gitea — issues live in the repo's Gitea Issues (uses the `tea` CLI)
|
||||||
- **Other** (Jira, Linear, etc.) — ask the user to describe the workflow in one paragraph; the skill will record it as freeform prose
|
- **Other** (Jira, Linear, etc.) — ask the user to describe the workflow in one paragraph; the skill will record it as freeform prose
|
||||||
|
|
||||||
If — and only if — the user picked **GitHub**, **GitLab** or **Gitea**, ask one follow-up:
|
Record the choice in `docs/agents/issue-tracker.md`. The GitHub and GitLab templates carry a "PRs as a request surface" flag, defaulted **off** — leave it off and don't raise it; a user who wants external PRs in the triage queue can flip the flag in the file later.
|
||||||
|
|
||||||
> Explainer: Open-source repos often receive feature requests as pull requests, not just issues — a PR is an issue with attached code. If you turn this on, `/triage` pulls *external* PRs into the same queue and runs them through the same labels and states as issues (collaborators' in-flight PRs are left alone). Leave it off if PRs aren't a request surface for you.
|
**Section B — Triage label vocabulary.** Skip this section entirely if the `triage` skill isn't installed (exploration told you) — an uninstalled skill needs no labels.
|
||||||
|
|
||||||
- **PRs as a request surface** — yes / no (default: no). Record the answer in `docs/agents/issue-tracker.md`. For local-markdown and other trackers, skip this question — there are no PRs.
|
If it is installed, ask exactly one question:
|
||||||
|
|
||||||
**Section B — Triage label vocabulary.**
|
> Do you want to keep the default triage labels? (recommended: **yes**)
|
||||||
|
|
||||||
> Explainer: When the `triage` skill processes an incoming issue, it moves it through a state machine — needs evaluation, waiting on reporter, ready for an AFK agent to pick up, ready for a human, or won't fix. To do that, it needs to apply labels (or the equivalent in your issue tracker) that match strings *you've actually configured*. If your repo already uses different label names (e.g. `bug:triage` instead of `needs-triage`), map them here so the skill applies the right ones instead of creating duplicates.
|
The defaults canonical roles are listed in `triage-labels.md` each label string equal to its name. On **yes**, write them as-is. Only if the user says no — usually because their tracker already uses other names (e.g. `bug:triage` for `needs-triage`) — collect the overrides so `triage` applies existing labels instead of creating duplicates.
|
||||||
|
|
||||||
Look at the `triage-labels.md` file for the labels and each roles they define.
|
**Section C — Domain docs.** Default to **single-context** — one `CONTEXT.md` + `docs/adr/` at the repo root. This fits almost every repo; write it without asking.
|
||||||
|
|
||||||
Default: each role's string equals its name. Ask the user if they want to override any. If their issue tracker has no existing labels, the defaults are fine.
|
Offer **multi-context** — a root `CONTEXT-MAP.md` pointing to per-context `CONTEXT.md` files — only when exploration found monorepo signals. Then confirm which layout they want.
|
||||||
|
|
||||||
**Section C — Domain docs.**
|
|
||||||
|
|
||||||
> Explainer: Some skills (`improve-codebase-architecture`, `diagnosing-bugs`, `tdd`) read a `CONTEXT.md` file to learn the project's domain language, and `adr` for past architectural decisions. They need to know whether the repo has one global context or multiple (e.g. a monorepo with separate frontend/backend contexts) so they look in the right place.
|
|
||||||
|
|
||||||
Confirm the layout:
|
|
||||||
|
|
||||||
- **Single-context** — one `CONTEXT.md` + `adr` at the repo root. Most repos are this.
|
|
||||||
- **Multi-context** — `CONTEXT-MAP.md` at the root pointing to per-context `CONTEXT.md` files (typically a monorepo).
|
|
||||||
|
|
||||||
**Section D — ADR wiki.**
|
**Section D — ADR wiki.**
|
||||||
|
|
||||||
@@ -74,7 +69,7 @@ Confirm the layout:
|
|||||||
Derive the wiki URL from the forge detected in Section A:
|
Derive the wiki URL from the forge detected in Section A:
|
||||||
|
|
||||||
| Forge | Wiki URL pattern |
|
| Forge | Wiki URL pattern |
|
||||||
|---|---|
|
| --- | --- |
|
||||||
| **GitHub** | `https://github.com/<owner>/<repo>.wiki.git` |
|
| **GitHub** | `https://github.com/<owner>/<repo>.wiki.git` |
|
||||||
| **Gitea** | `https://<host>/<owner>/<repo>.wiki.git` |
|
| **Gitea** | `https://<host>/<owner>/<repo>.wiki.git` |
|
||||||
| **GitLab** | `https://gitlab.com/<owner>/<repo>.wiki.git` |
|
| **GitLab** | `https://gitlab.com/<owner>/<repo>.wiki.git` |
|
||||||
@@ -94,7 +89,7 @@ Record the answers in `docs/agents/adr-wiki.md`.
|
|||||||
Show the user a draft of:
|
Show the user a draft of:
|
||||||
|
|
||||||
- The `## Agent skills` block to add to whichever of `CLAUDE.md` / `AGENTS.md` is being edited (see step 4 for selection rules)
|
- The `## Agent skills` block to add to whichever of `CLAUDE.md` / `AGENTS.md` is being edited (see step 4 for selection rules)
|
||||||
- The contents of `docs/agents/issue-tracker.md`, `docs/agents/triage-labels.md`, `docs/agents/domain.md`, and `docs/agents/adr-wiki.md`
|
- The contents of `docs/agents/issue-tracker.md`, `docs/agents/domain.md`, `docs/agents/adr-wiki.md` and `docs/agents/triage-labels.md` (the last only when `triage` is installed)
|
||||||
|
|
||||||
Let them edit before writing.
|
Let them edit before writing.
|
||||||
|
|
||||||
@@ -117,7 +112,7 @@ The block:
|
|||||||
|
|
||||||
### Issue tracker
|
### Issue tracker
|
||||||
|
|
||||||
[one-line summary of where issues are tracked, plus whether external PRs are a triage surface]. See `docs/agents/issue-tracker.md`.
|
[one-line summary of where issues are tracked]. See `docs/agents/issue-tracker.md`.
|
||||||
|
|
||||||
### Triage labels
|
### Triage labels
|
||||||
|
|
||||||
@@ -139,14 +134,16 @@ The block:
|
|||||||
|
|
||||||
Then write the docs files:
|
Then write the docs files:
|
||||||
|
|
||||||
- [issue-tracker-github.md](./issue-tracker-github.md) — GitHub issue tracker
|
- [issue-tracker-github.md](./issue-tracker-github.md) — GitHub issue tracker, including wayfinding operations
|
||||||
- [issue-tracker-gitlab.md](./issue-tracker-gitlab.md) — GitLab issue tracker
|
- [issue-tracker-gitlab.md](./issue-tracker-gitlab.md) — GitLab issue tracker, including wayfinding operations
|
||||||
- [issue-tracker-gitea.md](./issue-tracker-gitea.md) — Gitea issue tracker
|
- [issue-tracker-gitea.md](./issue-tracker-gitea.md) — Gitea issue tracker, including wayfinding operations
|
||||||
- [triage-labels.md](./triage-labels.md) — label mapping
|
- [triage-labels.md](./triage-labels.md) — label mapping
|
||||||
- [domain.md](./domain.md) — domain doc consumer rules + layout
|
- [domain.md](./domain.md) — domain doc consumer rules + layout
|
||||||
- [adr-wiki.md](./adr-wiki.md) — ADR wiki clone and push workflow
|
- [adr-wiki.md](./adr-wiki.md) — ADR wiki clone and push workflow
|
||||||
|
|
||||||
For "other" issue trackers, write `docs/agents/issue-tracker.md` from scratch using the user's description.
|
For GitHub, GitLab, and Gitea, the generated `docs/agents/issue-tracker.md` must include the full "Wayfinding operations" section from the selected template. Do not omit that section when composing the project file.
|
||||||
|
|
||||||
|
For "other" issue trackers, write `docs/agents/issue-tracker.md` from scratch using the user's description, and include an equivalent wayfinding section only if the user described a workable wayfinding workflow for that tracker.
|
||||||
|
|
||||||
### 5. Done
|
### 5. Done
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ Architecture Decision Records live on the forge wiki and are cloned into `docs/a
|
|||||||
<wiki-url>
|
<wiki-url>
|
||||||
```
|
```
|
||||||
|
|
||||||
Derived from the forge remote during `/setup-matt-pocock-skills`.
|
Derived from the forge remote during `/setup-skills`.
|
||||||
|
|
||||||
## Bootstrap
|
## Bootstrap
|
||||||
|
|
||||||
On first setup, `/setup-matt-pocock-skills` clones the wiki:
|
On first setup, `/setup-skills` clones the wiki:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone <wiki-url> docs/adr/
|
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
|
# Issue tracker: Gitea
|
||||||
|
|
||||||
Issues and PRDs for this repo live as Gitea issues. Use the `gitea` 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
|
## Conventions
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ Issues and PRDs for this repo live as Gitea issues. Use the `gitea` CLI for all
|
|||||||
- **Read an issue**: `tea issue <number> --comments`. Use `-o json` for machine-readable output.
|
- **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.
|
- **List issues**: `tea issue list --state open -o json` with appropriate `--labels` and `--state` filters.
|
||||||
- **Comment on an issue**: `tea comment <number> "..."`.
|
- **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.les
|
- **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.
|
- **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.
|
Infer the repo from git remote -v — `tea` does this automatically when run inside a clone.
|
||||||
@@ -17,11 +17,10 @@ Infer the repo from git remote -v — `tea` does this automatically when run ins
|
|||||||
|
|
||||||
**PRs as a request surface: no.** _(Set to `yes` if this repo treats external PRs as feature requests; `/triage` reads this flag.)_
|
**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 `gh pr` equivalents:
|
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.
|
- **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 (a contributor's MR, not a maintainer's in-flight work).
|
||||||
- **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`.
|
- **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`.
|
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`.
|
||||||
@@ -33,3 +32,14 @@ Create a Gitea issue.
|
|||||||
## When a skill says "fetch the relevant ticket"
|
## When a skill says "fetch the relevant ticket"
|
||||||
|
|
||||||
Run `tea issue <number> --comments`.
|
Run `tea issue <number> --comments`.
|
||||||
|
|
||||||
|
## Wayfinding operations
|
||||||
|
|
||||||
|
Used by `/wayfinder`. The **map** is a single issue with **child** issues as tickets.
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
- **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.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Issue tracker: GitHub
|
# 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
|
## Conventions
|
||||||
|
|
||||||
@@ -32,3 +32,14 @@ Create a GitHub issue.
|
|||||||
## When a skill says "fetch the relevant ticket"
|
## When a skill says "fetch the relevant ticket"
|
||||||
|
|
||||||
Run `gh issue view <number> --comments`.
|
Run `gh issue view <number> --comments`.
|
||||||
|
|
||||||
|
## Wayfinding operations
|
||||||
|
|
||||||
|
Used by `/wayfinder`. The **map** is a single issue with **child** issues as tickets.
|
||||||
|
|
||||||
|
- **Map**: a single issue labelled `wayfinder:map`, holding the Notes / Decisions-so-far / Fog body. `gh issue create --label wayfinder:map`.
|
||||||
|
- **Child ticket**: an issue linked to the map as a GitHub sub-issue (`gh 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 `gh api --method POST repos/<owner>/<repo>/issues/<child>/dependencies/blocked_by -F issue_id=<blocker-db-id>`, where `<blocker-db-id>` is the blocker's numeric **database id** (`gh api repos/<owner>/<repo>/issues/<n> --jq .id`, _not_ the `#number` or `node_id`). GitHub reports `issue_dependencies_summary.blocked_by` (open blockers only — the live gate). 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 children (`gh issue list --state open`, scoped to the map's sub-issues / task list), drop any with an open blocker (`issue_dependencies_summary.blocked_by > 0`, or an open issue in the `Blocked by` line) or an assignee; first in map order wins.
|
||||||
|
- **Claim**: `gh issue edit <n> --add-assignee @me` — the session's first write.
|
||||||
|
- **Resolve**: `gh issue comment <n> --body "<answer>"`, then `gh issue close <n>`, then append a context pointer (gist + link) to the map's Decisions-so-far.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Issue tracker: GitLab
|
# 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
|
## Conventions
|
||||||
|
|
||||||
@@ -33,3 +33,14 @@ Create a GitLab issue.
|
|||||||
## When a skill says "fetch the relevant ticket"
|
## When a skill says "fetch the relevant ticket"
|
||||||
|
|
||||||
Run `glab issue view <number> --comments`.
|
Run `glab issue view <number> --comments`.
|
||||||
|
|
||||||
|
## Wayfinding operations
|
||||||
|
|
||||||
|
Used by `/wayfinder`. The **map** is a single issue with **child** issues as tickets.
|
||||||
|
|
||||||
|
- **Map**: a single issue labelled `wayfinder:map`, holding the Notes / Decisions-so-far / Fog body. `glab issue create --label wayfinder:map`. (On GitLab tiers with native epics, an epic may hold the map instead; a labelled issue works everywhere.)
|
||||||
|
- **Child ticket**: an issue carrying `Part of #<map>` at the top of its description and labels `wayfinder:<type>` (`research`/`prototype`/`grilling`/`task`). Once claimed, the ticket is assigned to the driving dev.
|
||||||
|
- **Blocking**: GitLab's **native blocking link** — the canonical, UI-visible representation. Add it with the `/blocked_by #<n>` quick action, posted as a note (`glab issue note <child> --message "/blocked_by #<blocker>"`). Native blocking links are a Premium/Ultimate feature; on the free tier (or where unavailable) fall back to a `Blocked by: #<n>, #<n>` line at the top of the description. A ticket is unblocked when every blocker is closed.
|
||||||
|
- **Frontier query**: `glab issue list -F json` scoped to the map's children, drop any with an open blocker — a native `blocked_by` link to an open issue (`glab api projects/:id/issues/:iid/links`), or an open issue in the `Blocked by` line — or an assignee; first in map order wins.
|
||||||
|
- **Claim**: `glab issue update <n> --assignee @me` — the session's first write.
|
||||||
|
- **Resolve**: `glab issue note <n> --message "<answer>"`, then `glab issue close <n>`, then append a context pointer (gist + link) to the map's Decisions-so-far.
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# In-Progress Skills
|
||||||
|
|
||||||
|
Drafts not yet ready to ship.
|
||||||
|
|
||||||
|
## User-invoked
|
||||||
|
|
||||||
|
- [agent-handoff](agent-handoff/SKILL.md) — Hand the current conversation off to a fresh background agent that picks up the work immediately.
|
||||||
|
- [knowledge-gardener](knowledge-gardener/SKILL.md) — Run vault-aware semantic search, synthesis, note creation, linking, and Zettelkasten workflows for this Obsidian vault.
|
||||||
@@ -17,7 +17,7 @@ Arguments are optional. If provided, they describe what the next session should
|
|||||||
|
|
||||||
### 1. Assess the conversation
|
### 1. Assess the conversation
|
||||||
|
|
||||||
Review what has been done, what remains, and any user-provided focus. Identify existing artifacts that capture the work so far (PRDs, plans, ADRs, issues, commits, diffs) so they can be referenced rather than duplicated.
|
Review what has been done, what remains, and any user-provided focus. Identify existing artifacts that capture the work so far (PRDs/specs, plans, ADRs, issues/tickets, commits, diffs) so they can be referenced rather than duplicated.
|
||||||
|
|
||||||
If the user passed arguments, treat them as the priority or scope for the next session.
|
If the user passed arguments, treat them as the priority or scope for the next session.
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ Write a summary of the current state so a fresh agent can continue the work with
|
|||||||
- **Next steps** — what needs to be done next, in priority order
|
- **Next steps** — what needs to be done next, in priority order
|
||||||
- **Open questions** — decisions still needed, unknowns, trade-offs
|
- **Open questions** — decisions still needed, unknowns, trade-offs
|
||||||
- **Suggested skills** — a bullet list of skills the next agent should invoke (e.g. `/tdd`, `/code-review`)
|
- **Suggested skills** — a bullet list of skills the next agent should invoke (e.g. `/tdd`, `/code-review`)
|
||||||
- **References** — paths or URLs to existing artifacts (PRDs, plans, ADRs, issues, commits, diffs). Do **not** duplicate their content — reference them.
|
- **References** — paths or URLs to existing artifacts (PRDs/specs, plans, ADRs, issues/tickets, commits, diffs). Do **not** duplicate their content — reference them.
|
||||||
|
|
||||||
The summary is a **compass**, not a copy: it points the next agent where to go, it does not replay where you've been. Any content already captured in the referenced artifacts does not belong here.
|
The summary is a **compass**, not a copy: it points the next agent where to go, it does not replay where you've been. Any content already captured in the referenced artifacts does not belong here.
|
||||||
|
|
||||||
|
|||||||
@@ -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,106 @@
|
|||||||
|
---
|
||||||
|
name: knowledge-gardener
|
||||||
|
description: Run vault-aware semantic search, synthesis, note creation, linking, and Zettelkasten workflows for this Obsidian vault.
|
||||||
|
metadata:
|
||||||
|
vault_style: para-plus-zettelkasten
|
||||||
|
default_write_folder: Inbox
|
||||||
|
daily_folder: Dailies
|
||||||
|
templates_folder: Templates
|
||||||
|
disable-model-invocation: true
|
||||||
|
---
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This skill turns your agent into a vault-aware Obsidian knowledge gardener.
|
||||||
|
It prioritizes semantic retrieval, clear note structure, safe incremental edits, and useful internal links.
|
||||||
|
|
||||||
|
## Vault Conventions
|
||||||
|
|
||||||
|
- Preserve folder casing and names used in this vault: `Inbox/`, `Dailies/`, `projects/`, `resources/`, `archive/`, `Templates/`.
|
||||||
|
- Use Obsidian wikilinks: `[[Note Title]]`.
|
||||||
|
- Keep existing frontmatter schema compatible with existing notes:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
id: <slug-or-date-id>
|
||||||
|
aliases: []
|
||||||
|
tags: []
|
||||||
|
area: ""
|
||||||
|
project: ""
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
- Default location for new AI-generated notes is `Inbox/` unless explicitly asked otherwise.
|
||||||
|
|
||||||
|
## Core Workflows
|
||||||
|
|
||||||
|
1. Semantic Search
|
||||||
|
- Use `python tools/obsidian_semantic_query.py --vault-root . --query "..." --k 12`.
|
||||||
|
- Return ranked notes with short relevance rationale.
|
||||||
|
|
||||||
|
2. Summarization
|
||||||
|
- Retrieve nearest notes first, then synthesize.
|
||||||
|
- Include conflicts, unknowns, and suggested next notes.
|
||||||
|
|
||||||
|
3. New Note Creation
|
||||||
|
- Create with canonical frontmatter.
|
||||||
|
- Include `## Summary` and optional scaffold sections.
|
||||||
|
- If a summary is provided, include it verbatim under `## Summary`.
|
||||||
|
|
||||||
|
4. Automatic Linking
|
||||||
|
- Suggest links from semantically related notes.
|
||||||
|
- Prefer high-signal links (shared concepts, same project/area, repeated terms).
|
||||||
|
|
||||||
|
5. Refactor to Atomic Notes
|
||||||
|
- Split long mixed-topic notes into smaller notes.
|
||||||
|
- Keep parent note as a structure note and link to children.
|
||||||
|
|
||||||
|
6. Metadata Maintenance
|
||||||
|
- Keep `id`, `aliases`, `tags`, `area`, `project` valid.
|
||||||
|
- Suggest tags from note content; avoid noisy tag spam.
|
||||||
|
|
||||||
|
7. Research Capture
|
||||||
|
- Store source summary in vault.
|
||||||
|
- Extract key claims, evidence, confidence, and follow-up questions.
|
||||||
|
- Distinguish raw source claims, agent synthesis, user opinions, and open questions.
|
||||||
|
- Convert durable insights into permanent notes.
|
||||||
|
|
||||||
|
8. Compiled Wiki Maintenance
|
||||||
|
- For bounded research topics, maintain a Karpathy-style compiled wiki layer: immutable raw sources → maintained concept/claim/synthesis notes → retrievable answers.
|
||||||
|
- On ingest, update existing pages before creating duplicates; the graph should get denser, not just larger.
|
||||||
|
- File durable query answers back into notes, then add or update links from indexes/MOCs.
|
||||||
|
- Keep a lightweight log of ingests, filed answers, lint passes, promotions, and major corrections when the folder has a `Log.md` or equivalent.
|
||||||
|
|
||||||
|
9. Retrieval Lint
|
||||||
|
- Check for unsupported claims, stale or contradictory claims, orphan notes, missing backlinks, missing glossary terms, and unanswered questions.
|
||||||
|
- Verify a future agent can answer the main question from the maintained notes without rereading raw sources.
|
||||||
|
|
||||||
|
10. Packet Promotion
|
||||||
|
- Treat research packets as incubators and the main vault as the indexed library.
|
||||||
|
- Promote notes only when they are reusable beyond the packet, stand alone, have evidence/provenance, and connect to existing vault concepts.
|
||||||
|
- Leave a link behind in the packet and update relevant indexes/MOCs.
|
||||||
|
|
||||||
|
11. Zettelkasten Conversion
|
||||||
|
- Convert source note into atomic permanent notes.
|
||||||
|
- Add explicit links and one short structure note (MOC-lite) when useful.
|
||||||
|
|
||||||
|
## Quality Guardrails
|
||||||
|
|
||||||
|
- Never delete user content unless explicitly requested.
|
||||||
|
- Prefer additive edits and clear section boundaries.
|
||||||
|
- Keep writing concise and skimmable.
|
||||||
|
- Keep tags focused and reusable.
|
||||||
|
- Keep citations close to claims; do not let synthesized notes obscure source provenance.
|
||||||
|
- Before finalizing research edits, run a retrieval check: likely future questions should have obvious entry points through indexes, links, claims, or glossary terms.
|
||||||
|
- Rebuild semantic index after major note creation/refactor sessions.
|
||||||
|
|
||||||
|
## Operational Commands
|
||||||
|
|
||||||
|
- Build index: `python tools/obsidian_semantic_index.py --vault-root .`
|
||||||
|
- Query index: `python tools/obsidian_semantic_query.py --vault-root . --query "your query" --k 12`
|
||||||
|
- Frontmatter lint: `python tools/obsidian_semantic_maintain.py lint --vault-root .`
|
||||||
|
- Related notes: `python tools/obsidian_semantic_maintain.py related --vault-root . --note "Inbox/your-note.md" --k 8`
|
||||||
|
|
||||||
|
## Trigger Hints
|
||||||
|
|
||||||
|
Load this skill when the user asks to search, summarize, connect, refactor, or organize Obsidian notes.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "Knowledge Gardener"
|
||||||
|
short_description: "Manage and curate knowledge for the agent"
|
||||||
|
policy:
|
||||||
|
allow_implicit_invocation: false
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Misc Skills
|
||||||
|
|
||||||
|
Kept around but rarely used.
|
||||||
|
|
||||||
|
## User-invoked
|
||||||
|
|
||||||
|
- [tmux-launch-agent](tmux-launch-agent/SKILL.md) — Fork a new agent CLI session into a new tmux window, detected from the current agent.
|
||||||
@@ -6,52 +6,26 @@ disable-model-invocation: true
|
|||||||
|
|
||||||
## Agent CLI Seed Data
|
## 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.
|
4. **Build command** — Assemble the inner command:
|
||||||
|
- Start with `<binary>`.
|
||||||
3. **Parse flags** — Scan the user's arguments for optional flags (which must come before the prompt text):
|
- If model specified: append `<modelflag> <model-name>`.
|
||||||
- `--name <title>` or `-n <title>` — the tmux window title. Extract the title and remove the flag and its value from the arguments list.
|
- If prompt exists and `args` contains `{prompt}`: write to `/tmp/`, substitute path for `{prompt}`.
|
||||||
- `-c <path>` — the working directory for the new window. Extract the path and remove the flag and its value from the arguments list.
|
- If prompt exists and `args` is empty: pipe via `echo`.
|
||||||
|
- If no prompt: launch bare.
|
||||||
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.
|
|
||||||
|
|
||||||
|
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
|
- name: pi
|
||||||
binary: pi
|
binary: pi
|
||||||
args: "@{prompt}"
|
args: "@{prompt}"
|
||||||
|
modelflag: "--model"
|
||||||
description: "My primary agent harness. Accepts prompt file via {prompt}."
|
description: "My primary agent harness. Accepts prompt file via {prompt}."
|
||||||
|
|
||||||
- name: opencode
|
- name: opencode
|
||||||
binary: opencode
|
binary: opencode
|
||||||
args: ""
|
args: ""
|
||||||
|
modelflag: "-m"
|
||||||
description: "OpenCode agent. Pipes stdin via cat."
|
description: "OpenCode agent. Pipes stdin via cat."
|
||||||
note: "Uses stdin piping: args must be empty, prompt via pipe."
|
note: "Uses stdin piping: args must be empty, prompt via pipe."
|
||||||
|
|
||||||
- name: goose
|
- name: goose
|
||||||
binary: goose
|
binary: goose
|
||||||
args: ""
|
args: ""
|
||||||
|
modelflag: "--model"
|
||||||
description: "Goose agent. Accepts prompt file via -i flag."
|
description: "Goose agent. Accepts prompt file via -i flag."
|
||||||
note: "Uses stdin piping: args must be empty, prompt via pipe."
|
note: "Uses stdin piping: args must be empty, prompt via pipe."
|
||||||
|
|
||||||
- name: codex
|
- name: codex
|
||||||
binary: codex
|
binary: codex
|
||||||
args: "@{prompt}"
|
args: "@{prompt}"
|
||||||
|
modelflag: "-m"
|
||||||
description: "OpenAI Codex. Accepts prompt file via {prompt}."
|
description: "OpenAI Codex. Accepts prompt file via {prompt}."
|
||||||
|
|
||||||
- name: claude
|
- name: claude
|
||||||
binary: claude
|
binary: claude
|
||||||
args: ""
|
args: ""
|
||||||
|
modelflag: "--model"
|
||||||
description: "Anthropic Claude CLI. Pipes stdin via cat."
|
description: "Anthropic Claude CLI. Pipes stdin via cat."
|
||||||
note: "Uses stdin piping: args must be empty, prompt via pipe."
|
note: "Uses stdin piping: args must be empty, prompt via pipe."
|
||||||
```
|
```
|
||||||
@@ -36,9 +41,10 @@ agents:
|
|||||||
## Field meanings
|
## Field meanings
|
||||||
|
|
||||||
| Field | Description |
|
| Field | Description |
|
||||||
|-------|-------------|
|
| ------- | ------------- |
|
||||||
| `name` | Display name used in menus and `--agent` flag |
|
| `name` | Display name used in menus and `--agent` flag |
|
||||||
| `binary` | Command name expected on PATH |
|
| `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`). |
|
| `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 |
|
| `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 |
|
| `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
|
||||||
Executable
+115
@@ -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
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
# Personal Skills
|
# 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).
|
_No skills currently live in this bucket._
|
||||||
- `forge-preferences` has been moved to [deprecated/forge-preferences](../deprecated/forge-preferences/SKILL.md).
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
# PKM Skills
|
# PKM Skills
|
||||||
|
|
||||||
|
Personal knowledge management.
|
||||||
|
|
||||||
## User-invoked
|
## User-invoked
|
||||||
|
|
||||||
- [conversation-summary](conversation-summary/SKILL.md) — Save the current conversation as a comprehensive report note in your Obsidian vault.
|
- [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.
|
||||||
- [knowledge-gardener](knowledge-gardener/SKILL.md) — Run vault-aware semantic search, synthesis, note creation, linking, and Zettelkasten workflows for this Obsidian vault.
|
- [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.
|
||||||
- [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 both the summary and raw subtitles to the Video bundle in the Obsidian vault.
|
||||||
- [research-vault](research-vault/SKILL.md) — Research a topic through a one-question-at-a-time learning conversation and save a linked Obsidian research packet.
|
|
||||||
|
|
||||||
## Model-invoked
|
## Model-invoked
|
||||||
|
|
||||||
_None yet._
|
- [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
@@ -1,90 +1,67 @@
|
|||||||
---
|
---
|
||||||
name: crit
|
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
|
disable-model-invocation: true
|
||||||
---
|
---
|
||||||
|
|
||||||
## Purpose
|
## 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
|
## Steps
|
||||||
Before generating ideas, establish the foundation:
|
|
||||||
|
|
||||||
1. **Identify the Core Domain**
|
Run these four steps in order when the user invokes `/crit`.
|
||||||
- What field, industry, or subject area?
|
|
||||||
- What are the key constraints (time, resources, technical limitations)?
|
|
||||||
- Who is the target audience and their expertise level?
|
|
||||||
|
|
||||||
2. **Assess Current State**
|
### 1. Context — Give the AI your world
|
||||||
- What problems or opportunities exist?
|
|
||||||
- What has been tried before (if applicable)?
|
|
||||||
- What resources are available?
|
|
||||||
|
|
||||||
**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
|
Capture the answer in one paragraph. More detail is better.
|
||||||
Structure the brainstorming request:
|
|
||||||
|
|
||||||
1. **Define the Specific Goal**
|
**Completion criterion**: One paragraph covering identity, goal, audience, and
|
||||||
- What concrete outcome do you want?
|
constraints — confirmed by the user.
|
||||||
- What success criteria will be used?
|
|
||||||
- What is the expected timeline?
|
|
||||||
|
|
||||||
2. **Gather Input Requirements**
|
### 2. Role — Assign a viewpoint
|
||||||
- What information is needed to proceed?
|
|
||||||
- What assumptions should be validated?
|
|
||||||
- What data or resources are required?
|
|
||||||
|
|
||||||
**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
|
Guide toward a specific lens — "strategy coach who uncovers blind spots,"
|
||||||
Generate diverse, high-quality ideas:
|
"editor who cuts fluff," "architect who finds leverage points." Not "be
|
||||||
|
helpful."
|
||||||
|
|
||||||
1. **Divergent Thinking Phase**
|
**Completion criterion**: A single sentence assigning a named role that implies
|
||||||
- Generate 5-10 initial concepts without judgment
|
a specific viewpoint.
|
||||||
- Apply different perspectives (technical, business, user experience)
|
|
||||||
- Include both obvious and unconventional options
|
|
||||||
|
|
||||||
2. **Convergent Analysis Phase**
|
### 3. Interview — One question at a time
|
||||||
- Evaluate each idea against success criteria
|
|
||||||
- Score ideas on feasibility, impact, and alignment
|
|
||||||
- Identify patterns and synergies between ideas
|
|
||||||
|
|
||||||
**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
|
Ask one question. Wait for the answer. Then ask the next. Max three. Do not
|
||||||
Improve selected ideas:
|
batch them.
|
||||||
|
|
||||||
1. **Select Top Candidates**
|
This step forces the user to slow down and think, and teaches the AI what
|
||||||
- Choose 2-3 ideas with highest potential
|
actually matters.
|
||||||
- Detail implementation approach for each
|
|
||||||
- Identify risks and mitigation strategies
|
|
||||||
|
|
||||||
2. **Develop Action Plans**
|
**Completion criterion**: 1-3 questions asked and answered, one at a time.
|
||||||
- Break down into concrete steps
|
Stop asking when the user signals readiness or you've asked three.
|
||||||
- Assign priorities and dependencies
|
|
||||||
- Define success metrics and checkpoints
|
|
||||||
|
|
||||||
**Completion Criterion**: 2-3 refined ideas with detailed action plans.
|
### 4. Task — Issue the assignment
|
||||||
|
|
||||||
### Step 5: Tone & Delivery
|
Ask the user: "What's the task?"
|
||||||
Adapt communication to the audience:
|
|
||||||
|
|
||||||
1. **Choose Appropriate Role**
|
Guide toward a short, clear, slightly uncomfortable prompt that asks the AI to
|
||||||
- Subject Matter Expert for technical depth
|
*think*, not just write. Reference the preceding interview.
|
||||||
- Consultant for strategic guidance
|
|
||||||
- Teacher for complex concepts
|
|
||||||
- Collaborator for co-creation
|
|
||||||
- Analyst for multi-perspective evaluation
|
|
||||||
|
|
||||||
2. **Structure Response**
|
> "Based on our conversation, give me three non-obvious actions I can take.
|
||||||
- Lead with clear, actionable solutions
|
> Make them surprising but realistic."
|
||||||
- Organize information logically and concisely
|
|
||||||
- Include examples or analogies for clarity
|
|
||||||
- Suggest next steps and follow-up questions
|
|
||||||
|
|
||||||
**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.
|
||||||
|
|||||||
@@ -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,80 @@
|
|||||||
|
---
|
||||||
|
name: youtube-video-capture
|
||||||
|
description: 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. Use when the user wants to capture a YouTube video, mentions "summarize this video", "capture this talk", or pastes a YouTube URL wanting it saved to vault.
|
||||||
|
disable-model-invocation: true
|
||||||
|
---
|
||||||
|
|
||||||
|
Capture a YouTube video into the Obsidian vault. Extract subtitles, produce a summary, and save both to the `Video/` bundle as OKF-conformant notes.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
`yt-dlp` must be installed. Check with `which yt-dlp`. If missing, tell the user to install it (`pip install yt-dlp` or `brew install yt-dlp`) and stop.
|
||||||
|
|
||||||
|
## Extract subtitles
|
||||||
|
|
||||||
|
Run `yt-dlp` to download auto-generated English subtitles as SRT:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yt-dlp --write-auto-subs --sub-lang en-orig --convert-subs srt --skip-download -o "/tmp/yt-subs-%(id)s.%(ext)s" "<url>"
|
||||||
|
```
|
||||||
|
|
||||||
|
If no subtitles are available, tell the user and stop.
|
||||||
|
|
||||||
|
Read the SRT file. Strip timestamps and sequence numbers to produce clean, contiguous text for summarization.
|
||||||
|
|
||||||
|
## Summarize
|
||||||
|
|
||||||
|
Produce a prose summary from the full subtitle text. Synthesize the content — do not regurgitate the transcript. Then extract **key takeaways** as a bulleted list.
|
||||||
|
|
||||||
|
## Write to vault
|
||||||
|
|
||||||
|
The vault root is `/home/sjb/Documents/sjb-brain/`. The `Video/` bundle is an OKF bundle. Create the directory if it doesn't exist, with an `index.md` and `log.md` following the pattern of other bundles in the vault.
|
||||||
|
|
||||||
|
### Filenames
|
||||||
|
|
||||||
|
- Summary: `YYYY-MM-DD_HH-mm_<kebab-case-title>.md`
|
||||||
|
- Subtitle: `YYYY-MM-DD_HH-mm_<kebab-case-title>.srt`
|
||||||
|
|
||||||
|
Use the video title, kebab-cased.
|
||||||
|
|
||||||
|
### Summary note
|
||||||
|
|
||||||
|
Write a markdown note with OKF v0.1 frontmatter:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
id: <kebab-case-title>
|
||||||
|
type: Video Summary
|
||||||
|
aliases: []
|
||||||
|
tags: [video, youtube, <content-derived-tags>]
|
||||||
|
area: <derived-from-content-or-empty>
|
||||||
|
project: ''
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
Body:
|
||||||
|
|
||||||
|
- Video title as the note's `#` heading
|
||||||
|
- Link to the original YouTube URL
|
||||||
|
- The prose summary
|
||||||
|
- `## Key Takeaways` with bulleted list
|
||||||
|
- `## Subtitle` with a markdown link to the `.srt` file
|
||||||
|
|
||||||
|
Use standard markdown links, never `[[wikilinks]]`.
|
||||||
|
|
||||||
|
### Subtitle file
|
||||||
|
|
||||||
|
Copy the SRT file into `Video/` alongside the summary. No frontmatter.
|
||||||
|
|
||||||
|
### Bundle maintenance
|
||||||
|
|
||||||
|
After writing the pair:
|
||||||
|
|
||||||
|
1. Update `Video/index.md` — add the new summary under a `## Flat Notes` or appropriate heading, with a one-line description.
|
||||||
|
2. Append an entry to `Video/log.md` noting the addition with a timestamp.
|
||||||
|
|
||||||
|
### Safety
|
||||||
|
|
||||||
|
- If a filename collides, append a numeric suffix.
|
||||||
|
- Redact likely credentials, secrets, or tokens from any inline content.
|
||||||
|
- If the video has no subtitles, stop — do not attempt to transcribe.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "Youtube Video Capture"
|
||||||
|
short_description: "Capture a youtube video"
|
||||||
|
policy:
|
||||||
|
allow_implicit_invocation: false
|
||||||
@@ -1,11 +1,5 @@
|
|||||||
# Productivity Skills
|
# Productivity Skills
|
||||||
|
|
||||||
## User-invoked
|
Daily non-code workflow tools.
|
||||||
|
|
||||||
- [grill-me](grill-me/SKILL.md) — A relentless interview to sharpen a plan or design.
|
_No skills currently live in this bucket._
|
||||||
- [handoff](handoff/SKILL.md) — Compact the current conversation into a handoff document for another agent to pick up.
|
|
||||||
- [writing-great-skills](writing-great-skills/SKILL.md) — Reference for writing and editing skills well — the vocabulary and principles that make a skill predictable.
|
|
||||||
|
|
||||||
## Model-invoked
|
|
||||||
|
|
||||||
- [grilling](grilling/SKILL.md) — Interview the user relentlessly about a plan or design.
|
|
||||||
|
|||||||
@@ -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
|
## 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
|
## 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:
|
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.
|
- **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`.
|
- **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`.
|
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`.
|
||||||
@@ -32,3 +32,14 @@ Create a Gitea issue.
|
|||||||
## When a skill says "fetch the relevant ticket"
|
## When a skill says "fetch the relevant ticket"
|
||||||
|
|
||||||
Run `tea issue <number> --comments`.
|
Run `tea issue <number> --comments`.
|
||||||
|
|
||||||
|
## Wayfinding operations
|
||||||
|
|
||||||
|
Used by `/wayfinder`. The **map** is a single issue with **child** issues as tickets.
|
||||||
|
|
||||||
|
- **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-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 comment <n> "<answer>"`, then `tea issue close <n>`, then append a context pointer (gist + link) to the map's Decisions-so-far.
|
||||||
|
|||||||
@@ -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,14 +2,14 @@
|
|||||||
|
|
||||||
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.
|
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 |
|
| Label in skills | Label in our tracker | Meaning |
|
||||||
| -------------------------- | -------------------- | ---------------------------------------- |
|
| ----------------- | -------------------- | ---------------------------------------- |
|
||||||
| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
|
| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
|
||||||
| `needs-info` | `needs-info` | Waiting on reporter for more information |
|
| `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-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
|
||||||
| `ready-for-human` | `ready-for-human` | Requires human implementation |
|
| `ready-for-human` | `ready-for-human` | Requires human implementation |
|
||||||
| `in-progress` | `in-progress` | Being actively worked on by a human or agents |
|
| `in-progress` | `in-progress` | Being actively worked on by a human or agents |
|
||||||
| `wontfix` | `wontfix` | Will not be actioned |
|
| `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. Only one triage label should be active.
|
When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table. Only one triage label should be active.
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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.
|
||||||
@@ -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",
|
||||||
|
]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from .cli import main # type: ignore[reportMissingImports]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -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
@@ -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())
|
||||||
@@ -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})
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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("{}")
|
||||||
@@ -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)
|
||||||
@@ -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",
|
||||||
|
]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from .cli import main # type: ignore[reportMissingImports]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from tracker.cli import build_parser, main # type: ignore[reportMissingImports]
|
||||||
|
|
||||||
|
__all__ = ["build_parser", "main"]
|
||||||
Reference in New Issue
Block a user