Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dead6ca2bb | ||
|
|
f65a6ec812 | ||
|
|
7dc4758e9a | ||
|
|
07198e9aad |
@@ -1,6 +1,6 @@
|
||||
# Project Context Pack
|
||||
|
||||
Generated: 2026-07-16
|
||||
Generated: 2026-08-17
|
||||
Root: /home/sjb/Projects/personal/ws-sjb-skills/wt-master
|
||||
Working directory: .
|
||||
Status: fresh
|
||||
@@ -11,10 +11,10 @@ A collection of agent skills (slash commands and behaviors) loaded into Steve Be
|
||||
|
||||
## Project type
|
||||
|
||||
- **Agent skill repository** — markdown-defined agent instructions
|
||||
- Languages: Markdown (100%), one Bash script (detect-agent), one shell script (tmux-open)
|
||||
- Package managers: none
|
||||
- Build/test tools: none
|
||||
- **Agent skill repository plus standalone Python tracker package**
|
||||
- Languages: Python and Markdown, one Bash script (detect-agent), one shell script (tmux-open)
|
||||
- Package manager: `pyproject.toml`
|
||||
- Build/test tools: `python -m unittest discover`
|
||||
- Agent guidance: `AGENTS.md` at root, `docs/invocation.md` for invocation conventions, `docs/agents/` for issue tracker / triage labels / ADR wiki / domain docs
|
||||
|
||||
## Structure
|
||||
@@ -34,6 +34,10 @@ A collection of agent skills (slash commands and behaviors) loaded into Steve Be
|
||||
│ │ ├── issue-tracker.md # Gitea issue tracker docs
|
||||
│ │ └── triage-labels.md # Five-label triage vocabulary
|
||||
│ └── adr/ # ADR wiki clone (gitignored)
|
||||
├── tracker/ # Provider-neutral Python CLI/library
|
||||
├── tracker_automation/ # Compatibility import name
|
||||
├── tests/ # Public operation-boundary tests
|
||||
├── pyproject.toml # Package metadata and console scripts
|
||||
└── common/
|
||||
├── README.md # Lists all common skills by invocation type
|
||||
├── engineering/ # Model-invoked: lsp-code-analysis, pkm-curation; User-invoked: commit-staged, implement-issue, project-context-pack, setup-skills
|
||||
@@ -57,10 +61,10 @@ A collection of agent skills (slash commands and behaviors) loaded into Steve Be
|
||||
|
||||
## Commands
|
||||
|
||||
- Build: none
|
||||
- Test: none
|
||||
- Lint/typecheck: none
|
||||
- Run/dev: skills are invoked by AI agents — no server or dev command
|
||||
- Build/package: `python -m pip install .`
|
||||
- Test: `python -m unittest discover -v`
|
||||
- Typecheck: `lsp_diagnostics` on `tracker/` and `tests/`
|
||||
- Run: `python -m tracker` or installed `tracker`
|
||||
|
||||
## Entry points
|
||||
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
docs/adr/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
@@ -32,7 +32,7 @@ Every `SKILL.md` is either user-invoked (`disable-model-invocation: true`, reach
|
||||
|
||||
### Issue tracker
|
||||
|
||||
Issues are tracked in Gitea on gitea.sagacity.ca. See `docs/agents/issue-tracker.md`.
|
||||
Issues are tracked in Gitea on gitea.sagacity.ca; use the provider-neutral `tracker` package for normal automation. See `docs/agents/issue-tracker.md` and `docs/agents/tracker.md`.
|
||||
|
||||
### Triage labels
|
||||
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
# Skills
|
||||
|
||||
A collection of agent skills (slash commands and behaviors) loaded into my agent.
|
||||
Agent skills (slash commands and behaviors) loaded into my agent.
|
||||
|
||||
## Tracker automation
|
||||
|
||||
This repo also ships the standalone provider-neutral `tracker` CLI/library. See [`docs/agents/tracker.md`](docs/agents/tracker.md) and [`tracker/README.md`](tracker/README.md) for migration guidance.
|
||||
|
||||
## User-invoked
|
||||
|
||||
- [agent-handoff](common/in-progress/agent-handoff/SKILL.md) — Hand the current conversation off to a fresh background agent that picks up the work immediately.
|
||||
- [audio-product-dsp](common/deprecated/audio-production-dispatcher/SKILL.md) — Dispatch audio product DSP hardware/software engineering requests to the best specialist workflow with measurable product-focused outputs.
|
||||
- [commit-staged](common/engineering/commit-staged/SKILL.md) — Commit staged files with a conventional commit message.
|
||||
- [conversation-summary](common/pkm/conversation-summary/SKILL.md) — Save the current conversation as a comprehensive report note in your Obsidian vault, following OKF v0.1 conventions.
|
||||
- [crit](common/pkm/crit/SKILL.md) — Brainstorm with AI using the CRIT framework to generate and evaluate ideas.
|
||||
- [forge-router](common/deprecated/forge-router/SKILL.md) — High-level guidance for choosing the right forge skill.
|
||||
- [implement](common/engineering/implement/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 piece of work based on a PRD or set of issues.
|
||||
- [crit](common/pkm/crit/SKILL.md) — Run the CRIT framework — give the AI Context, assign it a Role, let it Interview you one question at a time, then issue the Task.
|
||||
- [implement-isolation](common/engineering/implement-isolation/SKILL.md) — Implement a piece of work based on a spec or set of tickets in isolation.
|
||||
- [implement-isolation-tmux](common/engineering/implement-isolation-tmux/SKILL.md) — Dispatch a child agent in an isolated git worktree to implement a piece of work based on a PRD or set of issues.
|
||||
- [knowledge-gardener](common/in-progress/knowledge-gardener/SKILL.md) — Run vault-aware semantic search, synthesis, note creation, linking, and Zettelkasten workflows for this Obsidian vault.
|
||||
- [project-context-pack](common/engineering/project-context-pack/SKILL.md) — Use when the user wants a bounded repo context pack, project map, codebase index, or cached memory file so later work uses fd/rg/tree-sitter/LSP instead of repeated browsing.
|
||||
- [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.
|
||||
- [project-context-pack](common/engineering/project-context-pack/SKILL.md) — Build a bounded repo context pack (project map, codebase index, cached memory file) so later work uses fd/rg/tree-sitter/LSP instead of repeated browsing.
|
||||
- [research-vault](common/pkm/research-vault/SKILL.md) — Research a topic through a one-question-at-a-time learning conversation, answer directly, share resources when useful, and save a linked OKF-conformant research packet in the Obsidian vault.
|
||||
- [setup-skills](common/engineering/setup-skills/SKILL.md) — Configure this repo for the engineering skills, set up its issue tracker, triage label vocabulary, and domain doc layout. Run once before first use of the other engineering skills.
|
||||
- [tmux-launch-agent](common/misc/tmux-launch-agent/SKILL.md) — Fork a new agent CLI session into a new tmux window, detected from the current agent.
|
||||
@@ -22,8 +23,5 @@ A collection of agent skills (slash commands and behaviors) loaded into my agent
|
||||
|
||||
## Model-invoked
|
||||
|
||||
- [forge-gitea](common/deprecated/forge-gitea/SKILL.md) — Use the Gitea CLI (`tea`) to interact with Gitea issues, pull requests, releases, CI, and repository state.
|
||||
- [forge-github](common/deprecated/forge-github/SKILL.md) — Use the GitHub CLI (`gh`) to interact with GitHub issues, pull requests, releases, CI, and repository state.
|
||||
- [forge-interaction](common/deprecated/forge-interaction/SKILL.md) — Use when the user wants forge work such as opening a PR, creating or listing issues, checking CI, looking at the repo, pushing a branch, publishing changes, or making a release on GitHub or Gitea. This skill now delegates to specialized skills for better predictability.
|
||||
- [forge-preferences](common/deprecated/forge-preferences/SKILL.md) — Use with forge-interaction to apply Steve's personal or project-specific GitHub/Gitea remote, CLI, issue, PR, and release preferences.
|
||||
- [lsp-code-analysis](common/engineering/lsp-code-analysis/SKILL.md) — Semantic code analysis via LSP. Navigate code (definitions, references, implementations), search symbols, preview refactorings, and get file outlines. Use for exploring unfamiliar codebases or performing safe refactoring.
|
||||
- [pkm-curation](common/pkm/pkm-curation/SKILL.md) — Curate an Obsidian vault — classify notes, normalize frontmatter, add links, extract atomic notes. Use when curating, batch-processing, reviewing, or doing a serendipity pick.
|
||||
|
||||
+5
-11
@@ -5,16 +5,13 @@ Skills that work in all CLI agents.
|
||||
## User-invoked
|
||||
|
||||
- [agent-handoff](in-progress/agent-handoff/SKILL.md) — Hand the current conversation off to a fresh background agent that picks up the work immediately.
|
||||
- [audio-product-dsp](deprecated/audio-production-dispatcher/SKILL.md) — Dispatch audio product DSP hardware/software engineering requests to the best specialist workflow with measurable product-focused outputs.
|
||||
- [commit-staged](engineering/commit-staged/SKILL.md) — Commit staged files with a conventional commit message.
|
||||
- [conversation-summary](pkm/conversation-summary/SKILL.md) — Save the current conversation as a comprehensive report note in your Obsidian vault, following OKF v0.1 conventions.
|
||||
- [crit](pkm/crit/SKILL.md) — Brainstorm with AI using the CRIT framework to generate and evaluate ideas.
|
||||
- [forge-router](deprecated/forge-router/SKILL.md) — High-level guidance for choosing the right forge skill.
|
||||
- [implement](engineering/implement/SKILL.md) — Implement a piece of work based on a spec or set of tickets in isolation.
|
||||
- [implement-issue](engineering/implement-issue/SKILL.md) — Dispatch a child agent in an isolated git worktree to implement a piece of work based on a PRD or set of issues.
|
||||
- [crit](pkm/crit/SKILL.md) — Run the CRIT framework — give the AI Context, assign it a Role, let it Interview you one question at a time, then issue the Task.
|
||||
- [implement-isolation](engineering/implement-isolation/SKILL.md) — Implement a piece of work based on a spec or set of tickets in isolation.
|
||||
- [implement-isolation-tmux](engineering/implement-isolation-tmux/SKILL.md) — Dispatch a child agent in an isolated git worktree to implement a piece of work based on a PRD or set of issues.
|
||||
- [knowledge-gardener](in-progress/knowledge-gardener/SKILL.md) — Run vault-aware semantic search, synthesis, note creation, linking, and Zettelkasten workflows for this Obsidian vault.
|
||||
- [project-context-pack](engineering/project-context-pack/SKILL.md) — Use when the user wants a bounded repo context pack, project map, codebase index, or cached memory file so later work uses fd/rg/tree-sitter/LSP instead of repeated browsing.
|
||||
- [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.
|
||||
- [project-context-pack](engineering/project-context-pack/SKILL.md) — Build a bounded repo context pack (project map, codebase index, cached memory file) so later work uses fd/rg/tree-sitter/LSP instead of repeated browsing.
|
||||
- [research-vault](pkm/research-vault/SKILL.md) — Research a topic through a one-question-at-a-time learning conversation, answer directly, share resources when useful, and save a linked OKF-conformant research packet in the Obsidian vault.
|
||||
- [setup-skills](engineering/setup-skills/SKILL.md) — Configure this repo for the engineering skills, set up its issue tracker, triage label vocabulary, and domain doc layout. Run once before first use of the other engineering skills.
|
||||
- [tmux-launch-agent](misc/tmux-launch-agent/SKILL.md) — Fork a new agent CLI session into a new tmux window, detected from the current agent.
|
||||
@@ -22,8 +19,5 @@ Skills that work in all CLI agents.
|
||||
|
||||
## Model-invoked
|
||||
|
||||
- [forge-gitea](deprecated/forge-gitea/SKILL.md) — Work with Gitea repositories, issues, pull requests, releases, and CI.
|
||||
- [forge-github](deprecated/forge-github/SKILL.md) — Work with GitHub repositories, issues, pull requests, releases, and CI.
|
||||
- [forge-interaction](deprecated/forge-interaction/SKILL.md) — Use when the user wants forge work such as opening a PR, creating or listing issues, checking CI, looking at the repo, pushing a branch, publishing changes, or making a release on GitHub or Gitea. This skill now delegates to specialized skills for better predictability.
|
||||
- [forge-preferences](deprecated/forge-preferences/SKILL.md) — Use with forge-interaction to apply Steve's personal or project-specific GitHub/Gitea remote, CLI, issue, PR, and release preferences.
|
||||
- [lsp-code-analysis](engineering/lsp-code-analysis/SKILL.md) — Semantic code analysis via LSP. Navigate code (definitions, references, implementations), search symbols, preview refactorings, and get file outlines. Use for exploring unfamiliar codebases or performing safe refactoring.
|
||||
- [pkm-curation](pkm/pkm-curation/SKILL.md) — Curate an Obsidian vault — classify notes, normalize frontmatter, add links, extract atomic notes. Use when curating, batch-processing, reviewing, or doing a serendipity pick.
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
# Deprecated Skills
|
||||
|
||||
No longer used.
|
||||
|
||||
## 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.
|
||||
- [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-router](forge-router/SKILL.md) — High-level guidance for choosing the right forge skill.
|
||||
|
||||
## Model-invoked
|
||||
|
||||
- [forge-gitea](forge-gitea/SKILL.md) — Work with Gitea repositories, issues, pull requests, releases, and CI.
|
||||
- [forge-github](forge-github/SKILL.md) — Work with GitHub repositories, issues, pull requests, releases, and CI.
|
||||
- [forge-interaction](forge-interaction/SKILL.md) — Use when the user wants forge work such as opening a PR, creating or listing issues, checking CI, looking at the repo, pushing a branch, publishing changes, or making a release on GitHub or Gitea. This skill now delegates to specialized skills for better predictability.
|
||||
- [forge-preferences](forge-preferences/SKILL.md) — Use with forge-interaction to apply Steve's personal or project-specific GitHub/Gitea remote, CLI, issue, PR, and release preferences.
|
||||
@@ -1,180 +0,0 @@
|
||||
---
|
||||
disable-model-invocation: true
|
||||
name: audio-product-dsp
|
||||
description: Dispatch audio product DSP hardware/software engineering requests to the best specialist workflow with measurable product-focused outputs
|
||||
---
|
||||
|
||||
Role: You are a dispatcher skill for audio product DSP research and engineering. You route requests to the right specialist path(s), enforce product constraints, and return one decision-ready answer.
|
||||
|
||||
Primary objectives:
|
||||
- Classify audio product requests across algorithm, embedded implementation, hardware integration, tuning, and validation.
|
||||
- Route to the best specialist workflow(s) using explicit scoring.
|
||||
- Deliver outputs tied to user-perceived quality, latency, power, and manufacturable constraints.
|
||||
- Keep recommendations testable and release-oriented.
|
||||
|
||||
Scope:
|
||||
- In scope: speech/audio enhancement, ANC, beamforming, AEC/NS/AGC, codec pipelines, loudness/tuning, fixed-point deployment, RT embedded audio, product validation plans.
|
||||
- Out of scope: medical diagnosis claims, regulatory/legal sign-off, unsafe hearing-level recommendations, fabricated bench/listening data.
|
||||
|
||||
Non-goals:
|
||||
- Do not claim audible improvements without metric or listening-test basis.
|
||||
- Do not suggest architecture changes that violate hard latency/power/platform constraints without calling out tradeoffs.
|
||||
- Do not present lab verification as completed if only conceptual.
|
||||
|
||||
Inputs expected:
|
||||
- User request text
|
||||
- Conversation context
|
||||
- Available specialist agents/skills
|
||||
- Product constraints (if available):
|
||||
- device type (earbuds, headset, speakerphone, soundbar, hearing-assist, etc.)
|
||||
- mic/speaker topology
|
||||
- sample rate/frame size
|
||||
- end-to-end latency budget
|
||||
- CPU/MIPS, RAM/flash
|
||||
- battery/power target
|
||||
- codec/transport constraints (BT, USB, VoIP, etc.)
|
||||
- target metrics and UX goals
|
||||
|
||||
Required output contract:
|
||||
- Always provide:
|
||||
1) Selected route
|
||||
2) Why route fits product goals
|
||||
3) Final recommendation
|
||||
4) Assumptions and open risks
|
||||
5) Verification plan (objective + subjective)
|
||||
6) Confidence level
|
||||
|
||||
Dispatch taxonomy (audio product specific):
|
||||
- Voice Quality Path: AEC/NS/AGC, double-talk robustness, far-end preservation, speech intelligibility.
|
||||
- Playback Quality Path: EQ/DRC/loudness, distortion management, clipping avoidance, tonal balance.
|
||||
- Spatial/Array Path: beamforming, DOA, mic calibration sensitivity, wind/noise robustness.
|
||||
- ANC Path: feedforward/feedback/hybrid ANC stability, leakage robustness, fit variance strategy.
|
||||
- Embedded RT Path: buffering, ISR/DMA, frame deadlines, SIMD acceleration, memory bandwidth.
|
||||
- Hardware Integration Path: codec clocks, interfaces, mic bias/noise floor, amp/headroom, thermal limits.
|
||||
- Validation Path: objective metrics, golden references, listening tests, production regression.
|
||||
- Research Synthesis Path: state-of-the-art comparison, feasibility/risk, phased experiment plan.
|
||||
|
||||
Routing policy:
|
||||
1. Parse request into one or more intents.
|
||||
2. Extract success criteria and hard product constraints.
|
||||
3. Score candidate routes:
|
||||
- Relevance (0-5)
|
||||
- Product-fit (0-5)
|
||||
- Feasibility/safety (0-5)
|
||||
- Evidence readiness (0-5)
|
||||
- Implementation cost (0-5, lower is better)
|
||||
4. Select single-route or multi-route orchestration.
|
||||
5. Dispatch structured task packets.
|
||||
6. Reconcile into one release-oriented recommendation.
|
||||
|
||||
Confidence rules:
|
||||
- High: clear winner and all critical constraints known.
|
||||
- Medium: winner exists but one non-critical constraint unknown; proceed with explicit assumptions.
|
||||
- Low: tied routes or missing critical constraint; ask exactly one targeted question.
|
||||
|
||||
Critical constraints checklist:
|
||||
- Product form factor and acoustic topology
|
||||
- Sample rate, frame size, channel count
|
||||
- End-to-end latency budget (capture->process->render)
|
||||
- CPU/MIPS and memory budgets
|
||||
- Power target and thermal envelope
|
||||
- Numeric format (float/fixed word lengths)
|
||||
- UX priority (call clarity, music fidelity, ANC depth, wake-word reliability, etc.)
|
||||
- Acceptance metrics and pass/fail thresholds
|
||||
|
||||
Audio product metrics catalog:
|
||||
- Voice/call: PESQ/POLQA, STOI, ERLE, double-talk performance, barge-in robustness.
|
||||
- Playback: THD+N, frequency response error, max SPL before limiting artifacts, crest-factor handling.
|
||||
- ANC: attenuation vs frequency, residual noise spectra, stability margin, fit-leak sensitivity.
|
||||
- System: RTL latency, glitch/dropout rate, CPU load, memory headroom, battery impact.
|
||||
- Subjective: MUSHRA/AB preference tests, panel notes, artifact taxonomy.
|
||||
|
||||
Safety and integrity gates:
|
||||
- Never fabricate measurements, listening outcomes, or citations.
|
||||
- If hearing safety could be impacted, require explicit level limits and verification steps.
|
||||
- If irreversible hardware actions are requested, require explicit confirmation and safe fallback path.
|
||||
- Protect credentials and proprietary parameters.
|
||||
|
||||
Specialist route mapping:
|
||||
- "Improve call quality" -> Voice Quality + Validation paths
|
||||
- "Reduce earbud power while keeping ANC" -> ANC + Embedded RT + Hardware Integration
|
||||
- "Fix audio glitches" -> Embedded RT + Hardware Integration + Validation
|
||||
- "Compare beamforming methods" -> Spatial/Array + Research Synthesis
|
||||
- "Ship-ready tuning plan" -> Playback/Voice/ANC (as relevant) + Validation
|
||||
|
||||
Task packet format for downstream specialists:
|
||||
```json
|
||||
{
|
||||
"objective": "<single product outcome>",
|
||||
"constraints": {
|
||||
"latency_ms": "<value or unknown>",
|
||||
"cpu_budget": "<value or unknown>",
|
||||
"power_budget": "<value or unknown>",
|
||||
"platform": "<SoC/DSP/MCU>",
|
||||
"sample_rate_hz": "<value>",
|
||||
"frame_size": "<value>
|
||||
"
|
||||
},
|
||||
"required_output": [
|
||||
"Recommended approach",
|
||||
"Why it fits product goals",
|
||||
"Tradeoffs",
|
||||
"Top 3 risks",
|
||||
"Objective metrics to track",
|
||||
"Subjective listening checks",
|
||||
"Implementation next steps"
|
||||
],
|
||||
"limits": [
|
||||
"No fabricated data",
|
||||
"State assumptions explicitly"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Orchestration rules:
|
||||
- Split only when subproblems are independent and interfaces are clear.
|
||||
- Normalize units (ms, dB, Hz, mW, MIPS) and definitions across outputs.
|
||||
- Resolve conflicts by preferring measured evidence > validated simulation > reasoned estimate.
|
||||
- If conflict remains, present it as a decision fork with verification to break the tie.
|
||||
|
||||
Fallback behavior:
|
||||
- If selected specialist fails, retry once with narrower objective and stricter output schema.
|
||||
- If retry fails, route to a generalist technical path and lower confidence.
|
||||
- If critical constraints are missing, provide best-effort baseline + one blocking question.
|
||||
|
||||
Response template:
|
||||
```text
|
||||
Route Selected:
|
||||
- <specialist path(s)>
|
||||
|
||||
Why This Route:
|
||||
- <1-3 product-focused bullets>
|
||||
|
||||
Recommendation:
|
||||
<final user-facing answer>
|
||||
|
||||
Assumptions and Risks:
|
||||
- <bullets>
|
||||
|
||||
Verification Plan:
|
||||
- Objective: <3-7 checks with metrics and thresholds>
|
||||
- Subjective: <2-5 listening test checks>
|
||||
|
||||
Confidence:
|
||||
- <High|Medium|Low> with one-line rationale
|
||||
```
|
||||
|
||||
Clarification template (only when blocked):
|
||||
```text
|
||||
I can dispatch this accurately, but I need one detail:
|
||||
- <single targeted question>
|
||||
|
||||
Default I will assume for speed:
|
||||
- <recommended default>
|
||||
```
|
||||
|
||||
Quality bar:
|
||||
- Product impact over algorithm novelty.
|
||||
- Verifiable claims over qualitative promises.
|
||||
- Fast experiment loops over broad rewrites.
|
||||
- Explicit uncertainty over false precision.
|
||||
@@ -1,150 +0,0 @@
|
||||
---
|
||||
disable-model-invocation: true
|
||||
name: research-engineering
|
||||
description: Route DSP hardware and software research-engineering requests to the best specialist workflow and return a unified, decision-ready output
|
||||
---
|
||||
|
||||
Role: You are a dispatcher skill for DSP hardware and software research engineering. You triage requests, select the right specialist path(s), enforce safety and reproducibility constraints, and return one coherent response.
|
||||
|
||||
Primary objectives:
|
||||
- Identify technical intent across algorithms, embedded implementation, hardware architecture, tooling, and validation.
|
||||
- Route work to the most appropriate specialist workflow(s) with explicit assumptions.
|
||||
- Produce practical, testable outputs for research engineering decisions.
|
||||
- Minimize unnecessary handoffs and avoid over-engineering.
|
||||
|
||||
Scope:
|
||||
- In scope: signal analysis, DSP algorithm design, fixed-point strategy, embedded audio/DSP implementation, architecture tradeoffs, measurement plans, benchmarking, verification strategy, literature-grounded research synthesis.
|
||||
- Out of scope: legal/compliance claims, medical claims, fabrication process sign-off, irreversible production actions.
|
||||
|
||||
Non-goals:
|
||||
- Do not pretend to run lab measurements that were not run.
|
||||
- Do not claim numerical performance without source, simulation, or measurement basis.
|
||||
- Do not bypass hardware safety, power, thermal, EMC, or hearing-safety constraints.
|
||||
|
||||
Inputs expected:
|
||||
- User request text
|
||||
- Current conversation context
|
||||
- Available specialist agents/skills
|
||||
- Environment/tooling constraints
|
||||
- Optional project constraints (sample rate, latency budget, CPU target, memory budget, power target, BOM constraints)
|
||||
|
||||
Required output contract:
|
||||
- Always provide:
|
||||
1) Selected route
|
||||
2) Why this route
|
||||
3) Final user-facing result
|
||||
4) Assumptions and unknowns
|
||||
5) Verification plan (how to confirm correctness/performance)
|
||||
|
||||
Dispatch taxonomy:
|
||||
- Algorithm Design: filters, adaptive processing, beamforming, detection/classification front-ends, denoising, dynamics, time-frequency methods.
|
||||
- Numerical Implementation: fixed-point, quantization noise, saturation behavior, scaling, coefficient sensitivity, stability under finite precision.
|
||||
- Embedded Software: RT constraints, DMA/ISR design, buffering, scheduling, memory layout, SIMD/accelerators, portability.
|
||||
- Hardware/Platform: MCU/DSP/FPGA partitioning, codec/interface constraints, clocking, throughput, latency, power/thermal tradeoffs.
|
||||
- Validation and Measurement: objective metrics, stimulus design, golden references, regression tests, bench/lab measurement plans.
|
||||
- Research Synthesis: literature scan, method comparison, risk/novelty assessment, experiment roadmap.
|
||||
|
||||
Routing policy:
|
||||
1. Parse request into one or more intents.
|
||||
2. Extract hard constraints and success criteria.
|
||||
3. Score candidate routes on:
|
||||
- Relevance (0-5)
|
||||
- Capability fit (0-5)
|
||||
- Safety/feasibility (0-5)
|
||||
- Evidence availability (0-5)
|
||||
- Execution cost (0-5, lower is better)
|
||||
4. Select route:
|
||||
- Single-route if one clear winner.
|
||||
- Multi-route if subproblems are separable and independent.
|
||||
5. Dispatch with structured task packets.
|
||||
6. Reconcile outputs into a single final response.
|
||||
|
||||
Confidence rules:
|
||||
- High: top route exceeds second by >= 3 and all hard constraints are known.
|
||||
- Medium: top route exceeds second by 1-2 or one non-critical constraint missing; proceed with explicit assumptions.
|
||||
- Low: tie score or missing critical constraint (platform, sample rate, latency, safety limit); ask exactly one targeted question.
|
||||
|
||||
Critical constraints checklist:
|
||||
- Target platform (e.g., Cortex-M4/M7, SHARC, FPGA family)
|
||||
- Sample rate and channel count
|
||||
- End-to-end latency budget
|
||||
- CPU/memory budget
|
||||
- Power/thermal envelope (if embedded/portable)
|
||||
- Numeric format (float/fixed, word lengths)
|
||||
- Required performance metrics (SNR, THD+N, PESQ/STOI, detection F1, etc.)
|
||||
|
||||
Safety and integrity gates (must run before dispatch):
|
||||
- If safety-critical or human-impacting audio claims are requested, include explicit uncertainty and verification requirements.
|
||||
- If destructive hardware actions are requested, require explicit confirmation and safe fallback.
|
||||
- Never expose secrets, proprietary keys, or internal credentials.
|
||||
- Never fabricate measurement data or citations.
|
||||
|
||||
Specialist route mapping:
|
||||
- Signal characterization question -> Signal Analysis specialist
|
||||
- Embedded DSP implementation/debug -> Embedded DSP specialist
|
||||
- Hardware/software partitioning -> Embedded hardware architect path
|
||||
- Literature-heavy "state of the art" request -> Research Assistant or literature path
|
||||
- Cross-domain request (algorithm + embedded + validation) -> Multi-route orchestration with unified recommendation
|
||||
|
||||
Task packet format for downstream specialists:
|
||||
```json
|
||||
{
|
||||
"objective": "<single clear objective>",
|
||||
"context": ["<key constraints>", "<known assumptions>"],
|
||||
"required_output": [
|
||||
"Approach",
|
||||
"Tradeoffs",
|
||||
"Risks",
|
||||
"Verification steps",
|
||||
"Confidence"
|
||||
],
|
||||
"limits": ["No fabricated data", "State unknowns explicitly"]
|
||||
}
|
||||
```
|
||||
|
||||
Multi-route orchestration rules:
|
||||
- Split only when interfaces between subproblems are clear.
|
||||
- Normalize units and terminology across outputs.
|
||||
- Resolve disagreements by preferring: measured evidence > validated simulation > reasoned estimate.
|
||||
- If unresolved conflict remains, surface it as a decision risk.
|
||||
|
||||
Fallback behavior:
|
||||
- If selected specialist fails, retry once with narrowed objective and stricter output format.
|
||||
- If retry fails, route to a generalist technical path and label confidence reduced.
|
||||
- If key constraints are missing, provide a best-effort scaffold plus one blocking question.
|
||||
|
||||
Response template:
|
||||
```text
|
||||
Route Selected:
|
||||
- <specialist path(s)>
|
||||
|
||||
Why This Route:
|
||||
- <1-3 concise bullets>
|
||||
|
||||
Result:
|
||||
<final user-facing answer>
|
||||
|
||||
Assumptions and Unknowns:
|
||||
- <bullet list or "None">
|
||||
|
||||
Verification Plan:
|
||||
- <3-7 concrete checks/tests/measurements>
|
||||
|
||||
Confidence:
|
||||
- <High|Medium|Low> with one-line rationale
|
||||
```
|
||||
|
||||
Clarification template (only when blocked):
|
||||
```text
|
||||
I can dispatch this precisely, but I need one detail:
|
||||
- <single targeted question>
|
||||
|
||||
Default I will assume if you prefer speed:
|
||||
- <recommended default>
|
||||
```
|
||||
|
||||
Quality bar:
|
||||
- Actionable over theoretical.
|
||||
- Reproducible over vague.
|
||||
- Explicit uncertainty over false precision.
|
||||
- Deliver the smallest valid plan that can be tested quickly.
|
||||
@@ -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,7 +0,0 @@
|
||||
# Forge Skills
|
||||
|
||||
These skills have been deprecated. See [../README.md](../README.md) for the current list.
|
||||
|
||||
## Overview
|
||||
|
||||
This directory previously hosted specialized forge interaction skills for GitHub and Gitea. The individual forge skills (`forge-gitea`, `forge-github`, `forge-interaction`, `forge-preferences`, `forge-router`) have been moved to the parent [deprecated](../) bucket.
|
||||
@@ -5,9 +5,9 @@ Daily code work.
|
||||
## User-invoked
|
||||
|
||||
- [commit-staged](commit-staged/SKILL.md) — Commit staged files with a conventional commit message.
|
||||
- [implement-isolation](implement-isolation/SKILL.md) — Implement work in an isolated git worktree via subagent with review.
|
||||
- [implement-isolation-tmux](implement-isolation-tmux/SKILL.md) — Dispatch a child agent into its own tmux window in an isolated git worktree for fire-and-forget implementation.
|
||||
- [project-context-pack](project-context-pack/SKILL.md) — Use when the user wants a bounded repo context pack, project map, codebase index, or cached memory file so later work uses fd/rg/tree-sitter/LSP instead of repeated browsing.
|
||||
- [implement-isolation](implement-isolation/SKILL.md) — Implement a piece of work based on a spec or set of tickets in isolation.
|
||||
- [implement-isolation-tmux](implement-isolation-tmux/SKILL.md) — Dispatch a child agent in an isolated git worktree to implement a piece of work based on a PRD or set of issues.
|
||||
- [project-context-pack](project-context-pack/SKILL.md) — Build a bounded repo context pack (project map, codebase index, cached memory file) so later work uses fd/rg/tree-sitter/LSP instead of repeated browsing.
|
||||
- [setup-skills](setup-skills/SKILL.md) — Configure this repo for the engineering skills, set up its issue tracker, triage label vocabulary, and domain doc layout. Run once before first use of the other engineering skills.
|
||||
|
||||
## Model-invoked
|
||||
|
||||
@@ -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,5 @@
|
||||
interface:
|
||||
display_name: "Implement Isolation Tmux"
|
||||
short_description: "Implement isolation using tmux for the agent"
|
||||
policy:
|
||||
allow_implicit_invocation: false
|
||||
@@ -0,0 +1,5 @@
|
||||
interface:
|
||||
display_name: "Implement Isolation"
|
||||
short_description: "Implement isolation for the agent"
|
||||
policy:
|
||||
allow_implicit_invocation: false
|
||||
@@ -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
|
||||
|
||||
When the provider-neutral `tracker` package is installed, generated issue-tracker guidance should call it for normal operations and retain provider-specific CLI commands only as adapter/fallback reference. See `docs/agents/tracker.md`.
|
||||
|
||||
Scaffold the per-repo configuration that the engineering skills assume:
|
||||
|
||||
- Issue tracker: where issues live (never assume a default forge)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
interface:
|
||||
display_name: "Setup Engineering Skills"
|
||||
short_description: "Setup engineering skills for the agent"
|
||||
policy:
|
||||
allow_implicit_invocation: false
|
||||
@@ -1,12 +1,12 @@
|
||||
# Issue tracker: Gitea
|
||||
|
||||
Issues and PRDs for this repo live as Gitea issues. Use the `tea` CLI for all operations.
|
||||
Prefer the provider-neutral `tracker` command for normal operations; it emits JSON and delegates to `tea`. See `docs/agents/tracker.md`. The `tea` commands below remain adapter and capability reference material.
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Create an issue**: `tea issue create --title "..." --description "..."`. Use a heredoc for multi-line descriptions.
|
||||
- **Create an issue**: `tea issue create --title "..." --description "..."`. Use a heredoc for multi-line descriptions.
|
||||
- **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> "..."`.
|
||||
- **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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Issue tracker: GitHub
|
||||
|
||||
Issues and PRDs for this repo live as GitHub issues. Use the `gh` CLI for all operations.
|
||||
Prefer the provider-neutral `tracker` command for normal operations; it emits JSON and delegates to `gh`. See `docs/agents/tracker.md`. The `gh` commands below remain adapter and capability reference material.
|
||||
|
||||
## Conventions
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Issue tracker: GitLab
|
||||
|
||||
Issues and PRDs for this repo live as GitLab issues. Use the [`glab`](https://gitlab.com/gitlab-org/cli) CLI for all operations.
|
||||
Prefer the provider-neutral `tracker` command for normal operations; it emits JSON and delegates to `glab`. See `docs/agents/tracker.md`. The `glab` commands below remain adapter and capability reference material.
|
||||
|
||||
## Conventions
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
interface:
|
||||
display_name: "Agent Handoff"
|
||||
short_description: "Handoff the agent to a human operator"
|
||||
policy:
|
||||
allow_implicit_invocation: false
|
||||
@@ -0,0 +1,5 @@
|
||||
interface:
|
||||
display_name: "Knowledge Gardener"
|
||||
short_description: "Manage and curate knowledge for the agent"
|
||||
policy:
|
||||
allow_implicit_invocation: false
|
||||
@@ -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
|
||||
@@ -5,7 +5,7 @@ Personal knowledge management.
|
||||
## User-invoked
|
||||
|
||||
- [conversation-summary](conversation-summary/SKILL.md) — Save the current conversation as a comprehensive report note in your Obsidian vault, following OKF v0.1 conventions.
|
||||
- [crit](crit/SKILL.md) — Brainstorm with AI using the CRIT framework to generate and evaluate ideas.
|
||||
- [crit](crit/SKILL.md) — Run the CRIT framework — give the AI Context, assign it a Role, let it Interview you one question at a time, then issue the Task.
|
||||
- [research-vault](research-vault/SKILL.md) — Research a topic through a one-question-at-a-time learning conversation, answer directly, share resources when useful, and save a linked OKF-conformant research packet in the Obsidian vault.
|
||||
- [youtube-video-capture](youtube-video-capture/SKILL.md) — Fetch subtitles from a YouTube video, summarize the content, and save both the summary and raw subtitles to the Video bundle in the Obsidian vault.
|
||||
|
||||
|
||||
@@ -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
|
||||
description: brainstorm with AI using the CRIT framework to generate and evaluate ideas.
|
||||
description: Run the CRIT framework — give the AI Context, assign it a Role, let it Interview you one question at a time, then issue the Task.
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
## Purpose
|
||||
|
||||
This skill implements the CRIT (Context-Request-Ideation-Tone) framework for structured brainstorming and idea evaluation.
|
||||
CRIT is a four-step prompt framework by Geoff Woods: **Context, Role, Interview,
|
||||
Task**. The sequence front-loads thinking before execution — the AI learns your
|
||||
world, takes a specific lens, interviews you to surface what matters, then acts.
|
||||
|
||||
## Core Workflow
|
||||
The insight: most people skip to Task and get generic output. The Interview
|
||||
step — one question at a time, max three — is where the signal lives.
|
||||
|
||||
### Step 1: Context Analysis
|
||||
Before generating ideas, establish the foundation:
|
||||
## Steps
|
||||
|
||||
1. **Identify the Core Domain**
|
||||
- What field, industry, or subject area?
|
||||
- What are the key constraints (time, resources, technical limitations)?
|
||||
- Who is the target audience and their expertise level?
|
||||
Run these four steps in order when the user invokes `/crit`.
|
||||
|
||||
2. **Assess Current State**
|
||||
- What problems or opportunities exist?
|
||||
- What has been tried before (if applicable)?
|
||||
- What resources are available?
|
||||
### 1. Context — Give the AI your world
|
||||
|
||||
**Completion Criterion**: Domain, constraints, and current state captured in a compact summary (3-5 sentences).
|
||||
Ask the user: "What should I know about you, your goals, your audience, and any
|
||||
constraints?"
|
||||
|
||||
### Step 2: Request Clarification
|
||||
Structure the brainstorming request:
|
||||
Capture the answer in one paragraph. More detail is better.
|
||||
|
||||
1. **Define the Specific Goal**
|
||||
- What concrete outcome do you want?
|
||||
- What success criteria will be used?
|
||||
- What is the expected timeline?
|
||||
**Completion criterion**: One paragraph covering identity, goal, audience, and
|
||||
constraints — confirmed by the user.
|
||||
|
||||
2. **Gather Input Requirements**
|
||||
- What information is needed to proceed?
|
||||
- What assumptions should be validated?
|
||||
- What data or resources are required?
|
||||
### 2. Role — Assign a viewpoint
|
||||
|
||||
**Completion Criterion**: Goal and input requirements captured as 1-3 concrete statements, each with a checkable success criterion.
|
||||
Ask the user: "What role should I take?"
|
||||
|
||||
### Step 3: Ideation
|
||||
Generate diverse, high-quality ideas:
|
||||
Guide toward a specific lens — "strategy coach who uncovers blind spots,"
|
||||
"editor who cuts fluff," "architect who finds leverage points." Not "be
|
||||
helpful."
|
||||
|
||||
1. **Divergent Thinking Phase**
|
||||
- Generate 5-10 initial concepts without judgment
|
||||
- Apply different perspectives (technical, business, user experience)
|
||||
- Include both obvious and unconventional options
|
||||
**Completion criterion**: A single sentence assigning a named role that implies
|
||||
a specific viewpoint.
|
||||
|
||||
2. **Convergent Analysis Phase**
|
||||
- Evaluate each idea against success criteria
|
||||
- Score ideas on feasibility, impact, and alignment
|
||||
- Identify patterns and synergies between ideas
|
||||
### 3. Interview — One question at a time
|
||||
|
||||
**Completion Criterion**: Minimum 5 distinct ideas generated and evaluated with scores.
|
||||
Instruct yourself: "Ask me no more than three questions, one at a time, to
|
||||
clarify what I'm trying to achieve."
|
||||
|
||||
### Step 4: Iterative Refinement
|
||||
Improve selected ideas:
|
||||
Ask one question. Wait for the answer. Then ask the next. Max three. Do not
|
||||
batch them.
|
||||
|
||||
1. **Select Top Candidates**
|
||||
- Choose 2-3 ideas with highest potential
|
||||
- Detail implementation approach for each
|
||||
- Identify risks and mitigation strategies
|
||||
This step forces the user to slow down and think, and teaches the AI what
|
||||
actually matters.
|
||||
|
||||
2. **Develop Action Plans**
|
||||
- Break down into concrete steps
|
||||
- Assign priorities and dependencies
|
||||
- Define success metrics and checkpoints
|
||||
**Completion criterion**: 1-3 questions asked and answered, one at a time.
|
||||
Stop asking when the user signals readiness or you've asked three.
|
||||
|
||||
**Completion Criterion**: 2-3 refined ideas with detailed action plans.
|
||||
### 4. Task — Issue the assignment
|
||||
|
||||
### Step 5: Tone & Delivery
|
||||
Adapt communication to the audience:
|
||||
Ask the user: "What's the task?"
|
||||
|
||||
1. **Choose Appropriate Role**
|
||||
- Subject Matter Expert for technical depth
|
||||
- Consultant for strategic guidance
|
||||
- Teacher for complex concepts
|
||||
- Collaborator for co-creation
|
||||
- Analyst for multi-perspective evaluation
|
||||
Guide toward a short, clear, slightly uncomfortable prompt that asks the AI to
|
||||
*think*, not just write. Reference the preceding interview.
|
||||
|
||||
2. **Structure Response**
|
||||
- Lead with clear, actionable solutions
|
||||
- Organize information logically and concisely
|
||||
- Include examples or analogies for clarity
|
||||
- Suggest next steps and follow-up questions
|
||||
> "Based on our conversation, give me three non-obvious actions I can take.
|
||||
> Make them surprising but realistic."
|
||||
|
||||
**Completion Criterion**: Response delivered in chosen role with sections clearly labeled (Context, Request, Ideation, Refinement, Next Steps).
|
||||
Execute the task.
|
||||
|
||||
**Completion criterion**: Task executed and result delivered to the user.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
interface:
|
||||
display_name: "Context Role Interview Task"
|
||||
short_description: "CRIT framework is a structured prompting and interaction methodology"
|
||||
policy:
|
||||
allow_implicit_invocation: false
|
||||
@@ -0,0 +1,5 @@
|
||||
interface:
|
||||
display_name: "Personal Knowledge Management Curation"
|
||||
short_description: "Curate and manage personal knowledge for the agent"
|
||||
policy:
|
||||
allow_implicit_invocation: false
|
||||
@@ -0,0 +1,5 @@
|
||||
interface:
|
||||
display_name: "Research Vault"
|
||||
short_description: "Store and retrieve research notes and documents"
|
||||
policy:
|
||||
allow_implicit_invocation: false
|
||||
@@ -0,0 +1,5 @@
|
||||
interface:
|
||||
display_name: "Youtube Video Capture"
|
||||
short_description: "Capture a youtube video"
|
||||
policy:
|
||||
allow_implicit_invocation: false
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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