# M4.1 — `mem skill draft` | Field | Value | |---|---| | Phase | M4 — Skills | | Size | M — 1–3 days | | Status | 🟡 In progress — `render_skill()` and `mem materialize` implemented | | Flags | — | | Spec | inlined below | | Blocks | M3.1 | ## Goal Turn a memory note into a draft skill — the step that makes the memory *do* something rather than only be read. ## Existing code (already implemented) **`crates/mem-core/src/lesson.rs`** already contains: | Function | What it does | Tests | |---|---|---| | `render_skill(tool, lessons)` | Generates `SKILL.md` with YAML frontmatter (`name`, `description`), per-lesson sections with `seen`/`last_seen`/`confidence`/`resolution`, and recurring-failure warnings | `skill_description_lists_symptoms_not_summary` | | `render_injection(hit, max_chars)` | Generates capped injection text for prompts | `injection_is_capped` | **`crates/mem-cli/src/lessons_cmd.rs`** already contains: | Command | What it does | |---|---| | `mem materialize` | Writes `skills/-failures/SKILL.md` per tool + `MEMORY.md` digest. Creates dirs, prints symlink instructions for Claude Code / pi. | ## What remains to complete this task The existing code generates skills from **command-level lessons** (`Lesson` struct). This task requires: 1. **Draft from L1/L2 memory notes** — `mem skill draft --from /` reads a GRU-Mem memory node, not a lesson 2. **LLM-assisted conversion** — prompt the model to convert descriptive memory into procedural instruction using the rubric 3. **`_drafts/` enforcement** — existing `mem materialize` writes directly to `skills/`; this task must write to `_drafts/` only 4. **`generated_from: ` provenance** — link back to the memory node 5. **Integration tests** — `tests/it_skill_draft.rs` (7 assertions) ## Files | Action | Path | |---|---| | **Exists** | `crates/mem-core/src/lesson.rs` — `render_skill()` | | **Exists** | `crates/mem-cli/src/lessons_cmd.rs` — `mem materialize` | | Modify | `crates/mem-cli/src/main.rs` — add `Commands::Skill { Draft }` subcommand | | Create | `tests/it_skill_draft.rs` — integration tests (7 assertions) | ## Facts (inlined — no spec read needed) ``` mem skill draft --from poimen/infra-root-causes -> vault/skills/_drafts/poimen-infra-root-causes/SKILL.md ``` **A skill is a projection, not a level.** L0/L1/L2 are descriptive — what happened. A skill is procedural — what to do next time. The gated loop does not produce it: "does this chunk contain evidence for Q" has no meaning when the output is an instruction. Format is free, because `SKILL.md` is YAML frontmatter plus markdown, which is exactly an Obsidian note. Verified against a real installed skill: ```yaml --- name: keyword-research description: 'Use when the user asks to "find keywords"... Not for X — use Y.' when_to_use: "Use when starting keyword research for a new page..." argument-hint: " [market/language]" --- ``` So the same file is a vault note and a loadable skill, with no conversion. **`description` is the whole game.** It is the trigger — a skill whose description does not match how the user actually phrases the request never fires, no matter how good the body is. Note the real example above spends half its description on *negative* routing ("Not for X — use Y"). Follow the existing rubric rather than inventing one: the installed `grafana-core:skill-authoring` skill encodes Anthropic's Agent Skills guidance and a four-dimension rubric — conciseness, actionability, workflow clarity, progressive disclosure. **Drafts land in `_drafts/` and are never auto-loaded.** A directory, not a frontmatter flag, because a directory cannot be accidentally globbed into `--skill`. ## Steps 1. `mem skill draft --from /` reads the L1 or L2 note. 2. Prompt the model to convert descriptive memory into procedural instruction, with the rubric's four dimensions in the prompt. 3. Emit frontmatter: `name`, `description`, `when_to_use`, plus `generated_from: ` and `generated_at`. 4. Write to `vault/skills/_drafts/-/SKILL.md`. 5. Refuse to write outside `_drafts/`. Promotion is a human `git mv`. 6. `--dry-run` prints without writing. ## Acceptance - Output parses as valid frontmatter + markdown. - `generated_from` resolves to a real node sha. - The file lands in `_drafts/` and nowhere else. - Promotion is not automated anywhere in the codebase. ## Verify **Harness:** seeded vault and log; scripted model client for determinism. **Integration test** — `tests/it_skill_draft.rs`: 1. `a1_valid_frontmatter` — parse the output; assert `name`, `description`, `when_to_use`, `generated_from` present and non-empty. 2. `a2_generated_from_resolves` — the sha exists in `memory_node`. 3. `a3_writes_only_to_drafts` — assert the path contains `_drafts/`; attempt to pass a path outside it and assert refusal. 4. `a4_no_promotion_path` — grep the workspace for any code writing to `vault/skills/` that is not under `_drafts/`; assert none. Promotion must be manual. 5. `a5_description_is_trigger_shaped` — assert `description` contains at least one phrasing cue (a quoted user phrase or "Use when"), matching the installed examples. 6. `a6_dry_run_writes_nothing` — assert no file created. 7. `a7_idempotent` — same input twice produces identical bytes apart from `generated_at`. **Command:** `cargo test --test it_skill_draft` **False pass:** - Asserting the file was written without asserting *where*. The entire safety property of this task is the location, and a draft written to `vault/skills/` is immediately loadable. - Accepting any non-empty `description`. A one-line restatement of the title never triggers, so the feature appears to work and the skill never fires — assertion 5 is a weak but real guard. ## Traps - Auto-promoting "when the draft looks good". That closes the loop this design deliberately leaves open, and there is no external verifier inside it. - Generating the body from L0 evidence. Skills are procedure distilled from synthesis; raw transcript produces a narrative, not an instruction. --- Background: [DESIGN.md](../DESIGN.md) — Skills, the procedural projection