forked from bots-garden/ori
🎉 Begin a project.
4edda86 added
.claude/CLAUDE.md +81 -0 | new file mode 100644 | ||
| @@ -0,0 +1,81 @@ | ||
| 1 | +# Working in this sandbox | |
| 2 | + | |
| 3 | +Instructions installed by the `dev-toolkit` kit. They apply to **every** session, whatever the project. | |
| 4 | + | |
| 5 | +## Always start by reading the project memory | |
| 6 | + | |
| 7 | +Before planning, before asking the user anything, before touching a file — check for a `.memory/` directory at the repository root: | |
| 8 | + | |
| 9 | +```bash | |
| 10 | +ls .memory/ 2>/dev/null | |
| 11 | +``` | |
| 12 | + | |
| 13 | +**If it exists, read it before doing anything else:** | |
| 14 | + | |
| 15 | +| Read | Why | | |
| 16 | +| --- | --- | | |
| 17 | +| `.memory/summary.md` | What the project is, its architecture, the decisions currently in force, the real build/test/run commands. | | |
| 18 | +| the tail of `.memory/history.md` | What has already been done, and why it was done that way. | | |
| 19 | +| the most recent file in `.memory/handoffs/` | Where the last session stopped: work in flight, next steps, blockers, traps. | | |
| 20 | + | |
| 21 | +Then, before you propose anything: | |
| 22 | + | |
| 23 | +- **Do not ask the user for information that is already recorded there.** Re-deriving state that is written down wastes the user's time and is the main reason this record exists. | |
| 24 | +- **Do not re-litigate a decision recorded in `summary.md`** without saying you are doing so and why. If you think a decision in force is wrong, say so explicitly and let the user decide. | |
| 25 | +- **Surface unfinished work.** If a handoff records work in flight, report it and ask whether to resume it or start something new — do not silently start something else on top of it. | |
| 26 | +- **Reuse the commands the memory documents.** Do not invent a parallel way to build or test a project that already has one. | |
| 27 | + | |
| 28 | +If `.memory/` does not exist yet, carry on — you will create it when you write your handoff (see below). | |
| 29 | + | |
| 30 | +## Always finish by updating the project memory | |
| 31 | + | |
| 32 | +**Every session ends with all three files below brought up to date. No exceptions** — not for a one-line fix, not for a session that only read code, not for a session that failed to achieve anything. | |
| 33 | + | |
| 34 | +You cannot detect the moment a session actually ends, so use this trigger instead: **do this as the last action before you hand control back, having finished or abandoned what the user asked for.** If the work then continues, extend what you already wrote instead of adding a second set of entries. | |
| 35 | + | |
| 36 | +Two rules govern all three files. **Match the length to the work** — a one-line fix earns a few lines, not a filled-in template; padding with "N/A" makes the record unreadable, which is the one way this mandate defeats itself. And **record what the next person cannot re-derive** — where you stopped, what you tried that failed, the trap you hit, the decision still waiting on the user. Not a diff summary; git already has that. | |
| 37 | + | |
| 38 | +### 1. `summary.md` — edit in place, never regenerate | |
| 39 | + | |
| 40 | +- **Change only the parts your session actually establishes or invalidates.** Leave the rest byte-for-byte alone. You have seen a slice of this project; the file holds what every previous session established, and rewriting it wholesale from your slice silently destroys accurate content you never looked at. This is the single biggest risk in this whole mandate. | |
| 41 | +- **Only from what you verified** — a command you ran, a file you read. Never a plausible guess. | |
| 42 | +- **If nothing about the project's state changed, leave the file untouched.** That is what "update" means; it is not an exception to the mandate. | |
| 43 | +- What you could not check goes under an explicit `## Not yet established` heading. A summary that states unknowns as unknown is useful; one that states guesses as fact is worse than none, because the next session will trust it. | |
| 44 | + | |
| 45 | +### 2. `history.md` — append exactly one entry | |
| 46 | + | |
| 47 | +- One dated entry per session, appended at the end. **Never rewrite or delete an existing entry**, including your own from an earlier turn. | |
| 48 | +- Cover: what was asked, what changed, decisions made and alternatives rejected, tests, quality gate result, docs touched — dropping the lines that have no content. | |
| 49 | +- A read-only or failed session gets an entry too. One line stating that, and why, is a complete entry. | |
| 50 | + | |
| 51 | +### 3. `handoffs/YYYY-MM-DD-<slug>.md` — write or update today's | |
| 52 | + | |
| 53 | +- The slug names the topic. If a file for today already covers this topic, **update it**; if today's work is a different topic, write a new file under a different slug. Never overwrite another session's handoff. | |
| 54 | +- **A session that achieved nothing still gets a handoff**, and it is one of the valuable ones: it stops the next agent walking into the same wall. | |
| 55 | + | |
| 56 | +### If `.memory/` does not exist | |
| 57 | + | |
| 58 | +Create the whole skeleton — `README.md`, `summary.md`, `history.md`, and `handoffs/` with your handoff in it. All four, even if your session touched one file, so a later session finds a record to read and extend rather than a bare directory. `summary.md` is then written under the same rules as above: only what you verified, the rest under `## Not yet established`. `history.md` starts with its first dated entry: this session. | |
| 59 | + | |
| 60 | +The full templates for all three files are in the `methodical-dev` skill (Phase 7). Use them as a menu, not a form to fill in. | |
| 61 | + | |
| 62 | +## Keep the memory true | |
| 63 | + | |
| 64 | +`.memory/` is **committed to the repository**, not gitignored. It only stays useful if it stays accurate, and the three files stay accurate in different ways: `summary.md` is a **snapshot** — it describes the present and carries no history. `history.md` and `handoffs/` are the **record** — they only ever grow, and are never tidied up, condensed, or corrected after the fact. A history you edit is not a history. | |
| 65 | + | |
| 66 | +`.memory/` is for whoever *continues building* the project. `docs/` is for whoever *uses* it. Keep the two apart rather than duplicating content between them. | |
| 67 | + | |
| 68 | +## Skills installed by this kit | |
| 69 | + | |
| 70 | +Prefer these over doing the same work by hand: | |
| 71 | + | |
| 72 | +| Skill | Use it for | | |
| 73 | +| --- | --- | | |
| 74 | +| `methodical-dev` | Any non-trivial change. Approval-gated: requirements → plan → implementation with tests → quality gate → EN+FR docs → `.memory/` update. Start here. | | |
| 75 | +| `quality` | Measuring code quality with qlty and refactoring until the gate passes. Never invoke qlty ad hoc instead. | | |
| 76 | +| `diataxis-doc` | Writing or restructuring documentation with the Diátaxis four-quadrant method. | | |
| 77 | + | |
| 78 | + | |
| 79 | +Never make a check pass by weakening it — no new `exclude_patterns`, no lowered thresholds, no disabled plugins, no deleted or skipped tests. Fix the code, or report that you could not. | |
| 80 | + | |
| 81 | +The `quality` skill defines two narrow exceptions, both conditioned on evidence and on the user's agreement: excluding a vendored `kits/` directory (the measurement tool cannot measure itself), and removing a plugin proven to analyse nothing. Neither is a licence to trim a number — read the skill before invoking either, and never widen them. | |
| new file mode 100644 | |||
| @@ -0,0 +1,81 @@ | |||
| 1 | +# Working in this sandbox | ||
| 2 | + | ||
| 3 | +Instructions installed by the `dev-toolkit` kit. They apply to **every** session, whatever the project. | ||
| 4 | + | ||
| 5 | +## Always start by reading the project memory | ||
| 6 | + | ||
| 7 | +Before planning, before asking the user anything, before touching a file — check for a `.memory/` directory at the repository root: | ||
| 8 | + | ||
| 9 | +```bash | ||
| 10 | +ls .memory/ 2>/dev/null | ||
| 11 | +``` | ||
| 12 | + | ||
| 13 | +**If it exists, read it before doing anything else:** | ||
| 14 | + | ||
| 15 | +| Read | Why | | ||
| 16 | +| --- | --- | | ||
| 17 | +| `.memory/summary.md` | What the project is, its architecture, the decisions currently in force, the real build/test/run commands. | | ||
| 18 | +| the tail of `.memory/history.md` | What has already been done, and why it was done that way. | | ||
| 19 | +| the most recent file in `.memory/handoffs/` | Where the last session stopped: work in flight, next steps, blockers, traps. | | ||
| 20 | + | ||
| 21 | +Then, before you propose anything: | ||
| 22 | + | ||
| 23 | +- **Do not ask the user for information that is already recorded there.** Re-deriving state that is written down wastes the user's time and is the main reason this record exists. | ||
| 24 | +- **Do not re-litigate a decision recorded in `summary.md`** without saying you are doing so and why. If you think a decision in force is wrong, say so explicitly and let the user decide. | ||
| 25 | +- **Surface unfinished work.** If a handoff records work in flight, report it and ask whether to resume it or start something new — do not silently start something else on top of it. | ||
| 26 | +- **Reuse the commands the memory documents.** Do not invent a parallel way to build or test a project that already has one. | ||
| 27 | + | ||
| 28 | +If `.memory/` does not exist yet, carry on — you will create it when you write your handoff (see below). | ||
| 29 | + | ||
| 30 | +## Always finish by updating the project memory | ||
| 31 | + | ||
| 32 | +**Every session ends with all three files below brought up to date. No exceptions** — not for a one-line fix, not for a session that only read code, not for a session that failed to achieve anything. | ||
| 33 | + | ||
| 34 | +You cannot detect the moment a session actually ends, so use this trigger instead: **do this as the last action before you hand control back, having finished or abandoned what the user asked for.** If the work then continues, extend what you already wrote instead of adding a second set of entries. | ||
| 35 | + | ||
| 36 | +Two rules govern all three files. **Match the length to the work** — a one-line fix earns a few lines, not a filled-in template; padding with "N/A" makes the record unreadable, which is the one way this mandate defeats itself. And **record what the next person cannot re-derive** — where you stopped, what you tried that failed, the trap you hit, the decision still waiting on the user. Not a diff summary; git already has that. | ||
| 37 | + | ||
| 38 | +### 1. `summary.md` — edit in place, never regenerate | ||
| 39 | + | ||
| 40 | +- **Change only the parts your session actually establishes or invalidates.** Leave the rest byte-for-byte alone. You have seen a slice of this project; the file holds what every previous session established, and rewriting it wholesale from your slice silently destroys accurate content you never looked at. This is the single biggest risk in this whole mandate. | ||
| 41 | +- **Only from what you verified** — a command you ran, a file you read. Never a plausible guess. | ||
| 42 | +- **If nothing about the project's state changed, leave the file untouched.** That is what "update" means; it is not an exception to the mandate. | ||
| 43 | +- What you could not check goes under an explicit `## Not yet established` heading. A summary that states unknowns as unknown is useful; one that states guesses as fact is worse than none, because the next session will trust it. | ||
| 44 | + | ||
| 45 | +### 2. `history.md` — append exactly one entry | ||
| 46 | + | ||
| 47 | +- One dated entry per session, appended at the end. **Never rewrite or delete an existing entry**, including your own from an earlier turn. | ||
| 48 | +- Cover: what was asked, what changed, decisions made and alternatives rejected, tests, quality gate result, docs touched — dropping the lines that have no content. | ||
| 49 | +- A read-only or failed session gets an entry too. One line stating that, and why, is a complete entry. | ||
| 50 | + | ||
| 51 | +### 3. `handoffs/YYYY-MM-DD-<slug>.md` — write or update today's | ||
| 52 | + | ||
| 53 | +- The slug names the topic. If a file for today already covers this topic, **update it**; if today's work is a different topic, write a new file under a different slug. Never overwrite another session's handoff. | ||
| 54 | +- **A session that achieved nothing still gets a handoff**, and it is one of the valuable ones: it stops the next agent walking into the same wall. | ||
| 55 | + | ||
| 56 | +### If `.memory/` does not exist | ||
| 57 | + | ||
| 58 | +Create the whole skeleton — `README.md`, `summary.md`, `history.md`, and `handoffs/` with your handoff in it. All four, even if your session touched one file, so a later session finds a record to read and extend rather than a bare directory. `summary.md` is then written under the same rules as above: only what you verified, the rest under `## Not yet established`. `history.md` starts with its first dated entry: this session. | ||
| 59 | + | ||
| 60 | +The full templates for all three files are in the `methodical-dev` skill (Phase 7). Use them as a menu, not a form to fill in. | ||
| 61 | + | ||
| 62 | +## Keep the memory true | ||
| 63 | + | ||
| 64 | +`.memory/` is **committed to the repository**, not gitignored. It only stays useful if it stays accurate, and the three files stay accurate in different ways: `summary.md` is a **snapshot** — it describes the present and carries no history. `history.md` and `handoffs/` are the **record** — they only ever grow, and are never tidied up, condensed, or corrected after the fact. A history you edit is not a history. | ||
| 65 | + | ||
| 66 | +`.memory/` is for whoever *continues building* the project. `docs/` is for whoever *uses* it. Keep the two apart rather than duplicating content between them. | ||
| 67 | + | ||
| 68 | +## Skills installed by this kit | ||
| 69 | + | ||
| 70 | +Prefer these over doing the same work by hand: | ||
| 71 | + | ||
| 72 | +| Skill | Use it for | | ||
| 73 | +| --- | --- | | ||
| 74 | +| `methodical-dev` | Any non-trivial change. Approval-gated: requirements → plan → implementation with tests → quality gate → EN+FR docs → `.memory/` update. Start here. | | ||
| 75 | +| `quality` | Measuring code quality with qlty and refactoring until the gate passes. Never invoke qlty ad hoc instead. | | ||
| 76 | +| `diataxis-doc` | Writing or restructuring documentation with the Diátaxis four-quadrant method. | | ||
| 77 | + | ||
| 78 | + | ||
| 79 | +Never make a check pass by weakening it — no new `exclude_patterns`, no lowered thresholds, no disabled plugins, no deleted or skipped tests. Fix the code, or report that you could not. | ||
| 80 | + | ||
| 81 | +The `quality` skill defines two narrow exceptions, both conditioned on evidence and on the user's agreement: excluding a vendored `kits/` directory (the measurement tool cannot measure itself), and removing a plugin proven to analyse nothing. Neither is a licence to trim a number — read the skill before invoking either, and never widen them. | ||
added
.claude/skills/diataxis-doc/SKILL.md +520 -0 | new file mode 100644 | ||
| @@ -0,0 +1,520 @@ | ||
| 1 | +--- | |
| 2 | +name: diataxis-doc | |
| 3 | +description: Document a project using the Diátaxis methodology (https://diataxis.fr). Organises documentation into four distinct quadrants — Tutorials (learning), How-to guides (solving a task), Reference (looking information up), Explanation (understanding) — along the two axes action/cognition and acquisition/application. Use when creating or restructuring a project's documentation in a rigorous, readable, user-needs-driven way. | |
| 4 | +--- | |
| 5 | +# Diátaxis Documentation Skill | |
| 6 | + | |
| 7 | +## Description | |
| 8 | + | |
| 9 | +Guides the creation and restructuring of a project's documentation according to the | |
| 10 | +**Diátaxis** methodology (Daniele Procida — https://diataxis.fr). Diátaxis is not a rigid | |
| 11 | +template: it is a **compass** that guarantees every documentation page serves **exactly one | |
| 12 | +clearly identified user need**, and that all four fundamental needs are covered. | |
| 13 | + | |
| 14 | +The strength of the method: it solves the three problems of any documentation — | |
| 15 | +**content** (what to write), **style** (how to write it) and **architecture** | |
| 16 | +(how to organise it) — by reducing them to two simple questions. | |
| 17 | + | |
| 18 | +## When to Use | |
| 19 | + | |
| 20 | +- When a project has no documentation, or a "catch-all" documentation | |
| 21 | +- When existing docs mix tutorials, recipes, reference and explanation in the same pages | |
| 22 | +- When users "can't find" the information despite voluminous docs | |
| 23 | +- When you want maintainable documentation that grows in small increments | |
| 24 | +- After adding a feature, to document it in the right place without reorganising everything | |
| 25 | + | |
| 26 | +--- | |
| 27 | + | |
| 28 | +## Core Principle: the two axes | |
| 29 | + | |
| 30 | +The whole method rests on **two orthogonal axes**. They do not merely cover the territory of | |
| 31 | +documentation — they **define** it. This is why there are **necessarily four quadrants**, | |
| 32 | +neither three nor five. | |
| 33 | + | |
| 34 | +``` | |
| 35 | + ACTION (practical know-how) | |
| 36 | + ▲ | |
| 37 | + │ | |
| 38 | + TUTORIALS │ HOW-TO GUIDES | |
| 39 | + (learning by doing) │ (achieving a goal) | |
| 40 | + │ | |
| 41 | + ACQUISITION ────────────────────┼──────────────────── APPLICATION | |
| 42 | + (studying a skill) │ (applying a skill) | |
| 43 | + │ | |
| 44 | + EXPLANATION │ REFERENCE | |
| 45 | + (understanding, thinking) │ (looking information up) | |
| 46 | + │ | |
| 47 | + ▼ | |
| 48 | + COGNITION (theoretical knowledge) | |
| 49 | +``` | |
| 50 | + | |
| 51 | +| Quadrant | Action/cognition axis | Acquisition/application axis | The user's question | | |
| 52 | +|----------|------------------------|-------------------------------|----------------------| | |
| 53 | +| **Tutorial** | Action | Acquisition | "Teach me to get started" | | |
| 54 | +| **How-to guide** | Action | Application | "How do I do X?" | | |
| 55 | +| **Reference** | Cognition | Application | "What are the exact details of X?" | | |
| 56 | +| **Explanation** | Cognition | Acquisition | "Can you explain why / the context?" | | |
| 57 | + | |
| 58 | +--- | |
| 59 | + | |
| 60 | +## The four quadrants in detail | |
| 61 | + | |
| 62 | +### 1. TUTORIALS — learning-oriented | |
| 63 | + | |
| 64 | +A tutorial is a **guided lesson**, an experience under a teacher's direction. Its goal is NOT | |
| 65 | +to accomplish a real task, but to make the beginner **acquire a skill**. The teacher is | |
| 66 | +responsible for the student's success. | |
| 67 | + | |
| 68 | +**Writing rules (mandatory):** | |
| 69 | +- ✅ Announce the **destination** up front ("by the end, you will have built…"), not what will be "learned" | |
| 70 | +- ✅ Produce a **visible result at every step**, however small | |
| 71 | +- ✅ Guarantee **perfect reliability**: every command must work for everyone, every time | |
| 72 | +- ✅ Show the **actual expected output** ("you should see: …") | |
| 73 | +- ✅ Use **"we"** ("we will now…") and the direct imperative ("Type this") | |
| 74 | +- ✅ Flag common mistakes at the point where they can occur | |
| 75 | +- ❌ **Minimise explanation to the extreme** — a tutorial is NOT the place for explanation. Link to it instead. | |
| 76 | +- ❌ **No alternatives, no options, no digressions** — a single path to success | |
| 77 | +- ❌ Do not explain the "why" in detail (it breaks the learning flow) | |
| 78 | + | |
| 79 | +### 2. HOW-TO GUIDES — task-oriented | |
| 80 | + | |
| 81 | +A how-to guide is a **recipe** for reaching a specific goal. It addresses an **already | |
| 82 | +competent** user who knows what they want to do. "How do I deploy?", "How do I enable | |
| 83 | +caching?". | |
| 84 | + | |
| 85 | +> ⚠️ Key distinction from a tutorial: *"Someone who expects a recipe and receives a cooking | |
| 86 | +> lesson will be disappointed and annoyed."* Tutorial = lesson; guide = recipe. | |
| 87 | + | |
| 88 | +**Writing rules:** | |
| 89 | +- ✅ Title = **the task**, stated clearly: "How to {do X}" | |
| 90 | +- ✅ Start from a **real problem**, not from a feature of the tool | |
| 91 | +- ✅ Contain **only the steps needed** for the task | |
| 92 | +- ✅ Stay **adaptable to the real world** ("if you use Y, do this instead") — a guide covers variants | |
| 93 | +- ✅ Assume the user **already knows** what they want to accomplish | |
| 94 | +- ❌ Do not teach the basic concepts (that is the tutorial's job) | |
| 95 | +- ❌ Do not drown the recipe in explanation (link to the explanation) | |
| 96 | + | |
| 97 | +### 3. REFERENCE — information-oriented | |
| 98 | + | |
| 99 | +Reference is a **neutral, exhaustive technical description** of the machinery: the commands, | |
| 100 | +options, parameters, structures. It is a **map of the territory**, austere and reliable, that | |
| 101 | +you **consult** (you do not read it end to end). | |
| 102 | + | |
| 103 | +**Writing rules:** | |
| 104 | +- ✅ **Describe**, do nothing else: no instruction, no explanation, no opinion | |
| 105 | +- ✅ **Accuracy and completeness**: every parameter, default value, type, error | |
| 106 | +- ✅ **Standard, predictable structure**: the same format for every entry (tables, lists) | |
| 107 | +- ✅ **Mirror the product's structure**: the reference's organisation reflects that of the code | |
| 108 | +- ✅ Include usage **examples**, without drifting into explanation | |
| 109 | +- ❌ Do not teach, do not tell the "why" — link to tutorials and explanations | |
| 110 | + | |
| 111 | +> Mental model: a **nutrition label**. Factual, standardised, rigorous, with no recipe and no | |
| 112 | +> marketing pitch. | |
| 113 | + | |
| 114 | +### 4. EXPLANATION — understanding-oriented | |
| 115 | + | |
| 116 | +Explanation **steps back**. It answers "Can you tell me about…?". It connects concepts, gives | |
| 117 | +context, history, design decisions, rejected alternatives. It is the documentation you read | |
| 118 | +"in the bath", away from active work. | |
| 119 | + | |
| 120 | +**Writing rules:** | |
| 121 | +- ✅ **Make connections** between topics, take a wider view | |
| 122 | +- ✅ Give the **context**: history, decisions, constraints, rejected alternatives | |
| 123 | +- ✅ Discuss the **why**, the trade-offs, the possibilities | |
| 124 | +- ✅ **Allow opinion** and acknowledge multiple points of view | |
| 125 | +- ✅ **Stay bounded** to one topic, so it does not absorb content that belongs elsewhere | |
| 126 | +- ❌ Do not give step-by-step instructions (that is the guides'/tutorials' job) | |
| 127 | +- ❌ Do not become a reference (no exhaustive parameter lists) | |
| 128 | + | |
| 129 | +--- | |
| 130 | + | |
| 131 | +## The Compass — choosing the right quadrant | |
| 132 | + | |
| 133 | +When you do not know where a piece of content belongs (or when the writing "gets stuck"), | |
| 134 | +answer **two questions**: | |
| 135 | + | |
| 136 | +``` | |
| 137 | +1. Does the content inform ACTION or COGNITION? | |
| 138 | + (practical steps ──vs── theoretical knowledge) | |
| 139 | + | |
| 140 | +2. Does it serve the ACQUISITION or the APPLICATION of a skill? | |
| 141 | + (the user is learning ──vs── the user is doing) | |
| 142 | +``` | |
| 143 | + | |
| 144 | +| If the content says… | …informs | …and serves | → then it is a | | |
| 145 | +|----------------------|----------|-------------|-----------------| | |
| 146 | +| "teach me by doing" | action | acquisition | **Tutorial** | | |
| 147 | +| "help me do this" | action | application | **How-to guide** | | |
| 148 | +| "give me the exact information" | cognition | application | **Reference** | | |
| 149 | +| "help me understand" | cognition | acquisition | **Explanation** | | |
| 150 | + | |
| 151 | +**Symptom of a misfiling:** if a page forces you to mix two styles (e.g. explaining the *why* | |
| 152 | +in the middle of a recipe), it contains two distinct pieces of content → **split it** and link | |
| 153 | +the parts to each other. | |
| 154 | + | |
| 155 | +The compass applies at every scale: a whole document, a section, or even a single sentence. | |
| 156 | + | |
| 157 | +--- | |
| 158 | + | |
| 159 | +## Recommended folder architecture | |
| 160 | + | |
| 161 | +Four folders, one per quadrant, plus an index: | |
| 162 | + | |
| 163 | +``` | |
| 164 | +docs/ (or ./new.docs, ./documentation, etc.) | |
| 165 | +├── README.md ← index: explains the structure and orients the reader | |
| 166 | +├── tutorials/ ← guided learning | |
| 167 | +│ └── getting-started.md | |
| 168 | +├── how-to/ ← task-oriented recipes | |
| 169 | +│ ├── do-x.md | |
| 170 | +│ └── do-y.md | |
| 171 | +├── reference/ ← exhaustive technical description | |
| 172 | +│ ├── cli.md | |
| 173 | +│ └── configuration.md | |
| 174 | +└── explanation/ ← understanding, context, decisions | |
| 175 | + ├── architecture.md | |
| 176 | + └── design-choices.md | |
| 177 | +``` | |
| 178 | + | |
| 179 | +**Architecture rules:** | |
| 180 | +- ✅ **Folder names** make the quadrant obvious (tutorials / how-to / reference / explanation) | |
| 181 | +- ✅ The **index** (`README.md`) briefly explains the four types and links to each | |
| 182 | +- ✅ Documents **link to each other**: a tutorial links to the reference and the explanation rather than including them | |
| 183 | +- ✅ Folders may be named in the documentation's own language (`tutoriels/`, `guides-pratiques/`, …) — stay consistent | |
| 184 | + | |
| 185 | +### Bilingual architecture (the "Both" option) | |
| 186 | + | |
| 187 | +When the user wants both languages, use **one subfolder per language**, each containing its own | |
| 188 | +complete four-quadrant structure. A root index acts as the language selector: | |
| 189 | + | |
| 190 | +``` | |
| 191 | +docs/ | |
| 192 | +├── README.md ← language selector (links to fr/ and en/) | |
| 193 | +├── fr/ | |
| 194 | +│ ├── README.md ← French index | |
| 195 | +│ ├── tutorials/ | |
| 196 | +│ ├── how-to/ | |
| 197 | +│ ├── reference/ | |
| 198 | +│ └── explanation/ | |
| 199 | +└── en/ | |
| 200 | + ├── README.md ← English index | |
| 201 | + ├── tutorials/ | |
| 202 | + ├── how-to/ | |
| 203 | + ├── reference/ | |
| 204 | + └── explanation/ | |
| 205 | +``` | |
| 206 | + | |
| 207 | +**Bilingual rules:** | |
| 208 | +- ✅ **Cross-links stay inside a single language** (a FR doc never links to an EN doc, and vice versa) | |
| 209 | +- ✅ **Quadrant folder names are identical** in both languages (`tutorials/`, `how-to/`…) to keep a 1-to-1 symmetry | |
| 210 | +- ✅ **File names may be translated** (`premiers-pas.md` ↔ `getting-started.md`) or kept identical — stay consistent | |
| 211 | +- ✅ The **content** is translated, not merely copied: adapt the examples where necessary | |
| 212 | +- ❌ Do not mix two languages in the same file | |
| 213 | +- ❌ Do not duplicate only one of the two indexes — each language has its own | |
| 214 | + | |
| 215 | +> Acceptable alternative for small projects: a per-file language suffix | |
| 216 | +> (`getting-started-en.md` / `premiers-pas-fr.md`) in a flat structure. Reserve this for cases | |
| 217 | +> where creating subfolders would be disproportionate. | |
| 218 | + | |
| 219 | +--- | |
| 220 | + | |
| 221 | +## Iterative workflow (the heart of the method) | |
| 222 | + | |
| 223 | +Diátaxis is **not** a grand plan to be executed in one go. It is an organic process: the | |
| 224 | +structure emerges from within, "one cell at a time". The documentation must always be | |
| 225 | +**"complete, not finished"** — useful and well-formed at every stage, while remaining open. | |
| 226 | + | |
| 227 | +**The loop, to be repeated indefinitely:** | |
| 228 | + | |
| 229 | +``` | |
| 230 | +1. CHOOSE — take a small piece (a paragraph, a page) | |
| 231 | +2. ASSESS — does it serve one clear need? Is it in the right quadrant? | |
| 232 | +3. DECIDE — identify ONE concrete improvement | |
| 233 | +4. ACT — apply it and publish immediately | |
| 234 | +``` | |
| 235 | + | |
| 236 | +**Workflow rules:** | |
| 237 | +- ✅ Work **small**: one improvement at a time, published right away | |
| 238 | +- ✅ "Every step in the right direction deserves to be published immediately" | |
| 239 | +- ❌ Do NOT create large empty sections "to be filled in later" | |
| 240 | +- ❌ Do NOT tear everything down to rebuild it all at once | |
| 241 | + | |
| 242 | +--- | |
| 243 | + | |
| 244 | +## Skill application phases | |
| 245 | + | |
| 246 | +### Phase 1 — Gathering information | |
| 247 | + | |
| 248 | +**First question, MANDATORY — the language of the documentation.** | |
| 249 | +Before anything else, ask the user which language(s) the documentation should be produced in, | |
| 250 | +**via `AskUserQuestion`**, with exactly these three options: | |
| 251 | + | |
| 252 | +| Option | Effect on the output | | |
| 253 | +|--------|-----------------------| | |
| 254 | +| **French** | All documentation in French, at the root of the chosen location. | | |
| 255 | +| **English** | All documentation in English, at the root of the chosen location. | | |
| 256 | +| **Both** | Bilingual documentation: one subfolder per language (see "Bilingual architecture"). | | |
| 257 | + | |
| 258 | +This choice determines the **folder architecture** (mono- or bilingual) and the language of | |
| 259 | +**every** document, title and link. Never infer it from the language the user is writing to | |
| 260 | +you in, or from the language of the existing code and comments: ask. If documentation already | |
| 261 | +exists in one language and the user wants "both", add the missing language without rewriting | |
| 262 | +what is already there. | |
| 263 | + | |
| 264 | +> **Exception — invoked by another skill.** When this skill is invoked from another skill that | |
| 265 | +> has already fixed the language, do not ask: use the language it specifies and go straight to | |
| 266 | +> Phase 2. The `methodical-dev` skill always specifies **Both** (English + French), because it | |
| 267 | +> requires bilingual documentation for every change. Asking again in that case is a redundant | |
| 268 | +> prompt for a question that is already settled. | |
| 269 | + | |
| 270 | +Then ask (via AskUserQuestion where relevant): | |
| 271 | + | |
| 272 | +1. **Documentation target**: which project / which scope should be documented? | |
| 273 | +2. **Audience**: beginners discovering the tool? advanced users? contributors? | |
| 274 | +3. **Location**: where should the docs be written? (`docs/`, `./new.docs`, etc.) Must existing content be preserved? | |
| 275 | +4. **Existing material**: is there already documentation to reuse / reorganise? | |
| 276 | + | |
| 277 | +### Phase 2 — Mapping the subject matter | |
| 278 | + | |
| 279 | +Before writing, understand **factually** what is being documented: | |
| 280 | +- Explore the code (entry point, commands, configuration, features) | |
| 281 | +- List the user-facing features and the machinery (CLI, config, API…) | |
| 282 | +- Spot the existing documentation so as not to reinvent it | |
| 283 | +- Produce a list of **topics** to be filed into the four quadrants | |
| 284 | + | |
| 285 | +### Phase 3 — Filing with the Compass | |
| 286 | + | |
| 287 | +For each identified topic, apply the compass and file it into a quadrant. Build a **plan** as a | |
| 288 | +table: | |
| 289 | + | |
| 290 | +``` | |
| 291 | +| Topic | Quadrant | File | | |
| 292 | +|-----------------------------|---------------|----------------------------------| | |
| 293 | +| First steps | Tutorial | tutorials/getting-started.md | | |
| 294 | +| Enabling feature X | How-to | how-to/enable-x.md | | |
| 295 | +| Configuration options | Reference | reference/configuration.md | | |
| 296 | +| Why this architecture | Explanation | explanation/architecture.md | | |
| 297 | +``` | |
| 298 | + | |
| 299 | +Present this plan to the user and have it approved before writing. | |
| 300 | + | |
| 301 | +### Phase 4 — Writing quadrant by quadrant | |
| 302 | + | |
| 303 | +Write the documents while **strictly** respecting the writing rules of the corresponding | |
| 304 | +quadrant (see the templates below). At the end of each document, apply the quadrant's checklist. | |
| 305 | + | |
| 306 | +### Phase 5 — Index and cross-links | |
| 307 | + | |
| 308 | +- Create / update `README.md` (the index) explaining the Diátaxis structure and orienting the reader | |
| 309 | +- Check that documents **link to each other** in the right direction (tutorial → reference/explanation, etc.) | |
| 310 | + | |
| 311 | +### Phase 6 — Final validation | |
| 312 | + | |
| 313 | +Apply the global checklist (see below) and present the result. | |
| 314 | + | |
| 315 | +--- | |
| 316 | + | |
| 317 | +## Templates | |
| 318 | + | |
| 319 | +### Template — Tutorial | |
| 320 | + | |
| 321 | +```markdown | |
| 322 | +# Tutorial: {what the reader will have accomplished} | |
| 323 | + | |
| 324 | +By the end of this tutorial, you will have {concrete, visible result}. | |
| 325 | +No prior knowledge of {X} is required. | |
| 326 | + | |
| 327 | +## Prerequisites | |
| 328 | +- {strictly what is required for EVERY step to work} | |
| 329 | + | |
| 330 | +## Step 1 — {first concrete action} | |
| 331 | +Type: | |
| 332 | +\`\`\`bash | |
| 333 | +{exact command} | |
| 334 | +\`\`\` | |
| 335 | +You should see: | |
| 336 | +\`\`\` | |
| 337 | +{actual expected output} | |
| 338 | +\`\`\` | |
| 339 | +We have just {result of the step, in one sentence}. | |
| 340 | + | |
| 341 | +## Step 2 — {next action} | |
| 342 | +... | |
| 343 | + | |
| 344 | +## What now? | |
| 345 | +You have built {recap}. To go further: | |
| 346 | +- To accomplish specific tasks → see the [how-to guides](../how-to/) | |
| 347 | +- To understand how it works → see the [explanation](../explanation/) | |
| 348 | +``` | |
| 349 | + | |
| 350 | +### Template — How-to guide | |
| 351 | + | |
| 352 | +```markdown | |
| 353 | +# How to {accomplish the task} | |
| 354 | + | |
| 355 | +This guide shows how to {task}. It assumes you {skill prerequisite}. | |
| 356 | + | |
| 357 | +## Steps | |
| 358 | +1. {step} | |
| 359 | +2. {step} | |
| 360 | + | |
| 361 | +## Variants | |
| 362 | +- If you use {case A}, do {…} instead | |
| 363 | +- For {case B}, add {…} | |
| 364 | + | |
| 365 | +## See also | |
| 366 | +- Options reference: [reference](../reference/…) | |
| 367 | +- Why it works this way: [explanation](../explanation/…) | |
| 368 | +``` | |
| 369 | + | |
| 370 | +### Template — Reference | |
| 371 | + | |
| 372 | +```markdown | |
| 373 | +# Reference: {component} | |
| 374 | + | |
| 375 | +> Neutral, exhaustive description of {component}. | |
| 376 | + | |
| 377 | +## {Sub-component / command / section} | |
| 378 | + | |
| 379 | +| Field / Option | Type | Default | Description | | |
| 380 | +|----------------|------|---------|-------------| | |
| 381 | +| {name} | {type} | {default} | {factual description} | | |
| 382 | + | |
| 383 | +### Example | |
| 384 | +\`\`\`{lang} | |
| 385 | +{minimal usage example} | |
| 386 | +\`\`\` | |
| 387 | + | |
| 388 | +### Errors | |
| 389 | +| Error | Cause | | |
| 390 | +|-------|-------| | |
| 391 | +| {message} | {factual cause} | | |
| 392 | +``` | |
| 393 | + | |
| 394 | +### Template — Explanation | |
| 395 | + | |
| 396 | +```markdown | |
| 397 | +# {Topic} — explanation | |
| 398 | + | |
| 399 | +## What is this about? | |
| 400 | +{framing of the topic, overview} | |
| 401 | + | |
| 402 | +## Why it is designed this way | |
| 403 | +{context, constraints, decisions} | |
| 404 | + | |
| 405 | +## Rejected alternatives | |
| 406 | +{what was considered and why it was rejected} | |
| 407 | + | |
| 408 | +## How it relates to the rest | |
| 409 | +{connections with the other parts of the system} | |
| 410 | +``` | |
| 411 | + | |
| 412 | +--- | |
| 413 | + | |
| 414 | +## Checklists | |
| 415 | + | |
| 416 | +### Per-quadrant checklist | |
| 417 | + | |
| 418 | +**Tutorial:** | |
| 419 | +``` | |
| 420 | +□ Is the destination announced up front? | |
| 421 | +□ Does every step produce a visible result? | |
| 422 | +□ Do all commands work without fail (perfect reliability)? | |
| 423 | +□ Is the actual expected output shown? | |
| 424 | +□ No alternatives / options / digressions? | |
| 425 | +□ Is explanation kept minimal (linked, not included)? | |
| 426 | +``` | |
| 427 | + | |
| 428 | +**How-to guide:** | |
| 429 | +``` | |
| 430 | +□ Does the title state a concrete task ("How to…")? | |
| 431 | +□ Does it start from a real problem, not a feature? | |
| 432 | +□ Does it contain only the necessary steps? | |
| 433 | +□ Does it cover real-world variants? | |
| 434 | +□ Does it avoid teaching the basics (assumes competence)? | |
| 435 | +``` | |
| 436 | + | |
| 437 | +**Reference:** | |
| 438 | +``` | |
| 439 | +□ Purely descriptive (no instruction, no explanation, no opinion)? | |
| 440 | +□ Exhaustive (every parameter, default, type, error)? | |
| 441 | +□ Standard, predictable format (same structure throughout)? | |
| 442 | +□ Mirrors the product's structure? | |
| 443 | +□ Contains examples without drifting into explanation? | |
| 444 | +``` | |
| 445 | + | |
| 446 | +**Explanation:** | |
| 447 | +``` | |
| 448 | +□ Does it step back and make connections? | |
| 449 | +□ Does it give the context (history, decisions, constraints)? | |
| 450 | +□ Does it discuss the why and the rejected alternatives? | |
| 451 | +□ Does it stay bounded (not absorbing reference/guide material)? | |
| 452 | +□ Is it free of step-by-step instructions? | |
| 453 | +``` | |
| 454 | + | |
| 455 | +### Global checklist (Phase 6) | |
| 456 | + | |
| 457 | +``` | |
| 458 | +□ Was the language (FR / EN / both) asked for — or taken from the invoking skill — and respected? | |
| 459 | +□ If bilingual: one subfolder per language, cross-links internal to each language? | |
| 460 | +□ Do all four quadrants exist (at least one document each)? | |
| 461 | +□ Does every document serve exactly ONE clearly identified need? | |
| 462 | +□ Do the folder names make the quadrant obvious? | |
| 463 | +□ Does the index (README) explain the structure and orient the reader? | |
| 464 | +□ Do documents link to each other (no duplication)? | |
| 465 | +□ Are there no empty "to be filled in later" sections? | |
| 466 | +□ Is every document "complete, not finished" (useful right now)? | |
| 467 | +``` | |
| 468 | + | |
| 469 | +--- | |
| 470 | + | |
| 471 | +## Markdown formatting | |
| 472 | + | |
| 473 | +Write Markdown for readers who soft-wrap in their editor, not for a fixed column | |
| 474 | +width. **Do not hard-wrap prose**: never insert a line break in the middle of a | |
| 475 | +sentence or paragraph. | |
| 476 | + | |
| 477 | +- **One logical line per block.** A paragraph is a single line. A list item is a | |
| 478 | + single line (its continuation text stays on the same line, not wrapped under | |
| 479 | + the bullet). A blockquote paragraph is a single `>` line. | |
| 480 | +- **Blank lines separate blocks** — keep exactly one blank line between | |
| 481 | + paragraphs, list items groups, headings, and code fences. | |
| 482 | +- **Leave code, tables, and headings alone.** Fenced code blocks keep their | |
| 483 | + internal newlines verbatim; table rows stay one row per line; a heading is its | |
| 484 | + own line. | |
| 485 | + | |
| 486 | +This keeps diffs meaningful (a reworded sentence changes one line, not a whole | |
| 487 | +reflowed block) and matches how the rendered output looks anyway. If you are | |
| 488 | +editing a file that was previously hard-wrapped, unwrap the blocks you touch. | |
| 489 | + | |
| 490 | +## Strict Rules | |
| 491 | + | |
| 492 | +**You must NEVER:** | |
| 493 | +- ❌ Mix two quadrants in the same document (recipe + explanation, reference + tutorial…) | |
| 494 | +- ❌ Put detailed explanation into a tutorial or a how-to guide | |
| 495 | +- ❌ Put step-by-step instructions into a reference or an explanation | |
| 496 | +- ❌ Create empty folders / sections in anticipation | |
| 497 | +- ❌ Brutally reorganise all the existing documentation at once (work in increments) | |
| 498 | +- ❌ Duplicate content across quadrants instead of linking | |
| 499 | +- ❌ Hard-wrap prose — no line break in the middle of a sentence or paragraph (see "Markdown formatting") | |
| 500 | + | |
| 501 | +**You must ALWAYS:** | |
| 502 | +- ✅ File every piece of content with the compass (action/cognition × acquisition/application) | |
| 503 | +- ✅ Respect the writing rules specific to the quadrant | |
| 504 | +- ✅ Guarantee absolute reliability in tutorials (every command mentally tested) | |
| 505 | +- ✅ Keep the reference neutral, exhaustive and structured | |
| 506 | +- ✅ Link documents to each other rather than merging them | |
| 507 | +- ✅ Work in small publishable increments ("complete, not finished") | |
| 508 | +- ✅ Write each paragraph, list item, and blockquote as a single unwrapped line | |
| 509 | + | |
| 510 | +--- | |
| 511 | + | |
| 512 | +## Notes | |
| 513 | + | |
| 514 | +Diátaxis imposes no technology (Markdown, Sphinx, MkDocs, Docusaurus… it does not matter). What | |
| 515 | +counts is the **clean separation of the four needs**. Documentation that respects this | |
| 516 | +separation is easier to write, to maintain and — above all — to use: every reader immediately | |
| 517 | +knows where to go depending on whether they want to **learn**, **do**, **look up** or | |
| 518 | +**understand**. | |
| 519 | + | |
| 520 | +Canonical reference: https://diataxis.fr | |
| new file mode 100644 | |||
| @@ -0,0 +1,520 @@ | |||
| 1 | +--- | ||
| 2 | +name: diataxis-doc | ||
| 3 | +description: Document a project using the Diátaxis methodology (https://diataxis.fr). Organises documentation into four distinct quadrants — Tutorials (learning), How-to guides (solving a task), Reference (looking information up), Explanation (understanding) — along the two axes action/cognition and acquisition/application. Use when creating or restructuring a project's documentation in a rigorous, readable, user-needs-driven way. | ||
| 4 | +--- | ||
| 5 | +# Diátaxis Documentation Skill | ||
| 6 | + | ||
| 7 | +## Description | ||
| 8 | + | ||
| 9 | +Guides the creation and restructuring of a project's documentation according to the | ||
| 10 | +**Diátaxis** methodology (Daniele Procida — https://diataxis.fr). Diátaxis is not a rigid | ||
| 11 | +template: it is a **compass** that guarantees every documentation page serves **exactly one | ||
| 12 | +clearly identified user need**, and that all four fundamental needs are covered. | ||
| 13 | + | ||
| 14 | +The strength of the method: it solves the three problems of any documentation — | ||
| 15 | +**content** (what to write), **style** (how to write it) and **architecture** | ||
| 16 | +(how to organise it) — by reducing them to two simple questions. | ||
| 17 | + | ||
| 18 | +## When to Use | ||
| 19 | + | ||
| 20 | +- When a project has no documentation, or a "catch-all" documentation | ||
| 21 | +- When existing docs mix tutorials, recipes, reference and explanation in the same pages | ||
| 22 | +- When users "can't find" the information despite voluminous docs | ||
| 23 | +- When you want maintainable documentation that grows in small increments | ||
| 24 | +- After adding a feature, to document it in the right place without reorganising everything | ||
| 25 | + | ||
| 26 | +--- | ||
| 27 | + | ||
| 28 | +## Core Principle: the two axes | ||
| 29 | + | ||
| 30 | +The whole method rests on **two orthogonal axes**. They do not merely cover the territory of | ||
| 31 | +documentation — they **define** it. This is why there are **necessarily four quadrants**, | ||
| 32 | +neither three nor five. | ||
| 33 | + | ||
| 34 | +``` | ||
| 35 | + ACTION (practical know-how) | ||
| 36 | + ▲ | ||
| 37 | + │ | ||
| 38 | + TUTORIALS │ HOW-TO GUIDES | ||
| 39 | + (learning by doing) │ (achieving a goal) | ||
| 40 | + │ | ||
| 41 | + ACQUISITION ────────────────────┼──────────────────── APPLICATION | ||
| 42 | + (studying a skill) │ (applying a skill) | ||
| 43 | + │ | ||
| 44 | + EXPLANATION │ REFERENCE | ||
| 45 | + (understanding, thinking) │ (looking information up) | ||
| 46 | + │ | ||
| 47 | + ▼ | ||
| 48 | + COGNITION (theoretical knowledge) | ||
| 49 | +``` | ||
| 50 | + | ||
| 51 | +| Quadrant | Action/cognition axis | Acquisition/application axis | The user's question | | ||
| 52 | +|----------|------------------------|-------------------------------|----------------------| | ||
| 53 | +| **Tutorial** | Action | Acquisition | "Teach me to get started" | | ||
| 54 | +| **How-to guide** | Action | Application | "How do I do X?" | | ||
| 55 | +| **Reference** | Cognition | Application | "What are the exact details of X?" | | ||
| 56 | +| **Explanation** | Cognition | Acquisition | "Can you explain why / the context?" | | ||
| 57 | + | ||
| 58 | +--- | ||
| 59 | + | ||
| 60 | +## The four quadrants in detail | ||
| 61 | + | ||
| 62 | +### 1. TUTORIALS — learning-oriented | ||
| 63 | + | ||
| 64 | +A tutorial is a **guided lesson**, an experience under a teacher's direction. Its goal is NOT | ||
| 65 | +to accomplish a real task, but to make the beginner **acquire a skill**. The teacher is | ||
| 66 | +responsible for the student's success. | ||
| 67 | + | ||
| 68 | +**Writing rules (mandatory):** | ||
| 69 | +- ✅ Announce the **destination** up front ("by the end, you will have built…"), not what will be "learned" | ||
| 70 | +- ✅ Produce a **visible result at every step**, however small | ||
| 71 | +- ✅ Guarantee **perfect reliability**: every command must work for everyone, every time | ||
| 72 | +- ✅ Show the **actual expected output** ("you should see: …") | ||
| 73 | +- ✅ Use **"we"** ("we will now…") and the direct imperative ("Type this") | ||
| 74 | +- ✅ Flag common mistakes at the point where they can occur | ||
| 75 | +- ❌ **Minimise explanation to the extreme** — a tutorial is NOT the place for explanation. Link to it instead. | ||
| 76 | +- ❌ **No alternatives, no options, no digressions** — a single path to success | ||
| 77 | +- ❌ Do not explain the "why" in detail (it breaks the learning flow) | ||
| 78 | + | ||
| 79 | +### 2. HOW-TO GUIDES — task-oriented | ||
| 80 | + | ||
| 81 | +A how-to guide is a **recipe** for reaching a specific goal. It addresses an **already | ||
| 82 | +competent** user who knows what they want to do. "How do I deploy?", "How do I enable | ||
| 83 | +caching?". | ||
| 84 | + | ||
| 85 | +> ⚠️ Key distinction from a tutorial: *"Someone who expects a recipe and receives a cooking | ||
| 86 | +> lesson will be disappointed and annoyed."* Tutorial = lesson; guide = recipe. | ||
| 87 | + | ||
| 88 | +**Writing rules:** | ||
| 89 | +- ✅ Title = **the task**, stated clearly: "How to {do X}" | ||
| 90 | +- ✅ Start from a **real problem**, not from a feature of the tool | ||
| 91 | +- ✅ Contain **only the steps needed** for the task | ||
| 92 | +- ✅ Stay **adaptable to the real world** ("if you use Y, do this instead") — a guide covers variants | ||
| 93 | +- ✅ Assume the user **already knows** what they want to accomplish | ||
| 94 | +- ❌ Do not teach the basic concepts (that is the tutorial's job) | ||
| 95 | +- ❌ Do not drown the recipe in explanation (link to the explanation) | ||
| 96 | + | ||
| 97 | +### 3. REFERENCE — information-oriented | ||
| 98 | + | ||
| 99 | +Reference is a **neutral, exhaustive technical description** of the machinery: the commands, | ||
| 100 | +options, parameters, structures. It is a **map of the territory**, austere and reliable, that | ||
| 101 | +you **consult** (you do not read it end to end). | ||
| 102 | + | ||
| 103 | +**Writing rules:** | ||
| 104 | +- ✅ **Describe**, do nothing else: no instruction, no explanation, no opinion | ||
| 105 | +- ✅ **Accuracy and completeness**: every parameter, default value, type, error | ||
| 106 | +- ✅ **Standard, predictable structure**: the same format for every entry (tables, lists) | ||
| 107 | +- ✅ **Mirror the product's structure**: the reference's organisation reflects that of the code | ||
| 108 | +- ✅ Include usage **examples**, without drifting into explanation | ||
| 109 | +- ❌ Do not teach, do not tell the "why" — link to tutorials and explanations | ||
| 110 | + | ||
| 111 | +> Mental model: a **nutrition label**. Factual, standardised, rigorous, with no recipe and no | ||
| 112 | +> marketing pitch. | ||
| 113 | + | ||
| 114 | +### 4. EXPLANATION — understanding-oriented | ||
| 115 | + | ||
| 116 | +Explanation **steps back**. It answers "Can you tell me about…?". It connects concepts, gives | ||
| 117 | +context, history, design decisions, rejected alternatives. It is the documentation you read | ||
| 118 | +"in the bath", away from active work. | ||
| 119 | + | ||
| 120 | +**Writing rules:** | ||
| 121 | +- ✅ **Make connections** between topics, take a wider view | ||
| 122 | +- ✅ Give the **context**: history, decisions, constraints, rejected alternatives | ||
| 123 | +- ✅ Discuss the **why**, the trade-offs, the possibilities | ||
| 124 | +- ✅ **Allow opinion** and acknowledge multiple points of view | ||
| 125 | +- ✅ **Stay bounded** to one topic, so it does not absorb content that belongs elsewhere | ||
| 126 | +- ❌ Do not give step-by-step instructions (that is the guides'/tutorials' job) | ||
| 127 | +- ❌ Do not become a reference (no exhaustive parameter lists) | ||
| 128 | + | ||
| 129 | +--- | ||
| 130 | + | ||
| 131 | +## The Compass — choosing the right quadrant | ||
| 132 | + | ||
| 133 | +When you do not know where a piece of content belongs (or when the writing "gets stuck"), | ||
| 134 | +answer **two questions**: | ||
| 135 | + | ||
| 136 | +``` | ||
| 137 | +1. Does the content inform ACTION or COGNITION? | ||
| 138 | + (practical steps ──vs── theoretical knowledge) | ||
| 139 | + | ||
| 140 | +2. Does it serve the ACQUISITION or the APPLICATION of a skill? | ||
| 141 | + (the user is learning ──vs── the user is doing) | ||
| 142 | +``` | ||
| 143 | + | ||
| 144 | +| If the content says… | …informs | …and serves | → then it is a | | ||
| 145 | +|----------------------|----------|-------------|-----------------| | ||
| 146 | +| "teach me by doing" | action | acquisition | **Tutorial** | | ||
| 147 | +| "help me do this" | action | application | **How-to guide** | | ||
| 148 | +| "give me the exact information" | cognition | application | **Reference** | | ||
| 149 | +| "help me understand" | cognition | acquisition | **Explanation** | | ||
| 150 | + | ||
| 151 | +**Symptom of a misfiling:** if a page forces you to mix two styles (e.g. explaining the *why* | ||
| 152 | +in the middle of a recipe), it contains two distinct pieces of content → **split it** and link | ||
| 153 | +the parts to each other. | ||
| 154 | + | ||
| 155 | +The compass applies at every scale: a whole document, a section, or even a single sentence. | ||
| 156 | + | ||
| 157 | +--- | ||
| 158 | + | ||
| 159 | +## Recommended folder architecture | ||
| 160 | + | ||
| 161 | +Four folders, one per quadrant, plus an index: | ||
| 162 | + | ||
| 163 | +``` | ||
| 164 | +docs/ (or ./new.docs, ./documentation, etc.) | ||
| 165 | +├── README.md ← index: explains the structure and orients the reader | ||
| 166 | +├── tutorials/ ← guided learning | ||
| 167 | +│ └── getting-started.md | ||
| 168 | +├── how-to/ ← task-oriented recipes | ||
| 169 | +│ ├── do-x.md | ||
| 170 | +│ └── do-y.md | ||
| 171 | +├── reference/ ← exhaustive technical description | ||
| 172 | +│ ├── cli.md | ||
| 173 | +│ └── configuration.md | ||
| 174 | +└── explanation/ ← understanding, context, decisions | ||
| 175 | + ├── architecture.md | ||
| 176 | + └── design-choices.md | ||
| 177 | +``` | ||
| 178 | + | ||
| 179 | +**Architecture rules:** | ||
| 180 | +- ✅ **Folder names** make the quadrant obvious (tutorials / how-to / reference / explanation) | ||
| 181 | +- ✅ The **index** (`README.md`) briefly explains the four types and links to each | ||
| 182 | +- ✅ Documents **link to each other**: a tutorial links to the reference and the explanation rather than including them | ||
| 183 | +- ✅ Folders may be named in the documentation's own language (`tutoriels/`, `guides-pratiques/`, …) — stay consistent | ||
| 184 | + | ||
| 185 | +### Bilingual architecture (the "Both" option) | ||
| 186 | + | ||
| 187 | +When the user wants both languages, use **one subfolder per language**, each containing its own | ||
| 188 | +complete four-quadrant structure. A root index acts as the language selector: | ||
| 189 | + | ||
| 190 | +``` | ||
| 191 | +docs/ | ||
| 192 | +├── README.md ← language selector (links to fr/ and en/) | ||
| 193 | +├── fr/ | ||
| 194 | +│ ├── README.md ← French index | ||
| 195 | +│ ├── tutorials/ | ||
| 196 | +│ ├── how-to/ | ||
| 197 | +│ ├── reference/ | ||
| 198 | +│ └── explanation/ | ||
| 199 | +└── en/ | ||
| 200 | + ├── README.md ← English index | ||
| 201 | + ├── tutorials/ | ||
| 202 | + ├── how-to/ | ||
| 203 | + ├── reference/ | ||
| 204 | + └── explanation/ | ||
| 205 | +``` | ||
| 206 | + | ||
| 207 | +**Bilingual rules:** | ||
| 208 | +- ✅ **Cross-links stay inside a single language** (a FR doc never links to an EN doc, and vice versa) | ||
| 209 | +- ✅ **Quadrant folder names are identical** in both languages (`tutorials/`, `how-to/`…) to keep a 1-to-1 symmetry | ||
| 210 | +- ✅ **File names may be translated** (`premiers-pas.md` ↔ `getting-started.md`) or kept identical — stay consistent | ||
| 211 | +- ✅ The **content** is translated, not merely copied: adapt the examples where necessary | ||
| 212 | +- ❌ Do not mix two languages in the same file | ||
| 213 | +- ❌ Do not duplicate only one of the two indexes — each language has its own | ||
| 214 | + | ||
| 215 | +> Acceptable alternative for small projects: a per-file language suffix | ||
| 216 | +> (`getting-started-en.md` / `premiers-pas-fr.md`) in a flat structure. Reserve this for cases | ||
| 217 | +> where creating subfolders would be disproportionate. | ||
| 218 | + | ||
| 219 | +--- | ||
| 220 | + | ||
| 221 | +## Iterative workflow (the heart of the method) | ||
| 222 | + | ||
| 223 | +Diátaxis is **not** a grand plan to be executed in one go. It is an organic process: the | ||
| 224 | +structure emerges from within, "one cell at a time". The documentation must always be | ||
| 225 | +**"complete, not finished"** — useful and well-formed at every stage, while remaining open. | ||
| 226 | + | ||
| 227 | +**The loop, to be repeated indefinitely:** | ||
| 228 | + | ||
| 229 | +``` | ||
| 230 | +1. CHOOSE — take a small piece (a paragraph, a page) | ||
| 231 | +2. ASSESS — does it serve one clear need? Is it in the right quadrant? | ||
| 232 | +3. DECIDE — identify ONE concrete improvement | ||
| 233 | +4. ACT — apply it and publish immediately | ||
| 234 | +``` | ||
| 235 | + | ||
| 236 | +**Workflow rules:** | ||
| 237 | +- ✅ Work **small**: one improvement at a time, published right away | ||
| 238 | +- ✅ "Every step in the right direction deserves to be published immediately" | ||
| 239 | +- ❌ Do NOT create large empty sections "to be filled in later" | ||
| 240 | +- ❌ Do NOT tear everything down to rebuild it all at once | ||
| 241 | + | ||
| 242 | +--- | ||
| 243 | + | ||
| 244 | +## Skill application phases | ||
| 245 | + | ||
| 246 | +### Phase 1 — Gathering information | ||
| 247 | + | ||
| 248 | +**First question, MANDATORY — the language of the documentation.** | ||
| 249 | +Before anything else, ask the user which language(s) the documentation should be produced in, | ||
| 250 | +**via `AskUserQuestion`**, with exactly these three options: | ||
| 251 | + | ||
| 252 | +| Option | Effect on the output | | ||
| 253 | +|--------|-----------------------| | ||
| 254 | +| **French** | All documentation in French, at the root of the chosen location. | | ||
| 255 | +| **English** | All documentation in English, at the root of the chosen location. | | ||
| 256 | +| **Both** | Bilingual documentation: one subfolder per language (see "Bilingual architecture"). | | ||
| 257 | + | ||
| 258 | +This choice determines the **folder architecture** (mono- or bilingual) and the language of | ||
| 259 | +**every** document, title and link. Never infer it from the language the user is writing to | ||
| 260 | +you in, or from the language of the existing code and comments: ask. If documentation already | ||
| 261 | +exists in one language and the user wants "both", add the missing language without rewriting | ||
| 262 | +what is already there. | ||
| 263 | + | ||
| 264 | +> **Exception — invoked by another skill.** When this skill is invoked from another skill that | ||
| 265 | +> has already fixed the language, do not ask: use the language it specifies and go straight to | ||
| 266 | +> Phase 2. The `methodical-dev` skill always specifies **Both** (English + French), because it | ||
| 267 | +> requires bilingual documentation for every change. Asking again in that case is a redundant | ||
| 268 | +> prompt for a question that is already settled. | ||
| 269 | + | ||
| 270 | +Then ask (via AskUserQuestion where relevant): | ||
| 271 | + | ||
| 272 | +1. **Documentation target**: which project / which scope should be documented? | ||
| 273 | +2. **Audience**: beginners discovering the tool? advanced users? contributors? | ||
| 274 | +3. **Location**: where should the docs be written? (`docs/`, `./new.docs`, etc.) Must existing content be preserved? | ||
| 275 | +4. **Existing material**: is there already documentation to reuse / reorganise? | ||
| 276 | + | ||
| 277 | +### Phase 2 — Mapping the subject matter | ||
| 278 | + | ||
| 279 | +Before writing, understand **factually** what is being documented: | ||
| 280 | +- Explore the code (entry point, commands, configuration, features) | ||
| 281 | +- List the user-facing features and the machinery (CLI, config, API…) | ||
| 282 | +- Spot the existing documentation so as not to reinvent it | ||
| 283 | +- Produce a list of **topics** to be filed into the four quadrants | ||
| 284 | + | ||
| 285 | +### Phase 3 — Filing with the Compass | ||
| 286 | + | ||
| 287 | +For each identified topic, apply the compass and file it into a quadrant. Build a **plan** as a | ||
| 288 | +table: | ||
| 289 | + | ||
| 290 | +``` | ||
| 291 | +| Topic | Quadrant | File | | ||
| 292 | +|-----------------------------|---------------|----------------------------------| | ||
| 293 | +| First steps | Tutorial | tutorials/getting-started.md | | ||
| 294 | +| Enabling feature X | How-to | how-to/enable-x.md | | ||
| 295 | +| Configuration options | Reference | reference/configuration.md | | ||
| 296 | +| Why this architecture | Explanation | explanation/architecture.md | | ||
| 297 | +``` | ||
| 298 | + | ||
| 299 | +Present this plan to the user and have it approved before writing. | ||
| 300 | + | ||
| 301 | +### Phase 4 — Writing quadrant by quadrant | ||
| 302 | + | ||
| 303 | +Write the documents while **strictly** respecting the writing rules of the corresponding | ||
| 304 | +quadrant (see the templates below). At the end of each document, apply the quadrant's checklist. | ||
| 305 | + | ||
| 306 | +### Phase 5 — Index and cross-links | ||
| 307 | + | ||
| 308 | +- Create / update `README.md` (the index) explaining the Diátaxis structure and orienting the reader | ||
| 309 | +- Check that documents **link to each other** in the right direction (tutorial → reference/explanation, etc.) | ||
| 310 | + | ||
| 311 | +### Phase 6 — Final validation | ||
| 312 | + | ||
| 313 | +Apply the global checklist (see below) and present the result. | ||
| 314 | + | ||
| 315 | +--- | ||
| 316 | + | ||
| 317 | +## Templates | ||
| 318 | + | ||
| 319 | +### Template — Tutorial | ||
| 320 | + | ||
| 321 | +```markdown | ||
| 322 | +# Tutorial: {what the reader will have accomplished} | ||
| 323 | + | ||
| 324 | +By the end of this tutorial, you will have {concrete, visible result}. | ||
| 325 | +No prior knowledge of {X} is required. | ||
| 326 | + | ||
| 327 | +## Prerequisites | ||
| 328 | +- {strictly what is required for EVERY step to work} | ||
| 329 | + | ||
| 330 | +## Step 1 — {first concrete action} | ||
| 331 | +Type: | ||
| 332 | +\`\`\`bash | ||
| 333 | +{exact command} | ||
| 334 | +\`\`\` | ||
| 335 | +You should see: | ||
| 336 | +\`\`\` | ||
| 337 | +{actual expected output} | ||
| 338 | +\`\`\` | ||
| 339 | +We have just {result of the step, in one sentence}. | ||
| 340 | + | ||
| 341 | +## Step 2 — {next action} | ||
| 342 | +... | ||
| 343 | + | ||
| 344 | +## What now? | ||
| 345 | +You have built {recap}. To go further: | ||
| 346 | +- To accomplish specific tasks → see the [how-to guides](../how-to/) | ||
| 347 | +- To understand how it works → see the [explanation](../explanation/) | ||
| 348 | +``` | ||
| 349 | + | ||
| 350 | +### Template — How-to guide | ||
| 351 | + | ||
| 352 | +```markdown | ||
| 353 | +# How to {accomplish the task} | ||
| 354 | + | ||
| 355 | +This guide shows how to {task}. It assumes you {skill prerequisite}. | ||
| 356 | + | ||
| 357 | +## Steps | ||
| 358 | +1. {step} | ||
| 359 | +2. {step} | ||
| 360 | + | ||
| 361 | +## Variants | ||
| 362 | +- If you use {case A}, do {…} instead | ||
| 363 | +- For {case B}, add {…} | ||
| 364 | + | ||
| 365 | +## See also | ||
| 366 | +- Options reference: [reference](../reference/…) | ||
| 367 | +- Why it works this way: [explanation](../explanation/…) | ||
| 368 | +``` | ||
| 369 | + | ||
| 370 | +### Template — Reference | ||
| 371 | + | ||
| 372 | +```markdown | ||
| 373 | +# Reference: {component} | ||
| 374 | + | ||
| 375 | +> Neutral, exhaustive description of {component}. | ||
| 376 | + | ||
| 377 | +## {Sub-component / command / section} | ||
| 378 | + | ||
| 379 | +| Field / Option | Type | Default | Description | | ||
| 380 | +|----------------|------|---------|-------------| | ||
| 381 | +| {name} | {type} | {default} | {factual description} | | ||
| 382 | + | ||
| 383 | +### Example | ||
| 384 | +\`\`\`{lang} | ||
| 385 | +{minimal usage example} | ||
| 386 | +\`\`\` | ||
| 387 | + | ||
| 388 | +### Errors | ||
| 389 | +| Error | Cause | | ||
| 390 | +|-------|-------| | ||
| 391 | +| {message} | {factual cause} | | ||
| 392 | +``` | ||
| 393 | + | ||
| 394 | +### Template — Explanation | ||
| 395 | + | ||
| 396 | +```markdown | ||
| 397 | +# {Topic} — explanation | ||
| 398 | + | ||
| 399 | +## What is this about? | ||
| 400 | +{framing of the topic, overview} | ||
| 401 | + | ||
| 402 | +## Why it is designed this way | ||
| 403 | +{context, constraints, decisions} | ||
| 404 | + | ||
| 405 | +## Rejected alternatives | ||
| 406 | +{what was considered and why it was rejected} | ||
| 407 | + | ||
| 408 | +## How it relates to the rest | ||
| 409 | +{connections with the other parts of the system} | ||
| 410 | +``` | ||
| 411 | + | ||
| 412 | +--- | ||
| 413 | + | ||
| 414 | +## Checklists | ||
| 415 | + | ||
| 416 | +### Per-quadrant checklist | ||
| 417 | + | ||
| 418 | +**Tutorial:** | ||
| 419 | +``` | ||
| 420 | +□ Is the destination announced up front? | ||
| 421 | +□ Does every step produce a visible result? | ||
| 422 | +□ Do all commands work without fail (perfect reliability)? | ||
| 423 | +□ Is the actual expected output shown? | ||
| 424 | +□ No alternatives / options / digressions? | ||
| 425 | +□ Is explanation kept minimal (linked, not included)? | ||
| 426 | +``` | ||
| 427 | + | ||
| 428 | +**How-to guide:** | ||
| 429 | +``` | ||
| 430 | +□ Does the title state a concrete task ("How to…")? | ||
| 431 | +□ Does it start from a real problem, not a feature? | ||
| 432 | +□ Does it contain only the necessary steps? | ||
| 433 | +□ Does it cover real-world variants? | ||
| 434 | +□ Does it avoid teaching the basics (assumes competence)? | ||
| 435 | +``` | ||
| 436 | + | ||
| 437 | +**Reference:** | ||
| 438 | +``` | ||
| 439 | +□ Purely descriptive (no instruction, no explanation, no opinion)? | ||
| 440 | +□ Exhaustive (every parameter, default, type, error)? | ||
| 441 | +□ Standard, predictable format (same structure throughout)? | ||
| 442 | +□ Mirrors the product's structure? | ||
| 443 | +□ Contains examples without drifting into explanation? | ||
| 444 | +``` | ||
| 445 | + | ||
| 446 | +**Explanation:** | ||
| 447 | +``` | ||
| 448 | +□ Does it step back and make connections? | ||
| 449 | +□ Does it give the context (history, decisions, constraints)? | ||
| 450 | +□ Does it discuss the why and the rejected alternatives? | ||
| 451 | +□ Does it stay bounded (not absorbing reference/guide material)? | ||
| 452 | +□ Is it free of step-by-step instructions? | ||
| 453 | +``` | ||
| 454 | + | ||
| 455 | +### Global checklist (Phase 6) | ||
| 456 | + | ||
| 457 | +``` | ||
| 458 | +□ Was the language (FR / EN / both) asked for — or taken from the invoking skill — and respected? | ||
| 459 | +□ If bilingual: one subfolder per language, cross-links internal to each language? | ||
| 460 | +□ Do all four quadrants exist (at least one document each)? | ||
| 461 | +□ Does every document serve exactly ONE clearly identified need? | ||
| 462 | +□ Do the folder names make the quadrant obvious? | ||
| 463 | +□ Does the index (README) explain the structure and orient the reader? | ||
| 464 | +□ Do documents link to each other (no duplication)? | ||
| 465 | +□ Are there no empty "to be filled in later" sections? | ||
| 466 | +□ Is every document "complete, not finished" (useful right now)? | ||
| 467 | +``` | ||
| 468 | + | ||
| 469 | +--- | ||
| 470 | + | ||
| 471 | +## Markdown formatting | ||
| 472 | + | ||
| 473 | +Write Markdown for readers who soft-wrap in their editor, not for a fixed column | ||
| 474 | +width. **Do not hard-wrap prose**: never insert a line break in the middle of a | ||
| 475 | +sentence or paragraph. | ||
| 476 | + | ||
| 477 | +- **One logical line per block.** A paragraph is a single line. A list item is a | ||
| 478 | + single line (its continuation text stays on the same line, not wrapped under | ||
| 479 | + the bullet). A blockquote paragraph is a single `>` line. | ||
| 480 | +- **Blank lines separate blocks** — keep exactly one blank line between | ||
| 481 | + paragraphs, list items groups, headings, and code fences. | ||
| 482 | +- **Leave code, tables, and headings alone.** Fenced code blocks keep their | ||
| 483 | + internal newlines verbatim; table rows stay one row per line; a heading is its | ||
| 484 | + own line. | ||
| 485 | + | ||
| 486 | +This keeps diffs meaningful (a reworded sentence changes one line, not a whole | ||
| 487 | +reflowed block) and matches how the rendered output looks anyway. If you are | ||
| 488 | +editing a file that was previously hard-wrapped, unwrap the blocks you touch. | ||
| 489 | + | ||
| 490 | +## Strict Rules | ||
| 491 | + | ||
| 492 | +**You must NEVER:** | ||
| 493 | +- ❌ Mix two quadrants in the same document (recipe + explanation, reference + tutorial…) | ||
| 494 | +- ❌ Put detailed explanation into a tutorial or a how-to guide | ||
| 495 | +- ❌ Put step-by-step instructions into a reference or an explanation | ||
| 496 | +- ❌ Create empty folders / sections in anticipation | ||
| 497 | +- ❌ Brutally reorganise all the existing documentation at once (work in increments) | ||
| 498 | +- ❌ Duplicate content across quadrants instead of linking | ||
| 499 | +- ❌ Hard-wrap prose — no line break in the middle of a sentence or paragraph (see "Markdown formatting") | ||
| 500 | + | ||
| 501 | +**You must ALWAYS:** | ||
| 502 | +- ✅ File every piece of content with the compass (action/cognition × acquisition/application) | ||
| 503 | +- ✅ Respect the writing rules specific to the quadrant | ||
| 504 | +- ✅ Guarantee absolute reliability in tutorials (every command mentally tested) | ||
| 505 | +- ✅ Keep the reference neutral, exhaustive and structured | ||
| 506 | +- ✅ Link documents to each other rather than merging them | ||
| 507 | +- ✅ Work in small publishable increments ("complete, not finished") | ||
| 508 | +- ✅ Write each paragraph, list item, and blockquote as a single unwrapped line | ||
| 509 | + | ||
| 510 | +--- | ||
| 511 | + | ||
| 512 | +## Notes | ||
| 513 | + | ||
| 514 | +Diátaxis imposes no technology (Markdown, Sphinx, MkDocs, Docusaurus… it does not matter). What | ||
| 515 | +counts is the **clean separation of the four needs**. Documentation that respects this | ||
| 516 | +separation is easier to write, to maintain and — above all — to use: every reader immediately | ||
| 517 | +knows where to go depending on whether they want to **learn**, **do**, **look up** or | ||
| 518 | +**understand**. | ||
| 519 | + | ||
| 520 | +Canonical reference: https://diataxis.fr | ||
added
.claude/skills/methodical-dev/SKILL.md +603 -0 | new file mode 100644 | ||
| @@ -0,0 +1,603 @@ | ||
| 1 | +--- | |
| 2 | +name: methodical-dev | |
| 3 | +description: Guide the user through a structured development methodology, following best practices for working with Claude Code in a controlled and effective way. Every change is delivered complete: readable and documented code, unit tests with a documented way to run them, bilingual (EN + FR) Diátaxis documentation, a passing quality gate, and an updated .memory/ project record. Use when starting a feature, refactoring, or whenever you want a controlled, approval-gated development process. | |
| 4 | +--- | |
| 5 | +# Methodical Development Skill | |
| 6 | + | |
| 7 | +## Description | |
| 8 | +Guide the user through a structured development methodology, following best practices for | |
| 9 | +working with Claude Code in a controlled and effective way. | |
| 10 | + | |
| 11 | +The core promise: **no change is "done" until it is readable, tested, documented in both | |
| 12 | +English and French, quality-gated, and recorded in the project's memory.** The phases below | |
| 13 | +enforce that; none of them is optional. | |
| 14 | + | |
| 15 | +## When to Use | |
| 16 | +- When starting a new feature | |
| 17 | +- When refactoring or changing existing behaviour | |
| 18 | +- When you want to follow a structured process | |
| 19 | +- To avoid the common pitfalls of AI-assisted development | |
| 20 | + | |
| 21 | +## Companion skills | |
| 22 | + | |
| 23 | +This skill orchestrates two others. Both ship alongside it and must be **invoked as skills**, | |
| 24 | +not reimplemented: | |
| 25 | + | |
| 26 | +| Skill | When this skill invokes it | Phase | | |
| 27 | +| --- | --- | --- | | |
| 28 | +| `quality` | After implementation, to measure quality and refactor until the gate passes | Phase 5 | | |
| 29 | +| `diataxis-doc` | After the quality gate, to write or update the documentation | Phase 6 | | |
| 30 | + | |
| 31 | +## Instructions | |
| 32 | + | |
| 33 | +You are a skill that guides the user through a rigorous development methodology. You must | |
| 34 | +follow this process step by step. | |
| 35 | + | |
| 36 | +--- | |
| 37 | + | |
| 38 | +## Phase 0: Read the project memory | |
| 39 | + | |
| 40 | +**Before asking the user anything**, read `.memory/` at the repository root: | |
| 41 | + | |
| 42 | +```bash | |
| 43 | +ls -la .memory/ 2>/dev/null && cat .memory/summary.md 2>/dev/null | |
| 44 | +``` | |
| 45 | + | |
| 46 | +- If `.memory/` exists, read `summary.md`, the tail of `history.md`, and the most recent file | |
| 47 | + in `handoffs/`. It tells you the project's current state, past decisions, and any work left | |
| 48 | + in flight — **do not ask the user for information that is already recorded there.** | |
| 49 | +- If `.memory/` does not exist, you will create it in Phase 7. Do not create it yet. | |
| 50 | + | |
| 51 | +If a handoff records unfinished work, surface it now and ask whether to resume it or start | |
| 52 | +something new. | |
| 53 | + | |
| 54 | +--- | |
| 55 | + | |
| 56 | +## Phase 1: Gathering Information | |
| 57 | + | |
| 58 | +Ask the user these questions using AskUserQuestion — skipping any the memory already answers: | |
| 59 | + | |
| 60 | +1. **Goal of the feature** | |
| 61 | + - What feature do you want to develop? | |
| 62 | + - What is the exact scope of this feature? | |
| 63 | + | |
| 64 | +2. **Technical Constraints** | |
| 65 | + - Which frameworks/libraries must you use? | |
| 66 | + - Are there any version constraints? | |
| 67 | + - Are there architectural patterns to follow? | |
| 68 | + | |
| 69 | +3. **Documentation and Examples** | |
| 70 | + - Do you have documentation to reference? | |
| 71 | + - Do you have similar existing code that could serve as an example? | |
| 72 | + | |
| 73 | +4. **Style and Conventions** | |
| 74 | + - Are there specific naming conventions? | |
| 75 | + - Is there a particular code style to follow? | |
| 76 | + | |
| 77 | +--- | |
| 78 | + | |
| 79 | +## Phase 2: Git Check | |
| 80 | + | |
| 81 | +Check the state of the repository: | |
| 82 | + | |
| 83 | +```bash | |
| 84 | +# Check that we are in a git repo | |
| 85 | +git status | |
| 86 | + | |
| 87 | +# If there is no repo, offer to initialise one | |
| 88 | +git init | |
| 89 | +``` | |
| 90 | + | |
| 91 | +If the user is not on a dedicated branch, **strongly recommend** creating a feature branch. | |
| 92 | + | |
| 93 | +**IMPORTANT**: Do not create the branch automatically. Ask the user: | |
| 94 | +- What branch name would they like? | |
| 95 | +- Do they want you to create the branch, or would they rather do it themselves? | |
| 96 | + | |
| 97 | +--- | |
| 98 | + | |
| 99 | +## Phase 3: Detailed Planning | |
| 100 | + | |
| 101 | +1. **Analyse the existing code** (if needed) | |
| 102 | + - Use Glob and Grep to understand the structure | |
| 103 | + - Identify the files to modify | |
| 104 | + - Identify the existing patterns to follow | |
| 105 | + | |
| 106 | +2. **Identify the project's test and build commands.** Look for a `Makefile`, `Taskfile.yml`, | |
| 107 | + `package.json` scripts, `pyproject.toml`, or the CI workflow. You need these for Phase 4 | |
| 108 | + and Phase 5, and you must reuse the project's existing convention rather than inventing a | |
| 109 | + parallel one. | |
| 110 | + | |
| 111 | +3. **Create a detailed plan** using TodoWrite | |
| 112 | + - Break the feature into logical steps (5–8 steps maximum) | |
| 113 | + - Each step must be atomic and testable | |
| 114 | + - Order the steps by dependency | |
| 115 | + - **Every implementation step carries its own unit tests** — do not plan a single | |
| 116 | + "write the tests" step at the end | |
| 117 | + - Include the mandatory closing phases as plan items: quality gate, bilingual | |
| 118 | + documentation, memory update | |
| 119 | + | |
| 120 | +4. **Present the plan** to the user | |
| 121 | + - Explain each step | |
| 122 | + - Ask for approval before continuing | |
| 123 | + - Allow adjustments | |
| 124 | + | |
| 125 | +--- | |
| 126 | + | |
| 127 | +## Phase 4: Guided Implementation | |
| 128 | + | |
| 129 | +For each step of the plan: | |
| 130 | + | |
| 131 | +1. **Before starting the step** | |
| 132 | + - Mark the step as `in_progress` with TodoWrite | |
| 133 | + - Explain what you are about to do | |
| 134 | + - Ask for confirmation if the step is complex | |
| 135 | + | |
| 136 | +2. **During the step** | |
| 137 | + - Implement only what is planned for this step | |
| 138 | + - Write the code to the **Code Standards** below — readable, maintainable, documented | |
| 139 | + with usage examples | |
| 140 | + - Write or update the **unit tests** for this step's behaviour (see Testing Requirements) | |
| 141 | + - Do **NOT** take shortcuts | |
| 142 | + - Do **NOT** delete existing code without asking | |
| 143 | + - Do **NOT** change the architecture without agreement | |
| 144 | + - Explain the technical choices as you go | |
| 145 | + | |
| 146 | +3. **After the step** | |
| 147 | + - Run the tests and show the result — a step whose tests do not pass is not finished | |
| 148 | + - Mark the step as `completed` with TodoWrite | |
| 149 | + - Summarise what was done | |
| 150 | + - List the files created/modified | |
| 151 | + - **STOP and wait for the user's approval** | |
| 152 | + | |
| 153 | +4. **Mandatory checkpoint** | |
| 154 | + - Ask the user to: | |
| 155 | + - Review the code produced | |
| 156 | + - Test the behaviour | |
| 157 | + - Confirm it matches their request | |
| 158 | + - Offer to: | |
| 159 | + - Continue to the next step | |
| 160 | + - Change something in the current step | |
| 161 | + - Adjust the remaining plan | |
| 162 | + | |
| 163 | +--- | |
| 164 | + | |
| 165 | +## Phase 5: Quality Gate (mandatory) | |
| 166 | + | |
| 167 | +**Invoke the `quality` skill.** Do not measure quality by eye and do not skip this phase, | |
| 168 | +even for a one-line change. | |
| 169 | + | |
| 170 | +The skill configures qlty if needed, measures lint issues / code smells / complexity, writes | |
| 171 | +a report under `.quality/`, records the run in its history so progression is visible, and | |
| 172 | +tells you whether the gate passed. | |
| 173 | + | |
| 174 | +- **Gate passed** → report the numbers and move to Phase 6. | |
| 175 | +- **Gate failed** → refactor as the skill directs, re-run the tests from Phase 4, and | |
| 176 | + re-measure. Repeat until the gate passes or the skill's own stopping conditions are hit | |
| 177 | + (five iterations, or two runs with no improvement). | |
| 178 | +- **Still failing at the stopping condition** → do not silently continue. Report what | |
| 179 | + improved, what remains, and why, then ask the user how to proceed. | |
| 180 | + | |
| 181 | +Never satisfy the gate by weakening it. Editing `.qlty/qlty.toml` exclusions, lowering | |
| 182 | +`.quality/gate.json` thresholds, or adding blanket lint suppressions is forbidden here for the | |
| 183 | +same reason it is forbidden inside the `quality` skill: it makes the measurement lie. | |
| 184 | + | |
| 185 | +For a long multi-step feature, running the quality skill after a large step — not only at the | |
| 186 | +end — catches drift earlier and is cheaper than one big cleanup. | |
| 187 | + | |
| 188 | +--- | |
| 189 | + | |
| 190 | +## Phase 6: Documentation (mandatory, bilingual) | |
| 191 | + | |
| 192 | +**Invoke the `diataxis-doc` skill** to write or update the documentation for what you just | |
| 193 | +built. Every creation and every change gets documented — new behaviour, changed behaviour, | |
| 194 | +and removed behaviour alike. | |
| 195 | + | |
| 196 | +> **Do not ask the user which language to document in.** `diataxis-doc` normally opens by | |
| 197 | +> asking; when invoked from this skill the answer is already fixed: **Both** — English *and* | |
| 198 | +> French. Tell the skill this so it goes straight to the bilingual architecture. | |
| 199 | + | |
| 200 | +This produces one subfolder per language, each with its own complete four-quadrant structure: | |
| 201 | + | |
| 202 | +``` | |
| 203 | +docs/ | |
| 204 | +├── README.md ← language selector | |
| 205 | +├── en/ | |
| 206 | +│ ├── README.md | |
| 207 | +│ ├── tutorials/ ← "teach me to get started" | |
| 208 | +│ ├── how-to/ ← "how do I do X?" | |
| 209 | +│ ├── reference/ ← "what are the exact details of X?" | |
| 210 | +│ └── explanation/ ← "why is it built this way?" | |
| 211 | +└── fr/ | |
| 212 | + └── … same structure | |
| 213 | +``` | |
| 214 | + | |
| 215 | +Rules that matter here: | |
| 216 | + | |
| 217 | +- **Both languages stay in sync.** A change documented in English but not French is an | |
| 218 | + incomplete change. Translate the content — do not copy the English text into `fr/`. | |
| 219 | +- **Cross-links stay inside one language.** A French page never links to an English page. | |
| 220 | +- **File the content with the Diátaxis compass**, do not dump everything into one page. New | |
| 221 | + feature → usually a `how-to/` page plus a `reference/` entry; a design decision → | |
| 222 | + `explanation/`. | |
| 223 | +- **The way to run the tests is documented**, in both languages (see Testing Requirements). | |
| 224 | +- **Every touched package's `README.md` is updated.** This is separate from `docs/` and applies to **all** packages, in whatever language that README already uses. Any package whose public surface you changed — a new, renamed, or removed exported function, type, method, flag, or CLI subcommand — gets its `README.md` brought back in sync **in the same change**. A README that lists part of a package's API but omits what you just added is a defect, not merely "a bit behind". If the repository keeps no per-package READMEs, this rule is inert — do not create them just to satisfy it. | |
| 225 | +- **A package-dependency diagram is kept in draw.io format.** It shows every package/module, the dependency arrows between them (and on third-party runtimes), and a one-line "what it is for" on each. Keep it at `docs/diagrams/packages.drawio` (or the project's existing diagrams location). **Create it when it does not exist, and update it whenever you add, remove, or re-wire a package** so it never drifts from the real import graph. Keep it as a single language-neutral file (labels in the docs' primary language), referenced from the architecture explanation page in each language. A `.drawio` file is plain XML (an `mxGraphModel`) that diagrams.net and the VS Code Draw.io extension open directly, so you can author and edit it as text; after writing it, sanity-check that the XML parses. | |
| 226 | + | |
| 227 | +--- | |
| 228 | + | |
| 229 | +## Phase 7: Update the project memory (mandatory) | |
| 230 | + | |
| 231 | +Create or update `.memory/` at the repository root. This is the project's durable record: it | |
| 232 | +is what lets you — or a different agent, in a fresh sandbox — pick the work up later without | |
| 233 | +re-deriving everything. | |
| 234 | + | |
| 235 | +``` | |
| 236 | +.memory/ | |
| 237 | +├── README.md ← what this folder is and how it is maintained | |
| 238 | +├── summary.md ← living snapshot of the project's current state (edited in place) | |
| 239 | +├── history.md ← append-only chronological log (never rewritten) | |
| 240 | +└── handoffs/ | |
| 241 | + └── YYYY-MM-DD-<slug>.md ← one per session or feature | |
| 242 | +``` | |
| 243 | + | |
| 244 | +`.memory/` is **committed to the repository**, not gitignored — that is the whole point. | |
| 245 | + | |
| 246 | +### `summary.md` — edited in place, never regenerated | |
| 247 | + | |
| 248 | +The current state of the project, kept short enough to stay read-worthy: | |
| 249 | + | |
| 250 | +- What the project is and does | |
| 251 | +- Architecture: main components and how they fit together | |
| 252 | +- Key technical decisions currently in force, and why | |
| 253 | +- How to build, test, and run it (the actual commands) | |
| 254 | +- Known limitations and open questions | |
| 255 | + | |
| 256 | +**Change only what this session establishes or invalidates, and leave the rest alone.** You have | |
| 257 | +seen a slice of the project; this file holds what every previous session established. Regenerating | |
| 258 | +it wholesale from your slice silently destroys accurate content you never looked at — the one way | |
| 259 | +this file stops being trustworthy. Write only what you verified; anything you could not check goes | |
| 260 | +under an explicit `## Not yet established` heading instead of a plausible guess, because the next | |
| 261 | +session will trust whatever is written here. If the project's state did not change, leave the file | |
| 262 | +untouched. | |
| 263 | + | |
| 264 | +### `history.md` — append only | |
| 265 | + | |
| 266 | +One dated entry per completed feature or session. **Never rewrite or delete past entries** — | |
| 267 | +a history you edit is not a history. | |
| 268 | + | |
| 269 | +```markdown | |
| 270 | +## 2026-07-25 — <what was done> | |
| 271 | + | |
| 272 | +- **Goal**: <what the user asked for> | |
| 273 | +- **Changes**: <files/components created or modified> | |
| 274 | +- **Decisions**: <choices made and why; alternatives rejected> | |
| 275 | +- **Tests**: <what was added, how to run it> | |
| 276 | +- **Quality**: <gate PASS/FAIL and the key numbers> | |
| 277 | +- **Docs**: <pages created/updated, in both languages> | |
| 278 | +``` | |
| 279 | + | |
| 280 | +### `handoffs/YYYY-MM-DD-<slug>.md` — written at the end of the session | |
| 281 | + | |
| 282 | +The document a fresh agent reads to resume: | |
| 283 | + | |
| 284 | +```markdown | |
| 285 | +# Handoff — <date> — <topic> | |
| 286 | + | |
| 287 | +## State | |
| 288 | +<what works right now; what was just finished> | |
| 289 | + | |
| 290 | +## In flight | |
| 291 | +<work started but not finished, and exactly where it stopped> | |
| 292 | + | |
| 293 | +## Next steps | |
| 294 | +1. <the concrete next action> | |
| 295 | +2. … | |
| 296 | + | |
| 297 | +## Open questions / blockers | |
| 298 | +<decisions awaiting the user; anything that blocked progress> | |
| 299 | + | |
| 300 | +## Watch out for | |
| 301 | +<traps discovered along the way — failing commands, fragile assumptions> | |
| 302 | +``` | |
| 303 | + | |
| 304 | +### Memory vs. documentation | |
| 305 | + | |
| 306 | +Keep the boundary clean, so content is not duplicated: | |
| 307 | + | |
| 308 | +| | Audience | Content | | |
| 309 | +| --- | --- | --- | | |
| 310 | +| `docs/` (Diátaxis) | **Users** of the project | How to use it, what the API is, why it is designed that way | | |
| 311 | +| `.memory/` | **Whoever continues the work** | Project history, session state, handoffs, decisions in progress | | |
| 312 | + | |
| 313 | +If a piece of information helps someone *use* the project, it belongs in `docs/`. If it helps | |
| 314 | +someone *continue building* it, it belongs in `.memory/`. | |
| 315 | + | |
| 316 | +--- | |
| 317 | + | |
| 318 | +## Phase 8: Final Validation | |
| 319 | + | |
| 320 | +Once all the steps and phases are complete: | |
| 321 | + | |
| 322 | +1. **Full summary** | |
| 323 | + - List of all files created | |
| 324 | + - List of all files modified | |
| 325 | + - Summary of the features implemented | |
| 326 | + | |
| 327 | +2. **Quality checklist** | |
| 328 | + - [ ] Does the feature match the request exactly? | |
| 329 | + - [ ] No unrequested deletions? | |
| 330 | + - [ ] Is the code readable, maintainable, and documented with usage examples? | |
| 331 | + - [ ] Are unit tests present for every change, and do they pass? | |
| 332 | + - [ ] Is there a documented, single command to run the tests? | |
| 333 | + - [ ] Did the `quality` skill run, and did the gate pass? | |
| 334 | + - [ ] Is the documentation updated in **both** English and French? | |
| 335 | + - [ ] Is every touched package's `README.md` updated to match its current public API? | |
| 336 | + - [ ] Is the package-dependency draw.io diagram present and consistent with the current import graph? | |
| 337 | + - [ ] Are the conventions respected? | |
| 338 | + - [ ] Is `.memory/` updated — `summary.md`, a new `history.md` entry, and a handoff? | |
| 339 | + | |
| 340 | +3. **Commit proposal** | |
| 341 | + - Propose a structured commit message | |
| 342 | + - List the files to add to the commit — including `docs/` and `.memory/` | |
| 343 | + - Do **NOT** commit automatically | |
| 344 | + - Let the user do it, or use the /commit skill | |
| 345 | + | |
| 346 | +--- | |
| 347 | + | |
| 348 | +## Code Standards | |
| 349 | + | |
| 350 | +Generated code must be readable and maintainable by a **human**, not merely correct. Someone | |
| 351 | +unfamiliar with it should understand it without asking you. | |
| 352 | + | |
| 353 | +**Readability** | |
| 354 | +- Names state intent: `retryAfterSeconds`, not `d` or `tmp2`. No abbreviations that are not | |
| 355 | + domain-standard. | |
| 356 | +- One function does one thing, and is short enough to read without scrolling. | |
| 357 | +- Prefer the explicit over the clever. A dense one-liner that needs a comment to be understood | |
| 358 | + should be several plain lines instead. | |
| 359 | +- Early returns over deep nesting. | |
| 360 | +- **Match the surrounding code.** Its existing style wins over your preferences. | |
| 361 | + | |
| 362 | +**Maintainability** | |
| 363 | +- No duplicated logic — extract it the second time it appears. | |
| 364 | +- Errors are handled where they can be handled meaningfully, never silently swallowed. | |
| 365 | +- No dead code, no commented-out code, no `TODO` without a concrete follow-up noted in | |
| 366 | + `.memory/handoffs/`. | |
| 367 | +- Keep the public surface small: expose what callers need, no more. | |
| 368 | + | |
| 369 | +**Documentation in the code** | |
| 370 | +- Every public/exported function, type, class, and module gets a doc comment saying what it | |
| 371 | + does, what it expects, and what it returns or raises. | |
| 372 | +- **Each public API's doc comment includes a short usage example** — a few lines a reader can | |
| 373 | + copy. Use the language's idiom for this: Go `Example` functions (which are also tests), | |
| 374 | + Python docstring examples, JSDoc `@example`, Rust doc-tests. | |
| 375 | +- Comments explain **why**, not what. If a comment restates the code, delete it and improve | |
| 376 | + the name instead. | |
| 377 | +- Document non-obvious constraints and invariants at the point they apply. | |
| 378 | + | |
| 379 | +**Markdown and prose files** | |
| 380 | +- Applies to every Markdown file you write — `docs/`, `.memory/`, `README`s. | |
| 381 | +- **Do not hard-wrap prose.** Write each paragraph, list item, and blockquote as | |
| 382 | + a single unwrapped line; never break a line in the middle of a sentence. Let the | |
| 383 | + reader's editor soft-wrap. This keeps diffs meaningful — a reworded sentence | |
| 384 | + touches one line, not a whole reflowed block. | |
| 385 | +- Leave fenced code blocks, tables, and headings as they are (code keeps its own | |
| 386 | + newlines; one table row per line; a heading on its own line). | |
| 387 | +- When editing a file that was previously hard-wrapped, unwrap the blocks you touch. | |
| 388 | + | |
| 389 | +--- | |
| 390 | + | |
| 391 | +## Testing Requirements | |
| 392 | + | |
| 393 | +Every addition, creation, and change ships with tests. A change without tests is not finished. | |
| 394 | + | |
| 395 | +**What to write** | |
| 396 | +- Unit tests for each new or modified behaviour, added in the same step as the code. | |
| 397 | +- Cover the happy path **and** every error or edge case the code explicitly handles. | |
| 398 | +- When fixing a bug, first write the test that reproduces it, and confirm it fails before the | |
| 399 | + fix. | |
| 400 | +- Tests are deterministic: no real network, no wall-clock or random dependence, no reliance on | |
| 401 | + test execution order. | |
| 402 | +- Test names state the behaviour under test, so a failure is legible without reading the body. | |
| 403 | + | |
| 404 | +**How to run them** | |
| 405 | +- There must be **one documented command** that runs the whole suite. | |
| 406 | +- **Reuse the project's existing convention** — add a `Makefile` / `Taskfile.yml` target, or a | |
| 407 | + `package.json` script, if one of those is already in use. Only create a | |
| 408 | + `scripts/test.sh`-style runner when the project has no such entry point, and make it | |
| 409 | + executable (`chmod +x`) with a `set -eu` guard. | |
| 410 | +- The command must work from a clean checkout, with no undocumented manual setup. | |
| 411 | + | |
| 412 | +**Where to document it** | |
| 413 | +- In `docs/` under both languages — usually a `how-to/` page ("How to run the tests"). | |
| 414 | +- In `.memory/summary.md`, in the build/test/run section. | |
| 415 | +- In the project `README` if it already documents commands. | |
| 416 | + | |
| 417 | +--- | |
| 418 | + | |
| 419 | +## Strict Rules | |
| 420 | + | |
| 421 | +**You must NEVER:** | |
| 422 | +- ❌ Create a commit without an explicit request | |
| 423 | +- ❌ Delete existing code without confirmation | |
| 424 | +- ❌ Change the architecture without agreement | |
| 425 | +- ❌ Skip a step without approval | |
| 426 | +- ❌ Continue if the user has not approved the previous step | |
| 427 | +- ❌ Take shortcuts "to keep things simple" | |
| 428 | +- ❌ Implement something different from what was asked | |
| 429 | +- ❌ Declare a change done without tests, docs in both languages, an updated `README.md` for | |
| 430 | + every touched package, a passing quality gate, and an updated `.memory/` | |
| 431 | +- ❌ Weaken the quality gate, delete tests, or disable a linter to make a check pass | |
| 432 | +- ❌ Document in only one language | |
| 433 | +- ❌ Rewrite or delete past `history.md` entries | |
| 434 | + | |
| 435 | +**You must ALWAYS:** | |
| 436 | +- ✅ Read `.memory/` before asking the user anything | |
| 437 | +- ✅ Stop after each step for approval | |
| 438 | +- ✅ Explain your technical choices | |
| 439 | +- ✅ Ask for confirmation on important decisions | |
| 440 | +- ✅ Follow the approved plan exactly | |
| 441 | +- ✅ Be transparent about what you are doing | |
| 442 | +- ✅ Propose alternatives if you see a problem | |
| 443 | +- ✅ Write tests alongside the code, in the same step | |
| 444 | +- ✅ Invoke the `quality` skill before declaring the work complete | |
| 445 | +- ✅ Invoke the `diataxis-doc` skill for documentation, in English and French | |
| 446 | +- ✅ Update `.memory/` at the end of the session | |
| 447 | + | |
| 448 | +--- | |
| 449 | + | |
| 450 | +## Handling Problems | |
| 451 | + | |
| 452 | +If you hit a problem during implementation: | |
| 453 | + | |
| 454 | +1. **STOP immediately** | |
| 455 | +2. Explain the problem clearly | |
| 456 | +3. Propose alternative solutions | |
| 457 | +4. **Wait** for the user's decision | |
| 458 | +5. **NEVER** work around the problem by deleting code | |
| 459 | + | |
| 460 | +If the session ends with the problem unresolved, record it in | |
| 461 | +`.memory/handoffs/` under "Open questions / blockers" before stopping. | |
| 462 | + | |
| 463 | +--- | |
| 464 | + | |
| 465 | +## Communication Format | |
| 466 | + | |
| 467 | +Use this format to communicate clearly: | |
| 468 | + | |
| 469 | +``` | |
| 470 | +=== STEP [N]: [Step name] === | |
| 471 | + | |
| 472 | +📋 What I am going to do: | |
| 473 | +- [Action 1] | |
| 474 | +- [Action 2] | |
| 475 | + | |
| 476 | +✅ Approval needed? [Yes/No] | |
| 477 | + | |
| 478 | +[If Yes, wait for a reply before continuing] | |
| 479 | + | |
| 480 | +--- | |
| 481 | + | |
| 482 | +[Implementation + tests] | |
| 483 | + | |
| 484 | +--- | |
| 485 | + | |
| 486 | +📊 STEP [N] SUMMARY: | |
| 487 | +✅ Created: [file1], [file2] | |
| 488 | +✅ Modified: [file3] | |
| 489 | +🧪 Tests: [what was added] — [pass/fail, command used] | |
| 490 | +✅ Feature: [description] | |
| 491 | + | |
| 492 | +⏸️ CHECKPOINT | |
| 493 | +Please review and approve before continuing. | |
| 494 | + | |
| 495 | +Options: | |
| 496 | +1. ✅ Continue to the next step | |
| 497 | +2. 🔧 Change something | |
| 498 | +3. 📝 Adjust the plan | |
| 499 | +``` | |
| 500 | + | |
| 501 | +For the closing phases: | |
| 502 | + | |
| 503 | +``` | |
| 504 | +=== PHASE 5: QUALITY GATE === | |
| 505 | +🔍 Invoking the `quality` skill… | |
| 506 | +📊 Gate: [PASS/FAIL] — errors: [n], warnings: [n], smells: [n] | |
| 507 | +[If FAIL: refactoring, then re-measuring] | |
| 508 | + | |
| 509 | +=== PHASE 6: DOCUMENTATION (EN + FR) === | |
| 510 | +📚 Invoking the `diataxis-doc` skill (language: Both)… | |
| 511 | +✅ docs/en/: [pages] | |
| 512 | +✅ docs/fr/: [pages] | |
| 513 | + | |
| 514 | +=== PHASE 7: PROJECT MEMORY === | |
| 515 | +🧠 .memory/summary.md — updated | |
| 516 | +🧠 .memory/history.md — entry appended | |
| 517 | +🧠 .memory/handoffs/… — written | |
| 518 | +``` | |
| 519 | + | |
| 520 | +--- | |
| 521 | + | |
| 522 | +## Usage Example | |
| 523 | + | |
| 524 | +``` | |
| 525 | +User: /methodical-dev | |
| 526 | + | |
| 527 | +Skill: I will guide you through a methodical development process. | |
| 528 | + | |
| 529 | +=== PHASE 0: PROJECT MEMORY === | |
| 530 | + | |
| 531 | +[Reads .memory/summary.md, history.md, handoffs/] | |
| 532 | + | |
| 533 | +Found a handoff from 2026-07-20: the parser is done, the encoder was left | |
| 534 | +half-written. Resume that, or start something new? | |
| 535 | + | |
| 536 | +=== PHASE 1: GATHERING INFORMATION === | |
| 537 | + | |
| 538 | +[Asks the remaining questions via AskUserQuestion] | |
| 539 | + | |
| 540 | +=== PHASE 2: GIT CHECK === | |
| 541 | + | |
| 542 | +[Checks git status] | |
| 543 | + | |
| 544 | +=== PHASE 3: PLANNING === | |
| 545 | + | |
| 546 | +Here is the proposed plan: | |
| 547 | + | |
| 548 | +□ Step 1: Create the base structure + tests | |
| 549 | +□ Step 2: Implement the business logic + tests | |
| 550 | +□ Step 3: Wire up the public API + doc comments with examples | |
| 551 | +□ Step 4: Quality gate (quality skill) | |
| 552 | +□ Step 5: Documentation EN + FR (diataxis-doc skill) | |
| 553 | +□ Step 6: Update .memory/ | |
| 554 | + | |
| 555 | +Does this plan work for you? | |
| 556 | + | |
| 557 | +[Wait for approval] | |
| 558 | + | |
| 559 | +=== PHASE 4: IMPLEMENTATION === | |
| 560 | + | |
| 561 | +=== STEP 1: Create the base structure === | |
| 562 | + | |
| 563 | +📋 What I am going to do: | |
| 564 | +- Create src/feature/index.ts | |
| 565 | +- Create src/feature/types.ts | |
| 566 | +- Create src/feature/index.test.ts | |
| 567 | +- Set up the exports | |
| 568 | + | |
| 569 | +[Implementation + tests] | |
| 570 | + | |
| 571 | +📊 STEP 1 SUMMARY: | |
| 572 | +✅ Created: src/feature/index.ts, src/feature/types.ts | |
| 573 | +✅ Modified: src/index.ts (exports) | |
| 574 | +🧪 Tests: src/feature/index.test.ts — 4 passing (npm test) | |
| 575 | + | |
| 576 | +⏸️ CHECKPOINT - Approval? | |
| 577 | + | |
| 578 | +... | |
| 579 | + | |
| 580 | +=== PHASE 5: QUALITY GATE === | |
| 581 | +📊 Gate: PASS — errors: 0, warnings: 0, smells: 0 | |
| 582 | + | |
| 583 | +=== PHASE 6: DOCUMENTATION (EN + FR) === | |
| 584 | +✅ docs/en/how-to/use-feature.md, docs/en/reference/feature.md | |
| 585 | +✅ docs/fr/how-to/utiliser-feature.md, docs/fr/reference/feature.md | |
| 586 | + | |
| 587 | +=== PHASE 7: PROJECT MEMORY === | |
| 588 | +🧠 .memory/ updated (summary, history entry, handoff) | |
| 589 | + | |
| 590 | +=== PHASE 8: FINAL VALIDATION === | |
| 591 | +[Checklist + commit proposal] | |
| 592 | +``` | |
| 593 | + | |
| 594 | +## Notes | |
| 595 | + | |
| 596 | +This skill is designed to maximise the user's control while still benefiting from AI | |
| 597 | +assistance. It forces a stop at every step to avoid the common drift of AI assistants. | |
| 598 | + | |
| 599 | +The user always stays in charge and can step in at any time. | |
| 600 | + | |
| 601 | +The closing phases (quality, documentation, memory) are what stop a feature from being | |
| 602 | +"finished" in the narrow sense — code that runs — while leaving behind untested logic, | |
| 603 | +undocumented behaviour, and no trace of why any of it was done that way. | |
| new file mode 100644 | |||
| @@ -0,0 +1,603 @@ | |||
| 1 | +--- | ||
| 2 | +name: methodical-dev | ||
| 3 | +description: Guide the user through a structured development methodology, following best practices for working with Claude Code in a controlled and effective way. Every change is delivered complete: readable and documented code, unit tests with a documented way to run them, bilingual (EN + FR) Diátaxis documentation, a passing quality gate, and an updated .memory/ project record. Use when starting a feature, refactoring, or whenever you want a controlled, approval-gated development process. | ||
| 4 | +--- | ||
| 5 | +# Methodical Development Skill | ||
| 6 | + | ||
| 7 | +## Description | ||
| 8 | +Guide the user through a structured development methodology, following best practices for | ||
| 9 | +working with Claude Code in a controlled and effective way. | ||
| 10 | + | ||
| 11 | +The core promise: **no change is "done" until it is readable, tested, documented in both | ||
| 12 | +English and French, quality-gated, and recorded in the project's memory.** The phases below | ||
| 13 | +enforce that; none of them is optional. | ||
| 14 | + | ||
| 15 | +## When to Use | ||
| 16 | +- When starting a new feature | ||
| 17 | +- When refactoring or changing existing behaviour | ||
| 18 | +- When you want to follow a structured process | ||
| 19 | +- To avoid the common pitfalls of AI-assisted development | ||
| 20 | + | ||
| 21 | +## Companion skills | ||
| 22 | + | ||
| 23 | +This skill orchestrates two others. Both ship alongside it and must be **invoked as skills**, | ||
| 24 | +not reimplemented: | ||
| 25 | + | ||
| 26 | +| Skill | When this skill invokes it | Phase | | ||
| 27 | +| --- | --- | --- | | ||
| 28 | +| `quality` | After implementation, to measure quality and refactor until the gate passes | Phase 5 | | ||
| 29 | +| `diataxis-doc` | After the quality gate, to write or update the documentation | Phase 6 | | ||
| 30 | + | ||
| 31 | +## Instructions | ||
| 32 | + | ||
| 33 | +You are a skill that guides the user through a rigorous development methodology. You must | ||
| 34 | +follow this process step by step. | ||
| 35 | + | ||
| 36 | +--- | ||
| 37 | + | ||
| 38 | +## Phase 0: Read the project memory | ||
| 39 | + | ||
| 40 | +**Before asking the user anything**, read `.memory/` at the repository root: | ||
| 41 | + | ||
| 42 | +```bash | ||
| 43 | +ls -la .memory/ 2>/dev/null && cat .memory/summary.md 2>/dev/null | ||
| 44 | +``` | ||
| 45 | + | ||
| 46 | +- If `.memory/` exists, read `summary.md`, the tail of `history.md`, and the most recent file | ||
| 47 | + in `handoffs/`. It tells you the project's current state, past decisions, and any work left | ||
| 48 | + in flight — **do not ask the user for information that is already recorded there.** | ||
| 49 | +- If `.memory/` does not exist, you will create it in Phase 7. Do not create it yet. | ||
| 50 | + | ||
| 51 | +If a handoff records unfinished work, surface it now and ask whether to resume it or start | ||
| 52 | +something new. | ||
| 53 | + | ||
| 54 | +--- | ||
| 55 | + | ||
| 56 | +## Phase 1: Gathering Information | ||
| 57 | + | ||
| 58 | +Ask the user these questions using AskUserQuestion — skipping any the memory already answers: | ||
| 59 | + | ||
| 60 | +1. **Goal of the feature** | ||
| 61 | + - What feature do you want to develop? | ||
| 62 | + - What is the exact scope of this feature? | ||
| 63 | + | ||
| 64 | +2. **Technical Constraints** | ||
| 65 | + - Which frameworks/libraries must you use? | ||
| 66 | + - Are there any version constraints? | ||
| 67 | + - Are there architectural patterns to follow? | ||
| 68 | + | ||
| 69 | +3. **Documentation and Examples** | ||
| 70 | + - Do you have documentation to reference? | ||
| 71 | + - Do you have similar existing code that could serve as an example? | ||
| 72 | + | ||
| 73 | +4. **Style and Conventions** | ||
| 74 | + - Are there specific naming conventions? | ||
| 75 | + - Is there a particular code style to follow? | ||
| 76 | + | ||
| 77 | +--- | ||
| 78 | + | ||
| 79 | +## Phase 2: Git Check | ||
| 80 | + | ||
| 81 | +Check the state of the repository: | ||
| 82 | + | ||
| 83 | +```bash | ||
| 84 | +# Check that we are in a git repo | ||
| 85 | +git status | ||
| 86 | + | ||
| 87 | +# If there is no repo, offer to initialise one | ||
| 88 | +git init | ||
| 89 | +``` | ||
| 90 | + | ||
| 91 | +If the user is not on a dedicated branch, **strongly recommend** creating a feature branch. | ||
| 92 | + | ||
| 93 | +**IMPORTANT**: Do not create the branch automatically. Ask the user: | ||
| 94 | +- What branch name would they like? | ||
| 95 | +- Do they want you to create the branch, or would they rather do it themselves? | ||
| 96 | + | ||
| 97 | +--- | ||
| 98 | + | ||
| 99 | +## Phase 3: Detailed Planning | ||
| 100 | + | ||
| 101 | +1. **Analyse the existing code** (if needed) | ||
| 102 | + - Use Glob and Grep to understand the structure | ||
| 103 | + - Identify the files to modify | ||
| 104 | + - Identify the existing patterns to follow | ||
| 105 | + | ||
| 106 | +2. **Identify the project's test and build commands.** Look for a `Makefile`, `Taskfile.yml`, | ||
| 107 | + `package.json` scripts, `pyproject.toml`, or the CI workflow. You need these for Phase 4 | ||
| 108 | + and Phase 5, and you must reuse the project's existing convention rather than inventing a | ||
| 109 | + parallel one. | ||
| 110 | + | ||
| 111 | +3. **Create a detailed plan** using TodoWrite | ||
| 112 | + - Break the feature into logical steps (5–8 steps maximum) | ||
| 113 | + - Each step must be atomic and testable | ||
| 114 | + - Order the steps by dependency | ||
| 115 | + - **Every implementation step carries its own unit tests** — do not plan a single | ||
| 116 | + "write the tests" step at the end | ||
| 117 | + - Include the mandatory closing phases as plan items: quality gate, bilingual | ||
| 118 | + documentation, memory update | ||
| 119 | + | ||
| 120 | +4. **Present the plan** to the user | ||
| 121 | + - Explain each step | ||
| 122 | + - Ask for approval before continuing | ||
| 123 | + - Allow adjustments | ||
| 124 | + | ||
| 125 | +--- | ||
| 126 | + | ||
| 127 | +## Phase 4: Guided Implementation | ||
| 128 | + | ||
| 129 | +For each step of the plan: | ||
| 130 | + | ||
| 131 | +1. **Before starting the step** | ||
| 132 | + - Mark the step as `in_progress` with TodoWrite | ||
| 133 | + - Explain what you are about to do | ||
| 134 | + - Ask for confirmation if the step is complex | ||
| 135 | + | ||
| 136 | +2. **During the step** | ||
| 137 | + - Implement only what is planned for this step | ||
| 138 | + - Write the code to the **Code Standards** below — readable, maintainable, documented | ||
| 139 | + with usage examples | ||
| 140 | + - Write or update the **unit tests** for this step's behaviour (see Testing Requirements) | ||
| 141 | + - Do **NOT** take shortcuts | ||
| 142 | + - Do **NOT** delete existing code without asking | ||
| 143 | + - Do **NOT** change the architecture without agreement | ||
| 144 | + - Explain the technical choices as you go | ||
| 145 | + | ||
| 146 | +3. **After the step** | ||
| 147 | + - Run the tests and show the result — a step whose tests do not pass is not finished | ||
| 148 | + - Mark the step as `completed` with TodoWrite | ||
| 149 | + - Summarise what was done | ||
| 150 | + - List the files created/modified | ||
| 151 | + - **STOP and wait for the user's approval** | ||
| 152 | + | ||
| 153 | +4. **Mandatory checkpoint** | ||
| 154 | + - Ask the user to: | ||
| 155 | + - Review the code produced | ||
| 156 | + - Test the behaviour | ||
| 157 | + - Confirm it matches their request | ||
| 158 | + - Offer to: | ||
| 159 | + - Continue to the next step | ||
| 160 | + - Change something in the current step | ||
| 161 | + - Adjust the remaining plan | ||
| 162 | + | ||
| 163 | +--- | ||
| 164 | + | ||
| 165 | +## Phase 5: Quality Gate (mandatory) | ||
| 166 | + | ||
| 167 | +**Invoke the `quality` skill.** Do not measure quality by eye and do not skip this phase, | ||
| 168 | +even for a one-line change. | ||
| 169 | + | ||
| 170 | +The skill configures qlty if needed, measures lint issues / code smells / complexity, writes | ||
| 171 | +a report under `.quality/`, records the run in its history so progression is visible, and | ||
| 172 | +tells you whether the gate passed. | ||
| 173 | + | ||
| 174 | +- **Gate passed** → report the numbers and move to Phase 6. | ||
| 175 | +- **Gate failed** → refactor as the skill directs, re-run the tests from Phase 4, and | ||
| 176 | + re-measure. Repeat until the gate passes or the skill's own stopping conditions are hit | ||
| 177 | + (five iterations, or two runs with no improvement). | ||
| 178 | +- **Still failing at the stopping condition** → do not silently continue. Report what | ||
| 179 | + improved, what remains, and why, then ask the user how to proceed. | ||
| 180 | + | ||
| 181 | +Never satisfy the gate by weakening it. Editing `.qlty/qlty.toml` exclusions, lowering | ||
| 182 | +`.quality/gate.json` thresholds, or adding blanket lint suppressions is forbidden here for the | ||
| 183 | +same reason it is forbidden inside the `quality` skill: it makes the measurement lie. | ||
| 184 | + | ||
| 185 | +For a long multi-step feature, running the quality skill after a large step — not only at the | ||
| 186 | +end — catches drift earlier and is cheaper than one big cleanup. | ||
| 187 | + | ||
| 188 | +--- | ||
| 189 | + | ||
| 190 | +## Phase 6: Documentation (mandatory, bilingual) | ||
| 191 | + | ||
| 192 | +**Invoke the `diataxis-doc` skill** to write or update the documentation for what you just | ||
| 193 | +built. Every creation and every change gets documented — new behaviour, changed behaviour, | ||
| 194 | +and removed behaviour alike. | ||
| 195 | + | ||
| 196 | +> **Do not ask the user which language to document in.** `diataxis-doc` normally opens by | ||
| 197 | +> asking; when invoked from this skill the answer is already fixed: **Both** — English *and* | ||
| 198 | +> French. Tell the skill this so it goes straight to the bilingual architecture. | ||
| 199 | + | ||
| 200 | +This produces one subfolder per language, each with its own complete four-quadrant structure: | ||
| 201 | + | ||
| 202 | +``` | ||
| 203 | +docs/ | ||
| 204 | +├── README.md ← language selector | ||
| 205 | +├── en/ | ||
| 206 | +│ ├── README.md | ||
| 207 | +│ ├── tutorials/ ← "teach me to get started" | ||
| 208 | +│ ├── how-to/ ← "how do I do X?" | ||
| 209 | +│ ├── reference/ ← "what are the exact details of X?" | ||
| 210 | +│ └── explanation/ ← "why is it built this way?" | ||
| 211 | +└── fr/ | ||
| 212 | + └── … same structure | ||
| 213 | +``` | ||
| 214 | + | ||
| 215 | +Rules that matter here: | ||
| 216 | + | ||
| 217 | +- **Both languages stay in sync.** A change documented in English but not French is an | ||
| 218 | + incomplete change. Translate the content — do not copy the English text into `fr/`. | ||
| 219 | +- **Cross-links stay inside one language.** A French page never links to an English page. | ||
| 220 | +- **File the content with the Diátaxis compass**, do not dump everything into one page. New | ||
| 221 | + feature → usually a `how-to/` page plus a `reference/` entry; a design decision → | ||
| 222 | + `explanation/`. | ||
| 223 | +- **The way to run the tests is documented**, in both languages (see Testing Requirements). | ||
| 224 | +- **Every touched package's `README.md` is updated.** This is separate from `docs/` and applies to **all** packages, in whatever language that README already uses. Any package whose public surface you changed — a new, renamed, or removed exported function, type, method, flag, or CLI subcommand — gets its `README.md` brought back in sync **in the same change**. A README that lists part of a package's API but omits what you just added is a defect, not merely "a bit behind". If the repository keeps no per-package READMEs, this rule is inert — do not create them just to satisfy it. | ||
| 225 | +- **A package-dependency diagram is kept in draw.io format.** It shows every package/module, the dependency arrows between them (and on third-party runtimes), and a one-line "what it is for" on each. Keep it at `docs/diagrams/packages.drawio` (or the project's existing diagrams location). **Create it when it does not exist, and update it whenever you add, remove, or re-wire a package** so it never drifts from the real import graph. Keep it as a single language-neutral file (labels in the docs' primary language), referenced from the architecture explanation page in each language. A `.drawio` file is plain XML (an `mxGraphModel`) that diagrams.net and the VS Code Draw.io extension open directly, so you can author and edit it as text; after writing it, sanity-check that the XML parses. | ||
| 226 | + | ||
| 227 | +--- | ||
| 228 | + | ||
| 229 | +## Phase 7: Update the project memory (mandatory) | ||
| 230 | + | ||
| 231 | +Create or update `.memory/` at the repository root. This is the project's durable record: it | ||
| 232 | +is what lets you — or a different agent, in a fresh sandbox — pick the work up later without | ||
| 233 | +re-deriving everything. | ||
| 234 | + | ||
| 235 | +``` | ||
| 236 | +.memory/ | ||
| 237 | +├── README.md ← what this folder is and how it is maintained | ||
| 238 | +├── summary.md ← living snapshot of the project's current state (edited in place) | ||
| 239 | +├── history.md ← append-only chronological log (never rewritten) | ||
| 240 | +└── handoffs/ | ||
| 241 | + └── YYYY-MM-DD-<slug>.md ← one per session or feature | ||
| 242 | +``` | ||
| 243 | + | ||
| 244 | +`.memory/` is **committed to the repository**, not gitignored — that is the whole point. | ||
| 245 | + | ||
| 246 | +### `summary.md` — edited in place, never regenerated | ||
| 247 | + | ||
| 248 | +The current state of the project, kept short enough to stay read-worthy: | ||
| 249 | + | ||
| 250 | +- What the project is and does | ||
| 251 | +- Architecture: main components and how they fit together | ||
| 252 | +- Key technical decisions currently in force, and why | ||
| 253 | +- How to build, test, and run it (the actual commands) | ||
| 254 | +- Known limitations and open questions | ||
| 255 | + | ||
| 256 | +**Change only what this session establishes or invalidates, and leave the rest alone.** You have | ||
| 257 | +seen a slice of the project; this file holds what every previous session established. Regenerating | ||
| 258 | +it wholesale from your slice silently destroys accurate content you never looked at — the one way | ||
| 259 | +this file stops being trustworthy. Write only what you verified; anything you could not check goes | ||
| 260 | +under an explicit `## Not yet established` heading instead of a plausible guess, because the next | ||
| 261 | +session will trust whatever is written here. If the project's state did not change, leave the file | ||
| 262 | +untouched. | ||
| 263 | + | ||
| 264 | +### `history.md` — append only | ||
| 265 | + | ||
| 266 | +One dated entry per completed feature or session. **Never rewrite or delete past entries** — | ||
| 267 | +a history you edit is not a history. | ||
| 268 | + | ||
| 269 | +```markdown | ||
| 270 | +## 2026-07-25 — <what was done> | ||
| 271 | + | ||
| 272 | +- **Goal**: <what the user asked for> | ||
| 273 | +- **Changes**: <files/components created or modified> | ||
| 274 | +- **Decisions**: <choices made and why; alternatives rejected> | ||
| 275 | +- **Tests**: <what was added, how to run it> | ||
| 276 | +- **Quality**: <gate PASS/FAIL and the key numbers> | ||
| 277 | +- **Docs**: <pages created/updated, in both languages> | ||
| 278 | +``` | ||
| 279 | + | ||
| 280 | +### `handoffs/YYYY-MM-DD-<slug>.md` — written at the end of the session | ||
| 281 | + | ||
| 282 | +The document a fresh agent reads to resume: | ||
| 283 | + | ||
| 284 | +```markdown | ||
| 285 | +# Handoff — <date> — <topic> | ||
| 286 | + | ||
| 287 | +## State | ||
| 288 | +<what works right now; what was just finished> | ||
| 289 | + | ||
| 290 | +## In flight | ||
| 291 | +<work started but not finished, and exactly where it stopped> | ||
| 292 | + | ||
| 293 | +## Next steps | ||
| 294 | +1. <the concrete next action> | ||
| 295 | +2. … | ||
| 296 | + | ||
| 297 | +## Open questions / blockers | ||
| 298 | +<decisions awaiting the user; anything that blocked progress> | ||
| 299 | + | ||
| 300 | +## Watch out for | ||
| 301 | +<traps discovered along the way — failing commands, fragile assumptions> | ||
| 302 | +``` | ||
| 303 | + | ||
| 304 | +### Memory vs. documentation | ||
| 305 | + | ||
| 306 | +Keep the boundary clean, so content is not duplicated: | ||
| 307 | + | ||
| 308 | +| | Audience | Content | | ||
| 309 | +| --- | --- | --- | | ||
| 310 | +| `docs/` (Diátaxis) | **Users** of the project | How to use it, what the API is, why it is designed that way | | ||
| 311 | +| `.memory/` | **Whoever continues the work** | Project history, session state, handoffs, decisions in progress | | ||
| 312 | + | ||
| 313 | +If a piece of information helps someone *use* the project, it belongs in `docs/`. If it helps | ||
| 314 | +someone *continue building* it, it belongs in `.memory/`. | ||
| 315 | + | ||
| 316 | +--- | ||
| 317 | + | ||
| 318 | +## Phase 8: Final Validation | ||
| 319 | + | ||
| 320 | +Once all the steps and phases are complete: | ||
| 321 | + | ||
| 322 | +1. **Full summary** | ||
| 323 | + - List of all files created | ||
| 324 | + - List of all files modified | ||
| 325 | + - Summary of the features implemented | ||
| 326 | + | ||
| 327 | +2. **Quality checklist** | ||
| 328 | + - [ ] Does the feature match the request exactly? | ||
| 329 | + - [ ] No unrequested deletions? | ||
| 330 | + - [ ] Is the code readable, maintainable, and documented with usage examples? | ||
| 331 | + - [ ] Are unit tests present for every change, and do they pass? | ||
| 332 | + - [ ] Is there a documented, single command to run the tests? | ||
| 333 | + - [ ] Did the `quality` skill run, and did the gate pass? | ||
| 334 | + - [ ] Is the documentation updated in **both** English and French? | ||
| 335 | + - [ ] Is every touched package's `README.md` updated to match its current public API? | ||
| 336 | + - [ ] Is the package-dependency draw.io diagram present and consistent with the current import graph? | ||
| 337 | + - [ ] Are the conventions respected? | ||
| 338 | + - [ ] Is `.memory/` updated — `summary.md`, a new `history.md` entry, and a handoff? | ||
| 339 | + | ||
| 340 | +3. **Commit proposal** | ||
| 341 | + - Propose a structured commit message | ||
| 342 | + - List the files to add to the commit — including `docs/` and `.memory/` | ||
| 343 | + - Do **NOT** commit automatically | ||
| 344 | + - Let the user do it, or use the /commit skill | ||
| 345 | + | ||
| 346 | +--- | ||
| 347 | + | ||
| 348 | +## Code Standards | ||
| 349 | + | ||
| 350 | +Generated code must be readable and maintainable by a **human**, not merely correct. Someone | ||
| 351 | +unfamiliar with it should understand it without asking you. | ||
| 352 | + | ||
| 353 | +**Readability** | ||
| 354 | +- Names state intent: `retryAfterSeconds`, not `d` or `tmp2`. No abbreviations that are not | ||
| 355 | + domain-standard. | ||
| 356 | +- One function does one thing, and is short enough to read without scrolling. | ||
| 357 | +- Prefer the explicit over the clever. A dense one-liner that needs a comment to be understood | ||
| 358 | + should be several plain lines instead. | ||
| 359 | +- Early returns over deep nesting. | ||
| 360 | +- **Match the surrounding code.** Its existing style wins over your preferences. | ||
| 361 | + | ||
| 362 | +**Maintainability** | ||
| 363 | +- No duplicated logic — extract it the second time it appears. | ||
| 364 | +- Errors are handled where they can be handled meaningfully, never silently swallowed. | ||
| 365 | +- No dead code, no commented-out code, no `TODO` without a concrete follow-up noted in | ||
| 366 | + `.memory/handoffs/`. | ||
| 367 | +- Keep the public surface small: expose what callers need, no more. | ||
| 368 | + | ||
| 369 | +**Documentation in the code** | ||
| 370 | +- Every public/exported function, type, class, and module gets a doc comment saying what it | ||
| 371 | + does, what it expects, and what it returns or raises. | ||
| 372 | +- **Each public API's doc comment includes a short usage example** — a few lines a reader can | ||
| 373 | + copy. Use the language's idiom for this: Go `Example` functions (which are also tests), | ||
| 374 | + Python docstring examples, JSDoc `@example`, Rust doc-tests. | ||
| 375 | +- Comments explain **why**, not what. If a comment restates the code, delete it and improve | ||
| 376 | + the name instead. | ||
| 377 | +- Document non-obvious constraints and invariants at the point they apply. | ||
| 378 | + | ||
| 379 | +**Markdown and prose files** | ||
| 380 | +- Applies to every Markdown file you write — `docs/`, `.memory/`, `README`s. | ||
| 381 | +- **Do not hard-wrap prose.** Write each paragraph, list item, and blockquote as | ||
| 382 | + a single unwrapped line; never break a line in the middle of a sentence. Let the | ||
| 383 | + reader's editor soft-wrap. This keeps diffs meaningful — a reworded sentence | ||
| 384 | + touches one line, not a whole reflowed block. | ||
| 385 | +- Leave fenced code blocks, tables, and headings as they are (code keeps its own | ||
| 386 | + newlines; one table row per line; a heading on its own line). | ||
| 387 | +- When editing a file that was previously hard-wrapped, unwrap the blocks you touch. | ||
| 388 | + | ||
| 389 | +--- | ||
| 390 | + | ||
| 391 | +## Testing Requirements | ||
| 392 | + | ||
| 393 | +Every addition, creation, and change ships with tests. A change without tests is not finished. | ||
| 394 | + | ||
| 395 | +**What to write** | ||
| 396 | +- Unit tests for each new or modified behaviour, added in the same step as the code. | ||
| 397 | +- Cover the happy path **and** every error or edge case the code explicitly handles. | ||
| 398 | +- When fixing a bug, first write the test that reproduces it, and confirm it fails before the | ||
| 399 | + fix. | ||
| 400 | +- Tests are deterministic: no real network, no wall-clock or random dependence, no reliance on | ||
| 401 | + test execution order. | ||
| 402 | +- Test names state the behaviour under test, so a failure is legible without reading the body. | ||
| 403 | + | ||
| 404 | +**How to run them** | ||
| 405 | +- There must be **one documented command** that runs the whole suite. | ||
| 406 | +- **Reuse the project's existing convention** — add a `Makefile` / `Taskfile.yml` target, or a | ||
| 407 | + `package.json` script, if one of those is already in use. Only create a | ||
| 408 | + `scripts/test.sh`-style runner when the project has no such entry point, and make it | ||
| 409 | + executable (`chmod +x`) with a `set -eu` guard. | ||
| 410 | +- The command must work from a clean checkout, with no undocumented manual setup. | ||
| 411 | + | ||
| 412 | +**Where to document it** | ||
| 413 | +- In `docs/` under both languages — usually a `how-to/` page ("How to run the tests"). | ||
| 414 | +- In `.memory/summary.md`, in the build/test/run section. | ||
| 415 | +- In the project `README` if it already documents commands. | ||
| 416 | + | ||
| 417 | +--- | ||
| 418 | + | ||
| 419 | +## Strict Rules | ||
| 420 | + | ||
| 421 | +**You must NEVER:** | ||
| 422 | +- ❌ Create a commit without an explicit request | ||
| 423 | +- ❌ Delete existing code without confirmation | ||
| 424 | +- ❌ Change the architecture without agreement | ||
| 425 | +- ❌ Skip a step without approval | ||
| 426 | +- ❌ Continue if the user has not approved the previous step | ||
| 427 | +- ❌ Take shortcuts "to keep things simple" | ||
| 428 | +- ❌ Implement something different from what was asked | ||
| 429 | +- ❌ Declare a change done without tests, docs in both languages, an updated `README.md` for | ||
| 430 | + every touched package, a passing quality gate, and an updated `.memory/` | ||
| 431 | +- ❌ Weaken the quality gate, delete tests, or disable a linter to make a check pass | ||
| 432 | +- ❌ Document in only one language | ||
| 433 | +- ❌ Rewrite or delete past `history.md` entries | ||
| 434 | + | ||
| 435 | +**You must ALWAYS:** | ||
| 436 | +- ✅ Read `.memory/` before asking the user anything | ||
| 437 | +- ✅ Stop after each step for approval | ||
| 438 | +- ✅ Explain your technical choices | ||
| 439 | +- ✅ Ask for confirmation on important decisions | ||
| 440 | +- ✅ Follow the approved plan exactly | ||
| 441 | +- ✅ Be transparent about what you are doing | ||
| 442 | +- ✅ Propose alternatives if you see a problem | ||
| 443 | +- ✅ Write tests alongside the code, in the same step | ||
| 444 | +- ✅ Invoke the `quality` skill before declaring the work complete | ||
| 445 | +- ✅ Invoke the `diataxis-doc` skill for documentation, in English and French | ||
| 446 | +- ✅ Update `.memory/` at the end of the session | ||
| 447 | + | ||
| 448 | +--- | ||
| 449 | + | ||
| 450 | +## Handling Problems | ||
| 451 | + | ||
| 452 | +If you hit a problem during implementation: | ||
| 453 | + | ||
| 454 | +1. **STOP immediately** | ||
| 455 | +2. Explain the problem clearly | ||
| 456 | +3. Propose alternative solutions | ||
| 457 | +4. **Wait** for the user's decision | ||
| 458 | +5. **NEVER** work around the problem by deleting code | ||
| 459 | + | ||
| 460 | +If the session ends with the problem unresolved, record it in | ||
| 461 | +`.memory/handoffs/` under "Open questions / blockers" before stopping. | ||
| 462 | + | ||
| 463 | +--- | ||
| 464 | + | ||
| 465 | +## Communication Format | ||
| 466 | + | ||
| 467 | +Use this format to communicate clearly: | ||
| 468 | + | ||
| 469 | +``` | ||
| 470 | +=== STEP [N]: [Step name] === | ||
| 471 | + | ||
| 472 | +📋 What I am going to do: | ||
| 473 | +- [Action 1] | ||
| 474 | +- [Action 2] | ||
| 475 | + | ||
| 476 | +✅ Approval needed? [Yes/No] | ||
| 477 | + | ||
| 478 | +[If Yes, wait for a reply before continuing] | ||
| 479 | + | ||
| 480 | +--- | ||
| 481 | + | ||
| 482 | +[Implementation + tests] | ||
| 483 | + | ||
| 484 | +--- | ||
| 485 | + | ||
| 486 | +📊 STEP [N] SUMMARY: | ||
| 487 | +✅ Created: [file1], [file2] | ||
| 488 | +✅ Modified: [file3] | ||
| 489 | +🧪 Tests: [what was added] — [pass/fail, command used] | ||
| 490 | +✅ Feature: [description] | ||
| 491 | + | ||
| 492 | +⏸️ CHECKPOINT | ||
| 493 | +Please review and approve before continuing. | ||
| 494 | + | ||
| 495 | +Options: | ||
| 496 | +1. ✅ Continue to the next step | ||
| 497 | +2. 🔧 Change something | ||
| 498 | +3. 📝 Adjust the plan | ||
| 499 | +``` | ||
| 500 | + | ||
| 501 | +For the closing phases: | ||
| 502 | + | ||
| 503 | +``` | ||
| 504 | +=== PHASE 5: QUALITY GATE === | ||
| 505 | +🔍 Invoking the `quality` skill… | ||
| 506 | +📊 Gate: [PASS/FAIL] — errors: [n], warnings: [n], smells: [n] | ||
| 507 | +[If FAIL: refactoring, then re-measuring] | ||
| 508 | + | ||
| 509 | +=== PHASE 6: DOCUMENTATION (EN + FR) === | ||
| 510 | +📚 Invoking the `diataxis-doc` skill (language: Both)… | ||
| 511 | +✅ docs/en/: [pages] | ||
| 512 | +✅ docs/fr/: [pages] | ||
| 513 | + | ||
| 514 | +=== PHASE 7: PROJECT MEMORY === | ||
| 515 | +🧠 .memory/summary.md — updated | ||
| 516 | +🧠 .memory/history.md — entry appended | ||
| 517 | +🧠 .memory/handoffs/… — written | ||
| 518 | +``` | ||
| 519 | + | ||
| 520 | +--- | ||
| 521 | + | ||
| 522 | +## Usage Example | ||
| 523 | + | ||
| 524 | +``` | ||
| 525 | +User: /methodical-dev | ||
| 526 | + | ||
| 527 | +Skill: I will guide you through a methodical development process. | ||
| 528 | + | ||
| 529 | +=== PHASE 0: PROJECT MEMORY === | ||
| 530 | + | ||
| 531 | +[Reads .memory/summary.md, history.md, handoffs/] | ||
| 532 | + | ||
| 533 | +Found a handoff from 2026-07-20: the parser is done, the encoder was left | ||
| 534 | +half-written. Resume that, or start something new? | ||
| 535 | + | ||
| 536 | +=== PHASE 1: GATHERING INFORMATION === | ||
| 537 | + | ||
| 538 | +[Asks the remaining questions via AskUserQuestion] | ||
| 539 | + | ||
| 540 | +=== PHASE 2: GIT CHECK === | ||
| 541 | + | ||
| 542 | +[Checks git status] | ||
| 543 | + | ||
| 544 | +=== PHASE 3: PLANNING === | ||
| 545 | + | ||
| 546 | +Here is the proposed plan: | ||
| 547 | + | ||
| 548 | +□ Step 1: Create the base structure + tests | ||
| 549 | +□ Step 2: Implement the business logic + tests | ||
| 550 | +□ Step 3: Wire up the public API + doc comments with examples | ||
| 551 | +□ Step 4: Quality gate (quality skill) | ||
| 552 | +□ Step 5: Documentation EN + FR (diataxis-doc skill) | ||
| 553 | +□ Step 6: Update .memory/ | ||
| 554 | + | ||
| 555 | +Does this plan work for you? | ||
| 556 | + | ||
| 557 | +[Wait for approval] | ||
| 558 | + | ||
| 559 | +=== PHASE 4: IMPLEMENTATION === | ||
| 560 | + | ||
| 561 | +=== STEP 1: Create the base structure === | ||
| 562 | + | ||
| 563 | +📋 What I am going to do: | ||
| 564 | +- Create src/feature/index.ts | ||
| 565 | +- Create src/feature/types.ts | ||
| 566 | +- Create src/feature/index.test.ts | ||
| 567 | +- Set up the exports | ||
| 568 | + | ||
| 569 | +[Implementation + tests] | ||
| 570 | + | ||
| 571 | +📊 STEP 1 SUMMARY: | ||
| 572 | +✅ Created: src/feature/index.ts, src/feature/types.ts | ||
| 573 | +✅ Modified: src/index.ts (exports) | ||
| 574 | +🧪 Tests: src/feature/index.test.ts — 4 passing (npm test) | ||
| 575 | + | ||
| 576 | +⏸️ CHECKPOINT - Approval? | ||
| 577 | + | ||
| 578 | +... | ||
| 579 | + | ||
| 580 | +=== PHASE 5: QUALITY GATE === | ||
| 581 | +📊 Gate: PASS — errors: 0, warnings: 0, smells: 0 | ||
| 582 | + | ||
| 583 | +=== PHASE 6: DOCUMENTATION (EN + FR) === | ||
| 584 | +✅ docs/en/how-to/use-feature.md, docs/en/reference/feature.md | ||
| 585 | +✅ docs/fr/how-to/utiliser-feature.md, docs/fr/reference/feature.md | ||
| 586 | + | ||
| 587 | +=== PHASE 7: PROJECT MEMORY === | ||
| 588 | +🧠 .memory/ updated (summary, history entry, handoff) | ||
| 589 | + | ||
| 590 | +=== PHASE 8: FINAL VALIDATION === | ||
| 591 | +[Checklist + commit proposal] | ||
| 592 | +``` | ||
| 593 | + | ||
| 594 | +## Notes | ||
| 595 | + | ||
| 596 | +This skill is designed to maximise the user's control while still benefiting from AI | ||
| 597 | +assistance. It forces a stop at every step to avoid the common drift of AI assistants. | ||
| 598 | + | ||
| 599 | +The user always stays in charge and can step in at any time. | ||
| 600 | + | ||
| 601 | +The closing phases (quality, documentation, memory) are what stop a feature from being | ||
| 602 | +"finished" in the narrow sense — code that runs — while leaving behind untested logic, | ||
| 603 | +undocumented behaviour, and no trace of why any of it was done that way. | ||
added
.claude/skills/quality/SKILL.md +266 -0 | new file mode 100644 | ||
| @@ -0,0 +1,266 @@ | ||
| 1 | +--- | |
| 2 | +name: quality | |
| 3 | +description: Audit and improve this project's code quality with qlty. Sets qlty up on first use, measures lint issues / code smells / complexity, writes a timestamped Markdown report under .quality/, tracks progression across runs, and refactors until the quality gate passes. Use when asked to check code quality, run a quality report, review technical debt, see whether quality is improving, or clean up / refactor the codebase. | |
| 4 | +--- | |
| 5 | + | |
| 6 | +# Quality | |
| 7 | + | |
| 8 | +Measure this project's code quality with [qlty](https://docs.qlty.sh), report it, and | |
| 9 | +refactor until the gate passes. | |
| 10 | + | |
| 11 | +The measurement is a script, not a judgement call — run it, then act on what it says. | |
| 12 | +It is read-only with respect to the source tree (`qlty check` runs with `--no-fix`), so | |
| 13 | +measuring never quietly rewrites the code you are about to reason about. | |
| 14 | + | |
| 15 | +## 1. Set up (only if needed) | |
| 16 | + | |
| 17 | +The setup is self-healing; just run the script. It verifies `qlty` is on `PATH`, that | |
| 18 | +the workspace is a git repository, and runs `qlty init --yes` when there is no | |
| 19 | +`.qlty/qlty.toml` yet. | |
| 20 | + | |
| 21 | +If `qlty` is missing entirely, the sandbox was not created with the `dev-toolkit` | |
| 22 | +kit. Say so and stop — do not install qlty by hand. | |
| 23 | + | |
| 24 | +**If the project vendors this kit** — a `kits/` directory holding the sources that get | |
| 25 | +installed into `~/.claude/` — add it to `exclude_patterns` in `.qlty/qlty.toml` on the | |
| 26 | +first run: | |
| 27 | + | |
| 28 | +```toml | |
| 29 | +exclude_patterns = [ | |
| 30 | + # … the defaults qlty init wrote … | |
| 31 | + "kits/**", # the kit's own sources, including this skill's quality_report.py | |
| 32 | +] | |
| 33 | +``` | |
| 34 | + | |
| 35 | +This is **not** an exception to "do not game the gate" below. The reason is narrower and | |
| 36 | +holds regardless of what the numbers say: `quality_report.py` is the script that | |
| 37 | +*produces* the measurement, so leaving it in scope means measuring the instrument with | |
| 38 | +itself. Two of its findings are structurally unfixable — a script whose job is to run | |
| 39 | +`qlty` must import and call `subprocess`, which every Python security linter flags. | |
| 40 | + | |
| 41 | +Two conditions make this legitimate rather than convenient, and both must hold: | |
| 42 | + | |
| 43 | +- **It is the project's decision, not yours.** Say what the exclusion hides and why, and | |
| 44 | + let the user choose. Adding it unilaterally is the forbidden move. | |
| 45 | +- **Write down what it hides.** Put the actual findings in a comment next to the pattern. | |
| 46 | + Some of them are usually real defects in the script — over-complex functions, unused | |
| 47 | + imports — and they should be fixed at the kit's source rather than forgotten. An | |
| 48 | + exclusion that records what it silences stays honest; one that just makes a number | |
| 49 | + drop is the start of the drift. | |
| 50 | + | |
| 51 | +## 2. Measure | |
| 52 | + | |
| 53 | +From the project root: | |
| 54 | + | |
| 55 | +```bash | |
| 56 | +python3 ~/.claude/skills/quality/scripts/quality_report.py --workspace . | |
| 57 | +``` | |
| 58 | + | |
| 59 | +On a cold cache the first run takes several minutes: qlty downloads its plugin | |
| 60 | +definitions and the linter binaries. Subsequent runs are fast (the cache lives on a | |
| 61 | +persistent volume at `~/.qlty`). | |
| 62 | + | |
| 63 | +The script: | |
| 64 | + | |
| 65 | +- runs `qlty check --all`, `qlty smells --all`, and `qlty metrics --all` | |
| 66 | +- writes `.quality/report-<timestamp>.md` and `.quality/report-latest.md` | |
| 67 | +- appends one JSON line to `.quality/history.jsonl` | |
| 68 | +- prints the report to stdout | |
| 69 | +- **exits `0` if the gate passed, `1` if it failed, `2` on a tooling error** | |
| 70 | + | |
| 71 | +Use the exit code as the loop condition. Do not re-derive the verdict yourself. | |
| 72 | + | |
| 73 | +## 3. Read the report | |
| 74 | + | |
| 75 | +The report contains: | |
| 76 | + | |
| 77 | +| Section | What to do with it | | |
| 78 | +| --- | --- | | |
| 79 | +| **Gate violations** | The work list. Each line is a threshold that was breached. | | |
| 80 | +| **Lint issues** by level, top rules, most affected files | Where to start — but read the count correctly first, see below. | | |
| 81 | +| **Code smells** | Duplication and over-complex structure — these need real refactoring, not a formatter. | | |
| 82 | +| **Metrics** | `complex` / `cyclo` per file; the "Most complex files" table names refactoring targets. | | |
| 83 | +| **Trend** | Last 10 runs. This is how you show progression. | | |
| 84 | +| **Tooling notes** | Present only when a qlty invocation failed — investigate before trusting the numbers. | | |
| 85 | + | |
| 86 | +### Read the count before you trust it | |
| 87 | + | |
| 88 | +"Fix the highest-count rule first" is the right instinct and the wrong first step. A | |
| 89 | +count is `occurrences`, not `distinct problems`, and the two diverge badly in two common | |
| 90 | +cases: | |
| 91 | + | |
| 92 | +- **One finding, repeated per manifest.** `osv-scanner` reports a dependency CVE once per | |
| 93 | + `go.mod` / `package.json` it appears in. A monorepo with a dozen manifests turns one | |
| 94 | + vulnerable version into a hundred-plus findings, and adding a three-line example | |
| 95 | + manifest multiplies the total without degrading anything. | |
| 96 | +- **One finding, repeated per file.** A linter that fails to parse the language emits one | |
| 97 | + finding per source file. That looks like the largest cluster in the report and is worth | |
| 98 | + exactly nothing. | |
| 99 | + | |
| 100 | +And before treating a *delta* as a regression: **re-run.** A count that moves while the | |
| 101 | +tree is untouched is measuring the tool, not the code. The usual cause is a linter | |
| 102 | +answering from its own partially-populated cache — qlty invokes several with | |
| 103 | +`--allow-parallel-runners`, and an invocation returning in a fraction of a second did | |
| 104 | +not re-analyse anything. That failure mode **under-reports**: the low number is the | |
| 105 | +wrong one, so it looks like an improvement. | |
| 106 | + | |
| 107 | +When you suspect it, clear that linter's cache and run its binary directly, several | |
| 108 | +times, until the result is stable; `.qlty/out/invoke-*.yaml` records the exact command, | |
| 109 | +environment and duration qlty used. Work from that figure, not from the report, and say | |
| 110 | +in your report which number you trust and why. Remember too that **a gate threshold | |
| 111 | +pinned to an unstable measurement will trip on its own** — give it margin, and write | |
| 112 | +down that the margin is the tool's fault, so it gets removed rather than inherited. | |
| 113 | + | |
| 114 | +So before picking a target, check **how many distinct rules and how many distinct files** | |
| 115 | +a count spans: | |
| 116 | + | |
| 117 | +```bash | |
| 118 | +# qlty check has no --json; SARIF is the machine-readable form | |
| 119 | +qlty check --all --no-fix --no-upgrade-check --sarif > /tmp/qlty.sarif | |
| 120 | +``` | |
| 121 | + | |
| 122 | +Then group the results by `ruleId` and by file URI. A rule spanning many files with one | |
| 123 | +occurrence each, or many rules spanning the same short file list, means the count is | |
| 124 | +telling you about the *shape of the scan*, not the state of the code. Say so in your | |
| 125 | +report — a total nobody can interpret gets ignored, and then a real regression hides in | |
| 126 | +it. | |
| 127 | + | |
| 128 | +## 4. Refactor until the gate passes | |
| 129 | + | |
| 130 | +If the gate failed, refactor. Loop: | |
| 131 | + | |
| 132 | +1. Pick the largest cluster of violations from the report (one rule, or one file). | |
| 133 | +2. Fix formatting mechanically first — `qlty fmt` handles it, and it clears a lot of | |
| 134 | + noise cheaply: | |
| 135 | + ```bash | |
| 136 | + qlty fmt --all --no-upgrade-check | |
| 137 | + ``` | |
| 138 | +3. Fix the remaining issues by editing the code. For smells, prefer extracting a | |
| 139 | + function or collapsing a duplicated block over suppressing the finding. | |
| 140 | +4. Verify nothing broke, using **this project's own** build and test commands. The | |
| 141 | + repository's tooling is the authority on what those are — check for a `Makefile`, | |
| 142 | + `Taskfile.yml`, `package.json` scripts, `pyproject.toml`, or the CI workflow before | |
| 143 | + assuming. Common cases: | |
| 144 | + | |
| 145 | + | Stack | Verification | | |
| 146 | + | --- | --- | | |
| 147 | + | Go | `go build ./... && go test ./...` | | |
| 148 | + | Go targeting WebAssembly | also `tinygo build -target=wasip1 ./...` — TinyGo rejects some constructs the standard toolchain accepts, so a passing `go build` is not sufficient | | |
| 149 | + | Node / TypeScript | `npm test` (or the script the project defines), plus `tsc --noEmit` if the project is typed | | |
| 150 | + | Python | `pytest` | | |
| 151 | + | Rust | `cargo build && cargo test && cargo clippy -- -D warnings` | | |
| 152 | + | Rust targeting WebAssembly | also `cargo build --target wasm32-wasip2` — and note the asymmetry below | | |
| 153 | + | VS Code extension | the compile script, then `vsce package` — packaging validates the manifest, which `tsc` does not | | |
| 154 | + | Zed extension | `cargo build --release --target wasm32-wasip2` | | |
| 155 | + | |
| 156 | + If the project has no tests, say so in the report rather than letting a green build | |
| 157 | + stand in as proof the refactoring was safe. | |
| 158 | + | |
| 159 | + **Two traps specific to WebAssembly targets.** Both let a refactoring look verified | |
| 160 | + when it is not: | |
| 161 | + | |
| 162 | + - A **wasm build passing does not mean the tests ran.** `cargo build --target | |
| 163 | + wasm32-wasip2` links with rustc's own bundled `rust-lld`, so it succeeds even | |
| 164 | + with no C toolchain present, while `cargo test` — which builds for the *host* — | |
| 165 | + fails at link time with ``error: linker `cc` not found``. If you see that error, | |
| 166 | + the tests did not run; do not report the wasm build as verification. | |
| 167 | + - An **extension cannot be exercised here.** No editor runs in this sandbox, so | |
| 168 | + neither VS Code's Extension Development Host nor Zed's *Install Dev Extension* | |
| 169 | + is available, and `@vscode/test-electron` needs an X server this kit does not | |
| 170 | + install. Compile, unit-test and package — then say plainly that the integration | |
| 171 | + path is untested rather than implying it passed. | |
| 172 | +5. Re-run the measurement from step 2 and compare against the previous run. | |
| 173 | + | |
| 174 | +Stop when any of these is true: | |
| 175 | + | |
| 176 | +- **The gate passes.** Report the before/after numbers from the Trend table. | |
| 177 | +- **Five iterations have run.** Stop and report honestly: what improved, what remains, | |
| 178 | + and why the rest is hard. | |
| 179 | +- **Two consecutive runs show no improvement.** You are stuck — stop and explain what | |
| 180 | + the blocker is rather than churning the code. | |
| 181 | + | |
| 182 | +Commit as you go, one focused commit per cluster of fixes, so the progression is | |
| 183 | +visible in git as well as in `.quality/history.jsonl`. | |
| 184 | + | |
| 185 | +## Do not game the gate | |
| 186 | + | |
| 187 | +The gate is only worth something if it is measuring the code. All of the following make | |
| 188 | +the number go down while making the project worse — never do them, even if the gate is | |
| 189 | +the only thing standing between you and "done": | |
| 190 | + | |
| 191 | +- Adding `exclude_patterns`, disabling a plugin, setting a plugin to `mode = "monitor"`, | |
| 192 | + or raising a threshold in `.qlty/qlty.toml`. | |
| 193 | +- Loosening `.quality/gate.json`. | |
| 194 | +- Deleting, skipping, or emptying tests. | |
| 195 | +- Adding blanket lint suppressions (`//nolint`, `// qlty-ignore`) to silence a finding | |
| 196 | + you could fix. | |
| 197 | + | |
| 198 | +A narrowly-scoped suppression with a comment explaining why the finding is a false | |
| 199 | +positive is legitimate. If you believe a threshold is genuinely wrong for this project, | |
| 200 | +say so and let the user decide — do not change it yourself. | |
| 201 | + | |
| 202 | +### The one plugin you may remove: a broken one | |
| 203 | + | |
| 204 | +"Disabling a plugin" above means a plugin that *finds things you would rather not fix*. A | |
| 205 | +plugin that **analyses nothing** is a different object, and keeping it is not rigour: its | |
| 206 | +findings crowd out the ones that matter and make the total unreadable. | |
| 207 | + | |
| 208 | +The two are indistinguishable from the report alone, so removal requires a **reproducible | |
| 209 | +diagnosis**, never an impression. Establish all four of these before touching the config: | |
| 210 | + | |
| 211 | +1. **It is not the language version or a specific construct.** Put a trivially small, | |
| 212 | + unambiguously valid file of that language in a *fresh* git repository, enable only | |
| 213 | + that plugin, and run it. If a five-line hello-world fails, no source construct is to | |
| 214 | + blame. | |
| 215 | +2. **It is not this project.** Same point — the probe repository shares nothing with the | |
| 216 | + codebase under measurement. | |
| 217 | +3. **It is not the environment qlty gives plugins.** qlty runs plugins with a stripped | |
| 218 | + environment (empty `PATH` and `HOME`). Run the plugin's own binary directly, with your | |
| 219 | + full environment, on the same probe file. Read the invocation qlty used from | |
| 220 | + `.qlty/out/invoke-*.yaml`, which records the exact `script`, `env` and `stderr`. | |
| 221 | +4. **It is not the plugin family.** If sibling plugins sharing the same runtime produce | |
| 222 | + real findings, the failure is specific rather than environmental. | |
| 223 | + | |
| 224 | +If all four hold, remove the plugin — and **write the diagnosis as a comment where you | |
| 225 | +removed it**, so the next session finds the evidence instead of re-adding it on instinct. | |
| 226 | +Note in your report which other tooling still covers that ground; if nothing does, the | |
| 227 | +answer is to replace the plugin, not merely to drop it. | |
| 228 | + | |
| 229 | +`mode = "monitor"` is *not* the remedy here. It does not stop a plugin's findings from | |
| 230 | +counting; it only changes how they are surfaced. A broken plugin left in monitor mode | |
| 231 | +keeps polluting the total, which is how one ends up unnoticed for months. | |
| 232 | + | |
| 233 | +**Do not encode "plugin X is broken" into this skill.** Such a failure belongs to one | |
| 234 | +version, one platform and one moment; a list of known-bad plugins ages badly and, worse, | |
| 235 | +tempts the next agent to skip the diagnosis. The method above is what belongs here — its | |
| 236 | +results belong in the project's own config and notes. | |
| 237 | + | |
| 238 | +## Tuning the gate | |
| 239 | + | |
| 240 | +Defaults are strict: zero error-level issues, zero warning-level issues, zero smells. | |
| 241 | +Override any subset by writing `.quality/gate.json`: | |
| 242 | + | |
| 243 | +```json | |
| 244 | +{ | |
| 245 | + "max_error": 0, | |
| 246 | + "max_warning": 0, | |
| 247 | + "max_note": null, | |
| 248 | + "max_smells": 0, | |
| 249 | + "max_file_complexity": 60, | |
| 250 | + "max_total_complexity": null | |
| 251 | +} | |
| 252 | +``` | |
| 253 | + | |
| 254 | +`null` disables a check. `max_file_complexity` flags individual files whose `complex` | |
| 255 | +metric exceeds the limit, which is the most useful knob for a codebase with existing | |
| 256 | +debt: it lets you hold the line on the worst files without demanding a perfect score | |
| 257 | +everywhere at once. | |
| 258 | + | |
| 259 | +## Reference | |
| 260 | + | |
| 261 | +- `qlty check --help`, `qlty smells --help`, `qlty metrics --help` for the full flag set. | |
| 262 | +- Note `qlty check` has **no** `--json`; machine-readable output is `--sarif`. | |
| 263 | +- Pass `--no-upgrade-check` to every qlty command — the sandbox blocks its update host. | |
| 264 | +- Reports and history under `.quality/` are meant to be committed so progression | |
| 265 | + survives sandbox recreation. Add `.quality/` to `.gitignore` if the user prefers them | |
| 266 | + to stay local. | |
| new file mode 100644 | |||
| @@ -0,0 +1,266 @@ | |||
| 1 | +--- | ||
| 2 | +name: quality | ||
| 3 | +description: Audit and improve this project's code quality with qlty. Sets qlty up on first use, measures lint issues / code smells / complexity, writes a timestamped Markdown report under .quality/, tracks progression across runs, and refactors until the quality gate passes. Use when asked to check code quality, run a quality report, review technical debt, see whether quality is improving, or clean up / refactor the codebase. | ||
| 4 | +--- | ||
| 5 | + | ||
| 6 | +# Quality | ||
| 7 | + | ||
| 8 | +Measure this project's code quality with [qlty](https://docs.qlty.sh), report it, and | ||
| 9 | +refactor until the gate passes. | ||
| 10 | + | ||
| 11 | +The measurement is a script, not a judgement call — run it, then act on what it says. | ||
| 12 | +It is read-only with respect to the source tree (`qlty check` runs with `--no-fix`), so | ||
| 13 | +measuring never quietly rewrites the code you are about to reason about. | ||
| 14 | + | ||
| 15 | +## 1. Set up (only if needed) | ||
| 16 | + | ||
| 17 | +The setup is self-healing; just run the script. It verifies `qlty` is on `PATH`, that | ||
| 18 | +the workspace is a git repository, and runs `qlty init --yes` when there is no | ||
| 19 | +`.qlty/qlty.toml` yet. | ||
| 20 | + | ||
| 21 | +If `qlty` is missing entirely, the sandbox was not created with the `dev-toolkit` | ||
| 22 | +kit. Say so and stop — do not install qlty by hand. | ||
| 23 | + | ||
| 24 | +**If the project vendors this kit** — a `kits/` directory holding the sources that get | ||
| 25 | +installed into `~/.claude/` — add it to `exclude_patterns` in `.qlty/qlty.toml` on the | ||
| 26 | +first run: | ||
| 27 | + | ||
| 28 | +```toml | ||
| 29 | +exclude_patterns = [ | ||
| 30 | + # … the defaults qlty init wrote … | ||
| 31 | + "kits/**", # the kit's own sources, including this skill's quality_report.py | ||
| 32 | +] | ||
| 33 | +``` | ||
| 34 | + | ||
| 35 | +This is **not** an exception to "do not game the gate" below. The reason is narrower and | ||
| 36 | +holds regardless of what the numbers say: `quality_report.py` is the script that | ||
| 37 | +*produces* the measurement, so leaving it in scope means measuring the instrument with | ||
| 38 | +itself. Two of its findings are structurally unfixable — a script whose job is to run | ||
| 39 | +`qlty` must import and call `subprocess`, which every Python security linter flags. | ||
| 40 | + | ||
| 41 | +Two conditions make this legitimate rather than convenient, and both must hold: | ||
| 42 | + | ||
| 43 | +- **It is the project's decision, not yours.** Say what the exclusion hides and why, and | ||
| 44 | + let the user choose. Adding it unilaterally is the forbidden move. | ||
| 45 | +- **Write down what it hides.** Put the actual findings in a comment next to the pattern. | ||
| 46 | + Some of them are usually real defects in the script — over-complex functions, unused | ||
| 47 | + imports — and they should be fixed at the kit's source rather than forgotten. An | ||
| 48 | + exclusion that records what it silences stays honest; one that just makes a number | ||
| 49 | + drop is the start of the drift. | ||
| 50 | + | ||
| 51 | +## 2. Measure | ||
| 52 | + | ||
| 53 | +From the project root: | ||
| 54 | + | ||
| 55 | +```bash | ||
| 56 | +python3 ~/.claude/skills/quality/scripts/quality_report.py --workspace . | ||
| 57 | +``` | ||
| 58 | + | ||
| 59 | +On a cold cache the first run takes several minutes: qlty downloads its plugin | ||
| 60 | +definitions and the linter binaries. Subsequent runs are fast (the cache lives on a | ||
| 61 | +persistent volume at `~/.qlty`). | ||
| 62 | + | ||
| 63 | +The script: | ||
| 64 | + | ||
| 65 | +- runs `qlty check --all`, `qlty smells --all`, and `qlty metrics --all` | ||
| 66 | +- writes `.quality/report-<timestamp>.md` and `.quality/report-latest.md` | ||
| 67 | +- appends one JSON line to `.quality/history.jsonl` | ||
| 68 | +- prints the report to stdout | ||
| 69 | +- **exits `0` if the gate passed, `1` if it failed, `2` on a tooling error** | ||
| 70 | + | ||
| 71 | +Use the exit code as the loop condition. Do not re-derive the verdict yourself. | ||
| 72 | + | ||
| 73 | +## 3. Read the report | ||
| 74 | + | ||
| 75 | +The report contains: | ||
| 76 | + | ||
| 77 | +| Section | What to do with it | | ||
| 78 | +| --- | --- | | ||
| 79 | +| **Gate violations** | The work list. Each line is a threshold that was breached. | | ||
| 80 | +| **Lint issues** by level, top rules, most affected files | Where to start — but read the count correctly first, see below. | | ||
| 81 | +| **Code smells** | Duplication and over-complex structure — these need real refactoring, not a formatter. | | ||
| 82 | +| **Metrics** | `complex` / `cyclo` per file; the "Most complex files" table names refactoring targets. | | ||
| 83 | +| **Trend** | Last 10 runs. This is how you show progression. | | ||
| 84 | +| **Tooling notes** | Present only when a qlty invocation failed — investigate before trusting the numbers. | | ||
| 85 | + | ||
| 86 | +### Read the count before you trust it | ||
| 87 | + | ||
| 88 | +"Fix the highest-count rule first" is the right instinct and the wrong first step. A | ||
| 89 | +count is `occurrences`, not `distinct problems`, and the two diverge badly in two common | ||
| 90 | +cases: | ||
| 91 | + | ||
| 92 | +- **One finding, repeated per manifest.** `osv-scanner` reports a dependency CVE once per | ||
| 93 | + `go.mod` / `package.json` it appears in. A monorepo with a dozen manifests turns one | ||
| 94 | + vulnerable version into a hundred-plus findings, and adding a three-line example | ||
| 95 | + manifest multiplies the total without degrading anything. | ||
| 96 | +- **One finding, repeated per file.** A linter that fails to parse the language emits one | ||
| 97 | + finding per source file. That looks like the largest cluster in the report and is worth | ||
| 98 | + exactly nothing. | ||
| 99 | + | ||
| 100 | +And before treating a *delta* as a regression: **re-run.** A count that moves while the | ||
| 101 | +tree is untouched is measuring the tool, not the code. The usual cause is a linter | ||
| 102 | +answering from its own partially-populated cache — qlty invokes several with | ||
| 103 | +`--allow-parallel-runners`, and an invocation returning in a fraction of a second did | ||
| 104 | +not re-analyse anything. That failure mode **under-reports**: the low number is the | ||
| 105 | +wrong one, so it looks like an improvement. | ||
| 106 | + | ||
| 107 | +When you suspect it, clear that linter's cache and run its binary directly, several | ||
| 108 | +times, until the result is stable; `.qlty/out/invoke-*.yaml` records the exact command, | ||
| 109 | +environment and duration qlty used. Work from that figure, not from the report, and say | ||
| 110 | +in your report which number you trust and why. Remember too that **a gate threshold | ||
| 111 | +pinned to an unstable measurement will trip on its own** — give it margin, and write | ||
| 112 | +down that the margin is the tool's fault, so it gets removed rather than inherited. | ||
| 113 | + | ||
| 114 | +So before picking a target, check **how many distinct rules and how many distinct files** | ||
| 115 | +a count spans: | ||
| 116 | + | ||
| 117 | +```bash | ||
| 118 | +# qlty check has no --json; SARIF is the machine-readable form | ||
| 119 | +qlty check --all --no-fix --no-upgrade-check --sarif > /tmp/qlty.sarif | ||
| 120 | +``` | ||
| 121 | + | ||
| 122 | +Then group the results by `ruleId` and by file URI. A rule spanning many files with one | ||
| 123 | +occurrence each, or many rules spanning the same short file list, means the count is | ||
| 124 | +telling you about the *shape of the scan*, not the state of the code. Say so in your | ||
| 125 | +report — a total nobody can interpret gets ignored, and then a real regression hides in | ||
| 126 | +it. | ||
| 127 | + | ||
| 128 | +## 4. Refactor until the gate passes | ||
| 129 | + | ||
| 130 | +If the gate failed, refactor. Loop: | ||
| 131 | + | ||
| 132 | +1. Pick the largest cluster of violations from the report (one rule, or one file). | ||
| 133 | +2. Fix formatting mechanically first — `qlty fmt` handles it, and it clears a lot of | ||
| 134 | + noise cheaply: | ||
| 135 | + ```bash | ||
| 136 | + qlty fmt --all --no-upgrade-check | ||
| 137 | + ``` | ||
| 138 | +3. Fix the remaining issues by editing the code. For smells, prefer extracting a | ||
| 139 | + function or collapsing a duplicated block over suppressing the finding. | ||
| 140 | +4. Verify nothing broke, using **this project's own** build and test commands. The | ||
| 141 | + repository's tooling is the authority on what those are — check for a `Makefile`, | ||
| 142 | + `Taskfile.yml`, `package.json` scripts, `pyproject.toml`, or the CI workflow before | ||
| 143 | + assuming. Common cases: | ||
| 144 | + | ||
| 145 | + | Stack | Verification | | ||
| 146 | + | --- | --- | | ||
| 147 | + | Go | `go build ./... && go test ./...` | | ||
| 148 | + | Go targeting WebAssembly | also `tinygo build -target=wasip1 ./...` — TinyGo rejects some constructs the standard toolchain accepts, so a passing `go build` is not sufficient | | ||
| 149 | + | Node / TypeScript | `npm test` (or the script the project defines), plus `tsc --noEmit` if the project is typed | | ||
| 150 | + | Python | `pytest` | | ||
| 151 | + | Rust | `cargo build && cargo test && cargo clippy -- -D warnings` | | ||
| 152 | + | Rust targeting WebAssembly | also `cargo build --target wasm32-wasip2` — and note the asymmetry below | | ||
| 153 | + | VS Code extension | the compile script, then `vsce package` — packaging validates the manifest, which `tsc` does not | | ||
| 154 | + | Zed extension | `cargo build --release --target wasm32-wasip2` | | ||
| 155 | + | ||
| 156 | + If the project has no tests, say so in the report rather than letting a green build | ||
| 157 | + stand in as proof the refactoring was safe. | ||
| 158 | + | ||
| 159 | + **Two traps specific to WebAssembly targets.** Both let a refactoring look verified | ||
| 160 | + when it is not: | ||
| 161 | + | ||
| 162 | + - A **wasm build passing does not mean the tests ran.** `cargo build --target | ||
| 163 | + wasm32-wasip2` links with rustc's own bundled `rust-lld`, so it succeeds even | ||
| 164 | + with no C toolchain present, while `cargo test` — which builds for the *host* — | ||
| 165 | + fails at link time with ``error: linker `cc` not found``. If you see that error, | ||
| 166 | + the tests did not run; do not report the wasm build as verification. | ||
| 167 | + - An **extension cannot be exercised here.** No editor runs in this sandbox, so | ||
| 168 | + neither VS Code's Extension Development Host nor Zed's *Install Dev Extension* | ||
| 169 | + is available, and `@vscode/test-electron` needs an X server this kit does not | ||
| 170 | + install. Compile, unit-test and package — then say plainly that the integration | ||
| 171 | + path is untested rather than implying it passed. | ||
| 172 | +5. Re-run the measurement from step 2 and compare against the previous run. | ||
| 173 | + | ||
| 174 | +Stop when any of these is true: | ||
| 175 | + | ||
| 176 | +- **The gate passes.** Report the before/after numbers from the Trend table. | ||
| 177 | +- **Five iterations have run.** Stop and report honestly: what improved, what remains, | ||
| 178 | + and why the rest is hard. | ||
| 179 | +- **Two consecutive runs show no improvement.** You are stuck — stop and explain what | ||
| 180 | + the blocker is rather than churning the code. | ||
| 181 | + | ||
| 182 | +Commit as you go, one focused commit per cluster of fixes, so the progression is | ||
| 183 | +visible in git as well as in `.quality/history.jsonl`. | ||
| 184 | + | ||
| 185 | +## Do not game the gate | ||
| 186 | + | ||
| 187 | +The gate is only worth something if it is measuring the code. All of the following make | ||
| 188 | +the number go down while making the project worse — never do them, even if the gate is | ||
| 189 | +the only thing standing between you and "done": | ||
| 190 | + | ||
| 191 | +- Adding `exclude_patterns`, disabling a plugin, setting a plugin to `mode = "monitor"`, | ||
| 192 | + or raising a threshold in `.qlty/qlty.toml`. | ||
| 193 | +- Loosening `.quality/gate.json`. | ||
| 194 | +- Deleting, skipping, or emptying tests. | ||
| 195 | +- Adding blanket lint suppressions (`//nolint`, `// qlty-ignore`) to silence a finding | ||
| 196 | + you could fix. | ||
| 197 | + | ||
| 198 | +A narrowly-scoped suppression with a comment explaining why the finding is a false | ||
| 199 | +positive is legitimate. If you believe a threshold is genuinely wrong for this project, | ||
| 200 | +say so and let the user decide — do not change it yourself. | ||
| 201 | + | ||
| 202 | +### The one plugin you may remove: a broken one | ||
| 203 | + | ||
| 204 | +"Disabling a plugin" above means a plugin that *finds things you would rather not fix*. A | ||
| 205 | +plugin that **analyses nothing** is a different object, and keeping it is not rigour: its | ||
| 206 | +findings crowd out the ones that matter and make the total unreadable. | ||
| 207 | + | ||
| 208 | +The two are indistinguishable from the report alone, so removal requires a **reproducible | ||
| 209 | +diagnosis**, never an impression. Establish all four of these before touching the config: | ||
| 210 | + | ||
| 211 | +1. **It is not the language version or a specific construct.** Put a trivially small, | ||
| 212 | + unambiguously valid file of that language in a *fresh* git repository, enable only | ||
| 213 | + that plugin, and run it. If a five-line hello-world fails, no source construct is to | ||
| 214 | + blame. | ||
| 215 | +2. **It is not this project.** Same point — the probe repository shares nothing with the | ||
| 216 | + codebase under measurement. | ||
| 217 | +3. **It is not the environment qlty gives plugins.** qlty runs plugins with a stripped | ||
| 218 | + environment (empty `PATH` and `HOME`). Run the plugin's own binary directly, with your | ||
| 219 | + full environment, on the same probe file. Read the invocation qlty used from | ||
| 220 | + `.qlty/out/invoke-*.yaml`, which records the exact `script`, `env` and `stderr`. | ||
| 221 | +4. **It is not the plugin family.** If sibling plugins sharing the same runtime produce | ||
| 222 | + real findings, the failure is specific rather than environmental. | ||
| 223 | + | ||
| 224 | +If all four hold, remove the plugin — and **write the diagnosis as a comment where you | ||
| 225 | +removed it**, so the next session finds the evidence instead of re-adding it on instinct. | ||
| 226 | +Note in your report which other tooling still covers that ground; if nothing does, the | ||
| 227 | +answer is to replace the plugin, not merely to drop it. | ||
| 228 | + | ||
| 229 | +`mode = "monitor"` is *not* the remedy here. It does not stop a plugin's findings from | ||
| 230 | +counting; it only changes how they are surfaced. A broken plugin left in monitor mode | ||
| 231 | +keeps polluting the total, which is how one ends up unnoticed for months. | ||
| 232 | + | ||
| 233 | +**Do not encode "plugin X is broken" into this skill.** Such a failure belongs to one | ||
| 234 | +version, one platform and one moment; a list of known-bad plugins ages badly and, worse, | ||
| 235 | +tempts the next agent to skip the diagnosis. The method above is what belongs here — its | ||
| 236 | +results belong in the project's own config and notes. | ||
| 237 | + | ||
| 238 | +## Tuning the gate | ||
| 239 | + | ||
| 240 | +Defaults are strict: zero error-level issues, zero warning-level issues, zero smells. | ||
| 241 | +Override any subset by writing `.quality/gate.json`: | ||
| 242 | + | ||
| 243 | +```json | ||
| 244 | +{ | ||
| 245 | + "max_error": 0, | ||
| 246 | + "max_warning": 0, | ||
| 247 | + "max_note": null, | ||
| 248 | + "max_smells": 0, | ||
| 249 | + "max_file_complexity": 60, | ||
| 250 | + "max_total_complexity": null | ||
| 251 | +} | ||
| 252 | +``` | ||
| 253 | + | ||
| 254 | +`null` disables a check. `max_file_complexity` flags individual files whose `complex` | ||
| 255 | +metric exceeds the limit, which is the most useful knob for a codebase with existing | ||
| 256 | +debt: it lets you hold the line on the worst files without demanding a perfect score | ||
| 257 | +everywhere at once. | ||
| 258 | + | ||
| 259 | +## Reference | ||
| 260 | + | ||
| 261 | +- `qlty check --help`, `qlty smells --help`, `qlty metrics --help` for the full flag set. | ||
| 262 | +- Note `qlty check` has **no** `--json`; machine-readable output is `--sarif`. | ||
| 263 | +- Pass `--no-upgrade-check` to every qlty command — the sandbox blocks its update host. | ||
| 264 | +- Reports and history under `.quality/` are meant to be committed so progression | ||
| 265 | + survives sandbox recreation. Add `.quality/` to `.gitignore` if the user prefers them | ||
| 266 | + to stay local. | ||
added
.claude/skills/quality/scripts/quality_report.py +463 -0 | new file mode 100644 | ||
| @@ -0,0 +1,463 @@ | ||
| 1 | +#!/usr/bin/env python3 | |
| 2 | +"""Measure project code quality with qlty and emit a report, a history line, and a gate verdict. | |
| 3 | + | |
| 4 | +Read-only with respect to the source tree: `qlty check` runs with --no-fix so a | |
| 5 | +measurement never silently rewrites the code it is measuring. | |
| 6 | + | |
| 7 | +Exit codes: 0 gate passed, 1 gate failed, 2 tooling/setup error. | |
| 8 | +""" | |
| 9 | + | |
| 10 | +from __future__ import annotations | |
| 11 | + | |
| 12 | +import argparse | |
| 13 | +import json | |
| 14 | +import re | |
| 15 | +import shutil | |
| 16 | + | |
| 17 | +# nosec B404 - running qlty *is* this script's job. Every command it launches is a | |
| 18 | +# literal argv list built in this file; none is assembled from user input, and none | |
| 19 | +# goes through a shell. See the matching justification on subprocess.run below. | |
| 20 | +import subprocess # nosec B404 | |
| 21 | +import sys | |
| 22 | +from datetime import datetime, timezone | |
| 23 | +from pathlib import Path | |
| 24 | + | |
| 25 | +ANSI = re.compile(r"\x1b\[[0-9;]*[A-Za-z]") | |
| 26 | + | |
| 27 | +# Overridable per project by writing .quality/gate.json with any subset of these | |
| 28 | +# keys. Kept strict on purpose: the gate is what the refactoring loop drives to | |
| 29 | +# zero. A null value disables that check. | |
| 30 | +DEFAULT_GATE = { | |
| 31 | + "max_error": 0, | |
| 32 | + "max_warning": 0, | |
| 33 | + "max_note": None, | |
| 34 | + "max_smells": 0, | |
| 35 | + "max_file_complexity": None, | |
| 36 | + "max_total_complexity": None, | |
| 37 | +} | |
| 38 | + | |
| 39 | +QLTY_COMMON = ["--no-upgrade-check"] | |
| 40 | + | |
| 41 | + | |
| 42 | +def run(cmd: list[str], cwd: Path, timeout: int = 1800) -> tuple[int, str, str]: | |
| 43 | + try: | |
| 44 | + # nosec B603 - cmd is always a literal argv list from this file (qlty or git | |
| 45 | + # with fixed subcommands), never a string and never shell-interpreted, so | |
| 46 | + # there is no injection surface. shell=False is the default and is what makes | |
| 47 | + # this the safe form rather than the risky one. | |
| 48 | + p = subprocess.run( # nosec B603 | |
| 49 | + cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout | |
| 50 | + ) | |
| 51 | + except FileNotFoundError: | |
| 52 | + return 127, "", f"command not found: {cmd[0]}" | |
| 53 | + except subprocess.TimeoutExpired: | |
| 54 | + return 124, "", f"timed out after {timeout}s: {' '.join(cmd)}" | |
| 55 | + return p.returncode, p.stdout, p.stderr | |
| 56 | + | |
| 57 | + | |
| 58 | +def git(args: list[str], cwd: Path) -> str: | |
| 59 | + code, out, _ = run(["git"] + args, cwd, timeout=60) | |
| 60 | + return out.strip() if code == 0 else "" | |
| 61 | + | |
| 62 | + | |
| 63 | +def ensure_setup(ws: Path, allow_init: bool) -> None: | |
| 64 | + """Verify qlty is usable here, running `qlty init` when the project has no config.""" | |
| 65 | + if shutil.which("qlty") is None: | |
| 66 | + die( | |
| 67 | + "qlty is not installed. This project expects the `dev-toolkit` sbx kit " | |
| 68 | + "(see kits/dev-toolkit/README.md)." | |
| 69 | + ) | |
| 70 | + if not (ws / ".git").exists() and not git(["rev-parse", "--git-dir"], ws): | |
| 71 | + die(f"{ws} is not a git repository — qlty needs git to scope its analysis.") | |
| 72 | + | |
| 73 | + if (ws / ".qlty" / "qlty.toml").is_file(): | |
| 74 | + return | |
| 75 | + if not allow_init: | |
| 76 | + die("no .qlty/qlty.toml found and --no-init was passed; run `qlty init --yes`.") | |
| 77 | + | |
| 78 | + log("no .qlty/qlty.toml found — running `qlty init --yes` (first-time setup)") | |
| 79 | + code, out, err = run(["qlty", "init", "--yes"] + QLTY_COMMON, ws, timeout=1800) | |
| 80 | + if code != 0 or not (ws / ".qlty" / "qlty.toml").is_file(): | |
| 81 | + die(f"`qlty init` failed (exit {code}).\n{(out + err).strip()[-2000:]}") | |
| 82 | + log("qlty initialised: .qlty/qlty.toml written") | |
| 83 | + | |
| 84 | + | |
| 85 | +def sarif_location(res: dict) -> tuple[str, int | None]: | |
| 86 | + """Return the file and start line of a SARIF result, or ("", None) if it has none. | |
| 87 | + | |
| 88 | + Every level of the structure is optional in the spec and plugins do omit them, | |
| 89 | + hence the `or {}` at each step rather than a single chained access. | |
| 90 | + """ | |
| 91 | + locs = res.get("locations") or [] | |
| 92 | + if not locs: | |
| 93 | + return "", None | |
| 94 | + phys = (locs[0] or {}).get("physicalLocation") or {} | |
| 95 | + uri = ((phys.get("artifactLocation") or {}).get("uri")) or "" | |
| 96 | + return uri, (phys.get("region") or {}).get("startLine") | |
| 97 | + | |
| 98 | + | |
| 99 | +def sarif_finding(res: dict) -> dict: | |
| 100 | + """Flatten one SARIF result into the shape the rest of this script uses.""" | |
| 101 | + uri, line = sarif_location(res) | |
| 102 | + return { | |
| 103 | + "rule": res.get("ruleId") or "unknown", | |
| 104 | + # SARIF omits `level` when it equals the rule default; treat an absent | |
| 105 | + # level as "warning" rather than dropping the finding. | |
| 106 | + "level": (res.get("level") or "warning").lower(), | |
| 107 | + "message": ((res.get("message") or {}).get("text") or "").strip(), | |
| 108 | + "file": uri, | |
| 109 | + "line": line, | |
| 110 | + } | |
| 111 | + | |
| 112 | + | |
| 113 | +def sarif_results(payload: str) -> list[dict]: | |
| 114 | + """Flatten SARIF runs into a list of result dicts, tolerating partial output.""" | |
| 115 | + try: | |
| 116 | + doc = json.loads(payload) | |
| 117 | + except json.JSONDecodeError: | |
| 118 | + return [] | |
| 119 | + return [ | |
| 120 | + sarif_finding(res) | |
| 121 | + for run_ in doc.get("runs") or [] | |
| 122 | + for res in run_.get("results") or [] | |
| 123 | + ] | |
| 124 | + | |
| 125 | + | |
| 126 | +def collect_check(ws: Path) -> tuple[list[dict], str]: | |
| 127 | + # --no-fail / --no-error: always emit a report instead of exiting on findings. | |
| 128 | + # --no-fix: measurement must not mutate the tree. | |
| 129 | + cmd = [ | |
| 130 | + "qlty", "check", "--all", "--no-fix", "--no-fail", "--no-error", | |
| 131 | + "--no-progress", "--sarif", | |
| 132 | + ] + QLTY_COMMON | |
| 133 | + code, out, err = run(cmd, ws) | |
| 134 | + results = sarif_results(out) | |
| 135 | + note = "" if (results or code == 0) else f"`qlty check` exit {code}: {err.strip()[-500:]}" | |
| 136 | + return results, note | |
| 137 | + | |
| 138 | + | |
| 139 | +def collect_smells(ws: Path) -> tuple[list[dict], str]: | |
| 140 | + cmd = ["qlty", "smells", "--all", "--quiet", "--sarif"] + QLTY_COMMON | |
| 141 | + code, out, err = run(cmd, ws) | |
| 142 | + results = sarif_results(out) | |
| 143 | + note = "" if (results or code == 0) else f"`qlty smells` exit {code}: {err.strip()[-500:]}" | |
| 144 | + return results, note | |
| 145 | + | |
| 146 | + | |
| 147 | +def table_cells(text: str) -> list[list[str]]: | |
| 148 | + """Split a pipe table into rows of cells, dropping rules and blank lines. | |
| 149 | + | |
| 150 | + qlty renders an ANSI-coloured table and ignores NO_COLOR, so the escape codes | |
| 151 | + are stripped here rather than by the caller. | |
| 152 | + """ | |
| 153 | + rows: list[list[str]] = [] | |
| 154 | + for raw in text.splitlines(): | |
| 155 | + line = ANSI.sub("", raw).strip() | |
| 156 | + if "|" not in line or set(line) <= set("-+| "): | |
| 157 | + continue | |
| 158 | + rows.append([c.strip() for c in line.split("|")]) | |
| 159 | + return rows | |
| 160 | + | |
| 161 | + | |
| 162 | +def metric_row(header: list[str], cells: list[str]) -> dict: | |
| 163 | + """Pair a data row with its header, keeping `name` textual and the rest numeric.""" | |
| 164 | + row: dict = {} | |
| 165 | + for key, val in zip(header, cells): | |
| 166 | + if key == "name": | |
| 167 | + row["name"] = val | |
| 168 | + continue | |
| 169 | + try: | |
| 170 | + row[key] = int(val) | |
| 171 | + except ValueError: | |
| 172 | + row[key] = val | |
| 173 | + return row | |
| 174 | + | |
| 175 | + | |
| 176 | +def parse_metrics_table(text: str) -> tuple[dict, list[dict]]: | |
| 177 | + """Split qlty's metrics table into its TOTAL row and its per-file rows. | |
| 178 | + | |
| 179 | + A row whose width does not match the header is skipped rather than guessed at: | |
| 180 | + qlty occasionally wraps a long path, and inventing a value would silently | |
| 181 | + corrupt the numbers this whole report rests on. | |
| 182 | + """ | |
| 183 | + header: list[str] = [] | |
| 184 | + total: dict = {} | |
| 185 | + rows: list[dict] = [] | |
| 186 | + for cells in table_cells(text): | |
| 187 | + if not header: | |
| 188 | + header = [c.lower() for c in cells] | |
| 189 | + continue | |
| 190 | + if len(cells) != len(header): | |
| 191 | + continue | |
| 192 | + row = metric_row(header, cells) | |
| 193 | + if row.get("name", "").upper() == "TOTAL": | |
| 194 | + total = {k: v for k, v in row.items() if k != "name"} | |
| 195 | + else: | |
| 196 | + rows.append(row) | |
| 197 | + return total, rows | |
| 198 | + | |
| 199 | + | |
| 200 | +def collect_metrics(ws: Path) -> tuple[dict, list[dict], str]: | |
| 201 | + """Run `qlty metrics` and return its TOTAL row, its per-file rows, and any note.""" | |
| 202 | + cmd = ["qlty", "metrics", "--all", "--quiet"] + QLTY_COMMON | |
| 203 | + code, out, err = run(cmd, ws) | |
| 204 | + if code != 0 and not out.strip(): | |
| 205 | + return {}, [], f"`qlty metrics` exit {code}: {err.strip()[-500:]}" | |
| 206 | + | |
| 207 | + total, rows = parse_metrics_table(out) | |
| 208 | + return total, rows, "" | |
| 209 | + | |
| 210 | + | |
| 211 | +def load_gate(ws: Path, override: Path | None) -> dict: | |
| 212 | + gate = dict(DEFAULT_GATE) | |
| 213 | + path = override or (ws / ".quality" / "gate.json") | |
| 214 | + if path.is_file(): | |
| 215 | + try: | |
| 216 | + gate.update(json.loads(path.read_text())) | |
| 217 | + except (json.JSONDecodeError, OSError) as exc: | |
| 218 | + die(f"could not read gate file {path}: {exc}") | |
| 219 | + return gate | |
| 220 | + | |
| 221 | + | |
| 222 | +def evaluate(gate: dict, counts: dict, smells: int, metrics: dict, files: list[dict]) -> list[str]: | |
| 223 | + """Return one human-readable violation string per breached threshold.""" | |
| 224 | + breaches: list[str] = [] | |
| 225 | + | |
| 226 | + def check(limit_key: str, actual: int, label: str) -> None: | |
| 227 | + limit = gate.get(limit_key) | |
| 228 | + if limit is not None and actual > limit: | |
| 229 | + breaches.append(f"{label}: {actual} (max {limit})") | |
| 230 | + | |
| 231 | + check("max_error", counts.get("error", 0), "error-level issues") | |
| 232 | + check("max_warning", counts.get("warning", 0), "warning-level issues") | |
| 233 | + check("max_note", counts.get("note", 0), "note-level issues") | |
| 234 | + check("max_smells", smells, "code smells") | |
| 235 | + check("max_total_complexity", metrics.get("complex", 0), "total complexity") | |
| 236 | + | |
| 237 | + limit = gate.get("max_file_complexity") | |
| 238 | + if limit is not None: | |
| 239 | + over = [f for f in files if isinstance(f.get("complex"), int) and f["complex"] > limit] | |
| 240 | + for f in sorted(over, key=lambda r: -r["complex"])[:10]: | |
| 241 | + breaches.append(f"{f['name']} complexity {f['complex']} (max {limit})") | |
| 242 | + return breaches | |
| 243 | + | |
| 244 | + | |
| 245 | +def tally(results: list[dict], key: str) -> dict[str, int]: | |
| 246 | + counts: dict[str, int] = {} | |
| 247 | + for r in results: | |
| 248 | + counts[r[key]] = counts.get(r[key], 0) + 1 | |
| 249 | + return counts | |
| 250 | + | |
| 251 | + | |
| 252 | +def log(msg: str) -> None: | |
| 253 | + print(f"[quality] {msg}", file=sys.stderr) | |
| 254 | + | |
| 255 | + | |
| 256 | +def die(msg: str) -> None: | |
| 257 | + print(f"[quality] error: {msg}", file=sys.stderr) | |
| 258 | + sys.exit(2) | |
| 259 | + | |
| 260 | + | |
| 261 | +def md_table(headers: list[str], rows: list[list[str]]) -> str: | |
| 262 | + if not rows: | |
| 263 | + return "_none_\n" | |
| 264 | + | |
| 265 | + out = ["| " + " | ".join(headers) + " |", "|" + "|".join(["---"] * len(headers)) + "|"] | |
| 266 | + out += ["| " + " | ".join(rows_) + " |" for rows_ in (map(str, r) for r in rows)] | |
| 267 | + return "\n".join(out) + "\n" | |
| 268 | + | |
| 269 | + | |
| 270 | +def delta(cur: int, prev: int | None) -> str: | |
| 271 | + if prev is None: | |
| 272 | + return "—" | |
| 273 | + d = cur - prev | |
| 274 | + if d == 0: | |
| 275 | + return "±0" | |
| 276 | + return f"{d:+d}" | |
| 277 | + | |
| 278 | + | |
| 279 | +def report_header(entry: dict, prev: dict | None, breaches: list[str]) -> list[str]: | |
| 280 | + verdict = "✅ **PASS**" if entry["gate_passed"] else "❌ **FAIL**" | |
| 281 | + lines = [ | |
| 282 | + f"# Quality report — {entry['timestamp']}", | |
| 283 | + "", | |
| 284 | + f"- **Gate**: {verdict}", | |
| 285 | + f"- **Commit**: `{entry['commit'] or 'n/a'}` on `{entry['branch'] or 'n/a'}`", | |
| 286 | + f"- **qlty**: {entry['qlty_version']}", | |
| 287 | + f"- **Run**: #{entry['run']}" | |
| 288 | + + (f" (previous: {prev['timestamp']})" if prev else " (first recorded run)"), | |
| 289 | + "", | |
| 290 | + ] | |
| 291 | + if breaches: | |
| 292 | + lines += ["## Gate violations", ""] | |
| 293 | + lines += [f"- {b}" for b in breaches] | |
| 294 | + lines.append("") | |
| 295 | + return lines | |
| 296 | + | |
| 297 | + | |
| 298 | +def report_issues(entry: dict, prev: dict | None, check_res: list[dict]) -> list[str]: | |
| 299 | + """The lint section: counts by level, then the two "where to start" tables.""" | |
| 300 | + c = entry["counts"] | |
| 301 | + p_counts = (prev or {}).get("counts") or {} | |
| 302 | + | |
| 303 | + lines = ["## Lint issues (`qlty check`)", ""] | |
| 304 | + lines.append( | |
| 305 | + md_table( | |
| 306 | + ["level", "count", "vs previous"], | |
| 307 | + [[lvl, c.get(lvl, 0), delta(c.get(lvl, 0), p_counts.get(lvl))] | |
| 308 | + for lvl in ("error", "warning", "note", "none") | |
| 309 | + if c.get(lvl) or p_counts.get(lvl)], | |
| 310 | + ) | |
| 311 | + ) | |
| 312 | + | |
| 313 | + by_rule = sorted(tally(check_res, "rule").items(), key=lambda kv: -kv[1])[:15] | |
| 314 | + lines += ["### Top rules", "", md_table(["rule", "count"], [[k, v] for k, v in by_rule])] | |
| 315 | + | |
| 316 | + by_file = sorted(tally([r for r in check_res if r["file"]], "file").items(), | |
| 317 | + key=lambda kv: -kv[1])[:15] | |
| 318 | + lines += ["### Most affected files", "", | |
| 319 | + md_table(["file", "issues"], [[k, v] for k, v in by_file])] | |
| 320 | + return lines | |
| 321 | + | |
| 322 | + | |
| 323 | +def report_smells(entry: dict, prev: dict | None, smell_res: list[dict]) -> list[str]: | |
| 324 | + lines = ["## Code smells (`qlty smells`)", "", | |
| 325 | + f"Total: **{entry['smells']}** (vs previous: " | |
| 326 | + f"{delta(entry['smells'], (prev or {}).get('smells'))})", ""] | |
| 327 | + smell_rows = [[r["rule"], r["file"] or "—", r["line"] or "—", r["message"][:110]] | |
| 328 | + for r in smell_res[:20]] | |
| 329 | + lines.append(md_table(["smell", "file", "line", "detail"], smell_rows)) | |
| 330 | + return lines | |
| 331 | + | |
| 332 | + | |
| 333 | +def report_metrics(entry: dict, prev: dict | None, files: list[dict]) -> list[str]: | |
| 334 | + m = entry["metrics"] | |
| 335 | + p_metrics = (prev or {}).get("metrics") or {} | |
| 336 | + | |
| 337 | + lines = ["## Metrics (`qlty metrics`)", ""] | |
| 338 | + metric_rows = [ | |
| 339 | + [k, m.get(k, "—"), delta(m[k], p_metrics.get(k)) if isinstance(m.get(k), int) else "—"] | |
| 340 | + for k in ("funcs", "classes", "fields", "cyclo", "complex", "lcom", "lines", "loc") | |
| 341 | + if k in m | |
| 342 | + ] | |
| 343 | + lines.append(md_table(["metric", "total", "vs previous"], metric_rows)) | |
| 344 | + | |
| 345 | + worst = sorted([f for f in files if isinstance(f.get("complex"), int)], | |
| 346 | + key=lambda r: -r["complex"])[:15] | |
| 347 | + lines += ["### Most complex files", "", | |
| 348 | + md_table(["file", "complex", "cyclo", "loc"], | |
| 349 | + [[f["name"], f.get("complex", "—"), f.get("cyclo", "—"), | |
| 350 | + f.get("loc", "—")] for f in worst])] | |
| 351 | + return lines | |
| 352 | + | |
| 353 | + | |
| 354 | +def report_trend(entry: dict) -> list[str]: | |
| 355 | + return ["## Trend", "", md_table( | |
| 356 | + ["run", "timestamp", "error", "warning", "smells", "complex", "gate"], | |
| 357 | + [[h["run"], h["timestamp"], (h.get("counts") or {}).get("error", 0), | |
| 358 | + (h.get("counts") or {}).get("warning", 0), h.get("smells", 0), | |
| 359 | + (h.get("metrics") or {}).get("complex", "—"), | |
| 360 | + "PASS" if h.get("gate_passed") else "FAIL"] | |
| 361 | + for h in entry["_trend"]], | |
| 362 | + )] | |
| 363 | + | |
| 364 | + | |
| 365 | +def build_report(entry: dict, prev: dict | None, check_res: list[dict], | |
| 366 | + smell_res: list[dict], files: list[dict], breaches: list[str], | |
| 367 | + notes: list[str]) -> str: | |
| 368 | + """Assemble the Markdown report, in the order a reader works through it.""" | |
| 369 | + lines = report_header(entry, prev, breaches) | |
| 370 | + lines += report_issues(entry, prev, check_res) | |
| 371 | + lines += report_smells(entry, prev, smell_res) | |
| 372 | + lines += report_metrics(entry, prev, files) | |
| 373 | + | |
| 374 | + if notes: | |
| 375 | + lines += ["## Tooling notes", ""] + [f"- {n}" for n in notes] + [""] | |
| 376 | + | |
| 377 | + lines += report_trend(entry) | |
| 378 | + return "\n".join(lines) | |
| 379 | + | |
| 380 | + | |
| 381 | +def main() -> int: | |
| 382 | + ap = argparse.ArgumentParser(description=__doc__) | |
| 383 | + ap.add_argument("--workspace", default=".", help="project root (default: cwd)") | |
| 384 | + ap.add_argument("--no-init", action="store_true", | |
| 385 | + help="fail instead of running `qlty init` when unconfigured") | |
| 386 | + ap.add_argument("--gate", type=Path, help="gate JSON file (default: .quality/gate.json)") | |
| 387 | + ap.add_argument("--json", action="store_true", help="print the history entry to stdout") | |
| 388 | + args = ap.parse_args() | |
| 389 | + | |
| 390 | + ws = Path(args.workspace).resolve() | |
| 391 | + if not ws.is_dir(): | |
| 392 | + die(f"workspace {ws} does not exist") | |
| 393 | + | |
| 394 | + ensure_setup(ws, allow_init=not args.no_init) | |
| 395 | + | |
| 396 | + notes: list[str] = [] | |
| 397 | + log("running qlty check / smells / metrics (this can take several minutes on a cold cache)") | |
| 398 | + check_res, n1 = collect_check(ws) | |
| 399 | + smell_res, n2 = collect_smells(ws) | |
| 400 | + metrics, files, n3 = collect_metrics(ws) | |
| 401 | + notes += [n for n in (n1, n2, n3) if n] | |
| 402 | + | |
| 403 | + counts = tally(check_res, "level") | |
| 404 | + gate = load_gate(ws, args.gate) | |
| 405 | + breaches = evaluate(gate, counts, len(smell_res), metrics, files) | |
| 406 | + | |
| 407 | + _, ver_out, _ = run(["qlty", "--version"], ws, timeout=60) | |
| 408 | + outdir = ws / ".quality" | |
| 409 | + outdir.mkdir(parents=True, exist_ok=True) | |
| 410 | + history = outdir / "history.jsonl" | |
| 411 | + | |
| 412 | + past: list[dict] = [] | |
| 413 | + if history.is_file(): | |
| 414 | + for line in history.read_text().splitlines(): | |
| 415 | + line = line.strip() | |
| 416 | + if line: | |
| 417 | + try: | |
| 418 | + past.append(json.loads(line)) | |
| 419 | + except json.JSONDecodeError: | |
| 420 | + continue | |
| 421 | + | |
| 422 | + now = datetime.now(timezone.utc) | |
| 423 | + entry = { | |
| 424 | + "run": len(past) + 1, | |
| 425 | + "timestamp": now.strftime("%Y-%m-%dT%H:%M:%SZ"), | |
| 426 | + "commit": git(["rev-parse", "--short", "HEAD"], ws), | |
| 427 | + "branch": git(["rev-parse", "--abbrev-ref", "HEAD"], ws), | |
| 428 | + "qlty_version": ANSI.sub("", ver_out).strip() or "unknown", | |
| 429 | + "counts": counts, | |
| 430 | + "smells": len(smell_res), | |
| 431 | + "metrics": metrics, | |
| 432 | + "gate": {k: v for k, v in gate.items() if v is not None}, | |
| 433 | + "gate_passed": not breaches, | |
| 434 | + "breaches": breaches, | |
| 435 | + } | |
| 436 | + | |
| 437 | + with history.open("a") as fh: | |
| 438 | + fh.write(json.dumps(entry, sort_keys=True) + "\n") | |
| 439 | + | |
| 440 | + entry["_trend"] = (past + [entry])[-10:] | |
| 441 | + report = build_report(entry, past[-1] if past else None, | |
| 442 | + check_res, smell_res, files, breaches, notes) | |
| 443 | + del entry["_trend"] | |
| 444 | + | |
| 445 | + stamp = now.strftime("%Y%m%dT%H%M%SZ") | |
| 446 | + (outdir / f"report-{stamp}.md").write_text(report) | |
| 447 | + (outdir / "report-latest.md").write_text(report) | |
| 448 | + | |
| 449 | + print(report) | |
| 450 | + log(f"report written to .quality/report-{stamp}.md (and report-latest.md)") | |
| 451 | + log(f"history appended to .quality/history.jsonl (run #{entry['run']})") | |
| 452 | + if args.json: | |
| 453 | + print(json.dumps(entry, indent=2, sort_keys=True)) | |
| 454 | + | |
| 455 | + if breaches: | |
| 456 | + log(f"GATE FAILED — {len(breaches)} violation(s)") | |
| 457 | + return 1 | |
| 458 | + log("GATE PASSED") | |
| 459 | + return 0 | |
| 460 | + | |
| 461 | + | |
| 462 | +if __name__ == "__main__": | |
| 463 | + sys.exit(main()) | |
| new file mode 100644 | |||
| @@ -0,0 +1,463 @@ | |||
| 1 | +#!/usr/bin/env python3 | ||
| 2 | +"""Measure project code quality with qlty and emit a report, a history line, and a gate verdict. | ||
| 3 | + | ||
| 4 | +Read-only with respect to the source tree: `qlty check` runs with --no-fix so a | ||
| 5 | +measurement never silently rewrites the code it is measuring. | ||
| 6 | + | ||
| 7 | +Exit codes: 0 gate passed, 1 gate failed, 2 tooling/setup error. | ||
| 8 | +""" | ||
| 9 | + | ||
| 10 | +from __future__ import annotations | ||
| 11 | + | ||
| 12 | +import argparse | ||
| 13 | +import json | ||
| 14 | +import re | ||
| 15 | +import shutil | ||
| 16 | + | ||
| 17 | +# nosec B404 - running qlty *is* this script's job. Every command it launches is a | ||
| 18 | +# literal argv list built in this file; none is assembled from user input, and none | ||
| 19 | +# goes through a shell. See the matching justification on subprocess.run below. | ||
| 20 | +import subprocess # nosec B404 | ||
| 21 | +import sys | ||
| 22 | +from datetime import datetime, timezone | ||
| 23 | +from pathlib import Path | ||
| 24 | + | ||
| 25 | +ANSI = re.compile(r"\x1b\[[0-9;]*[A-Za-z]") | ||
| 26 | + | ||
| 27 | +# Overridable per project by writing .quality/gate.json with any subset of these | ||
| 28 | +# keys. Kept strict on purpose: the gate is what the refactoring loop drives to | ||
| 29 | +# zero. A null value disables that check. | ||
| 30 | +DEFAULT_GATE = { | ||
| 31 | + "max_error": 0, | ||
| 32 | + "max_warning": 0, | ||
| 33 | + "max_note": None, | ||
| 34 | + "max_smells": 0, | ||
| 35 | + "max_file_complexity": None, | ||
| 36 | + "max_total_complexity": None, | ||
| 37 | +} | ||
| 38 | + | ||
| 39 | +QLTY_COMMON = ["--no-upgrade-check"] | ||
| 40 | + | ||
| 41 | + | ||
| 42 | +def run(cmd: list[str], cwd: Path, timeout: int = 1800) -> tuple[int, str, str]: | ||
| 43 | + try: | ||
| 44 | + # nosec B603 - cmd is always a literal argv list from this file (qlty or git | ||
| 45 | + # with fixed subcommands), never a string and never shell-interpreted, so | ||
| 46 | + # there is no injection surface. shell=False is the default and is what makes | ||
| 47 | + # this the safe form rather than the risky one. | ||
| 48 | + p = subprocess.run( # nosec B603 | ||
| 49 | + cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout | ||
| 50 | + ) | ||
| 51 | + except FileNotFoundError: | ||
| 52 | + return 127, "", f"command not found: {cmd[0]}" | ||
| 53 | + except subprocess.TimeoutExpired: | ||
| 54 | + return 124, "", f"timed out after {timeout}s: {' '.join(cmd)}" | ||
| 55 | + return p.returncode, p.stdout, p.stderr | ||
| 56 | + | ||
| 57 | + | ||
| 58 | +def git(args: list[str], cwd: Path) -> str: | ||
| 59 | + code, out, _ = run(["git"] + args, cwd, timeout=60) | ||
| 60 | + return out.strip() if code == 0 else "" | ||
| 61 | + | ||
| 62 | + | ||
| 63 | +def ensure_setup(ws: Path, allow_init: bool) -> None: | ||
| 64 | + """Verify qlty is usable here, running `qlty init` when the project has no config.""" | ||
| 65 | + if shutil.which("qlty") is None: | ||
| 66 | + die( | ||
| 67 | + "qlty is not installed. This project expects the `dev-toolkit` sbx kit " | ||
| 68 | + "(see kits/dev-toolkit/README.md)." | ||
| 69 | + ) | ||
| 70 | + if not (ws / ".git").exists() and not git(["rev-parse", "--git-dir"], ws): | ||
| 71 | + die(f"{ws} is not a git repository — qlty needs git to scope its analysis.") | ||
| 72 | + | ||
| 73 | + if (ws / ".qlty" / "qlty.toml").is_file(): | ||
| 74 | + return | ||
| 75 | + if not allow_init: | ||
| 76 | + die("no .qlty/qlty.toml found and --no-init was passed; run `qlty init --yes`.") | ||
| 77 | + | ||
| 78 | + log("no .qlty/qlty.toml found — running `qlty init --yes` (first-time setup)") | ||
| 79 | + code, out, err = run(["qlty", "init", "--yes"] + QLTY_COMMON, ws, timeout=1800) | ||
| 80 | + if code != 0 or not (ws / ".qlty" / "qlty.toml").is_file(): | ||
| 81 | + die(f"`qlty init` failed (exit {code}).\n{(out + err).strip()[-2000:]}") | ||
| 82 | + log("qlty initialised: .qlty/qlty.toml written") | ||
| 83 | + | ||
| 84 | + | ||
| 85 | +def sarif_location(res: dict) -> tuple[str, int | None]: | ||
| 86 | + """Return the file and start line of a SARIF result, or ("", None) if it has none. | ||
| 87 | + | ||
| 88 | + Every level of the structure is optional in the spec and plugins do omit them, | ||
| 89 | + hence the `or {}` at each step rather than a single chained access. | ||
| 90 | + """ | ||
| 91 | + locs = res.get("locations") or [] | ||
| 92 | + if not locs: | ||
| 93 | + return "", None | ||
| 94 | + phys = (locs[0] or {}).get("physicalLocation") or {} | ||
| 95 | + uri = ((phys.get("artifactLocation") or {}).get("uri")) or "" | ||
| 96 | + return uri, (phys.get("region") or {}).get("startLine") | ||
| 97 | + | ||
| 98 | + | ||
| 99 | +def sarif_finding(res: dict) -> dict: | ||
| 100 | + """Flatten one SARIF result into the shape the rest of this script uses.""" | ||
| 101 | + uri, line = sarif_location(res) | ||
| 102 | + return { | ||
| 103 | + "rule": res.get("ruleId") or "unknown", | ||
| 104 | + # SARIF omits `level` when it equals the rule default; treat an absent | ||
| 105 | + # level as "warning" rather than dropping the finding. | ||
| 106 | + "level": (res.get("level") or "warning").lower(), | ||
| 107 | + "message": ((res.get("message") or {}).get("text") or "").strip(), | ||
| 108 | + "file": uri, | ||
| 109 | + "line": line, | ||
| 110 | + } | ||
| 111 | + | ||
| 112 | + | ||
| 113 | +def sarif_results(payload: str) -> list[dict]: | ||
| 114 | + """Flatten SARIF runs into a list of result dicts, tolerating partial output.""" | ||
| 115 | + try: | ||
| 116 | + doc = json.loads(payload) | ||
| 117 | + except json.JSONDecodeError: | ||
| 118 | + return [] | ||
| 119 | + return [ | ||
| 120 | + sarif_finding(res) | ||
| 121 | + for run_ in doc.get("runs") or [] | ||
| 122 | + for res in run_.get("results") or [] | ||
| 123 | + ] | ||
| 124 | + | ||
| 125 | + | ||
| 126 | +def collect_check(ws: Path) -> tuple[list[dict], str]: | ||
| 127 | + # --no-fail / --no-error: always emit a report instead of exiting on findings. | ||
| 128 | + # --no-fix: measurement must not mutate the tree. | ||
| 129 | + cmd = [ | ||
| 130 | + "qlty", "check", "--all", "--no-fix", "--no-fail", "--no-error", | ||
| 131 | + "--no-progress", "--sarif", | ||
| 132 | + ] + QLTY_COMMON | ||
| 133 | + code, out, err = run(cmd, ws) | ||
| 134 | + results = sarif_results(out) | ||
| 135 | + note = "" if (results or code == 0) else f"`qlty check` exit {code}: {err.strip()[-500:]}" | ||
| 136 | + return results, note | ||
| 137 | + | ||
| 138 | + | ||
| 139 | +def collect_smells(ws: Path) -> tuple[list[dict], str]: | ||
| 140 | + cmd = ["qlty", "smells", "--all", "--quiet", "--sarif"] + QLTY_COMMON | ||
| 141 | + code, out, err = run(cmd, ws) | ||
| 142 | + results = sarif_results(out) | ||
| 143 | + note = "" if (results or code == 0) else f"`qlty smells` exit {code}: {err.strip()[-500:]}" | ||
| 144 | + return results, note | ||
| 145 | + | ||
| 146 | + | ||
| 147 | +def table_cells(text: str) -> list[list[str]]: | ||
| 148 | + """Split a pipe table into rows of cells, dropping rules and blank lines. | ||
| 149 | + | ||
| 150 | + qlty renders an ANSI-coloured table and ignores NO_COLOR, so the escape codes | ||
| 151 | + are stripped here rather than by the caller. | ||
| 152 | + """ | ||
| 153 | + rows: list[list[str]] = [] | ||
| 154 | + for raw in text.splitlines(): | ||
| 155 | + line = ANSI.sub("", raw).strip() | ||
| 156 | + if "|" not in line or set(line) <= set("-+| "): | ||
| 157 | + continue | ||
| 158 | + rows.append([c.strip() for c in line.split("|")]) | ||
| 159 | + return rows | ||
| 160 | + | ||
| 161 | + | ||
| 162 | +def metric_row(header: list[str], cells: list[str]) -> dict: | ||
| 163 | + """Pair a data row with its header, keeping `name` textual and the rest numeric.""" | ||
| 164 | + row: dict = {} | ||
| 165 | + for key, val in zip(header, cells): | ||
| 166 | + if key == "name": | ||
| 167 | + row["name"] = val | ||
| 168 | + continue | ||
| 169 | + try: | ||
| 170 | + row[key] = int(val) | ||
| 171 | + except ValueError: | ||
| 172 | + row[key] = val | ||
| 173 | + return row | ||
| 174 | + | ||
| 175 | + | ||
| 176 | +def parse_metrics_table(text: str) -> tuple[dict, list[dict]]: | ||
| 177 | + """Split qlty's metrics table into its TOTAL row and its per-file rows. | ||
| 178 | + | ||
| 179 | + A row whose width does not match the header is skipped rather than guessed at: | ||
| 180 | + qlty occasionally wraps a long path, and inventing a value would silently | ||
| 181 | + corrupt the numbers this whole report rests on. | ||
| 182 | + """ | ||
| 183 | + header: list[str] = [] | ||
| 184 | + total: dict = {} | ||
| 185 | + rows: list[dict] = [] | ||
| 186 | + for cells in table_cells(text): | ||
| 187 | + if not header: | ||
| 188 | + header = [c.lower() for c in cells] | ||
| 189 | + continue | ||
| 190 | + if len(cells) != len(header): | ||
| 191 | + continue | ||
| 192 | + row = metric_row(header, cells) | ||
| 193 | + if row.get("name", "").upper() == "TOTAL": | ||
| 194 | + total = {k: v for k, v in row.items() if k != "name"} | ||
| 195 | + else: | ||
| 196 | + rows.append(row) | ||
| 197 | + return total, rows | ||
| 198 | + | ||
| 199 | + | ||
| 200 | +def collect_metrics(ws: Path) -> tuple[dict, list[dict], str]: | ||
| 201 | + """Run `qlty metrics` and return its TOTAL row, its per-file rows, and any note.""" | ||
| 202 | + cmd = ["qlty", "metrics", "--all", "--quiet"] + QLTY_COMMON | ||
| 203 | + code, out, err = run(cmd, ws) | ||
| 204 | + if code != 0 and not out.strip(): | ||
| 205 | + return {}, [], f"`qlty metrics` exit {code}: {err.strip()[-500:]}" | ||
| 206 | + | ||
| 207 | + total, rows = parse_metrics_table(out) | ||
| 208 | + return total, rows, "" | ||
| 209 | + | ||
| 210 | + | ||
| 211 | +def load_gate(ws: Path, override: Path | None) -> dict: | ||
| 212 | + gate = dict(DEFAULT_GATE) | ||
| 213 | + path = override or (ws / ".quality" / "gate.json") | ||
| 214 | + if path.is_file(): | ||
| 215 | + try: | ||
| 216 | + gate.update(json.loads(path.read_text())) | ||
| 217 | + except (json.JSONDecodeError, OSError) as exc: | ||
| 218 | + die(f"could not read gate file {path}: {exc}") | ||
| 219 | + return gate | ||
| 220 | + | ||
| 221 | + | ||
| 222 | +def evaluate(gate: dict, counts: dict, smells: int, metrics: dict, files: list[dict]) -> list[str]: | ||
| 223 | + """Return one human-readable violation string per breached threshold.""" | ||
| 224 | + breaches: list[str] = [] | ||
| 225 | + | ||
| 226 | + def check(limit_key: str, actual: int, label: str) -> None: | ||
| 227 | + limit = gate.get(limit_key) | ||
| 228 | + if limit is not None and actual > limit: | ||
| 229 | + breaches.append(f"{label}: {actual} (max {limit})") | ||
| 230 | + | ||
| 231 | + check("max_error", counts.get("error", 0), "error-level issues") | ||
| 232 | + check("max_warning", counts.get("warning", 0), "warning-level issues") | ||
| 233 | + check("max_note", counts.get("note", 0), "note-level issues") | ||
| 234 | + check("max_smells", smells, "code smells") | ||
| 235 | + check("max_total_complexity", metrics.get("complex", 0), "total complexity") | ||
| 236 | + | ||
| 237 | + limit = gate.get("max_file_complexity") | ||
| 238 | + if limit is not None: | ||
| 239 | + over = [f for f in files if isinstance(f.get("complex"), int) and f["complex"] > limit] | ||
| 240 | + for f in sorted(over, key=lambda r: -r["complex"])[:10]: | ||
| 241 | + breaches.append(f"{f['name']} complexity {f['complex']} (max {limit})") | ||
| 242 | + return breaches | ||
| 243 | + | ||
| 244 | + | ||
| 245 | +def tally(results: list[dict], key: str) -> dict[str, int]: | ||
| 246 | + counts: dict[str, int] = {} | ||
| 247 | + for r in results: | ||
| 248 | + counts[r[key]] = counts.get(r[key], 0) + 1 | ||
| 249 | + return counts | ||
| 250 | + | ||
| 251 | + | ||
| 252 | +def log(msg: str) -> None: | ||
| 253 | + print(f"[quality] {msg}", file=sys.stderr) | ||
| 254 | + | ||
| 255 | + | ||
| 256 | +def die(msg: str) -> None: | ||
| 257 | + print(f"[quality] error: {msg}", file=sys.stderr) | ||
| 258 | + sys.exit(2) | ||
| 259 | + | ||
| 260 | + | ||
| 261 | +def md_table(headers: list[str], rows: list[list[str]]) -> str: | ||
| 262 | + if not rows: | ||
| 263 | + return "_none_\n" | ||
| 264 | + | ||
| 265 | + out = ["| " + " | ".join(headers) + " |", "|" + "|".join(["---"] * len(headers)) + "|"] | ||
| 266 | + out += ["| " + " | ".join(rows_) + " |" for rows_ in (map(str, r) for r in rows)] | ||
| 267 | + return "\n".join(out) + "\n" | ||
| 268 | + | ||
| 269 | + | ||
| 270 | +def delta(cur: int, prev: int | None) -> str: | ||
| 271 | + if prev is None: | ||
| 272 | + return "—" | ||
| 273 | + d = cur - prev | ||
| 274 | + if d == 0: | ||
| 275 | + return "±0" | ||
| 276 | + return f"{d:+d}" | ||
| 277 | + | ||
| 278 | + | ||
| 279 | +def report_header(entry: dict, prev: dict | None, breaches: list[str]) -> list[str]: | ||
| 280 | + verdict = "✅ **PASS**" if entry["gate_passed"] else "❌ **FAIL**" | ||
| 281 | + lines = [ | ||
| 282 | + f"# Quality report — {entry['timestamp']}", | ||
| 283 | + "", | ||
| 284 | + f"- **Gate**: {verdict}", | ||
| 285 | + f"- **Commit**: `{entry['commit'] or 'n/a'}` on `{entry['branch'] or 'n/a'}`", | ||
| 286 | + f"- **qlty**: {entry['qlty_version']}", | ||
| 287 | + f"- **Run**: #{entry['run']}" | ||
| 288 | + + (f" (previous: {prev['timestamp']})" if prev else " (first recorded run)"), | ||
| 289 | + "", | ||
| 290 | + ] | ||
| 291 | + if breaches: | ||
| 292 | + lines += ["## Gate violations", ""] | ||
| 293 | + lines += [f"- {b}" for b in breaches] | ||
| 294 | + lines.append("") | ||
| 295 | + return lines | ||
| 296 | + | ||
| 297 | + | ||
| 298 | +def report_issues(entry: dict, prev: dict | None, check_res: list[dict]) -> list[str]: | ||
| 299 | + """The lint section: counts by level, then the two "where to start" tables.""" | ||
| 300 | + c = entry["counts"] | ||
| 301 | + p_counts = (prev or {}).get("counts") or {} | ||
| 302 | + | ||
| 303 | + lines = ["## Lint issues (`qlty check`)", ""] | ||
| 304 | + lines.append( | ||
| 305 | + md_table( | ||
| 306 | + ["level", "count", "vs previous"], | ||
| 307 | + [[lvl, c.get(lvl, 0), delta(c.get(lvl, 0), p_counts.get(lvl))] | ||
| 308 | + for lvl in ("error", "warning", "note", "none") | ||
| 309 | + if c.get(lvl) or p_counts.get(lvl)], | ||
| 310 | + ) | ||
| 311 | + ) | ||
| 312 | + | ||
| 313 | + by_rule = sorted(tally(check_res, "rule").items(), key=lambda kv: -kv[1])[:15] | ||
| 314 | + lines += ["### Top rules", "", md_table(["rule", "count"], [[k, v] for k, v in by_rule])] | ||
| 315 | + | ||
| 316 | + by_file = sorted(tally([r for r in check_res if r["file"]], "file").items(), | ||
| 317 | + key=lambda kv: -kv[1])[:15] | ||
| 318 | + lines += ["### Most affected files", "", | ||
| 319 | + md_table(["file", "issues"], [[k, v] for k, v in by_file])] | ||
| 320 | + return lines | ||
| 321 | + | ||
| 322 | + | ||
| 323 | +def report_smells(entry: dict, prev: dict | None, smell_res: list[dict]) -> list[str]: | ||
| 324 | + lines = ["## Code smells (`qlty smells`)", "", | ||
| 325 | + f"Total: **{entry['smells']}** (vs previous: " | ||
| 326 | + f"{delta(entry['smells'], (prev or {}).get('smells'))})", ""] | ||
| 327 | + smell_rows = [[r["rule"], r["file"] or "—", r["line"] or "—", r["message"][:110]] | ||
| 328 | + for r in smell_res[:20]] | ||
| 329 | + lines.append(md_table(["smell", "file", "line", "detail"], smell_rows)) | ||
| 330 | + return lines | ||
| 331 | + | ||
| 332 | + | ||
| 333 | +def report_metrics(entry: dict, prev: dict | None, files: list[dict]) -> list[str]: | ||
| 334 | + m = entry["metrics"] | ||
| 335 | + p_metrics = (prev or {}).get("metrics") or {} | ||
| 336 | + | ||
| 337 | + lines = ["## Metrics (`qlty metrics`)", ""] | ||
| 338 | + metric_rows = [ | ||
| 339 | + [k, m.get(k, "—"), delta(m[k], p_metrics.get(k)) if isinstance(m.get(k), int) else "—"] | ||
| 340 | + for k in ("funcs", "classes", "fields", "cyclo", "complex", "lcom", "lines", "loc") | ||
| 341 | + if k in m | ||
| 342 | + ] | ||
| 343 | + lines.append(md_table(["metric", "total", "vs previous"], metric_rows)) | ||
| 344 | + | ||
| 345 | + worst = sorted([f for f in files if isinstance(f.get("complex"), int)], | ||
| 346 | + key=lambda r: -r["complex"])[:15] | ||
| 347 | + lines += ["### Most complex files", "", | ||
| 348 | + md_table(["file", "complex", "cyclo", "loc"], | ||
| 349 | + [[f["name"], f.get("complex", "—"), f.get("cyclo", "—"), | ||
| 350 | + f.get("loc", "—")] for f in worst])] | ||
| 351 | + return lines | ||
| 352 | + | ||
| 353 | + | ||
| 354 | +def report_trend(entry: dict) -> list[str]: | ||
| 355 | + return ["## Trend", "", md_table( | ||
| 356 | + ["run", "timestamp", "error", "warning", "smells", "complex", "gate"], | ||
| 357 | + [[h["run"], h["timestamp"], (h.get("counts") or {}).get("error", 0), | ||
| 358 | + (h.get("counts") or {}).get("warning", 0), h.get("smells", 0), | ||
| 359 | + (h.get("metrics") or {}).get("complex", "—"), | ||
| 360 | + "PASS" if h.get("gate_passed") else "FAIL"] | ||
| 361 | + for h in entry["_trend"]], | ||
| 362 | + )] | ||
| 363 | + | ||
| 364 | + | ||
| 365 | +def build_report(entry: dict, prev: dict | None, check_res: list[dict], | ||
| 366 | + smell_res: list[dict], files: list[dict], breaches: list[str], | ||
| 367 | + notes: list[str]) -> str: | ||
| 368 | + """Assemble the Markdown report, in the order a reader works through it.""" | ||
| 369 | + lines = report_header(entry, prev, breaches) | ||
| 370 | + lines += report_issues(entry, prev, check_res) | ||
| 371 | + lines += report_smells(entry, prev, smell_res) | ||
| 372 | + lines += report_metrics(entry, prev, files) | ||
| 373 | + | ||
| 374 | + if notes: | ||
| 375 | + lines += ["## Tooling notes", ""] + [f"- {n}" for n in notes] + [""] | ||
| 376 | + | ||
| 377 | + lines += report_trend(entry) | ||
| 378 | + return "\n".join(lines) | ||
| 379 | + | ||
| 380 | + | ||
| 381 | +def main() -> int: | ||
| 382 | + ap = argparse.ArgumentParser(description=__doc__) | ||
| 383 | + ap.add_argument("--workspace", default=".", help="project root (default: cwd)") | ||
| 384 | + ap.add_argument("--no-init", action="store_true", | ||
| 385 | + help="fail instead of running `qlty init` when unconfigured") | ||
| 386 | + ap.add_argument("--gate", type=Path, help="gate JSON file (default: .quality/gate.json)") | ||
| 387 | + ap.add_argument("--json", action="store_true", help="print the history entry to stdout") | ||
| 388 | + args = ap.parse_args() | ||
| 389 | + | ||
| 390 | + ws = Path(args.workspace).resolve() | ||
| 391 | + if not ws.is_dir(): | ||
| 392 | + die(f"workspace {ws} does not exist") | ||
| 393 | + | ||
| 394 | + ensure_setup(ws, allow_init=not args.no_init) | ||
| 395 | + | ||
| 396 | + notes: list[str] = [] | ||
| 397 | + log("running qlty check / smells / metrics (this can take several minutes on a cold cache)") | ||
| 398 | + check_res, n1 = collect_check(ws) | ||
| 399 | + smell_res, n2 = collect_smells(ws) | ||
| 400 | + metrics, files, n3 = collect_metrics(ws) | ||
| 401 | + notes += [n for n in (n1, n2, n3) if n] | ||
| 402 | + | ||
| 403 | + counts = tally(check_res, "level") | ||
| 404 | + gate = load_gate(ws, args.gate) | ||
| 405 | + breaches = evaluate(gate, counts, len(smell_res), metrics, files) | ||
| 406 | + | ||
| 407 | + _, ver_out, _ = run(["qlty", "--version"], ws, timeout=60) | ||
| 408 | + outdir = ws / ".quality" | ||
| 409 | + outdir.mkdir(parents=True, exist_ok=True) | ||
| 410 | + history = outdir / "history.jsonl" | ||
| 411 | + | ||
| 412 | + past: list[dict] = [] | ||
| 413 | + if history.is_file(): | ||
| 414 | + for line in history.read_text().splitlines(): | ||
| 415 | + line = line.strip() | ||
| 416 | + if line: | ||
| 417 | + try: | ||
| 418 | + past.append(json.loads(line)) | ||
| 419 | + except json.JSONDecodeError: | ||
| 420 | + continue | ||
| 421 | + | ||
| 422 | + now = datetime.now(timezone.utc) | ||
| 423 | + entry = { | ||
| 424 | + "run": len(past) + 1, | ||
| 425 | + "timestamp": now.strftime("%Y-%m-%dT%H:%M:%SZ"), | ||
| 426 | + "commit": git(["rev-parse", "--short", "HEAD"], ws), | ||
| 427 | + "branch": git(["rev-parse", "--abbrev-ref", "HEAD"], ws), | ||
| 428 | + "qlty_version": ANSI.sub("", ver_out).strip() or "unknown", | ||
| 429 | + "counts": counts, | ||
| 430 | + "smells": len(smell_res), | ||
| 431 | + "metrics": metrics, | ||
| 432 | + "gate": {k: v for k, v in gate.items() if v is not None}, | ||
| 433 | + "gate_passed": not breaches, | ||
| 434 | + "breaches": breaches, | ||
| 435 | + } | ||
| 436 | + | ||
| 437 | + with history.open("a") as fh: | ||
| 438 | + fh.write(json.dumps(entry, sort_keys=True) + "\n") | ||
| 439 | + | ||
| 440 | + entry["_trend"] = (past + [entry])[-10:] | ||
| 441 | + report = build_report(entry, past[-1] if past else None, | ||
| 442 | + check_res, smell_res, files, breaches, notes) | ||
| 443 | + del entry["_trend"] | ||
| 444 | + | ||
| 445 | + stamp = now.strftime("%Y%m%dT%H%M%SZ") | ||
| 446 | + (outdir / f"report-{stamp}.md").write_text(report) | ||
| 447 | + (outdir / "report-latest.md").write_text(report) | ||
| 448 | + | ||
| 449 | + print(report) | ||
| 450 | + log(f"report written to .quality/report-{stamp}.md (and report-latest.md)") | ||
| 451 | + log(f"history appended to .quality/history.jsonl (run #{entry['run']})") | ||
| 452 | + if args.json: | ||
| 453 | + print(json.dumps(entry, indent=2, sort_keys=True)) | ||
| 454 | + | ||
| 455 | + if breaches: | ||
| 456 | + log(f"GATE FAILED — {len(breaches)} violation(s)") | ||
| 457 | + return 1 | ||
| 458 | + log("GATE PASSED") | ||
| 459 | + return 0 | ||
| 460 | + | ||
| 461 | + | ||
| 462 | +if __name__ == "__main__": | ||
| 463 | + sys.exit(main()) | ||
added
.tickets/config.yaml +2 -0 | new file mode 100644 | ||
| @@ -0,0 +1,2 @@ | ||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | |
| 2 | +version: 1 | |
| new file mode 100644 | |||
| @@ -0,0 +1,2 @@ | |||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | ||
| 2 | +version: 1 | ||
added
.tickets/epics.yaml +2 -0 | new file mode 100644 | ||
| @@ -0,0 +1,2 @@ | ||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | |
| 2 | +epics: [] | |
| new file mode 100644 | |||
| @@ -0,0 +1,2 @@ | |||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | ||
| 2 | +epics: [] | ||
added
.tickets/issues/0001-draft.yaml +30 -0 | new file mode 100644 | ||
| @@ -0,0 +1,30 @@ | ||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | |
| 2 | +id: 1 | |
| 3 | +title: draft | |
| 4 | +state: open | |
| 5 | +author: | |
| 6 | + name: k33g | |
| 7 | + email: ph.charriere@gmail.com | |
| 8 | +createdAt: 2026-09-17T09:23:50.599Z | |
| 9 | +updatedAt: 2026-09-17T09:27:44.868Z | |
| 10 | +labels: [] | |
| 11 | +body: | | |
| 12 | + A web server using the ACP protocol to connect to an agent. | |
| 13 | + | |
| 14 | + Goal: run it in a sandbox and connect from the outside, intended for non-technical users. | |
| 15 | + | |
| 16 | + There could be 2 UI modes (non-technical and dev (with server-side code?)). | |
| 17 | + | |
| 18 | + Even in non-technical mode, the user will be able to have a view of what is in their workspace. | |
| 19 | + | |
| 20 | + And with document previews. | |
| 21 | +tasks: | |
| 22 | + - id: 1 | |
| 23 | + title: Check which agents work with ACP | |
| 24 | + state: open | |
| 25 | + priority: urgent | |
| 26 | + author: | |
| 27 | + name: k33g | |
| 28 | + email: ph.charriere@gmail.com | |
| 29 | + createdAt: 2026-09-17T09:27:44.864Z | |
| 30 | +comments: [] | |
| new file mode 100644 | |||
| @@ -0,0 +1,30 @@ | |||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | ||
| 2 | +id: 1 | ||
| 3 | +title: draft | ||
| 4 | +state: open | ||
| 5 | +author: | ||
| 6 | + name: k33g | ||
| 7 | + email: ph.charriere@gmail.com | ||
| 8 | +createdAt: 2026-09-17T09:23:50.599Z | ||
| 9 | +updatedAt: 2026-09-17T09:27:44.868Z | ||
| 10 | +labels: [] | ||
| 11 | +body: | | ||
| 12 | + A web server using the ACP protocol to connect to an agent. | ||
| 13 | + | ||
| 14 | + Goal: run it in a sandbox and connect from the outside, intended for non-technical users. | ||
| 15 | + | ||
| 16 | + There could be 2 UI modes (non-technical and dev (with server-side code?)). | ||
| 17 | + | ||
| 18 | + Even in non-technical mode, the user will be able to have a view of what is in their workspace. | ||
| 19 | + | ||
| 20 | + And with document previews. | ||
| 21 | +tasks: | ||
| 22 | + - id: 1 | ||
| 23 | + title: Check which agents work with ACP | ||
| 24 | + state: open | ||
| 25 | + priority: urgent | ||
| 26 | + author: | ||
| 27 | + name: k33g | ||
| 28 | + email: ph.charriere@gmail.com | ||
| 29 | + createdAt: 2026-09-17T09:27:44.864Z | ||
| 30 | +comments: [] | ||
added
.tickets/issues/0002-create-an-architecture-diagram.yaml +13 -0 | new file mode 100644 | ||
| @@ -0,0 +1,13 @@ | ||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | |
| 2 | +id: 2 | |
| 3 | +title: Create an architecture diagram | |
| 4 | +state: open | |
| 5 | +author: | |
| 6 | + name: k33g | |
| 7 | + email: ph.charriere@gmail.com | |
| 8 | +createdAt: 2026-09-17T09:36:53.046Z | |
| 9 | +updatedAt: 2026-09-17T09:36:53.046Z | |
| 10 | +labels: [] | |
| 11 | +body: "" | |
| 12 | +tasks: [] | |
| 13 | +comments: [] | |
| new file mode 100644 | |||
| @@ -0,0 +1,13 @@ | |||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | ||
| 2 | +id: 2 | ||
| 3 | +title: Create an architecture diagram | ||
| 4 | +state: open | ||
| 5 | +author: | ||
| 6 | + name: k33g | ||
| 7 | + email: ph.charriere@gmail.com | ||
| 8 | +createdAt: 2026-09-17T09:36:53.046Z | ||
| 9 | +updatedAt: 2026-09-17T09:36:53.046Z | ||
| 10 | +labels: [] | ||
| 11 | +body: "" | ||
| 12 | +tasks: [] | ||
| 13 | +comments: [] | ||
added
.tickets/issues/0003-how-to-run-it-outside-the-sandbox.yaml +13 -0 | new file mode 100644 | ||
| @@ -0,0 +1,13 @@ | ||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | |
| 2 | +id: 3 | |
| 3 | +title: How to run it outside the sandbox? | |
| 4 | +state: open | |
| 5 | +author: | |
| 6 | + name: k33g | |
| 7 | + email: ph.charriere@gmail.com | |
| 8 | +createdAt: 2026-09-17T10:09:16.901Z | |
| 9 | +updatedAt: 2026-09-17T10:09:16.901Z | |
| 10 | +labels: [] | |
| 11 | +body: "" | |
| 12 | +tasks: [] | |
| 13 | +comments: [] | |
| new file mode 100644 | |||
| @@ -0,0 +1,13 @@ | |||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | ||
| 2 | +id: 3 | ||
| 3 | +title: How to run it outside the sandbox? | ||
| 4 | +state: open | ||
| 5 | +author: | ||
| 6 | + name: k33g | ||
| 7 | + email: ph.charriere@gmail.com | ||
| 8 | +createdAt: 2026-09-17T10:09:16.901Z | ||
| 9 | +updatedAt: 2026-09-17T10:09:16.901Z | ||
| 10 | +labels: [] | ||
| 11 | +body: "" | ||
| 12 | +tasks: [] | ||
| 13 | +comments: [] | ||
added
.tickets/issues/0004-specifications.yaml +25 -0 | new file mode 100644 | ||
| @@ -0,0 +1,25 @@ | ||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | |
| 2 | +id: 4 | |
| 3 | +title: Specifications | |
| 4 | +state: open | |
| 5 | +author: | |
| 6 | + name: k33g | |
| 7 | + email: ph.charriere@gmail.com | |
| 8 | +createdAt: 2026-09-17T17:58:21.934Z | |
| 9 | +updatedAt: 2026-09-17T18:04:56.702Z | |
| 10 | +labels: [] | |
| 11 | +body: | | |
| 12 | + Je voudrais faire une webapp (front et back) qui soit un client ACP (Agent Client Protocol) d'un agent de code (on commencera par faire les tests avec claude code) | |
| 13 | + | |
| 14 | + donc on a : | |
| 15 | + | |
| 16 | + - 1 SPA servie par un Backend Go | |
| 17 | + - Le Backend Go se connecte avec ACP à l'agent de code | |
| 18 | + - L'IHM de la SPA devra dans un 1er temps reproduire les même principes que ce aue fait le panel agent de zed editor | |
| 19 | + - Il faudra penser l'application évolutive (par exemple pouvor afficher une preview de documents générés, liste de fichiers, ...) | |
| 20 | + | |
| 21 | + L'objectif est de faire une application en mode web avec une expérience utilisateur similaire à celle de Claude Desktop, mais dans une webapp | |
| 22 | + | |
| 23 | + Ainsi on pourra par exemple executer tout cela agent compris dans une sandbox, tout en s'y connectant avec son browser et en ayant une expérience utilisateur agréable | |
| 24 | +tasks: [] | |
| 25 | +comments: [] | |
| new file mode 100644 | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | ||
| 2 | +id: 4 | ||
| 3 | +title: Specifications | ||
| 4 | +state: open | ||
| 5 | +author: | ||
| 6 | + name: k33g | ||
| 7 | + email: ph.charriere@gmail.com | ||
| 8 | +createdAt: 2026-09-17T17:58:21.934Z | ||
| 9 | +updatedAt: 2026-09-17T18:04:56.702Z | ||
| 10 | +labels: [] | ||
| 11 | +body: | | ||
| 12 | + Je voudrais faire une webapp (front et back) qui soit un client ACP (Agent Client Protocol) d'un agent de code (on commencera par faire les tests avec claude code) | ||
| 13 | + | ||
| 14 | + donc on a : | ||
| 15 | + | ||
| 16 | + - 1 SPA servie par un Backend Go | ||
| 17 | + - Le Backend Go se connecte avec ACP à l'agent de code | ||
| 18 | + - L'IHM de la SPA devra dans un 1er temps reproduire les même principes que ce aue fait le panel agent de zed editor | ||
| 19 | + - Il faudra penser l'application évolutive (par exemple pouvor afficher une preview de documents générés, liste de fichiers, ...) | ||
| 20 | + | ||
| 21 | + L'objectif est de faire une application en mode web avec une expérience utilisateur similaire à celle de Claude Desktop, mais dans une webapp | ||
| 22 | + | ||
| 23 | + Ainsi on pourra par exemple executer tout cela agent compris dans une sandbox, tout en s'y connectant avec son browser et en ayant une expérience utilisateur agréable | ||
| 24 | +tasks: [] | ||
| 25 | +comments: [] | ||
added
.tickets/labels.yaml +38 -0 | new file mode 100644 | ||
| @@ -0,0 +1,38 @@ | ||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | |
| 2 | +labels: | |
| 3 | + - name: bug | |
| 4 | + color: "#d73a4a" | |
| 5 | + description: Something is not working | |
| 6 | + - name: documentation | |
| 7 | + color: "#0075ca" | |
| 8 | + description: Improvements or additions to documentation | |
| 9 | + - name: enhancement | |
| 10 | + color: "#a2eeef" | |
| 11 | + description: New feature or request | |
| 12 | + - name: question | |
| 13 | + color: "#d876e3" | |
| 14 | + description: Further information is requested | |
| 15 | + - name: wontfix | |
| 16 | + color: "#ffffff" | |
| 17 | + description: This will not be worked on | |
| 18 | + - name: priority::low | |
| 19 | + color: "#c5def5" | |
| 20 | + description: Can wait | |
| 21 | + - name: priority::medium | |
| 22 | + color: "#fbca04" | |
| 23 | + description: Normal priority | |
| 24 | + - name: priority::high | |
| 25 | + color: "#d93f0b" | |
| 26 | + description: Should be picked up next | |
| 27 | + - name: priority::urgent | |
| 28 | + color: "#b60205" | |
| 29 | + description: Drop everything | |
| 30 | + - name: kind::bug | |
| 31 | + color: "#d73a4a" | |
| 32 | + description: Something is broken | |
| 33 | + - name: kind::feature | |
| 34 | + color: "#0e8a16" | |
| 35 | + description: Something new | |
| 36 | + - name: kind::chore | |
| 37 | + color: "#bfdadc" | |
| 38 | + description: Maintenance, no visible change | |
| new file mode 100644 | |||
| @@ -0,0 +1,38 @@ | |||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | ||
| 2 | +labels: | ||
| 3 | + - name: bug | ||
| 4 | + color: "#d73a4a" | ||
| 5 | + description: Something is not working | ||
| 6 | + - name: documentation | ||
| 7 | + color: "#0075ca" | ||
| 8 | + description: Improvements or additions to documentation | ||
| 9 | + - name: enhancement | ||
| 10 | + color: "#a2eeef" | ||
| 11 | + description: New feature or request | ||
| 12 | + - name: question | ||
| 13 | + color: "#d876e3" | ||
| 14 | + description: Further information is requested | ||
| 15 | + - name: wontfix | ||
| 16 | + color: "#ffffff" | ||
| 17 | + description: This will not be worked on | ||
| 18 | + - name: priority::low | ||
| 19 | + color: "#c5def5" | ||
| 20 | + description: Can wait | ||
| 21 | + - name: priority::medium | ||
| 22 | + color: "#fbca04" | ||
| 23 | + description: Normal priority | ||
| 24 | + - name: priority::high | ||
| 25 | + color: "#d93f0b" | ||
| 26 | + description: Should be picked up next | ||
| 27 | + - name: priority::urgent | ||
| 28 | + color: "#b60205" | ||
| 29 | + description: Drop everything | ||
| 30 | + - name: kind::bug | ||
| 31 | + color: "#d73a4a" | ||
| 32 | + description: Something is broken | ||
| 33 | + - name: kind::feature | ||
| 34 | + color: "#0e8a16" | ||
| 35 | + description: Something new | ||
| 36 | + - name: kind::chore | ||
| 37 | + color: "#bfdadc" | ||
| 38 | + description: Maintenance, no visible change | ||
added
.tickets/milestones.yaml +2 -0 | new file mode 100644 | ||
| @@ -0,0 +1,2 @@ | ||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | |
| 2 | +milestones: [] | |
| new file mode 100644 | |||
| @@ -0,0 +1,2 @@ | |||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | ||
| 2 | +milestones: [] | ||
added
.vscode/extensions.json +12 -0 | new file mode 100644 | ||
| @@ -0,0 +1,12 @@ | ||
| 1 | +{ | |
| 2 | + "recommendations": [ | |
| 3 | + "ms-azuretools.vscode-docker", | |
| 4 | + "pkief.material-icon-theme", | |
| 5 | + "pkief.material-product-icons", | |
| 6 | + "aaron-bond.better-comments", | |
| 7 | + "hediet.vscode-drawio", | |
| 8 | + "anseki.vscode-color", | |
| 9 | + "jeronimoekerdt.color-picker-universal", | |
| 10 | + "wayou.vscode-todo-highlight", | |
| 11 | + ] | |
| 12 | +} | |
| \ No newline at end of file | ||
| new file mode 100644 | |||
| @@ -0,0 +1,12 @@ | |||
| 1 | +{ | ||
| 2 | + "recommendations": [ | ||
| 3 | + "ms-azuretools.vscode-docker", | ||
| 4 | + "pkief.material-icon-theme", | ||
| 5 | + "pkief.material-product-icons", | ||
| 6 | + "aaron-bond.better-comments", | ||
| 7 | + "hediet.vscode-drawio", | ||
| 8 | + "anseki.vscode-color", | ||
| 9 | + "jeronimoekerdt.color-picker-universal", | ||
| 10 | + "wayou.vscode-todo-highlight", | ||
| 11 | + ] | ||
| 12 | +} | ||
| \ No newline at end of file | \ No newline at end of file | ||
added
.vscode/settings.json +48 -0 | new file mode 100644 | ||
| @@ -0,0 +1,48 @@ | ||
| 1 | +{ | |
| 2 | + "workbench.iconTheme": "material-icon-theme", | |
| 3 | + "workbench.colorTheme": "Light 2026", | |
| 4 | + "editor.fontSize": 14, | |
| 5 | + "terminal.integrated.fontSize": 14, | |
| 6 | + "editor.insertSpaces": true, | |
| 7 | + "editor.tabSize": 4, | |
| 8 | + "editor.detectIndentation": true, | |
| 9 | + "files.autoSave": "afterDelay", | |
| 10 | + "files.autoSaveDelay": 1000, | |
| 11 | + "workbench.tree.indent": 20, | |
| 12 | + "window.zoomLevel": 1.0, | |
| 13 | + "[markdown]": { | |
| 14 | + "editor.unicodeHighlight.ambiguousCharacters": false, | |
| 15 | + "editor.unicodeHighlight.invisibleCharacters": false, | |
| 16 | + "diffEditor.ignoreTrimWhitespace": false, | |
| 17 | + "editor.fontWeight": "normal", | |
| 18 | + "editor.fontFamily": "'Droid Sans Mono', 'monospace', monospace", | |
| 19 | + "editor.fontSize": 14, | |
| 20 | + "editor.wordWrap": "on", | |
| 21 | + "editor.quickSuggestions": { | |
| 22 | + "comments": "off", | |
| 23 | + "strings": "off", | |
| 24 | + "other": "off" | |
| 25 | + } | |
| 26 | + }, | |
| 27 | + "markdown.preview.fontSize": 14, | |
| 28 | + // "workbench.editorAssociations": { | |
| 29 | + // "*.md": "vscode.markdown.preview.editor" | |
| 30 | + // }, | |
| 31 | + "[dockerfile]": { | |
| 32 | + "editor.fontSize": 14, | |
| 33 | + }, | |
| 34 | + "[dockercompose]": { | |
| 35 | + "editor.fontSize": 14, | |
| 36 | + }, | |
| 37 | + "[json]": { | |
| 38 | + "editor.fontSize": 14, | |
| 39 | + }, | |
| 40 | + "[yaml]": { | |
| 41 | + "editor.fontSize": 14, | |
| 42 | + | |
| 43 | + }, | |
| 44 | + "[go]": { | |
| 45 | + "editor.fontSize": 14, | |
| 46 | + }, | |
| 47 | + | |
| 48 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,48 @@ | |||
| 1 | +{ | ||
| 2 | + "workbench.iconTheme": "material-icon-theme", | ||
| 3 | + "workbench.colorTheme": "Light 2026", | ||
| 4 | + "editor.fontSize": 14, | ||
| 5 | + "terminal.integrated.fontSize": 14, | ||
| 6 | + "editor.insertSpaces": true, | ||
| 7 | + "editor.tabSize": 4, | ||
| 8 | + "editor.detectIndentation": true, | ||
| 9 | + "files.autoSave": "afterDelay", | ||
| 10 | + "files.autoSaveDelay": 1000, | ||
| 11 | + "workbench.tree.indent": 20, | ||
| 12 | + "window.zoomLevel": 1.0, | ||
| 13 | + "[markdown]": { | ||
| 14 | + "editor.unicodeHighlight.ambiguousCharacters": false, | ||
| 15 | + "editor.unicodeHighlight.invisibleCharacters": false, | ||
| 16 | + "diffEditor.ignoreTrimWhitespace": false, | ||
| 17 | + "editor.fontWeight": "normal", | ||
| 18 | + "editor.fontFamily": "'Droid Sans Mono', 'monospace', monospace", | ||
| 19 | + "editor.fontSize": 14, | ||
| 20 | + "editor.wordWrap": "on", | ||
| 21 | + "editor.quickSuggestions": { | ||
| 22 | + "comments": "off", | ||
| 23 | + "strings": "off", | ||
| 24 | + "other": "off" | ||
| 25 | + } | ||
| 26 | + }, | ||
| 27 | + "markdown.preview.fontSize": 14, | ||
| 28 | + // "workbench.editorAssociations": { | ||
| 29 | + // "*.md": "vscode.markdown.preview.editor" | ||
| 30 | + // }, | ||
| 31 | + "[dockerfile]": { | ||
| 32 | + "editor.fontSize": 14, | ||
| 33 | + }, | ||
| 34 | + "[dockercompose]": { | ||
| 35 | + "editor.fontSize": 14, | ||
| 36 | + }, | ||
| 37 | + "[json]": { | ||
| 38 | + "editor.fontSize": 14, | ||
| 39 | + }, | ||
| 40 | + "[yaml]": { | ||
| 41 | + "editor.fontSize": 14, | ||
| 42 | + | ||
| 43 | + }, | ||
| 44 | + "[go]": { | ||
| 45 | + "editor.fontSize": 14, | ||
| 46 | + }, | ||
| 47 | + | ||
| 48 | +} | ||
added
README.md +2 -0 | new file mode 100644 | ||
| @@ -0,0 +1,2 @@ | ||
| 1 | +# Ori | |
| 2 | +> The origin from which many voices are called, and through which they are known | |
| new file mode 100644 | |||
| @@ -0,0 +1,2 @@ | |||
| 1 | +# Ori | ||
| 2 | +> The origin from which many voices are called, and through which they are known | ||
added
git.sh +136 -0 | new file mode 100644 | ||
| @@ -0,0 +1,136 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +message="" | |
| 3 | +case $1 in | |
| 4 | + | |
| 5 | + # 🎨: art | |
| 6 | + art) | |
| 7 | + message="Improve structure / format of the code" | |
| 8 | + emoji="🎨" | |
| 9 | + ;; | |
| 10 | + | |
| 11 | + # 🐛: bug | |
| 12 | + bug|fix) | |
| 13 | + message="Fix a bug" | |
| 14 | + emoji="🐛" | |
| 15 | + ;; | |
| 16 | + | |
| 17 | + # 🤓: geek | |
| 18 | + human|human-fixed) | |
| 19 | + message="Human Fixed" | |
| 20 | + emoji="🤓" | |
| 21 | + ;; | |
| 22 | + | |
| 23 | + # 🤖: robot | |
| 24 | + ai|ai-generated) | |
| 25 | + message="AI generated" | |
| 26 | + emoji="🤖" | |
| 27 | + ;; | |
| 28 | + | |
| 29 | + # ✨: sparkles | |
| 30 | + sparkles|feature) | |
| 31 | + message="Introduce new feature(s)" | |
| 32 | + emoji="✨" | |
| 33 | + ;; | |
| 34 | + | |
| 35 | + # 🧩: jigsaw | |
| 36 | + jigsaw|example|examples|demo|demos) | |
| 37 | + message="Introduce new example(s)" | |
| 38 | + emoji="🧩" | |
| 39 | + ;; | |
| 40 | + | |
| 41 | + | |
| 42 | + # 📝: memo | |
| 43 | + memo|doc|documentation) | |
| 44 | + message="Add or update documentation" | |
| 45 | + emoji="📝" | |
| 46 | + ;; | |
| 47 | + | |
| 48 | + # 🌸: cherry_blossom | |
| 49 | + gardening|garden|clean|cleaning) | |
| 50 | + message="Gardening" | |
| 51 | + emoji="🌸" | |
| 52 | + ;; | |
| 53 | + | |
| 54 | + # 🚀: rocket | |
| 55 | + rocket|deploy) | |
| 56 | + message="Deploy stuff" | |
| 57 | + emoji="🚀" | |
| 58 | + ;; | |
| 59 | + | |
| 60 | + # 🎉: tada | |
| 61 | + tada|first) | |
| 62 | + message="Begin a project" | |
| 63 | + emoji="🎉" | |
| 64 | + ;; | |
| 65 | + | |
| 66 | + # 🚧: construction | |
| 67 | + construction|wip) | |
| 68 | + message="Work in progress" | |
| 69 | + emoji="🚧" | |
| 70 | + ;; | |
| 71 | + | |
| 72 | + # 📦️: package | |
| 73 | + package|build) | |
| 74 | + message="Add or update compiled files or packages" | |
| 75 | + emoji="📦️" | |
| 76 | + ;; | |
| 77 | + | |
| 78 | + # 📦️: package | |
| 79 | + release) | |
| 80 | + message="Create a release" | |
| 81 | + emoji="📦️" | |
| 82 | + ;; | |
| 83 | + | |
| 84 | + # 👽️: alien | |
| 85 | + alien|api) | |
| 86 | + message="Update code due to external API changes" | |
| 87 | + emoji="👽️" | |
| 88 | + ;; | |
| 89 | + | |
| 90 | + # 🐳: whale | |
| 91 | + docker|container) | |
| 92 | + message="Docker" | |
| 93 | + emoji="🐳" | |
| 94 | + ;; | |
| 95 | + | |
| 96 | + # 🍊: tangerine | |
| 97 | + gitpod|gitpodify) | |
| 98 | + message="Gitpodify" | |
| 99 | + emoji="🍊" | |
| 100 | + ;; | |
| 101 | + | |
| 102 | + # 🧪: test tube | |
| 103 | + alembic|experiments|experiment|xp) | |
| 104 | + message="Perform experiments" | |
| 105 | + emoji="🧪" | |
| 106 | + ;; | |
| 107 | + | |
| 108 | + # ✅: check mark | |
| 109 | + test|tests|testing) | |
| 110 | + message="Add or update tests" | |
| 111 | + emoji="✅" | |
| 112 | + ;; | |
| 113 | + | |
| 114 | + # 💾: floppy-disk | |
| 115 | + save) | |
| 116 | + message="Saved" | |
| 117 | + emoji="💾" | |
| 118 | + ;; | |
| 119 | + | |
| 120 | + *) | |
| 121 | + message="Updated" | |
| 122 | + emoji="🛟" | |
| 123 | + ;; | |
| 124 | + | |
| 125 | +esac | |
| 126 | + | |
| 127 | +find . -name '.DS_Store' -type f -delete | |
| 128 | + | |
| 129 | +if [ -z "$2" ] | |
| 130 | +then | |
| 131 | + # empty | |
| 132 | + git add .; git commit -m "$emoji $message."; git push | |
| 133 | +else | |
| 134 | + # not empty | |
| 135 | + git add .; git commit -m "$emoji $message: $2"; git push | |
| 136 | +fi | |
| new file mode 100644 | |||
| @@ -0,0 +1,136 @@ | |||
| 1 | +#!/bin/bash | ||
| 2 | +message="" | ||
| 3 | +case $1 in | ||
| 4 | + | ||
| 5 | + # 🎨: art | ||
| 6 | + art) | ||
| 7 | + message="Improve structure / format of the code" | ||
| 8 | + emoji="🎨" | ||
| 9 | + ;; | ||
| 10 | + | ||
| 11 | + # 🐛: bug | ||
| 12 | + bug|fix) | ||
| 13 | + message="Fix a bug" | ||
| 14 | + emoji="🐛" | ||
| 15 | + ;; | ||
| 16 | + | ||
| 17 | + # 🤓: geek | ||
| 18 | + human|human-fixed) | ||
| 19 | + message="Human Fixed" | ||
| 20 | + emoji="🤓" | ||
| 21 | + ;; | ||
| 22 | + | ||
| 23 | + # 🤖: robot | ||
| 24 | + ai|ai-generated) | ||
| 25 | + message="AI generated" | ||
| 26 | + emoji="🤖" | ||
| 27 | + ;; | ||
| 28 | + | ||
| 29 | + # ✨: sparkles | ||
| 30 | + sparkles|feature) | ||
| 31 | + message="Introduce new feature(s)" | ||
| 32 | + emoji="✨" | ||
| 33 | + ;; | ||
| 34 | + | ||
| 35 | + # 🧩: jigsaw | ||
| 36 | + jigsaw|example|examples|demo|demos) | ||
| 37 | + message="Introduce new example(s)" | ||
| 38 | + emoji="🧩" | ||
| 39 | + ;; | ||
| 40 | + | ||
| 41 | + | ||
| 42 | + # 📝: memo | ||
| 43 | + memo|doc|documentation) | ||
| 44 | + message="Add or update documentation" | ||
| 45 | + emoji="📝" | ||
| 46 | + ;; | ||
| 47 | + | ||
| 48 | + # 🌸: cherry_blossom | ||
| 49 | + gardening|garden|clean|cleaning) | ||
| 50 | + message="Gardening" | ||
| 51 | + emoji="🌸" | ||
| 52 | + ;; | ||
| 53 | + | ||
| 54 | + # 🚀: rocket | ||
| 55 | + rocket|deploy) | ||
| 56 | + message="Deploy stuff" | ||
| 57 | + emoji="🚀" | ||
| 58 | + ;; | ||
| 59 | + | ||
| 60 | + # 🎉: tada | ||
| 61 | + tada|first) | ||
| 62 | + message="Begin a project" | ||
| 63 | + emoji="🎉" | ||
| 64 | + ;; | ||
| 65 | + | ||
| 66 | + # 🚧: construction | ||
| 67 | + construction|wip) | ||
| 68 | + message="Work in progress" | ||
| 69 | + emoji="🚧" | ||
| 70 | + ;; | ||
| 71 | + | ||
| 72 | + # 📦️: package | ||
| 73 | + package|build) | ||
| 74 | + message="Add or update compiled files or packages" | ||
| 75 | + emoji="📦️" | ||
| 76 | + ;; | ||
| 77 | + | ||
| 78 | + # 📦️: package | ||
| 79 | + release) | ||
| 80 | + message="Create a release" | ||
| 81 | + emoji="📦️" | ||
| 82 | + ;; | ||
| 83 | + | ||
| 84 | + # 👽️: alien | ||
| 85 | + alien|api) | ||
| 86 | + message="Update code due to external API changes" | ||
| 87 | + emoji="👽️" | ||
| 88 | + ;; | ||
| 89 | + | ||
| 90 | + # 🐳: whale | ||
| 91 | + docker|container) | ||
| 92 | + message="Docker" | ||
| 93 | + emoji="🐳" | ||
| 94 | + ;; | ||
| 95 | + | ||
| 96 | + # 🍊: tangerine | ||
| 97 | + gitpod|gitpodify) | ||
| 98 | + message="Gitpodify" | ||
| 99 | + emoji="🍊" | ||
| 100 | + ;; | ||
| 101 | + | ||
| 102 | + # 🧪: test tube | ||
| 103 | + alembic|experiments|experiment|xp) | ||
| 104 | + message="Perform experiments" | ||
| 105 | + emoji="🧪" | ||
| 106 | + ;; | ||
| 107 | + | ||
| 108 | + # ✅: check mark | ||
| 109 | + test|tests|testing) | ||
| 110 | + message="Add or update tests" | ||
| 111 | + emoji="✅" | ||
| 112 | + ;; | ||
| 113 | + | ||
| 114 | + # 💾: floppy-disk | ||
| 115 | + save) | ||
| 116 | + message="Saved" | ||
| 117 | + emoji="💾" | ||
| 118 | + ;; | ||
| 119 | + | ||
| 120 | + *) | ||
| 121 | + message="Updated" | ||
| 122 | + emoji="🛟" | ||
| 123 | + ;; | ||
| 124 | + | ||
| 125 | +esac | ||
| 126 | + | ||
| 127 | +find . -name '.DS_Store' -type f -delete | ||
| 128 | + | ||
| 129 | +if [ -z "$2" ] | ||
| 130 | +then | ||
| 131 | + # empty | ||
| 132 | + git add .; git commit -m "$emoji $message."; git push | ||
| 133 | +else | ||
| 134 | + # not empty | ||
| 135 | + git add .; git commit -m "$emoji $message: $2"; git push | ||
| 136 | +fi | ||
added
kits/dev-toolkit/README.md +268 -0 | new file mode 100644 | ||
| @@ -0,0 +1,268 @@ | ||
| 1 | +# `dev-toolkit` kit | |
| 2 | + | |
| 3 | +A Docker Sandboxes **v2 mixin kit** that provisions a general-purpose development toolkit: | |
| 4 | +a code-quality engine, pinned Go / Rust / Node toolchains, and three Claude Code skills | |
| 5 | +covering code quality, documentation, and a controlled feature-development workflow. | |
| 6 | + | |
| 7 | +| Tool | Version | Installed at | Notes | | |
| 8 | +| --- | --- | --- | --- | | |
| 9 | +| qlty | 0.639.0 | `/usr/local/bin/qlty` | Lint + smells + metrics engine, for any language. | | |
| 10 | +| Go | 1.26.5 | `/usr/local/go` | Symlinked into `/usr/local/bin`, shadowing the template's own `/usr/bin/go`. | | |
| 11 | +| TinyGo | 0.41.1 | `/usr/local/tinygo` | `TINYGOROOT` set; for `wasm`, `wasip1`, `wasip2` targets. | | |
| 12 | +| Rust | 1.97.1 | `/usr/local/rustup`, `/usr/local/cargo` | Via rustup, `minimal` profile + `clippy` + `rustfmt`, with the `wasm32-wasip2` target. | | |
| 13 | +| Node.js | 24.19.0 | `/usr/local/node` | LTS "Krypton". Shadows the template's own `/usr/bin/node` (22.x). | | |
| 14 | +| pnpm | 11.22.0 | `/usr/local/share/npm-global/bin` | Plus bundled `corepack`, for projects that pin a version via `packageManager`. | | |
| 15 | +| vsce / ovsx | 3.9.2 / 1.1.1 | `/usr/local/share/npm-global/bin` | Package and publish VS Code extensions to the Marketplace / Open VSX. | | |
| 16 | +| gcc, g++ | Ubuntu's | `/usr/bin` | The base template has **no** C compiler; Rust and node-gyp both need one. | | |
| 17 | + | |
| 18 | +| Skill | Purpose | | |
| 19 | +| --- | --- | | |
| 20 | +| `quality` | Configures qlty, measures the project, writes a report, tracks progression, refactors until the gate passes. | | |
| 21 | +| `diataxis-doc` | Writes or restructures documentation with the Diátaxis four-quadrant method. | | |
| 22 | +| `methodical-dev` | Orchestrator. Drives a feature through requirements → plan → implementation → quality gate → bilingual docs → project memory, stopping for approval at every step. Invokes the other two. | | |
| 23 | + | |
| 24 | + | |
| 25 | +Plus a `~/.claude/CLAUDE.md`, loaded at the start of every session, which brackets the work at | |
| 26 | +both ends: **read** the project's committed `.memory/` record — summary, history, latest | |
| 27 | +handoff — before planning or asking the user anything, and **update all three** before handing | |
| 28 | +control back, unconditionally, however small the session. `summary.md` is edited in place and | |
| 29 | +never regenerated from one session's partial view; `history.md` gets exactly one appended | |
| 30 | +entry; `handoffs/` gets today's file. `methodical-dev` (Phase 7) holds the full templates — | |
| 31 | +`CLAUDE.md` is what makes the same discipline apply to ad-hoc work that never invokes the skill. | |
| 32 | + | |
| 33 | +**Only the toolchain binaries are language-specific.** qlty is not a Go tool: `qlty init` | |
| 34 | +detects the languages present and enables the matching linters, and all three skills are | |
| 35 | +language-agnostic. Drop the toolchain install steps from `spec.yaml` and the kit is a pure | |
| 36 | +quality/docs/workflow mixin for any stack. The install steps are independent of each other, | |
| 37 | +so a Go-only or Rust-only sandbox is a matter of deleting the ones you do not want — except | |
| 38 | +the C-toolchain step, which Rust and native npm addons both depend on. | |
| 39 | + | |
| 40 | +Project-specific skills do **not** belong here. `builtin-parity`, which describes the | |
| 41 | +GoloScript repository in particular, lives in the sibling [`golo-toolkit`](../golo-toolkit/) | |
| 42 | +kit and is composed in only where it applies. | |
| 43 | + | |
| 44 | +## Layout | |
| 45 | + | |
| 46 | +``` | |
| 47 | +kits/dev-toolkit/ | |
| 48 | +├── spec.yaml # schemaVersion "2", kind: mixin | |
| 49 | +├── README.md | |
| 50 | +└── files/home/.claude/ | |
| 51 | + ├── CLAUDE.md # loaded every session: read .memory/ first | |
| 52 | + └── skills/ | |
| 53 | + ├── quality/ | |
| 54 | + │ ├── SKILL.md # the workflow the agent follows | |
| 55 | + │ └── scripts/quality_report.py # measurement, report, history, gate verdict | |
| 56 | + ├── diataxis-doc/SKILL.md | |
| 57 | + └── methodical-dev/SKILL.md | |
| 58 | +``` | |
| 59 | + | |
| 60 | +`files/home/**` is copied into the agent's home at container start, so the skills land | |
| 61 | +under `~/.claude/skills/` and Claude Code picks them up as personal skills, and `CLAUDE.md` | |
| 62 | +lands at `~/.claude/CLAUDE.md` — the user-level memory file, prepended to **every** Claude | |
| 63 | +Code session in the sandbox regardless of which project is open. | |
| 64 | + | |
| 65 | +**Why both a `CLAUDE.md` and a skill say to read `.memory/`.** `methodical-dev`'s Phase 0 | |
| 66 | +only runs when that skill is invoked. `~/.claude/CLAUDE.md` is loaded unconditionally, so an | |
| 67 | +ad-hoc request ("fix this bug") also starts by reading the project record instead of | |
| 68 | +re-deriving state that is already written down. Keep the file short — it costs tokens on | |
| 69 | +every single session; the detail belongs in the skills and in the project's own | |
| 70 | +`.memory/README.md`. | |
| 71 | + | |
| 72 | +Everything in that tree is packed into the kit and shipped into the sandbox, so keep it to | |
| 73 | +the skills themselves — a stray `__pycache__/` would be shipped alongside them. The | |
| 74 | +repository `.gitignore` guards against that. | |
| 75 | + | |
| 76 | +## Usage | |
| 77 | + | |
| 78 | +```bash | |
| 79 | +# create the sandbox with the kit — `--kit` at create time, not `sbx kit add`, | |
| 80 | +# so the cache volumes are attached (see Known limitations) | |
| 81 | +sbx run claude --kit ./kits/dev-toolkit | |
| 82 | +``` | |
| 83 | + | |
| 84 | +Then, in the agent session: *"run a quality report"*, *"document this project with | |
| 85 | +Diátaxis"*, or *"let's build this feature methodically"*. | |
| 86 | + | |
| 87 | +### Editor extensions | |
| 88 | + | |
| 89 | +The two ecosystems use different toolchains, and both are provisioned: | |
| 90 | + | |
| 91 | +| | VS Code | Zed | | |
| 92 | +| --- | --- | --- | | |
| 93 | +| Language | TypeScript / JavaScript on Node | Rust compiled to WebAssembly | | |
| 94 | +| Manifest | `package.json` (`contributes`, `engines.vscode`) | `extension.toml` + `Cargo.toml` | | |
| 95 | +| Build | the project's own script (tsc, esbuild) | `cargo build --release --target wasm32-wasip2` | | |
| 96 | +| Package | `vsce package` → a `.vsix` | Zed's own extension builder, run by the editor | | |
| 97 | +| Publish | `vsce publish`, `ovsx publish` | a PR to `zed-industries/extensions` | | |
| 98 | + | |
| 99 | +`wasm32-wasip2` is preinstalled because that is the target Zed compiles extensions with; | |
| 100 | +building for anything else produces an artifact it refuses. Both flows were verified inside | |
| 101 | +the sandbox end to end — `npm install` → `tsc` → `vsce package` → `.vsix`, and | |
| 102 | +`zed_extension_api` → `cargo build --target wasm32-wasip2` → a WASM **component** (layer | |
| 103 | +`0100`, not a core module). | |
| 104 | + | |
| 105 | +What does **not** work here, by construction: no editor runs in the sandbox, so VS Code's F5 | |
| 106 | +Extension Development Host and Zed's *Install Dev Extension* are host-side actions, and | |
| 107 | +`@vscode/test-electron` needs an X server this kit does not install. Compile, unit-test, lint | |
| 108 | +and package in the sandbox; drive the editor on the host. | |
| 109 | + | |
| 110 | +## Design notes | |
| 111 | + | |
| 112 | +**Why a mixin and not a `kind: sandbox` kit.** The kit adds tooling to whatever agent you | |
| 113 | +are already using rather than defining its own image, so it composes: `--kit` it onto | |
| 114 | +`claude`, `codex`, `shell`, or anything else. Exactly one `sandbox` kit is allowed per | |
| 115 | +composition; mixins stack freely. | |
| 116 | + | |
| 117 | +**Why pinned tarballs instead of the official installers.** `curl https://qlty.sh | sh` | |
| 118 | +resolves `latest` (so two sandboxes built a day apart differ), POSTs an install event to | |
| 119 | +`cdp.customer.io`, and rewrites the user's shell rc files. Every artifact here is pinned | |
| 120 | +by version **and** SHA256, verified with `sha256sum -c` before it is unpacked — the same | |
| 121 | +pattern the official `trivy`, `vale`, and `mise` kits use. qlty's own published | |
| 122 | +`.sha256` files match the digests in `spec.yaml`. | |
| 123 | + | |
| 124 | +**Why Python unpacks the qlty archive.** qlty ships only `.tar.xz` and the sandbox | |
| 125 | +template has no `xz` binary. Installing `xz-utils` would mean running `apt-get update`, | |
| 126 | +which re-fetches *every* configured apt source — including the template's | |
| 127 | +`download.docker.com` — and fails if any one of them is not allow-listed. `python3` is | |
| 128 | +present, and `lzma` is in its standard library, so the archive is decompressed with no | |
| 129 | +extra egress at all. | |
| 130 | + | |
| 131 | +**Toolchain pairing is load-bearing.** TinyGo 0.41.1 accepts Go 1.19–1.26 *and* refuses a | |
| 132 | +GOROOT newer than the Go it was itself built with (1.26.2). Go 1.26.5 satisfies both | |
| 133 | +bounds. Bumping Go to 1.27 breaks TinyGo — bump the two together. `GOTOOLCHAIN=local` | |
| 134 | +prevents Go from silently downloading a different toolchain behind your back. | |
| 135 | + | |
| 136 | +**Why a C compiler is a hard requirement, not a nicety.** The base template ships `make` | |
| 137 | +but no `cc`, `gcc`, `clang`, `ld` or libc headers. That produces a genuinely misleading | |
| 138 | +failure: `cargo build --target wasm32-wasip2` **succeeds** without a C compiler, because | |
| 139 | +rustc links wasm targets with its own bundled `rust-lld` — while `cargo test`, which builds | |
| 140 | +for the host, dies with ``error: linker `cc` not found``. A Zed extension therefore appears | |
| 141 | +to build fine and cannot be unit-tested, which is the worst possible shape for a missing | |
| 142 | +dependency. The same gap breaks any crate with a C-compiling `build.rs`, and any npm package | |
| 143 | +with a native addon built through node-gyp. `apt-get update` is deliberately *not* run: the | |
| 144 | +template ships populated `/var/lib/apt/lists`, so a plain `apt-get install` works, whereas | |
| 145 | +`update` re-fetches every configured source (Docker's included) and fails if one is not | |
| 146 | +allow-listed. It stays only as a fallback for the day those lists go stale. | |
| 147 | + | |
| 148 | +**Why rustup rather than the standalone Rust tarballs.** A pinned `rustup-init` *binary* | |
| 149 | +(digest-verified, not the `curl https://sh.rustup.rs | sh` bootstrapper, which resolves | |
| 150 | +`latest` and edits shell rc files) buys a working `rustup target add` — which matters because | |
| 151 | +Zed itself shells out to exactly that when an extension needs a target that is not installed. | |
| 152 | +Rust lives under `/usr/local` rather than in the home directory, for the same reason Go does: | |
| 153 | +the install runs once at container creation, and only paths baked into the image survive a | |
| 154 | +restart. Both `RUSTUP_HOME` and `CARGO_HOME` are left world-writable — the layout the | |
| 155 | +official `rust` Docker image uses — so `rustup target add`, `rustup component add` and | |
| 156 | +`cargo install` work as the agent user without sudo. | |
| 157 | + | |
| 158 | +**Why the kit does *not* set npm's prefix.** The obvious move — `npm_config_prefix=/usr/local`, | |
| 159 | +so global installs land in `/usr/local/bin` — is wrong here. The base template already exports | |
| 160 | +`NPM_CONFIG_PREFIX=/usr/local/share/npm-global`, and that directory is **agent-owned and | |
| 161 | +already on PATH ahead of `/usr/local/bin`**. Overriding it would move global installs into a | |
| 162 | +root-owned tree and break `npm install -g` for the agent user, which currently works with no | |
| 163 | +sudo. The install step passes `--prefix` explicitly (rather than trusting the variable to be | |
| 164 | +exported into a root-run step) and then `chown`s the tree back to the agent. | |
| 165 | + | |
| 166 | +**`--allow-scripts` is not optional for vsce.** npm 11 skips unrecognised lifecycle scripts | |
| 167 | +and merely warns. Two of vsce's dependencies need theirs: `keytar` builds the native addon | |
| 168 | +that stores a publishing PAT, and `@vscode/vsce-sign` unpacks the signing binary. Skipped, | |
| 169 | +they fail at `vsce publish` time — long after the install that caused it. | |
| 170 | + | |
| 171 | +**The cache volumes.** Three, all of them things that are otherwise re-downloaded on a cold | |
| 172 | +start: `~/.qlty` (qlty provisions a JVM for `radarlint-go`, Node, and each linter binary on | |
| 173 | +demand), `/usr/local/cargo/registry` (Cargo's `.crate` files and sparse index) and `~/.npm`. | |
| 174 | +The Cargo one is deliberately the `registry` subdirectory rather than all of `CARGO_HOME`: | |
| 175 | +mounting an empty volume over `CARGO_HOME` would shadow the rustup shims in its `bin`. Volumes | |
| 176 | +are fixed at container creation, so `sbx kit add` warns and skips them — use `--kit` at create | |
| 177 | +time to get the caches. | |
| 178 | + | |
| 179 | +## Maintenance | |
| 180 | + | |
| 181 | +**Bumping a version.** Change the version *and* both per-arch `SHA256` values in the | |
| 182 | +matching `setup.install` entry in `spec.yaml`. Sources: | |
| 183 | + | |
| 184 | +- Go — <https://go.dev/dl/?mode=json> (`files[].sha256`) | |
| 185 | +- TinyGo — compute from the release tarball; the project publishes no checksums file | |
| 186 | +- qlty — the `<asset>.tar.xz.sha256` file next to each release asset | |
| 187 | +- Node — `https://nodejs.org/dist/v<version>/SHASUMS256.txt`. Pick from the **active LTS** | |
| 188 | + line; `https://nodejs.org/dist/index.json` marks it with a non-`false` `lts` field. Note the | |
| 189 | + `.tar.gz` digest, not `.tar.xz`: the template has no `xz`, and unlike qlty, Node publishes | |
| 190 | + both — which is why this step needs no python-lzma workaround | |
| 191 | +- rustup — `https://static.rust-lang.org/rustup/archive/<ver>/<triple>/rustup-init.sha256`, | |
| 192 | + with the current version at `https://static.rust-lang.org/rustup/release-stable.toml` | |
| 193 | +- Rust itself — no digest to update; rustup resolves and verifies the toolchain. The current | |
| 194 | + stable version is the `[pkg.rust] version` in | |
| 195 | + `https://static.rust-lang.org/dist/channel-rust-stable.toml` | |
| 196 | +- vsce / ovsx / pnpm — `npm view <pkg> version` | |
| 197 | + | |
| 198 | +**Before bumping the Zed target.** `wasm32-wasip2` is not a preference — it is read from Zed's | |
| 199 | +own extension builder (`RUST_TARGET` in `crates/extension/src/extension_builder.rs`). Confirm | |
| 200 | +there before changing it; an extension built for another target is one Zed refuses to load. | |
| 201 | + | |
| 202 | +**Validating a change.** | |
| 203 | + | |
| 204 | +```bash | |
| 205 | +sbx kit validate ./kits/dev-toolkit | |
| 206 | +sbx kit inspect ./kits/dev-toolkit --json | jq '.warnings' # expect null or [] | |
| 207 | +``` | |
| 208 | + | |
| 209 | +Empty warnings is the green light — a non-empty list means a v1 surface crept in that | |
| 210 | +will stop loading at the spec's Phase 6 cutover. | |
| 211 | + | |
| 212 | +`Install commands completed` only means the commands exited `0`. Verify the outcome: | |
| 213 | + | |
| 214 | +```bash | |
| 215 | +sbx exec <sandbox> -- sh -lc ' | |
| 216 | + go version && tinygo version && qlty --version && | |
| 217 | + rustc --version && cargo --version && rustup target list --installed && | |
| 218 | + node --version && npm --version && pnpm --version && vsce --version && ovsx --version && | |
| 219 | + cc --version | head -1' | |
| 220 | +``` | |
| 221 | + | |
| 222 | +And verify the thing versions cannot tell you — that host-target Rust actually **links**, | |
| 223 | +which is what the C-toolchain step exists for: | |
| 224 | + | |
| 225 | +```bash | |
| 226 | +sbx exec <sandbox> -- sh -lc ' | |
| 227 | + cd "$(mktemp -d)" && cargo init --lib -q . && cargo test 2>&1 | tail -3' | |
| 228 | +``` | |
| 229 | + | |
| 230 | +**Extending the network allowlist.** `permissions.network.allow` covers the install downloads, | |
| 231 | +qlty's plugin/linter resolution, the Go module proxy, Cargo's three hosts (the sparse index | |
| 232 | +`index.crates.io`, the CDN `static.crates.io`, and the API `crates.io` — all three are needed), | |
| 233 | +Ubuntu's archives for the C toolchain, and the VS Code Marketplace / Open VSX publishing paths. | |
| 234 | +A linter for another language will need its registry added. Find out what is missing rather | |
| 235 | +than guessing: | |
| 236 | + | |
| 237 | +```bash | |
| 238 | +sbx policy log <sandbox> | |
| 239 | +``` | |
| 240 | + | |
| 241 | +That prints what the proxy actually allowed and blocked. Add blocked hosts one at a time. | |
| 242 | + | |
| 243 | +## Known limitations | |
| 244 | + | |
| 245 | +- **`sbx kit add` does not write mixin context.** The engine gates the kit-memory write on | |
| 246 | + the artifact's own AI-profile filename, which mixins deliberately do not carry (`agentInstructions.filename` is ignored for a mixin), so adding this | |
| 247 | + kit at runtime silently skips `kits-memory/dev-toolkit.md`. Create the sandbox with | |
| 248 | + `--kit` instead. The same applies to the `~/.qlty` volume. | |
| 249 | +- **The skills and `CLAUDE.md` are Claude Code-specific.** The toolchain is not. The kit sets | |
| 250 | + no `requires.agent`, so it works with any agent; on non-Claude agents those files are inert | |
| 251 | + and `agentInstructions` carries the same instructions — the `.memory/`-first rule and the | |
| 252 | + `quality` script invocation. | |
| 253 | +- **Only `linux/amd64` and `linux/arm64`** are supported. Other architectures fail the | |
| 254 | + install with an explicit error rather than installing something wrong. | |
| 255 | +- **No editor, and no display.** Nothing in this kit can run VS Code or Zed. The | |
| 256 | + Extension Development Host (F5), Zed's *Install Dev Extension*, and any | |
| 257 | + `@vscode/test-electron` run all need a GUI session on the user's machine; there is no X | |
| 258 | + server here and the kit does not install `xvfb`. The sandbox covers compile, unit-test, lint | |
| 259 | + and package — write extensions so their logic is testable without the `vscode` module and | |
| 260 | + the split costs you nothing. | |
| 261 | +- **Tree-sitter grammars in Zed extensions are not reproducible here.** Compiling a parser | |
| 262 | + needs the wasi-sdk, which Zed downloads itself, on the host. The Rust half of such an | |
| 263 | + extension builds fine; the grammar half does not happen in this sandbox. | |
| 264 | +- **The C toolchain is the one unpinned install.** Go, TinyGo, qlty, Node and rustup are all | |
| 265 | + version-and-digest pinned; `gcc` comes from Ubuntu's archives at whatever version they serve | |
| 266 | + (15.2.0 at time of writing). Pinning it would mean pinning a `.deb` set and its transitive | |
| 267 | + closure, which is a much larger commitment than it is worth for a linker driver — but it does | |
| 268 | + mean two sandboxes built months apart can differ here. | |
| new file mode 100644 | |||
| @@ -0,0 +1,268 @@ | |||
| 1 | +# `dev-toolkit` kit | ||
| 2 | + | ||
| 3 | +A Docker Sandboxes **v2 mixin kit** that provisions a general-purpose development toolkit: | ||
| 4 | +a code-quality engine, pinned Go / Rust / Node toolchains, and three Claude Code skills | ||
| 5 | +covering code quality, documentation, and a controlled feature-development workflow. | ||
| 6 | + | ||
| 7 | +| Tool | Version | Installed at | Notes | | ||
| 8 | +| --- | --- | --- | --- | | ||
| 9 | +| qlty | 0.639.0 | `/usr/local/bin/qlty` | Lint + smells + metrics engine, for any language. | | ||
| 10 | +| Go | 1.26.5 | `/usr/local/go` | Symlinked into `/usr/local/bin`, shadowing the template's own `/usr/bin/go`. | | ||
| 11 | +| TinyGo | 0.41.1 | `/usr/local/tinygo` | `TINYGOROOT` set; for `wasm`, `wasip1`, `wasip2` targets. | | ||
| 12 | +| Rust | 1.97.1 | `/usr/local/rustup`, `/usr/local/cargo` | Via rustup, `minimal` profile + `clippy` + `rustfmt`, with the `wasm32-wasip2` target. | | ||
| 13 | +| Node.js | 24.19.0 | `/usr/local/node` | LTS "Krypton". Shadows the template's own `/usr/bin/node` (22.x). | | ||
| 14 | +| pnpm | 11.22.0 | `/usr/local/share/npm-global/bin` | Plus bundled `corepack`, for projects that pin a version via `packageManager`. | | ||
| 15 | +| vsce / ovsx | 3.9.2 / 1.1.1 | `/usr/local/share/npm-global/bin` | Package and publish VS Code extensions to the Marketplace / Open VSX. | | ||
| 16 | +| gcc, g++ | Ubuntu's | `/usr/bin` | The base template has **no** C compiler; Rust and node-gyp both need one. | | ||
| 17 | + | ||
| 18 | +| Skill | Purpose | | ||
| 19 | +| --- | --- | | ||
| 20 | +| `quality` | Configures qlty, measures the project, writes a report, tracks progression, refactors until the gate passes. | | ||
| 21 | +| `diataxis-doc` | Writes or restructures documentation with the Diátaxis four-quadrant method. | | ||
| 22 | +| `methodical-dev` | Orchestrator. Drives a feature through requirements → plan → implementation → quality gate → bilingual docs → project memory, stopping for approval at every step. Invokes the other two. | | ||
| 23 | + | ||
| 24 | + | ||
| 25 | +Plus a `~/.claude/CLAUDE.md`, loaded at the start of every session, which brackets the work at | ||
| 26 | +both ends: **read** the project's committed `.memory/` record — summary, history, latest | ||
| 27 | +handoff — before planning or asking the user anything, and **update all three** before handing | ||
| 28 | +control back, unconditionally, however small the session. `summary.md` is edited in place and | ||
| 29 | +never regenerated from one session's partial view; `history.md` gets exactly one appended | ||
| 30 | +entry; `handoffs/` gets today's file. `methodical-dev` (Phase 7) holds the full templates — | ||
| 31 | +`CLAUDE.md` is what makes the same discipline apply to ad-hoc work that never invokes the skill. | ||
| 32 | + | ||
| 33 | +**Only the toolchain binaries are language-specific.** qlty is not a Go tool: `qlty init` | ||
| 34 | +detects the languages present and enables the matching linters, and all three skills are | ||
| 35 | +language-agnostic. Drop the toolchain install steps from `spec.yaml` and the kit is a pure | ||
| 36 | +quality/docs/workflow mixin for any stack. The install steps are independent of each other, | ||
| 37 | +so a Go-only or Rust-only sandbox is a matter of deleting the ones you do not want — except | ||
| 38 | +the C-toolchain step, which Rust and native npm addons both depend on. | ||
| 39 | + | ||
| 40 | +Project-specific skills do **not** belong here. `builtin-parity`, which describes the | ||
| 41 | +GoloScript repository in particular, lives in the sibling [`golo-toolkit`](../golo-toolkit/) | ||
| 42 | +kit and is composed in only where it applies. | ||
| 43 | + | ||
| 44 | +## Layout | ||
| 45 | + | ||
| 46 | +``` | ||
| 47 | +kits/dev-toolkit/ | ||
| 48 | +├── spec.yaml # schemaVersion "2", kind: mixin | ||
| 49 | +├── README.md | ||
| 50 | +└── files/home/.claude/ | ||
| 51 | + ├── CLAUDE.md # loaded every session: read .memory/ first | ||
| 52 | + └── skills/ | ||
| 53 | + ├── quality/ | ||
| 54 | + │ ├── SKILL.md # the workflow the agent follows | ||
| 55 | + │ └── scripts/quality_report.py # measurement, report, history, gate verdict | ||
| 56 | + ├── diataxis-doc/SKILL.md | ||
| 57 | + └── methodical-dev/SKILL.md | ||
| 58 | +``` | ||
| 59 | + | ||
| 60 | +`files/home/**` is copied into the agent's home at container start, so the skills land | ||
| 61 | +under `~/.claude/skills/` and Claude Code picks them up as personal skills, and `CLAUDE.md` | ||
| 62 | +lands at `~/.claude/CLAUDE.md` — the user-level memory file, prepended to **every** Claude | ||
| 63 | +Code session in the sandbox regardless of which project is open. | ||
| 64 | + | ||
| 65 | +**Why both a `CLAUDE.md` and a skill say to read `.memory/`.** `methodical-dev`'s Phase 0 | ||
| 66 | +only runs when that skill is invoked. `~/.claude/CLAUDE.md` is loaded unconditionally, so an | ||
| 67 | +ad-hoc request ("fix this bug") also starts by reading the project record instead of | ||
| 68 | +re-deriving state that is already written down. Keep the file short — it costs tokens on | ||
| 69 | +every single session; the detail belongs in the skills and in the project's own | ||
| 70 | +`.memory/README.md`. | ||
| 71 | + | ||
| 72 | +Everything in that tree is packed into the kit and shipped into the sandbox, so keep it to | ||
| 73 | +the skills themselves — a stray `__pycache__/` would be shipped alongside them. The | ||
| 74 | +repository `.gitignore` guards against that. | ||
| 75 | + | ||
| 76 | +## Usage | ||
| 77 | + | ||
| 78 | +```bash | ||
| 79 | +# create the sandbox with the kit — `--kit` at create time, not `sbx kit add`, | ||
| 80 | +# so the cache volumes are attached (see Known limitations) | ||
| 81 | +sbx run claude --kit ./kits/dev-toolkit | ||
| 82 | +``` | ||
| 83 | + | ||
| 84 | +Then, in the agent session: *"run a quality report"*, *"document this project with | ||
| 85 | +Diátaxis"*, or *"let's build this feature methodically"*. | ||
| 86 | + | ||
| 87 | +### Editor extensions | ||
| 88 | + | ||
| 89 | +The two ecosystems use different toolchains, and both are provisioned: | ||
| 90 | + | ||
| 91 | +| | VS Code | Zed | | ||
| 92 | +| --- | --- | --- | | ||
| 93 | +| Language | TypeScript / JavaScript on Node | Rust compiled to WebAssembly | | ||
| 94 | +| Manifest | `package.json` (`contributes`, `engines.vscode`) | `extension.toml` + `Cargo.toml` | | ||
| 95 | +| Build | the project's own script (tsc, esbuild) | `cargo build --release --target wasm32-wasip2` | | ||
| 96 | +| Package | `vsce package` → a `.vsix` | Zed's own extension builder, run by the editor | | ||
| 97 | +| Publish | `vsce publish`, `ovsx publish` | a PR to `zed-industries/extensions` | | ||
| 98 | + | ||
| 99 | +`wasm32-wasip2` is preinstalled because that is the target Zed compiles extensions with; | ||
| 100 | +building for anything else produces an artifact it refuses. Both flows were verified inside | ||
| 101 | +the sandbox end to end — `npm install` → `tsc` → `vsce package` → `.vsix`, and | ||
| 102 | +`zed_extension_api` → `cargo build --target wasm32-wasip2` → a WASM **component** (layer | ||
| 103 | +`0100`, not a core module). | ||
| 104 | + | ||
| 105 | +What does **not** work here, by construction: no editor runs in the sandbox, so VS Code's F5 | ||
| 106 | +Extension Development Host and Zed's *Install Dev Extension* are host-side actions, and | ||
| 107 | +`@vscode/test-electron` needs an X server this kit does not install. Compile, unit-test, lint | ||
| 108 | +and package in the sandbox; drive the editor on the host. | ||
| 109 | + | ||
| 110 | +## Design notes | ||
| 111 | + | ||
| 112 | +**Why a mixin and not a `kind: sandbox` kit.** The kit adds tooling to whatever agent you | ||
| 113 | +are already using rather than defining its own image, so it composes: `--kit` it onto | ||
| 114 | +`claude`, `codex`, `shell`, or anything else. Exactly one `sandbox` kit is allowed per | ||
| 115 | +composition; mixins stack freely. | ||
| 116 | + | ||
| 117 | +**Why pinned tarballs instead of the official installers.** `curl https://qlty.sh | sh` | ||
| 118 | +resolves `latest` (so two sandboxes built a day apart differ), POSTs an install event to | ||
| 119 | +`cdp.customer.io`, and rewrites the user's shell rc files. Every artifact here is pinned | ||
| 120 | +by version **and** SHA256, verified with `sha256sum -c` before it is unpacked — the same | ||
| 121 | +pattern the official `trivy`, `vale`, and `mise` kits use. qlty's own published | ||
| 122 | +`.sha256` files match the digests in `spec.yaml`. | ||
| 123 | + | ||
| 124 | +**Why Python unpacks the qlty archive.** qlty ships only `.tar.xz` and the sandbox | ||
| 125 | +template has no `xz` binary. Installing `xz-utils` would mean running `apt-get update`, | ||
| 126 | +which re-fetches *every* configured apt source — including the template's | ||
| 127 | +`download.docker.com` — and fails if any one of them is not allow-listed. `python3` is | ||
| 128 | +present, and `lzma` is in its standard library, so the archive is decompressed with no | ||
| 129 | +extra egress at all. | ||
| 130 | + | ||
| 131 | +**Toolchain pairing is load-bearing.** TinyGo 0.41.1 accepts Go 1.19–1.26 *and* refuses a | ||
| 132 | +GOROOT newer than the Go it was itself built with (1.26.2). Go 1.26.5 satisfies both | ||
| 133 | +bounds. Bumping Go to 1.27 breaks TinyGo — bump the two together. `GOTOOLCHAIN=local` | ||
| 134 | +prevents Go from silently downloading a different toolchain behind your back. | ||
| 135 | + | ||
| 136 | +**Why a C compiler is a hard requirement, not a nicety.** The base template ships `make` | ||
| 137 | +but no `cc`, `gcc`, `clang`, `ld` or libc headers. That produces a genuinely misleading | ||
| 138 | +failure: `cargo build --target wasm32-wasip2` **succeeds** without a C compiler, because | ||
| 139 | +rustc links wasm targets with its own bundled `rust-lld` — while `cargo test`, which builds | ||
| 140 | +for the host, dies with ``error: linker `cc` not found``. A Zed extension therefore appears | ||
| 141 | +to build fine and cannot be unit-tested, which is the worst possible shape for a missing | ||
| 142 | +dependency. The same gap breaks any crate with a C-compiling `build.rs`, and any npm package | ||
| 143 | +with a native addon built through node-gyp. `apt-get update` is deliberately *not* run: the | ||
| 144 | +template ships populated `/var/lib/apt/lists`, so a plain `apt-get install` works, whereas | ||
| 145 | +`update` re-fetches every configured source (Docker's included) and fails if one is not | ||
| 146 | +allow-listed. It stays only as a fallback for the day those lists go stale. | ||
| 147 | + | ||
| 148 | +**Why rustup rather than the standalone Rust tarballs.** A pinned `rustup-init` *binary* | ||
| 149 | +(digest-verified, not the `curl https://sh.rustup.rs | sh` bootstrapper, which resolves | ||
| 150 | +`latest` and edits shell rc files) buys a working `rustup target add` — which matters because | ||
| 151 | +Zed itself shells out to exactly that when an extension needs a target that is not installed. | ||
| 152 | +Rust lives under `/usr/local` rather than in the home directory, for the same reason Go does: | ||
| 153 | +the install runs once at container creation, and only paths baked into the image survive a | ||
| 154 | +restart. Both `RUSTUP_HOME` and `CARGO_HOME` are left world-writable — the layout the | ||
| 155 | +official `rust` Docker image uses — so `rustup target add`, `rustup component add` and | ||
| 156 | +`cargo install` work as the agent user without sudo. | ||
| 157 | + | ||
| 158 | +**Why the kit does *not* set npm's prefix.** The obvious move — `npm_config_prefix=/usr/local`, | ||
| 159 | +so global installs land in `/usr/local/bin` — is wrong here. The base template already exports | ||
| 160 | +`NPM_CONFIG_PREFIX=/usr/local/share/npm-global`, and that directory is **agent-owned and | ||
| 161 | +already on PATH ahead of `/usr/local/bin`**. Overriding it would move global installs into a | ||
| 162 | +root-owned tree and break `npm install -g` for the agent user, which currently works with no | ||
| 163 | +sudo. The install step passes `--prefix` explicitly (rather than trusting the variable to be | ||
| 164 | +exported into a root-run step) and then `chown`s the tree back to the agent. | ||
| 165 | + | ||
| 166 | +**`--allow-scripts` is not optional for vsce.** npm 11 skips unrecognised lifecycle scripts | ||
| 167 | +and merely warns. Two of vsce's dependencies need theirs: `keytar` builds the native addon | ||
| 168 | +that stores a publishing PAT, and `@vscode/vsce-sign` unpacks the signing binary. Skipped, | ||
| 169 | +they fail at `vsce publish` time — long after the install that caused it. | ||
| 170 | + | ||
| 171 | +**The cache volumes.** Three, all of them things that are otherwise re-downloaded on a cold | ||
| 172 | +start: `~/.qlty` (qlty provisions a JVM for `radarlint-go`, Node, and each linter binary on | ||
| 173 | +demand), `/usr/local/cargo/registry` (Cargo's `.crate` files and sparse index) and `~/.npm`. | ||
| 174 | +The Cargo one is deliberately the `registry` subdirectory rather than all of `CARGO_HOME`: | ||
| 175 | +mounting an empty volume over `CARGO_HOME` would shadow the rustup shims in its `bin`. Volumes | ||
| 176 | +are fixed at container creation, so `sbx kit add` warns and skips them — use `--kit` at create | ||
| 177 | +time to get the caches. | ||
| 178 | + | ||
| 179 | +## Maintenance | ||
| 180 | + | ||
| 181 | +**Bumping a version.** Change the version *and* both per-arch `SHA256` values in the | ||
| 182 | +matching `setup.install` entry in `spec.yaml`. Sources: | ||
| 183 | + | ||
| 184 | +- Go — <https://go.dev/dl/?mode=json> (`files[].sha256`) | ||
| 185 | +- TinyGo — compute from the release tarball; the project publishes no checksums file | ||
| 186 | +- qlty — the `<asset>.tar.xz.sha256` file next to each release asset | ||
| 187 | +- Node — `https://nodejs.org/dist/v<version>/SHASUMS256.txt`. Pick from the **active LTS** | ||
| 188 | + line; `https://nodejs.org/dist/index.json` marks it with a non-`false` `lts` field. Note the | ||
| 189 | + `.tar.gz` digest, not `.tar.xz`: the template has no `xz`, and unlike qlty, Node publishes | ||
| 190 | + both — which is why this step needs no python-lzma workaround | ||
| 191 | +- rustup — `https://static.rust-lang.org/rustup/archive/<ver>/<triple>/rustup-init.sha256`, | ||
| 192 | + with the current version at `https://static.rust-lang.org/rustup/release-stable.toml` | ||
| 193 | +- Rust itself — no digest to update; rustup resolves and verifies the toolchain. The current | ||
| 194 | + stable version is the `[pkg.rust] version` in | ||
| 195 | + `https://static.rust-lang.org/dist/channel-rust-stable.toml` | ||
| 196 | +- vsce / ovsx / pnpm — `npm view <pkg> version` | ||
| 197 | + | ||
| 198 | +**Before bumping the Zed target.** `wasm32-wasip2` is not a preference — it is read from Zed's | ||
| 199 | +own extension builder (`RUST_TARGET` in `crates/extension/src/extension_builder.rs`). Confirm | ||
| 200 | +there before changing it; an extension built for another target is one Zed refuses to load. | ||
| 201 | + | ||
| 202 | +**Validating a change.** | ||
| 203 | + | ||
| 204 | +```bash | ||
| 205 | +sbx kit validate ./kits/dev-toolkit | ||
| 206 | +sbx kit inspect ./kits/dev-toolkit --json | jq '.warnings' # expect null or [] | ||
| 207 | +``` | ||
| 208 | + | ||
| 209 | +Empty warnings is the green light — a non-empty list means a v1 surface crept in that | ||
| 210 | +will stop loading at the spec's Phase 6 cutover. | ||
| 211 | + | ||
| 212 | +`Install commands completed` only means the commands exited `0`. Verify the outcome: | ||
| 213 | + | ||
| 214 | +```bash | ||
| 215 | +sbx exec <sandbox> -- sh -lc ' | ||
| 216 | + go version && tinygo version && qlty --version && | ||
| 217 | + rustc --version && cargo --version && rustup target list --installed && | ||
| 218 | + node --version && npm --version && pnpm --version && vsce --version && ovsx --version && | ||
| 219 | + cc --version | head -1' | ||
| 220 | +``` | ||
| 221 | + | ||
| 222 | +And verify the thing versions cannot tell you — that host-target Rust actually **links**, | ||
| 223 | +which is what the C-toolchain step exists for: | ||
| 224 | + | ||
| 225 | +```bash | ||
| 226 | +sbx exec <sandbox> -- sh -lc ' | ||
| 227 | + cd "$(mktemp -d)" && cargo init --lib -q . && cargo test 2>&1 | tail -3' | ||
| 228 | +``` | ||
| 229 | + | ||
| 230 | +**Extending the network allowlist.** `permissions.network.allow` covers the install downloads, | ||
| 231 | +qlty's plugin/linter resolution, the Go module proxy, Cargo's three hosts (the sparse index | ||
| 232 | +`index.crates.io`, the CDN `static.crates.io`, and the API `crates.io` — all three are needed), | ||
| 233 | +Ubuntu's archives for the C toolchain, and the VS Code Marketplace / Open VSX publishing paths. | ||
| 234 | +A linter for another language will need its registry added. Find out what is missing rather | ||
| 235 | +than guessing: | ||
| 236 | + | ||
| 237 | +```bash | ||
| 238 | +sbx policy log <sandbox> | ||
| 239 | +``` | ||
| 240 | + | ||
| 241 | +That prints what the proxy actually allowed and blocked. Add blocked hosts one at a time. | ||
| 242 | + | ||
| 243 | +## Known limitations | ||
| 244 | + | ||
| 245 | +- **`sbx kit add` does not write mixin context.** The engine gates the kit-memory write on | ||
| 246 | + the artifact's own AI-profile filename, which mixins deliberately do not carry (`agentInstructions.filename` is ignored for a mixin), so adding this | ||
| 247 | + kit at runtime silently skips `kits-memory/dev-toolkit.md`. Create the sandbox with | ||
| 248 | + `--kit` instead. The same applies to the `~/.qlty` volume. | ||
| 249 | +- **The skills and `CLAUDE.md` are Claude Code-specific.** The toolchain is not. The kit sets | ||
| 250 | + no `requires.agent`, so it works with any agent; on non-Claude agents those files are inert | ||
| 251 | + and `agentInstructions` carries the same instructions — the `.memory/`-first rule and the | ||
| 252 | + `quality` script invocation. | ||
| 253 | +- **Only `linux/amd64` and `linux/arm64`** are supported. Other architectures fail the | ||
| 254 | + install with an explicit error rather than installing something wrong. | ||
| 255 | +- **No editor, and no display.** Nothing in this kit can run VS Code or Zed. The | ||
| 256 | + Extension Development Host (F5), Zed's *Install Dev Extension*, and any | ||
| 257 | + `@vscode/test-electron` run all need a GUI session on the user's machine; there is no X | ||
| 258 | + server here and the kit does not install `xvfb`. The sandbox covers compile, unit-test, lint | ||
| 259 | + and package — write extensions so their logic is testable without the `vscode` module and | ||
| 260 | + the split costs you nothing. | ||
| 261 | +- **Tree-sitter grammars in Zed extensions are not reproducible here.** Compiling a parser | ||
| 262 | + needs the wasi-sdk, which Zed downloads itself, on the host. The Rust half of such an | ||
| 263 | + extension builds fine; the grammar half does not happen in this sandbox. | ||
| 264 | +- **The C toolchain is the one unpinned install.** Go, TinyGo, qlty, Node and rustup are all | ||
| 265 | + version-and-digest pinned; `gcc` comes from Ubuntu's archives at whatever version they serve | ||
| 266 | + (15.2.0 at time of writing). Pinning it would mean pinning a `.deb` set and its transitive | ||
| 267 | + closure, which is a much larger commitment than it is worth for a linker driver — but it does | ||
| 268 | + mean two sandboxes built months apart can differ here. | ||
added
kits/dev-toolkit/spec.yaml +603 -0 | new file mode 100644 | ||
| @@ -0,0 +1,603 @@ | ||
| 1 | +schemaVersion: "2" | |
| 2 | +kind: mixin | |
| 3 | +name: dev-toolkit | |
| 4 | +displayName: Development toolkit (Go + Rust + Node + qlty + skills) | |
| 5 | +description: > | |
| 6 | + A general-purpose development toolkit. Installs the qlty 0.639.0 code-quality | |
| 7 | + engine, which analyses whatever languages a project actually uses, plus three | |
| 8 | + pinned language toolchains: Go 1.26.5 with TinyGo 0.41.1 for WebAssembly / WASI | |
| 9 | + builds, Rust 1.97.1 via rustup with the `wasm32-wasip2` target (what Zed | |
| 10 | + compiles extensions to), and Node.js 24.19.0 with npm, pnpm and the | |
| 11 | + `@vscode/vsce` / `ovsx` extension packagers. Ships four Claude Code skills: | |
| 12 | + `quality` (self-configures qlty, measures the project, writes a Markdown report, | |
| 13 | + tracks progression across runs, and drives refactoring until the quality gate | |
| 14 | + passes), `diataxis-doc` (documents a project with the Diátaxis four-quadrant | |
| 15 | + method), `methodical-dev` (step-by-step, approval-gated feature development | |
| 16 | + that orchestrates the other two, and keeps a committed `.memory/` project | |
| 17 | + record), plus a `~/.claude/CLAUDE.md` that makes every session start by reading | |
| 18 | + that record, and `turbo-new-editor` (builds a complete terminal IDE for a new | |
| 19 | + language on top of the `turbo-core` library, inside `methodical-dev`). Only the | |
| 20 | + toolchain binaries are language-specific; the quality, documentation and | |
| 21 | + workflow skills are not. | |
| 22 | +licenses: | |
| 23 | + - MIT | |
| 24 | + | |
| 25 | +# Deliberately no `requires.agent`: the toolchain is agent-agnostic. The skills | |
| 26 | +# under files/home/.claude/skills/ and files/home/.claude/CLAUDE.md are | |
| 27 | +# Claude-Code-specific and are simply inert on other agents, which still get the | |
| 28 | +# quality workflow and the `.memory/` convention through `agentInstructions` below. | |
| 29 | + | |
| 30 | +permissions: | |
| 31 | + network: | |
| 32 | + allow: | |
| 33 | + # ---- install-time, one-shot ------------------------------------------- | |
| 34 | + # Go toolchain tarball. dl.google.com is the canonical host go.dev/dl | |
| 35 | + # redirects to; hitting it directly keeps the allowlist to one entry. | |
| 36 | + - dl.google.com | |
| 37 | + # TinyGo release tarball. github.com 302-redirects binary downloads to | |
| 38 | + # release-assets.githubusercontent.com (covered by the wildcard below). | |
| 39 | + - github.com | |
| 40 | + # Matches exactly one DNS label, so this single entry covers | |
| 41 | + # release-assets. / objects. / raw.githubusercontent.com. | |
| 42 | + - "*.githubusercontent.com" | |
| 43 | + # qlty CLI tarball (and `qlty upgrade`, if ever run by hand). | |
| 44 | + - qlty-releases.s3.amazonaws.com | |
| 45 | + # The C/C++ toolchain comes from Ubuntu's own archives. ports.* serves | |
| 46 | + # arm64, archive.*/security.* serve amd64 — which one is used depends on the | |
| 47 | + # sandbox architecture, so all three are listed. | |
| 48 | + - ports.ubuntu.com | |
| 49 | + - archive.ubuntu.com | |
| 50 | + - security.ubuntu.com | |
| 51 | + # Only reached if the cached apt lists ever go stale and the fallback | |
| 52 | + # `apt-get update` runs: it re-fetches *every* configured source, and the | |
| 53 | + # template configures Docker's as well as Ubuntu's. | |
| 54 | + - download.docker.com | |
| 55 | + # `rustup-init`, plus every toolchain component rustup resolves. One host | |
| 56 | + # serves both the rustup releases and the dist channel, so this covers the | |
| 57 | + # install *and* later `rustup target add` / `rustup component add`. | |
| 58 | + - static.rust-lang.org | |
| 59 | + # The Node.js tarball comes from nodejs.org, already listed under "qlty | |
| 60 | + # runtime" below — qlty provisions its own Node for the JS/TS linters, so the | |
| 61 | + # host is needed either way. | |
| 62 | + | |
| 63 | + # ---- qlty runtime ------------------------------------------------------ | |
| 64 | + # qlty resolves its default plugin source (qltysh/qlty-plugins) and every | |
| 65 | + # linter release through the GitHub API, its own GitHub proxy, and | |
| 66 | + # codeload for repository tarballs. | |
| 67 | + - api.github.com | |
| 68 | + - github-proxy.qlty.sh | |
| 69 | + - codeload.github.com | |
| 70 | + # radarlint-go — enabled by `qlty init` on Go repos; ships from its own | |
| 71 | + # bucket and pulls a Temurin JVM from GitHub releases. | |
| 72 | + - radarlint-releases.s3.amazonaws.com | |
| 73 | + # osv-scanner — also enabled by `qlty init` on Go repos. | |
| 74 | + - api.osv.dev | |
| 75 | + - osv-vulnerabilities.storage.googleapis.com | |
| 76 | + # Language runtimes qlty provisions on demand for non-Go linters. Trim | |
| 77 | + # these if you only ever lint Go — except nodejs.org, which this kit's own | |
| 78 | + # Node install step also needs. | |
| 79 | + - nodejs.org | |
| 80 | + - registry.npmjs.org | |
| 81 | + - pypi.org | |
| 82 | + - files.pythonhosted.org | |
| 83 | + | |
| 84 | + # ---- Go module resolution --------------------------------------------- | |
| 85 | + - proxy.golang.org | |
| 86 | + - sum.golang.org | |
| 87 | + | |
| 88 | + # ---- Cargo dependency resolution -------------------------------------- | |
| 89 | + # Three distinct hosts, all needed: the sparse index cargo reads by | |
| 90 | + # default, the CDN the .crate files come from, and the API `cargo add` / | |
| 91 | + # `cargo search` / `cargo publish` talk to. Git dependencies go through | |
| 92 | + # github.com, already allowed above. | |
| 93 | + - index.crates.io | |
| 94 | + - static.crates.io | |
| 95 | + - crates.io | |
| 96 | + | |
| 97 | + # ---- VS Code extension tooling ---------------------------------------- | |
| 98 | + # `vsce publish` / `vsce ls-publishers`, and the gallery query API that | |
| 99 | + # `@vscode/test-electron` uses to resolve extension dependencies. | |
| 100 | + - marketplace.visualstudio.com | |
| 101 | + # Extension asset (.vsix) download. Each publisher gets its own subdomain, | |
| 102 | + # and the wildcard matches exactly one DNS label, so both forms are needed. | |
| 103 | + - "*.gallery.vsassets.io" | |
| 104 | + - "*.gallerycdn.vsassets.io" | |
| 105 | + # `@vscode/test-electron` asks this for the build to test against, then | |
| 106 | + # downloads the archive from Microsoft's CDN. | |
| 107 | + - update.code.visualstudio.com | |
| 108 | + - vscode.download.prss.microsoft.com | |
| 109 | + # `ovsx publish` — the Open VSX registry, which is where a VS Code | |
| 110 | + # extension has to go to be installable in VSCodium, Cursor or Zed. | |
| 111 | + - open-vsx.org | |
| 112 | + | |
| 113 | +environment: | |
| 114 | + variables: | |
| 115 | + # Pin the toolchain we actually installed. Without this, Go 1.21+ silently | |
| 116 | + # downloads whatever `go`/`toolchain` line go.mod names, which would defeat | |
| 117 | + # the point of pinning 1.26.5 (and needs egress we haven't allowed). If a | |
| 118 | + # go.mod legitimately requires something newer, this surfaces as a clear | |
| 119 | + # error instead of a surprise download. | |
| 120 | + GOTOOLCHAIN: local | |
| 121 | + # TinyGo resolves its root from the executable path, which already works for | |
| 122 | + # the tarball layout. Setting it explicitly wins over any value baked in at | |
| 123 | + # release-build time, so `tinygo` keeps working when invoked via the | |
| 124 | + # /usr/local/bin symlink. | |
| 125 | + TINYGOROOT: /usr/local/tinygo | |
| 126 | + # qlty phones home to cdp.customer.io on every command otherwise. That host | |
| 127 | + # is not allow-listed above, so the calls would fail anyway — this turns | |
| 128 | + # them off at the source instead of relying on the firewall. | |
| 129 | + QLTY_TELEMETRY: "off" | |
| 130 | + # Rust lives under /usr/local rather than in the agent's home, for the same | |
| 131 | + # reason Go and TinyGo do: the install runs once at container creation, and | |
| 132 | + # only paths baked into the image are guaranteed to survive a restart. Both | |
| 133 | + # directories are made world-writable by the install step, so `rustup target | |
| 134 | + # add`, `rustup component add` and `cargo install` all work as the agent | |
| 135 | + # user — this is the layout the official `rust` Docker image uses. | |
| 136 | + RUSTUP_HOME: /usr/local/rustup | |
| 137 | + CARGO_HOME: /usr/local/cargo | |
| 138 | + # Deliberately NOT setting npm's prefix here. The base template already | |
| 139 | + # exports NPM_CONFIG_PREFIX=/usr/local/share/npm-global, and that directory is | |
| 140 | + # agent-owned and already on PATH ahead of /usr/local/bin — so `npm install -g` | |
| 141 | + # works for the agent user with no sudo. Overriding it to /usr/local would | |
| 142 | + # move global installs into a root-owned tree and break exactly that. The | |
| 143 | + # install step below installs its pinned CLIs into the template's prefix and | |
| 144 | + # hands ownership back to the agent. | |
| 145 | + | |
| 146 | +# Package caches. Each of these is repopulated over the network on a cold start, | |
| 147 | +# so persisting them across container restarts turns minutes into a no-op. | |
| 148 | +# Volumes are fixed at container creation: `sbx kit add` warns and skips them, so | |
| 149 | +# use `--kit` at create time to get the caches. | |
| 150 | +volumes: | |
| 151 | + # qlty downloads a JVM (for radarlint), Node, and every linter binary into | |
| 152 | + # ~/.qlty/cache. | |
| 153 | + - path: /home/agent/.qlty | |
| 154 | + # Cargo's downloaded .crate files and its copy of the sparse index. | |
| 155 | + # Deliberately the `registry` subdirectory rather than all of CARGO_HOME: | |
| 156 | + # mounting an empty volume over CARGO_HOME would shadow the rustup shims that | |
| 157 | + # the install step puts in its `bin`. | |
| 158 | + - path: /usr/local/cargo/registry | |
| 159 | + # npm's content-addressable cache (`npm config get cache`). | |
| 160 | + - path: /home/agent/.npm | |
| 161 | + | |
| 162 | +setup: | |
| 163 | + install: | |
| 164 | + # Go 1.26.5, version + digest pinned. The base template already ships a | |
| 165 | + # /usr/bin/go (1.26.0 at time of writing); /usr/local/bin precedes /usr/bin | |
| 166 | + # on PATH, so the symlinks below shadow it without removing anything. | |
| 167 | + # To bump: change GO_VERSION and both SHA256 values (from | |
| 168 | + # https://go.dev/dl/?mode=json). | |
| 169 | + - command: | | |
| 170 | + set -eu | |
| 171 | + GO_VERSION=1.26.5 | |
| 172 | + ARCH=$(dpkg --print-architecture) | |
| 173 | + case "$ARCH" in | |
| 174 | + amd64) SHA256=5c2c3b16caefa1d968a94c1daca04a7ca301a496d9b086e17ad77bb81393f053 ;; | |
| 175 | + arm64) SHA256=fe4789e92b1f33358680864bbe8704289e7bb5fc207d80623c308935bd696d49 ;; | |
| 176 | + *) echo "unsupported sandbox arch: $ARCH (expected amd64 or arm64)" >&2; exit 1 ;; | |
| 177 | + esac | |
| 178 | + if [ "$(/usr/local/go/bin/go version 2>/dev/null | awk '{print $3}')" != "go${GO_VERSION}" ]; then | |
| 179 | + URL="https://dl.google.com/go/go${GO_VERSION}.linux-${ARCH}.tar.gz" | |
| 180 | + curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/go.tgz "$URL" | |
| 181 | + echo "${SHA256} /tmp/go.tgz" | sha256sum -c - | |
| 182 | + rm -rf /usr/local/go | |
| 183 | + tar -C /usr/local -xzf /tmp/go.tgz | |
| 184 | + rm -f /tmp/go.tgz | |
| 185 | + fi | |
| 186 | + ln -sf /usr/local/go/bin/go /usr/local/bin/go | |
| 187 | + ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt | |
| 188 | + /usr/local/bin/go version | |
| 189 | + user: "0" | |
| 190 | + description: "Install Go 1.26.5 (version + digest pinned), shadowing the image's Go" | |
| 191 | + | |
| 192 | + # TinyGo 0.41.1. Its supported range is Go 1.19–1.26 and the published | |
| 193 | + # binary is itself built with go1.26.2, so it accepts the Go 1.26.5 above | |
| 194 | + # (TinyGo also refuses a GOROOT newer than its own build toolchain). | |
| 195 | + # Bumping Go to 1.27 would break this pairing — bump TinyGo together with it. | |
| 196 | + - command: | | |
| 197 | + set -eu | |
| 198 | + TINYGO_VERSION=0.41.1 | |
| 199 | + ARCH=$(dpkg --print-architecture) | |
| 200 | + case "$ARCH" in | |
| 201 | + amd64) SHA256=e156d1d93a376eef639a4143d13be07e8c463fb6cf2d7d447698ed4474d23e91 ;; | |
| 202 | + arm64) SHA256=789733bc3b5bace0bd1835a267b3ea267804a7ef1cfe69bc522c295f5226d624 ;; | |
| 203 | + *) echo "unsupported sandbox arch: $ARCH (expected amd64 or arm64)" >&2; exit 1 ;; | |
| 204 | + esac | |
| 205 | + if [ "$(/usr/local/tinygo/bin/tinygo version 2>/dev/null | awk '{print $3}')" != "$TINYGO_VERSION" ]; then | |
| 206 | + TARBALL="tinygo${TINYGO_VERSION}.linux-${ARCH}.tar.gz" | |
| 207 | + URL="https://github.com/tinygo-org/tinygo/releases/download/v${TINYGO_VERSION}/${TARBALL}" | |
| 208 | + curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/tinygo.tgz "$URL" | |
| 209 | + echo "${SHA256} /tmp/tinygo.tgz" | sha256sum -c - | |
| 210 | + rm -rf /usr/local/tinygo | |
| 211 | + tar -C /usr/local -xzf /tmp/tinygo.tgz | |
| 212 | + rm -f /tmp/tinygo.tgz | |
| 213 | + fi | |
| 214 | + ln -sf /usr/local/tinygo/bin/tinygo /usr/local/bin/tinygo | |
| 215 | + /usr/local/bin/tinygo version | |
| 216 | + user: "0" | |
| 217 | + description: "Install TinyGo 0.41.1 (version + digest pinned)" | |
| 218 | + | |
| 219 | + # qlty 0.639.0, pulled straight from the release bucket rather than through | |
| 220 | + # `curl https://qlty.sh | sh`. The install script resolves "latest" (not | |
| 221 | + # reproducible), POSTs an install event to cdp.customer.io, and rewrites the | |
| 222 | + # user's shell rc files — none of which belongs in a sandbox image. | |
| 223 | + # The asset is .tar.xz and the base image has no `xz`, so fall back to | |
| 224 | + # Python's lzma (stdlib, and python3 is present) instead of pulling xz-utils | |
| 225 | + # over apt. This step deliberately stays apt-free: it runs *before* the | |
| 226 | + # C-toolchain step below, so it cannot assume anything about apt having been | |
| 227 | + # exercised yet, and a self-contained step is one less ordering dependency. | |
| 228 | + # To bump: change QLTY_VERSION and both SHA256 values (published as | |
| 229 | + # <asset>.sha256 next to each release asset). | |
| 230 | + - command: | | |
| 231 | + set -eu | |
| 232 | + QLTY_VERSION=0.639.0 | |
| 233 | + ARCH=$(dpkg --print-architecture) | |
| 234 | + case "$ARCH" in | |
| 235 | + amd64) | |
| 236 | + TARGET=x86_64-unknown-linux-gnu | |
| 237 | + SHA256=49f8299d25a1c50cf02af8263483b1a04b8b665d71c5b6512847e08ccb392a90 | |
| 238 | + ;; | |
| 239 | + arm64) | |
| 240 | + TARGET=aarch64-unknown-linux-gnu | |
| 241 | + SHA256=8f2f5cd7eb06c3f1ba106866bbf80cfb563a06d70ee495c14391dced37d05a4e | |
| 242 | + ;; | |
| 243 | + *) echo "unsupported sandbox arch: $ARCH (expected amd64 or arm64)" >&2; exit 1 ;; | |
| 244 | + esac | |
| 245 | + if [ "$(/usr/local/bin/qlty --version 2>/dev/null | awk '{print $2}')" != "$QLTY_VERSION" ]; then | |
| 246 | + URL="https://qlty-releases.s3.amazonaws.com/qlty/v${QLTY_VERSION}/qlty-${TARGET}.tar.xz" | |
| 247 | + curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/qlty.tar.xz "$URL" | |
| 248 | + echo "${SHA256} /tmp/qlty.tar.xz" | sha256sum -c - | |
| 249 | + rm -rf "/tmp/qlty-${TARGET}" | |
| 250 | + if command -v xz >/dev/null 2>&1; then | |
| 251 | + tar -C /tmp -xJf /tmp/qlty.tar.xz "qlty-${TARGET}/qlty" | |
| 252 | + else | |
| 253 | + python3 -c 'import lzma,sys; sys.stdout.buffer.write(lzma.open(sys.argv[1],"rb").read())' \ | |
| 254 | + /tmp/qlty.tar.xz | tar -C /tmp -xf - "qlty-${TARGET}/qlty" | |
| 255 | + fi | |
| 256 | + install -m 0755 "/tmp/qlty-${TARGET}/qlty" /usr/local/bin/qlty | |
| 257 | + rm -rf /tmp/qlty.tar.xz "/tmp/qlty-${TARGET}" | |
| 258 | + fi | |
| 259 | + /usr/local/bin/qlty --version | |
| 260 | + user: "0" | |
| 261 | + description: "Install qlty CLI 0.639.0 (version + digest pinned, no install script)" | |
| 262 | + | |
| 263 | + # A C/C++ toolchain. Not optional, and not obvious: the base template ships | |
| 264 | + # `make` but **no `cc`, `gcc`, `clang`, `ld` or libc headers at all**. Without | |
| 265 | + # this step: | |
| 266 | + # - `cargo build` / `cargo test` for the host target fail with | |
| 267 | + # "error: linker `cc` not found" (measured). Cross-compiling to | |
| 268 | + # wasm32-wasip2 still works, because rustc uses its own bundled rust-lld | |
| 269 | + # for wasm targets — so a Zed extension builds but cannot be unit-tested. | |
| 270 | + # - any crate with a `build.rs` that compiles C, and any npm package with a | |
| 271 | + # native addon built through node-gyp, fails the same way. | |
| 272 | + # `apt-get update` is deliberately NOT run: the template ships populated | |
| 273 | + # /var/lib/apt/lists, so a plain install works, and `update` would re-fetch | |
| 274 | + # every configured source (including download.docker.com) and fail if any one | |
| 275 | + # of them is not allow-listed. It is kept only as a fallback for the day those | |
| 276 | + # cached lists go stale. | |
| 277 | + - command: | | |
| 278 | + set -eu | |
| 279 | + if ! command -v cc >/dev/null 2>&1; then | |
| 280 | + export DEBIAN_FRONTEND=noninteractive | |
| 281 | + PKGS="gcc g++ libc6-dev pkg-config" | |
| 282 | + # `-o DPkg::Lock::Timeout` is not optional here. The claude kit registers | |
| 283 | + # a *backgrounded* `apt-get update` as a startup command, so at the moment | |
| 284 | + # this step runs (~11s into the install sequence) that update may still | |
| 285 | + # hold the dpkg frontend lock. Without the timeout apt does not wait — it | |
| 286 | + # gives up at once with exit 100, and the whole kit apply fails. | |
| 287 | + APT="apt-get -o DPkg::Lock::Timeout=300" | |
| 288 | + $APT install -y --no-install-recommends $PKGS \ | |
| 289 | + || { $APT update && $APT install -y --no-install-recommends $PKGS; } | |
| 290 | + fi | |
| 291 | + cc --version | head -1 | |
| 292 | + c++ --version | head -1 | |
| 293 | + user: "0" | |
| 294 | + description: "Install a C/C++ toolchain (gcc, g++, libc6-dev, pkg-config) — required to link host-target Rust builds and native npm addons" | |
| 295 | + | |
| 296 | + # Node.js 24.19.0 — "Krypton", the active LTS line. The base template ships a | |
| 297 | + # /usr/bin/node (22.22.1 at time of writing); as with Go, /usr/local/bin | |
| 298 | + # precedes /usr/bin on PATH, so the symlinks shadow it without removing it. | |
| 299 | + # The .tar.gz is used rather than the (smaller) .tar.xz on purpose: the base | |
| 300 | + # image has no `xz`, and unlike qlty, Node publishes both formats — so this | |
| 301 | + # avoids the python-lzma workaround the qlty step needs. | |
| 302 | + # To bump: change NODE_VERSION and both SHA256 values, from | |
| 303 | + # https://nodejs.org/dist/v<version>/SHASUMS256.txt. | |
| 304 | + - command: | | |
| 305 | + set -eu | |
| 306 | + NODE_VERSION=24.19.0 | |
| 307 | + ARCH=$(dpkg --print-architecture) | |
| 308 | + case "$ARCH" in | |
| 309 | + amd64) NODE_ARCH=x64; SHA256=f625d97cd707df4ff96254916fbc5ff014f09c09effe5a1e0ca8f6d41a8789d4 ;; | |
| 310 | + arm64) NODE_ARCH=arm64; SHA256=d28c8a5bf0a808f0ed434a1dce8c54ae98f0371c0bd86ac58abc613f73e6643f ;; | |
| 311 | + *) echo "unsupported sandbox arch: $ARCH (expected amd64 or arm64)" >&2; exit 1 ;; | |
| 312 | + esac | |
| 313 | + if [ "$(/usr/local/node/bin/node --version 2>/dev/null)" != "v${NODE_VERSION}" ]; then | |
| 314 | + TARBALL="node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz" | |
| 315 | + URL="https://nodejs.org/dist/v${NODE_VERSION}/${TARBALL}" | |
| 316 | + curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/node.tgz "$URL" | |
| 317 | + echo "${SHA256} /tmp/node.tgz" | sha256sum -c - | |
| 318 | + rm -rf /usr/local/node | |
| 319 | + mkdir -p /usr/local/node | |
| 320 | + tar -C /usr/local/node --strip-components=1 -xzf /tmp/node.tgz | |
| 321 | + rm -f /tmp/node.tgz | |
| 322 | + fi | |
| 323 | + for b in node npm npx corepack; do | |
| 324 | + ln -sf "/usr/local/node/bin/$b" "/usr/local/bin/$b" | |
| 325 | + done | |
| 326 | + # Global installs go to the template's own prefix, which is already on | |
| 327 | + # PATH. --prefix is passed explicitly rather than trusting NPM_CONFIG_PREFIX | |
| 328 | + # to be exported into this root-run step. | |
| 329 | + NPM_PREFIX=/usr/local/share/npm-global | |
| 330 | + mkdir -p "$NPM_PREFIX" | |
| 331 | + # Pinned, and deliberately only the tools that belong to the *sandbox* | |
| 332 | + # rather than to a project: the two extension packagers, and pnpm. A | |
| 333 | + # project's TypeScript, esbuild and test runner belong in its own | |
| 334 | + # devDependencies, where the version is committed alongside the code. | |
| 335 | + # | |
| 336 | + # --allow-scripts is required by npm 11: it otherwise skips these two | |
| 337 | + # lifecycle scripts and only warns. keytar builds the native addon vsce | |
| 338 | + # uses to store a publishing PAT, and vsce-sign unpacks the signing | |
| 339 | + # binary — both are needed by `vsce publish`, and both fail silently | |
| 340 | + # (at publish time, not now) if their scripts never run. | |
| 341 | + /usr/local/bin/npm install -g --prefix "$NPM_PREFIX" --no-fund --no-audit \ | |
| 342 | + --allow-scripts=@vscode/vsce-sign,keytar \ | |
| 343 | + @vscode/vsce@3.9.2 \ | |
| 344 | + ovsx@1.1.1 \ | |
| 345 | + pnpm@11.22.0 | |
| 346 | + # Root just wrote into an agent-owned tree; hand it back so the agent can | |
| 347 | + # install or update globals later without sudo. | |
| 348 | + chown -R agent:agent "$NPM_PREFIX" | |
| 349 | + /usr/local/bin/node --version | |
| 350 | + /usr/local/bin/npm --version | |
| 351 | + "$NPM_PREFIX/bin/vsce" --version | |
| 352 | + user: "0" | |
| 353 | + description: "Install Node.js 24.19.0 LTS (digest pinned) + pinned vsce, ovsx, pnpm" | |
| 354 | + | |
| 355 | + # Rust 1.97.1 via a digest-pinned `rustup-init` *binary* — not the | |
| 356 | + # `curl https://sh.rustup.rs | sh` bootstrapper, which resolves "latest" and | |
| 357 | + # rewrites the user's shell rc files. rustup rather than the standalone | |
| 358 | + # tarballs because it is what Zed shells out to when an extension needs a | |
| 359 | + # target that is not installed yet (`rustup target add wasm32-wasip2`). | |
| 360 | + # `--profile minimal` skips the ~500 MB of offline docs; clippy and rustfmt | |
| 361 | + # are added explicitly because qlty's Rust plugins invoke them. | |
| 362 | + # To bump: change RUSTUP_VERSION and both SHA256 values (published at | |
| 363 | + # static.rust-lang.org/rustup/archive/<ver>/<triple>/rustup-init.sha256), | |
| 364 | + # and/or RUST_VERSION on its own (rustup resolves that one itself). | |
| 365 | + - command: | | |
| 366 | + set -eu | |
| 367 | + RUSTUP_VERSION=1.29.0 | |
| 368 | + RUST_VERSION=1.97.1 | |
| 369 | + ARCH=$(dpkg --print-architecture) | |
| 370 | + case "$ARCH" in | |
| 371 | + amd64) | |
| 372 | + TRIPLE=x86_64-unknown-linux-gnu | |
| 373 | + SHA256=4acc9acc76d5079515b46346a485974457b5a79893cfb01112423c89aeb5aa10 | |
| 374 | + ;; | |
| 375 | + arm64) | |
| 376 | + TRIPLE=aarch64-unknown-linux-gnu | |
| 377 | + SHA256=9732d6c5e2a098d3521fca8145d826ae0aaa067ef2385ead08e6feac88fa5792 | |
| 378 | + ;; | |
| 379 | + *) echo "unsupported sandbox arch: $ARCH (expected amd64 or arm64)" >&2; exit 1 ;; | |
| 380 | + esac | |
| 381 | + export RUSTUP_HOME=/usr/local/rustup | |
| 382 | + export CARGO_HOME=/usr/local/cargo | |
| 383 | + if [ "$(/usr/local/cargo/bin/rustc --version 2>/dev/null | awk '{print $2}')" != "$RUST_VERSION" ]; then | |
| 384 | + URL="https://static.rust-lang.org/rustup/archive/${RUSTUP_VERSION}/${TRIPLE}/rustup-init" | |
| 385 | + curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/rustup-init "$URL" | |
| 386 | + echo "${SHA256} /tmp/rustup-init" | sha256sum -c - | |
| 387 | + chmod 0755 /tmp/rustup-init | |
| 388 | + # --no-modify-path: PATH comes from the /usr/local/bin symlinks below, | |
| 389 | + # not from an appended line in a shell rc file. | |
| 390 | + /tmp/rustup-init -y --no-modify-path --profile minimal \ | |
| 391 | + --default-toolchain "$RUST_VERSION" \ | |
| 392 | + -c clippy -c rustfmt \ | |
| 393 | + -t wasm32-wasip2 | |
| 394 | + rm -f /tmp/rustup-init | |
| 395 | + fi | |
| 396 | + for b in rustc cargo rustup rustfmt cargo-fmt cargo-clippy clippy-driver; do | |
| 397 | + ln -sf "/usr/local/cargo/bin/$b" "/usr/local/bin/$b" | |
| 398 | + done | |
| 399 | + # The agent runs as a non-root user, and `rustup target add`, | |
| 400 | + # `rustup component add` and `cargo install` all write inside these two | |
| 401 | + # trees. Same approach as the official `rust` image: make them writable | |
| 402 | + # rather than forcing every Rust command through sudo. | |
| 403 | + chmod -R a+w /usr/local/rustup /usr/local/cargo | |
| 404 | + /usr/local/bin/rustc --version | |
| 405 | + /usr/local/bin/cargo --version | |
| 406 | + /usr/local/bin/rustup target list --installed | |
| 407 | + user: "0" | |
| 408 | + description: "Install Rust 1.97.1 via digest-pinned rustup-init, with clippy, rustfmt and the wasm32-wasip2 target" | |
| 409 | + | |
| 410 | + startup: | |
| 411 | + # Every cache volume mounts with a root-owned root because the runtime formats | |
| 412 | + # block volumes as ext4, which shadows the image's directory ownership. | |
| 413 | + # Without this the agent user cannot populate the caches — and for | |
| 414 | + # /usr/local/cargo/registry the failure is a confusing "permission denied" | |
| 415 | + # from the middle of a `cargo build`, not from anything the agent typed. | |
| 416 | + # Idempotent by construction — startup commands run on every container start. | |
| 417 | + - command: | |
| 418 | + - "sh" | |
| 419 | + - "-c" | |
| 420 | + - | | |
| 421 | + for d in /home/agent/.qlty /home/agent/.npm; do | |
| 422 | + mkdir -p "$d" && chown -R agent:agent "$d" 2>/dev/null || true | |
| 423 | + done | |
| 424 | + # Left group/world-writable rather than chowned, to stay consistent | |
| 425 | + # with the `chmod -R a+w` the Rust install applies to the rest of | |
| 426 | + # CARGO_HOME: root installs into it, the agent writes to it. | |
| 427 | + mkdir -p /usr/local/cargo/registry && chmod -R a+w /usr/local/cargo/registry 2>/dev/null || true | |
| 428 | + user: "0" | |
| 429 | + description: "Re-own the qlty, npm and cargo cache volumes so the agent user can write to them" | |
| 430 | + | |
| 431 | +agentInstructions: | |
| 432 | + # `filename` is deliberately absent: it is ignored (with a warning) for a | |
| 433 | + # mixin, whose content the engine writes to kits-memory/<kit-name>.md. | |
| 434 | + content: | | |
| 435 | + ## Development toolkit (kit `dev-toolkit`) | |
| 436 | + | |
| 437 | + This sandbox has a pinned toolchain installed at container creation: | |
| 438 | + | |
| 439 | + | Tool | Version | Location | Notes | | |
| 440 | + | -------- | ------- | ------------------- | ----- | | |
| 441 | + | qlty | 0.639.0 | `/usr/local/bin/qlty` | Lint + smells + metrics, for **any** language the project uses. | | |
| 442 | + | Go | 1.26.5 | `/usr/local/go` | Symlinked to `/usr/local/bin/go`; shadows the image's own `/usr/bin/go`. | | |
| 443 | + | TinyGo | 0.41.1 | `/usr/local/tinygo` | `TINYGOROOT` is set; use for `wasm`, `wasip1`, `wasip2` targets. | | |
| 444 | + | Rust | 1.97.1 | `/usr/local/rustup`, `/usr/local/cargo` | Via rustup. `clippy`, `rustfmt` and the `wasm32-wasip2` target are installed. | | |
| 445 | + | Node.js | 24.19.0 | `/usr/local/node` | LTS "Krypton". Shadows the image's own `/usr/bin/node` (22.x). | | |
| 446 | + | npm | 11.17.0 (bundled) | `/usr/local/bin/npm` | Globals go to `/usr/local/share/npm-global`, agent-owned and on PATH. | | |
| 447 | + | pnpm | 11.22.0 | `/usr/local/share/npm-global/bin` | Also `corepack`, if a project pins a different version via `packageManager`. | | |
| 448 | + | vsce / ovsx | 3.9.2 / 1.1.1 | `/usr/local/share/npm-global/bin` | Package and publish VS Code extensions (Marketplace / Open VSX). | | |
| 449 | + | gcc / g++ | Ubuntu's | `/usr/bin` | Installed by this kit — the base image has **no** C compiler at all. | | |
| 450 | + | |
| 451 | + Verify with | |
| 452 | + `qlty --version && go version && tinygo version && rustc --version && node --version`. | |
| 453 | + | |
| 454 | + qlty is **not** language-specific: `qlty init` detects the languages present | |
| 455 | + and enables the matching linters (on a Go repo that is gofmt, golangci-lint, | |
| 456 | + osv-scanner, radarlint-go, ripgrep and trufflehog; on a TypeScript or Rust repo | |
| 457 | + it is an entirely different set). Only the toolchain binaries above are | |
| 458 | + language-specific — the three skills are not. | |
| 459 | + | |
| 460 | + ### Toolchain constraints worth knowing | |
| 461 | + | |
| 462 | + - `GOTOOLCHAIN=local`. Go will **not** auto-download a different toolchain. | |
| 463 | + If a build fails with "go.mod requires go >= X", that is real — either lower | |
| 464 | + the requirement or bump this kit's pinned Go and TinyGo together. | |
| 465 | + - TinyGo 0.41.1 supports Go 1.19 through 1.26 **and** refuses a GOROOT newer | |
| 466 | + than the Go it was itself built with (1.26.2). Go 1.26.5 is inside both | |
| 467 | + bounds. Do not bump Go past 1.26 without bumping TinyGo. | |
| 468 | + - `QLTY_TELEMETRY=off`, and qlty's telemetry host is not allow-listed. Pass | |
| 469 | + `--no-upgrade-check` to qlty commands to skip its update probe as well. | |
| 470 | + - Rust lives under `/usr/local`, not in the home directory, and both | |
| 471 | + `RUSTUP_HOME` and `CARGO_HOME` are world-writable — so `rustup target add`, | |
| 472 | + `rustup component add` and `cargo install` work **without** sudo. Do not | |
| 473 | + re-run `rustup-init`; if a toolchain or target is missing, add it with | |
| 474 | + `rustup`. | |
| 475 | + - `wasm32-wasip2` is the only cross-compilation target preinstalled. Others | |
| 476 | + (`wasm32-unknown-unknown` for wasm-bindgen, say) are one | |
| 477 | + `rustup target add <target>` away — `static.rust-lang.org` is allow-listed. | |
| 478 | + - Rust needs a C linker for **host** builds, and the base image ships none — | |
| 479 | + this kit installs `gcc`/`g++` for that reason. If you ever see | |
| 480 | + "error: linker `cc` not found", the C toolchain step did not run; that is a | |
| 481 | + kit problem, not a project problem. Note the asymmetry: | |
| 482 | + `cargo build --target wasm32-wasip2` works *without* `cc` (rustc uses its own | |
| 483 | + bundled `rust-lld` for wasm), so a missing compiler breaks `cargo test` while | |
| 484 | + leaving the wasm build green. | |
| 485 | + - Prefer a project `devDependency` plus `npx` over `npm install -g`: the | |
| 486 | + version then lives in the repository instead of in this container. Globals do | |
| 487 | + work without sudo if you need them. | |
| 488 | + | |
| 489 | + ### Building editor extensions | |
| 490 | + | |
| 491 | + Both extension ecosystems are supported, and they use different toolchains: | |
| 492 | + | |
| 493 | + | | VS Code | Zed | | |
| 494 | + | --- | --- | --- | | |
| 495 | + | Language | TypeScript / JavaScript on Node | Rust compiled to WebAssembly | | |
| 496 | + | Manifest | `package.json` (`contributes`, `engines.vscode`) | `extension.toml` (+ `Cargo.toml`) | | |
| 497 | + | Build | the project's own script — `npm run compile`, usually tsc or esbuild | `cargo build --release --target wasm32-wasip2` | | |
| 498 | + | Package | `vsce package` → a `.vsix` | Zed's own extension builder, run by the editor | | |
| 499 | + | Publish | `vsce publish` (Marketplace), `ovsx publish` (Open VSX) | a PR to `zed-industries/extensions` | | |
| 500 | + | |
| 501 | + Two things that genuinely do not work in this sandbox, so plan around them | |
| 502 | + rather than debugging them: | |
| 503 | + | |
| 504 | + - **No editor runs here.** There is no display, so neither VS Code's F5 | |
| 505 | + "Extension Development Host" nor Zed's *Install Dev Extension* can be used | |
| 506 | + from inside the sandbox. Both are host-side actions on the user's machine. | |
| 507 | + What you *can* do here is compile, unit-test, lint and package. | |
| 508 | + - **`@vscode/test-electron` needs a display.** Its download hosts are | |
| 509 | + allow-listed and `--extensionDevelopmentPath` runs will fetch a build, but | |
| 510 | + launching it needs an X server (`xvfb-run`), which this kit does not install. | |
| 511 | + Write the extension so its logic is testable without the `vscode` module — | |
| 512 | + keep the `vscode` API calls in a thin activation layer and unit-test the rest | |
| 513 | + — and say so plainly rather than reporting an untested integration path as | |
| 514 | + verified. | |
| 515 | + | |
| 516 | + For a Zed extension, `wasm32-wasip2` is the target Zed itself compiles with; | |
| 517 | + building for anything else produces an artifact Zed will refuse. Extensions | |
| 518 | + that ship Tree-sitter **grammars** additionally need the wasi-sdk to compile | |
| 519 | + the parsers — Zed downloads that itself, on the host, so grammar compilation | |
| 520 | + is not reproducible inside this sandbox either. | |
| 521 | + | |
| 522 | + ### Checking code quality | |
| 523 | + | |
| 524 | + Use the **`quality` skill** (`~/.claude/skills/quality/SKILL.md`) rather than | |
| 525 | + invoking qlty ad hoc. It configures qlty on first use, measures the project, | |
| 526 | + writes a timestamped Markdown report under `.quality/`, records a history line | |
| 527 | + so progression across runs is visible, and then drives refactoring until the | |
| 528 | + gate passes. | |
| 529 | + | |
| 530 | + Non-Claude agents: run | |
| 531 | + `python3 ~/.claude/skills/quality/scripts/quality_report.py --workspace .` | |
| 532 | + to get the same report and history, then refactor against its findings. | |
| 533 | + | |
| 534 | + ### Other skills shipped by this kit | |
| 535 | + | |
| 536 | + They live under `~/.claude/skills/` and are picked up automatically: | |
| 537 | + | |
| 538 | + - **`diataxis-doc`** — writes or restructures project documentation with the | |
| 539 | + Diátaxis method (tutorials / how-to / reference / explanation). It asks which | |
| 540 | + language to produce the docs in before writing anything, unless the invoking | |
| 541 | + skill has already fixed it. | |
| 542 | + - **`methodical-dev`** — drives a new feature through gathering requirements, | |
| 543 | + planning, and implementation, stopping for approval after every step. It is the | |
| 544 | + **orchestrator**: it invokes `quality` and `diataxis-doc` as mandatory closing | |
| 545 | + phases, requires unit tests in the same step as the code they cover, and keeps a | |
| 546 | + committed `.memory/` record (summary, append-only history, handoffs) so work can | |
| 547 | + be resumed in a fresh sandbox. Start here for any non-trivial change. | |
| 548 | + - **`turbo-new-editor`** — builds a complete terminal IDE for one language, | |
| 549 | + `turbo-<language>`, on top of the `turbo-core` library: a command, a profile | |
| 550 | + and a syntax scanner, with everything else — windows, menus, dialogs, themes, | |
| 551 | + the LSP client, the terminal emulator — coming from the library. It runs | |
| 552 | + *inside* `methodical-dev` rather than replacing it, and takes the language and | |
| 553 | + the path of a repository the user has already created. | |
| 554 | + | |
| 555 | + ### `.memory/` — the project record (read it first) | |
| 556 | + | |
| 557 | + This kit also installs `~/.claude/CLAUDE.md`, which Claude Code loads at the start | |
| 558 | + of **every** session: it says to read `.memory/` before planning or asking the user | |
| 559 | + anything. On non-Claude agents that file is inert — the convention below applies | |
| 560 | + anyway. | |
| 561 | + | |
| 562 | + `methodical-dev` maintains `.memory/` at the repository root, **committed, not | |
| 563 | + gitignored**: | |
| 564 | + | |
| 565 | + | Path | Contents | | |
| 566 | + | ---- | -------- | | |
| 567 | + | `summary.md` | Living snapshot: architecture, decisions in force, build/test commands. Edited in place. | | |
| 568 | + | `history.md` | Append-only dated log, one entry per completed feature. Never rewritten. | | |
| 569 | + | `handoffs/` | Per-session handoff: state, work in flight, next steps, blockers. | | |
| 570 | + | |
| 571 | + If `.memory/` exists, **read `summary.md` and the latest handoff before starting | |
| 572 | + work** — it records the project's state and any unfinished business, and saves you | |
| 573 | + from re-deriving it or asking the user what is already written down. | |
| 574 | + | |
| 575 | + **Every session also ends by bringing all three files up to date** — no exceptions, | |
| 576 | + including sessions that fixed one line, only read code, or achieved nothing (those | |
| 577 | + last stop the next agent hitting the same wall): | |
| 578 | + | |
| 579 | + - `summary.md` — **edit in place, never regenerate.** Change only what your session | |
| 580 | + establishes or invalidates and leave the rest byte-for-byte alone; rewriting it | |
| 581 | + wholesale from one session's slice destroys accurate content you never looked at. | |
| 582 | + Only from what you verified, never a plausible guess; what you could not check goes | |
| 583 | + under an explicit `## Not yet established` heading. If the project's state did not | |
| 584 | + change, leave the file untouched. | |
| 585 | + - `history.md` — **append exactly one dated entry.** Never rewrite or delete an | |
| 586 | + existing one. | |
| 587 | + - `handoffs/YYYY-MM-DD-<slug>.md` — write today's, or update it if it already covers | |
| 588 | + this topic. Never overwrite another session's handoff. | |
| 589 | + | |
| 590 | + Match the length to the work: a one-line fix earns a few lines, not a filled-in | |
| 591 | + template — padding with "N/A" makes the record unreadable. Record what the next | |
| 592 | + person cannot re-derive (where you stopped, what failed, the trap, the pending | |
| 593 | + decision), not a diff summary. If `.memory/` does not exist yet, create the whole | |
| 594 | + skeleton: `README.md`, `summary.md`, `history.md` and `handoffs/`. | |
| 595 | + | |
| 596 | + Keep the boundary clean: `docs/` is for people *using* the project, `.memory/` is | |
| 597 | + for whoever *continues building* it. | |
| 598 | + | |
| 599 | + Two things that make the measurement meaningless — never do either: | |
| 600 | + | |
| 601 | + - Do not add `exclude_patterns`, disable a plugin, or raise a threshold in | |
| 602 | + `.qlty/qlty.toml` to make the gate pass. Fix the code. | |
| 603 | + - Do not delete or skip tests to clear a finding. | |
| new file mode 100644 | |||
| @@ -0,0 +1,603 @@ | |||
| 1 | +schemaVersion: "2" | ||
| 2 | +kind: mixin | ||
| 3 | +name: dev-toolkit | ||
| 4 | +displayName: Development toolkit (Go + Rust + Node + qlty + skills) | ||
| 5 | +description: > | ||
| 6 | + A general-purpose development toolkit. Installs the qlty 0.639.0 code-quality | ||
| 7 | + engine, which analyses whatever languages a project actually uses, plus three | ||
| 8 | + pinned language toolchains: Go 1.26.5 with TinyGo 0.41.1 for WebAssembly / WASI | ||
| 9 | + builds, Rust 1.97.1 via rustup with the `wasm32-wasip2` target (what Zed | ||
| 10 | + compiles extensions to), and Node.js 24.19.0 with npm, pnpm and the | ||
| 11 | + `@vscode/vsce` / `ovsx` extension packagers. Ships four Claude Code skills: | ||
| 12 | + `quality` (self-configures qlty, measures the project, writes a Markdown report, | ||
| 13 | + tracks progression across runs, and drives refactoring until the quality gate | ||
| 14 | + passes), `diataxis-doc` (documents a project with the Diátaxis four-quadrant | ||
| 15 | + method), `methodical-dev` (step-by-step, approval-gated feature development | ||
| 16 | + that orchestrates the other two, and keeps a committed `.memory/` project | ||
| 17 | + record), plus a `~/.claude/CLAUDE.md` that makes every session start by reading | ||
| 18 | + that record, and `turbo-new-editor` (builds a complete terminal IDE for a new | ||
| 19 | + language on top of the `turbo-core` library, inside `methodical-dev`). Only the | ||
| 20 | + toolchain binaries are language-specific; the quality, documentation and | ||
| 21 | + workflow skills are not. | ||
| 22 | +licenses: | ||
| 23 | + - MIT | ||
| 24 | + | ||
| 25 | +# Deliberately no `requires.agent`: the toolchain is agent-agnostic. The skills | ||
| 26 | +# under files/home/.claude/skills/ and files/home/.claude/CLAUDE.md are | ||
| 27 | +# Claude-Code-specific and are simply inert on other agents, which still get the | ||
| 28 | +# quality workflow and the `.memory/` convention through `agentInstructions` below. | ||
| 29 | + | ||
| 30 | +permissions: | ||
| 31 | + network: | ||
| 32 | + allow: | ||
| 33 | + # ---- install-time, one-shot ------------------------------------------- | ||
| 34 | + # Go toolchain tarball. dl.google.com is the canonical host go.dev/dl | ||
| 35 | + # redirects to; hitting it directly keeps the allowlist to one entry. | ||
| 36 | + - dl.google.com | ||
| 37 | + # TinyGo release tarball. github.com 302-redirects binary downloads to | ||
| 38 | + # release-assets.githubusercontent.com (covered by the wildcard below). | ||
| 39 | + - github.com | ||
| 40 | + # Matches exactly one DNS label, so this single entry covers | ||
| 41 | + # release-assets. / objects. / raw.githubusercontent.com. | ||
| 42 | + - "*.githubusercontent.com" | ||
| 43 | + # qlty CLI tarball (and `qlty upgrade`, if ever run by hand). | ||
| 44 | + - qlty-releases.s3.amazonaws.com | ||
| 45 | + # The C/C++ toolchain comes from Ubuntu's own archives. ports.* serves | ||
| 46 | + # arm64, archive.*/security.* serve amd64 — which one is used depends on the | ||
| 47 | + # sandbox architecture, so all three are listed. | ||
| 48 | + - ports.ubuntu.com | ||
| 49 | + - archive.ubuntu.com | ||
| 50 | + - security.ubuntu.com | ||
| 51 | + # Only reached if the cached apt lists ever go stale and the fallback | ||
| 52 | + # `apt-get update` runs: it re-fetches *every* configured source, and the | ||
| 53 | + # template configures Docker's as well as Ubuntu's. | ||
| 54 | + - download.docker.com | ||
| 55 | + # `rustup-init`, plus every toolchain component rustup resolves. One host | ||
| 56 | + # serves both the rustup releases and the dist channel, so this covers the | ||
| 57 | + # install *and* later `rustup target add` / `rustup component add`. | ||
| 58 | + - static.rust-lang.org | ||
| 59 | + # The Node.js tarball comes from nodejs.org, already listed under "qlty | ||
| 60 | + # runtime" below — qlty provisions its own Node for the JS/TS linters, so the | ||
| 61 | + # host is needed either way. | ||
| 62 | + | ||
| 63 | + # ---- qlty runtime ------------------------------------------------------ | ||
| 64 | + # qlty resolves its default plugin source (qltysh/qlty-plugins) and every | ||
| 65 | + # linter release through the GitHub API, its own GitHub proxy, and | ||
| 66 | + # codeload for repository tarballs. | ||
| 67 | + - api.github.com | ||
| 68 | + - github-proxy.qlty.sh | ||
| 69 | + - codeload.github.com | ||
| 70 | + # radarlint-go — enabled by `qlty init` on Go repos; ships from its own | ||
| 71 | + # bucket and pulls a Temurin JVM from GitHub releases. | ||
| 72 | + - radarlint-releases.s3.amazonaws.com | ||
| 73 | + # osv-scanner — also enabled by `qlty init` on Go repos. | ||
| 74 | + - api.osv.dev | ||
| 75 | + - osv-vulnerabilities.storage.googleapis.com | ||
| 76 | + # Language runtimes qlty provisions on demand for non-Go linters. Trim | ||
| 77 | + # these if you only ever lint Go — except nodejs.org, which this kit's own | ||
| 78 | + # Node install step also needs. | ||
| 79 | + - nodejs.org | ||
| 80 | + - registry.npmjs.org | ||
| 81 | + - pypi.org | ||
| 82 | + - files.pythonhosted.org | ||
| 83 | + | ||
| 84 | + # ---- Go module resolution --------------------------------------------- | ||
| 85 | + - proxy.golang.org | ||
| 86 | + - sum.golang.org | ||
| 87 | + | ||
| 88 | + # ---- Cargo dependency resolution -------------------------------------- | ||
| 89 | + # Three distinct hosts, all needed: the sparse index cargo reads by | ||
| 90 | + # default, the CDN the .crate files come from, and the API `cargo add` / | ||
| 91 | + # `cargo search` / `cargo publish` talk to. Git dependencies go through | ||
| 92 | + # github.com, already allowed above. | ||
| 93 | + - index.crates.io | ||
| 94 | + - static.crates.io | ||
| 95 | + - crates.io | ||
| 96 | + | ||
| 97 | + # ---- VS Code extension tooling ---------------------------------------- | ||
| 98 | + # `vsce publish` / `vsce ls-publishers`, and the gallery query API that | ||
| 99 | + # `@vscode/test-electron` uses to resolve extension dependencies. | ||
| 100 | + - marketplace.visualstudio.com | ||
| 101 | + # Extension asset (.vsix) download. Each publisher gets its own subdomain, | ||
| 102 | + # and the wildcard matches exactly one DNS label, so both forms are needed. | ||
| 103 | + - "*.gallery.vsassets.io" | ||
| 104 | + - "*.gallerycdn.vsassets.io" | ||
| 105 | + # `@vscode/test-electron` asks this for the build to test against, then | ||
| 106 | + # downloads the archive from Microsoft's CDN. | ||
| 107 | + - update.code.visualstudio.com | ||
| 108 | + - vscode.download.prss.microsoft.com | ||
| 109 | + # `ovsx publish` — the Open VSX registry, which is where a VS Code | ||
| 110 | + # extension has to go to be installable in VSCodium, Cursor or Zed. | ||
| 111 | + - open-vsx.org | ||
| 112 | + | ||
| 113 | +environment: | ||
| 114 | + variables: | ||
| 115 | + # Pin the toolchain we actually installed. Without this, Go 1.21+ silently | ||
| 116 | + # downloads whatever `go`/`toolchain` line go.mod names, which would defeat | ||
| 117 | + # the point of pinning 1.26.5 (and needs egress we haven't allowed). If a | ||
| 118 | + # go.mod legitimately requires something newer, this surfaces as a clear | ||
| 119 | + # error instead of a surprise download. | ||
| 120 | + GOTOOLCHAIN: local | ||
| 121 | + # TinyGo resolves its root from the executable path, which already works for | ||
| 122 | + # the tarball layout. Setting it explicitly wins over any value baked in at | ||
| 123 | + # release-build time, so `tinygo` keeps working when invoked via the | ||
| 124 | + # /usr/local/bin symlink. | ||
| 125 | + TINYGOROOT: /usr/local/tinygo | ||
| 126 | + # qlty phones home to cdp.customer.io on every command otherwise. That host | ||
| 127 | + # is not allow-listed above, so the calls would fail anyway — this turns | ||
| 128 | + # them off at the source instead of relying on the firewall. | ||
| 129 | + QLTY_TELEMETRY: "off" | ||
| 130 | + # Rust lives under /usr/local rather than in the agent's home, for the same | ||
| 131 | + # reason Go and TinyGo do: the install runs once at container creation, and | ||
| 132 | + # only paths baked into the image are guaranteed to survive a restart. Both | ||
| 133 | + # directories are made world-writable by the install step, so `rustup target | ||
| 134 | + # add`, `rustup component add` and `cargo install` all work as the agent | ||
| 135 | + # user — this is the layout the official `rust` Docker image uses. | ||
| 136 | + RUSTUP_HOME: /usr/local/rustup | ||
| 137 | + CARGO_HOME: /usr/local/cargo | ||
| 138 | + # Deliberately NOT setting npm's prefix here. The base template already | ||
| 139 | + # exports NPM_CONFIG_PREFIX=/usr/local/share/npm-global, and that directory is | ||
| 140 | + # agent-owned and already on PATH ahead of /usr/local/bin — so `npm install -g` | ||
| 141 | + # works for the agent user with no sudo. Overriding it to /usr/local would | ||
| 142 | + # move global installs into a root-owned tree and break exactly that. The | ||
| 143 | + # install step below installs its pinned CLIs into the template's prefix and | ||
| 144 | + # hands ownership back to the agent. | ||
| 145 | + | ||
| 146 | +# Package caches. Each of these is repopulated over the network on a cold start, | ||
| 147 | +# so persisting them across container restarts turns minutes into a no-op. | ||
| 148 | +# Volumes are fixed at container creation: `sbx kit add` warns and skips them, so | ||
| 149 | +# use `--kit` at create time to get the caches. | ||
| 150 | +volumes: | ||
| 151 | + # qlty downloads a JVM (for radarlint), Node, and every linter binary into | ||
| 152 | + # ~/.qlty/cache. | ||
| 153 | + - path: /home/agent/.qlty | ||
| 154 | + # Cargo's downloaded .crate files and its copy of the sparse index. | ||
| 155 | + # Deliberately the `registry` subdirectory rather than all of CARGO_HOME: | ||
| 156 | + # mounting an empty volume over CARGO_HOME would shadow the rustup shims that | ||
| 157 | + # the install step puts in its `bin`. | ||
| 158 | + - path: /usr/local/cargo/registry | ||
| 159 | + # npm's content-addressable cache (`npm config get cache`). | ||
| 160 | + - path: /home/agent/.npm | ||
| 161 | + | ||
| 162 | +setup: | ||
| 163 | + install: | ||
| 164 | + # Go 1.26.5, version + digest pinned. The base template already ships a | ||
| 165 | + # /usr/bin/go (1.26.0 at time of writing); /usr/local/bin precedes /usr/bin | ||
| 166 | + # on PATH, so the symlinks below shadow it without removing anything. | ||
| 167 | + # To bump: change GO_VERSION and both SHA256 values (from | ||
| 168 | + # https://go.dev/dl/?mode=json). | ||
| 169 | + - command: | | ||
| 170 | + set -eu | ||
| 171 | + GO_VERSION=1.26.5 | ||
| 172 | + ARCH=$(dpkg --print-architecture) | ||
| 173 | + case "$ARCH" in | ||
| 174 | + amd64) SHA256=5c2c3b16caefa1d968a94c1daca04a7ca301a496d9b086e17ad77bb81393f053 ;; | ||
| 175 | + arm64) SHA256=fe4789e92b1f33358680864bbe8704289e7bb5fc207d80623c308935bd696d49 ;; | ||
| 176 | + *) echo "unsupported sandbox arch: $ARCH (expected amd64 or arm64)" >&2; exit 1 ;; | ||
| 177 | + esac | ||
| 178 | + if [ "$(/usr/local/go/bin/go version 2>/dev/null | awk '{print $3}')" != "go${GO_VERSION}" ]; then | ||
| 179 | + URL="https://dl.google.com/go/go${GO_VERSION}.linux-${ARCH}.tar.gz" | ||
| 180 | + curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/go.tgz "$URL" | ||
| 181 | + echo "${SHA256} /tmp/go.tgz" | sha256sum -c - | ||
| 182 | + rm -rf /usr/local/go | ||
| 183 | + tar -C /usr/local -xzf /tmp/go.tgz | ||
| 184 | + rm -f /tmp/go.tgz | ||
| 185 | + fi | ||
| 186 | + ln -sf /usr/local/go/bin/go /usr/local/bin/go | ||
| 187 | + ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt | ||
| 188 | + /usr/local/bin/go version | ||
| 189 | + user: "0" | ||
| 190 | + description: "Install Go 1.26.5 (version + digest pinned), shadowing the image's Go" | ||
| 191 | + | ||
| 192 | + # TinyGo 0.41.1. Its supported range is Go 1.19–1.26 and the published | ||
| 193 | + # binary is itself built with go1.26.2, so it accepts the Go 1.26.5 above | ||
| 194 | + # (TinyGo also refuses a GOROOT newer than its own build toolchain). | ||
| 195 | + # Bumping Go to 1.27 would break this pairing — bump TinyGo together with it. | ||
| 196 | + - command: | | ||
| 197 | + set -eu | ||
| 198 | + TINYGO_VERSION=0.41.1 | ||
| 199 | + ARCH=$(dpkg --print-architecture) | ||
| 200 | + case "$ARCH" in | ||
| 201 | + amd64) SHA256=e156d1d93a376eef639a4143d13be07e8c463fb6cf2d7d447698ed4474d23e91 ;; | ||
| 202 | + arm64) SHA256=789733bc3b5bace0bd1835a267b3ea267804a7ef1cfe69bc522c295f5226d624 ;; | ||
| 203 | + *) echo "unsupported sandbox arch: $ARCH (expected amd64 or arm64)" >&2; exit 1 ;; | ||
| 204 | + esac | ||
| 205 | + if [ "$(/usr/local/tinygo/bin/tinygo version 2>/dev/null | awk '{print $3}')" != "$TINYGO_VERSION" ]; then | ||
| 206 | + TARBALL="tinygo${TINYGO_VERSION}.linux-${ARCH}.tar.gz" | ||
| 207 | + URL="https://github.com/tinygo-org/tinygo/releases/download/v${TINYGO_VERSION}/${TARBALL}" | ||
| 208 | + curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/tinygo.tgz "$URL" | ||
| 209 | + echo "${SHA256} /tmp/tinygo.tgz" | sha256sum -c - | ||
| 210 | + rm -rf /usr/local/tinygo | ||
| 211 | + tar -C /usr/local -xzf /tmp/tinygo.tgz | ||
| 212 | + rm -f /tmp/tinygo.tgz | ||
| 213 | + fi | ||
| 214 | + ln -sf /usr/local/tinygo/bin/tinygo /usr/local/bin/tinygo | ||
| 215 | + /usr/local/bin/tinygo version | ||
| 216 | + user: "0" | ||
| 217 | + description: "Install TinyGo 0.41.1 (version + digest pinned)" | ||
| 218 | + | ||
| 219 | + # qlty 0.639.0, pulled straight from the release bucket rather than through | ||
| 220 | + # `curl https://qlty.sh | sh`. The install script resolves "latest" (not | ||
| 221 | + # reproducible), POSTs an install event to cdp.customer.io, and rewrites the | ||
| 222 | + # user's shell rc files — none of which belongs in a sandbox image. | ||
| 223 | + # The asset is .tar.xz and the base image has no `xz`, so fall back to | ||
| 224 | + # Python's lzma (stdlib, and python3 is present) instead of pulling xz-utils | ||
| 225 | + # over apt. This step deliberately stays apt-free: it runs *before* the | ||
| 226 | + # C-toolchain step below, so it cannot assume anything about apt having been | ||
| 227 | + # exercised yet, and a self-contained step is one less ordering dependency. | ||
| 228 | + # To bump: change QLTY_VERSION and both SHA256 values (published as | ||
| 229 | + # <asset>.sha256 next to each release asset). | ||
| 230 | + - command: | | ||
| 231 | + set -eu | ||
| 232 | + QLTY_VERSION=0.639.0 | ||
| 233 | + ARCH=$(dpkg --print-architecture) | ||
| 234 | + case "$ARCH" in | ||
| 235 | + amd64) | ||
| 236 | + TARGET=x86_64-unknown-linux-gnu | ||
| 237 | + SHA256=49f8299d25a1c50cf02af8263483b1a04b8b665d71c5b6512847e08ccb392a90 | ||
| 238 | + ;; | ||
| 239 | + arm64) | ||
| 240 | + TARGET=aarch64-unknown-linux-gnu | ||
| 241 | + SHA256=8f2f5cd7eb06c3f1ba106866bbf80cfb563a06d70ee495c14391dced37d05a4e | ||
| 242 | + ;; | ||
| 243 | + *) echo "unsupported sandbox arch: $ARCH (expected amd64 or arm64)" >&2; exit 1 ;; | ||
| 244 | + esac | ||
| 245 | + if [ "$(/usr/local/bin/qlty --version 2>/dev/null | awk '{print $2}')" != "$QLTY_VERSION" ]; then | ||
| 246 | + URL="https://qlty-releases.s3.amazonaws.com/qlty/v${QLTY_VERSION}/qlty-${TARGET}.tar.xz" | ||
| 247 | + curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/qlty.tar.xz "$URL" | ||
| 248 | + echo "${SHA256} /tmp/qlty.tar.xz" | sha256sum -c - | ||
| 249 | + rm -rf "/tmp/qlty-${TARGET}" | ||
| 250 | + if command -v xz >/dev/null 2>&1; then | ||
| 251 | + tar -C /tmp -xJf /tmp/qlty.tar.xz "qlty-${TARGET}/qlty" | ||
| 252 | + else | ||
| 253 | + python3 -c 'import lzma,sys; sys.stdout.buffer.write(lzma.open(sys.argv[1],"rb").read())' \ | ||
| 254 | + /tmp/qlty.tar.xz | tar -C /tmp -xf - "qlty-${TARGET}/qlty" | ||
| 255 | + fi | ||
| 256 | + install -m 0755 "/tmp/qlty-${TARGET}/qlty" /usr/local/bin/qlty | ||
| 257 | + rm -rf /tmp/qlty.tar.xz "/tmp/qlty-${TARGET}" | ||
| 258 | + fi | ||
| 259 | + /usr/local/bin/qlty --version | ||
| 260 | + user: "0" | ||
| 261 | + description: "Install qlty CLI 0.639.0 (version + digest pinned, no install script)" | ||
| 262 | + | ||
| 263 | + # A C/C++ toolchain. Not optional, and not obvious: the base template ships | ||
| 264 | + # `make` but **no `cc`, `gcc`, `clang`, `ld` or libc headers at all**. Without | ||
| 265 | + # this step: | ||
| 266 | + # - `cargo build` / `cargo test` for the host target fail with | ||
| 267 | + # "error: linker `cc` not found" (measured). Cross-compiling to | ||
| 268 | + # wasm32-wasip2 still works, because rustc uses its own bundled rust-lld | ||
| 269 | + # for wasm targets — so a Zed extension builds but cannot be unit-tested. | ||
| 270 | + # - any crate with a `build.rs` that compiles C, and any npm package with a | ||
| 271 | + # native addon built through node-gyp, fails the same way. | ||
| 272 | + # `apt-get update` is deliberately NOT run: the template ships populated | ||
| 273 | + # /var/lib/apt/lists, so a plain install works, and `update` would re-fetch | ||
| 274 | + # every configured source (including download.docker.com) and fail if any one | ||
| 275 | + # of them is not allow-listed. It is kept only as a fallback for the day those | ||
| 276 | + # cached lists go stale. | ||
| 277 | + - command: | | ||
| 278 | + set -eu | ||
| 279 | + if ! command -v cc >/dev/null 2>&1; then | ||
| 280 | + export DEBIAN_FRONTEND=noninteractive | ||
| 281 | + PKGS="gcc g++ libc6-dev pkg-config" | ||
| 282 | + # `-o DPkg::Lock::Timeout` is not optional here. The claude kit registers | ||
| 283 | + # a *backgrounded* `apt-get update` as a startup command, so at the moment | ||
| 284 | + # this step runs (~11s into the install sequence) that update may still | ||
| 285 | + # hold the dpkg frontend lock. Without the timeout apt does not wait — it | ||
| 286 | + # gives up at once with exit 100, and the whole kit apply fails. | ||
| 287 | + APT="apt-get -o DPkg::Lock::Timeout=300" | ||
| 288 | + $APT install -y --no-install-recommends $PKGS \ | ||
| 289 | + || { $APT update && $APT install -y --no-install-recommends $PKGS; } | ||
| 290 | + fi | ||
| 291 | + cc --version | head -1 | ||
| 292 | + c++ --version | head -1 | ||
| 293 | + user: "0" | ||
| 294 | + description: "Install a C/C++ toolchain (gcc, g++, libc6-dev, pkg-config) — required to link host-target Rust builds and native npm addons" | ||
| 295 | + | ||
| 296 | + # Node.js 24.19.0 — "Krypton", the active LTS line. The base template ships a | ||
| 297 | + # /usr/bin/node (22.22.1 at time of writing); as with Go, /usr/local/bin | ||
| 298 | + # precedes /usr/bin on PATH, so the symlinks shadow it without removing it. | ||
| 299 | + # The .tar.gz is used rather than the (smaller) .tar.xz on purpose: the base | ||
| 300 | + # image has no `xz`, and unlike qlty, Node publishes both formats — so this | ||
| 301 | + # avoids the python-lzma workaround the qlty step needs. | ||
| 302 | + # To bump: change NODE_VERSION and both SHA256 values, from | ||
| 303 | + # https://nodejs.org/dist/v<version>/SHASUMS256.txt. | ||
| 304 | + - command: | | ||
| 305 | + set -eu | ||
| 306 | + NODE_VERSION=24.19.0 | ||
| 307 | + ARCH=$(dpkg --print-architecture) | ||
| 308 | + case "$ARCH" in | ||
| 309 | + amd64) NODE_ARCH=x64; SHA256=f625d97cd707df4ff96254916fbc5ff014f09c09effe5a1e0ca8f6d41a8789d4 ;; | ||
| 310 | + arm64) NODE_ARCH=arm64; SHA256=d28c8a5bf0a808f0ed434a1dce8c54ae98f0371c0bd86ac58abc613f73e6643f ;; | ||
| 311 | + *) echo "unsupported sandbox arch: $ARCH (expected amd64 or arm64)" >&2; exit 1 ;; | ||
| 312 | + esac | ||
| 313 | + if [ "$(/usr/local/node/bin/node --version 2>/dev/null)" != "v${NODE_VERSION}" ]; then | ||
| 314 | + TARBALL="node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz" | ||
| 315 | + URL="https://nodejs.org/dist/v${NODE_VERSION}/${TARBALL}" | ||
| 316 | + curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/node.tgz "$URL" | ||
| 317 | + echo "${SHA256} /tmp/node.tgz" | sha256sum -c - | ||
| 318 | + rm -rf /usr/local/node | ||
| 319 | + mkdir -p /usr/local/node | ||
| 320 | + tar -C /usr/local/node --strip-components=1 -xzf /tmp/node.tgz | ||
| 321 | + rm -f /tmp/node.tgz | ||
| 322 | + fi | ||
| 323 | + for b in node npm npx corepack; do | ||
| 324 | + ln -sf "/usr/local/node/bin/$b" "/usr/local/bin/$b" | ||
| 325 | + done | ||
| 326 | + # Global installs go to the template's own prefix, which is already on | ||
| 327 | + # PATH. --prefix is passed explicitly rather than trusting NPM_CONFIG_PREFIX | ||
| 328 | + # to be exported into this root-run step. | ||
| 329 | + NPM_PREFIX=/usr/local/share/npm-global | ||
| 330 | + mkdir -p "$NPM_PREFIX" | ||
| 331 | + # Pinned, and deliberately only the tools that belong to the *sandbox* | ||
| 332 | + # rather than to a project: the two extension packagers, and pnpm. A | ||
| 333 | + # project's TypeScript, esbuild and test runner belong in its own | ||
| 334 | + # devDependencies, where the version is committed alongside the code. | ||
| 335 | + # | ||
| 336 | + # --allow-scripts is required by npm 11: it otherwise skips these two | ||
| 337 | + # lifecycle scripts and only warns. keytar builds the native addon vsce | ||
| 338 | + # uses to store a publishing PAT, and vsce-sign unpacks the signing | ||
| 339 | + # binary — both are needed by `vsce publish`, and both fail silently | ||
| 340 | + # (at publish time, not now) if their scripts never run. | ||
| 341 | + /usr/local/bin/npm install -g --prefix "$NPM_PREFIX" --no-fund --no-audit \ | ||
| 342 | + --allow-scripts=@vscode/vsce-sign,keytar \ | ||
| 343 | + @vscode/vsce@3.9.2 \ | ||
| 344 | + ovsx@1.1.1 \ | ||
| 345 | + pnpm@11.22.0 | ||
| 346 | + # Root just wrote into an agent-owned tree; hand it back so the agent can | ||
| 347 | + # install or update globals later without sudo. | ||
| 348 | + chown -R agent:agent "$NPM_PREFIX" | ||
| 349 | + /usr/local/bin/node --version | ||
| 350 | + /usr/local/bin/npm --version | ||
| 351 | + "$NPM_PREFIX/bin/vsce" --version | ||
| 352 | + user: "0" | ||
| 353 | + description: "Install Node.js 24.19.0 LTS (digest pinned) + pinned vsce, ovsx, pnpm" | ||
| 354 | + | ||
| 355 | + # Rust 1.97.1 via a digest-pinned `rustup-init` *binary* — not the | ||
| 356 | + # `curl https://sh.rustup.rs | sh` bootstrapper, which resolves "latest" and | ||
| 357 | + # rewrites the user's shell rc files. rustup rather than the standalone | ||
| 358 | + # tarballs because it is what Zed shells out to when an extension needs a | ||
| 359 | + # target that is not installed yet (`rustup target add wasm32-wasip2`). | ||
| 360 | + # `--profile minimal` skips the ~500 MB of offline docs; clippy and rustfmt | ||
| 361 | + # are added explicitly because qlty's Rust plugins invoke them. | ||
| 362 | + # To bump: change RUSTUP_VERSION and both SHA256 values (published at | ||
| 363 | + # static.rust-lang.org/rustup/archive/<ver>/<triple>/rustup-init.sha256), | ||
| 364 | + # and/or RUST_VERSION on its own (rustup resolves that one itself). | ||
| 365 | + - command: | | ||
| 366 | + set -eu | ||
| 367 | + RUSTUP_VERSION=1.29.0 | ||
| 368 | + RUST_VERSION=1.97.1 | ||
| 369 | + ARCH=$(dpkg --print-architecture) | ||
| 370 | + case "$ARCH" in | ||
| 371 | + amd64) | ||
| 372 | + TRIPLE=x86_64-unknown-linux-gnu | ||
| 373 | + SHA256=4acc9acc76d5079515b46346a485974457b5a79893cfb01112423c89aeb5aa10 | ||
| 374 | + ;; | ||
| 375 | + arm64) | ||
| 376 | + TRIPLE=aarch64-unknown-linux-gnu | ||
| 377 | + SHA256=9732d6c5e2a098d3521fca8145d826ae0aaa067ef2385ead08e6feac88fa5792 | ||
| 378 | + ;; | ||
| 379 | + *) echo "unsupported sandbox arch: $ARCH (expected amd64 or arm64)" >&2; exit 1 ;; | ||
| 380 | + esac | ||
| 381 | + export RUSTUP_HOME=/usr/local/rustup | ||
| 382 | + export CARGO_HOME=/usr/local/cargo | ||
| 383 | + if [ "$(/usr/local/cargo/bin/rustc --version 2>/dev/null | awk '{print $2}')" != "$RUST_VERSION" ]; then | ||
| 384 | + URL="https://static.rust-lang.org/rustup/archive/${RUSTUP_VERSION}/${TRIPLE}/rustup-init" | ||
| 385 | + curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/rustup-init "$URL" | ||
| 386 | + echo "${SHA256} /tmp/rustup-init" | sha256sum -c - | ||
| 387 | + chmod 0755 /tmp/rustup-init | ||
| 388 | + # --no-modify-path: PATH comes from the /usr/local/bin symlinks below, | ||
| 389 | + # not from an appended line in a shell rc file. | ||
| 390 | + /tmp/rustup-init -y --no-modify-path --profile minimal \ | ||
| 391 | + --default-toolchain "$RUST_VERSION" \ | ||
| 392 | + -c clippy -c rustfmt \ | ||
| 393 | + -t wasm32-wasip2 | ||
| 394 | + rm -f /tmp/rustup-init | ||
| 395 | + fi | ||
| 396 | + for b in rustc cargo rustup rustfmt cargo-fmt cargo-clippy clippy-driver; do | ||
| 397 | + ln -sf "/usr/local/cargo/bin/$b" "/usr/local/bin/$b" | ||
| 398 | + done | ||
| 399 | + # The agent runs as a non-root user, and `rustup target add`, | ||
| 400 | + # `rustup component add` and `cargo install` all write inside these two | ||
| 401 | + # trees. Same approach as the official `rust` image: make them writable | ||
| 402 | + # rather than forcing every Rust command through sudo. | ||
| 403 | + chmod -R a+w /usr/local/rustup /usr/local/cargo | ||
| 404 | + /usr/local/bin/rustc --version | ||
| 405 | + /usr/local/bin/cargo --version | ||
| 406 | + /usr/local/bin/rustup target list --installed | ||
| 407 | + user: "0" | ||
| 408 | + description: "Install Rust 1.97.1 via digest-pinned rustup-init, with clippy, rustfmt and the wasm32-wasip2 target" | ||
| 409 | + | ||
| 410 | + startup: | ||
| 411 | + # Every cache volume mounts with a root-owned root because the runtime formats | ||
| 412 | + # block volumes as ext4, which shadows the image's directory ownership. | ||
| 413 | + # Without this the agent user cannot populate the caches — and for | ||
| 414 | + # /usr/local/cargo/registry the failure is a confusing "permission denied" | ||
| 415 | + # from the middle of a `cargo build`, not from anything the agent typed. | ||
| 416 | + # Idempotent by construction — startup commands run on every container start. | ||
| 417 | + - command: | ||
| 418 | + - "sh" | ||
| 419 | + - "-c" | ||
| 420 | + - | | ||
| 421 | + for d in /home/agent/.qlty /home/agent/.npm; do | ||
| 422 | + mkdir -p "$d" && chown -R agent:agent "$d" 2>/dev/null || true | ||
| 423 | + done | ||
| 424 | + # Left group/world-writable rather than chowned, to stay consistent | ||
| 425 | + # with the `chmod -R a+w` the Rust install applies to the rest of | ||
| 426 | + # CARGO_HOME: root installs into it, the agent writes to it. | ||
| 427 | + mkdir -p /usr/local/cargo/registry && chmod -R a+w /usr/local/cargo/registry 2>/dev/null || true | ||
| 428 | + user: "0" | ||
| 429 | + description: "Re-own the qlty, npm and cargo cache volumes so the agent user can write to them" | ||
| 430 | + | ||
| 431 | +agentInstructions: | ||
| 432 | + # `filename` is deliberately absent: it is ignored (with a warning) for a | ||
| 433 | + # mixin, whose content the engine writes to kits-memory/<kit-name>.md. | ||
| 434 | + content: | | ||
| 435 | + ## Development toolkit (kit `dev-toolkit`) | ||
| 436 | + | ||
| 437 | + This sandbox has a pinned toolchain installed at container creation: | ||
| 438 | + | ||
| 439 | + | Tool | Version | Location | Notes | | ||
| 440 | + | -------- | ------- | ------------------- | ----- | | ||
| 441 | + | qlty | 0.639.0 | `/usr/local/bin/qlty` | Lint + smells + metrics, for **any** language the project uses. | | ||
| 442 | + | Go | 1.26.5 | `/usr/local/go` | Symlinked to `/usr/local/bin/go`; shadows the image's own `/usr/bin/go`. | | ||
| 443 | + | TinyGo | 0.41.1 | `/usr/local/tinygo` | `TINYGOROOT` is set; use for `wasm`, `wasip1`, `wasip2` targets. | | ||
| 444 | + | Rust | 1.97.1 | `/usr/local/rustup`, `/usr/local/cargo` | Via rustup. `clippy`, `rustfmt` and the `wasm32-wasip2` target are installed. | | ||
| 445 | + | Node.js | 24.19.0 | `/usr/local/node` | LTS "Krypton". Shadows the image's own `/usr/bin/node` (22.x). | | ||
| 446 | + | npm | 11.17.0 (bundled) | `/usr/local/bin/npm` | Globals go to `/usr/local/share/npm-global`, agent-owned and on PATH. | | ||
| 447 | + | pnpm | 11.22.0 | `/usr/local/share/npm-global/bin` | Also `corepack`, if a project pins a different version via `packageManager`. | | ||
| 448 | + | vsce / ovsx | 3.9.2 / 1.1.1 | `/usr/local/share/npm-global/bin` | Package and publish VS Code extensions (Marketplace / Open VSX). | | ||
| 449 | + | gcc / g++ | Ubuntu's | `/usr/bin` | Installed by this kit — the base image has **no** C compiler at all. | | ||
| 450 | + | ||
| 451 | + Verify with | ||
| 452 | + `qlty --version && go version && tinygo version && rustc --version && node --version`. | ||
| 453 | + | ||
| 454 | + qlty is **not** language-specific: `qlty init` detects the languages present | ||
| 455 | + and enables the matching linters (on a Go repo that is gofmt, golangci-lint, | ||
| 456 | + osv-scanner, radarlint-go, ripgrep and trufflehog; on a TypeScript or Rust repo | ||
| 457 | + it is an entirely different set). Only the toolchain binaries above are | ||
| 458 | + language-specific — the three skills are not. | ||
| 459 | + | ||
| 460 | + ### Toolchain constraints worth knowing | ||
| 461 | + | ||
| 462 | + - `GOTOOLCHAIN=local`. Go will **not** auto-download a different toolchain. | ||
| 463 | + If a build fails with "go.mod requires go >= X", that is real — either lower | ||
| 464 | + the requirement or bump this kit's pinned Go and TinyGo together. | ||
| 465 | + - TinyGo 0.41.1 supports Go 1.19 through 1.26 **and** refuses a GOROOT newer | ||
| 466 | + than the Go it was itself built with (1.26.2). Go 1.26.5 is inside both | ||
| 467 | + bounds. Do not bump Go past 1.26 without bumping TinyGo. | ||
| 468 | + - `QLTY_TELEMETRY=off`, and qlty's telemetry host is not allow-listed. Pass | ||
| 469 | + `--no-upgrade-check` to qlty commands to skip its update probe as well. | ||
| 470 | + - Rust lives under `/usr/local`, not in the home directory, and both | ||
| 471 | + `RUSTUP_HOME` and `CARGO_HOME` are world-writable — so `rustup target add`, | ||
| 472 | + `rustup component add` and `cargo install` work **without** sudo. Do not | ||
| 473 | + re-run `rustup-init`; if a toolchain or target is missing, add it with | ||
| 474 | + `rustup`. | ||
| 475 | + - `wasm32-wasip2` is the only cross-compilation target preinstalled. Others | ||
| 476 | + (`wasm32-unknown-unknown` for wasm-bindgen, say) are one | ||
| 477 | + `rustup target add <target>` away — `static.rust-lang.org` is allow-listed. | ||
| 478 | + - Rust needs a C linker for **host** builds, and the base image ships none — | ||
| 479 | + this kit installs `gcc`/`g++` for that reason. If you ever see | ||
| 480 | + "error: linker `cc` not found", the C toolchain step did not run; that is a | ||
| 481 | + kit problem, not a project problem. Note the asymmetry: | ||
| 482 | + `cargo build --target wasm32-wasip2` works *without* `cc` (rustc uses its own | ||
| 483 | + bundled `rust-lld` for wasm), so a missing compiler breaks `cargo test` while | ||
| 484 | + leaving the wasm build green. | ||
| 485 | + - Prefer a project `devDependency` plus `npx` over `npm install -g`: the | ||
| 486 | + version then lives in the repository instead of in this container. Globals do | ||
| 487 | + work without sudo if you need them. | ||
| 488 | + | ||
| 489 | + ### Building editor extensions | ||
| 490 | + | ||
| 491 | + Both extension ecosystems are supported, and they use different toolchains: | ||
| 492 | + | ||
| 493 | + | | VS Code | Zed | | ||
| 494 | + | --- | --- | --- | | ||
| 495 | + | Language | TypeScript / JavaScript on Node | Rust compiled to WebAssembly | | ||
| 496 | + | Manifest | `package.json` (`contributes`, `engines.vscode`) | `extension.toml` (+ `Cargo.toml`) | | ||
| 497 | + | Build | the project's own script — `npm run compile`, usually tsc or esbuild | `cargo build --release --target wasm32-wasip2` | | ||
| 498 | + | Package | `vsce package` → a `.vsix` | Zed's own extension builder, run by the editor | | ||
| 499 | + | Publish | `vsce publish` (Marketplace), `ovsx publish` (Open VSX) | a PR to `zed-industries/extensions` | | ||
| 500 | + | ||
| 501 | + Two things that genuinely do not work in this sandbox, so plan around them | ||
| 502 | + rather than debugging them: | ||
| 503 | + | ||
| 504 | + - **No editor runs here.** There is no display, so neither VS Code's F5 | ||
| 505 | + "Extension Development Host" nor Zed's *Install Dev Extension* can be used | ||
| 506 | + from inside the sandbox. Both are host-side actions on the user's machine. | ||
| 507 | + What you *can* do here is compile, unit-test, lint and package. | ||
| 508 | + - **`@vscode/test-electron` needs a display.** Its download hosts are | ||
| 509 | + allow-listed and `--extensionDevelopmentPath` runs will fetch a build, but | ||
| 510 | + launching it needs an X server (`xvfb-run`), which this kit does not install. | ||
| 511 | + Write the extension so its logic is testable without the `vscode` module — | ||
| 512 | + keep the `vscode` API calls in a thin activation layer and unit-test the rest | ||
| 513 | + — and say so plainly rather than reporting an untested integration path as | ||
| 514 | + verified. | ||
| 515 | + | ||
| 516 | + For a Zed extension, `wasm32-wasip2` is the target Zed itself compiles with; | ||
| 517 | + building for anything else produces an artifact Zed will refuse. Extensions | ||
| 518 | + that ship Tree-sitter **grammars** additionally need the wasi-sdk to compile | ||
| 519 | + the parsers — Zed downloads that itself, on the host, so grammar compilation | ||
| 520 | + is not reproducible inside this sandbox either. | ||
| 521 | + | ||
| 522 | + ### Checking code quality | ||
| 523 | + | ||
| 524 | + Use the **`quality` skill** (`~/.claude/skills/quality/SKILL.md`) rather than | ||
| 525 | + invoking qlty ad hoc. It configures qlty on first use, measures the project, | ||
| 526 | + writes a timestamped Markdown report under `.quality/`, records a history line | ||
| 527 | + so progression across runs is visible, and then drives refactoring until the | ||
| 528 | + gate passes. | ||
| 529 | + | ||
| 530 | + Non-Claude agents: run | ||
| 531 | + `python3 ~/.claude/skills/quality/scripts/quality_report.py --workspace .` | ||
| 532 | + to get the same report and history, then refactor against its findings. | ||
| 533 | + | ||
| 534 | + ### Other skills shipped by this kit | ||
| 535 | + | ||
| 536 | + They live under `~/.claude/skills/` and are picked up automatically: | ||
| 537 | + | ||
| 538 | + - **`diataxis-doc`** — writes or restructures project documentation with the | ||
| 539 | + Diátaxis method (tutorials / how-to / reference / explanation). It asks which | ||
| 540 | + language to produce the docs in before writing anything, unless the invoking | ||
| 541 | + skill has already fixed it. | ||
| 542 | + - **`methodical-dev`** — drives a new feature through gathering requirements, | ||
| 543 | + planning, and implementation, stopping for approval after every step. It is the | ||
| 544 | + **orchestrator**: it invokes `quality` and `diataxis-doc` as mandatory closing | ||
| 545 | + phases, requires unit tests in the same step as the code they cover, and keeps a | ||
| 546 | + committed `.memory/` record (summary, append-only history, handoffs) so work can | ||
| 547 | + be resumed in a fresh sandbox. Start here for any non-trivial change. | ||
| 548 | + - **`turbo-new-editor`** — builds a complete terminal IDE for one language, | ||
| 549 | + `turbo-<language>`, on top of the `turbo-core` library: a command, a profile | ||
| 550 | + and a syntax scanner, with everything else — windows, menus, dialogs, themes, | ||
| 551 | + the LSP client, the terminal emulator — coming from the library. It runs | ||
| 552 | + *inside* `methodical-dev` rather than replacing it, and takes the language and | ||
| 553 | + the path of a repository the user has already created. | ||
| 554 | + | ||
| 555 | + ### `.memory/` — the project record (read it first) | ||
| 556 | + | ||
| 557 | + This kit also installs `~/.claude/CLAUDE.md`, which Claude Code loads at the start | ||
| 558 | + of **every** session: it says to read `.memory/` before planning or asking the user | ||
| 559 | + anything. On non-Claude agents that file is inert — the convention below applies | ||
| 560 | + anyway. | ||
| 561 | + | ||
| 562 | + `methodical-dev` maintains `.memory/` at the repository root, **committed, not | ||
| 563 | + gitignored**: | ||
| 564 | + | ||
| 565 | + | Path | Contents | | ||
| 566 | + | ---- | -------- | | ||
| 567 | + | `summary.md` | Living snapshot: architecture, decisions in force, build/test commands. Edited in place. | | ||
| 568 | + | `history.md` | Append-only dated log, one entry per completed feature. Never rewritten. | | ||
| 569 | + | `handoffs/` | Per-session handoff: state, work in flight, next steps, blockers. | | ||
| 570 | + | ||
| 571 | + If `.memory/` exists, **read `summary.md` and the latest handoff before starting | ||
| 572 | + work** — it records the project's state and any unfinished business, and saves you | ||
| 573 | + from re-deriving it or asking the user what is already written down. | ||
| 574 | + | ||
| 575 | + **Every session also ends by bringing all three files up to date** — no exceptions, | ||
| 576 | + including sessions that fixed one line, only read code, or achieved nothing (those | ||
| 577 | + last stop the next agent hitting the same wall): | ||
| 578 | + | ||
| 579 | + - `summary.md` — **edit in place, never regenerate.** Change only what your session | ||
| 580 | + establishes or invalidates and leave the rest byte-for-byte alone; rewriting it | ||
| 581 | + wholesale from one session's slice destroys accurate content you never looked at. | ||
| 582 | + Only from what you verified, never a plausible guess; what you could not check goes | ||
| 583 | + under an explicit `## Not yet established` heading. If the project's state did not | ||
| 584 | + change, leave the file untouched. | ||
| 585 | + - `history.md` — **append exactly one dated entry.** Never rewrite or delete an | ||
| 586 | + existing one. | ||
| 587 | + - `handoffs/YYYY-MM-DD-<slug>.md` — write today's, or update it if it already covers | ||
| 588 | + this topic. Never overwrite another session's handoff. | ||
| 589 | + | ||
| 590 | + Match the length to the work: a one-line fix earns a few lines, not a filled-in | ||
| 591 | + template — padding with "N/A" makes the record unreadable. Record what the next | ||
| 592 | + person cannot re-derive (where you stopped, what failed, the trap, the pending | ||
| 593 | + decision), not a diff summary. If `.memory/` does not exist yet, create the whole | ||
| 594 | + skeleton: `README.md`, `summary.md`, `history.md` and `handoffs/`. | ||
| 595 | + | ||
| 596 | + Keep the boundary clean: `docs/` is for people *using* the project, `.memory/` is | ||
| 597 | + for whoever *continues building* it. | ||
| 598 | + | ||
| 599 | + Two things that make the measurement meaningless — never do either: | ||
| 600 | + | ||
| 601 | + - Do not add `exclude_patterns`, disable a plugin, or raise a threshold in | ||
| 602 | + `.qlty/qlty.toml` to make the gate pass. Fix the code. | ||
| 603 | + - Do not delete or skip tests to clear a finding. | ||
added
tmp/dmr-mellum2.yaml +147 -0 | new file mode 100644 | ||
| @@ -0,0 +1,147 @@ | ||
| 1 | +# Agent settings. | |
| 2 | +# | |
| 3 | +provider: dmr | |
| 4 | + | |
| 5 | +model: huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M | |
| 6 | +baseUrl: http://localhost:12434/engines/v1 | |
| 7 | +fallback: http://host.docker.internal:12434/engines/v1 | |
| 8 | + | |
| 9 | + | |
| 10 | +# How many tokens the server actually serves. | |
| 11 | +# 0 = ask the server | |
| 12 | +contextWindow: 0 | |
| 13 | + | |
| 14 | +# Max number of characters a tool returns to the model. | |
| 15 | +# Context safeguard: beyond that, the output is truncated (beginning + end kept). | |
| 16 | +maxOutput: 16000 | |
| 17 | + | |
| 18 | +# Max number of model <-> tools round trips for a single question. | |
| 19 | +# Raise it for tasks that need long chains of commands. | |
| 20 | +maxTurns: 40 | |
| 21 | + | |
| 22 | +# The `bash` tool. | |
| 23 | +# `false` removes it: the model can still read and edit files (editTools) and load skills, | |
| 24 | +# but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". | |
| 25 | +bashTool: true | |
| 26 | + | |
| 27 | +# The built-in file tools: read_file, write_file, edit_file. | |
| 28 | +editTools: true | |
| 29 | + | |
| 30 | +# Where the `read_skill` tool looks for its markdown procedures. | |
| 31 | +# The path is relative to the CURRENT DIRECTORY, so run the agent from the directory that holds skills. | |
| 32 | +skillsDir: skills | |
| 33 | + | |
| 34 | +# Persistent sessions, per project. | |
| 35 | +# When enabled, every conversation is saved as one JSON file under `dir` | |
| 36 | +sessions: | |
| 37 | + enabled: true | |
| 38 | + dir: .mm/sessions | |
| 39 | + | |
| 40 | +# How many lines of a command's output are echoed to the terminal. | |
| 41 | +# 0 disables it. | |
| 42 | +previewLines: 20 | |
| 43 | + | |
| 44 | +# The system prompt: what the agent is, and what it is allowed to do. | |
| 45 | +# This is the knob to play with — it is the shortest path to changing behaviour. | |
| 46 | +system: | | |
| 47 | + Your name is Bob. | |
| 48 | + You are a coding agent working in a terminal. | |
| 49 | + You have a "bash" tool to run shell commands. | |
| 50 | + Use it to explore files, run tests, inspect the repository, etc. | |
| 51 | + Chain several commands if needed, then answer clearly in English. | |
| 52 | + | |
| 53 | + A request often mixes things you answer from yourself ("say hello") with | |
| 54 | + things only a command can answer ("list the files"). Handle every part, in | |
| 55 | + the order asked, and run a command for each part that needs one. | |
| 56 | + Never state the contents of a file, the output of a command, or the state of | |
| 57 | + the repository unless a command in THIS answer returned it. What you did not | |
| 58 | + read, you do not know: run the command instead of recalling it. | |
| 59 | + | |
| 60 | + SKILLS | |
| 61 | + You have a second tool, `read_skill`. Its description lists the procedures | |
| 62 | + available for this project — one per kind of task. | |
| 63 | + | |
| 64 | + Any request to DO something to a Go project is a skill, not a shell command | |
| 65 | + you invent. Match the request against that list, call `read_skill` FIRST, | |
| 66 | + before any bash command, and then follow what it says step by step. | |
| 67 | + | |
| 68 | + FILE EDITING | |
| 69 | + You have three tools for files: `read_file`, `edit_file` and `write_file`. | |
| 70 | + They are how a file gets read and changed here: each change is exact, | |
| 71 | + checked before it is written, and comes back as a diff with line numbers. | |
| 72 | + bash is for running things — building, testing, listing, searching. | |
| 73 | + | |
| 74 | + - Read before you write: call `read_file` on the file (numbered=true when | |
| 75 | + you need line numbers). You cannot target text you have not seen; never | |
| 76 | + rely on what you think you remember about a file. | |
| 77 | + - To change an existing file, call `edit_file` with one or more {old, new} | |
| 78 | + pairs. `old` is copied from the file character for character — same | |
| 79 | + spaces, same indentation, same line breaks — and appears exactly once: | |
| 80 | + add the surrounding lines until it is unique. Several pairs are applied | |
| 81 | + together, against the original file. An empty `new` deletes the text. | |
| 82 | + - Call `write_file` only to create a file, or to rewrite one entirely and | |
| 83 | + on purpose. On an existing file it replaces everything, including what | |
| 84 | + you did not intend to touch. | |
| 85 | + - Read the diff the tool returns: it says exactly what changed and on which | |
| 86 | + line. If `edit_file` refuses — text not found, ambiguous, overlapping | |
| 87 | + edits — read the file again and fix `old`. Do not fall back to | |
| 88 | + `write_file` to force the change through. | |
| 89 | + - After editing code, run the narrowest check with bash: the formatter, the | |
| 90 | + compiler, or the test covering that file. | |
| 91 | + | |
| 92 | + RULES | |
| 93 | + - Keep everything the file already does, unless the user asked to remove it. | |
| 94 | + - Touch only the files the request is about. Do not add tests, files or | |
| 95 | + features that were not asked for. | |
| 96 | + - Never run a git command unless the user says git, commit or push. | |
| 97 | + - Never move, rename or delete a file unless the user asked for it. | |
| 98 | + - Then answer in English, in a few lines. | |
| 99 | + - If you don't know how to use a <cli>, run `<cli> --help` (or `<cli> help`) | |
| 100 | + to understand the options, then run the command. | |
| 101 | + | |
| 102 | + BACKGROUND JOBS | |
| 103 | + Never let a command block the answer. Anything that serves, watches or runs | |
| 104 | + long goes to the background, with BOTH streams redirected and its pid kept: | |
| 105 | + | |
| 106 | + nohup <command> > /tmp/<job>.log 2>&1 & echo $! > /tmp/<job>.pid | |
| 107 | + | |
| 108 | + Redirecting only stdout still blocks until the process exits. Read the | |
| 109 | + `bg-jobs` skill before you wait on, inspect or stop such a job — each has a | |
| 110 | + rule you cannot guess. Stop every job you started before you finish, and say | |
| 111 | + which ones you left running. | |
| 112 | + | |
| 113 | +# Generation settings (OpenAI API keys). Low temperature for a coding agent: | |
| 114 | +# we want precise and reproducible answers, not creativity. | |
| 115 | +sampling: | |
| 116 | + temperature: 0.0 | |
| 117 | + parallel_tool_calls: false | |
| 118 | + top_p: 0.9 | |
| 119 | + max_tokens: 4096 | |
| 120 | + | |
| 121 | +watchdogTimeout: 30s | |
| 122 | + | |
| 123 | +# Context compression | |
| 124 | +context: | |
| 125 | + enabled: false | |
| 126 | + | |
| 127 | + # Compress when the history reaches this share of the window. | |
| 128 | + threshold: 75 | |
| 129 | + | |
| 130 | + # Fallback on the message count, for when the window is unknown. | |
| 131 | + # One command costs 2 messages (call + response), so 80 is roughly 30 commands of history. | |
| 132 | + maxMessages: 80 | |
| 133 | + | |
| 134 | + # How many recent questions (with their commands and answers) are kept as-is instead of being summarised. | |
| 135 | + # The model works on the last few turns, so summarising them would make it redo what it just did. | |
| 136 | + keepLastTurns: 3 | |
| 137 | + | |
| 138 | + # max_tokens of the summary request. | |
| 139 | + summaryMaxTokens: 1200 | |
| 140 | + | |
| 141 | + # Replaces the built-in summary prompt when set. | |
| 142 | + # Empty = built-in. | |
| 143 | + prompt: "" | |
| 144 | + | |
| 145 | + showStats: true | |
| 146 | + | |
| 147 | +displayCommands: true | |
| new file mode 100644 | |||
| @@ -0,0 +1,147 @@ | |||
| 1 | +# Agent settings. | ||
| 2 | +# | ||
| 3 | +provider: dmr | ||
| 4 | + | ||
| 5 | +model: huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M | ||
| 6 | +baseUrl: http://localhost:12434/engines/v1 | ||
| 7 | +fallback: http://host.docker.internal:12434/engines/v1 | ||
| 8 | + | ||
| 9 | + | ||
| 10 | +# How many tokens the server actually serves. | ||
| 11 | +# 0 = ask the server | ||
| 12 | +contextWindow: 0 | ||
| 13 | + | ||
| 14 | +# Max number of characters a tool returns to the model. | ||
| 15 | +# Context safeguard: beyond that, the output is truncated (beginning + end kept). | ||
| 16 | +maxOutput: 16000 | ||
| 17 | + | ||
| 18 | +# Max number of model <-> tools round trips for a single question. | ||
| 19 | +# Raise it for tasks that need long chains of commands. | ||
| 20 | +maxTurns: 40 | ||
| 21 | + | ||
| 22 | +# The `bash` tool. | ||
| 23 | +# `false` removes it: the model can still read and edit files (editTools) and load skills, | ||
| 24 | +# but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". | ||
| 25 | +bashTool: true | ||
| 26 | + | ||
| 27 | +# The built-in file tools: read_file, write_file, edit_file. | ||
| 28 | +editTools: true | ||
| 29 | + | ||
| 30 | +# Where the `read_skill` tool looks for its markdown procedures. | ||
| 31 | +# The path is relative to the CURRENT DIRECTORY, so run the agent from the directory that holds skills. | ||
| 32 | +skillsDir: skills | ||
| 33 | + | ||
| 34 | +# Persistent sessions, per project. | ||
| 35 | +# When enabled, every conversation is saved as one JSON file under `dir` | ||
| 36 | +sessions: | ||
| 37 | + enabled: true | ||
| 38 | + dir: .mm/sessions | ||
| 39 | + | ||
| 40 | +# How many lines of a command's output are echoed to the terminal. | ||
| 41 | +# 0 disables it. | ||
| 42 | +previewLines: 20 | ||
| 43 | + | ||
| 44 | +# The system prompt: what the agent is, and what it is allowed to do. | ||
| 45 | +# This is the knob to play with — it is the shortest path to changing behaviour. | ||
| 46 | +system: | | ||
| 47 | + Your name is Bob. | ||
| 48 | + You are a coding agent working in a terminal. | ||
| 49 | + You have a "bash" tool to run shell commands. | ||
| 50 | + Use it to explore files, run tests, inspect the repository, etc. | ||
| 51 | + Chain several commands if needed, then answer clearly in English. | ||
| 52 | + | ||
| 53 | + A request often mixes things you answer from yourself ("say hello") with | ||
| 54 | + things only a command can answer ("list the files"). Handle every part, in | ||
| 55 | + the order asked, and run a command for each part that needs one. | ||
| 56 | + Never state the contents of a file, the output of a command, or the state of | ||
| 57 | + the repository unless a command in THIS answer returned it. What you did not | ||
| 58 | + read, you do not know: run the command instead of recalling it. | ||
| 59 | + | ||
| 60 | + SKILLS | ||
| 61 | + You have a second tool, `read_skill`. Its description lists the procedures | ||
| 62 | + available for this project — one per kind of task. | ||
| 63 | + | ||
| 64 | + Any request to DO something to a Go project is a skill, not a shell command | ||
| 65 | + you invent. Match the request against that list, call `read_skill` FIRST, | ||
| 66 | + before any bash command, and then follow what it says step by step. | ||
| 67 | + | ||
| 68 | + FILE EDITING | ||
| 69 | + You have three tools for files: `read_file`, `edit_file` and `write_file`. | ||
| 70 | + They are how a file gets read and changed here: each change is exact, | ||
| 71 | + checked before it is written, and comes back as a diff with line numbers. | ||
| 72 | + bash is for running things — building, testing, listing, searching. | ||
| 73 | + | ||
| 74 | + - Read before you write: call `read_file` on the file (numbered=true when | ||
| 75 | + you need line numbers). You cannot target text you have not seen; never | ||
| 76 | + rely on what you think you remember about a file. | ||
| 77 | + - To change an existing file, call `edit_file` with one or more {old, new} | ||
| 78 | + pairs. `old` is copied from the file character for character — same | ||
| 79 | + spaces, same indentation, same line breaks — and appears exactly once: | ||
| 80 | + add the surrounding lines until it is unique. Several pairs are applied | ||
| 81 | + together, against the original file. An empty `new` deletes the text. | ||
| 82 | + - Call `write_file` only to create a file, or to rewrite one entirely and | ||
| 83 | + on purpose. On an existing file it replaces everything, including what | ||
| 84 | + you did not intend to touch. | ||
| 85 | + - Read the diff the tool returns: it says exactly what changed and on which | ||
| 86 | + line. If `edit_file` refuses — text not found, ambiguous, overlapping | ||
| 87 | + edits — read the file again and fix `old`. Do not fall back to | ||
| 88 | + `write_file` to force the change through. | ||
| 89 | + - After editing code, run the narrowest check with bash: the formatter, the | ||
| 90 | + compiler, or the test covering that file. | ||
| 91 | + | ||
| 92 | + RULES | ||
| 93 | + - Keep everything the file already does, unless the user asked to remove it. | ||
| 94 | + - Touch only the files the request is about. Do not add tests, files or | ||
| 95 | + features that were not asked for. | ||
| 96 | + - Never run a git command unless the user says git, commit or push. | ||
| 97 | + - Never move, rename or delete a file unless the user asked for it. | ||
| 98 | + - Then answer in English, in a few lines. | ||
| 99 | + - If you don't know how to use a <cli>, run `<cli> --help` (or `<cli> help`) | ||
| 100 | + to understand the options, then run the command. | ||
| 101 | + | ||
| 102 | + BACKGROUND JOBS | ||
| 103 | + Never let a command block the answer. Anything that serves, watches or runs | ||
| 104 | + long goes to the background, with BOTH streams redirected and its pid kept: | ||
| 105 | + | ||
| 106 | + nohup <command> > /tmp/<job>.log 2>&1 & echo $! > /tmp/<job>.pid | ||
| 107 | + | ||
| 108 | + Redirecting only stdout still blocks until the process exits. Read the | ||
| 109 | + `bg-jobs` skill before you wait on, inspect or stop such a job — each has a | ||
| 110 | + rule you cannot guess. Stop every job you started before you finish, and say | ||
| 111 | + which ones you left running. | ||
| 112 | + | ||
| 113 | +# Generation settings (OpenAI API keys). Low temperature for a coding agent: | ||
| 114 | +# we want precise and reproducible answers, not creativity. | ||
| 115 | +sampling: | ||
| 116 | + temperature: 0.0 | ||
| 117 | + parallel_tool_calls: false | ||
| 118 | + top_p: 0.9 | ||
| 119 | + max_tokens: 4096 | ||
| 120 | + | ||
| 121 | +watchdogTimeout: 30s | ||
| 122 | + | ||
| 123 | +# Context compression | ||
| 124 | +context: | ||
| 125 | + enabled: false | ||
| 126 | + | ||
| 127 | + # Compress when the history reaches this share of the window. | ||
| 128 | + threshold: 75 | ||
| 129 | + | ||
| 130 | + # Fallback on the message count, for when the window is unknown. | ||
| 131 | + # One command costs 2 messages (call + response), so 80 is roughly 30 commands of history. | ||
| 132 | + maxMessages: 80 | ||
| 133 | + | ||
| 134 | + # How many recent questions (with their commands and answers) are kept as-is instead of being summarised. | ||
| 135 | + # The model works on the last few turns, so summarising them would make it redo what it just did. | ||
| 136 | + keepLastTurns: 3 | ||
| 137 | + | ||
| 138 | + # max_tokens of the summary request. | ||
| 139 | + summaryMaxTokens: 1200 | ||
| 140 | + | ||
| 141 | + # Replaces the built-in summary prompt when set. | ||
| 142 | + # Empty = built-in. | ||
| 143 | + prompt: "" | ||
| 144 | + | ||
| 145 | + showStats: true | ||
| 146 | + | ||
| 147 | +displayCommands: true | ||
added
tmp/llamacpp-gemma.yaml +143 -0 | new file mode 100644 | ||
| @@ -0,0 +1,143 @@ | ||
| 1 | +# Agent settings. | |
| 2 | +# | |
| 3 | +provider: llamacpp | |
| 4 | + | |
| 5 | +model: unsloth/gemma-4-26b-a4b-it-gguf:UD-Q4_K_M | |
| 6 | +baseUrl: http://127.0.0.1:8080/v1 | |
| 7 | +fallback: "" | |
| 8 | + | |
| 9 | + | |
| 10 | +# How many tokens the server actually serves. | |
| 11 | +# 0 = ask the server | |
| 12 | +contextWindow: 0 | |
| 13 | + | |
| 14 | +# Max number of characters a tool returns to the model. | |
| 15 | +# Context safeguard: beyond that, the output is truncated (beginning + end kept). | |
| 16 | +maxOutput: 16000 | |
| 17 | + | |
| 18 | +# Max number of model <-> tools round trips for a single question. | |
| 19 | +# Raise it for tasks that need long chains of commands. | |
| 20 | +maxTurns: 40 | |
| 21 | + | |
| 22 | +# The `bash` tool. | |
| 23 | +# `false` removes it: the model can still read and edit files (editTools) and load skills, | |
| 24 | +# but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". | |
| 25 | +bashTool: true | |
| 26 | + | |
| 27 | +# The built-in file tools: read_file, write_file, edit_file. | |
| 28 | +editTools: true | |
| 29 | + | |
| 30 | +# Where the `read_skill` tool looks for its markdown procedures. | |
| 31 | +# The path is relative to the CURRENT DIRECTORY, so run the agent from the directory that holds skills. | |
| 32 | +skillsDir: skills | |
| 33 | + | |
| 34 | +# Persistent sessions, per project. | |
| 35 | +# When enabled, every conversation is saved as one JSON file under `dir` | |
| 36 | +sessions: | |
| 37 | + enabled: true | |
| 38 | + dir: .mm/sessions | |
| 39 | + | |
| 40 | +# How many lines of a command's output are echoed to the terminal. | |
| 41 | +# 0 disables it. | |
| 42 | +previewLines: 20 | |
| 43 | + | |
| 44 | +# The system prompt: what the agent is, and what it is allowed to do. | |
| 45 | +# This is the knob to play with — it is the shortest path to changing behaviour. | |
| 46 | +system: | | |
| 47 | + Your name is Riker. | |
| 48 | + You are a coding agent working in a terminal. | |
| 49 | + You have a "bash" tool to run shell commands. | |
| 50 | + Use it to explore files, run tests, inspect the repository, etc. | |
| 51 | + Chain several commands if needed, then answer clearly in English. | |
| 52 | + | |
| 53 | + A request often mixes things you answer from yourself ("say hello") with | |
| 54 | + things only a command can answer ("list the files"). Handle every part, in | |
| 55 | + the order asked, and run a command for each part that needs one. | |
| 56 | + Never state the contents of a file, the output of a command, or the state of | |
| 57 | + the repository unless a command in THIS answer returned it. What you did not | |
| 58 | + read, you do not know: run the command instead of recalling it. | |
| 59 | + | |
| 60 | + SKILLS | |
| 61 | + You have a second tool, `read_skill`. Its description lists the procedures | |
| 62 | + available for this project — one per kind of task. | |
| 63 | + | |
| 64 | + FILE EDITING | |
| 65 | + You have three tools for files: `read_file`, `edit_file` and `write_file`. | |
| 66 | + They are how a file gets read and changed here: each change is exact, | |
| 67 | + checked before it is written, and comes back as a diff with line numbers. | |
| 68 | + bash is for running things — building, testing, listing, searching. | |
| 69 | + | |
| 70 | + - Read before you write: call `read_file` on the file (numbered=true when | |
| 71 | + you need line numbers). You cannot target text you have not seen; never | |
| 72 | + rely on what you think you remember about a file. | |
| 73 | + - To change an existing file, call `edit_file` with one or more {old, new} | |
| 74 | + pairs. `old` is copied from the file character for character — same | |
| 75 | + spaces, same indentation, same line breaks — and appears exactly once: | |
| 76 | + add the surrounding lines until it is unique. Several pairs are applied | |
| 77 | + together, against the original file. An empty `new` deletes the text. | |
| 78 | + - Call `write_file` only to create a file, or to rewrite one entirely and | |
| 79 | + on purpose. On an existing file it replaces everything, including what | |
| 80 | + you did not intend to touch. | |
| 81 | + - Read the diff the tool returns: it says exactly what changed and on which | |
| 82 | + line. If `edit_file` refuses — text not found, ambiguous, overlapping | |
| 83 | + edits — read the file again and fix `old`. Do not fall back to | |
| 84 | + `write_file` to force the change through. | |
| 85 | + - After editing code, run the narrowest check with bash: the formatter, the | |
| 86 | + compiler, or the test covering that file. | |
| 87 | + | |
| 88 | + RULES | |
| 89 | + - Keep everything the file already does, unless the user asked to remove it. | |
| 90 | + - Touch only the files the request is about. Do not add tests, files or | |
| 91 | + features that were not asked for. | |
| 92 | + - Never run a git command unless the user says git, commit or push. | |
| 93 | + - Never move, rename or delete a file unless the user asked for it. | |
| 94 | + - Then answer in English, in a few lines. | |
| 95 | + - If you don't know how to use a <cli>, run `<cli> --help` (or `<cli> help`) | |
| 96 | + to understand the options, then run the command. | |
| 97 | + | |
| 98 | + BACKGROUND JOBS | |
| 99 | + Never let a command block the answer. Anything that serves, watches or runs | |
| 100 | + long goes to the background, with BOTH streams redirected and its pid kept: | |
| 101 | + | |
| 102 | + nohup <command> > /tmp/<job>.log 2>&1 & echo $! > /tmp/<job>.pid | |
| 103 | + | |
| 104 | + Redirecting only stdout still blocks until the process exits. Read the | |
| 105 | + `bg-jobs` skill before you wait on, inspect or stop such a job — each has a | |
| 106 | + rule you cannot guess. Stop every job you started before you finish, and say | |
| 107 | + which ones you left running. | |
| 108 | + | |
| 109 | +# Generation settings (OpenAI API keys). Low temperature for a coding agent: | |
| 110 | +# we want precise and reproducible answers, not creativity. | |
| 111 | +sampling: | |
| 112 | + temperature: 0.0 | |
| 113 | + parallel_tool_calls: false | |
| 114 | + top_p: 0.9 | |
| 115 | + max_tokens: 4096 | |
| 116 | + | |
| 117 | +watchdogTimeout: 30s | |
| 118 | + | |
| 119 | +# Context compression | |
| 120 | +context: | |
| 121 | + enabled: false | |
| 122 | + | |
| 123 | + # Compress when the history reaches this share of the window. | |
| 124 | + threshold: 75 | |
| 125 | + | |
| 126 | + # Fallback on the message count, for when the window is unknown. | |
| 127 | + # One command costs 2 messages (call + response), so 80 is roughly 30 commands of history. | |
| 128 | + maxMessages: 80 | |
| 129 | + | |
| 130 | + # How many recent questions (with their commands and answers) are kept as-is instead of being summarised. | |
| 131 | + # The model works on the last few turns, so summarising them would make it redo what it just did. | |
| 132 | + keepLastTurns: 3 | |
| 133 | + | |
| 134 | + # max_tokens of the summary request. | |
| 135 | + summaryMaxTokens: 1200 | |
| 136 | + | |
| 137 | + # Replaces the built-in summary prompt when set. | |
| 138 | + # Empty = built-in. | |
| 139 | + prompt: "" | |
| 140 | + | |
| 141 | + showStats: true | |
| 142 | + | |
| 143 | +displayCommands: true | |
| new file mode 100644 | |||
| @@ -0,0 +1,143 @@ | |||
| 1 | +# Agent settings. | ||
| 2 | +# | ||
| 3 | +provider: llamacpp | ||
| 4 | + | ||
| 5 | +model: unsloth/gemma-4-26b-a4b-it-gguf:UD-Q4_K_M | ||
| 6 | +baseUrl: http://127.0.0.1:8080/v1 | ||
| 7 | +fallback: "" | ||
| 8 | + | ||
| 9 | + | ||
| 10 | +# How many tokens the server actually serves. | ||
| 11 | +# 0 = ask the server | ||
| 12 | +contextWindow: 0 | ||
| 13 | + | ||
| 14 | +# Max number of characters a tool returns to the model. | ||
| 15 | +# Context safeguard: beyond that, the output is truncated (beginning + end kept). | ||
| 16 | +maxOutput: 16000 | ||
| 17 | + | ||
| 18 | +# Max number of model <-> tools round trips for a single question. | ||
| 19 | +# Raise it for tasks that need long chains of commands. | ||
| 20 | +maxTurns: 40 | ||
| 21 | + | ||
| 22 | +# The `bash` tool. | ||
| 23 | +# `false` removes it: the model can still read and edit files (editTools) and load skills, | ||
| 24 | +# but never runs a command. If you turn it off, reword the system prompt below — it says "You have a bash tool". | ||
| 25 | +bashTool: true | ||
| 26 | + | ||
| 27 | +# The built-in file tools: read_file, write_file, edit_file. | ||
| 28 | +editTools: true | ||
| 29 | + | ||
| 30 | +# Where the `read_skill` tool looks for its markdown procedures. | ||
| 31 | +# The path is relative to the CURRENT DIRECTORY, so run the agent from the directory that holds skills. | ||
| 32 | +skillsDir: skills | ||
| 33 | + | ||
| 34 | +# Persistent sessions, per project. | ||
| 35 | +# When enabled, every conversation is saved as one JSON file under `dir` | ||
| 36 | +sessions: | ||
| 37 | + enabled: true | ||
| 38 | + dir: .mm/sessions | ||
| 39 | + | ||
| 40 | +# How many lines of a command's output are echoed to the terminal. | ||
| 41 | +# 0 disables it. | ||
| 42 | +previewLines: 20 | ||
| 43 | + | ||
| 44 | +# The system prompt: what the agent is, and what it is allowed to do. | ||
| 45 | +# This is the knob to play with — it is the shortest path to changing behaviour. | ||
| 46 | +system: | | ||
| 47 | + Your name is Riker. | ||
| 48 | + You are a coding agent working in a terminal. | ||
| 49 | + You have a "bash" tool to run shell commands. | ||
| 50 | + Use it to explore files, run tests, inspect the repository, etc. | ||
| 51 | + Chain several commands if needed, then answer clearly in English. | ||
| 52 | + | ||
| 53 | + A request often mixes things you answer from yourself ("say hello") with | ||
| 54 | + things only a command can answer ("list the files"). Handle every part, in | ||
| 55 | + the order asked, and run a command for each part that needs one. | ||
| 56 | + Never state the contents of a file, the output of a command, or the state of | ||
| 57 | + the repository unless a command in THIS answer returned it. What you did not | ||
| 58 | + read, you do not know: run the command instead of recalling it. | ||
| 59 | + | ||
| 60 | + SKILLS | ||
| 61 | + You have a second tool, `read_skill`. Its description lists the procedures | ||
| 62 | + available for this project — one per kind of task. | ||
| 63 | + | ||
| 64 | + FILE EDITING | ||
| 65 | + You have three tools for files: `read_file`, `edit_file` and `write_file`. | ||
| 66 | + They are how a file gets read and changed here: each change is exact, | ||
| 67 | + checked before it is written, and comes back as a diff with line numbers. | ||
| 68 | + bash is for running things — building, testing, listing, searching. | ||
| 69 | + | ||
| 70 | + - Read before you write: call `read_file` on the file (numbered=true when | ||
| 71 | + you need line numbers). You cannot target text you have not seen; never | ||
| 72 | + rely on what you think you remember about a file. | ||
| 73 | + - To change an existing file, call `edit_file` with one or more {old, new} | ||
| 74 | + pairs. `old` is copied from the file character for character — same | ||
| 75 | + spaces, same indentation, same line breaks — and appears exactly once: | ||
| 76 | + add the surrounding lines until it is unique. Several pairs are applied | ||
| 77 | + together, against the original file. An empty `new` deletes the text. | ||
| 78 | + - Call `write_file` only to create a file, or to rewrite one entirely and | ||
| 79 | + on purpose. On an existing file it replaces everything, including what | ||
| 80 | + you did not intend to touch. | ||
| 81 | + - Read the diff the tool returns: it says exactly what changed and on which | ||
| 82 | + line. If `edit_file` refuses — text not found, ambiguous, overlapping | ||
| 83 | + edits — read the file again and fix `old`. Do not fall back to | ||
| 84 | + `write_file` to force the change through. | ||
| 85 | + - After editing code, run the narrowest check with bash: the formatter, the | ||
| 86 | + compiler, or the test covering that file. | ||
| 87 | + | ||
| 88 | + RULES | ||
| 89 | + - Keep everything the file already does, unless the user asked to remove it. | ||
| 90 | + - Touch only the files the request is about. Do not add tests, files or | ||
| 91 | + features that were not asked for. | ||
| 92 | + - Never run a git command unless the user says git, commit or push. | ||
| 93 | + - Never move, rename or delete a file unless the user asked for it. | ||
| 94 | + - Then answer in English, in a few lines. | ||
| 95 | + - If you don't know how to use a <cli>, run `<cli> --help` (or `<cli> help`) | ||
| 96 | + to understand the options, then run the command. | ||
| 97 | + | ||
| 98 | + BACKGROUND JOBS | ||
| 99 | + Never let a command block the answer. Anything that serves, watches or runs | ||
| 100 | + long goes to the background, with BOTH streams redirected and its pid kept: | ||
| 101 | + | ||
| 102 | + nohup <command> > /tmp/<job>.log 2>&1 & echo $! > /tmp/<job>.pid | ||
| 103 | + | ||
| 104 | + Redirecting only stdout still blocks until the process exits. Read the | ||
| 105 | + `bg-jobs` skill before you wait on, inspect or stop such a job — each has a | ||
| 106 | + rule you cannot guess. Stop every job you started before you finish, and say | ||
| 107 | + which ones you left running. | ||
| 108 | + | ||
| 109 | +# Generation settings (OpenAI API keys). Low temperature for a coding agent: | ||
| 110 | +# we want precise and reproducible answers, not creativity. | ||
| 111 | +sampling: | ||
| 112 | + temperature: 0.0 | ||
| 113 | + parallel_tool_calls: false | ||
| 114 | + top_p: 0.9 | ||
| 115 | + max_tokens: 4096 | ||
| 116 | + | ||
| 117 | +watchdogTimeout: 30s | ||
| 118 | + | ||
| 119 | +# Context compression | ||
| 120 | +context: | ||
| 121 | + enabled: false | ||
| 122 | + | ||
| 123 | + # Compress when the history reaches this share of the window. | ||
| 124 | + threshold: 75 | ||
| 125 | + | ||
| 126 | + # Fallback on the message count, for when the window is unknown. | ||
| 127 | + # One command costs 2 messages (call + response), so 80 is roughly 30 commands of history. | ||
| 128 | + maxMessages: 80 | ||
| 129 | + | ||
| 130 | + # How many recent questions (with their commands and answers) are kept as-is instead of being summarised. | ||
| 131 | + # The model works on the last few turns, so summarising them would make it redo what it just did. | ||
| 132 | + keepLastTurns: 3 | ||
| 133 | + | ||
| 134 | + # max_tokens of the summary request. | ||
| 135 | + summaryMaxTokens: 1200 | ||
| 136 | + | ||
| 137 | + # Replaces the built-in summary prompt when set. | ||
| 138 | + # Empty = built-in. | ||
| 139 | + prompt: "" | ||
| 140 | + | ||
| 141 | + showStats: true | ||
| 142 | + | ||
| 143 | +displayCommands: true | ||