Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dead6ca2bb | ||
|
|
f65a6ec812 | ||
|
|
7dc4758e9a | ||
|
|
07198e9aad | ||
|
|
8a7201b898 | ||
|
|
f4e953186e | ||
|
|
5a6d1fccb0 | ||
|
|
fa7619bfde | ||
|
|
0daa11851d | ||
|
|
22c3b3e45f | ||
|
|
102b163fc5 | ||
|
|
2a87789523 | ||
|
|
9b4c6952c5 | ||
|
|
d8be029afb | ||
|
|
cc10b66365 | ||
|
|
36536e0e11 | ||
|
|
e16ca1114a | ||
|
|
8829e27004 | ||
|
|
1b5398afdc | ||
|
|
c68f6b4267 | ||
|
|
a0c5280aac | ||
|
|
de4f6f137b | ||
|
|
41d1d48025 | ||
|
|
1676528fad | ||
|
|
c92ee69440 | ||
|
|
1614a0d469 | ||
|
|
b8bd012763 | ||
|
|
88d3f3e81e | ||
|
|
a6b00a7f11 | ||
|
|
cc3c36585b | ||
|
|
74572c1b78 | ||
|
|
c7ed24ea51 | ||
|
|
fe364b3c63 | ||
|
|
8a230f7853 | ||
|
|
032a842e82 | ||
|
|
f9611e1343 | ||
|
|
fa29b93a02 | ||
|
|
8af5a20da5 | ||
|
|
cfeb42398e | ||
|
|
dc9293c7be | ||
|
|
e6e322e542 | ||
|
|
cd40daec54 | ||
|
|
ff41e7965a | ||
|
|
fbca2ada7e | ||
|
|
371697375a | ||
|
|
68f9b4691e |
@@ -0,0 +1,119 @@
|
|||||||
|
# Project Context Pack
|
||||||
|
|
||||||
|
Generated: 2026-08-17
|
||||||
|
Root: /home/sjb/Projects/personal/ws-sjb-skills/wt-master
|
||||||
|
Working directory: .
|
||||||
|
Status: fresh
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
A collection of agent skills (slash commands and behaviors) loaded into Steve Beaulac's AI coding agents. Each skill is a SKILL.md file that teaches the agent how to handle a specific task — from codebase design and TDD to Obsidian PKM workflows and tmux agent launching.
|
||||||
|
|
||||||
|
## Project type
|
||||||
|
|
||||||
|
- **Agent skill repository plus standalone Python tracker package**
|
||||||
|
- Languages: Python and Markdown, one Bash script (detect-agent), one shell script (tmux-open)
|
||||||
|
- Package manager: `pyproject.toml`
|
||||||
|
- Build/test tools: `python -m unittest discover`
|
||||||
|
- Agent guidance: `AGENTS.md` at root, `docs/invocation.md` for invocation conventions, `docs/agents/` for issue tracker / triage labels / ADR wiki / domain docs
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── AGENTS.md # Top-level agent instructions for this repo
|
||||||
|
├── README.md # Project overview, lists user-invoked and model-invoked skills
|
||||||
|
├── .gitignore # Excludes docs/adr/
|
||||||
|
├── .agent/
|
||||||
|
│ └── project-context.md # This file
|
||||||
|
├── docs/
|
||||||
|
│ ├── invocation.md # Model-invoked vs user-invoked definitions
|
||||||
|
│ ├── agents/
|
||||||
|
│ │ ├── adr-wiki.md # ADR wiki setup docs
|
||||||
|
│ │ ├── domain.md # Domain docs layout
|
||||||
|
│ │ ├── 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
|
||||||
|
├── productivity/ # (currently only README.md)
|
||||||
|
├── pkm/ # User-invoked: conversation-summary, crit, research-vault, youtube-video-capture; Model-invoked: pkm-curation
|
||||||
|
├── personal/ # (currently only README.md)
|
||||||
|
├── misc/ # User-invoked: tmux-launch-agent
|
||||||
|
├── in-progress/ # User-invoked: agent-handoff, knowledge-gardener
|
||||||
|
└── deprecated/ # Deprecated forge-* and dsp-* skills
|
||||||
|
```
|
||||||
|
|
||||||
|
## Important files
|
||||||
|
|
||||||
|
- `AGENTS.md` — Agent entry point that describes structure, categories, and references docs
|
||||||
|
- `README.md` — Index of all skills divided into user-invoked and model-invoked
|
||||||
|
- `docs/invocation.md` — Defines the invocation model (disable-model-invocation frontmatter key, human vs model reachability, dependency rules)
|
||||||
|
- `docs/agents/` — Agent documentation for issue tracker, triage labels, ADR wiki, domain docs
|
||||||
|
- `common/engineering/project-context-pack/SKILL.md` — The skill that generated this file
|
||||||
|
- `common/misc/tmux-launch-agent/SKILL.md` — Fork agent CLI into new tmux window (user-invoked)
|
||||||
|
- `common/misc/tmux-launch-agent/tmux-open` — Reusable script that opens a command in a new tmux window/session
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
- Build/package: `python -m pip install .`
|
||||||
|
- Test: `python -m unittest discover -v`
|
||||||
|
- Typecheck: `lsp_diagnostics` on `tracker/` and `tests/`
|
||||||
|
- Run: `python -m tracker` or installed `tracker`
|
||||||
|
|
||||||
|
## Entry points
|
||||||
|
|
||||||
|
- `AGENTS.md` — loaded by the AI agent as project instructions (referred to in pi's agent config)
|
||||||
|
- Each `SKILL.md` under `common/` — referenced by agents via slash commands or auto-invocation
|
||||||
|
|
||||||
|
## Search and symbol notes
|
||||||
|
|
||||||
|
- All skills are `SKILL.md` files — search with `fd SKILL.md`
|
||||||
|
- Bucket READMEs: `fd README.md common/`
|
||||||
|
- Skills are classified as **user-invoked** (`disable-model-invocation: true` in frontmatter) or **model-invoked** (default, no frontmatter flag)
|
||||||
|
- Dependencies between skills use prose invocation ("Run the `/grilling` skill"), not file cross-references
|
||||||
|
- Shared reference docs live inside the owning skill's directory; other skills reach that material by invoking the skill
|
||||||
|
|
||||||
|
## Files inspected
|
||||||
|
|
||||||
|
- `AGENTS.md` — root agent instructions — fresh
|
||||||
|
- `README.md` — project overview and skill index — fresh
|
||||||
|
- `docs/invocation.md` — invocation model definitions — fresh
|
||||||
|
- `.gitignore` — excludes docs/adr/ — fresh
|
||||||
|
- `common/README.md` — common bucket index — fresh
|
||||||
|
- `common/misc/README.md` — misc bucket index — fresh
|
||||||
|
- `.agent/project-context.md` — this file (refreshed from stale 2026-06-25 version)
|
||||||
|
|
||||||
|
## Exclusions
|
||||||
|
|
||||||
|
- `.git/` — VCS data
|
||||||
|
- `docs/adr/` — gitignored ADR wiki clone
|
||||||
|
- `node_modules/`, `dist/`, `build/`, `target/`, `.venv/`, `__pycache__/`, `vendor/`, `coverage/` — not present, but excluded by policy
|
||||||
|
- `/home/sjb/.agents/skills/` — global install, NOT the source of truth for this repo
|
||||||
|
- Binary files, large artifacts, credentials, secrets, personal data
|
||||||
|
|
||||||
|
## Navigation rules for future agents
|
||||||
|
|
||||||
|
- Start with `fd SKILL.md` to find all skills, then narrow by `fd SKILL.md common/<category>/`
|
||||||
|
- To understand a skill's purpose, read its `SKILL.md` and the bucket `README.md` that indexes it
|
||||||
|
- For invocation rules (user-invoked vs model-invoked), read `docs/invocation.md`
|
||||||
|
- For agent-level docs (issue tracker, triage, ADR, domain), check `docs/agents/`
|
||||||
|
- Do not browse directories file-by-file; use `fd` and `rg` first
|
||||||
|
- Track every inspected file in this cache
|
||||||
|
- Re-read a file only when it changed, the cache is stale, or exact details are needed
|
||||||
|
|
||||||
|
## Edit boundaries
|
||||||
|
|
||||||
|
When cwd is inside this repo, all file edits MUST be scoped to paths under the repo root (`/home/sjb/Projects/personal/ws-sjb-skills/wt-master`). Do NOT touch files under `/home/sjb/.agents/skills/` or `/home/sjb/.pi/` — those are the installed/runtime copies, not the source of truth. The global install is synced separately; this repo is where source edits happen.
|
||||||
|
|
||||||
|
## Refresh notes
|
||||||
|
|
||||||
|
- This is a markdown-only repo with no build artifacts; refreshes are rarely needed unless skills are added or removed
|
||||||
|
- To refresh, re-run `tree -a -I '.git' -L 4` and re-read any changed bucket READMEs or SKILL.md files
|
||||||
|
- The `docs/adr/` directory is gitignored — if ADR data is needed, check the Gitea wiki directly
|
||||||
|
- If a skill is moved between buckets, update all README.md files that reference it (root, common, source bucket, destination bucket)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
docs/adr/
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
@@ -12,18 +12,44 @@ Skills are organized into buckets based on where they can be used and their cate
|
|||||||
|
|
||||||
Skill are organized into categories based on their function. For example, `/common/misc/` is a bucket for miscellaneous skills that work in all CLI agents.
|
Skill are organized into categories based on their function. For example, `/common/misc/` is a bucket for miscellaneous skills that work in all CLI agents.
|
||||||
|
|
||||||
If we have a skill that is only relevant to a specific agent, we can put it in an agent-specific bucket. For example, if we have a skill that is only relevant to the `opencode` agent, we can put it in `/opencod/misc`.
|
If we have a skill that is only relevant to a specific agent, we can put it in an agent-specific bucket. For example, if we have a skill that is only relevant to the `opencode` agent, we can put it in `/opencode/misc`.
|
||||||
|
|
||||||
|
|
||||||
## list of categories
|
## list of categories
|
||||||
|
|
||||||
- `engineering/` — daily code work
|
- `engineering/` — daily code work
|
||||||
- `productivity/` — daily non-code workflow tools
|
- `productivity/` — daily non-code workflow tools
|
||||||
- `misc/` — kept around but rarely used
|
- `misc/` — kept around but rarely used
|
||||||
- `personal/` — tied to my own setup, not promoted
|
- `personal/` — tied to my own setup, not promoted
|
||||||
|
- `pkm/` — personal knowledge management
|
||||||
- `in-progress/` — drafts not yet ready to ship
|
- `in-progress/` — drafts not yet ready to ship
|
||||||
- `deprecated/` — no longer used
|
- `deprecated/` — no longer used
|
||||||
|
|
||||||
|
|
||||||
Each bucket folder has a `README.md` that lists every skill in the bucket with a one-line description, with the skill name linked to its `SKILL.md`. Bucket `README.md`s and the top-level `README.md` group entries into **User-invoked** and **Model-invoked**.
|
Each bucket folder has a `README.md` that lists every skill in the bucket with a one-line description, with the skill name linked to its `SKILL.md`. Bucket `README.md`s and the top-level `README.md` group entries into **User-invoked** and **Model-invoked**.
|
||||||
|
|
||||||
Every `SKILL.md` is either user-invoked (`disable-model-invocation: true`, reachable only by the human) or model-invoked (model- or user-reachable). For the full definitions, description conventions, and why a user-invoked skill can invoke model-invoked skills but never another user-invoked one, see [docs/invocation.md](./docs/invocation.md).
|
Every `SKILL.md` is either user-invoked (`disable-model-invocation: true`, reachable only by the human) or model-invoked (model- or user-reachable). For the full definitions, description conventions, and why a user-invoked skill can invoke model-invoked skills but never another user-invoked one, see [docs/invocation.md](./docs/invocation.md).
|
||||||
|
|
||||||
|
## Agent skills
|
||||||
|
|
||||||
|
### Issue tracker
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
Seven-label vocabulary with default names (needs-triage, needs-info, needs-review, ready-for-agent, ready-for-human, in-progress, wontfix). See `docs/agents/triage-labels.md`.
|
||||||
|
|
||||||
|
### Domain docs
|
||||||
|
|
||||||
|
Single-context layout. See `docs/agents/domain.md`.
|
||||||
|
|
||||||
|
### ADR wiki
|
||||||
|
|
||||||
|
Gitea wiki at `git@gitea.sagacity.ca:steve/Skills.wiki.git`, cloned into `docs/adr/`, SSH key auth. See `docs/agents/adr-wiki.md`.
|
||||||
|
|
||||||
|
## Project Context Pack
|
||||||
|
|
||||||
|
Agent memory file that describes the repo's context, codebase, and navigation rules. See `.agents/project-context.md`.
|
||||||
|
|
||||||
|
### Agent CLI
|
||||||
|
|
||||||
|
`pi` used for child agents. See `docs/agents/agent-cli.md`.
|
||||||
|
|||||||
@@ -1,23 +1,27 @@
|
|||||||
# Skills
|
# Skills
|
||||||
|
|
||||||
A collection of agent skills (slash commands and behaviors) loaded into Steve Beaulac's agents.
|
Agent skills (slash commands and behaviors) loaded into my agent.
|
||||||
|
|
||||||
|
## Tracker automation
|
||||||
|
|
||||||
|
This repo also ships the standalone provider-neutral `tracker` CLI/library. See [`docs/agents/tracker.md`](docs/agents/tracker.md) and [`tracker/README.md`](tracker/README.md) for migration guidance.
|
||||||
|
|
||||||
## User-invoked
|
## User-invoked
|
||||||
|
|
||||||
- [conversation-summary](common/pkm/conversation-summary/SKILL.md) — Summarize the current AI conversation into a new Obsidian markdown note and matching transcript file.
|
- [agent-handoff](common/in-progress/agent-handoff/SKILL.md) — Hand the current conversation off to a fresh background agent that picks up the work immediately.
|
||||||
- [crit](common/pkm/crit/SKILL.md) — Brainstorm with AI using the CRIT framework to generate and evaluate ideas.
|
- [commit-staged](common/engineering/commit-staged/SKILL.md) — Commit staged files with a conventional commit message.
|
||||||
- [grill-me](common/productivity/grill-me/SKILL.md) — A relentless interview to sharpen a plan or design.
|
- [conversation-summary](common/pkm/conversation-summary/SKILL.md) — Save the current conversation as a comprehensive report note in your Obsidian vault, following OKF v0.1 conventions.
|
||||||
- [handoff](common/productivity/handoff/SKILL.md) — Compact the current conversation into a handoff document for another agent to pick up.
|
- [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.
|
||||||
- [knowledge-gardener](common/pkm/knowledge-gardener/SKILL.md) — Run vault-aware semantic search, synthesis, note creation, linking, and Zettelkasten workflows for this Obsidian vault.
|
- [implement-isolation](common/engineering/implement-isolation/SKILL.md) — Implement a piece of work based on a spec or set of tickets in isolation.
|
||||||
- [pkm-curation](common/personal/pkm-curation/SKILL.md) — Curate an Obsidian-style personal knowledge vault.
|
- [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.
|
||||||
- [research-vault](common/pkm/research-vault/SKILL.md) — Research a topic through a one-question-at-a-time learning conversation and save a linked Obsidian research packet.
|
- [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.
|
||||||
- [writing-great-skills](common/productivity/writing-great-skills/SKILL.md) — Reference for writing and editing skills well — the vocabulary and principles that make a skill predictable.
|
- [project-context-pack](common/engineering/project-context-pack/SKILL.md) — Build a bounded repo context pack (project map, codebase index, cached memory file) so later work uses fd/rg/tree-sitter/LSP instead of repeated browsing.
|
||||||
|
- [research-vault](common/pkm/research-vault/SKILL.md) — Research a topic through a one-question-at-a-time learning conversation, answer directly, share resources when useful, and save a linked OKF-conformant research packet in the Obsidian vault.
|
||||||
|
- [setup-skills](common/engineering/setup-skills/SKILL.md) — Configure this repo for the engineering skills, set up its issue tracker, triage label vocabulary, and domain doc layout. Run once before first use of the other engineering skills.
|
||||||
|
- [tmux-launch-agent](common/misc/tmux-launch-agent/SKILL.md) — Fork a new agent CLI session into a new tmux window, detected from the current agent.
|
||||||
|
- [youtube-video-capture](common/pkm/youtube-video-capture/SKILL.md) — Fetch subtitles from a YouTube video, summarize the content, and save both the summary and raw subtitles to the Video bundle in the Obsidian vault.
|
||||||
|
|
||||||
## Model-invoked
|
## Model-invoked
|
||||||
|
|
||||||
- [audio-product-dsp](common/deprecated/audio-production-dispatcher/SKILL.md) — Dispatch audio product DSP hardware/software engineering requests to the best specialist workflow with measurable product-focused outputs.
|
- [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.
|
||||||
- [forge-interaction](common/engineering/forge-interaction/SKILL.md) — Work safely with GitHub and Gitea repositories, issues, pull requests, releases, and remote forge state.
|
- [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.
|
||||||
- [forge-preferences](common/personal/forge-preferences/SKILL.md) — Apply Steve's personal or project-specific GitHub/Gitea forge preferences.
|
|
||||||
- [grilling](common/productivity/grilling/SKILL.md) — Interview the user relentlessly about a plan or design.
|
|
||||||
- [project-context-pack](common/engineering/project-context-pack/SKILL.md) — Build and refresh a bounded repo context memory file so agents use disciplined search instead of repeated browsing.
|
|
||||||
- [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.
|
|
||||||
|
|||||||
+14
-14
@@ -4,20 +4,20 @@ Skills that work in all CLI agents.
|
|||||||
|
|
||||||
## User-invoked
|
## User-invoked
|
||||||
|
|
||||||
- [conversation-summary](pkm/conversation-summary/SKILL.md) — Summarize the current AI conversation into a new Obsidian markdown note and matching transcript file.
|
- [agent-handoff](in-progress/agent-handoff/SKILL.md) — Hand the current conversation off to a fresh background agent that picks up the work immediately.
|
||||||
- [crit](pkm/crit/SKILL.md) — Brainstorm with AI using the CRIT framework to generate and evaluate ideas.
|
- [commit-staged](engineering/commit-staged/SKILL.md) — Commit staged files with a conventional commit message.
|
||||||
- [grill-me](productivity/grill-me/SKILL.md) — A relentless interview to sharpen a plan or design.
|
- [conversation-summary](pkm/conversation-summary/SKILL.md) — Save the current conversation as a comprehensive report note in your Obsidian vault, following OKF v0.1 conventions.
|
||||||
- [handoff](productivity/handoff/SKILL.md) — Compact the current conversation into a handoff document for another agent to pick up.
|
- [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.
|
||||||
- [knowledge-gardener](pkm/knowledge-gardener/SKILL.md) — Run vault-aware semantic search, synthesis, note creation, linking, and Zettelkasten workflows for this Obsidian vault.
|
- [implement-isolation](engineering/implement-isolation/SKILL.md) — Implement a piece of work based on a spec or set of tickets in isolation.
|
||||||
- [pkm-curation](personal/pkm-curation/SKILL.md) — Curate an Obsidian-style personal knowledge vault.
|
- [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.
|
||||||
- [research-vault](pkm/research-vault/SKILL.md) — Research a topic through a one-question-at-a-time learning conversation and save a linked Obsidian research packet.
|
- [knowledge-gardener](in-progress/knowledge-gardener/SKILL.md) — Run vault-aware semantic search, synthesis, note creation, linking, and Zettelkasten workflows for this Obsidian vault.
|
||||||
- [writing-great-skills](productivity/writing-great-skills/SKILL.md) — Reference for writing and editing skills well — the vocabulary and principles that make a skill predictable.
|
- [project-context-pack](engineering/project-context-pack/SKILL.md) — Build a bounded repo context pack (project map, codebase index, cached memory file) so later work uses fd/rg/tree-sitter/LSP instead of repeated browsing.
|
||||||
|
- [research-vault](pkm/research-vault/SKILL.md) — Research a topic through a one-question-at-a-time learning conversation, answer directly, share resources when useful, and save a linked OKF-conformant research packet in the Obsidian vault.
|
||||||
|
- [setup-skills](engineering/setup-skills/SKILL.md) — Configure this repo for the engineering skills, set up its issue tracker, triage label vocabulary, and domain doc layout. Run once before first use of the other engineering skills.
|
||||||
|
- [tmux-launch-agent](misc/tmux-launch-agent/SKILL.md) — Fork a new agent CLI session into a new tmux window, detected from the current agent.
|
||||||
|
- [youtube-video-capture](pkm/youtube-video-capture/SKILL.md) — Fetch subtitles from a YouTube video, summarize the content, and save both the summary and raw subtitles to the Video bundle in the Obsidian vault.
|
||||||
|
|
||||||
## Model-invoked
|
## Model-invoked
|
||||||
|
|
||||||
- [audio-product-dsp](deprecated/audio-production-dispatcher/SKILL.md) — Dispatch audio product DSP hardware/software engineering requests to the best specialist workflow with measurable product-focused outputs.
|
- [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.
|
||||||
- [forge-interaction](engineering/forge-interaction/SKILL.md) — Work safely with GitHub and Gitea repositories, issues, pull requests, releases, and remote forge state.
|
- [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.
|
||||||
- [forge-preferences](personal/forge-preferences/SKILL.md) — Apply Steve's personal or project-specific GitHub/Gitea forge preferences.
|
|
||||||
- [grilling](productivity/grilling/SKILL.md) — Interview the user relentlessly about a plan or design.
|
|
||||||
- [project-context-pack](engineering/project-context-pack/SKILL.md) — Build and refresh a bounded repo context memory file so agents use disciplined search instead of repeated browsing.
|
|
||||||
- [research-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.
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
# Deprecated Skills
|
|
||||||
|
|
||||||
## User-invoked
|
|
||||||
|
|
||||||
_None yet._
|
|
||||||
|
|
||||||
## Model-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.
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
---
|
|
||||||
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,149 +0,0 @@
|
|||||||
---
|
|
||||||
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,10 +1,15 @@
|
|||||||
# Engineering Skills
|
# Engineering Skills
|
||||||
|
|
||||||
|
Daily code work.
|
||||||
|
|
||||||
## User-invoked
|
## User-invoked
|
||||||
|
|
||||||
_None yet._
|
- [commit-staged](commit-staged/SKILL.md) — Commit staged files with a conventional commit message.
|
||||||
|
- [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
|
## Model-invoked
|
||||||
|
|
||||||
- [forge-interaction](forge-interaction/SKILL.md) — Work safely with GitHub and Gitea repositories, issues, pull requests, releases, and remote forge state.
|
- [lsp-code-analysis](lsp-code-analysis/SKILL.md) — Semantic code analysis via LSP. Navigate code (definitions, references, implementations), search symbols, preview refactorings, and get file outlines. Use for exploring unfamiliar codebases or performing safe refactoring.
|
||||||
- [project-context-pack](project-context-pack/SKILL.md) — Build and refresh a bounded repo context memory file so agents use disciplined search instead of repeated browsing.
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
name: commit-staged
|
||||||
|
description: Commit staged files with a conventional commit message.
|
||||||
|
disable-model-invocation: true
|
||||||
|
---
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
1. **Check the staging area** — Run `git diff --cached --stat`. If empty, report "nothing staged" and stop.
|
||||||
|
|
||||||
|
Completion criterion: Staged files are listed, or the skill terminates.
|
||||||
|
|
||||||
|
2. **Draft the message** — Inspect `git diff --cached` to understand what changed. Write a [conventional commit](https://www.conventionalcommits.org/) message:
|
||||||
|
|
||||||
|
- Format: `type(scope): summary` — scope is optional.
|
||||||
|
- Common types: `feat`, `fix`, `refactor`, `docs`, `chore`, `test`, `style`, `perf`, `ci`, `build`.
|
||||||
|
- Summary: imperative mood, lowercase, no period, ≤72 characters.
|
||||||
|
- Body (if needed): wrap at 72 characters, explain _what_ and _why_, not _how_.
|
||||||
|
|
||||||
|
Completion criterion: A conventional commit message is composed.
|
||||||
|
|
||||||
|
3. **Commit** — Run `git commit -m "<message>"`. If the commit fails, report the error and stop.
|
||||||
|
|
||||||
|
Completion criterion: `git commit` exits 0.
|
||||||
|
|
||||||
|
4. **Report** — Print the short commit hash and the first line of the message.
|
||||||
|
|
||||||
|
Completion criterion: The hash and message are printed.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "Commit Staged"
|
||||||
|
short_description: "Commit staged changes to the repository"
|
||||||
|
policy:
|
||||||
|
allow_implicit_invocation: false
|
||||||
@@ -1,212 +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.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Forge Interaction
|
|
||||||
|
|
||||||
Use this skill for Git forge work on GitHub or Gitea, especially when the user asks to:
|
|
||||||
|
|
||||||
- open a PR;
|
|
||||||
- create, list, modify, or comment on issues;
|
|
||||||
- create, list, modify, comment on, or merge pull requests;
|
|
||||||
- check CI;
|
|
||||||
- look at the repo on the forge;
|
|
||||||
- push a branch;
|
|
||||||
- publish changes;
|
|
||||||
- make or inspect a release.
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
Choose the correct forge CLI and use it to interact with issues, pull requests, releases, CI, and repository state.
|
|
||||||
|
|
||||||
This skill is intended to perform 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 forge and tool.
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
name: implement-isolation-tmux
|
||||||
|
description: "Dispatch a child agent in an isolated git worktree to implement a piece of work based on a PRD or set of issues."
|
||||||
|
disable-model-invocation: true
|
||||||
|
---
|
||||||
|
|
||||||
|
## Invocation
|
||||||
|
|
||||||
|
/skill:implement-isolation-tmux <N> [--base <branch>] [--force]
|
||||||
|
|
||||||
|
- `<N>` — required, the issue number
|
||||||
|
- `--base <branch>` — optional, target base branch (default: repo default branch)
|
||||||
|
- `--force` — optional, remove the existing worktree directory first, then recreate
|
||||||
|
|
||||||
|
Triage labels follow the vocabulary in `docs/agents/triage-labels.md`.
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
## 1. Claim and isolate
|
||||||
|
|
||||||
|
Change the issue triage label to `in-progress`, remove all other labels. Derive `<issue-slug>` from the issue title: lowercase, replace spaces with hyphens, strip non-alphanumeric characters (keep hyphens). Then create a git worktree for the ticket as a sibling directory named `../issue-<N>-<issue-slug>`, checked out from the base branch. Switch to the worktree. The branch name matches the directory: `issue-<N>-<issue-slug>`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git worktree add -b issue-<N>-<issue-slug> ../issue-<N>-<issue-slug> <base>
|
||||||
|
```
|
||||||
|
|
||||||
|
Stay in the worktree directory for the rest of the process.
|
||||||
|
|
||||||
|
Completion criterion: The issue label is confirmed as `in-progress` and the worktree exists at `../issue-<N>-<issue-slug>` with branch `issue-<N>-<issue-slug>` checked out.
|
||||||
|
|
||||||
|
### 2. Compose the child prompt
|
||||||
|
|
||||||
|
Assemble a single prompt that the child agent will receive. Include:
|
||||||
|
|
||||||
|
- **Issue body** — the full markdown body of the issue.
|
||||||
|
- **Comments** — all comments, if any
|
||||||
|
- **Standing instruction**: "Implement the work described by the user in the spec or tickets, using tdd skill where possible. Run typechecking regularly, run single test files regularly, and run the full test suite once at the end. Once done, use code-review skill to review the work. Commit your work and push the new branch and create a PR with a link to the issue, one-sentence summary, and short key-changes list. Comment on the issue with the PR link: `PR opened: <url>`. Change the issue triage label to needs-review when done. Remove all other labels"
|
||||||
|
|
||||||
|
- **Failure instruction**: "If any step fails, report where you stopped and what remains for manual recovery. Print the exact commands needed."
|
||||||
|
|
||||||
|
Completion criterion: The prompt is composed with all required sections (issue body, comments, standing instruction, failure instruction).
|
||||||
|
|
||||||
|
### 3. Launch the child via tmux-launch-agent
|
||||||
|
|
||||||
|
Use the `tmux-launch-agent` skill to fork the child agent into a new tmux window. Tmux-launch-agent handles agent detection, config lookup, command building (prompt-file or stdin-pipe), mise/SHELL wrapping, and `tmux new-window` creation.
|
||||||
|
|
||||||
|
Pass these parameters:
|
||||||
|
|
||||||
|
| Parameter | Value |
|
||||||
|
|-----------|-------|
|
||||||
|
| `--name` | `"issue-<N>-<issue-slug>"` — tmux window title |
|
||||||
|
| Remaining args | The full prompt text composed in step 2 |
|
||||||
|
|
||||||
|
Ensure the child agent starts in the worktree. If the current pane's working directory is already inside `<absolute-worktree-path>` (from step 1), tmux-launch-agent's new window inherits it. Otherwise, pass `-c <absolute-worktree-path>` when invoking tmux-launch-agent so the child agent starts in the worktree directory.
|
||||||
|
|
||||||
|
Let tmux-launch-agent handle prompt-file creation — it writes a temp file automatically when the target agent uses prompt-file mode. Pass the prompt text inline.
|
||||||
|
|
||||||
|
The window stays open after the child completes so the user can review the output.
|
||||||
|
|
||||||
|
Completion criterion: `tmux-launch-agent` exits 0 and a new tmux window appears with the child agent session active.
|
||||||
|
|
||||||
|
### 4. Print summary
|
||||||
|
|
||||||
|
After launching, print:
|
||||||
|
|
||||||
|
- Issue number and title
|
||||||
|
- Worktree path
|
||||||
|
- Branch name
|
||||||
|
- Base branch
|
||||||
|
- Agent used (detected by tmux-launch-agent)
|
||||||
|
- Tmux window name (so the user can find it)
|
||||||
|
- Cleanup command: `git worktree remove <worktree-path> && git worktree prune`
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "Implement Isolation Tmux"
|
||||||
|
short_description: "Implement isolation using tmux for the agent"
|
||||||
|
policy:
|
||||||
|
allow_implicit_invocation: false
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
name: implement-isolation
|
||||||
|
description: "Implement a piece of work based on a spec or set of tickets in isolation."
|
||||||
|
disable-model-invocation: true
|
||||||
|
---
|
||||||
|
|
||||||
|
## Invocation
|
||||||
|
|
||||||
|
/skill:implement-isolation <N> [--base <branch>] [--force]
|
||||||
|
|
||||||
|
- `<N>` — required, the ticket/issue number
|
||||||
|
- `--base <branch>` — optional, target base branch (default: repo default branch)
|
||||||
|
- `--force` — optional, remove the existing worktree directory first, then recreate
|
||||||
|
|
||||||
|
Triage labels follow the vocabulary in `docs/agents/triage-labels.md`.
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
### 1. Claim and isolate
|
||||||
|
|
||||||
|
Change the issue triage label to `in-progress`, remove all other labels. Derive `<issue-slug>` from the issue title: lowercase, replace spaces with hyphens, strip non-alphanumeric characters (keep hyphens). Then create a git worktree for the ticket as a sibling directory named `../issue-<N>-<issue-slug>`, checked out from the base branch. Switch to the worktree. The branch name matches the directory: `issue-<N>-<issue-slug>`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git worktree add -b issue-<N>-<issue-slug> ../issue-<N>-<issue-slug> <base>
|
||||||
|
```
|
||||||
|
|
||||||
|
Stay in the worktree directory for the rest of the process.
|
||||||
|
|
||||||
|
Completion criterion: The issue label is confirmed as `in-progress` and the worktree exists at `../issue-<N>-<issue-slug>` with branch `issue-<N>-<issue-slug>` checked out.
|
||||||
|
|
||||||
|
### 2. Implement
|
||||||
|
|
||||||
|
Start a subagent to implement the work in isolation. The subagent receives a single prompt that includes:
|
||||||
|
|
||||||
|
- Issue body: the full markdown body of the issue.
|
||||||
|
- Comments: all comments, if any
|
||||||
|
- Implement: the work described by the user in the spec or tickets, using tdd skill where possible. Run typechecking regularly, run single test files regularly, and run the full test suite once at the end.
|
||||||
|
|
||||||
|
Completion criterion: The subagent exits with code 0 and all changes are committed.
|
||||||
|
|
||||||
|
### 3. Review
|
||||||
|
|
||||||
|
Start a reviewer subagent that runs the `code-review` skill against the base branch to identify findings in the diff. Wait for the reviewer to finish.
|
||||||
|
|
||||||
|
Review each finding and fix the code until zero findings remain. Commit fixes as you go.
|
||||||
|
|
||||||
|
Completion criterion: All code-review findings are resolved (zero open findings) and fixes are committed.
|
||||||
|
|
||||||
|
### 4. Ship
|
||||||
|
|
||||||
|
Push the branch, open a PR. The PR description must include a link to the ticket and a key-changes list.
|
||||||
|
|
||||||
|
Comment on the issue: `PR opened: <url>`. Label the issue `needs-review`, remove all other labels.
|
||||||
|
|
||||||
|
Print a cleanup command for the user: `git worktree remove ../issue-<N>-<issue-slug> && git worktree prune`
|
||||||
|
|
||||||
|
### Failure recovery
|
||||||
|
|
||||||
|
If the implement subagent exits non-zero or the review finds issues that cannot be fully resolved, stop. Report to the user: issue number, what was completed, what failed, and the exact commands needed to resume or clean up (including `git worktree remove ...`). Do not push partial work.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "Implement Isolation"
|
||||||
|
short_description: "Implement isolation for the agent"
|
||||||
|
policy:
|
||||||
|
allow_implicit_invocation: false
|
||||||
@@ -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
|
||||||
@@ -1,36 +1,37 @@
|
|||||||
---
|
---
|
||||||
name: project-context-pack
|
name: project-context-pack
|
||||||
description: Use when the user wants a bounded repo context pack, project map, codebase index, cached memory file, or fewer repeated codebase searches.
|
description: 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.
|
||||||
|
disable-model-invocation: true
|
||||||
---
|
---
|
||||||
|
|
||||||
# Project Context Pack
|
# Project Context Pack
|
||||||
|
|
||||||
Create or refresh a bounded project memory file, then use it as the navigation map for the repo.
|
Create or refresh a bounded project memory file, then use it to navigate the codebase with discipline.
|
||||||
|
|
||||||
## Target
|
## Output target
|
||||||
|
|
||||||
Default cache path: `.agent/project-context.md` at the project root.
|
Default cache path: `.agent/project-context.md` at the project root.
|
||||||
|
|
||||||
Use a user-named path when provided. If the user did not explicitly ask for a cache file, ask before the first write into the repo. Never write secrets, credentials, private keys, tokens, browser data, personal data, or raw `.env*` content into the cache.
|
If the user names another path, use that path. If writing into the repo is risky or unexpected, ask before writing. Never write secrets into the cache.
|
||||||
|
|
||||||
## Procedure
|
## Procedure
|
||||||
|
|
||||||
1. **Root.** Prefer `git rev-parse --show-toplevel`; otherwise use `pwd`. Completion: the pack records the absolute root and current relative working directory.
|
1. **Find the root.** Prefer `git rev-parse --show-toplevel`; otherwise use `pwd`. Completion: you know the root and current relative working directory.
|
||||||
2. **Survey.** Use `fd` and `rg` first; fall back to `find` and `grep`. Respect ignore files by default. Exclude `.git`, dependency directories, build outputs, caches, generated files, binaries, and large artifacts. Completion: the pack has a bounded file inventory and top-level tree before broad file reads.
|
2. **Inventory cheaply.** Use `fd` and `rg` first; fall back to `find` and `grep`. Respect ignore files by default. Exclude `.git`, dependency directories, build outputs, caches, generated files, binaries, and large artifacts. Completion: you have a bounded file inventory and top-level tree without opening many files.
|
||||||
3. **Classify.** Detect language, framework, package manager, build/test tools, docs, agent guidance, and likely entry points from filenames and small config reads. Completion: every project-type claim cites at least one file.
|
3. **Classify the project.** Detect language, framework, package manager, build/test tools, docs, agent guidance, and likely entry points from filenames and small config reads. Completion: the pack names the project type and the files that justify it.
|
||||||
4. **Read selectively.** Read only high-value files by default: README, agent guidance, package/build config, docs index, main entry points, and files directly relevant to the user's current request. Cap per-file and total content. Completion: every included file has a recorded reason.
|
4. **Read selectively.** Read only high-value files by default: README, agent guidance, package/build config, docs index, main entry points, and files directly relevant to the user's current request. Cap per-file and total content. Completion: every included file has a reason.
|
||||||
5. **Index before browsing.** For code questions, prefer tree-sitter or LSP when available. Otherwise use `rg` for definitions, exports, imports, routes, tests, commands, TODOs, and error strings. Completion: the pack has search or symbol notes before any broad source reading.
|
5. **Index symbols before browsing.** For code questions, prefer tree-sitter or LSP when available. Otherwise use `rg` patterns for definitions, exports, imports, routes, tests, commands, TODOs, and error strings. Completion: the pack has symbol/search notes before any broad source reading.
|
||||||
6. **Cache.** Create or update the memory file with the template below. Completion: the file records timestamp, root, status, commands/searches used, files inspected, exclusions, and next navigation rules.
|
6. **Write the memory file.** Create or update the cache with the template below. Completion: the file exists and records when it was generated, commands/searches used, files inspected, and next navigation rules.
|
||||||
7. **Reuse.** Before future exploration, read the cache first. If stale or insufficient, refresh the smallest relevant section instead of rebuilding everything. Completion: subsequent work uses the cache or explains why it was bypassed.
|
7. **Use the cache.** Before future exploration, read the cache first. If stale or insufficient, refresh the smallest relevant section instead of rebuilding everything. Completion: subsequent work cites the cache and avoids repeated file browsing.
|
||||||
|
|
||||||
## Navigation discipline
|
## Navigation discipline
|
||||||
|
|
||||||
- Do not wander file-by-file. Start with the cache, `fd`, `rg`, tree-sitter, or LSP.
|
- Do not wander file-by-file. Start with `fd`, `rg`, tree-sitter, LSP, or the cache.
|
||||||
- Track inspected files in the memory file.
|
- Track inspected files in the memory file.
|
||||||
- Prefer narrow searches over whole-file reads.
|
- Prefer narrow searches over whole-file reads.
|
||||||
- Prefer symbol outlines and matching snippets over full content.
|
- Prefer symbol outlines and matching snippets over full content.
|
||||||
- Re-read a file only when it changed, the cache is stale, or exact details are needed.
|
- Re-read a file only when it changed, the cache is stale, or the exact details are needed.
|
||||||
- Ask before full-content extraction, large scans, or sensitive-file inspection.
|
- Ask before full-content extraction, large scans, or including sensitive files such as `.env`, credentials, private keys, tokens, browser profiles, or personal data.
|
||||||
|
|
||||||
## Useful command patterns
|
## Useful command patterns
|
||||||
|
|
||||||
@@ -38,8 +39,6 @@ Use a user-named path when provided. If the user did not explicitly ask for a ca
|
|||||||
root=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
root=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
||||||
cd "$root"
|
cd "$root"
|
||||||
|
|
||||||
date -Iseconds
|
|
||||||
|
|
||||||
# Inventory
|
# Inventory
|
||||||
fd --type f --hidden --exclude .git 2>/dev/null || find . -type f -not -path './.git/*'
|
fd --type f --hidden --exclude .git 2>/dev/null || find . -type f -not -path './.git/*'
|
||||||
tree -a -I '.git|node_modules|dist|build|target|.venv|__pycache__|vendor|coverage' -L 3
|
tree -a -I '.git|node_modules|dist|build|target|.venv|__pycache__|vendor|coverage' -L 3
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
---
|
||||||
|
name: setup-skills
|
||||||
|
description: 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.
|
||||||
|
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)
|
||||||
|
- Triage labels: the strings used for the canonical triage roles, labels are defined in `triage-labels.md`
|
||||||
|
- Domain docs: where `CONTEXT.md` and ADRs live, and the consumer rules for reading them
|
||||||
|
- **ADR wiki** — where ADRs are stored (forge wiki, cloned into `docs/adr/`), and the clone/push workflow
|
||||||
|
|
||||||
|
This is a prompt-driven skill, not a deterministic script. Explore, present what you found, confirm with the user, then write.
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
### 1. Explore
|
||||||
|
|
||||||
|
Look at the current repo to understand its starting state. Read whatever exists; don't assume:
|
||||||
|
|
||||||
|
- `git remote -v` and `.git/config` — is this a GitHub repo? Which one?
|
||||||
|
- `AGENTS.md` and `CLAUDE.md` at the repo root — does either exist? Is there already an `## Agent skills` section in either?
|
||||||
|
- `CONTEXT.md` and `CONTEXT-MAP.md` at the repo root
|
||||||
|
- `docs/adr/` and any `src/*/docs/adr/` directories
|
||||||
|
- `docs/agents/` — does this skill's prior output already exist?
|
||||||
|
- Is the `triage` skill installed? (a `triage` skill folder alongside this one, or `triage` in your available skills.) This decides whether Section B runs at all.
|
||||||
|
- Monorepo signals — a `pnpm-workspace.yaml`, a `workspaces` field in `package.json`, or a populated `packages/*` with its own `src/`. Present only in a genuinely large multi-package repo; their absence means single-context, which is almost every repo.
|
||||||
|
|
||||||
|
### 2. Present findings and ask
|
||||||
|
|
||||||
|
Summarise what's present and what's missing. Then take the sections in order — one section, one answer, then the next.
|
||||||
|
|
||||||
|
Lead each section with the recommended answer so the user can accept it in a word. Give a one-line explainer only when the choice genuinely branches; skip the section entirely when exploration already settled it (Section B when `triage` isn't installed, Section C when there's no monorepo).
|
||||||
|
|
||||||
|
Section A - Issue tracker:
|
||||||
|
|
||||||
|
> Explainer: The "issue tracker" is where issues live for this repo. Skills like `to-tickets`, `triage`, `to-spec`, and `qa` read from and write to it — they need to know whether to call `gh issue create`, or follow some other workflow you describe. Pick the place you actually track work for this repo.
|
||||||
|
|
||||||
|
Default posture: these skills were designed for GitHub. If a `git remote` points at GitHub, propose that. If a `git remote` points at GitLab (`gitlab.com` or a self-hosted host), propose GitLab. If a `git remote` point at a Gitea (a self-hosted host with a `gitea` in the url). Otherwise ask the user, offer:
|
||||||
|
|
||||||
|
- **GitHub** — issues live in the repo's GitHub Issues (uses the `gh` CLI)
|
||||||
|
- **GitLab** — issues live in the repo's GitLab Issues (uses the [`glab`](https://gitlab.com/gitlab-org/cli) CLI)
|
||||||
|
- Gitea — issues live in the repo's Gitea Issues (uses the `tea` CLI)
|
||||||
|
- **Other** (Jira, Linear, etc.) — ask the user to describe the workflow in one paragraph; the skill will record it as freeform prose
|
||||||
|
|
||||||
|
Record the choice in `docs/agents/issue-tracker.md`. The GitHub and GitLab templates carry a "PRs as a request surface" flag, defaulted **off** — leave it off and don't raise it; a user who wants external PRs in the triage queue can flip the flag in the file later.
|
||||||
|
|
||||||
|
**Section B — Triage label vocabulary.** Skip this section entirely if the `triage` skill isn't installed (exploration told you) — an uninstalled skill needs no labels.
|
||||||
|
|
||||||
|
If it is installed, ask exactly one question:
|
||||||
|
|
||||||
|
> Do you want to keep the default triage labels? (recommended: **yes**)
|
||||||
|
|
||||||
|
The defaults canonical roles are listed in `triage-labels.md` each label string equal to its name. On **yes**, write them as-is. Only if the user says no — usually because their tracker already uses other names (e.g. `bug:triage` for `needs-triage`) — collect the overrides so `triage` applies existing labels instead of creating duplicates.
|
||||||
|
|
||||||
|
**Section C — Domain docs.** Default to **single-context** — one `CONTEXT.md` + `docs/adr/` at the repo root. This fits almost every repo; write it without asking.
|
||||||
|
|
||||||
|
Offer **multi-context** — a root `CONTEXT-MAP.md` pointing to per-context `CONTEXT.md` files — only when exploration found monorepo signals. Then confirm which layout they want.
|
||||||
|
|
||||||
|
**Section D — ADR wiki.**
|
||||||
|
|
||||||
|
> Explainer: ADRs document significant architectural decisions. Rather than storing them in the source repo (where they clutter PRs, pollute git history, and live under a different lifecycle than code), they live on the forge's wiki. The wiki is cloned into `docs/adr/` during setup — skills read and write ADRs by relative path, never knowing the difference. At end of session, the agent pushes any new or modified ADRs to the wiki remote.
|
||||||
|
|
||||||
|
Derive the wiki URL from the forge detected in Section A:
|
||||||
|
|
||||||
|
| Forge | Wiki URL pattern |
|
||||||
|
| --- | --- |
|
||||||
|
| **GitHub** | `https://github.com/<owner>/<repo>.wiki.git` |
|
||||||
|
| **Gitea** | `https://<host>/<owner>/<repo>.wiki.git` |
|
||||||
|
| **GitLab** | `https://gitlab.com/<owner>/<repo>.wiki.git` |
|
||||||
|
|
||||||
|
After the forge is confirmed, present the workflow and confirm:
|
||||||
|
|
||||||
|
> Explainer: The agent will need to authenticate to push ADR changes to the wiki. This can use the same credentials the forge CLI (`gh` / `tea`) already stores, or a dedicated token. On every session start, `docs/adr/` is pulled to catch any web UI edits. At end of session, new/modified ADRs are committed and pushed with `git pull --rebase` to handle concurrent edits.
|
||||||
|
|
||||||
|
- **Wiki clone bootstrap** — yes, clone into `docs/adr/` and add `docs/adr/` to `.gitignore`. (Always yes for wiki-based ADRs.)
|
||||||
|
- **Auth method** — from forge CLI (`gh` / `tea`) / SSH key / dedicated token (default: forge CLI)
|
||||||
|
- **Commit message convention** — `docs(adr): <action> ADR-NNNN — <short description>` (user can customise)
|
||||||
|
|
||||||
|
Record the answers in `docs/agents/adr-wiki.md`.
|
||||||
|
|
||||||
|
### 3. Confirm and edit
|
||||||
|
|
||||||
|
Show the user a draft of:
|
||||||
|
|
||||||
|
- The `## Agent skills` block to add to whichever of `CLAUDE.md` / `AGENTS.md` is being edited (see step 4 for selection rules)
|
||||||
|
- The contents of `docs/agents/issue-tracker.md`, `docs/agents/domain.md`, `docs/agents/adr-wiki.md` and `docs/agents/triage-labels.md` (the last only when `triage` is installed)
|
||||||
|
|
||||||
|
Let them edit before writing.
|
||||||
|
|
||||||
|
### 4. Write
|
||||||
|
|
||||||
|
**Pick the file to edit:**
|
||||||
|
|
||||||
|
- If `CLAUDE.md` exists, edit it.
|
||||||
|
- Else if `AGENTS.md` exists, edit it.
|
||||||
|
- If neither exists, ask the user which one to create — don't pick for them.
|
||||||
|
|
||||||
|
Never create `AGENTS.md` when `CLAUDE.md` already exists (or vice versa) — always edit the one that's already there.
|
||||||
|
|
||||||
|
If an `## Agent skills` block already exists in the chosen file, update its contents in-place rather than appending a duplicate. Don't overwrite user edits to the surrounding sections.
|
||||||
|
|
||||||
|
The block:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Agent skills
|
||||||
|
|
||||||
|
### Issue tracker
|
||||||
|
|
||||||
|
[one-line summary of where issues are tracked]. See `docs/agents/issue-tracker.md`.
|
||||||
|
|
||||||
|
### Triage labels
|
||||||
|
|
||||||
|
[one-line summary of the label vocabulary]. See `docs/agents/triage-labels.md`.
|
||||||
|
|
||||||
|
### Domain docs
|
||||||
|
|
||||||
|
[one-line summary of layout — "single-context" or "multi-context"]. See `docs/agents/domain.md`.
|
||||||
|
|
||||||
|
### ADR wiki
|
||||||
|
|
||||||
|
[one-line summary — forge, wiki URL, auth method]. See `docs/agents/adr-wiki.md`.
|
||||||
|
|
||||||
|
## Project Context Pack
|
||||||
|
|
||||||
|
[Agent memory file that describes the repo's context, codebase, and navigation rules]. See `.agents/project-context.md`.
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Then write the docs files:
|
||||||
|
|
||||||
|
- [issue-tracker-github.md](./issue-tracker-github.md) — GitHub issue tracker, including wayfinding operations
|
||||||
|
- [issue-tracker-gitlab.md](./issue-tracker-gitlab.md) — GitLab issue tracker, including wayfinding operations
|
||||||
|
- [issue-tracker-gitea.md](./issue-tracker-gitea.md) — Gitea issue tracker, including wayfinding operations
|
||||||
|
- [triage-labels.md](./triage-labels.md) — label mapping
|
||||||
|
- [domain.md](./domain.md) — domain doc consumer rules + layout
|
||||||
|
- [adr-wiki.md](./adr-wiki.md) — ADR wiki clone and push workflow
|
||||||
|
|
||||||
|
For GitHub, GitLab, and Gitea, the generated `docs/agents/issue-tracker.md` must include the full "Wayfinding operations" section from the selected template. Do not omit that section when composing the project file.
|
||||||
|
|
||||||
|
For "other" issue trackers, write `docs/agents/issue-tracker.md` from scratch using the user's description, and include an equivalent wayfinding section only if the user described a workable wayfinding workflow for that tracker.
|
||||||
|
|
||||||
|
### 5. Done
|
||||||
|
|
||||||
|
Tell the user the setup is complete, which engineering skills will now read from these files, and that ADR changes are pushed to the forge wiki at end of session. Mention they can edit `docs/agents/*.md` directly later — re-running this skill is only necessary if they want to switch issue trackers, restart from scratch, or reconfigure the ADR wiki or agent CLI.
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# ADR Wiki
|
||||||
|
|
||||||
|
Architecture Decision Records live on the forge wiki and are cloned into `docs/adr/` during setup.
|
||||||
|
|
||||||
|
## Wiki URL
|
||||||
|
|
||||||
|
```
|
||||||
|
<wiki-url>
|
||||||
|
```
|
||||||
|
|
||||||
|
Derived from the forge remote during `/setup-skills`.
|
||||||
|
|
||||||
|
## Bootstrap
|
||||||
|
|
||||||
|
On first setup, `/setup-skills` clones the wiki:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <wiki-url> docs/adr/
|
||||||
|
```
|
||||||
|
|
||||||
|
And adds `docs/adr/` to `.gitignore`.
|
||||||
|
|
||||||
|
On subsequent sessions, pull the latest:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd docs/adr/ && git pull --rebase
|
||||||
|
```
|
||||||
|
|
||||||
|
## Auth
|
||||||
|
|
||||||
|
Authentication for pushing ADR changes uses: `<auth-method>`
|
||||||
|
|
||||||
|
- **Forge CLI token** — `gh` or `tea` already stores credentials; use them for git push via HTTPS with token auth.
|
||||||
|
- **SSH key** — wiki remote uses `git@<host>:<owner>/<repo>.wiki.git`.
|
||||||
|
- **Dedicated token** — stored in env var `GIT_WIKI_TOKEN`; used as password in HTTPS URL.
|
||||||
|
|
||||||
|
## Session-end push
|
||||||
|
|
||||||
|
At end of every session, the agent:
|
||||||
|
|
||||||
|
1. `cd docs/adr/ && git add -A && git commit -m "docs(adr): <action> ADR-NNNN — <description>"`
|
||||||
|
2. `git pull --rebase` (handle any web UI edits)
|
||||||
|
3. `git push`
|
||||||
|
|
||||||
|
If a conflict arises during rebase, surface it to the user for resolution.
|
||||||
|
|
||||||
|
## Commit message convention
|
||||||
|
|
||||||
|
```
|
||||||
|
docs(adr): add ADR-NNNN — title
|
||||||
|
docs(adr): update ADR-NNNN — reason
|
||||||
|
docs(adr): remove ADR-NNNN — superseded by ADR-NNNN
|
||||||
|
```
|
||||||
|
|
||||||
|
## Consumer skills
|
||||||
|
|
||||||
|
These skills read from `docs/adr/` by relative path — the wiki clone is transparent:
|
||||||
|
|
||||||
|
- `diagnosing-bugs`
|
||||||
|
- `tdd`
|
||||||
|
- `improve-codebase-architecture`
|
||||||
|
- `domain-modeling`
|
||||||
|
- `grill-with-docs`
|
||||||
|
|
||||||
|
These skills may create ADRs in `docs/adr/`; the wiki push is handled at session end:
|
||||||
|
|
||||||
|
- `domain-modeling`
|
||||||
|
- `improve-codebase-architecture`
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "Setup Engineering Skills"
|
||||||
|
short_description: "Setup engineering skills for the agent"
|
||||||
|
policy:
|
||||||
|
allow_implicit_invocation: false
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# Domain Docs
|
||||||
|
|
||||||
|
How the engineering skills should consume this repo's domain documentation when exploring the codebase.
|
||||||
|
|
||||||
|
## Before exploring, read these
|
||||||
|
|
||||||
|
- **`CONTEXT.md`** at the repo root, or
|
||||||
|
- **`CONTEXT-MAP.md`** at the repo root if it exists — it points at one `CONTEXT.md` per context. Read each one relevant to the topic.
|
||||||
|
- **`docs/adr/`** — read ADRs that touch the area you're about to work in. In multi-context repos, also check `/doc/adr/<context>/` for context-scoped decisions.
|
||||||
|
|
||||||
|
If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill (reached via `/grill-with-docs` and `/improve-codebase-architecture`) creates them lazily when terms or decisions actually get resolved.
|
||||||
|
|
||||||
|
## File structure
|
||||||
|
|
||||||
|
Single-context repo (most repos):
|
||||||
|
|
||||||
|
```
|
||||||
|
/
|
||||||
|
├── CONTEXT.md
|
||||||
|
├── docs/adr/
|
||||||
|
│ ├── 0001-event-sourced-orders.md
|
||||||
|
│ └── 0002-postgres-for-write-model.md
|
||||||
|
├── src/
|
||||||
|
└── .gitignore ← docs/adr/ ignored
|
||||||
|
```
|
||||||
|
|
||||||
|
Multi-context repo (presence of `CONTEXT-MAP.md` at the root):
|
||||||
|
|
||||||
|
```
|
||||||
|
/
|
||||||
|
├── CONTEXT-MAP.md
|
||||||
|
├── src/
|
||||||
|
├── .gitignore ← docs/adr/ ignored
|
||||||
|
└── docs/adr/ ← system-wide decisions
|
||||||
|
├── ordering/ ← context scope decisions
|
||||||
|
│ ├── CONTEXT.md
|
||||||
|
│ └── 0001-event-source-orders.md
|
||||||
|
└── billing/ ← context scope decisions
|
||||||
|
├── CONTEXT.md
|
||||||
|
└── 0002-postgres-for-write-model.md
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## ADR lifecycle
|
||||||
|
|
||||||
|
ADRs live on the forge wiki and are cloned into `docs/adr/` during setup (`/setup-matt-pocock-skills`). The source repo ignores `docs/adr/` via `.gitignore`.
|
||||||
|
|
||||||
|
- **Reading** — skills read ADRs by relative path (`docs/adr/...`) as before. The wiki clone is transparent.
|
||||||
|
- **Creating / updating** — skills write ADRs to `docs/adr/` as files. Changes accumulate in the wiki clone's local git state.
|
||||||
|
- **Pushing** — at end of session, the agent commits new/modified ADRs to the wiki clone and pushes to the forge wiki remote, using `git pull --rebase` before push to handle any concurrent web edits.
|
||||||
|
- **Commit message convention**: `docs(adr): <action> ADR-NNNN — <short description>`
|
||||||
|
|
||||||
|
## Use the glossary's vocabulary
|
||||||
|
|
||||||
|
When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids.
|
||||||
|
|
||||||
|
If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`).
|
||||||
|
|
||||||
|
## Flag ADR conflicts
|
||||||
|
|
||||||
|
If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
|
||||||
|
|
||||||
|
> _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_
|
||||||
|
|
||||||
|
## ADR storage
|
||||||
|
|
||||||
|
ADR documents are stored in a clone repo of the forge wiki, if the `docs/adr` folder does not exist ask the user for the repo url and clone it. Commit each changes without asking the user for a commit message and push to the remote.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Issue tracker: Gitea
|
||||||
|
|
||||||
|
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.
|
||||||
|
- **Read an issue**: `tea issue <number> --comments`. Use `-o json` for machine-readable output.
|
||||||
|
- **List issues**: `tea issue list --state open -o json` with appropriate `--labels` and `--state` filters.
|
||||||
|
- **Comment on an issue**: `tea comment <number> "..."`.
|
||||||
|
- **Apply / remove labels**: `tea issue edit <number> --add-label "..."` / `--remove-label "..."`. Multiple labels can be comma-separated or by repeating the flag. Labels need to be created first with `tea label create --name "..." --color "..."`.
|
||||||
|
- **Close**: `tea issue close <number>`. `tea issue close` does not accept a closing comment, so post the explanation first with `tea comment <number> "..."`, then close.
|
||||||
|
|
||||||
|
Infer the repo from git remote -v — `tea` does this automatically when run inside a clone.
|
||||||
|
|
||||||
|
## Pull requests as a triage surface
|
||||||
|
|
||||||
|
**PRs as a request surface: no.** _(Set to `yes` if this repo treats external PRs as feature requests; `/triage` reads this flag.)_
|
||||||
|
|
||||||
|
When set to `yes`, PRs run through the same labels and states as issues, using the `tea pr` equivalents:
|
||||||
|
|
||||||
|
- **Read a PR**: `tea pr <number> --comments` and `tea api /repos/{owner}/{repo}/pulls/<number>.diff` for the diff.
|
||||||
|
- **List external PRs for triage**: `tea pr list --state open -o json` then keep only PRs whose author is not a project member/owner (a contributor's MR, not a maintainer's in-flight work).
|
||||||
|
- **Comment / label / close**: `tea comment <number> "..."`, `tea pr edit --add-label`/`--remove-label`, `tea pr close`.
|
||||||
|
|
||||||
|
Gitea shares one number space across issues and PRs, so a bare `#42` may be either — resolve with `tea pr 42` and fall back to `tea issue 42`.
|
||||||
|
|
||||||
|
## When a skill says "publish to the issue tracker"
|
||||||
|
|
||||||
|
Create a Gitea issue.
|
||||||
|
|
||||||
|
## When a skill says "fetch the relevant ticket"
|
||||||
|
|
||||||
|
Run `tea issue <number> --comments`.
|
||||||
|
|
||||||
|
## Wayfinding operations
|
||||||
|
|
||||||
|
Used by `/wayfinder`. The **map** is a single issue with **child** issues as tickets.
|
||||||
|
|
||||||
|
- **Map**: a single issue labelled `wayfinder:map`, holding the Notes / Decisions-so-far / Fog body. `tea issue create --label wayfinder:map`.
|
||||||
|
- **Child ticket**: an issue linked to the map as a GitHub sub-issue (`tea api` on the sub-issues endpoint). Where sub-issues aren't enabled, add the child to a task list in the map body and put `Part of #<map>` at the top of the child body. Labels: `wayfinder:<type>` (`research`/`prototype`/`grilling`/`task`). Once claimed, the ticket is assigned to the driving dev.
|
||||||
|
- **Blocking**: GitHub's **native issue dependencies** — the canonical, UI-visible representation. Add an edge with `tea api --method POST /repos/{owner}/{repo}/issues/<child>/dependencies -F index=<blocker-issue-number> -F repo=<blocker-repo-name> -F owner=<bocker-owner-name>`, where `<blocker-usse-number>` is the blocker's numeric **issue number** (`tea api repos/{owner}/{repo}/issues/<n> --jq ".number. .repository.name, .repository.owner"`, where `.number` is the `<blocker-issue-number>`, `.repository.name` is the `<blocker-repo-name>` and `.repository.owner` is the `<blocker-repo-owner>`. Where dependencies aren't available, fall back to a `Blocked by: #<n>, #<n>` line at the top of the child body. A ticket is unblocked when every blocker is closed.
|
||||||
|
- **Frontier query**: list the map's open dependencies (`tea api /repos/{owner}/{repo}/issues/<n>/dependencies | jq '.[] | select(.state = "open") .number'`, scoped to the map's sub-issues / task list), drop any with an open blocker (`list of dependencies is not empty`, or an open issue in the `Blocked by` line) or an assignee; first in map order wins.
|
||||||
|
- **Claim**: `tea issue edit <n> --add-assignees @me` — the session's first write.
|
||||||
|
- **Resolve**: `tea comments <n> "<answer>"`, then `tea issue close <n>`, then append a context pointer (gist + link) to the map's Decisions-so-far.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Issue tracker: GitHub
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
- **Create an issue**: `gh issue create --title "..." --body "..."`. Use a heredoc for multi-line bodies.
|
||||||
|
- **Read an issue**: `gh issue view <number> --comments`, filtering comments by `jq` and also fetching labels.
|
||||||
|
- **List issues**: `gh issue list --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'` with appropriate `--label` and `--state` filters.
|
||||||
|
- **Comment on an issue**: `gh issue comment <number> --body "..."`
|
||||||
|
- **Apply / remove labels**: `gh issue edit <number> --add-label "..."` / `--remove-label "..."`
|
||||||
|
- **Close**: `gh issue close <number> --comment "..."`
|
||||||
|
|
||||||
|
Infer the repo from `git remote -v` — `gh` does this automatically when run inside a clone.
|
||||||
|
|
||||||
|
## Pull requests as a triage surface
|
||||||
|
|
||||||
|
**PRs as a request surface: no.** _(Set to `yes` if this repo treats external PRs as feature requests; `/triage` reads this flag.)_
|
||||||
|
|
||||||
|
When set to `yes`, PRs run through the same labels and states as issues, using the `gh pr` equivalents:
|
||||||
|
|
||||||
|
- **Read a PR**: `gh pr view <number> --comments` and `gh pr diff <number>` for the diff.
|
||||||
|
- **List external PRs for triage**: `gh pr list --state open --json number,title,body,labels,author,authorAssociation,comments` then keep only `authorAssociation` of `CONTRIBUTOR`, `FIRST_TIME_CONTRIBUTOR`, or `NONE` (drop `OWNER`/`MEMBER`/`COLLABORATOR`).
|
||||||
|
- **Comment / label / close**: `gh pr comment`, `gh pr edit --add-label`/`--remove-label`, `gh pr close`.
|
||||||
|
|
||||||
|
GitHub shares one number space across issues and PRs, so a bare `#42` may be either — resolve with `gh pr view 42` and fall back to `gh issue view 42`.
|
||||||
|
|
||||||
|
## When a skill says "publish to the issue tracker"
|
||||||
|
|
||||||
|
Create a GitHub issue.
|
||||||
|
|
||||||
|
## When a skill says "fetch the relevant ticket"
|
||||||
|
|
||||||
|
Run `gh issue view <number> --comments`.
|
||||||
|
|
||||||
|
## Wayfinding operations
|
||||||
|
|
||||||
|
Used by `/wayfinder`. The **map** is a single issue with **child** issues as tickets.
|
||||||
|
|
||||||
|
- **Map**: a single issue labelled `wayfinder:map`, holding the Notes / Decisions-so-far / Fog body. `gh issue create --label wayfinder:map`.
|
||||||
|
- **Child ticket**: an issue linked to the map as a GitHub sub-issue (`gh api` on the sub-issues endpoint). Where sub-issues aren't enabled, add the child to a task list in the map body and put `Part of #<map>` at the top of the child body. Labels: `wayfinder:<type>` (`research`/`prototype`/`grilling`/`task`). Once claimed, the ticket is assigned to the driving dev.
|
||||||
|
- **Blocking**: GitHub's **native issue dependencies** — the canonical, UI-visible representation. Add an edge with `gh api --method POST repos/<owner>/<repo>/issues/<child>/dependencies/blocked_by -F issue_id=<blocker-db-id>`, where `<blocker-db-id>` is the blocker's numeric **database id** (`gh api repos/<owner>/<repo>/issues/<n> --jq .id`, _not_ the `#number` or `node_id`). GitHub reports `issue_dependencies_summary.blocked_by` (open blockers only — the live gate). Where dependencies aren't available, fall back to a `Blocked by: #<n>, #<n>` line at the top of the child body. A ticket is unblocked when every blocker is closed.
|
||||||
|
- **Frontier query**: list the map's open children (`gh issue list --state open`, scoped to the map's sub-issues / task list), drop any with an open blocker (`issue_dependencies_summary.blocked_by > 0`, or an open issue in the `Blocked by` line) or an assignee; first in map order wins.
|
||||||
|
- **Claim**: `gh issue edit <n> --add-assignee @me` — the session's first write.
|
||||||
|
- **Resolve**: `gh issue comment <n> --body "<answer>"`, then `gh issue close <n>`, then append a context pointer (gist + link) to the map's Decisions-so-far.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Issue tracker: GitLab
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
- **Create an issue**: `glab issue create --title "..." --description "..."`. Use a heredoc for multi-line descriptions. Pass `--description -` to open an editor.
|
||||||
|
- **Read an issue**: `glab issue view <number> --comments`. Use `-F json` for machine-readable output.
|
||||||
|
- **List issues**: `glab issue list -F json` with appropriate `--label` filters.
|
||||||
|
- **Comment on an issue**: `glab issue note <number> --message "..."`. GitLab calls comments "notes".
|
||||||
|
- **Apply / remove labels**: `glab issue update <number> --label "..."` / `--unlabel "..."`. Multiple labels can be comma-separated or by repeating the flag.
|
||||||
|
- **Close**: `glab issue close <number>`. `glab issue close` does not accept a closing comment, so post the explanation first with `glab issue note <number> --message "..."`, then close.
|
||||||
|
- **Merge requests**: GitLab calls PRs "merge requests". Use `glab mr create`, `glab mr view`, `glab mr note`, etc. — the same shape as `gh pr ...` with `mr` in place of `pr` and `note`/`--message` in place of `comment`/`--body`.
|
||||||
|
|
||||||
|
Infer the repo from `git remote -v` — `glab` does this automatically when run inside a clone.
|
||||||
|
|
||||||
|
## Merge requests as a triage surface
|
||||||
|
|
||||||
|
**MRs as a request surface: no.** _(Set to `yes` if this repo treats external merge requests as feature requests; `/triage` reads this flag.)_
|
||||||
|
|
||||||
|
When set to `yes`, MRs run through the same labels and states as issues, using the `glab mr` equivalents:
|
||||||
|
|
||||||
|
- **Read an MR**: `glab mr view <number> --comments` and `glab mr diff <number>` for the diff.
|
||||||
|
- **List external MRs for triage**: `glab mr list -F json`, then keep only MRs whose author is not a project member/owner (a contributor's MR, not a maintainer's in-flight work).
|
||||||
|
- **Comment / label / close**: `glab mr note`, `glab mr update --label`/`--unlabel`, `glab mr close`.
|
||||||
|
|
||||||
|
Unlike GitHub, GitLab numbers issues and MRs separately, so `#42` is unambiguous once you know which surface the maintainer means.
|
||||||
|
|
||||||
|
## When a skill says "publish to the issue tracker"
|
||||||
|
|
||||||
|
Create a GitLab issue.
|
||||||
|
|
||||||
|
## When a skill says "fetch the relevant ticket"
|
||||||
|
|
||||||
|
Run `glab issue view <number> --comments`.
|
||||||
|
|
||||||
|
## Wayfinding operations
|
||||||
|
|
||||||
|
Used by `/wayfinder`. The **map** is a single issue with **child** issues as tickets.
|
||||||
|
|
||||||
|
- **Map**: a single issue labelled `wayfinder:map`, holding the Notes / Decisions-so-far / Fog body. `glab issue create --label wayfinder:map`. (On GitLab tiers with native epics, an epic may hold the map instead; a labelled issue works everywhere.)
|
||||||
|
- **Child ticket**: an issue carrying `Part of #<map>` at the top of its description and labels `wayfinder:<type>` (`research`/`prototype`/`grilling`/`task`). Once claimed, the ticket is assigned to the driving dev.
|
||||||
|
- **Blocking**: GitLab's **native blocking link** — the canonical, UI-visible representation. Add it with the `/blocked_by #<n>` quick action, posted as a note (`glab issue note <child> --message "/blocked_by #<blocker>"`). Native blocking links are a Premium/Ultimate feature; on the free tier (or where unavailable) fall back to a `Blocked by: #<n>, #<n>` line at the top of the description. A ticket is unblocked when every blocker is closed.
|
||||||
|
- **Frontier query**: `glab issue list -F json` scoped to the map's children, drop any with an open blocker — a native `blocked_by` link to an open issue (`glab api projects/:id/issues/:iid/links`), or an open issue in the `Blocked by` line — or an assignee; first in map order wins.
|
||||||
|
- **Claim**: `glab issue update <n> --assignee @me` — the session's first write.
|
||||||
|
- **Resolve**: `glab issue note <n> --message "<answer>"`, then `glab issue close <n>`, then append a context pointer (gist + link) to the map's Decisions-so-far.
|
||||||
+5
-3
@@ -1,15 +1,17 @@
|
|||||||
# Triage Labels
|
# 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.
|
The skills speak in terms of seven canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker.
|
||||||
|
|
||||||
| Label in mattpocock/skills | Label in our tracker | Meaning |
|
| Label in skills | Label in our tracker | Meaning |
|
||||||
| -------------------------- | -------------------- | ---------------------------------------- |
|
| -------------------------- | -------------------- | ---------------------------------------- |
|
||||||
| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
|
| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
|
||||||
| `needs-info` | `needs-info` | Waiting on reporter for more information |
|
| `needs-info` | `needs-info` | Waiting on reporter for more information |
|
||||||
|
| `needs-review` | `needs-review` | Waiting for reviewed by a human |
|
||||||
| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
|
| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
|
||||||
| `ready-for-human` | `ready-for-human` | Requires human implementation |
|
| `ready-for-human` | `ready-for-human` | Requires human implementation |
|
||||||
|
| `in-progress` | `in-progress` | Being actively worked on by a human or agents |
|
||||||
| `wontfix` | `wontfix` | Will not be actioned |
|
| `wontfix` | `wontfix` | Will not be actioned |
|
||||||
|
|
||||||
When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table.
|
When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table. Only one triage label should be active.
|
||||||
|
|
||||||
Edit the right-hand column to match whatever vocabulary you actually use.
|
Edit the right-hand column to match whatever vocabulary you actually use.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# In-Progress Skills
|
||||||
|
|
||||||
|
Drafts not yet ready to ship.
|
||||||
|
|
||||||
|
## User-invoked
|
||||||
|
|
||||||
|
- [agent-handoff](agent-handoff/SKILL.md) — Hand the current conversation off to a fresh background agent that picks up the work immediately.
|
||||||
|
- [knowledge-gardener](knowledge-gardener/SKILL.md) — Run vault-aware semantic search, synthesis, note creation, linking, and Zettelkasten workflows for this Obsidian vault.
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
name: agent-handoff
|
||||||
|
description: Hand the current conversation off to a fresh background agent that picks up the work immediately.
|
||||||
|
argument-hint: "What will the next session be used for?"
|
||||||
|
disable-model-invocation: true
|
||||||
|
---
|
||||||
|
|
||||||
|
## Invocation
|
||||||
|
|
||||||
|
```
|
||||||
|
/skill:agent-handoff [focus description]
|
||||||
|
```
|
||||||
|
|
||||||
|
Arguments are optional. If provided, they describe what the next session should focus on and the summary is tailored accordingly.
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
### 1. Assess the conversation
|
||||||
|
|
||||||
|
Review what has been done, what remains, and any user-provided focus. Identify existing artifacts that capture the work so far (PRDs/specs, plans, ADRs, issues/tickets, commits, diffs) so they can be referenced rather than duplicated.
|
||||||
|
|
||||||
|
If the user passed arguments, treat them as the priority or scope for the next session.
|
||||||
|
|
||||||
|
Completion criterion: The conversation is assessed — what's done, what's left, and the user's focus (if any) are identified.
|
||||||
|
|
||||||
|
### 2. Compose the handoff summary
|
||||||
|
|
||||||
|
Write a summary of the current state so a fresh agent can continue the work without reading the full conversation. Organise it with these sections:
|
||||||
|
|
||||||
|
- **Current state** — what was accomplished, branch/commit, what's working
|
||||||
|
- **Next steps** — what needs to be done next, in priority order
|
||||||
|
- **Open questions** — decisions still needed, unknowns, trade-offs
|
||||||
|
- **Suggested skills** — a bullet list of skills the next agent should invoke (e.g. `/tdd`, `/code-review`)
|
||||||
|
- **References** — paths or URLs to existing artifacts (PRDs/specs, plans, ADRs, issues/tickets, commits, diffs). Do **not** duplicate their content — reference them.
|
||||||
|
|
||||||
|
The summary is a **compass**, not a copy: it points the next agent where to go, it does not replay where you've been. Any content already captured in the referenced artifacts does not belong here.
|
||||||
|
|
||||||
|
Redact any sensitive information: API keys, passwords, personally identifiable information.
|
||||||
|
|
||||||
|
Completion criterion: The summary covers all required sections, references existing artifacts without duplicating them, and contains no sensitive information.
|
||||||
|
|
||||||
|
### 3. Launch the background agent
|
||||||
|
|
||||||
|
Use the [`tmux-launch-agent`](../misc/tmux-launch-agent/SKILL.md) skill to fork a fresh agent seeded with the summary:
|
||||||
|
|
||||||
|
```
|
||||||
|
/skill:tmux-launch-agent --name "<descriptive-title>" <handoff-summary>
|
||||||
|
```
|
||||||
|
|
||||||
|
- `--name` is **required** — use a short descriptive title (e.g. `"Fix login bug"`, `"Implement user roles"`). This sets the display name in the job list, session picker, and terminal title.
|
||||||
|
- The handoff summary from step 2 becomes the new agent's initial prompt.
|
||||||
|
|
||||||
|
Completion criterion: `tmux new-window` exits 0 and a new tmux window appears with the handoff summary as the agent's prompt.
|
||||||
@@ -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,7 @@
|
|||||||
|
# Misc Skills
|
||||||
|
|
||||||
|
Kept around but rarely used.
|
||||||
|
|
||||||
|
## User-invoked
|
||||||
|
|
||||||
|
- [tmux-launch-agent](tmux-launch-agent/SKILL.md) — Fork a new agent CLI session into a new tmux window, detected from the current agent.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
name: tmux-launch-agent
|
||||||
|
description: Fork a new agent CLI session into a new tmux window, detected from the current agent.
|
||||||
|
disable-model-invocation: true
|
||||||
|
---
|
||||||
|
|
||||||
|
## Agent CLI Seed Data
|
||||||
|
|
||||||
|
Agent config (binary, `args`, `modelflag`) is in [`agents-seed.md`](agents-seed.md). Step 2 reads it by `name`.
|
||||||
|
|
||||||
|
## Dispatch
|
||||||
|
|
||||||
|
1. **Detect agent** — Run `./detect-agent` (sibling). On success, the agent name is known. On failure (exit 1), stop.
|
||||||
|
|
||||||
|
2. **Look up config** — Find the agent's entry in [`agents-seed.md`](agents-seed.md) by `name`. Extract `binary`, `args`, and `modelflag`.
|
||||||
|
|
||||||
|
3. **Parse flags** — Scan user arguments for flags (before prompt text). For each flag, extract its value and remove both the flag and value from the argument list:
|
||||||
|
- `--name <title>` / `-n <title>` — tmux window title
|
||||||
|
- `-c <path>` — working directory (default: project directory)
|
||||||
|
- `--model <name>` / `-m <name>` — model for child session
|
||||||
|
|
||||||
|
Remaining text is the prompt. If `--name` is absent, tmux auto-names the window.
|
||||||
|
|
||||||
|
4. **Build command** — Assemble the inner command:
|
||||||
|
- Start with `<binary>`.
|
||||||
|
- If model specified: append `<modelflag> <model-name>`.
|
||||||
|
- If prompt exists and `args` contains `{prompt}`: write to `/tmp/`, substitute path for `{prompt}`.
|
||||||
|
- If prompt exists and `args` is empty: pipe via `echo`.
|
||||||
|
- If no prompt: launch bare.
|
||||||
|
|
||||||
|
5. **Open window** — Call `./tmux-open <window-name> <start-dir> <inner-command>` where `<inner-command>` is the assembled command from step 4 as separate arguments. On success, a new tmux window appears with the agent session active.
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# TMUX Launch Agent — Agent CLI Seed Data
|
||||||
|
|
||||||
|
## Agents
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
agents:
|
||||||
|
- name: pi
|
||||||
|
binary: pi
|
||||||
|
args: "@{prompt}"
|
||||||
|
modelflag: "--model"
|
||||||
|
description: "My primary agent harness. Accepts prompt file via {prompt}."
|
||||||
|
|
||||||
|
- name: opencode
|
||||||
|
binary: opencode
|
||||||
|
args: ""
|
||||||
|
modelflag: "-m"
|
||||||
|
description: "OpenCode agent. Pipes stdin via cat."
|
||||||
|
note: "Uses stdin piping: args must be empty, prompt via pipe."
|
||||||
|
|
||||||
|
- name: goose
|
||||||
|
binary: goose
|
||||||
|
args: ""
|
||||||
|
modelflag: "--model"
|
||||||
|
description: "Goose agent. Accepts prompt file via -i flag."
|
||||||
|
note: "Uses stdin piping: args must be empty, prompt via pipe."
|
||||||
|
|
||||||
|
- name: codex
|
||||||
|
binary: codex
|
||||||
|
args: "@{prompt}"
|
||||||
|
modelflag: "-m"
|
||||||
|
description: "OpenAI Codex. Accepts prompt file via {prompt}."
|
||||||
|
|
||||||
|
- name: claude
|
||||||
|
binary: claude
|
||||||
|
args: ""
|
||||||
|
modelflag: "--model"
|
||||||
|
description: "Anthropic Claude CLI. Pipes stdin via cat."
|
||||||
|
note: "Uses stdin piping: args must be empty, prompt via pipe."
|
||||||
|
```
|
||||||
|
|
||||||
|
## Field meanings
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
| ------- | ------------- |
|
||||||
|
| `name` | Display name used in menus and `--agent` flag |
|
||||||
|
| `binary` | Command name expected on PATH |
|
||||||
|
| `args` | Static arguments appended after the binary. May include `{prompt}` which is substituted at invocation time with the absolute path to the prompt file. Empty string means stdin piping (prompt is piped via `cat`). |
|
||||||
|
| `description` | Short human-readable description for the setup menu |
|
||||||
|
| `modelflag` | CLI flag used to select a model (e.g. `--model`, `-m`). Injected into the inner command when `--model <name>` is passed by the user. |
|
||||||
|
| `note` | Optional additional context |
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "Tmux Launch Agent"
|
||||||
|
short_description: "Launch an agent in a new tmux session"
|
||||||
|
policy:
|
||||||
|
allow_implicit_invocation: false
|
||||||
Executable
+353
@@ -0,0 +1,353 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# detect-agent — Detect which AI agent CLI launched this script.
|
||||||
|
#
|
||||||
|
# Checks environment variables and walks the parent process tree to
|
||||||
|
# identify the originating coding agent. Designed for agent-agnostic
|
||||||
|
# tools (skills, scripts, hooks) that need to adjust behaviour based
|
||||||
|
# on which agent invoked them.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# detect-agent # print name (e.g. "pi", "opencode")
|
||||||
|
# detect-agent --name # same as default
|
||||||
|
# detect-agent --json # JSON: {"agent":"pi","detected":true}
|
||||||
|
# detect-agent --verbose # human-readable with debug info
|
||||||
|
# detect-agent --list-agents # list all known agent names
|
||||||
|
#
|
||||||
|
# Exit codes:
|
||||||
|
# 0 agent detected
|
||||||
|
# 1 unknown / not detected
|
||||||
|
#
|
||||||
|
# Supported agents (add your own at the top of each definition map):
|
||||||
|
# pi, opencode, aider, claude, codex, cursor, windsurf, continue,
|
||||||
|
# github-copilot, goose
|
||||||
|
#
|
||||||
|
# Detection order:
|
||||||
|
# 1. Environment variable signatures (most reliable)
|
||||||
|
# 2. Walk parent process tree skipping shells
|
||||||
|
# 3. Check /proc/$PPID/{comm,cmdline} as fallback
|
||||||
|
#
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
########################################################################
|
||||||
|
# Agent definitions
|
||||||
|
########################################################################
|
||||||
|
|
||||||
|
# Each entry: "<env-var>:<match-value>:<agent-name>"
|
||||||
|
# If match-value is "*", any non-empty value matches.
|
||||||
|
# If match-value is a literal, the env var must equal it.
|
||||||
|
AGENT_ENV_SIGS=(
|
||||||
|
"PI_CODING_AGENT:true:pi"
|
||||||
|
"OPENCODE_CONFIG:*:opencode"
|
||||||
|
"OPENCODE_CONFIG_DIR:*:opencode"
|
||||||
|
"OPENCODE_API_KEY:*:opencode"
|
||||||
|
"CLAUDE_CODE_AGENT_RULE_DISABLED:*:claude"
|
||||||
|
"CLAUDE_CODE_SSE_PORT:*:claude"
|
||||||
|
"AIDER_DARK_MODE:*:aider"
|
||||||
|
"CODEX_API_KEY:*:codex"
|
||||||
|
"CURSOR_AGENT_RULE_DISABLED:*:cursor"
|
||||||
|
"WINDSURF_AGENT_RULE_DISABLED:*:windsurf"
|
||||||
|
"CONTINUE_ON_ERROR:*:continue"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Key = /proc/PID/comm (truncated to 15 chars on Linux).
|
||||||
|
# Value = canonical agent name.
|
||||||
|
declare -A AGENT_COMM
|
||||||
|
AGENT_COMM=(
|
||||||
|
[pi]="pi"
|
||||||
|
[opencode]="opencode"
|
||||||
|
[aider]="aider"
|
||||||
|
[claude]="claude"
|
||||||
|
[codex]="codex"
|
||||||
|
[cursor]="cursor"
|
||||||
|
[windsurf]="windsurf"
|
||||||
|
[continue]="continue"
|
||||||
|
[github-copilot]="github-copilot"
|
||||||
|
[copilot]="github-copilot"
|
||||||
|
[goose]="goose"
|
||||||
|
)
|
||||||
|
|
||||||
|
# For agents running under an interpreter (node, python, etc.).
|
||||||
|
# Key = substring to search in cmdline, Value = agent name.
|
||||||
|
declare -A AGENT_CMDLINE
|
||||||
|
AGENT_CMDLINE=(
|
||||||
|
["opencode"]="opencode"
|
||||||
|
["/pi "]="pi"
|
||||||
|
["/aider"]="aider"
|
||||||
|
["/claude"]="claude"
|
||||||
|
["/codex"]="codex"
|
||||||
|
["cursor"]="cursor"
|
||||||
|
["windsurf"]="windsurf"
|
||||||
|
["continue.dev"]="continue"
|
||||||
|
["github-copilot"]="github-copilot"
|
||||||
|
["goose"]="goose"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Shells to skip when walking ancestors.
|
||||||
|
SHELL_COMMS=(
|
||||||
|
bash zsh fish sh dash ksh mksh posh ash tcsh csh
|
||||||
|
tmux:server tmux screen
|
||||||
|
sudo doas
|
||||||
|
)
|
||||||
|
|
||||||
|
SHELL_CMDLINE_SUBSTRINGS=(
|
||||||
|
"/bash" "/zsh" "/fish" "/sh" "/dash" "/ksh"
|
||||||
|
)
|
||||||
|
|
||||||
|
########################################################################
|
||||||
|
# Helpers
|
||||||
|
########################################################################
|
||||||
|
|
||||||
|
comm_of() {
|
||||||
|
local pid="$1"
|
||||||
|
cat "/proc/${pid}/comm" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdline_of() {
|
||||||
|
local pid="$1"
|
||||||
|
tr '\0' ' ' <"/proc/${pid}/cmdline" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
is_shell() {
|
||||||
|
local pid="$1"
|
||||||
|
local c; c="$(comm_of "$pid")" || true
|
||||||
|
[[ -z "$c" ]] && return 1
|
||||||
|
|
||||||
|
for s in "${SHELL_COMMS[@]}"; do
|
||||||
|
[[ "$c" == "$s" ]] && return 0
|
||||||
|
done
|
||||||
|
|
||||||
|
local cl; cl="$(cmdline_of "$pid")" || true
|
||||||
|
for s in "${SHELL_CMDLINE_SUBSTRINGS[@]}"; do
|
||||||
|
[[ "$cl" == *"$s"* ]] && return 0
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
walk_to_agent_comm() {
|
||||||
|
local pid="$1"
|
||||||
|
local c
|
||||||
|
while [[ "$pid" -gt 1 ]]; do
|
||||||
|
c="$(comm_of "$pid")" || true
|
||||||
|
[[ -z "$c" ]] && break
|
||||||
|
if ! is_shell "$pid"; then
|
||||||
|
echo "$c"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
pid="$(awk '/^PPid:/{print $2}' "/proc/${pid}/status" 2>/dev/null || true)"
|
||||||
|
[[ -z "$pid" ]] && break
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
walk_to_agent_cmdline() {
|
||||||
|
local pid="$1"
|
||||||
|
local cl
|
||||||
|
while [[ "$pid" -gt 1 ]]; do
|
||||||
|
if ! is_shell "$pid"; then
|
||||||
|
cl="$(cmdline_of "$pid")" || true
|
||||||
|
if [[ -n "$cl" ]]; then
|
||||||
|
echo "$cl"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
pid="$(awk '/^PPid:/{print $2}' "/proc/${pid}/status" 2>/dev/null || true)"
|
||||||
|
[[ -z "$pid" ]] && break
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
check_env_sigs() {
|
||||||
|
local var val agent
|
||||||
|
for sig in "${AGENT_ENV_SIGS[@]}"; do
|
||||||
|
IFS=':' read -r var val agent <<< "$sig"
|
||||||
|
if [[ "$val" == "*" ]]; then
|
||||||
|
if [[ -n "${!var:-}" ]]; then
|
||||||
|
echo "$agent"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [[ "${!var:-}" == "$val" ]]; then
|
||||||
|
echo "$agent"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
match_cmdline() {
|
||||||
|
local cl="$1"
|
||||||
|
local needle agent
|
||||||
|
for needle in "${!AGENT_CMDLINE[@]}"; do
|
||||||
|
agent="${AGENT_CMDLINE[$needle]}"
|
||||||
|
if [[ "$cl" == *"$needle"* ]]; then
|
||||||
|
echo "$agent"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
match_comm() {
|
||||||
|
local c="$1"
|
||||||
|
local comm_name
|
||||||
|
for comm_name in "${!AGENT_COMM[@]}"; do
|
||||||
|
if [[ "$c" == "$comm_name" ]]; then
|
||||||
|
echo "${AGENT_COMM[$comm_name]}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
########################################################################
|
||||||
|
# Main detection logic
|
||||||
|
########################################################################
|
||||||
|
|
||||||
|
detect() {
|
||||||
|
local agent=""
|
||||||
|
|
||||||
|
# Phase 1 — environment variables (most reliable)
|
||||||
|
agent="$(check_env_sigs)" || true
|
||||||
|
[[ -n "$agent" ]] && echo "$agent" && return 0
|
||||||
|
|
||||||
|
# Phase 2 — walk ancestors, check comm
|
||||||
|
local ancestor_comm
|
||||||
|
ancestor_comm="$(walk_to_agent_comm "$PPID")" || true
|
||||||
|
if [[ -n "$ancestor_comm" ]]; then
|
||||||
|
agent="$(match_comm "$ancestor_comm")" || true
|
||||||
|
[[ -n "$agent" ]] && echo "$agent" && return 0
|
||||||
|
|
||||||
|
# comm matched but not in our map; try its cmdline
|
||||||
|
local ancestor_pid="$PPID"
|
||||||
|
while [[ "$ancestor_pid" -gt 1 ]]; do
|
||||||
|
local ac; ac="$(comm_of "$ancestor_pid")" || true
|
||||||
|
if [[ "$ac" == "$ancestor_comm" ]]; then
|
||||||
|
local ancestor_cl
|
||||||
|
ancestor_cl="$(cmdline_of "$ancestor_pid")" || true
|
||||||
|
if [[ -n "$ancestor_cl" ]]; then
|
||||||
|
agent="$(match_cmdline "$ancestor_cl")" || true
|
||||||
|
[[ -n "$agent" ]] && echo "$agent" && return 0
|
||||||
|
fi
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
ancestor_pid="$(awk '/^PPid:/{print $2}' "/proc/${ancestor_pid}/status" 2>/dev/null || true)"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Phase 3 — walk ancestors, check cmdline (catches node-based agents)
|
||||||
|
local ancestor_cl
|
||||||
|
ancestor_cl="$(walk_to_agent_cmdline "$PPID")" || true
|
||||||
|
if [[ -n "$ancestor_cl" ]]; then
|
||||||
|
agent="$(match_cmdline "$ancestor_cl")" || true
|
||||||
|
[[ -n "$agent" ]] && echo "$agent" && return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Phase 4 — fallback: check immediate PPID directly
|
||||||
|
local ppid_comm; ppid_comm="$(comm_of "$PPID")" || true
|
||||||
|
agent="$(match_comm "$ppid_comm")" || true
|
||||||
|
[[ -n "$agent" ]] && echo "$agent" && return 0
|
||||||
|
|
||||||
|
local ppid_cl; ppid_cl="$(cmdline_of "$PPID")" || true
|
||||||
|
agent="$(match_cmdline "$ppid_cl")" || true
|
||||||
|
[[ -n "$agent" ]] && echo "$agent" && return 0
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
########################################################################
|
||||||
|
# Output
|
||||||
|
########################################################################
|
||||||
|
|
||||||
|
mode="${1:---name}"
|
||||||
|
|
||||||
|
case "$mode" in
|
||||||
|
--list-agents)
|
||||||
|
echo "Known agents (by env-var / process-name):"
|
||||||
|
echo ""
|
||||||
|
echo "Environment variable signatures:"
|
||||||
|
for sig in "${AGENT_ENV_SIGS[@]}"; do
|
||||||
|
IFS=':' read -r var val agent <<< "$sig"
|
||||||
|
printf " %-12s %-8s %s=%s\n" "$agent" "(env)" "$var" "$val"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
echo "Process comm signatures:"
|
||||||
|
for comm_name in "${!AGENT_COMM[@]}"; do
|
||||||
|
printf " %-12s %-8s %s\n" "${AGENT_COMM[$comm_name]}" "(comm)" "$comm_name"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
echo "Cmdline substring signatures:"
|
||||||
|
for needle in "${!AGENT_CMDLINE[@]}"; do
|
||||||
|
printf " %-12s %-8s *%s*\n" "${AGENT_CMDLINE[$needle]}" "(cmdline)" "$needle"
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
|
||||||
|
--json)
|
||||||
|
agent="$(detect)" || agent="null"
|
||||||
|
if [[ "$agent" != "null" ]]; then
|
||||||
|
printf '{"agent":"%s","detected":true}\n' "$agent"
|
||||||
|
else
|
||||||
|
printf '{"agent":null,"detected":false}\n'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
|
||||||
|
--verbose)
|
||||||
|
echo "=== Agent Detection ==="
|
||||||
|
agent="$(detect)" || true
|
||||||
|
if [[ -n "$agent" ]]; then
|
||||||
|
echo "Detected agent: $agent"
|
||||||
|
else
|
||||||
|
echo "Detected agent: (unknown)"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Relevant environment variables:"
|
||||||
|
for sig in "${AGENT_ENV_SIGS[@]}"; do
|
||||||
|
IFS=':' read -r var val _ <<< "$sig"
|
||||||
|
# Skip entries that aren't real variable names (e.g. patterns with *).
|
||||||
|
[[ "$var" == *\** ]] && continue
|
||||||
|
if [[ -n "${!var:-}" ]]; then
|
||||||
|
echo " $var=${!var}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[[ -z "${PI_CODING_AGENT:-}" ]] && echo " PI_CODING_AGENT=<unset>"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Process tree (up from PPID=$PPID):"
|
||||||
|
_dbg_pid="$PPID"
|
||||||
|
_dbg_depth=0
|
||||||
|
while [[ "$_dbg_pid" -gt 1 && "$_dbg_depth" -lt 15 ]]; do
|
||||||
|
_dbg_c="$(comm_of "$_dbg_pid")" || true
|
||||||
|
_dbg_cl="$(cmdline_of "$_dbg_pid")" || true
|
||||||
|
if [[ -n "$_dbg_c" ]]; then
|
||||||
|
printf " [%d] %s" "$_dbg_depth" "$_dbg_c"
|
||||||
|
if [[ -n "$_dbg_cl" ]]; then
|
||||||
|
printf " <- %s" "${_dbg_cl:0:100}"
|
||||||
|
fi
|
||||||
|
if [[ -n "$agent" ]]; then
|
||||||
|
_dbg_mc="$(match_comm "$_dbg_c")" || true
|
||||||
|
if [[ "$_dbg_mc" == "$agent" ]]; then
|
||||||
|
printf " <-- DETECTED"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
_dbg_pid="$(awk '/^PPid:/{print $2}' "/proc/${_dbg_pid}/status" 2>/dev/null || true)"
|
||||||
|
_dbg_depth=$((_dbg_depth + 1))
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
|
||||||
|
--name|*)
|
||||||
|
agent="$(detect)" || agent=""
|
||||||
|
if [[ -n "$agent" ]]; then
|
||||||
|
echo "$agent"
|
||||||
|
else
|
||||||
|
echo "unknown"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Executable
+115
@@ -0,0 +1,115 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# tmux-open — open a new tmux window (or session) and run a command
|
||||||
|
# Usage: tmux-open [-k|--keep] [-h|--help] <window-name> <start-dir> <cmd> [args...]
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage: tmux-open [-k|--keep] [-h|--help] <window-name> <start-dir> <cmd> [args...]
|
||||||
|
|
||||||
|
Open a new tmux window in the current session and run a command.
|
||||||
|
If not inside tmux, create a new session named after the window and attach.
|
||||||
|
|
||||||
|
Positional arguments:
|
||||||
|
window-name Name for the new tmux window (and session, if creating one)
|
||||||
|
start-dir Working directory for the new window
|
||||||
|
cmd [args...] Command to run (variadic)
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-k, --keep After the command exits, keep the pane visible (remain-on-exit)
|
||||||
|
so you can inspect output. Default: pane closes when command exits.
|
||||||
|
-h, --help Show this help message
|
||||||
|
|
||||||
|
If `mise` is on PATH, the command runs under `mise x`.
|
||||||
|
|
||||||
|
Errors:
|
||||||
|
- tmux not installed
|
||||||
|
- fewer than 3 positional args
|
||||||
|
- start-dir does not exist
|
||||||
|
- a window with the same name already exists in the current session
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Flag parsing ---
|
||||||
|
keep_shell=0
|
||||||
|
while (( $# > 0 )); do
|
||||||
|
case "$1" in
|
||||||
|
-k|--keep) keep_shell=1; shift ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
--) shift; break ;;
|
||||||
|
-*) echo "error: unknown option: $1" >&2; usage >&2; exit 1 ;;
|
||||||
|
*) break ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- Positional arg validation ---
|
||||||
|
if (( $# < 3 )); then
|
||||||
|
echo "error: expected at least 3 positional arguments (window-name, start-dir, cmd)" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
window_name="$1"; shift
|
||||||
|
start_dir="$1"; shift
|
||||||
|
cmd_args=("$@")
|
||||||
|
|
||||||
|
# --- Pre-flight checks ---
|
||||||
|
if ! command -v tmux >/dev/null 2>&1; then
|
||||||
|
echo "error: tmux is not installed or not on PATH" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d "$start_dir" ]]; then
|
||||||
|
echo "error: directory does not exist: $start_dir" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( ${#cmd_args[@]} == 0 )); then
|
||||||
|
echo "error: no command specified" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Duplicate window name check (only relevant inside tmux)
|
||||||
|
if [[ -n "${TMUX:-}" ]]; then
|
||||||
|
if tmux list-windows -F '#{window_name}' | grep -Fxq "$window_name"; then
|
||||||
|
echo "error: a window named '$window_name' already exists in the current session" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Detect mise ---
|
||||||
|
has_mise=0
|
||||||
|
if command -v mise >/dev/null 2>&1; then
|
||||||
|
has_mise=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Build the command for the new window ---
|
||||||
|
# We need to construct a single shell command string for tmux.
|
||||||
|
# The command runs; pane closes on exit (or stays visible with --keep via remain-on-exit).
|
||||||
|
|
||||||
|
# Build the inner command based on mode and mise availability
|
||||||
|
# For argv mode, quote each argument
|
||||||
|
quoted_args=()
|
||||||
|
for arg in "${cmd_args[@]}"; do
|
||||||
|
quoted_args+=("$(printf '%q' "$arg")")
|
||||||
|
done
|
||||||
|
if (( has_mise )); then
|
||||||
|
inner_cmd="mise x -- ${quoted_args[*]}"
|
||||||
|
else
|
||||||
|
inner_cmd="${quoted_args[*]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Create window or session ---
|
||||||
|
if [[ -n "${TMUX:-}" ]]; then
|
||||||
|
tmux new-window -n "$window_name" -c "$start_dir" "$inner_cmd"
|
||||||
|
if (( keep_shell )); then
|
||||||
|
tmux set-option remain-on-exit on
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
tmux new-session -d -n "$window_name" -s "$window_name" -c "$start_dir" "$inner_cmd"
|
||||||
|
if (( keep_shell )); then
|
||||||
|
tmux set-option -t "$window_name" remain-on-exit on
|
||||||
|
fi
|
||||||
|
tmux attach-session -t "$window_name"
|
||||||
|
fi
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
# Personal Skills
|
# Personal Skills
|
||||||
|
|
||||||
## User-invoked
|
Tied to my own setup, not promoted.
|
||||||
|
|
||||||
- [pkm-curation](pkm-curation/SKILL.md) — Curate an Obsidian-style personal knowledge vault.
|
_No skills currently live in this bucket._
|
||||||
|
|
||||||
## Model-invoked
|
|
||||||
|
|
||||||
- [forge-preferences](forge-preferences/SKILL.md) — Apply Steve's personal or project-specific forge preferences.
|
|
||||||
|
|||||||
@@ -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,213 +0,0 @@
|
|||||||
---
|
|
||||||
name: pkm-curation
|
|
||||||
description: Curate an Obsidian-style personal knowledge vault by classifying notes, normalizing frontmatter, improving structure, extracting atomic notes, and adding meaningful wikilinks.
|
|
||||||
disable-model-invocation: true
|
|
||||||
---
|
|
||||||
|
|
||||||
# PKM Curation
|
|
||||||
|
|
||||||
Use this skill when working inside a Markdown-first vault that values curation over collection.
|
|
||||||
|
|
||||||
## Goals
|
|
||||||
|
|
||||||
- Turn raw notes into clear, reusable notes.
|
|
||||||
- Keep new notes consistent with vault conventions.
|
|
||||||
- Strengthen the link graph with meaningful `[[wikilinks]]` syntax `[[Note Title]]`.
|
|
||||||
- Extract atomic notes from long or mixed-topic notes.
|
|
||||||
- Avoid unnecessary reorganization and weak links.
|
|
||||||
|
|
||||||
## Read This First
|
|
||||||
|
|
||||||
- Read `AGENTS.md` in the current repo scope before editing notes.
|
|
||||||
- Read `references/vault-conventions.md` when normalizing metadata, deciding note types, or choosing folders.
|
|
||||||
- Read `references/agent-integration.md` when running this skill through an agent.
|
|
||||||
- Keep changes small and reviewable.
|
|
||||||
- Keep file operations local to the vault unless the user explicitly asks otherwise.
|
|
||||||
|
|
||||||
## Workflows
|
|
||||||
|
|
||||||
## Search for notes
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Search by filename
|
|
||||||
fd --type f ".md" "path/to/obsidian-vault" | rg -i "keyword"
|
|
||||||
|
|
||||||
# Search by content
|
|
||||||
rg -l "keyword" "path/to/obsidian-vault" --include "*.md"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Curate existing note
|
|
||||||
1. Inspect the target note or note set.
|
|
||||||
2. Identify the note type: inbox, source, atomic, project, daily, or reference.
|
|
||||||
3. Normalize frontmatter and basic structure.
|
|
||||||
4. Clarify the title if the current one is vague or timestamp-like.
|
|
||||||
5. Summarize or distill the note if it mixes too many ideas.
|
|
||||||
6. Add or suggest meaningful `[[wikilinks]]` to related notes.
|
|
||||||
7. If a note contains multiple durable ideas, extract 1-3 atomic notes.
|
|
||||||
8. Suggest moving the note only if the destination is clearly better.
|
|
||||||
|
|
||||||
## Find related notes
|
|
||||||
|
|
||||||
Search for `[[Note Title]]` across the vault to find backlinks:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rg -l "\\[\\[Note Title\\]\\]" "path/to/obsidian-vault" --include "*.md"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Find index notes
|
|
||||||
|
|
||||||
```bash
|
|
||||||
fd --type f "Index" "path/to/obsidian-vault"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Operating Rules
|
|
||||||
|
|
||||||
- Prefer reorganization over curation.
|
|
||||||
- Do not move, rename, or delete many notes at once unless the user asks.
|
|
||||||
- Do not invent links based only on shared words.
|
|
||||||
- Preserve the user's voice unless the user asks for a rewrite.
|
|
||||||
- Keep source material and evergreen ideas separate when possible.
|
|
||||||
- Treat `Inbox/` as temporary capture, not long-term storage.
|
|
||||||
- Preserve all command blocks, code snippets, configuration directives, and
|
|
||||||
step-by-step instructions verbatim. Do not summarize or condense them.
|
|
||||||
- For reference/source notes: add a brief overview at the top, but keep the
|
|
||||||
original commands and details intact below. Completeness > brevity.
|
|
||||||
- Read every file completely before editing. Do not rely on head/tail,
|
|
||||||
heading-only scans, or partial reads to judge a file's content.
|
|
||||||
- For any file you plan to move, rename, or modify, run `cat` on the full file
|
|
||||||
first. Only then decide what stays, what moves, and what changes.
|
|
||||||
|
|
||||||
## Note-Type Heuristics
|
|
||||||
|
|
||||||
### Inbox note
|
|
||||||
|
|
||||||
Use when the note is raw capture, partial thinking, copied text, or an unprocessed link dump.
|
|
||||||
|
|
||||||
Actions:
|
|
||||||
- clean obvious structure issues
|
|
||||||
- add frontmatter if missing
|
|
||||||
- classify for later promotion
|
|
||||||
- avoid over-polishing unless requested
|
|
||||||
|
|
||||||
### Source note
|
|
||||||
|
|
||||||
Use when the note is based on an article, video, book, paper, transcript, or other external material.
|
|
||||||
|
|
||||||
Actions:
|
|
||||||
- keep source context intact
|
|
||||||
- summarize key takeaways
|
|
||||||
- extract reusable ideas into separate atomic notes
|
|
||||||
- link to related concepts and projects
|
|
||||||
|
|
||||||
### Atomic note
|
|
||||||
|
|
||||||
Use when the note captures one durable idea, concept, claim, pattern, or insight.
|
|
||||||
|
|
||||||
Actions:
|
|
||||||
- ensure one main idea per note
|
|
||||||
- make the title concept-focused
|
|
||||||
- add links to neighboring ideas
|
|
||||||
- keep it concise and self-contained
|
|
||||||
|
|
||||||
### Project note
|
|
||||||
|
|
||||||
Use when the note supports active work, planning, resources, decisions, or tasks.
|
|
||||||
|
|
||||||
Actions:
|
|
||||||
- preserve project context
|
|
||||||
- link tasks to the project note
|
|
||||||
- avoid turning active project logistics into evergreen notes unless there is a reusable insight
|
|
||||||
|
|
||||||
### Daily note
|
|
||||||
|
|
||||||
Use when the note is date-based and captures activity, learning, tasks, or reflection for a single day.
|
|
||||||
|
|
||||||
Actions:
|
|
||||||
- preserve chronology
|
|
||||||
- link out to durable notes rather than stuffing ideas into the daily note
|
|
||||||
|
|
||||||
## Linking Guidance
|
|
||||||
|
|
||||||
Add links only when they express one of these relationships:
|
|
||||||
|
|
||||||
- concept to broader concept
|
|
||||||
- source note to extracted idea
|
|
||||||
- project note to relevant knowledge note
|
|
||||||
- daily note to work done or ideas learned
|
|
||||||
- sibling concepts that genuinely inform one another
|
|
||||||
|
|
||||||
When linking, prefer existing notes over creating speculative new ones.
|
|
||||||
|
|
||||||
## Extraction Guidance
|
|
||||||
|
|
||||||
Extract atomic notes when a note contains:
|
|
||||||
|
|
||||||
- multiple durable ideas
|
|
||||||
- a strong claim hidden in raw notes
|
|
||||||
- a reusable method, distinction, or definition
|
|
||||||
- a concept that should be linked from many places
|
|
||||||
|
|
||||||
Keep extracted notes short. One note, one idea.
|
|
||||||
|
|
||||||
## Common Tasks
|
|
||||||
|
|
||||||
### Curate one note
|
|
||||||
|
|
||||||
- inspect the note
|
|
||||||
- identify note type
|
|
||||||
- normalize frontmatter
|
|
||||||
- tighten headings and summary
|
|
||||||
- add a few strong links
|
|
||||||
- suggest extracted atomic notes if warranted
|
|
||||||
- locate the target file in the vault
|
|
||||||
- inspect nearby related notes before adding links
|
|
||||||
- patch the note in place
|
|
||||||
- return a short summary of edits and suggested follow-up notes
|
|
||||||
|
|
||||||
### Curate an inbox batch
|
|
||||||
|
|
||||||
- process a small batch, usually 5-10 notes
|
|
||||||
- classify each note
|
|
||||||
- normalize metadata
|
|
||||||
- suggest which notes should stay raw, become source notes, or become atomic notes
|
|
||||||
- avoid large folder reshuffles unless the pattern is clear
|
|
||||||
- enumerate a small set of `Inbox/` notes
|
|
||||||
- process them one at a time
|
|
||||||
- stop and summarize after each batch
|
|
||||||
|
|
||||||
### Review recent notes
|
|
||||||
|
|
||||||
- inspect recently edited notes
|
|
||||||
- identify missing links and vague titles
|
|
||||||
- flag notes with mixed concerns
|
|
||||||
- suggest a small set of follow-up curation actions
|
|
||||||
- search by recent filenames or recent folders when file metadata is available
|
|
||||||
- keep edits conservative and return a review summary
|
|
||||||
|
|
||||||
### Serendipity review
|
|
||||||
|
|
||||||
- choose a note from the vault
|
|
||||||
- summarize it briefly
|
|
||||||
- compare it to the user's current topic or active project
|
|
||||||
- suggest only high-confidence connections
|
|
||||||
- pick one note from a user-specified folder or from curated folders only
|
|
||||||
- avoid randomizing across obviously raw capture unless the user asks for that
|
|
||||||
|
|
||||||
## Output Style
|
|
||||||
|
|
||||||
When responding to the user:
|
|
||||||
|
|
||||||
- state what kind of note you think it is
|
|
||||||
- summarize the curation changes you made or recommend
|
|
||||||
- list any extracted notes to create
|
|
||||||
- list meaningful links added or suggested
|
|
||||||
- mention any move or rename separately before doing it
|
|
||||||
- name the file or files touched
|
|
||||||
- separate completed edits from suggested next actions
|
|
||||||
- call out anything that still needs user confirmation
|
|
||||||
|
|
||||||
## If You Need More Context
|
|
||||||
|
|
||||||
If the vault structure is unclear, inspect folders and a few nearby notes before editing.
|
|
||||||
If note conventions appear to conflict, follow the most local `AGENTS.md` instructions in scope.
|
|
||||||
Don't guess at user preferences. When in doubt, ask the user before making big changes or suggesting speculative links.
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
---
|
|
||||||
id: pkm-curation-vault-conventions
|
|
||||||
aliases:
|
|
||||||
- PKM Curation Vault Conventions
|
|
||||||
tags:
|
|
||||||
- knowledge-management
|
|
||||||
- obsidian
|
|
||||||
- reference
|
|
||||||
- ai
|
|
||||||
area: Personal Knowledge Management
|
|
||||||
project:
|
|
||||||
---
|
|
||||||
|
|
||||||
# Vault Conventions
|
|
||||||
|
|
||||||
## Required Frontmatter
|
|
||||||
|
|
||||||
All notes should include YAML frontmatter with:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
---
|
|
||||||
id: unique-id
|
|
||||||
aliases: []
|
|
||||||
tags: []
|
|
||||||
area: Primary area/domain
|
|
||||||
project: [[Project Note]]
|
|
||||||
---
|
|
||||||
```
|
|
||||||
|
|
||||||
Use an empty value for `project:` when there is no relevant project note.
|
|
||||||
|
|
||||||
## Core Principles
|
|
||||||
|
|
||||||
- Markdown-first
|
|
||||||
- explicit `[[wikilinks]]`
|
|
||||||
- atomic notes for durable ideas
|
|
||||||
- project, topic, and date-based organization
|
|
||||||
- consistency over novelty
|
|
||||||
|
|
||||||
## Folder Roles
|
|
||||||
|
|
||||||
- `Inbox/`: raw capture and unprocessed notes
|
|
||||||
- `Dailies/`: day-specific notes and reflection
|
|
||||||
- `Templates/`: note templates
|
|
||||||
- `Knowledge/`: preferred home for evergreen atomic notes if the folder exists or is created
|
|
||||||
- project/topic folders: active work and structured reference material
|
|
||||||
|
|
||||||
## Task Conventions
|
|
||||||
|
|
||||||
- Use Markdown task items: `- [ ] Task description [[Project Name]]`
|
|
||||||
- Add status and priority tags where useful
|
|
||||||
- Use `due:: YYYY-MM-DD` for due dates
|
|
||||||
- Add `start::` and `end::` only when explicitly requested or when tracking active work
|
|
||||||
|
|
||||||
## Curation Heuristics
|
|
||||||
|
|
||||||
- A saved thing is not yet a knowledge note.
|
|
||||||
- A source note is not the same as an evergreen note.
|
|
||||||
- A link should reflect a real conceptual or project relationship.
|
|
||||||
- A long note may remain long if it is reference material; only extract notes when reuse is likely.
|
|
||||||
- Prefer gradual improvement over mass refactoring.
|
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
# PKM Skills
|
# PKM Skills
|
||||||
|
|
||||||
|
Personal knowledge management.
|
||||||
|
|
||||||
## User-invoked
|
## User-invoked
|
||||||
|
|
||||||
- [conversation-summary](conversation-summary/SKILL.md) — Summarize the current AI conversation into a new Obsidian markdown note and matching transcript file.
|
- [conversation-summary](conversation-summary/SKILL.md) — Save the current conversation as a comprehensive report note in your Obsidian vault, following OKF v0.1 conventions.
|
||||||
- [crit](crit/SKILL.md) — Brainstorm with AI using the CRIT framework to generate and evaluate ideas.
|
- [crit](crit/SKILL.md) — Run the CRIT framework — give the AI Context, assign it a Role, let it Interview you one question at a time, then issue the Task.
|
||||||
- [knowledge-gardener](knowledge-gardener/SKILL.md) — Run vault-aware semantic search, synthesis, note creation, linking, and Zettelkasten workflows for this Obsidian vault.
|
- [research-vault](research-vault/SKILL.md) — Research a topic through a one-question-at-a-time learning conversation, answer directly, share resources when useful, and save a linked OKF-conformant research packet in the Obsidian vault.
|
||||||
- [research-vault](research-vault/SKILL.md) — Research a topic through a one-question-at-a-time learning conversation and save a linked Obsidian research packet.
|
- [youtube-video-capture](youtube-video-capture/SKILL.md) — Fetch subtitles from a YouTube video, summarize the content, and save both the summary and raw subtitles to the Video bundle in the Obsidian vault.
|
||||||
|
|
||||||
## Model-invoked
|
## Model-invoked
|
||||||
|
|
||||||
_None yet._
|
- [pkm-curation](pkm-curation/SKILL.md) — Curate an Obsidian vault — classify notes, normalize frontmatter, add links, extract atomic notes. Use when curating, batch-processing, reviewing, or doing a serendipity pick.
|
||||||
|
|||||||
@@ -1,224 +1,76 @@
|
|||||||
---
|
---
|
||||||
name: conversation-summary
|
name: conversation-summary
|
||||||
description: Summarize the current AI conversation into a new Obsidian markdown note and matching transcript file. Use when the user asks to save, export, log, archive, or summarize the current chat into an Obsidian vault, research note, markdown note, meeting note, decision log, or second-brain workflow.
|
description: Save the current conversation as a comprehensive report note in your Obsidian vault, following OKF v0.1 conventions.
|
||||||
disable-model-invocation: true
|
disable-model-invocation: true
|
||||||
---
|
---
|
||||||
|
|
||||||
Create exactly one new Obsidian summary note for the current conversation. Also create exactly one transcript note for the same conversation.
|
Save this conversation as two linked files in your Obsidian vault: a report (the narrative) and a transcript (the raw conversation). The files live inside the `AI Conversation Summaries/` bundle and follow OKF v0.1 conventions.
|
||||||
|
|
||||||
## Default location
|
## Report
|
||||||
|
|
||||||
- Write notes to `obsidian vault` unless the user requests another path.
|
Write a thorough, standalone report as a Markdown note. A rich narrative in prose, organized under natural headings that follow the conversation's own shape. Capture everything of substance:
|
||||||
- Ask if you don't know the location of the obsidian vault.
|
|
||||||
- Create the directory if it does not exist.
|
|
||||||
- Never overwrite an existing note.
|
|
||||||
- Never append to an existing note unless the user explicitly asks.
|
|
||||||
|
|
||||||
## Required behavior
|
- What prompted the conversation and what context was shared
|
||||||
|
- What was explored, investigated, or discussed in detail — including code, files, paths, URLs, tools, or skills that came up
|
||||||
|
- What was found, decided, agreed, or concluded
|
||||||
|
- Explanations given and concepts clarified
|
||||||
|
- Open questions or unresolved items
|
||||||
|
|
||||||
1. Read the current conversation context only.
|
The report must be self-contained — someone reading it later should understand the full discussion, including the reasoning and all relevant details, without having been there. Err on the side of including too much detail rather than too little.
|
||||||
2. Generate a filesystem-safe title.
|
|
||||||
3. Write one summary note.
|
|
||||||
4. Write one transcript note.
|
|
||||||
5. Verify both files exist.
|
|
||||||
6. Return the exact paths, generated title, and count of action items captured.
|
|
||||||
|
|
||||||
## Title rules
|
Include a link to the companion transcript where it fits naturally — a markdown link like `[transcript](2026-05-23_16-35_obsidian-summary-skill-upgrade_A7K2_transcript.md)` in the section where it makes the most sense (often near the end).
|
||||||
|
|
||||||
Use this format:
|
## Frontmatter
|
||||||
|
|
||||||
`YYYY-MM-DD_HH-mm_<descriptor>_<ID>`
|
Both the report and the transcript use the merged OKF + vault frontmatter schema:
|
||||||
|
|
||||||
Rules:
|
```yaml
|
||||||
- `<descriptor>`: 3-6 word slug based on the main topic.
|
|
||||||
- Allowed characters in the full title: letters, digits, `.`, `_`, `-`.
|
|
||||||
- Replace spaces with `-`.
|
|
||||||
- Remove other punctuation.
|
|
||||||
- Collapse repeated separators.
|
|
||||||
- `<ID>`: 4-character uppercase alphanumeric suffix.
|
|
||||||
- If a filename collision occurs, regenerate only `<ID>` until unique.
|
|
||||||
- If too long for the filesystem, shorten only `<descriptor>`.
|
|
||||||
|
|
||||||
## Obsidian-specific rules
|
|
||||||
|
|
||||||
- Use valid YAML frontmatter.
|
|
||||||
- Keep frontmatter simple and machine-safe.
|
|
||||||
- Use wikilink-friendly filenames.
|
|
||||||
- Include both standard markdown links and Obsidian wikilinks where useful.
|
|
||||||
- Keep headings shallow and scannable.
|
|
||||||
- Use UTF-8 markdown.
|
|
||||||
- Prefer stable tags in frontmatter plus inline hashtag tags at the end.
|
|
||||||
- If the conversation mentions files, URLs, papers, repos, tools, docs, or paths, capture them in a dedicated `References` section.
|
|
||||||
- If the conversation includes explicit choices, approvals, rejections, or resolved tradeoffs, capture them in `Decisions Made`.
|
|
||||||
|
|
||||||
## Summary quality rules
|
|
||||||
|
|
||||||
- Use only facts available in the current conversation.
|
|
||||||
- Do not invent references, decisions, action items, or conclusions.
|
|
||||||
- Do not write a play-by-play transcript in the summary note.
|
|
||||||
- Synthesize the conversation into a compact research/work summary.
|
|
||||||
- If information is missing, say so explicitly.
|
|
||||||
- If the conversation is short, still use the full template with fallback lines.
|
|
||||||
- If the transcript available to you is incomplete, mark it as partial.
|
|
||||||
|
|
||||||
## Automatic tags
|
|
||||||
|
|
||||||
Generate 4-8 tags total.
|
|
||||||
|
|
||||||
Always include:
|
|
||||||
- `ai-summary`
|
|
||||||
- one domain/topic tag based on the conversation
|
|
||||||
- one workflow tag based on the type of work, if clear
|
|
||||||
|
|
||||||
Add tags from these categories when supported by the conversation:
|
|
||||||
- domain: `research`, `coding`, `obsidian`, `automation`, `writing`, `planning`, `debugging`
|
|
||||||
- artifact: `skill`, `note`, `transcript`, `review`, `decision-log`
|
|
||||||
- status: `draft`, `completed`, `follow-up`
|
|
||||||
- technology/tool names in lowercase slug form when clearly central
|
|
||||||
|
|
||||||
Tag rules:
|
|
||||||
- Use lowercase kebab-case only.
|
|
||||||
- Prefer specific tags over generic ones.
|
|
||||||
- Do not add unsupported tags.
|
|
||||||
|
|
||||||
## References capture
|
|
||||||
|
|
||||||
In the `References` section, capture conversation-specific source material that was explicitly mentioned or used, such as:
|
|
||||||
- local file paths
|
|
||||||
- URLs
|
|
||||||
- repository paths
|
|
||||||
- document names
|
|
||||||
- tool names
|
|
||||||
- named skills, scripts, or commands
|
|
||||||
|
|
||||||
Format each reference as one bullet with a short label and what it was used for.
|
|
||||||
|
|
||||||
If none were mentioned, write:
|
|
||||||
- `- No explicit references or source artifacts captured.`
|
|
||||||
|
|
||||||
## Decisions capture
|
|
||||||
|
|
||||||
Capture only explicit decisions. Include items such as:
|
|
||||||
- accepted approach
|
|
||||||
- rejected alternative
|
|
||||||
- agreed file location
|
|
||||||
- approved implementation direction
|
|
||||||
- confirmed formatting preference
|
|
||||||
|
|
||||||
If none were made, write:
|
|
||||||
- `- No explicit decisions captured.`
|
|
||||||
|
|
||||||
## Transcript rules
|
|
||||||
|
|
||||||
- Save transcript in a separate file named `{{title}}_transcript.md`.
|
|
||||||
- Keep chronological order.
|
|
||||||
- Redact likely secrets or credentials.
|
|
||||||
- Add `[Transcript may be partial]` at the top if the available transcript is incomplete.
|
|
||||||
- Do not inline the full transcript inside the summary note.
|
|
||||||
|
|
||||||
## Summary note template
|
|
||||||
|
|
||||||
```md
|
|
||||||
---
|
---
|
||||||
id: {{title}}
|
type: Conversation Report # report: Conversation Report, transcript: Conversation Transcript
|
||||||
aliases:
|
title: <descriptive title>
|
||||||
- {{descriptor}}
|
description: <one-line summary>
|
||||||
- {{short human title}}
|
tags: [conversation, <topic>]
|
||||||
tags:
|
timestamp: <ISO 8601 datetime>
|
||||||
- ai-summary
|
id: <unique-slug>
|
||||||
- {{tag1}}
|
aliases: []
|
||||||
- {{tag2}}
|
area: <derived from conversation>
|
||||||
- {{tag3}}
|
project: ''
|
||||||
created: {{ISO-8601 timestamp}}
|
|
||||||
source: current-ai-conversation
|
|
||||||
conversation_type: {{research|coding|planning|review|general}}
|
|
||||||
status: {{draft|completed|follow-up}}
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# {{short human title}}
|
|
||||||
|
|
||||||
> [!abstract]
|
|
||||||
> **Created:** {{ISO-8601 timestamp}}
|
|
||||||
> **Source:** Current AI conversation
|
|
||||||
> **Main Topic:** {{one sentence, 8-20 words}}
|
|
||||||
|
|
||||||
## Research Question / Objective
|
|
||||||
{{One concise sentence. If none: `Not explicitly provided.`}}
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
{{3-5 sentences synthesizing the main discussion, findings, constraints, and outcome. If none: `No meaningful summary could be derived beyond the limited conversation context.`}}
|
|
||||||
|
|
||||||
## Key Points
|
|
||||||
- {{3-7 concrete bullets}}
|
|
||||||
|
|
||||||
## Decisions Made
|
|
||||||
- {{explicit decision}}
|
|
||||||
|
|
||||||
## Action Items
|
|
||||||
- [ ] {{explicit next step}}
|
|
||||||
|
|
||||||
## Limitations / Unresolved Assumptions
|
|
||||||
- {{limitation or assumption}}
|
|
||||||
|
|
||||||
## Open Questions
|
|
||||||
- {{unresolved question}}
|
|
||||||
|
|
||||||
## References
|
|
||||||
- {{reference label}} — {{why it mattered}}
|
|
||||||
|
|
||||||
## Related Notes
|
|
||||||
- Transcript: [[{{title}}_transcript]]
|
|
||||||
- Markdown link: [{{title}}_transcript.md]({{title}}_transcript.md)
|
|
||||||
|
|
||||||
## Tags
|
|
||||||
#ai-summary {{inline_tags}}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Transcript template
|
## Bundle Awareness
|
||||||
|
|
||||||
```md
|
`AI Conversation Summaries/` is an OKF bundle. After writing each new report/transcript pair:
|
||||||
[Transcript may be partial]
|
|
||||||
|
|
||||||
# {{short human title}} — Transcript
|
1. Update `AI Conversation Summaries/index.md` — add an entry for the new report and transcript.
|
||||||
|
2. Append an entry to `AI Conversation Summaries/log.md` noting the addition.
|
||||||
|
|
||||||
**Summary Note:** [[{{title}}]]
|
## Transcript
|
||||||
**Created:** {{ISO-8601 timestamp}}
|
|
||||||
|
|
||||||
{{verbatim conversation transcript with likely secrets redacted}}
|
Save the raw conversation as a separate Markdown note alongside the report. Keep it chronological with no editorializing. Redact likely credentials, secrets, tokens, and private keys.
|
||||||
```
|
|
||||||
|
|
||||||
If the transcript is complete, omit the `[Transcript may be partial]` line.
|
## Location
|
||||||
|
|
||||||
## Fallback lines
|
Save both files to `AI Conversation Summaries/` under your vault root. Create the directory if needed. Never overwrite existing notes — if a filename collides, append a numeric suffix.
|
||||||
|
|
||||||
Use these exact fallbacks when needed:
|
## Filenames
|
||||||
- Decisions Made: `- No explicit decisions captured.`
|
|
||||||
- Action Items: `- [ ] No explicit action items identified.`
|
|
||||||
- Limitations / Unresolved Assumptions: `- No limitations or unresolved assumptions captured.`
|
|
||||||
- Open Questions: `- No open questions identified.`
|
|
||||||
- References: `- No explicit references or source artifacts captured.`
|
|
||||||
|
|
||||||
## File paths
|
- Report: `YYYY-MM-DD_HH-mm_<topic-slug>.md`
|
||||||
|
- Transcript: `YYYY-MM-DD_HH-mm_<topic-slug>_transcript.md`
|
||||||
|
|
||||||
- Summary: `AI Conversation Summaries/{{title}}.md`
|
## Links
|
||||||
- Transcript: `AI Conversation Summaries/{{title}}_transcript.md`
|
|
||||||
|
|
||||||
## Safety rules
|
Use standard markdown links: `[text](relative/path.md)`. Do NOT use `[[wikilinks]]`.
|
||||||
|
|
||||||
- Never overwrite existing notes.
|
Link the report to its transcript and vice versa using filenames:
|
||||||
- Never fabricate transcript lines.
|
- In report: `[transcript](YYYY-MM-DD_HH-mm_<topic-slug>_transcript.md)`
|
||||||
- Never fabricate references or decisions.
|
- In transcript: `[report](YYYY-MM-DD_HH-mm_<topic-slug>.md)`
|
||||||
- Redact likely credentials, secrets, tokens, and private keys.
|
|
||||||
- If writing fails, return the full markdown for both files plus intended paths.
|
|
||||||
- If required context is unavailable, state that clearly in the note instead of guessing.
|
|
||||||
|
|
||||||
## Final response format
|
## Safety
|
||||||
|
|
||||||
Confirm success with:
|
- Use only facts from the conversation. Do not invent references, decisions, or conclusions.
|
||||||
- summary file path
|
- Redact likely credentials, secrets, tokens, and private keys from any inline content.
|
||||||
- transcript file path
|
|
||||||
- generated title
|
## Done
|
||||||
- number of action items captured
|
|
||||||
- tags generated
|
Confirm both file paths and a one-sentence description of what the report covers. Also confirm that `AI Conversation Summaries/index.md` and `log.md` were updated.
|
||||||
- number of references captured
|
|
||||||
- number of explicit decisions captured
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "Conversation Summary"
|
||||||
|
short_description: "Summarize the conversation"
|
||||||
|
policy:
|
||||||
|
allow_implicit_invocation: false
|
||||||
+38
-90
@@ -1,119 +1,67 @@
|
|||||||
---
|
---
|
||||||
name: crit
|
name: crit
|
||||||
description: brainstorm with AI using the CRIT framework to generate and evaluate ideas.
|
description: Run the CRIT framework — give the AI Context, assign it a Role, let it Interview you one question at a time, then issue the Task.
|
||||||
disable-model-invocation: true
|
disable-model-invocation: true
|
||||||
---
|
---
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
This skill implements the CRIT (Context-Request-Input-Tone) framework for structured brainstorming and idea evaluation. It transforms abstract discussions into concrete, actionable outcomes through systematic analysis and iterative refinement.
|
CRIT is a four-step prompt framework by Geoff Woods: **Context, Role, Interview,
|
||||||
|
Task**. The sequence front-loads thinking before execution — the AI learns your
|
||||||
|
world, takes a specific lens, interviews you to surface what matters, then acts.
|
||||||
|
|
||||||
## Core Workflow
|
The insight: most people skip to Task and get generic output. The Interview
|
||||||
|
step — one question at a time, max three — is where the signal lives.
|
||||||
|
|
||||||
### Step 1: Context Analysis
|
## Steps
|
||||||
Before generating ideas, establish the foundation:
|
|
||||||
|
|
||||||
1. **Identify the Core Domain**
|
Run these four steps in order when the user invokes `/crit`.
|
||||||
- What field, industry, or subject area?
|
|
||||||
- What are the key constraints (time, resources, technical limitations)?
|
|
||||||
- Who is the target audience and their expertise level?
|
|
||||||
|
|
||||||
2. **Assess Current State**
|
### 1. Context — Give the AI your world
|
||||||
- What problems or opportunities exist?
|
|
||||||
- What has been tried before (if applicable)?
|
|
||||||
- What resources are available?
|
|
||||||
|
|
||||||
**Completion Criterion**: Clear understanding of domain, constraints, and current state documented.
|
Ask the user: "What should I know about you, your goals, your audience, and any
|
||||||
|
constraints?"
|
||||||
|
|
||||||
### Step 2: Request Clarification
|
Capture the answer in one paragraph. More detail is better.
|
||||||
Structure the brainstorming request:
|
|
||||||
|
|
||||||
1. **Define the Specific Goal**
|
**Completion criterion**: One paragraph covering identity, goal, audience, and
|
||||||
- What concrete outcome do you want?
|
constraints — confirmed by the user.
|
||||||
- What success criteria will be used?
|
|
||||||
- What is the expected timeline?
|
|
||||||
|
|
||||||
2. **Gather Input Requirements**
|
### 2. Role — Assign a viewpoint
|
||||||
- What information is needed to proceed?
|
|
||||||
- What assumptions should be validated?
|
|
||||||
- What data or resources are required?
|
|
||||||
|
|
||||||
**Completion Criterion**: Specific goal and input requirements clearly defined.
|
Ask the user: "What role should I take?"
|
||||||
|
|
||||||
### Step 3: Idea Generation
|
Guide toward a specific lens — "strategy coach who uncovers blind spots,"
|
||||||
Generate diverse, high-quality ideas:
|
"editor who cuts fluff," "architect who finds leverage points." Not "be
|
||||||
|
helpful."
|
||||||
|
|
||||||
1. **Divergent Thinking Phase**
|
**Completion criterion**: A single sentence assigning a named role that implies
|
||||||
- Generate 5-10 initial concepts without judgment
|
a specific viewpoint.
|
||||||
- Apply different perspectives (technical, business, user experience)
|
|
||||||
- Include both obvious and unconventional options
|
|
||||||
|
|
||||||
2. **Convergent Analysis Phase**
|
### 3. Interview — One question at a time
|
||||||
- Evaluate each idea against success criteria
|
|
||||||
- Score ideas on feasibility, impact, and alignment
|
|
||||||
- Identify patterns and synergies between ideas
|
|
||||||
|
|
||||||
**Completion Criterion**: Minimum 5 distinct ideas generated and evaluated with scores.
|
Instruct yourself: "Ask me no more than three questions, one at a time, to
|
||||||
|
clarify what I'm trying to achieve."
|
||||||
|
|
||||||
### Step 4: Iterative Refinement
|
Ask one question. Wait for the answer. Then ask the next. Max three. Do not
|
||||||
Improve selected ideas:
|
batch them.
|
||||||
|
|
||||||
1. **Select Top Candidates**
|
This step forces the user to slow down and think, and teaches the AI what
|
||||||
- Choose 2-3 ideas with highest potential
|
actually matters.
|
||||||
- Detail implementation approach for each
|
|
||||||
- Identify risks and mitigation strategies
|
|
||||||
|
|
||||||
2. **Develop Action Plans**
|
**Completion criterion**: 1-3 questions asked and answered, one at a time.
|
||||||
- Break down into concrete steps
|
Stop asking when the user signals readiness or you've asked three.
|
||||||
- Assign priorities and dependencies
|
|
||||||
- Define success metrics and checkpoints
|
|
||||||
|
|
||||||
**Completion Criterion**: 2-3 refined ideas with detailed action plans.
|
### 4. Task — Issue the assignment
|
||||||
|
|
||||||
### Step 5: Tone & Delivery
|
Ask the user: "What's the task?"
|
||||||
Adapt communication to the audience:
|
|
||||||
|
|
||||||
1. **Choose Appropriate Role**
|
Guide toward a short, clear, slightly uncomfortable prompt that asks the AI to
|
||||||
- Subject Matter Expert for technical depth
|
*think*, not just write. Reference the preceding interview.
|
||||||
- Consultant for strategic guidance
|
|
||||||
- Teacher for complex concepts
|
|
||||||
- Collaborator for co-creation
|
|
||||||
- Analyst for multi-perspective evaluation
|
|
||||||
|
|
||||||
2. **Structure Response**
|
> "Based on our conversation, give me three non-obvious actions I can take.
|
||||||
- Lead with clear, actionable solutions
|
> Make them surprising but realistic."
|
||||||
- Organize information logically and concisely
|
|
||||||
- Include examples or analogies for clarity
|
|
||||||
- Suggest next steps and follow-up questions
|
|
||||||
|
|
||||||
**Completion Criterion**: Response delivered in appropriate tone with clear structure.
|
Execute the task.
|
||||||
|
|
||||||
## Quality Guardrails
|
|
||||||
|
|
||||||
- Never generate ideas without clear context and goals
|
|
||||||
- Always evaluate ideas against defined success criteria
|
|
||||||
- Maintain transparency about assumptions and limitations
|
|
||||||
- Encourage iteration and collaboration throughout the process
|
|
||||||
- Ensure all recommendations are practical and actionable
|
|
||||||
|
|
||||||
## Trigger Hints
|
|
||||||
|
|
||||||
Load this skill when the user asks to:
|
|
||||||
- Generate ideas for a specific problem or opportunity
|
|
||||||
- Brainstorm solutions with structured evaluation
|
|
||||||
- Get expert guidance through systematic analysis
|
|
||||||
- Develop actionable plans from abstract concepts
|
|
||||||
- Evaluate multiple approaches to a challenge
|
|
||||||
|
|
||||||
## Output Format
|
|
||||||
|
|
||||||
Return results in this structure:
|
|
||||||
|
|
||||||
1. **Context Analysis**: Documented understanding of domain and constraints
|
|
||||||
2. **Request Clarification**: Defined goals and input requirements
|
|
||||||
3. **Idea Generation**: List of ideas with evaluation scores
|
|
||||||
4. **Refined Solutions**: Detailed action plans for top candidates
|
|
||||||
5. **Next Steps**: Suggested follow-up actions and questions
|
|
||||||
|
|
||||||
Each section should be clearly labeled and include completion criteria verification.
|
|
||||||
|
|
||||||
|
**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,223 @@
|
|||||||
|
---
|
||||||
|
name: pkm-curation
|
||||||
|
description: Curate an Obsidian vault — classify notes, normalize frontmatter, add links, extract atomic notes. Use when curating, batch-processing, reviewing, or doing a serendipity pick.
|
||||||
|
---
|
||||||
|
|
||||||
|
# PKM Curation
|
||||||
|
|
||||||
|
Use this skill when working inside a Markdown-first vault that follows the [Open Knowledge Format (OKF) v0.1](https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md) conventions.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- Turn raw notes into reusable atomic notes.
|
||||||
|
- Keep new notes consistent with vault conventions.
|
||||||
|
- Strengthen the link graph with meaningful markdown links.
|
||||||
|
- Extract atomic notes from long or mixed-topic notes.
|
||||||
|
- Maintain bundle integrity: update index.md and log.md when adding or changing bundle contents.
|
||||||
|
|
||||||
|
## Read This First
|
||||||
|
|
||||||
|
- Read `AGENTS.md` in the current repo scope before editing notes.
|
||||||
|
- Read `references/vault-conventions.md` when normalizing metadata, deciding note types, or choosing folders. This reference now documents the merged OKF + vault frontmatter schema.
|
||||||
|
- Read `references/agent-integration.md` when running this skill through an agent.
|
||||||
|
- Keep changes small and reviewable.
|
||||||
|
- Keep file operations local to the vault unless the user explicitly asks otherwise.
|
||||||
|
|
||||||
|
## Search
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Search by filename
|
||||||
|
fd --type f ".md" "path/to/obsidian-vault" | rg -i "keyword"
|
||||||
|
|
||||||
|
# Search by content
|
||||||
|
rg -l "keyword" "path/to/obsidian-vault" -g "*.md"
|
||||||
|
|
||||||
|
# Find backlinks to a note (markdown link form)
|
||||||
|
rg -l "Note Title" "path/to/obsidian-vault" -g "*.md"
|
||||||
|
|
||||||
|
# Find bundle index files
|
||||||
|
fd "index.md" "path/to/obsidian-vault"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- Prefer curation over reorganization.
|
||||||
|
- Do not move, rename, or delete many notes at once unless the user asks.
|
||||||
|
- Do not invent links based only on shared words.
|
||||||
|
- Preserve the user's voice unless the user asks for a rewrite.
|
||||||
|
- Keep source material and evergreen ideas separate when possible.
|
||||||
|
- Treat `Inbox/` as temporary capture, not long-term storage.
|
||||||
|
- Preserve all command blocks, code snippets, configuration directives, and step-by-step instructions verbatim. Do not summarize or condense them.
|
||||||
|
- For reference/source notes: add a brief overview at the top, but keep the original commands and details intact below. Completeness > brevity.
|
||||||
|
- Read every file completely before moving, renaming, or modifying it. Do not rely on head/tail, heading-only scans, or partial reads to judge a file's content.
|
||||||
|
- **Bundle awareness**: When creating or editing notes inside a bundle directory, update the bundle's `index.md` (add or update the entry) and append an entry to `log.md`.
|
||||||
|
|
||||||
|
## Note-Type Heuristics
|
||||||
|
|
||||||
|
Use the type vocabulary from `references/vault-conventions.md`. When inferring a type for a note, use these heuristics:
|
||||||
|
|
||||||
|
### Inbox note → `type: Inbox`
|
||||||
|
Use when the note is raw capture, partial thinking, copied text, or an unprocessed link dump.
|
||||||
|
- Actions: clean obvious structure issues, add frontmatter if missing, classify for later promotion, avoid over-polishing unless requested.
|
||||||
|
|
||||||
|
### Source note → `type: Source`
|
||||||
|
Use when the note is based on an article, video, book, paper, transcript, or other external material.
|
||||||
|
- Actions: keep source context intact, summarize key takeaways, extract reusable ideas into separate atomic notes, link to related concepts and projects.
|
||||||
|
|
||||||
|
### Atomic note → `type: Concept`
|
||||||
|
Use when the note captures one durable idea, concept, claim, pattern, or insight.
|
||||||
|
- Actions: ensure one main idea per note, make the title concept-focused, add links to neighboring ideas, keep it concise and self-contained.
|
||||||
|
|
||||||
|
### Project note → `type: Project`
|
||||||
|
Use when the note supports active work, planning, resources, decisions, or tasks.
|
||||||
|
- Actions: preserve project context, link tasks to the project note, avoid turning active project logistics into evergreen notes unless there is a reusable insight.
|
||||||
|
|
||||||
|
### Daily note → `type: Daily`
|
||||||
|
Use when the note is date-based and captures activity, learning, tasks, or reflection for a single day.
|
||||||
|
- Actions: preserve chronology, link out to durable notes rather than stuffing ideas into the daily note.
|
||||||
|
|
||||||
|
### Reference note → `type: Reference`
|
||||||
|
Use when the note is lookup material, documentation, specs, or external reference.
|
||||||
|
- Actions: preserve the reference content, add structured overview at top, link to related notes.
|
||||||
|
|
||||||
|
## Frontmatter Normalization
|
||||||
|
|
||||||
|
All notes should have the merged OKF + vault frontmatter schema:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
type: <OKF type name> # OKF required
|
||||||
|
title: <display name> # OKF recommended
|
||||||
|
description: <one-line summary> # OKF recommended
|
||||||
|
resource: <canonical URI> # OKF recommended (when applicable)
|
||||||
|
tags: [<tag>, ...] # OKF recommended + vault required
|
||||||
|
timestamp: <ISO 8601 datetime> # OKF recommended
|
||||||
|
id: <unique identifier> # vault required
|
||||||
|
aliases: [<alias>, ...] # vault required
|
||||||
|
area: <area/domain> # vault required
|
||||||
|
project: <project name or ''> # vault required
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
When normalizing existing frontmatter:
|
||||||
|
- Add `type` using the classification heuristics above.
|
||||||
|
- Ensure `id` is a stable kebab-case slug.
|
||||||
|
- Ensure `timestamp` is ISO 8601 format.
|
||||||
|
- Ensure `project` is a plain string, not a `[[wikilink]]`.
|
||||||
|
- Preserve any additional OKF extension keys.
|
||||||
|
|
||||||
|
## Link Convention
|
||||||
|
|
||||||
|
Use **standard markdown links**: `[text](relative/path.md)`. Do NOT use `[[wikilinks]]`.
|
||||||
|
|
||||||
|
When converting existing wikilinks:
|
||||||
|
- `[[Note Title]]` → `[Note Title](Note%20Title.md)`
|
||||||
|
- `[[Note Title|alias]]` → `[alias](Note%20Title.md)`
|
||||||
|
|
||||||
|
Use vault-relative paths from the linking file to the target.
|
||||||
|
|
||||||
|
## Bundle Awareness
|
||||||
|
|
||||||
|
When working inside a bundle directory:
|
||||||
|
- **After creating a new note**: add an entry to the bundle's `index.md` and append an entry to `log.md`.
|
||||||
|
- **After modifying an existing note**: if the change is significant, update the description in `index.md` and append an entry to `log.md`.
|
||||||
|
- **After deleting or moving a note**: remove or update its entry in `index.md` and append an entry to `log.md`.
|
||||||
|
|
||||||
|
The bundle map is defined in `references/vault-conventions.md`. Key bundle directories include: `Knowledge/`, `Notes/` (with sub-bundles), `Resources/` (with sub-bundles), `Profiles/`, `AI Conversation Summaries/`, `Research/<packet>/`, `Projects/<project>/`.
|
||||||
|
|
||||||
|
Non-bundle directories (`Inbox/`, `Dailies/`, `Templates/`, `Clippings/`) do not get `index.md` or `log.md`.
|
||||||
|
|
||||||
|
## Linking Guidance
|
||||||
|
|
||||||
|
Add links only when they express one of these relationships:
|
||||||
|
|
||||||
|
- concept to broader concept
|
||||||
|
- source note to extracted idea
|
||||||
|
- project note to relevant knowledge note
|
||||||
|
- daily note to work done or ideas learned
|
||||||
|
- sibling concepts that genuinely inform one another
|
||||||
|
|
||||||
|
When linking, prefer existing notes over creating speculative new ones.
|
||||||
|
|
||||||
|
## Extraction Guidance
|
||||||
|
|
||||||
|
Extract atomic notes when a note contains:
|
||||||
|
|
||||||
|
- multiple durable ideas
|
||||||
|
- a strong claim hidden in raw notes
|
||||||
|
- a reusable method, distinction, or definition
|
||||||
|
- a concept that should be linked from many places
|
||||||
|
|
||||||
|
Keep extracted notes short. One note, one idea. When extracting into a bundle directory, update `index.md` and `log.md`.
|
||||||
|
|
||||||
|
## Common Tasks
|
||||||
|
|
||||||
|
### Curate one note
|
||||||
|
|
||||||
|
1. Inspect the target note and locate its file in the vault.
|
||||||
|
2. Identify the note type: inbox, source, concept, project, daily, reference, or person.
|
||||||
|
3. Normalize frontmatter and basic structure using the merged schema.
|
||||||
|
4. Clarify the title if vague or timestamp-like.
|
||||||
|
5. Tighten headings and summary; distill if it mixes too many ideas.
|
||||||
|
6. Inspect nearby related notes, then add a few strong markdown links.
|
||||||
|
7. If the note contains multiple durable ideas, extract 1-3 atomic notes.
|
||||||
|
8. Suggest moving only if the destination is clearly better.
|
||||||
|
9. If inside a bundle, update `index.md` and `log.md`.
|
||||||
|
10. Patch the note in place and return a short summary of edits.
|
||||||
|
|
||||||
|
**Completion Criterion**: Note inspected, classified, normalized, linked, and patched, with a summary returned to the user.
|
||||||
|
|
||||||
|
### Curate an inbox batch
|
||||||
|
|
||||||
|
- process a small batch, usually 5-10 notes
|
||||||
|
- classify each note
|
||||||
|
- normalize metadata
|
||||||
|
- suggest which notes should stay raw, become source notes, or become atomic notes
|
||||||
|
- avoid large folder reshuffles unless the pattern is clear
|
||||||
|
- enumerate a small set of `Inbox/` notes
|
||||||
|
- process them one at a time
|
||||||
|
- stop and summarize after each batch
|
||||||
|
|
||||||
|
**Completion Criterion**: Each note in the batch classified and normalized, with a summary returned after the batch.
|
||||||
|
|
||||||
|
### Review recent notes
|
||||||
|
|
||||||
|
- inspect recently edited notes
|
||||||
|
- identify missing links and vague titles
|
||||||
|
- flag notes with mixed concerns
|
||||||
|
- suggest a small set of follow-up curation actions
|
||||||
|
- search by recent filenames or recent folders when file metadata is available
|
||||||
|
- keep edits conservative and return a review summary
|
||||||
|
|
||||||
|
**Completion Criterion**: Recently edited notes inspected, missing links and mixed concerns flagged, with a review summary returned.
|
||||||
|
|
||||||
|
### Serendipity review
|
||||||
|
|
||||||
|
- choose a note from the vault
|
||||||
|
- summarize it briefly
|
||||||
|
- compare it to the user's current topic or active project
|
||||||
|
- suggest only high-confidence connections
|
||||||
|
- pick one note from a user-specified folder or from curated folders only
|
||||||
|
- avoid randomizing across obviously raw capture unless the user asks for that
|
||||||
|
|
||||||
|
**Completion Criterion**: One curated note chosen, briefly summarized, compared to active topic, with only high-confidence connections suggested.
|
||||||
|
|
||||||
|
## Output Style
|
||||||
|
|
||||||
|
When responding to the user:
|
||||||
|
|
||||||
|
- state what kind of note you think it is
|
||||||
|
- summarize the curation changes you made or recommend
|
||||||
|
- list any extracted notes to create
|
||||||
|
- list meaningful links added or suggested
|
||||||
|
- mention any move or rename separately before doing it
|
||||||
|
- name the file or files touched
|
||||||
|
- separate completed edits from suggested next actions
|
||||||
|
- call out anything that still needs user confirmation
|
||||||
|
- mention any bundle index/log updates made
|
||||||
|
|
||||||
|
## If You Need More Context
|
||||||
|
|
||||||
|
If the vault structure is unclear, inspect folders and a few nearby notes before editing.
|
||||||
|
If note conventions appear to conflict, follow the most local `AGENTS.md` instructions in scope.
|
||||||
|
Don't guess at user preferences. When in doubt, ask the user before making big changes or suggesting speculative links.
|
||||||
@@ -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
|
||||||
+17
-4
@@ -5,15 +5,16 @@ aliases:
|
|||||||
tags:
|
tags:
|
||||||
- knowledge-management
|
- knowledge-management
|
||||||
- reference
|
- reference
|
||||||
|
- okf
|
||||||
area: Personal Knowledge Management
|
area: Personal Knowledge Management
|
||||||
project:
|
project: ''
|
||||||
---
|
---
|
||||||
|
|
||||||
# Agent Integration
|
# Agent Integration
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
This skill should be usable from any agent, and it should also fit chat environments where the agent as tool access.
|
This skill should be usable from any agent, and it should also fit chat environments where the agent has tool access.
|
||||||
|
|
||||||
## Recommended Role
|
## Recommended Role
|
||||||
|
|
||||||
@@ -21,6 +22,14 @@ This skill should be usable from any agent, and it should also fit chat environm
|
|||||||
- interactive note refinement, serendipity review, and exploratory linking sessions
|
- interactive note refinement, serendipity review, and exploratory linking sessions
|
||||||
- editor-side entry point that delegates vault actions to agent where possible
|
- editor-side entry point that delegates vault actions to agent where possible
|
||||||
|
|
||||||
|
## Bundle Awareness
|
||||||
|
|
||||||
|
The vault follows OKF v0.1 bundle conventions. When running curation tasks:
|
||||||
|
|
||||||
|
- Creating a note inside a bundle directory: add an entry to `index.md` and an event to `log.md`.
|
||||||
|
- Modifying a note inside a bundle: update `index.md` description if the note's purpose changes; append to `log.md` for significant changes.
|
||||||
|
- Moving notes between bundle directories: update both source and destination bundle files.
|
||||||
|
|
||||||
## Preferred Behaviors
|
## Preferred Behaviors
|
||||||
|
|
||||||
- search the vault before proposing links
|
- search the vault before proposing links
|
||||||
@@ -29,14 +38,16 @@ This skill should be usable from any agent, and it should also fit chat environm
|
|||||||
- summarize edits in plain Markdown
|
- summarize edits in plain Markdown
|
||||||
- ask before removing any content or links from a note
|
- ask before removing any content or links from a note
|
||||||
- ask before moving, renaming, or creating many files
|
- ask before moving, renaming, or creating many files
|
||||||
|
- update bundle `index.md` and `log.md` when creating notes inside bundles
|
||||||
|
|
||||||
## Good Task Shapes
|
## Good Task Shapes
|
||||||
|
|
||||||
- curate a specific note
|
- curate a specific note (normalize frontmatter, classify type, add markdown links)
|
||||||
- process a small `Inbox/` batch
|
- process a small `Inbox/` batch (classify and normalize)
|
||||||
- review recent notes for missing links
|
- review recent notes for missing links
|
||||||
- extract atomic notes from one source note
|
- extract atomic notes from one source note
|
||||||
- run a serendipity review against a current topic
|
- run a serendipity review against a current topic
|
||||||
|
- update or regenerate bundle `index.md` for a directory
|
||||||
|
|
||||||
## Avoid
|
## Avoid
|
||||||
|
|
||||||
@@ -44,8 +55,10 @@ This skill should be usable from any agent, and it should also fit chat environm
|
|||||||
- broad speculative linking passes
|
- broad speculative linking passes
|
||||||
- converting every long note into atomic notes
|
- converting every long note into atomic notes
|
||||||
- changing note titles without stating why
|
- changing note titles without stating why
|
||||||
|
- using `[[wikilinks]]` — always use standard markdown `[...](...)` links
|
||||||
|
|
||||||
## Portability Guidance
|
## Portability Guidance
|
||||||
|
|
||||||
- keep the workflow in `SKILL.md` tool-agnostic where possible
|
- keep the workflow in `SKILL.md` tool-agnostic where possible
|
||||||
- prefer small deterministic file edits so the skill remains portable to other `SKILL.md`-based agents
|
- prefer small deterministic file edits so the skill remains portable to other `SKILL.md`-based agents
|
||||||
|
- always reference `vault-conventions.md` for the current frontmatter schema and type vocabulary
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
---
|
||||||
|
id: pkm-curation-vault-conventions
|
||||||
|
aliases:
|
||||||
|
- PKM Curation Vault Conventions
|
||||||
|
tags:
|
||||||
|
- knowledge-management
|
||||||
|
- obsidian
|
||||||
|
- reference
|
||||||
|
- okf
|
||||||
|
- ai
|
||||||
|
area: Personal Knowledge Management
|
||||||
|
project:
|
||||||
|
---
|
||||||
|
|
||||||
|
# Vault Conventions
|
||||||
|
|
||||||
|
This vault follows the [Open Knowledge Format (OKF) v0.1](https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md) as its structural backbone. All notes, bundles, and links conform to OKF conventions unless noted below.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Frontmatter Schema (merged — OKF + vault fields)
|
||||||
|
|
||||||
|
Every non-reserved `.md` file **MUST** have YAML frontmatter with the following fields:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
type: <OKF type name> # OKF required
|
||||||
|
title: <display name> # OKF recommended
|
||||||
|
description: <one-line summary> # OKF recommended
|
||||||
|
resource: <canonical URI> # OKF recommended (when applicable)
|
||||||
|
tags: [<tag>, ...] # OKF recommended + vault required
|
||||||
|
timestamp: <ISO 8601 datetime> # OKF recommended
|
||||||
|
id: <unique identifier> # vault required
|
||||||
|
aliases: [<alias>, ...] # vault required
|
||||||
|
area: <area/domain> # vault required
|
||||||
|
project: <project name or ''> # vault required
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
### Field Notes
|
||||||
|
|
||||||
|
- **`type`** — Must be one of the OKF type names from the Type Vocabulary table below. This is the **only** required OKF field.
|
||||||
|
- **`title`** — Human-readable display name. If omitted, consumers may derive a title from the filename.
|
||||||
|
- **`description`** — One-line summary. Used by index.md generators, search snippets, and previews.
|
||||||
|
- **`resource`** — A URI identifying the underlying asset the concept describes. Omit for abstract ideas.
|
||||||
|
- **`tags`** — YAML list of short strings for cross-cutting categorization. Always include at least one tag.
|
||||||
|
- **`timestamp`** — ISO 8601 datetime of last meaningful change (e.g. `2026-07-06T12:00:00Z`).
|
||||||
|
- **`id`** — Stable unique identifier (kebab-case slug, never changes).
|
||||||
|
- **`aliases`** — List of alternative titles for search/discovery.
|
||||||
|
- **`area`** — Primary domain or area of knowledge.
|
||||||
|
- **`project`** — Associated project name, or empty string `''` when none.
|
||||||
|
|
||||||
|
**OKF extensions:** Any additional producer-defined keys may be included. Consumers MUST preserve unknown keys when round-tripping.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Link Convention
|
||||||
|
|
||||||
|
Use **standard markdown links**: `[text](relative/path.md)`.
|
||||||
|
|
||||||
|
Do NOT use `[[wikilinks]]`. Obsidian renders standard markdown links identically to wikilinks, and markdown links are portable across all markdown renderers.
|
||||||
|
|
||||||
|
### Link Forms
|
||||||
|
|
||||||
|
| Target | Markdown form |
|
||||||
|
|--------|---------------|
|
||||||
|
| Another note in same directory | `[Note Title](Note%20Title.md)` |
|
||||||
|
| Note in subdirectory | `[Note Title](../Subdir/Note%20Title.md)` |
|
||||||
|
| Note with alias | `[alias](Note%20Title.md)` |
|
||||||
|
| Absolute (bundle-relative) | `[Note Title](/path/from/bundle/root.md)` |
|
||||||
|
| External URL | `[text](https://example.org)` |
|
||||||
|
|
||||||
|
### When to Link
|
||||||
|
|
||||||
|
Add links only when they express a real relationship:
|
||||||
|
- concept to broader concept
|
||||||
|
- source note to extracted idea
|
||||||
|
- project note to relevant knowledge note
|
||||||
|
- daily note to work done or ideas learned
|
||||||
|
- sibling concepts that genuinely inform one another
|
||||||
|
|
||||||
|
Do not link on shared words alone.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bundle Definition
|
||||||
|
|
||||||
|
A **bundle** is a subdirectory that follows OKF progressive-disclosure conventions. Every bundle contains two reserved files:
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `index.md` | OKF progressive-disclosure listing (no frontmatter, sections with bullet links) |
|
||||||
|
| `log.md` | OKF chronological update history (datestamped entries, newest first) |
|
||||||
|
|
||||||
|
These filenames are **reserved** — they MUST NOT be used for concept documents.
|
||||||
|
|
||||||
|
### Bundle root (vault root)
|
||||||
|
|
||||||
|
`sjb-brain/` (the vault root) is a bundle. It has its own `index.md` and `log.md`.
|
||||||
|
|
||||||
|
### Bundle map
|
||||||
|
|
||||||
|
| Path | bundle? |
|
||||||
|
|------|:-------:|
|
||||||
|
| `sjb-brain/` (root) | ✅ |
|
||||||
|
| `Knowledge/` | ✅ |
|
||||||
|
| `Notes/` + subdirs | ✅ (each subdir is a sub-bundle) |
|
||||||
|
| `Resources/` + subdirs | ✅ (each subdir is a sub-bundle) |
|
||||||
|
| `Profiles/` | ✅ |
|
||||||
|
| `AI Conversation Summaries/` | ✅ |
|
||||||
|
| `Research/<packet>/` | ✅ (each packet is a bundle) |
|
||||||
|
| `Projects/<project>/` | ✅ (each project is a bundle, migrate flat notes to dirs) |
|
||||||
|
| `Inbox/` | ❌ |
|
||||||
|
| `Dailies/` | ❌ |
|
||||||
|
| `Templates/` | ❌ |
|
||||||
|
| `Clippings/` | ❌ |
|
||||||
|
| `Tasks/`, `tools/`, `wts-services/` | ⏳ deferred |
|
||||||
|
|
||||||
|
Non-bundle directories are treated as flat collections. They do not get `index.md` or `log.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Type Vocabulary
|
||||||
|
|
||||||
|
The following OKF type names are used in this vault. Every note MUST have exactly one `type` from this table.
|
||||||
|
|
||||||
|
| type | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `Inbox` | Raw capture, unprocessed |
|
||||||
|
| `Source` | Based on external material |
|
||||||
|
| `Concept` | One durable evergreen idea |
|
||||||
|
| `Project` | Active work, planning |
|
||||||
|
| `Daily` | Day-specific activity |
|
||||||
|
| `Reference` | Lookup material, specs |
|
||||||
|
| `Conversation Report` | Narrative summary of conversation |
|
||||||
|
| `Conversation Transcript` | Raw conversation transcript |
|
||||||
|
| `Research Synthesis` | Compiled answer for a research topic |
|
||||||
|
| `Research Claim Index` | Collection of evidence-backed claims |
|
||||||
|
| `Research Source List` | Index of sources consulted |
|
||||||
|
| `Research Glossary` | Term definitions for a topic |
|
||||||
|
| `Research Questions` | Open and resolved questions |
|
||||||
|
| `Research Log` | Chronological research session log |
|
||||||
|
| `MOC` | Map of content / curated index |
|
||||||
|
| `Template` | Template note for note creation |
|
||||||
|
| `Person` | A person you want notes about |
|
||||||
|
| `Tool` | A tool, app, or service |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Folder Roles
|
||||||
|
|
||||||
|
- `Inbox/`: raw capture and unprocessed notes (not a bundle)
|
||||||
|
- `Dailies/`: day-specific notes and reflection (not a bundle)
|
||||||
|
- `Templates/`: note templates (not a bundle)
|
||||||
|
- `Clippings/`: clipped web articles (not a bundle)
|
||||||
|
- `Knowledge/`: preferred home for evergreen atomic notes (bundle)
|
||||||
|
- `Notes/`: technical and domain notes, with subdirectories for each topic (bundle with sub-bundles)
|
||||||
|
- `Resources/`: reference and resource notes, with subdirectories for each domain (bundle with sub-bundles)
|
||||||
|
- `Profiles/`: notes about people (bundle)
|
||||||
|
- `AI Conversation Summaries/`: saved conversation summaries and transcripts (bundle)
|
||||||
|
- `Research/<packet>/`: research packets, each a self-contained bundle
|
||||||
|
- `Projects/<project>/`: active project notes, each project a sub-bundle
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task Conventions
|
||||||
|
|
||||||
|
- Use Markdown task items: `- [ ] Task description [Project Name](Projects/Project%20Name.md)`
|
||||||
|
- Add status and priority tags where useful
|
||||||
|
- Use `due:: YYYY-MM-DD` for due dates
|
||||||
|
- Add `start::` and `end::` only when explicitly requested or when tracking active work
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Curation Heuristics
|
||||||
|
|
||||||
|
- A saved thing is not yet a knowledge note.
|
||||||
|
- A source note is not the same as an evergreen note.
|
||||||
|
- A link should reflect a real conceptual or project relationship.
|
||||||
|
- A long note may remain long if it is reference material; only extract notes when reuse is likely.
|
||||||
|
- Prefer gradual improvement over mass refactoring.
|
||||||
|
- When creating or editing notes inside a bundle directory, maintain the bundle's `index.md` (add/update entries).
|
||||||
|
- When a bundle's contents change significantly, append an entry to the bundle's `log.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Principles
|
||||||
|
|
||||||
|
- Markdown-first
|
||||||
|
- OKF-conformant bundles with `index.md` and `log.md`
|
||||||
|
- Standard markdown `[...](...)` links (not wikilinks)
|
||||||
|
- Atomic notes for durable ideas
|
||||||
|
- Project, topic, and date-based organization
|
||||||
|
- Consistency over novelty
|
||||||
@@ -1,16 +1,95 @@
|
|||||||
---
|
---
|
||||||
name: research-vault
|
name: research-vault
|
||||||
description: Research a topic through a one-question-at-a-time learning conversation, answer directly, share resources when useful, and save a linked Obsidian research packet.
|
description: 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.
|
||||||
disable-model-invocation: true
|
disable-model-invocation: true
|
||||||
---
|
---
|
||||||
|
|
||||||
# Research Vault
|
# Research Vault
|
||||||
|
|
||||||
Use when the user wants to learn or research a topic conversationally and save the outcome in an Obsidian vault.
|
Use when the user wants to learn or research a topic conversationally and save the outcome in an Obsidian vault using the [Open Knowledge Format (OKF) v0.1](https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md).
|
||||||
|
|
||||||
## Packet
|
## Packet
|
||||||
|
|
||||||
Create one vault folder per research run, normally `Research/<YYYY-MM-DD> <Topic>/`, containing all notes from the run: `Index.md`, `Sources.md`, `Synthesis.md`, `Claims.md`, `Questions.md`, `Conversation.md`, `Glossaries.md`, `Log.md`, and any atomic notes. Treat the packet as a Karpathy-style compiled wiki for the topic: raw sources stay immutable, packet notes are the maintained synthesis layer, and this skill is the schema. Load `references/research-packet-template.md` before writing packet files or when exact structure matters.
|
Create one vault folder per research run, normally `Research/<YYYY-MM-DD> <Topic>/`, containing an OKF bundle with `index.md` (progressive-disclosure listing, no frontmatter), `log.md` (chronological), and packet pages. Load `references/research-packet-template.md` before writing packet files or when exact structure matters.
|
||||||
|
|
||||||
|
### Bundle structure
|
||||||
|
|
||||||
|
```
|
||||||
|
Research/<YYYY-MM-DD> <Topic>/
|
||||||
|
├── index.md # OKF progressive-disclosure, no frontmatter
|
||||||
|
├── log.md # OKF chronological, datestamped entries (newest first)
|
||||||
|
├── Sources.md # Research Source List
|
||||||
|
├── Synthesis.md # Research Synthesis
|
||||||
|
├── Claims.md # Research Claim Index
|
||||||
|
├── Questions.md # Research Questions
|
||||||
|
├── Conversation.md # Conversation Report
|
||||||
|
├── Glossaries.md # Research Glossary
|
||||||
|
└── <atomic-note>.md # Concept / atomic notes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reserved filenames
|
||||||
|
|
||||||
|
`index.md` and `log.md` are reserved — they MUST follow OKF format and MUST NOT be used for concept documents.
|
||||||
|
|
||||||
|
### Frontmatter schema (merged — OKF + vault fields)
|
||||||
|
|
||||||
|
Every concept document (non-reserved `.md` file) MUST have:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
type: <OKF type name> # OKF required
|
||||||
|
title: <display name> # OKF recommended
|
||||||
|
description: <one-line summary> # OKF recommended
|
||||||
|
resource: <canonical URI> # OKF recommended (when applicable)
|
||||||
|
tags: [<tag>, ...] # OKF recommended + vault required
|
||||||
|
timestamp: <ISO 8601 datetime> # OKF recommended
|
||||||
|
id: <unique identifier> # vault required
|
||||||
|
aliases: [<alias>, ...] # vault required
|
||||||
|
area: <area/domain> # vault required
|
||||||
|
project: <project name or ''> # vault required
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the type vocabulary from `references/vault-conventions.md`.
|
||||||
|
|
||||||
|
### `index.md` (OKF progressive-disclosure)
|
||||||
|
|
||||||
|
No frontmatter. Sections with bullet links and descriptions:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# <Topic>
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
- Learning goal:
|
||||||
|
- Scope:
|
||||||
|
- Success criteria:
|
||||||
|
|
||||||
|
## Packet Map
|
||||||
|
- [Sources](Sources.md) — compiled source list
|
||||||
|
- [Synthesis](Synthesis.md) — synthesized answer
|
||||||
|
- [Claims](Claims.md) — evidence-backed claims
|
||||||
|
- [Questions](Questions.md) — open and resolved questions
|
||||||
|
- [Conversation](Conversation.md) — raw conversation record
|
||||||
|
- [Glossaries](Glossaries.md) — term definitions
|
||||||
|
- [Log](Log.md) — session log
|
||||||
|
|
||||||
|
## Compiled Pages
|
||||||
|
| Page | Type | Purpose | Status |
|
||||||
|
|---|---|---|---|
|
||||||
|
| | | | draft \| reviewed \| promoted |
|
||||||
|
```
|
||||||
|
|
||||||
|
### `log.md` (OKF chronological)
|
||||||
|
|
||||||
|
Datestamped entries, newest first:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Log
|
||||||
|
|
||||||
|
## YYYY-MM-DD
|
||||||
|
* **Event type**: Description of what happened.
|
||||||
|
* **Files changed**: ...
|
||||||
|
```
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
@@ -18,12 +97,12 @@ Create one vault folder per research run, normally `Research/<YYYY-MM-DD> <Topic
|
|||||||
2. **Frame the goal.** Ask one question at a time until you can restate the topic, learning objective, scope, and success criteria. Discover the user's goal, prior knowledge, intended use, depth, constraints, output preference, and trusted/distrusted sources only as needed.
|
2. **Frame the goal.** Ask one question at a time until you can restate the topic, learning objective, scope, and success criteria. Discover the user's goal, prior knowledge, intended use, depth, constraints, output preference, and trusted/distrusted sources only as needed.
|
||||||
3. **Create or defer the folder.** Create the packet folder once the topic has a stable working title; if still vague, keep conversing. Before writing durable notes, every artifact must have a path inside the packet folder.
|
3. **Create or defer the folder.** Create the packet folder once the topic has a stable working title; if still vague, keep conversing. Before writing durable notes, every artifact must have a path inside the packet folder.
|
||||||
4. **Map context.** Search the vault for the topic, synonyms, projects, and neighboring concepts. Read enough relevant notes to make meaningful links, not keyword matches.
|
4. **Map context.** Search the vault for the topic, synonyms, projects, and neighboring concepts. Read enough relevant notes to make meaningful links, not keyword matches.
|
||||||
5. **Ingest sources into claims.** For each source, preserve its path in `Sources.md`, extract evidence-backed claims into `Claims.md`, update affected concept/summary pages, and append an event to `Log.md`. Completion: every non-trivial factual claim has source, evidence, confidence, and at least one related link or explicit note that no link exists yet.
|
5. **Ingest sources into claims.** For each source, preserve its path in `Sources.md`, extract evidence-backed claims into `Claims.md`, update affected concept/summary pages, and append an event to `log.md`. Completion: every non-trivial factual claim has source, evidence, confidence, and at least one related link or explicit note that no link exists yet.
|
||||||
6. **Research and teach.** Answer the user directly from the compiled packet when possible, explain findings in small chunks, cite claims, preserve uncertainty, share resources when they improve learning or evidence quality, and correct misunderstandings before moving on.
|
6. **Research and teach.** Answer the user directly from the compiled packet when possible, explain findings in small chunks, cite claims, preserve uncertainty, share resources when they improve learning or evidence quality, and correct misunderstandings before moving on.
|
||||||
7. **File good answers.** When a query produces a durable synthesis, decision, comparison, or explanation, add it back into `Synthesis.md`, `Claims.md`, or an atomic note instead of leaving it only in chat. Append the query and filed destination to `Log.md`.
|
7. **File good answers.** When a query produces a durable synthesis, decision, comparison, or explanation, add it back into `Synthesis.md`, `Claims.md`, or an atomic note instead of leaving it only in chat. Append the query and filed destination to `log.md`.
|
||||||
8. **Check understanding and completeness.** Ask one diagnostic or completeness question per turn. Answer follow-ups with evidence, examples, counterarguments, implementation details, or resources as needed. Capture unresolved gaps in `Questions.md`.
|
8. **Check understanding and completeness.** Ask one diagnostic or completeness question per turn. Answer follow-ups with evidence, examples, counterarguments, implementation details, or resources as needed. Capture unresolved gaps in `Questions.md`.
|
||||||
9. **Write incrementally.** For long sessions, update `Conversation.md`, `Questions.md`, `Glossaries.md`, `Sources.md`, and `Log.md` as the work progresses. By the end, no research, teaching insight, user question, glossary term, source, or durable knowledge should exist only in chat.
|
9. **Write incrementally.** For long sessions, update `Conversation.md`, `Questions.md`, `Glossaries.md`, `Sources.md`, and `log.md` as the work progresses. By the end, no research, teaching insight, user question, glossary term, source, or durable knowledge should exist only in chat.
|
||||||
10. **Lint the packet.** Before final report or promotion, check for unsupported claims, stale or contradictory claims, missing source links, orphan atomic notes, glossary terms without links, unanswered questions, and missing backlinks from `Index.md`. Record notable lint findings or fixes in `Log.md`.
|
10. **Lint the packet.** Before final report or promotion, check for unsupported claims, stale or contradictory claims, missing source links, orphan atomic notes, glossary terms without links, unanswered questions, and missing backlinks from `index.md`. Record notable lint findings or fixes in `log.md`.
|
||||||
11. **Promote reusable notes.** If a packet note becomes broadly useful beyond the run, propose promotion to the main vault, copy or move only with user approval, leave a provenance link in the packet, and update relevant indexes/MOCs.
|
11. **Promote reusable notes.** If a packet note becomes broadly useful beyond the run, propose promotion to the main vault, copy or move only with user approval, leave a provenance link in the packet, and update relevant indexes/MOCs.
|
||||||
12. **Report.** Summarize the packet path, files changed, links or backlink suggestions, top takeaways, what the user now understands, remaining questions, lint status, and promotion candidates.
|
12. **Report.** Summarize the packet path, files changed, links or backlink suggestions, top takeaways, what the user now understands, remaining questions, lint status, and promotion candidates.
|
||||||
|
|
||||||
@@ -35,7 +114,7 @@ Create one vault folder per research run, normally `Research/<YYYY-MM-DD> <Topic
|
|||||||
- Use a teach-back loop for complex topics: plain-language explanation → concrete example → one check question or restatement prompt → correction → continue only when the user is satisfied or uncertainty is captured.
|
- Use a teach-back loop for complex topics: plain-language explanation → concrete example → one check question or restatement prompt → correction → continue only when the user is satisfied or uncertainty is captured.
|
||||||
- Ask checks before research, after initial framing, after major findings, and before finalizing notes.
|
- Ask checks before research, after initial framing, after major findings, and before finalizing notes.
|
||||||
- When the user corrects you, record the correction in `Conversation.md`, update affected packet notes, and prefer the corrected framing unless later evidence contradicts it.
|
- When the user corrects you, record the correction in `Conversation.md`, update affected packet notes, and prefer the corrected framing unless later evidence contradicts it.
|
||||||
- Record important answers and useful resources in `Conversation.md`; reflect scope-changing details in `Index.md`, `Sources.md`, or `Synthesis.md`.
|
- Record important answers and useful resources in `Conversation.md`; reflect scope-changing details in `index.md`, `Sources.md`, or `Synthesis.md`.
|
||||||
|
|
||||||
## Capture Rules
|
## Capture Rules
|
||||||
|
|
||||||
@@ -49,18 +128,20 @@ Create one vault folder per research run, normally `Research/<YYYY-MM-DD> <Topic
|
|||||||
- Keep citations close to claims; mark confidence when evidence is incomplete or contested.
|
- Keep citations close to claims; mark confidence when evidence is incomplete or contested.
|
||||||
- Preserve useful quotes verbatim with attribution.
|
- Preserve useful quotes verbatim with attribution.
|
||||||
- Distinguish source claims from interpretation; record failed searches or missing evidence when they affect the conclusion.
|
- Distinguish source claims from interpretation; record failed searches or missing evidence when they affect the conclusion.
|
||||||
- Create atomic notes in the packet folder for durable concepts; link each from `Index.md` and back to its source or synthesis section.
|
- Create atomic notes in the packet folder for durable concepts; link each from `index.md` and back to its source or synthesis section.
|
||||||
|
|
||||||
## Retrieval Checks
|
## Retrieval Checks
|
||||||
|
|
||||||
Run these checks before treating the packet as complete:
|
Run these checks before treating the packet as complete:
|
||||||
|
|
||||||
- Could a future agent answer the user's main question from `Index.md`, `Synthesis.md`, and `Claims.md` without rereading raw sources?
|
- Could a future agent answer the user's main question from `index.md`, `Synthesis.md`, and `Claims.md` without rereading raw sources?
|
||||||
- Does `Index.md` point to every durable packet page with enough context to choose the right page?
|
- Does `index.md` point to every durable packet page with enough context to choose the right page?
|
||||||
- Are important terms findable in `Glossaries.md` and linked from the pages that use them?
|
- Are important terms findable in `Glossaries.md` and linked from the pages that use them?
|
||||||
- Are contradictions, uncertainty, and missing evidence visible near the relevant claims?
|
- Are contradictions, uncertainty, and missing evidence visible near the relevant claims?
|
||||||
- Does `Log.md` show the ingest/query/lint history well enough to reconstruct what changed and why?
|
- Does `log.md` show the ingest/query/lint history well enough to reconstruct what changed and why?
|
||||||
|
|
||||||
## Linking Rules
|
## Linking Rules
|
||||||
|
|
||||||
Use Obsidian wikilinks: `[[Note Title]]` or `[[path/to/Note|alias]]`. Add links only when they explain context: broader concepts, projects, areas, MOCs, sources, authors, methods, tools, domains, supporting/refining/contradicting claims, or active problems. Do not link on shared words alone.
|
Use standard markdown links: `[text](relative/path.md)`. Do NOT use `[[wikilinks]]`. Add links only when they explain context: broader concepts, projects, areas, MOCs, sources, authors, methods, tools, domains, supporting/refining/contradicting claims, or active problems. Do not link on shared words alone.
|
||||||
|
|
||||||
|
Use bundle-relative links for intra-packet references (`[Sources](Sources.md)`) and vault-relative or absolute paths for cross-bundle references.
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "Research Vault"
|
||||||
|
short_description: "Store and retrieve research notes and documents"
|
||||||
|
policy:
|
||||||
|
allow_implicit_invocation: false
|
||||||
@@ -1,10 +1,27 @@
|
|||||||
# Research Packet Template
|
# Research Packet Template (OKF v0.1)
|
||||||
|
|
||||||
Use inside `Research/<YYYY-MM-DD> <Topic>/` unless the vault has a clearer convention.
|
Use inside `Research/<YYYY-MM-DD> <Topic>/` unless the vault has a clearer convention.
|
||||||
|
|
||||||
## Required files
|
The packet is an OKF bundle. See [vault-conventions.md](../../pkm-curation/references/vault-conventions.md) for the full frontmatter schema and type vocabulary.
|
||||||
|
|
||||||
### `Index.md`
|
## Bundle structure
|
||||||
|
|
||||||
|
```
|
||||||
|
Research/<YYYY-MM-DD> <Topic>/
|
||||||
|
├── index.md # OKF progressive-disclosure, no frontmatter
|
||||||
|
├── log.md # OKF chronological, datestamped entries (newest first)
|
||||||
|
├── Sources.md # Research Source List
|
||||||
|
├── Synthesis.md # Research Synthesis
|
||||||
|
├── Claims.md # Research Claim Index
|
||||||
|
├── Questions.md # Research Questions
|
||||||
|
├── Conversation.md # Conversation Report
|
||||||
|
├── Glossaries.md # Research Glossary
|
||||||
|
└── <atomic-note>.md # Concept
|
||||||
|
```
|
||||||
|
|
||||||
|
### `index.md`
|
||||||
|
|
||||||
|
No frontmatter. Progressive-disclosure listing:
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
# <Topic>
|
# <Topic>
|
||||||
@@ -18,19 +35,19 @@ Use inside `Research/<YYYY-MM-DD> <Topic>/` unless the vault has a clearer conve
|
|||||||
-
|
-
|
||||||
|
|
||||||
## Packet Map
|
## Packet Map
|
||||||
- [[Sources]]
|
- [Sources](Sources.md) — compiled source list
|
||||||
- [[Synthesis]]
|
- [Synthesis](Synthesis.md) — synthesized answer
|
||||||
- [[Claims]]
|
- [Claims](Claims.md) — evidence-backed claims
|
||||||
- [[Questions]]
|
- [Questions](Questions.md) — open and resolved questions
|
||||||
- [[Conversation]]
|
- [Conversation](Conversation.md) — raw conversation record
|
||||||
- [[Glossaries]]
|
- [Glossaries](Glossaries.md) — term definitions
|
||||||
- [[Log]]
|
- [Log](Log.md) — session log
|
||||||
|
|
||||||
## Related Vault Notes
|
## Related Vault Notes
|
||||||
-
|
-
|
||||||
|
|
||||||
## Compiled Pages
|
## Compiled Pages
|
||||||
| Page | Type | One-line purpose | Source basis | Status |
|
| Page | Type | Purpose | Source basis | Status |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| | source summary \| concept \| claim cluster \| synthesis \| promoted | | | draft \| reviewed \| promoted |
|
| | source summary \| concept \| claim cluster \| synthesis \| promoted | | | draft \| reviewed \| promoted |
|
||||||
|
|
||||||
@@ -38,9 +55,33 @@ Use inside `Research/<YYYY-MM-DD> <Topic>/` unless the vault has a clearer conve
|
|||||||
-
|
-
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `log.md`
|
||||||
|
|
||||||
|
Datestamped entries, newest first. No frontmatter required (concept notes in the bundle DO need frontmatter, but log.md follows OKF §7 format).
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Log
|
||||||
|
|
||||||
|
## YYYY-MM-DD
|
||||||
|
* **Event**: Description of what changed.
|
||||||
|
* **Files changed**: ...
|
||||||
|
```
|
||||||
|
|
||||||
### `Sources.md`
|
### `Sources.md`
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
|
---
|
||||||
|
type: Research Source List
|
||||||
|
title: <Topic> - Sources
|
||||||
|
description: Compiled source list for <Topic>.
|
||||||
|
tags: [research, sources]
|
||||||
|
timestamp: <ISO 8601>
|
||||||
|
id: <topic-slug>-sources
|
||||||
|
aliases: []
|
||||||
|
area: Research
|
||||||
|
project: ''
|
||||||
|
---
|
||||||
|
|
||||||
# Sources
|
# Sources
|
||||||
|
|
||||||
| Source | Type | Why useful | Reliability notes | Accessed |
|
| Source | Type | Why useful | Reliability notes | Accessed |
|
||||||
@@ -60,6 +101,18 @@ Include source links, books, papers, docs, videos, examples, search terms, or re
|
|||||||
### `Synthesis.md`
|
### `Synthesis.md`
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
|
---
|
||||||
|
type: Research Synthesis
|
||||||
|
title: <Topic> - Synthesis
|
||||||
|
description: Compiled answer for <Topic> research.
|
||||||
|
tags: [research, synthesis]
|
||||||
|
timestamp: <ISO 8601>
|
||||||
|
id: <topic-slug>-synthesis
|
||||||
|
aliases: []
|
||||||
|
area: Research
|
||||||
|
project: ''
|
||||||
|
---
|
||||||
|
|
||||||
# Synthesis
|
# Synthesis
|
||||||
|
|
||||||
## Short Answer
|
## Short Answer
|
||||||
@@ -79,6 +132,18 @@ Include source links, books, papers, docs, videos, examples, search terms, or re
|
|||||||
### `Claims.md`
|
### `Claims.md`
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
|
---
|
||||||
|
type: Research Claim Index
|
||||||
|
title: <Topic> - Claims
|
||||||
|
description: Evidence-backed claims for <Topic> research.
|
||||||
|
tags: [research, claims]
|
||||||
|
timestamp: <ISO 8601>
|
||||||
|
id: <topic-slug>-claims
|
||||||
|
aliases: []
|
||||||
|
area: Research
|
||||||
|
project: ''
|
||||||
|
---
|
||||||
|
|
||||||
# Claims
|
# Claims
|
||||||
|
|
||||||
## Claim: <single answerable claim>
|
## Claim: <single answerable claim>
|
||||||
@@ -96,6 +161,18 @@ Include source links, books, papers, docs, videos, examples, search terms, or re
|
|||||||
### `Questions.md`
|
### `Questions.md`
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
|
---
|
||||||
|
type: Research Questions
|
||||||
|
title: <Topic> - Questions
|
||||||
|
description: Open and resolved questions for <Topic> research.
|
||||||
|
tags: [research, questions]
|
||||||
|
timestamp: <ISO 8601>
|
||||||
|
id: <topic-slug>-questions
|
||||||
|
aliases: []
|
||||||
|
area: Research
|
||||||
|
project: ''
|
||||||
|
---
|
||||||
|
|
||||||
# Questions
|
# Questions
|
||||||
|
|
||||||
## User Questions
|
## User Questions
|
||||||
@@ -114,6 +191,18 @@ Include source links, books, papers, docs, videos, examples, search terms, or re
|
|||||||
### `Conversation.md`
|
### `Conversation.md`
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
|
---
|
||||||
|
type: Conversation Report
|
||||||
|
title: <Topic> - Conversation
|
||||||
|
description: Narrative summary of the research conversation for <Topic>.
|
||||||
|
tags: [research, conversation]
|
||||||
|
timestamp: <ISO 8601>
|
||||||
|
id: <topic-slug>-conversation
|
||||||
|
aliases: []
|
||||||
|
area: Research
|
||||||
|
project: ''
|
||||||
|
---
|
||||||
|
|
||||||
# Conversation
|
# Conversation
|
||||||
|
|
||||||
## Learning Goal
|
## Learning Goal
|
||||||
@@ -132,25 +221,21 @@ Include source links, books, papers, docs, videos, examples, search terms, or re
|
|||||||
|
|
||||||
Record important answers, corrections, and scope decisions as the session progresses.
|
Record important answers, corrections, and scope decisions as the session progresses.
|
||||||
|
|
||||||
### `Log.md`
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
# Log
|
|
||||||
|
|
||||||
Append one entry for each ingest, query filed back into the packet, lint pass, promotion, or major correction.
|
|
||||||
|
|
||||||
## YYYY-MM-DD HH:MM — <ingest | query | lint | promotion | correction>
|
|
||||||
- Input:
|
|
||||||
- Files read:
|
|
||||||
- Files changed:
|
|
||||||
- Claims added or revised:
|
|
||||||
- Links added:
|
|
||||||
- Open issues:
|
|
||||||
```
|
|
||||||
|
|
||||||
### `Glossaries.md`
|
### `Glossaries.md`
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
|
---
|
||||||
|
type: Research Glossary
|
||||||
|
title: <Topic> - Glossary
|
||||||
|
description: Term definitions for <Topic> research.
|
||||||
|
tags: [research, glossary]
|
||||||
|
timestamp: <ISO 8601>
|
||||||
|
id: <topic-slug>-glossary
|
||||||
|
aliases: []
|
||||||
|
area: Research
|
||||||
|
project: ''
|
||||||
|
---
|
||||||
|
|
||||||
# Glossaries
|
# Glossaries
|
||||||
|
|
||||||
## <Term or Acronym>
|
## <Term or Acronym>
|
||||||
@@ -167,12 +252,17 @@ Add every acronym, abbreviation, domain-specific phrase, specialized term, jargo
|
|||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
---
|
---
|
||||||
|
type: Concept
|
||||||
|
title: <Concept Name>
|
||||||
|
description: <One-line summary of the concept>.
|
||||||
|
tags: [research, atomic-note, <topic-tag>]
|
||||||
|
timestamp: <ISO 8601>
|
||||||
id: <stable-id>
|
id: <stable-id>
|
||||||
aliases: []
|
aliases: []
|
||||||
tags: [research, atomic-note]
|
|
||||||
area: <area>
|
area: <area>
|
||||||
project: [[<packet topic>]]
|
project: <packet topic>
|
||||||
---
|
---
|
||||||
|
|
||||||
# <Concept>
|
# <Concept>
|
||||||
|
|
||||||
## Idea
|
## Idea
|
||||||
@@ -182,7 +272,7 @@ project: [[<packet topic>]]
|
|||||||
<Explain retrieval, decision, or learning value.>
|
<Explain retrieval, decision, or learning value.>
|
||||||
|
|
||||||
## Evidence or source
|
## Evidence or source
|
||||||
- Claim: [[Claims#Claim <anchor or short title>]]
|
- Claim: [Claims](Claims.md)
|
||||||
- Source:
|
- Source:
|
||||||
|
|
||||||
## Links
|
## Links
|
||||||
@@ -191,5 +281,9 @@ project: [[<packet topic>]]
|
|||||||
- Contrasts:
|
- Contrasts:
|
||||||
|
|
||||||
## Promotion status
|
## Promotion status
|
||||||
- Packet-local | candidate for main vault | promoted to [[path/to/promoted note]]
|
- Packet-local | candidate for main vault | promoted to [path/to/promoted note](path/to/promoted.md)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Linking
|
||||||
|
|
||||||
|
Use standard markdown links `[text](relative/path.md)` throughout. Do NOT use `[[wikilinks]]`. Intra-packet links are bundle-relative (e.g., `[Claims](Claims.md)`). Cross-bundle links use vault-relative paths.
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
name: youtube-video-capture
|
||||||
|
description: Fetch subtitles from a YouTube video, summarize the content, and save both the summary and raw subtitles to the Video bundle in the Obsidian vault. Use when the user wants to capture a YouTube video, mentions "summarize this video", "capture this talk", or pastes a YouTube URL wanting it saved to vault.
|
||||||
|
disable-model-invocation: true
|
||||||
|
---
|
||||||
|
|
||||||
|
Capture a YouTube video into the Obsidian vault. Extract subtitles, produce a summary, and save both to the `Video/` bundle as OKF-conformant notes.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
`yt-dlp` must be installed. Check with `which yt-dlp`. If missing, tell the user to install it (`pip install yt-dlp` or `brew install yt-dlp`) and stop.
|
||||||
|
|
||||||
|
## Extract subtitles
|
||||||
|
|
||||||
|
Run `yt-dlp` to download auto-generated English subtitles as SRT:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yt-dlp --write-auto-subs --sub-lang en-orig --convert-subs srt --skip-download -o "/tmp/yt-subs-%(id)s.%(ext)s" "<url>"
|
||||||
|
```
|
||||||
|
|
||||||
|
If no subtitles are available, tell the user and stop.
|
||||||
|
|
||||||
|
Read the SRT file. Strip timestamps and sequence numbers to produce clean, contiguous text for summarization.
|
||||||
|
|
||||||
|
## Summarize
|
||||||
|
|
||||||
|
Produce a prose summary from the full subtitle text. Synthesize the content — do not regurgitate the transcript. Then extract **key takeaways** as a bulleted list.
|
||||||
|
|
||||||
|
## Write to vault
|
||||||
|
|
||||||
|
The vault root is `/home/sjb/Documents/sjb-brain/`. The `Video/` bundle is an OKF bundle. Create the directory if it doesn't exist, with an `index.md` and `log.md` following the pattern of other bundles in the vault.
|
||||||
|
|
||||||
|
### Filenames
|
||||||
|
|
||||||
|
- Summary: `YYYY-MM-DD_HH-mm_<kebab-case-title>.md`
|
||||||
|
- Subtitle: `YYYY-MM-DD_HH-mm_<kebab-case-title>.srt`
|
||||||
|
|
||||||
|
Use the video title, kebab-cased.
|
||||||
|
|
||||||
|
### Summary note
|
||||||
|
|
||||||
|
Write a markdown note with OKF v0.1 frontmatter:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
id: <kebab-case-title>
|
||||||
|
type: Video Summary
|
||||||
|
aliases: []
|
||||||
|
tags: [video, youtube, <content-derived-tags>]
|
||||||
|
area: <derived-from-content-or-empty>
|
||||||
|
project: ''
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
Body:
|
||||||
|
|
||||||
|
- Video title as the note's `#` heading
|
||||||
|
- Link to the original YouTube URL
|
||||||
|
- The prose summary
|
||||||
|
- `## Key Takeaways` with bulleted list
|
||||||
|
- `## Subtitle` with a markdown link to the `.srt` file
|
||||||
|
|
||||||
|
Use standard markdown links, never `[[wikilinks]]`.
|
||||||
|
|
||||||
|
### Subtitle file
|
||||||
|
|
||||||
|
Copy the SRT file into `Video/` alongside the summary. No frontmatter.
|
||||||
|
|
||||||
|
### Bundle maintenance
|
||||||
|
|
||||||
|
After writing the pair:
|
||||||
|
|
||||||
|
1. Update `Video/index.md` — add the new summary under a `## Flat Notes` or appropriate heading, with a one-line description.
|
||||||
|
2. Append an entry to `Video/log.md` noting the addition with a timestamp.
|
||||||
|
|
||||||
|
### Safety
|
||||||
|
|
||||||
|
- If a filename collides, append a numeric suffix.
|
||||||
|
- Redact likely credentials, secrets, or tokens from any inline content.
|
||||||
|
- If the video has no subtitles, stop — do not attempt to transcribe.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "Youtube Video Capture"
|
||||||
|
short_description: "Capture a youtube video"
|
||||||
|
policy:
|
||||||
|
allow_implicit_invocation: false
|
||||||
@@ -1,11 +1,5 @@
|
|||||||
# Productivity Skills
|
# Productivity Skills
|
||||||
|
|
||||||
## User-invoked
|
Daily non-code workflow tools.
|
||||||
|
|
||||||
- [grill-me](grill-me/SKILL.md) — A relentless interview to sharpen a plan or design.
|
_No skills currently live in this bucket._
|
||||||
- [handoff](handoff/SKILL.md) — Compact the current conversation into a handoff document for another agent to pick up.
|
|
||||||
- [writing-great-skills](writing-great-skills/SKILL.md) — Reference for writing and editing skills well — the vocabulary and principles that make a skill predictable.
|
|
||||||
|
|
||||||
## Model-invoked
|
|
||||||
|
|
||||||
- [grilling](grilling/SKILL.md) — Interview the user relentlessly about a plan or design.
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
name: grill-me
|
|
||||||
description: A relentless interview to sharpen a plan or design.
|
|
||||||
disable-model-invocation: true
|
|
||||||
---
|
|
||||||
|
|
||||||
Run a `/grilling` session.
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
---
|
|
||||||
name: grilling
|
|
||||||
description: Interview the user relentlessly about a plan or design. Use when the user wants to stress-test a plan before building, or uses any 'grill' trigger phrases.
|
|
||||||
---
|
|
||||||
|
|
||||||
Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.
|
|
||||||
|
|
||||||
Ask the questions one at a time, waiting for feedback on each question before continuing. Asking multiple questions at once is bewildering.
|
|
||||||
|
|
||||||
If a question can be answered by exploring the codebase, explore the codebase instead.
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
name: handoff
|
|
||||||
description: Compact the current conversation into a handoff document for another agent to pick up.
|
|
||||||
argument-hint: "What will the next session be used for?"
|
|
||||||
disable-model-invocation: true
|
|
||||||
---
|
|
||||||
|
|
||||||
Write a handoff document summarising the current conversation so a fresh agent can continue the work. Save to the temporary directory of the user's OS - not the current workspace.
|
|
||||||
|
|
||||||
Include a "suggested skills" section in the document, which suggests skills that the agent should invoke.
|
|
||||||
|
|
||||||
Do not duplicate content already captured in other artifacts (PRDs, plans, ADRs, issues, commits, diffs). Reference them by path or URL instead.
|
|
||||||
|
|
||||||
Redact any sensitive information, such as API keys, passwords, or personally identifiable information.
|
|
||||||
|
|
||||||
If the user passed arguments, treat them as a description of what the next session will focus on and tailor the doc accordingly.
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
---
|
|
||||||
name: writing-great-skills
|
|
||||||
description: Reference for writing and editing skills well — the vocabulary and principles that make a skill predictable.
|
|
||||||
disable-model-invocation: true
|
|
||||||
---
|
|
||||||
|
|
||||||
A skill exists to wrangle determinism out of a stochastic system. **Predictability** — the agent taking the same _process_ every run, not producing the same output — is the root virtue; every lever below serves it.
|
|
||||||
|
|
||||||
**Bold terms** are defined in [`GLOSSARY.md`](GLOSSARY.md); look them up there for the full meaning.
|
|
||||||
|
|
||||||
## Invocation
|
|
||||||
|
|
||||||
Two choices, trading different costs:
|
|
||||||
|
|
||||||
- A **model-invoked** skill keeps a **description**, so the agent can fire it autonomously _and_ other skills can reach it (you can still type its name too). It contributes to **context load** — the description sits in the window every turn. Mechanics: omit `disable-model-invocation`, and write a model-facing description with rich trigger phrasing ("Use when the user wants…, mentions…").
|
|
||||||
- A **user-invoked** skill strips the description from the agent's reach: only you, typing its name, can invoke it — and no other skill can. Zero context load, but it spends **cognitive load**: _you_ are the index that must remember it exists. Mechanics: set `disable-model-invocation: true`; the `description` becomes human-facing — a one-line summary, trigger lists stripped.
|
|
||||||
|
|
||||||
Pick model-invocation only when the agent must reach the skill on its own, or another skill must. If it only ever fires by hand, make it user-invoked and pay no context load.
|
|
||||||
|
|
||||||
When user-invoked skills multiply past what you can remember, that piled-up cognitive load is cured by a **router skill**: one user-invoked skill that names the others and when to reach for each.
|
|
||||||
|
|
||||||
## Writing the description
|
|
||||||
|
|
||||||
A model-invoked **description** does two jobs — state what the skill is, and list the **branches** that should trigger it. Every word increases **context load**, so a description earns even harder pruning than the body:
|
|
||||||
|
|
||||||
- **Front-load the skill's leading word** — the description is where it does its invocation work.
|
|
||||||
- **One trigger per branch.** Synonyms that rename a single branch are **duplication** — "build features using TDD … asks for test-first development" is one branch written twice. Collapse them; keep only genuinely distinct branches.
|
|
||||||
- **Cut identity that's already in the body.** Keep the description to triggers, plus any "when another skill needs…" reach clause.
|
|
||||||
|
|
||||||
## Information hierarchy
|
|
||||||
|
|
||||||
A skill is built from two content types — **steps** and **reference** — that mix freely: a skill can be all steps, all reference, or both. The core decision is which to use and where each sits on the **information hierarchy**, a ladder ranked by how immediately the agent needs the material:
|
|
||||||
|
|
||||||
1. **In-skill step** — an ordered action in `SKILL.md`, the primary tier: what the agent does, in order. Each step ends on a **completion criterion**, the condition that tells the agent the work is done. Make it _checkable_ (can the agent tell done from not-done?) and, where it matters, _exhaustive_ ("every modified model accounted for", not "produce a change list") — a vague criterion invites **premature completion**.
|
|
||||||
2. **In-skill reference** — a definition, rule, or fact in `SKILL.md`, consulted on demand. Often a legitimately flat peer-set (every rule of a review on one rung) — a fine arrangement, not a smell. _This skill is all reference._
|
|
||||||
3. **External reference** — reference pushed out of `SKILL.md` into a separate file, reached by a **context pointer**, loaded only when the pointer fires. (Spans _disclosed_ reference — a sibling file like `GLOSSARY.md`, still part of the skill — through fully **external reference** that lives outside the skill system and any skill can point at.)
|
|
||||||
|
|
||||||
A demanding completion criterion drives thorough **legwork** — the digging the agent does within the work — whether the skill has steps or not, since "every rule applied" binds flat reference just as "every step done" binds a sequence.
|
|
||||||
|
|
||||||
Push too little down and the top bloats; push too much and you hide material the agent actually needs. That tension is the whole decision.
|
|
||||||
|
|
||||||
**Progressive disclosure** is the move down the ladder — out of `SKILL.md` into a linked file — so the top stays legible. Mechanics: a linked `.md` file in the skill folder, named for what it holds (this skill discloses its full definitions to `GLOSSARY.md`). Some skills are used in more than one way, and each distinct way is a **branch** — different runs taking different paths through the skill. Branching is the cleanest disclosure test: inline what every branch needs, and push behind a pointer what only some branches reach. A **context pointer**'s _wording_, not its target, decides when and how reliably the agent reaches the material.
|
|
||||||
|
|
||||||
Where the ladder decides _how far down_ a piece sits, **co-location** decides _what sits beside it_ once there: keep a concept's definition, rules, and caveats under one heading rather than scattered, so reading one part brings its neighbours with it.
|
|
||||||
|
|
||||||
## When to split
|
|
||||||
|
|
||||||
**Granularity** is how finely you divide skills, and each cut spends one of the two loads, so split only when the cut earns it. Two cuts:
|
|
||||||
|
|
||||||
- **By invocation** — split off a **model-invoked** skill when you have a distinct **leading word** that should trigger it on its own, or another skill must reach it. You pay **context load** for the new always-loaded **description**, so that independent reach has to be worth it.
|
|
||||||
- **By sequence** — split a run of **steps** when the steps still ahead (a step's **post-completion steps**) tempt the agent to rush the one in front of it (**premature completion**). Keeping them out of view encourages the agent to do more **legwork** on the current task.
|
|
||||||
|
|
||||||
## Pruning
|
|
||||||
|
|
||||||
Keep each meaning in a **single source of truth**: one authoritative place, so changing the behaviour is a one-place edit.
|
|
||||||
|
|
||||||
Check every line for **relevance**: does it still bear on what the skill does?
|
|
||||||
|
|
||||||
Then hunt **no-ops** sentence by sentence, not just line by line: run the no-op test on each sentence in isolation, and when one fails, delete the whole sentence rather than trim words from it. Be aggressive — most prose that fails should go, not be rewritten.
|
|
||||||
|
|
||||||
## Leading words
|
|
||||||
|
|
||||||
A **leading word** is a compact concept already living in the model's pretraining that the agent thinks with while running the skill (e.g. _lesson_, _fog of war_, _tracer bullets_). Repeated throughout the text (though not necessarily - a strong leading word might only be needed once), it accumulates a distributed definition and anchors a whole region of behaviour in the fewest tokens, by recruiting priors the model already holds.
|
|
||||||
|
|
||||||
It serves predictability twice. In the body it anchors _execution_: the agent reaches for the same behaviour every time the word appears. In the description it anchors _invocation_: when the same word lives in your prompts, docs, and code, the agent links that shared language to the skill and fires it more reliably.
|
|
||||||
|
|
||||||
Hunt for opportunities to refactor skills to use leading words. A triad spelled out at three sites (**duplication**), a description spending a sentence to gesture at one idea — each is a passage begging to **collapse** into a single token. Examples include:
|
|
||||||
|
|
||||||
- "fast, deterministic, low-overhead" -> _tight_ — one quality restated across a phase — into a single pretrained word (a _tight_ loop).
|
|
||||||
- "a loop you believe in" -> _red_ — converts a fuzzy gate into a binary observable state (the loop goes _red_ on the bug, or it doesn't).
|
|
||||||
|
|
||||||
You win twice over: fewer tokens, _and_ a sharper hook for the agent to hang its thinking on. Assume every skill is carrying restatements that leading words retire — go find them.
|
|
||||||
|
|
||||||
## Failure modes
|
|
||||||
|
|
||||||
Use these to diagnose issues the user may be having with the skill.
|
|
||||||
|
|
||||||
- **Premature completion** — ending a step before it's genuinely done, attention slipping to _being done_. Defence, in order: sharpen the completion criterion first (cheap, local); only if it is irreducibly fuzzy _and_ you observe the rush, hide the post-completion steps by splitting (the sequence cut).
|
|
||||||
- **Duplication** — the same meaning in more than one place. Costs maintenance and tokens, and inflates a meaning's prominence on the ladder past its real rank.
|
|
||||||
- **Sediment** — stale layers that settle because adding feels safe and removing feels risky. The default fate of any skill without a pruning discipline.
|
|
||||||
- **Sprawl** — a skill simply too long, even when every line is live and unique. Hurts readability and maintainability and wastes tokens. The cure is the ladder: disclose **reference** behind pointers, and split by **branch** or sequence so each path carries only what it needs.
|
|
||||||
- **No-op** — a line the model already obeys by default, so you pay load to say nothing. The test: does it change behaviour versus the default? A weak leading word (_be thorough_ when the agent is already thorough-ish) is a no-op; the fix is a stronger word (_relentless_), not a different technique.
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
# Glossary — Building Great Skills
|
|
||||||
|
|
||||||
The domain model for what makes a skill great. A skill exists to wrangle determinism out of a stochastic system; every term below is a lever on that goal. This is the disclosed reference for [`writing-great-skills`](SKILL.md).
|
|
||||||
|
|
||||||
**Bold terms** in any definition are themselves defined in this glossary; find them by their heading.
|
|
||||||
|
|
||||||
## Language
|
|
||||||
|
|
||||||
### Predictability
|
|
||||||
|
|
||||||
The degree to which a skill makes the agent behave the same *way* on every run — the same process, not the same output (a brainstorming skill should *predictably* diverge; its tokens vary, its behaviour doesn't). The root virtue every other term serves — cost and maintainability are symptoms of it, not rivals.
|
|
||||||
|
|
||||||
_Avoid_: consistency, reliability, robustness, output-determinism
|
|
||||||
|
|
||||||
### Model-Invoked
|
|
||||||
|
|
||||||
A skill that keeps its **description** field, so the agent can see it and fire it autonomously — and the human can still type its name, so model-invocation always *includes* user reach. There is no model-only state: a description only ever *adds* agent discovery, never removes the human's. Pays a permanent **context load** on every turn in exchange for that discoverability. Reachable by other skills, because the description that makes it agent-discoverable makes it invocable. A model-invoked skill whose content is all **reference** is also one home for shared reference: another skill can invoke it, so reference needed by several skills lives in one place. Pick model-invocation only when the agent must reach the skill on its own; if it never fires except by hand, drop the description and pay no context load.
|
|
||||||
|
|
||||||
_Avoid_: ability, tool, capability
|
|
||||||
|
|
||||||
### User-Invoked
|
|
||||||
|
|
||||||
A skill with its **description** stripped — invisible to the agent and reachable only by the human typing its name (user-*only*, where **model-invoked** is user-*and-agent*). Trades agent-discoverability for zero **context load**. Because it has no description, nothing but the human can reach it: no other skill can fire it.
|
|
||||||
|
|
||||||
_Avoid_: procedure, workflow, command
|
|
||||||
|
|
||||||
### Description
|
|
||||||
|
|
||||||
The skill's machine-readable trigger, and the one **context pointer** a **model-invoked** skill is forced to keep loaded at all times. Its mere presence *is* the invocation axis: keep it and the skill is model-invoked (and reachable by other skills); delete it and the skill is **user-invoked**, reachable only by the human. The source of a model-invoked skill's **context load**.
|
|
||||||
|
|
||||||
_Avoid_: frontmatter, summary
|
|
||||||
|
|
||||||
### Context Pointer
|
|
||||||
|
|
||||||
A reference held in the agent's context that names some out-of-context material and encodes the condition for reaching it. The **description** is the top-level context pointer (context window → skill); pointers to disclosed files are the same object one level down. Its wording, not the target, decides *when* the agent reaches — and *how reliably*. A must-have target behind a weakly worded pointer is a variance bug: fix the wording first, and inline the material only if sharpening fails.
|
|
||||||
|
|
||||||
_Avoid_: link, reference, import
|
|
||||||
|
|
||||||
### Context Load
|
|
||||||
|
|
||||||
The cost a **model-invoked** skill imposes on the agent's context window — its **description**, always loaded, spending both tokens and attention. What **user-invoked** skills escape by having no description, and the brake on splitting into more model-invoked skills.
|
|
||||||
|
|
||||||
_Avoid_: token cost, context bloat
|
|
||||||
|
|
||||||
### Cognitive Load
|
|
||||||
|
|
||||||
The cost a **user-invoked** skill imposes on the human — what they must hold in their head: which skills exist and when to reach for each (the human is the index). What **model-invocation** removes by being agent-discoverable, and the brake on splitting into more user-invoked skills. Not a cost to minimise: it is the price of human agency, the reason some skills stay user-invoked. Spend it where human judgement matters; remove it where it does not.
|
|
||||||
|
|
||||||
_Avoid_: human index, burden, overhead
|
|
||||||
|
|
||||||
### Granularity
|
|
||||||
|
|
||||||
How finely you divide skills. Finer division spends one of the two loads: more **model-invoked** skills spend **context load** (more descriptions crowding the window and competing for attention); more **user-invoked** skills spend **cognitive load** (more for the human to remember and reach for). Two cuts guide the division. By **invocation**, split off a model-invoked skill where you have a distinct **leading word** to trigger it — a trigger word you actually use in your prompts. By **sequence**, split a run of **steps** where a step's **post-completion steps** need hiding, since isolating it in its own context clears what follows. Beware the reverse: merging sequences exposes each step's post-completion steps to what follows, inviting premature completion.
|
|
||||||
|
|
||||||
_Avoid_: chunking, modularity
|
|
||||||
|
|
||||||
### Router Skill
|
|
||||||
|
|
||||||
A **user-invoked** skill whose job is to point at your other user-invoked skills — naming each and when to reach for it — so the human has one skill to remember instead of many. It can only hint, never fire them: user-invoked skills have no **description**, so nothing but the human can reach them. The cure for **cognitive load** when user-invoked skills multiply.
|
|
||||||
|
|
||||||
_Avoid_: dispatcher, menu, registry, index, router procedure
|
|
||||||
|
|
||||||
### Information Hierarchy
|
|
||||||
|
|
||||||
A skill's content ranked by how immediately the agent needs it — a single ladder, produced by two cuts: in-file or behind a pointer, and step or reference. The rungs:
|
|
||||||
|
|
||||||
- **Steps** — in-file, primary
|
|
||||||
- **Reference**, in-file — secondary
|
|
||||||
- **Reference**, disclosed — behind a **context pointer**
|
|
||||||
|
|
||||||
A skill with no **steps** uses just the bottom two rungs — often a legitimately flat peer-set (e.g. every rule of a review on one rung), which is a fine arrangement, not a smell. The hierarchy is independent of invocation: a skill can be model- or user-invoked whether it is all steps, all reference, or both. When a skill has steps, in-file reference that should be disclosed buries them and turns attending to them into a coin-flip — a variance lever, not just a legibility one. Keep the top of the ladder legible; push down it whatever you can.
|
|
||||||
|
|
||||||
_Avoid_: structure, organization, layout
|
|
||||||
|
|
||||||
### Co-location
|
|
||||||
|
|
||||||
Keeping the material an agent needs at once in one place — a concept's definition, rules, and caveats under a single heading, not scattered across the file — so reading one part brings its neighbours with it. The within-file companion to the **Information Hierarchy**: the hierarchy ranks *how far down* a piece sits; co-location decides *what sits beside it* once there. There is no formula for the right format of a body of **reference**; the test is that a skill should read like documentation written for the agent, and grouped material reads that way where scattered material does not. Distinct from **Duplication**: that repeats one meaning in two places, where scattering fragments a single meaning across many.
|
|
||||||
|
|
||||||
_Avoid_: grouping, clustering, cohesion
|
|
||||||
|
|
||||||
### Branch
|
|
||||||
|
|
||||||
A distinct way a skill can be invoked — a case the skill handles — so different runs take different paths through it. A skill with many steps may carry many branches; a linear one has none.
|
|
||||||
|
|
||||||
_Avoid_: path, case, fork
|
|
||||||
|
|
||||||
### Progressive Disclosure
|
|
||||||
|
|
||||||
Moving **reference** down the ladder — out of SKILL.md and behind a **context pointer** — so the top stays legible. Not primarily a token optimisation; it is how the **information hierarchy** is protected. Licensed by **branching**: disclose what only some branches need, inline what every path needs, and if a pointer fires unreliably on must-have material, sharpen its wording, and pull it back inline only if that fails.
|
|
||||||
|
|
||||||
_Avoid_: lazy loading, chunking
|
|
||||||
|
|
||||||
### Steps
|
|
||||||
|
|
||||||
The ordered actions the agent performs — when a skill has them, the primary tier of its content, and the part that earns its place in SKILL.md. Not every skill has steps: a skill can be all steps (`tdd`), all **reference** (a review), or both, independent of invocation. Every step ends on a **completion criterion**, clear or vague.
|
|
||||||
|
|
||||||
_Avoid_: workflow, instructions, choreography
|
|
||||||
|
|
||||||
### Completion Criterion
|
|
||||||
|
|
||||||
The condition that tells the agent a unit of work is done — the target it judges against. Two properties make it a lever, not just a quality. Its **clarity** (can the agent tell done from not-done?) resists **premature completion** — a vague bound ("understanding reached") lets the agent declare done and slip to the next step; this axis needs *steps* to bite, since premature completion is a between-steps failure. Its **demand** (how much it requires) sets **legwork** — "every modified model accounted for" forces thorough work where "produce a change list" does not — and this axis is *not* step-bound: it can bind a body of flat reference too, which is how a skill with no steps still carries an exhaustiveness bar ("every rule applied"). The strongest criteria are both checkable and exhaustive.
|
|
||||||
|
|
||||||
_Avoid_: done condition, exit condition, stopping rule
|
|
||||||
|
|
||||||
### Post-Completion Steps
|
|
||||||
|
|
||||||
The **steps** that follow the current step. Visible, they pull the agent forward into **premature completion** — the more it sees, the stronger the tug; the defence is to hide them by splitting the sequence of steps into two.
|
|
||||||
|
|
||||||
_Avoid_: horizon, fog of war, lookahead
|
|
||||||
|
|
||||||
### Legwork
|
|
||||||
|
|
||||||
The work an agent does behind the scenes within a single step — reading files, exploring the codebase, making changes, digging up what it needs rather than offloading to the user. It lives below the step structure: never written as its own step, latent in the wording, controlled by the agent rather than the skill. The within-step counterpart to **post-completion steps**' across-step pull. Raised by a **leading word** (_comprehensive_, _thorough_) or a **completion criterion** that demands the work be exhaustive — including the demand axis applied to flat reference, which is what drives a skill of flat reference to cover all its rungs. Goes thin either when that demand is missing or when **premature completion** cuts the step short.
|
|
||||||
|
|
||||||
_Avoid_: scope, effort, diligence, coverage
|
|
||||||
|
|
||||||
### Reference
|
|
||||||
|
|
||||||
Material the agent refers to on demand — definitions, facts, parameters, examples, conditional instructions. When a skill has **steps** it is secondary to them; when a skill has none it is the entire content; or it lives outside any skill entirely — see **External Reference**. Reached via **context pointers**, and the prime candidate for **progressive disclosure**.
|
|
||||||
|
|
||||||
_Avoid_: supporting material, docs, background
|
|
||||||
|
|
||||||
### External Reference
|
|
||||||
|
|
||||||
**Reference** that lives outside the skill system — a plain file, no **description**, no **steps**, not invocable — that any skill can point at. The home for shared reference that needn't fire on its own, and the only shared home two **user-invoked** skills can use, since neither has a description and so neither can fire the other.
|
|
||||||
|
|
||||||
_Avoid_: doc, resource, knowledge base
|
|
||||||
|
|
||||||
### Leading Word
|
|
||||||
|
|
||||||
A compact concept — also called a *Leitwort* — already living in the model's pretraining, that the agent thinks with while running the skill. It encodes a behavioural principle in the fewest possible tokens by invoking priors the model already holds (e.g. _lesson_, _proximal zone of development_, _fog of war_, _tracer bullets_). Repeated as a token, never as a sentence, it accumulates a distributed definition across the skill and anchors a whole region of behaviour. Coining your own works if you define it clearly, but a made-up word recruits no priors — you pay in definition tokens what a pretrained word gives free. Reach for an existing word first.
|
|
||||||
|
|
||||||
A leading word serves **predictability** twice. In the body it anchors **execution** — the agent reaches for the same behaviour every time the concept appears, and inside flat reference it focuses attention on a class of thing to look for, recruiting the right checks each run. In the **description** it anchors **invocation** — and not only within the skill: when the same word lives in your prompts, your docs, and your codebase, the agent links that shared language to the skill and fires it more reliably. Word a description with the leading words you actually use when you want the skill.
|
|
||||||
|
|
||||||
_Avoid_: keyword, term, motif
|
|
||||||
|
|
||||||
### Single Source of Truth
|
|
||||||
|
|
||||||
The desired state where each meaning lives in exactly one authoritative place, so a change to the skill's behaviour is a change in one place. **Duplication** is its violation.
|
|
||||||
|
|
||||||
_Avoid_: home, canonical location
|
|
||||||
|
|
||||||
### Relevance
|
|
||||||
|
|
||||||
Whether a line still bears on what the skill does — the lens for what to keep. A line loses relevance either by never bearing on the task (mere exposition, or a **branch** that should be disclosed) or by going stale: drifting out of date as the behaviour or world it describes changes. Shorter skills are easier to keep relevant, because each line is cheaper to check. Distinct from **no-op**: relevance asks whether a line bears on the task, not whether it changes behaviour.
|
|
||||||
|
|
||||||
_Avoid_: load-bearing, staleness, freshness
|
|
||||||
|
|
||||||
## Failure Modes
|
|
||||||
|
|
||||||
### Premature Completion
|
|
||||||
|
|
||||||
Ending the current step before it is genuinely done, because the agent's attention slips to being done rather than to the work. A between-steps failure: it needs **steps** to occur — a skill with no steps that quits early isn't premature completion but thin **legwork** under an unmet demand. A tug-of-war between two forces: visible **post-completion steps** (the pull forward) and the **completion criterion**'s clarity (the resistance — a sharp, checkable bar holds; a vague one gives way). Fuzziness is the necessary condition: a sharp bound resists the pull no matter how many later steps are visible, so a step that never rushes needs no defending. Two levers hold a step that does, but reach for them in order: **sharpen the bound first** — it is local and cheap. Only when the criterion is irreducibly fuzzy *and* you actually observe the rush do you **hide the later steps** — and hiding only works across a real context boundary (a user-invoked hand-off or a subagent dispatch; an inline model-invoked call leaves the later steps in context and clears nothing). One cause of thin legwork, but distinct from it: legwork can be thin even when a step runs to full completion.
|
|
||||||
|
|
||||||
_Avoid_: premature closure, the rush, rushing, shortcutting
|
|
||||||
|
|
||||||
### Duplication
|
|
||||||
|
|
||||||
The same meaning given more than one **single source of truth**. It costs maintenance (change one place, you must change the others), costs tokens, and inflates prominence — repeating a meaning weights it on the ladder past its real rank. The accidental inverse of a **leading word**, which raises attention on purpose by repeating a token, never the meaning.
|
|
||||||
|
|
||||||
_Avoid_: repetition, redundancy
|
|
||||||
|
|
||||||
### Sediment
|
|
||||||
|
|
||||||
Layers of old content that settle in a skill and are never cleared, because adding feels safe and removing feels risky — so stale and irrelevant lines accumulate and you must core down through them to find what is still live. The default fate of any skill without a pruning discipline; the slow erosion of **relevance**, as opposed to **duplication**'s repeated meaning.
|
|
||||||
|
|
||||||
_Avoid_: accretion, bloat, cruft, rot
|
|
||||||
|
|
||||||
### Sprawl
|
|
||||||
|
|
||||||
A skill that is simply too long — too many lines in SKILL.md — independent of whether they are stale or repeated. Even an all-live, all-unique skill can sprawl. It costs readability (the agent wades through more before it can act, and attention thins across the excess), maintainability (every extra line is one more to keep **relevant**), and tokens. The cure is the **information hierarchy**: push **reference** down behind **context pointers**, and split by **branch** or sequence so each path carries only what it needs. Distinct from **sediment** (length from stale accumulation) and **duplication** (length from repeated meaning) — sprawl is length itself, whatever its cause.
|
|
||||||
|
|
||||||
_Avoid_: bloat, length, size, verbosity
|
|
||||||
|
|
||||||
### No-Op
|
|
||||||
|
|
||||||
An instruction that changes nothing because the model already does it by default — you pay load to tell the agent what it would do anyway. The test: does a line change behaviour versus the default? A line can be perfectly **relevant** and still be a no-op. The same priors that make a **leading word** free make a no-op worthless.
|
|
||||||
|
|
||||||
A leading word is a *technique*; No-Op is a *verdict* on a line — and they cross. A leading word too weak to beat the default is a no-op (_be thorough_ when the agent is already thorough-ish), and the fix is a stronger word that passes the verdict (_relentless_), not a different technique. So the No-Op test — does it change behaviour versus the default? — is also how you grade whether a leading word is earning its repetitions. This is model-relative, not reader-relative: two people disagreeing over whether a line is a no-op disagree about the default, and settle it by running the skill, not by debate.
|
|
||||||
|
|
||||||
_Avoid_: redundant instruction, restating the obvious, belaboring
|
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# ADR Wiki
|
||||||
|
|
||||||
|
Architecture Decision Records live on the forge wiki and are cloned into `docs/adr/` during setup.
|
||||||
|
|
||||||
|
## Wiki URL
|
||||||
|
|
||||||
|
```
|
||||||
|
git@gitea.sagacity.ca:steve/Skills.wiki.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Derived from the forge remote during `/setup-skills`.
|
||||||
|
|
||||||
|
## Bootstrap
|
||||||
|
|
||||||
|
On first setup, `/setup-skills` clones the wiki:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone git@gitea.sagacity.ca:steve/Skills.wiki.git docs/adr/
|
||||||
|
```
|
||||||
|
|
||||||
|
And adds `docs/adr/` to `.gitignore`.
|
||||||
|
|
||||||
|
On subsequent sessions, pull the latest:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd docs/adr/ && git pull --rebase
|
||||||
|
```
|
||||||
|
|
||||||
|
## Auth
|
||||||
|
|
||||||
|
Authentication for pushing ADR changes uses: **SSH key**
|
||||||
|
|
||||||
|
The wiki remote uses `git@gitea.sagacity.ca:steve/Skills.wiki.git` — same SSH key used for the source repo.
|
||||||
|
|
||||||
|
## Session-end push
|
||||||
|
|
||||||
|
At end of every session, the agent:
|
||||||
|
|
||||||
|
1. `cd docs/adr/ && git add -A && git commit -m "docs(adr): <action> ADR-NNNN — <description>"`
|
||||||
|
2. `git pull --rebase` (handle any web UI edits)
|
||||||
|
3. `git push`
|
||||||
|
|
||||||
|
If a conflict arises during rebase, surface it to the user for resolution.
|
||||||
|
|
||||||
|
## Commit message convention
|
||||||
|
|
||||||
|
```
|
||||||
|
docs(adr): add ADR-NNNN — title
|
||||||
|
docs(adr): update ADR-NNNN — reason
|
||||||
|
docs(adr): remove ADR-NNNN — superseded by ADR-NNNN
|
||||||
|
```
|
||||||
|
|
||||||
|
## Consumer skills
|
||||||
|
|
||||||
|
These skills read from `docs/adr/` by relative path — the wiki clone is transparent:
|
||||||
|
|
||||||
|
- `diagnosing-bugs`
|
||||||
|
- `tdd`
|
||||||
|
- `improve-codebase-architecture`
|
||||||
|
- `domain-modeling`
|
||||||
|
- `grill-with-docs`
|
||||||
|
|
||||||
|
These skills may create ADRs in `docs/adr/`; the wiki push is handled at session end:
|
||||||
|
|
||||||
|
- `domain-modeling`
|
||||||
|
- `improve-codebase-architecture`
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Domain Docs
|
||||||
|
|
||||||
|
How the engineering skills should consume this repo's domain documentation when exploring the codebase.
|
||||||
|
|
||||||
|
## Before exploring, read these
|
||||||
|
|
||||||
|
- **`CONTEXT.md`** at the repo root, or
|
||||||
|
- **`CONTEXT-MAP.md`** at the repo root if it exists — it points at one `CONTEXT.md` per context. Read each one relevant to the topic.
|
||||||
|
- **`docs/adr/`** — read ADRs that touch the area you're about to work in.
|
||||||
|
|
||||||
|
If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill creates them lazily when terms or decisions actually get resolved.
|
||||||
|
|
||||||
|
## File structure
|
||||||
|
|
||||||
|
**Single-context repo** (this repo):
|
||||||
|
|
||||||
|
```
|
||||||
|
/
|
||||||
|
├── CONTEXT.md
|
||||||
|
├── docs/adr/
|
||||||
|
│ ├── 0001-event-sourced-orders.md
|
||||||
|
│ └── 0002-postgres-for-write-model.md
|
||||||
|
├── src/
|
||||||
|
└── .gitignore ← docs/adr/ ignored
|
||||||
|
```
|
||||||
|
|
||||||
|
## ADR lifecycle
|
||||||
|
|
||||||
|
ADRs live on the forge wiki and are cloned into `docs/adr/` during setup (`/setup-skills`). The source repo ignores `docs/adr/` via `.gitignore`.
|
||||||
|
|
||||||
|
- **Reading** — skills read ADRs by relative path (`docs/adr/...`) as before. The wiki clone is transparent.
|
||||||
|
- **Creating / updating** — skills write ADRs to `docs/adr/` as files. Changes accumulate in the wiki clone's local git state.
|
||||||
|
- **Pushing** — at end of session, the agent commits new/modified ADRs to the wiki clone and pushes to the forge wiki remote, using `git pull --rebase` before push to handle any concurrent web edits.
|
||||||
|
- **Commit message convention**: `docs(adr): <action> ADR-NNNN — <short description>`
|
||||||
|
|
||||||
|
## Use the glossary's vocabulary
|
||||||
|
|
||||||
|
When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids.
|
||||||
|
|
||||||
|
If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`).
|
||||||
|
|
||||||
|
## Flag ADR conflicts
|
||||||
|
|
||||||
|
If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
|
||||||
|
|
||||||
|
> _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Issue tracker: provider-neutral tracker over Gitea
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
- **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.
|
||||||
|
- **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.
|
||||||
|
- **Close**: `tea issue close <number>`. `tea issue close` does not accept a closing comment, so post the explanation first with `tea comment <number> "..."`, then close.
|
||||||
|
|
||||||
|
Infer the repo from git remote -v — `tea` does this automatically when run inside a clone.
|
||||||
|
|
||||||
|
## Pull requests as a triage surface
|
||||||
|
|
||||||
|
**PRs as a request surface: no.** _(Set to `yes` if this repo treats external PRs as feature requests; `/triage` reads this flag.)_
|
||||||
|
|
||||||
|
When set to `yes`, PRs run through the same labels and states as issues, using the `tea pr` equivalents:
|
||||||
|
|
||||||
|
- **Read a PR**: `tea pr <number> --comments` and `tea api /repos/{owner}/{repo}/pulls/<number>.diff` for the diff.
|
||||||
|
- **List external PRs for triage**: `tea pr list --state open -o json` then keep only PRs whose author is not a project member/owner (a contributor's MR, not a maintainer's in-flight work).
|
||||||
|
- **Comment / label / close**: `tea comment <number> "..."`, `tea pr edit --add-label`/`--remove-label`, `tea pr close`.
|
||||||
|
|
||||||
|
Gitea shares one number space across issues and PRs, so a bare `#42` may be either — resolve with `tea pr 42` and fall back to `tea issue 42`.
|
||||||
|
|
||||||
|
## When a skill says "publish to the issue tracker"
|
||||||
|
|
||||||
|
Create a Gitea issue.
|
||||||
|
|
||||||
|
## When a skill says "fetch the relevant ticket"
|
||||||
|
|
||||||
|
Run `tea issue <number> --comments`.
|
||||||
|
|
||||||
|
## Wayfinding operations
|
||||||
|
|
||||||
|
Used by `/wayfinder`. The **map** is a single issue with **child** issues as tickets.
|
||||||
|
|
||||||
|
- **Map**: a single issue labelled `wayfinder:map`, holding the Notes / Decisions-so-far / Fog body. `tea issue create --label wayfinder:map`.
|
||||||
|
- **Child ticket**: an issue linked to the map as a GitHub sub-issue (`tea api` on the sub-issues endpoint). Where sub-issues aren't enabled, add the child to a task list in the map body and put `Part of #<map>` at the top of the child body. Labels: `wayfinder:<type>` (`research`/`prototype`/`grilling`/`task`). Once claimed, the ticket is assigned to the driving dev.
|
||||||
|
- **Blocking**: GitHub's **native issue dependencies** — the canonical, UI-visible representation. Add an edge with `tea api --method POST /repos/{owner}/{repo}/issues/<child>/dependencies -F index=<blocker-issue-number> -F repo=<blocker-repo-name> -F owner=<bocker-owner-name>`, where `<blocker-issue-number>` is the blocker's numeric **issue number** (`tea api repos/{owner}/{repo}/issues/<n> --jq ".number. .repository.name, .repository.owner"`, where `.number` is the `<blocker-issue-number>`, `.repository.name` is the `<blocker-repo-name>` and `.repository.owner` is the `<blocker-repo-owner>`. Where dependencies aren't available, fall back to a `Blocked by: #<n>, #<n>` line at the top of the child body. A ticket is unblocked when every blocker is closed.
|
||||||
|
- **Frontier query**: list the map's open dependencies (`tea api /repos/{owner}/{repo}/issues/<n>/dependencies | jq '.[] | select(.state == "open") | .number'`, scoped to the map's sub-issues / task list), drop any with an open blocker (`list of dependencies is not empty`, or an open issue in the `Blocked by` line) or an assignee; first in map order wins.
|
||||||
|
- **Claim**: `tea issue edit <n> --add-assignees @me` — the session's first write.
|
||||||
|
- **Resolve**: `tea comment <n> "<answer>"`, then `tea issue close <n>`, then append a context pointer (gist + link) to the map's Decisions-so-far.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Provider-neutral tracker automation
|
||||||
|
|
||||||
|
Use `tracker` for normal issue and pull-request automation. It owns the high-level operation, prerequisite checks, normalization, bounded retry policy, and JSON protocol; it delegates credentials and provider commands to `gh`, `glab`, or `tea`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
tracker --provider gitea issue get 16
|
||||||
|
TRACKER_PROVIDER=gitlab tracker issue list --state open --label ready-for-agent
|
||||||
|
tracker pr get 42 --diff
|
||||||
|
```
|
||||||
|
|
||||||
|
Provider selection is explicit CLI flag, then `TRACKER_PROVIDER`, then the `origin` remote. Always choose `issue` or `pr` explicitly for reads and writes. Use `resolve-reference` only for an intentionally ambiguous bare number.
|
||||||
|
|
||||||
|
Parse `ok` and `error.code`; do not parse provider output or issue exploratory retries. The library (`from tracker import Tracker`) returns the same envelope as the CLI. `RecordingRunner` provides the fake subprocess seam for tests.
|
||||||
|
|
||||||
|
Provider-specific capability and fallback references remain in `docs/agents/issue-tracker.md` and the setup templates. They are not the normal execution path. Wayfinding relationships may use native provider APIs where available and task-list/body or note fallbacks otherwise; the result's `details` identifies the relationship mode.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Triage Labels
|
||||||
|
|
||||||
|
The skills speak in terms of seven canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker.
|
||||||
|
|
||||||
|
| Label in skills | Label in our tracker | Meaning |
|
||||||
|
| ----------------- | -------------------- | ---------------------------------------- |
|
||||||
|
| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
|
||||||
|
| `needs-info` | `needs-info` | Waiting on reporter for more information |
|
||||||
|
| `needs-review` | `needs-review` | Waiting for review by a human |
|
||||||
|
| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
|
||||||
|
| `ready-for-human` | `ready-for-human` | Requires human implementation |
|
||||||
|
| `in-progress` | `in-progress` | Being actively worked on by a human or agents |
|
||||||
|
| `wontfix` | `wontfix` | Will not be actioned |
|
||||||
|
|
||||||
|
When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table. Only one triage label should be active.
|
||||||
@@ -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