Log errors into sessions #1
Merged@k33g wants to merge
feature/log-errors into mainMerged as
7eed80dd3947.29 files changed
added
.claude/CLAUDE.md +80 -0 | new file mode 100644 | ||
| @@ -0,0 +1,80 @@ | ||
| 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 | +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. | |
| 79 | + | |
| 80 | +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,80 @@ | |||
| 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 | +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. | ||
| 79 | + | ||
| 80 | +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 Large diff collapsed to keep the page fast — load it when you need it.
added
.claude/skills/ebook-builder/SKILL.md +110 -0 | new file mode 100644 | ||
| @@ -0,0 +1,110 @@ | ||
| 1 | +--- | |
| 2 | +name: ebook-builder | |
| 3 | +description: Create, generate and update an AsciiDoc e-book from a per-chapter project (one directory per chapter). Produces one chapter file per project chapter, a master document, diagrams, and builds to HTML/PDF. Use when asked to build/refresh an e-book or PDF book from a step-by-step project. | |
| 4 | +--- | |
| 5 | + | |
| 6 | +# ebook-builder | |
| 7 | + | |
| 8 | +Turn a project organised as **one directory per chapter** (`01-…/`, `02-…/`, each with a `README.md`) into a coherent **AsciiDoc e-book** that builds to **HTML and PDF**. The book is **curated and synthesised** from the chapters — not a raw concatenation of READMEs: it reads as a narrative with a consistent voice. | |
| 9 | + | |
| 10 | +## When to use | |
| 11 | + | |
| 12 | +- "Make an e-book / a PDF book out of this project." | |
| 13 | +- "Update the book after I changed / added a chapter." | |
| 14 | + | |
| 15 | +## Dependencies | |
| 16 | + | |
| 17 | +Nothing local is required if Docker is available (the build script uses the official `asciidoctor/docker-asciidoctor` image). To build without Docker: `gem install asciidoctor asciidoctor-pdf rouge`. | |
| 18 | + | |
| 19 | +## Layout | |
| 20 | + | |
| 21 | +```text | |
| 22 | +ebook/ | |
| 23 | +├── book.adoc ← master document: attributes + include::chapters/* | |
| 24 | +├── chapters/ | |
| 25 | +│ ├── 00-introduction.adoc | |
| 26 | +│ ├── 01-….adoc ← one per project chapter (curated from its README) | |
| 27 | +│ └── 99-conclusion.adoc | |
| 28 | +├── images/ | |
| 29 | +│ ├── *.drawio ← editable diagram sources | |
| 30 | +│ └── *.svg ← exported diagrams used by the book | |
| 31 | +├── Makefile ← optional convenience (pdf/html/docker-pdf) | |
| 32 | +├── README.adoc ← how to build/edit | |
| 33 | +└── build/ ← output (git-ignored) | |
| 34 | +``` | |
| 35 | + | |
| 36 | +## The master `book.adoc` | |
| 37 | + | |
| 38 | +Standard AsciiDoc book attributes, then one `include::` per chapter in order: | |
| 39 | + | |
| 40 | +```asciidoc | |
| 41 | += Book title | |
| 42 | +:doctype: book | |
| 43 | +:toc: left | |
| 44 | +:toclevels: 3 | |
| 45 | +:sectnums: | |
| 46 | +:icons: font | |
| 47 | +:source-highlighter: rouge | |
| 48 | +:imagesdir: images | |
| 49 | + | |
| 50 | +[preface] | |
| 51 | +== Foreword | |
| 52 | +… | |
| 53 | + | |
| 54 | +include::chapters/00-introduction.adoc[] | |
| 55 | +include::chapters/01-….adoc[] | |
| 56 | +… | |
| 57 | +include::chapters/99-conclusion.adoc[] | |
| 58 | +``` | |
| 59 | + | |
| 60 | +## Workflow | |
| 61 | + | |
| 62 | +### 1. Read the project | |
| 63 | + | |
| 64 | +List the chapter directories and read each `README.md`. Note, per chapter: the need it answers, the key config/code, the diagrams, the "what you retain". | |
| 65 | + | |
| 66 | +### 2. Scaffold (only if `ebook/` doesn't exist) | |
| 67 | + | |
| 68 | +Create the layout above: `book.adoc`, `chapters/`, `images/`, and (optionally) a `Makefile` + `README.adoc`. Add an `00-introduction` (hook + how the book is built) and a `99-conclusion`. | |
| 69 | + | |
| 70 | +### 3. Write one chapter file per project chapter | |
| 71 | + | |
| 72 | +Each `chapters/NN-….adoc` is **synthesised** from the matching README, in the book's voice. A reliable per-chapter shape: | |
| 73 | + | |
| 74 | +- `== <chapter title>` | |
| 75 | +- `=== À quoi on répond` — the need. | |
| 76 | +- `=== La configuration` / the key code, in a `[source,<lang>]` block (colourised via rouge). | |
| 77 | +- `=== Pourquoi` — the explanation. | |
| 78 | +- optional `=== Prompts d'essai` / a diagram (`image::name.svg[…]`). | |
| 79 | +- `=== Ce qu'on retient` — a short bullet recap. | |
| 80 | + | |
| 81 | +**Never hard-wrap prose in the `.adoc` files.** Write each paragraph, and each list item, as **one single physical line** — never insert a line break in the middle of a sentence to fit a column width. Let the editor soft-wrap. This keeps diffs meaningful (a reworded sentence touches one line) and avoids stray breaks. Leave code blocks (`----`), tables (`|===`), headings, block macros (`image::`), and admonition delimiters (`====`) as they are — the rule is about *prose*, not block structure. When editing a chapter that was hard-wrapped, unwrap the paragraphs you touch. | |
| 82 | + | |
| 83 | +### 4. Diagrams | |
| 84 | + | |
| 85 | +Author diagrams as `.drawio` (editable) + export `.svg` (used by the book), kept in `images/`. Hand-authored SVGs are fine too. After writing, sanity-check the XML parses. | |
| 86 | + | |
| 87 | +### 5. Build | |
| 88 | + | |
| 89 | +```bash | |
| 90 | +bash .claude/skills/ebook-builder/scripts/build_ebook.sh ebook # html + pdf | |
| 91 | +bash .claude/skills/ebook-builder/scripts/build_ebook.sh ebook html # html only | |
| 92 | +``` | |
| 93 | + | |
| 94 | +Confirm it builds with no errors and the diagrams appear. | |
| 95 | + | |
| 96 | +### 6. Update | |
| 97 | + | |
| 98 | +- Chapter changed → update its `.adoc`. | |
| 99 | +- Chapter added → add `chapters/NN-….adoc` **and** an `include::` line in `book.adoc` (in order). | |
| 100 | +- Chapter removed → delete the file and its `include::`. | |
| 101 | +- Re-build to verify. | |
| 102 | + | |
| 103 | +## Rules | |
| 104 | + | |
| 105 | +- **Synthesise, don't concatenate.** The book is a narrative, not a README dump. | |
| 106 | +- **Keep chapter order** aligned with the numbered directories. | |
| 107 | +- **Colourise code** with `[source,<lang>]` blocks. | |
| 108 | +- **Diagrams as drawio + svg**, referenced from the chapters. | |
| 109 | +- **Don't hard-wrap prose (hard rule).** One single physical line per paragraph and per list item in the `.adoc` files — no mid-sentence line breaks. Only block structure (code, tables, headings, macros, `====`) keeps its own lines. | |
| 110 | +- **Build before finishing**, and keep `build/` out of git. | |
| new file mode 100644 | |||
| @@ -0,0 +1,110 @@ | |||
| 1 | +--- | ||
| 2 | +name: ebook-builder | ||
| 3 | +description: Create, generate and update an AsciiDoc e-book from a per-chapter project (one directory per chapter). Produces one chapter file per project chapter, a master document, diagrams, and builds to HTML/PDF. Use when asked to build/refresh an e-book or PDF book from a step-by-step project. | ||
| 4 | +--- | ||
| 5 | + | ||
| 6 | +# ebook-builder | ||
| 7 | + | ||
| 8 | +Turn a project organised as **one directory per chapter** (`01-…/`, `02-…/`, each with a `README.md`) into a coherent **AsciiDoc e-book** that builds to **HTML and PDF**. The book is **curated and synthesised** from the chapters — not a raw concatenation of READMEs: it reads as a narrative with a consistent voice. | ||
| 9 | + | ||
| 10 | +## When to use | ||
| 11 | + | ||
| 12 | +- "Make an e-book / a PDF book out of this project." | ||
| 13 | +- "Update the book after I changed / added a chapter." | ||
| 14 | + | ||
| 15 | +## Dependencies | ||
| 16 | + | ||
| 17 | +Nothing local is required if Docker is available (the build script uses the official `asciidoctor/docker-asciidoctor` image). To build without Docker: `gem install asciidoctor asciidoctor-pdf rouge`. | ||
| 18 | + | ||
| 19 | +## Layout | ||
| 20 | + | ||
| 21 | +```text | ||
| 22 | +ebook/ | ||
| 23 | +├── book.adoc ← master document: attributes + include::chapters/* | ||
| 24 | +├── chapters/ | ||
| 25 | +│ ├── 00-introduction.adoc | ||
| 26 | +│ ├── 01-….adoc ← one per project chapter (curated from its README) | ||
| 27 | +│ └── 99-conclusion.adoc | ||
| 28 | +├── images/ | ||
| 29 | +│ ├── *.drawio ← editable diagram sources | ||
| 30 | +│ └── *.svg ← exported diagrams used by the book | ||
| 31 | +├── Makefile ← optional convenience (pdf/html/docker-pdf) | ||
| 32 | +├── README.adoc ← how to build/edit | ||
| 33 | +└── build/ ← output (git-ignored) | ||
| 34 | +``` | ||
| 35 | + | ||
| 36 | +## The master `book.adoc` | ||
| 37 | + | ||
| 38 | +Standard AsciiDoc book attributes, then one `include::` per chapter in order: | ||
| 39 | + | ||
| 40 | +```asciidoc | ||
| 41 | += Book title | ||
| 42 | +:doctype: book | ||
| 43 | +:toc: left | ||
| 44 | +:toclevels: 3 | ||
| 45 | +:sectnums: | ||
| 46 | +:icons: font | ||
| 47 | +:source-highlighter: rouge | ||
| 48 | +:imagesdir: images | ||
| 49 | + | ||
| 50 | +[preface] | ||
| 51 | +== Foreword | ||
| 52 | +… | ||
| 53 | + | ||
| 54 | +include::chapters/00-introduction.adoc[] | ||
| 55 | +include::chapters/01-….adoc[] | ||
| 56 | +… | ||
| 57 | +include::chapters/99-conclusion.adoc[] | ||
| 58 | +``` | ||
| 59 | + | ||
| 60 | +## Workflow | ||
| 61 | + | ||
| 62 | +### 1. Read the project | ||
| 63 | + | ||
| 64 | +List the chapter directories and read each `README.md`. Note, per chapter: the need it answers, the key config/code, the diagrams, the "what you retain". | ||
| 65 | + | ||
| 66 | +### 2. Scaffold (only if `ebook/` doesn't exist) | ||
| 67 | + | ||
| 68 | +Create the layout above: `book.adoc`, `chapters/`, `images/`, and (optionally) a `Makefile` + `README.adoc`. Add an `00-introduction` (hook + how the book is built) and a `99-conclusion`. | ||
| 69 | + | ||
| 70 | +### 3. Write one chapter file per project chapter | ||
| 71 | + | ||
| 72 | +Each `chapters/NN-….adoc` is **synthesised** from the matching README, in the book's voice. A reliable per-chapter shape: | ||
| 73 | + | ||
| 74 | +- `== <chapter title>` | ||
| 75 | +- `=== À quoi on répond` — the need. | ||
| 76 | +- `=== La configuration` / the key code, in a `[source,<lang>]` block (colourised via rouge). | ||
| 77 | +- `=== Pourquoi` — the explanation. | ||
| 78 | +- optional `=== Prompts d'essai` / a diagram (`image::name.svg[…]`). | ||
| 79 | +- `=== Ce qu'on retient` — a short bullet recap. | ||
| 80 | + | ||
| 81 | +**Never hard-wrap prose in the `.adoc` files.** Write each paragraph, and each list item, as **one single physical line** — never insert a line break in the middle of a sentence to fit a column width. Let the editor soft-wrap. This keeps diffs meaningful (a reworded sentence touches one line) and avoids stray breaks. Leave code blocks (`----`), tables (`|===`), headings, block macros (`image::`), and admonition delimiters (`====`) as they are — the rule is about *prose*, not block structure. When editing a chapter that was hard-wrapped, unwrap the paragraphs you touch. | ||
| 82 | + | ||
| 83 | +### 4. Diagrams | ||
| 84 | + | ||
| 85 | +Author diagrams as `.drawio` (editable) + export `.svg` (used by the book), kept in `images/`. Hand-authored SVGs are fine too. After writing, sanity-check the XML parses. | ||
| 86 | + | ||
| 87 | +### 5. Build | ||
| 88 | + | ||
| 89 | +```bash | ||
| 90 | +bash .claude/skills/ebook-builder/scripts/build_ebook.sh ebook # html + pdf | ||
| 91 | +bash .claude/skills/ebook-builder/scripts/build_ebook.sh ebook html # html only | ||
| 92 | +``` | ||
| 93 | + | ||
| 94 | +Confirm it builds with no errors and the diagrams appear. | ||
| 95 | + | ||
| 96 | +### 6. Update | ||
| 97 | + | ||
| 98 | +- Chapter changed → update its `.adoc`. | ||
| 99 | +- Chapter added → add `chapters/NN-….adoc` **and** an `include::` line in `book.adoc` (in order). | ||
| 100 | +- Chapter removed → delete the file and its `include::`. | ||
| 101 | +- Re-build to verify. | ||
| 102 | + | ||
| 103 | +## Rules | ||
| 104 | + | ||
| 105 | +- **Synthesise, don't concatenate.** The book is a narrative, not a README dump. | ||
| 106 | +- **Keep chapter order** aligned with the numbered directories. | ||
| 107 | +- **Colourise code** with `[source,<lang>]` blocks. | ||
| 108 | +- **Diagrams as drawio + svg**, referenced from the chapters. | ||
| 109 | +- **Don't hard-wrap prose (hard rule).** One single physical line per paragraph and per list item in the `.adoc` files — no mid-sentence line breaks. Only block structure (code, tables, headings, macros, `====`) keeps its own lines. | ||
| 110 | +- **Build before finishing**, and keep `build/` out of git. | ||
added
.claude/skills/ebook-builder/scripts/build_ebook.sh +44 -0 | new file mode 100755 | ||
| @@ -0,0 +1,44 @@ | ||
| 1 | +#!/usr/bin/env bash | |
| 2 | +# Build an AsciiDoc ebook to HTML and PDF. | |
| 3 | +# | |
| 4 | +# Reusable: point it at an ebook directory that contains a `book.adoc` master. | |
| 5 | +# Uses the official Asciidoctor Docker image, so no local Ruby is required. | |
| 6 | +# If `asciidoctor` is on PATH, it is used directly instead. | |
| 7 | +# | |
| 8 | +# Usage: | |
| 9 | +# build_ebook.sh [EBOOK_DIR] # default: ./ebook | |
| 10 | +# build_ebook.sh ./ebook html # only html (targets: html | pdf | all[default]) | |
| 11 | + | |
| 12 | +set -eu | |
| 13 | + | |
| 14 | +DIR="${1:-ebook}" | |
| 15 | +TARGET="${2:-all}" | |
| 16 | +SRC="book.adoc" | |
| 17 | +ATTRS=(-a source-highlighter=rouge) | |
| 18 | + | |
| 19 | +if [ ! -f "$DIR/$SRC" ]; then | |
| 20 | + echo "No $DIR/$SRC found (need an AsciiDoc master document)." >&2 | |
| 21 | + exit 1 | |
| 22 | +fi | |
| 23 | + | |
| 24 | +run() { | |
| 25 | + # $1 = asciidoctor binary/subcommand, rest = args | |
| 26 | + if command -v asciidoctor >/dev/null 2>&1; then | |
| 27 | + (cd "$DIR" && "$@") | |
| 28 | + else | |
| 29 | + (cd "$DIR" && docker run --rm -v "$PWD":/documents asciidoctor/docker-asciidoctor "$@") | |
| 30 | + fi | |
| 31 | +} | |
| 32 | + | |
| 33 | +case "$TARGET" in | |
| 34 | +html | all) | |
| 35 | + run asciidoctor "${ATTRS[@]}" -D build -o book.html "$SRC" | |
| 36 | + echo "→ $DIR/build/book.html" | |
| 37 | + ;; | |
| 38 | +esac | |
| 39 | +case "$TARGET" in | |
| 40 | +pdf | all) | |
| 41 | + run asciidoctor-pdf "${ATTRS[@]}" -D build -o book.pdf "$SRC" | |
| 42 | + echo "→ $DIR/build/book.pdf" | |
| 43 | + ;; | |
| 44 | +esac | |
| new file mode 100755 | |||
| @@ -0,0 +1,44 @@ | |||
| 1 | +#!/usr/bin/env bash | ||
| 2 | +# Build an AsciiDoc ebook to HTML and PDF. | ||
| 3 | +# | ||
| 4 | +# Reusable: point it at an ebook directory that contains a `book.adoc` master. | ||
| 5 | +# Uses the official Asciidoctor Docker image, so no local Ruby is required. | ||
| 6 | +# If `asciidoctor` is on PATH, it is used directly instead. | ||
| 7 | +# | ||
| 8 | +# Usage: | ||
| 9 | +# build_ebook.sh [EBOOK_DIR] # default: ./ebook | ||
| 10 | +# build_ebook.sh ./ebook html # only html (targets: html | pdf | all[default]) | ||
| 11 | + | ||
| 12 | +set -eu | ||
| 13 | + | ||
| 14 | +DIR="${1:-ebook}" | ||
| 15 | +TARGET="${2:-all}" | ||
| 16 | +SRC="book.adoc" | ||
| 17 | +ATTRS=(-a source-highlighter=rouge) | ||
| 18 | + | ||
| 19 | +if [ ! -f "$DIR/$SRC" ]; then | ||
| 20 | + echo "No $DIR/$SRC found (need an AsciiDoc master document)." >&2 | ||
| 21 | + exit 1 | ||
| 22 | +fi | ||
| 23 | + | ||
| 24 | +run() { | ||
| 25 | + # $1 = asciidoctor binary/subcommand, rest = args | ||
| 26 | + if command -v asciidoctor >/dev/null 2>&1; then | ||
| 27 | + (cd "$DIR" && "$@") | ||
| 28 | + else | ||
| 29 | + (cd "$DIR" && docker run --rm -v "$PWD":/documents asciidoctor/docker-asciidoctor "$@") | ||
| 30 | + fi | ||
| 31 | +} | ||
| 32 | + | ||
| 33 | +case "$TARGET" in | ||
| 34 | +html | all) | ||
| 35 | + run asciidoctor "${ATTRS[@]}" -D build -o book.html "$SRC" | ||
| 36 | + echo "→ $DIR/build/book.html" | ||
| 37 | + ;; | ||
| 38 | +esac | ||
| 39 | +case "$TARGET" in | ||
| 40 | +pdf | all) | ||
| 41 | + run asciidoctor-pdf "${ATTRS[@]}" -D build -o book.pdf "$SRC" | ||
| 42 | + echo "→ $DIR/build/book.pdf" | ||
| 43 | + ;; | ||
| 44 | +esac | ||
added
.claude/skills/methodical-dev/SKILL.md +603 -0 Large diff collapsed to keep the page fast — load it when you need it.
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 Large diff collapsed to keep the page fast — load it when you need it.
modified
.tickets/issues/0021-when-there-is-an-error-durring-the-completion-we.yaml +80 -3 | @@ -6,10 +6,87 @@ author: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-17T13:31:10.696Z |
| 9 | -updatedAt: 2026-09-17T13:34:49.484Z | |
| 9 | +updatedAt: 2026-09-18T14:20:00.000Z | |
| 10 | 10 | labels: |
| 11 | 11 | - priority::urgent |
| 12 | 12 | milestone: v0.0.2 |
| 13 | -body: "" | |
| 14 | -tasks: [] | |
| 13 | +body: | | |
| 14 | + ## Problem | |
| 15 | + | |
| 16 | + When a completion fails — server down, model not pulled, watchdog timeout, empty or malformed compaction summary — the front end shows one red line (`[error: …]` in the terminal and the window, a JSON-RPC error in the editor) and that is the only trace. It goes with the scrollback and is absent from the session file, because the file holds what the model sees and a failed turn leaves nothing for it (the unanswered question is dropped by `session.AfterTurn`). Nothing is left to hand to a coding agent asked "why did this fail, and what should change in the code or the YAML?". | |
| 17 | + | |
| 18 | + ## Implementation note (Claude, 2026-09-18, branch `feature/log-errors`, not yet committed) | |
| 19 | + | |
| 20 | + The session file (`sessions.enabled: true`, `<project>/.mm/sessions/<id>.json`) now carries an `errors` array next to `messages`, one record per error shown: | |
| 21 | + | |
| 22 | + ```json | |
| 23 | + { | |
| 24 | + "at": "2026-09-18T14:12:25Z", | |
| 25 | + "turn": 1, | |
| 26 | + "where": "generation", | |
| 27 | + "message": "dmr: \"no/such-model:none\" not found at http://…/engines/v1 — docker model pull no/such-model:none", | |
| 28 | + "detail": "failed to create completion: POST \"…/chat/completions\": 404 Not Found", | |
| 29 | + "question": "hello" | |
| 30 | + } | |
| 31 | + ``` | |
| 32 | + | |
| 33 | + - `where` is `generation` (a failed answer, `engine.Generate`), `compaction` (a failed summary, automatic or `/compact`) or `resume` (a `/resume` that could not load its session). A cancelled turn (Esc, Ctrl-C, new question) is not recorded. | |
| 34 | + - `message` is the line the user saw (`engine.Explain`'s words); `detail` is the raw Go error, only when it says more; `question` is the first line of the question being answered — stamped from the history *before* `AfterTurn` dropped it; `turn` counts the user questions at that point. | |
| 35 | + - Recorded by all three front ends: window (`internal/tui/turn.go`, `runner.record`), REPL (`internal/agent/agent.go`, `record`), ACP (`internal/acp/persist.go`, `front.fail`). Code: `store.Failure`, `store.NewFailure`, `(*store.Session).Fail`, `(*store.Keeper).Fail` in `internal/store`. | |
| 36 | + - Reload safety: `/resume` and `session/load` read `messages` alone, so the array is never replayed and never reaches the model; a file written before this change loads with no errors (`omitempty`, tested); `/compact` rewrites `messages` and leaves the trail; over ACP `/new` moves the trail to the archived conversation. The trail is capped at 200 records. | |
| 37 | + - Visible: the `/sessions` listing and the `↩️ resumed …` report end with ` · ⚠️ N error(s)` when a session has recorded errors. A session whose only content is an error has 0 turns: a bare `/resume` skips it (existing rule), `/resume <id>` reaches it. | |
| 38 | + - Workflow: `jq .errors .mm/sessions/<id>.json`, or hand the file to a coding agent and ask what went wrong and what to change (`watchdogTimeout`, `maxOutput`, `model`, `context.*`, or the code). | |
| 39 | + | |
| 40 | + Tested: `go test ./...` (new tests in `internal/store`, `internal/tui`, `internal/acp`); end to end from the sandbox with `agent.dmr.yaml` against the host's DMR (`host.docker.internal:12434`): an unknown model name records a `generation` error in the REPL and in the window mode (pseudo-terminal), `/resume <id>` of that session then a real answer keeps the trail. Docs EN+FR updated (`explanation/persistent-sessions.md`, `how-to/keep-sessions.md`, `reference/slash-commands.md`, `reference/configuration.md`). Quality gate: no new finding in the touched files. | |
| 41 | + | |
| 42 | + Not done: a slash command to show the errors from inside `mm` (jq or an agent does it); errors from tools (they return text to the model, by design, never a Go error) are not recorded — they are already in the history. | |
| 43 | +tasks: | |
| 44 | + - id: 1 | |
| 45 | + title: "internal/store: `errors` array in the session file, outside `messages` (Failure, NewFailure, Session.Fail, Keeper.Fail), capped at 200" | |
| 46 | + state: closed | |
| 47 | + priority: none | |
| 48 | + author: | |
| 49 | + name: k33g | |
| 50 | + email: ph.charriere@gmail.com | |
| 51 | + createdAt: 2026-09-18T14:20:00.000Z | |
| 52 | + updatedAt: 2026-09-18T14:20:00.000Z | |
| 53 | + closedAt: 2026-09-18T14:20:00.000Z | |
| 54 | + - id: 2 | |
| 55 | + title: "Record failed generations, failed compactions and failed /resume in the window mode, the REPL and over ACP; cancelled turns are not errors" | |
| 56 | + state: closed | |
| 57 | + priority: none | |
| 58 | + author: | |
| 59 | + name: k33g | |
| 60 | + email: ph.charriere@gmail.com | |
| 61 | + createdAt: 2026-09-18T14:20:00.000Z | |
| 62 | + updatedAt: 2026-09-18T14:20:00.000Z | |
| 63 | + closedAt: 2026-09-18T14:20:00.000Z | |
| 64 | + - id: 3 | |
| 65 | + title: "Reload safety: /resume and session/load ignore the array; old files load; /compact keeps it; ACP /new archives it with its conversation — tests" | |
| 66 | + state: closed | |
| 67 | + priority: none | |
| 68 | + author: | |
| 69 | + name: k33g | |
| 70 | + email: ph.charriere@gmail.com | |
| 71 | + createdAt: 2026-09-18T14:20:00.000Z | |
| 72 | + updatedAt: 2026-09-18T14:20:00.000Z | |
| 73 | + closedAt: 2026-09-18T14:20:00.000Z | |
| 74 | + - id: 4 | |
| 75 | + title: "/sessions listing and /resume report show ` · ⚠️ N error(s)`; docs EN+FR (explanation, how-to, slash-commands, configuration)" | |
| 76 | + state: closed | |
| 77 | + priority: none | |
| 78 | + author: | |
| 79 | + name: k33g | |
| 80 | + email: ph.charriere@gmail.com | |
| 81 | + createdAt: 2026-09-18T14:20:00.000Z | |
| 82 | + updatedAt: 2026-09-18T14:20:00.000Z | |
| 83 | + closedAt: 2026-09-18T14:20:00.000Z | |
| 84 | + - id: 5 | |
| 85 | + title: "Verify on macOS with the installed binary (`./install.sh`): trigger a red error in `mm -tui`, then `jq .errors` on the session file and `/resume <id>`" | |
| 86 | + state: open | |
| 87 | + priority: medium | |
| 88 | + author: | |
| 89 | + name: k33g | |
| 90 | + email: ph.charriere@gmail.com | |
| 91 | + createdAt: 2026-09-18T14:20:00.000Z | |
| 15 | 92 | comments: [] |
| @@ -6,10 +6,87 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-17T13:31:10.696Z | 8 | createdAt: 2026-09-17T13:31:10.696Z |
| 9 | -updatedAt: 2026-09-17T13:34:49.484Z | 9 | +updatedAt: 2026-09-18T14:20:00.000Z |
| 10 | labels: | 10 | labels: |
| 11 | - priority::urgent | 11 | - priority::urgent |
| 12 | milestone: v0.0.2 | 12 | milestone: v0.0.2 |
| 13 | -body: "" | 13 | +body: | |
| 14 | -tasks: [] | 14 | + ## Problem |
| 15 | + | ||
| 16 | + When a completion fails — server down, model not pulled, watchdog timeout, empty or malformed compaction summary — the front end shows one red line (`[error: …]` in the terminal and the window, a JSON-RPC error in the editor) and that is the only trace. It goes with the scrollback and is absent from the session file, because the file holds what the model sees and a failed turn leaves nothing for it (the unanswered question is dropped by `session.AfterTurn`). Nothing is left to hand to a coding agent asked "why did this fail, and what should change in the code or the YAML?". | ||
| 17 | + | ||
| 18 | + ## Implementation note (Claude, 2026-09-18, branch `feature/log-errors`, not yet committed) | ||
| 19 | + | ||
| 20 | + The session file (`sessions.enabled: true`, `<project>/.mm/sessions/<id>.json`) now carries an `errors` array next to `messages`, one record per error shown: | ||
| 21 | + | ||
| 22 | + ```json | ||
| 23 | + { | ||
| 24 | + "at": "2026-09-18T14:12:25Z", | ||
| 25 | + "turn": 1, | ||
| 26 | + "where": "generation", | ||
| 27 | + "message": "dmr: \"no/such-model:none\" not found at http://…/engines/v1 — docker model pull no/such-model:none", | ||
| 28 | + "detail": "failed to create completion: POST \"…/chat/completions\": 404 Not Found", | ||
| 29 | + "question": "hello" | ||
| 30 | + } | ||
| 31 | + ``` | ||
| 32 | + | ||
| 33 | + - `where` is `generation` (a failed answer, `engine.Generate`), `compaction` (a failed summary, automatic or `/compact`) or `resume` (a `/resume` that could not load its session). A cancelled turn (Esc, Ctrl-C, new question) is not recorded. | ||
| 34 | + - `message` is the line the user saw (`engine.Explain`'s words); `detail` is the raw Go error, only when it says more; `question` is the first line of the question being answered — stamped from the history *before* `AfterTurn` dropped it; `turn` counts the user questions at that point. | ||
| 35 | + - Recorded by all three front ends: window (`internal/tui/turn.go`, `runner.record`), REPL (`internal/agent/agent.go`, `record`), ACP (`internal/acp/persist.go`, `front.fail`). Code: `store.Failure`, `store.NewFailure`, `(*store.Session).Fail`, `(*store.Keeper).Fail` in `internal/store`. | ||
| 36 | + - Reload safety: `/resume` and `session/load` read `messages` alone, so the array is never replayed and never reaches the model; a file written before this change loads with no errors (`omitempty`, tested); `/compact` rewrites `messages` and leaves the trail; over ACP `/new` moves the trail to the archived conversation. The trail is capped at 200 records. | ||
| 37 | + - Visible: the `/sessions` listing and the `↩️ resumed …` report end with ` · ⚠️ N error(s)` when a session has recorded errors. A session whose only content is an error has 0 turns: a bare `/resume` skips it (existing rule), `/resume <id>` reaches it. | ||
| 38 | + - Workflow: `jq .errors .mm/sessions/<id>.json`, or hand the file to a coding agent and ask what went wrong and what to change (`watchdogTimeout`, `maxOutput`, `model`, `context.*`, or the code). | ||
| 39 | + | ||
| 40 | + Tested: `go test ./...` (new tests in `internal/store`, `internal/tui`, `internal/acp`); end to end from the sandbox with `agent.dmr.yaml` against the host's DMR (`host.docker.internal:12434`): an unknown model name records a `generation` error in the REPL and in the window mode (pseudo-terminal), `/resume <id>` of that session then a real answer keeps the trail. Docs EN+FR updated (`explanation/persistent-sessions.md`, `how-to/keep-sessions.md`, `reference/slash-commands.md`, `reference/configuration.md`). Quality gate: no new finding in the touched files. | ||
| 41 | + | ||
| 42 | + Not done: a slash command to show the errors from inside `mm` (jq or an agent does it); errors from tools (they return text to the model, by design, never a Go error) are not recorded — they are already in the history. | ||
| 43 | +tasks: | ||
| 44 | + - id: 1 | ||
| 45 | + title: "internal/store: `errors` array in the session file, outside `messages` (Failure, NewFailure, Session.Fail, Keeper.Fail), capped at 200" | ||
| 46 | + state: closed | ||
| 47 | + priority: none | ||
| 48 | + author: | ||
| 49 | + name: k33g | ||
| 50 | + email: ph.charriere@gmail.com | ||
| 51 | + createdAt: 2026-09-18T14:20:00.000Z | ||
| 52 | + updatedAt: 2026-09-18T14:20:00.000Z | ||
| 53 | + closedAt: 2026-09-18T14:20:00.000Z | ||
| 54 | + - id: 2 | ||
| 55 | + title: "Record failed generations, failed compactions and failed /resume in the window mode, the REPL and over ACP; cancelled turns are not errors" | ||
| 56 | + state: closed | ||
| 57 | + priority: none | ||
| 58 | + author: | ||
| 59 | + name: k33g | ||
| 60 | + email: ph.charriere@gmail.com | ||
| 61 | + createdAt: 2026-09-18T14:20:00.000Z | ||
| 62 | + updatedAt: 2026-09-18T14:20:00.000Z | ||
| 63 | + closedAt: 2026-09-18T14:20:00.000Z | ||
| 64 | + - id: 3 | ||
| 65 | + title: "Reload safety: /resume and session/load ignore the array; old files load; /compact keeps it; ACP /new archives it with its conversation — tests" | ||
| 66 | + state: closed | ||
| 67 | + priority: none | ||
| 68 | + author: | ||
| 69 | + name: k33g | ||
| 70 | + email: ph.charriere@gmail.com | ||
| 71 | + createdAt: 2026-09-18T14:20:00.000Z | ||
| 72 | + updatedAt: 2026-09-18T14:20:00.000Z | ||
| 73 | + closedAt: 2026-09-18T14:20:00.000Z | ||
| 74 | + - id: 4 | ||
| 75 | + title: "/sessions listing and /resume report show ` · ⚠️ N error(s)`; docs EN+FR (explanation, how-to, slash-commands, configuration)" | ||
| 76 | + state: closed | ||
| 77 | + priority: none | ||
| 78 | + author: | ||
| 79 | + name: k33g | ||
| 80 | + email: ph.charriere@gmail.com | ||
| 81 | + createdAt: 2026-09-18T14:20:00.000Z | ||
| 82 | + updatedAt: 2026-09-18T14:20:00.000Z | ||
| 83 | + closedAt: 2026-09-18T14:20:00.000Z | ||
| 84 | + - id: 5 | ||
| 85 | + title: "Verify on macOS with the installed binary (`./install.sh`): trigger a red error in `mm -tui`, then `jq .errors` on the session file and `/resume <id>`" | ||
| 86 | + state: open | ||
| 87 | + priority: medium | ||
| 88 | + author: | ||
| 89 | + name: k33g | ||
| 90 | + email: ph.charriere@gmail.com | ||
| 91 | + createdAt: 2026-09-18T14:20:00.000Z | ||
| 15 | comments: [] | 92 | comments: [] |
modified
agent.dmr.yaml +1 -1 | @@ -34,7 +34,7 @@ skillsDir: skills | ||
| 34 | 34 | # Persistent sessions, per project. |
| 35 | 35 | # When enabled, every conversation is saved as one JSON file under `dir` |
| 36 | 36 | sessions: |
| 37 | - enabled: false | |
| 37 | + enabled: true | |
| 38 | 38 | dir: .mm/sessions |
| 39 | 39 | |
| 40 | 40 | # How many lines of a command's output are echoed to the terminal. |
| @@ -34,7 +34,7 @@ skillsDir: skills | |||
| 34 | # Persistent sessions, per project. | 34 | # Persistent sessions, per project. |
| 35 | # When enabled, every conversation is saved as one JSON file under `dir` | 35 | # When enabled, every conversation is saved as one JSON file under `dir` |
| 36 | sessions: | 36 | sessions: |
| 37 | - enabled: false | 37 | + enabled: true |
| 38 | dir: .mm/sessions | 38 | dir: .mm/sessions |
| 39 | 39 | ||
| 40 | # How many lines of a command's output are echoed to the terminal. | 40 | # How many lines of a command's output are echoed to the terminal. |
modified
docs/en/explanation/persistent-sessions.md +9 -0 | @@ -28,6 +28,14 @@ A reloaded or resumed session continues under the current system prompt, not the | ||
| 28 | 28 | |
| 29 | 29 | In the terminal `/new` simply leaves the old file alone and lets the next question open a new one. Over ACP the editor owns the session id and keeps using it, so the id's file must become the fresh session; the conversation so far is moved to a new id first. Either way nothing is lost on disk, and a later reload of the thread gives the current conversation, not the one `/new` closed. |
| 30 | 30 | |
| 31 | +## Why errors are recorded next to the history, not in it | |
| 32 | + | |
| 33 | +A turn can fail — the server is down, the model is not pulled, the watchdog gave up, the summary of a compaction came back empty — and the front end says so in one red line. That line was the only trace: gone with the scrollback, absent from the file, because the file holds what the model sees and a failed turn leaves nothing for the model to see (the unanswered question is dropped). Since 2026-09-18 the session file also carries an `errors` array: one record per line shown, with the time, the turn count, the first line of the question being answered, the line as shown, and the raw error when it says more. Three kinds are recorded: `generation` (a failed answer), `compaction` (a failed summary, automatic or `/compact`) and `resume` (a `/resume` that could not load its session). A cancelled turn is not an error and is not recorded. | |
| 34 | + | |
| 35 | +The array sits beside `messages`, not inside it, on purpose. The model never reads it: an error is about the run, not about the project, and feeding it back would make the model explain its infrastructure instead of doing its task. `/resume` and `session/load` never replay it: both read `messages` alone, so an old session loads exactly as before and a new one with errors loads as if the field were not there. And compression leaves it alone: `/compact` rewrites `messages`, the trail stays whole. The record follows its conversation: over ACP, `/new` moves it to the archived file with the history it belongs to. | |
| 36 | + | |
| 37 | +What it is for: asking afterwards, with the file in hand, why a run went wrong — `jq .errors <file>` for a human, or the file handed to a coding agent asked to explain the failures and propose changes to the code or the YAML (a longer `watchdogTimeout`, a different model, a smaller `maxOutput`). The `/sessions` listing and the `/resume` report show ` · ⚠️ N error(s)` so a session worth investigating stands out. The trail is capped at 200 records, oldest first out, so a run that fails on every turn does not grow its file without end. | |
| 38 | + | |
| 31 | 39 | ## Rejected alternatives |
| 32 | 40 | |
| 33 | 41 | - **Resume the latest session automatically at start.** Rejected for the stale-context reason above, and because docker agent does not do it either. |
| @@ -35,6 +43,7 @@ In the terminal `/new` simply leaves the old file alone and lets the next questi | ||
| 35 | 43 | - **SQLite, like docker agent.** Rejected for a minimal agent: one more dependency, opaque files, and a per-user database where mini-me wants per-project files. |
| 36 | 44 | - **`session/resume` (unstable) instead of `session/load`.** Rejected: not in the stable spec, and it relies on the editor having kept the thread. |
| 37 | 45 | - **Replaying only text messages.** Rejected by the user: the tool calls are the part of a coding session worth seeing again. |
| 46 | +- **Recording errors as messages in the history.** Rejected: the model would read them, `session/load` would have to replay them as something, and an old file would load differently from a new one. A separate field costs nothing on load and changes nothing for the model. | |
| 38 | 47 | |
| 39 | 48 | ## How it relates to the rest |
| 40 | 49 | |
| @@ -28,6 +28,14 @@ A reloaded or resumed session continues under the current system prompt, not the | |||
| 28 | 28 | ||
| 29 | In the terminal `/new` simply leaves the old file alone and lets the next question open a new one. Over ACP the editor owns the session id and keeps using it, so the id's file must become the fresh session; the conversation so far is moved to a new id first. Either way nothing is lost on disk, and a later reload of the thread gives the current conversation, not the one `/new` closed. | 29 | In the terminal `/new` simply leaves the old file alone and lets the next question open a new one. Over ACP the editor owns the session id and keeps using it, so the id's file must become the fresh session; the conversation so far is moved to a new id first. Either way nothing is lost on disk, and a later reload of the thread gives the current conversation, not the one `/new` closed. |
| 30 | 30 | ||
| 31 | +## Why errors are recorded next to the history, not in it | ||
| 32 | + | ||
| 33 | +A turn can fail — the server is down, the model is not pulled, the watchdog gave up, the summary of a compaction came back empty — and the front end says so in one red line. That line was the only trace: gone with the scrollback, absent from the file, because the file holds what the model sees and a failed turn leaves nothing for the model to see (the unanswered question is dropped). Since 2026-09-18 the session file also carries an `errors` array: one record per line shown, with the time, the turn count, the first line of the question being answered, the line as shown, and the raw error when it says more. Three kinds are recorded: `generation` (a failed answer), `compaction` (a failed summary, automatic or `/compact`) and `resume` (a `/resume` that could not load its session). A cancelled turn is not an error and is not recorded. | ||
| 34 | + | ||
| 35 | +The array sits beside `messages`, not inside it, on purpose. The model never reads it: an error is about the run, not about the project, and feeding it back would make the model explain its infrastructure instead of doing its task. `/resume` and `session/load` never replay it: both read `messages` alone, so an old session loads exactly as before and a new one with errors loads as if the field were not there. And compression leaves it alone: `/compact` rewrites `messages`, the trail stays whole. The record follows its conversation: over ACP, `/new` moves it to the archived file with the history it belongs to. | ||
| 36 | + | ||
| 37 | +What it is for: asking afterwards, with the file in hand, why a run went wrong — `jq .errors <file>` for a human, or the file handed to a coding agent asked to explain the failures and propose changes to the code or the YAML (a longer `watchdogTimeout`, a different model, a smaller `maxOutput`). The `/sessions` listing and the `/resume` report show ` · ⚠️ N error(s)` so a session worth investigating stands out. The trail is capped at 200 records, oldest first out, so a run that fails on every turn does not grow its file without end. | ||
| 38 | + | ||
| 31 | ## Rejected alternatives | 39 | ## Rejected alternatives |
| 32 | 40 | ||
| 33 | - **Resume the latest session automatically at start.** Rejected for the stale-context reason above, and because docker agent does not do it either. | 41 | - **Resume the latest session automatically at start.** Rejected for the stale-context reason above, and because docker agent does not do it either. |
| @@ -35,6 +43,7 @@ In the terminal `/new` simply leaves the old file alone and lets the next questi | |||
| 35 | - **SQLite, like docker agent.** Rejected for a minimal agent: one more dependency, opaque files, and a per-user database where mini-me wants per-project files. | 43 | - **SQLite, like docker agent.** Rejected for a minimal agent: one more dependency, opaque files, and a per-user database where mini-me wants per-project files. |
| 36 | - **`session/resume` (unstable) instead of `session/load`.** Rejected: not in the stable spec, and it relies on the editor having kept the thread. | 44 | - **`session/resume` (unstable) instead of `session/load`.** Rejected: not in the stable spec, and it relies on the editor having kept the thread. |
| 37 | - **Replaying only text messages.** Rejected by the user: the tool calls are the part of a coding session worth seeing again. | 45 | - **Replaying only text messages.** Rejected by the user: the tool calls are the part of a coding session worth seeing again. |
| 46 | +- **Recording errors as messages in the history.** Rejected: the model would read them, `session/load` would have to replay them as something, and an old file would load differently from a new one. A separate field costs nothing on load and changes nothing for the model. | ||
| 38 | 47 | ||
| 39 | ## How it relates to the rest | 48 | ## How it relates to the rest |
| 40 | 49 | ||
modified
docs/en/how-to/keep-sessions.md +36 -1 | @@ -43,6 +43,40 @@ This guide shows how to make `mm` save every conversation of a project and get o | ||
| 43 | 43 | |
| 44 | 44 | A bare `/resume` takes the most recently updated conversation other than the current one, skipping empty ones; `/resume <id>` names one. From there, the model sees the resumed history — under the current system prompt — and every new turn is saved to that session's file. Nothing is replayed on screen: the one line says what came back. |
| 45 | 45 | |
| 46 | +## Find out why a run failed | |
| 47 | + | |
| 48 | +1. Spot the session in the listing: a session that showed at least one error ends its line with ` · ⚠️ N error(s)`. The same tail appears in the `↩️ resumed …` report. | |
| 49 | + | |
| 50 | + ```text | |
| 51 | + > /sessions | |
| 52 | + 2 session(s) in /work/project/.mm/sessions — /resume <id> to continue one, /resume for the latest | |
| 53 | + * 20260918-141321-db582a3d 2026-09-18 16:13 1 turn(s) say just: ok | |
| 54 | + 20260918-141225-02790c82 2026-09-18 16:12 0 turn(s) · ⚠️ 1 error(s) | |
| 55 | + ``` | |
| 56 | + | |
| 57 | +2. Read the `errors` array of its file. Each record says when, at which turn, on which question, what the screen showed (`message`) and, when the raw error says more, what it was (`detail`); `where` is `generation`, `compaction` or `resume`: | |
| 58 | + | |
| 59 | + ```bash | |
| 60 | + jq .errors .mm/sessions/20260918-141225-02790c82.json | |
| 61 | + ``` | |
| 62 | + | |
| 63 | + ```json | |
| 64 | + [ | |
| 65 | + { | |
| 66 | + "at": "2026-09-18T14:12:25Z", | |
| 67 | + "turn": 1, | |
| 68 | + "where": "generation", | |
| 69 | + "message": "dmr: \"no/such-model:none\" not found at http://localhost:12434/engines/v1 — docker model pull no/such-model:none", | |
| 70 | + "detail": "failed to create completion: POST \"http://localhost:12434/engines/v1/chat/completions\": 404 Not Found", | |
| 71 | + "question": "hello" | |
| 72 | + } | |
| 73 | + ] | |
| 74 | + ``` | |
| 75 | + | |
| 76 | +3. To have the failure analysed, hand the file to a coding agent (Claude Code, or `mm` itself in another project) and ask what went wrong and what to change — in the code, or in `agent.yaml` (`watchdogTimeout`, `maxOutput`, `model`, `context.*`). The history in the same file gives the context the model had; the `errors` array gives what it never saw. | |
| 77 | + | |
| 78 | + A session whose only content is an error has no turn: a bare `/resume` skips it, `/resume <id>` reaches it. | |
| 79 | + | |
| 46 | 80 | ## From an editor (ACP) |
| 47 | 81 | |
| 48 | 82 | 1. Keep `sessions.enabled: true` in the file `AGENT_CONFIG` points at. Each thread the editor opens is a session, saved under `<project>/.mm/sessions/<id>.json` from its creation. |
| @@ -55,7 +89,8 @@ This guide shows how to make `mm` save every conversation of a project and get o | ||
| 55 | 89 | |
| 56 | 90 | - **One store for every project.** Give `dir` an absolute path. Sessions of different projects then share one directory; each file records its `cwd`. |
| 57 | 91 | - **A project you cannot write to.** The banner or the `[acp]` log says `not saved` with the reason; the conversation goes on unpersisted. |
| 58 | -- **Compression.** A `/compact` or an automatic compression rewrites the file with the shortened history; a resumed or reloaded session starts from the summary, like the live one did. | |
| 92 | +- **Compression.** A `/compact` or an automatic compression rewrites the file with the shortened history; a resumed or reloaded session starts from the summary, like the live one did. The `errors` array is left as it is. | |
| 93 | +- **Errors over ACP.** The editor shows the failure in its own way; the record still goes to the thread's file, and `/new` moves it to the archived conversation with the history it belongs to. | |
| 59 | 94 | - **Starting fresh on purpose.** Nothing to do: every start is a new session. The old ones only come back on `/resume` or on an editor reload. |
| 60 | 95 | |
| 61 | 96 | ## See also |
| @@ -43,6 +43,40 @@ This guide shows how to make `mm` save every conversation of a project and get o | |||
| 43 | 43 | ||
| 44 | A bare `/resume` takes the most recently updated conversation other than the current one, skipping empty ones; `/resume <id>` names one. From there, the model sees the resumed history — under the current system prompt — and every new turn is saved to that session's file. Nothing is replayed on screen: the one line says what came back. | 44 | A bare `/resume` takes the most recently updated conversation other than the current one, skipping empty ones; `/resume <id>` names one. From there, the model sees the resumed history — under the current system prompt — and every new turn is saved to that session's file. Nothing is replayed on screen: the one line says what came back. |
| 45 | 45 | ||
| 46 | +## Find out why a run failed | ||
| 47 | + | ||
| 48 | +1. Spot the session in the listing: a session that showed at least one error ends its line with ` · ⚠️ N error(s)`. The same tail appears in the `↩️ resumed …` report. | ||
| 49 | + | ||
| 50 | + ```text | ||
| 51 | + > /sessions | ||
| 52 | + 2 session(s) in /work/project/.mm/sessions — /resume <id> to continue one, /resume for the latest | ||
| 53 | + * 20260918-141321-db582a3d 2026-09-18 16:13 1 turn(s) say just: ok | ||
| 54 | + 20260918-141225-02790c82 2026-09-18 16:12 0 turn(s) · ⚠️ 1 error(s) | ||
| 55 | + ``` | ||
| 56 | + | ||
| 57 | +2. Read the `errors` array of its file. Each record says when, at which turn, on which question, what the screen showed (`message`) and, when the raw error says more, what it was (`detail`); `where` is `generation`, `compaction` or `resume`: | ||
| 58 | + | ||
| 59 | + ```bash | ||
| 60 | + jq .errors .mm/sessions/20260918-141225-02790c82.json | ||
| 61 | + ``` | ||
| 62 | + | ||
| 63 | + ```json | ||
| 64 | + [ | ||
| 65 | + { | ||
| 66 | + "at": "2026-09-18T14:12:25Z", | ||
| 67 | + "turn": 1, | ||
| 68 | + "where": "generation", | ||
| 69 | + "message": "dmr: \"no/such-model:none\" not found at http://localhost:12434/engines/v1 — docker model pull no/such-model:none", | ||
| 70 | + "detail": "failed to create completion: POST \"http://localhost:12434/engines/v1/chat/completions\": 404 Not Found", | ||
| 71 | + "question": "hello" | ||
| 72 | + } | ||
| 73 | + ] | ||
| 74 | + ``` | ||
| 75 | + | ||
| 76 | +3. To have the failure analysed, hand the file to a coding agent (Claude Code, or `mm` itself in another project) and ask what went wrong and what to change — in the code, or in `agent.yaml` (`watchdogTimeout`, `maxOutput`, `model`, `context.*`). The history in the same file gives the context the model had; the `errors` array gives what it never saw. | ||
| 77 | + | ||
| 78 | + A session whose only content is an error has no turn: a bare `/resume` skips it, `/resume <id>` reaches it. | ||
| 79 | + | ||
| 46 | ## From an editor (ACP) | 80 | ## From an editor (ACP) |
| 47 | 81 | ||
| 48 | 1. Keep `sessions.enabled: true` in the file `AGENT_CONFIG` points at. Each thread the editor opens is a session, saved under `<project>/.mm/sessions/<id>.json` from its creation. | 82 | 1. Keep `sessions.enabled: true` in the file `AGENT_CONFIG` points at. Each thread the editor opens is a session, saved under `<project>/.mm/sessions/<id>.json` from its creation. |
| @@ -55,7 +89,8 @@ This guide shows how to make `mm` save every conversation of a project and get o | |||
| 55 | 89 | ||
| 56 | - **One store for every project.** Give `dir` an absolute path. Sessions of different projects then share one directory; each file records its `cwd`. | 90 | - **One store for every project.** Give `dir` an absolute path. Sessions of different projects then share one directory; each file records its `cwd`. |
| 57 | - **A project you cannot write to.** The banner or the `[acp]` log says `not saved` with the reason; the conversation goes on unpersisted. | 91 | - **A project you cannot write to.** The banner or the `[acp]` log says `not saved` with the reason; the conversation goes on unpersisted. |
| 58 | -- **Compression.** A `/compact` or an automatic compression rewrites the file with the shortened history; a resumed or reloaded session starts from the summary, like the live one did. | 92 | +- **Compression.** A `/compact` or an automatic compression rewrites the file with the shortened history; a resumed or reloaded session starts from the summary, like the live one did. The `errors` array is left as it is. |
| 93 | +- **Errors over ACP.** The editor shows the failure in its own way; the record still goes to the thread's file, and `/new` moves it to the archived conversation with the history it belongs to. | ||
| 59 | - **Starting fresh on purpose.** Nothing to do: every start is a new session. The old ones only come back on `/resume` or on an editor reload. | 94 | - **Starting fresh on purpose.** Nothing to do: every start is a new session. The old ones only come back on `/resume` or on an editor reload. |
| 60 | 95 | ||
| 61 | ## See also | 96 | ## See also |
modified
docs/en/reference/configuration.md +1 -1 | @@ -43,7 +43,7 @@ | ||
| 43 | 43 | |
| 44 | 44 | | Key | Type | Default | Description | |
| 45 | 45 | |-----|------|---------|-------------| |
| 46 | -| `sessions.enabled` | bool | `false` | Save every conversation to a file, rewritten after each turn, `/compact` and `/new`. Enables `/sessions` and `/resume` in the terminal and `session/load` over ACP. | | |
| 46 | +| `sessions.enabled` | bool | `false` | Save every conversation to a file, rewritten after each turn, `/compact` and `/new`. The file also records, outside the history, every error shown (`errors` array: failed generation, failed compaction, failed `/resume`). Enables `/sessions` and `/resume` in the terminal and `session/load` over ACP. | | |
| 47 | 47 | | `sessions.dir` | string | `.mm/sessions` | Directory of the session files, one `<id>.json` each. Relative to the directory `mm` is started from (terminal) or to the session's `cwd` (ACP); an absolute path is used as-is. Must not be empty. | |
| 48 | 48 | |
| 49 | 49 | ## Provider defaults |
| @@ -43,7 +43,7 @@ | |||
| 43 | 43 | ||
| 44 | | Key | Type | Default | Description | | 44 | | Key | Type | Default | Description | |
| 45 | |-----|------|---------|-------------| | 45 | |-----|------|---------|-------------| |
| 46 | -| `sessions.enabled` | bool | `false` | Save every conversation to a file, rewritten after each turn, `/compact` and `/new`. Enables `/sessions` and `/resume` in the terminal and `session/load` over ACP. | | 46 | +| `sessions.enabled` | bool | `false` | Save every conversation to a file, rewritten after each turn, `/compact` and `/new`. The file also records, outside the history, every error shown (`errors` array: failed generation, failed compaction, failed `/resume`). Enables `/sessions` and `/resume` in the terminal and `session/load` over ACP. | |
| 47 | | `sessions.dir` | string | `.mm/sessions` | Directory of the session files, one `<id>.json` each. Relative to the directory `mm` is started from (terminal) or to the session's `cwd` (ACP); an absolute path is used as-is. Must not be empty. | | 47 | | `sessions.dir` | string | `.mm/sessions` | Directory of the session files, one `<id>.json` each. Relative to the directory `mm` is started from (terminal) or to the session's `cwd` (ACP); an absolute path is used as-is. Must not be empty. | |
| 48 | 48 | ||
| 49 | ## Provider defaults | 49 | ## Provider defaults |
modified
docs/en/reference/slash-commands.md +2 -2 | @@ -10,8 +10,8 @@ | ||
| 10 | 10 | | `/abort` | Prints `⚠️ No generation in progress.` | Cancels the generation, prints `🛑 Aborting...`; the commands already run stay in the history. | |
| 11 | 11 | | `/compact` | Compresses the history now, regardless of the threshold; prints the `🗜️` report, or `🗜️ nothing to compact` when no turn is older than `context.keepLastTurns`. | Sent to the model as a new message (see below). | |
| 12 | 12 | | `/new` | Clears the history back to the system prompt, resets the loop detector and forgets the server's last token count; prints `🆕 new session: N message(s) forgotten` (N counts every message except the system prompt). | Treated like any other text: cancels the generation, keeps its partial history, then runs as `/new` on that history. | |
| 13 | -| `/sessions` | Lists the project's stored sessions, newest first: id, last update, question count, first question; `*` marks the current one. With `sessions.enabled: false`: `⚠️ sessions are off (set sessions.enabled: true in the YAML)`. | Sent to the model as a new message. | | |
| 14 | -| `/resume [id]` | Continues a stored session: the most recently updated one other than the current, skipping empty ones, or the named id. Prints `↩️ resumed <id> — N message(s), started with "<first question>"`; resets the loop detector and the server's token count; later turns are saved to that session. Refused with a `⚠️` reason when sessions are off, when there is no earlier session, when the id is unknown or is the current session. | Sent to the model as a new message. | | |
| 13 | +| `/sessions` | Lists the project's stored sessions, newest first: id, last update, question count, first question, then ` · ⚠️ N error(s)` when the session recorded errors; `*` marks the current one. With `sessions.enabled: false`: `⚠️ sessions are off (set sessions.enabled: true in the YAML)`. | Sent to the model as a new message. | | |
| 14 | +| `/resume [id]` | Continues a stored session: the most recently updated one other than the current, skipping empty ones, or the named id. Prints `↩️ resumed <id> — N message(s), started with "<first question>"` (plus ` · ⚠️ N error(s)` when the session recorded errors); resets the loop detector and the server's token count; later turns are saved to that session. Refused with a `⚠️` reason when sessions are off, when there is no earlier session, when the id is unknown or is the current session. | Sent to the model as a new message. | | |
| 15 | 15 | | `/help` | Prints the commands of this front end, one per line with their description, aligned. | Sent to the model as a new message. | |
| 16 | 16 | | `/copy [n\|all]` | Copies the last answer (`n`: the n-th from the end) to the clipboard through `pbcopy`/`wl-copy`/`xclip`/`xsel` or, failing that, OSC 52 when stdout is a terminal, and prints `📋 copied N chars (<road>)` or `⚠️ …`. `/copy all` is window-mode only: here it says so. | Sent to the model as a new message. | |
| 17 | 17 | |
| @@ -10,8 +10,8 @@ | |||
| 10 | | `/abort` | Prints `⚠️ No generation in progress.` | Cancels the generation, prints `🛑 Aborting...`; the commands already run stay in the history. | | 10 | | `/abort` | Prints `⚠️ No generation in progress.` | Cancels the generation, prints `🛑 Aborting...`; the commands already run stay in the history. | |
| 11 | | `/compact` | Compresses the history now, regardless of the threshold; prints the `🗜️` report, or `🗜️ nothing to compact` when no turn is older than `context.keepLastTurns`. | Sent to the model as a new message (see below). | | 11 | | `/compact` | Compresses the history now, regardless of the threshold; prints the `🗜️` report, or `🗜️ nothing to compact` when no turn is older than `context.keepLastTurns`. | Sent to the model as a new message (see below). | |
| 12 | | `/new` | Clears the history back to the system prompt, resets the loop detector and forgets the server's last token count; prints `🆕 new session: N message(s) forgotten` (N counts every message except the system prompt). | Treated like any other text: cancels the generation, keeps its partial history, then runs as `/new` on that history. | | 12 | | `/new` | Clears the history back to the system prompt, resets the loop detector and forgets the server's last token count; prints `🆕 new session: N message(s) forgotten` (N counts every message except the system prompt). | Treated like any other text: cancels the generation, keeps its partial history, then runs as `/new` on that history. | |
| 13 | -| `/sessions` | Lists the project's stored sessions, newest first: id, last update, question count, first question; `*` marks the current one. With `sessions.enabled: false`: `⚠️ sessions are off (set sessions.enabled: true in the YAML)`. | Sent to the model as a new message. | | 13 | +| `/sessions` | Lists the project's stored sessions, newest first: id, last update, question count, first question, then ` · ⚠️ N error(s)` when the session recorded errors; `*` marks the current one. With `sessions.enabled: false`: `⚠️ sessions are off (set sessions.enabled: true in the YAML)`. | Sent to the model as a new message. | |
| 14 | -| `/resume [id]` | Continues a stored session: the most recently updated one other than the current, skipping empty ones, or the named id. Prints `↩️ resumed <id> — N message(s), started with "<first question>"`; resets the loop detector and the server's token count; later turns are saved to that session. Refused with a `⚠️` reason when sessions are off, when there is no earlier session, when the id is unknown or is the current session. | Sent to the model as a new message. | | 14 | +| `/resume [id]` | Continues a stored session: the most recently updated one other than the current, skipping empty ones, or the named id. Prints `↩️ resumed <id> — N message(s), started with "<first question>"` (plus ` · ⚠️ N error(s)` when the session recorded errors); resets the loop detector and the server's token count; later turns are saved to that session. Refused with a `⚠️` reason when sessions are off, when there is no earlier session, when the id is unknown or is the current session. | Sent to the model as a new message. | |
| 15 | | `/help` | Prints the commands of this front end, one per line with their description, aligned. | Sent to the model as a new message. | | 15 | | `/help` | Prints the commands of this front end, one per line with their description, aligned. | Sent to the model as a new message. | |
| 16 | | `/copy [n\|all]` | Copies the last answer (`n`: the n-th from the end) to the clipboard through `pbcopy`/`wl-copy`/`xclip`/`xsel` or, failing that, OSC 52 when stdout is a terminal, and prints `📋 copied N chars (<road>)` or `⚠️ …`. `/copy all` is window-mode only: here it says so. | Sent to the model as a new message. | | 16 | | `/copy [n\|all]` | Copies the last answer (`n`: the n-th from the end) to the clipboard through `pbcopy`/`wl-copy`/`xclip`/`xsel` or, failing that, OSC 52 when stdout is a terminal, and prints `📋 copied N chars (<road>)` or `⚠️ …`. `/copy all` is window-mode only: here it says so. | Sent to the model as a new message. | |
| 17 | 17 | ||
modified
docs/fr/explanation/persistent-sessions.md +9 -0 | @@ -28,6 +28,14 @@ Une session rechargée ou reprise continue sous le prompt système actuel, pas c | ||
| 28 | 28 | |
| 29 | 29 | Dans le terminal, `/new` laisse simplement l'ancien fichier tel quel et la question suivante en ouvre un nouveau. En ACP, l'éditeur possède l'identifiant de session et continue de l'utiliser : le fichier de cet identifiant doit devenir la session neuve, et la conversation écoulée est d'abord déplacée sous un nouvel identifiant. Dans les deux cas rien n'est perdu sur disque, et un rechargement ultérieur du fil rend la conversation courante, pas celle que `/new` a fermée. |
| 30 | 30 | |
| 31 | +## Pourquoi les erreurs sont enregistrées à côté de l'historique, pas dedans | |
| 32 | + | |
| 33 | +Un tour peut échouer — serveur arrêté, modèle non téléchargé, chien de garde qui renonce, résumé de compression revenu vide — et la façade le dit en une ligne rouge. Cette ligne était la seule trace : partie avec le défilement, absente du fichier, parce que le fichier contient ce que voit le modèle et qu'un tour raté ne lui laisse rien à voir (la question sans réponse est retirée). Depuis le 18 septembre 2026, le fichier de session porte aussi un tableau `errors` : un enregistrement par ligne affichée, avec l'heure, le nombre de tours, la première ligne de la question en cours, la ligne telle qu'affichée, et l'erreur brute quand elle en dit plus. Trois sortes sont enregistrées : `generation` (une réponse échouée), `compaction` (un résumé échoué, automatique ou `/compact`) et `resume` (un `/resume` qui n'a pas pu charger sa session). Un tour annulé n'est pas une erreur et n'est pas enregistré. | |
| 34 | + | |
| 35 | +Le tableau est à côté de `messages`, pas dedans, à dessein. Le modèle ne le lit jamais : une erreur concerne l'exécution, pas le projet, et la lui renvoyer lui ferait expliquer son infrastructure au lieu de faire sa tâche. `/resume` et `session/load` ne le rejouent jamais : tous deux ne lisent que `messages`, donc une vieille session se charge exactement comme avant et une nouvelle avec des erreurs se charge comme si le champ n'existait pas. Et la compression n'y touche pas : `/compact` réécrit `messages`, la trace reste entière. L'enregistrement suit sa conversation : en ACP, `/new` le déplace dans le fichier archivé avec l'historique auquel il appartient. | |
| 36 | + | |
| 37 | +À quoi cela sert : demander après coup, le fichier en main, pourquoi une exécution a mal tourné — `jq .errors <fichier>` pour un humain, ou le fichier confié à un agent de code à qui l'on demande d'expliquer les échecs et de proposer des changements de code ou de YAML (un `watchdogTimeout` plus long, un autre modèle, un `maxOutput` plus petit). La liste `/sessions` et le compte rendu de `/resume` affichent ` · ⚠️ N error(s)` pour qu'une session à examiner se remarque. La trace est plafonnée à 200 enregistrements, les plus anciens sortent d'abord, pour qu'une exécution qui échoue à chaque tour ne fasse pas grossir son fichier sans fin. | |
| 38 | + | |
| 31 | 39 | ## Alternatives écartées |
| 32 | 40 | |
| 33 | 41 | - **Reprendre automatiquement la dernière session au lancement.** Écartée pour la raison du contexte périmé, et parce que docker agent ne le fait pas non plus. |
| @@ -35,6 +43,7 @@ Dans le terminal, `/new` laisse simplement l'ancien fichier tel quel et la quest | ||
| 35 | 43 | - **SQLite, comme docker agent.** Écarté pour un agent minimal : une dépendance de plus, des fichiers opaques, et une base par utilisateur là où mini-me veut des fichiers par projet. |
| 36 | 44 | - **`session/resume` (instable) plutôt que `session/load`.** Écarté : hors de la spécification stable, et dépendant d'un éditeur qui aurait gardé le fil. |
| 37 | 45 | - **Ne rejouer que les messages texte.** Écarté par l'utilisateur : les appels d'outils sont la partie d'une session de code qui vaut d'être revue. |
| 46 | +- **Enregistrer les erreurs comme des messages de l'historique.** Écarté : le modèle les lirait, `session/load` devrait les rejouer sous une forme ou une autre, et un vieux fichier se chargerait autrement qu'un nouveau. Un champ séparé ne coûte rien au chargement et ne change rien pour le modèle. | |
| 38 | 47 | |
| 39 | 48 | ## Liens avec le reste |
| 40 | 49 | |
| @@ -28,6 +28,14 @@ Une session rechargée ou reprise continue sous le prompt système actuel, pas c | |||
| 28 | 28 | ||
| 29 | Dans le terminal, `/new` laisse simplement l'ancien fichier tel quel et la question suivante en ouvre un nouveau. En ACP, l'éditeur possède l'identifiant de session et continue de l'utiliser : le fichier de cet identifiant doit devenir la session neuve, et la conversation écoulée est d'abord déplacée sous un nouvel identifiant. Dans les deux cas rien n'est perdu sur disque, et un rechargement ultérieur du fil rend la conversation courante, pas celle que `/new` a fermée. | 29 | Dans le terminal, `/new` laisse simplement l'ancien fichier tel quel et la question suivante en ouvre un nouveau. En ACP, l'éditeur possède l'identifiant de session et continue de l'utiliser : le fichier de cet identifiant doit devenir la session neuve, et la conversation écoulée est d'abord déplacée sous un nouvel identifiant. Dans les deux cas rien n'est perdu sur disque, et un rechargement ultérieur du fil rend la conversation courante, pas celle que `/new` a fermée. |
| 30 | 30 | ||
| 31 | +## Pourquoi les erreurs sont enregistrées à côté de l'historique, pas dedans | ||
| 32 | + | ||
| 33 | +Un tour peut échouer — serveur arrêté, modèle non téléchargé, chien de garde qui renonce, résumé de compression revenu vide — et la façade le dit en une ligne rouge. Cette ligne était la seule trace : partie avec le défilement, absente du fichier, parce que le fichier contient ce que voit le modèle et qu'un tour raté ne lui laisse rien à voir (la question sans réponse est retirée). Depuis le 18 septembre 2026, le fichier de session porte aussi un tableau `errors` : un enregistrement par ligne affichée, avec l'heure, le nombre de tours, la première ligne de la question en cours, la ligne telle qu'affichée, et l'erreur brute quand elle en dit plus. Trois sortes sont enregistrées : `generation` (une réponse échouée), `compaction` (un résumé échoué, automatique ou `/compact`) et `resume` (un `/resume` qui n'a pas pu charger sa session). Un tour annulé n'est pas une erreur et n'est pas enregistré. | ||
| 34 | + | ||
| 35 | +Le tableau est à côté de `messages`, pas dedans, à dessein. Le modèle ne le lit jamais : une erreur concerne l'exécution, pas le projet, et la lui renvoyer lui ferait expliquer son infrastructure au lieu de faire sa tâche. `/resume` et `session/load` ne le rejouent jamais : tous deux ne lisent que `messages`, donc une vieille session se charge exactement comme avant et une nouvelle avec des erreurs se charge comme si le champ n'existait pas. Et la compression n'y touche pas : `/compact` réécrit `messages`, la trace reste entière. L'enregistrement suit sa conversation : en ACP, `/new` le déplace dans le fichier archivé avec l'historique auquel il appartient. | ||
| 36 | + | ||
| 37 | +À quoi cela sert : demander après coup, le fichier en main, pourquoi une exécution a mal tourné — `jq .errors <fichier>` pour un humain, ou le fichier confié à un agent de code à qui l'on demande d'expliquer les échecs et de proposer des changements de code ou de YAML (un `watchdogTimeout` plus long, un autre modèle, un `maxOutput` plus petit). La liste `/sessions` et le compte rendu de `/resume` affichent ` · ⚠️ N error(s)` pour qu'une session à examiner se remarque. La trace est plafonnée à 200 enregistrements, les plus anciens sortent d'abord, pour qu'une exécution qui échoue à chaque tour ne fasse pas grossir son fichier sans fin. | ||
| 38 | + | ||
| 31 | ## Alternatives écartées | 39 | ## Alternatives écartées |
| 32 | 40 | ||
| 33 | - **Reprendre automatiquement la dernière session au lancement.** Écartée pour la raison du contexte périmé, et parce que docker agent ne le fait pas non plus. | 41 | - **Reprendre automatiquement la dernière session au lancement.** Écartée pour la raison du contexte périmé, et parce que docker agent ne le fait pas non plus. |
| @@ -35,6 +43,7 @@ Dans le terminal, `/new` laisse simplement l'ancien fichier tel quel et la quest | |||
| 35 | - **SQLite, comme docker agent.** Écarté pour un agent minimal : une dépendance de plus, des fichiers opaques, et une base par utilisateur là où mini-me veut des fichiers par projet. | 43 | - **SQLite, comme docker agent.** Écarté pour un agent minimal : une dépendance de plus, des fichiers opaques, et une base par utilisateur là où mini-me veut des fichiers par projet. |
| 36 | - **`session/resume` (instable) plutôt que `session/load`.** Écarté : hors de la spécification stable, et dépendant d'un éditeur qui aurait gardé le fil. | 44 | - **`session/resume` (instable) plutôt que `session/load`.** Écarté : hors de la spécification stable, et dépendant d'un éditeur qui aurait gardé le fil. |
| 37 | - **Ne rejouer que les messages texte.** Écarté par l'utilisateur : les appels d'outils sont la partie d'une session de code qui vaut d'être revue. | 45 | - **Ne rejouer que les messages texte.** Écarté par l'utilisateur : les appels d'outils sont la partie d'une session de code qui vaut d'être revue. |
| 46 | +- **Enregistrer les erreurs comme des messages de l'historique.** Écarté : le modèle les lirait, `session/load` devrait les rejouer sous une forme ou une autre, et un vieux fichier se chargerait autrement qu'un nouveau. Un champ séparé ne coûte rien au chargement et ne change rien pour le modèle. | ||
| 38 | 47 | ||
| 39 | ## Liens avec le reste | 48 | ## Liens avec le reste |
| 40 | 49 | ||
modified
docs/fr/how-to/keep-sessions.md +36 -1 | @@ -43,6 +43,40 @@ Ce guide montre comment faire enregistrer par `mm` chaque conversation d'un proj | ||
| 43 | 43 | |
| 44 | 44 | Un `/resume` seul prend la conversation la plus récemment mise à jour autre que la courante, en ignorant les sessions vides ; `/resume <id>` en désigne une. À partir de là, le modèle voit l'historique repris — sous le prompt système actuel — et chaque nouveau tour est enregistré dans le fichier de cette session. Rien n'est rejoué à l'écran : la ligne dit ce qui est revenu. |
| 45 | 45 | |
| 46 | +## Comprendre pourquoi une exécution a échoué | |
| 47 | + | |
| 48 | +1. Repérez la session dans la liste : une session qui a affiché au moins une erreur termine sa ligne par ` · ⚠️ N error(s)`. La même queue apparaît dans le compte rendu `↩️ resumed …`. | |
| 49 | + | |
| 50 | + ```text | |
| 51 | + > /sessions | |
| 52 | + 2 session(s) in /work/project/.mm/sessions — /resume <id> to continue one, /resume for the latest | |
| 53 | + * 20260918-141321-db582a3d 2026-09-18 16:13 1 turn(s) say just: ok | |
| 54 | + 20260918-141225-02790c82 2026-09-18 16:12 0 turn(s) · ⚠️ 1 error(s) | |
| 55 | + ``` | |
| 56 | + | |
| 57 | +2. Lisez le tableau `errors` de son fichier. Chaque enregistrement dit quand, à quel tour, sur quelle question, ce que l'écran a montré (`message`) et, quand l'erreur brute en dit plus, ce qu'elle était (`detail`) ; `where` vaut `generation`, `compaction` ou `resume` : | |
| 58 | + | |
| 59 | + ```bash | |
| 60 | + jq .errors .mm/sessions/20260918-141225-02790c82.json | |
| 61 | + ``` | |
| 62 | + | |
| 63 | + ```json | |
| 64 | + [ | |
| 65 | + { | |
| 66 | + "at": "2026-09-18T14:12:25Z", | |
| 67 | + "turn": 1, | |
| 68 | + "where": "generation", | |
| 69 | + "message": "dmr: \"no/such-model:none\" not found at http://localhost:12434/engines/v1 — docker model pull no/such-model:none", | |
| 70 | + "detail": "failed to create completion: POST \"http://localhost:12434/engines/v1/chat/completions\": 404 Not Found", | |
| 71 | + "question": "hello" | |
| 72 | + } | |
| 73 | + ] | |
| 74 | + ``` | |
| 75 | + | |
| 76 | +3. Pour faire analyser l'échec, confiez le fichier à un agent de code (Claude Code, ou `mm` lui-même dans un autre projet) et demandez ce qui s'est passé et quoi changer — dans le code, ou dans `agent.yaml` (`watchdogTimeout`, `maxOutput`, `model`, `context.*`). L'historique du même fichier donne le contexte qu'avait le modèle ; le tableau `errors` donne ce qu'il n'a jamais vu. | |
| 77 | + | |
| 78 | + Une session dont le seul contenu est une erreur n'a aucun tour : un `/resume` seul la saute, `/resume <id>` l'atteint. | |
| 79 | + | |
| 46 | 80 | ## Depuis un éditeur (ACP) |
| 47 | 81 | |
| 48 | 82 | 1. Gardez `sessions.enabled: true` dans le fichier désigné par `AGENT_CONFIG`. Chaque fil ouvert par l'éditeur est une session, enregistrée sous `<projet>/.mm/sessions/<id>.json` dès sa création. |
| @@ -55,7 +89,8 @@ Ce guide montre comment faire enregistrer par `mm` chaque conversation d'un proj | ||
| 55 | 89 | |
| 56 | 90 | - **Un seul dépôt pour tous les projets.** Donnez un chemin absolu à `dir`. Les sessions de différents projets partagent alors un répertoire ; chaque fichier enregistre son `cwd`. |
| 57 | 91 | - **Un projet en lecture seule.** La bannière ou le journal `[acp]` indique `not saved` avec la raison ; la conversation continue sans être enregistrée. |
| 58 | -- **Compression.** Un `/compact` ou une compression automatique réécrit le fichier avec l'historique raccourci ; une session reprise ou rechargée repart du résumé, comme la session en direct. | |
| 92 | +- **Compression.** Un `/compact` ou une compression automatique réécrit le fichier avec l'historique raccourci ; une session reprise ou rechargée repart du résumé, comme la session en direct. Le tableau `errors` reste tel quel. | |
| 93 | +- **Erreurs en ACP.** L'éditeur montre l'échec à sa manière ; l'enregistrement va quand même dans le fichier du fil, et `/new` le déplace dans la conversation archivée avec l'historique auquel il appartient. | |
| 59 | 94 | - **Repartir de zéro volontairement.** Rien à faire : chaque lancement est une nouvelle session. Les anciennes ne reviennent que sur `/resume` ou sur un rechargement depuis l'éditeur. |
| 60 | 95 | |
| 61 | 96 | ## Voir aussi |
| @@ -43,6 +43,40 @@ Ce guide montre comment faire enregistrer par `mm` chaque conversation d'un proj | |||
| 43 | 43 | ||
| 44 | Un `/resume` seul prend la conversation la plus récemment mise à jour autre que la courante, en ignorant les sessions vides ; `/resume <id>` en désigne une. À partir de là, le modèle voit l'historique repris — sous le prompt système actuel — et chaque nouveau tour est enregistré dans le fichier de cette session. Rien n'est rejoué à l'écran : la ligne dit ce qui est revenu. | 44 | Un `/resume` seul prend la conversation la plus récemment mise à jour autre que la courante, en ignorant les sessions vides ; `/resume <id>` en désigne une. À partir de là, le modèle voit l'historique repris — sous le prompt système actuel — et chaque nouveau tour est enregistré dans le fichier de cette session. Rien n'est rejoué à l'écran : la ligne dit ce qui est revenu. |
| 45 | 45 | ||
| 46 | +## Comprendre pourquoi une exécution a échoué | ||
| 47 | + | ||
| 48 | +1. Repérez la session dans la liste : une session qui a affiché au moins une erreur termine sa ligne par ` · ⚠️ N error(s)`. La même queue apparaît dans le compte rendu `↩️ resumed …`. | ||
| 49 | + | ||
| 50 | + ```text | ||
| 51 | + > /sessions | ||
| 52 | + 2 session(s) in /work/project/.mm/sessions — /resume <id> to continue one, /resume for the latest | ||
| 53 | + * 20260918-141321-db582a3d 2026-09-18 16:13 1 turn(s) say just: ok | ||
| 54 | + 20260918-141225-02790c82 2026-09-18 16:12 0 turn(s) · ⚠️ 1 error(s) | ||
| 55 | + ``` | ||
| 56 | + | ||
| 57 | +2. Lisez le tableau `errors` de son fichier. Chaque enregistrement dit quand, à quel tour, sur quelle question, ce que l'écran a montré (`message`) et, quand l'erreur brute en dit plus, ce qu'elle était (`detail`) ; `where` vaut `generation`, `compaction` ou `resume` : | ||
| 58 | + | ||
| 59 | + ```bash | ||
| 60 | + jq .errors .mm/sessions/20260918-141225-02790c82.json | ||
| 61 | + ``` | ||
| 62 | + | ||
| 63 | + ```json | ||
| 64 | + [ | ||
| 65 | + { | ||
| 66 | + "at": "2026-09-18T14:12:25Z", | ||
| 67 | + "turn": 1, | ||
| 68 | + "where": "generation", | ||
| 69 | + "message": "dmr: \"no/such-model:none\" not found at http://localhost:12434/engines/v1 — docker model pull no/such-model:none", | ||
| 70 | + "detail": "failed to create completion: POST \"http://localhost:12434/engines/v1/chat/completions\": 404 Not Found", | ||
| 71 | + "question": "hello" | ||
| 72 | + } | ||
| 73 | + ] | ||
| 74 | + ``` | ||
| 75 | + | ||
| 76 | +3. Pour faire analyser l'échec, confiez le fichier à un agent de code (Claude Code, ou `mm` lui-même dans un autre projet) et demandez ce qui s'est passé et quoi changer — dans le code, ou dans `agent.yaml` (`watchdogTimeout`, `maxOutput`, `model`, `context.*`). L'historique du même fichier donne le contexte qu'avait le modèle ; le tableau `errors` donne ce qu'il n'a jamais vu. | ||
| 77 | + | ||
| 78 | + Une session dont le seul contenu est une erreur n'a aucun tour : un `/resume` seul la saute, `/resume <id>` l'atteint. | ||
| 79 | + | ||
| 46 | ## Depuis un éditeur (ACP) | 80 | ## Depuis un éditeur (ACP) |
| 47 | 81 | ||
| 48 | 1. Gardez `sessions.enabled: true` dans le fichier désigné par `AGENT_CONFIG`. Chaque fil ouvert par l'éditeur est une session, enregistrée sous `<projet>/.mm/sessions/<id>.json` dès sa création. | 82 | 1. Gardez `sessions.enabled: true` dans le fichier désigné par `AGENT_CONFIG`. Chaque fil ouvert par l'éditeur est une session, enregistrée sous `<projet>/.mm/sessions/<id>.json` dès sa création. |
| @@ -55,7 +89,8 @@ Ce guide montre comment faire enregistrer par `mm` chaque conversation d'un proj | |||
| 55 | 89 | ||
| 56 | - **Un seul dépôt pour tous les projets.** Donnez un chemin absolu à `dir`. Les sessions de différents projets partagent alors un répertoire ; chaque fichier enregistre son `cwd`. | 90 | - **Un seul dépôt pour tous les projets.** Donnez un chemin absolu à `dir`. Les sessions de différents projets partagent alors un répertoire ; chaque fichier enregistre son `cwd`. |
| 57 | - **Un projet en lecture seule.** La bannière ou le journal `[acp]` indique `not saved` avec la raison ; la conversation continue sans être enregistrée. | 91 | - **Un projet en lecture seule.** La bannière ou le journal `[acp]` indique `not saved` avec la raison ; la conversation continue sans être enregistrée. |
| 58 | -- **Compression.** Un `/compact` ou une compression automatique réécrit le fichier avec l'historique raccourci ; une session reprise ou rechargée repart du résumé, comme la session en direct. | 92 | +- **Compression.** Un `/compact` ou une compression automatique réécrit le fichier avec l'historique raccourci ; une session reprise ou rechargée repart du résumé, comme la session en direct. Le tableau `errors` reste tel quel. |
| 93 | +- **Erreurs en ACP.** L'éditeur montre l'échec à sa manière ; l'enregistrement va quand même dans le fichier du fil, et `/new` le déplace dans la conversation archivée avec l'historique auquel il appartient. | ||
| 59 | - **Repartir de zéro volontairement.** Rien à faire : chaque lancement est une nouvelle session. Les anciennes ne reviennent que sur `/resume` ou sur un rechargement depuis l'éditeur. | 94 | - **Repartir de zéro volontairement.** Rien à faire : chaque lancement est une nouvelle session. Les anciennes ne reviennent que sur `/resume` ou sur un rechargement depuis l'éditeur. |
| 60 | 95 | ||
| 61 | ## Voir aussi | 96 | ## Voir aussi |
modified
docs/fr/reference/configuration.md +1 -1 | @@ -43,7 +43,7 @@ | ||
| 43 | 43 | |
| 44 | 44 | | Clé | Type | Défaut | Description | |
| 45 | 45 | |-----|------|--------|-------------| |
| 46 | -| `sessions.enabled` | bool | `false` | Enregistre chaque conversation dans un fichier, réécrit après chaque tour, `/compact` et `/new`. Active `/sessions` et `/resume` dans le terminal et `session/load` en ACP. | | |
| 46 | +| `sessions.enabled` | bool | `false` | Enregistre chaque conversation dans un fichier, réécrit après chaque tour, `/compact` et `/new`. Le fichier enregistre aussi, hors de l'historique, chaque erreur affichée (tableau `errors` : génération échouée, compression échouée, `/resume` échoué). Active `/sessions` et `/resume` dans le terminal et `session/load` en ACP. | | |
| 47 | 47 | | `sessions.dir` | chaîne | `.mm/sessions` | Répertoire des fichiers de session, un `<id>.json` chacun. Relatif au répertoire de lancement de `mm` (terminal) ou au `cwd` de la session (ACP) ; un chemin absolu est utilisé tel quel. Ne doit pas être vide. | |
| 48 | 48 | |
| 49 | 49 | ## Valeurs par défaut des fournisseurs |
| @@ -43,7 +43,7 @@ | |||
| 43 | 43 | ||
| 44 | | Clé | Type | Défaut | Description | | 44 | | Clé | Type | Défaut | Description | |
| 45 | |-----|------|--------|-------------| | 45 | |-----|------|--------|-------------| |
| 46 | -| `sessions.enabled` | bool | `false` | Enregistre chaque conversation dans un fichier, réécrit après chaque tour, `/compact` et `/new`. Active `/sessions` et `/resume` dans le terminal et `session/load` en ACP. | | 46 | +| `sessions.enabled` | bool | `false` | Enregistre chaque conversation dans un fichier, réécrit après chaque tour, `/compact` et `/new`. Le fichier enregistre aussi, hors de l'historique, chaque erreur affichée (tableau `errors` : génération échouée, compression échouée, `/resume` échoué). Active `/sessions` et `/resume` dans le terminal et `session/load` en ACP. | |
| 47 | | `sessions.dir` | chaîne | `.mm/sessions` | Répertoire des fichiers de session, un `<id>.json` chacun. Relatif au répertoire de lancement de `mm` (terminal) ou au `cwd` de la session (ACP) ; un chemin absolu est utilisé tel quel. Ne doit pas être vide. | | 47 | | `sessions.dir` | chaîne | `.mm/sessions` | Répertoire des fichiers de session, un `<id>.json` chacun. Relatif au répertoire de lancement de `mm` (terminal) ou au `cwd` de la session (ACP) ; un chemin absolu est utilisé tel quel. Ne doit pas être vide. | |
| 48 | 48 | ||
| 49 | ## Valeurs par défaut des fournisseurs | 49 | ## Valeurs par défaut des fournisseurs |
modified
docs/fr/reference/slash-commands.md +2 -2 | @@ -10,8 +10,8 @@ | ||
| 10 | 10 | | `/abort` | Affiche `⚠️ No generation in progress.` | Annule la génération, affiche `🛑 Aborting...` ; les commandes déjà exécutées restent dans l'historique. | |
| 11 | 11 | | `/compact` | Compresse l'historique immédiatement, sans tenir compte du seuil ; affiche le rapport `🗜️`, ou `🗜️ nothing to compact` quand aucun tour n'est plus ancien que `context.keepLastTurns`. | Envoyée au modèle comme nouveau message (voir ci-dessous). | |
| 12 | 12 | | `/new` | Ramène l'historique au seul prompt système, réinitialise le détecteur de boucles et oublie le dernier compte de tokens du serveur ; affiche `🆕 new session: N message(s) forgotten` (N compte tous les messages sauf le prompt système). | Traitée comme n'importe quel texte : annule la génération, conserve son historique partiel, puis s'exécute comme `/new` sur cet historique. | |
| 13 | -| `/sessions` | Liste les sessions enregistrées du projet, la plus récente d'abord : identifiant, dernière mise à jour, nombre de questions, première question ; `*` marque la session courante. Avec `sessions.enabled: false` : `⚠️ sessions are off (set sessions.enabled: true in the YAML)`. | Envoyée au modèle comme nouveau message. | | |
| 14 | -| `/resume [id]` | Reprend une session enregistrée : la plus récemment mise à jour autre que la courante, en ignorant les vides, ou l'identifiant donné. Affiche `↩️ resumed <id> — N message(s), started with "<première question>"` ; réinitialise le détecteur de boucles et le compte de tokens du serveur ; les tours suivants sont enregistrés dans cette session. Refusée avec une raison `⚠️` quand les sessions sont désactivées, qu'il n'y a pas de session antérieure, que l'identifiant est inconnu ou désigne la session courante. | Envoyée au modèle comme nouveau message. | | |
| 13 | +| `/sessions` | Liste les sessions enregistrées du projet, la plus récente d'abord : identifiant, dernière mise à jour, nombre de questions, première question, puis ` · ⚠️ N error(s)` quand la session a enregistré des erreurs ; `*` marque la session courante. Avec `sessions.enabled: false` : `⚠️ sessions are off (set sessions.enabled: true in the YAML)`. | Envoyée au modèle comme nouveau message. | | |
| 14 | +| `/resume [id]` | Reprend une session enregistrée : la plus récemment mise à jour autre que la courante, en ignorant les vides, ou l'identifiant donné. Affiche `↩️ resumed <id> — N message(s), started with "<première question>"` (suivi de ` · ⚠️ N error(s)` quand la session a enregistré des erreurs) ; réinitialise le détecteur de boucles et le compte de tokens du serveur ; les tours suivants sont enregistrés dans cette session. Refusée avec une raison `⚠️` quand les sessions sont désactivées, qu'il n'y a pas de session antérieure, que l'identifiant est inconnu ou désigne la session courante. | Envoyée au modèle comme nouveau message. | | |
| 15 | 15 | | `/help` | Affiche les commandes de cette façade, une par ligne avec leur description, alignées. | Envoyée au modèle comme nouveau message. | |
| 16 | 16 | | `/copy [n\|all]` | Copie la dernière réponse (`n` : la n-ième depuis la fin) dans le presse-papiers via `pbcopy`/`wl-copy`/`xclip`/`xsel` ou, à défaut, OSC 52 quand stdout est un terminal, et affiche `📋 copied N chars (<chemin>)` ou `⚠️ …`. `/copy all` est réservé au mode fenêtre : ici la commande le dit. | Envoyée au modèle comme nouveau message. | |
| 17 | 17 | |
| @@ -10,8 +10,8 @@ | |||
| 10 | | `/abort` | Affiche `⚠️ No generation in progress.` | Annule la génération, affiche `🛑 Aborting...` ; les commandes déjà exécutées restent dans l'historique. | | 10 | | `/abort` | Affiche `⚠️ No generation in progress.` | Annule la génération, affiche `🛑 Aborting...` ; les commandes déjà exécutées restent dans l'historique. | |
| 11 | | `/compact` | Compresse l'historique immédiatement, sans tenir compte du seuil ; affiche le rapport `🗜️`, ou `🗜️ nothing to compact` quand aucun tour n'est plus ancien que `context.keepLastTurns`. | Envoyée au modèle comme nouveau message (voir ci-dessous). | | 11 | | `/compact` | Compresse l'historique immédiatement, sans tenir compte du seuil ; affiche le rapport `🗜️`, ou `🗜️ nothing to compact` quand aucun tour n'est plus ancien que `context.keepLastTurns`. | Envoyée au modèle comme nouveau message (voir ci-dessous). | |
| 12 | | `/new` | Ramène l'historique au seul prompt système, réinitialise le détecteur de boucles et oublie le dernier compte de tokens du serveur ; affiche `🆕 new session: N message(s) forgotten` (N compte tous les messages sauf le prompt système). | Traitée comme n'importe quel texte : annule la génération, conserve son historique partiel, puis s'exécute comme `/new` sur cet historique. | | 12 | | `/new` | Ramène l'historique au seul prompt système, réinitialise le détecteur de boucles et oublie le dernier compte de tokens du serveur ; affiche `🆕 new session: N message(s) forgotten` (N compte tous les messages sauf le prompt système). | Traitée comme n'importe quel texte : annule la génération, conserve son historique partiel, puis s'exécute comme `/new` sur cet historique. | |
| 13 | -| `/sessions` | Liste les sessions enregistrées du projet, la plus récente d'abord : identifiant, dernière mise à jour, nombre de questions, première question ; `*` marque la session courante. Avec `sessions.enabled: false` : `⚠️ sessions are off (set sessions.enabled: true in the YAML)`. | Envoyée au modèle comme nouveau message. | | 13 | +| `/sessions` | Liste les sessions enregistrées du projet, la plus récente d'abord : identifiant, dernière mise à jour, nombre de questions, première question, puis ` · ⚠️ N error(s)` quand la session a enregistré des erreurs ; `*` marque la session courante. Avec `sessions.enabled: false` : `⚠️ sessions are off (set sessions.enabled: true in the YAML)`. | Envoyée au modèle comme nouveau message. | |
| 14 | -| `/resume [id]` | Reprend une session enregistrée : la plus récemment mise à jour autre que la courante, en ignorant les vides, ou l'identifiant donné. Affiche `↩️ resumed <id> — N message(s), started with "<première question>"` ; réinitialise le détecteur de boucles et le compte de tokens du serveur ; les tours suivants sont enregistrés dans cette session. Refusée avec une raison `⚠️` quand les sessions sont désactivées, qu'il n'y a pas de session antérieure, que l'identifiant est inconnu ou désigne la session courante. | Envoyée au modèle comme nouveau message. | | 14 | +| `/resume [id]` | Reprend une session enregistrée : la plus récemment mise à jour autre que la courante, en ignorant les vides, ou l'identifiant donné. Affiche `↩️ resumed <id> — N message(s), started with "<première question>"` (suivi de ` · ⚠️ N error(s)` quand la session a enregistré des erreurs) ; réinitialise le détecteur de boucles et le compte de tokens du serveur ; les tours suivants sont enregistrés dans cette session. Refusée avec une raison `⚠️` quand les sessions sont désactivées, qu'il n'y a pas de session antérieure, que l'identifiant est inconnu ou désigne la session courante. | Envoyée au modèle comme nouveau message. | |
| 15 | | `/help` | Affiche les commandes de cette façade, une par ligne avec leur description, alignées. | Envoyée au modèle comme nouveau message. | | 15 | | `/help` | Affiche les commandes de cette façade, une par ligne avec leur description, alignées. | Envoyée au modèle comme nouveau message. | |
| 16 | | `/copy [n\|all]` | Copie la dernière réponse (`n` : la n-ième depuis la fin) dans le presse-papiers via `pbcopy`/`wl-copy`/`xclip`/`xsel` ou, à défaut, OSC 52 quand stdout est un terminal, et affiche `📋 copied N chars (<chemin>)` ou `⚠️ …`. `/copy all` est réservé au mode fenêtre : ici la commande le dit. | Envoyée au modèle comme nouveau message. | | 16 | | `/copy [n\|all]` | Copie la dernière réponse (`n` : la n-ième depuis la fin) dans le presse-papiers via `pbcopy`/`wl-copy`/`xclip`/`xsel` ou, à défaut, OSC 52 quand stdout est un terminal, et affiche `📋 copied N chars (<chemin>)` ou `⚠️ …`. `/copy all` est réservé au mode fenêtre : ici la commande le dit. | Envoyée au modèle comme nouveau message. | |
| 17 | 17 | ||
modified
internal/acp/acp.go +17 -11 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/acp/acp_test.go +10 -9 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/acp/persist.go +12 -1 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/acp/persist_test.go +36 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/agent/agent.go +26 -7 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/store/keeper.go +29 -2 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/store/keeper_test.go +55 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/store/store.go +83 -2 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/store/store_test.go +66 -0 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/tui/turn.go +32 -8 Large diff collapsed to keep the page fast — load it when you need it.
modified
internal/tui/turn_test.go +31 -0 Large diff collapsed to keep the page fast — load it when you need it.
added
space_invader.html +27 -0 Large diff collapsed to keep the page fast — load it when you need it.