diff --git a/README.md b/README.md index 80af4e2..171310f 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ Any other input is sent to the agent. /install status /install upgrade scheduler-service -Eleven install targets exist side by side: `binary` (`~/.local/bin/tai.py`); a command-not-found hook per shell -- `bash-hook` (`~/.bashrc`), `zsh-hook` (`~/.zshrc`), `fish-hook` (`~/.config/fish/config.fish`, respecting `$XDG_CONFIG_HOME`) -- each a guarded block (backed up once to `.bak-tai` before the first write) adding a `tai` alias/function so the bare `tai` command resolves to the REPL instead of falling through, plus the shell's own not-found mechanism (`command_not_found_handle` for bash, `command_not_found_handler` for zsh, a `fish_command_not_found` function for fish) so any other unknown shell command is answered by the agent instead; a plexer auto-wrap hook per shell -- `plexer-hook` (bash), `zsh-plexer-hook`, `fish-plexer-hook` -- that transparently re-execs a fresh interactive shell into a managed, scriptable plexer session (`tai.py --plexer-wrap`; skipped automatically inside tmux, inside another plexer-wrapped shell, or when `~/.tai-no-plexer` exists) using whatever that shell can actually express (`[`/`case "$-"` for bash and zsh -- byte-identical content, since both are POSIX enough for it; `status is-interactive`/`test`/`isatty` for fish, which has neither); `venv` (`~/.tai/venv`, created whenever the venv module exists); `scheduler-service` and `telegram-service` (systemd user units that prefer the venv python); and `container` (the sandbox, whose image carries its own `/box/venv`). Installing a zsh/fish-specific target on a machine without that shell skips loudly (`"zsh-hook: zsh not found, skipped"`) rather than writing a dangling hook into a config file nothing will ever source; `uninstall`/`reinstall` still act on the file regardless, so a hook installed before a shell was later removed still gets cleaned up. `/install status` (or the `install` tool) reports exactly which of these exist, with versions and service states. `install` adds missing pieces, `upgrade` refreshes in place, `reinstall` rebuilds artifacts from scratch, `uninstall` removes them; service changes restart or start units immediately. Data handling is explicit: no action ever touches the vault (`memory.db`, secrets, schedules, records, backups). `./tai.py --install` (the one-shot CLI bootstrap) always installs the bash hook and, in addition, detects `$SHELL` and installs that shell's matching hook too when it's zsh or fish. +Twelve install targets exist side by side: `binary` (`~/.local/bin/tai.py`); a command-not-found hook per shell -- `bash-hook` (`~/.bashrc`), `zsh-hook` (`~/.zshrc`), `fish-hook` (`~/.config/fish/config.fish`, respecting `$XDG_CONFIG_HOME`) -- each a guarded block (backed up once to `.bak-tai` before the first write) adding a `tai` alias/function so the bare `tai` command resolves to the REPL instead of falling through, plus the shell's own not-found mechanism (`command_not_found_handle` for bash, `command_not_found_handler` for zsh, a `fish_command_not_found` function for fish) so any other unknown shell command is answered by the agent instead; a plexer auto-wrap hook per shell -- `plexer-hook` (bash), `zsh-plexer-hook`, `fish-plexer-hook` -- that transparently re-execs a fresh interactive shell into a managed, scriptable plexer session (`tai.py --plexer-wrap`; skipped automatically inside tmux, inside another plexer-wrapped shell, or when `~/.tai-no-plexer` exists) using whatever that shell can actually express (`[`/`case "$-"` for bash and zsh -- byte-identical content, since both are POSIX enough for it; `status is-interactive`/`test`/`isatty` for fish, which has neither); `venv` (`~/.tai/venv`, created whenever the venv module exists); `scheduler-service` and `telegram-service` (systemd user units that prefer the venv python); `container` (the sandbox, whose image carries its own `/box/venv`); and `skills` (six default skills into `~/.tai/skills`, see "Skills" below). Installing a zsh/fish-specific target on a machine without that shell skips loudly (`"zsh-hook: zsh not found, skipped"`) rather than writing a dangling hook into a config file nothing will ever source; `uninstall`/`reinstall` still act on the file regardless, so a hook installed before a shell was later removed still gets cleaned up. `/install status` (or the `install` tool) reports exactly which of these exist, with versions and service states. `install` adds missing pieces, `upgrade` refreshes in place, `reinstall` rebuilds artifacts from scratch, `uninstall` removes them; service changes restart or start units immediately. Data handling is explicit: no action ever touches the vault (`memory.db`, secrets, schedules, records, backups). `./tai.py --install` (the one-shot CLI bootstrap) always installs the bash hook and, in addition, detects `$SHELL` and installs that shell's matching hook too when it's zsh or fish. ## Plexer @@ -233,6 +233,8 @@ Its quieter cousin, `get_current_terminal_content`, grabs just the visible pane Standard agent skill files (`SKILL.md` with `name` plus `description` frontmatter, optional `scripts/`, `references/`, `assets/`, per the open Agent Skills format) are discovered from four places, in ascending priority so the most tai-specific location always wins a name collision: read-only interop with `~/.claude/skills` and `./.agents/skills` first (so a skill another tool already dropped next to your project is visible with *zero* install step), then tai's own `~/.tai/skills`, then `./.tai/skills`. Descriptions stay in the system prompt catalog at all times; full instructions load only through `load_skill`, on demand — the same lazy-loading philosophy as tools, applied to skills. +Six skills ship inside `tai.py` itself and land in `~/.tai/skills` with `./tai.py --install` (or `/install skills`): `deep-research` (parallel evidence-gathering method with per-domain source routing, adapted to `fork`/`poll` and `web_fetch`), `seriousdevelopment` (spec-first delivery method with ADRs and build tracking, verbatim), `git` (history-write safety plus a recovery script, with a note reconciling it with tai's approval settings), `plan` (explicit planning requests with research-before-drafting, `go` to execute), `requirements-clarification` (narrow pre-commit questions via `ask_user`), and `taste` (frontend anti-slop filter). They are installed, never forced: `install` writes only missing skills, `upgrade` refreshes pristine files and loudly keeps user-modified ones, `reinstall` rebuilds tracked files while keeping user-added extras, and `uninstall` removes only unmodified content. Edit an installed skill freely; its `.installs/` sidecar records the upstream provenance. + Two independent, complementary ways to get a skill: - **`create_skill`** authors a brand-new one from a brief: a dedicated deep-research subagent runs `sysinfo` first so the skill matches the actual machine, then researches with web search and shell until the material is independently verified, then writes `SKILL.md` plus supporting files — project-scoped by default. Six blueprints ship built in (`bot-creator`, `api-client`, `web-researcher`, `pdf-forms`, `data-wrangler`, `home-sysadmin`); `load_skill` builds a missing one through this exact same path the first time it's actually needed, so a skill finalizes lazily at the moment of use, not at install time. diff --git a/tai.py b/tai.py index f0c188d..b15a9ca 100755 --- a/tai.py +++ b/tai.py @@ -102,7 +102,7 @@ def _fromisoformat(text): return parsed.replace(tzinfo=tzinfo) raise ValueError("invalid datetime, use ISO like 2026-10-08T09:00") -VERSION = "1.39.0" +VERSION = "1.40.0" WORKER_STEPS = 12 CREATE_SKILL_STEPS = 40 FORK_MAX_DEPTH = 2 @@ -288,7 +288,7 @@ RestartSec=10 [Install] WantedBy=default.target """ -INSTALL_TARGETS = ("binary", "bash-hook", "zsh-hook", "fish-hook", "plexer-hook", "zsh-plexer-hook", "fish-plexer-hook", "venv", "scheduler-service", "telegram-service", "container") +INSTALL_TARGETS = ("binary", "bash-hook", "zsh-hook", "fish-hook", "plexer-hook", "zsh-plexer-hook", "fish-plexer-hook", "venv", "scheduler-service", "telegram-service", "container", "skills") CONTEXT_CAP = 32000 COMPACT_RATIO = 0.8 KEEP_TURNS = 6 @@ -2562,6 +2562,1925 @@ SKILL_BLUEPRINTS = { } +# Default skills, shipped in the file so a bare copy installs them anywhere. +# Provenance: deep-research plus seriousdevelopment v1.7.1 from the author's Muse +# user skills; git, plan, requirements-clarification, taste from Muse's bundled +# set. Adapted for tai where the host leaks through: load_skill (not read_skill), +# ask_user (not request_user_input), fork/poll workers, web_fetch (never curl), +# single-line SKILL.md descriptions (the frontmatter parser reads bare >/| only), +# no em dashes. seriousdevelopment is verbatim upstream. Edit these copies +# directly; they ARE the source. Raw strings keep backslashes byte-exact, so no +# embedded file may contain a triple-double-quote or end in a backslash. +BUNDLED_SKILLS = { + "deep-research": { + "SKILL.md": r"""--- +name: deep-research +description: "End-to-end deep research that turns a question into a cited, verifiable report. Use whenever the answer needs many external sources rather than a single lookup: technology/library selection, code and architecture questions, academic or medical literature, market and company facts, policy and standards, and 'what's the latest' or capability/benchmark questions. Routes each sub-question to the most authoritative sources for its domain (GitHub and official docs for code, arXiv/OpenAlex/Semantic Scholar for scholarship, PubMed/Europe PMC for medicine, SEC EDGAR for company facts, vendor and official pages for products), gathers evidence in parallel, triangulates claims, and writes a structured report with dated citations. Trigger on 调研/研究/调查/综述/对比一下 X、帮我查清楚 X、X 的最新进展/评测数据/方案有哪些, and any request for a research report, literature review, landscape survey, or source-backed answer - even when the user does not say the word 'research'." +--- + +# Deep Research + +Turn a question into a report a reader can trust: every claim traceable to a source, sources chosen for authority rather than convenience, and uncertainty stated instead of hidden. + +This skill is **self-contained and host-agnostic**. It defines the *method*; it requires no custom agent, plugin, or MCP server. Where it says "spawn workers", use whatever parallel-subagent facility the host provides (see *Running workers*); if none exists, run the same steps yourself, sequentially. + +## When to use + +Use it when the answer must be assembled from **multiple external sources** and needs to be current, comparative, or authoritative: + +- technical / library / architecture selection, migration paths, "X vs Y" +- literature reviews, "what does the research say about X" +- medical, clinical, or life-science questions +- market, company, competitive, or due-diligence facts +- policy, regulation, standards +- "what's the latest / what changed recently" about a fast-moving area +- any request for a research report, survey, landscape, or source-backed answer + +Do **not** use it for a single fact ("capital of France"), a quick definition, or when the user explicitly wants a short direct answer - just answer. + +## Principles + +1. **Primary sources first.** Prefer the thing itself - the paper, the official docs, the repository, the filing, the standard - over articles that describe it. Secondary sources orient and corroborate. +2. **Provenance on every non-obvious claim.** Each such claim carries a source URL and the date it was retrieved; quote the exact supporting text when wording matters. +3. **Triangulate.** A load-bearing claim needs ≥2 independent sources. If only one exists, say so and lower confidence. +4. **Freshness.** For "latest/current/recent" questions, constrain retrieval to a recent window and state the cutoff date. Version- or date-sensitive facts must name the version or date. +5. **Surface disagreement.** When credible sources conflict, present both and explain the difference; never silently pick one or average them. +6. **Separate fact from inference.** Label anything you derived; never present an inference as a fact. +7. **Uncertainty is data.** State what could not be verified and how that bounds the conclusion. +8. **Write in the user's language**, even though this skill is written in English. + +## Workflow + +Before Phase 2 (Route), read `references/source-routing.md`. Before Phase 5, read `references/report-template.md`. Before Phase 6, run `references/quality-checklist.md`. When spawning workers, take their prompt from `references/subagent-briefs.md`. + +Emit a one-line progress note per phase so a long run stays visible, e.g. `[deep-research] gather (phase 3) - 4 workers, 21 sources`. + +### 0 · Preflight +Verify retrieval works before planning: in tai this means the `web_search` and `web_fetch` tools, plus `shell` for git and code search. Tai forbids curl and wget via shell: every JSON-API recipe in `references/source-routing.md` is a plain GET, so issue it through `web_fetch` with the same URL and headers. If `web_search` is unavailable or has no provider configured, use the domain APIs in `references/source-routing.md` as the search path rather than relying on scraping search-engine result pages, and say so in the report's method section. For questions about a local codebase, also use code-search and file-read tools. + +### 1 · Frame +Restate the question in one or two sentences, then: +- Decompose it into 3–7 concrete sub-questions that together answer it. +- State the deliverable (default: a Markdown report file) and pick a depth (below). +- Define what "answered" means - which sub-questions must be covered. +- Ask the user **only** if a missing detail would change the whole approach; otherwise state your assumptions and proceed. They asked for a finished report, not a questionnaire. + +### 2 · Route +Classify the domain(s) and list, per sub-question, the authoritative source families from `references/source-routing.md`. Prefer primary/official sources. Note paywalled sources up front so they do not block later phases. + +### 3 · Gather (parallel) +Spawn one worker per sub-question or source family (see *Running workers*). Give each worker: the sub-question, the assigned source families, the recency window, and the evidence format from `references/subagent-briefs.md`. Each worker returns an **evidence pack**: + +``` +- Claim: + Source: - <URL> (retrieved YYYY-MM-DD) + Quote: "<verbatim supporting text>" + Tier: T1 primary | T2 authoritative secondary | T3 preprint/reputable community | T4 unvetted + Confidence: high | medium | low +``` + +Workers return evidence, not prose. They must not pad or speculate. + +### 4 · Consolidate +Merge the packs into one evidence table; deduplicate. Mark each load-bearing claim **verified** (≥2 independent T1/T2), **single-source**, or **contradicted**. List gaps and send targeted follow-ups until every "answered" item is covered or explicitly marked unanswerable. + +### 5 · Synthesize +Write the report using `references/report-template.md`: +- inline citation next to every non-obvious claim; +- facts, analysis, and recommendations visibly separate; +- a comparison table when comparing options; +- a "Confidence, gaps & disagreements" section. + +### 6 · Verify (before delivery) +Run `references/quality-checklist.md`. For contested or high-stakes topics, re-fetch a sample of load-bearing citations to confirm they still support the claim, and look for counter-evidence. Fix or annotate anything that fails. Never ship a claim whose cited page does not support it. + +### 7 · Deliver +- Write the report to a file (default `docs/research/<slug>-<YYYY-MM-DD>.md` unless the user names a location). +- Follow it with a 3–6 line summary: strongest conclusion, biggest uncertainty, most trusted sources. +- List sources with tier and retrieval date. + +## Running workers + +The method is host-agnostic; the spawning mechanics below are tai's (`fork`/`poll`). + +- **Spawning:** use the `fork` tool (one call per worker, task plus timeout up to one hour) and collect results with `poll` (each call waits up to two minutes, so poll in a loop until every worker finishes). Pass the worker prompt from `references/subagent-briefs.md` with the placeholders filled in. Tai workers share the machine and tools but get 12 steps, cannot ask the user, and stop nesting after 2 levels: keep briefs tight and self-contained. +- **Bound concurrency to 2–3 workers per turn.** Some providers reject a turn containing several parallel tool calls (e.g. "an assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'"). When there are more sub-questions than that, run them in batches. If a batch errors, relaunch only the failed workers in smaller batches, and if the error persists, do the work yourself sequentially. Tai caps live subagents at 8: never hold more than 8 unfinished `fork`s at once. +- **Worker profile:** `fork` accepts a profile; use the current one unless the task needs another identity. +- **If `fork` is unavailable**, run the same briefs yourself, sequentially, one sub-question at a time. The evidence format and routing do not change. +- Keep workers independent: one worker's conclusions must not frame another's search. + +## Depth levels + +| Depth | Use for | Shape | +|---|---|---| +| quick | simple, current facts | one gather pass, no independent verification, short report | +| standard (default) | most requests | parallel gather + consolidate + inline verification + full report | +| deep | contested, high-stakes, or "comprehensive" | multi-round, a dedicated verification worker, explicit disagreement analysis | + +## Bounded runs + +- Announce phase, worker count, and recency window before a long gather. +- If a source is rate-limited or paywalled, record it and move on - do not retry indefinitely. +- If a sub-question is still unanswered after two rounds, mark it and proceed. + +## Why this shape + +A single search returns undifferentiated snippets; a report needs provenance. The hard part is not fetching - it is knowing the authoritative source for each domain and proving the source actually supports the claim. The routing table and the verification pass exist for exactly that. +""", + "UPSTREAM.md": r"""# Upstream provenance + +- Source: https://github.com/wanqingying/debian-railway (path: opencode-config/opencode/skills/deep-research) +- Retrieved: 2026-10-11 +- License: none declared in upstream repo at retrieval time +- Installed verbatim via `muse skills install --scope user`; this file is the only local addition. +""", + "references/quality-checklist.md": r"""# Quality Checklist + +Run this in Phase 6, before delivery. It exists because the most common failure of AI research is confident prose whose citations do not actually support it. Fix or annotate every failed item - do not ship silently. + +## 1. Citation support + +- [ ] For every load-bearing claim, open the cited URL and confirm it **directly** supports the claim. +- [ ] The claim is not stronger than the source (no "proves" where the source says "suggests"). +- [ ] Quotes are verbatim and in context; no truncation that changes meaning. +- [ ] Version- and date-sensitive facts name the version/date, and the source is not obsolete. + +## 2. Coverage and balance + +- [ ] Every sub-question defined in Phase 1 is answered, or explicitly listed under "Not established". +- [ ] Load-bearing claims have ≥2 independent sources, or are labelled single-source. +- [ ] At least one attempt was made to find **counter-evidence** for contested claims. +- [ ] No single source is cited for many unrelated claims (citation monopolizing). + +## 3. Freshness + +- [ ] "Latest/current/recent" questions state the retrieval cutoff. +- [ ] The freshest authoritative source was used, not the most-indexed older one. +- [ ] Deprecated/retracted sources were replaced or flagged (check retraction notices, "superseded by" notes). + +## 4. Honesty + +- [ ] Facts, inferences, and recommendations are visibly separated. +- [ ] Uncertainties and gaps are stated where they matter, not buried. +- [ ] Source disagreements are shown, not averaged away. +- [ ] Tiers are assigned correctly (an article about a paper is not T1). +- [ ] No claim rests solely on a T4 source. + +## 5. Form + +- [ ] The report is in the user's language. +- [ ] The sections from `report-template.md` are present (TL;DR, Method, Findings, Confidence, Sources). +- [ ] Every source in the list has a tier and a retrieval date. +- [ ] The file is written where the user expects, and the chat summary names the strongest conclusion and the biggest uncertainty. + +## Failure handling + +- If a citation fails to support its claim: re-source it, downgrade the claim, or delete it - in that order. +- If two sources conflict and you cannot resolve it: report both, explain the likely reason (different dates, definitions, populations, methods), and say what would settle it. +- If a key sub-question is unanswerable within the time/source budget: say so plainly and recommend how to answer it (which source, dataset, or expert). +""", + "references/report-template.md": r"""# Report Template + +Use this structure for the deliverable. Keep it in the user's language. Omit a section only when it is genuinely empty, and say why. + +```markdown +# <Title - the question, answered in a phrase> + +**Question:** <the user's question, restated precisely> +**Date:** <YYYY-MM-DD> **Depth:** quick | standard | deep +**Sources:** <N> (<n1> primary) · **Cutoff:** <date range or "all dates"> + +## TL;DR +- <3–6 bullets: the conclusions a busy reader needs, each with a citation> +- <state the single biggest uncertainty here, not at the end> + +## Method +- Sub-questions investigated: <list> +- Source families used: <e.g. GitHub releases, arXiv, official docs, SEC EDGAR> +- What was not available: <paywalls, missing data, rate limits - and how that limits the result> + +## Findings +### 1. <Sub-question> +<Prose. Every non-obvious claim gets an inline citation [S1]. Keep facts and your analysis +visually distinct - mark derived statements clearly.> + +### 2. <Sub-question> +… + +## Comparison +<When options are compared. One row per option, one column per decision-relevant criterion. +Every cell carries a citation or "unverified".> + +| Option | Criterion A | Criterion B | Notes | +|---|---|---|---| +| X | … [S2] | … [S3] | | + +## Analysis +<What the evidence means together: patterns, trade-offs, the reasoning behind the +recommendation. Label this as interpretation, not fact.> + +## Confidence, gaps & disagreements +- **High confidence:** <claims backed by multiple independent T1/T2 sources> +- **Single-source / tentative:** <claims with only one source> +- **Contradicted:** <where credible sources disagree, both sides stated> +- **Not established:** <what could not be answered, and what would answer it> + +## Recommendations +<Only if the question implies a decision. Concrete, tied to the criteria above, and honest +about the conditions under which the advice would change.> + +## Sources +| ID | Source | Tier | Retrieved | +|---|---|---|---| +| S1 | <Title> - <URL> | T1 | <YYYY-MM-DD> | +| S2 | … | T2 | … | +``` + +## Rules + +- **Inline citations, next to claims.** A reader must reach a claim and its source together. A bare reference list at the end is not enough. +- **Cite the primary source.** If a secondary source points at a paper/dataset/filing, cite that too. +- **Dates everywhere.** Report date, source retrieval dates, and the recency cutoff for "latest" questions. +- **No orphan claims.** Anything non-obvious without a citation is either cited or moved to Analysis as a labelled inference. +- **Length follows the question**, not a page target. Density beats volume; never pad. +- **File output**: write to `docs/research/<slug>-<YYYY-MM-DD>.md` unless the user asked for another location or format. +""", + "references/source-routing.md": r"""# Source Routing + +Pick sources by **authority for the domain**, not by what a search engine ranks first. This file maps domains to primary sources and gives ready-to-run access recipes. Read it before Phase 2 (Route) and hand the relevant rows to each worker. + +## Source tiers + +| Tier | What it is | Examples | +|---|---|---| +| **T1 - primary** | The artifact itself; official first-party | source paper, official docs, the repository, the spec, an SEC filing, a vendor's own pricing page, a government site | +| **T2 - authoritative secondary** | Peer-reviewed or editorially accountable, but not the artifact | systematic reviews, standards bodies, reputable journalism, textbooks | +| **T3 - preprint / reputable community** | Useful but not yet vetted | arXiv/bioRxiv preprints, well-maintained community docs, conference talks | +| **T4 - unvetted** | SEO aggregators, unsourced blogs, forum posts, LLM-generated summaries | content farms, most "top 10" listicles, unsourced wiki mirrors | + +Rules of thumb: +- A claim is **verified** when ≥2 independent T1/T2 sources agree. Record the tier on every source. +- Use T3/T4 for leads and orientation, then chase the primary source they point to. +- Never cite a T4 source as the sole support for a load-bearing claim. + +## Domain → where to look + +| Domain | Go first to | Access | +|---|---|---| +| Code / libraries / infra | the project's official repo (README, releases, commits, issues/PRs), official docs site, changelog, migration guide, language/stdlib reference | GitHub REST API + ``git``/code search; `web_fetch` for docs | +| Package facts | npm, PyPI, crates.io, Maven Central, Hugging Face Hub | registry JSON APIs | +| AI/ML papers & methods | arXiv, OpenAlex, Semantic Scholar, Crossref, ACL/NeurIPS/ICML proceedings | APIs below | +| Models & capability data | vendor model cards and release notes, Hugging Face model cards, Papers with Code, official benchmark repos (SWE-bench, GAIA, τ-bench, BrowseComp, LiveCodeBench…), leaderboards (LMArena, Artificial Analysis, OpenRouter rankings) | official pages + arXiv tech reports | +| Medicine / clinical | PubMed & PMC, Europe PMC, Cochrane reviews, WHO, FDA/EMA labels, ClinicalTrials.gov | E-utilities / Europe PMC APIs | +| Life sciences | bioRxiv, medRxiv, UniProt, Ensembl, NCBI | APIs + `web_fetch` | +| General scholarship | OpenAlex, Crossref, Semantic Scholar, DOAJ, journal pages | APIs below | +| Company / finance | SEC EDGAR (10-K/10-Q/8-K, S-1), company IR and annual reports, exchange filings | EDGAR full-text search + submissions JSON | +| Markets & macro | World Bank, IMF, OECD, national statistics offices | open data APIs | +| Policy / law / standards | government sites and official gazettes, IETF RFCs, W3C recommendations, ISO/IEC pages | `web_search` → `web_fetch` official source | +| Products / pricing | the vendor's own docs, pricing page, and changelog | `web_fetch` | +| Security advisories | NVD, GitHub Security Advisories, vendor security bulletins, OSV | APIs + `web_fetch` | +| Sentiment / practice | Hacker News, Stack Overflow, Reddit, maintainer blogs | treat as T3/T4; corroborate | + +**China-specific topics:** prefer official Chinese sources (gov.cn, 行业协会, 公司官网/公告, 招股书) over community reposts. Treat CSDN, 简书, 微信公众号, 未注明出处的知乎回答 as T4 unless they link a primary source. + +## Access recipes + +Use `web_fetch` for HTML and for every JSON recipe below (tai forbids curl and wget via shell). Each recipe is a plain GET: pass the URL to `web_fetch`, headers via its `headers` argument, and read status plus body from the result. Quote URLs; many query values need encoding. + +**Always check the HTTP status.** `web_fetch` returns the HTTP status alongside the body: treat 4xx/5xx as errors to record, never as "no results". On `403`, add a `User-Agent` header; on `406`/`429`/`503`, wait and retry with backoff (arXiv, Semantic Scholar, and OpenAlex return these intermittently, and anonymous scholarly APIs are best-effort without a key). + +**GitHub** (a token raises the rate limit far above anonymous; store one with `store_secret` and pass it via the `web_fetch` `auth_secret` argument, or pass an `Authorization: Bearer ...` header): +``` +GET https://api.github.com/search/repositories?q=QUERY&sort=stars&order=desc&per_page=20 + header Accept: application/vnd.github+json +GET https://api.github.com/repos/OWNER/REPO/releases?per_page=10 +GET https://api.github.com/repos/OWNER/REPO/commits?per_page=10 +``` +Stars, issues, and commit dates are signals, not proof of quality - read the README and release notes. + +**arXiv** (no key; use the `https` URL as shown). arXiv throttles bursts with `406`, so send a `User-Agent` header and space requests out: +``` +GET https://export.arxiv.org/api/query?search_query=all:QUERY&start=0&max_results=20&sortBy=submittedDate&sortOrder=descending + header User-Agent: research/1.0 (mailto:you@example.com) +``` + +**Crossref** (no key): +``` +GET https://api.crossref.org/works?query=QUERY&rows=20&select=title,DOI,URL,issued,container-title,author +``` + +**OpenAlex** (add `&mailto=you@example.com`; anonymous search is often rate-limited, so retry with backoff, and use a free key for volume): +``` +GET https://api.openalex.org/works?search=QUERY&per-page=25&sort=publication_date:desc&mailto=you@example.com +``` + +**Semantic Scholar** (a free key is effectively required from shared/cloud IPs; anonymous calls frequently 429): +``` +GET https://api.semanticscholar.org/graph/v1/paper/search?query=QUERY&limit=20&fields=title,year,abstract,url,externalIds,citationCount,venue +``` + +**PubMed E-utilities** (no key): search IDs, then fetch summaries. +``` +GET https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&term=QUERY&retmax=20&retmode=json +GET https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&id=ID1,ID2&retmode=json +``` + +**Europe PMC** (no key): +``` +GET https://www.ebi.ac.uk/europepmc/webservices/rest/search?query=QUERY&format=json&pageSize=20 +``` + +**ClinicalTrials.gov** (no key; restrict `fields` - full records are large): +``` +GET https://clinicaltrials.gov/api/v2/studies?query.term=QUERY&pageSize=10&fields=NCTId,BriefTitle,OverallStatus,StartDate,Phase +``` + +**Hugging Face Hub** (no key): +``` +GET https://huggingface.co/api/models?search=QUERY&sort=downloads&direction=-1&limit=20 +``` + +**Package registries**: +``` +GET https://registry.npmjs.org/-/v1/search?text=QUERY&size=20 +GET https://pypi.org/pypi/PACKAGE/json +# crates.io returns 403 without a User-Agent header +GET https://crates.io/api/v1/crates?q=QUERY&per_page=20 + header User-Agent: research (mailto:you@example.com) +``` + +**SEC EDGAR** (no key; requires a descriptive `User-Agent`): +``` +# full-text search across filings +GET https://efts.sec.gov/LATEST/search-index?q=%22QUERY%22&forms=10-K + header User-Agent: research you@example.com +# company submission history (CIK zero-padded to 10 digits) +GET https://data.sec.gov/submissions/CIK0000320193.json + header User-Agent: research you@example.com +``` + +**World Bank** (no key): +``` +GET https://api.worldbank.org/v2/country/CN/indicator/NY.GDP.MKTP.CD?format=json&per_page=10 +``` + +**No `web_search`?** Every endpoint above is a plain `GET` served by `web_fetch` (JSON stays raw); use arXiv/Crossref/OpenAlex as a keyword-search fallback and parse the returned body. + +## Choosing within a domain + +- **Comparisons**: gather the same fields for every option - from each option's own primary source, then reconcile. +- **"Latest"**: sort by date and record the exact cutoff; a top-ranked page without a date is a red flag. +- **Contested topics**: deliberately search for the strongest opposing view (terms like "criticism", "limitations", "replication failure", "retracted"), not just confirming sources. +- **Numbers**: tie every figure to its definition, unit, population, and date; note when two sources measure different things. + +## Anti-patterns + +- Citing an article *about* a paper instead of the paper. +- One search engine's top results as the whole evidence base. +- Undated web pages for time-sensitive claims. +- Treating GitHub stars, download counts, or upvotes as quality evidence. +- Quoting an abstract as if it were the finding. +""", + "references/subagent-briefs.md": r"""# Subagent Briefs + +Copy-paste prompts for the workers described in Phase 3. Fill the `<…>` placeholders and pass the whole block as the subagent's task/prompt. The briefs are written so they work on any host - the text matters, not which agent runs it. + +Tai workers share the machine and tools: one `fork` per brief serves for retrieval and for shell/API calls alike. Keep each brief self-contained (12-step budget, no follow-up questions). + +> **Before spawning:** a worker has a fresh context and usually cannot read this skill's directory. So (a) replace `<SOURCE FAMILIES/RECIPES>` with the **actual URLs** from `references/source-routing.md`, not just the family name; and (b) keep the **SOURCE-TIER RUBRIC** line below in every brief so `Tier:` is unambiguous. + +## Worker brief - evidence gathering + +``` +You are one worker in a multi-worker deep-research run. Your only job is to gather and return +evidence for ONE sub-question. Do not write a report, do not speculate, do not pad. + +SUB-QUESTION: <sub-question> +SOURCE FAMILIES / RECIPES: <the actual URLs from source-routing.md for this sub-question, with any needed headers> +RECENCY WINDOW: <e.g. "published after 2025-01-01" or "any date"> +TODAY: <YYYY-MM-DD> +SOURCE-TIER RUBRIC: T1 = the artifact itself (paper, official docs, repo, filing, standard); +T2 = peer-reviewed or editorially accountable secondary; T3 = preprint / reputable community; +T4 = unvetted (SEO, unsourced blogs, forums). + +Method +1. Start from the most authoritative source listed; go to the primary source, not articles about it. +2. For each useful source, record the exact supporting text, not a paraphrase. +3. Stop when the sub-question is covered by at least two independent sources, or when you have + exhausted the assigned sources. Prefer 4–8 high-quality sources over many weak ones. Keep the + whole pack under ~60 lines. +4. If sources disagree, keep both and say so - do not resolve it yourself. +5. If a source is paywalled, rate-limited, or returns an error, note it and move on. On 406/429/503, + retry once with a short backoff; on 403, retry with a User-Agent header. + +Return ONLY this structure, one block per source: + +- Claim: <one sentence this source supports> + Source: <title> - <URL> (retrieved <YYYY-MM-DD>) + Quote: "<verbatim supporting text>" + Tier: T1 primary | T2 authoritative secondary | T3 preprint/reputable community | T4 unvetted + Confidence: high | medium | low + +Then: + +## Coverage +- Answered: <which parts of the sub-question are covered> +- Unanswered / gaps: <what you could not establish, and why> +- Disagreements: <conflicting claims, if any> +``` + +## Worker brief - verification (use in `deep` runs, Phase 6) + +``` +You are an independent verifier. You did not gather this evidence and must not trust it. + +You are given a list of load-bearing claims with their cited URLs. For each: +1. Fetch the cited URL. +2. Decide: does the page actually support the claim as stated? Watch for claims stronger than + the source (overreach), outdated versions, and quotes taken out of context. +3. Search for the strongest credible counter-evidence or a newer source that supersedes it. + +Return, per claim: + +- Claim: <as written> + Verdict: SUPPORTED | PARTIALLY SUPPORTED | NOT SUPPORTED | SUPERSEDED | UNVERIFIABLE + Evidence: <what the page actually says, with the relevant quote> + Counter-evidence: <if any, with URL> + Notes: <overreach, missing context, version/date problems> + +If a URL is dead, paywalled, or returns an error, use UNVERIFIABLE and record the error; you may +check the claim against the abstract or another copy of the same work, and say which. Never mark +something SUPPORTED on the basis of a page you could not read. +``` + +## Consolidation checklist (main agent, Phase 4) + +- [ ] Every evidence block has a URL and a retrieval date. +- [ ] Load-bearing claims are marked verified / single-source / contradicted. +- [ ] Contradictions between workers are surfaced, not averaged away. +- [ ] Gaps map to a follow-up worker or an explicit "unanswered" item. +- [ ] No T4 source is the sole support for a load-bearing claim. +- [ ] Source tiers look right (an article about a paper is T2/T3, not T1). +""", + }, + "git": { + "SKILL.md": r"""--- +name: git +description: Source-control safety for Git. Two rules apply whether or not you read the body. First, never commit, amend, push, tag, rebase, cherry-pick, revert, or reset --hard unless the user asked for that exact write in this session. An explicit request to create a new commit counts anywhere in the user's own task text, but it is not a request to amend, push, or tag. Finishing a task without that request is not authorization, so leave your work uncommitted for review. Second, when a Git lock file or corrupt index blocks you, never kill the holding process or bulldoze through with deletions - wait, retry, use the holder's own stop mechanism, or stop and report. Load the body only before writing Git history or recovering Git state. +user-invocable: false +--- + +# Git source-control safety + +The working tree belongs to the user. Make the change, leave it visible, and +let them decide what becomes history. + +## Never without an explicit ask + +The user must have asked for that exact write in this session, in their own +words. An explicit request to create a new commit counts anywhere in the user's +own task text, but it is not a request to amend, push, or tag. Finishing a task +without that request does not authorize a history write. + +- `commit`, `commit --amend`, `push`, `tag`, `rebase`, `cherry-pick`, `revert`, + `merge`, branch deletion, `reset --hard`, path restore, and `clean -f` all + require an explicit ask. +- Never publish merely because publication would be useful. + +### Authorization in tai + +In tai, the session's approval setting and standing rules are the explicit ask: when the user configured automatic commits, tags, or pushes (standing rules, `--yes`/`--auto`), those writes are pre-authorized and this skill does not veto them. It still governs everything else on this page: save a recovery snapshot before an authorized discard, never bulldoze locks, name files explicitly, never broaden the authorized operation. + +## Before an authorized discard + +Recovery is insurance, not authorization. A save never expands what the user +allowed you to delete or overwrite. + +When the user explicitly authorized an operation that may discard or broadly +overwrite workspace changes, and the workspace is an ordinary Git checkout: + +1. After `load_skill` lists this skill's bundled files, set `<recovery>` to the absolute `scripts/workspace-recovery.sh` path from that list and run: + + ```bash + bash <recovery> save before-discard + ``` + +2. Keep the printed `refs/tbh/recovery/...` value in your handoff, then perform + only the exact authorized operation. +3. If the save fails or the workspace is unsupported, stop before destruction + and ask the user. + +To recover that saved tree later, run: + +```bash +bash <recovery> restore <recovery-ref> +``` + +Restore first saves the current workspace, leaves HEAD, branch, and the real +index in place, and restores the selected snapshot into the worktree. The ref +also retains distinct staged bytes when the same path had later worktree edits. +Read those staged bytes with `git show '<recovery-ref>^2:<path>'`; the recovery +commit's second parent is the saved index tree. Ignored files are not saved; +restore refuses an ignored-path collision rather than overwrite data it could +not save. Use only the exact printed ref, not a revision expression. + +## Always fine + +Read-only inspection such as `status`, `log`, `show`, `diff`, `blame`, +`branch -v`, `show-ref`, `rev-parse`, `for-each-ref`, and `merge-base` is fine. +Staging named files to inspect `diff --cached`, and a temporary stash around a +tool that requires a clean tree, are also fine when the original state is put +back afterward. + +## The rest + +- Do not use a broad add in a tree you did not clean; name the files you changed. +- Do not initialize a repository inside vendored, build, data, or existing + source-control trees. +- Do not create or edit `.gitignore` or `.git/info/exclude` to hide the user's + files, and do not stash the user's files (including `stash push -u`) to make + `git status` look clean. "Leave the working tree clean", "tidy up", and + similar phrasing do not authorize committing, pushing, ignoring, excluding, + stashing, or deleting the user's files; clean up only scratch you created + and say the user's files remain. +- Do not hand-edit generated files; change their source of truth. +- Scratch repositories under a temporary directory are yours to experiment in. + +## Locks and corrupt state + +A Git lock usually means another process is writing. Never kill the holder on +your own initiative. Wait, inspect the holder read-only, use that tool's normal +stop mechanism, or report the block. Never delete a lock without proving no +live process owns it, and never respond to index corruption by deleting state +and committing anyway. + +Never commit or push while status reports changes you did not make and cannot +explain. + +## If you already made an unwanted write + +Say so plainly and stop. Do not attempt more history surgery without direction. +""", + "scripts/workspace-recovery.sh": r"""#!/usr/bin/env bash +set -euo pipefail +unset GIT_INDEX_FILE + +usage() { + echo "usage: workspace-recovery.sh save [label] | restore <recovery-ref>" >&2 + exit 2 +} + +die() { + echo "workspace recovery: $*" >&2 + exit 1 +} + +command -v git >/dev/null 2>&1 || die "git is unavailable" +root=$(git rev-parse --show-toplevel 2>/dev/null) || die "not an ordinary Git workspace" +head=$(git -C "$root" rev-parse --verify HEAD 2>/dev/null) || die "unborn HEAD is unsupported" +[ -z "$(git -C "$root" ls-files -u)" ] || die "an unmerged index is unsupported" +saved_ref= + +write_recovery_commit() { + local message=$1 tree=$2 + shift 2 + printf '%s\n' "$message" | + GIT_AUTHOR_NAME="TBH Recovery" \ + GIT_AUTHOR_EMAIL="recovery@localhost" \ + GIT_COMMITTER_NAME="TBH Recovery" \ + GIT_COMMITTER_EMAIL="recovery@localhost" \ + git -C "$root" commit-tree "$tree" "$@" +} + +save_snapshot() { + local label=${1:-checkpoint} + local alternate_index index_entries real_index index_tree index_commit tree commit timestamp + local entry mode path head_entry head_mode + + case "$label" in + ''|*[!A-Za-z0-9._-]*) die "label must use only letters, digits, dot, underscore, or dash" ;; + esac + timestamp=$(date -u +%Y%m%dT%H%M%SZ) + saved_ref="refs/tbh/recovery/$label/$timestamp-$$" + git check-ref-format "$saved_ref" >/dev/null 2>&1 || die "label does not form a valid recovery ref" + + alternate_index=$(mktemp "${TMPDIR:-/tmp}/tbh-recovery-index.XXXXXX") + index_entries=$(mktemp "${TMPDIR:-/tmp}/tbh-recovery-entries.XXXXXX") + rm -f "$alternate_index" + trap 'rm -f "$alternate_index" "$index_entries"' EXIT + trap 'exit 130' HUP INT TERM + + real_index=$(git -C "$root" rev-parse --git-path index) + case "$real_index" in + /*) ;; + *) real_index="$root/$real_index" ;; + esac + cp "$real_index" "$alternate_index" + index_tree=$(GIT_INDEX_FILE="$alternate_index" git -C "$root" write-tree) + index_commit=$( + write_recovery_commit "TBH recovery index: $label" "$index_tree" -p "$head" + ) + rm -f "$alternate_index" + GIT_INDEX_FILE="$alternate_index" git -C "$root" read-tree "$head" + GIT_INDEX_FILE="$alternate_index" git -C "$root" add -A -- . + GIT_INDEX_FILE="$alternate_index" git -C "$root" ls-files --stage -z >"$index_entries" + while IFS= read -r -d '' entry; do + mode=${entry%% *} + [ "$mode" = 160000 ] || continue + case "$entry" in + *$'\t'*) path=${entry#*$'\t'} ;; + *) die "cannot inspect saved Git links" ;; + esac + head_entry=$(GIT_LITERAL_PATHSPECS=1 git -C "$root" ls-tree "$head" -- "$path") + head_mode=${head_entry%% *} + [ "$head_mode" = 160000 ] || + die "cannot save untracked embedded Git repository: $path" + done <"$index_entries" + tree=$(GIT_INDEX_FILE="$alternate_index" git -C "$root" write-tree) + commit=$( + write_recovery_commit "TBH recovery worktree: $label" "$tree" -p "$head" -p "$index_commit" + ) + git -C "$root" update-ref "$saved_ref" "$commit" "" + + rm -f "$alternate_index" + rm -f "$index_entries" + trap - EXIT HUP INT TERM +} + +collect_one_restore_collision() { + local path=$1 leaf=$2 candidates=$3 directory_candidates=$4 + if [ ! -e "$root/$path" ] && [ ! -L "$root/$path" ]; then + return + fi + if [ "$leaf" = false ] && [ -d "$root/$path" ] && [ ! -L "$root/$path" ]; then + return + fi + if [ "$leaf" = true ] && [ -d "$root/$path" ] && [ ! -L "$root/$path" ]; then + printf '%s\0' "$path" >>"$directory_candidates" + fi + printf '%s\0' "$path" >>"$candidates" +} + +refuse_unsafe_restore_collisions() { + local recovery_commit=$1 collision_dir path_list index_paths candidates directory_candidates + local ignore_candidates ignore_matches path ancestor previous_path index_path index_eof + local directory_path directory_eof tracked_exact leaf_directory ignore_status ignored_path + local first_candidate LC_ALL=C + + collision_dir=$(mktemp -d "${TMPDIR:-/tmp}/tbh-recovery-collisions.XXXXXX") + path_list="$collision_dir/recovery-paths" + index_paths="$collision_dir/index-paths" + candidates="$collision_dir/collision-candidates" + directory_candidates="$collision_dir/directory-candidates" + ignore_candidates="$collision_dir/ignore-candidates" + ignore_matches="$collision_dir/ignore-matches" + : >"$candidates" + : >"$directory_candidates" + : >"$ignore_candidates" + trap 'rm -rf "$collision_dir"' EXIT + trap 'exit 130' HUP INT TERM + git -C "$root" ls-tree -r --name-only -z "$recovery_commit" >"$path_list" + git -C "$root" ls-files -z >"$index_paths" + + previous_path= + while IFS= read -r -d '' path; do + ancestor=$path + while [[ "$ancestor" == */* ]]; do + ancestor=${ancestor%/*} + case "$previous_path" in + "$ancestor"/*) continue ;; + esac + collect_one_restore_collision "$ancestor" false "$candidates" "$directory_candidates" + done + collect_one_restore_collision "$path" true "$candidates" "$directory_candidates" + previous_path=$path + done <"$path_list" + + LC_ALL=C sort -zu "$candidates" -o "$candidates" + LC_ALL=C sort -zu "$directory_candidates" -o "$directory_candidates" + LC_ALL=C sort -zu "$index_paths" -o "$index_paths" + + index_path= + index_eof=false + exec 9<"$index_paths" + if ! IFS= read -r -d '' index_path <&9; then + index_eof=true + fi + directory_path= + directory_eof=false + exec 8<"$directory_candidates" + if ! IFS= read -r -d '' directory_path <&8; then + directory_eof=true + fi + while IFS= read -r -d '' path; do + while [ "$index_eof" = false ] && [[ "$index_path" < "$path" ]]; do + if ! IFS= read -r -d '' index_path <&9; then + index_eof=true + index_path= + fi + done + tracked_exact=false + if [ "$index_eof" = false ] && [ "$index_path" = "$path" ]; then + tracked_exact=true + fi + + while [ "$directory_eof" = false ] && [[ "$directory_path" < "$path" ]]; do + if ! IFS= read -r -d '' directory_path <&8; then + directory_eof=true + directory_path= + fi + done + leaf_directory=false + if [ "$directory_eof" = false ] && [ "$directory_path" = "$path" ]; then + leaf_directory=true + fi + + [ "$tracked_exact" = true ] && continue + [ "$leaf_directory" = false ] || die "restore would replace an untracked directory: $path" + printf '%s\0' "$path" >>"$ignore_candidates" + done <"$candidates" + exec 9<&- + exec 8<&- + + if [ -s "$ignore_candidates" ]; then + if git -C "$root" check-ignore -z --stdin --no-index \ + <"$ignore_candidates" >"$ignore_matches"; then + if IFS= read -r -d '' ignored_path <"$ignore_matches"; then + die "restore would overwrite an ignored path: $ignored_path" + fi + die "cannot verify ignore rules for restore paths" + else + ignore_status=$? + if [ "$ignore_status" -ne 1 ]; then + if ! IFS= read -r -d '' first_candidate <"$ignore_candidates"; then + first_candidate="restore paths" + fi + die "cannot verify ignore rules for: $first_candidate" + fi + fi + fi + + rm -rf "$collision_dir" + trap - EXIT HUP INT TERM +} + +case ${1:-} in + save) + [ "$#" -le 2 ] || usage + save_snapshot "${2:-checkpoint}" + printf 'saved %s\n' "$saved_ref" + ;; + restore) + [ "$#" -eq 2 ] || usage + recovery_ref=$2 + case "$recovery_ref" in + refs/tbh/recovery/*) ;; + *) die "restore requires a refs/tbh/recovery/... ref" ;; + esac + git check-ref-format "$recovery_ref" >/dev/null 2>&1 || + die "restore requires an exact recovery ref" + recovery_oid=$(git -C "$root" show-ref --verify --hash "$recovery_ref" 2>/dev/null) || + die "recovery ref does not name a commit" + recovery_commit=$(git -C "$root" rev-parse --verify "$recovery_oid^{commit}" 2>/dev/null) || + die "recovery ref does not name a commit" + refuse_unsafe_restore_collisions "$recovery_commit" + save_snapshot before-restore + printf 'saved current workspace to %s\n' "$saved_ref" + git -C "$root" restore --source="$recovery_commit" --worktree -- . + printf 'restored %s\n' "$recovery_ref" + ;; + *) usage ;; +esac +""", + }, + "plan": { + "SKILL.md": r"""--- +name: plan +description: On an explicit planning request, always load this skill (load_skill) before answering. Research first, then return one concise inline plan; do not create a plan file unless the user explicitly asks or a governing workflow requires one. Start and end the reply by saying this is not a special mode and `go` executes the plan; do not implement in the planning turn. For ordinary implement, build, fix, debug, or refactor requests, work directly. When an explicit plan divides work into separate diffs, commits, or PRs and the user asks to publish, preserve that split and tell the user before deviating. +--- + +# Plan + +Create a grounded, decision-complete plan before complex work. + +## Scope + +- Use this skill ONLY when the user explicitly asks to plan - they request a plan, + design, approach, rollout or migration strategy, or PR breakdown, or invoke the + plan skill directly (load_skill). +- Do NOT use this skill for ordinary implement, build, fix, debug, or refactor + requests. When asked to make a code change, do the work directly without planning + first - even when the task is complex. Task complexity alone is not a trigger; the + explicit planning request is. +- Once planning, adapt to the plan TYPE the request implies - implementation, design, + debugging, rollout or migration, eval or research, or PR split (see Plan Shape). + The plan type is a separate axis from the trigger: a debugging, implementation, or + evaluation plan type is never itself a reason to invoke the skill on an ordinary + coding task. +- An explicit invocation ("plan this") always ends in a + plan and never in the change itself, however small the change: a user who + asks for a plan for a one-line edit wants the plan, not the edit. Only when the + skill was offered automatically, skip it for simple one-step edits, obvious + bug fixes, formatting, copy changes, and direct questions that can be + answered without planning. +- Treat this as planning guidance, not a host-enforced mode. Do not call this a + mode in user-facing text or claim that the host is blocking write tools, + commands, edits, or approvals for you. +- Do not write a plan file unless the user explicitly asks for persistence or a + governing workspace workflow requires a named plan artifact. You may write at + most one such markdown file. +- Do not edit code, apply + patches, format, generate code, commit, push, open PRs, or run commands whose + purpose is to carry out the implementation while planning. +- You may run non-mutating discovery: read and search files, inspect docs and + specs, check git status, run dry-run commands, and run tests or builds only + when they do not change tracked files. +- Resolve facts that can be discovered locally before asking the user. +- Ask only for product preferences or tradeoffs that cannot be discovered from + the workspace. + +## Research Before Drafting + +For an explicit plan, follow these steps in order. Do not write plan prose or a plan +file until the applicable research in steps 1-5 is complete: + +1. **Map the decisions.** Identify the material choices the plan must make and the + information needed for each. Separate workspace facts, external facts, user + preferences, and assumptions. +2. **Research workspace facts.** Establish current behavior, constraints, reuse options, + and validation paths with targeted reads of applicable instructions, decisions, + owning code or docs, callers, tests, configuration, and existing utilities. Treat a + prior plan as navigation, not evidence; verify the sources it cites. +3. **Ask only when necessary.** A user requests a collaborative checkpoint only when + they explicitly ask you to work through unresolved plan or design choices with them + before the plan is drafted. Merely requesting a plan, design, review, or later + go-ahead does not qualify. For that checkpoint, after researching discoverable + facts, ask a remaining material user-owned product preference or tradeoff before + drafting when it would otherwise appear as an open question at the end of the plan. + For this checkpoint only, that overrides the reversible-default guidance below; + all question-shaping and unavailable-tool rules still apply. + Otherwise follow the ordinary rule below. Before using a question tool, research any + factual unknown that could eliminate the question; this may include bounded, + preference-independent external research. Do not ask merely because scope, platform, + or experience is unspecified. Ask only when a remaining + user-owned preference cannot be resolved from the request, workspace, or research and + its answer would materially change the research or recommendation, or avoid substantial + rework. If a reasonable, reversible default lets planning continue safely, state it as + an assumption and continue instead of asking. Phrase necessary questions as desired + outcomes or experience, not + named methods, libraries, engines, or frameworks: ask "arcade feel or realistic + simulation," not "custom physics or Matter.js." Research and recommend the technical + implementation yourself. When input is necessary, ask only one necessary question per + turn. Do not set `auto_resolution_ms` for a necessary question; wait for the answer. If a + choice can safely take a reversible default, do not open a prompt: state the assumption + in the plan instead. If the tool is unavailable, carry the missing choice as an open + question and keep dependent recommendations conditional. + Ask through the `ask_user` tool (question plus options); it blocks until the user answers, so finish independent work first and ask last. `ask_user` is unavailable to background workers and in autonomous mode: there, state the assumption in the plan instead of asking. +4. **Research external facts when they matter.** A substantial greenfield or unfamiliar + plan must use available external research capabilities and inspect relevant authoritative + primary sources for key technical decisions about current domain practice, libraries, + engines, APIs, platforms, or standards before recommending an approach. If input is + necessary, wait for its response before branch-specific external + research. Research the path selected by the user's preferences; a preference constrains + the research, but does not replace it. Source discovery is not source inspection. Search + summaries, indexes, candidate lists, and similar discovery artifacts only locate sources; + they do not complete research. Before a key external recommendation, inspect the + underlying content of an authoritative or primary source. Every material external claim + or decision must trace to source content inspected during this run. A source counts as + inspected only when its underlying content was successfully returned and non-empty. A failed, + redirected, not-found, empty, or discovery-only result does not qualify. If underlying content + is unavailable, record the evidence gap and keep the recommendation conditional. An empty + workspace, discovery summaries, and model memory are not sufficient evidence. Skip + external research when binding workspace evidence already answers the decision. Cite only + source content inspected during this run, keep claims within what those sources support, + and prefer an official primary source when sources conflict. Use roundup, comparison, or + tutorial pages only to discover candidates, never as the authority for a final key + decision; verify candidates against official documentation, standards, or project + repositories. Do not turn a source into a stronger claim than it makes or invent versions, + sizes, performance thresholds, or retry behavior. +5. **Close delegated research.** Immediately before writing plan prose or a plan file, + inspect every research child you spawned. A `poll` that reports running is not a terminal + child state. While any child remains pending, keep polling until it finishes. + Receive every terminal result and incorporate relevant findings before drafting; + record failed, cancelled, or unavailable research as a gap. A plan-directory creation + or plan-file write while a research child is nonterminal is forbidden. If a child is abandoned, record its evidence gap before drafting. +6. **Synthesize, then draft.** Immediately before drafting, build a private decision-evidence + map. Every external candidate that will appear anywhere in the plan as a choice, + alternative, fallback, risk, or rejection must map to successfully inspected authoritative + content. Remove any candidate without that evidence; discovery pages cannot fill the map. + Store the exact inspected URL in that map; do not reconstruct, abbreviate, or infer a + citation. Only then recommend the approach. Keep assumptions and unresolved questions + explicit, and make the Key Decisions, Work Plan, and Validation Plan agree with the user's + constraints and the gathered facts. + +Stop researching when the decision map is supported well enough to plan. Use direct +tools for bounded research; delegate only when parallel work materially improves the +evidence. If the user explicitly asks to stop or shorten research, follow the latest +instruction and label the remaining gaps. + +## Quality Bar + +A plan is ready only when it is: + +- **Grounded**: cite the user goal, current facts, docs, nearby code, tests, + commands, logs, specs, issues, or PRs that shaped the plan when those sources + exist. Mark guesses as assumptions. +- **Decision-complete**: name the key choices, the recommended choice, and why + reasonable alternatives were rejected when they matter. +- **Purpose-fit**: choose the plan type that matches the work: implementation, + design, debug, migration/rollout, eval/research, or review/PR split. +- **Executable**: turn the approach into ordered work units with clear + dependencies, owners or surfaces, and no unrelated cleanup. +- **Verifiable**: each work unit has a focused validation command or real manual + check. Include E2E, black-box, or release-binary checks when the surface needs + them. +- **Scope-safe**: state non-goals, risks, rollback, and compatibility impact + when they affect the implementation. +- **Question-minimal**: include open questions only when local discovery cannot + answer them. +- **Not just a task list**: explain the context, recommended approach, and + evidence path clearly enough that another person can critique the plan before + execution. + +## Explicit File Output + +Deliver the plan inline by default. Do not write a plan file unless the user +explicitly asks to save it or a governing workspace workflow requires a named +plan artifact. Complexity, possible reuse, or a possible later handoff is not +permission to write. + +When persistence is authorized: + +- If the user gives a path or the governing workflow names one, use that path. +- If the workspace has an established durable plan location, follow it. Prefer, + in order: an active `specs/<feature>/plan.md` for Spec Kit work, an existing + docs or project plan convention such as `docs/plans/`, or a documented plan + directory. +- If saving and no stronger convention exists, save to + `.agents/plans/YYYY-MM-DD-<slug>.md`. +- Create parent directories as needed. +- You may update an active `specs/<feature>/plan.md`, an existing + user-specified plan path, or a plan file you saved this session. Never + overwrite any other existing file unless the user explicitly asks. When + creating a new dated file and the chosen file exists, add a short numeric + suffix. +- Do not use `/tmp` for the final plan. Temporary scratch is not a durable user + artifact. + +## User-Visible Delivery + +Start the final reply with this exact sentence on one line so the next action stays visible +even when a long plan is collapsed: + +This is a plan, not a special mode; I haven’t started implementation. Reply `go` to execute this plan, or tell me what to change. + +Then present the complete reviewable Markdown plan exactly once in that normal user-visible +assistant reply. Saving or rereading a plan file is not presenting it. + +Use one canonical Markdown plan body. Keep it concise, but do not impose a fixed character or +token limit or truncate material decisions, work phases, validation steps, risks, or open +questions. Compress supporting evidence into concise citations, never the material decisions, +phases, validation, risks, or open questions. If you save a plan file, its content must be +exactly the canonical body. Copy the canonical body verbatim into the normal assistant reply. +Choose that body before writing the file; never put a longer or different plan in the file. +Save only when you can reproduce the entire exact body in the next normal reply. If you cannot, +skip the file and deliver the complete plan inline. Do not create two independently expanded +versions of the plan. If you save the plan, put its path after the visible plan. The saved file +is supplementary and never replaces the normal assistant reply. +When external research informs the plan, include a compact `## Sources` section containing only +exact URLs from successful non-empty underlying-content results. Each material external decision +must cite one of those URLs. + +## Conversational Handoff + +Before delivery: + +- Audit the saved plan through complete readback and correction until it is stable; do not + present the plan during this audit loop. Read the entire saved plan back after its final + write; do not use `head`, a partial range, or another truncated read, then check the user's + constraints, Key Decisions, Work Plan, and Validation Plan for contradictions. +- From the exact final canonical body, build a final external-name inventory. Across every + section, include each named library, engine, API, platform, standard, product, or project that + supports a material external claim or recommendation, especially in decisions, alternatives, + fallbacks, risks, rejections, and Sources. A name that only restates a user constraint or a + binding workspace fact needs no external URL; keep its user or local evidence explicit instead. + Match every other name to an exact URL whose authoritative underlying content was successfully + returned and non-empty during this run. Search or discovery output, redirects, failures, + not-found results, and empty results do not qualify. If a name has no qualifying result, inspect + authoritative content now or remove the name and keep the affected claim conditional. Do not + deliver until every inventory entry is resolved. +- Audit each material external decision against its matched source. Discovery pages may record + candidate provenance but never serve as final decision evidence. Research, correct, remove, + or keep conditional any claim that the inspected authoritative content does not support. +- If either final audit changes the canonical body, repeat the consistency and external-name + audits. When a file is saved, rewrite it and read the entire saved plan again first. Deliver + only after one complete pass makes no changes. +- Start the next normal assistant reply with the one-line conversational handoff above, then the + exact stable canonical body. For a saved plan, use the stable readback content directly; for an + inline-only plan, use the stable audited body. Do not regenerate, shorten, summarize, or omit + any part. Put the saved path, when one exists, after the complete body. + +After delivery: + +- Do not use question tools for the final handoff. They remain reserved for + necessary pre-draft user-owned choices in Research step 3. +- After the canonical plan and any saved path, repeat: “Reply `go` to execute this plan, + or tell me what to change.” +- Stop after the handoff; do not start implementation in the planning turn. +- On `go`, execute the preceding plan directly without regenerating it. On a change request, + revise and present the complete plan again. + +## Plan Shape + +Write the plan so the next person can act without guessing. Use the smallest +subset of this shape that carries the decisions needed for execution: + +```markdown +## Goal +## Success Criteria +## Approach +## Steps +## Validation Plan +## Risks / Open Questions (only when material) +``` + +Write `None` for open questions only after checking the repo for answers. +Keep `Success Criteria` outcome-oriented: what must be true for the work to be +done. Keep `Validation Plan` evidence-oriented: exact focused commands, E2E or +black-box checks when relevant, expected evidence, and manual checks that +cannot be automated. + +Adapt the sections to the plan type: + +- For implementation work, `Steps` should name the code surfaces, data flow, + compatibility impact, existing utilities to reuse, and test strategy. Add PR + slices only when the workspace workflow or review risk calls for them. +- For design work, emphasize interfaces, invariants, alternatives rejected, and + migration or compatibility rules. +- For debugging work, list hypotheses, observations needed, instrumentation or + logs to inspect, reproduction steps, and the evidence that will confirm or + falsify each hypothesis. +- For rollout or migration work, include phases, gates, rollback, data safety, + monitoring, and user-visible impact. +- For eval or research work, include the benchmark/question, corpus or sources, + comparison arms, success metrics, confounders, and reproducibility evidence. + +Do not invent an issue, spec, PR, or file path just to fill a section. If the +workspace rules require them, cite the real item or state the missing prerequisite +as an open question or blocker. Do not repeat background the user already knows or +add empty sections. Keep the plan decision-complete enough that an implementer does +not need to invent scope, interfaces, or verification. + +## Workflow + +1. Classify whether planning is actually needed. If the task is simple AND the + skill was offered automatically, say so and answer or implement under normal + rules instead. On an explicit invocation, plan it regardless of size. +2. Complete Research Before Drafting when it applies. Ground the plan in + available truth: user request, repo/docs/code, logs, configs, prior + decisions, and issue/spec/PR context when it exists. +3. Identify the plan type and the smallest viable path that proves the approach. +4. Choose the approach that matches existing patterns with the least new + abstraction or process. +5. Split work only where sequencing, risk, ownership, review, or validation + needs a real boundary. +6. Map every work unit to validation evidence or a real manual check. +7. If the user or workflow requested a saved plan, briefly report its path. Do not + announce the absence of a file for the normal inline case. +8. Highlight the highest-risk validation step. +9. Present the plan in the User-Visible Delivery form and stop for the user's + ordinary next message before implementation. + +## Exit + +- After generating the plan, present the conversational handoff, the complete canonical + Markdown, and the repeated handoff in one normal assistant reply. +- Stop for the user's ordinary next message. Do not treat planning text as permission to edit. +- If the user says go, continue, implement, or otherwise starts execution, use the + preceding plan directly: leave planning and do the work under the normal workspace + rules, carrying the plan's structural commitments (see Executing The Plan). +- Re-check the latest user message before editing so stale plan assumptions do + not override a new instruction. + +## Executing The Plan + +These rules bind during execution whether the plan came from this skill or was +supplied by the user, and whether or not this skill was ever invoked. If you +loaded this skill at publish time, only this section applies - the planning +restrictions above do not. + +- A plan's structural commitments survive into execution: the diff/commit/PR + split and the unit ordering stay binding while the work is carried out and + published. +- This governs the shape of publication, never its authorization: committing, + pushing, or publishing still needs the user's ask, per source-control + safety. A plan step that says "commit" does not by itself authorize a + commit. +- When the user has you publish work the plan divides into N separate diffs, + commits, or PRs, publish exactly that split: commit each planned unit + separately, in the planned order, even when one combined commit would + satisfy the literal request ("commit this", "publish it"). +- Work already finished in one mixed working tree still gets split at publish + time: group the changed files by plan unit and commit unit by unit. If one + file's changes span units, split at the hunk level (`git add -p`) or assign + the file to the earliest unit and say so. +- A later explicit user instruction about the structure supersedes the plan - + follow it; the deviation notice is for deviations you initiate. +- Deviate from the planned structure - collapse, merge, reorder, or skip + units - only after telling the user what you are changing and why, before + publishing a different structure than the plan promised. +""", + }, + "requirements-clarification": { + "SKILL.md": r"""--- +name: requirements-clarification +user-invocable: false +description: Clarify a material user-owned product decision before substantive implementation commitments. Use only when request and named/governing evidence still leave a choice that would materially change scope, platform support, user-facing surface, data/API contract, domain values, project placement, or cross-system architecture; also use when a truncated request omits such a choice. On the initial turn, inspect only named or governing requirements evidence, then ask directly without loading: ask one blocking outcome question or at most three coupled questions; wait. After the answer, load the body before write or delegation. Do not use for implement/build/fix/debug/refactor work when expected behavior is settled, plan/design deliverables owned by Plan, complexity/greenfield/integration alone, agent-owned technical choices, locally discoverable facts, reversible defaults, permission to begin, or stop/no-tool turns. Never re-ask supplied facts or turn clarification into refusal. +--- + +# Requirements Clarification + +Resolve only a remaining user-owned product decision. Do not turn normal +implementation uncertainty into a question. + +1. Read the request and any explicitly named or governing requirements source. + Do not scan broadly for hypothetical ambiguity. If this evidence settles the + outcome, proceed without asking. +2. Separate user-owned outcomes and authoritative inputs from agent-owned + implementation choices. Research discoverable facts yourself. Never ask the + user to choose a library, framework, or method the agent can determine. +3. If a narrow reversible default safely satisfies the request, state it and + proceed. Complexity, greenfield status, file count, or integration work alone + never requires clarification. +4. Otherwise ask before substantive commitment. Reads, notes, or one small + branch-neutral probe are allowed; a scaffold, public contract, embedded domain + values, or cross-system architecture is a commitment. +5. Ask one blocking outcome question; + group at most three tightly coupled questions. Offer concrete outcome options + and a recommendation. Never ask merely for permission to begin. + Ask through the `ask_user` tool (question plus options); it blocks until the user answers. `ask_user` is unavailable to background workers and in autonomous mode: there, decide yourself and state the assumption instead of asking. +6. After the answer, do not ask again unless later evidence reveals a distinct + material fork. Apply the answer and continue. + +`plan` owns standalone plan and design deliverables. This skill does not create +an approval gate or stop a specified implementation merely because planning +would be useful. Never re-ask facts already supplied by the request or governing +evidence, and never turn clarification into a refusal. +""", + }, + "seriousdevelopment": { + "CHANGELOG.md": r"""<!-- doc: CHANGELOG.md | version: 1.7.1 | updated: 2026-10-11 --> +# Changelog (seriousdevelopment skill) + +| Version | Date | Change | +|---------|------|--------| +| 1.0.0 | 2026-10-11 | Initial skill: modes A/B, method, 6 templates | +| 1.1.0 | 2026-10-11 | Added CHANGELOG + AGENTS.md templates; scoped runs; Mode A self-grade loop; verification mechanics; skill changelog; clarified mode mixing | +| 1.2.0 | 2026-10-11 | Added "Writing decent software" (shape, one-way, data, daily loop, testing, ops, versioning, agent docs distilled from project playbooks) | +| 1.3.0 | 2026-10-11 | Added house stack defaults (FastAPI route tree, Jinja2, SQLite via dataset, plain ES modules, mirrored tests) | +| 1.4.0 | 2026-10-11 | Added logging standards (debug-by-default, rotation caps) and backup procedures (3-2-1, restic default, tested restores) | +| 1.5.0 | 2026-10-11 | Added secrets hands-off rule, dependency minimalism, DTAP coexistence, incident response, minimum CI, migration discipline | +| 1.6.0 | 2026-10-11 | Enforced DRY (hoist on second use), consistency-first with existing code, object-oriented modeling | +| 1.7.0 | 2026-10-11 | Required Podman/Debian container + compose, self-sufficient Makefile, traditional pyproject.toml with declared console binary | +| 1.7.1 | 2026-10-11 | Added author comment to SKILL.md frontmatter | +""", + "SKILL.md": r"""--- +# Author: retoor <retoor@molodetz.nl> +name: seriousdevelopment +description: Scaffold a new project or upgrade an existing one to seriousdevelopment quality (spec-first docs, ADRs, build track sheet, verification gates, living-docs discipline). Use when starting a greenfield project or when an existing project needs its engineering and documentation bar raised. +--- + +# Serious Development + +One method for building software seriously, whether the project is new or +already exists. The method has three pillars: a canonical spec with zero +open gaps, a decision + tracking system that survives multi-month and +multi-model builds, and mechanical verification after every change. + +Skill version: 1.7.1 (history in `CHANGELOG.md`). + +## Modes + +Ask which mode applies if it is not obvious from the request. Do not run +both modes in one run. (Mode B itself is audit-then-apply; that is one +mode, not mixing.) For partial work, see Scoped runs. + +### Mode A: new project + +Scaffold the project skeleton, then write the spec before any product +code. Minimum skeleton: + +- `SPEC.md` (canonical spec, from `assets/templates/SPEC.md`) +- `BUILD-TRACK.md` (from `assets/templates/BUILD-TRACK.md`) +- `docs/adr/` with `0001` recording why this method was adopted +- `docs/runbooks/` (empty, indexed in the spec runbook list) +- `CHANGELOG.md` (spec + product changes, ascending versions) +- `roadmap/` only once the spec passes 500 lines (milestone split, see + "Roadmap split" below) +- `AGENTS.md` (or the harness equivalent) in three layers: personal base + rules, root project rules, nested subsystem files next to the code + +Fill the spec top-down: control + changelog, goals/non-goals, scope, +requirements with normative IDs, architecture, data model, API, security, +compliance, ops, estimates, risks, decisions. No product code until the +spec has zero TBDs (see "No open gaps"). Then grade the spec A-F on: +completeness, consistency, testability, estimation honesty, decision +traceability. Fix everything found without deleting or degrading +content, re-run the mechanical checks, and report before/after grades. +Below A- overall, say so and name what is missing. + +### Mode B: upgrade an existing project + +1. **Audit, read-only.** Inventory: existing docs, rules files, stack, + tests, CI, versioning, changelog habits. Write nothing yet. +2. **Gap report.** List what is missing against the checklist in + "Upgrade checklist" below. Grade the project A-F on: spec + completeness, decision traceability, dev tracking, verification, + doc freshness, estimation honesty. +3. **Apply, smallest diffs first.** Adopt the missing pieces using the + templates. Backfill the spec from the code (code wins on facts, + spec wins on intent; record conflicts, see below). +4. **Upgrade report.** Write `UPGRADE-REPORT.md` (template provided) + listing applied changes, grades before/after, and every conflict. + +### Conflict rule (Mode B, binding) + +If the existing project has a rule that clashes with this method, DO +NOT overwrite it. Keep their rule, mention the clash in the upgrade +report (what clashed, why the method prefers its own, what was kept), +and move on. The only exception is a rule that is illegal or +demonstrably destructive: flag it loudly, propose the fix, and still +wait for the human before changing it. + +### Scoped runs + +Full-mode runs are the default, but partial application is allowed when +the user asks for it: ADRs only, track sheet only, runbooks only. Rules +for scoped runs: run the Mode B audit first (read-only, even for one +piece), record the narrowed scope in the upgrade report, and list what +was deliberately left out so a later run can continue. The conflict +rule still applies. + +## The method + +### No open gaps + +Anything that would normally be "TBD" gets a decided default with +rationale in the decisions section instead. Tracked assumptions are +allowed only with: owner, verify-by date, and a fallback if the +assumption fails. A spec with an untracked open ends the run as +incomplete: say so, do not ship it quietly. + +### Normative IDs + +Every requirement, rule, and surface gets a stable ID from a family +prefix: FR (functional), SEC (security), BIL (billing), OPS (ops), NFR +(non-functional), UX, PG (pages/surfaces), D (decisions), PIN (stack +pins), A (assumptions), TRK/DOC (tracking/docs). IDs are never reused +once retired. Cross-reference by ID, never by prose paraphrase. + +### Caps and floors + +Everything unbounded gets a hard cap: rates, sizes, counts, intervals, +memory. Every automated process gets a floor it may never cross on its +own (retention windows, minimum intervals, protected paths); when a +floor is hit, page a human instead of deleting. Prefer static ceilings +to autotuning. + +### Safety + +Destructive operations are dry-run by default, confirmed explicitly, +and scoped as narrowly as possible. Loud skips beat silent corruption: +reject unverifiable input with a clear reason and a counter. Verify +before you write (checksums, integrity checks, pre-action snapshots). +A delete that does not reclaim is not a delete. + +### Proof + +Measure, do not theorize: numbers in docs come from runs on real +machines. Test through the real path (real filesystem, real +concurrency, failure injection), not just units around it. After +behavior changes, run the suite. After deploys, verify live. + +### Research first, then decide with an opinion + +Load-bearing decisions (stack, vendors, money flows, compliance) start +with web research: log the queries, synthesize into the spec with +normative defaults, keep the log in an appendix. Then decide: a clear +rule with stated tradeoffs beats a configurable maybe. Revisit only +with new evidence, filed as a new ADR (never by editing history). + +### Decisions and stack pins + +Spec-level decisions live in the spec as D-IDs (one line each, with +rationale). Repo-level decisions live as ADRs in `docs/adr/`, one file +per decision (`NNNN-short-title.md`) with a MADR-lite body: Status, +Date, Context, Decision, Consequences, Links. Accepted ADRs are immutable; change +means a new ADR that supersedes. Every D-ID, PIN, or requirement +change needs an ADR number in the PR. Agent prompts MUST cite ADR +constraints, never silently invent new ones. Hedged "X or Y" stack +clauses MUST be pinned to one choice with a PIN-ID before build. + +### Estimates + +Person-weeks with drivers, ranges not theater: each line names what +drives the cost. Recompute totals on every scope change and state the +consequences honestly (date moves, cost moves). Freeze scope with a +D-ID; new ideas go to a later release unless a launch gate forces +them. If the owner overrules the freeze, re-baseline the freeze the +same day and record the new date and cost. Never absorb scope +silently. + +### Changelog and versioning + +One changelog table: version, date, change text, strictly ascending. +Every spec change gets a row. Patch = additions/clarifications, +minor = scope or phasing changes, major = breaking contract changes. +Every markdown doc carries a versioned header: +`<!-- doc: file | version | updated -->`. Project prose rules hold +everywhere (for this author: no em-dashes anywhere, code, docs, +comments, commits, chat). + +### Single source, synced mirrors + +One file is canonical. Split-offs (roadmap, runbooks, API docs) are +mirrors, synced in the SAME commit as the canonical change. Never let +a mirror drift: verify sync mechanically after every edit round. + +### Roadmap split + +When the spec passes ~500 lines, split a build-order roadmap with a +zero-loss proof: account for every source line mechanically (count +them; declared duplicates counted exactly). Each part gets a header +comment naming source file, source version, and covered sections. +Keep a coverage map (every section, exactly one home) and a build +table (order, file, milestone, weeks, exit criteria). + +### Verification gates + +Define launch gates per phase (tests green, pentest criticals zero, +restore drill pass, billing zero-diff over N days, docs synced, legal +pack signed). All green or no launch. After every change round, run +the mechanical checks: sync between canonical and mirrors, ID +uniqueness, changelog order, forbidden-pattern scan, and the repo's +own test suite for the touched area. A confident patch you never saw +pass is not done. Sample mechanics (adapt paths to the project): + +```sh +# ID uniqueness: must print nothing (no duplicate requirement IDs) +rg -o '[A-Z]{2,3}-[0-9]+' SPEC.md | sort | uniq -d +# changelog order: versions must ascend top to bottom +rg '^\| [0-9]+\.[0-9]+' SPEC.md +# forbidden patterns: project rules, e.g. untracked TBDs, em-dashes (U+2014) +rg -n 'TBD|\u2014' SPEC.md BUILD-TRACK.md docs/ +``` + +### Build tracking (multi-month, multi-model safe) + +`BUILD-TRACK.md` at repo root is the single source of build status. +One row per milestone and per gate drill: Milestone, Owner, Status, +Updated, Evidence, Drift. Owner is a human name or +`llm:model/version`. Status enum: NOT-STARTED, IN-PROGRESS, BLOCKED, +IN-REVIEW, DONE. Update triggers, all same-day: any status change, +end of every work session, every gate pass or fail, every merged PR. +Staleness: 7 days without update marks the row STALE (CI fails); +14 days escalates to the owner as BLOCKED. Context passes ONLY +through the track sheet, ADRs, and code, never through chat memory. +LLM output is reviewed by a human or a second model before merge; +billing, auth, and security merges need human sign-off. + +### Living docs + +Maintain a register: doc, owner, update trigger, max staleness. +Minimum register: canonical spec + mirrors (same commit, 0 days), +track sheet (7 days), ADRs (before merge), runbooks (incident/drill ++ 30 days), API changelog (CI-generated, 0 days), cost book (vendor +price change, same day), risks (monthly + incident), postmortems +(48h after SEV-2+). Same-PR rule: a behavior/API/config/ops change +updates its docs in the SAME PR; reviewers verify docs impact; +maintainers block merges with stale docs on user-facing changes. +Generated docs regenerate in CI; drift fails the build. Quarterly +audit: re-verify the register, delete what stopped being true. + +### Craft and ops + +Exact, minimal diffs: edit existing files, reuse helpers, one way of +doing anything. Boring technology, dumb backends, smart edges. Docs +state what the system does today, gaps listed next to the code that +will close them. Commit, deploy, verify, in that order, same +session. Schedules live inside the system on a loop (retention, +cleanup, checks) with human-readable states, not silent hope. + +### Writing decent software + +Shape: one language, one database, server-rendered first, no build +step unless earned. One data root with every path derived in one +config module. Layout mirrors URLs: if you cannot guess the file +from the URL, the layout is wrong. + +House stack (defaults when nothing else is picked; override per +project with an ADR, never by drifting): + +- Backend: Python + FastAPI, one typed framework. Routers mirror the + URL tree: flat file per resource (`routers/feed.py`), package per + sub-resource (`routers/projects/`), each leaf owning an APIRouter, + the package `__init__` aggregating. One app factory, middleware in + fixed order (timing outermost, then security headers, rate limit, + maintenance gate, error handlers). +- Templates: Jinja2 server-side, one shared instance, one `base.html`, + page CSS/JS in standard blocks, shared partials for repeated UI. +- Frontend: no framework, no build step. Plain ES modules, one class + per file, small shared utilities. The server renders what exists at + request time; the client only handles live content, sanitized + fail-closed. +- Database: SQLite via the `dataset` ORM, WAL mode, busy timeout, one + embedded file under the data root for small to medium scale. + Graduate to Postgres only on measured need (write concurrency, + size, or managed backups), filed as an ADR. +- Tests mirror the tree: `tests/unit`, `tests/api` (live server plus + temp DB), `tests/e2e` (browser). + +One way of everything: one auth resolver, one response helper, one +validation style, one outbound HTTP client (with SSRF guard), one +queue pattern. A second implementation of an existing helper is a +bug, not a choice. + +DRY, enforced: no logic in two places. If it appears twice, hoist +it into the one helper, predicate, or constant both callers use. +Single source per fact in code exactly as in docs; copy-paste is +never the answer, not even "just this once". + +Consistency, enforced: new code matches the existing code first. +Same helpers, same patterns, same naming, same file shapes; a +reviewer should not be able to tell which lines are new. If the +existing pattern is bad, either fix all usages in the same change +or follow it and file the cleanup (never a third style alongside). +Read before you write, always. + +Object-oriented, enforced: model the domain as objects with the +behavior living next to the data it acts on. Small classes with one +job, composition over inheritance, no god objects, no procedural +blobs where an object fits. Python: dataclasses and Pydantic models +for data, service objects for operations. Frontend: one class per +file as the house stack already demands. + +Data: opaque IDs, soft delete everywhere with garbage collection +doing the hard deletes, ownership checked by one helper, no N+1 +(batch loaders are the only way to load related rows), idempotent +schema init. + +Daily loop, eight steps in order, a failed step blocks the rest: +understand (match an existing example or agree the new pattern +first), data, server, view, safety, agent+docs (the most-forgotten +step: tool action, endpoint doc, user doc), document, validate. + +Testing: three tiers (unit, HTTP integration on temp DB, browser +E2E), serial runs, full suite green before done, cheapest-first +triage, never narrow a run to pass. For money, state machines, and +read-then-write races add: property checks, stateful fuzzing, +real-process concurrency tests, static analysis on touched files. + +Ops: every repeated action is a named command (no tribal one-liners), +DTAP pipeline with CI green before merge, destructive ops dry-run +first and explicitly confirmed, retention CLIs per expirable domain, +production data guarded by written confirmation, deploy then verify +live. + +Runtime: every project ships a container, Podman preferred, Docker +accepted. Debian-slim-based image, a compose file for the full stack +(app, db, workers), one command to boot it. No "works on my machine": +CI builds the image, prod runs the image. + +Makefile: required and self-sufficient. It assures its own +environment: creates the venv (or pulls images) when missing, +installs pinned deps, then runs every target inside it. Standard +targets: `install`, `dev`, `prod`, `test`, `test-fast`, `lint`, +`clean`, `up`, `down`, `logs`. No target may assume a +hand-prepared machine. + +Python packaging: traditional `pyproject.toml` (setuptools or +hatchling, src layout), version as the single source read live by +the app, one console-script binary declared under +`project.scripts` that is THE way to execute the software (no bare +`python app.py` in docs or deploy). Deps pinned, dev extras +separated. + +Versioning: one version source read live by the app, patch +auto-bumped per commit, minor/major manual release commits, assets +served immutable under versioned URLs, trunk plus short ticket +branches plus a production promotion target, annotated tags and one +changelog for releases. Every markdown doc versioned: header +comment plus history table, bumped in the same commit as the +content change. + +Agent docs: three layers (personal base, root project rules under +~500 lines, nested subsystem files next to the code). Every +statement marked hard rule, convention, or guidance, each with a +named enforcer (test path, hook, review gate). A rule without an +enforcer is a wish: the quarterly audit deletes one. Docs update in +the same change as the code, state today with no fiction, and die +with the feature they describe. + +Logging: stdlib `logging` configured once via `dictConfig` in one +module, `getLogger(__name__)` per module. Debug log by default: +DEBUG goes to a rotating debug file, INFO+ to the console, levels +tunable via env. Rotation is mandatory, never unbounded: +RotatingFileHandler for the app log (10 MB x 5), timed rotation +for debug/audit (midnight, keep 7-14), plus a total cap on the log +directory (default 500 MB) enforced by a prune check. Format: UTC +ISO timestamp, level, logger, message; JSONL only when a shipper +consumes it. QueueHandler for threaded workers; lazy %-formatting +and level guards in hot paths. Never log secrets or PII (token +fingerprints, redaction as backstop); bare except without logging +is forbidden. Files first; stdout only for container runtimes. + +Backups: 3-2-1 minimum (3 copies, 2 media, 1 off-site at a +different provider/location, not the same DC). Anything holding +money or PII gets 3-2-1-1-0 (one immutable copy, zero-error +verified restores). Default tool restic (or Borg): encrypted, +automated on schedule, calendar retention (7 daily, 4 weekly, +6 monthly), prune wired to the same tool. RPO/RTO stated per +domain and measured, not assumed. Test restores monthly into an +empty directory (quarterly minimum); an untested backup fails the +ops gate, and a missing scheduled run pages like any other alarm. +Named commands (`backup-run`, `backup-verify`, `backup-prune`), +keys backed up separately from the data they unlock. + +Secrets are human-only. AI MUST never print, copy, transmit, modify, +rotate, or delete secret values (passwords, tokens, keys, connection +strings), and MUST never commit them. Referencing an env var NAME to +wire config is fine; its VALUE stays unread and unmoved. Servers are +closed from outside by default; secret storage and rotation are the +owner's task. If a secret is missing, expired, or appears where it +should not (logs, diffs, chat), stop and page the human. No +workarounds, ever. + +Dependencies: fewest possible. Stdlib first, one well-chosen library +per need, no new dependency without an ADR entry justifying it (what, +why not stdlib, license, maintenance status). Pinned versions, +automated audit on schedule, CVE fix-or-remove with a deadline. Each +release, remove one dependency nobody can justify. + +Environments: DTAP, usually on one platform. Dev, test, accept, and +prod coexist without colliding: separate database, data dir, ports, +and env-prefixed resource names per environment; no shared state +between test and prod, ever. Test data is marked and isolated; +production data never flows into test. Accept mirrors prod (same +images, same flags). Promotion is deliberate: merge, deploy, verify +live. + +Incidents: every alert names an owner and a runbook; unowned alerts +are forbidden. On-call rotation with a human reachable; agents hold +and page instead of improvising past a floor. SEV-2+ gets a +postmortem within 48h (template provided). Customer-visible incidents +update the status page in the same hour; an incident is not closed +until comms, fix, and action items all landed. + +Minimum CI: lint touched files, serial full suite, doc-header check +(every `.md` carries a version line), generated-docs drift check, +dependency audit. All green before merge, no exceptions. + +Migrations: every schema change ships with its rollback, reviewed +together. Expand before contract (additive first, removal later); +destructive migrations need a snapshot plus a rehearsed restore. +Never edit a migration that already ran in prod; write a new one. + +## Checklists + +### New project + +- [ ] Skeleton files exist from templates +- [ ] Spec: zero untracked opens, IDs on everything, changelog started +- [ ] Stack pins: no "X or Y" clauses remain +- [ ] Engineering shape chosen: layout, data conventions, test tiers, commands +- [ ] Logging + backups: rotation caps set, 3-2-1 scheduled, restore drill dated +- [ ] DTAP coexistence verified: separate state per env, no test/prod sharing +- [ ] Incident loop defined: alert owners, on-call, status page, postmortem rule +- [ ] Assumptions have owner + verify date + fallback +- [ ] Estimates have drivers; scope freeze D-ID recorded +- [ ] Gates defined per phase; first gate (spec review) passed +- [ ] Spec graded A-F and fixed to A- or better, grades reported +- [ ] BUILD-TRACK.md has one row per milestone, all NOT-STARTED +- [ ] ADR 0001 records method adoption + +### Upgrade existing project + +- [ ] Read-only audit complete, nothing written yet +- [ ] Gap report with A-F grades on the six axes +- [ ] Missing pieces applied, smallest diffs first +- [ ] Conflicts kept-as-is and recorded (never overwritten) +- [ ] UPGRADE-REPORT.md written with before/after grades +- [ ] Mechanical checks pass (sync, IDs, changelog, tests) + +## Red flags (stop and say so) + +- A change that cannot name its spec ID or ADR. +- Scope absorbed without re-baselining date and cost. +- A mirror edited without its canonical source (or vice versa). +- Docs updated "later" instead of in the same PR. +- A gate waved through red, or narrowed to pass. +- An agent inventing a new queue, cache, or retry policy the ADRs + never approved. +- AI touching secret values (print, copy, change, delete), or working + around a missing secret instead of paging the human. +- Test and prod sharing state, or prod data flowing into test. +- A migration with no rollback, or an edit to one that already ran. + +## Templates + +In `assets/templates/`: `SPEC.md` (spec skeleton), `BUILD-TRACK.md`, +`ADR.md`, `RUNBOOK.md`, `POSTMORTEM.md`, `UPGRADE-REPORT.md` +(Mode B report with conflicts table), `CHANGELOG.md`, `AGENTS.md` +(root rules file, three-layer header). Copy, never link: the target +project owns its copies from day one. +""", + "assets/templates/ADR.md": r"""<!-- doc: docs/adr/NNNN-short-title.md | version: 1.0 | updated: YYYY-MM-DD --> +# NNNN: <Imperative title, e.g. "Use Postgres RLS for tenancy"> + +- Status: proposed | accepted | superseded by NNNN +- Date: YYYY-MM-DD +- Decision makers: <names> +- Spec IDs: <D-IDs, PINs, FRs touched> + +## Context + +What forced this decision. Facts only, with measurements where the +claim needs one. + +## Decision + +The rule, stated so a stranger can follow it without asking. + +## Consequences + +Good and bad. Include costs, caps/floors introduced, and what this +rules out. + +## Links + +PRs, issues, spec sections, superseded ADRs. +""", + "assets/templates/AGENTS.md": r"""<!-- doc: AGENTS.md | version: 0.1.0 | updated: YYYY-MM-DD --> +# Agent instructions: <project> + +Three layers (separate files, never merged): + +1. Personal base rules (operator-owned, every project): safety, proof, + craft, ops habits. +2. This file (root project rules): applies to the whole repo. Global + rules only; keep it short enough that agents actually read it. +3. Nested subsystem files (next to the code, loaded on touch): deep + mechanics, helpers, pitfalls, worked examples. + +## Root rules + +- Working directory: <repo root>. <scope constraints, e.g. work only here> +- Canonical spec: <SPEC.md>; mirrors sync in the same commit. +- Build status: <BUILD-TRACK.md>; update triggers per the spec tracking section. +- Decisions: <docs/adr/>; cite ADR constraints in every agent prompt. +- Prose rules: <e.g. no em-dashes anywhere: code, docs, comments, commits> +- Verification: <mechanical checks after every change round> +- Commit/deploy/verify order: <project policy> + +## Subsystem files + +<list nested instruction files and what each owns> +""", + "assets/templates/BUILD-TRACK.md": r"""<!-- doc: BUILD-TRACK.md | version: 0.1.0 | updated: YYYY-MM-DD --> +# Build track sheet + +Single source of build status. Status enum: NOT-STARTED, IN-PROGRESS, +BLOCKED, IN-REVIEW, DONE. Update triggers (same-day): status change, +session end, gate pass/fail, merged PR. 7 days stale fails docs CI; +14 days escalates to the owner as BLOCKED. + +| Milestone | Owner | Status | Updated | Evidence | Drift | +|-----------|-------|--------|---------|----------|-------| +| M0 | unassigned | NOT-STARTED | - | - | - | + +## Session history + +One line per work session, newest first: + +- YYYY-MM-DD | human: <name> or llm: <model/version> | goal | outcome | rows touched +""", + "assets/templates/CHANGELOG.md": r"""<!-- doc: CHANGELOG.md | version: 0.1.0 | updated: YYYY-MM-DD --> +# Changelog + +Strictly ascending. One row per change. Patch = additions and +clarifications, minor = scope or phasing changes, major = breaking +contract changes. + +| Version | Date | Change | +|---------|------|--------| +| 0.1.0 | YYYY-MM-DD | Initial changelog | +""", + "assets/templates/POSTMORTEM.md": r"""<!-- doc: docs/postmortems/YYYY-MM-DD-<name>.md | version: 1.0 | updated: YYYY-MM-DD --> +# Postmortem: <incident title> + +- Date: YYYY-MM-DD +- Severity: <SEV-1..4> +- Owner: <name> +- Status: draft | final + +## Timeline (UTC) + +- HH:MM <event, with evidence link> + +## Impact + +<who was affected, for how long, measured numbers> + +## Root cause + +<the cause, not the symptom; five whys if useful> + +## Action items + +| Action | Owner | Due | Done | +|--------|-------|-----|------| +| ... | ... | ... | ... | + +## Lessons + +<what changes in code, runbooks, or docs so this class cannot recur> +""", + "assets/templates/RUNBOOK.md": r"""<!-- doc: docs/runbooks/<name>.md | version: 1.0 | updated: YYYY-MM-DD --> +# Runbook: <name> + +- Trigger: <alert, symptom, or scheduled drill> +- Severity: <SEV-1..4> +- Owner: <role> + +## Steps + +1. <numbered, copy-pasteable, with expected output> + +## Rollback + +<exact undo for every mutating step> + +## Comms template + +<who gets told what, when; status page text if customer-visible> + +## Postmortem owner + +<name or role; SEV-2+ needs a postmortem within 48h> + +## Last drilled + +<date + result; empty means never, which fails the ops gate> +""", + "assets/templates/SPEC.md": r"""<!-- doc: SPEC.md | version: 0.1.0 | updated: YYYY-MM-DD --> +# <Project> - Complete Implementation Document + +- Version: 0.1.0 +- Status: Draft (zero TBDs required before build) +- Date: YYYY-MM-DD +- Owner: <name> + +## 0. Document control + +### 0.1 Conventions + +- Normative IDs: FR, SEC, BIL, OPS, NFR, UX, PG, D, PIN, A, TRK, DOC. + IDs are never reused once retired. Cross-reference by ID. +- No open gaps: anything that would be "TBD" gets a decided default + with rationale in Decisions, or a tracked assumption (owner + + verify-by + fallback) below. + +### 0.2 Assumptions (tracked, all need fallback) + +| ID | Assumption | Owner | Verify by | Fallback | +|----|-----------|-------|-----------|----------| +| A-01 | ... | ... | ... | ... | + +### 0.3 Hard global caps + +| Cap | Value | Enforced at | +|-----|-------|-------------| +| ... | ... | server-side | + +### 0.4 Changelog (ascending, one row per change) + +| Version | Date | Change | +|---------|------|--------| +| 0.1.0 | YYYY-MM-DD | Initial spec skeleton | + +## 1. Goals and non-goals + +- G-01..: measurable goals. +- NG-01..: explicit non-goals (decided, not deferred). + +## 2. Scope (personas, use cases) + +## 3. Requirements (FR/SEC/BIL/OPS/NFR/UX with IDs) + +## 4. Surfaces (PG inventory if user-facing) + +## 5. Architecture (pins as PIN-IDs, no "X or Y") + +## 6. Data model, API, security, compliance, ops + +## 7. Estimates (person-weeks with drivers) + +| Area | PW | Driver | +|------|----|--------| +| ... | ... | ... | + +## 8. Risks and mitigations + +## 9. Decisions (D-IDs, one line + rationale each) + +- D-01: ... + +## Appendix A. Research log (queries + dates + synthesis pointer) +""", + "assets/templates/UPGRADE-REPORT.md": r"""<!-- doc: UPGRADE-REPORT.md | version: 1.0 | updated: YYYY-MM-DD --> +# Upgrade report: seriousdevelopment applied (Mode B) + +- Date: YYYY-MM-DD +- Applied by: <human or llm:model/version> +- Scope: <repo + commit range> + +## Grades (A-F, before -> after) + +| Axis | Before | After | +|------|--------|-------| +| Spec completeness | | | +| Decision traceability | | | +| Dev tracking | | | +| Verification | | | +| Doc freshness | | | +| Estimation honesty | | | + +## Applied changes + +<list files created/changed, smallest diffs first, with one-line why> + +## Conflicts (existing rule kept, method rule NOT applied) + +| # | Existing rule (kept) | Method preference | Why kept | Location | +|---|---------------------|-------------------|----------|----------| +| 1 | ... | ... | ... | ... | + +Rule: a clash never overwrites the project's own rule. Illegal or +demonstrably destructive rules are flagged loudly and changed only +with explicit human approval (record the approval here). + +## Follow-ups + +<ordered list of what the project should do next, with owners> +""", + }, + "taste": { + "SKILL.md": r"""--- +name: taste +description: Mandatory preflight for web frontend visual design. If available, load the taste skill (load_skill) before the first frontend create or edit tool call whenever creating or visually styling a human-viewed web page, component, app, dashboard, or interactive report. Do not load it for non-visual frontend logic or machine-only HTML. If unavailable, continue without warning. Negative constraints only; user requests and other explicit styles still win. +user-invocable: false +--- + +# Anti-Slop: Frontend DON'Ts + +What NOT to do when designing a UI or page. This is a filter, not a style; it never prescribes a look. Stacking several of the below is what reads as AI-made on sight. Adaptive, so if the user or another skill asks for something specific, do that instead; these are defaults, not overrides. + +- purple / indigo / violet accent, or a purple-to-blue hero gradient (the top AI tell) +- `bg-clip-text` gradient headline text +- cream (`#faf8f4`) as the whole-page background +- aurora blobs / glowing colored shadows, or an identical glow on every card +- Inter everywhere, or Geist / Space Grotesk / Instrument Serif / Roboto / Arial as your only identity +- gray-400 / gray-500 body text +- one font weight and size throughout (no hierarchy) +- tracked all-caps micro-kickers above every section +- centered hero of eyebrow pill + headline + two CTAs +- the identical 3-card feature grid with a lucide icon in a rounded square +- an 8+ word, 48px+ headline that says nothing +- bento grid for everything +- the same eyebrow-headline-3cards section repeated back to back +- `rounded-2xl`, or any single radius, on every element +- glassmorphism blur + border + shadow on every surface +- `hover:scale-105` lift on cards +- emoji as feature icons (🚀 ⚡ ✨ 🎯 💡) +- left-border accent cards, a colored border on a rounded element, or cards nested inside cards +- fade-up / slide-up on everything as it scrolls in +- motion that ignores `prefers-reduced-motion` +- scribbly hand-drawn SVG mascots with no purpose +""", + }, +} + + +BUNDLED_SKILL_UPSTREAM = { + "deep-research": "muse-user/deep-research, adapted (fork/poll, web_fetch, no em dashes)", + "git": "muse-bundled/git, adapted (load_skill paths, tai authorization note)", + "plan": "muse-bundled/plan, adapted (load_skill, ask_user, fork/poll)", + "requirements-clarification": "muse-bundled/requirements-clarification, adapted (ask_user, single-line description)", + "seriousdevelopment": "muse-user/seriousdevelopment v1.7.1, verbatim", + "taste": "muse-bundled/taste, one-word adaptation (load_skill)", +} + + def skill_catalog(skills): lines = [] if skills: @@ -15621,7 +17540,10 @@ class Tools: return "error: unknown target, valid: %s" % ", ".join(INSTALL_TARGETS) if action != "status" and not self.app.ask_approval("install %s: %s (vault always kept)" % (action, ", ".join(targets))): return "denied by user" - return install_report(action, targets, self.app.config.home, self.app.store) + report = install_report(action, targets, self.app.config.home, self.app.store) + if action != "status" and "skills" in targets: + self.app.skills = discover_skills(self.app.config.home, os.getcwd()) + return report def run_search(self, args): query = str(args.get("query") or "").strip() @@ -17973,6 +19895,15 @@ def status_lines(home, store=None): lines.append("container: running, %s" % box_venv_state(engine)) else: lines.append("container: %s" % state) + present = [name for name in sorted(BUNDLED_SKILLS) if os.path.isfile(os.path.join(home, "skills", name, "SKILL.md"))] + missing = [name for name in sorted(BUNDLED_SKILLS) if name not in present] + modified = [name for name in present if not bundled_skill_pristine(home, name)] + detail = "skills: %d/%d bundled present" % (len(present), len(BUNDLED_SKILLS)) + if missing: + detail += ", missing: %s" % ", ".join(missing) + if modified: + detail += ", modified: %s" % ", ".join(modified) + lines.append(detail) lines.append(vault_line(home, store)) return lines @@ -18197,6 +20128,190 @@ def op_container(mode, _home): return "container: %s%s" % (state, detail) +def bundled_skill_digest(text): + return hashlib.sha256(text.encode("utf-8")).hexdigest()[:8] + + +def bundled_skill_pristine(home, name): + folder = os.path.join(home, "skills", name) + for rel, expected in BUNDLED_SKILLS[name].items(): + path = os.path.join(folder, rel) + try: + with open(path, "r", encoding="utf-8") as handle: + if handle.read() != expected: + return False + except OSError: + return False + return True + + +def write_bundled_skill(home, name, force): + """Write one bundled skill's files. Returns (written, kept, errors, overwritten). + + A missing file is written; an equal file is left; a differing file is kept + untouched unless force, and reported loudly. User-added extra files are + never touched under any mode.""" + base = os.path.join(home, "skills") + folder = os.path.join(base, name) + files = BUNDLED_SKILLS[name] + written, kept, errors, overwritten = [], [], [], [] + try: + os.makedirs(folder, exist_ok=True) + except OSError as exc: + return written, kept, ["%s: %s" % (name, short_error(exc))], overwritten + for rel in sorted(files): + path = os.path.join(folder, rel) + differ = False + try: + if os.path.isfile(path): + with open(path, "r", encoding="utf-8") as handle: + if handle.read() == files[rel]: + continue + if not force: + kept.append(rel) + continue + differ = True + elif os.path.dirname(path): + os.makedirs(os.path.dirname(path), exist_ok=True) + except OSError as exc: + errors.append("%s/%s: %s" % (name, rel, short_error(exc))) + continue + try: + with open(path, "w", encoding="utf-8") as handle: + handle.write(files[rel]) + except OSError as exc: + errors.append("%s/%s: %s" % (name, rel, short_error(exc))) + continue + written.append(rel) + if differ: + overwritten.append(rel) + if not errors and not kept: + meta = { + "source": "tai-bundled", "mode": "bundled", "scope": "home", + "upstream": BUNDLED_SKILL_UPSTREAM[name], "bundled_version": VERSION, + "files": dict((rel, bundled_skill_digest(files[rel])) for rel in sorted(files)), + "installed_at": datetime.now(timezone.utc).isoformat(), + } + try: + save_skill_install_meta(base, name, meta) + except OSError as exc: + errors.append("%s sidecar: %s" % (name, short_error(exc))) + return written, kept, errors, overwritten + + +def remove_bundled_skill(home, name): + """Remove one bundled skill's tracked files. Returns (removed, leftovers, errors). + + Tracked files that still match the bundled content (or the sidecar hash) go; + user-modified content and user-added files stay and are reported loudly.""" + base = os.path.join(home, "skills") + folder = os.path.join(base, name) + files = BUNDLED_SKILLS[name] + meta = load_skill_install_meta(base, name) or {} + tracked = meta.get("files") or {} + removed, leftovers, errors = [], [], [] + if not os.path.isdir(folder): + sidecar = skill_install_meta_path(base, name) + if os.path.isfile(sidecar): + try: + os.remove(sidecar) + except OSError as exc: + errors.append("%s sidecar: %s" % (name, short_error(exc))) + return removed, leftovers, errors + for rel in sorted(files): + path = os.path.join(folder, rel) + if not os.path.isfile(path): + continue + try: + with open(path, "r", encoding="utf-8") as handle: + current = handle.read() + except OSError as exc: + errors.append("%s/%s: %s" % (name, rel, short_error(exc))) + continue + if current == files[rel] or bundled_skill_digest(current) == tracked.get(rel): + try: + os.remove(path) + removed.append(rel) + except OSError as exc: + errors.append("%s/%s: %s" % (name, rel, short_error(exc))) + else: + leftovers.append(rel) + for root, _dirs, found in os.walk(folder): + for entry in found: + full = os.path.join(root, entry) + if os.path.relpath(full, folder) not in files: + leftovers.append(os.path.relpath(full, folder)) + if not leftovers and not errors: + try: + shutil.rmtree(folder) + except OSError as exc: + errors.append("%s: %s" % (name, short_error(exc))) + return removed, leftovers, errors + sidecar = skill_install_meta_path(base, name) + if os.path.isfile(sidecar): + try: + os.remove(sidecar) + except OSError as exc: + errors.append("%s sidecar: %s" % (name, short_error(exc))) + return removed, leftovers, errors + + +def op_skills(mode, home): + names = sorted(BUNDLED_SKILLS) + if mode == "uninstall": + gone, kept, errors = [], [], [] + for name in names: + folder = os.path.join(home, "skills", name) + sidecar = skill_install_meta_path(os.path.join(home, "skills"), name) + if not os.path.isdir(folder) and not os.path.isfile(sidecar): + continue + _removed, leftovers, errs = remove_bundled_skill(home, name) + errors += errs + if leftovers: + kept.append("%s (%s)" % (name, ", ".join(sorted(leftovers)))) + elif not errs: + gone.append(name) + if not gone and not kept and not errors: + return "skills: not present" + parts = [] + if gone: + parts.append("removed %d (%s)" % (len(gone), ", ".join(gone))) + if kept: + parts.append("kept modified %d (%s)" % (len(kept), "; ".join(kept))) + if errors: + parts.append("errors: %s" % "; ".join(errors)) + return "skills: %s" % ", ".join(parts) + outcomes = {"installed": [], "refreshed": [], "reinstalled": [], "current": [], "kept": [], "errors": []} + for name in names: + folder = os.path.join(home, "skills", name) + existed = os.path.isfile(os.path.join(folder, "SKILL.md")) + written, kept, errs, overwritten = write_bundled_skill(home, name, force=(mode == "reinstall")) + outcomes["errors"] += errs + if kept: + outcomes["kept"].append("%s (%s)" % (name, ", ".join(kept))) + elif mode == "reinstall" and (written or overwritten): + outcomes["reinstalled"].append(name) + elif written and not existed: + outcomes["installed"].append(name) + elif written: + outcomes["refreshed"].append(name) + elif not errs: + outcomes["current"].append(name) + parts = [] + for key in ("installed", "refreshed", "reinstalled"): + if outcomes[key]: + parts.append("%s %d (%s)" % (key, len(outcomes[key]), ", ".join(outcomes[key]))) + if outcomes["current"]: + parts.append("already current %d (%s)" % (len(outcomes["current"]), ", ".join(outcomes["current"]))) + if outcomes["kept"]: + parts.append("kept modified %d (%s)" % (len(outcomes["kept"]), "; ".join(outcomes["kept"]))) + if outcomes["errors"]: + parts.append("errors: %s" % "; ".join(outcomes["errors"])) + if len(parts) == 1 and len(outcomes["current"]) == len(names): + return "skills: already present (%d)" % len(names) + return "skills: %s" % ", ".join(parts) + + def install_report(action, targets, home, store=None): if action == "status": return "\n".join(status_lines(home, store)) @@ -18224,6 +20339,8 @@ def install_report(action, targets, home, store=None): lines.append(op_telegram_service(action, home)) elif target == "container": lines.append(op_container(action, home)) + elif target == "skills": + lines.append(op_skills(action, home)) lines.append(vault_line(home, store)) return "\n".join(lines) @@ -18318,6 +20435,7 @@ def install_self(): print(op_fish_hook("install", home)) rc = "~/.config/fish/config.fish" print(op_venv("install", home)) + print(op_skills("install", home)) print("restart your shell or run: source %s" % rc) return 0 diff --git a/test_tai.py b/test_tai.py index c8d9cb3..dc29a62 100644 --- a/test_tai.py +++ b/test_tai.py @@ -1956,7 +1956,7 @@ class InstallOpsTests(unittest.TestCase): report = tai.install_report("status", list(tai.INSTALL_TARGETS), self.config.home, self.store) for label in ( "binary:", "bash-hook:", "zsh-hook:", "fish-hook:", "plexer-hook:", "zsh-plexer-hook:", - "fish-plexer-hook:", "venv:", "scheduler-service:", "telegram-service:", "container:", "vault:", + "fish-plexer-hook:", "venv:", "scheduler-service:", "telegram-service:", "container:", "skills:", "vault:", ): self.assertIn(label, report) self.assertIn("not installed", report) @@ -2182,6 +2182,154 @@ class InstallOpsTests(unittest.TestCase): self.assertIn("use /install", printed) +class BundledSkillsTests(unittest.TestCase): + NAMES = ("deep-research", "seriousdevelopment", "git", "plan", "requirements-clarification", "taste") + + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.home = os.path.join(self.tmp.name, "home") + os.makedirs(self.home) + self.env = mock.patch.dict(os.environ, {"HOME": self.home, "TAI_HOME": os.path.join(self.home, ".tai")}) + self.env.start() + + class FakeArgs: + profile = "t" + yes = True + + self.config = tai.Config(FakeArgs()) + self.store = tai.Store(self.config, tai.Seal(self.config.home, "skills-test-1")) + self.app = FakeSecretApp(self.store) + self.app.config = self.config + self.app.skills = {} + self.tools = tai.Tools(self.app) + + def tearDown(self): + self.store.close() + self.env.stop() + self.tmp.cleanup() + os.environ.pop("TAI_HOME", None) + + def skill_path(self, name, rel="SKILL.md"): + return os.path.join(self.config.home, "skills", name, rel) + + def test_embedded_skills_parse(self): + self.assertEqual(sorted(tai.BUNDLED_SKILLS), sorted(self.NAMES)) + for name in self.NAMES: + files = tai.BUNDLED_SKILLS[name] + self.assertIn("SKILL.md", files) + skill = tai.parse_skill_front(files["SKILL.md"].split("\n---", 1)[0].splitlines()[1:]) + self.assertEqual(skill.get("name"), name) + self.assertTrue(len(skill.get("description", "")) > 100, name) + self.assertTrue(len(files["SKILL.md"]) > 1500, name) + + def test_embedded_content_guards(self): + for name in self.NAMES: + self.assertIn(name, tai.BUNDLED_SKILL_UPSTREAM) + self.assertTrue(tai.BUNDLED_SKILL_UPSTREAM[name], name) + for rel, text in tai.BUNDLED_SKILLS[name].items(): + label = "%s/%s" % (name, rel) + self.assertNotIn("\u2014", text, label) + self.assertNotIn('"""', text, label) + self.assertNotIn("read_skill", text, label) + self.assertNotIn("request_user_input", text, label) + self.assertNotIn("Claude Code", text, label) + self.assertNotIn("curl -", text, label) + self.assertNotIn("curl commands", text, label) + self.assertNotIn("`/plan`", text, label) + self.assertNotIn("/skill plan", text, label) + + def test_install_discovers_all(self): + report = tai.op_skills("install", self.config.home) + self.assertIn("installed 6", report) + for name in self.NAMES: + self.assertIn(name, report) + self.assertTrue(os.path.isfile(self.skill_path(name)), name) + skills = tai.discover_skills(self.config.home, self.tmp.name) + self.assertEqual(sorted(skills), sorted(self.NAMES)) + meta = tai.load_skill_install_meta(os.path.join(self.config.home, "skills"), "git") + self.assertEqual(meta["mode"], "bundled") + self.assertEqual(meta["scope"], "home") + self.assertEqual(meta["source"], "tai-bundled") + self.assertEqual(len(meta["files"]), len(tai.BUNDLED_SKILLS["git"])) + self.assertEqual(tai.skill_provenance_note(skills["git"]), "[installed bundled, home scope, from tai-bundled]") + extras = tai.skill_extra_files(os.path.join(self.config.home, "skills", "git")) + self.assertEqual(extras, [os.path.join(self.config.home, "skills", "git", "scripts", "workspace-recovery.sh")]) + + def test_install_idempotent_and_never_overwrites(self): + tai.op_skills("install", self.config.home) + path = self.skill_path("taste") + with open(path, "w", encoding="utf-8") as handle: + handle.write("user rewrite") + report = tai.op_skills("install", self.config.home) + self.assertNotIn("installed 6", report) + with open(path, encoding="utf-8") as handle: + self.assertEqual(handle.read(), "user rewrite") + + def test_upgrade_keeps_modified_loudly(self): + tai.op_skills("install", self.config.home) + path = self.skill_path("taste") + with open(path, "w", encoding="utf-8") as handle: + handle.write("user rewrite") + report = tai.op_skills("upgrade", self.config.home) + self.assertIn("already current 5", report) + self.assertIn("kept modified 1 (taste (SKILL.md))", report) + with open(path, encoding="utf-8") as handle: + self.assertEqual(handle.read(), "user rewrite") + self.assertIn("modified: taste", "\n".join(tai.status_lines(self.config.home, self.store))) + + def test_reinstall_refreshes_but_keeps_extras(self): + tai.op_skills("install", self.config.home) + path = self.skill_path("taste") + with open(path, "w", encoding="utf-8") as handle: + handle.write("user rewrite") + extra = os.path.join(self.config.home, "skills", "taste", "mynotes.md") + with open(extra, "w", encoding="utf-8") as handle: + handle.write("mine") + report = tai.op_skills("reinstall", self.config.home) + self.assertIn("reinstalled 1 (taste)", report) + self.assertTrue(tai.bundled_skill_pristine(self.config.home, "taste")) + self.assertTrue(os.path.isfile(extra)) + + def test_uninstall_keeps_user_content(self): + tai.op_skills("install", self.config.home) + path = self.skill_path("git") + with open(path, "w", encoding="utf-8") as handle: + handle.write("user rewrite") + extra = os.path.join(self.config.home, "skills", "git", "mynotes.md") + with open(extra, "w", encoding="utf-8") as handle: + handle.write("mine") + report = tai.op_skills("uninstall", self.config.home) + self.assertIn("removed 5", report) + self.assertIn("kept modified 1 (git (SKILL.md, mynotes.md))", report) + self.assertTrue(os.path.isfile(path)) + self.assertTrue(os.path.isfile(extra)) + self.assertTrue(os.path.isfile(tai.skill_install_meta_path(os.path.join(self.config.home, "skills"), "git"))) + self.assertFalse(os.path.isdir(os.path.join(self.config.home, "skills", "taste"))) + report = tai.op_skills("uninstall", self.config.home) + self.assertIn("kept modified 1", report) + os.remove(path) + os.remove(extra) + self.assertIn("removed 1 (git)", tai.op_skills("uninstall", self.config.home)) + self.assertIn("not present", tai.op_skills("uninstall", self.config.home)) + + def test_status_counts(self): + report = "\n".join(tai.status_lines(self.config.home, self.store)) + self.assertIn("skills: 0/6 bundled present, missing: deep-research, git, plan", report) + tai.op_skills("install", self.config.home) + report = "\n".join(tai.status_lines(self.config.home, self.store)) + self.assertIn("skills: 6/6 bundled present", report) + self.assertNotIn("missing:", report.split("skills:")[1].split("\n")[0]) + + def test_run_install_refreshes_live_skills(self): + self.assertEqual(self.app.skills, {}) + report = self.tools.dispatch("install", json.dumps({"action": "install", "targets": ["skills"]})) + self.assertIn("installed 6", report) + self.assertEqual(sorted(self.app.skills), sorted(self.NAMES)) + report = self.tools.dispatch("install", json.dumps({"action": "uninstall", "targets": ["skills"]})) + self.assertIn("removed 6", report) + self.assertEqual(self.app.skills, {}) + + class BoxCapsTests(unittest.TestCase): def setUp(self): tai._BOX_CAPS_STAMP["done"] = False