bots-garden/mini-mepublic Fork
⑂ 2db607d
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

Merge feature/tasks-builtin into main

rickub committed 2026-09-22T20:58:58Z Browse files
2db607d parents: d080878 75ffe85
modified .tickets/issues/0004-do-we-need-mcp.yaml +1 -2
@@ -6,9 +6,8 @@ author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-09-15T22:38:45.129Z
9-updatedAt: 2026-09-21T19:04:58.320Z
9+updatedAt: 2026-09-22T20:57:39.912Z
1010 labels: []
11-milestone: v0.04
1211 body: |
1312 I would say no - use a CLI
1413
@@ -6,9 +6,8 @@ author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-09-15T22:38:45.129Z8 createdAt: 2026-09-15T22:38:45.129Z
9-updatedAt: 2026-09-21T19:04:58.320Z9+updatedAt: 2026-09-22T20:57:39.912Z
10 labels: []10 labels: []
11-milestone: v0.04
12 body: |11 body: |
13 I would say no - use a CLI12 I would say no - use a CLI
14 13
modified .tickets/issues/0025-tasks-plan-builtin.yaml +26 -5
@@ -1,14 +1,35 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
22 id: 25
33 title: Tasks / Plan builtin
4-state: open
4+state: closed
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-09-19T16:06:33.119Z
9-updatedAt: 2026-09-19T16:06:33.119Z
10-labels: []
11-milestone: v0.04
12-body: ""
9+updatedAt: 2026-09-22T20:54:40.140Z
10+labels:
11+ - kind::feature
12+ - builtin tool
13+ - priority::medium
14+milestone: v0.0.4
15+body: |
16+ ## Implementation note (Mistral Vibe, 2026-09-22)
17+
18+ Model-driven plan management: when user gives a list of tasks, the agent autonomously creates and tracks a plan.
19+
20+ **Components:**
21+ - `internal/tools/plan.go`: plan tool with in-memory PlanStore (session-scoped)
22+ - Actions: create, get, update, next, status, list, delete
23+ - `skills/plan-manager/SKILL.md`: detection patterns and usage guidelines
24+ - Config: `planTool: true/false` in agent.yaml (default: true)
25+ - Integration: plans cleared on `/new` command
26+
27+ **Storage:** Option A (in-memory, per-session). Lost on restart.
28+
29+ **Display:** Status line in responses (e.g., `[2/5] Task 2: Fix bugs (in_progress)`)
30+
31+ **Tests:** `internal/tools/plan_test.go` - all passing
32+
33+ **Docs:** EN+FR how-to guides in `docs/en/how-to/manage-plans-and-tasks.md` and `docs/fr/how-to/manage-plans-and-tasks.md`
1334 tasks: []
1435 comments: []
@@ -1,14 +1,35 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2 id: 252 id: 25
3 title: Tasks / Plan builtin3 title: Tasks / Plan builtin
4-state: open4+state: closed
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-09-19T16:06:33.119Z8 createdAt: 2026-09-19T16:06:33.119Z
9-updatedAt: 2026-09-19T16:06:33.119Z9+updatedAt: 2026-09-22T20:54:40.140Z
10-labels: []10+labels:
11-milestone: v0.0411+ - kind::feature
12-body: ""12+ - builtin tool
13+ - priority::medium
14+milestone: v0.0.4
15+body: |
16+ ## Implementation note (Mistral Vibe, 2026-09-22)
17+
18+ Model-driven plan management: when user gives a list of tasks, the agent autonomously creates and tracks a plan.
19+
20+ **Components:**
21+ - `internal/tools/plan.go`: plan tool with in-memory PlanStore (session-scoped)
22+ - Actions: create, get, update, next, status, list, delete
23+ - `skills/plan-manager/SKILL.md`: detection patterns and usage guidelines
24+ - Config: `planTool: true/false` in agent.yaml (default: true)
25+ - Integration: plans cleared on `/new` command
26+
27+ **Storage:** Option A (in-memory, per-session). Lost on restart.
28+
29+ **Display:** Status line in responses (e.g., `[2/5] Task 2: Fix bugs (in_progress)`)
30+
31+ **Tests:** `internal/tools/plan_test.go` - all passing
32+
33+ **Docs:** EN+FR how-to guides in `docs/en/how-to/manage-plans-and-tasks.md` and `docs/fr/how-to/manage-plans-and-tasks.md`
13 tasks: []34 tasks: []
14 comments: []35 comments: []
modified .tickets/issues/0030-how-to-use-mini-me-with-claude.yaml +1 -2
@@ -6,9 +6,8 @@ author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-09-21T04:00:56.971Z
9-updatedAt: 2026-09-21T19:03:27.354Z
9+updatedAt: 2026-09-22T20:57:22.310Z
1010 labels: []
11-milestone: v0.04
1211 body: |
1312 ## Analysis (Claude, 2026-09-21 — not implemented)
1413
@@ -6,9 +6,8 @@ author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-09-21T04:00:56.971Z8 createdAt: 2026-09-21T04:00:56.971Z
9-updatedAt: 2026-09-21T19:03:27.354Z9+updatedAt: 2026-09-22T20:57:22.310Z
10 labels: []10 labels: []
11-milestone: v0.04
12 body: |11 body: |
13 ## Analysis (Claude, 2026-09-21 — not implemented)12 ## Analysis (Claude, 2026-09-21 — not implemented)
14 13
modified .tickets/issues/0031-how-to-use-mini-me-with-gemini.yaml +1 -2
@@ -6,9 +6,8 @@ author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-09-21T04:01:15.166Z
9-updatedAt: 2026-09-21T19:04:28.691Z
9+updatedAt: 2026-09-22T20:56:55.345Z
1010 labels: []
11-milestone: v0.04
1211 body: |
1312 ## Analysis (Claude, 2026-09-21 — not implemented)
1413
@@ -6,9 +6,8 @@ author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-09-21T04:01:15.166Z8 createdAt: 2026-09-21T04:01:15.166Z
9-updatedAt: 2026-09-21T19:04:28.691Z9+updatedAt: 2026-09-22T20:56:55.345Z
10 labels: []10 labels: []
11-milestone: v0.04
12 body: |11 body: |
13 ## Analysis (Claude, 2026-09-21 — not implemented)12 ## Analysis (Claude, 2026-09-21 — not implemented)
14 13
modified .tickets/issues/0032-add-prompt-flag.yaml +2 -2
@@ -6,9 +6,9 @@ author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-09-21T04:47:19.089Z
9-updatedAt: 2026-09-21T04:58:31.828Z
9+updatedAt: 2026-09-22T20:54:40.140Z
1010 labels: []
11-milestone: v0.04
11+milestone: v0.0.4
1212 epic: CLI
1313 body: ""
1414 tasks: []
@@ -6,9 +6,9 @@ author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-09-21T04:47:19.089Z8 createdAt: 2026-09-21T04:47:19.089Z
9-updatedAt: 2026-09-21T04:58:31.828Z9+updatedAt: 2026-09-22T20:54:40.140Z
10 labels: []10 labels: []
11-milestone: v0.0411+milestone: v0.0.4
12 epic: CLI12 epic: CLI
13 body: ""13 body: ""
14 tasks: []14 tasks: []
modified .tickets/issues/0033-invoke-another-mini-me.yaml +1 -2
@@ -6,9 +6,8 @@ author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-09-21T04:48:03.596Z
9-updatedAt: 2026-09-21T04:58:54.762Z
9+updatedAt: 2026-09-22T20:56:40.361Z
1010 labels: []
11-milestone: v0.04
1211 epic: Builtin tools
1312 body: |
1413 Should I use ACP ? I think yes
@@ -6,9 +6,8 @@ author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-09-21T04:48:03.596Z8 createdAt: 2026-09-21T04:48:03.596Z
9-updatedAt: 2026-09-21T04:58:54.762Z9+updatedAt: 2026-09-22T20:56:40.361Z
10 labels: []10 labels: []
11-milestone: v0.04
12 epic: Builtin tools11 epic: Builtin tools
13 body: |12 body: |
14 Should I use ACP ? I think yes13 Should I use ACP ? I think yes
modified .tickets/issues/0035-create-sbx-template-kit-for-mini-me.yaml +2 -2
@@ -1,12 +1,12 @@
11 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
22 id: 35
33 title: Create SBX Template + Kit for Mini-Me
4-state: open
4+state: closed
55 author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-09-21T04:51:17.051Z
9-updatedAt: 2026-09-21T19:06:52.839Z
9+updatedAt: 2026-09-22T20:55:22.352Z
1010 labels: []
1111 milestone: v0.0.3
1212 body: |
@@ -1,12 +1,12 @@
1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.1 # Managed by IssueSpec. Hand edits are welcome; keep the schema valid.
2 id: 352 id: 35
3 title: Create SBX Template + Kit for Mini-Me3 title: Create SBX Template + Kit for Mini-Me
4-state: open4+state: closed
5 author:5 author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-09-21T04:51:17.051Z8 createdAt: 2026-09-21T04:51:17.051Z
9-updatedAt: 2026-09-21T19:06:52.839Z9+updatedAt: 2026-09-22T20:55:22.352Z
10 labels: []10 labels: []
11 milestone: v0.0.311 milestone: v0.0.3
12 body: |12 body: |
modified .tickets/issues/0036-hooks.yaml +2 -2
@@ -6,9 +6,9 @@ author:
66 name: k33g
77 email: ph.charriere@gmail.com
88 createdAt: 2026-09-21T06:32:48.566Z
9-updatedAt: 2026-09-21T06:32:48.566Z
9+updatedAt: 2026-09-22T20:54:40.140Z
1010 labels: []
11-milestone: v0.04
11+milestone: v0.0.4
1212 body: ""
1313 tasks: []
1414 comments: []
@@ -6,9 +6,9 @@ author:
6 name: k33g6 name: k33g
7 email: ph.charriere@gmail.com7 email: ph.charriere@gmail.com
8 createdAt: 2026-09-21T06:32:48.566Z8 createdAt: 2026-09-21T06:32:48.566Z
9-updatedAt: 2026-09-21T06:32:48.566Z9+updatedAt: 2026-09-22T20:54:40.140Z
10 labels: []10 labels: []
11-milestone: v0.0411+milestone: v0.0.4
12 body: ""12 body: ""
13 tasks: []13 tasks: []
14 comments: []14 comments: []
modified .tickets/milestones.yaml +2 -2
@@ -4,8 +4,8 @@ milestones:
44 state: closed
55 dueOn: 2026-09-18
66 - title: v0.0.3
7- state: open
7+ state: closed
88 dueOn: 2026-09-30
9- - title: v0.04
9+ - title: v0.0.4
1010 state: open
1111 dueOn: 2026-10-15
@@ -4,8 +4,8 @@ milestones:
4 state: closed4 state: closed
5 dueOn: 2026-09-185 dueOn: 2026-09-18
6 - title: v0.0.36 - title: v0.0.3
7- state: open7+ state: closed
8 dueOn: 2026-09-308 dueOn: 2026-09-30
9- - title: v0.049+ - title: v0.0.4
10 state: open10 state: open
11 dueOn: 2026-10-1511 dueOn: 2026-10-15
added .vibe/AGENTS.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 .vibe/skills/diataxis-doc/SKILL.md +520 -0
new file mode 100644
@@ -0,0 +1,520 @@
1+---
2+name: diataxis-doc
3+description: Document a project using the Diátaxis methodology (https://diataxis.fr). Organises documentation into four distinct quadrants — Tutorials (learning), How-to guides (solving a task), Reference (looking information up), Explanation (understanding) — along the two axes action/cognition and acquisition/application. Use when creating or restructuring a project's documentation in a rigorous, readable, user-needs-driven way.
4+---
5+# Diátaxis Documentation Skill
6+
7+## Description
8+
9+Guides the creation and restructuring of a project's documentation according to the
10+**Diátaxis** methodology (Daniele Procida — https://diataxis.fr). Diátaxis is not a rigid
11+template: it is a **compass** that guarantees every documentation page serves **exactly one
12+clearly identified user need**, and that all four fundamental needs are covered.
13+
14+The strength of the method: it solves the three problems of any documentation —
15+**content** (what to write), **style** (how to write it) and **architecture**
16+(how to organise it) — by reducing them to two simple questions.
17+
18+## When to Use
19+
20+- When a project has no documentation, or a "catch-all" documentation
21+- When existing docs mix tutorials, recipes, reference and explanation in the same pages
22+- When users "can't find" the information despite voluminous docs
23+- When you want maintainable documentation that grows in small increments
24+- After adding a feature, to document it in the right place without reorganising everything
25+
26+---
27+
28+## Core Principle: the two axes
29+
30+The whole method rests on **two orthogonal axes**. They do not merely cover the territory of
31+documentation — they **define** it. This is why there are **necessarily four quadrants**,
32+neither three nor five.
33+
34+```
35+ ACTION (practical know-how)
36+ ▲
37+ │
38+ TUTORIALS │ HOW-TO GUIDES
39+ (learning by doing) │ (achieving a goal)
40+ │
41+ ACQUISITION ────────────────────┼──────────────────── APPLICATION
42+ (studying a skill) │ (applying a skill)
43+ │
44+ EXPLANATION │ REFERENCE
45+ (understanding, thinking) │ (looking information up)
46+ │
47+ ▼
48+ COGNITION (theoretical knowledge)
49+```
50+
51+| Quadrant | Action/cognition axis | Acquisition/application axis | The user's question |
52+|----------|------------------------|-------------------------------|----------------------|
53+| **Tutorial** | Action | Acquisition | "Teach me to get started" |
54+| **How-to guide** | Action | Application | "How do I do X?" |
55+| **Reference** | Cognition | Application | "What are the exact details of X?" |
56+| **Explanation** | Cognition | Acquisition | "Can you explain why / the context?" |
57+
58+---
59+
60+## The four quadrants in detail
61+
62+### 1. TUTORIALS — learning-oriented
63+
64+A tutorial is a **guided lesson**, an experience under a teacher's direction. Its goal is NOT
65+to accomplish a real task, but to make the beginner **acquire a skill**. The teacher is
66+responsible for the student's success.
67+
68+**Writing rules (mandatory):**
69+- ✅ Announce the **destination** up front ("by the end, you will have built…"), not what will be "learned"
70+- ✅ Produce a **visible result at every step**, however small
71+- ✅ Guarantee **perfect reliability**: every command must work for everyone, every time
72+- ✅ Show the **actual expected output** ("you should see: …")
73+- ✅ Use **"we"** ("we will now…") and the direct imperative ("Type this")
74+- ✅ Flag common mistakes at the point where they can occur
75+- ❌ **Minimise explanation to the extreme** — a tutorial is NOT the place for explanation. Link to it instead.
76+- ❌ **No alternatives, no options, no digressions** — a single path to success
77+- ❌ Do not explain the "why" in detail (it breaks the learning flow)
78+
79+### 2. HOW-TO GUIDES — task-oriented
80+
81+A how-to guide is a **recipe** for reaching a specific goal. It addresses an **already
82+competent** user who knows what they want to do. "How do I deploy?", "How do I enable
83+caching?".
84+
85+> ⚠️ Key distinction from a tutorial: *"Someone who expects a recipe and receives a cooking
86+> lesson will be disappointed and annoyed."* Tutorial = lesson; guide = recipe.
87+
88+**Writing rules:**
89+- ✅ Title = **the task**, stated clearly: "How to {do X}"
90+- ✅ Start from a **real problem**, not from a feature of the tool
91+- ✅ Contain **only the steps needed** for the task
92+- ✅ Stay **adaptable to the real world** ("if you use Y, do this instead") — a guide covers variants
93+- ✅ Assume the user **already knows** what they want to accomplish
94+- ❌ Do not teach the basic concepts (that is the tutorial's job)
95+- ❌ Do not drown the recipe in explanation (link to the explanation)
96+
97+### 3. REFERENCE — information-oriented
98+
99+Reference is a **neutral, exhaustive technical description** of the machinery: the commands,
100+options, parameters, structures. It is a **map of the territory**, austere and reliable, that
101+you **consult** (you do not read it end to end).
102+
103+**Writing rules:**
104+- ✅ **Describe**, do nothing else: no instruction, no explanation, no opinion
105+- ✅ **Accuracy and completeness**: every parameter, default value, type, error
106+- ✅ **Standard, predictable structure**: the same format for every entry (tables, lists)
107+- ✅ **Mirror the product's structure**: the reference's organisation reflects that of the code
108+- ✅ Include usage **examples**, without drifting into explanation
109+- ❌ Do not teach, do not tell the "why" — link to tutorials and explanations
110+
111+> Mental model: a **nutrition label**. Factual, standardised, rigorous, with no recipe and no
112+> marketing pitch.
113+
114+### 4. EXPLANATION — understanding-oriented
115+
116+Explanation **steps back**. It answers "Can you tell me about…?". It connects concepts, gives
117+context, history, design decisions, rejected alternatives. It is the documentation you read
118+"in the bath", away from active work.
119+
120+**Writing rules:**
121+- ✅ **Make connections** between topics, take a wider view
122+- ✅ Give the **context**: history, decisions, constraints, rejected alternatives
123+- ✅ Discuss the **why**, the trade-offs, the possibilities
124+- ✅ **Allow opinion** and acknowledge multiple points of view
125+- ✅ **Stay bounded** to one topic, so it does not absorb content that belongs elsewhere
126+- ❌ Do not give step-by-step instructions (that is the guides'/tutorials' job)
127+- ❌ Do not become a reference (no exhaustive parameter lists)
128+
129+---
130+
131+## The Compass — choosing the right quadrant
132+
133+When you do not know where a piece of content belongs (or when the writing "gets stuck"),
134+answer **two questions**:
135+
136+```
137+1. Does the content inform ACTION or COGNITION?
138+ (practical steps ──vs── theoretical knowledge)
139+
140+2. Does it serve the ACQUISITION or the APPLICATION of a skill?
141+ (the user is learning ──vs── the user is doing)
142+```
143+
144+| If the content says… | …informs | …and serves | → then it is a |
145+|----------------------|----------|-------------|-----------------|
146+| "teach me by doing" | action | acquisition | **Tutorial** |
147+| "help me do this" | action | application | **How-to guide** |
148+| "give me the exact information" | cognition | application | **Reference** |
149+| "help me understand" | cognition | acquisition | **Explanation** |
150+
151+**Symptom of a misfiling:** if a page forces you to mix two styles (e.g. explaining the *why*
152+in the middle of a recipe), it contains two distinct pieces of content → **split it** and link
153+the parts to each other.
154+
155+The compass applies at every scale: a whole document, a section, or even a single sentence.
156+
157+---
158+
159+## Recommended folder architecture
160+
161+Four folders, one per quadrant, plus an index:
162+
163+```
164+docs/ (or ./new.docs, ./documentation, etc.)
165+├── README.md ← index: explains the structure and orients the reader
166+├── tutorials/ ← guided learning
167+│ └── getting-started.md
168+├── how-to/ ← task-oriented recipes
169+│ ├── do-x.md
170+│ └── do-y.md
171+├── reference/ ← exhaustive technical description
172+│ ├── cli.md
173+│ └── configuration.md
174+└── explanation/ ← understanding, context, decisions
175+ ├── architecture.md
176+ └── design-choices.md
177+```
178+
179+**Architecture rules:**
180+- ✅ **Folder names** make the quadrant obvious (tutorials / how-to / reference / explanation)
181+- ✅ The **index** (`README.md`) briefly explains the four types and links to each
182+- ✅ Documents **link to each other**: a tutorial links to the reference and the explanation rather than including them
183+- ✅ Folders may be named in the documentation's own language (`tutoriels/`, `guides-pratiques/`, …) — stay consistent
184+
185+### Bilingual architecture (the "Both" option)
186+
187+When the user wants both languages, use **one subfolder per language**, each containing its own
188+complete four-quadrant structure. A root index acts as the language selector:
189+
190+```
191+docs/
192+├── README.md ← language selector (links to fr/ and en/)
193+├── fr/
194+│ ├── README.md ← French index
195+│ ├── tutorials/
196+│ ├── how-to/
197+│ ├── reference/
198+│ └── explanation/
199+└── en/
200+ ├── README.md ← English index
201+ ├── tutorials/
202+ ├── how-to/
203+ ├── reference/
204+ └── explanation/
205+```
206+
207+**Bilingual rules:**
208+- ✅ **Cross-links stay inside a single language** (a FR doc never links to an EN doc, and vice versa)
209+- ✅ **Quadrant folder names are identical** in both languages (`tutorials/`, `how-to/`…) to keep a 1-to-1 symmetry
210+- ✅ **File names may be translated** (`premiers-pas.md` ↔ `getting-started.md`) or kept identical — stay consistent
211+- ✅ The **content** is translated, not merely copied: adapt the examples where necessary
212+- ❌ Do not mix two languages in the same file
213+- ❌ Do not duplicate only one of the two indexes — each language has its own
214+
215+> Acceptable alternative for small projects: a per-file language suffix
216+> (`getting-started-en.md` / `premiers-pas-fr.md`) in a flat structure. Reserve this for cases
217+> where creating subfolders would be disproportionate.
218+
219+---
220+
221+## Iterative workflow (the heart of the method)
222+
223+Diátaxis is **not** a grand plan to be executed in one go. It is an organic process: the
224+structure emerges from within, "one cell at a time". The documentation must always be
225+**"complete, not finished"** — useful and well-formed at every stage, while remaining open.
226+
227+**The loop, to be repeated indefinitely:**
228+
229+```
230+1. CHOOSE — take a small piece (a paragraph, a page)
231+2. ASSESS — does it serve one clear need? Is it in the right quadrant?
232+3. DECIDE — identify ONE concrete improvement
233+4. ACT — apply it and publish immediately
234+```
235+
236+**Workflow rules:**
237+- ✅ Work **small**: one improvement at a time, published right away
238+- ✅ "Every step in the right direction deserves to be published immediately"
239+- ❌ Do NOT create large empty sections "to be filled in later"
240+- ❌ Do NOT tear everything down to rebuild it all at once
241+
242+---
243+
244+## Skill application phases
245+
246+### Phase 1 — Gathering information
247+
248+**First question, MANDATORY — the language of the documentation.**
249+Before anything else, ask the user which language(s) the documentation should be produced in,
250+**via `AskUserQuestion`**, with exactly these three options:
251+
252+| Option | Effect on the output |
253+|--------|-----------------------|
254+| **French** | All documentation in French, at the root of the chosen location. |
255+| **English** | All documentation in English, at the root of the chosen location. |
256+| **Both** | Bilingual documentation: one subfolder per language (see "Bilingual architecture"). |
257+
258+This choice determines the **folder architecture** (mono- or bilingual) and the language of
259+**every** document, title and link. Never infer it from the language the user is writing to
260+you in, or from the language of the existing code and comments: ask. If documentation already
261+exists in one language and the user wants "both", add the missing language without rewriting
262+what is already there.
263+
264+> **Exception — invoked by another skill.** When this skill is invoked from another skill that
265+> has already fixed the language, do not ask: use the language it specifies and go straight to
266+> Phase 2. The `methodical-dev` skill always specifies **Both** (English + French), because it
267+> requires bilingual documentation for every change. Asking again in that case is a redundant
268+> prompt for a question that is already settled.
269+
270+Then ask (via AskUserQuestion where relevant):
271+
272+1. **Documentation target**: which project / which scope should be documented?
273+2. **Audience**: beginners discovering the tool? advanced users? contributors?
274+3. **Location**: where should the docs be written? (`docs/`, `./new.docs`, etc.) Must existing content be preserved?
275+4. **Existing material**: is there already documentation to reuse / reorganise?
276+
277+### Phase 2 — Mapping the subject matter
278+
279+Before writing, understand **factually** what is being documented:
280+- Explore the code (entry point, commands, configuration, features)
281+- List the user-facing features and the machinery (CLI, config, API…)
282+- Spot the existing documentation so as not to reinvent it
283+- Produce a list of **topics** to be filed into the four quadrants
284+
285+### Phase 3 — Filing with the Compass
286+
287+For each identified topic, apply the compass and file it into a quadrant. Build a **plan** as a
288+table:
289+
290+```
291+| Topic | Quadrant | File |
292+|-----------------------------|---------------|----------------------------------|
293+| First steps | Tutorial | tutorials/getting-started.md |
294+| Enabling feature X | How-to | how-to/enable-x.md |
295+| Configuration options | Reference | reference/configuration.md |
296+| Why this architecture | Explanation | explanation/architecture.md |
297+```
298+
299+Present this plan to the user and have it approved before writing.
300+
301+### Phase 4 — Writing quadrant by quadrant
302+
303+Write the documents while **strictly** respecting the writing rules of the corresponding
304+quadrant (see the templates below). At the end of each document, apply the quadrant's checklist.
305+
306+### Phase 5 — Index and cross-links
307+
308+- Create / update `README.md` (the index) explaining the Diátaxis structure and orienting the reader
309+- Check that documents **link to each other** in the right direction (tutorial → reference/explanation, etc.)
310+
311+### Phase 6 — Final validation
312+
313+Apply the global checklist (see below) and present the result.
314+
315+---
316+
317+## Templates
318+
319+### Template — Tutorial
320+
321+```markdown
322+# Tutorial: {what the reader will have accomplished}
323+
324+By the end of this tutorial, you will have {concrete, visible result}.
325+No prior knowledge of {X} is required.
326+
327+## Prerequisites
328+- {strictly what is required for EVERY step to work}
329+
330+## Step 1 — {first concrete action}
331+Type:
332+\`\`\`bash
333+{exact command}
334+\`\`\`
335+You should see:
336+\`\`\`
337+{actual expected output}
338+\`\`\`
339+We have just {result of the step, in one sentence}.
340+
341+## Step 2 — {next action}
342+...
343+
344+## What now?
345+You have built {recap}. To go further:
346+- To accomplish specific tasks → see the [how-to guides](../how-to/)
347+- To understand how it works → see the [explanation](../explanation/)
348+```
349+
350+### Template — How-to guide
351+
352+```markdown
353+# How to {accomplish the task}
354+
355+This guide shows how to {task}. It assumes you {skill prerequisite}.
356+
357+## Steps
358+1. {step}
359+2. {step}
360+
361+## Variants
362+- If you use {case A}, do {…} instead
363+- For {case B}, add {…}
364+
365+## See also
366+- Options reference: [reference](../reference/…)
367+- Why it works this way: [explanation](../explanation/…)
368+```
369+
370+### Template — Reference
371+
372+```markdown
373+# Reference: {component}
374+
375+> Neutral, exhaustive description of {component}.
376+
377+## {Sub-component / command / section}
378+
379+| Field / Option | Type | Default | Description |
380+|----------------|------|---------|-------------|
381+| {name} | {type} | {default} | {factual description} |
382+
383+### Example
384+\`\`\`{lang}
385+{minimal usage example}
386+\`\`\`
387+
388+### Errors
389+| Error | Cause |
390+|-------|-------|
391+| {message} | {factual cause} |
392+```
393+
394+### Template — Explanation
395+
396+```markdown
397+# {Topic} — explanation
398+
399+## What is this about?
400+{framing of the topic, overview}
401+
402+## Why it is designed this way
403+{context, constraints, decisions}
404+
405+## Rejected alternatives
406+{what was considered and why it was rejected}
407+
408+## How it relates to the rest
409+{connections with the other parts of the system}
410+```
411+
412+---
413+
414+## Checklists
415+
416+### Per-quadrant checklist
417+
418+**Tutorial:**
419+```
420+□ Is the destination announced up front?
421+□ Does every step produce a visible result?
422+□ Do all commands work without fail (perfect reliability)?
423+□ Is the actual expected output shown?
424+□ No alternatives / options / digressions?
425+□ Is explanation kept minimal (linked, not included)?
426+```
427+
428+**How-to guide:**
429+```
430+□ Does the title state a concrete task ("How to…")?
431+□ Does it start from a real problem, not a feature?
432+□ Does it contain only the necessary steps?
433+□ Does it cover real-world variants?
434+□ Does it avoid teaching the basics (assumes competence)?
435+```
436+
437+**Reference:**
438+```
439+□ Purely descriptive (no instruction, no explanation, no opinion)?
440+□ Exhaustive (every parameter, default, type, error)?
441+□ Standard, predictable format (same structure throughout)?
442+□ Mirrors the product's structure?
443+□ Contains examples without drifting into explanation?
444+```
445+
446+**Explanation:**
447+```
448+□ Does it step back and make connections?
449+□ Does it give the context (history, decisions, constraints)?
450+□ Does it discuss the why and the rejected alternatives?
451+□ Does it stay bounded (not absorbing reference/guide material)?
452+□ Is it free of step-by-step instructions?
453+```
454+
455+### Global checklist (Phase 6)
456+
457+```
458+□ Was the language (FR / EN / both) asked for — or taken from the invoking skill — and respected?
459+□ If bilingual: one subfolder per language, cross-links internal to each language?
460+□ Do all four quadrants exist (at least one document each)?
461+□ Does every document serve exactly ONE clearly identified need?
462+□ Do the folder names make the quadrant obvious?
463+□ Does the index (README) explain the structure and orient the reader?
464+□ Do documents link to each other (no duplication)?
465+□ Are there no empty "to be filled in later" sections?
466+□ Is every document "complete, not finished" (useful right now)?
467+```
468+
469+---
470+
471+## Markdown formatting
472+
473+Write Markdown for readers who soft-wrap in their editor, not for a fixed column
474+width. **Do not hard-wrap prose**: never insert a line break in the middle of a
475+sentence or paragraph.
476+
477+- **One logical line per block.** A paragraph is a single line. A list item is a
478+ single line (its continuation text stays on the same line, not wrapped under
479+ the bullet). A blockquote paragraph is a single `>` line.
480+- **Blank lines separate blocks** — keep exactly one blank line between
481+ paragraphs, list items groups, headings, and code fences.
482+- **Leave code, tables, and headings alone.** Fenced code blocks keep their
483+ internal newlines verbatim; table rows stay one row per line; a heading is its
484+ own line.
485+
486+This keeps diffs meaningful (a reworded sentence changes one line, not a whole
487+reflowed block) and matches how the rendered output looks anyway. If you are
488+editing a file that was previously hard-wrapped, unwrap the blocks you touch.
489+
490+## Strict Rules
491+
492+**You must NEVER:**
493+- ❌ Mix two quadrants in the same document (recipe + explanation, reference + tutorial…)
494+- ❌ Put detailed explanation into a tutorial or a how-to guide
495+- ❌ Put step-by-step instructions into a reference or an explanation
496+- ❌ Create empty folders / sections in anticipation
497+- ❌ Brutally reorganise all the existing documentation at once (work in increments)
498+- ❌ Duplicate content across quadrants instead of linking
499+- ❌ Hard-wrap prose — no line break in the middle of a sentence or paragraph (see "Markdown formatting")
500+
501+**You must ALWAYS:**
502+- ✅ File every piece of content with the compass (action/cognition × acquisition/application)
503+- ✅ Respect the writing rules specific to the quadrant
504+- ✅ Guarantee absolute reliability in tutorials (every command mentally tested)
505+- ✅ Keep the reference neutral, exhaustive and structured
506+- ✅ Link documents to each other rather than merging them
507+- ✅ Work in small publishable increments ("complete, not finished")
508+- ✅ Write each paragraph, list item, and blockquote as a single unwrapped line
509+
510+---
511+
512+## Notes
513+
514+Diátaxis imposes no technology (Markdown, Sphinx, MkDocs, Docusaurus… it does not matter). What
515+counts is the **clean separation of the four needs**. Documentation that respects this
516+separation is easier to write, to maintain and — above all — to use: every reader immediately
517+knows where to go depending on whether they want to **learn**, **do**, **look up** or
518+**understand**.
519+
520+Canonical reference: https://diataxis.fr
new file mode 100644
@@ -0,0 +1,520 @@
1+---
2+name: diataxis-doc
3+description: Document a project using the Diátaxis methodology (https://diataxis.fr). Organises documentation into four distinct quadrants — Tutorials (learning), How-to guides (solving a task), Reference (looking information up), Explanation (understanding) — along the two axes action/cognition and acquisition/application. Use when creating or restructuring a project's documentation in a rigorous, readable, user-needs-driven way.
4+---
5+# Diátaxis Documentation Skill
6+
7+## Description
8+
9+Guides the creation and restructuring of a project's documentation according to the
10+**Diátaxis** methodology (Daniele Procida — https://diataxis.fr). Diátaxis is not a rigid
11+template: it is a **compass** that guarantees every documentation page serves **exactly one
12+clearly identified user need**, and that all four fundamental needs are covered.
13+
14+The strength of the method: it solves the three problems of any documentation —
15+**content** (what to write), **style** (how to write it) and **architecture**
16+(how to organise it) — by reducing them to two simple questions.
17+
18+## When to Use
19+
20+- When a project has no documentation, or a "catch-all" documentation
21+- When existing docs mix tutorials, recipes, reference and explanation in the same pages
22+- When users "can't find" the information despite voluminous docs
23+- When you want maintainable documentation that grows in small increments
24+- After adding a feature, to document it in the right place without reorganising everything
25+
26+---
27+
28+## Core Principle: the two axes
29+
30+The whole method rests on **two orthogonal axes**. They do not merely cover the territory of
31+documentation — they **define** it. This is why there are **necessarily four quadrants**,
32+neither three nor five.
33+
34+```
35+ ACTION (practical know-how)
36+ ▲
37+ │
38+ TUTORIALS │ HOW-TO GUIDES
39+ (learning by doing) │ (achieving a goal)
40+ │
41+ ACQUISITION ────────────────────┼──────────────────── APPLICATION
42+ (studying a skill) │ (applying a skill)
43+ │
44+ EXPLANATION │ REFERENCE
45+ (understanding, thinking) │ (looking information up)
46+ │
47+ ▼
48+ COGNITION (theoretical knowledge)
49+```
50+
51+| Quadrant | Action/cognition axis | Acquisition/application axis | The user's question |
52+|----------|------------------------|-------------------------------|----------------------|
53+| **Tutorial** | Action | Acquisition | "Teach me to get started" |
54+| **How-to guide** | Action | Application | "How do I do X?" |
55+| **Reference** | Cognition | Application | "What are the exact details of X?" |
56+| **Explanation** | Cognition | Acquisition | "Can you explain why / the context?" |
57+
58+---
59+
60+## The four quadrants in detail
61+
62+### 1. TUTORIALS — learning-oriented
63+
64+A tutorial is a **guided lesson**, an experience under a teacher's direction. Its goal is NOT
65+to accomplish a real task, but to make the beginner **acquire a skill**. The teacher is
66+responsible for the student's success.
67+
68+**Writing rules (mandatory):**
69+- ✅ Announce the **destination** up front ("by the end, you will have built…"), not what will be "learned"
70+- ✅ Produce a **visible result at every step**, however small
71+- ✅ Guarantee **perfect reliability**: every command must work for everyone, every time
72+- ✅ Show the **actual expected output** ("you should see: …")
73+- ✅ Use **"we"** ("we will now…") and the direct imperative ("Type this")
74+- ✅ Flag common mistakes at the point where they can occur
75+- ❌ **Minimise explanation to the extreme** — a tutorial is NOT the place for explanation. Link to it instead.
76+- ❌ **No alternatives, no options, no digressions** — a single path to success
77+- ❌ Do not explain the "why" in detail (it breaks the learning flow)
78+
79+### 2. HOW-TO GUIDES — task-oriented
80+
81+A how-to guide is a **recipe** for reaching a specific goal. It addresses an **already
82+competent** user who knows what they want to do. "How do I deploy?", "How do I enable
83+caching?".
84+
85+> ⚠️ Key distinction from a tutorial: *"Someone who expects a recipe and receives a cooking
86+> lesson will be disappointed and annoyed."* Tutorial = lesson; guide = recipe.
87+
88+**Writing rules:**
89+- ✅ Title = **the task**, stated clearly: "How to {do X}"
90+- ✅ Start from a **real problem**, not from a feature of the tool
91+- ✅ Contain **only the steps needed** for the task
92+- ✅ Stay **adaptable to the real world** ("if you use Y, do this instead") — a guide covers variants
93+- ✅ Assume the user **already knows** what they want to accomplish
94+- ❌ Do not teach the basic concepts (that is the tutorial's job)
95+- ❌ Do not drown the recipe in explanation (link to the explanation)
96+
97+### 3. REFERENCE — information-oriented
98+
99+Reference is a **neutral, exhaustive technical description** of the machinery: the commands,
100+options, parameters, structures. It is a **map of the territory**, austere and reliable, that
101+you **consult** (you do not read it end to end).
102+
103+**Writing rules:**
104+- ✅ **Describe**, do nothing else: no instruction, no explanation, no opinion
105+- ✅ **Accuracy and completeness**: every parameter, default value, type, error
106+- ✅ **Standard, predictable structure**: the same format for every entry (tables, lists)
107+- ✅ **Mirror the product's structure**: the reference's organisation reflects that of the code
108+- ✅ Include usage **examples**, without drifting into explanation
109+- ❌ Do not teach, do not tell the "why" — link to tutorials and explanations
110+
111+> Mental model: a **nutrition label**. Factual, standardised, rigorous, with no recipe and no
112+> marketing pitch.
113+
114+### 4. EXPLANATION — understanding-oriented
115+
116+Explanation **steps back**. It answers "Can you tell me about…?". It connects concepts, gives
117+context, history, design decisions, rejected alternatives. It is the documentation you read
118+"in the bath", away from active work.
119+
120+**Writing rules:**
121+- ✅ **Make connections** between topics, take a wider view
122+- ✅ Give the **context**: history, decisions, constraints, rejected alternatives
123+- ✅ Discuss the **why**, the trade-offs, the possibilities
124+- ✅ **Allow opinion** and acknowledge multiple points of view
125+- ✅ **Stay bounded** to one topic, so it does not absorb content that belongs elsewhere
126+- ❌ Do not give step-by-step instructions (that is the guides'/tutorials' job)
127+- ❌ Do not become a reference (no exhaustive parameter lists)
128+
129+---
130+
131+## The Compass — choosing the right quadrant
132+
133+When you do not know where a piece of content belongs (or when the writing "gets stuck"),
134+answer **two questions**:
135+
136+```
137+1. Does the content inform ACTION or COGNITION?
138+ (practical steps ──vs── theoretical knowledge)
139+
140+2. Does it serve the ACQUISITION or the APPLICATION of a skill?
141+ (the user is learning ──vs── the user is doing)
142+```
143+
144+| If the content says… | …informs | …and serves | → then it is a |
145+|----------------------|----------|-------------|-----------------|
146+| "teach me by doing" | action | acquisition | **Tutorial** |
147+| "help me do this" | action | application | **How-to guide** |
148+| "give me the exact information" | cognition | application | **Reference** |
149+| "help me understand" | cognition | acquisition | **Explanation** |
150+
151+**Symptom of a misfiling:** if a page forces you to mix two styles (e.g. explaining the *why*
152+in the middle of a recipe), it contains two distinct pieces of content → **split it** and link
153+the parts to each other.
154+
155+The compass applies at every scale: a whole document, a section, or even a single sentence.
156+
157+---
158+
159+## Recommended folder architecture
160+
161+Four folders, one per quadrant, plus an index:
162+
163+```
164+docs/ (or ./new.docs, ./documentation, etc.)
165+├── README.md ← index: explains the structure and orients the reader
166+├── tutorials/ ← guided learning
167+│ └── getting-started.md
168+├── how-to/ ← task-oriented recipes
169+│ ├── do-x.md
170+│ └── do-y.md
171+├── reference/ ← exhaustive technical description
172+│ ├── cli.md
173+│ └── configuration.md
174+└── explanation/ ← understanding, context, decisions
175+ ├── architecture.md
176+ └── design-choices.md
177+```
178+
179+**Architecture rules:**
180+- ✅ **Folder names** make the quadrant obvious (tutorials / how-to / reference / explanation)
181+- ✅ The **index** (`README.md`) briefly explains the four types and links to each
182+- ✅ Documents **link to each other**: a tutorial links to the reference and the explanation rather than including them
183+- ✅ Folders may be named in the documentation's own language (`tutoriels/`, `guides-pratiques/`, …) — stay consistent
184+
185+### Bilingual architecture (the "Both" option)
186+
187+When the user wants both languages, use **one subfolder per language**, each containing its own
188+complete four-quadrant structure. A root index acts as the language selector:
189+
190+```
191+docs/
192+├── README.md ← language selector (links to fr/ and en/)
193+├── fr/
194+│ ├── README.md ← French index
195+│ ├── tutorials/
196+│ ├── how-to/
197+│ ├── reference/
198+│ └── explanation/
199+└── en/
200+ ├── README.md ← English index
201+ ├── tutorials/
202+ ├── how-to/
203+ ├── reference/
204+ └── explanation/
205+```
206+
207+**Bilingual rules:**
208+- ✅ **Cross-links stay inside a single language** (a FR doc never links to an EN doc, and vice versa)
209+- ✅ **Quadrant folder names are identical** in both languages (`tutorials/`, `how-to/`…) to keep a 1-to-1 symmetry
210+- ✅ **File names may be translated** (`premiers-pas.md` ↔ `getting-started.md`) or kept identical — stay consistent
211+- ✅ The **content** is translated, not merely copied: adapt the examples where necessary
212+- ❌ Do not mix two languages in the same file
213+- ❌ Do not duplicate only one of the two indexes — each language has its own
214+
215+> Acceptable alternative for small projects: a per-file language suffix
216+> (`getting-started-en.md` / `premiers-pas-fr.md`) in a flat structure. Reserve this for cases
217+> where creating subfolders would be disproportionate.
218+
219+---
220+
221+## Iterative workflow (the heart of the method)
222+
223+Diátaxis is **not** a grand plan to be executed in one go. It is an organic process: the
224+structure emerges from within, "one cell at a time". The documentation must always be
225+**"complete, not finished"** — useful and well-formed at every stage, while remaining open.
226+
227+**The loop, to be repeated indefinitely:**
228+
229+```
230+1. CHOOSE — take a small piece (a paragraph, a page)
231+2. ASSESS — does it serve one clear need? Is it in the right quadrant?
232+3. DECIDE — identify ONE concrete improvement
233+4. ACT — apply it and publish immediately
234+```
235+
236+**Workflow rules:**
237+- ✅ Work **small**: one improvement at a time, published right away
238+- ✅ "Every step in the right direction deserves to be published immediately"
239+- ❌ Do NOT create large empty sections "to be filled in later"
240+- ❌ Do NOT tear everything down to rebuild it all at once
241+
242+---
243+
244+## Skill application phases
245+
246+### Phase 1 — Gathering information
247+
248+**First question, MANDATORY — the language of the documentation.**
249+Before anything else, ask the user which language(s) the documentation should be produced in,
250+**via `AskUserQuestion`**, with exactly these three options:
251+
252+| Option | Effect on the output |
253+|--------|-----------------------|
254+| **French** | All documentation in French, at the root of the chosen location. |
255+| **English** | All documentation in English, at the root of the chosen location. |
256+| **Both** | Bilingual documentation: one subfolder per language (see "Bilingual architecture"). |
257+
258+This choice determines the **folder architecture** (mono- or bilingual) and the language of
259+**every** document, title and link. Never infer it from the language the user is writing to
260+you in, or from the language of the existing code and comments: ask. If documentation already
261+exists in one language and the user wants "both", add the missing language without rewriting
262+what is already there.
263+
264+> **Exception — invoked by another skill.** When this skill is invoked from another skill that
265+> has already fixed the language, do not ask: use the language it specifies and go straight to
266+> Phase 2. The `methodical-dev` skill always specifies **Both** (English + French), because it
267+> requires bilingual documentation for every change. Asking again in that case is a redundant
268+> prompt for a question that is already settled.
269+
270+Then ask (via AskUserQuestion where relevant):
271+
272+1. **Documentation target**: which project / which scope should be documented?
273+2. **Audience**: beginners discovering the tool? advanced users? contributors?
274+3. **Location**: where should the docs be written? (`docs/`, `./new.docs`, etc.) Must existing content be preserved?
275+4. **Existing material**: is there already documentation to reuse / reorganise?
276+
277+### Phase 2 — Mapping the subject matter
278+
279+Before writing, understand **factually** what is being documented:
280+- Explore the code (entry point, commands, configuration, features)
281+- List the user-facing features and the machinery (CLI, config, API…)
282+- Spot the existing documentation so as not to reinvent it
283+- Produce a list of **topics** to be filed into the four quadrants
284+
285+### Phase 3 — Filing with the Compass
286+
287+For each identified topic, apply the compass and file it into a quadrant. Build a **plan** as a
288+table:
289+
290+```
291+| Topic | Quadrant | File |
292+|-----------------------------|---------------|----------------------------------|
293+| First steps | Tutorial | tutorials/getting-started.md |
294+| Enabling feature X | How-to | how-to/enable-x.md |
295+| Configuration options | Reference | reference/configuration.md |
296+| Why this architecture | Explanation | explanation/architecture.md |
297+```
298+
299+Present this plan to the user and have it approved before writing.
300+
301+### Phase 4 — Writing quadrant by quadrant
302+
303+Write the documents while **strictly** respecting the writing rules of the corresponding
304+quadrant (see the templates below). At the end of each document, apply the quadrant's checklist.
305+
306+### Phase 5 — Index and cross-links
307+
308+- Create / update `README.md` (the index) explaining the Diátaxis structure and orienting the reader
309+- Check that documents **link to each other** in the right direction (tutorial → reference/explanation, etc.)
310+
311+### Phase 6 — Final validation
312+
313+Apply the global checklist (see below) and present the result.
314+
315+---
316+
317+## Templates
318+
319+### Template — Tutorial
320+
321+```markdown
322+# Tutorial: {what the reader will have accomplished}
323+
324+By the end of this tutorial, you will have {concrete, visible result}.
325+No prior knowledge of {X} is required.
326+
327+## Prerequisites
328+- {strictly what is required for EVERY step to work}
329+
330+## Step 1 — {first concrete action}
331+Type:
332+\`\`\`bash
333+{exact command}
334+\`\`\`
335+You should see:
336+\`\`\`
337+{actual expected output}
338+\`\`\`
339+We have just {result of the step, in one sentence}.
340+
341+## Step 2 — {next action}
342+...
343+
344+## What now?
345+You have built {recap}. To go further:
346+- To accomplish specific tasks → see the [how-to guides](../how-to/)
347+- To understand how it works → see the [explanation](../explanation/)
348+```
349+
350+### Template — How-to guide
351+
352+```markdown
353+# How to {accomplish the task}
354+
355+This guide shows how to {task}. It assumes you {skill prerequisite}.
356+
357+## Steps
358+1. {step}
359+2. {step}
360+
361+## Variants
362+- If you use {case A}, do {…} instead
363+- For {case B}, add {…}
364+
365+## See also
366+- Options reference: [reference](../reference/…)
367+- Why it works this way: [explanation](../explanation/…)
368+```
369+
370+### Template — Reference
371+
372+```markdown
373+# Reference: {component}
374+
375+> Neutral, exhaustive description of {component}.
376+
377+## {Sub-component / command / section}
378+
379+| Field / Option | Type | Default | Description |
380+|----------------|------|---------|-------------|
381+| {name} | {type} | {default} | {factual description} |
382+
383+### Example
384+\`\`\`{lang}
385+{minimal usage example}
386+\`\`\`
387+
388+### Errors
389+| Error | Cause |
390+|-------|-------|
391+| {message} | {factual cause} |
392+```
393+
394+### Template — Explanation
395+
396+```markdown
397+# {Topic} — explanation
398+
399+## What is this about?
400+{framing of the topic, overview}
401+
402+## Why it is designed this way
403+{context, constraints, decisions}
404+
405+## Rejected alternatives
406+{what was considered and why it was rejected}
407+
408+## How it relates to the rest
409+{connections with the other parts of the system}
410+```
411+
412+---
413+
414+## Checklists
415+
416+### Per-quadrant checklist
417+
418+**Tutorial:**
419+```
420+□ Is the destination announced up front?
421+□ Does every step produce a visible result?
422+□ Do all commands work without fail (perfect reliability)?
423+□ Is the actual expected output shown?
424+□ No alternatives / options / digressions?
425+□ Is explanation kept minimal (linked, not included)?
426+```
427+
428+**How-to guide:**
429+```
430+□ Does the title state a concrete task ("How to…")?
431+□ Does it start from a real problem, not a feature?
432+□ Does it contain only the necessary steps?
433+□ Does it cover real-world variants?
434+□ Does it avoid teaching the basics (assumes competence)?
435+```
436+
437+**Reference:**
438+```
439+□ Purely descriptive (no instruction, no explanation, no opinion)?
440+□ Exhaustive (every parameter, default, type, error)?
441+□ Standard, predictable format (same structure throughout)?
442+□ Mirrors the product's structure?
443+□ Contains examples without drifting into explanation?
444+```
445+
446+**Explanation:**
447+```
448+□ Does it step back and make connections?
449+□ Does it give the context (history, decisions, constraints)?
450+□ Does it discuss the why and the rejected alternatives?
451+□ Does it stay bounded (not absorbing reference/guide material)?
452+□ Is it free of step-by-step instructions?
453+```
454+
455+### Global checklist (Phase 6)
456+
457+```
458+□ Was the language (FR / EN / both) asked for — or taken from the invoking skill — and respected?
459+□ If bilingual: one subfolder per language, cross-links internal to each language?
460+□ Do all four quadrants exist (at least one document each)?
461+□ Does every document serve exactly ONE clearly identified need?
462+□ Do the folder names make the quadrant obvious?
463+□ Does the index (README) explain the structure and orient the reader?
464+□ Do documents link to each other (no duplication)?
465+□ Are there no empty "to be filled in later" sections?
466+□ Is every document "complete, not finished" (useful right now)?
467+```
468+
469+---
470+
471+## Markdown formatting
472+
473+Write Markdown for readers who soft-wrap in their editor, not for a fixed column
474+width. **Do not hard-wrap prose**: never insert a line break in the middle of a
475+sentence or paragraph.
476+
477+- **One logical line per block.** A paragraph is a single line. A list item is a
478+ single line (its continuation text stays on the same line, not wrapped under
479+ the bullet). A blockquote paragraph is a single `>` line.
480+- **Blank lines separate blocks** — keep exactly one blank line between
481+ paragraphs, list items groups, headings, and code fences.
482+- **Leave code, tables, and headings alone.** Fenced code blocks keep their
483+ internal newlines verbatim; table rows stay one row per line; a heading is its
484+ own line.
485+
486+This keeps diffs meaningful (a reworded sentence changes one line, not a whole
487+reflowed block) and matches how the rendered output looks anyway. If you are
488+editing a file that was previously hard-wrapped, unwrap the blocks you touch.
489+
490+## Strict Rules
491+
492+**You must NEVER:**
493+- ❌ Mix two quadrants in the same document (recipe + explanation, reference + tutorial…)
494+- ❌ Put detailed explanation into a tutorial or a how-to guide
495+- ❌ Put step-by-step instructions into a reference or an explanation
496+- ❌ Create empty folders / sections in anticipation
497+- ❌ Brutally reorganise all the existing documentation at once (work in increments)
498+- ❌ Duplicate content across quadrants instead of linking
499+- ❌ Hard-wrap prose — no line break in the middle of a sentence or paragraph (see "Markdown formatting")
500+
501+**You must ALWAYS:**
502+- ✅ File every piece of content with the compass (action/cognition × acquisition/application)
503+- ✅ Respect the writing rules specific to the quadrant
504+- ✅ Guarantee absolute reliability in tutorials (every command mentally tested)
505+- ✅ Keep the reference neutral, exhaustive and structured
506+- ✅ Link documents to each other rather than merging them
507+- ✅ Work in small publishable increments ("complete, not finished")
508+- ✅ Write each paragraph, list item, and blockquote as a single unwrapped line
509+
510+---
511+
512+## Notes
513+
514+Diátaxis imposes no technology (Markdown, Sphinx, MkDocs, Docusaurus… it does not matter). What
515+counts is the **clean separation of the four needs**. Documentation that respects this
516+separation is easier to write, to maintain and — above all — to use: every reader immediately
517+knows where to go depending on whether they want to **learn**, **do**, **look up** or
518+**understand**.
519+
520+Canonical reference: https://diataxis.fr
added .vibe/skills/methodical-dev/SKILL.md +603 -0
new file mode 100644
@@ -0,0 +1,603 @@
1+---
2+name: methodical-dev
3+description: Guide the user through a structured development methodology, following best practices for working with Mistral Vibe in a controlled and effective way. Every change is delivered complete: readable and documented code, unit tests with a documented way to run them, bilingual (EN + FR) Diátaxis documentation, a passing quality gate, and an updated .memory/ project record. Use when starting a feature, refactoring, or whenever you want a controlled, approval-gated development process.
4+---
5+# Methodical Development Skill
6+
7+## Description
8+Guide the user through a structured development methodology, following best practices for
9+working with Mistral Vibe in a controlled and effective way.
10+
11+The core promise: **no change is "done" until it is readable, tested, documented in both
12+English and French, quality-gated, and recorded in the project's memory.** The phases below
13+enforce that; none of them is optional.
14+
15+## When to Use
16+- When starting a new feature
17+- When refactoring or changing existing behaviour
18+- When you want to follow a structured process
19+- To avoid the common pitfalls of AI-assisted development
20+
21+## Companion skills
22+
23+This skill orchestrates two others. Both ship alongside it and must be **invoked as skills**,
24+not reimplemented:
25+
26+| Skill | When this skill invokes it | Phase |
27+| --- | --- | --- |
28+| `quality` | After implementation, to measure quality and refactor until the gate passes | Phase 5 |
29+| `diataxis-doc` | After the quality gate, to write or update the documentation | Phase 6 |
30+
31+## Instructions
32+
33+You are a skill that guides the user through a rigorous development methodology. You must
34+follow this process step by step.
35+
36+---
37+
38+## Phase 0: Read the project memory
39+
40+**Before asking the user anything**, read `.memory/` at the repository root:
41+
42+```bash
43+ls -la .memory/ 2>/dev/null && cat .memory/summary.md 2>/dev/null
44+```
45+
46+- If `.memory/` exists, read `summary.md`, the tail of `history.md`, and the most recent file
47+ in `handoffs/`. It tells you the project's current state, past decisions, and any work left
48+ in flight — **do not ask the user for information that is already recorded there.**
49+- If `.memory/` does not exist, you will create it in Phase 7. Do not create it yet.
50+
51+If a handoff records unfinished work, surface it now and ask whether to resume it or start
52+something new.
53+
54+---
55+
56+## Phase 1: Gathering Information
57+
58+Ask the user these questions using AskUserQuestion — skipping any the memory already answers:
59+
60+1. **Goal of the feature**
61+ - What feature do you want to develop?
62+ - What is the exact scope of this feature?
63+
64+2. **Technical Constraints**
65+ - Which frameworks/libraries must you use?
66+ - Are there any version constraints?
67+ - Are there architectural patterns to follow?
68+
69+3. **Documentation and Examples**
70+ - Do you have documentation to reference?
71+ - Do you have similar existing code that could serve as an example?
72+
73+4. **Style and Conventions**
74+ - Are there specific naming conventions?
75+ - Is there a particular code style to follow?
76+
77+---
78+
79+## Phase 2: Git Check
80+
81+Check the state of the repository:
82+
83+```bash
84+# Check that we are in a git repo
85+git status
86+
87+# If there is no repo, offer to initialise one
88+git init
89+```
90+
91+If the user is not on a dedicated branch, **strongly recommend** creating a feature branch.
92+
93+**IMPORTANT**: Do not create the branch automatically. Ask the user:
94+- What branch name would they like?
95+- Do they want you to create the branch, or would they rather do it themselves?
96+
97+---
98+
99+## Phase 3: Detailed Planning
100+
101+1. **Analyse the existing code** (if needed)
102+ - Use Glob and Grep to understand the structure
103+ - Identify the files to modify
104+ - Identify the existing patterns to follow
105+
106+2. **Identify the project's test and build commands.** Look for a `Makefile`, `Taskfile.yml`,
107+ `package.json` scripts, `pyproject.toml`, or the CI workflow. You need these for Phase 4
108+ and Phase 5, and you must reuse the project's existing convention rather than inventing a
109+ parallel one.
110+
111+3. **Create a detailed plan** using TodoWrite
112+ - Break the feature into logical steps (5–8 steps maximum)
113+ - Each step must be atomic and testable
114+ - Order the steps by dependency
115+ - **Every implementation step carries its own unit tests** — do not plan a single
116+ "write the tests" step at the end
117+ - Include the mandatory closing phases as plan items: quality gate, bilingual
118+ documentation, memory update
119+
120+4. **Present the plan** to the user
121+ - Explain each step
122+ - Ask for approval before continuing
123+ - Allow adjustments
124+
125+---
126+
127+## Phase 4: Guided Implementation
128+
129+For each step of the plan:
130+
131+1. **Before starting the step**
132+ - Mark the step as `in_progress` with TodoWrite
133+ - Explain what you are about to do
134+ - Ask for confirmation if the step is complex
135+
136+2. **During the step**
137+ - Implement only what is planned for this step
138+ - Write the code to the **Code Standards** below — readable, maintainable, documented
139+ with usage examples
140+ - Write or update the **unit tests** for this step's behaviour (see Testing Requirements)
141+ - Do **NOT** take shortcuts
142+ - Do **NOT** delete existing code without asking
143+ - Do **NOT** change the architecture without agreement
144+ - Explain the technical choices as you go
145+
146+3. **After the step**
147+ - Run the tests and show the result — a step whose tests do not pass is not finished
148+ - Mark the step as `completed` with TodoWrite
149+ - Summarise what was done
150+ - List the files created/modified
151+ - **STOP and wait for the user's approval**
152+
153+4. **Mandatory checkpoint**
154+ - Ask the user to:
155+ - Review the code produced
156+ - Test the behaviour
157+ - Confirm it matches their request
158+ - Offer to:
159+ - Continue to the next step
160+ - Change something in the current step
161+ - Adjust the remaining plan
162+
163+---
164+
165+## Phase 5: Quality Gate (mandatory)
166+
167+**Invoke the `quality` skill.** Do not measure quality by eye and do not skip this phase,
168+even for a one-line change.
169+
170+The skill configures qlty if needed, measures lint issues / code smells / complexity, writes
171+a report under `.quality/`, records the run in its history so progression is visible, and
172+tells you whether the gate passed.
173+
174+- **Gate passed** → report the numbers and move to Phase 6.
175+- **Gate failed** → refactor as the skill directs, re-run the tests from Phase 4, and
176+ re-measure. Repeat until the gate passes or the skill's own stopping conditions are hit
177+ (five iterations, or two runs with no improvement).
178+- **Still failing at the stopping condition** → do not silently continue. Report what
179+ improved, what remains, and why, then ask the user how to proceed.
180+
181+Never satisfy the gate by weakening it. Editing `.qlty/qlty.toml` exclusions, lowering
182+`.quality/gate.json` thresholds, or adding blanket lint suppressions is forbidden here for the
183+same reason it is forbidden inside the `quality` skill: it makes the measurement lie.
184+
185+For a long multi-step feature, running the quality skill after a large step — not only at the
186+end — catches drift earlier and is cheaper than one big cleanup.
187+
188+---
189+
190+## Phase 6: Documentation (mandatory, bilingual)
191+
192+**Invoke the `diataxis-doc` skill** to write or update the documentation for what you just
193+built. Every creation and every change gets documented — new behaviour, changed behaviour,
194+and removed behaviour alike.
195+
196+> **Do not ask the user which language to document in.** `diataxis-doc` normally opens by
197+> asking; when invoked from this skill the answer is already fixed: **Both** — English *and*
198+> French. Tell the skill this so it goes straight to the bilingual architecture.
199+
200+This produces one subfolder per language, each with its own complete four-quadrant structure:
201+
202+```
203+docs/
204+├── README.md ← language selector
205+├── en/
206+│ ├── README.md
207+│ ├── tutorials/ ← "teach me to get started"
208+│ ├── how-to/ ← "how do I do X?"
209+│ ├── reference/ ← "what are the exact details of X?"
210+│ └── explanation/ ← "why is it built this way?"
211+└── fr/
212+ └── … same structure
213+```
214+
215+Rules that matter here:
216+
217+- **Both languages stay in sync.** A change documented in English but not French is an
218+ incomplete change. Translate the content — do not copy the English text into `fr/`.
219+- **Cross-links stay inside one language.** A French page never links to an English page.
220+- **File the content with the Diátaxis compass**, do not dump everything into one page. New
221+ feature → usually a `how-to/` page plus a `reference/` entry; a design decision →
222+ `explanation/`.
223+- **The way to run the tests is documented**, in both languages (see Testing Requirements).
224+- **Every touched package's `README.md` is updated.** This is separate from `docs/` and applies to **all** packages, in whatever language that README already uses. Any package whose public surface you changed — a new, renamed, or removed exported function, type, method, flag, or CLI subcommand — gets its `README.md` brought back in sync **in the same change**. A README that lists part of a package's API but omits what you just added is a defect, not merely "a bit behind". If the repository keeps no per-package READMEs, this rule is inert — do not create them just to satisfy it.
225+- **A package-dependency diagram is kept in draw.io format.** It shows every package/module, the dependency arrows between them (and on third-party runtimes), and a one-line "what it is for" on each. Keep it at `docs/diagrams/packages.drawio` (or the project's existing diagrams location). **Create it when it does not exist, and update it whenever you add, remove, or re-wire a package** so it never drifts from the real import graph. Keep it as a single language-neutral file (labels in the docs' primary language), referenced from the architecture explanation page in each language. A `.drawio` file is plain XML (an `mxGraphModel`) that diagrams.net and the VS Code Draw.io extension open directly, so you can author and edit it as text; after writing it, sanity-check that the XML parses.
226+
227+---
228+
229+## Phase 7: Update the project memory (mandatory)
230+
231+Create or update `.memory/` at the repository root. This is the project's durable record: it
232+is what lets you — or a different agent, in a fresh sandbox — pick the work up later without
233+re-deriving everything.
234+
235+```
236+.memory/
237+├── README.md ← what this folder is and how it is maintained
238+├── summary.md ← living snapshot of the project's current state (edited in place)
239+├── history.md ← append-only chronological log (never rewritten)
240+└── handoffs/
241+ └── YYYY-MM-DD-<slug>.md ← one per session or feature
242+```
243+
244+`.memory/` is **committed to the repository**, not gitignored — that is the whole point.
245+
246+### `summary.md` — edited in place, never regenerated
247+
248+The current state of the project, kept short enough to stay read-worthy:
249+
250+- What the project is and does
251+- Architecture: main components and how they fit together
252+- Key technical decisions currently in force, and why
253+- How to build, test, and run it (the actual commands)
254+- Known limitations and open questions
255+
256+**Change only what this session establishes or invalidates, and leave the rest alone.** You have
257+seen a slice of the project; this file holds what every previous session established. Regenerating
258+it wholesale from your slice silently destroys accurate content you never looked at — the one way
259+this file stops being trustworthy. Write only what you verified; anything you could not check goes
260+under an explicit `## Not yet established` heading instead of a plausible guess, because the next
261+session will trust whatever is written here. If the project's state did not change, leave the file
262+untouched.
263+
264+### `history.md` — append only
265+
266+One dated entry per completed feature or session. **Never rewrite or delete past entries** —
267+a history you edit is not a history.
268+
269+```markdown
270+## 2026-07-25 — <what was done>
271+
272+- **Goal**: <what the user asked for>
273+- **Changes**: <files/components created or modified>
274+- **Decisions**: <choices made and why; alternatives rejected>
275+- **Tests**: <what was added, how to run it>
276+- **Quality**: <gate PASS/FAIL and the key numbers>
277+- **Docs**: <pages created/updated, in both languages>
278+```
279+
280+### `handoffs/YYYY-MM-DD-<slug>.md` — written at the end of the session
281+
282+The document a fresh agent reads to resume:
283+
284+```markdown
285+# Handoff — <date> — <topic>
286+
287+## State
288+<what works right now; what was just finished>
289+
290+## In flight
291+<work started but not finished, and exactly where it stopped>
292+
293+## Next steps
294+1. <the concrete next action>
295+2. …
296+
297+## Open questions / blockers
298+<decisions awaiting the user; anything that blocked progress>
299+
300+## Watch out for
301+<traps discovered along the way — failing commands, fragile assumptions>
302+```
303+
304+### Memory vs. documentation
305+
306+Keep the boundary clean, so content is not duplicated:
307+
308+| | Audience | Content |
309+| --- | --- | --- |
310+| `docs/` (Diátaxis) | **Users** of the project | How to use it, what the API is, why it is designed that way |
311+| `.memory/` | **Whoever continues the work** | Project history, session state, handoffs, decisions in progress |
312+
313+If a piece of information helps someone *use* the project, it belongs in `docs/`. If it helps
314+someone *continue building* it, it belongs in `.memory/`.
315+
316+---
317+
318+## Phase 8: Final Validation
319+
320+Once all the steps and phases are complete:
321+
322+1. **Full summary**
323+ - List of all files created
324+ - List of all files modified
325+ - Summary of the features implemented
326+
327+2. **Quality checklist**
328+ - [ ] Does the feature match the request exactly?
329+ - [ ] No unrequested deletions?
330+ - [ ] Is the code readable, maintainable, and documented with usage examples?
331+ - [ ] Are unit tests present for every change, and do they pass?
332+ - [ ] Is there a documented, single command to run the tests?
333+ - [ ] Did the `quality` skill run, and did the gate pass?
334+ - [ ] Is the documentation updated in **both** English and French?
335+ - [ ] Is every touched package's `README.md` updated to match its current public API?
336+ - [ ] Is the package-dependency draw.io diagram present and consistent with the current import graph?
337+ - [ ] Are the conventions respected?
338+ - [ ] Is `.memory/` updated — `summary.md`, a new `history.md` entry, and a handoff?
339+
340+3. **Commit proposal**
341+ - Propose a structured commit message
342+ - List the files to add to the commit — including `docs/` and `.memory/`
343+ - Do **NOT** commit automatically
344+ - Let the user do it, or use the /commit skill
345+
346+---
347+
348+## Code Standards
349+
350+Generated code must be readable and maintainable by a **human**, not merely correct. Someone
351+unfamiliar with it should understand it without asking you.
352+
353+**Readability**
354+- Names state intent: `retryAfterSeconds`, not `d` or `tmp2`. No abbreviations that are not
355+ domain-standard.
356+- One function does one thing, and is short enough to read without scrolling.
357+- Prefer the explicit over the clever. A dense one-liner that needs a comment to be understood
358+ should be several plain lines instead.
359+- Early returns over deep nesting.
360+- **Match the surrounding code.** Its existing style wins over your preferences.
361+
362+**Maintainability**
363+- No duplicated logic — extract it the second time it appears.
364+- Errors are handled where they can be handled meaningfully, never silently swallowed.
365+- No dead code, no commented-out code, no `TODO` without a concrete follow-up noted in
366+ `.memory/handoffs/`.
367+- Keep the public surface small: expose what callers need, no more.
368+
369+**Documentation in the code**
370+- Every public/exported function, type, class, and module gets a doc comment saying what it
371+ does, what it expects, and what it returns or raises.
372+- **Each public API's doc comment includes a short usage example** — a few lines a reader can
373+ copy. Use the language's idiom for this: Go `Example` functions (which are also tests),
374+ Python docstring examples, JSDoc `@example`, Rust doc-tests.
375+- Comments explain **why**, not what. If a comment restates the code, delete it and improve
376+ the name instead.
377+- Document non-obvious constraints and invariants at the point they apply.
378+
379+**Markdown and prose files**
380+- Applies to every Markdown file you write — `docs/`, `.memory/`, `README`s.
381+- **Do not hard-wrap prose.** Write each paragraph, list item, and blockquote as
382+ a single unwrapped line; never break a line in the middle of a sentence. Let the
383+ reader's editor soft-wrap. This keeps diffs meaningful — a reworded sentence
384+ touches one line, not a whole reflowed block.
385+- Leave fenced code blocks, tables, and headings as they are (code keeps its own
386+ newlines; one table row per line; a heading on its own line).
387+- When editing a file that was previously hard-wrapped, unwrap the blocks you touch.
388+
389+---
390+
391+## Testing Requirements
392+
393+Every addition, creation, and change ships with tests. A change without tests is not finished.
394+
395+**What to write**
396+- Unit tests for each new or modified behaviour, added in the same step as the code.
397+- Cover the happy path **and** every error or edge case the code explicitly handles.
398+- When fixing a bug, first write the test that reproduces it, and confirm it fails before the
399+ fix.
400+- Tests are deterministic: no real network, no wall-clock or random dependence, no reliance on
401+ test execution order.
402+- Test names state the behaviour under test, so a failure is legible without reading the body.
403+
404+**How to run them**
405+- There must be **one documented command** that runs the whole suite.
406+- **Reuse the project's existing convention** — add a `Makefile` / `Taskfile.yml` target, or a
407+ `package.json` script, if one of those is already in use. Only create a
408+ `scripts/test.sh`-style runner when the project has no such entry point, and make it
409+ executable (`chmod +x`) with a `set -eu` guard.
410+- The command must work from a clean checkout, with no undocumented manual setup.
411+
412+**Where to document it**
413+- In `docs/` under both languages — usually a `how-to/` page ("How to run the tests").
414+- In `.memory/summary.md`, in the build/test/run section.
415+- In the project `README` if it already documents commands.
416+
417+---
418+
419+## Strict Rules
420+
421+**You must NEVER:**
422+- ❌ Create a commit without an explicit request
423+- ❌ Delete existing code without confirmation
424+- ❌ Change the architecture without agreement
425+- ❌ Skip a step without approval
426+- ❌ Continue if the user has not approved the previous step
427+- ❌ Take shortcuts "to keep things simple"
428+- ❌ Implement something different from what was asked
429+- ❌ Declare a change done without tests, docs in both languages, an updated `README.md` for
430+ every touched package, a passing quality gate, and an updated `.memory/`
431+- ❌ Weaken the quality gate, delete tests, or disable a linter to make a check pass
432+- ❌ Document in only one language
433+- ❌ Rewrite or delete past `history.md` entries
434+
435+**You must ALWAYS:**
436+- ✅ Read `.memory/` before asking the user anything
437+- ✅ Stop after each step for approval
438+- ✅ Explain your technical choices
439+- ✅ Ask for confirmation on important decisions
440+- ✅ Follow the approved plan exactly
441+- ✅ Be transparent about what you are doing
442+- ✅ Propose alternatives if you see a problem
443+- ✅ Write tests alongside the code, in the same step
444+- ✅ Invoke the `quality` skill before declaring the work complete
445+- ✅ Invoke the `diataxis-doc` skill for documentation, in English and French
446+- ✅ Update `.memory/` at the end of the session
447+
448+---
449+
450+## Handling Problems
451+
452+If you hit a problem during implementation:
453+
454+1. **STOP immediately**
455+2. Explain the problem clearly
456+3. Propose alternative solutions
457+4. **Wait** for the user's decision
458+5. **NEVER** work around the problem by deleting code
459+
460+If the session ends with the problem unresolved, record it in
461+`.memory/handoffs/` under "Open questions / blockers" before stopping.
462+
463+---
464+
465+## Communication Format
466+
467+Use this format to communicate clearly:
468+
469+```
470+=== STEP [N]: [Step name] ===
471+
472+📋 What I am going to do:
473+- [Action 1]
474+- [Action 2]
475+
476+✅ Approval needed? [Yes/No]
477+
478+[If Yes, wait for a reply before continuing]
479+
480+---
481+
482+[Implementation + tests]
483+
484+---
485+
486+📊 STEP [N] SUMMARY:
487+✅ Created: [file1], [file2]
488+✅ Modified: [file3]
489+🧪 Tests: [what was added] — [pass/fail, command used]
490+✅ Feature: [description]
491+
492+⏸️ CHECKPOINT
493+Please review and approve before continuing.
494+
495+Options:
496+1. ✅ Continue to the next step
497+2. 🔧 Change something
498+3. 📝 Adjust the plan
499+```
500+
501+For the closing phases:
502+
503+```
504+=== PHASE 5: QUALITY GATE ===
505+🔍 Invoking the `quality` skill…
506+📊 Gate: [PASS/FAIL] — errors: [n], warnings: [n], smells: [n]
507+[If FAIL: refactoring, then re-measuring]
508+
509+=== PHASE 6: DOCUMENTATION (EN + FR) ===
510+📚 Invoking the `diataxis-doc` skill (language: Both)…
511+✅ docs/en/: [pages]
512+✅ docs/fr/: [pages]
513+
514+=== PHASE 7: PROJECT MEMORY ===
515+🧠 .memory/summary.md — updated
516+🧠 .memory/history.md — entry appended
517+🧠 .memory/handoffs/… — written
518+```
519+
520+---
521+
522+## Usage Example
523+
524+```
525+User: /methodical-dev
526+
527+Skill: I will guide you through a methodical development process.
528+
529+=== PHASE 0: PROJECT MEMORY ===
530+
531+[Reads .memory/summary.md, history.md, handoffs/]
532+
533+Found a handoff from 2026-07-20: the parser is done, the encoder was left
534+half-written. Resume that, or start something new?
535+
536+=== PHASE 1: GATHERING INFORMATION ===
537+
538+[Asks the remaining questions via AskUserQuestion]
539+
540+=== PHASE 2: GIT CHECK ===
541+
542+[Checks git status]
543+
544+=== PHASE 3: PLANNING ===
545+
546+Here is the proposed plan:
547+
548+□ Step 1: Create the base structure + tests
549+□ Step 2: Implement the business logic + tests
550+□ Step 3: Wire up the public API + doc comments with examples
551+□ Step 4: Quality gate (quality skill)
552+□ Step 5: Documentation EN + FR (diataxis-doc skill)
553+□ Step 6: Update .memory/
554+
555+Does this plan work for you?
556+
557+[Wait for approval]
558+
559+=== PHASE 4: IMPLEMENTATION ===
560+
561+=== STEP 1: Create the base structure ===
562+
563+📋 What I am going to do:
564+- Create src/feature/index.ts
565+- Create src/feature/types.ts
566+- Create src/feature/index.test.ts
567+- Set up the exports
568+
569+[Implementation + tests]
570+
571+📊 STEP 1 SUMMARY:
572+✅ Created: src/feature/index.ts, src/feature/types.ts
573+✅ Modified: src/index.ts (exports)
574+🧪 Tests: src/feature/index.test.ts — 4 passing (npm test)
575+
576+⏸️ CHECKPOINT - Approval?
577+
578+...
579+
580+=== PHASE 5: QUALITY GATE ===
581+📊 Gate: PASS — errors: 0, warnings: 0, smells: 0
582+
583+=== PHASE 6: DOCUMENTATION (EN + FR) ===
584+✅ docs/en/how-to/use-feature.md, docs/en/reference/feature.md
585+✅ docs/fr/how-to/utiliser-feature.md, docs/fr/reference/feature.md
586+
587+=== PHASE 7: PROJECT MEMORY ===
588+🧠 .memory/ updated (summary, history entry, handoff)
589+
590+=== PHASE 8: FINAL VALIDATION ===
591+[Checklist + commit proposal]
592+```
593+
594+## Notes
595+
596+This skill is designed to maximise the user's control while still benefiting from AI
597+assistance. It forces a stop at every step to avoid the common drift of AI assistants.
598+
599+The user always stays in charge and can step in at any time.
600+
601+The closing phases (quality, documentation, memory) are what stop a feature from being
602+"finished" in the narrow sense — code that runs — while leaving behind untested logic,
603+undocumented behaviour, and no trace of why any of it was done that way.
new file mode 100644
@@ -0,0 +1,603 @@
1+---
2+name: methodical-dev
3+description: Guide the user through a structured development methodology, following best practices for working with Mistral Vibe in a controlled and effective way. Every change is delivered complete: readable and documented code, unit tests with a documented way to run them, bilingual (EN + FR) Diátaxis documentation, a passing quality gate, and an updated .memory/ project record. Use when starting a feature, refactoring, or whenever you want a controlled, approval-gated development process.
4+---
5+# Methodical Development Skill
6+
7+## Description
8+Guide the user through a structured development methodology, following best practices for
9+working with Mistral Vibe in a controlled and effective way.
10+
11+The core promise: **no change is "done" until it is readable, tested, documented in both
12+English and French, quality-gated, and recorded in the project's memory.** The phases below
13+enforce that; none of them is optional.
14+
15+## When to Use
16+- When starting a new feature
17+- When refactoring or changing existing behaviour
18+- When you want to follow a structured process
19+- To avoid the common pitfalls of AI-assisted development
20+
21+## Companion skills
22+
23+This skill orchestrates two others. Both ship alongside it and must be **invoked as skills**,
24+not reimplemented:
25+
26+| Skill | When this skill invokes it | Phase |
27+| --- | --- | --- |
28+| `quality` | After implementation, to measure quality and refactor until the gate passes | Phase 5 |
29+| `diataxis-doc` | After the quality gate, to write or update the documentation | Phase 6 |
30+
31+## Instructions
32+
33+You are a skill that guides the user through a rigorous development methodology. You must
34+follow this process step by step.
35+
36+---
37+
38+## Phase 0: Read the project memory
39+
40+**Before asking the user anything**, read `.memory/` at the repository root:
41+
42+```bash
43+ls -la .memory/ 2>/dev/null && cat .memory/summary.md 2>/dev/null
44+```
45+
46+- If `.memory/` exists, read `summary.md`, the tail of `history.md`, and the most recent file
47+ in `handoffs/`. It tells you the project's current state, past decisions, and any work left
48+ in flight — **do not ask the user for information that is already recorded there.**
49+- If `.memory/` does not exist, you will create it in Phase 7. Do not create it yet.
50+
51+If a handoff records unfinished work, surface it now and ask whether to resume it or start
52+something new.
53+
54+---
55+
56+## Phase 1: Gathering Information
57+
58+Ask the user these questions using AskUserQuestion — skipping any the memory already answers:
59+
60+1. **Goal of the feature**
61+ - What feature do you want to develop?
62+ - What is the exact scope of this feature?
63+
64+2. **Technical Constraints**
65+ - Which frameworks/libraries must you use?
66+ - Are there any version constraints?
67+ - Are there architectural patterns to follow?
68+
69+3. **Documentation and Examples**
70+ - Do you have documentation to reference?
71+ - Do you have similar existing code that could serve as an example?
72+
73+4. **Style and Conventions**
74+ - Are there specific naming conventions?
75+ - Is there a particular code style to follow?
76+
77+---
78+
79+## Phase 2: Git Check
80+
81+Check the state of the repository:
82+
83+```bash
84+# Check that we are in a git repo
85+git status
86+
87+# If there is no repo, offer to initialise one
88+git init
89+```
90+
91+If the user is not on a dedicated branch, **strongly recommend** creating a feature branch.
92+
93+**IMPORTANT**: Do not create the branch automatically. Ask the user:
94+- What branch name would they like?
95+- Do they want you to create the branch, or would they rather do it themselves?
96+
97+---
98+
99+## Phase 3: Detailed Planning
100+
101+1. **Analyse the existing code** (if needed)
102+ - Use Glob and Grep to understand the structure
103+ - Identify the files to modify
104+ - Identify the existing patterns to follow
105+
106+2. **Identify the project's test and build commands.** Look for a `Makefile`, `Taskfile.yml`,
107+ `package.json` scripts, `pyproject.toml`, or the CI workflow. You need these for Phase 4
108+ and Phase 5, and you must reuse the project's existing convention rather than inventing a
109+ parallel one.
110+
111+3. **Create a detailed plan** using TodoWrite
112+ - Break the feature into logical steps (5–8 steps maximum)
113+ - Each step must be atomic and testable
114+ - Order the steps by dependency
115+ - **Every implementation step carries its own unit tests** — do not plan a single
116+ "write the tests" step at the end
117+ - Include the mandatory closing phases as plan items: quality gate, bilingual
118+ documentation, memory update
119+
120+4. **Present the plan** to the user
121+ - Explain each step
122+ - Ask for approval before continuing
123+ - Allow adjustments
124+
125+---
126+
127+## Phase 4: Guided Implementation
128+
129+For each step of the plan:
130+
131+1. **Before starting the step**
132+ - Mark the step as `in_progress` with TodoWrite
133+ - Explain what you are about to do
134+ - Ask for confirmation if the step is complex
135+
136+2. **During the step**
137+ - Implement only what is planned for this step
138+ - Write the code to the **Code Standards** below — readable, maintainable, documented
139+ with usage examples
140+ - Write or update the **unit tests** for this step's behaviour (see Testing Requirements)
141+ - Do **NOT** take shortcuts
142+ - Do **NOT** delete existing code without asking
143+ - Do **NOT** change the architecture without agreement
144+ - Explain the technical choices as you go
145+
146+3. **After the step**
147+ - Run the tests and show the result — a step whose tests do not pass is not finished
148+ - Mark the step as `completed` with TodoWrite
149+ - Summarise what was done
150+ - List the files created/modified
151+ - **STOP and wait for the user's approval**
152+
153+4. **Mandatory checkpoint**
154+ - Ask the user to:
155+ - Review the code produced
156+ - Test the behaviour
157+ - Confirm it matches their request
158+ - Offer to:
159+ - Continue to the next step
160+ - Change something in the current step
161+ - Adjust the remaining plan
162+
163+---
164+
165+## Phase 5: Quality Gate (mandatory)
166+
167+**Invoke the `quality` skill.** Do not measure quality by eye and do not skip this phase,
168+even for a one-line change.
169+
170+The skill configures qlty if needed, measures lint issues / code smells / complexity, writes
171+a report under `.quality/`, records the run in its history so progression is visible, and
172+tells you whether the gate passed.
173+
174+- **Gate passed** → report the numbers and move to Phase 6.
175+- **Gate failed** → refactor as the skill directs, re-run the tests from Phase 4, and
176+ re-measure. Repeat until the gate passes or the skill's own stopping conditions are hit
177+ (five iterations, or two runs with no improvement).
178+- **Still failing at the stopping condition** → do not silently continue. Report what
179+ improved, what remains, and why, then ask the user how to proceed.
180+
181+Never satisfy the gate by weakening it. Editing `.qlty/qlty.toml` exclusions, lowering
182+`.quality/gate.json` thresholds, or adding blanket lint suppressions is forbidden here for the
183+same reason it is forbidden inside the `quality` skill: it makes the measurement lie.
184+
185+For a long multi-step feature, running the quality skill after a large step — not only at the
186+end — catches drift earlier and is cheaper than one big cleanup.
187+
188+---
189+
190+## Phase 6: Documentation (mandatory, bilingual)
191+
192+**Invoke the `diataxis-doc` skill** to write or update the documentation for what you just
193+built. Every creation and every change gets documented — new behaviour, changed behaviour,
194+and removed behaviour alike.
195+
196+> **Do not ask the user which language to document in.** `diataxis-doc` normally opens by
197+> asking; when invoked from this skill the answer is already fixed: **Both** — English *and*
198+> French. Tell the skill this so it goes straight to the bilingual architecture.
199+
200+This produces one subfolder per language, each with its own complete four-quadrant structure:
201+
202+```
203+docs/
204+├── README.md ← language selector
205+├── en/
206+│ ├── README.md
207+│ ├── tutorials/ ← "teach me to get started"
208+│ ├── how-to/ ← "how do I do X?"
209+│ ├── reference/ ← "what are the exact details of X?"
210+│ └── explanation/ ← "why is it built this way?"
211+└── fr/
212+ └── … same structure
213+```
214+
215+Rules that matter here:
216+
217+- **Both languages stay in sync.** A change documented in English but not French is an
218+ incomplete change. Translate the content — do not copy the English text into `fr/`.
219+- **Cross-links stay inside one language.** A French page never links to an English page.
220+- **File the content with the Diátaxis compass**, do not dump everything into one page. New
221+ feature → usually a `how-to/` page plus a `reference/` entry; a design decision →
222+ `explanation/`.
223+- **The way to run the tests is documented**, in both languages (see Testing Requirements).
224+- **Every touched package's `README.md` is updated.** This is separate from `docs/` and applies to **all** packages, in whatever language that README already uses. Any package whose public surface you changed — a new, renamed, or removed exported function, type, method, flag, or CLI subcommand — gets its `README.md` brought back in sync **in the same change**. A README that lists part of a package's API but omits what you just added is a defect, not merely "a bit behind". If the repository keeps no per-package READMEs, this rule is inert — do not create them just to satisfy it.
225+- **A package-dependency diagram is kept in draw.io format.** It shows every package/module, the dependency arrows between them (and on third-party runtimes), and a one-line "what it is for" on each. Keep it at `docs/diagrams/packages.drawio` (or the project's existing diagrams location). **Create it when it does not exist, and update it whenever you add, remove, or re-wire a package** so it never drifts from the real import graph. Keep it as a single language-neutral file (labels in the docs' primary language), referenced from the architecture explanation page in each language. A `.drawio` file is plain XML (an `mxGraphModel`) that diagrams.net and the VS Code Draw.io extension open directly, so you can author and edit it as text; after writing it, sanity-check that the XML parses.
226+
227+---
228+
229+## Phase 7: Update the project memory (mandatory)
230+
231+Create or update `.memory/` at the repository root. This is the project's durable record: it
232+is what lets you — or a different agent, in a fresh sandbox — pick the work up later without
233+re-deriving everything.
234+
235+```
236+.memory/
237+├── README.md ← what this folder is and how it is maintained
238+├── summary.md ← living snapshot of the project's current state (edited in place)
239+├── history.md ← append-only chronological log (never rewritten)
240+└── handoffs/
241+ └── YYYY-MM-DD-<slug>.md ← one per session or feature
242+```
243+
244+`.memory/` is **committed to the repository**, not gitignored — that is the whole point.
245+
246+### `summary.md` — edited in place, never regenerated
247+
248+The current state of the project, kept short enough to stay read-worthy:
249+
250+- What the project is and does
251+- Architecture: main components and how they fit together
252+- Key technical decisions currently in force, and why
253+- How to build, test, and run it (the actual commands)
254+- Known limitations and open questions
255+
256+**Change only what this session establishes or invalidates, and leave the rest alone.** You have
257+seen a slice of the project; this file holds what every previous session established. Regenerating
258+it wholesale from your slice silently destroys accurate content you never looked at — the one way
259+this file stops being trustworthy. Write only what you verified; anything you could not check goes
260+under an explicit `## Not yet established` heading instead of a plausible guess, because the next
261+session will trust whatever is written here. If the project's state did not change, leave the file
262+untouched.
263+
264+### `history.md` — append only
265+
266+One dated entry per completed feature or session. **Never rewrite or delete past entries** —
267+a history you edit is not a history.
268+
269+```markdown
270+## 2026-07-25 — <what was done>
271+
272+- **Goal**: <what the user asked for>
273+- **Changes**: <files/components created or modified>
274+- **Decisions**: <choices made and why; alternatives rejected>
275+- **Tests**: <what was added, how to run it>
276+- **Quality**: <gate PASS/FAIL and the key numbers>
277+- **Docs**: <pages created/updated, in both languages>
278+```
279+
280+### `handoffs/YYYY-MM-DD-<slug>.md` — written at the end of the session
281+
282+The document a fresh agent reads to resume:
283+
284+```markdown
285+# Handoff — <date> — <topic>
286+
287+## State
288+<what works right now; what was just finished>
289+
290+## In flight
291+<work started but not finished, and exactly where it stopped>
292+
293+## Next steps
294+1. <the concrete next action>
295+2. …
296+
297+## Open questions / blockers
298+<decisions awaiting the user; anything that blocked progress>
299+
300+## Watch out for
301+<traps discovered along the way — failing commands, fragile assumptions>
302+```
303+
304+### Memory vs. documentation
305+
306+Keep the boundary clean, so content is not duplicated:
307+
308+| | Audience | Content |
309+| --- | --- | --- |
310+| `docs/` (Diátaxis) | **Users** of the project | How to use it, what the API is, why it is designed that way |
311+| `.memory/` | **Whoever continues the work** | Project history, session state, handoffs, decisions in progress |
312+
313+If a piece of information helps someone *use* the project, it belongs in `docs/`. If it helps
314+someone *continue building* it, it belongs in `.memory/`.
315+
316+---
317+
318+## Phase 8: Final Validation
319+
320+Once all the steps and phases are complete:
321+
322+1. **Full summary**
323+ - List of all files created
324+ - List of all files modified
325+ - Summary of the features implemented
326+
327+2. **Quality checklist**
328+ - [ ] Does the feature match the request exactly?
329+ - [ ] No unrequested deletions?
330+ - [ ] Is the code readable, maintainable, and documented with usage examples?
331+ - [ ] Are unit tests present for every change, and do they pass?
332+ - [ ] Is there a documented, single command to run the tests?
333+ - [ ] Did the `quality` skill run, and did the gate pass?
334+ - [ ] Is the documentation updated in **both** English and French?
335+ - [ ] Is every touched package's `README.md` updated to match its current public API?
336+ - [ ] Is the package-dependency draw.io diagram present and consistent with the current import graph?
337+ - [ ] Are the conventions respected?
338+ - [ ] Is `.memory/` updated — `summary.md`, a new `history.md` entry, and a handoff?
339+
340+3. **Commit proposal**
341+ - Propose a structured commit message
342+ - List the files to add to the commit — including `docs/` and `.memory/`
343+ - Do **NOT** commit automatically
344+ - Let the user do it, or use the /commit skill
345+
346+---
347+
348+## Code Standards
349+
350+Generated code must be readable and maintainable by a **human**, not merely correct. Someone
351+unfamiliar with it should understand it without asking you.
352+
353+**Readability**
354+- Names state intent: `retryAfterSeconds`, not `d` or `tmp2`. No abbreviations that are not
355+ domain-standard.
356+- One function does one thing, and is short enough to read without scrolling.
357+- Prefer the explicit over the clever. A dense one-liner that needs a comment to be understood
358+ should be several plain lines instead.
359+- Early returns over deep nesting.
360+- **Match the surrounding code.** Its existing style wins over your preferences.
361+
362+**Maintainability**
363+- No duplicated logic — extract it the second time it appears.
364+- Errors are handled where they can be handled meaningfully, never silently swallowed.
365+- No dead code, no commented-out code, no `TODO` without a concrete follow-up noted in
366+ `.memory/handoffs/`.
367+- Keep the public surface small: expose what callers need, no more.
368+
369+**Documentation in the code**
370+- Every public/exported function, type, class, and module gets a doc comment saying what it
371+ does, what it expects, and what it returns or raises.
372+- **Each public API's doc comment includes a short usage example** — a few lines a reader can
373+ copy. Use the language's idiom for this: Go `Example` functions (which are also tests),
374+ Python docstring examples, JSDoc `@example`, Rust doc-tests.
375+- Comments explain **why**, not what. If a comment restates the code, delete it and improve
376+ the name instead.
377+- Document non-obvious constraints and invariants at the point they apply.
378+
379+**Markdown and prose files**
380+- Applies to every Markdown file you write — `docs/`, `.memory/`, `README`s.
381+- **Do not hard-wrap prose.** Write each paragraph, list item, and blockquote as
382+ a single unwrapped line; never break a line in the middle of a sentence. Let the
383+ reader's editor soft-wrap. This keeps diffs meaningful — a reworded sentence
384+ touches one line, not a whole reflowed block.
385+- Leave fenced code blocks, tables, and headings as they are (code keeps its own
386+ newlines; one table row per line; a heading on its own line).
387+- When editing a file that was previously hard-wrapped, unwrap the blocks you touch.
388+
389+---
390+
391+## Testing Requirements
392+
393+Every addition, creation, and change ships with tests. A change without tests is not finished.
394+
395+**What to write**
396+- Unit tests for each new or modified behaviour, added in the same step as the code.
397+- Cover the happy path **and** every error or edge case the code explicitly handles.
398+- When fixing a bug, first write the test that reproduces it, and confirm it fails before the
399+ fix.
400+- Tests are deterministic: no real network, no wall-clock or random dependence, no reliance on
401+ test execution order.
402+- Test names state the behaviour under test, so a failure is legible without reading the body.
403+
404+**How to run them**
405+- There must be **one documented command** that runs the whole suite.
406+- **Reuse the project's existing convention** — add a `Makefile` / `Taskfile.yml` target, or a
407+ `package.json` script, if one of those is already in use. Only create a
408+ `scripts/test.sh`-style runner when the project has no such entry point, and make it
409+ executable (`chmod +x`) with a `set -eu` guard.
410+- The command must work from a clean checkout, with no undocumented manual setup.
411+
412+**Where to document it**
413+- In `docs/` under both languages — usually a `how-to/` page ("How to run the tests").
414+- In `.memory/summary.md`, in the build/test/run section.
415+- In the project `README` if it already documents commands.
416+
417+---
418+
419+## Strict Rules
420+
421+**You must NEVER:**
422+- ❌ Create a commit without an explicit request
423+- ❌ Delete existing code without confirmation
424+- ❌ Change the architecture without agreement
425+- ❌ Skip a step without approval
426+- ❌ Continue if the user has not approved the previous step
427+- ❌ Take shortcuts "to keep things simple"
428+- ❌ Implement something different from what was asked
429+- ❌ Declare a change done without tests, docs in both languages, an updated `README.md` for
430+ every touched package, a passing quality gate, and an updated `.memory/`
431+- ❌ Weaken the quality gate, delete tests, or disable a linter to make a check pass
432+- ❌ Document in only one language
433+- ❌ Rewrite or delete past `history.md` entries
434+
435+**You must ALWAYS:**
436+- ✅ Read `.memory/` before asking the user anything
437+- ✅ Stop after each step for approval
438+- ✅ Explain your technical choices
439+- ✅ Ask for confirmation on important decisions
440+- ✅ Follow the approved plan exactly
441+- ✅ Be transparent about what you are doing
442+- ✅ Propose alternatives if you see a problem
443+- ✅ Write tests alongside the code, in the same step
444+- ✅ Invoke the `quality` skill before declaring the work complete
445+- ✅ Invoke the `diataxis-doc` skill for documentation, in English and French
446+- ✅ Update `.memory/` at the end of the session
447+
448+---
449+
450+## Handling Problems
451+
452+If you hit a problem during implementation:
453+
454+1. **STOP immediately**
455+2. Explain the problem clearly
456+3. Propose alternative solutions
457+4. **Wait** for the user's decision
458+5. **NEVER** work around the problem by deleting code
459+
460+If the session ends with the problem unresolved, record it in
461+`.memory/handoffs/` under "Open questions / blockers" before stopping.
462+
463+---
464+
465+## Communication Format
466+
467+Use this format to communicate clearly:
468+
469+```
470+=== STEP [N]: [Step name] ===
471+
472+📋 What I am going to do:
473+- [Action 1]
474+- [Action 2]
475+
476+✅ Approval needed? [Yes/No]
477+
478+[If Yes, wait for a reply before continuing]
479+
480+---
481+
482+[Implementation + tests]
483+
484+---
485+
486+📊 STEP [N] SUMMARY:
487+✅ Created: [file1], [file2]
488+✅ Modified: [file3]
489+🧪 Tests: [what was added] — [pass/fail, command used]
490+✅ Feature: [description]
491+
492+⏸️ CHECKPOINT
493+Please review and approve before continuing.
494+
495+Options:
496+1. ✅ Continue to the next step
497+2. 🔧 Change something
498+3. 📝 Adjust the plan
499+```
500+
501+For the closing phases:
502+
503+```
504+=== PHASE 5: QUALITY GATE ===
505+🔍 Invoking the `quality` skill…
506+📊 Gate: [PASS/FAIL] — errors: [n], warnings: [n], smells: [n]
507+[If FAIL: refactoring, then re-measuring]
508+
509+=== PHASE 6: DOCUMENTATION (EN + FR) ===
510+📚 Invoking the `diataxis-doc` skill (language: Both)…
511+✅ docs/en/: [pages]
512+✅ docs/fr/: [pages]
513+
514+=== PHASE 7: PROJECT MEMORY ===
515+🧠 .memory/summary.md — updated
516+🧠 .memory/history.md — entry appended
517+🧠 .memory/handoffs/… — written
518+```
519+
520+---
521+
522+## Usage Example
523+
524+```
525+User: /methodical-dev
526+
527+Skill: I will guide you through a methodical development process.
528+
529+=== PHASE 0: PROJECT MEMORY ===
530+
531+[Reads .memory/summary.md, history.md, handoffs/]
532+
533+Found a handoff from 2026-07-20: the parser is done, the encoder was left
534+half-written. Resume that, or start something new?
535+
536+=== PHASE 1: GATHERING INFORMATION ===
537+
538+[Asks the remaining questions via AskUserQuestion]
539+
540+=== PHASE 2: GIT CHECK ===
541+
542+[Checks git status]
543+
544+=== PHASE 3: PLANNING ===
545+
546+Here is the proposed plan:
547+
548+□ Step 1: Create the base structure + tests
549+□ Step 2: Implement the business logic + tests
550+□ Step 3: Wire up the public API + doc comments with examples
551+□ Step 4: Quality gate (quality skill)
552+□ Step 5: Documentation EN + FR (diataxis-doc skill)
553+□ Step 6: Update .memory/
554+
555+Does this plan work for you?
556+
557+[Wait for approval]
558+
559+=== PHASE 4: IMPLEMENTATION ===
560+
561+=== STEP 1: Create the base structure ===
562+
563+📋 What I am going to do:
564+- Create src/feature/index.ts
565+- Create src/feature/types.ts
566+- Create src/feature/index.test.ts
567+- Set up the exports
568+
569+[Implementation + tests]
570+
571+📊 STEP 1 SUMMARY:
572+✅ Created: src/feature/index.ts, src/feature/types.ts
573+✅ Modified: src/index.ts (exports)
574+🧪 Tests: src/feature/index.test.ts — 4 passing (npm test)
575+
576+⏸️ CHECKPOINT - Approval?
577+
578+...
579+
580+=== PHASE 5: QUALITY GATE ===
581+📊 Gate: PASS — errors: 0, warnings: 0, smells: 0
582+
583+=== PHASE 6: DOCUMENTATION (EN + FR) ===
584+✅ docs/en/how-to/use-feature.md, docs/en/reference/feature.md
585+✅ docs/fr/how-to/utiliser-feature.md, docs/fr/reference/feature.md
586+
587+=== PHASE 7: PROJECT MEMORY ===
588+🧠 .memory/ updated (summary, history entry, handoff)
589+
590+=== PHASE 8: FINAL VALIDATION ===
591+[Checklist + commit proposal]
592+```
593+
594+## Notes
595+
596+This skill is designed to maximise the user's control while still benefiting from AI
597+assistance. It forces a stop at every step to avoid the common drift of AI assistants.
598+
599+The user always stays in charge and can step in at any time.
600+
601+The closing phases (quality, documentation, memory) are what stop a feature from being
602+"finished" in the narrow sense — code that runs — while leaving behind untested logic,
603+undocumented behaviour, and no trace of why any of it was done that way.
added .vibe/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 .vibe/skills/quality/scripts/quality_report.py +463 -0
new file mode 100644
@@ -0,0 +1,463 @@
1+#!/usr/bin/env python3
2+"""Measure project code quality with qlty and emit a report, a history line, and a gate verdict.
3+
4+Read-only with respect to the source tree: `qlty check` runs with --no-fix so a
5+measurement never silently rewrites the code it is measuring.
6+
7+Exit codes: 0 gate passed, 1 gate failed, 2 tooling/setup error.
8+"""
9+
10+from __future__ import annotations
11+
12+import argparse
13+import json
14+import re
15+import shutil
16+
17+# nosec B404 - running qlty *is* this script's job. Every command it launches is a
18+# literal argv list built in this file; none is assembled from user input, and none
19+# goes through a shell. See the matching justification on subprocess.run below.
20+import subprocess # nosec B404
21+import sys
22+from datetime import datetime, timezone
23+from pathlib import Path
24+
25+ANSI = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
26+
27+# Overridable per project by writing .quality/gate.json with any subset of these
28+# keys. Kept strict on purpose: the gate is what the refactoring loop drives to
29+# zero. A null value disables that check.
30+DEFAULT_GATE = {
31+ "max_error": 0,
32+ "max_warning": 0,
33+ "max_note": None,
34+ "max_smells": 0,
35+ "max_file_complexity": None,
36+ "max_total_complexity": None,
37+}
38+
39+QLTY_COMMON = ["--no-upgrade-check"]
40+
41+
42+def run(cmd: list[str], cwd: Path, timeout: int = 1800) -> tuple[int, str, str]:
43+ try:
44+ # nosec B603 - cmd is always a literal argv list from this file (qlty or git
45+ # with fixed subcommands), never a string and never shell-interpreted, so
46+ # there is no injection surface. shell=False is the default and is what makes
47+ # this the safe form rather than the risky one.
48+ p = subprocess.run( # nosec B603
49+ cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout
50+ )
51+ except FileNotFoundError:
52+ return 127, "", f"command not found: {cmd[0]}"
53+ except subprocess.TimeoutExpired:
54+ return 124, "", f"timed out after {timeout}s: {' '.join(cmd)}"
55+ return p.returncode, p.stdout, p.stderr
56+
57+
58+def git(args: list[str], cwd: Path) -> str:
59+ code, out, _ = run(["git"] + args, cwd, timeout=60)
60+ return out.strip() if code == 0 else ""
61+
62+
63+def ensure_setup(ws: Path, allow_init: bool) -> None:
64+ """Verify qlty is usable here, running `qlty init` when the project has no config."""
65+ if shutil.which("qlty") is None:
66+ die(
67+ "qlty is not installed. This project expects the `dev-toolkit` sbx kit "
68+ "(see kits/dev-toolkit/README.md)."
69+ )
70+ if not (ws / ".git").exists() and not git(["rev-parse", "--git-dir"], ws):
71+ die(f"{ws} is not a git repository — qlty needs git to scope its analysis.")
72+
73+ if (ws / ".qlty" / "qlty.toml").is_file():
74+ return
75+ if not allow_init:
76+ die("no .qlty/qlty.toml found and --no-init was passed; run `qlty init --yes`.")
77+
78+ log("no .qlty/qlty.toml found — running `qlty init --yes` (first-time setup)")
79+ code, out, err = run(["qlty", "init", "--yes"] + QLTY_COMMON, ws, timeout=1800)
80+ if code != 0 or not (ws / ".qlty" / "qlty.toml").is_file():
81+ die(f"`qlty init` failed (exit {code}).\n{(out + err).strip()[-2000:]}")
82+ log("qlty initialised: .qlty/qlty.toml written")
83+
84+
85+def sarif_location(res: dict) -> tuple[str, int | None]:
86+ """Return the file and start line of a SARIF result, or ("", None) if it has none.
87+
88+ Every level of the structure is optional in the spec and plugins do omit them,
89+ hence the `or {}` at each step rather than a single chained access.
90+ """
91+ locs = res.get("locations") or []
92+ if not locs:
93+ return "", None
94+ phys = (locs[0] or {}).get("physicalLocation") or {}
95+ uri = ((phys.get("artifactLocation") or {}).get("uri")) or ""
96+ return uri, (phys.get("region") or {}).get("startLine")
97+
98+
99+def sarif_finding(res: dict) -> dict:
100+ """Flatten one SARIF result into the shape the rest of this script uses."""
101+ uri, line = sarif_location(res)
102+ return {
103+ "rule": res.get("ruleId") or "unknown",
104+ # SARIF omits `level` when it equals the rule default; treat an absent
105+ # level as "warning" rather than dropping the finding.
106+ "level": (res.get("level") or "warning").lower(),
107+ "message": ((res.get("message") or {}).get("text") or "").strip(),
108+ "file": uri,
109+ "line": line,
110+ }
111+
112+
113+def sarif_results(payload: str) -> list[dict]:
114+ """Flatten SARIF runs into a list of result dicts, tolerating partial output."""
115+ try:
116+ doc = json.loads(payload)
117+ except json.JSONDecodeError:
118+ return []
119+ return [
120+ sarif_finding(res)
121+ for run_ in doc.get("runs") or []
122+ for res in run_.get("results") or []
123+ ]
124+
125+
126+def collect_check(ws: Path) -> tuple[list[dict], str]:
127+ # --no-fail / --no-error: always emit a report instead of exiting on findings.
128+ # --no-fix: measurement must not mutate the tree.
129+ cmd = [
130+ "qlty", "check", "--all", "--no-fix", "--no-fail", "--no-error",
131+ "--no-progress", "--sarif",
132+ ] + QLTY_COMMON
133+ code, out, err = run(cmd, ws)
134+ results = sarif_results(out)
135+ note = "" if (results or code == 0) else f"`qlty check` exit {code}: {err.strip()[-500:]}"
136+ return results, note
137+
138+
139+def collect_smells(ws: Path) -> tuple[list[dict], str]:
140+ cmd = ["qlty", "smells", "--all", "--quiet", "--sarif"] + QLTY_COMMON
141+ code, out, err = run(cmd, ws)
142+ results = sarif_results(out)
143+ note = "" if (results or code == 0) else f"`qlty smells` exit {code}: {err.strip()[-500:]}"
144+ return results, note
145+
146+
147+def table_cells(text: str) -> list[list[str]]:
148+ """Split a pipe table into rows of cells, dropping rules and blank lines.
149+
150+ qlty renders an ANSI-coloured table and ignores NO_COLOR, so the escape codes
151+ are stripped here rather than by the caller.
152+ """
153+ rows: list[list[str]] = []
154+ for raw in text.splitlines():
155+ line = ANSI.sub("", raw).strip()
156+ if "|" not in line or set(line) <= set("-+| "):
157+ continue
158+ rows.append([c.strip() for c in line.split("|")])
159+ return rows
160+
161+
162+def metric_row(header: list[str], cells: list[str]) -> dict:
163+ """Pair a data row with its header, keeping `name` textual and the rest numeric."""
164+ row: dict = {}
165+ for key, val in zip(header, cells):
166+ if key == "name":
167+ row["name"] = val
168+ continue
169+ try:
170+ row[key] = int(val)
171+ except ValueError:
172+ row[key] = val
173+ return row
174+
175+
176+def parse_metrics_table(text: str) -> tuple[dict, list[dict]]:
177+ """Split qlty's metrics table into its TOTAL row and its per-file rows.
178+
179+ A row whose width does not match the header is skipped rather than guessed at:
180+ qlty occasionally wraps a long path, and inventing a value would silently
181+ corrupt the numbers this whole report rests on.
182+ """
183+ header: list[str] = []
184+ total: dict = {}
185+ rows: list[dict] = []
186+ for cells in table_cells(text):
187+ if not header:
188+ header = [c.lower() for c in cells]
189+ continue
190+ if len(cells) != len(header):
191+ continue
192+ row = metric_row(header, cells)
193+ if row.get("name", "").upper() == "TOTAL":
194+ total = {k: v for k, v in row.items() if k != "name"}
195+ else:
196+ rows.append(row)
197+ return total, rows
198+
199+
200+def collect_metrics(ws: Path) -> tuple[dict, list[dict], str]:
201+ """Run `qlty metrics` and return its TOTAL row, its per-file rows, and any note."""
202+ cmd = ["qlty", "metrics", "--all", "--quiet"] + QLTY_COMMON
203+ code, out, err = run(cmd, ws)
204+ if code != 0 and not out.strip():
205+ return {}, [], f"`qlty metrics` exit {code}: {err.strip()[-500:]}"
206+
207+ total, rows = parse_metrics_table(out)
208+ return total, rows, ""
209+
210+
211+def load_gate(ws: Path, override: Path | None) -> dict:
212+ gate = dict(DEFAULT_GATE)
213+ path = override or (ws / ".quality" / "gate.json")
214+ if path.is_file():
215+ try:
216+ gate.update(json.loads(path.read_text()))
217+ except (json.JSONDecodeError, OSError) as exc:
218+ die(f"could not read gate file {path}: {exc}")
219+ return gate
220+
221+
222+def evaluate(gate: dict, counts: dict, smells: int, metrics: dict, files: list[dict]) -> list[str]:
223+ """Return one human-readable violation string per breached threshold."""
224+ breaches: list[str] = []
225+
226+ def check(limit_key: str, actual: int, label: str) -> None:
227+ limit = gate.get(limit_key)
228+ if limit is not None and actual > limit:
229+ breaches.append(f"{label}: {actual} (max {limit})")
230+
231+ check("max_error", counts.get("error", 0), "error-level issues")
232+ check("max_warning", counts.get("warning", 0), "warning-level issues")
233+ check("max_note", counts.get("note", 0), "note-level issues")
234+ check("max_smells", smells, "code smells")
235+ check("max_total_complexity", metrics.get("complex", 0), "total complexity")
236+
237+ limit = gate.get("max_file_complexity")
238+ if limit is not None:
239+ over = [f for f in files if isinstance(f.get("complex"), int) and f["complex"] > limit]
240+ for f in sorted(over, key=lambda r: -r["complex"])[:10]:
241+ breaches.append(f"{f['name']} complexity {f['complex']} (max {limit})")
242+ return breaches
243+
244+
245+def tally(results: list[dict], key: str) -> dict[str, int]:
246+ counts: dict[str, int] = {}
247+ for r in results:
248+ counts[r[key]] = counts.get(r[key], 0) + 1
249+ return counts
250+
251+
252+def log(msg: str) -> None:
253+ print(f"[quality] {msg}", file=sys.stderr)
254+
255+
256+def die(msg: str) -> None:
257+ print(f"[quality] error: {msg}", file=sys.stderr)
258+ sys.exit(2)
259+
260+
261+def md_table(headers: list[str], rows: list[list[str]]) -> str:
262+ if not rows:
263+ return "_none_\n"
264+
265+ out = ["| " + " | ".join(headers) + " |", "|" + "|".join(["---"] * len(headers)) + "|"]
266+ out += ["| " + " | ".join(rows_) + " |" for rows_ in (map(str, r) for r in rows)]
267+ return "\n".join(out) + "\n"
268+
269+
270+def delta(cur: int, prev: int | None) -> str:
271+ if prev is None:
272+ return "—"
273+ d = cur - prev
274+ if d == 0:
275+ return "±0"
276+ return f"{d:+d}"
277+
278+
279+def report_header(entry: dict, prev: dict | None, breaches: list[str]) -> list[str]:
280+ verdict = "✅ **PASS**" if entry["gate_passed"] else "❌ **FAIL**"
281+ lines = [
282+ f"# Quality report — {entry['timestamp']}",
283+ "",
284+ f"- **Gate**: {verdict}",
285+ f"- **Commit**: `{entry['commit'] or 'n/a'}` on `{entry['branch'] or 'n/a'}`",
286+ f"- **qlty**: {entry['qlty_version']}",
287+ f"- **Run**: #{entry['run']}"
288+ + (f" (previous: {prev['timestamp']})" if prev else " (first recorded run)"),
289+ "",
290+ ]
291+ if breaches:
292+ lines += ["## Gate violations", ""]
293+ lines += [f"- {b}" for b in breaches]
294+ lines.append("")
295+ return lines
296+
297+
298+def report_issues(entry: dict, prev: dict | None, check_res: list[dict]) -> list[str]:
299+ """The lint section: counts by level, then the two "where to start" tables."""
300+ c = entry["counts"]
301+ p_counts = (prev or {}).get("counts") or {}
302+
303+ lines = ["## Lint issues (`qlty check`)", ""]
304+ lines.append(
305+ md_table(
306+ ["level", "count", "vs previous"],
307+ [[lvl, c.get(lvl, 0), delta(c.get(lvl, 0), p_counts.get(lvl))]
308+ for lvl in ("error", "warning", "note", "none")
309+ if c.get(lvl) or p_counts.get(lvl)],
310+ )
311+ )
312+
313+ by_rule = sorted(tally(check_res, "rule").items(), key=lambda kv: -kv[1])[:15]
314+ lines += ["### Top rules", "", md_table(["rule", "count"], [[k, v] for k, v in by_rule])]
315+
316+ by_file = sorted(tally([r for r in check_res if r["file"]], "file").items(),
317+ key=lambda kv: -kv[1])[:15]
318+ lines += ["### Most affected files", "",
319+ md_table(["file", "issues"], [[k, v] for k, v in by_file])]
320+ return lines
321+
322+
323+def report_smells(entry: dict, prev: dict | None, smell_res: list[dict]) -> list[str]:
324+ lines = ["## Code smells (`qlty smells`)", "",
325+ f"Total: **{entry['smells']}** (vs previous: "
326+ f"{delta(entry['smells'], (prev or {}).get('smells'))})", ""]
327+ smell_rows = [[r["rule"], r["file"] or "—", r["line"] or "—", r["message"][:110]]
328+ for r in smell_res[:20]]
329+ lines.append(md_table(["smell", "file", "line", "detail"], smell_rows))
330+ return lines
331+
332+
333+def report_metrics(entry: dict, prev: dict | None, files: list[dict]) -> list[str]:
334+ m = entry["metrics"]
335+ p_metrics = (prev or {}).get("metrics") or {}
336+
337+ lines = ["## Metrics (`qlty metrics`)", ""]
338+ metric_rows = [
339+ [k, m.get(k, "—"), delta(m[k], p_metrics.get(k)) if isinstance(m.get(k), int) else "—"]
340+ for k in ("funcs", "classes", "fields", "cyclo", "complex", "lcom", "lines", "loc")
341+ if k in m
342+ ]
343+ lines.append(md_table(["metric", "total", "vs previous"], metric_rows))
344+
345+ worst = sorted([f for f in files if isinstance(f.get("complex"), int)],
346+ key=lambda r: -r["complex"])[:15]
347+ lines += ["### Most complex files", "",
348+ md_table(["file", "complex", "cyclo", "loc"],
349+ [[f["name"], f.get("complex", "—"), f.get("cyclo", "—"),
350+ f.get("loc", "—")] for f in worst])]
351+ return lines
352+
353+
354+def report_trend(entry: dict) -> list[str]:
355+ return ["## Trend", "", md_table(
356+ ["run", "timestamp", "error", "warning", "smells", "complex", "gate"],
357+ [[h["run"], h["timestamp"], (h.get("counts") or {}).get("error", 0),
358+ (h.get("counts") or {}).get("warning", 0), h.get("smells", 0),
359+ (h.get("metrics") or {}).get("complex", "—"),
360+ "PASS" if h.get("gate_passed") else "FAIL"]
361+ for h in entry["_trend"]],
362+ )]
363+
364+
365+def build_report(entry: dict, prev: dict | None, check_res: list[dict],
366+ smell_res: list[dict], files: list[dict], breaches: list[str],
367+ notes: list[str]) -> str:
368+ """Assemble the Markdown report, in the order a reader works through it."""
369+ lines = report_header(entry, prev, breaches)
370+ lines += report_issues(entry, prev, check_res)
371+ lines += report_smells(entry, prev, smell_res)
372+ lines += report_metrics(entry, prev, files)
373+
374+ if notes:
375+ lines += ["## Tooling notes", ""] + [f"- {n}" for n in notes] + [""]
376+
377+ lines += report_trend(entry)
378+ return "\n".join(lines)
379+
380+
381+def main() -> int:
382+ ap = argparse.ArgumentParser(description=__doc__)
383+ ap.add_argument("--workspace", default=".", help="project root (default: cwd)")
384+ ap.add_argument("--no-init", action="store_true",
385+ help="fail instead of running `qlty init` when unconfigured")
386+ ap.add_argument("--gate", type=Path, help="gate JSON file (default: .quality/gate.json)")
387+ ap.add_argument("--json", action="store_true", help="print the history entry to stdout")
388+ args = ap.parse_args()
389+
390+ ws = Path(args.workspace).resolve()
391+ if not ws.is_dir():
392+ die(f"workspace {ws} does not exist")
393+
394+ ensure_setup(ws, allow_init=not args.no_init)
395+
396+ notes: list[str] = []
397+ log("running qlty check / smells / metrics (this can take several minutes on a cold cache)")
398+ check_res, n1 = collect_check(ws)
399+ smell_res, n2 = collect_smells(ws)
400+ metrics, files, n3 = collect_metrics(ws)
401+ notes += [n for n in (n1, n2, n3) if n]
402+
403+ counts = tally(check_res, "level")
404+ gate = load_gate(ws, args.gate)
405+ breaches = evaluate(gate, counts, len(smell_res), metrics, files)
406+
407+ _, ver_out, _ = run(["qlty", "--version"], ws, timeout=60)
408+ outdir = ws / ".quality"
409+ outdir.mkdir(parents=True, exist_ok=True)
410+ history = outdir / "history.jsonl"
411+
412+ past: list[dict] = []
413+ if history.is_file():
414+ for line in history.read_text().splitlines():
415+ line = line.strip()
416+ if line:
417+ try:
418+ past.append(json.loads(line))
419+ except json.JSONDecodeError:
420+ continue
421+
422+ now = datetime.now(timezone.utc)
423+ entry = {
424+ "run": len(past) + 1,
425+ "timestamp": now.strftime("%Y-%m-%dT%H:%M:%SZ"),
426+ "commit": git(["rev-parse", "--short", "HEAD"], ws),
427+ "branch": git(["rev-parse", "--abbrev-ref", "HEAD"], ws),
428+ "qlty_version": ANSI.sub("", ver_out).strip() or "unknown",
429+ "counts": counts,
430+ "smells": len(smell_res),
431+ "metrics": metrics,
432+ "gate": {k: v for k, v in gate.items() if v is not None},
433+ "gate_passed": not breaches,
434+ "breaches": breaches,
435+ }
436+
437+ with history.open("a") as fh:
438+ fh.write(json.dumps(entry, sort_keys=True) + "\n")
439+
440+ entry["_trend"] = (past + [entry])[-10:]
441+ report = build_report(entry, past[-1] if past else None,
442+ check_res, smell_res, files, breaches, notes)
443+ del entry["_trend"]
444+
445+ stamp = now.strftime("%Y%m%dT%H%M%SZ")
446+ (outdir / f"report-{stamp}.md").write_text(report)
447+ (outdir / "report-latest.md").write_text(report)
448+
449+ print(report)
450+ log(f"report written to .quality/report-{stamp}.md (and report-latest.md)")
451+ log(f"history appended to .quality/history.jsonl (run #{entry['run']})")
452+ if args.json:
453+ print(json.dumps(entry, indent=2, sort_keys=True))
454+
455+ if breaches:
456+ log(f"GATE FAILED — {len(breaches)} violation(s)")
457+ return 1
458+ log("GATE PASSED")
459+ return 0
460+
461+
462+if __name__ == "__main__":
463+ sys.exit(main())
new file mode 100644
@@ -0,0 +1,463 @@
1+#!/usr/bin/env python3
2+"""Measure project code quality with qlty and emit a report, a history line, and a gate verdict.
3+
4+Read-only with respect to the source tree: `qlty check` runs with --no-fix so a
5+measurement never silently rewrites the code it is measuring.
6+
7+Exit codes: 0 gate passed, 1 gate failed, 2 tooling/setup error.
8+"""
9+
10+from __future__ import annotations
11+
12+import argparse
13+import json
14+import re
15+import shutil
16+
17+# nosec B404 - running qlty *is* this script's job. Every command it launches is a
18+# literal argv list built in this file; none is assembled from user input, and none
19+# goes through a shell. See the matching justification on subprocess.run below.
20+import subprocess # nosec B404
21+import sys
22+from datetime import datetime, timezone
23+from pathlib import Path
24+
25+ANSI = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
26+
27+# Overridable per project by writing .quality/gate.json with any subset of these
28+# keys. Kept strict on purpose: the gate is what the refactoring loop drives to
29+# zero. A null value disables that check.
30+DEFAULT_GATE = {
31+ "max_error": 0,
32+ "max_warning": 0,
33+ "max_note": None,
34+ "max_smells": 0,
35+ "max_file_complexity": None,
36+ "max_total_complexity": None,
37+}
38+
39+QLTY_COMMON = ["--no-upgrade-check"]
40+
41+
42+def run(cmd: list[str], cwd: Path, timeout: int = 1800) -> tuple[int, str, str]:
43+ try:
44+ # nosec B603 - cmd is always a literal argv list from this file (qlty or git
45+ # with fixed subcommands), never a string and never shell-interpreted, so
46+ # there is no injection surface. shell=False is the default and is what makes
47+ # this the safe form rather than the risky one.
48+ p = subprocess.run( # nosec B603
49+ cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout
50+ )
51+ except FileNotFoundError:
52+ return 127, "", f"command not found: {cmd[0]}"
53+ except subprocess.TimeoutExpired:
54+ return 124, "", f"timed out after {timeout}s: {' '.join(cmd)}"
55+ return p.returncode, p.stdout, p.stderr
56+
57+
58+def git(args: list[str], cwd: Path) -> str:
59+ code, out, _ = run(["git"] + args, cwd, timeout=60)
60+ return out.strip() if code == 0 else ""
61+
62+
63+def ensure_setup(ws: Path, allow_init: bool) -> None:
64+ """Verify qlty is usable here, running `qlty init` when the project has no config."""
65+ if shutil.which("qlty") is None:
66+ die(
67+ "qlty is not installed. This project expects the `dev-toolkit` sbx kit "
68+ "(see kits/dev-toolkit/README.md)."
69+ )
70+ if not (ws / ".git").exists() and not git(["rev-parse", "--git-dir"], ws):
71+ die(f"{ws} is not a git repository — qlty needs git to scope its analysis.")
72+
73+ if (ws / ".qlty" / "qlty.toml").is_file():
74+ return
75+ if not allow_init:
76+ die("no .qlty/qlty.toml found and --no-init was passed; run `qlty init --yes`.")
77+
78+ log("no .qlty/qlty.toml found — running `qlty init --yes` (first-time setup)")
79+ code, out, err = run(["qlty", "init", "--yes"] + QLTY_COMMON, ws, timeout=1800)
80+ if code != 0 or not (ws / ".qlty" / "qlty.toml").is_file():
81+ die(f"`qlty init` failed (exit {code}).\n{(out + err).strip()[-2000:]}")
82+ log("qlty initialised: .qlty/qlty.toml written")
83+
84+
85+def sarif_location(res: dict) -> tuple[str, int | None]:
86+ """Return the file and start line of a SARIF result, or ("", None) if it has none.
87+
88+ Every level of the structure is optional in the spec and plugins do omit them,
89+ hence the `or {}` at each step rather than a single chained access.
90+ """
91+ locs = res.get("locations") or []
92+ if not locs:
93+ return "", None
94+ phys = (locs[0] or {}).get("physicalLocation") or {}
95+ uri = ((phys.get("artifactLocation") or {}).get("uri")) or ""
96+ return uri, (phys.get("region") or {}).get("startLine")
97+
98+
99+def sarif_finding(res: dict) -> dict:
100+ """Flatten one SARIF result into the shape the rest of this script uses."""
101+ uri, line = sarif_location(res)
102+ return {
103+ "rule": res.get("ruleId") or "unknown",
104+ # SARIF omits `level` when it equals the rule default; treat an absent
105+ # level as "warning" rather than dropping the finding.
106+ "level": (res.get("level") or "warning").lower(),
107+ "message": ((res.get("message") or {}).get("text") or "").strip(),
108+ "file": uri,
109+ "line": line,
110+ }
111+
112+
113+def sarif_results(payload: str) -> list[dict]:
114+ """Flatten SARIF runs into a list of result dicts, tolerating partial output."""
115+ try:
116+ doc = json.loads(payload)
117+ except json.JSONDecodeError:
118+ return []
119+ return [
120+ sarif_finding(res)
121+ for run_ in doc.get("runs") or []
122+ for res in run_.get("results") or []
123+ ]
124+
125+
126+def collect_check(ws: Path) -> tuple[list[dict], str]:
127+ # --no-fail / --no-error: always emit a report instead of exiting on findings.
128+ # --no-fix: measurement must not mutate the tree.
129+ cmd = [
130+ "qlty", "check", "--all", "--no-fix", "--no-fail", "--no-error",
131+ "--no-progress", "--sarif",
132+ ] + QLTY_COMMON
133+ code, out, err = run(cmd, ws)
134+ results = sarif_results(out)
135+ note = "" if (results or code == 0) else f"`qlty check` exit {code}: {err.strip()[-500:]}"
136+ return results, note
137+
138+
139+def collect_smells(ws: Path) -> tuple[list[dict], str]:
140+ cmd = ["qlty", "smells", "--all", "--quiet", "--sarif"] + QLTY_COMMON
141+ code, out, err = run(cmd, ws)
142+ results = sarif_results(out)
143+ note = "" if (results or code == 0) else f"`qlty smells` exit {code}: {err.strip()[-500:]}"
144+ return results, note
145+
146+
147+def table_cells(text: str) -> list[list[str]]:
148+ """Split a pipe table into rows of cells, dropping rules and blank lines.
149+
150+ qlty renders an ANSI-coloured table and ignores NO_COLOR, so the escape codes
151+ are stripped here rather than by the caller.
152+ """
153+ rows: list[list[str]] = []
154+ for raw in text.splitlines():
155+ line = ANSI.sub("", raw).strip()
156+ if "|" not in line or set(line) <= set("-+| "):
157+ continue
158+ rows.append([c.strip() for c in line.split("|")])
159+ return rows
160+
161+
162+def metric_row(header: list[str], cells: list[str]) -> dict:
163+ """Pair a data row with its header, keeping `name` textual and the rest numeric."""
164+ row: dict = {}
165+ for key, val in zip(header, cells):
166+ if key == "name":
167+ row["name"] = val
168+ continue
169+ try:
170+ row[key] = int(val)
171+ except ValueError:
172+ row[key] = val
173+ return row
174+
175+
176+def parse_metrics_table(text: str) -> tuple[dict, list[dict]]:
177+ """Split qlty's metrics table into its TOTAL row and its per-file rows.
178+
179+ A row whose width does not match the header is skipped rather than guessed at:
180+ qlty occasionally wraps a long path, and inventing a value would silently
181+ corrupt the numbers this whole report rests on.
182+ """
183+ header: list[str] = []
184+ total: dict = {}
185+ rows: list[dict] = []
186+ for cells in table_cells(text):
187+ if not header:
188+ header = [c.lower() for c in cells]
189+ continue
190+ if len(cells) != len(header):
191+ continue
192+ row = metric_row(header, cells)
193+ if row.get("name", "").upper() == "TOTAL":
194+ total = {k: v for k, v in row.items() if k != "name"}
195+ else:
196+ rows.append(row)
197+ return total, rows
198+
199+
200+def collect_metrics(ws: Path) -> tuple[dict, list[dict], str]:
201+ """Run `qlty metrics` and return its TOTAL row, its per-file rows, and any note."""
202+ cmd = ["qlty", "metrics", "--all", "--quiet"] + QLTY_COMMON
203+ code, out, err = run(cmd, ws)
204+ if code != 0 and not out.strip():
205+ return {}, [], f"`qlty metrics` exit {code}: {err.strip()[-500:]}"
206+
207+ total, rows = parse_metrics_table(out)
208+ return total, rows, ""
209+
210+
211+def load_gate(ws: Path, override: Path | None) -> dict:
212+ gate = dict(DEFAULT_GATE)
213+ path = override or (ws / ".quality" / "gate.json")
214+ if path.is_file():
215+ try:
216+ gate.update(json.loads(path.read_text()))
217+ except (json.JSONDecodeError, OSError) as exc:
218+ die(f"could not read gate file {path}: {exc}")
219+ return gate
220+
221+
222+def evaluate(gate: dict, counts: dict, smells: int, metrics: dict, files: list[dict]) -> list[str]:
223+ """Return one human-readable violation string per breached threshold."""
224+ breaches: list[str] = []
225+
226+ def check(limit_key: str, actual: int, label: str) -> None:
227+ limit = gate.get(limit_key)
228+ if limit is not None and actual > limit:
229+ breaches.append(f"{label}: {actual} (max {limit})")
230+
231+ check("max_error", counts.get("error", 0), "error-level issues")
232+ check("max_warning", counts.get("warning", 0), "warning-level issues")
233+ check("max_note", counts.get("note", 0), "note-level issues")
234+ check("max_smells", smells, "code smells")
235+ check("max_total_complexity", metrics.get("complex", 0), "total complexity")
236+
237+ limit = gate.get("max_file_complexity")
238+ if limit is not None:
239+ over = [f for f in files if isinstance(f.get("complex"), int) and f["complex"] > limit]
240+ for f in sorted(over, key=lambda r: -r["complex"])[:10]:
241+ breaches.append(f"{f['name']} complexity {f['complex']} (max {limit})")
242+ return breaches
243+
244+
245+def tally(results: list[dict], key: str) -> dict[str, int]:
246+ counts: dict[str, int] = {}
247+ for r in results:
248+ counts[r[key]] = counts.get(r[key], 0) + 1
249+ return counts
250+
251+
252+def log(msg: str) -> None:
253+ print(f"[quality] {msg}", file=sys.stderr)
254+
255+
256+def die(msg: str) -> None:
257+ print(f"[quality] error: {msg}", file=sys.stderr)
258+ sys.exit(2)
259+
260+
261+def md_table(headers: list[str], rows: list[list[str]]) -> str:
262+ if not rows:
263+ return "_none_\n"
264+
265+ out = ["| " + " | ".join(headers) + " |", "|" + "|".join(["---"] * len(headers)) + "|"]
266+ out += ["| " + " | ".join(rows_) + " |" for rows_ in (map(str, r) for r in rows)]
267+ return "\n".join(out) + "\n"
268+
269+
270+def delta(cur: int, prev: int | None) -> str:
271+ if prev is None:
272+ return "—"
273+ d = cur - prev
274+ if d == 0:
275+ return "±0"
276+ return f"{d:+d}"
277+
278+
279+def report_header(entry: dict, prev: dict | None, breaches: list[str]) -> list[str]:
280+ verdict = "✅ **PASS**" if entry["gate_passed"] else "❌ **FAIL**"
281+ lines = [
282+ f"# Quality report — {entry['timestamp']}",
283+ "",
284+ f"- **Gate**: {verdict}",
285+ f"- **Commit**: `{entry['commit'] or 'n/a'}` on `{entry['branch'] or 'n/a'}`",
286+ f"- **qlty**: {entry['qlty_version']}",
287+ f"- **Run**: #{entry['run']}"
288+ + (f" (previous: {prev['timestamp']})" if prev else " (first recorded run)"),
289+ "",
290+ ]
291+ if breaches:
292+ lines += ["## Gate violations", ""]
293+ lines += [f"- {b}" for b in breaches]
294+ lines.append("")
295+ return lines
296+
297+
298+def report_issues(entry: dict, prev: dict | None, check_res: list[dict]) -> list[str]:
299+ """The lint section: counts by level, then the two "where to start" tables."""
300+ c = entry["counts"]
301+ p_counts = (prev or {}).get("counts") or {}
302+
303+ lines = ["## Lint issues (`qlty check`)", ""]
304+ lines.append(
305+ md_table(
306+ ["level", "count", "vs previous"],
307+ [[lvl, c.get(lvl, 0), delta(c.get(lvl, 0), p_counts.get(lvl))]
308+ for lvl in ("error", "warning", "note", "none")
309+ if c.get(lvl) or p_counts.get(lvl)],
310+ )
311+ )
312+
313+ by_rule = sorted(tally(check_res, "rule").items(), key=lambda kv: -kv[1])[:15]
314+ lines += ["### Top rules", "", md_table(["rule", "count"], [[k, v] for k, v in by_rule])]
315+
316+ by_file = sorted(tally([r for r in check_res if r["file"]], "file").items(),
317+ key=lambda kv: -kv[1])[:15]
318+ lines += ["### Most affected files", "",
319+ md_table(["file", "issues"], [[k, v] for k, v in by_file])]
320+ return lines
321+
322+
323+def report_smells(entry: dict, prev: dict | None, smell_res: list[dict]) -> list[str]:
324+ lines = ["## Code smells (`qlty smells`)", "",
325+ f"Total: **{entry['smells']}** (vs previous: "
326+ f"{delta(entry['smells'], (prev or {}).get('smells'))})", ""]
327+ smell_rows = [[r["rule"], r["file"] or "—", r["line"] or "—", r["message"][:110]]
328+ for r in smell_res[:20]]
329+ lines.append(md_table(["smell", "file", "line", "detail"], smell_rows))
330+ return lines
331+
332+
333+def report_metrics(entry: dict, prev: dict | None, files: list[dict]) -> list[str]:
334+ m = entry["metrics"]
335+ p_metrics = (prev or {}).get("metrics") or {}
336+
337+ lines = ["## Metrics (`qlty metrics`)", ""]
338+ metric_rows = [
339+ [k, m.get(k, "—"), delta(m[k], p_metrics.get(k)) if isinstance(m.get(k), int) else "—"]
340+ for k in ("funcs", "classes", "fields", "cyclo", "complex", "lcom", "lines", "loc")
341+ if k in m
342+ ]
343+ lines.append(md_table(["metric", "total", "vs previous"], metric_rows))
344+
345+ worst = sorted([f for f in files if isinstance(f.get("complex"), int)],
346+ key=lambda r: -r["complex"])[:15]
347+ lines += ["### Most complex files", "",
348+ md_table(["file", "complex", "cyclo", "loc"],
349+ [[f["name"], f.get("complex", "—"), f.get("cyclo", "—"),
350+ f.get("loc", "—")] for f in worst])]
351+ return lines
352+
353+
354+def report_trend(entry: dict) -> list[str]:
355+ return ["## Trend", "", md_table(
356+ ["run", "timestamp", "error", "warning", "smells", "complex", "gate"],
357+ [[h["run"], h["timestamp"], (h.get("counts") or {}).get("error", 0),
358+ (h.get("counts") or {}).get("warning", 0), h.get("smells", 0),
359+ (h.get("metrics") or {}).get("complex", "—"),
360+ "PASS" if h.get("gate_passed") else "FAIL"]
361+ for h in entry["_trend"]],
362+ )]
363+
364+
365+def build_report(entry: dict, prev: dict | None, check_res: list[dict],
366+ smell_res: list[dict], files: list[dict], breaches: list[str],
367+ notes: list[str]) -> str:
368+ """Assemble the Markdown report, in the order a reader works through it."""
369+ lines = report_header(entry, prev, breaches)
370+ lines += report_issues(entry, prev, check_res)
371+ lines += report_smells(entry, prev, smell_res)
372+ lines += report_metrics(entry, prev, files)
373+
374+ if notes:
375+ lines += ["## Tooling notes", ""] + [f"- {n}" for n in notes] + [""]
376+
377+ lines += report_trend(entry)
378+ return "\n".join(lines)
379+
380+
381+def main() -> int:
382+ ap = argparse.ArgumentParser(description=__doc__)
383+ ap.add_argument("--workspace", default=".", help="project root (default: cwd)")
384+ ap.add_argument("--no-init", action="store_true",
385+ help="fail instead of running `qlty init` when unconfigured")
386+ ap.add_argument("--gate", type=Path, help="gate JSON file (default: .quality/gate.json)")
387+ ap.add_argument("--json", action="store_true", help="print the history entry to stdout")
388+ args = ap.parse_args()
389+
390+ ws = Path(args.workspace).resolve()
391+ if not ws.is_dir():
392+ die(f"workspace {ws} does not exist")
393+
394+ ensure_setup(ws, allow_init=not args.no_init)
395+
396+ notes: list[str] = []
397+ log("running qlty check / smells / metrics (this can take several minutes on a cold cache)")
398+ check_res, n1 = collect_check(ws)
399+ smell_res, n2 = collect_smells(ws)
400+ metrics, files, n3 = collect_metrics(ws)
401+ notes += [n for n in (n1, n2, n3) if n]
402+
403+ counts = tally(check_res, "level")
404+ gate = load_gate(ws, args.gate)
405+ breaches = evaluate(gate, counts, len(smell_res), metrics, files)
406+
407+ _, ver_out, _ = run(["qlty", "--version"], ws, timeout=60)
408+ outdir = ws / ".quality"
409+ outdir.mkdir(parents=True, exist_ok=True)
410+ history = outdir / "history.jsonl"
411+
412+ past: list[dict] = []
413+ if history.is_file():
414+ for line in history.read_text().splitlines():
415+ line = line.strip()
416+ if line:
417+ try:
418+ past.append(json.loads(line))
419+ except json.JSONDecodeError:
420+ continue
421+
422+ now = datetime.now(timezone.utc)
423+ entry = {
424+ "run": len(past) + 1,
425+ "timestamp": now.strftime("%Y-%m-%dT%H:%M:%SZ"),
426+ "commit": git(["rev-parse", "--short", "HEAD"], ws),
427+ "branch": git(["rev-parse", "--abbrev-ref", "HEAD"], ws),
428+ "qlty_version": ANSI.sub("", ver_out).strip() or "unknown",
429+ "counts": counts,
430+ "smells": len(smell_res),
431+ "metrics": metrics,
432+ "gate": {k: v for k, v in gate.items() if v is not None},
433+ "gate_passed": not breaches,
434+ "breaches": breaches,
435+ }
436+
437+ with history.open("a") as fh:
438+ fh.write(json.dumps(entry, sort_keys=True) + "\n")
439+
440+ entry["_trend"] = (past + [entry])[-10:]
441+ report = build_report(entry, past[-1] if past else None,
442+ check_res, smell_res, files, breaches, notes)
443+ del entry["_trend"]
444+
445+ stamp = now.strftime("%Y%m%dT%H%M%SZ")
446+ (outdir / f"report-{stamp}.md").write_text(report)
447+ (outdir / "report-latest.md").write_text(report)
448+
449+ print(report)
450+ log(f"report written to .quality/report-{stamp}.md (and report-latest.md)")
451+ log(f"history appended to .quality/history.jsonl (run #{entry['run']})")
452+ if args.json:
453+ print(json.dumps(entry, indent=2, sort_keys=True))
454+
455+ if breaches:
456+ log(f"GATE FAILED — {len(breaches)} violation(s)")
457+ return 1
458+ log("GATE PASSED")
459+ return 0
460+
461+
462+if __name__ == "__main__":
463+ sys.exit(main())
modified agent.dmr.yaml +4 -0
@@ -27,6 +27,10 @@ bashTool: true
2727 # The built-in file tools: read_file, write_file, edit_file.
2828 editTools: true
2929
30+# The built-in plan tool for managing multi-step plans.
31+# Set to false to disable plan creation and tracking via the `plan` tool.
32+planTool: true
33+
3034 # Language servers for the `lsp` tool (definition, references, hover, symbols, diagnostics, rename).
3135 # The file extension picks the server. A server missing from the PATH is skipped with a warning.
3236 # No server listed = no `lsp` tool. Examples (uncomment what you have installed):
@@ -27,6 +27,10 @@ bashTool: true
27 # The built-in file tools: read_file, write_file, edit_file. 27 # The built-in file tools: read_file, write_file, edit_file.
28 editTools: true28 editTools: true
29 29
30+# The built-in plan tool for managing multi-step plans.
31+# Set to false to disable plan creation and tracking via the `plan` tool.
32+planTool: true
33+
30 # Language servers for the `lsp` tool (definition, references, hover, symbols, diagnostics, rename).34 # Language servers for the `lsp` tool (definition, references, hover, symbols, diagnostics, rename).
31 # The file extension picks the server. A server missing from the PATH is skipped with a warning.35 # The file extension picks the server. A server missing from the PATH is skipped with a warning.
32 # No server listed = no `lsp` tool. Examples (uncomment what you have installed):36 # No server listed = no `lsp` tool. Examples (uncomment what you have installed):
modified agent.llamacpp.yaml +4 -0
@@ -23,6 +23,10 @@ bashTool: true
2323 # The built-in file tools: read_file, write_file, edit_file.
2424 editTools: true
2525
26+# The built-in plan tool for managing multi-step plans.
27+# Set to false to disable plan creation and tracking via the `plan` tool.
28+planTool: true
29+
2630 # Language servers for the `lsp` tool (definition, references, hover, symbols, diagnostics, rename).
2731 # The file extension picks the server. A server missing from the PATH is skipped with a warning.
2832 # No server listed = no `lsp` tool. Examples (uncomment what you have installed):
@@ -23,6 +23,10 @@ bashTool: true
23 # The built-in file tools: read_file, write_file, edit_file. 23 # The built-in file tools: read_file, write_file, edit_file.
24 editTools: true24 editTools: true
25 25
26+# The built-in plan tool for managing multi-step plans.
27+# Set to false to disable plan creation and tracking via the `plan` tool.
28+planTool: true
29+
26 # Language servers for the `lsp` tool (definition, references, hover, symbols, diagnostics, rename).30 # Language servers for the `lsp` tool (definition, references, hover, symbols, diagnostics, rename).
27 # The file extension picks the server. A server missing from the PATH is skipped with a warning.31 # The file extension picks the server. A server missing from the PATH is skipped with a warning.
28 # No server listed = no `lsp` tool. Examples (uncomment what you have installed):32 # No server listed = no `lsp` tool. Examples (uncomment what you have installed):
modified agent.lsp.yaml +4 -0
@@ -34,6 +34,10 @@ bashTool: true
3434 # The built-in file tools: read_file, write_file, edit_file.
3535 editTools: true
3636
37+# The built-in plan tool for managing multi-step plans.
38+# Set to false to disable plan creation and tracking via the `plan` tool.
39+planTool: true
40+
3741 # Language servers for the `lsp` tool (definition, references, hover, symbols, diagnostics, rename).
3842 # The file extension picks the server. A server missing from the PATH is skipped with a warning at start-up;
3943 # with no server on the PATH the tool is not declared. Install what you use, uncomment its entry.
@@ -34,6 +34,10 @@ bashTool: true
34 # The built-in file tools: read_file, write_file, edit_file. 34 # The built-in file tools: read_file, write_file, edit_file.
35 editTools: true35 editTools: true
36 36
37+# The built-in plan tool for managing multi-step plans.
38+# Set to false to disable plan creation and tracking via the `plan` tool.
39+planTool: true
40+
37 # Language servers for the `lsp` tool (definition, references, hover, symbols, diagnostics, rename).41 # Language servers for the `lsp` tool (definition, references, hover, symbols, diagnostics, rename).
38 # The file extension picks the server. A server missing from the PATH is skipped with a warning at start-up;42 # The file extension picks the server. A server missing from the PATH is skipped with a warning at start-up;
39 # with no server on the PATH the tool is not declared. Install what you use, uncomment its entry.43 # with no server on the PATH the tool is not declared. Install what you use, uncomment its entry.
added demo/README.md +9 -0
new file mode 100644
@@ -0,0 +1,9 @@
1+# Mini Me Project
2+
3+## What is Mini Me?
4+
5+[Insert a detailed explanation of what Mini Me is here.]
6+
7+## License
8+
9+This project is licensed under the WTF License.
new file mode 100644
@@ -0,0 +1,9 @@
1+# Mini Me Project
2+
3+## What is Mini Me?
4+
5+[Insert a detailed explanation of what Mini Me is here.]
6+
7+## License
8+
9+This project is licensed under the WTF License.
modified demo/agent.dmr.2.yaml +4 -0
@@ -29,6 +29,10 @@ bashTool: true
2929 # The built-in file tools: read_file, write_file, edit_file.
3030 editTools: true
3131
32+# The built-in plan tool for managing multi-step plans.
33+# Set to false to disable plan creation and tracking via the `plan` tool.
34+planTool: true
35+
3236 lsp:
3337 servers:
3438 - name: gopls # go install golang.org/x/tools/gopls@latest
@@ -29,6 +29,10 @@ bashTool: true
29 # The built-in file tools: read_file, write_file, edit_file. 29 # The built-in file tools: read_file, write_file, edit_file.
30 editTools: true30 editTools: true
31 31
32+# The built-in plan tool for managing multi-step plans.
33+# Set to false to disable plan creation and tracking via the `plan` tool.
34+planTool: true
35+
32 lsp:36 lsp:
33 servers:37 servers:
34 - name: gopls # go install golang.org/x/tools/gopls@latest38 - name: gopls # go install golang.org/x/tools/gopls@latest
modified demo/agent.dmr.yaml +4 -0
@@ -29,6 +29,10 @@ bashTool: true
2929 # The built-in file tools: read_file, write_file, edit_file.
3030 editTools: true
3131
32+# The built-in plan tool for managing multi-step plans.
33+# Set to false to disable plan creation and tracking via the `plan` tool.
34+planTool: true
35+
3236 lsp:
3337 servers:
3438 - name: gopls # go install golang.org/x/tools/gopls@latest
@@ -29,6 +29,10 @@ bashTool: true
29 # The built-in file tools: read_file, write_file, edit_file. 29 # The built-in file tools: read_file, write_file, edit_file.
30 editTools: true30 editTools: true
31 31
32+# The built-in plan tool for managing multi-step plans.
33+# Set to false to disable plan creation and tracking via the `plan` tool.
34+planTool: true
35+
32 lsp:36 lsp:
33 servers:37 servers:
34 - name: gopls # go install golang.org/x/tools/gopls@latest38 - name: gopls # go install golang.org/x/tools/gopls@latest
modified demo/agent.lsp.yaml +4 -0
@@ -39,6 +39,10 @@ bashTool: true
3939 # The built-in file tools: read_file, write_file, edit_file.
4040 editTools: true
4141
42+# The built-in plan tool for managing multi-step plans.
43+# Set to false to disable plan creation and tracking via the `plan` tool.
44+planTool: true
45+
4246 # Language servers for the `lsp` tool (definition, references, hover, symbols, diagnostics, rename).
4347 # The file extension picks the server. A server missing from the PATH is skipped with a warning at start-up;
4448 # with no server on the PATH the tool is not declared. Install what you use, uncomment its entry.
@@ -39,6 +39,10 @@ bashTool: true
39 # The built-in file tools: read_file, write_file, edit_file. 39 # The built-in file tools: read_file, write_file, edit_file.
40 editTools: true40 editTools: true
41 41
42+# The built-in plan tool for managing multi-step plans.
43+# Set to false to disable plan creation and tracking via the `plan` tool.
44+planTool: true
45+
42 # Language servers for the `lsp` tool (definition, references, hover, symbols, diagnostics, rename).46 # Language servers for the `lsp` tool (definition, references, hover, symbols, diagnostics, rename).
43 # The file extension picks the server. A server missing from the PATH is skipped with a warning at start-up;47 # The file extension picks the server. A server missing from the PATH is skipped with a warning at start-up;
44 # with no server on the PATH the tool is not declared. Install what you use, uncomment its entry.48 # with no server on the PATH the tool is not declared. Install what you use, uncomment its entry.
added docs/en/how-to/manage-plans-and-tasks.md +440 -0
new file mode 100644
@@ -0,0 +1,440 @@
1+---
2+title: Manage Plans and Tasks
3+---
4+
5+# Manage Plans and Tasks
6+
7+Mini-me includes a built-in **plan** tool that lets the agent create, track, and manage multi-step todo lists. When you give the agent a list of tasks to do, it automatically creates a plan and tracks progress, showing you the current status after each response.
8+
9+## Quick Example
10+
11+```
12+You: Do these 3 things: 1. Create a README file, 2. Add a license, 3. Commit the changes
13+
14+Agent: I'll handle this in 3 steps. Starting with: Create a README file
15+📋 plan: create user-request-1
16+
17+Agent: README file created.
18+📋 [2/3] 🔄 Task 2: Add a license (in_progress)
19+
20+Agent: License added.
21+📋 [3/3] 🔄 Task 3: Commit the changes (in_progress)
22+
23+Agent: All changes committed.
24+📋 [3/3] ✅ All tasks complete!
25+```
26+
27+The `📋` line shows you exactly where the agent is in your plan.
28+
29+---
30+
31+## How It Works
32+
33+### The Plan Lifecycle
34+
35+1. **You state your request** as a list of tasks
36+2. **Agent detects the plan** and creates it using the `plan` tool
37+3. **Agent works on tasks** one by one, updating status
38+4. **Agent reports progress** after each action
39+5. **Plan completes** when all tasks are done
40+
41+### Status Icons
42+
43+| Icon | Meaning |
44+|------|---------|
45+| ⏳ | Task not started (open) |
46+| 🔄 | Task currently in progress |
47+| ✅ | Task completed successfully |
48+| ❌ | Task failed |
49+
50+### Status Line Format
51+
52+```
53+📋 [current/total] icon Task N: description (status)
54+```
55+
56+Examples:
57+- `📋 [1/5] ⏳ Task 1: Analyze the code (open)` - First task, not started yet
58+- `📋 [2/5] 🔄 Task 2: Fix the bugs (in_progress)` - Second task, currently working
59+- `📋 [5/5] ✅ All tasks complete!` - All done!
60+- `📋 [3/5] ❌ Task 3: Run tests (failed: connection timeout)` - Task failed with error
61+
62+---
63+
64+## User Scenarios
65+
66+### Scenario 1: Numbered List
67+
68+**Your input:**
69+```
70+Do these 5 things:
71+1. Check the current version
72+2. Update dependencies
73+3. Run tests
74+4. Fix any failures
75+5. Deploy to staging
76+```
77+
78+**What happens:**
79+- Agent creates a plan with 5 tasks
80+- Works through each sequentially
81+- Shows progress: `📋 [1/5]`, `📋 [2/5]`, etc.
82+- Completes with: `📋 [5/5] ✅ All tasks complete!`
83+
84+---
85+
86+### Scenario 2: Comma-Separated Tasks
87+
88+**Your input:**
89+```
90+Set up the project: create the directory, initialize git, and add a README
91+```
92+
93+**What happens:**
94+- Agent detects this as a multi-step request
95+- Creates a plan with 3 tasks
96+- Executes in order
97+
98+---
99+
100+### Scenario 3: Multiple Plans
101+
102+You can have multiple active plans in a single session.
103+
104+**Your input:**
105+```
106+First, fix the auth module: 1. Find the bug, 2. Write the fix, 3. Test it
107+```
108+
109+Agent works on the auth plan, shows: `📋 [2/3] 🔄 Task 2: Write the fix (in_progress)`
110+
111+**Then you say:**
112+```
113+Also, update the documentation: 1. Update README, 2. Update CHANGELOG
114+```
115+
116+Agent creates a second plan and manages both independently.
117+
118+**To see all active plans:**
119+```
120+What plans do I have?
121+```
122+
123+---
124+
125+## Interacting with Plans
126+
127+### Check Current Plan Status
128+
129+**Ask:** "What's the status of my plan?"
130+
131+**Agent responds:**
132+```
133+Current plan: [2/3] 🔄 Task 2: Write the fix (in_progress). Next: Test it
134+```
135+
136+### Skip a Task
137+
138+**Ask:** "Skip the test step, just deploy"
139+
140+**Agent:**
141+- Marks the current task as done
142+- Skips to the next task
143+- Confirms: "Skipping test step. Deploying to staging..."
144+
145+### Abandon a Plan
146+
147+**Ask:** "Never mind, I don't need this anymore"
148+
149+**Agent:**
150+- Asks for confirmation
151+- If you confirm, deletes the plan
152+- All progress is lost
153+
154+---
155+
156+## Plan Management Commands
157+
158+While the plan tool is primarily model-driven (the agent uses it automatically), you can also interact with plans directly:
159+
160+### List All Plans
161+
162+**Command:** `/plans` (not yet implemented in v1 - coming in v2)
163+
164+**What it shows:**
165+```
166+Active plans:
167+1. setup-project (2/5 tasks complete)
168+2. fix-bugs (0/3 tasks complete)
169+```
170+
171+### View Plan Details
172+
173+**Ask the agent:** "Show me the details of plan X"
174+
175+**Agent responds:** Full task list with status for each task
176+
177+### Delete a Plan
178+
179+**Ask the agent:** "Delete plan X"
180+
181+**Agent:** Confirms and deletes the plan
182+
183+---
184+
185+## Session Lifecycle
186+
187+### Plans and /new Command
188+
189+When you start a new session with `/new`:
190+- All active plans are **cleared**
191+- This is intentional: a new session means a fresh start
192+- If you want to continue an old plan, don't use `/new`
193+
194+### Plans and Agent Restart
195+
196+In v1 (current version):
197+- Plans are stored **in memory only**
198+- Plans are **lost** when the agent restarts
199+- This keeps v1 simple and fast
200+
201+In v2 (future):
202+- Plans will be persisted to session files
203+- Plans will survive agent restarts
204+
205+---
206+
207+## The plan Tool (For Advanced Users)
208+
209+The `plan` tool is available for the model to call. It supports these actions:
210+
211+| Action | Purpose | Required Parameters |
212+|--------|---------|---------------------|
213+| `create` | Create a new plan | `tasks` (array of `{description}`) |
214+| `get` | Get plan details | `name` |
215+| `update` | Update task status | `name`, `taskId`, `status` |
216+| `next` | Move to next task | `name` |
217+| `list` | List all plans | none |
218+| `delete` | Delete a plan | `name` |
219+| `status` | Get status line | `name` |
220+
221+### Status Values
222+
223+- `open` - Task not started
224+- `in_progress` - Task currently being worked on
225+- `done` - Task completed successfully
226+- `failed` - Task failed
227+
228+---
229+
230+## The plan-manager Skill
231+
232+The agent uses the `plan-manager` skill to know **when and how** to use the plan tool. This skill includes:
233+
234+- **Detection patterns:** How to recognize when a user gives a multi-step request
235+- **Usage guidelines:** When to create plans, update tasks, move between tasks
236+- **Display format:** How to show plan status to the user
237+- **Error handling:** What to do when things go wrong
238+
239+You don't need to manually load this skill - the agent does it automatically when appropriate.
240+
241+---
242+
243+## Examples
244+
245+### Example 1: Bug Fixing Workflow
246+
247+```
248+You: Fix these issues in the auth module:
249+ 1. Check for null pointer exceptions
250+ 2. Add input validation
251+ 3. Write unit tests
252+ 4. Update the documentation
253+
254+Agent: I'll handle this in 4 steps. Starting with: Check for null pointer exceptions
255+📋 plan: create user-request-1
256+
257+... (agent checks code) ...
258+
259+Agent: Found 3 potential null pointer issues.
260+📋 [2/4] 🔄 Task 2: Add input validation (in_progress)
261+
262+... (agent adds validation) ...
263+
264+Agent: Input validation added to auth.go.
265+📋 [3/4] 🔄 Task 3: Write unit tests (in_progress)
266+
267+... (agent writes tests) ...
268+
269+Agent: Tests written and passing.
270+📋 [4/4] 🔄 Task 4: Update the documentation (in_progress)
271+
272+... (agent updates docs) ...
273+
274+Agent: Documentation updated with new validation requirements.
275+📋 [4/4] ✅ All tasks complete!
276+```
277+
278+---
279+
280+### Example 2: Project Setup
281+
282+```
283+You: Set up a new Go project with:
284+ 1. Module initialization
285+ 2. Directory structure
286+ 3. Main file
287+ 4. Test file
288+
289+Agent: I'll set this up in 4 steps. Starting with: Module initialization
290+📋 plan: create user-request-1
291+
292+Agent: Module initialized with go mod
293+📋 [2/4] 🔄 Task 2: Directory structure (in_progress)
294+
295+Agent: Directories created (cmd, internal, pkg, tests)
296+📋 [3/4] 🔄 Task 3: Main file (in_progress)
297+
298+Agent: Main file created with basic structure
299+📋 [4/4] 🔄 Task 4: Test file (in_progress)
300+
301+Agent: Test file created with sample tests
302+📋 [4/4] ✅ All tasks complete!
303+```
304+
305+---
306+
307+### Example 3: Code Review
308+
309+```
310+You: Review this PR and:
311+ 1. Check for security issues
312+ 2. Verify tests pass
313+ 3. Review the documentation
314+ 4. Leave comments
315+
316+Agent: I'll review this PR in 4 steps. Starting with: Check for security issues
317+📋 plan: create user-request-1
318+
319+Agent: No security issues found.
320+📋 [2/4] 🔄 Task 2: Verify tests pass (in_progress)
321+
322+Agent: All tests passing.
323+📋 [3/4] 🔄 Task 3: Review the documentation (in_progress)
324+
325+Agent: Documentation looks good, just needs a typo fix.
326+📋 [4/4] 🔄 Task 4: Leave comments (in_progress)
327+
328+Agent: Comments left on the PR.
329+📋 [4/4] ✅ All tasks complete!
330+```
331+
332+---
333+
334+### Example 4: With a Task Failure
335+
336+```
337+You: Do these:
338+ 1. Connect to the database
339+ 2. Run the migration
340+ 3. Import the data
341+
342+Agent: I'll handle this in 3 steps. Starting with: Connect to the database
343+📋 plan: create user-request-1
344+
345+Agent: Database connection established.
346+📋 [2/3] 🔄 Task 2: Run the migration (in_progress)
347+
348+Agent: Migration completed.
349+📋 [3/3] 🔄 Task 3: Import the data (in_progress)
350+
351+Agent: Error: connection timeout to data source
352+📋 plan: update user-request-1
353+
354+Agent: ⚠️ Task 3 failed: connection timeout to data source
355+📋 [3/3] ❌ Task 3: Import the data (failed: connection timeout)
356+
357+Agent: 2 of 3 tasks completed. The data import failed due to a connection timeout. Would you like me to retry, or skip this task?
358+```
359+
360+---
361+
362+## Configuration
363+
364+The plan tool can be enabled or disabled via the YAML configuration file:
365+
366+```yaml
367+# In your agent.yaml file
368+planTool: true # Enabled by default
369+```
370+
371+To disable:
372+```yaml
373+planTool: false
374+```
375+
376+When disabled, the agent will not have access to the `plan` tool and will not automatically create or track plans.
377+
378+---
379+
380+## Troubleshooting
381+
382+### Agent Not Creating Plans
383+
384+If the agent isn't creating plans for your multi-step requests:
385+
386+1. **Make your request clearer** - Use numbered lists
387+ - ❌ "Fix some things"
388+ - ✅ "Do these: 1. Fix A, 2. Fix B, 3. Fix C"
389+
390+2. **Be more explicit** - Use words like "plan", "steps", "then"
391+ - ✅ "My plan is to first do X, then do Y"
392+
393+3. **Check the skill is loaded** - The agent should automatically load the `plan-manager` skill when it detects a plan-like request
394+
395+### Plans Disappearing
396+
397+In v1, plans are stored in memory and will be lost when:
398+- You start a new session with `/new`
399+- The agent restarts
400+- You close and reopen the terminal
401+
402+This is expected behavior in v1. Session persistence is coming in v2.
403+
404+---
405+
406+## Best Practices
407+
408+1. **Be specific** - The more detail in your task descriptions, the better the agent can execute them
409+
410+2. **Keep plans focused** - One plan per logical request. Don't mix unrelated tasks
411+
412+3. **Check progress** - The status line shows you exactly where the agent is
413+
414+4. **Ask for clarification** - If the agent seems stuck or confused, ask "What's the status?"
415+
416+5. **Start small** - For complex multi-step work, start with a few tasks and add more as needed
417+
418+---
419+
420+## Comparison with Other Tools
421+
422+| Feature | plan tool | skills | slash commands |
423+|---------|-----------|--------|----------------|
424+| Purpose | Track multi-step work | Load procedures | User commands |
425+| Trigger | Automatic (model detects) | Explicit (`/skill`) | Explicit (`/command`) |
426+| Persistence (v1) | In-memory, session only | File-based | N/A |
427+| User interaction | Indirect (via model) | Indirect (model loads) | Direct |
428+
429+The `plan` tool complements other mini-me features:
430+- Use **skills** for complex, reusable procedures
431+- Use **plan tool** for ad-hoc multi-step requests
432+- Use **slash commands** for direct user actions
433+
434+---
435+
436+## See Also
437+
438+- [Slash Commands Reference](../reference/slash-commands.md) - For user commands
439+- [Tools Reference](../reference/tools.md) - For all built-in tools
440+- [Skills Overview](../explanation/skills.md) - For the skill system
new file mode 100644
@@ -0,0 +1,440 @@
1+---
2+title: Manage Plans and Tasks
3+---
4+
5+# Manage Plans and Tasks
6+
7+Mini-me includes a built-in **plan** tool that lets the agent create, track, and manage multi-step todo lists. When you give the agent a list of tasks to do, it automatically creates a plan and tracks progress, showing you the current status after each response.
8+
9+## Quick Example
10+
11+```
12+You: Do these 3 things: 1. Create a README file, 2. Add a license, 3. Commit the changes
13+
14+Agent: I'll handle this in 3 steps. Starting with: Create a README file
15+📋 plan: create user-request-1
16+
17+Agent: README file created.
18+📋 [2/3] 🔄 Task 2: Add a license (in_progress)
19+
20+Agent: License added.
21+📋 [3/3] 🔄 Task 3: Commit the changes (in_progress)
22+
23+Agent: All changes committed.
24+📋 [3/3] ✅ All tasks complete!
25+```
26+
27+The `📋` line shows you exactly where the agent is in your plan.
28+
29+---
30+
31+## How It Works
32+
33+### The Plan Lifecycle
34+
35+1. **You state your request** as a list of tasks
36+2. **Agent detects the plan** and creates it using the `plan` tool
37+3. **Agent works on tasks** one by one, updating status
38+4. **Agent reports progress** after each action
39+5. **Plan completes** when all tasks are done
40+
41+### Status Icons
42+
43+| Icon | Meaning |
44+|------|---------|
45+| ⏳ | Task not started (open) |
46+| 🔄 | Task currently in progress |
47+| ✅ | Task completed successfully |
48+| ❌ | Task failed |
49+
50+### Status Line Format
51+
52+```
53+📋 [current/total] icon Task N: description (status)
54+```
55+
56+Examples:
57+- `📋 [1/5] ⏳ Task 1: Analyze the code (open)` - First task, not started yet
58+- `📋 [2/5] 🔄 Task 2: Fix the bugs (in_progress)` - Second task, currently working
59+- `📋 [5/5] ✅ All tasks complete!` - All done!
60+- `📋 [3/5] ❌ Task 3: Run tests (failed: connection timeout)` - Task failed with error
61+
62+---
63+
64+## User Scenarios
65+
66+### Scenario 1: Numbered List
67+
68+**Your input:**
69+```
70+Do these 5 things:
71+1. Check the current version
72+2. Update dependencies
73+3. Run tests
74+4. Fix any failures
75+5. Deploy to staging
76+```
77+
78+**What happens:**
79+- Agent creates a plan with 5 tasks
80+- Works through each sequentially
81+- Shows progress: `📋 [1/5]`, `📋 [2/5]`, etc.
82+- Completes with: `📋 [5/5] ✅ All tasks complete!`
83+
84+---
85+
86+### Scenario 2: Comma-Separated Tasks
87+
88+**Your input:**
89+```
90+Set up the project: create the directory, initialize git, and add a README
91+```
92+
93+**What happens:**
94+- Agent detects this as a multi-step request
95+- Creates a plan with 3 tasks
96+- Executes in order
97+
98+---
99+
100+### Scenario 3: Multiple Plans
101+
102+You can have multiple active plans in a single session.
103+
104+**Your input:**
105+```
106+First, fix the auth module: 1. Find the bug, 2. Write the fix, 3. Test it
107+```
108+
109+Agent works on the auth plan, shows: `📋 [2/3] 🔄 Task 2: Write the fix (in_progress)`
110+
111+**Then you say:**
112+```
113+Also, update the documentation: 1. Update README, 2. Update CHANGELOG
114+```
115+
116+Agent creates a second plan and manages both independently.
117+
118+**To see all active plans:**
119+```
120+What plans do I have?
121+```
122+
123+---
124+
125+## Interacting with Plans
126+
127+### Check Current Plan Status
128+
129+**Ask:** "What's the status of my plan?"
130+
131+**Agent responds:**
132+```
133+Current plan: [2/3] 🔄 Task 2: Write the fix (in_progress). Next: Test it
134+```
135+
136+### Skip a Task
137+
138+**Ask:** "Skip the test step, just deploy"
139+
140+**Agent:**
141+- Marks the current task as done
142+- Skips to the next task
143+- Confirms: "Skipping test step. Deploying to staging..."
144+
145+### Abandon a Plan
146+
147+**Ask:** "Never mind, I don't need this anymore"
148+
149+**Agent:**
150+- Asks for confirmation
151+- If you confirm, deletes the plan
152+- All progress is lost
153+
154+---
155+
156+## Plan Management Commands
157+
158+While the plan tool is primarily model-driven (the agent uses it automatically), you can also interact with plans directly:
159+
160+### List All Plans
161+
162+**Command:** `/plans` (not yet implemented in v1 - coming in v2)
163+
164+**What it shows:**
165+```
166+Active plans:
167+1. setup-project (2/5 tasks complete)
168+2. fix-bugs (0/3 tasks complete)
169+```
170+
171+### View Plan Details
172+
173+**Ask the agent:** "Show me the details of plan X"
174+
175+**Agent responds:** Full task list with status for each task
176+
177+### Delete a Plan
178+
179+**Ask the agent:** "Delete plan X"
180+
181+**Agent:** Confirms and deletes the plan
182+
183+---
184+
185+## Session Lifecycle
186+
187+### Plans and /new Command
188+
189+When you start a new session with `/new`:
190+- All active plans are **cleared**
191+- This is intentional: a new session means a fresh start
192+- If you want to continue an old plan, don't use `/new`
193+
194+### Plans and Agent Restart
195+
196+In v1 (current version):
197+- Plans are stored **in memory only**
198+- Plans are **lost** when the agent restarts
199+- This keeps v1 simple and fast
200+
201+In v2 (future):
202+- Plans will be persisted to session files
203+- Plans will survive agent restarts
204+
205+---
206+
207+## The plan Tool (For Advanced Users)
208+
209+The `plan` tool is available for the model to call. It supports these actions:
210+
211+| Action | Purpose | Required Parameters |
212+|--------|---------|---------------------|
213+| `create` | Create a new plan | `tasks` (array of `{description}`) |
214+| `get` | Get plan details | `name` |
215+| `update` | Update task status | `name`, `taskId`, `status` |
216+| `next` | Move to next task | `name` |
217+| `list` | List all plans | none |
218+| `delete` | Delete a plan | `name` |
219+| `status` | Get status line | `name` |
220+
221+### Status Values
222+
223+- `open` - Task not started
224+- `in_progress` - Task currently being worked on
225+- `done` - Task completed successfully
226+- `failed` - Task failed
227+
228+---
229+
230+## The plan-manager Skill
231+
232+The agent uses the `plan-manager` skill to know **when and how** to use the plan tool. This skill includes:
233+
234+- **Detection patterns:** How to recognize when a user gives a multi-step request
235+- **Usage guidelines:** When to create plans, update tasks, move between tasks
236+- **Display format:** How to show plan status to the user
237+- **Error handling:** What to do when things go wrong
238+
239+You don't need to manually load this skill - the agent does it automatically when appropriate.
240+
241+---
242+
243+## Examples
244+
245+### Example 1: Bug Fixing Workflow
246+
247+```
248+You: Fix these issues in the auth module:
249+ 1. Check for null pointer exceptions
250+ 2. Add input validation
251+ 3. Write unit tests
252+ 4. Update the documentation
253+
254+Agent: I'll handle this in 4 steps. Starting with: Check for null pointer exceptions
255+📋 plan: create user-request-1
256+
257+... (agent checks code) ...
258+
259+Agent: Found 3 potential null pointer issues.
260+📋 [2/4] 🔄 Task 2: Add input validation (in_progress)
261+
262+... (agent adds validation) ...
263+
264+Agent: Input validation added to auth.go.
265+📋 [3/4] 🔄 Task 3: Write unit tests (in_progress)
266+
267+... (agent writes tests) ...
268+
269+Agent: Tests written and passing.
270+📋 [4/4] 🔄 Task 4: Update the documentation (in_progress)
271+
272+... (agent updates docs) ...
273+
274+Agent: Documentation updated with new validation requirements.
275+📋 [4/4] ✅ All tasks complete!
276+```
277+
278+---
279+
280+### Example 2: Project Setup
281+
282+```
283+You: Set up a new Go project with:
284+ 1. Module initialization
285+ 2. Directory structure
286+ 3. Main file
287+ 4. Test file
288+
289+Agent: I'll set this up in 4 steps. Starting with: Module initialization
290+📋 plan: create user-request-1
291+
292+Agent: Module initialized with go mod
293+📋 [2/4] 🔄 Task 2: Directory structure (in_progress)
294+
295+Agent: Directories created (cmd, internal, pkg, tests)
296+📋 [3/4] 🔄 Task 3: Main file (in_progress)
297+
298+Agent: Main file created with basic structure
299+📋 [4/4] 🔄 Task 4: Test file (in_progress)
300+
301+Agent: Test file created with sample tests
302+📋 [4/4] ✅ All tasks complete!
303+```
304+
305+---
306+
307+### Example 3: Code Review
308+
309+```
310+You: Review this PR and:
311+ 1. Check for security issues
312+ 2. Verify tests pass
313+ 3. Review the documentation
314+ 4. Leave comments
315+
316+Agent: I'll review this PR in 4 steps. Starting with: Check for security issues
317+📋 plan: create user-request-1
318+
319+Agent: No security issues found.
320+📋 [2/4] 🔄 Task 2: Verify tests pass (in_progress)
321+
322+Agent: All tests passing.
323+📋 [3/4] 🔄 Task 3: Review the documentation (in_progress)
324+
325+Agent: Documentation looks good, just needs a typo fix.
326+📋 [4/4] 🔄 Task 4: Leave comments (in_progress)
327+
328+Agent: Comments left on the PR.
329+📋 [4/4] ✅ All tasks complete!
330+```
331+
332+---
333+
334+### Example 4: With a Task Failure
335+
336+```
337+You: Do these:
338+ 1. Connect to the database
339+ 2. Run the migration
340+ 3. Import the data
341+
342+Agent: I'll handle this in 3 steps. Starting with: Connect to the database
343+📋 plan: create user-request-1
344+
345+Agent: Database connection established.
346+📋 [2/3] 🔄 Task 2: Run the migration (in_progress)
347+
348+Agent: Migration completed.
349+📋 [3/3] 🔄 Task 3: Import the data (in_progress)
350+
351+Agent: Error: connection timeout to data source
352+📋 plan: update user-request-1
353+
354+Agent: ⚠️ Task 3 failed: connection timeout to data source
355+📋 [3/3] ❌ Task 3: Import the data (failed: connection timeout)
356+
357+Agent: 2 of 3 tasks completed. The data import failed due to a connection timeout. Would you like me to retry, or skip this task?
358+```
359+
360+---
361+
362+## Configuration
363+
364+The plan tool can be enabled or disabled via the YAML configuration file:
365+
366+```yaml
367+# In your agent.yaml file
368+planTool: true # Enabled by default
369+```
370+
371+To disable:
372+```yaml
373+planTool: false
374+```
375+
376+When disabled, the agent will not have access to the `plan` tool and will not automatically create or track plans.
377+
378+---
379+
380+## Troubleshooting
381+
382+### Agent Not Creating Plans
383+
384+If the agent isn't creating plans for your multi-step requests:
385+
386+1. **Make your request clearer** - Use numbered lists
387+ - ❌ "Fix some things"
388+ - ✅ "Do these: 1. Fix A, 2. Fix B, 3. Fix C"
389+
390+2. **Be more explicit** - Use words like "plan", "steps", "then"
391+ - ✅ "My plan is to first do X, then do Y"
392+
393+3. **Check the skill is loaded** - The agent should automatically load the `plan-manager` skill when it detects a plan-like request
394+
395+### Plans Disappearing
396+
397+In v1, plans are stored in memory and will be lost when:
398+- You start a new session with `/new`
399+- The agent restarts
400+- You close and reopen the terminal
401+
402+This is expected behavior in v1. Session persistence is coming in v2.
403+
404+---
405+
406+## Best Practices
407+
408+1. **Be specific** - The more detail in your task descriptions, the better the agent can execute them
409+
410+2. **Keep plans focused** - One plan per logical request. Don't mix unrelated tasks
411+
412+3. **Check progress** - The status line shows you exactly where the agent is
413+
414+4. **Ask for clarification** - If the agent seems stuck or confused, ask "What's the status?"
415+
416+5. **Start small** - For complex multi-step work, start with a few tasks and add more as needed
417+
418+---
419+
420+## Comparison with Other Tools
421+
422+| Feature | plan tool | skills | slash commands |
423+|---------|-----------|--------|----------------|
424+| Purpose | Track multi-step work | Load procedures | User commands |
425+| Trigger | Automatic (model detects) | Explicit (`/skill`) | Explicit (`/command`) |
426+| Persistence (v1) | In-memory, session only | File-based | N/A |
427+| User interaction | Indirect (via model) | Indirect (model loads) | Direct |
428+
429+The `plan` tool complements other mini-me features:
430+- Use **skills** for complex, reusable procedures
431+- Use **plan tool** for ad-hoc multi-step requests
432+- Use **slash commands** for direct user actions
433+
434+---
435+
436+## See Also
437+
438+- [Slash Commands Reference](../reference/slash-commands.md) - For user commands
439+- [Tools Reference](../reference/tools.md) - For all built-in tools
440+- [Skills Overview](../explanation/skills.md) - For the skill system
added docs/fr/how-to/manage-plans-and-tasks.md +440 -0
new file mode 100644
@@ -0,0 +1,440 @@
1+---
2+title: Gérer les plans et les tâches
3+---
4+
5+# Gérer les plans et les tâches
6+
7+Mini-me inclut un outil intégré **plan** qui permet à l'agent de créer, suivre et gérer des listes de tâches multi-étapes. Lorsque vous donnez à l'agent une liste de tâches à effectuer, il crée automatiquement un plan et suit la progression, en vous montrant l'état actuel après chaque réponse.
8+
9+## Exemple rapide
10+
11+```
12+Vous : Fais ces 3 choses : 1. Créer un fichier README, 2. Ajouter une licence, 3. Valider les modifications
13+
14+Agent : Je vais m'en occuper en 3 étapes. En commençant par : Créer un fichier README
15+📋 plan: create user-request-1
16+
17+Agent : Fichier README créé.
18+📋 [2/3] 🔄 Tâche 2 : Ajouter une licence (in_progress)
19+
20+Agent : Licence ajoutée.
21+📋 [3/3] 🔄 Tâche 3 : Valider les modifications (in_progress)
22+
23+Agent : Toutes les modifications ont été validées.
24+📋 [3/3] ✅ Toutes les tâches sont terminées !
25+```
26+
27+La ligne `📋` vous montre exactement où se trouve l'agent dans votre plan.
28+
29+---
30+
31+## Fonctionnement
32+
33+### Cycle de vie d'un plan
34+
35+1. **Vous formulez votre demande** sous forme de liste de tâches
36+2. **L'agent détecte le plan** et le crée en utilisant l'outil `plan`
37+3. **L'agent travaille sur les tâches** une par une, en mettant à jour l'état
38+4. **L'agent signale la progression** après chaque action
39+5. **Le plan est terminé** lorsque toutes les tâches sont terminées
40+
41+### Icônes d'état
42+
43+| Icône | Signification |
44+|-------|--------------|
45+| ⏳ | Tâche non démarrée (ouverte) |
46+| 🔄 | Tâche en cours |
47+| ✅ | Tâche terminée avec succès |
48+| ❌ | Tâche en échec |
49+
50+### Format de la ligne d'état
51+
52+```
53+📋 [courante/total] icône Tâche N : description (état)
54+```
55+
56+Exemples :
57+- `📋 [1/5] ⏳ Tâche 1 : Analyser le code (open)` - Première tâche, non démarrée
58+- `📋 [2/5] 🔄 Tâche 2 : Corriger les bugs (in_progress)` - Deuxième tâche, en cours
59+- `📋 [5/5] ✅ Toutes les tâches sont terminées !` - Tout est terminé !
60+- `📋 [3/5] ❌ Tâche 3 : Exécuter les tests (failed: timeout de connexion)` - Tâche en échec avec erreur
61+
62+---
63+
64+## Scénarios utilisateur
65+
66+### Scénario 1 : Liste numérotée
67+
68+**Votre entrée :**
69+```
70+Fais ces 5 choses :
71+1. Vérifier la version actuelle
72+2. Mettre à jour les dépendances
73+3. Exécuter les tests
74+4. Corriger les échecs
75+5. Déployer en staging
76+```
77+
78+**Ce qui se passe :**
79+- L'agent crée un plan avec 5 tâches
80+- Travaille sur chaque tâche séquentiellement
81+- Affiche la progression : `📋 [1/5]`, `📋 [2/5]`, etc.
82+- Termine avec : `📋 [5/5] ✅ Toutes les tâches sont terminées !`
83+
84+---
85+
86+### Scénario 2 : Tâches séparées par des virgules
87+
88+**Votre entrée :**
89+```
90+Configurer le projet : créer le répertoire, initialiser git, et ajouter un README
91+```
92+
93+**Ce qui se passe :**
94+- L'agent détecte qu'il s'agit d'une demande multi-étapes
95+- Crée un plan avec 3 tâches
96+- Exécute dans l'ordre
97+
98+---
99+
100+### Scénario 3 : Plusieurs plans
101+
102+Vous pouvez avoir plusieurs plans actifs dans une seule session.
103+
104+**Votre entrée :**
105+```
106+D'abord, corrige le module d'authentification : 1. Trouver le bug, 2. Écrire la correction, 3. Le tester
107+```
108+
109+L'agent travaille sur le plan d'authentification, affiche : `📋 [2/3] 🔄 Tâche 2 : Écrire la correction (in_progress)`
110+
111+**Ensuite vous dites :**
112+```
113+Aussi, mettre à jour la documentation : 1. Mettre à jour le README, 2. Mettre à jour le CHANGELOG
114+```
115+
116+L'agent crée un deuxième plan et gère les deux indépendamment.
117+
118+**Pour voir tous les plans actifs :**
119+```
120+Quels plans ai-je ?
121+```
122+
123+---
124+
125+## Interagir avec les plans
126+
127+### Vérifier l'état actuel du plan
128+
129+**Demandez :** "Quel est l'état de mon plan ?"
130+
131+**L'agent répond :**
132+```
133+Plan actuel : [2/3] 🔄 Tâche 2 : Écrire la correction (in_progress). Suivant : Le tester
134+```
135+
136+### Sauter une tâche
137+
138+**Demandez :** "Saute l'étape de test, déploie directement"
139+
140+**Agent :**
141+- Marque la tâche actuelle comme terminée
142+- Saute à la tâche suivante
143+- Confirme : "Je saute l'étape de test. Déploiement en staging..."
144+
145+### Abandonner un plan
146+
147+**Demandez :** "Peu importe, je n'en ai plus besoin"
148+
149+**Agent :**
150+- Demande confirmation
151+- Si vous confirmez, supprime le plan
152+- Toute la progression est perdue
153+
154+---
155+
156+## Commandes de gestion des plans
157+
158+Bien que l'outil plan soit principalement piloté par le modèle (l'agent l'utilise automatiquement), vous pouvez également interagir directement avec les plans :
159+
160+### Lister tous les plans
161+
162+**Commande :** `/plans` (pas encore implémenté en v1 - arrive en v2)
163+
164+**Ce que cela affiche :**
165+```
166+Plans actifs :
167+1. configuration-projet (2/5 tâches terminées)
168+2. corriger-bugs (0/3 tâches terminées)
169+```
170+
171+### Voir les détails d'un plan
172+
173+**Demandez à l'agent :** "Montre-moi les détails du plan X"
174+
175+**L'agent répond :** Liste complète des tâches avec l'état pour chaque tâche
176+
177+### Supprimer un plan
178+
179+**Demandez à l'agent :** "Supprime le plan X"
180+
181+**Agent :** Confirme et supprime le plan
182+
183+---
184+
185+## Cycle de vie de la session
186+
187+### Plans et commande /new
188+
189+Lorsque vous démarrez une nouvelle session avec `/new` :
190+- Tous les plans actifs sont **supprimés**
191+- C'est intentionnel : une nouvelle session signifie un nouveau départ
192+- Si vous voulez continuer un ancien plan, ne pas utiliser `/new`
193+
194+### Plans et redémarrage de l'agent
195+
196+En v1 (version actuelle) :
197+- Les plans sont stockés **uniquement en mémoire**
198+- Les plans sont **perdus** lorsque l'agent redémarre
199+- Cela garde la v1 simple et rapide
200+
201+En v2 (future) :
202+- Les plans seront persistés dans les fichiers de session
203+- Les plans survivront aux redémarrages de l'agent
204+
205+---
206+
207+## L'outil plan (pour les utilisateurs avancés)
208+
209+L'outil `plan` est disponible pour que le modèle puisse l'appeler. Il prend en charge ces actions :
210+
211+| Action | Objectif | Paramètres requis |
212+|--------|----------|-------------------|
213+| `create` | Créer un nouveau plan | `tasks` (tableau de `{description}`) |
214+| `get` | Obtenir les détails d'un plan | `name` |
215+| `update` | Mettre à jour l'état d'une tâche | `name`, `taskId`, `status` |
216+| `next` | Passer à la tâche suivante | `name` |
217+| `list` | Lister tous les plans | aucun |
218+| `delete` | Supprimer un plan | `name` |
219+| `status` | Obtenir la ligne d'état | `name` |
220+
221+### Valeurs d'état
222+
223+- `open` - Tâche non démarrée
224+- `in_progress` - Tâche actuellement en cours
225+- `done` - Tâche terminée avec succès
226+- `failed` - Tâche en échec
227+
228+---
229+
230+## La compétence plan-manager
231+
232+L'agent utilise la compétence `plan-manager` pour savoir **quand et comment** utiliser l'outil plan. Cette compétence inclut :
233+
234+- **Modèles de détection** : Comment reconnaître quand un utilisateur donne une demande multi-étapes
235+- **Directives d'utilisation** : Quand créer des plans, mettre à jour des tâches, passer entre les tâches
236+- **Format d'affichage** : Comment montrer l'état du plan à l'utilisateur
237+- **Gestion des erreurs** : Que faire quand les choses tournent mal
238+
239+Vous n'avez pas besoin de charger manuellement cette compétence - l'agent le fait automatiquement lorsque cela est approprié.
240+
241+---
242+
243+## Exemples
244+
245+### Exemple 1 : Workflow de correction de bug
246+
247+```
248+Vous : Corrige ces problèmes dans le module d'authentification :
249+ 1. Vérifier les exceptions de pointeur nul
250+ 2. Ajouter la validation des entrées
251+ 3. Écrire des tests unitaires
252+ 4. Mettre à jour la documentation
253+
254+Agent : Je vais m'en occuper en 4 étapes. En commençant par : Vérifier les exceptions de pointeur nul
255+📋 plan: create user-request-1
256+
257+... (l'agent vérifie le code) ...
258+
259+Agent : 3 problèmes de pointeur nul potentiels trouvés.
260+📋 [2/4] 🔄 Tâche 2 : Ajouter la validation des entrées (in_progress)
261+
262+... (l'agent ajoute la validation) ...
263+
264+Agent : Validation des entrées ajoutée à auth.go.
265+📋 [3/4] 🔄 Tâche 3 : Écrire des tests unitaires (in_progress)
266+
267+... (l'agent écrit les tests) ...
268+
269+Agent : Tests écrits et réussis.
270+📋 [4/4] 🔄 Tâche 4 : Mettre à jour la documentation (in_progress)
271+
272+... (l'agent met à jour les docs) ...
273+
274+Agent : Documentation mise à jour avec les nouvelles exigences de validation.
275+📋 [4/4] ✅ Toutes les tâches sont terminées !
276+```
277+
278+---
279+
280+### Exemple 2 : Configuration d'un projet
281+
282+```
283+Vous : Configure un nouveau projet Go avec :
284+ 1. Initialisation du module
285+ 2. Structure des répertoires
286+ 3. Fichier main
287+ 4. Fichier de test
288+
289+Agent : Je vais configurer cela en 4 étapes. En commençant par : Initialisation du module
290+📋 plan: create user-request-1
291+
292+Agent : Module initialisé avec go mod
293+📋 [2/4] 🔄 Tâche 2 : Structure des répertoires (in_progress)
294+
295+Agent : Répertoires créés (cmd, internal, pkg, tests)
296+📋 [3/4] 🔄 Tâche 3 : Fichier main (in_progress)
297+
298+Agent : Fichier main créé avec la structure de base
299+📋 [4/4] 🔄 Tâche 4 : Fichier de test (in_progress)
300+
301+Agent : Fichier de test créé avec des tests exemples
302+📋 [4/4] ✅ Toutes les tâches sont terminées !
303+```
304+
305+---
306+
307+### Exemple 3 : Revue de code
308+
309+```
310+Vous : Revoir cette PR et :
311+ 1. Vérifier les problèmes de sécurité
312+ 2. Vérifier que les tests passent
313+ 3. Revoir la documentation
314+ 4. Laisser des commentaires
315+
316+Agent : Je vais revoir cette PR en 4 étapes. En commençant par : Vérifier les problèmes de sécurité
317+📋 plan: create user-request-1
318+
319+Agent : Aucun problème de sécurité trouvé.
320+📋 [2/4] 🔄 Tâche 2 : Vérifier que les tests passent (in_progress)
321+
322+Agent : Tous les tests passent.
323+📋 [3/4] 🔄 Tâche 3 : Revoir la documentation (in_progress)
324+
325+Agent : La documentation a l'air bonne, il faut juste corriger une faute de frappe.
326+📋 [4/4] 🔄 Tâche 4 : Laisser des commentaires (in_progress)
327+
328+Agent : Commentaires laissés sur la PR.
329+📋 [4/4] ✅ Toutes les tâches sont terminées !
330+```
331+
332+---
333+
334+### Exemple 4 : Avec un échec de tâche
335+
336+```
337+Vous : Fais ceci :
338+ 1. Se connecter à la base de données
339+ 2. Exécuter la migration
340+ 3. Importer les données
341+
342+Agent : Je vais m'en occuper en 3 étapes. En commençant par : Se connecter à la base de données
343+📋 plan: create user-request-1
344+
345+Agent : Connexion à la base de données établie.
346+📋 [2/3] 🔄 Tâche 2 : Exécuter la migration (in_progress)
347+
348+Agent : Migration terminée.
349+📋 [3/3] 🔄 Tâche 3 : Importer les données (in_progress)
350+
351+Agent : Erreur : timeout de connexion à la source de données
352+📋 plan: update user-request-1
353+
354+Agent : ⚠️ La tâche 3 a échoué : timeout de connexion à la source de données
355+📋 [3/3] ❌ Tâche 3 : Importer les données (failed: connection timeout)
356+
357+Agent : 2 des 3 tâches sont terminées. L'import des données a échoué en raison d'un timeout de connexion. Voulez-vous que je réessaye, ou que je saute cette tâche ?
358+```
359+
360+---
361+
362+## Configuration
363+
364+L'outil plan peut être activé ou désactivé via le fichier de configuration YAML :
365+
366+```yaml
367+# Dans votre fichier agent.yaml
368+planTool: true # Activé par défaut
369+```
370+
371+Pour désactiver :
372+```yaml
373+planTool: false
374+```
375+
376+Lorsque désactivé, l'agent n'aura pas accès à l'outil `plan` et ne créera pas automatiquement de plans.
377+
378+---
379+
380+## Résolution des problèmes
381+
382+### L'agent ne crée pas de plans
383+
384+Si l'agent ne crée pas de plans pour vos demandes multi-étapes :
385+
386+1. **Rendez votre demande plus claire** - Utilisez des listes numérotées
387+ - ❌ "Corrige quelques choses"
388+ - ✅ "Fais ceci : 1. Corrige A, 2. Corrige B, 3. Corrige C"
389+
390+2. **Soyez plus explicite** - Utilisez des mots comme "plan", "étapes", "puis"
391+ - ✅ "Mon plan est de d'abord faire X, puis faire Y"
392+
393+3. **Vérifiez que la compétence est chargée** - L'agent devrait automatiquement charger la compétence `plan-manager` lorsqu'il détecte une demande de type plan
394+
395+### Les plans disparaissent
396+
397+En v1, les plans sont stockés en mémoire et seront perdus lorsque :
398+- Vous démarrez une nouvelle session avec `/new`
399+- L'agent redémarre
400+- Vous fermez et rouvrez le terminal
401+
402+C'est un comportement attendu en v1. La persistance des sessions arrive en v2.
403+
404+---
405+
406+## Bonnes pratiques
407+
408+1. **Soyez spécifique** - Plus il y a de détails dans vos descriptions de tâches, mieux l'agent peut les exécuter
409+
410+2. **Gardez les plans concentrés** - Un plan par demande logique. Ne mélangez pas des tâches sans rapport
411+
412+3. **Vérifiez la progression** - La ligne d'état vous montre exactement où se trouve l'agent
413+
414+4. **Demandez des clarifications** - Si l'agent semble bloqué ou confus, demandez "Quel est l'état ?"
415+
416+5. **Commencez petit** - Pour un travail multi-étapes complexe, commencez par quelques tâches et ajoutez-en davantage si nécessaire
417+
418+---
419+
420+## Comparaison avec d'autres outils
421+
422+| Fonctionnalité | outil plan | compétences | commandes slash |
423+|---------------|------------|-------------|-----------------|
424+| Objectif | Suivre le travail multi-étapes | Charger des procédures | Commandes utilisateur |
425+| Déclenchement | Automatique (modèle détecte) | Explicite (`/skill`) | Explicite (`/command`) |
426+| Persistance (v1) | En mémoire, session uniquement | Basé sur des fichiers | N/A |
427+| Interaction utilisateur | Indirecte (via le modèle) | Indirecte (modèle charge) | Directe |
428+
429+L'outil `plan` complète les autres fonctionnalités de mini-me :
430+- Utilisez les **compétences** pour des procédures complexes et réutilisables
431+- Utilisez l'**outil plan** pour des demandes multi-étapes ad-hoc
432+- Utilisez les **commandes slash** pour des actions utilisateur directes
433+
434+---
435+
436+## Voir aussi
437+
438+- [Référence des commandes slash](../reference/slash-commands.md) - Pour les commandes utilisateur
439+- [Référence des outils](../reference/tools.md) - Pour tous les outils intégrés
440+- [Aperçu des compétences](../explanation/skills.md) - Pour le système de compétences
new file mode 100644
@@ -0,0 +1,440 @@
1+---
2+title: Gérer les plans et les tâches
3+---
4+
5+# Gérer les plans et les tâches
6+
7+Mini-me inclut un outil intégré **plan** qui permet à l'agent de créer, suivre et gérer des listes de tâches multi-étapes. Lorsque vous donnez à l'agent une liste de tâches à effectuer, il crée automatiquement un plan et suit la progression, en vous montrant l'état actuel après chaque réponse.
8+
9+## Exemple rapide
10+
11+```
12+Vous : Fais ces 3 choses : 1. Créer un fichier README, 2. Ajouter une licence, 3. Valider les modifications
13+
14+Agent : Je vais m'en occuper en 3 étapes. En commençant par : Créer un fichier README
15+📋 plan: create user-request-1
16+
17+Agent : Fichier README créé.
18+📋 [2/3] 🔄 Tâche 2 : Ajouter une licence (in_progress)
19+
20+Agent : Licence ajoutée.
21+📋 [3/3] 🔄 Tâche 3 : Valider les modifications (in_progress)
22+
23+Agent : Toutes les modifications ont été validées.
24+📋 [3/3] ✅ Toutes les tâches sont terminées !
25+```
26+
27+La ligne `📋` vous montre exactement où se trouve l'agent dans votre plan.
28+
29+---
30+
31+## Fonctionnement
32+
33+### Cycle de vie d'un plan
34+
35+1. **Vous formulez votre demande** sous forme de liste de tâches
36+2. **L'agent détecte le plan** et le crée en utilisant l'outil `plan`
37+3. **L'agent travaille sur les tâches** une par une, en mettant à jour l'état
38+4. **L'agent signale la progression** après chaque action
39+5. **Le plan est terminé** lorsque toutes les tâches sont terminées
40+
41+### Icônes d'état
42+
43+| Icône | Signification |
44+|-------|--------------|
45+| ⏳ | Tâche non démarrée (ouverte) |
46+| 🔄 | Tâche en cours |
47+| ✅ | Tâche terminée avec succès |
48+| ❌ | Tâche en échec |
49+
50+### Format de la ligne d'état
51+
52+```
53+📋 [courante/total] icône Tâche N : description (état)
54+```
55+
56+Exemples :
57+- `📋 [1/5] ⏳ Tâche 1 : Analyser le code (open)` - Première tâche, non démarrée
58+- `📋 [2/5] 🔄 Tâche 2 : Corriger les bugs (in_progress)` - Deuxième tâche, en cours
59+- `📋 [5/5] ✅ Toutes les tâches sont terminées !` - Tout est terminé !
60+- `📋 [3/5] ❌ Tâche 3 : Exécuter les tests (failed: timeout de connexion)` - Tâche en échec avec erreur
61+
62+---
63+
64+## Scénarios utilisateur
65+
66+### Scénario 1 : Liste numérotée
67+
68+**Votre entrée :**
69+```
70+Fais ces 5 choses :
71+1. Vérifier la version actuelle
72+2. Mettre à jour les dépendances
73+3. Exécuter les tests
74+4. Corriger les échecs
75+5. Déployer en staging
76+```
77+
78+**Ce qui se passe :**
79+- L'agent crée un plan avec 5 tâches
80+- Travaille sur chaque tâche séquentiellement
81+- Affiche la progression : `📋 [1/5]`, `📋 [2/5]`, etc.
82+- Termine avec : `📋 [5/5] ✅ Toutes les tâches sont terminées !`
83+
84+---
85+
86+### Scénario 2 : Tâches séparées par des virgules
87+
88+**Votre entrée :**
89+```
90+Configurer le projet : créer le répertoire, initialiser git, et ajouter un README
91+```
92+
93+**Ce qui se passe :**
94+- L'agent détecte qu'il s'agit d'une demande multi-étapes
95+- Crée un plan avec 3 tâches
96+- Exécute dans l'ordre
97+
98+---
99+
100+### Scénario 3 : Plusieurs plans
101+
102+Vous pouvez avoir plusieurs plans actifs dans une seule session.
103+
104+**Votre entrée :**
105+```
106+D'abord, corrige le module d'authentification : 1. Trouver le bug, 2. Écrire la correction, 3. Le tester
107+```
108+
109+L'agent travaille sur le plan d'authentification, affiche : `📋 [2/3] 🔄 Tâche 2 : Écrire la correction (in_progress)`
110+
111+**Ensuite vous dites :**
112+```
113+Aussi, mettre à jour la documentation : 1. Mettre à jour le README, 2. Mettre à jour le CHANGELOG
114+```
115+
116+L'agent crée un deuxième plan et gère les deux indépendamment.
117+
118+**Pour voir tous les plans actifs :**
119+```
120+Quels plans ai-je ?
121+```
122+
123+---
124+
125+## Interagir avec les plans
126+
127+### Vérifier l'état actuel du plan
128+
129+**Demandez :** "Quel est l'état de mon plan ?"
130+
131+**L'agent répond :**
132+```
133+Plan actuel : [2/3] 🔄 Tâche 2 : Écrire la correction (in_progress). Suivant : Le tester
134+```
135+
136+### Sauter une tâche
137+
138+**Demandez :** "Saute l'étape de test, déploie directement"
139+
140+**Agent :**
141+- Marque la tâche actuelle comme terminée
142+- Saute à la tâche suivante
143+- Confirme : "Je saute l'étape de test. Déploiement en staging..."
144+
145+### Abandonner un plan
146+
147+**Demandez :** "Peu importe, je n'en ai plus besoin"
148+
149+**Agent :**
150+- Demande confirmation
151+- Si vous confirmez, supprime le plan
152+- Toute la progression est perdue
153+
154+---
155+
156+## Commandes de gestion des plans
157+
158+Bien que l'outil plan soit principalement piloté par le modèle (l'agent l'utilise automatiquement), vous pouvez également interagir directement avec les plans :
159+
160+### Lister tous les plans
161+
162+**Commande :** `/plans` (pas encore implémenté en v1 - arrive en v2)
163+
164+**Ce que cela affiche :**
165+```
166+Plans actifs :
167+1. configuration-projet (2/5 tâches terminées)
168+2. corriger-bugs (0/3 tâches terminées)
169+```
170+
171+### Voir les détails d'un plan
172+
173+**Demandez à l'agent :** "Montre-moi les détails du plan X"
174+
175+**L'agent répond :** Liste complète des tâches avec l'état pour chaque tâche
176+
177+### Supprimer un plan
178+
179+**Demandez à l'agent :** "Supprime le plan X"
180+
181+**Agent :** Confirme et supprime le plan
182+
183+---
184+
185+## Cycle de vie de la session
186+
187+### Plans et commande /new
188+
189+Lorsque vous démarrez une nouvelle session avec `/new` :
190+- Tous les plans actifs sont **supprimés**
191+- C'est intentionnel : une nouvelle session signifie un nouveau départ
192+- Si vous voulez continuer un ancien plan, ne pas utiliser `/new`
193+
194+### Plans et redémarrage de l'agent
195+
196+En v1 (version actuelle) :
197+- Les plans sont stockés **uniquement en mémoire**
198+- Les plans sont **perdus** lorsque l'agent redémarre
199+- Cela garde la v1 simple et rapide
200+
201+En v2 (future) :
202+- Les plans seront persistés dans les fichiers de session
203+- Les plans survivront aux redémarrages de l'agent
204+
205+---
206+
207+## L'outil plan (pour les utilisateurs avancés)
208+
209+L'outil `plan` est disponible pour que le modèle puisse l'appeler. Il prend en charge ces actions :
210+
211+| Action | Objectif | Paramètres requis |
212+|--------|----------|-------------------|
213+| `create` | Créer un nouveau plan | `tasks` (tableau de `{description}`) |
214+| `get` | Obtenir les détails d'un plan | `name` |
215+| `update` | Mettre à jour l'état d'une tâche | `name`, `taskId`, `status` |
216+| `next` | Passer à la tâche suivante | `name` |
217+| `list` | Lister tous les plans | aucun |
218+| `delete` | Supprimer un plan | `name` |
219+| `status` | Obtenir la ligne d'état | `name` |
220+
221+### Valeurs d'état
222+
223+- `open` - Tâche non démarrée
224+- `in_progress` - Tâche actuellement en cours
225+- `done` - Tâche terminée avec succès
226+- `failed` - Tâche en échec
227+
228+---
229+
230+## La compétence plan-manager
231+
232+L'agent utilise la compétence `plan-manager` pour savoir **quand et comment** utiliser l'outil plan. Cette compétence inclut :
233+
234+- **Modèles de détection** : Comment reconnaître quand un utilisateur donne une demande multi-étapes
235+- **Directives d'utilisation** : Quand créer des plans, mettre à jour des tâches, passer entre les tâches
236+- **Format d'affichage** : Comment montrer l'état du plan à l'utilisateur
237+- **Gestion des erreurs** : Que faire quand les choses tournent mal
238+
239+Vous n'avez pas besoin de charger manuellement cette compétence - l'agent le fait automatiquement lorsque cela est approprié.
240+
241+---
242+
243+## Exemples
244+
245+### Exemple 1 : Workflow de correction de bug
246+
247+```
248+Vous : Corrige ces problèmes dans le module d'authentification :
249+ 1. Vérifier les exceptions de pointeur nul
250+ 2. Ajouter la validation des entrées
251+ 3. Écrire des tests unitaires
252+ 4. Mettre à jour la documentation
253+
254+Agent : Je vais m'en occuper en 4 étapes. En commençant par : Vérifier les exceptions de pointeur nul
255+📋 plan: create user-request-1
256+
257+... (l'agent vérifie le code) ...
258+
259+Agent : 3 problèmes de pointeur nul potentiels trouvés.
260+📋 [2/4] 🔄 Tâche 2 : Ajouter la validation des entrées (in_progress)
261+
262+... (l'agent ajoute la validation) ...
263+
264+Agent : Validation des entrées ajoutée à auth.go.
265+📋 [3/4] 🔄 Tâche 3 : Écrire des tests unitaires (in_progress)
266+
267+... (l'agent écrit les tests) ...
268+
269+Agent : Tests écrits et réussis.
270+📋 [4/4] 🔄 Tâche 4 : Mettre à jour la documentation (in_progress)
271+
272+... (l'agent met à jour les docs) ...
273+
274+Agent : Documentation mise à jour avec les nouvelles exigences de validation.
275+📋 [4/4] ✅ Toutes les tâches sont terminées !
276+```
277+
278+---
279+
280+### Exemple 2 : Configuration d'un projet
281+
282+```
283+Vous : Configure un nouveau projet Go avec :
284+ 1. Initialisation du module
285+ 2. Structure des répertoires
286+ 3. Fichier main
287+ 4. Fichier de test
288+
289+Agent : Je vais configurer cela en 4 étapes. En commençant par : Initialisation du module
290+📋 plan: create user-request-1
291+
292+Agent : Module initialisé avec go mod
293+📋 [2/4] 🔄 Tâche 2 : Structure des répertoires (in_progress)
294+
295+Agent : Répertoires créés (cmd, internal, pkg, tests)
296+📋 [3/4] 🔄 Tâche 3 : Fichier main (in_progress)
297+
298+Agent : Fichier main créé avec la structure de base
299+📋 [4/4] 🔄 Tâche 4 : Fichier de test (in_progress)
300+
301+Agent : Fichier de test créé avec des tests exemples
302+📋 [4/4] ✅ Toutes les tâches sont terminées !
303+```
304+
305+---
306+
307+### Exemple 3 : Revue de code
308+
309+```
310+Vous : Revoir cette PR et :
311+ 1. Vérifier les problèmes de sécurité
312+ 2. Vérifier que les tests passent
313+ 3. Revoir la documentation
314+ 4. Laisser des commentaires
315+
316+Agent : Je vais revoir cette PR en 4 étapes. En commençant par : Vérifier les problèmes de sécurité
317+📋 plan: create user-request-1
318+
319+Agent : Aucun problème de sécurité trouvé.
320+📋 [2/4] 🔄 Tâche 2 : Vérifier que les tests passent (in_progress)
321+
322+Agent : Tous les tests passent.
323+📋 [3/4] 🔄 Tâche 3 : Revoir la documentation (in_progress)
324+
325+Agent : La documentation a l'air bonne, il faut juste corriger une faute de frappe.
326+📋 [4/4] 🔄 Tâche 4 : Laisser des commentaires (in_progress)
327+
328+Agent : Commentaires laissés sur la PR.
329+📋 [4/4] ✅ Toutes les tâches sont terminées !
330+```
331+
332+---
333+
334+### Exemple 4 : Avec un échec de tâche
335+
336+```
337+Vous : Fais ceci :
338+ 1. Se connecter à la base de données
339+ 2. Exécuter la migration
340+ 3. Importer les données
341+
342+Agent : Je vais m'en occuper en 3 étapes. En commençant par : Se connecter à la base de données
343+📋 plan: create user-request-1
344+
345+Agent : Connexion à la base de données établie.
346+📋 [2/3] 🔄 Tâche 2 : Exécuter la migration (in_progress)
347+
348+Agent : Migration terminée.
349+📋 [3/3] 🔄 Tâche 3 : Importer les données (in_progress)
350+
351+Agent : Erreur : timeout de connexion à la source de données
352+📋 plan: update user-request-1
353+
354+Agent : ⚠️ La tâche 3 a échoué : timeout de connexion à la source de données
355+📋 [3/3] ❌ Tâche 3 : Importer les données (failed: connection timeout)
356+
357+Agent : 2 des 3 tâches sont terminées. L'import des données a échoué en raison d'un timeout de connexion. Voulez-vous que je réessaye, ou que je saute cette tâche ?
358+```
359+
360+---
361+
362+## Configuration
363+
364+L'outil plan peut être activé ou désactivé via le fichier de configuration YAML :
365+
366+```yaml
367+# Dans votre fichier agent.yaml
368+planTool: true # Activé par défaut
369+```
370+
371+Pour désactiver :
372+```yaml
373+planTool: false
374+```
375+
376+Lorsque désactivé, l'agent n'aura pas accès à l'outil `plan` et ne créera pas automatiquement de plans.
377+
378+---
379+
380+## Résolution des problèmes
381+
382+### L'agent ne crée pas de plans
383+
384+Si l'agent ne crée pas de plans pour vos demandes multi-étapes :
385+
386+1. **Rendez votre demande plus claire** - Utilisez des listes numérotées
387+ - ❌ "Corrige quelques choses"
388+ - ✅ "Fais ceci : 1. Corrige A, 2. Corrige B, 3. Corrige C"
389+
390+2. **Soyez plus explicite** - Utilisez des mots comme "plan", "étapes", "puis"
391+ - ✅ "Mon plan est de d'abord faire X, puis faire Y"
392+
393+3. **Vérifiez que la compétence est chargée** - L'agent devrait automatiquement charger la compétence `plan-manager` lorsqu'il détecte une demande de type plan
394+
395+### Les plans disparaissent
396+
397+En v1, les plans sont stockés en mémoire et seront perdus lorsque :
398+- Vous démarrez une nouvelle session avec `/new`
399+- L'agent redémarre
400+- Vous fermez et rouvrez le terminal
401+
402+C'est un comportement attendu en v1. La persistance des sessions arrive en v2.
403+
404+---
405+
406+## Bonnes pratiques
407+
408+1. **Soyez spécifique** - Plus il y a de détails dans vos descriptions de tâches, mieux l'agent peut les exécuter
409+
410+2. **Gardez les plans concentrés** - Un plan par demande logique. Ne mélangez pas des tâches sans rapport
411+
412+3. **Vérifiez la progression** - La ligne d'état vous montre exactement où se trouve l'agent
413+
414+4. **Demandez des clarifications** - Si l'agent semble bloqué ou confus, demandez "Quel est l'état ?"
415+
416+5. **Commencez petit** - Pour un travail multi-étapes complexe, commencez par quelques tâches et ajoutez-en davantage si nécessaire
417+
418+---
419+
420+## Comparaison avec d'autres outils
421+
422+| Fonctionnalité | outil plan | compétences | commandes slash |
423+|---------------|------------|-------------|-----------------|
424+| Objectif | Suivre le travail multi-étapes | Charger des procédures | Commandes utilisateur |
425+| Déclenchement | Automatique (modèle détecte) | Explicite (`/skill`) | Explicite (`/command`) |
426+| Persistance (v1) | En mémoire, session uniquement | Basé sur des fichiers | N/A |
427+| Interaction utilisateur | Indirecte (via le modèle) | Indirecte (modèle charge) | Directe |
428+
429+L'outil `plan` complète les autres fonctionnalités de mini-me :
430+- Utilisez les **compétences** pour des procédures complexes et réutilisables
431+- Utilisez l'**outil plan** pour des demandes multi-étapes ad-hoc
432+- Utilisez les **commandes slash** pour des actions utilisateur directes
433+
434+---
435+
436+## Voir aussi
437+
438+- [Référence des commandes slash](../reference/slash-commands.md) - Pour les commandes utilisateur
439+- [Référence des outils](../reference/tools.md) - Pour tous les outils intégrés
440+- [Aperçu des compétences](../explanation/skills.md) - Pour le système de compétences
modified internal/agent/agent.go +3 -0
@@ -21,6 +21,7 @@ import (
2121 "mm/internal/skills"
2222 "mm/internal/spinner"
2323 "mm/internal/store"
24+ "mm/internal/tools"
2425
2526 "github.com/firebase/genkit/go/ai"
2627 )
@@ -443,6 +444,8 @@ func startNewSession(e *engine.Engine, d *detector.LoopDetector, system string,
443444 forgotten := session.Forgotten(old)
444445 e.ForgetInputTokens()
445446 d.Reset()
447+ // Clear any active plans when starting a new session
448+ tools.ClearAllPlans()
446449 dimln(fmt.Sprintf("🆕 new session: %d message(s) forgotten", forgotten))
447450 return session.Fresh(system)
448451 }
@@ -21,6 +21,7 @@ import (
21 "mm/internal/skills"21 "mm/internal/skills"
22 "mm/internal/spinner"22 "mm/internal/spinner"
23 "mm/internal/store"23 "mm/internal/store"
24+ "mm/internal/tools"
24 25
25 "github.com/firebase/genkit/go/ai"26 "github.com/firebase/genkit/go/ai"
26 )27 )
@@ -443,6 +444,8 @@ func startNewSession(e *engine.Engine, d *detector.LoopDetector, system string,
443 forgotten := session.Forgotten(old)444 forgotten := session.Forgotten(old)
444 e.ForgetInputTokens()445 e.ForgetInputTokens()
445 d.Reset()446 d.Reset()
447+ // Clear any active plans when starting a new session
448+ tools.ClearAllPlans()
446 dimln(fmt.Sprintf("🆕 new session: %d message(s) forgotten", forgotten))449 dimln(fmt.Sprintf("🆕 new session: %d message(s) forgotten", forgotten))
447 return session.Fresh(system)450 return session.Fresh(system)
448 }451 }
modified internal/config/config.go +5 -0
@@ -96,6 +96,10 @@ type Config struct {
9696 // One binary, two set-ups: that is what lets the two be compared.
9797 EditTools bool `yaml:"editTools"`
9898
99+ // PlanTool: declare the built-in plan tool for managing multi-step plans.
100+ // On by default. Off to disable plan creation and tracking via the `plan` tool.
101+ PlanTool bool `yaml:"planTool"`
102+
99103 // Sampling: generation settings (OpenAI API keys; the plugin converts them.)
100104 Sampling map[string]any `yaml:"sampling"`
101105
@@ -251,6 +255,7 @@ var Cfg = Config{
251255 SkillsDir: "skills",
252256 BashTool: true,
253257 EditTools: true,
258+ PlanTool: true,
254259 System: `You are a coding agent working in a terminal.
255260 You have a "bash" tool to run shell commands.
256261 Use it to explore files, run tests, inspect the repository, etc.
@@ -96,6 +96,10 @@ type Config struct {
96 // One binary, two set-ups: that is what lets the two be compared.96 // One binary, two set-ups: that is what lets the two be compared.
97 EditTools bool `yaml:"editTools"`97 EditTools bool `yaml:"editTools"`
98 98
99+ // PlanTool: declare the built-in plan tool for managing multi-step plans.
100+ // On by default. Off to disable plan creation and tracking via the `plan` tool.
101+ PlanTool bool `yaml:"planTool"`
102+
99 // Sampling: generation settings (OpenAI API keys; the plugin converts them.)103 // Sampling: generation settings (OpenAI API keys; the plugin converts them.)
100 Sampling map[string]any `yaml:"sampling"`104 Sampling map[string]any `yaml:"sampling"`
101 105
@@ -251,6 +255,7 @@ var Cfg = Config{
251 SkillsDir: "skills",255 SkillsDir: "skills",
252 BashTool: true,256 BashTool: true,
253 EditTools: true,257 EditTools: true,
258+ PlanTool: true,
254 System: `You are a coding agent working in a terminal.259 System: `You are a coding agent working in a terminal.
255 You have a "bash" tool to run shell commands.260 You have a "bash" tool to run shell commands.
256 Use it to explore files, run tests, inspect the repository, etc.261 Use it to explore files, run tests, inspect the repository, etc.
added internal/tools/plan.go +694 -0
new file mode 100644
@@ -0,0 +1,694 @@
1+package tools
2+
3+import (
4+ "context"
5+ "encoding/json"
6+ "fmt"
7+ "sort"
8+ "strings"
9+ "sync"
10+ "time"
11+
12+ "mm/internal/detector"
13+ "mm/internal/spinner"
14+ "mm/internal/ui"
15+
16+ "github.com/firebase/genkit/go/ai"
17+ "github.com/firebase/genkit/go/genkit"
18+)
19+
20+// --- plan --------------------------------------------------------------------
21+//
22+// The plan tool lets the model create and manage multi-step plans as todo lists.
23+// When the user gives the model a numbered list of tasks ("Do: 1. X, 2. Y, 3. Z"),
24+// the model creates a plan via this tool, then tracks progress through the tasks.
25+//
26+// Storage: in-memory, per-session. Plans are lost when the agent restarts or when
27+// a new session begins (/new command). This keeps v1 simple; persistence to session
28+// files is a v2 enhancement.
29+//
30+// Failures are returned AS TEXT (never as Go errors), so the model can read them
31+// and react. Every call prints one scannable line before running and echoes its
32+// result to the user before the model sees it.
33+
34+// Plan represents a named plan with its tasks.
35+type Plan struct {
36+ Name string `json:"name"`
37+ Tasks []PlanTask `json:"tasks"`
38+ Current int `json:"current"` // 0 = not started, 1 = first task, etc.
39+ CreatedAt string `json:"createdAt"` // ISO timestamp
40+}
41+
42+// PlanTask represents a single task within a plan.
43+type PlanTask struct {
44+ ID int `json:"id"`
45+ Description string `json:"description"`
46+ Status string `json:"status"` // open, in_progress, done, failed
47+ StartedAt string `json:"startedAt,omitempty"`
48+ CompletedAt string `json:"completedAt,omitempty"`
49+ Notes string `json:"notes,omitempty"`
50+}
51+
52+// planInput is the input schema for the plan tool.
53+type planInput struct {
54+ // Action is required: create, get, update, next, list, delete, status
55+ Action string `json:"action" jsonschema_description:"Required. One of: create, get, update, next, list, delete, status"`
56+ // Name is the plan identifier (default: 'default' for unnamed plans)
57+ Name string `json:"name,omitempty" jsonschema_description:"Plan name. Defaults to 'user-request' if not provided."`
58+ // Tasks is used with 'create' action to define the plan's tasks
59+ Tasks []PlanTaskInput `json:"tasks,omitempty" jsonschema_description:"For create action: list of tasks to add"`
60+ // TaskID identifies which task for update/next actions (1-based)
61+ TaskID int `json:"taskId,omitempty" jsonschema_description:"Task ID (1-based) for update/delete/get actions"`
62+ // Status is used with 'update' action: open, in_progress, done, failed
63+ Status string `json:"status,omitempty" jsonschema_description:"For update action: one of open, in_progress, done, failed"`
64+ // Message is an optional note (e.g., error message for failed status)
65+ Message string `json:"message,omitempty" jsonschema_description:"Optional note for status updates"`
66+}
67+
68+// PlanTaskInput is the input format for task creation.
69+type PlanTaskInput struct {
70+ ID int `json:"id,omitempty"`
71+ Description string `json:"description"`
72+}
73+
74+// PlanOutput is the output format for plan operations.
75+type PlanOutput struct {
76+ Result string `json:"result"`
77+ PlanName string `json:"planName,omitempty"`
78+ Plan *Plan `json:"plan,omitempty"`
79+ Task *PlanTask `json:"task,omitempty"`
80+ Status string `json:"status,omitempty"`
81+ Error string `json:"error,omitempty"`
82+ Current int `json:"current,omitempty"`
83+ Total int `json:"total,omitempty"`
84+}
85+
86+// PlanStore holds plans in memory, keyed by session ID and plan name.
87+// This is a package-level variable so plans persist across turns within
88+// a single session. Plans are cleared when a new session starts.
89+type PlanStore struct {
90+ mu sync.RWMutex
91+ plans map[string]map[string]*Plan // sessionID -> planName -> Plan
92+ nextID map[string]int // sessionID -> next task ID
93+}
94+
95+var planStore = &PlanStore{
96+ plans: make(map[string]map[string]*Plan),
97+ nextID: make(map[string]int),
98+}
99+
100+// getSessionID extracts a session identifier from the tool context.
101+// For simplicity in v1, we use a placeholder since the actual session ID
102+// isn't easily accessible from tool context. In practice, plans are cleared
103+// on /new command via ClearPlans().
104+func getSessionID() string {
105+ // v1: use a single session for simplicity
106+ // v2: extract from context when available
107+ return "current-session"
108+}
109+
110+// newPlanName generates a unique plan name if none is provided.
111+// Must be called with planStore.mu held.
112+func newPlanName(sessionID string, plans map[string]map[string]*Plan) string {
113+ count := len(plans[sessionID])
114+ return fmt.Sprintf("user-request-%d", count+1)
115+}
116+
117+// PlanStore methods
118+
119+// CreatePlan creates a new plan with the given tasks.
120+func (s *PlanStore) CreatePlan(name string, tasks []PlanTaskInput) (*Plan, string, error) {
121+ sessionID := getSessionID()
122+ s.mu.Lock()
123+ defer s.mu.Unlock()
124+
125+ if name == "" {
126+ name = newPlanName(sessionID, s.plans)
127+ }
128+
129+ // Check if plan already exists
130+ if plans, ok := s.plans[sessionID]; ok {
131+ if _, exists := plans[name]; exists {
132+ return nil, "", fmt.Errorf("plan '%s' already exists", name)
133+ }
134+ }
135+
136+ // Ensure we have a session entry
137+ if _, ok := s.plans[sessionID]; !ok {
138+ s.plans[sessionID] = make(map[string]*Plan)
139+ }
140+
141+ // Convert input tasks to PlanTasks
142+ planTasks := make([]PlanTask, len(tasks))
143+ for i, t := range tasks {
144+ planTasks[i] = PlanTask{
145+ ID: t.ID,
146+ Description: t.Description,
147+ Status: "open",
148+ }
149+ // Auto-assign IDs if not provided
150+ if t.ID == 0 {
151+ planTasks[i].ID = i + 1
152+ }
153+ }
154+
155+ plan := &Plan{
156+ Name: name,
157+ Tasks: planTasks,
158+ Current: 0,
159+ CreatedAt: time.Now().UTC().Format(time.RFC3339),
160+ }
161+
162+ s.plans[sessionID][name] = plan
163+
164+ return plan, fmt.Sprintf("Plan '%s' created with %d tasks. Current: Task %d", name, len(tasks), 1), nil
165+}
166+
167+// GetPlan retrieves a plan by name.
168+func (s *PlanStore) GetPlan(name string) (*Plan, string, error) {
169+ sessionID := getSessionID()
170+ s.mu.RLock()
171+ defer s.mu.RUnlock()
172+
173+ plans, ok := s.plans[sessionID]
174+ if !ok {
175+ return nil, "", fmt.Errorf("no plans found for this session")
176+ }
177+
178+ plan, exists := plans[name]
179+ if !exists {
180+ return nil, "", fmt.Errorf("no plan named '%s' found", name)
181+ }
182+
183+ return plan, fmt.Sprintf("Retrieved plan '%s' with %d tasks", name, len(plan.Tasks)), nil
184+}
185+
186+// ListPlans returns all plans for the current session.
187+func (s *PlanStore) ListPlans() ([]*Plan, string, error) {
188+ sessionID := getSessionID()
189+ s.mu.RLock()
190+ defer s.mu.RUnlock()
191+
192+ plans, ok := s.plans[sessionID]
193+ if !ok || len(plans) == 0 {
194+ return nil, "No active plans", nil
195+ }
196+
197+ // Sort plans by creation time (approximate via name for v1)
198+ var planList []*Plan
199+ for _, p := range plans {
200+ planList = append(planList, p)
201+ }
202+ sort.Slice(planList, func(i, j int) bool {
203+ return planList[i].CreatedAt < planList[j].CreatedAt
204+ })
205+
206+ return planList, fmt.Sprintf("Found %d plan(s)", len(planList)), nil
207+}
208+
209+// UpdateTask updates a task's status in a plan.
210+func (s *PlanStore) UpdateTask(name string, taskID int, status string, message string) (*Plan, string, error) {
211+ sessionID := getSessionID()
212+ s.mu.Lock()
213+ defer s.mu.Unlock()
214+
215+ plans, ok := s.plans[sessionID]
216+ if !ok {
217+ return nil, "", fmt.Errorf("no plans found for this session")
218+ }
219+
220+ plan, exists := plans[name]
221+ if !exists {
222+ return nil, "", fmt.Errorf("no plan named '%s' found", name)
223+ }
224+
225+ // Validate status
226+ validStatuses := map[string]bool{"open": true, "in_progress": true, "done": true, "failed": true}
227+ if !validStatuses[status] {
228+ return nil, "", fmt.Errorf("invalid status '%s'. Must be one of: open, in_progress, done, failed", status)
229+ }
230+
231+ // Find task by ID (1-based)
232+ taskIndex := -1
233+ for i, t := range plan.Tasks {
234+ if t.ID == taskID {
235+ taskIndex = i
236+ break
237+ }
238+ }
239+
240+ if taskIndex == -1 {
241+ return nil, "", fmt.Errorf("no task with id %d in plan '%s'", taskID, name)
242+ }
243+
244+ // Update task
245+ task := &plan.Tasks[taskIndex]
246+ task.Status = status
247+ task.Notes = message
248+
249+ // Set timestamps
250+ now := time.Now().UTC().Format(time.RFC3339)
251+ if status == "in_progress" && task.StartedAt == "" {
252+ task.StartedAt = now
253+ }
254+ if status == "done" || status == "failed" {
255+ task.CompletedAt = now
256+ }
257+
258+ return plan, fmt.Sprintf("Task %d in plan '%s' updated to '%s'", taskID, name, status), nil
259+}
260+
261+// NextTask advances to the next task in a plan, marking current as done.
262+func (s *PlanStore) NextTask(name string) (*Plan, *PlanTask, string, error) {
263+ sessionID := getSessionID()
264+ s.mu.Lock()
265+ defer s.mu.Unlock()
266+
267+ plans, ok := s.plans[sessionID]
268+ if !ok {
269+ return nil, nil, "", fmt.Errorf("no plans found for this session")
270+ }
271+
272+ plan, exists := plans[name]
273+ if !exists {
274+ return nil, nil, "", fmt.Errorf("no plan named '%s' found", name)
275+ }
276+
277+ // If plan is complete
278+ if plan.Current > len(plan.Tasks) {
279+ return plan, nil, fmt.Sprintf("Plan '%s' is complete! All %d tasks done.", name, len(plan.Tasks)), nil
280+ }
281+
282+ // Mark current task as done (if any)
283+ if plan.Current > 0 && plan.Current <= len(plan.Tasks) {
284+ plan.Tasks[plan.Current-1].Status = "done"
285+ plan.Tasks[plan.Current-1].CompletedAt = time.Now().UTC().Format(time.RFC3339)
286+ }
287+
288+ // Move to next
289+ plan.Current++
290+
291+ // Return next task
292+ if plan.Current <= len(plan.Tasks) {
293+ nextTask := &plan.Tasks[plan.Current-1]
294+ // Auto-set to in_progress
295+ nextTask.Status = "in_progress"
296+ nextTask.StartedAt = time.Now().UTC().Format(time.RFC3339)
297+ return plan, nextTask, fmt.Sprintf("Task %d done. Next: Task %d - %s", plan.Current-1, plan.Current, nextTask.Description), nil
298+ }
299+
300+ // Plan is now complete
301+ return plan, nil, fmt.Sprintf("Plan '%s' is complete! All %d tasks done.", name, len(plan.Tasks)), nil
302+}
303+
304+// DeletePlan removes a plan.
305+func (s *PlanStore) DeletePlan(name string) (string, error) {
306+ sessionID := getSessionID()
307+ s.mu.Lock()
308+ defer s.mu.Unlock()
309+
310+ plans, ok := s.plans[sessionID]
311+ if !ok {
312+ return "", fmt.Errorf("no plans found for this session")
313+ }
314+
315+ if _, exists := plans[name]; !exists {
316+ return "", fmt.Errorf("no plan named '%s' found", name)
317+ }
318+
319+ delete(plans, name)
320+ return fmt.Sprintf("Plan '%s' deleted", name), nil
321+}
322+
323+// GetStatus returns a compact status line for a plan.
324+func (s *PlanStore) GetStatus(name string) (string, string, error) {
325+ sessionID := getSessionID()
326+ s.mu.RLock()
327+ defer s.mu.RUnlock()
328+
329+ plans, ok := s.plans[sessionID]
330+ if !ok {
331+ return "", "", fmt.Errorf("no plans found for this session")
332+ }
333+
334+ plan, exists := plans[name]
335+ if !exists {
336+ return "", "", fmt.Errorf("no plan named '%s' found", name)
337+ }
338+
339+ // Count statuses
340+ doneCount := 0
341+ for _, t := range plan.Tasks {
342+ if t.Status == "done" {
343+ doneCount++
344+ }
345+ }
346+
347+ // Format status line
348+ // Current=0 means not started yet, Task 1 is next
349+ // Current>0 means we've started, Task Current is the next one
350+ if plan.Current == 0 {
351+ // Not started, show Task 1
352+ task := plan.Tasks[0]
353+ statusIcon := "⏳"
354+ if task.Status == "done" {
355+ statusIcon = "✅"
356+ } else if task.Status == "in_progress" {
357+ statusIcon = "🔄"
358+ } else if task.Status == "failed" {
359+ statusIcon = "❌"
360+ }
361+ return fmt.Sprintf("[%d/%d] %s Task %d: %s (%s)", 1, len(plan.Tasks), statusIcon, 1, task.Description, task.Status),
362+ fmt.Sprintf("Plan '%s': %d/%d tasks complete", name, doneCount, len(plan.Tasks)), nil
363+ }
364+
365+ // Current > 0, show Task Current
366+ if plan.Current <= len(plan.Tasks) {
367+ task := plan.Tasks[plan.Current-1]
368+ statusIcon := ""
369+ switch task.Status {
370+ case "done":
371+ statusIcon = "✅"
372+ case "in_progress":
373+ statusIcon = "🔄"
374+ case "failed":
375+ statusIcon = "❌"
376+ default:
377+ statusIcon = "⏳"
378+ }
379+ return fmt.Sprintf("[%d/%d] %s Task %d: %s (%s)", plan.Current, len(plan.Tasks), statusIcon, plan.Current, task.Description, task.Status),
380+ fmt.Sprintf("Plan '%s': %d/%d tasks complete", name, doneCount, len(plan.Tasks)), nil
381+ }
382+
383+ // All tasks complete
384+ return fmt.Sprintf("[%d/%d] All tasks complete!", len(plan.Tasks), len(plan.Tasks)),
385+ fmt.Sprintf("Plan '%s': All %d tasks done ✅", name, len(plan.Tasks)), nil
386+}
387+
388+// ClearPlans removes all plans for the current session.
389+// Called when /new command is issued.
390+func (s *PlanStore) ClearPlans() {
391+ sessionID := getSessionID()
392+ s.mu.Lock()
393+ defer s.mu.Unlock()
394+ delete(s.plans, sessionID)
395+}
396+
397+// ClearAllPlans removes all plans. Used for testing.
398+func (s *PlanStore) ClearAllPlans() {
399+ s.mu.Lock()
400+ defer s.mu.Unlock()
401+ s.plans = make(map[string]map[string]*Plan)
402+}
403+
404+// announcePlan prints the display line for a plan operation.
405+func announcePlan(line string) {
406+ spinner.Suspend(func() {
407+ printMu.Lock()
408+ fmt.Printf("\n📋 %s\n", clip(line))
409+ printMu.Unlock()
410+ })
411+}
412+
413+// PlanTool declares the `plan` tool.
414+func PlanTool(g *genkit.Genkit, d *detector.LoopDetector) ai.ToolRef {
415+ description := `Create and manage multi-step plans. When the user gives you a list of tasks to do (e.g., "Do: 1. X, 2. Y, 3. Z"), use this tool to create a plan and track progress.
416+
417+Actions:
418+- create: Create a new plan with tasks. Required: tasks (array of {description}). Optional: name.
419+- get: Retrieve a plan by name. Required: name.
420+- update: Update a task's status. Required: name, taskId, status (open/in_progress/done/failed). Optional: message.
421+- next: Move to the next task (marks current as done). Required: name.
422+- list: List all plans for this session.
423+- delete: Delete a plan. Required: name.
424+- status: Get a compact status line for display. Required: name.
425+
426+Every action returns a text summary that you should include in your response to the user.
427+
428+Example workflow:
429+1. User says "Do these: 1. Read file, 2. Analyze, 3. Report"
430+2. You call: plan(action="create", tasks=[{description:"Read file"}, {description:"Analyze"}, {description:"Report"}])
431+3. You call: plan(action="update", name="user-request-1", taskId=1, status="in_progress")
432+4. You do the work
433+5. You call: plan(action="update", name="user-request-1", taskId=1, status="done")
434+6. You call: plan(action="next", name="user-request-1")
435+7. Include the status line from plan(action="status") in your response.
436+
437+Always show the user the current plan status using the status action.`
438+
439+ return genkit.DefineTool(g, "plan",
440+ description,
441+ func(tctx *ai.ToolContext, in planInput) (string, error) {
442+ // Event-consuming front end (ACP) uses evented variant
443+ if s := ui.ActiveSink(); s != nil {
444+ return planEvented(tctx, s, d, in)
445+ }
446+
447+ // Terminal/REPL: announce the call
448+ spinner.Suspend(func() {
449+ printMu.Lock()
450+ fmt.Printf("\n📋 plan: %s %s\n", in.Action, in.Name)
451+ printMu.Unlock()
452+ })
453+
454+ spinner.Start("Running")
455+
456+ var result string
457+
458+ switch in.Action {
459+ case "create":
460+ plan, msg, createErr := planStore.CreatePlan(in.Name, in.Tasks)
461+ if createErr != nil {
462+ result = fmt.Sprintf("[error: %v]", createErr)
463+ break
464+ }
465+ // Return plan info as JSON for model, plus text summary
466+ output := PlanOutput{
467+ Result: msg,
468+ PlanName: plan.Name,
469+ Plan: plan,
470+ Current: plan.Current,
471+ Total: len(plan.Tasks),
472+ }
473+ outputBytes, _ := json.Marshal(output)
474+ result = string(outputBytes)
475+
476+ case "get":
477+ plan, msg, getErr := planStore.GetPlan(in.Name)
478+ if getErr != nil {
479+ result = fmt.Sprintf("[error: %v]", getErr)
480+ break
481+ }
482+ output := PlanOutput{
483+ Result: msg,
484+ Plan: plan,
485+ }
486+ outputBytes, _ := json.Marshal(output)
487+ result = string(outputBytes)
488+
489+ case "update":
490+ _, msg, updateErr := planStore.UpdateTask(in.Name, in.TaskID, in.Status, in.Message)
491+ if updateErr != nil {
492+ result = fmt.Sprintf("[error: %v]", updateErr)
493+ break
494+ }
495+ // Get status line
496+ statusLine, _, _ := planStore.GetStatus(in.Name)
497+ result = fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine)
498+
499+ case "next":
500+ _, nextTask, msg, nextErr := planStore.NextTask(in.Name)
501+ if nextErr != nil {
502+ result = fmt.Sprintf("[error: %v]", nextErr)
503+ break
504+ }
505+ // Get status line
506+ statusLine, _, _ := planStore.GetStatus(in.Name)
507+ if nextTask != nil {
508+ result = fmt.Sprintf(`{"result":"%s","status":"%s","nextTask":{"id":%d,"description":"%s"}}`,
509+ msg, statusLine, nextTask.ID, strings.ReplaceAll(nextTask.Description, "\"", "\\\""))
510+ } else {
511+ result = fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine)
512+ }
513+
514+ case "list":
515+ plans, msg, listErr := planStore.ListPlans()
516+ if listErr != nil {
517+ result = fmt.Sprintf("[error: %v]", listErr)
518+ break
519+ }
520+ if len(plans) == 0 {
521+ result = `{"result":"` + msg + `","plans":[]}`
522+ break
523+ }
524+ // Build simple list
525+ var planNames []string
526+ for _, p := range plans {
527+ planNames = append(planNames, p.Name)
528+ }
529+ result = fmt.Sprintf(`{"result":"%s","plans":["%s"]}`, msg, strings.Join(planNames, `", "`))
530+
531+ case "delete":
532+ msg, delErr := planStore.DeletePlan(in.Name)
533+ if delErr != nil {
534+ result = fmt.Sprintf("[error: %v]", delErr)
535+ break
536+ }
537+ result = fmt.Sprintf(`{"result":"%s"}`, msg)
538+
539+ case "status":
540+ statusLine, msg, statusErr := planStore.GetStatus(in.Name)
541+ if statusErr != nil {
542+ result = fmt.Sprintf("[error: %v]", statusErr)
543+ break
544+ }
545+ result = fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine)
546+
547+ default:
548+ result = fmt.Sprintf("[error: unknown action '%s'. Valid actions: create, get, update, next, list, delete, status]", in.Action)
549+ }
550+
551+ // Record in loop detector
552+ inputJSON, _ := json.Marshal(in)
553+ return record(d, "plan", string(inputJSON), result)
554+ })
555+}
556+
557+// planEvented is the plan tool for event-consuming front ends (ACP).
558+func planEvented(ctx context.Context, s ui.Sink, d *detector.LoopDetector, in planInput) (string, error) {
559+ eventKind := "read"
560+ if in.Action == "create" || in.Action == "update" || in.Action == "next" || in.Action == "delete" {
561+ eventKind = "edit"
562+ }
563+
564+ ev := ui.ToolEvent{
565+ ID: ui.NextCallID(),
566+ Tool: "plan",
567+ Title: fmt.Sprintf("plan: %s %s", in.Action, in.Name),
568+ Kind: eventKind,
569+ Input: map[string]any{"action": in.Action, "name": in.Name, "taskId": in.TaskID, "status": in.Status},
570+ }
571+
572+ s.ToolStart(ev)
573+
574+ // For destructive actions, check permission
575+ if eventKind == "edit" {
576+ if !s.Allow(ctx, ev) {
577+ out := "[plan: command rejected by the user]"
578+ s.ToolEnd(ev.ID, ui.ToolResult{Output: out, Failed: true})
579+ inputJSON, _ := json.Marshal(in)
580+ return record(d, "plan", string(inputJSON), out)
581+ }
582+ }
583+
584+ s.ToolRunning(ev.ID)
585+
586+ // Execute the action
587+ var result string
588+
589+ switch in.Action {
590+ case "create":
591+ plan, msg, createErr := planStore.CreatePlan(in.Name, in.Tasks)
592+ if createErr != nil {
593+ result = fmt.Sprintf("[error: %v]", createErr)
594+ } else {
595+ output := PlanOutput{
596+ Result: msg,
597+ PlanName: plan.Name,
598+ Current: plan.Current,
599+ Total: len(plan.Tasks),
600+ }
601+ outputBytes, _ := json.Marshal(output)
602+ result = string(outputBytes)
603+ }
604+
605+ case "get":
606+ plan, msg, getErr := planStore.GetPlan(in.Name)
607+ if getErr != nil {
608+ result = fmt.Sprintf("[error: %v]", getErr)
609+ } else {
610+ output := PlanOutput{
611+ Result: msg,
612+ Plan: plan,
613+ }
614+ outputBytes, _ := json.Marshal(output)
615+ result = string(outputBytes)
616+ }
617+
618+ case "update":
619+ _, msg, updateErr := planStore.UpdateTask(in.Name, in.TaskID, in.Status, in.Message)
620+ if updateErr != nil {
621+ result = fmt.Sprintf("[error: %v]", updateErr)
622+ } else {
623+ statusLine, _, _ := planStore.GetStatus(in.Name)
624+ result = fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine)
625+ }
626+
627+ case "next":
628+ _, nextTask, msg, nextErr := planStore.NextTask(in.Name)
629+ if nextErr != nil {
630+ result = fmt.Sprintf("[error: %v]", nextErr)
631+ } else {
632+ statusLine, _, _ := planStore.GetStatus(in.Name)
633+ if nextTask != nil {
634+ result = fmt.Sprintf(`{"result":"%s","status":"%s","nextTask":{"id":%d,"description":"%s"}}`,
635+ msg, statusLine, nextTask.ID, strings.ReplaceAll(nextTask.Description, "\"", "\\\""))
636+ } else {
637+ result = fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine)
638+ }
639+ }
640+
641+ case "list":
642+ plans, msg, listErr := planStore.ListPlans()
643+ if listErr != nil {
644+ result = fmt.Sprintf("[error: %v]", listErr)
645+ } else if len(plans) == 0 {
646+ result = `{"result":"` + msg + `","plans":[]}`
647+ } else {
648+ var planNames []string
649+ for _, p := range plans {
650+ planNames = append(planNames, p.Name)
651+ }
652+ result = fmt.Sprintf(`{"result":"%s","plans":["%s"]}`, msg, strings.Join(planNames, `", "`))
653+ }
654+
655+ case "delete":
656+ msg, delErr := planStore.DeletePlan(in.Name)
657+ if delErr != nil {
658+ result = fmt.Sprintf("[error: %v]", delErr)
659+ } else {
660+ result = fmt.Sprintf(`{"result":"%s"}`, msg)
661+ }
662+
663+ case "status":
664+ statusLine, msg, statusErr := planStore.GetStatus(in.Name)
665+ if statusErr != nil {
666+ result = fmt.Sprintf("[error: %v]", statusErr)
667+ } else {
668+ result = fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine)
669+ }
670+
671+ default:
672+ result = fmt.Sprintf("[error: unknown action '%s']", in.Action)
673+ }
674+
675+ // Check if failed
676+ failed := strings.HasPrefix(result, "[error:")
677+
678+ s.ToolEnd(ev.ID, ui.ToolResult{Output: result, Failed: failed})
679+
680+ // Record in loop detector
681+ inputJSON, _ := json.Marshal(in)
682+ return record(d, "plan", string(inputJSON), result)
683+}
684+
685+// ClearAllPlans clears all plans for the current session.
686+// Called when a new session starts (/new command).
687+func ClearAllPlans() {
688+ planStore.ClearPlans()
689+}
690+
691+// ClearAllPlansForTesting exposes ClearAllPlans for tests.
692+func ClearAllPlansForTesting() {
693+ planStore.ClearAllPlans()
694+}
new file mode 100644
@@ -0,0 +1,694 @@
1+package tools
2+
3+import (
4+ "context"
5+ "encoding/json"
6+ "fmt"
7+ "sort"
8+ "strings"
9+ "sync"
10+ "time"
11+
12+ "mm/internal/detector"
13+ "mm/internal/spinner"
14+ "mm/internal/ui"
15+
16+ "github.com/firebase/genkit/go/ai"
17+ "github.com/firebase/genkit/go/genkit"
18+)
19+
20+// --- plan --------------------------------------------------------------------
21+//
22+// The plan tool lets the model create and manage multi-step plans as todo lists.
23+// When the user gives the model a numbered list of tasks ("Do: 1. X, 2. Y, 3. Z"),
24+// the model creates a plan via this tool, then tracks progress through the tasks.
25+//
26+// Storage: in-memory, per-session. Plans are lost when the agent restarts or when
27+// a new session begins (/new command). This keeps v1 simple; persistence to session
28+// files is a v2 enhancement.
29+//
30+// Failures are returned AS TEXT (never as Go errors), so the model can read them
31+// and react. Every call prints one scannable line before running and echoes its
32+// result to the user before the model sees it.
33+
34+// Plan represents a named plan with its tasks.
35+type Plan struct {
36+ Name string `json:"name"`
37+ Tasks []PlanTask `json:"tasks"`
38+ Current int `json:"current"` // 0 = not started, 1 = first task, etc.
39+ CreatedAt string `json:"createdAt"` // ISO timestamp
40+}
41+
42+// PlanTask represents a single task within a plan.
43+type PlanTask struct {
44+ ID int `json:"id"`
45+ Description string `json:"description"`
46+ Status string `json:"status"` // open, in_progress, done, failed
47+ StartedAt string `json:"startedAt,omitempty"`
48+ CompletedAt string `json:"completedAt,omitempty"`
49+ Notes string `json:"notes,omitempty"`
50+}
51+
52+// planInput is the input schema for the plan tool.
53+type planInput struct {
54+ // Action is required: create, get, update, next, list, delete, status
55+ Action string `json:"action" jsonschema_description:"Required. One of: create, get, update, next, list, delete, status"`
56+ // Name is the plan identifier (default: 'default' for unnamed plans)
57+ Name string `json:"name,omitempty" jsonschema_description:"Plan name. Defaults to 'user-request' if not provided."`
58+ // Tasks is used with 'create' action to define the plan's tasks
59+ Tasks []PlanTaskInput `json:"tasks,omitempty" jsonschema_description:"For create action: list of tasks to add"`
60+ // TaskID identifies which task for update/next actions (1-based)
61+ TaskID int `json:"taskId,omitempty" jsonschema_description:"Task ID (1-based) for update/delete/get actions"`
62+ // Status is used with 'update' action: open, in_progress, done, failed
63+ Status string `json:"status,omitempty" jsonschema_description:"For update action: one of open, in_progress, done, failed"`
64+ // Message is an optional note (e.g., error message for failed status)
65+ Message string `json:"message,omitempty" jsonschema_description:"Optional note for status updates"`
66+}
67+
68+// PlanTaskInput is the input format for task creation.
69+type PlanTaskInput struct {
70+ ID int `json:"id,omitempty"`
71+ Description string `json:"description"`
72+}
73+
74+// PlanOutput is the output format for plan operations.
75+type PlanOutput struct {
76+ Result string `json:"result"`
77+ PlanName string `json:"planName,omitempty"`
78+ Plan *Plan `json:"plan,omitempty"`
79+ Task *PlanTask `json:"task,omitempty"`
80+ Status string `json:"status,omitempty"`
81+ Error string `json:"error,omitempty"`
82+ Current int `json:"current,omitempty"`
83+ Total int `json:"total,omitempty"`
84+}
85+
86+// PlanStore holds plans in memory, keyed by session ID and plan name.
87+// This is a package-level variable so plans persist across turns within
88+// a single session. Plans are cleared when a new session starts.
89+type PlanStore struct {
90+ mu sync.RWMutex
91+ plans map[string]map[string]*Plan // sessionID -> planName -> Plan
92+ nextID map[string]int // sessionID -> next task ID
93+}
94+
95+var planStore = &PlanStore{
96+ plans: make(map[string]map[string]*Plan),
97+ nextID: make(map[string]int),
98+}
99+
100+// getSessionID extracts a session identifier from the tool context.
101+// For simplicity in v1, we use a placeholder since the actual session ID
102+// isn't easily accessible from tool context. In practice, plans are cleared
103+// on /new command via ClearPlans().
104+func getSessionID() string {
105+ // v1: use a single session for simplicity
106+ // v2: extract from context when available
107+ return "current-session"
108+}
109+
110+// newPlanName generates a unique plan name if none is provided.
111+// Must be called with planStore.mu held.
112+func newPlanName(sessionID string, plans map[string]map[string]*Plan) string {
113+ count := len(plans[sessionID])
114+ return fmt.Sprintf("user-request-%d", count+1)
115+}
116+
117+// PlanStore methods
118+
119+// CreatePlan creates a new plan with the given tasks.
120+func (s *PlanStore) CreatePlan(name string, tasks []PlanTaskInput) (*Plan, string, error) {
121+ sessionID := getSessionID()
122+ s.mu.Lock()
123+ defer s.mu.Unlock()
124+
125+ if name == "" {
126+ name = newPlanName(sessionID, s.plans)
127+ }
128+
129+ // Check if plan already exists
130+ if plans, ok := s.plans[sessionID]; ok {
131+ if _, exists := plans[name]; exists {
132+ return nil, "", fmt.Errorf("plan '%s' already exists", name)
133+ }
134+ }
135+
136+ // Ensure we have a session entry
137+ if _, ok := s.plans[sessionID]; !ok {
138+ s.plans[sessionID] = make(map[string]*Plan)
139+ }
140+
141+ // Convert input tasks to PlanTasks
142+ planTasks := make([]PlanTask, len(tasks))
143+ for i, t := range tasks {
144+ planTasks[i] = PlanTask{
145+ ID: t.ID,
146+ Description: t.Description,
147+ Status: "open",
148+ }
149+ // Auto-assign IDs if not provided
150+ if t.ID == 0 {
151+ planTasks[i].ID = i + 1
152+ }
153+ }
154+
155+ plan := &Plan{
156+ Name: name,
157+ Tasks: planTasks,
158+ Current: 0,
159+ CreatedAt: time.Now().UTC().Format(time.RFC3339),
160+ }
161+
162+ s.plans[sessionID][name] = plan
163+
164+ return plan, fmt.Sprintf("Plan '%s' created with %d tasks. Current: Task %d", name, len(tasks), 1), nil
165+}
166+
167+// GetPlan retrieves a plan by name.
168+func (s *PlanStore) GetPlan(name string) (*Plan, string, error) {
169+ sessionID := getSessionID()
170+ s.mu.RLock()
171+ defer s.mu.RUnlock()
172+
173+ plans, ok := s.plans[sessionID]
174+ if !ok {
175+ return nil, "", fmt.Errorf("no plans found for this session")
176+ }
177+
178+ plan, exists := plans[name]
179+ if !exists {
180+ return nil, "", fmt.Errorf("no plan named '%s' found", name)
181+ }
182+
183+ return plan, fmt.Sprintf("Retrieved plan '%s' with %d tasks", name, len(plan.Tasks)), nil
184+}
185+
186+// ListPlans returns all plans for the current session.
187+func (s *PlanStore) ListPlans() ([]*Plan, string, error) {
188+ sessionID := getSessionID()
189+ s.mu.RLock()
190+ defer s.mu.RUnlock()
191+
192+ plans, ok := s.plans[sessionID]
193+ if !ok || len(plans) == 0 {
194+ return nil, "No active plans", nil
195+ }
196+
197+ // Sort plans by creation time (approximate via name for v1)
198+ var planList []*Plan
199+ for _, p := range plans {
200+ planList = append(planList, p)
201+ }
202+ sort.Slice(planList, func(i, j int) bool {
203+ return planList[i].CreatedAt < planList[j].CreatedAt
204+ })
205+
206+ return planList, fmt.Sprintf("Found %d plan(s)", len(planList)), nil
207+}
208+
209+// UpdateTask updates a task's status in a plan.
210+func (s *PlanStore) UpdateTask(name string, taskID int, status string, message string) (*Plan, string, error) {
211+ sessionID := getSessionID()
212+ s.mu.Lock()
213+ defer s.mu.Unlock()
214+
215+ plans, ok := s.plans[sessionID]
216+ if !ok {
217+ return nil, "", fmt.Errorf("no plans found for this session")
218+ }
219+
220+ plan, exists := plans[name]
221+ if !exists {
222+ return nil, "", fmt.Errorf("no plan named '%s' found", name)
223+ }
224+
225+ // Validate status
226+ validStatuses := map[string]bool{"open": true, "in_progress": true, "done": true, "failed": true}
227+ if !validStatuses[status] {
228+ return nil, "", fmt.Errorf("invalid status '%s'. Must be one of: open, in_progress, done, failed", status)
229+ }
230+
231+ // Find task by ID (1-based)
232+ taskIndex := -1
233+ for i, t := range plan.Tasks {
234+ if t.ID == taskID {
235+ taskIndex = i
236+ break
237+ }
238+ }
239+
240+ if taskIndex == -1 {
241+ return nil, "", fmt.Errorf("no task with id %d in plan '%s'", taskID, name)
242+ }
243+
244+ // Update task
245+ task := &plan.Tasks[taskIndex]
246+ task.Status = status
247+ task.Notes = message
248+
249+ // Set timestamps
250+ now := time.Now().UTC().Format(time.RFC3339)
251+ if status == "in_progress" && task.StartedAt == "" {
252+ task.StartedAt = now
253+ }
254+ if status == "done" || status == "failed" {
255+ task.CompletedAt = now
256+ }
257+
258+ return plan, fmt.Sprintf("Task %d in plan '%s' updated to '%s'", taskID, name, status), nil
259+}
260+
261+// NextTask advances to the next task in a plan, marking current as done.
262+func (s *PlanStore) NextTask(name string) (*Plan, *PlanTask, string, error) {
263+ sessionID := getSessionID()
264+ s.mu.Lock()
265+ defer s.mu.Unlock()
266+
267+ plans, ok := s.plans[sessionID]
268+ if !ok {
269+ return nil, nil, "", fmt.Errorf("no plans found for this session")
270+ }
271+
272+ plan, exists := plans[name]
273+ if !exists {
274+ return nil, nil, "", fmt.Errorf("no plan named '%s' found", name)
275+ }
276+
277+ // If plan is complete
278+ if plan.Current > len(plan.Tasks) {
279+ return plan, nil, fmt.Sprintf("Plan '%s' is complete! All %d tasks done.", name, len(plan.Tasks)), nil
280+ }
281+
282+ // Mark current task as done (if any)
283+ if plan.Current > 0 && plan.Current <= len(plan.Tasks) {
284+ plan.Tasks[plan.Current-1].Status = "done"
285+ plan.Tasks[plan.Current-1].CompletedAt = time.Now().UTC().Format(time.RFC3339)
286+ }
287+
288+ // Move to next
289+ plan.Current++
290+
291+ // Return next task
292+ if plan.Current <= len(plan.Tasks) {
293+ nextTask := &plan.Tasks[plan.Current-1]
294+ // Auto-set to in_progress
295+ nextTask.Status = "in_progress"
296+ nextTask.StartedAt = time.Now().UTC().Format(time.RFC3339)
297+ return plan, nextTask, fmt.Sprintf("Task %d done. Next: Task %d - %s", plan.Current-1, plan.Current, nextTask.Description), nil
298+ }
299+
300+ // Plan is now complete
301+ return plan, nil, fmt.Sprintf("Plan '%s' is complete! All %d tasks done.", name, len(plan.Tasks)), nil
302+}
303+
304+// DeletePlan removes a plan.
305+func (s *PlanStore) DeletePlan(name string) (string, error) {
306+ sessionID := getSessionID()
307+ s.mu.Lock()
308+ defer s.mu.Unlock()
309+
310+ plans, ok := s.plans[sessionID]
311+ if !ok {
312+ return "", fmt.Errorf("no plans found for this session")
313+ }
314+
315+ if _, exists := plans[name]; !exists {
316+ return "", fmt.Errorf("no plan named '%s' found", name)
317+ }
318+
319+ delete(plans, name)
320+ return fmt.Sprintf("Plan '%s' deleted", name), nil
321+}
322+
323+// GetStatus returns a compact status line for a plan.
324+func (s *PlanStore) GetStatus(name string) (string, string, error) {
325+ sessionID := getSessionID()
326+ s.mu.RLock()
327+ defer s.mu.RUnlock()
328+
329+ plans, ok := s.plans[sessionID]
330+ if !ok {
331+ return "", "", fmt.Errorf("no plans found for this session")
332+ }
333+
334+ plan, exists := plans[name]
335+ if !exists {
336+ return "", "", fmt.Errorf("no plan named '%s' found", name)
337+ }
338+
339+ // Count statuses
340+ doneCount := 0
341+ for _, t := range plan.Tasks {
342+ if t.Status == "done" {
343+ doneCount++
344+ }
345+ }
346+
347+ // Format status line
348+ // Current=0 means not started yet, Task 1 is next
349+ // Current>0 means we've started, Task Current is the next one
350+ if plan.Current == 0 {
351+ // Not started, show Task 1
352+ task := plan.Tasks[0]
353+ statusIcon := "⏳"
354+ if task.Status == "done" {
355+ statusIcon = "✅"
356+ } else if task.Status == "in_progress" {
357+ statusIcon = "🔄"
358+ } else if task.Status == "failed" {
359+ statusIcon = "❌"
360+ }
361+ return fmt.Sprintf("[%d/%d] %s Task %d: %s (%s)", 1, len(plan.Tasks), statusIcon, 1, task.Description, task.Status),
362+ fmt.Sprintf("Plan '%s': %d/%d tasks complete", name, doneCount, len(plan.Tasks)), nil
363+ }
364+
365+ // Current > 0, show Task Current
366+ if plan.Current <= len(plan.Tasks) {
367+ task := plan.Tasks[plan.Current-1]
368+ statusIcon := ""
369+ switch task.Status {
370+ case "done":
371+ statusIcon = "✅"
372+ case "in_progress":
373+ statusIcon = "🔄"
374+ case "failed":
375+ statusIcon = "❌"
376+ default:
377+ statusIcon = "⏳"
378+ }
379+ return fmt.Sprintf("[%d/%d] %s Task %d: %s (%s)", plan.Current, len(plan.Tasks), statusIcon, plan.Current, task.Description, task.Status),
380+ fmt.Sprintf("Plan '%s': %d/%d tasks complete", name, doneCount, len(plan.Tasks)), nil
381+ }
382+
383+ // All tasks complete
384+ return fmt.Sprintf("[%d/%d] All tasks complete!", len(plan.Tasks), len(plan.Tasks)),
385+ fmt.Sprintf("Plan '%s': All %d tasks done ✅", name, len(plan.Tasks)), nil
386+}
387+
388+// ClearPlans removes all plans for the current session.
389+// Called when /new command is issued.
390+func (s *PlanStore) ClearPlans() {
391+ sessionID := getSessionID()
392+ s.mu.Lock()
393+ defer s.mu.Unlock()
394+ delete(s.plans, sessionID)
395+}
396+
397+// ClearAllPlans removes all plans. Used for testing.
398+func (s *PlanStore) ClearAllPlans() {
399+ s.mu.Lock()
400+ defer s.mu.Unlock()
401+ s.plans = make(map[string]map[string]*Plan)
402+}
403+
404+// announcePlan prints the display line for a plan operation.
405+func announcePlan(line string) {
406+ spinner.Suspend(func() {
407+ printMu.Lock()
408+ fmt.Printf("\n📋 %s\n", clip(line))
409+ printMu.Unlock()
410+ })
411+}
412+
413+// PlanTool declares the `plan` tool.
414+func PlanTool(g *genkit.Genkit, d *detector.LoopDetector) ai.ToolRef {
415+ description := `Create and manage multi-step plans. When the user gives you a list of tasks to do (e.g., "Do: 1. X, 2. Y, 3. Z"), use this tool to create a plan and track progress.
416+
417+Actions:
418+- create: Create a new plan with tasks. Required: tasks (array of {description}). Optional: name.
419+- get: Retrieve a plan by name. Required: name.
420+- update: Update a task's status. Required: name, taskId, status (open/in_progress/done/failed). Optional: message.
421+- next: Move to the next task (marks current as done). Required: name.
422+- list: List all plans for this session.
423+- delete: Delete a plan. Required: name.
424+- status: Get a compact status line for display. Required: name.
425+
426+Every action returns a text summary that you should include in your response to the user.
427+
428+Example workflow:
429+1. User says "Do these: 1. Read file, 2. Analyze, 3. Report"
430+2. You call: plan(action="create", tasks=[{description:"Read file"}, {description:"Analyze"}, {description:"Report"}])
431+3. You call: plan(action="update", name="user-request-1", taskId=1, status="in_progress")
432+4. You do the work
433+5. You call: plan(action="update", name="user-request-1", taskId=1, status="done")
434+6. You call: plan(action="next", name="user-request-1")
435+7. Include the status line from plan(action="status") in your response.
436+
437+Always show the user the current plan status using the status action.`
438+
439+ return genkit.DefineTool(g, "plan",
440+ description,
441+ func(tctx *ai.ToolContext, in planInput) (string, error) {
442+ // Event-consuming front end (ACP) uses evented variant
443+ if s := ui.ActiveSink(); s != nil {
444+ return planEvented(tctx, s, d, in)
445+ }
446+
447+ // Terminal/REPL: announce the call
448+ spinner.Suspend(func() {
449+ printMu.Lock()
450+ fmt.Printf("\n📋 plan: %s %s\n", in.Action, in.Name)
451+ printMu.Unlock()
452+ })
453+
454+ spinner.Start("Running")
455+
456+ var result string
457+
458+ switch in.Action {
459+ case "create":
460+ plan, msg, createErr := planStore.CreatePlan(in.Name, in.Tasks)
461+ if createErr != nil {
462+ result = fmt.Sprintf("[error: %v]", createErr)
463+ break
464+ }
465+ // Return plan info as JSON for model, plus text summary
466+ output := PlanOutput{
467+ Result: msg,
468+ PlanName: plan.Name,
469+ Plan: plan,
470+ Current: plan.Current,
471+ Total: len(plan.Tasks),
472+ }
473+ outputBytes, _ := json.Marshal(output)
474+ result = string(outputBytes)
475+
476+ case "get":
477+ plan, msg, getErr := planStore.GetPlan(in.Name)
478+ if getErr != nil {
479+ result = fmt.Sprintf("[error: %v]", getErr)
480+ break
481+ }
482+ output := PlanOutput{
483+ Result: msg,
484+ Plan: plan,
485+ }
486+ outputBytes, _ := json.Marshal(output)
487+ result = string(outputBytes)
488+
489+ case "update":
490+ _, msg, updateErr := planStore.UpdateTask(in.Name, in.TaskID, in.Status, in.Message)
491+ if updateErr != nil {
492+ result = fmt.Sprintf("[error: %v]", updateErr)
493+ break
494+ }
495+ // Get status line
496+ statusLine, _, _ := planStore.GetStatus(in.Name)
497+ result = fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine)
498+
499+ case "next":
500+ _, nextTask, msg, nextErr := planStore.NextTask(in.Name)
501+ if nextErr != nil {
502+ result = fmt.Sprintf("[error: %v]", nextErr)
503+ break
504+ }
505+ // Get status line
506+ statusLine, _, _ := planStore.GetStatus(in.Name)
507+ if nextTask != nil {
508+ result = fmt.Sprintf(`{"result":"%s","status":"%s","nextTask":{"id":%d,"description":"%s"}}`,
509+ msg, statusLine, nextTask.ID, strings.ReplaceAll(nextTask.Description, "\"", "\\\""))
510+ } else {
511+ result = fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine)
512+ }
513+
514+ case "list":
515+ plans, msg, listErr := planStore.ListPlans()
516+ if listErr != nil {
517+ result = fmt.Sprintf("[error: %v]", listErr)
518+ break
519+ }
520+ if len(plans) == 0 {
521+ result = `{"result":"` + msg + `","plans":[]}`
522+ break
523+ }
524+ // Build simple list
525+ var planNames []string
526+ for _, p := range plans {
527+ planNames = append(planNames, p.Name)
528+ }
529+ result = fmt.Sprintf(`{"result":"%s","plans":["%s"]}`, msg, strings.Join(planNames, `", "`))
530+
531+ case "delete":
532+ msg, delErr := planStore.DeletePlan(in.Name)
533+ if delErr != nil {
534+ result = fmt.Sprintf("[error: %v]", delErr)
535+ break
536+ }
537+ result = fmt.Sprintf(`{"result":"%s"}`, msg)
538+
539+ case "status":
540+ statusLine, msg, statusErr := planStore.GetStatus(in.Name)
541+ if statusErr != nil {
542+ result = fmt.Sprintf("[error: %v]", statusErr)
543+ break
544+ }
545+ result = fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine)
546+
547+ default:
548+ result = fmt.Sprintf("[error: unknown action '%s'. Valid actions: create, get, update, next, list, delete, status]", in.Action)
549+ }
550+
551+ // Record in loop detector
552+ inputJSON, _ := json.Marshal(in)
553+ return record(d, "plan", string(inputJSON), result)
554+ })
555+}
556+
557+// planEvented is the plan tool for event-consuming front ends (ACP).
558+func planEvented(ctx context.Context, s ui.Sink, d *detector.LoopDetector, in planInput) (string, error) {
559+ eventKind := "read"
560+ if in.Action == "create" || in.Action == "update" || in.Action == "next" || in.Action == "delete" {
561+ eventKind = "edit"
562+ }
563+
564+ ev := ui.ToolEvent{
565+ ID: ui.NextCallID(),
566+ Tool: "plan",
567+ Title: fmt.Sprintf("plan: %s %s", in.Action, in.Name),
568+ Kind: eventKind,
569+ Input: map[string]any{"action": in.Action, "name": in.Name, "taskId": in.TaskID, "status": in.Status},
570+ }
571+
572+ s.ToolStart(ev)
573+
574+ // For destructive actions, check permission
575+ if eventKind == "edit" {
576+ if !s.Allow(ctx, ev) {
577+ out := "[plan: command rejected by the user]"
578+ s.ToolEnd(ev.ID, ui.ToolResult{Output: out, Failed: true})
579+ inputJSON, _ := json.Marshal(in)
580+ return record(d, "plan", string(inputJSON), out)
581+ }
582+ }
583+
584+ s.ToolRunning(ev.ID)
585+
586+ // Execute the action
587+ var result string
588+
589+ switch in.Action {
590+ case "create":
591+ plan, msg, createErr := planStore.CreatePlan(in.Name, in.Tasks)
592+ if createErr != nil {
593+ result = fmt.Sprintf("[error: %v]", createErr)
594+ } else {
595+ output := PlanOutput{
596+ Result: msg,
597+ PlanName: plan.Name,
598+ Current: plan.Current,
599+ Total: len(plan.Tasks),
600+ }
601+ outputBytes, _ := json.Marshal(output)
602+ result = string(outputBytes)
603+ }
604+
605+ case "get":
606+ plan, msg, getErr := planStore.GetPlan(in.Name)
607+ if getErr != nil {
608+ result = fmt.Sprintf("[error: %v]", getErr)
609+ } else {
610+ output := PlanOutput{
611+ Result: msg,
612+ Plan: plan,
613+ }
614+ outputBytes, _ := json.Marshal(output)
615+ result = string(outputBytes)
616+ }
617+
618+ case "update":
619+ _, msg, updateErr := planStore.UpdateTask(in.Name, in.TaskID, in.Status, in.Message)
620+ if updateErr != nil {
621+ result = fmt.Sprintf("[error: %v]", updateErr)
622+ } else {
623+ statusLine, _, _ := planStore.GetStatus(in.Name)
624+ result = fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine)
625+ }
626+
627+ case "next":
628+ _, nextTask, msg, nextErr := planStore.NextTask(in.Name)
629+ if nextErr != nil {
630+ result = fmt.Sprintf("[error: %v]", nextErr)
631+ } else {
632+ statusLine, _, _ := planStore.GetStatus(in.Name)
633+ if nextTask != nil {
634+ result = fmt.Sprintf(`{"result":"%s","status":"%s","nextTask":{"id":%d,"description":"%s"}}`,
635+ msg, statusLine, nextTask.ID, strings.ReplaceAll(nextTask.Description, "\"", "\\\""))
636+ } else {
637+ result = fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine)
638+ }
639+ }
640+
641+ case "list":
642+ plans, msg, listErr := planStore.ListPlans()
643+ if listErr != nil {
644+ result = fmt.Sprintf("[error: %v]", listErr)
645+ } else if len(plans) == 0 {
646+ result = `{"result":"` + msg + `","plans":[]}`
647+ } else {
648+ var planNames []string
649+ for _, p := range plans {
650+ planNames = append(planNames, p.Name)
651+ }
652+ result = fmt.Sprintf(`{"result":"%s","plans":["%s"]}`, msg, strings.Join(planNames, `", "`))
653+ }
654+
655+ case "delete":
656+ msg, delErr := planStore.DeletePlan(in.Name)
657+ if delErr != nil {
658+ result = fmt.Sprintf("[error: %v]", delErr)
659+ } else {
660+ result = fmt.Sprintf(`{"result":"%s"}`, msg)
661+ }
662+
663+ case "status":
664+ statusLine, msg, statusErr := planStore.GetStatus(in.Name)
665+ if statusErr != nil {
666+ result = fmt.Sprintf("[error: %v]", statusErr)
667+ } else {
668+ result = fmt.Sprintf(`{"result":"%s","status":"%s"}`, msg, statusLine)
669+ }
670+
671+ default:
672+ result = fmt.Sprintf("[error: unknown action '%s']", in.Action)
673+ }
674+
675+ // Check if failed
676+ failed := strings.HasPrefix(result, "[error:")
677+
678+ s.ToolEnd(ev.ID, ui.ToolResult{Output: result, Failed: failed})
679+
680+ // Record in loop detector
681+ inputJSON, _ := json.Marshal(in)
682+ return record(d, "plan", string(inputJSON), result)
683+}
684+
685+// ClearAllPlans clears all plans for the current session.
686+// Called when a new session starts (/new command).
687+func ClearAllPlans() {
688+ planStore.ClearPlans()
689+}
690+
691+// ClearAllPlansForTesting exposes ClearAllPlans for tests.
692+func ClearAllPlansForTesting() {
693+ planStore.ClearAllPlans()
694+}
added internal/tools/plan_test.go +542 -0
new file mode 100644
@@ -0,0 +1,542 @@
1+package tools
2+
3+import (
4+ "encoding/json"
5+ "strings"
6+ "testing"
7+)
8+
9+func TestPlanStore_CreatePlan(t *testing.T) {
10+ // Clear any existing plans
11+ ClearAllPlansForTesting()
12+
13+ store := planStore
14+
15+ // Test creating a plan
16+ plan, msg, err := store.CreatePlan("test-plan", []PlanTaskInput{
17+ {Description: "Task 1"},
18+ {Description: "Task 2"},
19+ {Description: "Task 3"},
20+ })
21+
22+ if err != nil {
23+ t.Fatalf("CreatePlan failed: %v", err)
24+ }
25+ if plan == nil {
26+ t.Fatal("CreatePlan returned nil plan")
27+ }
28+ if plan.Name != "test-plan" {
29+ t.Errorf("Expected name 'test-plan', got '%s'", plan.Name)
30+ }
31+ if len(plan.Tasks) != 3 {
32+ t.Errorf("Expected 3 tasks, got %d", len(plan.Tasks))
33+ }
34+ if plan.Current != 0 {
35+ t.Errorf("Expected Current=0, got %d", plan.Current)
36+ }
37+ if plan.Tasks[0].ID != 1 || plan.Tasks[1].ID != 2 || plan.Tasks[2].ID != 3 {
38+ t.Errorf("Task IDs not assigned correctly")
39+ }
40+ if plan.Tasks[0].Status != "open" {
41+ t.Errorf("Expected task status 'open', got '%s'", plan.Tasks[0].Status)
42+ }
43+
44+ // Check message
45+ if !strings.Contains(msg, "created with 3 tasks") {
46+ t.Errorf("Unexpected message: %s", msg)
47+ }
48+}
49+
50+func TestPlanStore_CreatePlan_AutoName(t *testing.T) {
51+ ClearAllPlansForTesting()
52+
53+ store := planStore
54+
55+ // Create plan with empty name
56+ plan, _, err := store.CreatePlan("", []PlanTaskInput{
57+ {Description: "Task 1"},
58+ })
59+
60+ if err != nil {
61+ t.Fatalf("CreatePlan with empty name failed: %v", err)
62+ }
63+ if plan == nil {
64+ t.Fatal("CreatePlan returned nil plan")
65+ }
66+ if !strings.HasPrefix(plan.Name, "user-request-") {
67+ t.Errorf("Expected auto-generated name starting with 'user-request-', got '%s'", plan.Name)
68+ }
69+
70+ // Create another, should have incremented name
71+ plan2, _, err := store.CreatePlan("", []PlanTaskInput{
72+ {Description: "Task A"},
73+ })
74+ if err != nil {
75+ t.Fatalf("CreatePlan second failed: %v", err)
76+ }
77+ if plan2.Name == plan.Name {
78+ t.Errorf("Expected unique names, got same name: %s", plan2.Name)
79+ }
80+}
81+
82+func TestPlanStore_CreatePlan_Duplicate(t *testing.T) {
83+ ClearAllPlansForTesting()
84+
85+ store := planStore
86+
87+ // Create first plan
88+ _, _, err := store.CreatePlan("duplicate-test", []PlanTaskInput{
89+ {Description: "Task 1"},
90+ })
91+ if err != nil {
92+ t.Fatalf("First CreatePlan failed: %v", err)
93+ }
94+
95+ // Try to create duplicate
96+ _, _, err = store.CreatePlan("duplicate-test", []PlanTaskInput{
97+ {Description: "Task 2"},
98+ })
99+ if err == nil {
100+ t.Error("Expected error for duplicate plan, got nil")
101+ }
102+ if !strings.Contains(err.Error(), "already exists") {
103+ t.Errorf("Expected 'already exists' in error, got: %v", err)
104+ }
105+}
106+
107+func TestPlanStore_GetPlan(t *testing.T) {
108+ ClearAllPlansForTesting()
109+
110+ store := planStore
111+
112+ // Create a plan
113+ _, _, err := store.CreatePlan("get-test", []PlanTaskInput{
114+ {Description: "Task 1"},
115+ })
116+ if err != nil {
117+ t.Fatalf("CreatePlan failed: %v", err)
118+ }
119+
120+ // Get the plan
121+ plan, _, err := store.GetPlan("get-test")
122+ if err != nil {
123+ t.Fatalf("GetPlan failed: %v", err)
124+ }
125+ if plan == nil {
126+ t.Fatal("GetPlan returned nil plan")
127+ }
128+ if plan.Name != "get-test" {
129+ t.Errorf("Expected name 'get-test', got '%s'", plan.Name)
130+ }
131+
132+ // Get non-existent plan
133+ _, _, err = store.GetPlan("non-existent")
134+ if err == nil {
135+ t.Error("Expected error for non-existent plan, got nil")
136+ }
137+}
138+
139+func TestPlanStore_UpdateTask(t *testing.T) {
140+ ClearAllPlansForTesting()
141+
142+ store := planStore
143+
144+ // Create a plan
145+ _, _, err := store.CreatePlan("update-test", []PlanTaskInput{
146+ {Description: "Task 1"},
147+ {Description: "Task 2"},
148+ })
149+ if err != nil {
150+ t.Fatalf("CreatePlan failed: %v", err)
151+ }
152+
153+ // Update task 1 to in_progress
154+ plan, _, err := store.UpdateTask("update-test", 1, "in_progress", "")
155+ if err != nil {
156+ t.Fatalf("UpdateTask failed: %v", err)
157+ }
158+ if plan.Tasks[0].Status != "in_progress" {
159+ t.Errorf("Expected status 'in_progress', got '%s'", plan.Tasks[0].Status)
160+ }
161+ if plan.Tasks[0].StartedAt == "" {
162+ t.Error("Expected StartedAt to be set")
163+ }
164+
165+ // Update task 1 to done
166+ plan, _, err = store.UpdateTask("update-test", 1, "done", "Task completed")
167+ if err != nil {
168+ t.Fatalf("UpdateTask to done failed: %v", err)
169+ }
170+ if plan.Tasks[0].Status != "done" {
171+ t.Errorf("Expected status 'done', got '%s'", plan.Tasks[0].Status)
172+ }
173+ if plan.Tasks[0].Notes != "Task completed" {
174+ t.Errorf("Expected notes 'Task completed', got '%s'", plan.Tasks[0].Notes)
175+ }
176+ if plan.Tasks[0].CompletedAt == "" {
177+ t.Error("Expected CompletedAt to be set")
178+ }
179+
180+ // Invalid status
181+ _, _, err = store.UpdateTask("update-test", 1, "invalid", "")
182+ if err == nil {
183+ t.Error("Expected error for invalid status, got nil")
184+ }
185+
186+ // Invalid task ID
187+ _, _, err = store.UpdateTask("update-test", 99, "done", "")
188+ if err == nil {
189+ t.Error("Expected error for invalid task ID, got nil")
190+ }
191+}
192+
193+func TestPlanStore_NextTask(t *testing.T) {
194+ ClearAllPlansForTesting()
195+
196+ store := planStore
197+
198+ // Create a plan with 3 tasks
199+ _, _, err := store.CreatePlan("next-test", []PlanTaskInput{
200+ {Description: "Task 1"},
201+ {Description: "Task 2"},
202+ {Description: "Task 3"},
203+ })
204+ if err != nil {
205+ t.Fatalf("CreatePlan failed: %v", err)
206+ }
207+
208+ // First next - should go to task 1 (auto marked as in_progress)
209+ plan, nextTask, msg, err := store.NextTask("next-test")
210+ if err != nil {
211+ t.Fatalf("First NextTask failed: %v", err)
212+ }
213+ if plan.Current != 1 {
214+ t.Errorf("Expected Current=1, got %d", plan.Current)
215+ }
216+ if nextTask == nil {
217+ t.Fatal("Expected nextTask, got nil")
218+ }
219+ if nextTask.ID != 1 {
220+ t.Errorf("Expected nextTask.ID=1, got %d", nextTask.ID)
221+ }
222+ if nextTask.Status != "in_progress" {
223+ t.Errorf("Expected nextTask status 'in_progress', got '%s'", nextTask.Status)
224+ }
225+ if !strings.Contains(msg, "Next: Task 1") {
226+ t.Errorf("Unexpected message: %s", msg)
227+ }
228+
229+ // Second next - should mark task 1 as done, go to task 2
230+ plan, nextTask, msg, err = store.NextTask("next-test")
231+ if err != nil {
232+ t.Fatalf("Second NextTask failed: %v", err)
233+ }
234+ if plan.Current != 2 {
235+ t.Errorf("Expected Current=2, got %d", plan.Current)
236+ }
237+ if plan.Tasks[0].Status != "done" {
238+ t.Errorf("Expected Task 1 status 'done', got '%s'", plan.Tasks[0].Status)
239+ }
240+ if nextTask.ID != 2 {
241+ t.Errorf("Expected nextTask.ID=2, got %d", nextTask.ID)
242+ }
243+
244+ // Third next - should mark task 2 as done, go to task 3
245+ plan, nextTask, msg, err = store.NextTask("next-test")
246+ if err != nil {
247+ t.Fatalf("Third NextTask failed: %v", err)
248+ }
249+ if plan.Current != 3 {
250+ t.Errorf("Expected Current=3, got %d", plan.Current)
251+ }
252+ if plan.Tasks[1].Status != "done" {
253+ t.Errorf("Expected Task 2 status 'done', got '%s'", plan.Tasks[1].Status)
254+ }
255+ if nextTask.ID != 3 {
256+ t.Errorf("Expected nextTask.ID=3, got %d", nextTask.ID)
257+ }
258+
259+ // Fourth next - should complete the plan
260+ plan, nextTask, msg, err = store.NextTask("next-test")
261+ if err != nil {
262+ t.Fatalf("Fourth NextTask failed: %v", err)
263+ }
264+ if plan.Current != 4 {
265+ t.Errorf("Expected Current=4, got %d", plan.Current)
266+ }
267+ if nextTask != nil {
268+ t.Error("Expected nextTask nil (plan complete), got non-nil")
269+ }
270+ if !strings.Contains(msg, "complete") {
271+ t.Errorf("Expected 'complete' in message: %s", msg)
272+ }
273+
274+ // Fifth next - should return already complete
275+ _, _, msg, err = store.NextTask("next-test")
276+ if err != nil {
277+ t.Fatalf("Fifth NextTask failed: %v", err)
278+ }
279+ if !strings.Contains(msg, "already complete") && !strings.Contains(msg, "is complete") {
280+ t.Errorf("Expected completion message: %s", msg)
281+ }
282+}
283+
284+func TestPlanStore_DeletePlan(t *testing.T) {
285+ ClearAllPlansForTesting()
286+
287+ store := planStore
288+
289+ // Create a plan
290+ _, _, err := store.CreatePlan("delete-test", []PlanTaskInput{
291+ {Description: "Task 1"},
292+ })
293+ if err != nil {
294+ t.Fatalf("CreatePlan failed: %v", err)
295+ }
296+
297+ // Delete the plan
298+ msg, err := store.DeletePlan("delete-test")
299+ if err != nil {
300+ t.Fatalf("DeletePlan failed: %v", err)
301+ }
302+ if !strings.Contains(msg, "deleted") {
303+ t.Errorf("Expected 'deleted' in message: %s", msg)
304+ }
305+
306+ // Try to get deleted plan
307+ _, _, err = store.GetPlan("delete-test")
308+ if err == nil {
309+ t.Error("Expected error for deleted plan, got nil")
310+ }
311+
312+ // Delete non-existent plan
313+ _, err = store.DeletePlan("non-existent")
314+ if err == nil {
315+ t.Error("Expected error for non-existent plan, got nil")
316+ }
317+}
318+
319+func TestPlanStore_GetStatus(t *testing.T) {
320+ ClearAllPlansForTesting()
321+
322+ store := planStore
323+
324+ // Create a plan
325+ _, _, err := store.CreatePlan("status-test", []PlanTaskInput{
326+ {Description: "Task 1"},
327+ {Description: "Task 2"},
328+ {Description: "Task 3"},
329+ })
330+ if err != nil {
331+ t.Fatalf("CreatePlan failed: %v", err)
332+ }
333+
334+ // Get status before starting
335+ statusLine, msg, err := store.GetStatus("status-test")
336+ if err != nil {
337+ t.Fatalf("GetStatus failed: %v", err)
338+ }
339+ if !strings.Contains(statusLine, "[1/3]") {
340+ t.Errorf("Expected '[1/3]' in status, got: %s", statusLine)
341+ }
342+ if !strings.Contains(statusLine, "Task 1") {
343+ t.Errorf("Expected 'Task 1' in status, got: %s", statusLine)
344+ }
345+
346+ // Move to task 1 (first NextTask starts Task 1)
347+ _, _, _, err = store.NextTask("status-test")
348+ if err != nil {
349+ t.Fatalf("NextTask failed: %v", err)
350+ }
351+
352+ // Get status at task 1
353+ statusLine, _, err = store.GetStatus("status-test")
354+ if err != nil {
355+ t.Fatalf("GetStatus failed: %v", err)
356+ }
357+ if !strings.Contains(statusLine, "[1/3]") {
358+ t.Errorf("Expected '[1/3]' in status, got: %s", statusLine)
359+ }
360+ if !strings.Contains(statusLine, "in_progress") {
361+ t.Errorf("Expected 'in_progress' in status, got: %s", statusLine)
362+ }
363+
364+ // Complete all tasks (need 3 more NextTask calls to complete all 3 tasks)
365+ store.NextTask("status-test") // Marks Task 1 as done, moves to Task 2
366+ store.NextTask("status-test") // Marks Task 2 as done, moves to Task 3
367+ store.NextTask("status-test") // Marks Task 3 as done, moves to Current=4 (complete)
368+
369+ // Get status when complete
370+ statusLine, _, err = store.GetStatus("status-test")
371+ if err != nil {
372+ t.Fatalf("GetStatus failed: %v", err)
373+ }
374+ if !strings.Contains(statusLine, "All tasks complete") {
375+ t.Errorf("Expected 'All tasks complete' in status, got: %s", statusLine)
376+ }
377+
378+ _ = msg // Used above
379+}
380+
381+func TestPlanStore_ListPlans(t *testing.T) {
382+ ClearAllPlansForTesting()
383+
384+ store := planStore
385+
386+ // List with no plans
387+ plans, msg, err := store.ListPlans()
388+ if err != nil {
389+ t.Fatalf("ListPlans with no plans failed: %v", err)
390+ }
391+ if len(plans) != 0 {
392+ t.Errorf("Expected 0 plans, got %d", len(plans))
393+ }
394+ if !strings.Contains(msg, "No active plans") {
395+ t.Errorf("Expected 'No active plans' in message: %s", msg)
396+ }
397+
398+ // Create multiple plans
399+ store.CreatePlan("plan-a", []PlanTaskInput{{Description: "A1"}})
400+ store.CreatePlan("plan-b", []PlanTaskInput{{Description: "B1"}})
401+ store.CreatePlan("plan-c", []PlanTaskInput{{Description: "C1"}})
402+
403+ // List plans
404+ plans, msg, err = store.ListPlans()
405+ if err != nil {
406+ t.Fatalf("ListPlans failed: %v", err)
407+ }
408+ if len(plans) != 3 {
409+ t.Errorf("Expected 3 plans, got %d", len(plans))
410+ }
411+ if !strings.Contains(msg, "3 plan(s)") {
412+ t.Errorf("Expected '3 plan(s)' in message: %s", msg)
413+ }
414+}
415+
416+func TestPlanStore_ClearPlans(t *testing.T) {
417+ ClearAllPlansForTesting()
418+
419+ store := planStore
420+
421+ // Create some plans
422+ store.CreatePlan("clear-test-1", []PlanTaskInput{{Description: "T1"}})
423+ store.CreatePlan("clear-test-2", []PlanTaskInput{{Description: "T2"}})
424+
425+ // Verify plans exist
426+ plans, _, _ := store.ListPlans()
427+ if len(plans) != 2 {
428+ t.Errorf("Expected 2 plans, got %d", len(plans))
429+ }
430+
431+ // Clear plans
432+ store.ClearPlans()
433+
434+ // Verify plans cleared
435+ plans, _, _ = store.ListPlans()
436+ if len(plans) != 0 {
437+ t.Errorf("Expected 0 plans after clear, got %d", len(plans))
438+ }
439+}
440+
441+func TestPlanStore_ClearAllPlans(t *testing.T) {
442+ ClearAllPlansForTesting()
443+
444+ store := planStore
445+
446+ // Create some plans
447+ store.CreatePlan("all-clear-1", []PlanTaskInput{{Description: "T1"}})
448+ store.CreatePlan("all-clear-2", []PlanTaskInput{{Description: "T2"}})
449+
450+ // Clear all
451+ store.ClearAllPlans()
452+
453+ // Verify all cleared
454+ plans, _, _ := store.ListPlans()
455+ if len(plans) != 0 {
456+ t.Errorf("Expected 0 plans after ClearAllPlans, got %d", len(plans))
457+ }
458+}
459+
460+func TestPlanTool_Integration(t *testing.T) {
461+ // Skip tool integration test - requires full Genkit setup
462+ // The store tests above verify the core functionality
463+ t.Skip("Skipping tool integration test - requires full Genkit setup")
464+}
465+
466+func TestPlanInput_JSON(t *testing.T) {
467+ // Test that input can be serialized/deserialized
468+ input := planInput{
469+ Action: "create",
470+ Name: "test",
471+ Tasks: []PlanTaskInput{
472+ {ID: 1, Description: "Task 1"},
473+ {ID: 2, Description: "Task 2"},
474+ },
475+ }
476+
477+ // Marshal to JSON
478+ data, err := json.Marshal(input)
479+ if err != nil {
480+ t.Fatalf("Failed to marshal input: %v", err)
481+ }
482+
483+ // Unmarshal back
484+ var decoded planInput
485+ if err := json.Unmarshal(data, &decoded); err != nil {
486+ t.Fatalf("Failed to unmarshal input: %v", err)
487+ }
488+
489+ if decoded.Action != input.Action {
490+ t.Errorf("Action mismatch: expected '%s', got '%s'", input.Action, decoded.Action)
491+ }
492+ if decoded.Name != input.Name {
493+ t.Errorf("Name mismatch: expected '%s', got '%s'", input.Name, decoded.Name)
494+ }
495+ if len(decoded.Tasks) != len(input.Tasks) {
496+ t.Errorf("Tasks length mismatch: expected %d, got %d", len(input.Tasks), len(decoded.Tasks))
497+ }
498+ if decoded.Tasks[0].Description != input.Tasks[0].Description {
499+ t.Errorf("Task description mismatch")
500+ }
501+}
502+
503+func TestPlanOutput_JSON(t *testing.T) {
504+ // Test that output can be serialized
505+ plan := &Plan{
506+ Name: "test",
507+ Tasks: []PlanTask{
508+ {ID: 1, Description: "Task 1", Status: "done"},
509+ },
510+ Current: 1,
511+ }
512+
513+ output := PlanOutput{
514+ Result: "success",
515+ PlanName: "test",
516+ Plan: plan,
517+ Current: 1,
518+ Total: 1,
519+ }
520+
521+ // Marshal to JSON
522+ data, err := json.Marshal(output)
523+ if err != nil {
524+ t.Fatalf("Failed to marshal output: %v", err)
525+ }
526+
527+ // Verify it contains expected fields
528+ str := string(data)
529+ if !strings.Contains(str, `"result":"success"`) {
530+ t.Errorf("Output missing result field: %s", str)
531+ }
532+ if !strings.Contains(str, `"planName":"test"`) {
533+ t.Errorf("Output missing planName field: %s", str)
534+ }
535+}
536+
537+// Test with actual tool call via genkit (if possible in test environment)
538+func TestPlanTool_CreateAndStatus(t *testing.T) {
539+ // Skip tool integration test - requires full Genkit setup
540+ // The store tests above verify the core functionality
541+ t.Skip("Skipping tool integration test - requires full Genkit setup")
542+}
new file mode 100644
@@ -0,0 +1,542 @@
1+package tools
2+
3+import (
4+ "encoding/json"
5+ "strings"
6+ "testing"
7+)
8+
9+func TestPlanStore_CreatePlan(t *testing.T) {
10+ // Clear any existing plans
11+ ClearAllPlansForTesting()
12+
13+ store := planStore
14+
15+ // Test creating a plan
16+ plan, msg, err := store.CreatePlan("test-plan", []PlanTaskInput{
17+ {Description: "Task 1"},
18+ {Description: "Task 2"},
19+ {Description: "Task 3"},
20+ })
21+
22+ if err != nil {
23+ t.Fatalf("CreatePlan failed: %v", err)
24+ }
25+ if plan == nil {
26+ t.Fatal("CreatePlan returned nil plan")
27+ }
28+ if plan.Name != "test-plan" {
29+ t.Errorf("Expected name 'test-plan', got '%s'", plan.Name)
30+ }
31+ if len(plan.Tasks) != 3 {
32+ t.Errorf("Expected 3 tasks, got %d", len(plan.Tasks))
33+ }
34+ if plan.Current != 0 {
35+ t.Errorf("Expected Current=0, got %d", plan.Current)
36+ }
37+ if plan.Tasks[0].ID != 1 || plan.Tasks[1].ID != 2 || plan.Tasks[2].ID != 3 {
38+ t.Errorf("Task IDs not assigned correctly")
39+ }
40+ if plan.Tasks[0].Status != "open" {
41+ t.Errorf("Expected task status 'open', got '%s'", plan.Tasks[0].Status)
42+ }
43+
44+ // Check message
45+ if !strings.Contains(msg, "created with 3 tasks") {
46+ t.Errorf("Unexpected message: %s", msg)
47+ }
48+}
49+
50+func TestPlanStore_CreatePlan_AutoName(t *testing.T) {
51+ ClearAllPlansForTesting()
52+
53+ store := planStore
54+
55+ // Create plan with empty name
56+ plan, _, err := store.CreatePlan("", []PlanTaskInput{
57+ {Description: "Task 1"},
58+ })
59+
60+ if err != nil {
61+ t.Fatalf("CreatePlan with empty name failed: %v", err)
62+ }
63+ if plan == nil {
64+ t.Fatal("CreatePlan returned nil plan")
65+ }
66+ if !strings.HasPrefix(plan.Name, "user-request-") {
67+ t.Errorf("Expected auto-generated name starting with 'user-request-', got '%s'", plan.Name)
68+ }
69+
70+ // Create another, should have incremented name
71+ plan2, _, err := store.CreatePlan("", []PlanTaskInput{
72+ {Description: "Task A"},
73+ })
74+ if err != nil {
75+ t.Fatalf("CreatePlan second failed: %v", err)
76+ }
77+ if plan2.Name == plan.Name {
78+ t.Errorf("Expected unique names, got same name: %s", plan2.Name)
79+ }
80+}
81+
82+func TestPlanStore_CreatePlan_Duplicate(t *testing.T) {
83+ ClearAllPlansForTesting()
84+
85+ store := planStore
86+
87+ // Create first plan
88+ _, _, err := store.CreatePlan("duplicate-test", []PlanTaskInput{
89+ {Description: "Task 1"},
90+ })
91+ if err != nil {
92+ t.Fatalf("First CreatePlan failed: %v", err)
93+ }
94+
95+ // Try to create duplicate
96+ _, _, err = store.CreatePlan("duplicate-test", []PlanTaskInput{
97+ {Description: "Task 2"},
98+ })
99+ if err == nil {
100+ t.Error("Expected error for duplicate plan, got nil")
101+ }
102+ if !strings.Contains(err.Error(), "already exists") {
103+ t.Errorf("Expected 'already exists' in error, got: %v", err)
104+ }
105+}
106+
107+func TestPlanStore_GetPlan(t *testing.T) {
108+ ClearAllPlansForTesting()
109+
110+ store := planStore
111+
112+ // Create a plan
113+ _, _, err := store.CreatePlan("get-test", []PlanTaskInput{
114+ {Description: "Task 1"},
115+ })
116+ if err != nil {
117+ t.Fatalf("CreatePlan failed: %v", err)
118+ }
119+
120+ // Get the plan
121+ plan, _, err := store.GetPlan("get-test")
122+ if err != nil {
123+ t.Fatalf("GetPlan failed: %v", err)
124+ }
125+ if plan == nil {
126+ t.Fatal("GetPlan returned nil plan")
127+ }
128+ if plan.Name != "get-test" {
129+ t.Errorf("Expected name 'get-test', got '%s'", plan.Name)
130+ }
131+
132+ // Get non-existent plan
133+ _, _, err = store.GetPlan("non-existent")
134+ if err == nil {
135+ t.Error("Expected error for non-existent plan, got nil")
136+ }
137+}
138+
139+func TestPlanStore_UpdateTask(t *testing.T) {
140+ ClearAllPlansForTesting()
141+
142+ store := planStore
143+
144+ // Create a plan
145+ _, _, err := store.CreatePlan("update-test", []PlanTaskInput{
146+ {Description: "Task 1"},
147+ {Description: "Task 2"},
148+ })
149+ if err != nil {
150+ t.Fatalf("CreatePlan failed: %v", err)
151+ }
152+
153+ // Update task 1 to in_progress
154+ plan, _, err := store.UpdateTask("update-test", 1, "in_progress", "")
155+ if err != nil {
156+ t.Fatalf("UpdateTask failed: %v", err)
157+ }
158+ if plan.Tasks[0].Status != "in_progress" {
159+ t.Errorf("Expected status 'in_progress', got '%s'", plan.Tasks[0].Status)
160+ }
161+ if plan.Tasks[0].StartedAt == "" {
162+ t.Error("Expected StartedAt to be set")
163+ }
164+
165+ // Update task 1 to done
166+ plan, _, err = store.UpdateTask("update-test", 1, "done", "Task completed")
167+ if err != nil {
168+ t.Fatalf("UpdateTask to done failed: %v", err)
169+ }
170+ if plan.Tasks[0].Status != "done" {
171+ t.Errorf("Expected status 'done', got '%s'", plan.Tasks[0].Status)
172+ }
173+ if plan.Tasks[0].Notes != "Task completed" {
174+ t.Errorf("Expected notes 'Task completed', got '%s'", plan.Tasks[0].Notes)
175+ }
176+ if plan.Tasks[0].CompletedAt == "" {
177+ t.Error("Expected CompletedAt to be set")
178+ }
179+
180+ // Invalid status
181+ _, _, err = store.UpdateTask("update-test", 1, "invalid", "")
182+ if err == nil {
183+ t.Error("Expected error for invalid status, got nil")
184+ }
185+
186+ // Invalid task ID
187+ _, _, err = store.UpdateTask("update-test", 99, "done", "")
188+ if err == nil {
189+ t.Error("Expected error for invalid task ID, got nil")
190+ }
191+}
192+
193+func TestPlanStore_NextTask(t *testing.T) {
194+ ClearAllPlansForTesting()
195+
196+ store := planStore
197+
198+ // Create a plan with 3 tasks
199+ _, _, err := store.CreatePlan("next-test", []PlanTaskInput{
200+ {Description: "Task 1"},
201+ {Description: "Task 2"},
202+ {Description: "Task 3"},
203+ })
204+ if err != nil {
205+ t.Fatalf("CreatePlan failed: %v", err)
206+ }
207+
208+ // First next - should go to task 1 (auto marked as in_progress)
209+ plan, nextTask, msg, err := store.NextTask("next-test")
210+ if err != nil {
211+ t.Fatalf("First NextTask failed: %v", err)
212+ }
213+ if plan.Current != 1 {
214+ t.Errorf("Expected Current=1, got %d", plan.Current)
215+ }
216+ if nextTask == nil {
217+ t.Fatal("Expected nextTask, got nil")
218+ }
219+ if nextTask.ID != 1 {
220+ t.Errorf("Expected nextTask.ID=1, got %d", nextTask.ID)
221+ }
222+ if nextTask.Status != "in_progress" {
223+ t.Errorf("Expected nextTask status 'in_progress', got '%s'", nextTask.Status)
224+ }
225+ if !strings.Contains(msg, "Next: Task 1") {
226+ t.Errorf("Unexpected message: %s", msg)
227+ }
228+
229+ // Second next - should mark task 1 as done, go to task 2
230+ plan, nextTask, msg, err = store.NextTask("next-test")
231+ if err != nil {
232+ t.Fatalf("Second NextTask failed: %v", err)
233+ }
234+ if plan.Current != 2 {
235+ t.Errorf("Expected Current=2, got %d", plan.Current)
236+ }
237+ if plan.Tasks[0].Status != "done" {
238+ t.Errorf("Expected Task 1 status 'done', got '%s'", plan.Tasks[0].Status)
239+ }
240+ if nextTask.ID != 2 {
241+ t.Errorf("Expected nextTask.ID=2, got %d", nextTask.ID)
242+ }
243+
244+ // Third next - should mark task 2 as done, go to task 3
245+ plan, nextTask, msg, err = store.NextTask("next-test")
246+ if err != nil {
247+ t.Fatalf("Third NextTask failed: %v", err)
248+ }
249+ if plan.Current != 3 {
250+ t.Errorf("Expected Current=3, got %d", plan.Current)
251+ }
252+ if plan.Tasks[1].Status != "done" {
253+ t.Errorf("Expected Task 2 status 'done', got '%s'", plan.Tasks[1].Status)
254+ }
255+ if nextTask.ID != 3 {
256+ t.Errorf("Expected nextTask.ID=3, got %d", nextTask.ID)
257+ }
258+
259+ // Fourth next - should complete the plan
260+ plan, nextTask, msg, err = store.NextTask("next-test")
261+ if err != nil {
262+ t.Fatalf("Fourth NextTask failed: %v", err)
263+ }
264+ if plan.Current != 4 {
265+ t.Errorf("Expected Current=4, got %d", plan.Current)
266+ }
267+ if nextTask != nil {
268+ t.Error("Expected nextTask nil (plan complete), got non-nil")
269+ }
270+ if !strings.Contains(msg, "complete") {
271+ t.Errorf("Expected 'complete' in message: %s", msg)
272+ }
273+
274+ // Fifth next - should return already complete
275+ _, _, msg, err = store.NextTask("next-test")
276+ if err != nil {
277+ t.Fatalf("Fifth NextTask failed: %v", err)
278+ }
279+ if !strings.Contains(msg, "already complete") && !strings.Contains(msg, "is complete") {
280+ t.Errorf("Expected completion message: %s", msg)
281+ }
282+}
283+
284+func TestPlanStore_DeletePlan(t *testing.T) {
285+ ClearAllPlansForTesting()
286+
287+ store := planStore
288+
289+ // Create a plan
290+ _, _, err := store.CreatePlan("delete-test", []PlanTaskInput{
291+ {Description: "Task 1"},
292+ })
293+ if err != nil {
294+ t.Fatalf("CreatePlan failed: %v", err)
295+ }
296+
297+ // Delete the plan
298+ msg, err := store.DeletePlan("delete-test")
299+ if err != nil {
300+ t.Fatalf("DeletePlan failed: %v", err)
301+ }
302+ if !strings.Contains(msg, "deleted") {
303+ t.Errorf("Expected 'deleted' in message: %s", msg)
304+ }
305+
306+ // Try to get deleted plan
307+ _, _, err = store.GetPlan("delete-test")
308+ if err == nil {
309+ t.Error("Expected error for deleted plan, got nil")
310+ }
311+
312+ // Delete non-existent plan
313+ _, err = store.DeletePlan("non-existent")
314+ if err == nil {
315+ t.Error("Expected error for non-existent plan, got nil")
316+ }
317+}
318+
319+func TestPlanStore_GetStatus(t *testing.T) {
320+ ClearAllPlansForTesting()
321+
322+ store := planStore
323+
324+ // Create a plan
325+ _, _, err := store.CreatePlan("status-test", []PlanTaskInput{
326+ {Description: "Task 1"},
327+ {Description: "Task 2"},
328+ {Description: "Task 3"},
329+ })
330+ if err != nil {
331+ t.Fatalf("CreatePlan failed: %v", err)
332+ }
333+
334+ // Get status before starting
335+ statusLine, msg, err := store.GetStatus("status-test")
336+ if err != nil {
337+ t.Fatalf("GetStatus failed: %v", err)
338+ }
339+ if !strings.Contains(statusLine, "[1/3]") {
340+ t.Errorf("Expected '[1/3]' in status, got: %s", statusLine)
341+ }
342+ if !strings.Contains(statusLine, "Task 1") {
343+ t.Errorf("Expected 'Task 1' in status, got: %s", statusLine)
344+ }
345+
346+ // Move to task 1 (first NextTask starts Task 1)
347+ _, _, _, err = store.NextTask("status-test")
348+ if err != nil {
349+ t.Fatalf("NextTask failed: %v", err)
350+ }
351+
352+ // Get status at task 1
353+ statusLine, _, err = store.GetStatus("status-test")
354+ if err != nil {
355+ t.Fatalf("GetStatus failed: %v", err)
356+ }
357+ if !strings.Contains(statusLine, "[1/3]") {
358+ t.Errorf("Expected '[1/3]' in status, got: %s", statusLine)
359+ }
360+ if !strings.Contains(statusLine, "in_progress") {
361+ t.Errorf("Expected 'in_progress' in status, got: %s", statusLine)
362+ }
363+
364+ // Complete all tasks (need 3 more NextTask calls to complete all 3 tasks)
365+ store.NextTask("status-test") // Marks Task 1 as done, moves to Task 2
366+ store.NextTask("status-test") // Marks Task 2 as done, moves to Task 3
367+ store.NextTask("status-test") // Marks Task 3 as done, moves to Current=4 (complete)
368+
369+ // Get status when complete
370+ statusLine, _, err = store.GetStatus("status-test")
371+ if err != nil {
372+ t.Fatalf("GetStatus failed: %v", err)
373+ }
374+ if !strings.Contains(statusLine, "All tasks complete") {
375+ t.Errorf("Expected 'All tasks complete' in status, got: %s", statusLine)
376+ }
377+
378+ _ = msg // Used above
379+}
380+
381+func TestPlanStore_ListPlans(t *testing.T) {
382+ ClearAllPlansForTesting()
383+
384+ store := planStore
385+
386+ // List with no plans
387+ plans, msg, err := store.ListPlans()
388+ if err != nil {
389+ t.Fatalf("ListPlans with no plans failed: %v", err)
390+ }
391+ if len(plans) != 0 {
392+ t.Errorf("Expected 0 plans, got %d", len(plans))
393+ }
394+ if !strings.Contains(msg, "No active plans") {
395+ t.Errorf("Expected 'No active plans' in message: %s", msg)
396+ }
397+
398+ // Create multiple plans
399+ store.CreatePlan("plan-a", []PlanTaskInput{{Description: "A1"}})
400+ store.CreatePlan("plan-b", []PlanTaskInput{{Description: "B1"}})
401+ store.CreatePlan("plan-c", []PlanTaskInput{{Description: "C1"}})
402+
403+ // List plans
404+ plans, msg, err = store.ListPlans()
405+ if err != nil {
406+ t.Fatalf("ListPlans failed: %v", err)
407+ }
408+ if len(plans) != 3 {
409+ t.Errorf("Expected 3 plans, got %d", len(plans))
410+ }
411+ if !strings.Contains(msg, "3 plan(s)") {
412+ t.Errorf("Expected '3 plan(s)' in message: %s", msg)
413+ }
414+}
415+
416+func TestPlanStore_ClearPlans(t *testing.T) {
417+ ClearAllPlansForTesting()
418+
419+ store := planStore
420+
421+ // Create some plans
422+ store.CreatePlan("clear-test-1", []PlanTaskInput{{Description: "T1"}})
423+ store.CreatePlan("clear-test-2", []PlanTaskInput{{Description: "T2"}})
424+
425+ // Verify plans exist
426+ plans, _, _ := store.ListPlans()
427+ if len(plans) != 2 {
428+ t.Errorf("Expected 2 plans, got %d", len(plans))
429+ }
430+
431+ // Clear plans
432+ store.ClearPlans()
433+
434+ // Verify plans cleared
435+ plans, _, _ = store.ListPlans()
436+ if len(plans) != 0 {
437+ t.Errorf("Expected 0 plans after clear, got %d", len(plans))
438+ }
439+}
440+
441+func TestPlanStore_ClearAllPlans(t *testing.T) {
442+ ClearAllPlansForTesting()
443+
444+ store := planStore
445+
446+ // Create some plans
447+ store.CreatePlan("all-clear-1", []PlanTaskInput{{Description: "T1"}})
448+ store.CreatePlan("all-clear-2", []PlanTaskInput{{Description: "T2"}})
449+
450+ // Clear all
451+ store.ClearAllPlans()
452+
453+ // Verify all cleared
454+ plans, _, _ := store.ListPlans()
455+ if len(plans) != 0 {
456+ t.Errorf("Expected 0 plans after ClearAllPlans, got %d", len(plans))
457+ }
458+}
459+
460+func TestPlanTool_Integration(t *testing.T) {
461+ // Skip tool integration test - requires full Genkit setup
462+ // The store tests above verify the core functionality
463+ t.Skip("Skipping tool integration test - requires full Genkit setup")
464+}
465+
466+func TestPlanInput_JSON(t *testing.T) {
467+ // Test that input can be serialized/deserialized
468+ input := planInput{
469+ Action: "create",
470+ Name: "test",
471+ Tasks: []PlanTaskInput{
472+ {ID: 1, Description: "Task 1"},
473+ {ID: 2, Description: "Task 2"},
474+ },
475+ }
476+
477+ // Marshal to JSON
478+ data, err := json.Marshal(input)
479+ if err != nil {
480+ t.Fatalf("Failed to marshal input: %v", err)
481+ }
482+
483+ // Unmarshal back
484+ var decoded planInput
485+ if err := json.Unmarshal(data, &decoded); err != nil {
486+ t.Fatalf("Failed to unmarshal input: %v", err)
487+ }
488+
489+ if decoded.Action != input.Action {
490+ t.Errorf("Action mismatch: expected '%s', got '%s'", input.Action, decoded.Action)
491+ }
492+ if decoded.Name != input.Name {
493+ t.Errorf("Name mismatch: expected '%s', got '%s'", input.Name, decoded.Name)
494+ }
495+ if len(decoded.Tasks) != len(input.Tasks) {
496+ t.Errorf("Tasks length mismatch: expected %d, got %d", len(input.Tasks), len(decoded.Tasks))
497+ }
498+ if decoded.Tasks[0].Description != input.Tasks[0].Description {
499+ t.Errorf("Task description mismatch")
500+ }
501+}
502+
503+func TestPlanOutput_JSON(t *testing.T) {
504+ // Test that output can be serialized
505+ plan := &Plan{
506+ Name: "test",
507+ Tasks: []PlanTask{
508+ {ID: 1, Description: "Task 1", Status: "done"},
509+ },
510+ Current: 1,
511+ }
512+
513+ output := PlanOutput{
514+ Result: "success",
515+ PlanName: "test",
516+ Plan: plan,
517+ Current: 1,
518+ Total: 1,
519+ }
520+
521+ // Marshal to JSON
522+ data, err := json.Marshal(output)
523+ if err != nil {
524+ t.Fatalf("Failed to marshal output: %v", err)
525+ }
526+
527+ // Verify it contains expected fields
528+ str := string(data)
529+ if !strings.Contains(str, `"result":"success"`) {
530+ t.Errorf("Output missing result field: %s", str)
531+ }
532+ if !strings.Contains(str, `"planName":"test"`) {
533+ t.Errorf("Output missing planName field: %s", str)
534+ }
535+}
536+
537+// Test with actual tool call via genkit (if possible in test environment)
538+func TestPlanTool_CreateAndStatus(t *testing.T) {
539+ // Skip tool integration test - requires full Genkit setup
540+ // The store tests above verify the core functionality
541+ t.Skip("Skipping tool integration test - requires full Genkit setup")
542+}
modified main.go +4 -0
@@ -233,6 +233,10 @@ func assembleTools(e *engine.Engine, d *detector.LoopDetector) toolSet {
233233 tools.EditFile(e.G, d))
234234 set.names = append(set.names, "read_file", "write_file", "edit_file")
235235 }
236+ if config.Cfg.PlanTool {
237+ set.tools = append(set.tools, tools.PlanTool(e.G, d))
238+ set.names = append(set.names, "plan")
239+ }
236240 assembleLSP(e, d, &set)
237241 return set
238242 }
@@ -233,6 +233,10 @@ func assembleTools(e *engine.Engine, d *detector.LoopDetector) toolSet {
233 tools.EditFile(e.G, d))233 tools.EditFile(e.G, d))
234 set.names = append(set.names, "read_file", "write_file", "edit_file")234 set.names = append(set.names, "read_file", "write_file", "edit_file")
235 }235 }
236+ if config.Cfg.PlanTool {
237+ set.tools = append(set.tools, tools.PlanTool(e.G, d))
238+ set.names = append(set.names, "plan")
239+ }
236 assembleLSP(e, d, &set)240 assembleLSP(e, d, &set)
237 return set241 return set
238 }242 }
added skills/plan-manager/SKILL.md +472 -0
new file mode 100644
@@ -0,0 +1,472 @@
1+---
2+name: plan-manager
3+description: Manage multi-step work as a plan. When the user gives you a list of tasks to do, or you decide to break work into steps, use the plan tool to create and track a todo list. This ensures the user can see your progress and you don't lose track of what remains to be done.
4+keywords: plan, todo, tasks, steps, first, second, third, then, after that, next, do this, do these, i need, i want, following, steps are, task list, to do
5+---
6+# Plan Manager
7+
8+## What This Skill Does
9+
10+This skill teaches you how to **detect, create, and manage plans** when users give you multi-step requests.
11+
12+A **plan** is a structured way to track progress through multiple tasks. When you create a plan, you:
13+1. Store the list of tasks the user wants done
14+2. Track which task you're currently working on
15+3. Update the status of each task as you complete it
16+4. Report progress to the user with clear status messages
17+
18+## When to Use This Skill
19+
20+**ALWAYS** create and use a plan when:
21+
22+1. **User gives you a numbered list:**
23+ - "Do these 3 things: 1. X, 2. Y, 3. Z"
24+ - "Please: 1. First, 2. Second, 3. Third"
25+ - "My plan is: 1. Step one, 2. Step two"
26+
27+2. **User gives you a comma-separated or bullet list of actions:**
28+ - "I need you to fix A, then update B, and finally test C"
29+ - "Please handle: - A, - B, - C"
30+
31+3. **User explicitly says they have a plan:**
32+ - "Here's my plan: ..."
33+ - "My plan is to..."
34+ - "I want to do X, Y, and Z"
35+
36+4. **You decide to break complex work into steps:**
37+ - When a single request requires multiple distinct actions
38+ - When you need to track progress for a multi-part task
39+
40+**NEVER** use the plan tool for:
41+- Single, simple requests (just do it directly)
42+- Conversational questions with no action required
43+- Greetings or social interactions
44+- Requests that don't involve doing something
45+
46+## The Plan Tool
47+
48+The `plan` tool is a builtin tool available to you. It supports these **actions**:
49+
50+### Action: create
51+
52+**Purpose:** Create a new plan with a list of tasks.
53+
54+**Required parameters:**
55+- `tasks`: Array of task objects, each with `description` (string)
56+
57+**Optional parameters:**
58+- `name`: Plan name (string). If not provided, auto-generated as "user-request-N"
59+
60+**Example:**
61+```json
62+{
63+ "action": "create",
64+ "name": "setup-project",
65+ "tasks": [
66+ {"description": "Initialize the repository"},
67+ {"description": "Create the main file"},
68+ {"description": "Add configuration"}
69+ ]
70+}
71+```
72+
73+**What happens:**
74+- A new plan is created with the given tasks
75+- Each task gets an auto-incrementing ID (1, 2, 3...)
76+- All tasks start with status "open"
77+- The plan is stored for this session
78+
79+**What you should do:**
80+- Call `plan` with `action: "create"` as soon as you detect a multi-step request
81+- Then immediately call `plan` with `action: "update"` to mark the first task as "in_progress"
82+- Tell the user: "I'll handle this in N steps. Starting with: [first task description]"
83+
84+---
85+
86+### Action: update
87+
88+**Purpose:** Update the status of a specific task.
89+
90+**Required parameters:**
91+- `name`: Plan name
92+- `taskId`: The task ID to update (1-based)
93+- `status`: New status. One of: `open`, `in_progress`, `done`, `failed`
94+
95+**Optional parameters:**
96+- `message`: A note about this status change (e.g., error message for failed)
97+
98+**Example:**
99+```json
100+{
101+ "action": "update",
102+ "name": "setup-project",
103+ "taskId": 1,
104+ "status": "in_progress"
105+}
106+```
107+
108+**When to use each status:**
109+- `in_progress`: When you start working on a task
110+- `done`: When you successfully complete a task
111+- `failed`: When a task cannot be completed (include error in message)
112+- `open`: To reset a task to not-started (rarely needed)
113+
114+---
115+
116+### Action: next
117+
118+**Purpose:** Move to the next task, automatically marking the current task as done.
119+
120+**Required parameters:**
121+- `name`: Plan name
122+
123+**Example:**
124+```json
125+{
126+ "action": "next",
127+ "name": "setup-project"
128+}
129+```
130+
131+**What happens:**
132+- Current task is marked as "done" with completion timestamp
133+- Plan's current pointer moves to the next task
134+- Next task is automatically set to "in_progress"
135+- Returns info about the next task
136+
137+**What you should do:**
138+- Call `next` after completing a task
139+- Use the returned next task info to continue your work
140+- Include the status line in your response to the user
141+
142+---
143+
144+### Action: get
145+
146+**Purpose:** Retrieve the full details of a plan.
147+
148+**Required parameters:**
149+- `name`: Plan name
150+
151+**Example:**
152+```json
153+{
154+ "action": "get",
155+ "name": "setup-project"
156+}
157+```
158+
159+**What you should do:**
160+- Use this when you need to see all tasks in a plan
161+- Rarely needed in normal workflow
162+
163+---
164+
165+### Action: list
166+
167+**Purpose:** List all active plans for this session.
168+
169+**Example:**
170+```json
171+{
172+ "action": "list"
173+}
174+```
175+
176+**What you should do:**
177+- Use this when the user asks "What plans do I have?"
178+- Returns names of all active plans
179+
180+---
181+
182+### Action: delete
183+
184+**Purpose:** Delete a plan.
185+
186+**Required parameters:**
187+- `name`: Plan name
188+
189+**Example:**
190+```json
191+{
192+ "action": "delete",
193+ "name": "old-plan"
194+}
195+```
196+
197+**What you should do:**
198+- Use when user asks to abandon a plan
199+- Confirm with user before deleting
200+
201+---
202+
203+### Action: status
204+
205+**Purpose:** Get a compact status line for display.
206+
207+**Required parameters:**
208+- `name`: Plan name
209+
210+**Example:**
211+```json
212+{
213+ "action": "status",
214+ "name": "setup-project"
215+}
216+```
217+
218+**What it returns:**
219+```
220+[2/5] Task 2: Create the main file (in_progress)
221+```
222+
223+**What you should do:**
224+- **ALWAYS** call `plan(action="status")` at the end of your response when a plan is active
225+- Include the status line in your response so the user can see progress
226+- The status line format: `[current/total] icon Task N: description (status)`
227+
228+---
229+
230+## Complete Workflow Example
231+
232+**User:** "I need you to set up a new project. Do these 3 things: 1. Create the directory structure, 2. Initialize git, 3. Create a README file"
233+
234+**Your internal process:**
235+
236+1. **Detect the plan:** User gave a numbered list
237+
238+2. **Create the plan:**
239+ ```json
240+ plan(action="create", name="setup-project", tasks=[
241+ {description: "Create the directory structure"},
242+ {description: "Initialize git"},
243+ {description: "Create a README file"}
244+ ])
245+ ```
246+ Tool returns: `{"result":"Plan 'setup-project' created with 3 tasks. Current: Task 1", ...}`
247+
248+3. **Start first task:**
249+ ```json
250+ plan(action="update", name="setup-project", taskId=1, status="in_progress")
251+ ```
252+ Tool returns: `{"result":"Task 1 in plan 'setup-project' updated to 'in_progress'"}`
253+
254+4. **Do the work:** Use bash or other tools to create directory structure
255+ ```json
256+ bash(command: "mkdir -p project/src project/tests project/docs")
257+ ```
258+
259+5. **Mark first task done:**
260+ ```json
261+ plan(action="update", name="setup-project", taskId=1, status="done")
262+ ```
263+
264+6. **Move to next task:**
265+ ```json
266+ plan(action="next", name="setup-project")
267+ ```
268+ Tool returns: `{"result":"Task 1 done. Next: Task 2 - Initialize git", ...}`
269+
270+7. **Get status for display:**
271+ ```json
272+ plan(action="status", name="setup-project")
273+ ```
274+ Tool returns: `{"result":"Plan 'setup-project': 1/3 tasks complete", "status":"[2/3] Task 2: Initialize git (in_progress)"}`
275+
276+8. **Respond to user:**
277+ ```
278+ Directory structure created.
279+
280+ 📋 [2/3] Task 2: Initialize git (in_progress)
281+ ```
282+
283+9. **Continue with task 2:** Initialize git, then call `next` again
284+
285+10. **Continue with task 3:** Create README, then call `next` again
286+
287+11. **Plan complete:**
288+ ```
289+ All done! Project set up with:
290+ - Directory structure created
291+ - Git initialized
292+ - README file created
293+
294+ 📋 [3/3] All tasks complete! ✅
295+ ```
296+
297+---
298+
299+## Handling User Interruptions
300+
301+### User asks about the plan
302+
303+**User:** "What's the status of my plan?"
304+
305+**You:**
306+1. Call: `plan(action="status", name="setup-project")`
307+2. Use the returned status line in your response
308+
309+**Response:**
310+```
311+Current plan: [2/3] Task 2: Initialize git (in_progress). Next: Create a README file
312+```
313+
314+---
315+
316+### User wants to skip a task
317+
318+**User:** "Skip the git initialization, just do the README"
319+
320+**You:**
321+1. Mark current task as done: `plan(action="update", name="setup-project", taskId=2, status="done")`
322+2. Call next: `plan(action="next", name="setup-project")`
323+3. Confirm to user: "Skipping git initialization. Creating README file..."
324+
325+---
326+
327+### User wants to abandon the plan
328+
329+**User:** "Never mind, I don't need this anymore"
330+
331+**You:**
332+1. Ask for confirmation: "Are you sure you want to abandon the plan? We've completed 1 of 3 tasks."
333+2. If user confirms: `plan(action="delete", name="setup-project")`
334+3. Respond: "Plan abandoned. All progress is lost."
335+
336+---
337+
338+## Multiple Plans
339+
340+A session can have multiple active plans. Use different names for each.
341+
342+**Example:**
343+- User: "First, fix the bug in the auth module. Then, update the documentation."
344+- You: Create plan "fix-auth-bug" with 3 tasks
345+- User (later): "Also, refactor the API"
346+- You: Create plan "refactor-api" with 2 tasks
347+
348+To list all plans: `plan(action="list")`
349+
350+---
351+
352+## Status Line Format
353+
354+Always show the plan status in this exact format:
355+
356+```
357+📋 [current/total] icon Task N: description (status)
358+```
359+
360+**Icons:**
361+- ✅ = done
362+- 🔄 = in_progress
363+- ❌ = failed
364+- ⏳ = open (not started)
365+
366+**Examples:**
367+- `📋 [1/5] ⏳ Task 1: Analyze code (open)`
368+- `📋 [2/5] 🔄 Task 2: Fix the bugs (in_progress)`
369+- `📋 [5/5] ✅ All tasks complete!`
370+- `📋 [3/5] ❌ Task 3: Run tests (failed: connection timeout)`
371+
372+---
373+
374+## Important Rules
375+
376+1. **Always create a plan for multi-step requests** - Don't just say "I'll do it" and then forget what was requested
377+
378+2. **Always show the status line** - Include `plan(action="status")` result in every response when a plan is active
379+
380+3. **Always update task status** - Call `update` when starting and completing each task
381+
382+4. **Use meaningful plan names** - Auto-generated names are fine, but if the user gives a clear purpose, use it
383+
384+5. **Keep plans focused** - One plan per distinct request. Don't mix unrelated tasks in one plan
385+
386+6. **Handle errors gracefully** - If a task fails, mark it as failed with a note, then continue or ask the user
387+
388+7. **Clean up on /new** - Plans are automatically cleared when the user starts a new session
389+
390+---
391+
392+## Common Patterns
393+
394+### Pattern: Sequential tasks with tool calls
395+
396+```
397+User: "Fix these issues: 1. Add error handling, 2. Write tests"
398+
399+You:
400+1. plan(create, tasks=[...])
401+2. plan(update, taskId=1, status=in_progress)
402+3. edit_file(...) // or bash or other tools
403+4. plan(update, taskId=1, status=done)
404+5. plan(next)
405+6. plan(update, taskId=2, status=in_progress)
406+7. write_file(...)
407+8. plan(update, taskId=2, status=done)
408+9. plan(next) // Triggers completion message
409+10. Respond with status line
410+```
411+
412+---
413+
414+### Pattern: User gives unordered list
415+
416+**User:** "I need A, B, and C done"
417+
418+**You:** Convert to ordered tasks:
419+```json
420+{
421+ "action": "create",
422+ "tasks": [
423+ {"description": "Do A"},
424+ {"description": "Do B"},
425+ {"description": "Do C"}
426+ ]
427+}
428+```
429+
430+---
431+
432+### Pattern: Complex task requiring subtasks
433+
434+If a single task from the user's list requires multiple steps, you can:
435+- Option A: Add it as a single task and just do it
436+- Option B: Create a nested plan (advanced, not required for v1)
437+
438+For v1, use Option A - keep it simple.
439+
440+---
441+
442+## What NOT to Do
443+
444+❌ **Don't ignore numbered lists** - Always create a plan
445+❌ **Don't forget to update status** - Track progress accurately
446+❌ **Don't create plans for single tasks** - Overkill for simple requests
447+❌ **Don't make up tasks** - Only create tasks the user actually requested
448+❌ **Don't delete plans without asking** - Always confirm with user
449+
450+---
451+
452+## Testing Your Understanding
453+
454+Read these user messages and decide if you should create a plan:
455+
456+| User Message | Create Plan? | Why |
457+|--------------|-------------|-----|
458+| "Write a function to sort an array" | No | Single task |
459+| "Fix these 3 bugs in the code" | Yes | Multi-step, but needs clarification |
460+| "1. Fix bug A, 2. Fix bug B, 3. Test both" | Yes | Explicit numbered list |
461+| "Hello, how are you?" | No | Not a task |
462+| "What's the capital of France?" | No | Question, not a task |
463+| "Set up the project: create dirs, init git, add README" | Yes | Comma-separated tasks |
464+| "I need the documentation updated and the tests fixed" | Yes | Multiple distinct tasks |
465+
466+---
467+
468+## Summary
469+
470+**Remember:** When the user gives you a list of things to do, **CREATE A PLAN**. Then **TRACK PROGRESS**. Then **REPORT STATUS** to the user.
471+
472+This makes you more reliable, transparent, and easier to work with.
new file mode 100644
@@ -0,0 +1,472 @@
1+---
2+name: plan-manager
3+description: Manage multi-step work as a plan. When the user gives you a list of tasks to do, or you decide to break work into steps, use the plan tool to create and track a todo list. This ensures the user can see your progress and you don't lose track of what remains to be done.
4+keywords: plan, todo, tasks, steps, first, second, third, then, after that, next, do this, do these, i need, i want, following, steps are, task list, to do
5+---
6+# Plan Manager
7+
8+## What This Skill Does
9+
10+This skill teaches you how to **detect, create, and manage plans** when users give you multi-step requests.
11+
12+A **plan** is a structured way to track progress through multiple tasks. When you create a plan, you:
13+1. Store the list of tasks the user wants done
14+2. Track which task you're currently working on
15+3. Update the status of each task as you complete it
16+4. Report progress to the user with clear status messages
17+
18+## When to Use This Skill
19+
20+**ALWAYS** create and use a plan when:
21+
22+1. **User gives you a numbered list:**
23+ - "Do these 3 things: 1. X, 2. Y, 3. Z"
24+ - "Please: 1. First, 2. Second, 3. Third"
25+ - "My plan is: 1. Step one, 2. Step two"
26+
27+2. **User gives you a comma-separated or bullet list of actions:**
28+ - "I need you to fix A, then update B, and finally test C"
29+ - "Please handle: - A, - B, - C"
30+
31+3. **User explicitly says they have a plan:**
32+ - "Here's my plan: ..."
33+ - "My plan is to..."
34+ - "I want to do X, Y, and Z"
35+
36+4. **You decide to break complex work into steps:**
37+ - When a single request requires multiple distinct actions
38+ - When you need to track progress for a multi-part task
39+
40+**NEVER** use the plan tool for:
41+- Single, simple requests (just do it directly)
42+- Conversational questions with no action required
43+- Greetings or social interactions
44+- Requests that don't involve doing something
45+
46+## The Plan Tool
47+
48+The `plan` tool is a builtin tool available to you. It supports these **actions**:
49+
50+### Action: create
51+
52+**Purpose:** Create a new plan with a list of tasks.
53+
54+**Required parameters:**
55+- `tasks`: Array of task objects, each with `description` (string)
56+
57+**Optional parameters:**
58+- `name`: Plan name (string). If not provided, auto-generated as "user-request-N"
59+
60+**Example:**
61+```json
62+{
63+ "action": "create",
64+ "name": "setup-project",
65+ "tasks": [
66+ {"description": "Initialize the repository"},
67+ {"description": "Create the main file"},
68+ {"description": "Add configuration"}
69+ ]
70+}
71+```
72+
73+**What happens:**
74+- A new plan is created with the given tasks
75+- Each task gets an auto-incrementing ID (1, 2, 3...)
76+- All tasks start with status "open"
77+- The plan is stored for this session
78+
79+**What you should do:**
80+- Call `plan` with `action: "create"` as soon as you detect a multi-step request
81+- Then immediately call `plan` with `action: "update"` to mark the first task as "in_progress"
82+- Tell the user: "I'll handle this in N steps. Starting with: [first task description]"
83+
84+---
85+
86+### Action: update
87+
88+**Purpose:** Update the status of a specific task.
89+
90+**Required parameters:**
91+- `name`: Plan name
92+- `taskId`: The task ID to update (1-based)
93+- `status`: New status. One of: `open`, `in_progress`, `done`, `failed`
94+
95+**Optional parameters:**
96+- `message`: A note about this status change (e.g., error message for failed)
97+
98+**Example:**
99+```json
100+{
101+ "action": "update",
102+ "name": "setup-project",
103+ "taskId": 1,
104+ "status": "in_progress"
105+}
106+```
107+
108+**When to use each status:**
109+- `in_progress`: When you start working on a task
110+- `done`: When you successfully complete a task
111+- `failed`: When a task cannot be completed (include error in message)
112+- `open`: To reset a task to not-started (rarely needed)
113+
114+---
115+
116+### Action: next
117+
118+**Purpose:** Move to the next task, automatically marking the current task as done.
119+
120+**Required parameters:**
121+- `name`: Plan name
122+
123+**Example:**
124+```json
125+{
126+ "action": "next",
127+ "name": "setup-project"
128+}
129+```
130+
131+**What happens:**
132+- Current task is marked as "done" with completion timestamp
133+- Plan's current pointer moves to the next task
134+- Next task is automatically set to "in_progress"
135+- Returns info about the next task
136+
137+**What you should do:**
138+- Call `next` after completing a task
139+- Use the returned next task info to continue your work
140+- Include the status line in your response to the user
141+
142+---
143+
144+### Action: get
145+
146+**Purpose:** Retrieve the full details of a plan.
147+
148+**Required parameters:**
149+- `name`: Plan name
150+
151+**Example:**
152+```json
153+{
154+ "action": "get",
155+ "name": "setup-project"
156+}
157+```
158+
159+**What you should do:**
160+- Use this when you need to see all tasks in a plan
161+- Rarely needed in normal workflow
162+
163+---
164+
165+### Action: list
166+
167+**Purpose:** List all active plans for this session.
168+
169+**Example:**
170+```json
171+{
172+ "action": "list"
173+}
174+```
175+
176+**What you should do:**
177+- Use this when the user asks "What plans do I have?"
178+- Returns names of all active plans
179+
180+---
181+
182+### Action: delete
183+
184+**Purpose:** Delete a plan.
185+
186+**Required parameters:**
187+- `name`: Plan name
188+
189+**Example:**
190+```json
191+{
192+ "action": "delete",
193+ "name": "old-plan"
194+}
195+```
196+
197+**What you should do:**
198+- Use when user asks to abandon a plan
199+- Confirm with user before deleting
200+
201+---
202+
203+### Action: status
204+
205+**Purpose:** Get a compact status line for display.
206+
207+**Required parameters:**
208+- `name`: Plan name
209+
210+**Example:**
211+```json
212+{
213+ "action": "status",
214+ "name": "setup-project"
215+}
216+```
217+
218+**What it returns:**
219+```
220+[2/5] Task 2: Create the main file (in_progress)
221+```
222+
223+**What you should do:**
224+- **ALWAYS** call `plan(action="status")` at the end of your response when a plan is active
225+- Include the status line in your response so the user can see progress
226+- The status line format: `[current/total] icon Task N: description (status)`
227+
228+---
229+
230+## Complete Workflow Example
231+
232+**User:** "I need you to set up a new project. Do these 3 things: 1. Create the directory structure, 2. Initialize git, 3. Create a README file"
233+
234+**Your internal process:**
235+
236+1. **Detect the plan:** User gave a numbered list
237+
238+2. **Create the plan:**
239+ ```json
240+ plan(action="create", name="setup-project", tasks=[
241+ {description: "Create the directory structure"},
242+ {description: "Initialize git"},
243+ {description: "Create a README file"}
244+ ])
245+ ```
246+ Tool returns: `{"result":"Plan 'setup-project' created with 3 tasks. Current: Task 1", ...}`
247+
248+3. **Start first task:**
249+ ```json
250+ plan(action="update", name="setup-project", taskId=1, status="in_progress")
251+ ```
252+ Tool returns: `{"result":"Task 1 in plan 'setup-project' updated to 'in_progress'"}`
253+
254+4. **Do the work:** Use bash or other tools to create directory structure
255+ ```json
256+ bash(command: "mkdir -p project/src project/tests project/docs")
257+ ```
258+
259+5. **Mark first task done:**
260+ ```json
261+ plan(action="update", name="setup-project", taskId=1, status="done")
262+ ```
263+
264+6. **Move to next task:**
265+ ```json
266+ plan(action="next", name="setup-project")
267+ ```
268+ Tool returns: `{"result":"Task 1 done. Next: Task 2 - Initialize git", ...}`
269+
270+7. **Get status for display:**
271+ ```json
272+ plan(action="status", name="setup-project")
273+ ```
274+ Tool returns: `{"result":"Plan 'setup-project': 1/3 tasks complete", "status":"[2/3] Task 2: Initialize git (in_progress)"}`
275+
276+8. **Respond to user:**
277+ ```
278+ Directory structure created.
279+
280+ 📋 [2/3] Task 2: Initialize git (in_progress)
281+ ```
282+
283+9. **Continue with task 2:** Initialize git, then call `next` again
284+
285+10. **Continue with task 3:** Create README, then call `next` again
286+
287+11. **Plan complete:**
288+ ```
289+ All done! Project set up with:
290+ - Directory structure created
291+ - Git initialized
292+ - README file created
293+
294+ 📋 [3/3] All tasks complete! ✅
295+ ```
296+
297+---
298+
299+## Handling User Interruptions
300+
301+### User asks about the plan
302+
303+**User:** "What's the status of my plan?"
304+
305+**You:**
306+1. Call: `plan(action="status", name="setup-project")`
307+2. Use the returned status line in your response
308+
309+**Response:**
310+```
311+Current plan: [2/3] Task 2: Initialize git (in_progress). Next: Create a README file
312+```
313+
314+---
315+
316+### User wants to skip a task
317+
318+**User:** "Skip the git initialization, just do the README"
319+
320+**You:**
321+1. Mark current task as done: `plan(action="update", name="setup-project", taskId=2, status="done")`
322+2. Call next: `plan(action="next", name="setup-project")`
323+3. Confirm to user: "Skipping git initialization. Creating README file..."
324+
325+---
326+
327+### User wants to abandon the plan
328+
329+**User:** "Never mind, I don't need this anymore"
330+
331+**You:**
332+1. Ask for confirmation: "Are you sure you want to abandon the plan? We've completed 1 of 3 tasks."
333+2. If user confirms: `plan(action="delete", name="setup-project")`
334+3. Respond: "Plan abandoned. All progress is lost."
335+
336+---
337+
338+## Multiple Plans
339+
340+A session can have multiple active plans. Use different names for each.
341+
342+**Example:**
343+- User: "First, fix the bug in the auth module. Then, update the documentation."
344+- You: Create plan "fix-auth-bug" with 3 tasks
345+- User (later): "Also, refactor the API"
346+- You: Create plan "refactor-api" with 2 tasks
347+
348+To list all plans: `plan(action="list")`
349+
350+---
351+
352+## Status Line Format
353+
354+Always show the plan status in this exact format:
355+
356+```
357+📋 [current/total] icon Task N: description (status)
358+```
359+
360+**Icons:**
361+- ✅ = done
362+- 🔄 = in_progress
363+- ❌ = failed
364+- ⏳ = open (not started)
365+
366+**Examples:**
367+- `📋 [1/5] ⏳ Task 1: Analyze code (open)`
368+- `📋 [2/5] 🔄 Task 2: Fix the bugs (in_progress)`
369+- `📋 [5/5] ✅ All tasks complete!`
370+- `📋 [3/5] ❌ Task 3: Run tests (failed: connection timeout)`
371+
372+---
373+
374+## Important Rules
375+
376+1. **Always create a plan for multi-step requests** - Don't just say "I'll do it" and then forget what was requested
377+
378+2. **Always show the status line** - Include `plan(action="status")` result in every response when a plan is active
379+
380+3. **Always update task status** - Call `update` when starting and completing each task
381+
382+4. **Use meaningful plan names** - Auto-generated names are fine, but if the user gives a clear purpose, use it
383+
384+5. **Keep plans focused** - One plan per distinct request. Don't mix unrelated tasks in one plan
385+
386+6. **Handle errors gracefully** - If a task fails, mark it as failed with a note, then continue or ask the user
387+
388+7. **Clean up on /new** - Plans are automatically cleared when the user starts a new session
389+
390+---
391+
392+## Common Patterns
393+
394+### Pattern: Sequential tasks with tool calls
395+
396+```
397+User: "Fix these issues: 1. Add error handling, 2. Write tests"
398+
399+You:
400+1. plan(create, tasks=[...])
401+2. plan(update, taskId=1, status=in_progress)
402+3. edit_file(...) // or bash or other tools
403+4. plan(update, taskId=1, status=done)
404+5. plan(next)
405+6. plan(update, taskId=2, status=in_progress)
406+7. write_file(...)
407+8. plan(update, taskId=2, status=done)
408+9. plan(next) // Triggers completion message
409+10. Respond with status line
410+```
411+
412+---
413+
414+### Pattern: User gives unordered list
415+
416+**User:** "I need A, B, and C done"
417+
418+**You:** Convert to ordered tasks:
419+```json
420+{
421+ "action": "create",
422+ "tasks": [
423+ {"description": "Do A"},
424+ {"description": "Do B"},
425+ {"description": "Do C"}
426+ ]
427+}
428+```
429+
430+---
431+
432+### Pattern: Complex task requiring subtasks
433+
434+If a single task from the user's list requires multiple steps, you can:
435+- Option A: Add it as a single task and just do it
436+- Option B: Create a nested plan (advanced, not required for v1)
437+
438+For v1, use Option A - keep it simple.
439+
440+---
441+
442+## What NOT to Do
443+
444+❌ **Don't ignore numbered lists** - Always create a plan
445+❌ **Don't forget to update status** - Track progress accurately
446+❌ **Don't create plans for single tasks** - Overkill for simple requests
447+❌ **Don't make up tasks** - Only create tasks the user actually requested
448+❌ **Don't delete plans without asking** - Always confirm with user
449+
450+---
451+
452+## Testing Your Understanding
453+
454+Read these user messages and decide if you should create a plan:
455+
456+| User Message | Create Plan? | Why |
457+|--------------|-------------|-----|
458+| "Write a function to sort an array" | No | Single task |
459+| "Fix these 3 bugs in the code" | Yes | Multi-step, but needs clarification |
460+| "1. Fix bug A, 2. Fix bug B, 3. Test both" | Yes | Explicit numbered list |
461+| "Hello, how are you?" | No | Not a task |
462+| "What's the capital of France?" | No | Question, not a task |
463+| "Set up the project: create dirs, init git, add README" | Yes | Comma-separated tasks |
464+| "I need the documentation updated and the tests fixed" | Yes | Multiple distinct tasks |
465+
466+---
467+
468+## Summary
469+
470+**Remember:** When the user gives you a list of things to do, **CREATE A PLAN**. Then **TRACK PROGRESS**. Then **REPORT STATUS** to the user.
471+
472+This makes you more reliable, transparent, and easier to work with.