🛟 Updated.
75ffe85 parent: d080878 modified
.tickets/issues/0004-do-we-need-mcp.yaml +1 -2 | @@ -6,9 +6,8 @@ author: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-15T22:38:45.129Z |
| 9 | -updatedAt: 2026-09-21T19:04:58.320Z | |
| 9 | +updatedAt: 2026-09-22T20:57:39.912Z | |
| 10 | 10 | labels: [] |
| 11 | -milestone: v0.04 | |
| 12 | 11 | body: | |
| 13 | 12 | I would say no - use a CLI |
| 14 | 13 | |
| @@ -6,9 +6,8 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-15T22:38:45.129Z | 8 | createdAt: 2026-09-15T22:38:45.129Z |
| 9 | -updatedAt: 2026-09-21T19:04:58.320Z | 9 | +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 CLI | 12 | I would say no - use a CLI |
| 14 | 13 | ||
modified
.tickets/issues/0025-tasks-plan-builtin.yaml +26 -5 | @@ -1,14 +1,35 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 25 |
| 3 | 3 | title: Tasks / Plan builtin |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | 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` | |
| 13 | 34 | tasks: [] |
| 14 | 35 | 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: 25 | 2 | id: 25 |
| 3 | title: Tasks / Plan builtin | 3 | title: Tasks / Plan builtin |
| 4 | -state: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-19T16:06:33.119Z | 8 | createdAt: 2026-09-19T16:06:33.119Z |
| 9 | -updatedAt: 2026-09-19T16:06:33.119Z | 9 | +updatedAt: 2026-09-22T20:54:40.140Z |
| 10 | -labels: [] | 10 | +labels: |
| 11 | -milestone: v0.04 | 11 | + - 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: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-21T04:00:56.971Z |
| 9 | -updatedAt: 2026-09-21T19:03:27.354Z | |
| 9 | +updatedAt: 2026-09-22T20:57:22.310Z | |
| 10 | 10 | labels: [] |
| 11 | -milestone: v0.04 | |
| 12 | 11 | body: | |
| 13 | 12 | ## Analysis (Claude, 2026-09-21 — not implemented) |
| 14 | 13 | |
| @@ -6,9 +6,8 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-21T04:00:56.971Z | 8 | createdAt: 2026-09-21T04:00:56.971Z |
| 9 | -updatedAt: 2026-09-21T19:03:27.354Z | 9 | +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: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-21T04:01:15.166Z |
| 9 | -updatedAt: 2026-09-21T19:04:28.691Z | |
| 9 | +updatedAt: 2026-09-22T20:56:55.345Z | |
| 10 | 10 | labels: [] |
| 11 | -milestone: v0.04 | |
| 12 | 11 | body: | |
| 13 | 12 | ## Analysis (Claude, 2026-09-21 — not implemented) |
| 14 | 13 | |
| @@ -6,9 +6,8 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-21T04:01:15.166Z | 8 | createdAt: 2026-09-21T04:01:15.166Z |
| 9 | -updatedAt: 2026-09-21T19:04:28.691Z | 9 | +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: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-21T04:47:19.089Z |
| 9 | -updatedAt: 2026-09-21T04:58:31.828Z | |
| 9 | +updatedAt: 2026-09-22T20:54:40.140Z | |
| 10 | 10 | labels: [] |
| 11 | -milestone: v0.04 | |
| 11 | +milestone: v0.0.4 | |
| 12 | 12 | epic: CLI |
| 13 | 13 | body: "" |
| 14 | 14 | tasks: [] |
| @@ -6,9 +6,9 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-21T04:47:19.089Z | 8 | createdAt: 2026-09-21T04:47:19.089Z |
| 9 | -updatedAt: 2026-09-21T04:58:31.828Z | 9 | +updatedAt: 2026-09-22T20:54:40.140Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | -milestone: v0.04 | 11 | +milestone: v0.0.4 |
| 12 | epic: CLI | 12 | 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: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-21T04:48:03.596Z |
| 9 | -updatedAt: 2026-09-21T04:58:54.762Z | |
| 9 | +updatedAt: 2026-09-22T20:56:40.361Z | |
| 10 | 10 | labels: [] |
| 11 | -milestone: v0.04 | |
| 12 | 11 | epic: Builtin tools |
| 13 | 12 | body: | |
| 14 | 13 | Should I use ACP ? I think yes |
| @@ -6,9 +6,8 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-21T04:48:03.596Z | 8 | createdAt: 2026-09-21T04:48:03.596Z |
| 9 | -updatedAt: 2026-09-21T04:58:54.762Z | 9 | +updatedAt: 2026-09-22T20:56:40.361Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | -milestone: v0.04 | ||
| 12 | epic: Builtin tools | 11 | epic: Builtin tools |
| 13 | body: | | 12 | body: | |
| 14 | Should I use ACP ? I think yes | 13 | 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 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 35 |
| 3 | 3 | title: Create SBX Template + Kit for Mini-Me |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-21T04:51:17.051Z |
| 9 | -updatedAt: 2026-09-21T19:06:52.839Z | |
| 9 | +updatedAt: 2026-09-22T20:55:22.352Z | |
| 10 | 10 | labels: [] |
| 11 | 11 | milestone: v0.0.3 |
| 12 | 12 | 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: 35 | 2 | id: 35 |
| 3 | title: Create SBX Template + Kit for Mini-Me | 3 | title: Create SBX Template + Kit for Mini-Me |
| 4 | -state: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-21T04:51:17.051Z | 8 | createdAt: 2026-09-21T04:51:17.051Z |
| 9 | -updatedAt: 2026-09-21T19:06:52.839Z | 9 | +updatedAt: 2026-09-22T20:55:22.352Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | milestone: v0.0.3 | 11 | milestone: v0.0.3 |
| 12 | body: | | 12 | body: | |
modified
.tickets/issues/0036-hooks.yaml +2 -2 | @@ -6,9 +6,9 @@ author: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-21T06:32:48.566Z |
| 9 | -updatedAt: 2026-09-21T06:32:48.566Z | |
| 9 | +updatedAt: 2026-09-22T20:54:40.140Z | |
| 10 | 10 | labels: [] |
| 11 | -milestone: v0.04 | |
| 11 | +milestone: v0.0.4 | |
| 12 | 12 | body: "" |
| 13 | 13 | tasks: [] |
| 14 | 14 | comments: [] |
| @@ -6,9 +6,9 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-21T06:32:48.566Z | 8 | createdAt: 2026-09-21T06:32:48.566Z |
| 9 | -updatedAt: 2026-09-21T06:32:48.566Z | 9 | +updatedAt: 2026-09-22T20:54:40.140Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | -milestone: v0.04 | 11 | +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: | ||
| 4 | 4 | state: closed |
| 5 | 5 | dueOn: 2026-09-18 |
| 6 | 6 | - title: v0.0.3 |
| 7 | - state: open | |
| 7 | + state: closed | |
| 8 | 8 | dueOn: 2026-09-30 |
| 9 | - - title: v0.04 | |
| 9 | + - title: v0.0.4 | |
| 10 | 10 | state: open |
| 11 | 11 | dueOn: 2026-10-15 |
| @@ -4,8 +4,8 @@ milestones: | |||
| 4 | state: closed | 4 | state: closed |
| 5 | dueOn: 2026-09-18 | 5 | dueOn: 2026-09-18 |
| 6 | - title: v0.0.3 | 6 | - title: v0.0.3 |
| 7 | - state: open | 7 | + state: closed |
| 8 | dueOn: 2026-09-30 | 8 | dueOn: 2026-09-30 |
| 9 | - - title: v0.04 | 9 | + - title: v0.0.4 |
| 10 | state: open | 10 | state: open |
| 11 | dueOn: 2026-10-15 | 11 | 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 | ||
| 27 | 27 | # The built-in file tools: read_file, write_file, edit_file. |
| 28 | 28 | 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 | 34 | # Language servers for the `lsp` tool (definition, references, hover, symbols, diagnostics, rename). |
| 31 | 35 | # The file extension picks the server. A server missing from the PATH is skipped with a warning. |
| 32 | 36 | # 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: true | 28 | 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 | ||
| 23 | 23 | # The built-in file tools: read_file, write_file, edit_file. |
| 24 | 24 | 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 | 30 | # Language servers for the `lsp` tool (definition, references, hover, symbols, diagnostics, rename). |
| 27 | 31 | # The file extension picks the server. A server missing from the PATH is skipped with a warning. |
| 28 | 32 | # 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: true | 24 | 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 | ||
| 34 | 34 | # The built-in file tools: read_file, write_file, edit_file. |
| 35 | 35 | 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 | 41 | # Language servers for the `lsp` tool (definition, references, hover, symbols, diagnostics, rename). |
| 38 | 42 | # The file extension picks the server. A server missing from the PATH is skipped with a warning at start-up; |
| 39 | 43 | # 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: true | 35 | 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 | ||
| 29 | 29 | # The built-in file tools: read_file, write_file, edit_file. |
| 30 | 30 | 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 | 36 | lsp: |
| 33 | 37 | servers: |
| 34 | 38 | - 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: true | 30 | 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@latest | 38 | - name: gopls # go install golang.org/x/tools/gopls@latest |
modified
demo/agent.dmr.yaml +4 -0 | @@ -29,6 +29,10 @@ bashTool: true | ||
| 29 | 29 | # The built-in file tools: read_file, write_file, edit_file. |
| 30 | 30 | 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 | 36 | lsp: |
| 33 | 37 | servers: |
| 34 | 38 | - 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: true | 30 | 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@latest | 38 | - name: gopls # go install golang.org/x/tools/gopls@latest |
modified
demo/agent.lsp.yaml +4 -0 | @@ -39,6 +39,10 @@ bashTool: true | ||
| 39 | 39 | # The built-in file tools: read_file, write_file, edit_file. |
| 40 | 40 | 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 | 46 | # Language servers for the `lsp` tool (definition, references, hover, symbols, diagnostics, rename). |
| 43 | 47 | # The file extension picks the server. A server missing from the PATH is skipped with a warning at start-up; |
| 44 | 48 | # 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: true | 40 | 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 ( | ||
| 21 | 21 | "mm/internal/skills" |
| 22 | 22 | "mm/internal/spinner" |
| 23 | 23 | "mm/internal/store" |
| 24 | + "mm/internal/tools" | |
| 24 | 25 | |
| 25 | 26 | "github.com/firebase/genkit/go/ai" |
| 26 | 27 | ) |
| @@ -443,6 +444,8 @@ func startNewSession(e *engine.Engine, d *detector.LoopDetector, system string, | ||
| 443 | 444 | forgotten := session.Forgotten(old) |
| 444 | 445 | e.ForgetInputTokens() |
| 445 | 446 | d.Reset() |
| 447 | + // Clear any active plans when starting a new session | |
| 448 | + tools.ClearAllPlans() | |
| 446 | 449 | dimln(fmt.Sprintf("🆕 new session: %d message(s) forgotten", forgotten)) |
| 447 | 450 | return session.Fresh(system) |
| 448 | 451 | } |
| @@ -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 { | ||
| 96 | 96 | // One binary, two set-ups: that is what lets the two be compared. |
| 97 | 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 | 103 | // Sampling: generation settings (OpenAI API keys; the plugin converts them.) |
| 100 | 104 | Sampling map[string]any `yaml:"sampling"` |
| 101 | 105 | |
| @@ -251,6 +255,7 @@ var Cfg = Config{ | ||
| 251 | 255 | SkillsDir: "skills", |
| 252 | 256 | BashTool: true, |
| 253 | 257 | EditTools: true, |
| 258 | + PlanTool: true, | |
| 254 | 259 | System: `You are a coding agent working in a terminal. |
| 255 | 260 | You have a "bash" tool to run shell commands. |
| 256 | 261 | 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 { | ||
| 233 | 233 | tools.EditFile(e.G, d)) |
| 234 | 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 | 240 | assembleLSP(e, d, &set) |
| 237 | 241 | return set |
| 238 | 242 | } |
| @@ -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 set | 241 | 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. | ||